#if USE_ARTICY // Copyright (c) Pixel Crushers. All rights reserved. using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using UnityEngine; namespace PixelCrushers.DialogueSystem.Articy { /// /// This class does the actual work of converting ArticyData (version-independent /// articy:draft data) into a dialogue database. /// public class ArticyConverter { #region Public Static Utility Methods public delegate void ProgressCallbackDelegate(string info, float progress); public static event ProgressCallbackDelegate onProgressCallback = delegate { }; /// /// Creates a new database from an articy:draft XML file. /// /// Articy XML data (i.e., the contents of an articy:draft XML export). /// Optional ConverterPrefs to use. Does not use prefs.ProjectFilename. /// Optional template for dialogue database assets. /// public static DialogueDatabase ConvertXmlDataToDatabase(string xmlData, ConverterPrefs prefs = null, Template template = null) { if (prefs == null) prefs = new ConverterPrefs(); if (template == null) template = new Template(); var database = DatabaseUtility.CreateDialogueDatabaseInstance(); var articyData = ArticySchemaTools.LoadArticyDataFromXmlData(xmlData, prefs); if (articyData == null) { if (DialogueDebug.logWarnings) Debug.LogWarning("Dialogue System: Can't convert articy:draft project; unable to import articy:draft data."); return null; } ConvertArticyDataToDatabase(articyData, prefs, template, database); return database; } /// /// This static utility method creates a converter and uses it to run the conversion. /// /// /// Articy data. /// /// /// Prefs. /// /// /// Dialogue database. /// public static void ConvertArticyDataToDatabase(ArticyData articyData, ConverterPrefs prefs, Template template, DialogueDatabase database) { ArticyConverter converter = new ArticyConverter(); converter.Convert(articyData, prefs, template, database); } #endregion #region Variables protected const string ArticyIdFieldTitle = "Articy Id"; protected const string ArticyTechnicalNameFieldTitle = "Technical Name"; protected const string DestinationArticyIdFieldTitle = "destinationArticyId"; protected const int StartEntryID = 0; protected ArticyData articyData; protected ConverterPrefs prefs; protected DialogueDatabase database; protected Template template; protected int conversationID; protected int actorID; protected int itemID; protected int locationID; protected static List fullVariableNames = new List(); // Make static to expose ConvertExpression(). protected HashSet otherScriptFieldTitles = new HashSet(); protected List documentConversations = new List(); #endregion #region Stacks protected List flowFragmentNameStack = new List(); protected List conversationStack = new List(); protected Dictionary conversationLastEntryID = new Dictionary(); protected Dictionary> entriesByArticyId = new Dictionary>(); protected Dictionary entriesByPinID = new Dictionary(); protected Dictionary jumpsToProcess = new Dictionary(); protected List unusedOutputEntries = new List(); protected virtual void ResetStacks() { flowFragmentNameStack.Clear(); conversationStack.Clear(); conversationLastEntryID.Clear(); entriesByPinID.Clear(); jumpsToProcess.Clear(); unusedOutputEntries.Clear(); } protected virtual void PushFlowFragment(ArticyData.FlowFragment flowFragment) { if (flowFragment == null) return; flowFragmentNameStack.Add(flowFragment.displayName.DefaultText); } protected virtual void PopFlowFragment() { if (flowFragmentNameStack.Count < 1) return; flowFragmentNameStack.RemoveAt(flowFragmentNameStack.Count - 1); } protected virtual void PushConversation(Conversation conversation) { if (conversation == null) return; conversationStack.Add(conversation); } protected virtual void PopConversation() { if (conversationStack.Count < 1) return; conversationStack.RemoveAt(conversationStack.Count - 1); } protected virtual Conversation GetConversationStackTop() { return (conversationStack.Count > 0) ? conversationStack[conversationStack.Count - 1] : null; } protected virtual int GetNextConversationEntryID(Conversation conversation) { if (conversation == null) return 0; if (!conversationLastEntryID.ContainsKey(conversation)) { conversationLastEntryID.Add(conversation, 0); return 0; } else { conversationLastEntryID[conversation]++; return conversationLastEntryID[conversation]; } } protected virtual void ResetArticyIdIndex() { entriesByArticyId.Clear(); } protected virtual void IndexDialogueEntryByArticyId(DialogueEntry entry, string articyId) { if (entriesByArticyId.ContainsKey(articyId)) { if (!entriesByArticyId[articyId].Contains(entry)) { entriesByArticyId[articyId].Add(entry); } } else { entriesByArticyId.Add(articyId, new List()); entriesByArticyId[articyId].Add(entry); } } #endregion #region Top Level Conversion Methods /// /// Convert the ArticyData, using the preferences in Prefs, into a dialogue database. /// /// Articy data. /// Prefs. /// Dialogue database. public virtual void Convert(ArticyData articyData, ConverterPrefs prefs, Template template, DialogueDatabase database) { if (articyData != null) { onProgressCallback("Converting non-dialogue elements", 0.01f); Setup(articyData, prefs, template, database); ConvertProjectAttributes(); ConvertVariables(); ConvertEntities(); ConvertLocations(); ConvertFlowFragmentsToQuests(); ConvertDialogues(); ResetArticyIdIndex(); ConvertEmVarSet(); if (!prefs.ImportDocuments) DeleteDocumentConversations(); } } /// /// Sets up the conversion process. /// /// Articy data. /// Prefs. /// Dialogue database. protected virtual void Setup(ArticyData articyData, ConverterPrefs prefs, Template template, DialogueDatabase database) { this.articyData = articyData; this.prefs = prefs; this.database = database; database.actors = new List(); database.items = new List(); database.locations = new List(); database.variables = new List(); database.conversations = new List(); conversationID = 0; actorID = 0; itemID = 0; locationID = 0; //documentConversation = null; //lastDocumentEntry = null; fullVariableNames.Clear(); otherScriptFieldTitles.Clear(); documentConversations.Clear(); foreach (var otherScriptFieldTitle in prefs.OtherScriptFields.Split(';')) { otherScriptFieldTitles.Add(otherScriptFieldTitle.Trim()); } ResetArticyIdIndex(); this.template = template; } protected virtual void ConvertProjectAttributes() { database.version = articyData.ProjectVersion; database.author = articyData.ProjectAuthor; } #endregion #region Non-Dialogue Conversion /// /// Converts articy entities into Dialogue System actors and items/quests. /// protected virtual void ConvertEntities() { foreach (ArticyData.Entity articyEntity in articyData.entities.Values) { ConversionSetting conversionSetting = prefs.ConversionSettings.GetConversionSetting(articyEntity.id); if (conversionSetting.Include) { var category = conversionSetting.Category; if (HasField(articyEntity.features, "IsNPC", false)) category = EntityCategory.NPC; if (HasField(articyEntity.features, "IsPlayer", true)) category = EntityCategory.Player; if (HasField(articyEntity.features, "IsItem", true)) category = EntityCategory.Item; if (HasField(articyEntity.features, "IsQuest", true)) category = EntityCategory.Quest; switch (category) { case EntityCategory.NPC: case EntityCategory.Player: actorID++; bool isPlayer = (conversionSetting.Category == EntityCategory.Player); Actor actor = template.CreateActor(actorID, articyEntity.displayName.DefaultText, isPlayer); Field.SetValue(actor.fields, ArticyIdFieldTitle, articyEntity.id, FieldType.Text); Field.SetValue(actor.fields, ArticyTechnicalNameFieldTitle, articyEntity.technicalName, FieldType.Text); Field.SetValue(actor.fields, "Description", articyEntity.text.DefaultText, FieldType.Text); if (!string.IsNullOrEmpty(articyEntity.previewImage)) Field.SetValue(actor.fields, "Pictures", string.Format("[{0}]", articyEntity.previewImage), FieldType.Text); SetFeatureFields(actor.fields, articyEntity.features); ConvertLocalizableText(actor.fields, "Name", articyEntity.displayName); if (prefs.UseTechnicalNames) { Field.SetValue(actor.fields, "Name", articyEntity.technicalName, FieldType.Text); } if (prefs.UseTechnicalNames || prefs.SetDisplayName) { Field.SetValue(actor.fields, "Display Name", articyEntity.displayName.DefaultText, FieldType.Text); } if (prefs.CustomDisplayName) UseCustomDisplayName(actor.fields); database.actors.Add(actor); break; case EntityCategory.Item: case EntityCategory.Quest: itemID++; Item item = template.CreateItem(itemID, articyEntity.displayName.DefaultText); Field.SetValue(item.fields, ArticyIdFieldTitle, articyEntity.id, FieldType.Text); Field.SetValue(item.fields, ArticyTechnicalNameFieldTitle, articyEntity.technicalName, FieldType.Text); Field.SetValue(item.fields, "Description", articyEntity.text.DefaultText, FieldType.Text); Field.SetValue(item.fields, "Is Item", ((category == EntityCategory.Item) ? "True" : "False"), FieldType.Boolean); if (prefs.UseTechnicalNames) Field.SetValue(item.fields, "Display Name", articyEntity.displayName.DefaultText, FieldType.Text); SetFeatureFields(item.fields, articyEntity.features); ConvertLocalizableText(item.fields, "Name", articyEntity.displayName); if (prefs.UseTechnicalNames) { Field.SetValue(item.fields, "Name", articyEntity.technicalName, FieldType.Text); } if (prefs.UseTechnicalNames || prefs.SetDisplayName) { Field.SetValue(item.fields, "Display Name", articyEntity.displayName.DefaultText, FieldType.Text); } if (prefs.CustomDisplayName) UseCustomDisplayName(item.fields); database.items.Add(item); break; default: Debug.LogError("Dialogue System: Internal error converting entity type '" + conversionSetting.Category + "' (Articy ID: " + articyEntity.id + ")."); break; } } } foreach (var actor in database.actors) // Find actors' portraits. { FindPortraitTextureInResources(actor); } } /// /// Converts locations. /// protected virtual void ConvertLocations() { foreach (ArticyData.Location articyLocation in articyData.locations.Values) { if (prefs.ConversionSettings.GetConversionSetting(articyLocation.id).Include) { locationID++; Location location = template.CreateLocation(locationID, articyLocation.displayName.DefaultText); Field.SetValue(location.fields, ArticyIdFieldTitle, articyLocation.id, FieldType.Text); Field.SetValue(location.fields, ArticyTechnicalNameFieldTitle, articyLocation.technicalName, FieldType.Text); Field.SetValue(location.fields, "Description", articyLocation.text.DefaultText, FieldType.Text); if (prefs.UseTechnicalNames) Field.SetValue(location.fields, "Display Name", articyLocation.displayName.DefaultText, FieldType.Text); SetFeatureFields(location.fields, articyLocation.features); ConvertLocalizableText(location.fields, "Name", articyLocation.displayName); if (prefs.UseTechnicalNames) { Field.SetValue(location.fields, "Name", articyLocation.technicalName, FieldType.Text); Field.SetValue(location.fields, "Display Name", articyLocation.displayName.DefaultText, FieldType.Text); } if (prefs.CustomDisplayName) UseCustomDisplayName(location.fields); database.locations.Add(location); } } } /// /// Converts flow fragments into items. (The quest system uses the Item[] table.) /// This is only called if the flow fragment mode is set to Quests. /// protected virtual void ConvertFlowFragmentsToQuests() { if (prefs.FlowFragmentMode != ConverterPrefs.FlowFragmentModes.Quests) return; foreach (ArticyData.FlowFragment articyFlowFragment in articyData.flowFragments.Values) { if (prefs.ConversionSettings.GetConversionSetting(articyFlowFragment.id).Include) { itemID++; Item item = template.CreateItem(itemID, articyFlowFragment.displayName.DefaultText); Field.SetValue(item.fields, ArticyIdFieldTitle, articyFlowFragment.id, FieldType.Text); Field.SetValue(item.fields, ArticyTechnicalNameFieldTitle, articyFlowFragment.technicalName, FieldType.Text); Field.SetValue(item.fields, "Description", articyFlowFragment.text.DefaultText, FieldType.Text); Field.SetValue(item.fields, "Success Description", string.Empty, FieldType.Text); Field.SetValue(item.fields, "Failure Description", string.Empty, FieldType.Text); Field.SetValue(item.fields, "State", "unassigned", FieldType.Text); Field.SetValue(item.fields, "Is Item", "False", FieldType.Boolean); SetFeatureFields(item.fields, articyFlowFragment.features); ConvertLocalizableText(item.fields, "Name", articyFlowFragment.displayName); database.items.Add(item); } } } protected virtual void SetFeatureFields(List fields, ArticyData.Features features) { // Note: quest State and Entry_#_State fields are fixed up in the Articy_#_#_Tools class // for each schema. foreach (ArticyData.Feature feature in features.features) { foreach (ArticyData.Property property in feature.properties) { foreach (Field field in property.fields) { if (!string.IsNullOrEmpty(field.title)) { var fieldTitle = ConvertSpecialTechnicalNames(field.title); if (prefs.IncludeFeatureNameInFields && !IsSpecialFieldTitle(field.title)) { fieldTitle = $"{feature.name}.{fieldTitle}"; } var fieldValue = IsOtherScriptField(fieldTitle) ? ConvertExpression(field.value, false) : field.value; var existingField = Field.Lookup(fields, fieldTitle); if (existingField != null) { existingField.value = fieldValue; } else { fields.Add(new Field(fieldTitle, fieldValue, field.type)); } } } } } } protected virtual void UseCustomDisplayName(List fields) { // Look for a field named 'DisplayName'. If present, replace 'Display Name' field with it. var customField = Field.Lookup(fields, "DisplayName"); if (customField != null) { fields.RemoveAll(field => field.title == "Display Name"); customField.title = "Display Name"; } } protected virtual bool IsOtherScriptField(string fieldTitle) { return otherScriptFieldTitles.Contains(fieldTitle); } protected static List SpecialFieldTitles = new List(new string[] { DialogueSystemFields.Name, DialogueSystemFields.DisplayName, DialogueSystemFields.IsPlayer, DialogueSystemFields.CurrentPortrait, DialogueSystemFields.IsItem, DialogueSystemFields.Group, DialogueSystemFields.Description, DialogueSystemFields.SuccessDescription, DialogueSystemFields.FailureDescription, DialogueSystemFields.EntryCount, DialogueSystemFields.Title, DialogueSystemFields.Actor, DialogueSystemFields.Conversant, DialogueSystemFields.Priority, DialogueSystemFields.Sequence, DialogueSystemFields.ResponseMenuSequence, DialogueSystemFields.VoiceOverFile, DialogueSystemFields.DialogueText, DialogueSystemFields.MenuText }); protected static List SpecialFieldTitleStarters = new List(new string[] { "Entry ", }); protected virtual bool IsSpecialFieldTitle(string fieldTitle) { if (SpecialFieldTitles.Find(x => x == fieldTitle) != null) return true; foreach (var starter in SpecialFieldTitleStarters) { if (fieldTitle.StartsWith(starter)) return true; } return false; } protected virtual string ConvertSpecialTechnicalNames(string technicalName) { if (string.Equals(technicalName, "Response_Menu_Sequence") || string.Equals(technicalName, "Success_Description") || string.Equals(technicalName, "Failure_Description") || string.Equals(technicalName, "Entry_Count") || Regex.Match(technicalName, @"^Entry_[0-9]").Success) { return technicalName.Replace("_", " "); } else { return technicalName; } } public static bool HasField(ArticyData.Features features, string fieldName, bool mustBeTrue) { foreach (ArticyData.Feature feature in features.features) { foreach (ArticyData.Property property in feature.properties) { foreach (Field field in property.fields) { if (string.Equals(field.title, fieldName)) { return mustBeTrue ? string.Equals(field.value, "True", System.StringComparison.OrdinalIgnoreCase) : true; } } } } return false; } /// /// Converts articy variable sets and variables into Dialogue System variables. /// protected virtual void ConvertVariables() { int variableID = 0; foreach (ArticyData.VariableSet articyVariableSet in articyData.variableSets.Values) { foreach (ArticyData.Variable articyVariable in articyVariableSet.variables) { string fullName = ArticyData.FullVariableName(articyVariableSet, articyVariable); fullVariableNames.Add(fullName); if (prefs.ConversionSettings.GetConversionSetting(fullName).Include) { variableID++; Variable variable = template.CreateVariable(variableID, fullName, articyVariable.defaultValue); variable.Type = (articyVariable.dataType == ArticyData.VariableDataType.Boolean) ? FieldType.Boolean : ((articyVariable.dataType == ArticyData.VariableDataType.Integer) ? FieldType.Number : FieldType.Text); if (!string.IsNullOrEmpty(articyVariable.description)) { var descriptionField = Field.Lookup(variable.fields, "Description"); if (descriptionField != null) { descriptionField.value = articyVariable.description; } else { variable.fields.Add(new Field("Description", articyVariable.description, FieldType.Text)); } } database.variables.Add(variable); } } } } #endregion #region Dialogue Conversion protected virtual void DeleteDocumentConversations() { database.conversations.RemoveAll(conversation => documentConversations.Contains(conversation)); } /// /// Converts dialogues using the articy project's hierarchy. /// protected virtual void ConvertDialogues() { ResetStacks(); onProgressCallback("Converting dialogues", 0.2f); ConvertDialoguesToConversations(); onProgressCallback("Processing hierarchy", 0.3f); ProcessHierarchy(); InsertDelayEvaluationNodesBeforeInputPins(); onProgressCallback("Sorting links by position", 0.7f); SortAllLinksByPosition(); if (prefs.SplitTextOnPipes) SplitPipesIntoEntries(); onProgressCallback("Converting VoiceOver properties", 0.9f); ConvertVoiceOverProperties(); } protected virtual bool IncludeDialogue(string dialogueId) { var setting = (prefs == null) ? null : prefs.ConversionSettings.GetConversionSetting(dialogueId); return (setting == null) || setting.Include; } protected virtual void ConvertDialoguesToConversations() { foreach (var articyDialogue in articyData.dialogues.Values) { if (!IncludeDialogue(articyDialogue.id)) continue; CreateNewConversation(articyDialogue); } } /// /// Creates a new Dialogue System conversation from an articy dialogue. This also adds the /// conversation's mandatory first dialogue entry, "START". /// /// The new conversation. /// Articy dialogue. protected virtual Conversation CreateNewConversation(ArticyData.Dialogue articyDialogue) { if (articyDialogue == null) return null; // Create conversation: conversationID++; var conversationTitle = string.Empty; conversationTitle += articyDialogue.displayName.DefaultText; if (articyDialogue.isDocument && !string.IsNullOrEmpty(prefs.DocumentsSubmenu)) { conversationTitle = prefs.DocumentsSubmenu + "/" + conversationTitle; } Conversation conversation = template.CreateConversation(conversationID, conversationTitle); Field.SetValue(conversation.fields, ArticyIdFieldTitle, articyDialogue.id, FieldType.Text); Field.SetValue(conversation.fields, "Description", articyDialogue.text.DefaultText, FieldType.Text); SetConversationOverrideProperties(conversation, articyDialogue.features); SetFeatureFields(conversation.fields, articyDialogue.features); conversation.ActorID = FindActorIdFromArticyDialogue(articyDialogue, 0, 1); conversation.ConversantID = FindActorIdFromArticyDialogue(articyDialogue, 1, 2); database.conversations.Add(conversation); if (articyDialogue.isDocument) documentConversations.Add(conversation); // Create START entry: DialogueEntry startEntry = template.CreateDialogueEntry(GetNextConversationEntryID(conversation), conversationID, "START"); startEntry.canvasRect = new Rect(articyDialogue.position.x, articyDialogue.position.y, DialogueEntry.CanvasRectWidth, DialogueEntry.CanvasRectHeight); SetDialogueEntryParticipants(startEntry, conversation.ActorID, conversation.ConversantID); Field.SetValue(startEntry.fields, ArticyIdFieldTitle, articyDialogue.id, FieldType.Text); IndexDialogueEntryByArticyId(startEntry, articyDialogue.id); //-- Pins are added to input and output entries instead: ConvertPinExpressionsToConditionsAndScripts(startEntry, articyDialogue.pins); startEntry.outgoingLinks = new List(); var conversationSequenceField = Field.Lookup(conversation.fields, "Sequence"); if (conversationSequenceField != null && !string.IsNullOrEmpty(conversationSequenceField.value)) { conversation.fields.Remove(conversationSequenceField); Field.SetValue(startEntry.fields, "Sequence", conversationSequenceField.value, FieldType.Text); } else { Field.SetValue(startEntry.fields, "Sequence", "Continue()", FieldType.Text); } conversation.dialogueEntries.Add(startEntry); // Convert dialogue's in and out pins to [passthrough group] entries: for (int i = 0; i < articyDialogue.pins.Count; i++) { var pin = articyDialogue.pins[i]; if (string.IsNullOrEmpty(pin.expression)) continue; var isInputPin = pin.semantic == ArticyData.SemanticType.Input; var isOutputPin = pin.semantic == ArticyData.SemanticType.Output; if (isOutputPin && prefs.RecursionMode == ConverterPrefs.RecursionModes.Off) continue; var entryID = GetNextConversationEntryID(conversation); var title = isInputPin ? "input" : "output"; var entry = template.CreateDialogueEntry(entryID, conversationID, title); entry.canvasRect = new Rect(articyDialogue.position.x, articyDialogue.position.y, DialogueEntry.CanvasRectWidth, DialogueEntry.CanvasRectHeight); SetDialogueEntryParticipants(entry, conversation.ConversantID, conversation.ActorID); ConvertPinExpressionsToConditionsAndScripts(entry, articyDialogue.pins, isInputPin, !isInputPin); entry.isGroup = true; //Field.SetValue(entry.fields, "Sequence", "Continue()", FieldType.Text); Field.SetValue(entry.fields, ArticyIdFieldTitle, pin.id, FieldType.Text); if (isInputPin) { var link = new Link(); link.originConversationID = conversationID; link.originDialogueID = startEntry.id; link.destinationConversationID = conversationID; link.destinationDialogueID = entry.id; startEntry.outgoingLinks.Add(link); } else { unusedOutputEntries.Add(entry); } IndexDialogueEntryByArticyId(entry, pin.id); entry.outgoingLinks = new List(); conversation.dialogueEntries.Add(entry); RecordPin(pin, entry); } return conversation; } // Extract special conversation override features and set conversation override settings: protected virtual void SetConversationOverrideProperties(Conversation conversation, ArticyData.Features features) { foreach (ArticyData.Feature feature in features.features) { foreach (ArticyData.Property property in feature.properties) { for (int i = property.fields.Count - 1; i >= 0; i--) { var field = property.fields[i]; var deleteField = true; switch (field.title) { case "ShowNPCSubtitlesDuringLine": conversation.overrideSettings.useOverrides = true; conversation.overrideSettings.overrideSubtitleSettings = true; conversation.overrideSettings.showNPCSubtitlesDuringLine = Tools.StringToBool(field.value); break; case "ShowNPCSubtitlesWithResponses": conversation.overrideSettings.useOverrides = true; conversation.overrideSettings.overrideSubtitleSettings = true; conversation.overrideSettings.showNPCSubtitlesWithResponses = Tools.StringToBool(field.value); break; case "ShowPCSubtitlesDuringLine": conversation.overrideSettings.useOverrides = true; conversation.overrideSettings.overrideSubtitleSettings = true; conversation.overrideSettings.showPCSubtitlesDuringLine = Tools.StringToBool(field.value); break; case "SkipPCSubtitleAfterResponseMenu": conversation.overrideSettings.useOverrides = true; conversation.overrideSettings.overrideSubtitleSettings = true; conversation.overrideSettings.skipPCSubtitleAfterResponseMenu = Tools.StringToBool(field.value); break; case "SubtitleCharsPerSecond": conversation.overrideSettings.useOverrides = true; conversation.overrideSettings.overrideSubtitleSettings = true; conversation.overrideSettings.subtitleCharsPerSecond = Tools.StringToFloat(field.value); break; case "MinSubtitleSeconds": conversation.overrideSettings.useOverrides = true; conversation.overrideSettings.overrideSubtitleSettings = true; conversation.overrideSettings.minSubtitleSeconds = Tools.StringToFloat(field.value); break; case "ContinueButton": conversation.overrideSettings.useOverrides = true; conversation.overrideSettings.overrideSubtitleSettings = true; conversation.overrideSettings.continueButton = StringToContinueButtonMode(field.value); break; //--- case "DefaultSequence": conversation.overrideSettings.useOverrides = true; conversation.overrideSettings.overrideSequenceSettings = true; conversation.overrideSettings.defaultSequence = field.value; break; case "DefaultPlayerSequence": conversation.overrideSettings.useOverrides = true; conversation.overrideSettings.overrideSequenceSettings = true; conversation.overrideSettings.defaultPlayerSequence = field.value; break; case "DefaultResponseMenuSequence": conversation.overrideSettings.useOverrides = true; conversation.overrideSettings.overrideSequenceSettings = true; conversation.overrideSettings.defaultResponseMenuSequence = field.value; break; //--- case "AlwaysForceResponseMenu": conversation.overrideSettings.useOverrides = true; conversation.overrideSettings.overrideInputSettings = true; conversation.overrideSettings.alwaysForceResponseMenu = Tools.StringToBool(field.value); break; case "IncludeInvalidEntries": conversation.overrideSettings.useOverrides = true; conversation.overrideSettings.overrideInputSettings = true; conversation.overrideSettings.includeInvalidEntries = Tools.StringToBool(field.value); break; case "ResponseTimeout": conversation.overrideSettings.useOverrides = true; conversation.overrideSettings.overrideInputSettings = true; conversation.overrideSettings.responseTimeout = Tools.StringToFloat(field.value); break; default: deleteField = false; break; } if (deleteField) { property.fields.RemoveAt(i); } } } } } protected virtual DisplaySettings.SubtitleSettings.ContinueButtonMode StringToContinueButtonMode(string value) { var enumValues = System.Enum.GetValues(typeof(DisplaySettings.SubtitleSettings.ContinueButtonMode)); for (int i = 0; i < enumValues.Length; i++) { var enumMode = (DisplaySettings.SubtitleSettings.ContinueButtonMode)i; if (string.Equals(value, enumMode.ToString(), System.StringComparison.OrdinalIgnoreCase)) { return enumMode; } } return DisplaySettings.SubtitleSettings.ContinueButtonMode.Never; } protected virtual void SetDialogueEntryParticipants(DialogueEntry startEntry, int actorID, int conversantID) { startEntry.ActorID = actorID; startEntry.ConversantID = conversantID; } protected virtual int GetDefaultActorID(Conversation conversation) { return (conversation != null) ? conversation.ActorID : (prefs.UseDefaultActorsIfNoneAssignedToDialogue ? 1 : -1); } protected virtual int GetDefaultConversantID(Conversation conversation) { return (conversation != null) ? conversation.ConversantID : (prefs.UseDefaultActorsIfNoneAssignedToDialogue ? 2 : -1); } /// /// Creates a new Dialogue System conversation from an articy flow fragment. This also adds the /// conversation's mandatory first dialogue entry, "START". /// /// The new conversation. /// Articy flow fragment. protected virtual Conversation FindOrCreateFlowFragmentConversation(ArticyData.FlowFragment articyFlowFragment, bool isTopLevel) { if (articyFlowFragment == null) return null; // Create conversation: conversationID++; var conversationTitle = articyFlowFragment.displayName.DefaultText + " Conversation"; Conversation conversation = template.CreateConversation(conversationID, conversationTitle); Field.SetValue(conversation.fields, ArticyIdFieldTitle, articyFlowFragment.id, FieldType.Text); Field.SetValue(conversation.fields, "Description", articyFlowFragment.text.DefaultText, FieldType.Text); SetFeatureFields(conversation.fields, articyFlowFragment.features); var parentConversation = GetConversationStackTop(); conversation.ActorID = GetDefaultActorID(parentConversation); conversation.ConversantID = GetDefaultConversantID(parentConversation); database.conversations.Add(conversation); // Create START entry: DialogueEntry startEntry = template.CreateDialogueEntry(GetNextConversationEntryID(conversation), conversationID, "START"); SetDialogueEntryParticipants(startEntry, conversation.ActorID, conversation.ConversantID); Field.SetValue(startEntry.fields, ArticyIdFieldTitle, articyFlowFragment.id, FieldType.Text); IndexDialogueEntryByArticyId(startEntry, articyFlowFragment.id); ConvertPinExpressionsToConditionsAndScripts(startEntry, articyFlowFragment.pins); startEntry.outgoingLinks = new List(); var conversationSequenceField = Field.Lookup(conversation.fields, "Sequence"); if (conversationSequenceField != null && !string.IsNullOrEmpty(conversationSequenceField.value)) { conversation.fields.Remove(conversationSequenceField); Field.SetValue(startEntry.fields, "Sequence", conversationSequenceField.value, FieldType.Text); } else { Field.SetValue(startEntry.fields, "Sequence", "Continue()", FieldType.Text); } conversation.dialogueEntries.Add(startEntry); // Convert dialogue's in and out pins to passthrough group entries: for (int i = 0; i < articyFlowFragment.pins.Count; i++) { var pin = articyFlowFragment.pins[i]; if (pin.semantic == ArticyData.SemanticType.Output && prefs.RecursionMode == ConverterPrefs.RecursionModes.Off) continue; var entryID = GetNextConversationEntryID(conversation); var title = (pin.semantic == ArticyData.SemanticType.Input) ? "input" : "output"; var entry = template.CreateDialogueEntry(entryID, conversationID, title); SetDialogueEntryParticipants(entry, conversation.ConversantID, conversation.ActorID); entry.isGroup = true; Field.SetValue(entry.fields, "Sequence", "Continue()", FieldType.Text); Field.SetValue(entry.fields, ArticyIdFieldTitle, pin.id, FieldType.Text); if (pin.semantic == ArticyData.SemanticType.Input) { var link = new Link(); link.originConversationID = conversationID; link.originDialogueID = startEntry.id; link.destinationConversationID = conversationID; link.destinationDialogueID = entry.id; startEntry.outgoingLinks.Add(link); } else if (!(isTopLevel && pin.semantic == ArticyData.SemanticType.Output)) { unusedOutputEntries.Add(entry); } IndexDialogueEntryByArticyId(entry, pin.id); ConvertPinExpressionsToConditionsAndScripts(entry, articyFlowFragment.pins); entry.outgoingLinks = new List(); conversation.dialogueEntries.Add(entry); RecordPin(pin, entry); } if (isTopLevel) { // Connect input pin entry to output pin entries: var inputEntry = conversation.dialogueEntries.Find(x => x.Title == "input"); if (inputEntry != null) { foreach (var outputEntry in conversation.dialogueEntries) { if (outputEntry.Title == "output") { var link = new Link(); link.originConversationID = conversationID; link.originDialogueID = inputEntry.id; link.destinationConversationID = conversationID; link.destinationDialogueID = outputEntry.id; inputEntry.outgoingLinks.Add(link); } } } } return conversation; } protected virtual void ProcessHierarchy() { onProgressCallback("Processing dialogue nodes", 0.4f); BuildDialogueEntriesFromNode(articyData.hierarchy.node, 0); onProgressCallback("Connecting dialogue nodes", 0.5f); ProcessConnections(); onProgressCallback("Checking if jumps are group nodes", 0.6f); CheckJumpsForGroupNodes(); } protected virtual void InsertDelayEvaluationNodesBeforeInputPins() { foreach (var conversation in database.conversations) { var numEntries = conversation.dialogueEntries.Count; for (int i = 1; i < numEntries; i++) { var parentEntry = conversation.dialogueEntries[i]; if (string.IsNullOrEmpty(parentEntry.userScript)) continue; foreach (var link in parentEntry.outgoingLinks) { var childEntry = conversation.GetDialogueEntry(link.destinationDialogueID); if (string.IsNullOrEmpty(childEntry.conditionsString)) continue; // Parent has script and child has conditions, so create a buffer entry between them to delay evaluation: var childArticyId = Field.LookupValue(childEntry.fields, ArticyIdFieldTitle); var bufferEntry = CreateNewDialogueEntry(conversation, "Delay Evaluation", childArticyId + "-1"); conversation.dialogueEntries.Add(bufferEntry); bufferEntry.Sequence = "Continue()"; bufferEntry.outgoingLinks = new List() { new Link(link) }; link.destinationDialogueID = bufferEntry.id; } } } } protected const int MaxRecursionDepth = 1000; /// /// Processes a node in the hierarchy to build dialogue entries, /// also recursively processing the node's children. /// /// Node to process. protected virtual void BuildDialogueEntriesFromNode(ArticyData.Node node, int recursionDepth) { if (recursionDepth > MaxRecursionDepth) { Debug.LogError("Dialogue System: Internal error - Exceeded max recursion depth " + MaxRecursionDepth + " in ArticyConverter.BuildDialogueEntriesFromNode."); return; } var addedTopLevelFlowConversation = false; if ((node.type == ArticyData.NodeType.Dialogue) && !IncludeDialogue(node.id)) return; switch (node.type) { case ArticyData.NodeType.FlowFragment: var flowFragment = LookupArticyFlowFragment(node.id); PushFlowFragment(flowFragment); if (GetConversationStackTop() != null) { // The stack has a conversation, so push a nested conversation: if (prefs.FlowFragmentMode == ConverterPrefs.FlowFragmentModes.NestedConversationGroups && articyData.flowFragments.ContainsKey(node.id)) { var flowFragmentConversation = FindOrCreateFlowFragmentConversation(articyData.flowFragments[node.id], false); if (flowFragmentConversation != null) { PushConversation(flowFragmentConversation); PrependFlowStackToConversationTitle(flowFragmentConversation); } } else { AddFlowFragmentAsDialogueEntry(GetConversationStackTop(), flowFragment); } } else if (prefs.CreateConversationsForLooseFlow) { // Otherwise, make this flow fragment a top-level conversation: var flowFragmentConversation = FindOrCreateFlowFragmentConversation(articyData.flowFragments[node.id], true); if (flowFragmentConversation != null) { PushConversation(flowFragmentConversation); PrependFlowStackToConversationTitle(flowFragmentConversation); addedTopLevelFlowConversation = true; } } break; case ArticyData.NodeType.Dialogue: var conversation = database.conversations.Find(x => string.Equals(x.LookupValue(ArticyIdFieldTitle), node.id)); PushConversation(conversation); PrependFlowStackToConversationTitle(conversation); break; case ArticyData.NodeType.DialogueFragment: BuildDialogueEntryFromDialogueFragment(GetConversationStackTop(), LookupArticyDialogueFragment(node.id)); break; case ArticyData.NodeType.Hub: BuildDialogueEntryFromHub(GetConversationStackTop(), LookupArticyHub(node.id)); break; case ArticyData.NodeType.Jump: BuildDialogueEntryFromJump(GetConversationStackTop(), LookupArticyJump(node.id)); break; case ArticyData.NodeType.Condition: BuildDialogueEntriesFromCondition(GetConversationStackTop(), LookupArticyCondition(node.id)); break; case ArticyData.NodeType.Instruction: BuildDialogueEntryFromInstruction(GetConversationStackTop(), LookupArticyInstruction(node.id)); break; } // Process child nodes: foreach (ArticyData.Node childNode in node.nodes) { BuildDialogueEntriesFromNode(childNode, recursionDepth + 1); } // Pop from stacks as we leave node: switch (node.type) { case ArticyData.NodeType.FlowFragment: if (!addedTopLevelFlowConversation) { PopFlowFragment(); if (prefs.FlowFragmentMode == ConverterPrefs.FlowFragmentModes.NestedConversationGroups) { var pushedFlowFragmentConversation = database.conversations.Find(x => string.Equals(x.LookupValue(ArticyIdFieldTitle), node.id)); if (pushedFlowFragmentConversation != null) PopConversation(); } } break; case ArticyData.NodeType.Dialogue: PopConversation(); break; } } protected virtual void PrependFlowStackToConversationTitle(Conversation conversation) { var isFlowFragmentModeConversationGroups = prefs.FlowFragmentMode == ConverterPrefs.FlowFragmentModes.ConversationGroups || prefs.FlowFragmentMode == ConverterPrefs.FlowFragmentModes.NestedConversationGroups; if (conversation == null || !isFlowFragmentModeConversationGroups || flowFragmentNameStack.Count <= 0) return; var s = string.Empty; foreach (var flowFragmentName in flowFragmentNameStack) { s += flowFragmentName + "/"; } conversation.Title = s + conversation.Title; } protected virtual void RecordPins(List pins, DialogueEntry entry) { if (pins == null) return; for (int i = 0; i < pins.Count; i++) { RecordPin(pins[i], entry); } } protected virtual void RecordPin(ArticyData.Pin pin, DialogueEntry entry) { if (pin == null || entry == null || entriesByPinID.ContainsKey(pin.id)) return; entriesByPinID.Add(pin.id, entry); Field.SetValue(entry.fields, (pin.semantic == ArticyData.SemanticType.Input) ? "InputId" : "OutputId", pin.id); } protected virtual void ProcessConnections() { // Process regular connections: foreach (var kvp in articyData.connections) { ProcessConnectionNew(kvp.Value); } // Process jumps: foreach (var kvp in jumpsToProcess) { ProcessJumpConnection(kvp.Key, kvp.Value); } // Remove unused output entries: RemoveUnusedOutputEntries(); } protected virtual void ProcessConnectionNew(ArticyData.Connection connection) { if (connection == null) return; DialogueEntry sourceEntry, targetEntry; // If connection is from dialogue, connect from node: var dialogue = LookupArticyDialogue(connection.source.idRef); if (dialogue != null) { var conversation = database.conversations.Find(x => string.Equals(x.LookupValue(ArticyIdFieldTitle), connection.source.idRef)); if (conversation == null) return; sourceEntry = conversation.GetFirstDialogueEntry(); } // Otherwise connect from source entry: else { if (!entriesByPinID.ContainsKey(connection.source.pinRef)) { return; } sourceEntry = entriesByPinID[connection.source.pinRef]; } // Either way, connect to target: if (!entriesByPinID.ContainsKey(connection.target.pinRef)) { return; } targetEntry = entriesByPinID[connection.target.pinRef]; var linksToSelf = sourceEntry.conversationID == targetEntry.conversationID && sourceEntry.id == targetEntry.id; if (!linksToSelf) { var link = new Link(); link.originConversationID = sourceEntry.conversationID; link.originDialogueID = sourceEntry.id; link.destinationConversationID = targetEntry.conversationID; link.destinationDialogueID = targetEntry.id; link.isConnector = false; link.priority = ArticyData.ColorToPriority(connection.color); sourceEntry.outgoingLinks.Add(link); } MarkTargetUsed(targetEntry); } protected virtual void ProcessJumpConnection(ArticyData.Jump jump, DialogueEntry jumpEntry) { if (jump == null || jumpEntry == null || !entriesByPinID.ContainsKey(jump.target.pinRef)) return; var targetEntry = entriesByPinID[jump.target.pinRef]; Link link = new Link(); link.originConversationID = jumpEntry.conversationID; link.originDialogueID = jumpEntry.id; link.destinationConversationID = targetEntry.conversationID; link.destinationDialogueID = targetEntry.id; link.isConnector = false; jumpEntry.outgoingLinks.Add(link); MarkTargetUsed(targetEntry); } protected virtual void MarkTargetUsed(DialogueEntry targetEntry) { unusedOutputEntries.Remove(targetEntry); } protected virtual void RemoveUnusedOutputEntries() { for (int i = 0; i < unusedOutputEntries.Count; i++) { var entry = unusedOutputEntries[i]; var conversation = database.GetConversation(entry.conversationID); if (conversation == null) continue; conversation.dialogueEntries.Remove(entry); } } protected virtual ArticyData.Dialogue LookupArticyDialogue(string id) { return articyData.dialogues.ContainsKey(id) ? articyData.dialogues[id] : null; } protected virtual ArticyData.DialogueFragment LookupArticyDialogueFragment(string id) { return articyData.dialogueFragments.ContainsKey(id) ? articyData.dialogueFragments[id] : null; } protected virtual ArticyData.Hub LookupArticyHub(string id) { return articyData.hubs.ContainsKey(id) ? articyData.hubs[id] : null; } protected virtual ArticyData.Jump LookupArticyJump(string id) { return articyData.jumps.ContainsKey(id) ? articyData.jumps[id] : null; } protected virtual ArticyData.Condition LookupArticyCondition(string id) { return articyData.conditions.ContainsKey(id) ? articyData.conditions[id] : null; } protected virtual ArticyData.Instruction LookupArticyInstruction(string id) { return articyData.instructions.ContainsKey(id) ? articyData.instructions[id] : null; } protected virtual ArticyData.Connection LookupArticyConnection(string id) { return articyData.connections.ContainsKey(id) ? articyData.connections[id] : null; } protected virtual ArticyData.FlowFragment LookupArticyFlowFragment(string id) { return articyData.flowFragments.ContainsKey(id) ? articyData.flowFragments[id] : null; } /// /// Converts a dialogue fragment, including fields such as text, sequence, and pins, but doesn't /// connect it yet. /// /// Conversation. /// Fragment. protected virtual void BuildDialogueEntryFromDialogueFragment(Conversation conversation, ArticyData.DialogueFragment fragment) { if (fragment == null || conversation == null) return; var entry = CreateNewDialogueEntry(conversation, fragment.displayName.DefaultText, fragment.id); entry.canvasRect = new Rect(fragment.position.x, fragment.position.y, DialogueEntry.CanvasRectWidth, DialogueEntry.CanvasRectHeight); ConvertLocalizableText(entry, "Dialogue Text", fragment.text, true); ConvertLocalizableText(entry, "Menu Text", fragment.menuText, true); ConvertLocalizableText(entry, "Title", fragment.displayName); SetFeatureFields(entry.fields, fragment.features); switch (prefs.StageDirectionsMode) { case ConverterPrefs.StageDirModes.Sequences: var defaultSequenceText = fragment.stageDirections.DefaultText; if (!string.IsNullOrEmpty(defaultSequenceText) && (defaultSequenceText.Contains("(") || defaultSequenceText.Contains("{{"))) { ConvertLocalizableText(entry, "Sequence", fragment.stageDirections); } break; case ConverterPrefs.StageDirModes.Description: var description = fragment.stageDirections.DefaultText; Field.SetValue(entry.fields, "Description", description); break; } var conditionsField = Field.Lookup(entry.fields, "Conditions"); if (conditionsField != null) // Conditions field is handled differently. { entry.conditionsString = AddToUserScript(entry.conditionsString, conditionsField.value); entry.fields.Remove(conditionsField); } var scriptField = Field.Lookup(entry.fields, "Script"); if (scriptField != null) // Script field is handled differently. { entry.userScript = AddToUserScript(entry.userScript, scriptField.value); entry.fields.Remove(scriptField); } Actor actor = FindActorByArticyId(fragment.speakerIdRef); entry.ActorID = (actor != null) ? actor.id : (prefs.UseDefaultActorsIfNoneAssignedToDialogue ? conversation.ActorID : 0); var conversantEntity = Field.Lookup(entry.fields, "ConversantEntity"); var conversantActor = (conversantEntity == null) ? null : (prefs.ConvertSlotsAs == ConverterPrefs.ConvertSlotsModes.ID) ? FindActorByArticyId(conversantEntity.value) : (prefs.ConvertSlotsAs == ConverterPrefs.ConvertSlotsModes.TechnicalName) ? FindActorByTechnicalName(conversantEntity.value) : FindActorByDisplayName(conversantEntity.value); if (conversantActor != null) { entry.ConversantID = conversantActor.id; } else { entry.ConversantID = prefs.UseDefaultActorsIfNoneAssignedToDialogue ? ((entry.ActorID == conversation.ActorID) ? conversation.ConversantID : conversation.ActorID) : 0; } ConvertPinExpressionsToConditionsAndScripts(entry, fragment.pins); RecordPins(fragment.pins, entry); // No longer used: //// Handle documents: //if (documentConversation != null && lastDocumentEntry != null && !DoesLinkExist(lastDocumentEntry.outgoingLinks, entry)) //{ // Debug.Log("Adding link in conv " + documentConversation.Title + " entry " + lastDocumentEntry.id + " to entry " + entry.conversationID + ":" + entry.id); // var link = new Link(lastDocumentEntry.conversationID, lastDocumentEntry.id, entry.conversationID, entry.id); // lastDocumentEntry.outgoingLinks.Add(link); // lastDocumentEntry = entry; //} } protected virtual bool DoesLinkExist(List outgoingLinks, DialogueEntry destination) { if (outgoingLinks == null || destination == null) return false; for (int i = 0; i < outgoingLinks.Count; i++) { if (outgoingLinks[i] != null && outgoingLinks[i].destinationConversationID == destination.conversationID && outgoingLinks[i].destinationDialogueID == destination.id) { return true; } } return false; } protected virtual void AddFlowFragmentAsDialogueEntry(Conversation conversation, ArticyData.FlowFragment flowFragment) { if (flowFragment == null || conversation == null) return; var entry = CreateNewDialogueEntry(conversation, flowFragment.displayName.DefaultText, flowFragment.id); entry.canvasRect = new Rect(flowFragment.position.x, flowFragment.position.y, DialogueEntry.CanvasRectWidth, DialogueEntry.CanvasRectHeight); ConvertLocalizableText(entry, "Title", flowFragment.displayName); entry.Title = "Flow: " + entry.Title; SetFeatureFields(entry.fields, flowFragment.features); var scriptField = Field.Lookup(entry.fields, "Script"); if (scriptField != null) // Script is handled differently. { entry.userScript = AddToUserScript(entry.userScript, scriptField.value); entry.fields.Remove(scriptField); } entry.ActorID = conversation.ActorID; entry.ConversantID = (entry.ActorID == conversation.ActorID) ? conversation.ConversantID : conversation.ActorID; if (!string.IsNullOrEmpty(prefs.FlowFragmentScript)) { entry.userScript = prefs.FlowFragmentScript + "(\"" + flowFragment.displayName.DefaultText.Replace("\"", "'") + "\")"; } entry.isGroup = true; ConvertPinExpressionsToConditionsAndScripts(entry, flowFragment.pins); RecordPins(flowFragment.pins, entry); } /// /// Converts a hub into a group dialogue entry in a conversation. /// /// /// Conversation. /// /// /// Hub. /// protected virtual void BuildDialogueEntryFromHub(Conversation conversation, ArticyData.Hub hub) { if (hub == null || conversation == null) return; DialogueEntry groupEntry = CreateNewDialogueEntry(conversation, hub.displayName.DefaultText, hub.id); groupEntry.canvasRect = new Rect(hub.position.x, hub.position.y, DialogueEntry.CanvasRectWidth, DialogueEntry.CanvasRectHeight); SetFeatureFields(groupEntry.fields, hub.features); ConvertLocalizableText(groupEntry, "Title", hub.displayName); groupEntry.isGroup = true; ConvertPinExpressionsToConditionsAndScripts(groupEntry, hub.pins); RecordPins(hub.pins, groupEntry); } /// /// Converts a jump into a group dialogue entry in a conversation. /// /// /// Conversation. /// /// /// Jump. /// protected virtual void BuildDialogueEntryFromJump(Conversation conversation, ArticyData.Jump jump) { if (jump == null || conversation == null) return; DialogueEntry jumpEntry = CreateNewDialogueEntry(conversation, jump.displayName.DefaultText, jump.id); jumpEntry.canvasRect = new Rect(jump.position.x, jump.position.y, DialogueEntry.CanvasRectWidth, DialogueEntry.CanvasRectHeight); SetFeatureFields(jumpEntry.fields, jump.features); ConvertLocalizableText(jumpEntry, "Title", jump.displayName); jumpEntry.isGroup = true; // We'll set isGroup correctly in a final pass in CheckJumpsForGroupNodes. //jumpEntry.currentSequence = "Continue()"; ConvertPinExpressionsToConditionsAndScripts(jumpEntry, jump.pins); RecordPins(jump.pins, jumpEntry); jumpsToProcess.Add(jump, jumpEntry); var flowFragment = FindFlowFragment(jump.target.idRef); if (flowFragment != null) { var flowEntry = CreateNewDialogueEntry(conversation, "Flow: " + flowFragment.displayName.DefaultText, flowFragment.id); flowEntry.canvasRect = new Rect(jump.position.x, jump.position.y + 32f, DialogueEntry.CanvasRectWidth, DialogueEntry.CanvasRectHeight); SetFeatureFields(flowEntry.fields, flowFragment.features); flowEntry.isGroup = true; //flowEntry.currentSequence = "Continue()"; ConvertPinExpressionsToConditionsAndScripts(flowEntry, flowFragment.pins); RecordPins(flowFragment.pins, flowEntry); } } /// /// Jumps that link only to other jumps or group nodes should be group nodes themselves. /// This method sets the isGroup property correctly for all jump entries. /// /// CHANGED [2.2.1]: Jumps should always be groups unless they have a script. This is because /// scripts are always processed when passing through the group, which we don't want to do /// if we don't end up using the jump's destination entries. /// protected virtual void CheckJumpsForGroupNodes() { var jumpEntries = new HashSet(jumpsToProcess.Values); foreach (var jumpEntry in jumpEntries) { if (jumpEntry == null) continue; jumpEntry.isGroup = string.IsNullOrEmpty(jumpEntry.userScript); //jumpEntry.isGroup = true; //for (int i = 0; i < jumpEntry.outgoingLinks.Count; i++) //{ // var destEntry = database.GetDialogueEntry(jumpEntry.outgoingLinks[i]); // if (destEntry == null) continue; // var linksToJump = jumpEntries.Contains(jumpEntry); // var linksToGroup = destEntry.isGroup; // if (!(linksToJump || linksToGroup)) // { // jumpEntry.isGroup = false; // break; // } //} } } /// /// Converts a condition node into multiple dialogue entries - the condition entry and then /// some number of outgoing pins for true and false results. /// /// Conversation. /// Condition. protected virtual void BuildDialogueEntriesFromCondition(Conversation conversation, ArticyData.Condition condition) { if (condition == null || conversation == null) return; // Main condition node: var conditionEntry = CreateNewDialogueEntry(conversation, condition.expression, condition.id); conditionEntry.canvasRect = new Rect(condition.position.x, condition.position.y, DialogueEntry.CanvasRectWidth, DialogueEntry.CanvasRectHeight); conditionEntry.ActorID = conversation.ConversantID; conditionEntry.ConversantID = conversation.ActorID; conditionEntry.currentDialogueText = string.Empty; conditionEntry.currentMenuText = string.Empty; //conditionEntry.currentSequence = "Continue()"; conditionEntry.isGroup = true; string trueLuaConditions = ConvertExpression(condition.expression, true); string falseLuaConditions = string.IsNullOrEmpty(trueLuaConditions) ? "false" : string.Format("({0}) == false", RemoveTrailingSemicolon(trueLuaConditions)); // Separate child nodes for each output pin: float y = condition.position.y; foreach (var pin in condition.pins) { if (pin.semantic == ArticyData.SemanticType.Input) { RecordPin(pin, conditionEntry); conditionEntry.conditionsString = AddToConditions(conditionEntry.conditionsString, ConvertExpression(pin.expression, true)); } else if (pin.semantic == ArticyData.SemanticType.Output) { bool isTruePath = (pin.index == 0); string title = isTruePath ? condition.expression : string.Format("!({0})", condition.expression); var entry = CreateNewDialogueEntry(conversation, title, condition.id); entry.canvasRect = new Rect(condition.position.x, y, DialogueEntry.CanvasRectWidth, DialogueEntry.CanvasRectHeight); y += 2f; entry.ActorID = conversation.ConversantID; entry.ConversantID = conversation.ActorID; entry.currentDialogueText = string.Empty; entry.currentMenuText = string.Empty; //entry.currentSequence = "Continue()"; entry.isGroup = true; string luaConditions = isTruePath ? trueLuaConditions : falseLuaConditions; entry.conditionsString = AddToConditions(entry.conditionsString, luaConditions); entry.userScript = AddToUserScript(entry.userScript, ConvertExpression(pin.expression, false)); Link link = new Link(); link.originConversationID = conditionEntry.conversationID; link.originDialogueID = conditionEntry.id; link.destinationConversationID = entry.conversationID; link.destinationDialogueID = entry.id; link.isConnector = false; conditionEntry.outgoingLinks.Add(link); RecordPin(pin, entry); } } } protected string RemoveTrailingSemicolon(string s) { if (!string.IsNullOrEmpty(s) && s[s.Length - 1] == ';') { return s.Substring(0, s.Length - 1); } else { return s; } } protected virtual void BuildDialogueEntryFromInstruction(Conversation conversation, ArticyData.Instruction instruction) { if (instruction == null || conversation == null) return; DialogueEntry entry = CreateNewDialogueEntry(conversation, instruction.expression, instruction.id); entry.ActorID = conversation.ConversantID; entry.ConversantID = conversation.ActorID; entry.currentDialogueText = string.Empty; entry.currentMenuText = string.Empty; entry.currentSequence = "Continue()"; // Since it's not a group, make sure we continue past it immediately. entry.isGroup = false; // Since groups are processed one level ahead, don't make this a group: entry.isGroup = true; entry.conditionsString = string.Empty; entry.userScript = AddToUserScript(entry.userScript, ConvertExpression(instruction.expression, false)); ConvertPinExpressionsToConditionsAndScripts(entry, instruction.pins); RecordPins(instruction.pins, entry); } protected virtual string AddToConditions(string conditions, string moreConditions) { if (string.IsNullOrEmpty(conditions) && string.IsNullOrEmpty(moreConditions)) { return string.Empty; } else if (string.IsNullOrEmpty(conditions)) { return moreConditions; } else if (string.IsNullOrEmpty(moreConditions)) { return conditions; } else { return string.Format("({0}) and ({1})", conditions, moreConditions); } } protected virtual string AddToUserScript(string script, string moreScript) { if (string.IsNullOrEmpty(script) && string.IsNullOrEmpty(moreScript)) { return string.Empty; } else if (string.IsNullOrEmpty(script)) { return moreScript; } else if (string.IsNullOrEmpty(moreScript)) { return script; } else { return string.Format("{0}; {1}", script, moreScript); } } /// /// Creates a new dialogue entry and adds it to a conversation. /// /// The new dialogue entry. /// Conversation. /// Title. /// Articy identifier. protected DialogueEntry CreateNewDialogueEntry(Conversation conversation, string title, string articyId) { if (conversation == null) { Debug.Log("Conversation is null! " + articyId + " / " + title); return null; } DialogueEntry entry = template.CreateDialogueEntry(GetNextConversationEntryID(conversation), conversation.id, title); SetDialogueEntryParticipants(entry, conversation.ConversantID, conversation.ActorID); // Assume speaker is conversant until changed. Field.SetValue(entry.fields, ArticyIdFieldTitle, articyId, FieldType.Text); IndexDialogueEntryByArticyId(entry, articyId); conversation.dialogueEntries.Add(entry); return entry; } /// /// Converts input pins as a dialogue entry's Conditions, and output pins as User Script. /// /// Entry. /// Pins./param> /// Apply pin's input conditions to entry. /// Apply pin's output conditions to entry. protected virtual void ConvertPinExpressionsToConditionsAndScripts(DialogueEntry entry, List pins, bool convertInput = true, bool convertOutput = true) { foreach (ArticyData.Pin pin in pins) { switch (pin.semantic) { case ArticyData.SemanticType.Input: if (convertInput) { entry.conditionsString = AddToConditions(entry.conditionsString, ConvertExpression(pin.expression, true)); } break; case ArticyData.SemanticType.Output: if (convertOutput) { entry.userScript = AddToUserScript(entry.userScript, ConvertExpression(pin.expression, false)); } break; default: Debug.LogWarning("Dialogue System: Unexpected semantic type " + pin.semantic + " for pin " + pin.id + "."); break; } } } /// /// Converts an articy expresso expression into Lua. /// /// A Lua version of the expression. /// articy expresso expression. public static string ConvertExpression(string expression, bool isCondition = false) { if (string.IsNullOrEmpty(expression)) return expression; if (isCondition && expression.Trim().StartsWith("//") && !expression.Contains("\n")) return string.Empty; // If already Lua, return it: if (expression.Contains("Variable[")) return expression; // If no semicolon, convert single expression: if (!expression.Contains(";")) return ConvertSingleExpression(expression); var s = string.Empty; var singleExpressions = expression.Split(';'); // [TODO]: Handle semicolons nested inside string literals. for (int i = 0; i < singleExpressions.Length; i++) { var singleExpression = singleExpressions[i]; if (isCondition && singleExpression.Trim().StartsWith("//")) continue; if (string.IsNullOrEmpty(singleExpression)) continue; if (s.Length > 0) s += ";\n"; s += ConvertSingleExpression(singleExpression); } return s; } public static string ConvertSingleExpression(string expression) { if (string.IsNullOrEmpty(expression)) return expression; // If already Lua, return it: if (expression.Contains("Variable[")) return expression; // If no quotes, handle it as a single fragment: if (!expression.Contains("\"")) return ConvertExpressionFragment(expression); // Otherwise split on quotes except escaped quotes: string[] fragments = Regex.Split(expression, @"(?<=[^\\])[\""]", RegexOptions.None); var s = string.Empty; bool insideString = false; for (int i = 0; i < fragments.Length; i++) { s += insideString ? fragments[i] : ConvertExpressionFragment(fragments[i]); if (i + 1 < fragments.Length) s += '"'; insideString = !insideString; } return s; } /// /// Converts an articy expresso expression into Lua without handling quotes. /// This is a helper method meant to be called by ConvertExpression(). /// /// A Lua version of the expression. /// articy expresso expression. protected static string ConvertExpressionFragment(string expression) { if (string.IsNullOrEmpty(expression)) return expression; // Convert comments: string s = expression.Trim().Replace("///", "").Replace("//", "--"); // If already Lua, return it: if (expression.Contains("Variable[")) return expression; // Convert random to math.random: s = Regex.Replace(s, @"(? { return "not " + match.Value.Substring(1); }); // Convert arithmetic assignment operators (e.g., +=): if (ContainsArithmeticAssignment(s)) { string[] tokens = s.Split(null); for (int i = 1; i < tokens.Length; i++) { string token = tokens[i]; if (ContainsArithmeticAssignment(token)) { char operation = token[0]; tokens[i] = string.Format("= {0} {1}", tokens[i - 1], operation); } } s = string.Join(" ", tokens); } return s; } public static string IncDecMatchEvaluator(Match match) { var variableName = match.Value.Substring(0, match.Value.Length - 2).Trim(); var operation = match.Value.Substring(match.Value.Length - 1); return variableName + " = " + variableName + " " + operation + " 1"; } protected static bool ContainsArithmeticAssignment(string s) { return (s != null) && (s.Contains("+=") || s.Contains("-=")); } protected virtual void ConvertLocalizableText(DialogueEntry entry, string baseFieldTitle, ArticyData.LocalizableText localizableText, bool replaceNewlines = false) { if (entry == null) return; var defaultText = localizableText.DefaultText; if (!string.IsNullOrEmpty(defaultText)) Field.SetValue(entry.fields, baseFieldTitle, defaultText); foreach (KeyValuePair kvp in localizableText.localizedString) { if (string.IsNullOrEmpty(kvp.Key)) { Field.SetValue(entry.fields, baseFieldTitle, RemoveFormattingTags(kvp.Value, replaceNewlines), FieldType.Text); } else { string localizedTitle = string.Equals("Dialogue Text", baseFieldTitle) ? kvp.Key : string.Format("{0} {1}", baseFieldTitle, kvp.Key); Field.SetValue(entry.fields, localizedTitle, RemoveFormattingTags(kvp.Value, replaceNewlines), FieldType.Localization); } } } protected virtual void ConvertLocalizableText(List fields, string baseFieldTitle, ArticyData.LocalizableText localizableText) { foreach (KeyValuePair kvp in localizableText.localizedString) { if (string.IsNullOrEmpty(kvp.Key)) { Field.SetValue(fields, baseFieldTitle, RemoveFormattingTags(kvp.Value), FieldType.Text); } else { string localizedTitle = string.Equals("Dialogue Text", baseFieldTitle) ? kvp.Key : string.Format("{0} {1}", baseFieldTitle, kvp.Key); Field.SetValue(fields, localizedTitle, RemoveFormattingTags(kvp.Value), FieldType.Localization); } } } protected virtual string RemoveFormattingTags(string s, bool replaceNewlines = false) { if (string.IsNullOrEmpty(s)) return s; if (replaceNewlines && s.Contains(@"\n")) s = s.Replace(@"\n", "\n"); if (s.Contains("font-size")) { Regex regex = new Regex("{font-size:[0-9]+pt;}"); return regex.Replace(s, string.Empty); } else { return s; } } /// /// Sets a conversation's start cutscene to None() if it's otherwise not set. /// /// Conversation. protected static void SetConversationStartCutsceneToNone(Conversation conversation) { DialogueEntry entry = conversation.GetFirstDialogueEntry(); if (entry == null) { Debug.LogWarning("Dialogue System: Conversation '" + conversation.Title + "' doesn't have a START dialogue entry."); } else { if (string.IsNullOrEmpty(entry.currentSequence)) entry.currentSequence = "Continue()"; } } protected virtual Conversation FindConversationByArticyId(string articyId) { foreach (var conversation in database.conversations) { if (string.Equals(Field.LookupValue(conversation.fields, ArticyIdFieldTitle), articyId)) return conversation; } return null; } protected virtual DialogueEntry FindDialogueEntryByArticyId(Conversation conversation, string articyId) { if (conversation == null) return null; // Check cache first: if (entriesByArticyId.ContainsKey(articyId)) { var list = entriesByArticyId[articyId]; for (int i = 0; i < list.Count; i++) { if (list[i].conversationID == conversation.id) return list[i]; } } //Then check all entries in conversation: foreach (DialogueEntry entry in conversation.dialogueEntries) { if (string.Equals(Field.LookupValue(entry.fields, ArticyIdFieldTitle), articyId)) return entry; } return null; } protected virtual DialogueEntry FindDialogueEntryByArticyId(string articyId) { if (entriesByArticyId.ContainsKey(articyId)) { var list = entriesByArticyId[articyId]; if (list.Count > 0) return list[0]; } return null; } protected virtual List FindAllDialogueEntriesByArticyId(string articyId) { if (entriesByArticyId.ContainsKey(articyId)) { return entriesByArticyId[articyId]; } return new List(); } protected virtual ArticyData.FlowFragment FindFlowFragment(string articyId) { foreach (ArticyData.FlowFragment articyFlowFragment in articyData.flowFragments.Values) { if (prefs.ConversionSettings.GetConversionSetting(articyFlowFragment.id).Include && string.Equals(articyFlowFragment.id, articyId)) return articyFlowFragment; } return null; } protected virtual Actor FindActorByArticyId(string articyId) { foreach (Actor actor in database.actors) { if (string.Equals(actor.LookupValue(ArticyIdFieldTitle), articyId)) return actor; } return null; } protected virtual Actor FindActorByTechnicalName(string technicalName) { foreach (Actor actor in database.actors) { if (string.Equals(actor.LookupValue(ArticyTechnicalNameFieldTitle), technicalName)) return actor; } return null; } protected virtual Actor FindActorByDisplayName(string displayName) { foreach (Actor actor in database.actors) { if (string.Equals(actor.Name, displayName)) return actor; } return null; } protected virtual int FindActorIdFromArticyDialogue(ArticyData.Dialogue articyDialogue, int index, int defaultActorID) { Actor actor = null; if (0 <= index && index < articyDialogue.references.Count) { actor = FindActorByArticyId(articyDialogue.references[index]); } return (actor != null) ? actor.id : (prefs.UseDefaultActorsIfNoneAssignedToDialogue ? defaultActorID : -1); } protected virtual void SplitPipesIntoEntries() { foreach (var conversation in database.conversations) { conversation.SplitPipesIntoEntries(true, prefs.TrimWhitespace, ArticyIdFieldTitle); } } protected virtual void SortAllLinksByPosition() // articy orders links by Y position. { foreach (var conversation in database.conversations) { SortLinksByPosition(conversation); } } protected virtual void SortLinksByPosition(Conversation conversation) { // Sort by each element's Y position: foreach (var entry in conversation.dialogueEntries) { entry.outgoingLinks.Sort( delegate (Link A, Link B) { if (A.destinationConversationID != B.destinationConversationID) //return 0; // Only sort links in same conversation. { // Changed: Now sort cross-conversation links. // Keeping separate block in case this causes and issue and needs to be reverted. var destA = database.GetDialogueEntry(A); var destB = database.GetDialogueEntry(B); if (destA == null || destB == null) { Debug.LogWarning("Dialogue System: Unexpected error sorting links by position. destA=" + ((destA == null) ? "null" : destA.ToString()) + " (" + A.destinationConversationID + ":" + A.destinationDialogueID + "), destB=" + ((destB == null) ? "null" : destB.ToString()) + " (" + B.destinationConversationID + ":" + B.destinationDialogueID + ") in conversation '" + conversation.Title + "' entry " + entry.id + "."); } return (destA == null || destB == null) ? A.destinationDialogueID.CompareTo(B.destinationDialogueID) : destA.canvasRect.y.CompareTo(destB.canvasRect.y); } else { var destA = conversation.GetDialogueEntry(A.destinationDialogueID); var destB = conversation.GetDialogueEntry(B.destinationDialogueID); if (destA == null || destB == null) { Debug.LogWarning("Dialogue System: Unexpected error sorting links by position. destA=" + ((destA == null) ? "null" : destA.ToString()) + " (" + A.destinationConversationID + ":" + A.destinationDialogueID + "), destB=" + ((destB == null) ? "null" : destB.ToString()) + " (" + B.destinationConversationID + ":" + B.destinationDialogueID + ") in conversation '" + conversation.Title + "' entry " + entry.id + "."); } return (destA == null || destB == null) ? A.destinationDialogueID.CompareTo(B.destinationDialogueID) : destA.canvasRect.y.CompareTo(destB.canvasRect.y); } } ); } // Reset position because articy's positions don't necessarily map well onto the Dialogue Editor's canvas. foreach (var entry in conversation.dialogueEntries) { entry.canvasRect = new Rect(0, 0, DialogueEntry.CanvasRectWidth, DialogueEntry.CanvasRectHeight); } } /// /// If a dialogue fragment has an arrow to the dialogue's endpoint, redirect it to the dialogue's first external link. /// If the dialogue doesn't have an external link, remove the arrow (link). /// protected virtual void RedirectLinkbacksToStartToLinkOutFromStart() { foreach (var conversation in database.conversations) { var startEntry = conversation.GetFirstDialogueEntry(); if (startEntry == null) continue; var firstExternalLink = startEntry.outgoingLinks.Find(x => x.destinationConversationID != conversation.id); if (firstExternalLink != null) startEntry.outgoingLinks.Remove(firstExternalLink); foreach (var entry in conversation.dialogueEntries) { if (entry == startEntry) continue; for (int i = entry.outgoingLinks.Count - 1; i >= 0; i--) { var link = entry.outgoingLinks[i]; if (link.destinationConversationID == conversation.id && link.destinationDialogueID == startEntry.id) { if (firstExternalLink == null) { entry.outgoingLinks.RemoveAt(i); } else { link.destinationConversationID = firstExternalLink.destinationConversationID; link.destinationDialogueID = firstExternalLink.destinationDialogueID; } } } } } } protected virtual bool DoesEntryLinkOutsideConversation(DialogueEntry entry) { if (entry == null) return false; foreach (var link in entry.outgoingLinks) { if (link.destinationConversationID != entry.conversationID) return true; } return false; } protected virtual void ConvertVoiceOverProperties() { foreach (var conversation in database.conversations) { foreach (var entry in conversation.dialogueEntries) { ConvertVoiceOverProperty(entry); } } } protected virtual void ConvertVoiceOverProperty(DialogueEntry entry) { if (entry == null) return; var voiceOverPropertyField = Field.Lookup(entry.fields, prefs.VoiceOverProperty); if (voiceOverPropertyField == null) return; var assetID = voiceOverPropertyField.value; var asset = articyData.assets.ContainsKey(assetID) ? articyData.assets[assetID] : null; if (asset == null) { Debug.LogWarning("Dialogue System: Can't find voice-over asset with ID " + assetID + " for dialogue entry [" + entry.conversationID + ":" + entry.id + "]: '" + entry.currentDialogueText + "'."); return; } entry.fields.Remove(voiceOverPropertyField); entry.fields.Add(new Field(DialogueDatabase.VoiceOverFileFieldName, Path.GetFileNameWithoutExtension(asset.assetFilename), FieldType.Text)); } protected virtual void FindPortraitTextureInResources(Actor actor) { if (actor == null || actor.portrait != null) return; string textureName = actor.textureName; if (!string.IsNullOrEmpty(textureName)) { actor.portrait = LoadTexture(textureName); } // Alternate portraits: var s = actor.LookupValue("SUBTABLE__AlternatePortraits"); if (!string.IsNullOrEmpty(s)) { var alternatePortraitIDs = s.Split(';'); foreach (var alternatePortraitID in alternatePortraitIDs) { if (articyData.assets.ContainsKey(alternatePortraitID)) { var portrait = LoadTexture(articyData.assets[alternatePortraitID].displayName.DefaultText); if (portrait != null) actor.alternatePortraits.Add(portrait); } } } } protected virtual Texture2D LoadTexture(string originalPath) { string filename = Path.GetFileNameWithoutExtension(originalPath).Replace('\\', '/'); if (Application.isPlaying) { return DialogueManager.LoadAsset(filename, typeof(Texture2D)) as Texture2D; } else { return Resources.Load(filename, typeof(Texture2D)) as Texture2D; } } #endregion #region Em Var Set protected virtual void ConvertEmVarSet() { for (int i = 0; i < DialogueDatabase.NumEmphasisSettings; i++) { ConvertEmVars(prefs.emVarSet.emVars[i], database.emphasisSettings[i]); } } protected virtual void ConvertEmVars(ArticyEmVars emVars, EmphasisSetting emSetting) { if (emVars == null || emSetting == null) return; var colorVar = GetEmVar(emVars.color); var boldVar = GetEmVar(emVars.bold); var italicVar = GetEmVar(emVars.italic); var underlineVar = GetEmVar(emVars.underline); emSetting.color = (colorVar != null) ? Tools.WebColor(colorVar.InitialValue) : Color.white; emSetting.bold = (boldVar != null) ? boldVar.InitialBoolValue : false; emSetting.italic = (italicVar != null) ? italicVar.InitialBoolValue : false; emSetting.underline = (underlineVar != null) ? underlineVar.InitialBoolValue : false; } protected virtual Variable GetEmVar(string variableName) { return string.IsNullOrEmpty(variableName) ? null : database.GetVariable(variableName); } #endregion } } #endif