using System; using System.Collections.Generic; using System.Linq; using NitroxClient.Communication.Abstract; using NitroxClient.GameLogic.Helper; using NitroxClient.GameLogic.Spawning.Metadata; using NitroxClient.MonoBehaviours; using NitroxClient.Unity.Helper; using NitroxModel.DataStructures; using NitroxModel.DataStructures.GameLogic; using NitroxModel.DataStructures.GameLogic.Entities; using NitroxModel.DataStructures.GameLogic.Entities.Metadata; using NitroxModel.DataStructures.Util; using NitroxModel.Packets; using NitroxModel_Subnautica.DataStructures; using UnityEngine; namespace NitroxClient.GameLogic; public class Items { private readonly IPacketSender packetSender; private readonly Entities entities; public static GameObject PickingUpObject { get; private set; } private readonly EntityMetadataManager entityMetadataManager; /// /// Whether or not is running. It's useful to discriminate between Inventory.Pickup from /// a regular /// public bool IsInventoryPickingUp; public Items(IPacketSender packetSender, Entities entities, EntityMetadataManager entityMetadataManager) { this.packetSender = packetSender; this.entities = entities; this.entityMetadataManager = entityMetadataManager; } public void PickedUp(GameObject gameObject, TechType techType) { PickingUpObject = gameObject; // Try catch to avoid blocking PickingUpObject with a non null value outside of the current context try { // Newly created objects are always placed into the player's inventory. if (!Player.main.TryGetNitroxId(out NitroxId playerId)) { Log.ErrorOnce($"[{nameof(Items)}] Player has no id! Could not set parent of picked up item {gameObject.name}."); PickingUpObject = null; return; } InventoryItemEntity inventoryItemEntity = ConvertToInventoryEntityUntracked(gameObject, playerId); if (inventoryItemEntity.TechType.ToUnity() != techType) { Log.Warn($"Provided TechType: {techType} is different than the one automatically attributed to the item {inventoryItemEntity.TechType}"); } PickupItem pickupItem = new(inventoryItemEntity); if (packetSender.Send(pickupItem)) { Log.Debug($"Picked up item {inventoryItemEntity}"); } } catch (Exception exception) { Log.Error(exception); } PickingUpObject = null; } public void Planted(GameObject gameObject, NitroxId parentId) { InventoryItemEntity inventoryItemEntity = ConvertToInventoryEntityUntracked(gameObject, parentId); if (packetSender.Send(new EntitySpawnedByClient(inventoryItemEntity, true))) { Log.Debug($"Planted item {inventoryItemEntity}"); } } /// /// Tracks the object (as dropped) and notifies the server to spawn the item for other players. /// public void Dropped(GameObject gameObject, TechType? techType = null) { techType ??= CraftData.GetTechType(gameObject); // there is a theoretical possibility of a stray remote tracking packet that re-adds the monobehavior, this is purely a safety call. RemoveAnyRemoteControl(gameObject); // WaterParkCreatures need at least one ManagedUpdate to run so their data is correctly refreshed (isMature and timeNextBreed) if (gameObject.TryGetComponent(out WaterParkCreature waterParkCreature)) { waterParkCreature.ManagedUpdate(); } NitroxId id = NitroxEntity.GetIdOrGenerateNew(gameObject); Optional metadata = entityMetadataManager.Extract(gameObject); string classId = gameObject.GetComponent().ClassId; WorldEntity droppedItem; List childrenEntities = GetPrefabChildren(gameObject, id, entityMetadataManager).ToList(); // If the item is dropped in a WaterPark we need to handle it differently NitroxId parentId = null; if (IsGlobalRootObject(gameObject) || (gameObject.GetComponent() && TryGetParentWaterParkId(gameObject.transform.parent, out parentId))) { // We cast it to an entity type that is always seeable by clients // therefore, the packet will be redirected to everyone droppedItem = new GlobalRootEntity(gameObject.transform.ToLocalDto(), 0, classId, true, id, techType.Value.ToDto(), metadata.OrNull(), parentId, childrenEntities); } else if (gameObject.TryGetComponent(out OxygenPipe oxygenPipe)) { // We can't spawn an OxygenPipe without its parent and root // Dropped patch is called in OxygenPipe.PlaceInWorld which is why OxygenPipe.ghostModel is valid IPipeConnection parentConnection = OxygenPipe.ghostModel.GetParent(); if (parentConnection == null || !parentConnection.GetGameObject() || !parentConnection.GetGameObject().TryGetNitroxId(out NitroxId parentPipeId)) { Log.Error($"Couldn't find a valid reference to the OxygenPipe's parent pipe"); return; } IPipeConnection rootConnection = parentConnection.GetRoot(); if (rootConnection == null || !rootConnection.GetGameObject() || !rootConnection.GetGameObject().TryGetNitroxId(out NitroxId rootPipeId)) { Log.Error($"Couldn't find a valid reference to the OxygenPipe's root pipe"); return; } // Updating the local pipe's references to replace the UniqueIdentifier's id by their NitroxEntity's id oxygenPipe.rootPipeUID = rootPipeId.ToString(); oxygenPipe.parentPipeUID = parentPipeId.ToString(); droppedItem = new OxygenPipeEntity(gameObject.transform.ToWorldDto(), 0, classId, false, id, techType.Value.ToDto(), metadata.OrNull(), null, childrenEntities, rootPipeId, parentPipeId, parentConnection.GetAttachPoint().ToDto()); } else { // Generic case droppedItem = new(gameObject.transform.ToWorldDto(), 0, classId, false, id, techType.Value.ToDto(), metadata.OrNull(), null, childrenEntities); } if (packetSender.Send(new EntitySpawnedByClient(droppedItem, true))) { Log.Debug($"Dropping item: {droppedItem}"); } } /// /// Handles objects placed as figures and posters, or LEDLights so that we can spawn them accordingly afterwards. /// public void Placed(GameObject gameObject, TechType techType) { RemoveAnyRemoteControl(gameObject); NitroxId id = NitroxEntity.GetIdOrGenerateNew(gameObject); Optional metadata = entityMetadataManager.Extract(gameObject); string classId = gameObject.GetComponent().ClassId; List childrenEntities = GetPrefabChildren(gameObject, id, entityMetadataManager).ToList(); WorldEntity placedItem; // If the object is dropped in the water, it'll be parented to a CellRoot so we let it as WorldEntity (see Items.Dropped) // PlaceTool's object is located under GlobalRoot or under a CellRoot (we differentiate both by giving a different type) // Because objects under CellRoots must only spawn when visible while objects under GlobalRoot must be spawned at all times switch (gameObject.AliveOrNull()) { case not null when IsGlobalRootObject(gameObject): placedItem = new GlobalRootEntity(gameObject.transform.ToWorldDto(), 0, classId, true, id, techType.ToDto(), metadata.OrNull(), null, childrenEntities); break; case not null when Player.main.AliveOrNull()?.GetCurrentSub().AliveOrNull()?.TryGetNitroxId(out NitroxId parentId) == true: placedItem = new GlobalRootEntity(gameObject.transform.ToLocalDto(), 0, classId, true, id, techType.ToDto(), metadata.OrNull(), parentId, childrenEntities); break; default: // If the object is not under a SubRoot nor in GlobalRoot, it'll be under a CellRoot but we still want to remember its state placedItem = new PlacedWorldEntity(gameObject.transform.ToWorldDto(), 0, classId, true, id, techType.ToDto(), metadata.OrNull(), null, childrenEntities); break; } if (packetSender.Send(new EntitySpawnedByClient(placedItem, true))) { Log.Debug($"Placed object: {placedItem}"); } } // This function will record any notable children of the dropped item as a PrefabChildEntity. In this case, a 'notable' // child is one that UWE has tagged with a PrefabIdentifier (class id) and has entity metadata that can be extracted. An // example would be recording a Battery PrefabChild inside of a Flashlight WorldEntity. public static IEnumerable GetPrefabChildren(GameObject gameObject, NitroxId parentId, EntityMetadataManager entityMetadataManager) { foreach (IGrouping prefabGroup in gameObject.GetAllComponentsInChildren() .Where(prefab => prefab.gameObject != gameObject) .GroupBy(prefab => prefab.classId)) { int indexInGroup = 0; foreach (PrefabIdentifier prefab in prefabGroup) { NitroxId id = NitroxEntity.GetIdOrGenerateNew(prefab.gameObject); // We do this here bc a MetadataExtractor could be requiring the id to increment or so Optional metadata = entityMetadataManager.Extract(prefab.gameObject); if (metadata.HasValue) { TechTag techTag = prefab.gameObject.GetComponent(); TechType techType = (techTag) ? techTag.type : TechType.None; yield return new PrefabChildEntity(id, prefab.classId, techType.ToDto(), indexInGroup, metadata.Value, parentId); indexInGroup++; } } } } /// /// Overloads and removes any tracking on /// private InventoryItemEntity ConvertToInventoryEntityUntracked(GameObject gameObject, NitroxId parentId) { InventoryItemEntity inventoryItemEntity = ConvertToInventoryItemEntity(gameObject, parentId, entityMetadataManager); // Some picked up entities are not known by the server for several reasons. First it can be picked up via a spawn item command. Another // example is that some obects are not 'real' objects until they are clicked and end up spawning a prefab. For example, the fire extinguisher // in the escape pod (mono: IntroFireExtinguisherHandTarget) or Creepvine seeds (mono: PickupPrefab). When clicked, these spawn new prefabs // directly into the player's inventory. These will ultimately be registered server side with the above inventoryItemEntity. entities.MarkAsSpawned(inventoryItemEntity); // We want to remove any remote tracking immediately on pickup as it can cause weird behavior like holding a ghost item still in the world. RemoveAnyRemoteControl(gameObject); EntityPositionBroadcaster.StopWatchingEntity(inventoryItemEntity.Id); return inventoryItemEntity; } public static InventoryItemEntity ConvertToInventoryItemEntity(GameObject gameObject, NitroxId parentId, EntityMetadataManager entityMetadataManager) { NitroxId itemId = NitroxEntity.GetIdOrGenerateNew(gameObject); // id may not exist, create if missing string classId = gameObject.RequireComponent().ClassId; TechType techType = gameObject.RequireComponent().GetTechType(); Optional metadata = entityMetadataManager.Extract(gameObject); List children = GetPrefabChildren(gameObject, itemId, entityMetadataManager).ToList(); InventoryItemEntity inventoryItemEntity = new(itemId, classId, techType.ToDto(), metadata.OrNull(), parentId, children); BatteryChildEntityHelper.TryPopulateInstalledBattery(gameObject, inventoryItemEntity.ChildEntities, itemId); return inventoryItemEntity; } /// /// Some items might be remotely simulated if they were dropped by other players. We'll want to remove /// any remote tracking when we actively handle the item. /// private void RemoveAnyRemoteControl(GameObject gameObject) { UnityEngine.Object.Destroy(gameObject.GetComponent()); } /// Parent of the GameObject to check public static bool TryGetParentWaterPark(Transform parent, out WaterPark waterPark) { // NB: When dropped in a WaterPark, items are placed under WaterPark/items_root/ // So we need to search two steps higher to find the WaterPark if (parent && parent.parent && parent.parent.TryGetComponent(out waterPark)) { return true; } waterPark = null; return false; } /// private static bool TryGetParentWaterParkId(Transform parent, out NitroxId waterParkId) { if (TryGetParentWaterPark(parent, out WaterPark waterPark) && waterPark.TryGetNitroxId(out waterParkId)) { return true; } waterParkId = null; return false; } public static List GetEquipmentModuleEntities(Equipment equipment, NitroxId equipmentId, EntityMetadataManager entityMetadataManager) { List entities = new(); foreach (KeyValuePair itemEntry in equipment.equipment) { InventoryItem item = itemEntry.Value; if (item != null) { Pickupable pickupable = item.item; string classId = pickupable.RequireComponent().ClassId; NitroxId itemId = NitroxEntity.GetIdOrGenerateNew(pickupable.gameObject); Optional metadata = entityMetadataManager.Extract(pickupable.gameObject); List children = GetPrefabChildren(pickupable.gameObject, itemId, entityMetadataManager).ToList(); entities.Add(new(itemEntry.Key, classId, itemId, pickupable.GetTechType().ToDto(), metadata.OrNull(), equipmentId, children)); } } return entities; } private static bool IsGlobalRootObject(GameObject gameObject) { return gameObject.TryGetComponent(out LargeWorldEntity largeWorldEntity) && largeWorldEntity.initialCellLevel == LargeWorldEntity.CellLevel.Global; } }