/** * RPG Companion - Main Entry Point * SillyTavern extension for RPG-style character tracking. * * This file is a thin orchestrator that delegates to sub-modules. * See src/ directory for the actual implementation. */ import { getContext, renderExtensionTemplateAsync, extension_settings as st_extension_settings } from '../../../extensions.js'; import { eventSource, event_types, substituteParams, chat, saveSettingsDebounced, chat_metadata, saveChatDebounced, user_avatar, getThumbnailUrl, characters, this_chid, extension_prompt_types, extension_prompt_roles, setExtensionPrompt, reloadCurrentChat, Generate, getRequestHeaders } from '../../../../script.js'; import { selected_group, getGroupMembers } from '../../../group-chats.js'; import { power_user } from '../../../power-user.js'; // Core modules import { extensionName, extensionFolderPath } from './src/core/config.js'; import { i18n } from './src/core/i18n.js'; import { migrateToV3JSON } from './src/utils/jsonMigration.js'; import { extensionSettings, lastGeneratedData, committedTrackerData, lastActionWasSwipe, isGenerating, isPlotProgression, pendingDiceRoll, FALLBACK_AVATAR_DATA_URI, $panelContainer, $userStatsContainer, $infoBoxContainer, $thoughtsContainer, $inventoryContainer, $equipmentContainer, $questsContainer, $musicPlayerContainer, setExtensionSettings, updateExtensionSettings, setLastGeneratedData, updateLastGeneratedData, setCommittedTrackerData, updateCommittedTrackerData, setLastActionWasSwipe, setIsGenerating, setIsPlotProgression, setPendingDiceRoll, setPanelContainer, setUserStatsContainer, setInfoBoxContainer, setThoughtsContainer, setInventoryContainer, setEquipmentContainer, setQuestsContainer, setMusicPlayerContainer, clearSessionAvatarPrompts, clearDomCache, clearDebugLogs } from './src/core/state.js'; import { loadSettings, saveSettings, saveChatData, loadChatData, updateMessageSwipeData, commitTrackerDataFromPriorMessage } from './src/core/persistence.js'; import { registerAllEvents, on as onEvent } from './src/core/events.js'; import { addExtensionSettings } from './src/core/settingsPanel.js'; // Generation & Parsing modules import { generateTrackerExample, generateTrackerInstructions, generateContextualSummary, generateRPGPromptText, generateSeparateUpdatePrompt } from './src/systems/generation/promptBuilder.js'; import { parseResponse, parseUserStats } from './src/systems/generation/parser.js'; import { updateRPGData, testExternalAPIConnection } from './src/systems/generation/apiClient.js'; import { onGenerationStarted } from './src/systems/generation/injector.js'; // Rendering modules import { getSafeThumbnailUrl } from './src/utils/avatars.js'; import { renderUserStats } from './src/systems/rendering/userStats.js'; import { renderInfoBox, updateInfoBoxField } from './src/systems/rendering/infoBox.js'; import { renderThoughts, updateCharacterField, removeCharacter, updateChatThoughts, createThoughtPanel } from './src/systems/rendering/thoughts.js'; import { renderInventory } from './src/systems/rendering/inventory.js'; import { renderEquipment } from './src/systems/rendering/equipment.js'; import { renderQuests } from './src/systems/rendering/quests.js'; import { renderMusicPlayer } from './src/systems/rendering/musicPlayer.js'; import { toggleSnowflakes, initSnowflakes } from './src/systems/ui/snowflakes.js'; import { toggleDynamicWeather, initWeatherEffects, updateWeatherEffect } from './src/systems/ui/weatherEffects.js'; // Interaction modules import { initInventoryEventListeners } from './src/systems/interaction/inventoryActions.js'; import { initEquipmentEventListeners } from './src/systems/interaction/equipmentActions.js'; // UI Systems modules import { applyTheme, applyCustomTheme, toggleCustomColors, toggleAnimations, updateFeatureTogglesVisibility, updateSettingsPopupTheme, applyCustomThemeToSettingsPopup } from './src/systems/ui/theme.js'; import { DiceModal, SettingsModal, setupDiceRoller, setupSettingsPopup, updateDiceDisplay, addDiceQuickReply, getSettingsModal, showWelcomeModalIfNeeded, showDeprecationModalIfNeeded } from './src/systems/ui/modals.js'; import { initTrackerEditor } from './src/systems/ui/trackerEditor.js'; import { initPromptsEditor } from './src/systems/ui/promptsEditor.js'; import { initChapterCheckpointUI, injectCheckpointButton, updateAllCheckpointIndicators, cleanupCheckpointUI } from './src/systems/ui/checkpointUI.js'; import { restoreCheckpointOnLoad } from './src/systems/features/chapterCheckpoint.js'; import { togglePlotButtons, updateCollapseToggleIcon, setupCollapseToggle, updatePanelVisibility, updateSectionVisibility, applyPanelPosition, updateGenerationModeUI } from './src/systems/ui/layout.js'; import { setupMobileToggle, constrainFabToViewport, setupMobileTabs, removeMobileTabs, setupMobileKeyboardHandling, setupContentEditableScrolling, updateMobileTabLabels, updateFabWidgets } from './src/systems/ui/mobile.js'; import { setupDesktopTabs, removeDesktopTabs, updateStripWidgets } from './src/systems/ui/desktop.js'; import { removeAlternatePresentCharactersPanel, renderAlternatePresentCharacters } from './src/systems/ui/alternatePresentCharacters.js'; import { initThoughtBasedExpressions, queueThoughtBasedExpressionsUpdate, onThoughtBasedExpressionsSettingChanged, onAlternatePresentCharactersVisibilityChanged, onHideDefaultExpressionDisplaySettingChanged, clearThoughtBasedExpressionsCache, onThoughtBasedExpressionsChatChanged, setThoughtBasedExpressionsRefreshHandler } from './src/systems/integration/thoughtBasedExpressions.js'; // Feature modules import { setupPlotButtons, sendPlotProgression } from './src/systems/features/plotProgression.js'; import { setupClassicStatsButtons } from './src/systems/features/classicStats.js'; import { ensureHtmlCleaningRegex, detectConflictingRegexScripts, ensureTrackerCleaningRegex } from './src/systems/features/htmlCleaning.js'; import { ensureJsonCleaningRegex, removeJsonCleaningRegex } from './src/systems/features/jsonCleaning.js'; import { parseAndStoreSpotifyUrl } from './src/systems/features/musicPlayer.js'; import { DEFAULT_HTML_PROMPT } from './src/systems/generation/promptBuilder.js'; import { openEncounterModal } from './src/systems/ui/encounterUI.js'; // Integration modules import { commitTrackerData, onMessageSent, onMessageReceived, onCharacterChanged, onChatLoaded, onMessageDeleted, onMessageSwiped, scheduleChatStateRehydration, updatePersonaAvatar, clearExtensionPrompts, onGenerationEnded, initHistoryInjection } from './src/systems/integration/sillytavern.js'; // Settings UI event listeners (extracted from initUI) import { bindSettingsListeners, updateWeatherSubOptionsVisibility } from './src/systems/ui/settingsListeners.js'; // Set up thought-based expressions refresh handler setThoughtBasedExpressionsRefreshHandler(() => { renderAlternatePresentCharacters({ useCommittedFallback: true }); }); /** * Updates UI elements that are dynamically generated and not covered by data-i18n-key. */ function updateDynamicLabels() { const refreshBtn = document.getElementById('rpg-manual-update'); if (refreshBtn && !refreshBtn.disabled) { const refreshText = i18n.getTranslation('template.mainPanel.refreshRpgInfo') || 'Refresh RPG Info'; refreshBtn.innerHTML = ` ${refreshText}`; } updateDiceDisplay(); updateMobileTabLabels(); } /** * Initializes the UI for the extension. */ async function initUI() { await i18n.init(); if (!extensionSettings.enabled) { return; } // Load and inject the HTML template const templateHtml = await renderExtensionTemplateAsync(extensionName, 'template'); $('body').append(templateHtml); // Add mobile toggle button (FAB) const theme = extensionSettings.theme || 'default'; const mobileToggleHtml = ` `; $('body').append(mobileToggleHtml); if (window.innerWidth > 1000) { $('#rpg-mobile-toggle').hide(); } // Cache UI elements using state setters // Clear stale DOM references first to prevent memory leaks on panel rebuild clearDomCache(); setPanelContainer($('#rpg-companion-panel')); setUserStatsContainer($('#rpg-user-stats')); setInfoBoxContainer($('#rpg-info-box')); setThoughtsContainer($('#rpg-thoughts')); setInventoryContainer($('#rpg-inventory')); setEquipmentContainer($('#rpg-equipment')); setQuestsContainer($('#rpg-quests')); setMusicPlayerContainer($('#rpg-music-player')); // Re-apply translations to catch all new elements from the template i18n.applyTranslations(document.body); // Bind all settings event listeners (extracted to settingsListeners.js) bindSettingsListeners($); // Initialize mobile UI setupMobileToggle(); constrainFabToViewport(); setupMobileTabs(); setupMobileKeyboardHandling(); setupContentEditableScrolling(); // Initialize desktop UI setupDesktopTabs(); // Initialize collapse toggle setupCollapseToggle(); // Initialize dice roller setupDiceRoller(); // Initialize settings popup setupSettingsPopup(); // Initialize tracker editor initTrackerEditor(); // Initialize prompts editor initPromptsEditor(); // Initialize plot buttons setupPlotButtons(sendPlotProgression, openEncounterModal); // Initialize classic stats buttons setupClassicStatsButtons(); // Initialize inventory event listeners initInventoryEventListeners(); initEquipmentEventListeners(); // Initialize chapter checkpoint UI initChapterCheckpointUI(); injectCheckpointButton(); // Expose weather effect functions globally for cross-module access if (!window.RPGCompanion) { window.RPGCompanion = {}; } window.RPGCompanion.updateWeatherEffect = updateWeatherEffect; } // Main initialization jQuery(async () => { try { console.log('[RPG Companion] Starting initialization...'); // Load settings with validation try { loadSettings(); } catch (error) { console.error('[RPG Companion] Settings load failed, continuing with defaults:', error); } // Check if migration to v3 JSON format is needed try { if (extensionSettings.settingsVersion < 3) { await migrateToV3JSON(); updateExtensionSettings({ settingsVersion: 3 }); await saveSettings(); } } catch (error) { console.error('[RPG Companion] Migration to v3 failed:', error); } // Initialize i18n early for the settings panel await i18n.init(); // Set up a central listener for language changes to update dynamic UI parts i18n.addEventListener('languageChanged', updateDynamicLabels); // Add extension settings to Extensions tab try { await addExtensionSettings($, renderExtensionTemplateAsync, clearExtensionPrompts, updateChatThoughts, cleanupCheckpointUI, clearThoughtBasedExpressionsCache, toggleDynamicWeather, initUI, loadChatData, scheduleChatStateRehydration, initThoughtBasedExpressions, injectCheckpointButton, updateAllCheckpointIndicators, removeAlternatePresentCharactersPanel); } catch (error) { console.error('[RPG Companion] Failed to add extension settings tab:', error); } // Initialize UI try { await initUI(); } catch (error) { console.error('[RPG Companion] UI initialization failed:', error); throw error; } // Load chat-specific data for current chat try { loadChatData(); scheduleChatStateRehydration(); initThoughtBasedExpressions(); updateFabWidgets(); updateStripWidgets(); } catch (error) { console.error('[RPG Companion] Chat data load failed, using defaults:', error); } // Import cleaning regexes try { await ensureHtmlCleaningRegex(st_extension_settings, saveSettingsDebounced); } catch (error) { console.error('[RPG Companion] HTML regex import failed:', error); } try { await ensureTrackerCleaningRegex(st_extension_settings, saveSettingsDebounced); } catch (error) { console.error('[RPG Companion] Tracker cleaning regex import failed:', error); } try { await ensureJsonCleaningRegex(st_extension_settings, saveSettingsDebounced); } catch (error) { console.error('[RPG Companion] JSON cleaning regex setup failed:', error); } // Detect conflicting regex scripts try { detectConflictingRegexScripts(st_extension_settings); } catch (error) { console.error('[RPG Companion] Conflict detection failed:', error); } // Initialize history injection event listeners try { initHistoryInjection(); } catch (error) { console.error('[RPG Companion] History injection init failed:', error); } // Register all event listeners try { registerAllEvents({ [event_types.MESSAGE_SENT]: onMessageSent, [event_types.GENERATION_STARTED]: onGenerationStarted, [event_types.MESSAGE_RECEIVED]: onMessageReceived, [event_types.GENERATION_STOPPED]: onGenerationEnded, [event_types.GENERATION_ENDED]: onGenerationEnded, [event_types.CHAT_CHANGED]: [onCharacterChanged, updatePersonaAvatar, restoreCheckpointOnLoad, clearSessionAvatarPrompts, clearDebugLogs], [event_types.CHAT_LOADED]: onChatLoaded, [event_types.MESSAGE_DELETED]: onMessageDeleted, [event_types.MESSAGE_SWIPE_DELETED]: onMessageDeleted, [event_types.MESSAGE_SWIPED]: onMessageSwiped, [event_types.USER_MESSAGE_RENDERED]: updatePersonaAvatar, [event_types.SETTINGS_UPDATED]: updatePersonaAvatar }); // Use tracked event registration (onEvent) instead of direct eventSource.on() // This ensures all handlers are tracked for cleanup by unregisterAllEvents() onEvent(event_types.CHARACTER_MESSAGE_RENDERED, (messageId) => { if (!extensionSettings.enabled) return; const renderedMessage = chat[messageId]; if (renderedMessage && !renderedMessage.is_user && !renderedMessage.is_system) { queueThoughtBasedExpressionsUpdate(); } }); onEvent(event_types.MESSAGE_UPDATED, (messageId) => { if (!extensionSettings.enabled) return; const updatedMessage = chat[messageId]; if (updatedMessage && !updatedMessage.is_user && !updatedMessage.is_system) { queueThoughtBasedExpressionsUpdate(); } }); onEvent(event_types.MESSAGE_SWIPED, (messageIndex) => { if (!extensionSettings.enabled) return; const swipedMessage = chat[messageIndex]; if (swipedMessage && !swipedMessage.is_user && !swipedMessage.is_system) { queueThoughtBasedExpressionsUpdate({ immediate: true }); } }); onEvent(event_types.CHAT_CHANGED, () => { clearThoughtBasedExpressionsCache(); setTimeout(() => onThoughtBasedExpressionsChatChanged(), 0); }); onEvent(event_types.MESSAGE_DELETED, () => { if (!extensionSettings.enabled) return; clearThoughtBasedExpressionsCache(); setTimeout(() => onThoughtBasedExpressionsChatChanged(), 0); }); onEvent(event_types.MESSAGE_SWIPE_DELETED, () => { if (!extensionSettings.enabled) return; clearThoughtBasedExpressionsCache(); setTimeout(() => onThoughtBasedExpressionsChatChanged(), 0); }); } catch (error) { console.error('[RPG Companion] Event registration failed:', error); throw error; } // Restore checkpoint state if one exists await restoreCheckpointOnLoad(); // Initialize snowflakes effect if enabled try { initSnowflakes(); } catch (error) { console.error('[RPG Companion] Snowflakes initialization failed:', error); } // Show startup modals try { const deprecationModalShown = showDeprecationModalIfNeeded(); if (!deprecationModalShown) { showWelcomeModalIfNeeded(); } } catch (error) { console.error('[RPG Companion] Startup modal failed:', error); } console.log('[RPG Companion] ✅ Extension loaded successfully.'); } catch (error) { console.error('[RPG Companion] ❌ Critical initialization failure:', error); console.error('[RPG Companion] Error details:', error.message, error.stack); toastr.error( 'RPG Companion failed to initialize. Check console for details. Please try refreshing the page or resetting extension settings.', 'RPG Companion Error', { timeOut: 10000 } ); } });