Skip to content

Asset Base Class and the Asset Registry

The Asset abstract base class is the root of Unturned's entire content system. Every gameplay-defining object — items, vehicles, effects, objects, NPCs, spawn tables, road definitions, weather profiles, and animations — is represented by a class that inherits from Asset. The Assets singleton manages the master registry that maps GUIDs and legacy IDs to their corresponding asset instances, orchestrates the multi-threaded loading pipeline, resolves master bundle references, and provides the lookup API used by every subsystem at runtime.

This article documents the Asset base class fields and methods, the Assets singleton's architecture including the dual mapping system (GUID → asset and legacy ID → asset), the AssetsWorker multi-threaded loading pipeline, the PopulateAsset lifecycle method, the TypeRegistryDictionary that maps string type names to asset classes, the master bundle resolution system (MasterBundleConfig), and the asset error reporting infrastructure.

Source code location: Unturned/Bundles/Asset.cs, Unturned/Bundles/Assets.cs, Unturned/Bundles/AssetsWorker.cs, Unturned/Bundles/AssetReference.cs

The Asset Base Class

Asset is defined in SDG.Unturned and implements IAssetErrorContext. Every asset in the game — regardless of type — carries these fields:

FieldTypeSet byPurpose
namestringConstructor / PopulateAssetInternal asset name
idushortPopulateAssetLegacy 16-bit numeric ID
GUIDGuidPopulateAsset128-bit globally unique identifier
originAssetOriginPopulateAssetSource of the asset (core, workshop, map)
hashbyte[]PopulateAssetSHA1 hash of the asset's input file
originMasterBundleMasterBundleConfigPopulateAssetMaster bundle this asset was loaded from
absoluteOriginFilePathstringLoadFileFull path to the asset's source file
hasBeenReplacedboolAddToMappingTrue if another asset with same ID/GUID replaced this one
hasErrorsboolPopulateAssetTrue if any errors were reported during loading
requiredShaderUpgradeboolLoadFileTrue if shaders were converted to Standard
ignoreNPOTboolPopulateAssetSkip non-power-of-two texture warnings
ignoreTextureReadableboolPopulateAssetSkip read/write texture warnings
OriginParsedDataIDatDictionaryWhen resaving enabledOriginal file contents for re-serialization
LocalizationLocalWhen Keep_Localization_Loaded is trueTranslation data for the asset

Constructor

The base constructor sets name = GetType().Name — meaning if no name override is provided, the asset's name defaults to its C# class name (e.g., "ItemGunAsset"). Subclass constructors can override this.

PopulateAsset

PopulateAsset is the core initialization method. It is called by the Assets loading pipeline after the asset class is instantiated but before it is registered in the mapping. The method receives a PopulateAssetParameters struct:

csharp
public struct PopulateAssetParameters
{
    public Bundle bundle;       // The bundle containing this asset's prefab
    public IDatDictionary data; // Parsed key-value data from the .dat file
    public Local localization;  // Translation data
    public bool CanPerformDataConversions; // True if re-saving is allowed
}

The base Asset.PopulateAsset implementation:

  1. Sets name from the bundle name, or falls back to "Asset_" + id
  2. Reads the originMasterBundle from the bundle if it is a MasterBundle
  3. Reads Ignore_NPOT and Ignore_TexRW flags from the data dictionary

Subclasses override PopulateAsset to read their type-specific fields. The call chain is:

Asset.PopulateAsset (base)
  └─ ItemAsset.PopulateAsset (adds item-specific fields)
       └─ ItemWeaponAsset.PopulateAsset (adds weapon fields)
            └─ ItemGunAsset.PopulateAsset (adds gun-specific fields)

Error Reporting

The IAssetErrorContext interface provides the error reporting contract:

csharp
public interface IAssetErrorContext
{
    string AssetErrorPrefix { get; }  // Formatted string: "Origin FriendlyName (Type) [GUID]"
    void ReportAssetError(string message);
}

The AssetErrorPrefix implementation produces strings like: "Vanilla Eaglefire (Gun) [b03d581a5c1a490f995f8deba57b0f17]"

When an error is reported, asset.HasErrors is set to true and the error is logged via UnturnedLog.warn.

AssetReference<T>

The AssetReference<T> struct is a type-safe GUID reference to any asset. It wraps a single Guid and provides Find() which delegates to Assets.find(this).

csharp
public struct AssetReference<T> where T : Asset
{
    public Guid GUID;
    public bool isValid => GUID != Guid.Empty;
    public T Find(); // Resolves via Assets.find
}

The struct uses the GUID-based lookup exclusively. It does not store a cached reference, so every Find() call performs a dictionary lookup. When newer code needs to cache the result, CachingAssetRef<T> is recommended instead.

Parse support is provided for both inline GUID values and nested dictionary GUIDs:

In .dat:  "MyAsset b03d581a5c1a490f995f8deba57b0f17"
In .dat:  "MyAsset {GUID b03d581a5c1a490f995f8deba57b0f17}"

The Assets Singleton

Assets is a MonoBehaviour that serves as the global asset manager. It owns the asset mapping dictionaries, manages the loading pipeline, provides lookup methods, and tracks loading progress.

Dual Mapping Architecture

Assets are stored in two parallel data structures within AssetMapping:

csharp
internal class AssetMapping
{
    public Dictionary<EAssetType, Dictionary<ushort, Asset>> legacyAssetsTable;
    public Dictionary<Guid, Asset> assetDictionary;
    public List<Asset> assetList;
}
  1. GUID dictionary (assetDictionary) — the primary lookup by 128-bit GUID. Every registered asset is stored here.
  2. Legacy ID table (legacyAssetsTable) — a tiered lookup by EAssetTypeushort. Only assets with a non-zero id are stored here. Used for backward compatibility with code that references assets by 16-bit numeric ID.
  3. Flat list (assetList) — an ordered list of all assets, used for iteration.

There are two AssetMapping instances:

MappingPurpose
defaultAssetMappingContains all assets loaded from disk. Used in singleplayer and the level editor.
currentAssetMappingIn singleplayer, same as defaultAssetMapping. When playing on a server, a subset of assets based on server workshop files.

The ApplyServerAssetMapping method constructs currentAssetMapping from origins in a specific order: core → level → server workshop files → remaining origins. On dedicated servers, remaining non-core origins are inserted at the front to reduce ID conflict chances.

Type Registry Dictionaries

The Assets.assetTypes and Assets.useableTypes are TypeRegistryDictionary instances that map string names to System.Type:

csharp
Assets.assetTypes.addType("Gun", typeof(ItemGunAsset));
Assets.assetTypes.getType("Gun"); // Returns typeof(ItemGunAsset)

The type registry is populated by UnturnedNexus.initialize() and is used by the asset parser (LoadFile method) to determine which class to instantiate when parsing a .dat file:

csharp
string legacyType = data.GetString("Type");
assetType = assetTypes.getType(legacyType);

Find Methods

The lookup methods support RedirectorAsset chaining — when a lookup hits a RedirectorAsset, it follows the redirect chain up to 32 hops before giving up:

csharp
public static Asset find(Guid GUID)  // Primary GUID lookup
public static T find<T>(Guid guid)   // Type-safe GUID lookup
public static T find<T>(AssetReference<T> reference) // Reference wrapper
public static Asset find(EAssetType type, ushort id) // Legacy ID lookup
public static void find<T>(List<T> results) // Filter all assets by type

The redirectCount limiter at 32 prevents infinite redirect loops from corrupted or malicious asset files.

Master Bundle Resolution

The findMasterBundleByPath method finds the most specific master bundle for a given asset file path:

csharp
public static MasterBundleConfig findMasterBundleByPath(string path)

It checks all loaded master bundles and selects the one with the longest matching directory prefix. For example, if two bundles are registered at "Items" and "Items/Guns", an asset at "Items/Guns/Eaglefire/Eaglefire.asset" matches "Items/Guns" because it has a longer path.

Asset files can also override their master bundle with the Master_Bundle_Override field in the .dat, or exclude themselves from master bundle usage with Exclude_From_Master_Bundle.

The AssetsWorker — Multi-Threaded Loading

The AssetsWorker class handles file discovery and parsing on background threads, while the main thread handles type instantiation and asset population.

Worker Architecture

AssetsWorker

      ├── Main thread calls RequestSearch(path, origin)
      │    └── Queues a worker thread state

      ├── SearcherThreadMain (ThreadPool thread)
      │    └── Recursively scans directories:
      │         ├── Looks for MasterBundle.dat
      │         ├── Looks for *.asset or *.dat files
      │         └── Enqueues ReaderWorkItem for each found file

      ├── ReaderThreadMain (ThreadPool thread, async)
      │    └── Dequeues ReaderWorkItem:
      │         ├── MasterBundle item:
      │         │    ├── Read MasterBundle.dat → parse config
      │         │    ├── Read .unity3d bytes + compute SHA1 hash
      │         │    └── Enqueue MasterBundle result
      │         │
      │         └── Asset item:
      │              ├── Read .asset/.dat file
      │              ├── Parse via DatParser
      │              ├── Read localization files (English.dat / language.dat)
      │              └── Enqueue AssetDefinition result

      └── Main thread Update() drains results:
           ├── MasterBundle result → register bundle, load asynchronously
           └── AssetDefinition result → instantiate, PopulateAsset, register

Seacher Thread File Discovery

The searcher thread enumerates directories recursively. For each directory, it calls FindAssets() which checks for asset files in priority order:

  1. <FolderName>.asset — highest priority, single asset per folder
  2. <FolderName>.dat — old format, same folder structure
  3. Asset.dat — default asset name
  4. *.asset — folder with multiple assets

The search is breadth-first (using a Queue<string>). The thread does not sleep while work is available; it loops at maximum speed until the directory tree is fully enumerated.

Reader Thread

The reader thread processes ReaderWorkItem objects asynchronously using await. For each file:

  • Master bundles: Reads MasterBundle.dat (JSON configuration), then reads the .unity3d file bytes into memory with SHA1 hashing via SHA1Stream.
  • Assets: Reads the .asset or .dat file, parses it with DatParser, reads localization files (Language.dat + optionally English.dat as fallback), computes the SHA1 hash, and enqueues an AssetDefinition.

Both results are enqueued to the ConcurrentQueue<ResultItem>. The main thread drains this queue in its Update loop.

Progress Tracking

The worker tracks progress through Interlocked counters:

csharp
internal int totalMasterBundlesFound;
internal int totalMasterBundlesRead;
internal int totalAssetDefinitionsFound;
internal int totalAssetDefinitionsRead;
internal int totalSearchLocationRequests;
internal int totalSearchLocationsFinishedSearching;
internal int totalSearchLocationsFinishedReading;

These counters feed into AssetLoadingStats which the LoadingUI reads to update progress bars.

The LoadFile Pipeline

The LoadFile static method on Assets processes each AssetDefinition on the main thread. The sequence is:

LoadFile(AssetDefinition file)

      ├─ 1. Validate path length (warn if > 260 chars for Windows MAX_PATH)
      ├─ 2. Check for parser errors from worker thread

      ├─ 3. Extract GUID and Type from Metadata dictionary
      │    (or assign a new GUID if none is present)

      ├─ 4. Fall back to legacy Type field if no metadata
      │    (look up in assetTypes registry)

      ├─ 5. Verify type derives from Asset

      ├─ 6. Resolve master bundle by path or override

      ├─ 7. Create Bundle or MasterBundle wrapper
      │    (determines shader conversion flags from version)

      ├─ 8. Parse legacy ID and GUID from data

      ├─ 9. Instantiate asset via Activator.CreateInstance(assetType)

      ├─ 10. Set base fields: id, GUID, hash, origin, error flags

      ├─ 11. Call asset.PopulateAsset(parameters)

      ├─ 12. Add to origin's asset list

      ├─ 13. Call AddToMapping(asset, ...) to register in dictionaries

      └─ 14. Unload the bundle

GUID Auto-Assignment

If a .dat file is missing a GUID entirely (not mis-formatted, but absent), the loader generates one:

csharp
if (!rootData.ContainsKey("GUID"))
{
    assetGuid = Guid.NewGuid();
    // Write GUID back to file for persistence
    string text = "GUID " + assetGuid.ToString("N") + Environment.NewLine + text;
    File.WriteAllText(assetPath, text);
    UnturnedLog.info($"Assigned GUID {assetGuid:N} to asset \"{assetPath}\"");
}

This auto-assignment is a convenience for mod authors. Once assigned, the GUID is written back to the file so subsequent loads use the same value.

AssetBundleVersion Constants

The AssetBundleVersion class defines constants for each Unity version's asset bundle format:

ConstantValueUnity Version
UNITY_51Unity 5.5 and earlier
UNITY_2017_LTS2Unity 2017 LTS
UNITY_2018_AND_2019_LTS3Unity 2018/2019 LTS
UNITY_2020_LTS4Unity 2020 LTS
UNITY_2021_LTS5Unity 2021 LTS
NEWEST6Unity 2022 LTS+

The version determines shader conversion behavior:

  • < UNITY_2017_LTS: Convert shaders to Standard
  • < UNITY_2018_AND_2019_LTS: Consolidate shaders (convert to a shared Standard variant)
  • >= UNITY_2018_AND_2019_LTS: No conversion needed

The effective version is the maximum of the master bundle version and the per-asset Asset_Bundle_Version field.

Asset Recording

The asset system can be started with the -ExportAssetsReport flag, which generates a JSON report of all loaded assets. Each asset contributes to Cargo data tables through BuildCargoData. These tables are used by the wiki's Cargo system to auto-generate asset reference pages:

TablePurpose
AssetBase asset metadata: GUID, ID, filename, master bundle, origin, type
ClothingClothing-specific fields (armor, proof flags, movement speed)
BagBag-specific fields (width, height for storage)
GearGear-specific fields (hair, beard visibility)
GlassesGlasses-specific fields (vision mode, nightvision color)

Asset Class Registration Lifecycle

The complete lifecycle of an asset from disk to registry:

1. Worker thread: File discovery
   └─ FindAssets() scans directory for .asset/.dat files

2. Worker thread: File parsing
   └─ DatParser.Parse() reads key-value pairs from file

3. Worker thread: Localization
   └─ Read English.dat and language-specific .dat files

4. Worker thread: Hash computation
   └─ SHA1 hash of file content via SHA1Stream

5. Worker thread: Enqueue AssetDefinition
   └─ Add to ConcurrentQueue<ResultItem>

6. Main thread: TryDequeueResult
   └─ Drain completed results each Update() frame

7. Main thread: LoadFile()
   ├─ Extract GUID and Type
   ├─ Resolve master bundle
   ├─ Activator.CreateInstance(assetType)
   ├─ Set base fields (id, GUID, hash, origin)
   └─ Call asset.PopulateAsset(parameters)

8. Main thread: AddToMapping()
   ├─ Check for duplicate GUID → error if exists
   ├─ Check for duplicate legacy ID → error if exists
   ├─ Register in assetDictionary by GUID
   ├─ Register in legacyAssetsTable by type + ID
   └─ Add to assetList

9. Main thread: Bundle unload
   └─ bundle.unload() releases loaded resources

AssetOrigin System

Each asset has an AssetOrigin that tracks its source:

csharp
internal class AssetOrigin
{
    public string name;            // Display name for logging
    public ulong workshopFileId;   // Non-zero for workshop content
    public bool canResave;         // Allow re-saving for assets in Maps folder
    public bool shouldAssetsOverrideExistingIds; // Workshop files override existing
    public List<Asset> assets;     // Assets loaded from this origin
}

Origins are tracked in Assets.assetOrigins (a List<AssetOrigin>). The built-in origins are:

OriginNameworkshopFileIdcanResave
coreOriginCore0false
reloadOriginReload0false
legacyServerSharedOriginLegacy Shared0false
legacyPerServerOriginLegacy Server0false
Workshop originsWorkshop File ({id})PublishedFileIdtrue
Map originsMap "{name}"0 (or workshop ID)true

The shouldAssetsOverrideExistingIds flag is set to true for workshop origins, which means workshop content replaces existing assets with the same ID. Core origins have this as false, so duplicate IDs in vanilla assets produce an error rather than a silent replacement.

Asset Lookup Resolution with Redirectors

When Assets.find(GUID) is called, the resolver follows RedirectorAsset chains:

csharp
public static Asset find(Guid GUID)
{
    Asset resultAsset;
    currentAssetMapping.assetDictionary.TryGetValue(GUID, out resultAsset);

    int redirectCount = 0;
    do
    {
        if (resultAsset is RedirectorAsset redirectorAsset)
        {
            currentAssetMapping.assetDictionary.TryGetValue(
                redirectorAsset.TargetGuid, out resultAsset);
            ++redirectCount;
            if (redirectCount > 32)
            {
                resultAsset = null;
                break;
            }
        }
        else
        {
            break;
        }
    }
    while (true);

    return resultAsset;
}

The 32-hop limit prevents infinite redirect loops from malicious or corrupted asset files. Each hop resolves the current asset's TargetGuid from the dictionary. If a RedirectorAsset points to a GUID that does not exist in the dictionary, resultAsset becomes null and the loop exits.

The find(EAssetType, ushort) overload performs the same redirect chain but starts from the legacy ID table.

MasterBundleConfig Cache

Master bundle configurations are stored in two lists:

csharp
private static List<MasterBundleConfig> allMasterBundles;   // Fully loaded
private static List<MasterBundleConfig> pendingMasterBundles; // In loading queue

The allMasterBundles list is used for path-based lookups via findMasterBundleByPath. Bundles are added to this list after their AssetBundle is fully loaded from disk. The pendingMasterBundles list tracks bundles that have been discovered but not yet loaded into memory.

The findMasterBundleByPath method uses the longest-matching-prefix algorithm rather than an exact match, because an asset file at Items/Guns/Eaglefire/Eaglefire.asset should match the bundle configured at Items/Guns even though the asset is two subdirectories deep. The method checks that the character immediately after the bundle directory path is a path separator, preventing false matches on same-prefix directory names (e.g., Items_Guns should not match Items).

Re-Saving and Data Conversions

The asset system supports automatic re-saving of .dat files when started with the appropriate command-line flags:

FlagEffectRequired for
-ParseAssetMetadataEnable metadata parsing (line numbers, comments)Re-saving, error context
-ResaveAssetsRe-save all assets after loadingAutomatic field upgrades

When re-saving is enabled, asset.OriginParsedData is populated with the original IDatDictionary, allowing BuildCargoData and PreResaveAsset to compare original vs. modified values. Modifications are only saved if the asset has no errors — the check asset.HasErrors == false prevents data loss from partially-loaded assets.

The CanPerformDataConversions field in PopulateAssetParameters is set to true only when all three conditions are met: -ResaveAssets flag is set, the asset's origin allows re-saving (origin.canResave == true), and -ParseAssetMetadata is set.

Appendix A: Asset System Command-Line Flags

FlagTypeDefaultPurpose
-SkipAssetsbool flagNot setSkip all asset loading (quick dev launches)
-NoDeferAssetsbool flagNot setDisable deferred asset loading
-ValidateAssetsbool flagNot setEnable extra validation during loading
-ParseAssetMetadatabool flagNot setParse line numbers and comments in .dat files
-ResaveAssetsbool flagNot setRe-save all .dat files after loading
-AggressiveGCbool flagNot setGC and clean unused assets every loading frame
-LogWorkshopAssetsbool flagNot setLog workshop asset names and IDs during loading
-LogSpawnInsertionsbool flagNot setLog spawn table root insertions
-ExportAssetsReportbool flagNot setExport JSON report of all assets (editor only)
-AlwaysLoadItemPrefabbool flagNot setLoad item prefabs on dedicated server
-LoadCoreAssetBundleFromSteamInstallbool flagNot setLoad core bundle from Steam install (dev)

Appendix B: AssetMapping Structure

AssetMapping
├── legacyAssetsTable: Dictionary<EAssetType, Dictionary<ushort, Asset>>
│   ├── EAssetType.ITEM → Dictionary<ushort, ItemAsset>
│   ├── EAssetType.EFFECT → Dictionary<ushort, EffectAsset>
│   ├── EAssetType.OBJECT → Dictionary<ushort, ObjectAsset>
│   ├── EAssetType.RESOURCE → Dictionary<ushort, ResourceAsset>
│   ├── EAssetType.VEHICLE → Dictionary<ushort, VehicleAsset>
│   ├── EAssetType.ANIMAL → Dictionary<ushort, AnimalAsset>
│   ├── EAssetType.MYTHIC → Dictionary<ushort, MythicAsset>
│   ├── EAssetType.SKIN → Dictionary<ushort, SkinAsset>
│   ├── EAssetType.SPAWN → Dictionary<ushort, SpawnAsset>
│   └── EAssetType.NPC → Dictionary<ushort, NPCAsset>

├── assetDictionary: Dictionary<Guid, Asset>
│   └── All assets of all types, keyed by GUID

├── assetList: List<Asset>
│   └── All assets in registration order

└── modificationCounter: int
    └── Incremented on every add/remove

Appendix C: Error Message Format

Error messages reported by Assets.ReportError use the format:

{OriginName} {FriendlyName} ({TypeFriendlyName}) [{GUID:N}]: {message}

Example outputs:

  • "Vanilla Eaglefire (Gun) [b03d581a5c1a490f995f8deba57b0f17]: missing 'Barrel' GameObject"
  • "Workshop File (12345) MyCustomItem (Hat) [c4d5e6f7081a4b9c2d3e4f5a6b7c8d9e]: needs a non-zero ID"
  • "Map 'PEI' Item_3001 (Supply) [a1b2c3d4e5f64a7b8c9d0e1f2a3b4c5d]: legacy ID 3001 already taken by Eaglefire (Gun) in Vanilla!"

The AssetErrorPrefix property constructs this string from the asset's origin name, friendly name, type name (with "Asset" suffix removed and in title case), and GUID in hex format.

Asset Origin Resolution During Level Loading

When a level is loaded, Assets.ApplyServerAssetMapping constructs the per-server currentAssetMapping. The mapping includes origins for the specific level being loaded:

csharp
AssetOrigin levelAssetOrigin = null;
if (pendingLevel != null)
{
    levelAssetOrigin = FindLevelOrigin(pendingLevel);
    if (levelAssetOrigin != null)
    {
        originsToAdd.Add(levelAssetOrigin);
    }
}

The FindLevelOrigin method checks whether the level is a workshop item (by checking publishedFileId) or a local map. Workshop levels use FindWorkshopFileOrigin; local maps create a new origin with name Map "{name}" and canResave = true.

If the level asset origin matches a workshop file origin that was already added, it is skipped to prevent duplication. This happens when a workshop file contains both assets and a map — the same origin handles both.

Asset Hot-Reloading

The Assets.reload(string absolutePath) method triggers a partial reload of a specific directory:

csharp
public static void reload(string absolutePath)
{
    if (hasFinishedInitialStartupLoading && !isLoading)
    {
        loadingStats.Reset();
        RequestAddSearchLocation(absolutePath, reloadOrigin);
    }
}

The reloadOrigin is a special origin with shouldAssetsOverrideExistingIds = true. When assets are loaded from this origin, they replace any existing asset with the same GUID or legacy ID in the mapping. This allows workshop content updates to replace assets without restarting the game.

The reloadOrigin is cleared after each reload operation — it is a transient origin that does not persist across reloads.

Asset Error Reporting During Loading

Errors encountered during asset loading are accumulated in a global error list:

csharp
private static List<string> errors;

public static void reportError(string error)
{
    errors.Add(error);
    UnturnedLog.warn(error);
}

public static List<string> getReportedErrorsList()
{
    return errors;
}

The error list is consumed by the loading UI and server startup scripts. Each error is formatted with the asset's error prefix (origin name, friendly name, type, GUID) for easy identification.

The ReportError overloads on Assets accept an IAssetErrorContext (typically the Asset itself):

csharp
public static void ReportError(IAssetErrorContext context, string error)
{
    if (context is Asset asset)
    {
        asset.HasErrors = true;
    }
    reportError($"{context.AssetErrorPrefix}: {error}");
}

Setting asset.HasErrors = true prevents the asset from being re-saved (avoiding data loss). Modders can check this flag at runtime to determine whether an asset loaded correctly.

TypeRegistryDictionary Implementation Details

The TypeRegistryDictionary class provides the string-to-type mapping used by the asset parser:

csharp
public class TypeRegistryDictionary
{
    private Dictionary<string, Type> typeDict;

    public void addType(string name, Type type);
    public Type getType(string name);
    public void removeType(string name);
}

The dictionary is populated in UnturnedNexus.initialize() with entries like:

csharp
Assets.assetTypes.addType("Gun", typeof(ItemGunAsset));

The addType method stores the mapping in an internal Dictionary<string, Type>. The getType method performs a case-sensitive lookup. If the string is not found, it attempts to parse the string as a full C# type name (for the "v2" metadata format). The removeType method removes the entry (used during module shutdown).

Spawn Table Linking

The linkSpawns() method processes SpawnAsset root insertions after all assets are loaded:

csharp
public static void linkSpawns()
{
    if (hasUnlinkedSpawns)
        hasUnlinkedSpawns = false;
    else
        return;

    List<SpawnAsset> spawnAssetList = new List<SpawnAsset>();
    FindAssetsByType_UseDefaultAssetMapping(spawnAssetList);
    // Process insertRoots for each SpawnAsset
}

The spawn linking process:

  1. Collects all SpawnAsset instances
  2. For each asset's insertRoots, finds the parent spawn table by GUID or legacy ID
  3. Inserts the asset as a child of the parent table
  4. Sets isLink = true to mark the inserted entry
  5. Marks the parent's tables as dirty for re-evaluation

The hasUnlinkedSpawns flag is set to true when a new SpawnAsset with EAssetType.SPAWN is added to the mapping. This triggers the linking process on the next call to linkSpawns().

Asset Integrity Verification

The ClientAssetIntegrity class provides a hash-based verification system for multiplayer. When a client connects to a server, the server can compare asset hashes against the client's hashes to detect modified assets.

Each asset stores a SHA1 hash of its input file:

csharp
public byte[] hash;
public void appendHash(byte[] otherHash)
{
    hash = Hash.combineSHA1Hashes(hash, otherHash);
}

The hash is computed from the source file content (including metadata if present) and incorporates the bundle-relative path to prevent path-swapping attacks (public issue #4279).

Document history

VersionDateAuthorNotes
1.02026-07-2757 StudiosInitial publication. Asset base class, dual mapping, worker pipeline, type registry, master bundle resolution.