Files
rpg-companion-sillytavern/src/core/persistence.js
T
Lucas 'Paperboy' Rose-Winters e21e71b03a fix(inventory): ensure stored locations always initialized properly
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
2025-10-20 07:19:46 +11:00

248 lines
8.1 KiB
JavaScript

/**
* Core Persistence Module
* Handles saving/loading extension settings and chat data
*/
import { saveSettingsDebounced, chat_metadata, saveChatDebounced } from '../../../../../../script.js';
import { power_user } from '../../../../../power-user.js';
import { getContext } from '../../../../../extensions.js';
import {
extensionSettings,
lastGeneratedData,
setExtensionSettings,
updateExtensionSettings,
setLastGeneratedData,
FEATURE_FLAGS
} from './state.js';
import { migrateInventory } from '../utils/migration.js';
import { validateStoredInventory } from '../utils/security.js';
const extensionName = 'third-party/rpg-companion-sillytavern';
/**
* Loads the extension settings from the global settings object.
* Automatically migrates v1 inventory to v2 format if needed.
*/
export function loadSettings() {
if (power_user.extensions && power_user.extensions[extensionName]) {
updateExtensionSettings(power_user.extensions[extensionName]);
// console.log('[RPG Companion] Settings loaded:', extensionSettings);
} else {
// console.log('[RPG Companion] No saved settings found, using defaults');
}
// Migrate inventory if feature flag enabled
if (FEATURE_FLAGS.useNewInventory) {
const migrationResult = migrateInventory(extensionSettings.userStats.inventory);
if (migrationResult.migrated) {
console.log(`[RPG Companion] Inventory migrated from ${migrationResult.source} to v2 format`);
extensionSettings.userStats.inventory = migrationResult.inventory;
saveSettings(); // Persist migrated inventory
}
}
// Validate inventory structure (Bug #3 fix)
validateInventoryStructure(extensionSettings.userStats.inventory, 'settings');
}
/**
* Saves the extension settings to the global settings object.
*/
export function saveSettings() {
if (!power_user.extensions) {
power_user.extensions = {};
}
power_user.extensions[extensionName] = extensionSettings;
saveSettingsDebounced();
}
/**
* Saves RPG data to the current chat's metadata.
*/
export function saveChatData() {
if (!chat_metadata) {
return;
}
chat_metadata.rpg_companion = {
userStats: extensionSettings.userStats,
classicStats: extensionSettings.classicStats,
lastGeneratedData: lastGeneratedData,
timestamp: Date.now()
};
saveChatDebounced();
}
/**
* Updates the last assistant message's swipe data with current tracker data.
* This ensures user edits are preserved across swipes and included in generation context.
*/
export function updateMessageSwipeData() {
const chat = getContext().chat;
if (!chat || chat.length === 0) {
return;
}
// Find the last assistant message
for (let i = chat.length - 1; i >= 0; i--) {
const message = chat[i];
if (!message.is_user) {
// Found last assistant message - update its swipe data
if (!message.extra) {
message.extra = {};
}
if (!message.extra.rpg_companion_swipes) {
message.extra.rpg_companion_swipes = {};
}
const swipeId = message.swipe_id || 0;
message.extra.rpg_companion_swipes[swipeId] = {
userStats: lastGeneratedData.userStats,
infoBox: lastGeneratedData.infoBox,
characterThoughts: lastGeneratedData.characterThoughts
};
// console.log('[RPG Companion] Updated message swipe data after user edit');
break;
}
}
}
/**
* Loads RPG data from the current chat's metadata.
* Automatically migrates v1 inventory to v2 format if needed.
*/
export function loadChatData() {
if (!chat_metadata || !chat_metadata.rpg_companion) {
// Reset to defaults if no data exists
updateExtensionSettings({
userStats: {
health: 100,
satiety: 100,
energy: 100,
hygiene: 100,
arousal: 0,
mood: '😐',
conditions: 'None',
// Use v2 inventory format for defaults
inventory: {
version: 2,
onPerson: "None",
stored: {},
assets: "None"
}
}
});
setLastGeneratedData({
userStats: null,
infoBox: null,
characterThoughts: null,
html: null
});
return;
}
const savedData = chat_metadata.rpg_companion;
// Restore stats
if (savedData.userStats) {
extensionSettings.userStats = { ...savedData.userStats };
}
// Restore classic stats
if (savedData.classicStats) {
extensionSettings.classicStats = { ...savedData.classicStats };
}
// Restore last generated data
if (savedData.lastGeneratedData) {
setLastGeneratedData({ ...savedData.lastGeneratedData });
}
// Migrate inventory in chat data if feature flag enabled
if (FEATURE_FLAGS.useNewInventory && extensionSettings.userStats.inventory) {
const migrationResult = migrateInventory(extensionSettings.userStats.inventory);
if (migrationResult.migrated) {
console.log(`[RPG Companion] Chat inventory migrated from ${migrationResult.source} to v2 format`);
extensionSettings.userStats.inventory = migrationResult.inventory;
saveChatData(); // Persist migrated inventory to chat metadata
}
}
// 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();
}
}
}