// Copyright (c) Pixel Crushers. All rights reserved. using System.Collections; using System.Collections.Generic; using System.IO; using System.Text; using System.Text.RegularExpressions; using UnityEngine; namespace PixelCrushers.DialogueSystem { public delegate string GetCustomSaveDataDelegate(); /// /// A static class for saving and loading game data using the Dialogue System's /// Lua environment. It allows you to save or load a game with a single line of code. /// /// For more information, see @ref saveLoadSystem /// public static class PersistentDataManager { #region Variables /// /// Set true to include actor data in save data, false to exclude. /// public static bool includeActorData = true; /// /// Set this true to include all item fields in saved-game data. This is /// false by default to minimize the size of the saved-game data by only /// recording State and Track (for quests). /// public static bool includeAllItemData = false; /// /// Set true to include location data in save data, false to exclude. /// public static bool includeLocationData = false; /// /// Set true to include all conversation fields, false to exclude. /// public static bool includeAllConversationFields = false; /// /// Set this true to exclude Conversation[#].Dialog[#].SimStatus values from /// saved-game data. If you don't use SimStatus in your Lua conditions, there's no /// need to save it. /// public static bool includeSimStatus = false; /// /// Optional field to use when saving a conversation's SimStatus info (e.g., Title). /// This feature is handy if you can't guarantee that conversation IDs will be the /// same across saved games. If set, saves the conversation's SimStatus info into /// a field. If blank, uses conversation ID. /// public static string saveConversationSimStatusWithField = string.Empty; /// /// Optional field to use when saving a dialogue entry's SimStatus info (e.g,. Title). /// This feature is handy if you can't guarantee that dialogue entry IDs will be the /// same across saved games. If set, saves the entry's SimStatus value into a field. /// If blank, uses entry's ID. /// public static string saveDialogueEntrySimStatusWithField = string.Empty; /// /// Set true to include the status & relationship tables in save data, /// false to exclude. /// public static bool includeRelationshipAndStatusData = true; /// /// Initialize variables and quests that were added to database after saved game.")] /// public static bool initializeNewVariables = true; /// /// Initialize new SimStatus values for entries that were added to database after saved game. /// public static bool initializeNewSimStatus = true; /// /// PersistentDataManager will call this delegate (if set) to add custom data /// to the saved-game data string. The custom data should be valid Lua code. /// public static GetCustomSaveDataDelegate GetCustomSaveData = null; public enum RecordPersistentDataOn { /// /// Inform all components on all GameObjects in the scene to record their persistent data /// if supported. /// AllGameObjects, /// /// Inform only components that have registered to receive notifications to record their /// persistent data. /// OnlyRegisteredGameObjects, /// /// Entirely skip informing any GameObjects to record their persistent data. /// NoGameObjects } public static RecordPersistentDataOn recordPersistentDataOn = RecordPersistentDataOn.AllGameObjects; private static HashSet listeners = new HashSet(); #if UNITY_2019_3_OR_NEWER && UNITY_EDITOR [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] static void InitStaticVariables() { GetCustomSaveData = null; listeners = new HashSet(); } #endif #endregion #region Register Persistent Data Components /// /// /// /// GameObject that should receive notifications. public static void RegisterPersistentData(GameObject go) { if (go == null || !Application.isPlaying) return; listeners.Add(go); } /// /// /// /// GameObject that should no longer receive notifications. public static void UnregisterPersistentData(GameObject go) { if (!Application.isPlaying) return; listeners.Remove(go); } #endregion #region Main Data Management Methods /// /// Resets the Lua environment -- for example, when starting a new game. /// /// /// The database reset options can be: /// /// - RevertToDefault: Removes all but the default database, then resets it. /// - KeepAllLoaded: Keeps all loaded databases in memory and just resets them. /// public static void Reset(DatabaseResetOptions databaseResetOptions) { DialogueManager.ResetDatabase(databaseResetOptions); } /// /// Resets the Lua environment -- for example, when starting a new game -- keeping all loaded database /// in memory and just resetting them. /// public static void Reset() { Reset(DatabaseResetOptions.KeepAllLoaded); } /// /// Sends the OnRecordPersistentData message to all GameObjects in the scene to give them /// an opportunity to record their state in the Lua environment. You can limit which GameObjects /// receive messages by changing recordPersistentDataOn. /// public static void Record() { if (DialogueDebug.LogInfo) Debug.Log(string.Format("{0}: Recording persistent data to Lua environment.", new System.Object[] { DialogueDebug.Prefix })); SendPersistentDataMessage("OnRecordPersistentData"); } /// /// Sends the OnApplyPersistentData message to all game objects in the scene to give them an /// opportunity to retrieve their state from the Lua environment. If calling this after loading /// a new scene, you may want to wait one frame to allow other GameObject's Start methods /// to complete first. You can limit which GameObjects receive messages by changing /// recordPersistentDataOn. /// public static void Apply() { if (DialogueDebug.LogInfo) Debug.Log(string.Format("{0}: Applying persistent data from Lua environment.", new System.Object[] { DialogueDebug.Prefix })); SendPersistentDataMessage("OnApplyPersistentData"); DialogueManager.SendUpdateTracker(); // Update quest tracker HUD. } private static void SendPersistentDataMessage(string message) { switch (recordPersistentDataOn) { case RecordPersistentDataOn.AllGameObjects: Tools.SendMessageToEveryone(message); break; case RecordPersistentDataOn.OnlyRegisteredGameObjects: var gos = new List(listeners); // listeners may change during loop. for (int i = gos.Count - 1; i >= 0; i--) { var go = gos[i]; if (go != null) go.SendMessage(message, SendMessageOptions.DontRequireReceiver); } break; default: break; } } /// /// Sends the OnLevelWillBeUnloaded message to all game objects in the scene in case they /// need to change their behavior. For example, scripts that do something special when /// destroyed during play may not want to do the same thing when being destroyed by a /// level unload. /// public static void LevelWillBeUnloaded() { if (DialogueDebug.LogInfo) Debug.Log(string.Format("{0}: Broadcasting that level will be unloaded.", new System.Object[] { DialogueDebug.Prefix })); SendPersistentDataMessage("OnLevelWillBeUnloaded"); } /// /// Loads a saved game by applying a saved-game string. /// /// /// A saved-game string previously returned by GetSaveData(). /// /// /// Database reset options. /// public static void ApplySaveData(string saveData, DatabaseResetOptions databaseResetOptions = DatabaseResetOptions.KeepAllLoaded) { if (DialogueDebug.LogInfo) Debug.Log(string.Format("{0}: Resetting Lua environment.", new System.Object[] { DialogueDebug.Prefix })); DialogueManager.ResetDatabase(databaseResetOptions); if (DialogueDebug.LogInfo) Debug.Log(string.Format("{0}: Updating Lua environment with saved data.", new System.Object[] { DialogueDebug.Prefix })); ApplyLuaInternal(saveData); Apply(); } /// /// Loads data into the Lua environment. /// /// /// public static void ApplyLuaInternal(string saveData, bool allowExceptions = false) { if (!string.IsNullOrEmpty(saveData)) { EnsureConversationTablesExistForAllSimX(saveData); EnsureQuestsExist(saveData); Lua.Run(saveData, DialogueDebug.LogInfo); ExpandCompressedSimStatusData(); RefreshRelationshipAndStatusTablesFromLua(); if (initializeNewVariables) { InitializeNewVariablesFromDatabase(); InitializeNewActorFieldsFromDatabase(); InitializeNewQuestEntriesFromDatabase(); InitializeNewSimStatusFromDatabase(); } } } #if USE_NLUA /// /// If using SimStatus, make sure Conversation[#] elements exist for all onversations in /// the saved game data. /// /// private static void EnsureConversationTablesExistForAllSimX(string saveData) { if (!(includeSimStatus && DialogueManager.Instance.includeSimStatus)) return; var conversationTable = Lua.Run("return Conversation").asTable; if (conversationTable == null) return; var keysHashSet = new HashSet(conversationTable.keys); var preLength = "Conversation[".Length; foreach (Match match in Regex.Matches(saveData, @"Conversation\[\d+\]")) { var idString = match.Value.Substring(preLength, match.Value.Length - (preLength + 1)); if (!keysHashSet.Contains(idString)) Lua.Run("Conversation[" + idString + "] = {}"); } } /// /// If only saving quest states (and not all item/quest data), make sure Item["x"] elements /// exist for all quests in the saved game data. /// /// private static void EnsureQuestsExist(string saveData) { if (includeAllItemData || DialogueManager.Instance.persistentDataSettings.includeAllItemData) return; var itemTable = Lua.Run("return Item").asTable; if (itemTable == null) return; var keysHashSet = new HashSet(itemTable.keys); var preLength = "Item[".Length; var postLength = "].State".Length; foreach (Match match in Regex.Matches(saveData, @"Item\[[^\]]+\].State")) { var s = match.Value.Substring(preLength + 1, match.Value.Length - (preLength + postLength + 2)); if (!keysHashSet.Contains(s)) Lua.Run("Item[" + s + "] = { Name='" + s.Replace("\"", "\\\"") + "', State='unassigned' }"); } } #else /// /// If using SimStatus, make sure Conversation[#] elements exist for all onversations in /// the saved game data. /// /// private static void EnsureConversationTablesExistForAllSimX(string saveData) { if (!(includeSimStatus && DialogueManager.Instance.includeSimStatus)) return; var conversationTable = Lua.Environment.GetValue("Conversation") as Language.Lua.LuaTable; if (conversationTable == null) return; var preLength = "Conversation[".Length; foreach (Match match in Regex.Matches(saveData, @"Conversation\[\d+\]")) { var idString = match.Value.Substring(preLength, match.Value.Length - (preLength + 1)); var key = new Language.Lua.LuaNumber(SafeConvert.ToInt(idString)); if (!conversationTable.ContainsKey(key)) { conversationTable.SetKeyValue(key, new Language.Lua.LuaTable()); } } } /// /// If only saving quest states (and not all item/quest data), make sure Item["x"] elements /// exist for all quests in the saved game data. /// /// private static void EnsureQuestsExist(string saveData) { if (includeAllItemData || DialogueManager.Instance.persistentDataSettings.includeAllItemData) return; var itemTable = Lua.Environment.GetValue("Item") as Language.Lua.LuaTable; if (itemTable == null) return; var preLength = "Item[".Length; var postLength = "].State".Length; foreach (Match match in Regex.Matches(saveData, @"Item\[[^\]]+\].State")) { var s = match.Value.Substring(preLength + 1, match.Value.Length - (preLength + postLength + 2)); if (itemTable.GetKey(s) == Language.Lua.LuaNil.Nil) { var questKey = new Language.Lua.LuaString(s); var table = new Language.Lua.LuaTable(); table.RawSetValue("Name", new Language.Lua.LuaString(s)); table.RawSetValue("State", new Language.Lua.LuaString("unassigned")); itemTable.SetKeyValue(questKey, table); } } } #endif /// /// Saves a game by retrieving the Lua environment and returning it as a saved-game string. /// This method calls Record() to allow all game objects in the scene to record their state /// to the Lua environment first. The returned string is human-readable Lua code. /// /// /// The saved-game data. /// /// /// To reduce saved-game data size, only the following information is recorded from the /// Chat Mapper tables (Item[], Actor[], etc): /// /// - Actor[]: all data /// - Item[]: only State (for quest log system) /// - Location[]: nothing /// - Variable[]: current value of each variable /// - Conversation[]: SimStatus /// - Relationship and status information is recorded /// public static string GetSaveData() { Record(); string saveData; var sb = new StringBuilder(); AppendDialogueSystemData(sb); saveData = sb.ToString(); if (DialogueDebug.LogInfo) Debug.Log(string.Format("{0}: Saved data: {1}", new System.Object[] { DialogueDebug.Prefix, saveData })); return saveData; } #endregion #region Save (Non-Conversation Data) public static void AppendDialogueSystemData(StringBuilder sb) { if (sb == null) return; AppendVariableData(sb); AppendItemData(sb); AppendLocationData(sb); if (includeActorData) AppendActorData(sb); AppendConversationData(sb); if (includeRelationshipAndStatusData) AppendRelationshipAndStatusTables(sb); if (GetCustomSaveData != null) sb.Append(GetCustomSaveData()); } /// /// Appends the user variable table to a (saved-game) string. /// public static void AppendVariableData(StringBuilder sb) { try { LuaTableWrapper variableTable = Lua.Run("return Variable").AsTable; if (variableTable == null) { if (DialogueDebug.LogErrors) Debug.LogError(string.Format("{0}: Persistent Data Manager couldn't access Lua Variable[] table", new System.Object[] { DialogueDebug.Prefix })); return; } sb.Append("Variable={"); var first = true; foreach (var key in variableTable.Keys) { if (string.IsNullOrEmpty(key)) continue; if (!first) sb.Append(", "); first = false; var value = variableTable[key.ToString()]; sb.AppendFormat("{0}={1}", new System.Object[] { GetFieldKeyString(key), GetFieldValueString(value) }); } sb.Append("}; "); } catch (System.Exception e) { Debug.LogError(string.Format("{0}: GetSaveData() failed to get variable data: {1}", new System.Object[] { DialogueDebug.Prefix, e.Message })); } } /// /// Appends the item table to a (saved-game) string. /// public static void AppendItemData(StringBuilder sb) { try { LuaTableWrapper itemTable = Lua.Run("return Item").AsTable; if (itemTable == null) { if (DialogueDebug.LogErrors) Debug.LogError(string.Format("{0}: Persistent Data Manager couldn't access Lua Item[] table", new System.Object[] { DialogueDebug.Prefix })); return; } // Cache titles of items in database: HashSet itemsInDatabase = new HashSet(); if (!includeAllItemData) { var database = DialogueManager.masterDatabase; for (int i = 0; i < database.items.Count; i++) { itemsInDatabase.Add(DialogueLua.StringToTableIndex(database.items[i].Name)); } } // Process all items: foreach (var title in itemTable.Keys) { LuaTableWrapper fields = itemTable[title.ToString()] as LuaTableWrapper; bool onlySaveQuestData = !includeAllItemData && itemsInDatabase.Contains(title); //---Was: (DialogueManager.MasterDatabase.items.Find(i => string.Equals(DialogueLua.StringToTableIndex(i.Name), title)) != null); if (fields != null) { if (onlySaveQuestData) { // If in the database, just record quest statuses and tracking: foreach (var fieldKey in fields.Keys) { if (string.IsNullOrEmpty(fieldKey)) continue; string fieldTitle = fieldKey.ToString(); if (fieldTitle.EndsWith("State")) { sb.AppendFormat("Item[\"{0}\"].{1}=\"{2}\"; ", new System.Object[] { DialogueLua.StringToTableIndex(title), (System.Object)fieldTitle, (System.Object)fields[fieldTitle] }); } else if (string.Equals(fieldTitle, "Track") || string.Equals(fieldTitle, "Viewed")) { sb.AppendFormat("Item[\"{0}\"].{1}={2}; ", new System.Object[] { DialogueLua.StringToTableIndex(title), fieldTitle, fields[fieldTitle].ToString().ToLower() }); } } } else { // If saving all data or item is not in the database, record all fields: sb.AppendFormat("Item[\"{0}\"]=", new System.Object[] { DialogueLua.StringToTableIndex(title) }); AppendFields(sb, fields); } } } } catch (System.Exception e) { Debug.LogError(string.Format("{0}: GetSaveData() failed to get item data: {1}", new System.Object[] { DialogueDebug.Prefix, e.Message })); } } private static void AppendFields(StringBuilder sb, LuaTableWrapper fields) { sb.Append("{"); try { if (fields != null) { foreach (var key in fields.Keys) { if (string.IsNullOrEmpty(key)) continue; var value = fields[key]; var valueString = GetFieldValueString(value); if (string.Equals(key, "Pictures")) valueString = valueString.Replace("\\", "/"); // Sanitize backslashes in Pictures. sb.AppendFormat("{0}={1}, ", new System.Object[] { GetFieldKeyString(key), valueString }); } } } finally { sb.Append("}; "); } } // Faster to check manually than use Regex: private static string GetFieldKeyString(string key) { key = DialogueLua.StringToTableIndex(key); return IsValidVarName(key) ? key : ("[\"" + key + "\"]"); } private static bool IsValidVarName(string key) { if (string.IsNullOrEmpty(key)) return false; char firstChar = key[0]; if (!(firstChar == '_' || ('a' <= firstChar && firstChar <= 'z') || ('A' <= firstChar && firstChar <= 'Z'))) return false; for (int i = 1; i < key.Length; i++) { var c = key[i]; if (!(c == '_' || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9'))) return false; } return true; } private static string GetFieldValueString(object o) { if (o == null) { return "nil"; } else { System.Type type = o.GetType(); if (type == typeof(string)) { return string.Format("\"{0}\"", new System.Object[] { DialogueLua.DoubleQuotesToSingle(o.ToString().Replace("\n", "\\n").Replace("\\ ", "/ ")) }); } else if (type == typeof(bool)) { return o.ToString().ToLower(); } else if (type == typeof(float) || type == typeof(double)) { return ((float)o).ToString(System.Globalization.CultureInfo.InvariantCulture); } else if (type == typeof(LuaTableWrapper)) { StringBuilder sb = new StringBuilder(); AppendFields(sb, (LuaTableWrapper)o); return "{" + sb.ToString() + "}"; } else { return o.ToString(); } } } /// /// Appends the location table to a (saved-game) string. Currently doesn't save anything unless /// includeLocationData is true. /// public static void AppendLocationData(StringBuilder sb) { if (!includeLocationData) return; try { LuaTableWrapper locationTable = Lua.Run("return Location").AsTable; if (locationTable == null) { if (DialogueDebug.LogErrors) Debug.LogError(string.Format("{0}: Persistent Data Manager couldn't access Lua Location[] table", new System.Object[] { DialogueDebug.Prefix })); return; } sb.Append("Location={"); var first = true; foreach (var key in locationTable.Keys) { if (string.IsNullOrEmpty(key)) continue; LuaTableWrapper fields = locationTable[key] as LuaTableWrapper; if (!first) sb.Append(", "); first = false; sb.Append(GetFieldKeyString(key)); sb.Append("={"); try { AppendAssetFieldData(sb, fields); } finally { sb.Append("}"); } } sb.Append("}; "); } catch (System.Exception e) { Debug.LogError(string.Format("{0}: GetSaveData() failed to get location data: {1}", new System.Object[] { DialogueDebug.Prefix, e.Message })); } } /// /// Appends the actor table to a (saved-game) string. /// public static void AppendActorData(StringBuilder sb) { try { LuaTableWrapper actorTable = Lua.Run("return Actor").AsTable; if (actorTable == null) { if (DialogueDebug.LogErrors) Debug.LogError(string.Format("{0}: Persistent Data Manager couldn't access Lua Actor[] table", new System.Object[] { DialogueDebug.Prefix })); return; } sb.Append("Actor={"); var first = true; foreach (var key in actorTable.Keys) { if (string.IsNullOrEmpty(key)) continue; LuaTableWrapper fields = actorTable[key] as LuaTableWrapper; if (!first) sb.Append(", "); first = false; sb.Append(GetFieldKeyString(key)); sb.Append("={"); try { AppendAssetFieldData(sb, fields); } finally { sb.Append("}"); } } sb.Append("}; "); } catch (System.Exception e) { Debug.LogError(string.Format("{0}: GetSaveData() failed to get actor data: {1}", new System.Object[] { DialogueDebug.Prefix, e.Message })); } } /// /// Appends an actor record to a saved-game string. /// private static void AppendAssetFieldData(StringBuilder sb, LuaTableWrapper fields) { if (fields == null) return; var first = true; foreach (var key in fields.Keys) { if (string.IsNullOrEmpty(key)) continue; if (!first) sb.Append(", "); first = false; var value = fields[key]; sb.AppendFormat("{0}={1}", new System.Object[] { GetFieldKeyString(key), GetFieldValueString(value) }); } } /// /// Appends the relationship and status tables to a (saved-game) string. /// /// StringBuilder to append to. public static void AppendRelationshipAndStatusTables(StringBuilder sb) { try { sb.Append(DialogueLua.GetStatusTableAsLua()); sb.Append(DialogueLua.GetRelationshipTableAsLua()); } catch (System.Exception e) { Debug.LogError(string.Format("{0}: GetSaveData() failed to get relationship and status data: {1}", new System.Object[] { DialogueDebug.Prefix, e.Message })); } } /// /// Instructs the Dialogue System to refresh its internal relationship and status tables /// from the values in the Lua environment. Call this after putting new values in the /// Lua environment, such as when loading a saved game. /// public static void RefreshRelationshipAndStatusTablesFromLua() { DialogueLua.RefreshStatusTableFromLua(); DialogueLua.RefreshRelationshipTableFromLua(); } #endregion #region Save (Conversation Data) /// /// Appends the conversation table to a (saved-game) string. To conserve space, only the /// SimStatus is recorded. If includeSimStatus is false, nothing is recorded. /// The exception is if includeAllConversationFields is true. /// public static void AppendConversationData(StringBuilder sb) { if (includeAllConversationFields || DialogueManager.Instance.persistentDataSettings.includeAllConversationFields) { AppendAllConversationFields(sb); } if (includeSimStatus && DialogueManager.Instance.includeSimStatus) { AppendSimStatus(sb); } } /// /// Appends all conversation fields to a saved-game string. Note that this doesn't /// append the fields inside each dialogue entry, just the fields in the conversation /// objects themselves. /// private static void AppendAllConversationFields(StringBuilder sb) { try { LuaTableWrapper conversationTable = Lua.Run("return Conversation").AsTable; if (conversationTable == null) { if (DialogueDebug.LogErrors) Debug.LogError(string.Format("{0}: Persistent Data Manager couldn't access Lua Conversation[] table", new System.Object[] { DialogueDebug.Prefix })); return; } foreach (var convIndex in conversationTable.Keys) // Loop through conversations: { LuaTableWrapper fields = Lua.Run("return Conversation[" + convIndex + "]").AsTable; if (fields == null) continue; sb.Append("Conversation[" + convIndex + "]={"); try { var first = true; foreach (var key in fields.Keys) { if (string.IsNullOrEmpty(key)) continue; if (string.Equals(key, "Dialog")) continue; if (!first) sb.Append(", "); first = false; var value = fields[key.ToString()]; sb.AppendFormat("{0}={1}", new System.Object[] { GetFieldKeyString(key), GetFieldValueString(value) }); } } finally { sb.Append("}; "); } } } catch (System.Exception e) { Debug.LogError(string.Format("{0}: GetSaveData() failed to get conversation data: {1}", new System.Object[] { DialogueDebug.Prefix, e.Message })); } } #if USE_NLUA /// /// Appends SimStatus for all conversations. /// private static void AppendSimStatus(StringBuilder sb) { try { var useConversationID = string.IsNullOrEmpty(saveConversationSimStatusWithField); var useEntryID = string.IsNullOrEmpty(saveDialogueEntrySimStatusWithField); foreach (var conversation in DialogueManager.MasterDatabase.conversations) { if (useConversationID) { sb.AppendFormat("Conversation[{0}].SimX=\"", conversation.id); } else { var fieldValue = DialogueLua.StringToTableIndex(conversation.LookupValue(saveConversationSimStatusWithField)); if (string.IsNullOrEmpty(fieldValue)) fieldValue = conversation.id.ToString(); sb.AppendFormat("Variable[\"Conversation_SimX_{0}\"]=\"", fieldValue); } var dialogTable = Lua.Run("return Conversation[" + conversation.id + "].Dialog").asTable; var first = true; for (int i = 0; i < conversation.dialogueEntries.Count; i++) { var entry = conversation.dialogueEntries[i]; var entryID = entry.id; var dialogFields = dialogTable[entryID] as NLua.LuaTable; if (dialogFields != null) { if (!first) sb.Append(";"); first = false; sb.Append(useEntryID ? entryID.ToString() : Field.LookupValue(entry.fields, saveDialogueEntrySimStatusWithField)); sb.Append(";"); var simStatus = dialogFields[DialogueLua.SimStatus].ToString(); sb.Append(SimStatusToChar(simStatus)); } } sb.Append("\"; "); } } catch (System.Exception e) { Debug.LogError(string.Format("{0}: GetSaveData() failed to get conversation data: {1}", new System.Object[] { DialogueDebug.Prefix, e.Message })); } } private static void ExpandCompressedSimStatusData() { if (!(includeSimStatus && DialogueManager.Instance.includeSimStatus)) return; try { var useConversationID = string.IsNullOrEmpty(saveConversationSimStatusWithField); var useEntryID = string.IsNullOrEmpty(saveDialogueEntrySimStatusWithField); var entryDict = new Dictionary(); foreach (var conversation in DialogueManager.MasterDatabase.conversations) { // If saving dialogue entries' SimStatus with value of a field, make a lookup table: if (!useEntryID) { entryDict.Clear(); for (int i = 0; i < conversation.dialogueEntries.Count; i++) { var entry = conversation.dialogueEntries[i]; var entryFieldValue = Field.LookupValue(entry.fields, saveDialogueEntrySimStatusWithField); if (!entryDict.ContainsKey(entryFieldValue)) entryDict.Add(entryFieldValue, entry); } } var sb = new StringBuilder(); string simX; if (useConversationID) { simX = Lua.Run("return Conversation[" + conversation.id + "].SimX").AsString; } else { var fieldValue = DialogueLua.StringToTableIndex(conversation.LookupValue(saveConversationSimStatusWithField)); if (string.IsNullOrEmpty(fieldValue)) fieldValue = conversation.id.ToString(); simX = Lua.Run("return Variable[\"Conversation_SimX_" + fieldValue + "\"]").AsString; } if (string.IsNullOrEmpty(simX) || string.Equals(simX, "nil")) continue; var clearSimXCommand = useConversationID ? ("Conversation[" + conversation.id + "].SimX=nil;") : ("Variable[\"Conversation_SimX_" + DialogueLua.StringToTableIndex(conversation.LookupValue(saveConversationSimStatusWithField)) + "\"]=nil;"); sb.Append("Conversation["); sb.Append(conversation.id); sb.Append("].Dialog={}; "); var simXFields = simX.Split(';'); var numFields = simXFields.Length / 2; for (int i = 0; i < numFields; i++) { var simXEntryIDValue = simXFields[2 * i]; string entryID; if (useEntryID) { entryID = simXEntryIDValue; } else { entryID = entryDict.ContainsKey(simXEntryIDValue) ? entryDict[simXEntryIDValue].id.ToString() : "-1"; } var simStatus = CharToSimStatus(simXFields[(2 * i) + 1][0]); sb.Append("Conversation["); sb.Append(conversation.id); sb.Append("].Dialog["); sb.Append(entryID); sb.Append("]={SimStatus='"); sb.Append(simStatus); sb.Append("'}; "); } sb.Append(clearSimXCommand); Lua.Run(sb.ToString()); } } catch (System.Exception e) { Debug.LogError(string.Format("{0}: ApplySaveData() failed to re-expand compressed SimStatus data: {1}", new System.Object[] { DialogueDebug.Prefix, e.Message })); } } #else // Used by SimStatus methods: private static bool useConversationID = true; private static bool useEntryID = true; /// /// Appends SimStatus for all conversations. /// public static void AppendSimStatus(StringBuilder sb) { try { useConversationID = string.IsNullOrEmpty(saveConversationSimStatusWithField); useEntryID = string.IsNullOrEmpty(saveDialogueEntrySimStatusWithField); var conversationTable = Lua.Environment.GetValue("Conversation") as Language.Lua.LuaTable; if (conversationTable == null) return; for (int i = 0; i < conversationTable.List.Count; i++) { var conversationID = i + 1; var fieldTable = conversationTable.List[i] as Language.Lua.LuaTable; AppendSimStatusForConversation(sb, conversationTable, conversationID, fieldTable); } foreach (var kvp in conversationTable.Dict) { if (kvp.Key == null || kvp.Value == null || !(kvp.Value is Language.Lua.LuaTable)) continue; var conversationID = Tools.StringToInt(kvp.Key.ToString()); var fieldTable = kvp.Value as Language.Lua.LuaTable; AppendSimStatusForConversation(sb, conversationTable, conversationID, fieldTable); } } catch (System.Exception e) { Debug.LogError(string.Format("{0}: GetSaveData() failed to get conversation data: {1}", new System.Object[] { DialogueDebug.Prefix, e.Message })); } } private static Dictionary s_dialogueEntrySimStatusFieldLookupTable = new Dictionary(); /// /// Appends the SimStatus info for a single conversation. /// /// Returns the number of dialogue entries in the conversation. private static int AppendSimStatusForConversation(StringBuilder sb, Language.Lua.LuaTable conversationTable, int conversationID, Language.Lua.LuaTable fieldTable) { if (sb == null || conversationTable == null || fieldTable == null) return 0; var dialogTable = fieldTable.GetValue("Dialog") as Language.Lua.LuaTable; if (dialogTable == null) return 0; var conversation = DialogueManager.MasterDatabase.GetConversation(conversationID); if (conversation == null) return 0; if (useConversationID) { sb.AppendFormat("Conversation[{0}].SimX=\"", conversationID); } else { sb.AppendFormat("Variable[\"Conversation_SimX_{0}\"]=\"", DialogueLua.StringToTableIndex(conversation.LookupValue(saveConversationSimStatusWithField))); } var first = true; for (int i = 0; i < dialogTable.List.Count; i++) { var entryID = i + 1; var entryIDString = entryID.ToString(); var simStatusTable = dialogTable.List[i] as Language.Lua.LuaTable; if (!first) sb.Append(";"); first = false; if (useEntryID) { sb.Append(entryIDString); } else { var entry = conversation.GetDialogueEntry(entryID); var fieldName = (entry != null) ? Field.LookupValue(entry.fields, saveDialogueEntrySimStatusWithField) : entryIDString; sb.Append(fieldName); } sb.Append(";"); var simStatus = simStatusTable.GetValue(DialogueLua.SimStatus).ToString(); sb.Append(SimStatusToChar(simStatus)); } if (!useEntryID) { // Create a lookup table to speed up lookups of each dialogue entry's SimStatus field: s_dialogueEntrySimStatusFieldLookupTable.Clear(); for (int i = 0; i < conversation.dialogueEntries.Count; i++) { var entry = conversation.dialogueEntries[i]; var field = Field.Lookup(entry.fields, saveDialogueEntrySimStatusWithField); var fieldName = (field != null) ? field.value : entry.id.ToString(); s_dialogueEntrySimStatusFieldLookupTable.Add(entry.id, fieldName); } } foreach (var kvp2 in dialogTable.KeyValuePairs) { var entryIDString = kvp2.Key.ToString(); var simStatusTable = kvp2.Value as Language.Lua.LuaTable; if (!first) sb.Append(";"); first = false; if (useEntryID) { sb.Append(entryIDString); } else { var entryID = Tools.StringToInt(entryIDString); sb.Append(s_dialogueEntrySimStatusFieldLookupTable[entryID]); } sb.Append(";"); var simStatus = simStatusTable.GetValue(DialogueLua.SimStatus).ToString(); sb.Append(SimStatusToChar(simStatus)); } sb.Append("\"; "); s_dialogueEntrySimStatusFieldLookupTable.Clear(); return conversation.dialogueEntries.Count; } /// /// When reapplying saved data, expands compress SimX info into conversations' SimStatus tables. /// public static void ExpandCompressedSimStatusData() { if (!(includeSimStatus && DialogueManager.Instance.includeSimStatus)) return; // Track conversations so we know which ones were added after the saved game: var conversationsLeft = new HashSet(); var conversations = DialogueManager.MasterDatabase.conversations; for (int i = 0; i < conversations.Count; i++) { conversationsLeft.Add(conversations[i].id); } // Reusable dialogue entry cache used by ExpandSimStatusForConversation: var dialogueEntryCache = new Dictionary(); var luaStringSimX = new Language.Lua.LuaString("SimX"); useConversationID = string.IsNullOrEmpty(saveConversationSimStatusWithField); useEntryID = string.IsNullOrEmpty(saveDialogueEntrySimStatusWithField); var conversationTable = Lua.Environment.GetValue("Conversation") as Language.Lua.LuaTable; if (conversationTable == null) return; var sb = new StringBuilder(16384, System.Int32.MaxValue); for (int i = 0; i < conversationTable.List.Count; i++) { var conversationID = i + 1; var fieldTable = conversationTable.List[i] as Language.Lua.LuaTable; if (ExpandSimStatusForConversation(sb, conversationID, conversationID.ToString(), fieldTable, luaStringSimX, dialogueEntryCache)) { conversationsLeft.Remove(conversationID); } } foreach (var kvp in conversationTable.Dict) { if (kvp.Key == null || kvp.Value == null || !(kvp.Value is Language.Lua.LuaTable)) continue; var conversationIDString = kvp.Key.ToString(); var conversationID = Tools.StringToInt(conversationIDString); var fieldTable = kvp.Value as Language.Lua.LuaTable; if (ExpandSimStatusForConversation(sb, conversationID, conversationIDString, fieldTable, luaStringSimX, dialogueEntryCache)) { conversationsLeft.Remove(conversationID); } } Lua.Run(sb.ToString()); // Add SimStatus for new conversations: if (conversationsLeft.Count > 0) { var enumerator = conversationsLeft.GetEnumerator(); while (enumerator.MoveNext()) { var conversationID = enumerator.Current; var conversation = DialogueManager.MasterDatabase.GetConversation(conversationID); if (conversation == null) continue; #if SAFE_SIMSTATUS if (DialogueDebug.logInfo) Debug.Log("DEBUG: Add SimStatus for new conversation [" + conversationID + "]: " + conversation.Title); #endif DialogueLua.AddToConversationTable(conversationTable, conversation, true); } } } /// /// Expands SimX for a conversation. /// private static bool ExpandSimStatusForConversation(StringBuilder sb, int conversationID, string conversationIDString, Language.Lua.LuaTable fieldTable, Language.Lua.LuaString luaStringSimX, Dictionary dialogueEntryCache) { // Find our Lua Dialog[] table and conversation asset: var dialogTable = fieldTable.GetValue("Dialog") as Language.Lua.LuaTable; if (dialogTable == null) { dialogTable = new Language.Lua.LuaTable(); fieldTable.AddRaw("Dialog", dialogTable); } dialogTable.List.Clear(); dialogTable.Dict.Clear(); var conversation = DialogueManager.MasterDatabase.GetConversation(conversationID); if (conversation == null) return false; // Get the compressed SimStatus string: string simX; if (useConversationID) { var simXLuaValue = fieldTable.GetValue(luaStringSimX); if (simXLuaValue == null) return false; simX = simXLuaValue.ToString(); sb.AppendFormat("Conversation[{0}].SimX=nil;", conversationIDString); } else { var fieldValue = DialogueLua.StringToTableIndex(conversation.LookupValue(saveConversationSimStatusWithField)); if (string.IsNullOrEmpty(fieldValue)) fieldValue = conversation.id.ToString(); simX = Lua.Run("return Variable[\"Conversation_SimX_" + fieldValue + "\"]").AsString; sb.Append("Variable[\"Conversation_SimX_" + fieldValue + "\"]=nil;"); } if (string.IsNullOrEmpty(simX) || string.Equals(simX, "nil")) return false; var simXFields = simX.Split(';'); var numFields = simXFields.Length / 2; // Index dialogue entries by ID: (don't worry about unused old entries; this conversation shouldn't reference them) DialogueEntry entry; for (int i = 0; i < conversation.dialogueEntries.Count; i++) { entry = conversation.dialogueEntries[i]; dialogueEntryCache[entry.id] = entry; } // Make table of SimStatus fields to entry IDs. Dictionary simStatusFieldValueToID = null; if (!useEntryID) { simStatusFieldValueToID = new Dictionary(); for (int i = 0; i < conversation.dialogueEntries.Count; i++) { entry = conversation.dialogueEntries[i]; var field = (entry != null) ? Field.Lookup(entry.fields, saveDialogueEntrySimStatusWithField) : null; simStatusFieldValueToID[(field != null) ? field.value : entry.id.ToString()] = entry.id; } } // Iterate through fields of compressed SimStatus string: for (int i = 0; i < numFields; i++) { var simXEntryIDValue = simXFields[2 * i]; var simStatus = CharToSimStatus(simXFields[(2 * i) + 1][0]); var simStatusTable = new Language.Lua.LuaTable(); simStatusTable.AddRaw(DialogueLua.SimStatus, new Language.Lua.LuaString(simStatus)); if (!useEntryID && !simStatusFieldValueToID.ContainsKey(simXEntryIDValue)) continue; var entryID = useEntryID ? Tools.StringToInt(simXEntryIDValue) : simStatusFieldValueToID[simXEntryIDValue]; dialogueEntryCache[entryID] = null; // Mark that SimStatus has been added for this entry. if (useEntryID) { dialogTable.AddRaw(entryID, simStatusTable); } else { if (simStatusFieldValueToID.ContainsKey(simXEntryIDValue)) { dialogTable.AddRaw(simStatusFieldValueToID[simXEntryIDValue], simStatusTable); } } } // Backfill any new entries that weren't included in the compressed SimStatus string: for (int i = 0; i < conversation.dialogueEntries.Count; i++) { entry = conversation.dialogueEntries[i]; if (dialogueEntryCache[entry.id] != null) { #if SAFE_SIMSTATUS if (DialogueDebug.logInfo) Debug.Log("DEBUG: Adding SimStatus for new entry [" + entry.id + "] in existing conversation [" + conversation.id + "]: " + conversation.Title); #endif // Missing. Need to add: var simStatusTable = new Language.Lua.LuaTable(); simStatusTable.AddRaw(DialogueLua.SimStatus, new Language.Lua.LuaString(DialogueLua.Untouched)); dialogTable.AddRaw(entry.id, simStatusTable); } } return true; } #endif private static char SimStatusToChar(string simStatus) { switch (simStatus) { default: return 'X'; case DialogueLua.Untouched: return 'u'; case DialogueLua.WasDisplayed: return 'd'; case DialogueLua.WasOffered: return 'o'; } } private static string CharToSimStatus(char c) { switch (c) { default: return "ERROR"; case 'u': return DialogueLua.Untouched; case 'd': return DialogueLua.WasDisplayed; case 'o': return DialogueLua.WasOffered; } } #endregion #region Initialize New Fields After Load /// /// Instructs the Dialogue System to add any missing variables that are in the master /// database but not in Lua. /// public static void InitializeNewVariablesFromDatabase() { try { LuaTableWrapper variableTable = Lua.Run("return Variable").AsTable; if (variableTable == null) { if (DialogueDebug.LogErrors) Debug.LogError(string.Format("{0}: Persistent Data Manager couldn't access Lua Variable[] table", new System.Object[] { DialogueDebug.Prefix })); return; } var database = DialogueManager.MasterDatabase; if (database == null) return; var inLua = new HashSet(variableTable.Keys); for (int i = 0; i < database.variables.Count; i++) { var variable = database.variables[i]; var variableName = variable.Name; var variableIndex = DialogueLua.StringToTableIndex(variableName); if (!inLua.Contains(variableIndex)) { switch (variable.Type) { case FieldType.Boolean: DialogueLua.SetVariable(variableName, variable.InitialBoolValue); break; case FieldType.Actor: case FieldType.Item: case FieldType.Location: case FieldType.Number: DialogueLua.SetVariable(variableName, variable.InitialFloatValue); break; default: DialogueLua.SetVariable(variableName, variable.InitialValue); break; } } } } catch (System.Exception e) { Debug.LogError(string.Format("{0}: InitializeNewVariablesFromDatabase() failed to get variable data: {1}", new System.Object[] { DialogueDebug.Prefix, e.Message })); } } #if USE_NLUA /// /// Adds any new actors or actor fields that were not in the saved data. /// public static void InitializeNewActorFieldsFromDatabase() { try { var database = DialogueManager.MasterDatabase; if (database == null) return; var actorTable = Lua.Run("return Actor").asTable; if (actorTable == null || !actorTable.IsValid) throw new System.Exception("Internal error: Can't access Actor table"); for (int i = 0; i < database.actors.Count; i++) { var dbActor = database.actors[i]; var actorName = dbActor.Name; var actorNameTableIndex = DialogueLua.StringToTableIndex(actorName); var actorRecord = Lua.Run("return Actor[\"" + actorNameTableIndex + "\"]"); if (!actorRecord.isTable) { // This is a new actor not in the save data. Add it: var newActorLuaCode = "Actor[\"" + actorNameTableIndex + "\"] = {"; for (int j = 0; j < dbActor.fields.Count; j++) { var field = dbActor.fields[j]; var fieldIndex = DialogueLua.StringToFieldName(field.title); newActorLuaCode += fieldIndex + DialogueLua.FieldValueAsString(field) + ", "; } newActorLuaCode += "}"; Lua.Run(newActorLuaCode); } else { // Existing actor. Add any missing fields: var fieldTable = actorRecord.asTable; if (fieldTable == null) continue; var existingFields = new HashSet(fieldTable.keys); for (int j = 0; j < dbActor.fields.Count; j++) { var field = dbActor.fields[j]; var fieldTableIndex = DialogueLua.StringToFieldName(field.title); if (!existingFields.Contains(fieldTableIndex)) { Lua.Run("Actor[\"" + actorNameTableIndex + "\"]." + fieldTableIndex + " = " + DialogueLua.FieldValueAsString(field)); } } } } } catch (System.Exception e) { Debug.LogError(string.Format("{0}: InitializeNewActorFieldsFromDatabase() failed to get actor data: {1}", new System.Object[] { DialogueDebug.Prefix, e.Message })); } } #else /// /// Adds any new actors or actor fields that were not in the saved data. /// public static void InitializeNewActorFieldsFromDatabase() { try { var database = DialogueManager.MasterDatabase; if (database == null) return; var actorTable = Lua.Run("return Actor").AsTable; if (actorTable == null || !actorTable.IsValid) throw new System.Exception("Internal error: Can't access Actor table"); for (int i = 0; i < database.actors.Count; i++) { var dbActor = database.actors[i]; var actorName = dbActor.Name; var actorNameTableIndex = DialogueLua.StringToTableIndex(actorName); var fieldTable = actorTable.luaTable.GetValue(actorNameTableIndex) as Language.Lua.LuaTable; if (fieldTable == null) { // This is a new actor not in the save data. Add it: fieldTable = new Language.Lua.LuaTable(); for (int j = 0; j < dbActor.fields.Count; j++) { var field = dbActor.fields[j]; var fieldIndex = DialogueLua.StringToFieldName(field.title); fieldTable.AddRaw(fieldIndex, DialogueLua.GetFieldLuaValue(field)); } actorTable.luaTable.AddRaw(actorNameTableIndex, fieldTable); } else { // Existing actor. Add any missing fields: var existingFields = new HashSet(); foreach (var key in fieldTable.Keys) { existingFields.Add(key.ToString()); } for (int j = 0; j < dbActor.fields.Count; j++) { var field = dbActor.fields[j]; var fieldTableIndex = DialogueLua.StringToFieldName(field.title); if (!existingFields.Contains(fieldTableIndex)) { var fieldValue = DialogueLua.GetFieldLuaValue(field); fieldTable.AddRaw(fieldTableIndex, fieldValue); } } } } } catch (System.Exception e) { Debug.LogError(string.Format("{0}: InitializeNewActorFieldsFromDatabase() failed to get actor data: {1}", new System.Object[] { DialogueDebug.Prefix, e.Message })); } } #endif /// /// Instructs the Dialogue System to add any missing quests and entries that are in the master /// database but not in Lua. /// public static void InitializeNewQuestEntriesFromDatabase() { try { var luaCode = string.Empty; var database = DialogueManager.MasterDatabase; if (database == null) return; for (int i = 0; i < database.items.Count; i++) { if (database.items[i].IsItem) continue; var dbQuest = database.items[i]; var questName = dbQuest.Name; var questNameTableIndex = DialogueLua.StringToTableIndex(questName); // Add any missing quests: if (!DialogueLua.DoesTableElementExist("Item", questName)) { var questCode = string.Empty; questCode = "Item[\"" + DialogueLua.StringToTableIndex(questName) + "\"] = {{"; for (int j = 0; j < dbQuest.fields.Count; j++) { var field = dbQuest.fields[j]; questCode += DialogueLua.StringToFieldName(field.title) + "=" + DialogueLua.ValueAsString(field.type, field.value) + ", "; } questCode += "}}; "; luaCode += questCode; } // Add any missing entries: var dbEntryCount = dbQuest.LookupInt("Entry Count"); var luaEntryCount = DialogueLua.GetQuestField(questName, "Entry Count").AsInt; if (luaEntryCount < dbEntryCount) { luaCode += "Item[\"" + questNameTableIndex + "\"].Entry_Count=" + dbEntryCount + "; "; for (int j = 0; j < dbQuest.fields.Count; j++) { var field = dbQuest.fields[j]; if (field.title.StartsWith("Entry ") && !field.title.EndsWith(" Count")) { luaCode += "Item[\"" + questNameTableIndex + "\"]." + DialogueLua.StringToFieldName(field.title) + " = " + DialogueLua.ValueAsString(field.type, field.value) + "; "; } } } Lua.Run(luaCode); } } catch (System.Exception e) { Debug.LogError(string.Format("{0}: InitializeNewQuestEntriesFromDatabase() failed to get quest data: {1}", new System.Object[] { DialogueDebug.Prefix, e.Message })); } } /// /// Initializes SimStatus for entries that were added to the database after the saved game. /// public static void InitializeNewSimStatusFromDatabase() { // For LuaInterpreter, ExpandSimStatusForConversation also initializes new SimStatus, // so we only need this for NLua: #if NLUA || SAFE_SIMSTATUS if (!(includeSimStatus && initializeNewSimStatus)) return; try { var database = DialogueManager.MasterDatabase; if (database == null) return; var missingConversations = new List(); var fakeLoadedDatabases = new List(); var luaCode = string.Empty; for (int i = 0; i < database.conversations.Count; i++) { var conversation = database.conversations[i]; var convTableIdent = "Conversation[" + conversation.id + "]"; var convTable = Lua.Run("return " + convTableIdent).AsTable; if (!convTable.IsValid) { missingConversations.Add(conversation); } else { for (int j = 0; j < conversation.dialogueEntries.Count; j++) { var entry = conversation.dialogueEntries[j]; var dialogTableIdent = convTableIdent + ".Dialog[" + entry.id + "]"; var entryTable = Lua.Run("return " + dialogTableIdent).AsTable; if (!entryTable.IsValid) { luaCode += dialogTableIdent + "={SimStatus=\"Untouched\"}; "; } } } } Lua.Run(luaCode, DialogueDebug.LogInfo); DialogueLua.AddToConversationTable(missingConversations, fakeLoadedDatabases); } catch (System.Exception e) { Debug.LogError("Dialogue System: InitializeNewSimStatusFromDatabase() failed: " + e.Message); } #endif } #endregion #region Async Saving //====================================================================== // Asynchronous saving: public static int asyncGameObjectBatchSize = 1000; public static int asyncDialogueEntryBatchSize = 100; public class AsyncSaveOperation { public bool isDone = false; public string content = string.Empty; } public static AsyncSaveOperation GetSaveDataAsync() { var asyncOp = new AsyncSaveOperation(); DialogueManager.Instance.StartCoroutine(GetSaveDataAsyncCoroutine(asyncOp)); return asyncOp; } private static IEnumerator GetSaveDataAsyncCoroutine(AsyncSaveOperation asyncOp) { if (DialogueDebug.LogInfo) Debug.Log(string.Format("{0}: Saving data asynchronously...", new System.Object[] { DialogueDebug.Prefix, asyncGameObjectBatchSize })); switch (recordPersistentDataOn) { case RecordPersistentDataOn.AllGameObjects: yield return DialogueManager.Instance.StartCoroutine(Tools.SendMessageToEveryoneAsync("OnRecordPersistentData", asyncGameObjectBatchSize)); break; case RecordPersistentDataOn.OnlyRegisteredGameObjects: int count = 0; foreach (var go in listeners) { if (go != null) { go.SendMessage("OnRecordPersistentData", SendMessageOptions.DontRequireReceiver); count++; if (count > asyncGameObjectBatchSize) { count = 0; yield return null; } } } break; default: break; } StringBuilder sb = new StringBuilder(); AppendVariableData(sb); yield return null; AppendItemData(sb); yield return null; AppendLocationData(sb); yield return null; if (includeActorData) AppendActorData(sb); yield return null; yield return DialogueManager.Instance.StartCoroutine(AppendConversationDataAsync(sb)); yield return null; if (includeRelationshipAndStatusData) AppendRelationshipAndStatusTables(sb); if (GetCustomSaveData != null) sb.Append(GetCustomSaveData()); string saveData = sb.ToString(); if (DialogueDebug.LogInfo) Debug.Log(string.Format("{0}: Saved data asynchronously: {1}", new System.Object[] { DialogueDebug.Prefix, saveData })); asyncOp.content = saveData; asyncOp.isDone = true; } /// /// Sends the OnRecordPersistentData message to all game objects in the scene to give them /// an opportunity to record their state in the Lua environment. Runs in batches specified /// by the value of asyncGameObjectBatchSize. /// public static void RecordAsync() { if (DialogueDebug.LogInfo) Debug.Log(string.Format("{0}: Recording persistent data to Lua environment in batches of {1} GameObjects.", new System.Object[] { DialogueDebug.Prefix, asyncGameObjectBatchSize })); DialogueManager.Instance.StartCoroutine(Tools.SendMessageToEveryoneAsync("OnRecordPersistentData", asyncGameObjectBatchSize)); } private static IEnumerator AppendConversationDataAsync(StringBuilder sb) { if (includeAllConversationFields || DialogueManager.Instance.persistentDataSettings.includeAllConversationFields) { AppendAllConversationFields(sb); } if (includeSimStatus && DialogueManager.Instance.includeSimStatus) { var count = 0; #if USE_NLUA var useConversationID = string.IsNullOrEmpty(saveConversationSimStatusWithField); var useEntryID = string.IsNullOrEmpty(saveDialogueEntrySimStatusWithField); foreach (var conversation in DialogueManager.MasterDatabase.conversations) { if (useConversationID) { sb.AppendFormat("Conversation[{0}].SimX=\"", conversation.id); } else { var fieldValue = DialogueLua.StringToTableIndex(conversation.LookupValue(saveConversationSimStatusWithField)); if (string.IsNullOrEmpty(fieldValue)) fieldValue = conversation.id.ToString(); sb.AppendFormat("Variable[\"Conversation_SimX_{0}\"]=\"", fieldValue); } var dialogTable = Lua.Run("return Conversation[" + conversation.id + "].Dialog").asTable; var first = true; for (int i = 0; i < conversation.dialogueEntries.Count; i++) { try { var entry = conversation.dialogueEntries[i]; var entryID = entry.id; var dialogFields = dialogTable[entryID] as NLua.LuaTable; if (dialogFields != null) { if (!first) sb.Append(";"); first = false; sb.Append(useEntryID ? entryID.ToString() : Field.LookupValue(entry.fields, saveDialogueEntrySimStatusWithField)); sb.Append(";"); var simStatus = dialogFields[DialogueLua.SimStatus].ToString(); sb.Append(SimStatusToChar(simStatus)); } } catch (System.Exception e) { Debug.LogError(string.Format("{0}: GetSaveData() failed to get conversation data: {1}", new System.Object[] { DialogueDebug.Prefix, e.Message })); } count++; if (count >= asyncDialogueEntryBatchSize) { count = 0; yield return null; } } sb.Append("\"; "); } #else useConversationID = string.IsNullOrEmpty(saveConversationSimStatusWithField); useEntryID = string.IsNullOrEmpty(saveDialogueEntrySimStatusWithField); var conversationTable = Lua.Environment.GetValue("Conversation") as Language.Lua.LuaTable; if (conversationTable == null) yield break; for (int i = 0; i < conversationTable.List.Count; i++) { var conversationID = i + 1; var fieldTable = conversationTable.List[i] as Language.Lua.LuaTable; count += AppendSimStatusForConversation(sb, conversationTable, conversationID, fieldTable); if (count >= asyncDialogueEntryBatchSize) { count = 0; yield return null; } } foreach (var kvp in conversationTable.Dict) { if (kvp.Key == null || kvp.Value == null || !(kvp.Value is Language.Lua.LuaTable)) continue; var conversationID = Tools.StringToInt(kvp.Key.ToString()); var fieldTable = kvp.Value as Language.Lua.LuaTable; count += AppendSimStatusForConversation(sb, conversationTable, conversationID, fieldTable); if (count >= asyncDialogueEntryBatchSize) { count = 0; yield return null; } } #endif } } #endregion #region Raw Dump #if USE_NLUA // Note: NLua doesn't implement raw dump. It just does a passthrough to the regular save technique. public static byte[] GetRawData() { return Encoding.UTF8.GetBytes(GetSaveData()); } public static void ApplyRawData(byte[] bytes) { string s = Encoding.UTF8.GetString(bytes); ApplySaveData(s); } #else // Note: Raw dump is only implemented for LuaInterpreter (the default Lua implementation). public class AsyncRawDataOperation { public bool isDone = false; public byte[] content = null; } public static byte[] GetRawData() { Record(); using (var ms = new MemoryStream()) { var writer = new BinaryWriter(ms); var conversationTable = Lua.Run("return Conversation").AsTable.luaTable; PrepSimStatusForRawData(conversationTable); WriteValue(writer, Lua.Run("return Actor").AsTable.luaTable); WriteValue(writer, Lua.Run("return Item").AsTable.luaTable); WriteValue(writer, Lua.Run("return Location").AsTable.luaTable); WriteValue(writer, Lua.Run("return Variable").AsTable.luaTable); WriteValue(writer, conversationTable); WriteExtraData(writer); writer.Flush(); return ms.GetBuffer(); } } public static AsyncRawDataOperation GetRawDataAsync() { var asyncOp = new AsyncRawDataOperation(); DialogueManager.Instance.StartCoroutine(GetRawDataAsyncCoroutine(asyncOp)); return asyncOp; } private static IEnumerator GetRawDataAsyncCoroutine(AsyncRawDataOperation asyncOp) { if (DialogueDebug.LogInfo) Debug.Log("Dialogue System: Saving raw Lua data asynchronously..."); // Record persistent data objects async: switch (recordPersistentDataOn) { case RecordPersistentDataOn.AllGameObjects: yield return DialogueManager.Instance.StartCoroutine(Tools.SendMessageToEveryoneAsync("OnRecordPersistentData", asyncGameObjectBatchSize)); break; case RecordPersistentDataOn.OnlyRegisteredGameObjects: int count = 0; foreach (var go in listeners) { if (go != null) { go.SendMessage("OnRecordPersistentData", SendMessageOptions.DontRequireReceiver); count++; if (count > asyncGameObjectBatchSize) { count = 0; yield return null; } } } break; default: break; } using (var ms = new MemoryStream()) { var writer = new BinaryWriter(ms); var conversationTable = Lua.Run("return Conversation").AsTable.luaTable; yield return DialogueManager.Instance.StartCoroutine(PrepSimStatusForRawDataAsync(conversationTable)); WriteValue(writer, Lua.Run("return Actor").AsTable.luaTable); yield return null; WriteValue(writer, Lua.Run("return Item").AsTable.luaTable); yield return null; WriteValue(writer, Lua.Run("return Location").AsTable.luaTable); yield return null; WriteValue(writer, Lua.Run("return Variable").AsTable.luaTable); yield return null; WriteValue(writer, conversationTable); yield return null; WriteExtraData(writer); writer.Flush(); asyncOp.content = ms.GetBuffer(); } asyncOp.isDone = true; } private static void WriteValue(BinaryWriter writer, Language.Lua.LuaValue value) { if (value is Language.Lua.LuaTable) { WriteTable(writer, value as Language.Lua.LuaTable); } else if (value is Language.Lua.LuaString) { writer.Write('S'); writer.Write((value as Language.Lua.LuaString).Text); } else if (value is Language.Lua.LuaNumber) { writer.Write('N'); writer.Write((value as Language.Lua.LuaNumber).Number); } else if (value is Language.Lua.LuaBoolean) { writer.Write('B'); writer.Write((value as Language.Lua.LuaBoolean).BoolValue); } else if (value is Language.Lua.LuaNil) { writer.Write('X'); } else { Debug.LogError("WriteValue unhandled " + value.GetType().Name + ": " + value.ToString()); } } private static void WriteTable(BinaryWriter writer, Language.Lua.LuaTable table) { writer.Write('T'); if (table.List == null) { writer.Write((int)0); } else { writer.Write(table.List.Count); for (int i = 0; i < table.List.Count; i++) { WriteValue(writer, table.List[i]); } } if (table.Dict == null) { writer.Write((int)0); } else { writer.Write(table.Dict.Count); var enumerator = table.Dict.GetEnumerator(); // Enumerates manually to avoid garbage. while (enumerator.MoveNext()) { WriteValue(writer, enumerator.Current.Key); WriteValue(writer, enumerator.Current.Value); } } } public static void ApplyRawData(byte[] bytes) { using (var ms = new MemoryStream(bytes)) { using (var reader = new BinaryReader(ms)) { Lua.Run("Actor = {}; Item = {}; Location = {}; Variable = {}; Conversation = {}"); ReadTable(reader, Lua.Run("return Actor").AsTable.luaTable); ReadTable(reader, Lua.Run("return Item").AsTable.luaTable); ReadTable(reader, Lua.Run("return Location").AsTable.luaTable); ReadTable(reader, Lua.Run("return Variable").AsTable.luaTable); ReadTable(reader, Lua.Run("return Conversation").AsTable.luaTable); ApplySimStatusFromRawData(); ApplyExtraData(reader); RefreshRelationshipAndStatusTablesFromLua(); if (initializeNewVariables) { InitializeNewVariablesFromDatabase(); InitializeNewQuestEntriesFromDatabase(); // Do not need this. It's done implicitly when expanding SimX: InitializeNewSimStatusFromDatabase(); } } } Apply(); } private static Language.Lua.LuaValue ReadValue(BinaryReader reader) { if ((char)reader.PeekChar() == 'T') { var luaTable = new Language.Lua.LuaTable(); ReadTable(reader, luaTable); return luaTable; } var typeChar = reader.ReadChar(); if (typeChar == 'S') { var s = reader.ReadString(); return new Language.Lua.LuaString(s); } else if (typeChar == 'N') { var n = reader.ReadDouble(); return new Language.Lua.LuaNumber(n); } else if (typeChar == 'B') { var b = reader.ReadBoolean(); return (b == true) ? Language.Lua.LuaBoolean.True : Language.Lua.LuaBoolean.False; } else if (typeChar == 'X') { return Language.Lua.LuaNil.Nil; } else { Debug.LogError("ReadValue unhandled type code " + typeChar); return Language.Lua.LuaNil.Nil; } } private static void ReadTable(BinaryReader reader, Language.Lua.LuaTable table) { reader.Read(); // 'T' int listLength = reader.ReadInt32(); for (int i = 0; i < listLength; i++) { var value = ReadValue(reader); table.List.Add(value); } int dictLength = reader.ReadInt32(); for (int i = 0; i < dictLength; i++) { var key = ReadValue(reader); var value = ReadValue(reader); table.Dict.Add(key, value); } } // Save relationship & status tables, and custom save data delegate. private static void WriteExtraData(BinaryWriter writer) { if (includeRelationshipAndStatusData) { var sb = new StringBuilder(); AppendRelationshipAndStatusTables(sb); writer.Write(sb.ToString()); } if (GetCustomSaveData != null) { writer.Write(GetCustomSaveData()); } } private static void ApplyExtraData(BinaryReader reader) { if (includeRelationshipAndStatusData) { Lua.Run(reader.ReadString()); } if (GetCustomSaveData != null) { Lua.Run(reader.ReadString(), DialogueDebug.LogInfo); } } // If saveConversationSimStatusWithField or saveDialogueEntrySimStatusWithField are set, // copy the current SimStatus into the specified variables/fields. private static void PrepSimStatusForRawData(Language.Lua.LuaTable conversationTable) { // Only need to do if saving to fields: if (!(includeSimStatus && DialogueManager.Instance.includeSimStatus && conversationTable != null)) return; useConversationID = string.IsNullOrEmpty(saveConversationSimStatusWithField); useEntryID = string.IsNullOrEmpty(saveDialogueEntrySimStatusWithField); if (useConversationID && useEntryID) return; // Reuse these vars to reduce GC: var dialogueEntryCache = new Dictionary(); var sb = new StringBuilder(16384, System.Int32.MaxValue); for (int i = 0; i < conversationTable.List.Count; i++) { var conversationID = i + 1; var fieldTable = conversationTable.List[i] as Language.Lua.LuaTable; PrepConversationSimStatusForRawData(conversationTable, conversationID, fieldTable, dialogueEntryCache, sb); } foreach (var kvp in conversationTable.Dict) { if (kvp.Key == null || kvp.Value == null || !(kvp.Value is Language.Lua.LuaTable)) continue; var conversationID = Tools.StringToInt(kvp.Key.ToString()); var fieldTable = kvp.Value as Language.Lua.LuaTable; PrepConversationSimStatusForRawData(conversationTable, conversationID, fieldTable, dialogueEntryCache, sb); } } // Async version: private static IEnumerator PrepSimStatusForRawDataAsync(Language.Lua.LuaTable conversationTable) { // Only need to do if saving to fields: if (!(includeSimStatus && DialogueManager.Instance.includeSimStatus && conversationTable != null)) yield break; useConversationID = string.IsNullOrEmpty(saveConversationSimStatusWithField); useEntryID = string.IsNullOrEmpty(saveDialogueEntrySimStatusWithField); if (useConversationID && useEntryID) yield break; // Reuse these vars to reduce GC: var dialogueEntryCache = new Dictionary(); var sb = new StringBuilder(16384, System.Int32.MaxValue); int numEntriesDone = 0; for (int i = 0; i < conversationTable.List.Count; i++) { var conversationID = i + 1; var fieldTable = conversationTable.List[i] as Language.Lua.LuaTable; numEntriesDone += PrepConversationSimStatusForRawData(conversationTable, conversationID, fieldTable, dialogueEntryCache, sb); if (numEntriesDone >= asyncDialogueEntryBatchSize) { numEntriesDone = 0; yield return null; } } foreach (var kvp in conversationTable.Dict) { if (kvp.Key == null || kvp.Value == null || !(kvp.Value is Language.Lua.LuaTable)) continue; var conversationID = Tools.StringToInt(kvp.Key.ToString()); var fieldTable = kvp.Value as Language.Lua.LuaTable; numEntriesDone += PrepConversationSimStatusForRawData(conversationTable, conversationID, fieldTable, dialogueEntryCache, sb); if (numEntriesDone >= asyncDialogueEntryBatchSize) { numEntriesDone = 0; yield return null; } } } private static int PrepConversationSimStatusForRawData(Language.Lua.LuaTable conversationTable, int conversationID, Language.Lua.LuaTable fieldTable, Dictionary dialogueEntryCache, StringBuilder sb) { if (conversationTable == null || fieldTable == null) return 0; var dialogTable = fieldTable.GetValue("Dialog") as Language.Lua.LuaTable; if (dialogTable == null) return 0; var conversation = DialogueManager.MasterDatabase.GetConversation(conversationID); if (conversation == null) return 0; sb.Length = 0; // Index dialogue entries by ID: (don't worry about unused old entries; this conversation shouldn't reference them) DialogueEntry entry; for (int i = 0; i < conversation.dialogueEntries.Count; i++) { entry = conversation.dialogueEntries[i]; dialogueEntryCache[entry.id] = entry; } var first = true; // Handle Dialog table's List: for (int i = 0; i < dialogTable.List.Count; i++) { var entryID = i + 1; var simStatusTable = dialogTable.List[i] as Language.Lua.LuaTable; if (!first) sb.Append(";"); first = false; if (!useEntryID && dialogueEntryCache.TryGetValue(entryID, out entry)) { sb.Append(Field.LookupValue(entry.fields, saveDialogueEntrySimStatusWithField)); } else { sb.Append(entryID); } sb.Append(";"); //--- Optimization since we know table only has one field. Was: var simStatus = simStatusTable.GetValue(DialogueLua.SimStatus).ToString(); var enumerator = simStatusTable.Dict.GetEnumerator(); enumerator.MoveNext(); var simStatus = enumerator.Current.Value.ToString(); sb.Append(SimStatusToChar(simStatus)); } // Handle Dialog table's Dict: foreach (var kvp2 in dialogTable.KeyValuePairs) { var simStatusTable = kvp2.Value as Language.Lua.LuaTable; if (!first) sb.Append(";"); first = false; if (!useEntryID) { var entryID = (kvp2.Key is Language.Lua.LuaNumber) ? (int)((kvp2.Key as Language.Lua.LuaNumber).Number) : Tools.StringToInt(kvp2.Key.ToString()); if (dialogueEntryCache.TryGetValue(entryID, out entry)) { sb.Append(Field.LookupValue(entry.fields, saveDialogueEntrySimStatusWithField)); } else { sb.Append(entryID); } } else { sb.Append(kvp2.Key.ToString()); } sb.Append(";"); //--- Optimization since we know table only has one field. Was: var simStatus = simStatusTable.GetValue(DialogueLua.SimStatus).ToString(); var enumerator = simStatusTable.Dict.GetEnumerator(); enumerator.MoveNext(); var simStatus = enumerator.Current.Value.ToString(); sb.Append(SimStatusToChar(simStatus)); } if (useConversationID) { Lua.Run("Conversation[" + conversationID + "].SimX=\"" + sb.ToString() + "\""); } else { var fieldName = DialogueLua.StringToTableIndex(conversation.LookupValue(saveConversationSimStatusWithField)); Lua.Run("Variable[\"Conversation_SimX_" + fieldName + "\"]=\"" + sb.ToString() + "\""); } return conversation.dialogueEntries.Count; } // If saveConversationSimStatusWithField or saveDialogueEntrySimStatusWithField are set, // repopoulate SimStatus from the values in the specified variables/fields. private static void ApplySimStatusFromRawData() { if (includeSimStatus && DialogueManager.Instance.includeSimStatus && (!string.IsNullOrEmpty(saveConversationSimStatusWithField) || !string.IsNullOrEmpty(saveDialogueEntrySimStatusWithField))) { ExpandCompressedSimStatusData(); } } #endif #endregion } }