Skip to content

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:

  1. The barricade is not locked (!isLocked).
  2. The accessing player is the owner (enemyPlayer == owner).
  3. 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
... && !isOpen

Locked 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

TypeLock Check MethodAdditional Guard
StoragecheckStore!isOpen
MannequincheckUpdateSP bypass for singleplayer server
DoorcheckDoor!isOpen
SignThrough BarricadeManager.onModifySignRequestedText-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

  1. No standalone safe class — Lock behavior is a cross-cutting concern applied consistently across interactable types.
  2. Asset-driven lock stateisLocked comes from the asset definition, not per-instance state.
  3. Owner/group access pattern — Consistent three-condition check used across all interactables.
  4. 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

FeatureBarricade LockDoor LockVehicle LockStorage Lock
Lock sourceItemBarricadeAsset.isLockedAsset flagVehicleManager.lockedOwnerAsset flag
Owner checkdata.owner == playerIdTransform hierarchy lookuplockedOwner == playerIddata.owner == playerId
Group checkdata.group == playerGroupSame patternVehicle-specificdata.group == playerGroup
Open guard!isOpen (storage)!isOpen (door)N/A!isOpen
SP bypassProvider.isServer && !DedicatedSame patternSame patternProvider.isServer && !Dedicated
Toggle authorityBarricadeManager.ServerSetBarricadeLockedInternalDoor-specific managerVehicleManager.ServerSetVehicleLockBarricadeManager.ServerSetBarricadeLockedInternal
Legacy comboNo (modern boolean)NoNoNo (modern boolean)
Plugin hookonModifySignRequested (signs)VariousonToggleVehicleLockedVarious

Failure Modes and Common Mistakes

  1. 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 group field 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.

  2. Empty group ID comparisons — The code checks group != CSteamID.Nil before 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 the Nil guard when writing custom lock checks.

  3. Singleplayer server edge case — On a server that is not dedicated, Provider.isServer && !Dedicator.IsDedicatedServer evaluates 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.

  4. Lock state desync after plugin modification — If a plugin directly modifies the ItemBarricadeAsset.isLocked field without calling BarricadeManager.updateState(), the lock state desynchronizes between server and clients. The server may deny access while clients show the barricade as unlocked (or vice versa).

  5. 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 !isOpen guard is checked once at interaction start, not continuously. A concurrent close operation defeats the lock temporarily.

  6. Missing ownership on server migration — If barricade data is migrated between server databases, the owner field 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 InteractableSafe class and no combo validation code. The isLocked boolean on ItemBarricadeAsset is 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 isLocked is toggled at runtime and stored per-instance. In the SDK, isLocked is 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 through ServerSetBarricadeLockedInternal, 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 isLocked flag only — it does not refresh the group assignment.

  • SDG docs state the "open" guard applies to all interactables. The !isOpen check is specific to storage-type interactables (InteractableStorage) and door-type interactables (InteractableDoor). Mannequin lock checks (checkUpdate) do not have an !isOpen guard. 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

Document history