CatGunner CookbookBardCat 1.1.59 · L1 ANALYSIS · tài liệu học
Trang chủCookbook › 11 — Monetization (IAA + IAP): quảng cáo là bản dùng thử của cửa hàng
Nguồn: 11-monetization.md · Cập nhật lần cuối: 2026-09-06 23:05 +07 · sha256 nguồn c24a2735f3a6

11 — 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đú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_passvip_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_2is_Vip = true + 500 dia; luxury_pass 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, 0x2B32AA40x2B328F8
catgunner_rebirth_singleUpgradeRebirth_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 > 988Stage 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 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ất0,6⁵ ≈ 7,8% (bình thường) so với 0,4⁵ ≈ 1% (đáng nghi). Quy tắc chung: public/[SerializeField] có trong scene ⇒ scene thắng; private khô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ờ) 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ự:

  1. Đừng dùng DateTime.Today củ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ờ.
  2. 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.
  3. Bị chặn thì GIỮ NGUYÊN ngày cũ.savedDate chưa đổi, điều kiện savedDate < Today vẫ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.
  4. 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_Reloadcả hai đường: mỗi lần vẽ lại UI của bề mặt đó, 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 (derived từ 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à 0 nê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 OnConfigApplied ngay trong Cheat_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. ApplyServerSegment return ở 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ỗi Awake (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 = 0 trê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 = 15stage hiển thị (Stage_Value_Max/10 + 1), không phải Stage_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) trong UpgradeRebirth_Start_By_IAP, interstitial trong Monster_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 = Today trướ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, max 900 s) + IsPermanent cho 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 / IRemoteConfigService với NullService mặ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ện OnConfigApplied; đổ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, 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 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_Able 0x2A5CD34 + Inter_CountUp 0x2A5D644 + ShowRewardAd 0x2A59294 (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_return 0x2B367D4.
  • Cửa sổ hiện thật của StepUp / WeeklyShop / DailyGun / PlusPack: UNKNOWN — 5 nút HUD đều active = 0 trên save user. Đóng: segment 1 + đẩy Stage_Value_Max qua 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ằng Banner_Stage_Min = 190 là 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; IAP core_* nạp ô ALL.
  • ~~StepUp.Gun_Rewards là khẩu nào~~: ĐÃ ĐÓNG (derived từ Gun_list scene — doc 11 §3.1): 5 bậc StepUp = 5 khẩu S → XX.
  • catgunner_luxury_pass vs catgunner_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_pass vs vip_pass_2: cùng chỉ số 12, cùng ApplyVipPass — 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 12 thật trên production: UNKNOWN — chỉ mới thấy segment 0. Cái đã biết là bản nhúng: EmbeddedDefaults chỉ có hai nhánh (== 1≠ 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ại ConfigResp.
  • 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 ConfigReq client 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.DrawRandomGun 0x2B0A278): UNKNOWN, chưa hề được đọc (doc 07 §5).
  • Đã đóng (ghi để khỏi tìm lại): G144 — Gun_manager.Balance_Reload / Request_Sheet 0 lần fire qua 3 phiên ⇒ bảng súng không remote-driven (doc 10 §10); PlayFab GetTitleData 0 caller; không field kinh tế nào trong 15 key ConfigResp (tĩnh 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.mdsegment 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 R1
  • docs/framework/09-prestige-rebirth.md §3.5 — rebirth tăng cường và call-site R1
  • docs/framework/01-economy-money.md §3.2, §3.6 — nhánh ×3 và quy tắc fail-closed
  • [liveops catalog: LIVEOPS_SURFACE.md] §2–§6 — 15 key ConfigResp, 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