using NewHorizons.External.Modules; using NewHorizons.Handlers; using NewHorizons.Utility; using NewHorizons.Utility.Geometry; using NewHorizons.Utility.OWMLUtilities; using Newtonsoft.Json; using OWML.Utils; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml; using UnityEngine; using Logger = NewHorizons.Utility.Logger; using Random = UnityEngine.Random; namespace NewHorizons.Builder.Props { public static class TranslatorTextBuilder { private static Material _ghostArcMaterial; private static Material _adultArcMaterial; private static Material _childArcMaterial; private static GameObject _scrollPrefab; private static GameObject _computerPrefab; private static GameObject _preCrashComputerPrefab; private static GameObject _cairnPrefab; private static GameObject _cairnVariantPrefab; private static GameObject _recorderPrefab; private static GameObject _preCrashRecorderPrefab; private static GameObject _trailmarkerPrefab; private static Dictionary arcInfoToCorrespondingSpawnedGameObject = new Dictionary(); public static GameObject GetSpawnedGameObjectByNomaiTextArcInfo(PropModule.NomaiTextArcInfo arc) { if (!arcInfoToCorrespondingSpawnedGameObject.ContainsKey(arc)) return null; return arcInfoToCorrespondingSpawnedGameObject[arc]; } private static Dictionary conversationInfoToCorrespondingSpawnedGameObject = new Dictionary(); public static GameObject GetSpawnedGameObjectByNomaiTextInfo(PropModule.NomaiTextInfo convo) { Logger.LogVerbose("Retrieving wall text obj for " + convo); if (!conversationInfoToCorrespondingSpawnedGameObject.ContainsKey(convo)) return null; return conversationInfoToCorrespondingSpawnedGameObject[convo]; } private static bool _isInit; internal static void InitPrefabs() { if (_isInit) return; _isInit = true; if (_adultArcMaterial == null) { _adultArcMaterial = SearchUtilities.Find("BrittleHollow_Body/Sector_BH/Sector_Crossroads/Interactables_Crossroads/Trailmarkers/Prefab_NOM_BH_Cairn_Arc (2)/Props_TH_ClutterSmall/Arc_Short/Arc") .GetComponent() .sharedMaterial; } if (_childArcMaterial == null) { _childArcMaterial = SearchUtilities.Find("BrittleHollow_Body/Sector_BH/Sector_OldSettlement/Fragment OldSettlement 5/Core_OldSettlement 5/Interactables_Core_OldSettlement5/Arc_BH_OldSettlement_ChildrensRhyme/Arc 1") .GetComponent() .sharedMaterial; } if (_ghostArcMaterial == null) { _ghostArcMaterial = SearchUtilities.Find("RingWorld_Body/Sector_RingInterior/Sector_Zone1/Interactables_Zone1/Props_IP_ZoneSign_1/Arc_TestAlienWriting/Arc 1") .GetComponent() .sharedMaterial; } if (_scrollPrefab == null) _scrollPrefab = SearchUtilities.Find("BrittleHollow_Body/Sector_BH/Sector_NorthHemisphere/Sector_NorthPole/Sector_HangingCity/Sector_HangingCity_District2/Interactables_HangingCity_District2/Prefab_NOM_Scroll").InstantiateInactive().Rename("Prefab_NOM_Scroll").DontDestroyOnLoad(); if (_computerPrefab == null) { _computerPrefab = SearchUtilities.Find("VolcanicMoon_Body/Sector_VM/Interactables_VM/Prefab_NOM_Computer").InstantiateInactive().Rename("Prefab_NOM_Computer").DontDestroyOnLoad(); _computerPrefab.transform.rotation = Quaternion.identity; } if (_preCrashComputerPrefab == null) { _preCrashComputerPrefab = SearchUtilities.Find("BrittleHollow_Body/Sector_BH/Sector_EscapePodCrashSite/Sector_CrashFragment/EscapePod_Socket/Interactibles_EscapePod/Prefab_NOM_Vessel_Computer").InstantiateInactive().Rename("Prefab_NOM_Vessel_Computer").DontDestroyOnLoad(); _preCrashComputerPrefab.transform.rotation = Quaternion.identity; } if (_cairnPrefab == null) { _cairnPrefab = SearchUtilities.Find("BrittleHollow_Body/Sector_BH/Sector_Crossroads/Interactables_Crossroads/Trailmarkers/Prefab_NOM_BH_Cairn_Arc (1)").InstantiateInactive().Rename("Prefab_NOM_Cairn").DontDestroyOnLoad(); _cairnPrefab.transform.rotation = Quaternion.identity; } if (_cairnVariantPrefab == null) { _cairnVariantPrefab = SearchUtilities.Find("TimberHearth_Body/Sector_TH/Sector_NomaiMines/Interactables_NomaiMines/Prefab_NOM_TH_Cairn_Arc").InstantiateInactive().Rename("Prefab_NOM_Cairn").DontDestroyOnLoad(); _cairnVariantPrefab.transform.rotation = Quaternion.identity; } if (_recorderPrefab == null) { _recorderPrefab = SearchUtilities.Find("Comet_Body/Prefab_NOM_Shuttle/Sector_NomaiShuttleInterior/Interactibles_NomaiShuttleInterior/Prefab_NOM_Recorder").InstantiateInactive().Rename("Prefab_NOM_Recorder").DontDestroyOnLoad(); _recorderPrefab.transform.rotation = Quaternion.identity; } if (_preCrashRecorderPrefab == null) { _preCrashRecorderPrefab = SearchUtilities.Find("BrittleHollow_Body/Sector_BH/Sector_EscapePodCrashSite/Sector_CrashFragment/Interactables_CrashFragment/Prefab_NOM_Recorder").InstantiateInactive().Rename("Prefab_NOM_Recorder_Vessel").DontDestroyOnLoad(); _preCrashRecorderPrefab.transform.rotation = Quaternion.identity; } if (_trailmarkerPrefab == null) { _trailmarkerPrefab = SearchUtilities.Find("BrittleHollow_Body/Sector_BH/Sector_NorthHemisphere/Sector_NorthPole/Sector_HangingCity/Sector_HangingCity_District2/Interactables_HangingCity_District2/Prefab_NOM_Sign").InstantiateInactive().Rename("Prefab_NOM_Trailmarker").DontDestroyOnLoad(); _trailmarkerPrefab.transform.rotation = Quaternion.identity; } } public static GameObject Make(GameObject planetGO, Sector sector, PropModule.NomaiTextInfo info, NewHorizonsBody nhBody) { InitPrefabs(); var xmlPath = File.ReadAllText(Path.Combine(nhBody.Mod.ModHelper.Manifest.ModFolderPath, info.xmlFile)); switch (info.type) { case PropModule.NomaiTextInfo.NomaiTextType.Wall: { var nomaiWallTextObj = MakeWallText(planetGO, sector, info, xmlPath, nhBody).gameObject; nomaiWallTextObj = GeneralPropBuilder.MakeFromExisting(nomaiWallTextObj, planetGO, sector, info); if (info.normal != null) { // In global coordinates (normal was in local coordinates) var up = (nomaiWallTextObj.transform.position - planetGO.transform.position).normalized; var forward = planetGO.transform.TransformDirection(info.normal).normalized; if (info.isRelativeToParent) { nomaiWallTextObj.transform.up = up; nomaiWallTextObj.transform.forward = forward; } else { nomaiWallTextObj.transform.forward = forward; var desiredUp = Vector3.ProjectOnPlane(up, forward); var zRotation = Vector3.SignedAngle(nomaiWallTextObj.transform.up, desiredUp, forward); nomaiWallTextObj.transform.RotateAround(nomaiWallTextObj.transform.position, forward, zRotation); } } // nomaiWallTextObj.GetComponent().DrawBoundsWithDebugSpheres(); nomaiWallTextObj.SetActive(true); conversationInfoToCorrespondingSpawnedGameObject[info] = nomaiWallTextObj; return nomaiWallTextObj; } case PropModule.NomaiTextInfo.NomaiTextType.Scroll: { var customScroll = GeneralPropBuilder.MakeFromPrefab(_scrollPrefab, _scrollPrefab.name, planetGO, sector, info, alignToBody: info.rotation == null); var nomaiWallText = MakeWallText(planetGO, sector, info, xmlPath, nhBody); nomaiWallText.transform.parent = customScroll.transform; nomaiWallText.transform.localPosition = Vector3.zero; nomaiWallText.transform.localRotation = Quaternion.identity; nomaiWallText._showTextOnStart = false; // Don't want to be able to translate until its in a socket nomaiWallText.GetComponent().enabled = false; nomaiWallText.gameObject.SetActive(true); var scrollItem = customScroll.GetComponent(); // Idk why this thing is always around GameObject.Destroy(customScroll.transform.Find("Arc_BH_City_Forum_2").gameObject); // This variable is the bane of my existence i dont get it scrollItem._nomaiWallText = nomaiWallText; // Because the scroll was already awake it does weird shit in Awake and makes some of the entries in this array be null scrollItem._colliders = new OWCollider[] { scrollItem.GetComponent() }; // Else when you put them down you can't pick them back up customScroll.GetComponent()._physicsRemoved = false; customScroll.SetActive(true); Delay.FireOnNextUpdate( () => { Logger.LogVerbose("Fixing scroll!"); scrollItem._nomaiWallText = nomaiWallText; scrollItem.SetSector(sector); customScroll.transform.Find("Props_NOM_Scroll/Props_NOM_Scroll_Geo").GetComponent().enabled = true; customScroll.transform.Find("Props_NOM_Scroll/Props_NOM_Scroll_Collider").gameObject.SetActive(true); nomaiWallText.gameObject.GetComponent().enabled = false; customScroll.GetComponent().enabled = true; scrollItem._nomaiWallText.HideImmediate(); scrollItem._nomaiWallText._collider.SetActivation(true); scrollItem.SetColliderActivation(true); } ); conversationInfoToCorrespondingSpawnedGameObject[info] = customScroll; return customScroll; } case PropModule.NomaiTextInfo.NomaiTextType.Computer: { var computerObject = GeneralPropBuilder.MakeFromPrefab(_computerPrefab, _computerPrefab.name, planetGO, sector, info, alignToBody: true, normal: info.normal); var computer = computerObject.GetComponent(); computer.SetSector(sector); computer._location = EnumUtils.Parse(info.location.ToString()); computer._dictNomaiTextData = MakeNomaiTextDict(xmlPath); computer._nomaiTextAsset = new TextAsset(xmlPath); computer._nomaiTextAsset.name = Path.GetFileNameWithoutExtension(info.xmlFile); AddTranslation(xmlPath); // Make sure the computer model is loaded StreamingHandler.SetUpStreaming(computerObject, sector); computerObject.SetActive(true); conversationInfoToCorrespondingSpawnedGameObject[info] = computerObject; return computerObject; } case PropModule.NomaiTextInfo.NomaiTextType.PreCrashComputer: { var detailInfo = new PropModule.DetailInfo() { position = info.position, parentPath = info.parentPath, isRelativeToParent = info.isRelativeToParent, rename = info.rename }; var computerObject = DetailBuilder.Make(planetGO, sector, _preCrashComputerPrefab, detailInfo); computerObject.SetActive(false); var up = computerObject.transform.position - planetGO.transform.position; if (info.normal != null) up = planetGO.transform.TransformDirection(info.normal); computerObject.transform.rotation = Quaternion.FromToRotation(Vector3.up, up) * computerObject.transform.rotation; var computer = computerObject.GetComponent(); computer.SetSector(sector); computer._location = EnumUtils.Parse(info.location.ToString()); computer._dictNomaiTextData = MakeNomaiTextDict(xmlPath); computer._nomaiTextAsset = new TextAsset(xmlPath); computer._nomaiTextAsset.name = Path.GetFileNameWithoutExtension(info.xmlFile); AddTranslation(xmlPath); // Make fifth ring work var fifthRingObject = computerObject.FindChild("Props_NOM_Vessel_Computer 1/Props_NOM_Vessel_Computer_Effects (4)"); fifthRingObject.SetActive(true); var fifthRing = fifthRingObject.GetComponent(); //fifthRing._baseProjectorColor = new Color(1.4118, 1.5367, 4, 1); //fifthRing._baseTextColor = new Color(0.8824, 0.9604, 2.5, 1); //fifthRing._baseTextShadowColor = new Color(0.3529, 0.3843, 1, 0.25); fifthRing._computer = computer; computerObject.SetActive(true); // All rings are rendered by detail builder so dont do that (have to wait for entries to be set) Delay.FireOnNextUpdate(() => { for (var i = computer.GetNumTextBlocks(); i < 5; i++) { var ring = computer._computerRings[i]; ring.gameObject.SetActive(false); } }); conversationInfoToCorrespondingSpawnedGameObject[info] = computerObject; return computerObject; } case PropModule.NomaiTextInfo.NomaiTextType.Cairn: case PropModule.NomaiTextInfo.NomaiTextType.CairnVariant: { var cairnPrefab = info.type == PropModule.NomaiTextInfo.NomaiTextType.CairnVariant ? _cairnVariantPrefab : _cairnPrefab; var cairnObject = GeneralPropBuilder.MakeFromPrefab(cairnPrefab, _cairnPrefab.name, planetGO, sector, info, alignToBody: info.rotation == null); // Idk do we have to set it active before finding things? cairnObject.SetActive(true); // Make it do the thing when it finishes being knocked over foreach (var rock in cairnObject.GetComponent()._rocks) { rock._returning = false; rock._owCollider.SetActivation(true); rock.enabled = false; } // So we can actually knock it over cairnObject.GetComponent().enabled = true; var nomaiWallText = cairnObject.transform.Find("Props_TH_ClutterSmall/Arc_Short").GetComponent(); nomaiWallText.SetSector(sector); nomaiWallText._location = EnumUtils.Parse(info.location.ToString()); nomaiWallText._dictNomaiTextData = MakeNomaiTextDict(xmlPath); nomaiWallText._nomaiTextAsset = new TextAsset(xmlPath); nomaiWallText._nomaiTextAsset.name = Path.GetFileNameWithoutExtension(info.xmlFile); AddTranslation(xmlPath); // Make sure the computer model is loaded StreamingHandler.SetUpStreaming(cairnObject, sector); conversationInfoToCorrespondingSpawnedGameObject[info] = cairnObject; return cairnObject; } case PropModule.NomaiTextInfo.NomaiTextType.PreCrashRecorder: case PropModule.NomaiTextInfo.NomaiTextType.Recorder: { var prefab = (info.type == PropModule.NomaiTextInfo.NomaiTextType.PreCrashRecorder ? _preCrashRecorderPrefab : _recorderPrefab); var detailInfo = new PropModule.DetailInfo { parentPath = info.parentPath, rotation = info.rotation, position = info.position, isRelativeToParent = info.isRelativeToParent, rename = info.rename, alignToNormal = info.rotation == null, }; var recorderObject = DetailBuilder.Make(planetGO, sector, prefab, detailInfo); recorderObject.SetActive(false); var nomaiText = recorderObject.GetComponentInChildren(); nomaiText.SetSector(sector); nomaiText._location = EnumUtils.Parse(info.location.ToString()); nomaiText._dictNomaiTextData = MakeNomaiTextDict(xmlPath); nomaiText._nomaiTextAsset = new TextAsset(xmlPath); nomaiText._nomaiTextAsset.name = Path.GetFileNameWithoutExtension(info.xmlFile); AddTranslation(xmlPath); recorderObject.SetActive(true); recorderObject.transform.Find("InteractSphere").gameObject.GetComponent().enabled = true; conversationInfoToCorrespondingSpawnedGameObject[info] = recorderObject; return recorderObject; } case PropModule.NomaiTextInfo.NomaiTextType.Trailmarker: { var trailmarkerObject = GeneralPropBuilder.MakeFromPrefab(_trailmarkerPrefab, _trailmarkerPrefab.name, planetGO, sector, info, alignToBody: info.rotation == null); // shrink because that is what mobius does on all trailmarkers or else they are the size of the player trailmarkerObject.transform.localScale = Vector3.one * 0.75f; // Idk do we have to set it active before finding things? trailmarkerObject.SetActive(true); var nomaiWallText = trailmarkerObject.transform.Find("Arc_Short").GetComponent(); nomaiWallText.SetSector(sector); nomaiWallText._location = EnumUtils.Parse(info.location.ToString()); nomaiWallText._dictNomaiTextData = MakeNomaiTextDict(xmlPath); nomaiWallText._nomaiTextAsset = new TextAsset(xmlPath); nomaiWallText._nomaiTextAsset.name = Path.GetFileNameWithoutExtension(info.xmlFile); AddTranslation(xmlPath); // Make sure the model is loaded StreamingHandler.SetUpStreaming(trailmarkerObject, sector); conversationInfoToCorrespondingSpawnedGameObject[info] = trailmarkerObject; return trailmarkerObject; } default: Logger.LogError($"Unsupported NomaiText type {info.type}"); return null; } } private static NomaiWallText MakeWallText(GameObject go, Sector sector, PropModule.NomaiTextInfo info, string xmlPath, NewHorizonsBody nhBody) { GameObject nomaiWallTextObj = new GameObject("NomaiWallText"); nomaiWallTextObj.SetActive(false); var box = nomaiWallTextObj.AddComponent(); box.center = new Vector3(-0.0643f, 1.1254f, 0f); box.size = new Vector3(6.1424f, 5.2508f, 0.5f); box.isTrigger = true; nomaiWallTextObj.AddComponent(); var nomaiWallText = nomaiWallTextObj.AddComponent(); nomaiWallText._location = EnumUtils.Parse(info.location.ToString()); var text = new TextAsset(xmlPath); // Text assets need a name to be used with VoiceMod text.name = Path.GetFileNameWithoutExtension(info.xmlFile); BuildArcs(xmlPath, nomaiWallText, nomaiWallTextObj, info, nhBody); AddTranslation(xmlPath); nomaiWallText._nomaiTextAsset = text; nomaiWallText.SetTextAsset(text); // #433 fuzzy stranger text if (info.arcInfo != null && info.arcInfo.Any(x => x.type == PropModule.NomaiTextArcInfo.NomaiTextArcType.Stranger)) { StreamingHandler.SetUpStreaming(AstroObject.Name.RingWorld, sector); } return nomaiWallText; } internal static void BuildArcs(string xml, NomaiWallText nomaiWallText, GameObject conversationZone, PropModule.NomaiTextInfo info, NewHorizonsBody nhBody) { var dict = MakeNomaiTextDict(xml); nomaiWallText._dictNomaiTextData = dict; var cacheKey = xml.GetHashCode() + " " + JsonConvert.SerializeObject(info).GetHashCode(); RefreshArcs(nomaiWallText, conversationZone, info, nhBody, cacheKey); } [Serializable] private struct ArcCacheData { public MMesh mesh; public MVector3[] skeletonPoints; public MVector3 position; public float zRotation; public bool mirrored; } internal static void RefreshArcs(NomaiWallText nomaiWallText, GameObject conversationZone, PropModule.NomaiTextInfo info, NewHorizonsBody nhBody, string cacheKey) { var dict = nomaiWallText._dictNomaiTextData; Random.InitState(info.seed == 0 ? info.xmlFile.GetHashCode() : info.seed); var arcsByID = new Dictionary(); if (info.arcInfo != null && info.arcInfo.Count() != dict.Values.Count()) { Logger.LogError($"Can't make NomaiWallText, arcInfo length [{info.arcInfo.Count()}] doesn't equal text entries [{dict.Values.Count()}]"); return; } ArcCacheData[] cachedData = null; if (nhBody.Cache?.ContainsKey(cacheKey) ?? false) cachedData = nhBody.Cache.Get(cacheKey); var arranger = nomaiWallText.gameObject.AddComponent(); // Generate spiral meshes/GOs var i = 0; foreach (var textData in dict.Values) { var arcInfo = info.arcInfo?.Length > i ? info.arcInfo[i] : null; var textEntryID = textData.ID; var parentID = textData.ParentID; var parent = parentID == -1 ? null : arcsByID[parentID]; GameObject arcReadFromCache = null; if (cachedData != null) { var skeletonPoints = cachedData[i].skeletonPoints.Select(mv => (Vector3)mv).ToArray(); arcReadFromCache = NomaiTextArcBuilder.BuildSpiralGameObject(skeletonPoints, cachedData[i].mesh); arcReadFromCache.transform.parent = arranger.transform; arcReadFromCache.transform.localScale = new Vector3(cachedData[i].mirrored? -1 : 1, 1, 1); arcReadFromCache.transform.localPosition = cachedData[i].position; arcReadFromCache.transform.localEulerAngles = new Vector3(0, 0, cachedData[i].zRotation); } GameObject arc = MakeArc(arcInfo, conversationZone, parent, textEntryID, arcReadFromCache); arc.name = $"Arc {textEntryID} - Child of {parentID}"; arcsByID.Add(textEntryID, arc); i++; } // no need to arrange if the cache exists if (cachedData == null) { Logger.LogVerbose("Cache and/or cache entry was null, proceding with wall text arc arrangment."); // auto placement var overlapFound = true; for (var k = 0; k < arranger.spirals.Count*2; k++) { overlapFound = arranger.AttemptOverlapResolution(); if (!overlapFound) break; for(var a = 0; a < 10; a++) arranger.FDGSimulationStep(); } if (overlapFound) Logger.LogVerbose("Overlap resolution failed!"); // manual placement for (var j = 0; j < info.arcInfo?.Length; j++) { var arcInfo = info.arcInfo[j]; var arc = arranger.spirals[j]; if (arcInfo.keepAutoPlacement) continue; if (arcInfo.position == null) arc.transform.localPosition = Vector3.zero; else arc.transform.localPosition = new Vector3(arcInfo.position.x, arcInfo.position.y, 0); arc.transform.localRotation = Quaternion.Euler(0, 0, arcInfo.zRotation); if (arcInfo.mirror) arc.transform.localScale = new Vector3(-1, 1, 1); else arc.transform.localScale = new Vector3( 1, 1, 1); } // make an entry in the cache for all these spirals if (nhBody.Cache != null) { var cacheData = arranger.spirals.Select(spiralManipulator => new ArcCacheData() { mesh = spiralManipulator.GetComponent().sharedMesh, skeletonPoints = spiralManipulator.NomaiTextLine._points.Select(v => (MVector3)v).ToArray(), position = spiralManipulator.transform.localPosition, zRotation = spiralManipulator.transform.localEulerAngles.z, mirrored = spiralManipulator.transform.localScale.x < 0 }).ToArray(); nhBody.Cache.Set(cacheKey, cacheData); } } } internal static GameObject MakeArc(PropModule.NomaiTextArcInfo arcInfo, GameObject conversationZone, GameObject parent, int textEntryID, GameObject prebuiltArc = null) { GameObject arc; var type = arcInfo != null ? arcInfo.type : PropModule.NomaiTextArcInfo.NomaiTextArcType.Adult; NomaiTextArcBuilder.SpiralProfile profile; Material mat; Mesh overrideMesh = null; Color? overrideColor = null; switch (type) { case PropModule.NomaiTextArcInfo.NomaiTextArcType.Child: profile = NomaiTextArcBuilder.childSpiralProfile; mat = _childArcMaterial; break; case PropModule.NomaiTextArcInfo.NomaiTextArcType.Stranger when _ghostArcMaterial != null: profile = NomaiTextArcBuilder.strangerSpiralProfile; mat = _ghostArcMaterial; overrideMesh = MeshUtilities.RectangleMeshFromCorners(new Vector3[]{ new Vector3(-0.9f, 0.0f, 0.0f), new Vector3(0.9f, 0.0f, 0.0f), new Vector3(-0.9f, 2.0f, 0.0f), new Vector3(0.9f, 2.0f, 0.0f) }); overrideColor = new Color(0.0158f, 1.0f, 0.5601f, 1f); break; case PropModule.NomaiTextArcInfo.NomaiTextArcType.Adult: default: profile = NomaiTextArcBuilder.adultSpiralProfile; mat = _adultArcMaterial; break; } if (prebuiltArc != null) { arc = prebuiltArc; } else { if (parent != null) arc = parent.GetComponent().AddChild(profile).gameObject; else arc = NomaiTextArcArranger.CreateSpiral(profile, conversationZone).gameObject; } if (mat != null) arc.GetComponent().sharedMaterial = mat; arc.transform.parent = conversationZone.transform; arc.GetComponent()._prebuilt = false; arc.GetComponent().SetEntryID(textEntryID); arc.GetComponent().enabled = false; if (overrideMesh != null) arc.GetComponent().sharedMesh = overrideMesh; if (overrideColor != null) arc.GetComponent()._targetColor = (Color)overrideColor; arc.SetActive(true); if (arcInfo != null) arcInfoToCorrespondingSpawnedGameObject[arcInfo] = arc; return arc; } private static Dictionary MakeNomaiTextDict(string xmlPath) { var dict = new Dictionary(); XmlDocument xmlDocument = new XmlDocument(); xmlDocument.LoadXml(xmlPath); XmlNode rootNode = xmlDocument.SelectSingleNode("NomaiObject"); foreach (object obj in rootNode.SelectNodes("TextBlock")) { XmlNode xmlNode = (XmlNode)obj; int textEntryID = -1; int parentID = -1; XmlNode textNode = xmlNode.SelectSingleNode("Text"); XmlNode entryIDNode = xmlNode.SelectSingleNode("ID"); XmlNode parentIDNode = xmlNode.SelectSingleNode("ParentID"); if (entryIDNode != null && !int.TryParse(entryIDNode.InnerText, out textEntryID)) { Logger.LogError($"Couldn't parse int ID in [{entryIDNode?.InnerText}] for [{xmlPath}]"); textEntryID = -1; } if (parentIDNode != null && !int.TryParse(parentIDNode.InnerText, out parentID)) { Logger.LogError($"Couldn't parse int ParentID in [{parentIDNode?.InnerText}] for [{xmlPath}]"); parentID = -1; } NomaiText.NomaiTextData value = new NomaiText.NomaiTextData(textEntryID, parentID, textNode, false, NomaiText.Location.UNSPECIFIED); dict.Add(textEntryID, value); } return dict; } private static void AddTranslation(string xmlPath) { XmlDocument xmlDocument = new XmlDocument(); xmlDocument.LoadXml(xmlPath); XmlNode xmlNode = xmlDocument.SelectSingleNode("NomaiObject"); XmlNodeList xmlNodeList = xmlNode.SelectNodes("TextBlock"); foreach (object obj in xmlNodeList) { XmlNode xmlNode2 = (XmlNode)obj; var text = xmlNode2.SelectSingleNode("Text").InnerText; TranslationHandler.AddDialogue(text); } } } }