Skip to content

BarricadeManager

BarricadeManager (3562 lines in Unturned/Managers/BarricadeManager.cs) is the server-authoritative system for all placed barricades in the Unturned world. It manages placement validation, health tracking, region-based storage, vehicle-attached barricades, damage and repair pipelines, ownership and group management, salvage, transform editing, and special interactable state operations (sign text, mannequin poses, tank amounts, stereo tracks).

Barricades are the small buildable objects in Unturned — signs, storage containers, sentries, generators, traps, farms, beds, mannequins, stereos, ovens, rain barrels, tanks, oil pumps, and more. Unlike structures, barricades can be placed on vehicles, can carry arbitrary state byte arrays, and have a wider variety of interactable behaviors.

Source code location: Unturned/Managers/BarricadeManager.cs

Architecture Overview

BarricadeManager extends SteamCaller and operates as a static singleton accessible via BarricadeManager.instance. Its architecture follows the standard manager pattern: static delegates for plugin hooks, region-based spatial partitioning, NetId-based drop addressing, and batched state replication.

Key architectural components:

  • Region grid (regions[x, y]): 2D array of BarricadeRegion instances, each containing List<BarricadeData> (serverside state) and List<BarricadeDrop> (runtime components).
  • Vehicle regions (internalVehicleRegions): List<VehicleBarricadeRegion> for barricades planted on vehicles.
  • Delegates: Static multicast delegate hooks (onDeployBarricadeRequested, onDamageBarricadeRequested, etc.) exposed for plugin interception.
  • NetId registry: NetIdRegistry provides O(1) transform-to-drop lookup via BarricadeDrop.FindByRootFast.

Region-Based Storage

World Regions

csharp
public static BarricadeRegion[,] regions { get; private set; }

Barricade regions partition the world into a 2D grid. The region count per dimension is defined by BARRICADE_REGIONS = 2, meaning lookups extend two regions beyond the player's current region for a 5×5 area check.

Each BarricadeRegion contains:

CollectionTypePurpose
dropsList<BarricadeDrop>Runtime component references (model transforms, interactables)
barricadesList<BarricadeData>Serverside state (owner, group, position, rotation, health, state bytes)

Regions are initialized during awake():

  1. Creates the regions[x, y] 2D array sized by WORLD_SIZE.
  2. Initializes each region's drops list and barricades list.
  3. Initializes internalVehicleRegions as an empty list.
  4. Sets backwardsCompatVehicleRegions cache to null, forcing re-creation on next access.

Vehicle Barricade Regions

csharp
private static List<VehicleBarricadeRegion> internalVehicleRegions;
public static IReadOnlyList<VehicleBarricadeRegion> vehicleRegions { get; private set; }

Each VehicleBarricadeRegion wraps a vehicle Transform and holds a List<BarricadeDrop> for barricades planted on that vehicle. The legacy plants property provides backward compatibility by copying the vehicle region list on access.

Vehicle barricade regions have a distinct lifecycle:

EventAction
Vehicle spawnedNo region yet — created lazily when first barricade is placed
Barricade placed on vehicleRegion created if absent; barricade added
Vehicle destroyedtrimPlant called to drop all barricades; region destroyed
Vehicle explodeduprootPlant called for the main vehicle and all train cars

Spatial Queries

Three getBarricadesInRadius overloads provide flexible spatial querying:

OverloadScopeStrategy
(Vector3, float, List<RegionCoordinate>, List<Transform>)World regions onlyIterates caller-provided region list, checks each barricade's model position against squared radius
(Vector3, float, ushort plant, List<Transform>)Single vehicle regionQueries by plant index, checks plant < vehicleRegions.Count
(Vector3, float, List<Transform>)Combined (all regions + vehicles)Queries all world regions via Regions.GetRegionSearchCoordinates, then all vehicle regions with 256-meter early-out optimization

The combined overload checks the vehicle region's parent position distance before iterating individual drops — this 256-meter early-out prevents unnecessary iteration over distant vehicle barricades.

Placement and Deployment

The Deployment Pipeline

Barricades are deployed through dropReplicatedBarricade (internal) or the legacy dropBarricade method. The legacy path:

csharp
public static bool dropBarricade(Barricade barricade, Transform hit, Vector3 point,
    float angle_x, float angle_y, float angle_z, ulong owner, ulong group)

This fires onDeployBarricadeRequested which lets plugins modify placement point, angles, owner, group, or cancel entirely. After the plugin hook, dropReplicatedBarricade is called which:

  1. Region resolution: Determines the region coordinates from the placement point.
  2. Vehicle detection: Determines whether the barricade is vehicle-mounted (if the hit transform belongs to a vehicle).
  3. Server-side state creation: Creates BarricadeData with owner, group, timestamp, and instance ID.
  4. NetId allocation: Claims a NetId block.
  5. Prefab instantiation: Instantiates the barricade prefab and calls IBarricadePlacedHandler.OnBarricadePlaced if the component implements the interface.
  6. Network broadcast: Broadcasts SendSingleBarricade to relevant clients.
  7. Event fire: Fires onBarricadeSpawned event.

Placement Validation

Position validation checks:

  1. The position must be within level bounds (Level.checkSafeIncludingClipVolumes).
  2. The EBuild type determines valid placement surfaces (ground, wall, ceiling, etc.).
  3. The position must not overlap with existing barricades or structures (checked by region overlap).
  4. For vehicle barricades: the maxDistanceFromHull config value limits how far a buildable can extend from the vehicle's collider surface.

If validation fails, the drop is silently rejected and the client receives no confirmation.

Deploy Region Assignment

For barricades:

  1. Hit detection determines the surface (ground, wall, vehicle).
  2. If the surface is a vehicle: assign to a VehicleBarricadeRegion. Create one if the vehicle does not yet have a region.
  3. If the surface is the ground: assign to the world BarricadeRegion at the placement coordinates.
  4. Validate placement constraints from the ItemBarricadeAsset (e.g., EBuild type determines ground vs wall vs ceiling placement).

Damage Pipeline

Damage Application

csharp
public static void damage(Transform barricade, Vector3 direction, float damage,
    float times, bool armor, CSteamID instigatorSteamID, EDamageOrigin damageOrigin)

The damage pipeline:

  1. Resolve the region and find the BarricadeDrop by root transform.
  2. Check the asset's canBeDamaged flag.
  3. If armor is true, multiply times by the config-defined armor multiplier.
  4. Calculate totalDamage = (ushort)(damage * times).
  5. Fire onDamageBarricadeRequested for plugin interception.
  6. If allowed, call barricade.serversideData.barricade.askDamage(totalDamage).
  7. On death: trigger explosion effect (if configured), spawn item drops, destroy with ragdoll force.

Explosion Effects on Destruction

When a barricade is destroyed by damage, the manager checks the asset for explosion effect flags:

csharp
EffectAsset explosionAsset = asset.FindExplosionEffectAsset();
if (explosionAsset != null)
{
    TriggerEffectParameters explosion = new TriggerEffectParameters(explosionAsset);
    if (asset.ExplosionEffectFlags.HasFlag(EPlaceableExplosionEffectFlags.CopyModelPosition))
        explosion.position = transform.position;
    else
        explosion.position = transform.position + (Vector3.down * HEIGHT);
    if (asset.ExplosionEffectFlags.HasFlag(EPlaceableExplosionEffectFlags.CopyModelRotation))
        explosion.SetRotation(transform.rotation);
    explosion.relevantDistance = EffectManager.MEDIUM;
    explosion.reliable = true;
    EffectManager.triggerEffect(explosion);
}

The explosion effect plays at MEDIUM distance (128 units) with reliable delivery.

Health Sync

After damage or repair, sendHealthChanged broadcasts the new health percentage to clients who are within region range and have ownership visibility:

csharp
byte healthPercent = (byte)Mathf.RoundToInt(
    drop.serversideData.barricade.health / (float)drop.asset.health * 100);

Only players who own (or are in the group of) the barricade AND are within BARRICADE_REGIONS distance receive health updates. This prevents leaking health information to non-owners.

Repair Pipeline

csharp
public static event RepairBarricadeRequestHandler OnRepairRequested;
public static event RepairedBarricadeHandler OnRepaired;

The repair pipeline:

  1. Trigger OnRepairRequested with the instigator, barricade transform, pending total healing, and allow flag.
  2. If allowed, apply healing to the barricade's health.
  3. Fire OnRepaired with the instigator, transform, and total healing amount.
  4. Broadcast the updated health via sendHealthChanged.

Ownership and Group Management

changeOwnerAndGroup

csharp
public static void changeOwnerAndGroup(Transform transform, ulong newOwner, ulong newGroup)

The method:

  1. Resolves the region and finds the drop.
  2. Broadcasts the new owner/group via SendOwnerAndGroup.
  3. Updates the serverside data.
  4. Calls sendHealthChanged to push the updated state.

Salvage

Salvage returns the placed item to the player's inventory:

csharp
BarricadeManager.salvageBarricade(Transform)

Sends SendSalvageRequest. Fires plugin-interceptable events — the old onSalvageBarricadeRequested is deprecated; modern plugins use BarricadeDrop.OnSalvageRequested_Global.

Transform Editing

Client-Requested Transform

csharp
public static void transformBarricade(Transform transform, Vector3 point, Quaternion rotation)

Sends SendTransformRequest (client-to-server). The server validates through onTransformRequested and, if allowed, broadcasts SendTransform with the new position and rotation.

Server-Side Direct Transform

csharp
public static bool ServerSetBarricadeTransform(Transform transform, Vector3 position,
    Quaternion rotation)

For plugin-authorized moves — skips the request delegate and directly updates the transform via InternalSetBarricadeTransform.

Special Interactable Operations

Sign Text

csharp
public static bool ServerSetSignText(InteractableSign sign, string newText)

The method trims the text using the sign's trimText validator, validates with isTextValid, then updates the barricade's state byte array by embedding the UTF-8-encoded text after the 16-byte header.

Sign State Encoding

The ServerSetSignTextInternal method demonstrates the barricade state byte array layout:

csharp
// oldState is 16 bytes (Interactable base state)
// newState layout:
//   [0..15] = copied from oldState (transform, health, etc.)
//   [16]    = text length (byte)
//   [17..]  = UTF-8 encoded text
byte[] newState = new byte[16 + 1 + textState.Length];
System.Buffer.BlockCopy(oldState, 0, newState, 0, 16);
newState[16] = (byte) textState.Length;
System.Buffer.BlockCopy(textState, 0, newState, 17, textState.Length);

The first 16 bytes are the base barricade state (health, flags). Byte 16 stores the text length. The remaining bytes store the UTF-8 text content.

Mannequin Pose

csharp
public static bool ServerSetMannequinPose(InteractableMannequin mannequin, byte poseComp)

Broadcasts SendPose and calls mannequin.rebuildState() to update the mannequin's visual appearance.

Tank Amount

InteractableTank.ServerSetAmount handles fuel/water tank amount changes. The old updateTank is deprecated — callers should use the interactable component's direct method.

Stereo Track

ServerSetStereoTrack sets the GUID of the currently playing track on an InteractableStereo.

State Byte Array

Each Barricade has a state byte array that stores type-specific data. The first 16 bytes always contain the Interactable base state (health, owner flags, etc.):

Interactable TypeState Layout
InteractableSign16 bytes base + 1 byte text length + UTF-8 text
InteractableStorage16 bytes base + item count + serialized items
InteractableMannequin16 bytes base + pose byte + equipped item slots
InteractableTank16 bytes base + 2 bytes (ushort) amount
InteractableGenerator16 bytes base + fuel amount + wire status
InteractableFarm16 bytes base + growth timer + fertilized flag
InteractableSentry16 bytes base + targeting mode + ammo state
InteractableTrap16 bytes base + armed flag + cooldown timer
InteractableStereo16 bytes base + track GUID
InteractableOven16 bytes base + cooking state
InteractableRainBarrel16 bytes base + water amount

BarricadeDrop and Find Methods

FindByRootFast

csharp
public static BarricadeDrop FindByRootFast(Transform transform)

This method looks up the barricade's NetId from the transform and uses the NetIdRegistry for O(1) lookup, avoiding a linear scan through all regions.

FindBarricadeByRootTransform

csharp
public BarricadeDrop FindBarricadeByRootTransform(Transform transform)

The fallback method that iterates the region's drops list linearly. Used when the transform is known but the NetId is not available.

Interaction Validation Delegates

Open Storage

csharp
public static event OpenStorageRequestHandler onOpenStorageRequested;

Fires when a player attempts to open a storage barricade's inventory. Can be used to implement locked chests per-zone.

Modify Sign Text

csharp
public static event ModifySignRequestHandler onModifySignRequested;

Fires when a player attempts to modify a sign's text. The delegate receives the instigator, the sign component, the proposed text (mutable), and the allow/cancel flag.

Interaction Distance Validation

When a player interacts with a barricade (open storage, modify sign, salvage), the server validates the interaction distance. The distance check is implicit in the tryGetRegion call — the player must be within region range of the barricade.

Decay System

Barricades support an optional decay system:

  • Configurable timer + serverActiveDate triggers health reduction over time.
  • Only barricades placed on naturally-spawned objects may be eligible for decay.
  • The serverActiveDate static field records the server's active time for decay calculations.

Save/Load System

Save Version History

ConstantVersionChange
SAVEDATA_VERSION_INCLUDE_BUILD_ENUM18Added EBuild enum to fix state length issues (public issue #3725)
SAVEDATA_VERSION_REPLACE_EULER_ANGLES_WITH_QUATERNION19Replaced euler angle byte decomposition with quaternion serialization

Save Format

Each barricade in save data contains:

GUID (16 bytes)
position (Vector3: 12 bytes)   -- or vehicle parent ID
rotation (Quaternion: 16 bytes) -- compressed in latest version
state (byte array, variable)
owner (ulong: 8 bytes)
group (ulong: 8 bytes)
instanceID (uint: 4 bytes)
timestamp (uint: 4 bytes)
// Version-specific fields
build enum (byte)               -- SAVEDATA_VERSION >= 18

Region File Naming

TypePatternExample
World barricade regionBarricades_{x}_{y}.datBarricades_3_5.dat
Vehicle barricadeVehicles_{instanceID}_Barricades.datVehicles_42_Barricades.dat

Vehicle barricade files are identified by the vehicle's instanceID, ensuring each vehicle's barricades are loaded with the correct vehicle.

Barricade Save/Load Process

Barricades are saved per region. The server serializes each BarricadeRegion to a separate file. The client receives barricade data during initial replication via SendMultipleBarricades. Each barricade is serialized with full state including health and visibility.

Vehicle Barricade Save/Load

Barricades planted on vehicles are saved in separate vehicle region files. These are loaded during Level.isLoadingVehicles = false, after all vehicles have been instantiated. The uprootPlant method is called during vehicle destruction to detach and drop all barricades on the vehicle.

Instance ID Allocation

csharp
private static uint instanceCount;

The instanceCount is incremented for each new barricade placed. The counter is saved and loaded with the region data, ensuring unique IDs persist across server restarts.

Plugin Delegate Reference

DelegateSignatureFires
onDeployBarricadeRequested(Barricade, ItemBarricadeAsset, Transform, ref Vector3, ref float, ref float, ref float, ref ulong, ref ulong, ref bool)Before placement
onDamageBarricadeRequested(CSteamID, Transform, ref ushort, ref bool, EDamageOrigin)Before damage
OnRepairRequested(CSteamID, Transform, ref float, ref bool)Before repair
OnRepaired(CSteamID, Transform, float)After repair
onBarricadeSpawned(BarricadeRegion, BarricadeDrop)After spawn
onModifySignRequested(CSteamID, InteractableSign, ref string, ref bool)Before sign text change
onOpenStorageRequested(CSteamID, InteractableStorage, ref bool)Before storage open
onTransformRequested(CSteamID, byte, byte, ushort, uint, ref Vector3, ref byte, ref byte, ref byte, ref bool)Before transform change

NetId Rewrite — Migration from Index-Based to NetId-Based Addressing

BarricadeManager underwent a significant networking rewrite that moved from index-based addressing (byte x, byte y, ushort plant, ushort index) to NetId-based addressing. The signature change is visible in the obsolete delegates:

csharp
[System.Obsolete]
public void tellBarricadeOwnerAndGroup(CSteamID steamID, byte x, byte y,
    ushort plant, ushort index, ulong newOwner, ulong newGroup)
{
    throw new System.NotSupportedException("Moved into instance method as part of barricade NetId rewrite");
}

Modern methods use BarricadeDrop.SendOwnerAndGroup.InvokeAndLoopback(barricade.GetNetId(), ...).

Uprooting and Plant Removal

When a vehicle is destroyed or explodes, any barricades planted on it must be dropped:

csharp
BarricadeManager.trimPlant(vehicle.transform);

For trains, each car is individually processed:

csharp
if (vehicle.trainCars != null)
{
    for (int carIndex = 1; carIndex < vehicle.trainCars.Length; ++carIndex)
        BarricadeManager.uprootPlant(vehicle.trainCars[carIndex].root);
}

uprootPlant detaches the barricade from the vehicle, drops it as a world item, and removes the barricade from the vehicle region.

BarricadeColliders and Region Pending Destroy

csharp
private static List<BarricadeRegion> regionsPendingDestroy;
private static List<Collider> barricadeColliders;

The regionsPendingDestroy list holds regions queued for cleanup. The barricadeColliders list is shared across all barricade overlap checks to reduce allocation.

Interactable Barricade Type Hierarchy

BarricadeManager exposes server-authoritative methods for several interactable types:

Interactable TypeServer MethodPurpose
InteractableSignServerSetSignTextUpdate sign text (UTF-8 encoded in state bytes)
InteractableMannequinServerSetMannequinPoseChange mannequin pose
InteractableStorage(via onOpenStorageRequested)Validate storage access
InteractableTankServerSetAmountSet fuel/water level
InteractableStereoServerSetStereoTrackSet current music track GUID
InteractableFarm(via OnHarvestRequested_Global)Validate plant harvesting

Key Type References

  • BarricadeDrop — Runtime drop component wrapping the transform, serverside data, asset reference, and NetId
  • BarricadeData — Serverside state: barricade instance, position, rotation, owner, group, timestamp, instance ID
  • BarricadeRegion — Region container holding a List<BarricadeData> and a List<BarricadeDrop>
  • VehicleBarricadeRegion — Vehicle-attached region with parent transform and child barricade lists

Example: Placing a Barricade Programmatically

csharp
ItemBarricadeAsset asset = Assets.find(EAssetType.ITEM, barricadeID) as ItemBarricadeAsset;
if (asset == null) return;

Barricade barricade = new Barricade(asset);
ulong owner = player.channel.owner.playerID.steamID.m_SteamID;
ulong group = player.quests.groupID.m_SteamID;

BarricadeManager.dropBarricade(barricade, hitTransform, point,
    0, 90, 0, owner, group);

Example: Finding a Barricade by Transform

csharp
Transform barricadeRoot = /* transform from raycast or component reference */;
byte x, y;
ushort plant;
BarricadeRegion region;

if (BarricadeManager.tryGetRegion(barricadeRoot, out x, out y, out plant, out region))
{
    BarricadeDrop drop = region.FindBarricadeByRootTransform(barricadeRoot);
    if (drop != null)
    {
        ulong owner = drop.serversideData.owner;
        ushort health = drop.serversideData.barricade.health;
    }
}

Document history