Skip to content

ZombieManager — Zombie AI and Spawning

The ZombieManager (1960 lines in Unturned/Managers/ZombieManager.cs) is the server-authoritative manager controlling the zombie population of an Unturned level. It manages zombie spawning from navmesh-defined regions, the per-zombie AI state machine, special ability replication (spit, charge, acid, boulder, stun, breath, spark, stomp, throw), loot table resolution on death, the wave/horde-beacon system, and the tick-based update loop that processes zombie behavior in time-sliced batches.

The zombie system is structured around several supporting types: Zombie (the runtime component), ZombieRegion (region container per navmesh bound), ZombieSpawnpoint (spawn location linked to a zombie table), ZombieTable (tiered slot configuration with loot and difficulty), ZombieSlot (clothing slot with chance-weighted item tables), and ZombieCloth (individual clothing item assignment).

Source files: Unturned/Managers/ZombieManager.cs (1960 lines), Unturned/Level/ZombieSpawnpoint.cs (38 lines), Unturned/Level/ZombieTable.cs, Unturned/Level/ZombieSlot.cs, Unturned/Level/ZombieCloth.cs. This article references the zombie-related types in Unturned/Level/.

Who this article is for

This article is for module and plugin developers who need to spawn zombies, manage AI behavior slots, configure loot tables, work with horde beacons, or create custom wave encounters. It assumes familiarity with the navmesh bound system (LevelNavigation) and the Regions spatial partitioning model.

What you'll learn

  • The zombie region model: one region per navmesh bound
  • The tick-based AI processing loop with wandering, idle, target acquisition states
  • Special zombie variants and their replicated abilities
  • Horde spawning and wave management
  • The weighted-random speciality selection system
  • Loot drop resolution from zombie tables
  • The prefab selection for dedicated server / listen server / client builds

Zombie region model

Zombies are grouped into ZombieRegion instances, one per navmesh navigation bound:

csharp
private static ZombieRegion[] _regions;
public static ZombieRegion[] regions => _regions;
internal static HashSet<int> regionsWithPlayers;

Each ZombieRegion corresponds to a LevelNavigation bound — the same bounds used for navmesh baking. The regionsWithPlayers set tracks which regions currently have at least one player present, which drives the spawning and ticking decisions.

Zombie list

All zombies are accessible both per-region and globally:

csharp
public static List<Zombie> AllZombies { get; private set; }

The per-region zombie list is regions[bound].zombies. Each zombie's bound field stores its navmesh bound index.

getZombiesInRadius

csharp
public static void getZombiesInRadius(Vector3 center, float sqrRadius, List<Zombie> result)

Resolves the navmesh bound from the center point via LevelNavigation.tryGetNavigation, then iterates the zombies in that region only. This is a narrow, bound-scoped search — unlike the manager spatial queries that span multiple regions.

Zombie spawning

addZombie — Instantiation

csharp
public void addZombie(byte bound, byte type, byte speciality, byte shirt, byte pants,
    byte hat, byte gear, byte move, byte idle, Vector3 position, float angle, bool isDead)

The method selects the appropriate prefab based on the build context:

csharp
GameObject zombiePrefab;
if (Dedicator.IsDedicatedServer)
    zombiePrefab = dedicatedZombiePrefab;      // "Characters/Zombie_Dedicated"
else if (Provider.isServer)
    zombiePrefab = serverZombiePrefab;           // "Characters/Zombie_Server"
else
    zombiePrefab = clientZombiePrefab;           // "Characters/Zombie_Client"

The dedicated server prefab has minimal visual components. The client prefab includes the full renderer, animation, and audio. The listen server prefab sits in the middle.

The zombie's id is assigned as the index in the region's zombie list:

csharp
character.id = (ushort) regions[bound].zombies.Count;

After setting all fields (type, speciality, bound, slot, clothing, move/idle state, dead flag), character.init() is called to wire up the zombie's runtime state.

Initial replication to clients

When a client enters a region, the server sends all zombies in that region via SendZombies:

csharp
private static void SendZombies_Write(NetPakWriter writer, byte bound)

Each zombie is serialized with: type, speciality, shirt, pants, hat, gear, move state, idle state, position, yaw, and isDead flag.

Difficulty asset resolution

csharp
public static ZombieDifficultyAsset getDifficultyInBound(byte bound)
public static ZombieDifficultyAsset GetDifficultyInBoundForTable(byte bound,
    ZombieTable table, bool forSpawnOverrides)

The difficulty for a bound can come from either the navmesh flag data (LevelNavigation.flagData[bound]) or the zombie table. The prioritization is controlled by Level.getAsset()?.ZombieDifficultyAssetPrioritization:

  • NavmeshOverridesTable (default): check navmesh first, fall back to table
  • TableOverridesNavmesh: check table first, fall back to navmesh

The forSpawnOverrides flag controls whether the result must have Overrides_Spawn_Chance set to be considered valid for spawn chance overrides.

AI state system

Tick processing

The AI update loop uses a time-slice approach:

csharp
private static int tickIndex;
private static List<Zombie> _tickingZombies;

Only a subset of zombies are processed each frame, determined by tickIndex. The canSpareWanderer property controls whether new wandering zombies can be spawned:

csharp
public static bool canSpareWanderer => wanderingCount < 8 && tickingZombies.Count < 50;

This caps wandering zombies at 8 and total ticking zombies at 50.

Wandering and idle

Zombies have move and idle byte fields that encode their wandering pattern and idle animation set. Movement is driven by the navmesh system — zombies pathfind toward targets using the Unturned navmesh (ASPFP or Unity NavMesh depending on the build).

Target acquisition

Zombies switch between wandering and chase states based on player proximity, noise events, and damage taken. The startle mechanism (replicated via SendZombieStartle) triggers a brief reaction animation and aggressive state.

Special zombie abilities

Zombie specialities are defined by the EZombieSpeciality enum. Each speciality has a dedicated RPC pair for its unique ability:

AbilityRPCMethod on Zombie
ThrowSendZombieThrow / ReceiveZombieThrowaskThrow()
SpitSendZombieSpit / ReceiveZombieSpitaskSpit()
ChargeSendZombieCharge / ReceiveZombieChargeaskCharge()
StompSendZombieStomp / ReceiveZombieStompaskStomp()
BreathSendZombieBreath / ReceiveZombieBreathaskBreath()
AcidSendZombieAcid / ReceiveZombieAcidaskAcid(origin, direction)
Spark (electrical)SendZombieSpark / ReceiveZombieSparkaskSpark(target)
BoulderSendZombieBoulder / ReceiveZombieBoulderaskBoulder(origin, direction)
AttackSendZombieAttack / ReceiveZombieAttackaskAttack(attackType)

All ability RPCs validate that the zombie index is within the region's list before dispatching. The ESteamCallValidation.ONLY_FROM_SERVER attribute ensures clients cannot forge these calls.

Speciality weighted random

The ZombieSpecialityWeightedRandom inner class provides weighted random selection of zombie specialities for spawn:

csharp
private class ZombieSpecialityWeightedRandom : IComparer<Entry>

Entries are added with a weight value, sorted by weight ascending via binary search insertion. The get() method performs a weighted random draw by projecting a Random.value * totalWeight onto the sorted entry list. This is used to select which speciality a newly spawned zombie receives based on difficulty asset configuration.

Loot drops

csharp
public static void dropLoot(Zombie zombie)

The drop resolution has three tiers:

Zombie typeDrop count rangeConfig keys
Boss / BOSS_ALLMin_Boss_Drops to Max_Boss_DropsZombies config
MegaMin_Mega_Drops to Max_Mega_DropsZombies config
NormalMin_Drops to Max_DropsZombies config

Drops are clamped to [0, 100] to prevent crash exploits.

The loot resolution chain:

  1. If LevelZombies.tables[zombie.type].lootID != 0, resolve via SpawnTableTool.ResolveLegacyId.
  2. Otherwise, if lootIndex is valid, use LevelItems.getItem(lootIndex).
  3. Each resolved item is dropped via ItemManager.dropItem with EItemOrigin.WORLD.

Mega zombie tracking: when a mega zombie dies, regions[zombie.bound].lastMega and hasMega are updated to prevent multiple mega zombies in the same region.

Wave and horde beacon system

Wave state

csharp
private static bool _waveReady;
public static bool waveReady => _waveReady;
private static int _waveIndex;
public static int waveIndex => _waveIndex;
private static int _waveRemaining;
public static int waveRemaining => _waveRemaining;

The wave system is active when Level.info.type == ELevelType.HORDE. The lastWave timer and respawnZombiesBound variable control wave progression.

Beacon state per region

csharp
regions[reference].hasBeacon = hasBeacon;

The SendBeacon / ReceiveBeacon RPC pair synchronizes the beacon-activated state per region. When a beacon is active, the region spawns waves of zombies targeting the beacon location.

Wave notifications

csharp
public static WaveUpdated onWaveUpdated;

Fired on both server and client when wave state changes. The SendInitialGlobalState method sends the current wave state to newly connecting clients.

Custom cooldown system

csharp
private static Dictionary<string, double> cooldowns = new Dictionary<string, double>(
    StringComparer.InvariantCultureIgnoreCase);

The CheckCustomCooldown method provides a general-purpose timed cooldown check:

csharp
public static bool CheckCustomCooldown(string cooldownId, double duration)

Returns true if the cooldown has expired (or doesn't exist), then resets the timer. Returns false if the cooldown is still active. Uses Time.timeAsDouble for precision.

Zombie speciality and state replication

Alive state

csharp
public static void sendZombieAlive(Zombie zombie, byte newType, byte newSpeciality,
    byte newShirt, byte newPants, byte newHat, byte newGear,
    Vector3 newPosition, byte newAngle)

Broadcasts the full alive state (type, speciality, clothing, position, angle). Also fires regions[zombie.bound].onZombieLifeUpdated.

Dead state

csharp
public static void sendZombieDead(Zombie zombie, Vector3 newRagdoll,
    ERagdollEffect newRagdollEffect = ERagdollEffect.None)

Broadcasts death with ragdoll force and effect flag. Fires onZombieLifeUpdated.

State updates

csharp
public static void ReceiveZombieStates(in ClientInvocationContext context)

The batched state update uses a monotonically increasing sequence number (seq) to reject stale packets. Each zombie's position and yaw are read via ReadClampedVector3 and ReadDegrees.

ZombieSpawnpoint

The ZombieSpawnpoint class (38 lines in Unturned/Level/ZombieSpawnpoint.cs) is a lightweight data holder:

csharp
public class ZombieSpawnpoint
{
    public byte type;
    public Vector3 point => _point;
    public Transform node => _node;
}

The type field indexes into LevelZombies.tables to determine which zombie table this spawnpoint uses. In the editor, node is created as a colored sphere marker (color from LevelZombies.tables[type].color).

ZombieTable data structure

LevelZombies loads zombie tables from Spawns/Zombies.dat. Each ZombieTable contains:

  • Color and name for editor identification
  • isMega flag
  • Health, damage, loot ID/index
  • XP reward
  • Regen rate
  • Difficulty GUID
  • 4 ZombieSlot slots (clothing layers), each with a spawn chance and a list of ZombieCloth items

Tables are identified by a tableUniqueId (int) for cross-referencing. The FindTableIndexByUniqueId method provides lookup by unique ID rather than by index.

Audio

csharp
public static AudioClip[] roars;
public static AudioClip[] groans;
public static AudioClip[] spits;
public static AudioClip[] dl_attacks;
public static AudioClip[] dl_deaths;
public static AudioClip[] dl_enemy_spotted;
public static AudioClip[] dl_taunt;

Audio clips are loaded as static arrays and referenced by the Zombie component for ambient and combat sounds.

Plugin integration

ZombieManager exposes relatively few static delegate hooks compared to VehicleManager or BarricadeManager. The primary integration point is onWaveUpdated for wave-based game modes. Custom zombie behavior is typically implemented through the Zombie component's virtual methods or by replacing the zombie prefab.

Zombie init and runtime component

When addZombie creates a new zombie, the Zombie component's init() method configures:

  1. Slot system: Each zombie has 4 clothing slots (shirt, pants, hat, gear). These are resolved from the ZombieSlot entries in the zombie table at spawn time by calling applySlot(slotIndex), which performs a weighted random draw to select a ZombieCloth item.
  2. Difficulty scaling: The bound's ZombieDifficultyAsset may override health, damage, speed, and speciality weights.
  3. Speciality assignment: The weighted random system selects the zombie's speciality based on the difficulty asset's speciality weights.
  4. Navmesh agent: The zombie's navmesh agent is configured with the correct speed, acceleration, and stopping distance from the difficulty asset.
  5. Visual appearance: The clothing items are applied to the zombie's renderer. Skin color, eye glow, and other visual overrides are applied from the table configuration.

The Zombie component tracks:

FieldTypePurpose
idushortIndex in the region's zombie list
boundbyteNavmesh bound index
typebyteIndex into LevelZombies.tables
specialityEZombieSpecialityVariant type (NORMAL, MEGA, SPRINTER, CRAWLER, etc.)
shirt, pants, hat, gearbyteClothing slot indices
movebyteMovement animation state
idlebyteIdle animation state
isDeadboolAlive/dead state
zombieRegionZombieRegionReference to the owning region

AI state machine

The Zombie component implements a state machine with these states:

StateTriggerBehavior
IDLENo target detectedStand still, play idle animation, play ambient sounds
PATROLRandom intervalWander to a random point within wander radius
ALERTPlayer detected, noise heardFace target, play alert animation, transition to CHASE
CHASETarget acquiredPathfind toward target using navmesh
ATTACKWithin melee rangeMelee attack on cooldown
SPECIALAbility cooldown readyUse special ability (spit, charge, etc.)
STUNDamage or flashbangStand still, play stun animation, recover after stun duration
STARTLESurprise damagePlay startle animation before transitioning to CHASE
DEADHealth reaches zeroPlay death animation, trigger ragdoll, spawn loot

The tickZombie method (part of the tick time-slice system) evaluates transitions once per unit interval.

ZombieRegion

Each ZombieRegion manages:

csharp
// Not directly in source but derived from usage:
// - List<Zombie> zombies
// - bool isNetworked
// - bool hasBeacon
// - float lastMega
// - bool hasMega
// - event onZombieLifeUpdated

The isNetworked flag is set when the full zombie list has been sent to a client. Clients do not create zombies until ReceiveZombies is processed for a region.

The hasBeacon flag triggers wave-based horde spawning in that region.

Horde wave spawning

When hasBeacon is true for a region:

  1. The respawnZombiesBound timer controls how often new zombies are spawned.
  2. Zombies are spawned at beacon spawnpoints with increased frequency.
  3. The _waveReady flag is set when a wave is prepared.
  4. _waveIndex increments with each completed wave.
  5. _waveRemaining tracks how many zombies remain in the current wave.

Wave spawning respects LevelZombies.tables[zombieType] configuration for zombie types, but may increase the proportion of special zombies based on the wave index.

Tick system — time-sliced processing

csharp
private static int tickIndex;
private static List<Zombie> _tickingZombies;

The tick system distributes AI processing across frames:

  1. tickIndex advances by regionsWithPlayers.Count each tick.
  2. Zombies in player-active regions are added to _tickingZombies.
  3. Each tick, one zombie from _tickingZombies is processed.
  4. The wanderingCount cap ensures no more than 8 zombies wander simultaneously.
  5. The _tickingZombies.Count < 50 cap prevents AI overload.

This time-slicing ensures that AI overhead scales with player count, not total zombie count.

Zombie clothing slot system

The ZombieSlot class holds:

csharp
public class ZombieSlot
{
    public float chance;           // Probability this slot is populated
    public List<ZombieCloth> table; // Weighted clothing items
}

Each of the 4 slots (shirt, pants, hat, gear) is independently rolled. ZombieCloth references an item ID:

csharp
public class ZombieCloth
{
    public ushort item;  // Item ID for the clothing asset
}

The LevelZombies.load() method reads slot data from Zombies.dat:

csharp
ZombieSlot[] slots = new ZombieSlot[4];
byte slotCount = block.readByte();
for (byte slotIndex = 0; slotIndex < slotCount; slotIndex++)
{
    float chance = block.readSingle();
    byte clothCount = block.readByte();
    List<ZombieCloth> cloths = new List<ZombieCloth>();
    for (byte clothIndex = 0; clothIndex < clothCount; clothIndex++)
    {
        ushort item = block.readUInt16();
        cloths.Add(new ZombieCloth(item));
    }
    slots[slotIndex] = new ZombieSlot(chance, cloths);
}

Zombie spawn cycle (respawn system)

The tick method of ZombieManager handles zombie respawning:

csharp
private static byte respawnZombiesBound;
private static float lastWave;

Each tick:

  1. If the level type is HORDE, check wave timers and spawn horde zombies.
  2. For each region with players (regionsWithPlayers):
    • Check if the region is below its maxZombies cap.
    • If below cap and enough time has passed since last spawn, spawn a new zombie.
    • Select a random ZombieSpawnpoint from the region.
    • Resolve the zombie type and speciality from the table.
    • Apply clothing slots via weighted random from the table's ZombieSlot entries.
  3. For regions without players: zombies remain in their current state and are not actively respawned.

The respawnZombiesBound index round-robins across regions to distribute spawn processing across frames.

Server-side request validation

RPCs like ReceiveZombieThrow, ReceiveZombieCharge, etc. (all prefixed with askZombie) are marked with [SteamCall(ESteamCallValidation.SERVERSIDE)]. This attribute ensures they can only be called by the server — client-originated invocations are silently dropped.

However, ReceiveZombieAlive, ReceiveZombieDead, ReceiveZombieSpeciality, and the other state synchronization RPCs use [SteamCall(ESteamCallValidation.ONLY_FROM_SERVER)] with a client-side guard:

csharp
if (!Provider.isServer)
{
    if (!regions[reference].isNetworked)
        return;
}

This ensures that even if the server sends state for a region the client hasn't fully received yet, the state is safely ignored.

ZombieSpawnpoint usage at spawn time

During zombie spawning, the manager iterates the LevelZombies.spawns[x, y] list for the player's current region and uses those positions as spawn locations:

csharp
public static List<ZombieSpawnpoint>[,] spawns => _spawns;

Each ZombieSpawnpoint has:

  • type: Index into the zombie tables
  • point: World position

The spawner filters spawnpoints by type to match the expected zombie table, and validates the spawn position is clear (not inside a barricade or structure).

BOSS_ALL speciality handling

The EZombieSpeciality.BOSS_ALL speciality is treated as a boss-type modifier rather than a specific ability. Zombies with BOSS_ALL:

  • Use boss-level loot drops (Min_Boss_Drops / Max_Boss_Drops).
  • Are not subject to the per-bound max zombie limit.
  • The regions[bound].lastMega and hasMega tracking does not apply (BOSS_ALL is managed separately from mega zombies).

State update batching

The SendZombieStates packet uses a monotonic sequence number and compressed position:

csharp
reader.ReadUInt32(out newSeq);
if (newSeq <= seq) return;  // Reject stale
seq = newSeq;

reader.ReadUInt16(out count);
for (ushort index = 0; index < count; ++index)
{
    reader.ReadUInt16(out zombieIndex);
    reader.ReadClampedVector3(out position);
    reader.ReadDegrees(out yaw);
    regions[reference].zombies[zombieIndex].tellState(position, yaw);
}

This batch only includes zombies in the player's current region (determined by the reference byte).

Zombie speciality value propagation

When a zombie's speciality changes at runtime (e.g., a normal zombie transforms into a mega zombie), sendZombieSpeciality broadcasts the change:

csharp
public static void sendZombieSpeciality(Zombie zombie, EZombieSpeciality speciality)
{
    SendZombieSpeciality.InvokeAndLoopback(ENetReliability.Unreliable,
        GatherRemoteClientConnections(zombie.bound),
        zombie.bound, zombie.id, speciality);
}

This RPC is unreliable (fire-and-forget) because speciality changes are cosmetic on the receiving end.

Zombie breed configuration

The LevelZombies.tables list stores every zombie table for the current level. Each table contains:

FieldTypeDescription
namestringEditor-only identifier
colorColorEditor gizmo color
isMegaboolMega zombie variant
healthushortBase health
damagebyteBase melee damage
lootbyteLegacy loot index
lootIDushortSpawn table ID for loot
xpuintExperience reward
regenfloatHealth regen per second
difficultyGUIDstringZombieDifficultyAsset reference
tableUniqueIdintPersistent unique identifier
slots[4]ZombieSlot[]Clothing slot configurations

Server-side zombie throw handling

When the server receives a throw request (ReceiveZombieThrow), it:

  1. Validates the zombie index is within the region's list.
  2. Calls zombie.askThrow() which:
    • Selects a target position near the nearest player.
    • Launches a Throwable projectile (typically a rock or other damaging object).
    • Applies damage if the projectile hits a player.
  3. Broadcasts SendZombieThrow to nearby clients for projectile visual sync.

Similar flows apply to askBoulder (boulder boss attack), askAcid (acid spray), and askSpark (electrical attack). These all carry position and direction data via the RPC.

Stun and startle mechanics

Startle

SendZombieStartle is triggered when a zombie takes damage from a source it was not already targeting. The startle animation plays briefly before the zombie transitions to CHASE state. The startle byte parameter selects the animation variant.

Stun

SendZombieStun is triggered by explosive damage, flashbangs, or environmental hazards. The stun byte parameter encodes the stun duration (in frames or seconds). While stunned, the zombie:

  • Stops all movement and navigation.
  • Plays the stun animation.
  • Cannot attack or use special abilities.
  • Recovers after the stun duration expires.

Per-region zombie limits

Each navmesh bound (via FlagData) configures:

csharp
byte maxZombies;     // Default 64
bool spawnZombies;   // Can zombies spawn here?
int maxBossZombies;  // Boss count limit (-1 = no limit)

The regionsWithPlayers hash set tracks which regions have active players. Only regions in this set process zombie spawning and AI ticking. The maxZombies cap per region prevents one area from accumulating all zombies.

Mega zombie spawn cooldown

csharp
regions[zombie.bound].lastMega = Time.realtimeSinceStartup;
regions[zombie.bound].hasMega = false;

When a mega zombie dies, the region tracks the death time in lastMega and clears hasMega. A new mega zombie cannot spawn in that region until a cooldown period has elapsed. The cooldown prevents mega zombies from being farmed for loot too quickly.

Zombie loot resolution chain

The dropLoot method uses a two-path resolution:

if (LevelZombies.tables[type].lootID != 0)
    path = SPAWN_TABLE_TOOL;
else if (LevelZombies.tables[type].lootIndex < LevelItems.tables.Count)
    path = LEGACY_ITEM_TABLE;
else
    path = NO_LOOT;

The spawn table tool path uses SpawnTableTool.ResolveLegacyId which supports GUID-based redirectors. The legacy path uses LevelItems.getItem which is a weighted random draw from the legacy item spawn table.

Horde mode specific behavior

When Level.info.type == ELevelType.HORDE:

  1. SendInitialGlobalState sends the current wave state (waveReady, waveIndex).
  2. The lastWave timer controls auto-advancement to the next wave.
  3. Zombies only spawn at horde beacon spawnpoints.
  4. Wave completion triggers:
    • _waveIndex++.
    • New zombie batch spawns with increased difficulty.
    • Loot rewards are distributed to wave participants.
  5. Boss zombies have increased spawn weight in later waves.

Zombie slot data loading

The slot system (ZombieSlot) supports weighted random clothing:

csharp
for (byte slotIndex = 0; slotIndex < slotCount; slotIndex++)
{
    float chance = block.readSingle(); // Probability this slot is filled
    byte clothCount = block.readByte();
    List<ZombieCloth> cloths = new List<ZombieCloth>();
    for (byte clothIndex = 0; clothIndex < clothCount; clothIndex++)
    {
        ushort item = block.readUInt16();
        ItemAsset asset = Assets.find(EAssetType.ITEM, item) as ItemAsset;
        if (asset == null) continue;
        cloths.Add(new ZombieCloth(item));
    }
    slots[slotIndex] = new ZombieSlot(chance, cloths);
}

Missing clothing items are silently skipped (not added to the clothing list). This prevents broken visuals when clothing assets are missing.

Example: Getting zombies in a region

csharp
byte bound;
if (LevelNavigation.tryGetBounds(player.transform.position, out bound))
{
    List<Zombie> nearbyZombies = new List<Zombie>();
    ZombieManager.getZombiesInRadius(player.transform.position, 10000, nearbyZombies);
    // nearbyZombies now contains all zombies within 100m in the same nav bound
}

Example: Triggering a custom zombie wave

csharp
// Manually trigger a wave for a horde beacon
if (ZombieManager.waveReady)
{
    // The wave system will auto-spawn zombies at beacon locations
    // Custom wave behavior can be added via onWaveUpdated
}

Example: Forcing loot from a zombie

csharp
// After a zombie dies, the dropLoot method is called automatically
// To add custom loot:
ZombieManager.dropLoot(zombie);
// After calling dropLoot, the zombie's table decides what items are dropped

Document history