Files
test/Docs/Addressables-Bootstrap.md
2026-05-27 17:48:15 +07:00

320 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/<Platform>/`. Để 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``_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/<Platform>`** 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)` (1100).
### 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).*