// Copyright (c) Pixel Crushers. All rights reserved. using UnityEngine; using System.Collections.Generic; namespace PixelCrushers.DialogueSystem { /// /// A conversation asset. A conversation is a collection of dialogue entries (see /// DialogueEntry) that are linked together to form branching, interactive dialogue between two /// actors (see Actor). /// [System.Serializable] public class Conversation : Asset { /// /// Optional settings to override the Dialogue Manager's Display Settings. /// public ConversationOverrideDisplaySettings overrideSettings = new ConversationOverrideDisplaySettings(); /// /// Currently unused by the dialogue system, this is the nodeColor value defined in Chat /// Mapper. /// public string nodeColor = null; /// /// The dialogue entries in the conversation. /// public List dialogueEntries = new List(); public List entryGroups = new List(); /// /// Conversation's scroll position in Dialogue Editor window canvas. /// [HideInInspector] public Vector2 canvasScrollPosition = Vector2.zero; /// /// Conversation's zoom level in Dialogue Editor window. /// [HideInInspector] public float canvasZoom = 1; /// /// Gets or sets the Title field. /// /// /// The title of the conversation, most often used to look up and start a specific /// conversation. /// public string Title { get { return LookupValue(DialogueSystemFields.Title); } set { Field.SetValue(fields, DialogueSystemFields.Title, value); } } /// /// Gets or sets the Actor ID. The actor is the primary participant in the conversation. /// /// /// The actor ID. /// public int ActorID { get { return LookupInt(DialogueSystemFields.Actor); } set { Field.SetValue(fields, DialogueSystemFields.Actor, value.ToString(), FieldType.Actor); } } /// /// Gets or sets the Conversant ID. The conversant is the other participant in the /// conversation. /// /// /// The conversant ID. /// public int ConversantID { get { return LookupInt(DialogueSystemFields.Conversant); } set { Field.SetValue(fields, DialogueSystemFields.Conversant, value.ToString(), FieldType.Actor); } } /// /// Initializes a new Conversation. /// public Conversation() { } public Conversation(Conversation sourceConversation) : base(sourceConversation as Asset) { this.nodeColor = sourceConversation.nodeColor; this.overrideSettings = sourceConversation.overrideSettings; this.dialogueEntries = CopyDialogueEntries(sourceConversation.dialogueEntries); this.entryGroups = CopyEntryGroups(sourceConversation.entryGroups); } /// /// Initializes a new Conversation copied from a Chat Mapper conversation. /// /// /// The Chat Mapper conversation. /// public Conversation(ChatMapper.Conversation chatMapperConversation, bool putEndSequenceOnLastSplit = true) { Assign(chatMapperConversation, putEndSequenceOnLastSplit); } /// /// Copies a Chat Mapper conversation. /// /// /// The Chat Mapper conversation. /// public void Assign(ChatMapper.Conversation chatMapperConversation, bool putEndSequenceOnLastSplit = true) { if (chatMapperConversation != null) { Assign(chatMapperConversation.ID, chatMapperConversation.Fields); nodeColor = chatMapperConversation.NodeColor; foreach (var chatMapperEntry in chatMapperConversation.DialogEntries) { AddConversationDialogueEntry(chatMapperEntry); } SplitPipesIntoEntries(putEndSequenceOnLastSplit); // Set priority of links to the destination entry's priority: foreach (var entry in dialogueEntries) { foreach (var link in entry.outgoingLinks) { if (link.destinationConversationID != id) continue; var dest = GetDialogueEntry(link.destinationDialogueID); if (dest == null) continue; link.priority = dest.conditionPriority; } } } } /// /// Adds the conversation dialogue entry. Starting in Chat Mapper 1.6, XML entries don't /// include the conversation ID, so we set it manually here. /// /// /// Chat Mapper entry. /// private void AddConversationDialogueEntry(ChatMapper.DialogEntry chatMapperEntry) { var entry = new DialogueEntry(chatMapperEntry); entry.conversationID = id; dialogueEntries.Add(entry); } /// /// Looks up a dialogue entry by title. /// /// /// The dialogue entry whose title matches, or null if no such entry exists. /// /// /// The title of the dialogue entry. /// public DialogueEntry GetDialogueEntry(string title) { return dialogueEntries.Find(e => string.Equals(e.Title, title)); } /// /// Looks up a dialogue entry by its ID. /// /// /// The dialogue entry whose Id matches, or null if no such entry exists. /// /// /// The dialogue entry ID. /// public DialogueEntry GetDialogueEntry(int dialogueEntryID) { return dialogueEntries.Find(e => e.id == dialogueEntryID); } /// /// Looks up the first dialogue entry in the conversation, defined (as in Chat Mapper) as /// the entry titled START. /// /// /// The first dialogue entry in the conversation. /// public DialogueEntry GetFirstDialogueEntry() { return dialogueEntries.Find(e => string.Equals(e.Title, "START")); } /// /// Processes all dialogue entries, splitting entries containing pipe characters ("|") /// into multiple entries. /// /// /// Put sequencer commands with end keyword on the last split entry, other commands on the /// first entry, and use default delay for middle entries. /// /// Trim whitespace such as newlines. /// If specified, add "-1", "-2", etc., to this field. public void SplitPipesIntoEntries(bool putEndSequenceOnLastSplit = true, bool trimWhitespace = false, string uniqueFieldTitle = null) { if (dialogueEntries != null) { var count = dialogueEntries.Count; for (int entryIndex = 0; entryIndex < count; entryIndex++) { var dialogueText = dialogueEntries[entryIndex].DialogueText; if (!string.IsNullOrEmpty(dialogueText)) { if (dialogueText.Contains("|")) { SplitEntryAtPipes(entryIndex, dialogueText, putEndSequenceOnLastSplit, trimWhitespace, uniqueFieldTitle); } } } } } private void SplitEntryAtPipes(int originalEntryIndex, string dialogueText, bool putEndSequenceOnLastSplit, bool trimWhitespace, string uniqueFieldTitle = null) { // Split by Dialogue Text: var substrings = dialogueText.Split(new char[] { '|' }); var originalEntry = dialogueEntries[originalEntryIndex]; originalEntry.DialogueText = trimWhitespace ? substrings[0].Trim() : substrings[0]; var originalOutgoingLinks = originalEntry.outgoingLinks; ConditionPriority priority = ((originalOutgoingLinks != null) && (originalOutgoingLinks.Count > 0)) ? originalOutgoingLinks[0].priority : ConditionPriority.Normal; var currentEntry = originalEntry; var entries = new List(); entries.Add(currentEntry); // Split Menu Text: var defaultMenuText = (originalEntry != null && originalEntry.MenuText != null) ? originalEntry.MenuText : string.Empty; var menuTextSubstrings = defaultMenuText.Split(new char[] { '|' }); // Split Audio Files: var audioFilesText = originalEntry.AudioFiles; audioFilesText = ((audioFilesText != null) && (audioFilesText.Length >= 2)) ? audioFilesText.Substring(1, audioFilesText.Length - 2) : string.Empty; var audioFiles = audioFilesText.Split(new char[] { ';' }); currentEntry.AudioFiles = string.Format("[{0}]", new System.Object[] { (audioFiles.Length > 0) ? audioFiles[0] : string.Empty }); // Prep for adding -1, -2, etc., to unique field value: var updateUniqueField = !string.IsNullOrEmpty(uniqueFieldTitle); var uniqueFieldValue = updateUniqueField ? Field.LookupValue(currentEntry.fields, uniqueFieldTitle) : string.Empty; // Create new dialogue entries for the split parts: int i = 1; while (i < substrings.Length) { var newEntryDialogueText = substrings[i]; // Don't add blank entry at end if original text ends with pipe: if (string.IsNullOrEmpty(substrings[i]) && i == substrings.Length - 1) { i++; continue; } var newEntryMenuText = (i < menuTextSubstrings.Length) ? menuTextSubstrings[i] : string.Empty; if (trimWhitespace) { newEntryDialogueText = newEntryDialogueText.Trim(); newEntryMenuText = newEntryMenuText.Trim(); } var newEntry = AddNewDialogueEntry(originalEntry, newEntryDialogueText, i, trimWhitespace); newEntry.canvasRect = new Rect(originalEntry.canvasRect.x + i * 20, originalEntry.canvasRect.y + i * 10, originalEntry.canvasRect.width, originalEntry.canvasRect.height); newEntry.currentMenuText = newEntryMenuText; newEntry.AudioFiles = string.Format("[{0}]", new System.Object[] { (i < audioFiles.Length) ? audioFiles[i] : string.Empty }); if (updateUniqueField) { Field.SetValue(newEntry.fields, uniqueFieldTitle, $"{uniqueFieldValue}-{i}"); } currentEntry.outgoingLinks = new List() { NewLink(currentEntry, newEntry, priority) }; currentEntry = newEntry; entries.Add(newEntry); i++; } // Set the last entry's links to the original outgoing links: currentEntry.outgoingLinks = originalOutgoingLinks; // Fix up the other splittable fields in the original entry: foreach (var field in originalEntry.fields) { if (string.IsNullOrEmpty(field.title)) continue; string fieldValue = (field.value != null) ? field.value : string.Empty; bool isSequence = field.title.StartsWith(DialogueSystemFields.Sequence); bool isLocalization = (field.type == FieldType.Localization); bool containsPipes = fieldValue.Contains("|"); bool isSplittable = (isSequence || isLocalization) && !string.IsNullOrEmpty(field.value) && containsPipes; if (isSplittable) { substrings = field.value.Split(new char[] { '|' }); if (substrings.Length > 1) { fieldValue = trimWhitespace ? substrings[0].Trim() : substrings[0]; field.value = fieldValue; } } else if (isSequence && putEndSequenceOnLastSplit && !containsPipes) { if (!string.IsNullOrEmpty(field.value) && field.value.Contains(SequencerKeywords.End)) { PutEndSequenceOnLastSplit(entries, field); } } } } private void PutEndSequenceOnLastSplit(List entries, Field field) { var commands = field.value.Split(new char[] { ';' }); for (int entryNum = 0; entryNum < entries.Count; entryNum++) { var entry = entries[entryNum]; var entryField = Field.Lookup(entry.fields, field.title); entryField.value = string.Empty; if (entryNum == 0) { foreach (var command in commands) { if (!command.Contains(SequencerKeywords.End)) { entryField.value += command.Trim() + "; "; } } entryField.value += SequencerKeywords.DelayEndCommand; } else if (entryNum == (entries.Count - 1)) { foreach (var command in commands) { if (command.Contains(SequencerKeywords.End)) { entryField.value += command.Trim() + "; "; } } } else { entryField.value = SequencerKeywords.DelayEndCommand; } } } private DialogueEntry AddNewDialogueEntry(DialogueEntry originalEntry, string dialogueText, int partNum, bool trimWhitespace) { var newEntry = new DialogueEntry(); newEntry.id = GetHighestDialogueEntryID() + 1; newEntry.conversationID = originalEntry.conversationID; newEntry.isRoot = originalEntry.isRoot; newEntry.isGroup = originalEntry.isGroup; newEntry.nodeColor = originalEntry.nodeColor; newEntry.delaySimStatus = originalEntry.delaySimStatus; newEntry.falseConditionAction = originalEntry.falseConditionAction; newEntry.conditionsString = string.Equals(originalEntry.falseConditionAction, "Passthrough") ? originalEntry.conditionsString : string.Empty; newEntry.userScript = string.Empty; newEntry.fields = new List(); foreach (var field in originalEntry.fields) { if (string.IsNullOrEmpty(field.title)) continue; string fieldValue = field.value; bool isSplittable = (field.title.StartsWith(DialogueSystemFields.Sequence) || (field.type == FieldType.Localization)) && !string.IsNullOrEmpty(field.value) && field.value.Contains("|"); if (isSplittable) { string[] substrings = field.value.Split(new char[] { '|' }); if (partNum < substrings.Length) { fieldValue = trimWhitespace ? substrings[partNum].Trim() : substrings[partNum].Trim(); } } newEntry.fields.Add(new Field(field.title, fieldValue, field.type)); } newEntry.DialogueText = dialogueText; dialogueEntries.Add(newEntry); return newEntry; } private int GetHighestDialogueEntryID() { int highest = 0; foreach (var entry in dialogueEntries) { highest = Mathf.Max(entry.id, highest); } return highest; } private Link NewLink(DialogueEntry origin, DialogueEntry destination, ConditionPriority priority = ConditionPriority.Normal) { var newLink = new Link(); newLink.originConversationID = origin.conversationID; newLink.originDialogueID = origin.id; newLink.destinationConversationID = destination.conversationID; newLink.destinationDialogueID = destination.id; newLink.isConnector = (origin.conversationID != destination.conversationID); newLink.priority = priority; return newLink; } private List CopyDialogueEntries(List sourceEntries) { var entries = new List(); foreach (var sourceEntry in sourceEntries) { entries.Add(new DialogueEntry(sourceEntry)); } return entries; } private List CopyEntryGroups(List sourceGroups) { var groups = new List(); foreach (var group in sourceGroups) { groups.Add(new EntryGroup(group)); } return groups; } } }