Files
Fallout-Shelter-Save-Editor/renderer.js
2025-08-05 04:54:50 +00:00

824 lines
28 KiB
JavaScript

const { ipcRenderer } = require('electron');
const CryptoJS = require('crypto-js');
// Application state
let currentSaveData = null;
let currentFilePath = null;
let isModified = false;
// AES encryption constants
const AES_KEY_WORDS = [2815074099, 1725469378, 4039046167, 874293617, 3063605751, 3133984764, 4097598161, 3620741625];
const AES_KEY = CryptoJS.lib.WordArray.create(AES_KEY_WORDS);
const AES_IV = CryptoJS.enc.Hex.parse('7475383967656A693334307438397532');
// Initialize the application
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
startupAnimation();
}, 100);
});
function startupAnimation() {
const overlay = document.getElementById('startup-overlay');
const progressFill = document.getElementById('startup-progress');
const progressPercent = document.getElementById('progress-percent');
const statusText = document.getElementById('startup-status');
if (!overlay || !progressFill || !progressPercent || !statusText) {
initializeApp();
return;
}
// Initialize
progressFill.style.width = '0%';
progressPercent.textContent = '0%';
const steps = [
{ progress: 25, status: 'Loading Vault-Tec Database...', delay: 300 },
{ progress: 50, status: 'Initializing Encryption...', delay: 300 },
{ progress: 75, status: 'Connecting Systems...', delay: 300 },
{ progress: 100, status: 'Welcome to Vault-Tec!', delay: 400 }
];
let currentStep = 0;
function updateProgress() {
if (currentStep < steps.length) {
const step = steps[currentStep];
statusText.textContent = step.status;
progressFill.style.width = step.progress + '%';
progressPercent.textContent = step.progress + '%';
currentStep++;
setTimeout(updateProgress, step.delay);
} else {
setTimeout(() => {
overlay.classList.add('fade-out');
setTimeout(() => {
overlay.style.display = 'none';
initializeApp();
}, 800);
}, 200);
}
}
setTimeout(updateProgress, 500);
}
function initializeApp() {
initializeEventListeners();
initializeTabSystem();
updateUI();
setStatus('Ready');
setTimeout(() => {
showToast('Vault-Tec Save Editor ready!', 'success');
}, 200);
}
function initializeEventListeners() {
// Navigation tabs
document.querySelectorAll('.nav-tab').forEach(tab => {
tab.addEventListener('click', () => switchTab(tab.dataset.tab));
});
// File operations
const openFileBtn = document.getElementById('open-file-btn');
const getStartedBtn = document.getElementById('get-started-btn');
const saveBtn = document.getElementById('save-btn');
const backupBtn = document.getElementById('backup-btn');
if (openFileBtn) openFileBtn.addEventListener('click', openFile);
if (getStartedBtn) getStartedBtn.addEventListener('click', openFile);
if (saveBtn) saveBtn.addEventListener('click', saveFile);
if (backupBtn) backupBtn.addEventListener('click', createBackup);
// Resource operations
const maxResourcesBtn = document.getElementById('max-resources-btn');
const resetResourcesBtn = document.getElementById('reset-resources-btn');
const applyResourcesBtn = document.getElementById('apply-resources-btn');
if (maxResourcesBtn) maxResourcesBtn.addEventListener('click', maxAllResources);
if (resetResourcesBtn) resetResourcesBtn.addEventListener('click', resetResources);
if (applyResourcesBtn) applyResourcesBtn.addEventListener('click', applyResourceChanges);
// Vault operations
const applyVaultBtn = document.getElementById('apply-vault-btn');
if (applyVaultBtn) applyVaultBtn.addEventListener('click', applyVaultChanges);
// Raw data operations
const formatJsonBtn = document.getElementById('format-json-btn');
const copyJsonBtn = document.getElementById('copy-json-btn');
if (formatJsonBtn) formatJsonBtn.addEventListener('click', formatJSON);
if (copyJsonBtn) copyJsonBtn.addEventListener('click', copyJSON);
// Dweller operations
const maxAllDwellersBtn = document.getElementById('max-all-dwellers-btn');
const refreshDwellersBtn = document.getElementById('refresh-dwellers-btn');
if (maxAllDwellersBtn) maxAllDwellersBtn.addEventListener('click', maxAllDwellers);
if (refreshDwellersBtn) refreshDwellersBtn.addEventListener('click', populateDwellers);
// IPC listeners
ipcRenderer.on('file-opened', handleFileOpened);
ipcRenderer.on('menu-save', saveFile);
}
function initializeTabSystem() {
switchTab('overview');
}
function switchTab(tabName) {
document.querySelectorAll('.nav-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.tab === tabName);
});
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.toggle('active', content.id === `${tabName}-tab`);
});
}
function openFile() {
ipcRenderer.send('open-file-dialog');
}
async function handleFileOpened(event, fileData) {
showLoading(true);
setStatus('Loading save file...');
try {
currentFilePath = fileData.path;
document.getElementById('current-file').textContent = fileData.name;
const decryptedData = decryptSaveData(fileData.data.trim());
currentSaveData = JSON.parse(decryptedData);
populateResourceInputs();
populateVaultInfo();
populateDwellers();
updateRawData();
document.getElementById('save-btn').disabled = false;
document.getElementById('backup-btn').disabled = false;
switchTab('resources');
setStatus('Save file loaded successfully');
showToast('Save file loaded successfully', 'success');
setModified(false);
} catch (error) {
console.error('Error loading save file:', error);
showToast(`Error loading save file: ${error.message}`, 'error');
setStatus('Error loading save file');
} finally {
showLoading(false);
}
}
function decryptSaveData(base64Data) {
try {
const ciphertext = CryptoJS.enc.Base64.parse(base64Data);
const cipherParams = CryptoJS.lib.CipherParams.create({
ciphertext: ciphertext
});
const decrypted = CryptoJS.AES.decrypt(cipherParams, AES_KEY, {
iv: AES_IV,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
const decryptedString = decrypted.toString(CryptoJS.enc.Utf8);
if (!decryptedString) {
throw new Error('Decryption failed - invalid key or corrupted data');
}
return decryptedString;
} catch (error) {
throw new Error(`Decryption failed: ${error.message}`);
}
}
function encryptSaveData(jsonString) {
try {
const encrypted = CryptoJS.AES.encrypt(jsonString, AES_KEY, {
iv: AES_IV,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.toString();
} catch (error) {
throw new Error(`Encryption failed: ${error.message}`);
}
}
function populateResourceInputs() {
if (!currentSaveData || !currentSaveData.vault || !currentSaveData.vault.storage) {
console.log('No vault storage data found');
return;
}
const resources = currentSaveData.vault.storage.resources;
console.log('Available resources:', Object.keys(resources));
const resourceMap = {
'caps-input': 'Nuka',
'food-input': 'Food',
'water-input': 'Water',
'power-input': 'Energy',
'stimpaks-input': 'StimPack',
'radaway-input': 'RadAway',
'quantum-input': 'NukaColaQuantum',
'lunchbox-input': 'Lunchbox',
'mrhandy-input': 'MrHandy',
'petcarrier-input': 'PetCarrier'
};
Object.entries(resourceMap).forEach(([inputId, resourceKey]) => {
const input = document.getElementById(inputId);
if (input && resources[resourceKey] !== undefined) {
input.value = Math.floor(resources[resourceKey]);
console.log(`Set ${inputId} to ${resources[resourceKey]}`);
} else {
console.log(`Could not find ${inputId} or ${resourceKey}`);
}
});
}
function populateVaultInfo() {
if (!currentSaveData || !currentSaveData.vault) {
return;
}
const vault = currentSaveData.vault;
const vaultNameInput = document.getElementById('vault-name-input');
if (vaultNameInput && vault.VaultName !== undefined) {
vaultNameInput.value = vault.VaultName;
}
const vaultModeInput = document.getElementById('vault-mode-input');
if (vaultModeInput && vault.VaultMode !== undefined) {
vaultModeInput.value = vault.VaultMode;
}
const vaultThemeInput = document.getElementById('vault-theme-input');
if (vaultThemeInput && vault.VaultTheme !== undefined) {
vaultThemeInput.value = vault.VaultTheme;
}
}
function applyResourceChanges() {
if (!currentSaveData || !currentSaveData.vault || !currentSaveData.vault.storage) {
showToast('No save data loaded', 'warning');
return;
}
console.log('Applying resource changes...');
const resources = currentSaveData.vault.storage.resources;
console.log('Current resources before changes:', resources);
const resourceMap = {
'caps-input': 'Nuka',
'food-input': 'Food',
'water-input': 'Water',
'power-input': 'Energy',
'stimpaks-input': 'StimPack',
'radaway-input': 'RadAway',
'quantum-input': 'NukaColaQuantum',
'lunchbox-input': 'Lunchbox',
'mrhandy-input': 'MrHandy',
'petcarrier-input': 'PetCarrier'
};
let changesApplied = 0;
Object.entries(resourceMap).forEach(([inputId, resourceKey]) => {
const input = document.getElementById(inputId);
if (input && input.value.trim()) {
const value = parseFloat(input.value);
if (!isNaN(value) && value >= 0) {
console.log(`Changing ${resourceKey} from ${resources[resourceKey]} to ${value}`);
resources[resourceKey] = value;
changesApplied++;
}
}
});
console.log('Resources after changes:', resources);
console.log(`Applied ${changesApplied} changes`);
if (changesApplied > 0) {
setModified(true);
updateRawData();
showToast(`Applied ${changesApplied} resource changes`, 'success');
} else {
showToast('No valid changes to apply', 'warning');
}
}
function maxAllResources() {
const maxValues = {
'caps-input': 999999,
'food-input': 999999,
'water-input': 999999,
'power-input': 999999,
'stimpaks-input': 999999,
'radaway-input': 999999,
'quantum-input': 999,
'lunchbox-input': 999,
'mrhandy-input': 99,
'petcarrier-input': 999
};
Object.entries(maxValues).forEach(([inputId, value]) => {
const input = document.getElementById(inputId);
if (input) {
input.value = value;
}
});
applyResourceChanges();
}
function resetResources() {
populateResourceInputs();
showToast('Resources reset to saved values', 'info');
}
function applyVaultChanges() {
if (!currentSaveData || !currentSaveData.vault) {
showToast('No save data loaded', 'warning');
return;
}
const vault = currentSaveData.vault;
let changesApplied = 0;
const vaultNameInput = document.getElementById('vault-name-input');
if (vaultNameInput && vaultNameInput.value.trim()) {
vault.VaultName = vaultNameInput.value.trim();
changesApplied++;
}
const vaultModeInput = document.getElementById('vault-mode-input');
if (vaultModeInput && vaultModeInput.value) {
vault.VaultMode = vaultModeInput.value;
changesApplied++;
}
const vaultThemeInput = document.getElementById('vault-theme-input');
if (vaultThemeInput && vaultThemeInput.value.trim()) {
const theme = parseInt(vaultThemeInput.value);
if (!isNaN(theme) && theme >= 0) {
vault.VaultTheme = theme;
changesApplied++;
}
}
if (changesApplied > 0) {
setModified(true);
updateRawData();
showToast(`Applied ${changesApplied} vault changes`, 'success');
} else {
showToast('No valid changes to apply', 'warning');
}
}
function updateRawData() {
const textarea = document.getElementById('raw-data-textarea');
if (currentSaveData && textarea) {
textarea.value = JSON.stringify(currentSaveData, null, 2);
}
}
function formatJSON() {
const textarea = document.getElementById('raw-data-textarea');
if (!textarea) return;
try {
const parsed = JSON.parse(textarea.value);
textarea.value = JSON.stringify(parsed, null, 2);
showToast('JSON formatted successfully', 'success');
} catch (error) {
showToast('Invalid JSON format', 'error');
}
}
function copyJSON() {
const textarea = document.getElementById('raw-data-textarea');
if (!textarea) return;
textarea.select();
document.execCommand('copy');
showToast('JSON copied to clipboard', 'success');
}
async function saveFile() {
if (!currentSaveData || !currentFilePath) {
showToast('No file loaded to save', 'warning');
return;
}
console.log('Starting save process...');
console.log('Current file path:', currentFilePath);
console.log('Save data exists:', !!currentSaveData);
showLoading(true);
setStatus('Saving file...');
try {
console.log('Converting to JSON...');
const jsonString = JSON.stringify(currentSaveData);
console.log('JSON string length:', jsonString.length);
console.log('Encrypting data...');
const encryptedData = encryptSaveData(jsonString);
console.log('Encrypted data length:', encryptedData.length);
console.log('Sending to main process...');
const result = await ipcRenderer.invoke('save-file', currentFilePath, encryptedData);
console.log('Save result:', result);
if (result.success) {
setModified(false);
setStatus('File saved successfully');
showToast('File saved successfully', 'success');
console.log('Save completed successfully');
} else {
throw new Error(result.error);
}
} catch (error) {
console.error('Error saving file:', error);
showToast(`Error saving file: ${error.message}`, 'error');
setStatus('Error saving file');
} finally {
showLoading(false);
}
}
async function createBackup() {
if (!currentFilePath) {
showToast('No file loaded to backup', 'warning');
return;
}
try {
const result = await ipcRenderer.invoke('create-backup', currentFilePath);
if (result.success) {
showToast(`Backup created: ${require('path').basename(result.backupPath)}`, 'success');
} else {
throw new Error(result.error);
}
} catch (error) {
console.error('Error creating backup:', error);
showToast(`Error creating backup: ${error.message}`, 'error');
}
}
function setStatus(message) {
const statusText = document.getElementById('status-text');
if (statusText) {
statusText.textContent = message;
}
}
function setModified(modified) {
isModified = modified;
updateUI();
}
function updateUI() {
if (isModified) {
document.title = 'Fallout Shelter Save Editor - Modified';
} else {
document.title = 'Fallout Shelter Save Editor';
}
}
function showLoading(show) {
const overlay = document.getElementById('loading-overlay');
if (overlay) {
overlay.classList.toggle('show', show);
}
}
function showToast(message, type = 'info') {
const container = document.getElementById('toast-container');
if (!container) return;
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.textContent = message;
container.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 4000);
}
// Dwellers Management
function populateDwellers() {
const dwellersList = document.getElementById('dwellers-list');
if (!currentSaveData || !currentSaveData.dwellers || !currentSaveData.dwellers.dwellers) {
if (dwellersList) {
dwellersList.innerHTML = `
<div class="placeholder">
<i class="fas fa-users"></i>
<p>No dwellers found in save file</p>
</div>
`;
}
return;
}
const dwellers = currentSaveData.dwellers.dwellers;
if (dwellers.length === 0) {
dwellersList.innerHTML = `
<div class="placeholder">
<i class="fas fa-users"></i>
<p>No dwellers in vault</p>
</div>
`;
return;
}
console.log(`Found ${dwellers.length} dwellers`);
dwellersList.innerHTML = dwellers.map((dweller, index) => {
const name = dweller.name || `Dweller ${index + 1}`;
const level = dweller.experience?.currentLevel || 1;
const happiness = Math.round(dweller.happiness?.happinessValue || 0);
const health = Math.round(dweller.health?.healthValue || 100);
const gender = dweller.gender === 2 ? 'Female' : 'Male';
const pregnant = dweller.relations?.pregnant || false;
// SPECIAL stats
const special = dweller.serializeableSpecialStats || {};
const strength = special.stats?.[1] || 1;
const perception = special.stats?.[2] || 1;
const endurance = special.stats?.[3] || 1;
const charisma = special.stats?.[4] || 1;
const intelligence = special.stats?.[5] || 1;
const agility = special.stats?.[6] || 1;
const luck = special.stats?.[7] || 1;
return `
<div class="dweller-card" data-index="${index}">
<div class="dweller-header">
<h3 class="dweller-name">${name}</h3>
<span class="dweller-gender ${gender.toLowerCase()}">${gender}</span>
${pregnant ? '<span class="pregnant-badge">Pregnant</span>' : ''}
</div>
<div class="dweller-stats">
<div class="stat-row">
<span>Level:</span>
<input type="number" class="dweller-input" data-field="experience.currentLevel" value="${level}" min="1" max="50">
</div>
<div class="stat-row">
<span>Happiness:</span>
<input type="number" class="dweller-input" data-field="happiness.happinessValue" value="${happiness}" min="0" max="100">%
</div>
<div class="stat-row">
<span>Health:</span>
<input type="number" class="dweller-input" data-field="health.healthValue" value="${health}" min="0" max="100">%
</div>
</div>
<div class="special-stats">
<h4>SPECIAL</h4>
<div class="special-grid">
<div class="special-stat">
<label>S</label>
<input type="number" class="special-input" data-stat="1" value="${strength}" min="1" max="10">
</div>
<div class="special-stat">
<label>P</label>
<input type="number" class="special-input" data-stat="2" value="${perception}" min="1" max="10">
</div>
<div class="special-stat">
<label>E</label>
<input type="number" class="special-input" data-stat="3" value="${endurance}" min="1" max="10">
</div>
<div class="special-stat">
<label>C</label>
<input type="number" class="special-input" data-stat="4" value="${charisma}" min="1" max="10">
</div>
<div class="special-stat">
<label>I</label>
<input type="number" class="special-input" data-stat="5" value="${intelligence}" min="1" max="10">
</div>
<div class="special-stat">
<label>A</label>
<input type="number" class="special-input" data-stat="6" value="${agility}" min="1" max="10">
</div>
<div class="special-stat">
<label>L</label>
<input type="number" class="special-input" data-stat="7" value="${luck}" min="1" max="10">
</div>
</div>
</div>
<div class="dweller-actions">
<button class="secondary-btn apply-dweller-btn" data-index="${index}">
<i class="fas fa-check"></i>
Apply Changes
</button>
<button class="secondary-btn max-special-btn" data-index="${index}">
<i class="fas fa-arrow-up"></i>
Max SPECIAL
</button>
</div>
</div>
`;
}).join('');
// Add event listeners for dweller editing
addDwellerEventListeners();
}
function addDwellerEventListeners() {
// Apply changes buttons
document.querySelectorAll('.apply-dweller-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const index = parseInt(e.target.closest('.apply-dweller-btn').dataset.index);
applyDwellerChanges(index);
});
});
// Max SPECIAL buttons
document.querySelectorAll('.max-special-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const index = parseInt(e.target.closest('.max-special-btn').dataset.index);
maxDwellerSpecial(index);
});
});
}
function applyDwellerChanges(dwellerIndex) {
if (!currentSaveData || !currentSaveData.dwellers || !currentSaveData.dwellers.dwellers[dwellerIndex]) {
showToast('Dweller not found', 'error');
return;
}
const dweller = currentSaveData.dwellers.dwellers[dwellerIndex];
const card = document.querySelector(`.dweller-card[data-index="${dwellerIndex}"]`);
if (!card) return;
let changesApplied = 0;
// Apply basic stats
card.querySelectorAll('.dweller-input').forEach(input => {
const field = input.dataset.field;
const value = parseFloat(input.value);
if (!isNaN(value)) {
setNestedValue(dweller, field, value);
changesApplied++;
}
});
// Apply SPECIAL stats
card.querySelectorAll('.special-input').forEach(input => {
const statIndex = parseInt(input.dataset.stat);
const value = parseInt(input.value);
if (!isNaN(value) && value >= 1 && value <= 10) {
if (!dweller.serializeableSpecialStats) {
dweller.serializeableSpecialStats = { stats: {} };
}
if (!dweller.serializeableSpecialStats.stats) {
dweller.serializeableSpecialStats.stats = {};
}
dweller.serializeableSpecialStats.stats[statIndex] = value;
changesApplied++;
}
});
if (changesApplied > 0) {
setModified(true);
updateRawData();
showToast(`Applied ${changesApplied} changes to dweller`, 'success');
} else {
showToast('No valid changes to apply', 'warning');
}
}
function maxDwellerSpecial(dwellerIndex) {
const card = document.querySelector(`.dweller-card[data-index="${dwellerIndex}"]`);
if (!card) return;
// Set all SPECIAL stats to 10
card.querySelectorAll('.special-input').forEach(input => {
input.value = 10;
});
applyDwellerChanges(dwellerIndex);
}
function setNestedValue(obj, path, value) {
const keys = path.split('.');
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
if (!current[keys[i]]) {
current[keys[i]] = {};
}
current = current[keys[i]];
}
current[keys[keys.length - 1]] = value;
}
// Advanced Editing Functions
function addAdvancedResourceOptions() {
// Add lunchbox and other premium resources
const advancedResources = {
'lunchbox-input': 'Lunchbox',
'mrhandy-input': 'MrHandy',
'petcarrier-input': 'PetCarrier',
'craftedoutfit-input': 'CraftedOutfit',
'craftedweapon-input': 'CraftedWeapon',
'craftedtheme-input': 'CraftedTheme'
};
if (currentSaveData && currentSaveData.vault && currentSaveData.vault.storage) {
const resources = currentSaveData.vault.storage.resources;
Object.entries(advancedResources).forEach(([inputId, resourceKey]) => {
const input = document.getElementById(inputId);
if (input && resources[resourceKey] !== undefined) {
input.value = Math.floor(resources[resourceKey]);
}
});
}
}
function maxAllDwellers() {
if (!currentSaveData || !currentSaveData.dwellers || !currentSaveData.dwellers.dwellers) {
showToast('No dwellers found', 'warning');
return;
}
const dwellers = currentSaveData.dwellers.dwellers;
let modifiedCount = 0;
dwellers.forEach((dweller, index) => {
// Max level
if (dweller.experience) {
dweller.experience.currentLevel = 50;
dweller.experience.experienceValue = 2916000; // Max XP for level 50
}
// Max happiness and health
if (dweller.happiness) {
dweller.happiness.happinessValue = 100;
}
if (dweller.health) {
dweller.health.healthValue = 100;
}
// Max SPECIAL
if (!dweller.serializeableSpecialStats) {
dweller.serializeableSpecialStats = { stats: {} };
}
if (!dweller.serializeableSpecialStats.stats) {
dweller.serializeableSpecialStats.stats = {};
}
for (let i = 1; i <= 7; i++) {
dweller.serializeableSpecialStats.stats[i] = 10;
}
modifiedCount++;
});
if (modifiedCount > 0) {
setModified(true);
updateRawData();
populateDwellers(); // Refresh the display
showToast(`Maxed out ${modifiedCount} dwellers`, 'success');
}
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}