// Copyright (c) Pixel Crushers. All rights reserved.
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;
using UnityEngine.SceneManagement;
using System.Linq;
#if USE_ADDRESSABLES
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
#endif
namespace PixelCrushers.DialogueSystem
{
public delegate bool GetInputButtonDownDelegate(string buttonName);
public delegate void TransformDelegate(Transform t);
public delegate void AssetLoadedDelegate(UnityEngine.Object asset);
public delegate string GetLocalizedTextDelegate(string s);
///
/// This component ties together the elements of the Dialogue System: dialogue database,
/// dialogue UI, sequencer, and conversation controller. You will typically add this to a
/// GameObject named DialogueManager in your scene. For simplified script access, you can
/// use the DialogueManager static class.
///
[AddComponentMenu("")] // Use wrapper.
public class DialogueSystemController : MonoBehaviour
{
#region Public Fields & Properties
///
/// The initial dialogue database.
///
[Tooltip("This dialogue database is loaded automatically. Use an Extra Databases component to load additional databases.")]
public DialogueDatabase initialDatabase = null;
///
/// The display settings to use for the dialogue UI and sequencer.
///
public DisplaySettings displaySettings = new DisplaySettings();
///
/// Settings to apply to the PersistentDataManager.
///
public PersistentDataSettings persistentDataSettings = new PersistentDataSettings();
///
/// Set true to allow more than one conversation to play simultaneously.
///
[Tooltip("Allow more than one conversation to play simultaneously.")]
public bool allowSimultaneousConversations = false;
///
/// If not allowing simultaneous conversations and a conversation is active, stop it if another conversation wants to start.
///
[Tooltip("If not allowing simultaneous conversations and a conversation is active, stop it if another conversation wants to start.")]
public bool interruptActiveConversations = false;
///
/// Set true to include sim status for each dialogue entry.
///
[Tooltip("Tick if your conversations reference Dialog[x].SimStatus.")]
public bool includeSimStatus = false;
[Tooltip("Use a copy of the dialogue database at runtime instead of the asset file directly. This allows you to change the database without affecting the asset.")]
public bool instantiateDatabase = true;
///
/// If true, preloads the master database and dialogue UI. Otherwise they're lazy-
/// loaded only before the first time they're needed.
///
[Tooltip("Preload the dialogue database and dialogue UI at Start. Otherwise they're loaded at first use.")]
public bool preloadResources = true;
public enum WarmUpMode { On, Extra, Off }
[Tooltip("Warm up conversation engine and dialogue UI at Start to avoid a small amount of overhead on first use. 'Extra' performs deeper warmup that takes 1.25s at startup.")]
public WarmUpMode warmUpConversationController = WarmUpMode.On;
[Tooltip("Don't run HideImmediate on dialogue UI when warming it up on start.")]
public bool dontHideImmediateDuringWarmup = false;
///
/// If true, Unity will not destroy the game object when loading a new level.
///
[Tooltip("Retain this GameObject when changing levels. Note: If InputDeviceManager's Singleton checkbox is ticked or GameObject has SaveSystem, GameObject will still be marked Don't Destroy On Load.")]
public bool dontDestroyOnLoad = true;
///
/// If true, new DialogueSystemController objects will destroy themselves if one
/// already exists in the scene. Otherwise, if you reload a level and dontDestroyOnLoad
/// is true, you'll end up with a second object.
///
[Tooltip("Ensure only one Dialogue Manager in the scene.")]
public bool allowOnlyOneInstance = true;
///
/// If true, Dialogue System Triggers set to OnStart should wait until save data has been applied or variables initialized.
///
[Tooltip("Dialogue System Triggers set to OnStart should wait until save data has been applied or variables initialized.")]
public bool onStartTriggerWaitForSaveDataApplied = false;
///
/// Time mode to use for conversations.
/// - Realtime: Independent of Time.timeScale.
/// - Gameplay: Observe Time.timeScale.
/// - Custom: You must manually set DialogueTime.time.
///
[Tooltip("Time mode to use for conversations.\nRealtime: Independent of Time.timeScale.\nGameplay: Observe Time.timeScale.\nCustom: You must manually set DialogueTime.time.")]
public DialogueTime.TimeMode dialogueTimeMode = DialogueTime.TimeMode.Realtime;
///
/// The debug level. Information at this level or higher is logged to the console. This can
/// be helpful when tracing through conversations.
///
[Tooltip("Set to higher levels for troubleshooting.")]
public DialogueDebug.DebugLevel debugLevel = DialogueDebug.DebugLevel.Warning;
///
/// Raised when the Dialogue System receives an UpdateTracker message
/// to update the quest tracker HUD and quest log window.
///
public event System.Action receivedUpdateTracker = delegate { };
///
/// Raised when a conversation starts. Parameter is primary actor.
///
public event TransformDelegate conversationStarted = delegate { };
///
/// Raised when a conversation ends. Parameter is primary actor.
///
public event TransformDelegate conversationEnded = delegate { };
///
/// Raised when StopAllConversations() is called.
///
public event System.Action stoppingAllConversations = delegate { };
///
/// Assign to replace the Dialogue System's built-in GetLocalizedText().
///
public GetLocalizedTextDelegate overrideGetLocalizedText = null;
///
/// Raised when the Dialogue System has completely initialized, including
/// loading the initial dialogue database and registering Lua functions.
///
public event System.Action initializationComplete = delegate { };
///
/// True when this Dialogue System Controller is fully initialized.
///
public bool isInitialized { get { return m_isInitialized; } }
private bool m_isInitialized = false;
private const string DefaultDialogueUIResourceName = "Default Dialogue UI";
private DatabaseManager m_databaseManager = null;
private IDialogueUI m_currentDialogueUI = null;
[HideInInspector] // Prevents accidental serialization if inspector is in Debug mode.
private IDialogueUI m_originalDialogueUI = null;
[HideInInspector] // Prevents accidental serialization if inspector is in Debug mode.
private DisplaySettings m_originalDisplaySettings = null;
private bool m_overrodeDisplaySettings = false; // Inspector Debug mode will deserialize default into m_originalDisplaySettings, so use this bool instead of counting on m_originalDisplaySettings being null.
private bool m_isOverrideUIPrefab = false;
private bool m_dontDestroyOverrideUI = false;
private OverrideDialogueUI m_overrideDialogueUI = null; // Used temporarily to set its ui property to instance.
private ConversationController m_conversationController = null;
private IsDialogueEntryValidDelegate m_isDialogueEntryValid = null;
private System.Action m_customResponseTimeoutHandler = null;
private GetInputButtonDownDelegate m_savedGetInputButtonDownDelegate = null;
private LuaWatchers m_luaWatchers = new LuaWatchers();
private PixelCrushers.DialogueSystem.AssetBundleManager m_assetBundleManager = new PixelCrushers.DialogueSystem.AssetBundleManager();
private bool m_started = false;
private DialogueDebug.DebugLevel m_lastDebugLevelSet = DialogueDebug.DebugLevel.None;
private List m_activeConversations = new List();
private Queue alertsQueuedForConversationEnd = new Queue();
private UILocalizationManager m_uiLocalizationManager = null;
private bool m_calledRandomizeNextEntry = false;
private bool m_isDuplicateBeingDestroyed = false;
private Coroutine warmupCoroutine = null;
public static bool isWarmingUp = false;
public static bool applicationIsQuitting = false;
public static string lastInitialDatabaseName = null;
///
/// Gets the dialogue database manager.
///
///
/// The database manager.
///
public DatabaseManager databaseManager { get { return m_databaseManager; } }
///
/// Gets the master dialogue database, which contains the initial database and any
/// additional databases that you have added.
///
///
/// The master database.
///
public DialogueDatabase masterDatabase { get { return m_databaseManager.masterDatabase; } }
///
/// Gets or sets the dialogue UI, which is an implementation of IDialogueUI.
///
///
/// The dialogue UI.
///
public IDialogueUI dialogueUI
{
get { return GetDialogueUI(); }
set { SetDialogueUI(value); }
}
///
/// Convenience property that casts the dialogueUI property as a StandardDialogueUI.
/// If the dialogueUI is not a StandardDialogueUI, returns null.
///
public StandardDialogueUI standardDialogueUI
{
get { return dialogueUI as StandardDialogueUI; }
set { SetDialogueUI(value); }
}
///
/// The IsDialogueEntryValid delegate (if one is assigned). This is an optional delegate that you
/// can add to check if a dialogue entry is valid before allowing a conversation to use it.
///
public IsDialogueEntryValidDelegate isDialogueEntryValid
{
get
{
return m_isDialogueEntryValid;
}
set
{
m_isDialogueEntryValid = value;
if (m_conversationController != null) m_conversationController.isDialogueEntryValid = value;
}
}
///
/// If response timeout action is set to Custom and menu times out, call this method.
///
public System.Action customResponseTimeoutHandler
{
get { return m_customResponseTimeoutHandler; }
set { m_customResponseTimeoutHandler = value; }
}
///
/// The GetInputButtonDown delegate. Overrides calls to the standard Unity
/// Input.GetButtonDown function.
///
public GetInputButtonDownDelegate getInputButtonDown { get; set; }
///
/// Indicates whether a conversation is currently active.
///
///
/// true if the conversation is active; otherwise, false.
///
public bool isConversationActive { get { return isAlternateConversationActive || ((m_conversationController != null) && m_conversationController.isActive); } }
///
/// Set true to make isConversationActive report true even if a regular conversation isn't currently active.
///
public bool isAlternateConversationActive { get; set; } = false;
///
/// Gets the current actor of the last conversation started if a conversation is active.
///
/// The current actor.
public Transform currentActor { get; private set; }
///
/// Gets the current conversant of the last conversation started if a conversation is active.
///
/// The current conversant.
public Transform currentConversant { get; private set; }
///
/// Gets or sets the current conversation state of the last conversation that had a line.
/// This is set by ConversationController as the conversation moves from state to state.
///
/// The current conversation state.
public ConversationState currentConversationState { get; set; }
///
/// Gets the title of the last conversation started. If a conversation is active,
/// this is the title of the active conversation.
///
/// The title of the last conversation started.
public string lastConversationStarted { get; private set; }
public string lastConversationEnded { get; set; } // Set by ConversationController.
///
/// Gets the ID of the last conversation started.
///
public int lastConversationID { get; private set; }
public ConversationController conversationController { get { return m_conversationController; } }
public ConversationModel conversationModel { get { return (m_conversationController != null) ? m_conversationController.conversationModel : null; } }
public ConversationView conversationView { get { return (m_conversationController != null) ? m_conversationController.conversationView : null; } }
///
/// List of conversations that are currently active.
///
public List activeConversations { get { return m_activeConversations; } }
///
/// Conversation that is currently being examined by Conditions, Scripts, OnConversationLine, etc.
///
public ActiveConversationRecord activeConversation { get; set; }
/// @cond FOR_V1_COMPATIBILITY
public DatabaseManager DatabaseManager { get { return databaseManager; } }
public DialogueDatabase MasterDatabase { get { return masterDatabase; } }
public IDialogueUI DialogueUI { get { return dialogueUI; } set { dialogueUI = value; } }
public IsDialogueEntryValidDelegate IsDialogueEntryValid { get { return isDialogueEntryValid; } set { isDialogueEntryValid = value; } }
public GetInputButtonDownDelegate GetInputButtonDown { get { return getInputButtonDown; } set { getInputButtonDown = value; } }
public bool IsConversationActive { get { return isConversationActive; } }
public Transform CurrentActor { get { return currentActor; } private set { currentActor = value; } }
public Transform CurrentConversant { get { return currentConversant; } set { currentConversant = value; } }
public ConversationState CurrentConversationState { get { return currentConversationState; } set { currentConversationState = value; } }
public string LastConversationStarted { get { return lastConversationStarted; } set { lastConversationStarted = value; } }
public int LastConversationID { get { return lastConversationID; } set { lastConversationID = value; } }
public ConversationController ConversationController { get { return conversationController; } }
public ConversationModel ConversationModel { get { return conversationModel; } }
public ConversationView ConversationView { get { return conversationView; } }
public List ActiveConversations { get { return activeConversations; } }
/// @endcond
///
/// Indicates whether to allow the Lua environment to pass exceptions up to the
/// caller. The default is false, which allows Lua to catch exceptions
/// and just log an error to the console.
///
/// true to allow Lua exceptions; otherwise, false.
public bool allowLuaExceptions { get; set; }
///
/// If `true`, warns if a conversation starts with the actor and conversant
/// pointing to the same transform. Default is `false`.
///
/// true to warn when actor and conversant are the same; otherwise, false.
public bool warnIfActorAndConversantSame { get; set; }
///
/// Unload addressables when changing scenes. Some sequencer commands such as Audio() do not unload their addressables, so this cleans them up.
///
public bool unloadAddressablesOnSceneChange { get { return m_unloadAddressablesOnSceneChange; } set { m_unloadAddressablesOnSceneChange = value; } }
private bool m_unloadAddressablesOnSceneChange = true;
#endregion
#region Initialization
#if UNITY_2019_3_OR_NEWER && UNITY_EDITOR
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
static void InitStaticVariables()
{
applicationIsQuitting = false;
lastInitialDatabaseName = null;
}
#endif
public void OnDestroy()
{
if (dontDestroyOnLoad && allowOnlyOneInstance) applicationIsQuitting = true;
if (!applicationIsQuitting && !m_isDuplicateBeingDestroyed && DialogueTime.isPaused) Unpause();
UnityEngine.SceneManagement.SceneManager.sceneLoaded -= OnSceneLoaded;
UILocalizationManager.languageChanged -= OnLanguageChanged;
//--- No need to unregister static Lua functions: UnregisterLuaFunctions();
}
///
/// Initializes the component by setting up the dialogue database and preparing the
/// dialogue UI. If the assigned UI is a prefab, an instance is added. If no UI is
/// assigned, the default UI is loaded.
///
public void Awake()
{
if (allowOnlyOneInstance && (GameObjectUtility.FindObjectsByType().Length > 1))
{
// Scene already has an instance, so destroy this one:
m_isDuplicateBeingDestroyed = true;
Destroy(gameObject);
}
else
{
// Otherwise initialize this one:
getInputButtonDown = StandardGetInputButtonDown;
if ((instantiateDatabase || (warmUpConversationController != WarmUpMode.Off)) && initialDatabase != null)
{
var databaseInstance = Instantiate(initialDatabase) as DialogueDatabase;
databaseInstance.name = initialDatabase.name;
initialDatabase = databaseInstance;
}
bool visuallyMarkOldResponses = ((displaySettings != null) && (displaySettings.inputSettings != null) && (displaySettings.inputSettings.emTagForOldResponses != EmTag.None));
DialogueLua.includeSimStatus = includeSimStatus || visuallyMarkOldResponses;
Sequencer.reportMissingAudioFiles = displaySettings.cameraSettings.reportMissingAudioFiles;
PersistentDataManager.includeSimStatus = DialogueLua.includeSimStatus;
PersistentDataManager.includeActorData = persistentDataSettings.includeActorData;
PersistentDataManager.includeAllItemData = persistentDataSettings.includeAllItemData;
PersistentDataManager.includeLocationData = persistentDataSettings.includeLocationData;
PersistentDataManager.includeRelationshipAndStatusData = persistentDataSettings.includeStatusAndRelationshipData;
PersistentDataManager.includeAllConversationFields = persistentDataSettings.includeAllConversationFields;
PersistentDataManager.initializeNewVariables = persistentDataSettings.initializeNewVariables;
PersistentDataManager.saveConversationSimStatusWithField = persistentDataSettings.saveConversationSimStatusWithField;
PersistentDataManager.saveDialogueEntrySimStatusWithField = persistentDataSettings.saveDialogueEntrySimStatusWithField;
PersistentDataManager.recordPersistentDataOn = persistentDataSettings.recordPersistentDataOn;
PersistentDataManager.asyncGameObjectBatchSize = persistentDataSettings.asyncGameObjectBatchSize;
PersistentDataManager.asyncDialogueEntryBatchSize = persistentDataSettings.asyncDialogueEntryBatchSize;
if (dontDestroyOnLoad)
{
#if UNITY_EDITOR
if (Application.isPlaying)
{ // If GameObject is hidden in Scene view, DontDestroyOnLoad will report (harmless) error.
UnityEditor.SceneVisibilityManager.instance.Show(gameObject, true);
}
#endif
if (this.transform.parent != null) this.transform.SetParent(null, false);
DontDestroyOnLoad(this.gameObject);
}
else
{
var saveSystem = GetComponent();
var inputDeviceManager = GetComponent();
if (saveSystem != null)
{
if (DialogueDebug.logWarnings) Debug.LogWarning("Dialogue System: The Dialogue Manager's Don't Destroy On Load checkbox is UNticked, but the GameObject has a Save System which will mark it Don't Destroy On Load anyway. You may want to tick Don't Destroy On Load or move the Save System to another GameObject.", this);
}
else if (inputDeviceManager != null && inputDeviceManager.singleton)
{
if (DialogueDebug.logWarnings) Debug.LogWarning("Dialogue System: The Dialogue Manager's Don't Destroy On Load checkbox is UNticked, but the GameObject has an Input Device Manager whose Singleton checkbox is ticked, which will mark it Don't Destroy On Load anyway. You may want to tick Don't Destroy On Load or untick the Input Device Manager's Singleton checkbox.", this);
}
}
allowLuaExceptions = false;
warnIfActorAndConversantSame = false;
DialogueTime.mode = dialogueTimeMode;
DialogueDebug.level = debugLevel;
m_lastDebugLevelSet = debugLevel;
lastConversationStarted = string.Empty;
lastConversationEnded = string.Empty;
lastConversationID = -1;
currentActor = null;
currentConversant = null;
currentConversationState = null;
InitializeDatabase();
InitializeDisplaySettings();
InitializeLocalization();
QuestLog.RegisterQuestLogFunctions();
RegisterLuaFunctions();
UnityEngine.SceneManagement.SceneManager.sceneLoaded += OnSceneLoaded;
}
}
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
if (mode == LoadSceneMode.Single)
{
if (unloadAddressablesOnSceneChange)
{
UnloadAssets();
}
else
{
ClearLoadedAssetHashes();
}
}
}
///
/// Start by enforcing only one instance is specified. Then start monitoring alerts.
///
public void Start()
{
StartCoroutine(MonitorAlerts());
m_started = true;
if (preloadResources) PreloadResources();
if (warmUpConversationController != WarmUpMode.Off) WarmUpConversationController();
//QuestLog.RegisterQuestLogFunctions(); // Moved to Awake.
//RegisterLuaFunctions();
m_isInitialized = true;
initializationComplete();
}
private void InitializeDisplaySettings()
{
if (displaySettings == null)
{
displaySettings = new DisplaySettings();
displaySettings.cameraSettings = new DisplaySettings.CameraSettings();
displaySettings.inputSettings = new DisplaySettings.InputSettings();
displaySettings.inputSettings.cancel = new InputTrigger(KeyCode.Escape);
displaySettings.inputSettings.qteButtons = new string[2] { "Fire1", "Fire2" };
displaySettings.subtitleSettings = new DisplaySettings.SubtitleSettings();
displaySettings.localizationSettings = new DisplaySettings.LocalizationSettings();
}
}
private void InitializeLocalization()
{
if (displaySettings.localizationSettings.useSystemLanguage)
{
displaySettings.localizationSettings.language = Localization.GetLanguage(Application.systemLanguage);
}
var m_uiLocalizationManager = GetComponent() ?? GameObjectUtility.FindFirstObjectByType();
var needsLocalizationManager = !string.IsNullOrEmpty(displaySettings.localizationSettings.language) || displaySettings.localizationSettings.textTable != null;
if (needsLocalizationManager && m_uiLocalizationManager == null)
{
m_uiLocalizationManager = gameObject.AddComponent();
}
if (m_uiLocalizationManager != null)
{
m_uiLocalizationManager.Initialize();
if (m_uiLocalizationManager.textTable == null)
{
m_uiLocalizationManager.textTable = displaySettings.localizationSettings.textTable;
}
}
if (m_uiLocalizationManager != null && !string.IsNullOrEmpty(m_uiLocalizationManager.currentLanguage))
{
SetLanguage(m_uiLocalizationManager.currentLanguage);
}
else
{
SetLanguage(displaySettings.localizationSettings.language);
}
UILocalizationManager.languageChanged += OnLanguageChanged;
}
///
/// Sets the language to use for localized text.
///
///
/// Language to use. Specify null or an emtpy string to use the default language.
///
public void SetLanguage(string language)
{
if (m_uiLocalizationManager == null)
{
m_uiLocalizationManager = GetComponent() ?? GameObjectUtility.FindFirstObjectByType();
if (m_uiLocalizationManager == null)
{
m_uiLocalizationManager = gameObject.AddComponent();
}
if (m_uiLocalizationManager.textTable == null)
{
m_uiLocalizationManager.textTable = displaySettings.localizationSettings.textTable;
}
}
m_uiLocalizationManager.currentLanguage = language;
displaySettings.localizationSettings.language = language;
Localization.language = language;
}
private void OnLanguageChanged(string newLanguage)
{
displaySettings.localizationSettings.language = newLanguage;
UpdateLocalizationOnActiveConversations();
}
///
/// Preloads the master database. The Dialogue System delays loading of the dialogue database
/// until the data is needed. This avoids potentially long delays during Start(). If you want
/// to load the database manually (for example to run Lua commands on its contents), call
/// this method.
///
public void PreloadMasterDatabase()
{
DialogueDatabase db = masterDatabase;
if (DialogueDebug.logInfo) Debug.Log(string.Format("{0}: Loaded master database '{1}'", new System.Object[] { DialogueDebug.Prefix, db.name }));
}
///
/// Preloads the dialogue UI. The Dialogue System delays loading of the dialogue UI until the
/// first time it's needed for a conversation or alert. Since dialogue UIs often contain
/// textures and other art assets, loading can cause a slight pause. You may want to preload
/// the UI at a time of your design by using this method.
///
public void PreloadDialogueUI()
{
IDialogueUI ui = dialogueUI;
if ((ui == null) && DialogueDebug.logWarnings) Debug.LogWarning(string.Format("{0}: Unable to load the dialogue UI.", DialogueDebug.Prefix));
}
///
/// Preloads the resources used by the Dialogue System to avoid delays caused by lazy loading.
///
public void PreloadResources()
{
PreloadMasterDatabase();
PreloadDialogueUI();
Sequencer.Preload();
}
// These variables are used for extra warmup:
private Conversation fakeConversation;
private StandardDialogueUI warmupStandardDialogueUI;
private ConversationController warmupController;
private bool addTempCanvasGroup;
private CanvasGroup warmupCanvasGroup;
private DialogueDebug.DebugLevel warmupPreviousLogLevel;
private float warmupPreviousAlpha;
private const int FakeConversationID = -1;
private const string FakeConversationTitle = "__Internal_Warmup__";
///
/// Stop and start a fake conversation to initialize things to avoid a small delay the first time a conversation starts.
///
public void WarmUpConversationController()
{
if (isConversationActive) return;
// Get the dialogue UI canvas:
var abstractDialogueUI = dialogueUI as AbstractDialogueUI;
if (abstractDialogueUI == null) return;
var canvas = abstractDialogueUI.GetComponentInParent