Skip to content

Level Loading System

The Level class (2419 lines in Unturned/Level/Level.cs) is the central coordinator for all level lifecycle in the Unturned SDK. It manages the 19-step loading sequence, level metadata (size/border/type), hash verification across 10+ subsystems, LevelInfo construction from Level.dat and Config.json, LevelAsset resolution with context tags, the save pipeline across 14 subsystems, and the editor vs. play mode distinction. Level is a MonoBehaviour-based singleton that coordinates loading through coroutines, five static event delegates, and direct file I/O.

Source code location: Unturned/Level/Level.cs

Architecture Overview

Level is not a SteamCaller subclass. It serves as the root coordinator driving loading across 14+ subsystems. Key architectural components:

  • Five event delegates: onPrePreLevelLoaded, onPreLevelLoaded, onLevelLoaded, onPostLevelLoaded, and loadingSteps for progress updates.
  • 19-step coroutine: The loading() coroutine with const float STEPS = 19f progress tracking.
  • Loading state flags: Six isLoading* booleans gating gameplay systems until loading is complete.
  • Hash aggregation: includeHash + combineHashes for multi-subsystem integrity verification.

Level Size and Border

Size Table

SizesizeborderUsable AreaWorld Regions
TINY51216480 × 4801 × 1
SMALL102464960 × 9602 × 2
MEDIUM2048641984 × 19844 × 4
LARGE4096644032 × 40328 × 8
INSANE81921288064 × 806416 × 16

Height Bounds

csharp
public const float HEIGHT = 1024f;   // Max vertical range
public const float TERRAIN = 256f;   // Terrain height range
public const ushort CLIP = 8;        // Clip volume margin

checkSafeIncludingClipVolumes checks both the legacy square border bounds and (if Use_Legacy_Clip_Borders is false) the PlayerClipVolumeManager volume system. Points outside ±1024 Y or beyond the usable area are invalid.

csharp
public static bool checkSafeIncludingClipVolumes(Vector3 point)
{
    // Legacy border check
    float halfSize = size / 2f;
    float margin = border;
    if (Mathf.Abs(point.x) > halfSize - margin ||
        Mathf.Abs(point.y) > HEIGHT ||
        Mathf.Abs(point.z) > halfSize - margin)
        return false;

    // Clip volume check
    if (!Use_Legacy_Clip_Borders && PlayerClipVolumeManager.IsPointInsideClipVolume(point))
        return false;

    return true;
}

The 19-Step Loading Pipeline

Pipeline Structure

csharp
private IEnumerator loading()
{
    const float STEPS = 19f;
    int currentStep = 0;

    // Step 0: Pre-initialization
    onPrePreLevelLoaded?.Invoke(currentStep);
    loadingSteps?.Invoke($"Initializing...");
    yield return null;

    // Steps 1-2: Terrain
    LevelGround.load();
    loadingSteps?.Invoke($"Loading terrain ({++currentStep}/{STEPS})");
    yield return null;

    // Steps 3-4: Objects
    LevelObjects.load();
    loadingSteps?.Invoke($"Loading objects ({++currentStep}/{STEPS})");
    yield return null;

    // Steps 5-6: Lighting
    LevelLighting.load();
    loadingSteps?.Invoke($"Loading lighting ({++currentStep}/{STEPS})");
    yield return null;

    // Steps 7-8: Roads
    LevelRoads.load();
    loadingSteps?.Invoke($"Loading roads ({++currentStep}/{STEPS})");
    yield return null;

    // Steps 9-10: Navigation
    LevelNavigation.load();
    loadingSteps?.Invoke($"Loading navigation ({++currentStep}/{STEPS})");
    yield return null;

    // Steps 11-12: Nodes
    LevelNodes.load();
    loadingSteps?.Invoke($"Loading nodes ({++currentStep}/{STEPS})");
    yield return null;

    // Steps 13-14: Items + Players
    LevelItems.load();
    LevelPlayers.load();
    loadingSteps?.Invoke($"Loading spawns ({++currentStep}/{STEPS})");
    yield return null;

    // Steps 15-16: Spawn tables
    LevelZombies.load();
    LevelVehicles.load();
    LevelAnimals.load();
    loadingSteps?.Invoke($"Loading creatures ({++currentStep}/{STEPS})");
    yield return null;

    // Step 17: Visibility + Batching
    LevelVisibility.load();
    LevelBatching.load();
    loadingSteps?.Invoke($"Loading visibility ({++currentStep}/{STEPS})");
    yield return null;

    // Step 18: Navmesh build
    UnturnedPathfinding.BuildNavmesh();
    loadingSteps?.Invoke($"Building navigation ({++currentStep}/{STEPS})");
    yield return null;

    // Step 19: Final hooks
    onPreLevelLoaded?.Invoke(currentStep);
    onLevelLoaded?.Invoke(currentStep);
    onPostLevelLoaded?.Invoke(currentStep);
}

Event Delegate Firing Order

DelegateWhenUse Case
onPrePreLevelLoadedBefore any subsystem loadsGlobal state reset, pre-load configuration
loadingStepsAfter each stepLoading screen progress bar update
onPreLevelLoadedAfter all subsystems loaded, before navmeshFinal plugin initialization
onLevelLoadedAfter navmesh buildPlugin level setup
onPostLevelLoadedAfter all hooks completePost-load UI, achievement triggers

Loading State Flags

csharp
public static bool isLoadingContent = true;
public static bool isLoadingLighting = true;
public static bool isLoadingVehicles = true;
public static bool isLoadingBarricades = true;
public static bool isLoadingStructures = true;
public static bool isLoadingArea = true;

public static bool isLoading
{
    get
    {
        if (!Provider.isServer || Dedicator.IsDedicatedServer)
            return isLoadingContent;
        return isLoadingContent || isLoadingLighting || isLoadingVehicles ||
               isLoadingBarricades || isLoadingStructures || isLoadingArea;
    }
}

Each flag is cleared when its corresponding system finishes loading. The aggregate isLoading property gates gameplay systems — spawn protection, damage immunity, etc.

Level Entry

Level.load (Play Mode)

csharp
Level.load(LevelInfo newInfo, bool hasAuthority)
  1. Sets _isEditor = false.
  2. Stores the LevelInfo and resolves LevelAsset via getAsset().
  3. SceneManager.LoadScene("Game").
  4. PlayLevelLoadingScreenMusic() — starts loading screen music.
  5. Resets Provider.channels and Steamworks.SteamFriends.SetRichPresence().
  6. Achievement unlock check for official map names.
  7. DevkitTransactionManager.ResetTransactionCache().
  8. Updates holiday redirect via HolidayUtil.RefreshHolidayRedirect().
  9. Configures level batching via shouldUseLevelBatching and ShouldSkipInstantiatingClutter.

Level.edit (Editor Mode)

Level.edit(LevelInfo newInfo) — Same as load() but sets _isEditor = true and loads editor-specific scenes and tools.

LevelInfo Construction

csharp
private static LevelInfo ReadLevelInfo(string directoryPath, ulong publishedFileId)
{
    string datPath = directoryPath + "/Level.dat";
    using (Block block = ReadBlock(datPath))
    {
        byte version = block.readByte();
        byte[] hash = block.readByteArray();
        CSteamID creator = new CSteamID(block.readUInt64());
        ELevelSize size = (ELevelSize)block.readByte();
        ELevelType type = version > 1 ? (ELevelType)block.readByte() : ELevelType.SURVIVAL;

        bool isEditable = (creator == Provider.client || File.Exists(directoryPath + "/.unlocker"));

        string configPath = directoryPath + "/Config.json";
        LevelInfoConfigData configData = JsonConvert.DeserializeObject<LevelInfoConfigData>(
            File.ReadAllText(configPath));

        return new LevelInfo(directoryPath, Path.GetFileName(directoryPath),
            size, type, isEditable, configData, publishedFileId, hash);
    }
}

Config.json Fields

FieldTypePurpose
VersionstringPacked version string (major.minor.build.revision)
Use_Legacy_Clip_BordersboolLegacy square border vs. clip volume checks
Allow_Holiday_RedirectboolWhether holiday overlays are permitted
Batching_VersionintBatching format version (>1 enables batching)
Enable_Clutter_OptionboolAllows players to disable clutter
AssetGUIDLevelAsset reference
Crafting_Blacklist / Crafting_WhitelistGUID[]Crafting permission filters
Categorystring[]Server browser category tags

LevelAsset System

csharp
public static LevelAsset getAsset()
{
    if (!didResolveLevelAsset)
    {
        if (info.configData.Asset.isValid)
            cachedLevelAsset = Assets.find(info.configData.Asset);

        if (cachedLevelAsset == null)
            cachedLevelAsset = Assets.find(LevelAsset.defaultLevel);

        // Build resolvedTags list
        resolvedTags = new List<string>(cachedLevelAsset.Tags);

        // Context tag: singleplayer
        if (Provider.isServer && !Dedicator.IsDedicatedServer)
            resolvedTags.Add("Singleplayer");

        // Context tag: building in safezones allowed (SP only)
        if (Provider.isServer && !Dedicator.IsDedicatedServer)
            resolvedTags.Add("BuildingInSafezonesAllowed");

        // Context tag: not singleplayer
        if (Dedicator.IsDedicatedServer)
            resolvedTags.Add("NotSingleplayer");

        didResolveLevelAsset = true;
    }
    return cachedLevelAsset;
}

Tags are used by CraftingTagProvider for recipe availability and by safezone/building permission checks.

Level Hash Verification

csharp
private static List<byte[]> pendingHashes = new List<byte[]>();

public static void includeHash(string id, byte[] pendingHash)
{
    pendingHashes.Add(pendingHash);
}

private static void combineHashes()
{
    hash = Hash.combine(pendingHashes);
    pendingHashes.Clear();
}

Subsystem Hash Sources

SubsystemHash SourceFile
LevelObjectsSHA1 of binary save dataObjects.dat
LevelLightingSHA1 of lighting configLighting.dat
LevelGroundTrees.dat SHA1Trees.dat

The combined hash is sent to clients during connection. If the client's computed hash doesn't match, the connection is terminated with message "You have been kicked for having modified level files."

Debugging with -LogLevelHash

The -LogLevelHash command-line flag enables per-file hash logging:

Level hash: Objects.dat = ABCD1234...
Level hash: Lighting.dat = EFAB5678...
Level hash: Trees.dat = 9C0DEF12...
Combined hash: 1A2B3C4D...

This output helps diagnose hash mismatch kicks when multiple players report the same issue.

Level Save Pipeline

csharp
public static void save()
{
    DirtyManager.save();
    LevelObjects.save();
    LevelLighting.save();
    LevelGround.save();
    LevelRoads.save();
    LevelNavigation.save();
    LevelNodes.save();
    LevelItems.save();
    LevelPlayers.save();
    LevelZombies.save();
    LevelVehicles.save();
    LevelAnimals.save();
    LevelVisibility.save();
    Editor.save();

    combineHashes();
}

Server vs. Client Loading

AspectServerClient
Prefab instantiationDedicated server (low-poly) prefabsFull visual prefabs
Foliage renderingNot instantiatedGPU-instanced rendering
Object batchingDisabledEnabled when Batching_Version > 1
ClutterNot instantiatedOptional per graphics settings
NavmeshBaked and used for AINot received by clients
Lighting audioNot playedFull audio ambiance
Water renderingNot renderedReflection camera + surface rendering

Loading Screen Music

csharp
private static void PlayLevelLoadingScreenMusic()
{
    LevelAsset asset = getAsset();
    if (asset.loadingScreenMusic != null)
    {
        musicAudioSource.clip = asset.loadingScreenMusic;
        musicAudioSource.Play();
    }
    musicOutroClip = asset.loadingScreenMusicOutro;
    musicOutroVolume = asset.loadingScreenMusicVolume;
}

Level Hierarchy Transforms

csharp
private static Transform _level;   // Root of all level objects
private static Transform _roots;   // Editor-placed object hierarchy
private static Transform _clips;   // Player clip volume objects
private static Transform _editing; // Editor-only editing gizmos

Deprecated transforms (effects and spawns) exist only as lazy-created objects with warnings:

csharp
[Obsolete]
public static Transform effects
{
    get
    {
        if (_effects == null)
        {
            _effects = new GameObject("effects").transform;
            _effects.parent = _level;
            UnturnedLog.warning("Level.effects is deprecated");
        }
        return _effects;
    }
}

Level Creation from Template

csharp
public static void add(string name, ELevelSize size, ELevelType type)
{
    string source = ReadWrite.PATH + "/LevelTemplate";
    string dest = ReadWrite.PATH + "/Maps/" + name;

    // Copy all template files
    DirectoryCopy(source, dest);

    // Write minimal Level.dat
    using (Block block = new Block())
    {
        block.writeByte(2); // version
        block.writeHash(Hash.SHA1(new byte[0]));
        block.writeUInt64(Provider.client.m_SteamID);
        block.writeByte((byte)size);
        block.writeByte((byte)type);
        File.WriteAllBytes(dest + "/Level.dat", block.getBytes());
    }

    // Create Charts.unity3d (satellite texture placeholder)
    // Create terrain directories
    Directory.CreateDirectory(dest + "/Terrain");
    Directory.CreateDirectory(dest + "/Environment");
    Directory.CreateDirectory(dest + "/Spawns");
}

Level Exit and Cleanup

csharp
public static void exit()
{
    onLevelExited?.Invoke();

    if (musicAudioSource != null)
    {
        musicAudioSource.Stop();
        musicAudioSource.clip = null;
    }

    _info = null;
    didResolveLevelAsset = false;
    cachedLevelAsset = null;
    resolvedTags?.Clear();

    SceneManager.LoadScene("MainMenu");
}

Satellite Capture System

csharp
private static Camera satelliteCaptureCamera;
private static RenderTexture satelliteCaptureTexture;

private static IEnumerator CaptureSatelliteView()
{
    satelliteCaptureCamera.transform.position = new Vector3(0, HEIGHT - 1, 0);
    satelliteCaptureCamera.transform.rotation = Quaternion.Euler(90, 0, 0);

    yield return new WaitForEndOfFrame();

    // Render satellite view to texture
    satelliteCaptureCamera.targetTexture = satelliteCaptureTexture;
    satelliteCaptureCamera.Render();

    // Extract pixels for chart texture
    Texture2D chart = new Texture2D(1024, 1024);
    chart.ReadPixels(new Rect(0, 0, 1024, 1024), 0, 0);
    chart.Apply();

    // Save to Charts.unity3d
    // ...
}

KnownLevels Scan Implementation

csharp
public static void ScanKnownLevels()
{
    knownLevels.Clear();

    // 1. Local maps
    foreach (string dir in Directory.GetDirectories(ReadWrite.PATH + "/Maps"))
    {
        string datPath = dir + "/Level.dat";
        if (File.Exists(datPath))
            knownLevels.Add(ReadLevelInfo(dir, 0));
    }

    // 2. Workshop content
    string workshopDir = SteamApps.AppInstallDir() + "/workshop/content/304930";
    if (Directory.Exists(workshopDir))
    {
        foreach (string dir in Directory.GetDirectories(workshopDir))
        {
            string datPath = dir + "/Level.dat";
            if (File.Exists(datPath))
            {
                ulong fileId = ulong.Parse(Path.GetFileName(dir));
                knownLevels.Add(ReadLevelInfo(dir, fileId));
            }
        }
    }

    // 3. Curated maps (bundled with game)
    string curatedDir = ReadWrite.PATH + "/CuratedMaps";
    if (Directory.Exists(curatedDir))
    {
        foreach (string dir in Directory.GetDirectories(curatedDir))
        {
            string datPath = dir + "/Level.dat";
            if (File.Exists(datPath))
                knownLevels.Add(ReadLevelInfo(dir, 0));
        }
    }
}

Workshop Level Resolution

csharp
public static void UpdateLevelReference(ref LevelInfo levelInfo)
{
    // Rescan if level was removed (unsubscribed workshop item)
    if (!File.Exists(levelInfo.path + "/Level.dat"))
    {
        ScanKnownLevels();
        foreach (LevelInfo known in knownLevels)
        {
            if (known.publishedFileId == levelInfo.publishedFileId)
            {
                levelInfo = known;
                return;
            }
        }
        levelInfo = null; // Level gone
    }
}

Example: Waiting for Level Load

csharp
private IEnumerator WaitForLevelLoad()
{
    while (Level.isLoading)
        yield return null;

    // All subsystems loaded, safe to access
    Vector3 spawnPoint = LevelPlayers.getRandomSpawnPoint();
    Player.player.teleportToLocationUnsafe(spawnPoint, 0f);
}

Document history