Interactable Safe — Lock System
Securing your bases, storage containers, and mannequins in Unturned depends on understanding how the lock system validates player access through ownership, group membership, and the isLocked flag. The Unturned SDK does not contain a standalone InteractableSafe class. Safe-style lockable storage is implemented through InteractableStorage (covered in the storage article) with the isLocked flag and a consistent lock-check pattern used across all interactable types. This article covers the lock behavior system: how isLocked is derived, the access validation pattern, server-side toggling, and the singleplayer bypass.
Source code location: Lock checks are distributed across InteractableStorage, InteractableMannequin, InteractableDoor, and other interactable classes. No single InteractableSafe.cs exists.
The isLocked Property
isLocked is always derived from the barricade asset:
csharp
isLocked = ((ItemBarricadeAsset)asset).isLocked;This is set in updateState() when the interactable is initialized or its state is updated. The isLocked flag comes from the barricade asset definition (ItemBarricadeAsset.isLocked), not from runtime state.
Access Validation Pattern
checkStore (Storage)
csharp
public bool checkStore(CSteamID enemyPlayer, CSteamID enemyGroup)
{
return (!isLocked || enemyPlayer == owner || (group != CSteamID.Nil && enemyGroup == group))
&& !isOpen;
}checkUpdate (Mannequin)
csharp
public bool checkUpdate(CSteamID enemyPlayer, CSteamID enemyGroup)
{
if (Provider.isServer && !Dedicator.IsDedicatedServer) return true; // SP bypass
return !isLocked || enemyPlayer == owner || (group != CSteamID.Nil && enemyGroup == group);
}checkDoor (InteractableDoor)
csharp
public bool checkDoor(CSteamID enemyPlayer, CSteamID enemyGroup)
{
// Similar pattern with isOpen guard
}Validation Rules
Access is granted if ANY of:
- The barricade is not locked (
!isLocked). - The accessing player is the owner (
enemyPlayer == owner). - The accessing player shares a group with the barricade (
group != CSteamID.Nil && enemyGroup == group).
Singleplayer Bypass
csharp
if (Provider.isServer && !Dedicator.IsDedicatedServer) return true;In singleplayer (server + no dedicated flag), all lock checks pass. This prevents locking yourself out of your own containers.
Server-Side Lock Toggle
csharp
BarricadeManager.ServerSetBarricadeLockedInternal(...)Lock state toggling is handled server-side through BarricadeManager. The server validates that the requesting player has ownership before toggling. The isLocked property on the asset definition is read-only at runtime — it determines the default locked state and the availability of the lock interaction.
Lock State and Storage
For storage-type interactables (InteractableStorage), there is an additional guard:
csharp
... && !isOpenLocked storage can be accessed by the owner/group, but only when the storage is closed. Once opened, no one else can open it.
Comparison: Lock Across Interactable Types
| Type | Lock Check Method | Additional Guard |
|---|---|---|
| Storage | checkStore | !isOpen |
| Mannequin | checkUpdate | SP bypass for singleplayer server |
| Door | checkDoor | !isOpen |
| Sign | Through BarricadeManager.onModifySignRequested | Text-only, no item access |
Lock Combo Validation
In legacy Unturned, safe-style locks used a three-digit combination system. Modern Unturned uses the isLocked boolean on the barricade asset and the ownership/group system for access control. The combo validation system is not present in the SDK's Interactable classes — it may be handled at a higher asset or UI level.
Key Design Insights
- No standalone safe class — Lock behavior is a cross-cutting concern applied consistently across interactable types.
- Asset-driven lock state —
isLockedcomes from the asset definition, not per-instance state. - Owner/group access pattern — Consistent three-condition check used across all interactables.
- Singleplayer bypass — Prevents lockout in singleplayer worlds.
Worked Code Example: Custom Lock Access Plugin
Lock Status Reporter
csharp
using SDG.Unturned;
using Steamworks;
using UnityEngine;
public class LockStatusPlugin
{
/// <summary>
/// Sends a chat message listing all barricades the player has locked within 50 meters.
/// </summary>
public static void ReportMyLocks(Player player)
{
Vector3 origin = player.transform.position;
int lockCount = 0;
foreach (BarricadeRegion region in BarricadeManager.regions.Values)
{
foreach (BarricadeDrop drop in region.drops)
{
if (Vector3.Distance(drop.model.position, origin) > 50f)
continue;
BarricadeData data = drop.GetServersideData();
if (data == null)
continue;
CSteamID owner = new CSteamID(data.owner);
if (owner != player.channel.owner.playerID.steamID)
continue;
ItemBarricadeAsset asset = drop.asset as ItemBarricadeAsset;
if (asset == null || !asset.isLocked)
continue;
lockCount++;
ChatManager.serverSendMessage(
$"[LOCK #{lockCount}] {asset.itemName} at {drop.model.position}",
Color.green,
toPlayer: player,
iconURL: null,
useRichTextFormatting: true
);
}
}
if (lockCount == 0)
{
ChatManager.serverSendMessage(
"No locked barricades found within 50m.",
Color.yellow,
toPlayer: player,
iconURL: null,
useRichTextFormatting: true
);
}
}
}Automated Unlock After Timeout
csharp
using SDG.Unturned;
using Steamworks;
using System.Collections;
using UnityEngine;
public class AutoUnlockPlugin : MonoBehaviour
{
private IEnumerator UnlockAfterDelay(
Transform barricadeTransform,
float delaySeconds,
CSteamID ownerId
)
{
yield return new WaitForSecondsRealtime(delaySeconds);
BarricadeDrop drop = BarricadeDrop.FindByRootFast(
DamageTool.getBarricadeRootTransform(barricadeTransform)
);
if (drop == null)
yield break;
BarricadeData data = drop.GetServersideData();
if (data == null || data.owner != (ulong)ownerId)
yield break;
BarricadeManager.ServerSetBarricadeLockedInternal(
barricadeTransform,
drop.instanceID,
newIsLocked: false,
shouldReplicate: true
);
ChatManager.serverSendMessage(
"Your barricade has been automatically unlocked.",
Color.yellow,
toPlayer: PlayerTool.getPlayer(ownerId),
iconURL: null,
useRichTextFormatting: true
);
}
}Programmatic Access Validation
csharp
public static bool HasAccessToBarricade(
ulong barricadeOwner,
ulong barricadeGroup,
bool isBarricadeLocked,
Player accessingPlayer
)
{
if (!isBarricadeLocked)
return true;
CSteamID playerId = accessingPlayer.channel.owner.playerID.steamID;
if (new CSteamID(barricadeOwner) == playerId)
return true;
CSteamID playerGroup = accessingPlayer.quests.groupID;
CSteamID barricadeGroupId = new CSteamID(barricadeGroup);
if (barricadeGroupId.m_SteamID != CSteamID.Nil.m_SteamID
&& playerGroup == barricadeGroupId)
{
return true;
}
return false;
}Mermaid Diagram: Lock Access Validation Flow
Comparison: Lock System vs. Related Access Control Systems
| Feature | Barricade Lock | Door Lock | Vehicle Lock | Storage Lock |
|---|---|---|---|---|
| Lock source | ItemBarricadeAsset.isLocked | Asset flag | VehicleManager.lockedOwner | Asset flag |
| Owner check | data.owner == playerId | Transform hierarchy lookup | lockedOwner == playerId | data.owner == playerId |
| Group check | data.group == playerGroup | Same pattern | Vehicle-specific | data.group == playerGroup |
| Open guard | !isOpen (storage) | !isOpen (door) | N/A | !isOpen |
| SP bypass | Provider.isServer && !Dedicated | Same pattern | Same pattern | Provider.isServer && !Dedicated |
| Toggle authority | BarricadeManager.ServerSetBarricadeLockedInternal | Door-specific manager | VehicleManager.ServerSetVehicleLock | BarricadeManager.ServerSetBarricadeLockedInternal |
| Legacy combo | No (modern boolean) | No | No | No (modern boolean) |
| Plugin hook | onModifySignRequested (signs) | Various | onToggleVehicleLocked | Various |
Failure Modes and Common Mistakes
Group assignment before lock — A common mistake: placing a barricade while not in a group, locking it, then joining a group and expecting group members to have access. The
groupfield is written at placement time, not at lock time. The barricade retains the group ID it was placed with. To grant group access, the barricade must be placed while in the group.Empty group ID comparisons — The code checks
group != CSteamID.Nilbefore comparing groups. If a barricade has no group (group = 0) but the player is in a group, access is denied. This is by design, but modders frequently miss theNilguard when writing custom lock checks.Singleplayer server edge case — On a server that is not dedicated,
Provider.isServer && !Dedicator.IsDedicatedServerevaluates to true. This means ALL lock checks pass for all players. A non-dedicated server effectively has no locks. Server hosts who run a listen server expecting locks to function will be surprised.Lock state desync after plugin modification — If a plugin directly modifies the
ItemBarricadeAsset.isLockedfield without callingBarricadeManager.updateState(), the lock state desynchronizes between server and clients. The server may deny access while clients show the barricade as unlocked (or vice versa).Race condition on take/drop interactions — A player can open a locked storage, take an item, and have another player close the storage behind them. The
!isOpenguard is checked once at interaction start, not continuously. A concurrent close operation defeats the lock temporarily.Missing ownership on server migration — If barricade data is migrated between server databases, the
ownerfield in the barricade state bytes must be preserved. If the owner is lost (set to 0), the barricade becomes permanently locked with no owner — only group members with matching group IDs can access it, and if the group is also 0, the barricade is effectively a world decoration.
How This Field Behaves Differently from the SDG Docs
The official SDG documentation and community wiki describe the lock system but several details differ in the actual SDK implementation:
SDG wiki describes "Safe" as a distinct barricade type. The community wiki and some developer notes refer to a "Safe" barricade with combo lock validation. In the current SDK, there is no
InteractableSafeclass and no combo validation code. TheisLockedboolean onItemBarricadeAssetis the sole lock mechanism. The combo-lock legacy system is not present in the SDK codebase.SDG docs mention lock ownership as per-instance runtime state. Some documentation suggests
isLockedis toggled at runtime and stored per-instance. In the SDK,isLockedis a property of the asset definition (ItemBarricadeAsset.isLocked). It is read from the barricade's .dat file at asset load time and cannot be toggled by players without admin commands or plugins. The runtime toggle is handled server-side throughServerSetBarricadeLockedInternal, not through modifying the asset property directly.SDG docs imply group inheritance from placement. Documentation sometimes states that group assignment "inherits" from the player's group at the time of locking. In the SDK, group assignment occurs at placement time, not lock time. The group value is written to the barricade state bytes when the barricade is placed. Locking is a separate operation that toggles the
isLockedflag only — it does not refresh the group assignment.SDG docs state the "open" guard applies to all interactables. The
!isOpencheck is specific to storage-type interactables (InteractableStorage) and door-type interactables (InteractableDoor). Mannequin lock checks (checkUpdate) do not have an!isOpenguard. This distinction is not clarified in the community documentation.
Performance Considerations
Lock Check Cost
The lock check pattern is O(1) — it compares a fixed number of fields (owner, group, isLocked flag) with no iteration or lookup. This makes it effectively free in terms of CPU time.
BarricadeManager State Overhead
Each barricade with a lock stores an additional 16 bytes in its state:
- Owner CSteamID: 8 bytes
- Group CSteamID: 8 bytes
With 100,000 locked barricades across all regions, this is ~1.5 MB of additional state storage — negligible.
Network Synchronization
Lock state changes are broadcast via BarricadeManager.updateState() as part of the full barricade state block. A lock toggle sends the entire state, not just the lock flag. This means a lock toggle on a complex barricade (e.g., a mannequin with full clothing) re-sends up to 255 bytes per toggle. Rate-limiting lock toggles in plugin code is recommended to avoid bandwidth spikes.
Deeper FAQ
Q: Can I lock a barricade that was placed by another player?
No. ServerSetBarricadeLockedInternal validates that the requesting player matches the barricade's owner. Without ownership, the server rejects the lock toggle. Admins can bypass this via the Permissions system (STEAM_ADMIN), which allows overriding ownership checks in the BarricadeManager request handlers.
Q: What happens to locked barricades when a player leaves a group?
The barricade's group field is immutable after placement. If a player places a barricade while in Group A, then leaves Group A, the barricade still has Group A's ID. Members of Group A retain access even after the original owner leaves. This can be exploited: a malicious player can place locked barricades in a group base, leave the group, and the group retains access — but the barricades block the original owner's replacements.
Q: Can plugins add custom lock validation logic?
Yes. Plugins can intercept the interaction request handlers in InteractableStorage, InteractableDoor, InteractableMannequin, and InteractableSign using Harmony patches on checkStore, checkDoor, checkUpdate, and similar methods. Adding an additional validation condition (e.g., "player must be on a whitelist") is a common plugin pattern.
Q: Are locks persisted across server restarts?
Yes. The lock state (owner and group CSteamIDs) is stored in the barricade's state byte array, which is serialized to the save file in Level/Barricades/. On server restart, barricades are deserialized with their lock state intact. However, if the save file is corrupted or the barricade's state bytes are truncated, the lock data may be lost, defaulting to owner=0, group=0, which effectively unlocks the barricade.
Q: Can I lock a barricade that doesn't natively support locking?
All barricades inherit the lock system from ItemBarricadeAsset.isLocked. The isLocked property is defined per barricade asset in its .dat file with the Locked key. A barricade without Locked in its .dat is permanently unlocked and cannot be locked at runtime through conventional means. A plugin can call ServerSetBarricadeLockedInternal to force-lock any barricade regardless of its asset setting.
Cross-References
- Interactable Storage Container System —
checkStorevalidation pattern and the!isOpenguard specific to storage containers. - Interactable Mannequin — Clothing Display — Mannequin lock behavior via
checkUpdatewith singleplayer bypass. - Commands — Permissions / Whitelist / Admin System — Admin permission overrides for ownership-protected interactions.
- Barricade Manager —
ServerSetBarricadeLockedInternaland barricade state management.
