Spawner
Population spawners over server entities: keep N alive in an area, respawn on removal. Server-only.
Seams you implement 1
Crossplay calls these; your game supplies them. Each ships an inert or permissive default, so register yours before services.AddCrossplaySpawner(); and it wins.
ISpawnPlacementPolicy game SPI
The GAME's say over WHERE a spawn lands: the spawner offers each candidate point before it spawns anything, and the policy accepts it as-is, moves it (write the new coordinates back), or refuses it. This is how "a tree never stands in the lake" is expressed without spawning wet and moving after. How to implement. Register your policy in DI BEFORE AddCrossplaySpawner (it is a TryAdd seam). It runs on the poll thread, up to PlacementAttempts times per spawn, so keep it a cheap lookup (a baked mask, a height sample). It is asked for every spawner spawn and for paced-set spawns that pick from Points; a set with no points spawns at its authored X/Z without asking. The shipped default accepts every point unchanged, so installing the piece changes nothing.
public sealed class DryLandPlacement : ISpawnPlacementPolicy
{
private readonly IMyTerrain _terrain;
public DryLandPlacement(IMyTerrain terrain) => _terrain = terrain;
// Refuse water; the spawner rolls another point (SpawnerOptions.PlacementAttempts).
public bool TryPlace(string? zone, ref float x, ref float z) => !_terrain.IsWater(zone, x, z);
} -
bool TryPlace(string zone, ref float x, ref float z)Accepts, adjusts, or refuses one candidate spawn point.
Services you call 1
Crossplay implements these. Resolve them from DI and call them from your own systems.
ISpawnerService
Population spawners: keep Count entities alive in an area and respawn them on a timer after they are removed — HOWEVER they are removed (the game's death→IServerEntities.Despawn, a dungeon reset, anything). The piece never knows what an entity is; the game attaches hp/brains/loot through the Spawned hook. Alongside the keep-N-alive spawners it also runs one-shot PACED SETS (StartSpawnSet): a fixed wave spaced out over time, no respawns.
-
long AddSpawner(SpawnerDefinition definition)Registers a population and spawns its first wave immediately — or, for a definition with InstanceTemplate set, registers a template that spawns nothing itself and populates each instance of its zone as it activates.
-
bool CancelSpawnSet(long setId)Stops a set's pacing. Already-spawned entities stay alive (the set simply forgets them), and a canceled set never raises SpawnSetCleared.
-
bool RemoveSpawner(long spawnerId)Removes a spawner and despawns its live entities (no respawns follow).
-
event Action<long> SpawnSetClearedRaised once per set — with its set id — when the set has FULLY spawned AND every entity it spawned has been removed from the world (however removed). The "wave down" hook a composition bridge uses to advance whatever paced the wave. Never raised for canceled sets; fires on the poll thread.
-
event Action<long, long> SpawnedRaised once per spawned entity: (spawnerId, entityId) — the game-glue hook. The piece only keeps a bare entity alive; attach everything a live entity needs here — hp via Stats, an NPC brain via IAgentService.Attach, a loot table, etc. Fires on the first wave and on every respawn, on the poll thread. Paced sets raise it too, with the set id first (ids never collide — spawners and sets share one counter).
-
long StartSpawnSet(SpawnSetDefinition definition)Starts a PACED SET: Total spawns paced out over time (one per PacingSeconds; all at once when ≤ 0) at the definition's points/zone, each spawned entity carrying the definition's opaque appearance. Unlike a spawner, a set NEVER respawns — it is how a composition layer paces a population wave from data; growth/cycling policy deliberately lives OUTSIDE the piece (the bridge computes Total/Pacing per cycle). Spawned fires per entity (set id first) so downstream hooks work unchanged.
-
bool TryGetSpawnerOf(long entityId, out long spawnerId)The spawner a live entity belongs to, if any.
Configuration 1
Every tunable lives in an options object — there are no magic numbers to hunt for.
SpawnerOptions
Spawner tuning (defaults here, never inline).
-
double DefaultRespawnSeconds { get; set; }Respawn delay used when a definition leaves it at 0.
-
int PlacementAttempts { get; set; }How many candidate points a spawn rolls before it gives up, each one offered to the ISpawnPlacementPolicy (which may accept it, adjust it, or refuse it). A spawner re-rolls a fresh point on its disc; a scattering paced set re-picks one of its points. When every candidate is refused that spawn is SKIPPED for now and retried on the normal cadence: a spawner re-arms it on its respawn timer (RespawnSeconds), a paced set on its next pacing interval (an unpaced set on the next tick). Values below 1 count as 1. The default, 1, with the shipped accept-every-point policy, places exactly as before the seam existed.
-
List<SpawnerDefinition> Populations { get; set; }Ambient populations registered automatically at server start — each through the same AddSpawner path (first wave spawns immediately, kept alive on the respawn timer). This is what makes a standing population reachable from CONFIGURATION: a generated server binds this list from its Crossplay:Spawner section, so an entity "simply exists in the world" with zero server code. The game still attaches hp/brains/loot through the Spawned hook (or the World EntitySpawned event) exactly as for a code-registered spawner. Empty (the default) = nothing is auto-registered — byte-identical to the bare piece; the game may still call AddSpawner itself.