Add new entity spawn test & fix misc bugs (#19953)
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Content.Server.Humanoid.Components;
|
||||||
using Content.Shared.Coordinates;
|
using Content.Shared.Coordinates;
|
||||||
|
using Content.Shared.Prototypes;
|
||||||
using Robust.Shared;
|
using Robust.Shared;
|
||||||
using Robust.Shared.Configuration;
|
using Robust.Shared.Configuration;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
@@ -183,7 +185,7 @@ namespace Content.IntegrationTests.Tests
|
|||||||
var query = entityMan.AllEntityQueryEnumerator<TComp>();
|
var query = entityMan.AllEntityQueryEnumerator<TComp>();
|
||||||
while (query.MoveNext(out var uid, out var meta))
|
while (query.MoveNext(out var uid, out var meta))
|
||||||
yield return (uid, meta);
|
yield return (uid, meta);
|
||||||
};
|
}
|
||||||
|
|
||||||
var entityMetas = Query<MetaDataComponent>(sEntMan).ToList();
|
var entityMetas = Query<MetaDataComponent>(sEntMan).ToList();
|
||||||
foreach (var (uid, meta) in entityMetas)
|
foreach (var (uid, meta) in entityMetas)
|
||||||
@@ -198,6 +200,100 @@ namespace Content.IntegrationTests.Tests
|
|||||||
await pair.CleanReturnAsync();
|
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]
|
[Test]
|
||||||
public async Task AllComponentsOneToOneDeleteTest()
|
public async Task AllComponentsOneToOneDeleteTest()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Linq;
|
||||||
using Content.Server.Administration.Commands;
|
using Content.Server.Administration.Commands;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.KillTracking;
|
using Content.Server.KillTracking;
|
||||||
@@ -95,7 +96,7 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem<DeathMatchRuleComponen
|
|||||||
if (ev.Assist is KillPlayerSource assist && dm.Victor == null)
|
if (ev.Assist is KillPlayerSource assist && dm.Victor == null)
|
||||||
_point.AdjustPointValue(assist.PlayerId, 1, uid, point);
|
_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);
|
EntityManager.SpawnEntities(Transform(ev.Entity).MapPosition, spawns);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ public sealed partial class GatherableSystem : EntitySystem
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var getLoot = _prototypeManager.Index<EntityLootTablePrototype>(table);
|
var getLoot = _prototypeManager.Index<EntityLootTablePrototype>(table);
|
||||||
var spawnLoot = getLoot.GetSpawns();
|
var spawnLoot = getLoot.GetSpawns(_random);
|
||||||
var spawnPos = pos.Offset(_random.NextVector2(0.3f));
|
var spawnPos = pos.Offset(_random.NextVector2(0.3f));
|
||||||
Spawn(spawnLoot[0], spawnPos);
|
Spawn(spawnLoot[0], spawnPos);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ public class IdentitySystem : SharedIdentitySystem
|
|||||||
SubscribeLocalEvent<IdentityComponent, DidEquipHandEvent>((uid, _, _) => QueueIdentityUpdate(uid));
|
SubscribeLocalEvent<IdentityComponent, DidEquipHandEvent>((uid, _, _) => QueueIdentityUpdate(uid));
|
||||||
SubscribeLocalEvent<IdentityComponent, DidUnequipEvent>((uid, _, _) => QueueIdentityUpdate(uid));
|
SubscribeLocalEvent<IdentityComponent, DidUnequipEvent>((uid, _, _) => QueueIdentityUpdate(uid));
|
||||||
SubscribeLocalEvent<IdentityComponent, DidUnequipHandEvent>((uid, _, _) => QueueIdentityUpdate(uid));
|
SubscribeLocalEvent<IdentityComponent, DidUnequipHandEvent>((uid, _, _) => QueueIdentityUpdate(uid));
|
||||||
|
SubscribeLocalEvent<IdentityComponent, MapInitEvent>(OnMapInit);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Update(float frameTime)
|
public override void Update(float frameTime)
|
||||||
@@ -53,10 +54,8 @@ public class IdentitySystem : SharedIdentitySystem
|
|||||||
}
|
}
|
||||||
|
|
||||||
// This is where the magic happens
|
// 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);
|
var ident = Spawn(null, Transform(uid).Coordinates);
|
||||||
|
|
||||||
QueueIdentityUpdate(uid);
|
QueueIdentityUpdate(uid);
|
||||||
|
|||||||
@@ -110,7 +110,8 @@ namespace Content.Server.Kitchen.EntitySystems
|
|||||||
if (args.Handled)
|
if (args.Handled)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (component.PrototypesToSpawn?.Count > 0) {
|
if (component.PrototypesToSpawn?.Count > 0)
|
||||||
|
{
|
||||||
_popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-knife-needed"), uid, args.User);
|
_popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-knife-needed"), uid, args.User);
|
||||||
args.Handled = true;
|
args.Handled = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,12 +136,7 @@ public abstract partial class StationEventSystem<T> : GameRuleSystem<T> where T
|
|||||||
|
|
||||||
protected bool TryGetRandomStation([NotNullWhen(true)] out EntityUid? station, Func<EntityUid, bool>? filter = null)
|
protected bool TryGetRandomStation([NotNullWhen(true)] out EntityUid? station, Func<EntityUid, bool>? filter = null)
|
||||||
{
|
{
|
||||||
var stations = new ValueList<EntityUid>();
|
var stations = new ValueList<EntityUid>(Count<StationEventEligibleComponent>());
|
||||||
|
|
||||||
if (filter == null)
|
|
||||||
{
|
|
||||||
stations.EnsureCapacity(Count<StationEventEligibleComponent>());
|
|
||||||
}
|
|
||||||
|
|
||||||
filter ??= _ => true;
|
filter ??= _ => true;
|
||||||
var query = AllEntityQuery<StationEventEligibleComponent>();
|
var query = AllEntityQuery<StationEventEligibleComponent>();
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
using Content.Server.Spawners.Components;
|
||||||
using Content.Server.Storage.Components;
|
using Content.Server.Storage.Components;
|
||||||
|
using Content.Shared.Prototypes;
|
||||||
using Content.Shared.Storage;
|
using Content.Shared.Storage;
|
||||||
using Content.Shared.Storage.Components;
|
using Content.Shared.Storage.Components;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
namespace Content.Server.Storage.EntitySystems;
|
namespace Content.Server.Storage.EntitySystems;
|
||||||
|
|
||||||
@@ -25,10 +29,13 @@ public sealed partial class StorageSystem
|
|||||||
var spawnItems = EntitySpawnCollection.GetSpawns(component.Contents, Random);
|
var spawnItems = EntitySpawnCollection.GetSpawns(component.Contents, Random);
|
||||||
foreach (var item in spawnItems)
|
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);
|
var ent = EntityManager.SpawnEntity(item, coordinates);
|
||||||
|
|
||||||
// handle depending on storage component, again this should be unified after ECS
|
// 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;
|
continue;
|
||||||
|
|
||||||
if (storageComp != null && Insert(uid, ent, out _, storageComp: storageComp, playSound: false))
|
if (storageComp != null && Insert(uid, ent, out _, storageComp: storageComp, playSound: false))
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ using Robust.Server.GameObjects;
|
|||||||
using Robust.Server.Player;
|
using Robust.Server.Player;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Player;
|
using Robust.Shared.Player;
|
||||||
using Robust.Shared.Players;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
namespace Content.Server.Storage.EntitySystems;
|
namespace Content.Server.Storage.EntitySystems;
|
||||||
@@ -20,6 +20,7 @@ namespace Content.Server.Storage.EntitySystems;
|
|||||||
public sealed partial class StorageSystem : SharedStorageSystem
|
public sealed partial class StorageSystem : SharedStorageSystem
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IAdminManager _admin = default!;
|
[Dependency] private readonly IAdminManager _admin = default!;
|
||||||
|
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||||
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
|
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public sealed class EntityLootTablePrototype : IPrototype
|
|||||||
public ImmutableList<EntitySpawnEntry> Entries = ImmutableList<EntitySpawnEntry>.Empty;
|
public ImmutableList<EntitySpawnEntry> Entries = ImmutableList<EntitySpawnEntry>.Empty;
|
||||||
|
|
||||||
/// <inheritdoc cref="EntitySpawnCollection.GetSpawns"/>
|
/// <inheritdoc cref="EntitySpawnCollection.GetSpawns"/>
|
||||||
public List<string?> GetSpawns(IRobustRandom? random = null)
|
public List<string> GetSpawns(IRobustRandom random)
|
||||||
{
|
{
|
||||||
return EntitySpawnCollection.GetSpawns(Entries, random);
|
return EntitySpawnCollection.GetSpawns(Entries, random);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public sealed partial class KitchenSpikeComponent : Component
|
|||||||
[DataField("sound")]
|
[DataField("sound")]
|
||||||
public SoundSpecifier SpikeSound = new SoundPathSpecifier("/Audio/Effects/Fluids/splat.ogg");
|
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?)
|
// TODO: Spiking alive mobs? (Replace with uid) (deal damage to their limbs on spiking, kill on first butcher attempt?)
|
||||||
public string MeatSource1p = "?";
|
public string MeatSource1p = "?";
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public abstract class SharedStationSpawningSystem : EntitySystem
|
|||||||
if (!string.IsNullOrEmpty(equipmentStr))
|
if (!string.IsNullOrEmpty(equipmentStr))
|
||||||
{
|
{
|
||||||
var equipmentEntity = EntityManager.SpawnEntity(equipmentStr, EntityManager.GetComponent<TransformComponent>(entity).Coordinates);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,12 +78,12 @@ public static class EntitySpawnCollection
|
|||||||
/// <param name="entries">The entity spawn entries.</param>
|
/// <param name="entries">The entity spawn entries.</param>
|
||||||
/// <param name="random">Resolve param.</param>
|
/// <param name="random">Resolve param.</param>
|
||||||
/// <returns>A list of entity prototypes that should be spawned.</returns>
|
/// <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)
|
IRobustRandom? random = null)
|
||||||
{
|
{
|
||||||
IoCManager.Resolve(ref random);
|
IoCManager.Resolve(ref random);
|
||||||
|
|
||||||
var spawned = new List<string?>();
|
var spawned = new List<string>();
|
||||||
var ungrouped = CollectOrGroups(entries, out var orGroupedSpawns);
|
var ungrouped = CollectOrGroups(entries, out var orGroupedSpawns);
|
||||||
|
|
||||||
foreach (var entry in ungrouped)
|
foreach (var entry in ungrouped)
|
||||||
@@ -93,6 +93,9 @@ public static class EntitySpawnCollection
|
|||||||
if (entry.SpawnProbability != 1f && !random.Prob(entry.SpawnProbability))
|
if (entry.SpawnProbability != 1f && !random.Prob(entry.SpawnProbability))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (entry.PrototypeId == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
var amount = (int) entry.GetAmount(random);
|
var amount = (int) entry.GetAmount(random);
|
||||||
|
|
||||||
for (var i = 0; i < amount; i++)
|
for (var i = 0; i < amount; i++)
|
||||||
@@ -116,6 +119,9 @@ public static class EntitySpawnCollection
|
|||||||
if (diceRoll > cumulative)
|
if (diceRoll > cumulative)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (entry.PrototypeId == null)
|
||||||
|
break;
|
||||||
|
|
||||||
// Dice roll succeeded, add item and break loop
|
// Dice roll succeeded, add item and break loop
|
||||||
var amount = (int) entry.GetAmount(random);
|
var amount = (int) entry.GetAmount(random);
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ using Robust.Shared.Network;
|
|||||||
using Robust.Shared.Physics;
|
using Robust.Shared.Physics;
|
||||||
using Robust.Shared.Physics.Components;
|
using Robust.Shared.Physics.Components;
|
||||||
using Robust.Shared.Physics.Systems;
|
using Robust.Shared.Physics.Systems;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
@@ -260,9 +261,12 @@ public abstract class SharedEntityStorageSystem : EntitySystem
|
|||||||
}
|
}
|
||||||
|
|
||||||
_joints.RecursiveClearJoints(toInsert);
|
_joints.RecursiveClearJoints(toInsert);
|
||||||
|
if (!component.Contents.Insert(toInsert, EntityManager))
|
||||||
|
return false;
|
||||||
|
|
||||||
var inside = EnsureComp<InsideEntityStorageComponent>(toInsert);
|
var inside = EnsureComp<InsideEntityStorageComponent>(toInsert);
|
||||||
inside.Storage = container;
|
inside.Storage = container;
|
||||||
return component.Contents.Insert(toInsert, EntityManager);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Remove(EntityUid toRemove, EntityUid container, SharedEntityStorageComponent? component = null, TransformComponent? xform = null)
|
public bool Remove(EntityUid toRemove, EntityUid container, SharedEntityStorageComponent? component = null, TransformComponent? xform = null)
|
||||||
|
|||||||
@@ -92,30 +92,33 @@
|
|||||||
id: CratePartsT3
|
id: CratePartsT3
|
||||||
name: tier 3 parts crate
|
name: tier 3 parts crate
|
||||||
description: Contains 5 random tier 3 parts for upgrading machines.
|
description: Contains 5 random tier 3 parts for upgrading machines.
|
||||||
components:
|
# TODO add contents.
|
||||||
- type: StorageFill
|
#components:
|
||||||
contents:
|
#- type: StorageFill
|
||||||
- id: SalvagePartsT3Spawner
|
# contents:
|
||||||
amount: 5
|
# - id: SalvagePartsT3Spawner
|
||||||
|
# amount: 5
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
parent: CrateGenericSteel
|
parent: CrateGenericSteel
|
||||||
id: CratePartsT3T4
|
id: CratePartsT3T4
|
||||||
name: tier 3/4 parts crate
|
name: tier 3/4 parts crate
|
||||||
description: Contains 5 random tier 3 or 4 parts for upgrading machines.
|
description: Contains 5 random tier 3 or 4 parts for upgrading machines.
|
||||||
components:
|
# TODO add contents.
|
||||||
- type: StorageFill
|
#components:
|
||||||
contents:
|
# type: StorageFill
|
||||||
- id: SalvagePartsT3T4Spawner
|
# contents:
|
||||||
amount: 5
|
# - id: SalvagePartsT3T4Spawner
|
||||||
|
# amount: 5
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
parent: CrateGenericSteel
|
parent: CrateGenericSteel
|
||||||
id: CratePartsT4
|
id: CratePartsT4
|
||||||
name: tier 4 parts crate
|
name: tier 4 parts crate
|
||||||
description: Contains 5 random tier 4 parts for upgrading machines.
|
description: Contains 5 random tier 4 parts for upgrading machines.
|
||||||
components:
|
# TODO add contents.
|
||||||
- type: StorageFill
|
#components:
|
||||||
contents:
|
#- type: StorageFill
|
||||||
- id: SalvagePartsT4Spawner
|
# contents:
|
||||||
amount: 5
|
# - id: SalvagePartsT4Spawner
|
||||||
|
# amount: 5
|
||||||
|
|||||||
Reference in New Issue
Block a user