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:
- Platform Manifest — a standard Unity
AssetBundleManifestbundle, downloaded from{baseURL}/{platform}. The platform name is resolved at runtime (Android,iOS, etc.). - 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:
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:
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 /:
The final CDN URL takes the form:
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:
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:
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:
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:
AssetBundleManifest.GetAllDependencies(A)returns the list of required bundles.- Each dependency is recursively enqueued for download via
LoadAssetBundleInternal(). GetLoadedAssetBundle()only returnsAas 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:
| 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:
-
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).
-
Download the raw bundle file via HTTP GET.
-
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). -
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:
-
For each bundle in your working directory: - Compute the MD5 hash of the file's binary content. This becomes the
versionfield. - Compute the CDN hashed path by applying the salted MD5 to the bundle's logical name. This becomes thenamefield used by the client to fetch the file. - SetisEncrypttofalsefor all modified bundles (since they are served as plaintext). -
Build the version list JSON in the format:
{ "list": [ { "name": "<hashed CDN path>", "version": "<MD5 of file>", "isEncrypt": false } ] } -
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.