// 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
}
}