using NewHorizons.Components.Quantum; using NewHorizons.External.Configs; using NewHorizons.External.Modules.Props; using NewHorizons.External.Modules.Props.Quantum; using NewHorizons.Utility.Files; using NewHorizons.Utility.Geometry; using NewHorizons.Utility.OWML; using System; using System.Collections.Generic; using System.Linq; using NewHorizons.External.Modules.Props; using UnityEngine; using OWML.Common; namespace NewHorizons.Builder.Props { public static class QuantumBuilder { public static void Make(GameObject planetGO, Sector sector, IModBehaviour mod, BaseQuantumGroupInfo[] quantumGroups) { foreach (var group in quantumGroups) { Make(planetGO, sector, mod, group); } } public static void Make(GameObject planetGO, Sector sector, IModBehaviour mod, BaseQuantumGroupInfo quantumGroup) { if (quantumGroup.details == null || !quantumGroup.details.Any()) { NHLogger.LogError($"Found quantum group with no details - [{planetGO.name}] [{quantumGroup.rename}]"); return; } if (quantumGroup is SocketQuantumGroupInfo socketGroup) { MakeSocketGroup(planetGO, sector, mod, socketGroup); } else if (quantumGroup is StateQuantumGroupInfo stateGroup) { MakeStateGroup(planetGO, sector, mod, stateGroup); } else if (quantumGroup is LightningQuantumGroupInfo lightningGroup) { MakeQuantumLightning(planetGO, sector, mod, lightningGroup); } } public static void MakeQuantumLightning(GameObject planetGO, Sector sector, IModBehaviour mod, LightningQuantumGroupInfo quantumGroup) { (GameObject go, QuantumDetailInfo detail)[] propsInGroup = quantumGroup.details.Select(x => (DetailBuilder.Make(planetGO, sector, mod, x), x)).ToArray(); // Bases its position off the first object with position set var root = propsInGroup.FirstOrDefault(x => x.detail.position != null || x.detail.rotation != null || x.detail.isRelativeToParent); var rootGo = root == default ? sector.gameObject : root.go; var lightning = DetailBuilder.Make(planetGO, sector, Main.Instance, AssetBundleUtilities.EyeLightning.LoadAsset("Prefab_EYE_QuantumLightningObject"), new DetailInfo()); lightning.transform.position = rootGo.transform.position; lightning.transform.rotation = rootGo.transform.rotation; lightning.transform.parent = rootGo.transform.parent; var lightningStatesParent = lightning.transform.Find("Models"); foreach (var (go, _) in propsInGroup) { go.transform.parent = lightningStatesParent; go.transform.localPosition = Vector3.zero; go.transform.localRotation = Quaternion.identity; } var lightningController = lightning.GetComponent(); lightningController._models = propsInGroup.Select(x => x.go).ToArray(); lightningController.enabled = true; lightning.name = quantumGroup.rename; lightning.SetActive(true); // Not sure why but it isn't enabling itself Delay.FireOnNextUpdate(() => lightningController.enabled = true); } // Nice to have: socket groups that specify a filledSocketObject and an emptySocketObject (eg the archway in the giant's deep tower) public static void MakeSocketGroup(GameObject planetGO, Sector sector, IModBehaviour mod, SocketQuantumGroupInfo quantumGroup) { (GameObject go, QuantumDetailInfo detail)[] propsInGroup = quantumGroup.details.Select(x => (DetailBuilder.Make(planetGO, sector, mod, x), x)).ToArray(); GameObject specialProp = null; QuantumDetailInfo specialInfo = null; if (propsInGroup.Length == quantumGroup.sockets.Length) { // Special case! specialProp = propsInGroup.Last().go; specialInfo = propsInGroup.Last().detail; var propsInGroupList = propsInGroup.ToList(); propsInGroupList.RemoveAt(propsInGroup.Length - 1); propsInGroup = propsInGroupList.ToArray(); } var groupRoot = new GameObject(quantumGroup.rename); groupRoot.transform.parent = sector?.transform ?? planetGO.transform; groupRoot.transform.localPosition = Vector3.zero; groupRoot.transform.localEulerAngles = Vector3.zero; var sockets = new QuantumSocket[quantumGroup.sockets.Length]; for (int i = 0; i < quantumGroup.sockets.Length; i++) { var socketInfo = quantumGroup.sockets[i]; var socket = GeneralPropBuilder.MakeNew("Socket " + i, planetGO, sector, socketInfo, defaultParent: groupRoot.transform); sockets[i] = socket.AddComponent(); sockets[i]._lightSources = new Light[0]; // TODO: make this customizable? socket.SetActive(true); } foreach (var prop in propsInGroup) { prop.go.SetActive(false); var quantumObject = prop.go.AddComponent(); quantumObject._socketRoot = groupRoot; quantumObject._socketList = sockets.ToList(); quantumObject._sockets = sockets; quantumObject._prebuilt = true; quantumObject._alignWithSocket = !prop.detail.alignWithGravity; quantumObject._randomYRotation = prop.detail.randomizeYRotation; quantumObject._alignWithGravity = prop.detail.alignWithGravity; quantumObject._childSockets = new List(); if (prop.go.GetComponentInChildren() == null) { BoundsUtilities.AddBoundsVisibility(prop.go); } prop.go.SetActive(true); } if (specialProp != null) { // Can't have 4 objects in 4 slots // Instead we have a duplicate of the final object for each slot, which appears when that slot is "empty" for (int i = 0; i < sockets.Length; i++) { var emptySocketObject = DetailBuilder.Make(planetGO, sector, mod, specialProp, new DetailInfo()); var socket = sockets[i]; socket._emptySocketObject = emptySocketObject; emptySocketObject.SetActive(socket._quantumObject == null); emptySocketObject.transform.parent = socket.transform; emptySocketObject.transform.localPosition = Vector3.zero; emptySocketObject.transform.localRotation = Quaternion.identity; // Need to add a visibility tracker for this socket else it doesn't stay "empty" when photographed socket.SetActive(false); var tracker = new GameObject("VisibilityTracker"); tracker.transform.parent = socket.transform; tracker.transform.localPosition = Vector3.zero; tracker.transform.localRotation = Quaternion.identity; var box = tracker.AddComponent(); box.size = new Vector3(0.2f, 0.6f, 0.2f); box.center = new Vector3(0, 0.3f, 0); tracker.AddComponent(); // Using a quantum object bc it can be locked by camera var quantumObject = socket.gameObject.AddComponent(); quantumObject._alignWithSocket = !specialInfo.alignWithGravity; quantumObject._randomYRotation = specialInfo.randomizeYRotation; quantumObject._alignWithGravity = specialInfo.alignWithGravity; quantumObject.emptySocketObject = emptySocketObject; socket._visibilityObject = quantumObject; socket.SetActive(true); } } } public static void MakeStateGroup(GameObject go, Sector sector, IModBehaviour mod, StateQuantumGroupInfo quantumGroup) { // NOTE: States groups need special consideration that socket groups don't // this is because the base class QuantumObject (and this is important) IGNORES PICTURES TAKEN FROM OVER 100 METERS AWAY // why does this affect states and not sockets? Well because sockets put the QuantumObject component (QuantumSocketedObject) on the actual props themselves // while states put the QuantumObject component (NHMultiStateQuantumObject) on the parent, which is located at the center of the planet // this means that the distance measured by QuantumObject is not accurate, since it's not measuring from the active prop, but from the center of the planet var propsInGroup = quantumGroup.details.Select(x => DetailBuilder.Make(go, sector, mod, x)).ToArray(); var groupRoot = new GameObject(quantumGroup.rename); groupRoot.transform.parent = sector?.transform ?? go.transform; groupRoot.transform.localPosition = Vector3.zero; var states = new List(); foreach (var prop in propsInGroup) { prop.transform.parent = groupRoot.transform; var state = prop.AddComponent(); state._probability = 1; states.Add(state); if (prop.GetComponentInChildren() != null) continue; BoundsUtilities.AddBoundsVisibility(prop); } if (quantumGroup.hasEmptyState) { var template = propsInGroup[0]; var empty = new GameObject("Empty State"); empty.transform.parent = groupRoot.transform; var state = empty.AddComponent(); states.Add(state); var boxBounds = BoundsUtilities.GetBoundsOfSelfAndChildMeshes(template); var boxShape = empty.AddComponent(); boxShape.center = boxBounds.center; boxShape.extents = boxBounds.size; empty.AddComponent(); empty.AddComponent(); } groupRoot.SetActive(false); var multiState = groupRoot.AddComponent(); multiState._loop = quantumGroup.loop; multiState._sequential = quantumGroup.sequential; multiState._states = states.ToArray(); multiState._prerequisiteObjects = new MultiStateQuantumObject[0]; // TODO: _prerequisiteObjects multiState._initialState = 0; // snapshot events arent listened to outside of the sector, so fortunately this isnt really infinite multiState._maxSnapshotLockRange = Mathf.Infinity; // TODO: maybe expose this at some point if it breaks a puzzle or something groupRoot.SetActive(true); } public static void MakeShuffleGroup(GameObject go, Sector sector, BaseQuantumGroupInfo quantumGroup, GameObject[] propsInGroup) { //var averagePosition = propsInGroup.Aggregate(Vector3.zero, (avg, prop) => avg + prop.transform.position) / propsInGroup.Count(); GameObject shuffleParent = new GameObject(quantumGroup.rename); shuffleParent.SetActive(false); shuffleParent.transform.parent = sector?.transform ?? go.transform; shuffleParent.transform.localPosition = Vector3.zero; propsInGroup.ToList().ForEach(p => p.transform.parent = shuffleParent.transform); var shuffle = shuffleParent.AddComponent(); shuffle._shuffledObjects = propsInGroup.Select(p => p.transform).ToArray(); shuffle.Awake(); // this doesn't get called on its own for some reason. what? how? AddBoundsVisibility(shuffleParent); shuffleParent.SetActive(true); } struct BoxShapeReciever { public MeshFilter f; public SkinnedMeshRenderer s; public GameObject gameObject; } public static void AddBoundsVisibility(GameObject g) { var meshFilters = g.GetComponentsInChildren(); var skinnedMeshRenderers = g.GetComponentsInChildren(); var boxShapeRecievers = meshFilters .Select(f => new BoxShapeReciever() { f = f, gameObject = f.gameObject }) .Concat( skinnedMeshRenderers.Select(s => new BoxShapeReciever() { s = s, gameObject = s.gameObject }) ) .ToList(); foreach (var boxshapeReciever in boxShapeRecievers) { var box = boxshapeReciever.gameObject.AddComponent(); boxshapeReciever.gameObject.AddComponent(); boxshapeReciever.gameObject.AddComponent(); var fixer = boxshapeReciever.gameObject.AddComponent(); fixer.shape = box; fixer.meshFilter = boxshapeReciever.f; fixer.skinnedMeshRenderer = boxshapeReciever.s; } } // BUG: ignores skinned guys. this coincidentally makes it work without BoxShapeFixer public static Bounds GetBoundsOfSelfAndChildMeshes(GameObject g) { var meshFilters = g.GetComponentsInChildren(); var corners = meshFilters.SelectMany(m => GetMeshCorners(m, g)).ToList(); Bounds b = new Bounds(corners[0], Vector3.zero); corners.ForEach(corner => b.Encapsulate(corner)); return b; } public static Vector3[] GetMeshCorners(MeshFilter m, GameObject relativeTo = null) { var bounds = m.mesh.bounds; var localCorners = new Vector3[] { bounds.min, bounds.max, new Vector3(bounds.min.x, bounds.min.y, bounds.max.z), new Vector3(bounds.min.x, bounds.max.y, bounds.min.z), new Vector3(bounds.max.x, bounds.min.y, bounds.min.z), new Vector3(bounds.min.x, bounds.max.y, bounds.max.z), new Vector3(bounds.max.x, bounds.min.y, bounds.max.z), new Vector3(bounds.max.x, bounds.max.y, bounds.min.z), }; var globalCorners = localCorners.Select(localCorner => m.transform.TransformPoint(localCorner)).ToArray(); if (relativeTo == null) return globalCorners; return globalCorners.Select(globalCorner => relativeTo.transform.InverseTransformPoint(globalCorner)).ToArray(); } } /// /// for some reason mesh bounds are wrong unless we wait a bit /// so this script contiously checks everything until it is correct /// /// this actually only seems to be a problem with skinned renderers. normal ones work fine /// TODO: at some point narrow this down to just skinned, instead of doing everything and checking every frame /// public class BoxShapeFixer : MonoBehaviour { public BoxShape shape; public MeshFilter meshFilter; public SkinnedMeshRenderer skinnedMeshRenderer; public void Update() { if (meshFilter == null && skinnedMeshRenderer == null) { NHLogger.LogVerbose("Useless BoxShapeFixer, destroying"); DestroyImmediate(this); } Mesh sharedMesh = null; if (meshFilter != null) sharedMesh = meshFilter.sharedMesh; if (skinnedMeshRenderer != null) sharedMesh = skinnedMeshRenderer.sharedMesh; if (sharedMesh == null) return; if (sharedMesh.bounds.size == Vector3.zero) return; shape.size = sharedMesh.bounds.size; shape.center = sharedMesh.bounds.center; DestroyImmediate(this); } } }