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, andloadingStepsfor progress updates. - 19-step coroutine: The
loading()coroutine withconst float STEPS = 19fprogress tracking. - Loading state flags: Six
isLoading*booleans gating gameplay systems until loading is complete. - Hash aggregation:
includeHash+combineHashesfor multi-subsystem integrity verification.
Level Size and Border
Size Table
| Size | size | border | Usable Area | World Regions |
|---|---|---|---|---|
| TINY | 512 | 16 | 480 × 480 | 1 × 1 |
| SMALL | 1024 | 64 | 960 × 960 | 2 × 2 |
| MEDIUM | 2048 | 64 | 1984 × 1984 | 4 × 4 |
| LARGE | 4096 | 64 | 4032 × 4032 | 8 × 8 |
| INSANE | 8192 | 128 | 8064 × 8064 | 16 × 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 margincheckSafeIncludingClipVolumes 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
| Delegate | When | Use Case |
|---|---|---|
onPrePreLevelLoaded | Before any subsystem loads | Global state reset, pre-load configuration |
loadingSteps | After each step | Loading screen progress bar update |
onPreLevelLoaded | After all subsystems loaded, before navmesh | Final plugin initialization |
onLevelLoaded | After navmesh build | Plugin level setup |
onPostLevelLoaded | After all hooks complete | Post-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)- Sets
_isEditor = false. - Stores the
LevelInfoand resolvesLevelAssetviagetAsset(). SceneManager.LoadScene("Game").PlayLevelLoadingScreenMusic()— starts loading screen music.- Resets
Provider.channelsandSteamworks.SteamFriends.SetRichPresence(). - Achievement unlock check for official map names.
DevkitTransactionManager.ResetTransactionCache().- Updates holiday redirect via
HolidayUtil.RefreshHolidayRedirect(). - Configures level batching via
shouldUseLevelBatchingandShouldSkipInstantiatingClutter.
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
| Field | Type | Purpose |
|---|---|---|
Version | string | Packed version string (major.minor.build.revision) |
Use_Legacy_Clip_Borders | bool | Legacy square border vs. clip volume checks |
Allow_Holiday_Redirect | bool | Whether holiday overlays are permitted |
Batching_Version | int | Batching format version (>1 enables batching) |
Enable_Clutter_Option | bool | Allows players to disable clutter |
Asset | GUID | LevelAsset reference |
Crafting_Blacklist / Crafting_Whitelist | GUID[] | Crafting permission filters |
Category | string[] | 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
| Subsystem | Hash Source | File |
|---|---|---|
| LevelObjects | SHA1 of binary save data | Objects.dat |
| LevelLighting | SHA1 of lighting config | Lighting.dat |
| LevelGround | Trees.dat SHA1 | Trees.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
| Aspect | Server | Client |
|---|---|---|
| Prefab instantiation | Dedicated server (low-poly) prefabs | Full visual prefabs |
| Foliage rendering | Not instantiated | GPU-instanced rendering |
| Object batching | Disabled | Enabled when Batching_Version > 1 |
| Clutter | Not instantiated | Optional per graphics settings |
| Navmesh | Baked and used for AI | Not received by clients |
| Lighting audio | Not played | Full audio ambiance |
| Water rendering | Not rendered | Reflection 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 gizmosDeprecated 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);
}