diff --git a/NewHorizons/Builder/Props/DetailBuilder.cs b/NewHorizons/Builder/Props/DetailBuilder.cs index 6a05cb090..65a37584c 100644 --- a/NewHorizons/Builder/Props/DetailBuilder.cs +++ b/NewHorizons/Builder/Props/DetailBuilder.cs @@ -156,7 +156,7 @@ public static GameObject Make(GameObject go, Sector sector, IModBehaviour mod, G // If they're adding dialogue we have to manually register the xml text if (isFromAssetBundle && component is CharacterDialogueTree dialogue) { - DialogueBuilder.AddTranslation(dialogue._xmlCharacterDialogueAsset.text, null); + DialogueBuilder.HandleUnityCreatedDialogue(dialogue); } FixComponent(component, go, detail.ignoreSun); diff --git a/NewHorizons/Builder/Props/DialogueBuilder.cs b/NewHorizons/Builder/Props/DialogueBuilder.cs index 429626e55..89d70da7a 100644 --- a/NewHorizons/Builder/Props/DialogueBuilder.cs +++ b/NewHorizons/Builder/Props/DialogueBuilder.cs @@ -5,10 +5,14 @@ using NewHorizons.Utility.OuterWilds; using NewHorizons.Utility.OWML; using OWML.Common; +using System; using System.Collections.Generic; +using System.Data.SqlTypes; using System.IO; using System.Xml; +using System.Xml.Linq; using UnityEngine; +using UnityEngine.XR; namespace NewHorizons.Builder.Props { @@ -102,7 +106,11 @@ private static CharacterDialogueTree AddToExistingDialogue(DialogueInfo info, st // We just have to merge the dialogue options var dialogueOptions = newDialogueNode.GetChildNode("DialogueOptionsList").GetChildNodes("DialogueOption"); var existingDialogueOptionsList = existingNode.GetChildNode("DialogueOptionsList"); - var firstNode = existingDialogueOptionsList.ChildNodes[0]; + if (existingDialogueOptionsList == null) + { + existingDialogueOptionsList = existingDialogueDoc.CreateElement("DialogueOptionsList"); + existingNode.AppendChild(existingDialogueOptionsList); + } foreach (XmlNode node in dialogueOptions) { var importedNode = existingDialogueOptionsList.OwnerDocument.ImportNode(node, true); @@ -118,6 +126,10 @@ private static CharacterDialogueTree AddToExistingDialogue(DialogueInfo info, st } } + // Character name is required for adding translations, something to do with how OW prefixes its dialogue + var characterName = existingDialogueTree.SelectSingleNode("NameField").InnerText; + AddTranslation(additionalDialogueDoc.GetChildNode("DialogueTree"), characterName); + var newTextAsset = new TextAsset(existingDialogueDoc.OuterXml) { name = existingDialogue._xmlCharacterDialogueAsset.name @@ -125,13 +137,84 @@ private static CharacterDialogueTree AddToExistingDialogue(DialogueInfo info, st existingDialogue.SetTextXml(newTextAsset); - // Chracter name is required for adding translations, something to do with how OW prefixes its dialogue - var characterName = existingDialogueTree.SelectSingleNode("NameField").InnerText; - AddTranslation(xml, characterName); + FixDialogueNextFrame(existingDialogue); return existingDialogue; } + private static void FixDialogueNextFrame(CharacterDialogueTree characterDialogueTree) + { + Delay.FireOnNextUpdate(() => + { + var rawText = characterDialogueTree._xmlCharacterDialogueAsset.text; + + var doc = new XmlDocument(); + doc.LoadXml(rawText); + var dialogueTree = doc.DocumentElement.SelectSingleNode("//DialogueTree"); + + DoDialogueOptionsListReplacement(dialogueTree); + + var newTextAsset = new TextAsset(doc.OuterXml) + { + name = characterDialogueTree._xmlCharacterDialogueAsset.name + }; + + characterDialogueTree.SetTextXml(newTextAsset); + }); + } + + /// + /// Always call this after adding translations, else it won't update them properly + /// + /// + private static void DoDialogueOptionsListReplacement(XmlNode dialogueTree) + { + var optionsListsByName = new Dictionary(); + var dialogueNodes = dialogueTree.GetChildNodes("DialogueNode"); + foreach (XmlNode dialogueNode in dialogueNodes) + { + var optionsList = dialogueNode.GetChildNode("DialogueOptionsList"); + if (optionsList != null) + { + var name = dialogueNode.GetChildNode("Name").InnerText; + optionsListsByName[name] = optionsList; + } + } + foreach (var (name, optionsList) in optionsListsByName) + { + var replacement = optionsList.GetChildNode("ReuseDialogueOptionsListFrom"); + if (replacement != null) + { + var replacementName = replacement.InnerText; + if (optionsListsByName.TryGetValue(replacementName, out var replacementOptionsList)) + { + if (replacementOptionsList.GetChildNode("ReuseDialogueOptionsListFrom") != null) + { + NHLogger.LogError($"Can not target a node with ReuseDialogueOptionsListFrom that also reuses options when making dialogue. Node {name} cannot reuse the list from {replacement.InnerText}"); + } + var dialogueNode = optionsList.ParentNode; + dialogueNode.RemoveChild(optionsList); + dialogueNode.AppendChild(replacementOptionsList.Clone()); + + // Have to manually fix the translations here + var characterName = dialogueTree.SelectSingleNode("NameField").InnerText; + + var xmlText = replacementOptionsList.SelectNodes("DialogueOption/Text"); + foreach (object option in xmlText) + { + var optionData = (XmlNode)option; + var text = optionData.InnerText.Trim(); + TranslationHandler.ReuseDialogueTranslation(text, new string[] { characterName, replacementName }, new string[] { characterName, name }); + } + } + else + { + NHLogger.LogError($"Could not reuse dialogue options list from node with Name {replacement.InnerText} to node with Name {name}"); + } + } + } + } + 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); @@ -187,13 +270,19 @@ private static CharacterDialogueTree MakeConversationZone(GameObject planetGO, S var dialogueTree = conversationZone.AddComponent(); + var dialogueDoc = new XmlDocument(); + dialogueDoc.LoadXml(xml); + var xmlNode = dialogueDoc.SelectSingleNode("DialogueTree"); + AddTranslation(xmlNode); + + xml = xmlNode.OuterXml; + var text = new TextAsset(xml) { // Text assets need a name to be used with VoiceMod name = dialogueName }; dialogueTree.SetTextXml(text); - AddTranslation(xml); switch (info.flashlightToggle) { @@ -214,6 +303,8 @@ private static CharacterDialogueTree MakeConversationZone(GameObject planetGO, S conversationZone.SetActive(true); + FixDialogueNextFrame(dialogueTree); + return dialogueTree; } @@ -378,11 +469,17 @@ private static void MakePlayerTrackingZone(GameObject go, CharacterDialogueTree } } + [Obsolete("Pass in the DialogueTree XmlNode instead, this is still here because Pikpik was using it in EOTP")] public static void AddTranslation(string xml, string characterName = null) { var xmlDocument = new XmlDocument(); xmlDocument.LoadXml(xml); var xmlNode = xmlDocument.SelectSingleNode("DialogueTree"); + AddTranslation(xmlNode, characterName); + } + + public static void AddTranslation(XmlNode xmlNode, string characterName = null) + { var xmlNodeList = xmlNode.SelectNodes("DialogueNode"); // When adding dialogue to existing stuff, we have to pass in the character name @@ -417,5 +514,21 @@ public static void AddTranslation(string xml, string characterName = null) } } } + + public static void HandleUnityCreatedDialogue(CharacterDialogueTree dialogue) + { + var text = dialogue._xmlCharacterDialogueAsset.text; + var dialogueDoc = new XmlDocument(); + dialogueDoc.LoadXml(text); + var xmlNode = dialogueDoc.SelectSingleNode("DialogueTree"); + AddTranslation(xmlNode, null); + var newTextAsset = new TextAsset(dialogueDoc.OuterXml) + { + name = dialogue._xmlCharacterDialogueAsset.name + }; + dialogue.SetTextXml(newTextAsset); + + FixDialogueNextFrame(dialogue); + } } } diff --git a/NewHorizons/Components/Fixers/PlayerShipAtmosphereDetectorFix.cs b/NewHorizons/Components/Fixers/PlayerShipAtmosphereDetectorFix.cs index 25d135c16..1eb440d9a 100644 --- a/NewHorizons/Components/Fixers/PlayerShipAtmosphereDetectorFix.cs +++ b/NewHorizons/Components/Fixers/PlayerShipAtmosphereDetectorFix.cs @@ -14,7 +14,11 @@ internal class PlayerShipAtmosphereDetectorFix : MonoBehaviour public void Start() { _fluidDetector = Locator.GetPlayerCameraDetector().GetComponent(); - _shipAtmosphereVolume = Locator.GetShipBody().transform.Find("Volumes/ShipAtmosphereVolume").GetComponent(); + _shipAtmosphereVolume = Locator.GetShipBody()?.transform?.Find("Volumes/ShipAtmosphereVolume")?.GetComponent(); + if (_shipAtmosphereVolume == null) + { + Destroy(this); + } } public void Update() diff --git a/NewHorizons/Components/ShipLog/ShipLogStarChartMode.cs b/NewHorizons/Components/ShipLog/ShipLogStarChartMode.cs index a1d466eb5..57d48c35d 100644 --- a/NewHorizons/Components/ShipLog/ShipLogStarChartMode.cs +++ b/NewHorizons/Components/ShipLog/ShipLogStarChartMode.cs @@ -282,7 +282,7 @@ private void SetWarpTarget(ShipLogEntryCard shipLogEntryCard) var name = UniqueIDToName(shipLogEntryCard.name); - var warpNotificationDataText = TranslationHandler.GetTranslation("WARP_LOCKED", TranslationHandler.TextType.UI).Replace("{0}", name.ToUpper()); + var warpNotificationDataText = TranslationHandler.GetTranslation("WARP_LOCKED", TranslationHandler.TextType.UI).Replace("{0}", name.ToUpperFixed()); _warpNotificationData = new NotificationData(warpNotificationDataText); NotificationManager.SharedInstance.PostNotification(_warpNotificationData, true); diff --git a/NewHorizons/Handlers/TranslationHandler.cs b/NewHorizons/Handlers/TranslationHandler.cs index c2f49ebf8..43f787a05 100644 --- a/NewHorizons/Handlers/TranslationHandler.cs +++ b/NewHorizons/Handlers/TranslationHandler.cs @@ -173,6 +173,26 @@ public static void AddDialogue(string rawText, bool trimRawTextForKey = false, p TextTranslation.Get().m_table.theTable[key] = value; } + /// + /// Two dialogue nodes might share indentical text but they will have different prefixes. Still, we want to reuse that old text. + /// + /// + /// + /// + public static void ReuseDialogueTranslation(string rawText, string[] oldPrefixes, string[] newPrefixes) + { + var key = string.Join(string.Empty, newPrefixes) + rawText; + var existingKey = string.Join(string.Empty, oldPrefixes) + rawText; + if (TextTranslation.Get().m_table.theTable.TryGetValue(existingKey, out var existingTranslation)) + { + TextTranslation.Get().m_table.theTable[key] = existingTranslation; + } + else + { + NHLogger.LogWarning($"Couldn't find translation key {existingKey}"); + } + } + public static void AddShipLog(string rawText, params string[] rawPreText) { var key = string.Join(string.Empty, rawPreText) + rawText; @@ -189,7 +209,7 @@ public static int AddUI(string rawText) { var uiTable = TextTranslation.Get().m_table.theUITable; - var text = GetTranslation(rawText, TextType.UI).ToUpper(); + var text = GetTranslation(rawText, TextType.UI).ToUpperFixed(); var key = uiTable.Keys.Max() + 1; try diff --git a/NewHorizons/Patches/SignalPatches/AudioSignalPatches.cs b/NewHorizons/Patches/SignalPatches/AudioSignalPatches.cs index 9f1aacfe3..58beba598 100644 --- a/NewHorizons/Patches/SignalPatches/AudioSignalPatches.cs +++ b/NewHorizons/Patches/SignalPatches/AudioSignalPatches.cs @@ -2,6 +2,7 @@ using NewHorizons.Builder.Props.Audio; using NewHorizons.External; using NewHorizons.Handlers; +using NewHorizons.Utility; using System; using UnityEngine; @@ -17,7 +18,7 @@ public static bool AudioSignal_SignalNameToString(SignalName name, ref string __ var customSignalName = SignalBuilder.GetCustomSignalName(name); if (!string.IsNullOrEmpty(customSignalName)) { - __result = TranslationHandler.GetTranslation(customSignalName, TranslationHandler.TextType.UI, false).ToUpper(); + __result = TranslationHandler.GetTranslation(customSignalName, TranslationHandler.TextType.UI, false).ToUpperFixed(); return false; } return true; @@ -68,7 +69,7 @@ public static bool AudioSignal_FrequencyToString(SignalFrequency frequency, ref var customName = SignalBuilder.GetCustomFrequencyName(frequency); if (!string.IsNullOrEmpty(customName)) { - if (NewHorizonsData.KnowsFrequency(customName)) __result = TranslationHandler.GetTranslation(customName, TranslationHandler.TextType.UI, false).ToUpper(); + if (NewHorizonsData.KnowsFrequency(customName)) __result = TranslationHandler.GetTranslation(customName, TranslationHandler.TextType.UI, false).ToUpperFixed(); else __result = UITextLibrary.GetString(UITextType.SignalFreqUnidentified); return false; } diff --git a/NewHorizons/Utility/DebugTools/DebugReload.cs b/NewHorizons/Utility/DebugTools/DebugReload.cs index cd067d986..c40d76b8a 100644 --- a/NewHorizons/Utility/DebugTools/DebugReload.cs +++ b/NewHorizons/Utility/DebugTools/DebugReload.cs @@ -15,7 +15,7 @@ public static class DebugReload public static void InitializePauseMenu(IPauseMenuManager pauseMenu) { - _reloadButton = pauseMenu.MakeSimpleButton(TranslationHandler.GetTranslation("Reload Configs", TranslationHandler.TextType.UI).ToUpper(), 3, true); + _reloadButton = pauseMenu.MakeSimpleButton(TranslationHandler.GetTranslation("Reload Configs", TranslationHandler.TextType.UI).ToUpperFixed(), 3, true); _reloadButton.OnSubmitAction += ReloadConfigs; UpdateReloadButton(); } diff --git a/NewHorizons/Utility/DebugTools/Menu/DebugMenu.cs b/NewHorizons/Utility/DebugTools/Menu/DebugMenu.cs index 92e77893c..882b9cee5 100644 --- a/NewHorizons/Utility/DebugTools/Menu/DebugMenu.cs +++ b/NewHorizons/Utility/DebugTools/Menu/DebugMenu.cs @@ -89,7 +89,7 @@ private void OnChangeStarSystem(string _) public static void InitializePauseMenu(IPauseMenuManager pauseMenu) { - pauseMenuButton = pauseMenu.MakeSimpleButton(TranslationHandler.GetTranslation("Toggle Dev Tools Menu", TranslationHandler.TextType.UI).ToUpper(), 3, true); + pauseMenuButton = pauseMenu.MakeSimpleButton(TranslationHandler.GetTranslation("Toggle Dev Tools Menu", TranslationHandler.TextType.UI).ToUpperFixed(), 3, true); _instance?.InitMenu(); } diff --git a/NewHorizons/Utility/NewHorizonExtensions.cs b/NewHorizons/Utility/NewHorizonExtensions.cs index c34182e18..3ba29885c 100644 --- a/NewHorizons/Utility/NewHorizonExtensions.cs +++ b/NewHorizons/Utility/NewHorizonExtensions.cs @@ -7,6 +7,7 @@ using OWML.Utils; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Reflection; @@ -69,6 +70,70 @@ public static float GetFalloffExponent(this GravityVolume gv) return 0; } + public static string ToLanguageName(this TextTranslation.Language language) + { + switch (language) + { + case TextTranslation.Language.UNKNOWN: + case TextTranslation.Language.TOTAL: + case TextTranslation.Language.ENGLISH: + return "English"; + case TextTranslation.Language.SPANISH_LA: + return "Spanish"; + case TextTranslation.Language.GERMAN: + return "German"; + case TextTranslation.Language.FRENCH: + return "French"; + case TextTranslation.Language.ITALIAN: + return "Italian"; + case TextTranslation.Language.POLISH: + return "Polish"; + case TextTranslation.Language.PORTUGUESE_BR: + return "Portuguese (Brazil)"; + case TextTranslation.Language.JAPANESE: + return "Japanese"; + case TextTranslation.Language.RUSSIAN: + return "Russian"; + case TextTranslation.Language.CHINESE_SIMPLE: + return "Chinese (Simplified)"; + case TextTranslation.Language.KOREAN: + return "Korean"; + case TextTranslation.Language.TURKISH: + return "Turkish"; + default: + return language.ToString().Replace("_", " ").ToTitleCase(); + } + } + + public static CultureInfo ToCultureInfo(this TextTranslation.Language language) + { + return CultureInfo.GetCultures(CultureTypes.AllCultures).FirstOrDefault(culture => + { + var name = language.ToLanguageName(); + return culture.EnglishName == name || culture.NativeName.ToTitleCase() == name; + }) ?? CultureInfo.CurrentCulture; + } + + public static string ToUpperFixed(this string str) + { + return str.ToUpper(TextTranslation.Get().m_language); + } + + public static string ToLowerFixed(this string str) + { + return str.ToLower(TextTranslation.Get().m_language); + } + + public static string ToUpper(this string str, TextTranslation.Language language) + { + return str.ToUpper(language.ToCultureInfo()); + } + + public static string ToLower(this string str, TextTranslation.Language language) + { + return str.ToLower(language.ToCultureInfo()); + } + public static string ToCamelCase(this string str) { StringBuilder strBuilder = new StringBuilder(str); @@ -78,7 +143,7 @@ public static string ToCamelCase(this string str) public static string ToTitleCase(this string str) { - StringBuilder strBuilder = new StringBuilder(str); + StringBuilder strBuilder = new StringBuilder(str.ToLowerInvariant()); strBuilder[0] = strBuilder[0].ToString().ToUpperInvariant().ToCharArray()[0]; return strBuilder.ToString(); } @@ -348,7 +413,7 @@ public static List GetChildNodes(this XmlNode parentNode, string tagNam public static XmlNode GetChildNode(this XmlNode parentNode, string tagName) { - return parentNode.ChildNodes.Cast().First(node => node.LocalName == tagName); + return parentNode.ChildNodes.Cast().FirstOrDefault(node => node.LocalName == tagName); } public static string TruncateWhitespaceAndToLower(this string text) diff --git a/NewHorizons/manifest.json b/NewHorizons/manifest.json index 74ce1e6ee..c57ae22d4 100644 --- a/NewHorizons/manifest.json +++ b/NewHorizons/manifest.json @@ -4,7 +4,7 @@ "author": "xen, Bwc9876, JohnCorby, MegaPiggy, Clay, Trifid, and friends", "name": "New Horizons", "uniqueName": "xen.NewHorizons", - "version": "1.20.1", + "version": "1.20.2", "owmlVersion": "2.10.3", "dependencies": [ "JohnCorby.VanillaFix", "xen.CommonCameraUtility", "dgarro.CustomShipLogModes" ], "conflicts": [ "PacificEngine.OW_CommonResources" ],