From e21e71b03acec5126113c5c37c540fe9ca9fda68 Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Mon, 20 Oct 2025 07:19:46 +1100 Subject: [PATCH] fix(inventory): ensure stored locations always initialized properly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes Bug #3: Locations disappearing when switching tabs or on reload. Root cause: inventory.stored could become corrupted (null, array, or undefined) due to incomplete validation during load/save operations. Solution - Defense in Depth: 1. **Persistence Layer** (src/core/persistence.js): - New validateInventoryStructure() function - Validates on loadSettings() and loadChatData() - Checks all v2 fields (onPerson, stored, assets, version) - Ensures stored is always a plain object - Validates stored keys/values using validateStoredInventory() - Auto-repairs corrupted data with console warnings - Persists repairs immediately 2. **Form State Management** (src/systems/interaction/inventoryActions.js): - Enhanced restoreFormStates() to detect deleted locations - Cleans up orphaned form states automatically - Prevents errors from forms referencing non-existent locations Validation checks: - ✓ inventory.stored is object (not null/array/undefined) - ✓ All stored keys are safe (no __proto__, constructor, etc.) - ✓ All stored values are strings - ✓ onPerson and assets are strings - ✓ version field exists Auto-repair scenarios: - Corrupted stored → reset to {} - Invalid onPerson/assets → reset to "None" - Missing version → set to 2 - Dangerous keys → removed with warning Result: - Locations persist across tab switches ✓ - Empty locations persist ✓ - Data corruption auto-repaired on load ✓ - Orphaned form states cleaned up ✓ - No crashes from invalid data ✓ Fixes: Location disappears when switching tabs or reloading --- src/core/persistence.js | 77 +++++++++++++++++++++ src/systems/interaction/inventoryActions.js | 25 +++++-- 2 files changed, 98 insertions(+), 4 deletions(-) diff --git a/src/core/persistence.js b/src/core/persistence.js index e143bdd..30e04cd 100644 --- a/src/core/persistence.js +++ b/src/core/persistence.js @@ -15,6 +15,7 @@ import { FEATURE_FLAGS } from './state.js'; import { migrateInventory } from '../utils/migration.js'; +import { validateStoredInventory } from '../utils/security.js'; const extensionName = 'third-party/rpg-companion-sillytavern'; @@ -39,6 +40,9 @@ export function loadSettings() { saveSettings(); // Persist migrated inventory } } + + // Validate inventory structure (Bug #3 fix) + validateInventoryStructure(extensionSettings.userStats.inventory, 'settings'); } /** @@ -166,5 +170,78 @@ export function loadChatData() { } } + // Validate inventory structure (Bug #3 fix) + validateInventoryStructure(extensionSettings.userStats.inventory, 'chat'); + // console.log('[RPG Companion] Loaded chat data:', savedData); } + +/** + * Validates and repairs inventory structure to prevent corruption. + * Ensures all v2 fields exist and are the correct type. + * Fixes Bug #3: Location disappears when switching tabs + * + * @param {Object} inventory - Inventory object to validate + * @param {string} source - Source of load ('settings' or 'chat') for logging + * @private + */ +function validateInventoryStructure(inventory, source) { + if (!inventory || typeof inventory !== 'object') { + console.error(`[RPG Companion] Invalid inventory from ${source}, resetting to defaults`); + extensionSettings.userStats.inventory = { + version: 2, + onPerson: "None", + stored: {}, + assets: "None" + }; + saveSettings(); + return; + } + + let needsSave = false; + + // Ensure v2 structure + if (inventory.version !== 2) { + console.warn(`[RPG Companion] Inventory from ${source} missing version, setting to 2`); + inventory.version = 2; + needsSave = true; + } + + // Validate onPerson field + if (typeof inventory.onPerson !== 'string') { + console.warn(`[RPG Companion] Invalid onPerson from ${source}, resetting to "None"`); + inventory.onPerson = "None"; + needsSave = true; + } + + // Validate stored field (CRITICAL for Bug #3) + if (!inventory.stored || typeof inventory.stored !== 'object' || Array.isArray(inventory.stored)) { + console.error(`[RPG Companion] Corrupted stored inventory from ${source}, resetting to empty object`); + inventory.stored = {}; + needsSave = true; + } else { + // Validate stored object keys/values + const cleanedStored = validateStoredInventory(inventory.stored); + if (JSON.stringify(cleanedStored) !== JSON.stringify(inventory.stored)) { + console.warn(`[RPG Companion] Cleaned dangerous/invalid stored locations from ${source}`); + inventory.stored = cleanedStored; + needsSave = true; + } + } + + // Validate assets field + if (typeof inventory.assets !== 'string') { + console.warn(`[RPG Companion] Invalid assets from ${source}, resetting to "None"`); + inventory.assets = "None"; + needsSave = true; + } + + // Persist repairs if needed + if (needsSave) { + console.log(`[RPG Companion] Repaired inventory structure from ${source}, saving...`); + saveSettings(); + if (source === 'chat') { + saveChatData(); + } + } +} diff --git a/src/systems/interaction/inventoryActions.js b/src/systems/interaction/inventoryActions.js index 3038764..74eaf09 100644 --- a/src/systems/interaction/inventoryActions.js +++ b/src/systems/interaction/inventoryActions.js @@ -539,6 +539,7 @@ export function getInventoryRenderOptions() { /** * Restores the state of inline forms after re-rendering. * This ensures forms that were open before re-render are shown again. + * Also cleans up orphaned form states for deleted locations (Bug #3 fix). */ export function restoreFormStates() { // Restore add location form @@ -570,15 +571,31 @@ export function restoreFormStates() { } // Restore add item stored forms (for each location) + // Clean up orphaned states for deleted locations (Bug #3 fix) if (openForms.addItemStored && typeof openForms.addItemStored === 'object') { + const inventory = extensionSettings.userStats.inventory; + const locationsToDelete = []; + for (const location in openForms.addItemStored) { if (openForms.addItemStored[location]) { - const locationId = location.replace(/\s+/g, '-'); - const form = $(`#rpg-add-item-form-stored-${locationId}`); - if (form.length > 0) { - form.show(); + // Check if location still exists in inventory + if (inventory?.stored && inventory.stored.hasOwnProperty(location)) { + // Location exists, restore form + const locationId = location.replace(/\s+/g, '-'); + const form = $(`#rpg-add-item-form-stored-${locationId}`); + if (form.length > 0) { + form.show(); + } + } else { + // Location was deleted, mark for cleanup + locationsToDelete.push(location); } } } + + // Clean up orphaned form states + for (const location of locationsToDelete) { + delete openForms.addItemStored[location]; + } } }