10-monetization-iaa-iap.md · Cập nhật lần cuối: 2026-09-13 21:18 +07 · sha256 nguồn 70575d11449010 — Kiếm tiền: Quảng cáo (IAA) + Mua trong ứng dụng (IAP)
Nguồn: chưng cất từ restore SOT
com.Chodun.CatGunner1.1.59 (BardCat). Mọi C# / công thức ở đây là L1 ANALYSIS — đọc từ ARM64 / Hex-Rays củalibil2cpp.sotại RVA ghi kèm, từ IL2CPP metadata đã giải mã, từMain.unity, và từ hai lượt probe trên thiết bị: (quan sát IAA dưới segment 1 trên bản.sot— save riêng, đãpm clearsau khi xong). Không phải source gốc.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 (
IAdService,IPurchaseService, …) và không bao giờ được cấp thưởng, entitlement hay purchase. Tài liệu này chưng cất THIẾT KẾ kiếm tiền (placement, trigger, gate, offer, timing), không chưng cất nội tại SDK. Không có đoạn nào ở đây mô tả cách giả mạo mua hàng hay bỏ qua quảng cáo.Liên quan: 01 Kinh tế · 09 Prestige · 07 Tiến trình ·
[liveops catalog: LIVEOPS_SURFACE.md]·[liveops catalog: UNLOCK_GATES.md]·[liveops catalog: PROBE_RESULT.md]·[liveops catalog: PROBE_RESULT.md]
1. Vì sao monetization là "song sinh" của core loop
Trong game idle, người chơi chờ. Mọi thứ rút ngắn cái chờ đó là hàng hoá. CatGunner bán đúng 4 thứ:
| Hàng hoá | Bán bằng quảng cáo (IAA) | Bán bằng tiền (IAP) |
|---|---|---|
| Nhân thu nhập / sát thương tạm thời | Buff ×3, 300 s/lần | buff_pack → ×3 VĨNH VIỄN |
| Nhân phần thưởng một lần | Bonus sau Mine/Boss/Hunt/Challenge, Mission, Fish gacha | PlusPack (×2 Monster/Boss, ×1.5 Mine) |
| Nhảy cóc tiến trình | Rebirth tăng cường ×3 (1 lần/ngày) | catgunner_rebirth_single (thêm 1 lần/ngày, mở ở stage ~100) |
| Bỏ ma sát | FreeCash link → hết phải xem reward-ad | remove_forced_ads / remove_all_ads |
Hai cột không cạnh tranh nhau — chúng xếp tầng: cột IAA là cách người chơi F2P nếm trải giá trị
của multiplier; cột IAP bán chính cái multiplier đó ở dạng vĩnh viễn. Đó là toàn bộ ý đồ thiết kế.
Điểm nối kỹ thuật của hai cột là Shop_manager.Is_Remove_AD_All / Is_Buff_Pack — hai cờ
non-consumable đọc trực tiếp bởi tầng quảng cáo (§4.1, §5).
2. Stable truths
Mức bằng chứng:
disasm-proven= ARM64/Hex-Rays tại RVA ghi kèm ·metadata-proven= IL2CPP metadata đã giải mã (dump.cs/ field-default-value blob) ·scene-YAML= đọc từMain.unity·runtime-verified (live)= đo trên thiết bị bằng probe đã disasm ·UNKNOWN= chưa biết, không được thay bằng số hợp lý.
2.1 Tầng quảng cáo (IAA)
| Claim | Mức | Nguồn |
|---|---|---|
Lớp Admob KHÔNG dùng Google AdMob — nó bọc SDK ADX (adop): AdxRewardedAd, AdxInterstitialAd, AdxBannerAd, ADXConfiguration. Tên lớp là di sản |
metadata-proven | dump.cs:585348-585700 |
| 3 định dạng quảng cáo: rewarded · interstitial · banner. Mỗi định dạng có 1 ad-unit id cho Android + 1 cho iOS (6 hằng) + 2 App ID | metadata-proven | dump.cs:585362-585379, :585392, :585421-585422 |
10 AD_Type (0..9) = 10 loại phần thưởng của rewarded ad |
metadata-proven | dump.cs:584959; §3 |
Dispatch phần thưởng nằm trong một coroutine: Admob.Reward_Get() → switch(ad_type) → gọi đúng 1 manager |
disasm-proven | <Reward_Get>d__63.MoveNext RVA 0x2A5F0B0 |
Sau mỗi rewarded ad: AD_Cool = 20.0f (hằng ghi thẳng 0x41A00000), Sound_manager.PlaySfx(7), và chỉ log AppsFlyer khi !Is_Remove_AD_All |
disasm-proven | 0x2A5F0B0 @0x2a5f2d8/0x2a5f30c |
AD_Cool đếm ngược bằng Time.deltaTime trong LateUpdate, kẹp ≥ 0 ⇒ 20 s giãn cách giữa 2 rewarded ad |
disasm-proven | Admob.LateUpdate RVA 0x2A58CC8 |
Bấm nút reward khi AD_Cool > 0 ⇒ UI_manager.Alert("Try in {0} seconds") |
disasm-proven | ShowRewardAd(AD_Type) 0x2A59294; literal 0x5E97D28 |
Đường tắt entitlement: nếu Shop_manager.Is_Remove_AD_All \|\| FreeCash_manager.RewardAdRemoved thì ShowRewardAd trao thưởng ngay, không hiện quảng cáo |
disasm-proven | 0x2A59294 @0x2a59310-0x2a59334 |
Shop_manager.Is_RewardBonus_Auto = Is_Remove_AD_All \|\| FreeCash_manager.RewardAdRemoved — cùng vị ngữ, dùng để tắt interstitial và đổi nút "xem QC" thành nút "nhận luôn" |
disasm-proven | 0x2B2D510; Mine_manager.SweepNoAds_Btn_obj dump.cs |
5 Admob.Inter_Place: Rebirth=0, Mine=1, Challenge=2, Boss=3, Hunt=4 — khớp 1-1 với 5 field inter_daily_cnt_* của ServerConfig |
metadata-proven + disasm-proven | dump.cs:584976; Inter_DailyLimit 0x2A5CC04 |
Inter_DailyLimit(place) chuyển thẳng sang getter ServerConfig tương ứng (tail-jump 0x2AC7E64/EC0/F1C/F78/FD4), default 0 |
disasm-proven | 0x2A5CC04 |
Bộ đếm interstitial nằm ở PlayerPrefs, không phải SaveData: khoá "inter_place_cnt_" + (int)place, tổng "inter_place_cnt_whole", ngày "inter_place_date" (yyyyMMdd) |
disasm-proven | Inter_CountKey 0x2A5C784, Inter_DayCheck 0x2A5C7F0, Inter_CountUp 0x2A5D644; literal 0x5EA3168/70/78 |
Inter_DayCheck() reset mọi bộ đếm khi PlayerPrefs["inter_place_date"] != today (duyệt Enum.GetValues(Inter_Place)) |
disasm-proven | 0x2A5C7F0 |
| Cổng interstitial (7 điều kiện, theo đúng thứ tự) — xem §4.2 để có pseudocode | disasm-proven | Inter_Placement_Able 0x2A5CD34 |
Is_Interstitial_Blocked() = is_Interstitial_Disable \|\| Shop_manager.Is_Remove_AD_forced \|\| Is_Remove_AD_All |
disasm-proven | 0x2A5B4D4 |
Is_Banner_Blocked() = Is_Remove_AD_forced \|\| Is_Remove_AD_All (banner không có cờ tắt riêng trong Inspector) |
disasm-proven | 0x2A5BC70 |
Banner chỉ chạy trong trận Boss / Hunt / Raid: ShowBannerAd gọi từ Boss_manager.Enter_Cor, Monster_manager.Enter_Cor, Raid_manager.Enter_Cor; HideBannerAd từ 3 Game_Exit/StopAllGameCoroutines tương ứng |
disasm-proven (xrefs) | xrefs 0x2A5BCF4 / 0x2A5BD58 |
Banner: BANNER_HEIGHT_DP = 50, BANNER_RETRY_DELAY = 30 s, có bannerPlaceholder RectTransform + Refresh_SafeArea |
metadata-proven | dump.cs:585392-585404 |
Khi trạng thái IAP đổi, Admob.Handle_InAppPurchaseChanged() huỷ ngay banner / reward-ad đã nạp / interstitial đã nạp nếu bị chặn |
disasm-proven | 0x2A5C2CC |
Hằng độ bền quảng cáo: 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 trong scene: is_AD_Disable_Version 0, is_Interstitial_Disable 0, isTestMode 0, AD_Cool 0, adLoadTimeout 12, interLoadingSeconds 1.2, interLoadMaxAttempts 1, interLoadWaitPerAttempt 3 |
scene-YAML | Main.unity:20647245-20647268 (MonoBehaviour &120003, GO 1737) |
Bảng ánh xạ AD_Type → mã log AppsFlyer (m_ads_reward) = [5,6,1,2,3,4,7,8,9,10] |
disasm-proven (đọc .rodata) |
mảng 0x109EAE4, dùng tại 0x2A5F0B0 |
2.2 Tầng IAP
| Claim | Mức | 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 dùng chung chỉ số 12; 31..35 bỏ trống) |
disasm-proven | ItemIdxFromProductId RVA 0x2B2DE44 — bảng đầy đủ ở §6.1 |
Backend IAP = Unity StoreController (Unity IAP mới), có xác thực receipt phía server qua 2 Cloud Function (verifyInAppPurchaseApplePBV5 / verifyInAppPurchaseGoogleYooSu) |
metadata-proven | dump.cs:600320-600321; ValidateAndComplete 0x2B33DAC |
Chống trùng giao dịch: _inflightTxns (HashSet), ProcessedTxnIds (List, ProcessedTxnIdsMax = 50), MarkProcessed/ReleaseInflight |
metadata-proven + disasm-proven | dump.cs:600334-600337; 0x2B33B94 / 0x2B33D38 |
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 |
ApplyNonConsumable trao: autoupgrade_pack → +500 dia; buff_pack → +1000 dia; dia_pack → +16000 dia; mine_pack → +500 dia + reload Mine_Start_UI; remove_forced_ads/remove_all_ads → không dia, nhưng DiaPig.Init() nếu đây là cờ đầu tiên |
disasm-proven | 0x2B311EC |
Shop_manager.DiaRewards = [240, 600, 1500, 3500, 8000] cho dia_0..dia_4 |
metadata-proven (field-default-value blob) | <PrivateImplementationDetails>.F68B1971…AD97D2 trong global-metadata.decrypted.dat |
Khuyến mãi mua-lần-đầu = DỊCH TRÁI 1 BIT: amount = DiaRewards[i] << (Is_Dia_FirstBuy(i) ? 1 : 0) ⇒ đúng gấp đôi, một lần cho mỗi bậc, cờ lưu trong Dia_FirstBuy_Used[], có DiaFirstBuy_Season_Current = 1 (mùa) |
disasm-proven | ApplyDiaReward 0x2B32D90 @0x2b32e94; dump.cs:600315-600318 |
Shop_manager.Core_Amount_return = [100, 500, 1050, 3300, 750, 1750, 6000] cho 7 InApp_Core_Type; "Core" được trao qua Adventure_manager.Core_Get(Core_Type.<3>, amount) |
disasm-proven (đọc .rodata 0x109EB1C) |
0x2B30018, ApplyCoreReward 0x2B304BC |
3 gói core có nhịp thời gian: _daily_750/_weekly_1750/_weekly_6000 ghi LastDailyCorePurchase_dateTime / LastWeekly… (static 0x10/0x18/0x20) ⇒ mua lại theo chu kỳ (Is_Core_Special_Available, Core_Special_Remain_Duration) |
disasm-proven + metadata-proven | 0x2B304BC; dump.cs:600312-600314; 0x2B24F9C / 0x2B25348 |
vip_pass + vip_pass_2 → ApplyVipPass(): Mission_manager.is_Vip = true, +500 dia |
disasm-proven | 0x2B32C50 |
catgunner_luxury_pass và catgunner_luxury_upgrade → thân hàm giống hệt nhau: is_Vip = is_Luxury = true (ghi word 257), +1000 dia, Mission_manager.Exp_Get(300) = 3 cấp battle-pass (Exp_balance = 100, Max_Lv = 25) |
disasm-proven | 0x2B32AA4 / 0x2B328F8; dump.cs:587346-587350 |
catgunner_rebirth_single → ApplyRebirthSingle() → Rebirth_manager.UpgradeRebirth_Start_By_IAP(); nếu Rebirth_manager null thì hoãn, thử lại lần chạy sau (không mất hàng) |
disasm-proven | 0x2B3280C; literal 0x5E9AAD8 |
StepUp.Gun_Rewards = [23, 28, 33, 39, 44], StepUp.Dia_Rewards = [0, 200, 400, 600, 1000] (5 bậc) |
metadata-proven (blob) | <PrivateImplementationDetails>.EA64A4C1… / .AA838700… |
DailyGun.MaxCounts = [2, 2, 1], PetCoin_Rewards = [300, 700, 1500], GunNum_Base = [20, 30, 35], WeekEpoch = 2024-01-01 |
metadata-proven (blob + cctor) | DailyGun.cctor 0x2B26894 |
is_DiaPig_Unlock() = Is_Remove_AD_All \|\| Is_Remove_AD_forced; DiaPig_TickAmount() = (Is_Remove_AD_forced?1:0) + (Is_Remove_AD_All?1:0) ⇒ heo kim cương là phần thưởng đi kèm của gói xoá quảng cáo, tick 1 hoặc 2 dia mỗi 60 s |
disasm-proven | 0x2B27764 / 0x2B2823C; TICK_SECONDS = 60 |
InappCntForServer() = max(PurchaseCount, PurchaseHistory.Count), nếu 0 thì HasEverPurchased() ⇒ số lần mua được gửi lên server phân khúc quảng cáo |
disasm-proven | 0x2B36528; ConfigReq.inapp_cnt |
place (chuỗi) đi kèm mọi lệnh mua để log AppsFlyer m_offer_page: store, popup, hud, banner, banner_reward, banner_enter, banner_weapon, auto_weapon/auto_pet/auto_fish |
disasm-proven | Purchase* 0x2B2F618/0x2B2FF0C/0x2B2FF68/0x2B2FFC0; literal 0x5E9E7E8/F0/F8/800 |
vip_pass_2 không phải sản phẩm Play — được TryApplyPurchase/ItemIdxFromProductId xử lý nhưng không có trong 33 id store trả giá ⇒ 34 id trong code, 33 id đang bán |
negative evidence (live) | iap_prices_store.json |
remove_forced_ads 79.000đ là SKU rẻ nhất; dia_4 và dia_pack cùng 1.300.000đ là đắt nhất; các bậc Core (26k/105k/210k/631k) và StepUp (26k/130k/261k/523k/1.05M) là thang hình học ≈ ×2 |
derived (từ 33 giá đo) | iap_prices_store.json |
ĐÍNH CHÍNH LIVEOPS_SURFACE.md §6.1: mine_ticket, fish_free, banner_enter, banner_reward, banner_weapon, sp_iap, reward_hub, f_cash KHÔNG phải product id — chúng là chuỗi place/khoá analytics/tên màn Almedia |
disasm-proven (xrefs literal) | Mine_manager.Ticket_Reload, Fish_manager.FreeGacha_Reload, PlusPack_Inapp_item.OnClick, AppsFlyerInit.AddSpecValues, AlmediaScreenExtensions, AppsFlyerInit.SendLog_Launch |
2.3 Phân khúc (segment) — công tắc tổng của IAA
Bản giải thích dân dã đầy đủ:
docs/functions/segment-lieu-ops.md— segment là gì bằng 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 cấu hình thắng), phân biệt ba tầng segment / ServerConfig / placement, danh sách cạm bẫy khi tự dựng, và mục "Nếu bạn muốn tự làm" 13 bước. Viết cho người không đọc disasm; mọi số và RVA ở đó lấy từ mục này.ĐÍNH CHÍNH (QD-28, 2026-09-06): ba nút
Seg 0/1/2của panel debug gốc KHÔNG phải R1-out —docs/functions/cheat-panel.mdtừng xếp nhầm chúng vào cột "sẽ không dựng".AppsFlyerInit.Cheat_SetSegment(0x2A600CC) không gọi hàm SDK AppsFlyer nào: nó đặtSegment(static0x8) +SegmentCheatOverride(0x21), ghiPlayerPrefs["af_segment"]/["af_segment_cheat"], đổi nhãnsegmentText, rồi gọiServerConfig_manager.Cheat_OnSegmentChanged()(0x2AC96D0— phátOnConfigApplied+SaveSystem.Save). Đã dựng 1:1, không lấn R1. Cái thật sự R1-out là đường AppsFlyer gán segment từ mạng (OnUserSegmentReceived→ApplyServerSegment), xem §9.3.
| Claim | Mức | Nguồn |
|---|---|---|
ServerConfig_manager.ConfigResp có đúng 15 field, 12 trong đó là số điều khiển quảng cáo. Không có field kinh tế nào |
metadata-proven + runtime-verified (live) | dump.cs:589438; LIVEOPS_SURFACE.md §2.2; payload production ở PROBE_RESULT.md §1.1 |
EmbeddedDefaults(segment) (0x2AC7B20): segment 1 = [freecash_stage_min 15, freecash_stage_max_hud 35, offerwall_stage_min 10, inter_stage_min 12, inter_cooltime 0, rebirth 6, mine 2, challenge 0, boss 0, hunt 0, whole 99999, weapon_02 1]; segment ≠ 1 = 4 ngưỡng stage 99999, còn lại 0 ⇒ tắt sạch |
disasm-proven (.rodata) |
LIVEOPS_SURFACE.md §2.3; chứng minh offset ở §2.3a |
Cả hai nhánh của EmbeddedDefaults đều đặt rule_version = "0" (literal 0x5E82F08) và reason = "client_embedded_default" (literal 0x5E9F3E0) ⇒ nhìn reason là biết config đến từ client hay server |
disasm-proven | 0x2AC7B20 |
Server phân khúc theo: bundle_id, segment, uuid, country_code, platform, version, campaign, media_source, install_date, inapp_cnt, reward_ad_cnt, test, app_version (13 field ConfigReq) ⇒ nguồn cài đặt (AppsFlyer) + quốc gia + lịch sử chi tiêu quyết định bạn thấy bao nhiêu quảng cáo |
metadata-proven | dump.cs:589413; LIVEOPS_SURFACE.md §2.1 |
AppsFlyerInit.Cheat_SetSegment(seg) ghi AppsFlyerInit.Segment (static 0x8), bật SegmentCheatOverride (0x21), lưu PlayerPrefs["af_segment"]/["af_segment_cheat"], rồi gọi ServerConfig_manager.Cheat_OnSegmentChanged() |
disasm-proven | 0x2A600CC; dump.cs:585772-585783 |
Segment đổi HÀNH VI, không chỉ đổi số: cặp đo có đối chứng trên Pixel (cùng save, cùng thao tác hợp nhất, chỉ khác segment) — segment 1 cho mượn súng hạng B 1800 s (AD_Gun_SlotIndex=6, AD_Gun_Num=14, đồng hồ 29:59 trên lưới), segment 0 không cho gì và không tiêu cờ AD_Gun_Event_Given |
runtime-verified (device) | G1739; [report: f025_weapon02_seg1_vs_seg0.png] |
Hiệu lực tức thì trong cùng phiên chơi đến từ Config => _config ?? EmbeddedDefaults(Segment) được tính lúc đọc, KHÔNG phải từ OnConfigApplied — ép segment giữa phiên là lần hợp nhất kế tiếp đã theo bảng mới |
disasm-proven + runtime-verified (device) | G1739; ServerConfig_manager.get_Config 0x2AC7A40 |
ServerConfig_manager.OnConfigApplied có đúng HAI người nghe trong cả game: FreeCash_manager.Awake (0x2A6C6E4) và OfferWall_manager.OnEnable (0x2B28D4C) — cả hai đều R1-out ⇒ trên bản học tập sự kiện này không vẽ lại được gì, và đó là ĐÚNG thiết kế chứ không phải nợ restore |
disasm-proven (xrefs add_OnConfigApplied 0x2AC7888) |
G1740 |
2.3a Chứng minh ánh xạ 12 số → 12 field (đọc theo OFFSET, không theo thứ tự tên)
Đây là chỗ dễ sai nhất của cả tài liệu, nên ghi lại nguyên văn phép ghi bộ nhớ.
EmbeddedDefaults(1) (0x2AC7B20) ghi ba khối 16 byte vào object ConfigResp:
*(_OWORD *)(obj + 40) = xmmword_105B0E0; // 0x28 .. 0x37
*(_OWORD *)(obj + 56) = xmmword_105D5A0; // 0x38 .. 0x47
*(_OWORD *)(obj + 72) = xmmword_105B890; // 0x48 .. 0x57
Giá trị đọc từ .rodata (IDA get_dword):
105B0E0 = [15, 35, 10, 12] · 105D5A0 = [0, 6, 2, 0] · 105B890 = [0, 0, 99999, 1]
Ghép với offset field của ConfigResp (dump.cs:589438):
| Offset | Field | Khối | Giá trị segment 1 |
|---|---|---|---|
0x28 |
freecash_stage_min |
105B0E0[0] |
15 |
0x2C |
freecash_stage_max_hud |
105B0E0[1] |
35 |
0x30 |
offerwall_stage_min |
105B0E0[2] |
10 |
0x34 |
inter_stage_min |
105B0E0[3] |
12 |
0x38 |
inter_cooltime |
105D5A0[0] |
0 |
0x3C |
inter_daily_cnt_rebirth |
105D5A0[1] |
6 |
0x40 |
inter_daily_cnt_mine |
105D5A0[2] |
2 |
0x44 |
inter_daily_cnt_challenge |
105D5A0[3] |
0 |
0x48 |
inter_daily_cnt_boss |
105B890[0] |
0 |
0x4C |
inter_daily_cnt_hunt |
105B890[1] |
0 |
0x50 |
inter_daily_cnt_whole |
105B890[2] |
99999 |
0x54 |
weapon_02_ad_onoff |
105B890[3] |
1 |
Nhánh segment != 1 ghi vdupq_n_s32(0x1869F) = 99999 vào cả 4 ô của khối đầu và 0 vào 8 ô còn
lại; segment = 2 nếu tham số là 2, ngược lại 0.
interstitial ở 12, FreeCash bật ở 15 và ẩn ở 36. Nếu ánh xạ bị lệch một nhịp
(offerwall ← 35, inter ← 15) thì OfferWall phải lật ở 35 và interstitial ở 15 — đo được là không.
Hai kênh độc lập (byte trong .rodata + hành vi trên thiết bị) cùng chỉ về một bảng.
| Claim | Mức | Nguồn |
|---|---|---|
Mô tả tiếng Việt của buff_pack xác nhận IsPermanentBuff áp cho CẢ hai buff Power và Money |
runtime-verified (live) | phaseA/01_shop.png; đối chiếu IsBuffActive 0x2AE4AF8 |
| Claim | Mức | Nguồn |
|---|---|---|
3. Danh mục AD_Type — 10 giá trị và mỗi giá trị mở cái gì
public enum AD_Type — dump.cs:584959. Dispatch: Admob.Reward_Get() 0x2A5F0B0.
| # | AD_Type |
Điểm gọi ShowRewardAd (RVA) |
Hàm nhận thưởng khi xem xong (RVA) | Phần thưởng | Mã AppsFlyer |
|---|---|---|---|---|---|
| 0 | Money_Buff |
Buff_Panel.AD_Watch 0x2AE4F38 |
Buff_manager.AddBuff(0) 0x2AE4510 |
moneyBuffTime = min(+300, 900) + Gamemanager.InvalidateMoneyCache() |
5 |
| 1 | Power_Buff |
Buff_Panel.AD_Watch 0x2AE4F38 |
Buff_manager.AddBuff(1) 0x2AE4510 |
powerBuffTime = min(+300, 900) |
6 |
| 2 | SpeedRun_Bonus |
SpeedRun_manager.Bonus_AD_Show 0x2B40E08, SweepBonus_AD_Show 0x2B41A04 |
SpeedRun_manager.Bonus_Get |
thưởng bonus của chế độ Play/Challenge | 1 |
| 3 | Mine_Bonus |
Mine_manager.Bonus_AD_Show 0x2AF562C, SweepBonus_AD_Show 0x2AF5B28 |
Mine_manager.Bonus_Get |
thưởng bonus mỏ | 2 |
| 4 | Boss_Bonus |
Boss_manager.Bonus_AD_Show 0x2AF21A8, SweepBonus_AD_Show 0x2AF2D94 |
Boss_manager.Bonus_Get |
thưởng bonus boss | 3 |
| 5 | Monster_Bonus |
Monster_manager.Bonus_AD_Show 0x2AFB094, SweepBonus_AD_Show 0x2AFAEC4 |
Monster_manager.Bonus_Get |
thưởng bonus Hunt (PetCoin) | 4 |
| 6 | Mission_Reward |
Mission_manager.Reward_Get 0x2A71A34 |
Mission_manager.Reward_Get_By_Ad |
nhận thưởng mission (kiểm FindFirstEmptyOwned != -1 trước) |
7 |
| 7 | Fish_Gacha |
Fish_manager.FreeGacha 0x2B00AB4 |
Fish_manager.FreeGacha_RewardGet |
1 lượt gacha cá miễn phí | 8 |
| 8 | Upgrade_Rebirth |
Rebirth_manager.UpgradeRebirth_Start_By_IAP 0x2B23454 (tail @0x2b23668) |
Rebirth_manager.UpgradeRebirth_Start_By_Ad 0x2B233D0 → Rebirth_FX.Start_Internal |
bật is_UpgradeRebirth ⇒ thưởng Rebirth ×3 |
9 |
| 9 | AD_Reward_Gun |
Gun_manager.AdRewardGun_TryWatch 0x2B0C484 |
Gun_manager.AdRewardGun_Acquire 0x2B0C508 |
giữ vĩnh viễn khẩu súng thưởng đang đếm ngược | 10 |
Ghi chú quan trọng: IsBuffActive(type) trả true cho mọi type khi IsPermanentBuff — nghĩa là
buff_pack bật vĩnh viễn cả hai buff, không phải một (0x2AE4AF8). GetBuffRemainingTime trả -1
khi vĩnh viễn (0x2AE4B4C) → Buff_Panel.Update (0x2AE4D4C) hiểu -1 là "ẩn nút xem QC, fill = 1.0".
4. Bề mặt IAA — 6 loại placement, 28 điểm kích hoạt quảng cáo trong code
|---|---|---|---|
| Rewarded | 13 điểm gọi ShowRewardAd(AD_Type) (+1 overload ShowRewardAd(int) nối từ scene) | AD_Cool, _isShowing, is_AD_Disable_Version, mạng | Chạy (rewarded KHÔNG bị segment tắt) | Chạy |
| Interstitial | 9 (5 Inter_Place) | 7 điều kiện §4.2 | Tắt (inter_stage_min = 99999) | Đủ tư cách chỉ Rebirth (6/ngày) + Mine (2/ngày) từ Stage 12 |
| Banner | 3 show / 3 hide | Is_Banner_Blocked | Chạy khi vào Boss/Hunt/Raid | như segment 0 (không bị ServerConfig gate) |
| OfferWall (Tapjoy) | 2 (Open_Hud, Open_Shop) | offerwall_stage_min | Tắt (99999) | BẬT từ Stage 10, vĩnh viễn |
| FreeCash (AlmediaLink) | 1 panel + HUD + banner shop | freecash_stage_min + trạng thái Almedia | Tắt (99999) | Cửa sổ Stage 15 → 35, ngoài khoảng thì ẩn |
| Ad-gun weapon_02 | (dùng lại 1 trong 13 điểm rewarded; CheckAdGunEvent là trigger cấp súng thử, không phải điểm hiện quảng cáo) | weapon_02_ad_onoff | Tắt (0) | BẬT (1) |
4.1 Rewarded — Admob.ShowRewardAd(AD_Type) 0x2A59294
ShowRewardAd(AD_Type type):
if (Shop_manager.instance == null) -> NRE
// ĐƯỜNG TẮT ENTITLEMENT (không có quảng cáo nào được hiện)
if (Shop_manager.Is_Remove_AD_All || FreeCash_manager.RewardAdRemoved):
ad_type = type; StartCoroutine(Reward_Get()); return // @0x2a59310..0x2a59334
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", (int)AD_Cool); return
ad_type = type
if (rewardAd != null && rewardAd.IsLoaded() && Is_LoadedAdExpired()) // > 50 phút
Log("[ADX] Loaded rewarded ad expired -> discard & reload"); Discard_RewardAd()
if (rewardAd != null && rewardAd.IsLoaded()) -> TryShowLoadedAd()
else if (!_sdkReady) -> Alert("Try Later")
else if (Application.internetReachability == NotReachable) -> Alert("Check Network Connection")
else { _pendingShow = true; ShowAdLoadingPanel(); StartLoadTimeout(); LoadRewardedAd(); }
Reward_Get() (0x2A5F0B0) — chạy sau khi SDK báo earned:
yield return null // 1 frame
AD_Cool = 20.0f // giãn cách toàn cục
Sound_manager.PlaySfx(7)
if (!Shop_manager.Is_Remove_AD_All)
AppsFlyerInit.SendLog_AdsReward(AF_CODE[(int)ad_type]) // [5,6,1,2,3,4,7,8,9,10]
switch ((int)ad_type) { 0,1 -> Buff_manager.AddBuff(ad_type); 2 -> SpeedRun.Bonus_Get(); ... }
Bài học thiết kế: phần thưởng KHÔNG được trao trong callback của SDK mà trong một coroutine của
game — nên đường "đã mua xoá quảng cáo" tái dùng đúng cùng một đường trao thưởng. Một điểm trao
thưởng duy nhất = một điểm để kiểm toán. (Trong bản restore, đường này nằm sau IAdService fail-closed:
NullAdService.ShowRewarded() trả failure, coroutine trao thưởng không chạy.)
4.1a Nhánh Granted — SỐ ĐO THẬT đầu tiên trên thiết bị (F024, 2026-09-06)
Cho tới 06-09, mọi thứ trong §4.1 là đọc từ binary: bản dựng lại đi qua IAdService, và
NullAdService (fail-closed theo R1) luôn trả NotAvailable, nên không một nhánh nhận thưởng nào
từng chạy trên máy. F024 (QD-23, và R1a trong quy tắc dự án (nội bộ)) thêm một provider quân xanh
(SimulatedAdService, chỉ có dưới CATGUNNER_LEARNING_DEBUG, chỉ bind khi bật công tắc) để chạy
được nhánh đó và đo. Quân xanh KHÔNG chứng minh luồng quảng cáo đã restore 1:1 — cái được nghiệm
thu 1:1 là phía game xử lý callback.
Placement Mine_Bonus (AD_Type 3) — đo trên Pixel 8 Pro, APK f024-sim:
| Bước | Điều kiện / thời điểm | Số đo (đọc từ save, không đọc bằng mắt) |
|---|---|---|
| Vào Mỏ | cần 1 vé (TicketUse_Count < 2); ván dài 60 s |
vé 2/2 → 1/2 |
Panel Game End mở |
Reward_Panel_Reload 0x2AF5404 vừa vẽ vừa PHÁT Reward_Value |
Dia 980 → 1078 (+98 = Reward_Value) |
| Panel quảng cáo | nhãn Phần thưởng = 4 × Reward_Value |
hiện 392 (= 4×98) ✔ |
Bấm Nhận ×4 → xem hết → Granted |
Bonus_Get 0x2AF5634 cộng 3× (vì panel đã phát 1× lúc mở) |
Dia 1078 → 1372 (+294 = 3×98) ✔ |
| Ngay sau khi cấp | Bonus_Get gọi thẳng Game_Exit |
tự thoát về HUD chính ✔ |
Đối chứng: bấm Bỏ qua → Denied |
quân xanh trả Denied |
Dia 1468 → 1468, panel giữ nguyên, 0 exception ✔ |
⇒ Xác nhận bằng số đo cái mà trước đây chỉ suy từ binary: tổng tiền của một ván Mỏ có xem quảng
cáo đúng bằng 4 × Reward_Value, và hệ số của riêng nhánh quảng cáo là 3×, không phải 4×.
Sai lầm dễ mắc khi chỉ đọc UI: nhãn ghi ×4 nhưng hàm chỉ cộng 3×.
Placement Fish_Gacha (AD_Type 7) — đo trên Pixel 8 Pro, APK g1722, hồ sơ p54 (2026-09-11):
| Bước | Điều kiện / thời điểm | Số đo (đọc từ save sau force-stop) |
|---|---|---|
Bấm Free Fish Draw lần 1-3 |
FreeGachaCount 0,1,2 (< 3) ⇒ cấp THẲNG, không quảng cáo |
FishInventory [] → [0,0,0] |
| Bấm lần 4 | FreeGachaCount == 3 ⇒ ShowRewarded(Fish_Gacha) |
logcat [CGSIM] rewarded Fish_Gacha -> opened simulated screen (5,0s) |
Claim reward → Granted |
cấp bằng FreeGacha_RewardGet() — CÙNG hàm với nhánh cho thẳng |
FishInventory → [0,0,0,0], FreeGachaCount → 4 |
| Cổng trên nó | Inventory.Count >= 20 ⇒ Alert Alarm/MaxFish; FreeGachaCount >= 5 ⇒ Alert Alarm/MaxFreeGacha |
(chưa chạm trong lượt đo) |
⇒ Bài học placement đáng chép: quảng cáo ở đây KHÔNG mở thêm lượt — nó chỉ được cài vào 2 lượt
CUỐI của 5 lượt miễn phí mỗi ngày. Lượt xem quảng cáo vẫn cộng FreeGachaCount y như lượt free,
nên trần vẫn là 5/ngày. Đây là mẫu "gate muộn": người chơi nhận 3 lượt không ma sát để hình thành
thói quen, rồi mới bị hỏi xem quảng cáo cho 2 lượt còn lại — khác hẳn mẫu Mine_Bonus (quảng cáo
NHÂN phần thưởng lên 4×). Hai mẫu này nên được phân biệt rõ khi thiết kế: gate lượt vs nhân thưởng.
SPEC: [evidence codegen: SPEC-F024-T4-fish-gacha.md].
Placement Power_Buff (AD_Type 1) / Money_Buff (0) — đo trên Pixel 8 Pro, APK g1722 (2026-09-11):
Đường bấm: biểu tượng x3 trên HUD mở popup Buff (hai thẻ Power/Money, mỗi thẻ một đồng hồ và
một nút +5 min có icon video). Bấm +5 min → ShowRewarded → Granted →
Buff_manager.AddBuff 0x2AE4510.
| Bước | Số đo |
|---|---|
[CGSIM] rewarded Power_Buff -> Granted |
save PowerBuffTime 0.0 → 288.27 |
[CGSIM] rewarded Money_Buff -> Granted |
save MoneyBuffTime 0.0 → 282.23 (cùng khuôn, cộng thêm Gamemanager.InvalidateMoneyCache()) |
| Đối chiếu L1 | powerBuffTime = Mathf.Min(powerBuffTime + 300f, 900f) ⇒ cấp 300 s, phần hụt (~12 s) là thời gian trôi giữa lúc cấp và lúc ghi save — buff đếm ngược THEO THỜI GIAN THỰC |
| Trần | 900 s: xem lần thứ tư trong 15 phút không thêm được gì (nút vẫn bấm được, giá trị bị kẹp) |
⇒ Mẫu placement thứ ba, khác cả Mine_Bonus (nhân thưởng) lẫn Fish_Gacha (gate lượt): đây là
"nạp thời gian có trần" — phần thưởng cộng dồn nhưng bị kẹp, nên người chơi được khuyến khích
xem ĐỀU (3 lần/15 phút) chứ không xem dồn. Hai chi tiết 1:1 dễ bỏ sót: AddBuff gọi
Cat_manager.Balance_Reload() ở MỌI AD_Type kể cả loại không phải buff; và
GetBuffRemainingTime dùng −1f làm sentinel "vĩnh viễn" cho gói Buff IAP.
SPEC: [evidence codegen: SPEC-F024-T4-power-buff.md].
Placement SpeedRun_Bonus (AD_Type 2) — đo trên Pixel 8 Pro, APK g1722, p344 (2026-09-11):
| Bước | Số đo |
|---|---|
HUD Chơi → Stage 2 — Phá hủy mọi cây trong 1 phút! → Thử thách |
ván tự chạy, không cần điều khiển tay (khác Mỏ/Săn) |
| ~60 s → panel VICTORY | Phần thưởng hiện 120, nút Nhận ×4, dòng từ chối Không, cảm ơn 30 |
[CGSIM] rewarded SpeedRun_Bonus -> Granted |
save Dia 290 → 410 (+120), SpeedRun_Stage_Now 1 → 2 |
⇒ Cùng khuôn Mine_Bonus: Reward_Value = 30 (hằng duy nhất trong .cctor 0x2B42214), panel trả
1× lúc mở, Bonus_Get (0x2B40E10) trả thêm 3× ⇒ nhãn ×4 là TỔNG, nhánh quảng cáo chỉ
cộng 3×. Tổng +120 là số đo; phần tách 30+90 là suy từ L1 (chưa chụp riêng Dia lúc panel mở).
Bốn mẫu placement đã phân biệt được: nhân thưởng (Mine_Bonus, SpeedRun_Bonus), gate lượt
(Fish_Gacha), nạp thời gian có trần (Power/Money_Buff), mở nhánh khác (Upgrade_Rebirth).
SPEC: [evidence codegen: SPEC-F024-T4-speedrun-bonus.md].
Placement Boss_Bonus (AD_Type 4) — đo trên Pixel 8 Pro, APK g1722 (2026-09-12):
Đây là placement đầu tiên đo được CẢ HAI nhánh bằng số, nên nó là chỗ chốt cho khuôn
"nhân thưởng" mà Mine_Bonus/SpeedRun_Bonus mới chỉ suy từ L1:
| Lượt | Hành động | Dia trước → sau | Δ |
|---|---|---|---|
| 1 | thắng → Nhận ×3 (xem quảng cáo) |
290 → 740 | +450 |
| 2 | thắng → Không, cảm ơn (bỏ qua) |
740 → 890 | +150 |
⇒ thắng cấp 1× (150), quảng cáo cấp thêm 2× (300); nhãn 450 trên panel là TỔNG, nút từ
chối ghi đúng phần đã cấp. Khớp Boss_manager.Reward_Value = 150 (.cctor 0x2AF2F14),
Game_Win 0x2AF1C2C (Get_Dia(reward), Bonus_txt = reward*3), Bonus_Get 0x2AF21B0
(Get_Dia(reward*2) rồi Game_Exit()).
Bài học dàn cảnh (đắt, đã trả giá hai phiên): chế độ Boss vô hiệu hoá Upgrade Power/Speed
(panel vào Boss ghi thẳng ra), và máu boss theo Boss_manager.Stage_Now (save Boss_Stage) chứ
không theo Stage_Value_Max. Muốn thắng để chạm nút quảng cáo thì nâng rebirth và hạ
Boss stage, đừng nâng Upgrade. Trigger interstitial cũng lặp lại đúng mẫu Mỏ: bỏ qua thì có,
đã xem rewarded thì không. SPEC: [evidence codegen: SPEC-F024-T4-boss-bonus.md].
Placement Mission_Reward (AD_Type 6) — đo trên Pixel 8 Pro, APK g1722 (2026-09-12):
Mẫu thứ năm, và là mẫu dễ bị xếp nhầm nhất: quảng cáo ở đây không nhân phần thưởng, nó là một cổng đặt trên mốc battle-pass.
| Lv (0-based) | Ô Normal | Nhận bằng | Dia |
|---|---|---|---|
| 0 / 1 | 200 / 240 dia | bấm thẳng | 890 → 1090 → 1330 |
| 2 | súng Single |
bấm thẳng | không đổi |
| 3 | 320 dia + 📽 | xem quảng cáo | 1330 → 1650 (+320) |
Nhận đúng mệnh giá in trên ô, không hơn một dia. is_AD_Reward (nội tuyến trong Reward_Get
0x2A71A34) = Lv >= 3 && Lv % 3 == 0 && degree == Normal, và đã kiểm bằng mắt: sau khi nhận Lv 3
thì ô mang biểu tượng phim kế tiếp là Lv 6 — đúng chu kỳ 3.
Kèm theo là một chi tiết UX đáng chép: mốc bị khoá tuần tự (is_Reward_SequenceLocked,
Alert Mission/SequenceLocked), và mỗi lần nhận xong dải phần thưởng tự cuộn tới mốc chưa nhận
đầu tiên. Người chơi vì thế luôn nhìn thấy đúng một "việc kế tiếp", và cứ 3 việc thì có 1 việc đòi
quảng cáo — nhịp ép xem quảng cáo được gài vào tiến trình, không gài vào kết quả trận đấu
như nhóm *_Bonus. SPEC: [evidence codegen: SPEC-F024-T4-mission-reward.md].
Placement AD_Reward_Gun (AD_Type 9) — mẫu THỨ SÁU: cho mượn rồi đòi lại.
Đo trên Pixel 8 Pro, APK g1726 (2026-09-12), sau khi dựng L1 cả cụm Gun_manager (xem SPEC).
Đây là mẫu sắc sảo nhất trong sáu mẫu, vì nó không bán một phần thưởng — nó bán quyền giữ lại thứ bạn đã cầm trong tay:
- Ngay sau lần hợp nhất súng đầu tiên, game đặt khẩu súng số 14 (hạng B, tốt hơn hẳn thứ người chơi đang có) vào thẳng kho, kèm đồng hồ đếm ngược 1800 s hiện trên ô.
- Chạm vào nó, trang bị nó, hay hợp nhất nó — cả ba đều bật lời mời xem quảng cáo
(
Gun_Item.OnPointerUp,Gun_Item_List.EquipToSlot,Gun_Item_List.FuseItems). - Xem xong: đồng hồ biến mất, súng ở lại. Không xem: hết 1800 s,
Update(nhịp 0,25 s) gọiAdRewardGun_Expirevà rút súng khỏi kho.
So với "nhân thưởng", đòn bẩy tâm lý đảo chiều: người chơi không cân nhắc được thêm bao nhiêu mà
sợ mất cái đang có. Hai chi tiết cho thấy thiết kế được mài kỹ: Update hoãn thu hồi khi
quảng cáo đang mở (Admob.IsRewardAdBusy) — không ai xem hết quảng cáo để nhận về ô trống; và
CheckAdGunEvent bỏ qua người đã có súng hạng ≥ B (đặt cờ mà không tặng gì) — mồi chỉ dành cho
người nó thật sự cám dỗ được.
Cổng bật/tắt nằm ở liveops: weapon_02_ad_onoff == 1, và bản production đang TẮT cho phân khúc
0 (đo được từ ServerConfig_Cache của hồ sơ store thật). Bảng nhúng của client chỉ bật ở phân khúc
1. SPEC: [evidence codegen: SPEC-F024-T4-ad-reward-gun.md].
Sáu mẫu placement đã phân biệt được (xương sống để chép sang game khác):
| Mẫu | Đại diện | Quảng cáo làm gì | Người chơi mất gì nếu không xem |
|---|---|---|---|
| Nhân thưởng | Mine_Bonus, Boss_Bonus, SpeedRun_Bonus |
×3–×4 phần thưởng cuối trận | vẫn giữ 1× |
| Cổng mốc tiến trình | Mission_Reward |
mở mốc mỗi 3 bậc, không nhân | mốc đứng đó, tiến trình tắc |
| Gate lượt | Fish_Gacha |
mở 2 lượt cuối của 5 lượt free/ngày | mất 2 lượt |
| Nạp thời gian có trần | Power_Buff, Money_Buff |
+300 s buff, trần 900 s | không có buff |
| Mở nhánh khác | Upgrade_Rebirth |
mở lượt rebirth tăng cường | đi nhánh thường |
| Cho mượn rồi đòi lại | AD_Reward_Gun |
giữ khẩu súng đã đặt sẵn trong kho | mất thứ đang cầm sau 1800 s |
Trigger phụ đo được cùng lượt — interstitial khi thoát Mỏ. Mine_manager.Game_Exit bắn
interstitial chỉ khi _bonusAdWatched == false (cờ +0x72, do Bonus_Get bật). Đo:
- thoát bằng Không, cảm ơn (chưa xem thưởng) → có interstitial;
- thoát bằng đường Bonus_Get (đã xem thưởng) → không interstitial.
Đây là một mẫu thiết kế đáng chép: người chơi vừa xem một rewarded thì được miễn interstitial ngay sau đó — tránh dí hai quảng cáo liên tiếp vào đúng người vừa hợp tác.
Trạng thái đo của 10 placement. Tới 2026-09-12 (phiên 5aad97a9): 10/10 đã chạy nhánh Granted
trên máy VÀ 10/10 có SỐ. Bảng này giờ không còn ô trống:
AD_Type |
Đo được? | Ghi chú |
|---|---|---|
Mine_Bonus, Fish_Gacha, Power_Buff, Money_Buff, SpeedRun_Bonus, Boss_Bonus, Mission_Reward, Upgrade_Rebirth |
ĐÃ ĐO có số | 8/10 |
AD_Reward_Gun |
ĐÃ ĐO | dựng L1 cả cụm 2026-09-12; cổng liveops đang TẮT ở production phân khúc 0 |
Monster_Bonus |
ĐÃ ĐO có số (G1733, 2026-09-12) | ô cuối cùng được lấp. Hoá ra KHÔNG cần điều khiển tay: mèo tự bắn, PetCoin tự hút, để yên ~60 s là qua vài cấp. Cộng dồn 30 PetCoin/cấp cấp NGAY lúc chơi; Nhận ×2 cấp THÊM đúng 1× (ví 240 → 270 với cộng dồn 30); bỏ qua cấp 0 thêm + bắn interstitial (cộng dồn 210 → ví giữ 210). Chi tiết: docs/functions/san-monster-song-va-petcoin.md |
Bài học phương pháp từ ô cuối này: dòng cũ ghi "Săn là chế độ điều khiển tay ⇒ PetCoin_Accum = 0
khi để mèo tự đứng" — sai. Giả thiết "chế độ tay thì không tự tiến được" chưa từng được thử; thử
ĐÚNG MỘT LẦN (vào màn, đợi 60 s, thoát) là ra số. Trước khi ghi một ô là "không đo được vì điều kiện
màn", hãy bỏ 60 giây đứng yên trong màn đó.
Chi tiết bộ giả lập + cổng an toàn (bản identity không chứa một byte nào của nó):
docs/functions/gia-lap-quang-cao-iap.md.
4.1b Mẫu "quota ngày có phần cho không + phần gắn quảng cáo" — placement Fish_Gacha (F008 T5b)
Đây là kiểu placement thứ hai của game, khác hẳn §4.1/§4.1a. Mine_Bonus là nhân phần thưởng
vừa nhận (một lần, ở cuối một ván). Fish_Gacha là một quota ngày có hai đoạn: đoạn đầu cho
không, đoạn sau gắn quảng cáo. Đo trọn từng nấc trên Pixel 8 Pro, APK f008-t5b2, 0 exception.
FreeGacha(): // Fish_manager 0x2B00A24 — CHỈ định tuyến
FreeGacha_Reload() // nạp lại quota nếu sang ngày mới — doc 07 §3.7
if (Inventory.Count >= MAX_FISH) -> Alert(MaxFish); return // trần kho chặn TRƯỚC
if (FreeGachaCount >= 5) -> Alert(MaxFreeGacha); return // hết quota
if (FreeGachaCount >= 3) -> ShowRewarded(Fish_Gacha); return
FreeGacha_RewardGet() // cấp NGAY, không quảng cáo
FreeGachaCount |
Bấm nút | Nhãn x{5−n} |
Biểu tượng quảng cáo |
|---|---|---|---|
| 0, 1, 2 | thưởng ngay, không quảng cáo | x5 → x4 → x3 | tắt |
| 3, 4 | phải xem rewarded rồi mới nhận | x2 → x1 | bật |
| ≥ 5 | Alert(MaxFreeGacha) |
x0, nút sang màu disable | tắt lại |
Đo được: ba lượt đầu ra thưởng ngay (x5 → x4 → x2, chỉ số cộng đúng bảng); lượt thứ tư hiện
biểu tượng và đi nhánh rewarded, Granted → thưởng vào kho, nhãn x1; sau lượt thứ năm nhãn
x0, biểu tượng biến mất, nút đổi màu disable. Save đối chứng: FreeGachaCount 3 → 5.
Ba quy tắc rút ra:
- Phần cho không phải đứng TRƯỚC phần gắn quảng cáo, không được trộn lẫn. Ba lượt đầu là dạy giá trị: người chơi thấy phần thưởng rơi ra và thấy chỉ số nhích lên ngay trên cùng màn hình. Tới lượt thứ tư, câu hỏi "xem quảng cáo chứ?" mới là câu hỏi có giá đã niêm yết. Gắn quảng cáo từ lượt đầu biến bề mặt thành máy bán hàng cho một món chưa ai biết giá. Ngoài ra, phần cho không giữ lý do quay lại hằng ngày cho cả nhóm người không bao giờ xem quảng cáo — bề mặt IAA không đuổi ai đi. Và biên trên (5) giữ cho nó không thành máy cày.
- Đặt cổng đếm bằng HAI hằng số trên MỘT bộ đếm, không bằng hai bộ đếm:
FreeGacha_MaxCount = 5vàFreeGacha_NoAdCount = 3cùng đọcFreeGachaCount. Đổi tỉ lệ cho-không/gắn-quảng-cáo chỉ là đổi một số; không có trạng thái thứ hai để lệch pha với save. - Hiển thị biểu tượng quảng cáo theo DẢI, không theo NGƯỠNG.
csharp AD_Draw_Icon_obj.SetActive((uint)(FreeGachaCount - 3) < 2u); // count ∈ {3,4}Một phép trừ + một so sánh unsigned bắt trọn cả hai biên. Viếtcount >= 3thì biểu tượng dính lại vĩnh viễn sau khi hết quota và người chơi bấm vào một nút hứa quảng cáo để nhận về một thông báo lỗi. Tổng quát:visible = (uint)(used − free) < adSlots.
Cùng bề mặt còn có một nút trả tiền, và nó KHÔNG mua tỉ lệ tốt hơn — nó mua một SÀN cao hơn.
Cả hai nút dùng chung một bảng tỉ lệ cộng dồn — {60, 30, 10, 0} đọc từ scene
(Main.unity:20930271); nút miễn phí trả baseGrade + i với baseGrade = 0, nút 200 Dia trả
baseGrade = 1. Vì mỗi bậc mạnh gấp ~3, dịch một bậc đã là gấp ba — không cần bảng tỉ lệ thứ hai.
Ưu điểm kiến trúc: một bảng duy nhất phục vụ mọi nút, thêm một hạng nút chỉ là Roll(base + 1),
không có hai bảng để lệch nhau. (Đo: Dia 1368 → 1168.)
| Nút | baseGrade |
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 điều đáng đọc trong bảng này. (a) Phân phối lệch mạnh về bậc nền và đuôi bị cắt cụt — gacha
ở đây là máy rót nguyên liệu, không phải máy tạo bất ngờ; toàn bộ cảm giác tiến bộ được đẩy sang
mạch hợp nhất. (b) Ô thứ tư giữ chỗ nhưng để 0: bậc thưởng cao nhất có sẵn slot, bật lại chỉ
là đổi một số trong scene, không phải sửa code — một mẹo vận hành rẻ cho live-ops.
⚠️ BẪY ĐÃ LÀM SAI MỘT LẦN Ở CHÍNH TÀI LIỆU NÀY (G1682).
GachaChanceslà fieldpublic⇒ được serialize ⇒ giá trị trongMain.unitythắng lúc chạy. Ctor0x2B01D08ghi{40, 30, 20, 10}; bản đầu của §4.1b chép con số đó và publish ra dashboard. Bảng thật là{60, 30, 10, 0}. Cái bắt được lỗi là một quan sát trên máy: năm lượt Epic liên tiếp đều ra hạng thấp nhất —0,6⁵ ≈ 7,8%(bình thường) so với0,4⁵ ≈ 1%(đáng nghi). Quy tắc: với mọi bảng tỉ lệ / bảng giá / bảng cân bằng, kiểm loại field trước khi tin con số — xem doc 07 §4 mục 16. Ctor vẫn phải chép vào code (initializer là nợ 1:1 thật), nhưng viết con số của ctor vào tài liệu như cân bằng của game là sai. Hai việc khác nhau.
Thứ tự cổng có ý nghĩa đạo đức, không chỉ kỹ thuật: nhánh trả tiền kiểm kho đầy TRƯỚC khi kiểm tiền, nên kho đầy thì không bị trừ Dia. Đảo lại là ăn cắp.
Bẫy dựng lại (G1681): chép thẳng Admob.instance.ShowRewardAd(...) từ binary nổ NRE ở đúng
lượt thứ tư vì Admob là lớp R1-out (instance không bao giờ được gán). Mọi lời gọi tới lớp
R1-out phải đi qua ServiceLocator.Ads.ShowRewarded(placementId, cb) và cấp thưởng trong nhánh
Granted (§9.3). Cổng kiểm cho mọi lô mới: grep Admob.instance|IAP_manager.instance|
Tapjoy_manager.instance trong file vừa dựng phải rỗng.
Chi tiết đầy đủ của hệ này: docs/functions/ca-fish.md.
4.2 Interstitial — chuỗi cổng đầy đủ (Inter_Placement_Able 0x2A5CD34)
bool Inter_Placement_Able(Inter_Place place):
1. if (is_AD_Disable_Version) -> false // @0x2a5ce5c
2. if (Is_Interstitial_Blocked()) -> false // @0x2a5ce98
= is_Interstitial_Disable || Is_Remove_AD_forced || Is_Remove_AD_All
3. if (_interShowing || _interPlacementRunning) -> false // @0x2a5cf34
4. stageDisplay = Tree_manager.instance != null
? Tree_manager.instance.Stage_Value_Max / 10 + 1 : 0 // @0x2a5d080
if (stageDisplay < ServerConfig.InterStageMin) -> false // @0x2a5d0b0
5. cool = ServerConfig.InterCooltime
if (cool >= 1 && _lastInterShownUtc != DateTime.MinValue
&& (UtcNow - _lastInterShownUtc).TotalSeconds < cool) -> false // @0x2a5d1d4
6. Inter_DayCheck() // @0x2a5d270
cnt = PlayerPrefs.GetInt("inter_place_cnt_" + (int)place)
if (cnt >= Inter_DailyLimit(place)) -> false // @0x2a5d29c
7. whole = PlayerPrefs.GetInt("inter_place_cnt_whole")
if (whole >= ServerConfig.InterDailyCntWhole) -> false // @0x2a5d2e0
return true
ShowPlacementInterstitial(place, onClosed) (0x2A5D508) → nếu không able thì gọi thẳng
onClosed() (không bao giờ chặn luồng game). Nếu able → PlacementInterCo (0x2A5EADC):
_interPlacementRunning = true; interLoadingPanel.SetActive(true)
yield WaitForSecondsRealtime(interLoadingSeconds) // scene: 1.2 s
for (attempt = 0; attempt < interLoadMaxAttempts; attempt++) { // scene: 1
if (IsInterstitialReady) break
if (interstitialAd.IsLoaded() && Is_InterExpired()) Discard_InterAd()
LoadInterstitialAd()
t = 0; while (t < interLoadWaitPerAttempt) { // scene: 3 s
if (IsInterstitialReady) break
if (_interLoading) { t += unscaledDeltaTime; yield null } else yield WaitRealtime(0.5) }
}
interLoadingPanel.SetActive(false); _interPlacementRunning = false
if (IsInterstitialReady) { ShowInterstitialAd(onClosed); if (_interShowing) Inter_CountUp(place) }
else { LogWarning("[ADX] Placement 전면광고 로드 실패({0}회) → 스킵 — {1}"); onClosed?.Invoke(); LoadInterstitialAd() }
Inter_CountUp(place) (0x2A5D644): DayCheck → +1 cho khoá của place → +1 cho whole →
PlayerPrefs.Save() → _lastInterShownUtc = UtcNow → log.
Stage_Value_Max |
MaxStageDisplay |
OfferWall_Btn |
FreeCash_Btn |
Interstitial đủ tư cách | Ngưỡng chạm |
|---|---|---|---|---|---|
| 0 – 80 | 1 – 9 | ✗ | ✗ | – | |
| 90 | 10 | ✓ BẬT | ✗ | – | offerwall_stage_min = 10 |
| 100 | 11 | ✓ | ✗ | – | |
| 110 | 12 | ✓ | ✗ | Rebirth ✓ · Mine ✓ | inter_stage_min = 12 |
| 130 | 14 | ✓ | ✗ | Rebirth · Mine | |
| 140 | 15 | ✓ | ✓ BẬT | Rebirth · Mine | freecash_stage_min = 15 |
| 150 – 340 | 16 – 35 | ✓ | ✓ | Rebirth · Mine | |
| 349 | 35 | ✓ | ✓ (cạnh cuối) | Rebirth · Mine | freecash_stage_max_hud = 35 |
| 350 | 36 | ✓ | ✗ ẨN LẠI | Rebirth · Mine | vượt max_hud |
| 400 · 990 | 41 · 100 | ✓ | ✗ | Rebirth · Mine |
is_AD_Disable_Version = 0 · is_Interstitial_Disable = 0 ·
Is_Interstitial_Blocked = Is_Remove_AD_All || Is_Remove_AD_forced = 0 · reentrancy = 0 —
chỉ duy nhất cổng stage (gate 4) đổi trạng thái theo tiến trình.
Challenge / Boss / Hunt không xuất hiện ở bất kỳ hàng nào — chúng bị chặn thuần tuý bởi
trần/ngày = 0 (gate 6), không phải bởi cổng stage. Ở segment 1, toàn bộ ngân sách interstitial nằm ở Rebirth (6/ngày) + Mine (2/ngày):
đúng hai khoảnh khắc "vừa nhận một khoản thưởng lớn", nơi người chơi ít khó chịu nhất.
BIÊN QUAN SÁT (đừng đọc quá): phiên đo chỉ chứng minh ĐIỀU KIỆN ĐỦ TƯ CÁCH (
Inter_Placement_Abletrảtrue/false).IsInterstitialReady= 0 suốt phiên vì mediation không có fill trên bản.sotsideload ⇒ không quảng cáo nào được hiện,Inter_CountUpchưa bao giờ chạy, và việc bộ đếm/cooldown có được thực thi lúc hiện hay không vẫn UNKNOWN
9 điểm kích hoạt (tất cả đều là "đóng màn kết quả", không bao giờ giữa trận):
Inter_Place |
Điểm gọi (RVA) | Điều kiện bổ sung tại điểm gọi |
|---|---|---|
Rebirth (0) |
Rebirth_manager.Rebirth @0x2b22dfc |
!(rebirthWasBoosted \|\| reviewPopupOpened) — xem §7.4 |
Mine (1) |
Mine_manager.Sweep_Close @0x2af5c1c |
!_bonusAdWatched && !Shop_manager.Is_RewardBonus_Auto |
Mine (1) |
Mine_manager.<Exit_Cor>d__38 @0x2af6514 |
_clearedThisRun && !_bonusAdWatched |
Challenge (2) |
SpeedRun_manager.Sweep_Close @0x2b41af8 |
tương tự Mine |
Challenge (2) |
SpeedRun_manager.<Exit_Cor>d__28 @0x2b42650 |
tương tự Mine |
Boss (3) |
Boss_manager.Sweep_Close @0x2af2e88 |
tương tự Mine |
Boss (3) |
Boss_manager.<Exit_Cor>d__32 @0x2af3308 |
tương tự Mine |
Hunt (4) |
Monster_manager.SweepBonus_Get @0x2afb030 |
nhánh bị chặn: hàm đặt cờ +194 = 1 rồi mới kiểm !cờ ⇒ thực tế không bao giờ chạy |
Hunt (4) |
Monster_manager.<Exit_Cor>d__46 @0x2afba4c |
tương tự Mine |
Luật vàng nhìn thấy được: nếu người chơi vừa xem một rewarded ad (
_bonusAdWatched), hoặc đã mua/được miễn quảng cáo (Is_RewardBonus_Auto), thì KHÔNG chèn interstitial. Game không bao giờ "phạt" người vừa tự nguyện xem quảng cáo.
4.3 Banner
- Bật:
Admob.ShowBannerAd()0x2A5BCF4←Boss_manager.<Enter_Cor>d__27,Monster_manager.<Enter_Cor>d__36,Raid_manager.<Enter_Cor>d__29. - Tắt:
HideBannerAd()0x2A5BD58←Boss_manager.Game_Exit,Monster_manager.StopAllGameCoroutines,Raid_manager.Game_Exit, vàHandle_InAppPurchaseChanged. - Chặn:
Is_Banner_Blocked()=Is_Remove_AD_forced || Is_Remove_AD_All(0x2A5BC70). Update_BannerPlaceholder()+Refresh_SafeArea()đẩy layout lên 50 dp để banner không che UI.- ⇒ Banner chỉ tồn tại trong 3 chế độ trận đấu có thời lượng cố định, không có ở màn idle chính.
Raid_managerxuất hiện ở đây là bằng chứng độc lập rằng Raid là nội dung đã code nhưng chưa mở (khớpUNLOCK_GATES §3"Raid — KHÔNG có cổng nào").
4.4 OfferWall (Tapjoy, placement offerwall_aos)
(khối mã nội bộ đã lược bỏ)
- Phần thưởng: kim cương, qua currency của Tapjoy →
OnEarnedCurrency→GrantGem(amount)(0x2B2AB4C). - Chống mất thưởng: nếu save chưa nạp, ghi
PlayerPrefs["ow_pending_gem"] = amountvà trao ở lần chạy sau (RecoverPendingGem0x2B29740). Timeout claimClaimTimeoutSec = 20. - 2 nơi mở:
Open_Hud()(placehud) vàOpen_Shop()(0x2B2A0F4/0x2B2A13C). - 3 object UI theo event:
OfferWall_Hud_obj,Event_Hud_obj,Event_Shop_obj. - Đo sống (segment 1):
OfferWall_Btnbật ởMaxStageDisplay = 10và giữ bật vĩnh viễn tới
4.5 FreeCash (AlmediaLink) — "liên kết tài khoản để hết phải xem quảng cáo"
(khối mã nội bộ đã lược bỏ)
vẫn không làm nút hiện; chỉ khi ghi
off_5E27948 + 0x38 = 1nó mới bật. Mặc định trên máy fresh là 3.Đính chính nhãn (có bằng chứng tĩnh): trường đó không phải
AppsFlyerInit.Segment.script.json/ScriptMetadataánh xạoff_5E27948→AlmediaLink.AlmediaLinkSDK_TypeInfo, vàAlmediaLinkSDK._almediaStatusnằm đúng ở static0x38(dump.cs:1108434).AppsFlyerInit.Segmentlà static0x8củaoff_5E27E20(dump.cs:585775), doAppsFlyerInit.Cheat_SetSegment0x2A600CCghi — một trường hoàn toàn khác.enum AlmediaStatus=NotInitialized 0, Eligible 1, Linked 2, NotAvailable 3, Blocked 4, Disabled 5(dump.cs:1110114) ⇒ mặc định 3 =NotAvailable, khớp chính xác quan sát "máy fresh không bao giờ thấy FreeCash dù ServerConfig có bật". Kết luận hành vi KHÔNG đổi — chỉ nhãn cơ chế được sửa: bản cài mới, hoặc thị trường mà Almedia không phục vụ, thì FreeCash không bao giờ lộ diện, bất kể ServerConfig nói gì.
- Cửa sổ nổi: HUD chỉ hiện trong khoảng
[freecash_stage_min, freecash_stage_max_hud](segment 1: stage 15 → 35). Banner trong shop thì không có trần trên. Đo sống:Stage_Value_Max = 349(Stage 35) ⇒ hiện;350(Stage 36) ⇒ ẩn lại - Phần thưởng:
CURRENT_REWARD = RewardType.RemoveRewardAd (1)⇒TryGiveLinkReward(0x2A6D3C4) đặtisRewardAdRemoved = truevà gọiAdmob.On_FreeCash_RewardAdRemoved()(0x2A5A37C) để huỷ quảng cáo đang nạp. Từ đóShowRewardAdtrao thưởng thẳng (§4.1). (RewardType.Gun = 2+REWARD_GUN_NUM = 25tồn tại nhưng không được chọn ở 1.1.59.) - Nếu save chưa nạp:
pendingReward = true, trao sau (0x2A6D3C4@0x2a6d3d8). placelưu ởPlayerPrefs["fc_link_place"], xoá sau khi trao; log AppsFlyerm_freecash_link.- Hằng độ bền:
INIT_MAX_RETRY 20,INIT_RETRY_DELAY 1 s,LINK_CHECK_TIMEOUT 10 s.
4.6 Súng thưởng quảng cáo weapon_02 — Gun_manager
CheckAdGunEvent() 0x2B0C74C // gọi từ Gun_Item_List.FuseItems (sau khi người chơi GHÉP súng)
if (AD_Gun_Event_Given) return
if (!ServerConfig.Weapon02AdOn) return // weapon_02_ad_onoff
if ( Has_Degree_Or_Higher(2) // đã có súng bậc >= 2 -> khỏi cần tặng
|| ( Has_Merged && AD_Gun_SlotIndex == -1
&& AddAdRewardGun(gunNum: 14, cooltimeSec: 1800f) != -1 ) )
AD_Gun_Event_Given = true // một-lần-cho-cả-đời-tài-khoản
AddAdRewardGun(0x2B0C340): đặt súng #14 vào ô trống đầu tiên, ghiAD_Gun_EndUtcTicks = UtcNow + 1800 s(30 phút đếm ngược).Update()(0x2B0C64C): mỗi 0.25 s (unscaled), nếu hết giờ và không đang xem quảng cáo →AdRewardGun_Expire()— súng biến mất.- Chạm vào súng đó (
Gun_Item.OnPointerUp,Gun_Item_List.EquipToSlot,FuseItems) →AdRewardGun_TryWatch()→ShowRewardAd(AD_Reward_Gun). - Xem xong →
AdRewardGun_Acquire()(0x2B0C508): xoá timer + slot marker ⇒ giữ súng vĩnh viễn, bậtNewGun_UI_ForceShow.
Đây là mẫu "dùng thử có hạn giờ" kinh điển: đưa món đồ vào tay người chơi, cho họ nếm sức mạnh, rồi đặt một quảng cáo giữa họ và việc giữ nó. Trigger là hành vi (vừa ghép súng lần đầu), không phải thời gian — nên nó rơi đúng lúc người chơi đang quan tâm tới súng.
5. Kinh tế Buff ×3
| Đại lượng | Giá trị | Nguồn |
|---|---|---|
| Một lần xem quảng cáo cộng | 300 s | AddBuff 0x2AE4510 |
| Trần tích luỹ | 900 s (BUFF_MAX) — xem 3 lần liên tiếp là đầy |
0x2AE4510 (fminf(x+300, 900)) |
| Loại | Money_Buff (0) và Power_Buff (1) — 2 đồng hồ độc lập (moneyBuffTime@0x28, powerBuffTime@0x2C) |
dump.cs:592890-592891 |
| Hệ số | ×3 (UpgradeRebirth_Multiplier cùng giá trị; hệ số buff áp trong chuỗi thu nhập) |
01-economy-money.md; Cat_manager.Balance_Reload được gọi ngay sau AddBuff |
| Vĩnh viễn qua IAP | IsPermanentBuff = Shop_manager.Is_Buff_Pack (static 0xB) ⇒ IsBuffActive(**mọi** type) = true |
0x2AE4A08, 0x2AE4AF8 |
| Hiển thị | Buff_Panel.Update 0x2AE4D4C: fill = time / 900, text "{0}:{1}" phút:giây; vĩnh viễn ⇒ text rỗng, fill = 1.0, ẩn nút xem QC |
0x2AE4D4C |
| Cache | AddBuff(Money_Buff) gọi Gamemanager.InvalidateMoneyCache() — buff nằm trong chuỗi nhân thu nhập có cache LRU |
0x2AE4510; xem 01-economy-money.md |
| Lưu | SaveData.MoneyBuffTime / PowerBuffTime (offset 0x2A8/0x2AC), nạp qua LoadBuffTimes 0x2AE4180 |
FABLE_ANALYSIS §B-6 |
Giá của cùng một thứ: 300 s ×3 tiền = 1 quảng cáo. Vĩnh viễn ×3 cả hai = buff_pack
210.000đ (phaseA/01_shop.png). Đó là toàn bộ luận điểm bán hàng — và nó được đặt ở dòng thứ hai
của cửa hàng, ngay dưới gói Auto Upgrade.
6. IAP — danh mục đầy đủ
6.1 34 product id → chỉ số offer → thứ được trao
Chỉ số lấy từ Shop_manager.ItemIdxFromProductId (0x2B2DE44), dùng cho log AppsFlyer m_offer_page.
| Idx | Product id | Loại | Được trao (hàm, RVA) | Giá quan sát (VND, live) |
|---|---|---|---|---|
| 1 | dia_0 |
consumable | ApplyDiaReward 0x2B32D90 → 240 dia (×2 lần đầu) |
52.000đ |
| 2 | dia_1 |
consumable | → 600 dia (×2 lần đầu) | 131.000đ |
| 3 | dia_2 |
consumable | → 1500 dia (×2 lần đầu) | 263.000đ |
| 4 | dia_3 |
consumable | → 3500 dia (×2 lần đầu) | 526.000đ |
| 5 | dia_4 |
consumable | → 8000 dia (×2 lần đầu) | 1.300.000đ |
| 6 | remove_forced_ads |
non-consumable | Is_Remove_AD_forced = true; mở DiaPig |
79,000đ |
| 7 | remove_all_ads |
non-consumable | Is_Remove_AD_All = true; mở DiaPig; tắt interstitial + banner, và rewarded ad tự trao thưởng |
263,000đ |
| 8 | autoupgrade_pack |
non-consumable | Is_AutoUpgrade_Pack = true + 500 dia |
211.000đ |
| 9 | buff_pack |
non-consumable | Is_Buff_Pack = true (×3 Power+Money vĩnh viễn) + 1000 dia + reload Cat_manager/Buff_manager |
210.000đ |
| 10 | dia_pack |
non-consumable | Is_Dia_Pack = true + 16000 dia |
1.300.000đ |
| 11 | mine_pack |
non-consumable | Is_Mine_Pack = true + 500 dia (gói duy nhất không có hiệu ứng dia bay — nó mua từ popup trong màn Mỏ, không có điểm xuất phát trên UI cửa hàng) |
158,000đ |
| 12 | vip_pass và vip_pass_2 |
non-consumable | ApplyVipPass 0x2B32C50: Mission_manager.is_Vip = true + 500 dia |
vip_pass 210,000đ; vip_pass_2 KHÔNG có trên store (xem ghi chú dưới bảng) |
| 13 | catgunner_core_100 |
consumable | ApplyCoreReward 0x2B304BC → Adventure_manager.Core_Get(Core_Type.ALL, 100) ⇒ Currency_Core[3] += 100 |
26,000đ |
| 14 | catgunner_core_500 |
consumable | → Currency_Core[3] += 500 |
105,000đ |
| 15 | catgunner_core_1050 |
consumable | → Currency_Core[3] += 1050 |
210,000đ |
| 16 | catgunner_core_3300 |
consumable | → Currency_Core[3] += 3300 |
631,000đ |
| 17 | catgunner_core_daily_750 |
consumable, nhịp ngày | → Currency_Core[3] += 750 + ghi LastDailyCorePurchase_dateTime |
105,000đ |
| 18 | catgunner_core_weekly_1750 |
consumable, nhịp tuần | → Currency_Core[3] += 1750 + LastWeeklyCorePurchase_dateTime |
210,000đ |
| 22 | catgunner_core_weekly_6000 |
consumable, nhịp tuần | → Currency_Core[3] += 6000 + LastWeekly6000CorePurchase_dateTime |
631,000đ |
| 19 | catgunner_luxury_pass |
non-consumable | ApplyLuxuryPass 0x2B32AA4: is_Vip = is_Luxury = true + 1000 dia + Exp 300 (3 cấp pass) |
394,000đ |
| 20 | catgunner_luxury_upgrade |
non-consumable | ApplyLuxuryUpgrade 0x2B328F8 — thân hàm giống hệt idx 19 |
210,000đ |
| 21 | catgunner_rebirth_single |
consumable | ApplyRebirthSingle 0x2B3280C → UpgradeRebirth_Start_By_IAP ⇒ thêm 1 lượt Rebirth ×3 trong ngày |
52,000đ |
| 23-27 | catgunner_stepup_0..4 |
consumable ×5 | ApplyStepUpReward 0x2B2E90C → StepUp.Gun_Rewards[i] = súng [23,28,33,39,44] + Dia_Rewards[i] = [0,200,400,600,1000] dia |
26,000 / 130,000 / 261,000 / 523,000 / 1,050,000đ |
| 28-30 | catgunner_dailygun_0..2 |
consumable ×3 | ApplyDailyGunReward 0x2B2EDB8 → DailyGun.ApplyReward(idx); nền súng GunNum_Base=[20,30,35], PetCoin [300,700,1500], giới hạn tuần MaxCounts=[2,2,1] |
130,000 / 523,000 / 915,000đ |
| 36 | catgunner_pluspack_time |
consumable | ApplyPlusPackReward 0x2B2EF0C → PlusPack.ApplyReward(Time), hạn 7 ngày |
104,000đ |
| 37 | catgunner_pluspack_reward |
consumable | → PlusPack.ApplyReward(Reward); ×2 Monster, ×2 Boss, ×1.5 Mine |
234,000đ |
| 38 | catgunner_pluspack_auto |
consumable | → PlusPack.ApplyReward(Auto); giảm giờ Adventure (PlusPack_Auto_Reduce_Hours = 16) |
156,000đ |
Chỉ số 31-35 bỏ trống — có thể là sản phẩm đã gỡ hoặc chừa chỗ. Đây là bằng chứng phủ định, không phải suy đoán:
ItemIdxFromProductIdkhông trả về giá trị nào trong khoảng đó.
Giá: 33/34 id đã có giá thật (đóng G162). Dump Shop_manager.Localized_Price_return (0x2B367D4)
read-only trên bản Play Store chính hãng: dict_count = 33, resolved = 33
([liveops catalog: iap_prices_store.json]). 8/8 giá trùng khớp ảnh chụp cửa hàng của
do store cấp — không phải hằng số trong build (§6.3).
vip_pass_2 KHÔNG phải sản phẩm Play — nó có mặt trong TryApplyPurchase/ItemIdxFromProductId
nhưng không nằm trong 33 id mà store trả giá ⇒ 34 id trong code, 33 id đang bán.
Nhiều khả năng là SKU cũ giữ lại để người đã mua vẫn khôi phục được. (Bằng chứng phủ định, không suy đoán
về lý do.) Tương tự, fish_free, mine_ticket, sp_iap, reward_hub, f_cash, banner_* không
xuất hiện trong dump giá — củng cố kết luận §2.2 rằng chúng là chuỗi place/khoá analytics.
Phân bố giá (33 SKU, VND): thấp nhất remove_forced_ads 79.000đ · cao nhất
dia_4 = dia_pack 1.300.000đ. Ba bậc giá lặp lại rõ rệt — 26.000 / 105.000 / 210.000 / 631.000
(Core) và 26.000 / 130.000 / 261.000 / 523.000 / 1.050.000 (StepUp) gần như nhân đôi mỗi bậc:
đây là một thang giá hình học ×2, không phải giá đặt tay từng món.
6.1b Số đo trên thiết bị — cả 34 SKU đã CHẠY THẬT (F024 NT4, 2026-09-12)
Bảng §6.1 ở trên đọc từ binary. Mục này là phép đo: từng product id được đẩy qua đúng đường của
game (Shop_manager.BeginPurchase → ServiceLocator.Purchases → quân xanh F024 → Granted →
TryApplyPurchase) trên Pixel 8 Pro, trạng thái đọc bằng save trước/sau từng giao dịch.
34/34 thành công, 0 exception, mọi con số ở §6.1 trúng. Công cụ: tools/iap_catalog_oracle.py;
bảng đầy đủ: [evidence codegen: SPEC-F024-T5-iap-catalog.md].
Bốn hình thái SKU — đây mới là thứ tái dùng được, bảng 34 dòng chỉ là dữ liệu của một game:
| Hình thái | Bán cái gì | SKU trong CatGunner | Dấu hiệu trong save |
|---|---|---|---|
| Tiền thuần | một con số | 5 × dia_*, 7 × Core |
ô tiền đổi, không có cờ nào bật |
| Quyền vĩnh viễn + quà kèm | một CỜ, quà chỉ là mồi | 6 non-consumable, VIP, Luxury | cờ false→true, tiền cộng một lần duy nhất |
| Quyền CÓ HẠN | một khoảng thời gian | 3 × PlusPack | chỉ một dấu thời gian, không tiền, không cờ |
| Hàng hoá + tiền phụ | một vật phẩm | 5 × StepUp, 3 × DailyGun | ô kho vật phẩm đổi + tiền phụ cộng |
Một SKU có thể lai hai hình thái: ba gói Core theo nhịp vừa cấp tiền vừa đóng dấu
Last*CorePurchase_Date để tính "đã tới lượt mua lại chưa".
Cái bẫy trung tâm của hình thái 2, đo được bằng hai vế đối xứng trong cùng một lượt chạy:
vip_pass_2mua khiis_Vipđã true ⇒ vẫn +500 dia.ApplyVipPasskhông có cổng "đã có thì thôi".autoupgrade_pack/mine_packmua khi cờ đã true ⇒ không đổi một bit. PhầnGet_Diacủa chúng nằm trongif (!<cờ>), còn<cờ> = truenằm ngoài.
Cùng mang tiếng "gói vĩnh viễn" nhưng hai nhóm xử quà kèm ngược nhau. Đây cũng là lý do
RestorePurchases chạy lại được bao nhiêu lần cũng an toàn với nhóm sau — và không an toàn với
VIP nếu ai đó nối nhầm khôi phục vào ApplyVipPass. Quy tắc framework: quyền và quà kèm phải
qua HAI cổng riêng — cổng "đặt quyền" idempotent, cổng "trao quà" một lần.
Năm số đo còn lại mà đọc code không cho ra (chi tiết G1789):
catgunner_rebirth_singlethi hành luôn một lượt Rebirth ngay lúc mua, không phát một lượt để dành: đo đượcUpgrade_Lv [100,100,99] → [0,0,0]và Ruby484,97 → 96484trong cùng giao dịch.- Bảy SKU Core đổ vào một ô duy nhất
Adventure_Currency_Core[3](Core_Type.ALL); ba ô theo vùng[0..2]không đụng ⇒ Core bán ra là tiền VẠN NĂNG. - Mua chồng PlusPack là mất tiền: ô ngày chỉ DỜI mốc (
03:17:19 → 04:51:44), không cộng dồn hạn. remove_forced_adsđánh thức heo đất ngay lúc mua —diaPig.LastTickTimenhảy trong cùng giao dịch, tức nhánhDiaPig.Init(null)có thật chứ không phải mã chết.StepUp_Start_Datedo lần mua bậc 0 đóng dấu — đồng hồ của cả chuỗi StepUp bắt đầu từ lần mua đầu, không phải từ lúc mở panel.
6.1a "Core" là tiền tệ gì (giải quyết phần lớn G163)
enum Core_Type { Forest = 0, Desert = 1, Tundra = 2, ALL = 3 } // dump.cs:591540
Adventure_manager.Currency_Core : int[] // instance +0x28
Adventure_manager.Core_Get(Core_Type type, int value) 0x2ADA8C8
=> Currency_Core[(int)type] += value; OnCoreChanged?.Invoke(); Alert_Reload()
Adventure_manager.Core_Lose / CoreCount_return — cùng chỉ mục
const Adventure_Reward_Core_perHour = 20 // kiếm được bằng cách đi Trip
const Adventure_Reward_Core_PetDegree = 0.1 // +0.1 Core mỗi bậc pet
⇒ Core là tiền tệ của Trip (Adventure), kiếm bằng cách cử pet đi chuyến (20/giờ), tiêu vào
cây Ability của Trip. Có 4 ô: 3 ô theo map (Forest / Desert / Tundra) + ô ALL (index 3).
Mọi IAP catgunner_core_* đều nạp vào ô ALL — tức loại Core dùng được cho mọi map.
Panel Trip đang chạy hiển thị đúng 4 chip tiền tệ: 3 viên màu (xanh lá / cam / xanh dương, khớp
thứ tự enum Forest/Desert/Tundra) + 1 ngôi sao tím "PrismCore"
HYPOTHESIS (mức trung bình): ngôi sao tím = ô ALL (index 3) = "PrismCore" — dựa trên
(a) Core_Type có đúng 4 giá trị, (b) panel có đúng 4 chip, (c) banner promo của panel mở
PrismCore_Purchase_popup (Main.unity:22690) và (d) scene có 7 object Prism_Pack, 3 trong đó
đánh dấu (Limited). Chưa xác nhận: chuỗi UI ↔ index chưa được đọc từ code
(Adventure_Panel dump.cs:592475, Core_sprite_return dump.cs:591675-591681) → xem §10.
6.2 Đường đi của một giao dịch
Đính chính 2026-09-12 (QD-61, G1768, F024 T5): tiêu đề cũ của mục này ghi "không có bước nào ở đây được tái hiện trong bản restore" — nay SAI. Hai đầu của chuỗi CHẠY THẬT và đo được:
BeginPurchase(đã dựng, chuyển hướng quaServiceLocator.Purchases) vàTryApplyPurchasecùng toàn bộ họApply*(dựng đủ, đã chạy 34/34 SKU, §6.1b). Phần còn chưa dựng là khúc GIỮA —OnPurchasePending→StartValidate→ValidateAndComplete→CompletePurchaseSuccess— vì nó cần mô hìnhPendingOrder/receipt của store thật (R1-out, G218/G1691).
<nút UI> -> Shop_manager.Purchase*(type, place) // ghi _pendingPlace + _pendingIdx
-> BeginPurchase(productId) 0x2B2E7D8 // ShowPurchasingPanel(); StoreController.PurchaseProduct
-> [Unity IAP + store]
-> OnPurchasePending(order) 0x2B31758 // GetTxnId, AlreadyOwned, _inflightTxns
-> StartValidate(order, product) 0x2B32144
-> ValidateAndComplete(...) 0x2B33DAC // POST receipt -> Cloud Function verify*
-> CompletePurchaseSuccess(...) 0x2B34164
-> TryApplyPurchase(id, type) 0x2B32204 // <== ĐIỂM TRAO DUY NHẤT
-> MarkProcessed(txnId); SaveSystem.Save(); LogFacebookPurchase / LogFirebasePurchase
TryApplyPurchase (0x2B32204) là một chuỗi so khớp id: 6 non-consumable → ApplyNonConsumable;
catgunner_rebirth_single; catgunner_luxury_upgrade; catgunner_luxury_pass;
vip_pass/vip_pass_2; TryGetCoreTypeFromId; rồi 4 nhánh theo prefix
(dia_, catgunner_stepup_, catgunner_dailygun_, catgunner_pluspack_);
không khớp gì → LogError("[IAP] Unknown product id '{0}' (type={1}) → apply 실패 처리") và trả false.
Ba mẫu đáng học từ đoạn này:
1. Trao hàng là idempotent và hoãn được. Nếu manager phụ trách còn null (chưa xong Awake),
hàm trả false và log "확정 보류, 다음 실행 재시도" — giao dịch không được finalize, store sẽ
phát lại ở lần chạy sau. Người chơi không bao giờ mất hàng vì thứ tự khởi tạo.
2. Một điểm trao duy nhất cho cả mua mới, khôi phục (RestorePurchases 0x2B35AD0) và đơn hoãn
(_deferredOrders).
3. Prefix hoá sản phẩm có chỉ số (catgunner_stepup_0..4) — thêm bậc mới không phải sửa code
dispatch, chỉ sửa mảng phần thưởng.
6.3 Định giá — cái game LÀM và cái game KHÔNG làm
- Game không hardcode giá.
Localized_Price_return(...)(6 overload,0x2B367D4…) đọc giá từProduct.metadatacủa store, rồiFormatPrice_Display(price, iso)(0x2B368B0) định dạng theoCurrencyFormat/ZeroDecimalCurrency(2 bảng static trong.cctor0x2B36F9C). ⇒ Mọi con số đồng trong tài liệu này là giá cửa hàng quan sát được ở VN tại thời điểm chụp, KHÔNG phải hằng số trong build. TryGetPrice(productId, out decimal, out string currency)(0x2B36D00) cho phép UI hiển thị giá chuẩn từng thị trường.- Bảng giá thật (VN, 33 SKU) nằm ở §6.1 — dump từ chính
Localized_Price_returntrên bản store chính hãng ([liveops catalog: iap_prices_store.json]). Đọc nó như một mẫu định giá, không phải như hằng số của game: cùng build ở thị trường khác sẽ hiện con số khác. ZeroDecimalCurrencylà lý do VND hiện52,000đchứ không phải52,000.00đ. Nội dung hai bảng (đọc từ.cctor0x2B36F9C, G399):CurrencyFormat= 23 mục tiền tệ → chuỗi định dạng (+0x68);ZeroDecimalCurrency= {KRW, JPY, VND, IDR, CLP, HUF} (+0x70) — sáu tiền tệ không có phần thập phân. Cùng.cctorcấpProcessedTxnIds(+0x40) vàPurchaseHistory(+0x48) — thiếu chúng là autosave trên save mới ném mỗi 10 s (G399).
7. Kích hoạt offer & thời điểm
7.0 BA LỚP CỔNG xếp chồng — tutorial → thang stage → segment
LỚP 1 — TUTORIAL (Tutorial_manager, dump.cs:602669)
Save mới: CHỈ SpeedRun_Btn + Setting_Btn active.
Shop / Gun / Skin / Mission / DailyCheck / Auto / Eco / DiaPig / Buff đều tắt,
và KHÔNG do Contents_Lock_Reload bật -> Tutorial_manager mở dần.
TutorialStep: Upgrade 0 -> Challenge 1 -> GunOpen 2 -> GunBuy 3 -> GunEquip 4 -> Done 5
Scene: DisableOnTutorial[] = 32 object (Main.unity:20342185+)
v (người chơi làm xong tutorial)
LỚP 2 — THANG STAGE (Tree_manager.Contents_Lock_Reload 0x2B4441C)
Stage_Value_Max >= 20/30/50/90/110/110/340 -> Rebirth/Mine/Fish/Boss/Pet/Hunt/Trip
v (nội dung mở dần)
LỚP 3 — SEGMENT (ServerConfig_manager, 15 số từ server)
MaxStageDisplay >= offerwall_stage_min / inter_stage_min / freecash_stage_min
+ trần ngày theo place + cooldown -> bề mặt QUẢNG CÁO bật/tắt
Hệ quả cho phễu kiếm tiền: một người chơi mới không nhìn thấy cả cửa hàng cho tới khi
tutorial mở Shop_Btn. Nghĩa là thứ tự tiếp xúc là gameplay trước, cửa hàng sau, quảng cáo sau nữa
(bề mặt IAA sớm nhất là OfferWall ở Stage 10 — tức khoảng 100 màn sau khi bắt đầu).
7.1 Công tắc tổng: segment
boot -> ServerConfig_manager.Awake 0x2AC8154
-> LoadFromSaveData 0x2AC1FA8 (dùng cache) -> Tree_manager.Contents_Lock_Reload
-> FetchRoutine 0x2AC9864 : POST {BASE_URL}/[endpoint], TIMEOUT_SEC 2,
FAIL_RETRY_DELAY 60, FAIL_RETRY_MAX 1, POST 1 lần/ngày (NeedPost/TodayKey),
FIRST_POST_WAIT_CONVERSION_SEC 10 (chờ AppsFlyer trả conversion data trước khi POST lần đầu)
-> nếu chưa có phản hồi: EmbeddedDefaults(segment) 0x2AC7B20
| segment | Hệ quả |
|---|---|
| 1 | Bật IAA: FreeCash stage 15-35, OfferWall từ stage 10, interstitial từ stage 12, cap Rebirth 6/ngày, Mine 2/ngày, các place khác 0, cap toàn cục 99999, ad-gun bật |
| 0 / ≠1 | Tắt sạch IAA (mọi ngưỡng 99999, mọi cap 0, ad-gun tắt) — đây là mặc định và là điều production đang trả về cho user thật (rule_version 41, reason ok) |
⇒ Bài học vận hành lớn nhất của tài liệu này: một game có thể ship toàn bộ hạ tầng IAA và vẫn
chạy 0 quảng cáo cho một phân khúc, chỉ bằng 15 con số từ server. Phân khúc quyết định bởi
nguồn cài đặt / quốc gia / lịch sử chi tiêu (ConfigReq), không phải bởi build.
7.2 Offer gắn với TIẾN TRÌNH (đọc Stage_Value_Max, xem UNLOCK_GATES.md §1)
stageDisplay = Stage_Value_Max / 10 + 1
| Bề mặt | Điều kiện | RVA |
|---|---|---|
| Interstitial | stageDisplay >= inter_stage_min (seg 1: 12) |
0x2A5CD34 |
| OfferWall HUD | stageDisplay >= offerwall_stage_min (seg 1: 10) |
0x2B2B20C |
| FreeCash HUD | freecash_stage_min <= stageDisplay <= freecash_stage_max_hud (seg 1: 15 → 35) |
0x2A6CF08 + 0x2A6D1F8 |
| FreeCash banner (shop) | chỉ >= freecash_stage_min (không trần) |
0x2A6D644 |
Popup VIP trong panel Mùa mới (VIP_Purchase_popup) |
NewSeason_Btn hiện khi Reward_Lv > 24; popup SetActive(!is_Vip) trong OnEnable (Reload TRƯỚC, OnInAppPurchaseChanged += Reload SAU ⇒ đã VIP thì rò một subscriber trên popup ẩn — hành vi SOT); bấm ⇒ Purchase_VIP_Banner(type, "banner") |
0x2B3E6CC/0x2B3E778; đo Pixel lô 13 (docs/functions/popup-mua-vip.md) |
Setting › Restore (Setting.Restore → Shop_manager.RestorePurchases(onSuccess,onFail)) |
không StoreController ⇒ onFail NGAY ⇒ Alert Alarm/RestoreFail; có store ⇒ _isRestoring=true, OnPurchasesFetched→duyệt receipt→onSuccess, OnStoreDisconnected→onFail, Cleanup gỡ handler; closure có cờ completed chống gọi hai lần |
0x2B35AD0 (640 B) + <>c__DisplayClass136_0; đo Pixel lô 14a (docs/functions/setting-restore-cloud-update-review.md) |
Setting › Đổi mã (Redeem_UI → RedeemCode_manager) |
KHÔNG có máy chủ: từ điển 3 hash SHA-256(code.Trim().ToLowerInvariant()) → 500 dia, expire 0; đã dùng ghi UsedCodes_Log (save); thứ tự kiểm Empty→Invalid→AlreadyUsed→Expired; thành công lưu ngay + log AppsFlyer |
.cctor 0x2ACC228, Redeem 0x2ACBD18; oracle Hash = shasum (docs/functions/doi-ma-lien-he-biet-danh.md) |
Setting › Liên hệ (Setting.SendEmail) |
mailto: tới <địa chỉ hỗ trợ NPH, literal 0x5EA7E48>, cc <địa chỉ cc NPH, literal 0x5E87B98>, subject "Cat Gunner Feedback (Email_Body_str (scene) với save nén SaveCompressor.PackForMail (GZ1: + base64url(GZip)) — không SDK |
0x2ACE2F0 (1276 B); Pixel: Gmail compose mở, body trống (G1705) |
| PlusPack banner | Banner_Stage_Min = 190 (⇒ Stage 20-1) và gói đó chưa active |
dump.cs const; PlusPack.Banner_ShouldShow 0x2B2C778 |
7.3 Offer gắn với HÀNH VI (không phải stage, không phải giờ)
| Bề mặt | Trigger | RVA |
|---|---|---|
Ad-gun weapon_02 |
ghép súng (Gun_Item_List.FuseItems) lần đầu, khi chưa có súng bậc ≥2 |
0x2B0C74C |
| StepUp banner | !Is_AllPurchased && !Is_Expired && Gun_manager.Has_Degree_Or_Higher(2) (khớp Banner_Unlock_Degree = 2) |
0x2B3A670 |
| StepUp đồng hồ 7 ngày | StepUp_Start() chỉ chạy lần đầu banner/popup được reload — đồng hồ bắt đầu khi người chơi nhìn thấy, không phải khi cài |
0x2B3A328 ← Hud_Reload/Popup_Reload |
| Interstitial sau Rebirth | chỉ khi lần rebirth đó không được boost và popup review không mở | Rebirth() @0x2b22d9c |
| Rewarded "bonus" | mở panel kết quả Mine/Boss/Hunt/Challenge (nút Bonus_Btn_obj, SweepBonus_Btn_obj); nếu Is_RewardBonus_Auto thì hiện SweepNoAds_Btn_obj thay thế |
dump.cs Mine_manager fields |
7.4 Tương tác popup Review ↔ interstitial (mẫu đáng chép)
Rebirth_manager.Rebirth() 0x2B22AE0 — đuôi hàm:
bool reviewOpened = Review_manager.instance.Review_Panel_Open(); // @0x2b22d24
Mission_manager.instance.MissionValue_Increase(Mission_Type.Rebirth);
if (is_UpgradeRebirth) { if (!is_UpgradeRebirth_IAP) { UpgradeRebirth_UseCount++;
UpgradeRebirth_LastDate = today; }
is_UpgradeRebirth = is_UpgradeRebirth_IAP = false; }
SaveSystem.Save();
if ( !(boosted || reviewOpened) && Admob.instance != null ) // @0x2b22d9c
Admob.instance.ShowPlacementInterstitial(Inter_Place.Rebirth, null);
Review_Panel_Open() (0x2AC0DE8) trả true đúng một lần trong đời tài khoản
(is_reviewed lưu trong SaveData) và chỉ có một người gọi: Rebirth().
⇒ Ba luật ưu tiên popup được mã hoá trong đúng một dòng if:
1. Người vừa trả tiền / xem quảng cáo để boost ⇒ không ăn thêm interstitial.
2. Xin đánh giá app thắng interstitial (chỉ có một cơ hội, và nó rơi vào lần rebirth đầu tiên —
đúng khoảnh khắc người chơi vừa cảm thấy phần thưởng lớn nhất của game).
3. Còn lại thì interstitial, và vẫn phải qua 7 cổng ở §4.2.
7.5 Offer theo LỊCH (cố định trong binary, không remote)
| Hệ | Nhịp | Hằng | RVA |
|---|---|---|---|
| StepUp | 7 ngày kể từ lần hiện đầu, 5 bậc, mua tuần tự | StepCount 5, Duration_Days 7 |
Is_Expired 0x2B3A4E0, Current_Purchasable_Step 0x2B3A630 |
| DailyGun | xoay theo tuần, WeekEpoch = 2024-01-01, 3 món/tuần, giới hạn [2,2,1] lượt |
ProductCount 3 |
WeekStart_return 0x2B25A38, WeekIndex_return 0x2B25F34, GunNum_return 0x2B26080 |
| PlusPack | 7 ngày kể từ lúc mua, 3 loại độc lập | Duration_Days 7 |
Is_Active 0x2B2B5EC, Remain_Duration 0x2B2BEE8 |
PlusPack Auto — quyền bật công tắc hợp-nhất-tự-động |
hai cổng ĐỘC LẬP: mốc tiến trình Stage_Value_Max > 189 cho nút hiện, gói Auto còn hạn cho nút bật được; hết hạn thì cờ is_AutoMerge bị dọn lười ở lần *_UI_Reload kế tiếp, không polling |
đo trên máy 2026-09-07 (F008 T8) | Pet_manager.AutoMerge_UI_Reload 0x2B17A90, WeeklyShop.Is_VIP_Unlocked 0x2B3EA58; docs/functions/cong-tac-hop-nhat-tu-dong.md |
| Core daily/weekly | mốc LastDaily…/LastWeekly… |
– | Is_Core_Special_Available 0x2B24F9C |
| DiaPig | tick 60 s, bù tối đa 3 tick khi quay lại | TICK_SECONDS 60, CatchUpMaxPending 3 |
LIVEOPS_SURFACE §6 |
| Trip — vé | Ticket_Max = 20, nạp 1 vé / 4 giờ (Ticket_Charge_Interval_Hours = 4) |
dump.cs:591608+ |
Ticket_Refresh |
| DailyCheck | mốc ngày, chuỗi 7 ngày lặp | SyncWaitTimeout 4; bảng dia 70/200/400/600/800/1000/1500 — đã dựng lại hoàn chỉnh, xem §7.6 (G378) |
LIVEOPS_SURFACE §6; G378 |
| UpgradeRebirth | 1 lượt ad + 1 lượt IAP mỗi ngày, reset bằng chuỗi ngày | UpgradeRebirth_DailyLimit 1, ..._IAP_DailyLimit 1 |
UpgradeRebirth_DayCheck 0x2B23260, Able_return 0x2B232B0, IAP_Able_return 0x2B2339C |
| Interstitial | cap ngày theo place + cap toàn cục, reset bằng PlayerPrefs["inter_place_date"] |
ServerConfig | 0x2A5C7F0 |
7.6 Điểm danh 7 ngày (DailyCheck) — hệ retention, đọc trọn từ binary (G378)
Mục tiêu học R9: đây là vòng lặp quay lại mỗi ngày của game — thứ nuôi số ngày hoạt động (retention) mà
mọi offer khác đứng trên. 13 method đã dựng lại (OnSynced 0x2A6B470, OnCheckSettled 0x2A6B588,
RefreshOrShow 0x2A6B4DC, is_DateChange 0x2A6B90C, is_GetAble 0x2A6BC64, CheckStatus_return
0x2A6B9F0, UI_reload 0x2A6B58C, NewUser_Reload 0x2A6B828, Open_Btn 0x2A6B720, Test_Btn
0x2A6B678, Get_Reward 0x2A6BC7C, DailyReward_Get 0x2A6BA9C, Show_WhenReady MoveNext 0x2A6BD08).
| Claim | Mức bằng chứng | Nguồn / provenance |
|---|---|---|
Bảng thưởng DiaReward_list (scene) = 70 / 200 / 400 / 600 / 800 / 1000 / 1500 dia cho ngày 1–7 — tổng 4 570 dia một chu kỳ, tăng dần với mốc nhảy ở ngày 7 |
scene-YAML | G378 |
Điều kiện sang ngày mới = kiểm đồng hồ tin cậy TRƯỚC, rồi mới so ngày: is_DateChange = CanRolloverDaily("daily", maxPending 1) && LastCheck_Date.Date < Today — không có giờ tin cậy thì không có "ngày mới", bất kể đồng hồ máy |
disasm-proven | is_DateChange 0x2A6B90C; G378 |
Chuỗi 7 ngày LẶP VÔ HẠN: CheckNum == 7 && đổi ngày ⇒ CheckNum = 0 — không có "tháng", không reset về 0 khi bỏ lỡ ngày (chỉ chờ ngày đổi) |
disasm-proven | G378 |
Người dùng mới được lùi ngày một hôm (NewUser_Reload) ⇒ ngày 1 nhận được ngay lần mở đầu tiên — phần thưởng đầu tiên không bắt chờ |
disasm-proven | NewUser_Reload 0x2A6B828; G378 |
Nhận thưởng kiểm lại điều kiện (is_GetAble) và từ chối kèm sfx Dont nếu không còn đủ; hợp lệ ⇒ Time_manager.RecordGain("daily") ghi sổ cái TRƯỚC khi cấp dia; rồi CheckNum++ và TotalCheckNum++ (cặp), LastCheck_Date = Today, Dia_Attraction.Dia_Spawn (dia bay về ví), sfx Purchase |
disasm-proven | Get_Reward 0x2A6BC7C, DailyReward_Get 0x2A6BA9C; G378 |
Mở panel ép kiểm lại giờ (Open_Btn → Time_Hacker_ForceCheck) — người chơi tự kích hoạt lần kiểm chống chỉnh giờ mỗi khi vào xem điểm danh |
disasm-proven | Open_Btn 0x2A6B720; G378 |
Nút lùi ngày debug (Test_Btn) gate bằng Debug.isDebugBuild ⇒ tắt trong bản phát hành (dùng được trên bản học vì debuggable) |
disasm-proven | Test_Btn 0x2A6B678; G378 |
Show_WhenReady: yield null → WaitForCheck(4 s) → Try_Show (chờ đồng bộ giờ trước khi hiện panel ngày mới) |
disasm-proven | MoveNext 0x2A6BD08; G359, G378 |
TotalCheckNum (đếm suốt đời) tách khỏi CheckNum (vị trí trong chu kỳ) — số liệu retention và trạng thái UI không dùng chung một biến |
disasm-proven | G378 |
Bốn quyết định đáng chép: (1) thưởng đầu tiên ngay lập tức (lùi ngày người mới) — chi phí bằng 0, gỡ rào cản
đầu tiên; (2) chuỗi lặp, không phạt — bỏ lỡ một ngày không mất tiến độ, chỉ chờ; (3) đồng hồ tin cậy là điều
kiện tiên quyết, không phải kiểm tra sau; (4) ghi sổ trước khi cấp — mọi thưởng theo thời gian đi qua cùng
một sổ cái (RecordGain, doc 07 §2.6) nên có thể đối chiếu ngược.
8. Mô hình hành vi — phễu kiếm tiền
TUTORIAL (lớp 1)
────────────────
save mới ──> chỉ SpeedRun_Btn + Setting_Btn ──> Tutorial_manager mở dần Shop/Gun/Skin/Mission/
DailyCheck/Auto/Eco/DiaPig/Buff ──> CHỈ TỪ ĐÂY cửa hàng mới tồn tại với người chơi
TIẾN TRÌNH (lớp 2) ĐIỀU KIỆN LỘ DIỆN (lớp 3) OFFER PHẦN THƯỞNG TÁC ĐỘNG KINH TẾ
───────── ───────────────── ───── ─────────── ────────────────
Stage_Value_Max ──┬──> stageDisplay = SVM/10 + 1
│
├──> >= inter_stage_min ────────────> interstitial ───────────> (không thưởng) ────> 0 (chỉ doanh thu)
│ && cooldown ok && cap ngày ok
│ && !boosted && !justWatchedRewarded
│
├──> >= offerwall_stage_min ───────> OfferWall (Tapjoy) ──────> Dia ───────────────> mua shop / gacha
│ && Tapjoy connected
│
├──> [freecash_min, freecash_max] ─> FreeCash link ───────────> RewardAdRemoved ───> MỌI rewarded
│ && Almedia ∈ {Eligible, Linked} tự trao ⇒ buff/bonus
│ (seg 1: Stage 15→35, CÓ TRẦN TRÊN) không còn ma sát
├──> > 189 ─────────────────────────> WeeklyShop / PlusPack banner
└──> > 988 ─────────────────────────> catgunner_rebirth_single
HÀNH VI CHUỖI THU NHẬP (doc 01)
─────── ───────────────────────
ghép súng lần đầu ─────> ad-gun #14, 30 phút ──> rewarded ──> giữ súng ────────────────────────────> Damage ↑ ⇒ stage nhanh hơn
bấm nút Buff ──────────> rewarded ────────────> +300 s (trần 900) ─────────────────────────────────> ×3 Money hoặc ×3 Power
(InvalidateMoneyCache)
xong Mine/Boss/Hunt ───> rewarded "Bonus" ────> nhân thưởng màn ───────────────────────────────────> Money / PetCoin / Dia
nhấn Rebirth ──────────> popup "xem QC để ×3" ─> is_UpgradeRebirth ────────────────────────────────> Ruby ×3 (doc 09 §4)
(1 lượt/ngày)
IAP
───
buff_pack ─────────────> IsPermanentBuff ─────────────────────────────────────────────────────────> ×3 Money & Power VĨNH VIỄN
remove_all_ads ────────> Is_Remove_AD_All ────────────────────────────────────────────────────────> tắt inter+banner,
rewarded tự trao,
DiaPig tick +1
autoupgrade_pack ──────> Is_AutoUpgrade_Pack + 500 dia ───────────────────────────────────────────> tự mua upgrade
dia_* / core_* ────────> Dia / Core ──────────────────────────────────────────────────────────────> gacha, relic, adventure
rebirth_single ────────> +1 lượt Rebirth ×3/ngày (stage >= 100) ──────────────────────────────────> Ruby ×3 lần thứ hai
OfferWall (Stage 10 →, vĩnh viễn) · Interstitial (Stage 12 →, chỉ Rebirth 6/ngày + Mine 2/ngày) ·
FreeCash (Stage 15 → 35, rồi tự ẩn). Challenge/Boss/Hunt bị trần-0 chặn tuyệt đối.
Năm quan sát về hình dạng của phễu này:
- Interstitial là lớp duy nhất không trao gì cho người chơi — nên nó cũng là lớp bị gate nặng nhất: 7 điều kiện, cap theo từng place, cap toàn cục, cooldown, và 2 lần "miễn" (vừa xem rewarded, vừa được boost).
- Mọi thứ IAA trao đều là thứ IAP bán ở dạng vĩnh viễn. Không có phần thưởng quảng cáo nào không có bản IAP tương ứng. Quảng cáo là bản dùng thử của cửa hàng.
- FreeCash là một "IAP giá 0đ": nó cấp đúng cái mà
remove_all_adscấp cho tầng rewarded (Is_RewardBonus_Autogộp cả hai vào một vị ngữ,0x2B2D510). Người chơi trả bằng dữ liệu liên kết tài khoản thay vì tiền. - Cửa sổ lộ diện có cả trần trên (
freecash_stage_max_hud). Sau stage 35 (segment 1) HUD FreeCash tự biến mất — đo sống: 349 ⇒ hiện, 350 ⇒ ẩn. Offer chuyển đổi kém ở late-game thì bị thu hồi, không để rác trên màn hình. - Ngân sách interstitial được dồn vào đúng 2 khoảnh khắc "vừa được thưởng lớn" (Rebirth, Mine),
và bị tắt hẳn ở 3 chế độ còn lại bằng cách đặt trần/ngày = 0 — chứ không phải bằng một cờ
bật/tắt riêng. Một con số
0trong config làm việc của một feature flag.
9. Framework takeaway — tầng placement/offer/segment tổng quát
9.1 Bốn khái niệm, tách bạch
// 1) PLACEMENT — "ở đâu, khi nào có thể hiện"
interface IPlacement {
string Id { get; } // "inter.rebirth", "rewarded.money_buff"
bool CanShow(IGateContext ctx); // chuỗi gate, fail-closed
void Show(Action onClosed); // KHÔNG BAO GIỜ chặn: not-able -> onClosed() ngay
}
// 2) GATE — mỗi gate là một vị ngữ thuần, xếp theo thứ tự RẺ→ĐẮT, log lý do từ chối
interface IGate { bool Pass(IGateContext ctx, out string reason); }
// BuildFlagGate -> hằng build (is_AD_Disable_Version)
// EntitlementGate-> người chơi đã mua quyền không thấy (remove_ads)
// ReentrancyGate -> đang hiện / đang chạy
// ProgressGate -> progressMetric >= remote.minThreshold (và <= maxThreshold nếu có trần)
// CooldownGate -> now - lastShownUtc >= remote.cooldownSec
// PerPlaceCapGate-> counter[place] < remote.dailyCap[place]
// GlobalCapGate -> counter[global] < remote.dailyCapWhole
// CourtesyGate -> !justRewarded && !justPaid <-- luật "không phạt người vừa trả"
// 3) OFFER — "bán cái gì, khi nào bật, hết hạn khi nào"
interface IOffer {
bool IsActive(DateTime now, IPlayerState s); // lịch + tiến trình + hành vi
IReadOnlyList<Reward> Preview { get; } // để UI vẽ trước khi mua
}
// 4) SEGMENT — công tắc tổng, do server cấp, có bản nhúng fail-safe
interface IRemoteAdConfig { // 15 số, KHÔNG có số kinh tế
int Segment { get; } string RuleVersion { get; }
int MinStage(string placementId); int MaxStage(string placementId);
int CooldownSec { get; } int DailyCap(string placementId); int DailyCapGlobal { get; }
bool FeatureOn(string featureId);
}
9.2 Bảy quy tắc rút ra từ SOT (mỗi quy tắc là một quyết định thật trong libil2cpp.so)
- Mặc định nhúng phải là "tắt", không phải "bật".
EmbeddedDefaults(segment != 1)=99999/0. Server chết ⇒ người chơi thấy ít quảng cáo hơn, không phải nhiều hơn. (0x2AC7B20) - Remote config chỉ được chứa số PLACEMENT, không chứa số KINH TẾ. 15/15 field của
ConfigResplà ad-placement — đã kiểm chứng bằng payload production thật. Cân bằng nằm trong build ⇒ mọi người chơi cùng một game. (PROBE_RESULT.md §4) - Bộ đếm quảng cáo tách khỏi save game.
PlayerPrefschointer_place_cnt_*,ow_pending_gem,fc_link_place;SaveDatacho tiến trình. Reset save không mở lại quota quảng cáo, và ngược lại. - Không bao giờ chặn luồng game vì quảng cáo.
ShowPlacementInterstitialgọionClosed()ngay khi gate fail;PlacementInterCocó timeout từng lớp (interLoadingSeconds,interLoadMaxAttempts,interLoadWaitPerAttempt,LOAD_HARD_RESET_SEC,adLoadTimeout). - Entitlement chảy ngược vào tầng quảng cáo qua MỘT vị ngữ.
Is_RewardBonus_Autogộp "đã mua" và "đã liên kết FreeCash" — thêm nguồn miễn quảng cáo thứ ba chỉ cần sửa một hàm. - Điểm trao thưởng duy nhất, idempotent, hoãn được. Cả rewarded (
Reward_Get) và IAP (TryApplyPurchase) đều hội tụ về một hàm; nếu hệ thống đích chưa sẵn sàng thì không finalize và thử lại lần chạy sau. - Lịch sự với người vừa trả.
!_bonusAdWatched,!Is_RewardBonus_Auto,!boosted,!reviewOpened— bốn lần "miễn" nằm rải trong code, tất cả cùng một tinh thần. - Xếp CHỒNG cổng, đừng gộp. Ba lớp độc lập — tutorial (onboarding), thang stage (nội dung), segment (kiếm tiền) — mỗi lớp một chủ sở hữu, một vòng đời, một cách đổi. Người chơi mới không
- Tắt một placement bằng SỐ, không bằng cờ.
inter_daily_cnt_challenge = 0tắt hẳn placement Challenge mà không cần thêm một trường boolean nào. Một trục số duy nhất (0 … 99999) vừa là công tắc, vừa là van tiết lưu — và cùng một dòng code (cnt >= limit) phục vụ cả hai. - Bề mặt kém hiệu quả phải TỰ THU HỒI.
freecash_stage_max_hudcho offer một hạn dùng theo tiến trình. Rất ít game làm điều này; hầu hết chỉ biết bật. - Cho không trước, gắn quảng cáo sau — trên CÙNG một bộ đếm.
Fish_Gacha: quota 5/ngày với 3 lượt đầu không quảng cáo (§4.1b). Hai hằng số (max,noAd) đọc một biến duy nhất, nên tỉ lệ cho-không/gắn-quảng-cáo là một tham số chỉnh được, không phải một nhánh code. - Chỉ báo quảng cáo hiển thị theo DẢI, không theo ngưỡng.
(uint)(used − free) < adSlots. Ngưỡng để lại một biểu tượng chết sau khi hết quota — nút hứa quảng cáo, trả về lỗi.
9.3 Ranh giới fail-closed cho bản restore (R1)
// Assets/Scripts/Services/IAdService.cs
public enum AdResult { Failed, Dismissed, Rewarded }
public interface IAdService {
bool IsRewardedReady { get; }
void ShowRewarded(string placementId, Action<AdResult> done);
void ShowInterstitial(string placementId, Action done);
void ShowBanner(); void HideBanner();
}
public sealed class NullAdService : IAdService { // BINDING MẶC ĐỊNH
public bool IsRewardedReady => false;
public void ShowRewarded(string p, Action<AdResult> d) { Debug.Log($"[NullAd] {p}");
d?.Invoke(AdResult.Failed); } // KHÔNG Rewarded
public void ShowInterstitial(string p, Action d) => d?.Invoke(); // no-op, không chặn
public void ShowBanner() { } public void HideBanner() { }
}
public interface IPurchaseService {
bool IsOwned(string productId); // luôn false ở Null
void Purchase(string productId, string place, Action<bool> done); // luôn done(false)
}
Với IRemoteConfigService, bản Null phải trả đúng profile "tắt" — không phải null, không phải
throw: MinStage(*) = 99999, DailyCap(*) = 0, FeatureOn(*) = false. Đó chính là
(EmbeddedDefaults(1) trả đúng profile bật) — nên cả hai nhánh đều là hành vi đã kiểm chứng.
Bất biến bắt buộc: NullAdService không bao giờ trả AdResult.Rewarded;
NullPurchaseService.IsOwned không bao giờ trả true; NullRemoteConfig không bao giờ trả
giá trị bật quảng cáo — nó trả bản nhúng "tắt" (99999/0), đúng như SOT làm cho segment ≠ 1.
Nhờ đó bản restore chạy được toàn bộ luồng UI monetization mà không cấp một entitlement nào.
10. UNKNOWN / bằng chứng phủ định — và probe phải bắt gì để đóng
| Điều chưa biết | Trạng thái | Bắt gì để đóng (probe / device slot) |
|---|---|---|
| Thực thi cooldown / trần LÚC HIỆN quảng cáo | UNKNOWN — lý do chính xác: ADX không bao giờ có fill trên bản sideload (_sdkReady = 1, _interLoading = 1, _interShowing = 0, IsInterstitialReady = 0 suốt phiên) ⇒ không quảng cáo nào được hiện, Inter_CountUp 0x2A5D644 chưa bao giờ chạy, ShowRewardAd 0x2A59294 / ShowPlacementInterstitial 0x2A5D508 được hook nhưng không bị gọi |
Cần một môi trường có fill quảng cáo thật (bản có bundle_id được mediation nhận). Sau đó: đóng panel Mine 3 lần liên tiếp và đọc PlayerPrefs["inter_place_cnt_1"] + ["inter_place_cnt_whole"] + _lastInterShownUtc sau mỗi lần; lặp qua nửa đêm để kiểm Inter_DayCheck |
| Lịch "Ngày mở" của map Trip nằm ở đâu trong code | ✅ ĐÓNG (G1829, 2026-09-12) — không cần tìm literal: Adventure_manager.is_OpenDay_return(Map_Type, DayOfWeek) (0x2ADC890) là static 2 đối số, gọi thẳng trên .sot cho cả bảng: Forest CN·T2·T5 · Desert CN·T3·T6 · Tundra CN·T4·T7 (mask 19/37/73). Bản dựng lại khớp 21/21 ô. ~~UNKNOWN~~ — chỉ quan sát được trên UI (Forest T2/T5/CN · Desert T3/T6/CN · Glacier T4/T7/CN); không tìm thấy literal lịch trong stringliteral.json |
Decompile Adventure_Map_item (dump.cs:592454) / Adventure_Map_info_item (:592400) / Adventure_Panel (:592475); tìm DateTime.DayOfWeek |
Tutorial_manager mở nút nào ở bước nào |
✅ ĐÓNG (G375, G385): cổng vào = !_cleared && Lv(Power) < 1; BeginTutorial tắt bộ 32 DisableOnTutorial (= hàng 27 nút HUD); máy trạng thái Upgrade → Challenge → GunOpen → GunBuy → GunEquip → Done với coroutine kiểm điều kiện (MoneyCheckLoop đọc Lv(Power) mỗi 0.2 s, CheckSpeedRunClear, CheckGunSlotActive/Purchased/Equipped); CompleteTutorial bật lại cả hàng. Bước GunBuy nạp dia theo kịch bản (Dia ≤ 99 ⇒ Get_Dia(100 − Dia)) để lần mua đầu chắc chắn mua được — không phải đường kiếm tiền |
doc 07 §2.8 |
Chuỗi UI ↔ chỉ mục Currency_Core (ngôi sao tím = Core_Type.ALL index 3 = "PrismCore"?) |
HYPOTHESIS — mạnh thêm sau G1864: 7 nút Prism_Pack đều là IAP, và 7 SKU catgunner_core_* đều nạp vào ô ALL (index 3) ⇒ "PrismCore" nhiều khả năng đúng là ô đó. |
— 4 giá trị enum ↔ 4 chip trên panel, banner mở PrismCore_Purchase_popup |
Prism_Pack ×7 trong scene (3 bản (Limited)) — là IAP hay mua bằng Core? |
✅ ĐÓNG — LÀ IAP (G1864, 2026-09-13). Kết luận cũ ở ô này SAI, giữ lại để thấy vì sao. Đo trực tiếp trên binary GỐC: bật PrismCore_Purchase_popup bằng sot_click_oracle --force-on rồi bấm từng nút — cả 7 nút đều mở Canvas/Shop_manager/Loading_UI (Inapp Purchasing), đúng hình của mọi nút IAP khác trong game. Cái sai của lập luận cũ: nó suy từ TÊN (không có literal prism*) ra BẢN CHẤT; mà tên nút trong scene không phải product id — id sống trong code, và panel tham chiếu bằng enum, không bằng chuỗi (0 kết quả khi grep mọi SKU trong Main.unity). Ánh xạ ĐÃ ĐO (G1865) — đọc Core_Purchase_Panel.type lúc chạy: (2)→core_100 · (3)→core_500 · (4)→core_3300 · (5)→core_1050 · (Limited)→core_daily_750 · (Limited)(1)→core_weekly_1750 · (Limited)(2)→core_weekly_6000. Lưu ý: KHÔNG theo thứ tự tên nút — (4) và (5) đảo nhau, nên phép đoán theo thứ tự sẽ sai hai ô. |
ĐÃ ĐÓNG |
| Cửa sổ hiện thật của StepUp / WeeklyShop / DailyGun / PlusPack | THU HẸP 2026-09-07 (F008 T8 / F014 T3, đo trên Pixel bản .learning): WeeklyShop + công tắc tiện lợi ĐÃ ĐO — Stage_Value_Max = 100 thì nút vào panel và công tắc Auto đều không hiện, = 1000 thì hiện và tab VIP hết ổ khoá; và chu kỳ 7 ngày của PlusPack đo được bằng cách đặt mốc mua now − 7 ngày + 5 phút để gói hết hạn giữa phiên. Còn UNKNOWN: StepUp và DailyGun |
Đừng dịch đồng hồ hệ thống — Time_manager có sàn đơn điệu _floor + phát hiện nhảy giờ nên lùi giờ hay bị kẹp; đẩy mốc thời gian vào SAVE (tools/patch_save.py) rẻ và sạch hơn |
~~PlusPack.Is_Banner_Stage_OK (0x2B2C6AC) so sánh chính xác cái gì~~ |
ĐÃ ĐÓNG 2026-09-07 (G1689, F014 T4): thân của nó giống TỪNG LỆNH với WeeklyShop.Is_VIP_Unlocked (0x2B3EA58) — Tree_manager.instance != null && Tree_manager.instance.Stage_Value_Max > 189. Mốc banner gói và mốc mở khu VIP là MỘT (Stage 20-1); hằng Banner_Stage_Min = 190 không được binary đọc, nó so > 189 trực tiếp |
disasm-proven — cách gỡ: so hai bản decompile của một hàm ĐÃ BIẾT có cùng hình, rẻ hơn sửa biên hàm |
StepUp.Gun_Rewards = [23,28,33,39,44] ánh xạ sang khẩu súng nào |
✅ ĐÓNG (derived, doc 11 §3.1): #23 유령 저격총 (S, LASER) · #28 미사일 SS (SS, MISSILE) · #33 미사일 SSS (SSS, MISSILE) · #39 폭발레이저 X (X, BLAST_SNIPER) · #44 폭발레이저 XX (XX, BLAST_SNIPER) — 5 bậc S → XX. Giá trị blob tái xác nhận bằng SHA-256 (G353) | Gun_list scene 20307980; TABLE.md hàng 19 |
catgunner_luxury_pass vs catgunner_luxury_upgrade khác nhau ở đâu |
✅ ĐÓNG: khác ở GIÁ. Thân hàm trao giống hệt (0x2B32AA4 ≡ 0x2B328F8) nhưng store bán 394.000đ vs 210.000đ ⇒ _upgrade là bản nâng cấp rẻ hơn cho người đã có VIP. Điều kiện hiển thị của từng panel vẫn UNKNOWN |
iap_prices_store.json; Purchase_Luxury 0x2B2FAC4 / Purchase_Luxury_Upgrade 0x2B2FBA4 |
vip_pass vs vip_pass_2 |
✅ ĐÓNG một nửa: vip_pass = 210.000đ trên store; vip_pass_2 không được store trả giá ⇒ không phải sản phẩm Play ở bản này. Vì sao code vẫn giữ nó (khôi phục cho người đã mua?) vẫn UNKNOWN |
iap_prices_store.json; TryApplyPurchase 0x2B32204 |
| Chỉ số offer 31-35 | Bằng chứng phủ định: không product id nào trả về | – |
Nhánh ShowRewardAd(8) trong UpgradeRebirth_Start_By_IAP |
Không thể đạt tới trong thực tế: hàm đặt is_UpgradeRebirth = is_UpgradeRebirth_IAP = 1 (word 257) rồi mới kiểm !is_UpgradeRebirth \|\| is_UpgradeRebirth_IAP ⇒ luôn đi nhánh Rebirth_FX.Start_Internal |
Hook 0x2b23668 dưới segment 1 để xác nhận 0 lần fire |
Nhánh interstitial trong Monster_manager.SweepBonus_Get |
Không thể đạt tới: đặt cờ +194 = 1 rồi kiểm !cờ |
như trên |
Admob.ShowRewardAd(int AdIndex) (0x2A59C04) — nút nào gọi |
UNKNOWN: 0 code-xref ⇒ nối từ scene bằng UnityEvent | grep m_MethodName: ShowRewardAd trong Main.unity |
Súng quảng cáo weapon_02 có nhánh nào trong cụm bắn của Cat không |
✅ ĐÓNG — KHÔNG (G363): 12 RVA của cụm bắn đã quét, không có weapon_02 / AD_Type; súng thưởng chỉ chạm Gun_manager (§4.6) ⇒ không gì trong đường bắn route qua IAdService |
— |
| Cơ chế kích hoạt Season reset và backend Redeem code | UNKNOWN (kế thừa LIVEOPS_SURFACE §7) |
probe openpanel + bắt network |
Nội dung /[endpoint] cho segment 1 và 2 thật trên production |
UNKNOWN — chỉ mới thấy segment 0 | POST thử với test flag, hoặc Cheat_OnSegmentChanged (0x2AC96D0) rồi dump CachedConfigJson |
Shop_manager.DiaRewards / Dia_FirstBuy_Used trên save mới |
✅ ĐÓNG (G350): .cctor 0x2B36F9C cấp Dia_FirstBuy_Used = new bool[5] và DiaRewards (blob SHA MATCH); Is_Dia_FirstBuy = 3 phép thử của binary (index < 0, Length <= index, !Used[index]), không null-guard |
TABLE.md hàng 17 |
| Không quan sát thấy | Gun_manager.Balance_Reload / Request_Sheet: 0 lần fire qua 3 phiên ⇒ bảng súng không remote-driven |
PROBE_RESULT.md §3 (G144 đã đóng) |
| Không quan sát thấy | PlayFab.GetTitleData: 0 caller ⇒ TitleData không phải kênh liveops |
FABLE_ANALYSIS §U4 |
11. Raw evidence
[liveops catalog: PROBE_RESULT.md]— payload ServerConfig production thật (segment 0), sweep ngưỡng, đóng G144[liveops catalog: 01_shop.png],02_shop_scrolled.png— giá + phần thưởng shop thật (VND)[liveops catalog: phaseA_capture.json]— 9 419 node canvas, trạng thái active của mọi nút liveops[liveops catalog: sweep.json]— bảng sweep ngưỡng 0→1000[liveops catalog: PROBE_RESULT.md]— thang bật/tắt IAA dưới segment 1 (§1.3), gate kép FreeCash (§1.4), biên quan sát "no fill" (§1.5), panel late-game (§2), lớp cổng tutorial (§6)[liveops catalog: probe2d_capture.json]— bảng đoMaxStageDisplay→ OfferWall/FreeCash/interstitial[liveops catalog: iap_prices_store.json]— 33 giá VND thật dump read-only từLocalized_Price_returntrên bản store chính hãng (dict_count 33,resolved 33)[liveops catalog: 31_panel_Rebirth.png]— "Super Rebirth! ×3" + nút quảng cáo1/1[liveops catalog: 31_panel_Trip.png]— 4 tiền tệ, bannerMua→ PrismCore, lịch map theo thứ[liveops catalog: 30_all_unlocked_hud.png]— HUD sau khi mở đủ 7 feature[liveops catalog: LIVEOPS_SURFACE.md]— 15 keyConfigResp,EmbeddedDefaults, Firebase RC, endpoint[liveops catalog: UNLOCK_GATES.md]— thangStage_Value_Maxvà công thứcstageDisplay[phân tích nội bộ]§U2, §B-6, §B-7 — cờ boost-kind, buff 300/900, ảnh hưởng segment[evidence codegen: Rebirth.c]—Rebirth()0x2B22AE0(đuôi: review ↔ interstitial)[evidence codegen: PRESTIGE_RULES.md]— boost ×3, daily limit, kind 1/2/3dump.cs—Admob:585348,AD_Type:584959,Inter_Place:584976,FreeCash_manager:586919,Review_manager:588967,Buff_manager:592886,OfferWall_manager:599641,DailyGun:599310,PlusPack:599810,Shop_manager:600302,InApp_*enum:600059-600102,Mission_manager.is_Vip/is_Luxury:587346stringliteral.json— product id, place, khoá PlayerPrefs, log ADX[native: global-metadata.decrypted.dat]— blob khởi tạo mảng static (DiaRewards,StepUp.Gun_Rewards/Dia_Rewards,DailyGun.MaxCounts/PetCoin_Rewards/GunNum_Base)[export AssetRipper: Main.unity:20647245-20647268]— giá trịAdmobserialize trong scene[restore project: Services]— ranh giới R1 fail-closed của bản restore
12. NGUỒN MỚI (2026-09-13): IAPProductCatalog — bảng khai báo SKU CÓ KIỂU
Độ tin cậy: CAO (đọc thẳng từ asset trong gói SOT, 4 bản sao trùng nhau từng byte).
Tài liệu này trước đây suy kiểu consumable/non-consumable từ hình dạng hàm Apply*. Có một
nguồn thẩm quyền hơn mà bản cũ không nhắc tới:
[SOT: IAPProductCatalog.txt] — catalog Unity IAP codeless, 8.264 byte,
sha256 7f76ad2e14b640eb…, trùng byte với 3 bản sao khác trong repo. Nó khai 32 product (không
phải 34 như số mã trong code), mỗi mục có trường type rõ ràng.
Hai điều nó nói mà không nguồn nào khác nói được:
vip_passvàvip_pass_2khai làtype: 0= CONSUMABLE, không phải non-consumable như §6.1 đang ghi. Bằng chứng hỗ trợ phía mã:ApplyVipPass(0x2B32C50) không có cổng "đã sở hữu" — mua lại là cộng thêm 500 dia, khác hẳn khuôn của 6 gói non-consumable; vàIs_Purchased(InApp_Type)chỉ phủ 6 cờ, không phủ VIP. ⇒ Mâu thuẫn CHƯA giải quyết giữa §6.1 và catalog — giữ cả hai trên bàn, đừng chép một phía vào thiết kế game mới.- Hai SKU Luxury (
catgunner_luxury_pass,catgunner_luxury_upgrade) KHÔNG có trong catalog 32 mục, nhưng store vẫn trả giá cho chúng (394.000₫ / 210.000₫). ⇒ chúng được đăng ký ở đường khác; đường đó chưa chứng minh được vìShop_manager.InitializeIAPlà state machine rỗng ở bản dựng lại (R1-out).
Ba điều cần đính chính khi đọc tài liệu cũ:
googlePrice.num = 0.0vàapplePriceTier = 0cho cả 32 mục ⇒ không giá nào nằm trong build. Mọi con số VND trong §6.1 đến từ đo store thật, không từ gói.title/descriptionđể rỗng cả 32 mục ⇒ chữ hiển thị đến từ Play Console + I2.Loc.- Trường
entries_len: 37trong dump không phải "37 SKU" — đó là dung lượng mảng nội bộ củaDictionary. Ai đọc file thô rất dễ đếm nhầm thành 37 sản phẩm.
Cho người làm game idle mới — đây là phần đáng chép lại:
| Quyết định của bản gốc | Số thật | Vì sao đáng để ý |
|---|---|---|
| Gói tiền cứng chia 5 bậc | dia_0..4 = 240 / 600 / 1.500 / 3.500 / 8.000 dia |
tỉ lệ ~2,5× mỗi bậc, bậc cuối ~33× bậc đầu |
| Khuyến mãi lần đầu | nhân đôi bằng dịch trái 1 bit, cờ Dia_FirstBuy_Used[5], có mùa (DiaFirstBuy_Season_Current = 1) |
"×2 lần đầu" tái sử dụng được theo mùa, không phải một lần đời |
| Gói tiện ích non-consumable | 6 cờ: Remove_AD_forced, Remove_AD_All, AutoUpgrade_Pack (+500 dia), Buff_Pack (+1.000), Dia_Pack (+16.000), Mine_Pack (+500) |
gói tiện ích vẫn kèm tiền — trừ hai gói xoá quảng cáo, chúng cho 0 dia |
| Bậc thang StepUp | 5 bậc, bắt buộc mua tuần tự, mỗi bậc một lần, hạn 7 ngày; súng {23,28,33,39,44}, dia {0,200,400,600,1000} |
bậc đầu 0 dia — mồi rẻ, giá trị dồn về cuối |
| Gói theo lịch | DailyGun trần {2,2,1}/tuần lịch (epoch 2024-01-01); Core daily 24 h, weekly 7 ngày tính từ lần mua trước |
trần theo lịch và trần theo khoảng cách mua là hai cơ chế khác nhau, game này dùng cả hai |
| PlusPack | 3 loại độc lập, 7 ngày; Mult_Monster=2, Mult_Boss=2, Mult_Mine=1.5; bản Auto rút Trip 16 giờ |
nhân số nhỏ (1,5–2×) nhưng cộng dồn theo chế độ |
| Rebirth bán lẻ | catgunner_rebirth_single, trần 1 lượt/ngày, mở bán từ Stage_Value_Max > 989 |
bán một lần bấm nút, không bán tài nguyên |
| Chống lặp giao dịch | ProcessedTxnIdsMax = 50 |
hàng đợi id đã xử lý, không phải cờ đơn |
| Tiền tệ hiển thị | 23 định dạng, 6 loại không phần thập phân (KRW, JPY, VND, IDR, CLP, HUF) | phát hành đa thị trường thì đây là bảng phải có sẵn từ ngày đầu |
Nguồn từng dòng: IAPProductCatalog.txt; [reconstructed: Shop_manager],StepUp,DailyGun,PlusPack,Rebirth_manager,DiaPig,Mission_manager}.cs
(mỗi hằng có chú thích RVA tại chỗ); dump.cs,stringliteral.json};
giá VND: [liveops catalog: iap_prices_store.json].