const tables = require("../tables"); const { migrateSave } = require("../services/migrateSave"); // --- Anti-cheat validation --- // Max production théorique par seconde (5 generators maxés + click humain réaliste) // Tier 5 = 150/s × 200 owned = 30 000/s (extreme case) // Tous tiers cumulés extreme ≈ 50 000/s // × prestige multiplier max réaliste (×10) = 500 000/s // × evolution tree multiplier (×1.5) = 750 000/s // Marge ×1.1 appliquée dans la validation const MAX_PRODUCTION_PER_SECOND = 750_000; const CHEAT_MARGIN = 1.1; function validateGameState(gameState, previousSave) { // Vérification structurelle basique if (!gameState || typeof gameState !== "object") { return { valid: false, reason: "Invalid game state format" }; } if (typeof gameState.resources !== "number" || gameState.resources < 0) { return { valid: false, reason: "Invalid resources value" }; } if (!Array.isArray(gameState.generators)) { return { valid: false, reason: "Invalid generators" }; } // Si pas de save précédente, c'est la première — accepter if (!previousSave) { return { valid: true }; } // Calcul du temps écoulé depuis le dernier save const lastSaveTime = new Date(previousSave.last_save).getTime(); const now = Date.now(); const elapsedSeconds = Math.max((now - lastSaveTime) / 1000, 0); // Ressources max possibles = production max × temps × marge const maxPossibleResources = MAX_PRODUCTION_PER_SECOND * elapsedSeconds * CHEAT_MARGIN; // Comparer les ressources actuelles avec le maximum théorique // On compare le delta (gain depuis dernier save) const previousState = previousSave.game_state; const previousResources = typeof previousState === "object" ? previousState.resources ?? 0 : 0; const resourceDelta = gameState.resources - previousResources; if (resourceDelta > maxPossibleResources && resourceDelta > 0) { return { valid: false, reason: `Resource gain (${Math.floor(resourceDelta)}) exceeds maximum possible (${Math.floor(maxPossibleResources)}) in ${Math.floor(elapsedSeconds)}s`, }; } return { valid: true }; } // --- Controllers --- /** * GET /api/save * Charge la save du joueur connecté. */ const load = async (req, res) => { try { const userId = req.user; const save = await tables.game_saves.getByUserId(userId); if (!save) { return res.status(200).json({ gameState: null }); } // game_state est stocké en JSON — MySQL le retourne comme objet si type JSON const rawState = typeof save.game_state === "string" ? JSON.parse(save.game_state) : save.game_state; // Migrate on load — lazy migration, never touch DB rows directly const gameState = migrateSave(rawState); return res.status(200).json({ gameState, lastSave: save.last_save, lifetimeTadpoles: save.lifetime_tadpoles, prestigeCount: save.prestige_count, playTimeSeconds: save.play_time_seconds, }); } catch (err) { console.error("save/load error:", err); return res.status(500).json({ message: "Failed to load save." }); } }; /** * POST /api/save * Sauvegarde le game state du joueur connecté. * Anti-cheat : valide le delta de ressources vs temps écoulé. */ const save = async (req, res) => { try { const userId = req.user; const { gameState, playTimeSeconds } = req.body; if (!gameState) { return res.status(400).json({ message: "gameState required." }); } // Récupérer la save précédente pour validation const previousSave = await tables.game_saves.getByUserId(userId); // Valider l'état const validation = validateGameState(gameState, previousSave); if (!validation.valid) { console.warn( `Anti-cheat: user ${userId} rejected — ${validation.reason}` ); return res.status(422).json({ message: "Save rejected: anomaly detected.", reason: validation.reason, }); } // Extraire les métadonnées pour la table const metadata = { lifetimeTadpoles: gameState.lifetimeTadpoles ?? 0, prestigeCount: gameState.prestigeCount ?? 0, playTimeSeconds: playTimeSeconds ?? 0, }; await tables.game_saves.upsert(userId, gameState, metadata); return res.status(200).json({ message: "Save successful.", lastSave: new Date().toISOString() }); } catch (err) { console.error("save/save error:", err); return res.status(500).json({ message: "Failed to save." }); } }; module.exports = { load, save };