/**
* @description Auto-backup system that creates save snapshots before and after each battle encounter.
* Stores up to 50 rolling backups per save slot in save/autobackups/file{id}/
* Filenames: "{timestamp} - before enemy - {troopName}.rpgsave" / "{timestamp} - after enemy - {troopName}.rpgsave"
*/
var MATTIE = MATTIE || {};
MATTIE.multiplayer = MATTIE.multiplayer || {};
MATTIE.multiplayer.autoBackup = MATTIE.multiplayer.autoBackup || {};
(function () {
var autoBackup = MATTIE.multiplayer.autoBackup;
var fs = require('fs');
var path = require('path');
autoBackup.MAX_BACKUPS = 50;
/**
* Get the base save directory (same as StorageManager)
*/
autoBackup.getSaveDir = function () {
var base = path.dirname(process.mainModule.filename);
return path.join(base, 'save');
};
/**
* Get the autobackup directory for a specific save slot, creating it if needed.
* Does NOT use {recursive:true} since older Node.js (bundled with NW.js) does not support it.
* @param {number} savefileId
* @returns {string} full path to backup directory
*/
autoBackup.getBackupDir = function (savefileId) {
var autobackupsDir = path.join(this.getSaveDir(), 'autobackups');
var slotDir = path.join(autobackupsDir, `file${savefileId}`);
if (!fs.existsSync(autobackupsDir)) {
fs.mkdirSync(autobackupsDir);
}
if (!fs.existsSync(slotDir)) {
fs.mkdirSync(slotDir);
}
return slotDir;
};
/**
* Sanitize a string for use in filenames - remove invalid filesystem characters
* @param {string} name
* @returns {string}
*/
autoBackup.sanitizeFilename = function (name) {
if (!name) return 'unknown';
return name.replace(/[<>:"/\\|?*]/g, '_').replace(/\s+/g, ' ').trim() || 'unknown';
};
/**
* Generate a timestamp string for filename ordering
* @returns {string} e.g. "20260404_153022_123"
*/
autoBackup.getTimestamp = function () {
var d = new Date();
var pad = function (n, len) { return String(n).padStart(len || 2, '0'); };
return `${pad(d.getFullYear(), 4) + pad(d.getMonth() + 1) + pad(d.getDate())
}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())
}_${pad(d.getMilliseconds(), 3)}`;
};
/**
* Get the current active save file ID, or null if none
* @returns {number|null}
*/
autoBackup.getCurrentSavefileId = function () {
try {
if (DataManager._lastAccessedId && DataManager._lastAccessedId > 0) {
return DataManager._lastAccessedId;
}
} catch (e) { console.log(e); }
return null;
};
/**
* Create a backup of the current game state
* @param {string} prefix - "before enemy" or "after enemy"
* @param {string} troopName - the troop/enemy name
*/
autoBackup.createBackup = function (prefix, troopName) {
try {
var savefileId = this.getCurrentSavefileId();
if (!savefileId) return;
if (!$gameSystem || !$gameParty) return;
var safeName = this.sanitizeFilename(troopName);
var timestamp = this.getTimestamp();
var filename = `${timestamp} - ${prefix} - ${safeName}.rpgsave`;
var dir = this.getBackupDir(savefileId);
var filePath = path.join(dir, filename);
// Serialize game state using the same pipeline as normal saves
var contents = DataManager.makeSaveContents();
var json = JsonEx.stringify(contents);
var data = LZString.compressToBase64(json);
fs.writeFileSync(filePath, data);
// Enforce the backup limit
this.enforceLimit(savefileId);
console.log(`[AutoBackup] Created: ${filename}`);
} catch (e) {
console.error('[AutoBackup] Failed to create backup:', e);
}
};
/**
* Enforce the maximum backup count for a save slot, deleting oldest files
* @param {number} savefileId
*/
autoBackup.enforceLimit = function (savefileId) {
try {
var dir = this.getBackupDir(savefileId);
var files = fs.readdirSync(dir)
.filter((f) => f.endsWith('.rpgsave'))
.sort(); // timestamp prefix ensures chronological sort
while (files.length > this.MAX_BACKUPS) {
var oldest = files.shift();
var oldPath = path.join(dir, oldest);
fs.unlinkSync(oldPath);
console.log(`[AutoBackup] Pruned old backup: ${oldest}`);
}
} catch (e) {
console.error('[AutoBackup] Failed to enforce limit:', e);
}
};
/**
* List all backup files for a save slot, newest first.
* Does NOT create the directory - returns empty array if it does not exist.
* @param {number} savefileId
* @returns {Array<{filename: string, fullPath: string}>}
*/
autoBackup.listBackups = function (savefileId) {
try {
var dir = path.join(this.getSaveDir(), 'autobackups', `file${savefileId}`);
if (!fs.existsSync(dir)) return [];
var files = fs.readdirSync(dir)
.filter((f) => f.endsWith('.rpgsave'))
.sort()
.reverse(); // newest first
return files.map((f) => ({ filename: f, fullPath: path.join(dir, f) }));
} catch (e) {
return [];
}
};
/**
* List all save slot IDs that have backup folders with at least one backup
* @returns {number[]}
*/
autoBackup.listBackupSlots = function () {
var baseDir = path.join(this.getSaveDir(), 'autobackups');
if (!fs.existsSync(baseDir)) return [];
try {
return fs.readdirSync(baseDir)
.filter((d) => d.startsWith('file'))
.map((d) => parseInt(d.replace('file', ''), 10))
.filter((id) => !isNaN(id) && id > 0)
.filter((id) => {
// Only include slots that actually have backup files
var slotDir = path.join(baseDir, `file${id}`);
try {
return fs.readdirSync(slotDir).some((f) => f.endsWith('.rpgsave'));
} catch (e) { return false; }
})
.sort((a, b) => a - b);
} catch (e) {
return [];
}
};
/**
* Restore a backup file as the active save for a given slot
* @param {number} savefileId
* @param {string} backupFilePath - full path to the backup .rpgsave file
* @returns {boolean} true if successful
*/
autoBackup.restoreBackup = function (savefileId, backupFilePath) {
try {
if (!fs.existsSync(backupFilePath)) {
console.error('[AutoBackup] Backup file not found:', backupFilePath);
return false;
}
var data = fs.readFileSync(backupFilePath, { encoding: 'utf8' });
// Write to the normal save location
var saveDir = StorageManager.localFileDirectoryPath();
if (!fs.existsSync(saveDir)) {
fs.mkdirSync(saveDir);
}
var targetPath = StorageManager.localFilePath(savefileId);
fs.writeFileSync(targetPath, data);
console.log(`[AutoBackup] Restored backup to save slot ${savefileId} from: ${backupFilePath}`);
return true;
} catch (e) {
console.error('[AutoBackup] Failed to restore backup:', e);
return false;
}
};
/**
* Parse a backup filename into its display components
* @param {string} filename
* @returns {{timestamp: string, prefix: string, troopName: string, displayName: string}}
*/
autoBackup.parseBackupFilename = function (filename) {
// Format: "20260404_153022_123 - before enemy - Troop Name.rpgsave"
var base = filename.replace('.rpgsave', '');
var parts = base.split(' - ');
var timestamp = parts[0] || '';
var prefix = parts[1] || '';
var troopName = parts.slice(2).join(' - ') || '';
// Format timestamp for display: "2026/04/04 15:30:22"
var displayTime = timestamp;
var match = timestamp.match(/^(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})/);
if (match) {
displayTime = `${match[1]}/${match[2]}/${match[3]} ${
match[4]}:${match[5]}:${match[6]}`;
}
return {
timestamp,
prefix,
troopName,
displayName: `${displayTime} - ${prefix} - ${troopName}`,
};
};
// =========================================================================
// Battle Hooks
// =========================================================================
/**
* Hook into BattleManager.setup to create "before enemy" backup
*/
var _BattleManager_setup = BattleManager.setup;
BattleManager.setup = function (troopId, canEscape, canLose) {
_BattleManager_setup.call(this, troopId, canEscape, canLose);
try {
var troopName = $gameTroop.troop() ? $gameTroop.troop().name : 'Unknown';
autoBackup.createBackup('before enemy', troopName);
} catch (e) {
console.error('[AutoBackup] Error in BattleManager.setup hook:', e);
}
};
/**
* Hook into BattleManager.endBattle to create "after enemy" backup
*/
var _BattleManager_endBattle = BattleManager.endBattle;
BattleManager.endBattle = function (result) {
_BattleManager_endBattle.call(this, result);
try {
var troopName = $gameTroop.troop() ? $gameTroop.troop().name : 'Unknown';
autoBackup.createBackup('after enemy', troopName);
} catch (e) {
console.error('[AutoBackup] Error in BattleManager.endBattle hook:', e);
}
};
}());