using System.Collections.Generic;
using System.Linq;
+using Content.Server.Humanoid.Components;
using Content.Shared.Coordinates;
+using Content.Shared.Prototypes;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
var query = entityMan.AllEntityQueryEnumerator<TComp>();
while (query.MoveNext(out var uid, out var meta))
yield return (uid, meta);
- };
+ }
var entityMetas = Query<MetaDataComponent>(sEntMan).ToList();
foreach (var (uid, meta) in entityMetas)
await pair.CleanReturnAsync();
}
+ /// <summary>
+ /// This test checks that spawning and deleting an entity doesn't somehow create other unrelated entities.
+ /// </summary>
+ /// <remarks>
+ /// Unless an entity is intentionally designed to spawn other entities (e.g., mob spawners), they should
+ /// generally not spawn unrelated / detached entities. Any entities that do get spawned should be parented to
+ /// the spawned entity (e.g., in a container). If an entity needs to spawn an entity somewhere in null-space,
+ /// it should delete that entity when it is no longer required. This test mainly exists to prevent "entity leak"
+ /// bugs, where spawning some entity starts spawning unrelated entities in null space.
+ /// </remarks>
+ [Test]
+ public async Task SpawnAndDeleteEntityCountTest()
+ {
+ var settings = new PoolSettings { Connected = true, Dirty = true };
+ await using var pair = await PoolManager.GetServerClient(settings);
+ var server = pair.Server;
+ var client = pair.Client;
+
+ var excluded = new[]
+ {
+ "MapGrid",
+ "StationEvent",
+ "TimedDespawn",
+
+ // Spawner entities
+ "DragonRift",
+ "RandomHumanoidSpawner",
+ "RandomSpawner",
+ "ConditionalSpawner",
+ "GhostRoleMobSpawner",
+ "NukeOperativeSpawner",
+ "TimedSpawner",
+ };
+
+ Assert.That(server.CfgMan.GetCVar(CVars.NetPVS), Is.False);
+
+ var protoIds = server.ProtoMan
+ .EnumeratePrototypes<EntityPrototype>()
+ .Where(p => !p.Abstract)
+ .Where(p => !pair.IsTestPrototype(p))
+ .Where(p => !excluded.Any(p.Components.ContainsKey))
+ .Select(p => p.ID)
+ .ToList();
+
+ MapCoordinates coords = default;
+ await server.WaitPost(() =>
+ {
+ var map = server.MapMan.CreateMap();
+ coords = new MapCoordinates(default, map);
+ });
+
+ await pair.RunTicksSync(3);
+
+ List<string> badPrototypes = new();
+ foreach (var protoId in protoIds)
+ {
+ // TODO fix ninja
+ // Currently ninja fails to equip their own loadout.
+ if (protoId == "MobHumanSpaceNinja")
+ continue;
+
+ var count = server.EntMan.EntityCount;
+ var clientCount = client.EntMan.EntityCount;
+ EntityUid uid = default;
+ await server.WaitPost(() => uid = server.EntMan.SpawnEntity(protoId, coords));
+ await pair.RunTicksSync(3);
+
+ // If the entity deleted itself, check that it didn't spawn other entities
+ if (!server.EntMan.EntityExists(uid))
+ {
+ if (server.EntMan.EntityCount != count || client.EntMan.EntityCount != clientCount)
+ badPrototypes.Add(protoId);
+ continue;
+ }
+
+ // Check that the number of entities has increased.
+ if (server.EntMan.EntityCount <= count || client.EntMan.EntityCount <= clientCount)
+ {
+ badPrototypes.Add(protoId);
+ continue;
+ }
+
+ await server.WaitPost(() => server.EntMan.DeleteEntity(uid));
+ await pair.RunTicksSync(3);
+
+ // Check that the number of entities has gone back to the original value.
+ if (server.EntMan.EntityCount != count || client.EntMan.EntityCount != clientCount)
+ badPrototypes.Add(protoId);
+ }
+
+ Assert.That(badPrototypes, Is.Empty);
+ await pair.CleanReturnAsync();
+ }
+
[Test]
public async Task AllComponentsOneToOneDeleteTest()
{
+using System.Linq;
using Content.Server.Administration.Commands;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.KillTracking;
if (ev.Assist is KillPlayerSource assist && dm.Victor == null)
_point.AdjustPointValue(assist.PlayerId, 1, uid, point);
- var spawns = EntitySpawnCollection.GetSpawns(dm.RewardSpawns);
+ var spawns = EntitySpawnCollection.GetSpawns(dm.RewardSpawns).Cast<string?>().ToList();
EntityManager.SpawnEntities(Transform(ev.Entity).MapPosition, spawns);
}
}
continue;
}
var getLoot = _prototypeManager.Index<EntityLootTablePrototype>(table);
- var spawnLoot = getLoot.GetSpawns();
+ var spawnLoot = getLoot.GetSpawns(_random);
var spawnPos = pos.Offset(_random.NextVector2(0.3f));
Spawn(spawnLoot[0], spawnPos);
}
SubscribeLocalEvent<IdentityComponent, DidEquipHandEvent>((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent<IdentityComponent, DidUnequipEvent>((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent<IdentityComponent, DidUnequipHandEvent>((uid, _, _) => QueueIdentityUpdate(uid));
+ SubscribeLocalEvent<IdentityComponent, MapInitEvent>(OnMapInit);
}
public override void Update(float frameTime)
}
// This is where the magic happens
- protected override void OnComponentInit(EntityUid uid, IdentityComponent component, ComponentInit args)
+ private void OnMapInit(EntityUid uid, IdentityComponent component, MapInitEvent args)
{
- base.OnComponentInit(uid, component, args);
-
var ident = Spawn(null, Transform(uid).Coordinates);
QueueIdentityUpdate(uid);
if (args.Handled)
return;
- if (component.PrototypesToSpawn?.Count > 0) {
+ if (component.PrototypesToSpawn?.Count > 0)
+ {
_popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-knife-needed"), uid, args.User);
args.Handled = true;
}
protected bool TryGetRandomStation([NotNullWhen(true)] out EntityUid? station, Func<EntityUid, bool>? filter = null)
{
- var stations = new ValueList<EntityUid>();
-
- if (filter == null)
- {
- stations.EnsureCapacity(Count<StationEventEligibleComponent>());
- }
+ var stations = new ValueList<EntityUid>(Count<StationEventEligibleComponent>());
filter ??= _ => true;
var query = AllEntityQuery<StationEventEligibleComponent>();
+using Content.Server.Spawners.Components;
using Content.Server.Storage.Components;
+using Content.Shared.Prototypes;
using Content.Shared.Storage;
using Content.Shared.Storage.Components;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
namespace Content.Server.Storage.EntitySystems;
var spawnItems = EntitySpawnCollection.GetSpawns(component.Contents, Random);
foreach (var item in spawnItems)
{
+ // No, you are not allowed to fill a container with entity spawners.
+ DebugTools.Assert(!_prototype.Index<EntityPrototype>(item)
+ .HasComponent(typeof(RandomSpawnerComponent)));
var ent = EntityManager.SpawnEntity(item, coordinates);
// handle depending on storage component, again this should be unified after ECS
- if (entityStorageComp != null && EntityStorage.Insert(ent, uid))
+ if (entityStorageComp != null && EntityStorage.Insert(ent, uid, entityStorageComp))
continue;
if (storageComp != null && Insert(uid, ent, out _, storageComp: storageComp, playSound: false))
using Robust.Server.Player;
using Robust.Shared.Map;
using Robust.Shared.Player;
-using Robust.Shared.Players;
+using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Server.Storage.EntitySystems;
public sealed partial class StorageSystem : SharedStorageSystem
{
[Dependency] private readonly IAdminManager _admin = default!;
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
public override void Initialize()
public ImmutableList<EntitySpawnEntry> Entries = ImmutableList<EntitySpawnEntry>.Empty;
/// <inheritdoc cref="EntitySpawnCollection.GetSpawns"/>
- public List<string?> GetSpawns(IRobustRandom? random = null)
+ public List<string> GetSpawns(IRobustRandom random)
{
return EntitySpawnCollection.GetSpawns(Entries, random);
}
[DataField("sound")]
public SoundSpecifier SpikeSound = new SoundPathSpecifier("/Audio/Effects/Fluids/splat.ogg");
- public List<string?>? PrototypesToSpawn;
+ public List<string>? PrototypesToSpawn;
// TODO: Spiking alive mobs? (Replace with uid) (deal damage to their limbs on spiking, kill on first butcher attempt?)
public string MeatSource1p = "?";
if (!string.IsNullOrEmpty(equipmentStr))
{
var equipmentEntity = EntityManager.SpawnEntity(equipmentStr, EntityManager.GetComponent<TransformComponent>(entity).Coordinates);
- InventorySystem.TryEquip(entity, equipmentEntity, slot.Name, true);
+ InventorySystem.TryEquip(entity, equipmentEntity, slot.Name, true, force:true);
}
}
}
/// <param name="entries">The entity spawn entries.</param>
/// <param name="random">Resolve param.</param>
/// <returns>A list of entity prototypes that should be spawned.</returns>
- public static List<string?> GetSpawns(IEnumerable<EntitySpawnEntry> entries,
+ public static List<string> GetSpawns(IEnumerable<EntitySpawnEntry> entries,
IRobustRandom? random = null)
{
IoCManager.Resolve(ref random);
- var spawned = new List<string?>();
+ var spawned = new List<string>();
var ungrouped = CollectOrGroups(entries, out var orGroupedSpawns);
foreach (var entry in ungrouped)
if (entry.SpawnProbability != 1f && !random.Prob(entry.SpawnProbability))
continue;
+ if (entry.PrototypeId == null)
+ continue;
+
var amount = (int) entry.GetAmount(random);
for (var i = 0; i < amount; i++)
if (diceRoll > cumulative)
continue;
+ if (entry.PrototypeId == null)
+ break;
+
// Dice roll succeeded, add item and break loop
var amount = (int) entry.GetAmount(random);
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
+using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
}
_joints.RecursiveClearJoints(toInsert);
+ if (!component.Contents.Insert(toInsert, EntityManager))
+ return false;
+
var inside = EnsureComp<InsideEntityStorageComponent>(toInsert);
inside.Storage = container;
- return component.Contents.Insert(toInsert, EntityManager);
+ return true;
}
public bool Remove(EntityUid toRemove, EntityUid container, SharedEntityStorageComponent? component = null, TransformComponent? xform = null)
id: CratePartsT3
name: tier 3 parts crate
description: Contains 5 random tier 3 parts for upgrading machines.
- components:
- - type: StorageFill
- contents:
- - id: SalvagePartsT3Spawner
- amount: 5
+ # TODO add contents.
+ #components:
+ #- type: StorageFill
+ # contents:
+ # - id: SalvagePartsT3Spawner
+ # amount: 5
- type: entity
parent: CrateGenericSteel
id: CratePartsT3T4
name: tier 3/4 parts crate
description: Contains 5 random tier 3 or 4 parts for upgrading machines.
- components:
- - type: StorageFill
- contents:
- - id: SalvagePartsT3T4Spawner
- amount: 5
+ # TODO add contents.
+ #components:
+ # type: StorageFill
+ # contents:
+ # - id: SalvagePartsT3T4Spawner
+ # amount: 5
- type: entity
parent: CrateGenericSteel
id: CratePartsT4
name: tier 4 parts crate
description: Contains 5 random tier 4 parts for upgrading machines.
- components:
- - type: StorageFill
- contents:
- - id: SalvagePartsT4Spawner
- amount: 5
+ # TODO add contents.
+ #components:
+ #- type: StorageFill
+ # contents:
+ # - id: SalvagePartsT4Spawner
+ # amount: 5