using System.Collections; using NitroxClient.MonoBehaviours; using NitroxClient.MonoBehaviours.CinematicController; using NitroxClient.Unity.Helper; using NitroxModel.DataStructures.GameLogic.Entities; using NitroxModel.DataStructures.GameLogic.Entities.Metadata; using NitroxModel.DataStructures.Util; using NitroxModel.Helper; using NitroxModel_Subnautica.DataStructures; using UnityEngine; namespace NitroxClient.GameLogic.Spawning.WorldEntities; public class VehicleWorldEntitySpawner : IWorldEntitySpawner { private readonly Entities entities; public VehicleWorldEntitySpawner(Entities entities) { this.entities = entities; } // The constructor has mixed results when the remote player is a long distance away. UWE even has a built in distance tracker to ensure // that they are within allowed range. However, this range is a bit restrictive. We will allow constructor spawning up to a specified // distance - anything more will simply use world spawning (no need to play the animation anyways). private const float ALLOWED_CONSTRUCTOR_DISTANCE = 100.0f; public IEnumerator SpawnAsync(WorldEntity entity, Optional parent, EntityCell cellRoot, TaskResult> result) { VehicleWorldEntity vehicleEntity = (VehicleWorldEntity)entity; bool withinConstructorSpawnWindow = (DayNightCycle.main.timePassedAsFloat - vehicleEntity.ConstructionTime) < GetCraftDuration(vehicleEntity.TechType.ToUnity()); Optional spawnerObj = NitroxEntity.GetObjectFrom(vehicleEntity.SpawnerId); if (withinConstructorSpawnWindow && spawnerObj.HasValue) { Constructor constructor = spawnerObj.Value.GetComponent(); float distance = (constructor.transform.position - Player.main.transform.position).magnitude; bool withinDistance = distance <= ALLOWED_CONSTRUCTOR_DISTANCE; if (constructor && withinDistance) { MobileVehicleBay.TransmitLocalSpawns = false; yield return SpawnViaConstructor(vehicleEntity, constructor, result); MobileVehicleBay.TransmitLocalSpawns = true; yield break; } } yield return SpawnInWorld(vehicleEntity, result, parent); } private IEnumerator SpawnInWorld(VehicleWorldEntity vehicleEntity, TaskResult> result, Optional parent) { TechType techType = vehicleEntity.TechType.ToUnity(); GameObject gameObject = null; bool isCyclops = techType == TechType.Cyclops; if (isCyclops) { GameObject prefab = null; LightmappedPrefabs.main.RequestScenePrefab("cyclops", (go) => prefab = go); yield return new WaitUntil(() => prefab != null); SubConsoleCommand.main.OnSubPrefabLoaded(prefab); gameObject = SubConsoleCommand.main.GetLastCreatedSub(); } else { CoroutineTask techPrefabCoroutine = CraftData.GetPrefabForTechTypeAsync(techType, false); yield return techPrefabCoroutine; GameObject techPrefab = techPrefabCoroutine.GetResult(); gameObject = Utils.SpawnPrefabAt(techPrefab, null, vehicleEntity.Transform.Position.ToUnity()); Validate.NotNull(gameObject, $"{nameof(VehicleWorldEntitySpawner)}: No prefab for tech type: {techType}"); Vehicle vehicle = gameObject.GetComponent(); if (vehicle) { vehicle.LazyInitialize(); } } AddCinematicControllers(gameObject); gameObject.transform.position = vehicleEntity.Transform.Position.ToUnity(); gameObject.transform.rotation = vehicleEntity.Transform.Rotation.ToUnity(); gameObject.SetActive(true); gameObject.SendMessage("StartConstruction", SendMessageOptions.DontRequireReceiver); CrafterLogic.NotifyCraftEnd(gameObject, CraftData.GetTechType(gameObject)); Rigidbody rigidBody = gameObject.RequireComponent(); rigidBody.isKinematic = false; yield return Yielders.WaitForEndOfFrame; RemoveConstructionAnimations(gameObject); yield return Yielders.WaitForEndOfFrame; Vehicles.RemoveNitroxEntitiesTagging(gameObject); NitroxEntity.SetNewId(gameObject, vehicleEntity.Id); if (vehicleEntity.Metadata is CyclopsMetadata cyclopsMetadata && cyclopsMetadata.IsDestroyed) { // Swap to destroyed look without triggering animations / effects gameObject.BroadcastMessage("SwapToDamagedModels"); gameObject.BroadcastMessage("OnKill"); gameObject.BroadcastMessage("CyclopsDeathEvent", SendMessageOptions.DontRequireReceiver); } if (parent.HasValue) { DockVehicle(gameObject, parent.Value); } result.Set(gameObject); } private IEnumerator SpawnViaConstructor(VehicleWorldEntity vehicleEntity, Constructor constructor, TaskResult> result) { if (!constructor.deployed) { constructor.Deploy(true); } float craftDuration = GetCraftDuration(vehicleEntity.TechType.ToUnity()) - (DayNightCycle.main.timePassedAsFloat - vehicleEntity.ConstructionTime); ConstructorInput crafter = constructor.gameObject.RequireComponentInChildren(true); yield return crafter.OnCraftingBeginAsync(vehicleEntity.TechType.ToUnity(), craftDuration); GameObject constructedObject = MobileVehicleBay.MostRecentlyCrafted; Validate.IsTrue(constructedObject, $"Could not find constructed object from MobileVehicleBay {constructor.gameObject.name}"); NitroxEntity.SetNewId(constructedObject, vehicleEntity.Id); AddCinematicControllers(constructedObject); result.Set(constructedObject); yield break; } /// /// For scene objects like cyclops, PlayerCinematicController Start() will not be called to add Cinematic reference. /// private void AddCinematicControllers(GameObject gameObject) { if (gameObject.GetComponent()) { return; } PlayerCinematicController[] controllers = gameObject.GetComponentsInChildren(true); if (controllers.Length == 0) { return; } MultiplayerCinematicReference reference = gameObject.AddComponent(); foreach (PlayerCinematicController controller in controllers) { reference.AddController(controller); } } /// /// When loading in vehicles, they still briefly have their blue crafting animation playing. Force them to stop. /// private void RemoveConstructionAnimations(GameObject gameObject) { VFXConstructing[] vfxConstructions = gameObject.GetComponentsInChildren(); foreach (VFXConstructing vfxConstructing in vfxConstructions) { vfxConstructing.EndGracefully(); } } private void DockVehicle(GameObject gameObject, GameObject parent) { Vehicle vehicle = gameObject.GetComponent(); if (!vehicle) { Log.Info($"Could not find vehicle component on docked vehicle {gameObject.name}"); return; } VehicleDockingBay dockingBay = parent.GetComponentInChildren(true); if (!dockingBay) { Log.Info($"Could not find VehicleDockingBay component on dock object {parent.name}"); return; } dockingBay.DockVehicle(vehicle); } public bool SpawnsOwnChildren() { return false; } private float GetCraftDuration(TechType techType) { // UWE hard codes the build times into if/else logic inside ConstructorInput.Craft(). switch(techType) { case TechType.Seamoth: return 10f; case TechType.Exosuit: return 10f; case TechType.Cyclops: return 20f; case TechType.RocketBase: return 25f; } return 10f; } }