# Addressables — Bootstrap & tóm tắt nghiệp vụ Tài liệu này tóm tắt ghi chép **Addressables** (catalog / cache / remote) và các script runtime trong project **`perfect-world-unity`**: `AddressablesCatalogUpdater`, `AddressablesRuntimeUrlRewriter`, `GameContentBootstrap`, cùng chỉnh sửa `AddressableManager`. Tham chiếu thêm: bản ghi chép tổng quan ban đầu (`Addressables-Tong-hop.md` nếu có trong repo hoặc bản copy ngoài project). --- ## 0. Hai scene — Content trước, Bootstrap sau (khuyến nghị) Tách **tải Addressables / catalog / bulk** khỏi scene game để `Bootstrap.unity` (UI, `CECUIManager`, `AddressableManager`, …) không bị ảnh hưởng. ```mermaid flowchart LR A[GameContentBootstrap.unity index 0] -->|sync OK LoadScene Single| B[Bootstrap.unity] B -->|BootstrapSceneController ~1s| C[LoginScene] ``` | Scene | Build index | Chứa gì | |-------|-------------|---------| | **GameContentBootstrap** | **0** | Chỉ `GameContentBootstrap` (+ UI loading tuỳ chọn) | | **Bootstrap** | 1 | `BootstrapSceneController`, `AddressableManager`, `CECUIManager`, … **không** gắn `GameContentBootstrap` | | **LoginScene** | … | Load từ `BootstrapSceneController._nextSceneName` như cũ | **Setup một lần trong Editor:** menu **Perfect World → Addressables → Setup Two-Scene Bootstrap** - Tạo `Assets/PerfectWorld/Scene/GameContentBootstrap.unity` (nếu chưa có) - Gỡ `GameContentBootstrap` khỏi `Bootstrap.unity` - Đưa scene Content lên **index 0** Build Settings **Trên component `GameContentBootstrap` (scene Content):** | Field | Gợi ý | |--------|--------| | `_loadNextSceneAfterSuccess` | **Bật** | | `_nextSceneName` | `Bootstrap` (tên scene trong Build Settings) | | `_stayOnSceneWhenSyncFails` | **Bật** — lỗi thì không vào game | Sau khi sync thành công: `GameContentBootstrapSession.IsContentReady = true`, Addressables đã init → scene **Bootstrap** chỉ cần `AddressableManager` / `AUIManager` gọi `AddressablesInitService` (không chờ gate). --- ### Cấu hình component (`GameContentBootstrap`) ### Hai chế độ | Chế độ | `_useServerForVersionInfo` | Bạn làm gì | |--------|---------------------------|------------| | **Test / không API** | **Tắt** | Chỉ cần hardcode (bảng dưới). Không cần `_versionEndpointUrl`. | | **Production** | **Bật** | Điền URL API + (tuỳ chọn) POST body. Server **sau khi kiểm tra** dữ liệu trả JSON có `contentVersion` và (nếu cần) `assetsBaseUrl`. | **Editor:** bật `_skipRemoteCallInEditor` nếu muốn **không gọi mạng** trong Editor; lúc đó luôn dùng hardcode / `_editorFakeContentVersion` như checklist cuối tài liệu. ### Các biến cần biết (theo thứ tự ưu tiên đọc) | Biến | Khi nào cần | Điền gì (ngắn gọn) | |------|-------------|---------------------| | `_useServerForVersionInfo` | Luôn quyết định trước | Test → **tắt**. Live → **bật**. | | `_hardcodedContentVersion` | Server tắt hoặc Editor skip | Chuỗi version (vd `1`, `20250514`). Đổi số này = game coi như có content mới → catalog + bulk download. | | `_hardcodedAssetsBaseUrl` | Test CDN / không dùng server | **Prefix URL** thư mục remote Addressables trên CDN (vd `https://my-cdn.com/PW/Android/`). Phải khớp cấu trúc upload `ServerData//`. Để trống nếu bundle đã trỏ đúng URL lúc build, không cần đổi host. | | `_versionEndpointUrl` | **Chỉ khi** server **bật** | URL đầy đủ một endpoint do backend cung cấp, vd `https://api.game.com/v1/addressables/resolve`. | | `_usePostForVersionRequest` | Server bật | **Bật** nếu luồng của bạn là: client gửi JSON lên → server validate → trả link/prefix. **Tắt** = GET (không body). | | `_versionPostBody` | POST bật | JSON một dòng hoặc nhiều dòng (vd `{"platform":"Android","token":"..."}`). Để trống → client gửi `{}`. | | `_bakedRemoteUrlPrefixForRewrite` | Khi dùng `assetsBaseUrl` (từ server **hoặc** hardcode) | **Đúng** phần đầu URL đã **bake** trong catalog (trùng `Remote.LoadPath` lúc build), vd `https://old-build-cdn.example.com/Android/`. Runtime thay prefix này bằng `assetsBaseUrl`. Để trống nếu **không** rewrite. Phải khớp đầu mọi `InternalId` remote cần đổi host. | | `_remoteBulkDownloadLabel` | Gần như luôn | Trùng **label** trên group Addressables remote cần tải full (mặc định `RemoteContent`). | | `_holdAddressablesInitUntilVersionChecked` | Khuyến nghị | Giữ **bật** để Addressables init sau khi có version + rewrite. | | `_skipRemoteCallInEditor` / `_editorFakeContentVersion` | Editor | Bật skip + fake version khi không muốn HTTP trong Editor. | ### Quan trọng: Profile Addressables vs field trên `GameContentBootstrap` **URL thật sự tải catalog/bundle** được **ghi cứng vào catalog lúc build** từ profile **Remote.LoadPath** (vd `https://prefect-world-asset.wcscdn51.v1.wcsapi.com`). Runtime **không** đọc profile Editor. | Field bootstrap | Có dùng không? | Ghi chú | |-----------------|----------------|---------| | `_versionEndpointUrl` | Chỉ khi **`_useServerForVersionInfo` bật** | Test tắt server → field này **bị bỏ qua**. Không phải URL tải bundle. | | `_hardcodedAssetsBaseUrl` | Chỉ khi server tắt / Editor skip | Là **URL đích** (`assetsBaseUrl`) cho rewrite, không thay thế profile. | | `_bakedRemoteUrlPrefixForRewrite` | Chỉ khi có `assetsBaseUrl` **và** prefix **khớp** URL trong catalog | Phải = **Remote.LoadPath lúc build bản APK đang chạy**, không phải URL LAN/test đích. Sai prefix → rewrite không chạy → vẫn tải từ host trong catalog (profile cũ). | **Ví dụ lỗi thường gặp (LoadScene):** cả `_hardcodedAssetsBaseUrl` và `_bakedRemoteUrlPrefixForRewrite` đều `http://192.168.x.x/...` trong khi catalog build với `https://prefect-world-asset....wcsapi.com` → game **vẫn gọi wcsapi**, không gọi IP local. **Cấu hình test LAN đúng:** 1. `_useServerForVersionInfo` = **tắt** 2. `_hardcodedContentVersion` = version test 3. `_bakedRemoteUrlPrefixForRewrite` = prefix **trong catalog** (copy từ profile **Remote.LoadPath** lúc build) 4. `_hardcodedAssetsBaseUrl` = `http://192.168.x.x:8080/Android/` (server LAN của bạn) Log mong đợi: `URL rewrite | from=https://prefect-world-asset... → to=http://192.168...` ### `_versionEndpointUrl` vs `_bakedRemoteUrlPrefixForRewrite` — ví dụ một dòng - **`_versionEndpointUrl`**: không phải link tải từng bundle; là **URL gọi API** (GET hoặc POST) để lấy **metadata**: `contentVersion` + tuỳ chọn `assetsBaseUrl`. - **`_bakedRemoteUrlPrefixForRewrite`**: không gọi trực tiếp; là **chuỗi copy từ build** (prefix URL trong catalog). Ví dụ build profile `Remote.LoadPath` = `https://cdn-a.com/Android` → điền y hệt vào field này; server trả `assetsBaseUrl` = `https://cdn-b.com/Android` → mọi bundle path đổi từ `cdn-a` sang `cdn-b`. ### Luồng bạn muốn (POST → server kiểm tra → trả “link”) 1. Bật `_useServerForVersionInfo`, điền `_versionEndpointUrl`, bật `_usePostForVersionRequest`, điền `_versionPostBody` (vd token, platform, build channel). 2. Client **POST** `Content-Type: application/json` tới URL đó. 3. Server validate; nếu OK trả **cùng format** như trước (để `JsonUtility` parse được): ```json { "contentVersion": "12", "assetsBaseUrl": "https://cdn.example.com/PW/Android/" } ``` `assetsBaseUrl` ở đây là **base / prefix** cho remote Addressables (nơi có `catalog_*.json` và bundle), không nhất thiết là một URL file đơn lẻ — Addressables vẫn dùng catalog + download theo label như cũ. ### Option test: luôn dùng link hardcode để download 1. Tắt `_useServerForVersionInfo`. 2. Điền `_hardcodedContentVersion` (vd `test1`). 3. Nếu cần trỏ CDN test khác với URL đã bake trong catalog: điền `_hardcodedAssetsBaseUrl` **và** `_bakedRemoteUrlPrefixForRewrite` như bảng trên. 4. Editor: có thể bật `_skipRemoteCallInEditor` để khỏi gọi server khi test trong Editor. --- ## 1. Khái niệm nhanh (catalog / bundle / cache) | Thành phần | Vai trò | |------------|---------| | **Catalog** (`catalog_*.json` + `.hash`) | Danh mục: address → bundle, URL, hash/CRC… Catalog mới = tín hiệu “có nội dung mới”. | | **AssetBundle** | Chứa dữ liệu asset thực tế. | | **Nhóm Local / Remote** | Local: kèm build / `StreamingAssets`. Remote: tải qua HTTP(S). | | **Player / Editor** | Bundle **theo platform** — không dùng bundle Windows cho Android. | **Cập nhật trên máy người chơi** - Catalog trỏ tới bundle/hash/CRC **khác** lần trước **và** luồng runtime thực hiện **tải catalog mới** / **cập nhật catalog** / **load bundle** thành công. - **Không** tự cập nhật chỉ vì file trên server đổi nếu game **không** tải catalog mới hoặc không load đúng key. - **`Release`** (RAM): asset có thể hết trong bộ nhớ; lần load sau có thể **đọc lại từ bundle đã cache trên đĩa** — **không** đồng nghĩa “tải lại toàn bộ từ CDN”. **Cache bundle** - Nếu bundle đã cache **hợp lệ** theo catalog (CRC/hash đúng cấu hình), hệ thống **ưu tiên dùng bản local**, không tải lại cả file từ server. **Runtime đổi CDN / base URL** - `Remote.LoadPath` trong profile là **lúc build** (URL ghi vào catalog). - Đổi host/path **lúc chạy**: dùng **`Addressables.InternalIdTransformFunc`** (rewrite `InternalId`) — **trước** `InitializeAsync` / load remote nhiều nhất có thể. **HTTP / HTTPS** - Player Settings: **Allow downloads over HTTP** nếu dùng `http://` (thường chỉ dev). - Production: **HTTPS**; Android/iOS có thể chặn cleartext. --- ## 2. Script trong project ### 2.1. `AddressablesCatalogUpdater.cs` Bọc API Addressables (package ~2.7.x): - `EnsureInitializedAsync` - `CheckForCatalogUpdatesAsync` / `UpdateCatalogsAsync` / `CheckAndUpdateCatalogsIfNeededAsync` - `GetDownloadSizeBytesAsync` - `DownloadDependenciesAsync` - `CleanBundleCacheAsync` ### 2.2. `AddressablesRuntimeUrlRewriter.cs` - `InstallPrefixRewrite(fromPrefix, toPrefix)` — thay prefix URL trong `InternalId` (CDN mới). - `ClearInstalledRewrite` - `ChainPreviousTransform` — gọi transform cũ trước (nếu có). ### 2.3. `GameContentVersionServerClient.cs` - Struct **`GameContentVersionFetchResult`** (`Ok`, `ContentVersion`, `AssetsBaseUrl`, `Error`). - **`GameContentVersionServerClient.FetchAsync(url, timeoutSeconds, usePost, postJsonBody)`** — GET **hoặc** POST JSON (`Content-Type: application/json`), parse JSON phẳng (`JsonUtility`). ### 2.4. `GameContentBootstrap.cs` (scene bootstrap) **Nguồn phiên bản / URL** - **`_useServerForVersionInfo`**: tắt = không gọi server, dùng **`_hardcodedContentVersion`** / **`_hardcodedAssetsBaseUrl`** (test, API chưa chốt). - Bật = gọi **`GameContentVersionServerClient.FetchAsync`** tới **`_versionEndpointUrl`** (GET hoặc POST theo **`_usePostForVersionRequest`** + **`_versionPostBody`**) (trừ khi Editor bật skip bên dưới). - **`_skipRemoteCallInEditor`**: Editor không HTTP; ưu tiên hardcode nếu có version, không thì **`_editorFakeContentVersion`**. - Bật server nhưng URL trống → fallback hardcode (cảnh báo log). **Luồng** 1. Resolve version (server hoặc hardcode theo trên). 2. Nếu có `assetsBaseUrl` **và** **`_bakedRemoteUrlPrefixForRewrite`** → `InstallPrefixRewrite`. 3. **Mở gate** → cho phép `AddressableManager` gọi `Addressables.InitializeAsync`. 4. `EnsureInitializedAsync`, so sánh version với `PlayerPrefs`: - **Lần đầu** hoặc **`contentVersion` khác** bản đã lưu → `CheckAndUpdateCatalogsIfNeededAsync` + `DownloadDependenciesAsync` theo **`_remoteBulkDownloadLabel`**. - **Trùng version** (và không cần làm content work) → **không** catalog/bulk download. **PlayerPrefs** - `PW_GameContent_FirstRemoteSyncDone` - `PW_GameContent_LastContentVersion` **Event / API** - `Finished` (`BootstrapResult`: `Success`, `DidContentWork`, `ErrorMessage`, `ServerContentVersion`). - `RunInternalAsync()` — gọi từ code khác nếu cần. **JSON mẫu server (phẳng, đúng `JsonUtility`)** ```json { "contentVersion": "12", "assetsBaseUrl": "https://cdn.example.com/Android" } ``` ### 2.5. `AddressableManager.cs` - Trước `Addressables.InitializeAsync()` **await** `GameContentBootstrap.WaitForPreAddressablesSetupIfAnyAsync()` để không init Addressables trước khi bootstrap xong HTTP + rewrite (khi gate được tạo). --- ## 3. Bảng liên hệ “MD nghiệp vụ ↔ code” | Nghiệp vụ | Code | |-----------|------| | Kiểm tra / cập nhật catalog | `AddressablesCatalogUpdater` + gọi trong `GameContentBootstrap` | | Đổi CDN runtime | `AddressablesRuntimeUrlRewriter` + `assetsBaseUrl` từ API | | First run / mỗi lần mở + so version (server hoặc hardcode) | `GameContentBootstrap` + `GameContentVersionServerClient` + `PlayerPrefs` | | Init Addressables sau rewrite | Gate + `AddressablesInitService` + `AddressableManager` chờ gate | | Init tập trung (tránh init sớm) | `AddressablesInitService` — **không** gọi `Addressables.InitializeAsync()` trực tiếp; `AUIManager` đã chuyển sang service | --- ## 4. Checklist cấu hình (team) - [ ] Scene bootstrap có **`GameContentBootstrap`**: tắt **`_useServerForVersionInfo`** + gán hardcode khi test; production bật server + **`_versionEndpointUrl`**. - [ ] **`_bakedRemoteUrlPrefixForRewrite`** trùng prefix URL **đã bake trong bản build đang chạy** (không chỉ `Remote.LoadPath` hiện tại trong Editor). Ví dụ build cũ trỏ `https://prefect-world-asset....wcsapi.com/` → phải điền đúng prefix đó; **`_hardcodedAssetsBaseUrl`** / server `assetsBaseUrl` = CDN mới (vd `https://pw-assets.brewmonster.vn/Android/`). - [ ] Mọi entry remote cần “tải full” lần đầu / khi đổi version đều có **cùng label** với **`_remoteBulkDownloadLabel`** (mặc định `RemoteContent`). - [ ] Build Addressables đúng platform → upload đúng **`ServerData/`** lên host (nếu dùng remote). - [ ] Có luồng **catalog** khi hot-update (bootstrap đã gọi khi version đổi / first run). - [ ] Production dùng **HTTPS**; mọi chỗ load Addressables dùng **`AddressablesInitService`** (hoặc `AddressableManager.WaitUntilInitializedAsync`), không `InitializeAsync().WaitForCompletion()` trực tiếp. - [ ] **Hai scene:** index 0 = `GameContentBootstrap`, **không** gắn `GameContentBootstrap` trên `Bootstrap.unity` (chạy menu Setup Two-Scene Bootstrap). - [ ] `_nextSceneName` = `Bootstrap`; `BootstrapSceneController._nextSceneName` vẫn trỏ `LoginScene` (hoặc scene in-game) như cũ. --- ## 5. Mobile: `SSL CA certificate error` / catalog URL CDN cũ **Triệu chứng:** `CheckForCatalogUpdates` / tải `catalog_*.hash` lỗi `ConnectionError : SSL CA certificate error`, URL vẫn là host cũ (vd `prefect-world-asset....wcsapi.com`) dù profile Editor đã đổi sang CDN khác. **Nguyên nhân thường gặp:** 1. **Init sớm:** UI (`AUIManager` / `CECUIManager.Awake`) gọi `Addressables.InitializeAsync()` **trước** `GameContentBootstrap` gắn `InternalIdTransformFunc` → request vẫn tới CDN bake trong catalog build. 2. **Thiếu rewrite:** `assetsBaseUrl` có nhưng `_bakedRemoteUrlPrefixForRewrite` trống → không đổi host. 3. **Chứng chỉ CDN:** Chuỗi SSL/intermediate trên host đích không tin cậy được trên Android (sửa phía CDN / Let's Encrypt full chain). **Đã xử lý trong code:** `AddressablesInitService` chờ gate bootstrap; `AUIManager` dùng service thay vì init trực tiếp. **Bạn cần kiểm tra trên scene mobile:** | Field | Gợi ý | |--------|--------| | `_bakedRemoteUrlPrefixForRewrite` | Prefix y hệt URL trong catalog bản APK (vd `https://prefect-world-asset.wcscdn51.v1.wcsapi.com/`) | | `_hardcodedAssetsBaseUrl` | CDN đích HTTPS hợp lệ (vd `https://pw-assets.brewmonster.vn/Android/`) | Log mong đợi trước init: `[Cuong] GameContentBootstrap: URL rewrite | from=... → to=...` Các file khác vẫn có thể gọi `InitializeAsync().WaitForCompletion()` (`EC_Game`, `EC_HPWorkNavigate`, …) — nên chuyển dần sang `AddressablesInitService`. --- ## 6. Ghi chú Editor - Bật **`_skipRemoteCallInEditor`** để không gọi mạng; dùng **`_hardcodedContentVersion`** / **`_hardcodedAssetsBaseUrl`** hoặc **`_editorFakeContentVersion`** (khi hardcode version trống). --- ## 7. Debug log runtime (`[Cuong]`) Trong Console Unity, lọc `[Cuong]` để theo dõi bootstrap / tải remote. ### Tiến độ tải (% + MB) `AddressablesCatalogUpdater.DownloadDependenciesAsync` log **mỗi 5%** (mặc định) và lúc **bắt đầu / kết thúc 100%**, kèm dung lượng khi Addressables báo được `TotalBytes`: ```text [Cuong] AddressablesCatalogUpdater: Bắt đầu tải dependencies (key=RemoteContent)... [Cuong] AddressablesCatalogUpdater: 0% (0.0/512.3 MB) — key=RemoteContent [Cuong] AddressablesCatalogUpdater: 5% (25.6/512.3 MB) — key=RemoteContent [Cuong] AddressablesCatalogUpdater: 10% (51.2/512.3 MB) — key=RemoteContent ... [Cuong] AddressablesCatalogUpdater: 100% (512.3/512.3 MB) — key=RemoteContent [Cuong] AddressablesCatalogUpdater: Tải xong dependencies (key=RemoteContent). ``` Trước khi bulk download, `GameContentBootstrap` gọi `GetDownloadSizeBytesAsync` và log **dung lượng cần tải** (ước lượng theo catalog + cache): ```text [Cuong] GameContentBootstrap: Đang tải remote content (label=RemoteContent), dung lượng cần tải ~512.3 MB... ``` Nếu đã cache đủ: `dung lượng cần tải ~0 MB` hoặc log “đã có trong cache”. Đổi bước log %: gọi `DownloadDependenciesAsync(key, progressLogStepPercent: 10)` (1–100). ### Bảng log theo giai đoạn | Giai đoạn | Ví dụ log | |-----------|-----------| | Bắt đầu bootstrap | `GameContentBootstrap: Bắt đầu bootstrap Addressables...` | | Đang lấy version | `Đang lấy contentVersion / assetsBaseUrl...` | | Đang init / catalog | `Đang khởi tạo Addressables...`, `Đang kiểm tra / cập nhật catalog...` | | Trước bulk | `dung lượng cần tải ~N MB` (label=...) | | Đang tải bulk | `AddressablesCatalogUpdater: N% (x/y MB) — key=...` (mỗi ~5%) | | Xong (không cần tải) | `Hoàn tất — không cần tải catalog/bulk (version không đổi).` | | Xong (đã tải hết) | `Bootstrap hoàn tất — đã tải xong hết nội dung` | | AddressableManager | `Bootstrap gate xong`, `InitializeAsync xong — sẵn sàng load asset` | Lỗi dùng `Debug.LogError` cùng tiền tố `[Cuong]`. --- *Tài liệu mô tả hành vi và cấu hình trong repo; chi tiết API theo đúng phiên bản `com.unity.addressables` nên đối chiếu [Unity Manual: Addressables](https://docs.unity3d.com/Packages/com.unity.addressables@latest).*