CapersProject/Assets/Plugins/Pixel Crushers/Dialogue System/Scripts/Importers/Articy/ArticyConverter.cs

2074 lines
99 KiB
C#

#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
{
/// <summary>
/// This class does the actual work of converting ArticyData (version-independent
/// articy:draft data) into a dialogue database.
/// </summary>
public class ArticyConverter
{
#region Public Static Utility Methods
public delegate void ProgressCallbackDelegate(string info, float progress);
public static event ProgressCallbackDelegate onProgressCallback = delegate { };
/// <summary>
/// Creates a new database from an articy:draft XML file.
/// </summary>
/// <param name="xmlData">Articy XML data (i.e., the contents of an articy:draft XML export).</param>
/// <param name="prefs">Optional ConverterPrefs to use. Does not use prefs.ProjectFilename.</param>
/// <param name="template">Optional template for dialogue database assets.</param>
/// <returns></returns>
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;
}
/// <summary>
/// This static utility method creates a converter and uses it to run the conversion.
/// </summary>
/// <param name='articyData'>
/// Articy data.
/// </param>
/// <param name='prefs'>
/// Prefs.
/// </param>
/// <param name='database'>
/// Dialogue database.
/// </param>
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<string> fullVariableNames = new List<string>(); // Make static to expose ConvertExpression().
protected HashSet<string> otherScriptFieldTitles = new HashSet<string>();
protected List<Conversation> documentConversations = new List<Conversation>();
#endregion
#region Stacks
protected List<string> flowFragmentNameStack = new List<string>();
protected List<Conversation> conversationStack = new List<Conversation>();
protected Dictionary<Conversation, int> conversationLastEntryID = new Dictionary<DialogueSystem.Conversation, int>();
protected Dictionary<string, List<DialogueEntry>> entriesByArticyId = new Dictionary<string, List<DialogueEntry>>();
protected Dictionary<string, DialogueEntry> entriesByPinID = new Dictionary<string, DialogueEntry>();
protected Dictionary<ArticyData.Jump, DialogueEntry> jumpsToProcess = new Dictionary<ArticyData.Jump, DialogueEntry>();
protected List<DialogueEntry> unusedOutputEntries = new List<DialogueEntry>();
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<DialogueEntry>());
entriesByArticyId[articyId].Add(entry);
}
}
#endregion
#region Top Level Conversion Methods
/// <summary>
/// Convert the ArticyData, using the preferences in Prefs, into a dialogue database.
/// </summary>
/// <param name='articyData'>Articy data.</param>
/// <param name='prefs'>Prefs.</param>
/// <param name='database'>Dialogue database.</param>
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();
}
}
/// <summary>
/// Sets up the conversion process.
/// </summary>
/// <param name='articyData'>Articy data.</param>
/// <param name='prefs'>Prefs.</param>
/// <param name='database'>Dialogue database.</param>
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<Actor>();
database.items = new List<Item>();
database.locations = new List<Location>();
database.variables = new List<Variable>();
database.conversations = new List<Conversation>();
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
/// <summary>
/// Converts articy entities into Dialogue System actors and items/quests.
/// </summary>
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);
}
}
/// <summary>
/// Converts locations.
/// </summary>
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);
}
}
}
/// <summary>
/// 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.
/// </summary>
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<Field> 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<Field> 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<string> SpecialFieldTitles = new List<string>(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<string> SpecialFieldTitleStarters = new List<string>(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;
}
/// <summary>
/// Converts articy variable sets and variables into Dialogue System variables.
/// </summary>
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));
}
/// <summary>
/// Converts dialogues using the articy project's hierarchy.
/// </summary>
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);
}
}
/// <summary>
/// Creates a new Dialogue System conversation from an articy dialogue. This also adds the
/// conversation's mandatory first dialogue entry, "START".
/// </summary>
/// <returns>The new conversation.</returns>
/// <param name='articyDialogue'>Articy dialogue.</param>
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<Link>();
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<Link>();
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);
}
/// <summary>
/// Creates a new Dialogue System conversation from an articy flow fragment. This also adds the
/// conversation's mandatory first dialogue entry, "START".
/// </summary>
/// <returns>The new conversation.</returns>
/// <param name='articyFlowFragment'>Articy flow fragment.</param>
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<Link>();
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<Link>();
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<Link>() { new Link(link) };
link.destinationDialogueID = bufferEntry.id;
}
}
}
}
protected const int MaxRecursionDepth = 1000;
/// <summary>
/// Processes a node in the hierarchy to build dialogue entries,
/// also recursively processing the node's children.
/// </summary>
/// <param name='node'>Node to process.</param>
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<ArticyData.Pin> 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 <START> 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;
}
/// <summary>
/// Converts a dialogue fragment, including fields such as text, sequence, and pins, but doesn't
/// connect it yet.
/// </summary>
/// <param name='conversation'>Conversation.</param>
/// <param name='fragment'>Fragment.</param>
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<Link> 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);
}
/// <summary>
/// Converts a hub into a group dialogue entry in a conversation.
/// </summary>
/// <param name='conversation'>
/// Conversation.
/// </param>
/// <param name='hub'>
/// Hub.
/// </param>
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);
}
/// <summary>
/// Converts a jump into a group dialogue entry in a conversation.
/// </summary>
/// <param name='conversation'>
/// Conversation.
/// </param>
/// <param name='jump'>
/// Jump.
/// </param>
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);
}
}
/// <summary>
/// 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.
/// </summary>
protected virtual void CheckJumpsForGroupNodes()
{
var jumpEntries = new HashSet<DialogueEntry>(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;
// }
//}
}
}
/// <summary>
/// Converts a condition node into multiple dialogue entries - the condition entry and then
/// some number of outgoing pins for true and false results.
/// </summary>
/// <param name='conversation'>Conversation.</param>
/// <param name='condition'>Condition.</param>
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);
}
}
/// <summary>
/// Creates a new dialogue entry and adds it to a conversation.
/// </summary>
/// <returns>The new dialogue entry.</returns>
/// <param name='conversation'>Conversation.</param>
/// <param name='title'>Title.</param>
/// <param name='articyId'>Articy identifier.</param>
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;
}
/// <summary>
/// Converts input pins as a dialogue entry's Conditions, and output pins as User Script.
/// </summary>
/// <param name='entry'>Entry.</param>
/// <param name='pins'>Pins./param>
/// <param name="convertInput">Apply pin's input conditions to entry.</param>
/// <param name="convertOutput">Apply pin's output conditions to entry.</param>
protected virtual void ConvertPinExpressionsToConditionsAndScripts(DialogueEntry entry, List<ArticyData.Pin> 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;
}
}
}
/// <summary>
/// Converts an articy expresso expression into Lua.
/// </summary>
/// <returns>A Lua version of the expression.</returns>
/// <param name='expression'>articy expresso expression.</param>
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;
}
/// <summary>
/// Converts an articy expresso expression into Lua without handling quotes.
/// This is a helper method meant to be called by ConvertExpression().
/// </summary>
/// <returns>A Lua version of the expression.</returns>
/// <param name='expression'>articy expresso expression.</param>
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, @"(?<!math\.)random\(", "math.random(");
// Convert conditionals:
s = s.Replace("&&", " and ");
s = s.Replace("||", " or ");
s = s.Replace("!=", "~=");
var incDecMatchEvaluator = new MatchEvaluator(IncDecMatchEvaluator);
// Convert variable names: (fixed to use regex to handle variable names that are subsets of other variable names)
foreach (string fullVariableName in fullVariableNames)
{
if (s.Contains(fullVariableName))
{
// Convert variable++ to variable = variable + 1:
string pattern = @"\b" + fullVariableName + @"\b\s*(\+\+|\-\-)";
s = Regex.Replace(s, pattern, incDecMatchEvaluator);
// Convert expresso variable name to Lua:
pattern = @"\b" + fullVariableName + @"\b";
string luaVariableReference = string.Format("Variable[\"{0}\"]", fullVariableName);
s = Regex.Replace(s, pattern, luaVariableReference);
}
}
// Convert negation (!) to "==false":
s = s.Replace("!Variable", "not Variable");
s = s.Replace("!(", "not (");
const string negatedFunctionPattern = @"!\b(_\w+|[\w-[0-9_]]\w*)\b";
s = Regex.Replace(s, negatedFunctionPattern, (match) =>
{
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<string, string> 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<Field> fields, string baseFieldTitle, ArticyData.LocalizableText localizableText)
{
foreach (KeyValuePair<string, string> 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;
}
}
/// <summary>
/// Sets a conversation's start cutscene to None() if it's otherwise not set.
/// </summary>
/// <param name='conversation'>Conversation.</param>
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<DialogueEntry> FindAllDialogueEntriesByArticyId(string articyId)
{
if (entriesByArticyId.ContainsKey(articyId))
{
return entriesByArticyId[articyId];
}
return new List<DialogueEntry>();
}
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);
}
}
/// <summary>
/// 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).
/// </summary>
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