/** * Save Game Editor - Public API * * High-level functions for managing LEGO Island save files */ // Re-export format utilities (from formats/) export { BinaryReader, BinaryWriter } from '../formats/index.js'; export { SaveGameParser, parseSaveGame } from '../formats/index.js'; export { SaveGameSerializer, createSerializer } from '../formats/index.js'; export { PlayersParser, parsePlayers } from '../formats/index.js'; export { PlayersSerializer, createPlayersSerializer } from '../formats/index.js'; export * from './constants.js'; export * from './colorUtils.js'; // Import dependencies import { readBinaryFile, writeBinaryFile, fileExists, listFiles } from '../opfs.js'; import { parseSaveGame, parsePlayers, createSerializer, createPlayersSerializer } from '../formats/index.js'; import { getSaveFileName, PLAYERS_FILE, Actor, ActorNames } from './constants.js'; /** * @typedef {Object} SaveSlot * @property {number} slotNumber - Slot number (0-9) * @property {boolean} exists - Whether the save file exists * @property {string} fileName - Save file name * @property {Object|null} header - Parsed header data * @property {Object|null} missions - Mission scores * @property {Map|null} variables - Parsed variables (name -> { value, nameOffset, valueOffset, valueLength }) * @property {string|null} playerName - Player name from Players.gsi * @property {ArrayBuffer|null} buffer - Raw file buffer (for editing) */ /** * Cache for player names (avoids re-parsing Players.gsi) */ let playersCache = null; /** * Load and cache player names from Players.gsi * @returns {Promise<{ count: number, players: Array<{ letters: number[], name: string }> }|null>} */ async function loadPlayers() { if (playersCache) return playersCache; const buffer = await readBinaryFile(PLAYERS_FILE); if (!buffer) return null; playersCache = parsePlayers(buffer); return playersCache; } /** * Clear the players cache (call when Players.gsi changes) */ export function clearPlayersCache() { playersCache = null; } /** * Get player name by index * @param {number} index - Player index in Players.gsi * @returns {Promise} */ async function getPlayerName(index) { const players = await loadPlayers(); if (!players || index < 0 || index >= players.players.length) { return null; } return players.players[index].name; } /** * List all save slots (0-9) with basic info * @returns {Promise} */ export async function listSaveSlots() { const slots = []; // First load players to resolve names await loadPlayers(); for (let i = 0; i < 10; i++) { const fileName = getSaveFileName(i); const exists = await fileExists(fileName); const slot = { slotNumber: i, exists, fileName, header: null, missions: null, variables: null, act1State: null, characters: null, charactersOffset: null, playerName: null, buffer: null }; if (exists) { try { const buffer = await readBinaryFile(fileName); if (buffer) { const parsed = parseSaveGame(buffer); slot.header = parsed.header; slot.missions = parsed.missions; slot.variables = parsed.variables; slot.act1State = parsed.act1State || null; slot.characters = parsed.characters || null; slot.charactersOffset = parsed.charactersOffset || null; slot.buffer = buffer; // Try to get player name // The playerId in the save file corresponds to an index in Players.gsi // Actually, we need to match by looking at order - player names are stored // in order of most recently played, so we just use the index if (playersCache && playersCache.players.length > 0) { // Find player by matching the slot with Players.gsi order // For now, just use the first player if available // In reality, we'd need to match by player_id const playerIndex = i < playersCache.players.length ? i : 0; if (playersCache.players[playerIndex]) { slot.playerName = playersCache.players[playerIndex].name; } } } } catch (e) { console.error(`Failed to parse save slot ${i}:`, e); } } slots.push(slot); } return slots; } /** * Load a specific save slot with full details * @param {number} slotNumber - Slot number (0-9) * @returns {Promise} */ export async function loadSaveSlot(slotNumber) { if (slotNumber < 0 || slotNumber > 9) { throw new Error('Invalid slot number'); } const fileName = getSaveFileName(slotNumber); const buffer = await readBinaryFile(fileName); if (!buffer) { return null; } const parsed = parseSaveGame(buffer); const players = await loadPlayers(); let playerName = null; if (players && players.players.length > 0) { // Use slot index as a simple mapping const playerIndex = slotNumber < players.players.length ? slotNumber : 0; if (players.players[playerIndex]) { playerName = players.players[playerIndex].name; } } return { slotNumber, exists: true, fileName, header: parsed.header, missions: parsed.missions, variables: parsed.variables, act1State: parsed.act1State || null, characters: parsed.characters || null, charactersOffset: parsed.charactersOffset || null, playerName, buffer }; } /** * Save changes to a save slot * @param {number} slotNumber - Slot number (0-9) * @param {ArrayBuffer} buffer - Modified buffer to save * @param {boolean} [silent=false] - If true, don't show toast * @returns {Promise} */ export async function saveSaveSlot(slotNumber, buffer, silent = false) { if (slotNumber < 0 || slotNumber > 9) { throw new Error('Invalid slot number'); } const fileName = getSaveFileName(slotNumber); return await writeBinaryFile(fileName, buffer, silent, 'Save updated'); } /** * Update a save slot with specific changes * @param {number} slotNumber - Slot number (0-9) * @param {Object} updates - Updates to apply * @param {Object} [updates.header] - Header updates (currentAct, actorId) * @param {Object} [updates.missionScore] - Single mission score update * @returns {Promise} - Updated slot data */ export async function updateSaveSlot(slotNumber, updates) { const slot = await loadSaveSlot(slotNumber); if (!slot || !slot.buffer) { console.error('Failed to load save slot'); return null; } const serializer = createSerializer(slot.buffer); let newBuffer = slot.buffer; let modified = false; // Apply header updates if (updates.header) { newBuffer = serializer.updateHeader(updates.header); modified = true; } // Apply mission score update if (updates.missionScore) { const { missionType, actorId, scoreType, value } = updates.missionScore; // Create new serializer with current buffer state const scoreSerializer = createSerializer(newBuffer); // updateMissionScore will add missing states automatically const result = scoreSerializer.updateMissionScore(missionType, actorId, scoreType, value); if (result) { newBuffer = result; modified = true; } } // Apply variable update if (updates.variable) { const { name, value } = updates.variable; const varSerializer = createSerializer(newBuffer); const result = varSerializer.updateVariable(name, value); if (result) { newBuffer = result; modified = true; } } // Apply character update(s) if (updates.character) { const entries = Array.isArray(updates.character) ? updates.character : [updates.character]; for (const { characterIndex, field, value } of entries) { const charSerializer = createSerializer(newBuffer); const result = charSerializer.updateCharacter(characterIndex, field, value); if (result) { newBuffer = result; modified = true; } } } // Apply texture update if (updates.texture) { const { textureName, textureData } = updates.texture; const texSerializer = createSerializer(newBuffer); const result = texSerializer.updateAct1Texture(textureName, textureData); if (result) { newBuffer = result; modified = true; } } // Only save if something was actually modified if (!modified) { return slot; } // Save the modified buffer const success = await saveSaveSlot(slotNumber, newBuffer); if (!success) { console.error('Failed to save slot'); return null; } // Re-load and return updated slot return await loadSaveSlot(slotNumber); } /** * Get all existing save slots (convenience function) * @returns {Promise} */ export async function getExistingSaveSlots() { const allSlots = await listSaveSlots(); return allSlots.filter(slot => slot.exists); } /** * Update a player's name in Players.gsi * @param {number} playerIndex - Index in Players.gsi (0-8) * @param {string} newName - New name (max 7 chars, A-Z only) * @returns {Promise} - True if successful */ export async function updatePlayerName(playerIndex, newName) { // Load current players data const players = await loadPlayers(); if (!players) { console.error('Failed to load Players.gsi'); return false; } if (playerIndex < 0 || playerIndex >= players.players.length) { console.error('Invalid player index'); return false; } // Create serializer and update name const serializer = createPlayersSerializer(players); serializer.updateName(playerIndex, newName); // Serialize and save const buffer = serializer.serialize(); const success = await writeBinaryFile(PLAYERS_FILE, buffer, false, 'Save updated'); if (success) { // Clear cache so next load gets fresh data clearPlayersCache(); } return success; }