404 lines
8.4 KiB
JavaScript
404 lines
8.4 KiB
JavaScript
const {
|
|
getProfileCollections,
|
|
getProfileStats,
|
|
readStatValue
|
|
} = require("./profileSystem");
|
|
|
|
const {
|
|
getUnixTime
|
|
} = require("./cookieEconomy");
|
|
|
|
function getActiveDefinitions(
|
|
achievementConfig
|
|
) {
|
|
return achievementConfig
|
|
.find({
|
|
active: {
|
|
$ne: false
|
|
}
|
|
})
|
|
.toArray();
|
|
}
|
|
|
|
async function reconcileUserAchievements({
|
|
guildId,
|
|
userId,
|
|
dryRun = false
|
|
}) {
|
|
const {
|
|
achievements,
|
|
achievementConfig
|
|
} = getProfileCollections();
|
|
|
|
const [
|
|
statsDocument,
|
|
definitions,
|
|
achievementDocument
|
|
] = await Promise.all([
|
|
getProfileStats(
|
|
userId,
|
|
guildId
|
|
),
|
|
getActiveDefinitions(
|
|
achievementConfig
|
|
),
|
|
achievements.findOne({
|
|
userId,
|
|
guildId
|
|
})
|
|
]);
|
|
|
|
const unlocked = {
|
|
...(achievementDocument?.unlocked ?? {})
|
|
};
|
|
|
|
const additions = {};
|
|
let skippedEventOnly = 0;
|
|
|
|
const normalDefinitions =
|
|
definitions.filter(
|
|
definition =>
|
|
definition.measure !==
|
|
"achievementPercentage"
|
|
);
|
|
|
|
for (const definition of normalDefinitions) {
|
|
if (unlocked[definition.achievementId]) {
|
|
continue;
|
|
}
|
|
|
|
if (!definition.stat) {
|
|
skippedEventOnly++;
|
|
continue;
|
|
}
|
|
|
|
const currentValue =
|
|
readStatValue(
|
|
statsDocument,
|
|
definition
|
|
);
|
|
|
|
if (
|
|
currentValue <
|
|
Number(definition.target ?? 0)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
const timestamp =
|
|
getUnixTime();
|
|
|
|
additions[
|
|
definition.achievementId
|
|
] = timestamp;
|
|
|
|
unlocked[
|
|
definition.achievementId
|
|
] = timestamp;
|
|
}
|
|
|
|
const collectibleDefinitions =
|
|
definitions.filter(
|
|
definition =>
|
|
definition.measure !==
|
|
"achievementPercentage"
|
|
);
|
|
|
|
const unlockedCollectible =
|
|
collectibleDefinitions.filter(
|
|
definition =>
|
|
Boolean(
|
|
unlocked[
|
|
definition.achievementId
|
|
]
|
|
)
|
|
).length;
|
|
|
|
const percentage =
|
|
collectibleDefinitions.length > 0
|
|
? unlockedCollectible /
|
|
collectibleDefinitions.length
|
|
: 0;
|
|
|
|
for (
|
|
const definition
|
|
of definitions.filter(
|
|
item =>
|
|
item.measure ===
|
|
"achievementPercentage"
|
|
)
|
|
) {
|
|
if (
|
|
unlocked[
|
|
definition.achievementId
|
|
]
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
percentage <
|
|
Number(definition.target ?? 0)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
additions[
|
|
definition.achievementId
|
|
] = getUnixTime();
|
|
}
|
|
|
|
const additionEntries =
|
|
Object.entries(additions);
|
|
|
|
if (
|
|
!dryRun &&
|
|
additionEntries.length > 0
|
|
) {
|
|
const set = {};
|
|
|
|
for (
|
|
const [achievementId, timestamp]
|
|
of additionEntries
|
|
) {
|
|
set[
|
|
`unlocked.${achievementId}`
|
|
] = timestamp;
|
|
}
|
|
|
|
await achievements.updateOne(
|
|
{
|
|
userId,
|
|
guildId
|
|
},
|
|
{
|
|
$setOnInsert: {
|
|
userId,
|
|
guildId
|
|
},
|
|
$set: set
|
|
},
|
|
{
|
|
upsert: true
|
|
}
|
|
);
|
|
}
|
|
|
|
return {
|
|
userId,
|
|
checked:
|
|
definitions.length,
|
|
unlocked:
|
|
additionEntries.length,
|
|
skippedEventOnly,
|
|
dryRun,
|
|
achievementIds:
|
|
additionEntries.map(
|
|
([achievementId]) =>
|
|
achievementId
|
|
)
|
|
};
|
|
}
|
|
|
|
async function reconcileAllAchievements({
|
|
guildId,
|
|
dryRun = false
|
|
}) {
|
|
const {
|
|
stats
|
|
} = getProfileCollections();
|
|
|
|
const userIds =
|
|
await stats.distinct(
|
|
"userId",
|
|
{
|
|
guildId
|
|
}
|
|
);
|
|
|
|
const summary = {
|
|
users:
|
|
userIds.length,
|
|
unlocked:
|
|
0,
|
|
skippedEventOnly:
|
|
0,
|
|
results: []
|
|
};
|
|
|
|
for (const userId of userIds) {
|
|
const result =
|
|
await reconcileUserAchievements({
|
|
guildId,
|
|
userId,
|
|
dryRun
|
|
});
|
|
|
|
summary.unlocked +=
|
|
result.unlocked;
|
|
|
|
summary.skippedEventOnly +=
|
|
result.skippedEventOnly;
|
|
|
|
summary.results.push(
|
|
result
|
|
);
|
|
}
|
|
|
|
return summary;
|
|
}
|
|
|
|
async function auditProfileData(
|
|
guildId
|
|
) {
|
|
const {
|
|
stats,
|
|
achievements,
|
|
achievementConfig,
|
|
badges
|
|
} = getProfileCollections();
|
|
|
|
const [
|
|
statDocuments,
|
|
achievementDocuments,
|
|
activeAchievements,
|
|
badgeDocuments
|
|
] = await Promise.all([
|
|
stats.find({
|
|
guildId
|
|
}).toArray(),
|
|
achievements.find({
|
|
guildId
|
|
}).toArray(),
|
|
achievementConfig.countDocuments({
|
|
active: {
|
|
$ne: false
|
|
}
|
|
}),
|
|
badges.find({
|
|
guildId
|
|
}).toArray()
|
|
]);
|
|
|
|
const issues = [];
|
|
|
|
for (const document of statDocuments) {
|
|
if (!document.userId) {
|
|
issues.push(
|
|
"Stats document missing userId."
|
|
);
|
|
continue;
|
|
}
|
|
|
|
const total =
|
|
document.games?.total ?? {};
|
|
|
|
const played =
|
|
Number(total.played ?? 0);
|
|
|
|
const resolved =
|
|
Number(total.won ?? 0) +
|
|
Number(total.lost ?? 0) +
|
|
Number(total.pushed ?? 0);
|
|
|
|
if (played !== resolved) {
|
|
issues.push(
|
|
`${document.userId}: games.total.played (${played}) does not match W/L/P total (${resolved}).`
|
|
);
|
|
}
|
|
|
|
for (
|
|
const game
|
|
of [
|
|
"slots",
|
|
"blackjack",
|
|
"roulette",
|
|
"heist"
|
|
]
|
|
) {
|
|
const value =
|
|
document.games?.[game] ?? {};
|
|
|
|
const gamePlayed =
|
|
Number(value.played ?? 0);
|
|
|
|
const gameResolved =
|
|
Number(value.won ?? 0) +
|
|
Number(value.lost ?? 0) +
|
|
Number(value.pushed ?? 0);
|
|
|
|
if (
|
|
gamePlayed !==
|
|
gameResolved
|
|
) {
|
|
issues.push(
|
|
`${document.userId}: games.${game}.played (${gamePlayed}) does not match W/L/P total (${gameResolved}).`
|
|
);
|
|
}
|
|
}
|
|
|
|
const nonNegativeFields = [
|
|
document.economy?.cookiesObtainedTotal,
|
|
document.economy?.cookiesLostTotal,
|
|
document.economy?.cookiesGivenTotal,
|
|
document.economy?.stolenTotal,
|
|
document.economy?.stolenFromMeTotal,
|
|
document.economy?.vaultStolenTotal
|
|
];
|
|
|
|
if (
|
|
nonNegativeFields.some(
|
|
value =>
|
|
Number(value ?? 0) < 0
|
|
)
|
|
) {
|
|
issues.push(
|
|
`${document.userId}: one or more economy counters are negative.`
|
|
);
|
|
}
|
|
}
|
|
|
|
const statUsers =
|
|
new Set(
|
|
statDocuments.map(
|
|
document =>
|
|
document.userId
|
|
)
|
|
);
|
|
|
|
const orphanAchievementDocs =
|
|
achievementDocuments.filter(
|
|
document =>
|
|
!statUsers.has(
|
|
document.userId
|
|
)
|
|
).length;
|
|
|
|
const claimedBadges =
|
|
badgeDocuments.filter(
|
|
badge =>
|
|
Boolean(badge.ownerId)
|
|
).length;
|
|
|
|
return {
|
|
statDocuments:
|
|
statDocuments.length,
|
|
achievementDocuments:
|
|
achievementDocuments.length,
|
|
activeAchievements,
|
|
badgeDocuments:
|
|
badgeDocuments.length,
|
|
claimedBadges,
|
|
orphanAchievementDocs,
|
|
issueCount:
|
|
issues.length,
|
|
issues
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
reconcileUserAchievements,
|
|
reconcileAllAchievements,
|
|
auditProfileData
|
|
};
|