Files
2025-07-06 00:23:46 +02:00

316 lines
15 KiB
C#

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;
/// <summary>
/// Whether or not <see cref="Inventory.Pickup"/> is running. It's useful to discriminate between Inventory.Pickup from
/// a regular <see cref="Pickupable.Pickup"/>
/// </summary>
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}");
}
}
/// <summary>
/// Tracks the object (as dropped) and notifies the server to spawn the item for other players.
/// </summary>
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<EntityMetadata> metadata = entityMetadataManager.Extract(gameObject);
string classId = gameObject.GetComponent<PrefabIdentifier>().ClassId;
WorldEntity droppedItem;
List<Entity> 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<Pickupable>() && 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}");
}
}
/// <summary>
/// Handles objects placed as figures and posters, or LEDLights so that we can spawn them accordingly afterwards.
/// </summary>
public void Placed(GameObject gameObject, TechType techType)
{
RemoveAnyRemoteControl(gameObject);
NitroxId id = NitroxEntity.GetIdOrGenerateNew(gameObject);
Optional<EntityMetadata> metadata = entityMetadataManager.Extract(gameObject);
string classId = gameObject.GetComponent<PrefabIdentifier>().ClassId;
List<Entity> 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<Entity> GetPrefabChildren(GameObject gameObject, NitroxId parentId, EntityMetadataManager entityMetadataManager)
{
foreach (IGrouping<string, PrefabIdentifier> prefabGroup in gameObject.GetAllComponentsInChildren<PrefabIdentifier>()
.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<EntityMetadata> metadata = entityMetadataManager.Extract(prefab.gameObject);
if (metadata.HasValue)
{
TechTag techTag = prefab.gameObject.GetComponent<TechTag>();
TechType techType = (techTag) ? techTag.type : TechType.None;
yield return new PrefabChildEntity(id, prefab.classId, techType.ToDto(), indexInGroup, metadata.Value, parentId);
indexInGroup++;
}
}
}
}
/// <summary>
/// Overloads <see cref="ConvertToInventoryItemEntity"/> and removes any tracking on <paramref name="gameObject"/>
/// </summary>
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<PrefabIdentifier>().ClassId;
TechType techType = gameObject.RequireComponent<Pickupable>().GetTechType();
Optional<EntityMetadata> metadata = entityMetadataManager.Extract(gameObject);
List<Entity> 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;
}
/// <summary>
/// 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.
/// </summary>
private void RemoveAnyRemoteControl(GameObject gameObject)
{
UnityEngine.Object.Destroy(gameObject.GetComponent<RemotelyControlled>());
}
/// <param name="parent">Parent of the GameObject to check</param>
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;
}
/// <inheritdoc cref="TryGetParentWaterPark" />
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<InstalledModuleEntity> GetEquipmentModuleEntities(Equipment equipment, NitroxId equipmentId, EntityMetadataManager entityMetadataManager)
{
List<InstalledModuleEntity> entities = new();
foreach (KeyValuePair<string, InventoryItem> itemEntry in equipment.equipment)
{
InventoryItem item = itemEntry.Value;
if (item != null)
{
Pickupable pickupable = item.item;
string classId = pickupable.RequireComponent<PrefabIdentifier>().ClassId;
NitroxId itemId = NitroxEntity.GetIdOrGenerateNew(pickupable.gameObject);
Optional<EntityMetadata> metadata = entityMetadataManager.Extract(pickupable.gameObject);
List<Entity> 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;
}
}