From 38fb3d8c51f563bc42db0936474b109e9b724cfe Mon Sep 17 00:00:00 2001 From: Spicy_Marinara Date: Mon, 4 May 2026 13:08:52 +0200 Subject: [PATCH] Fix tracker issues and add deprecation notice --- .markdownlint.json | 3 + README.md | 15 +- index.js | 12 +- manifest.json | 2 +- package.json | 2 +- src/core/persistence.js | 289 ++++++++++++++++++++++++- src/core/state.js | 36 ++- src/systems/integration/sillytavern.js | 5 +- src/systems/rendering/quests.js | 2 +- src/systems/ui/modals.js | 63 ++++++ template.html | 36 +++ 11 files changed, 423 insertions(+), 42 deletions(-) create mode 100644 .markdownlint.json diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..67d2ae5 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,3 @@ +{ + "MD013": false +} diff --git a/README.md b/README.md index 641a003..831214a 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ An immersive RPG extension for browsers that tracks character stats, scene infor Moving on to developing the Marinara Engine frontend, the extension will now be maintained by the community! -https://github.com/Pasta-Devs/Marinara-Engine + ## 📥 Installation @@ -21,7 +21,7 @@ https://github.com/Pasta-Devs/Marinara-Engine 3. Go to Install extension -4. Copy-paste this link: https://github.com/SpicyMarinara/rpg-companion-sillytavern +4. Copy-paste this link: 5. Press Install for all users/Install just for me @@ -99,11 +99,13 @@ AI: Trackers + Full roleplay response ↓ Main chat shows clean roleplay text Pros: + - Single API call - Faster response - Simpler setup Cons: + - Tracker formatting mixed in AI response - May affect roleplay quality slightly @@ -127,11 +129,13 @@ AI: Separate call with just the tracker data ↓ Context summary injected into the next generation Pros: + - Clean roleplay responses - Better roleplay quality - Contextual summary enhances immersion Cons: + - Extra API call - Slightly slower @@ -163,16 +167,19 @@ You can edit most fields by clicking on them: Access comprehensive customization through the Tracker Settings button: **User Stats Configuration:** + - Add/remove custom stats with unique names - Configure Status section (mood emoji + custom fields) - Configure Skills section with custom skill fields - Toggle RPG attributes display **Info Box Configuration:** + - Enable/disable individual widgets (Date, Weather, Temperature, Time, Location, Recent Events) - Choose temperature unit (Celsius/Fahrenheit) **Present Characters Configuration:** + - Add custom character fields (appearance, action, demeanor, etc.) - Configure relationship status options - Enable character-specific stats tracking @@ -199,11 +206,11 @@ This extension detects when a "guided generation" prompt is submitted (for examp If you want tracker prompts to apply during a guided generation, run the update via separate generation or temporarily disable guided generation in the other extension. There is a new setting "Skip Tracker & HTML Injections during Guided Generations" in the RPG Companion settings (Advanced section). It now supports three modes: + - none: never skip (always inject the tracker prompts as usual, default) - impersonation: only skip when an impersonation-style guided generation is detected - guided: skip whenever a guided `instruct` or `quiet_prompt` generation is detected - ## 🎨 Themes Choose from 6 beautiful themes: @@ -286,4 +293,4 @@ SpicyMarinara, Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDea Made with ❤️ by Marinara PS I'm looking for a job or a sponsor to fund my custom AI frontend, contact me if interested: -mgrabower97@gmail.com +[mgrabower97@gmail.com](mailto:mgrabower97@gmail.com) diff --git a/index.js b/index.js index 7b39b84..738c91e 100644 --- a/index.js +++ b/index.js @@ -95,7 +95,8 @@ import { updateDiceDisplay, addDiceQuickReply, getSettingsModal, - showWelcomeModalIfNeeded + showWelcomeModalIfNeeded, + showDeprecationModalIfNeeded } from './src/systems/ui/modals.js'; import { initTrackerEditor @@ -1511,11 +1512,14 @@ jQuery(async () => { // Non-critical - continue without it } - // Show welcome modal for v3.0 on first launch + // Show deprecation notice once for this release; otherwise keep the old welcome flow. try { - showWelcomeModalIfNeeded(); + const deprecationModalShown = showDeprecationModalIfNeeded(); + if (!deprecationModalShown) { + showWelcomeModalIfNeeded(); + } } catch (error) { - console.error('[RPG Companion] Welcome modal failed:', error); + console.error('[RPG Companion] Startup modal failed:', error); // Non-critical - continue without it } diff --git a/manifest.json b/manifest.json index 55c1c6d..2258425 100644 --- a/manifest.json +++ b/manifest.json @@ -6,6 +6,6 @@ "js": "index.js", "css": "style.css", "author": "Marinara", - "version": "3.7.3", + "version": "3.7.4", "homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern" } diff --git a/package.json b/package.json index b290249..b3bfc76 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rpg-complanion-sillytavern", - "version": "3.7.3", + "version": "3.7.4", "description": "", "main": "index.js", "scripts": { diff --git a/src/core/persistence.js b/src/core/persistence.js index 15f3e02..79e58ca 100644 --- a/src/core/persistence.js +++ b/src/core/persistence.js @@ -3,7 +3,7 @@ * Handles saving/loading extension settings and chat data */ -import { saveSettingsDebounced, chat_metadata, saveChatDebounced } from '../../../../../../script.js'; +import { saveSettingsDebounced, chat_metadata, saveChatDebounced, getCurrentChatId } from '../../../../../../script.js'; import { getContext } from '../../../../../extensions.js'; import { extensionSettings, @@ -23,6 +23,245 @@ import { validateStoredInventory, cleanItemString } from '../utils/security.js'; import { migrateToV3JSON } from '../utils/jsonMigration.js'; const extensionName = 'third-party/rpg-companion-sillytavern'; +const CURRENT_SETTINGS_VERSION = 5; + +const DEFAULT_USER_STATS = { + health: 100, + satiety: 100, + energy: 100, + hygiene: 100, + arousal: 0, + mood: '😐', + conditions: 'None', + skills: [], + inventory: { + version: 2, + onPerson: "None", + clothing: "None", + stored: {}, + assets: "None" + } +}; + +const DEFAULT_EXTENSION_SETTINGS = cloneSerializable(extensionSettings); +DEFAULT_EXTENSION_SETTINGS.settingsVersion = CURRENT_SETTINGS_VERSION; + +let hasDeferredChatDataSave = false; + +function cloneSerializable(value) { + if (value === undefined) { + return undefined; + } + + try { + return structuredClone(value); + } catch { + return JSON.parse(JSON.stringify(value)); + } +} + +function isPlainObject(value) { + return !!value && typeof value === 'object' && !Array.isArray(value); +} + +function mergeWithDefaults(defaultValue, savedValue) { + if (savedValue === undefined) { + return cloneSerializable(defaultValue); + } + + if (isPlainObject(defaultValue) && isPlainObject(savedValue)) { + const merged = cloneSerializable(defaultValue); + for (const [key, value] of Object.entries(savedValue)) { + merged[key] = mergeWithDefaults(defaultValue[key], value); + } + return merged; + } + + return cloneSerializable(savedValue); +} + +function parseMaybeJSON(value) { + if (typeof value !== 'string') { + return value; + } + + const trimmed = value.trim(); + if (!trimmed || (!trimmed.startsWith('{') && !trimmed.startsWith('['))) { + return value; + } + + try { + return JSON.parse(trimmed); + } catch { + return value; + } +} + +function stringifyInventoryItems(items) { + if (typeof items === 'string') { + return items.trim() || 'None'; + } + + if (!Array.isArray(items)) { + return 'None'; + } + + const text = items + .map(item => { + if (isPlainObject(item) && item.name) { + const quantity = Number(item.quantity); + return quantity > 1 ? `${quantity}x ${item.name}` : item.name; + } + return String(item || '').trim(); + }) + .filter(Boolean) + .join(', '); + + return text || 'None'; +} + +function normalizeStoredInventory(stored) { + if (!isPlainObject(stored)) { + return {}; + } + + const normalized = {}; + for (const [location, items] of Object.entries(stored)) { + normalized[location] = stringifyInventoryItems(items); + } + return normalized; +} + +function normalizeInventoryValue(inventory) { + const parsedInventory = parseMaybeJSON(inventory); + + if (isPlainObject(parsedInventory) && ( + Array.isArray(parsedInventory.onPerson) + || Array.isArray(parsedInventory.clothing) + || Array.isArray(parsedInventory.assets) + || isPlainObject(parsedInventory.stored) + )) { + return { + version: 2, + onPerson: stringifyInventoryItems(parsedInventory.onPerson), + clothing: stringifyInventoryItems(parsedInventory.clothing), + stored: normalizeStoredInventory(parsedInventory.stored), + assets: stringifyInventoryItems(parsedInventory.assets) + }; + } + + const migrationResult = migrateInventory(parsedInventory); + return mergeWithDefaults(DEFAULT_USER_STATS.inventory, migrationResult.inventory); +} + +function normalizeUserStatsValue(userStats) { + const parsedStats = parseMaybeJSON(userStats); + const normalized = cloneSerializable(DEFAULT_USER_STATS); + + if (!isPlainObject(parsedStats)) { + return normalized; + } + + if (Array.isArray(parsedStats.stats)) { + for (const stat of parsedStats.stats) { + if (!stat || typeof stat !== 'object') continue; + const id = stat.id || stat.name?.toLowerCase?.(); + if (id && stat.value !== undefined) { + normalized[id] = stat.value; + } + } + } else { + for (const [key, value] of Object.entries(parsedStats)) { + if (!['stats', 'status', 'inventory', 'quests'].includes(key) && value !== undefined) { + normalized[key] = cloneSerializable(value); + } + } + } + + if (isPlainObject(parsedStats.status)) { + for (const [key, value] of Object.entries(parsedStats.status)) { + if (value !== undefined) { + normalized[key] = cloneSerializable(value); + } + } + } + + if (parsedStats.inventory !== undefined) { + normalized.inventory = normalizeInventoryValue(parsedStats.inventory); + } + + for (const [key, defaultValue] of Object.entries(DEFAULT_USER_STATS)) { + if (typeof defaultValue !== 'number') continue; + + const numericValue = Number(normalized[key]); + normalized[key] = Number.isFinite(numericValue) ? numericValue : defaultValue; + } + + return mergeWithDefaults(DEFAULT_USER_STATS, normalized); +} + +function normalizeQuestValue(quest) { + let value = quest; + while (isPlainObject(value) && value.value !== undefined) { + value = value.value; + } + + if (typeof value === 'string') { + return value.trim() || 'None'; + } + + if (isPlainObject(value)) { + return value.title || value.description || JSON.stringify(value); + } + + return value == null ? 'None' : String(value); +} + +function normalizeQuestsValue(quests) { + if (!isPlainObject(quests)) { + return { main: 'None', optional: [] }; + } + + const optionalSource = Array.isArray(quests.optional) + ? quests.optional + : (Array.isArray(quests.active) ? quests.active : []); + + return { + main: normalizeQuestValue(quests.main), + optional: optionalSource + .map(normalizeQuestValue) + .filter(quest => quest && quest !== 'None') + }; +} + +function normalizeSettings(savedSettings) { + const sourceSettings = isPlainObject(savedSettings) ? savedSettings : {}; + const normalized = mergeWithDefaults(DEFAULT_EXTENSION_SETTINGS, sourceSettings); + const savedVersion = Number(sourceSettings.settingsVersion); + normalized.settingsVersion = Number.isFinite(savedVersion) && savedVersion > 0 ? savedVersion : 1; + normalized.userStats = normalizeUserStatsValue(sourceSettings.userStats); + + const parsedUserStats = parseMaybeJSON(sourceSettings.userStats); + if (sourceSettings.quests !== undefined) { + normalized.quests = normalizeQuestsValue(sourceSettings.quests); + } else if (isPlainObject(parsedUserStats) && parsedUserStats.quests !== undefined) { + normalized.quests = normalizeQuestsValue(parsedUserStats.quests); + } + + return { + settings: normalized, + changed: JSON.stringify(normalized) !== JSON.stringify(savedSettings) + }; +} + +function isChatDataSaveReady() { + return !!( + chat_metadata + && typeof chat_metadata === 'object' + && chat_metadata.integrity + && getCurrentChatId() + ); +} function hasTrackerPayload(payload) { return !!(payload && typeof payload === 'object' && ( @@ -273,7 +512,8 @@ function validateSettings(settings) { // Check for required top-level properties if (typeof settings.enabled !== 'boolean' || typeof settings.autoUpdate !== 'boolean' || - !settings.userStats || typeof settings.userStats !== 'object') { + !settings.userStats || typeof settings.userStats !== 'object' || + Array.isArray(settings.userStats)) { console.warn('[RPG Companion] Settings validation failed: missing required properties'); return false; } @@ -282,7 +522,8 @@ function validateSettings(settings) { const stats = settings.userStats; if (typeof stats.health !== 'number' || typeof stats.satiety !== 'number' || - typeof stats.energy !== 'number') { + typeof stats.energy !== 'number' || + !stats.inventory || typeof stats.inventory !== 'object') { console.warn('[RPG Companion] Settings validation failed: invalid userStats structure'); return false; } @@ -307,21 +548,23 @@ export function loadSettings() { if (extension_settings[extensionName]) { const savedSettings = extension_settings[extensionName]; + const normalizedResult = normalizeSettings(savedSettings); + const normalizedSettings = normalizedResult.settings; - // Validate loaded settings - if (!validateSettings(savedSettings)) { + // Validate loaded settings after schema repair/normalization + if (!validateSettings(normalizedSettings)) { console.warn('[RPG Companion] Loaded settings failed validation, using defaults'); - console.warn('[RPG Companion] Invalid settings:', savedSettings); + console.warn('[RPG Companion] Invalid settings:', normalizedSettings); // Save valid defaults to replace corrupt data saveSettings(); return; } - updateExtensionSettings(savedSettings); + updateExtensionSettings(normalizedSettings); // Perform settings migrations based on version const currentVersion = extensionSettings.settingsVersion || 1; - let settingsChanged = false; + let settingsChanged = normalizedResult.changed; // Migration to version 2: Enable dynamic weather for existing users if (currentVersion < 2) { @@ -455,7 +698,8 @@ export function saveSettings() { * Saves RPG data to the current chat's metadata. */ export function saveChatData() { - if (!chat_metadata) { + if (!isChatDataSaveReady()) { + hasDeferredChatDataSave = true; return; } @@ -480,6 +724,16 @@ export function saveChatData() { saveChatDebounced(); } +export function flushDeferredChatDataSave() { + if (!hasDeferredChatDataSave || !isChatDataSaveReady()) { + return false; + } + + hasDeferredChatDataSave = false; + saveChatData(); + return true; +} + /** * Mirrors a tracker data entry into message.swipe_info so it survives page reloads. * ST only serializes swipe_info to disk; message.extra is in-memory only. @@ -711,6 +965,7 @@ export function loadChatData() { inventory: { version: 2, onPerson: "None", + clothing: "None", stored: {}, assets: "None" } @@ -830,6 +1085,7 @@ function validateInventoryStructure(inventory, source) { extensionSettings.userStats.inventory = { version: 2, onPerson: "None", + clothing: "None", stored: {}, assets: "None" }; @@ -861,6 +1117,20 @@ function validateInventoryStructure(inventory, source) { } } + // Validate clothing field + if (typeof inventory.clothing !== 'string') { + console.warn(`[RPG Companion] Invalid clothing from ${source}, resetting to "None"`); + inventory.clothing = "None"; + needsSave = true; + } else { + const cleanedClothing = cleanItemString(inventory.clothing); + if (cleanedClothing !== inventory.clothing) { + console.warn(`[RPG Companion] Cleaned corrupted items from clothing inventory (${source})`); + inventory.clothing = cleanedClothing; + 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`); @@ -1559,4 +1829,3 @@ export function importPresets(importData, overwrite = false) { return importCount; } - diff --git a/src/core/state.js b/src/core/state.js index 6ebff56..c2925ef 100644 --- a/src/core/state.js +++ b/src/core/state.js @@ -10,7 +10,7 @@ * Extension settings - persisted to SillyTavern settings */ export let extensionSettings = { - settingsVersion: 4, // Version number for settings migrations + settingsVersion: 5, // Version number for settings migrations enabled: true, autoUpdate: false, updateDepth: 4, // How many messages to include in the context @@ -108,27 +108,23 @@ export let extensionSettings = { stats: { enabled: true }, // All stats as compact numbers attributes: { enabled: true } // Compact RPG attributes display }, - userStats: JSON.stringify({ - stats: [ - { id: 'health', name: 'Health', value: 100 }, - { id: 'satiety', name: 'Satiety', value: 100 }, - { id: 'energy', name: 'Energy', value: 100 }, - { id: 'hygiene', name: 'Hygiene', value: 100 }, - { id: 'arousal', name: 'Arousal', value: 0 } - ], - status: { - mood: '😐', - conditions: 'None' - }, + userStats: { + health: 100, + satiety: 100, + energy: 100, + hygiene: 100, + arousal: 0, + mood: '😐', + conditions: 'None', + skills: [], inventory: { - onPerson: [], - stored: [] - }, - quests: { - active: [], - completed: [] + version: 2, + onPerson: "None", + clothing: "None", + stored: {}, + assets: "None" } - }, null, 2), + }, statNames: { health: 'Health', satiety: 'Satiety', diff --git a/src/systems/integration/sillytavern.js b/src/systems/integration/sillytavern.js index 2298f91..7b64734 100644 --- a/src/systems/integration/sillytavern.js +++ b/src/systems/integration/sillytavern.js @@ -33,7 +33,8 @@ import { setMessageSwipeTrackerData, getSwipeData, commitTrackerDataFromPriorMessage, - inheritSwipeDataFromPriorMessage + inheritSwipeDataFromPriorMessage, + flushDeferredChatDataSave } from '../../core/persistence.js'; import { i18n } from '../../core/i18n.js'; @@ -389,6 +390,7 @@ export function onChatLoaded() { restoreOrRepairLatestTrackerState(); maybeRehydrateUserStatsFromDisplayData(); rerenderRpgState(); + flushDeferredChatDataSave(); scheduleChatStateRehydration(); updateAllCheckpointIndicators(); } @@ -658,6 +660,7 @@ export function onCharacterChanged() { // Load chat-specific data when switching chats loadChatData(); + flushDeferredChatDataSave(); // chat_metadata may not reflect the actual chat tail for branches, so // loadChatData() may have just restored stale data from the parent chat. diff --git a/src/systems/rendering/quests.js b/src/systems/rendering/quests.js index 4667fb0..0c9aea5 100644 --- a/src/systems/rendering/quests.js +++ b/src/systems/rendering/quests.js @@ -219,7 +219,7 @@ export function renderOptionalQuestsView(optionalQuests) { * Main render function for quests */ export function renderQuests() { - if (!extensionSettings.showInventory || !$questsContainer) { + if (!extensionSettings.showQuests || !$questsContainer) { return; } diff --git a/src/systems/ui/modals.js b/src/systems/ui/modals.js index 2c9d577..ac07fd5 100644 --- a/src/systems/ui/modals.js +++ b/src/systems/ui/modals.js @@ -612,6 +612,28 @@ export function showWelcomeModalIfNeeded() { } } +/** + * Shows the deprecation notice once for users updating to the deprecation release. + * @returns {boolean} True when the modal was displayed. + */ +export function showDeprecationModalIfNeeded() { + const DEPRECATION_NOTICE_VERSION = '3.7.4'; + const STORAGE_KEY = 'rpg_companion_deprecation_notice_seen'; + + try { + const seenVersion = localStorage.getItem(STORAGE_KEY); + + if (seenVersion !== DEPRECATION_NOTICE_VERSION) { + showDeprecationModal(DEPRECATION_NOTICE_VERSION, STORAGE_KEY); + return true; + } + } catch (error) { + console.error('[RPG Companion] Failed to check deprecation modal status:', error); + } + + return false; +} + /** * Shows the welcome modal * @param {string} version - The version to mark as seen @@ -663,3 +685,44 @@ function showWelcomeModal(version, storageKey) { } }, { once: true }); } + +function showDeprecationModal(version, storageKey) { + const modal = document.getElementById('rpg-deprecation-modal'); + if (!modal) { + console.error('[RPG Companion] Deprecation modal element not found'); + return; + } + + const theme = extensionSettings.theme || 'default'; + modal.setAttribute('data-theme', theme); + + modal.style.display = 'flex'; + modal.classList.add('is-open'); + + const closeBtn = document.getElementById('rpg-deprecation-close'); + const gotItBtn = document.getElementById('rpg-deprecation-got-it'); + + const closeModal = () => { + modal.classList.add('is-closing'); + + setTimeout(() => { + modal.style.display = 'none'; + modal.classList.remove('is-open', 'is-closing'); + }, 200); + + try { + localStorage.setItem(storageKey, version); + } catch (error) { + console.error('[RPG Companion] Failed to save deprecation modal status:', error); + } + }; + + closeBtn?.addEventListener('click', closeModal, { once: true }); + gotItBtn?.addEventListener('click', closeModal, { once: true }); + + modal.addEventListener('click', (e) => { + if (e.target === modal) { + closeModal(); + } + }, { once: true }); +} diff --git a/template.html b/template.html index b51c991..1cd1f5e 100644 --- a/template.html +++ b/template.html @@ -212,6 +212,42 @@ + + +