]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Entity Tables (EntitySpawnEntry replacement) (#30579)
authorNemanja <98561806+EmoGarbage404@users.noreply.github.com>
Sat, 10 Aug 2024 02:12:40 +0000 (22:12 -0400)
committerGitHub <noreply@github.com>
Sat, 10 Aug 2024 02:12:40 +0000 (22:12 -0400)
* Entity table code

* entity table examples

* fix dat shit

* access

* tests tests tests

* sloth review

19 files changed:
Content.Server/Spawners/Components/EntityTableSpawnerComponent.cs [new file with mode: 0644]
Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs
Content.Shared/Containers/ContainerFillSystem.cs
Content.Shared/Containers/EntityTableContainerFillComponent.cs [new file with mode: 0644]
Content.Shared/EntityTable/EntitySelectors/AllSelector.cs [new file with mode: 0644]
Content.Shared/EntityTable/EntitySelectors/EntSelector.cs [new file with mode: 0644]
Content.Shared/EntityTable/EntitySelectors/EntityTableSelector.cs [new file with mode: 0644]
Content.Shared/EntityTable/EntitySelectors/GroupSelector.cs [new file with mode: 0644]
Content.Shared/EntityTable/EntitySelectors/NestedSelector.cs [new file with mode: 0644]
Content.Shared/EntityTable/EntitySelectors/NoneSelector.cs [new file with mode: 0644]
Content.Shared/EntityTable/EntityTablePrototype.cs [new file with mode: 0644]
Content.Shared/EntityTable/EntityTableSystem.cs [new file with mode: 0644]
Content.Shared/EntityTable/ValueSelector/ConstantNumberSelector.cs [new file with mode: 0644]
Content.Shared/EntityTable/ValueSelector/NumberSelector.cs [new file with mode: 0644]
Content.Shared/EntityTable/ValueSelector/RangeNumberSelector.cs [new file with mode: 0644]
Content.Shared/Random/Helpers/SharedRandomExtensions.cs
Resources/Prototypes/Catalog/Fills/Crates/fun.yml
Resources/Prototypes/Catalog/Fills/Lockers/misc.yml
Resources/Prototypes/Entities/Markers/Spawners/Random/maintenance.yml

diff --git a/Content.Server/Spawners/Components/EntityTableSpawnerComponent.cs b/Content.Server/Spawners/Components/EntityTableSpawnerComponent.cs
new file mode 100644 (file)
index 0000000..d122eb0
--- /dev/null
@@ -0,0 +1,30 @@
+using Content.Server.Spawners.EntitySystems;
+using Content.Shared.EntityTable.EntitySelectors;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Spawners.Components;
+
+[RegisterComponent, EntityCategory("Spawner"), Access(typeof(ConditionalSpawnerSystem))]
+public sealed partial class EntityTableSpawnerComponent : Component
+{
+    /// <summary>
+    /// Table that determines what gets spawned.
+    /// </summary>
+    [DataField(required: true)]
+    public EntityTableSelector Table = default!;
+
+    /// <summary>
+    /// Scatter of entity spawn coordinates
+    /// </summary>
+    [DataField]
+    public float Offset = 0.2f;
+
+    /// <summary>
+    /// A variable meaning whether the spawn will
+    /// be able to be used again or whether
+    /// it will be destroyed after the first use
+    /// </summary>
+    [DataField]
+    public bool DeleteSpawnerAfterSpawn = true;
+}
+
index f57481b05b630104339d4a0e4c59c08d3903d4f3..ad59fc83cfa97a1a6219c6acdec81644be4697c2 100644 (file)
@@ -1,9 +1,10 @@
 using System.Numerics;
 using Content.Server.GameTicking;
-using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Spawners.Components;
+using Content.Shared.EntityTable;
 using Content.Shared.GameTicking.Components;
 using JetBrains.Annotations;
+using Robust.Shared.Map;
 using Robust.Shared.Random;
 
 namespace Content.Server.Spawners.EntitySystems
@@ -13,6 +14,7 @@ namespace Content.Server.Spawners.EntitySystems
     {
         [Dependency] private readonly IRobustRandom _robustRandom = default!;
         [Dependency] private readonly GameTicker _ticker = default!;
+        [Dependency] private readonly EntityTableSystem _entityTable = default!;
 
         public override void Initialize()
         {
@@ -21,6 +23,7 @@ namespace Content.Server.Spawners.EntitySystems
             SubscribeLocalEvent<GameRuleStartedEvent>(OnRuleStarted);
             SubscribeLocalEvent<ConditionalSpawnerComponent, MapInitEvent>(OnCondSpawnMapInit);
             SubscribeLocalEvent<RandomSpawnerComponent, MapInitEvent>(OnRandSpawnMapInit);
+            SubscribeLocalEvent<EntityTableSpawnerComponent, MapInitEvent>(OnEntityTableSpawnMapInit);
         }
 
         private void OnCondSpawnMapInit(EntityUid uid, ConditionalSpawnerComponent component, MapInitEvent args)
@@ -35,6 +38,13 @@ namespace Content.Server.Spawners.EntitySystems
                 QueueDel(uid);
         }
 
+        private void OnEntityTableSpawnMapInit(Entity<EntityTableSpawnerComponent> ent, ref MapInitEvent args)
+        {
+            Spawn(ent);
+            if (ent.Comp.DeleteSpawnerAfterSpawn && !TerminatingOrDeleted(ent) && Exists(ent))
+                QueueDel(ent);
+        }
+
         private void OnRuleStarted(ref GameRuleStartedEvent args)
         {
             var query = EntityQueryEnumerator<ConditionalSpawnerComponent>();
@@ -110,5 +120,23 @@ namespace Content.Server.Spawners.EntitySystems
 
             EntityManager.SpawnEntity(_robustRandom.Pick(component.Prototypes), coordinates);
         }
+
+        private void Spawn(Entity<EntityTableSpawnerComponent> ent)
+        {
+            if (TerminatingOrDeleted(ent) || !Exists(ent))
+                return;
+
+            var coords = Transform(ent).Coordinates;
+
+            var spawns = _entityTable.GetSpawns(ent.Comp.Table);
+            foreach (var proto in spawns)
+            {
+                var xOffset = _robustRandom.NextFloat(-ent.Comp.Offset, ent.Comp.Offset);
+                var yOffset = _robustRandom.NextFloat(-ent.Comp.Offset, ent.Comp.Offset);
+                var trueCoords = coords.Offset(new Vector2(xOffset, yOffset));
+
+                Spawn(proto, trueCoords);
+            }
+        }
     }
 }
index e120b6bc8838ce30dc0fd4ed08ae666f8d251088..51c7c48e40f08f7101b4921797ca5a643c6a5062 100644 (file)
@@ -1,4 +1,5 @@
 using System.Numerics;
+using Content.Shared.EntityTable;
 using Robust.Shared.Containers;
 using Robust.Shared.Map;
 
@@ -7,11 +8,14 @@ namespace Content.Shared.Containers;
 public sealed class ContainerFillSystem : EntitySystem
 {
     [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
+    [Dependency] private readonly EntityTableSystem _entityTable = default!;
+    [Dependency] private readonly SharedTransformSystem _transform = default!;
 
     public override void Initialize()
     {
         base.Initialize();
         SubscribeLocalEvent<ContainerFillComponent, MapInitEvent>(OnMapInit);
+        SubscribeLocalEvent<EntityTableContainerFillComponent, MapInitEvent>(OnTableMapInit);
     }
 
     private void OnMapInit(EntityUid uid, ContainerFillComponent component, MapInitEvent args)
@@ -42,4 +46,37 @@ public sealed class ContainerFillSystem : EntitySystem
             }
         }
     }
+
+    private void OnTableMapInit(Entity<EntityTableContainerFillComponent> ent, ref MapInitEvent args)
+    {
+        if (!TryComp(ent, out ContainerManagerComponent? containerComp))
+            return;
+
+        if (TerminatingOrDeleted(ent) || !Exists(ent))
+            return;
+
+        var xform = Transform(ent);
+        var coords = new EntityCoordinates(ent, Vector2.Zero);
+
+        foreach (var (containerId, table) in ent.Comp.Containers)
+        {
+            if (!_containerSystem.TryGetContainer(ent, containerId, out var container, containerComp))
+            {
+                Log.Error($"Entity {ToPrettyString(ent)} with a {nameof(EntityTableContainerFillComponent)} is missing a container ({containerId}).");
+                continue;
+            }
+
+            var spawns = _entityTable.GetSpawns(table);
+            foreach (var proto in spawns)
+            {
+                var spawn = Spawn(proto, coords);
+                if (!_containerSystem.Insert(spawn, container, containerXform: xform))
+                {
+                    Log.Error($"Entity {ToPrettyString(ent)} with a {nameof(EntityTableContainerFillComponent)} failed to insert an entity: {ToPrettyString(spawn)}.");
+                    _transform.AttachToGridOrMap(spawn);
+                    break;
+                }
+            }
+        }
+    }
 }
diff --git a/Content.Shared/Containers/EntityTableContainerFillComponent.cs b/Content.Shared/Containers/EntityTableContainerFillComponent.cs
new file mode 100644 (file)
index 0000000..3f30dc8
--- /dev/null
@@ -0,0 +1,13 @@
+using Content.Shared.EntityTable.EntitySelectors;
+
+namespace Content.Shared.Containers;
+
+/// <summary>
+/// Version of <see cref="ContainerFillComponent"/> that utilizes <see cref="EntityTableSelector"/>
+/// </summary>
+[RegisterComponent, Access(typeof(ContainerFillSystem))]
+public sealed partial class EntityTableContainerFillComponent : Component
+{
+    [DataField]
+    public Dictionary<string, EntityTableSelector> Containers = new();
+}
diff --git a/Content.Shared/EntityTable/EntitySelectors/AllSelector.cs b/Content.Shared/EntityTable/EntitySelectors/AllSelector.cs
new file mode 100644 (file)
index 0000000..8fb8b5e
--- /dev/null
@@ -0,0 +1,25 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.EntityTable.EntitySelectors;
+
+/// <summary>
+/// Gets spawns from all of the child selectors
+/// </summary>
+public sealed partial class AllSelector : EntityTableSelector
+{
+    [DataField(required: true)]
+    public List<EntityTableSelector> Children;
+
+    protected override IEnumerable<EntProtoId> GetSpawnsImplementation(System.Random rand,
+        IEntityManager entMan,
+        IPrototypeManager proto)
+    {
+        foreach (var child in Children)
+        {
+            foreach (var spawn in child.GetSpawns(rand, entMan, proto))
+            {
+                yield return spawn;
+            }
+        }
+    }
+}
diff --git a/Content.Shared/EntityTable/EntitySelectors/EntSelector.cs b/Content.Shared/EntityTable/EntitySelectors/EntSelector.cs
new file mode 100644 (file)
index 0000000..b1e712b
--- /dev/null
@@ -0,0 +1,27 @@
+using Content.Shared.EntityTable.ValueSelector;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.EntityTable.EntitySelectors;
+
+/// <summary>
+/// Gets the spawn for the entity prototype specified at whatever count specified.
+/// </summary>
+public sealed partial class EntSelector : EntityTableSelector
+{
+    [DataField(required: true)]
+    public EntProtoId Id;
+
+    [DataField]
+    public NumberSelector Amount = new ConstantNumberSelector(1);
+
+    protected override IEnumerable<EntProtoId> GetSpawnsImplementation(System.Random rand,
+        IEntityManager entMan,
+        IPrototypeManager proto)
+    {
+        var num = (int) Math.Round(Amount.Get(rand, entMan, proto));
+        for (var i = 0; i < num; i++)
+        {
+            yield return Id;
+        }
+    }
+}
diff --git a/Content.Shared/EntityTable/EntitySelectors/EntityTableSelector.cs b/Content.Shared/EntityTable/EntitySelectors/EntityTableSelector.cs
new file mode 100644 (file)
index 0000000..2533f17
--- /dev/null
@@ -0,0 +1,49 @@
+using Content.Shared.EntityTable.ValueSelector;
+using JetBrains.Annotations;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Shared.EntityTable.EntitySelectors;
+
+[ImplicitDataDefinitionForInheritors, UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)]
+public abstract partial class EntityTableSelector
+{
+    /// <summary>
+    /// The number of times this selector is run
+    /// </summary>
+    [DataField]
+    public NumberSelector Rolls = new ConstantNumberSelector(1);
+
+    /// <summary>
+    /// A weight used to pick between selectors.
+    /// </summary>
+    [DataField]
+    public float Weight = 1;
+
+    /// <summary>
+    /// A simple chance that the selector will run.
+    /// </summary>
+    [DataField]
+    public double Prob = 1;
+
+    public IEnumerable<EntProtoId> GetSpawns(System.Random rand,
+        IEntityManager entMan,
+        IPrototypeManager proto)
+    {
+        var rolls = Rolls.Get(rand, entMan, proto);
+        for (var i = 0; i < rolls; i++)
+        {
+            if (!rand.Prob(Prob))
+                continue;
+
+            foreach (var spawn in GetSpawnsImplementation(rand, entMan, proto))
+            {
+                yield return spawn;
+            }
+        }
+    }
+
+    protected abstract IEnumerable<EntProtoId> GetSpawnsImplementation(System.Random rand,
+        IEntityManager entMan,
+        IPrototypeManager proto);
+}
diff --git a/Content.Shared/EntityTable/EntitySelectors/GroupSelector.cs b/Content.Shared/EntityTable/EntitySelectors/GroupSelector.cs
new file mode 100644 (file)
index 0000000..8f761f9
--- /dev/null
@@ -0,0 +1,28 @@
+using Content.Shared.Random.Helpers;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.EntityTable.EntitySelectors;
+
+/// <summary>
+/// Gets the spawns from one of the child selectors, based on the weight of the children
+/// </summary>
+public sealed partial class GroupSelector : EntityTableSelector
+{
+    [DataField(required: true)]
+    public List<EntityTableSelector> Children = new();
+
+    protected override IEnumerable<EntProtoId> GetSpawnsImplementation(System.Random rand,
+        IEntityManager entMan,
+        IPrototypeManager proto)
+    {
+        var children = new Dictionary<EntityTableSelector, float>(Children.Count);
+        foreach (var child in Children)
+        {
+            children.Add(child, child.Weight);
+        }
+
+        var pick = SharedRandomExtensions.Pick(children, rand);
+
+        return pick.GetSpawns(rand, entMan, proto);
+    }
+}
diff --git a/Content.Shared/EntityTable/EntitySelectors/NestedSelector.cs b/Content.Shared/EntityTable/EntitySelectors/NestedSelector.cs
new file mode 100644 (file)
index 0000000..fc8d8f0
--- /dev/null
@@ -0,0 +1,20 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.EntityTable.EntitySelectors;
+
+/// <summary>
+/// Gets the spawns from the entity table prototype specified.
+/// Can be used to reuse common tables.
+/// </summary>
+public sealed partial class NestedSelector : EntityTableSelector
+{
+    [DataField(required: true)]
+    public ProtoId<EntityTablePrototype> TableId;
+
+    protected override IEnumerable<EntProtoId> GetSpawnsImplementation(System.Random rand,
+        IEntityManager entMan,
+        IPrototypeManager proto)
+    {
+        return proto.Index(TableId).Table.GetSpawns(rand, entMan, proto);
+    }
+}
diff --git a/Content.Shared/EntityTable/EntitySelectors/NoneSelector.cs b/Content.Shared/EntityTable/EntitySelectors/NoneSelector.cs
new file mode 100644 (file)
index 0000000..21fcb6d
--- /dev/null
@@ -0,0 +1,16 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.EntityTable.EntitySelectors;
+
+/// <summary>
+/// Selects nothing.
+/// </summary>
+public sealed partial class NoneSelector : EntityTableSelector
+{
+    protected override IEnumerable<EntProtoId> GetSpawnsImplementation(System.Random rand,
+        IEntityManager entMan,
+        IPrototypeManager proto)
+    {
+        yield break;
+    }
+}
diff --git a/Content.Shared/EntityTable/EntityTablePrototype.cs b/Content.Shared/EntityTable/EntityTablePrototype.cs
new file mode 100644 (file)
index 0000000..63cebe9
--- /dev/null
@@ -0,0 +1,18 @@
+using Content.Shared.EntityTable.EntitySelectors;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.EntityTable;
+
+/// <summary>
+/// This is a prototype for...
+/// </summary>
+[Prototype]
+public sealed partial class EntityTablePrototype : IPrototype
+{
+    /// <inheritdoc/>
+    [IdDataField]
+    public string ID { get; } = default!;
+
+    [DataField(required: true)]
+    public EntityTableSelector Table = default!;
+}
diff --git a/Content.Shared/EntityTable/EntityTableSystem.cs b/Content.Shared/EntityTable/EntityTableSystem.cs
new file mode 100644 (file)
index 0000000..ff499e6
--- /dev/null
@@ -0,0 +1,20 @@
+using Content.Shared.EntityTable.EntitySelectors;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Shared.EntityTable;
+
+public sealed class EntityTableSystem : EntitySystem
+{
+    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+    [Dependency] private readonly IRobustRandom _random = default!;
+
+    public IEnumerable<EntProtoId> GetSpawns(EntityTableSelector? table, System.Random? rand = null)
+    {
+        if (table == null)
+            return new List<EntProtoId>();
+
+        rand ??= _random.GetRandom();
+        return table.GetSpawns(rand, EntityManager, _prototypeManager);
+    }
+}
diff --git a/Content.Shared/EntityTable/ValueSelector/ConstantNumberSelector.cs b/Content.Shared/EntityTable/ValueSelector/ConstantNumberSelector.cs
new file mode 100644 (file)
index 0000000..0baf678
--- /dev/null
@@ -0,0 +1,22 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.EntityTable.ValueSelector;
+
+/// <summary>
+/// Gives a constant value.
+/// </summary>
+public sealed partial class ConstantNumberSelector : NumberSelector
+{
+    [DataField]
+    public float Value = 1;
+
+    public ConstantNumberSelector(float value)
+    {
+        Value = value;
+    }
+
+    public override float Get(System.Random rand, IEntityManager entMan, IPrototypeManager proto)
+    {
+        return Value;
+    }
+}
diff --git a/Content.Shared/EntityTable/ValueSelector/NumberSelector.cs b/Content.Shared/EntityTable/ValueSelector/NumberSelector.cs
new file mode 100644 (file)
index 0000000..8a7743c
--- /dev/null
@@ -0,0 +1,16 @@
+using Content.Shared.EntityTable.EntitySelectors;
+using JetBrains.Annotations;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.EntityTable.ValueSelector;
+
+/// <summary>
+/// Used for implementing custom value selection for <see cref="EntityTableSelector"/>
+/// </summary>
+[ImplicitDataDefinitionForInheritors, UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)]
+public abstract partial class NumberSelector
+{
+    public abstract float Get(System.Random rand,
+        IEntityManager entMan,
+        IPrototypeManager proto);
+}
diff --git a/Content.Shared/EntityTable/ValueSelector/RangeNumberSelector.cs b/Content.Shared/EntityTable/ValueSelector/RangeNumberSelector.cs
new file mode 100644 (file)
index 0000000..e8356fc
--- /dev/null
@@ -0,0 +1,19 @@
+using System.Numerics;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Shared.EntityTable.ValueSelector;
+
+/// <summary>
+/// Gives a value between the two numbers specified, inclusive.
+/// </summary>
+public sealed partial class RangeNumberSelector : NumberSelector
+{
+    [DataField]
+    public Vector2 Range = new(1, 1);
+
+    public override float Get(System.Random rand, IEntityManager entMan, IPrototypeManager proto)
+    {
+        return rand.NextFloat(Range.X, Range.Y + 1);
+    }
+}
index 376e91743d073100b3ccdc9142239bfed8b8dfc6..42d92a90653b79062570899d1d62bae8da944851 100644 (file)
@@ -108,6 +108,27 @@ namespace Content.Shared.Random.Helpers
             return true;
         }
 
+        public static T Pick<T>(Dictionary<T, float> weights, System.Random random)
+            where T : notnull
+        {
+            var sum = weights.Values.Sum();
+            var accumulated = 0f;
+
+            var rand = random.NextFloat() * sum;
+
+            foreach (var (key, weight) in weights)
+            {
+                accumulated += weight;
+
+                if (accumulated >= rand)
+                {
+                    return key;
+                }
+            }
+
+            throw new InvalidOperationException("Invalid weighted pick");
+        }
+
         public static (string reagent, FixedPoint2 quantity) Pick(this WeightedRandomFillSolutionPrototype prototype, IRobustRandom? random = null)
         {
             var randomFill = prototype.PickRandomFill(random);
index c52e13bd13d93d0140c59510de25c49fcff04a3e..ebc7a446b9663a706e950f8ea6177aab41d55696 100644 (file)
@@ -1,33 +1,81 @@
+- type: entityTable
+  id: AllPlushiesTable
+  table: !type:GroupSelector
+    children:
+    - !type:EntSelector
+      id: PlushieBee
+    - !type:EntSelector
+      id: PlushieNar
+      weight: 0.5
+    - !type:EntSelector
+      id: PlushieRatvar
+      weight: 0.5
+    - !type:EntSelector
+      id: PlushieNuke
+    - !type:EntSelector
+      id: PlushieSlime
+    - !type:EntSelector
+      id: PlushieSnake
+    - !type:GroupSelector
+      children:
+      - !type:EntSelector
+        id: PlushieLizard
+        weight: 9
+      - !type:EntSelector
+        id: PlushieSpaceLizard
+        weight: 1
+    - !type:GroupSelector
+      children:
+      - !type:EntSelector
+        id: PlushieCarp
+      - !type:EntSelector
+        id: PlushieHolocarp
+        weight: 0.25
+      - !type:EntSelector
+        id: PlushieMagicarp
+        weight: 0.25
+      - !type:EntSelector
+        id: PlushieRainbowCarp
+        weight: 0.15
+    - !type:EntSelector
+      id: PlushieVox
+    - !type:EntSelector
+      id: PlushieRouny
+    - !type:GroupSelector
+      children:
+      - !type:EntSelector
+        id: PlushieSharkBlue
+      - !type:EntSelector
+        id: PlushieSharkGrey
+      - !type:EntSelector
+        id: PlushieSharkPink
+    - !type:EntSelector
+      id: PlushieAtmosian
+    - !type:EntSelector
+      id: PlushieDiona
+    - !type:EntSelector
+      id: PlushieXeno
+    - !type:EntSelector
+      id: PlushieHampter
+    - !type:EntSelector
+      id: PlushieMoth
+    - !type:EntSelector
+      id: PlushieArachind
+    - !type:EntSelector
+      id: PlushiePenguin
+
 - type: entity
   id: CrateFunPlushie
   parent: CrateGenericSteel
   name: plushie crate
   description: A buncha soft plushies. Throw them around and then wonder how you're gonna explain this purchase to NT.
   components:
-  - type: StorageFill
-    contents:
-      - id: PlushieBee
-      - id: PlushieNar
-      - id: PlushieCarp
-      - id: PlushieNuke
-      - id: PlushieSlime
-      - id: PlushieSnake
-      - id: PlushieLizard
-      - id: PlushieSpaceLizard
-      - id: PlushieVox
-      - id: PlushieRouny
-      - id: PlushieRatvar
-      - id: PlushieSharkBlue
-        orGroup: PlushieShark
-      - id: PlushieSharkGrey
-        orGroup: PlushieShark
-      - id: PlushieAtmosian
-      - id: PlushieDiona
-      - id: PlushieXeno
-      - id: PlushieHampter
-      - id: PlushieMoth
-      - id: PlushieArachind
-      - id: PlushiePenguin
+  - type: EntityTableContainerFill
+    containers:
+      entity_storage: !type:NestedSelector
+        tableId: AllPlushiesTable
+        rolls: !type:ConstantNumberSelector
+          value: 10
 
 - type: entity
   id: CrateFunLizardPlushieBulk
   name: bulk lizard plushie crate
   description: A buncha soft lizard plushies. Throw them around and then wonder how you're gonna explain this purchase to NT.
   components:
-  - type: StorageFill
-    contents:
-      - id: PlushieLizard
-        amount: 3
-      - id: PlushieSpaceLizard
-        amount: 2
+  - type: EntityTableContainerFill
+    containers:
+      entity_storage: !type:AllSelector
+        children:
+        - !type:EntSelector
+          id: PlushieLizard
+          amount: !type:ConstantNumberSelector
+            value: 3
+        - !type:EntSelector
+          id: PlushieSpaceLizard
+          amount: !type:ConstantNumberSelector
+            value: 3
 
 - type: entity
   id: CrateFunInstrumentsVariety
index a5b06fca03c0e6794db0c3c949eddbe25c2d6b5d..cf270a631065cbbb5dbd1ed9df9925b776ec89c4 100644 (file)
   suffix: Filled
   parent: LockerSyndicatePersonal
   components:
-  - type: StorageFill
-    contents:
-      - id: ClothingBeltMilitaryWebbing
-      - id: ClothingHandsGlovesCombat
-      - id: JetpackBlackFilled
-      - id: ClothingUniformJumpsuitOperative
-      - id: ClothingUniformJumpskirtOperative
-      - id: ClothingHeadsetAltSyndicate
-      - id: ClothingEyesHudSyndicate
+  - type: EntityTableContainerFill
+    containers:
+      entity_storage: !type:AllSelector
+        children:
+        - !type:EntSelector
+          id: ClothingBeltMilitaryWebbing
+        - !type:EntSelector
+          id: ClothingHandsGlovesCombat
+        - !type:EntSelector
+          id: JetpackBlackFilled
+        - !type:EntSelector
+          id: ClothingUniformJumpsuitOperative
+        - !type:EntSelector
+          id: ClothingUniformJumpskirtOperative
+        - !type:EntSelector
+          id: ClothingHeadsetAltSyndicate
+        - !type:EntSelector
+          id: ClothingEyesHudSyndicate
+
+- type: entityTable
+  id: FillLockerEmergencyStandard
+  table: !type:AllSelector
+    children:
+    - !type:EntSelector
+      id: ClothingMaskBreath
+    - !type:EntSelector
+      id: ClothingOuterSuitEmergency
+    - !type:GroupSelector
+      children:
+      - !type:EntSelector
+        id: EmergencyOxygenTankFilled
+      - !type:EntSelector
+        id: OxygenTankFilled
+    - !type:EntSelector
+      id: ToolboxEmergencyFilled
+      prob: 0.5
+    - !type:EntSelector
+      id: MedkitOxygenFilled
+      prob: 0.2
+    - !type:EntSelector
+      id: WeaponFlareGun
+      prob: 0.05
+    - !type:EntSelector
+      id: BoxMRE
+      prob: 0.1
 
 - type: entity
   id: ClosetEmergencyFilledRandom
   parent: ClosetEmergency
   suffix: Filled, Random
   components:
-  - type: StorageFill
-    contents:
-      - id: ClothingMaskBreath
-      - id: ClothingOuterSuitEmergency
-      - id: EmergencyOxygenTankFilled
-        prob: 0.5
-        orGroup: OxygenTank
-      - id: OxygenTankFilled
-        prob: 0.5
-        orGroup: OxygenTank
-      - id: ToolboxEmergencyFilled
-        prob: 0.5
-      - id: MedkitOxygenFilled
-        prob: 0.2
-      - id: WeaponFlareGun
-        prob: 0.05
-      - id: BoxMRE
-        prob: 0.1
+  - type: EntityTableContainerFill
+    containers:
+      entity_storage: !type:NestedSelector
+        tableId: FillLockerEmergencyStandard
 
 - type: entity
   id: ClosetWallEmergencyFilledRandom
   parent: ClosetWallEmergency
   suffix: Filled, Random
   components:
-  - type: StorageFill
-    contents:
-      - id: ClothingMaskBreath
-      - id: ClothingOuterSuitEmergency
-      - id: EmergencyOxygenTankFilled
-        prob: 0.5
-        orGroup: OxygenTank
-      - id: OxygenTankFilled
-        prob: 0.5
-        orGroup: OxygenTank
-      - id: ToolboxEmergencyFilled
-        prob: 0.5
-      - id: MedkitOxygenFilled
-        prob: 0.2
-      - id: WeaponFlareGun
-        prob: 0.05
-      - id: BoxMRE
-        prob: 0.1
+  - type: EntityTableContainerFill
+    containers:
+      entity_storage: !type:NestedSelector
+        tableId: FillLockerEmergencyStandard
 
 - type: entity
   id: ClosetEmergencyN2FilledRandom
   parent: ClosetEmergencyN2
   suffix: Filled, Random
   components:
-  - type: StorageFill
-    contents:
-      - id: ClothingMaskBreath
-      - id: ClothingOuterSuitEmergency
-      - id: EmergencyNitrogenTankFilled
-        prob: 0.5
-        orGroup: NitrogenTank
-      - id: NitrogenTankFilled
-        prob: 0.5
-        orGroup: NitrogenTank
+  - type: EntityTableContainerFill
+    containers:
+      entity_storage: !type:AllSelector
+        children:
+        - !type:EntSelector
+          id: ClothingMaskBreath
+        - !type:EntSelector
+          id: ClothingOuterSuitEmergency
+        - !type:GroupSelector
+          children:
+          - !type:EntSelector
+            id: EmergencyNitrogenTankFilled
+          - !type:EntSelector
+            id: NitrogenTankFilled
+
+- type: entityTable
+  id: FillLockerFireStandard
+  table: !type:AllSelector
+    children:
+    - !type:EntSelector
+      id: ClothingOuterSuitFire
+    - !type:EntSelector
+      id: ClothingHeadHelmetFire
+    - !type:EntSelector
+      id: ClothingMaskGas
+    - !type:GroupSelector
+      children:
+      - !type:EntSelector
+        id: EmergencyOxygenTankFilled
+      - !type:EntSelector
+        id: OxygenTankFilled
+    - !type:EntSelector
+      id: CrowbarRed
+    - !type:GroupSelector
+      children:
+      - !type:EntSelector
+        id: FireExtinguisher
+        weight: 98
+      - !type:EntSelector
+        id: SprayBottleWater #It's just budget cut after budget cut man
+        weight: 2
 
 - type: entity
   id: ClosetFireFilled
   parent: ClosetFire
   suffix: Filled
   components:
-  - type: StorageFill
-    contents:
-      - id: ClothingOuterSuitFire
-      - id: ClothingHeadHelmetFire
-      - id: ClothingMaskGas
-      - id: EmergencyOxygenTankFilled
-        prob: 0.5
-        orGroup: OxygenTank
-      - id: OxygenTankFilled
-        prob: 0.5
-        orGroup: OxygenTank
-      - id: CrowbarRed
-      - id: FireExtinguisher
-        prob: 0.98
-        orGroup: FireExtinguisher
-      - id: SprayBottleWater #It's just budget cut after budget cut man
-        prob: 0.02
-        orGroup: FireExtinguisher
+  - type: EntityTableContainerFill
+    containers:
+      entity_storage: !type:NestedSelector
+        tableId: FillLockerFireStandard
 
 - type: entity
   id: ClosetWallFireFilledRandom
   parent: ClosetWallFire
   suffix: Filled
   components:
-  - type: StorageFill
-    contents:
-      - id: ClothingOuterSuitFire
-      - id: ClothingHeadHelmetFire
-      - id: ClothingMaskGas
-      - id: EmergencyOxygenTankFilled
-        prob: 0.5
-        orGroup: OxygenTank
-      - id: OxygenTankFilled
-        prob: 0.5
-        orGroup: OxygenTank
-      - id: CrowbarRed
-      - id: FireExtinguisher
-        prob: 0.98
-        orGroup: FireExtinguisher
-      - id: SprayBottleWater #It's just budget cut after budget cut man
-        prob: 0.02
-        orGroup: FireExtinguisher
+  - type: EntityTableContainerFill
+    containers:
+      entity_storage: !type:NestedSelector
+        tableId: FillLockerFireStandard
+
+- type: entityTable
+  id: SyndieMaintLoot
+  table: !type:GroupSelector
+    children:
+    - !type:GroupSelector
+      children:
+      - !type:EntSelector
+        id: ClothingUniformJumpsuitOperative
+      - !type:EntSelector
+        id: ClothingUniformJumpskirtOperative
+    - !type:EntSelector
+      id: ClothingBackpackDuffelSyndicate
+    - !type:EntSelector
+      id: CyberPen
+    - !type:EntSelector
+      id: CigPackSyndicate
+    - !type:EntSelector
+      id: ClothingBackpackDuffelSyndicatePyjamaBundle
+    - !type:EntSelector
+      id: ClothingBeltMilitaryWebbing
+    - !type:EntSelector
+      id: ClothingShoesBootsCombatFilled
+    - !type:EntSelector
+      id: ToolboxSyndicateFilled
+    - !type:EntSelector
+      id: BalloonSyn
+    - !type:EntSelector
+      id: WeaponSniperMosin
+      weight: 2
+
+- type: entityTable
+  id: MaintenanceLockerLoot
+  table: !type:AllSelector
+    children:
+    - !type:EntSelector
+      id: StrangePill
+      prob: 0.20
+    # Tools
+    - !type:NestedSelector
+      tableId: MaintToolsTable
+      rolls: !type:RangeNumberSelector
+        range: 1, 5
+    # Fluff
+    - !type:NestedSelector
+      tableId: MaintFluffTable
+      prob: 0.33
+      rolls: !type:RangeNumberSelector
+        range: 0, 2
+    # Plushies
+    - !type:NestedSelector
+      tableId: AllPlushiesTable
+      prob: 0.10
+      rolls: !type:RangeNumberSelector
+        range: 1, 2
+    # Weapons
+    - !type:NestedSelector
+      tableId: MaintWeaponTable
+      prob: 0.075
+    # Syndie Loot
+    - !type:NestedSelector
+      tableId: SyndieMaintLoot
+      prob: 0.05
 
 - type: entity
   id: ClosetMaintenanceFilledRandom
   suffix: Filled, Random
   parent: ClosetMaintenance
   components:
-    - type: StorageFill
-      contents:
-        - id: Lantern
-          prob: 0.50
-        - id: Wirecutter
-          prob: 0.33
-        - id: Screwdriver
-          prob: 0.33
-        - id: Wrench
-          prob: 0.33
-        - id: Crowbar
-          prob: 0.50
-        - id: Welder
-          prob: 0.33
-        - id: Multitool
-          prob: 0.10
-        - id: Soap
-          prob: 0.44
-        - id: null
-          prob: 0.67
-          orGroup: carp
-        - id: PlushieCarp
-          prob: 0.2
-          orGroup: carp
-        - id: PlushieHolocarp
-          prob: 0.05
-          orGroup: carp
-        - id: PlushieMagicarp
-          prob: 0.05
-          orGroup: carp
-        - id: PlushieRainbowCarp
-          prob: 0.03
-          orGroup: carp
-        - id: PlushieSlime
-          prob: 0.2
-        - id: PlushieSnake
-          prob: 0.2
-        - id: ClothingShoesSkates
-          prob: 0.1
-        - id: ClothingHandsGlovesColorYellow
-          prob: 0.05
-        - id: ClothingHandsGlovesFingerlessInsulated
-          prob: 0.07
-        - id: ClothingBeltUtility
-          prob: 0.10
-        - id: ClothingHeadHatCone
-          prob: 0.2
-        - id: WeaponFlareGun
-          prob: 0.1
-        - id: ClothingHandsGlovesColorYellowBudget
-          prob: 0.25
-        - id: StrangePill
-          prob: 0.20
-        - id: DrinkMopwataBottleRandom
-          prob: 0.20
-        - id: ModularReceiver
-          prob: 0.1
-        - id: DrinkSpaceGlue
-          prob: 0.20
-        - id: DrinkSpaceLube
-          prob: 0.20
-        - id: BarberScissors
-          prob: 0.05
-        - id: Wristwatch
-          prob: 0.05
-        - id: BookRandomStory
-          prob: 0.1
-        # Syndicate loot
-        - id: null
-          prob: 0.95
-          orGroup: syndiemaintloot
-        - id: ClothingUniformJumpskirtOperative
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: ClothingUniformJumpsuitOperative
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: ClothingBackpackDuffelSyndicate
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: CyberPen
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: CigPackSyndicate
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: ClothingBackpackDuffelSyndicatePyjamaBundle
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: ClothingBeltMilitaryWebbing
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: ClothingShoesBootsCombatFilled
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: ToolboxSyndicateFilled
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: BalloonSyn
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: WeaponSniperMosin
-          prob: 0.0010
-          orGroup: syndiemaintloot
+  - type: EntityTableContainerFill
+    containers:
+      entity_storage: !type:NestedSelector
+        tableId: MaintenanceLockerLoot
 
 - type: entity
   id: ClosetWallMaintenanceFilledRandom
   parent: ClosetWall
   suffix: Filled, Random
   components:
-  - type: StorageFill
-    contents:
-        - id: Lantern
-          prob: 0.50
-        - id: Wirecutter
-          prob: 0.33
-        - id: Screwdriver
-          prob: 0.33
-        - id: Wrench
-          prob: 0.33
-        - id: Crowbar
-          prob: 0.50
-        - id: Welder
-          prob: 0.33
-        - id: Multitool
-          prob: 0.10
-        - id: Soap
-          prob: 0.44
-        - id: null
-          prob: 0.67
-          orGroup: carp
-        - id: PlushieCarp
-          prob: 0.2
-          orGroup: carp
-        - id: PlushieHolocarp
-          prob: 0.05
-          orGroup: carp
-        - id: PlushieMagicarp
-          prob: 0.05
-          orGroup: carp
-        - id: PlushieRainbowCarp
-          prob: 0.03
-          orGroup: carp
-        - id: PlushieSlime
-          prob: 0.2
-        - id: PlushieSnake
-          prob: 0.2
-        - id: ClothingHandsGlovesColorYellow
-          prob: 0.05
-        - id: ClothingBeltQuiver
-          prob: 0.02
-        - id: ClothingBeltUtility
-          prob: 0.10
-        - id: ClothingHeadHatCone
-          prob: 0.2
-        - id: WeaponFlareGun
-          prob: 0.1
-        - id: ClothingHandsGlovesColorYellowBudget
-          prob: 0.25
-        - id: StrangePill
-          prob: 0.20
-        - id: DrinkSpaceGlue
-          prob: 0.20
-        - id: ModularReceiver
-          prob: 0.1
-        # Syndicate loot
-        - id: null
-          prob: 0.95
-          orGroup: syndiemaintloot
-        - id: ClothingUniformJumpskirtOperative
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: ClothingUniformJumpsuitOperative
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: ClothingBackpackDuffelSyndicate
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: CyberPen
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: ClothingHeadHatOutlawHat
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: ClothingEyesGlassesOutlawGlasses
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: CigPackSyndicate
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: ClothingBackpackDuffelSyndicatePyjamaBundle
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: ClothingBeltMilitaryWebbing
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: ClothingShoesBootsCombatFilled
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: ToolboxSyndicateFilled
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: BalloonSyn
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: WeaponSniperMosin
-          prob: 0.0010
-          orGroup: syndiemaintloot
+  - type: EntityTableContainerFill
+    containers:
+      entity_storage: !type:NestedSelector
+        tableId: MaintenanceLockerLoot
index 00798b36e0f559eedd2cad6bb2c7e4930cdd4095..193edb069e7752eda30a9ec6bcaff8ed43674e3d 100644 (file)
+- type: entityTable
+  id: MaintFluffTable
+  table: !type:GroupSelector
+    children:
+    # Common Group
+    - !type:GroupSelector
+      weight: 75
+      children:
+      # Smoker's specialty
+      - !type:AllSelector
+        children:
+        - !type:EntSelector
+          id: Lighter
+        - !type:EntSelector
+          id: CigCartonBlue
+      # Gar glasses
+      - !type:GroupSelector
+        children:
+        - !type:EntSelector
+          id: ClothingEyesGlassesGar
+        - !type:EntSelector
+          id: ClothingEyesGlassesGarOrange
+        - !type:EntSelector
+          id: ClothingEyesGlassesGarGiga
+      - !type:EntSelector
+        id: Wristwatch
+        weight: 0.5
+      - !type:EntSelector
+        id: ClothingHeadHatCake
+      - !type:EntSelector
+        id: ClothingHeadHatSkub
+      - !type:EntSelector
+        id: ClothingHeadHatCone
+      - !type:EntSelector
+        id: ClothingNeckBling
+      - !type:EntSelector
+        id: ClothingHeadHelmetCosmonaut
+      - !type:EntSelector
+        id: ClothingHeadHelmetBasic
+      - !type:EntSelector
+        id: ClothingShoeSlippersDuck
+      - !type:EntSelector
+        id: ClothingUnderSocksBee
+      - !type:EntSelector
+        id: ClothingUnderSocksCoder
+      - !type:EntSelector
+        id: ClothingHeadHatSquid
+      # Animal Masks
+      - !type:GroupSelector
+        children:
+        - !type:EntSelector
+          id: ClothingMaskRat
+        - !type:EntSelector
+          id: ClothingMaskFox
+        - !type:EntSelector
+          id: ClothingMaskBee
+        - !type:EntSelector
+          id: ClothingMaskBear
+        - !type:EntSelector
+          id: ClothingMaskRaven
+        - !type:EntSelector
+          id: ClothingMaskJackal
+        - !type:EntSelector
+          id: ClothingMaskBat
+      - !type:EntSelector
+        id: ClothingBeltSuspenders
+      - !type:EntSelector
+        id: ClothingEyesEyepatch
+      - !type:EntSelector
+        id: ClothingEyesGlasses
+      - !type:EntSelector
+        id: ClothingHandsGlovesLatex
+      - !type:EntSelector
+        id: ClothingHandsGlovesFingerless
+      - !type:EntSelector
+        id: ClothingHandsGlovesColorBlack
+      - !type:EntSelector
+        id: ClothingHeadHatBeret
+      - !type:EntSelector
+        id: ClothingHeadHatBowlerHat
+      - !type:EntSelector
+        id: ClothingHeadHatFedoraBrown
+        weight: 0.5
+      - !type:EntSelector
+        id: ClothingHeadHatFedoraGrey
+        weight: 0.5
+      - !type:EntSelector
+        id: ClothingHeadHatFez
+      - !type:EntSelector
+        id: ClothingHeadHatPaper
+      - !type:EntSelector
+        id: ClothingHeadHatPirate
+      - !type:EntSelector
+        id: ClothingMaskSterile
+      - !type:EntSelector
+        id: ClothingNeckHeadphones
+      - !type:EntSelector
+        id: ClothingNeckTieRed
+      - !type:EntSelector
+        id: ClothingOuterCoatGentle
+      - !type:AllSelector
+        children:
+        - !type:EntSelector
+          id: ClothingOuterCoatJensen
+        - !type:EntSelector
+          id: ClothingEyesGlassesJensen
+      - !type:EntSelector
+        id: ClothingOuterCoatLab
+      - !type:AllSelector
+        children:
+        - !type:EntSelector
+          id: ClothingOuterCoatPirate
+        - !type:EntSelector
+          id: ClothingHeadHatPirateTricord
+      - !type:EntSelector
+        id: ClothingHeadHatTophat
+      - !type:EntSelector
+        id: ClothingOuterHoodieBlack
+        weight: 0.5
+      - !type:EntSelector
+        id: ClothingOuterHoodieGrey
+        weight: 0.5
+      - !type:GroupSelector
+        children:
+        - !type:EntSelector
+          id: ClothingOuterFlannelRed
+        - !type:EntSelector
+          id: ClothingOuterFlannelBlue
+        - !type:EntSelector
+          id: ClothingOuterFlannelGreen
+      - !type:EntSelector
+        id: ClothingOuterVestHazard
+      - !type:EntSelector
+        id: ClothingShoesBootsJack
+      - !type:EntSelector
+        id: ClothingShoesHighheelBoots
+      - !type:EntSelector
+        id: ClothingShoesBootsLaceup
+      - !type:EntSelector
+        id: ClothingShoesLeather
+      - !type:EntSelector
+        id: ClothingShoesBootsSalvage
+      - !type:EntSelector
+        id: ClothingShoesBootsWork
+      - !type:EntSelector
+        id: ClothingShoesTourist
+      - !type:EntSelector
+        id: ClothingUniformJumpsuitLoungewear
+      - !type:EntSelector
+        id: ClothingHeadHatCowboyRed
+    # Uncommon Group
+    - !type:GroupSelector
+      weight: 23
+      children:
+      - !type:EntSelector
+        id: ClothingNeckCloakHerald
+      - !type:EntSelector
+        id: ClothingHeadHelmetTemplar
+      # Cloaks
+      - !type:GroupSelector
+        children:
+        - !type:EntSelector
+          id: ClothingNeckCloakTrans
+        - !type:EntSelector
+          id: ClothingNeckCloakAdmin
+        - !type:EntSelector
+          id: ClothingNeckCloakMoth
+        - !type:EntSelector
+          id: ClothingNeckCloakVoid
+        - !type:EntSelector
+          id: ClothingNeckCloakGoliathCloak
+        - !type:EntSelector
+          id: ClothingNeckCloakAce
+        - !type:EntSelector
+          id: ClothingNeckCloakAro
+        - !type:EntSelector
+          id: ClothingNeckCloakBi
+        - !type:EntSelector
+          id: ClothingNeckCloakIntersex
+        - !type:EntSelector
+          id: ClothingNeckCloakLesbian
+        - !type:EntSelector
+          id: ClothingNeckCloakGay
+        - !type:EntSelector
+          id: ClothingNeckCloakEnby
+        - !type:EntSelector
+          id: ClothingNeckCloakPan
+      - !type:EntSelector
+        id: ToySkeleton
+      - !type:EntSelector
+        id: Basketball
+      - !type:EntSelector
+        id: Football
+      - !type:EntSelector
+        id: BalloonNT
+      - !type:EntSelector
+        id: BalloonCorgi
+      - !type:EntSelector
+        id: MysteryFigureBox
+      # Cult
+      - !type:AllSelector
+        children:
+        - !type:EntSelector
+          id: ClothingOuterRobesCult
+        - !type:EntSelector
+          id: ClothingShoesCult
+      - !type:EntSelector
+        id: ClothingHandsGlovesMercFingerless
+      - !type:EntSelector
+        id: ClothingHandsGlovesNitrile
+      - !type:EntSelector
+        id: ClothingHandsGlovesPowerglove
+      - !type:EntSelector
+        id: ClothingHeadHatAnimalHeadslime
+      - !type:EntSelector
+        id: ClothingHeadHatBeretMerc
+      - !type:EntSelector
+        id: ClothingHeadHatOutlawHat
+      - !type:EntSelector
+        id: ClothingHeadHatUshanka
+      - !type:EntSelector
+        id: ClothingHeadHatBunny
+      - !type:EntSelector
+        id: ClothingMaskNeckGaiter
+      - !type:EntSelector
+        id: ClothingNeckScarfStripedZebra
+      - !type:EntSelector
+        id: ClothingOuterGhostSheet
+      - !type:EntSelector
+        id: ClothingUniformJumpsuitAncient
+      - !type:EntSelector
+        id: ClothingUniformJumpsuitPirate
+      - !type:EntSelector
+        id: ClothingShoesBootsCowboyFancy
+      - !type:EntSelector
+        id: ClothingHeadHatCowboyBountyHunter
+      # Pins
+      - !type:GroupSelector
+        children:
+        - !type:EntSelector
+          id: ClothingNeckLGBTPin
+        - !type:EntSelector
+          id: ClothingNeckAromanticPin
+        - !type:EntSelector
+          id: ClothingNeckAsexualPin
+        - !type:EntSelector
+          id: ClothingNeckBisexualPin
+        - !type:EntSelector
+          id: ClothingNeckIntersexPin
+        - !type:EntSelector
+          id: ClothingNeckLesbianPin
+        - !type:EntSelector
+          id: ClothingNeckNonBinaryPin
+        - !type:EntSelector
+          id: ClothingNeckPansexualPin
+        - !type:EntSelector
+          id: ClothingNeckTransPin
+        - !type:EntSelector
+          id: ClothingNeckAutismPin
+        - !type:EntSelector
+          id: ClothingNeckGoldAutismPin
+    # Rare Group
+    - !type:GroupSelector
+      weight: 2
+      children:
+      - !type:EntSelector
+        id: Skub
+      - !type:EntSelector
+        id: PonderingOrb
+      - !type:EntSelector
+        id: CluwneHorn
+      - !type:EntSelector
+        id: ClothingShoesSkates
+      - !type:EntSelector
+        id: DrinkMugDog
+      - !type:EntSelector
+        id: CigarGold
+      - !type:EntSelector
+        id: ClothingUniformJumpsuitFamilyGuy
+      - !type:EntSelector
+        id: WristwatchGold
+
 - type: entity
   name: Maint Loot Spawner
   suffix: Fluff+Clothes
   id: MaintenanceFluffSpawner
   parent: MarkerBase
   components:
-    - type: Sprite
-      layers:
-        - state: red
-        - sprite: Clothing/Eyes/Glasses/gar.rsi
-          state: icon-super
-    - type: RandomSpawner
-      rarePrototypes:
-        - ClothingUniformJumpsuitFamilyGuy
-        - CigarGold
-        - ClothingNeckCloakHerald
-        - ClothingHeadHelmetTemplar
-        - ClothingNeckCloakTrans
-        - ClothingNeckCloakAdmin
-        - ClothingNeckCloakMoth
-        - ClothingNeckCloakVoid
-        - ClothingNeckCloakGoliathCloak
-        - ClothingNeckCloakAce
-        - ClothingNeckCloakAro
-        - ClothingNeckCloakBi
-        - ClothingNeckCloakIntersex
-        - ClothingNeckCloakLesbian
-        - ClothingNeckCloakGay
-        - ClothingNeckCloakEnby
-        - ClothingNeckCloakPan
-        - ToySkeleton
-        - Basketball
-        - Football
-        - BalloonCorgi
-        - BalloonNT
-        - PonderingOrb
-        - Skub
-        - DrinkMugDog
-        - ClothingNeckLGBTPin
-        - ClothingNeckAromanticPin
-        - ClothingNeckAsexualPin
-        - ClothingNeckBisexualPin
-        - ClothingNeckIntersexPin
-        - ClothingNeckLesbianPin
-        - ClothingNeckNonBinaryPin
-        - ClothingNeckPansexualPin
-        - ClothingNeckTransPin
-        - CluwneHorn
-        - ClothingMaskRat
-        - MysteryFigureBox
-        - ClothingHandsGlovesMercFingerless
-        - ClothingHandsGlovesNitrile
-        - ClothingHandsGlovesPowerglove
-        - ClothingHeadHatAnimalHeadslime
-        - ClothingHeadHatBeretMerc
-        - ClothingHeadHatOutlawHat
-        - ClothingHeadHatUshanka
-        - ClothingHeadHatBunny
-        - ClothingMaskNeckGaiter
-        - ClothingNeckScarfStripedZebra
-        - ClothingOuterRobesCult
-        - ClothingOuterGhostSheet
-        - ClothingShoesCult
-        - ClothingUniformJumpsuitAncient
-        - ClothingUniformJumpsuitPirate
-        - ClothingShoesBootsCowboyFancy
-        - ClothingHeadHatCowboyBountyHunter
-        - ClothingNeckAutismPin
-        - ClothingNeckGoldAutismPin
-        - WristwatchGold
-      rareChance: 0.01
-      prototypes:
-        - Lighter
-        - CigCartonBlue
-        - ClothingEyesGlassesGarGiga
-        - ClothingEyesGlassesGarOrange
-        - ClothingEyesGlassesGar
-        - ClothingHeadHatCake
-        - ClothingHeadHatSkub
-        - ClothingHeadHatCone
-        - ClothingNeckBling
-        - ClothingHeadHelmetCosmonaut
-        - ClothingHeadHelmetBasic
-        - ClothingShoeSlippersDuck
-        - ClothingUnderSocksBee
-        - ClothingUnderSocksCoder
-        - ClothingHeadHatSquid
-        - ClothingMaskFox
-        - ClothingMaskBee
-        - ClothingMaskBear
-        - ClothingMaskRaven
-        - ClothingMaskJackal
-        - ClothingMaskBat
-        - ClothingBeltSuspenders
-        - ClothingEyesEyepatch
-        - ClothingEyesGlasses
-        - ClothingHandsGlovesLatex
-        - ClothingHandsGlovesFingerless
-        - ClothingHandsGlovesColorBlack
-        - ClothingHeadHatBeret
-        - ClothingHeadHatBowlerHat
-        - ClothingHeadHatFedoraBrown
-        - ClothingHeadHatFedoraGrey
-        - ClothingHeadHatFez
-        - ClothingHeadHatPaper
-        - ClothingHeadHatPirate
-        - ClothingHeadHatPirateTricord
-        - ClothingHeadHatTophat
-        - ClothingMaskSterile
-        - ClothingNeckHeadphones
-        - ClothingNeckTieRed
-        - ClothingOuterCoatGentle
-        - ClothingOuterCoatJensen
-        - ClothingEyesGlassesJensen
-        - ClothingOuterCoatLab
-        - ClothingOuterCoatPirate
-        - ClothingOuterHoodieBlack
-        - ClothingOuterHoodieGrey
-        - ClothingOuterFlannelRed
-        - ClothingOuterFlannelBlue
-        - ClothingOuterFlannelGreen
-        - ClothingOuterVestHazard
-        - ClothingShoesBootsJack
-        - ClothingShoesHighheelBoots
-        - ClothingShoesBootsLaceup
-        - ClothingShoesLeather
-        - ClothingShoesBootsSalvage
-        - ClothingShoesBootsWork
-        - ClothingShoesTourist
-        - ClothingUniformJumpsuitLoungewear
-        - ClothingHeadHatCowboyRed
-        - Wristwatch
-      chance: 0.6
-      offset: 0.0
+  - type: Sprite
+    layers:
+      - state: red
+      - sprite: Clothing/Eyes/Glasses/gar.rsi
+        state: icon-super
+  - type: EntityTableSpawner
+    table: !type:NestedSelector
+      tableId: MaintFluffTable
+      prob: 0.6
 
+- type: entityTable
+  id: MaintToolsTable
+  table: !type:GroupSelector
+    children:
+    # Common Group
+    - !type:GroupSelector
+      weight: 75
+      children:
+      - !type:EntSelector
+        id: FlashlightLantern
+      - !type:EntSelector
+        id: ToolboxEmergencyFilled
+      - !type:GroupSelector
+        children:
+        - !type:EntSelector
+          id: OxygenTankFilled
+        - !type:EntSelector
+          id: DoubleEmergencyOxygenTankFilled
+      - !type:GroupSelector
+        children:
+        - !type:EntSelector
+          id: NitrogenTankFilled
+        - !type:EntSelector
+          id: DoubleEmergencyNitrogenTankFilled
+      - !type:EntSelector
+        id: EmergencyFunnyOxygenTankFilled
+        weight: 0.5
+      - !type:GroupSelector
+        weight: 3
+        children:
+        - !type:EntSelector
+          id: SheetSteel10
+        - !type:EntSelector
+          id: SheetPlastic10
+        - !type:EntSelector
+          id: SheetGlass10
+        - !type:EntSelector
+          id: PartRodMetal10
+        - !type:EntSelector
+          id: MaterialCardboard10
+          weight: 0.25
+        - !type:EntSelector
+          id: MaterialCloth10
+          weight: 0.25
+        - !type:EntSelector
+          id: MaterialWoodPlank10
+          weight: 0.25
+      - !type:EntSelector
+        id: Plunger
+      - !type:EntSelector
+        id: PowerCellMedium
+      - !type:EntSelector
+        id: PowerCellSmall
+      - !type:EntSelector
+        id: Soap
+      - !type:EntSelector
+        id: Wirecutter
+      - !type:EntSelector
+        id: Screwdriver
+      - !type:EntSelector
+        id: Wrench
+      - !type:EntSelector
+        id: Crowbar
+      - !type:EntSelector
+        id: Multitool
+      - !type:EntSelector
+        id: Shovel
+      - !type:EntSelector
+        id: Welder
+      - !type:EntSelector
+        id: GasAnalyzer
+      - !type:EntSelector
+        id: SprayPainter
+      - !type:EntSelector
+        id: Flare
+      - !type:EntSelector
+        id: Beaker
+      - !type:EntSelector
+        id: ClothingMaskGas
+      - !type:EntSelector
+        id: ClothingMaskBreath
+      - !type:EntSelector
+        id: DoorElectronics
+      - !type:EntSelector
+        id: APCElectronics
+      - !type:EntSelector
+        id: InflatableWallStack5
+      - !type:EntSelector
+        id: CableHVStack10
+      - !type:EntSelector
+        id: CableMVStack10
+      - !type:EntSelector
+        id: CableApcStack10
+      - !type:GroupSelector
+        children:
+        - !type:EntSelector
+          id: ClothingHandsGlovesColorYellowBudget
+          weight: 5
+        - !type:EntSelector
+          id: ClothingHandsGlovesFingerlessInsulated
+          weight: 0.5
+        - !type:EntSelector
+          id: ClothingHandsGlovesColorYellow
+          weight: 1
+      # Uncommon Group
+    - !type:GroupSelector
+      weight: 23
+      children:
+      - !type:EntSelector
+        id: ClothingHeadHatCone
+        weight: 2
+      - !type:EntSelector
+        id: BookRandomStory
+        weight: 0.25
+      - !type:EntSelector
+        id: ToolboxElectricalFilled
+      - !type:EntSelector
+        id: ToolboxMechanicalFilled
+      - !type:EntSelector
+        id: ClothingBeltUtility
+      - !type:EntSelector
+        id: ToolboxArtisticFilled
+      - !type:EntSelector
+        id: GeigerCounter
+      - !type:EntSelector
+        id: trayScanner
+      - !type:EntSelector
+        id: HandheldGPSBasic
+      - !type:EntSelector
+        id: HandLabeler
+      - !type:EntSelector
+        id: GlowstickBase
+      - !type:EntSelector
+        id: Bucket
+      - !type:EntSelector
+        id: RadioHandheld
+      - !type:EntSelector
+        id: AppraisalTool
+      - !type:EntSelector
+        id: ModularReceiver
+      - !type:EntSelector
+        id: WeaponFlareGun
+      - !type:EntSelector
+        id: BarberScissors
+      - !type:GroupSelector
+        children:
+        - !type:EntSelector
+          id: DrinkSpaceGlue
+        - !type:EntSelector
+          id: DrinkSpaceLube
+      # Rare Group
+    - !type:GroupSelector
+      weight: 2
+      children:
+      - !type:EntSelector
+        id: LanternFlash
+      - !type:EntSelector
+        id: PowerCellHigh
+      - !type:EntSelector
+        id: NetProbeCartridge
+      - !type:EntSelector
+        id: WelderIndustrial
+      - !type:EntSelector
+        id: SheetPlasteel10
+      - !type:EntSelector
+        id: ClothingMaskGasExplorer
+      - !type:EntSelector
+        id: TechnologyDisk
+      - !type:EntSelector
+        id: ResearchDisk5000
+      - !type:EntSelector
+        id: PetCarrier
+      - !type:EntSelector
+        id: DrinkMopwataBottleRandom
+      - !type:EntSelector
+        id: LidSalami
+        weight: 0.05
 
 - type: entity
   name: Maint Loot Spawner
   id: MaintenanceToolSpawner
   parent: MarkerBase
   components:
-    - type: Sprite
-      layers:
-        - state: red
-        - sprite: Objects/Power/power_cells.rsi
-          state: high
-    - type: RandomSpawner
-      rarePrototypes:
-        - LanternFlash
-        - PowerCellHigh
-        - NetProbeCartridge
-        - WelderIndustrial
-        - SheetPlasteel10
-        - ClothingMaskGasExplorer
-      rareChance: 0.08
-      prototypes:
-        - FlashlightLantern
-        - OxygenTankFilled
-        - DoubleEmergencyOxygenTankFilled
-        - ToolboxEmergencyFilled
-        - ToolboxArtisticFilled
-        - NitrogenTankFilled
-        - DoubleEmergencyNitrogenTankFilled
-        - EmergencyFunnyOxygenTankFilled
-        - ToolboxElectricalFilled
-        - ToolboxMechanicalFilled
-        - ClothingBeltUtility
-        - Shovel
-        - Welder
-        - WeaponFlareGun
-        - SheetSteel10
-        - SheetPlastic10
-        - SheetGlass10
-        - PartRodMetal10
-        - MaterialCardboard10
-        - MaterialCloth10
-        - MaterialWoodPlank10
-        - ResearchDisk
-        - Plunger
-        - TechnologyDisk
-        - PowerCellMedium
-        - PowerCellSmall
-        - Wirecutter
-        - Screwdriver
-        - Wrench
-        - Crowbar
-        - NetworkConfigurator
-        - trayScanner
-        - GasAnalyzer
-        - SprayPainter
-        - AppraisalTool
-        - Flare
-        - HandheldGPSBasic
-        - HandLabeler
-        - GlowstickBase
-        - Bucket
-        - RadioHandheld
-        - GeigerCounter
-        - Beaker
-        - ClothingMaskGas
-        - ClothingMaskBreath
-        - DoorElectronics
-        - APCElectronics
-        - InflatableWallStack5
-        - CableHVStack10
-        - CableMVStack10
-        - CableApcStack10
-        - PetCarrier
-      chance: 0.6
-      offset: 0.0
+  - type: Sprite
+    layers:
+      - state: red
+      - sprite: Objects/Power/power_cells.rsi
+        state: high
+  - type: EntityTableSpawner
+    table: !type:NestedSelector
+      tableId: MaintToolsTable
+      prob: 0.6
+
+- type: entityTable
+  id: MaintWeaponTable
+  table: !type:GroupSelector
+    children:
+    # Common Group
+    - !type:GroupSelector
+      weight: 95
+      children:
+      - !type:EntSelector
+        id: Machete
+      - !type:EntSelector
+        id: BaseBallBat
+      - !type:EntSelector
+        id: CombatKnife
+      - !type:EntSelector
+        id: Spear
+      - !type:EntSelector
+        id: RifleStock
+      - !type:EntSelector
+        id: ModularReceiver
+      - !type:EntSelector
+        id: HydroponicsToolScythe
+    # Rare Group
+    - !type:GroupSelector
+      weight: 5
+      children:
+      - !type:EntSelector
+        id: Lighter
+      - !type:EntSelector
+        id: Matchbox
+      - !type:EntSelector
+        id: ClothingEyesBlindfold
+      - !type:EntSelector
+        id: ClothingMaskMuzzle
+      - !type:EntSelector
+        id: ClothingMaskGasSecurity
+      - !type:EntSelector
+        id: ShardGlass
+        weight: 2
+      - !type:EntSelector
+        id: Syringe
+      - !type:EntSelector
+        id: Mousetrap
+      - !type:GroupSelector
+        weight: 2
+        children:
+        - !type:EntSelector
+          id: Brutepack1
+        - !type:EntSelector
+          id: Ointment1
+        - !type:EntSelector
+          id: Gauze1
+      - !type:EntSelector
+        id: Bola
+      - !type:EntSelector
+        id: SurvivalKnife
+      - !type:EntSelector
+        id: ScalpelShiv
+      - !type:EntSelector
+        id: Shiv
+      - !type:EntSelector
+        id: SawImprov
+      - !type:EntSelector
+        id: HydroponicsToolMiniHoe
 
 - type: entity
   name: Maint Loot Spawner
   id: MaintenanceWeaponSpawner
   parent: MarkerBase
   components:
-    - type: Sprite
-      layers:
-        - state: red
-        - sprite: Objects/Weapons/Melee/machete.rsi
-          state: icon
-    - type: RandomSpawner
-      rarePrototypes:
-        - Machete
-        - BaseBallBat
-        - CombatKnife
-        - Spear
-        - RifleStock
-        - ModularReceiver
-        - HydroponicsToolScythe
-      rareChance: 0.05
-      prototypes:
-        - FlashlightLantern
-        - OxygenTankFilled
-        - DoubleEmergencyOxygenTankFilled
-        - NitrogenTankFilled
-        - DoubleEmergencyNitrogenTankFilled
-        - Lighter
-        - Matchbox
-        - Crowbar
-        - Shovel
-        - Welder
-        - WeaponFlareGun
-        - LidSalami
-        - ClothingEyesBlindfold
-        - ClothingMaskMuzzle
-        - ClothingMaskGasSecurity
-        - ShardGlass
-        - Syringe
-        - Mousetrap
-        - Brutepack1
-        - Ointment1
-        - Gauze1
-        - Bola
-        - SurvivalKnife
-        - ScalpelShiv
-        - Shiv
-        - SawImprov
-        - HydroponicsToolMiniHoe
-      chance: 0.6
-      offset: 0.0
+  - type: Sprite
+    layers:
+      - state: red
+      - sprite: Objects/Weapons/Melee/machete.rsi
+        state: icon
+  - type: EntityTableSpawner
+    table: !type:NestedSelector
+      tableId: MaintWeaponTable
+      prob: 0.6
 
 - type: entity
   name: Maint Loot Spawner