This document explains how Hytale's mob spawning system works and how HyperSpawns integrates with it to control spawns.
- Spawning Overview
- NPC Roles and Groups
- Spawn Suppression System
- Chunk-Based Spawning
- Environmental Factors
- How HyperSpawns Integrates
Hytale uses an ECS (Entity Component System) architecture for mob spawning. The spawning system is managed by the SpawningPlugin, which handles:
- Natural mob spawning based on biome and conditions
- Spawn rate calculations
- Population density management
- Spawn suppression
Spawn Cycle
│
├── 1. Select candidate chunks
│
├── 2. Check spawn conditions
│ ├── Light level
│ ├── Y level
│ ├── Time of day
│ ├── Moon phase
│ └── Biome rules
│
├── 3. Check suppression
│ └── ChunkSuppressionEntry
│
├── 4. Select NPC type
│ └── Based on spawn table
│
└── 5. Spawn entity
Each mob type in Hytale has a unique role identifier. Roles are integer indices that map to specific NPC types.
Examples:
zombie→ role indexskeleton→ role indexspider→ role index
When HyperSpawns filters by role, it uses these indices for efficient matching.
Roles are organized into groups for categorization:
| Group | Description | Examples |
|---|---|---|
hostile |
Aggressive mobs | Zombies, skeletons, spiders |
passive |
Non-aggressive mobs | Chickens, cows, pigs |
neutral |
Conditionally aggressive | Wolves, bees |
Hytale manages group membership through TagSetPlugin<NPCGroup>:
TagSetPlugin<NPCGroup> tagSetPlugin = TagSetPlugin.get(NPCGroup.class);
IntSet rolesInGroup = tagSetPlugin.getSet(groupIndex);HyperSpawns uses this to resolve group names to individual role indices.
The spawn suppression system is Hytale's built-in mechanism for controlling spawns at the chunk level.
A world-level resource that maintains the suppression state:
SpawnSuppressionController controller = entityStore.getResource(
SpawnSuppressionController.getResourceType()
);
// Access the chunk suppression map
Long2ObjectConcurrentHashMap<ChunkSuppressionEntry> chunkMap =
controller.getChunkSuppressionMap();The map key is a packed long representing chunk coordinates (ChunkUtil.indexChunk(x, z)).
Contains suppression data for a single chunk:
public class ChunkSuppressionEntry {
private List<SuppressionSpan> suppressionSpans;
public List<SuppressionSpan> getSuppressionSpans();
}A chunk can have multiple suppression spans from different sources (plugins, world features, etc.).
Defines a vertical range and which NPC roles are suppressed:
public class SuppressionSpan {
private UUID suppressorId; // Who created this suppression
private int minY; // Minimum Y level
private int maxY; // Maximum Y level
private IntSet suppressedRoles; // null = all roles
public UUID getSuppressorId();
public int getMinY();
public int getMaxY();
public IntSet getSuppressedRoles();
}When the spawning system attempts to spawn a mob:
- Get the chunk's
ChunkSuppressionEntry - For each
SuppressionSpan:- Check if spawn Y is within [minY, maxY]
- Check if the NPC's role is in
suppressedRoles(or if null, all roles)
- If any span matches, suppress the spawn
Hytale uses 16x16 block chunks. Conversion:
int chunkX = blockX >> 4; // or Math.floor(blockX / 16.0)
int chunkZ = blockZ >> 4;
long chunkIndex = ChunkUtil.indexChunk(chunkX, chunkZ);- Efficiency - O(1) lookup for suppression data
- Locality - Spawning naturally clusters in chunks
- Loading - Matches chunk loading/unloading lifecycle
When chunks load, they need suppression data applied:
ChunkSuppressionQueue queue = chunkStore.getResource(
SpawningPlugin.get().getChunkSuppressionQueueResourceType()
);
// Queue suppression for newly loaded chunk
queue.queueForAdd(chunkRef, suppressionEntry);
// Queue removal when chunk unloads
queue.queueForRemove(chunkRef);Hytale's spawning considers various environmental conditions:
- Range: 0-15
- Hostile mobs typically spawn at low light (0-7)
- Passive mobs may spawn at any light level
Light Level 0-7: Hostile spawning allowed
Light Level 8-15: Hostile spawning typically blocked
- Underground: More cave mobs
- Surface: Biome-specific spawns
- Sky: Flying mobs
| Period | Description |
|---|---|
day |
Full daylight hours |
night |
Nighttime |
dawn |
Sunrise transition |
dusk |
Sunset transition |
Hostile mobs typically spawn more at night.
Hytale uses a moon cycle (phases 0-7):
| Phase | Description |
|---|---|
| 0 | Full moon |
| 4 | New moon |
Some mobs have increased spawn rates during specific moon phases.
Each biome has its own spawn table defining:
- Which mobs can spawn
- Spawn weights
- Special conditions
HyperSpawns integrates at the suppression system level, not the spawn event level. This is important for performance.
Traditional Approach (Event-Based):
Spawn Event → Event Handler → Check Zones → Allow/Cancel
❌ Runs on every spawn attempt
❌ Must iterate through zones
❌ Cancellation overhead
HyperSpawns Approach (Suppression-Based):
Zone Created → Update Suppression Map → Spawning System Checks Map
✅ One-time setup per zone change
✅ O(1) lookup via chunk index
✅ Native integration with spawning
This is the core integration class:
public void applyToWorld(World world, SpawnZoneManager zoneManager) {
// 1. Get suppression controller
SpawnSuppressionController controller = ...;
// 2. Clear existing HyperSpawns suppressions
clearExistingSuppression(controller, ...);
// 3. For each zone in this world
for (SpawnZone zone : zones) {
if (zone.getMode() != BLOCK && zone.getMode() != DENY) continue;
if (!zone.isEnabled()) continue;
// 4. Apply zone to all intersecting chunks
applyZoneSuppression(zone, controller, ...);
}
}HyperSpawns generates unique IDs with a recognizable prefix:
// Prefix: "HSPS" in hex (HyperSpawns)
private static final UUID HYPERSPAWNS_PREFIX =
UUID.fromString("48535053-0000-0000-0000-000000000000");
// Each zone gets a derived ID
UUID suppressorId = new UUID(
HYPERSPAWNS_PREFIX.getMostSignificantBits() ^ zoneId.getMostSignificantBits(),
HYPERSPAWNS_PREFIX.getLeastSignificantBits() ^ zoneId.getLeastSignificantBits()
);This allows HyperSpawns to:
- Identify its own suppression entries
- Remove them without affecting other plugins
- Update zones without full re-application
// Suppress ALL roles (null = all)
IntSet suppressedRoles = null;
// Creates span covering zone's Y range
new SuppressionSpan(suppressorId, minY, maxY, null);// Suppress only filtered roles
IntSet suppressedRoles = filter.getCompiledRoleIndices();
// Plus roles from filter groups
for (String group : filter.getNpcGroups()) {
IntSet groupRoles = resolveGroup(group);
suppressedRoles.addAll(groupRoles);
}
new SuppressionSpan(suppressorId, minY, maxY, suppressedRoles);ALLOW, MODIFY, and REPLACE modes don't use the suppression system directly. They would require event-based handling for full implementation.
Chunks affected = Σ (zone.getChunkCount()) for all zones
Memory per chunk = ~100-200 bytes per suppression span
For a 200x200 block zone (13x13 = 169 chunks), memory usage is minimal.
Spawn check: O(1) map lookup + O(spans) iteration
Typically spans ≤ 5 per chunk
- Server start
- Plugin reload (
/hyperspawns reload) - Zone creation/modification
- Chunk loading (for new chunks)
| Aspect | Suppression-Based | Event-Based |
|---|---|---|
| Setup cost | Higher (one-time) | Lower |
| Per-spawn cost | Minimal | Higher |
| Scalability | Excellent | Limited |
| Complexity | Requires native API | Simpler |
| Flexibility | Modes limited | Any logic possible |
HyperSpawns uses suppression for BLOCK/DENY (most common) and would need event handling for MODIFY/REPLACE/ALLOW to be fully implemented.
- Architecture - HyperSpawns code structure
- Admin Guide - Using HyperSpawns
- Troubleshooting - Common issues