using System.Collections; using System.Collections.Generic; using System.Linq; using NitroxClient.Communication; using NitroxClient.GameLogic.Spawning.WorldEntities; using NitroxModel.Packets; using UnityEngine; namespace NitroxClient.MonoBehaviours.Cyclops; /// /// Script responsible for creating a virtual counterpart of every cyclops, which will always be horizontal and motionless so that simulated movement is always clear. /// Contains a pawn for each player entering the regular cyclops. /// public class VirtualCyclops : MonoBehaviour { public static VirtualCyclops Instance; public const string NAME = "VirtualCyclops"; private static readonly Dictionary cacheColliderCopy = []; private readonly Dictionary virtualOpenableByName = []; private readonly Dictionary realOpenableByName = []; private readonly Dictionary virtualConstructableByRealGameObject = []; public NitroxCyclops Cyclops; public Transform axis; private Rigidbody rigidbody; private Vector3 InitialPosition; private Quaternion InitialRotation; public static void Initialize() { CreateVirtualCyclops(); Multiplayer.OnAfterMultiplayerEnd += Dispose; } public static void Dispose() { Destroy(Instance.gameObject); Instance = null; Multiplayer.OnAfterMultiplayerEnd -= Dispose; } /// /// Initializes the object with reduced utility to ensure the virtual cyclops won't be eating too much performance. /// public static void CreateVirtualCyclops() { if (Instance) { return; } LightmappedPrefabs.main.RequestScenePrefab("cyclops", (cyclopsPrefab) => { SubConsoleCommand.main.OnSubPrefabLoaded(cyclopsPrefab); GameObject model = SubConsoleCommand.main.GetLastCreatedSub(); model.name = NAME; Vector3 position = Vector3.up * 500; Quaternion rotation = Quaternion.identity; model.transform.position = position; model.transform.rotation = rotation; Instance = model.AddComponent(); Instance.axis = model.GetComponent().subAxis; GameObject.Destroy(model.GetComponent()); GameObject.Destroy(model.GetComponent()); GameObject.Destroy(model.GetComponent()); GameObject.Destroy(model.GetComponent()); Instance.InitialPosition = position; Instance.InitialRotation = rotation; Instance.rigidbody = Instance.GetComponent(); Instance.rigidbody.constraints = RigidbodyConstraints.FreezeAll; model.GetComponent().enabled = false; model.GetComponent().lockInterpolation = false; model.GetComponent().stabilizerEnabled = false; model.GetComponent().isKinematic = true; model.GetComponent().invincible = true; Instance.RegisterVirtualOpenables(); Instance.ToggleRenderers(false); Instance.DisableBadComponents(); model.SetActive(true); }); } public static IEnumerator InitializeConstructablesCache() { List constructableTechTypes = []; CraftData.GetBuilderGroupTech(TechGroup.InteriorModules, constructableTechTypes, true); CraftData.GetBuilderGroupTech(TechGroup.Miscellaneous, constructableTechTypes, true); TaskResult result = new(); foreach (TechType techType in constructableTechTypes) { yield return DefaultWorldEntitySpawner.RequestPrefab(techType, result); if (result.value && result.value.GetComponent()) { // We immediately destroy the copy because we only want to cache it for now Destroy(CreateColliderCopy(result.value, techType)); } } } public void Populate() { foreach (Constructable constructable in Cyclops.GetComponentsInChildren(true)) { ReplicateConstructable(constructable); } foreach (Openable openable in Cyclops.GetComponentsInChildren(true)) { openable.blocked = false; ReplicateOpening(openable, openable.isOpen); realOpenableByName.Add(openable.name, openable); } } public void Depopulate() { foreach (GameObject virtualObject in virtualConstructableByRealGameObject.Values) { Destroy(virtualObject); } virtualConstructableByRealGameObject.Clear(); foreach (Openable openable in realOpenableByName.Values) { openable.blocked = false; } realOpenableByName.Clear(); } public void SetCurrentCyclops(NitroxCyclops nitroxCyclops) { if (Cyclops) { Cyclops.Virtual = null; Depopulate(); Cyclops = null; } Cyclops = nitroxCyclops; if (Cyclops) { Populate(); } } public void Update() { transform.position = InitialPosition; transform.rotation = InitialRotation; } public void ToggleRenderers(bool toggled) { foreach (Renderer renderer in transform.GetComponentsInChildren(true)) { renderer.enabled = toggled; } } private void RegisterVirtualOpenables() { foreach (Openable openable in transform.GetComponentsInChildren(true)) { virtualOpenableByName.Add(openable.name, openable); } } private void DisableBadComponents() { CyclopsLightingPanel cyclopsLightingPanel = GetComponentInChildren(true); cyclopsLightingPanel.floodlightsOn = false; cyclopsLightingPanel.lightingOn = false; cyclopsLightingPanel.SetExternalLighting(false); cyclopsLightingPanel.cyclopsRoot.ForceLightingState(false); cyclopsLightingPanel.enabled = false; Destroy(cyclopsLightingPanel); // Disable a source of useless logs foreach (FMOD_CustomEmitter customEmitter in GetComponentsInChildren(true)) { customEmitter.enabled = false; } foreach (PlayerCinematicController cinematicController in GetComponentsInChildren(true)) { cinematicController.enabled = false; } } public void ReplicateOpening(Openable openable, bool openState) { if (virtualOpenableByName.TryGetValue(openable.name, out Openable virtualOpenable)) { using (PacketSuppressor.Suppress()) { virtualOpenable.PlayOpenAnimation(openState, virtualOpenable.animTime); } } } public void ReplicateBlock(Openable openable, bool blockState) { if (realOpenableByName.TryGetValue(openable.name, out Openable realOpenable)) { realOpenable.blocked = blockState; } } public void ReplicateConstructable(Constructable constructable) { if (virtualConstructableByRealGameObject.ContainsKey(constructable.gameObject)) { return; } GameObject colliderCopy = CreateColliderCopy(constructable.gameObject, constructable.techType); colliderCopy.transform.parent = transform; colliderCopy.transform.CopyLocals(constructable.transform); virtualConstructableByRealGameObject.Add(constructable.gameObject, colliderCopy); } /// /// Creates an empty shell simulating the presence of modules by copying its children containing a collider. /// public static GameObject CreateColliderCopy(GameObject realObject, TechType techType) { if (cacheColliderCopy.TryGetValue(techType, out GameObject colliderCopy)) { return GameObject.Instantiate(colliderCopy); } colliderCopy = new GameObject($"{realObject.name}-collidercopy"); // This will act as a prefab but will stay in the material world so we put it out of hands in the meantime colliderCopy.transform.position = Vector3.up * 1000 + Vector3.right * 10 * cacheColliderCopy.Count; Transform transform = realObject.transform; Dictionary copiedTransformByRealTransform = []; copiedTransformByRealTransform[transform] = colliderCopy.transform; IEnumerable uniqueColliderObjects = realObject.GetComponentsInChildren(true).Select(c => c.gameObject).Distinct(); foreach (GameObject colliderObject in uniqueColliderObjects) { GameObject copiedColliderObject = new(colliderObject.name); copiedColliderObject.transform.CopyLocals(colliderObject.transform); foreach (Collider collider in colliderObject.GetComponents()) { collider.CopyComponent(copiedColliderObject); } // "child" is always a copied transform looking for its copied parent Transform child = copiedColliderObject.transform; // "parent" is always the real parent of the real transform corresponding to "child" Transform parent = colliderObject.transform.parent; while (!copiedTransformByRealTransform.ContainsKey(parent)) { Transform copiedParent = copiedTransformByRealTransform[parent] = Instantiate(parent); child.SetParent(copiedParent, false); child = copiedParent; parent = parent.parent; } // At the top of the tree we can simply stick the latest child to the collider child.SetParent(colliderCopy.transform, false); } cacheColliderCopy.Add(techType, colliderCopy); return GameObject.Instantiate(colliderCopy); } public void UnregisterConstructable(GameObject realObject) { if (virtualConstructableByRealGameObject.TryGetValue(realObject, out GameObject virtualConstructable)) { Destroy(virtualConstructable); virtualConstructableByRealGameObject.Remove(realObject); } } }