Auto spiral placement 2 electric boogaloo (#519)

<!-- A new module or something else important -->
## Major features
- NOTE: "nomaiText" has been deprecated in order to preserve the legacy
nomai text generation. Please use "translatorText" in the future, it is
exactly the same except it lets you use the new automatic spiral
placement, caching, and so on
- Caching! 
   - Lots to say about this, both for nh devs and addon devs
   - Addon Devs:
- This feature introduces new .nhcache files, which New Horizons will
generate automatically. One nhcache file will be generated per planet
- Make sure to include these files when releasing your addon. This will
allow your players to have fast loading times on their first loop. If
you forget to include them, no worries, their first loop will be slow to
load but every following loop will be fast.
- ~~WARNING: before releasing, delete all your cache files and
regenerate them by opening the game~~
- ~~the reason for this is that as you develop, your caches will get
filled up with old data. Deleting them and letting them regenerate keeps
your release light~~
- Cache files automatically clear unused entries, so there's no need for
you to manage them, they should stay minimal on their own
   - NH Devs:
- Checkout the Cache property on NewHorizonBody. Use it how you want,
whether that's storing procedurally generated meshes or the private key
you brute forced using a botnet you embedded in new horizons.
- Actually scratch that second usecase. Caching doesn't support illegal
activity
- For an example of an actual use of caching, check out
NomaiTextBuilder.MakeArc()
      - Also read the section for Addon Devs if you haven't
   - How does auto spirals use the cache?
- When building a wall text, the cache is checked to see if it has been
built before, and if so, is loaded from there
- Whenever a NomaiTextArcInfo in the json is changed, the corresponding
wall text generates a new entry in the cache
- Also, whenever an xml file used by a wall text is changed, that wall
text will generate a new entry in the cache
- These old entries do accumulate during development of an addon, so
please make sure to check the note above for addon devs for how to deal
with that
- New Horizon's Nomai text arcs are now procedurally generated 
- Automatic Nomai text arc placement! 
- Nomai wall text is now automatically arranged to fit on a whiteboard,
no need to place text manually anymore!
   - Manual placement is still supported if you want it
- If you want to specify certain arcs to be written by children (or
strangers) instead of adults without requiring manual placement, use the
"keepAutoPlacement" property of ArcInfo
- I hope this feature makes wall text less intimidating to include in
addons, I know it'll save me literal hours
- Despite being listed last here, this is the actual main feature of
this PR


<!-- A new parameter added to a module, or API feature -->
## Minor features
-

<!-- Some improvement that requires no action on the part of add-on
creators i.e., improved star graphics -->
## Improvements
- Everything technically goes here too, except maybe caching. Auto
spiral placement is automatic unless overriden by specifying arcInfo
(without using the "keepAutoPlacement" property)

<!-- Be sure to reference the existing issue if it exists -->
## Bug fixes
-
This commit is contained in:
FreezeDriedMangos 2023-02-21 18:11:37 -05:00 committed by GitHub
commit b325f87f5f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1972 additions and 32 deletions

View File

@ -14,6 +14,9 @@ using OWML.Utils;
namespace NewHorizons.Builder.Props namespace NewHorizons.Builder.Props
{ {
/// <summary>
/// Legacy - this class is used with the deprecated "nomaiText" module (deprecated on release of autospirals)
/// </summary>
public static class NomaiTextBuilder public static class NomaiTextBuilder
{ {
private static List<GameObject> _arcPrefabs; private static List<GameObject> _arcPrefabs;

View File

@ -1,6 +1,7 @@
using NewHorizons.Builder.Body; using NewHorizons.Builder.Body;
using NewHorizons.Builder.ShipLog; using NewHorizons.Builder.ShipLog;
using NewHorizons.External.Configs; using NewHorizons.External.Configs;
using NewHorizons.Utility;
using OWML.Common; using OWML.Common;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -10,8 +11,11 @@ namespace NewHorizons.Builder.Props
{ {
public static class PropBuildManager 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) if (config.Props.scatter != null)
{ {
try try
@ -128,7 +132,22 @@ namespace NewHorizons.Builder.Props
{ {
try 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) catch (Exception ex)
{ {
@ -205,7 +224,7 @@ namespace NewHorizons.Builder.Props
{ {
try try
{ {
RemoteBuilder.Make(go, sector, remoteInfo, mod); RemoteBuilder.Make(go, sector, remoteInfo, nhBody);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -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(); InitPrefabs();
var mod = nhBody.Mod;
var id = RemoteHandler.GetPlatformID(info.id); var id = RemoteHandler.GetPlatformID(info.id);
Texture2D decal = Texture2D.whiteTexture; Texture2D decal = Texture2D.whiteTexture;
@ -142,7 +143,7 @@ namespace NewHorizons.Builder.Props
{ {
try try
{ {
RemoteBuilder.MakeWhiteboard(go, sector, id, decal, info.whiteboard, mod); RemoteBuilder.MakeWhiteboard(go, sector, id, decal, info.whiteboard, nhBody);
} }
catch (Exception ex) 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() var detailInfo = new PropModule.DetailInfo()
{ {
@ -193,7 +194,7 @@ namespace NewHorizons.Builder.Props
{ {
var textInfo = info.nomaiText[i]; var textInfo = info.nomaiText[i];
component._remoteIDs[i] = RemoteHandler.GetPlatformID(textInfo.id); 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, arcInfo = textInfo.arcInfo,
location = textInfo.location, location = textInfo.location,
@ -204,7 +205,7 @@ namespace NewHorizons.Builder.Props
seed = textInfo.seed, seed = textInfo.seed,
type = PropModule.NomaiTextInfo.NomaiTextType.Wall, type = PropModule.NomaiTextInfo.NomaiTextType.Wall,
xmlFile = textInfo.xmlFile xmlFile = textInfo.xmlFile
}, mod).GetComponent<NomaiWallText>(); }, nhBody).GetComponent<NomaiWallText>();
wallText._showTextOnStart = false; wallText._showTextOnStart = false;
component._nomaiTexts[i] = wallText; component._nomaiTexts[i] = wallText;
} }

View File

@ -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<SpiralManipulator> spirals = new List<SpiralManipulator>();
public List<SpiralManipulator> reverseToposortedSpirals = null;
private bool updateToposortOnNextStep = true;
private Dictionary<int, int> sprialOverlapResolutionPriority = new Dictionary<int, int>();
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<SpiralManipulator>();
if (Random.value < 0.5) manip.transform.localScale = new Vector3(-1, 1, 1); // randomly mirror
// add to arranger
var arranger = spiralMeshHolder.GetAddComponent<NomaiTextArcArranger>();
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<SpiralManipulator, Vector2> childForces = new Dictionary<SpiralManipulator, Vector2>();
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<NomaiTextLine>().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<SpiralManipulator>();
Queue<SpiralManipulator> frontierQueue = new Queue<SpiralManipulator>();
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<SpiralManipulator> children = new List<SpiralManipulator>();
public HashSet<int> pointsOccupiedByChildren = new HashSet<int>();
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<NomaiTextLine>();
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<NomaiTextLine>().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;
}
}
}

View File

@ -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<MeshFilter>().sharedMesh = mesh;
g.AddComponent<MeshRenderer>().sharedMaterial = new Material(Shader.Find("Sprites/Default"));
g.GetComponent<MeshRenderer>().sharedMaterial.color = Color.magenta;
var owNomaiTextLine = g.AddComponent<NomaiTextLine>();
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<Vector3> skeleton;
public List<Vector2> 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<Vector3> 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<Vector3> 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<int> triangles = new List<int>();
for (int i = 0; i<newVerts.Length - 2; i += 2) {
/*
| |
| |
2 *-----* 3
| |
| |
| |
0 *-----* 1
| |
*/
triangles.Add(i + 2);
triangles.Add(i + 1);
triangles.Add(i);
triangles.Add(i + 2);
triangles.Add(i + 3);
triangles.Add(i + 1);
}
var startT = tFromArcLen(endS);
var endT = tFromArcLen(startS);
var rangeT = endT - startT;
var rangeS = startS - endS;
Vector2[] uvs = new Vector2[newVerts.Length];
Vector2[] uv2s = new Vector2[newVerts.Length];
for (int i = 0; i<skeleton.Count(); i++) {
float fraction = 1 - ((float) i) / ((float) skeleton.Count()); // casting is so uuuuuuuugly
// note: cutting the sprial into numPoints equal slices of arclen would
// provide evenly spaced skeleton points
// on the other hand, cutting the spiral into numPoints equal slices of t
// will cluster points in areas of higher detail. this is the way Mobius does it, so it is the way we also will do it
float inputT = startT + rangeT * fraction;
float inputS = tToArcLen(inputT);
float sFraction = (inputS - endS) / rangeS;
float absoluteS = (inputS - endS);
float u = absoluteS * uvScale * baseUVScale + uvOffset;
uvs[i * 2] = new Vector2(u, 0);
uvs[i * 2 + 1] = new Vector2(u, 1);
uv2s[i * 2] = new Vector2(1 - sFraction, 0);
uv2s[i * 2 + 1] = new Vector2(1 - sFraction, 1);
}
Vector3[] normals = new Vector3[newVerts.Length];
for (int i = 0; i<newVerts.Length; i++) normals[i] = new Vector3(0, 0, 1);
if (mesh == null){
mesh = new Mesh();
}
mesh.vertices = newVerts.ToArray();
mesh.triangles = triangles.ToArray().Reverse().ToArray(); // triangles need to be reversed so the spirals face the right way (I generated them backwards above, on accident)
mesh.uv = uvs;
mesh.uv2 = uv2s;
mesh.normals = normals;
mesh.RecalculateBounds();
}
}
#endregion mesh generation
#region underlying math
// NOTE: startS is greater than endS because the math equation traces the spiral outward - it starts at the center
// and winds its way out. However, since we want to think of the least curly part as the start, that means we
// start at a higher S and end at a lower S
//
// note: t refers to theta, and s refers to arc length
//
// All this math is based off this Desmos graph I made. Play around with it if something doesn't make sense :)
// https://www.desmos.com/calculator/9gdfgyuzf6
public class MathematicalSpiral {
public float a;
public float b;
public float startSOnParent;
public float scale;
public float x;
public float y;
public float ang;
public float endS = 42.87957f;
public float startS = 342.8796f;
SpiralProfile profile;
public MathematicalSpiral(SpiralProfile profile) {
this.profile = profile;
this.Randomize();
}
public MathematicalSpiral(float startSOnParent = 0, float len = 300, float a = 0.5f, float b = 0.43f, float scale = 0.01f) {
this.a = a;
this.b = b;
this.startSOnParent = startSOnParent;
this.scale = scale;
this.x = 0;
this.y = 0;
this.ang = 0;
}
public virtual void Randomize() {
this.a = UnityEngine.Random.Range(profile.a.x, profile.a.y);
this.b = UnityEngine.Random.Range(profile.b.x, profile.b.y);
this.endS = UnityEngine.Random.Range(profile.endS.x, profile.endS.y);
this.startS = UnityEngine.Random.Range(profile.startS.x, profile.startS.y);
this.scale = UnityEngine.Random.Range(profile.skeletonScale.x, profile.skeletonScale.y);
}
internal virtual void updateChild(MathematicalSpiral child) {
Vector3 pointAndNormal = getDrawnSpiralPointAndNormal(child.startSOnParent);
var cx = pointAndNormal.x;
var cy = pointAndNormal.y;
var cang = pointAndNormal.z;
child.x = cx;
child.y = cy;
child.ang = cang;
}
// note: each Vector3 in this list is of form <x, y, angle in radians of the normal at this point>
public List<Vector3> 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<Vector2> 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 <inputT, inputS> evenly distributed over the whole spiral. `numPoints` number of <inputT, inputS> pairs are generated
public IEnumerable<Vector2> WalkAlongSpiral(int numPoints) {
var endT = tFromArcLen(startS);
var startT = tFromArcLen(endS);
var rangeT = endT - startT;
for (int i = 0; i<numPoints; i++) {
float fraction = ((float) i) / ((float) numPoints - 1f); // casting is so uuuuuuuugly
// note: cutting the sprial into numPoints equal slices of arclen would
// provide evenly spaced skeleton points
// on the other hand, cutting the spiral into numPoints equal slices of t
// will cluster points in areas of higher detail. this is the way Mobius does it, so it is the way we also will do it
float inputT = startT + rangeT * fraction;
float inputS = tToArcLen(inputT);
yield return new Vector2(inputT, inputS);
}
}
// get the (x, y) coordinates and the normal angle at the given location (measured in arcLen) of a spiral with the given parameters
// note: arcLen is inverted so that 0 refers to what we consider the start of the Nomai spiral
public Vector3 getDrawnSpiralPointAndNormal(float arcLen) {
float offsetX = this.x;
float offsetY = this.y;
float offsetAngle = this.ang;
var startT = tFromArcLen(startS); // this is the `t` value for the root of the spiral (the end of the non-curled side)
var startPoint = spiralPoint(startT); // and this is the (x,y) location of the non-curled side, relative to the rest of the spiral. we'll offset everything so this is at (0,0) later
var startX = startPoint.x;
var startY = startPoint.y;
var t = tFromArcLen(arcLen);
var point = spiralPoint(t); // the absolute (x,y) location that corresponds to `arcLen`, before accounting for things like putting the start point at (0,0), or dealing with offsetX/offsetY/offsetAngle
var x = point.x;
var y = point.y;
var ang = normalAngle(t);
// translate so that startPoint is at (0,0)
// (also scale the spiral)
var retX = scale * (x - startX);
var retY = scale * (y - startY);
// rotate offsetAngle rads
var retX2 = retX * cos(offsetAngle) -
retY * sin(offsetAngle);
var retY2 = retX * sin(offsetAngle) +
retY * cos(offsetAngle);
retX = retX2;
retY = retY2;
// translate for offsetX, offsetY
retX += offsetX;
retY += offsetY;
return new Vector3(retX, retY, ang + offsetAngle + Mathf.PI / 2f);
}
// the base formula for the spiral
public Vector2 spiralPoint(float t) {
var r = a * exp(b * t);
var retval = new Vector2(r * cos(t), r * sin(t));
return retval;
}
// the spiral's got two functions: x(t) and y(t)
// so it's got two derrivatives (with respect to t) x'(t) and y'(t)
public Vector2 spiralDerivative(float t) { // derrivative with respect to t
var r = a * exp(b * t);
return new Vector2(
-r * (sin(t) - b * cos(t)),
r * (b * sin(t) + cos(t))
);
}
// returns the length of the spiral between t0 and t1
public float spiralArcLength(float t0, float t1) {
return (a / b) * sqrt(b * b + 1) * (exp(b * t1) - exp(b * t0));
}
// converts from a value of t to the equivalent value of s (the value of s that corresponds to the same point on the spiral as t)
public float tToArcLen(float t) {
return spiralArcLength(0, t);
}
// reverse of above
public float tFromArcLen(float s) {
return ln(
1 + s / (
(a / b) * sqrt(b * b + 1)
)
) / b;
}
// returns the angle of the spiral's normal at a given point
public float normalAngle(float t) {
var d = spiralDerivative(t);
var n = new Vector2(d.y, -d.x);
var angle = Mathf.Atan2(n.y, n.x);
return angle - Mathf.PI / 2;
}
}
// convenience, so the math above is more readable
private static float lerp(float a, float b, float t) {
return a * t + b * (1 - t);
}
private static float cos(float t) {
return Mathf.Cos(t);
}
private static float sin(float t) {
return Mathf.Sin(t);
}
private static float exp(float t) {
return Mathf.Exp(t);
}
private static float sqrt(float t) {
return Mathf.Sqrt(t);
}
private static float ln(float t) {
return Mathf.Log(t);
}
#endregion underlying math
}
}

View File

@ -0,0 +1,845 @@
using NewHorizons.External.Modules;
using NewHorizons.Handlers;
using NewHorizons.Utility;
using OWML.Common;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using UnityEngine;
using Enum = System.Enum;
using Logger = NewHorizons.Utility.Logger;
using Random = UnityEngine.Random;
using OWML.Utils;
using Newtonsoft.Json;
using System;
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<PropModule.NomaiTextArcInfo, GameObject> arcInfoToCorrespondingSpawnedGameObject = new Dictionary<PropModule.NomaiTextArcInfo, GameObject>();
public static GameObject GetSpawnedGameObjectByNomaiTextArcInfo(PropModule.NomaiTextArcInfo arc)
{
if (!arcInfoToCorrespondingSpawnedGameObject.ContainsKey(arc)) return null;
return arcInfoToCorrespondingSpawnedGameObject[arc];
}
private static Dictionary<PropModule.NomaiTextInfo, GameObject> conversationInfoToCorrespondingSpawnedGameObject = new Dictionary<PropModule.NomaiTextInfo, GameObject>();
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<MeshRenderer>()
.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<MeshRenderer>()
.sharedMaterial;
}
if (_ghostArcMaterial == null)
{
_ghostArcMaterial = SearchUtilities.Find("RingWorld_Body/Sector_RingInterior/Sector_Zone1/Interactables_Zone1/Props_IP_ZoneSign_1/Arc_TestAlienWriting/Arc 1")
.GetComponent<MeshRenderer>()
.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<NomaiTextArcArranger>().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<Collider>().enabled = false;
nomaiWallText.gameObject.SetActive(true);
var scrollItem = customScroll.GetComponent<ScrollItem>();
// 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<OWCollider>() };
// Else when you put them down you can't pick them back up
customScroll.GetComponent<OWCollider>()._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<MeshRenderer>().enabled = true;
customScroll.transform.Find("Props_NOM_Scroll/Props_NOM_Scroll_Collider").gameObject.SetActive(true);
nomaiWallText.gameObject.GetComponent<Collider>().enabled = false;
customScroll.GetComponent<CapsuleCollider>().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<NomaiComputer>();
computer.SetSector(sector);
computer._location = EnumUtils.Parse<NomaiText.Location>(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<NomaiVesselComputer>();
computer.SetSector(sector);
computer._location = EnumUtils.Parse<NomaiText.Location>(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<NomaiVesselComputerRing>();
//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<NomaiCairn>()._rocks)
{
rock._returning = false;
rock._owCollider.SetActivation(true);
rock.enabled = false;
}
// So we can actually knock it over
cairnObject.GetComponent<CapsuleCollider>().enabled = true;
var nomaiWallText = cairnObject.transform.Find("Props_TH_ClutterSmall/Arc_Short").GetComponent<NomaiWallText>();
nomaiWallText.SetSector(sector);
nomaiWallText._location = EnumUtils.Parse<NomaiText.Location>(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>();
nomaiText.SetSector(sector);
nomaiText._location = EnumUtils.Parse<NomaiText.Location>(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<SphereShape>().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>();
nomaiWallText.SetSector(sector);
nomaiWallText._location = EnumUtils.Parse<NomaiText.Location>(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<BoxCollider>();
box.center = new Vector3(-0.0643f, 1.1254f, 0f);
box.size = new Vector3(6.1424f, 5.2508f, 0.5f);
box.isTrigger = true;
nomaiWallTextObj.AddComponent<OWCollider>();
var nomaiWallText = nomaiWallTextObj.AddComponent<NomaiWallText>();
nomaiWallText._location = EnumUtils.Parse<NomaiText.Location>(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<int, GameObject>();
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<ArcCacheData[]>(cacheKey);
var arranger = nomaiWallText.gameObject.AddComponent<NomaiTextArcArranger>();
// 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<MeshFilter>().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<SpiralManipulator>().AddChild(profile).gameObject;
else arc = NomaiTextArcArranger.CreateSpiral(profile, conversationZone).gameObject;
}
if (mat != null) arc.GetComponent<MeshRenderer>().sharedMaterial = mat;
arc.transform.parent = conversationZone.transform;
arc.GetComponent<NomaiTextLine>()._prebuilt = false;
arc.GetComponent<NomaiTextLine>().SetEntryID(textEntryID);
arc.GetComponent<MeshRenderer>().enabled = false;
if (overrideMesh != null)
arc.GetComponent<MeshFilter>().sharedMesh = overrideMesh;
if (overrideColor != null)
arc.GetComponent<NomaiTextLine>()._targetColor = (Color)overrideColor;
arc.SetActive(true);
if (arcInfo != null) arcInfoToCorrespondingSpawnedGameObject[arcInfo] = arc;
return arc;
}
private static Dictionary<int, NomaiText.NomaiTextData> MakeNomaiTextDict(string xmlPath)
{
var dict = new Dictionary<int, NomaiText.NomaiTextData>();
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);
}
}
}
}

View File

@ -33,10 +33,16 @@ namespace NewHorizons.External.Modules
/// </summary> /// </summary>
public GeyserInfo[] geysers; public GeyserInfo[] geysers;
/// <summary>
/// Add translatable text to this planet. (LEGACY - for use with pre-autospirals configs)
/// </summary>
[Obsolete("nomaiText is deprecated as of the release of auto spirals, instead please use translatorText with new configs.")]
public NomaiTextInfo[] nomaiText;
/// <summary> /// <summary>
/// Add translatable text to this planet /// Add translatable text to this planet
/// </summary> /// </summary>
public NomaiTextInfo[] nomaiText; public NomaiTextInfo[] translatorText;
/// <summary> /// <summary>
/// Details which will be shown from 50km away. Meant to be lower resolution. /// Details which will be shown from 50km away. Meant to be lower resolution.
@ -704,6 +710,11 @@ namespace NewHorizons.External.Modules
[EnumMember(Value = @"stranger")] Stranger = 2 [EnumMember(Value = @"stranger")] Stranger = 2
} }
/// <summary>
/// Whether to skip modifying this spiral's placement, and instead keep the automatically determined placement.
/// </summary>
public bool keepAutoPlacement;
/// <summary> /// <summary>
/// Whether to flip the spiral from left-curling to right-curling or vice versa. /// Whether to flip the spiral from left-curling to right-curling or vice versa.
/// </summary> /// </summary>

View File

@ -136,6 +136,8 @@ namespace NewHorizons.Handlers
public static bool LoadBody(NewHorizonsBody body, bool defaultPrimaryToSun = false) 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 // I don't remember doing this why is it exceptions what am I doing
GameObject existingPlanet = null; GameObject existingPlanet = null;
try try
@ -202,6 +204,7 @@ namespace NewHorizons.Handlers
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError($"Couldn't make quantum state for [{body.Config.name}]:\n{ex}"); Logger.LogError($"Couldn't make quantum state for [{body.Config.name}]:\n{ex}");
body.UnloadCache();
return false; return false;
} }
} }
@ -217,6 +220,7 @@ namespace NewHorizons.Handlers
catch (Exception e) catch (Exception e)
{ {
Logger.LogError($"Couldn't update body {body.Config?.name}:\n{e}"); Logger.LogError($"Couldn't update body {body.Config?.name}:\n{e}");
body.UnloadCache();
return false; return false;
} }
} }
@ -237,8 +241,12 @@ namespace NewHorizons.Handlers
{ {
Logger.Log($"Creating [{body.Config.name}]"); Logger.Log($"Creating [{body.Config.name}]");
var planetObject = GenerateBody(body, defaultPrimaryToSun); 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<NHAstroObject>(); var ao = planetObject.GetComponent<NHAstroObject>();
@ -250,6 +258,7 @@ namespace NewHorizons.Handlers
catch (Exception e) catch (Exception e)
{ {
Logger.LogError($"Couldn't generate body {body.Config?.name}:\n{e}"); Logger.LogError($"Couldn't generate body {body.Config?.name}:\n{e}");
body.UnloadCache();
return false; return false;
} }
} }
@ -264,6 +273,7 @@ namespace NewHorizons.Handlers
Logger.LogError($"Error in event handler for OnPlanetLoaded on body {body.Config.name}: {e}"); Logger.LogError($"Error in event handler for OnPlanetLoaded on body {body.Config.name}: {e}");
} }
body.UnloadCache(true);
return true; return true;
} }
@ -628,7 +638,7 @@ namespace NewHorizons.Handlers
if (body.Config.Props != null) 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) if (body.Config.Volumes != null)

View File

@ -275,6 +275,7 @@ namespace NewHorizons
GeyserBuilder.InitPrefab(); GeyserBuilder.InitPrefab();
LavaBuilder.InitPrefabs(); LavaBuilder.InitPrefabs();
NomaiTextBuilder.InitPrefabs(); NomaiTextBuilder.InitPrefabs();
TranslatorTextBuilder.InitPrefabs();
RemoteBuilder.InitPrefabs(); RemoteBuilder.InitPrefabs();
SandBuilder.InitPrefabs(); SandBuilder.InitPrefabs();
SingularityBuilder.InitPrefabs(); SingularityBuilder.InitPrefabs();

View File

@ -947,7 +947,7 @@
"$ref": "#/definitions/GeyserInfo" "$ref": "#/definitions/GeyserInfo"
} }
}, },
"nomaiText": { "translatorText": {
"type": "array", "type": "array",
"description": "Add translatable text to this planet", "description": "Add translatable text to this planet",
"items": { "items": {
@ -1345,6 +1345,10 @@
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"keepAutoPlacement": {
"type": "boolean",
"description": "Whether to skip modifying this spiral's placement, and instead keep the automatically determined placement."
},
"mirror": { "mirror": {
"type": "boolean", "type": "boolean",
"description": "Whether to flip the spiral from left-curling to right-curling or vice versa." "description": "Whether to flip the spiral from left-curling to right-curling or vice versa."

View File

@ -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<string, string> data = new Dictionary<string, string>();
HashSet<string> accessedKeys = new HashSet<string>();
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<string, string>();
return;
}
var json = File.ReadAllText(fullPath);
data = JsonConvert.DeserializeObject<Dictionary<string, string>>(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<Dictionary<string, string>>(filepath);
}
public void WriteToFile()
{
mod.ModHelper.Storage.Save<Dictionary<string, string>>(data, filepath);
}
public bool ContainsKey(string key)
{
return data.ContainsKey(key);
}
public T Get<T>(string key)
{
accessedKeys.Add(key);
var json = data[key];
return JsonConvert.DeserializeObject<T>(json);
}
public void Set<T>(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);
}
}
}
}

View File

@ -100,7 +100,7 @@ namespace NewHorizons.Utility.DebugMenu
ConversationMetadata conversationMetadata = new ConversationMetadata() ConversationMetadata conversationMetadata = new ConversationMetadata()
{ {
conversation = conversation, conversation = conversation,
conversationGo = NomaiTextBuilder.GetSpawnedGameObjectByNomaiTextInfo(conversation), conversationGo = TranslatorTextBuilder.GetSpawnedGameObjectByNomaiTextInfo(conversation),
planetConfig = config, planetConfig = config,
spirals = new List<SpiralMetadata>(), spirals = new List<SpiralMetadata>(),
collapsed = true collapsed = true
@ -120,7 +120,7 @@ namespace NewHorizons.Utility.DebugMenu
SpiralMetadata metadata = new SpiralMetadata() SpiralMetadata metadata = new SpiralMetadata()
{ {
spiral = arcInfo, spiral = arcInfo,
spiralGo = NomaiTextBuilder.GetSpawnedGameObjectByNomaiTextArcInfo(arcInfo), spiralGo = TranslatorTextBuilder.GetSpawnedGameObjectByNomaiTextArcInfo(arcInfo),
conversation = conversation, conversation = conversation,
planetConfig = config, planetConfig = config,
planetName = config.name, planetName = config.name,
@ -266,7 +266,7 @@ namespace NewHorizons.Utility.DebugMenu
GUILayout.Label("Variation"); GUILayout.Label("Variation");
GUILayout.BeginHorizontal(); GUILayout.BeginHorizontal();
var varietyCount = GetVarietyCountForType(spiralMeta.spiral.type); var varietyCount = 1;
//var newVariation = int.Parse(GUILayout.TextField(spiralMeta.spiral.variation+"")); //var newVariation = int.Parse(GUILayout.TextField(spiralMeta.spiral.variation+""));
//newVariation = Mathf.Min(Mathf.Max(0, newVariation), varietyCount); //newVariation = Mathf.Min(Mathf.Max(0, newVariation), varietyCount);
//if (newVariation != spiralMeta.spiral.variation) changed = true; //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; for (indexInParent = 0; indexInParent < wallTextComponent._textLines.Length; indexInParent++) if (oldTextLineComponent == wallTextComponent._textLines[indexInParent]) break;
var textEntryId = oldTextLineComponent._entryID; var textEntryId = oldTextLineComponent._entryID;
GameObject.Destroy(spiralMeta.spiralGo); 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<NomaiTextLine>(); wallTextComponent._textLines[indexInParent] = spiralMeta.spiralGo.GetComponent<NomaiTextLine>();
spiralMeta.spiralGo.name = "Brandnewspiral"; 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) void UpdateConversationTransform(ConversationMetadata conversationMetadata, GameObject sectorParent)
{ {
var nomaiWallTextObj = conversationMetadata.conversationGo; var nomaiWallTextObj = conversationMetadata.conversationGo;

View File

@ -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;
}
}
}

View File

@ -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<verts.Length; i++) normals[i] = new Vector3(0, 0, 1);
MVector2[] uv = new MVector2[] {
new Vector2(0, 0), new Vector2(0, 1),
new Vector2(1, 0), new Vector2(1, 1),
};
MVector2[] uv2 = new MVector2[] {
new Vector2(0, 0), new Vector2(0, 1),
new Vector2(1, 0), new Vector2(1, 1),
};
return new MMesh(verts, triangles, normals, uv, uv2);
}
}
}

View File

@ -1,5 +1,6 @@
using NewHorizons.External.Configs; using NewHorizons.External.Configs;
using OWML.Common; using OWML.Common;
using System;
using System.Linq; using System.Linq;
using UnityEngine; using UnityEngine;
namespace NewHorizons.Utility namespace NewHorizons.Utility
@ -17,10 +18,44 @@ namespace NewHorizons.Utility
public PlanetConfig Config; public PlanetConfig Config;
public IModBehaviour Mod; public IModBehaviour Mod;
public Cache Cache;
public string RelativePath; public string RelativePath;
public GameObject Object; public GameObject Object;
#region Cache
public void LoadCache()
{
if (RelativePath == null)
{
Logger.LogWarning("Cannot load cache! RelativePath is null!");
return;
}
try
{
var pathWithoutExtension = RelativePath.Substring(0, RelativePath.LastIndexOf('.'));
Cache = new Cache(Mod, pathWithoutExtension+".nhcache");
}
catch (Exception e)
{
Logger.LogError("Cache failed to load: " + e.Message);
Cache = null;
}
}
public void UnloadCache(bool writeBeforeUnload=false)
{
if (writeBeforeUnload)
{
Cache?.ClearUnaccessed();
Cache?.WriteToFile();
}
Cache = null; // garbage collection will take care of it
}
#endregion Cache
#region Migration #region Migration
private static readonly string[] _keepLoadedModsList = new string[] private static readonly string[] _keepLoadedModsList = new string[]
{ {
@ -45,6 +80,7 @@ namespace NewHorizons.Utility
} }
} }
} }
#endregion #endregion
} }
} }