diff --git a/Content.Shared/EntityEffects/Effects/VariationPass/EntitySpawnVariationPassEntityEffect.cs b/Content.Shared/EntityEffects/Effects/VariationPass/EntitySpawnVariationPassEntityEffect.cs
new file mode 100644
index 0000000000..cdfad8e848
--- /dev/null
+++ b/Content.Shared/EntityEffects/Effects/VariationPass/EntitySpawnVariationPassEntityEffect.cs
@@ -0,0 +1,111 @@
+using System.Linq;
+using Content.Shared.EntityTable;
+using Content.Shared.EntityTable.EntitySelectors;
+using Content.Shared.Physics;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Physics;
+using Robust.Shared.Random;
+
+namespace Content.Shared.EntityEffects.Effects.VariationPass;
+
+///
+/// Used for spawning entities randomly dotted around the grid.
+///
+///
+public sealed partial class EntitySpawnVariationPassEntityEffectSystem : EntityEffectSystem
+{
+ [Dependency] private SharedMapSystem _map = default!;
+ [Dependency] private IRobustRandom _random = default!;
+ [Dependency] private EntityLookupSystem _lookup = default!;
+ [Dependency] private EntityTableSystem _tables = default!;
+
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ var tiles = _map.GetAllTiles(entity, entity).ToList();
+ var totalTiles = tiles.Count();
+
+ var dirtyMod = _random.NextGaussian(args.Effect.TilesPerEntityAverage, args.Effect.TilesPerEntityStdDev);
+ var trashTiles = Math.Max((int) (totalTiles * (1 / dirtyMod)), 0);
+
+ for (var i = 0; i < trashTiles; i++)
+ {
+ if (TryFindRandomTile(entity, tiles, out var coords))
+ {
+ var ents = _tables.GetSpawns(args.Effect.Table, _random);
+ foreach (var spawn in ents)
+ {
+ SpawnAtPosition(spawn, coords);
+ }
+ }
+ }
+ }
+
+ /// Attempts to find an empty tile 10 times, returns true if successful.
+ private bool TryFindRandomTile(Entity grid,
+ List tiles,
+ out EntityCoordinates targetCoords)
+ {
+ targetCoords = EntityCoordinates.Invalid;
+
+ for (var i = 0; i < 10; i++)
+ {
+ var tile = _random.Pick(tiles);
+ var tileCoords = tile.GridIndices;
+
+ if (CheckTileEntities(grid, tileCoords))
+ {
+ targetCoords = _map.GridTileToLocal(grid, grid.Comp, tileCoords);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /// Returns false if the tile is occupied.
+ private bool CheckTileEntities(Entity grid, Vector2i tileCoords)
+ {
+ var intersectingEntities = new HashSet();
+ _lookup.GetLocalEntitiesIntersecting(grid, tileCoords, intersectingEntities, -0.05f, LookupFlags.Uncontained);
+
+ foreach (var ent in intersectingEntities)
+ {
+ if (TryComp(ent, out var fixtures))
+ {
+ foreach (var fixture in fixtures.Fixtures.Values)
+ {
+ // Continue if no collision is possible
+ if (!fixture.Hard || fixture.CollisionLayer <= 0 || (fixture.CollisionLayer & (int) CollisionGroup.Impassable) == 0)
+ continue;
+
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+}
+
+///
+public sealed partial class EntitySpawnVariationPass : EntityEffectBase
+{
+ ///
+ /// Number of tiles before we spawn one entity on average.
+ ///
+ [DataField]
+ public float TilesPerEntityAverage = 50f;
+
+ ///
+ /// Standard deviation for the randomness selection.
+ ///
+ [DataField]
+ public float TilesPerEntityStdDev = 7f;
+
+ ///
+ /// Spawn table for each chosen location.
+ ///
+ [DataField(required: true)]
+ public EntityTableSelector Table = default!;
+}
diff --git a/Content.Shared/EntityEffects/EntityEffectOnMapInitComponent.cs b/Content.Shared/EntityEffects/EntityEffectOnMapInitComponent.cs
new file mode 100644
index 0000000000..64d51dfdb4
--- /dev/null
+++ b/Content.Shared/EntityEffects/EntityEffectOnMapInitComponent.cs
@@ -0,0 +1,15 @@
+namespace Content.Shared.EntityEffects;
+
+///
+/// Applies a set of EntityEffects to the entity upon map initialization.
+/// Useful for e.g. station spawning.
+///
+[RegisterComponent]
+public sealed partial class EntityEffectOnMapInitComponent : Component
+{
+ ///
+ /// Effects that should be applied upon the map being initiated.
+ ///
+ [DataField(required: true)]
+ public EntityEffect[] Effects { get; set; } = default!;
+}
diff --git a/Content.Shared/EntityEffects/SharedEntityEffectsSystem.cs b/Content.Shared/EntityEffects/SharedEntityEffectsSystem.cs
index a7c3fe1768..a1d862b7d3 100644
--- a/Content.Shared/EntityEffects/SharedEntityEffectsSystem.cs
+++ b/Content.Shared/EntityEffects/SharedEntityEffectsSystem.cs
@@ -22,6 +22,7 @@ public sealed partial class SharedEntityEffectsSystem : EntitySystem, IEntityEff
public override void Initialize()
{
SubscribeLocalEvent(OnReactive);
+ SubscribeLocalEvent(OnMapInit);
}
private void OnReactive(Entity entity, ref ReactionEntityEvent args)
@@ -58,6 +59,11 @@ public sealed partial class SharedEntityEffectsSystem : EntitySystem, IEntityEff
}
}
+ private void OnMapInit(Entity entity, ref MapInitEvent args)
+ {
+ ApplyEffects(entity, entity.Comp.Effects);
+ }
+
///
public void ApplyEffects(EntityUid target, EntityEffect[] effects, FixedPoint2 scale, EntityUid? user = null)
{