10-monetization-iaa-iap.md · Cập nhật lần cuối: 2026-09-06 23:04 +07 · sha256 nguồn b3b20140c50410 — 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 |
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×.
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. Chỉ 6/10 đo được ngay; 4 cái còn lại không phải giới hạn
R1 mà là nợ restore của bản dựng lại (chỗ gọi ShowRewarded chưa có thân):
AD_Type |
Đo được? | Vì sao |
|---|---|---|
Mine_Bonus |
ĐÃ ĐO (bảng trên) | — |
SpeedRun_Bonus, Boss_Bonus, Monster_Bonus, Mission_Reward, Upgrade_Rebirth |
đo được, chưa đo | chỗ gọi đã dựng, cần dựng điều kiện màn |
Money_Buff, Power_Buff |
chưa | Buff_manager.AddBuff còn rỗng |
Fish_Gacha |
ĐÃ ĐO (§4.1b) | F008 T5b, 2026-09-06 — Fish_manager.{FreeGacha, FreeGacha_RewardGet, PremiumGacha} đã dựng và chạy trọn nhánh Granted trên máy |
AD_Reward_Gun |
chưa | Gun_manager.{AdRewardGun_TryWatch, AdRewardGun_Acquire} rỗng |
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 + reload Mine_Start_UI |
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.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 (không có bước nào ở đây được tái hiện trong bản restore)
<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 |
| 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 |
| 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 | 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 — 4 giá trị enum ↔ 4 chip trên panel, banner mở PrismCore_Purchase_popup |
Decompile Adventure_Panel + Core_sprite_return(int); hoặc chụp panel sau khi nạp Currency_Core[3] |
Prism_Pack ×7 trong scene (3 bản (Limited)) — là IAP hay mua bằng Core? |
UNKNOWN — không có product id prism* nào trong stringliteral.json, và ItemIdxFromProductId không nhận id nào như vậy ⇒ bằng chứng phủ định: không phải IAP của bản 1.1.59 |
Mở PrismCore_Purchase_popup trên .sot ở Trip Lv cao và chụp |
PlusPack.Is_Banner_Stage_OK (0x2B2C6AC) so sánh chính xác cái gì |
UNKNOWN (IDA gộp biên hàm; chỉ có hằng Banner_Stage_Min = 190 là metadata-proven) |
Decompile lại sau khi sửa biên hàm, hoặc hook trả về |
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