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 };