Theme là lớp sự kiện. Skin đổi ván chơi.
Theo checklist Skin & Theme V2 (DEC-253), theme chỉ là lớp trang trí sự kiện ở Home và popup Settings: logo + anim logo, con ma bay, nút Nâng cao tự lún, đàn dơi khi bấm nút, nhạc Home, khung popup Settings. Gameplay không thuộc theme. Skin là phần ván chơi (khối, màu ăn hàng, nền + bàn ván từ bg.png/board.png trong gói skin, FX ăn hàng, tiếng đặt/ăn, nhạc ván), đổi khi thanh đủ 25/25. Phần còn lại là giao diện Block Crush cố định. Cả hai tải bằng zip cùng một cơ chế. Hai thứ độc lập, mỗi người chơi có kho theme và kho skin riêng, bắt đầu mặc định hoặc theo nguồn cài. Theme đổi theo luật Remote Config (RC) gồm input + công thức; skin đổi khi thanh tiến độ đầy. Theme hiển thị chọn theo 6 lớp ưu tiên cố định; skin không đổi theme. Mỗi theme định danh bản bằng bundle.sha256, như skin. Zip theme không có manifest.json: mỗi slot có tên file cố định, như block/cube_N.png của skin; token và nhạc Home nằm trong themes.json (DEC-254).
Đọc trước để không hiểu nhầm
Theme và skin là hai thứ độc lập, mỗi thứ có kho riêng của từng người chơi. Bảng dưới định nghĩa các từ dùng trong trang này; cột cuối ghi điều hay bị hiểu nhầm.
| Thuật ngữ | Nghĩa | Không phải là |
|---|---|---|
| Theme | Lớp trang trí sự kiện (DEC-253): logo Home + anim lặp, con ma bay qua Home, nút Nâng cao tự lún, đàn dơi khi bấm nút ở Home/Settings, nhạc Home, khung + toggle popup Settings. Đổi theo thời gian hoặc theo luật trong RC (6 lớp ưu tiên, mục 05). Định danh bằng theme_id (vd default, ocean). | Không đổi gì trong ván (nền, bàn, HUD, khối, âm/FX/rung ván), không đổi Adventure, font, màu chung. Đổi theme không làm đổi skin. |
| Skin (skin khối) | Phần ván chơi: 7 khối, màu ăn hàng, nền + bàn ván (bg.png, board.png trong gói, DEC-253), tiếng đặt/ăn, nhạc ván, FX ăn hàng riêng. Đổi khi thanh tiến độ đủ 25/25 theo logic hiện tại. Định danh bằng skin_id số (vd 1000, 4003). | Không đổi Home, HUD, popup; không đổi theme. Thanh hệ số (+0.1 tới x2) không đổi nền. |
| Gói skin | Zip riêng của một skin (Remote_Skin_Material_<id>.zip): 7 ảnh khối, bg.png/board.png tuỳ chọn, FX ăn hàng riêng. bundle{url,sha256,size} gắn thẳng vào skin. | Không phải theme. Thay trường family trong code (plan bỏ). |
| Gói theme | Zip riêng của một bản của một theme (vd themes/ocean_a1b2c3d4.zip): mọi asset của theme. bundle{url,sha256,size} gắn vào theme; bundle.sha256 là định danh bản. Tải, kiểm, giải nén y như gói skin. | Không tải từng file lẻ. Sửa một file nhỏ thì tải lại cả zip mới. |
| RC (Remote Config) | Bảng key/giá trị Firebase gửi xuống app: cờ, ID, URL, luật. Sửa trên Firebase, không cần phát hành app. | Không chứa từng đường dẫn ảnh. |
| Catalog theme | File themes.json trên backend cạnh các zip theme (như skins.json của skin; đường dẫn cố định ở gốc CDN theme, DEC-251): danh sách theme server cho phép, mỗi theme có bundle (url, sha256, size), min_app_version, activation. Không có version gốc; trùng id thì bỏ cả catalog, giữ bản đã lưu (DEC-240, DEC-250). | Không phải key RC chứa JSON (bỏ từ DEC-250). |
| Tên file cố định | Zip theme không có manifest.json (DEC-254). Mỗi slot có đường dẫn cố định trong zip (mục 03). Có file thì slot chạy; thiếu thì giữ art gốc. | Không khai type, atlas, frame, preload. |
| Asset ID | Tên ổn định của một chỗ hiển thị, vd home.button.play. | Không phải tên file. |
| Token | Giá trị không phải file: màu chữ, chu kỳ anim, giới hạn FX. Nằm trong object theme ở themes.json (DEC-254), kiểm từng token khi đọc. | Không nằm trong zip; đổi token không cần zip mới. |
| Kho (theme / skin) | Danh sách theme hoặc skin một người chơi đã có, lưu trong save trên máy. | Không phải pool chung của server. |
| Pool | Toàn bộ theme/skin server cho phép. | Không phải kho người chơi. |
| Rule (luật RC) | Điều kiện server cấu hình: input + công thức + cách chọn kết quả (pick). | Không phải code app. |
| Input | Biến app tự đếm đưa vào công thức: games_played, days_since_install, skin_id... | — |
| Công thức | Biểu thức như games_played >= 10 && games_played % 5 == 0, chạy bằng bộ đánh giá an toàn trong game/core/. | Không phải script tuỳ ý. |
| Fallback / LKG | Thiếu asset hoặc theme lỗi thì lấy bản đã tải đủ gần nhất (LKG), rồi tới default đóng gói. | — |
| Creative / deeplink | Mã quảng cáo / đường link người chơi bấm để cài hay mở app; dùng chọn theme và skin bắt đầu. | — |
| Resolver | ThemeService/AssetResolver: đổi Asset ID thành file thật theo theme hiện tại. | — |
| Ranh giới màn/ván | Lúc mở màn mới hoặc bắt đầu ván mới. Theme chỉ đổi tại đây; phiên bản tài nguyên mới tải (theme hoặc skin) chỉ kích hoạt tại đây. | Không áp cho đổi skin đã sẵn tài nguyên (đổi trong ván). Theme không có ngoại lệ. |
| Điểm an toàn trong ván | Sau khi lượt đặt khối xử lý xong (ăn hàng, điểm, FX của lượt), không có khối đang kéo. | — |
| Tiến độ đổi skin | Cộng số hàng + cột bị xoá mỗi lượt; đủ ngưỡng (mặc định 25) thì đổi skin kế, giữ phần dư. | Không phải số lượt, số ô, hay thanh hệ số điểm. |
Theme sự kiện đổi (DEC-253)
- EH-01 Logo Home
home.logo - EA-06 Anim logo lặp
home.logo.anim(loopDurationMs,loopDelayMs) - EA-01 Con ma bay qua Home theo chu kỳ
home.ghost.anim(ghostIntervalMs) - EA-05 Nút Nâng cao tự lún theo chu kỳ
home.button.adventure.idle(intervalMs,pressDepth) - EF-06 Đàn dơi khi bấm nút ở Home và popup Settings
fx.button_tap(maxConcurrent,cooldownMs) - ES-01 Nhạc Home
audio.music.home - EG-08 Popup Settings:
ui.popup.background,ui.popup.inner_frame,ui.popup.ornament,ui.button.close,settings.toggle.*; tokenui.text.title,ui.text.item,color.text.accent
Skin đổi
- 7 hình khối trên board và khay (
block/cube_1..7.png, DEC-245) - Màu ăn hàng (
matchColors), danh sách màu sinh khối (colorPool, 1-7 màu, giữ nguyên list) - Nền ván + bàn ván:
bg.png,board.pngở gốc zip skin; thiếu thì ảnh gốc (DEC-253) - FX ăn hàng riêng của skin (trong gói skin)
- Âm là key dùng chung ngoài zip (DEC-247):
putAudioKey,lineClearSfxKey(DEC-248),bgmName(nhạc ván, ghi đè nhạc ván mặc định) - Thanh tiến độ 25 hàng/cột dẫn tới skin kế; thanh hệ số không đổi nền
Block Crush cố định (không thuộc cả hai)
- Nền Home, nút/tab Home, bong bóng tiến độ
- Adventure (chọn cấp + ván Nâng cao)
- HUD ván, nút Settings trong ván (bỏ EG-03)
- Popup Ngôn ngữ, Cài Đặt+, Revive, Settlement
- Font, màu chữ chung, âm UI
- Anim/FX/âm/rung chung trong ván (FX chung bật tắt bằng RC
fx_*) - Nhạc ván mặc định (bỏ ES-02); skin
bgmNameghi đè
ocean + skin 4003. Thanh tiến độ đầy, đổi sang skin 4010: khối, nền/bàn ván và tiếng ăn đổi theo skin; logo Home, con ma, đàn dơi và popup Settings vẫn là ocean. Server bật luật đổi theme sang halloween: logo, anim Home, nhạc Home, popup Settings đổi; ván chơi vẫn là skin 4010.Từ lúc mở app tới đổi theme, đổi skin
Trái: khởi động, chọn theme bắt đầu và tải an toàn. Phải trên: kho theme nhận theme theo nguồn; ở mỗi ranh giới màn/ván, luật RC (input + công thức) quyết định có đổi theme. Phải dưới: kho skin riêng; thanh tiến độ đầy thì đổi skin, theme không đổi. Ô vàng là điểm quyết định, ô đỏ là đường lỗi/fallback, ô viền đứt là key RC.
Hai luật thời điểm: (1) phiên bản tài nguyên mới tải (theme hoặc skin) chỉ kích hoạt ở ranh giới màn/ván; (2) đổi sang skin đã sẵn tài nguyên được làm giữa ván, tại điểm an toàn sau lượt, không đổi hình khối đang kéo. Theme chỉ đổi ở ranh giới, không có ngoại lệ. Theme chưa tải đủ asset không được luật RC chọn; skin chưa tải đủ gói của nó không được chọn làm skin kế. Không tải code thực thi (shader, script) từ server. Không A/B bucket ở giai đoạn này.
Toàn bộ trường cần cho 1 theme
Một theme gồm một object trong list themes của file themes.json ở gốc CDN theme (DEC-250, DEC-251; cùng cơ chế với skins.json) và zip của theme (tên file cố định, không có manifest, mục 03). Bên trái là khung JSON đầy đủ. Các tab bên dưới giải thích từng trường, kể cả trường nhỏ bên trong activation, tokens và skin khối (skin thuộc catalog riêng, viết ở đây để tra cùng chỗ). Danh sách slot nằm ở mục 04.
Trong themes.json (backend)
{
"id": "halloween",
"bundle": { "url": "themes/halloween_a1b2c3d4.zip", "sha256": "64 ký tự hex", "size": 1823112 },
"min_app_version": "1.4.0",
"activation": {
"enabled": true,
"priority": 10,
"start_at": "2027-10-25",
"end_at": "2027-11-02",
"min_level": 5,
"max_level": 0,
"creative_ids": ["halloween_ad"]
},
"audio": { "home": "bgm_halloween" },
"tokens": {
"ui.text.title": "#FFB347",
"ui.text.item": "#FFFFFF",
"color.text.accent": "#FF7A00",
"logo.loopDurationMs": 2000,
"logo.loopDelayMs": 3000,
"ghost.intervalMs": 15000,
"ghost.durationMs": 4000,
"ghost.path": "left_to_right",
"ghost.layer": "above_background",
"adventure_button.intervalMs": 5000,
"adventure_button.pressDepth": 0.08,
"adventure_button.durationMs": 300,
"button_tap.maxConcurrent": 3,
"button_tap.cooldownMs": 300,
"button_tap.particleCount": 5,
"button_tap.lifetimeMs": 1200,
"button_tap.spawnAnchor": "tap_point"
}
}Gốc themes.json: map audio dùng chung
{
"themes": [ /* các theme như trên */ ],
"audio": {
"bgm_halloween": { "url": "audio/bgm_halloween_9f8e7d6c.ogg", "sha256": "64-hex", "size": 812345 }
}
}Một skin (catalog skin riêng, không nằm trong theme)
{
"id": 4042,
"matchColors": [
["#F2ECE5","#A279A9","#3C253E"], ["#F5F8FA","#7C8BCC","#333565"],
["#F2ECE5","#A279A9","#3C253E"], ["#F5F8FA","#7C8BCC","#333565"],
["#F2ECE5","#A279A9","#3C253E"], ["#F5F8FA","#7C8BCC","#333565"],
["#F2ECE5","#A279A9","#3C253E"]
],
"colorPool": [1,2],
"putAudioKey": "s_put",
"lineClearSfxKey": "marble_streak",
"bgmName": "bgm_spring",
"bgColor": "",
"bundle": {
"url": "skins/Remote_Skin_Material_4042.zip",
"sha256": "64-hex",
"size": 812345
}
}Đọc khung này thế nào
- Cấp theme (
themes.json): id, bundle, min_app_version, audio, tokens. Không cóversion; định danh bản làbundle.sha256(DEC-254). Theme không có trường skin; đổi theme không đổi khối. - activation: theme sự kiện (lớp 4): ngày, level, creative quảng cáo; xét ở mọi ranh giới, không ghi vào theme riêng của người chơi. Theme riêng đổi do RC
theme_switch_rules(mục 05). - Skin khối: nằm ở catalog skin riêng (khung thứ hai). Bộ khối tối đa 7 màu (thiếu màu nào dùng mặc định), màu hiệu ứng, âm thanh, nhạc riêng. Đổi khi thanh tiến độ đầy.
- tokens (trong theme, DEC-254): màu chữ, chu kỳ anim, giới hạn FX (tab tokens). Token sai bị bỏ, dùng mặc định; theme vẫn bật.
- audio.home: key nhạc Home trong map
audioở gốcthemes.json, không nằm trong zip. - File ảnh/Spine: nằm trong zip theo tên cố định (mục 03). Thiếu file nào thì slot đó giữ art gốc.
Giải thích từng trường
Cây thư mục và tên file trong zip
Theme và skin đều là zip với tên file cố định; app nhận slot theo đường dẫn, không đọc manifest (DEC-245 cho skin, DEC-254 cho theme). Gốc zip là một thư mục <id>/ hoặc để file ngay ở gốc. File ngoài danh sách bị bỏ qua. Thiếu file thì slot đó giữ art gốc, trừ 7 khối của skin là bắt buộc.
Zip theme bỏ manifest.json, schema_version, theme_id, type, atlas_path, frame, preload; theme bỏ version. Token và nhạc Home khai trong themes.json (mục 02). Code đổi ở Task 13.
Zip theme · themes/halloween_a1b2c3d4.zip
halloween/
├── home/
│ ├── logo.png
│ ├── logo_anim.json
│ ├── logo_anim.atlas
│ ├── logo_anim.png
│ ├── ghost.json
│ ├── ghost.atlas
│ └── ghost.png
├── fx/
│ ├── button_tap.json
│ ├── button_tap.atlas
│ └── button_tap.png
└── settings/
├── popup_bg.png
├── popup_inner.png
├── popup_ornament.png
├── close.png
├── toggle_sound_on.png
├── toggle_sound_on_press.png
├── toggle_sound_off.png
├── toggle_sound_off_press.png
├── toggle_bgm_on.png
├── toggle_bgm_on_press.png
├── toggle_bgm_off.png
├── toggle_bgm_off_press.png
├── toggle_vibration_on.png
├── toggle_vibration_on_press.png
├── toggle_vibration_off.png
└── toggle_vibration_off_press.png
Không có trong zip:
nhạc Home → themes.json: audio.home + map audio
màu, chu kỳ → themes.json: tokens
nút Nâng cao lún (EA-05) → tween, chỉ cần tokensZip skin · skins/Remote_Skin_Material_1022.zip
1022/
├── block/
│ ├── cube_1.png
│ ├── cube_2.png
│ ├── cube_3.png
│ ├── cube_4.png
│ ├── cube_5.png
│ ├── cube_6.png
│ └── cube_7.png
├── shadow.png
├── bg.png
├── board.png
├── crown.png
└── effects/
├── gameplay_eliminate_022.json
├── gameplay_eliminate_022.atlas
├── gameplay_eliminate_022.png
├── gameplay_ready_eliminate_022.json
├── gameplay_ready_eliminate_022.atlas
└── gameplay_ready_eliminate_022.png
022 = 3 số cuối của id skin, đệm 0 (skin 4042 → 042).
Không có trong zip:
putAudioKey, lineClearSfxKey, bgmName → key trong map audio của skins.json
matchColors, colorPool → skins.json
Đếm thật trên 46 zip mẫu:
cube_1..7 46/46 · shadow 46 · bg, board 44
crown 32 · eliminate 24 · ready_eliminate 7Từng file trong zip theme
| Đường dẫn | Bắt buộc | Định dạng | Kích thước | Slot (checklist) | Thiếu thì sao |
|---|---|---|---|---|---|
home/logo.png | Tuỳ chọn | PNG RGBA | 767×353 (như logo_club_5) | home.logo · EH-01 | Logo gốc |
home/logo_anim.json + .atlas + .png | Tuỳ chọn, đủ 3 file | Spine 4.2 | Khung bằng logo 767×353 | home.logo.anim · EA-06 | Logo tĩnh (logo.png hoặc gốc). Có thì thay ảnh logo, chạy lặp theo logo.* |
home/ghost.json + .atlas + .png | Tuỳ chọn, đủ 3 file | Spine 4.2 | Tự do, nên dưới 512 px | home.ghost.anim · EA-01 | Không có ma bay |
fx/button_tap.json + .atlas + .png | Tuỳ chọn, đủ 3 file | Spine 4.2 | Nên dưới 256 px mỗi con | fx.button_tap · EF-06 | Bấm nút không có đàn dơi |
settings/popup_bg.png | Tuỳ chọn | PNG RGBA, 9-slice | 950×638 (như pop_bg1), giữ viền cũ | ui.popup.background · EG-08 | Khung gốc |
settings/popup_inner.png | Tuỳ chọn | PNG RGBA, 9-slice | 100×100 (như bg_inner_frame) | ui.popup.inner_frame · EG-08 | Khung trong gốc |
settings/popup_ornament.png | Tuỳ chọn | PNG RGBA | 552×50 (như huawen) | ui.popup.ornament · EG-08 | Hoa văn gốc |
settings/close.png | Tuỳ chọn | PNG RGBA | 78×78 (như common_btn_close) | ui.button.close · EG-08 | Nút X gốc |
settings/toggle_<t>_on.png, _on_press, _off, _off_press<t> = sound, bgm, vibration | Tuỳ chọn, đủ 4 file mỗi toggle | PNG RGBA | Như sprite gốc set_btn_<t>On, Dn, Off, offDn | settings.toggle.* · EG-08 | Toggle đó dùng ảnh gốc. Toggle skin không theme |
Từng file trong zip skin (đang chạy, planFlatPack ở skin_bundle.cpp:170-206)
| Đường dẫn | Bắt buộc | Định dạng | Kích thước | Dùng ở đâu | Thiếu thì sao |
|---|---|---|---|---|---|
block/cube_1.png … cube_7.png | Bắt buộc, đủ 7 | PNG RGBA | 106×106 (kCubeStdPx) | 7 hình khối trên bàn và khay, chỉ số theo colorPool | Cả gói bị từ chối, giữ skin cũ |
shadow.png | Tuỳ chọn | PNG RGBA | 106×106 | Bóng khối khi kéo | Bóng mặc định |
bg.png | Tuỳ chọn | PNG RGBA | Cao 1707; rộng 960 hoặc dải mỏng (4, 20 px) được kéo giãn ngang | Nền ván (game_bg), DEC-253, Task 13 | bgColor, rồi nền gốc |
board.png | Tuỳ chọn | PNG RGBA | Khoảng 870×870 | Bàn: nền, khung, ô trống (qipannew, QiPanxin), DEC-253, Task 13 | Bàn gốc |
crown.png | Tuỳ chọn | PNG RGBA | Theo bản gốc | Vương miện trên khối | Vương miện mặc định |
effects/gameplay_eliminate_<NNN>.json + .atlas + .png | Tuỳ chọn, đủ 3 file | Spine 4.2 (xuất từ 3.8.99), atlas 1 trang RGBA8888 | — | Tia chạy dọc hàng/cột khi ăn | FX ăn hàng chung |
effects/gameplay_ready_eliminate_<NNN>.json + .atlas + .png | Tuỳ chọn, đủ 3 file | Spine 4.2 | — | Khối nhấp nháy báo sắp ăn | Không nhấp nháy riêng |
config.json ở gốc | Không được có | — | — | Đánh dấu gói kiểu cũ | Có thì đọc như gói cũ, không phải gói phẳng |
Luật đóng gói chung
| Luật | Theme | Skin |
|---|---|---|
| Tên zip | themes/<id>_<sha8>.zip (sha8 = 8 ký tự đầu sha256 của zip) | skins/Remote_Skin_Material_<id>.zip |
| Gốc zip | Một thư mục <id>/ (như mẫu) hoặc file ngay ở gốc. Không lồng thêm cấp. | |
| Tên file | Đúng tên trong bảng, chữ thường, ký tự [a-z0-9_.-]. Tên trong effects/ tối đa 96 ký tự, không bắt đầu bằng ., không chứa ... | |
| Ảnh | PNG RGBA 8-bit. Giữ kích thước như ảnh gốc ghi trong bảng để khỏi lệch layout. | |
| Spine | Spine 4.2 (runtime của engine). Ba file cùng tên gốc .json + .atlas + .png; atlas một trang, tên trang trong .atlas trùng file .png. Thiếu một file thì cả slot bỏ. | |
| Không đưa vào zip | Âm thanh (dùng map audio của catalog), JSON cấu hình, manifest.json, .DS_Store, __MACOSX/, file nháp. | |
| sha256 + size | Tính trên cả file zip, ghi vào bundle trong catalog. Sửa một file = zip mới = sha256 mới; app tải lại đúng theme/skin đó. | |
| Công cụ | node tools/theme/validate.mjs <dir> rồi node tools/theme/build-theme-catalog.mjs <dir>… --out <dir> --min-app-version 1.4.0 (đóng zip, tính sha, sinh themes.json; Task 13 cập nhật cho cây mới) | node tools/skin/convert_skins.mjs sinh skins.json; zip giữ nguyên từ gói gốc |
Danh mục asset ID theo từng tab
Đây là toàn bộ key nằm trong assets và tokens của một theme (tab Design tokens thuộc tokens, còn lại thuộc assets; tab Theme khối thuộc catalog skin). Cột Thấy ở đâu trong game nói bằng lời thường ID đó vẽ/phát cái gì. Mỗi tab khớp một mục 5.x trong PLAN. Cột Type hiện tại là định dạng game đang dùng thật (đo từ game/Content và game/Source). Cột Ghi chú nêu type đề xuất cho server nếu khác. Lưới ảnh trên mỗi bảng là sprite hiện tại mà ID thay thế. Chỉ gồm 6 màn/popup đang có trong port: Home (Cơ bản), Nâng cao chọn cấp, Ingame Cơ bản, Ingame Nâng cao, popup Ngôn ngữ, popup Cài Đặt+.
Từ 28.10.2026 theme chỉ còn tab Theme sự kiện (DEC-253). Các tab khác là kiểm kê sprite/âm hiện tại của game: chúng về giao diện Block Crush cố định (hoặc theo skin: nền/bàn ván) và bị gỡ khỏi theme ở Task 13. Từ DEC-254 zip theme không còn manifest: slot đọc theo tên file cố định ở mục 03, file khác bị bỏ.
DEC-253: theme không còn đọc legacy.sprite.* / legacy.audio.* (Task 13 gỡ). Theme chỉ override slot sự kiện; slot thiếu thì không chạy anim/FX và dùng ảnh gốc.
Từng field làm gì, ví dụ ra sao
RC (Remote Config) là cấu hình trên Firebase, đổi được không cần phát hành app. Mỗi tab là một nhóm key. Field bên trong một theme (catalog) đã giải thích ở mục 02. Tab cuối là dữ liệu người chơi lưu trên máy (kho + tiến độ).
14 task, một đường rollback
Tách core schema, store, resolver, từng consumer, kho theme và QA để review độc lập. Task 9 mới cho kho theme + luật RC; Task 10 mới cho kho skin; Task 11 dọn schema skin cũ, bỏ icons (ảnh khối lấy từ file trong gói), bỏ clearSfxKey, thêm lineClearSfxKey (âm ăn hàng riêng skin, DEC-248); âm thanh là key dùng chung, thiếu thì tải từ map audio (DEC-247) ngay trong bản này. Task 12 đưa catalog theme về themes.json như skin (DEC-251); Task 13 thu theme về lớp sự kiện, nền/bàn ván theo skin; Task 14 thêm slot sự kiện mới (DEC-253). Adventure gameplay per-level giữ TBD.
Schema + validator
C++ thuần. ID, URL, hash, manifest (Task 13 gỡ theo DEC-254), giới hạn độ sâu JSON, activation, luật theme_switch_rules sai bị bỏ qua. Skin: bundle bắt buộc, validator colorPool list.
Store + LKG (zip)
Tải zip theme (.tmp tải tiếp được), kiểm size + sha256, giải nén vào <sha8>.part, rename nguyên tử, ghi installed_theme_bundles, kiểm lại lúc mở app, quota LRU, offline, rollback. Theo đúng luồng gói skin.
Resolver + aliases
Semantic ID, 255 sprite alias, toàn bộ audio alias.
RC + deeplink
Chọn theme theo rule/creative, áp ở ranh giới màn.
UI toàn app
Home (Cơ bản), Nâng cao (chọn cấp + ingame), popup Ngôn ngữ, popup Cài Đặt+.
Ingame đầy đủ
Block, board, animation, sound, haptic, pet. FX độc lập theme đọc RC fx_*. Âm mở bàn audio.game.start, bảng sự kiện âm thanh, âm dọn sạch bàn của skin nối vào dọn sạch bàn.
Publish tooling
Validate, đóng zip cố định (cùng nội dung ra cùng sha256), hash, diff, tên bất biến <theme>_<sha8>.zip.
QA + rollback
Trước/sau, offline, file hỏng, LKG, crash context. Case skin: 24 + 2 hàng còn 1, khối đang kéo không đổi, pool qua resume, âm all-clear/mở bàn. Ma trận Android / Web (iOS, macOS để sau, DEC-241): tải lần đầu, tắt app giữa lúc tải, mở lại không tải lại, server đổi zip một theme (sha256 mới).
Kho theme + luật RC
Kho theme theo người chơi, grant theo nguồn, theme bắt đầu, bộ đánh giá công thức trong core, theme_switch_rules, chọn theme hiển thị theo 6 lớp ưu tiên, random có seed, bản riêng theo bundle.sha256. Không đụng skin.
Kho skin
Kho skin theo người chơi, skin độc lập (bỏ family, thêm bundle), colorPool giữ list, skin bắt đầu theo nguồn, skin_progress_target (số hàng/cột) thay 25 hard-code, đổi skin giữa ván ở điểm an toàn, skin_next_rule. Skin không đổi theme. Pool rỗng về skin 1000 + analytics.
Dọn schema cũ + đổi tên
Xoá family, SkinFamily, markFamilyDownloaded, remoteFamilyTag, skinIdOfFamily, colorPoolSize, displayColorPoolSize, kSkinNeedNum, 25 hard-code ở BoardPlay. Bỏ icons, bỏ clearSfxKey, thêm lineClearSfxKey; dọn sạch bàn dùng âm chung (DEC-248) (code, skins.json qua tools/skin/convert_skins.mjs, test, migrate save cũ); đọc block/cube_N.png tên cố định trong gói; giữ putAudioKey, bgmName; server phát map audio (DEC-247). Bỏ tương thích manifest cũ; chỉ giữ migrate save trên máy (cờ đã tải). Cổng: grep không còn symbol cũ.
Catalog theme như skin
themes.json cố định ở gốc CDN theme, list themes có id + map audio; app đóng sẵn config/themes.json, merge như skin. Bỏ RC theme_catalog_url, asset_base_url (DEC-251).
Thu theme về lớp sự kiện
Xoá khỏi theme mọi ID ngoài slot sự kiện; gỡ đường đọc theme ở ván, Adventure, popup khác, font, màu, legacy.sprite.*. Nền/bàn ván lấy bg.png/board.png của skin. Bỏ manifest.json + version: cài zip theo tên file cố định, tokens/audio đọc từ themes.json, sửa theme_catalog, ThemeStore, ThemeService, tools/theme/* (DEC-254). Cổng: grep BoardView, BoardFx*, FxService, UnityClip không gọi ThemeService.
Slot theme sự kiện
ThemeEventFx: anim logo lặp, con ma, nút Nâng cao lún, đàn dơi (cooldown + giới hạn đồng thời), nhạc Home, popup Settings. Một instance mỗi anim, dừng khi rời Home, không chặn input; theme default không có gì mới.
Ở mỗi ranh giới màn/ván, một hàm trong core (resolveDisplayTheme) đi từ lớp 1 xuống; lớp đầu cho ra theme dùng được (có trong catalog, đạt min_app_version, zip đã tải và qua kiểm) thắng, chưa dùng được thì xuống lớp kế, không chờ. (1) theme_enabled=false → default. (2) home/ingame/adventure_theme_override_id, chỉ màn đó. (3) theme_active_id khác rỗng, ép cả app. (4) Theme sự kiện theo activation (ngày UTC, level, creative), nhiều cái thì activation.priority. (5) current_theme_id: theme riêng của người chơi, đặt bởi theme_start_rules, đổi bởi theme_switch_rules hoặc người chơi tự chọn. (6) default trong app. Lớp 2-4 không ghi vào current_theme_id: hết sự kiện/ép thì người chơi về đúng theme của họ. theme_switch_rules chỉ chạy khi đang ở lớp 5/6, nên sự kiện không bị luật xoay đè và cooldown không bị tiêu. Skin không đổi theme (đã bỏ theme_link; muốn theme theo skin thì luật RC dùng input skin_id). Ví dụ: Halloween activation 25/10–02/11 priority 50 + luật every5: trong khung mọi người thấy halloween, every5 tạm dừng; hết hạn về theme riêng ở ván kế.
Gói theme và skin ghi vào FileUtils::getWritablePath(), lưu bền qua các lần mở app. Bố cục: skinpack/ (đã có) và themepack/<theme_id>/<sha8>/ (8 ký tự đầu sha256).
| Nền tảng | Thư mục | Ghi chú |
|---|---|---|
| Android | Context.getFilesDir() qua AxmolEngine.getInternalFilesDir | External nếu bật AX_USE_ANDROID_EXTERNAL_FILES_DIR. Gỡ app là mất gói. |
| iOS | NSDocumentDirectory | Đề xuất loại khỏi iCloud backup (NSURLIsExcludedFromBackupKey). Chưa chạy thử tải skin trên iOS. |
| Web/WASM | /axmolPersistPath/ (IDBFS) | Lưu bền nhờ FS.syncfs. Rủi ro: quota/trình duyệt xoá (đề xuất navigator.storage.persist()), RAM heap khi giải nén, CDN cần CORS, gói và cờ đã tải phải flush cùng lượt. |
Server cập nhật: theme tải <cdn>/themes.json lúc mở app (gốc CDN lấy lúc build như skin), merge như skin, lỗi thì giữ bản đã lưu, chưa có thì dùng config/themes.json đóng sẵn (DEC-250, DEC-251, không version gốc); catalog skin trên CDN cũng không còn version gốc (DEC-244), mỗi lần tải đều merge. Sau đó so từng gói: skin và theme đều chỉ so bundle.sha256 (url đổi mà sha256 giữ nguyên thì không tải lại; version của theme chỉ là nhãn). Skin/theme mới = id mới trong catalog. Đã tải = cờ đã lưu (downloaded_skins / installed_theme_bundles) VÀ thư mục trên đĩa qua kiểm lúc mở app. Tải xong = size + sha256 zip đúng, giải nén đủ vào .part, kiểm file đạt, rename xong, cờ đã lưu. Có trong kho = id trong owned_theme_ids / kho skin; dùng được khi vừa có trong kho vừa đã tải.
Adventure nâng cao: target, layout, difficulty, mission, theme riêng từng level từ server. Bổ sung sau, không chặn theme UI toàn app.