diff --git a/NewHorizons/Builder/Props/NomaiTextBuilder.cs b/NewHorizons/Builder/Props/NomaiTextBuilder.cs index a9b9e607..b76b04ce 100644 --- a/NewHorizons/Builder/Props/NomaiTextBuilder.cs +++ b/NewHorizons/Builder/Props/NomaiTextBuilder.cs @@ -14,6 +14,9 @@ using OWML.Utils; namespace NewHorizons.Builder.Props { + /// + /// Legacy - this class is used with the deprecated "nomaiText" module (deprecated on release of autospirals) + /// public static class NomaiTextBuilder { private static List _arcPrefabs; diff --git a/NewHorizons/Builder/Props/PropBuildManager.cs b/NewHorizons/Builder/Props/PropBuildManager.cs index d3dcbf4f..10536aaa 100644 --- a/NewHorizons/Builder/Props/PropBuildManager.cs +++ b/NewHorizons/Builder/Props/PropBuildManager.cs @@ -1,6 +1,7 @@ using NewHorizons.Builder.Body; using NewHorizons.Builder.ShipLog; using NewHorizons.External.Configs; +using NewHorizons.Utility; using OWML.Common; using System; using System.Collections.Generic; @@ -10,8 +11,11 @@ namespace NewHorizons.Builder.Props { public static class PropBuildManager { - public static void Make(GameObject go, Sector sector, OWRigidbody planetBody, PlanetConfig config, IModBehaviour mod) + public static void Make(GameObject go, Sector sector, OWRigidbody planetBody, NewHorizonsBody nhBody) { + PlanetConfig config = nhBody.Config; + IModBehaviour mod = nhBody.Mod; + if (config.Props.scatter != null) { try @@ -128,7 +132,22 @@ namespace NewHorizons.Builder.Props { try { - NomaiTextBuilder.Make(go, sector, nomaiTextInfo, mod); + NomaiTextBuilder.Make(go, sector, nomaiTextInfo, nhBody.Mod); + } + catch (Exception ex) + { + Logger.LogError($"Couldn't make text [{nomaiTextInfo.xmlFile}] for [{go.name}]:\n{ex}"); + } + + } + } + if (config.Props.translatorText != null) + { + foreach (var nomaiTextInfo in config.Props.translatorText) + { + try + { + TranslatorTextBuilder.Make(go, sector, nomaiTextInfo, nhBody); } catch (Exception ex) { @@ -205,7 +224,7 @@ namespace NewHorizons.Builder.Props { try { - RemoteBuilder.Make(go, sector, remoteInfo, mod); + RemoteBuilder.Make(go, sector, remoteInfo, nhBody); } catch (Exception ex) { diff --git a/NewHorizons/Builder/Props/RemoteBuilder.cs b/NewHorizons/Builder/Props/RemoteBuilder.cs index c8cd3334..4fa179f3 100644 --- a/NewHorizons/Builder/Props/RemoteBuilder.cs +++ b/NewHorizons/Builder/Props/RemoteBuilder.cs @@ -116,10 +116,11 @@ namespace NewHorizons.Builder.Props } } - public static void Make(GameObject go, Sector sector, PropModule.RemoteInfo info, IModBehaviour mod) + public static void Make(GameObject go, Sector sector, PropModule.RemoteInfo info, NewHorizonsBody nhBody) { InitPrefabs(); + var mod = nhBody.Mod; var id = RemoteHandler.GetPlatformID(info.id); Texture2D decal = Texture2D.whiteTexture; @@ -142,7 +143,7 @@ namespace NewHorizons.Builder.Props { try { - RemoteBuilder.MakeWhiteboard(go, sector, id, decal, info.whiteboard, mod); + RemoteBuilder.MakeWhiteboard(go, sector, id, decal, info.whiteboard, nhBody); } catch (Exception ex) { @@ -166,7 +167,7 @@ namespace NewHorizons.Builder.Props } } - public static void MakeWhiteboard(GameObject go, Sector sector, NomaiRemoteCameraPlatform.ID id, Texture2D decal, PropModule.RemoteInfo.WhiteboardInfo info, IModBehaviour mod) + public static void MakeWhiteboard(GameObject go, Sector sector, NomaiRemoteCameraPlatform.ID id, Texture2D decal, PropModule.RemoteInfo.WhiteboardInfo info, NewHorizonsBody nhBody) { var detailInfo = new PropModule.DetailInfo() { @@ -193,7 +194,7 @@ namespace NewHorizons.Builder.Props { var textInfo = info.nomaiText[i]; component._remoteIDs[i] = RemoteHandler.GetPlatformID(textInfo.id); - var wallText = NomaiTextBuilder.Make(whiteboard, sector, new PropModule.NomaiTextInfo + var wallText = TranslatorTextBuilder.Make(whiteboard, sector, new PropModule.NomaiTextInfo { arcInfo = textInfo.arcInfo, location = textInfo.location, @@ -204,7 +205,7 @@ namespace NewHorizons.Builder.Props seed = textInfo.seed, type = PropModule.NomaiTextInfo.NomaiTextType.Wall, xmlFile = textInfo.xmlFile - }, mod).GetComponent(); + }, nhBody).GetComponent(); wallText._showTextOnStart = false; component._nomaiTexts[i] = wallText; } diff --git a/NewHorizons/Builder/Props/TranslatorText/NomaiTextArcArranger.cs b/NewHorizons/Builder/Props/TranslatorText/NomaiTextArcArranger.cs new file mode 100644 index 00000000..d05bda58 --- /dev/null +++ b/NewHorizons/Builder/Props/TranslatorText/NomaiTextArcArranger.cs @@ -0,0 +1,393 @@ +using NewHorizons.Utility; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.Profiling; +using Logger = NewHorizons.Utility.Logger; + +namespace NewHorizons.Builder.Props +{ + public class NomaiTextArcArranger : MonoBehaviour { + private static int MAX_MOVE_DISTANCE = 2; + + public List spirals = new List(); + public List reverseToposortedSpirals = null; + private bool updateToposortOnNextStep = true; + private Dictionary sprialOverlapResolutionPriority = new Dictionary(); + + public SpiralManipulator root { get; private set; } + + public float maxX = 2.7f; + public float minX = -2.7f; + public float maxY = 2.6f; + public float minY = -1f; + + public static SpiralManipulator CreateSpiral(NomaiTextArcBuilder.SpiralProfile profile, GameObject spiralMeshHolder) + { + var rootArc = NomaiTextArcBuilder.BuildSpiralGameObject(profile); + rootArc.transform.parent = spiralMeshHolder.transform; + rootArc.transform.localEulerAngles = new Vector3(0, 0, Random.Range(-60, 60)); + + var manip = rootArc.AddComponent(); + if (Random.value < 0.5) manip.transform.localScale = new Vector3(-1, 1, 1); // randomly mirror + + // add to arranger + var arranger = spiralMeshHolder.GetAddComponent(); + if (arranger.root == null) arranger.root = manip; + arranger.spirals.Add(manip); + arranger.updateToposortOnNextStep = true; + + return manip; + } + + public void FDGSimulationStep() + { + if (updateToposortOnNextStep) + { + updateToposortOnNextStep = false; + GenerateReverseToposort(); + } + + Dictionary childForces = new Dictionary(); + + foreach (var s1 in reverseToposortedSpirals) // treating the conversation like a tree datastructure, move "leaf" spirals first so that we can propogate their force up to the parents + { + Vector2 force = Vector2.zero; + + // accumulate the force the children feel + if (childForces.ContainsKey(s1)) + { + force += 0.9f * childForces[s1]; + } + + // push away from fellow spirals + foreach (var s2 in spirals) + { + if (s1 == s2) continue; + if (s1.parent == s2) continue; + if (s1 == s2.parent) continue; + + var f = (s2.center - s1.center); + force -= f / Mathf.Pow(f.magnitude, 6); + + var f2 = (s2.localPosition - s1.localPosition); + force -= f2 / Mathf.Pow(f2.magnitude, 6); + } + + // push away from the edges + var MAX_EDGE_PUSH_FORCE = 1; + force += new Vector2(0, -1) * Mathf.Max(0, (s1.transform.localPosition.y + maxY)*(MAX_EDGE_PUSH_FORCE / maxY) - MAX_EDGE_PUSH_FORCE); + force += new Vector2(0, 1) * Mathf.Max(0, (s1.transform.localPosition.y + minY)*(MAX_EDGE_PUSH_FORCE / minY) - MAX_EDGE_PUSH_FORCE); + force += new Vector2(-1, 0) * Mathf.Max(0, (s1.transform.localPosition.x + maxX)*(MAX_EDGE_PUSH_FORCE / maxX) - MAX_EDGE_PUSH_FORCE); + force += new Vector2(1, 0) * Mathf.Max(0, (s1.transform.localPosition.x + minX)*(MAX_EDGE_PUSH_FORCE / minX) - MAX_EDGE_PUSH_FORCE); + + // push up just to make everything a little more pretty (this is not neccessary to get an arrangement that simply has no overlap/spirals exiting the bounds) + force += new Vector2(0, 1) * 1; + + // renormalize the force magnitude (keeps force sizes reasonable, and improves stability in the case of small forces) + var avg = 1; // the size of vector required to get a medium push + var scale = 0.75f; + force = force.normalized * scale * (1 / (1 + Mathf.Exp(avg-force.magnitude)) - 1 / (1 + Mathf.Exp(avg))); // apply a sigmoid-ish smoothing operation, so only giant forces actually move the spirals + + // if this is the root spiral, then rotate it instead of trying to move it + if (s1.parent == null) + { + // this is the root spiral, so rotate instead of moving + var finalAngle = Mathf.Atan2(force.y, force.x); // root spiral is always at 0, 0 + var currentAngle = Mathf.Atan2(s1.center.y, s1.center.x); // root spiral is always at 0, 0 + s1.transform.localEulerAngles = new Vector3(0, 0, finalAngle-currentAngle); + s1.UpdateChildren(); + + continue; + } + + // pick the parent point that's closest to center+force, and move to there + var spiral = s1; + var parentPoints = spiral.parent.GetComponent().GetPoints(); + + var idealPoint = spiral.position + force; + var bestPointIndex = 0; + var bestPointDistance = 99999999f; + for (var j = SpiralManipulator.MIN_PARENT_POINT; j < SpiralManipulator.MAX_PARENT_POINT && j < parentPoints.Length; j++) + { + // don't put this spiral on a point already occupied by a sibling + if (j != spiral._parentPointIndex && spiral.parent.pointsOccupiedByChildren.Contains(j)) continue; + + var point = parentPoints[j]; + point = spiral.parent.transform.TransformPoint(point); + + var dist = Vector2.Distance(point, idealPoint); + if (dist < bestPointDistance) { + bestPointDistance = dist; + bestPointIndex = j; + } + } + + // limit the distance a spiral can move in a single step + bestPointIndex = spiral._parentPointIndex + Mathf.Min(MAX_MOVE_DISTANCE, Mathf.Max(-MAX_MOVE_DISTANCE, bestPointIndex - spiral._parentPointIndex)); // minimize step size to help stability + + // actually move the spiral + spiral.PlaceOnParentPoint(bestPointIndex); + + // Enforce bounds + if (OutsideBounds(s1)) + { + var start = s1._parentPointIndex; + var originalMirror = s1.Mirrored; + + var success = AttemptToPushSpiralInBounds(s1, start); + if (!success) + { + // try flipping it if nothing worked with original mirror state + s1.Mirror(); + success = AttemptToPushSpiralInBounds(s1, start); + } + + if (!success) + { + // if we couldn't put it inside the bounds, put it back how we found it (this increases stability of the rest of the spirals) + if (s1.Mirrored != originalMirror) s1.Mirror(); + s1.PlaceOnParentPoint(start); + Logger.LogVerbose("Unable to place spiral " + s1.gameObject.name + " within bounds."); + } + } + + // record force for parents + if (!childForces.ContainsKey(s1.parent)) childForces[s1.parent] = Vector2.zero; + childForces[s1.parent] += force; + } + } + + public void GenerateReverseToposort() + { + reverseToposortedSpirals = new List(); + Queue frontierQueue = new Queue(); + frontierQueue.Enqueue(root); + + while(frontierQueue.Count > 0) + { + var spiral = frontierQueue.Dequeue(); + reverseToposortedSpirals.Add(spiral); + + foreach(var child in spiral.children) frontierQueue.Enqueue(child); + } + + reverseToposortedSpirals.Reverse(); + } + + #region overlap handling + + // returns whether there was overlap or not + public bool AttemptOverlapResolution() + { + var overlappingSpirals = FindOverlap(); + if (overlappingSpirals.x < 0) return false; + + if (!sprialOverlapResolutionPriority.ContainsKey(overlappingSpirals.x)) sprialOverlapResolutionPriority[overlappingSpirals.x] = 0; + if (!sprialOverlapResolutionPriority.ContainsKey(overlappingSpirals.y)) sprialOverlapResolutionPriority[overlappingSpirals.y] = 0; + + int mirrorIndex = overlappingSpirals.x; + if (sprialOverlapResolutionPriority[overlappingSpirals.y] > sprialOverlapResolutionPriority[overlappingSpirals.x]) mirrorIndex = overlappingSpirals.y; + + this.spirals[mirrorIndex].Mirror(); + sprialOverlapResolutionPriority[mirrorIndex]--; + + return true; + } + + public Vector2Int FindOverlap() + { + var index = -1; + foreach (var s1 in spirals) + { + index++; + if (s1.parent == null) continue; + + var jndex = -1; + foreach (var s2 in spirals) + { + jndex++; + if (SpiralsOverlap(s1, s2)) return new Vector2Int(index, jndex);; + } + } + + return new Vector2Int(-1, -1); + } + + public bool SpiralsOverlap(SpiralManipulator s1, SpiralManipulator s2) + { + if (s1 == s2) return false; + if (Vector3.Distance(s1.center, s2.center) > Mathf.Max(s1.NomaiTextLine.GetWorldRadius(), s2.NomaiTextLine.GetWorldRadius())) return false; // no overlap possible - too far away + + var s1Points = s1.NomaiTextLine.GetPoints().Select(p => s1.transform.TransformPoint(p)).ToList(); + var s2Points = s2.NomaiTextLine.GetPoints().Select(p => s2.transform.TransformPoint(p)).ToList(); + var s1ThresholdForOverlap = Vector3.Distance(s1Points[0], s1Points[1]); + var s2ThresholdForOverlap = Vector3.Distance(s2Points[0], s2Points[1]); + var thresholdForOverlap = Mathf.Pow(Mathf.Max(s1ThresholdForOverlap, s2ThresholdForOverlap), 2); // square to save on computation (we'll work in distance squared from here on) + + if (s1.parent == s2) s1Points.RemoveAt(0); // don't consider the base points so that we can check if children overlap their parents + if (s2.parent == s1) s2Points.RemoveAt(0); // (note: the base point of a child is always exactly overlapping with one of the parent's points) + + foreach(var p1 in s1Points) + { + foreach(var p2 in s2Points) + { + if (Vector3.SqrMagnitude(p1-p2) <= thresholdForOverlap) return true; // s1 and s2 overlap + } + } + + return false; + } + + #endregion overlap handling + + #region bounds handling + + public bool OutsideBounds(SpiralManipulator spiral) + { + var points = spiral.NomaiTextLine.GetPoints() + .Select(p => spiral.transform.TransformPoint(p)) + .Select(p => spiral.transform.parent.InverseTransformPoint(p)) + .ToList(); + + foreach(var point in points) { + if (point.x < minX || point.x > maxX || + point.y < minY || point.y > maxY) + { + return true; + } + } + + return false; + } + + private bool AttemptToPushSpiralInBounds(SpiralManipulator s1, int start) + { + var range = Mathf.Max(start-SpiralManipulator.MIN_PARENT_POINT, SpiralManipulator.MAX_PARENT_POINT-start); + + for (var i = 1; i <= range; i++) + { + if (start-i >= SpiralManipulator.MIN_PARENT_POINT) + { + s1.PlaceOnParentPoint(start-i); + if (!OutsideBounds(s1)) return true; + } + + if (start+i <= SpiralManipulator.MAX_PARENT_POINT) + { + s1.PlaceOnParentPoint(start+i); + if (!OutsideBounds(s1)) return true; + } + } + + return false; + } + + public void DrawBoundsWithDebugSpheres() + { + AddDebugShape.AddSphere(this.gameObject, 0.1f, Color.green).transform.localPosition = new Vector3(minX, minY, 0); + AddDebugShape.AddSphere(this.gameObject, 0.1f, Color.green).transform.localPosition = new Vector3(minX, maxY, 0); + AddDebugShape.AddSphere(this.gameObject, 0.1f, Color.green).transform.localPosition = new Vector3(maxX, maxY, 0); + AddDebugShape.AddSphere(this.gameObject, 0.1f, Color.green).transform.localPosition = new Vector3(maxX, minY, 0); + AddDebugShape.AddSphere(this.gameObject, 0.1f, Color.red).transform.localPosition = new Vector3(0, 0, 0); + } + + #endregion bounds handling + } + + public class SpiralManipulator : MonoBehaviour { + public SpiralManipulator parent; + public List children = new List(); + + public HashSet pointsOccupiedByChildren = new HashSet(); + public int _parentPointIndex = -1; + + public static int MIN_PARENT_POINT = 3; + public static int MAX_PARENT_POINT = 26; + + #region properties + + public bool Mirrored { get { return this.transform.localScale.x < 0; } } + + private NomaiTextLine _NomaiTextLine; + public NomaiTextLine NomaiTextLine + { + get + { + if (_NomaiTextLine == null) _NomaiTextLine = GetComponent(); + return _NomaiTextLine; + } + } + + public Vector2 center + { + get { return NomaiTextLine.GetWorldCenter(); } + } + + public Vector2 localPosition + { + get { return new Vector2(this.transform.localPosition.x, this.transform.localPosition.y); } + } + public Vector2 position + { + get { return new Vector2(this.transform.position.x, this.transform.position.y); } + } + + #endregion properties + + public SpiralManipulator AddChild(NomaiTextArcBuilder.SpiralProfile profile) { + var child = NomaiTextArcArranger.CreateSpiral(profile, this.transform.parent.gameObject); + + var index = Random.Range(MIN_PARENT_POINT, MAX_PARENT_POINT); + child.transform.parent = this.transform.parent; + child.parent = this; + child.PlaceOnParentPoint(index); + + this.children.Add(child); + return child; + } + + public void Mirror() + { + this.transform.localScale = new Vector3(-this.transform.localScale.x, 1, 1); + if (this.parent != null) this.PlaceOnParentPoint(this._parentPointIndex); + } + + public void UpdateChildren() + { + foreach(var child in this.children) + { + child.PlaceOnParentPoint(child._parentPointIndex); + } + } + + public int PlaceOnParentPoint(int parentPointIndex, bool updateChildren=true) + { + // validate + var _points = parent.GetComponent().GetPoints(); + parentPointIndex = Mathf.Max(0, Mathf.Min(parentPointIndex, _points.Length-1)); + + // track occupied points + if (this._parentPointIndex != -1) parent.pointsOccupiedByChildren.Remove(this._parentPointIndex); + this._parentPointIndex = parentPointIndex; + parent.pointsOccupiedByChildren.Add(parentPointIndex); + + // calculate the normal + var normal = _points[Mathf.Min(parentPointIndex+1, _points.Length-1)] - _points[Mathf.Max(parentPointIndex-1, 0)]; + if (parent.transform.localScale.x < 0) normal = new Vector3(normal.x, -normal.y, -normal.z); + float rot = Mathf.Atan2(normal.y, normal.x) * Mathf.Rad2Deg; + + // get location of the point + var point = _points[parentPointIndex]; + if (parent.transform.localScale.x < 0) point = new Vector3(-point.x, point.y, point.z); + + // finalize + this.transform.localPosition = Quaternion.Euler(0, 0, parent.transform.localEulerAngles.z) * point + parent.transform.localPosition; + this.transform.localEulerAngles = new Vector3(0, 0, rot + parent.transform.localEulerAngles.z); + if (updateChildren) this.UpdateChildren(); + + return parentPointIndex; + } + } +} \ No newline at end of file diff --git a/NewHorizons/Builder/Props/TranslatorText/NomaiTextArcBuilder.cs b/NewHorizons/Builder/Props/TranslatorText/NomaiTextArcBuilder.cs new file mode 100644 index 00000000..cb7343fa --- /dev/null +++ b/NewHorizons/Builder/Props/TranslatorText/NomaiTextArcBuilder.cs @@ -0,0 +1,473 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace NewHorizons.Builder.Props +{ + public static class NomaiTextArcBuilder { + // TODO: stranger arcs + // Note: building a wall text (making meshes and arranging) takes 0.1s for an example with 10 spirals + // TODO: caching - maybe make a "cachable" annotaion! if cache does not contain results of function, run function, write results to cache file. otherwise return results from cache file. + // cache file should be shipped with release but doesn't need to be. if debug mode is enabled, always regen cache, if click regen configs, reload cache + + public static GameObject BuildSpiralGameObject(SpiralProfile profile, string goName="New Nomai Spiral") + { + var m = new SpiralMesh(profile); + m.Randomize(); + m.updateMesh(); + + // + // rotate mesh to point up + // + + var norm = m.skeleton[1] - m.skeleton[0]; + float r = Mathf.Atan2(-norm.y, norm.x) * Mathf.Rad2Deg; + var ang = -90-r; + + // using m.sharedMesh causes old meshes to disappear for some reason, idk why + var mesh = m.mesh; + var newVerts = mesh.vertices.Select(v => Quaternion.Euler(-90, 0, 0) * Quaternion.Euler(0, ang, 0) * v).ToArray(); + mesh.vertices = newVerts; + mesh.RecalculateBounds(); + + // rotate the skeleton to point up, too + var _points = m.skeleton + .Select((point) => + Quaternion.Euler(-90, 0, 0) * Quaternion.Euler(0, ang, 0) * (new Vector3(point.x, 0, point.y)) + ) + .ToArray(); + + + return BuildSpiralGameObject(_points, mesh, goName); + } + + public static GameObject BuildSpiralGameObject(Vector3[] _points, Mesh mesh, string goName="New Nomai Spiral") + { + var g = new GameObject(goName); + g.SetActive(false); + g.transform.localPosition = Vector3.zero; + g.transform.localEulerAngles = Vector3.zero; + + g.AddComponent().sharedMesh = mesh; + g.AddComponent().sharedMaterial = new Material(Shader.Find("Sprites/Default")); + g.GetComponent().sharedMaterial.color = Color.magenta; + + var owNomaiTextLine = g.AddComponent(); + + owNomaiTextLine._points = _points; + owNomaiTextLine._active = true; + owNomaiTextLine._prebuilt = false; + + g.SetActive(true); + return g; + } + + #region spiral shape definitions + + public struct SpiralProfile { + // all of the Vector2 params here refer to a range of valid values + public string profileName; + public Vector2 a; + public Vector2 b; + public Vector2 startS; + public Vector2 endS; + public Vector2 skeletonScale; + public int numSkeletonPoints; + public float uvScale; + public float innerWidth; // width at the tip + public float outerWidth; // width at the base + public Material material; + } + + public static SpiralProfile adultSpiralProfile = new SpiralProfile() { + profileName="Adult", + a = new Vector2(0.5f, 0.5f), + b = new Vector2(0.3f, 0.6f), + startS = new Vector2(342.8796f, 342.8796f), + endS = new Vector2(0, 50f), + skeletonScale = 0.75f * new Vector2(0.01f, 0.01f), + numSkeletonPoints = 51, + + innerWidth = 0.001f, + outerWidth = 0.05f, + uvScale = 4.9f, + }; + + public static SpiralProfile childSpiralProfile = new SpiralProfile() { + profileName="Child", + a = new Vector2(0.9f, 0.9f), + b = new Vector2(0.17f, 0.4f), + startS = new Vector2(342.8796f, 342.8796f), + endS = new Vector2(35f, 25f), + skeletonScale = 0.8f * new Vector2(0.01f, 0.01f), + numSkeletonPoints = 51, + + innerWidth = 0.001f/10f, + outerWidth = 2f*0.05f, + uvScale = 4.9f * 0.55f, + }; + + public static SpiralProfile strangerSpiralProfile = new SpiralProfile() { + profileName="Stranger", + a = new Vector2(0.9f, 0.9f), // this value doesn't really matter for this + b = new Vector2(5f, 5f), + startS = new Vector2(1.8505f, 1.8505f), + endS = new Vector2(0, 0), + skeletonScale = new Vector2(0.6f, 0.6f), + numSkeletonPoints = 17, + + innerWidth = 0.75f, + outerWidth = 0.75f, + uvScale = 1f/1.8505f, + + + }; + + + #endregion spiral shape definitions + + #region mesh generation + + public class SpiralMesh: MathematicalSpiral { + public List skeleton; + public List skeletonOutsidePoints; + + public int numSkeletonPoints = 51; // seems to be Mobius' default + + public float innerWidth = 0.001f; // width at the tip + public float outerWidth = 0.05f; // width at the base + public float uvScale = 4.9f; + private float baseUVScale = 1f / 300f; + public float uvOffset = 0; + + public Mesh mesh; + + public SpiralMesh(SpiralProfile profile): base(profile) { + this.numSkeletonPoints = profile.numSkeletonPoints; + this.innerWidth = profile.innerWidth; + this.outerWidth = profile.outerWidth; + this.uvScale = profile.uvScale; + + this.uvOffset = UnityEngine.Random.value; + } + + public override void Randomize() { + base.Randomize(); + uvOffset = UnityEngine.Random.value; // this way even two spirals that are exactly the same shape will look different (this changes the starting point of the handwriting texture) + } + + internal void updateMesh() { + skeleton = this.getSkeleton(numSkeletonPoints); + skeletonOutsidePoints = this.getSkeletonOutsidePoints(numSkeletonPoints); + + List vertsSide1 = skeleton.Select((skeletonPoint, index) => { + Vector3 normal = new Vector3(cos(skeletonPoint.z), 0, sin(skeletonPoint.z)); + float width = lerp(((float) index) / ((float) skeleton.Count()), outerWidth, innerWidth); + + return new Vector3(skeletonPoint.x, 0, skeletonPoint.y) + width * normal; + }).ToList(); + + List vertsSide2 = skeleton.Select((skeletonPoint, index) => { + Vector3 normal = new Vector3(cos(skeletonPoint.z), 0, sin(skeletonPoint.z)); + float width = lerp(((float) index) / ((float) skeleton.Count()), outerWidth, innerWidth); + + return new Vector3(skeletonPoint.x, 0, skeletonPoint.y) - width * normal; + }).ToList(); + + Vector3[] newVerts = vertsSide1.Zip(vertsSide2, (f, s) => new [] { + f, + s + }).SelectMany(f =>f).ToArray(); // interleave vertsSide1 and vertsSide2 + + List triangles = new List(); + for (int i = 0; i + public List getSkeleton(int numPoints) { + var skeleton = + WalkAlongSpiral(numPoints) + .Select(input => { + float inputS = input.y; + var skeletonPoint = getDrawnSpiralPointAndNormal(inputS); + return skeletonPoint; + }) + .Reverse() + .ToList(); + + return skeleton; + } + + public List getSkeletonOutsidePoints(int numPoints) { + var outsidePoints = + WalkAlongSpiral(numPoints) + .Select(input => { + float inputT = input.x; + float inputS = input.y; + + var skeletonPoint = getDrawnSpiralPointAndNormal(inputS); + + var deriv = spiralDerivative(inputT); + var outsidePoint = new Vector2(skeletonPoint.x, skeletonPoint.y) - (new Vector2(-deriv.y, deriv.x)).normalized * 0.1f; + return outsidePoint; + }) + .Reverse() + .ToList(); + + return outsidePoints; + } + + // generate a list of evenly distributed over the whole spiral. `numPoints` number of pairs are generated + public IEnumerable WalkAlongSpiral(int numPoints) { + var endT = tFromArcLen(startS); + var startT = tFromArcLen(endS); + var rangeT = endT - startT; + + for (int i = 0; i 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; + + if (!string.IsNullOrEmpty(info.rename)) + { + nomaiWallTextObj.name = info.rename; + } + + nomaiWallTextObj.transform.parent = sector?.transform ?? planetGO.transform; + + if (!string.IsNullOrEmpty(info.parentPath)) + { + var newParent = planetGO.transform.Find(info.parentPath); + if (newParent != null) + { + nomaiWallTextObj.transform.parent = newParent; + } + else + { + Logger.LogError($"Cannot find parent object at path: {planetGO.name}/{info.parentPath}"); + } + } + + var pos = (Vector3)(info.position ?? Vector3.zero); + if (info.isRelativeToParent) + { + nomaiWallTextObj.transform.localPosition = pos; + 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; + + nomaiWallTextObj.transform.up = up; + nomaiWallTextObj.transform.forward = forward; + } + if (info.rotation != null) + { + nomaiWallTextObj.transform.localRotation = Quaternion.Euler(info.rotation); + } + } + else + { + nomaiWallTextObj.transform.position = planetGO.transform.TransformPoint(pos); + 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; + + 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); + } + if (info.rotation != null) + { + nomaiWallTextObj.transform.rotation = planetGO.transform.TransformRotation(Quaternion.Euler(info.rotation)); + } + } + + // nomaiWallTextObj.GetComponent().DrawBoundsWithDebugSpheres(); + + nomaiWallTextObj.SetActive(true); + conversationInfoToCorrespondingSpawnedGameObject[info] = nomaiWallTextObj; + + return nomaiWallTextObj; + } + case PropModule.NomaiTextInfo.NomaiTextType.Scroll: + { + var customScroll = _scrollPrefab.InstantiateInactive(); + + if (!string.IsNullOrEmpty(info.rename)) + { + customScroll.name = info.rename; + } + else + { + customScroll.name = _scrollPrefab.name; + } + + 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; + + // Place scroll + customScroll.transform.parent = sector?.transform ?? planetGO.transform; + + if (!string.IsNullOrEmpty(info.parentPath)) + { + var newParent = planetGO.transform.Find(info.parentPath); + if (newParent != null) + { + customScroll.transform.parent = newParent; + } + else + { + Logger.LogError($"Cannot find parent object at path: {planetGO.name}/{info.parentPath}"); + } + } + + var pos = (Vector3)(info.position ?? Vector3.zero); + if (info.isRelativeToParent) customScroll.transform.localPosition = pos; + else customScroll.transform.position = planetGO.transform.TransformPoint(pos); + + var up = planetGO.transform.InverseTransformPoint(customScroll.transform.position).normalized; + if (info.rotation != null) + { + customScroll.transform.rotation = planetGO.transform.TransformRotation(Quaternion.Euler(info.rotation)); + } + else + { + customScroll.transform.rotation = Quaternion.FromToRotation(customScroll.transform.up, up) * customScroll.transform.rotation; + } + + customScroll.SetActive(true); + + // Enable the collider and renderer + Delay.RunWhen( + () => Main.IsSystemReady, + () => + { + 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; + } + ); + conversationInfoToCorrespondingSpawnedGameObject[info] = customScroll; + + return customScroll; + } + case PropModule.NomaiTextInfo.NomaiTextType.Computer: + { + var computerObject = _computerPrefab.InstantiateInactive(); + + if (!string.IsNullOrEmpty(info.rename)) + { + computerObject.name = info.rename; + } + else + { + computerObject.name = _computerPrefab.name; + } + + computerObject.transform.parent = sector?.transform ?? planetGO.transform; + + if (!string.IsNullOrEmpty(info.parentPath)) + { + var newParent = planetGO.transform.Find(info.parentPath); + if (newParent != null) + { + computerObject.transform.parent = newParent; + } + else + { + Logger.LogError($"Cannot find parent object at path: {planetGO.name}/{info.parentPath}"); + } + } + + var pos = (Vector3)(info.position ?? Vector3.zero); + if (info.isRelativeToParent) computerObject.transform.localPosition = pos; + else computerObject.transform.position = planetGO.transform.TransformPoint(pos); + + 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 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 cairnObject = (info.type == PropModule.NomaiTextInfo.NomaiTextType.CairnVariant ? _cairnVariantPrefab : _cairnPrefab).InstantiateInactive(); + + if (!string.IsNullOrEmpty(info.rename)) + { + cairnObject.name = info.rename; + } + else + { + cairnObject.name = _cairnPrefab.name; + } + + cairnObject.transform.parent = sector?.transform ?? planetGO.transform; + + if (!string.IsNullOrEmpty(info.parentPath)) + { + var newParent = planetGO.transform.Find(info.parentPath); + if (newParent != null) + { + cairnObject.transform.parent = newParent; + } + else + { + Logger.LogError($"Cannot find parent object at path: {planetGO.name}/{info.parentPath}"); + } + } + + var pos = (Vector3)(info.position ?? Vector3.zero); + if (info.isRelativeToParent) cairnObject.transform.localPosition = pos; + else cairnObject.transform.position = planetGO.transform.TransformPoint(pos); + + if (info.rotation != null) + { + var rot = Quaternion.Euler(info.rotation); + if (info.isRelativeToParent) cairnObject.transform.localRotation = rot; + else cairnObject.transform.rotation = planetGO.transform.TransformRotation(rot); + } + else + { + // By default align it to normal + var up = (cairnObject.transform.position - planetGO.transform.position).normalized; + cairnObject.transform.rotation = Quaternion.FromToRotation(Vector3.up, up) * cairnObject.transform.rotation; + } + + // 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 + }; + var recorderObject = DetailBuilder.Make(planetGO, sector, prefab, detailInfo); + recorderObject.SetActive(false); + + if (info.rotation == null) + { + var up = recorderObject.transform.position - planetGO.transform.position; + recorderObject.transform.rotation = Quaternion.FromToRotation(Vector3.up, up) * recorderObject.transform.rotation; + } + + 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 = _trailmarkerPrefab.InstantiateInactive(); + + if (!string.IsNullOrEmpty(info.rename)) + { + trailmarkerObject.name = info.rename; + } + else + { + trailmarkerObject.name = _trailmarkerPrefab.name; + } + + trailmarkerObject.transform.parent = sector?.transform ?? planetGO.transform; + + if (!string.IsNullOrEmpty(info.parentPath)) + { + var newParent = planetGO.transform.Find(info.parentPath); + if (newParent != null) + { + trailmarkerObject.transform.parent = newParent; + } + else + { + Logger.LogError($"Cannot find parent object at path: {planetGO.name}/{info.parentPath}"); + } + } + + var pos = (Vector3)(info.position ?? Vector3.zero); + if (info.isRelativeToParent) trailmarkerObject.transform.localPosition = pos; + else trailmarkerObject.transform.position = planetGO.transform.TransformPoint(pos); + + // 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; + + if (info.rotation != null) + { + var rot = Quaternion.Euler(info.rotation); + if (info.isRelativeToParent) trailmarkerObject.transform.localRotation = rot; + else trailmarkerObject.transform.rotation = planetGO.transform.TransformRotation(rot); + } + else + { + // By default align it to normal + var up = (trailmarkerObject.transform.position - planetGO.transform.position).normalized; + trailmarkerObject.transform.rotation = Quaternion.FromToRotation(Vector3.up, up) * trailmarkerObject.transform.rotation; + } + + // 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); + } + } + } +} diff --git a/NewHorizons/External/Modules/PropModule.cs b/NewHorizons/External/Modules/PropModule.cs index 2fbf0f0a..5ff5625e 100644 --- a/NewHorizons/External/Modules/PropModule.cs +++ b/NewHorizons/External/Modules/PropModule.cs @@ -33,10 +33,16 @@ namespace NewHorizons.External.Modules /// public GeyserInfo[] geysers; + /// + /// Add translatable text to this planet. (LEGACY - for use with pre-autospirals configs) + /// + [Obsolete("nomaiText is deprecated as of the release of auto spirals, instead please use translatorText with new configs.")] + public NomaiTextInfo[] nomaiText; + /// /// Add translatable text to this planet /// - public NomaiTextInfo[] nomaiText; + public NomaiTextInfo[] translatorText; /// /// Details which will be shown from 50km away. Meant to be lower resolution. @@ -638,7 +644,7 @@ namespace NewHorizons.External.Modules /// Additional information about each arc in the text /// public NomaiTextArcInfo[] arcInfo; - + /// /// The normal vector for this object. Used for writing on walls and positioning computers. /// @@ -703,6 +709,11 @@ namespace NewHorizons.External.Modules [EnumMember(Value = @"stranger")] Stranger = 2 } + + /// + /// Whether to skip modifying this spiral's placement, and instead keep the automatically determined placement. + /// + public bool keepAutoPlacement; /// /// Whether to flip the spiral from left-curling to right-curling or vice versa. diff --git a/NewHorizons/Handlers/PlanetCreationHandler.cs b/NewHorizons/Handlers/PlanetCreationHandler.cs index e0912246..ee7cbba5 100644 --- a/NewHorizons/Handlers/PlanetCreationHandler.cs +++ b/NewHorizons/Handlers/PlanetCreationHandler.cs @@ -136,6 +136,8 @@ namespace NewHorizons.Handlers public static bool LoadBody(NewHorizonsBody body, bool defaultPrimaryToSun = false) { + body.LoadCache(); + // I don't remember doing this why is it exceptions what am I doing GameObject existingPlanet = null; try @@ -202,6 +204,7 @@ namespace NewHorizons.Handlers catch (Exception ex) { Logger.LogError($"Couldn't make quantum state for [{body.Config.name}]:\n{ex}"); + body.UnloadCache(); return false; } } @@ -217,6 +220,7 @@ namespace NewHorizons.Handlers catch (Exception e) { Logger.LogError($"Couldn't update body {body.Config?.name}:\n{e}"); + body.UnloadCache(); return false; } } @@ -237,8 +241,12 @@ namespace NewHorizons.Handlers { Logger.Log($"Creating [{body.Config.name}]"); var planetObject = GenerateBody(body, defaultPrimaryToSun); - if (planetObject == null) return false; - planetObject.SetActive(true); + planetObject?.SetActive(true); + if (planetObject == null) + { + body.UnloadCache(); + return false; + } var ao = planetObject.GetComponent(); @@ -250,6 +258,7 @@ namespace NewHorizons.Handlers catch (Exception e) { Logger.LogError($"Couldn't generate body {body.Config?.name}:\n{e}"); + body.UnloadCache(); return false; } } @@ -264,6 +273,7 @@ namespace NewHorizons.Handlers Logger.LogError($"Error in event handler for OnPlanetLoaded on body {body.Config.name}: {e}"); } + body.UnloadCache(true); return true; } @@ -628,7 +638,7 @@ namespace NewHorizons.Handlers if (body.Config.Props != null) { - PropBuildManager.Make(go, sector, rb, body.Config, body.Mod); + PropBuildManager.Make(go, sector, rb, body); } if (body.Config.Volumes != null) diff --git a/NewHorizons/Main.cs b/NewHorizons/Main.cs index 93e4ed9c..eff0ff25 100644 --- a/NewHorizons/Main.cs +++ b/NewHorizons/Main.cs @@ -275,6 +275,7 @@ namespace NewHorizons GeyserBuilder.InitPrefab(); LavaBuilder.InitPrefabs(); NomaiTextBuilder.InitPrefabs(); + TranslatorTextBuilder.InitPrefabs(); RemoteBuilder.InitPrefabs(); SandBuilder.InitPrefabs(); SingularityBuilder.InitPrefabs(); diff --git a/NewHorizons/Schemas/body_schema.json b/NewHorizons/Schemas/body_schema.json index bc3099a1..92b1e581 100644 --- a/NewHorizons/Schemas/body_schema.json +++ b/NewHorizons/Schemas/body_schema.json @@ -947,7 +947,7 @@ "$ref": "#/definitions/GeyserInfo" } }, - "nomaiText": { + "translatorText": { "type": "array", "description": "Add translatable text to this planet", "items": { @@ -1345,6 +1345,10 @@ "type": "object", "additionalProperties": false, "properties": { + "keepAutoPlacement": { + "type": "boolean", + "description": "Whether to skip modifying this spiral's placement, and instead keep the automatically determined placement." + }, "mirror": { "type": "boolean", "description": "Whether to flip the spiral from left-curling to right-curling or vice versa." diff --git a/NewHorizons/Utility/Cache.cs b/NewHorizons/Utility/Cache.cs new file mode 100644 index 00000000..ba062551 --- /dev/null +++ b/NewHorizons/Utility/Cache.cs @@ -0,0 +1,70 @@ +using Newtonsoft.Json; +using OWML.Common; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Serialization; + +namespace NewHorizons.Utility +{ + public class Cache + { + string filepath; + IModBehaviour mod; + Dictionary data = new Dictionary(); + HashSet accessedKeys = new HashSet(); + + public Cache(IModBehaviour mod, string cacheFilePath) + { + this.mod = mod; + this.filepath = cacheFilePath; + var fullPath = mod.ModHelper.Manifest.ModFolderPath + cacheFilePath; + + if (!File.Exists(fullPath)) + { + Logger.LogWarning("Cache file not found! Cache path: " + cacheFilePath); + data = new Dictionary(); + return; + } + + var json = File.ReadAllText(fullPath); + data = JsonConvert.DeserializeObject>(json); + // the code above does exactly the same thing that the code below does, but the below for some reason always returns null. no clue why + // data = mod.ModHelper.Storage.Load>(filepath); + } + + public void WriteToFile() + { + mod.ModHelper.Storage.Save>(data, filepath); + } + + public bool ContainsKey(string key) + { + return data.ContainsKey(key); + } + + public T Get(string key) + { + accessedKeys.Add(key); + var json = data[key]; + return JsonConvert.DeserializeObject(json); + } + + public void Set(string key, T value) + { + accessedKeys.Add(key); + data[key] = JsonConvert.SerializeObject(value); + } + + public void ClearUnaccessed() + { + var keys = data.Keys.ToList(); + foreach(var key in keys) + { + if (accessedKeys.Contains(key)) continue; + data.Remove(key); + } + } + } +} diff --git a/NewHorizons/Utility/DebugMenu/DebugMenuNomaiText.cs b/NewHorizons/Utility/DebugMenu/DebugMenuNomaiText.cs index 99843836..07d192d8 100644 --- a/NewHorizons/Utility/DebugMenu/DebugMenuNomaiText.cs +++ b/NewHorizons/Utility/DebugMenu/DebugMenuNomaiText.cs @@ -100,7 +100,7 @@ namespace NewHorizons.Utility.DebugMenu ConversationMetadata conversationMetadata = new ConversationMetadata() { conversation = conversation, - conversationGo = NomaiTextBuilder.GetSpawnedGameObjectByNomaiTextInfo(conversation), + conversationGo = TranslatorTextBuilder.GetSpawnedGameObjectByNomaiTextInfo(conversation), planetConfig = config, spirals = new List(), collapsed = true @@ -120,7 +120,7 @@ namespace NewHorizons.Utility.DebugMenu SpiralMetadata metadata = new SpiralMetadata() { spiral = arcInfo, - spiralGo = NomaiTextBuilder.GetSpawnedGameObjectByNomaiTextArcInfo(arcInfo), + spiralGo = TranslatorTextBuilder.GetSpawnedGameObjectByNomaiTextArcInfo(arcInfo), conversation = conversation, planetConfig = config, planetName = config.name, @@ -266,7 +266,7 @@ namespace NewHorizons.Utility.DebugMenu GUILayout.Label("Variation"); GUILayout.BeginHorizontal(); - var varietyCount = GetVarietyCountForType(spiralMeta.spiral.type); + var varietyCount = 1; //var newVariation = int.Parse(GUILayout.TextField(spiralMeta.spiral.variation+"")); //newVariation = Mathf.Min(Mathf.Max(0, newVariation), varietyCount); //if (newVariation != spiralMeta.spiral.variation) changed = true; @@ -365,7 +365,7 @@ namespace NewHorizons.Utility.DebugMenu for (indexInParent = 0; indexInParent < wallTextComponent._textLines.Length; indexInParent++) if (oldTextLineComponent == wallTextComponent._textLines[indexInParent]) break; var textEntryId = oldTextLineComponent._entryID; GameObject.Destroy(spiralMeta.spiralGo); - spiralMeta.spiralGo = NomaiTextBuilder.MakeArc(spiralMeta.spiral, conversationZone, null, textEntryId); + spiralMeta.spiralGo = TranslatorTextBuilder.MakeArc(spiralMeta.spiral, conversationZone, null, textEntryId); wallTextComponent._textLines[indexInParent] = spiralMeta.spiralGo.GetComponent(); spiralMeta.spiralGo.name = "Brandnewspiral"; @@ -469,20 +469,6 @@ namespace NewHorizons.Utility.DebugMenu }); } - private int GetVarietyCountForType(NomaiTextArcInfo.NomaiTextArcType type) - { - switch(type) - { - case NomaiTextArcInfo.NomaiTextArcType.Stranger: - return NomaiTextBuilder.GetGhostArcPrefabs().Count(); - case NomaiTextArcInfo.NomaiTextArcType.Child: - return NomaiTextBuilder.GetChildArcPrefabs().Count(); - default: - case NomaiTextArcInfo.NomaiTextArcType.Adult: - return NomaiTextBuilder.GetArcPrefabs().Count(); - } - } - void UpdateConversationTransform(ConversationMetadata conversationMetadata, GameObject sectorParent) { var nomaiWallTextObj = conversationMetadata.conversationGo; diff --git a/NewHorizons/Utility/MMesh.cs b/NewHorizons/Utility/MMesh.cs new file mode 100644 index 00000000..6ab39722 --- /dev/null +++ b/NewHorizons/Utility/MMesh.cs @@ -0,0 +1,51 @@ +using System.ComponentModel; +using System.Linq; +using Newtonsoft.Json; +using UnityEngine; +namespace NewHorizons.Utility +{ + [JsonObject] + public class MMesh + { + public MMesh(MVector3[] vertices, int[] triangles, MVector3[] normals, MVector2[] uv, MVector2[] uv2) + { + this.vertices = vertices; + this.triangles = triangles; + this.normals = normals; + this.uv = uv; + this.uv2 = uv2; + } + + public MVector3[] vertices; + public int[] triangles; + public MVector3[] normals; + public MVector2[] uv; + public MVector2[] uv2; + + public static implicit operator MMesh(Mesh mesh) + { + return new MMesh + ( + mesh.vertices.Select(v => (MVector3)v).ToArray(), + mesh.triangles, + mesh.normals.Select(v => (MVector3)v).ToArray(), + mesh.uv.Select(v => (MVector2)v).ToArray(), + mesh.uv2.Select(v => (MVector2)v).ToArray() + ); + } + + public static implicit operator Mesh(MMesh mmesh) + { + var mesh = new Mesh(); + + mesh.vertices = mmesh.vertices.Select(mv => (Vector3)mv).ToArray(); + mesh.triangles = mmesh.triangles; + mesh.normals = mmesh.normals.Select(mv => (Vector3)mv).ToArray(); + mesh.uv = mmesh.uv.Select(mv => (Vector2)mv).ToArray(); + mesh.uv2 = mmesh.uv2.Select(mv => (Vector2)mv).ToArray(); + mesh.RecalculateBounds(); + + return mesh; + } + } +} diff --git a/NewHorizons/Utility/MeshUtilities.cs b/NewHorizons/Utility/MeshUtilities.cs new file mode 100644 index 00000000..b9f2d320 --- /dev/null +++ b/NewHorizons/Utility/MeshUtilities.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace NewHorizons.Utility +{ + public class MeshUtilities + { + public static Mesh RectangleMeshFromCorners(Vector3[] corners) + { + MVector3[] verts = corners.Select(v => (MVector3)v).ToArray(); + + int[] triangles = new int[] { + 0, 1, 2, + 1, 3, 2, + }; + + MVector3[] normals = new MVector3[verts.Length]; + for (int i = 0; i