Theme server blueprint · cập nhật 27.10.2026

Theme đổi giao diện. Skin đổi khối.

Theme là phần giao diện của game (nền, nút, HUD, popup, font, nhạc, rung), đổi theo thời gian hoặc theo luật RC; skin là phần ingame (khối, màu ăn hàng, FX, tiếng đặt/ăn), đổi khi thanh đủ 25/25 theo logic hiện tại. 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.

255sprite ID hiện tại
280skin đã có metadata
2kho riêng: theme + skin
12RC key mới cho kho, luật, tiến độ
00 / THUẬT NGỮ

Đọ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ĩaKhông phải là
ThemePhần giao diện của game: nền, logo, nút, HUD, popup, font, màu, nhạc nền, âm UI, rung. Đổi theo thời gian hoặc theo luật trong RC (6 lớp ưu tiên, mục 04). Định danh bằng theme_id (vd default, ocean).Không đổi hình khối. Đổi theme không làm đổi skin.
Skin (skin khối)Phần ingame: 7 khối, màu ăn hàng, tiếng đặt/ă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 nền/nút/popup, không đổi theme.
Gói skinZip riêng của một skin (Remote_Skin_Material_<id>.zip): 7 ảnh khối + 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 themeZip 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.
CatalogFile JSON trên CDN: danh sách theme, gói zip, Asset ID → file. catalog_version ở gốc chặn catalog cũ.Không phải RC.
Asset IDTên ổn định của một chỗ hiển thị, vd home.button.play.Không phải tên file.
Asset entryMột dòng catalog: Asset ID → type, path, sha256, preload.—
TokenGiá trị không phải file: màu, cỡ chữ, scale (tokens.*).—
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.
PoolToà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.
InputBiến app tự đếm đưa vào công thức: games_played, days_since_install, skin_id...—
Công thứcBiể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 / LKGThiế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 / deeplinkMã 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.—
ResolverThemeService/AssetResolver: đổi Asset ID thành file thật theo theme hiện tại.—
Ranh giới màn/vánLú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ánSau 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 skinCộ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 đổi

  • Nền Home, nền Ingame, nền Adventure
  • Logo, nút Play / Cơ bản / Nâng cao, icon nút
  • Khung board, ô trống, HUD (điểm, số, nút cài đặt)
  • Popup Ngôn ngữ, Cài Đặt+, Revive, Settlement
  • Font, màu chữ, token màu/scale
  • Nhạc nền, âm UI (bấm nút, mở popup), âm mở bàn audio.game.start, âm xoá hàng audio.block.clear, bảng rung

Skin đổi

  • 7 hình khối trên board và khay
  • Màu ăn hàng (matchColors), danh sách màu sinh khối (colorPool, giữ nguyên list)
  • Tiếng đặt khối (putAudioKey), tiếng dọn sạch bàn (allClearSfxKey, gốc ClearSfxKey)
  • FX ăn hàng riêng của skin (trong gói của skin)
  • bgmName, bgColor: chỉ ghi đè theme khi khác rỗng
  • Thanh tiến độ (số hàng/cột xoá, mặc định 25) dẫn tới skin kế

Không thuộc cả hai (RC fx_*)

  • FX chung: combo, ăn nhiều hàng, rung màn, chữ khen
  • Hệ số tốc độ, thời lượng FX
  • Bật/tắt từng FX chung
Ví dụ. Người chơi đang theme ocean + skin 4003. Thanh tiến độ đầy, đổi sang skin 4010: khối và tiếng ăn đổi, nền và nút Play vẫn là ocean. Server bật luật đổi theme sang candy: nền và nút đổi, khối vẫn là 4010.
01 / LUỒNG

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.

Bước xử lýQuyết địnhLỗi / fallbackKết quả trên mànRC key điều khiển
Nguyên tắc

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.

02 / MỘT THEME

Toàn bộ trường cần cho 1 theme

Một theme là một object trong catalog.themes.<theme_id>. 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, asset entry và skin khối (skin thuộc catalog riêng, viết ở đây để tra cùng chỗ). Danh sách key đầy đủ trong assets/tokens nằm ở mục 03.

Khung JSON của 1 theme

"ocean": {
  "version": 3,
  "bundle": { "url": "themes/ocean_a1b2c3d4.zip", "sha256": "64 ký tự hex", "size": 4823112 },
  "fallback_theme_id": "default",
  "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": ["ocean_ad"]
  },
  "tokens": {
    "color.background.ingame": "#0F3550",
    "font_scale.ui": 1.0
  },
  "assets": {
    "home.background": {
      "path": "home/bg.webp",
      "type": "image",
      "atlas_path": "",
      "frame": "",
      "preload": "boot"
    }
  }
}

Một skin (catalog skin riêng, không nằm trong theme)

{
  "id": 4042,
  "icons": ["cube_1.png","cube_2.png","cube_3.png","cube_4.png",
            "cube_5.png","cube_6.png","cube_7.png"],
  "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",
  "allClearSfxKey": "",
  "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: version, fallback, min_app_version. 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 04).
  • 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: màu, font, scale, âm lượng (key ở mục 03, tab Design tokens).
  • assets: mỗi Asset ID ở mục 03 ứng với một asset entry. Theme khác default chỉ cần khai ID muốn đổi.

Giải thích từng trường

03 / INVENTORY

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+.

Không bỏ sót

Ngoài ID semantic, cả 255 tên trong layout/_sprites.json tự import thành legacy.sprite.<name>. Mọi key trong config/audio.json tự import thành legacy.audio.<key>. Theme chỉ cần override ID muốn đổi; ID còn lại fallback về default.

04 / FIELDS

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 độ).

05 / DELIVERY

11 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ũ và đổi tên allClearSfxKey ngay trong bản này. Adventure gameplay per-level giữ TBD.

TASK 1

Schema + validator

C++ thuần. ID, URL, hash, fallback cycle, activation, luật theme_switch_rules sai bị bỏ qua. Skin: bundle bắt buộc, validator colorPool list.

TASK 2

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.

TASK 3

Resolver + aliases

Semantic ID, 255 sprite alias, toàn bộ audio alias.

TASK 4

RC + deeplink

Chọn theme theo rule/creative, áp ở ranh giới màn.

TASK 5

UI toàn app

Home (Cơ bản), Nâng cao (chọn cấp + ingame), popup Ngôn ngữ, popup Cài Đặt+.

TASK 6

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, allClearSfxKey nối vào dọn sạch bàn.

TASK 7

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.

TASK 8

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 / iOS / Web: 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).

TASK 9 · 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.

TASK 10 · MỚI

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.

TASK 11 · MỚI

Dọn schema cũ + đổi tên

Xoá family, SkinFamily, markFamilyDownloaded, remoteFamilyTag, skinIdOfFamily, colorPoolSize, displayColorPoolSize, kSkinNeedNum, 25 hard-code ở BoardPlay. Đổi clearSfxKey → allClearSfxKey (code, 280 skin trong skins.json qua tools/skin/convert_skins.mjs, test). 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ũ.

TASK 1 ├── TASK 2 ──┐ ├── TASK 3 ──┼── TASK 4 ──┬── TASK 9 ── TASK 10 ── TASK 11 ──┐ │ ├── TASK 5 ──┼──────────────────────────────────┤ │ └── TASK 6 ──┴──────────────────────────────────┼── TASK 8 └── TASK 7 ──────────────────────────────────────────────────┘
Chọn theme hiển thị: 6 lớp ưu tiên

Ở 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ế.

Nền tảng: gói lưu ở đâu

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ảngThư mụcGhi chú
AndroidContext.getFilesDir() qua AxmolEngine.getInternalFilesDirExternal nếu bật AX_USE_ANDROID_EXTERNAL_FILES_DIR. Gỡ app là mất gói.
iOSNSDocumentDirectoryĐề 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.
Đã tải, tải xong, có trong kho, bản mới

Server cập nhật: version gốc của catalog/manifest tăng mới xét (skin hiện tại: SkinStore::merge trả Stale nếu không tăng). Sau đó so từng gói: skin so url/sha256/size, theme so bundle.sha256 (giống skin; 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.

TBD

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.