475 lines
20 KiB
C#
475 lines
20 KiB
C#
using Barotrauma.Extensions;
|
|
using Barotrauma.Items.Components;
|
|
using Barotrauma.Networking;
|
|
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace Barotrauma
|
|
{
|
|
partial class EntitySpawner : Entity, IServerSerializable
|
|
{
|
|
private enum SpawnableType { Item, Character };
|
|
|
|
public interface IEntitySpawnInfo
|
|
{
|
|
Entity Spawn();
|
|
void OnSpawned(Entity entity);
|
|
}
|
|
|
|
public class ItemSpawnInfo : IEntitySpawnInfo
|
|
{
|
|
public readonly ItemPrefab Prefab;
|
|
|
|
public readonly Vector2 Position;
|
|
public readonly Inventory Inventory;
|
|
public readonly Submarine Submarine;
|
|
public readonly Option<float> Condition;
|
|
public readonly Option<int> Quality;
|
|
|
|
public bool SpawnIfInventoryFull = true;
|
|
public bool IgnoreLimbSlots = false;
|
|
public InvSlotType Slot = InvSlotType.None;
|
|
|
|
private readonly Action<Item> onSpawned;
|
|
|
|
public ItemSpawnInfo(ItemPrefab prefab, Vector2 worldPosition, Action<Item> onSpawned, float? condition = null, int? quality = null)
|
|
: this(prefab, onSpawned, condition, quality)
|
|
{
|
|
Position = worldPosition;
|
|
}
|
|
|
|
public ItemSpawnInfo(ItemPrefab prefab, Vector2 position, Submarine sub, Action<Item> onSpawned, float? condition = null, int? quality = null)
|
|
: this(prefab, onSpawned, condition, quality)
|
|
{
|
|
Position = position;
|
|
Submarine = sub;
|
|
}
|
|
|
|
public ItemSpawnInfo(ItemPrefab prefab, Inventory inventory, Action<Item> onSpawned, float? condition = null, int? quality = null)
|
|
: this(prefab, onSpawned, condition, quality)
|
|
{
|
|
Inventory = inventory;
|
|
}
|
|
|
|
private ItemSpawnInfo(ItemPrefab prefab, Action<Item> onSpawned, float? condition = null, int? quality = null)
|
|
{
|
|
Prefab = prefab ?? throw new ArgumentException("ItemSpawnInfo prefab cannot be null.");
|
|
Condition = condition.HasValue ? Option<float>.Some(condition.Value) : Option<float>.None();
|
|
Quality = quality.HasValue ? Option<int>.Some(quality.Value) : Option<int>.None();
|
|
this.onSpawned = onSpawned;
|
|
}
|
|
|
|
public Entity Spawn()
|
|
{
|
|
if (Prefab == null)
|
|
{
|
|
return null;
|
|
}
|
|
Item spawnedItem;
|
|
if (Inventory?.Owner != null)
|
|
{
|
|
if (!SpawnIfInventoryFull && !Inventory.CanProbablyBePut(Prefab))
|
|
{
|
|
return null;
|
|
}
|
|
spawnedItem = new Item(Prefab, Inventory.Owner.Position, Inventory.Owner.Submarine);
|
|
//this needs to be done before attempting to put the item in the inventory,
|
|
//because the quality and condition may affect whether it can go in the inventory (into an existing stack)
|
|
SetItemProperties(spawnedItem);
|
|
|
|
var slot = Slot != InvSlotType.None ? Slot.ToEnumerable() : spawnedItem.AllowedSlots;
|
|
if (!Inventory.Owner.Removed && !Inventory.TryPutItem(spawnedItem, null, slot))
|
|
{
|
|
if (IgnoreLimbSlots)
|
|
{
|
|
for (int i = 0; i < Inventory.Capacity; i++)
|
|
{
|
|
if (Inventory.GetItemAt(i) == null)
|
|
{
|
|
Inventory.ForceToSlot(spawnedItem, i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (Inventory.Owner is Character { DisabledByEvent: true })
|
|
{
|
|
spawnedItem.IsActive = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
spawnedItem = new Item(Prefab, Position, Submarine);
|
|
SetItemProperties(spawnedItem);
|
|
}
|
|
return spawnedItem;
|
|
|
|
void SetItemProperties(Item spawnedItem)
|
|
{
|
|
if (Condition.TryUnwrap(out float condition))
|
|
{
|
|
spawnedItem.Condition = condition;
|
|
}
|
|
if (Quality.TryUnwrap(out int quality))
|
|
{
|
|
spawnedItem.Quality = quality;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void OnSpawned(Entity spawnedItem)
|
|
{
|
|
if (spawnedItem is not Item item) { throw new ArgumentException($"The entity passed to ItemSpawnInfo.OnSpawned must be an Item (value was {spawnedItem?.ToString() ?? "null"})."); }
|
|
onSpawned?.Invoke(item);
|
|
}
|
|
}
|
|
|
|
class CharacterSpawnInfo : IEntitySpawnInfo
|
|
{
|
|
public readonly Identifier Identifier;
|
|
public readonly CharacterInfo CharacterInfo;
|
|
|
|
public readonly Vector2 Position;
|
|
public readonly Submarine Submarine;
|
|
|
|
private readonly Action<Character> onSpawned;
|
|
|
|
public CharacterSpawnInfo(Identifier identifier, Vector2 worldPosition, Action<Character> onSpawn = null)
|
|
{
|
|
this.Identifier = identifier;
|
|
if (identifier.IsEmpty) { throw new ArgumentException($"{nameof(CharacterSpawnInfo)} identifier cannot be null."); }
|
|
Position = worldPosition;
|
|
this.onSpawned = onSpawn;
|
|
}
|
|
|
|
public CharacterSpawnInfo(Identifier identifier, Vector2 position, Submarine sub, Action<Character> onSpawn = null)
|
|
{
|
|
this.Identifier = identifier;
|
|
if (identifier.IsEmpty) { throw new ArgumentException($"{nameof(CharacterSpawnInfo)} identifier cannot be null."); }
|
|
Position = position;
|
|
Submarine = sub;
|
|
this.onSpawned = onSpawn;
|
|
}
|
|
|
|
public CharacterSpawnInfo(Identifier identifier, Vector2 position, CharacterInfo characterInfo, Action<Character> onSpawn = null) : this (identifier, position, onSpawn)
|
|
{
|
|
CharacterInfo = characterInfo;
|
|
}
|
|
|
|
public Entity Spawn()
|
|
{
|
|
var character = Identifier.IsEmpty ? null :
|
|
Character.Create(Identifier,
|
|
Submarine == null ? Position : Submarine.Position + Position,
|
|
ToolBox.RandomSeed(8), CharacterInfo, createNetworkEvent: false);
|
|
return character;
|
|
}
|
|
|
|
public void OnSpawned(Entity spawnedCharacter)
|
|
{
|
|
if (!(spawnedCharacter is Character character)) { throw new ArgumentException($"The entity passed to CharacterSpawnInfo.OnSpawned must be a Character (value was {spawnedCharacter?.ToString() ?? "null"})."); }
|
|
onSpawned?.Invoke(character);
|
|
}
|
|
}
|
|
|
|
class SubmarineSpawnInfo : IEntitySpawnInfo
|
|
{
|
|
public readonly string Name;
|
|
|
|
public readonly Vector2 Position;
|
|
|
|
private readonly Action<Character> onSpawned;
|
|
|
|
public SubmarineSpawnInfo(string name, Vector2 worldPosition, Action<Character> onSpawn = null)
|
|
{
|
|
this.Name = name ?? throw new ArgumentException("ItemSpawnInfo prefab cannot be null.");
|
|
Position = worldPosition;
|
|
this.onSpawned = onSpawn;
|
|
}
|
|
|
|
|
|
public Entity Spawn()
|
|
{
|
|
var submarine = string.IsNullOrEmpty(Name) ? null :
|
|
new Submarine(SubmarineInfo.SavedSubmarines.First(s => s.Name.Equals(Name, StringComparison.OrdinalIgnoreCase)));
|
|
return submarine;
|
|
}
|
|
|
|
public void OnSpawned(Entity spawnedCharacter)
|
|
{
|
|
if (!(spawnedCharacter is Character character)) { throw new ArgumentException($"The entity passed to CharacterSpawnInfo.OnSpawned must be a Character (value was {spawnedCharacter?.ToString() ?? "null"})."); }
|
|
onSpawned?.Invoke(character);
|
|
}
|
|
}
|
|
|
|
private readonly Queue<Either<IEntitySpawnInfo, Entity>> spawnOrRemoveQueue;
|
|
|
|
public abstract class SpawnOrRemove : NetEntityEvent.IData
|
|
{
|
|
public readonly Entity Entity;
|
|
public UInt16 ID => Entity.ID;
|
|
|
|
public readonly UInt16 InventoryID;
|
|
|
|
public readonly byte ItemContainerIndex;
|
|
public readonly int SlotIndex;
|
|
|
|
public override string ToString()
|
|
{
|
|
return
|
|
"(" +
|
|
((Entity as MapEntity)?.Name ?? "[NULL]") +
|
|
$", {ID}, {InventoryID}, {SlotIndex})";
|
|
}
|
|
|
|
protected SpawnOrRemove(Entity entity)
|
|
{
|
|
Entity = entity;
|
|
if (!(entity is Item { ParentInventory: { Owner: { } } } item)) { return; }
|
|
|
|
InventoryID = item.ParentInventory.Owner.ID;
|
|
SlotIndex = item.ParentInventory.FindIndex(item);
|
|
//find the index of the ItemContainer this item is inside to get the item to
|
|
//spawn in the correct inventory in multi-inventory items like fabricators
|
|
if (item.Container == null) { return; }
|
|
|
|
foreach (ItemComponent component in item.Container.Components)
|
|
{
|
|
if (component is ItemContainer container &&
|
|
container.Inventory == item.ParentInventory)
|
|
{
|
|
ItemContainerIndex = (byte)item.Container.GetComponentIndex(component);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public sealed class SpawnEntity : SpawnOrRemove
|
|
{
|
|
public SpawnEntity(Entity entity) : base(entity) { }
|
|
public override string ToString()
|
|
=> $"Spawn {base.ToString()}";
|
|
}
|
|
|
|
public sealed class RemoveEntity : SpawnOrRemove
|
|
{
|
|
public RemoveEntity(Entity entity) : base(entity) { }
|
|
public override string ToString()
|
|
=> $"Remove {base.ToString()}";
|
|
}
|
|
|
|
public EntitySpawner()
|
|
: base(null, Entity.EntitySpawnerID)
|
|
{
|
|
spawnOrRemoveQueue = new Queue<Either<IEntitySpawnInfo, Entity>>();
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
return "EntitySpawner";
|
|
}
|
|
|
|
public void AddItemToSpawnQueue(ItemPrefab itemPrefab, Vector2 worldPosition, float? condition = null, int? quality = null, Action<Item> onSpawned = null)
|
|
{
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
|
if (itemPrefab == null)
|
|
{
|
|
string errorMsg = "Attempted to add a null item to entity spawn queue.\n" + Environment.StackTrace.CleanupStackTrace();
|
|
DebugConsole.ThrowError(errorMsg);
|
|
GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue1:ItemPrefabNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
|
|
return;
|
|
}
|
|
spawnOrRemoveQueue.Enqueue(new ItemSpawnInfo(itemPrefab, worldPosition, onSpawned, condition, quality));
|
|
}
|
|
|
|
public void AddItemToSpawnQueue(ItemPrefab itemPrefab, Vector2 position, Submarine sub, float? condition = null, int? quality = null, Action<Item> onSpawned = null)
|
|
{
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
|
if (itemPrefab == null)
|
|
{
|
|
string errorMsg = "Attempted to add a null item to entity spawn queue.\n" + Environment.StackTrace.CleanupStackTrace();
|
|
DebugConsole.ThrowError(errorMsg);
|
|
GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue2:ItemPrefabNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
|
|
return;
|
|
}
|
|
spawnOrRemoveQueue.Enqueue(new ItemSpawnInfo(itemPrefab, position, sub, onSpawned, condition, quality));
|
|
}
|
|
|
|
public void AddItemToSpawnQueue(ItemPrefab itemPrefab, Inventory inventory, float? condition = null, int? quality = null, Action<Item> onSpawned = null, bool spawnIfInventoryFull = true, bool ignoreLimbSlots = false, InvSlotType slot = InvSlotType.None)
|
|
{
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
|
if (itemPrefab == null)
|
|
{
|
|
string errorMsg = "Attempted to add a null item to entity spawn queue.\n" + Environment.StackTrace.CleanupStackTrace();
|
|
DebugConsole.ThrowError(errorMsg);
|
|
GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue3:ItemPrefabNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
|
|
return;
|
|
}
|
|
spawnOrRemoveQueue.Enqueue(new ItemSpawnInfo(itemPrefab, inventory, onSpawned, condition, quality)
|
|
{
|
|
SpawnIfInventoryFull = spawnIfInventoryFull,
|
|
IgnoreLimbSlots = ignoreLimbSlots,
|
|
Slot = slot
|
|
});
|
|
}
|
|
|
|
public void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 worldPosition, Action<Character> onSpawn = null)
|
|
{
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
|
if (speciesName.IsEmpty)
|
|
{
|
|
string errorMsg = "Attempted to add an empty/null species name to entity spawn queue.\n" + Environment.StackTrace.CleanupStackTrace();
|
|
DebugConsole.ThrowError(errorMsg);
|
|
GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue4:SpeciesNameNullOrEmpty", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
|
|
return;
|
|
}
|
|
spawnOrRemoveQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, onSpawn));
|
|
}
|
|
|
|
public void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 position, Submarine sub, Action<Character> onSpawn = null)
|
|
{
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
|
if (speciesName.IsEmpty)
|
|
{
|
|
string errorMsg = "Attempted to add an empty/null species name to entity spawn queue.\n" + Environment.StackTrace.CleanupStackTrace();
|
|
DebugConsole.ThrowError(errorMsg);
|
|
GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue5:SpeciesNameNullOrEmpty", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
|
|
return;
|
|
}
|
|
spawnOrRemoveQueue.Enqueue(new CharacterSpawnInfo(speciesName, position, sub, onSpawn));
|
|
}
|
|
|
|
public void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 worldPosition, CharacterInfo characterInfo, Action<Character> onSpawn = null)
|
|
{
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
|
if (speciesName.IsEmpty)
|
|
{
|
|
string errorMsg = "Attempted to add an empty/null species name to entity spawn queue.\n" + Environment.StackTrace.CleanupStackTrace();
|
|
DebugConsole.ThrowError(errorMsg);
|
|
GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue4:SpeciesNameNullOrEmpty", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
|
|
return;
|
|
}
|
|
spawnOrRemoveQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, characterInfo, onSpawn));
|
|
}
|
|
|
|
public void AddEntityToRemoveQueue(Entity entity)
|
|
{
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
|
if (entity == null || IsInRemoveQueue(entity) || entity.Removed || entity.IdFreed) { return; }
|
|
if (entity is Item item) { AddItemToRemoveQueue(item); return; }
|
|
if (entity is Character)
|
|
{
|
|
Character character = entity as Character;
|
|
#if SERVER
|
|
if (GameMain.Server != null)
|
|
{
|
|
Client client = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
|
|
if (client != null)
|
|
{
|
|
GameMain.Server.SetClientCharacter(client, null);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
spawnOrRemoveQueue.Enqueue(entity);
|
|
}
|
|
|
|
public void AddItemToRemoveQueue(Item item)
|
|
{
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
|
if (IsInRemoveQueue(item) || item.Removed) { return; }
|
|
|
|
spawnOrRemoveQueue.Enqueue(item);
|
|
item.IsInRemoveQueue = true;
|
|
|
|
foreach (var containedItem in item.ContainedItems)
|
|
{
|
|
if (containedItem != null)
|
|
{
|
|
AddItemToRemoveQueue(containedItem);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Are there any entities in the spawn queue that match the given predicate
|
|
/// </summary>
|
|
public bool IsInSpawnQueue(Predicate<IEntitySpawnInfo> predicate)
|
|
{
|
|
foreach (var spawnOrRemove in spawnOrRemoveQueue)
|
|
{
|
|
if (spawnOrRemove.TryGet(out IEntitySpawnInfo spawnInfo) && predicate(spawnInfo)) { return true; }
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// How many entities in the spawn queue match the given predicate
|
|
/// </summary>
|
|
public int CountSpawnQueue(Predicate<IEntitySpawnInfo> predicate)
|
|
{
|
|
int count = 0;
|
|
foreach (var spawnOrRemove in spawnOrRemoveQueue)
|
|
{
|
|
if (spawnOrRemove.TryGet(out IEntitySpawnInfo spawnInfo) && predicate(spawnInfo)) { count++; }
|
|
}
|
|
return count;
|
|
}
|
|
|
|
public bool IsInRemoveQueue(Entity entity)
|
|
{
|
|
foreach (var spawnOrRemove in spawnOrRemoveQueue)
|
|
{
|
|
if (spawnOrRemove.TryGet(out Entity entityToRemove) && entityToRemove == entity) { return true; }
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public void Update(bool createNetworkEvents = true)
|
|
{
|
|
if (GameMain.NetworkMember is { IsClient: true }) { return; }
|
|
while (spawnOrRemoveQueue.Count > 0)
|
|
{
|
|
if (!spawnOrRemoveQueue.TryDequeue(out var spawnOrRemove)) { break; }
|
|
if (spawnOrRemove.TryGet(out Entity entityToRemove))
|
|
{
|
|
if (entityToRemove is Item item)
|
|
{
|
|
item.SendPendingNetworkUpdates();
|
|
}
|
|
if (createNetworkEvents)
|
|
{
|
|
CreateNetworkEventProjSpecific(new RemoveEntity(entityToRemove));
|
|
}
|
|
entityToRemove.Remove();
|
|
}
|
|
else if (spawnOrRemove.TryGet(out IEntitySpawnInfo spawnInfo))
|
|
{
|
|
var spawnedEntity = spawnInfo.Spawn();
|
|
if (spawnedEntity == null) { continue; }
|
|
if (createNetworkEvents)
|
|
{
|
|
CreateNetworkEventProjSpecific(new SpawnEntity(spawnedEntity));
|
|
}
|
|
spawnInfo.OnSpawned(spawnedEntity);
|
|
GameMain.GameSession?.EventManager?.EntitySpawned(spawnedEntity);
|
|
}
|
|
}
|
|
}
|
|
|
|
partial void CreateNetworkEventProjSpecific(SpawnOrRemove spawnOrRemove);
|
|
|
|
public void Reset()
|
|
{
|
|
spawnOrRemoveQueue.Clear();
|
|
#if CLIENT
|
|
receivedEvents.Clear();
|
|
#endif
|
|
}
|
|
}
|
|
}
|