Dialogue addition (#788)

## Minor features
- New `pathToExistingDialogue` field for adding new DialogueNodes and
DialogueOptions to existing character dialogues. Just add new responses
to Slate's dialogue instead of replacing it! Will improve mod compat and
simplify translating.
This commit is contained in:
xen-42 2024-02-26 01:41:19 -05:00 committed by GitHub
commit 4a2a7636e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 116 additions and 8 deletions

View File

@ -5,9 +5,9 @@ using NewHorizons.Utility;
using NewHorizons.Utility.OuterWilds; using NewHorizons.Utility.OuterWilds;
using NewHorizons.Utility.OWML; using NewHorizons.Utility.OWML;
using OWML.Common; using OWML.Common;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Xml; using System.Xml;
using System.Xml.Linq;
using UnityEngine; using UnityEngine;
namespace NewHorizons.Builder.Props namespace NewHorizons.Builder.Props
@ -24,6 +24,18 @@ namespace NewHorizons.Builder.Props
// Create dialogue directly from xml string instead of loading it from a file // Create dialogue directly from xml string instead of loading it from a file
public static (CharacterDialogueTree, RemoteDialogueTrigger) Make(GameObject go, Sector sector, DialogueInfo info, string xml, string dialogueName) public static (CharacterDialogueTree, RemoteDialogueTrigger) Make(GameObject go, Sector sector, DialogueInfo info, string xml, string dialogueName)
{
if (string.IsNullOrEmpty(info.pathToExistingDialogue))
{
return MakeNewDialogue(go, sector, info, xml, dialogueName);
}
else
{
return (AddToExistingDialogue(info, xml), null);
}
}
private static (CharacterDialogueTree, RemoteDialogueTrigger) MakeNewDialogue(GameObject go, Sector sector, DialogueInfo info, string xml, string dialogueName)
{ {
NHLogger.LogVerbose($"[DIALOGUE] Created a new character dialogue [{info.rename}] on [{info.parentPath}]"); NHLogger.LogVerbose($"[DIALOGUE] Created a new character dialogue [{info.rename}] on [{info.parentPath}]");
@ -54,6 +66,68 @@ namespace NewHorizons.Builder.Props
return (dialogue, remoteTrigger); return (dialogue, remoteTrigger);
} }
private static CharacterDialogueTree AddToExistingDialogue(DialogueInfo info, string xml)
{
AddTranslation(xml);
var existingDialogue = SearchUtilities.Find(info.pathToExistingDialogue)?.GetComponent<CharacterDialogueTree>();
if (existingDialogue == null)
{
NHLogger.LogError($"Couldn't find dialogue at {info.pathToExistingDialogue}!");
return null;
}
var existingText = existingDialogue._xmlCharacterDialogueAsset.text;
var existingDialogueDoc = new XmlDocument();
existingDialogueDoc.LoadXml(existingText);
var existingDialogueTree = existingDialogueDoc.DocumentElement.SelectSingleNode("//DialogueTree");
var existingDialogueNodesByName = new Dictionary<string, XmlNode>();
foreach (XmlNode existingDialogueNode in existingDialogueTree.GetChildNodes("DialogueNode"))
{
var name = existingDialogueNode.GetChildNode("Name").InnerText;
existingDialogueNodesByName[name] = existingDialogueNode;
}
var additionalDialogueDoc = new XmlDocument();
additionalDialogueDoc.LoadXml(xml);
var newDialogueNodes = additionalDialogueDoc.DocumentElement.SelectSingleNode("//DialogueTree").GetChildNodes("DialogueNode");
foreach (XmlNode newDialogueNode in newDialogueNodes)
{
var name = newDialogueNode.GetChildNode("Name").InnerText;
if (existingDialogueNodesByName.TryGetValue(name, out var existingNode))
{
// We just have to merge the dialogue options
var dialogueOptions = newDialogueNode.GetChildNode("DialogueOptionsList").GetChildNodes("DialogueOption");
var existingDialogueOptionsList = existingNode.GetChildNode("DialogueOptionsList");
foreach (XmlNode node in dialogueOptions)
{
var importedNode = existingDialogueOptionsList.OwnerDocument.ImportNode(node, true);
existingDialogueOptionsList.AppendChild(importedNode);
}
}
else
{
// We add the new dialogue node to the existing dialogue
var importedNode = existingDialogueTree.OwnerDocument.ImportNode(newDialogueNode, true);
existingDialogueTree.AppendChild(importedNode);
}
}
var newTextAsset = new TextAsset(existingDialogueDoc.OuterXml)
{
name = existingDialogue._xmlCharacterDialogueAsset.name
};
existingDialogue.SetTextXml(newTextAsset);
return existingDialogue;
}
private static RemoteDialogueTrigger MakeRemoteDialogueTrigger(GameObject planetGO, Sector sector, DialogueInfo info, CharacterDialogueTree dialogue) private static RemoteDialogueTrigger MakeRemoteDialogueTrigger(GameObject planetGO, Sector sector, DialogueInfo info, CharacterDialogueTree dialogue)
{ {
var conversationTrigger = GeneralPropBuilder.MakeNew("ConversationTrigger", planetGO, sector, info.remoteTrigger, defaultPosition: info.position, defaultParentPath: info.pathToAnimController); var conversationTrigger = GeneralPropBuilder.MakeNew("ConversationTrigger", planetGO, sector, info.remoteTrigger, defaultPosition: info.position, defaultParentPath: info.pathToAnimController);

View File

@ -31,6 +31,11 @@ namespace NewHorizons.External.Modules.Props.Dialogue
/// </summary> /// </summary>
public string pathToAnimController; public string pathToAnimController;
/// <summary>
/// If this dialogue is adding to existing character dialogue, put a path to the game object with the dialogue on it here
/// </summary>
public string pathToExistingDialogue;
/// <summary> /// <summary>
/// Radius of the spherical collision volume where you get the "talk to" prompt when looking at. If you use a /// Radius of the spherical collision volume where you get the "talk to" prompt when looking at. If you use a
/// remoteTrigger, you can set this to 0 to make the dialogue only trigger remotely. /// remoteTrigger, you can set this to 0 to make the dialogue only trigger remotely.

View File

@ -121,22 +121,36 @@ namespace NewHorizons.Handlers
} }
} }
public static (string, string) FixKeyValue(string key, string value)
{
key = key.Replace("&lt;", "<").Replace("&gt;", ">").Replace("<![CDATA[", "").Replace("]]>", "");
value = value.Replace("&lt;", "<").Replace("&gt;", ">").Replace("<![CDATA[", "").Replace("]]>", "");
return (key, value);
}
public static void AddDialogue(string rawText, bool trimRawTextForKey = false, params string[] rawPreText) public static void AddDialogue(string rawText, bool trimRawTextForKey = false, params string[] rawPreText)
{ {
var key = string.Join(string.Empty, rawPreText) + (trimRawTextForKey? rawText.Trim() : rawText); var key = string.Join(string.Empty, rawPreText) + (trimRawTextForKey? rawText.Trim() : rawText);
var text = GetTranslation(rawText, TextType.DIALOGUE); var value = GetTranslation(rawText, TextType.DIALOGUE);
TextTranslation.Get().m_table.Insert(key, text); // Manually insert directly into the dictionary, otherwise it logs errors about duplicates but we want to allow replacing
(key, value) = FixKeyValue(key, value);
TextTranslation.Get().m_table.theTable[key] = value;
} }
public static void AddShipLog(string rawText, params string[] rawPreText) public static void AddShipLog(string rawText, params string[] rawPreText)
{ {
var key = string.Join(string.Empty, rawPreText) + rawText; var key = string.Join(string.Empty, rawPreText) + rawText;
string text = GetTranslation(rawText, TextType.SHIPLOG); string value = GetTranslation(rawText, TextType.SHIPLOG);
TextTranslation.Get().m_table.InsertShipLog(key, text); // Manually insert directly into the dictionary, otherwise it logs errors about duplicates but we want to allow replacing
(key, value) = FixKeyValue(key, value);
TextTranslation.Get().m_table.theShipLogTable[key] = value;
} }
public static int AddUI(string rawText) public static int AddUI(string rawText)
@ -154,7 +168,7 @@ namespace NewHorizons.Handlers
} }
catch (Exception) { } catch (Exception) { }
TextTranslation.Get().m_table.Insert_UI(key, text); TextTranslation.Get().m_table.theUITable[key] = text;
return key; return key;
} }

View File

@ -1568,6 +1568,10 @@
"type": "string", "type": "string",
"description": "If this dialogue is meant for a character, this is the relative path from the planet to that character's\nCharacterAnimController, TravelerController, TravelerEyeController (eye of the universe), FacePlayerWhenTalking, \nHearthianRecorderEffects or SolanumAnimController.\n\nIf it's a Recorder this will also delete the existing dialogue already attached to that prop.\n\nIf none of those components are present it will add a FacePlayerWhenTalking component." "description": "If this dialogue is meant for a character, this is the relative path from the planet to that character's\nCharacterAnimController, TravelerController, TravelerEyeController (eye of the universe), FacePlayerWhenTalking, \nHearthianRecorderEffects or SolanumAnimController.\n\nIf it's a Recorder this will also delete the existing dialogue already attached to that prop.\n\nIf none of those components are present it will add a FacePlayerWhenTalking component."
}, },
"pathToExistingDialogue": {
"type": "string",
"description": "If this dialogue is adding to existing character dialogue, put a path to the game object with the dialogue on it here"
},
"radius": { "radius": {
"type": "number", "type": "number",
"description": "Radius of the spherical collision volume where you get the \"talk to\" prompt when looking at. If you use a\nremoteTrigger, you can set this to 0 to make the dialogue only trigger remotely.", "description": "Radius of the spherical collision volume where you get the \"talk to\" prompt when looking at. If you use a\nremoteTrigger, you can set this to 0 to make the dialogue only trigger remotely.",

View File

@ -12,6 +12,7 @@ using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Xml;
using UnityEngine; using UnityEngine;
using static NewHorizons.External.Modules.ParticleFieldModule; using static NewHorizons.External.Modules.ParticleFieldModule;
using NomaiCoordinates = NewHorizons.External.Configs.StarSystemConfig.NomaiCoordinates; using NomaiCoordinates = NewHorizons.External.Configs.StarSystemConfig.NomaiCoordinates;
@ -24,7 +25,7 @@ namespace NewHorizons.Utility
{ {
NullValueHandling = NullValueHandling.Ignore, NullValueHandling = NullValueHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore,
Formatting = Formatting.Indented, Formatting = Newtonsoft.Json.Formatting.Indented,
}; };
private static StringBuilder stringBuilder = new StringBuilder(); private static StringBuilder stringBuilder = new StringBuilder();
@ -36,7 +37,7 @@ namespace NewHorizons.Utility
{ {
using (JsonTextWriter jsonTextWriter = new JsonTextWriter(stringWriter) using (JsonTextWriter jsonTextWriter = new JsonTextWriter(stringWriter)
{ {
Formatting = Formatting.Indented, Formatting = Newtonsoft.Json.Formatting.Indented,
IndentChar = '\t', IndentChar = '\t',
Indentation = 1 Indentation = 1
}) })
@ -339,5 +340,15 @@ namespace NewHorizons.Utility
} }
} }
} }
public static List<XmlNode> GetChildNodes(this XmlNode parentNode, string tagName)
{
return parentNode.ChildNodes.Cast<XmlNode>().Where(node => node.LocalName == tagName).ToList();
}
public static XmlNode GetChildNode(this XmlNode parentNode, string tagName)
{
return parentNode.ChildNodes.Cast<XmlNode>().First(node => node.LocalName == tagName);
}
} }
} }