CapersProject/Assets/Plugins/Pixel Crushers/Dialogue System/Scripts/Importers/Celtx3/CeltxGem3ToDialogueDatabase.cs

1471 lines
63 KiB
C#

#if USE_CELTX3
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using UnityEngine;
namespace PixelCrushers.DialogueSystem.Celtx
{
/// <summary>
/// This class does the actual work of converting Celtx Data (Raw) into a dialogue database.
/// </summary>
public class CeltxGem3ToDialogueDatabase
{
Template template = Template.FromDefault();
DialogueDatabase database;
dynamic celtxDataObject;
bool importGameplayAsEmptyNodes;
Dictionary<string, Actor> actorLookupViaCeltxId = new Dictionary<string, Actor>();
Dictionary<string, Item> itemLookupViaCeltxId = new Dictionary<string, Item>();
Dictionary<string, Location> locationLookupViaCeltxId = new Dictionary<string, Location>();
Dictionary<string, Variable> variableLookupViaCeltxId = new Dictionary<string, Variable>();
Dictionary<string, DialogueEntry> portalsPendingLinks = new Dictionary<string, DialogueEntry>();
Dictionary<string, dynamic> radioVarOptionsLookupViaCeltxId = new Dictionary<string, dynamic>();
Dictionary<string, CeltxCondition> celtxConditionLookupViaCeltxId = new Dictionary<string, CeltxCondition>();
Dictionary<string, string> catalogTypeByBreakdownId = new Dictionary<string, string>();
Dictionary<string, string> catalogIdByBreakdownId = new Dictionary<string, string>();
Dictionary<string, string> customNameByBreakdownId = new Dictionary<string, string>();
Dictionary<string, string> customTypeByBreakdownId = new Dictionary<string, string>();
public DialogueDatabase ProcessCeltxGem3DataObject(dynamic celtxDataObject, DialogueDatabase database, bool importGameplayAsEmptyNodes, bool importGameplayScriptText, bool importBreakdownCatalogContent, bool checkSequenceSyntax)
{
try
{
this.database = database;
this.celtxDataObject = celtxDataObject;
this.database.version = celtxDataObject.meta.version;
this.importGameplayAsEmptyNodes = importGameplayAsEmptyNodes;
GenerateActorsFromCharacters();
GenerateLocationsFromCatalog();
GenerateItemFromCatalog();
ImportCeltxVariables();
GenerateConditions();
GenerateConversationsFromLanes();
if (checkSequenceSyntax) CheckSequenceSyntax();
ConvertBlankNodesToGroupNodes();
}
catch (System.Exception e)
{
LogError(MethodBase.GetCurrentMethod(), e);
}
return database;
}
#region General Helper Methods
private void AppendToField(List<Field> fields, string title, string value, FieldType fieldType)
{
string currentFieldValue = Field.LookupValue(fields, title);
if (value == null || value.Equals("") || currentFieldValue != null && currentFieldValue.Equals(value)) { return; }
string updatedString;
if (currentFieldValue == null || currentFieldValue.Equals("") || currentFieldValue.Equals("[]")) { updatedString = value; }
else { updatedString = currentFieldValue + " " + value; }
Field.SetValue(fields, title, updatedString, fieldType);
}
private DialogueEntry CreateAdditionalEntryForSequence(Conversation conversation, DialogueEntry currentEntry, DialogueEntry initialEntry, int entryCount)
{
DialogueEntry newEntry = CreateNextDialogueEntryForConversation(conversation, initialEntry.Title + "-" + entryCount,
Field.LookupValue(initialEntry.fields, CeltxFields.CeltxId) + "-" + entryCount);
newEntry.ActorID = currentEntry.ActorID;
newEntry.ConversantID = currentEntry.ConversantID;
LinkDialogueEntries(currentEntry, newEntry, null);
return newEntry;
}
private void LinkDialogueEntries(DialogueEntry source, DialogueEntry destination, string conditionId)
{
try
{
if (conditionId == null)
{
source.outgoingLinks.Add(new Link(source.conversationID, source.id, destination.conversationID, destination.id));
}
else
{
CeltxCondition condition = celtxConditionLookupViaCeltxId[conditionId];
DialogueEntry entryToLinkToCondition = source;
if (condition.delay)
{
DialogueEntry delayEntry = CreateNextDialogueEntryForConversation(database.GetConversation(source.conversationID),
"[D]-" + condition.name,
"D-" + Field.LookupValue(source.fields, CeltxFields.CeltxId) + "-" + Field.LookupValue(destination.fields, CeltxFields.CeltxId), true);
source.outgoingLinks.Add(new Link(source.conversationID, source.id, delayEntry.conversationID, delayEntry.id));
entryToLinkToCondition = delayEntry;
}
DialogueEntry conditionEntry = CreateNextDialogueEntryForConversation(database.GetConversation(source.conversationID),
"[COND]" + condition.name,
"C-" + Field.LookupValue(source.fields, CeltxFields.CeltxId) + "-" + Field.LookupValue(destination.fields, CeltxFields.CeltxId), true);
conditionEntry.conditionsString = condition.luaConditionString;
entryToLinkToCondition.outgoingLinks.Add(new Link(entryToLinkToCondition.conversationID, entryToLinkToCondition.id, conditionEntry.conversationID, conditionEntry.id));
conditionEntry.outgoingLinks.Add(new Link(conditionEntry.conversationID, conditionEntry.id, destination.conversationID, destination.id));
}
}
catch (System.Exception e)
{
LogError(MethodBase.GetCurrentMethod(), e, source.Title + "-" + destination.Title + "-" + conditionId);
}
}
private DialogueEntry CreateNextDialogueEntryForConversation(Conversation conversation, string title, string celtxId, bool isGroup = false)
{
try
{
DialogueEntry dialogueEntry = template.CreateDialogueEntry(template.GetNextDialogueEntryID(conversation), conversation.id, title);
conversation.dialogueEntries.Add(dialogueEntry);
dialogueEntry.isGroup = isGroup;
dialogueEntry.ActorID = conversation.ActorID;
dialogueEntry.ConversantID = conversation.ConversantID;
if (celtxId != null)
{
Field.SetValue(dialogueEntry.fields, CeltxFields.CeltxId, celtxId);
//celtxData.dialogueEntryLookupByCeltxId.Add(celtxId, dialogueEntry);
}
return dialogueEntry;
}
catch (System.Exception e)
{
LogError(MethodBase.GetCurrentMethod(), e, celtxId, title);
}
return null;
}
/// <summary>
/// Check syntax of all sequences in all dialogue entries in database.
/// Log warning for any entries with syntax errors.
/// </summary>
private void CheckSequenceSyntax()
{
if (database == null) return;
var parser = new SequenceParser();
foreach (Conversation conversation in database.conversations)
{
foreach (DialogueEntry entry in conversation.dialogueEntries)
{
var sequence = entry.Sequence;
if (string.IsNullOrEmpty(sequence)) continue;
var result = parser.Parse(sequence);
if (result == null || result.Count == 0)
{
var text = entry.Title;
if (string.IsNullOrEmpty(text)) text = entry.subtitleText;
LogWarning("Dialogue entry " + conversation.id + ":" + entry.id +
" in conversation '" + conversation.Title + "' has syntax error in [SEQ] Sequence: " + sequence,
Field.LookupValue(entry.fields, "Celtx ID"), entry.Title);
}
}
}
}
private void ConvertBlankNodesToGroupNodes()
{
foreach (Conversation conversation in database.conversations)
{
foreach (DialogueEntry entry in conversation.dialogueEntries)
{
var isBlankNode = entry.id != 0 && // <START> nodes can't be group nodes.
string.IsNullOrEmpty(entry.DialogueText) &&
string.IsNullOrEmpty(entry.MenuText) &&
string.IsNullOrEmpty(entry.Sequence);
if (isBlankNode)
{
entry.isGroup = true;
}
}
}
}
#endregion
#region Non-Dialogue Processing
private void GenerateActorsFromCharacters()
{
var characters = celtxDataObject.subdocuments.catalog.character;
foreach (var characterObject in characters)
{
var characterData = characterObject.Value;
string characterDesc = (string)characterData.desc;
bool isPlayer = false;
if (StringStartsWithTag(characterDesc, "[PLAYER]"))
{
isPlayer = true;
}
Actor actor = database.GetActor((string)characterData.name);
if (actor == null)
{
actor = template.CreateActor(template.GetNextActorID(database), (string)characterData.name, isPlayer);
AppendToField(actor.fields, CeltxFields.CeltxId, (string)characterData.id, FieldType.Text);
AppendToField(actor.fields, DialogueSystemFields.Description, GetTextWithoutTag(characterDesc), FieldType.Text);
database.actors.Add(actor);
}
else
{
var originalActor = (database.syncInfo.syncActors && database.syncInfo.syncActorsDatabase != null)
? database.syncInfo.syncActorsDatabase.GetActor(actor.Name) : null;
if (originalActor != null)
{
AppendToField(actor.fields, CeltxFields.CeltxId, (string)characterData.id, FieldType.Text);
AppendToField(actor.fields, DialogueSystemFields.Description, GetTextWithoutTag(characterDesc), FieldType.Text);
#if UNITY_EDITOR
UnityEditor.EditorUtility.SetDirty(database.syncInfo.syncActorsDatabase);
#endif
}
}
AppendToField(actor.fields, CeltxFields.Pictures, GetPictureString(characterData), FieldType.Files);
actorLookupViaCeltxId[(string)characterData.id] = actor;
}
}
private string GetPictureString(dynamic catalogItemAttrs)
{
try
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("[");
if (catalogItemAttrs.item_data.media != null && catalogItemAttrs.item_data.media.Count > 0)
{
var mediaCount = catalogItemAttrs.item_data.media.Count;
for (int i = 0; i < mediaCount; i++)
{
var media = catalogItemAttrs.item_data.media[i];
stringBuilder.Append(media.name);
if (i == mediaCount - 1)
{
stringBuilder.Append("]");
}
else
{
stringBuilder.Append(";");
}
}
return stringBuilder.ToString();
}
else
{
return "[]";
}
}
catch (System.Exception e)
{
LogError(MethodBase.GetCurrentMethod(), e, catalogItemAttrs.id, catalogItemAttrs.title);
}
return "[]";
}
private void GenerateLocationsFromCatalog()
{
var locations = celtxDataObject.subdocuments.catalog.location;
foreach (var locationObject in locations)
{
var locationData = locationObject.Value;
Location location = database.GetLocation((string)locationData.name);
if (location == null)
{
location = template.CreateLocation(template.GetNextLocationID(database), (string)locationData.name);
AppendToField(location.fields, CeltxFields.CeltxId, (string)locationData.id, FieldType.Text);
AppendToField(location.fields, DialogueSystemFields.Description, (string)locationData.desc, FieldType.Text);
database.locations.Add(location);
}
else
{
var originalLocation = (database.syncInfo.syncLocations && database.syncInfo.syncLocationsDatabase != null)
? database.syncInfo.syncLocationsDatabase.GetLocation(location.Name) : null;
if (originalLocation != null)
{
AppendToField(location.fields, CeltxFields.CeltxId, (string)locationData.id, FieldType.Text);
AppendToField(location.fields, DialogueSystemFields.Description, (string)locationData.desc, FieldType.Text);
#if UNITY_EDITOR
UnityEditor.EditorUtility.SetDirty(database.syncInfo.syncLocationsDatabase);
#endif
}
locationLookupViaCeltxId[(string)locationData.id] = location;
}
}
}
private void GenerateItemFromCatalog()
{
var items = celtxDataObject.subdocuments.catalog.item;
foreach (var itemObject in items)
{
var itemData = itemObject.Value;
Item item = database.GetItem((string)itemData.name);
if (item == null)
{
item = template.CreateItem(template.GetNextItemID(database), (string)itemData.name);
AppendToField(item.fields, CeltxFields.CeltxId, (string)itemData.id, FieldType.Text);
AppendToField(item.fields, DialogueSystemFields.Description, (string)itemData.desc, FieldType.Text);
database.items.Add(item);
}
else
{
var originalItem = (database.syncInfo.syncItems && database.syncInfo.syncItemsDatabase != null)
? database.syncInfo.syncItemsDatabase.GetItem(item.Name) : null;
if (originalItem != null)
{
AppendToField(item.fields, CeltxFields.CeltxId, (string)itemData.id, FieldType.Text);
AppendToField(item.fields, DialogueSystemFields.Description, (string)itemData.desc, FieldType.Text);
#if UNITY_EDITOR
UnityEditor.EditorUtility.SetDirty(database.syncInfo.syncItemsDatabase);
#endif
}
}
itemLookupViaCeltxId[(string)itemData.id] = item;
}
}
private void ImportCeltxVariables()
{
foreach (var variableObject in celtxDataObject.variables)
{
var variableData = variableObject.Value;
string variableDefaultValue = "";
FieldType fieldType = FieldType.Text;
var variableType = (string)variableData.type;
switch (variableType)
{
case "boolean":
fieldType = FieldType.Boolean;
variableDefaultValue = (string)variableData.config.defaultValue;
break;
case "number":
case "range":
fieldType = FieldType.Number;
variableDefaultValue = (string)variableData.config.defaultValue;
break;
case "date":
case "text":
case "textarea":
case "time":
fieldType = FieldType.Text;
variableDefaultValue = (string)variableData.config.defaultValue;
break;
case "radio":
fieldType = FieldType.Text;
variableDefaultValue = (string)variableData.config.options[(int)variableData.config.defaultValue].value;
radioVarOptionsLookupViaCeltxId.Add((string)variableData.id, variableData.config.options);
break;
}
var variable = database.GetVariable((string)variableData.name);
if (variable == null)
{
int variableId = template.GetNextVariableID(database);
variable = template.CreateVariable(variableId, (string)variableData.name, variableDefaultValue, fieldType);
database.variables.Add(variable);
}
else
{
var originalVariable = (database.syncInfo.syncVariables && database.syncInfo.syncVariablesDatabase != null)
? database.syncInfo.syncVariablesDatabase.GetVariable(variableObject.name) : null;
if (originalVariable != null)
{
#if UNITY_EDITOR
UnityEditor.EditorUtility.SetDirty(database.syncInfo.syncVariablesDatabase);
#endif
}
}
AppendToField(variable.fields, CeltxFields.Description, (string)variableData.desc, FieldType.Text);
variableLookupViaCeltxId.Add((string)variableData.id, variable);
}
}
#endregion
#region Condition Processing
private void GenerateConditions()
{
foreach (var conditionObject in celtxDataObject.conditions)
{
var conditionData = conditionObject.Value;
var conditionId = (string)conditionData.id;
var conditionName = (string)conditionData.name;
var conditionDesc = (string)conditionData.desc;
var conditionClause = (string)conditionData.clause;
var conditionLiterals = conditionData.literals;
CeltxCondition celtxCondition = new CeltxCondition();
celtxCondition.id = conditionId;
celtxCondition.name = conditionName;
celtxCondition.description = conditionDesc;
if (conditionDesc != null)
{
celtxCondition.delay = conditionDesc.Contains("[D]");
}
if (celtxCondition.delay) celtxCondition.description = conditionDesc.Substring(0, "[D]".Length);
foreach (var literalObject in conditionLiterals)
{
CeltxConditionLiteral literal = new CeltxConditionLiteral();
literal.literalData = literalObject.Value;
celtxCondition.literalsLookup.Add(literalObject.Name, literal);
}
celtxConditionLookupViaCeltxId.Add(conditionId, celtxCondition);
SetFollowingOperators(celtxCondition, conditionClause);
}
foreach (var celtxCondition in celtxConditionLookupViaCeltxId.Values)
{
GenerateLuaCondition(celtxCondition);
}
}
// Split the condition clause to note whether each literal is followed by "and" or "or"
private void SetFollowingOperators(CeltxCondition condition, string clause)
{
try
{
List<string> subSections = new List<string>();
List<string> orSplit = new List<string>();
subSections.AddRange(System.Text.RegularExpressions.Regex.Split(clause, "(\\+|\\.)"));
if (subSections.ElementAt(subSections.Count - 1).Equals("")) { subSections.RemoveAt(subSections.Count - 1); }
for (int i = 0; i < subSections.Count - 1; i += 2)
{
CeltxConditionLiteral literal = condition.literalsLookup[subSections.ElementAt(i)];
literal.followingOp = subSections.ElementAt(i + 1);
}
}
catch (System.Exception e)
{
LogError(MethodBase.GetCurrentMethod(), e, condition.id, condition.name);
}
}
private void GenerateLuaCondition(CeltxCondition condition)
{
try
{
if (condition.literalsLookup.Count == 0)
{
LogMessage(LogLevel.W,
"Condition has no literals(literals list is null). Lua script generation will be skipped for this condition. Please add literals to the condition in Celtx", condition.id);
return;
}
StringBuilder conditionString = new StringBuilder();
string followingOp = null;
foreach (string literalKey in condition.literalsLookup.Keys)
{
if (followingOp != null) { conditionString.Append(" " + followingOp + " "); }
CeltxConditionLiteral conditionLiteral = condition.literalsLookup[literalKey];
var literal = conditionLiteral.literalData;
var literalType = (string)literal.type;
if (literalType.Equals("variable"))
{
var variableId = (string)literal.id;
Variable variable = variableLookupViaCeltxId[variableId];
conditionString.Append("Variable");
conditionString.Append("[\"" + variable.Name + "\"]");
string comparisonVal;
comparisonVal = (string)literal.comparisonValue.value;
Variable var = variableLookupViaCeltxId[variableId];
if (comparisonVal.Equals("Any"))
{
conditionString.Append(" ~= nil");
}
else
{
conditionString.Append(" " + GetLuaComparisonOperator((string)literal.comparisonOperator) + " ");
if (var.Type == FieldType.Text)
{
conditionString.Append("\"" + comparisonVal + "\"");
}
else
{
conditionString.Append(comparisonVal);
}
}
}
else if (literalType.Equals("condition"))
{
conditionString.Append("(");
conditionString.Append(GetNestedConditionString((string)literal.conditionId));
conditionString.Append(") == ");
conditionString.Append(literal.comparisonValue.value);
}
else
{
var predicate = (string)literal.predicate;
if (predicate.Equals("exists"))
{
conditionString.Append("exists(\"");
}
else
{
conditionString.Append("not exists(");
}
conditionString.Append((string)literal.name);
conditionString.Append("\")");
}
if (conditionLiteral.followingOp.Equals(".")) { followingOp = "and"; }
else if (conditionLiteral.followingOp.Equals("+")) { followingOp = "or"; }
else
{
LogMessage(LogLevel.W,
"Unsupported following operator(" + conditionLiteral.followingOp + ") in condition. Only \".\"(AND) and \"+\"(OR) are supported", condition.id + "-" + (string)literalKey);
continue;
}
}
condition.luaConditionString = conditionString.ToString();
}
catch (System.Exception e)
{
LogError(MethodBase.GetCurrentMethod(), e, condition.id, condition.name);
}
}
private string GetNestedConditionString(string conditionId)
{
try
{
CeltxCondition condition = celtxConditionLookupViaCeltxId[conditionId];
if (condition.luaConditionString == null)
{
GenerateLuaCondition(condition);
}
return condition.luaConditionString;
}
catch (System.Exception e)
{
LogError(MethodBase.GetCurrentMethod(), e, conditionId);
}
return null;
}
private string GetLuaComparisonOperator(string literalOp)
{
switch (literalOp)
{
case "<":
case "<=":
case ">":
case ">=":
return literalOp;
case "=":
return "==";
case "!=":
return "~=";
case "exists":
LogMessage(LogLevel.W,
"Literal operator(" + literalOp + ") not supported. Defaulting to \"==\"");
return "==";
case "does not exist":
LogMessage(LogLevel.W,
"Literal operator(" + literalOp + ") not supported. Defaulting to \"~=\"");
return "~=";
default:
LogMessage(LogLevel.W,
"Unknown comparison operator(" + literalOp + ") in literal. Defaulting to \"==\"");
return "==";
}
}
#endregion
#region LaneProcessing
private void GenerateConversationsFromLanes()
{
foreach (var laneObject in celtxDataObject.lanes)
{
var laneData = laneObject.Value;
//Debug.Log(laneData);
//Debug.Log(laneData.name + " - " + laneData.id + laneData.desc);
string conversationName = CreateHierarchicalConversationName(laneData);
// Create the conversation for the lane
Conversation conversation = template.CreateConversation(template.GetNextConversationID(database), conversationName);
conversation.Description = laneData.desc;
AppendToField(conversation.fields, "CeltxId", (string)laneData.id, FieldType.Text);
database.conversations.Add(conversation);
// Get the nodes and edges for the lane
var laneSubdocument = celtxDataObject.subdocuments[(string)laneData.id];
GenerateDialogueEntriesAndLinks(conversation, laneSubdocument);
}
}
private string CreateHierarchicalConversationName(dynamic laneData)
{
// Get name hierarchy for lane name
StringBuilder nameBuilder = new StringBuilder();
var laneName = laneData.name;
nameBuilder.Append(laneName);
var parentInfo = laneData.parent;
do
{
var parentId = (string)parentInfo.id;
var parentKind = (string)parentInfo.kind;
string parentName = "";
dynamic parentObject = null;
switch (parentKind)
{
case "episode":
var episodes = celtxDataObject.episodes;
parentObject = episodes[parentId];
parentName = parentObject.name;
break;
case "scene":
var scenes = celtxDataObject.scenes;
parentObject = scenes[parentId];
parentName = parentObject.name;
break;
case "mode":
var modes = celtxDataObject.modes;
parentObject = modes[parentId];
parentName = parentObject.name;
break;
}
nameBuilder.Insert(0, parentName + " / ");
parentInfo = parentObject.parent;
}
while (parentInfo != null);
return nameBuilder.ToString();
}
private void GenerateDialogueEntriesAndLinks(Conversation conversation, dynamic laneSubdocument)
{
Dictionary<string, DialogueEntry> dialogueEntryToDict = new Dictionary<string, DialogueEntry>();
Dictionary<string, DialogueEntry> dialogueEntryFromDict = new Dictionary<string, DialogueEntry>();
catalogTypeByBreakdownId.Clear();
catalogIdByBreakdownId.Clear();
customNameByBreakdownId.Clear();
customTypeByBreakdownId.Clear();
foreach (var breakdown in laneSubdocument.breakdowns)
{
var breakdownData = breakdown.Value;
var breakdown_id = breakdownData.id.ToString();
catalogTypeByBreakdownId[breakdown_id] = breakdownData.type.ToString();
catalogIdByBreakdownId[breakdown_id] = breakdownData.catalog_id.ToString();
customNameByBreakdownId[breakdown_id] = breakdownData.name.ToString();
customTypeByBreakdownId[breakdown_id] = breakdownData.custom_type.ToString();
}
var laneNodes = laneSubdocument.nodes;
bool rootProcessed = false;
DialogueEntry startEntry = null;
// Make list of all non-root nodes so we can identify root node:
HashSet<string> nonRootNodeIds = new HashSet<string>();
foreach (var edgeObject in laneSubdocument.edges)
{
var edgeData = edgeObject.Value;
var to = (string)edgeData.to;
nonRootNodeIds.Add(to);
}
// Create <START> node:
startEntry = CreateNextDialogueEntryForConversation(conversation, "START", "");
// Set conversation's ActorID and ConversantID:
foreach (var nodeObject in laneNodes)
{
var nodeData = nodeObject.Value;
var nodeId = (string)nodeData.id;
var nodeName = (string)nodeData.name;
var scriptObject = GetNodeScriptObject(nodeId, laneSubdocument);
if (!rootProcessed && !nonRootNodeIds.Contains(nodeId))
{
List<Actor> actors = GetCharactersFromRootPage(scriptObject);
if (actors.Count >= 2)
{
conversation.ActorID = actors[0].id;
conversation.ConversantID = actors[1].id;
}
else if (actors.Count == 1)
{
conversation.ConversantID = conversation.ActorID = actors[0].id;
}
else
{
Debug.LogWarning("Node " + nodeName + " with id " + nodeId + " has no actors.");
}
startEntry.ActorID = conversation.ActorID;
startEntry.ConversantID = conversation.ConversantID;
Field.SetValue(startEntry.fields, CeltxFields.CeltxId, (string)nodeId);
}
}
// Create dialogue entries and links for the nodes and edges
foreach (var nodeObject in laneNodes)
{
var nodeData = nodeObject.Value;
var nodeId = (string)nodeData.id;
var nodeName = (string)nodeData.name;
var nodeDesc = (string)nodeData.desc;
var nodeType = (string)nodeData.type;
DialogueEntry lastEntry = null;
var scriptObject = GetNodeScriptObject(nodeId, laneSubdocument);
if (!rootProcessed && !nonRootNodeIds.Contains(nodeId))
{
List<Actor> actors = GetCharactersFromRootPage(scriptObject);
if (actors.Count >= 2)
{
conversation.ActorID = actors[0].id;
conversation.ConversantID = actors[1].id;
}
else if (actors.Count == 1)
{
conversation.ConversantID = conversation.ActorID = actors[0].id;
}
else
{
Debug.LogWarning("Node " + nodeName + " with id " + nodeId + " has no actors.");
}
startEntry.ActorID = conversation.ActorID;
startEntry.ConversantID = conversation.ConversantID;
Field.SetValue(startEntry.fields, CeltxFields.CeltxId, (string)nodeId);
AppendToField(startEntry.fields, DialogueSystemFields.Description, (string)nodeDesc, FieldType.Text);
}
lastEntry = CreateNextDialogueEntryForConversation(conversation, nodeName, (string)nodeId);
AppendToField(lastEntry.fields, DialogueSystemFields.Description, (string)nodeDesc, FieldType.Text);
// Set variables
AddSetVariableScriptForNode(lastEntry, nodeData.customVars);
if (!rootProcessed && !nonRootNodeIds.Contains(nodeId))
{
startEntry.outgoingLinks.Add(new Link(startEntry.conversationID, startEntry.id, lastEntry.conversationID, lastEntry.id));
dialogueEntryFromDict.Add(nodeId, lastEntry);
dialogueEntryToDict.Add(nodeId, lastEntry);
lastEntry.isGroup = true;
rootProcessed = true;
}
else
{
// When a node contains a script (the node is a sequence), linking to the node will get the first dialogueEntry created from the node
// Likewise, linking from the node will get the last dialogueEntry created from the node
dialogueEntryToDict.Add(nodeId, lastEntry);
if (scriptObject != null)
{
lastEntry = ProcessSequenceNodePage(conversation, lastEntry, scriptObject);
}
dialogueEntryFromDict.Add(nodeId, lastEntry);
}
// Portals
if (nodeType.Equals("portal"))
{
ProcessPortalNode(nodeData, nodeId, lastEntry);
}
}
LinkDialogueEntriesFromEdges(laneSubdocument.edges, dialogueEntryFromDict, dialogueEntryToDict);
}
private void ProcessPortalNode(dynamic nodeData, string nodeId, DialogueEntry lastEntry)
{
var portalRef = (string)nodeData.portalRef;
if (portalsPendingLinks.ContainsKey(portalRef))
{
DialogueEntry portalForRef = portalsPendingLinks[portalRef];
var currentPortalType = (string)nodeData.portalType;
DialogueEntry fromDialogueObject;
DialogueEntry toDialogueObject;
if (currentPortalType.Equals("to"))
{
toDialogueObject = lastEntry;
fromDialogueObject = portalForRef;
}
else
{
toDialogueObject = portalForRef;
fromDialogueObject = lastEntry;
}
fromDialogueObject.outgoingLinks.Add(new Link(fromDialogueObject.conversationID, fromDialogueObject.id, toDialogueObject.conversationID, toDialogueObject.id));
portalsPendingLinks.Remove(portalRef);
}
else
{
portalsPendingLinks.Add(nodeId, lastEntry);
}
}
private void LinkDialogueEntriesFromEdges(dynamic laneEdges, Dictionary<string, DialogueEntry> dialogueEntryFromDict, Dictionary<string, DialogueEntry> dialogueEntryToDict)
{
foreach (var edgeObject in laneEdges)
{
var edgeData = edgeObject.Value;
var to = (string)edgeData.to;
var from = (string)edgeData.from;
string conditionId = null;
try
{
conditionId = (string)edgeData.condition;
}
catch (System.Exception)
{
conditionId = null;
}
DialogueEntry toDialogueObject, fromDialogueObject;
if (!dialogueEntryToDict.TryGetValue(to, out toDialogueObject))
{
Debug.LogWarning($"LinkDialogueEntriesFromEdges cannot locate destination entry {to}.");
continue;
}
if (!dialogueEntryFromDict.TryGetValue(from, out fromDialogueObject))
{
Debug.LogWarning($"LinkDialogueEntriesFromEdges cannot locate origin entry {from}.");
continue;
}
LinkDialogueEntries(fromDialogueObject, toDialogueObject, conditionId);
}
}
private DialogueEntry ProcessSequenceNodePage(Conversation conversation, DialogueEntry initialNodeEntry, dynamic scriptObject)
{
DialogueEntry currentEntry = initialNodeEntry;
DialogueEntry breakdownEntry = currentEntry;
bool createdGameplayEntry = false;
try
{
var pageContents = scriptObject.content[0].content[0].content;
int entryCount = 1;
bool createNewEntry = false;
foreach (var contentObject in pageContents)
{
string contentType = GetContentType(contentObject);
if (!(contentType.Equals("cxgameplay") ||
contentType.Equals("cxcharacter") ||
contentType.Equals("cxparenthetical") ||
contentType.Equals("cxdialog") ||
contentType.Equals("cxcharacter_item") ||
contentType.Equals("cxdirective")))
{
Debug.LogWarning("Skipping content with type: " + contentType + " for node " + initialNodeEntry.Title);
continue;
}
if (importGameplayAsEmptyNodes && contentType == "cxgameplay")
{
var firstText = string.Empty;
foreach (var content in contentObject.content)
{
if (content.type == "text")
{
firstText = content.text;
break;
}
}
if (!string.IsNullOrEmpty(firstText) &&
!(firstText.StartsWith("[SEQ]", System.StringComparison.OrdinalIgnoreCase) ||
firstText.StartsWith("[COND]", System.StringComparison.OrdinalIgnoreCase) ||
firstText.StartsWith("[SCRIPT]", System.StringComparison.OrdinalIgnoreCase)))
{
entryCount++;
DialogueEntry newEntry = CreateNextDialogueEntryForConversation(conversation, string.Empty, (string)contentObject.attrs.id, true);
newEntry.ActorID = currentEntry.ActorID;
newEntry.ConversantID = currentEntry.ConversantID;
newEntry.Title = GetContentText(contentObject, currentEntry, null);
LinkDialogueEntries(currentEntry, newEntry, null);
currentEntry = newEntry;
breakdownEntry = currentEntry;
createdGameplayEntry = true;
createNewEntry = true;
}
}
if (createNewEntry)
{
entryCount++;
currentEntry = CreateAdditionalEntryForSequence(conversation, currentEntry, initialNodeEntry, entryCount);
createNewEntry = false;
if (!createdGameplayEntry) breakdownEntry = currentEntry;
}
string id = contentObject.attrs.id;
Field.SetValue(currentEntry.fields, CeltxFields.CeltxId, id);
createdGameplayEntry = false;
string combinedText = GetContentText(contentObject, currentEntry, breakdownEntry); // Note: May add fields to currentEntry and/or breakdownEntry.
switch (contentType)
{
case "cxgameplay":
if (StringStartsWithTag(combinedText, "[SEQ]"))
{
currentEntry.Sequence = GetTextWithoutTag(combinedText);
}
else if (StringStartsWithTag(combinedText, "[COND]"))
{
if (!string.IsNullOrEmpty(currentEntry.conditionsString)) currentEntry.conditionsString += ";\n";
currentEntry.conditionsString += GetTextWithoutTag(combinedText);
}
else if (StringStartsWithTag(combinedText, "[SCRIPT]"))
{
if (!string.IsNullOrEmpty(currentEntry.userScript)) currentEntry.userScript += ";\n";
currentEntry.userScript += GetTextWithoutTag(combinedText);
}
else
{
currentEntry.Title = combinedText;
}
break;
case "cxdirective":
if (StringStartsWithTag(combinedText, "[COND]"))
{
if (!string.IsNullOrEmpty(currentEntry.conditionsString)) currentEntry.conditionsString += ";\n";
currentEntry.conditionsString += GetTextWithoutTag(combinedText);
}
else if (StringStartsWithTag(combinedText, "[SCRIPT]"))
{
if (!string.IsNullOrEmpty(currentEntry.userScript)) currentEntry.userScript += ";\n";
currentEntry.userScript += GetTextWithoutTag(combinedText);
}
else
{
currentEntry.Sequence = combinedText;
}
break;
case "cxcharacter_item":
currentEntry.ActorID = GetActorForCharacterItemContent(contentObject).id;
if (currentEntry.ActorID == conversation.ActorID)
{
currentEntry.ConversantID = conversation.ConversantID;
}
else
{
currentEntry.ConversantID = conversation.ActorID;
}
break;
case "cxparenthetical":
if (StringStartsWithTag(combinedText, "[C]"))
{
currentEntry.ConversantID = database.GetActor(GetTextWithoutTag(combinedText).ToUpper()).id;
}
else if (StringStartsWithTag(combinedText, "[VO]"))
{
AppendToField(currentEntry.fields, CeltxFields.VoiceOverFile, GetTextWithoutTag(combinedText), FieldType.Files);
}
else
{
currentEntry.MenuText = combinedText;
}
break;
case "cxdialog":
currentEntry.DialogueText = combinedText;
createNewEntry = true;
break;
}
breakdownEntry = currentEntry;
}
}
catch (System.Exception e)
{
LogError(MethodBase.GetCurrentMethod(), e, initialNodeEntry.Title);
}
return currentEntry;
}
private void AddSetVariableScriptForNode(DialogueEntry entry, dynamic customVars)
{
StringBuilder luaScript = new StringBuilder();
foreach (var customVar in customVars)
{
var variableId = (string)customVar.id;
var newValue = customVar.value;
if (luaScript.Length != 0) { luaScript.Append("; "); }
if (!variableLookupViaCeltxId.ContainsKey(variableId)) Debug.LogError("Celtx Import: Can't find variable with ID " + variableId);
Variable var = variableLookupViaCeltxId[variableId];
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("Variable");
stringBuilder.Append("[\"" + var.Name + "\"]");
stringBuilder.Append(" = ");
if (var.Type == FieldType.Boolean)
{
stringBuilder.Append(((string)newValue).ToLower());
}
else if (var.Type == FieldType.Number)
{
stringBuilder.Append((string)newValue);
}
else if (var.Type == FieldType.Text)
{
if (radioVarOptionsLookupViaCeltxId.ContainsKey(variableId))
{
stringBuilder.Append(radioVarOptionsLookupViaCeltxId[variableId][(int)newValue].value);
}
else
{
stringBuilder.Append("\"" + (string)newValue + "\"");
}
}
luaScript.Append(stringBuilder.ToString());
}
entry.userScript = luaScript.ToString();
}
private List<Actor> GetCharactersFromRootPage(dynamic scriptObject)
{
List<Actor> actors = new List<Actor>();
dynamic pageContent = null;
if (scriptObject != null)
{
foreach (var contentLv0 in scriptObject.content)
{
if (contentLv0 != null)
{
foreach (var contentLv1 in contentLv0.content)
{
if (contentLv1 != null)
{
pageContent = contentLv1.content;
break;
}
}
break;
}
}
}
if (pageContent != null)
{
foreach (var contentObject in pageContent)
{
if (contentObject == null) continue;
if (GetContentType(contentObject).Equals("cxcharacter_item"))
{
actors.Add(actorLookupViaCeltxId[(string)contentObject.attrs.catalog_id]);
}
}
}
return actors;
}
private string GetContentType(dynamic contentObject)
{
return (string)contentObject.type;
}
private dynamic GetNodeScriptObject(string nodeId, dynamic laneSubdocument)
{
return laneSubdocument.scripts[nodeId];
}
private Actor GetActorForCharacterItemContent(dynamic characterObject)
{
var characterCatalogId = characterObject.attrs.catalog_id;
return actorLookupViaCeltxId[(string)characterCatalogId];
}
#endregion
#region Text Processing
private bool StringStartsWithTag(string stringToCheck, string targetTag)
{
if (stringToCheck == null || targetTag == null) { return false; }
return stringToCheck.StartsWith(targetTag, System.StringComparison.OrdinalIgnoreCase);
}
private string GetTextWithoutTag(string text)
{
if (string.IsNullOrEmpty(text)) { return ""; }
//return System.Text.RegularExpressions.Regex.Split(text, "(\\[.*\\])")[2].Trim();
var endTagPos = text.IndexOf(']');
return (endTagPos == -1) ? "" : text.Substring(endTagPos + 1);
}
private string GetContentText(dynamic contentObject, DialogueEntry currentEntry, DialogueEntry breakdownEntry)
{
StringBuilder stringBuilder = new StringBuilder();
if (contentObject != null && contentObject.content != null)
{
foreach (var subContentObject in contentObject.content)
{
if (GetContentType(subContentObject).Equals("text"))
{
stringBuilder.Append(GetMarkedText(subContentObject, currentEntry, breakdownEntry));
}
}
}
return stringBuilder.ToString();
}
private string GetMarkedText(dynamic textContent, DialogueEntry currentEntry, DialogueEntry breakdownEntry)
{
try
{
string text = textContent.text;
if (textContent.marks != null && textContent.marks.Count > 0)
{
string prependString = "";
string appendString = "";
foreach (var mark in textContent.marks)
{
string markType = (string)mark.type;
switch (markType)
{
case "strong":
prependString = prependString + "<b>";
appendString = "</b>" + appendString;
break;
case "em":
prependString = prependString + "<i>";
appendString = "</i>" + appendString;
break;
case "underline":
prependString = prependString + "<u>";
appendString = "</u>" + appendString;
break;
case "strikethrough":
prependString = prependString + "<s>";
appendString = "</s>" + appendString;
break;
case "cxbreakdown":
if (breakdownEntry != null)
{
var breakdown_attrs = mark.attrs;
string attrName = breakdown_attrs.name;
string type = (string)breakdown_attrs.type;
string catalog_id = (string)breakdown_attrs.catalog_id;
AddBreakdownField(currentEntry, breakdownEntry, mark.id, type, catalog_id, attrName);
}
break;
case "cxmultitagbreakdown":
if (breakdownEntry != null)
{
var multitagbreakdown_attrs = mark.attrs;
foreach (var asset in multitagbreakdown_attrs.assetList)
{
string attrName = multitagbreakdown_attrs.name;
string breakdown_id = (string)asset;
string multitag_type;
if (catalogTypeByBreakdownId.TryGetValue(breakdown_id, out multitag_type))
{
string multitag_catalog_id;
if (catalogIdByBreakdownId.TryGetValue(breakdown_id, out multitag_catalog_id))
{
AddBreakdownField(currentEntry, breakdownEntry, breakdown_id, multitag_type, multitag_catalog_id, attrName);
}
}
}
}
break;
}
}
return prependString + text + appendString;
}
else
{
return text;
}
}
catch (System.Exception e)
{
LogError(MethodBase.GetCurrentMethod(), e, textContent.attrs.id);
return "";
}
}
private void AddBreakdownField(DialogueEntry currentEntry, DialogueEntry breakdownEntry, string breakdown_id,
string type, string catalog_id, string attrName)
{
switch (type)
{
case "character":
AddActorBreakdownField(currentEntry, catalog_id);
break;
case "item":
AddItemBreakdownField(breakdownEntry, catalog_id);
break;
case "location":
AddLocationBreakdownField(breakdownEntry, catalog_id);
break;
default:
AddCustomBreakdownField(breakdownEntry, breakdown_id, catalog_id);
break;
}
}
private void AddToField(List<Field> fields, string fieldTitle, string value, FieldType fieldType)
{
Field field = Field.Lookup(fields, fieldTitle);
if (field != null)
{
if (string.IsNullOrEmpty(field.value))
{
field.value = value;
}
else
{
if (value.Contains(';')) Debug.LogWarning($"Appending '{value}' to {fieldTitle} using ';' as separator, but '{value}' also contains ';'");
field.value += ';' + value;
}
field.type = fieldType;
}
else
{
fields.Add(new Field(fieldTitle, value, fieldType));
}
}
private void AddActorBreakdownField(DialogueEntry currentEntry, string catalog_id)
{
Actor actor;
if (actorLookupViaCeltxId.TryGetValue(catalog_id, out actor))
{
string fieldTitle = GetAvailableFieldTitle(currentEntry, "BreakdownActor");
AddToField(currentEntry.fields, fieldTitle, actor.id.ToString(), FieldType.Actor);
}
else
{
Debug.LogWarning($"Can't find actor with Celtx ID {catalog_id} to reference in breakdown.");
}
}
private void AddItemBreakdownField(DialogueEntry currentEntry, string catalog_id)
{
Item item;
if (itemLookupViaCeltxId.TryGetValue(catalog_id, out item))
{
string fieldTitle = GetAvailableFieldTitle(currentEntry, "BreakdownItem");
AddToField(currentEntry.fields, fieldTitle, item.id.ToString(), FieldType.Item);
}
else
{
Debug.LogWarning($"Can't find item with Celtx ID {catalog_id} to reference in breakdown.");
}
}
private void AddLocationBreakdownField(DialogueEntry currentEntry, string catalog_id)
{
Location location;
if (locationLookupViaCeltxId.TryGetValue(catalog_id, out location))
{
string fieldTitle = GetAvailableFieldTitle(currentEntry, "BreakdownLocation");
AddToField(currentEntry.fields, fieldTitle, location.id.ToString(), FieldType.Location);
}
else
{
Debug.LogWarning($"Can't find item with Celtx ID {catalog_id} to reference in breakdown.");
}
}
private void AddCustomBreakdownField(DialogueEntry currentEntry, string breakdown_id, string catalog_id)
{
string customName = null;
var custom = celtxDataObject.subdocuments.catalog.custom;
foreach (var customObject in custom)
{
var customData = customObject.Value;
string customId = (string) customData.id;
if (string.Equals(customId, catalog_id))
{
customName = (string) customData.name;
break;
}
}
string customType;
if (customName != null && customTypeByBreakdownId.TryGetValue(breakdown_id, out customType))
{
if (string.IsNullOrEmpty(customType)) customType = GetAvailableFieldTitle(currentEntry, "BreakdownCustom");
AddToField(currentEntry.fields, customType, customName, FieldType.Text);
}
//--- By customer request, we use breakdown name in field instead of custom value.
////---Was: (But in JSON multiple breakdown types share the same catalog_id. Bug?)
//var custom = celtxDataObject.subdocuments.catalog.custom;
//foreach (var customObject in custom)
//{
// var customData = customObject.Value;
// string customId = (string)customData.id;
// if (string.Equals(customId, catalog_id))
// {
// string customName = (string)customData.name;
// string fieldTitle = (string)customData.custom_type;
// if (string.IsNullOrEmpty(fieldTitle)) fieldTitle = GetAvailableFieldTitle(currentEntry, "BreakdownCustom");
// AddToField(currentEntry.fields, fieldTitle, customName, FieldType.Text);
// break;
// }
//}
}
private string GetAvailableFieldTitle(DialogueEntry currentEntry, string defaultFieldTitle)
{
if (!Field.FieldExists(currentEntry.fields, defaultFieldTitle)) return defaultFieldTitle;
int num = 2;
int safeguard = 0;
while (Field.FieldExists(currentEntry.fields, defaultFieldTitle + num) && ++safeguard < 99)
{
num++;
}
return defaultFieldTitle + num;
}
#endregion
#region Logging
private string GetNullDataString(string nullFieldName)
{
return nullFieldName + " is null - ";
}
private string GetErrorString(MethodBase methodName, System.Exception ex)
{
return "Error in " + methodName + " : " + ex.ToString();
}
private void LogError(MethodBase methodName, System.Exception ex, string celtxId = null, string name = null)
{
string messageCore = GetErrorString(methodName, ex);
LogMessage(LogLevel.E, messageCore, celtxId, name);
}
private void LogWarning(string messageCore, string celtxId, string name)
{
LogMessage(LogLevel.W, messageCore, celtxId, name);
}
private void LogMessage(LogLevel logLevel, string messageCore, string celtxId = null, string name = null)
{
StringBuilder logMessage = new StringBuilder();
if (celtxId != null) { logMessage.Append("Celtx Object : " + celtxId + "(" + name + ")"); }
logMessage.Append(" | " + messageCore);
if (logLevel == LogLevel.W)
{
Debug.LogWarning(logMessage.ToString());
}
else if (logLevel == LogLevel.E)
{
Debug.LogError(logMessage.ToString());
}
else
{
Debug.Log(logMessage.ToString());
}
}
enum LogLevel
{
W, E, I
}
#endregion
#region TODO Items
// TODO: Waiting on Celtx team
//private bool IsPlayerCharacter(CxAttrs characterAttrs)
//{
// try
// {
// if (characterAttrs.item_data == null)
// {
// LogWarning(GetNullDataString("attrs.item_data") +
// "Cannot determine if character is a player. Please ensure Character Type is set in the catalog", characterAttrs.id, characterAttrs.title);
// return false;
// }
// if (characterAttrs.item_data.character_type == null)
// {
// LogWarning(GetNullDataString("attrs.item_data.character_type") +
// "Defaulting to non-player Actor. Please ensure Character Type is set in the catalog", characterAttrs.id, characterAttrs.title);
// return false;
// }
// return characterAttrs.item_data.character_type.Equals("pc");
// }
// catch (System.Exception e)
// {
// LogError(MethodBase.GetCurrentMethod(), e, characterAttrs.id, characterAttrs.title);
// }
// return false;
//}
#endregion
}
}
#endif