feat(stats): log unknown categories in updateStats
All checks were successful
Build / build (18) (pull_request) Successful in 39s
Build / build (22) (pull_request) Successful in 55s
Build / build (20) (pull_request) Successful in 1m9s

This commit is contained in:
AMelonInsideLemon 2025-02-11 09:34:33 +01:00
parent 30061fb0e3
commit 5075fafa6d
5 changed files with 404 additions and 261 deletions

View File

@ -1,14 +1,15 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getStats, uploadStats } from "@/src/services/statsService"; import { getStats, updateStats } from "@/src/services/statsService";
import { IStatsUpload } from "@/src/types/statTypes"; import { IStatsUpdate } from "@/src/types/statTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
const uploadController: RequestHandler = async (req, res) => { const uploadController: RequestHandler = async (req, res) => {
const payload = getJSONfromString<IStatsUpload>(String(req.body)); // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { PS, ...payload } = getJSONfromString<IStatsUpdate>(String(req.body));
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const playerStats = await getStats(accountId); const playerStats = await getStats(accountId);
await uploadStats(playerStats, payload); await updateStats(playerStats, payload);
res.status(200).end(); res.status(200).end();
}; };

View File

@ -5,14 +5,14 @@ import allScans from "@/static/fixed_responses/allScans.json";
import { ExportEnemies } from "warframe-public-export-plus"; import { ExportEnemies } from "warframe-public-export-plus";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getStats } from "@/src/services/statsService"; import { getStats } from "@/src/services/statsService";
import { IStatsView } from "@/src/types/statTypes"; import { IStatsClient } from "@/src/types/statTypes";
const viewController: RequestHandler = async (req, res) => { const viewController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "XPInfo"); const inventory = await getInventory(accountId, "XPInfo");
const playerStats = await getStats(accountId); const playerStats = await getStats(accountId);
const responseJson: IStatsView = playerStats.toJSON(); const responseJson = playerStats.toJSON() as IStatsClient;
responseJson.Weapons ??= []; responseJson.Weapons ??= [];
for (const item of inventory.XPInfo) { for (const item of inventory.XPInfo) {
const weaponIndex = responseJson.Weapons.findIndex(element => element.type == item.ItemType); const weaponIndex = responseJson.Weapons.findIndex(element => element.type == item.ItemType);

View File

@ -1,5 +1,5 @@
import { Document, Schema, Types, model } from "mongoose"; import { Document, Schema, Types, model } from "mongoose";
import { IEnemy, IMission, IScan, ITutorial, IAbility, IWeapon, IStatsDatabase } from "@/src/types/statTypes"; import { IEnemy, IMission, IScan, ITutorial, IAbility, IWeapon, IStatsDatabase, IRace } from "@/src/types/statTypes";
const abilitySchema = new Schema<IAbility>( const abilitySchema = new Schema<IAbility>(
{ {
@ -58,6 +58,13 @@ const weaponSchema = new Schema<IWeapon>(
{ _id: false } { _id: false }
); );
const raceSchema = new Schema<IRace>(
{
highScore: Number
},
{ _id: false }
);
const statsSchema = new Schema<IStatsDatabase>({ const statsSchema = new Schema<IStatsDatabase>({
accountOwnerId: { type: Schema.Types.ObjectId, required: true }, accountOwnerId: { type: Schema.Types.ObjectId, required: true },
CiphersSolved: Number, CiphersSolved: Number,
@ -69,6 +76,8 @@ const statsSchema = new Schema<IStatsDatabase>({
MissionsCompleted: Number, MissionsCompleted: Number,
MissionsQuit: Number, MissionsQuit: Number,
MissionsFailed: Number, MissionsFailed: Number,
MissionsInterrupted: Number,
MissionsDumped: Number,
TimePlayedSec: Number, TimePlayedSec: Number,
PickupCount: Number, PickupCount: Number,
Tutorial: { type: Map, of: tutorialSchema, default: {} }, Tutorial: { type: Map, of: tutorialSchema, default: {} },
@ -81,7 +90,8 @@ const statsSchema = new Schema<IStatsDatabase>({
Missions: { type: [missionSchema], default: [] }, Missions: { type: [missionSchema], default: [] },
Deaths: Number, Deaths: Number,
HealCount: Number, HealCount: Number,
ReviveCount: Number ReviveCount: Number,
Races: { type: Map, of: raceSchema, default: {} }
}); });
statsSchema.set("toJSON", { statsSchema.set("toJSON", {

View File

@ -1,5 +1,15 @@
import { Stats, TStatsDatabaseDocument } from "@/src/models/statsModel"; import { Stats, TStatsDatabaseDocument } from "@/src/models/statsModel";
import { IStatsUpload } from "@/src/types/statTypes"; import {
IEnemy,
IStatsAdd,
IStatsMax,
IStatsSet,
IStatsTimers,
IStatsUpdate,
IUploadEntry,
IWeapon
} from "@/src/types/statTypes";
import { logger } from "../utils/logger";
export const createStats = async (accountId: string): Promise<TStatsDatabaseDocument> => { export const createStats = async (accountId: string): Promise<TStatsDatabaseDocument> => {
const stats = new Stats({ accountOwnerId: accountId }); const stats = new Stats({ accountOwnerId: accountId });
@ -15,28 +25,16 @@ export const getStats = async (accountOwnerId: string): Promise<TStatsDatabaseDo
return stats; return stats;
}; };
export const uploadStats = async (playerStats: TStatsDatabaseDocument, payload: IStatsUpload): Promise<void> => { export const updateStats = async (playerStats: TStatsDatabaseDocument, payload: IStatsUpdate): Promise<void> => {
if (payload.add) { const unknownCategories: Record<string, string[]> = {};
const {
MISSION_COMPLETE,
PICKUP_ITEM,
SCAN,
USE_ABILITY,
FIRE_WEAPON,
HIT_ENTITY_ITEM,
HEADSHOT_ITEM,
KILL_ENEMY_ITEM,
KILL_ENEMY,
EXECUTE_ENEMY,
HEADSHOT,
DIE,
MELEE_KILL,
INCOME,
CIPHER
} = payload.add;
if (MISSION_COMPLETE) { for (const [action, actionData] of Object.entries(payload)) {
for (const [key, value] of Object.entries(MISSION_COMPLETE)) { switch (action) {
case "add":
for (const [category, data] of Object.entries(actionData as IStatsAdd)) {
switch (category) {
case "MISSION_COMPLETE":
for (const [key, value] of Object.entries(data as IUploadEntry)) {
switch (key) { switch (key) {
case "GS_SUCCESS": case "GS_SUCCESS":
playerStats.MissionsCompleted ??= 0; playerStats.MissionsCompleted ??= 0;
@ -50,234 +48,360 @@ export const uploadStats = async (playerStats: TStatsDatabaseDocument, payload:
playerStats.MissionsFailed ??= 0; playerStats.MissionsFailed ??= 0;
playerStats.MissionsFailed += value; playerStats.MissionsFailed += value;
break; break;
case "GS_INTERRUPTED":
playerStats.MissionsInterrupted ??= 0;
playerStats.MissionsInterrupted += value;
break;
case "GS_DUMPED":
playerStats.MissionsDumped ??= 0;
playerStats.MissionsDumped += value;
break;
default:
if (!ignoredCategories.includes(category)) {
if (!unknownCategories[action]) {
unknownCategories[action] = [];
}
unknownCategories[action].push(category);
}
break;
} }
} }
} break;
if (PICKUP_ITEM) { case "PICKUP_ITEM":
for (const value of Object.values(PICKUP_ITEM)) {
playerStats.PickupCount ??= 0; playerStats.PickupCount ??= 0;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const [_key, value] of Object.entries(data as IUploadEntry)) {
playerStats.PickupCount += value; playerStats.PickupCount += value;
} }
} break;
if (SCAN) { case "SCAN":
playerStats.Scans ??= []; playerStats.Scans ??= [];
for (const [key, scans] of Object.entries(SCAN)) { for (const [type, scans] of Object.entries(data as IUploadEntry)) {
const scan = playerStats.Scans.find(element => element.type === key); const scan = playerStats.Scans.find(element => element.type === type);
if (scan) { if (scan) {
scan.scans ??= 0; scan.scans ??= 0;
scan.scans += scans; scan.scans += scans;
} else { } else {
playerStats.Scans.push({ type: key, scans }); playerStats.Scans.push({ type: type, scans });
}
} }
} }
break;
if (USE_ABILITY) { case "USE_ABILITY":
playerStats.Abilities ??= []; playerStats.Abilities ??= [];
for (const [key, used] of Object.entries(USE_ABILITY)) { for (const [type, used] of Object.entries(data as IUploadEntry)) {
const ability = playerStats.Abilities.find(element => element.type === key); const ability = playerStats.Abilities.find(element => element.type === type);
if (ability) { if (ability) {
ability.used ??= 0; ability.used ??= 0;
ability.used += used; ability.used += used;
} else { } else {
playerStats.Abilities.push({ type: key, used }); playerStats.Abilities.push({ type: type, used });
}
} }
} }
break;
if (FIRE_WEAPON) { case "FIRE_WEAPON":
case "HIT_ENTITY_ITEM":
case "HEADSHOT_ITEM":
case "KILL_ENEMY_ITEM":
playerStats.Weapons ??= []; playerStats.Weapons ??= [];
for (const [key, fired] of Object.entries(FIRE_WEAPON)) { const statKey = {
const weapon = playerStats.Weapons.find(element => element.type === key); FIRE_WEAPON: "fired",
if (weapon) { HIT_ENTITY_ITEM: "hits",
weapon.fired ??= 0; HEADSHOT_ITEM: "headshots",
weapon.fired += fired; KILL_ENEMY_ITEM: "kills"
} else { }[category] as "fired" | "hits" | "headshots" | "kills";
playerStats.Weapons.push({ type: key, fired });
}
}
}
if (HIT_ENTITY_ITEM) { for (const [type, count] of Object.entries(data as IUploadEntry)) {
playerStats.Weapons ??= []; const weapon = playerStats.Weapons.find(element => element.type === type);
for (const [key, hits] of Object.entries(HIT_ENTITY_ITEM)) {
const weapon = playerStats.Weapons.find(element => element.type === key);
if (weapon) { if (weapon) {
weapon.hits ??= 0; weapon[statKey] ??= 0;
weapon.hits += hits; weapon[statKey] += count;
} else { } else {
playerStats.Weapons.push({ type: key, hits }); const newWeapon: IWeapon = { type: type };
} newWeapon[statKey] = count;
playerStats.Weapons.push(newWeapon);
} }
} }
break;
if (HEADSHOT_ITEM) { case "KILL_ENEMY":
playerStats.Weapons ??= []; case "EXECUTE_ENEMY":
for (const [key, headshots] of Object.entries(HEADSHOT_ITEM)) { case "HEADSHOT":
const weapon = playerStats.Weapons.find(element => element.type === key);
if (weapon) {
weapon.headshots ??= 0;
weapon.headshots += headshots;
} else {
playerStats.Weapons.push({ type: key, headshots });
}
}
}
if (KILL_ENEMY_ITEM) {
playerStats.Weapons ??= [];
for (const [key, kills] of Object.entries(KILL_ENEMY_ITEM)) {
const weapon = playerStats.Weapons.find(element => element.type === key);
if (weapon) {
weapon.kills ??= 0;
weapon.kills += kills;
} else {
playerStats.Weapons.push({ type: key, kills });
}
}
}
if (KILL_ENEMY) {
playerStats.Enemies ??= []; playerStats.Enemies ??= [];
for (const [key, kills] of Object.entries(KILL_ENEMY)) { const enemyStatKey = {
const enemy = playerStats.Enemies.find(element => element.type === key); KILL_ENEMY: "kills",
EXECUTE_ENEMY: "executions",
HEADSHOT: "headshots"
}[category] as "kills" | "executions" | "headshots";
for (const [type, count] of Object.entries(data as IUploadEntry)) {
const enemy = playerStats.Enemies.find(element => element.type === type);
if (enemy) { if (enemy) {
enemy.kills ??= 0; enemy[enemyStatKey] ??= 0;
enemy.kills += kills; enemy[enemyStatKey] += count;
} else { } else {
playerStats.Enemies.push({ type: key, kills }); const newEnemy: IEnemy = { type: type };
} newEnemy[enemyStatKey] = count;
playerStats.Enemies.push(newEnemy);
} }
} }
break;
if (EXECUTE_ENEMY) { case "DIE":
playerStats.Enemies ??= []; playerStats.Enemies ??= [];
for (const [key, executions] of Object.entries(EXECUTE_ENEMY)) {
const enemy = playerStats.Enemies.find(element => element.type === key);
if (enemy) {
enemy.executions ??= 0;
enemy.executions += executions;
} else {
playerStats.Enemies.push({ type: key, executions });
}
}
}
if (HEADSHOT) {
playerStats.Enemies ??= [];
for (const [key, headshots] of Object.entries(HEADSHOT)) {
const enemy = playerStats.Enemies.find(element => element.type === key);
if (enemy) {
enemy.headshots ??= 0;
enemy.headshots += headshots;
} else {
playerStats.Enemies.push({ type: key, headshots });
}
}
}
if (DIE) {
playerStats.Enemies ??= [];
for (const [key, deaths] of Object.entries(DIE)) {
playerStats.Deaths ??= 0; playerStats.Deaths ??= 0;
for (const [type, deaths] of Object.entries(data as IUploadEntry)) {
playerStats.Deaths += deaths; playerStats.Deaths += deaths;
const enemy = playerStats.Enemies.find(element => element.type === key); const enemy = playerStats.Enemies.find(element => element.type === type);
if (enemy) { if (enemy) {
enemy.deaths ??= 0; enemy.deaths ??= 0;
enemy.deaths += deaths; enemy.deaths += deaths;
} else { } else {
playerStats.Enemies.push({ type: key, deaths }); playerStats.Enemies.push({ type: type, deaths });
}
} }
} }
break;
if (MELEE_KILL) { case "MELEE_KILL":
playerStats.MeleeKills ??= 0; playerStats.MeleeKills ??= 0;
for (const kills of Object.values(MELEE_KILL)) { // eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const [_key, kills] of Object.entries(data as IUploadEntry)) {
playerStats.MeleeKills += kills; playerStats.MeleeKills += kills;
} }
} break;
if (INCOME) { case "INCOME":
playerStats.Income ??= 0; playerStats.Income ??= 0;
playerStats.Income += INCOME; playerStats.Income += data;
} break;
if (CIPHER) { case "CIPHER":
if (CIPHER["0"] > 0) { if (data["0"] > 0) {
playerStats.CiphersFailed ??= 0; playerStats.CiphersFailed ??= 0;
playerStats.CiphersFailed += CIPHER["0"]; playerStats.CiphersFailed += data["0"];
} }
if (CIPHER["1"] > 0) { if (data["1"] > 0) {
playerStats.CiphersSolved ??= 0; playerStats.CiphersSolved ??= 0;
playerStats.CiphersSolved += CIPHER["1"]; playerStats.CiphersSolved += data["1"];
}
}
} }
break;
if (payload.timers) { default:
const { EQUIP_WEAPON, CURRENT_MISSION_TIME, CIPHER_TIME } = payload.timers; if (!ignoredCategories.includes(category)) {
if (!unknownCategories[action]) {
unknownCategories[action] = [];
}
unknownCategories[action].push(category);
}
break;
}
}
break;
if (EQUIP_WEAPON) { case "timers":
for (const [category, data] of Object.entries(actionData as IStatsTimers)) {
switch (category) {
case "EQUIP_WEAPON":
playerStats.Weapons ??= []; playerStats.Weapons ??= [];
for (const [key, equipTime] of Object.entries(EQUIP_WEAPON)) { for (const [type, equipTime] of Object.entries(data as IUploadEntry)) {
const weapon = playerStats.Weapons.find(element => element.type === key); const weapon = playerStats.Weapons.find(element => element.type === type);
if (weapon) { if (weapon) {
weapon.equipTime ??= 0; weapon.equipTime ??= 0;
weapon.equipTime += equipTime; weapon.equipTime += equipTime;
} else { } else {
playerStats.Weapons.push({ type: key, equipTime }); playerStats.Weapons.push({ type: type, equipTime });
}
} }
} }
break;
if (CURRENT_MISSION_TIME) { case "CURRENT_MISSION_TIME":
playerStats.TimePlayedSec ??= 0; playerStats.TimePlayedSec ??= 0;
playerStats.TimePlayedSec += CURRENT_MISSION_TIME; playerStats.TimePlayedSec += data;
} break;
if (CIPHER_TIME) { case "CIPHER_TIME":
playerStats.CipherTime ??= 0; playerStats.CipherTime ??= 0;
playerStats.CipherTime += CIPHER_TIME; playerStats.CipherTime += data;
break;
default:
if (!ignoredCategories.includes(category)) {
if (!unknownCategories[action]) {
unknownCategories[action] = [];
}
unknownCategories[action].push(category);
}
break;
} }
} }
break;
if (payload.max) { case "max":
const { WEAPON_XP, MISSION_SCORE } = payload.max; for (const [category, data] of Object.entries(actionData as IStatsMax)) {
switch (category) {
if (WEAPON_XP) { case "WEAPON_XP":
playerStats.Weapons ??= []; playerStats.Weapons ??= [];
for (const [key, xp] of Object.entries(WEAPON_XP)) { for (const [type, xp] of Object.entries(data as IUploadEntry)) {
const weapon = playerStats.Weapons.find(element => element.type === key); const weapon = playerStats.Weapons.find(element => element.type === type);
if (weapon) { if (weapon) {
if (xp > (weapon.xp ?? 0)) {
weapon.xp = xp; weapon.xp = xp;
}
} else { } else {
playerStats.Weapons.push({ type: key, xp }); playerStats.Weapons.push({ type: type, xp });
}
} }
} }
break;
if (MISSION_SCORE) { case "MISSION_SCORE":
playerStats.Missions ??= []; playerStats.Missions ??= [];
for (const [key, highScore] of Object.entries(MISSION_SCORE)) { for (const [type, highScore] of Object.entries(data as IUploadEntry)) {
const mission = playerStats.Missions.find(element => element.type === key); const mission = playerStats.Missions.find(element => element.type === type);
if (mission) { if (mission) {
if (highScore > mission.highScore) {
mission.highScore = highScore; mission.highScore = highScore;
}
} else { } else {
playerStats.Missions.push({ type: key, highScore }); playerStats.Missions.push({ type: type, highScore });
} }
} }
break;
case "RACE_SCORE":
playerStats.Races ??= new Map();
for (const [race, highScore] of Object.entries(data as Record<string, number>)) {
const currentRace = playerStats.Races.get(race);
if (currentRace) {
if (highScore > currentRace.highScore) {
playerStats.Races.set(race, { highScore });
}
} else {
playerStats.Races.set(race, { highScore });
} }
} }
if (payload.set) { break;
const { ELO_RATING, RANK, PLAYER_LEVEL } = payload.set;
if (ELO_RATING) playerStats.Rating = ELO_RATING; default:
if (RANK) playerStats.Rank = RANK; if (!ignoredCategories.includes(category)) {
if (PLAYER_LEVEL) playerStats.PlayerLevel = PLAYER_LEVEL; if (!unknownCategories[action]) {
unknownCategories[action] = [];
}
unknownCategories[action].push(category);
}
break;
}
}
break;
case "set":
for (const [category, value] of Object.entries(actionData as IStatsSet)) {
switch (category) {
case "ELO_RATING":
playerStats.Rating = value;
break;
case "RANK":
playerStats.Rank = value;
break;
case "PLAYER_LEVEL":
playerStats.PlayerLevel = value;
break;
default:
if (!ignoredCategories.includes(category)) {
if (!unknownCategories[action]) {
unknownCategories[action] = [];
}
unknownCategories[action].push(category);
}
break;
}
}
break;
case "displayName":
case "guildId":
break;
default:
logger.debug(`Unknown updateStats action: ${action}`);
break;
}
}
for (const [action, categories] of Object.entries(unknownCategories)) {
logger.debug(`Unknown updateStats ${action} action categories: ${categories.join(", ")}`);
} }
await playerStats.save(); await playerStats.save();
}; };
const ignoredCategories = [
//add action
"MISSION_STARTED",
"HOST_OS",
"CPU_CORES",
"CPU_MODEL",
"CPU_VENDOR",
"GPU_CLASS",
"GFX_DRIVER",
"GFX_RESOLUTION",
"GFX_ASPECT",
"GFX_WINDOW",
"GPU_VENDOR",
"GFX_HDR",
"SPEAKER_COUNT",
"MISSION_MATCHMAKING",
"PLAYER_COUNT",
"HOST_MIGRATION",
"DESTROY_DECORATION",
"MOVEMENT",
"RECEIVE_UPGRADE",
"EQUIP_COSMETIC",
"EQUIP_UPGRADE",
"MISSION_TYPE",
"MISSION_FACTION",
"MISSION_PLAYED",
"MISSION_PLAYED_TIME",
"CPU_CLOCK",
"CPU_FEATURE",
"RAM",
"ADDR_SPACE",
"GFX_SCALE",
"LOGINS",
"GPU_MODEL",
"MEDALS_TOP",
"STATS_TIMERS_RESET",
"INPUT_ACTIVITY_TIME",
"LOGINS_ITEM",
"TAKE_DAMAGE",
"SQUAD_KILL_ENEMY",
"SQUAD_HEADSHOT",
"SQUAD_MELEE_KILL",
"MELEE_KILL_ITEM",
"TAKE_DAMAGE_ITEM",
"SQUAD_KILL_ENEMY_ITEM",
"SQUAD_HEADSHOT_ITEM",
"SQUAD_MELEE_KILL_ITEM",
"PRE_DIE",
"PRE_DIE_ITEM",
"GEAR_USED",
"DIE_ITEM",
// timers action
"IN_SHIP_TIME",
"IN_SHIP_VIEW_TIME",
"MISSION_LOAD_TIME",
"MISSION_TIME",
"REGION_TIME",
"PLATFORM_TIME",
"PRE_DIE_TIME",
"VEHICLE_TIME"
];

View File

@ -1,6 +1,6 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
export interface IStatsView { export interface IStatsClient {
CiphersSolved?: number; CiphersSolved?: number;
CiphersFailed?: number; CiphersFailed?: number;
CipherTime?: number; CipherTime?: number;
@ -10,9 +10,11 @@ export interface IStatsView {
MissionsCompleted?: number; MissionsCompleted?: number;
MissionsQuit?: number; MissionsQuit?: number;
MissionsFailed?: number; MissionsFailed?: number;
MissionsInterrupted?: number;
MissionsDumped?: number;
TimePlayedSec?: number; TimePlayedSec?: number;
PickupCount?: number; PickupCount?: number;
Tutorial?: { [key: string]: ITutorial }; Tutorial?: Map<string, ITutorial>;
Abilities?: IAbility[]; Abilities?: IAbility[];
Rating?: number; Rating?: number;
Income?: number; Income?: number;
@ -23,9 +25,10 @@ export interface IStatsView {
Deaths?: number; Deaths?: number;
HealCount?: number; HealCount?: number;
ReviveCount?: number; ReviveCount?: number;
Races?: Map<string, IRace>;
} }
export interface IStatsDatabase extends IStatsView { export interface IStatsDatabase extends IStatsClient {
accountOwnerId: Types.ObjectId; accountOwnerId: Types.ObjectId;
} }
@ -68,7 +71,11 @@ export interface IWeapon {
fired?: number; fired?: number;
} }
export interface IStatsUpload { export interface IRace {
highScore: number;
}
export interface IStatsUpdate {
displayName: string; displayName: string;
guildId?: string; guildId?: string;
PS?: string; PS?: string;
@ -128,6 +135,7 @@ export interface IUploadEntry {
export interface IStatsMax { export interface IStatsMax {
WEAPON_XP?: IUploadEntry; WEAPON_XP?: IUploadEntry;
MISSION_SCORE?: IUploadEntry; MISSION_SCORE?: IUploadEntry;
RACE_SCORE?: IUploadEntry;
} }
export interface IStatsSet { export interface IStatsSet {