first commit
This commit is contained in:
315
NitroxClient/GameLogic/Items.cs
Normal file
315
NitroxClient/GameLogic/Items.cs
Normal file
@@ -0,0 +1,315 @@
|
||||
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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user