BarricadeManager
Advanced60-90 minutesWindowsLinuxVisual StudioU3 SDK
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 ofBarricadeRegioninstances, each containingList<BarricadeData>(serverside state) andList<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:
NetIdRegistryprovides O(1) transform-to-drop lookup viaBarricadeDrop.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:
| Collection | Type | Purpose |
|---|---|---|
drops | List<BarricadeDrop> | Runtime component references (model transforms, interactables) |
barricades | List<BarricadeData> | Serverside state (owner, group, position, rotation, health, state bytes) |
Regions are initialized during awake():
- Creates the
regions[x, y]2D array sized byWORLD_SIZE. - Initializes each region's
dropslist andbarricadeslist. - Initializes
internalVehicleRegionsas an empty list. - Sets
backwardsCompatVehicleRegionscache 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:
| Event | Action |
|---|---|
| Vehicle spawned | No region yet — created lazily when first barricade is placed |
| Barricade placed on vehicle | Region created if absent; barricade added |
| Vehicle destroyed | trimPlant called to drop all barricades; region destroyed |
| Vehicle exploded | uprootPlant called for the main vehicle and all train cars |
Spatial Queries
Three getBarricadesInRadius overloads provide flexible spatial querying:
| Overload | Scope | Strategy |
|---|---|---|
(Vector3, float, List<RegionCoordinate>, List<Transform>) | World regions only | Iterates caller-provided region list, checks each barricade's model position against squared radius |
(Vector3, float, ushort plant, List<Transform>) | Single vehicle region | Queries 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:
- Region resolution: Determines the region coordinates from the placement point.
- Vehicle detection: Determines whether the barricade is vehicle-mounted (if the hit transform belongs to a vehicle).
- Server-side state creation: Creates
BarricadeDatawith owner, group, timestamp, and instance ID. - NetId allocation: Claims a
NetIdblock. - Prefab instantiation: Instantiates the barricade prefab and calls
IBarricadePlacedHandler.OnBarricadePlacedif the component implements the interface. - Network broadcast: Broadcasts
SendSingleBarricadeto relevant clients. - Event fire: Fires
onBarricadeSpawnedevent.
Placement Validation
Position validation checks:
- The position must be within level bounds (
Level.checkSafeIncludingClipVolumes). - The
EBuildtype determines valid placement surfaces (ground, wall, ceiling, etc.). - The position must not overlap with existing barricades or structures (checked by region overlap).
- For vehicle barricades: the
maxDistanceFromHullconfig 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:
- Hit detection determines the surface (ground, wall, vehicle).
- If the surface is a vehicle: assign to a
VehicleBarricadeRegion. Create one if the vehicle does not yet have a region. - If the surface is the ground: assign to the world
BarricadeRegionat the placement coordinates. - Validate placement constraints from the
ItemBarricadeAsset(e.g.,EBuildtype 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:
- Resolve the region and find the
BarricadeDropby root transform. - Check the asset's
canBeDamagedflag. - If
armoris true, multiplytimesby the config-defined armor multiplier. - Calculate
totalDamage = (ushort)(damage * times). - Fire
onDamageBarricadeRequestedfor plugin interception. - If allowed, call
barricade.serversideData.barricade.askDamage(totalDamage). - 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:
- Trigger
OnRepairRequestedwith the instigator, barricade transform, pending total healing, and allow flag. - If allowed, apply healing to the barricade's health.
- Fire
OnRepairedwith the instigator, transform, and total healing amount. - Broadcast the updated health via
sendHealthChanged.
Ownership and Group Management
changeOwnerAndGroup
csharp
public static void changeOwnerAndGroup(Transform transform, ulong newOwner, ulong newGroup)The method:
- Resolves the region and finds the drop.
- Broadcasts the new owner/group via
SendOwnerAndGroup. - Updates the serverside data.
- Calls
sendHealthChangedto 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 Type | State Layout |
|---|---|
InteractableSign | 16 bytes base + 1 byte text length + UTF-8 text |
InteractableStorage | 16 bytes base + item count + serialized items |
InteractableMannequin | 16 bytes base + pose byte + equipped item slots |
InteractableTank | 16 bytes base + 2 bytes (ushort) amount |
InteractableGenerator | 16 bytes base + fuel amount + wire status |
InteractableFarm | 16 bytes base + growth timer + fertilized flag |
InteractableSentry | 16 bytes base + targeting mode + ammo state |
InteractableTrap | 16 bytes base + armed flag + cooldown timer |
InteractableStereo | 16 bytes base + track GUID |
InteractableOven | 16 bytes base + cooking state |
InteractableRainBarrel | 16 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 +
serverActiveDatetriggers health reduction over time. - Only barricades placed on naturally-spawned objects may be eligible for decay.
- The
serverActiveDatestatic field records the server's active time for decay calculations.
Save/Load System
Save Version History
| Constant | Version | Change |
|---|---|---|
SAVEDATA_VERSION_INCLUDE_BUILD_ENUM | 18 | Added EBuild enum to fix state length issues (public issue #3725) |
SAVEDATA_VERSION_REPLACE_EULER_ANGLES_WITH_QUATERNION | 19 | Replaced 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 >= 18Region File Naming
| Type | Pattern | Example |
|---|---|---|
| World barricade region | Barricades_{x}_{y}.dat | Barricades_3_5.dat |
| Vehicle barricade | Vehicles_{instanceID}_Barricades.dat | Vehicles_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
| Delegate | Signature | Fires |
|---|---|---|
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 Type | Server Method | Purpose |
|---|---|---|
InteractableSign | ServerSetSignText | Update sign text (UTF-8 encoded in state bytes) |
InteractableMannequin | ServerSetMannequinPose | Change mannequin pose |
InteractableStorage | (via onOpenStorageRequested) | Validate storage access |
InteractableTank | ServerSetAmount | Set fuel/water level |
InteractableStereo | ServerSetStereoTrack | Set 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, andNetIdBarricadeData— Serverside state: barricade instance, position, rotation, owner, group, timestamp, instance IDBarricadeRegion— Region container holding aList<BarricadeData>and aList<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;
}
}