mirror of
https://github.com/Outer-Wilds-New-Horizons/new-horizons.git
synced 2025-12-11 20:15:44 +01:00
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:
commit
b325f87f5f
@ -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;
|
||||||
|
|||||||
@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
393
NewHorizons/Builder/Props/TranslatorText/NomaiTextArcArranger.cs
Normal file
393
NewHorizons/Builder/Props/TranslatorText/NomaiTextArcArranger.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
473
NewHorizons/Builder/Props/TranslatorText/NomaiTextArcBuilder.cs
Normal file
473
NewHorizons/Builder/Props/TranslatorText/NomaiTextArcBuilder.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
NewHorizons/External/Modules/PropModule.cs
vendored
13
NewHorizons/External/Modules/PropModule.cs
vendored
@ -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>
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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."
|
||||||
|
|||||||
70
NewHorizons/Utility/Cache.cs
Normal file
70
NewHorizons/Utility/Cache.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|||||||
51
NewHorizons/Utility/MMesh.cs
Normal file
51
NewHorizons/Utility/MMesh.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
NewHorizons/Utility/MeshUtilities.cs
Normal file
37
NewHorizons/Utility/MeshUtilities.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user