mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-13 17:58:42 +03:00
Convert space indentation to tabs.
This commit is contained in:
+2
-1
@@ -31,7 +31,8 @@
|
||||
"singleQuote": false,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"endOfLine": "auto"
|
||||
"endOfLine": "auto",
|
||||
"useTabs": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
#188 - Spaces to tabs
|
||||
+2874
-2874
File diff suppressed because it is too large
Load Diff
Vendored
+96
-96
@@ -1,30 +1,30 @@
|
||||
import {
|
||||
CounterDocument,
|
||||
FolderDocument,
|
||||
GoalDocument,
|
||||
IIDXBPIData,
|
||||
IIDXEamusementScoreDocument,
|
||||
ImportDocument,
|
||||
TierlistParent,
|
||||
InviteCodeDocument,
|
||||
APITokenDocument,
|
||||
TierlistDataDocument,
|
||||
MilestoneDocument,
|
||||
NotificationDocument,
|
||||
FolderChartLookup,
|
||||
ImportTimingsDocument,
|
||||
PrivateUserDocument,
|
||||
ScoreDocument,
|
||||
KaiAuthDocument,
|
||||
SessionDocument,
|
||||
AnySongDocument,
|
||||
AnyChartDocument,
|
||||
UserGameStats,
|
||||
UserGoalDocument,
|
||||
PBScoreDocument,
|
||||
UserMilestoneDocument,
|
||||
BMSCourseDocument,
|
||||
ImportLockDocument,
|
||||
CounterDocument,
|
||||
FolderDocument,
|
||||
GoalDocument,
|
||||
IIDXBPIData,
|
||||
IIDXEamusementScoreDocument,
|
||||
ImportDocument,
|
||||
TierlistParent,
|
||||
InviteCodeDocument,
|
||||
APITokenDocument,
|
||||
TierlistDataDocument,
|
||||
MilestoneDocument,
|
||||
NotificationDocument,
|
||||
FolderChartLookup,
|
||||
ImportTimingsDocument,
|
||||
PrivateUserDocument,
|
||||
ScoreDocument,
|
||||
KaiAuthDocument,
|
||||
SessionDocument,
|
||||
AnySongDocument,
|
||||
AnyChartDocument,
|
||||
UserGameStats,
|
||||
UserGoalDocument,
|
||||
PBScoreDocument,
|
||||
UserMilestoneDocument,
|
||||
BMSCourseDocument,
|
||||
ImportLockDocument,
|
||||
} from "tachi-common";
|
||||
import monk from "monk";
|
||||
import { MONGO_CONNECTION_URL, MONGO_DATABASE_NAME } from "../../lib/setup/config";
|
||||
@@ -37,98 +37,98 @@ let dbName = MONGO_DATABASE_NAME;
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (process.env.NODE_ENV === "test") {
|
||||
if (process.env.TACHI_PARALLEL_TESTS) {
|
||||
dbName = `test-ephemeral-${process.pid}`;
|
||||
} else {
|
||||
dbName = `testingdb`;
|
||||
}
|
||||
if (process.env.TACHI_PARALLEL_TESTS) {
|
||||
dbName = `test-ephemeral-${process.pid}`;
|
||||
} else {
|
||||
dbName = `testingdb`;
|
||||
}
|
||||
}
|
||||
|
||||
let dbtime: [number, number] = [0, 0];
|
||||
/* istanbul ignore next */
|
||||
if (process.env.NODE_ENV !== "test") {
|
||||
logger.info(`Connecting to database ${MONGO_CONNECTION_URL}/${dbName}...`);
|
||||
dbtime = process.hrtime();
|
||||
logger.info(`Connecting to database ${MONGO_CONNECTION_URL}/${dbName}...`);
|
||||
dbtime = process.hrtime();
|
||||
}
|
||||
|
||||
export const monkDB = monk(`${MONGO_CONNECTION_URL}/${dbName}`);
|
||||
|
||||
/* istanbul ignore next */
|
||||
monkDB
|
||||
.then(() => {
|
||||
if (process.env.NODE_ENV !== "test") {
|
||||
const time = process.hrtime(dbtime);
|
||||
const elapsed = time[0] + time[1] / 1e6;
|
||||
logger.info(`Database connection successful: took ${elapsed}ms`);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.crit(err);
|
||||
process.exit(1);
|
||||
});
|
||||
.then(() => {
|
||||
if (process.env.NODE_ENV !== "test") {
|
||||
const time = process.hrtime(dbtime);
|
||||
const elapsed = time[0] + time[1] / 1e6;
|
||||
logger.info(`Database connection successful: took ${elapsed}ms`);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.crit(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
export async function CloseMongoConnection() {
|
||||
await monkDB.close();
|
||||
await monkDB.close();
|
||||
}
|
||||
|
||||
const songs = {
|
||||
bms: monkDB.get<AnySongDocument>(`songs-bms`),
|
||||
chunithm: monkDB.get<AnySongDocument>(`songs-chunithm`),
|
||||
ddr: monkDB.get<AnySongDocument>(`songs-ddr`),
|
||||
gitadora: monkDB.get<AnySongDocument>(`songs-gitadora`),
|
||||
iidx: monkDB.get<AnySongDocument>(`songs-iidx`),
|
||||
jubeat: monkDB.get<AnySongDocument>(`songs-jubeat`),
|
||||
maimai: monkDB.get<AnySongDocument>(`songs-maimai`),
|
||||
museca: monkDB.get<AnySongDocument>(`songs-museca`),
|
||||
popn: monkDB.get<AnySongDocument>(`songs-popn`),
|
||||
sdvx: monkDB.get<AnySongDocument>(`songs-sdvx`),
|
||||
usc: monkDB.get<AnySongDocument>(`songs-usc`),
|
||||
bms: monkDB.get<AnySongDocument>(`songs-bms`),
|
||||
chunithm: monkDB.get<AnySongDocument>(`songs-chunithm`),
|
||||
ddr: monkDB.get<AnySongDocument>(`songs-ddr`),
|
||||
gitadora: monkDB.get<AnySongDocument>(`songs-gitadora`),
|
||||
iidx: monkDB.get<AnySongDocument>(`songs-iidx`),
|
||||
jubeat: monkDB.get<AnySongDocument>(`songs-jubeat`),
|
||||
maimai: monkDB.get<AnySongDocument>(`songs-maimai`),
|
||||
museca: monkDB.get<AnySongDocument>(`songs-museca`),
|
||||
popn: monkDB.get<AnySongDocument>(`songs-popn`),
|
||||
sdvx: monkDB.get<AnySongDocument>(`songs-sdvx`),
|
||||
usc: monkDB.get<AnySongDocument>(`songs-usc`),
|
||||
};
|
||||
|
||||
const charts = {
|
||||
bms: monkDB.get<AnyChartDocument>(`charts-bms`),
|
||||
chunithm: monkDB.get<AnyChartDocument>(`charts-chunithm`),
|
||||
ddr: monkDB.get<AnyChartDocument>(`charts-ddr`),
|
||||
gitadora: monkDB.get<AnyChartDocument>(`charts-gitadora`),
|
||||
iidx: monkDB.get<AnyChartDocument>(`charts-iidx`),
|
||||
jubeat: monkDB.get<AnyChartDocument>(`charts-jubeat`),
|
||||
maimai: monkDB.get<AnyChartDocument>(`charts-maimai`),
|
||||
museca: monkDB.get<AnyChartDocument>(`charts-museca`),
|
||||
popn: monkDB.get<AnyChartDocument>(`charts-popn`),
|
||||
sdvx: monkDB.get<AnyChartDocument>(`charts-sdvx`),
|
||||
usc: monkDB.get<AnyChartDocument>(`charts-usc`),
|
||||
bms: monkDB.get<AnyChartDocument>(`charts-bms`),
|
||||
chunithm: monkDB.get<AnyChartDocument>(`charts-chunithm`),
|
||||
ddr: monkDB.get<AnyChartDocument>(`charts-ddr`),
|
||||
gitadora: monkDB.get<AnyChartDocument>(`charts-gitadora`),
|
||||
iidx: monkDB.get<AnyChartDocument>(`charts-iidx`),
|
||||
jubeat: monkDB.get<AnyChartDocument>(`charts-jubeat`),
|
||||
maimai: monkDB.get<AnyChartDocument>(`charts-maimai`),
|
||||
museca: monkDB.get<AnyChartDocument>(`charts-museca`),
|
||||
popn: monkDB.get<AnyChartDocument>(`charts-popn`),
|
||||
sdvx: monkDB.get<AnyChartDocument>(`charts-sdvx`),
|
||||
usc: monkDB.get<AnyChartDocument>(`charts-usc`),
|
||||
};
|
||||
|
||||
const db = {
|
||||
// i have to handwrite this out for TS... :(
|
||||
// dont worry, it was all macro'd
|
||||
songs,
|
||||
charts,
|
||||
scores: monkDB.get<ScoreDocument>("scores"),
|
||||
tierlists: monkDB.get<TierlistParent>("tierlists"),
|
||||
"tierlist-data": monkDB.get<TierlistDataDocument<never>>("tierlist-data"),
|
||||
"personal-bests": monkDB.get<PBScoreDocument>("personal-bests"),
|
||||
folders: monkDB.get<FolderDocument>("folders"),
|
||||
"folder-chart-lookup": monkDB.get<FolderChartLookup>("folder-chart-lookup"),
|
||||
goals: monkDB.get<GoalDocument>("goals"),
|
||||
"user-goals": monkDB.get<UserGoalDocument>("user-goals"),
|
||||
milestones: monkDB.get<MilestoneDocument>("milestones"),
|
||||
"user-milestones": monkDB.get<UserMilestoneDocument>("user-milestones"),
|
||||
users: monkDB.get<PrivateUserDocument>("users"),
|
||||
imports: monkDB.get<ImportDocument>("imports"),
|
||||
"import-timings": monkDB.get<ImportTimingsDocument>("import-timings"),
|
||||
notifications: monkDB.get<NotificationDocument>("notifications"),
|
||||
sessions: monkDB.get<SessionDocument>("sessions"),
|
||||
"iidx-bpi-data": monkDB.get<IIDXBPIData>("iidx-bpi-data"),
|
||||
invites: monkDB.get<InviteCodeDocument>("invites"),
|
||||
counters: monkDB.get<CounterDocument>("counters"),
|
||||
"iidx-eam-scores": monkDB.get<IIDXEamusementScoreDocument>("iidx-eam-scores"),
|
||||
"game-stats": monkDB.get<UserGameStats>("game-stats"),
|
||||
"kai-auth-tokens": monkDB.get<KaiAuthDocument>("kai-auth-tokens"),
|
||||
"bms-course-lookup": monkDB.get<BMSCourseDocument>("bms-course-lookup"),
|
||||
"api-tokens": monkDB.get<APITokenDocument>("api-tokens"),
|
||||
"orphan-scores": monkDB.get<OrphanScoreDocument>("orphan-scores"),
|
||||
"import-locks": monkDB.get<ImportLockDocument>("import-locks"),
|
||||
// i have to handwrite this out for TS... :(
|
||||
// dont worry, it was all macro'd
|
||||
songs,
|
||||
charts,
|
||||
scores: monkDB.get<ScoreDocument>("scores"),
|
||||
tierlists: monkDB.get<TierlistParent>("tierlists"),
|
||||
"tierlist-data": monkDB.get<TierlistDataDocument<never>>("tierlist-data"),
|
||||
"personal-bests": monkDB.get<PBScoreDocument>("personal-bests"),
|
||||
folders: monkDB.get<FolderDocument>("folders"),
|
||||
"folder-chart-lookup": monkDB.get<FolderChartLookup>("folder-chart-lookup"),
|
||||
goals: monkDB.get<GoalDocument>("goals"),
|
||||
"user-goals": monkDB.get<UserGoalDocument>("user-goals"),
|
||||
milestones: monkDB.get<MilestoneDocument>("milestones"),
|
||||
"user-milestones": monkDB.get<UserMilestoneDocument>("user-milestones"),
|
||||
users: monkDB.get<PrivateUserDocument>("users"),
|
||||
imports: monkDB.get<ImportDocument>("imports"),
|
||||
"import-timings": monkDB.get<ImportTimingsDocument>("import-timings"),
|
||||
notifications: monkDB.get<NotificationDocument>("notifications"),
|
||||
sessions: monkDB.get<SessionDocument>("sessions"),
|
||||
"iidx-bpi-data": monkDB.get<IIDXBPIData>("iidx-bpi-data"),
|
||||
invites: monkDB.get<InviteCodeDocument>("invites"),
|
||||
counters: monkDB.get<CounterDocument>("counters"),
|
||||
"iidx-eam-scores": monkDB.get<IIDXEamusementScoreDocument>("iidx-eam-scores"),
|
||||
"game-stats": monkDB.get<UserGameStats>("game-stats"),
|
||||
"kai-auth-tokens": monkDB.get<KaiAuthDocument>("kai-auth-tokens"),
|
||||
"bms-course-lookup": monkDB.get<BMSCourseDocument>("bms-course-lookup"),
|
||||
"api-tokens": monkDB.get<APITokenDocument>("api-tokens"),
|
||||
"orphan-scores": monkDB.get<OrphanScoreDocument>("orphan-scores"),
|
||||
"import-locks": monkDB.get<ImportLockDocument>("import-locks"),
|
||||
};
|
||||
|
||||
export default db;
|
||||
|
||||
Vendored
+103
-103
@@ -8,132 +8,132 @@ import { CONF_INFO } from "../../lib/setup/config";
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
interface Index {
|
||||
fields: Record<string, unknown>;
|
||||
options?: IndexOptions;
|
||||
fields: Record<string, unknown>;
|
||||
options?: IndexOptions;
|
||||
}
|
||||
|
||||
function index(fields: Record<string, unknown>, options?: IndexOptions) {
|
||||
return { fields, options };
|
||||
return { fields, options };
|
||||
}
|
||||
|
||||
const UNIQUE = { unique: true };
|
||||
|
||||
const staticIndexes: Partial<Record<ValidDatabases, Index[]>> = {
|
||||
scores: [index({ scoreID: 1 }, UNIQUE)],
|
||||
"personal-bests": [
|
||||
index({ chartID: 1, userID: 1 }, UNIQUE),
|
||||
index({ chartID: 1, "scoreData.percent": 1 }),
|
||||
],
|
||||
sessions: [
|
||||
// lol
|
||||
index({ timeStarted: 1, timeEnded: 1, userID: 1, game: 1, playtype: 1 }),
|
||||
index({ name: "text" }),
|
||||
],
|
||||
"game-stats": [index({ userID: 1, game: 1, playtype: 1 }, UNIQUE)],
|
||||
"folder-chart-lookup": [index({ chartID: 1, folderID: 1 }, UNIQUE)],
|
||||
"tierlist-data": [
|
||||
index({ tierlistDataID: 1 }, UNIQUE),
|
||||
index({ chartID: 1, tierlistID: 1 }),
|
||||
index({ chartID: 1, tierlistID: 1, type: 1 }),
|
||||
index({ chartID: 1, tierlistID: 1, type: 1, key: 1 }, UNIQUE),
|
||||
],
|
||||
goals: [index({ goalID: 1 }, UNIQUE)],
|
||||
"user-goals": [index({ goalID: 1, userID: 1 }, UNIQUE), index({ goalID: 1 })],
|
||||
milestones: [index({ milestoneID: 1 }, UNIQUE), index({ group: 1, game: 1, playtype: 1 })],
|
||||
"user-milestones": [
|
||||
index({ milestoneID: 1, userID: 1 }, UNIQUE),
|
||||
index({ userID: 1, game: 1, playtype: 1 }),
|
||||
],
|
||||
imports: [index({ importID: 1 }, UNIQUE)],
|
||||
"import-timings": [
|
||||
index({ importID: 1 }, UNIQUE),
|
||||
index({ timestamp: 1 }),
|
||||
index({ total: 1 }),
|
||||
],
|
||||
users: [index({ id: 1 }, UNIQUE)],
|
||||
tierlists: [
|
||||
index({ tierlistID: 1 }, UNIQUE),
|
||||
index({ game: 1, playtype: 1, isDefault: 1 }, UNIQUE),
|
||||
],
|
||||
folders: [
|
||||
index({ folderID: 1 }, UNIQUE),
|
||||
index({ game: 1, playtype: 1 }),
|
||||
index({ game: 1, playtype: 1, table: 1 }),
|
||||
index({ game: 1, playtype: 1, table: 1, tableIndex: 1 }),
|
||||
],
|
||||
"kai-auth-tokens": [index({ userID: 1, service: 1 }, UNIQUE)],
|
||||
"charts-iidx": [
|
||||
index(
|
||||
{ "data.arcChartID": 1 },
|
||||
{ unique: true, partialFilterExpression: { "data.arcChartID": { $type: "string" } } }
|
||||
),
|
||||
index({ "data.hashSHA256": 1 }),
|
||||
],
|
||||
"bms-course-lookup": [index({ md5sums: 1 }, UNIQUE)],
|
||||
"api-tokens": [index({ token: 1 }, UNIQUE)],
|
||||
scores: [index({ scoreID: 1 }, UNIQUE)],
|
||||
"personal-bests": [
|
||||
index({ chartID: 1, userID: 1 }, UNIQUE),
|
||||
index({ chartID: 1, "scoreData.percent": 1 }),
|
||||
],
|
||||
sessions: [
|
||||
// lol
|
||||
index({ timeStarted: 1, timeEnded: 1, userID: 1, game: 1, playtype: 1 }),
|
||||
index({ name: "text" }),
|
||||
],
|
||||
"game-stats": [index({ userID: 1, game: 1, playtype: 1 }, UNIQUE)],
|
||||
"folder-chart-lookup": [index({ chartID: 1, folderID: 1 }, UNIQUE)],
|
||||
"tierlist-data": [
|
||||
index({ tierlistDataID: 1 }, UNIQUE),
|
||||
index({ chartID: 1, tierlistID: 1 }),
|
||||
index({ chartID: 1, tierlistID: 1, type: 1 }),
|
||||
index({ chartID: 1, tierlistID: 1, type: 1, key: 1 }, UNIQUE),
|
||||
],
|
||||
goals: [index({ goalID: 1 }, UNIQUE)],
|
||||
"user-goals": [index({ goalID: 1, userID: 1 }, UNIQUE), index({ goalID: 1 })],
|
||||
milestones: [index({ milestoneID: 1 }, UNIQUE), index({ group: 1, game: 1, playtype: 1 })],
|
||||
"user-milestones": [
|
||||
index({ milestoneID: 1, userID: 1 }, UNIQUE),
|
||||
index({ userID: 1, game: 1, playtype: 1 }),
|
||||
],
|
||||
imports: [index({ importID: 1 }, UNIQUE)],
|
||||
"import-timings": [
|
||||
index({ importID: 1 }, UNIQUE),
|
||||
index({ timestamp: 1 }),
|
||||
index({ total: 1 }),
|
||||
],
|
||||
users: [index({ id: 1 }, UNIQUE)],
|
||||
tierlists: [
|
||||
index({ tierlistID: 1 }, UNIQUE),
|
||||
index({ game: 1, playtype: 1, isDefault: 1 }, UNIQUE),
|
||||
],
|
||||
folders: [
|
||||
index({ folderID: 1 }, UNIQUE),
|
||||
index({ game: 1, playtype: 1 }),
|
||||
index({ game: 1, playtype: 1, table: 1 }),
|
||||
index({ game: 1, playtype: 1, table: 1, tableIndex: 1 }),
|
||||
],
|
||||
"kai-auth-tokens": [index({ userID: 1, service: 1 }, UNIQUE)],
|
||||
"charts-iidx": [
|
||||
index(
|
||||
{ "data.arcChartID": 1 },
|
||||
{ unique: true, partialFilterExpression: { "data.arcChartID": { $type: "string" } } }
|
||||
),
|
||||
index({ "data.hashSHA256": 1 }),
|
||||
],
|
||||
"bms-course-lookup": [index({ md5sums: 1 }, UNIQUE)],
|
||||
"api-tokens": [index({ token: 1 }, UNIQUE)],
|
||||
};
|
||||
|
||||
const indexes: Partial<Record<ValidDatabases, Index[]>> = staticIndexes;
|
||||
|
||||
for (const game of CONF_INFO.SUPPORTED_GAMES) {
|
||||
if (indexes[`charts-${game}` as ValidDatabases]) {
|
||||
indexes[`charts-${game}` as ValidDatabases]!.push(
|
||||
index({ chartID: 1 }, UNIQUE),
|
||||
index(
|
||||
{ songID: 1, difficulty: 1, playtype: 1, isPrimary: 1 },
|
||||
{ unique: true, partialFilterExpression: { isPrimary: { $eq: true } } }
|
||||
)
|
||||
);
|
||||
} else {
|
||||
indexes[`charts-${game}` as ValidDatabases] = [
|
||||
index({ chartID: 1 }, UNIQUE),
|
||||
index({ songID: 1, difficulty: 1, playtype: 1, isPrimary: 1 }, UNIQUE),
|
||||
];
|
||||
}
|
||||
if (indexes[`charts-${game}` as ValidDatabases]) {
|
||||
indexes[`charts-${game}` as ValidDatabases]!.push(
|
||||
index({ chartID: 1 }, UNIQUE),
|
||||
index(
|
||||
{ songID: 1, difficulty: 1, playtype: 1, isPrimary: 1 },
|
||||
{ unique: true, partialFilterExpression: { isPrimary: { $eq: true } } }
|
||||
)
|
||||
);
|
||||
} else {
|
||||
indexes[`charts-${game}` as ValidDatabases] = [
|
||||
index({ chartID: 1 }, UNIQUE),
|
||||
index({ songID: 1, difficulty: 1, playtype: 1, isPrimary: 1 }, UNIQUE),
|
||||
];
|
||||
}
|
||||
|
||||
if (indexes[`songs-${game}` as ValidDatabases]) {
|
||||
indexes[`songs-${game}` as ValidDatabases]!.push(
|
||||
index({ id: 1 }, UNIQUE),
|
||||
index({ title: "text", artist: "text", "alt-titles": "text", "search-titles": "text" })
|
||||
);
|
||||
} else {
|
||||
indexes[`songs-${game}` as ValidDatabases] = [
|
||||
index({ id: 1 }, UNIQUE),
|
||||
index({ title: 1 }),
|
||||
index({ title: "text", artist: "text", "alt-titles": "text", "search-titles": "text" }),
|
||||
];
|
||||
}
|
||||
if (indexes[`songs-${game}` as ValidDatabases]) {
|
||||
indexes[`songs-${game}` as ValidDatabases]!.push(
|
||||
index({ id: 1 }, UNIQUE),
|
||||
index({ title: "text", artist: "text", "alt-titles": "text", "search-titles": "text" })
|
||||
);
|
||||
} else {
|
||||
indexes[`songs-${game}` as ValidDatabases] = [
|
||||
index({ id: 1 }, UNIQUE),
|
||||
index({ title: 1 }),
|
||||
index({ title: "text", artist: "text", "alt-titles": "text", "search-titles": "text" }),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export async function SetIndexes(mongoURL: string, reset: boolean) {
|
||||
const db = monk(mongoURL);
|
||||
const db = monk(mongoURL);
|
||||
|
||||
logger.debug(`Starting indexing for ${mongoURL}...`);
|
||||
logger.debug(`Starting indexing for ${mongoURL}...`);
|
||||
|
||||
const collections = (await db.listCollections()).map((e) => e.name);
|
||||
const collections = (await db.listCollections()).map((e) => e.name);
|
||||
|
||||
for (const collection in indexes) {
|
||||
if (!collections.includes(collection)) {
|
||||
// this creates a collection, i cant find the createCollection
|
||||
// call.
|
||||
await db.get(collection).insert({});
|
||||
await db.get(collection).remove({});
|
||||
}
|
||||
for (const collection in indexes) {
|
||||
if (!collections.includes(collection)) {
|
||||
// this creates a collection, i cant find the createCollection
|
||||
// call.
|
||||
await db.get(collection).insert({});
|
||||
await db.get(collection).remove({});
|
||||
}
|
||||
|
||||
if (reset) {
|
||||
await db.get(collection).dropIndexes();
|
||||
logger.debug(`Reset ${collection}.`);
|
||||
}
|
||||
if (reset) {
|
||||
await db.get(collection).dropIndexes();
|
||||
logger.debug(`Reset ${collection}.`);
|
||||
}
|
||||
|
||||
// @ts-expect-error dru(n)kts
|
||||
for (const index of indexes[collection]) {
|
||||
const r = await db.get(collection).createIndex(index.fields, index.options);
|
||||
// @ts-expect-error dru(n)kts
|
||||
for (const index of indexes[collection]) {
|
||||
const r = await db.get(collection).createIndex(index.fields, index.options);
|
||||
|
||||
logger.debug(r);
|
||||
}
|
||||
}
|
||||
logger.debug(r);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Done.");
|
||||
logger.debug("Done.");
|
||||
|
||||
await db.close();
|
||||
await db.close();
|
||||
}
|
||||
|
||||
Vendored
+352
-352
@@ -1,12 +1,12 @@
|
||||
import deepmerge from "deepmerge";
|
||||
import { Game, Playtypes } from "tachi-common";
|
||||
import {
|
||||
grades,
|
||||
importTypes,
|
||||
lamps,
|
||||
validHitData,
|
||||
validDifficulties,
|
||||
validPlaytypes,
|
||||
grades,
|
||||
importTypes,
|
||||
lamps,
|
||||
validHitData,
|
||||
validDifficulties,
|
||||
validPlaytypes,
|
||||
} from "tachi-common/js/config";
|
||||
import p, { PrudenceSchema, ValidSchemaValue } from "prudence";
|
||||
import { CONF_INFO } from "../../lib/setup/config";
|
||||
@@ -14,99 +14,99 @@ import { CONF_INFO } from "../../lib/setup/config";
|
||||
const LAZY_EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/u;
|
||||
|
||||
export const PRUDENCE_PUBLIC_USER: PrudenceSchema = {
|
||||
_id: p.any,
|
||||
username: p.regex(/^[a-zA-Z_-][a-zA-Z0-9_-]{2,20}$/u),
|
||||
usernameLowercase: (self, parent) => self === (parent!.username as string).toLowerCase(),
|
||||
id: p.isPositiveInteger,
|
||||
settings: {
|
||||
nsfwSplashes: "boolean",
|
||||
invisible: "boolean",
|
||||
},
|
||||
friends: [p.isPositiveInteger],
|
||||
socialMedia: {
|
||||
discord: "*?string",
|
||||
twitter: "*?string",
|
||||
github: "*?string",
|
||||
steam: "*?string",
|
||||
youtube: "*?string",
|
||||
twitch: "*?string",
|
||||
},
|
||||
about: p.isBoundedString(0, 4000),
|
||||
customPfp: "boolean",
|
||||
customBanner: "boolean",
|
||||
permissions: {
|
||||
admin: "*boolean",
|
||||
},
|
||||
clan: p.nullable(p.isBoundedString(1, 4)),
|
||||
lastSeen: p.nullable(p.isPositiveInteger),
|
||||
_id: p.any,
|
||||
username: p.regex(/^[a-zA-Z_-][a-zA-Z0-9_-]{2,20}$/u),
|
||||
usernameLowercase: (self, parent) => self === (parent!.username as string).toLowerCase(),
|
||||
id: p.isPositiveInteger,
|
||||
settings: {
|
||||
nsfwSplashes: "boolean",
|
||||
invisible: "boolean",
|
||||
},
|
||||
friends: [p.isPositiveInteger],
|
||||
socialMedia: {
|
||||
discord: "*?string",
|
||||
twitter: "*?string",
|
||||
github: "*?string",
|
||||
steam: "*?string",
|
||||
youtube: "*?string",
|
||||
twitch: "*?string",
|
||||
},
|
||||
about: p.isBoundedString(0, 4000),
|
||||
customPfp: "boolean",
|
||||
customBanner: "boolean",
|
||||
permissions: {
|
||||
admin: "*boolean",
|
||||
},
|
||||
clan: p.nullable(p.isBoundedString(1, 4)),
|
||||
lastSeen: p.nullable(p.isPositiveInteger),
|
||||
};
|
||||
|
||||
export const PRUDENCE_PRIVATE_USER = Object.assign(
|
||||
{
|
||||
password: "string", // could be a tighter fit related to bcrypt?
|
||||
email: p.regex(LAZY_EMAIL_REGEX),
|
||||
},
|
||||
PRUDENCE_PUBLIC_USER
|
||||
{
|
||||
password: "string", // could be a tighter fit related to bcrypt?
|
||||
email: p.regex(LAZY_EMAIL_REGEX),
|
||||
},
|
||||
PRUDENCE_PUBLIC_USER
|
||||
);
|
||||
|
||||
export const PRUDENCE_IIDX_BPI_DATA = {
|
||||
coef: p.nullable(p.isPositiveNonZero),
|
||||
kavg: p.isPositiveNonZeroInteger,
|
||||
wr: p.isPositiveNonZeroInteger,
|
||||
chartID: "string",
|
||||
kesd: p.isPositiveNonZero,
|
||||
coef: p.nullable(p.isPositiveNonZero),
|
||||
kavg: p.isPositiveNonZeroInteger,
|
||||
wr: p.isPositiveNonZeroInteger,
|
||||
chartID: "string",
|
||||
kesd: p.isPositiveNonZero,
|
||||
};
|
||||
|
||||
export const PRUDENCE_COUNTER = {
|
||||
counterName: "string",
|
||||
value: p.isPositiveNonZeroInteger, // is nonzero?
|
||||
counterName: "string",
|
||||
value: p.isPositiveNonZeroInteger, // is nonzero?
|
||||
};
|
||||
|
||||
export const PR_SCORE_GENERIC = {
|
||||
service: p.isBoundedString(3, 64),
|
||||
game: "string",
|
||||
playtype: "string",
|
||||
difficulty: "string",
|
||||
userID: p.isPositiveNonZeroInteger,
|
||||
scoreData: {
|
||||
score: p.isPositive,
|
||||
percent: p.isBetween(0, 100), // could be overrode!
|
||||
lamp: "string",
|
||||
grade: "string",
|
||||
lampIndex: p.isPositiveInteger,
|
||||
gradeIndex: p.isPositiveInteger,
|
||||
hitData: {},
|
||||
hitMeta: {},
|
||||
},
|
||||
scoreMeta: {},
|
||||
calculatedData: {
|
||||
rating: p.isPositive,
|
||||
lampRating: p.isPositive,
|
||||
gameSpecific: {},
|
||||
// ranking: p.nullable(p.isPositiveNonZeroInteger),
|
||||
// outOf: p.nullable(
|
||||
// p.and(
|
||||
// p.isPositiveNonZeroInteger,
|
||||
// (self, parent: Record<string, unknown>) =>
|
||||
// (parent.ranking as number) <= (self as number)
|
||||
// )
|
||||
// ),
|
||||
},
|
||||
timeAchieved: p.nullable(p.isPositive),
|
||||
songID: p.isInteger,
|
||||
chartID: (self: unknown) => typeof self === "string" && self.length === 40,
|
||||
highlight: "boolean",
|
||||
isPrimary: "boolean",
|
||||
comment: p.nullable(p.isBoundedString(1, 240)),
|
||||
timeAdded: p.isPositive,
|
||||
scoreID: "string", // temp
|
||||
importType: p.nullable(p.isIn(importTypes)),
|
||||
service: p.isBoundedString(3, 64),
|
||||
game: "string",
|
||||
playtype: "string",
|
||||
difficulty: "string",
|
||||
userID: p.isPositiveNonZeroInteger,
|
||||
scoreData: {
|
||||
score: p.isPositive,
|
||||
percent: p.isBetween(0, 100), // could be overrode!
|
||||
lamp: "string",
|
||||
grade: "string",
|
||||
lampIndex: p.isPositiveInteger,
|
||||
gradeIndex: p.isPositiveInteger,
|
||||
hitData: {},
|
||||
hitMeta: {},
|
||||
},
|
||||
scoreMeta: {},
|
||||
calculatedData: {
|
||||
rating: p.isPositive,
|
||||
lampRating: p.isPositive,
|
||||
gameSpecific: {},
|
||||
// ranking: p.nullable(p.isPositiveNonZeroInteger),
|
||||
// outOf: p.nullable(
|
||||
// p.and(
|
||||
// p.isPositiveNonZeroInteger,
|
||||
// (self, parent: Record<string, unknown>) =>
|
||||
// (parent.ranking as number) <= (self as number)
|
||||
// )
|
||||
// ),
|
||||
},
|
||||
timeAchieved: p.nullable(p.isPositive),
|
||||
songID: p.isInteger,
|
||||
chartID: (self: unknown) => typeof self === "string" && self.length === 40,
|
||||
highlight: "boolean",
|
||||
isPrimary: "boolean",
|
||||
comment: p.nullable(p.isBoundedString(1, 240)),
|
||||
timeAdded: p.isPositive,
|
||||
scoreID: "string", // temp
|
||||
importType: p.nullable(p.isIn(importTypes)),
|
||||
};
|
||||
|
||||
type ScoreSchemas = {
|
||||
[G in Game]: {
|
||||
[P in Playtypes[G]]: PrudenceSchema;
|
||||
};
|
||||
[G in Game]: {
|
||||
[P in Playtypes[G]]: PrudenceSchema;
|
||||
};
|
||||
};
|
||||
|
||||
const optionalPositiveInt = p.optional(p.isPositiveInteger);
|
||||
@@ -116,315 +116,315 @@ 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),
|
||||
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 PR_SCORE_IIDX_SP: PrudenceSchema = CreatePRScore(
|
||||
"iidx",
|
||||
"SP",
|
||||
{
|
||||
bp: nullableAndOptional(p.isPositiveInteger),
|
||||
gauge: nullableAndOptional(p.isBetween(0, 100)),
|
||||
gaugeHistory: p.optional([nullableAndOptional(p.isBetween(0, 100))]),
|
||||
comboBreak: optionalPositiveInt,
|
||||
deadMeasure: nullableAndOptional(p.isPositiveInteger),
|
||||
deadNote: nullableAndOptional(p.isPositiveInteger),
|
||||
},
|
||||
{
|
||||
random: nullableAndOptional(p.isIn(["NONRAN", "RANDOM", "R-RANDOM", "S-RANDOM", "MIRROR"])),
|
||||
gauge: nullableAndOptional(p.isIn(["ASSISTED EASY", "EASY", "NORMAL", "HARD", "EX-HARD"])),
|
||||
assist: nullableAndOptional(
|
||||
p.isIn(["NO ASSIST", "AUTO SCRATCH", "LEGACY NOTE", "ASCR + LEGACY"])
|
||||
),
|
||||
range: nullableAndOptional(
|
||||
p.isIn(["NONE", "SUDDEN+", "HIDDEN+", "SUD+ HID+", "LIFT", "LIFT SUD+"])
|
||||
),
|
||||
pacemaker: "*?string", // lazy
|
||||
pacemakerName: "*?string",
|
||||
pacemakerTarget: nullableAndOptional(p.isPositiveInteger),
|
||||
},
|
||||
{
|
||||
BPI: "?number",
|
||||
"K%": p.nullable(p.isBetween(0, 100)),
|
||||
}
|
||||
"iidx",
|
||||
"SP",
|
||||
{
|
||||
bp: nullableAndOptional(p.isPositiveInteger),
|
||||
gauge: nullableAndOptional(p.isBetween(0, 100)),
|
||||
gaugeHistory: p.optional([nullableAndOptional(p.isBetween(0, 100))]),
|
||||
comboBreak: optionalPositiveInt,
|
||||
deadMeasure: nullableAndOptional(p.isPositiveInteger),
|
||||
deadNote: nullableAndOptional(p.isPositiveInteger),
|
||||
},
|
||||
{
|
||||
random: nullableAndOptional(p.isIn(["NONRAN", "RANDOM", "R-RANDOM", "S-RANDOM", "MIRROR"])),
|
||||
gauge: nullableAndOptional(p.isIn(["ASSISTED EASY", "EASY", "NORMAL", "HARD", "EX-HARD"])),
|
||||
assist: nullableAndOptional(
|
||||
p.isIn(["NO ASSIST", "AUTO SCRATCH", "LEGACY NOTE", "ASCR + LEGACY"])
|
||||
),
|
||||
range: nullableAndOptional(
|
||||
p.isIn(["NONE", "SUDDEN+", "HIDDEN+", "SUD+ HID+", "LIFT", "LIFT SUD+"])
|
||||
),
|
||||
pacemaker: "*?string", // lazy
|
||||
pacemakerName: "*?string",
|
||||
pacemakerTarget: nullableAndOptional(p.isPositiveInteger),
|
||||
},
|
||||
{
|
||||
BPI: "?number",
|
||||
"K%": p.nullable(p.isBetween(0, 100)),
|
||||
}
|
||||
);
|
||||
|
||||
const PR_SCORE_BMS_7K: PrudenceSchema = CreatePRScore(
|
||||
"bms",
|
||||
"7K",
|
||||
{
|
||||
gauge: nullableAndOptional(p.isBetween(0, 100)),
|
||||
bp: nullableAndOptionalPosInt,
|
||||
diedAt: nullableAndOptionalPosInt,
|
||||
lpr: nullableAndOptionalPosInt,
|
||||
lbd: nullableAndOptionalPosInt,
|
||||
lgd: nullableAndOptionalPosInt,
|
||||
lgr: nullableAndOptionalPosInt,
|
||||
lpg: nullableAndOptionalPosInt,
|
||||
epr: nullableAndOptionalPosInt,
|
||||
ebd: nullableAndOptionalPosInt,
|
||||
egd: nullableAndOptionalPosInt,
|
||||
egr: nullableAndOptionalPosInt,
|
||||
epg: nullableAndOptionalPosInt,
|
||||
},
|
||||
{
|
||||
random: nullableAndOptional(p.isIn(["NONRAN", "RANDOM", "R-RANDOM", "S-RANDOM", "MIRROR"])),
|
||||
inputDevice: nullableAndOptional(p.isIn(["KEYBOARD", "BM_CONTROLLER", "MIDI"])),
|
||||
}
|
||||
"bms",
|
||||
"7K",
|
||||
{
|
||||
gauge: nullableAndOptional(p.isBetween(0, 100)),
|
||||
bp: nullableAndOptionalPosInt,
|
||||
diedAt: nullableAndOptionalPosInt,
|
||||
lpr: nullableAndOptionalPosInt,
|
||||
lbd: nullableAndOptionalPosInt,
|
||||
lgd: nullableAndOptionalPosInt,
|
||||
lgr: nullableAndOptionalPosInt,
|
||||
lpg: nullableAndOptionalPosInt,
|
||||
epr: nullableAndOptionalPosInt,
|
||||
ebd: nullableAndOptionalPosInt,
|
||||
egd: nullableAndOptionalPosInt,
|
||||
egr: nullableAndOptionalPosInt,
|
||||
epg: nullableAndOptionalPosInt,
|
||||
},
|
||||
{
|
||||
random: nullableAndOptional(p.isIn(["NONRAN", "RANDOM", "R-RANDOM", "S-RANDOM", "MIRROR"])),
|
||||
inputDevice: nullableAndOptional(p.isIn(["KEYBOARD", "BM_CONTROLLER", "MIDI"])),
|
||||
}
|
||||
);
|
||||
|
||||
function CreatePRScore<G extends Game>(
|
||||
game: G,
|
||||
playtype: Playtypes[G],
|
||||
mergeHitMeta: PrudenceSchema = {},
|
||||
mergeScoreMeta: PrudenceSchema = {},
|
||||
mergeGameSpecific: PrudenceSchema = {}
|
||||
game: G,
|
||||
playtype: Playtypes[G],
|
||||
mergeHitMeta: PrudenceSchema = {},
|
||||
mergeScoreMeta: PrudenceSchema = {},
|
||||
mergeGameSpecific: PrudenceSchema = {}
|
||||
) {
|
||||
return deepmerge(PR_SCORE_GENERIC, {
|
||||
game: p.equalTo(game),
|
||||
playtype: p.equalTo(playtype),
|
||||
scoreData: CreateGameScoreData(game, mergeHitMeta),
|
||||
scoreMeta: mergeScoreMeta,
|
||||
calculatedData: {
|
||||
gameSpecific: mergeGameSpecific,
|
||||
},
|
||||
});
|
||||
return deepmerge(PR_SCORE_GENERIC, {
|
||||
game: p.equalTo(game),
|
||||
playtype: p.equalTo(playtype),
|
||||
scoreData: CreateGameScoreData(game, mergeHitMeta),
|
||||
scoreMeta: mergeScoreMeta,
|
||||
calculatedData: {
|
||||
gameSpecific: mergeGameSpecific,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function DoublePlayTwinRandomTuple(self: unknown) {
|
||||
if (!Array.isArray(self)) {
|
||||
return "Expected an array.";
|
||||
}
|
||||
if (!Array.isArray(self)) {
|
||||
return "Expected an array.";
|
||||
}
|
||||
|
||||
if (self.length !== 2) {
|
||||
return "Expected exactly 2 elements.";
|
||||
}
|
||||
if (self.length !== 2) {
|
||||
return "Expected exactly 2 elements.";
|
||||
}
|
||||
|
||||
const ls = p.isIn(["NONRAN", "RANDOM", "R-RANDOM", "S-RANDOM", "MIRROR"])(self[0]);
|
||||
const ls = p.isIn(["NONRAN", "RANDOM", "R-RANDOM", "S-RANDOM", "MIRROR"])(self[0]);
|
||||
|
||||
if (ls !== true) {
|
||||
return ls;
|
||||
}
|
||||
if (ls !== true) {
|
||||
return ls;
|
||||
}
|
||||
|
||||
const rs = p.isIn(["NONRAN", "RANDOM", "R-RANDOM", "S-RANDOM", "MIRROR"])(self[1]);
|
||||
const rs = p.isIn(["NONRAN", "RANDOM", "R-RANDOM", "S-RANDOM", "MIRROR"])(self[1]);
|
||||
|
||||
return rs;
|
||||
return rs;
|
||||
}
|
||||
|
||||
export const PRUDENCE_SCORE_SCHEMAS: ScoreSchemas = {
|
||||
iidx: {
|
||||
SP: PR_SCORE_IIDX_SP,
|
||||
DP: deepmerge(PR_SCORE_IIDX_SP, {
|
||||
playtype: p.equalTo("DP"),
|
||||
scoreMeta: {
|
||||
random: nullableAndOptional(DoublePlayTwinRandomTuple),
|
||||
},
|
||||
calculatedData: {
|
||||
gameSpecific: {
|
||||
"K%": "undefined", // remove with override
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
sdvx: {
|
||||
Single: CreatePRScore(
|
||||
"sdvx",
|
||||
"Single",
|
||||
{
|
||||
gauge: p.optional(p.isBetween(0, 100)),
|
||||
btnRate: p.optional(p.isBetween(0, 100)),
|
||||
holdRate: p.optional(p.isBetween(0, 100)),
|
||||
laserRate: p.optional(p.isBetween(0, 100)),
|
||||
},
|
||||
{},
|
||||
{
|
||||
VF4: p.nullable(p.isPositiveInteger),
|
||||
VF5: p.nullable(p.isBetween(0, 0.5)), // hack
|
||||
}
|
||||
),
|
||||
},
|
||||
bms: {
|
||||
"7K": PR_SCORE_BMS_7K,
|
||||
"14K": deepmerge(PR_SCORE_BMS_7K, {
|
||||
playtype: p.equalTo("14K"),
|
||||
scoreMeta: {
|
||||
random: p.optional(DoublePlayTwinRandomTuple),
|
||||
},
|
||||
}),
|
||||
},
|
||||
chunithm: {
|
||||
Single: deepmerge(CreatePRScore("chunithm", "Single"), {
|
||||
scoreData: { percent: p.isBetween(0, 101) },
|
||||
}),
|
||||
},
|
||||
ddr: {
|
||||
SP: CreatePRScore(
|
||||
"ddr",
|
||||
"SP",
|
||||
{},
|
||||
{},
|
||||
{
|
||||
MFCP: p.nullable(p.isBoundedInteger(1, 25)),
|
||||
}
|
||||
),
|
||||
DP: CreatePRScore(
|
||||
"ddr",
|
||||
"DP",
|
||||
{},
|
||||
{},
|
||||
{
|
||||
MFCP: p.nullable(p.isBoundedInteger(1, 25)),
|
||||
}
|
||||
),
|
||||
},
|
||||
gitadora: {
|
||||
Gita: CreatePRScore("gitadora", "Gita"),
|
||||
Dora: CreatePRScore("gitadora", "Dora"),
|
||||
},
|
||||
jubeat: {
|
||||
Single: CreatePRScore("jubeat", "Single"),
|
||||
},
|
||||
maimai: {
|
||||
Single: deepmerge(CreatePRScore("maimai", "Single"), {
|
||||
scoreData: { percent: p.isBetween(0, 150) },
|
||||
}),
|
||||
},
|
||||
museca: {
|
||||
Single: CreatePRScore("museca", "Single"),
|
||||
},
|
||||
popn: {
|
||||
"9B": CreatePRScore("popn", "9B", { gauge: p.optional(p.isBetween(0, 100)) }),
|
||||
},
|
||||
usc: {
|
||||
Single: CreatePRScore("usc", "Single", { gauge: p.optional(p.isBetween(0, 100)) }),
|
||||
},
|
||||
iidx: {
|
||||
SP: PR_SCORE_IIDX_SP,
|
||||
DP: deepmerge(PR_SCORE_IIDX_SP, {
|
||||
playtype: p.equalTo("DP"),
|
||||
scoreMeta: {
|
||||
random: nullableAndOptional(DoublePlayTwinRandomTuple),
|
||||
},
|
||||
calculatedData: {
|
||||
gameSpecific: {
|
||||
"K%": "undefined", // remove with override
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
sdvx: {
|
||||
Single: CreatePRScore(
|
||||
"sdvx",
|
||||
"Single",
|
||||
{
|
||||
gauge: p.optional(p.isBetween(0, 100)),
|
||||
btnRate: p.optional(p.isBetween(0, 100)),
|
||||
holdRate: p.optional(p.isBetween(0, 100)),
|
||||
laserRate: p.optional(p.isBetween(0, 100)),
|
||||
},
|
||||
{},
|
||||
{
|
||||
VF4: p.nullable(p.isPositiveInteger),
|
||||
VF5: p.nullable(p.isBetween(0, 0.5)), // hack
|
||||
}
|
||||
),
|
||||
},
|
||||
bms: {
|
||||
"7K": PR_SCORE_BMS_7K,
|
||||
"14K": deepmerge(PR_SCORE_BMS_7K, {
|
||||
playtype: p.equalTo("14K"),
|
||||
scoreMeta: {
|
||||
random: p.optional(DoublePlayTwinRandomTuple),
|
||||
},
|
||||
}),
|
||||
},
|
||||
chunithm: {
|
||||
Single: deepmerge(CreatePRScore("chunithm", "Single"), {
|
||||
scoreData: { percent: p.isBetween(0, 101) },
|
||||
}),
|
||||
},
|
||||
ddr: {
|
||||
SP: CreatePRScore(
|
||||
"ddr",
|
||||
"SP",
|
||||
{},
|
||||
{},
|
||||
{
|
||||
MFCP: p.nullable(p.isBoundedInteger(1, 25)),
|
||||
}
|
||||
),
|
||||
DP: CreatePRScore(
|
||||
"ddr",
|
||||
"DP",
|
||||
{},
|
||||
{},
|
||||
{
|
||||
MFCP: p.nullable(p.isBoundedInteger(1, 25)),
|
||||
}
|
||||
),
|
||||
},
|
||||
gitadora: {
|
||||
Gita: CreatePRScore("gitadora", "Gita"),
|
||||
Dora: CreatePRScore("gitadora", "Dora"),
|
||||
},
|
||||
jubeat: {
|
||||
Single: CreatePRScore("jubeat", "Single"),
|
||||
},
|
||||
maimai: {
|
||||
Single: deepmerge(CreatePRScore("maimai", "Single"), {
|
||||
scoreData: { percent: p.isBetween(0, 150) },
|
||||
}),
|
||||
},
|
||||
museca: {
|
||||
Single: CreatePRScore("museca", "Single"),
|
||||
},
|
||||
popn: {
|
||||
"9B": CreatePRScore("popn", "9B", { gauge: p.optional(p.isBetween(0, 100)) }),
|
||||
},
|
||||
usc: {
|
||||
Single: CreatePRScore("usc", "Single", { gauge: p.optional(p.isBetween(0, 100)) }),
|
||||
},
|
||||
};
|
||||
|
||||
const PRUDENCE_CHART_BASE: PrudenceSchema = {
|
||||
chartID: "string",
|
||||
rgcID: "?string",
|
||||
songID: p.isPositiveInteger,
|
||||
level: "string",
|
||||
levelNum: p.isPositive,
|
||||
isPrimary: "boolean",
|
||||
versions: ["string"],
|
||||
chartID: "string",
|
||||
rgcID: "?string",
|
||||
songID: p.isPositiveInteger,
|
||||
level: "string",
|
||||
levelNum: p.isPositive,
|
||||
isPrimary: "boolean",
|
||||
versions: ["string"],
|
||||
};
|
||||
|
||||
function CreatePrChart(game: Game, flags: string[], data: PrudenceSchema) {
|
||||
return deepmerge(PRUDENCE_CHART_BASE, {
|
||||
difficulty: p.isIn(validDifficulties[game]),
|
||||
playtype: p.isIn(validPlaytypes[game]),
|
||||
flags: Object.fromEntries(flags.map((e) => [e, "boolean"])),
|
||||
data,
|
||||
});
|
||||
return deepmerge(PRUDENCE_CHART_BASE, {
|
||||
difficulty: p.isIn(validDifficulties[game]),
|
||||
playtype: p.isIn(validPlaytypes[game]),
|
||||
flags: Object.fromEntries(flags.map((e) => [e, "boolean"])),
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export const PRUDENCE_CHART_SCHEMAS: Record<Game, PrudenceSchema> = {
|
||||
iidx: CreatePrChart("iidx", ["IN BASE GAME", "OMNIMIX", "N-1"], {
|
||||
notecount: p.isPositiveInteger,
|
||||
inGameID: p.isPositiveInteger,
|
||||
arcChartID: "?string",
|
||||
}),
|
||||
bms: CreatePrChart("bms", [], { hashMD5: "string", hashSHA256: "string" }),
|
||||
chunithm: CreatePrChart("chunithm", ["IN BASE GAME", "OMNIMIX"], {
|
||||
inGameID: p.isPositiveInteger,
|
||||
}),
|
||||
ddr: CreatePrChart("ddr", ["IN BASE GAME", "N-1"], {
|
||||
songHash: "string",
|
||||
inGameID: "string",
|
||||
}),
|
||||
gitadora: CreatePrChart("gitadora", ["IN BASE GAME", "OMNIMIX", "HOT N-1", "HOT"], {
|
||||
inGameID: p.isPositiveInteger,
|
||||
}),
|
||||
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"], {}),
|
||||
sdvx: CreatePrChart("sdvx", ["IN BASE GAME", "OMNIMIX", "N-1"], {
|
||||
inGameID: p.isPositiveInteger,
|
||||
}),
|
||||
usc: CreatePrChart("usc", ["CUSTOM"], {
|
||||
hashSHA256: "string",
|
||||
}),
|
||||
iidx: CreatePrChart("iidx", ["IN BASE GAME", "OMNIMIX", "N-1"], {
|
||||
notecount: p.isPositiveInteger,
|
||||
inGameID: p.isPositiveInteger,
|
||||
arcChartID: "?string",
|
||||
}),
|
||||
bms: CreatePrChart("bms", [], { hashMD5: "string", hashSHA256: "string" }),
|
||||
chunithm: CreatePrChart("chunithm", ["IN BASE GAME", "OMNIMIX"], {
|
||||
inGameID: p.isPositiveInteger,
|
||||
}),
|
||||
ddr: CreatePrChart("ddr", ["IN BASE GAME", "N-1"], {
|
||||
songHash: "string",
|
||||
inGameID: "string",
|
||||
}),
|
||||
gitadora: CreatePrChart("gitadora", ["IN BASE GAME", "OMNIMIX", "HOT N-1", "HOT"], {
|
||||
inGameID: p.isPositiveInteger,
|
||||
}),
|
||||
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"], {}),
|
||||
sdvx: CreatePrChart("sdvx", ["IN BASE GAME", "OMNIMIX", "N-1"], {
|
||||
inGameID: p.isPositiveInteger,
|
||||
}),
|
||||
usc: CreatePrChart("usc", ["CUSTOM"], {
|
||||
hashSHA256: "string",
|
||||
}),
|
||||
};
|
||||
|
||||
const PRUDENCE_SONG_BASE: PrudenceSchema = {
|
||||
id: p.isPositiveInteger,
|
||||
title: "string",
|
||||
artist: "string",
|
||||
"search-titles": [p.and("string", (self, parent) => self !== parent.title)],
|
||||
"alt-titles": [p.and("string", (self, parent) => self !== parent.title)],
|
||||
firstVersion: "string",
|
||||
id: p.isPositiveInteger,
|
||||
title: "string",
|
||||
artist: "string",
|
||||
"search-titles": [p.and("string", (self, parent) => self !== parent.title)],
|
||||
"alt-titles": [p.and("string", (self, parent) => self !== parent.title)],
|
||||
firstVersion: "string",
|
||||
};
|
||||
|
||||
function CreatePrSong(data: PrudenceSchema) {
|
||||
return deepmerge(PRUDENCE_SONG_BASE, {
|
||||
data,
|
||||
});
|
||||
return deepmerge(PRUDENCE_SONG_BASE, {
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
const PRUDENCE_SONG_WITH_GENRE = CreatePrSong({ genre: "string" });
|
||||
|
||||
export const PRUDENCE_SONG_SCHEMAS: Record<Game, PrudenceSchema> = {
|
||||
iidx: PRUDENCE_SONG_WITH_GENRE,
|
||||
bms: PRUDENCE_SONG_WITH_GENRE,
|
||||
chunithm: PRUDENCE_SONG_WITH_GENRE,
|
||||
ddr: CreatePrSong({}),
|
||||
gitadora: CreatePrSong({}),
|
||||
jubeat: CreatePrSong({}),
|
||||
maimai: CreatePrSong({ titleJP: "string", artistJP: "string", genre: "string" }),
|
||||
museca: CreatePrSong({ titleJP: "string", artistJP: "string" }),
|
||||
popn: PRUDENCE_SONG_WITH_GENRE,
|
||||
sdvx: CreatePrSong({ uscEquiv: p.nullable(p.isPositiveInteger) }),
|
||||
usc: CreatePrSong({ sdvxEquiv: p.nullable(p.isPositiveInteger) }),
|
||||
iidx: PRUDENCE_SONG_WITH_GENRE,
|
||||
bms: PRUDENCE_SONG_WITH_GENRE,
|
||||
chunithm: PRUDENCE_SONG_WITH_GENRE,
|
||||
ddr: CreatePrSong({}),
|
||||
gitadora: CreatePrSong({}),
|
||||
jubeat: CreatePrSong({}),
|
||||
maimai: CreatePrSong({ titleJP: "string", artistJP: "string", genre: "string" }),
|
||||
museca: CreatePrSong({ titleJP: "string", artistJP: "string" }),
|
||||
popn: PRUDENCE_SONG_WITH_GENRE,
|
||||
sdvx: CreatePrSong({ uscEquiv: p.nullable(p.isPositiveInteger) }),
|
||||
usc: CreatePrSong({ sdvxEquiv: p.nullable(p.isPositiveInteger) }),
|
||||
};
|
||||
|
||||
const PRUDENCE_FOLDER_CHART_LOOKUP: PrudenceSchema = {
|
||||
chartID: "string",
|
||||
folderID: "string",
|
||||
chartID: "string",
|
||||
folderID: "string",
|
||||
};
|
||||
|
||||
const PRUDENCE_FOLDER: PrudenceSchema = {
|
||||
title: "string",
|
||||
game: p.isIn(CONF_INFO.SUPPORTED_GAMES),
|
||||
playtype: (self, parent) => p.isIn(validPlaytypes[parent.game as Game])(self),
|
||||
folderID: "string",
|
||||
table: "string",
|
||||
tableIndex: "number",
|
||||
type: p.isIn("songs", "charts", "static"),
|
||||
data: (self, parent) => {
|
||||
if (parent.type === "static") {
|
||||
return Array.isArray(self) && self.every((e) => typeof e === "string");
|
||||
}
|
||||
title: "string",
|
||||
game: p.isIn(CONF_INFO.SUPPORTED_GAMES),
|
||||
playtype: (self, parent) => p.isIn(validPlaytypes[parent.game as Game])(self),
|
||||
folderID: "string",
|
||||
table: "string",
|
||||
tableIndex: "number",
|
||||
type: p.isIn("songs", "charts", "static"),
|
||||
data: (self, parent) => {
|
||||
if (parent.type === "static") {
|
||||
return Array.isArray(self) && self.every((e) => typeof e === "string");
|
||||
}
|
||||
|
||||
// this is a temp hackjob, it should technically
|
||||
// check whether this document matches a
|
||||
// partialised chart/song doc
|
||||
// but.. can be done later.
|
||||
return typeof self === "object" && !!self; // whatever
|
||||
},
|
||||
// this is a temp hackjob, it should technically
|
||||
// check whether this document matches a
|
||||
// partialised chart/song doc
|
||||
// but.. can be done later.
|
||||
return typeof self === "object" && !!self; // whatever
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -433,11 +433,11 @@ const PRUDENCE_FOLDER: PrudenceSchema = {
|
||||
* being different depending on the score.game field.
|
||||
*/
|
||||
export const STATIC_SCHEMAS = {
|
||||
users: PRUDENCE_PRIVATE_USER,
|
||||
"iidx-bpi-data": PRUDENCE_IIDX_BPI_DATA,
|
||||
counters: PRUDENCE_COUNTER,
|
||||
charts: PRUDENCE_CHART_SCHEMAS,
|
||||
songs: PRUDENCE_SONG_SCHEMAS,
|
||||
"folder-chart-lookup": PRUDENCE_FOLDER_CHART_LOOKUP,
|
||||
folders: PRUDENCE_FOLDER,
|
||||
users: PRUDENCE_PRIVATE_USER,
|
||||
"iidx-bpi-data": PRUDENCE_IIDX_BPI_DATA,
|
||||
counters: PRUDENCE_COUNTER,
|
||||
charts: PRUDENCE_CHART_SCHEMAS,
|
||||
songs: PRUDENCE_SONG_SCHEMAS,
|
||||
"folder-chart-lookup": PRUDENCE_FOLDER_CHART_LOOKUP,
|
||||
folders: PRUDENCE_FOLDER,
|
||||
};
|
||||
|
||||
+7
-7
@@ -3,14 +3,14 @@ import t from "tap";
|
||||
import { CloseRedisPubSub, RedisPub, RedisSub } from "./redis-IPC";
|
||||
|
||||
t.test("Basic PUB/SUB testing", (t) => {
|
||||
t.setTimeout(2000);
|
||||
RedisSub("test" as any, (d) => {
|
||||
t.equal(d.userID, 1);
|
||||
t.pass();
|
||||
t.end();
|
||||
});
|
||||
t.setTimeout(2000);
|
||||
RedisSub("test" as any, (d) => {
|
||||
t.equal(d.userID, 1);
|
||||
t.pass();
|
||||
t.end();
|
||||
});
|
||||
|
||||
RedisPub("test" as any, { userID: 1 } as any);
|
||||
RedisPub("test" as any, { userID: 1 } as any);
|
||||
});
|
||||
|
||||
t.teardown(CloseRedisPubSub);
|
||||
|
||||
+43
-43
@@ -8,7 +8,7 @@ const logger = CreateLogCtx(__filename);
|
||||
type RedisSubCallback<T extends RedisIPCChannels> = (data: RedisIPCData[T]) => void;
|
||||
|
||||
type SubCallbacks = {
|
||||
[K in RedisIPCChannels]: RedisSubCallback<K>[];
|
||||
[K in RedisIPCChannels]: RedisSubCallback<K>[];
|
||||
};
|
||||
|
||||
const SubCallbacks: Partial<SubCallbacks> = {};
|
||||
@@ -22,64 +22,64 @@ const PubClient = redis.createClient();
|
||||
const PREFIX = CONFIG.TYPE.toUpperCase(); // KTCHI or BTCHI
|
||||
|
||||
export function RedisPub<T extends RedisIPCChannels>(channel: T, data: RedisIPCData[T]) {
|
||||
PubClient.publish(`${PREFIX}-${channel}`, JSON.stringify(data));
|
||||
PubClient.publish(`${PREFIX}-${channel}`, JSON.stringify(data));
|
||||
}
|
||||
|
||||
export function RedisSub<T extends RedisIPCChannels>(channel: T, callback: RedisSubCallback<T>) {
|
||||
if (SubCallbacks[channel]) {
|
||||
// @ts-expect-error It's complaining that the T in channel might be a different T to the T in callback
|
||||
// this is obviously nonsense.
|
||||
SubCallbacks[channel]!.push(callback);
|
||||
logger.debug(`Pushed callback ${callback.name} to channel ${channel}.`);
|
||||
} else {
|
||||
// @ts-expect-error see above.
|
||||
SubCallbacks[channel] = [callback];
|
||||
SubClient.subscribe(`${PREFIX}-${channel}`);
|
||||
logger.debug(`Added first callback ${callback.name} to channel ${channel}.`);
|
||||
}
|
||||
if (SubCallbacks[channel]) {
|
||||
// @ts-expect-error It's complaining that the T in channel might be a different T to the T in callback
|
||||
// this is obviously nonsense.
|
||||
SubCallbacks[channel]!.push(callback);
|
||||
logger.debug(`Pushed callback ${callback.name} to channel ${channel}.`);
|
||||
} else {
|
||||
// @ts-expect-error see above.
|
||||
SubCallbacks[channel] = [callback];
|
||||
SubClient.subscribe(`${PREFIX}-${channel}`);
|
||||
logger.debug(`Added first callback ${callback.name} to channel ${channel}.`);
|
||||
}
|
||||
}
|
||||
|
||||
SubClient.on("message", (channel, strData) => {
|
||||
if (!channel.startsWith(`${PREFIX}-`)) {
|
||||
return; // not our business
|
||||
}
|
||||
if (!channel.startsWith(`${PREFIX}-`)) {
|
||||
return; // not our business
|
||||
}
|
||||
|
||||
const ktChannel = channel.slice(`${PREFIX}-`.length) as RedisIPCChannels;
|
||||
const ktChannel = channel.slice(`${PREFIX}-`.length) as RedisIPCChannels;
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(SubCallbacks, ktChannel)) {
|
||||
return; // no callbacks to call
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(SubCallbacks, ktChannel)) {
|
||||
return; // no callbacks to call
|
||||
}
|
||||
|
||||
const jsData = JSON.parse(strData);
|
||||
const jsData = JSON.parse(strData);
|
||||
|
||||
for (const cb of SubCallbacks[ktChannel]!) {
|
||||
try {
|
||||
cb(jsData);
|
||||
} catch (err) {
|
||||
logger.error(`Error calling callback ${cb.name} for channel ${ktChannel}`, { err });
|
||||
}
|
||||
}
|
||||
for (const cb of SubCallbacks[ktChannel]!) {
|
||||
try {
|
||||
cb(jsData);
|
||||
} catch (err) {
|
||||
logger.error(`Error calling callback ${cb.name} for channel ${ktChannel}`, { err });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Awful...
|
||||
// Function is near-impossible to test.
|
||||
/* istanbul ignore next */
|
||||
export function CloseRedisPubSub() {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
PubClient.quit((err) => {
|
||||
if (err) {
|
||||
logger.crit(`PubClient QUIT error: ${err}`, { err });
|
||||
reject(err);
|
||||
}
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
PubClient.quit((err) => {
|
||||
if (err) {
|
||||
logger.crit(`PubClient QUIT error: ${err}`, { err });
|
||||
reject(err);
|
||||
}
|
||||
|
||||
SubClient.quit((err) => {
|
||||
if (err) {
|
||||
logger.crit(`SubClient QUIT error: ${err}`, { err });
|
||||
reject(err);
|
||||
}
|
||||
SubClient.quit((err) => {
|
||||
if (err) {
|
||||
logger.crit(`SubClient QUIT error: ${err}`, { err });
|
||||
reject(err);
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Vendored
+7
-7
@@ -9,20 +9,20 @@ export const RedisClient = redis.createClient();
|
||||
logger.verbose("Instantiated Redis Client");
|
||||
|
||||
function EmitCritical() {
|
||||
/* istanbul ignore next */
|
||||
if (!RedisClient.connected) {
|
||||
logger.crit(`Could not connect to redis in time. No more information is available.`);
|
||||
}
|
||||
/* istanbul ignore next */
|
||||
if (!RedisClient.connected) {
|
||||
logger.crit(`Could not connect to redis in time. No more information is available.`);
|
||||
}
|
||||
}
|
||||
|
||||
const ref = setTimeout(EmitCritical, 10000);
|
||||
|
||||
RedisClient.on("connect", () => {
|
||||
logger.verbose(`Connected to Redis.`);
|
||||
logger.verbose(`Connected to Redis.`);
|
||||
|
||||
clearTimeout(ref);
|
||||
clearTimeout(ref);
|
||||
});
|
||||
|
||||
export function CloseRedisConnection() {
|
||||
return RedisClient.quit();
|
||||
return RedisClient.quit();
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ import { EvaluateGoalForUser, HumaniseGoalProgress } from "./goals";
|
||||
import { GoalDocument } from "tachi-common";
|
||||
import deepmerge from "deepmerge";
|
||||
import {
|
||||
HC511Goal,
|
||||
Testing511SPA,
|
||||
TestingIIDXFolderSP10,
|
||||
TestingIIDXSPScorePB,
|
||||
HC511Goal,
|
||||
Testing511SPA,
|
||||
TestingIIDXFolderSP10,
|
||||
TestingIIDXSPScorePB,
|
||||
} from "../../test-utils/test-data";
|
||||
import { CreateFolderChartLookup } from "../../utils/folder";
|
||||
import { CloseAllConnections } from "../../test-utils/close-connections";
|
||||
@@ -17,292 +17,292 @@ import { CloseAllConnections } from "../../test-utils/close-connections";
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
t.test("#EvaluateGoalForUser", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should correctly evaluate against single goals.", (t) => {
|
||||
t.beforeEach(async () => {
|
||||
await db["personal-bests"].insert(TestingIIDXSPScorePB);
|
||||
delete TestingIIDXSPScorePB._id;
|
||||
});
|
||||
t.test("Should correctly evaluate against single goals.", (t) => {
|
||||
t.beforeEach(async () => {
|
||||
await db["personal-bests"].insert(TestingIIDXSPScorePB);
|
||||
delete TestingIIDXSPScorePB._id;
|
||||
});
|
||||
|
||||
t.test("Should correctly evaluate goals if user succeeds.", async (t) => {
|
||||
const res = await EvaluateGoalForUser(HC511Goal, 1, logger);
|
||||
t.test("Should correctly evaluate goals if user succeeds.", async (t) => {
|
||||
const res = await EvaluateGoalForUser(HC511Goal, 1, logger);
|
||||
|
||||
t.strictSame(
|
||||
res,
|
||||
{
|
||||
achieved: true,
|
||||
outOf: 5,
|
||||
progress: 6,
|
||||
outOfHuman: "HARD CLEAR",
|
||||
progressHuman: "EX HARD CLEAR (BP: 2)",
|
||||
},
|
||||
"Should correctly evaluate the goal if the user succeeds."
|
||||
);
|
||||
t.strictSame(
|
||||
res,
|
||||
{
|
||||
achieved: true,
|
||||
outOf: 5,
|
||||
progress: 6,
|
||||
outOfHuman: "HARD CLEAR",
|
||||
progressHuman: "EX HARD CLEAR (BP: 2)",
|
||||
},
|
||||
"Should correctly evaluate the goal if the user succeeds."
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should correctly evaluate goals if user fails.", async (t) => {
|
||||
await db["personal-bests"].update(
|
||||
{ userID: 1, chartID: Testing511SPA.chartID },
|
||||
{
|
||||
$set: {
|
||||
"scoreData.lamp": "CLEAR",
|
||||
"scoreData.lampIndex": 4,
|
||||
},
|
||||
}
|
||||
);
|
||||
t.test("Should correctly evaluate goals if user fails.", async (t) => {
|
||||
await db["personal-bests"].update(
|
||||
{ userID: 1, chartID: Testing511SPA.chartID },
|
||||
{
|
||||
$set: {
|
||||
"scoreData.lamp": "CLEAR",
|
||||
"scoreData.lampIndex": 4,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const res = await EvaluateGoalForUser(HC511Goal, 1, logger);
|
||||
const res = await EvaluateGoalForUser(HC511Goal, 1, logger);
|
||||
|
||||
t.strictSame(
|
||||
res,
|
||||
{
|
||||
achieved: false,
|
||||
outOf: 5,
|
||||
progress: 4,
|
||||
outOfHuman: "HARD CLEAR",
|
||||
progressHuman: "CLEAR (BP: 2)",
|
||||
},
|
||||
"Should correctly evaluate the goal if the user fails."
|
||||
);
|
||||
t.strictSame(
|
||||
res,
|
||||
{
|
||||
achieved: false,
|
||||
outOf: 5,
|
||||
progress: 4,
|
||||
outOfHuman: "HARD CLEAR",
|
||||
progressHuman: "CLEAR (BP: 2)",
|
||||
},
|
||||
"Should correctly evaluate the goal if the user fails."
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should correctly evaluate goals when user has no score.", async (t) => {
|
||||
await db["personal-bests"].remove({});
|
||||
t.test("Should correctly evaluate goals when user has no score.", async (t) => {
|
||||
await db["personal-bests"].remove({});
|
||||
|
||||
const res = await EvaluateGoalForUser(HC511Goal, 1, logger);
|
||||
const res = await EvaluateGoalForUser(HC511Goal, 1, logger);
|
||||
|
||||
t.strictSame(
|
||||
res,
|
||||
{
|
||||
achieved: false,
|
||||
outOf: 5,
|
||||
progress: null,
|
||||
outOfHuman: "HARD CLEAR",
|
||||
progressHuman: "NO DATA",
|
||||
},
|
||||
"Should correctly evaluate the goal if the user has no score."
|
||||
);
|
||||
t.strictSame(
|
||||
res,
|
||||
{
|
||||
achieved: false,
|
||||
outOf: 5,
|
||||
progress: null,
|
||||
outOfHuman: "HARD CLEAR",
|
||||
progressHuman: "NO DATA",
|
||||
},
|
||||
"Should correctly evaluate the goal if the user has no score."
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should correctly evaluate against multi goals.", (t) => {
|
||||
// @ts-expect-error ???
|
||||
// this goal is effectively "HC any of the charts"
|
||||
const multiGoal: GoalDocument = deepmerge(HC511Goal, {
|
||||
charts: {
|
||||
type: "multi",
|
||||
data: [Testing511SPA.chartID, "fake_other_chart_id"],
|
||||
},
|
||||
});
|
||||
t.test("Should correctly evaluate against multi goals.", (t) => {
|
||||
// @ts-expect-error ???
|
||||
// this goal is effectively "HC any of the charts"
|
||||
const multiGoal: GoalDocument = deepmerge(HC511Goal, {
|
||||
charts: {
|
||||
type: "multi",
|
||||
data: [Testing511SPA.chartID, "fake_other_chart_id"],
|
||||
},
|
||||
});
|
||||
|
||||
t.beforeEach(async () => {
|
||||
await db["personal-bests"].insert(TestingIIDXSPScorePB);
|
||||
delete TestingIIDXSPScorePB._id;
|
||||
t.beforeEach(async () => {
|
||||
await db["personal-bests"].insert(TestingIIDXSPScorePB);
|
||||
delete TestingIIDXSPScorePB._id;
|
||||
|
||||
await db["personal-bests"].insert(
|
||||
deepmerge(TestingIIDXSPScorePB, {
|
||||
scoreData: { lamp: "CLEAR", lampIndex: 4 },
|
||||
chartID: "fake_other_chart_id",
|
||||
})
|
||||
);
|
||||
});
|
||||
await db["personal-bests"].insert(
|
||||
deepmerge(TestingIIDXSPScorePB, {
|
||||
scoreData: { lamp: "CLEAR", lampIndex: 4 },
|
||||
chartID: "fake_other_chart_id",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
t.test("Should work if 511 is >= HARD CLEAR", async (t) => {
|
||||
const res = await EvaluateGoalForUser(multiGoal, 1, logger);
|
||||
t.test("Should work if 511 is >= HARD CLEAR", async (t) => {
|
||||
const res = await EvaluateGoalForUser(multiGoal, 1, logger);
|
||||
|
||||
t.strictSame(
|
||||
res,
|
||||
{
|
||||
achieved: true,
|
||||
outOf: 5,
|
||||
progress: 6,
|
||||
outOfHuman: "HARD CLEAR",
|
||||
progressHuman: "EX HARD CLEAR (BP: 2)",
|
||||
},
|
||||
"Should correctly evaluate the goal if the user succeeds."
|
||||
);
|
||||
t.strictSame(
|
||||
res,
|
||||
{
|
||||
achieved: true,
|
||||
outOf: 5,
|
||||
progress: 6,
|
||||
outOfHuman: "HARD CLEAR",
|
||||
progressHuman: "EX HARD CLEAR (BP: 2)",
|
||||
},
|
||||
"Should correctly evaluate the goal if the user succeeds."
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should work if other chart is >= HARD CLEAR", async (t) => {
|
||||
await db["personal-bests"].update(
|
||||
{ userID: 1, chartID: Testing511SPA.chartID },
|
||||
{ $set: { "scoreData.lamp": "CLEAR", "scoreData.lampIndex": 4 } }
|
||||
);
|
||||
await db["personal-bests"].update(
|
||||
{ userID: 1, chartID: "fake_other_chart_id" },
|
||||
{ $set: { "scoreData.lamp": "HARD CLEAR", "scoreData.lampIndex": 5 } }
|
||||
);
|
||||
t.test("Should work if other chart is >= HARD CLEAR", async (t) => {
|
||||
await db["personal-bests"].update(
|
||||
{ userID: 1, chartID: Testing511SPA.chartID },
|
||||
{ $set: { "scoreData.lamp": "CLEAR", "scoreData.lampIndex": 4 } }
|
||||
);
|
||||
await db["personal-bests"].update(
|
||||
{ userID: 1, chartID: "fake_other_chart_id" },
|
||||
{ $set: { "scoreData.lamp": "HARD CLEAR", "scoreData.lampIndex": 5 } }
|
||||
);
|
||||
|
||||
const res = await EvaluateGoalForUser(multiGoal, 1, logger);
|
||||
const res = await EvaluateGoalForUser(multiGoal, 1, logger);
|
||||
|
||||
t.strictSame(
|
||||
res,
|
||||
{
|
||||
achieved: true,
|
||||
outOf: 5,
|
||||
progress: 5,
|
||||
outOfHuman: "HARD CLEAR",
|
||||
progressHuman: "HARD CLEAR (BP: 2)",
|
||||
},
|
||||
"Should correctly evaluate the goal if the user succeeds."
|
||||
);
|
||||
t.strictSame(
|
||||
res,
|
||||
{
|
||||
achieved: true,
|
||||
outOf: 5,
|
||||
progress: 5,
|
||||
outOfHuman: "HARD CLEAR",
|
||||
progressHuman: "HARD CLEAR (BP: 2)",
|
||||
},
|
||||
"Should correctly evaluate the goal if the user succeeds."
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should work if user has no scores", async (t) => {
|
||||
await db["personal-bests"].remove({});
|
||||
t.test("Should work if user has no scores", async (t) => {
|
||||
await db["personal-bests"].remove({});
|
||||
|
||||
const res = await EvaluateGoalForUser(multiGoal, 1, logger);
|
||||
const res = await EvaluateGoalForUser(multiGoal, 1, logger);
|
||||
|
||||
t.strictSame(
|
||||
res,
|
||||
{
|
||||
achieved: false,
|
||||
outOf: 5,
|
||||
progress: null,
|
||||
outOfHuman: "HARD CLEAR",
|
||||
progressHuman: "NO DATA",
|
||||
},
|
||||
"Should correctly evaluate the goal if the user has no data."
|
||||
);
|
||||
t.strictSame(
|
||||
res,
|
||||
{
|
||||
achieved: false,
|
||||
outOf: 5,
|
||||
progress: null,
|
||||
outOfHuman: "HARD CLEAR",
|
||||
progressHuman: "NO DATA",
|
||||
},
|
||||
"Should correctly evaluate the goal if the user has no data."
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should correctly evaluate against folder goals.", (t) => {
|
||||
// @ts-expect-error ???
|
||||
// this goal is effectively "HC any of the sp10s"
|
||||
const folderGoal: GoalDocument = deepmerge(HC511Goal, {
|
||||
charts: {
|
||||
type: "folder",
|
||||
data: "ed9d8c734447ce67d7135c0067441a98cc81aeaf",
|
||||
},
|
||||
});
|
||||
t.test("Should correctly evaluate against folder goals.", (t) => {
|
||||
// @ts-expect-error ???
|
||||
// this goal is effectively "HC any of the sp10s"
|
||||
const folderGoal: GoalDocument = deepmerge(HC511Goal, {
|
||||
charts: {
|
||||
type: "folder",
|
||||
data: "ed9d8c734447ce67d7135c0067441a98cc81aeaf",
|
||||
},
|
||||
});
|
||||
|
||||
t.beforeEach(async () => {
|
||||
await db["personal-bests"].insert(TestingIIDXSPScorePB);
|
||||
delete TestingIIDXSPScorePB._id;
|
||||
t.beforeEach(async () => {
|
||||
await db["personal-bests"].insert(TestingIIDXSPScorePB);
|
||||
delete TestingIIDXSPScorePB._id;
|
||||
|
||||
await db["personal-bests"].insert(
|
||||
deepmerge(TestingIIDXSPScorePB, {
|
||||
scoreData: { lamp: "CLEAR", lampIndex: 4 },
|
||||
chartID: "other_sp10",
|
||||
})
|
||||
);
|
||||
await db["personal-bests"].insert(
|
||||
deepmerge(TestingIIDXSPScorePB, {
|
||||
scoreData: { lamp: "CLEAR", lampIndex: 4 },
|
||||
chartID: "other_sp10",
|
||||
})
|
||||
);
|
||||
|
||||
delete Testing511SPA._id;
|
||||
delete Testing511SPA._id;
|
||||
|
||||
await db.charts.iidx.insert([
|
||||
// @ts-expect-error ???
|
||||
deepmerge(Testing511SPA, {
|
||||
songID: 123,
|
||||
level: "9",
|
||||
chartID: "not_sp10",
|
||||
}),
|
||||
// @ts-expect-error ???
|
||||
deepmerge(Testing511SPA, {
|
||||
songID: 124,
|
||||
level: "10",
|
||||
chartID: "other_sp10",
|
||||
}),
|
||||
]);
|
||||
await db.charts.iidx.insert([
|
||||
// @ts-expect-error ???
|
||||
deepmerge(Testing511SPA, {
|
||||
songID: 123,
|
||||
level: "9",
|
||||
chartID: "not_sp10",
|
||||
}),
|
||||
// @ts-expect-error ???
|
||||
deepmerge(Testing511SPA, {
|
||||
songID: 124,
|
||||
level: "10",
|
||||
chartID: "other_sp10",
|
||||
}),
|
||||
]);
|
||||
|
||||
await CreateFolderChartLookup(TestingIIDXFolderSP10);
|
||||
});
|
||||
await CreateFolderChartLookup(TestingIIDXFolderSP10);
|
||||
});
|
||||
|
||||
t.test("Should work if 511 is >= HARD CLEAR", async (t) => {
|
||||
const res = await EvaluateGoalForUser(folderGoal, 1, logger);
|
||||
t.test("Should work if 511 is >= HARD CLEAR", async (t) => {
|
||||
const res = await EvaluateGoalForUser(folderGoal, 1, logger);
|
||||
|
||||
t.strictSame(
|
||||
res,
|
||||
{
|
||||
achieved: true,
|
||||
outOf: 5,
|
||||
progress: 6,
|
||||
outOfHuman: "HARD CLEAR",
|
||||
progressHuman: "EX HARD CLEAR (BP: 2)",
|
||||
},
|
||||
"Should correctly evaluate the goal if the user succeeds."
|
||||
);
|
||||
t.strictSame(
|
||||
res,
|
||||
{
|
||||
achieved: true,
|
||||
outOf: 5,
|
||||
progress: 6,
|
||||
outOfHuman: "HARD CLEAR",
|
||||
progressHuman: "EX HARD CLEAR (BP: 2)",
|
||||
},
|
||||
"Should correctly evaluate the goal if the user succeeds."
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should work if other chart is >= HARD CLEAR", async (t) => {
|
||||
await db["personal-bests"].update(
|
||||
{ userID: 1, chartID: Testing511SPA.chartID },
|
||||
{ $set: { "scoreData.lamp": "CLEAR", "scoreData.lampIndex": 4 } }
|
||||
);
|
||||
t.test("Should work if other chart is >= HARD CLEAR", async (t) => {
|
||||
await db["personal-bests"].update(
|
||||
{ userID: 1, chartID: Testing511SPA.chartID },
|
||||
{ $set: { "scoreData.lamp": "CLEAR", "scoreData.lampIndex": 4 } }
|
||||
);
|
||||
|
||||
await db["personal-bests"].update(
|
||||
{ userID: 1, chartID: "other_sp10" },
|
||||
{ $set: { "scoreData.lamp": "HARD CLEAR", "scoreData.lampIndex": 5 } }
|
||||
);
|
||||
await db["personal-bests"].update(
|
||||
{ userID: 1, chartID: "other_sp10" },
|
||||
{ $set: { "scoreData.lamp": "HARD CLEAR", "scoreData.lampIndex": 5 } }
|
||||
);
|
||||
|
||||
const res = await EvaluateGoalForUser(folderGoal, 1, logger);
|
||||
const res = await EvaluateGoalForUser(folderGoal, 1, logger);
|
||||
|
||||
t.strictSame(
|
||||
res,
|
||||
{
|
||||
achieved: true,
|
||||
outOf: 5,
|
||||
progress: 5,
|
||||
outOfHuman: "HARD CLEAR",
|
||||
progressHuman: "HARD CLEAR (BP: 2)",
|
||||
},
|
||||
"Should correctly evaluate the goal if the user succeeds."
|
||||
);
|
||||
t.strictSame(
|
||||
res,
|
||||
{
|
||||
achieved: true,
|
||||
outOf: 5,
|
||||
progress: 5,
|
||||
outOfHuman: "HARD CLEAR",
|
||||
progressHuman: "HARD CLEAR (BP: 2)",
|
||||
},
|
||||
"Should correctly evaluate the goal if the user succeeds."
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should work if user has no scores", async (t) => {
|
||||
await db["personal-bests"].remove({});
|
||||
t.test("Should work if user has no scores", async (t) => {
|
||||
await db["personal-bests"].remove({});
|
||||
|
||||
const res = await EvaluateGoalForUser(folderGoal, 1, logger);
|
||||
const res = await EvaluateGoalForUser(folderGoal, 1, logger);
|
||||
|
||||
t.strictSame(
|
||||
res,
|
||||
{
|
||||
achieved: false,
|
||||
outOf: 5,
|
||||
progress: null,
|
||||
outOfHuman: "HARD CLEAR",
|
||||
progressHuman: "NO DATA",
|
||||
},
|
||||
"Should correctly evaluate the goal if the user has no data."
|
||||
);
|
||||
t.strictSame(
|
||||
res,
|
||||
{
|
||||
achieved: false,
|
||||
outOf: 5,
|
||||
progress: null,
|
||||
outOfHuman: "HARD CLEAR",
|
||||
progressHuman: "NO DATA",
|
||||
},
|
||||
"Should correctly evaluate the goal if the user has no data."
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
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", "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.end();
|
||||
t.end();
|
||||
});
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
+152
-152
@@ -7,11 +7,11 @@ import { FilterQuery } from "mongodb";
|
||||
import fjsh from "fast-json-stable-hash";
|
||||
|
||||
export interface EvaluatedGoalReturn {
|
||||
achieved: boolean;
|
||||
progress: number | null;
|
||||
outOf: number;
|
||||
progressHuman: string;
|
||||
outOfHuman: string;
|
||||
achieved: boolean;
|
||||
progress: number | null;
|
||||
outOf: number;
|
||||
progressHuman: string;
|
||||
outOfHuman: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,146 +26,146 @@ export interface EvaluatedGoalReturn {
|
||||
* to multiple games.
|
||||
*/
|
||||
export function CreateGoalID(charts: GoalDocument["charts"], criteria: GoalDocument["criteria"]) {
|
||||
return fjsh.hash({ charts, criteria }, "sha256");
|
||||
return fjsh.hash({ charts, criteria }, "sha256");
|
||||
}
|
||||
|
||||
export async function EvaluateGoalForUser(
|
||||
goal: GoalDocument,
|
||||
userID: integer,
|
||||
logger: KtLogger
|
||||
goal: GoalDocument,
|
||||
userID: integer,
|
||||
logger: KtLogger
|
||||
): Promise<EvaluatedGoalReturn | null> {
|
||||
// First, we need to resolve the set of charts this
|
||||
// goal involves.
|
||||
const chartIDs = await ResolveGoalCharts(goal);
|
||||
// First, we need to resolve the set of charts this
|
||||
// goal involves.
|
||||
const chartIDs = await ResolveGoalCharts(goal);
|
||||
|
||||
if (chartIDs === undefined) {
|
||||
logger.error(
|
||||
`Invalid goal ${goal.goalID} - has nonsense chartsType of ${goal.charts.type}, ignoring.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if (chartIDs === undefined) {
|
||||
logger.error(
|
||||
`Invalid goal ${goal.goalID} - has nonsense chartsType of ${goal.charts.type}, ignoring.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// lets configure a "base" query for our requests.
|
||||
const scoreQuery: FilterQuery<PBScoreDocument> = {
|
||||
userID,
|
||||
// normally, this would be a VERY WORRYING line of code, but goal.criteria.key is guaranteed to be
|
||||
// within a specific set of fields.
|
||||
[goal.criteria.key]: { $gte: goal.criteria.value },
|
||||
};
|
||||
// lets configure a "base" query for our requests.
|
||||
const scoreQuery: FilterQuery<PBScoreDocument> = {
|
||||
userID,
|
||||
// normally, this would be a VERY WORRYING line of code, but goal.criteria.key is guaranteed to be
|
||||
// within a specific set of fields.
|
||||
[goal.criteria.key]: { $gte: goal.criteria.value },
|
||||
};
|
||||
|
||||
if (chartIDs) {
|
||||
scoreQuery.chartID = { $in: chartIDs };
|
||||
}
|
||||
if (chartIDs) {
|
||||
scoreQuery.chartID = { $in: chartIDs };
|
||||
}
|
||||
|
||||
// Next, we need to figure out our criteria.
|
||||
if (goal.criteria.mode === "single") {
|
||||
const res = await db["personal-bests"].findOne(scoreQuery);
|
||||
// hack, but guaranteed to work.
|
||||
const scoreDataKey = goal.criteria.key.split(".")[1] as
|
||||
| "lampIndex"
|
||||
| "gradeIndex"
|
||||
| "score"
|
||||
| "percent";
|
||||
// Next, we need to figure out our criteria.
|
||||
if (goal.criteria.mode === "single") {
|
||||
const res = await db["personal-bests"].findOne(scoreQuery);
|
||||
// hack, but guaranteed to work.
|
||||
const scoreDataKey = goal.criteria.key.split(".")[1] as
|
||||
| "lampIndex"
|
||||
| "gradeIndex"
|
||||
| "score"
|
||||
| "percent";
|
||||
|
||||
const outOfHuman = HumaniseGoalProgress(
|
||||
goal.game,
|
||||
goal.criteria.key,
|
||||
goal.criteria.value,
|
||||
null
|
||||
);
|
||||
const outOfHuman = HumaniseGoalProgress(
|
||||
goal.game,
|
||||
goal.criteria.key,
|
||||
goal.criteria.value,
|
||||
null
|
||||
);
|
||||
|
||||
if (res) {
|
||||
return {
|
||||
achieved: true,
|
||||
outOf: goal.criteria.value,
|
||||
progress: res.scoreData[scoreDataKey],
|
||||
outOfHuman,
|
||||
progressHuman: HumaniseGoalProgress(
|
||||
goal.game,
|
||||
goal.criteria.key,
|
||||
res.scoreData[scoreDataKey],
|
||||
res
|
||||
),
|
||||
};
|
||||
}
|
||||
if (res) {
|
||||
return {
|
||||
achieved: true,
|
||||
outOf: goal.criteria.value,
|
||||
progress: res.scoreData[scoreDataKey],
|
||||
outOfHuman,
|
||||
progressHuman: HumaniseGoalProgress(
|
||||
goal.game,
|
||||
goal.criteria.key,
|
||||
res.scoreData[scoreDataKey],
|
||||
res
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// if we weren't successful, we have to get the users next best score and put it up here
|
||||
// this is made infinitely easier by the existance of personal-bests.
|
||||
const nextBestQuery: FilterQuery<PBScoreDocument> = {
|
||||
userID,
|
||||
};
|
||||
// if we weren't successful, we have to get the users next best score and put it up here
|
||||
// this is made infinitely easier by the existance of personal-bests.
|
||||
const nextBestQuery: FilterQuery<PBScoreDocument> = {
|
||||
userID,
|
||||
};
|
||||
|
||||
if (chartIDs) {
|
||||
nextBestQuery.chartID = { $in: chartIDs };
|
||||
}
|
||||
if (chartIDs) {
|
||||
nextBestQuery.chartID = { $in: chartIDs };
|
||||
}
|
||||
|
||||
const nextBestScore = await db["personal-bests"].findOne(nextBestQuery, {
|
||||
sort: { [goal.criteria.key]: -1 },
|
||||
});
|
||||
const nextBestScore = await db["personal-bests"].findOne(nextBestQuery, {
|
||||
sort: { [goal.criteria.key]: -1 },
|
||||
});
|
||||
|
||||
if (!nextBestScore) {
|
||||
return {
|
||||
achieved: false,
|
||||
outOf: goal.criteria.value,
|
||||
progress: null,
|
||||
outOfHuman,
|
||||
progressHuman: "NO DATA",
|
||||
};
|
||||
}
|
||||
if (!nextBestScore) {
|
||||
return {
|
||||
achieved: false,
|
||||
outOf: goal.criteria.value,
|
||||
progress: null,
|
||||
outOfHuman,
|
||||
progressHuman: "NO DATA",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
achieved: false,
|
||||
outOf: goal.criteria.value,
|
||||
outOfHuman,
|
||||
progress: nextBestScore.scoreData[scoreDataKey],
|
||||
progressHuman: HumaniseGoalProgress(
|
||||
goal.game,
|
||||
goal.criteria.key,
|
||||
nextBestScore.scoreData[scoreDataKey],
|
||||
nextBestScore
|
||||
),
|
||||
};
|
||||
} else if (goal.criteria.mode === "abs" || goal.criteria.mode === "proportion") {
|
||||
let count;
|
||||
return {
|
||||
achieved: false,
|
||||
outOf: goal.criteria.value,
|
||||
outOfHuman,
|
||||
progress: nextBestScore.scoreData[scoreDataKey],
|
||||
progressHuman: HumaniseGoalProgress(
|
||||
goal.game,
|
||||
goal.criteria.key,
|
||||
nextBestScore.scoreData[scoreDataKey],
|
||||
nextBestScore
|
||||
),
|
||||
};
|
||||
} else if (goal.criteria.mode === "abs" || goal.criteria.mode === "proportion") {
|
||||
let count;
|
||||
|
||||
// abs -> Absolute mode, such as clear 10 charts.
|
||||
if (goal.criteria.mode === "abs") {
|
||||
count = goal.criteria.countNum;
|
||||
} else {
|
||||
// proportion -> Proportional mode, the value
|
||||
// is a multiplier for the amount of charts
|
||||
// available -- i.e. 0.1 * charts.
|
||||
// abs -> Absolute mode, such as clear 10 charts.
|
||||
if (goal.criteria.mode === "abs") {
|
||||
count = goal.criteria.countNum;
|
||||
} else {
|
||||
// proportion -> Proportional mode, the value
|
||||
// is a multiplier for the amount of charts
|
||||
// available -- i.e. 0.1 * charts.
|
||||
|
||||
let totalChartCount;
|
||||
let totalChartCount;
|
||||
|
||||
if (chartIDs === null) {
|
||||
// edge case: proportion goals on "any"
|
||||
// charts (i.e. clear 20% of charts) need to
|
||||
// know how many charts the game has!
|
||||
totalChartCount = await db.charts[goal.game].count({ playtype: goal.playtype });
|
||||
} else {
|
||||
totalChartCount = chartIDs.length;
|
||||
}
|
||||
if (chartIDs === null) {
|
||||
// edge case: proportion goals on "any"
|
||||
// charts (i.e. clear 20% of charts) need to
|
||||
// know how many charts the game has!
|
||||
totalChartCount = await db.charts[goal.game].count({ playtype: goal.playtype });
|
||||
} else {
|
||||
totalChartCount = chartIDs.length;
|
||||
}
|
||||
|
||||
count = Math.floor(goal.criteria.countNum * totalChartCount);
|
||||
}
|
||||
count = Math.floor(goal.criteria.countNum * totalChartCount);
|
||||
}
|
||||
|
||||
const userCount = await db["personal-bests"].count(scoreQuery);
|
||||
const userCount = await db["personal-bests"].count(scoreQuery);
|
||||
|
||||
return {
|
||||
achieved: userCount >= count,
|
||||
progress: userCount,
|
||||
outOf: count,
|
||||
progressHuman: userCount.toString(),
|
||||
outOfHuman: userCount.toString(),
|
||||
};
|
||||
}
|
||||
return {
|
||||
achieved: userCount >= count,
|
||||
progress: userCount,
|
||||
outOf: count,
|
||||
progressHuman: userCount.toString(),
|
||||
outOfHuman: userCount.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
logger.error(
|
||||
`Invalid goal: ${goal.goalID}, unknown criteria.mode ${goal.criteria.mode}, ignoring.`
|
||||
);
|
||||
logger.error(
|
||||
`Invalid goal: ${goal.goalID}, unknown criteria.mode ${goal.criteria.mode}, ignoring.`
|
||||
);
|
||||
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -174,15 +174,15 @@ export async function EvaluateGoalForUser(
|
||||
* @returns An array of chartIDs, except if the goal chart type is "any", in which case, it returns null.
|
||||
*/
|
||||
function ResolveGoalCharts(goal: GoalDocument): Promise<string[]> | string[] | null | undefined {
|
||||
if (goal.charts.type === "single") {
|
||||
return [goal.charts.data];
|
||||
} else if (goal.charts.type === "multi") {
|
||||
return goal.charts.data;
|
||||
} else if (goal.charts.type === "folder") {
|
||||
return GetFolderChartIDs(goal.charts.data);
|
||||
} else if (goal.charts.type === "any") {
|
||||
return null; // special case.
|
||||
}
|
||||
if (goal.charts.type === "single") {
|
||||
return [goal.charts.data];
|
||||
} else if (goal.charts.type === "multi") {
|
||||
return goal.charts.data;
|
||||
} else if (goal.charts.type === "folder") {
|
||||
return GetFolderChartIDs(goal.charts.data);
|
||||
} else if (goal.charts.type === "any") {
|
||||
return null; // special case.
|
||||
}
|
||||
}
|
||||
|
||||
type GoalKeys = GoalDocument["criteria"]["key"];
|
||||
@@ -191,26 +191,26 @@ 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,
|
||||
key: GoalKeys,
|
||||
value: number,
|
||||
userPB: PBScoreDocument | null
|
||||
game: Game,
|
||||
key: GoalKeys,
|
||||
value: number,
|
||||
userPB: PBScoreDocument | null
|
||||
): string {
|
||||
switch (key) {
|
||||
case "scoreData.gradeIndex":
|
||||
return grades[game][value];
|
||||
case "scoreData.lampIndex":
|
||||
if (userPB && (game === "iidx" || game === "bms")) {
|
||||
return `${lamps[game][value]} (BP: ${
|
||||
(userPB as IIDXOrBMSPB).scoreData.hitMeta.bp ?? "N/A"
|
||||
})`;
|
||||
}
|
||||
return lamps[game][value];
|
||||
case "scoreData.percent":
|
||||
return `${value.toFixed(2)}%`;
|
||||
case "scoreData.score":
|
||||
return value.toString();
|
||||
default:
|
||||
throw new Error(`Broken goal - invalid key ${key}.`);
|
||||
}
|
||||
switch (key) {
|
||||
case "scoreData.gradeIndex":
|
||||
return grades[game][value];
|
||||
case "scoreData.lampIndex":
|
||||
if (userPB && (game === "iidx" || game === "bms")) {
|
||||
return `${lamps[game][value]} (BP: ${
|
||||
(userPB as IIDXOrBMSPB).scoreData.hitMeta.bp ?? "N/A"
|
||||
})`;
|
||||
}
|
||||
return lamps[game][value];
|
||||
case "scoreData.percent":
|
||||
return `${value.toFixed(2)}%`;
|
||||
case "scoreData.score":
|
||||
return value.toString();
|
||||
default:
|
||||
throw new Error(`Broken goal - invalid key ${key}.`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,16 +14,16 @@ const logger = CreateLogCtx(__filename);
|
||||
* Joins a file location against the location of the CDN static store.
|
||||
*/
|
||||
function CDNRoot(fileLoc: string) {
|
||||
return path.join(KTCDN_ROOT, fileLoc);
|
||||
return path.join(KTCDN_ROOT, fileLoc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the data of the file at the given CDN location.
|
||||
*/
|
||||
export function RetrieveCDN(fileLoc: string) {
|
||||
logger.debug(`Retrieving path ${fileLoc}.`);
|
||||
logger.debug(`Retrieving path ${fileLoc}.`);
|
||||
|
||||
return readFilePromise(CDNRoot(fileLoc));
|
||||
return readFilePromise(CDNRoot(fileLoc));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,12 +36,12 @@ const WRITE_NO_OVERWRITE = "wx";
|
||||
* @returns Nothing on success. Throws on error.
|
||||
*/
|
||||
export async function StoreCDN(fileLoc: string, data: Buffer | string) {
|
||||
logger.debug(`Storing path ${fileLoc}.`);
|
||||
logger.debug(`Storing path ${fileLoc}.`);
|
||||
|
||||
const loc = CDNRoot(fileLoc);
|
||||
const loc = CDNRoot(fileLoc);
|
||||
|
||||
// make the parent folders if they dont exist. else, mkdirp is a no-op.
|
||||
await mkdirp(path.dirname(loc));
|
||||
// make the parent folders if they dont exist. else, mkdirp is a no-op.
|
||||
await mkdirp(path.dirname(loc));
|
||||
|
||||
return writeFilePromise(loc, data, { flag: WRITE_NO_OVERWRITE });
|
||||
return writeFilePromise(loc, data, { flag: WRITE_NO_OVERWRITE });
|
||||
}
|
||||
|
||||
+136
-136
@@ -1,161 +1,161 @@
|
||||
export enum IIDXClasses {
|
||||
KYU_7,
|
||||
KYU_6,
|
||||
KYU_5,
|
||||
KYU_4,
|
||||
KYU_3,
|
||||
KYU_2,
|
||||
KYU_1,
|
||||
KYU_7,
|
||||
KYU_6,
|
||||
KYU_5,
|
||||
KYU_4,
|
||||
KYU_3,
|
||||
KYU_2,
|
||||
KYU_1,
|
||||
|
||||
DAN_1,
|
||||
DAN_2,
|
||||
DAN_3,
|
||||
DAN_4,
|
||||
DAN_5,
|
||||
DAN_6,
|
||||
DAN_7,
|
||||
DAN_8,
|
||||
DAN_9,
|
||||
DAN_10,
|
||||
DAN_1,
|
||||
DAN_2,
|
||||
DAN_3,
|
||||
DAN_4,
|
||||
DAN_5,
|
||||
DAN_6,
|
||||
DAN_7,
|
||||
DAN_8,
|
||||
DAN_9,
|
||||
DAN_10,
|
||||
|
||||
CHUUDEN,
|
||||
KAIDEN,
|
||||
CHUUDEN,
|
||||
KAIDEN,
|
||||
}
|
||||
|
||||
export enum GitadoraColours {
|
||||
WHITE,
|
||||
WHITE,
|
||||
|
||||
ORANGE,
|
||||
ORANGE_GRADIENT,
|
||||
YELLOW,
|
||||
YELLOW_GRADIENT,
|
||||
GREEN,
|
||||
GREEN_GRADIENT,
|
||||
BLUE,
|
||||
BLUE_GRADIENT,
|
||||
PURPLE,
|
||||
PURPLE_GRADIENT,
|
||||
RED,
|
||||
RED_GRADIENT,
|
||||
ORANGE,
|
||||
ORANGE_GRADIENT,
|
||||
YELLOW,
|
||||
YELLOW_GRADIENT,
|
||||
GREEN,
|
||||
GREEN_GRADIENT,
|
||||
BLUE,
|
||||
BLUE_GRADIENT,
|
||||
PURPLE,
|
||||
PURPLE_GRADIENT,
|
||||
RED,
|
||||
RED_GRADIENT,
|
||||
|
||||
BRONZE,
|
||||
SILVER,
|
||||
GOLD,
|
||||
RAINBOW,
|
||||
BRONZE,
|
||||
SILVER,
|
||||
GOLD,
|
||||
RAINBOW,
|
||||
}
|
||||
|
||||
export enum BMSGenocideDans {
|
||||
NORMAL_1,
|
||||
NORMAL_2,
|
||||
NORMAL_3,
|
||||
NORMAL_4,
|
||||
NORMAL_5,
|
||||
NORMAL_6,
|
||||
NORMAL_7,
|
||||
NORMAL_8,
|
||||
NORMAL_9,
|
||||
NORMAL_10,
|
||||
NORMAL_1,
|
||||
NORMAL_2,
|
||||
NORMAL_3,
|
||||
NORMAL_4,
|
||||
NORMAL_5,
|
||||
NORMAL_6,
|
||||
NORMAL_7,
|
||||
NORMAL_8,
|
||||
NORMAL_9,
|
||||
NORMAL_10,
|
||||
|
||||
INSANE_1,
|
||||
INSANE_2,
|
||||
INSANE_3,
|
||||
INSANE_4,
|
||||
INSANE_5,
|
||||
INSANE_6,
|
||||
INSANE_7,
|
||||
INSANE_8,
|
||||
INSANE_9,
|
||||
INSANE_10,
|
||||
INSANE_KAIDEN,
|
||||
INSANE_1,
|
||||
INSANE_2,
|
||||
INSANE_3,
|
||||
INSANE_4,
|
||||
INSANE_5,
|
||||
INSANE_6,
|
||||
INSANE_7,
|
||||
INSANE_8,
|
||||
INSANE_9,
|
||||
INSANE_10,
|
||||
INSANE_KAIDEN,
|
||||
|
||||
OVERJOY,
|
||||
OVERJOY,
|
||||
}
|
||||
|
||||
export enum BMSStSlDans {
|
||||
SL_0,
|
||||
SL_1,
|
||||
SL_2,
|
||||
SL_3,
|
||||
SL_4,
|
||||
SL_5,
|
||||
SL_6,
|
||||
SL_7,
|
||||
SL_8,
|
||||
SL_9,
|
||||
SL_10,
|
||||
SL_11,
|
||||
SL_12,
|
||||
SL_0,
|
||||
SL_1,
|
||||
SL_2,
|
||||
SL_3,
|
||||
SL_4,
|
||||
SL_5,
|
||||
SL_6,
|
||||
SL_7,
|
||||
SL_8,
|
||||
SL_9,
|
||||
SL_10,
|
||||
SL_11,
|
||||
SL_12,
|
||||
|
||||
ST_0,
|
||||
ST_1,
|
||||
ST_2,
|
||||
ST_3,
|
||||
ST_4,
|
||||
ST_5,
|
||||
ST_6,
|
||||
ST_7,
|
||||
ST_8,
|
||||
ST_9,
|
||||
ST_10,
|
||||
ST_11,
|
||||
ST_12,
|
||||
ST_0,
|
||||
ST_1,
|
||||
ST_2,
|
||||
ST_3,
|
||||
ST_4,
|
||||
ST_5,
|
||||
ST_6,
|
||||
ST_7,
|
||||
ST_8,
|
||||
ST_9,
|
||||
ST_10,
|
||||
ST_11,
|
||||
ST_12,
|
||||
}
|
||||
|
||||
export enum SDVXDans {
|
||||
DAN_1,
|
||||
DAN_2,
|
||||
DAN_3,
|
||||
DAN_4,
|
||||
DAN_5,
|
||||
DAN_6,
|
||||
DAN_7,
|
||||
DAN_8,
|
||||
DAN_9,
|
||||
DAN_10,
|
||||
DAN_11,
|
||||
INF,
|
||||
DAN_1,
|
||||
DAN_2,
|
||||
DAN_3,
|
||||
DAN_4,
|
||||
DAN_5,
|
||||
DAN_6,
|
||||
DAN_7,
|
||||
DAN_8,
|
||||
DAN_9,
|
||||
DAN_10,
|
||||
DAN_11,
|
||||
INF,
|
||||
}
|
||||
|
||||
// ewww
|
||||
export enum SDVXVFClasses {
|
||||
SIENNA_I,
|
||||
SIENNA_II,
|
||||
SIENNA_III,
|
||||
SIENNA_IV,
|
||||
COBALT_I,
|
||||
COBALT_II,
|
||||
COBALT_III,
|
||||
COBALT_IV,
|
||||
DANDELION_I,
|
||||
DANDELION_II,
|
||||
DANDELION_III,
|
||||
DANDELION_IV,
|
||||
CYAN_I,
|
||||
CYAN_II,
|
||||
CYAN_III,
|
||||
CYAN_IV,
|
||||
SCARLET_I,
|
||||
SCARLET_II,
|
||||
SCARLET_III,
|
||||
SCARLET_IV,
|
||||
CORAL_I,
|
||||
CORAL_II,
|
||||
CORAL_III,
|
||||
CORAL_IV,
|
||||
ARGENTO_I,
|
||||
ARGENTO_II,
|
||||
ARGENTO_III,
|
||||
ARGENTO_IV,
|
||||
ELDORA_I,
|
||||
ELDORA_II,
|
||||
ELDORA_III,
|
||||
ELDORA_IV,
|
||||
CRIMSON_I,
|
||||
CRIMSON_II,
|
||||
CRIMSON_III,
|
||||
CRIMSON_IV,
|
||||
IMPERIAL_I,
|
||||
IMPERIAL_II,
|
||||
IMPERIAL_III,
|
||||
IMPERIAL_IV,
|
||||
SIENNA_I,
|
||||
SIENNA_II,
|
||||
SIENNA_III,
|
||||
SIENNA_IV,
|
||||
COBALT_I,
|
||||
COBALT_II,
|
||||
COBALT_III,
|
||||
COBALT_IV,
|
||||
DANDELION_I,
|
||||
DANDELION_II,
|
||||
DANDELION_III,
|
||||
DANDELION_IV,
|
||||
CYAN_I,
|
||||
CYAN_II,
|
||||
CYAN_III,
|
||||
CYAN_IV,
|
||||
SCARLET_I,
|
||||
SCARLET_II,
|
||||
SCARLET_III,
|
||||
SCARLET_IV,
|
||||
CORAL_I,
|
||||
CORAL_II,
|
||||
CORAL_III,
|
||||
CORAL_IV,
|
||||
ARGENTO_I,
|
||||
ARGENTO_II,
|
||||
ARGENTO_III,
|
||||
ARGENTO_IV,
|
||||
ELDORA_I,
|
||||
ELDORA_II,
|
||||
ELDORA_III,
|
||||
ELDORA_IV,
|
||||
CRIMSON_I,
|
||||
CRIMSON_II,
|
||||
CRIMSON_III,
|
||||
CRIMSON_IV,
|
||||
IMPERIAL_I,
|
||||
IMPERIAL_II,
|
||||
IMPERIAL_III,
|
||||
IMPERIAL_IV,
|
||||
}
|
||||
|
||||
@@ -11,29 +11,29 @@ const PATCH = 0;
|
||||
// The fact that this album is very similar to Mezzanine
|
||||
// - the album used for Kamaitachi v1 - is deliberate. :) - zkldi
|
||||
const PORTISHEAD_DUMMY = [
|
||||
"Mysterons", // v2.0.0
|
||||
"Sour Times", // v2.1.0
|
||||
"Strangers", // v2.2.0
|
||||
"It Could Be Sweet", // v2.3.0
|
||||
"Wandering Star", // v2.4.0
|
||||
"It's a Fire", // v2.5.0
|
||||
"Numb", // v2.6.0
|
||||
"Roads", // v2.7.0
|
||||
"Pedestal", // v2.8.0
|
||||
"Biscuit", // v2.9.0
|
||||
"Glory Box", // v2.10.0
|
||||
// v2.11+ onwards are not expected to occur, but if they do, we'll figure something out.
|
||||
"Mysterons", // v2.0.0
|
||||
"Sour Times", // v2.1.0
|
||||
"Strangers", // v2.2.0
|
||||
"It Could Be Sweet", // v2.3.0
|
||||
"Wandering Star", // v2.4.0
|
||||
"It's a Fire", // v2.5.0
|
||||
"Numb", // v2.6.0
|
||||
"Roads", // v2.7.0
|
||||
"Pedestal", // v2.8.0
|
||||
"Biscuit", // v2.9.0
|
||||
"Glory Box", // v2.10.0
|
||||
// v2.11+ onwards are not expected to occur, but if they do, we'll figure something out.
|
||||
];
|
||||
|
||||
export const VERSION_INFO = {
|
||||
major: MAJOR,
|
||||
minor: MINOR,
|
||||
patch: PATCH,
|
||||
name: PORTISHEAD_DUMMY[MINOR],
|
||||
major: MAJOR,
|
||||
minor: MINOR,
|
||||
patch: PATCH,
|
||||
name: PORTISHEAD_DUMMY[MINOR],
|
||||
};
|
||||
|
||||
export const VERSION_STR = `${MAJOR}.${MINOR}.${PATCH}`;
|
||||
|
||||
export function FormatVersion() {
|
||||
return `v${VERSION_STR} (${VERSION_INFO.name})`;
|
||||
return `v${VERSION_STR} (${VERSION_INFO.name})`;
|
||||
}
|
||||
|
||||
@@ -3,23 +3,23 @@ import { CloseAllConnections } from "../../test-utils/close-connections";
|
||||
import CreateLogCtx, { Transports } from "./logger";
|
||||
|
||||
t.test("Logger Tests", (t) => {
|
||||
const logger = CreateLogCtx(__filename);
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
Transports[0].level = "debug"; // lol
|
||||
Transports[0].level = "debug"; // lol
|
||||
|
||||
logger.debug("Debug Message Test");
|
||||
logger.verbose("Verbose Message Test");
|
||||
logger.info("Info Message Test");
|
||||
logger.warn("Warning Message Test");
|
||||
logger.error("Error Message Test");
|
||||
logger.severe("Severe Message Test");
|
||||
logger.crit("Critical Message Test");
|
||||
logger.debug("Debug Message Test");
|
||||
logger.verbose("Verbose Message Test");
|
||||
logger.info("Info Message Test");
|
||||
logger.warn("Warning Message Test");
|
||||
logger.error("Error Message Test");
|
||||
logger.severe("Severe Message Test");
|
||||
logger.crit("Critical Message Test");
|
||||
|
||||
Transports[0].level = process.env.LOG_LEVEL ?? "info";
|
||||
Transports[0].level = process.env.LOG_LEVEL ?? "info";
|
||||
|
||||
logger.debug("This message shouldn't appear.");
|
||||
logger.debug("This message shouldn't appear.");
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -10,150 +10,150 @@ const level = LOG_LEVEL ?? "info";
|
||||
const IN_TESTING = process.env.NODE_ENV === "test";
|
||||
|
||||
const formatExcessProperties = (meta: Record<string, unknown>) => {
|
||||
let i = 0;
|
||||
for (const key in meta) {
|
||||
const val = meta[key];
|
||||
let i = 0;
|
||||
for (const key in meta) {
|
||||
const val = meta[key];
|
||||
|
||||
if (val instanceof Error) {
|
||||
meta[key] = { message: val.message, stack: val.stack };
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (val instanceof Error) {
|
||||
meta[key] = { message: val.message, stack: val.stack };
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
if (!i) {
|
||||
return "";
|
||||
}
|
||||
if (!i) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return ` ${JSON.stringify(meta)}`;
|
||||
return ` ${JSON.stringify(meta)}`;
|
||||
};
|
||||
|
||||
const formatExcessPropertiesNoStack = (meta: Record<string, unknown>, omitKeys: string[] = []) => {
|
||||
let i = 0;
|
||||
const realMeta: Record<string, unknown> = {};
|
||||
let i = 0;
|
||||
const realMeta: Record<string, unknown> = {};
|
||||
|
||||
for (const key in meta) {
|
||||
if (omitKeys.includes(key)) {
|
||||
continue;
|
||||
}
|
||||
for (const key in meta) {
|
||||
if (omitKeys.includes(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const val = meta[key];
|
||||
const val = meta[key];
|
||||
|
||||
if (val instanceof Error) {
|
||||
realMeta[key] = { message: val.message };
|
||||
} else {
|
||||
realMeta[key] = val;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (val instanceof Error) {
|
||||
realMeta[key] = { message: val.message };
|
||||
} else {
|
||||
realMeta[key] = val;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
if (!i) {
|
||||
return "";
|
||||
}
|
||||
if (!i) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return ` ${JSON.stringify(realMeta)}`;
|
||||
return ` ${JSON.stringify(realMeta)}`;
|
||||
};
|
||||
|
||||
const ktblackPrintf = format.printf(
|
||||
({ level, message, context = "ktblack-root", timestamp, ...meta }) =>
|
||||
`${timestamp} [${
|
||||
Array.isArray(context) ? context.join(" | ") : context
|
||||
}] ${level}: ${message}${formatExcessProperties(meta)}`
|
||||
({ level, message, context = "ktblack-root", timestamp, ...meta }) =>
|
||||
`${timestamp} [${
|
||||
Array.isArray(context) ? context.join(" | ") : context
|
||||
}] ${level}: ${message}${formatExcessProperties(meta)}`
|
||||
);
|
||||
|
||||
const ktblackConsolePrintf = format.printf(
|
||||
({ level, message, context = "ktblack-root", timestamp, hideFromConsole, ...meta }) =>
|
||||
`${timestamp} [${
|
||||
Array.isArray(context) ? context.join(" | ") : context
|
||||
}] ${level}: ${message}${formatExcessPropertiesNoStack(meta, hideFromConsole)}`
|
||||
({ level, message, context = "ktblack-root", timestamp, hideFromConsole, ...meta }) =>
|
||||
`${timestamp} [${
|
||||
Array.isArray(context) ? context.join(" | ") : context
|
||||
}] ${level}: ${message}${formatExcessPropertiesNoStack(meta, hideFromConsole)}`
|
||||
);
|
||||
|
||||
winston.addColors({
|
||||
crit: ["bgRed", "black"],
|
||||
severe: ["bgBrightRed", "black"],
|
||||
error: ["red"],
|
||||
warn: ["yellow"],
|
||||
info: ["blue"],
|
||||
verbose: ["cyan"],
|
||||
debug: ["white"],
|
||||
crit: ["bgRed", "black"],
|
||||
severe: ["bgBrightRed", "black"],
|
||||
error: ["red"],
|
||||
warn: ["yellow"],
|
||||
info: ["blue"],
|
||||
verbose: ["cyan"],
|
||||
debug: ["white"],
|
||||
});
|
||||
|
||||
const baseFormatRoute = format.combine(
|
||||
format.timestamp({
|
||||
format: "YYYY-MM-DD HH:mm:ss",
|
||||
})
|
||||
format.timestamp({
|
||||
format: "YYYY-MM-DD HH:mm:ss",
|
||||
})
|
||||
);
|
||||
|
||||
const defaultFormatRoute = format.combine(
|
||||
baseFormatRoute,
|
||||
format.errors({ stack: false }),
|
||||
ktblackPrintf
|
||||
baseFormatRoute,
|
||||
format.errors({ stack: false }),
|
||||
ktblackPrintf
|
||||
);
|
||||
|
||||
const consoleFormatRoute = format.combine(
|
||||
baseFormatRoute,
|
||||
format.errors({ stack: false }),
|
||||
ktblackConsolePrintf,
|
||||
format.colorize({
|
||||
all: true,
|
||||
})
|
||||
baseFormatRoute,
|
||||
format.errors({ stack: false }),
|
||||
ktblackConsolePrintf,
|
||||
format.colorize({
|
||||
all: true,
|
||||
})
|
||||
);
|
||||
|
||||
let tports = [];
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (IN_TESTING) {
|
||||
tports = [
|
||||
new transports.Console({
|
||||
format: consoleFormatRoute,
|
||||
}),
|
||||
];
|
||||
tports = [
|
||||
new transports.Console({
|
||||
format: consoleFormatRoute,
|
||||
}),
|
||||
];
|
||||
} else {
|
||||
tports = [
|
||||
new transports.File({
|
||||
filename: "logs/ktblack-error.log",
|
||||
level: "error",
|
||||
format: defaultFormatRoute,
|
||||
}),
|
||||
new transports.File({ filename: "logs/ktblack.log", format: defaultFormatRoute }),
|
||||
new transports.Console({
|
||||
format: consoleFormatRoute,
|
||||
}),
|
||||
];
|
||||
tports = [
|
||||
new transports.File({
|
||||
filename: "logs/ktblack-error.log",
|
||||
level: "error",
|
||||
format: defaultFormatRoute,
|
||||
}),
|
||||
new transports.File({ filename: "logs/ktblack.log", format: defaultFormatRoute }),
|
||||
new transports.Console({
|
||||
format: consoleFormatRoute,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
export const rootLogger = winston.createLogger({
|
||||
levels: {
|
||||
crit: 0, // entire process termination is necessary
|
||||
severe: 1, // something is wrong, and more than one function is affected (such as a failed assertion that is definitely expected to be true).
|
||||
error: 2, // function call (or related process) has failed unexpectedly
|
||||
warn: 3, // function call has hit something it didn't want, but can recover
|
||||
info: 4, // something has happened that is expected, but worth logging
|
||||
verbose: 5, // something has happened
|
||||
debug: 6, // glorified console.log debugging
|
||||
},
|
||||
level,
|
||||
format: defaultFormatRoute,
|
||||
transports: tports,
|
||||
levels: {
|
||||
crit: 0, // entire process termination is necessary
|
||||
severe: 1, // something is wrong, and more than one function is affected (such as a failed assertion that is definitely expected to be true).
|
||||
error: 2, // function call (or related process) has failed unexpectedly
|
||||
warn: 3, // function call has hit something it didn't want, but can recover
|
||||
info: 4, // something has happened that is expected, but worth logging
|
||||
verbose: 5, // something has happened
|
||||
debug: 6, // glorified console.log debugging
|
||||
},
|
||||
level,
|
||||
format: defaultFormatRoute,
|
||||
transports: tports,
|
||||
});
|
||||
|
||||
function CreateLogCtx(filename: string, lg = rootLogger): KtLogger {
|
||||
const replacedFilename = filename.replace(
|
||||
new RegExp(`^${EscapeStringRegexp(process.cwd())}/((js|src)/)?`, "u"),
|
||||
""
|
||||
);
|
||||
const replacedFilename = filename.replace(
|
||||
new RegExp(`^${EscapeStringRegexp(process.cwd())}/((js|src)/)?`, "u"),
|
||||
""
|
||||
);
|
||||
|
||||
const logger = lg.child({
|
||||
context: [replacedFilename],
|
||||
}) as KtLogger;
|
||||
const logger = lg.child({
|
||||
context: [replacedFilename],
|
||||
}) as KtLogger;
|
||||
|
||||
logger.defaultMeta = { context: [replacedFilename] };
|
||||
return logger;
|
||||
logger.defaultMeta = { context: [replacedFilename] };
|
||||
return logger;
|
||||
}
|
||||
|
||||
export function AppendLogCtx(context: string, lg: KtLogger): KtLogger {
|
||||
const newContext = [...lg.defaultMeta.context, context];
|
||||
const newContext = [...lg.defaultMeta.context, context];
|
||||
|
||||
return lg.child({ context: newContext }) as KtLogger;
|
||||
return lg.child({ context: newContext }) as KtLogger;
|
||||
}
|
||||
|
||||
export const Transports = tports;
|
||||
|
||||
@@ -4,11 +4,11 @@ import db from "../../../../external/mongo/db";
|
||||
import CreateLogCtx from "../../../logger/logger";
|
||||
import { prAssert } from "../../../../test-utils/asserts";
|
||||
import {
|
||||
Testing511SPA,
|
||||
TestingDoraChart,
|
||||
TestingGITADORADoraDryScore,
|
||||
TestingIIDXSPDryScore,
|
||||
TestingSDVXSingleDryScore,
|
||||
Testing511SPA,
|
||||
TestingDoraChart,
|
||||
TestingGITADORADoraDryScore,
|
||||
TestingIIDXSPDryScore,
|
||||
TestingSDVXSingleDryScore,
|
||||
} from "../../../../test-utils/test-data";
|
||||
import { CreateCalculatedData, CalculateDataForGamePT } from "./calculated-data";
|
||||
import deepmerge from "deepmerge";
|
||||
@@ -18,374 +18,374 @@ import { CloseAllConnections } from "../../../../test-utils/close-connections";
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
t.test("#CreateCalculatedData", async (t) => {
|
||||
const res = await CreateCalculatedData(TestingIIDXSPDryScore, Testing511SPA, 30, logger);
|
||||
const res = await CreateCalculatedData(TestingIIDXSPDryScore, Testing511SPA, 30, logger);
|
||||
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
ktRating: p.aprx(2.65),
|
||||
ktLampRating: p.equalTo(10),
|
||||
BPI: "?number",
|
||||
"K%": "?number",
|
||||
},
|
||||
"Should correctly produce calculatedData"
|
||||
);
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
ktRating: p.aprx(2.65),
|
||||
ktLampRating: p.equalTo(10),
|
||||
BPI: "?number",
|
||||
"K%": "?number",
|
||||
},
|
||||
"Should correctly produce calculatedData"
|
||||
);
|
||||
|
||||
const gitadoraRes = await CreateCalculatedData(
|
||||
TestingGITADORADoraDryScore,
|
||||
TestingDoraChart,
|
||||
30,
|
||||
logger
|
||||
);
|
||||
const gitadoraRes = await CreateCalculatedData(
|
||||
TestingGITADORADoraDryScore,
|
||||
TestingDoraChart,
|
||||
30,
|
||||
logger
|
||||
);
|
||||
|
||||
prAssert(
|
||||
gitadoraRes,
|
||||
{
|
||||
skill: p.isPositiveNonZero,
|
||||
},
|
||||
"Should correctly call rating function overrides for different games"
|
||||
);
|
||||
prAssert(
|
||||
gitadoraRes,
|
||||
{
|
||||
skill: p.isPositiveNonZero,
|
||||
},
|
||||
"Should correctly call rating function overrides for different games"
|
||||
);
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
/**
|
||||
* These tests only check that the right properties are assigned.
|
||||
*/
|
||||
t.test("#CalculateDataForGamePT", (t) => {
|
||||
const defaultTierlistID = undefined;
|
||||
t.test("IIDX:SP", async (t) => {
|
||||
const res = await CalculateDataForGamePT(
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
TestingIIDXSPDryScore,
|
||||
30,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
const defaultTierlistID = undefined;
|
||||
t.test("IIDX:SP", async (t) => {
|
||||
const res = await CalculateDataForGamePT(
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
TestingIIDXSPDryScore,
|
||||
30,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
ktRating: "?number",
|
||||
ktLampRating: "?number",
|
||||
BPI: "?number",
|
||||
"K%": "?number",
|
||||
},
|
||||
"Response should contain keys for IIDX:SP"
|
||||
);
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
ktRating: "?number",
|
||||
ktLampRating: "?number",
|
||||
BPI: "?number",
|
||||
"K%": "?number",
|
||||
},
|
||||
"Response should contain keys for IIDX:SP"
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("IIDX:DP", async (t) => {
|
||||
const res = await CalculateDataForGamePT(
|
||||
"iidx",
|
||||
"DP",
|
||||
Testing511SPA,
|
||||
TestingIIDXSPDryScore, // fake! this is an SP score. but we're testing
|
||||
30,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
t.test("IIDX:DP", async (t) => {
|
||||
const res = await CalculateDataForGamePT(
|
||||
"iidx",
|
||||
"DP",
|
||||
Testing511SPA,
|
||||
TestingIIDXSPDryScore, // fake! this is an SP score. but we're testing
|
||||
30,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
ktRating: "?number",
|
||||
ktLampRating: "?number",
|
||||
BPI: "?number",
|
||||
},
|
||||
"Response should contain keys for IIDX:DP"
|
||||
);
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
ktRating: "?number",
|
||||
ktLampRating: "?number",
|
||||
BPI: "?number",
|
||||
},
|
||||
"Response should contain keys for IIDX:DP"
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("SDVX:Single", async (t) => {
|
||||
const res = await CalculateDataForGamePT(
|
||||
"sdvx",
|
||||
"Single",
|
||||
Testing511SPA,
|
||||
TestingSDVXSingleDryScore,
|
||||
null,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
t.test("SDVX:Single", async (t) => {
|
||||
const res = await CalculateDataForGamePT(
|
||||
"sdvx",
|
||||
"Single",
|
||||
Testing511SPA,
|
||||
TestingSDVXSingleDryScore,
|
||||
null,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
VF6: p.nullable(p.isPositive),
|
||||
},
|
||||
"Response should contain keys for SDVX:Single"
|
||||
);
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
VF6: p.nullable(p.isPositive),
|
||||
},
|
||||
"Response should contain keys for SDVX:Single"
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("DDR:SP", async (t) => {
|
||||
const res = await CalculateDataForGamePT(
|
||||
"ddr",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing
|
||||
null,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
t.test("DDR:SP", async (t) => {
|
||||
const res = await CalculateDataForGamePT(
|
||||
"ddr",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing
|
||||
null,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
MFCP: p.nullable(p.isPositiveInteger),
|
||||
ktRating: "?number",
|
||||
},
|
||||
"Response should contain keys for DDR:SP"
|
||||
);
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
MFCP: p.nullable(p.isPositiveInteger),
|
||||
ktRating: "?number",
|
||||
},
|
||||
"Response should contain keys for DDR:SP"
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("DDR:DP", async (t) => {
|
||||
const res = await CalculateDataForGamePT(
|
||||
"ddr",
|
||||
"DP",
|
||||
Testing511SPA,
|
||||
TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing
|
||||
null,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
t.test("DDR:DP", async (t) => {
|
||||
const res = await CalculateDataForGamePT(
|
||||
"ddr",
|
||||
"DP",
|
||||
Testing511SPA,
|
||||
TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing
|
||||
null,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
MFCP: "null",
|
||||
ktRating: "?number",
|
||||
},
|
||||
"Response should contain nulled keys for DDR:DP"
|
||||
);
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
MFCP: "null",
|
||||
ktRating: "?number",
|
||||
},
|
||||
"Response should contain nulled keys for DDR:DP"
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("chunithm:Single", async (t) => {
|
||||
const res = await CalculateDataForGamePT(
|
||||
"chunithm",
|
||||
"Single",
|
||||
Testing511SPA,
|
||||
TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing
|
||||
null,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
t.test("chunithm:Single", async (t) => {
|
||||
const res = await CalculateDataForGamePT(
|
||||
"chunithm",
|
||||
"Single",
|
||||
Testing511SPA,
|
||||
TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing
|
||||
null,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
rating: "?number",
|
||||
},
|
||||
"Response should contain nulled keys for chunithm:Single"
|
||||
);
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
rating: "?number",
|
||||
},
|
||||
"Response should contain nulled keys for chunithm:Single"
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("maimai:Single", async (t) => {
|
||||
const res = await CalculateDataForGamePT(
|
||||
"maimai",
|
||||
"Single",
|
||||
Testing511SPA,
|
||||
TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing
|
||||
null,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
t.test("maimai:Single", async (t) => {
|
||||
const res = await CalculateDataForGamePT(
|
||||
"maimai",
|
||||
"Single",
|
||||
Testing511SPA,
|
||||
TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing
|
||||
null,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
ktRating: "?number",
|
||||
},
|
||||
"Response should contain nulled keys for maimai:Single"
|
||||
);
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
ktRating: "?number",
|
||||
},
|
||||
"Response should contain nulled keys for maimai:Single"
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("museca:Single", async (t) => {
|
||||
const res = await CalculateDataForGamePT(
|
||||
"museca",
|
||||
"Single",
|
||||
Testing511SPA,
|
||||
TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing
|
||||
null,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
t.test("museca:Single", async (t) => {
|
||||
const res = await CalculateDataForGamePT(
|
||||
"museca",
|
||||
"Single",
|
||||
Testing511SPA,
|
||||
TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing
|
||||
null,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
ktRating: "?number",
|
||||
},
|
||||
"Response should contain nulled keys for museca:Single"
|
||||
);
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
ktRating: "?number",
|
||||
},
|
||||
"Response should contain nulled keys for museca:Single"
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("bms:7K", async (t) => {
|
||||
const res = await CalculateDataForGamePT(
|
||||
"bms",
|
||||
"7K",
|
||||
Testing511SPA,
|
||||
TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing
|
||||
null,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
t.test("bms:7K", async (t) => {
|
||||
const res = await CalculateDataForGamePT(
|
||||
"bms",
|
||||
"7K",
|
||||
Testing511SPA,
|
||||
TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing
|
||||
null,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
ktLampRating: "?number",
|
||||
},
|
||||
"Response should contain nulled keys for bms:7K"
|
||||
);
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
ktLampRating: "?number",
|
||||
},
|
||||
"Response should contain nulled keys for bms:7K"
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("bms:14K", async (t) => {
|
||||
const res = await CalculateDataForGamePT(
|
||||
"bms",
|
||||
"14K",
|
||||
Testing511SPA,
|
||||
TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing
|
||||
null,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
t.test("bms:14K", async (t) => {
|
||||
const res = await CalculateDataForGamePT(
|
||||
"bms",
|
||||
"14K",
|
||||
Testing511SPA,
|
||||
TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing
|
||||
null,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
ktLampRating: "?number",
|
||||
},
|
||||
"Response should contain nulled keys for bms:14K"
|
||||
);
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
ktLampRating: "?number",
|
||||
},
|
||||
"Response should contain nulled keys for bms:14K"
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("gitadora:Gita", async (t) => {
|
||||
const res = await CalculateDataForGamePT(
|
||||
"gitadora",
|
||||
"Gita",
|
||||
Testing511SPA,
|
||||
TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing
|
||||
null,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
t.test("gitadora:Gita", async (t) => {
|
||||
const res = await CalculateDataForGamePT(
|
||||
"gitadora",
|
||||
"Gita",
|
||||
Testing511SPA,
|
||||
TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing
|
||||
null,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
skill: "?number",
|
||||
},
|
||||
"Response should contain nulled keys for gitadora:Gita"
|
||||
);
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
skill: "?number",
|
||||
},
|
||||
"Response should contain nulled keys for gitadora:Gita"
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("gitadora:Dora", async (t) => {
|
||||
const res = await CalculateDataForGamePT(
|
||||
"gitadora",
|
||||
"Dora",
|
||||
Testing511SPA,
|
||||
TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing
|
||||
null,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
t.test("gitadora:Dora", async (t) => {
|
||||
const res = await CalculateDataForGamePT(
|
||||
"gitadora",
|
||||
"Dora",
|
||||
Testing511SPA,
|
||||
TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing
|
||||
null,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
skill: "?number",
|
||||
},
|
||||
"Response should contain nulled keys for gitadora:Dora"
|
||||
);
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
skill: "?number",
|
||||
},
|
||||
"Response should contain nulled keys for gitadora:Dora"
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("usc:Single", async (t) => {
|
||||
const res = await CalculateDataForGamePT(
|
||||
"usc",
|
||||
"Single",
|
||||
Testing511SPA,
|
||||
TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing
|
||||
null,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
t.test("usc:Single", async (t) => {
|
||||
const res = await CalculateDataForGamePT(
|
||||
"usc",
|
||||
"Single",
|
||||
Testing511SPA,
|
||||
TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing
|
||||
null,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
VF6: "?number",
|
||||
},
|
||||
"Response should contain nulled keys for usc:Single"
|
||||
);
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
VF6: "?number",
|
||||
},
|
||||
"Response should contain nulled keys for usc:Single"
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
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();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {
|
||||
AnyChartDocument,
|
||||
Game,
|
||||
Playtypes,
|
||||
ScoreDocument,
|
||||
ChartDocument,
|
||||
Grades,
|
||||
Lamps,
|
||||
IDStrings,
|
||||
AnyChartDocument,
|
||||
Game,
|
||||
Playtypes,
|
||||
ScoreDocument,
|
||||
ChartDocument,
|
||||
Grades,
|
||||
Lamps,
|
||||
IDStrings,
|
||||
} from "tachi-common";
|
||||
import db from "../../../../external/mongo/db";
|
||||
import { HasOwnProperty } from "../../../../utils/misc";
|
||||
@@ -15,308 +15,308 @@ import { GetDefaultTierlist } from "../../../../utils/tierlist";
|
||||
import { KtLogger } from "../../../logger/logger";
|
||||
import { DryScore } from "../common/types";
|
||||
import {
|
||||
CalculateCHUNITHMRating,
|
||||
CalculateGITADORASkill,
|
||||
CalculateBPI,
|
||||
CalculateMFCP,
|
||||
KaidenPercentile,
|
||||
CalculateKTRating,
|
||||
CalculateKTLampRating,
|
||||
CalculateVF6,
|
||||
CalculateCHUNITHMRating,
|
||||
CalculateGITADORASkill,
|
||||
CalculateBPI,
|
||||
CalculateMFCP,
|
||||
KaidenPercentile,
|
||||
CalculateKTRating,
|
||||
CalculateKTLampRating,
|
||||
CalculateVF6,
|
||||
} from "./stats";
|
||||
|
||||
export async function CreateCalculatedData(
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument,
|
||||
esd: number | null,
|
||||
logger: KtLogger
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument,
|
||||
esd: number | null,
|
||||
logger: KtLogger
|
||||
): Promise<ScoreDocument["calculatedData"]> {
|
||||
const game = dryScore.game;
|
||||
const playtype = chart.playtype;
|
||||
const game = dryScore.game;
|
||||
const playtype = chart.playtype;
|
||||
|
||||
const defaultTierlist = await GetDefaultTierlist(game, playtype);
|
||||
const defaultTierlistID = defaultTierlist?.tierlistID; // tierlistID | undefined
|
||||
const defaultTierlist = await GetDefaultTierlist(game, playtype);
|
||||
const defaultTierlistID = defaultTierlist?.tierlistID; // tierlistID | undefined
|
||||
|
||||
const calculatedData = await CalculateDataForGamePT(
|
||||
game,
|
||||
playtype,
|
||||
chart,
|
||||
dryScore,
|
||||
esd,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
const calculatedData = await CalculateDataForGamePT(
|
||||
game,
|
||||
playtype,
|
||||
chart,
|
||||
dryScore,
|
||||
esd,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
|
||||
return calculatedData;
|
||||
return calculatedData;
|
||||
}
|
||||
|
||||
type CalculatedDataFunctions = {
|
||||
[G in Game]: {
|
||||
[P in Playtypes[G]]: (
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument,
|
||||
defaultTierlistID: string,
|
||||
logger: KtLogger
|
||||
) => Promise<ScoreDocument["calculatedData"]> | ScoreDocument["calculatedData"];
|
||||
};
|
||||
[G in Game]: {
|
||||
[P in Playtypes[G]]: (
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument,
|
||||
defaultTierlistID: string,
|
||||
logger: KtLogger
|
||||
) => Promise<ScoreDocument["calculatedData"]> | ScoreDocument["calculatedData"];
|
||||
};
|
||||
};
|
||||
|
||||
const CalculatedDataFunctions: CalculatedDataFunctions = {
|
||||
iidx: {
|
||||
SP: CalculateDataIIDXSP,
|
||||
DP: CalculateDataIIDXDP,
|
||||
},
|
||||
sdvx: {
|
||||
Single: CalculateDataSDVXorUSC,
|
||||
},
|
||||
popn: {
|
||||
"9B": () => ({}),
|
||||
},
|
||||
museca: {
|
||||
Single: CalculateDataMuseca,
|
||||
},
|
||||
chunithm: {
|
||||
Single: CalculateDataCHUNITHM,
|
||||
},
|
||||
maimai: {
|
||||
Single: CalculateDataMaimai,
|
||||
},
|
||||
gitadora: {
|
||||
Gita: CalculateDataGitadora,
|
||||
Dora: CalculateDataGitadora,
|
||||
},
|
||||
bms: {
|
||||
"7K": CalculateDataBMS7K,
|
||||
"14K": CalculateDataBMS14K,
|
||||
},
|
||||
ddr: {
|
||||
SP: CalculateDataDDR,
|
||||
DP: CalculateDataDDR,
|
||||
},
|
||||
jubeat: {
|
||||
Single: CalculateDataJubeat,
|
||||
},
|
||||
usc: {
|
||||
Single: CalculateDataSDVXorUSC,
|
||||
},
|
||||
iidx: {
|
||||
SP: CalculateDataIIDXSP,
|
||||
DP: CalculateDataIIDXDP,
|
||||
},
|
||||
sdvx: {
|
||||
Single: CalculateDataSDVXorUSC,
|
||||
},
|
||||
popn: {
|
||||
"9B": () => ({}),
|
||||
},
|
||||
museca: {
|
||||
Single: CalculateDataMuseca,
|
||||
},
|
||||
chunithm: {
|
||||
Single: CalculateDataCHUNITHM,
|
||||
},
|
||||
maimai: {
|
||||
Single: CalculateDataMaimai,
|
||||
},
|
||||
gitadora: {
|
||||
Gita: CalculateDataGitadora,
|
||||
Dora: CalculateDataGitadora,
|
||||
},
|
||||
bms: {
|
||||
"7K": CalculateDataBMS7K,
|
||||
"14K": CalculateDataBMS14K,
|
||||
},
|
||||
ddr: {
|
||||
SP: CalculateDataDDR,
|
||||
DP: CalculateDataDDR,
|
||||
},
|
||||
jubeat: {
|
||||
Single: CalculateDataJubeat,
|
||||
},
|
||||
usc: {
|
||||
Single: CalculateDataSDVXorUSC,
|
||||
},
|
||||
};
|
||||
|
||||
// Creates Game-Specific calculatedData for the provided game & playtype.
|
||||
export function CalculateDataForGamePT<G extends Game>(
|
||||
game: G,
|
||||
playtype: Playtypes[G],
|
||||
chart: AnyChartDocument,
|
||||
dryScore: DryScore,
|
||||
// ESD gets specially passed through because it's not part of the DryScore, but
|
||||
// can be used for statistics anyway.
|
||||
esd: number | null,
|
||||
defaultTierlistID: string | undefined,
|
||||
logger: KtLogger
|
||||
game: G,
|
||||
playtype: Playtypes[G],
|
||||
chart: AnyChartDocument,
|
||||
dryScore: DryScore,
|
||||
// ESD gets specially passed through because it's not part of the DryScore, but
|
||||
// can be used for statistics anyway.
|
||||
esd: number | null,
|
||||
defaultTierlistID: string | undefined,
|
||||
logger: KtLogger
|
||||
): Promise<ScoreDocument["calculatedData"]> {
|
||||
const GameRatingFns = CalculatedDataFunctions[game];
|
||||
const GameRatingFns = CalculatedDataFunctions[game];
|
||||
|
||||
if (!HasOwnProperty(GameRatingFns, playtype)) {
|
||||
logger.error(
|
||||
`Invalid playtype of ${playtype} given for game ${game} in CalculateDataForGamePT, returning an empty object.`
|
||||
);
|
||||
return new Promise((resolve) => resolve({}));
|
||||
}
|
||||
if (!HasOwnProperty(GameRatingFns, playtype)) {
|
||||
logger.error(
|
||||
`Invalid playtype of ${playtype} given for game ${game} in CalculateDataForGamePT, returning an empty object.`
|
||||
);
|
||||
return new Promise((resolve) => resolve({}));
|
||||
}
|
||||
|
||||
// @ts-expect-error standard game->pt stuff.
|
||||
return GameRatingFns[playtype](dryScore, chart, defaultTierlistID, logger);
|
||||
// @ts-expect-error standard game->pt stuff.
|
||||
return GameRatingFns[playtype](dryScore, chart, defaultTierlistID, logger);
|
||||
}
|
||||
|
||||
type CalculatedData<I extends IDStrings> = Required<ScoreDocument<I>["calculatedData"]>;
|
||||
|
||||
async function CalculateDataIIDXSP(
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument,
|
||||
defaultTierlistID: string | undefined,
|
||||
logger: KtLogger
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument,
|
||||
defaultTierlistID: string | undefined,
|
||||
logger: KtLogger
|
||||
): Promise<CalculatedData<"iidx:SP">> {
|
||||
const BPIData = await db["iidx-bpi-data"].findOne({
|
||||
chartID: chart.chartID,
|
||||
});
|
||||
const BPIData = await db["iidx-bpi-data"].findOne({
|
||||
chartID: chart.chartID,
|
||||
});
|
||||
|
||||
let bpi;
|
||||
const kPercent = await KaidenPercentile(dryScore, chart);
|
||||
let bpi;
|
||||
const kPercent = await KaidenPercentile(dryScore, chart);
|
||||
|
||||
if (BPIData) {
|
||||
bpi = CalculateBPI(
|
||||
BPIData.kavg,
|
||||
BPIData.wr,
|
||||
dryScore.scoreData.score,
|
||||
(chart as ChartDocument<"iidx:DP" | "iidx:SP">).data.notecount * 2,
|
||||
BPIData.coef
|
||||
);
|
||||
if (BPIData) {
|
||||
bpi = CalculateBPI(
|
||||
BPIData.kavg,
|
||||
BPIData.wr,
|
||||
dryScore.scoreData.score,
|
||||
(chart as ChartDocument<"iidx:DP" | "iidx:SP">).data.notecount * 2,
|
||||
BPIData.coef
|
||||
);
|
||||
|
||||
// kesdc = esd === null ? null : CalculateKESDC(BPIData.kesd, esd); disabled
|
||||
} else {
|
||||
bpi = null;
|
||||
}
|
||||
// kesdc = esd === null ? null : CalculateKESDC(BPIData.kesd, esd); disabled
|
||||
} else {
|
||||
bpi = null;
|
||||
}
|
||||
|
||||
return {
|
||||
BPI: bpi,
|
||||
"K%": kPercent,
|
||||
ktRating: await CalculateKTRating(dryScore, "iidx", "SP", chart, logger, defaultTierlistID),
|
||||
ktLampRating: await CalculateKTLampRating(dryScore, "iidx", "SP", chart, defaultTierlistID),
|
||||
};
|
||||
return {
|
||||
BPI: bpi,
|
||||
"K%": kPercent,
|
||||
ktRating: await CalculateKTRating(dryScore, "iidx", "SP", chart, logger, defaultTierlistID),
|
||||
ktLampRating: await CalculateKTLampRating(dryScore, "iidx", "SP", chart, defaultTierlistID),
|
||||
};
|
||||
}
|
||||
|
||||
async function CalculateDataIIDXDP(
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument,
|
||||
defaultTierlistID: string | undefined,
|
||||
logger: KtLogger
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument,
|
||||
defaultTierlistID: string | undefined,
|
||||
logger: KtLogger
|
||||
): Promise<CalculatedData<"iidx:DP">> {
|
||||
const BPIData = await db["iidx-bpi-data"].findOne({
|
||||
chartID: chart.chartID,
|
||||
});
|
||||
const BPIData = await db["iidx-bpi-data"].findOne({
|
||||
chartID: chart.chartID,
|
||||
});
|
||||
|
||||
let bpi;
|
||||
let bpi;
|
||||
|
||||
if (BPIData) {
|
||||
bpi = CalculateBPI(
|
||||
BPIData.kavg,
|
||||
BPIData.wr,
|
||||
dryScore.scoreData.score,
|
||||
(chart as ChartDocument<"iidx:DP" | "iidx:SP">).data.notecount * 2,
|
||||
BPIData.coef
|
||||
);
|
||||
if (BPIData) {
|
||||
bpi = CalculateBPI(
|
||||
BPIData.kavg,
|
||||
BPIData.wr,
|
||||
dryScore.scoreData.score,
|
||||
(chart as ChartDocument<"iidx:DP" | "iidx:SP">).data.notecount * 2,
|
||||
BPIData.coef
|
||||
);
|
||||
|
||||
// kesdc = esd === null ? null : CalculateKESDC(BPIData.kesd, esd); disabled
|
||||
} else {
|
||||
bpi = null;
|
||||
}
|
||||
// kesdc = esd === null ? null : CalculateKESDC(BPIData.kesd, esd); disabled
|
||||
} else {
|
||||
bpi = null;
|
||||
}
|
||||
|
||||
return {
|
||||
BPI: bpi,
|
||||
ktRating: await CalculateKTRating(dryScore, "iidx", "DP", chart, logger, defaultTierlistID),
|
||||
ktLampRating: await CalculateKTLampRating(dryScore, "iidx", "DP", chart, defaultTierlistID),
|
||||
};
|
||||
return {
|
||||
BPI: bpi,
|
||||
ktRating: await CalculateKTRating(dryScore, "iidx", "DP", chart, logger, defaultTierlistID),
|
||||
ktLampRating: await CalculateKTLampRating(dryScore, "iidx", "DP", chart, defaultTierlistID),
|
||||
};
|
||||
}
|
||||
|
||||
function CalculateDataSDVXorUSC(
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument,
|
||||
defaultTierlistID: string | undefined,
|
||||
logger: KtLogger
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument,
|
||||
defaultTierlistID: string | undefined,
|
||||
logger: KtLogger
|
||||
): CalculatedData<"sdvx:Single" | "usc:Single"> {
|
||||
const VF6 = CalculateVF6(
|
||||
dryScore.scoreData.grade as Grades["sdvx:Single"],
|
||||
dryScore.scoreData.lamp as Lamps["sdvx:Single"],
|
||||
dryScore.scoreData.percent,
|
||||
chart.levelNum,
|
||||
logger
|
||||
);
|
||||
const VF6 = CalculateVF6(
|
||||
dryScore.scoreData.grade as Grades["sdvx:Single"],
|
||||
dryScore.scoreData.lamp as Lamps["sdvx:Single"],
|
||||
dryScore.scoreData.percent,
|
||||
chart.levelNum,
|
||||
logger
|
||||
);
|
||||
|
||||
return {
|
||||
VF6,
|
||||
};
|
||||
return {
|
||||
VF6,
|
||||
};
|
||||
}
|
||||
|
||||
async function CalculateDataMuseca(
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument,
|
||||
defaultTierlistID: string | undefined,
|
||||
logger: KtLogger
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument,
|
||||
defaultTierlistID: string | undefined,
|
||||
logger: KtLogger
|
||||
): Promise<CalculatedData<"museca:Single">> {
|
||||
return {
|
||||
ktRating: await CalculateKTRating(
|
||||
dryScore,
|
||||
"museca",
|
||||
"Single",
|
||||
chart,
|
||||
logger,
|
||||
defaultTierlistID
|
||||
),
|
||||
};
|
||||
return {
|
||||
ktRating: await CalculateKTRating(
|
||||
dryScore,
|
||||
"museca",
|
||||
"Single",
|
||||
chart,
|
||||
logger,
|
||||
defaultTierlistID
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function CalculateDataCHUNITHM(
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument
|
||||
): CalculatedData<"chunithm:Single"> {
|
||||
return {
|
||||
rating: CalculateCHUNITHMRating(dryScore, chart),
|
||||
};
|
||||
return {
|
||||
rating: CalculateCHUNITHMRating(dryScore, chart),
|
||||
};
|
||||
}
|
||||
|
||||
async function CalculateDataMaimai(
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument,
|
||||
defaultTierlistID: string | undefined,
|
||||
logger: KtLogger
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument,
|
||||
defaultTierlistID: string | undefined,
|
||||
logger: KtLogger
|
||||
): Promise<CalculatedData<"maimai:Single">> {
|
||||
return {
|
||||
ktRating: await CalculateKTRating(
|
||||
dryScore,
|
||||
"maimai",
|
||||
"Single",
|
||||
chart,
|
||||
logger,
|
||||
defaultTierlistID
|
||||
),
|
||||
};
|
||||
return {
|
||||
ktRating: await CalculateKTRating(
|
||||
dryScore,
|
||||
"maimai",
|
||||
"Single",
|
||||
chart,
|
||||
logger,
|
||||
defaultTierlistID
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function CalculateDataGitadora(
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument
|
||||
): CalculatedData<"gitadora:Gita" | "gitadora:Dora"> {
|
||||
return {
|
||||
skill: CalculateGITADORASkill(dryScore, chart),
|
||||
};
|
||||
return {
|
||||
skill: CalculateGITADORASkill(dryScore, chart),
|
||||
};
|
||||
}
|
||||
|
||||
async function CalculateDataDDR(
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument,
|
||||
defaultTierlistID: string | undefined,
|
||||
logger: KtLogger
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument,
|
||||
defaultTierlistID: string | undefined,
|
||||
logger: KtLogger
|
||||
): Promise<CalculatedData<"ddr:SP" | "ddr:DP">> {
|
||||
return {
|
||||
MFCP: CalculateMFCP(dryScore, chart, logger),
|
||||
ktRating: await CalculateKTRating(
|
||||
dryScore,
|
||||
"museca",
|
||||
"Single",
|
||||
chart,
|
||||
logger,
|
||||
defaultTierlistID
|
||||
),
|
||||
};
|
||||
return {
|
||||
MFCP: CalculateMFCP(dryScore, chart, logger),
|
||||
ktRating: await CalculateKTRating(
|
||||
dryScore,
|
||||
"museca",
|
||||
"Single",
|
||||
chart,
|
||||
logger,
|
||||
defaultTierlistID
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async function CalculateDataBMS14K(
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument,
|
||||
defaultTierlistID: string | undefined,
|
||||
logger: KtLogger
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument,
|
||||
defaultTierlistID: string | undefined,
|
||||
logger: KtLogger
|
||||
): Promise<CalculatedData<"bms:14K">> {
|
||||
return {
|
||||
ktLampRating: await CalculateKTLampRating(dryScore, "bms", "14K", chart, defaultTierlistID),
|
||||
};
|
||||
return {
|
||||
ktLampRating: await CalculateKTLampRating(dryScore, "bms", "14K", chart, defaultTierlistID),
|
||||
};
|
||||
}
|
||||
|
||||
async function CalculateDataBMS7K(
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument,
|
||||
defaultTierlistID: string | undefined,
|
||||
logger: KtLogger
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument,
|
||||
defaultTierlistID: string | undefined,
|
||||
logger: KtLogger
|
||||
): Promise<CalculatedData<"bms:7K">> {
|
||||
return {
|
||||
ktLampRating: await CalculateKTLampRating(dryScore, "bms", "7K", chart, defaultTierlistID),
|
||||
};
|
||||
return {
|
||||
ktLampRating: await CalculateKTLampRating(dryScore, "bms", "7K", chart, defaultTierlistID),
|
||||
};
|
||||
}
|
||||
|
||||
async function CalculateDataJubeat(
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument,
|
||||
defaultTierlistID: string | undefined,
|
||||
logger: KtLogger
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument,
|
||||
defaultTierlistID: string | undefined,
|
||||
logger: KtLogger
|
||||
): Promise<CalculatedData<"jubeat:Single">> {
|
||||
return {
|
||||
jubility: 0, // @todo #163 Jubeat Jubility
|
||||
};
|
||||
return {
|
||||
jubility: 0, // @todo #163 Jubeat Jubility
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,546 +2,546 @@ import t from "tap";
|
||||
import db from "../../../../external/mongo/db";
|
||||
import { AnyChartDocument, Difficulties, Lamps, ScoreDocument } from "tachi-common";
|
||||
import {
|
||||
CalculateBPI,
|
||||
CalculateCHUNITHMRating,
|
||||
CalculateGITADORASkill,
|
||||
CalculateKESDC,
|
||||
CalculateMFCP,
|
||||
KaidenPercentile,
|
||||
CalculateKTLampRating,
|
||||
CalculateKTRating,
|
||||
CalculateVF6,
|
||||
CalculateBPI,
|
||||
CalculateCHUNITHMRating,
|
||||
CalculateGITADORASkill,
|
||||
CalculateKESDC,
|
||||
CalculateMFCP,
|
||||
KaidenPercentile,
|
||||
CalculateKTLampRating,
|
||||
CalculateKTRating,
|
||||
CalculateVF6,
|
||||
} from "./stats";
|
||||
import CreateLogCtx from "../../../logger/logger";
|
||||
import { isApproximately } from "../../../../test-utils/asserts";
|
||||
import { DryScore } from "../common/types";
|
||||
import {
|
||||
GetKTDataJSON,
|
||||
Testing511SPA,
|
||||
TestingIIDXSPDryScore,
|
||||
GetKTDataJSON,
|
||||
Testing511SPA,
|
||||
TestingIIDXSPDryScore,
|
||||
} from "../../../../test-utils/test-data";
|
||||
import { CloseAllConnections } from "../../../../test-utils/close-connections";
|
||||
import deepmerge from "deepmerge";
|
||||
import ResetDBState from "../../../../test-utils/resets";
|
||||
|
||||
t.test("#CalculateBPI", (t) => {
|
||||
t.test("AA BPI tests", (t) => {
|
||||
// data accurate as of 14/04/2021
|
||||
const WR_AA = 3650;
|
||||
const KAVG_AA = 3204;
|
||||
const MAX_AA = 1834 * 2;
|
||||
const COEF_AA = 1.25945;
|
||||
t.test("AA BPI tests", (t) => {
|
||||
// data accurate as of 14/04/2021
|
||||
const WR_AA = 3650;
|
||||
const KAVG_AA = 3204;
|
||||
const MAX_AA = 1834 * 2;
|
||||
const COEF_AA = 1.25945;
|
||||
|
||||
// AA, the song.
|
||||
function AA_BPI(yourEx: number) {
|
||||
return CalculateBPI(KAVG_AA, WR_AA, yourEx, MAX_AA, COEF_AA);
|
||||
}
|
||||
// AA, the song.
|
||||
function AA_BPI(yourEx: number) {
|
||||
return CalculateBPI(KAVG_AA, WR_AA, yourEx, MAX_AA, COEF_AA);
|
||||
}
|
||||
|
||||
t.equal(AA_BPI(KAVG_AA), 0, "A score of KAVG should be exactly 0BPI");
|
||||
isApproximately(AA_BPI(3393), 10.02, "A score with 3393 should be approximately 10.02BPI");
|
||||
isApproximately(AA_BPI(3481), 20.09, "A score with 3481 should be approximately 20.09BPI");
|
||||
isApproximately(AA_BPI(3535), 30.01, "A score with 3535 should be approximately 30.01BPI");
|
||||
isApproximately(AA_BPI(3572), 40.18, "A score with 3572 should be approximately 40.18BPI");
|
||||
isApproximately(AA_BPI(3597), 50.1, "A score with 3597 should be approximately 50.1BPI");
|
||||
isApproximately(AA_BPI(3615), 60.12, "A score with 3615 should be approximately 60.12BPI");
|
||||
isApproximately(AA_BPI(3628), 70.11, "A score with 3628 should be approximately 70.11BPI");
|
||||
isApproximately(AA_BPI(3638), 80.62, "A score with 3638 should be approximately 80.62BPI");
|
||||
isApproximately(AA_BPI(3645), 90.59, "A score with 3645 should be approximately 90.59BPI");
|
||||
t.equal(AA_BPI(WR_AA), 100, "A score of WR should be exactly 100BPI");
|
||||
t.equal(AA_BPI(KAVG_AA), 0, "A score of KAVG should be exactly 0BPI");
|
||||
isApproximately(AA_BPI(3393), 10.02, "A score with 3393 should be approximately 10.02BPI");
|
||||
isApproximately(AA_BPI(3481), 20.09, "A score with 3481 should be approximately 20.09BPI");
|
||||
isApproximately(AA_BPI(3535), 30.01, "A score with 3535 should be approximately 30.01BPI");
|
||||
isApproximately(AA_BPI(3572), 40.18, "A score with 3572 should be approximately 40.18BPI");
|
||||
isApproximately(AA_BPI(3597), 50.1, "A score with 3597 should be approximately 50.1BPI");
|
||||
isApproximately(AA_BPI(3615), 60.12, "A score with 3615 should be approximately 60.12BPI");
|
||||
isApproximately(AA_BPI(3628), 70.11, "A score with 3628 should be approximately 70.11BPI");
|
||||
isApproximately(AA_BPI(3638), 80.62, "A score with 3638 should be approximately 80.62BPI");
|
||||
isApproximately(AA_BPI(3645), 90.59, "A score with 3645 should be approximately 90.59BPI");
|
||||
t.equal(AA_BPI(WR_AA), 100, "A score of WR should be exactly 100BPI");
|
||||
|
||||
isApproximately(AA_BPI(3041), -5, "A score with 3628 should be approximately -5BPI");
|
||||
isApproximately(AA_BPI(2886), -9.99, "A score with 3638 should be approximately -9.99BPI");
|
||||
isApproximately(AA_BPI(3041), -5, "A score with 3628 should be approximately -5BPI");
|
||||
isApproximately(AA_BPI(2886), -9.99, "A score with 3638 should be approximately -9.99BPI");
|
||||
|
||||
t.equal(AA_BPI(0), -15, "Excessively bad (vs kavg) scores should cap at -15BPI");
|
||||
t.equal(AA_BPI(0), -15, "Excessively bad (vs kavg) scores should cap at -15BPI");
|
||||
|
||||
isApproximately(AA_BPI(MAX_AA), 244.56, "A score of MAX should be approximately 244.56BPI");
|
||||
isApproximately(AA_BPI(MAX_AA), 244.56, "A score of MAX should be approximately 244.56BPI");
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
// This song has both no co-efficient and is an sp11, so it serves
|
||||
// as another interesting test.
|
||||
t.test("Afterimage d'automne BPI tests", (t) => {
|
||||
// data accurate as of 14/04/2021
|
||||
const WR_AFT = 2891;
|
||||
const KAVG_AFT = 2497;
|
||||
const MAX_AFT = 1480 * 2;
|
||||
const COEF_AFT = null;
|
||||
// This song has both no co-efficient and is an sp11, so it serves
|
||||
// as another interesting test.
|
||||
t.test("Afterimage d'automne BPI tests", (t) => {
|
||||
// data accurate as of 14/04/2021
|
||||
const WR_AFT = 2891;
|
||||
const KAVG_AFT = 2497;
|
||||
const MAX_AFT = 1480 * 2;
|
||||
const COEF_AFT = null;
|
||||
|
||||
function AFT_BPI(yourEx: number) {
|
||||
return CalculateBPI(KAVG_AFT, WR_AFT, yourEx, MAX_AFT, COEF_AFT);
|
||||
}
|
||||
function AFT_BPI(yourEx: number) {
|
||||
return CalculateBPI(KAVG_AFT, WR_AFT, yourEx, MAX_AFT, COEF_AFT);
|
||||
}
|
||||
|
||||
t.equal(AFT_BPI(KAVG_AFT), 0, "A score of KAVG should be exactly 0BPI");
|
||||
isApproximately(AFT_BPI(2606), 10, "A score with 2606 should be approximately 10BPI");
|
||||
isApproximately(AFT_BPI(2675), 20.07, "A score with 2675 should be approximately 20.07BPI");
|
||||
isApproximately(AFT_BPI(2727), 30.18, "A score with 2727 should be approximately 30.18BPI");
|
||||
isApproximately(AFT_BPI(2767), 40.12, "A score with 2767 should be approximately 40.12BPI");
|
||||
isApproximately(AFT_BPI(2799), 50.06, "A score with 2799 should be approximately 50.06BPI");
|
||||
isApproximately(AFT_BPI(2826), 60.43, "A score with 2826 should be approximately 60.43BPI");
|
||||
isApproximately(AFT_BPI(2847), 70.3, "A score with 2847 should be approximately 70.3BPI");
|
||||
isApproximately(AFT_BPI(2865), 80.57, "A score with 2865 should be approximately 80.57BPI");
|
||||
isApproximately(AFT_BPI(2879), 90.18, "A score with 2879 should be approximately 90.18BPI");
|
||||
t.equal(AFT_BPI(WR_AFT), 100, "A score of WR should be exactly 100BPI");
|
||||
t.equal(AFT_BPI(KAVG_AFT), 0, "A score of KAVG should be exactly 0BPI");
|
||||
isApproximately(AFT_BPI(2606), 10, "A score with 2606 should be approximately 10BPI");
|
||||
isApproximately(AFT_BPI(2675), 20.07, "A score with 2675 should be approximately 20.07BPI");
|
||||
isApproximately(AFT_BPI(2727), 30.18, "A score with 2727 should be approximately 30.18BPI");
|
||||
isApproximately(AFT_BPI(2767), 40.12, "A score with 2767 should be approximately 40.12BPI");
|
||||
isApproximately(AFT_BPI(2799), 50.06, "A score with 2799 should be approximately 50.06BPI");
|
||||
isApproximately(AFT_BPI(2826), 60.43, "A score with 2826 should be approximately 60.43BPI");
|
||||
isApproximately(AFT_BPI(2847), 70.3, "A score with 2847 should be approximately 70.3BPI");
|
||||
isApproximately(AFT_BPI(2865), 80.57, "A score with 2865 should be approximately 80.57BPI");
|
||||
isApproximately(AFT_BPI(2879), 90.18, "A score with 2879 should be approximately 90.18BPI");
|
||||
t.equal(AFT_BPI(WR_AFT), 100, "A score of WR should be exactly 100BPI");
|
||||
|
||||
isApproximately(AFT_BPI(2423), -4.99, "A score with 2423 should be approximately -4.99BPI");
|
||||
isApproximately(
|
||||
AFT_BPI(2354),
|
||||
-10.04,
|
||||
"A score with 2354 should be approximately -10.04BPI"
|
||||
);
|
||||
isApproximately(AFT_BPI(2423), -4.99, "A score with 2423 should be approximately -4.99BPI");
|
||||
isApproximately(
|
||||
AFT_BPI(2354),
|
||||
-10.04,
|
||||
"A score with 2354 should be approximately -10.04BPI"
|
||||
);
|
||||
|
||||
t.equal(AFT_BPI(0), -15, "Excessively bad (vs kavg) scores should cap at -15BPI");
|
||||
t.equal(AFT_BPI(0), -15, "Excessively bad (vs kavg) scores should cap at -15BPI");
|
||||
|
||||
isApproximately(
|
||||
AFT_BPI(MAX_AFT),
|
||||
431.57,
|
||||
"A score of MAX should be approximately 431.57BPI"
|
||||
);
|
||||
isApproximately(
|
||||
AFT_BPI(MAX_AFT),
|
||||
431.57,
|
||||
"A score of MAX should be approximately 431.57BPI"
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#CalculateGITADORARating", (t) => {
|
||||
function TestGitadoraRating(percent: number, levelNum: number) {
|
||||
return CalculateGITADORASkill(
|
||||
{ scoreData: { percent } } as DryScore,
|
||||
{ levelNum } as AnyChartDocument
|
||||
);
|
||||
}
|
||||
function TestGitadoraRating(percent: number, levelNum: number) {
|
||||
return CalculateGITADORASkill(
|
||||
{ scoreData: { percent } } as DryScore,
|
||||
{ levelNum } as AnyChartDocument
|
||||
);
|
||||
}
|
||||
|
||||
t.equal(
|
||||
TestGitadoraRating(89.48, 3.4),
|
||||
60.84,
|
||||
"Test GITADORA Rating function aligns with game (1)"
|
||||
);
|
||||
t.equal(
|
||||
TestGitadoraRating(89.48, 3.4),
|
||||
60.84,
|
||||
"Test GITADORA Rating function aligns with game (1)"
|
||||
);
|
||||
|
||||
t.equal(
|
||||
TestGitadoraRating(70.76, 5.8),
|
||||
82.08,
|
||||
"Test GITADORA Rating function aligns with game (2)"
|
||||
);
|
||||
t.equal(
|
||||
TestGitadoraRating(70.76, 5.8),
|
||||
82.08,
|
||||
"Test GITADORA Rating function aligns with game (2)"
|
||||
);
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
// this metric is ground to be removed, possibly.
|
||||
t.test("#CalculateKESDC", (t) => {
|
||||
t.equal(CalculateKESDC(10, 4), 37.18460876390943, "Should perform ESDC on two numbers.");
|
||||
t.equal(CalculateKESDC(null, 4), null, "Should return null if kaidenESD is null.");
|
||||
t.equal(CalculateKESDC(10, 4), 37.18460876390943, "Should perform ESDC on two numbers.");
|
||||
t.equal(CalculateKESDC(null, 4), null, "Should return null if kaidenESD is null.");
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
t.test("#CalculateMFCP", (t) => {
|
||||
function TestMFCP(
|
||||
lamp: Lamps["ddr:SP" | "ddr:DP"],
|
||||
levelNum: number,
|
||||
difficulty: Difficulties["ddr:DP" | "ddr:SP"]
|
||||
) {
|
||||
return CalculateMFCP(
|
||||
{
|
||||
scoreData: {
|
||||
lamp,
|
||||
},
|
||||
} as ScoreDocument,
|
||||
{
|
||||
difficulty,
|
||||
levelNum,
|
||||
} as AnyChartDocument,
|
||||
logger
|
||||
);
|
||||
}
|
||||
function TestMFCP(
|
||||
lamp: Lamps["ddr:SP" | "ddr:DP"],
|
||||
levelNum: number,
|
||||
difficulty: Difficulties["ddr:DP" | "ddr:SP"]
|
||||
) {
|
||||
return CalculateMFCP(
|
||||
{
|
||||
scoreData: {
|
||||
lamp,
|
||||
},
|
||||
} as ScoreDocument,
|
||||
{
|
||||
difficulty,
|
||||
levelNum,
|
||||
} as AnyChartDocument,
|
||||
logger
|
||||
);
|
||||
}
|
||||
|
||||
t.equal(TestMFCP("FAILED", 10, "EXPERT"), null, "Should return null for non-mfcs");
|
||||
t.equal(TestMFCP("FAILED", 10, "EXPERT"), null, "Should return null for non-mfcs");
|
||||
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 10, "BASIC"),
|
||||
null,
|
||||
"Should reject charts on BASIC difficulty."
|
||||
);
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 10, "BASIC"),
|
||||
null,
|
||||
"Should reject charts on BASIC difficulty."
|
||||
);
|
||||
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 10, "BEGINNER"),
|
||||
null,
|
||||
"Should reject charts on BEGINNER difficulty."
|
||||
);
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 10, "BEGINNER"),
|
||||
null,
|
||||
"Should reject charts on BEGINNER difficulty."
|
||||
);
|
||||
|
||||
t.test("Should return null for charts with level less than 8", (t) => {
|
||||
for (let i = 1; i <= 7; i++) {
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", i, "EXPERT"),
|
||||
null,
|
||||
`Should return null for charts with level ${i}`
|
||||
);
|
||||
}
|
||||
t.test("Should return null for charts with level less than 8", (t) => {
|
||||
for (let i = 1; i <= 7; i++) {
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", i, "EXPERT"),
|
||||
null,
|
||||
`Should return null for charts with level ${i}`
|
||||
);
|
||||
}
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 8, "EXPERT"),
|
||||
1,
|
||||
"Should return 1 for charts with level 8"
|
||||
);
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 9, "EXPERT"),
|
||||
1,
|
||||
"Should return 1 for charts with level 9"
|
||||
);
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 10, "EXPERT"),
|
||||
1,
|
||||
"Should return 1 for charts with level 10"
|
||||
);
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 8, "EXPERT"),
|
||||
1,
|
||||
"Should return 1 for charts with level 8"
|
||||
);
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 9, "EXPERT"),
|
||||
1,
|
||||
"Should return 1 for charts with level 9"
|
||||
);
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 10, "EXPERT"),
|
||||
1,
|
||||
"Should return 1 for charts with level 10"
|
||||
);
|
||||
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 11, "EXPERT"),
|
||||
2,
|
||||
"Should return 2 for charts with level 11"
|
||||
);
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 12, "EXPERT"),
|
||||
2,
|
||||
"Should return 2 for charts with level 12"
|
||||
);
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 11, "EXPERT"),
|
||||
2,
|
||||
"Should return 2 for charts with level 11"
|
||||
);
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 12, "EXPERT"),
|
||||
2,
|
||||
"Should return 2 for charts with level 12"
|
||||
);
|
||||
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 13, "EXPERT"),
|
||||
4,
|
||||
"Should return 4 for charts with level 13"
|
||||
);
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 13, "EXPERT"),
|
||||
4,
|
||||
"Should return 4 for charts with level 13"
|
||||
);
|
||||
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 14, "EXPERT"),
|
||||
8,
|
||||
"Should return 8 for charts with level 14"
|
||||
);
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 14, "EXPERT"),
|
||||
8,
|
||||
"Should return 8 for charts with level 14"
|
||||
);
|
||||
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 15, "EXPERT"),
|
||||
15,
|
||||
"Should return 15 for charts with level 15"
|
||||
);
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 15, "EXPERT"),
|
||||
15,
|
||||
"Should return 15 for charts with level 15"
|
||||
);
|
||||
|
||||
t.test("Should return 25 for charts with level 16-20", (t) => {
|
||||
for (let i = 16; i <= 20; i++) {
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", i, "EXPERT"),
|
||||
25,
|
||||
`Should return 25 for charts with level ${i}`
|
||||
);
|
||||
}
|
||||
t.end();
|
||||
});
|
||||
t.test("Should return 25 for charts with level 16-20", (t) => {
|
||||
for (let i = 16; i <= 20; i++) {
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", i, "EXPERT"),
|
||||
25,
|
||||
`Should return 25 for charts with level ${i}`
|
||||
);
|
||||
}
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", NaN, "EXPERT"),
|
||||
null,
|
||||
"Invalid level triggers failsafe."
|
||||
);
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", NaN, "EXPERT"),
|
||||
null,
|
||||
"Invalid level triggers failsafe."
|
||||
);
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
const bbkk = GetKTDataJSON("./tachi/chunithm-bbkk-chart.json");
|
||||
|
||||
// unit testing a mathematical function is a square-round-hole problem.
|
||||
t.test("#CalculateCHUNITHMRating", (t) => {
|
||||
// cutoffs
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 1_010_000 } } as DryScore, bbkk), 5.0);
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 1_005_000 } } as DryScore, bbkk), 4.5);
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 1_000_000 } } as DryScore, bbkk), 4);
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 975_000 } } as DryScore, bbkk), 3);
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 925_000 } } as DryScore, bbkk), 0);
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 900_000 } } as DryScore, bbkk), 0);
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 800_000 } } as DryScore, bbkk), 0);
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 0 } } as DryScore, bbkk), 0);
|
||||
// cutoffs
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 1_010_000 } } as DryScore, bbkk), 5.0);
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 1_005_000 } } as DryScore, bbkk), 4.5);
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 1_000_000 } } as DryScore, bbkk), 4);
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 975_000 } } as DryScore, bbkk), 3);
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 925_000 } } as DryScore, bbkk), 0);
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 900_000 } } as DryScore, bbkk), 0);
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 800_000 } } as DryScore, bbkk), 0);
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 0 } } as DryScore, bbkk), 0);
|
||||
|
||||
// inbetweens
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 987_000 } } as DryScore, bbkk), 3.48);
|
||||
// inbetweens
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 987_000 } } as DryScore, bbkk), 3.48);
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Random assertions plucked from bemaniwiki.
|
||||
t.test("#CalculateVF6", (t) => {
|
||||
t.equal(CalculateVF6("S", "CLEAR", 99, 16, logger), 0.332);
|
||||
t.equal(CalculateVF6("S", "CLEAR", 99, 16, logger), 0.332);
|
||||
|
||||
t.equal(CalculateVF6("AAA+", "ULTIMATE CHAIN", 98, 17, logger), 0.356);
|
||||
t.equal(CalculateVF6("AAA+", "ULTIMATE CHAIN", 98, 17, logger), 0.356);
|
||||
|
||||
t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 16, logger), 0.369);
|
||||
t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 17, logger), 0.392);
|
||||
t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 18, logger), 0.415);
|
||||
t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 19, logger), 0.438);
|
||||
t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 20, logger), 0.462);
|
||||
t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 16, logger), 0.369);
|
||||
t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 17, logger), 0.392);
|
||||
t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 18, logger), 0.415);
|
||||
t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 19, logger), 0.438);
|
||||
t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 20, logger), 0.462);
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#KaidenPercentile", async (t) => {
|
||||
await db["iidx-eam-scores"].insert(
|
||||
[100, 200, 300, 400, 1000, 2000, 3000, 4000].map((e) => ({
|
||||
chartID: Testing511SPA.chartID,
|
||||
score: e,
|
||||
lamp: "EASY CLEAR",
|
||||
ranking: 0, // ignore
|
||||
}))
|
||||
);
|
||||
await db["iidx-eam-scores"].insert(
|
||||
[100, 200, 300, 400, 1000, 2000, 3000, 4000].map((e) => ({
|
||||
chartID: Testing511SPA.chartID,
|
||||
score: e,
|
||||
lamp: "EASY CLEAR",
|
||||
ranking: 0, // ignore
|
||||
}))
|
||||
);
|
||||
|
||||
const res = await KaidenPercentile(TestingIIDXSPDryScore, Testing511SPA);
|
||||
t.equal(res, 50);
|
||||
const res = await KaidenPercentile(TestingIIDXSPDryScore, Testing511SPA);
|
||||
t.equal(res, 50);
|
||||
|
||||
await db["iidx-eam-scores"].remove({});
|
||||
const res2 = await KaidenPercentile(TestingIIDXSPDryScore, Testing511SPA);
|
||||
await db["iidx-eam-scores"].remove({});
|
||||
const res2 = await KaidenPercentile(TestingIIDXSPDryScore, Testing511SPA);
|
||||
|
||||
t.equal(res2, null);
|
||||
t.equal(res2, null);
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#CalculateRating", (t) => {
|
||||
t.test("Should call the success calculator if percent > pivotPercent", async (t) => {
|
||||
const r = await CalculateKTRating(
|
||||
deepmerge(TestingIIDXSPDryScore, { scoreData: { percent: 80 } }),
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
logger
|
||||
);
|
||||
t.test("Should call the success calculator if percent > pivotPercent", async (t) => {
|
||||
const r = await CalculateKTRating(
|
||||
deepmerge(TestingIIDXSPDryScore, { scoreData: { percent: 80 } }),
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
logger
|
||||
);
|
||||
|
||||
t.ok(r > 10, "Should return rating greater than the levelNum of the chart.");
|
||||
t.ok(r > 10, "Should return rating greater than the levelNum of the chart.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should call the fail calculator if percent > pivotPercent", async (t) => {
|
||||
const r = await CalculateKTRating(
|
||||
TestingIIDXSPDryScore,
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
logger
|
||||
);
|
||||
t.test("Should call the fail calculator if percent > pivotPercent", async (t) => {
|
||||
const r = await CalculateKTRating(
|
||||
TestingIIDXSPDryScore,
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
logger
|
||||
);
|
||||
|
||||
t.ok(r < 10, "Should return rating less than the levelNum of the chart.");
|
||||
t.ok(r < 10, "Should return rating less than the levelNum of the chart.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should call levelNum if percent === pivotPercent", async (t) => {
|
||||
const r = await CalculateKTRating(
|
||||
deepmerge(TestingIIDXSPDryScore, { scoreData: { percent: 77.7777 } }),
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
logger
|
||||
);
|
||||
t.test("Should call levelNum if percent === pivotPercent", async (t) => {
|
||||
const r = await CalculateKTRating(
|
||||
deepmerge(TestingIIDXSPDryScore, { scoreData: { percent: 77.7777 } }),
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
logger
|
||||
);
|
||||
|
||||
t.equal(
|
||||
// hack for approximate tests
|
||||
parseFloat(r.toFixed(2)),
|
||||
10,
|
||||
"Should return rating exactly that of the levelNum of the chart."
|
||||
);
|
||||
t.equal(
|
||||
// hack for approximate tests
|
||||
parseFloat(r.toFixed(2)),
|
||||
10,
|
||||
"Should return rating exactly that of the levelNum of the chart."
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test(
|
||||
"Should trigger safety if completely invalid percent somehow gets through",
|
||||
async (t) => {
|
||||
let r = await CalculateKTRating(
|
||||
deepmerge(TestingIIDXSPDryScore, { scoreData: { percent: 1000000000 } }),
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
logger
|
||||
);
|
||||
t.test(
|
||||
"Should trigger safety if completely invalid percent somehow gets through",
|
||||
async (t) => {
|
||||
let r = await CalculateKTRating(
|
||||
deepmerge(TestingIIDXSPDryScore, { scoreData: { percent: 1000000000 } }),
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
logger
|
||||
);
|
||||
|
||||
t.equal(r, 0, "Should safely return 0 and log a warning.");
|
||||
t.equal(r, 0, "Should safely return 0 and log a warning.");
|
||||
|
||||
r = await CalculateKTRating(
|
||||
// not high enough to be non-finite but high enough to be > 1000
|
||||
deepmerge(TestingIIDXSPDryScore, { scoreData: { percent: 200 } }),
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
logger
|
||||
);
|
||||
r = await CalculateKTRating(
|
||||
// not high enough to be non-finite but high enough to be > 1000
|
||||
deepmerge(TestingIIDXSPDryScore, { scoreData: { percent: 200 } }),
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
logger
|
||||
);
|
||||
|
||||
t.equal(r, 0, "Should safely return 0 and log a warning.");
|
||||
t.equal(r, 0, "Should safely return 0 and log a warning.");
|
||||
|
||||
t.end();
|
||||
}
|
||||
);
|
||||
t.end();
|
||||
}
|
||||
);
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#CalculateLampRating", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
const defaultTierlist = {
|
||||
createdAt: 1620150338858,
|
||||
tierlistID: "ee9b756e50cff8282091102257b01f423ef855f2",
|
||||
createdBy: 1,
|
||||
description: "The official Kamaitachi Tierlist for beatmania IIDX (SP).",
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
isDefault: true,
|
||||
name: "Kamaitachi IIDX SP Official",
|
||||
lastUpdated: 1620150338858,
|
||||
permissions: {
|
||||
anyPlayer: {
|
||||
edit: 0,
|
||||
submit: 1,
|
||||
vote: 1,
|
||||
},
|
||||
},
|
||||
config: {
|
||||
autoHumanise: false,
|
||||
flags: ["Individual Difference"],
|
||||
requireState: "clear",
|
||||
},
|
||||
};
|
||||
const defaultTierlist = {
|
||||
createdAt: 1620150338858,
|
||||
tierlistID: "ee9b756e50cff8282091102257b01f423ef855f2",
|
||||
createdBy: 1,
|
||||
description: "The official Kamaitachi Tierlist for beatmania IIDX (SP).",
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
isDefault: true,
|
||||
name: "Kamaitachi IIDX SP Official",
|
||||
lastUpdated: 1620150338858,
|
||||
permissions: {
|
||||
anyPlayer: {
|
||||
edit: 0,
|
||||
submit: 1,
|
||||
vote: 1,
|
||||
},
|
||||
},
|
||||
config: {
|
||||
autoHumanise: false,
|
||||
flags: ["Individual Difference"],
|
||||
requireState: "clear",
|
||||
},
|
||||
};
|
||||
|
||||
t.test("TierlistData", async (t) => {
|
||||
const lampRating = await CalculateKTLampRating(
|
||||
TestingIIDXSPDryScore,
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
defaultTierlist!.tierlistID
|
||||
);
|
||||
t.test("TierlistData", async (t) => {
|
||||
const lampRating = await CalculateKTLampRating(
|
||||
TestingIIDXSPDryScore,
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
defaultTierlist!.tierlistID
|
||||
);
|
||||
|
||||
t.equal(lampRating, 10, "Should equal the levelNum of the chart.");
|
||||
t.equal(lampRating, 10, "Should equal the levelNum of the chart.");
|
||||
|
||||
const lampRatingFail = await CalculateKTLampRating(
|
||||
deepmerge(TestingIIDXSPDryScore, { scoreData: { lamp: "FAILED" } }),
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
defaultTierlist!.tierlistID
|
||||
);
|
||||
const lampRatingFail = await CalculateKTLampRating(
|
||||
deepmerge(TestingIIDXSPDryScore, { scoreData: { lamp: "FAILED" } }),
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
defaultTierlist!.tierlistID
|
||||
);
|
||||
|
||||
t.equal(lampRatingFail, 0, "Should equal 0, if the score is not a clear.");
|
||||
t.equal(lampRatingFail, 0, "Should equal 0, if the score is not a clear.");
|
||||
|
||||
const lampRatingHC = await CalculateKTLampRating(
|
||||
deepmerge(TestingIIDXSPDryScore, { scoreData: { lamp: "HARD CLEAR" } }),
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
defaultTierlist!.tierlistID
|
||||
);
|
||||
const lampRatingHC = await CalculateKTLampRating(
|
||||
deepmerge(TestingIIDXSPDryScore, { scoreData: { lamp: "HARD CLEAR" } }),
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
defaultTierlist!.tierlistID
|
||||
);
|
||||
|
||||
t.equal(lampRatingHC, 10.6, "Should equal the tierlist value for HC on this chart.");
|
||||
t.equal(lampRatingHC, 10.6, "Should equal the tierlist value for HC on this chart.");
|
||||
|
||||
const lampRatingEXHC = await CalculateKTLampRating(
|
||||
deepmerge(TestingIIDXSPDryScore, { scoreData: { lamp: "EX HARD CLEAR" } }),
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
defaultTierlist!.tierlistID
|
||||
);
|
||||
const lampRatingEXHC = await CalculateKTLampRating(
|
||||
deepmerge(TestingIIDXSPDryScore, { scoreData: { lamp: "EX HARD CLEAR" } }),
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
defaultTierlist!.tierlistID
|
||||
);
|
||||
|
||||
// deliberate - there is no EXHC data here
|
||||
t.equal(lampRatingEXHC, 10.6, "Should equal the tierlist value for HC on this chart.");
|
||||
// deliberate - there is no EXHC data here
|
||||
t.equal(lampRatingEXHC, 10.6, "Should equal the tierlist value for HC on this chart.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("TierlistData edge cases", async (t) => {
|
||||
// mock document that implies 5.1.1. SPA is worth 11.9 to NC and 10.6 to HC.
|
||||
await db["tierlist-data"].insert({
|
||||
chartID: Testing511SPA.chartID,
|
||||
type: "lamp",
|
||||
key: "CLEAR",
|
||||
tierlistDataID: "asdf",
|
||||
tierlistID: defaultTierlist!.tierlistID,
|
||||
data: {
|
||||
flags: {},
|
||||
humanised: "a",
|
||||
value: 11.9,
|
||||
},
|
||||
});
|
||||
t.test("TierlistData edge cases", async (t) => {
|
||||
// mock document that implies 5.1.1. SPA is worth 11.9 to NC and 10.6 to HC.
|
||||
await db["tierlist-data"].insert({
|
||||
chartID: Testing511SPA.chartID,
|
||||
type: "lamp",
|
||||
key: "CLEAR",
|
||||
tierlistDataID: "asdf",
|
||||
tierlistID: defaultTierlist!.tierlistID,
|
||||
data: {
|
||||
flags: {},
|
||||
humanised: "a",
|
||||
value: 11.9,
|
||||
},
|
||||
});
|
||||
|
||||
const lampRating = await CalculateKTLampRating(
|
||||
TestingIIDXSPDryScore,
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
defaultTierlist!.tierlistID
|
||||
);
|
||||
const lampRating = await CalculateKTLampRating(
|
||||
TestingIIDXSPDryScore,
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
defaultTierlist!.tierlistID
|
||||
);
|
||||
|
||||
t.equal(lampRating, 11.9, "Should equal the NC value of the chart.");
|
||||
t.equal(lampRating, 11.9, "Should equal the NC value of the chart.");
|
||||
|
||||
const lampRatingFail = await CalculateKTLampRating(
|
||||
deepmerge(TestingIIDXSPDryScore, { scoreData: { lamp: "FAILED" } }),
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
defaultTierlist!.tierlistID
|
||||
);
|
||||
const lampRatingFail = await CalculateKTLampRating(
|
||||
deepmerge(TestingIIDXSPDryScore, { scoreData: { lamp: "FAILED" } }),
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
defaultTierlist!.tierlistID
|
||||
);
|
||||
|
||||
t.equal(lampRatingFail, 0, "Should equal 0, if the score is not a clear.");
|
||||
t.equal(lampRatingFail, 0, "Should equal 0, if the score is not a clear.");
|
||||
|
||||
const lampRatingHC = await CalculateKTLampRating(
|
||||
deepmerge(TestingIIDXSPDryScore, { scoreData: { lamp: "HARD CLEAR" } }),
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
defaultTierlist!.tierlistID
|
||||
);
|
||||
const lampRatingHC = await CalculateKTLampRating(
|
||||
deepmerge(TestingIIDXSPDryScore, { scoreData: { lamp: "HARD CLEAR" } }),
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
defaultTierlist!.tierlistID
|
||||
);
|
||||
|
||||
t.equal(
|
||||
lampRatingHC,
|
||||
11.9,
|
||||
"Should equal the tierlist value for NC, as it is higher than HC on this chart."
|
||||
);
|
||||
t.equal(
|
||||
lampRatingHC,
|
||||
11.9,
|
||||
"Should equal the tierlist value for NC, as it is higher than HC on this chart."
|
||||
);
|
||||
|
||||
const lampRatingEXHC = await CalculateKTLampRating(
|
||||
deepmerge(TestingIIDXSPDryScore, { scoreData: { lamp: "EX HARD CLEAR" } }),
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
defaultTierlist!.tierlistID
|
||||
);
|
||||
const lampRatingEXHC = await CalculateKTLampRating(
|
||||
deepmerge(TestingIIDXSPDryScore, { scoreData: { lamp: "EX HARD CLEAR" } }),
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA,
|
||||
defaultTierlist!.tierlistID
|
||||
);
|
||||
|
||||
t.equal(lampRatingEXHC, 11.9, "Should equal the tierlist value for NC on this chart.");
|
||||
t.equal(lampRatingEXHC, 11.9, "Should equal the tierlist value for NC on this chart.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("No TierlistData", async (t) => {
|
||||
await db["tierlist-data"].remove({});
|
||||
const lampRating = await CalculateKTLampRating(
|
||||
TestingIIDXSPDryScore,
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA
|
||||
);
|
||||
t.test("No TierlistData", async (t) => {
|
||||
await db["tierlist-data"].remove({});
|
||||
const lampRating = await CalculateKTLampRating(
|
||||
TestingIIDXSPDryScore,
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA
|
||||
);
|
||||
|
||||
t.equal(lampRating, 10, "Should equal the levelNum of the chart.");
|
||||
t.equal(lampRating, 10, "Should equal the levelNum of the chart.");
|
||||
|
||||
const lampRatingFail = await CalculateKTLampRating(
|
||||
deepmerge(TestingIIDXSPDryScore, { scoreData: { lamp: "FAILED" } }),
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA
|
||||
);
|
||||
const lampRatingFail = await CalculateKTLampRating(
|
||||
deepmerge(TestingIIDXSPDryScore, { scoreData: { lamp: "FAILED" } }),
|
||||
"iidx",
|
||||
"SP",
|
||||
Testing511SPA
|
||||
);
|
||||
|
||||
t.equal(lampRatingFail, 0, "Should equal 0, if the score is not a clear.");
|
||||
t.equal(lampRatingFail, 0, "Should equal 0, if the score is not a clear.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -9,37 +9,37 @@ import { lamps, clearLamp, ratingParameters } from "tachi-common/js/config";
|
||||
* Calculates the in-game CHUNITHM rating for a score.
|
||||
*/
|
||||
export function CalculateCHUNITHMRating(dryScore: DryScore, chartData: AnyChartDocument) {
|
||||
const score = dryScore.scoreData.score;
|
||||
const levelBase = chartData.levelNum * 100;
|
||||
const score = dryScore.scoreData.score;
|
||||
const levelBase = chartData.levelNum * 100;
|
||||
|
||||
let val = 0;
|
||||
let val = 0;
|
||||
|
||||
if (score >= 1_007_500) {
|
||||
val = levelBase + 200;
|
||||
} else if (score >= 1_005_000) {
|
||||
val = levelBase + 150 + ((score - 1_005_000) * 10) / 500;
|
||||
} else if (score >= 1_000_000) {
|
||||
val = levelBase + 100 + ((score - 1_000_000) * 5) / 100;
|
||||
} else if (score >= 975_000) {
|
||||
val = levelBase + ((score - 975_000) * 2) / 500;
|
||||
} else if (score >= 925_000) {
|
||||
val = levelBase - 300 + ((score - 925_000) * 3) / 500;
|
||||
} else if (score >= 900_000) {
|
||||
val = levelBase - 500 + ((score - 900_000) * 4) / 500;
|
||||
} else if (score >= 800_000) {
|
||||
val = (levelBase - 500) / 2 + ((score - 800_000) * ((levelBase - 500) / 2)) / 100_000;
|
||||
}
|
||||
if (score >= 1_007_500) {
|
||||
val = levelBase + 200;
|
||||
} else if (score >= 1_005_000) {
|
||||
val = levelBase + 150 + ((score - 1_005_000) * 10) / 500;
|
||||
} else if (score >= 1_000_000) {
|
||||
val = levelBase + 100 + ((score - 1_000_000) * 5) / 100;
|
||||
} else if (score >= 975_000) {
|
||||
val = levelBase + ((score - 975_000) * 2) / 500;
|
||||
} else if (score >= 925_000) {
|
||||
val = levelBase - 300 + ((score - 925_000) * 3) / 500;
|
||||
} else if (score >= 900_000) {
|
||||
val = levelBase - 500 + ((score - 900_000) * 4) / 500;
|
||||
} else if (score >= 800_000) {
|
||||
val = (levelBase - 500) / 2 + ((score - 800_000) * ((levelBase - 500) / 2)) / 100_000;
|
||||
}
|
||||
|
||||
return Math.max(Math.floor(val) / 100, 0);
|
||||
return Math.max(Math.floor(val) / 100, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the in-game GITADORA rating for a score.
|
||||
*/
|
||||
export function CalculateGITADORASkill(dryScore: DryScore, chartData: AnyChartDocument) {
|
||||
const trueRating = (dryScore.scoreData.percent / 100) * chartData.levelNum * 20;
|
||||
const flooredRating = Math.floor(trueRating * 100) / 100;
|
||||
return flooredRating;
|
||||
const trueRating = (dryScore.scoreData.percent / 100) * chartData.levelNum * 20;
|
||||
const flooredRating = Math.floor(trueRating * 100) / 100;
|
||||
return flooredRating;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,7 +47,7 @@ export function CalculateGITADORASkill(dryScore: DryScore, chartData: AnyChartDo
|
||||
* @returns
|
||||
*/
|
||||
function BPIPikaGreatFn(score: integer, max: integer) {
|
||||
return score === max ? max * 0.8 : 1 + (score / max - 0.5) / (1 - score / max);
|
||||
return score === max ? max * 0.8 : 1 + (score / max - 0.5) / (1 - score / max);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,35 +66,35 @@ function BPIPikaGreatFn(score: integer, max: integer) {
|
||||
* returns can be above 100.
|
||||
*/
|
||||
export function CalculateBPI(
|
||||
kaidenEx: integer,
|
||||
wrEx: integer,
|
||||
yourEx: integer,
|
||||
max: integer,
|
||||
pc: number | null
|
||||
kaidenEx: integer,
|
||||
wrEx: integer,
|
||||
yourEx: integer,
|
||||
max: integer,
|
||||
pc: number | null
|
||||
) {
|
||||
const powCoef = pc ?? 1.175;
|
||||
const yourPGF = BPIPikaGreatFn(yourEx, max);
|
||||
const kaidenPGF = BPIPikaGreatFn(kaidenEx, max);
|
||||
const wrPGF = BPIPikaGreatFn(wrEx, max);
|
||||
const powCoef = pc ?? 1.175;
|
||||
const yourPGF = BPIPikaGreatFn(yourEx, max);
|
||||
const kaidenPGF = BPIPikaGreatFn(kaidenEx, max);
|
||||
const wrPGF = BPIPikaGreatFn(wrEx, max);
|
||||
|
||||
// no idea what these var names are
|
||||
const _s_ = yourPGF / kaidenPGF,
|
||||
_z_ = wrPGF / kaidenPGF;
|
||||
// no idea what these var names are
|
||||
const _s_ = yourPGF / kaidenPGF,
|
||||
_z_ = wrPGF / kaidenPGF;
|
||||
|
||||
const isBetterThanKavg = yourEx >= kaidenEx;
|
||||
const isBetterThanKavg = yourEx >= kaidenEx;
|
||||
|
||||
// this line of code isn't mine, and that's why it's *really* bad here.
|
||||
return Math.max(
|
||||
-15,
|
||||
Math.round(
|
||||
(isBetterThanKavg ? 100 : -100) *
|
||||
Math.pow(
|
||||
(isBetterThanKavg ? Math.log(_s_) : -Math.log(_s_)) / Math.log(_z_),
|
||||
powCoef
|
||||
) *
|
||||
100
|
||||
) / 100
|
||||
);
|
||||
// this line of code isn't mine, and that's why it's *really* bad here.
|
||||
return Math.max(
|
||||
-15,
|
||||
Math.round(
|
||||
(isBetterThanKavg ? 100 : -100) *
|
||||
Math.pow(
|
||||
(isBetterThanKavg ? Math.log(_s_) : -Math.log(_s_)) / Math.log(_z_),
|
||||
powCoef
|
||||
) *
|
||||
100
|
||||
) / 100
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,20 +102,20 @@ export function CalculateBPI(
|
||||
* @returns Null, if this chart has no kaidens, a percent between 0 and 100, if there are.
|
||||
*/
|
||||
export async function KaidenPercentile(scoreObj: DryScore, chartData: AnyChartDocument) {
|
||||
const scoreCount = await db["iidx-eam-scores"].count({
|
||||
chartID: chartData.chartID,
|
||||
});
|
||||
const scoreCount = await db["iidx-eam-scores"].count({
|
||||
chartID: chartData.chartID,
|
||||
});
|
||||
|
||||
if (!scoreCount) {
|
||||
return null;
|
||||
}
|
||||
if (!scoreCount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const worseScores = await db["iidx-eam-scores"].count({
|
||||
chartID: chartData.chartID,
|
||||
score: { $lt: scoreObj.scoreData.score },
|
||||
});
|
||||
const worseScores = await db["iidx-eam-scores"].count({
|
||||
chartID: chartData.chartID,
|
||||
score: { $lt: scoreObj.scoreData.score },
|
||||
});
|
||||
|
||||
return (100 * worseScores) / scoreCount;
|
||||
return (100 * worseScores) / scoreCount;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,10 +123,10 @@ export async function KaidenPercentile(scoreObj: DryScore, chartData: AnyChartDo
|
||||
* For those who know what ESDC is (me), this is just ESDC(kesd, your esd).
|
||||
*/
|
||||
export function CalculateKESDC(kaidenESD: number | null, yourESD: number) {
|
||||
if (!kaidenESD) {
|
||||
return null;
|
||||
}
|
||||
return ESDCore.ESDCompare(kaidenESD, yourESD);
|
||||
if (!kaidenESD) {
|
||||
return null;
|
||||
}
|
||||
return ESDCore.ESDCompare(kaidenESD, yourESD);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,105 +136,105 @@ export function CalculateKESDC(kaidenESD: number | null, yourESD: number) {
|
||||
* @returns Null if this score was not eligible, a number otherwise.
|
||||
*/
|
||||
export function CalculateMFCP(dryScore: DryScore, chartData: AnyChartDocument, logger: KtLogger) {
|
||||
if (dryScore.scoreData.lamp !== "MARVELOUS FULL COMBO") {
|
||||
return null;
|
||||
}
|
||||
if (dryScore.scoreData.lamp !== "MARVELOUS FULL COMBO") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Beginner and BASIC scores are explicitly excluded.
|
||||
if (chartData.difficulty === "BEGINNER" || chartData.difficulty === "BASIC") {
|
||||
return null;
|
||||
}
|
||||
// Beginner and BASIC scores are explicitly excluded.
|
||||
if (chartData.difficulty === "BEGINNER" || chartData.difficulty === "BASIC") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (chartData.levelNum < 8) {
|
||||
return null;
|
||||
} else if (chartData.levelNum <= 10) {
|
||||
return 1;
|
||||
} else if (chartData.levelNum <= 12) {
|
||||
return 2;
|
||||
} else if (chartData.levelNum === 13) {
|
||||
return 4;
|
||||
} else if (chartData.levelNum === 14) {
|
||||
return 8;
|
||||
} else if (chartData.levelNum === 15) {
|
||||
return 15;
|
||||
} else if (chartData.levelNum >= 16) {
|
||||
return 25;
|
||||
}
|
||||
if (chartData.levelNum < 8) {
|
||||
return null;
|
||||
} else if (chartData.levelNum <= 10) {
|
||||
return 1;
|
||||
} else if (chartData.levelNum <= 12) {
|
||||
return 2;
|
||||
} else if (chartData.levelNum === 13) {
|
||||
return 4;
|
||||
} else if (chartData.levelNum === 14) {
|
||||
return 8;
|
||||
} else if (chartData.levelNum === 15) {
|
||||
return 15;
|
||||
} else if (chartData.levelNum >= 16) {
|
||||
return 25;
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
`Invalid levelNum passed to MFCP ${chartData.levelNum}. ChartID ${chartData.chartID}.`
|
||||
);
|
||||
logger.warn(
|
||||
`Invalid levelNum passed to MFCP ${chartData.levelNum}. ChartID ${chartData.chartID}.`
|
||||
);
|
||||
|
||||
// failsafe
|
||||
return null;
|
||||
// failsafe
|
||||
return null;
|
||||
}
|
||||
|
||||
const VF4GradeCoefficients = {
|
||||
S: 1.0,
|
||||
"AAA+": 0.99,
|
||||
AAA: 0.98,
|
||||
"AA+": 0.97,
|
||||
AA: 0.96,
|
||||
"A+": 0.95,
|
||||
A: 0.94,
|
||||
B: 0.93,
|
||||
C: 0.92,
|
||||
D: 0.91,
|
||||
S: 1.0,
|
||||
"AAA+": 0.99,
|
||||
AAA: 0.98,
|
||||
"AA+": 0.97,
|
||||
AA: 0.96,
|
||||
"A+": 0.95,
|
||||
A: 0.94,
|
||||
B: 0.93,
|
||||
C: 0.92,
|
||||
D: 0.91,
|
||||
};
|
||||
|
||||
const VF5GradeCoefficients = {
|
||||
S: 1.05,
|
||||
"AAA+": 1.02,
|
||||
AAA: 1.0,
|
||||
"AA+": 0.97,
|
||||
AA: 0.94,
|
||||
"A+": 0.91, // everything below this point (incl. this) is marked with a (?) in bemaniwiki.
|
||||
A: 0.88,
|
||||
B: 0.85,
|
||||
C: 0.82,
|
||||
D: 0.8,
|
||||
S: 1.05,
|
||||
"AAA+": 1.02,
|
||||
AAA: 1.0,
|
||||
"AA+": 0.97,
|
||||
AA: 0.94,
|
||||
"A+": 0.91, // everything below this point (incl. this) is marked with a (?) in bemaniwiki.
|
||||
A: 0.88,
|
||||
B: 0.85,
|
||||
C: 0.82,
|
||||
D: 0.8,
|
||||
};
|
||||
|
||||
const VF5LampCoefficients = {
|
||||
"PERFECT ULTIMATE CHAIN": 1.1,
|
||||
"ULTIMATE CHAIN": 1.05,
|
||||
"EXCESSIVE CLEAR": 1.02,
|
||||
CLEAR: 1.0,
|
||||
FAILED: 0.5,
|
||||
"PERFECT ULTIMATE CHAIN": 1.1,
|
||||
"ULTIMATE CHAIN": 1.05,
|
||||
"EXCESSIVE CLEAR": 1.02,
|
||||
CLEAR: 1.0,
|
||||
FAILED: 0.5,
|
||||
};
|
||||
|
||||
function FloorToNDP(number: number, dp: integer) {
|
||||
const mul = 10 ** dp;
|
||||
return Math.floor(number * mul) / mul;
|
||||
const mul = 10 ** dp;
|
||||
return Math.floor(number * mul) / mul;
|
||||
}
|
||||
|
||||
export function CalculateVF6(
|
||||
grade: Grades["sdvx:Single"],
|
||||
lamp: Lamps["sdvx:Single"],
|
||||
per: number,
|
||||
levelNum: number,
|
||||
logger: KtLogger
|
||||
grade: Grades["sdvx:Single"],
|
||||
lamp: Lamps["sdvx:Single"],
|
||||
per: number,
|
||||
levelNum: number,
|
||||
logger: KtLogger
|
||||
) {
|
||||
const gradeCoefficient = VF5GradeCoefficients[grade];
|
||||
const lampCoefficient = VF5LampCoefficients[lamp];
|
||||
const gradeCoefficient = VF5GradeCoefficients[grade];
|
||||
const lampCoefficient = VF5LampCoefficients[lamp];
|
||||
|
||||
if (!lampCoefficient) {
|
||||
logger.warn(`Invalid lamp of ${lamp} passed to CalculateVF5. Returning null.`);
|
||||
return null;
|
||||
}
|
||||
if (!gradeCoefficient) {
|
||||
logger.warn(`Invalid grade of ${grade} passed to CalculateVF5. Returning null.`);
|
||||
return null;
|
||||
}
|
||||
if (!lampCoefficient) {
|
||||
logger.warn(`Invalid lamp of ${lamp} passed to CalculateVF5. Returning null.`);
|
||||
return null;
|
||||
}
|
||||
if (!gradeCoefficient) {
|
||||
logger.warn(`Invalid grade of ${grade} passed to CalculateVF5. Returning null.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const percent = per / 100;
|
||||
if (!levelNum || !percent) {
|
||||
return 0;
|
||||
}
|
||||
const percent = per / 100;
|
||||
if (!levelNum || !percent) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const realVF6 = (levelNum * 2 * percent * gradeCoefficient * lampCoefficient) / 100;
|
||||
const realVF6 = (levelNum * 2 * percent * gradeCoefficient * lampCoefficient) / 100;
|
||||
|
||||
return FloorToNDP(realVF6, 3);
|
||||
return FloorToNDP(realVF6, 3);
|
||||
}
|
||||
|
||||
// function CalculateJubility(
|
||||
@@ -246,151 +246,151 @@ export function CalculateVF6(
|
||||
// }
|
||||
|
||||
interface RatingParameters {
|
||||
failHarshnessMultiplier: number;
|
||||
pivotPercent: number;
|
||||
clearExpMultiplier: number;
|
||||
failHarshnessMultiplier: number;
|
||||
pivotPercent: number;
|
||||
clearExpMultiplier: number;
|
||||
}
|
||||
|
||||
// Generic Rating Calc that is guaranteed to work for everything. This is unspecialised, and not great.
|
||||
function KTRatingCalcV1(
|
||||
percent: number,
|
||||
levelNum: number,
|
||||
parameters: RatingParameters,
|
||||
logger: KtLogger
|
||||
percent: number,
|
||||
levelNum: number,
|
||||
parameters: RatingParameters,
|
||||
logger: KtLogger
|
||||
) {
|
||||
const percentDiv100 = percent / 100;
|
||||
const percentDiv100 = percent / 100;
|
||||
|
||||
if (percentDiv100 < parameters.pivotPercent) {
|
||||
return RatingCalcV0Fail(percentDiv100, levelNum, parameters);
|
||||
}
|
||||
if (percentDiv100 < parameters.pivotPercent) {
|
||||
return RatingCalcV0Fail(percentDiv100, levelNum, parameters);
|
||||
}
|
||||
|
||||
return RatingCalcV1Clear(percentDiv100, levelNum, parameters, logger);
|
||||
return RatingCalcV1Clear(percentDiv100, levelNum, parameters, logger);
|
||||
}
|
||||
|
||||
function RatingCalcV1Clear(
|
||||
percentDiv100: number,
|
||||
levelNum: number,
|
||||
parameters: RatingParameters,
|
||||
logger: KtLogger
|
||||
percentDiv100: number,
|
||||
levelNum: number,
|
||||
parameters: RatingParameters,
|
||||
logger: KtLogger
|
||||
) {
|
||||
// https://www.desmos.com/calculator/hn7uxjmjkc
|
||||
// https://www.desmos.com/calculator/hn7uxjmjkc
|
||||
|
||||
const rating =
|
||||
Math.cosh(
|
||||
parameters.clearExpMultiplier * levelNum * (percentDiv100 - parameters.pivotPercent)
|
||||
) +
|
||||
(levelNum - 1);
|
||||
const rating =
|
||||
Math.cosh(
|
||||
parameters.clearExpMultiplier * levelNum * (percentDiv100 - parameters.pivotPercent)
|
||||
) +
|
||||
(levelNum - 1);
|
||||
|
||||
// checks for Infinity or NaN. I'm not sure how this would happen, but it's a failsafe.
|
||||
if (!Number.isFinite(rating)) {
|
||||
logger.warn(
|
||||
`Percent: ${percentDiv100}, Level: ${levelNum} resulted in rating of ${rating}, which is invalid. Defaulting to 0.`
|
||||
);
|
||||
return 0;
|
||||
} else if (rating > 1000) {
|
||||
logger.warn(
|
||||
`Percent: ${percentDiv100}, Level: ${levelNum} resulted in rating of ${rating}, which is invalid (> 1000). Defaulting to 0.`
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
// checks for Infinity or NaN. I'm not sure how this would happen, but it's a failsafe.
|
||||
if (!Number.isFinite(rating)) {
|
||||
logger.warn(
|
||||
`Percent: ${percentDiv100}, Level: ${levelNum} resulted in rating of ${rating}, which is invalid. Defaulting to 0.`
|
||||
);
|
||||
return 0;
|
||||
} else if (rating > 1000) {
|
||||
logger.warn(
|
||||
`Percent: ${percentDiv100}, Level: ${levelNum} resulted in rating of ${rating}, which is invalid (> 1000). Defaulting to 0.`
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return rating;
|
||||
return rating;
|
||||
}
|
||||
|
||||
function RatingCalcV0Fail(percentDiv100: number, levelNum: number, parameters: RatingParameters) {
|
||||
// https://www.desmos.com/calculator/ngjie5elto
|
||||
return (
|
||||
percentDiv100 ** (parameters.failHarshnessMultiplier * levelNum) *
|
||||
(levelNum / parameters.pivotPercent ** (parameters.failHarshnessMultiplier * levelNum))
|
||||
);
|
||||
// https://www.desmos.com/calculator/ngjie5elto
|
||||
return (
|
||||
percentDiv100 ** (parameters.failHarshnessMultiplier * levelNum) *
|
||||
(levelNum / parameters.pivotPercent ** (parameters.failHarshnessMultiplier * levelNum))
|
||||
);
|
||||
}
|
||||
|
||||
export async function CalculateKTRating(
|
||||
dryScore: DryScore,
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
chart: AnyChartDocument,
|
||||
logger: KtLogger,
|
||||
defaultTierlistID?: string
|
||||
dryScore: DryScore,
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
chart: AnyChartDocument,
|
||||
logger: KtLogger,
|
||||
defaultTierlistID?: string
|
||||
) {
|
||||
// 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];
|
||||
// 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];
|
||||
|
||||
let levelNum = chart.levelNum;
|
||||
let levelNum = chart.levelNum;
|
||||
|
||||
if (defaultTierlistID) {
|
||||
const tierlistData = await GetOneTierlistData(
|
||||
game,
|
||||
chart,
|
||||
"score",
|
||||
null,
|
||||
defaultTierlistID
|
||||
);
|
||||
if (defaultTierlistID) {
|
||||
const tierlistData = await GetOneTierlistData(
|
||||
game,
|
||||
chart,
|
||||
"score",
|
||||
null,
|
||||
defaultTierlistID
|
||||
);
|
||||
|
||||
if (tierlistData) {
|
||||
levelNum = tierlistData.data.value;
|
||||
}
|
||||
}
|
||||
if (tierlistData) {
|
||||
levelNum = tierlistData.data.value;
|
||||
}
|
||||
}
|
||||
|
||||
return KTRatingCalcV1(dryScore.scoreData.percent, levelNum, parameters, logger);
|
||||
return KTRatingCalcV1(dryScore.scoreData.percent, levelNum, parameters, logger);
|
||||
}
|
||||
|
||||
export async function CalculateKTLampRating(
|
||||
dryScore: DryScore,
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
chart: AnyChartDocument,
|
||||
defaultTierlistID?: string
|
||||
dryScore: DryScore,
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
chart: AnyChartDocument,
|
||||
defaultTierlistID?: string
|
||||
) {
|
||||
// if no tierlist data
|
||||
if (!defaultTierlistID) {
|
||||
return LampRatingNoTierlistInfo(dryScore, game, chart);
|
||||
}
|
||||
// if no tierlist data
|
||||
if (!defaultTierlistID) {
|
||||
return LampRatingNoTierlistInfo(dryScore, game, chart);
|
||||
}
|
||||
|
||||
const lampTierlistInfo = await GetAllTierlistDataOfType(game, chart, "lamp", defaultTierlistID);
|
||||
const lampTierlistInfo = await GetAllTierlistDataOfType(game, chart, "lamp", defaultTierlistID);
|
||||
|
||||
// if no tierlist info
|
||||
if (lampTierlistInfo.length === 0) {
|
||||
return LampRatingNoTierlistInfo(dryScore, game, chart);
|
||||
}
|
||||
// if no tierlist info
|
||||
if (lampTierlistInfo.length === 0) {
|
||||
return LampRatingNoTierlistInfo(dryScore, game, chart);
|
||||
}
|
||||
|
||||
const userLampIndex = lamps[game].indexOf(dryScore.scoreData.lamp);
|
||||
const userLampIndex = lamps[game].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;
|
||||
// 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;
|
||||
|
||||
// why is this like this and not a lookup table?
|
||||
// Some charts have higher values for *lower* lamps, such as
|
||||
// one more lovely in IIDX being harder to NC than it is to HC!
|
||||
// this means we have to iterate over all tierlist data, in general.
|
||||
for (const tierlistData of lampTierlistInfo) {
|
||||
if (
|
||||
// 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
|
||||
) {
|
||||
lampRating = tierlistData.data.value;
|
||||
}
|
||||
}
|
||||
// why is this like this and not a lookup table?
|
||||
// Some charts have higher values for *lower* lamps, such as
|
||||
// one more lovely in IIDX being harder to NC than it is to HC!
|
||||
// this means we have to iterate over all tierlist data, in general.
|
||||
for (const tierlistData of lampTierlistInfo) {
|
||||
if (
|
||||
// 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
|
||||
) {
|
||||
lampRating = tierlistData.data.value;
|
||||
}
|
||||
}
|
||||
|
||||
return lampRating;
|
||||
return lampRating;
|
||||
}
|
||||
|
||||
function LampRatingNoTierlistInfo(dryScore: DryScore, game: Game, chart: AnyChartDocument) {
|
||||
const CLEAR_LAMP_INDEX = lamps[game].indexOf(clearLamp[game]);
|
||||
const CLEAR_LAMP_INDEX = lamps[game].indexOf(clearLamp[game]);
|
||||
|
||||
// if this is a clear
|
||||
if (lamps[game].indexOf(dryScore.scoreData.lamp) >= CLEAR_LAMP_INDEX) {
|
||||
// return this chart's numeric level as the lamp rating
|
||||
return chart.levelNum;
|
||||
}
|
||||
// if this is a clear
|
||||
if (lamps[game].indexOf(dryScore.scoreData.lamp) >= CLEAR_LAMP_INDEX) {
|
||||
// return this chart's numeric level as the lamp rating
|
||||
return chart.levelNum;
|
||||
}
|
||||
|
||||
// else, this score is worth 0.
|
||||
return 0;
|
||||
// else, this score is worth 0.
|
||||
return 0;
|
||||
}
|
||||
|
||||
// deprecated calcs
|
||||
|
||||
@@ -5,11 +5,11 @@ import { ImportTypes } from "tachi-common";
|
||||
import { ImportTypeDataMap, ImportTypeContextMap } from "../../import-types/common/types";
|
||||
|
||||
export class ConverterFailure {
|
||||
message: string | null;
|
||||
message: string | null;
|
||||
|
||||
constructor(message: string | null) {
|
||||
this.message = message;
|
||||
}
|
||||
constructor(message: string | null) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,22 +25,22 @@ export class SkipScoreFailure extends ConverterFailure {}
|
||||
* Alongside any other fields used in the query.
|
||||
*/
|
||||
export class KTDataNotFoundFailure<T extends ImportTypes> extends ConverterFailure {
|
||||
data: ImportTypeDataMap[T];
|
||||
converterContext: ImportTypeContextMap[T];
|
||||
importType: T;
|
||||
data: ImportTypeDataMap[T];
|
||||
converterContext: ImportTypeContextMap[T];
|
||||
importType: T;
|
||||
|
||||
constructor(
|
||||
message: string | null,
|
||||
importType: T,
|
||||
data: ImportTypeDataMap[T],
|
||||
context: ImportTypeContextMap[T]
|
||||
) {
|
||||
super(message);
|
||||
constructor(
|
||||
message: string | null,
|
||||
importType: T,
|
||||
data: ImportTypeDataMap[T],
|
||||
context: ImportTypeContextMap[T]
|
||||
) {
|
||||
super(message);
|
||||
|
||||
this.importType = importType;
|
||||
this.data = data;
|
||||
this.converterContext = context;
|
||||
}
|
||||
this.importType = importType;
|
||||
this.data = data;
|
||||
this.converterContext = context;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,19 +4,19 @@ import { PublicUserDocument } from "tachi-common";
|
||||
import { CloseAllConnections } from "../../../../test-utils/close-connections";
|
||||
|
||||
t.test("#CreateImportLoggerAndID", (t) => {
|
||||
const { importID, logger } = CreateImportLoggerAndID(
|
||||
{ username: "foo", id: 1 } as PublicUserDocument,
|
||||
"file/eamusement-iidx-csv"
|
||||
);
|
||||
const { importID, logger } = CreateImportLoggerAndID(
|
||||
{ username: "foo", id: 1 } as PublicUserDocument,
|
||||
"file/eamusement-iidx-csv"
|
||||
);
|
||||
|
||||
t.match(importID, /^[a-f0-9]{40}$/u, "Should return a 40 character importID.");
|
||||
t.match(importID, /^[a-f0-9]{40}$/u, "Should return a 40 character importID.");
|
||||
|
||||
// not possible to automate this, probably.
|
||||
logger.info(
|
||||
"Namespaced log test - if this does not have 'file/eamusement-iidx-csv | foo (#1)' next to it, it's broke."
|
||||
);
|
||||
// not possible to automate this, probably.
|
||||
logger.info(
|
||||
"Namespaced log test - if this does not have 'file/eamusement-iidx-csv | foo (#1)' next to it, it's broke."
|
||||
);
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -4,24 +4,24 @@ import { FormatUserDoc } from "../../../../utils/user";
|
||||
import { rootLogger, KtLogger } from "../../../logger/logger";
|
||||
|
||||
export function CreateImportLoggerAndID(userDoc: PublicUserDocument, importType: ImportTypes) {
|
||||
const importID = crypto.randomBytes(20).toString("hex");
|
||||
return { logger: CreateScoreLogger(userDoc, importID, importType), importID };
|
||||
const importID = crypto.randomBytes(20).toString("hex");
|
||||
return { logger: CreateScoreLogger(userDoc, importID, importType), importID };
|
||||
}
|
||||
|
||||
export function CreateScoreLogger(
|
||||
user: PublicUserDocument,
|
||||
importID: string,
|
||||
importType: ImportTypes
|
||||
user: PublicUserDocument,
|
||||
importID: string,
|
||||
importType: ImportTypes
|
||||
): KtLogger {
|
||||
const meta = {
|
||||
context: ["Score Import", importType, FormatUserDoc(user)],
|
||||
importID,
|
||||
};
|
||||
const meta = {
|
||||
context: ["Score Import", importType, FormatUserDoc(user)],
|
||||
importID,
|
||||
};
|
||||
|
||||
// used so appendLogCtx works
|
||||
const childLogger = rootLogger.child(meta);
|
||||
// used so appendLogCtx works
|
||||
const childLogger = rootLogger.child(meta);
|
||||
|
||||
childLogger.defaultMeta = meta;
|
||||
childLogger.defaultMeta = meta;
|
||||
|
||||
return childLogger as KtLogger;
|
||||
return childLogger as KtLogger;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {
|
||||
config,
|
||||
Game,
|
||||
Grades,
|
||||
IDStrings,
|
||||
AnyChartDocument,
|
||||
ChartDocument,
|
||||
GameToIDStrings,
|
||||
ESDCore,
|
||||
Playtypes,
|
||||
config,
|
||||
Game,
|
||||
Grades,
|
||||
IDStrings,
|
||||
AnyChartDocument,
|
||||
ChartDocument,
|
||||
GameToIDStrings,
|
||||
ESDCore,
|
||||
Playtypes,
|
||||
} from "tachi-common";
|
||||
import { judgementWindows, gamePercentMax } from "tachi-common/js/config";
|
||||
import CreateLogCtx from "../../../logger/logger";
|
||||
@@ -19,22 +19,22 @@ const logger = CreateLogCtx(__filename);
|
||||
* Util for getting a games' grade for a given percent.
|
||||
*/
|
||||
export function GetGradeFromPercent<I extends IDStrings = IDStrings>(
|
||||
game: Game,
|
||||
percent: number
|
||||
game: Game,
|
||||
percent: number
|
||||
): Grades[I] {
|
||||
// @todo #102 update config to use game->pt
|
||||
const boundaries = config.gradeBoundaries[game];
|
||||
const grades = config.grades[game];
|
||||
// @todo #102 update config to use game->pt
|
||||
const boundaries = config.gradeBoundaries[game];
|
||||
const grades = config.grades[game];
|
||||
|
||||
// eslint doesn't like backwards for loops (hey, this for loop is backwards!)
|
||||
for (let i = boundaries.length; i >= 0; i--) {
|
||||
if (percent >= boundaries[i]) {
|
||||
return grades[i] as Grades[I];
|
||||
}
|
||||
}
|
||||
// eslint doesn't like backwards for loops (hey, this for loop is backwards!)
|
||||
for (let i = boundaries.length; i >= 0; i--) {
|
||||
if (percent >= boundaries[i]) {
|
||||
return grades[i] as Grades[I];
|
||||
}
|
||||
}
|
||||
|
||||
logger.error(`Could not resolve grade for percent ${percent} on game ${game}`);
|
||||
throw new InternalFailure(`Could not resolve grade for percent ${percent} on game ${game}`);
|
||||
logger.error(`Could not resolve grade for percent ${percent} on game ${game}`);
|
||||
throw new InternalFailure(`Could not resolve grade for percent ${percent} on game ${game}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,44 +42,44 @@ export function GetGradeFromPercent<I extends IDStrings = IDStrings>(
|
||||
* a given game.
|
||||
*/
|
||||
export function GenericCalculatePercent(
|
||||
game: Game,
|
||||
score: number,
|
||||
chart?: AnyChartDocument
|
||||
game: Game,
|
||||
score: number,
|
||||
chart?: AnyChartDocument
|
||||
): number {
|
||||
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 "gitadora":
|
||||
case "maimai":
|
||||
return score;
|
||||
case "bms":
|
||||
case "iidx":
|
||||
if (!chart) {
|
||||
logger.severe("No Chart passed to GenericCalcPercent but game was iidx/bms.");
|
||||
throw new InternalFailure(
|
||||
"No Chart passed to GenericCalcPercent but game was iidx/bms."
|
||||
);
|
||||
}
|
||||
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 "gitadora":
|
||||
case "maimai":
|
||||
return score;
|
||||
case "bms":
|
||||
case "iidx":
|
||||
if (!chart) {
|
||||
logger.severe("No Chart passed to GenericCalcPercent but game was iidx/bms.");
|
||||
throw new InternalFailure(
|
||||
"No Chart passed to GenericCalcPercent but game was iidx/bms."
|
||||
);
|
||||
}
|
||||
|
||||
// Yeah, we declare it like this so the below return is actually clear.
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const MAX =
|
||||
(chart as ChartDocument<"iidx:SP" | "bms:7K" | "bms:14K" | "iidx:DP">).data
|
||||
.notecount * 2;
|
||||
// Yeah, we declare it like this so the below return is actually clear.
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const MAX =
|
||||
(chart as ChartDocument<"iidx:SP" | "bms:7K" | "bms:14K" | "iidx:DP">).data
|
||||
.notecount * 2;
|
||||
|
||||
return (100 * score) / MAX;
|
||||
default:
|
||||
logger.severe(`Invalid game passed of ${game} to GenericCalcPercent.`);
|
||||
throw new InternalFailure(`Invalid game passed of ${game} to GenericCalcPercent.`);
|
||||
}
|
||||
return (100 * score) / MAX;
|
||||
default:
|
||||
logger.severe(`Invalid game passed of ${game} to GenericCalcPercent.`);
|
||||
throw new InternalFailure(`Invalid game passed of ${game} to GenericCalcPercent.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,21 +89,21 @@ export function GenericCalculatePercent(
|
||||
* This exists to support maimai, as it has a dynamic "max percent".
|
||||
*/
|
||||
export function ValidatePercent(game: Game, percent: number, chart: AnyChartDocument) {
|
||||
// i love needing a helper function for *ONE* game.
|
||||
if (game === "maimai") {
|
||||
const mmChart = chart as ChartDocument<"maimai:Single">;
|
||||
if (percent > mmChart.data.maxPercent) {
|
||||
throw new InvalidScoreFailure(
|
||||
`Invalid percent - expected less than ${mmChart.data.maxPercent}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
// i love needing a helper function for *ONE* game.
|
||||
if (game === "maimai") {
|
||||
const mmChart = chart as ChartDocument<"maimai:Single">;
|
||||
if (percent > mmChart.data.maxPercent) {
|
||||
throw new InvalidScoreFailure(
|
||||
`Invalid percent - expected less than ${mmChart.data.maxPercent}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (percent > gamePercentMax[game]) {
|
||||
throw new InvalidScoreFailure(
|
||||
`Invalid percent of ${percent} - expected a value less than ${gamePercentMax[game]}% (${chart.songID} ${chart.playtype} ${chart.difficulty}).`
|
||||
);
|
||||
}
|
||||
if (percent > gamePercentMax[game]) {
|
||||
throw new InvalidScoreFailure(
|
||||
`Invalid percent of ${percent} - expected a value less than ${gamePercentMax[game]}% (${chart.songID} ${chart.playtype} ${chart.difficulty}).`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,17 +112,17 @@ export function ValidatePercent(game: Game, percent: number, chart: AnyChartDocu
|
||||
* or if the grade is invalid.
|
||||
*/
|
||||
export function GenericGetGradeAndPercent<G extends Game>(
|
||||
game: G,
|
||||
score: number,
|
||||
chart: AnyChartDocument
|
||||
game: G,
|
||||
score: number,
|
||||
chart: AnyChartDocument
|
||||
) {
|
||||
const percent = GenericCalculatePercent(game, score, chart);
|
||||
const percent = GenericCalculatePercent(game, score, chart);
|
||||
|
||||
ValidatePercent(game, percent, chart);
|
||||
ValidatePercent(game, percent, chart);
|
||||
|
||||
const grade = GetGradeFromPercent(game, percent) as Grades[GameToIDStrings[G]];
|
||||
const grade = GetGradeFromPercent(game, percent) as Grades[GameToIDStrings[G]];
|
||||
|
||||
return { percent, grade };
|
||||
return { percent, grade };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,24 +130,24 @@ export function GenericGetGradeAndPercent<G extends Game>(
|
||||
* null if the game does not support support ESD.
|
||||
*/
|
||||
export function CalculateESDForGame(
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
percent: number
|
||||
game: Game,
|
||||
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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,15 +155,15 @@ export function CalculateESDForGame(
|
||||
* @returns Miliseconds from the unix epoch, or null if the initial argument was null or undefined.
|
||||
*/
|
||||
export function ParseDateFromString(str: string | undefined | null): number | null {
|
||||
if (!str) {
|
||||
return null;
|
||||
}
|
||||
if (!str) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const date = Date.parse(str);
|
||||
const date = Date.parse(str);
|
||||
|
||||
if (Number.isNaN(date)) {
|
||||
throw new InvalidScoreFailure(`Invalid/Unparsable score timestamp of ${str}.`);
|
||||
}
|
||||
if (Number.isNaN(date)) {
|
||||
throw new InvalidScoreFailure(`Invalid/Unparsable score timestamp of ${str}.`);
|
||||
}
|
||||
|
||||
return date;
|
||||
return date;
|
||||
}
|
||||
|
||||
@@ -3,177 +3,177 @@ import { InvalidScoreFailure } from "./converter-failures";
|
||||
import { AssertStrAsPositiveInt, AssertStrAsPositiveNonZeroInt } from "./string-asserts";
|
||||
|
||||
function astr(v: string) {
|
||||
try {
|
||||
const r = AssertStrAsPositiveInt(v, "err");
|
||||
try {
|
||||
const r = AssertStrAsPositiveInt(v, "err");
|
||||
|
||||
return r;
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
return r;
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
function astrp(v: string) {
|
||||
try {
|
||||
const r = AssertStrAsPositiveNonZeroInt(v, "err");
|
||||
try {
|
||||
const r = AssertStrAsPositiveNonZeroInt(v, "err");
|
||||
|
||||
return r;
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
return r;
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
t.test("#AssertStrAsPositiveInt", (t) => {
|
||||
t.strictSame(
|
||||
astr("---"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject non-int input"
|
||||
);
|
||||
t.strictSame(
|
||||
astr("---"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject non-int input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astr("1.4"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject float input"
|
||||
);
|
||||
t.strictSame(
|
||||
astr("1.4"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject float input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astr("NaN"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject NaN"
|
||||
);
|
||||
t.strictSame(
|
||||
astr("-0.3"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject negative float input"
|
||||
);
|
||||
t.strictSame(
|
||||
astr("NaN"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject NaN"
|
||||
);
|
||||
t.strictSame(
|
||||
astr("-0.3"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject negative float input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astr("0xFF"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject hex input"
|
||||
);
|
||||
t.strictSame(
|
||||
astr("0xFF"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject hex input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astr("0b11"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject binary input"
|
||||
);
|
||||
t.strictSame(
|
||||
astr("0b11"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject binary input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astr("0o77"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject oct input"
|
||||
);
|
||||
t.strictSame(
|
||||
astr("0o77"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject oct input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astr("--1"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject double neg input"
|
||||
);
|
||||
t.strictSame(
|
||||
astr("--1"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject double neg input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astr("12f"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject valid parseInt but invalid Number() input"
|
||||
);
|
||||
t.strictSame(
|
||||
astr("12f"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject valid parseInt but invalid Number() input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astr(`${Number.MAX_SAFE_INTEGER.toString()}000`),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject numbers > max_safe_integer."
|
||||
);
|
||||
t.strictSame(
|
||||
astr(`${Number.MAX_SAFE_INTEGER.toString()}000`),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject numbers > max_safe_integer."
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astr("-1"),
|
||||
new InvalidScoreFailure(`err (Was negative.)`),
|
||||
"Should reject negative integer input"
|
||||
);
|
||||
t.strictSame(
|
||||
astr("-1"),
|
||||
new InvalidScoreFailure(`err (Was negative.)`),
|
||||
"Should reject negative integer input"
|
||||
);
|
||||
|
||||
t.strictSame(astr("-0"), 0, "Should allow negative 0 input");
|
||||
t.strictSame(astr("0"), 0, "Should allow 0 input");
|
||||
t.strictSame(astr("13"), 13, "Should allow valid int input");
|
||||
t.strictSame(astr("-0"), 0, "Should allow negative 0 input");
|
||||
t.strictSame(astr("0"), 0, "Should allow 0 input");
|
||||
t.strictSame(astr("13"), 13, "Should allow valid int input");
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#AssertStrAsPositiveNonZeroInt", (t) => {
|
||||
t.strictSame(
|
||||
astrp("---"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject non-int input"
|
||||
);
|
||||
t.strictSame(
|
||||
astrp("---"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject non-int input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astrp("1.4"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject float input"
|
||||
);
|
||||
t.strictSame(
|
||||
astrp("1.4"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject float input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astrp("NaN"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject NaN"
|
||||
);
|
||||
t.strictSame(
|
||||
astrp("-0.3"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject negative float input"
|
||||
);
|
||||
t.strictSame(
|
||||
astrp("NaN"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject NaN"
|
||||
);
|
||||
t.strictSame(
|
||||
astrp("-0.3"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject negative float input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astrp("0xFF"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject hex input"
|
||||
);
|
||||
t.strictSame(
|
||||
astrp("0xFF"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject hex input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astrp("0b11"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject binary input"
|
||||
);
|
||||
t.strictSame(
|
||||
astrp("0b11"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject binary input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astrp("0o77"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject oct input"
|
||||
);
|
||||
t.strictSame(
|
||||
astrp("0o77"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject oct input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astrp("--1"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject double neg input"
|
||||
);
|
||||
t.strictSame(
|
||||
astrp("--1"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject double neg input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astrp("12f"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject valid parseInt but invalid Number() input"
|
||||
);
|
||||
t.strictSame(
|
||||
astrp("12f"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject valid parseInt but invalid Number() input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astrp(`${Number.MAX_SAFE_INTEGER.toString()}000`),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject numbers > max_safe_integer."
|
||||
);
|
||||
t.strictSame(
|
||||
astrp(`${Number.MAX_SAFE_INTEGER.toString()}000`),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
"Should reject numbers > max_safe_integer."
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astrp("-1"),
|
||||
new InvalidScoreFailure(`err (Was negative or zero.)`),
|
||||
"Should reject negative integer input"
|
||||
);
|
||||
t.strictSame(
|
||||
astrp("-1"),
|
||||
new InvalidScoreFailure(`err (Was negative or zero.)`),
|
||||
"Should reject negative integer input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astrp("-0"),
|
||||
new InvalidScoreFailure(`err (Was negative or zero.)`),
|
||||
"Should reject negative 0 input"
|
||||
);
|
||||
t.strictSame(
|
||||
astrp("-0"),
|
||||
new InvalidScoreFailure(`err (Was negative or zero.)`),
|
||||
"Should reject negative 0 input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astrp("0"),
|
||||
new InvalidScoreFailure(`err (Was negative or zero.)`),
|
||||
"Should reject 0 input"
|
||||
);
|
||||
t.strictSame(
|
||||
astrp("0"),
|
||||
new InvalidScoreFailure(`err (Was negative or zero.)`),
|
||||
"Should reject 0 input"
|
||||
);
|
||||
|
||||
t.strictSame(astrp("13"), 13, "Should allow valid int input");
|
||||
t.strictSame(astrp("13"), 13, "Should allow valid int input");
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
@@ -3,55 +3,55 @@ import { Difficulties, Game, IDStrings, Playtypes } from "tachi-common";
|
||||
import { validDifficulties } from "tachi-common/js/config";
|
||||
|
||||
export function AssertStrAsDifficulty(
|
||||
strVal: string,
|
||||
game: Game,
|
||||
playtype: Playtypes[Game]
|
||||
strVal: string,
|
||||
game: Game,
|
||||
playtype: Playtypes[Game]
|
||||
): Difficulties[IDStrings] {
|
||||
if (!validDifficulties[game].includes(strVal)) {
|
||||
throw new InvalidScoreFailure(
|
||||
`Invalid Difficulty for ${game} ${playtype} - Expected any of ${validDifficulties[
|
||||
game
|
||||
].join(", ")}`
|
||||
);
|
||||
}
|
||||
if (!validDifficulties[game].includes(strVal)) {
|
||||
throw new InvalidScoreFailure(
|
||||
`Invalid Difficulty for ${game} ${playtype} - Expected any of ${validDifficulties[
|
||||
game
|
||||
].join(", ")}`
|
||||
);
|
||||
}
|
||||
|
||||
return strVal as Difficulties[IDStrings];
|
||||
return strVal as Difficulties[IDStrings];
|
||||
}
|
||||
|
||||
const isIntegerRegex = /^-?\d+$/u;
|
||||
|
||||
export function AssertStrAsPositiveInt(strVal: string, errorMessage: string) {
|
||||
const isInt = isIntegerRegex.test(strVal);
|
||||
const isInt = isIntegerRegex.test(strVal);
|
||||
|
||||
if (!isInt) {
|
||||
throw new InvalidScoreFailure(`${errorMessage} (Not an integer.)`);
|
||||
}
|
||||
if (!isInt) {
|
||||
throw new InvalidScoreFailure(`${errorMessage} (Not an integer.)`);
|
||||
}
|
||||
|
||||
const val = Number(strVal);
|
||||
const val = Number(strVal);
|
||||
|
||||
if (!Number.isSafeInteger(val)) {
|
||||
throw new InvalidScoreFailure(`${errorMessage} (Not an integer.)`);
|
||||
} else if (val < 0) {
|
||||
throw new InvalidScoreFailure(`${errorMessage} (Was negative.)`);
|
||||
}
|
||||
if (!Number.isSafeInteger(val)) {
|
||||
throw new InvalidScoreFailure(`${errorMessage} (Not an integer.)`);
|
||||
} else if (val < 0) {
|
||||
throw new InvalidScoreFailure(`${errorMessage} (Was negative.)`);
|
||||
}
|
||||
|
||||
return val;
|
||||
return val;
|
||||
}
|
||||
|
||||
export function AssertStrAsPositiveNonZeroInt(strVal: string, errorMessage: string) {
|
||||
const isInt = isIntegerRegex.test(strVal);
|
||||
const isInt = isIntegerRegex.test(strVal);
|
||||
|
||||
if (!isInt) {
|
||||
throw new InvalidScoreFailure(`${errorMessage} (Not an integer.)`);
|
||||
}
|
||||
if (!isInt) {
|
||||
throw new InvalidScoreFailure(`${errorMessage} (Not an integer.)`);
|
||||
}
|
||||
|
||||
const val = Number(strVal);
|
||||
const val = Number(strVal);
|
||||
|
||||
if (!Number.isSafeInteger(val)) {
|
||||
throw new InvalidScoreFailure(`${errorMessage} (Not an integer.)`);
|
||||
} else if (val <= 0) {
|
||||
throw new InvalidScoreFailure(`${errorMessage} (Was negative or zero.)`);
|
||||
}
|
||||
if (!Number.isSafeInteger(val)) {
|
||||
throw new InvalidScoreFailure(`${errorMessage} (Not an integer.)`);
|
||||
} else if (val <= 0) {
|
||||
throw new InvalidScoreFailure(`${errorMessage} (Was negative or zero.)`);
|
||||
}
|
||||
|
||||
return val;
|
||||
return val;
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ import { IDStrings, ScoreDocument, Playtypes, Game } from "tachi-common";
|
||||
* HydrateScore.
|
||||
*/
|
||||
export type DryScore<I extends IDStrings = IDStrings> = Pick<
|
||||
ScoreDocument<I>,
|
||||
"service" | "game" | "scoreMeta" | "timeAchieved" | "comment" | "importType"
|
||||
ScoreDocument<I>,
|
||||
"service" | "game" | "scoreMeta" | "timeAchieved" | "comment" | "importType"
|
||||
> & {
|
||||
scoreData: Omit<ScoreDocument<I>["scoreData"], "gradeIndex" | "lampIndex" | "esd">;
|
||||
scoreData: Omit<ScoreDocument<I>["scoreData"], "gradeIndex" | "lampIndex" | "esd">;
|
||||
};
|
||||
|
||||
export type ScorePlaytypeMap = Partial<Record<Playtypes[Game], ScoreDocument[]>>;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
ImportTypes,
|
||||
PublicUserDocument,
|
||||
SuccessfulAPIResponse,
|
||||
UnsuccessfulAPIResponse,
|
||||
ImportDocument,
|
||||
ImportTypes,
|
||||
PublicUserDocument,
|
||||
SuccessfulAPIResponse,
|
||||
UnsuccessfulAPIResponse,
|
||||
ImportDocument,
|
||||
} from "tachi-common";
|
||||
import { ImportInputParser } from "../import-types/common/types";
|
||||
import { CreateImportLoggerAndID } from "./common/import-logger";
|
||||
@@ -11,8 +11,8 @@ import ScoreImportMain from "./score-import-main";
|
||||
import ScoreImportFatalError from "./score-importing/score-import-error";
|
||||
|
||||
export interface WrappedAPIResponse {
|
||||
statusCode: number;
|
||||
body: SuccessfulAPIResponse<ImportDocument> | UnsuccessfulAPIResponse;
|
||||
statusCode: number;
|
||||
body: SuccessfulAPIResponse<ImportDocument> | UnsuccessfulAPIResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,48 +21,48 @@ export interface WrappedAPIResponse {
|
||||
* be immediately sent with res.json().
|
||||
*/
|
||||
export async function ExpressWrappedScoreImportMain<D, C>(
|
||||
user: PublicUserDocument,
|
||||
userIntent: boolean,
|
||||
importType: ImportTypes,
|
||||
InputParser: ImportInputParser<D, C>
|
||||
user: PublicUserDocument,
|
||||
userIntent: boolean,
|
||||
importType: ImportTypes,
|
||||
InputParser: ImportInputParser<D, C>
|
||||
): Promise<WrappedAPIResponse> {
|
||||
const { importID, logger } = CreateImportLoggerAndID(user, importType);
|
||||
logger.debug("Received import request.");
|
||||
const { importID, logger } = CreateImportLoggerAndID(user, importType);
|
||||
logger.debug("Received import request.");
|
||||
|
||||
try {
|
||||
const res = await ScoreImportMain(user, userIntent, importType, InputParser, {
|
||||
importID,
|
||||
logger,
|
||||
});
|
||||
try {
|
||||
const res = await ScoreImportMain(user, userIntent, importType, InputParser, {
|
||||
importID,
|
||||
logger,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
success: true,
|
||||
description: "Import successful.",
|
||||
body: res,
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof ScoreImportFatalError) {
|
||||
logger.info(err.message);
|
||||
return {
|
||||
statusCode: 400,
|
||||
body: {
|
||||
success: false,
|
||||
description: err.message,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
success: true,
|
||||
description: "Import successful.",
|
||||
body: res,
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof ScoreImportFatalError) {
|
||||
logger.info(err.message);
|
||||
return {
|
||||
statusCode: 400,
|
||||
body: {
|
||||
success: false,
|
||||
description: err.message,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
logger.error(err);
|
||||
return {
|
||||
statusCode: 500,
|
||||
body: {
|
||||
success: false,
|
||||
description:
|
||||
"An internal service error has occured. Please do not repeat this request.",
|
||||
},
|
||||
};
|
||||
}
|
||||
logger.error(err);
|
||||
return {
|
||||
statusCode: 500,
|
||||
body: {
|
||||
success: false,
|
||||
description:
|
||||
"An internal service error has occured. Please do not repeat this request.",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ import { GetRelevantFolderGoals, GetRelevantGoals, UpdateGoalsForUser, ProcessGo
|
||||
import { GoalDocument, UserGoalDocument } from "tachi-common";
|
||||
import { CreateFolderChartLookup } from "../../../../utils/folder";
|
||||
import {
|
||||
GetKTDataJSON,
|
||||
Testing511SPA,
|
||||
TestingIIDXSPScorePB,
|
||||
HC511Goal,
|
||||
HC511UserGoal,
|
||||
TestingIIDXFolderSP10,
|
||||
GetKTDataJSON,
|
||||
Testing511SPA,
|
||||
TestingIIDXSPScorePB,
|
||||
HC511Goal,
|
||||
HC511UserGoal,
|
||||
TestingIIDXFolderSP10,
|
||||
} from "../../../../test-utils/test-data";
|
||||
import deepmerge from "deepmerge";
|
||||
import CreateLogCtx from "../../../logger/logger";
|
||||
@@ -20,403 +20,403 @@ import { CloseAllConnections } from "../../../../test-utils/close-connections";
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
t.test("#GetRelevantFolderGoals", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
const fakeFolderGoalDocument: GoalDocument = {
|
||||
charts: {
|
||||
type: "folder",
|
||||
data: "ed9d8c734447ce67d7135c0067441a98cc81aeaf",
|
||||
},
|
||||
createdBy: 1,
|
||||
game: "iidx",
|
||||
goalID: "fake_goal_id",
|
||||
playtype: "SP",
|
||||
timeAdded: 0,
|
||||
title: "get > 1 ex score on any level 10.",
|
||||
criteria: {
|
||||
mode: "single",
|
||||
value: 1,
|
||||
key: "scoreData.score",
|
||||
},
|
||||
};
|
||||
const fakeFolderGoalDocument: GoalDocument = {
|
||||
charts: {
|
||||
type: "folder",
|
||||
data: "ed9d8c734447ce67d7135c0067441a98cc81aeaf",
|
||||
},
|
||||
createdBy: 1,
|
||||
game: "iidx",
|
||||
goalID: "fake_goal_id",
|
||||
playtype: "SP",
|
||||
timeAdded: 0,
|
||||
title: "get > 1 ex score on any level 10.",
|
||||
criteria: {
|
||||
mode: "single",
|
||||
value: 1,
|
||||
key: "scoreData.score",
|
||||
},
|
||||
};
|
||||
|
||||
const notFolderGoalDocument: GoalDocument = {
|
||||
charts: {
|
||||
type: "folder",
|
||||
data: "some_fake_folder_id",
|
||||
},
|
||||
createdBy: 1,
|
||||
game: "iidx",
|
||||
goalID: "fake_bad_goal_id",
|
||||
playtype: "SP",
|
||||
timeAdded: 0,
|
||||
title: "get > 1 ex score on some other folder.",
|
||||
criteria: {
|
||||
mode: "single",
|
||||
value: 1,
|
||||
key: "scoreData.score",
|
||||
},
|
||||
};
|
||||
const notFolderGoalDocument: GoalDocument = {
|
||||
charts: {
|
||||
type: "folder",
|
||||
data: "some_fake_folder_id",
|
||||
},
|
||||
createdBy: 1,
|
||||
game: "iidx",
|
||||
goalID: "fake_bad_goal_id",
|
||||
playtype: "SP",
|
||||
timeAdded: 0,
|
||||
title: "get > 1 ex score on some other folder.",
|
||||
criteria: {
|
||||
mode: "single",
|
||||
value: 1,
|
||||
key: "scoreData.score",
|
||||
},
|
||||
};
|
||||
|
||||
t.beforeEach(async () => {
|
||||
// @ts-expect-error garbage types
|
||||
const sp11folder = deepmerge(TestingIIDXFolderSP10, { data: { level: "11" } });
|
||||
await db.folders.insert(TestingIIDXFolderSP10);
|
||||
await db.folders.insert(sp11folder);
|
||||
await db.goals.insert(fakeFolderGoalDocument);
|
||||
await db.goals.insert(notFolderGoalDocument);
|
||||
await CreateFolderChartLookup(TestingIIDXFolderSP10);
|
||||
await CreateFolderChartLookup(sp11folder);
|
||||
});
|
||||
t.beforeEach(async () => {
|
||||
// @ts-expect-error garbage types
|
||||
const sp11folder = deepmerge(TestingIIDXFolderSP10, { data: { level: "11" } });
|
||||
await db.folders.insert(TestingIIDXFolderSP10);
|
||||
await db.folders.insert(sp11folder);
|
||||
await db.goals.insert(fakeFolderGoalDocument);
|
||||
await db.goals.insert(notFolderGoalDocument);
|
||||
await CreateFolderChartLookup(TestingIIDXFolderSP10);
|
||||
await CreateFolderChartLookup(sp11folder);
|
||||
});
|
||||
|
||||
t.test("Should correctly find the goals on this folder.", async (t) => {
|
||||
const res = await GetRelevantFolderGoals(
|
||||
["fake_goal_id", "fake_bad_goal_id"],
|
||||
[Testing511SPA.chartID]
|
||||
);
|
||||
t.test("Should correctly find the goals on this folder.", async (t) => {
|
||||
const res = await GetRelevantFolderGoals(
|
||||
["fake_goal_id", "fake_bad_goal_id"],
|
||||
[Testing511SPA.chartID]
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
res,
|
||||
[fakeFolderGoalDocument],
|
||||
"Should correctly return only the 511 goal document."
|
||||
);
|
||||
t.strictSame(
|
||||
res,
|
||||
[fakeFolderGoalDocument],
|
||||
"Should correctly return only the 511 goal document."
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#GetRelevantGoals", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(async () => {
|
||||
// use the real data so we have enough charts loaded for this to test properly.
|
||||
await db.songs.iidx.remove({});
|
||||
await db.charts.iidx.remove({});
|
||||
await db.charts.iidx.insert(GetKTDataJSON("./tachi/ktblack-charts-iidx.json"));
|
||||
await db.songs.iidx.insert(GetKTDataJSON("./tachi/ktblack-songs-iidx.json"));
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(async () => {
|
||||
// use the real data so we have enough charts loaded for this to test properly.
|
||||
await db.songs.iidx.remove({});
|
||||
await db.charts.iidx.remove({});
|
||||
await db.charts.iidx.insert(GetKTDataJSON("./tachi/ktblack-charts-iidx.json"));
|
||||
await db.songs.iidx.insert(GetKTDataJSON("./tachi/ktblack-songs-iidx.json"));
|
||||
|
||||
const lotsOfCharts = await db.charts.iidx.find({}, { limit: 20 });
|
||||
const goals: GoalDocument[] = lotsOfCharts.map((e) => ({
|
||||
charts: {
|
||||
type: "single",
|
||||
data: e.chartID,
|
||||
},
|
||||
createdBy: 1,
|
||||
game: "iidx",
|
||||
goalID: crypto.randomBytes(20).toString("hex"),
|
||||
playtype: "SP",
|
||||
timeAdded: 0,
|
||||
title: "get > 1 ex score on some other folder.",
|
||||
criteria: {
|
||||
mode: "single",
|
||||
value: 1,
|
||||
key: "scoreData.score",
|
||||
},
|
||||
}));
|
||||
const lotsOfCharts = await db.charts.iidx.find({}, { limit: 20 });
|
||||
const goals: GoalDocument[] = lotsOfCharts.map((e) => ({
|
||||
charts: {
|
||||
type: "single",
|
||||
data: e.chartID,
|
||||
},
|
||||
createdBy: 1,
|
||||
game: "iidx",
|
||||
goalID: crypto.randomBytes(20).toString("hex"),
|
||||
playtype: "SP",
|
||||
timeAdded: 0,
|
||||
title: "get > 1 ex score on some other folder.",
|
||||
criteria: {
|
||||
mode: "single",
|
||||
value: 1,
|
||||
key: "scoreData.score",
|
||||
},
|
||||
}));
|
||||
|
||||
await db.goals.insert(goals);
|
||||
await db.goals.insert(goals);
|
||||
|
||||
await db["user-goals"].insert(
|
||||
goals.map((e) => ({
|
||||
achieved: false,
|
||||
timeAchieved: null,
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
goalID: e.goalID,
|
||||
lastInteraction: null,
|
||||
outOf: 5,
|
||||
outOfHuman: "HARD CLEAR",
|
||||
progress: null,
|
||||
progressHuman: "NO DATA",
|
||||
timeSet: Date.now(),
|
||||
userID: 1,
|
||||
}))
|
||||
);
|
||||
});
|
||||
await db["user-goals"].insert(
|
||||
goals.map((e) => ({
|
||||
achieved: false,
|
||||
timeAchieved: null,
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
goalID: e.goalID,
|
||||
lastInteraction: null,
|
||||
outOf: 5,
|
||||
outOfHuman: "HARD CLEAR",
|
||||
progress: null,
|
||||
progressHuman: "NO DATA",
|
||||
timeSet: Date.now(),
|
||||
userID: 1,
|
||||
}))
|
||||
);
|
||||
});
|
||||
|
||||
t.test(
|
||||
"Should successfully filter goals to only those that are relevant to the session.",
|
||||
async (t) => {
|
||||
// lets pretend our session had scores on charts 0->4 and 20->25. We also only have goals on
|
||||
// charts 1->20, so only 5 of these should resolve.
|
||||
const ourCharts = [
|
||||
...(await db.charts.iidx.find({}, { limit: 5 })),
|
||||
...(await db.charts.iidx.find({}, { skip: 20, limit: 5 })),
|
||||
];
|
||||
t.test(
|
||||
"Should successfully filter goals to only those that are relevant to the session.",
|
||||
async (t) => {
|
||||
// lets pretend our session had scores on charts 0->4 and 20->25. We also only have goals on
|
||||
// charts 1->20, so only 5 of these should resolve.
|
||||
const ourCharts = [
|
||||
...(await db.charts.iidx.find({}, { limit: 5 })),
|
||||
...(await db.charts.iidx.find({}, { skip: 20, limit: 5 })),
|
||||
];
|
||||
|
||||
const chartIDs = new Set(ourCharts.map((e) => e.chartID));
|
||||
const chartIDs = new Set(ourCharts.map((e) => e.chartID));
|
||||
|
||||
const res = await GetRelevantGoals("iidx", 1, chartIDs, logger);
|
||||
const res = await GetRelevantGoals("iidx", 1, chartIDs, logger);
|
||||
|
||||
t.equal(res.goals.length, 5, "Should correctly resolve to 5 goals.");
|
||||
t.equal(res.userGoalsMap.size, 5, "Should also return 5 userGoals.");
|
||||
t.equal(res.goals.length, 5, "Should correctly resolve to 5 goals.");
|
||||
t.equal(res.userGoalsMap.size, 5, "Should also return 5 userGoals.");
|
||||
|
||||
t.end();
|
||||
}
|
||||
);
|
||||
t.end();
|
||||
}
|
||||
);
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#UpdateGoalsForUser", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
const baseGoalDocument: GoalDocument = {
|
||||
charts: {
|
||||
type: "single" as const,
|
||||
data: Testing511SPA.chartID,
|
||||
},
|
||||
createdBy: 1,
|
||||
game: "iidx",
|
||||
goalID: "FAKE_GOAL_ID",
|
||||
playtype: "SP",
|
||||
timeAdded: 0,
|
||||
title: "get > 1 ex score on some other folder.",
|
||||
criteria: {
|
||||
mode: "single",
|
||||
value: 1,
|
||||
key: "scoreData.score",
|
||||
},
|
||||
};
|
||||
const baseGoalDocument: GoalDocument = {
|
||||
charts: {
|
||||
type: "single" as const,
|
||||
data: Testing511SPA.chartID,
|
||||
},
|
||||
createdBy: 1,
|
||||
game: "iidx",
|
||||
goalID: "FAKE_GOAL_ID",
|
||||
playtype: "SP",
|
||||
timeAdded: 0,
|
||||
title: "get > 1 ex score on some other folder.",
|
||||
criteria: {
|
||||
mode: "single",
|
||||
value: 1,
|
||||
key: "scoreData.score",
|
||||
},
|
||||
};
|
||||
|
||||
const baseUserGoalDocument: UserGoalDocument = {
|
||||
achieved: false,
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
goalID: "FAKE_GOAL_ID",
|
||||
lastInteraction: null,
|
||||
outOf: 1,
|
||||
outOfHuman: "1",
|
||||
progress: 0,
|
||||
progressHuman: "0",
|
||||
timeAchieved: null,
|
||||
timeSet: 0,
|
||||
userID: 1,
|
||||
};
|
||||
const baseUserGoalDocument: UserGoalDocument = {
|
||||
achieved: false,
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
goalID: "FAKE_GOAL_ID",
|
||||
lastInteraction: null,
|
||||
outOf: 1,
|
||||
outOfHuman: "1",
|
||||
progress: 0,
|
||||
progressHuman: "0",
|
||||
timeAchieved: null,
|
||||
timeSet: 0,
|
||||
userID: 1,
|
||||
};
|
||||
|
||||
t.test("Should correctly update goals when user achieves goal.", async (t) => {
|
||||
await db.goals.insert(baseGoalDocument);
|
||||
delete baseGoalDocument._id;
|
||||
t.test("Should correctly update goals when user achieves goal.", async (t) => {
|
||||
await db.goals.insert(baseGoalDocument);
|
||||
delete baseGoalDocument._id;
|
||||
|
||||
await db["user-goals"].insert(baseUserGoalDocument);
|
||||
// we dont delete _id here because updategoalsforuser
|
||||
// depends on usergoal _id
|
||||
await db["user-goals"].insert(baseUserGoalDocument);
|
||||
// we dont delete _id here because updategoalsforuser
|
||||
// depends on usergoal _id
|
||||
|
||||
await db["personal-bests"].insert(TestingIIDXSPScorePB);
|
||||
delete TestingIIDXSPScorePB._id;
|
||||
await db["personal-bests"].insert(TestingIIDXSPScorePB);
|
||||
delete TestingIIDXSPScorePB._id;
|
||||
|
||||
const ugMap = new Map([["FAKE_GOAL_ID", baseUserGoalDocument]]);
|
||||
const ugMap = new Map([["FAKE_GOAL_ID", baseUserGoalDocument]]);
|
||||
|
||||
const res = await UpdateGoalsForUser([baseGoalDocument], ugMap, 1, logger);
|
||||
const res = await UpdateGoalsForUser([baseGoalDocument], ugMap, 1, logger);
|
||||
|
||||
t.strictSame(res, [
|
||||
{
|
||||
goalID: "FAKE_GOAL_ID",
|
||||
old: {
|
||||
progress: 0,
|
||||
progressHuman: "0",
|
||||
outOf: 1,
|
||||
outOfHuman: "1",
|
||||
achieved: false,
|
||||
},
|
||||
new: {
|
||||
progress: 1479,
|
||||
progressHuman: "1479",
|
||||
outOf: 1,
|
||||
outOfHuman: "1",
|
||||
achieved: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
t.strictSame(res, [
|
||||
{
|
||||
goalID: "FAKE_GOAL_ID",
|
||||
old: {
|
||||
progress: 0,
|
||||
progressHuman: "0",
|
||||
outOf: 1,
|
||||
outOfHuman: "1",
|
||||
achieved: false,
|
||||
},
|
||||
new: {
|
||||
progress: 1479,
|
||||
progressHuman: "1479",
|
||||
outOf: 1,
|
||||
outOfHuman: "1",
|
||||
achieved: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const r = await db["user-goals"].findOne({ goalID: "FAKE_GOAL_ID", userID: 1 });
|
||||
const r = await db["user-goals"].findOne({ goalID: "FAKE_GOAL_ID", userID: 1 });
|
||||
|
||||
t.hasStrict(
|
||||
r,
|
||||
{
|
||||
progress: 1479,
|
||||
progressHuman: "1479",
|
||||
outOf: 1,
|
||||
outOfHuman: "1",
|
||||
achieved: true,
|
||||
},
|
||||
"Should update goals in the database."
|
||||
);
|
||||
t.hasStrict(
|
||||
r,
|
||||
{
|
||||
progress: 1479,
|
||||
progressHuman: "1479",
|
||||
outOf: 1,
|
||||
outOfHuman: "1",
|
||||
achieved: true,
|
||||
},
|
||||
"Should update goals in the database."
|
||||
);
|
||||
|
||||
delete baseUserGoalDocument._id;
|
||||
delete baseUserGoalDocument._id;
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should correctly update goals when user does not achieve goal.", async (t) => {
|
||||
const goal = deepmerge(baseGoalDocument, { criteria: { value: 2 } });
|
||||
await db.goals.insert(goal);
|
||||
t.test("Should correctly update goals when user does not achieve goal.", async (t) => {
|
||||
const goal = deepmerge(baseGoalDocument, { criteria: { value: 2 } });
|
||||
await db.goals.insert(goal);
|
||||
|
||||
const userGoal = deepmerge(baseUserGoalDocument, {
|
||||
outOf: 2,
|
||||
outOfHuman: "2",
|
||||
}) as unknown as UserGoalDocument;
|
||||
const userGoal = deepmerge(baseUserGoalDocument, {
|
||||
outOf: 2,
|
||||
outOfHuman: "2",
|
||||
}) as unknown as UserGoalDocument;
|
||||
|
||||
await db["user-goals"].insert(userGoal);
|
||||
// we dont delete _id here because updategoalsforuser
|
||||
// depends on usergoal _id
|
||||
await db["user-goals"].insert(userGoal);
|
||||
// we dont delete _id here because updategoalsforuser
|
||||
// depends on usergoal _id
|
||||
|
||||
await db["personal-bests"].insert(
|
||||
deepmerge(TestingIIDXSPScorePB, { scoreData: { score: 1 } })
|
||||
);
|
||||
await db["personal-bests"].insert(
|
||||
deepmerge(TestingIIDXSPScorePB, { scoreData: { score: 1 } })
|
||||
);
|
||||
|
||||
const ugMap = new Map([["FAKE_GOAL_ID", userGoal]]);
|
||||
const ugMap = new Map([["FAKE_GOAL_ID", userGoal]]);
|
||||
|
||||
const res = await UpdateGoalsForUser([goal], ugMap, 1, logger);
|
||||
const res = await UpdateGoalsForUser([goal], ugMap, 1, logger);
|
||||
|
||||
t.strictSame(res, [
|
||||
{
|
||||
goalID: "FAKE_GOAL_ID",
|
||||
old: {
|
||||
progress: 0,
|
||||
progressHuman: "0",
|
||||
outOf: 2,
|
||||
outOfHuman: "2",
|
||||
achieved: false,
|
||||
},
|
||||
new: {
|
||||
progress: 1,
|
||||
progressHuman: "1",
|
||||
outOf: 2,
|
||||
outOfHuman: "2",
|
||||
achieved: false,
|
||||
},
|
||||
},
|
||||
]);
|
||||
t.strictSame(res, [
|
||||
{
|
||||
goalID: "FAKE_GOAL_ID",
|
||||
old: {
|
||||
progress: 0,
|
||||
progressHuman: "0",
|
||||
outOf: 2,
|
||||
outOfHuman: "2",
|
||||
achieved: false,
|
||||
},
|
||||
new: {
|
||||
progress: 1,
|
||||
progressHuman: "1",
|
||||
outOf: 2,
|
||||
outOfHuman: "2",
|
||||
achieved: false,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const r = await db["user-goals"].findOne({ goalID: "FAKE_GOAL_ID", userID: 1 });
|
||||
const r = await db["user-goals"].findOne({ goalID: "FAKE_GOAL_ID", userID: 1 });
|
||||
|
||||
t.hasStrict(
|
||||
r,
|
||||
{
|
||||
progress: 1,
|
||||
progressHuman: "1",
|
||||
outOf: 2,
|
||||
outOfHuman: "2",
|
||||
achieved: false,
|
||||
},
|
||||
"Should update goals in the database."
|
||||
);
|
||||
t.hasStrict(
|
||||
r,
|
||||
{
|
||||
progress: 1,
|
||||
progressHuman: "1",
|
||||
outOf: 2,
|
||||
outOfHuman: "2",
|
||||
achieved: false,
|
||||
},
|
||||
"Should update goals in the database."
|
||||
);
|
||||
|
||||
delete baseUserGoalDocument._id;
|
||||
delete baseUserGoalDocument._id;
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should return [] if no data is to be changed.", async (t) => {
|
||||
const res = await UpdateGoalsForUser([], new Map(), 1, logger);
|
||||
t.test("Should return [] if no data is to be changed.", async (t) => {
|
||||
const res = await UpdateGoalsForUser([], new Map(), 1, logger);
|
||||
|
||||
t.strictSame(res, []);
|
||||
t.strictSame(res, []);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should handle (skip) goals if no usergoal is set.", async (t) => {
|
||||
const res = await UpdateGoalsForUser([baseGoalDocument], new Map(), 1, logger);
|
||||
t.test("Should handle (skip) goals if no usergoal is set.", async (t) => {
|
||||
const res = await UpdateGoalsForUser([baseGoalDocument], new Map(), 1, logger);
|
||||
|
||||
t.strictSame(res, []);
|
||||
t.strictSame(res, []);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should handle (skip) invalid goals.", async (t) => {
|
||||
const res = await UpdateGoalsForUser(
|
||||
[deepmerge(baseGoalDocument, { charts: { type: "INVALID" } })],
|
||||
new Map([["FAKE_GOAL_ID", baseUserGoalDocument]]),
|
||||
1,
|
||||
logger
|
||||
);
|
||||
t.test("Should handle (skip) invalid goals.", async (t) => {
|
||||
const res = await UpdateGoalsForUser(
|
||||
[deepmerge(baseGoalDocument, { charts: { type: "INVALID" } })],
|
||||
new Map([["FAKE_GOAL_ID", baseUserGoalDocument]]),
|
||||
1,
|
||||
logger
|
||||
);
|
||||
|
||||
t.strictSame(res, []);
|
||||
t.strictSame(res, []);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.todo("#GetAndUpdateUsersGoals", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#ProcessGoal", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(async () => {
|
||||
await db.goals.insert(HC511Goal);
|
||||
});
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(async () => {
|
||||
await db.goals.insert(HC511Goal);
|
||||
});
|
||||
|
||||
t.test("Should process the users goal if a score has changed.", async (t) => {
|
||||
await db["user-goals"].insert(HC511UserGoal);
|
||||
await db["personal-bests"].insert(TestingIIDXSPScorePB); // score is EX HARD CLEAR by default.
|
||||
t.test("Should process the users goal if a score has changed.", async (t) => {
|
||||
await db["user-goals"].insert(HC511UserGoal);
|
||||
await db["personal-bests"].insert(TestingIIDXSPScorePB); // score is EX HARD CLEAR by default.
|
||||
|
||||
const res = await ProcessGoal(HC511Goal, HC511UserGoal, 1, logger);
|
||||
const res = await ProcessGoal(HC511Goal, HC511UserGoal, 1, logger);
|
||||
|
||||
t.not(res, undefined, "Should NOT return undefined.");
|
||||
t.not(res, undefined, "Should NOT return undefined.");
|
||||
|
||||
t.strictSame(
|
||||
res!.import,
|
||||
{
|
||||
goalID: "mock_goalID",
|
||||
old: {
|
||||
progress: null,
|
||||
progressHuman: "NO DATA",
|
||||
outOf: 5,
|
||||
outOfHuman: "HARD CLEAR",
|
||||
achieved: false,
|
||||
},
|
||||
new: {
|
||||
progress: 6,
|
||||
progressHuman: "EX HARD CLEAR (BP: 2)",
|
||||
outOf: 5,
|
||||
outOfHuman: "HARD CLEAR",
|
||||
achieved: true,
|
||||
},
|
||||
},
|
||||
"Should return a valid import goal fragment."
|
||||
);
|
||||
t.strictSame(
|
||||
res!.import,
|
||||
{
|
||||
goalID: "mock_goalID",
|
||||
old: {
|
||||
progress: null,
|
||||
progressHuman: "NO DATA",
|
||||
outOf: 5,
|
||||
outOfHuman: "HARD CLEAR",
|
||||
achieved: false,
|
||||
},
|
||||
new: {
|
||||
progress: 6,
|
||||
progressHuman: "EX HARD CLEAR (BP: 2)",
|
||||
outOf: 5,
|
||||
outOfHuman: "HARD CLEAR",
|
||||
achieved: true,
|
||||
},
|
||||
},
|
||||
"Should return a valid import goal fragment."
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should return undefined if there's no score.", async (t) => {
|
||||
await db["user-goals"].insert(HC511UserGoal);
|
||||
t.test("Should return undefined if there's no score.", async (t) => {
|
||||
await db["user-goals"].insert(HC511UserGoal);
|
||||
|
||||
const res = await ProcessGoal(HC511Goal, HC511UserGoal, 1, logger);
|
||||
const res = await ProcessGoal(HC511Goal, HC511UserGoal, 1, logger);
|
||||
|
||||
t.equal(res, undefined, "Should return undefined.");
|
||||
t.equal(res, undefined, "Should return undefined.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should return undefined if the progress has not changed.", async (t) => {
|
||||
await db["user-goals"].insert(HC511UserGoal);
|
||||
await db["personal-bests"].insert(TestingIIDXSPScorePB);
|
||||
t.test("Should return undefined if the progress has not changed.", async (t) => {
|
||||
await db["user-goals"].insert(HC511UserGoal);
|
||||
await db["personal-bests"].insert(TestingIIDXSPScorePB);
|
||||
|
||||
const firstUpdate = await ProcessGoal(HC511Goal, HC511UserGoal, 1, logger);
|
||||
const firstUpdate = await ProcessGoal(HC511Goal, HC511UserGoal, 1, logger);
|
||||
|
||||
// ignore this one
|
||||
t.not(firstUpdate, undefined, "Should NOT return undefined.");
|
||||
// ignore this one
|
||||
t.not(firstUpdate, undefined, "Should NOT return undefined.");
|
||||
|
||||
await db["user-goals"].bulkWrite([firstUpdate!.bwrite]);
|
||||
await db["user-goals"].bulkWrite([firstUpdate!.bwrite]);
|
||||
|
||||
const userGoal = await db["user-goals"].findOne({ userID: 1, goalID: HC511Goal.goalID });
|
||||
const userGoal = await db["user-goals"].findOne({ userID: 1, goalID: HC511Goal.goalID });
|
||||
|
||||
const secondUpdate = await ProcessGoal(HC511Goal, userGoal!, 1, logger);
|
||||
const secondUpdate = await ProcessGoal(HC511Goal, userGoal!, 1, logger);
|
||||
|
||||
t.equal(secondUpdate, undefined, "Should return undefined.");
|
||||
t.equal(secondUpdate, undefined, "Should return undefined.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -8,65 +8,65 @@ import { RedisPub } from "../../../../external/redis/redis-IPC";
|
||||
* Update a user's progress on all of their set goals.
|
||||
*/
|
||||
export async function GetAndUpdateUsersGoals(
|
||||
game: Game,
|
||||
userID: integer,
|
||||
chartIDs: Set<string>,
|
||||
logger: KtLogger
|
||||
game: Game,
|
||||
userID: integer,
|
||||
chartIDs: Set<string>,
|
||||
logger: KtLogger
|
||||
) {
|
||||
const { goals, userGoalsMap } = await GetRelevantGoals(game, userID, chartIDs, logger);
|
||||
const { goals, userGoalsMap } = await GetRelevantGoals(game, userID, chartIDs, logger);
|
||||
|
||||
if (!goals.length) {
|
||||
// if we hit the below code with an empty array mongodb will flip out on the bulkwrite op
|
||||
return [];
|
||||
}
|
||||
if (!goals.length) {
|
||||
// if we hit the below code with an empty array mongodb will flip out on the bulkwrite op
|
||||
return [];
|
||||
}
|
||||
|
||||
logger.verbose(`Found ${goals.length} relevant goals.`);
|
||||
logger.verbose(`Found ${goals.length} relevant goals.`);
|
||||
|
||||
return UpdateGoalsForUser(goals, userGoalsMap, userID, logger);
|
||||
return UpdateGoalsForUser(goals, userGoalsMap, userID, logger);
|
||||
}
|
||||
|
||||
export async function UpdateGoalsForUser(
|
||||
goals: GoalDocument[],
|
||||
userGoalsMap: Map<string, UserGoalDocument>,
|
||||
userID: integer,
|
||||
logger: KtLogger
|
||||
goals: GoalDocument[],
|
||||
userGoalsMap: Map<string, UserGoalDocument>,
|
||||
userID: integer,
|
||||
logger: KtLogger
|
||||
) {
|
||||
const returns = await Promise.all(
|
||||
goals.map((goal: GoalDocument) => {
|
||||
const userGoal = userGoalsMap.get(goal.goalID);
|
||||
const returns = await Promise.all(
|
||||
goals.map((goal: GoalDocument) => {
|
||||
const userGoal = userGoalsMap.get(goal.goalID);
|
||||
|
||||
if (!userGoal) {
|
||||
logger.error(
|
||||
`UserGoal:GoalID mismatch ${goal.goalID} - this user has no userGoal for this, yet it is set.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!userGoal) {
|
||||
logger.error(
|
||||
`UserGoal:GoalID mismatch ${goal.goalID} - this user has no userGoal for this, yet it is set.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
return ProcessGoal(goal, userGoal, userID, logger);
|
||||
})
|
||||
);
|
||||
return ProcessGoal(goal, userGoal, userID, logger);
|
||||
})
|
||||
);
|
||||
|
||||
const importInfo = [];
|
||||
const bulkWrite = [];
|
||||
const importInfo = [];
|
||||
const bulkWrite = [];
|
||||
|
||||
for (const ret of returns) {
|
||||
if (!ret) {
|
||||
continue;
|
||||
}
|
||||
for (const ret of returns) {
|
||||
if (!ret) {
|
||||
continue;
|
||||
}
|
||||
|
||||
importInfo.push(ret.import);
|
||||
bulkWrite.push(ret.bwrite);
|
||||
}
|
||||
importInfo.push(ret.import);
|
||||
bulkWrite.push(ret.bwrite);
|
||||
}
|
||||
|
||||
if (bulkWrite.length === 0) {
|
||||
// bulkwrite cannot be an empty array -- this means there's nothing to update or return, then.
|
||||
// i.e. goals was non empty but returns was entirely [undefined, undefined...].
|
||||
return [];
|
||||
}
|
||||
if (bulkWrite.length === 0) {
|
||||
// bulkwrite cannot be an empty array -- this means there's nothing to update or return, then.
|
||||
// i.e. goals was non empty but returns was entirely [undefined, undefined...].
|
||||
return [];
|
||||
}
|
||||
|
||||
await db["user-goals"].bulkWrite(bulkWrite, { ordered: false });
|
||||
await db["user-goals"].bulkWrite(bulkWrite, { ordered: false });
|
||||
|
||||
return importInfo;
|
||||
return importInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,73 +76,73 @@ export async function UpdateGoalsForUser(
|
||||
* to say (i.e. user didnt raise the goal).
|
||||
*/
|
||||
export async function ProcessGoal(
|
||||
goal: GoalDocument,
|
||||
userGoal: UserGoalDocument,
|
||||
userID: integer,
|
||||
logger: KtLogger
|
||||
goal: GoalDocument,
|
||||
userGoal: UserGoalDocument,
|
||||
userID: integer,
|
||||
logger: KtLogger
|
||||
) {
|
||||
const res = await EvaluateGoalForUser(goal, userID, logger);
|
||||
const res = await EvaluateGoalForUser(goal, userID, logger);
|
||||
|
||||
if (!res) {
|
||||
// some sort of error occured - its logged by the previous function.
|
||||
return;
|
||||
}
|
||||
if (!res) {
|
||||
// some sort of error occured - its logged by the previous function.
|
||||
return;
|
||||
}
|
||||
|
||||
// nothing has changed
|
||||
if (userGoal.progress === res.progress && userGoal.outOf === res.outOf) {
|
||||
return;
|
||||
}
|
||||
// nothing has changed
|
||||
if (userGoal.progress === res.progress && userGoal.outOf === res.outOf) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newData = {
|
||||
progress: res.progress,
|
||||
progressHuman: res.progressHuman,
|
||||
outOf: res.outOf,
|
||||
outOfHuman: res.outOfHuman,
|
||||
achieved: res.achieved,
|
||||
};
|
||||
const newData = {
|
||||
progress: res.progress,
|
||||
progressHuman: res.progressHuman,
|
||||
outOf: res.outOf,
|
||||
outOfHuman: res.outOfHuman,
|
||||
achieved: res.achieved,
|
||||
};
|
||||
|
||||
const oldData = {
|
||||
progress: userGoal.progress,
|
||||
progressHuman: userGoal.progressHuman,
|
||||
outOf: userGoal.outOf,
|
||||
outOfHuman: userGoal.outOfHuman,
|
||||
achieved: userGoal.achieved,
|
||||
};
|
||||
const oldData = {
|
||||
progress: userGoal.progress,
|
||||
progressHuman: userGoal.progressHuman,
|
||||
outOf: userGoal.outOf,
|
||||
outOfHuman: userGoal.outOfHuman,
|
||||
achieved: userGoal.achieved,
|
||||
};
|
||||
|
||||
// if this is a newly-achieved goal
|
||||
if (res.achieved && !userGoal.achieved) {
|
||||
RedisPub("goal-achieved", {
|
||||
userID,
|
||||
goalID: goal.goalID,
|
||||
old: oldData,
|
||||
new: newData,
|
||||
});
|
||||
}
|
||||
// if this is a newly-achieved goal
|
||||
if (res.achieved && !userGoal.achieved) {
|
||||
RedisPub("goal-achieved", {
|
||||
userID,
|
||||
goalID: goal.goalID,
|
||||
old: oldData,
|
||||
new: newData,
|
||||
});
|
||||
}
|
||||
|
||||
const bulkWrite = {
|
||||
updateOne: {
|
||||
filter: { _id: userGoal._id! },
|
||||
update: {
|
||||
$set: {
|
||||
...newData,
|
||||
timeAchieved: newData.achieved ? Date.now() : null,
|
||||
// we're guaranteed that this works, because things
|
||||
// that haven't changed return nothing instead of
|
||||
// getting to this point.
|
||||
lastInteraction: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const bulkWrite = {
|
||||
updateOne: {
|
||||
filter: { _id: userGoal._id! },
|
||||
update: {
|
||||
$set: {
|
||||
...newData,
|
||||
timeAchieved: newData.achieved ? Date.now() : null,
|
||||
// we're guaranteed that this works, because things
|
||||
// that haven't changed return nothing instead of
|
||||
// getting to this point.
|
||||
lastInteraction: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
bwrite: bulkWrite,
|
||||
import: {
|
||||
goalID: goal.goalID,
|
||||
old: oldData,
|
||||
new: newData,
|
||||
},
|
||||
};
|
||||
return {
|
||||
bwrite: bulkWrite,
|
||||
import: {
|
||||
goalID: goal.goalID,
|
||||
old: oldData,
|
||||
new: newData,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,57 +154,57 @@ export async function ProcessGoal(
|
||||
* @returns An array of Goals, and an array of userGoals.
|
||||
*/
|
||||
export async function GetRelevantGoals(
|
||||
game: Game,
|
||||
userID: integer,
|
||||
chartIDs: Set<string>,
|
||||
logger: KtLogger
|
||||
game: Game,
|
||||
userID: integer,
|
||||
chartIDs: Set<string>,
|
||||
logger: KtLogger
|
||||
): Promise<{ goals: GoalDocument[]; userGoalsMap: Map<string, UserGoalDocument> }> {
|
||||
const userGoals = await db["user-goals"].find({ game, userID });
|
||||
const userGoals = await db["user-goals"].find({ game, userID });
|
||||
|
||||
logger.verbose(`Found user has ${userGoals.length} goals.`);
|
||||
logger.verbose(`Found user has ${userGoals.length} goals.`);
|
||||
|
||||
if (!userGoals.length) {
|
||||
return { goals: [], userGoalsMap: new Map() };
|
||||
}
|
||||
if (!userGoals.length) {
|
||||
return { goals: [], userGoalsMap: new Map() };
|
||||
}
|
||||
|
||||
const goalIDs = userGoals.map((e) => e.goalID);
|
||||
const goalIDs = userGoals.map((e) => e.goalID);
|
||||
|
||||
const chartIDsArr: string[] = [];
|
||||
for (const c of chartIDs) {
|
||||
chartIDsArr.push(c);
|
||||
}
|
||||
const chartIDsArr: string[] = [];
|
||||
for (const c of chartIDs) {
|
||||
chartIDsArr.push(c);
|
||||
}
|
||||
|
||||
const goals = await Promise.all([
|
||||
// this gets the relevantGoals for direct and multi
|
||||
db.goals.find({
|
||||
"charts.type": { $in: ["single", "multi"] },
|
||||
"charts.data": { $in: chartIDsArr },
|
||||
goalID: { $in: goalIDs },
|
||||
}),
|
||||
db.goals.find({
|
||||
"charts.type": "any",
|
||||
goalID: { $in: goalIDs },
|
||||
}),
|
||||
GetRelevantFolderGoals(goalIDs, chartIDsArr),
|
||||
]).then((r) => r.flat(1));
|
||||
const goals = await Promise.all([
|
||||
// this gets the relevantGoals for direct and multi
|
||||
db.goals.find({
|
||||
"charts.type": { $in: ["single", "multi"] },
|
||||
"charts.data": { $in: chartIDsArr },
|
||||
goalID: { $in: goalIDs },
|
||||
}),
|
||||
db.goals.find({
|
||||
"charts.type": "any",
|
||||
goalID: { $in: goalIDs },
|
||||
}),
|
||||
GetRelevantFolderGoals(goalIDs, chartIDsArr),
|
||||
]).then((r) => r.flat(1));
|
||||
|
||||
const goalSet = new Set(goals.map((e) => e.goalID));
|
||||
const goalSet = new Set(goals.map((e) => e.goalID));
|
||||
|
||||
const userGoalsMap: Map<string, UserGoalDocument> = new Map();
|
||||
const userGoalsMap: Map<string, UserGoalDocument> = new Map();
|
||||
|
||||
for (const userGoal of userGoals) {
|
||||
if (!goalSet.has(userGoal.goalID)) {
|
||||
continue;
|
||||
}
|
||||
// since these are guaranteed to be unique, lets make a hot map of goalID -> userGoalDocument, so we can
|
||||
// pull them in for post-processing and filter out the userGoalDocuments that aren't relevant.
|
||||
userGoalsMap.set(userGoal.goalID, userGoal);
|
||||
}
|
||||
for (const userGoal of userGoals) {
|
||||
if (!goalSet.has(userGoal.goalID)) {
|
||||
continue;
|
||||
}
|
||||
// since these are guaranteed to be unique, lets make a hot map of goalID -> userGoalDocument, so we can
|
||||
// pull them in for post-processing and filter out the userGoalDocuments that aren't relevant.
|
||||
userGoalsMap.set(userGoal.goalID, userGoal);
|
||||
}
|
||||
|
||||
return {
|
||||
goals,
|
||||
userGoalsMap,
|
||||
};
|
||||
return {
|
||||
goals,
|
||||
userGoalsMap,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -212,34 +212,34 @@ export async function GetRelevantGoals(
|
||||
* of chartIDsArr.
|
||||
*/
|
||||
export function GetRelevantFolderGoals(goalIDs: string[], chartIDsArr: string[]) {
|
||||
// Slightly black magic - this is kind of like doing an SQL join.
|
||||
// it's weird to do this in mongodb, but this seems like the right
|
||||
// way to actually handle this.
|
||||
// Slightly black magic - this is kind of like doing an SQL join.
|
||||
// it's weird to do this in mongodb, but this seems like the right
|
||||
// way to actually handle this.
|
||||
|
||||
return db.goals.aggregate([
|
||||
{
|
||||
$match: {
|
||||
"charts.type": "folder",
|
||||
goalID: { $in: goalIDs },
|
||||
},
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "folder-chart-lookup",
|
||||
localField: "charts.data",
|
||||
foreignField: "folderID",
|
||||
as: "folderCharts",
|
||||
},
|
||||
},
|
||||
{
|
||||
$match: {
|
||||
"folderCharts.chartID": { $in: chartIDsArr },
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
folderCharts: 0,
|
||||
},
|
||||
},
|
||||
]);
|
||||
return db.goals.aggregate([
|
||||
{
|
||||
$match: {
|
||||
"charts.type": "folder",
|
||||
goalID: { $in: goalIDs },
|
||||
},
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "folder-chart-lookup",
|
||||
localField: "charts.data",
|
||||
foreignField: "folderID",
|
||||
as: "folderCharts",
|
||||
},
|
||||
},
|
||||
{
|
||||
$match: {
|
||||
"folderCharts.chartID": { $in: chartIDsArr },
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
folderCharts: 0,
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -8,27 +8,27 @@ import { integer } from "tachi-common";
|
||||
* If a lock exists for the user, the lock is returned.
|
||||
*/
|
||||
export function GetOrSetUserLock(userID: integer) {
|
||||
return db["import-locks"].findOneAndUpdate(
|
||||
{
|
||||
userID,
|
||||
},
|
||||
{
|
||||
$set: { userID },
|
||||
},
|
||||
{
|
||||
upsert: true,
|
||||
// this is marked as deprecated, but it shouldn't be, as returnDocument: "before"
|
||||
// does nothing.
|
||||
returnOriginal: true,
|
||||
}
|
||||
);
|
||||
return db["import-locks"].findOneAndUpdate(
|
||||
{
|
||||
userID,
|
||||
},
|
||||
{
|
||||
$set: { userID },
|
||||
},
|
||||
{
|
||||
upsert: true,
|
||||
// this is marked as deprecated, but it shouldn't be, as returnDocument: "before"
|
||||
// does nothing.
|
||||
returnOriginal: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a users import lock.
|
||||
*/
|
||||
export function RemoveUserLock(userID: integer) {
|
||||
return db["import-locks"].remove({
|
||||
userID,
|
||||
});
|
||||
return db["import-locks"].remove({
|
||||
userID,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,347 +11,347 @@ import { CloseAllConnections } from "../../../../test-utils/close-connections";
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
function CreateMockGII(...garr: [string, boolean][]) {
|
||||
return garr.map((e) => ({
|
||||
goalID: e[0],
|
||||
old: {},
|
||||
new: { achieved: e[1] },
|
||||
})) as unknown as GoalImportInfo[];
|
||||
return garr.map((e) => ({
|
||||
goalID: e[0],
|
||||
old: {},
|
||||
new: { achieved: e[1] },
|
||||
})) as unknown as GoalImportInfo[];
|
||||
}
|
||||
|
||||
t.test("#UpdateUsersMilestones", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(() => db.milestones.insert(TestingIIDXSPMilestone));
|
||||
t.beforeEach(() =>
|
||||
db["user-milestones"].insert({
|
||||
achieved: false,
|
||||
game: "iidx",
|
||||
milestoneID: TestingIIDXSPMilestone.milestoneID,
|
||||
playtype: "SP",
|
||||
progress: 0,
|
||||
timeAchieved: null,
|
||||
timeSet: 0,
|
||||
userID: 1,
|
||||
})
|
||||
);
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(() => db.milestones.insert(TestingIIDXSPMilestone));
|
||||
t.beforeEach(() =>
|
||||
db["user-milestones"].insert({
|
||||
achieved: false,
|
||||
game: "iidx",
|
||||
milestoneID: TestingIIDXSPMilestone.milestoneID,
|
||||
playtype: "SP",
|
||||
progress: 0,
|
||||
timeAchieved: null,
|
||||
timeSet: 0,
|
||||
userID: 1,
|
||||
})
|
||||
);
|
||||
|
||||
t.test("Test with clean achieved milestone.", async (t) => {
|
||||
const res = await UpdateUsersMilestones(
|
||||
CreateMockGII(
|
||||
["eg_goal_1", true],
|
||||
["eg_goal_2", true],
|
||||
["eg_goal_3", true],
|
||||
["eg_goal_4", true]
|
||||
),
|
||||
"iidx",
|
||||
["SP"],
|
||||
1,
|
||||
logger
|
||||
);
|
||||
t.test("Test with clean achieved milestone.", async (t) => {
|
||||
const res = await UpdateUsersMilestones(
|
||||
CreateMockGII(
|
||||
["eg_goal_1", true],
|
||||
["eg_goal_2", true],
|
||||
["eg_goal_3", true],
|
||||
["eg_goal_4", true]
|
||||
),
|
||||
"iidx",
|
||||
["SP"],
|
||||
1,
|
||||
logger
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
res,
|
||||
[
|
||||
{
|
||||
milestoneID: "example_milestone_id",
|
||||
old: {
|
||||
progress: 0,
|
||||
achieved: false,
|
||||
},
|
||||
new: {
|
||||
progress: 4,
|
||||
achieved: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"Should correctly assert the milestone is achieved."
|
||||
);
|
||||
t.strictSame(
|
||||
res,
|
||||
[
|
||||
{
|
||||
milestoneID: "example_milestone_id",
|
||||
old: {
|
||||
progress: 0,
|
||||
achieved: false,
|
||||
},
|
||||
new: {
|
||||
progress: 4,
|
||||
achieved: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"Should correctly assert the milestone is achieved."
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Test with unclean achieved milestone.", async (t) => {
|
||||
const res = await UpdateUsersMilestones(
|
||||
CreateMockGII(
|
||||
["eg_goal_1", true],
|
||||
["eg_goal_2", true],
|
||||
["eg_goal_3", true],
|
||||
["eg_goal_4", true],
|
||||
["x_goal_1", true],
|
||||
["x_goal_2", true],
|
||||
["x_goal_3", true]
|
||||
),
|
||||
"iidx",
|
||||
["SP"],
|
||||
1,
|
||||
logger
|
||||
);
|
||||
t.test("Test with unclean achieved milestone.", async (t) => {
|
||||
const res = await UpdateUsersMilestones(
|
||||
CreateMockGII(
|
||||
["eg_goal_1", true],
|
||||
["eg_goal_2", true],
|
||||
["eg_goal_3", true],
|
||||
["eg_goal_4", true],
|
||||
["x_goal_1", true],
|
||||
["x_goal_2", true],
|
||||
["x_goal_3", true]
|
||||
),
|
||||
"iidx",
|
||||
["SP"],
|
||||
1,
|
||||
logger
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
res,
|
||||
[
|
||||
{
|
||||
milestoneID: "example_milestone_id",
|
||||
old: {
|
||||
progress: 0,
|
||||
achieved: false,
|
||||
},
|
||||
new: {
|
||||
progress: 4,
|
||||
achieved: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"Should correctly assert the milestone is achieved."
|
||||
);
|
||||
t.strictSame(
|
||||
res,
|
||||
[
|
||||
{
|
||||
milestoneID: "example_milestone_id",
|
||||
old: {
|
||||
progress: 0,
|
||||
achieved: false,
|
||||
},
|
||||
new: {
|
||||
progress: 4,
|
||||
achieved: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"Should correctly assert the milestone is achieved."
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Test with increased progress on milestone.", async (t) => {
|
||||
const res = await UpdateUsersMilestones(
|
||||
CreateMockGII(["eg_goal_1", true], ["eg_goal_2", true]),
|
||||
"iidx",
|
||||
["SP"],
|
||||
1,
|
||||
logger
|
||||
);
|
||||
t.test("Test with increased progress on milestone.", async (t) => {
|
||||
const res = await UpdateUsersMilestones(
|
||||
CreateMockGII(["eg_goal_1", true], ["eg_goal_2", true]),
|
||||
"iidx",
|
||||
["SP"],
|
||||
1,
|
||||
logger
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
res,
|
||||
[
|
||||
{
|
||||
milestoneID: "example_milestone_id",
|
||||
old: {
|
||||
progress: 0,
|
||||
achieved: false,
|
||||
},
|
||||
new: {
|
||||
progress: 2,
|
||||
achieved: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
"Should correctly assert the milestone progress has increased."
|
||||
);
|
||||
t.strictSame(
|
||||
res,
|
||||
[
|
||||
{
|
||||
milestoneID: "example_milestone_id",
|
||||
old: {
|
||||
progress: 0,
|
||||
achieved: false,
|
||||
},
|
||||
new: {
|
||||
progress: 2,
|
||||
achieved: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
"Should correctly assert the milestone progress has increased."
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Test with no new goals on milestone.", async (t) => {
|
||||
const res = await UpdateUsersMilestones([], "iidx", ["SP"], 1, logger);
|
||||
t.test("Test with no new goals on milestone.", async (t) => {
|
||||
const res = await UpdateUsersMilestones([], "iidx", ["SP"], 1, logger);
|
||||
|
||||
t.strictSame(res, [], "Should correctly return no changes.");
|
||||
t.strictSame(res, [], "Should correctly return no changes.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Test with new goals on multiple milestones.", async (t) => {
|
||||
delete TestingIIDXSPMilestone._id;
|
||||
await db.milestones.insert(
|
||||
// @ts-expect-error lol
|
||||
deepmerge(TestingIIDXSPMilestone, {
|
||||
milestoneID: "some_other_milestone_with_mutual_goals",
|
||||
})
|
||||
);
|
||||
await db["user-milestones"].insert({
|
||||
achieved: false,
|
||||
game: "iidx",
|
||||
milestoneID: "some_other_milestone_with_mutual_goals",
|
||||
playtype: "SP",
|
||||
progress: 0,
|
||||
timeAchieved: null,
|
||||
timeSet: 0,
|
||||
userID: 1,
|
||||
});
|
||||
t.test("Test with new goals on multiple milestones.", async (t) => {
|
||||
delete TestingIIDXSPMilestone._id;
|
||||
await db.milestones.insert(
|
||||
// @ts-expect-error lol
|
||||
deepmerge(TestingIIDXSPMilestone, {
|
||||
milestoneID: "some_other_milestone_with_mutual_goals",
|
||||
})
|
||||
);
|
||||
await db["user-milestones"].insert({
|
||||
achieved: false,
|
||||
game: "iidx",
|
||||
milestoneID: "some_other_milestone_with_mutual_goals",
|
||||
playtype: "SP",
|
||||
progress: 0,
|
||||
timeAchieved: null,
|
||||
timeSet: 0,
|
||||
userID: 1,
|
||||
});
|
||||
|
||||
const res = await UpdateUsersMilestones(
|
||||
CreateMockGII(
|
||||
["eg_goal_1", true],
|
||||
["eg_goal_2", true],
|
||||
["eg_goal_3", true],
|
||||
["eg_goal_4", true],
|
||||
["x_goal_1", true],
|
||||
["x_goal_2", true],
|
||||
["x_goal_3", true]
|
||||
),
|
||||
"iidx",
|
||||
["SP", "DP"],
|
||||
1,
|
||||
logger
|
||||
);
|
||||
const res = await UpdateUsersMilestones(
|
||||
CreateMockGII(
|
||||
["eg_goal_1", true],
|
||||
["eg_goal_2", true],
|
||||
["eg_goal_3", true],
|
||||
["eg_goal_4", true],
|
||||
["x_goal_1", true],
|
||||
["x_goal_2", true],
|
||||
["x_goal_3", true]
|
||||
),
|
||||
"iidx",
|
||||
["SP", "DP"],
|
||||
1,
|
||||
logger
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
res,
|
||||
[
|
||||
{
|
||||
milestoneID: "example_milestone_id",
|
||||
old: {
|
||||
progress: 0,
|
||||
achieved: false,
|
||||
},
|
||||
new: {
|
||||
progress: 4,
|
||||
achieved: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
milestoneID: "some_other_milestone_with_mutual_goals",
|
||||
old: {
|
||||
progress: 0,
|
||||
achieved: false,
|
||||
},
|
||||
new: {
|
||||
progress: 4,
|
||||
achieved: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"Should correctly assert the milestones are achieved."
|
||||
);
|
||||
t.end();
|
||||
});
|
||||
t.strictSame(
|
||||
res,
|
||||
[
|
||||
{
|
||||
milestoneID: "example_milestone_id",
|
||||
old: {
|
||||
progress: 0,
|
||||
achieved: false,
|
||||
},
|
||||
new: {
|
||||
progress: 4,
|
||||
achieved: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
milestoneID: "some_other_milestone_with_mutual_goals",
|
||||
old: {
|
||||
progress: 0,
|
||||
achieved: false,
|
||||
},
|
||||
new: {
|
||||
progress: 4,
|
||||
achieved: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"Should correctly assert the milestones are achieved."
|
||||
);
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Test with multiple milestones that only some match", async (t) => {
|
||||
delete TestingIIDXSPMilestone._id;
|
||||
await db.milestones.insert([
|
||||
// @ts-expect-error lol
|
||||
deepmerge(TestingIIDXSPMilestone, {
|
||||
milestoneID: "some_other_milestone_with_mutual_goals",
|
||||
}),
|
||||
deepmerge(
|
||||
TestingIIDXSPMilestone,
|
||||
{
|
||||
milestoneID: "dp_milestone",
|
||||
playtype: "DP",
|
||||
milestoneData: [
|
||||
{
|
||||
goals: [{ goalID: "foobar" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ arrayMerge: (d, s) => s }
|
||||
),
|
||||
deepmerge(
|
||||
TestingIIDXSPMilestone,
|
||||
{
|
||||
milestoneID: "other_game_milestone",
|
||||
game: "museca",
|
||||
playtype: "Single",
|
||||
milestoneData: [
|
||||
{
|
||||
goals: [{ goalID: "foo" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ arrayMerge: (d, s) => s }
|
||||
),
|
||||
deepmerge(
|
||||
TestingIIDXSPMilestone,
|
||||
{
|
||||
milestoneID: "iidx_with_not_goal",
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
milestoneData: [
|
||||
{
|
||||
goals: [{ goalID: "not_real_goal_id" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ arrayMerge: (d, s) => s }
|
||||
),
|
||||
]);
|
||||
t.test("Test with multiple milestones that only some match", async (t) => {
|
||||
delete TestingIIDXSPMilestone._id;
|
||||
await db.milestones.insert([
|
||||
// @ts-expect-error lol
|
||||
deepmerge(TestingIIDXSPMilestone, {
|
||||
milestoneID: "some_other_milestone_with_mutual_goals",
|
||||
}),
|
||||
deepmerge(
|
||||
TestingIIDXSPMilestone,
|
||||
{
|
||||
milestoneID: "dp_milestone",
|
||||
playtype: "DP",
|
||||
milestoneData: [
|
||||
{
|
||||
goals: [{ goalID: "foobar" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ arrayMerge: (d, s) => s }
|
||||
),
|
||||
deepmerge(
|
||||
TestingIIDXSPMilestone,
|
||||
{
|
||||
milestoneID: "other_game_milestone",
|
||||
game: "museca",
|
||||
playtype: "Single",
|
||||
milestoneData: [
|
||||
{
|
||||
goals: [{ goalID: "foo" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ arrayMerge: (d, s) => s }
|
||||
),
|
||||
deepmerge(
|
||||
TestingIIDXSPMilestone,
|
||||
{
|
||||
milestoneID: "iidx_with_not_goal",
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
milestoneData: [
|
||||
{
|
||||
goals: [{ goalID: "not_real_goal_id" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ arrayMerge: (d, s) => s }
|
||||
),
|
||||
]);
|
||||
|
||||
await db["user-milestones"].insert([
|
||||
{
|
||||
achieved: false,
|
||||
game: "iidx",
|
||||
milestoneID: "some_other_milestone_with_mutual_goals",
|
||||
playtype: "SP",
|
||||
progress: 0,
|
||||
timeAchieved: null,
|
||||
timeSet: 0,
|
||||
userID: 1,
|
||||
},
|
||||
{
|
||||
achieved: false,
|
||||
game: "iidx",
|
||||
milestoneID: "dp_milestone",
|
||||
playtype: "DP",
|
||||
progress: 0,
|
||||
timeAchieved: null,
|
||||
timeSet: 0,
|
||||
userID: 1,
|
||||
},
|
||||
{
|
||||
achieved: false,
|
||||
game: "museca",
|
||||
milestoneID: "other_game_milestone",
|
||||
playtype: "Single",
|
||||
progress: 0,
|
||||
timeAchieved: null,
|
||||
timeSet: 0,
|
||||
userID: 1,
|
||||
},
|
||||
{
|
||||
achieved: false,
|
||||
game: "iidx",
|
||||
milestoneID: "iidx_with_not_goal",
|
||||
playtype: "SP",
|
||||
progress: 0,
|
||||
timeAchieved: null,
|
||||
timeSet: 0,
|
||||
userID: 1,
|
||||
},
|
||||
]);
|
||||
await db["user-milestones"].insert([
|
||||
{
|
||||
achieved: false,
|
||||
game: "iidx",
|
||||
milestoneID: "some_other_milestone_with_mutual_goals",
|
||||
playtype: "SP",
|
||||
progress: 0,
|
||||
timeAchieved: null,
|
||||
timeSet: 0,
|
||||
userID: 1,
|
||||
},
|
||||
{
|
||||
achieved: false,
|
||||
game: "iidx",
|
||||
milestoneID: "dp_milestone",
|
||||
playtype: "DP",
|
||||
progress: 0,
|
||||
timeAchieved: null,
|
||||
timeSet: 0,
|
||||
userID: 1,
|
||||
},
|
||||
{
|
||||
achieved: false,
|
||||
game: "museca",
|
||||
milestoneID: "other_game_milestone",
|
||||
playtype: "Single",
|
||||
progress: 0,
|
||||
timeAchieved: null,
|
||||
timeSet: 0,
|
||||
userID: 1,
|
||||
},
|
||||
{
|
||||
achieved: false,
|
||||
game: "iidx",
|
||||
milestoneID: "iidx_with_not_goal",
|
||||
playtype: "SP",
|
||||
progress: 0,
|
||||
timeAchieved: null,
|
||||
timeSet: 0,
|
||||
userID: 1,
|
||||
},
|
||||
]);
|
||||
|
||||
const res = await UpdateUsersMilestones(
|
||||
CreateMockGII(
|
||||
["eg_goal_1", true],
|
||||
["eg_goal_2", true],
|
||||
["eg_goal_3", true],
|
||||
["eg_goal_4", true],
|
||||
["x_goal_1", true],
|
||||
["x_goal_2", true],
|
||||
["x_goal_3", true]
|
||||
),
|
||||
"iidx",
|
||||
["SP", "DP"],
|
||||
1,
|
||||
logger
|
||||
);
|
||||
const res = await UpdateUsersMilestones(
|
||||
CreateMockGII(
|
||||
["eg_goal_1", true],
|
||||
["eg_goal_2", true],
|
||||
["eg_goal_3", true],
|
||||
["eg_goal_4", true],
|
||||
["x_goal_1", true],
|
||||
["x_goal_2", true],
|
||||
["x_goal_3", true]
|
||||
),
|
||||
"iidx",
|
||||
["SP", "DP"],
|
||||
1,
|
||||
logger
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
res,
|
||||
[
|
||||
{
|
||||
milestoneID: "example_milestone_id",
|
||||
old: {
|
||||
progress: 0,
|
||||
achieved: false,
|
||||
},
|
||||
new: {
|
||||
progress: 4,
|
||||
achieved: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
milestoneID: "some_other_milestone_with_mutual_goals",
|
||||
old: {
|
||||
progress: 0,
|
||||
achieved: false,
|
||||
},
|
||||
new: {
|
||||
progress: 4,
|
||||
achieved: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"Should correctly assert the milestones are achieved."
|
||||
);
|
||||
t.end();
|
||||
});
|
||||
t.strictSame(
|
||||
res,
|
||||
[
|
||||
{
|
||||
milestoneID: "example_milestone_id",
|
||||
old: {
|
||||
progress: 0,
|
||||
achieved: false,
|
||||
},
|
||||
new: {
|
||||
progress: 4,
|
||||
achieved: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
milestoneID: "some_other_milestone_with_mutual_goals",
|
||||
old: {
|
||||
progress: 0,
|
||||
achieved: false,
|
||||
},
|
||||
new: {
|
||||
progress: 4,
|
||||
achieved: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"Should correctly assert the milestones are achieved."
|
||||
);
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { KtLogger } from "../../../logger/logger";
|
||||
import {
|
||||
integer,
|
||||
Game,
|
||||
Playtypes,
|
||||
GoalImportInfo,
|
||||
UserMilestoneDocument,
|
||||
MilestoneImportInfo,
|
||||
MilestoneDocument,
|
||||
integer,
|
||||
Game,
|
||||
Playtypes,
|
||||
GoalImportInfo,
|
||||
UserMilestoneDocument,
|
||||
MilestoneImportInfo,
|
||||
MilestoneDocument,
|
||||
} from "tachi-common";
|
||||
import db from "../../../../external/mongo/db";
|
||||
import { BulkWriteUpdateOneOperation } from "mongodb";
|
||||
@@ -18,154 +18,154 @@ import { RedisPub, RedisSub } from "../../../../external/redis/redis-IPC";
|
||||
* about goals from imports)
|
||||
*/
|
||||
export function ProcessMilestoneFromGII(
|
||||
milestone: MilestoneDocument,
|
||||
gii: Map<string, GoalImportInfo["new"]>
|
||||
milestone: MilestoneDocument,
|
||||
gii: Map<string, GoalImportInfo["new"]>
|
||||
) {
|
||||
const goalIDs = GetGoalIDsFromMilestone(milestone);
|
||||
const goalIDs = GetGoalIDsFromMilestone(milestone);
|
||||
|
||||
let progress = 0;
|
||||
let progress = 0;
|
||||
|
||||
for (const goalID of goalIDs) {
|
||||
const userInfo = gii.get(goalID);
|
||||
for (const goalID of goalIDs) {
|
||||
const userInfo = gii.get(goalID);
|
||||
|
||||
if (!userInfo) {
|
||||
continue;
|
||||
}
|
||||
if (!userInfo) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!userInfo.achieved) {
|
||||
continue;
|
||||
}
|
||||
if (!userInfo.achieved) {
|
||||
continue;
|
||||
}
|
||||
|
||||
progress++;
|
||||
}
|
||||
progress++;
|
||||
}
|
||||
|
||||
const outOf = CalculateMilestoneOutOf(milestone, goalIDs);
|
||||
const outOf = CalculateMilestoneOutOf(milestone, goalIDs);
|
||||
|
||||
// milestone achieved!
|
||||
if (progress >= outOf) {
|
||||
return { achieved: true, progress };
|
||||
}
|
||||
// milestone achieved!
|
||||
if (progress >= outOf) {
|
||||
return { achieved: true, progress };
|
||||
}
|
||||
|
||||
return { achieved: false, progress };
|
||||
return { achieved: false, progress };
|
||||
}
|
||||
|
||||
export async function UpdateUsersMilestones(
|
||||
importGoalInfo: GoalImportInfo[],
|
||||
game: Game,
|
||||
playtypes: Playtypes[Game][],
|
||||
userID: integer,
|
||||
logger: KtLogger
|
||||
importGoalInfo: GoalImportInfo[],
|
||||
game: Game,
|
||||
playtypes: Playtypes[Game][],
|
||||
userID: integer,
|
||||
logger: KtLogger
|
||||
) {
|
||||
const userGoalInfoMap: Map<string, GoalImportInfo["new"]> = new Map();
|
||||
const userGoalInfoMap: Map<string, GoalImportInfo["new"]> = new Map();
|
||||
|
||||
const goalIDs = [];
|
||||
for (const e of importGoalInfo) {
|
||||
userGoalInfoMap.set(e.goalID, e.new);
|
||||
goalIDs.push(e.goalID);
|
||||
}
|
||||
const goalIDs = [];
|
||||
for (const e of importGoalInfo) {
|
||||
userGoalInfoMap.set(e.goalID, e.new);
|
||||
goalIDs.push(e.goalID);
|
||||
}
|
||||
|
||||
const { milestones, userMilestones } = await GetRelevantMilestones(
|
||||
goalIDs,
|
||||
game,
|
||||
playtypes,
|
||||
userID,
|
||||
logger
|
||||
);
|
||||
const { milestones, userMilestones } = await GetRelevantMilestones(
|
||||
goalIDs,
|
||||
game,
|
||||
playtypes,
|
||||
userID,
|
||||
logger
|
||||
);
|
||||
|
||||
// create a map here to avoid linear searching when
|
||||
// co-iterating
|
||||
const userMilestoneMap = new Map();
|
||||
for (const um of userMilestones) {
|
||||
userMilestoneMap.set(um.milestoneID, um);
|
||||
}
|
||||
// create a map here to avoid linear searching when
|
||||
// co-iterating
|
||||
const userMilestoneMap = new Map();
|
||||
for (const um of userMilestones) {
|
||||
userMilestoneMap.set(um.milestoneID, um);
|
||||
}
|
||||
|
||||
const importGoalMap = new Map();
|
||||
const importGoalMap = new Map();
|
||||
|
||||
for (const ig of importGoalInfo) {
|
||||
importGoalMap.set(ig.goalID, ig.new);
|
||||
}
|
||||
for (const ig of importGoalInfo) {
|
||||
importGoalMap.set(ig.goalID, ig.new);
|
||||
}
|
||||
|
||||
const bwrite: BulkWriteUpdateOneOperation<UserMilestoneDocument>[] = [];
|
||||
const bwrite: BulkWriteUpdateOneOperation<UserMilestoneDocument>[] = [];
|
||||
|
||||
const importMilestoneInfo: MilestoneImportInfo[] = [];
|
||||
const importMilestoneInfo: MilestoneImportInfo[] = [];
|
||||
|
||||
for (const milestone of milestones) {
|
||||
const { achieved, progress } = ProcessMilestoneFromGII(milestone, importGoalMap);
|
||||
for (const milestone of milestones) {
|
||||
const { achieved, progress } = ProcessMilestoneFromGII(milestone, importGoalMap);
|
||||
|
||||
const userMilestone = userMilestoneMap.get(milestone.milestoneID);
|
||||
const userMilestone = userMilestoneMap.get(milestone.milestoneID);
|
||||
|
||||
if (!userMilestone) {
|
||||
logger.severe(
|
||||
`Invalid state achieved in milestone processing - processed milestone that user did not have? ${milestone.milestoneID}`
|
||||
);
|
||||
if (!userMilestone) {
|
||||
logger.severe(
|
||||
`Invalid state achieved in milestone processing - processed milestone that user did not have? ${milestone.milestoneID}`
|
||||
);
|
||||
|
||||
throw new Error("Invalid state achieved in milestone processing.");
|
||||
}
|
||||
throw new Error("Invalid state achieved in milestone processing.");
|
||||
}
|
||||
|
||||
bwrite.push({
|
||||
updateOne: {
|
||||
filter: { milestoneID: milestone.milestoneID },
|
||||
update: {
|
||||
$set: {
|
||||
achieved,
|
||||
progress,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
bwrite.push({
|
||||
updateOne: {
|
||||
filter: { milestoneID: milestone.milestoneID },
|
||||
update: {
|
||||
$set: {
|
||||
achieved,
|
||||
progress,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const milestoneInfo = {
|
||||
milestoneID: userMilestone.milestoneID,
|
||||
old: {
|
||||
progress: userMilestone.progress,
|
||||
achieved: userMilestone.achieved,
|
||||
},
|
||||
new: {
|
||||
progress,
|
||||
achieved,
|
||||
},
|
||||
};
|
||||
const milestoneInfo = {
|
||||
milestoneID: userMilestone.milestoneID,
|
||||
old: {
|
||||
progress: userMilestone.progress,
|
||||
achieved: userMilestone.achieved,
|
||||
},
|
||||
new: {
|
||||
progress,
|
||||
achieved,
|
||||
},
|
||||
};
|
||||
|
||||
if (progress !== userMilestone.progress) {
|
||||
importMilestoneInfo.push(milestoneInfo);
|
||||
}
|
||||
if (progress !== userMilestone.progress) {
|
||||
importMilestoneInfo.push(milestoneInfo);
|
||||
}
|
||||
|
||||
if (achieved && !userMilestone.achieved) {
|
||||
RedisPub("milestone-achieved", {
|
||||
userID,
|
||||
...milestoneInfo,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (achieved && !userMilestone.achieved) {
|
||||
RedisPub("milestone-achieved", {
|
||||
userID,
|
||||
...milestoneInfo,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (bwrite.length !== 0) {
|
||||
await db["user-milestones"].bulkWrite(bwrite, { ordered: false });
|
||||
}
|
||||
if (bwrite.length !== 0) {
|
||||
await db["user-milestones"].bulkWrite(bwrite, { ordered: false });
|
||||
}
|
||||
|
||||
return importMilestoneInfo;
|
||||
return importMilestoneInfo;
|
||||
}
|
||||
|
||||
async function GetRelevantMilestones(
|
||||
goalIDs: string[],
|
||||
game: Game,
|
||||
playtypes: Playtypes[Game][],
|
||||
userID: integer,
|
||||
logger: KtLogger
|
||||
goalIDs: string[],
|
||||
game: Game,
|
||||
playtypes: Playtypes[Game][],
|
||||
userID: integer,
|
||||
logger: KtLogger
|
||||
) {
|
||||
const userMilestones = await db["user-milestones"].find({
|
||||
game,
|
||||
playtype: { $in: playtypes },
|
||||
userID,
|
||||
});
|
||||
const userMilestones = await db["user-milestones"].find({
|
||||
game,
|
||||
playtype: { $in: playtypes },
|
||||
userID,
|
||||
});
|
||||
|
||||
logger.debug(`Found ${userMilestones.length} user-milestones.`);
|
||||
logger.debug(`Found ${userMilestones.length} user-milestones.`);
|
||||
|
||||
const milestones = await db.milestones.find({
|
||||
milestoneID: { $in: userMilestones.map((e) => e.milestoneID) },
|
||||
"milestoneData.goals.goalID": { $in: goalIDs },
|
||||
});
|
||||
const milestones = await db.milestones.find({
|
||||
milestoneID: { $in: userMilestones.map((e) => e.milestoneID) },
|
||||
"milestoneData.goals.goalID": { $in: goalIDs },
|
||||
});
|
||||
|
||||
logger.debug(`Found ${milestones.length} relevant milestones.`);
|
||||
logger.debug(`Found ${milestones.length} relevant milestones.`);
|
||||
|
||||
return { userMilestones, milestones };
|
||||
return { userMilestones, milestones };
|
||||
}
|
||||
|
||||
@@ -12,242 +12,242 @@ import deepmerge from "deepmerge";
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
const batchManualScore: BatchManualScore = {
|
||||
score: 500,
|
||||
lamp: "HARD CLEAR",
|
||||
matchType: "songTitle",
|
||||
identifier: "5.1.1.",
|
||||
playtype: "SP",
|
||||
difficulty: "ANOTHER",
|
||||
score: 500,
|
||||
lamp: "HARD CLEAR",
|
||||
matchType: "songTitle",
|
||||
identifier: "5.1.1.",
|
||||
playtype: "SP",
|
||||
difficulty: "ANOTHER",
|
||||
};
|
||||
|
||||
const batchManualContext: BatchManualContext = { game: "iidx", service: "foo", version: "27" };
|
||||
|
||||
t.test("#OrphanScore", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should orphan data and context information.", async (t) => {
|
||||
const res = await OrphanScore(
|
||||
"ir/direct-manual",
|
||||
1,
|
||||
batchManualScore,
|
||||
batchManualContext,
|
||||
"Example Error Message",
|
||||
logger
|
||||
);
|
||||
t.test("Should orphan data and context information.", async (t) => {
|
||||
const res = await OrphanScore(
|
||||
"ir/direct-manual",
|
||||
1,
|
||||
batchManualScore,
|
||||
batchManualContext,
|
||||
"Example Error Message",
|
||||
logger
|
||||
);
|
||||
|
||||
t.equal(res.success, true);
|
||||
t.equal(
|
||||
res.orphanID,
|
||||
`O${fjsh.hash(
|
||||
{
|
||||
importType: "ir/direct-manual",
|
||||
data: batchManualScore,
|
||||
context: batchManualContext,
|
||||
userID: 1,
|
||||
},
|
||||
"sha256"
|
||||
)}`,
|
||||
"Should be a checksum of the orphan's contents."
|
||||
);
|
||||
t.equal(res.success, true);
|
||||
t.equal(
|
||||
res.orphanID,
|
||||
`O${fjsh.hash(
|
||||
{
|
||||
importType: "ir/direct-manual",
|
||||
data: batchManualScore,
|
||||
context: batchManualContext,
|
||||
userID: 1,
|
||||
},
|
||||
"sha256"
|
||||
)}`,
|
||||
"Should be a checksum of the orphan's contents."
|
||||
);
|
||||
|
||||
const dbCheck = await db["orphan-scores"].findOne({
|
||||
orphanID: res.orphanID,
|
||||
});
|
||||
const dbCheck = await db["orphan-scores"].findOne({
|
||||
orphanID: res.orphanID,
|
||||
});
|
||||
|
||||
t.hasStrict(dbCheck, {
|
||||
orphanID: res.orphanID,
|
||||
userID: 1,
|
||||
data: batchManualScore,
|
||||
context: batchManualContext,
|
||||
importType: "ir/direct-manual",
|
||||
errMsg: "Example Error Message",
|
||||
});
|
||||
t.hasStrict(dbCheck, {
|
||||
orphanID: res.orphanID,
|
||||
userID: 1,
|
||||
data: batchManualScore,
|
||||
context: batchManualContext,
|
||||
importType: "ir/direct-manual",
|
||||
errMsg: "Example Error Message",
|
||||
});
|
||||
|
||||
t.ok(
|
||||
Math.abs(dbCheck!.timeInserted - Date.now()) < 10_000,
|
||||
"timeInserted should be decently close to now."
|
||||
);
|
||||
t.ok(
|
||||
Math.abs(dbCheck!.timeInserted - Date.now()) < 10_000,
|
||||
"timeInserted should be decently close to now."
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should skip orphans that already exist.", async (t) => {
|
||||
const res1 = await OrphanScore(
|
||||
"ir/direct-manual",
|
||||
1,
|
||||
batchManualScore,
|
||||
batchManualContext,
|
||||
"Example Error Message",
|
||||
logger
|
||||
);
|
||||
t.test("Should skip orphans that already exist.", async (t) => {
|
||||
const res1 = await OrphanScore(
|
||||
"ir/direct-manual",
|
||||
1,
|
||||
batchManualScore,
|
||||
batchManualContext,
|
||||
"Example Error Message",
|
||||
logger
|
||||
);
|
||||
|
||||
const res2 = await OrphanScore(
|
||||
"ir/direct-manual",
|
||||
1,
|
||||
batchManualScore,
|
||||
batchManualContext,
|
||||
"Example Error Message",
|
||||
logger
|
||||
);
|
||||
const res2 = await OrphanScore(
|
||||
"ir/direct-manual",
|
||||
1,
|
||||
batchManualScore,
|
||||
batchManualContext,
|
||||
"Example Error Message",
|
||||
logger
|
||||
);
|
||||
|
||||
t.equal(res2.success, false);
|
||||
t.equal(res1.orphanID, res2.orphanID);
|
||||
t.equal(res2.success, false);
|
||||
t.equal(res1.orphanID, res2.orphanID);
|
||||
|
||||
const dbCheck = await db["orphan-scores"].count();
|
||||
const dbCheck = await db["orphan-scores"].count();
|
||||
|
||||
t.equal(dbCheck, 1);
|
||||
});
|
||||
t.equal(dbCheck, 1);
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#ReprocessOrphan", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
const orphanDoc: OrphanScoreDocument = {
|
||||
context: batchManualContext,
|
||||
data: batchManualScore,
|
||||
errMsg: "foo",
|
||||
importType: "ir/direct-manual",
|
||||
orphanID: "foo",
|
||||
timeInserted: 0,
|
||||
userID: 1,
|
||||
};
|
||||
const orphanDoc: OrphanScoreDocument = {
|
||||
context: batchManualContext,
|
||||
data: batchManualScore,
|
||||
errMsg: "foo",
|
||||
importType: "ir/direct-manual",
|
||||
orphanID: "foo",
|
||||
timeInserted: 0,
|
||||
userID: 1,
|
||||
};
|
||||
|
||||
t.test("Should convert and insert an orphan if parents are found", async (t) => {
|
||||
// this orphan doc is for 5.1.1. SPA, which definitely exists in
|
||||
// the test DB.
|
||||
const res = await ReprocessOrphan(orphanDoc, logger);
|
||||
t.test("Should convert and insert an orphan if parents are found", async (t) => {
|
||||
// this orphan doc is for 5.1.1. SPA, which definitely exists in
|
||||
// the test DB.
|
||||
const res = await ReprocessOrphan(orphanDoc, logger);
|
||||
|
||||
t.hasStrict(
|
||||
res,
|
||||
{
|
||||
success: true,
|
||||
type: "ScoreImported",
|
||||
message: null,
|
||||
content: {
|
||||
score: {
|
||||
game: "iidx",
|
||||
service: "foo (DIRECT-MANUAL)",
|
||||
comment: null,
|
||||
importType: "ir/direct-manual",
|
||||
timeAchieved: null,
|
||||
scoreMeta: {},
|
||||
scoreData: {
|
||||
lampIndex: 5,
|
||||
gradeIndex: 1,
|
||||
esd: 60.546875,
|
||||
lamp: "HARD CLEAR",
|
||||
score: 500,
|
||||
grade: "E",
|
||||
percent: 31.806615776081426,
|
||||
hitData: {},
|
||||
hitMeta: {},
|
||||
},
|
||||
highlight: false,
|
||||
userID: 1,
|
||||
calculatedData: {
|
||||
ktRating: 0.6840944877852578,
|
||||
ktLampRating: 10.6,
|
||||
BPI: -15,
|
||||
"K%": null,
|
||||
},
|
||||
songID: 1,
|
||||
chartID: "c2311194e3897ddb5745b1760d2c0141f933e683",
|
||||
scoreID:
|
||||
"R7a3a2b04bd4882ec06c198d78297fe3d56561502c7b134c067214bbfdf4f1602",
|
||||
playtype: "SP",
|
||||
isPrimary: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"Should successfully import the score."
|
||||
);
|
||||
t.hasStrict(
|
||||
res,
|
||||
{
|
||||
success: true,
|
||||
type: "ScoreImported",
|
||||
message: null,
|
||||
content: {
|
||||
score: {
|
||||
game: "iidx",
|
||||
service: "foo (DIRECT-MANUAL)",
|
||||
comment: null,
|
||||
importType: "ir/direct-manual",
|
||||
timeAchieved: null,
|
||||
scoreMeta: {},
|
||||
scoreData: {
|
||||
lampIndex: 5,
|
||||
gradeIndex: 1,
|
||||
esd: 60.546875,
|
||||
lamp: "HARD CLEAR",
|
||||
score: 500,
|
||||
grade: "E",
|
||||
percent: 31.806615776081426,
|
||||
hitData: {},
|
||||
hitMeta: {},
|
||||
},
|
||||
highlight: false,
|
||||
userID: 1,
|
||||
calculatedData: {
|
||||
ktRating: 0.6840944877852578,
|
||||
ktLampRating: 10.6,
|
||||
BPI: -15,
|
||||
"K%": null,
|
||||
},
|
||||
songID: 1,
|
||||
chartID: "c2311194e3897ddb5745b1760d2c0141f933e683",
|
||||
scoreID:
|
||||
"R7a3a2b04bd4882ec06c198d78297fe3d56561502c7b134c067214bbfdf4f1602",
|
||||
playtype: "SP",
|
||||
isPrimary: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"Should successfully import the score."
|
||||
);
|
||||
|
||||
const orphan = await db["orphan-scores"].findOne({ orphanID: orphanDoc.orphanID });
|
||||
const orphan = await db["orphan-scores"].findOne({ orphanID: orphanDoc.orphanID });
|
||||
|
||||
t.equal(orphan, null, "Should remove the orphan document.");
|
||||
t.equal(orphan, null, "Should remove the orphan document.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should delete the orphan doc and return null if the score is invalid.", async (t) => {
|
||||
await db["orphan-scores"].insert(orphanDoc);
|
||||
const res = await ReprocessOrphan(
|
||||
deepmerge(orphanDoc, {
|
||||
data: {
|
||||
score: 99999,
|
||||
},
|
||||
}),
|
||||
logger
|
||||
);
|
||||
t.test("Should delete the orphan doc and return null if the score is invalid.", async (t) => {
|
||||
await db["orphan-scores"].insert(orphanDoc);
|
||||
const res = await ReprocessOrphan(
|
||||
deepmerge(orphanDoc, {
|
||||
data: {
|
||||
score: 99999,
|
||||
},
|
||||
}),
|
||||
logger
|
||||
);
|
||||
|
||||
t.equal(res, null);
|
||||
t.equal(res, null);
|
||||
|
||||
const orphan = await db["orphan-scores"].findOne({ orphanID: orphanDoc.orphanID });
|
||||
const orphan = await db["orphan-scores"].findOne({ orphanID: orphanDoc.orphanID });
|
||||
|
||||
t.equal(orphan, null, "Should remove the orphan document.");
|
||||
t.equal(orphan, null, "Should remove the orphan document.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test(
|
||||
"Should keep the orphan doc and return false if no parents could be found.",
|
||||
async (t) => {
|
||||
await db["orphan-scores"].insert(orphanDoc);
|
||||
const res = await ReprocessOrphan(
|
||||
deepmerge(orphanDoc, {
|
||||
data: {
|
||||
identifier: "NONSENSE CHART TITLE",
|
||||
},
|
||||
}),
|
||||
logger
|
||||
);
|
||||
t.test(
|
||||
"Should keep the orphan doc and return false if no parents could be found.",
|
||||
async (t) => {
|
||||
await db["orphan-scores"].insert(orphanDoc);
|
||||
const res = await ReprocessOrphan(
|
||||
deepmerge(orphanDoc, {
|
||||
data: {
|
||||
identifier: "NONSENSE CHART TITLE",
|
||||
},
|
||||
}),
|
||||
logger
|
||||
);
|
||||
|
||||
t.equal(res, false);
|
||||
t.equal(res, false);
|
||||
|
||||
const orphan = await db["orphan-scores"].findOne({ orphanID: orphanDoc.orphanID });
|
||||
const orphan = await db["orphan-scores"].findOne({ orphanID: orphanDoc.orphanID });
|
||||
|
||||
t.not(orphan, null, "Should not remove the orphan document.");
|
||||
t.not(orphan, null, "Should not remove the orphan document.");
|
||||
|
||||
t.end();
|
||||
}
|
||||
);
|
||||
t.end();
|
||||
}
|
||||
);
|
||||
|
||||
t.test(
|
||||
"Should keep the orphan doc and return false if an internal failure was triggered.",
|
||||
async (t) => {
|
||||
await db["orphan-scores"].insert(orphanDoc);
|
||||
const GAZER_SHA256 = "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d";
|
||||
t.test(
|
||||
"Should keep the orphan doc and return false if an internal failure was triggered.",
|
||||
async (t) => {
|
||||
await db["orphan-scores"].insert(orphanDoc);
|
||||
const GAZER_SHA256 = "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d";
|
||||
|
||||
// force an internal failure by removing all bms songs and then
|
||||
// leaving a valid bms chart to match with.
|
||||
await db.songs.bms.remove({});
|
||||
// force an internal failure by removing all bms songs and then
|
||||
// leaving a valid bms chart to match with.
|
||||
await db.songs.bms.remove({});
|
||||
|
||||
const res = await ReprocessOrphan(
|
||||
deepmerge(orphanDoc, {
|
||||
context: {
|
||||
game: "bms",
|
||||
},
|
||||
data: {
|
||||
matchType: "bmsChartHash",
|
||||
identifier: GAZER_SHA256,
|
||||
},
|
||||
}),
|
||||
logger
|
||||
);
|
||||
const res = await ReprocessOrphan(
|
||||
deepmerge(orphanDoc, {
|
||||
context: {
|
||||
game: "bms",
|
||||
},
|
||||
data: {
|
||||
matchType: "bmsChartHash",
|
||||
identifier: GAZER_SHA256,
|
||||
},
|
||||
}),
|
||||
logger
|
||||
);
|
||||
|
||||
t.equal(res, false);
|
||||
t.equal(res, false);
|
||||
|
||||
const orphan = await db["orphan-scores"].findOne({ orphanID: orphanDoc.orphanID });
|
||||
const orphan = await db["orphan-scores"].findOne({ orphanID: orphanDoc.orphanID });
|
||||
|
||||
t.not(orphan, null, "Should not remove the orphan document.");
|
||||
t.not(orphan, null, "Should not remove the orphan document.");
|
||||
|
||||
t.end();
|
||||
}
|
||||
);
|
||||
t.end();
|
||||
}
|
||||
);
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import db from "../../../../external/mongo/db";
|
||||
import {
|
||||
ConverterFunction,
|
||||
ConverterFnReturnOrFailure,
|
||||
ImportTypeContextMap,
|
||||
ImportTypeDataMap,
|
||||
OrphanScoreDocument,
|
||||
ConverterFunction,
|
||||
ConverterFnReturnOrFailure,
|
||||
ImportTypeContextMap,
|
||||
ImportTypeDataMap,
|
||||
OrphanScoreDocument,
|
||||
} from "../../import-types/common/types";
|
||||
import { ImportTypes, integer } from "tachi-common";
|
||||
import fjsh from "fast-json-stable-hash";
|
||||
import { KtLogger } from "../../../logger/logger";
|
||||
import { Converters } from "../../import-types/converters";
|
||||
import {
|
||||
ConverterFailure,
|
||||
InternalFailure,
|
||||
KTDataNotFoundFailure,
|
||||
ConverterFailure,
|
||||
InternalFailure,
|
||||
KTDataNotFoundFailure,
|
||||
} from "../common/converter-failures";
|
||||
import { ProcessSuccessfulConverterReturn } from "../score-importing/score-importing";
|
||||
|
||||
@@ -24,39 +24,39 @@ import { ProcessSuccessfulConverterReturn } from "../score-importing/score-impor
|
||||
* @returns Returns { success: true | false, orphanID }
|
||||
*/
|
||||
export async function OrphanScore<T extends ImportTypes = ImportTypes>(
|
||||
importType: T,
|
||||
userID: integer,
|
||||
data: ImportTypeDataMap[T],
|
||||
context: ImportTypeContextMap[T],
|
||||
errMsg: string | null,
|
||||
logger: KtLogger
|
||||
importType: T,
|
||||
userID: integer,
|
||||
data: ImportTypeDataMap[T],
|
||||
context: ImportTypeContextMap[T],
|
||||
errMsg: string | null,
|
||||
logger: KtLogger
|
||||
) {
|
||||
const orphan: Pick<OrphanScoreDocument, "importType" | "data" | "context" | "userID"> = {
|
||||
importType,
|
||||
data,
|
||||
context: context,
|
||||
userID,
|
||||
};
|
||||
const orphan: Pick<OrphanScoreDocument, "importType" | "data" | "context" | "userID"> = {
|
||||
importType,
|
||||
data,
|
||||
context: context,
|
||||
userID,
|
||||
};
|
||||
|
||||
const orphanID = `O${fjsh.hash(orphan, "sha256")}`;
|
||||
const orphanID = `O${fjsh.hash(orphan, "sha256")}`;
|
||||
|
||||
const exists = await db["orphan-scores"].findOne({ orphanID });
|
||||
const exists = await db["orphan-scores"].findOne({ orphanID });
|
||||
|
||||
if (exists) {
|
||||
logger.debug(`Skipped orphaning score ${orphanID} because it already exists.`);
|
||||
return { success: false, orphanID };
|
||||
}
|
||||
if (exists) {
|
||||
logger.debug(`Skipped orphaning score ${orphanID} because it already exists.`);
|
||||
return { success: false, orphanID };
|
||||
}
|
||||
|
||||
const orphanScoreDoc: OrphanScoreDocument = {
|
||||
...orphan,
|
||||
orphanID,
|
||||
errMsg,
|
||||
timeInserted: Date.now(),
|
||||
};
|
||||
const orphanScoreDoc: OrphanScoreDocument = {
|
||||
...orphan,
|
||||
orphanID,
|
||||
errMsg,
|
||||
timeInserted: Date.now(),
|
||||
};
|
||||
|
||||
await db["orphan-scores"].insert(orphanScoreDoc);
|
||||
await db["orphan-scores"].insert(orphanScoreDoc);
|
||||
|
||||
return { success: true, orphanID };
|
||||
return { success: true, orphanID };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,50 +68,50 @@ export async function OrphanScore<T extends ImportTypes = ImportTypes>(
|
||||
* ImportProcessingInfo on success.
|
||||
*/
|
||||
export async function ReprocessOrphan(orphan: OrphanScoreDocument, logger: KtLogger) {
|
||||
const ConverterFunction = Converters[orphan.importType] as ConverterFunction<
|
||||
ImportTypeDataMap[ImportTypes],
|
||||
ImportTypeContextMap[ImportTypes]
|
||||
>;
|
||||
const ConverterFunction = Converters[orphan.importType] as ConverterFunction<
|
||||
ImportTypeDataMap[ImportTypes],
|
||||
ImportTypeContextMap[ImportTypes]
|
||||
>;
|
||||
|
||||
let res: ConverterFnReturnOrFailure;
|
||||
let res: ConverterFnReturnOrFailure;
|
||||
|
||||
try {
|
||||
res = await ConverterFunction(orphan.data, orphan.context, orphan.importType, logger);
|
||||
} catch (err) {
|
||||
// this is impossible to test, so we're going to ignore it
|
||||
/* istanbul ignore next */
|
||||
if (!(err instanceof ConverterFailure)) {
|
||||
logger.error(`Converter function ${orphan.importType} returned unexpected error.`, {
|
||||
err,
|
||||
});
|
||||
throw err; // throw this higher up, i guess.
|
||||
}
|
||||
try {
|
||||
res = await ConverterFunction(orphan.data, orphan.context, orphan.importType, logger);
|
||||
} catch (err) {
|
||||
// this is impossible to test, so we're going to ignore it
|
||||
/* istanbul ignore next */
|
||||
if (!(err instanceof ConverterFailure)) {
|
||||
logger.error(`Converter function ${orphan.importType} returned unexpected error.`, {
|
||||
err,
|
||||
});
|
||||
throw err; // throw this higher up, i guess.
|
||||
}
|
||||
|
||||
res = err;
|
||||
}
|
||||
res = err;
|
||||
}
|
||||
|
||||
// If the data still can't be found, we do nothing about it.
|
||||
if (res instanceof KTDataNotFoundFailure) {
|
||||
logger.debug(`Unorphaning ${orphan.orphanID} failed. (${res.message})`);
|
||||
return false;
|
||||
} else if (res instanceof InternalFailure) {
|
||||
logger.error(`Orphan Internal Failure - ${res.message}, OrphanID ${orphan.orphanID}`);
|
||||
// If the data still can't be found, we do nothing about it.
|
||||
if (res instanceof KTDataNotFoundFailure) {
|
||||
logger.debug(`Unorphaning ${orphan.orphanID} failed. (${res.message})`);
|
||||
return false;
|
||||
} else if (res instanceof InternalFailure) {
|
||||
logger.error(`Orphan Internal Failure - ${res.message}, OrphanID ${orphan.orphanID}`);
|
||||
|
||||
return false;
|
||||
} else if (res instanceof ConverterFailure) {
|
||||
logger.warn(
|
||||
`Recieved ConverterFailure ${res.message} on orphan ${orphan.orphanID}. Removing orphan.`
|
||||
);
|
||||
return false;
|
||||
} else if (res instanceof ConverterFailure) {
|
||||
logger.warn(
|
||||
`Recieved ConverterFailure ${res.message} on orphan ${orphan.orphanID}. Removing orphan.`
|
||||
);
|
||||
|
||||
// @danger - This could go terribly, if there's a mistake in the converterFN we might accidentally
|
||||
// remove a users score.
|
||||
await db["orphan-scores"].remove({ orphanID: orphan.orphanID });
|
||||
// @danger - This could go terribly, if there's a mistake in the converterFN we might accidentally
|
||||
// remove a users score.
|
||||
await db["orphan-scores"].remove({ orphanID: orphan.orphanID });
|
||||
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
await db["orphan-scores"].remove({ orphanID: orphan.orphanID });
|
||||
await db["orphan-scores"].remove({ orphanID: orphan.orphanID });
|
||||
|
||||
// else, import the orphan.
|
||||
return ProcessSuccessfulConverterReturn(orphan.userID, res, logger);
|
||||
// else, import the orphan.
|
||||
return ProcessSuccessfulConverterReturn(orphan.userID, res, logger);
|
||||
}
|
||||
|
||||
@@ -14,159 +14,159 @@ let IIDXScore = TestingIIDXSPScore;
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
t.test("#CreatePBDoc", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(() => {
|
||||
// monk adds _id onto the file when you import it, so lets try and avoid that
|
||||
IIDXScore = GetKTDataJSON("./tachi/iidx-score.json");
|
||||
});
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(() => {
|
||||
// monk adds _id onto the file when you import it, so lets try and avoid that
|
||||
IIDXScore = GetKTDataJSON("./tachi/iidx-score.json");
|
||||
});
|
||||
|
||||
const chartID = Testing511SPA.chartID;
|
||||
const chartID = Testing511SPA.chartID;
|
||||
|
||||
const ExamplePBDoc = {
|
||||
chartID,
|
||||
userID: 1,
|
||||
songID: 1,
|
||||
// rankingInfo -- is not present because it is not added until post-processing.
|
||||
highlight: false,
|
||||
isPrimary: true,
|
||||
timeAchieved: 1619454485988,
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
comments: [],
|
||||
composedFrom: {
|
||||
scorePB: IIDXScore.scoreID,
|
||||
lampPB: "LAMP_PB_ID",
|
||||
},
|
||||
scoreData: {
|
||||
score: IIDXScore.scoreData.score,
|
||||
percent: IIDXScore.scoreData.percent,
|
||||
esd: IIDXScore.scoreData.esd,
|
||||
grade: IIDXScore.scoreData.grade,
|
||||
gradeIndex: IIDXScore.scoreData.gradeIndex,
|
||||
hitData: IIDXScore.scoreData.hitData,
|
||||
lamp: "FULL COMBO",
|
||||
lampIndex: lamps.iidx.indexOf("FULL COMBO"),
|
||||
hitMeta: { bp: 1 },
|
||||
},
|
||||
calculatedData: {
|
||||
ktRating: IIDXScore.calculatedData.ktRating,
|
||||
ktLampRating: 12,
|
||||
},
|
||||
};
|
||||
const ExamplePBDoc = {
|
||||
chartID,
|
||||
userID: 1,
|
||||
songID: 1,
|
||||
// rankingInfo -- is not present because it is not added until post-processing.
|
||||
highlight: false,
|
||||
isPrimary: true,
|
||||
timeAchieved: 1619454485988,
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
comments: [],
|
||||
composedFrom: {
|
||||
scorePB: IIDXScore.scoreID,
|
||||
lampPB: "LAMP_PB_ID",
|
||||
},
|
||||
scoreData: {
|
||||
score: IIDXScore.scoreData.score,
|
||||
percent: IIDXScore.scoreData.percent,
|
||||
esd: IIDXScore.scoreData.esd,
|
||||
grade: IIDXScore.scoreData.grade,
|
||||
gradeIndex: IIDXScore.scoreData.gradeIndex,
|
||||
hitData: IIDXScore.scoreData.hitData,
|
||||
lamp: "FULL COMBO",
|
||||
lampIndex: lamps.iidx.indexOf("FULL COMBO"),
|
||||
hitMeta: { bp: 1 },
|
||||
},
|
||||
calculatedData: {
|
||||
ktRating: IIDXScore.calculatedData.ktRating,
|
||||
ktLampRating: 12,
|
||||
},
|
||||
};
|
||||
|
||||
t.test(
|
||||
"(IIDX) Should use the GameSpecificMergeFN to also join the BP PB if necessary.",
|
||||
async (t) => {
|
||||
await db.scores.remove({});
|
||||
await db.scores.insert([
|
||||
IIDXScore,
|
||||
deepmerge(IIDXScore, {
|
||||
scoreData: {
|
||||
lamp: "FULL COMBO",
|
||||
lampIndex: lamps.iidx.indexOf("FULL COMBO"),
|
||||
score: 0,
|
||||
percent: 0,
|
||||
hitMeta: {
|
||||
bp: 15,
|
||||
},
|
||||
},
|
||||
calculatedData: {
|
||||
ktLampRating: 12,
|
||||
},
|
||||
scoreID: "LAMP_PB_ID",
|
||||
}),
|
||||
deepmerge(IIDXScore, {
|
||||
scoreData: {
|
||||
lamp: "CLEAR",
|
||||
lampIndex: lamps.iidx.indexOf("CLEAR"),
|
||||
score: 1,
|
||||
percent: 1,
|
||||
hitMeta: {
|
||||
bp: 5,
|
||||
},
|
||||
},
|
||||
calculatedData: {
|
||||
ktLampRating: 10,
|
||||
},
|
||||
scoreID: "BP_PB_ID",
|
||||
}),
|
||||
]);
|
||||
t.test(
|
||||
"(IIDX) Should use the GameSpecificMergeFN to also join the BP PB if necessary.",
|
||||
async (t) => {
|
||||
await db.scores.remove({});
|
||||
await db.scores.insert([
|
||||
IIDXScore,
|
||||
deepmerge(IIDXScore, {
|
||||
scoreData: {
|
||||
lamp: "FULL COMBO",
|
||||
lampIndex: lamps.iidx.indexOf("FULL COMBO"),
|
||||
score: 0,
|
||||
percent: 0,
|
||||
hitMeta: {
|
||||
bp: 15,
|
||||
},
|
||||
},
|
||||
calculatedData: {
|
||||
ktLampRating: 12,
|
||||
},
|
||||
scoreID: "LAMP_PB_ID",
|
||||
}),
|
||||
deepmerge(IIDXScore, {
|
||||
scoreData: {
|
||||
lamp: "CLEAR",
|
||||
lampIndex: lamps.iidx.indexOf("CLEAR"),
|
||||
score: 1,
|
||||
percent: 1,
|
||||
hitMeta: {
|
||||
bp: 5,
|
||||
},
|
||||
},
|
||||
calculatedData: {
|
||||
ktLampRating: 10,
|
||||
},
|
||||
scoreID: "BP_PB_ID",
|
||||
}),
|
||||
]);
|
||||
|
||||
const res = await CreatePBDoc(1, chartID, logger);
|
||||
const res = await CreatePBDoc(1, chartID, logger);
|
||||
|
||||
t.not(res, undefined, "Should actually return something.");
|
||||
t.not(res, undefined, "Should actually return something.");
|
||||
|
||||
t.strictSame(
|
||||
res,
|
||||
deepmerge(ExamplePBDoc, {
|
||||
composedFrom: {
|
||||
other: [{ name: "Best BP", scoreID: "BP_PB_ID" }],
|
||||
},
|
||||
scoreData: {
|
||||
hitMeta: {
|
||||
bp: 5,
|
||||
gsm: null,
|
||||
gauge: null,
|
||||
gaugeHistory: null,
|
||||
},
|
||||
},
|
||||
}),
|
||||
"Should correctly return a merged PBDocument with BP"
|
||||
);
|
||||
t.strictSame(
|
||||
res,
|
||||
deepmerge(ExamplePBDoc, {
|
||||
composedFrom: {
|
||||
other: [{ name: "Best BP", scoreID: "BP_PB_ID" }],
|
||||
},
|
||||
scoreData: {
|
||||
hitMeta: {
|
||||
bp: 5,
|
||||
gsm: null,
|
||||
gauge: null,
|
||||
gaugeHistory: null,
|
||||
},
|
||||
},
|
||||
}),
|
||||
"Should correctly return a merged PBDocument with BP"
|
||||
);
|
||||
|
||||
t.end();
|
||||
}
|
||||
);
|
||||
t.end();
|
||||
}
|
||||
);
|
||||
|
||||
t.test("Should merge a score and lamp PB into one document.", async (t) => {
|
||||
const d = deepmerge(IIDXScore, {
|
||||
scoreData: {
|
||||
lamp: "FULL COMBO",
|
||||
lampIndex: lamps.iidx.indexOf("FULL COMBO"),
|
||||
score: 0,
|
||||
percent: 0,
|
||||
hitMeta: {
|
||||
bp: 1,
|
||||
},
|
||||
},
|
||||
calculatedData: {
|
||||
ktLampRating: 12,
|
||||
},
|
||||
scoreID: "LAMP_PB_ID",
|
||||
});
|
||||
t.test("Should merge a score and lamp PB into one document.", async (t) => {
|
||||
const d = deepmerge(IIDXScore, {
|
||||
scoreData: {
|
||||
lamp: "FULL COMBO",
|
||||
lampIndex: lamps.iidx.indexOf("FULL COMBO"),
|
||||
score: 0,
|
||||
percent: 0,
|
||||
hitMeta: {
|
||||
bp: 1,
|
||||
},
|
||||
},
|
||||
calculatedData: {
|
||||
ktLampRating: 12,
|
||||
},
|
||||
scoreID: "LAMP_PB_ID",
|
||||
});
|
||||
|
||||
await db.scores.remove({});
|
||||
await db.scores.insert([IIDXScore, d]);
|
||||
await db.scores.remove({});
|
||||
await db.scores.insert([IIDXScore, d]);
|
||||
|
||||
const res = await CreatePBDoc(1, chartID, logger);
|
||||
const res = await CreatePBDoc(1, chartID, logger);
|
||||
|
||||
t.not(res, undefined, "Should actually return something.");
|
||||
t.not(res, undefined, "Should actually return something.");
|
||||
|
||||
t.strictSame(res, ExamplePBDoc, "Should correctly return a merged PBDocument");
|
||||
t.strictSame(res, ExamplePBDoc, "Should correctly return a merged PBDocument");
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should bail safely if no score exists when one should", async (t) => {
|
||||
// a work of genius
|
||||
const fakeLogger = {
|
||||
severe: () => (severeCalled = true),
|
||||
} as unknown as KtLogger;
|
||||
t.test("Should bail safely if no score exists when one should", async (t) => {
|
||||
// a work of genius
|
||||
const fakeLogger = {
|
||||
severe: () => (severeCalled = true),
|
||||
} as unknown as KtLogger;
|
||||
|
||||
let severeCalled = false;
|
||||
let severeCalled = false;
|
||||
|
||||
await db.scores.remove({});
|
||||
await db.scores.remove({});
|
||||
|
||||
const res = await CreatePBDoc(1, chartID, fakeLogger);
|
||||
const res = await CreatePBDoc(1, chartID, fakeLogger);
|
||||
|
||||
t.equal(res, undefined, "Should return nothing (and emit a warning)");
|
||||
t.equal(res, undefined, "Should return nothing (and emit a warning)");
|
||||
|
||||
t.equal(severeCalled, true, "Severe logging should have been called.");
|
||||
t.equal(severeCalled, true, "Severe logging should have been called.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -7,89 +7,89 @@ import { BulkWriteUpdateOneOperation } from "mongodb";
|
||||
export type PBScoreDocumentNoRank = Omit<PBScoreDocument, "rankingData">;
|
||||
|
||||
export async function CreatePBDoc(userID: integer, chartID: string, logger: KtLogger) {
|
||||
const scorePB = await db.scores.findOne(
|
||||
{
|
||||
userID,
|
||||
chartID,
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
"scoreData.percent": -1,
|
||||
},
|
||||
}
|
||||
);
|
||||
const scorePB = await db.scores.findOne(
|
||||
{
|
||||
userID,
|
||||
chartID,
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
"scoreData.percent": -1,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!scorePB) {
|
||||
logger.severe(`User has no scores on chart, but a PB was attempted to be created?`, {
|
||||
chartID,
|
||||
userID,
|
||||
});
|
||||
return; // ??
|
||||
}
|
||||
if (!scorePB) {
|
||||
logger.severe(`User has no scores on chart, but a PB was attempted to be created?`, {
|
||||
chartID,
|
||||
userID,
|
||||
});
|
||||
return; // ??
|
||||
}
|
||||
|
||||
const lampPB = (await db.scores.findOne(
|
||||
{
|
||||
userID,
|
||||
chartID,
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
"scoreData.lampIndex": -1,
|
||||
},
|
||||
}
|
||||
)) as ScoreDocument; // guaranteed to not be null, as this always resolves
|
||||
// to atleast one score (and we got ScorePB above, so we know there's
|
||||
// atleast one).
|
||||
const lampPB = (await db.scores.findOne(
|
||||
{
|
||||
userID,
|
||||
chartID,
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
"scoreData.lampIndex": -1,
|
||||
},
|
||||
}
|
||||
)) as ScoreDocument; // guaranteed to not be null, as this always resolves
|
||||
// to atleast one score (and we got ScorePB above, so we know there's
|
||||
// atleast one).
|
||||
|
||||
const pbDoc = await MergeScoreLampIntoPB(userID, scorePB, lampPB, logger);
|
||||
const pbDoc = await MergeScoreLampIntoPB(userID, scorePB, lampPB, logger);
|
||||
|
||||
if (!pbDoc) {
|
||||
return;
|
||||
}
|
||||
if (!pbDoc) {
|
||||
return;
|
||||
}
|
||||
|
||||
// finally, return our full pbDoc, that does NOT have the ranking props.
|
||||
// (We will add those later)
|
||||
return pbDoc;
|
||||
// finally, return our full pbDoc, that does NOT have the ranking props.
|
||||
// (We will add those later)
|
||||
return pbDoc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates users' rankings on a given chart.
|
||||
*/
|
||||
export async function UpdateChartRanking(chartID: string) {
|
||||
const scores = await db["personal-bests"].find(
|
||||
{ chartID },
|
||||
{
|
||||
sort: {
|
||||
"scoreData.percent": -1,
|
||||
timeAchieved: -1,
|
||||
},
|
||||
}
|
||||
);
|
||||
const scores = await db["personal-bests"].find(
|
||||
{ chartID },
|
||||
{
|
||||
sort: {
|
||||
"scoreData.percent": -1,
|
||||
timeAchieved: -1,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const bwrite: BulkWriteUpdateOneOperation<PBScoreDocument>[] = [];
|
||||
const bwrite: BulkWriteUpdateOneOperation<PBScoreDocument>[] = [];
|
||||
|
||||
let rank = 0;
|
||||
let rank = 0;
|
||||
|
||||
for (let i = 0; i < scores.length; i++) {
|
||||
const score = scores[i];
|
||||
rank++;
|
||||
for (let i = 0; i < scores.length; i++) {
|
||||
const score = scores[i];
|
||||
rank++;
|
||||
|
||||
bwrite.push({
|
||||
updateOne: {
|
||||
filter: { chartID: score.chartID, userID: score.userID },
|
||||
update: {
|
||||
$set: {
|
||||
rankingInfo: {
|
||||
rank,
|
||||
outOf: scores.length,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
bwrite.push({
|
||||
updateOne: {
|
||||
filter: { chartID: score.chartID, userID: score.userID },
|
||||
update: {
|
||||
$set: {
|
||||
rankingInfo: {
|
||||
rank,
|
||||
outOf: scores.length,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await db["personal-bests"].bulkWrite(bwrite, { ordered: false });
|
||||
await db["personal-bests"].bulkWrite(bwrite, { ordered: false });
|
||||
}
|
||||
|
||||
// Explicit acknowledgement that typing this properly simply takes too much time
|
||||
@@ -97,67 +97,67 @@ export async function UpdateChartRanking(chartID: string) {
|
||||
// They return true on success, false on failure, and mutate their arguments.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const GAME_SPECIFIC_MERGE_FNS: Record<string, any> = {
|
||||
iidx: IIDXMergeFn,
|
||||
sdvx: SDVXMergeFn,
|
||||
iidx: IIDXMergeFn,
|
||||
sdvx: SDVXMergeFn,
|
||||
};
|
||||
|
||||
async function MergeScoreLampIntoPB(
|
||||
userID: integer,
|
||||
scorePB: ScoreDocument,
|
||||
lampPB: ScoreDocument,
|
||||
logger: KtLogger
|
||||
userID: integer,
|
||||
scorePB: ScoreDocument,
|
||||
lampPB: ScoreDocument,
|
||||
logger: KtLogger
|
||||
): Promise<PBScoreDocumentNoRank | void> {
|
||||
// @hack
|
||||
// since time cannot be negative, this is a rough hack
|
||||
// to resolve nullable timeAchieveds without hitting NaN.
|
||||
let timeAchieved: number | null = Math.max(
|
||||
scorePB.timeAchieved ?? -1,
|
||||
lampPB.timeAchieved ?? -1
|
||||
);
|
||||
// @hack
|
||||
// since time cannot be negative, this is a rough hack
|
||||
// to resolve nullable timeAchieveds without hitting NaN.
|
||||
let timeAchieved: number | null = Math.max(
|
||||
scorePB.timeAchieved ?? -1,
|
||||
lampPB.timeAchieved ?? -1
|
||||
);
|
||||
|
||||
if (timeAchieved === -1) {
|
||||
timeAchieved = null;
|
||||
}
|
||||
if (timeAchieved === -1) {
|
||||
timeAchieved = null;
|
||||
}
|
||||
|
||||
const pbDoc: PBScoreDocumentNoRank = {
|
||||
composedFrom: {
|
||||
scorePB: scorePB.scoreID,
|
||||
lampPB: lampPB.scoreID,
|
||||
},
|
||||
chartID: scorePB.chartID,
|
||||
comments: [scorePB.comment, lampPB.comment].filter((e) => e !== null) as string[],
|
||||
userID,
|
||||
songID: scorePB.songID,
|
||||
highlight: scorePB.highlight || lampPB.highlight,
|
||||
timeAchieved,
|
||||
game: scorePB.game,
|
||||
playtype: scorePB.playtype,
|
||||
isPrimary: scorePB.isPrimary,
|
||||
scoreData: {
|
||||
score: scorePB.scoreData.score,
|
||||
percent: scorePB.scoreData.percent,
|
||||
esd: scorePB.scoreData.esd,
|
||||
grade: scorePB.scoreData.grade,
|
||||
gradeIndex: scorePB.scoreData.gradeIndex,
|
||||
lamp: lampPB.scoreData.lamp,
|
||||
lampIndex: lampPB.scoreData.lampIndex,
|
||||
hitData: scorePB.scoreData.hitData,
|
||||
hitMeta: scorePB.scoreData.hitMeta, // this will probably be overrode by game-specific fns
|
||||
},
|
||||
calculatedData: scorePB.calculatedData,
|
||||
};
|
||||
const pbDoc: PBScoreDocumentNoRank = {
|
||||
composedFrom: {
|
||||
scorePB: scorePB.scoreID,
|
||||
lampPB: lampPB.scoreID,
|
||||
},
|
||||
chartID: scorePB.chartID,
|
||||
comments: [scorePB.comment, lampPB.comment].filter((e) => e !== null) as string[],
|
||||
userID,
|
||||
songID: scorePB.songID,
|
||||
highlight: scorePB.highlight || lampPB.highlight,
|
||||
timeAchieved,
|
||||
game: scorePB.game,
|
||||
playtype: scorePB.playtype,
|
||||
isPrimary: scorePB.isPrimary,
|
||||
scoreData: {
|
||||
score: scorePB.scoreData.score,
|
||||
percent: scorePB.scoreData.percent,
|
||||
esd: scorePB.scoreData.esd,
|
||||
grade: scorePB.scoreData.grade,
|
||||
gradeIndex: scorePB.scoreData.gradeIndex,
|
||||
lamp: lampPB.scoreData.lamp,
|
||||
lampIndex: lampPB.scoreData.lampIndex,
|
||||
hitData: scorePB.scoreData.hitData,
|
||||
hitMeta: scorePB.scoreData.hitMeta, // this will probably be overrode by game-specific fns
|
||||
},
|
||||
calculatedData: scorePB.calculatedData,
|
||||
};
|
||||
|
||||
const GameSpecificMergeFn = GAME_SPECIFIC_MERGE_FNS[scorePB.game];
|
||||
if (GameSpecificMergeFn) {
|
||||
const success = await GameSpecificMergeFn(pbDoc, scorePB, lampPB, logger);
|
||||
const GameSpecificMergeFn = GAME_SPECIFIC_MERGE_FNS[scorePB.game];
|
||||
if (GameSpecificMergeFn) {
|
||||
const success = await GameSpecificMergeFn(pbDoc, scorePB, lampPB, logger);
|
||||
|
||||
// If the mergeFn returns false, this means something has gone
|
||||
// rather wrong. We just return undefined here, which in turn
|
||||
// tells our calling code to skip this PB entirely.
|
||||
if (success === false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If the mergeFn returns false, this means something has gone
|
||||
// rather wrong. We just return undefined here, which in turn
|
||||
// tells our calling code to skip this PB entirely.
|
||||
if (success === false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return pbDoc;
|
||||
return pbDoc;
|
||||
}
|
||||
|
||||
@@ -6,57 +6,57 @@ import { CalculateVF6 } from "../calculated-data/stats";
|
||||
import { InternalFailure } from "../common/converter-failures";
|
||||
|
||||
export async function IIDXMergeFn(
|
||||
pbDoc: PBScoreDocument<"iidx:SP" | "iidx:DP">,
|
||||
scorePB: ScoreDocument<"iidx:SP" | "iidx:DP">,
|
||||
lampPB: ScoreDocument<"iidx:SP" | "iidx:DP">,
|
||||
logger: KtLogger
|
||||
pbDoc: PBScoreDocument<"iidx:SP" | "iidx:DP">,
|
||||
scorePB: ScoreDocument<"iidx:SP" | "iidx:DP">,
|
||||
lampPB: ScoreDocument<"iidx:SP" | "iidx:DP">,
|
||||
logger: KtLogger
|
||||
): Promise<boolean> {
|
||||
// lampRating needs to be updated.
|
||||
pbDoc.calculatedData.ktLampRating = lampPB.calculatedData.ktLampRating;
|
||||
// lampRating needs to be updated.
|
||||
pbDoc.calculatedData.ktLampRating = lampPB.calculatedData.ktLampRating;
|
||||
|
||||
// bad+poor PB document. This is a weird, third indepdenent metric that IIDX players sometimes care about.
|
||||
const bpPB = (await db.scores.findOne(
|
||||
{
|
||||
userID: scorePB.userID,
|
||||
chartID: scorePB.chartID,
|
||||
"scoreData.hitMeta.bp": { $exists: true },
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
"scoreData.hitMeta.bp": 1, // bp 0 is the best BP, bp 1 is worse, so on
|
||||
},
|
||||
}
|
||||
)) as ScoreDocument<"iidx:SP" | "iidx:DP">;
|
||||
// bad+poor PB document. This is a weird, third indepdenent metric that IIDX players sometimes care about.
|
||||
const bpPB = (await db.scores.findOne(
|
||||
{
|
||||
userID: scorePB.userID,
|
||||
chartID: scorePB.chartID,
|
||||
"scoreData.hitMeta.bp": { $exists: true },
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
"scoreData.hitMeta.bp": 1, // bp 0 is the best BP, bp 1 is worse, so on
|
||||
},
|
||||
}
|
||||
)) as ScoreDocument<"iidx:SP" | "iidx:DP">;
|
||||
|
||||
if (!bpPB) {
|
||||
logger.verbose(
|
||||
`Could not find BP PB for ${scorePB.userID} ${scorePB.chartID} in PB joining. User likely has no scores with BP defined.`,
|
||||
{ pbDoc }
|
||||
);
|
||||
// this isn't actually an error! we just don't have to do anything.
|
||||
return true;
|
||||
}
|
||||
if (!bpPB) {
|
||||
logger.verbose(
|
||||
`Could not find BP PB for ${scorePB.userID} ${scorePB.chartID} in PB joining. User likely has no scores with BP defined.`,
|
||||
{ pbDoc }
|
||||
);
|
||||
// this isn't actually an error! we just don't have to do anything.
|
||||
return true;
|
||||
}
|
||||
|
||||
// by default scorePB is chosen for hitMeta fields, so, we can skip any assignments here by returning here.
|
||||
if (bpPB.scoreID === scorePB.scoreID) {
|
||||
logger.debug(`Skipped merging BP PB as scorePB was also BP PB.`);
|
||||
return true;
|
||||
} else if (bpPB.scoreID === lampPB.scoreID) {
|
||||
pbDoc.scoreData.hitMeta.bp = lampPB.scoreData.hitMeta.bp;
|
||||
logger.debug(`Skipped adding BP PB as composedFrom because lampPB was also BP PB.`);
|
||||
return true;
|
||||
}
|
||||
// by default scorePB is chosen for hitMeta fields, so, we can skip any assignments here by returning here.
|
||||
if (bpPB.scoreID === scorePB.scoreID) {
|
||||
logger.debug(`Skipped merging BP PB as scorePB was also BP PB.`);
|
||||
return true;
|
||||
} else if (bpPB.scoreID === lampPB.scoreID) {
|
||||
pbDoc.scoreData.hitMeta.bp = lampPB.scoreData.hitMeta.bp;
|
||||
logger.debug(`Skipped adding BP PB as composedFrom because lampPB was also BP PB.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
pbDoc.scoreData.hitMeta.bp = bpPB.scoreData.hitMeta.bp!;
|
||||
pbDoc.scoreData.hitMeta.bp = bpPB.scoreData.hitMeta.bp!;
|
||||
|
||||
pbDoc.composedFrom.other = [{ name: "Best BP", scoreID: bpPB.scoreID }];
|
||||
pbDoc.composedFrom.other = [{ name: "Best BP", scoreID: bpPB.scoreID }];
|
||||
|
||||
// Update lamp related iidx-specific info from the lampPB.
|
||||
pbDoc.scoreData.hitMeta.gsm = lampPB.scoreData.hitMeta.gsm ?? null;
|
||||
pbDoc.scoreData.hitMeta.gauge = lampPB.scoreData.hitMeta.gauge ?? null;
|
||||
pbDoc.scoreData.hitMeta.gaugeHistory = lampPB.scoreData.hitMeta.gaugeHistory ?? null;
|
||||
// Update lamp related iidx-specific info from the lampPB.
|
||||
pbDoc.scoreData.hitMeta.gsm = lampPB.scoreData.hitMeta.gsm ?? null;
|
||||
pbDoc.scoreData.hitMeta.gauge = lampPB.scoreData.hitMeta.gauge ?? null;
|
||||
pbDoc.scoreData.hitMeta.gaugeHistory = lampPB.scoreData.hitMeta.gaugeHistory ?? null;
|
||||
|
||||
return true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,28 +66,28 @@ export async function IIDXMergeFn(
|
||||
* re-calculated for any different permutation of scorePB + lampPB.
|
||||
*/
|
||||
export async function SDVXMergeFn(
|
||||
pbDoc: PBScoreDocument<"sdvx:Single">,
|
||||
scorePB: ScoreDocument<"sdvx:Single">,
|
||||
lampPB: ScoreDocument<"sdvx:Single">,
|
||||
logger: KtLogger
|
||||
pbDoc: PBScoreDocument<"sdvx:Single">,
|
||||
scorePB: ScoreDocument<"sdvx:Single">,
|
||||
lampPB: ScoreDocument<"sdvx:Single">,
|
||||
logger: KtLogger
|
||||
): Promise<boolean> {
|
||||
// @optimisable
|
||||
// This is a re-fetch, but it's difficult to pass the chart all
|
||||
// the way down here due to how chartIDs (set) works. :(
|
||||
const chart = await FindChartWithChartID("sdvx", pbDoc.chartID);
|
||||
// @optimisable
|
||||
// This is a re-fetch, but it's difficult to pass the chart all
|
||||
// the way down here due to how chartIDs (set) works. :(
|
||||
const chart = await FindChartWithChartID("sdvx", pbDoc.chartID);
|
||||
|
||||
if (!chart) {
|
||||
logger.severe(`Chart ${pbDoc.chartID} disappeared underfoot?`);
|
||||
throw new InternalFailure(`Chart ${pbDoc.chartID} disappeared underfoot?`);
|
||||
}
|
||||
if (!chart) {
|
||||
logger.severe(`Chart ${pbDoc.chartID} disappeared underfoot?`);
|
||||
throw new InternalFailure(`Chart ${pbDoc.chartID} disappeared underfoot?`);
|
||||
}
|
||||
|
||||
pbDoc.calculatedData.VF6 = CalculateVF6(
|
||||
pbDoc.scoreData.grade,
|
||||
pbDoc.scoreData.lamp,
|
||||
pbDoc.scoreData.percent,
|
||||
chart.levelNum,
|
||||
logger
|
||||
);
|
||||
pbDoc.calculatedData.VF6 = CalculateVF6(
|
||||
pbDoc.scoreData.grade,
|
||||
pbDoc.scoreData.lamp,
|
||||
pbDoc.scoreData.percent,
|
||||
chart.levelNum,
|
||||
logger
|
||||
);
|
||||
|
||||
return true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -11,52 +11,52 @@ import { CloseAllConnections } from "../../../../test-utils/close-connections";
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
t.test("#ProcessPBs", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should successfully insert a pb into the score-pb database", async (t) => {
|
||||
await db["personal-bests"].remove({});
|
||||
t.test("Should successfully insert a pb into the score-pb database", async (t) => {
|
||||
await db["personal-bests"].remove({});
|
||||
|
||||
// scores on 511 SPA are pre-loaded into the database
|
||||
await ProcessPBs(1, new Set([Testing511SPA.chartID]), logger);
|
||||
// scores on 511 SPA are pre-loaded into the database
|
||||
await ProcessPBs(1, new Set([Testing511SPA.chartID]), logger);
|
||||
|
||||
const pbs = await db["personal-bests"].find({});
|
||||
const pbs = await db["personal-bests"].find({});
|
||||
|
||||
t.equal(pbs.length, 1, "Should match the amount of PBs inserted into the DB.");
|
||||
t.equal(pbs.length, 1, "Should match the amount of PBs inserted into the DB.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should successfully insert multiple pbs into the score-pb database", async (t) => {
|
||||
await db["personal-bests"].remove({});
|
||||
t.test("Should successfully insert multiple pbs into the score-pb database", async (t) => {
|
||||
await db["personal-bests"].remove({});
|
||||
|
||||
await db.scores.insert([
|
||||
// @ts-expect-error lol
|
||||
deepmerge(TestingIIDXSPScore, {
|
||||
chartID: "test1",
|
||||
scoreID: crypto.randomBytes(20).toString("hex"),
|
||||
}),
|
||||
// @ts-expect-error lol
|
||||
deepmerge(TestingIIDXSPScore, {
|
||||
chartID: "test2",
|
||||
scoreID: crypto.randomBytes(20).toString("hex"),
|
||||
}),
|
||||
// @ts-expect-error lol
|
||||
deepmerge(TestingIIDXSPScore, {
|
||||
chartID: "test3",
|
||||
scoreID: crypto.randomBytes(20).toString("hex"),
|
||||
}),
|
||||
]);
|
||||
await db.scores.insert([
|
||||
// @ts-expect-error lol
|
||||
deepmerge(TestingIIDXSPScore, {
|
||||
chartID: "test1",
|
||||
scoreID: crypto.randomBytes(20).toString("hex"),
|
||||
}),
|
||||
// @ts-expect-error lol
|
||||
deepmerge(TestingIIDXSPScore, {
|
||||
chartID: "test2",
|
||||
scoreID: crypto.randomBytes(20).toString("hex"),
|
||||
}),
|
||||
// @ts-expect-error lol
|
||||
deepmerge(TestingIIDXSPScore, {
|
||||
chartID: "test3",
|
||||
scoreID: crypto.randomBytes(20).toString("hex"),
|
||||
}),
|
||||
]);
|
||||
|
||||
await ProcessPBs(1, new Set([Testing511SPA.chartID, "test1", "test2", "test3"]), logger);
|
||||
await ProcessPBs(1, new Set([Testing511SPA.chartID, "test1", "test2", "test3"]), logger);
|
||||
|
||||
const pbs = await db["personal-bests"].find({});
|
||||
const pbs = await db["personal-bests"].find({});
|
||||
|
||||
t.equal(pbs.length, 4, "Should match the amount of PBs inserted into the DB.");
|
||||
t.equal(pbs.length, 4, "Should match the amount of PBs inserted into the DB.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -4,57 +4,57 @@ import { KtLogger } from "../../../logger/logger";
|
||||
import { CreatePBDoc, UpdateChartRanking, PBScoreDocumentNoRank } from "./create-pb-doc";
|
||||
|
||||
export async function ProcessPBs(
|
||||
userID: integer,
|
||||
chartIDs: Set<string>,
|
||||
logger: KtLogger
|
||||
userID: integer,
|
||||
chartIDs: Set<string>,
|
||||
logger: KtLogger
|
||||
): Promise<void> {
|
||||
if (chartIDs.size === 0) {
|
||||
return;
|
||||
}
|
||||
if (chartIDs.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const promises = [];
|
||||
const promises = [];
|
||||
|
||||
for (const chartID of chartIDs) {
|
||||
promises.push(CreatePBDoc(userID, chartID, logger));
|
||||
}
|
||||
for (const chartID of chartIDs) {
|
||||
promises.push(CreatePBDoc(userID, chartID, logger));
|
||||
}
|
||||
|
||||
const pbDocsReturn = await Promise.all(promises);
|
||||
const pbDocsReturn = await Promise.all(promises);
|
||||
|
||||
const pbDocs: PBScoreDocumentNoRank[] = [];
|
||||
const pbDocs: PBScoreDocumentNoRank[] = [];
|
||||
|
||||
for (const doc of pbDocsReturn) {
|
||||
if (!doc) {
|
||||
continue;
|
||||
}
|
||||
for (const doc of pbDocsReturn) {
|
||||
if (!doc) {
|
||||
continue;
|
||||
}
|
||||
|
||||
pbDocs.push(doc);
|
||||
}
|
||||
pbDocs.push(doc);
|
||||
}
|
||||
|
||||
if (pbDocsReturn.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (pbDocsReturn.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// so here's the kinda awkward part - for the time between this operation
|
||||
// and the next one - THE SCORE PBS ARE IN THE DATABASE WITHOUT RANKINGINFO.
|
||||
// this *is* bad behaviour, but I don't have a nice way to fix it.
|
||||
// This should be fixed in the future to avoid crashes between these two
|
||||
// calls - but that is unlikely.
|
||||
await db["personal-bests"].bulkWrite(
|
||||
pbDocs.map((e) => ({
|
||||
updateOne: {
|
||||
filter: { chartID: e.chartID, userID: e.userID },
|
||||
update: { $set: e },
|
||||
upsert: true,
|
||||
},
|
||||
})),
|
||||
{
|
||||
ordered: false,
|
||||
}
|
||||
);
|
||||
// so here's the kinda awkward part - for the time between this operation
|
||||
// and the next one - THE SCORE PBS ARE IN THE DATABASE WITHOUT RANKINGINFO.
|
||||
// this *is* bad behaviour, but I don't have a nice way to fix it.
|
||||
// This should be fixed in the future to avoid crashes between these two
|
||||
// calls - but that is unlikely.
|
||||
await db["personal-bests"].bulkWrite(
|
||||
pbDocs.map((e) => ({
|
||||
updateOne: {
|
||||
filter: { chartID: e.chartID, userID: e.userID },
|
||||
update: { $set: e },
|
||||
upsert: true,
|
||||
},
|
||||
})),
|
||||
{
|
||||
ordered: false,
|
||||
}
|
||||
);
|
||||
|
||||
// now that everything has been updated or inserted, we can refresh
|
||||
// the chart rankings.
|
||||
await Promise.all(pbDocs.map((e) => UpdateChartRanking(e.chartID)));
|
||||
// now that everything has been updated or inserted, we can refresh
|
||||
// the chart rankings.
|
||||
await Promise.all(pbDocs.map((e) => UpdateChartRanking(e.chartID)));
|
||||
|
||||
// and we're done!
|
||||
// and we're done!
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {
|
||||
ImportProcessingInfo,
|
||||
ImportTypes,
|
||||
ImportDocument,
|
||||
PublicUserDocument,
|
||||
Playtypes,
|
||||
Game,
|
||||
integer,
|
||||
IDStrings,
|
||||
ImportProcessingInfo,
|
||||
ImportTypes,
|
||||
ImportDocument,
|
||||
PublicUserDocument,
|
||||
Playtypes,
|
||||
Game,
|
||||
integer,
|
||||
IDStrings,
|
||||
} from "tachi-common";
|
||||
import { KtLogger } from "../../logger/logger";
|
||||
import { ImportAllIterableData } from "./score-importing/score-importing";
|
||||
@@ -29,225 +29,225 @@ import ScoreImportFatalError from "./score-importing/score-import-error";
|
||||
* Performs a Score Import.
|
||||
*/
|
||||
export default async function ScoreImportMain<D, C>(
|
||||
user: PublicUserDocument,
|
||||
userIntent: boolean,
|
||||
importType: ImportTypes,
|
||||
InputParser: ImportInputParser<D, C>,
|
||||
providedImportObjects?: { logger: KtLogger; importID: string }
|
||||
user: PublicUserDocument,
|
||||
userIntent: boolean,
|
||||
importType: ImportTypes,
|
||||
InputParser: ImportInputParser<D, C>,
|
||||
providedImportObjects?: { logger: KtLogger; importID: string }
|
||||
) {
|
||||
// in the event of any error, we remove the user lock.
|
||||
try {
|
||||
const lock = await GetOrSetUserLock(user.id);
|
||||
// in the event of any error, we remove the user lock.
|
||||
try {
|
||||
const lock = await GetOrSetUserLock(user.id);
|
||||
|
||||
if (lock) {
|
||||
// @danger
|
||||
// Throwing away an import if the user already has one outgoing is *bad*, as in the case
|
||||
// of degraded performance we might just start throwing scores away. This is obviously
|
||||
// not great, but any other solution involves making a queue, which can't be done because
|
||||
// InputParser is a very dynamic function that cannot be stored in redis or something.
|
||||
//
|
||||
// Under normal circumstances, there is no scenario where a user would have two ongoing
|
||||
// imports at the same time - even if they were using single-score imports on a 5 second
|
||||
// chart, as each score import takes only around ~10-15miliseconds.
|
||||
throw new ScoreImportFatalError(409, "This user already has an ongoing import.");
|
||||
}
|
||||
if (lock) {
|
||||
// @danger
|
||||
// Throwing away an import if the user already has one outgoing is *bad*, as in the case
|
||||
// of degraded performance we might just start throwing scores away. This is obviously
|
||||
// not great, but any other solution involves making a queue, which can't be done because
|
||||
// InputParser is a very dynamic function that cannot be stored in redis or something.
|
||||
//
|
||||
// Under normal circumstances, there is no scenario where a user would have two ongoing
|
||||
// imports at the same time - even if they were using single-score imports on a 5 second
|
||||
// chart, as each score import takes only around ~10-15miliseconds.
|
||||
throw new ScoreImportFatalError(409, "This user already has an ongoing import.");
|
||||
}
|
||||
|
||||
const timeStarted = Date.now();
|
||||
let importID;
|
||||
let logger;
|
||||
const timeStarted = Date.now();
|
||||
let importID;
|
||||
let logger;
|
||||
|
||||
if (!providedImportObjects) {
|
||||
// If they weren't given to us -
|
||||
// we create an "import logger".
|
||||
// this holds a reference to the user's name, ID, and type
|
||||
// of score import for any future debugging.
|
||||
({ importID, logger } = CreateImportLoggerAndID(user, importType));
|
||||
logger.debug("Received import request.");
|
||||
} else {
|
||||
({ importID, logger } = providedImportObjects);
|
||||
}
|
||||
if (!providedImportObjects) {
|
||||
// If they weren't given to us -
|
||||
// we create an "import logger".
|
||||
// this holds a reference to the user's name, ID, and type
|
||||
// of score import for any future debugging.
|
||||
({ importID, logger } = CreateImportLoggerAndID(user, importType));
|
||||
logger.debug("Received import request.");
|
||||
} else {
|
||||
({ importID, logger } = providedImportObjects);
|
||||
}
|
||||
|
||||
// --- 1. Parsing ---
|
||||
// We get an iterable from the provided parser function, alongside some context and a converter function.
|
||||
// This iterable does not have to be an array - it's anything that's iterable, like a generator.
|
||||
const parseTimeStart = process.hrtime.bigint();
|
||||
const { iterable, context, game, classHandler } = await InputParser(logger);
|
||||
// --- 1. Parsing ---
|
||||
// We get an iterable from the provided parser function, alongside some context and a converter function.
|
||||
// This iterable does not have to be an array - it's anything that's iterable, like a generator.
|
||||
const parseTimeStart = process.hrtime.bigint();
|
||||
const { iterable, context, game, classHandler } = await InputParser(logger);
|
||||
|
||||
const parseTime = GetMilisecondsSince(parseTimeStart);
|
||||
const parseTime = GetMilisecondsSince(parseTimeStart);
|
||||
|
||||
logger.debug(`Parsing took ${parseTime} miliseconds.`);
|
||||
logger.debug(`Parsing took ${parseTime} miliseconds.`);
|
||||
|
||||
// We have to cast here due to typescript generic confusions. This is guaranteed` to be correct.
|
||||
const ConverterFunction = Converters[importType] as unknown as ConverterFunction<D, C>;
|
||||
// We have to cast here due to typescript generic confusions. This is guaranteed` to be correct.
|
||||
const ConverterFunction = Converters[importType] as unknown as ConverterFunction<D, C>;
|
||||
|
||||
// --- 2. Importing ---
|
||||
// ImportAllIterableData iterates over the iterable, applying the converter function to each bit of data.
|
||||
const importTimeStart = process.hrtime.bigint();
|
||||
const importInfo = await ImportAllIterableData(
|
||||
user.id,
|
||||
importType,
|
||||
iterable,
|
||||
ConverterFunction,
|
||||
context,
|
||||
logger
|
||||
);
|
||||
// --- 2. Importing ---
|
||||
// ImportAllIterableData iterates over the iterable, applying the converter function to each bit of data.
|
||||
const importTimeStart = process.hrtime.bigint();
|
||||
const importInfo = await ImportAllIterableData(
|
||||
user.id,
|
||||
importType,
|
||||
iterable,
|
||||
ConverterFunction,
|
||||
context,
|
||||
logger
|
||||
);
|
||||
|
||||
const importTime = GetMilisecondsSince(importTimeStart);
|
||||
const importTimeRel = importTime / importInfo.length;
|
||||
const importTime = GetMilisecondsSince(importTimeStart);
|
||||
const importTimeRel = importTime / importInfo.length;
|
||||
|
||||
logger.debug(`Importing took ${importTime} miliseconds. (${importTimeRel}ms/doc)`);
|
||||
logger.debug(`Importing took ${importTime} miliseconds. (${importTimeRel}ms/doc)`);
|
||||
|
||||
// --- 3. ParseImportInfo ---
|
||||
// ImportInfo is a relatively complex structure. We need some information from it for subsequent steps
|
||||
// such as the list of chartIDs involved in this import.
|
||||
const importParseTimeStart = process.hrtime.bigint();
|
||||
const { scorePlaytypeMap, errors, scoreIDs, chartIDs } = ParseImportInfo(importInfo);
|
||||
// --- 3. ParseImportInfo ---
|
||||
// ImportInfo is a relatively complex structure. We need some information from it for subsequent steps
|
||||
// such as the list of chartIDs involved in this import.
|
||||
const importParseTimeStart = process.hrtime.bigint();
|
||||
const { scorePlaytypeMap, errors, scoreIDs, chartIDs } = ParseImportInfo(importInfo);
|
||||
|
||||
const importParseTime = GetMilisecondsSince(importParseTimeStart);
|
||||
const importParseTimeRel = importParseTime / importInfo.length;
|
||||
const importParseTime = GetMilisecondsSince(importParseTimeStart);
|
||||
const importParseTimeRel = importParseTime / importInfo.length;
|
||||
|
||||
logger.debug(
|
||||
`Import Parsing took ${importParseTime} miliseconds. (${importParseTimeRel}ms/doc)`
|
||||
);
|
||||
logger.debug(
|
||||
`Import Parsing took ${importParseTime} miliseconds. (${importParseTimeRel}ms/doc)`
|
||||
);
|
||||
|
||||
// --- 4. Sessions ---
|
||||
// We create (or update existing) sessions here. This uses the aforementioned parsed import info
|
||||
// to determine what goes where.
|
||||
const sessionTimeStart = process.hrtime.bigint();
|
||||
const sessionInfo = await CreateSessions(
|
||||
user.id,
|
||||
importType,
|
||||
game,
|
||||
scorePlaytypeMap,
|
||||
logger
|
||||
);
|
||||
// --- 4. Sessions ---
|
||||
// We create (or update existing) sessions here. This uses the aforementioned parsed import info
|
||||
// to determine what goes where.
|
||||
const sessionTimeStart = process.hrtime.bigint();
|
||||
const sessionInfo = await CreateSessions(
|
||||
user.id,
|
||||
importType,
|
||||
game,
|
||||
scorePlaytypeMap,
|
||||
logger
|
||||
);
|
||||
|
||||
const sessionTime = GetMilisecondsSince(sessionTimeStart);
|
||||
const sessionTimeRel = sessionTime / sessionInfo.length;
|
||||
const sessionTime = GetMilisecondsSince(sessionTimeStart);
|
||||
const sessionTimeRel = sessionTime / sessionInfo.length;
|
||||
|
||||
logger.debug(
|
||||
`Session Processing took ${sessionTime} miliseconds (${sessionTimeRel}ms/doc).`
|
||||
);
|
||||
logger.debug(
|
||||
`Session Processing took ${sessionTime} miliseconds (${sessionTimeRel}ms/doc).`
|
||||
);
|
||||
|
||||
// --- 5. PersonalBests ---
|
||||
// We want to keep an updated reference of a users best score on a given chart.
|
||||
// This function also handles conjoining different scores together (such as unioning best lamp and
|
||||
// best score).
|
||||
const pbTimeStart = process.hrtime.bigint();
|
||||
await ProcessPBs(user.id, chartIDs, logger);
|
||||
// --- 5. PersonalBests ---
|
||||
// We want to keep an updated reference of a users best score on a given chart.
|
||||
// This function also handles conjoining different scores together (such as unioning best lamp and
|
||||
// best score).
|
||||
const pbTimeStart = process.hrtime.bigint();
|
||||
await ProcessPBs(user.id, chartIDs, logger);
|
||||
|
||||
const pbTime = GetMilisecondsSince(pbTimeStart);
|
||||
const pbTimeRel = pbTime / chartIDs.size;
|
||||
const pbTime = GetMilisecondsSince(pbTimeStart);
|
||||
const pbTimeRel = pbTime / chartIDs.size;
|
||||
|
||||
logger.debug(`PB Processing took ${pbTime} miliseconds (${pbTimeRel}ms/doc)`);
|
||||
logger.debug(`PB Processing took ${pbTime} miliseconds (${pbTimeRel}ms/doc)`);
|
||||
|
||||
const playtypes = Object.keys(scorePlaytypeMap) as Playtypes[Game][];
|
||||
const playtypes = Object.keys(scorePlaytypeMap) as Playtypes[Game][];
|
||||
|
||||
// --- 6. Game Stats ---
|
||||
// This function updates the users "stats" for this game - such as their profile rating or their classes.
|
||||
const ugsTimeStart = process.hrtime.bigint();
|
||||
const classDeltas = await UpdateUsersGameStats(
|
||||
game,
|
||||
playtypes,
|
||||
user.id,
|
||||
classHandler,
|
||||
logger
|
||||
);
|
||||
// --- 6. Game Stats ---
|
||||
// This function updates the users "stats" for this game - such as their profile rating or their classes.
|
||||
const ugsTimeStart = process.hrtime.bigint();
|
||||
const classDeltas = await UpdateUsersGameStats(
|
||||
game,
|
||||
playtypes,
|
||||
user.id,
|
||||
classHandler,
|
||||
logger
|
||||
);
|
||||
|
||||
const ugsTime = GetMilisecondsSince(ugsTimeStart);
|
||||
const ugsTime = GetMilisecondsSince(ugsTimeStart);
|
||||
|
||||
logger.debug(`UGS Processing took ${ugsTime} miliseconds.`);
|
||||
logger.debug(`UGS Processing took ${ugsTime} miliseconds.`);
|
||||
|
||||
// --- 7. Goals ---
|
||||
// Evaluate and update the users goals. This returns information about goals that have changed.
|
||||
const goalTimeStart = process.hrtime.bigint();
|
||||
const goalInfo = await GetAndUpdateUsersGoals(game, user.id, chartIDs, logger);
|
||||
// --- 7. Goals ---
|
||||
// Evaluate and update the users goals. This returns information about goals that have changed.
|
||||
const goalTimeStart = process.hrtime.bigint();
|
||||
const goalInfo = await GetAndUpdateUsersGoals(game, user.id, chartIDs, logger);
|
||||
|
||||
const goalTime = GetMilisecondsSince(goalTimeStart);
|
||||
const goalTime = GetMilisecondsSince(goalTimeStart);
|
||||
|
||||
logger.debug(`Goal Processing took ${goalTime} miliseconds.`);
|
||||
logger.debug(`Goal Processing took ${goalTime} miliseconds.`);
|
||||
|
||||
// --- 8. Milestones ---
|
||||
// Evaluate and update the users milestones. This returns...
|
||||
const milestoneTimeStart = process.hrtime.bigint();
|
||||
const milestoneInfo = await UpdateUsersMilestones(
|
||||
goalInfo,
|
||||
game,
|
||||
playtypes,
|
||||
user.id,
|
||||
logger
|
||||
);
|
||||
// --- 8. Milestones ---
|
||||
// Evaluate and update the users milestones. This returns...
|
||||
const milestoneTimeStart = process.hrtime.bigint();
|
||||
const milestoneInfo = await UpdateUsersMilestones(
|
||||
goalInfo,
|
||||
game,
|
||||
playtypes,
|
||||
user.id,
|
||||
logger
|
||||
);
|
||||
|
||||
const milestoneTime = GetMilisecondsSince(milestoneTimeStart);
|
||||
const milestoneTime = GetMilisecondsSince(milestoneTimeStart);
|
||||
|
||||
logger.debug(`Milestone Processing took ${milestoneTime} miliseconds.`);
|
||||
logger.debug(`Milestone Processing took ${milestoneTime} miliseconds.`);
|
||||
|
||||
// --- 9. Finalise Import Document ---
|
||||
// Create and Save an import document to the database, and finish everything up!
|
||||
const ImportDocument: ImportDocument = {
|
||||
importType,
|
||||
idStrings: playtypes.map((e) => `${game}:${e}`) as IDStrings[],
|
||||
scoreIDs,
|
||||
errors,
|
||||
importID,
|
||||
timeFinished: Date.now(),
|
||||
timeStarted,
|
||||
createdSessions: sessionInfo,
|
||||
userID: user.id,
|
||||
classDeltas,
|
||||
goalInfo,
|
||||
milestoneInfo,
|
||||
userIntent,
|
||||
};
|
||||
// --- 9. Finalise Import Document ---
|
||||
// Create and Save an import document to the database, and finish everything up!
|
||||
const ImportDocument: ImportDocument = {
|
||||
importType,
|
||||
idStrings: playtypes.map((e) => `${game}:${e}`) as IDStrings[],
|
||||
scoreIDs,
|
||||
errors,
|
||||
importID,
|
||||
timeFinished: Date.now(),
|
||||
timeStarted,
|
||||
createdSessions: sessionInfo,
|
||||
userID: user.id,
|
||||
classDeltas,
|
||||
goalInfo,
|
||||
milestoneInfo,
|
||||
userIntent,
|
||||
};
|
||||
|
||||
const logMessage = `Import took: ${ImportDocument.timeFinished - timeStarted}ms, with ${
|
||||
importInfo.length
|
||||
} documents (Fails: ${errors.length}, Successes: ${scoreIDs.length}, Sessions: ${
|
||||
sessionInfo.length
|
||||
}). Aprx ${(ImportDocument.timeFinished - timeStarted) / importInfo.length}ms/doc`;
|
||||
const logMessage = `Import took: ${ImportDocument.timeFinished - timeStarted}ms, with ${
|
||||
importInfo.length
|
||||
} documents (Fails: ${errors.length}, Successes: ${scoreIDs.length}, Sessions: ${
|
||||
sessionInfo.length
|
||||
}). Aprx ${(ImportDocument.timeFinished - timeStarted) / importInfo.length}ms/doc`;
|
||||
|
||||
// I only really want to log "big" imports. The others are here for debugging purposes.
|
||||
if (scoreIDs.length > 500) {
|
||||
logger.info(logMessage);
|
||||
} else if (scoreIDs.length > 1) {
|
||||
logger.verbose(logMessage);
|
||||
} else {
|
||||
logger.debug(logMessage);
|
||||
}
|
||||
// I only really want to log "big" imports. The others are here for debugging purposes.
|
||||
if (scoreIDs.length > 500) {
|
||||
logger.info(logMessage);
|
||||
} else if (scoreIDs.length > 1) {
|
||||
logger.verbose(logMessage);
|
||||
} else {
|
||||
logger.debug(logMessage);
|
||||
}
|
||||
|
||||
await db.imports.insert(ImportDocument);
|
||||
await db.imports.insert(ImportDocument);
|
||||
|
||||
// we don't await this because we don't
|
||||
// particularly care about waiting for it.
|
||||
db["import-timings"].insert({
|
||||
importID,
|
||||
timestamp: Date.now(),
|
||||
total: ImportDocument.timeFinished - timeStarted,
|
||||
rel: {
|
||||
import: importTimeRel,
|
||||
importParse: importParseTimeRel,
|
||||
pb: pbTimeRel,
|
||||
session: sessionTimeRel,
|
||||
},
|
||||
abs: {
|
||||
parse: parseTime,
|
||||
import: importTime,
|
||||
importParse: importParseTime,
|
||||
session: sessionTime,
|
||||
pb: pbTime,
|
||||
ugs: ugsTime,
|
||||
goal: goalTime,
|
||||
milestone: milestoneTime,
|
||||
},
|
||||
});
|
||||
// we don't await this because we don't
|
||||
// particularly care about waiting for it.
|
||||
db["import-timings"].insert({
|
||||
importID,
|
||||
timestamp: Date.now(),
|
||||
total: ImportDocument.timeFinished - timeStarted,
|
||||
rel: {
|
||||
import: importTimeRel,
|
||||
importParse: importParseTimeRel,
|
||||
pb: pbTimeRel,
|
||||
session: sessionTimeRel,
|
||||
},
|
||||
abs: {
|
||||
parse: parseTime,
|
||||
import: importTime,
|
||||
importParse: importParseTime,
|
||||
session: sessionTime,
|
||||
pb: pbTime,
|
||||
ugs: ugsTime,
|
||||
goal: goalTime,
|
||||
milestone: milestoneTime,
|
||||
},
|
||||
});
|
||||
|
||||
await RemoveUserLock(user.id);
|
||||
await RemoveUserLock(user.id);
|
||||
|
||||
return ImportDocument;
|
||||
} catch (err) {
|
||||
await RemoveUserLock(user.id);
|
||||
throw err;
|
||||
}
|
||||
return ImportDocument;
|
||||
} catch (err) {
|
||||
await RemoveUserLock(user.id);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -255,20 +255,20 @@ export default async function ScoreImportMain<D, C>(
|
||||
* @returns A flattened array of ClassDeltas
|
||||
*/
|
||||
async function UpdateUsersGameStats(
|
||||
game: Game,
|
||||
playtypes: Playtypes[Game][],
|
||||
userID: integer,
|
||||
classHandler: ClassHandler | null,
|
||||
logger: KtLogger
|
||||
game: Game,
|
||||
playtypes: Playtypes[Game][],
|
||||
userID: integer,
|
||||
classHandler: ClassHandler | null,
|
||||
logger: KtLogger
|
||||
) {
|
||||
const promises = [];
|
||||
const promises = [];
|
||||
|
||||
for (const pt of playtypes) {
|
||||
promises.push(UpdateUsersGamePlaytypeStats(game, pt, userID, classHandler, logger));
|
||||
}
|
||||
for (const pt of playtypes) {
|
||||
promises.push(UpdateUsersGamePlaytypeStats(game, pt, userID, classHandler, logger));
|
||||
}
|
||||
|
||||
const r = await Promise.all(promises);
|
||||
return r.flat(1);
|
||||
const r = await Promise.all(promises);
|
||||
return r.flat(1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -279,26 +279,26 @@ async function UpdateUsersGameStats(
|
||||
* on their playtype.
|
||||
*/
|
||||
function ParseImportInfo(importInfo: ImportProcessingInfo[]) {
|
||||
const scorePlaytypeMap: ScorePlaytypeMap = Object.create(null);
|
||||
const scorePlaytypeMap: ScorePlaytypeMap = Object.create(null);
|
||||
|
||||
const scoreIDs = [];
|
||||
const errors = [];
|
||||
const chartIDs: Set<string> = new Set();
|
||||
const scoreIDs = [];
|
||||
const errors = [];
|
||||
const chartIDs: Set<string> = new Set();
|
||||
|
||||
for (const info of importInfo) {
|
||||
if (info.success) {
|
||||
scoreIDs.push(info.content.score.scoreID);
|
||||
chartIDs.add(info.content.score.chartID);
|
||||
for (const info of importInfo) {
|
||||
if (info.success) {
|
||||
scoreIDs.push(info.content.score.scoreID);
|
||||
chartIDs.add(info.content.score.chartID);
|
||||
|
||||
if (scorePlaytypeMap[info.content.score.playtype]) {
|
||||
scorePlaytypeMap[info.content.score.playtype]!.push(info.content.score);
|
||||
} else {
|
||||
scorePlaytypeMap[info.content.score.playtype] = [info.content.score];
|
||||
}
|
||||
} else {
|
||||
errors.push({ type: info.type, message: info.message });
|
||||
}
|
||||
}
|
||||
if (scorePlaytypeMap[info.content.score.playtype]) {
|
||||
scorePlaytypeMap[info.content.score.playtype]!.push(info.content.score);
|
||||
} else {
|
||||
scorePlaytypeMap[info.content.score.playtype] = [info.content.score];
|
||||
}
|
||||
} else {
|
||||
errors.push({ type: info.type, message: info.message });
|
||||
}
|
||||
}
|
||||
|
||||
return { scoreIDs, errors, scorePlaytypeMap, chartIDs };
|
||||
return { scoreIDs, errors, scorePlaytypeMap, chartIDs };
|
||||
}
|
||||
|
||||
@@ -2,5 +2,5 @@ import t from "tap";
|
||||
import { HydrateScore } from "./hydrate-score";
|
||||
|
||||
t.todo("#HydrateScore", (t) => {
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
@@ -10,50 +10,50 @@ import { DryScore } from "../common/types";
|
||||
* @param userID The userID this score is for.
|
||||
*/
|
||||
export async function HydrateScore(
|
||||
userID: integer,
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument,
|
||||
song: AnySongDocument,
|
||||
scoreID: string,
|
||||
logger: KtLogger
|
||||
userID: integer,
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument,
|
||||
song: AnySongDocument,
|
||||
scoreID: string,
|
||||
logger: KtLogger
|
||||
): Promise<ScoreDocument> {
|
||||
const esd = CalculateESDForGame(
|
||||
dryScore.game,
|
||||
chart.playtype,
|
||||
dryScore.scoreData.percent / 100
|
||||
);
|
||||
const esd = CalculateESDForGame(
|
||||
dryScore.game,
|
||||
chart.playtype,
|
||||
dryScore.scoreData.percent / 100
|
||||
);
|
||||
|
||||
const calculatedData = await CreateCalculatedData(dryScore, chart, esd, logger);
|
||||
const calculatedData = await CreateCalculatedData(dryScore, chart, esd, logger);
|
||||
|
||||
const { scoreData: dryScoreData, ...rest } = dryScore;
|
||||
const { scoreData: dryScoreData, ...rest } = dryScore;
|
||||
|
||||
// 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),
|
||||
esd,
|
||||
},
|
||||
dryScoreData
|
||||
);
|
||||
// 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),
|
||||
esd,
|
||||
},
|
||||
dryScoreData
|
||||
);
|
||||
|
||||
const score: ScoreDocument = {
|
||||
// extract all of the non-scoreData props from a dry score and push them here
|
||||
...rest,
|
||||
// then push our score data.
|
||||
scoreData,
|
||||
// everything below this point is sane
|
||||
highlight: false,
|
||||
timeAdded: Date.now(),
|
||||
userID,
|
||||
calculatedData,
|
||||
songID: song.id,
|
||||
chartID: chart.chartID,
|
||||
scoreID,
|
||||
playtype: chart.playtype,
|
||||
isPrimary: chart.isPrimary,
|
||||
};
|
||||
const score: ScoreDocument = {
|
||||
// extract all of the non-scoreData props from a dry score and push them here
|
||||
...rest,
|
||||
// then push our score data.
|
||||
scoreData,
|
||||
// everything below this point is sane
|
||||
highlight: false,
|
||||
timeAdded: Date.now(),
|
||||
userID,
|
||||
calculatedData,
|
||||
songID: song.id,
|
||||
chartID: chart.chartID,
|
||||
scoreID,
|
||||
playtype: chart.playtype,
|
||||
isPrimary: chart.isPrimary,
|
||||
};
|
||||
|
||||
return score;
|
||||
return score;
|
||||
}
|
||||
|
||||
@@ -7,105 +7,105 @@ import { InsertQueue, QueueScoreInsert } from "./insert-score";
|
||||
|
||||
// these two get the same tests, because they're too closely linked
|
||||
t.test("#QueueScoreInsert, #InsertQueue", async (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
// empty scoreDB after
|
||||
t.afterEach(async () => {
|
||||
await db.scores.remove({});
|
||||
});
|
||||
// empty scoreDB after
|
||||
t.afterEach(async () => {
|
||||
await db.scores.remove({});
|
||||
});
|
||||
|
||||
t.test("Single Queue Test", async (t) => {
|
||||
// fake score doc
|
||||
const res = await QueueScoreInsert({ scoreID: "foo" } as unknown as ScoreDocument);
|
||||
t.test("Single Queue Test", async (t) => {
|
||||
// fake score doc
|
||||
const res = await QueueScoreInsert({ scoreID: "foo" } as unknown as ScoreDocument);
|
||||
|
||||
t.equal(
|
||||
res,
|
||||
true,
|
||||
"QueueScoreInsert should not insert a score when the queue is not full."
|
||||
);
|
||||
t.equal(
|
||||
res,
|
||||
true,
|
||||
"QueueScoreInsert should not insert a score when the queue is not full."
|
||||
);
|
||||
|
||||
// this is the best way to get the size of the queue
|
||||
const flushSize = await InsertQueue();
|
||||
// this is the best way to get the size of the queue
|
||||
const flushSize = await InsertQueue();
|
||||
|
||||
t.equal(flushSize, 1, "QueueScoreInsert should append the score to the queue.");
|
||||
t.equal(flushSize, 1, "QueueScoreInsert should append the score to the queue.");
|
||||
|
||||
const dbRes = await db.scores.find({
|
||||
scoreID: "foo",
|
||||
});
|
||||
const dbRes = await db.scores.find({
|
||||
scoreID: "foo",
|
||||
});
|
||||
|
||||
t.equal(
|
||||
dbRes.length,
|
||||
1,
|
||||
"InsertQueue should insert all 1 members of the queue into the database."
|
||||
);
|
||||
t.equal(
|
||||
dbRes.length,
|
||||
1,
|
||||
"InsertQueue should insert all 1 members of the queue into the database."
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
let r = await InsertQueue(); // flush queue just incase former test fails.
|
||||
let r = await InsertQueue(); // flush queue just incase former test fails.
|
||||
|
||||
t.equal(r, 0, "Queue should be empty after test.");
|
||||
t.equal(r, 0, "Queue should be empty after test.");
|
||||
|
||||
t.test("Queue Overflow Test", async (t) => {
|
||||
for (let i = 0; i < 499; i++) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await QueueScoreInsert({ scoreID: i, chartID: "test" } as unknown as ScoreDocument);
|
||||
}
|
||||
t.test("Queue Overflow Test", async (t) => {
|
||||
for (let i = 0; i < 499; i++) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await QueueScoreInsert({ scoreID: i, chartID: "test" } as unknown as ScoreDocument);
|
||||
}
|
||||
|
||||
const overflowRes = await QueueScoreInsert({
|
||||
scoreID: "foo",
|
||||
chartID: "test",
|
||||
} as unknown as ScoreDocument);
|
||||
const overflowRes = await QueueScoreInsert({
|
||||
scoreID: "foo",
|
||||
chartID: "test",
|
||||
} as unknown as ScoreDocument);
|
||||
|
||||
t.equal(
|
||||
overflowRes,
|
||||
500,
|
||||
"Appending 500 items to the queue should result in them being inserted."
|
||||
);
|
||||
t.equal(
|
||||
overflowRes,
|
||||
500,
|
||||
"Appending 500 items to the queue should result in them being inserted."
|
||||
);
|
||||
|
||||
const flushRes = await InsertQueue();
|
||||
const flushRes = await InsertQueue();
|
||||
|
||||
t.equal(flushRes, 0, "The queue should now be empty.");
|
||||
t.equal(flushRes, 0, "The queue should now be empty.");
|
||||
|
||||
const dbRes = await db.scores.find({
|
||||
scoreID: { $exists: true },
|
||||
chartID: "test",
|
||||
});
|
||||
const dbRes = await db.scores.find({
|
||||
scoreID: { $exists: true },
|
||||
chartID: "test",
|
||||
});
|
||||
|
||||
t.equal(
|
||||
dbRes.length,
|
||||
500,
|
||||
"InsertQueue should insert all 500 members of the queue into the database."
|
||||
);
|
||||
t.equal(
|
||||
dbRes.length,
|
||||
500,
|
||||
"InsertQueue should insert all 500 members of the queue into the database."
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
r = await InsertQueue(); // flush queue just incase former test fails.
|
||||
r = await InsertQueue(); // flush queue just incase former test fails.
|
||||
|
||||
t.equal(r, 0, "Queue should be empty after test.");
|
||||
t.equal(r, 0, "Queue should be empty after test.");
|
||||
|
||||
t.test("Queue Dedupe Test", async (t) => {
|
||||
await QueueScoreInsert({ scoreID: 1, chartID: "foo" } as unknown as ScoreDocument);
|
||||
const r2 = await QueueScoreInsert({
|
||||
scoreID: 1,
|
||||
chartID: "foo",
|
||||
} as unknown as ScoreDocument);
|
||||
t.test("Queue Dedupe Test", async (t) => {
|
||||
await QueueScoreInsert({ scoreID: 1, chartID: "foo" } as unknown as ScoreDocument);
|
||||
const r2 = await QueueScoreInsert({
|
||||
scoreID: 1,
|
||||
chartID: "foo",
|
||||
} as unknown as ScoreDocument);
|
||||
|
||||
t.equal(r2, null, "Should return null when a duplicate scoreID is submitted");
|
||||
t.equal(r2, null, "Should return null when a duplicate scoreID is submitted");
|
||||
|
||||
const flushRes = await InsertQueue();
|
||||
const flushRes = await InsertQueue();
|
||||
|
||||
t.equal(flushRes, 1, "Should flush 1 score document");
|
||||
t.equal(flushRes, 1, "Should flush 1 score document");
|
||||
|
||||
const dbRes = await db.scores.find({
|
||||
chartID: "foo",
|
||||
});
|
||||
const dbRes = await db.scores.find({
|
||||
chartID: "foo",
|
||||
});
|
||||
|
||||
t.equal(dbRes.length, 1, "Should only insert one document");
|
||||
});
|
||||
t.equal(dbRes.length, 1, "Should only insert one document");
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -14,21 +14,21 @@ const MAX_PIPELINE_LENGTH = 500;
|
||||
* the score provided is already loaded.
|
||||
*/
|
||||
export function QueueScoreInsert(score: ScoreDocument) {
|
||||
if (ScoreIDs.has(score.scoreID)) {
|
||||
// skip
|
||||
logger.verbose(`Triggered skip for ID ${score.scoreID}`);
|
||||
return null;
|
||||
}
|
||||
if (ScoreIDs.has(score.scoreID)) {
|
||||
// skip
|
||||
logger.verbose(`Triggered skip for ID ${score.scoreID}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
ScoreQueue.push(score);
|
||||
ScoreIDs.add(score.scoreID);
|
||||
ScoreQueue.push(score);
|
||||
ScoreIDs.add(score.scoreID);
|
||||
|
||||
if (ScoreQueue.length >= MAX_PIPELINE_LENGTH) {
|
||||
logger.verbose(`Triggered pipeline flush with len ${ScoreQueue.length}.`);
|
||||
return InsertQueue();
|
||||
}
|
||||
if (ScoreQueue.length >= MAX_PIPELINE_LENGTH) {
|
||||
logger.verbose(`Triggered pipeline flush with len ${ScoreQueue.length}.`);
|
||||
return InsertQueue();
|
||||
}
|
||||
|
||||
return true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,18 +36,18 @@ export function QueueScoreInsert(score: ScoreDocument) {
|
||||
* @warn Be cautious of inducing race conditions when using this function.
|
||||
*/
|
||||
export async function InsertQueue() {
|
||||
const temp = ScoreQueue.splice(0);
|
||||
if (temp.length !== 0) {
|
||||
ScoreIDs = new Set();
|
||||
try {
|
||||
await db.scores.insert(temp);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`Triggered duplicate key protection. Race condition protected against, but this is not good.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const temp = ScoreQueue.splice(0);
|
||||
if (temp.length !== 0) {
|
||||
ScoreIDs = new Set();
|
||||
try {
|
||||
await db.scores.insert(temp);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`Triggered duplicate key protection. Race condition protected against, but this is not good.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return temp.length;
|
||||
return temp.length;
|
||||
}
|
||||
|
||||
@@ -7,90 +7,90 @@ import { CreateScoreID, GetWithScoreID } from "./score-id";
|
||||
|
||||
t.beforeEach(ResetDBState);
|
||||
t.test("#GetWithScoreID", async (t) => {
|
||||
const sc = await GetWithScoreID("TESTING_SCORE_ID");
|
||||
const sc = await GetWithScoreID("TESTING_SCORE_ID");
|
||||
|
||||
t.not(sc, null, "Return a score for a valid score id");
|
||||
t.not(sc, null, "Return a score for a valid score id");
|
||||
|
||||
const nl = await GetWithScoreID("INVALID SCORE ID");
|
||||
const nl = await GetWithScoreID("INVALID SCORE ID");
|
||||
|
||||
t.equal(nl, null, "Should return null for score IDs it cannot resolve");
|
||||
t.equal(nl, null, "Should return null for score IDs it cannot resolve");
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#CreateScoreID", (t) => {
|
||||
const scoreID = CreateScoreID(1, TestingIIDXSPDryScore, Testing511SPA.chartID);
|
||||
const scoreID = CreateScoreID(1, TestingIIDXSPDryScore, Testing511SPA.chartID);
|
||||
|
||||
t.match(
|
||||
scoreID,
|
||||
/^R[0-9a-f]{40}/u,
|
||||
"Should return an R followed by 40 characters of lowercase hex as scoreID."
|
||||
);
|
||||
t.match(
|
||||
scoreID,
|
||||
/^R[0-9a-f]{40}/u,
|
||||
"Should return an R followed by 40 characters of lowercase hex as scoreID."
|
||||
);
|
||||
|
||||
t.not(
|
||||
scoreID,
|
||||
CreateScoreID(2, TestingIIDXSPDryScore, Testing511SPA.chartID),
|
||||
"Hash should be affected by userID."
|
||||
);
|
||||
t.not(
|
||||
scoreID,
|
||||
CreateScoreID(2, TestingIIDXSPDryScore, Testing511SPA.chartID),
|
||||
"Hash should be affected by userID."
|
||||
);
|
||||
|
||||
t.equal(
|
||||
scoreID,
|
||||
CreateScoreID(1, TestingIIDXSPDryScore, Testing511SPA.chartID),
|
||||
"ScoreIDs should be consistently generatable"
|
||||
);
|
||||
t.equal(
|
||||
scoreID,
|
||||
CreateScoreID(1, TestingIIDXSPDryScore, Testing511SPA.chartID),
|
||||
"ScoreIDs should be consistently generatable"
|
||||
);
|
||||
|
||||
// duplicate score
|
||||
const minimalScore = {
|
||||
scoreData: {
|
||||
percent: TestingIIDXSPDryScore.scoreData.percent,
|
||||
score: TestingIIDXSPDryScore.scoreData.score,
|
||||
grade: TestingIIDXSPDryScore.scoreData.grade,
|
||||
lamp: TestingIIDXSPDryScore.scoreData.lamp,
|
||||
},
|
||||
};
|
||||
// duplicate score
|
||||
const minimalScore = {
|
||||
scoreData: {
|
||||
percent: TestingIIDXSPDryScore.scoreData.percent,
|
||||
score: TestingIIDXSPDryScore.scoreData.score,
|
||||
grade: TestingIIDXSPDryScore.scoreData.grade,
|
||||
lamp: TestingIIDXSPDryScore.scoreData.lamp,
|
||||
},
|
||||
};
|
||||
|
||||
t.equal(
|
||||
scoreID,
|
||||
CreateScoreID(1, minimalScore as ScoreDocument, Testing511SPA.chartID),
|
||||
"ScoreIDs should only be affected by score, percent, grade and lamp."
|
||||
);
|
||||
t.equal(
|
||||
scoreID,
|
||||
CreateScoreID(1, minimalScore as ScoreDocument, Testing511SPA.chartID),
|
||||
"ScoreIDs should only be affected by score, percent, grade and lamp."
|
||||
);
|
||||
|
||||
minimalScore.scoreData.percent = 0;
|
||||
minimalScore.scoreData.percent = 0;
|
||||
|
||||
t.not(
|
||||
scoreID,
|
||||
CreateScoreID(1, minimalScore as ScoreDocument, Testing511SPA.chartID),
|
||||
"ScoreIDs should not produce the same value if percent is different."
|
||||
);
|
||||
t.not(
|
||||
scoreID,
|
||||
CreateScoreID(1, minimalScore as ScoreDocument, Testing511SPA.chartID),
|
||||
"ScoreIDs should not produce the same value if percent is different."
|
||||
);
|
||||
|
||||
minimalScore.scoreData.percent = TestingIIDXSPDryScore.scoreData.percent;
|
||||
minimalScore.scoreData.score = 0;
|
||||
minimalScore.scoreData.percent = TestingIIDXSPDryScore.scoreData.percent;
|
||||
minimalScore.scoreData.score = 0;
|
||||
|
||||
t.not(
|
||||
scoreID,
|
||||
CreateScoreID(1, minimalScore as ScoreDocument, Testing511SPA.chartID),
|
||||
"ScoreIDs should not produce the same value if score is different."
|
||||
);
|
||||
t.not(
|
||||
scoreID,
|
||||
CreateScoreID(1, minimalScore as ScoreDocument, Testing511SPA.chartID),
|
||||
"ScoreIDs should not produce the same value if score is different."
|
||||
);
|
||||
|
||||
minimalScore.scoreData.score = TestingIIDXSPDryScore.scoreData.score;
|
||||
minimalScore.scoreData.lamp = "ASSIST CLEAR";
|
||||
minimalScore.scoreData.score = TestingIIDXSPDryScore.scoreData.score;
|
||||
minimalScore.scoreData.lamp = "ASSIST CLEAR";
|
||||
|
||||
t.not(
|
||||
scoreID,
|
||||
CreateScoreID(1, minimalScore as ScoreDocument, Testing511SPA.chartID),
|
||||
"ScoreIDs should not produce the same value if lamp is different."
|
||||
);
|
||||
t.not(
|
||||
scoreID,
|
||||
CreateScoreID(1, minimalScore as ScoreDocument, Testing511SPA.chartID),
|
||||
"ScoreIDs should not produce the same value if lamp is different."
|
||||
);
|
||||
|
||||
minimalScore.scoreData.lamp = TestingIIDXSPDryScore.scoreData.lamp;
|
||||
minimalScore.scoreData.grade = "F";
|
||||
minimalScore.scoreData.lamp = TestingIIDXSPDryScore.scoreData.lamp;
|
||||
minimalScore.scoreData.grade = "F";
|
||||
|
||||
t.not(
|
||||
scoreID,
|
||||
CreateScoreID(1, minimalScore as ScoreDocument, Testing511SPA.chartID),
|
||||
"ScoreIDs should not produce the same value if grade is different."
|
||||
);
|
||||
t.not(
|
||||
scoreID,
|
||||
CreateScoreID(1, minimalScore as ScoreDocument, Testing511SPA.chartID),
|
||||
"ScoreIDs should not produce the same value if grade is different."
|
||||
);
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -4,9 +4,9 @@ import { integer } from "tachi-common";
|
||||
import { DryScore } from "../common/types";
|
||||
|
||||
function CreateScoreIDString(userID: integer, partialScore: DryScore, chartID: string) {
|
||||
const { lamp, grade } = partialScore.scoreData;
|
||||
const { lamp, grade } = partialScore.scoreData;
|
||||
|
||||
return `${userID}|${chartID}|${lamp}|${grade}|${partialScore.scoreData.score}|${partialScore.scoreData.percent}`;
|
||||
return `${userID}|${chartID}|${lamp}|${grade}|${partialScore.scoreData.score}|${partialScore.scoreData.percent}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -15,7 +15,7 @@ function CreateScoreIDString(userID: integer, partialScore: DryScore, chartID: s
|
||||
* @returns A sha256 checksum in lowercase hex.
|
||||
*/
|
||||
function HashScoreIDString(scoreIDString: string) {
|
||||
return crypto.createHash("sha256").update(scoreIDString).digest("hex");
|
||||
return crypto.createHash("sha256").update(scoreIDString).digest("hex");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,13 +24,13 @@ function HashScoreIDString(scoreIDString: string) {
|
||||
* @returns @see HashScoreIDString - prefixed with R.
|
||||
*/
|
||||
export function CreateScoreID(userID: integer, dryScore: DryScore, chartID: string) {
|
||||
const scoreIDString = CreateScoreIDString(userID, dryScore, chartID);
|
||||
const scoreIDString = CreateScoreIDString(userID, dryScore, chartID);
|
||||
|
||||
return `R${HashScoreIDString(scoreIDString)}`;
|
||||
return `R${HashScoreIDString(scoreIDString)}`;
|
||||
}
|
||||
|
||||
export function GetWithScoreID(scoreID: string) {
|
||||
return db.scores.findOne({
|
||||
scoreID,
|
||||
});
|
||||
return db.scores.findOne({
|
||||
scoreID,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ import t from "tap";
|
||||
import ScoreImportFatalError from "./score-import-error";
|
||||
|
||||
t.test("#new ScoreImportFatalError()", (t) => {
|
||||
const err = new ScoreImportFatalError(500, "error message");
|
||||
const err = new ScoreImportFatalError(500, "error message");
|
||||
|
||||
t.equal(err.statusCode, 500, "Should store the status code passed to it.");
|
||||
t.equal(err.message, "error message", "Should store description as the error message.");
|
||||
t.equal(err.statusCode, 500, "Should store the status code passed to it.");
|
||||
t.equal(err.message, "error message", "Should store description as the error message.");
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
* process.
|
||||
*/
|
||||
export default class ScoreImportFatalError extends Error {
|
||||
statusCode: number;
|
||||
statusCode: number;
|
||||
|
||||
constructor(statusCode: number, description: string) {
|
||||
super(description);
|
||||
constructor(statusCode: number, description: string) {
|
||||
super(description);
|
||||
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import t from "tap";
|
||||
import { ImportAllIterableData, ImportIterableDatapoint } from "./score-importing";
|
||||
|
||||
t.todo("#ImportAllIterableData", (t) => {
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.todo("#ImportIterableDatapoint", (t) => {
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import {
|
||||
AnyChartDocument,
|
||||
ImportProcessingInfo,
|
||||
integer,
|
||||
ScoreDocument,
|
||||
AnySongDocument,
|
||||
ImportTypes,
|
||||
IDStrings,
|
||||
AnyChartDocument,
|
||||
ImportProcessingInfo,
|
||||
integer,
|
||||
ScoreDocument,
|
||||
AnySongDocument,
|
||||
ImportTypes,
|
||||
IDStrings,
|
||||
} from "tachi-common";
|
||||
import { HydrateScore } from "./hydrate-score";
|
||||
import { InsertQueue, QueueScoreInsert, ScoreIDs } from "./insert-score";
|
||||
import {
|
||||
ConverterFailure,
|
||||
InternalFailure,
|
||||
InvalidScoreFailure,
|
||||
KTDataNotFoundFailure,
|
||||
SkipScoreFailure,
|
||||
ConverterFailure,
|
||||
InternalFailure,
|
||||
InvalidScoreFailure,
|
||||
KTDataNotFoundFailure,
|
||||
SkipScoreFailure,
|
||||
} from "../common/converter-failures";
|
||||
import { CreateScoreID } from "./score-id";
|
||||
import db from "../../../../external/mongo/db";
|
||||
import { AppendLogCtx, KtLogger } from "../../../logger/logger";
|
||||
|
||||
import {
|
||||
ConverterFnReturnOrFailure,
|
||||
ConverterFunction,
|
||||
ConverterFnSuccessReturn,
|
||||
ConverterFnReturnOrFailure,
|
||||
ConverterFunction,
|
||||
ConverterFnSuccessReturn,
|
||||
} from "../../import-types/common/types";
|
||||
import { DryScore } from "../common/types";
|
||||
import { OrphanScore } from "../orphans/orphans";
|
||||
@@ -37,49 +37,49 @@ import { OrphanScore } from "../orphans/orphans";
|
||||
* @returns An array of ImportProcessInfo objects.
|
||||
*/
|
||||
export async function ImportAllIterableData<D, C>(
|
||||
userID: integer,
|
||||
importType: ImportTypes,
|
||||
iterableData: Iterable<D> | AsyncIterable<D>,
|
||||
ConverterFunction: ConverterFunction<D, C>,
|
||||
context: C,
|
||||
logger: KtLogger
|
||||
userID: integer,
|
||||
importType: ImportTypes,
|
||||
iterableData: Iterable<D> | AsyncIterable<D>,
|
||||
ConverterFunction: ConverterFunction<D, C>,
|
||||
context: C,
|
||||
logger: KtLogger
|
||||
): Promise<ImportProcessingInfo[]> {
|
||||
logger.verbose(`Starting Data Processing...`);
|
||||
logger.verbose(`Starting Data Processing...`);
|
||||
|
||||
const promises = [];
|
||||
const promises = [];
|
||||
|
||||
// for await is used here as iterableData may be an async iterable
|
||||
// An example would be making an api request after exhausting
|
||||
// the first set of data.
|
||||
for await (const data of iterableData) {
|
||||
promises.push(
|
||||
ImportIterableDatapoint(userID, importType, data, ConverterFunction, context, logger)
|
||||
);
|
||||
}
|
||||
// for await is used here as iterableData may be an async iterable
|
||||
// An example would be making an api request after exhausting
|
||||
// the first set of data.
|
||||
for await (const data of iterableData) {
|
||||
promises.push(
|
||||
ImportIterableDatapoint(userID, importType, data, ConverterFunction, context, logger)
|
||||
);
|
||||
}
|
||||
|
||||
// We need to filter out nulls, which we don't care for (these are neither successes or failures)
|
||||
const processedResults = await Promise.all(promises);
|
||||
// We need to filter out nulls, which we don't care for (these are neither successes or failures)
|
||||
const processedResults = await Promise.all(promises);
|
||||
|
||||
logger.verbose(`Finished Importing Data (${promises.length} datapoints).`);
|
||||
logger.debug(`Removing null returns...`);
|
||||
logger.verbose(`Finished Importing Data (${promises.length} datapoints).`);
|
||||
logger.debug(`Removing null returns...`);
|
||||
|
||||
const datapoints = processedResults.filter(
|
||||
(e) => e !== null
|
||||
) as ImportProcessingInfo<IDStrings>[];
|
||||
const datapoints = processedResults.filter(
|
||||
(e) => e !== null
|
||||
) as ImportProcessingInfo<IDStrings>[];
|
||||
|
||||
logger.debug(`Removed null from results.`);
|
||||
logger.debug(`Removed null from results.`);
|
||||
|
||||
logger.verbose(`Recieved ${datapoints.length} returns, from ${promises.length} data.`);
|
||||
logger.verbose(`Recieved ${datapoints.length} returns, from ${promises.length} data.`);
|
||||
|
||||
// Flush the score queue out after finishing most of the import. This ensures no scores get left in the
|
||||
// queue.
|
||||
const emptied = await InsertQueue();
|
||||
// Flush the score queue out after finishing most of the import. This ensures no scores get left in the
|
||||
// queue.
|
||||
const emptied = await InsertQueue();
|
||||
|
||||
if (emptied) {
|
||||
logger.verbose(`Emptied ${emptied} documents from score queue.`);
|
||||
}
|
||||
if (emptied) {
|
||||
logger.verbose(`Emptied ${emptied} documents from score queue.`);
|
||||
}
|
||||
|
||||
return datapoints;
|
||||
return datapoints;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,140 +91,140 @@ export async function ImportAllIterableData<D, C>(
|
||||
* @returns An array of ImportProcessingInfo objects, or a single ImportProcessingInfo object
|
||||
*/
|
||||
export async function ImportIterableDatapoint<D, C>(
|
||||
userID: integer,
|
||||
importType: ImportTypes,
|
||||
data: D,
|
||||
ConverterFunction: ConverterFunction<D, C>,
|
||||
context: C,
|
||||
logger: KtLogger
|
||||
userID: integer,
|
||||
importType: ImportTypes,
|
||||
data: D,
|
||||
ConverterFunction: ConverterFunction<D, C>,
|
||||
context: C,
|
||||
logger: KtLogger
|
||||
): Promise<ImportProcessingInfo | null> {
|
||||
// Converter Function Return
|
||||
let cfnReturn: ConverterFnReturnOrFailure;
|
||||
// Converter Function Return
|
||||
let cfnReturn: ConverterFnReturnOrFailure;
|
||||
|
||||
try {
|
||||
cfnReturn = await ConverterFunction(data, context, importType, logger);
|
||||
} catch (err) {
|
||||
cfnReturn = err;
|
||||
}
|
||||
try {
|
||||
cfnReturn = await ConverterFunction(data, context, importType, logger);
|
||||
} catch (err) {
|
||||
cfnReturn = err;
|
||||
}
|
||||
|
||||
// if this conversion failed, return it in the proper format
|
||||
if (cfnReturn instanceof ConverterFailure) {
|
||||
if (cfnReturn instanceof KTDataNotFoundFailure) {
|
||||
logger.warn(`KTDataNotFoundFailure: ${cfnReturn.message ?? "No message?"}`, {
|
||||
cfnReturn,
|
||||
hideFromConsole: ["cfnReturn"],
|
||||
});
|
||||
// if this conversion failed, return it in the proper format
|
||||
if (cfnReturn instanceof ConverterFailure) {
|
||||
if (cfnReturn instanceof KTDataNotFoundFailure) {
|
||||
logger.warn(`KTDataNotFoundFailure: ${cfnReturn.message ?? "No message?"}`, {
|
||||
cfnReturn,
|
||||
hideFromConsole: ["cfnReturn"],
|
||||
});
|
||||
|
||||
const insertOrphan = await OrphanScore(
|
||||
cfnReturn.importType,
|
||||
userID,
|
||||
cfnReturn.data,
|
||||
cfnReturn.converterContext,
|
||||
cfnReturn.message,
|
||||
logger
|
||||
);
|
||||
const insertOrphan = await OrphanScore(
|
||||
cfnReturn.importType,
|
||||
userID,
|
||||
cfnReturn.data,
|
||||
cfnReturn.converterContext,
|
||||
cfnReturn.message,
|
||||
logger
|
||||
);
|
||||
|
||||
if (insertOrphan.success) {
|
||||
return {
|
||||
success: false,
|
||||
type: "KTDataNotFound",
|
||||
message: cfnReturn.message,
|
||||
content: {
|
||||
context: cfnReturn.converterContext,
|
||||
data: cfnReturn.data,
|
||||
orphanID: insertOrphan.orphanID,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (insertOrphan.success) {
|
||||
return {
|
||||
success: false,
|
||||
type: "KTDataNotFound",
|
||||
message: cfnReturn.message,
|
||||
content: {
|
||||
context: cfnReturn.converterContext,
|
||||
data: cfnReturn.data,
|
||||
orphanID: insertOrphan.orphanID,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
type: "OrphanExists",
|
||||
message: cfnReturn.message,
|
||||
content: {
|
||||
orphanID: insertOrphan.orphanID,
|
||||
},
|
||||
};
|
||||
} else if (cfnReturn instanceof InvalidScoreFailure) {
|
||||
logger.info(`InvalidScoreFailure: ${cfnReturn.message ?? "No message?"}`, {
|
||||
cfnReturn,
|
||||
hideFromConsole: ["cfnReturn"],
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
type: "InvalidDatapoint",
|
||||
message: cfnReturn.message,
|
||||
content: {},
|
||||
};
|
||||
} else if (cfnReturn instanceof InternalFailure) {
|
||||
logger.error(`Internal error occured.`, { cfnReturn });
|
||||
return {
|
||||
success: false,
|
||||
type: "InternalError",
|
||||
// could return cfnReturn.message here, but we might want to hide the details of the crash.
|
||||
message: "An internal error has occured.",
|
||||
content: {},
|
||||
};
|
||||
} else if (cfnReturn instanceof SkipScoreFailure) {
|
||||
return null;
|
||||
} else {
|
||||
logger.warn(`Unknown error returned as ConverterFailure, Ignoring.`, {
|
||||
err: cfnReturn,
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
type: "InternalError",
|
||||
message: "An internal service error has occured.",
|
||||
content: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
type: "OrphanExists",
|
||||
message: cfnReturn.message,
|
||||
content: {
|
||||
orphanID: insertOrphan.orphanID,
|
||||
},
|
||||
};
|
||||
} else if (cfnReturn instanceof InvalidScoreFailure) {
|
||||
logger.info(`InvalidScoreFailure: ${cfnReturn.message ?? "No message?"}`, {
|
||||
cfnReturn,
|
||||
hideFromConsole: ["cfnReturn"],
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
type: "InvalidDatapoint",
|
||||
message: cfnReturn.message,
|
||||
content: {},
|
||||
};
|
||||
} else if (cfnReturn instanceof InternalFailure) {
|
||||
logger.error(`Internal error occured.`, { cfnReturn });
|
||||
return {
|
||||
success: false,
|
||||
type: "InternalError",
|
||||
// could return cfnReturn.message here, but we might want to hide the details of the crash.
|
||||
message: "An internal error has occured.",
|
||||
content: {},
|
||||
};
|
||||
} else if (cfnReturn instanceof SkipScoreFailure) {
|
||||
return null;
|
||||
} else {
|
||||
logger.warn(`Unknown error returned as ConverterFailure, Ignoring.`, {
|
||||
err: cfnReturn,
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
type: "InternalError",
|
||||
message: "An internal service error has occured.",
|
||||
content: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (cfnReturn instanceof Error) {
|
||||
logger.error(`Unknown error thrown from converter, Ignoring.`, {
|
||||
err: cfnReturn,
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
type: "InternalError",
|
||||
message: "An internal service error has occured.",
|
||||
content: {},
|
||||
};
|
||||
}
|
||||
if (cfnReturn instanceof Error) {
|
||||
logger.error(`Unknown error thrown from converter, Ignoring.`, {
|
||||
err: cfnReturn,
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
type: "InternalError",
|
||||
message: "An internal service error has occured.",
|
||||
content: {},
|
||||
};
|
||||
}
|
||||
|
||||
return ProcessSuccessfulConverterReturn(userID, cfnReturn as ConverterFnSuccessReturn, logger);
|
||||
return ProcessSuccessfulConverterReturn(userID, cfnReturn as ConverterFnSuccessReturn, logger);
|
||||
}
|
||||
|
||||
export async function ProcessSuccessfulConverterReturn(
|
||||
userID: integer,
|
||||
cfnReturn: ConverterFnSuccessReturn,
|
||||
logger: KtLogger
|
||||
userID: integer,
|
||||
cfnReturn: ConverterFnSuccessReturn,
|
||||
logger: KtLogger
|
||||
): Promise<ImportProcessingInfo | null> {
|
||||
const result = await HydrateAndInsertScore(
|
||||
userID,
|
||||
cfnReturn.dryScore,
|
||||
cfnReturn.chart,
|
||||
cfnReturn.song,
|
||||
logger
|
||||
);
|
||||
const result = await HydrateAndInsertScore(
|
||||
userID,
|
||||
cfnReturn.dryScore,
|
||||
cfnReturn.chart,
|
||||
cfnReturn.song,
|
||||
logger
|
||||
);
|
||||
|
||||
// This used to be a ScoreExists error. However, we never actually care about
|
||||
// handling ScoreExists errors (they're nobodies issue)
|
||||
// so instead, the function will just return null, and we pass that on here.
|
||||
if (result === null) {
|
||||
return null;
|
||||
}
|
||||
// This used to be a ScoreExists error. However, we never actually care about
|
||||
// handling ScoreExists errors (they're nobodies issue)
|
||||
// so instead, the function will just return null, and we pass that on here.
|
||||
if (result === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.debug(`Successfully imported score: ${result.scoreID}`);
|
||||
logger.debug(`Successfully imported score: ${result.scoreID}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
type: "ScoreImported",
|
||||
message: null,
|
||||
content: {
|
||||
score: result,
|
||||
},
|
||||
};
|
||||
return {
|
||||
success: true,
|
||||
type: "ScoreImported",
|
||||
message: null,
|
||||
content: {
|
||||
score: result,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -235,52 +235,52 @@ export async function ProcessSuccessfulConverterReturn(
|
||||
* @param song - The song this score is on.
|
||||
*/
|
||||
async function HydrateAndInsertScore(
|
||||
userID: integer,
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument,
|
||||
song: AnySongDocument,
|
||||
importLogger: KtLogger
|
||||
userID: integer,
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument,
|
||||
song: AnySongDocument,
|
||||
importLogger: KtLogger
|
||||
): Promise<ScoreDocument | null> {
|
||||
const scoreID = CreateScoreID(userID, dryScore, chart.chartID);
|
||||
const scoreID = CreateScoreID(userID, dryScore, chart.chartID);
|
||||
|
||||
// sub-context the logger so the below logs are more accurate
|
||||
const logger = AppendLogCtx(scoreID, importLogger);
|
||||
// sub-context the logger so the below logs are more accurate
|
||||
const logger = AppendLogCtx(scoreID, importLogger);
|
||||
|
||||
const existingScore = await db.scores.findOne(
|
||||
{
|
||||
scoreID,
|
||||
},
|
||||
{
|
||||
// micro-optimisation - mongoDB is significantly faster when returning less fields
|
||||
// since we only care about whether we have a score or not here, we can minimise returned
|
||||
// fields.
|
||||
//
|
||||
// feel free to test this yourself! - zkldi (09/04/2021)
|
||||
projection: {
|
||||
_id: 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
const existingScore = await db.scores.findOne(
|
||||
{
|
||||
scoreID,
|
||||
},
|
||||
{
|
||||
// micro-optimisation - mongoDB is significantly faster when returning less fields
|
||||
// since we only care about whether we have a score or not here, we can minimise returned
|
||||
// fields.
|
||||
//
|
||||
// feel free to test this yourself! - zkldi (09/04/2021)
|
||||
projection: {
|
||||
_id: 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (existingScore) {
|
||||
logger.verbose(`Skipped score.`);
|
||||
return null;
|
||||
}
|
||||
if (existingScore) {
|
||||
logger.verbose(`Skipped score.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (ScoreIDs.has(scoreID)) {
|
||||
logger.verbose(`Skipped score.`);
|
||||
return null;
|
||||
}
|
||||
if (ScoreIDs.has(scoreID)) {
|
||||
logger.verbose(`Skipped score.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const score = await HydrateScore(userID, dryScore, chart, song, scoreID, logger);
|
||||
const score = await HydrateScore(userID, dryScore, chart, song, scoreID, logger);
|
||||
|
||||
const res = await QueueScoreInsert(score);
|
||||
const res = await QueueScoreInsert(score);
|
||||
|
||||
// emergency state - this is a last resort for avoiding doubled imports
|
||||
if (res === null) {
|
||||
logger.verbose(`Skipped score - Race Condition protection triggered.`);
|
||||
return null;
|
||||
}
|
||||
// emergency state - this is a last resort for avoiding doubled imports
|
||||
if (res === null) {
|
||||
logger.verbose(`Skipped score - Race Condition protection triggered.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return score;
|
||||
return score;
|
||||
}
|
||||
|
||||
@@ -2,68 +2,68 @@ import t from "tap";
|
||||
import { CreateSessionCalcData } from "./calculated-data";
|
||||
|
||||
function ratingwrap(ratings: [number, number][]) {
|
||||
return ratings.map((e) => ({
|
||||
calculatedData: {
|
||||
ktRating: e[0],
|
||||
ktLampRating: e[1],
|
||||
},
|
||||
}));
|
||||
return ratings.map((e) => ({
|
||||
calculatedData: {
|
||||
ktRating: e[0],
|
||||
ktLampRating: e[1],
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
function avgbest10(arr: number[]) {
|
||||
return (
|
||||
arr
|
||||
.sort((a, b) => b - a)
|
||||
.slice(0, 10)
|
||||
.reduce((a, r) => a + r, 0) / 10
|
||||
);
|
||||
return (
|
||||
arr
|
||||
.sort((a, b) => b - a)
|
||||
.slice(0, 10)
|
||||
.reduce((a, r) => a + r, 0) / 10
|
||||
);
|
||||
}
|
||||
|
||||
t.test("#CreateSessionCalcData", (t) => {
|
||||
t.test("Should return null if less than 10 scores", (t) => {
|
||||
const res = CreateSessionCalcData("iidx", "SP", ratingwrap([[1, 2]]));
|
||||
t.test("Should return null if less than 10 scores", (t) => {
|
||||
const res = CreateSessionCalcData("iidx", "SP", ratingwrap([[1, 2]]));
|
||||
|
||||
t.strictSame(res, { BPI: null, ktRating: null, ktLampRating: null });
|
||||
t.strictSame(res, { BPI: null, ktRating: null, ktLampRating: null });
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should calculate session performance", (t) => {
|
||||
const { BPI, ktRating, ktLampRating } = CreateSessionCalcData(
|
||||
"iidx",
|
||||
"SP",
|
||||
ratingwrap([
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
[3, 4],
|
||||
[4, 4],
|
||||
[6, 1],
|
||||
[1, 6],
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
[3, 4],
|
||||
[4, 4],
|
||||
[6, 1],
|
||||
[1, 6],
|
||||
])
|
||||
);
|
||||
t.test("Should calculate session performance", (t) => {
|
||||
const { BPI, ktRating, ktLampRating } = CreateSessionCalcData(
|
||||
"iidx",
|
||||
"SP",
|
||||
ratingwrap([
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
[3, 4],
|
||||
[4, 4],
|
||||
[6, 1],
|
||||
[1, 6],
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
[3, 4],
|
||||
[4, 4],
|
||||
[6, 1],
|
||||
[1, 6],
|
||||
])
|
||||
);
|
||||
|
||||
t.equal(
|
||||
ktLampRating,
|
||||
avgbest10([1, 2, 4, 4, 1, 6, 1, 2, 4, 4, 1, 6]),
|
||||
"Should correctly calculate lamp performance"
|
||||
);
|
||||
t.equal(
|
||||
ktLampRating,
|
||||
avgbest10([1, 2, 4, 4, 1, 6, 1, 2, 4, 4, 1, 6]),
|
||||
"Should correctly calculate lamp performance"
|
||||
);
|
||||
|
||||
t.equal(
|
||||
ktRating,
|
||||
avgbest10([1, 2, 3, 4, 6, 1, 1, 2, 3, 4, 6, 1]),
|
||||
"Should correctly calculate score performance"
|
||||
);
|
||||
t.equal(
|
||||
ktRating,
|
||||
avgbest10([1, 2, 3, 4, 6, 1, 1, 2, 3, 4, 6, 1]),
|
||||
"Should correctly calculate score performance"
|
||||
);
|
||||
|
||||
t.equal(BPI, null);
|
||||
t.equal(BPI, null);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
@@ -5,131 +5,131 @@ type ScoreCalculatedDataOnly = Pick<ScoreDocument, "calculatedData">;
|
||||
const RELEVANT_SCORES = 10;
|
||||
|
||||
function AverageBest10(vals: (number | null | undefined)[]) {
|
||||
const numbers = vals.filter((e) => typeof e === "number") as number[];
|
||||
const numbers = vals.filter((e) => typeof e === "number") as number[];
|
||||
|
||||
if (numbers.length < RELEVANT_SCORES) {
|
||||
return null;
|
||||
}
|
||||
if (numbers.length < RELEVANT_SCORES) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
numbers
|
||||
.sort((a, b) => b - a)
|
||||
.slice(0, RELEVANT_SCORES)
|
||||
.reduce((a, r) => a + r, 0) / RELEVANT_SCORES
|
||||
);
|
||||
return (
|
||||
numbers
|
||||
.sort((a, b) => b - a)
|
||||
.slice(0, RELEVANT_SCORES)
|
||||
.reduce((a, r) => a + r, 0) / RELEVANT_SCORES
|
||||
);
|
||||
}
|
||||
|
||||
function SumAll(arr: ScoreCalculatedDataOnly[], prop: keyof ScoreDocument["calculatedData"]) {
|
||||
return arr.reduce((a, r) => {
|
||||
if (typeof r.calculatedData[prop] === "number") {
|
||||
return a + (r.calculatedData[prop] as number);
|
||||
}
|
||||
return arr.reduce((a, r) => {
|
||||
if (typeof r.calculatedData[prop] === "number") {
|
||||
return a + (r.calculatedData[prop] as number);
|
||||
}
|
||||
|
||||
return a;
|
||||
}, 0);
|
||||
return a;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function AvgBest10Map(arr: ScoreCalculatedDataOnly[], prop: keyof ScoreDocument["calculatedData"]) {
|
||||
return AverageBest10(arr.map((e) => e.calculatedData[prop]));
|
||||
return AverageBest10(arr.map((e) => e.calculatedData[prop]));
|
||||
}
|
||||
|
||||
type CalculatedDataFunctions = {
|
||||
[G in Game]: {
|
||||
[P in Playtypes[G]]: (
|
||||
scoreCalcData: ScoreCalculatedDataOnly[]
|
||||
) => SessionDocument["calculatedData"];
|
||||
};
|
||||
[G in Game]: {
|
||||
[P in Playtypes[G]]: (
|
||||
scoreCalcData: ScoreCalculatedDataOnly[]
|
||||
) => SessionDocument["calculatedData"];
|
||||
};
|
||||
};
|
||||
|
||||
const CalculatedDataFunctions: CalculatedDataFunctions = {
|
||||
iidx: {
|
||||
SP: (scd) => ({
|
||||
BPI: AvgBest10Map(scd, "BPI"),
|
||||
ktRating: AvgBest10Map(scd, "ktRating"),
|
||||
ktLampRating: AvgBest10Map(scd, "ktLampRating"),
|
||||
}),
|
||||
DP: (scd) => ({
|
||||
BPI: AvgBest10Map(scd, "BPI"),
|
||||
ktRating: AvgBest10Map(scd, "ktRating"),
|
||||
ktLampRating: AvgBest10Map(scd, "ktLampRating"),
|
||||
}),
|
||||
},
|
||||
sdvx: {
|
||||
Single: (scd) => {
|
||||
const VF6 = AvgBest10Map(scd, "VF6");
|
||||
iidx: {
|
||||
SP: (scd) => ({
|
||||
BPI: AvgBest10Map(scd, "BPI"),
|
||||
ktRating: AvgBest10Map(scd, "ktRating"),
|
||||
ktLampRating: AvgBest10Map(scd, "ktLampRating"),
|
||||
}),
|
||||
DP: (scd) => ({
|
||||
BPI: AvgBest10Map(scd, "BPI"),
|
||||
ktRating: AvgBest10Map(scd, "ktRating"),
|
||||
ktLampRating: AvgBest10Map(scd, "ktLampRating"),
|
||||
}),
|
||||
},
|
||||
sdvx: {
|
||||
Single: (scd) => {
|
||||
const VF6 = AvgBest10Map(scd, "VF6");
|
||||
|
||||
return {
|
||||
VF6,
|
||||
profileVF6: VF6 === null ? null : VF6 * 50,
|
||||
};
|
||||
},
|
||||
},
|
||||
popn: {
|
||||
"9B": () => ({}),
|
||||
},
|
||||
museca: {
|
||||
Single: (scd) => ({
|
||||
ktRating: AvgBest10Map(scd, "ktRating"),
|
||||
}),
|
||||
},
|
||||
chunithm: {
|
||||
Single: (scd) => ({
|
||||
rating: AvgBest10Map(scd, "rating"),
|
||||
}),
|
||||
},
|
||||
maimai: {
|
||||
Single: (scd) => ({
|
||||
ktRating: AvgBest10Map(scd, "ktRating"),
|
||||
}),
|
||||
},
|
||||
gitadora: {
|
||||
Gita: (scd) => ({
|
||||
skill: AvgBest10Map(scd, "skill"),
|
||||
}),
|
||||
Dora: (scd) => ({
|
||||
skill: AvgBest10Map(scd, "skill"),
|
||||
}),
|
||||
},
|
||||
bms: {
|
||||
"7K": (scd) => ({
|
||||
ktLampRating: AvgBest10Map(scd, "ktLampRating"),
|
||||
}),
|
||||
"14K": (scd) => ({
|
||||
ktLampRating: AvgBest10Map(scd, "ktLampRating"),
|
||||
}),
|
||||
},
|
||||
ddr: {
|
||||
SP: (scd) => ({
|
||||
MFCP: SumAll(scd, "MFCP"),
|
||||
ktRating: AvgBest10Map(scd, "ktRating"),
|
||||
}),
|
||||
DP: (scd) => ({
|
||||
MFCP: SumAll(scd, "MFCP"),
|
||||
ktRating: AvgBest10Map(scd, "ktRating"),
|
||||
}),
|
||||
},
|
||||
jubeat: {
|
||||
Single: (scd) => ({
|
||||
jubility: AvgBest10Map(scd, "jubility"),
|
||||
}),
|
||||
},
|
||||
usc: {
|
||||
Single: (scd) => {
|
||||
const VF6 = AvgBest10Map(scd, "VF6");
|
||||
return {
|
||||
VF6,
|
||||
profileVF6: VF6 === null ? null : VF6 * 50,
|
||||
};
|
||||
},
|
||||
},
|
||||
popn: {
|
||||
"9B": () => ({}),
|
||||
},
|
||||
museca: {
|
||||
Single: (scd) => ({
|
||||
ktRating: AvgBest10Map(scd, "ktRating"),
|
||||
}),
|
||||
},
|
||||
chunithm: {
|
||||
Single: (scd) => ({
|
||||
rating: AvgBest10Map(scd, "rating"),
|
||||
}),
|
||||
},
|
||||
maimai: {
|
||||
Single: (scd) => ({
|
||||
ktRating: AvgBest10Map(scd, "ktRating"),
|
||||
}),
|
||||
},
|
||||
gitadora: {
|
||||
Gita: (scd) => ({
|
||||
skill: AvgBest10Map(scd, "skill"),
|
||||
}),
|
||||
Dora: (scd) => ({
|
||||
skill: AvgBest10Map(scd, "skill"),
|
||||
}),
|
||||
},
|
||||
bms: {
|
||||
"7K": (scd) => ({
|
||||
ktLampRating: AvgBest10Map(scd, "ktLampRating"),
|
||||
}),
|
||||
"14K": (scd) => ({
|
||||
ktLampRating: AvgBest10Map(scd, "ktLampRating"),
|
||||
}),
|
||||
},
|
||||
ddr: {
|
||||
SP: (scd) => ({
|
||||
MFCP: SumAll(scd, "MFCP"),
|
||||
ktRating: AvgBest10Map(scd, "ktRating"),
|
||||
}),
|
||||
DP: (scd) => ({
|
||||
MFCP: SumAll(scd, "MFCP"),
|
||||
ktRating: AvgBest10Map(scd, "ktRating"),
|
||||
}),
|
||||
},
|
||||
jubeat: {
|
||||
Single: (scd) => ({
|
||||
jubility: AvgBest10Map(scd, "jubility"),
|
||||
}),
|
||||
},
|
||||
usc: {
|
||||
Single: (scd) => {
|
||||
const VF6 = AvgBest10Map(scd, "VF6");
|
||||
|
||||
return {
|
||||
VF6,
|
||||
profileVF6: VF6 === null ? null : VF6 * 50,
|
||||
};
|
||||
},
|
||||
},
|
||||
return {
|
||||
VF6,
|
||||
profileVF6: VF6 === null ? null : VF6 * 50,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function CreateSessionCalcData(
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
scoreCalcData: ScoreCalculatedDataOnly[]
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
scoreCalcData: ScoreCalculatedDataOnly[]
|
||||
): SessionDocument["calculatedData"] {
|
||||
// @ts-expect-error standard game->pt stuff.
|
||||
return CalculatedDataFunctions[game][playtype](scoreCalcData);
|
||||
// @ts-expect-error standard game->pt stuff.
|
||||
return CalculatedDataFunctions[game][playtype](scoreCalcData);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import t from "tap";
|
||||
import { GenerateRandomSessionName } from "./name-generation";
|
||||
|
||||
t.test("#GenerateRandomSessionName", (t) => {
|
||||
const res = GenerateRandomSessionName();
|
||||
const res = GenerateRandomSessionName();
|
||||
|
||||
t.type(res, "string", "Should return a string.");
|
||||
t.type(res, "string", "Should return a string.");
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { GetRandomAdjective, GetRandomNoun } from "../../../../datasets/words";
|
||||
|
||||
export function GenerateRandomSessionName() {
|
||||
const adj1 = GetRandomAdjective();
|
||||
const adj2 = GetRandomAdjective();
|
||||
const noun = GetRandomNoun();
|
||||
const adj1 = GetRandomAdjective();
|
||||
const adj2 = GetRandomAdjective();
|
||||
const noun = GetRandomNoun();
|
||||
|
||||
return `${adj1} ${adj2} ${noun}`;
|
||||
return `${adj1} ${adj2} ${noun}`;
|
||||
}
|
||||
|
||||
@@ -9,440 +9,440 @@ import { PublicUserDocument, ScoreDocument, SessionDocument } from "tachi-common
|
||||
import deepmerge from "deepmerge";
|
||||
|
||||
const logger = CreateScoreLogger(
|
||||
{ username: "test_zkldi", id: 1 } as PublicUserDocument,
|
||||
"foo",
|
||||
"ir/direct-manual"
|
||||
{ username: "test_zkldi", id: 1 } as PublicUserDocument,
|
||||
"foo",
|
||||
"ir/direct-manual"
|
||||
);
|
||||
|
||||
t.test("#CreateSessions", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(() => db.sessions.remove({}));
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(() => db.sessions.remove({}));
|
||||
|
||||
t.test("Should compose sessions from one timestamped score provided.", async (t) => {
|
||||
const res = await CreateSessions(
|
||||
1,
|
||||
"ir/direct-manual",
|
||||
"iidx",
|
||||
{ SP: [TestingIIDXSPScore] },
|
||||
logger
|
||||
);
|
||||
t.test("Should compose sessions from one timestamped score provided.", async (t) => {
|
||||
const res = await CreateSessions(
|
||||
1,
|
||||
"ir/direct-manual",
|
||||
"iidx",
|
||||
{ SP: [TestingIIDXSPScore] },
|
||||
logger
|
||||
);
|
||||
|
||||
t.match(res, [
|
||||
{
|
||||
sessionID: /^Q[a-f0-9]{40}$/u,
|
||||
type: "Created",
|
||||
},
|
||||
]);
|
||||
t.match(res, [
|
||||
{
|
||||
sessionID: /^Q[a-f0-9]{40}$/u,
|
||||
type: "Created",
|
||||
},
|
||||
]);
|
||||
|
||||
t.equal(res.length, 1);
|
||||
t.equal(res.length, 1);
|
||||
|
||||
const session = await db.sessions.findOne({ userID: 1, game: "iidx", playtype: "SP" });
|
||||
const session = await db.sessions.findOne({ userID: 1, game: "iidx", playtype: "SP" });
|
||||
|
||||
const sessionCount = await db.sessions.count({ userID: 1, game: "iidx", playtype: "SP" });
|
||||
const sessionCount = await db.sessions.count({ userID: 1, game: "iidx", playtype: "SP" });
|
||||
|
||||
t.equal(sessionCount, 1);
|
||||
t.equal(sessionCount, 1);
|
||||
|
||||
t.hasStrict(session, {
|
||||
userID: 1,
|
||||
importType: "ir/direct-manual",
|
||||
// name: "adjective1 adjective2 noun1",
|
||||
sessionID: res[0].sessionID,
|
||||
desc: null,
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
highlight: false,
|
||||
scoreInfo: [
|
||||
{
|
||||
scoreID: "TESTING_SCORE_ID",
|
||||
isNewScore: true,
|
||||
},
|
||||
],
|
||||
// timeInserted: 1622289329729,
|
||||
timeStarted: 1619454485988,
|
||||
timeEnded: 1619454485988,
|
||||
calculatedData: {},
|
||||
});
|
||||
t.hasStrict(session, {
|
||||
userID: 1,
|
||||
importType: "ir/direct-manual",
|
||||
// name: "adjective1 adjective2 noun1",
|
||||
sessionID: res[0].sessionID,
|
||||
desc: null,
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
highlight: false,
|
||||
scoreInfo: [
|
||||
{
|
||||
scoreID: "TESTING_SCORE_ID",
|
||||
isNewScore: true,
|
||||
},
|
||||
],
|
||||
// timeInserted: 1622289329729,
|
||||
timeStarted: 1619454485988,
|
||||
timeEnded: 1619454485988,
|
||||
calculatedData: {},
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should not compose sessions from untimestamped scores.", async (t) => {
|
||||
const res = await CreateSessions(
|
||||
1,
|
||||
"ir/direct-manual",
|
||||
"iidx",
|
||||
{ SP: [deepmerge(TestingIIDXSPScore, { timeAchieved: null })] },
|
||||
logger
|
||||
);
|
||||
t.test("Should not compose sessions from untimestamped scores.", async (t) => {
|
||||
const res = await CreateSessions(
|
||||
1,
|
||||
"ir/direct-manual",
|
||||
"iidx",
|
||||
{ SP: [deepmerge(TestingIIDXSPScore, { timeAchieved: null })] },
|
||||
logger
|
||||
);
|
||||
|
||||
t.strictSame(res, []);
|
||||
t.strictSame(res, []);
|
||||
|
||||
const sessionCount = await db.sessions.count({ userID: 1, game: "iidx", playtype: "SP" });
|
||||
const sessionCount = await db.sessions.count({ userID: 1, game: "iidx", playtype: "SP" });
|
||||
|
||||
t.equal(sessionCount, 0);
|
||||
t.equal(sessionCount, 0);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should compose sessions for multiple playtypes.", async (t) => {
|
||||
const res = await CreateSessions(
|
||||
1,
|
||||
"ir/direct-manual",
|
||||
"iidx",
|
||||
{ DP: [deepmerge(TestingIIDXSPScore, { playtype: "DP" })], SP: [TestingIIDXSPScore] },
|
||||
logger
|
||||
);
|
||||
t.test("Should compose sessions for multiple playtypes.", async (t) => {
|
||||
const res = await CreateSessions(
|
||||
1,
|
||||
"ir/direct-manual",
|
||||
"iidx",
|
||||
{ DP: [deepmerge(TestingIIDXSPScore, { playtype: "DP" })], SP: [TestingIIDXSPScore] },
|
||||
logger
|
||||
);
|
||||
|
||||
t.equal(res.length, 2);
|
||||
t.equal(res.length, 2);
|
||||
|
||||
const sessionCount = await db.sessions.count({ userID: 1, game: "iidx", playtype: "SP" });
|
||||
const sessionCount = await db.sessions.count({ userID: 1, game: "iidx", playtype: "SP" });
|
||||
|
||||
t.equal(sessionCount, 1);
|
||||
t.equal(sessionCount, 1);
|
||||
|
||||
const sessionCountDP = await db.sessions.count({ userID: 1, game: "iidx", playtype: "DP" });
|
||||
const sessionCountDP = await db.sessions.count({ userID: 1, game: "iidx", playtype: "DP" });
|
||||
|
||||
t.equal(sessionCountDP, 1);
|
||||
t.equal(sessionCountDP, 1);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
// just an arbitrary start point for our sessions as an offset
|
||||
const start = 1619454485988;
|
||||
|
||||
t.test("#LoadScoresIntoSessions", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(() => db.sessions.remove({}));
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(() => db.sessions.remove({}));
|
||||
|
||||
t.test("Should create sessions from scores.", async (t) => {
|
||||
const res = await LoadScoresIntoSessions(
|
||||
1,
|
||||
"ir/direct-manual",
|
||||
[null, start, start + 1000, start + 2000, start + 3000].map((e, i) =>
|
||||
deepmerge(TestingIIDXSPScore, { timeAchieved: e, scoreID: `SCORE_ID_${i}` })
|
||||
) as ScoreDocument[],
|
||||
"iidx",
|
||||
"SP",
|
||||
logger
|
||||
);
|
||||
t.test("Should create sessions from scores.", async (t) => {
|
||||
const res = await LoadScoresIntoSessions(
|
||||
1,
|
||||
"ir/direct-manual",
|
||||
[null, start, start + 1000, start + 2000, start + 3000].map((e, i) =>
|
||||
deepmerge(TestingIIDXSPScore, { timeAchieved: e, scoreID: `SCORE_ID_${i}` })
|
||||
) as ScoreDocument[],
|
||||
"iidx",
|
||||
"SP",
|
||||
logger
|
||||
);
|
||||
|
||||
t.match(res, [
|
||||
{
|
||||
sessionID: /^Q[a-f0-9]{40}$/u,
|
||||
type: "Created",
|
||||
},
|
||||
]);
|
||||
t.match(res, [
|
||||
{
|
||||
sessionID: /^Q[a-f0-9]{40}$/u,
|
||||
type: "Created",
|
||||
},
|
||||
]);
|
||||
|
||||
const sessions = await db.sessions.find({ game: "iidx", playtype: "SP", userID: 1 });
|
||||
const sessions = await db.sessions.find({ game: "iidx", playtype: "SP", userID: 1 });
|
||||
|
||||
t.equal(sessions.length, 1);
|
||||
t.equal(sessions.length, 1);
|
||||
|
||||
t.strictSame(sessions[0].scoreInfo, [
|
||||
{
|
||||
scoreID: "SCORE_ID_1",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_2",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_3",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_4",
|
||||
isNewScore: true,
|
||||
},
|
||||
]);
|
||||
t.strictSame(sessions[0].scoreInfo, [
|
||||
{
|
||||
scoreID: "SCORE_ID_1",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_2",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_3",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_4",
|
||||
isNewScore: true,
|
||||
},
|
||||
]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should sort scores before creating sessions.", async (t) => {
|
||||
const res = await LoadScoresIntoSessions(
|
||||
1,
|
||||
"ir/direct-manual",
|
||||
[null, start, start - 4000, start + 2000, start - 3000, null, start + 1000].map(
|
||||
(e, i) =>
|
||||
deepmerge(TestingIIDXSPScore, { timeAchieved: e, scoreID: `SCORE_ID_${i}` })
|
||||
) as ScoreDocument[],
|
||||
"iidx",
|
||||
"SP",
|
||||
logger
|
||||
);
|
||||
t.test("Should sort scores before creating sessions.", async (t) => {
|
||||
const res = await LoadScoresIntoSessions(
|
||||
1,
|
||||
"ir/direct-manual",
|
||||
[null, start, start - 4000, start + 2000, start - 3000, null, start + 1000].map(
|
||||
(e, i) =>
|
||||
deepmerge(TestingIIDXSPScore, { timeAchieved: e, scoreID: `SCORE_ID_${i}` })
|
||||
) as ScoreDocument[],
|
||||
"iidx",
|
||||
"SP",
|
||||
logger
|
||||
);
|
||||
|
||||
t.match(res, [
|
||||
{
|
||||
sessionID: /^Q[a-f0-9]{40}$/u,
|
||||
type: "Created",
|
||||
},
|
||||
]);
|
||||
t.match(res, [
|
||||
{
|
||||
sessionID: /^Q[a-f0-9]{40}$/u,
|
||||
type: "Created",
|
||||
},
|
||||
]);
|
||||
|
||||
const sessions = await db.sessions.find({ game: "iidx", playtype: "SP", userID: 1 });
|
||||
const sessions = await db.sessions.find({ game: "iidx", playtype: "SP", userID: 1 });
|
||||
|
||||
t.equal(sessions.length, 1);
|
||||
t.equal(sessions.length, 1);
|
||||
|
||||
t.strictSame(sessions[0].scoreInfo, [
|
||||
{
|
||||
scoreID: "SCORE_ID_2",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_4",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_1",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_6",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_3",
|
||||
isNewScore: true,
|
||||
},
|
||||
]);
|
||||
t.strictSame(sessions[0].scoreInfo, [
|
||||
{
|
||||
scoreID: "SCORE_ID_2",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_4",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_1",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_6",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_3",
|
||||
isNewScore: true,
|
||||
},
|
||||
]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
const TWO_HOURS = 1000 * 60 * 60 * 2;
|
||||
const TWO_HOURS = 1000 * 60 * 60 * 2;
|
||||
|
||||
t.test("Should create multiple sessions if scores are far apart.", async (t) => {
|
||||
const res = await LoadScoresIntoSessions(
|
||||
1,
|
||||
"ir/direct-manual",
|
||||
[
|
||||
null,
|
||||
start,
|
||||
start + 1000,
|
||||
start + 2000,
|
||||
start + TWO_HOURS + 4000,
|
||||
start + TWO_HOURS + 5000,
|
||||
start + TWO_HOURS + 6000,
|
||||
].map((e, i) =>
|
||||
deepmerge(TestingIIDXSPScore, { timeAchieved: e, scoreID: `SCORE_ID_${i}` })
|
||||
) as ScoreDocument[],
|
||||
"iidx",
|
||||
"SP",
|
||||
logger
|
||||
);
|
||||
t.test("Should create multiple sessions if scores are far apart.", async (t) => {
|
||||
const res = await LoadScoresIntoSessions(
|
||||
1,
|
||||
"ir/direct-manual",
|
||||
[
|
||||
null,
|
||||
start,
|
||||
start + 1000,
|
||||
start + 2000,
|
||||
start + TWO_HOURS + 4000,
|
||||
start + TWO_HOURS + 5000,
|
||||
start + TWO_HOURS + 6000,
|
||||
].map((e, i) =>
|
||||
deepmerge(TestingIIDXSPScore, { timeAchieved: e, scoreID: `SCORE_ID_${i}` })
|
||||
) as ScoreDocument[],
|
||||
"iidx",
|
||||
"SP",
|
||||
logger
|
||||
);
|
||||
|
||||
t.match(res, [
|
||||
{
|
||||
sessionID: /^Q[a-f0-9]{40}$/u,
|
||||
type: "Created",
|
||||
},
|
||||
{
|
||||
sessionID: /^Q[a-f0-9]{40}$/u,
|
||||
type: "Created",
|
||||
},
|
||||
]);
|
||||
t.match(res, [
|
||||
{
|
||||
sessionID: /^Q[a-f0-9]{40}$/u,
|
||||
type: "Created",
|
||||
},
|
||||
{
|
||||
sessionID: /^Q[a-f0-9]{40}$/u,
|
||||
type: "Created",
|
||||
},
|
||||
]);
|
||||
|
||||
const sessions = await db.sessions.find({ game: "iidx", playtype: "SP", userID: 1 });
|
||||
const sessions = await db.sessions.find({ game: "iidx", playtype: "SP", userID: 1 });
|
||||
|
||||
t.equal(sessions.length, 2);
|
||||
t.equal(sessions.length, 2);
|
||||
|
||||
t.strictSame(sessions[0].scoreInfo, [
|
||||
{
|
||||
scoreID: "SCORE_ID_1",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_2",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_3",
|
||||
isNewScore: true,
|
||||
},
|
||||
]);
|
||||
t.strictSame(sessions[1].scoreInfo, [
|
||||
{
|
||||
scoreID: "SCORE_ID_4",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_5",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_6",
|
||||
isNewScore: true,
|
||||
},
|
||||
]);
|
||||
t.strictSame(sessions[0].scoreInfo, [
|
||||
{
|
||||
scoreID: "SCORE_ID_1",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_2",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_3",
|
||||
isNewScore: true,
|
||||
},
|
||||
]);
|
||||
t.strictSame(sessions[1].scoreInfo, [
|
||||
{
|
||||
scoreID: "SCORE_ID_4",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_5",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_6",
|
||||
isNewScore: true,
|
||||
},
|
||||
]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should append to existing sessions.", async (t) => {
|
||||
await db.sessions.insert({
|
||||
sessionID: "EXAMPLE_SESSION_ID",
|
||||
userID: 1,
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
importType: "ir/direct-manual",
|
||||
timeStarted: start,
|
||||
timeEnded: start,
|
||||
scoreInfo: [
|
||||
{
|
||||
scoreID: "EXAMPLE_SCORE_ID",
|
||||
isNewScore: true,
|
||||
},
|
||||
],
|
||||
} as SessionDocument);
|
||||
t.test("Should append to existing sessions.", async (t) => {
|
||||
await db.sessions.insert({
|
||||
sessionID: "EXAMPLE_SESSION_ID",
|
||||
userID: 1,
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
importType: "ir/direct-manual",
|
||||
timeStarted: start,
|
||||
timeEnded: start,
|
||||
scoreInfo: [
|
||||
{
|
||||
scoreID: "EXAMPLE_SCORE_ID",
|
||||
isNewScore: true,
|
||||
},
|
||||
],
|
||||
} as SessionDocument);
|
||||
|
||||
const res = await LoadScoresIntoSessions(
|
||||
1,
|
||||
"ir/direct-manual",
|
||||
[null, start, start + 1000, start + 2000].map((e, i) =>
|
||||
deepmerge(TestingIIDXSPScore, { timeAchieved: e, scoreID: `SCORE_ID_${i}` })
|
||||
) as ScoreDocument[],
|
||||
"iidx",
|
||||
"SP",
|
||||
logger
|
||||
);
|
||||
const res = await LoadScoresIntoSessions(
|
||||
1,
|
||||
"ir/direct-manual",
|
||||
[null, start, start + 1000, start + 2000].map((e, i) =>
|
||||
deepmerge(TestingIIDXSPScore, { timeAchieved: e, scoreID: `SCORE_ID_${i}` })
|
||||
) as ScoreDocument[],
|
||||
"iidx",
|
||||
"SP",
|
||||
logger
|
||||
);
|
||||
|
||||
t.strictSame(res, [
|
||||
{
|
||||
sessionID: "EXAMPLE_SESSION_ID",
|
||||
type: "Appended",
|
||||
},
|
||||
]);
|
||||
t.strictSame(res, [
|
||||
{
|
||||
sessionID: "EXAMPLE_SESSION_ID",
|
||||
type: "Appended",
|
||||
},
|
||||
]);
|
||||
|
||||
const sessions = await db.sessions.find({ game: "iidx", playtype: "SP", userID: 1 });
|
||||
const sessions = await db.sessions.find({ game: "iidx", playtype: "SP", userID: 1 });
|
||||
|
||||
t.equal(sessions.length, 1);
|
||||
t.equal(sessions.length, 1);
|
||||
|
||||
t.strictSame(sessions[0].scoreInfo, [
|
||||
{
|
||||
scoreID: "EXAMPLE_SCORE_ID",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_1",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_2",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_3",
|
||||
isNewScore: true,
|
||||
},
|
||||
]);
|
||||
t.strictSame(sessions[0].scoreInfo, [
|
||||
{
|
||||
scoreID: "EXAMPLE_SCORE_ID",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_1",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_2",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_3",
|
||||
isNewScore: true,
|
||||
},
|
||||
]);
|
||||
|
||||
t.equal(sessions[0].timeEnded, start + 2000);
|
||||
t.equal(sessions[0].timeEnded, start + 2000);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should change the timeStarted of existing sessions if prepending.", async (t) => {
|
||||
await db.sessions.insert({
|
||||
sessionID: "EXAMPLE_SESSION_ID",
|
||||
userID: 1,
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
importType: "ir/direct-manual",
|
||||
timeStarted: start,
|
||||
timeEnded: start,
|
||||
scoreInfo: [
|
||||
{
|
||||
scoreID: "EXAMPLE_SCORE_ID",
|
||||
isNewScore: true,
|
||||
},
|
||||
],
|
||||
} as SessionDocument);
|
||||
t.test("Should change the timeStarted of existing sessions if prepending.", async (t) => {
|
||||
await db.sessions.insert({
|
||||
sessionID: "EXAMPLE_SESSION_ID",
|
||||
userID: 1,
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
importType: "ir/direct-manual",
|
||||
timeStarted: start,
|
||||
timeEnded: start,
|
||||
scoreInfo: [
|
||||
{
|
||||
scoreID: "EXAMPLE_SCORE_ID",
|
||||
isNewScore: true,
|
||||
},
|
||||
],
|
||||
} as SessionDocument);
|
||||
|
||||
const res = await LoadScoresIntoSessions(
|
||||
1,
|
||||
"ir/direct-manual",
|
||||
[null, start, start - 1000, start - 2000].map((e, i) =>
|
||||
deepmerge(TestingIIDXSPScore, { timeAchieved: e, scoreID: `SCORE_ID_${i}` })
|
||||
) as ScoreDocument[],
|
||||
"iidx",
|
||||
"SP",
|
||||
logger
|
||||
);
|
||||
const res = await LoadScoresIntoSessions(
|
||||
1,
|
||||
"ir/direct-manual",
|
||||
[null, start, start - 1000, start - 2000].map((e, i) =>
|
||||
deepmerge(TestingIIDXSPScore, { timeAchieved: e, scoreID: `SCORE_ID_${i}` })
|
||||
) as ScoreDocument[],
|
||||
"iidx",
|
||||
"SP",
|
||||
logger
|
||||
);
|
||||
|
||||
t.strictSame(res, [
|
||||
{
|
||||
sessionID: "EXAMPLE_SESSION_ID",
|
||||
type: "Appended",
|
||||
},
|
||||
]);
|
||||
t.strictSame(res, [
|
||||
{
|
||||
sessionID: "EXAMPLE_SESSION_ID",
|
||||
type: "Appended",
|
||||
},
|
||||
]);
|
||||
|
||||
const sessions = await db.sessions.find({ game: "iidx", playtype: "SP", userID: 1 });
|
||||
const sessions = await db.sessions.find({ game: "iidx", playtype: "SP", userID: 1 });
|
||||
|
||||
t.equal(sessions.length, 1);
|
||||
t.equal(sessions.length, 1);
|
||||
|
||||
t.strictSame(sessions[0].scoreInfo, [
|
||||
{
|
||||
scoreID: "EXAMPLE_SCORE_ID",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_3",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_2",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_1",
|
||||
isNewScore: true,
|
||||
},
|
||||
]);
|
||||
t.strictSame(sessions[0].scoreInfo, [
|
||||
{
|
||||
scoreID: "EXAMPLE_SCORE_ID",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_3",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_2",
|
||||
isNewScore: true,
|
||||
},
|
||||
{
|
||||
scoreID: "SCORE_ID_1",
|
||||
isNewScore: true,
|
||||
},
|
||||
]);
|
||||
|
||||
t.equal(sessions[0].timeStarted, start - 2000);
|
||||
t.equal(sessions[0].timeStarted, start - 2000);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should calculate pbDifferences if a scorePB exists.", async (t) => {
|
||||
await db["personal-bests"].insert(TestingIIDXSPScorePB);
|
||||
t.test("Should calculate pbDifferences if a scorePB exists.", async (t) => {
|
||||
await db["personal-bests"].insert(TestingIIDXSPScorePB);
|
||||
|
||||
const res = await LoadScoresIntoSessions(
|
||||
1,
|
||||
"ir/direct-manual",
|
||||
[TestingIIDXSPScore],
|
||||
"iidx",
|
||||
"SP",
|
||||
logger
|
||||
);
|
||||
const res = await LoadScoresIntoSessions(
|
||||
1,
|
||||
"ir/direct-manual",
|
||||
[TestingIIDXSPScore],
|
||||
"iidx",
|
||||
"SP",
|
||||
logger
|
||||
);
|
||||
|
||||
t.match(res, [
|
||||
{
|
||||
sessionID: /^Q[a-f0-9]{40}$/u,
|
||||
type: "Created",
|
||||
},
|
||||
]);
|
||||
t.match(res, [
|
||||
{
|
||||
sessionID: /^Q[a-f0-9]{40}$/u,
|
||||
type: "Created",
|
||||
},
|
||||
]);
|
||||
|
||||
const sessions = await db.sessions.find({ game: "iidx", playtype: "SP", userID: 1 });
|
||||
const sessions = await db.sessions.find({ game: "iidx", playtype: "SP", userID: 1 });
|
||||
|
||||
t.equal(sessions.length, 1);
|
||||
t.equal(sessions.length, 1);
|
||||
|
||||
t.strictSame(sessions[0].scoreInfo, [
|
||||
{
|
||||
scoreID: "TESTING_SCORE_ID",
|
||||
isNewScore: false,
|
||||
gradeDelta: 4,
|
||||
lampDelta: -2,
|
||||
percentDelta: -44.08396946564885,
|
||||
scoreDelta: -693,
|
||||
},
|
||||
]);
|
||||
t.strictSame(sessions[0].scoreInfo, [
|
||||
{
|
||||
scoreID: "TESTING_SCORE_ID",
|
||||
isNewScore: false,
|
||||
gradeDelta: 4,
|
||||
lampDelta: -2,
|
||||
percentDelta: -44.08396946564885,
|
||||
scoreDelta: -693,
|
||||
},
|
||||
]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {
|
||||
integer,
|
||||
ImportTypes,
|
||||
ScoreDocument,
|
||||
SessionDocument,
|
||||
Playtypes,
|
||||
PBScoreDocument,
|
||||
SessionScoreInfo,
|
||||
SessionInfoReturn,
|
||||
Game,
|
||||
integer,
|
||||
ImportTypes,
|
||||
ScoreDocument,
|
||||
SessionDocument,
|
||||
Playtypes,
|
||||
PBScoreDocument,
|
||||
SessionScoreInfo,
|
||||
SessionInfoReturn,
|
||||
Game,
|
||||
} from "tachi-common";
|
||||
import db from "../../../../external/mongo/db";
|
||||
import { AppendLogCtx, KtLogger } from "../../../logger/logger";
|
||||
@@ -21,33 +21,33 @@ import { ScorePlaytypeMap } from "../common/types";
|
||||
const TWO_HOURS = 1000 * 60 * 60 * 2;
|
||||
|
||||
export async function CreateSessions(
|
||||
userID: integer,
|
||||
importType: ImportTypes,
|
||||
game: Game,
|
||||
scorePtMap: ScorePlaytypeMap,
|
||||
logger: KtLogger
|
||||
userID: integer,
|
||||
importType: ImportTypes,
|
||||
game: Game,
|
||||
scorePtMap: ScorePlaytypeMap,
|
||||
logger: KtLogger
|
||||
) {
|
||||
const allSessionInfo = [];
|
||||
const allSessionInfo = [];
|
||||
|
||||
/* eslint-disable no-await-in-loop */
|
||||
for (const playtype in scorePtMap) {
|
||||
// @ts-expect-error This is my least favourite thing about ts.
|
||||
const scores = scorePtMap[playtype] as ScoreDocument[];
|
||||
/* eslint-disable no-await-in-loop */
|
||||
for (const playtype in scorePtMap) {
|
||||
// @ts-expect-error This is my least favourite thing about ts.
|
||||
const scores = scorePtMap[playtype] as ScoreDocument[];
|
||||
|
||||
const sessionInfo = await LoadScoresIntoSessions(
|
||||
userID,
|
||||
importType,
|
||||
scores,
|
||||
game,
|
||||
playtype as Playtypes[Game],
|
||||
logger
|
||||
);
|
||||
const sessionInfo = await LoadScoresIntoSessions(
|
||||
userID,
|
||||
importType,
|
||||
scores,
|
||||
game,
|
||||
playtype as Playtypes[Game],
|
||||
logger
|
||||
);
|
||||
|
||||
allSessionInfo.push(...sessionInfo);
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
allSessionInfo.push(...sessionInfo);
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
|
||||
return allSessionInfo;
|
||||
return allSessionInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,228 +55,228 @@ export async function CreateSessions(
|
||||
* as a SessionScoreInfo object.
|
||||
*/
|
||||
function ProcessScoreIntoSessionScoreInfo(
|
||||
score: ScoreDocument,
|
||||
previousPB: PBScoreDocument | undefined
|
||||
score: ScoreDocument,
|
||||
previousPB: PBScoreDocument | undefined
|
||||
): SessionScoreInfo {
|
||||
if (!previousPB) {
|
||||
return {
|
||||
scoreID: score.scoreID,
|
||||
isNewScore: true,
|
||||
};
|
||||
}
|
||||
if (!previousPB) {
|
||||
return {
|
||||
scoreID: score.scoreID,
|
||||
isNewScore: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
scoreID: score.scoreID,
|
||||
isNewScore: false,
|
||||
gradeDelta: score.scoreData.gradeIndex - previousPB.scoreData.gradeIndex,
|
||||
lampDelta: score.scoreData.lampIndex - previousPB.scoreData.lampIndex,
|
||||
percentDelta: score.scoreData.percent - previousPB.scoreData.percent,
|
||||
scoreDelta: score.scoreData.score - previousPB.scoreData.score,
|
||||
};
|
||||
return {
|
||||
scoreID: score.scoreID,
|
||||
isNewScore: false,
|
||||
gradeDelta: score.scoreData.gradeIndex - previousPB.scoreData.gradeIndex,
|
||||
lampDelta: score.scoreData.lampIndex - previousPB.scoreData.lampIndex,
|
||||
percentDelta: score.scoreData.percent - previousPB.scoreData.percent,
|
||||
scoreDelta: score.scoreData.score - previousPB.scoreData.score,
|
||||
};
|
||||
}
|
||||
|
||||
function CreateSessionID() {
|
||||
return `Q${crypto.randomBytes(20).toString("hex")}`;
|
||||
return `Q${crypto.randomBytes(20).toString("hex")}`;
|
||||
}
|
||||
|
||||
function UpdateExistingSession(
|
||||
existingSession: SessionDocument,
|
||||
newInfo: SessionScoreInfo[],
|
||||
oldScores: ScoreDocument[],
|
||||
newScores: ScoreDocument[]
|
||||
existingSession: SessionDocument,
|
||||
newInfo: SessionScoreInfo[],
|
||||
oldScores: ScoreDocument[],
|
||||
newScores: ScoreDocument[]
|
||||
) {
|
||||
const allScores = [...oldScores, ...newScores];
|
||||
const allScores = [...oldScores, ...newScores];
|
||||
|
||||
const calculatedData = CreateSessionCalcData(
|
||||
existingSession.game,
|
||||
existingSession.playtype,
|
||||
allScores
|
||||
);
|
||||
const calculatedData = CreateSessionCalcData(
|
||||
existingSession.game,
|
||||
existingSession.playtype,
|
||||
allScores
|
||||
);
|
||||
|
||||
existingSession.calculatedData = calculatedData;
|
||||
existingSession.scoreInfo = [...existingSession.scoreInfo, ...newInfo];
|
||||
existingSession.calculatedData = calculatedData;
|
||||
existingSession.scoreInfo = [...existingSession.scoreInfo, ...newInfo];
|
||||
|
||||
if (newScores[0].timeAchieved! < existingSession.timeStarted) {
|
||||
existingSession.timeStarted = newScores[0].timeAchieved!;
|
||||
}
|
||||
if (newScores[0].timeAchieved! < existingSession.timeStarted) {
|
||||
existingSession.timeStarted = newScores[0].timeAchieved!;
|
||||
}
|
||||
|
||||
if (newScores[newScores.length - 1].timeAchieved! > existingSession.timeEnded) {
|
||||
existingSession.timeEnded = newScores[newScores.length - 1].timeAchieved!;
|
||||
}
|
||||
if (newScores[newScores.length - 1].timeAchieved! > existingSession.timeEnded) {
|
||||
existingSession.timeEnded = newScores[newScores.length - 1].timeAchieved!;
|
||||
}
|
||||
|
||||
return existingSession;
|
||||
return existingSession;
|
||||
}
|
||||
|
||||
function CreateSession(
|
||||
userID: integer,
|
||||
importType: ImportTypes,
|
||||
groupInfo: SessionScoreInfo[],
|
||||
groupScores: ScoreDocument[],
|
||||
game: Game,
|
||||
playtype: Playtypes[Game]
|
||||
userID: integer,
|
||||
importType: ImportTypes,
|
||||
groupInfo: SessionScoreInfo[],
|
||||
groupScores: ScoreDocument[],
|
||||
game: Game,
|
||||
playtype: Playtypes[Game]
|
||||
): SessionDocument {
|
||||
const name = GenerateRandomSessionName();
|
||||
const name = GenerateRandomSessionName();
|
||||
|
||||
const calculatedData = CreateSessionCalcData(game, playtype, groupScores);
|
||||
const calculatedData = CreateSessionCalcData(game, playtype, groupScores);
|
||||
|
||||
return {
|
||||
userID,
|
||||
importType,
|
||||
name,
|
||||
sessionID: CreateSessionID(),
|
||||
desc: null,
|
||||
game,
|
||||
playtype,
|
||||
highlight: false,
|
||||
scoreInfo: groupInfo,
|
||||
timeInserted: Date.now(),
|
||||
timeStarted: groupScores[0].timeAchieved!,
|
||||
timeEnded: groupScores[groupScores.length - 1].timeAchieved!,
|
||||
calculatedData,
|
||||
};
|
||||
return {
|
||||
userID,
|
||||
importType,
|
||||
name,
|
||||
sessionID: CreateSessionID(),
|
||||
desc: null,
|
||||
game,
|
||||
playtype,
|
||||
highlight: false,
|
||||
scoreInfo: groupInfo,
|
||||
timeInserted: Date.now(),
|
||||
timeStarted: groupScores[0].timeAchieved!,
|
||||
timeEnded: groupScores[groupScores.length - 1].timeAchieved!,
|
||||
calculatedData,
|
||||
};
|
||||
}
|
||||
|
||||
export async function LoadScoresIntoSessions(
|
||||
userID: integer,
|
||||
importType: ImportTypes,
|
||||
importScores: ScoreDocument[],
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
baseLogger: KtLogger
|
||||
userID: integer,
|
||||
importType: ImportTypes,
|
||||
importScores: ScoreDocument[],
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
baseLogger: KtLogger
|
||||
): Promise<SessionInfoReturn[]> {
|
||||
const logger = AppendLogCtx("Session Generation", baseLogger);
|
||||
const logger = AppendLogCtx("Session Generation", baseLogger);
|
||||
|
||||
const timestampedScores = [];
|
||||
const timestampedScores = [];
|
||||
|
||||
for (const score of importScores) {
|
||||
if (!score.timeAchieved) {
|
||||
logger.verbose(`Ignored score ${score.scoreID}, as it had no timeAchieved.`);
|
||||
// ignore scores without timestamps. We can't use these for sessions.
|
||||
continue;
|
||||
}
|
||||
for (const score of importScores) {
|
||||
if (!score.timeAchieved) {
|
||||
logger.verbose(`Ignored score ${score.scoreID}, as it had no timeAchieved.`);
|
||||
// ignore scores without timestamps. We can't use these for sessions.
|
||||
continue;
|
||||
}
|
||||
|
||||
timestampedScores.push(score);
|
||||
}
|
||||
timestampedScores.push(score);
|
||||
}
|
||||
|
||||
// If we have nothing to work with, why bother?
|
||||
if (timestampedScores.length === 0) {
|
||||
logger.verbose(`Skipped calculating sessions as there were no timestamped scores`);
|
||||
return [];
|
||||
}
|
||||
// If we have nothing to work with, why bother?
|
||||
if (timestampedScores.length === 0) {
|
||||
logger.verbose(`Skipped calculating sessions as there were no timestamped scores`);
|
||||
return [];
|
||||
}
|
||||
|
||||
// sort scores ascendingly.
|
||||
timestampedScores.sort((a, b) => a.timeAchieved! - b.timeAchieved!);
|
||||
// sort scores ascendingly.
|
||||
timestampedScores.sort((a, b) => a.timeAchieved! - b.timeAchieved!);
|
||||
|
||||
// The "Score Groups" for the array of scores provided.
|
||||
// This contains scores split on 2hr margins, which allows for more optimised
|
||||
// session db requests.
|
||||
const sessionScoreGroups: ScoreDocument[][] = [];
|
||||
let curGroup: ScoreDocument[] = [];
|
||||
let lastTimestamp = 0;
|
||||
// The "Score Groups" for the array of scores provided.
|
||||
// This contains scores split on 2hr margins, which allows for more optimised
|
||||
// session db requests.
|
||||
const sessionScoreGroups: ScoreDocument[][] = [];
|
||||
let curGroup: ScoreDocument[] = [];
|
||||
let lastTimestamp = 0;
|
||||
|
||||
for (const score of timestampedScores) {
|
||||
if (score.timeAchieved! < lastTimestamp + TWO_HOURS) {
|
||||
curGroup.push(score);
|
||||
} else {
|
||||
sessionScoreGroups.push(curGroup);
|
||||
curGroup = [score];
|
||||
}
|
||||
lastTimestamp = score.timeAchieved!;
|
||||
}
|
||||
for (const score of timestampedScores) {
|
||||
if (score.timeAchieved! < lastTimestamp + TWO_HOURS) {
|
||||
curGroup.push(score);
|
||||
} else {
|
||||
sessionScoreGroups.push(curGroup);
|
||||
curGroup = [score];
|
||||
}
|
||||
lastTimestamp = score.timeAchieved!;
|
||||
}
|
||||
|
||||
// There's no state here where curGroup is empty,
|
||||
// so push the group (which is guaranteed to have atleast one score)
|
||||
sessionScoreGroups.push(curGroup);
|
||||
// There's no state here where curGroup is empty,
|
||||
// so push the group (which is guaranteed to have atleast one score)
|
||||
sessionScoreGroups.push(curGroup);
|
||||
|
||||
logger.verbose(`Created ${sessionScoreGroups.length} groups from timestamped scores.`);
|
||||
logger.verbose(`Created ${sessionScoreGroups.length} groups from timestamped scores.`);
|
||||
|
||||
const sessionInfoReturns: SessionInfoReturn[] = [];
|
||||
const sessionInfoReturns: SessionInfoReturn[] = [];
|
||||
|
||||
// All async operations inside here *need* to be done in lockstep to avoid colliding sessions.
|
||||
// realistically, that shouldn't be possible, but hey.
|
||||
/* eslint-disable no-await-in-loop */
|
||||
for (const groupScores of sessionScoreGroups) {
|
||||
if (groupScores.length === 0) {
|
||||
continue;
|
||||
}
|
||||
// All async operations inside here *need* to be done in lockstep to avoid colliding sessions.
|
||||
// realistically, that shouldn't be possible, but hey.
|
||||
/* eslint-disable no-await-in-loop */
|
||||
for (const groupScores of sessionScoreGroups) {
|
||||
if (groupScores.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const startOfGroup = groupScores[0].timeAchieved!;
|
||||
const endOfGroup = groupScores[groupScores.length - 1].timeAchieved!;
|
||||
const startOfGroup = groupScores[0].timeAchieved!;
|
||||
const endOfGroup = groupScores[groupScores.length - 1].timeAchieved!;
|
||||
|
||||
// A bug exists here where if a session is created
|
||||
// backwards in time, this will grab your PBs
|
||||
// from the future.
|
||||
// @todo #179
|
||||
const pbs = await db["personal-bests"].find({
|
||||
chartID: { $in: groupScores.map((e) => e.chartID) },
|
||||
userID,
|
||||
});
|
||||
// A bug exists here where if a session is created
|
||||
// backwards in time, this will grab your PBs
|
||||
// from the future.
|
||||
// @todo #179
|
||||
const pbs = await db["personal-bests"].find({
|
||||
chartID: { $in: groupScores.map((e) => e.chartID) },
|
||||
userID,
|
||||
});
|
||||
|
||||
const pbMap: Map<string, PBScoreDocument> = new Map();
|
||||
for (const pb of pbs) {
|
||||
pbMap.set(pb.chartID, pb);
|
||||
}
|
||||
const pbMap: Map<string, PBScoreDocument> = new Map();
|
||||
for (const pb of pbs) {
|
||||
pbMap.set(pb.chartID, pb);
|
||||
}
|
||||
|
||||
const groupInfo = groupScores.map((e) =>
|
||||
ProcessScoreIntoSessionScoreInfo(e, pbMap.get(e.chartID))
|
||||
);
|
||||
const groupInfo = groupScores.map((e) =>
|
||||
ProcessScoreIntoSessionScoreInfo(e, pbMap.get(e.chartID))
|
||||
);
|
||||
|
||||
// Find any sessions with +/-2hrs of this group. This is rather exhaustive, and could result in some issues
|
||||
// if this query returns more than one session. We should account for that by smushing sessions together.
|
||||
// As of now, we dont currently do it. @TODO #148.
|
||||
const nearbySession = await db.sessions.findOne({
|
||||
userID,
|
||||
game,
|
||||
playtype,
|
||||
importType,
|
||||
$or: [
|
||||
{ timeStarted: { $gte: startOfGroup - TWO_HOURS, $lt: endOfGroup + TWO_HOURS } },
|
||||
{ timeEnded: { $gte: startOfGroup - TWO_HOURS, $lt: endOfGroup + TWO_HOURS } },
|
||||
],
|
||||
});
|
||||
// Find any sessions with +/-2hrs of this group. This is rather exhaustive, and could result in some issues
|
||||
// if this query returns more than one session. We should account for that by smushing sessions together.
|
||||
// As of now, we dont currently do it. @TODO #148.
|
||||
const nearbySession = await db.sessions.findOne({
|
||||
userID,
|
||||
game,
|
||||
playtype,
|
||||
importType,
|
||||
$or: [
|
||||
{ timeStarted: { $gte: startOfGroup - TWO_HOURS, $lt: endOfGroup + TWO_HOURS } },
|
||||
{ timeEnded: { $gte: startOfGroup - TWO_HOURS, $lt: endOfGroup + TWO_HOURS } },
|
||||
],
|
||||
});
|
||||
|
||||
let infoReturn: SessionInfoReturn;
|
||||
let infoReturn: SessionInfoReturn;
|
||||
|
||||
if (nearbySession) {
|
||||
logger.verbose(
|
||||
`Found nearby session for ${userID} (${game} ${playtype}) around ${startOfGroup} ${endOfGroup}.`
|
||||
);
|
||||
if (nearbySession) {
|
||||
logger.verbose(
|
||||
`Found nearby session for ${userID} (${game} ${playtype}) around ${startOfGroup} ${endOfGroup}.`
|
||||
);
|
||||
|
||||
const oldScores = await GetScoresFromSession(nearbySession);
|
||||
const oldScores = await GetScoresFromSession(nearbySession);
|
||||
|
||||
const session = UpdateExistingSession(nearbySession, groupInfo, oldScores, groupScores);
|
||||
const session = UpdateExistingSession(nearbySession, groupInfo, oldScores, groupScores);
|
||||
|
||||
infoReturn = { sessionID: session.sessionID, type: "Appended" };
|
||||
infoReturn = { sessionID: session.sessionID, type: "Appended" };
|
||||
|
||||
await db.sessions.update(
|
||||
{
|
||||
sessionID: session.sessionID,
|
||||
},
|
||||
{
|
||||
$set: session,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
logger.debug(
|
||||
`Creating new session for ${userID} (${game} ${playtype}) around ${startOfGroup} ${endOfGroup}.`
|
||||
);
|
||||
await db.sessions.update(
|
||||
{
|
||||
sessionID: session.sessionID,
|
||||
},
|
||||
{
|
||||
$set: session,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
logger.debug(
|
||||
`Creating new session for ${userID} (${game} ${playtype}) around ${startOfGroup} ${endOfGroup}.`
|
||||
);
|
||||
|
||||
const session = CreateSession(
|
||||
userID,
|
||||
importType,
|
||||
groupInfo,
|
||||
groupScores,
|
||||
game,
|
||||
playtype
|
||||
);
|
||||
const session = CreateSession(
|
||||
userID,
|
||||
importType,
|
||||
groupInfo,
|
||||
groupScores,
|
||||
game,
|
||||
playtype
|
||||
);
|
||||
|
||||
infoReturn = { sessionID: session.sessionID, type: "Created" };
|
||||
await db.sessions.insert(session);
|
||||
}
|
||||
infoReturn = { sessionID: session.sessionID, type: "Created" };
|
||||
await db.sessions.insert(session);
|
||||
}
|
||||
|
||||
sessionInfoReturns.push(infoReturn);
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
sessionInfoReturns.push(infoReturn);
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
|
||||
return sessionInfoReturns;
|
||||
return sessionInfoReturns;
|
||||
}
|
||||
|
||||
+81
-81
@@ -2,106 +2,106 @@ import t from "tap";
|
||||
import { GitadoraColours, SDVXVFClasses } from "../../../constants/classes";
|
||||
import CreateLogCtx from "../../../logger/logger";
|
||||
import {
|
||||
CalculateGitadoraColour,
|
||||
GitadoraSkillToColour,
|
||||
CalculateSDVXClass,
|
||||
SDVXVF6ToClass,
|
||||
CalculateGitadoraColour,
|
||||
GitadoraSkillToColour,
|
||||
CalculateSDVXClass,
|
||||
SDVXVF6ToClass,
|
||||
} from "./builtin-class-handlers";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
t.test("#CalculateGitadoraColour", (t) => {
|
||||
t.strictSame(
|
||||
CalculateGitadoraColour("gitadora", "Gita", 1, { skill: 1500 }, logger),
|
||||
{
|
||||
colour: GitadoraColours.ORANGE_GRADIENT,
|
||||
},
|
||||
"Should wrap the skill colour."
|
||||
);
|
||||
t.strictSame(
|
||||
CalculateGitadoraColour("gitadora", "Gita", 1, { skill: 1500 }, logger),
|
||||
{
|
||||
colour: GitadoraColours.ORANGE_GRADIENT,
|
||||
},
|
||||
"Should wrap the skill colour."
|
||||
);
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#GitadoraSkillToColour", (t) => {
|
||||
t.equal(GitadoraSkillToColour(0), GitadoraColours.WHITE);
|
||||
t.equal(GitadoraSkillToColour(999), GitadoraColours.WHITE);
|
||||
t.equal(GitadoraSkillToColour(1000), GitadoraColours.ORANGE);
|
||||
t.equal(GitadoraSkillToColour(1500), GitadoraColours.ORANGE_GRADIENT);
|
||||
t.equal(GitadoraSkillToColour(2000), GitadoraColours.YELLOW);
|
||||
t.equal(GitadoraSkillToColour(2500), GitadoraColours.YELLOW_GRADIENT);
|
||||
t.equal(GitadoraSkillToColour(3000), GitadoraColours.GREEN);
|
||||
t.equal(GitadoraSkillToColour(3500), GitadoraColours.GREEN_GRADIENT);
|
||||
t.equal(GitadoraSkillToColour(4000), GitadoraColours.BLUE);
|
||||
t.equal(GitadoraSkillToColour(4500), GitadoraColours.BLUE_GRADIENT);
|
||||
t.equal(GitadoraSkillToColour(5000), GitadoraColours.PURPLE);
|
||||
t.equal(GitadoraSkillToColour(5500), GitadoraColours.PURPLE_GRADIENT);
|
||||
t.equal(GitadoraSkillToColour(6000), GitadoraColours.RED);
|
||||
t.equal(GitadoraSkillToColour(6500), GitadoraColours.RED_GRADIENT);
|
||||
t.equal(GitadoraSkillToColour(7000), GitadoraColours.BRONZE);
|
||||
t.equal(GitadoraSkillToColour(7500), GitadoraColours.SILVER);
|
||||
t.equal(GitadoraSkillToColour(8000), GitadoraColours.GOLD);
|
||||
t.equal(GitadoraSkillToColour(8500), GitadoraColours.RAINBOW);
|
||||
t.equal(GitadoraSkillToColour(0), GitadoraColours.WHITE);
|
||||
t.equal(GitadoraSkillToColour(999), GitadoraColours.WHITE);
|
||||
t.equal(GitadoraSkillToColour(1000), GitadoraColours.ORANGE);
|
||||
t.equal(GitadoraSkillToColour(1500), GitadoraColours.ORANGE_GRADIENT);
|
||||
t.equal(GitadoraSkillToColour(2000), GitadoraColours.YELLOW);
|
||||
t.equal(GitadoraSkillToColour(2500), GitadoraColours.YELLOW_GRADIENT);
|
||||
t.equal(GitadoraSkillToColour(3000), GitadoraColours.GREEN);
|
||||
t.equal(GitadoraSkillToColour(3500), GitadoraColours.GREEN_GRADIENT);
|
||||
t.equal(GitadoraSkillToColour(4000), GitadoraColours.BLUE);
|
||||
t.equal(GitadoraSkillToColour(4500), GitadoraColours.BLUE_GRADIENT);
|
||||
t.equal(GitadoraSkillToColour(5000), GitadoraColours.PURPLE);
|
||||
t.equal(GitadoraSkillToColour(5500), GitadoraColours.PURPLE_GRADIENT);
|
||||
t.equal(GitadoraSkillToColour(6000), GitadoraColours.RED);
|
||||
t.equal(GitadoraSkillToColour(6500), GitadoraColours.RED_GRADIENT);
|
||||
t.equal(GitadoraSkillToColour(7000), GitadoraColours.BRONZE);
|
||||
t.equal(GitadoraSkillToColour(7500), GitadoraColours.SILVER);
|
||||
t.equal(GitadoraSkillToColour(8000), GitadoraColours.GOLD);
|
||||
t.equal(GitadoraSkillToColour(8500), GitadoraColours.RAINBOW);
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#CalculateSDVXClass", (t) => {
|
||||
t.strictSame(CalculateSDVXClass("sdvx", "Single", 1, { VF6: 10 }, logger), {
|
||||
vfClass: SDVXVFClasses.COBALT_I,
|
||||
});
|
||||
t.strictSame(CalculateSDVXClass("sdvx", "Single", 1, { VF6: 10 }, logger), {
|
||||
vfClass: SDVXVFClasses.COBALT_I,
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#SDVXVF6ToClass", (t) => {
|
||||
function f(vf: number) {
|
||||
return SDVXVF6ToClass(vf, logger);
|
||||
}
|
||||
function f(vf: number) {
|
||||
return SDVXVF6ToClass(vf, logger);
|
||||
}
|
||||
|
||||
t.equal(f(0), SDVXVFClasses.SIENNA_I);
|
||||
t.equal(f(2.5), SDVXVFClasses.SIENNA_II);
|
||||
t.equal(f(5), SDVXVFClasses.SIENNA_III);
|
||||
t.equal(f(7.5), SDVXVFClasses.SIENNA_IV);
|
||||
t.equal(f(10), SDVXVFClasses.COBALT_I);
|
||||
t.equal(f(10.5), SDVXVFClasses.COBALT_II);
|
||||
t.equal(f(11), SDVXVFClasses.COBALT_III);
|
||||
t.equal(f(11.5), SDVXVFClasses.COBALT_IV);
|
||||
t.equal(f(12), SDVXVFClasses.DANDELION_I);
|
||||
t.equal(f(12.5), SDVXVFClasses.DANDELION_II);
|
||||
t.equal(f(13), SDVXVFClasses.DANDELION_III);
|
||||
t.equal(f(13.5), SDVXVFClasses.DANDELION_IV);
|
||||
t.equal(f(0), SDVXVFClasses.SIENNA_I);
|
||||
t.equal(f(2.5), SDVXVFClasses.SIENNA_II);
|
||||
t.equal(f(5), SDVXVFClasses.SIENNA_III);
|
||||
t.equal(f(7.5), SDVXVFClasses.SIENNA_IV);
|
||||
t.equal(f(10), SDVXVFClasses.COBALT_I);
|
||||
t.equal(f(10.5), SDVXVFClasses.COBALT_II);
|
||||
t.equal(f(11), SDVXVFClasses.COBALT_III);
|
||||
t.equal(f(11.5), SDVXVFClasses.COBALT_IV);
|
||||
t.equal(f(12), SDVXVFClasses.DANDELION_I);
|
||||
t.equal(f(12.5), SDVXVFClasses.DANDELION_II);
|
||||
t.equal(f(13), SDVXVFClasses.DANDELION_III);
|
||||
t.equal(f(13.5), SDVXVFClasses.DANDELION_IV);
|
||||
|
||||
t.equal(f(14), SDVXVFClasses.CYAN_I);
|
||||
t.equal(f(14.25), SDVXVFClasses.CYAN_II);
|
||||
t.equal(f(14.5), SDVXVFClasses.CYAN_III);
|
||||
t.equal(f(14.75), SDVXVFClasses.CYAN_IV);
|
||||
t.equal(f(15), SDVXVFClasses.SCARLET_I);
|
||||
t.equal(f(15.25), SDVXVFClasses.SCARLET_II);
|
||||
t.equal(f(15.5), SDVXVFClasses.SCARLET_III);
|
||||
t.equal(f(15.75), SDVXVFClasses.SCARLET_IV);
|
||||
t.equal(f(16), SDVXVFClasses.CORAL_I);
|
||||
t.equal(f(16.25), SDVXVFClasses.CORAL_II);
|
||||
t.equal(f(16.5), SDVXVFClasses.CORAL_III);
|
||||
t.equal(f(16.75), SDVXVFClasses.CORAL_IV);
|
||||
t.equal(f(17), SDVXVFClasses.ARGENTO_I);
|
||||
t.equal(f(17.25), SDVXVFClasses.ARGENTO_II);
|
||||
t.equal(f(17.5), SDVXVFClasses.ARGENTO_III);
|
||||
t.equal(f(17.75), SDVXVFClasses.ARGENTO_IV);
|
||||
t.equal(f(18), SDVXVFClasses.ELDORA_I);
|
||||
t.equal(f(18.25), SDVXVFClasses.ELDORA_II);
|
||||
t.equal(f(18.5), SDVXVFClasses.ELDORA_III);
|
||||
t.equal(f(18.75), SDVXVFClasses.ELDORA_IV);
|
||||
t.equal(f(19), SDVXVFClasses.CRIMSON_I);
|
||||
t.equal(f(19.25), SDVXVFClasses.CRIMSON_II);
|
||||
t.equal(f(19.5), SDVXVFClasses.CRIMSON_III);
|
||||
t.equal(f(19.75), SDVXVFClasses.CRIMSON_IV);
|
||||
t.equal(f(14), SDVXVFClasses.CYAN_I);
|
||||
t.equal(f(14.25), SDVXVFClasses.CYAN_II);
|
||||
t.equal(f(14.5), SDVXVFClasses.CYAN_III);
|
||||
t.equal(f(14.75), SDVXVFClasses.CYAN_IV);
|
||||
t.equal(f(15), SDVXVFClasses.SCARLET_I);
|
||||
t.equal(f(15.25), SDVXVFClasses.SCARLET_II);
|
||||
t.equal(f(15.5), SDVXVFClasses.SCARLET_III);
|
||||
t.equal(f(15.75), SDVXVFClasses.SCARLET_IV);
|
||||
t.equal(f(16), SDVXVFClasses.CORAL_I);
|
||||
t.equal(f(16.25), SDVXVFClasses.CORAL_II);
|
||||
t.equal(f(16.5), SDVXVFClasses.CORAL_III);
|
||||
t.equal(f(16.75), SDVXVFClasses.CORAL_IV);
|
||||
t.equal(f(17), SDVXVFClasses.ARGENTO_I);
|
||||
t.equal(f(17.25), SDVXVFClasses.ARGENTO_II);
|
||||
t.equal(f(17.5), SDVXVFClasses.ARGENTO_III);
|
||||
t.equal(f(17.75), SDVXVFClasses.ARGENTO_IV);
|
||||
t.equal(f(18), SDVXVFClasses.ELDORA_I);
|
||||
t.equal(f(18.25), SDVXVFClasses.ELDORA_II);
|
||||
t.equal(f(18.5), SDVXVFClasses.ELDORA_III);
|
||||
t.equal(f(18.75), SDVXVFClasses.ELDORA_IV);
|
||||
t.equal(f(19), SDVXVFClasses.CRIMSON_I);
|
||||
t.equal(f(19.25), SDVXVFClasses.CRIMSON_II);
|
||||
t.equal(f(19.5), SDVXVFClasses.CRIMSON_III);
|
||||
t.equal(f(19.75), SDVXVFClasses.CRIMSON_IV);
|
||||
|
||||
t.equal(f(20), SDVXVFClasses.IMPERIAL_I);
|
||||
t.equal(f(21), SDVXVFClasses.IMPERIAL_II);
|
||||
t.equal(f(22), SDVXVFClasses.IMPERIAL_III);
|
||||
t.equal(f(23), SDVXVFClasses.IMPERIAL_IV);
|
||||
t.equal(f(24), SDVXVFClasses.IMPERIAL_IV);
|
||||
t.equal(f(1000), SDVXVFClasses.IMPERIAL_IV);
|
||||
t.equal(f(20), SDVXVFClasses.IMPERIAL_I);
|
||||
t.equal(f(21), SDVXVFClasses.IMPERIAL_II);
|
||||
t.equal(f(22), SDVXVFClasses.IMPERIAL_III);
|
||||
t.equal(f(23), SDVXVFClasses.IMPERIAL_IV);
|
||||
t.equal(f(24), SDVXVFClasses.IMPERIAL_IV);
|
||||
t.equal(f(1000), SDVXVFClasses.IMPERIAL_IV);
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
@@ -4,17 +4,17 @@ import { GitadoraColours, SDVXVFClasses } from "../../../constants/classes";
|
||||
import { KtLogger } from "../../../logger/logger";
|
||||
|
||||
export function CalculateSDVXClass(
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
userID: integer,
|
||||
ratings: Partial<Record<ScoreCalculatedDataLookup["sdvx:Single"], number>>,
|
||||
logger: KtLogger
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
userID: integer,
|
||||
ratings: Partial<Record<ScoreCalculatedDataLookup["sdvx:Single"], number>>,
|
||||
logger: KtLogger
|
||||
): Partial<GameClasses<"sdvx:Single">> {
|
||||
if (!ratings.VF6) {
|
||||
return {};
|
||||
}
|
||||
if (!ratings.VF6) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return { vfClass: SDVXVF6ToClass(ratings.VF6, logger) };
|
||||
return { vfClass: SDVXVF6ToClass(ratings.VF6, logger) };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,83 +24,83 @@ export function CalculateSDVXClass(
|
||||
* @see https://bemaniwiki.com/index.php?SOUND%20VOLTEX%20VIVID%20WAVE/VOLFORCE
|
||||
*/
|
||||
export function SDVXVF6ToClass(vf: number, logger: KtLogger) {
|
||||
// This is impossible, but a failsafe regardless
|
||||
if (vf >= 24) {
|
||||
logger.warn(`User has excessive VF5 of ${vf}. Defaulting to Imperial IV.`);
|
||||
return SDVXVFClasses.IMPERIAL_IV;
|
||||
} else if (vf >= 20) {
|
||||
// imperial i -> iv has gaps of 1
|
||||
return SDVXVFClasses.IMPERIAL_I + Math.floor(vf - 20);
|
||||
} else if (vf >= 14) {
|
||||
// cyan i -> crimson iv has gaps of 0.25
|
||||
return SDVXVFClasses.CYAN_I + Math.floor(4 * (vf - 14));
|
||||
} else if (vf >= 10) {
|
||||
// cobalt i -> dandelion iv have gaps of 0.5
|
||||
return SDVXVFClasses.COBALT_I + Math.floor(2 * (vf - 10));
|
||||
}
|
||||
// This is impossible, but a failsafe regardless
|
||||
if (vf >= 24) {
|
||||
logger.warn(`User has excessive VF5 of ${vf}. Defaulting to Imperial IV.`);
|
||||
return SDVXVFClasses.IMPERIAL_IV;
|
||||
} else if (vf >= 20) {
|
||||
// imperial i -> iv has gaps of 1
|
||||
return SDVXVFClasses.IMPERIAL_I + Math.floor(vf - 20);
|
||||
} else if (vf >= 14) {
|
||||
// cyan i -> crimson iv has gaps of 0.25
|
||||
return SDVXVFClasses.CYAN_I + Math.floor(4 * (vf - 14));
|
||||
} else if (vf >= 10) {
|
||||
// cobalt i -> dandelion iv have gaps of 0.5
|
||||
return SDVXVFClasses.COBALT_I + Math.floor(2 * (vf - 10));
|
||||
}
|
||||
|
||||
return Math.floor(vf / 2.5);
|
||||
return Math.floor(vf / 2.5);
|
||||
}
|
||||
|
||||
export function CalculateGitadoraColour(
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
userID: integer,
|
||||
ratings: Record<string, number>,
|
||||
logger: KtLogger
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
userID: integer,
|
||||
ratings: Record<string, number>,
|
||||
logger: KtLogger
|
||||
): Partial<GameClasses<"gitadora:Dora" | "gitadora:Gita">> {
|
||||
const colour = GitadoraSkillToColour(ratings.skill);
|
||||
const colour = GitadoraSkillToColour(ratings.skill);
|
||||
|
||||
return {
|
||||
colour,
|
||||
};
|
||||
return {
|
||||
colour,
|
||||
};
|
||||
}
|
||||
|
||||
export function GitadoraSkillToColour(sk: number) {
|
||||
if (sk >= 8500) {
|
||||
return GitadoraColours.RAINBOW;
|
||||
} else if (sk >= 8000) {
|
||||
return GitadoraColours.GOLD;
|
||||
} else if (sk >= 7500) {
|
||||
return GitadoraColours.SILVER;
|
||||
} else if (sk >= 7000) {
|
||||
return GitadoraColours.BRONZE;
|
||||
} else if (sk >= 6500) {
|
||||
return GitadoraColours.RED_GRADIENT;
|
||||
} else if (sk >= 6000) {
|
||||
return GitadoraColours.RED;
|
||||
} else if (sk >= 5500) {
|
||||
return GitadoraColours.PURPLE_GRADIENT;
|
||||
} else if (sk >= 5000) {
|
||||
return GitadoraColours.PURPLE;
|
||||
} else if (sk >= 4500) {
|
||||
return GitadoraColours.BLUE_GRADIENT;
|
||||
} else if (sk >= 4000) {
|
||||
return GitadoraColours.BLUE;
|
||||
} else if (sk >= 3500) {
|
||||
return GitadoraColours.GREEN_GRADIENT;
|
||||
} else if (sk >= 3000) {
|
||||
return GitadoraColours.GREEN;
|
||||
} else if (sk >= 2500) {
|
||||
return GitadoraColours.YELLOW_GRADIENT;
|
||||
} else if (sk >= 2000) {
|
||||
return GitadoraColours.YELLOW;
|
||||
} else if (sk >= 1500) {
|
||||
return GitadoraColours.ORANGE_GRADIENT;
|
||||
} else if (sk >= 1000) {
|
||||
return GitadoraColours.ORANGE;
|
||||
}
|
||||
if (sk >= 8500) {
|
||||
return GitadoraColours.RAINBOW;
|
||||
} else if (sk >= 8000) {
|
||||
return GitadoraColours.GOLD;
|
||||
} else if (sk >= 7500) {
|
||||
return GitadoraColours.SILVER;
|
||||
} else if (sk >= 7000) {
|
||||
return GitadoraColours.BRONZE;
|
||||
} else if (sk >= 6500) {
|
||||
return GitadoraColours.RED_GRADIENT;
|
||||
} else if (sk >= 6000) {
|
||||
return GitadoraColours.RED;
|
||||
} else if (sk >= 5500) {
|
||||
return GitadoraColours.PURPLE_GRADIENT;
|
||||
} else if (sk >= 5000) {
|
||||
return GitadoraColours.PURPLE;
|
||||
} else if (sk >= 4500) {
|
||||
return GitadoraColours.BLUE_GRADIENT;
|
||||
} else if (sk >= 4000) {
|
||||
return GitadoraColours.BLUE;
|
||||
} else if (sk >= 3500) {
|
||||
return GitadoraColours.GREEN_GRADIENT;
|
||||
} else if (sk >= 3000) {
|
||||
return GitadoraColours.GREEN;
|
||||
} else if (sk >= 2500) {
|
||||
return GitadoraColours.YELLOW_GRADIENT;
|
||||
} else if (sk >= 2000) {
|
||||
return GitadoraColours.YELLOW;
|
||||
} else if (sk >= 1500) {
|
||||
return GitadoraColours.ORANGE_GRADIENT;
|
||||
} else if (sk >= 1000) {
|
||||
return GitadoraColours.ORANGE;
|
||||
}
|
||||
|
||||
return GitadoraColours.WHITE;
|
||||
return GitadoraColours.WHITE;
|
||||
}
|
||||
|
||||
export function CalculateJubeatColour(
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
userID: integer,
|
||||
ratings: Record<string, number>
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
userID: integer,
|
||||
ratings: Record<string, number>
|
||||
): GameClasses<"jubeat:Single"> {
|
||||
throw new Error("Not implemented.");
|
||||
throw new Error("Not implemented.");
|
||||
}
|
||||
|
||||
// function JubilityToColour(jb: number) {}
|
||||
|
||||
@@ -9,131 +9,131 @@ import { GitadoraColours } from "../../../constants/classes";
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
t.test("#UpdateUGSClasses", (t) => {
|
||||
t.test("Should produce an empty object by default", async (t) => {
|
||||
const res = await UpdateUGSClasses("iidx", "SP", 1, {}, null, logger);
|
||||
t.test("Should produce an empty object by default", async (t) => {
|
||||
const res = await UpdateUGSClasses("iidx", "SP", 1, {}, null, logger);
|
||||
|
||||
t.strictSame(res, {});
|
||||
t.strictSame(res, {});
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should call and merge the ClassHandler", async (t) => {
|
||||
const res = await UpdateUGSClasses("iidx", "SP", 1, {}, () => ({ dan: 2 }), logger);
|
||||
t.test("Should call and merge the ClassHandler", async (t) => {
|
||||
const res = await UpdateUGSClasses("iidx", "SP", 1, {}, () => ({ dan: 2 }), logger);
|
||||
|
||||
t.strictSame(res, { dan: 2 });
|
||||
t.strictSame(res, { dan: 2 });
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should call static handlers if there is one", async (t) => {
|
||||
const res = await UpdateUGSClasses(
|
||||
"gitadora",
|
||||
"Dora",
|
||||
1,
|
||||
{
|
||||
skill: 9000,
|
||||
},
|
||||
null,
|
||||
logger
|
||||
);
|
||||
t.test("Should call static handlers if there is one", async (t) => {
|
||||
const res = await UpdateUGSClasses(
|
||||
"gitadora",
|
||||
"Dora",
|
||||
1,
|
||||
{
|
||||
skill: 9000,
|
||||
},
|
||||
null,
|
||||
logger
|
||||
);
|
||||
|
||||
t.strictSame(res, { colour: GitadoraColours.RAINBOW });
|
||||
t.strictSame(res, { colour: GitadoraColours.RAINBOW });
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#CalculateClassDeltas", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should return improved classes from null", (t) => {
|
||||
const res = CalculateClassDeltas("SP", { dan: 18 }, null, 1, logger);
|
||||
t.test("Should return improved classes from null", (t) => {
|
||||
const res = CalculateClassDeltas("SP", { dan: 18 }, null, 1, logger);
|
||||
|
||||
t.strictSame(res, [
|
||||
{
|
||||
set: "dan",
|
||||
playtype: "SP",
|
||||
old: null,
|
||||
new: 18,
|
||||
},
|
||||
]);
|
||||
t.strictSame(res, [
|
||||
{
|
||||
set: "dan",
|
||||
playtype: "SP",
|
||||
old: null,
|
||||
new: 18,
|
||||
},
|
||||
]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should return improved classes from null class", (t) => {
|
||||
const res = CalculateClassDeltas(
|
||||
"SP",
|
||||
{ dan: 18 },
|
||||
{ classes: {} } as UserGameStats,
|
||||
1,
|
||||
logger
|
||||
);
|
||||
t.test("Should return improved classes from null class", (t) => {
|
||||
const res = CalculateClassDeltas(
|
||||
"SP",
|
||||
{ dan: 18 },
|
||||
{ classes: {} } as UserGameStats,
|
||||
1,
|
||||
logger
|
||||
);
|
||||
|
||||
t.strictSame(res, [
|
||||
{
|
||||
set: "dan",
|
||||
playtype: "SP",
|
||||
old: null,
|
||||
new: 18,
|
||||
},
|
||||
]);
|
||||
t.strictSame(res, [
|
||||
{
|
||||
set: "dan",
|
||||
playtype: "SP",
|
||||
old: null,
|
||||
new: 18,
|
||||
},
|
||||
]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should return improved classes", (t) => {
|
||||
const res = CalculateClassDeltas(
|
||||
"SP",
|
||||
{ dan: 18 },
|
||||
{ classes: { dan: 17 } } as unknown as UserGameStats,
|
||||
1,
|
||||
logger
|
||||
);
|
||||
t.test("Should return improved classes", (t) => {
|
||||
const res = CalculateClassDeltas(
|
||||
"SP",
|
||||
{ dan: 18 },
|
||||
{ classes: { dan: 17 } } as unknown as UserGameStats,
|
||||
1,
|
||||
logger
|
||||
);
|
||||
|
||||
t.strictSame(res, [
|
||||
{
|
||||
set: "dan",
|
||||
playtype: "SP",
|
||||
old: 17,
|
||||
new: 18,
|
||||
},
|
||||
]);
|
||||
t.strictSame(res, [
|
||||
{
|
||||
set: "dan",
|
||||
playtype: "SP",
|
||||
old: 17,
|
||||
new: 18,
|
||||
},
|
||||
]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should not return identical classes", (t) => {
|
||||
const res = CalculateClassDeltas(
|
||||
"SP",
|
||||
{ dan: 18 },
|
||||
{ classes: { dan: 18 } } as unknown as UserGameStats,
|
||||
1,
|
||||
logger
|
||||
);
|
||||
t.test("Should not return identical classes", (t) => {
|
||||
const res = CalculateClassDeltas(
|
||||
"SP",
|
||||
{ dan: 18 },
|
||||
{ classes: { dan: 18 } } as unknown as UserGameStats,
|
||||
1,
|
||||
logger
|
||||
);
|
||||
|
||||
t.strictSame(res, []);
|
||||
t.strictSame(res, []);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should not return worse classes", (t) => {
|
||||
const res = CalculateClassDeltas(
|
||||
"SP",
|
||||
{ dan: 16 },
|
||||
{ classes: { dan: 18 } } as unknown as UserGameStats,
|
||||
1,
|
||||
logger
|
||||
);
|
||||
t.test("Should not return worse classes", (t) => {
|
||||
const res = CalculateClassDeltas(
|
||||
"SP",
|
||||
{ dan: 16 },
|
||||
{ classes: { dan: 18 } } as unknown as UserGameStats,
|
||||
1,
|
||||
logger
|
||||
);
|
||||
|
||||
t.strictSame(res, []);
|
||||
t.strictSame(res, []);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -3,20 +3,20 @@ import { GameClasses } from "tachi-common/js/game-classes";
|
||||
import deepmerge from "deepmerge";
|
||||
import { KtLogger } from "../../../logger/logger";
|
||||
import {
|
||||
CalculateGitadoraColour,
|
||||
CalculateJubeatColour,
|
||||
CalculateSDVXClass,
|
||||
CalculateGitadoraColour,
|
||||
CalculateJubeatColour,
|
||||
CalculateSDVXClass,
|
||||
} from "./builtin-class-handlers";
|
||||
import { ReturnClassIfGreater } from "../../../../utils/class";
|
||||
import { RedisPub } from "../../../../external/redis/redis-IPC";
|
||||
import { ClassHandler, ScoreClasses } from "./types";
|
||||
|
||||
type ClassHandlerMap = {
|
||||
[G in Game]:
|
||||
| {
|
||||
[P in Playtypes[G]]: ClassHandler;
|
||||
}
|
||||
| null;
|
||||
[G in Game]:
|
||||
| {
|
||||
[P in Playtypes[G]]: ClassHandler;
|
||||
}
|
||||
| null;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -29,24 +29,24 @@ type ClassHandlerMap = {
|
||||
* on your profile skill level, which is known at the time this function is called.
|
||||
*/
|
||||
const STATIC_CLASS_HANDLERS: ClassHandlerMap = {
|
||||
iidx: null,
|
||||
bms: null,
|
||||
chunithm: null,
|
||||
ddr: null,
|
||||
gitadora: {
|
||||
Gita: CalculateGitadoraColour,
|
||||
Dora: CalculateGitadoraColour,
|
||||
},
|
||||
jubeat: {
|
||||
Single: CalculateJubeatColour,
|
||||
},
|
||||
maimai: null,
|
||||
museca: null,
|
||||
popn: null,
|
||||
sdvx: {
|
||||
Single: CalculateSDVXClass,
|
||||
},
|
||||
usc: null,
|
||||
iidx: null,
|
||||
bms: null,
|
||||
chunithm: null,
|
||||
ddr: null,
|
||||
gitadora: {
|
||||
Gita: CalculateGitadoraColour,
|
||||
Dora: CalculateGitadoraColour,
|
||||
},
|
||||
jubeat: {
|
||||
Single: CalculateJubeatColour,
|
||||
},
|
||||
maimai: null,
|
||||
museca: null,
|
||||
popn: null,
|
||||
sdvx: {
|
||||
Single: CalculateSDVXClass,
|
||||
},
|
||||
usc: null,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -71,35 +71,35 @@ const STATIC_CLASS_HANDLERS: ClassHandlerMap = {
|
||||
* defaults.
|
||||
*/
|
||||
export async function UpdateUGSClasses(
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
userID: integer,
|
||||
ratings: Record<string, number>,
|
||||
ClassHandler: ClassHandler | null,
|
||||
logger: KtLogger
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
userID: integer,
|
||||
ratings: Record<string, number>,
|
||||
ClassHandler: ClassHandler | null,
|
||||
logger: KtLogger
|
||||
): Promise<ScoreClasses> {
|
||||
let classes: ScoreClasses = {};
|
||||
let classes: ScoreClasses = {};
|
||||
|
||||
// @ts-expect-error This one sucks - I need to look into a better way of representing these types
|
||||
if (STATIC_CLASS_HANDLERS[game] && STATIC_CLASS_HANDLERS[game][playtype]) {
|
||||
// @ts-expect-error see above
|
||||
classes = await STATIC_CLASS_HANDLERS[game][playtype](
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
ratings,
|
||||
logger
|
||||
);
|
||||
}
|
||||
// @ts-expect-error This one sucks - I need to look into a better way of representing these types
|
||||
if (STATIC_CLASS_HANDLERS[game] && STATIC_CLASS_HANDLERS[game][playtype]) {
|
||||
// @ts-expect-error see above
|
||||
classes = await STATIC_CLASS_HANDLERS[game][playtype](
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
ratings,
|
||||
logger
|
||||
);
|
||||
}
|
||||
|
||||
if (ClassHandler) {
|
||||
logger.debug(`Calling custom class handler.`);
|
||||
const customClasses = (await ClassHandler(game, playtype, userID, ratings, logger)) ?? {};
|
||||
if (ClassHandler) {
|
||||
logger.debug(`Calling custom class handler.`);
|
||||
const customClasses = (await ClassHandler(game, playtype, userID, ratings, logger)) ?? {};
|
||||
|
||||
classes = deepmerge(customClasses, classes);
|
||||
}
|
||||
classes = deepmerge(customClasses, classes);
|
||||
}
|
||||
|
||||
return classes;
|
||||
return classes;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,54 +110,54 @@ export async function UpdateUGSClasses(
|
||||
* so that other services can listen for it. In the future we might allow webhooks, too.
|
||||
*/
|
||||
export function CalculateClassDeltas(
|
||||
playtype: Playtypes[Game],
|
||||
classes: ScoreClasses,
|
||||
userGameStats: UserGameStats | null,
|
||||
userID: integer,
|
||||
logger: KtLogger
|
||||
playtype: Playtypes[Game],
|
||||
classes: ScoreClasses,
|
||||
userGameStats: UserGameStats | null,
|
||||
userID: integer,
|
||||
logger: KtLogger
|
||||
): ClassDelta[] {
|
||||
const deltas: ClassDelta[] = [];
|
||||
const deltas: ClassDelta[] = [];
|
||||
|
||||
for (const s in classes) {
|
||||
const classSet = s as keyof GameClasses<IDStrings>;
|
||||
const classVal = classes[classSet];
|
||||
for (const s in classes) {
|
||||
const classSet = s as keyof GameClasses<IDStrings>;
|
||||
const classVal = classes[classSet];
|
||||
|
||||
if (classVal === undefined) {
|
||||
logger.debug(`Skipped deltaing-class ${classSet}.`);
|
||||
continue;
|
||||
}
|
||||
if (classVal === undefined) {
|
||||
logger.debug(`Skipped deltaing-class ${classSet}.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const isGreater = ReturnClassIfGreater(classSet, classVal, userGameStats);
|
||||
try {
|
||||
const isGreater = ReturnClassIfGreater(classSet, classVal, userGameStats);
|
||||
|
||||
if (isGreater === false) {
|
||||
continue;
|
||||
} else {
|
||||
let delta: ClassDelta;
|
||||
if (isGreater === null) {
|
||||
delta = {
|
||||
set: classSet,
|
||||
playtype,
|
||||
old: null,
|
||||
new: classVal,
|
||||
};
|
||||
} else {
|
||||
delta = {
|
||||
set: classSet,
|
||||
playtype,
|
||||
old: userGameStats!.classes[classSet]!,
|
||||
new: classVal,
|
||||
};
|
||||
}
|
||||
if (isGreater === false) {
|
||||
continue;
|
||||
} else {
|
||||
let delta: ClassDelta;
|
||||
if (isGreater === null) {
|
||||
delta = {
|
||||
set: classSet,
|
||||
playtype,
|
||||
old: null,
|
||||
new: classVal,
|
||||
};
|
||||
} else {
|
||||
delta = {
|
||||
set: classSet,
|
||||
playtype,
|
||||
old: userGameStats!.classes[classSet]!,
|
||||
new: classVal,
|
||||
};
|
||||
}
|
||||
|
||||
RedisPub("class-update", { userID, ...delta });
|
||||
RedisPub("class-update", { userID, ...delta });
|
||||
|
||||
deltas.push(delta);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
}
|
||||
deltas.push(delta);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
return deltas;
|
||||
return deltas;
|
||||
}
|
||||
|
||||
@@ -7,77 +7,77 @@ import { CloseAllConnections } from "../../../../test-utils/close-connections";
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
t.test("#CalculateRatings", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should work for games with no custom ratings", async (t) => {
|
||||
const res = await CalculateRatings("popn", "9B", 1, logger);
|
||||
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.strictSame(res, {}, "Should return an empty object.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should return BPI for IIDX", async (t) => {
|
||||
const res = await CalculateRatings("iidx", "SP", 1, logger);
|
||||
t.test("Should return BPI for IIDX", async (t) => {
|
||||
const res = await CalculateRatings("iidx", "SP", 1, logger);
|
||||
|
||||
t.strictSame(
|
||||
res,
|
||||
{ BPI: 0, ktRating: 0, ktLampRating: 0 },
|
||||
"Should return BPI as a custom key."
|
||||
);
|
||||
t.strictSame(
|
||||
res,
|
||||
{ BPI: 0, ktRating: 0, ktLampRating: 0 },
|
||||
"Should return BPI as a custom key."
|
||||
);
|
||||
|
||||
const resDP = await CalculateRatings("iidx", "DP", 1, logger);
|
||||
const resDP = await CalculateRatings("iidx", "DP", 1, logger);
|
||||
|
||||
t.strictSame(
|
||||
resDP,
|
||||
{ BPI: 0, ktRating: 0, ktLampRating: 0 },
|
||||
"Should return BPI as a custom key."
|
||||
);
|
||||
t.strictSame(
|
||||
resDP,
|
||||
{ BPI: 0, ktRating: 0, ktLampRating: 0 },
|
||||
"Should return BPI as a custom key."
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should return VF4 and VF5 for SDVX", async (t) => {
|
||||
const res = await CalculateRatings("sdvx", "Single", 1, logger);
|
||||
t.test("Should return VF4 and VF5 for SDVX", async (t) => {
|
||||
const res = await CalculateRatings("sdvx", "Single", 1, logger);
|
||||
|
||||
t.strictSame(res, { VF6: 0 }, "Should return VF4 and VF5 keys.");
|
||||
t.strictSame(res, { VF6: 0 }, "Should return VF4 and VF5 keys.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should return VF4 and VF5 for USC", async (t) => {
|
||||
const res = await CalculateRatings("usc", "Single", 1, logger);
|
||||
t.test("Should return VF4 and VF5 for USC", async (t) => {
|
||||
const res = await CalculateRatings("usc", "Single", 1, logger);
|
||||
|
||||
t.strictSame(res, { VF6: 0 }, "Should return VF4 and VF5 keys.");
|
||||
t.strictSame(res, { VF6: 0 }, "Should return VF4 and VF5 keys.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should return MFCP for DDR", async (t) => {
|
||||
const res = await CalculateRatings("ddr", "SP", 1, logger);
|
||||
t.test("Should return MFCP for DDR", async (t) => {
|
||||
const res = await CalculateRatings("ddr", "SP", 1, logger);
|
||||
|
||||
t.strictSame(res, { MFCP: 0, ktRating: 0 }, "Should return MFCP keys.");
|
||||
t.strictSame(res, { MFCP: 0, ktRating: 0 }, "Should return MFCP keys.");
|
||||
|
||||
const resDP = await CalculateRatings("ddr", "DP", 1, logger);
|
||||
const resDP = await CalculateRatings("ddr", "DP", 1, logger);
|
||||
|
||||
t.strictSame(resDP, { MFCP: 0, ktRating: 0 }, "Should return MFCP keys.");
|
||||
t.strictSame(resDP, { MFCP: 0, ktRating: 0 }, "Should return MFCP keys.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should return skill for Gitadora", async (t) => {
|
||||
const res = await CalculateRatings("gitadora", "Dora", 1, logger);
|
||||
t.test("Should return skill for Gitadora", async (t) => {
|
||||
const res = await CalculateRatings("gitadora", "Dora", 1, logger);
|
||||
|
||||
t.strictSame(res, { skill: 0 }, "Should return skill keys.");
|
||||
t.strictSame(res, { skill: 0 }, "Should return skill keys.");
|
||||
|
||||
const resDP = await CalculateRatings("gitadora", "Gita", 1, logger);
|
||||
const resDP = await CalculateRatings("gitadora", "Gita", 1, logger);
|
||||
|
||||
t.strictSame(resDP, { skill: 0 }, "Should return skill keys.");
|
||||
t.strictSame(resDP, { skill: 0 }, "Should return skill keys.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {
|
||||
Game,
|
||||
Playtypes,
|
||||
integer,
|
||||
ScoreCalculatedDataLookup,
|
||||
IDStrings,
|
||||
UserGameStats,
|
||||
Game,
|
||||
Playtypes,
|
||||
integer,
|
||||
ScoreCalculatedDataLookup,
|
||||
IDStrings,
|
||||
UserGameStats,
|
||||
} from "tachi-common";
|
||||
import db from "../../../../external/mongo/db";
|
||||
import { KtLogger } from "../../../logger/logger";
|
||||
@@ -12,219 +12,219 @@ import { KtLogger } from "../../../logger/logger";
|
||||
type CustomCalcNames = ScoreCalculatedDataLookup[IDStrings];
|
||||
|
||||
function LazySumAll(key: CustomCalcNames) {
|
||||
return async (game: Game, playtype: Playtypes[Game], userID: integer) => {
|
||||
const sc = await db["personal-bests"].find({
|
||||
game: game,
|
||||
playtype: playtype,
|
||||
userID: userID,
|
||||
isPrimary: true,
|
||||
[`calculatedData.${key}`]: { $gt: 0 },
|
||||
});
|
||||
return async (game: Game, playtype: Playtypes[Game], userID: integer) => {
|
||||
const sc = await db["personal-bests"].find({
|
||||
game: game,
|
||||
playtype: playtype,
|
||||
userID: userID,
|
||||
isPrimary: true,
|
||||
[`calculatedData.${key}`]: { $gt: 0 },
|
||||
});
|
||||
|
||||
const result = sc.reduce((a, b) => a + b.calculatedData[key]!, 0);
|
||||
const result = sc.reduce((a, b) => a + b.calculatedData[key]!, 0);
|
||||
|
||||
return result;
|
||||
};
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
function LazyCalcN(key: CustomCalcNames, n: integer, returnMean?: boolean) {
|
||||
return async (game: Game, playtype: Playtypes[Game], userID: integer) => {
|
||||
const sc = await db["personal-bests"].find(
|
||||
{
|
||||
game: game,
|
||||
playtype: playtype,
|
||||
userID: userID,
|
||||
isPrimary: true,
|
||||
[`calculatedData.${key}`]: { $gt: 0 },
|
||||
},
|
||||
{
|
||||
limit: n,
|
||||
sort: { [`calculatedData.${key}`]: -1 },
|
||||
}
|
||||
);
|
||||
return async (game: Game, playtype: Playtypes[Game], userID: integer) => {
|
||||
const sc = await db["personal-bests"].find(
|
||||
{
|
||||
game: game,
|
||||
playtype: playtype,
|
||||
userID: userID,
|
||||
isPrimary: true,
|
||||
[`calculatedData.${key}`]: { $gt: 0 },
|
||||
},
|
||||
{
|
||||
limit: n,
|
||||
sort: { [`calculatedData.${key}`]: -1 },
|
||||
}
|
||||
);
|
||||
|
||||
let result = sc.reduce((a, b) => a + b.calculatedData[key]!, 0);
|
||||
let result = sc.reduce((a, b) => a + b.calculatedData[key]!, 0);
|
||||
|
||||
if (returnMean) {
|
||||
result = result / n;
|
||||
}
|
||||
if (returnMean) {
|
||||
result = result / n;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
const LazySumN = (key: CustomCalcNames, n: integer) => LazyCalcN(key, n, false);
|
||||
const LazyMeanN = (key: CustomCalcNames, n: integer) => LazyCalcN(key, n, true);
|
||||
|
||||
type RatingFunctions = {
|
||||
[G in Game]: {
|
||||
[P in Playtypes[G]]: (
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
userID: integer,
|
||||
logger: KtLogger
|
||||
) => Promise<UserGameStats["ratings"]>;
|
||||
};
|
||||
[G in Game]: {
|
||||
[P in Playtypes[G]]: (
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
userID: integer,
|
||||
logger: KtLogger
|
||||
) => Promise<UserGameStats["ratings"]>;
|
||||
};
|
||||
};
|
||||
|
||||
const RatingFunctions: RatingFunctions = {
|
||||
iidx: {
|
||||
SP: async (g, p, u) => ({
|
||||
BPI: await LazyMeanN("BPI", 20)(g, p, u),
|
||||
ktRating: await LazyMeanN("ktRating", 20)(g, p, u),
|
||||
ktLampRating: await LazyMeanN("ktLampRating", 20)(g, p, u),
|
||||
}),
|
||||
DP: async (g, p, u) => ({
|
||||
BPI: await LazyMeanN("BPI", 20)(g, p, u),
|
||||
ktRating: await LazyMeanN("ktRating", 20)(g, p, u),
|
||||
ktLampRating: await LazyMeanN("ktLampRating", 20)(g, p, u),
|
||||
}),
|
||||
},
|
||||
sdvx: {
|
||||
Single: async (g, p, u) => ({
|
||||
VF6: await LazySumN("VF6", 50)(g, p, u),
|
||||
}),
|
||||
},
|
||||
usc: {
|
||||
Single: async (g, p, u) => ({
|
||||
VF6: await LazySumN("VF6", 50)(g, p, u),
|
||||
}),
|
||||
},
|
||||
ddr: {
|
||||
SP: async (g, p, u) => ({
|
||||
MFCP: await LazySumAll("MFCP")(g, p, u),
|
||||
ktRating: await LazyMeanN("ktRating", 20)(g, p, u),
|
||||
}),
|
||||
DP: async (g, p, u) => ({
|
||||
MFCP: await LazySumAll("MFCP")(g, p, u),
|
||||
ktRating: await LazyMeanN("ktRating", 20)(g, p, u),
|
||||
}),
|
||||
},
|
||||
gitadora: {
|
||||
Gita: async (g, p, u, l) => ({ skill: await CalculateGitadoraSkill(g, p, u, l) }),
|
||||
Dora: async (g, p, u, l) => ({ skill: await CalculateGitadoraSkill(g, p, u, l) }),
|
||||
},
|
||||
bms: {
|
||||
"7K": async (g, p, u) => ({
|
||||
ktLampRating: await LazyMeanN("ktLampRating", 20)(g, p, u),
|
||||
}),
|
||||
"14K": async (g, p, u) => ({
|
||||
ktLampRating: await LazyMeanN("ktLampRating", 20)(g, p, u),
|
||||
}),
|
||||
},
|
||||
chunithm: {
|
||||
Single: async (g, p, u) => ({
|
||||
naiveRating: await LazyMeanN("rating", 20)(g, p, u),
|
||||
}),
|
||||
},
|
||||
popn: {
|
||||
"9B": async () => ({}),
|
||||
},
|
||||
museca: {
|
||||
Single: async (g, p, u) => ({
|
||||
ktRating: await LazyMeanN("ktRating", 20)(g, p, u),
|
||||
}),
|
||||
},
|
||||
maimai: {
|
||||
Single: async (g, p, u) => ({
|
||||
ktRating: await LazyMeanN("ktRating", 20)(g, p, u),
|
||||
}),
|
||||
},
|
||||
jubeat: {
|
||||
Single: async (g, p, u, l) => ({
|
||||
jubility: await CalculateJubility(g, p, u, l),
|
||||
}),
|
||||
},
|
||||
iidx: {
|
||||
SP: async (g, p, u) => ({
|
||||
BPI: await LazyMeanN("BPI", 20)(g, p, u),
|
||||
ktRating: await LazyMeanN("ktRating", 20)(g, p, u),
|
||||
ktLampRating: await LazyMeanN("ktLampRating", 20)(g, p, u),
|
||||
}),
|
||||
DP: async (g, p, u) => ({
|
||||
BPI: await LazyMeanN("BPI", 20)(g, p, u),
|
||||
ktRating: await LazyMeanN("ktRating", 20)(g, p, u),
|
||||
ktLampRating: await LazyMeanN("ktLampRating", 20)(g, p, u),
|
||||
}),
|
||||
},
|
||||
sdvx: {
|
||||
Single: async (g, p, u) => ({
|
||||
VF6: await LazySumN("VF6", 50)(g, p, u),
|
||||
}),
|
||||
},
|
||||
usc: {
|
||||
Single: async (g, p, u) => ({
|
||||
VF6: await LazySumN("VF6", 50)(g, p, u),
|
||||
}),
|
||||
},
|
||||
ddr: {
|
||||
SP: async (g, p, u) => ({
|
||||
MFCP: await LazySumAll("MFCP")(g, p, u),
|
||||
ktRating: await LazyMeanN("ktRating", 20)(g, p, u),
|
||||
}),
|
||||
DP: async (g, p, u) => ({
|
||||
MFCP: await LazySumAll("MFCP")(g, p, u),
|
||||
ktRating: await LazyMeanN("ktRating", 20)(g, p, u),
|
||||
}),
|
||||
},
|
||||
gitadora: {
|
||||
Gita: async (g, p, u, l) => ({ skill: await CalculateGitadoraSkill(g, p, u, l) }),
|
||||
Dora: async (g, p, u, l) => ({ skill: await CalculateGitadoraSkill(g, p, u, l) }),
|
||||
},
|
||||
bms: {
|
||||
"7K": async (g, p, u) => ({
|
||||
ktLampRating: await LazyMeanN("ktLampRating", 20)(g, p, u),
|
||||
}),
|
||||
"14K": async (g, p, u) => ({
|
||||
ktLampRating: await LazyMeanN("ktLampRating", 20)(g, p, u),
|
||||
}),
|
||||
},
|
||||
chunithm: {
|
||||
Single: async (g, p, u) => ({
|
||||
naiveRating: await LazyMeanN("rating", 20)(g, p, u),
|
||||
}),
|
||||
},
|
||||
popn: {
|
||||
"9B": async () => ({}),
|
||||
},
|
||||
museca: {
|
||||
Single: async (g, p, u) => ({
|
||||
ktRating: await LazyMeanN("ktRating", 20)(g, p, u),
|
||||
}),
|
||||
},
|
||||
maimai: {
|
||||
Single: async (g, p, u) => ({
|
||||
ktRating: await LazyMeanN("ktRating", 20)(g, p, u),
|
||||
}),
|
||||
},
|
||||
jubeat: {
|
||||
Single: async (g, p, u, l) => ({
|
||||
jubility: await CalculateJubility(g, p, u, l),
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export function CalculateRatings(
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
userID: integer,
|
||||
logger: KtLogger
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
userID: integer,
|
||||
logger: KtLogger
|
||||
): Promise<Partial<Record<CustomCalcNames, number>>> {
|
||||
// @ts-expect-error too lazy to type this properly
|
||||
return RatingFunctions[game][playtype](game, playtype, userID, logger);
|
||||
// @ts-expect-error too lazy to type this properly
|
||||
return RatingFunctions[game][playtype](game, playtype, userID, logger);
|
||||
}
|
||||
|
||||
async function CalculateGitadoraSkill(
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
userID: integer,
|
||||
logger: KtLogger
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
userID: integer,
|
||||
logger: KtLogger
|
||||
) {
|
||||
const hotCharts = await db.charts.gitadora.find(
|
||||
{ "flags.HOT N-1": true },
|
||||
{ projection: { chartID: 1 } }
|
||||
);
|
||||
const hotCharts = await db.charts.gitadora.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.skill": -1 },
|
||||
limit: 25,
|
||||
projection: { "calculatedData.skill": 1 },
|
||||
}
|
||||
),
|
||||
// @inefficient
|
||||
// $nin is VERY expensive, there might be a better way to do this.
|
||||
db["personal-bests"].find(
|
||||
{ userID, chartID: { $nin: hotChartIDs } },
|
||||
{
|
||||
sort: { "calculatedData.skill": -1 },
|
||||
limit: 25,
|
||||
projection: { "calculatedData.skill": 1 },
|
||||
}
|
||||
),
|
||||
]);
|
||||
const [bestHotScores, bestScores] = await Promise.all([
|
||||
db["personal-bests"].find(
|
||||
{ userID, chartID: { $in: hotChartIDs } },
|
||||
{
|
||||
sort: { "calculatedData.skill": -1 },
|
||||
limit: 25,
|
||||
projection: { "calculatedData.skill": 1 },
|
||||
}
|
||||
),
|
||||
// @inefficient
|
||||
// $nin is VERY expensive, there might be a better way to do this.
|
||||
db["personal-bests"].find(
|
||||
{ userID, chartID: { $nin: hotChartIDs } },
|
||||
{
|
||||
sort: { "calculatedData.skill": -1 },
|
||||
limit: 25,
|
||||
projection: { "calculatedData.skill": 1 },
|
||||
}
|
||||
),
|
||||
]);
|
||||
|
||||
let skill = 0;
|
||||
skill += bestHotScores.reduce((a, r) => a + r.calculatedData.skill!, 0);
|
||||
skill += bestScores.reduce((a, r) => a + r.calculatedData.skill!, 0);
|
||||
let skill = 0;
|
||||
skill += bestHotScores.reduce((a, r) => a + r.calculatedData.skill!, 0);
|
||||
skill += bestScores.reduce((a, r) => a + r.calculatedData.skill!, 0);
|
||||
|
||||
return skill;
|
||||
return skill;
|
||||
}
|
||||
|
||||
async function CalculateJubility(
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
userID: integer,
|
||||
logger: KtLogger
|
||||
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 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;
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ import { KtLogger } from "../../../logger/logger";
|
||||
export type ScoreClasses = Partial<GameClasses<IDStrings>>;
|
||||
|
||||
export interface ClassHandler {
|
||||
(
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
userID: integer,
|
||||
ratings: Record<string, number>,
|
||||
logger: KtLogger
|
||||
): Promise<ScoreClasses> | ScoreClasses | undefined;
|
||||
(
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
userID: integer,
|
||||
ratings: Record<string, number>,
|
||||
logger: KtLogger
|
||||
): Promise<ScoreClasses> | ScoreClasses | undefined;
|
||||
}
|
||||
|
||||
@@ -12,185 +12,185 @@ const logger = CreateLogCtx(__filename);
|
||||
|
||||
// more of an integration test
|
||||
t.test("#UpdateUsersGamePlaytypeStats", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should create new UserGameStats if the user has none", async (t) => {
|
||||
await db["game-stats"].remove({});
|
||||
t.test("Should create new UserGameStats if the user has none", async (t) => {
|
||||
await db["game-stats"].remove({});
|
||||
|
||||
const res = await UpdateUsersGamePlaytypeStats("iidx", "SP", 1, null, logger);
|
||||
const res = await UpdateUsersGamePlaytypeStats("iidx", "SP", 1, null, logger);
|
||||
|
||||
t.strictSame(res, [], "Should return an empty object");
|
||||
t.strictSame(res, [], "Should return an empty object");
|
||||
|
||||
const gs = await db["game-stats"].findOne();
|
||||
const gs = await db["game-stats"].findOne();
|
||||
|
||||
t.hasStrict(
|
||||
gs,
|
||||
{
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
userID: 1,
|
||||
ratings: { ktRating: 0, ktLampRating: 0 },
|
||||
classes: {},
|
||||
},
|
||||
"Should insert an appropriate game-stats object"
|
||||
);
|
||||
t.hasStrict(
|
||||
gs,
|
||||
{
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
userID: 1,
|
||||
ratings: { ktRating: 0, ktLampRating: 0 },
|
||||
classes: {},
|
||||
},
|
||||
"Should insert an appropriate game-stats object"
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should update UserGameStats if the user has one", async (t) => {
|
||||
await db["game-stats"].remove({});
|
||||
t.test("Should update UserGameStats if the user has one", async (t) => {
|
||||
await db["game-stats"].remove({});
|
||||
|
||||
await db["game-stats"].insert({
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
userID: 1,
|
||||
ratings: { ktRating: 0, ktLampRating: 0 },
|
||||
classes: {},
|
||||
});
|
||||
await db["game-stats"].insert({
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
userID: 1,
|
||||
ratings: { ktRating: 0, ktLampRating: 0 },
|
||||
classes: {},
|
||||
});
|
||||
|
||||
// insert some mock scores
|
||||
const ratings = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
// insert some mock scores
|
||||
const ratings = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
|
||||
await db["personal-bests"].insert(
|
||||
ratings.map((e) =>
|
||||
deepmerge(TestingIIDXSPScorePB, {
|
||||
chartID: crypto.randomBytes(20).toString("hex"),
|
||||
calculatedData: {
|
||||
ktRating: e,
|
||||
ktLampRating: 0,
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
await db["personal-bests"].insert(
|
||||
ratings.map((e) =>
|
||||
deepmerge(TestingIIDXSPScorePB, {
|
||||
chartID: crypto.randomBytes(20).toString("hex"),
|
||||
calculatedData: {
|
||||
ktRating: e,
|
||||
ktLampRating: 0,
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const res = await UpdateUsersGamePlaytypeStats("iidx", "SP", 1, null, logger);
|
||||
const res = await UpdateUsersGamePlaytypeStats("iidx", "SP", 1, null, logger);
|
||||
|
||||
t.strictSame(res, [], "Should return an empty object");
|
||||
t.strictSame(res, [], "Should return an empty object");
|
||||
|
||||
const gs = await db["game-stats"].findOne();
|
||||
const gs = await db["game-stats"].findOne();
|
||||
|
||||
t.hasStrict(
|
||||
gs,
|
||||
{
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
userID: 1,
|
||||
ratings: { ktRating: ratings.reduce((a, r) => a + r, 0) / 20, ktLampRating: 0 },
|
||||
classes: {},
|
||||
},
|
||||
"Should update the game-stats object"
|
||||
);
|
||||
t.hasStrict(
|
||||
gs,
|
||||
{
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
userID: 1,
|
||||
ratings: { ktRating: ratings.reduce((a, r) => a + r, 0) / 20, ktLampRating: 0 },
|
||||
classes: {},
|
||||
},
|
||||
"Should update the game-stats object"
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should return class deltas", async (t) => {
|
||||
await db["game-stats"].remove({});
|
||||
t.test("Should return class deltas", async (t) => {
|
||||
await db["game-stats"].remove({});
|
||||
|
||||
await db["game-stats"].insert({
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
userID: 1,
|
||||
ratings: { ktRating: 0, ktLampRating: 0 },
|
||||
await db["game-stats"].insert({
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
userID: 1,
|
||||
ratings: { ktRating: 0, ktLampRating: 0 },
|
||||
|
||||
classes: {},
|
||||
});
|
||||
classes: {},
|
||||
});
|
||||
|
||||
const res = await UpdateUsersGamePlaytypeStats(
|
||||
"iidx",
|
||||
"SP",
|
||||
1,
|
||||
() => ({ dan: 18 }), // lmao
|
||||
logger
|
||||
);
|
||||
const res = await UpdateUsersGamePlaytypeStats(
|
||||
"iidx",
|
||||
"SP",
|
||||
1,
|
||||
() => ({ dan: 18 }), // lmao
|
||||
logger
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
res,
|
||||
[
|
||||
{
|
||||
set: "dan",
|
||||
playtype: "SP",
|
||||
old: null,
|
||||
new: 18,
|
||||
},
|
||||
],
|
||||
"Should return the class delta"
|
||||
);
|
||||
t.strictSame(
|
||||
res,
|
||||
[
|
||||
{
|
||||
set: "dan",
|
||||
playtype: "SP",
|
||||
old: null,
|
||||
new: 18,
|
||||
},
|
||||
],
|
||||
"Should return the class delta"
|
||||
);
|
||||
|
||||
const gs = await db["game-stats"].findOne();
|
||||
const gs = await db["game-stats"].findOne();
|
||||
|
||||
t.hasStrict(
|
||||
gs,
|
||||
{
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
userID: 1,
|
||||
ratings: { ktRating: 0, ktLampRating: 0 },
|
||||
classes: {
|
||||
dan: 18,
|
||||
},
|
||||
},
|
||||
"Should update the game-stats object"
|
||||
);
|
||||
t.hasStrict(
|
||||
gs,
|
||||
{
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
userID: 1,
|
||||
ratings: { ktRating: 0, ktLampRating: 0 },
|
||||
classes: {
|
||||
dan: 18,
|
||||
},
|
||||
},
|
||||
"Should update the game-stats object"
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should return updated class deltas", async (t) => {
|
||||
await db["game-stats"].remove({});
|
||||
t.test("Should return updated class deltas", async (t) => {
|
||||
await db["game-stats"].remove({});
|
||||
|
||||
await db["game-stats"].insert({
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
userID: 1,
|
||||
ratings: { ktRating: 0, ktLampRating: 0 },
|
||||
classes: {
|
||||
dan: 17,
|
||||
},
|
||||
});
|
||||
await db["game-stats"].insert({
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
userID: 1,
|
||||
ratings: { ktRating: 0, ktLampRating: 0 },
|
||||
classes: {
|
||||
dan: 17,
|
||||
},
|
||||
});
|
||||
|
||||
const res = await UpdateUsersGamePlaytypeStats(
|
||||
"iidx",
|
||||
"SP",
|
||||
1,
|
||||
() => ({ dan: 18 }), // lmao
|
||||
logger
|
||||
);
|
||||
const res = await UpdateUsersGamePlaytypeStats(
|
||||
"iidx",
|
||||
"SP",
|
||||
1,
|
||||
() => ({ dan: 18 }), // lmao
|
||||
logger
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
res,
|
||||
[
|
||||
{
|
||||
set: "dan",
|
||||
playtype: "SP",
|
||||
old: 17,
|
||||
new: 18,
|
||||
},
|
||||
],
|
||||
"Should return the updated class delta"
|
||||
);
|
||||
t.strictSame(
|
||||
res,
|
||||
[
|
||||
{
|
||||
set: "dan",
|
||||
playtype: "SP",
|
||||
old: 17,
|
||||
new: 18,
|
||||
},
|
||||
],
|
||||
"Should return the updated class delta"
|
||||
);
|
||||
|
||||
const gs = await db["game-stats"].findOne();
|
||||
const gs = await db["game-stats"].findOne();
|
||||
|
||||
t.hasStrict(
|
||||
gs,
|
||||
{
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
userID: 1,
|
||||
ratings: { ktRating: 0, ktLampRating: 0 },
|
||||
classes: {
|
||||
dan: 18,
|
||||
},
|
||||
},
|
||||
"Should update the game-stats object"
|
||||
);
|
||||
t.hasStrict(
|
||||
gs,
|
||||
{
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
userID: 1,
|
||||
ratings: { ktRating: 0, ktLampRating: 0 },
|
||||
classes: {
|
||||
dan: 18,
|
||||
},
|
||||
},
|
||||
"Should update the game-stats object"
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -10,61 +10,61 @@ import { CalculateRatings } from "./rating";
|
||||
import { ClassHandler } from "./types";
|
||||
|
||||
export async function UpdateUsersGamePlaytypeStats(
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
userID: integer,
|
||||
classHandler: ClassHandler | null,
|
||||
logger: KtLogger
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
userID: integer,
|
||||
classHandler: ClassHandler | null,
|
||||
logger: KtLogger
|
||||
): Promise<ClassDelta[]> {
|
||||
const ratings = await CalculateRatings(game, playtype, userID, logger);
|
||||
const ratings = await CalculateRatings(game, playtype, userID, logger);
|
||||
|
||||
// Attempt to find a users game stats if one already exists. If one doesn't exist,
|
||||
// this is this players first import for this game!
|
||||
const userGameStats = await db["game-stats"].findOne({
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
});
|
||||
// Attempt to find a users game stats if one already exists. If one doesn't exist,
|
||||
// this is this players first import for this game!
|
||||
const userGameStats = await db["game-stats"].findOne({
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
});
|
||||
|
||||
logger.debug(`Calculating UGSClasses...`);
|
||||
logger.debug(`Calculating UGSClasses...`);
|
||||
|
||||
const classes = await UpdateUGSClasses(game, playtype, userID, ratings, classHandler, logger);
|
||||
const classes = await UpdateUGSClasses(game, playtype, userID, ratings, classHandler, logger);
|
||||
|
||||
logger.debug(`Finished Calculating UGSClasses`);
|
||||
logger.debug(`Finished Calculating UGSClasses`);
|
||||
|
||||
logger.debug(`Calculating Class Deltas...`);
|
||||
logger.debug(`Calculating Class Deltas...`);
|
||||
|
||||
const deltas = CalculateClassDeltas(playtype, classes, userGameStats, userID, logger);
|
||||
const deltas = CalculateClassDeltas(playtype, classes, userGameStats, userID, logger);
|
||||
|
||||
logger.debug(`Had ${deltas.length} deltas.`);
|
||||
logger.debug(`Had ${deltas.length} deltas.`);
|
||||
|
||||
if (userGameStats) {
|
||||
logger.debug(`Updated player gamestats for ${game} (${playtype})`);
|
||||
await db["game-stats"].update(
|
||||
{
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
ratings,
|
||||
classes,
|
||||
},
|
||||
}
|
||||
);
|
||||
} else {
|
||||
const newStats: UserGameStats = {
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
ratings,
|
||||
classes,
|
||||
};
|
||||
if (userGameStats) {
|
||||
logger.debug(`Updated player gamestats for ${game} (${playtype})`);
|
||||
await db["game-stats"].update(
|
||||
{
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
ratings,
|
||||
classes,
|
||||
},
|
||||
}
|
||||
);
|
||||
} else {
|
||||
const newStats: UserGameStats = {
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
ratings,
|
||||
classes,
|
||||
};
|
||||
|
||||
logger.info(`Created new gamestats for ${game} (${playtype})`);
|
||||
await db["game-stats"].insert(newStats);
|
||||
}
|
||||
logger.info(`Created new gamestats for ${game} (${playtype})`);
|
||||
await db["game-stats"].insert(newStats);
|
||||
}
|
||||
|
||||
return deltas;
|
||||
return deltas;
|
||||
}
|
||||
|
||||
@@ -14,96 +14,96 @@ const putySong = GetKTDataJSON("./tachi/ddr-puty-song.json") as ARCDDRScore;
|
||||
const putyChart = GetKTDataJSON("./tachi/ddr-puty-chart.json") as ARCDDRScore;
|
||||
|
||||
t.test("#ConvertAPIArcDDR", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should convert a valid score", async (t) => {
|
||||
const res = await ConvertAPIArcDDR(arcScore, {}, "api/arc-ddr", logger);
|
||||
t.test("Should convert a valid score", async (t) => {
|
||||
const res = await ConvertAPIArcDDR(arcScore, {}, "api/arc-ddr", logger);
|
||||
|
||||
t.hasStrict(res, {
|
||||
song: putySong,
|
||||
chart: putyChart,
|
||||
dryScore: {
|
||||
comment: null,
|
||||
game: "ddr",
|
||||
importType: "api/arc-ddr",
|
||||
timeAchieved: 1591822550346,
|
||||
service: "ARC DDR Ace",
|
||||
scoreData: {
|
||||
grade: "AAA",
|
||||
percent: 99.976,
|
||||
score: 807,
|
||||
hitData: {
|
||||
marvelous: 236,
|
||||
perfect: 24,
|
||||
great: 0,
|
||||
good: 0,
|
||||
boo: 0,
|
||||
miss: 0,
|
||||
ok: 17,
|
||||
ng: 0,
|
||||
},
|
||||
hitMeta: {
|
||||
fast: 10,
|
||||
slow: 14,
|
||||
maxCombo: 260,
|
||||
exScore: 807,
|
||||
},
|
||||
lamp: "PERFECT FULL COMBO",
|
||||
},
|
||||
scoreMeta: {},
|
||||
},
|
||||
});
|
||||
t.hasStrict(res, {
|
||||
song: putySong,
|
||||
chart: putyChart,
|
||||
dryScore: {
|
||||
comment: null,
|
||||
game: "ddr",
|
||||
importType: "api/arc-ddr",
|
||||
timeAchieved: 1591822550346,
|
||||
service: "ARC DDR Ace",
|
||||
scoreData: {
|
||||
grade: "AAA",
|
||||
percent: 99.976,
|
||||
score: 807,
|
||||
hitData: {
|
||||
marvelous: 236,
|
||||
perfect: 24,
|
||||
great: 0,
|
||||
good: 0,
|
||||
boo: 0,
|
||||
miss: 0,
|
||||
ok: 17,
|
||||
ng: 0,
|
||||
},
|
||||
hitMeta: {
|
||||
fast: 10,
|
||||
slow: 14,
|
||||
maxCombo: 260,
|
||||
exScore: 807,
|
||||
},
|
||||
lamp: "PERFECT FULL COMBO",
|
||||
},
|
||||
scoreMeta: {},
|
||||
},
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should throw on an invalid score", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ConvertAPIArcDDR(
|
||||
deepmerge(arcScore, { ex_score: "foo" }),
|
||||
{},
|
||||
"api/arc-ddr",
|
||||
logger
|
||||
),
|
||||
{
|
||||
message: /Invalid ARC Score:/iu,
|
||||
} as any
|
||||
);
|
||||
t.test("Should throw on an invalid score", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ConvertAPIArcDDR(
|
||||
deepmerge(arcScore, { ex_score: "foo" }),
|
||||
{},
|
||||
"api/arc-ddr",
|
||||
logger
|
||||
),
|
||||
{
|
||||
message: /Invalid ARC Score:/iu,
|
||||
} as any
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should throw on no chart", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ConvertAPIArcDDR(
|
||||
deepmerge(arcScore, { chart_id: "foo" }),
|
||||
{},
|
||||
"api/arc-ddr",
|
||||
logger
|
||||
),
|
||||
{
|
||||
message: /Could not find chart/iu,
|
||||
} as any
|
||||
);
|
||||
t.test("Should throw on no chart", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ConvertAPIArcDDR(
|
||||
deepmerge(arcScore, { chart_id: "foo" }),
|
||||
{},
|
||||
"api/arc-ddr",
|
||||
logger
|
||||
),
|
||||
{
|
||||
message: /Could not find chart/iu,
|
||||
} as any
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#ResolveARCDDRLamp", (t) => {
|
||||
t.equal(ResolveARCDDRLamp("FAIL"), "FAILED");
|
||||
t.equal(ResolveARCDDRLamp("CLEAR"), "CLEAR");
|
||||
t.equal(ResolveARCDDRLamp("CLEAR_3LIFE"), "LIFE4");
|
||||
t.equal(ResolveARCDDRLamp("GOOD_FC"), "FULL COMBO");
|
||||
t.equal(ResolveARCDDRLamp("GREAT_FC"), "GREAT FULL COMBO");
|
||||
t.equal(ResolveARCDDRLamp("PERFECT_FC"), "PERFECT FULL COMBO");
|
||||
t.equal(ResolveARCDDRLamp("MARVELOUS_FC"), "MARVELOUS FULL COMBO");
|
||||
t.equal(ResolveARCDDRLamp("FAIL"), "FAILED");
|
||||
t.equal(ResolveARCDDRLamp("CLEAR"), "CLEAR");
|
||||
t.equal(ResolveARCDDRLamp("CLEAR_3LIFE"), "LIFE4");
|
||||
t.equal(ResolveARCDDRLamp("GOOD_FC"), "FULL COMBO");
|
||||
t.equal(ResolveARCDDRLamp("GREAT_FC"), "GREAT FULL COMBO");
|
||||
t.equal(ResolveARCDDRLamp("PERFECT_FC"), "PERFECT FULL COMBO");
|
||||
t.equal(ResolveARCDDRLamp("MARVELOUS_FC"), "MARVELOUS FULL COMBO");
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -2,8 +2,8 @@ import { EmptyObject } from "../../../../../utils/types";
|
||||
import { ConverterFunction } from "../../common/types";
|
||||
import p, { PrudenceSchema } from "prudence";
|
||||
import {
|
||||
InvalidScoreFailure,
|
||||
KTDataNotFoundFailure,
|
||||
InvalidScoreFailure,
|
||||
KTDataNotFoundFailure,
|
||||
} from "../../../framework/common/converter-failures";
|
||||
import { FormatPrError } from "../../../../../utils/prudence";
|
||||
import { ARCDDRScore } from "./types";
|
||||
@@ -11,128 +11,128 @@ import { FindChartOnARCID } from "../../../../../utils/queries/charts";
|
||||
import { FindSongOnIDGuaranteed } from "../../../../../utils/queries/songs";
|
||||
import { DryScore } from "../../../framework/common/types";
|
||||
import {
|
||||
GenericGetGradeAndPercent,
|
||||
ParseDateFromString,
|
||||
GenericGetGradeAndPercent,
|
||||
ParseDateFromString,
|
||||
} from "../../../framework/common/score-utils";
|
||||
import { Lamps } from "tachi-common";
|
||||
|
||||
// There's a bunch of other useless fields but we don't care
|
||||
const PR_ArcDDRScore: PrudenceSchema = {
|
||||
chart_id: "string",
|
||||
lamp: p.isIn(
|
||||
"MARVELOUS_FC",
|
||||
"PERFECT_FC",
|
||||
"GREAT_FC",
|
||||
"GOOD_FC",
|
||||
"CLEAR_3LIFE",
|
||||
"CLEAR",
|
||||
"FAIL"
|
||||
),
|
||||
score: p.isBoundedInteger(0, 1_000_000),
|
||||
ex_score: p.isPositiveInteger,
|
||||
max_combo: p.isPositiveInteger,
|
||||
judgments: {
|
||||
marvelous: p.isPositiveInteger,
|
||||
perfect: p.isPositiveInteger,
|
||||
great: p.isPositiveInteger,
|
||||
good: p.isPositiveInteger,
|
||||
boo: p.isPositiveInteger,
|
||||
miss: p.isPositiveInteger,
|
||||
ok: p.isPositiveInteger,
|
||||
ng: p.isPositiveInteger,
|
||||
},
|
||||
fast: p.isPositiveInteger,
|
||||
slow: p.isPositiveInteger,
|
||||
timestamp: "string",
|
||||
chart_id: "string",
|
||||
lamp: p.isIn(
|
||||
"MARVELOUS_FC",
|
||||
"PERFECT_FC",
|
||||
"GREAT_FC",
|
||||
"GOOD_FC",
|
||||
"CLEAR_3LIFE",
|
||||
"CLEAR",
|
||||
"FAIL"
|
||||
),
|
||||
score: p.isBoundedInteger(0, 1_000_000),
|
||||
ex_score: p.isPositiveInteger,
|
||||
max_combo: p.isPositiveInteger,
|
||||
judgments: {
|
||||
marvelous: p.isPositiveInteger,
|
||||
perfect: p.isPositiveInteger,
|
||||
great: p.isPositiveInteger,
|
||||
good: p.isPositiveInteger,
|
||||
boo: p.isPositiveInteger,
|
||||
miss: p.isPositiveInteger,
|
||||
ok: p.isPositiveInteger,
|
||||
ng: p.isPositiveInteger,
|
||||
},
|
||||
fast: p.isPositiveInteger,
|
||||
slow: p.isPositiveInteger,
|
||||
timestamp: "string",
|
||||
};
|
||||
|
||||
export const ConvertAPIArcDDR: ConverterFunction<unknown, EmptyObject> = async (
|
||||
data,
|
||||
context,
|
||||
importType,
|
||||
logger
|
||||
data,
|
||||
context,
|
||||
importType,
|
||||
logger
|
||||
) => {
|
||||
const err = p(data, PR_ArcDDRScore, {}, { throwOnNonObject: false, allowExcessKeys: true });
|
||||
const err = p(data, PR_ArcDDRScore, {}, { throwOnNonObject: false, allowExcessKeys: true });
|
||||
|
||||
if (err) {
|
||||
throw new InvalidScoreFailure(FormatPrError(err, "Invalid ARC Score: "));
|
||||
}
|
||||
if (err) {
|
||||
throw new InvalidScoreFailure(FormatPrError(err, "Invalid ARC Score: "));
|
||||
}
|
||||
|
||||
// confirmed by Prudence above.
|
||||
const score = data as ARCDDRScore;
|
||||
// confirmed by Prudence above.
|
||||
const score = data as ARCDDRScore;
|
||||
|
||||
const chart = await FindChartOnARCID("ddr", score.chart_id);
|
||||
const chart = await FindChartOnARCID("ddr", score.chart_id);
|
||||
|
||||
if (!chart) {
|
||||
throw new KTDataNotFoundFailure(
|
||||
`Could not find chart with chart_id ${score.chart_id}.`,
|
||||
importType,
|
||||
data,
|
||||
context
|
||||
);
|
||||
}
|
||||
if (!chart) {
|
||||
throw new KTDataNotFoundFailure(
|
||||
`Could not find chart with chart_id ${score.chart_id}.`,
|
||||
importType,
|
||||
data,
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
const song = await FindSongOnIDGuaranteed("ddr", chart.songID, logger);
|
||||
const song = await FindSongOnIDGuaranteed("ddr", chart.songID, logger);
|
||||
|
||||
const { grade, percent } = GenericGetGradeAndPercent("ddr", score.score, chart);
|
||||
const { grade, percent } = GenericGetGradeAndPercent("ddr", score.score, chart);
|
||||
|
||||
const timeAchieved = ParseDateFromString(score.timestamp);
|
||||
const timeAchieved = ParseDateFromString(score.timestamp);
|
||||
|
||||
const lamp = ResolveARCDDRLamp(score.lamp);
|
||||
const lamp = ResolveARCDDRLamp(score.lamp);
|
||||
|
||||
const dryScore: DryScore<"ddr:SP" | "ddr:DP"> = {
|
||||
comment: null,
|
||||
game: "ddr",
|
||||
importType,
|
||||
timeAchieved,
|
||||
service: "ARC DDR Ace",
|
||||
scoreData: {
|
||||
grade,
|
||||
percent,
|
||||
score: score.ex_score,
|
||||
hitData: {
|
||||
marvelous: score.judgments.marvelous,
|
||||
perfect: score.judgments.perfect,
|
||||
great: score.judgments.great,
|
||||
good: score.judgments.good,
|
||||
boo: score.judgments.boo,
|
||||
miss: score.judgments.miss,
|
||||
ok: score.judgments.ok,
|
||||
ng: score.judgments.ng,
|
||||
},
|
||||
hitMeta: {
|
||||
fast: score.fast,
|
||||
slow: score.slow,
|
||||
maxCombo: score.max_combo,
|
||||
exScore: score.ex_score,
|
||||
},
|
||||
lamp,
|
||||
},
|
||||
scoreMeta: {},
|
||||
};
|
||||
const dryScore: DryScore<"ddr:SP" | "ddr:DP"> = {
|
||||
comment: null,
|
||||
game: "ddr",
|
||||
importType,
|
||||
timeAchieved,
|
||||
service: "ARC DDR Ace",
|
||||
scoreData: {
|
||||
grade,
|
||||
percent,
|
||||
score: score.ex_score,
|
||||
hitData: {
|
||||
marvelous: score.judgments.marvelous,
|
||||
perfect: score.judgments.perfect,
|
||||
great: score.judgments.great,
|
||||
good: score.judgments.good,
|
||||
boo: score.judgments.boo,
|
||||
miss: score.judgments.miss,
|
||||
ok: score.judgments.ok,
|
||||
ng: score.judgments.ng,
|
||||
},
|
||||
hitMeta: {
|
||||
fast: score.fast,
|
||||
slow: score.slow,
|
||||
maxCombo: score.max_combo,
|
||||
exScore: score.ex_score,
|
||||
},
|
||||
lamp,
|
||||
},
|
||||
scoreMeta: {},
|
||||
};
|
||||
|
||||
return { song, chart, dryScore };
|
||||
return { song, chart, dryScore };
|
||||
};
|
||||
|
||||
export function ResolveARCDDRLamp(lamp: ARCDDRScore["lamp"]): Lamps["ddr:SP" | "ddr:DP"] {
|
||||
switch (lamp) {
|
||||
case "FAIL":
|
||||
return "FAILED";
|
||||
case "CLEAR":
|
||||
return "CLEAR";
|
||||
case "CLEAR_3LIFE":
|
||||
return "LIFE4";
|
||||
case "GOOD_FC":
|
||||
return "FULL COMBO";
|
||||
case "GREAT_FC":
|
||||
return "GREAT FULL COMBO";
|
||||
case "PERFECT_FC":
|
||||
return "PERFECT FULL COMBO";
|
||||
case "MARVELOUS_FC":
|
||||
return "MARVELOUS FULL COMBO";
|
||||
}
|
||||
switch (lamp) {
|
||||
case "FAIL":
|
||||
return "FAILED";
|
||||
case "CLEAR":
|
||||
return "CLEAR";
|
||||
case "CLEAR_3LIFE":
|
||||
return "LIFE4";
|
||||
case "GOOD_FC":
|
||||
return "FULL COMBO";
|
||||
case "GREAT_FC":
|
||||
return "GREAT FULL COMBO";
|
||||
case "PERFECT_FC":
|
||||
return "PERFECT FULL COMBO";
|
||||
case "MARVELOUS_FC":
|
||||
return "MARVELOUS FULL COMBO";
|
||||
}
|
||||
|
||||
// failsafe
|
||||
/* istanbul ignore next */
|
||||
throw new InvalidScoreFailure(`Invalid lamp ${lamp} - Could not resolve.`);
|
||||
// failsafe
|
||||
/* istanbul ignore next */
|
||||
throw new InvalidScoreFailure(`Invalid lamp ${lamp} - Could not resolve.`);
|
||||
}
|
||||
|
||||
@@ -9,38 +9,38 @@ import { ParseArcDDR } from "./parser";
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
t.test("#ParseArcDDR", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should iterate over the API.", async (t) => {
|
||||
const mockArcAPI = MockJSONFetch({
|
||||
"https://arc.example.com/api/v1/ddr/16/player_bests?profile_id=profile": {
|
||||
_links: {
|
||||
_next: "https://arc.example.com/api/v1/ddr/16/player_bests?profile_id=profile&page=2",
|
||||
},
|
||||
_items: [1, 2, 3],
|
||||
},
|
||||
"https://arc.example.com/api/v1/ddr/16/player_bests?profile_id=profile&page=2": {
|
||||
_links: {
|
||||
_next: null,
|
||||
},
|
||||
_items: [4, 5, 6],
|
||||
},
|
||||
});
|
||||
t.test("Should iterate over the API.", async (t) => {
|
||||
const mockArcAPI = MockJSONFetch({
|
||||
"https://arc.example.com/api/v1/ddr/16/player_bests?profile_id=profile": {
|
||||
_links: {
|
||||
_next: "https://arc.example.com/api/v1/ddr/16/player_bests?profile_id=profile&page=2",
|
||||
},
|
||||
_items: [1, 2, 3],
|
||||
},
|
||||
"https://arc.example.com/api/v1/ddr/16/player_bests?profile_id=profile&page=2": {
|
||||
_links: {
|
||||
_next: null,
|
||||
},
|
||||
_items: [4, 5, 6],
|
||||
},
|
||||
});
|
||||
|
||||
const res = ParseArcDDR("profile", logger, mockArcAPI);
|
||||
const res = ParseArcDDR("profile", logger, mockArcAPI);
|
||||
|
||||
t.equal(res.game, "ddr");
|
||||
t.strictSame(res.context, {});
|
||||
t.equal(res.classHandler, null);
|
||||
t.equal(res.game, "ddr");
|
||||
t.strictSame(res.context, {});
|
||||
t.equal(res.classHandler, null);
|
||||
|
||||
const iter = await agta(res.iterable);
|
||||
const iter = await agta(res.iterable);
|
||||
|
||||
t.strictSame(iter, [1, 2, 3, 4, 5, 6]);
|
||||
t.strictSame(iter, [1, 2, 3, 4, 5, 6]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -6,21 +6,21 @@ import { EmptyObject } from "../../../../../utils/types";
|
||||
import { ARC_API_URL, ARC_AUTH_TOKEN } from "../../../../setup/config";
|
||||
|
||||
export function ParseArcDDR(
|
||||
arcProfileID: string,
|
||||
logger: KtLogger,
|
||||
fetch = nodeFetch
|
||||
arcProfileID: string,
|
||||
logger: KtLogger,
|
||||
fetch = nodeFetch
|
||||
): ParserFunctionReturns<unknown, EmptyObject> {
|
||||
return {
|
||||
iterable: TraverseKaiAPI(
|
||||
ARC_API_URL,
|
||||
// DDR Ace.
|
||||
`/api/v1/ddr/16/player_bests?profile_id=${arcProfileID}`,
|
||||
ARC_AUTH_TOKEN,
|
||||
logger,
|
||||
fetch
|
||||
),
|
||||
context: {},
|
||||
classHandler: null,
|
||||
game: "ddr",
|
||||
};
|
||||
return {
|
||||
iterable: TraverseKaiAPI(
|
||||
ARC_API_URL,
|
||||
// DDR Ace.
|
||||
`/api/v1/ddr/16/player_bests?profile_id=${arcProfileID}`,
|
||||
ARC_AUTH_TOKEN,
|
||||
logger,
|
||||
fetch
|
||||
),
|
||||
context: {},
|
||||
classHandler: null,
|
||||
game: "ddr",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { integer } from "tachi-common";
|
||||
|
||||
export interface ARCDDRScore {
|
||||
chart_id: string;
|
||||
lamp: "MARVELOUS_FC" | "PERFECT_FC" | "GREAT_FC" | "GOOD_FC" | "CLEAR_3LIFE" | "CLEAR" | "FAIL";
|
||||
score: integer;
|
||||
ex_score: integer;
|
||||
max_combo: integer;
|
||||
judgments: {
|
||||
marvelous: integer;
|
||||
perfect: integer;
|
||||
great: integer;
|
||||
good: integer;
|
||||
boo: integer;
|
||||
miss: integer;
|
||||
ok: integer;
|
||||
ng: integer;
|
||||
};
|
||||
fast: integer;
|
||||
slow: integer;
|
||||
timestamp: string;
|
||||
chart_id: string;
|
||||
lamp: "MARVELOUS_FC" | "PERFECT_FC" | "GREAT_FC" | "GOOD_FC" | "CLEAR_3LIFE" | "CLEAR" | "FAIL";
|
||||
score: integer;
|
||||
ex_score: integer;
|
||||
max_combo: integer;
|
||||
judgments: {
|
||||
marvelous: integer;
|
||||
perfect: integer;
|
||||
great: integer;
|
||||
good: integer;
|
||||
boo: integer;
|
||||
miss: integer;
|
||||
ok: integer;
|
||||
ng: integer;
|
||||
};
|
||||
fast: integer;
|
||||
slow: integer;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
@@ -9,139 +9,139 @@ import { CreateArcIIDXClassHandler } from "./class-handler";
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
t.test("#CreateArcIIDXClassHandler", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should return a working ClassHandler.", async (t) => {
|
||||
const mockFetch = MockJSONFetch({
|
||||
"https://arc.example.com/api/v1/iidx/27/profiles?_id=profile": {
|
||||
_items: [
|
||||
{
|
||||
sp: {
|
||||
rank: "中伝",
|
||||
},
|
||||
dp: {
|
||||
rank: "ニ段",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
t.test("Should return a working ClassHandler.", async (t) => {
|
||||
const mockFetch = MockJSONFetch({
|
||||
"https://arc.example.com/api/v1/iidx/27/profiles?_id=profile": {
|
||||
_items: [
|
||||
{
|
||||
sp: {
|
||||
rank: "中伝",
|
||||
},
|
||||
dp: {
|
||||
rank: "ニ段",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const fn = await CreateArcIIDXClassHandler("profile", "token", mockFetch);
|
||||
const fn = await CreateArcIIDXClassHandler("profile", "token", mockFetch);
|
||||
|
||||
t.equal(fn.length, 5);
|
||||
t.equal(fn.length, 5);
|
||||
|
||||
// its not async but typescript complains
|
||||
const res = await fn("iidx", "SP", 1, {}, logger);
|
||||
// its not async but typescript complains
|
||||
const res = await fn("iidx", "SP", 1, {}, logger);
|
||||
|
||||
t.equal(res!.dan, IIDXClasses.CHUUDEN);
|
||||
t.equal(res!.dan, IIDXClasses.CHUUDEN);
|
||||
|
||||
const res2 = await fn("iidx", "DP", 1, {}, logger);
|
||||
t.equal(res2!.dan, IIDXClasses.DAN_2);
|
||||
const res2 = await fn("iidx", "DP", 1, {}, logger);
|
||||
t.equal(res2!.dan, IIDXClasses.DAN_2);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should handle null.", async (t) => {
|
||||
const mockFetch = MockJSONFetch({
|
||||
"https://arc.example.com/api/v1/iidx/27/profiles?_id=profile": {
|
||||
_items: [
|
||||
{
|
||||
sp: {
|
||||
rank: null,
|
||||
},
|
||||
dp: {
|
||||
rank: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
t.test("Should handle null.", async (t) => {
|
||||
const mockFetch = MockJSONFetch({
|
||||
"https://arc.example.com/api/v1/iidx/27/profiles?_id=profile": {
|
||||
_items: [
|
||||
{
|
||||
sp: {
|
||||
rank: null,
|
||||
},
|
||||
dp: {
|
||||
rank: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const fn = await CreateArcIIDXClassHandler("profile", "token", mockFetch);
|
||||
const fn = await CreateArcIIDXClassHandler("profile", "token", mockFetch);
|
||||
|
||||
t.equal(fn.length, 5);
|
||||
t.equal(fn.length, 5);
|
||||
|
||||
const res = await fn("iidx", "SP", 1, {}, logger);
|
||||
const res = await fn("iidx", "SP", 1, {}, logger);
|
||||
|
||||
t.strictSame(res, {});
|
||||
t.strictSame(res, {});
|
||||
|
||||
const res2 = await fn("iidx", "DP", 1, {}, logger);
|
||||
t.strictSame(res2, {});
|
||||
const res2 = await fn("iidx", "DP", 1, {}, logger);
|
||||
t.strictSame(res2, {});
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should handle undefined.", async (t) => {
|
||||
const mockFetch = MockJSONFetch({
|
||||
"https://arc.example.com/api/v1/iidx/27/profiles?_id=profile": {
|
||||
_items: [{}],
|
||||
},
|
||||
});
|
||||
t.test("Should handle undefined.", async (t) => {
|
||||
const mockFetch = MockJSONFetch({
|
||||
"https://arc.example.com/api/v1/iidx/27/profiles?_id=profile": {
|
||||
_items: [{}],
|
||||
},
|
||||
});
|
||||
|
||||
const fn = await CreateArcIIDXClassHandler("profile", "token", mockFetch);
|
||||
const fn = await CreateArcIIDXClassHandler("profile", "token", mockFetch);
|
||||
|
||||
t.equal(fn.length, 5);
|
||||
t.equal(fn.length, 5);
|
||||
|
||||
const res = await fn("iidx", "SP", 1, {}, logger);
|
||||
const res = await fn("iidx", "SP", 1, {}, logger);
|
||||
|
||||
t.strictSame(res, {});
|
||||
t.strictSame(res, {});
|
||||
|
||||
const res2 = await fn("iidx", "DP", 1, {}, logger);
|
||||
t.strictSame(res2, {});
|
||||
const res2 = await fn("iidx", "DP", 1, {}, logger);
|
||||
t.strictSame(res2, {});
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should handle invalid classes.", async (t) => {
|
||||
const mockFetch = MockJSONFetch({
|
||||
"https://arc.example.com/api/v1/iidx/27/profiles?_id=profile": {
|
||||
_items: [
|
||||
{
|
||||
sp: {
|
||||
rank: "invalid",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
t.test("Should handle invalid classes.", async (t) => {
|
||||
const mockFetch = MockJSONFetch({
|
||||
"https://arc.example.com/api/v1/iidx/27/profiles?_id=profile": {
|
||||
_items: [
|
||||
{
|
||||
sp: {
|
||||
rank: "invalid",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const fn = await CreateArcIIDXClassHandler("profile", "token", mockFetch);
|
||||
const fn = await CreateArcIIDXClassHandler("profile", "token", mockFetch);
|
||||
|
||||
t.equal(fn.length, 5);
|
||||
t.equal(fn.length, 5);
|
||||
|
||||
const res = await fn("iidx", "SP", 1, {}, logger);
|
||||
const res = await fn("iidx", "SP", 1, {}, logger);
|
||||
|
||||
t.strictSame(res, {});
|
||||
t.strictSame(res, {});
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should trigger failsafe if invalid playtype is used.", async (t) => {
|
||||
const mockFetch = MockJSONFetch({
|
||||
"https://arc.example.com/api/v1/iidx/27/profiles?_id=profile": {
|
||||
_items: [
|
||||
{
|
||||
sp: {
|
||||
rank: "中伝",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
t.test("Should trigger failsafe if invalid playtype is used.", async (t) => {
|
||||
const mockFetch = MockJSONFetch({
|
||||
"https://arc.example.com/api/v1/iidx/27/profiles?_id=profile": {
|
||||
_items: [
|
||||
{
|
||||
sp: {
|
||||
rank: "中伝",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const fn = await CreateArcIIDXClassHandler("profile", "token", mockFetch);
|
||||
const fn = await CreateArcIIDXClassHandler("profile", "token", mockFetch);
|
||||
|
||||
t.equal(fn.length, 5);
|
||||
t.equal(fn.length, 5);
|
||||
|
||||
const res = await fn("iidx", "14K", 1, {}, logger);
|
||||
const res = await fn("iidx", "14K", 1, {}, logger);
|
||||
|
||||
t.strictSame(res, {});
|
||||
t.strictSame(res, {});
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -5,86 +5,86 @@ import { ARC_API_URL } from "../../../../setup/config";
|
||||
import { ClassHandler } from "../../../framework/user-game-stats/types";
|
||||
|
||||
export async function CreateArcIIDXClassHandler(
|
||||
profileID: string,
|
||||
token: string,
|
||||
fetch = nodeFetch
|
||||
profileID: string,
|
||||
token: string,
|
||||
fetch = nodeFetch
|
||||
): Promise<ClassHandler> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let json: any;
|
||||
let err: Error | undefined;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let json: any;
|
||||
let err: Error | undefined;
|
||||
|
||||
// SP and DP dans are located in the same place,
|
||||
// fetch once, then return a function that traverses this data.
|
||||
try {
|
||||
const res = await fetch(`${ARC_API_URL}/api/v1/iidx/27/profiles?_id=${profileID}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
// SP and DP dans are located in the same place,
|
||||
// fetch once, then return a function that traverses this data.
|
||||
try {
|
||||
const res = await fetch(`${ARC_API_URL}/api/v1/iidx/27/profiles?_id=${profileID}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
json = await res.json();
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
json = await res.json();
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
return (game, playtype, userID, ratings, logger) => {
|
||||
/* istanbul ignore next */
|
||||
if (err) {
|
||||
logger.error(`An error occured while updating classes for ${ARC_API_URL}.`, { err });
|
||||
return {};
|
||||
}
|
||||
return (game, playtype, userID, ratings, logger) => {
|
||||
/* istanbul ignore next */
|
||||
if (err) {
|
||||
logger.error(`An error occured while updating classes for ${ARC_API_URL}.`, { err });
|
||||
return {};
|
||||
}
|
||||
|
||||
// we're just going to lazily path directly towards the rank.
|
||||
// if ARC sends us an unexpected JSON structure or whatever
|
||||
// we wont crash.
|
||||
let arcClass: string | undefined | null;
|
||||
// we're just going to lazily path directly towards the rank.
|
||||
// if ARC sends us an unexpected JSON structure or whatever
|
||||
// we wont crash.
|
||||
let arcClass: string | undefined | null;
|
||||
|
||||
if (playtype === "SP") {
|
||||
arcClass = json?._items?.[0]?.sp?.rank;
|
||||
} else if (playtype === "DP") {
|
||||
arcClass = json?._items?.[0]?.dp?.rank;
|
||||
} else {
|
||||
logger.error(`ARCIIDXClassUpdater called with invalid playtype of ${playtype}.`);
|
||||
return {};
|
||||
}
|
||||
if (playtype === "SP") {
|
||||
arcClass = json?._items?.[0]?.sp?.rank;
|
||||
} else if (playtype === "DP") {
|
||||
arcClass = json?._items?.[0]?.dp?.rank;
|
||||
} else {
|
||||
logger.error(`ARCIIDXClassUpdater called with invalid playtype of ${playtype}.`);
|
||||
return {};
|
||||
}
|
||||
|
||||
if (arcClass === null) {
|
||||
return {};
|
||||
}
|
||||
if (arcClass === null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// arc's classes sometimes have trailing/leading spaces.
|
||||
const trimmedArcClass = (arcClass ?? "").trim();
|
||||
// arc's classes sometimes have trailing/leading spaces.
|
||||
const trimmedArcClass = (arcClass ?? "").trim();
|
||||
|
||||
if (!HasOwnProperty(ARCClasses, trimmedArcClass)) {
|
||||
logger.warn(`Invalid dan sent from ARC ${trimmedArcClass}. Ignoring.`);
|
||||
return {};
|
||||
}
|
||||
if (!HasOwnProperty(ARCClasses, trimmedArcClass)) {
|
||||
logger.warn(`Invalid dan sent from ARC ${trimmedArcClass}. Ignoring.`);
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
dan: ARCClasses[trimmedArcClass],
|
||||
};
|
||||
};
|
||||
return {
|
||||
dan: ARCClasses[trimmedArcClass],
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const ARCClasses = {
|
||||
皆伝: IIDXClasses.KAIDEN,
|
||||
中伝: IIDXClasses.CHUUDEN,
|
||||
十段: IIDXClasses.DAN_10,
|
||||
九段: IIDXClasses.DAN_9,
|
||||
八段: IIDXClasses.DAN_8,
|
||||
七段: IIDXClasses.DAN_7,
|
||||
六段: IIDXClasses.DAN_6,
|
||||
五段: IIDXClasses.DAN_5,
|
||||
四段: IIDXClasses.DAN_4,
|
||||
三段: IIDXClasses.DAN_3,
|
||||
二段: IIDXClasses.DAN_2, // These two look very similar but they aren't
|
||||
ニ段: IIDXClasses.DAN_2, // and ARC uses both, from what I can tell.
|
||||
初段: IIDXClasses.DAN_1,
|
||||
一級: IIDXClasses.KYU_1,
|
||||
二級: IIDXClasses.KYU_2,
|
||||
三級: IIDXClasses.KYU_3,
|
||||
四級: IIDXClasses.KYU_4,
|
||||
五級: IIDXClasses.KYU_5,
|
||||
六級: IIDXClasses.KYU_6,
|
||||
七級: IIDXClasses.KYU_7,
|
||||
皆伝: IIDXClasses.KAIDEN,
|
||||
中伝: IIDXClasses.CHUUDEN,
|
||||
十段: IIDXClasses.DAN_10,
|
||||
九段: IIDXClasses.DAN_9,
|
||||
八段: IIDXClasses.DAN_8,
|
||||
七段: IIDXClasses.DAN_7,
|
||||
六段: IIDXClasses.DAN_6,
|
||||
五段: IIDXClasses.DAN_5,
|
||||
四段: IIDXClasses.DAN_4,
|
||||
三段: IIDXClasses.DAN_3,
|
||||
二段: IIDXClasses.DAN_2, // These two look very similar but they aren't
|
||||
ニ段: IIDXClasses.DAN_2, // and ARC uses both, from what I can tell.
|
||||
初段: IIDXClasses.DAN_1,
|
||||
一級: IIDXClasses.KYU_1,
|
||||
二級: IIDXClasses.KYU_2,
|
||||
三級: IIDXClasses.KYU_3,
|
||||
四級: IIDXClasses.KYU_4,
|
||||
五級: IIDXClasses.KYU_5,
|
||||
六級: IIDXClasses.KYU_6,
|
||||
七級: IIDXClasses.KYU_7,
|
||||
};
|
||||
|
||||
@@ -12,84 +12,84 @@ const logger = CreateLogCtx(__filename);
|
||||
const arcScore = GetKTDataJSON("./api-arc/iidx-score.json") as ARCIIDXScore;
|
||||
|
||||
t.test("#ConvertAPIArcIIDX", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should convert a valid score", async (t) => {
|
||||
const res = await ConvertAPIArcIIDX(arcScore, {}, "api/arc-iidx", logger);
|
||||
t.test("Should convert a valid score", async (t) => {
|
||||
const res = await ConvertAPIArcIIDX(arcScore, {}, "api/arc-iidx", logger);
|
||||
|
||||
t.hasStrict(res, {
|
||||
song: Testing511Song,
|
||||
chart: Testing511SPA,
|
||||
dryScore: {
|
||||
comment: null,
|
||||
game: "iidx",
|
||||
importType: "api/arc-iidx",
|
||||
timeAchieved: 1604784681894,
|
||||
service: "ARC IIDX27",
|
||||
scoreData: {
|
||||
grade: "A",
|
||||
score: 1200,
|
||||
hitData: {},
|
||||
hitMeta: {
|
||||
bp: 43,
|
||||
},
|
||||
lamp: "HARD CLEAR",
|
||||
},
|
||||
scoreMeta: {},
|
||||
},
|
||||
});
|
||||
t.hasStrict(res, {
|
||||
song: Testing511Song,
|
||||
chart: Testing511SPA,
|
||||
dryScore: {
|
||||
comment: null,
|
||||
game: "iidx",
|
||||
importType: "api/arc-iidx",
|
||||
timeAchieved: 1604784681894,
|
||||
service: "ARC IIDX27",
|
||||
scoreData: {
|
||||
grade: "A",
|
||||
score: 1200,
|
||||
hitData: {},
|
||||
hitMeta: {
|
||||
bp: 43,
|
||||
},
|
||||
lamp: "HARD CLEAR",
|
||||
},
|
||||
scoreMeta: {},
|
||||
},
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should throw on an invalid score", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ConvertAPIArcIIDX(
|
||||
deepmerge(arcScore, { ex_score: "foo" }),
|
||||
{},
|
||||
"api/arc-iidx",
|
||||
logger
|
||||
),
|
||||
{
|
||||
message: /Invalid ARC Score:/iu,
|
||||
} as any
|
||||
);
|
||||
t.test("Should throw on an invalid score", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ConvertAPIArcIIDX(
|
||||
deepmerge(arcScore, { ex_score: "foo" }),
|
||||
{},
|
||||
"api/arc-iidx",
|
||||
logger
|
||||
),
|
||||
{
|
||||
message: /Invalid ARC Score:/iu,
|
||||
} as any
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should throw on no chart", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ConvertAPIArcIIDX(
|
||||
deepmerge(arcScore, { chart_id: "foo" }),
|
||||
{},
|
||||
"api/arc-iidx",
|
||||
logger
|
||||
),
|
||||
{
|
||||
message: /Could not find chart/iu,
|
||||
} as any
|
||||
);
|
||||
t.test("Should throw on no chart", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ConvertAPIArcIIDX(
|
||||
deepmerge(arcScore, { chart_id: "foo" }),
|
||||
{},
|
||||
"api/arc-iidx",
|
||||
logger
|
||||
),
|
||||
{
|
||||
message: /Could not find chart/iu,
|
||||
} as any
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#ResolveARCIIDXLamp", (t) => {
|
||||
t.equal(ResolveARCIIDXLamp("NO_PLAY"), "NO PLAY");
|
||||
t.equal(ResolveARCIIDXLamp("FAILED"), "FAILED");
|
||||
t.equal(ResolveARCIIDXLamp("ASSIST_CLEAR"), "ASSIST CLEAR");
|
||||
t.equal(ResolveARCIIDXLamp("EASY_CLEAR"), "EASY CLEAR");
|
||||
t.equal(ResolveARCIIDXLamp("CLEAR"), "CLEAR");
|
||||
t.equal(ResolveARCIIDXLamp("HARD_CLEAR"), "HARD CLEAR");
|
||||
t.equal(ResolveARCIIDXLamp("EX_HARD_CLEAR"), "EX HARD CLEAR");
|
||||
t.equal(ResolveARCIIDXLamp("FULL_COMBO"), "FULL COMBO");
|
||||
t.equal(ResolveARCIIDXLamp("NO_PLAY"), "NO PLAY");
|
||||
t.equal(ResolveARCIIDXLamp("FAILED"), "FAILED");
|
||||
t.equal(ResolveARCIIDXLamp("ASSIST_CLEAR"), "ASSIST CLEAR");
|
||||
t.equal(ResolveARCIIDXLamp("EASY_CLEAR"), "EASY CLEAR");
|
||||
t.equal(ResolveARCIIDXLamp("CLEAR"), "CLEAR");
|
||||
t.equal(ResolveARCIIDXLamp("HARD_CLEAR"), "HARD CLEAR");
|
||||
t.equal(ResolveARCIIDXLamp("EX_HARD_CLEAR"), "EX HARD CLEAR");
|
||||
t.equal(ResolveARCIIDXLamp("FULL_COMBO"), "FULL COMBO");
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -2,8 +2,8 @@ import { EmptyObject } from "../../../../../utils/types";
|
||||
import { ConverterFunction } from "../../common/types";
|
||||
import p, { PrudenceSchema } from "prudence";
|
||||
import {
|
||||
InvalidScoreFailure,
|
||||
KTDataNotFoundFailure,
|
||||
InvalidScoreFailure,
|
||||
KTDataNotFoundFailure,
|
||||
} from "../../../framework/common/converter-failures";
|
||||
import { FormatPrError } from "../../../../../utils/prudence";
|
||||
import { ARCIIDXScore } from "./types";
|
||||
@@ -11,106 +11,106 @@ import { FindChartOnARCID } from "../../../../../utils/queries/charts";
|
||||
import { FindSongOnIDGuaranteed } from "../../../../../utils/queries/songs";
|
||||
import { DryScore } from "../../../framework/common/types";
|
||||
import {
|
||||
GenericGetGradeAndPercent,
|
||||
ParseDateFromString,
|
||||
GenericGetGradeAndPercent,
|
||||
ParseDateFromString,
|
||||
} from "../../../framework/common/score-utils";
|
||||
import { Lamps } from "tachi-common";
|
||||
|
||||
// There's a bunch of other useless fields but we don't care
|
||||
const PR_ArcIIDXScore: PrudenceSchema = {
|
||||
chart_id: "string",
|
||||
lamp: p.isIn(
|
||||
"FULL_COMBO",
|
||||
"EX_HARD_CLEAR",
|
||||
"HARD_CLEAR",
|
||||
"CLEAR",
|
||||
"EASY_CLEAR",
|
||||
"ASSIST_CLEAR",
|
||||
"FAILED",
|
||||
"NO_PLAY"
|
||||
),
|
||||
ex_score: p.isPositiveInteger,
|
||||
miss_count: p.nullable(p.isPositiveInteger),
|
||||
timestamp: "string",
|
||||
chart_id: "string",
|
||||
lamp: p.isIn(
|
||||
"FULL_COMBO",
|
||||
"EX_HARD_CLEAR",
|
||||
"HARD_CLEAR",
|
||||
"CLEAR",
|
||||
"EASY_CLEAR",
|
||||
"ASSIST_CLEAR",
|
||||
"FAILED",
|
||||
"NO_PLAY"
|
||||
),
|
||||
ex_score: p.isPositiveInteger,
|
||||
miss_count: p.nullable(p.isPositiveInteger),
|
||||
timestamp: "string",
|
||||
};
|
||||
|
||||
export const ConvertAPIArcIIDX: ConverterFunction<unknown, EmptyObject> = async (
|
||||
data,
|
||||
context,
|
||||
importType,
|
||||
logger
|
||||
data,
|
||||
context,
|
||||
importType,
|
||||
logger
|
||||
) => {
|
||||
const err = p(data, PR_ArcIIDXScore, {}, { throwOnNonObject: false, allowExcessKeys: true });
|
||||
const err = p(data, PR_ArcIIDXScore, {}, { throwOnNonObject: false, allowExcessKeys: true });
|
||||
|
||||
if (err) {
|
||||
throw new InvalidScoreFailure(FormatPrError(err, "Invalid ARC Score: "));
|
||||
}
|
||||
if (err) {
|
||||
throw new InvalidScoreFailure(FormatPrError(err, "Invalid ARC Score: "));
|
||||
}
|
||||
|
||||
// confirmed by Prudence above.
|
||||
const score = data as ARCIIDXScore;
|
||||
// confirmed by Prudence above.
|
||||
const score = data as ARCIIDXScore;
|
||||
|
||||
const chart = await FindChartOnARCID("iidx", score.chart_id);
|
||||
const chart = await FindChartOnARCID("iidx", score.chart_id);
|
||||
|
||||
if (!chart) {
|
||||
throw new KTDataNotFoundFailure(
|
||||
`Could not find chart with chart_id ${score.chart_id}.`,
|
||||
importType,
|
||||
data,
|
||||
context
|
||||
);
|
||||
}
|
||||
if (!chart) {
|
||||
throw new KTDataNotFoundFailure(
|
||||
`Could not find chart with chart_id ${score.chart_id}.`,
|
||||
importType,
|
||||
data,
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
const song = await FindSongOnIDGuaranteed("iidx", chart.songID, logger);
|
||||
const song = await FindSongOnIDGuaranteed("iidx", chart.songID, logger);
|
||||
|
||||
const { grade, percent } = GenericGetGradeAndPercent("iidx", score.ex_score, chart);
|
||||
const { grade, percent } = GenericGetGradeAndPercent("iidx", score.ex_score, chart);
|
||||
|
||||
const timeAchieved = ParseDateFromString(score.timestamp);
|
||||
const timeAchieved = ParseDateFromString(score.timestamp);
|
||||
|
||||
const lamp = ResolveARCIIDXLamp(score.lamp);
|
||||
const lamp = ResolveARCIIDXLamp(score.lamp);
|
||||
|
||||
const dryScore: DryScore<"iidx:SP" | "iidx:DP"> = {
|
||||
comment: null,
|
||||
game: "iidx",
|
||||
importType,
|
||||
timeAchieved,
|
||||
service: "ARC IIDX27",
|
||||
scoreData: {
|
||||
grade,
|
||||
percent,
|
||||
score: score.ex_score,
|
||||
hitData: {},
|
||||
hitMeta: {
|
||||
bp: score.miss_count,
|
||||
},
|
||||
lamp,
|
||||
},
|
||||
scoreMeta: {},
|
||||
};
|
||||
const dryScore: DryScore<"iidx:SP" | "iidx:DP"> = {
|
||||
comment: null,
|
||||
game: "iidx",
|
||||
importType,
|
||||
timeAchieved,
|
||||
service: "ARC IIDX27",
|
||||
scoreData: {
|
||||
grade,
|
||||
percent,
|
||||
score: score.ex_score,
|
||||
hitData: {},
|
||||
hitMeta: {
|
||||
bp: score.miss_count,
|
||||
},
|
||||
lamp,
|
||||
},
|
||||
scoreMeta: {},
|
||||
};
|
||||
|
||||
return { song, chart, dryScore };
|
||||
return { song, chart, dryScore };
|
||||
};
|
||||
|
||||
export function ResolveARCIIDXLamp(lamp: ARCIIDXScore["lamp"]): Lamps["iidx:SP" | "iidx:DP"] {
|
||||
switch (lamp) {
|
||||
case "NO_PLAY":
|
||||
return "NO PLAY";
|
||||
case "FAILED":
|
||||
return "FAILED";
|
||||
case "ASSIST_CLEAR":
|
||||
return "ASSIST CLEAR";
|
||||
case "EASY_CLEAR":
|
||||
return "EASY CLEAR";
|
||||
case "CLEAR":
|
||||
return "CLEAR";
|
||||
case "HARD_CLEAR":
|
||||
return "HARD CLEAR";
|
||||
case "EX_HARD_CLEAR":
|
||||
return "EX HARD CLEAR";
|
||||
case "FULL_COMBO":
|
||||
return "FULL COMBO";
|
||||
}
|
||||
switch (lamp) {
|
||||
case "NO_PLAY":
|
||||
return "NO PLAY";
|
||||
case "FAILED":
|
||||
return "FAILED";
|
||||
case "ASSIST_CLEAR":
|
||||
return "ASSIST CLEAR";
|
||||
case "EASY_CLEAR":
|
||||
return "EASY CLEAR";
|
||||
case "CLEAR":
|
||||
return "CLEAR";
|
||||
case "HARD_CLEAR":
|
||||
return "HARD CLEAR";
|
||||
case "EX_HARD_CLEAR":
|
||||
return "EX HARD CLEAR";
|
||||
case "FULL_COMBO":
|
||||
return "FULL COMBO";
|
||||
}
|
||||
|
||||
// theoretically impossible, but a failsafe regardless.
|
||||
/* istanbul ignore next */
|
||||
throw new InvalidScoreFailure(`Invalid lamp ${lamp} - Could not resolve.`);
|
||||
// theoretically impossible, but a failsafe regardless.
|
||||
/* istanbul ignore next */
|
||||
throw new InvalidScoreFailure(`Invalid lamp ${lamp} - Could not resolve.`);
|
||||
}
|
||||
|
||||
@@ -9,37 +9,37 @@ import { ParseArcIIDX } from "./parser";
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
t.test("#ParseArcIIDX", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should iterate over the API.", async (t) => {
|
||||
const mockArcAPI = MockJSONFetch({
|
||||
"https://arc.example.com/api/v1/iidx/27/player_bests?profile_id=profile": {
|
||||
_links: {
|
||||
_next: "https://arc.example.com/api/v1/iidx/27/player_bests?profile_id=profile&page=2",
|
||||
},
|
||||
_items: [1, 2, 3],
|
||||
},
|
||||
"https://arc.example.com/api/v1/iidx/27/player_bests?profile_id=profile&page=2": {
|
||||
_links: {
|
||||
_next: null,
|
||||
},
|
||||
_items: [4, 5, 6],
|
||||
},
|
||||
});
|
||||
t.test("Should iterate over the API.", async (t) => {
|
||||
const mockArcAPI = MockJSONFetch({
|
||||
"https://arc.example.com/api/v1/iidx/27/player_bests?profile_id=profile": {
|
||||
_links: {
|
||||
_next: "https://arc.example.com/api/v1/iidx/27/player_bests?profile_id=profile&page=2",
|
||||
},
|
||||
_items: [1, 2, 3],
|
||||
},
|
||||
"https://arc.example.com/api/v1/iidx/27/player_bests?profile_id=profile&page=2": {
|
||||
_links: {
|
||||
_next: null,
|
||||
},
|
||||
_items: [4, 5, 6],
|
||||
},
|
||||
});
|
||||
|
||||
const res = await ParseArcIIDX("profile", logger, mockArcAPI);
|
||||
const res = await ParseArcIIDX("profile", logger, mockArcAPI);
|
||||
|
||||
t.equal(res.game, "iidx");
|
||||
t.strictSame(res.context, {});
|
||||
t.equal(res.game, "iidx");
|
||||
t.strictSame(res.context, {});
|
||||
|
||||
const iter = await agta(res.iterable);
|
||||
const iter = await agta(res.iterable);
|
||||
|
||||
t.strictSame(iter, [1, 2, 3, 4, 5, 6]);
|
||||
t.strictSame(iter, [1, 2, 3, 4, 5, 6]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -7,21 +7,21 @@ import { ARC_API_URL, ARC_AUTH_TOKEN } from "../../../../setup/config";
|
||||
import { CreateArcIIDXClassHandler } from "./class-handler";
|
||||
|
||||
export async function ParseArcIIDX(
|
||||
arcProfileID: string,
|
||||
logger: KtLogger,
|
||||
fetch = nodeFetch
|
||||
arcProfileID: string,
|
||||
logger: KtLogger,
|
||||
fetch = nodeFetch
|
||||
): Promise<ParserFunctionReturns<unknown, EmptyObject>> {
|
||||
return {
|
||||
iterable: TraverseKaiAPI(
|
||||
ARC_API_URL,
|
||||
// HEROIC VERSE
|
||||
`/api/v1/iidx/27/player_bests?profile_id=${arcProfileID}`,
|
||||
ARC_AUTH_TOKEN,
|
||||
logger,
|
||||
fetch
|
||||
),
|
||||
context: {},
|
||||
classHandler: await CreateArcIIDXClassHandler(arcProfileID, ARC_AUTH_TOKEN),
|
||||
game: "iidx",
|
||||
};
|
||||
return {
|
||||
iterable: TraverseKaiAPI(
|
||||
ARC_API_URL,
|
||||
// HEROIC VERSE
|
||||
`/api/v1/iidx/27/player_bests?profile_id=${arcProfileID}`,
|
||||
ARC_AUTH_TOKEN,
|
||||
logger,
|
||||
fetch
|
||||
),
|
||||
context: {},
|
||||
classHandler: await CreateArcIIDXClassHandler(arcProfileID, ARC_AUTH_TOKEN),
|
||||
game: "iidx",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { integer } from "tachi-common";
|
||||
|
||||
export interface ARCIIDXScore {
|
||||
chart_id: string;
|
||||
lamp:
|
||||
| "NO_PLAY"
|
||||
| "FAILED"
|
||||
| "ASSIST_CLEAR"
|
||||
| "EASY_CLEAR"
|
||||
| "CLEAR"
|
||||
| "HARD_CLEAR"
|
||||
| "EX_HARD_CLEAR"
|
||||
| "FULL_COMBO";
|
||||
ex_score: integer;
|
||||
miss_count: integer | null;
|
||||
timestamp: string;
|
||||
chart_id: string;
|
||||
lamp:
|
||||
| "NO_PLAY"
|
||||
| "FAILED"
|
||||
| "ASSIST_CLEAR"
|
||||
| "EASY_CLEAR"
|
||||
| "CLEAR"
|
||||
| "HARD_CLEAR"
|
||||
| "EX_HARD_CLEAR"
|
||||
| "FULL_COMBO";
|
||||
ex_score: integer;
|
||||
miss_count: integer | null;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import t from "tap";
|
||||
import { CloseAllConnections } from "../../../../../test-utils/close-connections";
|
||||
import ResetDBState from "../../../../../test-utils/resets";
|
||||
import {
|
||||
GetKTDataJSON,
|
||||
TestingAlbidaADV,
|
||||
TestingSDVXAlbidaSong,
|
||||
GetKTDataJSON,
|
||||
TestingAlbidaADV,
|
||||
TestingSDVXAlbidaSong,
|
||||
} from "../../../../../test-utils/test-data";
|
||||
import CreateLogCtx from "../../../../logger/logger";
|
||||
import { ConvertAPIArcSDVX, ResolveARCSDVXLamp } from "./converter";
|
||||
@@ -16,89 +16,89 @@ const logger = CreateLogCtx(__filename);
|
||||
const arcScore = GetKTDataJSON("./api-arc/sdvx-score.json") as ARCSDVXScore;
|
||||
|
||||
t.test("#ConvertAPIArcSDVX", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should convert a valid score", async (t) => {
|
||||
const res = await ConvertAPIArcSDVX(arcScore, {}, "api/arc-sdvx", logger);
|
||||
t.test("Should convert a valid score", async (t) => {
|
||||
const res = await ConvertAPIArcSDVX(arcScore, {}, "api/arc-sdvx", logger);
|
||||
|
||||
t.hasStrict(res, {
|
||||
song: TestingSDVXAlbidaSong,
|
||||
chart: TestingAlbidaADV,
|
||||
dryScore: {
|
||||
comment: null,
|
||||
game: "sdvx",
|
||||
importType: "api/arc-sdvx",
|
||||
timeAchieved: 1567685885077,
|
||||
service: "ARC SDVX V",
|
||||
scoreData: {
|
||||
grade: "AA",
|
||||
percent: 93.125,
|
||||
score: 9312500,
|
||||
hitData: {
|
||||
critical: 946,
|
||||
near: 45,
|
||||
miss: 49,
|
||||
},
|
||||
hitMeta: {
|
||||
btnRate: 89.5,
|
||||
holdRate: 92.5,
|
||||
laserRate: 96.5,
|
||||
maxCombo: 184,
|
||||
},
|
||||
lamp: "CLEAR",
|
||||
},
|
||||
scoreMeta: {},
|
||||
},
|
||||
});
|
||||
t.hasStrict(res, {
|
||||
song: TestingSDVXAlbidaSong,
|
||||
chart: TestingAlbidaADV,
|
||||
dryScore: {
|
||||
comment: null,
|
||||
game: "sdvx",
|
||||
importType: "api/arc-sdvx",
|
||||
timeAchieved: 1567685885077,
|
||||
service: "ARC SDVX V",
|
||||
scoreData: {
|
||||
grade: "AA",
|
||||
percent: 93.125,
|
||||
score: 9312500,
|
||||
hitData: {
|
||||
critical: 946,
|
||||
near: 45,
|
||||
miss: 49,
|
||||
},
|
||||
hitMeta: {
|
||||
btnRate: 89.5,
|
||||
holdRate: 92.5,
|
||||
laserRate: 96.5,
|
||||
maxCombo: 184,
|
||||
},
|
||||
lamp: "CLEAR",
|
||||
},
|
||||
scoreMeta: {},
|
||||
},
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should throw on an invalid score", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ConvertAPIArcSDVX(
|
||||
deepmerge(arcScore, { score: "foo" }),
|
||||
{},
|
||||
"api/arc-sdvx",
|
||||
logger
|
||||
),
|
||||
{
|
||||
message: /Invalid ARC Score:/iu,
|
||||
} as any
|
||||
);
|
||||
t.test("Should throw on an invalid score", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ConvertAPIArcSDVX(
|
||||
deepmerge(arcScore, { score: "foo" }),
|
||||
{},
|
||||
"api/arc-sdvx",
|
||||
logger
|
||||
),
|
||||
{
|
||||
message: /Invalid ARC Score:/iu,
|
||||
} as any
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should throw on no chart", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ConvertAPIArcSDVX(
|
||||
deepmerge(arcScore, { chart_id: "foo" }),
|
||||
{},
|
||||
"api/arc-sdvx",
|
||||
logger
|
||||
),
|
||||
{
|
||||
message: /Could not find chart/iu,
|
||||
} as any
|
||||
);
|
||||
t.test("Should throw on no chart", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ConvertAPIArcSDVX(
|
||||
deepmerge(arcScore, { chart_id: "foo" }),
|
||||
{},
|
||||
"api/arc-sdvx",
|
||||
logger
|
||||
),
|
||||
{
|
||||
message: /Could not find chart/iu,
|
||||
} as any
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#ResolveARCSDVXLamp", (t) => {
|
||||
t.equal(ResolveARCSDVXLamp("PLAY"), "FAILED");
|
||||
t.equal(ResolveARCSDVXLamp("CLEAR"), "CLEAR");
|
||||
t.equal(ResolveARCSDVXLamp("HC"), "EXCESSIVE CLEAR");
|
||||
t.equal(ResolveARCSDVXLamp("UC"), "ULTIMATE CHAIN");
|
||||
t.equal(ResolveARCSDVXLamp("PUC"), "PERFECT ULTIMATE CHAIN");
|
||||
t.equal(ResolveARCSDVXLamp("PLAY"), "FAILED");
|
||||
t.equal(ResolveARCSDVXLamp("CLEAR"), "CLEAR");
|
||||
t.equal(ResolveARCSDVXLamp("HC"), "EXCESSIVE CLEAR");
|
||||
t.equal(ResolveARCSDVXLamp("UC"), "ULTIMATE CHAIN");
|
||||
t.equal(ResolveARCSDVXLamp("PUC"), "PERFECT ULTIMATE CHAIN");
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -2,8 +2,8 @@ import { EmptyObject } from "../../../../../utils/types";
|
||||
import { ConverterFunction } from "../../common/types";
|
||||
import p, { PrudenceSchema } from "prudence";
|
||||
import {
|
||||
InvalidScoreFailure,
|
||||
KTDataNotFoundFailure,
|
||||
InvalidScoreFailure,
|
||||
KTDataNotFoundFailure,
|
||||
} from "../../../framework/common/converter-failures";
|
||||
import { FormatPrError } from "../../../../../utils/prudence";
|
||||
import { ARCSDVXScore } from "./types";
|
||||
@@ -11,86 +11,86 @@ import { FindChartOnARCID } from "../../../../../utils/queries/charts";
|
||||
import { FindSongOnIDGuaranteed } from "../../../../../utils/queries/songs";
|
||||
import { DryScore } from "../../../framework/common/types";
|
||||
import {
|
||||
GenericGetGradeAndPercent,
|
||||
ParseDateFromString,
|
||||
GenericGetGradeAndPercent,
|
||||
ParseDateFromString,
|
||||
} from "../../../framework/common/score-utils";
|
||||
import { Lamps } from "tachi-common";
|
||||
|
||||
// There's a bunch of other useless fields but we don't care
|
||||
const PR_ArcSDVXScore: PrudenceSchema = {
|
||||
chart_id: "string",
|
||||
score: p.isBoundedInteger(0, 10_000_000),
|
||||
lamp: p.isIn("UC", "PUC", "HC", "PLAY", "CLEAR"),
|
||||
btn_rate: p.isBetween(0, 100),
|
||||
long_rate: p.isBetween(0, 100),
|
||||
vol_rate: p.isBetween(0, 100),
|
||||
critical: p.isPositiveInteger,
|
||||
near: p.isPositiveInteger,
|
||||
error: p.isPositiveInteger,
|
||||
timestamp: "string",
|
||||
chart_id: "string",
|
||||
score: p.isBoundedInteger(0, 10_000_000),
|
||||
lamp: p.isIn("UC", "PUC", "HC", "PLAY", "CLEAR"),
|
||||
btn_rate: p.isBetween(0, 100),
|
||||
long_rate: p.isBetween(0, 100),
|
||||
vol_rate: p.isBetween(0, 100),
|
||||
critical: p.isPositiveInteger,
|
||||
near: p.isPositiveInteger,
|
||||
error: p.isPositiveInteger,
|
||||
timestamp: "string",
|
||||
};
|
||||
|
||||
export const ConvertAPIArcSDVX: ConverterFunction<unknown, EmptyObject> = async (
|
||||
data,
|
||||
context,
|
||||
importType,
|
||||
logger
|
||||
data,
|
||||
context,
|
||||
importType,
|
||||
logger
|
||||
) => {
|
||||
const err = p(data, PR_ArcSDVXScore, {}, { throwOnNonObject: false, allowExcessKeys: true });
|
||||
const err = p(data, PR_ArcSDVXScore, {}, { throwOnNonObject: false, allowExcessKeys: true });
|
||||
|
||||
if (err) {
|
||||
throw new InvalidScoreFailure(FormatPrError(err, "Invalid ARC Score: "));
|
||||
}
|
||||
if (err) {
|
||||
throw new InvalidScoreFailure(FormatPrError(err, "Invalid ARC Score: "));
|
||||
}
|
||||
|
||||
// confirmed by Prudence above.
|
||||
const score = data as ARCSDVXScore;
|
||||
// confirmed by Prudence above.
|
||||
const score = data as ARCSDVXScore;
|
||||
|
||||
const chart = await FindChartOnARCID("sdvx", score.chart_id);
|
||||
const chart = await FindChartOnARCID("sdvx", score.chart_id);
|
||||
|
||||
if (!chart) {
|
||||
throw new KTDataNotFoundFailure(
|
||||
`Could not find chart with chart_id ${score.chart_id}.`,
|
||||
importType,
|
||||
data,
|
||||
context
|
||||
);
|
||||
}
|
||||
if (!chart) {
|
||||
throw new KTDataNotFoundFailure(
|
||||
`Could not find chart with chart_id ${score.chart_id}.`,
|
||||
importType,
|
||||
data,
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
const song = await FindSongOnIDGuaranteed("sdvx", chart.songID, logger);
|
||||
const song = await FindSongOnIDGuaranteed("sdvx", chart.songID, logger);
|
||||
|
||||
const { grade, percent } = GenericGetGradeAndPercent("sdvx", score.score, chart);
|
||||
const { grade, percent } = GenericGetGradeAndPercent("sdvx", score.score, chart);
|
||||
|
||||
const timeAchieved = ParseDateFromString(score.timestamp);
|
||||
const timeAchieved = ParseDateFromString(score.timestamp);
|
||||
|
||||
const lamp = ResolveARCSDVXLamp(score.lamp);
|
||||
const lamp = ResolveARCSDVXLamp(score.lamp);
|
||||
|
||||
const dryScore: DryScore<"sdvx:Single"> = {
|
||||
comment: null,
|
||||
game: "sdvx",
|
||||
importType,
|
||||
timeAchieved,
|
||||
service: "ARC SDVX V",
|
||||
scoreData: {
|
||||
grade,
|
||||
percent,
|
||||
score: score.score,
|
||||
hitData: {
|
||||
critical: score.critical,
|
||||
near: score.near,
|
||||
miss: score.error,
|
||||
},
|
||||
hitMeta: {
|
||||
btnRate: score.btn_rate,
|
||||
holdRate: score.long_rate,
|
||||
laserRate: score.vol_rate,
|
||||
maxCombo: score.max_chain,
|
||||
},
|
||||
lamp,
|
||||
},
|
||||
scoreMeta: {},
|
||||
};
|
||||
const dryScore: DryScore<"sdvx:Single"> = {
|
||||
comment: null,
|
||||
game: "sdvx",
|
||||
importType,
|
||||
timeAchieved,
|
||||
service: "ARC SDVX V",
|
||||
scoreData: {
|
||||
grade,
|
||||
percent,
|
||||
score: score.score,
|
||||
hitData: {
|
||||
critical: score.critical,
|
||||
near: score.near,
|
||||
miss: score.error,
|
||||
},
|
||||
hitMeta: {
|
||||
btnRate: score.btn_rate,
|
||||
holdRate: score.long_rate,
|
||||
laserRate: score.vol_rate,
|
||||
maxCombo: score.max_chain,
|
||||
},
|
||||
lamp,
|
||||
},
|
||||
scoreMeta: {},
|
||||
};
|
||||
|
||||
return { song, chart, dryScore };
|
||||
return { song, chart, dryScore };
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -98,18 +98,18 @@ export const ConvertAPIArcSDVX: ConverterFunction<unknown, EmptyObject> = async
|
||||
* This function takes the score and lamp and rederives the lamp.
|
||||
*/
|
||||
export function ResolveARCSDVXLamp(lamp: ARCSDVXScore["lamp"]): Lamps["sdvx:Single"] {
|
||||
switch (lamp) {
|
||||
case "PLAY":
|
||||
return "FAILED";
|
||||
case "CLEAR":
|
||||
return "CLEAR";
|
||||
case "HC":
|
||||
return "EXCESSIVE CLEAR";
|
||||
case "UC":
|
||||
return "ULTIMATE CHAIN";
|
||||
case "PUC":
|
||||
return "PERFECT ULTIMATE CHAIN";
|
||||
}
|
||||
switch (lamp) {
|
||||
case "PLAY":
|
||||
return "FAILED";
|
||||
case "CLEAR":
|
||||
return "CLEAR";
|
||||
case "HC":
|
||||
return "EXCESSIVE CLEAR";
|
||||
case "UC":
|
||||
return "ULTIMATE CHAIN";
|
||||
case "PUC":
|
||||
return "PERFECT ULTIMATE CHAIN";
|
||||
}
|
||||
|
||||
throw new InvalidScoreFailure(`Invalid lamp ${lamp} - Could not convert.`);
|
||||
throw new InvalidScoreFailure(`Invalid lamp ${lamp} - Could not convert.`);
|
||||
}
|
||||
|
||||
@@ -9,38 +9,38 @@ import { ParseArcSDVX } from "./parser";
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
t.test("#ParseArcSDVX", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should iterate over the API.", async (t) => {
|
||||
const mockArcAPI = MockJSONFetch({
|
||||
"https://arc.example.com/api/v1/sdvx/5/player_bests?profile_id=profile": {
|
||||
_links: {
|
||||
_next: "https://arc.example.com/api/v1/sdvx/5/player_bests?profile_id=profile&page=2",
|
||||
},
|
||||
_items: [1, 2, 3],
|
||||
},
|
||||
"https://arc.example.com/api/v1/sdvx/5/player_bests?profile_id=profile&page=2": {
|
||||
_links: {
|
||||
_next: null,
|
||||
},
|
||||
_items: [4, 5, 6],
|
||||
},
|
||||
});
|
||||
t.test("Should iterate over the API.", async (t) => {
|
||||
const mockArcAPI = MockJSONFetch({
|
||||
"https://arc.example.com/api/v1/sdvx/5/player_bests?profile_id=profile": {
|
||||
_links: {
|
||||
_next: "https://arc.example.com/api/v1/sdvx/5/player_bests?profile_id=profile&page=2",
|
||||
},
|
||||
_items: [1, 2, 3],
|
||||
},
|
||||
"https://arc.example.com/api/v1/sdvx/5/player_bests?profile_id=profile&page=2": {
|
||||
_links: {
|
||||
_next: null,
|
||||
},
|
||||
_items: [4, 5, 6],
|
||||
},
|
||||
});
|
||||
|
||||
const res = ParseArcSDVX("profile", logger, mockArcAPI);
|
||||
const res = ParseArcSDVX("profile", logger, mockArcAPI);
|
||||
|
||||
t.equal(res.game, "sdvx");
|
||||
t.strictSame(res.context, {});
|
||||
t.equal(res.classHandler, null);
|
||||
t.equal(res.game, "sdvx");
|
||||
t.strictSame(res.context, {});
|
||||
t.equal(res.classHandler, null);
|
||||
|
||||
const iter = await agta(res.iterable);
|
||||
const iter = await agta(res.iterable);
|
||||
|
||||
t.strictSame(iter, [1, 2, 3, 4, 5, 6]);
|
||||
t.strictSame(iter, [1, 2, 3, 4, 5, 6]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -6,21 +6,21 @@ import { EmptyObject } from "../../../../../utils/types";
|
||||
import { ARC_API_URL, ARC_AUTH_TOKEN } from "../../../../setup/config";
|
||||
|
||||
export function ParseArcSDVX(
|
||||
arcProfileID: string,
|
||||
logger: KtLogger,
|
||||
fetch = nodeFetch
|
||||
arcProfileID: string,
|
||||
logger: KtLogger,
|
||||
fetch = nodeFetch
|
||||
): ParserFunctionReturns<unknown, EmptyObject> {
|
||||
return {
|
||||
iterable: TraverseKaiAPI(
|
||||
ARC_API_URL,
|
||||
// VIVID WAVE.
|
||||
`/api/v1/sdvx/5/player_bests?profile_id=${arcProfileID}`,
|
||||
ARC_AUTH_TOKEN,
|
||||
logger,
|
||||
fetch
|
||||
),
|
||||
context: {},
|
||||
classHandler: null,
|
||||
game: "sdvx",
|
||||
};
|
||||
return {
|
||||
iterable: TraverseKaiAPI(
|
||||
ARC_API_URL,
|
||||
// VIVID WAVE.
|
||||
`/api/v1/sdvx/5/player_bests?profile_id=${arcProfileID}`,
|
||||
ARC_AUTH_TOKEN,
|
||||
logger,
|
||||
fetch
|
||||
),
|
||||
context: {},
|
||||
classHandler: null,
|
||||
game: "sdvx",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { integer } from "tachi-common";
|
||||
|
||||
export interface ARCSDVXScore {
|
||||
chart_id: string;
|
||||
score: integer;
|
||||
lamp: "CLEAR" | "UC" | "PUC" | "PLAY" | "HC";
|
||||
btn_rate: number;
|
||||
long_rate: number;
|
||||
vol_rate: number;
|
||||
critical: integer;
|
||||
near: integer;
|
||||
error: integer;
|
||||
max_chain: integer;
|
||||
timestamp: string;
|
||||
chart_id: string;
|
||||
score: integer;
|
||||
lamp: "CLEAR" | "UC" | "PUC" | "PLAY" | "HC";
|
||||
btn_rate: number;
|
||||
long_rate: number;
|
||||
vol_rate: number;
|
||||
critical: integer;
|
||||
near: integer;
|
||||
error: integer;
|
||||
max_chain: integer;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
@@ -3,5 +3,5 @@ import { ParseKaiIIDX } from "../../common/api-kai/iidx/parser";
|
||||
import { KaiAuthDocument } from "tachi-common";
|
||||
|
||||
export function ParseEagIIDX(authDoc: KaiAuthDocument, logger: KtLogger) {
|
||||
return ParseKaiIIDX("EAG", authDoc, logger);
|
||||
return ParseKaiIIDX("EAG", authDoc, logger);
|
||||
}
|
||||
|
||||
@@ -3,5 +3,5 @@ import { KaiAuthDocument } from "tachi-common";
|
||||
import { ParseKaiSDVX } from "../../common/api-kai/sdvx/parser";
|
||||
|
||||
export function ParseEagSDVX(authDoc: KaiAuthDocument, logger: KtLogger) {
|
||||
return ParseKaiSDVX("EAG", authDoc, logger);
|
||||
return ParseKaiSDVX("EAG", authDoc, logger);
|
||||
}
|
||||
|
||||
@@ -3,5 +3,5 @@ import { ParseKaiIIDX } from "../../common/api-kai/iidx/parser";
|
||||
import { KaiAuthDocument } from "tachi-common";
|
||||
|
||||
export function ParseFloIIDX(authDoc: KaiAuthDocument, logger: KtLogger) {
|
||||
return ParseKaiIIDX("FLO", authDoc, logger);
|
||||
return ParseKaiIIDX("FLO", authDoc, logger);
|
||||
}
|
||||
|
||||
@@ -3,5 +3,5 @@ import { KaiAuthDocument } from "tachi-common";
|
||||
import { ParseKaiSDVX } from "../../common/api-kai/sdvx/parser";
|
||||
|
||||
export function ParseFloSDVX(authDoc: KaiAuthDocument, logger: KtLogger) {
|
||||
return ParseKaiSDVX("FLO", authDoc, logger);
|
||||
return ParseKaiSDVX("FLO", authDoc, logger);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import t from "tap";
|
||||
import { CloseAllConnections } from "../../../../../../test-utils/close-connections";
|
||||
import ResetDBState from "../../../../../../test-utils/resets";
|
||||
import {
|
||||
GetKTDataJSON,
|
||||
Testing511Song,
|
||||
Testing511SPA,
|
||||
GetKTDataJSON,
|
||||
Testing511Song,
|
||||
Testing511SPA,
|
||||
} from "../../../../../../test-utils/test-data";
|
||||
import CreateLogCtx from "../../../../../logger/logger";
|
||||
import { ConvertAPIKaiIIDX } from "./converter";
|
||||
@@ -16,84 +16,84 @@ const logger = CreateLogCtx(__filename);
|
||||
const iidxScore = GetKTDataJSON("./api-kai/iidx-score.json");
|
||||
|
||||
t.test("#ConvertAPIKaiIIDX", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should return a dryScore on valid input.", async (t) => {
|
||||
const res = await ConvertAPIKaiIIDX(iidxScore, { service: "FLO" }, "api/flo-iidx", logger);
|
||||
t.test("Should return a dryScore on valid input.", async (t) => {
|
||||
const res = await ConvertAPIKaiIIDX(iidxScore, { service: "FLO" }, "api/flo-iidx", logger);
|
||||
|
||||
t.hasStrict(res, {
|
||||
song: Testing511Song,
|
||||
chart: Testing511SPA,
|
||||
dryScore: {
|
||||
comment: null,
|
||||
game: "iidx",
|
||||
importType: "api/flo-iidx",
|
||||
timeAchieved: 1604171450000,
|
||||
service: "FLO",
|
||||
scoreData: {
|
||||
grade: "MAX-",
|
||||
// percent: 99.87277353689568 floating point,
|
||||
score: 1570,
|
||||
lamp: "HARD CLEAR",
|
||||
hitData: {},
|
||||
hitMeta: {
|
||||
fast: null,
|
||||
slow: null,
|
||||
bp: 24,
|
||||
},
|
||||
},
|
||||
scoreMeta: {},
|
||||
},
|
||||
});
|
||||
t.hasStrict(res, {
|
||||
song: Testing511Song,
|
||||
chart: Testing511SPA,
|
||||
dryScore: {
|
||||
comment: null,
|
||||
game: "iidx",
|
||||
importType: "api/flo-iidx",
|
||||
timeAchieved: 1604171450000,
|
||||
service: "FLO",
|
||||
scoreData: {
|
||||
grade: "MAX-",
|
||||
// percent: 99.87277353689568 floating point,
|
||||
score: 1570,
|
||||
lamp: "HARD CLEAR",
|
||||
hitData: {},
|
||||
hitMeta: {
|
||||
fast: null,
|
||||
slow: null,
|
||||
bp: 24,
|
||||
},
|
||||
},
|
||||
scoreMeta: {},
|
||||
},
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should throw KTDataNotFound on unknown chart", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ConvertAPIKaiIIDX(
|
||||
deepmerge(iidxScore, { music_id: 0 }),
|
||||
{ service: "FLO" },
|
||||
"api/flo-iidx",
|
||||
logger
|
||||
),
|
||||
{
|
||||
message: /Could not find chart with songID 0 \(SP ANOTHER - Version 26\)/u,
|
||||
} as any
|
||||
);
|
||||
t.test("Should throw KTDataNotFound on unknown chart", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ConvertAPIKaiIIDX(
|
||||
deepmerge(iidxScore, { music_id: 0 }),
|
||||
{ service: "FLO" },
|
||||
"api/flo-iidx",
|
||||
logger
|
||||
),
|
||||
{
|
||||
message: /Could not find chart with songID 0 \(SP ANOTHER - Version 26\)/u,
|
||||
} as any
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should throw InvalidScoreFailure on invalid score", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ConvertAPIKaiIIDX(
|
||||
deepmerge(iidxScore, { music_id: "foo" }),
|
||||
{ service: "FLO" },
|
||||
"api/flo-iidx",
|
||||
logger
|
||||
),
|
||||
{
|
||||
message: /Error: music_id.*Expected a positive integer.* received foo \[string\]/iu,
|
||||
} as any
|
||||
);
|
||||
t.test("Should throw InvalidScoreFailure on invalid score", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ConvertAPIKaiIIDX(
|
||||
deepmerge(iidxScore, { music_id: "foo" }),
|
||||
{ service: "FLO" },
|
||||
"api/flo-iidx",
|
||||
logger
|
||||
),
|
||||
{
|
||||
message: /Error: music_id.*Expected a positive integer.* received foo \[string\]/iu,
|
||||
} as any
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should throw InternalFailure on song-chart desync", async (t) => {
|
||||
await db.songs.iidx.remove({});
|
||||
t.test("Should throw InternalFailure on song-chart desync", async (t) => {
|
||||
await db.songs.iidx.remove({});
|
||||
|
||||
t.rejects(() => ConvertAPIKaiIIDX(iidxScore, { service: "FLO" }, "api/flo-iidx", logger), {
|
||||
message: /Song-Chart desync/u,
|
||||
} as any);
|
||||
t.rejects(() => ConvertAPIKaiIIDX(iidxScore, { service: "FLO" }, "api/flo-iidx", logger), {
|
||||
message: /Song-Chart desync/u,
|
||||
} as any);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -2,121 +2,121 @@ import { FindIIDXChartOnInGameIDVersion } from "../../../../../../utils/queries/
|
||||
import { KaiContext, KaiIIDXScore } from "../types";
|
||||
import p from "prudence";
|
||||
import {
|
||||
InternalFailure,
|
||||
InvalidScoreFailure,
|
||||
KTDataNotFoundFailure,
|
||||
InternalFailure,
|
||||
InvalidScoreFailure,
|
||||
KTDataNotFoundFailure,
|
||||
} from "../../../../framework/common/converter-failures";
|
||||
import { FormatPrError } from "../../../../../../utils/prudence";
|
||||
import { FindSongOnID } from "../../../../../../utils/queries/songs";
|
||||
import {
|
||||
GenericGetGradeAndPercent,
|
||||
ParseDateFromString,
|
||||
GenericGetGradeAndPercent,
|
||||
ParseDateFromString,
|
||||
} from "../../../../framework/common/score-utils";
|
||||
import { Lamps } from "tachi-common";
|
||||
import { ConverterFunction } from "../../types";
|
||||
import { DryScore } from "../../../../framework/common/types";
|
||||
|
||||
const PR_KaiIIDXScore = {
|
||||
music_id: p.isPositiveInteger,
|
||||
play_style: p.isIn("SINGLE", "DOUBLE"),
|
||||
difficulty: p.isIn("BEGINNER", "NORMAL", "HYPER", "ANOTHER", "LEGGENDARIA"),
|
||||
version_played: p.isBoundedInteger(9, 28),
|
||||
lamp: p.isBoundedInteger(0, 7),
|
||||
ex_score: p.isPositiveInteger,
|
||||
miss_count: p.or(p.isPositiveInteger, p.is(-1)),
|
||||
fast_count: p.nullable(p.isPositiveInteger),
|
||||
slow_count: p.nullable(p.isPositiveInteger),
|
||||
timestamp: "string",
|
||||
music_id: p.isPositiveInteger,
|
||||
play_style: p.isIn("SINGLE", "DOUBLE"),
|
||||
difficulty: p.isIn("BEGINNER", "NORMAL", "HYPER", "ANOTHER", "LEGGENDARIA"),
|
||||
version_played: p.isBoundedInteger(9, 28),
|
||||
lamp: p.isBoundedInteger(0, 7),
|
||||
ex_score: p.isPositiveInteger,
|
||||
miss_count: p.or(p.isPositiveInteger, p.is(-1)),
|
||||
fast_count: p.nullable(p.isPositiveInteger),
|
||||
slow_count: p.nullable(p.isPositiveInteger),
|
||||
timestamp: "string",
|
||||
};
|
||||
|
||||
function ResolveKaiLamp(lamp: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7): Lamps["iidx:SP" | "iidx:DP"] {
|
||||
switch (lamp) {
|
||||
case 0:
|
||||
return "NO PLAY";
|
||||
case 1:
|
||||
return "FAILED";
|
||||
case 3:
|
||||
return "EASY CLEAR";
|
||||
case 2:
|
||||
return "ASSIST CLEAR";
|
||||
case 4:
|
||||
return "CLEAR";
|
||||
case 5:
|
||||
return "HARD CLEAR";
|
||||
case 6:
|
||||
return "EX HARD CLEAR";
|
||||
case 7:
|
||||
return "FULL COMBO";
|
||||
}
|
||||
switch (lamp) {
|
||||
case 0:
|
||||
return "NO PLAY";
|
||||
case 1:
|
||||
return "FAILED";
|
||||
case 3:
|
||||
return "EASY CLEAR";
|
||||
case 2:
|
||||
return "ASSIST CLEAR";
|
||||
case 4:
|
||||
return "CLEAR";
|
||||
case 5:
|
||||
return "HARD CLEAR";
|
||||
case 6:
|
||||
return "EX HARD CLEAR";
|
||||
case 7:
|
||||
return "FULL COMBO";
|
||||
}
|
||||
}
|
||||
|
||||
export const ConvertAPIKaiIIDX: ConverterFunction<unknown, KaiContext> = async (
|
||||
data,
|
||||
context,
|
||||
importType,
|
||||
logger
|
||||
data,
|
||||
context,
|
||||
importType,
|
||||
logger
|
||||
) => {
|
||||
const err = p(data, PR_KaiIIDXScore, {}, { allowExcessKeys: true });
|
||||
const err = p(data, PR_KaiIIDXScore, {}, { allowExcessKeys: true });
|
||||
|
||||
if (err) {
|
||||
throw new InvalidScoreFailure(FormatPrError(err));
|
||||
}
|
||||
if (err) {
|
||||
throw new InvalidScoreFailure(FormatPrError(err));
|
||||
}
|
||||
|
||||
// prudence checks this above.
|
||||
const score = data as KaiIIDXScore;
|
||||
// prudence checks this above.
|
||||
const score = data as KaiIIDXScore;
|
||||
|
||||
const playtype = score.play_style === "SINGLE" ? "SP" : "DP";
|
||||
const playtype = score.play_style === "SINGLE" ? "SP" : "DP";
|
||||
|
||||
const chart = await FindIIDXChartOnInGameIDVersion(
|
||||
score.music_id,
|
||||
playtype,
|
||||
score.difficulty,
|
||||
// they send integers like 25, 27 - this will convert to our versions.
|
||||
score.version_played.toString()
|
||||
);
|
||||
const chart = await FindIIDXChartOnInGameIDVersion(
|
||||
score.music_id,
|
||||
playtype,
|
||||
score.difficulty,
|
||||
// they send integers like 25, 27 - this will convert to our versions.
|
||||
score.version_played.toString()
|
||||
);
|
||||
|
||||
if (!chart) {
|
||||
throw new KTDataNotFoundFailure(
|
||||
`Could not find chart with songID ${score.music_id} (${playtype} ${score.difficulty} - Version ${score.version_played})`,
|
||||
importType,
|
||||
data,
|
||||
context
|
||||
);
|
||||
}
|
||||
if (!chart) {
|
||||
throw new KTDataNotFoundFailure(
|
||||
`Could not find chart with songID ${score.music_id} (${playtype} ${score.difficulty} - Version ${score.version_played})`,
|
||||
importType,
|
||||
data,
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
const song = await FindSongOnID("iidx", chart.songID);
|
||||
const song = await FindSongOnID("iidx", chart.songID);
|
||||
|
||||
if (!song) {
|
||||
logger.severe(`Song-Chart desync with song ID ${chart.songID} (iidx).`);
|
||||
throw new InternalFailure(`Song-Chart desync with song ID ${chart.songID} (iidx).`);
|
||||
}
|
||||
if (!song) {
|
||||
logger.severe(`Song-Chart desync with song ID ${chart.songID} (iidx).`);
|
||||
throw new InternalFailure(`Song-Chart desync with song ID ${chart.songID} (iidx).`);
|
||||
}
|
||||
|
||||
const lamp = ResolveKaiLamp(score.lamp);
|
||||
const lamp = ResolveKaiLamp(score.lamp);
|
||||
|
||||
const { percent, grade } = GenericGetGradeAndPercent("iidx", score.ex_score, chart);
|
||||
const { percent, grade } = GenericGetGradeAndPercent("iidx", score.ex_score, chart);
|
||||
|
||||
const timeAchieved = ParseDateFromString(score.timestamp);
|
||||
const timeAchieved = ParseDateFromString(score.timestamp);
|
||||
|
||||
const dryScore: DryScore<"iidx:SP" | "iidx:DP"> = {
|
||||
comment: null,
|
||||
game: "iidx",
|
||||
importType,
|
||||
timeAchieved,
|
||||
service: context.service,
|
||||
scoreData: {
|
||||
grade,
|
||||
percent,
|
||||
score: score.ex_score,
|
||||
lamp,
|
||||
hitData: {},
|
||||
hitMeta: {
|
||||
fast: score.fast_count,
|
||||
slow: score.slow_count,
|
||||
bp: score.miss_count === -1 ? null : score.miss_count,
|
||||
},
|
||||
},
|
||||
scoreMeta: {},
|
||||
};
|
||||
const dryScore: DryScore<"iidx:SP" | "iidx:DP"> = {
|
||||
comment: null,
|
||||
game: "iidx",
|
||||
importType,
|
||||
timeAchieved,
|
||||
service: context.service,
|
||||
scoreData: {
|
||||
grade,
|
||||
percent,
|
||||
score: score.ex_score,
|
||||
lamp,
|
||||
hitData: {},
|
||||
hitMeta: {
|
||||
fast: score.fast_count,
|
||||
slow: score.slow_count,
|
||||
bp: score.miss_count === -1 ? null : score.miss_count,
|
||||
},
|
||||
},
|
||||
scoreMeta: {},
|
||||
};
|
||||
|
||||
return { song, chart, dryScore };
|
||||
return { song, chart, dryScore };
|
||||
};
|
||||
|
||||
@@ -6,84 +6,84 @@ import CreateLogCtx from "../../../../../logger/logger";
|
||||
import { ParseKaiIIDX } from "./parser";
|
||||
|
||||
const fakeAuth = {
|
||||
userID: 1,
|
||||
refreshToken: "foo",
|
||||
service: "FLO" as const,
|
||||
token: "bar",
|
||||
userID: 1,
|
||||
refreshToken: "foo",
|
||||
service: "FLO" as const,
|
||||
token: "bar",
|
||||
};
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
t.test("#ParseKaiIIDX", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should iterate over the IIDX FLO API asynchronously.", async (t) => {
|
||||
const mockFloAPI = MockJSONFetch({
|
||||
"https://flo.example.com/api/iidx/v2/play_history": {
|
||||
_links: {
|
||||
_next: "https://flo.example.com/api/iidx/v2/play_history?page=2",
|
||||
},
|
||||
_items: [1, 2, 3, 4],
|
||||
},
|
||||
"https://flo.example.com/api/iidx/v2/play_history?page=2": {
|
||||
_links: {
|
||||
_next: null,
|
||||
},
|
||||
_items: [5, 6],
|
||||
},
|
||||
});
|
||||
t.test("Should iterate over the IIDX FLO API asynchronously.", async (t) => {
|
||||
const mockFloAPI = MockJSONFetch({
|
||||
"https://flo.example.com/api/iidx/v2/play_history": {
|
||||
_links: {
|
||||
_next: "https://flo.example.com/api/iidx/v2/play_history?page=2",
|
||||
},
|
||||
_items: [1, 2, 3, 4],
|
||||
},
|
||||
"https://flo.example.com/api/iidx/v2/play_history?page=2": {
|
||||
_links: {
|
||||
_next: null,
|
||||
},
|
||||
_items: [5, 6],
|
||||
},
|
||||
});
|
||||
|
||||
const res = ParseKaiIIDX("FLO", fakeAuth, logger, mockFloAPI);
|
||||
const res = ParseKaiIIDX("FLO", fakeAuth, logger, mockFloAPI);
|
||||
|
||||
t.equal(res.game, "iidx");
|
||||
t.equal(res.classHandler, null);
|
||||
t.strictSame(res.context, { service: "FLO" });
|
||||
t.equal(res.game, "iidx");
|
||||
t.equal(res.classHandler, null);
|
||||
t.strictSame(res.context, { service: "FLO" });
|
||||
|
||||
const iter = [];
|
||||
const iter = [];
|
||||
|
||||
for await (const el of res.iterable) {
|
||||
iter.push(el);
|
||||
}
|
||||
for await (const el of res.iterable) {
|
||||
iter.push(el);
|
||||
}
|
||||
|
||||
t.strictSame(iter, [1, 2, 3, 4, 5, 6]);
|
||||
t.strictSame(iter, [1, 2, 3, 4, 5, 6]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should iterate over the IIDX EAG API asynchronously.", async (t) => {
|
||||
const mockEagAPI = MockJSONFetch({
|
||||
"https://eag.example.com/api/iidx/v2/play_history": {
|
||||
_links: {
|
||||
_next: "https://eag.example.com/api/iidx/v2/play_history?page=2",
|
||||
},
|
||||
_items: [1, 2, 3, 4],
|
||||
},
|
||||
"https://eag.example.com/api/iidx/v2/play_history?page=2": {
|
||||
_links: {
|
||||
_next: null,
|
||||
},
|
||||
_items: [5, 6],
|
||||
},
|
||||
});
|
||||
t.test("Should iterate over the IIDX EAG API asynchronously.", async (t) => {
|
||||
const mockEagAPI = MockJSONFetch({
|
||||
"https://eag.example.com/api/iidx/v2/play_history": {
|
||||
_links: {
|
||||
_next: "https://eag.example.com/api/iidx/v2/play_history?page=2",
|
||||
},
|
||||
_items: [1, 2, 3, 4],
|
||||
},
|
||||
"https://eag.example.com/api/iidx/v2/play_history?page=2": {
|
||||
_links: {
|
||||
_next: null,
|
||||
},
|
||||
_items: [5, 6],
|
||||
},
|
||||
});
|
||||
|
||||
const res = ParseKaiIIDX("EAG", fakeAuth, logger, mockEagAPI);
|
||||
const res = ParseKaiIIDX("EAG", fakeAuth, logger, mockEagAPI);
|
||||
|
||||
t.equal(res.game, "iidx");
|
||||
t.equal(res.classHandler, null);
|
||||
t.strictSame(res.context, { service: "EAG" });
|
||||
t.equal(res.game, "iidx");
|
||||
t.equal(res.classHandler, null);
|
||||
t.strictSame(res.context, { service: "EAG" });
|
||||
|
||||
const iter = [];
|
||||
const iter = [];
|
||||
|
||||
for await (const el of res.iterable) {
|
||||
iter.push(el);
|
||||
}
|
||||
for await (const el of res.iterable) {
|
||||
iter.push(el);
|
||||
}
|
||||
|
||||
t.strictSame(iter, [1, 2, 3, 4, 5, 6]);
|
||||
t.strictSame(iter, [1, 2, 3, 4, 5, 6]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -7,25 +7,25 @@ import { ParserFunctionReturns } from "../../types";
|
||||
import { FLO_API_URL, EAG_API_URL } from "../../../../../setup/config";
|
||||
|
||||
export function ParseKaiIIDX(
|
||||
service: "FLO" | "EAG",
|
||||
authDoc: KaiAuthDocument,
|
||||
logger: KtLogger,
|
||||
fetch = nodeFetch
|
||||
service: "FLO" | "EAG",
|
||||
authDoc: KaiAuthDocument,
|
||||
logger: KtLogger,
|
||||
fetch = nodeFetch
|
||||
): ParserFunctionReturns<unknown, KaiContext> {
|
||||
const baseUrl = service === "FLO" ? FLO_API_URL : EAG_API_URL;
|
||||
const baseUrl = service === "FLO" ? FLO_API_URL : EAG_API_URL;
|
||||
|
||||
return {
|
||||
iterable: TraverseKaiAPI(
|
||||
baseUrl,
|
||||
"/api/iidx/v2/play_history",
|
||||
authDoc.token,
|
||||
logger,
|
||||
fetch
|
||||
),
|
||||
context: {
|
||||
service,
|
||||
},
|
||||
classHandler: null,
|
||||
game: "iidx",
|
||||
};
|
||||
return {
|
||||
iterable: TraverseKaiAPI(
|
||||
baseUrl,
|
||||
"/api/iidx/v2/play_history",
|
||||
authDoc.token,
|
||||
logger,
|
||||
fetch
|
||||
),
|
||||
context: {
|
||||
service,
|
||||
},
|
||||
classHandler: null,
|
||||
game: "iidx",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import t from "tap";
|
||||
import { CloseAllConnections } from "../../../../../../test-utils/close-connections";
|
||||
import ResetDBState from "../../../../../../test-utils/resets";
|
||||
import {
|
||||
GetKTDataJSON,
|
||||
TestingAlbidaADV,
|
||||
TestingSDVXAlbidaSong,
|
||||
GetKTDataJSON,
|
||||
TestingAlbidaADV,
|
||||
TestingSDVXAlbidaSong,
|
||||
} from "../../../../../../test-utils/test-data";
|
||||
import CreateLogCtx from "../../../../../logger/logger";
|
||||
import { ConvertAPIKaiSDVX, ConvertDifficulty, ConvertVersion, ResolveKaiLamp } from "./converter";
|
||||
@@ -16,118 +16,118 @@ const logger = CreateLogCtx(__filename);
|
||||
const sdvxScore = GetKTDataJSON("./api-kai/sdvx-score.json");
|
||||
|
||||
t.test("#ConvertAPIKaiSDVX", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should return a dryScore on valid input.", async (t) => {
|
||||
const res = await ConvertAPIKaiSDVX(sdvxScore, { service: "FLO" }, "api/flo-sdvx", logger);
|
||||
t.test("Should return a dryScore on valid input.", async (t) => {
|
||||
const res = await ConvertAPIKaiSDVX(sdvxScore, { service: "FLO" }, "api/flo-sdvx", logger);
|
||||
|
||||
t.hasStrict(res, {
|
||||
song: TestingSDVXAlbidaSong,
|
||||
chart: TestingAlbidaADV,
|
||||
dryScore: {
|
||||
comment: null,
|
||||
game: "sdvx",
|
||||
importType: "api/flo-sdvx",
|
||||
timeAchieved: 1598792891000,
|
||||
service: "FLO",
|
||||
scoreData: {
|
||||
grade: "AA",
|
||||
// percent: 93.10699, floating point
|
||||
score: 9310699,
|
||||
lamp: "EXCESSIVE CLEAR",
|
||||
hitData: {},
|
||||
hitMeta: {
|
||||
fast: 70,
|
||||
slow: 42,
|
||||
gauge: 90,
|
||||
maxCombo: 179,
|
||||
},
|
||||
},
|
||||
scoreMeta: {},
|
||||
},
|
||||
});
|
||||
t.hasStrict(res, {
|
||||
song: TestingSDVXAlbidaSong,
|
||||
chart: TestingAlbidaADV,
|
||||
dryScore: {
|
||||
comment: null,
|
||||
game: "sdvx",
|
||||
importType: "api/flo-sdvx",
|
||||
timeAchieved: 1598792891000,
|
||||
service: "FLO",
|
||||
scoreData: {
|
||||
grade: "AA",
|
||||
// percent: 93.10699, floating point
|
||||
score: 9310699,
|
||||
lamp: "EXCESSIVE CLEAR",
|
||||
hitData: {},
|
||||
hitMeta: {
|
||||
fast: 70,
|
||||
slow: 42,
|
||||
gauge: 90,
|
||||
maxCombo: 179,
|
||||
},
|
||||
},
|
||||
scoreMeta: {},
|
||||
},
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should throw KTDataNotFound on unknown chart", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ConvertAPIKaiSDVX(
|
||||
deepmerge(sdvxScore, { music_id: 0 }),
|
||||
{ service: "FLO" },
|
||||
"api/flo-sdvx",
|
||||
logger
|
||||
),
|
||||
{
|
||||
message: /Could not find chart with songID 0 \(ADV - Version vivid\)/u,
|
||||
} as any
|
||||
);
|
||||
t.test("Should throw KTDataNotFound on unknown chart", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ConvertAPIKaiSDVX(
|
||||
deepmerge(sdvxScore, { music_id: 0 }),
|
||||
{ service: "FLO" },
|
||||
"api/flo-sdvx",
|
||||
logger
|
||||
),
|
||||
{
|
||||
message: /Could not find chart with songID 0 \(ADV - Version vivid\)/u,
|
||||
} as any
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should throw InvalidScoreFailure on invalid score", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ConvertAPIKaiSDVX(
|
||||
deepmerge(sdvxScore, { music_id: "foo" }),
|
||||
{ service: "FLO" },
|
||||
"api/flo-sdvx",
|
||||
logger
|
||||
),
|
||||
{
|
||||
message: /Error: music_id.*Expected a positive integer.* received foo \[string\]/iu,
|
||||
} as any
|
||||
);
|
||||
t.test("Should throw InvalidScoreFailure on invalid score", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ConvertAPIKaiSDVX(
|
||||
deepmerge(sdvxScore, { music_id: "foo" }),
|
||||
{ service: "FLO" },
|
||||
"api/flo-sdvx",
|
||||
logger
|
||||
),
|
||||
{
|
||||
message: /Error: music_id.*Expected a positive integer.* received foo \[string\]/iu,
|
||||
} as any
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should throw InternalFailure on song-chart desync", async (t) => {
|
||||
await db.songs.sdvx.remove({});
|
||||
t.test("Should throw InternalFailure on song-chart desync", async (t) => {
|
||||
await db.songs.sdvx.remove({});
|
||||
|
||||
t.rejects(() => ConvertAPIKaiSDVX(sdvxScore, { service: "FLO" }, "api/flo-sdvx", logger), {
|
||||
message: /Song-Chart desync/u,
|
||||
} as any);
|
||||
t.rejects(() => ConvertAPIKaiSDVX(sdvxScore, { service: "FLO" }, "api/flo-sdvx", logger), {
|
||||
message: /Song-Chart desync/u,
|
||||
} as any);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#ConvertDifficulty", (t) => {
|
||||
t.equal(ConvertDifficulty(0), "NOV");
|
||||
t.equal(ConvertDifficulty(1), "ADV");
|
||||
t.equal(ConvertDifficulty(2), "EXH");
|
||||
t.equal(ConvertDifficulty(3), "ANY_INF");
|
||||
t.equal(ConvertDifficulty(4), "MXM");
|
||||
t.throws(() => ConvertDifficulty(5));
|
||||
t.equal(ConvertDifficulty(0), "NOV");
|
||||
t.equal(ConvertDifficulty(1), "ADV");
|
||||
t.equal(ConvertDifficulty(2), "EXH");
|
||||
t.equal(ConvertDifficulty(3), "ANY_INF");
|
||||
t.equal(ConvertDifficulty(4), "MXM");
|
||||
t.throws(() => ConvertDifficulty(5));
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#ConvertVersion", (t) => {
|
||||
t.equal(ConvertVersion(0), "booth");
|
||||
t.equal(ConvertVersion(1), "inf");
|
||||
t.equal(ConvertVersion(2), "gw");
|
||||
t.equal(ConvertVersion(3), "heaven");
|
||||
t.equal(ConvertVersion(4), "vivid");
|
||||
t.throws(() => ConvertVersion(5));
|
||||
t.equal(ConvertVersion(0), "booth");
|
||||
t.equal(ConvertVersion(1), "inf");
|
||||
t.equal(ConvertVersion(2), "gw");
|
||||
t.equal(ConvertVersion(3), "heaven");
|
||||
t.equal(ConvertVersion(4), "vivid");
|
||||
t.throws(() => ConvertVersion(5));
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#ResolveKaiLamp", (t) => {
|
||||
t.equal(ResolveKaiLamp(0), "FAILED");
|
||||
t.equal(ResolveKaiLamp(1), "CLEAR");
|
||||
t.equal(ResolveKaiLamp(2), "EXCESSIVE CLEAR");
|
||||
t.equal(ResolveKaiLamp(3), "ULTIMATE CHAIN");
|
||||
t.equal(ResolveKaiLamp(4), "PERFECT ULTIMATE CHAIN");
|
||||
t.throws(() => ConvertDifficulty(5));
|
||||
t.equal(ResolveKaiLamp(0), "FAILED");
|
||||
t.equal(ResolveKaiLamp(1), "CLEAR");
|
||||
t.equal(ResolveKaiLamp(2), "EXCESSIVE CLEAR");
|
||||
t.equal(ResolveKaiLamp(3), "ULTIMATE CHAIN");
|
||||
t.equal(ResolveKaiLamp(4), "PERFECT ULTIMATE CHAIN");
|
||||
t.throws(() => ConvertDifficulty(5));
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -2,150 +2,150 @@ import { FindSDVXChartOnInGameIDVersion } from "../../../../../../utils/queries/
|
||||
import { KaiContext, KaiSDVXScore } from "../types";
|
||||
import p from "prudence";
|
||||
import {
|
||||
InternalFailure,
|
||||
InvalidScoreFailure,
|
||||
KTDataNotFoundFailure,
|
||||
InternalFailure,
|
||||
InvalidScoreFailure,
|
||||
KTDataNotFoundFailure,
|
||||
} from "../../../../framework/common/converter-failures";
|
||||
import { FormatPrError } from "../../../../../../utils/prudence";
|
||||
import { FindSongOnID } from "../../../../../../utils/queries/songs";
|
||||
import {
|
||||
GenericGetGradeAndPercent,
|
||||
ParseDateFromString,
|
||||
GenericGetGradeAndPercent,
|
||||
ParseDateFromString,
|
||||
} from "../../../../framework/common/score-utils";
|
||||
import { Lamps } from "tachi-common";
|
||||
import { ConverterFunction } from "../../types";
|
||||
import { DryScore } from "../../../../framework/common/types";
|
||||
|
||||
const PR_KaiSDVXScore = {
|
||||
music_id: p.isPositiveInteger,
|
||||
music_difficulty: p.isBoundedInteger(0, 4),
|
||||
played_version: p.isBoundedInteger(1, 5),
|
||||
clear_type: p.isBoundedInteger(0, 4),
|
||||
max_chain: p.isPositiveInteger,
|
||||
score: p.isBoundedInteger(0, 10_000_000),
|
||||
critical: p.isPositiveInteger,
|
||||
near: p.isPositiveInteger,
|
||||
error: p.isPositiveInteger,
|
||||
early: p.isPositiveInteger,
|
||||
late: p.isPositiveInteger,
|
||||
gauge_rate: p.isBoundedInteger(0, 100),
|
||||
timestamp: "string",
|
||||
music_id: p.isPositiveInteger,
|
||||
music_difficulty: p.isBoundedInteger(0, 4),
|
||||
played_version: p.isBoundedInteger(1, 5),
|
||||
clear_type: p.isBoundedInteger(0, 4),
|
||||
max_chain: p.isPositiveInteger,
|
||||
score: p.isBoundedInteger(0, 10_000_000),
|
||||
critical: p.isPositiveInteger,
|
||||
near: p.isPositiveInteger,
|
||||
error: p.isPositiveInteger,
|
||||
early: p.isPositiveInteger,
|
||||
late: p.isPositiveInteger,
|
||||
gauge_rate: p.isBoundedInteger(0, 100),
|
||||
timestamp: "string",
|
||||
};
|
||||
|
||||
export const ConvertAPIKaiSDVX: ConverterFunction<unknown, KaiContext> = async (
|
||||
data,
|
||||
context,
|
||||
importType,
|
||||
logger
|
||||
data,
|
||||
context,
|
||||
importType,
|
||||
logger
|
||||
) => {
|
||||
const err = p(data, PR_KaiSDVXScore, {}, { allowExcessKeys: true });
|
||||
const err = p(data, PR_KaiSDVXScore, {}, { allowExcessKeys: true });
|
||||
|
||||
if (err) {
|
||||
throw new InvalidScoreFailure(FormatPrError(err));
|
||||
}
|
||||
if (err) {
|
||||
throw new InvalidScoreFailure(FormatPrError(err));
|
||||
}
|
||||
|
||||
// prudence checks this above.
|
||||
const score = data as KaiSDVXScore;
|
||||
// prudence checks this above.
|
||||
const score = data as KaiSDVXScore;
|
||||
|
||||
const difficulty = ConvertDifficulty(score.music_difficulty);
|
||||
const version = ConvertVersion(score.played_version);
|
||||
const difficulty = ConvertDifficulty(score.music_difficulty);
|
||||
const version = ConvertVersion(score.played_version);
|
||||
|
||||
const chart = await FindSDVXChartOnInGameIDVersion(score.music_id, difficulty, version);
|
||||
const chart = await FindSDVXChartOnInGameIDVersion(score.music_id, difficulty, version);
|
||||
|
||||
if (!chart) {
|
||||
throw new KTDataNotFoundFailure(
|
||||
`Could not find chart with songID ${score.music_id} (${difficulty} - Version ${version})`,
|
||||
importType,
|
||||
data,
|
||||
context
|
||||
);
|
||||
}
|
||||
if (!chart) {
|
||||
throw new KTDataNotFoundFailure(
|
||||
`Could not find chart with songID ${score.music_id} (${difficulty} - Version ${version})`,
|
||||
importType,
|
||||
data,
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
const song = await FindSongOnID("sdvx", chart.songID);
|
||||
const song = await FindSongOnID("sdvx", chart.songID);
|
||||
|
||||
if (!song) {
|
||||
logger.severe(`Song-Chart desync with song ID ${chart.songID} (sdvx).`);
|
||||
throw new InternalFailure(`Song-Chart desync with song ID ${chart.songID} (sdvx).`);
|
||||
}
|
||||
if (!song) {
|
||||
logger.severe(`Song-Chart desync with song ID ${chart.songID} (sdvx).`);
|
||||
throw new InternalFailure(`Song-Chart desync with song ID ${chart.songID} (sdvx).`);
|
||||
}
|
||||
|
||||
const lamp = ResolveKaiLamp(score.clear_type);
|
||||
const lamp = ResolveKaiLamp(score.clear_type);
|
||||
|
||||
const { percent, grade } = GenericGetGradeAndPercent("sdvx", score.score, chart);
|
||||
const { percent, grade } = GenericGetGradeAndPercent("sdvx", score.score, chart);
|
||||
|
||||
const timeAchieved = ParseDateFromString(score.timestamp);
|
||||
const timeAchieved = ParseDateFromString(score.timestamp);
|
||||
|
||||
const dryScore: DryScore<"sdvx:Single"> = {
|
||||
comment: null,
|
||||
game: "sdvx",
|
||||
importType,
|
||||
timeAchieved,
|
||||
service: context.service,
|
||||
scoreData: {
|
||||
grade,
|
||||
percent,
|
||||
score: score.score,
|
||||
lamp,
|
||||
hitData: {},
|
||||
hitMeta: {
|
||||
fast: score.early,
|
||||
slow: score.late,
|
||||
gauge: score.gauge_rate,
|
||||
maxCombo: score.max_chain,
|
||||
},
|
||||
},
|
||||
scoreMeta: {},
|
||||
};
|
||||
const dryScore: DryScore<"sdvx:Single"> = {
|
||||
comment: null,
|
||||
game: "sdvx",
|
||||
importType,
|
||||
timeAchieved,
|
||||
service: context.service,
|
||||
scoreData: {
|
||||
grade,
|
||||
percent,
|
||||
score: score.score,
|
||||
lamp,
|
||||
hitData: {},
|
||||
hitMeta: {
|
||||
fast: score.early,
|
||||
slow: score.late,
|
||||
gauge: score.gauge_rate,
|
||||
maxCombo: score.max_chain,
|
||||
},
|
||||
},
|
||||
scoreMeta: {},
|
||||
};
|
||||
|
||||
return { song, chart, dryScore };
|
||||
return { song, chart, dryScore };
|
||||
};
|
||||
|
||||
export function ConvertDifficulty(diff: number) {
|
||||
switch (diff) {
|
||||
case 0:
|
||||
return "NOV";
|
||||
case 1:
|
||||
return "ADV";
|
||||
case 2:
|
||||
return "EXH";
|
||||
case 3:
|
||||
return "ANY_INF";
|
||||
case 4:
|
||||
return "MXM";
|
||||
}
|
||||
switch (diff) {
|
||||
case 0:
|
||||
return "NOV";
|
||||
case 1:
|
||||
return "ADV";
|
||||
case 2:
|
||||
return "EXH";
|
||||
case 3:
|
||||
return "ANY_INF";
|
||||
case 4:
|
||||
return "MXM";
|
||||
}
|
||||
|
||||
throw new InvalidScoreFailure(`Invalid difficulty of ${diff} - Could not convert.`);
|
||||
throw new InvalidScoreFailure(`Invalid difficulty of ${diff} - Could not convert.`);
|
||||
}
|
||||
|
||||
export function ConvertVersion(ver: number) {
|
||||
switch (ver) {
|
||||
case 0:
|
||||
return "booth";
|
||||
case 1:
|
||||
return "inf";
|
||||
case 2:
|
||||
return "gw";
|
||||
case 3:
|
||||
return "heaven";
|
||||
case 4:
|
||||
return "vivid";
|
||||
}
|
||||
switch (ver) {
|
||||
case 0:
|
||||
return "booth";
|
||||
case 1:
|
||||
return "inf";
|
||||
case 2:
|
||||
return "gw";
|
||||
case 3:
|
||||
return "heaven";
|
||||
case 4:
|
||||
return "vivid";
|
||||
}
|
||||
|
||||
throw new InvalidScoreFailure(`Unknown Game Version ${ver}.`);
|
||||
throw new InvalidScoreFailure(`Unknown Game Version ${ver}.`);
|
||||
}
|
||||
|
||||
export function ResolveKaiLamp(clear: number): Lamps["sdvx:Single"] {
|
||||
switch (clear) {
|
||||
case 0:
|
||||
return "FAILED";
|
||||
case 1:
|
||||
return "CLEAR";
|
||||
case 2:
|
||||
return "EXCESSIVE CLEAR";
|
||||
case 3:
|
||||
return "ULTIMATE CHAIN";
|
||||
case 4:
|
||||
return "PERFECT ULTIMATE CHAIN";
|
||||
}
|
||||
switch (clear) {
|
||||
case 0:
|
||||
return "FAILED";
|
||||
case 1:
|
||||
return "CLEAR";
|
||||
case 2:
|
||||
return "EXCESSIVE CLEAR";
|
||||
case 3:
|
||||
return "ULTIMATE CHAIN";
|
||||
case 4:
|
||||
return "PERFECT ULTIMATE CHAIN";
|
||||
}
|
||||
|
||||
throw new InvalidScoreFailure(`Invalid lamp of ${clear} - Could not convert.`);
|
||||
throw new InvalidScoreFailure(`Invalid lamp of ${clear} - Could not convert.`);
|
||||
}
|
||||
|
||||
@@ -6,84 +6,84 @@ import CreateLogCtx from "../../../../../logger/logger";
|
||||
import { ParseKaiSDVX } from "./parser";
|
||||
|
||||
const fakeAuth = {
|
||||
userID: 1,
|
||||
refreshToken: "foo",
|
||||
service: "FLO" as const,
|
||||
token: "bar",
|
||||
userID: 1,
|
||||
refreshToken: "foo",
|
||||
service: "FLO" as const,
|
||||
token: "bar",
|
||||
};
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
t.test("#ParseKaiSDVX", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should iterate over the SDVX FLO API asynchronously.", async (t) => {
|
||||
const mockFloAPI = MockJSONFetch({
|
||||
"https://flo.example.com/api/sdvx/v1/play_history": {
|
||||
_links: {
|
||||
_next: "https://flo.example.com/api/sdvx/v1/play_history?page=2",
|
||||
},
|
||||
_items: [1, 2, 3, 4],
|
||||
},
|
||||
"https://flo.example.com/api/sdvx/v1/play_history?page=2": {
|
||||
_links: {
|
||||
_next: null,
|
||||
},
|
||||
_items: [5, 6],
|
||||
},
|
||||
});
|
||||
t.test("Should iterate over the SDVX FLO API asynchronously.", async (t) => {
|
||||
const mockFloAPI = MockJSONFetch({
|
||||
"https://flo.example.com/api/sdvx/v1/play_history": {
|
||||
_links: {
|
||||
_next: "https://flo.example.com/api/sdvx/v1/play_history?page=2",
|
||||
},
|
||||
_items: [1, 2, 3, 4],
|
||||
},
|
||||
"https://flo.example.com/api/sdvx/v1/play_history?page=2": {
|
||||
_links: {
|
||||
_next: null,
|
||||
},
|
||||
_items: [5, 6],
|
||||
},
|
||||
});
|
||||
|
||||
const res = ParseKaiSDVX("FLO", fakeAuth, logger, mockFloAPI);
|
||||
const res = ParseKaiSDVX("FLO", fakeAuth, logger, mockFloAPI);
|
||||
|
||||
t.equal(res.game, "sdvx");
|
||||
t.equal(res.classHandler, null);
|
||||
t.strictSame(res.context, { service: "FLO" });
|
||||
t.equal(res.game, "sdvx");
|
||||
t.equal(res.classHandler, null);
|
||||
t.strictSame(res.context, { service: "FLO" });
|
||||
|
||||
const iter = [];
|
||||
const iter = [];
|
||||
|
||||
for await (const el of res.iterable) {
|
||||
iter.push(el);
|
||||
}
|
||||
for await (const el of res.iterable) {
|
||||
iter.push(el);
|
||||
}
|
||||
|
||||
t.strictSame(iter, [1, 2, 3, 4, 5, 6]);
|
||||
t.strictSame(iter, [1, 2, 3, 4, 5, 6]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should iterate over the SDVX EAG API asynchronously.", async (t) => {
|
||||
const mockEagAPI = MockJSONFetch({
|
||||
"https://eag.example.com/api/sdvx/v1/play_history": {
|
||||
_links: {
|
||||
_next: "https://eag.example.com/api/sdvx/v1/play_history?page=2",
|
||||
},
|
||||
_items: [1, 2, 3, 4],
|
||||
},
|
||||
"https://eag.example.com/api/sdvx/v1/play_history?page=2": {
|
||||
_links: {
|
||||
_next: null,
|
||||
},
|
||||
_items: [5, 6],
|
||||
},
|
||||
});
|
||||
t.test("Should iterate over the SDVX EAG API asynchronously.", async (t) => {
|
||||
const mockEagAPI = MockJSONFetch({
|
||||
"https://eag.example.com/api/sdvx/v1/play_history": {
|
||||
_links: {
|
||||
_next: "https://eag.example.com/api/sdvx/v1/play_history?page=2",
|
||||
},
|
||||
_items: [1, 2, 3, 4],
|
||||
},
|
||||
"https://eag.example.com/api/sdvx/v1/play_history?page=2": {
|
||||
_links: {
|
||||
_next: null,
|
||||
},
|
||||
_items: [5, 6],
|
||||
},
|
||||
});
|
||||
|
||||
const res = ParseKaiSDVX("EAG", fakeAuth, logger, mockEagAPI);
|
||||
const res = ParseKaiSDVX("EAG", fakeAuth, logger, mockEagAPI);
|
||||
|
||||
t.equal(res.game, "sdvx");
|
||||
t.equal(res.classHandler, null);
|
||||
t.strictSame(res.context, { service: "EAG" });
|
||||
t.equal(res.game, "sdvx");
|
||||
t.equal(res.classHandler, null);
|
||||
t.strictSame(res.context, { service: "EAG" });
|
||||
|
||||
const iter = [];
|
||||
const iter = [];
|
||||
|
||||
for await (const el of res.iterable) {
|
||||
iter.push(el);
|
||||
}
|
||||
for await (const el of res.iterable) {
|
||||
iter.push(el);
|
||||
}
|
||||
|
||||
t.strictSame(iter, [1, 2, 3, 4, 5, 6]);
|
||||
t.strictSame(iter, [1, 2, 3, 4, 5, 6]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -7,25 +7,25 @@ import { ParserFunctionReturns } from "../../types";
|
||||
import { EAG_API_URL, FLO_API_URL } from "../../../../../setup/config";
|
||||
|
||||
export function ParseKaiSDVX(
|
||||
service: "FLO" | "EAG",
|
||||
authDoc: KaiAuthDocument,
|
||||
logger: KtLogger,
|
||||
fetch = nodeFetch
|
||||
service: "FLO" | "EAG",
|
||||
authDoc: KaiAuthDocument,
|
||||
logger: KtLogger,
|
||||
fetch = nodeFetch
|
||||
): ParserFunctionReturns<unknown, KaiContext> {
|
||||
const baseUrl = service === "FLO" ? FLO_API_URL : EAG_API_URL;
|
||||
const baseUrl = service === "FLO" ? FLO_API_URL : EAG_API_URL;
|
||||
|
||||
return {
|
||||
iterable: TraverseKaiAPI(
|
||||
baseUrl,
|
||||
"/api/sdvx/v1/play_history",
|
||||
authDoc.token,
|
||||
logger,
|
||||
fetch
|
||||
),
|
||||
context: {
|
||||
service,
|
||||
},
|
||||
classHandler: null,
|
||||
game: "sdvx",
|
||||
};
|
||||
return {
|
||||
iterable: TraverseKaiAPI(
|
||||
baseUrl,
|
||||
"/api/sdvx/v1/play_history",
|
||||
authDoc.token,
|
||||
logger,
|
||||
fetch
|
||||
),
|
||||
context: {
|
||||
service,
|
||||
},
|
||||
classHandler: null,
|
||||
game: "sdvx",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,146 +12,146 @@ const logger = CreateLogCtx(__filename);
|
||||
const fakeAuth = "bar";
|
||||
|
||||
t.test("#TraverseKaiAPI", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should traverse the API", async (t) => {
|
||||
const mockKaiAPI = MockJSONFetch({
|
||||
"http://url.com/sub": {
|
||||
_links: {
|
||||
_next: "http://url.com/sub?page=2",
|
||||
},
|
||||
_items: [1, 2, 3, 4],
|
||||
},
|
||||
"http://url.com/sub?page=2": {
|
||||
_links: {
|
||||
_next: null,
|
||||
},
|
||||
_items: [5, 6],
|
||||
},
|
||||
});
|
||||
t.test("Should traverse the API", async (t) => {
|
||||
const mockKaiAPI = MockJSONFetch({
|
||||
"http://url.com/sub": {
|
||||
_links: {
|
||||
_next: "http://url.com/sub?page=2",
|
||||
},
|
||||
_items: [1, 2, 3, 4],
|
||||
},
|
||||
"http://url.com/sub?page=2": {
|
||||
_links: {
|
||||
_next: null,
|
||||
},
|
||||
_items: [5, 6],
|
||||
},
|
||||
});
|
||||
|
||||
const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI);
|
||||
const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI);
|
||||
|
||||
const elements = await agta(res);
|
||||
const elements = await agta(res);
|
||||
|
||||
t.strictSame(elements, [1, 2, 3, 4, 5, 6]);
|
||||
t.strictSame(elements, [1, 2, 3, 4, 5, 6]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should throw on attempted SSRF", (t) => {
|
||||
const mockKaiAPI = MockJSONFetch({
|
||||
"http://url.com/sub": {
|
||||
_links: {
|
||||
_next: "http://evil.com/sub?page=2",
|
||||
},
|
||||
_items: [1, 2, 3, 4],
|
||||
},
|
||||
"http://url.com/sub?page=2": {
|
||||
_links: {
|
||||
_next: null,
|
||||
},
|
||||
_items: [5, 6],
|
||||
},
|
||||
});
|
||||
const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI);
|
||||
t.test("Should throw on attempted SSRF", (t) => {
|
||||
const mockKaiAPI = MockJSONFetch({
|
||||
"http://url.com/sub": {
|
||||
_links: {
|
||||
_next: "http://evil.com/sub?page=2",
|
||||
},
|
||||
_items: [1, 2, 3, 4],
|
||||
},
|
||||
"http://url.com/sub?page=2": {
|
||||
_links: {
|
||||
_next: null,
|
||||
},
|
||||
_items: [5, 6],
|
||||
},
|
||||
});
|
||||
const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI);
|
||||
|
||||
t.rejects(
|
||||
() => agta(res),
|
||||
new ScoreImportFatalError(500, `http://url.com returned invalid data.`)
|
||||
);
|
||||
t.rejects(
|
||||
() => agta(res),
|
||||
new ScoreImportFatalError(500, `http://url.com returned invalid data.`)
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should throw on invalid response JSON", (t) => {
|
||||
const mockKaiAPI = () => ({ json: null } as any);
|
||||
const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI);
|
||||
t.test("Should throw on invalid response JSON", (t) => {
|
||||
const mockKaiAPI = () => ({ json: null } as any);
|
||||
const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI);
|
||||
|
||||
t.rejects(() => agta(res));
|
||||
t.rejects(() => agta(res));
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should throw on request failure", (t) => {
|
||||
const mockKaiAPI = () => {
|
||||
throw new Error("Fake Request timeout...");
|
||||
};
|
||||
const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI);
|
||||
t.test("Should throw on request failure", (t) => {
|
||||
const mockKaiAPI = () => {
|
||||
throw new Error("Fake Request timeout...");
|
||||
};
|
||||
const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI);
|
||||
|
||||
t.rejects(() => agta(res));
|
||||
t.rejects(() => agta(res));
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should throw on invalid _links", (t) => {
|
||||
const mockKaiAPI = MockJSONFetch({
|
||||
"http://url.com/sub": {
|
||||
_links: null,
|
||||
},
|
||||
});
|
||||
t.test("Should throw on invalid _links", (t) => {
|
||||
const mockKaiAPI = MockJSONFetch({
|
||||
"http://url.com/sub": {
|
||||
_links: null,
|
||||
},
|
||||
});
|
||||
|
||||
const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI);
|
||||
const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI);
|
||||
|
||||
t.rejects(() => agta(res));
|
||||
t.rejects(() => agta(res));
|
||||
|
||||
const mockKaiAPI2 = MockJSONFetch({
|
||||
"http://url.com/sub": {
|
||||
_links: "foo",
|
||||
},
|
||||
});
|
||||
const mockKaiAPI2 = MockJSONFetch({
|
||||
"http://url.com/sub": {
|
||||
_links: "foo",
|
||||
},
|
||||
});
|
||||
|
||||
const res2 = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI2);
|
||||
const res2 = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI2);
|
||||
|
||||
t.rejects(() => agta(res2));
|
||||
t.rejects(() => agta(res2));
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should throw on invalid _links._next", (t) => {
|
||||
const mockKaiAPI = MockJSONFetch({
|
||||
"http://url.com/sub": {
|
||||
_links: {},
|
||||
},
|
||||
});
|
||||
t.test("Should throw on invalid _links._next", (t) => {
|
||||
const mockKaiAPI = MockJSONFetch({
|
||||
"http://url.com/sub": {
|
||||
_links: {},
|
||||
},
|
||||
});
|
||||
|
||||
const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI);
|
||||
const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI);
|
||||
|
||||
t.rejects(() => agta(res));
|
||||
t.rejects(() => agta(res));
|
||||
|
||||
const mockKaiAPI2 = MockJSONFetch({
|
||||
"http://url.com/sub": {
|
||||
_links: {
|
||||
_next: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
const mockKaiAPI2 = MockJSONFetch({
|
||||
"http://url.com/sub": {
|
||||
_links: {
|
||||
_next: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const res2 = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI2);
|
||||
const res2 = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI2);
|
||||
|
||||
t.rejects(() => agta(res2));
|
||||
t.rejects(() => agta(res2));
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should throw on invalid _items", (t) => {
|
||||
const mockKaiAPI = MockJSONFetch({
|
||||
"http://url.com/sub": {
|
||||
_links: {
|
||||
_next: null,
|
||||
},
|
||||
_items: {},
|
||||
},
|
||||
});
|
||||
t.test("Should throw on invalid _items", (t) => {
|
||||
const mockKaiAPI = MockJSONFetch({
|
||||
"http://url.com/sub": {
|
||||
_links: {
|
||||
_next: null,
|
||||
},
|
||||
_items: {},
|
||||
},
|
||||
});
|
||||
|
||||
const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI);
|
||||
const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI);
|
||||
|
||||
t.rejects(() => agta(res));
|
||||
t.rejects(() => agta(res));
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -13,75 +13,75 @@ import { CONF_INFO } from "../../../../setup/config";
|
||||
* that wont make http requests during tests.
|
||||
*/
|
||||
export async function* TraverseKaiAPI(
|
||||
baseUrl: string,
|
||||
subUrl: string,
|
||||
token: string,
|
||||
logger: KtLogger,
|
||||
fetch = nodeFetch
|
||||
baseUrl: string,
|
||||
subUrl: string,
|
||||
token: string,
|
||||
logger: KtLogger,
|
||||
fetch = nodeFetch
|
||||
) {
|
||||
let fetchMoreData = true;
|
||||
let url = `${baseUrl}${subUrl}`;
|
||||
let fetchMoreData = true;
|
||||
let url = `${baseUrl}${subUrl}`;
|
||||
|
||||
while (fetchMoreData) {
|
||||
// Rough SSRF check - KAI-like apis could potentially get us to perform
|
||||
// SSRF. We check if the origin of the next request is exactly the
|
||||
// original, expected, base URL.
|
||||
const origin = new URL(url).origin;
|
||||
while (fetchMoreData) {
|
||||
// Rough SSRF check - KAI-like apis could potentially get us to perform
|
||||
// SSRF. We check if the origin of the next request is exactly the
|
||||
// original, expected, base URL.
|
||||
const origin = new URL(url).origin;
|
||||
|
||||
if (origin !== baseUrl) {
|
||||
logger.severe(`${baseUrl} attempted SSRF with url ${url}?`);
|
||||
throw new ScoreImportFatalError(500, `${baseUrl} returned invalid data.`);
|
||||
}
|
||||
if (origin !== baseUrl) {
|
||||
logger.severe(`${baseUrl} attempted SSRF with url ${url}?`);
|
||||
throw new ScoreImportFatalError(500, `${baseUrl} returned invalid data.`);
|
||||
}
|
||||
|
||||
let json;
|
||||
// wrap all this in a try catch just incase the fetch or the res
|
||||
// json call fails.
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"User-Agent": `${CONF_INFO.NAME}/${VERSION_STR}`,
|
||||
},
|
||||
});
|
||||
let json;
|
||||
// wrap all this in a try catch just incase the fetch or the res
|
||||
// json call fails.
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"User-Agent": `${CONF_INFO.NAME}/${VERSION_STR}`,
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
json = await res.json();
|
||||
} catch (err) {
|
||||
logger.error(`Recieved invalid response from ${url}.`, { err });
|
||||
throw new ScoreImportFatalError(
|
||||
500,
|
||||
`Recieved invalid response from ${url}. Are they down?`
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
json = await res.json();
|
||||
} catch (err) {
|
||||
logger.error(`Recieved invalid response from ${url}.`, { err });
|
||||
throw new ScoreImportFatalError(
|
||||
500,
|
||||
`Recieved invalid response from ${url}. Are they down?`
|
||||
);
|
||||
}
|
||||
|
||||
if (json._links === null || typeof json._links !== "object") {
|
||||
logger.error(`Recieved invalid JSON from ${url}. Invalid _links.`);
|
||||
if (json._links === null || typeof json._links !== "object") {
|
||||
logger.error(`Recieved invalid JSON from ${url}. Invalid _links.`);
|
||||
|
||||
throw new ScoreImportFatalError(
|
||||
500,
|
||||
`Recieved no _links prop from ${url}. This is not an error with Kamaitachi.`
|
||||
);
|
||||
}
|
||||
throw new ScoreImportFatalError(
|
||||
500,
|
||||
`Recieved no _links prop from ${url}. This is not an error with Kamaitachi.`
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof json._links._next === "string") {
|
||||
url = json._links._next;
|
||||
} else if (json._links._next === null) {
|
||||
// exit the loop after this, we're on the last page.
|
||||
fetchMoreData = false;
|
||||
} else {
|
||||
logger.error(`Recieved invalid response from ${url}. Invalid _links._next.`);
|
||||
if (typeof json._links._next === "string") {
|
||||
url = json._links._next;
|
||||
} else if (json._links._next === null) {
|
||||
// exit the loop after this, we're on the last page.
|
||||
fetchMoreData = false;
|
||||
} else {
|
||||
logger.error(`Recieved invalid response from ${url}. Invalid _links._next.`);
|
||||
|
||||
throw new ScoreImportFatalError(500, `Recieved invalid _links._next prop from ${url}.`);
|
||||
}
|
||||
throw new ScoreImportFatalError(500, `Recieved invalid _links._next prop from ${url}.`);
|
||||
}
|
||||
|
||||
if (!Array.isArray(json._items)) {
|
||||
throw new ScoreImportFatalError(500, `Recieved invalid _items from ${url}.`);
|
||||
}
|
||||
if (!Array.isArray(json._items)) {
|
||||
throw new ScoreImportFatalError(500, `Recieved invalid _items from ${url}.`);
|
||||
}
|
||||
|
||||
// yield everything out of the score array
|
||||
for (const score of json._items) {
|
||||
yield score as unknown;
|
||||
}
|
||||
}
|
||||
// yield everything out of the score array
|
||||
for (const score of json._items) {
|
||||
yield score as unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
import { integer } from "tachi-common";
|
||||
|
||||
export interface KaiIIDXScore {
|
||||
music_id: integer;
|
||||
play_style: "SINGLE" | "DOUBLE";
|
||||
difficulty: "BEGINNER" | "NORMAL" | "HYPER" | "ANOTHER" | "LEGGENDARIA";
|
||||
version_played: integer;
|
||||
lamp: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
||||
ex_score: integer;
|
||||
miss_count: integer; // -1 => null
|
||||
fast_count: integer | null;
|
||||
slow_count: integer | null;
|
||||
timestamp: string;
|
||||
music_id: integer;
|
||||
play_style: "SINGLE" | "DOUBLE";
|
||||
difficulty: "BEGINNER" | "NORMAL" | "HYPER" | "ANOTHER" | "LEGGENDARIA";
|
||||
version_played: integer;
|
||||
lamp: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
||||
ex_score: integer;
|
||||
miss_count: integer; // -1 => null
|
||||
fast_count: integer | null;
|
||||
slow_count: integer | null;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface KaiSDVXScore {
|
||||
music_id: integer;
|
||||
music_difficulty: 0 | 1 | 2 | 3 | 4;
|
||||
played_version: integer;
|
||||
clear_type: 0 | 1 | 2 | 3 | 4; // hm
|
||||
score: integer;
|
||||
max_chain: integer;
|
||||
critical: integer;
|
||||
near: integer;
|
||||
error: integer;
|
||||
early: integer;
|
||||
late: integer;
|
||||
gauge_type: 0 | 1 | 2 | 3;
|
||||
gauge_rate: integer;
|
||||
timestamp: string;
|
||||
music_id: integer;
|
||||
music_difficulty: 0 | 1 | 2 | 3 | 4;
|
||||
played_version: integer;
|
||||
clear_type: 0 | 1 | 2 | 3 | 4; // hm
|
||||
score: integer;
|
||||
max_chain: integer;
|
||||
critical: integer;
|
||||
near: integer;
|
||||
error: integer;
|
||||
early: integer;
|
||||
late: integer;
|
||||
gauge_type: 0 | 1 | 2 | 3;
|
||||
gauge_rate: integer;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface KaiContext {
|
||||
service: "FLO" | "EAG";
|
||||
service: "FLO" | "EAG";
|
||||
}
|
||||
|
||||
@@ -10,25 +10,25 @@ import { InvalidScoreFailure } from "../../../framework/common/converter-failure
|
||||
import { CloseAllConnections } from "../../../../../test-utils/close-connections";
|
||||
|
||||
const baseBatchManualScore = {
|
||||
score: 500,
|
||||
lamp: "HARD CLEAR" as const,
|
||||
matchType: "kamaitachiSongID" as const,
|
||||
identifier: "1",
|
||||
playtype: "SP" as const,
|
||||
difficulty: "ANOTHER",
|
||||
score: 500,
|
||||
lamp: "HARD CLEAR" as const,
|
||||
matchType: "kamaitachiSongID" as const,
|
||||
identifier: "1",
|
||||
playtype: "SP" as const,
|
||||
difficulty: "ANOTHER",
|
||||
};
|
||||
|
||||
const context = {
|
||||
game: "iidx" as const,
|
||||
service: "foo",
|
||||
version: null,
|
||||
game: "iidx" as const,
|
||||
service: "foo",
|
||||
version: null,
|
||||
};
|
||||
|
||||
const ktdWrap = (msg: string, game: Game = "iidx", version = null): any => ({
|
||||
importType: "file/batch-manual",
|
||||
message: new RegExp(EscapeStringRegexp(msg), "u"),
|
||||
converterContext: { game, service: "foo", version },
|
||||
data: {}, // any under t.match rules.
|
||||
importType: "file/batch-manual",
|
||||
message: new RegExp(EscapeStringRegexp(msg), "u"),
|
||||
converterContext: { game, service: "foo", version },
|
||||
data: {}, // any under t.match rules.
|
||||
});
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
@@ -36,361 +36,361 @@ const logger = CreateLogCtx(__filename);
|
||||
const importType = "file/batch-manual" as const;
|
||||
|
||||
t.test("#ResolveMatchTypeToKTData", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should resolve for the songID if the matchType is songID", async (t) => {
|
||||
const res = await ResolveMatchTypeToKTData(
|
||||
baseBatchManualScore,
|
||||
context,
|
||||
importType,
|
||||
logger
|
||||
);
|
||||
t.test("Should resolve for the songID if the matchType is songID", async (t) => {
|
||||
const res = await ResolveMatchTypeToKTData(
|
||||
baseBatchManualScore,
|
||||
context,
|
||||
importType,
|
||||
logger
|
||||
);
|
||||
|
||||
t.hasStrict(
|
||||
res,
|
||||
{ song: { id: 1 }, chart: Testing511SPA },
|
||||
"Should return the right song and chart."
|
||||
);
|
||||
t.hasStrict(
|
||||
res,
|
||||
{ song: { id: 1 }, chart: Testing511SPA },
|
||||
"Should return the right song and chart."
|
||||
);
|
||||
|
||||
t.rejects(
|
||||
() =>
|
||||
ResolveMatchTypeToKTData(
|
||||
// @ts-expect-error bad
|
||||
deepmerge(baseBatchManualScore, { identifier: "90000" }),
|
||||
context,
|
||||
importType,
|
||||
logger
|
||||
),
|
||||
ktdWrap("Cannot find song with songID 90000")
|
||||
);
|
||||
t.rejects(
|
||||
() =>
|
||||
ResolveMatchTypeToKTData(
|
||||
// @ts-expect-error bad
|
||||
deepmerge(baseBatchManualScore, { identifier: "90000" }),
|
||||
context,
|
||||
importType,
|
||||
logger
|
||||
),
|
||||
ktdWrap("Cannot find song with songID 90000")
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should resolve for the song title if the matchType is songTitle", async (t) => {
|
||||
const res = await ResolveMatchTypeToKTData(
|
||||
deepmerge(baseBatchManualScore, { matchType: "songTitle", identifier: "5.1.1." }),
|
||||
context,
|
||||
importType,
|
||||
logger
|
||||
);
|
||||
t.test("Should resolve for the song title if the matchType is songTitle", async (t) => {
|
||||
const res = await ResolveMatchTypeToKTData(
|
||||
deepmerge(baseBatchManualScore, { matchType: "songTitle", identifier: "5.1.1." }),
|
||||
context,
|
||||
importType,
|
||||
logger
|
||||
);
|
||||
|
||||
t.hasStrict(
|
||||
res,
|
||||
{ song: { id: 1 }, chart: Testing511SPA },
|
||||
"Should return the right song and chart."
|
||||
);
|
||||
t.hasStrict(
|
||||
res,
|
||||
{ song: { id: 1 }, chart: Testing511SPA },
|
||||
"Should return the right song and chart."
|
||||
);
|
||||
|
||||
t.rejects(
|
||||
() =>
|
||||
ResolveMatchTypeToKTData(
|
||||
deepmerge(baseBatchManualScore, {
|
||||
matchType: "songTitle",
|
||||
identifier: "INVALID_TITLE",
|
||||
}),
|
||||
context,
|
||||
importType,
|
||||
logger
|
||||
),
|
||||
ktdWrap("Cannot find song with title INVALID_TITLE")
|
||||
);
|
||||
t.rejects(
|
||||
() =>
|
||||
ResolveMatchTypeToKTData(
|
||||
deepmerge(baseBatchManualScore, {
|
||||
matchType: "songTitle",
|
||||
identifier: "INVALID_TITLE",
|
||||
}),
|
||||
context,
|
||||
importType,
|
||||
logger
|
||||
),
|
||||
ktdWrap("Cannot find song with title INVALID_TITLE")
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should resolve for the bms chartHash if the matchType is bmsChartHash", async (t) => {
|
||||
const GAZER17MD5 = "38616b85332037cc12924f2ae2840262";
|
||||
const GAZER17SHA256 = "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d";
|
||||
t.test("Should resolve for the bms chartHash if the matchType is bmsChartHash", async (t) => {
|
||||
const GAZER17MD5 = "38616b85332037cc12924f2ae2840262";
|
||||
const GAZER17SHA256 = "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d";
|
||||
|
||||
const gazerSong = GetKTDataJSON("./tachi/bms-gazer-song.json");
|
||||
const gazerChart = GetKTDataJSON("./tachi/bms-gazer-chart.json");
|
||||
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" });
|
||||
|
||||
const resMD5 = await ResolveMatchTypeToKTData(
|
||||
deepmerge(baseBatchManualScore, {
|
||||
matchType: "bmsChartHash",
|
||||
identifier: GAZER17MD5,
|
||||
}),
|
||||
bmsContext,
|
||||
importType,
|
||||
logger
|
||||
);
|
||||
const resMD5 = await ResolveMatchTypeToKTData(
|
||||
deepmerge(baseBatchManualScore, {
|
||||
matchType: "bmsChartHash",
|
||||
identifier: GAZER17MD5,
|
||||
}),
|
||||
bmsContext,
|
||||
importType,
|
||||
logger
|
||||
);
|
||||
|
||||
t.hasStrict(
|
||||
resMD5,
|
||||
{ song: gazerSong, chart: gazerChart },
|
||||
"Should return the right song and chart."
|
||||
);
|
||||
t.hasStrict(
|
||||
resMD5,
|
||||
{ song: gazerSong, chart: gazerChart },
|
||||
"Should return the right song and chart."
|
||||
);
|
||||
|
||||
const resSHA256 = await ResolveMatchTypeToKTData(
|
||||
deepmerge(baseBatchManualScore, {
|
||||
matchType: "bmsChartHash",
|
||||
identifier: GAZER17SHA256,
|
||||
}),
|
||||
bmsContext,
|
||||
importType,
|
||||
logger
|
||||
);
|
||||
const resSHA256 = await ResolveMatchTypeToKTData(
|
||||
deepmerge(baseBatchManualScore, {
|
||||
matchType: "bmsChartHash",
|
||||
identifier: GAZER17SHA256,
|
||||
}),
|
||||
bmsContext,
|
||||
importType,
|
||||
logger
|
||||
);
|
||||
|
||||
t.hasStrict(
|
||||
resSHA256,
|
||||
{ song: gazerSong, chart: gazerChart },
|
||||
"Should return the right song and chart."
|
||||
);
|
||||
t.hasStrict(
|
||||
resSHA256,
|
||||
{ song: gazerSong, chart: gazerChart },
|
||||
"Should return the right song and chart."
|
||||
);
|
||||
|
||||
t.rejects(
|
||||
() =>
|
||||
ResolveMatchTypeToKTData(
|
||||
deepmerge(baseBatchManualScore, {
|
||||
matchType: "bmsChartHash",
|
||||
identifier: "bad_hash",
|
||||
}),
|
||||
bmsContext,
|
||||
importType,
|
||||
logger
|
||||
),
|
||||
ktdWrap("Cannot find chart for hash ", "bms")
|
||||
);
|
||||
t.rejects(
|
||||
() =>
|
||||
ResolveMatchTypeToKTData(
|
||||
deepmerge(baseBatchManualScore, {
|
||||
matchType: "bmsChartHash",
|
||||
identifier: "bad_hash",
|
||||
}),
|
||||
bmsContext,
|
||||
importType,
|
||||
logger
|
||||
),
|
||||
ktdWrap("Cannot find chart for hash ", "bms")
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should reject if ddrSongHash is called without game = ddr", (t) => {
|
||||
t.rejects(
|
||||
ResolveMatchTypeToKTData(
|
||||
deepmerge(baseBatchManualScore, {
|
||||
matchType: "ddrSongHash",
|
||||
playtype: "SP",
|
||||
difficulty: "DIFFICULT",
|
||||
}),
|
||||
context,
|
||||
importType,
|
||||
logger
|
||||
),
|
||||
new InvalidScoreFailure("Cannot use ddrSongHash lookup on iidx.") as any
|
||||
);
|
||||
t.test("Should reject if ddrSongHash is called without game = ddr", (t) => {
|
||||
t.rejects(
|
||||
ResolveMatchTypeToKTData(
|
||||
deepmerge(baseBatchManualScore, {
|
||||
matchType: "ddrSongHash",
|
||||
playtype: "SP",
|
||||
difficulty: "DIFFICULT",
|
||||
}),
|
||||
context,
|
||||
importType,
|
||||
logger
|
||||
),
|
||||
new InvalidScoreFailure("Cannot use ddrSongHash lookup on iidx.") as any
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should resolve for the ddr songHash if the matchType is ddrSongHash", async (t) => {
|
||||
const PUTY_ID = "DQlQ1DlPbq900oqdOo8l0d6I1lIOl99l";
|
||||
t.test("Should resolve for the ddr songHash if the matchType is ddrSongHash", async (t) => {
|
||||
const PUTY_ID = "DQlQ1DlPbq900oqdOo8l0d6I1lIOl99l";
|
||||
|
||||
const res = await ResolveMatchTypeToKTData(
|
||||
deepmerge(baseBatchManualScore, {
|
||||
matchType: "ddrSongHash",
|
||||
identifier: PUTY_ID,
|
||||
playtype: "SP",
|
||||
difficulty: "DIFFICULT",
|
||||
}),
|
||||
deepmerge(context, { game: "ddr" }),
|
||||
importType,
|
||||
logger
|
||||
);
|
||||
const res = await ResolveMatchTypeToKTData(
|
||||
deepmerge(baseBatchManualScore, {
|
||||
matchType: "ddrSongHash",
|
||||
identifier: PUTY_ID,
|
||||
playtype: "SP",
|
||||
difficulty: "DIFFICULT",
|
||||
}),
|
||||
deepmerge(context, { game: "ddr" }),
|
||||
importType,
|
||||
logger
|
||||
);
|
||||
|
||||
t.hasStrict(
|
||||
res,
|
||||
{
|
||||
song: { id: 10 },
|
||||
chart: { chartID: "48024d36bbe76c9fed09c3ffdc19412925d1efd3" },
|
||||
},
|
||||
"Should return the right song and chart."
|
||||
);
|
||||
t.hasStrict(
|
||||
res,
|
||||
{
|
||||
song: { id: 10 },
|
||||
chart: { chartID: "48024d36bbe76c9fed09c3ffdc19412925d1efd3" },
|
||||
},
|
||||
"Should return the right song and chart."
|
||||
);
|
||||
|
||||
t.rejects(
|
||||
() =>
|
||||
ResolveMatchTypeToKTData(
|
||||
deepmerge(baseBatchManualScore, {
|
||||
matchType: "ddrSongHash",
|
||||
identifier: "Bad_ID",
|
||||
playtype: "SP",
|
||||
difficulty: "EXPERT",
|
||||
}),
|
||||
deepmerge(context, { game: "ddr" }),
|
||||
importType,
|
||||
logger
|
||||
),
|
||||
ktdWrap("Cannot find chart for songHash", "ddr")
|
||||
);
|
||||
t.rejects(
|
||||
() =>
|
||||
ResolveMatchTypeToKTData(
|
||||
deepmerge(baseBatchManualScore, {
|
||||
matchType: "ddrSongHash",
|
||||
identifier: "Bad_ID",
|
||||
playtype: "SP",
|
||||
difficulty: "EXPERT",
|
||||
}),
|
||||
deepmerge(context, { game: "ddr" }),
|
||||
importType,
|
||||
logger
|
||||
),
|
||||
ktdWrap("Cannot find chart for songHash", "ddr")
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should trigger failsave if invalid matchType is provided.", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ResolveMatchTypeToKTData(
|
||||
deepmerge(baseBatchManualScore, {
|
||||
matchType: "BAD_MATCHTYPE",
|
||||
}),
|
||||
context,
|
||||
importType,
|
||||
logger
|
||||
),
|
||||
new InvalidScoreFailure(`Invalid matchType BAD_MATCHTYPE`) as any
|
||||
);
|
||||
t.test("Should trigger failsave if invalid matchType is provided.", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ResolveMatchTypeToKTData(
|
||||
deepmerge(baseBatchManualScore, {
|
||||
matchType: "BAD_MATCHTYPE",
|
||||
}),
|
||||
context,
|
||||
importType,
|
||||
logger
|
||||
),
|
||||
new InvalidScoreFailure(`Invalid matchType BAD_MATCHTYPE`) as any
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#ResolveChartFromSong", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should return the chart for the song + ptdf", async (t) => {
|
||||
const res = await ResolveChartFromSong(
|
||||
Testing511Song,
|
||||
baseBatchManualScore, // has playtype + diff
|
||||
{ game: "iidx", service: "foo", version: null },
|
||||
importType
|
||||
);
|
||||
t.test("Should return the chart for the song + ptdf", async (t) => {
|
||||
const res = await ResolveChartFromSong(
|
||||
Testing511Song,
|
||||
baseBatchManualScore, // has playtype + diff
|
||||
{ game: "iidx", service: "foo", version: null },
|
||||
importType
|
||||
);
|
||||
|
||||
t.hasStrict(res, Testing511SPA);
|
||||
t.hasStrict(res, Testing511SPA);
|
||||
|
||||
t.end();
|
||||
});
|
||||
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.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.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 },
|
||||
importType
|
||||
),
|
||||
new InvalidScoreFailure(
|
||||
`Missing 'difficulty' field, but was necessary for this lookup.`
|
||||
) as any
|
||||
);
|
||||
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 },
|
||||
importType
|
||||
),
|
||||
new InvalidScoreFailure(
|
||||
`Missing 'difficulty' field, but was necessary for this lookup.`
|
||||
) as any
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should throw an error if an invalid difficulty is provided.", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ResolveChartFromSong(
|
||||
Testing511Song,
|
||||
// @ts-expect-error faulty deepmerge types
|
||||
deepmerge(baseBatchManualScore, { difficulty: "NOT_VALID_DIFFICULTY" }),
|
||||
{ game: "iidx", service: "foo", version: null },
|
||||
importType
|
||||
),
|
||||
new InvalidScoreFailure(
|
||||
`Invalid Difficulty for iidx SP - Expected any of BEGINNER, NORMAL, HYPER, ANOTHER, LEGGENDARIA`
|
||||
) as any
|
||||
);
|
||||
t.test("Should throw an error if an invalid difficulty is provided.", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ResolveChartFromSong(
|
||||
Testing511Song,
|
||||
// @ts-expect-error faulty deepmerge types
|
||||
deepmerge(baseBatchManualScore, { difficulty: "NOT_VALID_DIFFICULTY" }),
|
||||
{ game: "iidx", service: "foo", version: null },
|
||||
importType
|
||||
),
|
||||
new InvalidScoreFailure(
|
||||
`Invalid Difficulty for iidx SP - Expected any of BEGINNER, NORMAL, HYPER, ANOTHER, LEGGENDARIA`
|
||||
) as any
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should throw an error if no chart could be found.", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ResolveChartFromSong(
|
||||
Testing511Song,
|
||||
// @ts-expect-error faulty deepmerge types
|
||||
deepmerge(baseBatchManualScore, { difficulty: "LEGGENDARIA" }), // 511 has no legg (yet, lol)
|
||||
{ game: "iidx", service: "foo", version: null },
|
||||
importType
|
||||
),
|
||||
ktdWrap("Cannot find chart for 5.1.1. (SP LEGGENDARIA)")
|
||||
);
|
||||
t.test("Should throw an error if no chart could be found.", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ResolveChartFromSong(
|
||||
Testing511Song,
|
||||
// @ts-expect-error faulty deepmerge types
|
||||
deepmerge(baseBatchManualScore, { difficulty: "LEGGENDARIA" }), // 511 has no legg (yet, lol)
|
||||
{ game: "iidx", service: "foo", version: null },
|
||||
importType
|
||||
),
|
||||
ktdWrap("Cannot find chart for 5.1.1. (SP LEGGENDARIA)")
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should successfully lookup if version is provided.", async (t) => {
|
||||
const res = await ResolveChartFromSong(
|
||||
Testing511Song,
|
||||
baseBatchManualScore,
|
||||
{
|
||||
game: "iidx",
|
||||
service: "foo",
|
||||
version: "27",
|
||||
},
|
||||
importType
|
||||
);
|
||||
t.test("Should successfully lookup if version is provided.", async (t) => {
|
||||
const res = await ResolveChartFromSong(
|
||||
Testing511Song,
|
||||
baseBatchManualScore,
|
||||
{
|
||||
game: "iidx",
|
||||
service: "foo",
|
||||
version: "27",
|
||||
},
|
||||
importType
|
||||
);
|
||||
|
||||
t.hasStrict(res, Testing511SPA);
|
||||
t.hasStrict(res, Testing511SPA);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#ConverterFn", (t) => {
|
||||
t.test("Should produce a DryScore", async (t) => {
|
||||
const res = await ConverterBatchManual(
|
||||
baseBatchManualScore,
|
||||
{ game: "iidx", service: "foo", version: null },
|
||||
importType,
|
||||
logger
|
||||
);
|
||||
t.test("Should produce a DryScore", async (t) => {
|
||||
const res = await ConverterBatchManual(
|
||||
baseBatchManualScore,
|
||||
{ game: "iidx", service: "foo", version: null },
|
||||
importType,
|
||||
logger
|
||||
);
|
||||
|
||||
t.hasStrict(res, {
|
||||
chart: Testing511SPA,
|
||||
song: { id: 1 },
|
||||
dryScore: {
|
||||
game: "iidx",
|
||||
service: "foo (BATCH-MANUAL)",
|
||||
comment: null,
|
||||
importType: "file/batch-manual",
|
||||
timeAchieved: null,
|
||||
scoreData: {
|
||||
lamp: "HARD CLEAR",
|
||||
score: 500,
|
||||
grade: "E",
|
||||
// percent: 31.5, -- ish, FPA is hard.
|
||||
hitData: {},
|
||||
hitMeta: {},
|
||||
},
|
||||
scoreMeta: {},
|
||||
},
|
||||
});
|
||||
t.hasStrict(res, {
|
||||
chart: Testing511SPA,
|
||||
song: { id: 1 },
|
||||
dryScore: {
|
||||
game: "iidx",
|
||||
service: "foo (BATCH-MANUAL)",
|
||||
comment: null,
|
||||
importType: "file/batch-manual",
|
||||
timeAchieved: null,
|
||||
scoreData: {
|
||||
lamp: "HARD CLEAR",
|
||||
score: 500,
|
||||
grade: "E",
|
||||
// percent: 31.5, -- ish, FPA is hard.
|
||||
hitData: {},
|
||||
hitMeta: {},
|
||||
},
|
||||
scoreMeta: {},
|
||||
},
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should reject a score with > 100%", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ConverterBatchManual(
|
||||
// @ts-expect-error broken deepmerge
|
||||
deepmerge(baseBatchManualScore, { score: 2000 }),
|
||||
{ game: "iidx", service: "foo", version: null },
|
||||
importType,
|
||||
logger
|
||||
),
|
||||
{ message: /Invalid percent/u } as any
|
||||
);
|
||||
t.test("Should reject a score with > 100%", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ConverterBatchManual(
|
||||
// @ts-expect-error broken deepmerge
|
||||
deepmerge(baseBatchManualScore, { score: 2000 }),
|
||||
{ game: "iidx", service: "foo", version: null },
|
||||
importType,
|
||||
logger
|
||||
),
|
||||
{ message: /Invalid percent/u } as any
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { BatchManualContext, BatchManualScore } from "./types";
|
||||
import { AnyChartDocument, AnySongDocument, ImportTypes } from "tachi-common";
|
||||
import {
|
||||
InternalFailure,
|
||||
InvalidScoreFailure,
|
||||
KTDataNotFoundFailure,
|
||||
InternalFailure,
|
||||
InvalidScoreFailure,
|
||||
KTDataNotFoundFailure,
|
||||
} from "../../../framework/common/converter-failures";
|
||||
import { FindSongOnID, FindSongOnTitleInsensitive } from "../../../../../utils/queries/songs";
|
||||
import {
|
||||
FindBMSChartOnHash,
|
||||
FindChartWithPTDF,
|
||||
FindChartWithPTDFVersion,
|
||||
FindDDRChartOnSongHash,
|
||||
FindBMSChartOnHash,
|
||||
FindChartWithPTDF,
|
||||
FindChartWithPTDFVersion,
|
||||
FindDDRChartOnSongHash,
|
||||
} from "../../../../../utils/queries/charts";
|
||||
import {
|
||||
AssertStrAsDifficulty,
|
||||
AssertStrAsPositiveInt,
|
||||
AssertStrAsDifficulty,
|
||||
AssertStrAsPositiveInt,
|
||||
} from "../../../framework/common/string-asserts";
|
||||
import { GenericGetGradeAndPercent } from "../../../framework/common/score-utils";
|
||||
import { KtLogger } from "../../../../logger/logger";
|
||||
@@ -28,207 +28,207 @@ import { ConverterFunction } from "../types";
|
||||
* @returns A BatchManualScore Converter.
|
||||
*/
|
||||
export const ConverterBatchManual: ConverterFunction<BatchManualScore, BatchManualContext> = async (
|
||||
data,
|
||||
context,
|
||||
importType,
|
||||
logger
|
||||
data,
|
||||
context,
|
||||
importType,
|
||||
logger
|
||||
) => {
|
||||
const game = context.game;
|
||||
const game = context.game;
|
||||
|
||||
const { song, chart } = await ResolveMatchTypeToKTData(data, context, importType, logger);
|
||||
const { song, chart } = await ResolveMatchTypeToKTData(data, context, importType, logger);
|
||||
|
||||
const { percent, grade } = GenericGetGradeAndPercent(context.game, data.score, chart);
|
||||
const { percent, grade } = GenericGetGradeAndPercent(context.game, data.score, chart);
|
||||
|
||||
let service = context.service;
|
||||
let service = context.service;
|
||||
|
||||
if (importType === "ir/direct-manual") {
|
||||
service += " (DIRECT-MANUAL)";
|
||||
} else if (importType === "file/batch-manual") {
|
||||
service += " (BATCH-MANUAL)";
|
||||
}
|
||||
if (importType === "ir/direct-manual") {
|
||||
service += " (DIRECT-MANUAL)";
|
||||
} else if (importType === "file/batch-manual") {
|
||||
service += " (BATCH-MANUAL)";
|
||||
}
|
||||
|
||||
const dryScore: DryScore = {
|
||||
game: game,
|
||||
service,
|
||||
comment: data.comment ?? null,
|
||||
importType,
|
||||
timeAchieved: data.timeAchieved ?? null,
|
||||
scoreData: {
|
||||
lamp: data.lamp,
|
||||
score: data.score,
|
||||
grade,
|
||||
percent,
|
||||
hitData: data.hitData ?? {},
|
||||
hitMeta: data.hitMeta ?? {},
|
||||
},
|
||||
scoreMeta: {}, // @todo #74
|
||||
};
|
||||
const dryScore: DryScore = {
|
||||
game: game,
|
||||
service,
|
||||
comment: data.comment ?? null,
|
||||
importType,
|
||||
timeAchieved: data.timeAchieved ?? null,
|
||||
scoreData: {
|
||||
lamp: data.lamp,
|
||||
score: data.score,
|
||||
grade,
|
||||
percent,
|
||||
hitData: data.hitData ?? {},
|
||||
hitMeta: data.hitMeta ?? {},
|
||||
},
|
||||
scoreMeta: {}, // @todo #74
|
||||
};
|
||||
|
||||
return {
|
||||
chart,
|
||||
song,
|
||||
dryScore,
|
||||
};
|
||||
return {
|
||||
chart,
|
||||
song,
|
||||
dryScore,
|
||||
};
|
||||
};
|
||||
|
||||
export async function ResolveMatchTypeToKTData(
|
||||
data: BatchManualScore,
|
||||
context: BatchManualContext,
|
||||
importType: ImportTypes,
|
||||
logger: KtLogger
|
||||
data: BatchManualScore,
|
||||
context: BatchManualContext,
|
||||
importType: ImportTypes,
|
||||
logger: KtLogger
|
||||
): Promise<{ song: AnySongDocument; chart: AnyChartDocument }> {
|
||||
const game = context.game;
|
||||
const game = context.game;
|
||||
|
||||
if (data.matchType === "bmsChartHash" || data.matchType === "hash") {
|
||||
if (game !== "bms") {
|
||||
throw new InvalidScoreFailure(`Cannot use bmsChartHash lookup on ${game}.`);
|
||||
}
|
||||
if (data.matchType === "bmsChartHash" || data.matchType === "hash") {
|
||||
if (game !== "bms") {
|
||||
throw new InvalidScoreFailure(`Cannot use bmsChartHash lookup on ${game}.`);
|
||||
}
|
||||
|
||||
const chart = await FindBMSChartOnHash(data.identifier);
|
||||
const chart = await FindBMSChartOnHash(data.identifier);
|
||||
|
||||
if (!chart) {
|
||||
throw new KTDataNotFoundFailure(
|
||||
`Cannot find chart for hash ${data.identifier}.`,
|
||||
importType,
|
||||
data,
|
||||
context
|
||||
);
|
||||
}
|
||||
if (!chart) {
|
||||
throw new KTDataNotFoundFailure(
|
||||
`Cannot find chart for hash ${data.identifier}.`,
|
||||
importType,
|
||||
data,
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
const song = await FindSongOnID(game, chart.songID);
|
||||
const song = await FindSongOnID(game, chart.songID);
|
||||
|
||||
if (!song) {
|
||||
logger.severe(`BMS songID ${chart.songID} has charts but no parent song.`);
|
||||
throw new InternalFailure(`BMS songID ${chart.songID} has charts but no parent song.`);
|
||||
}
|
||||
if (!song) {
|
||||
logger.severe(`BMS songID ${chart.songID} has charts but no parent song.`);
|
||||
throw new InternalFailure(`BMS songID ${chart.songID} has charts but no parent song.`);
|
||||
}
|
||||
|
||||
return { chart, song };
|
||||
} else if (data.matchType === "ddrSongHash" || data.matchType === "songHash") {
|
||||
if (game !== "ddr") {
|
||||
throw new InvalidScoreFailure(`Cannot use ddrSongHash lookup on ${game}.`);
|
||||
}
|
||||
return { chart, song };
|
||||
} else if (data.matchType === "ddrSongHash" || data.matchType === "songHash") {
|
||||
if (game !== "ddr") {
|
||||
throw new InvalidScoreFailure(`Cannot use ddrSongHash lookup on ${game}.`);
|
||||
}
|
||||
|
||||
if (!data.difficulty) {
|
||||
throw new InvalidScoreFailure(
|
||||
`Missing 'difficulty' field, but is needed for ddrSongHash lookup.`
|
||||
);
|
||||
}
|
||||
if (!data.difficulty) {
|
||||
throw new InvalidScoreFailure(
|
||||
`Missing 'difficulty' field, but is needed for ddrSongHash lookup.`
|
||||
);
|
||||
}
|
||||
|
||||
if (!data.playtype) {
|
||||
throw new InvalidScoreFailure(
|
||||
`Missing 'playtype' field, but is needed for ddrSongHash lookup.`
|
||||
);
|
||||
}
|
||||
if (!data.playtype) {
|
||||
throw new InvalidScoreFailure(
|
||||
`Missing 'playtype' field, but is needed for ddrSongHash lookup.`
|
||||
);
|
||||
}
|
||||
|
||||
const difficulty = AssertStrAsDifficulty(data.difficulty, game, data.playtype);
|
||||
const difficulty = AssertStrAsDifficulty(data.difficulty, game, data.playtype);
|
||||
|
||||
const chart = await FindDDRChartOnSongHash(data.identifier, data.playtype, difficulty);
|
||||
const chart = await FindDDRChartOnSongHash(data.identifier, data.playtype, difficulty);
|
||||
|
||||
if (!chart) {
|
||||
throw new KTDataNotFoundFailure(
|
||||
`Cannot find chart for songHash ${data.identifier} (${data.playtype} ${difficulty}).`,
|
||||
importType,
|
||||
data,
|
||||
context
|
||||
);
|
||||
}
|
||||
if (!chart) {
|
||||
throw new KTDataNotFoundFailure(
|
||||
`Cannot find chart for songHash ${data.identifier} (${data.playtype} ${difficulty}).`,
|
||||
importType,
|
||||
data,
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
const song = await FindSongOnID(game, chart.songID);
|
||||
const song = await FindSongOnID(game, chart.songID);
|
||||
|
||||
if (!song) {
|
||||
logger.severe(`DDR songID ${chart.songID} has charts but no parent song.`);
|
||||
throw new InternalFailure(`DDR songID ${chart.songID} has charts but no parent song.`);
|
||||
}
|
||||
if (!song) {
|
||||
logger.severe(`DDR songID ${chart.songID} has charts but no parent song.`);
|
||||
throw new InternalFailure(`DDR songID ${chart.songID} has charts but no parent song.`);
|
||||
}
|
||||
|
||||
return { song, chart };
|
||||
} else if (data.matchType === "kamaitachiSongID" || data.matchType === "songID") {
|
||||
const songID = AssertStrAsPositiveInt(
|
||||
data.identifier,
|
||||
"Invalid songID - must be a stringified positive integer."
|
||||
);
|
||||
return { song, chart };
|
||||
} else if (data.matchType === "kamaitachiSongID" || data.matchType === "songID") {
|
||||
const songID = AssertStrAsPositiveInt(
|
||||
data.identifier,
|
||||
"Invalid songID - must be a stringified positive integer."
|
||||
);
|
||||
|
||||
const song = await FindSongOnID(game, songID);
|
||||
const song = await FindSongOnID(game, songID);
|
||||
|
||||
if (!song) {
|
||||
throw new KTDataNotFoundFailure(
|
||||
`Cannot find song with songID ${data.identifier}.`,
|
||||
importType,
|
||||
data,
|
||||
context
|
||||
);
|
||||
}
|
||||
if (!song) {
|
||||
throw new KTDataNotFoundFailure(
|
||||
`Cannot find song with songID ${data.identifier}.`,
|
||||
importType,
|
||||
data,
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
const chart = await ResolveChartFromSong(song, data, context, importType);
|
||||
const chart = await ResolveChartFromSong(song, data, context, importType);
|
||||
|
||||
return { song, chart };
|
||||
} else if (data.matchType === "songTitle" || data.matchType === "title") {
|
||||
const song = await FindSongOnTitleInsensitive(game, data.identifier);
|
||||
return { song, chart };
|
||||
} else if (data.matchType === "songTitle" || data.matchType === "title") {
|
||||
const song = await FindSongOnTitleInsensitive(game, data.identifier);
|
||||
|
||||
if (!song) {
|
||||
throw new KTDataNotFoundFailure(
|
||||
`Cannot find song with title ${data.identifier}.`,
|
||||
importType,
|
||||
data,
|
||||
context
|
||||
);
|
||||
}
|
||||
if (!song) {
|
||||
throw new KTDataNotFoundFailure(
|
||||
`Cannot find song with title ${data.identifier}.`,
|
||||
importType,
|
||||
data,
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
const chart = await ResolveChartFromSong(song, data, context, importType);
|
||||
const chart = await ResolveChartFromSong(song, data, context, importType);
|
||||
|
||||
return { song, chart };
|
||||
}
|
||||
return { song, chart };
|
||||
}
|
||||
|
||||
logger.error(
|
||||
`Invalid matchType ${data.matchType} ended up in conversion - should have been rejected by prudence?`
|
||||
);
|
||||
// really, this could be a larger error. - it's an internal failure because prudence should reject this.
|
||||
throw new InvalidScoreFailure(`Invalid matchType ${data.matchType}.`);
|
||||
logger.error(
|
||||
`Invalid matchType ${data.matchType} ended up in conversion - should have been rejected by prudence?`
|
||||
);
|
||||
// really, this could be a larger error. - it's an internal failure because prudence should reject this.
|
||||
throw new InvalidScoreFailure(`Invalid matchType ${data.matchType}.`);
|
||||
}
|
||||
|
||||
export async function ResolveChartFromSong(
|
||||
song: AnySongDocument,
|
||||
data: BatchManualScore,
|
||||
context: BatchManualContext,
|
||||
importType: ImportTypes
|
||||
song: AnySongDocument,
|
||||
data: BatchManualScore,
|
||||
context: BatchManualContext,
|
||||
importType: ImportTypes
|
||||
) {
|
||||
const game = context.game;
|
||||
const game = context.game;
|
||||
|
||||
if (!data.difficulty) {
|
||||
throw new InvalidScoreFailure(
|
||||
`Missing 'difficulty' field, but was necessary for this lookup.`
|
||||
);
|
||||
}
|
||||
if (!data.difficulty) {
|
||||
throw new InvalidScoreFailure(
|
||||
`Missing 'difficulty' field, but was necessary for this lookup.`
|
||||
);
|
||||
}
|
||||
|
||||
if (!data.playtype) {
|
||||
throw new InvalidScoreFailure(
|
||||
`Missing 'playtype' field, but was necessary for this lookup.`
|
||||
);
|
||||
}
|
||||
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, data.playtype);
|
||||
|
||||
let chart;
|
||||
let chart;
|
||||
|
||||
if (context.version) {
|
||||
chart = await FindChartWithPTDFVersion(
|
||||
game,
|
||||
song.id,
|
||||
data.playtype,
|
||||
difficulty,
|
||||
context.version
|
||||
);
|
||||
} else {
|
||||
chart = await FindChartWithPTDF(game, song.id, data.playtype, difficulty);
|
||||
}
|
||||
if (context.version) {
|
||||
chart = await FindChartWithPTDFVersion(
|
||||
game,
|
||||
song.id,
|
||||
data.playtype,
|
||||
difficulty,
|
||||
context.version
|
||||
);
|
||||
} else {
|
||||
chart = await FindChartWithPTDF(game, song.id, data.playtype, difficulty);
|
||||
}
|
||||
|
||||
if (!chart) {
|
||||
throw new KTDataNotFoundFailure(
|
||||
`Cannot find chart for ${song.title} (${data.playtype} ${difficulty})`,
|
||||
importType,
|
||||
data,
|
||||
context
|
||||
);
|
||||
}
|
||||
if (!chart) {
|
||||
throw new KTDataNotFoundFailure(
|
||||
`Cannot find chart for ${song.title} (${data.playtype} ${difficulty})`,
|
||||
importType,
|
||||
data,
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
return chart;
|
||||
return chart;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user