Skip to content

Asset Bundles

This page explains the architecture of the game AssetBundle system: how bundles are identified, fetched, stored, decrypted, and loaded at runtime.


Overview

All game resources (textures, audio, scenes, data tables, etc.) are distributed as Unity AssetBundles. The game client implements a custom layer on top of Unity's standard pipeline, adding the following capabilities:

Feature Description
Path obfuscation Bundle names are hashed (MD5 + salt) on the CDN, hiding their real paths
Version management Each bundle is tagged with the MD5 of its content, used as a version string
Selective encryption Some bundles use a custom AES-CBC envelope; others are raw Unity AssetBundles
Disk cache Downloaded bundles are persisted to a device cache directory to avoid re-downloads
Concurrency throttling Simultaneous downloads and loads are capped by project-level constants

The custom extension used for all AssetBundle files in this game is .muast.


Startup and Initialization Flow

The following sequence occurs every time the game launches, driven by the title screen state machine (TitleState_Init).

sequenceDiagram
    participant Client
    participant API as "Game API Server"
    participant CDN as "Asset CDN"

    Client->>API: "POST /versionget (platform, version=1.0.3)"
    API-->>Client: "{ abVer: N }"
    Note over Client: "Sets X-STAR-AB header to N"

    Client->>CDN: "GET {baseURL}/{platform}?t=... (Manifest bundle)"
    CDN-->>Client: "Unity AssetBundleManifest"

    Client->>CDN: "GET {baseURL}/v.b?t=... (Version list)"
    CDN-->>Client: "zlib-compressed JSON"
    Note over Client: "isReady = true"

    Client->>CDN: "GET {baseURL}/{encoded_name}?t=... (Individual bundles)"
    CDN-->>Client: "Raw or Encrypted .muast bundle"

Phase 1: Version Check

Before the AssetBundle system is initialized, the client calls /version/get. The server responds with an integer abVer — the current AssetBundle generation number — which is stored and attached to all subsequent HTTP requests as the X-STAR-AB header.

Phase 2: Manifest and Version List Download

After the version check succeeds, PrepareAssetBundles() is called. This launches the UpdateManifest coroutine, which performs two fetches:

  1. Platform Manifest — a standard Unity AssetBundleManifest bundle, downloaded from {baseURL}/{platform}. The platform name is resolved at runtime (Android, iOS, etc.).
  2. Version list (v.b) — a zlib-compressed JSON file mapping every bundle name to its expected MD5 hash. See Version List Format.

Once both fetches succeed, AssetBundleManager.isReady becomes true and all queued requests begin processing.


Bundle Naming and URL Construction

Bundle Name Format

The game code always refers to bundles using the .muast extension:

database.muast
anim/player/common_battle_body.muast

When queued for download, the name is passed through AssetBundleNameUtility.Encode(). In the current source, AlphaEncode() is a passthrough — the encoded name is identical to the raw name.

CDN Path Obfuscation

The bundle is not served at its raw name on the CDN. The AddressCalculator hashes each path segment using a salted MD5:

\[\text{hash}(s) = \text{MD5}\!\left(\, s_{\text{UTF-8}} \,\|\, \text{salt}[{:}\,|s|] \,\right)\]

Where salt is a secret byte sequence provided at deploy time (not embedded in the client binary). For paths containing a /, the directory portion and the filename are hashed independently and joined with /:

\[\text{hashedPath}(p) = \text{hash}(\text{dir}(p)) + \text{"/"} + \text{hash}(\text{basename}(p))\]

The final CDN URL takes the form:

{HOST}/{RVH}/{platform}/{hash(dir)}/{hash(filename)}

URL Reference Table

Resource URL Pattern
Platform Manifest {baseURL}/{platform}?t={timestamp}
Version list {baseURL}/v.b?t={timestamp}
Individual bundle {baseURL}/{encoded_name}?t={timestamp}

Note

The ?t= timestamp parameter is appended to every request to bust HTTP-level caches.


Version List Format

The v.b file is the authoritative source of bundle version information. It is transmitted as a zlib-compressed byte stream. After decompression, the content is a UTF-8 JSON document:

{
  "list": [
    {
      "name": "chara/hero_001.muast",
      "version": "a1b2c3d4e5f67890a1b2c3d4e5f67890"
    }
  ]
}

The version field is the lowercase hex MD5 of the bundle file on disk. The client computes the MD5 of every downloaded file and compares it against this value. A mismatch triggers a manifest refresh and retry.


Plain vs. Encrypted Bundles

Detection

When a bundle finishes downloading, the first 7 bytes of the file are inspected by FileDownloadHandler to determine whether it is encrypted:

Magic bytes for a plain Unity AssetBundle:
  55 6E 69 74 79 46 53  ==  "UnityFS"

The detection logic in Utility.IsAssetbundle() is:

// IsAssetbundle: 先頭7バイトが "UnityFS" と一致するか確認する
return src[0] == 0x55 && src[1] == 0x6E && src[2] == 0x69 &&
    src[3] == 0x74 && src[4] == 0x79 && src[5] == 0x46 && src[6] == 0x53;
Magic bytes present? Type Loading method
Yes (UnityFS) Plain bundle AssetBundle.LoadFromFileAsync(path)
No Encrypted bundle Decrypt → AssetBundle.LoadFromMemoryAsync(bytes)

Cache Metadata

The isEncrypted flag is persisted in PlayerPrefs alongside the version string, using the key format:

Key:   AssetbundleVersion_{bundleName}
Value: "{md5Version}_{isEncrypted:0|1}_{systemVersion}"

Example:
Key:   AssetbundleVersion_chara/hero_001.muast
Value: "a1b2c3d4_1_2"
        └─ md5   ┘│└┘
                │ └─ system version (always 2)
                └─── 1=encrypted, 0=plain

On subsequent launches, this metadata tells the client whether to run decryption on the cached file, without re-downloading.


Encrypted Bundle Format and Decryption

Encrypted bundles use a simple, self-contained binary format.

Binary Layout

flowchart LR
    classDef field fill:#0984e3,stroke:#74b9ff,color:#fff,stroke-width:2px
    classDef large fill:#6c5ce7,stroke:#a29bfe,color:#fff,stroke-width:2px

    A["1 byte\nIV length (N)"]:::field
    B["N bytes\nIV (random ASCII)"]:::field
    C["4 bytes\nciphertext length"]:::field
    D["M bytes\nAES-256-CBC ciphertext"]:::large

    A --> B --> C --> D

Note

In practice, N is always 16 (controlled by EncryptPasswordCount = 16 in AssetBundleCryptUtility).

Cryptographic Parameters

Parameter Value
Algorithm AES-256-CBC (via RijndaelManaged)
Padding PKCS7
Key size 256 bits (32 bytes)
Block size 128 bits
Key Static key defined in AssetBundleCryptUtility.cs
IV 16 randomly generated ASCII characters (0-9, a-z, A-Z) — prepended to each file

Decryption Steps

flowchart LR
    classDef step fill:#0984e3,stroke:#74b9ff,color:#fff,stroke-width:2px
    classDef io fill:#2d3436,stroke:#b2bec3,color:#fff,stroke-width:2px
    classDef result fill:#00b894,stroke:#55efc4,color:#fff,stroke-width:2px

    A(["Encrypted .muast file"]):::io
    B["Read 1 byte → ivLength"]:::step
    C["Read ivLength bytes → IV"]:::step
    D["Read 4 bytes (Int32) → ciphertextLength"]:::step
    E["Read ciphertextLength bytes → ciphertext"]:::step
    F["AES-256-CBC Decrypt\n(static key + IV)"]:::step
    G(["Plaintext Unity AssetBundle"]):::result

    A --> B --> C --> D --> E --> F --> G

Important

This AES envelope is distinct from the .muast multi-segment puzzle encryption described in Asset Encryption. The AssetBundle-level encryption is a straightforward AES header wrapping the entire binary. The .muast format is an additional layer applied to certain asset files inside the bundle.


Disk Cache

Downloaded bundles are written to the device's temporary cache path:

{Application.temporaryCachePath}/a.b/{bundleName}

The cache check happens before every network request. If a valid cached file exists and its recorded MD5 version matches the current v.b entry, the download is skipped.

File access is serialized via an in-memory access list (m_accessLists) to prevent concurrent read/write collisions on the same cache file.

flowchart TD
    classDef decision fill:#fdcb6e,stroke:#e17055,color:#2d3436,stroke-width:2px
    classDef action fill:#0984e3,stroke:#74b9ff,color:#fff,stroke-width:2px
    classDef done fill:#00b894,stroke:#55efc4,color:#fff,stroke-width:2px

    Start(["LoadAssetBundle(name)"])
    CacheHit{"Cache exists\nand version matches?"}:::decision
    Encrypted{"isEncrypted?"}:::decision
    LoadFile["AssetBundle.LoadFromFileAsync()"]:::action
    LoadEncrypted["Read bytes → Decrypt → LoadFromMemoryAsync()"]:::action
    Download["HTTP GET from CDN\n→ Write to cache"]:::action
    DetectType{"IsAssetBundle\n(UnityFS magic)?"}:::decision
    SaveMeta["Save version + isEncrypted\nto PlayerPrefs"]:::action
    Done(["Bundle ready"]):::done

    Start --> CacheHit
    CacheHit -- Yes --> Encrypted
    CacheHit -- No --> Download
    Encrypted -- No --> LoadFile
    Encrypted -- Yes --> LoadEncrypted
    LoadFile --> Done
    LoadEncrypted --> Done
    Download --> DetectType
    DetectType -- Plain --> SaveMeta
    DetectType -- Encrypted --> SaveMeta
    SaveMeta --> Encrypted

Dependency Resolution

The AssetBundleManifest encodes dependency relationships between bundles. When bundle A is requested:

  1. AssetBundleManifest.GetAllDependencies(A) returns the list of required bundles.
  2. Each dependency is recursively enqueued for download via LoadAssetBundleInternal().
  3. GetLoadedAssetBundle() only returns A as ready once all dependencies are also fully loaded.

Dependencies are tracked in a dedicated dictionary (m_Dependencies) and their reference counts are managed in parallel with the root bundle's reference count, so a dependency shared by multiple bundles is only unloaded when all roots that use it have been released.


Startup Pre-fetch (Download Screen)

During the first-launch and session-start download screen, the game pre-fetches two pre-configured bundle lists:

List Used when
ABDLDB_First First install, or tutorial not yet complete
ABDLDB_Always Every startup after tutorial

Each list is a ScriptableObject (AssetBundleDownloadListDB) containing arrays of bundle paths. The GetPaths() extension automatically appends .muast to any path missing the extension.

All bundles are requested with the DownloadOnly option — they are saved to disk but not loaded into memory during this phase.

The progress bar weights are:

\[\text{progress} = \sum_{i=0}^{k-1} w_i + p_k \cdot w_k\]
Phase Weight (\(w\))
AssetBundle pre-fetch 50%
CRI audio file download 49%
Post-processing 1%

Extra: Minimal Asset Bundle Modification Guide

Note

This section describes the conceptual workflow for modifying game AssetBundles and serving them from a custom asset server. The steps are tool-agnostic — they represent the logical operations that any implementation must perform.

Overview

Serving modified AssetBundles requires three conceptual phases: acquiring and decrypting the original bundles, editing their contents, and regenerating the metadata the client uses for version validation.

flowchart LR
    classDef phase fill:#0984e3,stroke:#74b9ff,color:#fff,stroke-width:2px
    classDef data fill:#2d3436,stroke:#b2bec3,color:#fff,stroke-width:2px

    CDN(["Production CDN"]):::data
    Unpack["Step 1\nAcquire & Decrypt"]:::phase
    Edit["Step 2\nEdit Bundle Contents"]:::phase
    Repack["Step 3\nRecompute Metadata"]:::phase
    Serve["Step 4\nServe from Custom Host"]:::phase
    Client(["Game Client"]):::data

    CDN --> Unpack --> Edit --> Repack --> Serve --> Client

Step 1 — Acquire and Decrypt

To obtain editable bundle files, you must:

  1. Resolve the CDN URL for each bundle. The CDN does not serve bundles at their logical names. Each path component (directory and filename) is independently hashed with a salted MD5. To reconstruct any URL, you need the salt value (see the CDN Path Obfuscation section).

  2. Download the raw bundle file via HTTP GET.

  3. Detect the bundle type by inspecting the first 7 bytes. If the magic bytes are UnityFS, the file is already a plain Unity AssetBundle. Otherwise, the file is AES-256-CBC encrypted and must be decrypted first (see Encrypted Bundle Format and Decryption).

  4. Output the plaintext Unity AssetBundle to a working directory, preserving the original logical path (e.g., chara/hero_001.muast).

After this step, all working files should start with the UnityFS magic header.


Step 2 — Edit Bundle Contents

The decrypted .muast files are standard Unity AssetBundles and can be opened with any compatible Unity asset editing tool.

Tool Primary Use
UABEA Read/write textures, text assets, ScriptableObjects
AssetStudio Read-only inspection and extraction
Other Unity tooling Scenes, animations, and other asset types

Important

After editing, save the file as a plain Unity AssetBundle (i.e., the file must start with the UnityFS magic bytes). The client detects plain bundles by this header and loads them directly without decryption. Do not re-encrypt modified bundles — the client will handle them correctly as long as the magic header is present and the isEncrypt flag in the version list is set to false.


Step 3 — Recompute the Version List

The version list (v.b) is the single source of truth the client uses to validate every downloaded bundle. After modifying any bundle, you must regenerate this file. The regeneration process is:

  1. For each bundle in your working directory: - Compute the MD5 hash of the file's binary content. This becomes the version field. - Compute the CDN hashed path by applying the salted MD5 to the bundle's logical name. This becomes the name field used by the client to fetch the file. - Set isEncrypt to false for all modified bundles (since they are served as plaintext).

  2. Build the version list JSON in the format:

      {
        "list": [
          { "name": "<hashed CDN path>", "version": "<MD5 of file>", "isEncrypt": false }
        ]
      }
    
  3. Compress the JSON with zlib (deflate, no header) and write the result to a file. The filename served to the client is the salted-MD5 hash of the logical name v.b — compute it the same way as any other bundle path.

Warning

The client fetches the version list using the same hashed-path scheme as all other bundles. The filename you serve must be the salted-MD5 hash of the string v.b, not the literal string v.b.


Step 4 — Serve from a Custom Host

Place the regenerated version list and modified bundles in a directory served over HTTP. The expected directory layout mirrors the CDN structure:

<serve root>/
├── <hash("v.b")>                    ← Regenerated version list (zlib-compressed JSON)
├── <hash("Android")>                ← Platform manifest bundle (copy from CDN)
├── <hash("dir")>/<hash("file")>     ← Modified bundles at their hashed CDN paths
└── ...

All unmodified bundles can remain on the production CDN. The recommended minimal setup is a reverse proxy that:

  • Routes requests for the regenerated version list and modified bundle paths → custom server
  • Routes all other bundle requests → production CDN

This way, only the version list and the specific bundles you changed need to be hosted locally.

flowchart TD
    classDef custom fill:#6c5ce7,stroke:#a29bfe,color:#fff,stroke-width:2px
    classDef prod fill:#0984e3,stroke:#74b9ff,color:#fff,stroke-width:2px
    classDef client fill:#00b894,stroke:#55efc4,color:#fff,stroke-width:2px

    Client(["Game Client"]):::client
    Proxy["Reverse Proxy"]:::custom
    Custom["Custom Server\nVersion list + modified bundles"]:::custom
    ProdCDN["Production CDN\nAll other bundles"]:::prod

    Client -->|"All asset requests"| Proxy
    Proxy -->|"Version list + modified bundles"| Custom
    Proxy -->|"Everything else"| ProdCDN

Note

Because the client validates every bundle against the MD5 recorded in the version list, the version field for unmodified bundles in your regenerated v.b must match what is actually served — whether from the production CDN or your custom host. If the client fetches a bundle from the CDN but your version list contains an incorrect MD5, it will reject the file and trigger a re-download loop.