11-monetization.md · Cập nhật lần cuối: 2026-09-06 23:05 +07 · sha256 nguồn c24a2735f3a611 — Monetization (IAA + IAP): quảng cáo là bản dùng thử của cửa hàng
Nguồn framework:
docs/framework/10-monetization-iaa-iap.md§1–§10 (nguồn chính),docs/framework/07-progression-managers.md§2.4, §3.4, §3.6,docs/framework/09-prestige-rebirth.md§3.5,docs/framework/01-economy-money.md§3.2, §3.6,[liveops catalog: LIVEOPS_SURFACE.md]§2–§6,[liveops catalog: UNLOCK_GATES.md]§1, §3,[phân tích nội bộ]U2, U4. Cấp bằng chứng: L1 ANALYSIS — dựng lại clean-room từ disasm + metadata + scene + probe read-only trên bản Play Store chính hãng của user; không phải source gốc. Mỗi dòng bên dưới mang nhãn bằng chứng riêng. Ranh giới R1 / R9: SDK quảng cáo / IAP / attribution / offerwall không được tích hợp vào bản restore; chúng nằm sau interface fail-closed và không bao giờ cấp thưởng. Trang này chưng cất thiết kế kiếm tiền (placement, gate, offer, timing), không chưng cất nội tại SDK.
Mục tiêu
Bạn muốn biết một game idle thương mại bán gì, bán bằng quảng cáo hay bằng tiền, đặt offer ở đâu, khi nào, và điều gì quyết định một người chơi có thấy quảng cáo hay không — để thiết kế tầng IAA/IAP cho game của mình mà không phá vòng lặp (doc 01/03) và không "phạt" người chơi. CatGunner bán đúng 4 thứ, mỗi thứ có một bản quảng cáo (nếm thử) và một bản IAP (vĩnh viễn) — đó là toàn bộ ý đồ, và mọi con số bên dưới đọc được từ binary + scene + một phiên probe sống.
Game gốc làm gì
Bốn hàng hoá — hai cột xếp tầng (doc 10 §1)
| Hàng hoá | Bán bằng quảng cáo (IAA) | Bán bằng tiền (IAP) | Mức bằng chứng |
|---|---|---|---|
| Nhân thu nhập / sát thương tạm thời | Buff ×3, 300 s/lần, trần 900 s | buff_pack → ×3 vĩnh viễn cả hai buff |
disasm-proven |
| Nhân phần thưởng một lần | Bonus sau Mine / Boss / Hunt / Challenge, Mission, Fish gacha | PlusPack (×2 Monster, ×2 Boss, ×1.5 Mine) | disasm-proven |
| Nhảy cóc tiến trình | Rebirth tăng cường ×3 (1 lượt/ngày) | catgunner_rebirth_single (thêm 1 lượt/ngày, mở ở Stage 100-1) |
disasm-proven |
| Bỏ ma sát | FreeCash link → rewarded ad tự trao thưởng | remove_forced_ads / remove_all_ads |
disasm-proven |
Điểm nối kỹ thuật của hai cột: hai cờ non-consumable Shop_manager.Is_Remove_AD_All / Is_Buff_Pack được tầng quảng cáo đọc trực tiếp (doc 10 §1, §4.1).
Tầng quảng cáo (IAA)
| Điều game làm | Con số / hằng số | Mức bằng chứng | Nguồn |
|---|---|---|---|
Lớp Admob không dùng Google AdMob — bọc SDK ADX (AdxRewardedAd, AdxInterstitialAd, AdxBannerAd); tên lớp là di sản |
3 định dạng: rewarded · interstitial · banner | metadata-proven | doc 10 §2.1; dump.cs:585348-585700 |
enum AD_Type = 10 loại phần thưởng của rewarded ad |
Money_Buff=0 … AD_Reward_Gun=9 |
metadata-proven | dump.cs:584959 |
| Trao thưởng trong một coroutine của game, không trong callback SDK | Admob.Reward_Get() → switch(ad_type) → đúng 1 manager |
disasm-proven | RVA 0x2A5F0B0 |
| Giãn cách giữa 2 rewarded ad | AD_Cool = 20.0f, đếm ngược Time.deltaTime trong LateUpdate; bấm sớm ⇒ Alert("Try in {0} seconds") |
disasm-proven | RVA 0x2A5F0B0, 0x2A58CC8, 0x2A59294 |
Đường tắt entitlement: đã mua remove_all_ads hoặc đã liên kết FreeCash ⇒ ShowRewardAd trao thưởng ngay, không hiện quảng cáo |
Is_Remove_AD_All \|\| FreeCash_manager.RewardAdRemoved |
disasm-proven | RVA 0x2A59294 |
Cùng vị ngữ đó là Shop_manager.Is_RewardBonus_Auto — dùng để tắt interstitial và đổi nút "xem QC" thành "nhận luôn" |
— | disasm-proven | RVA 0x2B2D510 |
5 Admob.Inter_Place khớp 1-1 với 5 field inter_daily_cnt_* của ServerConfig |
Rebirth=0, Mine=1, Challenge=2, Boss=3, Hunt=4 |
metadata-proven + disasm-proven | dump.cs:584976; Inter_DailyLimit 0x2A5CC04 |
| Bộ đếm interstitial nằm ở PlayerPrefs, không phải SaveData; reset khi đổi ngày | khoá inter_place_cnt_<place>, inter_place_cnt_whole, inter_place_date (yyyyMMdd) |
disasm-proven | 0x2A5C784, 0x2A5C7F0, 0x2A5D644 |
| Cổng interstitial = 7 điều kiện theo thứ tự (xem pseudocode) | — | disasm-proven | Inter_Placement_Able 0x2A5CD34 |
Interstitial không bao giờ chặn luồng game: gate fail ⇒ gọi onClosed() ngay |
— | disasm-proven | ShowPlacementInterstitial 0x2A5D508 |
| Chặn interstitial / banner | Is_Interstitial_Blocked = is_Interstitial_Disable \|\| Is_Remove_AD_forced \|\| Is_Remove_AD_All; Is_Banner_Blocked = Is_Remove_AD_forced \|\| Is_Remove_AD_All |
disasm-proven | 0x2A5B4D4, 0x2A5BC70 |
Banner chỉ chạy trong trận Boss / Hunt / Raid (show ở Enter_Cor, hide ở Game_Exit) |
BANNER_HEIGHT_DP = 50, BANNER_RETRY_DELAY = 30 s |
disasm-proven (xrefs) + metadata-proven | 0x2A5BCF4, 0x2A5BD58; dump.cs:585392-585404 |
| Đổi trạng thái IAP ⇒ huỷ ngay banner / ad đã nạp nếu bị chặn | Handle_InAppPurchaseChanged |
disasm-proven | 0x2A5C2CC |
| Hằng độ bền | REWARD_RETRY_BASE 15 s, REWARD_RETRY_MAX 120 s, REWARD_AD_EXPIRE_MIN 50 phút, LOAD_HARD_RESET_SEC 60 s |
metadata-proven | dump.cs:585374-585377 |
Giá trị serialize của Admob trong scene |
is_AD_Disable_Version 0, is_Interstitial_Disable 0, adLoadTimeout 12, interLoadingSeconds 1.2, interLoadMaxAttempts 1, interLoadWaitPerAttempt 3 |
scene-YAML | Main.unity:20647245-20647268 |
| 28 điểm kích hoạt quảng cáo trong code | rewarded 13 · interstitial 9 (5 place) · banner 3 show / 3 hide · OfferWall 2 · FreeCash 1 | disasm-proven | doc 10 §4 |
Súng thưởng quảng cáo weapon_02: trigger là hành vi (ghép súng lần đầu khi chưa có súng bậc ≥ 2) |
súng #14, đếm ngược 1800 s, một-lần-cho-đời-tài-khoản; xem QC ⇒ giữ vĩnh viễn; hết giờ ⇒ biến mất | disasm-proven | CheckAdGunEvent 0x2B0C74C, AddAdRewardGun 0x2B0C340, AdRewardGun_Acquire 0x2B0C508 |
Danh mục AD_Type (doc 10 §3, dispatch 0x2A5F0B0)
| # | AD_Type |
Điểm gọi ShowRewardAd |
Phần thưởng |
|---|---|---|---|
| 0 | Money_Buff |
Buff_Panel.AD_Watch 0x2AE4F38 |
moneyBuffTime = min(+300, 900) + Gamemanager.InvalidateMoneyCache() |
| 1 | Power_Buff |
Buff_Panel.AD_Watch |
powerBuffTime = min(+300, 900) |
| 2 | SpeedRun_Bonus |
SpeedRun_manager.Bonus_AD_Show 0x2B40E08 / SweepBonus_AD_Show 0x2B41A04 |
bonus chế độ Play/Challenge |
| 3 | Mine_Bonus |
Mine_manager.Bonus_AD_Show 0x2AF562C / 0x2AF5B28 |
bonus mỏ |
| 4 | Boss_Bonus |
Boss_manager.Bonus_AD_Show 0x2AF21A8 / 0x2AF2D94 |
bonus boss |
| 5 | Monster_Bonus |
Monster_manager.Bonus_AD_Show 0x2AFB094 / 0x2AFAEC4 |
bonus Hunt (PetCoin) |
| 6 | Mission_Reward |
Mission_manager.Reward_Get 0x2A71A34 |
nhận thưởng mission |
| 7 | Fish_Gacha |
Fish_manager.FreeGacha 0x2B00AB4 |
1 lượt gacha cá miễn phí |
| 8 | Upgrade_Rebirth |
Rebirth_manager.UpgradeRebirth_Start_By_Ad 0x2B233D0 |
bật is_UpgradeRebirth ⇒ thưởng Rebirth ×3 |
| 9 | AD_Reward_Gun |
Gun_manager.AdRewardGun_TryWatch 0x2B0C484 |
giữ vĩnh viễn khẩu súng thưởng |
IsBuffActive(type) trả true cho mọi type khi IsPermanentBuff (0x2AE4AF8) ⇒ buff_pack bật vĩnh viễn cả hai buff; GetBuffRemainingTime trả -1 khi vĩnh viễn (0x2AE4B4C) và Buff_Panel.Update (0x2AE4D4C) hiểu -1 là "ẩn nút xem QC, fill = 1.0" — disasm-proven, doc 10 §3, §5.
Phân khúc (segment) — công tắc tổng của IAA
| Điều game làm | Con số / hằng số | Mức bằng chứng | Nguồn |
|---|---|---|---|
ServerConfig_manager.ConfigResp có đúng 15 field, 12 là số điều khiển quảng cáo, 0 field kinh tế |
15 | metadata-proven + runtime-verified (live) | dump.cs:589438; LIVEOPS_SURFACE §2.2; doc 10 §2.3 |
Payload gửi lên ConfigReq (13 field) ⇒ server phân khúc theo nguồn cài đặt (AppsFlyer campaign/media_source), quốc gia, ngày cài, số lần mua, số reward-ad đã xem |
13 | metadata-proven | dump.cs:589413 |
EmbeddedDefaults(segment == 1) |
freecash_stage_min 15, freecash_stage_max_hud 35, offerwall_stage_min 10, inter_stage_min 12, inter_cooltime 0, inter_daily_cnt_rebirth 6, _mine 2, _challenge 0, _boss 0, _hunt 0, _whole 99999, weapon_02_ad_onoff 1 |
disasm-proven (.rodata) |
0x2AC7B20; LIVEOPS_SURFACE §2.3 |
EmbeddedDefaults(segment ≠ 1) |
4 ngưỡng stage = 99999, mọi cap = 0, ad-gun tắt ⇒ tắt sạch IAA |
disasm-proven | như trên |
Payload production thật cho user này: segment 0, rule_version "41", reason "ok", mọi ngưỡng 99999, mọi cap 0 ⇒ toàn bộ IAA tắt; user này kiếm tiền IAP-only |
— | runtime-verified (live) | doc 10 §2.3–§2.4 |
| Server trả đúng 15 key đã suy ra tĩnh, không thừa không thiếu ⇒ liveops không chạm balance | — | runtime-verified (live) | doc 10 §2.3; CONVERGENCE U4 |
Phân khúc gắn với package id: đổi bundle_id ⇒ server trả reason: default_rules_fallback, rule_version 0 |
— | runtime-verified (live) | doc 10 §2.3 |
Lộ diện = f(tiến trình) × f(segment): stageDisplay = Stage_Value_Max / 10 + 1 |
interstitial ≥ inter_stage_min (seg 1: 12); OfferWall ≥ offerwall_stage_min (10); FreeCash HUD trong [15, 35] |
disasm-proven | 0x2A5CD34, 0x2B2B20C, 0x2A6CF08 + 0x2A6D1F8 |
Firebase Remote Config chỉ 5 key: version gate + timehack_pardon + offerwall_event_onoff |
5 | VERIFIED | LIVEOPS_SURFACE §3 |
| Có công tắc QA đổi phân khúc / ép lộ diện ngay trong build shipped | — | metadata-proven | doc 10 §2.3; LIVEOPS_SURFACE §2.5 |
Nút QA đổi phân khúc AppsFlyerInit.Cheat_SetSegment(seg): ghi Segment (static 0x8) + cờ ưu tiên (0x21), lưu PlayerPrefs["af_segment"]/["af_segment_cheat"], đổi nhãn TMP, gọi ServerConfig_manager.Cheat_OnSegmentChanged() (0x2AC96D0) ⇒ phát OnConfigApplied + SaveSystem.Save |
— | disasm-proven | 0x2A600CC; 0x2AC96D0; doc 10 §2.3 |
Đường server đặt segment (ApplyServerSegment 0x2A6025C) thoát ở dòng đầu khi cờ QA đã bật (0x2a60320) ⇒ ưu tiên QA > server > mặc định 0 |
— | disasm-proven | 0x2A6025C; doc 10 §2.3 |
Ba tầng đừng lẫn (chỗ nhầm phổ biến nhất khi tự dựng lại hệ này):
| Segment | ServerConfig (ConfigResp) |
Placement | |
|---|---|---|---|
| Là gì | một con số dán lên người chơi (0/1/2) | bộ 15 field ứng với con số đó | một chỗ cụ thể trong game có thể chiếu quảng cáo |
| Trả lời | bạn thuộc nhóm nào | nhóm của bạn được đối xử ra sao | ở đây, ngay bây giờ, có được chiếu không |
| Ai đặt | server attribution (hoặc nút QA) | server liveops; mất mạng ⇒ EmbeddedDefaults |
người làm game đặt sẵn trong code/scene |
| Đổi khi nào | hiếm (lúc cài / lúc ép) | mỗi lần khởi động, hoặc khi segment đổi | không đổi — nó là địa điểm |
| Trong Cat Gunner | 3 giá trị | 15 field (12 số quảng cáo, 0 số kinh tế) | 6 loại × 28 điểm kích hoạt |
Đơn vị đo của mọi ngưỡng là stage hiển thị Stage_Value_Max / 10 + 1, không phải Stage_Value_Max thô
(0x2A6D118, 0x2B2B12C) — nhầm chỗ này thì mọi ngưỡng lệch mười lần mà không có lỗi nào nổ.
Tầng IAP
| Điều game làm | Con số / hằng số | Mức bằng chứng | Nguồn |
|---|---|---|---|
34 product id được TryApplyPurchase xử lý, ánh xạ sang 33 chỉ số offer (1..30, 36..38; vip_pass và vip_pass_2 chung chỉ số 12; 31..35 trống) |
34 | disasm-proven | ItemIdxFromProductId 0x2B2DE44; doc 10 §6.1 |
Đính chính LIVEOPS_SURFACE §6.1: mine_ticket, fish_free, banner_enter, banner_reward, banner_weapon, sp_iap, reward_hub, f_cash không phải product id — là chuỗi place / khoá analytics |
8 chuỗi | disasm-proven (xrefs literal) | doc 10 §2.2 |
Backend = Unity StoreController, có xác thực receipt phía server (2 Cloud Function) |
— | metadata-proven | dump.cs:600320-600321; ValidateAndComplete 0x2B33DAC |
| Chống trùng giao dịch | _inflightTxns, ProcessedTxnIds (ProcessedTxnIdsMax = 50) |
metadata-proven + disasm-proven | dump.cs:600334-600337 |
6 non-consumable là cờ static của Shop_manager |
Is_Remove_AD_forced@0x8, Is_Remove_AD_All@0x9, Is_AutoUpgrade_Pack@0xA, Is_Buff_Pack@0xB, Is_Dia_Pack@0xC, Is_Mine_Pack@0xD |
metadata-proven | dump.cs:600306-600311 |
| Dia kèm non-consumable | autoupgrade_pack +500 · buff_pack +1000 · dia_pack +16000 · mine_pack +500 · remove_*_ads 0 dia nhưng mở DiaPig |
disasm-proven + runtime-verified (live) | ApplyNonConsumable 0x2B311EC; doc 10 §2.2 |
| 5 bậc dia | DiaRewards = [240, 600, 1500, 3500, 8000] — blob được chứng minh bằng SHA-256 của chính tên field (preimage không thể trùng); cấp trong static ctor cùng Dia_FirstBuy_Used = new bool[5] |
metadata-proven (blob SHA MATCH) | doc 10 §2.2 (G350, G353) |
| Khuyến mãi mua-lần-đầu = dịch trái 1 bit (đúng gấp đôi, một lần mỗi bậc, có mùa) | amount = DiaRewards[i] << (Is_Dia_FirstBuy(i) ? 1 : 0); DiaFirstBuy_Season_Current = 1 |
disasm-proven + runtime-verified (live) | ApplyDiaReward 0x2B32D90; dump.cs:600315-600318 |
| Core (tiền tệ Adventure) | [100, 500, 1050, 3300, 750, 1750, 6000]; 3 gói có nhịp ngày/tuần (LastDailyCorePurchase_dateTime …) |
disasm-proven (.rodata) |
0x2B30018, 0x2B304BC |
| VIP / Luxury pass | vip_pass/vip_pass_2 → is_Vip = true + 500 dia; luxury_pass và luxury_upgrade → thân hàm giống hệt: is_Vip = is_Luxury = true + 1000 dia + Exp_Get(300) = 3 cấp pass (Exp_balance 100, Max_Lv 25) |
disasm-proven | 0x2B32C50, 0x2B32AA4 ≡ 0x2B328F8 |
catgunner_rebirth_single → UpgradeRebirth_Start_By_IAP; nếu manager còn null thì hoãn, thử lại lần chạy sau |
— | disasm-proven | ApplyRebirthSingle 0x2B3280C |
| StepUp 5 bậc | Gun_Rewards = [23, 28, 33, 39, 44] = 유령 저격총 (S, LASER) · 미사일 SS · 미사일 SSS · 폭발레이저 X · 폭발레이저 XX — thang S → XX; Dia_Rewards = [0, 200, 400, 600, 1000] |
metadata-proven (blob SHA MATCH) + derived (tên từ Gun_list scene) |
doc 10 §2.2, doc 11 §3.1 (G353) |
| DailyGun | MaxCounts = [2, 2, 1], PetCoin_Rewards = [300, 700, 1500], GunNum_Base = [20, 30, 35], WeekEpoch = 2024-01-01 |
metadata-proven | DailyGun.cctor 0x2B26894 |
| DiaPig là phần thưởng đi kèm gói xoá quảng cáo | is_DiaPig_Unlock = Is_Remove_AD_All \|\| Is_Remove_AD_forced; tick (forced?1:0)+(all?1:0) dia mỗi 60 s |
disasm-proven | 0x2B27764, 0x2B2823C |
Giá không hardcode — đọc từ Product.metadata của store, định dạng theo CurrencyFormat (23 tiền tệ → định dạng) / ZeroDecimalCurrency (KRW, JPY, VND, IDR, CLP, HUF — không thập phân) |
6 overload Localized_Price_return; 2 bảng trong static ctor |
disasm-proven | 0x2B367D4, 0x2B368B0; G399 |
| Giá quan sát sống (VND, tại thời điểm chụp — không phải hằng trong build) | Auto Upgrade 211.000đ · Buff 210.000đ · Bom Kim Cương 1.300.000đ · dia 52.000 / 131.000 / 263.000 / 526.000 / 1.300.000đ | runtime-verified (live) | doc 10 §2.4 |
| Số lần mua được gửi lên server phân khúc | InappCntForServer = max(PurchaseCount, PurchaseHistory.Count) → ConfigReq.inapp_cnt |
disasm-proven | 0x2B36528 |
Rebirth tăng cường ×3 (doc 09 §3.5; doc 10 §7.4)
| Điều game làm | Con số / hằng số | Mức bằng chứng | Nguồn |
|---|---|---|---|
Thưởng ×3 nếu is_UpgradeRebirth |
UpgradeRebirth_Multiplier = 3 |
disasm-proven + metadata-proven | RVA 0x2B22F18; dump.cs:598987 |
| Hai lối vào, mỗi lối 1 lượt/ngày, reset theo chuỗi ngày | UpgradeRebirth_DailyLimit = 1, _IAP_DailyLimit = 1; DayCheck 0x2B23260 |
metadata-proven + disasm-proven | doc 10 §7.5 |
Kênh IAP mở khi Stage_Value_Max > 988 ⇒ Stage 100-1 |
UpgradeRebirth_IAP_UnlockStageValue = 989 (HYPOTHESIS cũ đã được giải quyết) |
VERIFIED | 0x2B232D0 @0x2b23394; UNLOCK_GATES §3 |
Mã "kind" log: 1 = xem QC, 2 = đã mua Remove-AD-All, 3 = IAP; cờ chọn Shop_manager.Is_Remove_AD_All |
1/2/3 | VERIFIED (2 phân tích hội tụ) | CONVERGENCE U2 |
Interstitial sau Rebirth chỉ khi không boost và popup review không mở; Review_Panel_Open() trả true đúng một lần trong đời tài khoản |
— | disasm-proven | Rebirth() @0x2b22d9c; 0x2AC0DE8 |
Lịch offer (cố định trong binary — doc 10 §7.5)
| Hệ | Nhịp | Hằng | RVA |
|---|---|---|---|
| StepUp | 7 ngày kể từ lần đầu banner được nhìn thấy, 5 bậc mua tuần tự; banner cần súng bậc ≥ 2 | StepCount 5, Duration_Days 7, Banner_Unlock_Degree 2 |
0x2B3A328, 0x2B3A670, 0x2B3A4E0 |
| DailyGun | xoay theo tuần từ WeekEpoch, 3 món, giới hạn [2,2,1] |
ProductCount 3 |
0x2B25F34, 0x2B26080 |
| PlusPack | 7 ngày kể từ lúc mua, 3 loại độc lập; banner từ Banner_Stage_Min 190 (Stage 20-1) |
Duration_Days 7, PlusPack_Auto_Reduce_Hours 16 |
0x2B2B5EC, 0x2B2C778 |
| WeeklyShop / VIP | Stage_Value_Max > 189 ⇒ Stage 20-1 |
Unlock_Degree 3 |
0x2B3EA58 |
| Core daily/weekly | mốc LastDaily… / LastWeekly… |
— | 0x2B24F9C |
| DiaPig | tick 60 s, bù tối đa 3 tick | TICK_SECONDS 60, CatchUpMaxPending 3 |
LIVEOPS_SURFACE §6 |
| DailyCheck | chuỗi 7 ngày lặp | dia 70/200/400/600/800/1000/1500 — xem mục "7. Điểm danh 7 ngày" bên dưới (G378) |
doc 10 §7.6 |
Công thức / pseudocode
1. Rewarded — Admob.ShowRewardAd(AD_Type) — L1 ANALYSIS, RVA 0x2A59294 (doc 10 §4.1)
ShowRewardAd(type):
if (Shop_manager.Is_Remove_AD_All || FreeCash_manager.RewardAdRemoved):
ad_type = type; StartCoroutine(Reward_Get()); return // ĐƯỜNG TẮT: không có quảng cáo nào
if (is_AD_Disable_Version) -> Alert("Try Later"); return
if (_pendingShow || _isShowing) -> Alert("Try Later"); return
if (AD_Cool > 0) -> Alert("Try in {0} seconds"); return
ad_type = type
if (loaded && expired > 50 phút) -> Discard + reload
if (loaded) -> TryShowLoadedAd()
else if (!_sdkReady) -> Alert("Try Later")
else if (no network) -> Alert("Check Network Connection")
else -> _pendingShow = true; loading panel; LoadRewardedAd()
Reward_Get(): // RVA 0x2A5F0B0, sau khi SDK báo earned
yield 1 frame; AD_Cool = 20.0f; Sound_manager.PlaySfx(7)
if (!Is_Remove_AD_All) log analytics AF_CODE[ad_type] // [5,6,1,2,3,4,7,8,9,10]
switch (ad_type): 0,1 -> Buff_manager.AddBuff(ad_type); 2 -> SpeedRun.Bonus_Get(); ... // ĐIỂM TRAO DUY NHẤT
1b. Quota ngày: "cho không mấy lượt trước, gắn quảng cáo mấy lượt sau" (doc 10 §4.1b)
Muốn làm một bề mặt kiểu "rút thưởng hằng ngày" vừa giữ chân người không xem quảng cáo vừa bán được lượt cho người có xem, thì đây là hình dạng đã chạy thật (đo trên máy, placement rút cá):
Draw():
Quota_Reload() // §7b: nạp lại nếu sang ngày mới
if (kho đầy) -> Alert("Kho đã đầy"); return // TRẦN KHO chặn trước MỌI thứ
if (used >= MAX) -> Alert("Hết lượt"); return // MAX = 5
if (used >= FREE) -> ShowRewarded(place); return // FREE = 3 -> nhánh Granted gọi lại Grant()
Grant() // 3 lượt đầu: thưởng NGAY
Grant(): // Granted của quảng cáo cũng vào đúng đây
if (kho đầy) -> Alert; return // kiểm LẠI: giữa lúc xem quảng cáo kho có thể đầy
grade = Roll(baseGrade = 0); kho.Add(grade); vẽ lại chỉ số; popup "vật phẩm mới"; PlaySfx
used++; Btn_UI_Reload()
// Nhãn và biểu tượng
Btn_UI_Reload():
count_txt = "x" + (MAX - used)
btn_color = (used >= MAX) ? Color_Disable : Color.white
ad_icon.SetActive((uint)(used - FREE) < (MAX - FREE)) // DẢI {3,4}, KHÔNG phải used >= 3
Ba con số cần chốt trước khi viết dòng nào: MAX (5), FREE (3), giá nút trả tiền (200 Dia).
Đo được trên máy: x5 → x4 → x3 không quảng cáo; x2, x1 phải xem; x0 nút xám và biểu tượng
quảng cáo biến mất.
Vì sao cho không trước, không phải gắn quảng cáo từ lượt đầu. Ba lượt đầu là dạy giá: người chơi thấy phần thưởng rơi ra và thấy hai ô chỉ số nhích lên ngay trên cùng màn hình. Tới lượt thứ tư, "xem quảng cáo chứ?" mới là một câu hỏi có giá đã niêm yết — trước đó nó chỉ là một lời mời mù. Phần cho không còn giữ lý do quay lại hằng ngày cho nhóm không bao giờ xem quảng cáo, và biên trên (5) giữ cho bề mặt không thành máy cày.
Nút trả tiền mua SÀN, không mua tỉ lệ. Cùng một bảng tỉ lệ cộng dồn cho cả hai nút; nút miễn phí
trả base + i với base = 0, nút 200 Dia trả base = 1. Vì mỗi bậc mạnh gấp ~3, dịch một bậc đã
là gấp ba. Một bảng duy nhất phục vụ mọi nút ⇒ thêm hạng nút cao hơn chỉ là Roll(base + 1), không
có bảng thứ hai để lệch nhau.
Roll(base): # tỉ lệ CỘNG DỒN, tổng đúng 100
r = Random(0, 100); acc = 0
for i, p in enumerate([60, 30, 10, 0]): # bảng THẬT, đọc từ scene — xem cảnh báo dưới
acc += p # 60 -> 90 -> 100 -> 100
if r < acc: return base + i # base=0 -> hạng 0..2 ; base=1 -> hạng 1..3
return base # không bao giờ chạy khi tổng = 100
| Nút | base |
Phân phối |
|---|---|---|
| miễn phí | 0 | hạng 0 (60%) · 1 (30%) · 2 (10%) · ~~3 (0%)~~ |
| 200 Dia | 1 | hạng 1 (60%) · 2 (30%) · 3 (10%) · ~~4 (0%)~~ |
Hai lựa chọn cân bằng đáng đọc: lệch mạnh về bậc nền và đuôi bị cắt cụt (gacha là máy rót nguyên
liệu, không phải máy tạo bất ngờ — cảm giác tiến bộ được đẩy sang mạch hợp nhất); và giữ ô thứ tư
trong mảng nhưng để 0 — bậc thưởng cao nhất có sẵn chỗ, bật lại chỉ là đổi một số trong dữ liệu
chứ không phải sửa code.
⚠️ BẪY: đừng lấy tỉ lệ từ constructor. Mảng tỉ lệ ở đây là field public ⇒ được serialize, nên giá trị trong scene THẮNG giá trị ctor lúc chạy. Constructor của game gốc ghi
{40, 30, 20, 10}; bản trước của trang này chép đúng con số đó và sai — bảng thật là{60, 30, 10, 0}. Cái bắt được lỗi là một phép thử tần suất: năm lượt trả tiền liên tiếp đều ra bậc thấp nhất —0,6⁵ ≈ 7,8%(bình thường) so với0,4⁵ ≈ 1%(đáng nghi). Quy tắc chung:public/[SerializeField]có trong scene ⇒ scene thắng;privatekhông[SerializeField]⇒ ctor thắng;const⇒ ctor thắng. Kiểm loại field trước khi tin con số.
2. Interstitial — 7 cổng theo thứ tự — L1 ANALYSIS, RVA 0x2A5CD34 (doc 10 §4.2)
Inter_Placement_Able(place):
1. is_AD_Disable_Version -> false
2. Is_Interstitial_Blocked() (= disable || Remove_forced || Remove_All) -> false
3. _interShowing || _interPlacementRunning -> false
4. stageDisplay = Stage_Value_Max / 10 + 1; stageDisplay < inter_stage_min -> false
5. inter_cooltime >= 1 && (UtcNow - _lastInterShownUtc) < cooltime -> false
6. Inter_DayCheck(); PlayerPrefs["inter_place_cnt_"+place] >= Inter_DailyLimit(place) -> false
7. PlayerPrefs["inter_place_cnt_whole"] >= inter_daily_cnt_whole -> false
return true
ShowPlacementInterstitial(place, onClosed): !able ? onClosed() ngay : PlacementInterCo (1.2 s loading, 1 attempt, 3 s chờ, fail -> onClosed())
9 điểm gọi đều là "đóng màn kết quả" (Sweep_Close / Exit_Cor của Mine, Challenge, Boss, Hunt; đuôi Rebirth()), với điều kiện
!_bonusAdWatched && !Is_RewardBonus_Auto — không bao giờ giữa trận (doc 10 §4.2).
3. Buff ×3 — doc 10 §5, doc 01 §3.2
AddBuff(Money_Buff): moneyBuffTime = min(moneyBuffTime + 300, 900); Gamemanager.InvalidateMoneyCache() // 0x2AE4510
IsBuffActive(t): IsPermanentBuff || timer[t] > 0 // IsPermanentBuff = Shop_manager.Is_Buff_Pack (0x2AE4A08)
MoneyValue_return(v): r = v × Upgrade% × Rebirth% × Skin% × Pet% × Relic% (mỗi /100); if IsBuffActive(Money_Buff): r *= 3
4. Lộ diện theo tiến trình × segment (doc 10 §4.4–§4.5, §7.2)
(khối mã nội bộ đã lược bỏ)
5. Giao dịch IAP — một điểm trao duy nhất (doc 10 §6.2)
UI -> Shop_manager.Purchase*(type, place) -> BeginPurchase(productId) 0x2B2E7D8 -> [Unity IAP + store]
-> OnPurchasePending 0x2B31758 -> StartValidate 0x2B32144 -> ValidateAndComplete 0x2B33DAC (receipt -> server)
-> CompletePurchaseSuccess 0x2B34164 -> TryApplyPurchase(id, type) 0x2B32204 // <== ĐIỂM TRAO DUY NHẤT
-> MarkProcessed(txnId); SaveSystem.Save(); log
TryApplyPurchase: 6 non-consumable -> ApplyNonConsumable; rebirth_single; luxury_*; vip_pass*; core; rồi 4 nhánh theo PREFIX
(dia_, catgunner_stepup_, catgunner_dailygun_, catgunner_pluspack_); không khớp -> LogError, return false
manager đích còn null -> return false, KHÔNG finalize -> store phát lại lần chạy sau (không mất hàng)
6. Gợi ý framework (doc 10 §9 — không phải SOT)
interface IPlacement { string Id; bool CanShow(IGateContext ctx); void Show(Action onClosed); } // not-able -> onClosed() ngay
interface IGate { bool Pass(IGateContext ctx, out string reason); }
// thứ tự RẺ -> ĐẮT: BuildFlagGate, EntitlementGate, ReentrancyGate, ProgressGate(min & max), CooldownGate,
// PerPlaceCapGate, GlobalCapGate, CourtesyGate (!justRewarded && !justPaid)
interface IOffer { bool IsActive(DateTime now, IPlayerState s); IReadOnlyList<Reward> Preview { get; } }
interface IRemoteAdConfig { int Segment; int MinStage(id); int MaxStage(id); int CooldownSec; int DailyCap(id); int DailyCapGlobal; bool FeatureOn(id); }
// R1 — binding mặc định của bản restore:
public sealed class NullAdService : IAdService {
public bool IsRewardedReady => false;
public void ShowRewarded(string p, Action<AdResult> d) => d?.Invoke(AdResult.Failed); // KHÔNG BAO GIỜ Rewarded
public void ShowInterstitial(string p, Action d) => d?.Invoke(); // no-op, không chặn
}
// NullPurchaseService.IsOwned luôn false; NullRemoteConfig trả bản nhúng "tắt" (99999 / 0) — đúng như SOT cho segment ≠ 1.
7. Điểm danh 7 ngày — hệ retention (doc 10 §7.6, G378)
Vòng lặp quay lại mỗi ngày — thứ nuôi số ngày hoạt động mà mọi offer khác đứng trên. Đọc trọn từ binary (13 method).
| Điều game làm | Con số / hằng số | Mức bằng chứng | Nguồn |
|---|---|---|---|
| Bảng thưởng ngày 1–7 | 70 / 200 / 400 / 600 / 800 / 1000 / 1500 dia (tổng 4 570 / chu kỳ; nhảy vọt ở ngày 7) | scene-YAML | doc 10 §7.6 (G378) |
| Sang ngày mới = đồng hồ tin cậy TRƯỚC, rồi mới so ngày | CanRolloverDaily("daily", tối đa 1 đang chờ) và LastCheck_Date < Today |
disasm-proven | G378 |
| Chuỗi 7 ngày lặp vô hạn, không phạt khi bỏ lỡ | ngày 7 + đổi ngày ⇒ về ngày 1; bỏ lỡ chỉ chờ, không reset | disasm-proven | G378 |
| Người mới được lùi ngày một hôm ⇒ ngày 1 nhận ngay lần mở đầu | NewUser_Reload |
disasm-proven | G378 |
Nhận thưởng kiểm lại điều kiện; từ chối kèm sfx Dont; hợp lệ ⇒ ghi sổ cái (RecordGain("daily")) TRƯỚC khi cấp dia; rồi CheckNum++ + TotalCheckNum++ (cặp), LastCheck_Date = Today, dia bay về ví, sfx Purchase |
2 bộ đếm | disasm-proven | G378 |
| Mở panel ép kiểm lại giờ (chống chỉnh giờ) | Open_Btn → Time_Hacker_ForceCheck |
disasm-proven | G378 |
| Nút lùi ngày debug tắt trong bản phát hành | Debug.isDebugBuild |
disasm-proven | G378 |
| Panel chỉ hiện sau khi chờ đồng bộ giờ | Show_WhenReady: chờ 4 s kiểm giờ rồi Try_Show |
disasm-proven | G359, G378 |
# Điểm danh — L1 ANALYSIS (doc 10 §7.6)
is_DateChange = CanRolloverDaily("daily", maxPending 1) && LastCheck_Date.Date < Today # giờ tin cậy trước
if CheckNum == 7 and is_DateChange: CheckNum = 0 # chuỗi lặp
NewUser: LastCheck_Date = Today - 1 day # ngày 1 nhận ngay
Get_Reward:
if !is_GetAble(): PlaySfx(Dont); return
Time_manager.RecordGain("daily") # ghi sổ TRƯỚC
Get_Dia(DiaReward_list[CheckNum]); CheckNum++; TotalCheckNum++; LastCheck_Date = Today
Dia_Attraction.Dia_Spawn(); PlaySfx(Purchase)
Open_Btn: Time_Hacker_ForceCheck()
Bốn quyết định đáng chép: thưởng đầu ngay lập tức (lùi ngày người mới); chuỗi lặp, không phạt; đồng hồ tin cậy là điều kiện tiên quyết chứ không phải kiểm tra sau; ghi sổ trước khi cấp — mọi thưởng theo thời gian đi qua một sổ cái (xem 14).
7b. Reset quota theo ngày — khuôn dùng chung cho MỌI thứ "mỗi ngày N lần" (doc 07 §3.7)
Điểm danh ở §7 và quota rút thưởng ở §1b là cùng một khuôn. Viết một lần, dùng lại khắp nơi:
Quota_Reload(key, maxPending):
if savedDate.Date >= Today: return # chưa sang ngày mới (Today = giờ ĐÃ HIỆU CHỈNH)
if !CanRolloverDaily(key, maxPending): return # tầng trust chưa đồng ý -> KHÔNG ghi ngày mới
RecordGain(key) # ghi sổ TRƯỚC
used = 0 # rồi mới phát
savedDate = Today # và mới đánh dấu
OnClockSynced(): # khi đo được lệch giờ với máy chủ
savedDate = ClampFutureLocalDate(savedDate) # kéo ngày ở TƯƠNG LAI về, trần = Today + 1 ngày
Bốn điều dễ làm sai nhất, theo thứ tự:
- Đừng dùng
DateTime.Todaycủa hệ thống. Cả game phải hỏi một nguồn giờ đã hiệu chỉnh. Hai nguồn giờ = hai lịch, và chúng lệch nhau đúng lúc người chơi đổi múi giờ. - Cổng chống chỉnh giờ là một sổ nợ có TRẦN, không phải một cờ.
PendingCount(key) < 3: cho phép "ứng trước" tối đa 3 lần rollover chưa xác thực. Người chơi offline dài ngày vẫn nhận được; chỉ gian lận lặp lại mới bị chặn. Mỗi bề mặt một khoá riêng ("fish_free","daily"…) nên một hệ bị khoá không kéo theo hệ khác. - Bị chặn thì GIỮ NGUYÊN ngày cũ. Vì
savedDatechưa đổi, điều kiệnsavedDate < Todayvẫn đúng ở lần gọi sau ⇒ hệ thử lại. Ghi ngày trước khi qua cổng = người chơi mất trắng quota hôm đó, âm thầm. Quy tắc: đừng đánh dấu "đã phát" trước khi phát. - Phải kẹp ngày lưu nằm ở tương lai. Không có bước
ClampFutureLocalDate, người chơi đẩy giờ máy tới năm sau rồi trả về là quota đóng băng vĩnh viễn.
Gọi Quota_Reload ở cả hai đường: mỗi lần vẽ lại UI của bề mặt đó, và khi đồng hồ vừa đồng
bộ lúc UI đang đóng. Chỉ gọi một chỗ là có một cửa sổ dùng bộ đếm cũ để quyết định.
8. Segment — dựng tầng A/B test kiếm tiền (doc 10 §2.3, §7.1; bản dân dã: docs/functions/segment-lieu-ops.md)
Vì sao cần: không ai đoán đúng "thêm một quảng cáo ép thì lãi hay lỗ" — thêm quảng cáo tăng doanh thu hôm nay nhưng vay của ngày mai, và dấu của phép trừ đó khác nhau theo game, theo nước, theo nguồn cài. Cả thế giới tải một file APK, nên cách duy nhất để thử hai kiểu là dán nhãn lên người chơi rồi cho game tự đọc nhãn.
// L1 ANALYSIS — chuỗi thật (doc 10 §2.3):
Cheat_SetSegment(seg) 0x2A600CC // nút QA: ép nhãn
Segment = seg; SegmentCheatOverride = true // static +0x8, +0x21
PlayerPrefs "af_segment" = seg; "af_segment_cheat" = 1; PlayerPrefs.Save()
segmentText.text = seg.ToString()
ServerConfig_manager.Cheat_OnSegmentChanged() 0x2AC96D0
OnConfigApplied?.Invoke() // UI liveops vẽ lại NGAY, không cần restart
SaveSystem.Save(...) // nhãn không được mất vì app chết
ApplyServerSegment(seg) 0x2A6025C // đường server
if (SegmentCheatOverride) return; // 0x2a60320 — QA thắng server
if (Segment == seg) return; // 0x2a60374
// KHÔNG bật cờ QA, KHÔNG gọi Cheat_OnSegmentChanged
// Gợi ý framework (KHÔNG phải SOT) — hình dạng tối thiểu cho game của bạn:
AdConfig EmbeddedDefaults(int segment); // luôn có, không gọi mạng, không ném lỗi; nhánh mặc định = TẮT
AdConfig Current { get; } // MỘT cửa duy nhất; đến từ server hay bản nhúng đều qua đây
event Action OnConfigApplied; // phát mỗi khi Current đổi
string Current.reason; // "ok" | "client_embedded_default" | "default_rules_fallback"
Vòng đời một người chơi, đọc từ trên xuống — mỗi mũi tên là một chỗ có thể hỏng:
cài (từ chiến dịch X) -> SDK đo lường trả về SEGMENT -> ConfigReq(13 field) -> ConfigResp(15 field)
-> [mất mạng? => EmbeddedDefaults(segment)] -> chơi: mỗi PLACEMENT hỏi bộ số "được chiếu không?"
-> log mỗi lần chiếu/mua KÈM segment + rule_version -> sau 2 tuần so doanh thu/người & giữ chân D7
-> đổi quy tắc phân khúc TRÊN SERVER (đợt quy tắc mới = rule_version mới), không build lại, không bắt cập nhật
Vì sao segment 0 gần như không có quảng cáo ép là quyết định đáng chép, không phải sơ suất:
- hạ tầng ship đủ (6 loại placement, 28 điểm kích hoạt, OfferWall + FreeCash + súng thưởng) — không ai xây từng ấy rồi quên bật;
- khoá bằng dữ liệu (
99999) chứ không bằng cách gỡ code ⇒ mở lại từ xa trong một giây; - rewarded vẫn chạy, và không một field nào trong 12 số cấu hình khoá được rewarded (
derivedtừ bảng field doc 10 §2.3a) ⇒ đây là chọn loại quảng cáo (giữ loại tự bấm, bỏ loại ép xem), không phải tắt quảng cáo; - interstitial còn miễn 4 lần cho người vừa xem/vừa boost/vừa mua (doc 10 §9.2) — cùng một tinh thần.
Vì sao họ chọn segment 0 cho người dùng này thì UNKNOWN — bằng chứng chỉ nói họ đã chọn thế, không nói lý do.
Bẫy đã gặp
- Stub trả "thành công" = game tự trao thưởng cho chính nó. 10
AD_Type+ 34 product id đan khắp tiến trình; một stub sai làm mọi phép đo parity vô nghĩa một cách âm thầm. Fail-closed = sai theo hướng an toàn và quan sát được (doc 07 §3.6; doc 10 §9.3; R1). - Segment mặc định tắt ads.
EmbeddedDefaults(segment ≠ 1)=99999/0, và production trả segment 0 cho user thật ⇒ toàn bộ hạ tầng IAA ship nhưng chạy 0 quảng cáo. Một bản restore không có server sẽ không bao giờ hiện ads dù có SDK (doc 10 §7.1). - Quên bản nhúng ⇒ mất mạng là game hỏng. Không có
EmbeddedDefaults, server im lặng 5 phút là màn loading treo — hoặc tệ hơn: ngưỡng thiếu bị hiểu là0nên quảng cáo nổ khắp nơi từ stage 1. Quy tắc: thiếu ngưỡng =99999(khoá), thiếu trần ngày =0; nhánh mặc định của bản nhúng phải là nhánh tắt sạch (doc 10 §2.3). - Đổi segment giữa phiên mà không phát sự kiện ⇒ UI sai. Ép sang segment 1 xong nút OfferWall vẫn chưa hiện, phải tắt app mở lại mới
thấy — rồi đi sửa nhầm chỗ. SOT phát
OnConfigAppliedngay trongCheat_OnSegmentChanged(0x2AC96D0) đúng vì lý do này. - Cờ ép không thắng server ⇒ QA test một đằng ghi kết quả một nẻo.
ApplyServerSegmentreturn ở dòng đầu khi cờ bật (0x2a60320). Kèm theo: phải có đường về (xoáPlayerPrefs["af_segment_cheat"]), nếu không máy QA vĩnh viễn không nhận cấu hình thật nữa. - Nhãn segment nằm trong file save ⇒ nhảy nhóm. Reset tiến trình / khôi phục save cũ là đổi nhóm giữa chừng, phép đo A/B hỏng âm thầm.
SOT để nhãn ở
PlayerPrefs, tách khỏi save, đọc lại mỗiAwake(0x2A5FD78). - Không biết config đang đến từ đâu. Ba giá trị
reason(ok/client_embedded_default/default_rules_fallback) là thứ tiết kiệm hàng giờ debug — nhìn một chữ biết ngay server trả hay bản nhúng chạy (doc 10 §2.3). - Early-game oracle không thấy monetization. Mọi nút liveops (StepUp / WeeklyShop / DailyGun / OfferWall / FreeCash) là
active = 0trên save thật; chúng bật bởi tiến trình × segment × hành vi × lịch (doc 10 §2.4, §7). - Đếm nhầm product id từ string literal. LIVEOPS_SURFACE §6.1 liệt kê 24 id nhưng 8 chuỗi trong đó là
place/khoá analytics; danh sách thật là 34 id từItemIdxFromProductId(doc 10 §2.2). Xref literal, đừng đếm chuỗi. - Không suy ngưỡng từ tên key.
freecash_stage_min = 15là stage hiển thị (Stage_Value_Max/10 + 1), không phảiStage_Value(UNLOCK_GATES §1). - Giá không nằm trong build. Mọi con số đồng là giá store VN lúc chụp (
Localized_Price_return); đừng hardcode giá vào tài liệu như hằng. - Buff không phải danh sách instance. Cộng dồn có trần (300/900);
List<ActiveBuff>sẽ cho tích trữ vô hạn (doc 07 §3.4). - Interstitial ngay sau người vừa trả tiền. SOT miễn interstitial 4 lần:
!_bonusAdWatched,!Is_RewardBonus_Auto,!boosted,!reviewOpened— cùng một tinh thần "lịch sự với người vừa trả" (doc 10 §9.2). - Hai nhánh code không thể đạt tới (đặt cờ rồi mới kiểm
!cờ):ShowRewardAd(8)trongUpgradeRebirth_Start_By_IAP, interstitial trongMonster_manager.SweepBonus_Get— đọc disasm mà không thấy điều này sẽ đếm dư placement (doc 10 §10). - Biểu tượng quảng cáo hiện theo NGƯỠNG thay vì theo DẢI.
used >= FREEđể lại một biểu tượng chết sau khi hết quota: nút hứa quảng cáo nhưng chỉ trả về thông báo lỗi. Đúng là(uint)(used − FREE) < adSlots— một phép trừ + một so sánh unsigned bắt trọn cả hai biên (§1b). - Kiểm tiền trước khi kiểm kho đầy. Nhánh trả tiền phải kiểm kho đầy TRƯỚC, nếu không người chơi mất tiền mà không nhận được gì. Đây là thứ tự thật trong SOT, không phải tuỳ chọn.
- Không kiểm lại điều kiện trong nhánh
Granted. Giữa lúc xem quảng cáo 30 giây, kho có thể đã đầy vì một đường khác. Hàm cấp thưởng phải kiểm lại trần kho ở dòng đầu (§1b), dù người gọi đã kiểm rồi. - Đánh dấu "đã phát" trước khi phát. Ghi
savedDate = Todaytrước khi qua cổng chống chỉnh giờ ⇒ người chơi mất trắng quota hôm đó, không có lỗi nào hiện ra (§7b). - Lấy bảng tỉ lệ gacha từ constructor thay vì từ scene. Field serialize thì scene thắng;
trang này từng publish bảng
{40,30,20,10}của ctor trong khi bản ship dùng{60,30,10,0}(§1b). Kiểm loại field trước khi tin con số, và đặt cạnh mỗi bảng tỉ lệ một phép thử tần suất rẻ (N lượt liên tiếp ra cùng bậc: xác suất bao nhiêu với mỗi bảng?) — đó là thứ bắt được sai lệch khi không đọc được dữ liệu gốc.
Checklist khi tự làm
- [ ] Một enum loại phần thưởng quảng cáo (như
AD_Type); buff theo thời gian tách khỏi phần thưởng một lần. - [ ] Mỗi thứ IAA trao đều có bản IAP vĩnh viễn tương ứng — quảng cáo là bản dùng thử của cửa hàng.
- [ ] Buff = thời gian còn lại có trần (
+300 s, max900 s) +IsPermanentcho IAP; buff nằm trong chuỗi nhân có cache ⇒ invalidate khi đổi. - [ ] Rewarded: một điểm trao thưởng duy nhất trong coroutine của game; đường "đã mua miễn QC" tái dùng đúng điểm đó.
- [ ] Interstitial: chuỗi gate rẻ → đắt (build flag, entitlement, reentrancy, stage min, cooldown, cap theo place, cap toàn cục, lịch sự); gate fail ⇒
onClosed()ngay, không bao giờ chặn luồng. - [ ] Chỉ đặt interstitial ở "đóng màn kết quả", không giữa trận; miễn cho người vừa xem rewarded / vừa boost / vừa thấy popup review.
- [ ] Bộ đếm quảng cáo ở PlayerPrefs, tách khỏi save; reset theo chuỗi ngày.
- [ ] Quota ngày kiểu "rút thưởng": chốt 3 con số (MAX lượt, FREE lượt không quảng cáo, giá nút trả tiền) trước khi viết; một bộ đếm cho cả hai đoạn; biểu tượng quảng cáo hiện theo dải
(uint)(used − FREE) < MAX − FREE(§1b). - [ ] Nút trả tiền của cùng bề mặt mua sàn cao hơn trên cùng một bảng tỉ lệ, không phải một bảng tỉ lệ riêng — thêm hạng nút =
Roll(base + 1). - [ ] Bảng tỉ lệ sống ở dữ liệu (scene/asset/remote), không ở constructor; ghi rõ nguồn ngay cạnh bảng, và có một phép thử tần suất để phát hiện bảng tài liệu trôi khỏi bảng chạy thật (§1b).
- [ ] Mọi reset theo ngày đi qua một khuôn: giờ đã hiệu chỉnh + sổ nợ có trần + giữ nguyên ngày cũ khi bị chặn + kẹp ngày ở tương lai (§7b).
- [ ] Remote config chỉ chứa số placement (15 số kiểu
ConfigResp), có bản nhúng fail-safe mặc định tắt; không bao giờ chứa số kinh tế. - [ ] Lộ diện = f(tiến trình) × f(segment), có cả trần trên cho offer chuyển đổi kém ở late-game.
- [ ] IAP: một điểm trao idempotent, hoãn được khi hệ đích chưa sẵn sàng; prefix hoá sản phẩm có chỉ số; giá đọc từ store.
- [ ] Non-consumable là cờ; entitlement chảy ngược vào tầng quảng cáo qua một vị ngữ (
Is_RewardBonus_Auto). - [ ] Offer theo hành vi (ghép súng lần đầu ⇒ súng thử 30 phút) bên cạnh offer theo stage và theo lịch.
- [ ]
IAdService/IPurchaseService/IRemoteConfigServicevớiNullServicemặc định: không bao giờ trả thành công / owned / bật ads. - [ ] Liệt kê hết placement trước khi viết code; mỗi placement cần đúng 3 số (ngưỡng tiến trình, cooldown, trần ngày) + ngưỡng tối đa nếu có cửa sổ.
- [ ] Viết
EmbeddedDefaults(segment)trước code gọi mạng; kiểm bằng chế độ máy bay: cài mới, chơi 10 phút, không quảng cáo ép nào, không treo. - [ ] Một cửa duy nhất đọc cấu hình (
Current) + một sự kiệnOnConfigApplied; đổi segment lúc đang chơi ⇒ UI liveops vẽ lại trong cùng khung hình. - [ ] Nhãn segment ở kho riêng (không trong save), đọc lại lúc khởi động, mặc định
0; ghi xuống đĩa ngay trong hàm đổi. - [ ] Nút ép segment cho QA + cờ ưu tiên thắng server, và một đường về xoá cờ.
- [ ] Mọi payload mang
reason+rule_version; mọi log chiếu/mua kèm segment — không có hai trường này thì dữ liệu không chia nhóm được. - [ ] Mỗi lần thử chỉ đổi một con số giữa hai nhóm; chốt thời gian chạy và ngưỡng quyết định (doanh thu/người và giữ chân D7) trước khi nhìn số.
- [ ] Không A/B test giá — chỉ test tần suất mời. Cat Gunner không có field giá trong 15 key config.
Điều chưa biết (UNKNOWN)
Chép từ doc 10 §10 (kèm cách đóng):
- Toàn bộ hành vi IAA lúc chạy (cooldown thật, cap ngày thật, thứ tự popup): UNKNOWN-pending-live — user thật ở segment 0 nên chưa từng thấy
một quảng cáo nào. Đóng bằng phiên probe ở segment 1, hook
Inter_Placement_Able0x2A5CD34+Inter_CountUp0x2A5D644+ShowRewardAd0x2A59294(G178). - Giá VND của 25/34 product id: UNKNOWN (chỉ 9 giá quan sát được). Đóng: mở đủ panel shop trên bản store hoặc hook
Localized_Price_return0x2B367D4. - Cửa sổ hiện thật của StepUp / WeeklyShop / DailyGun / PlusPack: UNKNOWN — 5 nút HUD đều
active = 0trên save user. Đóng: segment 1 + đẩyStage_Value_Maxqua 190 và 988. PlusPack.Is_Banner_Stage_OK(0x2B2C6AC) so sánh chính xác cái gì: UNKNOWN (IDA gộp biên hàm; chỉ hằngBanner_Stage_Min = 190là metadata-proven).- "Core" (
Adventure_manager.Core_Get): ĐÃ ĐÓNG phần lớn (doc 10 §6.1a) — tiền tệ Trip, 4 ôForest/Desert/Tundra/ALL, kiếm 20/giờ đi Trip, tiêu vào cây Ability; IAPcore_*nạp ôALL. - ~~
StepUp.Gun_Rewardslà khẩu nào~~: ĐÃ ĐÓNG (derived từGun_listscene — doc 11 §3.1): 5 bậc StepUp = 5 khẩu S → XX. catgunner_luxury_passvscatgunner_luxury_upgrade: bằng chứng phủ định — thân hàm giống hệt; khác biệt (nếu có) nằm ở giá / điều kiện hiển thị.vip_passvsvip_pass_2: cùng chỉ số 12, cùngApplyVipPass— nhiều khả năng đổi giá/SKU, chưa kiểm store listing.- Chỉ số offer 31–35: bằng chứng phủ định — không product id nào trả về.
Admob.ShowRewardAd(int)(0x2A59C04) nút nào gọi: UNKNOWN — 0 code-xref ⇒ nối từ scene bằng UnityEvent.- Cơ chế kích hoạt Season reset và backend Redeem code: UNKNOWN (kế thừa LIVEOPS_SURFACE §7).
- Nội dung config cho segment 1 và 2 thật trên production: UNKNOWN — chỉ mới thấy segment 0. Cái đã biết là bản nhúng:
EmbeddedDefaultschỉ có hai nhánh (== 1và≠ 1) nên bản nhúng của segment 2 trùng segment 0 (disasm, doc 10 §2.3a); cái server trả cho segment 2 thì chưa ai thấy. Đóng: ép segment 2 rồi đọc lạiConfigResp. - Quy tắc phân khúc phía server (ai được gán 0, ai được gán 1): UNKNOWN và nhiều khả năng không đo được từ phía client — nó sống trên
máy chủ nhà phát hành. Cái đo được là 13 field
ConfigReqclient gửi lên, tức danh sách thứ có thể đã dùng để chia nhóm, không phải công thức. - Tỉ lệ rơi gacha (
Gun_manager.DrawRandomGun0x2B0A278): UNKNOWN, chưa hề được đọc (doc 07 §5). - Đã đóng (ghi để khỏi tìm lại): G144 —
Gun_manager.Balance_Reload/Request_Sheet0 lần fire qua 3 phiên ⇒ bảng súng không remote-driven (doc 10 §10); PlayFabGetTitleData0 caller; không field kinh tế nào trong 15 keyConfigResp(tĩnh và payload production).
Đọc thêm
docs/framework/10-monetization-iaa-iap.md— toàn bộ, đặc biệt §4 (28 điểm kích hoạt), §6.1 (34 product id), §8 (phễu), §9 (framework)docs/functions/segment-lieu-ops.md— segment giải thích dân dã: ví dụ đời thường, vòng đời một người chơi (cài → attribution → cấu hình → placement → đo → chọn), ba tầng segment/ServerConfig/placement, 12 cạm bẫy, 13 bước "nếu bạn muốn tự làm"docs/framework/07-progression-managers.md§2.4, §3.4, §3.6 —AD_Type,Buff_manager, ranh giới R1docs/framework/09-prestige-rebirth.md§3.5 — rebirth tăng cường và call-site R1docs/framework/01-economy-money.md§3.2, §3.6 — nhánh×3và quy tắc fail-closed[liveops catalog: LIVEOPS_SURFACE.md]§2–§6 — 15 keyConfigResp,EmbeddedDefaults, Firebase RC, lịch offer[liveops catalog: UNLOCK_GATES.md]§1, §3 —stageDisplay, cổng Rebirth-IAP, PlusPack, StepUp, WeeklyShop[phân tích nội bộ]— U2 (boost kind), U4 (liveops không ghi đè kinh tế)- 03 Prestige / Rebirth · 10 Tiến trình & mở khoá · 02 Kinh tế & số lớn
gap list (nội bộ)— G008, G141, G143, G144, G145, G149, G178