CatGunner CookbookBardCat 1.1.59 · L1 ANALYSIS · tài liệu học
Trang chủTài liệu framework › 07 — Tiến trình & bộ khung Manager
Nguồn: 07-progression-managers.md · Cập nhật lần cuối: 2026-09-06 23:04 +07 · sha256 nguồn bc4b07b3f1c6

07 — Tiến trình & bộ khung Manager

Nguồn: chưng cất từ restore SOT com.Chodun.CatGunner 1.1.59 (BardCat). Mọi C# ở đây là L1 ANALYSIS, không phải source gốc. Prestige/Rebirth có tài liệu riêng: 09-prestige-rebirth.md.


1. Câu hỏi tài liệu này trả lời

Một game idle thương mại là rất nhiều hệ thống nhỏ đan vào nhau: nâng cấp, gacha, pet, relic, skin, nhiệm vụ, xếp hạng, buff, sự kiện hằng ngày. CatGunner tổ chức tất cả bằng 35 singleton X_manager. Tài liệu này mô tả bộ khung đó, các hệ thống tiến trình cốt lõi, và ranh giới rõ ràng giữa gameplay và dịch vụ bên ngoài.


2. Stable truths

2.1 Bộ khung manager

Claim Mức bằng chứng Nguồn / provenance
Game có 35 singleton dạng *_manager / Gamemanager / UpdateManager / OfflineProgressManager; mỗi cái tồn tại đúng một instance trong scene (hoặc dựng lúc boot) scene-census [report: codegen_lane.md] §1.2
Assembly-CSharp731 type; chỉ 190 trong số đó thực sự được đặt vào scene scene-census [index: scene_census.json] (30 082 tham chiếu m_Script)
Tập làm việc giá trị cao = {managers} ∪ {class ≥ 20 instance} = 66 class / 1 284 method trên tổng 5 194 method scene-census [index: priority_static.json]
Xếp hạng Tier-0 theo số method: Shop_manager 107 · Gamemanager 70 · Mission_manager 54 · Time_manager 51 · Adventure_manager 49 · Gun_manager 47 · ServerConfig_manager 43 · Pet_manager 43 · OfferWall_manager 40 · Fish_manager 38 · Monster_manager 37 · Rank_manager 35 · Tree_manager 35 · FreeCash_manager 34 · Cheat_manager 32 · Mine_manager 31 · Boss_manager 29 · Raid_manager 26 · SpeedRun_manager 26 · Tutorial_manager 22 · Upgrade_manager 22 · UpdateManager 21 · Relic_manager 20 · Skin_manager 19 · Rebirth_manager 18 · OfflineProgressManager 12 · UI_manager 12 · Buff_manager 12 · Notification_manager 11 · Cat_manager 11 · Pet_Ingame_manager 11 · Sound_manager 8 · Polling_manager 7 · Review_manager 5 scene-census + metadata-proven [report: codegen_lane.md] §1.3
hai biến thể singleton: (a) field static trần public static X instance; (đa số), (b) property có backing field public static X instance { get; set; } (Sound_manager) metadata-proven dump.cs từng class; Sound_manager dump.cs:590265
Time_managerRedeemCode_manager0 instance trong scene — được dựng lúc chạy scene-census [report: codegen_lane.md] §1.3
Ba ngoại lệ của khuôn Awake: instance = this (đọc từ binary khi khôi phục 22 singleton): DailyCheck không có Awake — gán instance trong OnEnable (0x2A6B278), gỡ trong OnDisable (0x2A6B390); Cat.Awake chỉ gán khi !is_AI (singleton = mèo người chơi, follower không ghi đè); Fish_Tank không có singleton — đi qua Fish_manager.instance.fishTank disasm-proven G296, G352, G330 (gap list (nội bộ))
Gun_manager.Awake gọi InitOwnedArray (inline, 0x2B0991C): chuẩn hoá Owned_Gun_list về đúng 16 ô -1 và chép các ô cũ sang — vì thế bản cài mới không bao giờ lỗi khi index kho súng disasm-proven G296
WeeklyShop.Awake (0x2B3E900) = instance = this; .ctor đặt Open_Place = "hud" disasm-proven G309
UI_manager.UI_Reload (0x2AD7FB8, trên đường Game_Start): đặt currentType rồi UI_objs[i].SetActive((int)type == i). UI_objs5 phần tử nhưng UI_type6 giá trị ⇒ chọn Raid tắt mọi UI — hành vi thật của binary (nhất quán với Raid chưa phát hành) disasm-proven G354
Hệ cảnh báo UI_manager.Alert(string) = round-robin trên Alert_list (3 Alert trong scene): txt.text, SetActive, SetAsLastSibling — mọi cảnh báo cho người dùng ("mở ở stage N", hết vé, trần level…) đi qua đây. Alert.Reload từng là stub phía project ⇒ mọi cảnh báo đều câm cho tới đợt 9. Cùng lô: Loading_On/Off, OnBackButton (InputAction bật/tắt theo OnEnable/OnDisable), CloseExitPanel, QuitGame = Save rồi Quit disasm-proven G395
Mẫu save/load thống nhất: mỗi manager có SaveData_return() (trả int[] hoặc tương tự) + LoadData(SaveData data) metadata-proven Upgrade_manager RVA 0x2B4A68C/0x2B4A750; Rebirth_manager 0x2B22974/0x2B22A38
Đường boot: LoadData.Load(data)data == null ? InitNewGame() : OfflineProgressManager.instance.EvaluateAndApply(data, ContinueLoad) — tiến trình offline được đánh giá TRƯỚC, và ContinueLoad được truyền vào làm CONTINUATION disasm-proven CodeRefsTo(EvaluateAndApply) == {LoadData.Load} chính xác
⇒ Popup "Chào mừng trở lại!" nằm trong luồng điều khiển, không phải hiệu ứng phụ: phần còn lại của việc nạp save chờ disasm-proven như trên
ContinueLoad sau đó toả ra ~48 điểm gọi: X.LoadFromSaveData(data) cho từng manager + cặp Time_manager.get_Now / DateParseSafe cho từng bộ đếm thời gian disasm-proven như trên
Save KHÔNG mã hoá, KHÔNG salt: SaveSystem.TryLoadFrom (0x2AC6064) + JsonData_Decoding (0x2AC6228) = Base64(UTF8(JSON của JsonUtility)) disasm-proven RVA đã ghi
Đường dẫn: Application.persistentDataPath + "/saves/", chính NoSaveFile.json, dự phòng NoSaveFile.json.bak; Load rơi về bản dự phòng khi bản chính parse ra null disasm-proven như trên
ACTk Obscured* chỉ dùng TRONG BỘ NHỚ. JsonUtility serialize giá trị thường: SaveData khai báo public int Dia, không phải blob obscured metadata-proven (đã kiểm độc lập: 0 lần xuất hiện Obscured trong toàn khối SaveData) dump.cs:589019+ — public InfVal Money; // 0x18, public int Dia; // 0x78
SaveData100+ field; khối restore chép thẳng vào static của manager (vd data.Money@+0x18 → Gamemanager static +0x28) metadata-proven dump.cs:589019+
Mẫu cân bằng thống nhất: mỗi manager có Balance_Reload() sinh bảng số từ công thức metadata-proven Upgrade_manager RVA 0x2B4A02C; Rebirth_manager 0x2B21CB4; Relic_manager 0x2ADFE20; Skin_manager 0x2B3F2EC
NHƯNG Balance_Reload là DEAD CODE trong 1.1.59CodeRefsTo = [] (đã kiểm 3/13); bảng thật lúc chạy là mảng đã serialize trong Main.unity, nhiều khả năng do một Editor script ngoài build bake vào VERIFIED [phân tích nội bộ] §U4, §B-3
Mẫu UI thống nhất: UI_Reload() / Reload() / Value_Reload() / First_Reload() — vẽ lại từ state, không giữ state riêng metadata-proven + runtime-observed nhiều class; Mission_Reward_Item.First_Reload 200×, Value_Reload 150×
Dữ liệu nhạy cảm dùng ObscuredInt từ Anti-Cheat Toolkit (Asset Store 202695, user sở hữu) metadata-proven + package-source Gamemanager.Dia, Rebirth_Upgrade_Info.Lv; quy tắc dự án (nội bộ) R2

2.2 Nâng cấp (trong vòng chơi)

Claim Mức bằng chứng Nguồn / provenance
Upgrade_manager.MaxLv = 10000 (public const) metadata-proven dump.cs:602928
Hằng số cân bằng: PowerMultPerLv = 1.08, MoneyMultPerLv = 1.08, SpeedAddPerLv = 8, PriceBase = 50, PriceGrowth = 1.38, PriceSoftCapLv = 5000, PriceLateGrowth = 2, SpeedStagePenalty = 2 metadata-proven dump.cs:602928
Upgrade_manager.CalcPrice (0x2B4A474) — công thức giá dạng đóng dùng làm fallback: lv ≤ 5000 ? ((InfVal)(1.38^lv × 50)).ToPrecision(3) : ((InfVal)(1.38^5000 × 50) × MathInfVal.Pow(2, lv − 5000)).ToPrecision(3) — điểm gãy ×2^(lv−5000) từ lv 5001, 3 chữ số có nghĩa. Không tới được trong 1.1.59 vì mảng scene có đúng 10 001 phần tử (§3.2); ghi để hoàn chỉnh — để nhắc: dạng đóng ≠ mảng ship (Price_balance[1] = 68 vs 69) disasm-proven G385
Upgrade_manager.Firse_reload (0x2B49FAC) = for i < 3: Upgrade_list[i].Lv = lv[i] (3 là literal); người gọi duy nhất = OfflineProgressSimulator.ApplyToManagers (0x2AB7504) — hàm đẩy level mô phỏng offline vào game disasm-proven G385
Bảng THẬT (mảng scene): Money_balance/Power_balance = [0]=100; [i] = trunc3([i-1]·1.08) (lặp có CẮT); Speed_balance = 100 + 8·lv; Price_balance: acc = trunc4(acc·1.38_double), lưu trunc3(int(acc))[1] = 68 chứ không phải 69 scene-YAML + VERIFIED (tái tạo 0/360 lệch) Main.unity:20393431 / :20413434 / :20353425; sim mục [M]
Bảng đó lệch rất xa dạng đóng 100·1.08^lv: −10.2 % ở lv 50, ~6.5 lần ở lv 1 000 VERIFIED như trên; 09 §3.2
Xác nhận độc lập: Speed_balance[46] = 100 + 8·46 = 468; ảnh chụp save thật của người dùng ở Lv.46 hiển thị đúng 468 disasm-proven + observed [device evidence: user_screenshot_stage5-4.png] (sha256 04ef8221…)
AutoUpgrade_All() (RVA 0x2B499AC) và Upgrade_Silent(int) — nâng cấp hàng loạt không hiệu ứng metadata-proven dump.cs:602928
Runtime: Upgrade_manager.Lv_return 409×, Money_return 95×, Power_return 12×, Price_return 10×, Value_str_Return , Upgrade trong ~40 s runtime-observed [runtime trace: runtime_ranking.json]

2.3 Súng: gacha + auto-merge

Claim Mức bằng chứng Nguồn / provenance
Kho súng cố định 16 ô, ô trống ký hiệu -1: [Header("Owned Gun Inventory (16칸, -1 = 비어있음)")] public int[] Owned_Gun_list metadata-proven dump.cs:596607
Rút gacha: DrawRandomGun(int Degree) (RVA 0x2B0A278), DrawRandom_Normal() (0x2B0A410), DrawRandom_Epic() (0x2B0A92C), DrawGun_Degree_5(int) (0x2B0A930) metadata-proven dump.cs:596607
Tự động ghép: is_AutoMerge, AutoMerge_Degree, public const int AutoMerge_Exclude_Degree = 7 (bậc ≥ 7 luôn bị loại khỏi auto-merge), TryStartAutoMerge, AutoMerge_Validate, AutoMerge_Limit_Degree metadata-proven dump.cs:596607
Màu theo bậc: Gun_manager.Degree_Color_return(int Degree) = instance.Degree_Colors[Degree] disasm-proven _evidence/Gun_manager/rule_statics.cs.evidence, RVA 0x2B0BD90
Bậc của một khẩu = gunNum / 5 (5 khẩu mỗi bậc) disasm-proven Gun_Item_Demo.Reload RVA 0x2B11A18
Tên loại súng đi qua I2 Localization: Gun_Type_str_return(GunType)ScriptLocalization.Gun.<TYPE> disasm-proven RVA 0x2B0BE08
10 bậc: [Header("등급별 누적 획득 개수 (AF 로그용, 10등급)")] public int[] Weapon_Get_Counts metadata-proven dump.cs:596607
Súng thưởng từ quảng cáo có hạn dùng: AD_Gun_SlotIndex, AD_Gun_Num, AD_Gun_EndUtcTicks (long), AD_Gun_RemainSec, AddAdRewardGun(gunNum, cooltimeSec), AdRewardGun_Expire() metadata-proven dump.cs:596607
Hằng số sự kiện súng quảng cáo: AD_GUN_EVENT_GUN_NUM = 14, AD_GUN_EVENT_COOLTIME = 1800 (30 phút) metadata-proven dump.cs:596607
Cân bằng súng tải từ xa: private const string URL = "[URL đã lược bỏ]" + IEnumerator Request_Sheet() (RVA 0x2B0AB6C) ghi Gun_list[i].{Name,type,Damage,AtkSpeed} metadata-proven dump.cs:596607
NHƯNG nó là dead code trong 1.1.59: Request_Sheet chỉ được gọi từ Gun_manager.Balance_Reload (0x2B0AB4C), mà CodeRefsTo của hàm đó = []bảng súng lúc chạy đến từ inspector (scene), không từ Google Sheet VERIFIED FABLE §U4; Main.unity:20307980 (65 phần tử Gun_list)
Dải A2:I669 cột × 65 dòng — khớp với Gun_Info có 13 field nhưng chỉ một phần được cấu hình từ xa metadata-proven (đọc từ chuỗi URL) như trên
Mission_manager.HighTier_Gun_Table = int[8] {16, 18, 20, 22, 24, 25, 27, 29} (static, .cctor 0x2A72888) — bảng blob <PrivateImplementationDetails> được chứng minh bằng SHA-256 của chính tên field (metadata 0x9256C8). Súng tương ứng (đối chiếu Gun_list scene, doc 11 §3.2): #16 드럼 샷건 (A) · #18 플라즈마 캐논 (A) · #20 개틀링건 (S) · #22 화염 캐논 (S) · #24 전갈 미사일 (S) · #25 관통 SS · #27 레이저 SS · #29 폭발레이저 SS. Nơi tiêu thụ bảng này chưa đọc ⇒ ý nghĩa gameplay UNKNOWN metadata-proven (blob SHA MATCH) + derived (tên) G353, [evidence codegen: TABLE.md] hàng 8
Cùng kỹ thuật: StepUp.Gun_Rewards {23, 28, 33, 39, 44} / Dia_Rewards {0, 200, 400, 600, 1000} (.cctor 0x2B3B648); DailyGun.MaxCounts {2, 2, 1} / PetCoin_Rewards {300, 700, 1500} / GunNum_Base {20, 30, 35} (.cctor 0x2B26894); Shop_manager.DiaRewards {240, 600, 1500, 3500, 8000} (.cctor 0x2B36F9C) — 448/448 blob toàn build khớp SHA, không còn bảng InitializeArray nào là "không đọc được" metadata-proven (blob SHA MATCH) G350, G353; TABLE.md hàng 3-5, 17-19
Raid_manager.Gun_List / Gun_Index_List = int[3] {-1, -1, -1} (.ctor 0x2AFE24C) — 3 ô súng raid, trống = -1 (cùng quy ước với kho 16 ô) metadata-proven (blob) G353; TABLE.md hàng 15-16

2.4 Buff, Relic, Skin

Claim Mức bằng chứng Nguồn / provenance
Buff_manager: private const float BUFF_DURATION = 300 (5 phút), public const float BUFF_MAX = 900 (15 phút) metadata-proven dump.cs:592886
Chỉ hai đồng hồ buff: moneyBuffTime @0x28, powerBuffTime @0x2C; API: AddBuff(AD_Type), IsBuffActive(AD_Type), GetBuffRemainingTime(AD_Type), IsPermanentBuff metadata-proven dump.cs:592886
Một coroutine đếm duy nhất BuffTickCoroutine + TryStartCoroutine() (chỉ chạy khi cần) metadata-proven dump.cs:592886
Runtime: Buff_manager.get_IsPermanentBuff 103×, IsBuffActive 92× trong ~40 s runtime-observed [runtime trace: runtime_ranking.json]
enum AD_Type10 giá trị: Money_Buff=0, Power_Buff=1, SpeedRun_Bonus=2, Mine_Bonus=3, Boss_Bonus=4, Monster_Bonus=5, Mission_Reward=6, Fish_Gacha=7, Upgrade_Rebirth=8, AD_Reward_Gun=9 metadata-proven dump.cs:584959
Relic_manager phơi 7 static: Dmg_Value, Money_Value, CriDmg_Value (InfVal) + MoveSpeed_Value, Offline_Value, Rebirth_Value, Skip_Value (int) metadata-proven dump.cs:592186
Relic_manager.DrawPrice = 20 (const int); rút theo Core_Type: Relic_Draw(Core_Type type, int Count) metadata-proven dump.cs:592186
Bảng relic (Relic_manager.Balance_Reload 0x2ADFE20, 796 B): xây MỘT đường cong InfVal[300], seed 105, arr[i] = ToPrecision(arr[i-1] × 1.05, 3) rồi −100 (⇒ +5 %, +10.2 %, … ở 3 chữ số có nghĩa), và chép đường cong đó cho cả Damage / Money / CriDmg. Chỉ được gọi trên đường nạp khi bảng còn null disasm-proven G360, G361
Relic_manager.Value_Reload (0x2ADF8F0, 1 328 B): baseline Dmg_Value = Money_Value = CriDmg_Value = 100, MoveSpeed = 0, Offline = 0, Rebirth = 100, Skip = 0; cộng relic đã mở: bậc _0/_1/_2 của cùng loại = đường cong ×1 / ×1.5 (bỏ phần thập phân) / ×2; relic đơn lẻ tuyến tính: MoveSpeed & Skip = 2·Lv + 2, Offline = 10·Lv + 10 (phút), Rebirth = 5·Lv + 5. Chỉ gọi Cat.Balance_Reload nếu một giá trị chiến đấu đổi, chỉ InvalidateMoneyCache nếu Money_Value đổi (làm mới có chọn lọc) disasm-proven G361
R13 — bẫy base-100: khi Value_Reload còn rỗng, Dmg_Value = 0Cat.Balance_Reload tính Damage × 0 / 100 = 0 sát thương mà không có ngoại lệ nào; cùng lớp lỗi ở Skin_manager.Skin_Value_Reload (mèo 0 damage / 0 tốc độ), Rebirth_manager.Damage_return, tổng Fish disasm-proven + quy tắc G359, G363; quy tắc dự án (nội bộ) R13
Đường khởi tạo save MỚI (đóng G423): Relic_manager.Start (0x2ADF8EC) = B Value_Reload trần (4 B); Skin_manager.Start (0x2B3EFA0) = UI_First_Reload(); Skin_Value_Reload(); Cat_manager.instance.Skin_Reload() (không guard, như binary). Vì thế save mới cũng có baseline 100 (InfVal (1000000000, −7) == 100). Save đã nạp lấy baseline qua nhánh non-null của Loadhai đường, cùng đích disasm-proven G374
Skin: Skin_manager.is_Unlock (0x2B3F514) — skin 0 luôn mở, còn lại theo bool[] trong save; Skin_Bone_return (0x2B3FE88) / Skin_Color_return (0x2B3FEBC) cấp sprite xương + màu tay — khi ba hàm này còn stub, thân mèo null và tay trong suốt sau Skin_Reload (G425, đã đóng) disasm-proven G374
Skin_manager phơi 3 static int: Damage_Accum, Speed_Accum, Money_Accum — cộng dồn từ các skin đã mở metadata-proven dump.cs:601415
Skin có cả tác dụng hình ảnh: Skin_Bone_return(), Skin_Color_return(), Skin_Colors[], Skin_Bone_sprites[] metadata-proven dump.cs:601415
Có công tắc "skin không cho chỉ số": is_skinValueZero + Skin_Value_Zero() metadata-proven dump.cs:601415

2.5 Nhiệm vụ

Claim Mức bằng chứng Nguồn / provenance
enum Mission_Type chỉ có 4 giá trị: TreeCount = 0, StageClear = 1, Rebirth = 2, PlayMine = 3 metadata-proven dump.cs:587114
Cập nhật tiến độ qua một API duy nhất: Mission_manager.instance.MissionValue_Increase(Mission_Type) disasm-proven Tree.Die_Reload RVA 0x2B43904; Rebirth() RVA 0x2B22AE0
Có khoá tuần tự phần thưởng: is_Reward_SequenceLocked (150×) + Reward_Status_return (150×) metadata-proven + runtime-observed [runtime trace: runtime_ranking.json]
Có hệ mùa: Season_Reload(), is_Season_Closed() metadata-proven + runtime-observed như trên (5× mỗi cái)
Runtime: Mission_manager.MissionValue_Increase 86× ≈ số cây đổ (85× Tree.Die_Reload) runtime-observed như trên
Đính chính người gọi: MissionValue_Increase(TreeCount) được gọi từ Tree_manager.Tree_Destroyed (0x2B439BC, 864 B), không phải từ Tree.Die_Reload — IDA từng gộp hai hàm làm một (tail call + helper noreturn); Tree.Die_Reload thật chỉ có 2 dòng (spawn hạt + gọi Tree_Destroyed) disasm-proven (sau khi tách biên hàm) G346
Mission_manager.is_Vipstatic (không phải instance): ContinueLoad ghi data.Mission_is_Vip (@0x269) vào static_fields+0x0 bằng một word store dùng chung với is_SeasonStart (+0x65 instance) — trích xuất tự động từng gán nhầm cả khối Mission sang Adventure_manager disasm-proven (Hex-Rays + disasm) G329, [evidence codegen: FIELD_MAP.md]

2.6 Tính toàn vẹn / chống gian lận (NGOÀI PHẠM VI, ghi để nhận diện)

Claim Mức bằng chứng Nguồn / provenance
Time_manager (51 method) là hệ thống thời gian tin cậy / chống gian lận thời gian, KHÔNG phải hệ thu nhập offline metadata-proven G134 trong gap list (nội bộ)
Runtime Time_manager rất bận: get_MonoSec 435×, get_UtcNow 221×, get_Now 130×, get_Today 110×, DeviceJumpSec 79×, EndAwaitIfTimedOut 79× runtime-observed [runtime trace: runtime_ranking.json]
Gamemanager mang một nửa số method là máy móc toàn vẹn: Time_Hacker_Check/FirstCheck/EnsureAlive/Restart/ForceCheck, OnTimeChecked, OnCheatDetected, FlagAsHacker ×2, NetWatch, ClockJumpWatch, OfflineStreak_Count/Reset, GainCheck_*, OnFutureGain, AutoSaveRoutine, PlayTime_* metadata-proven dump.cs:583820
Hằng số chống gian lận: AutoSaveInterval = 10, TimeCheckIntervalMinutes = 30, WrongTimeThresholdMinutes = 10, RealCheatThresholdMinutes = 60, OfflineFlagThreshold = 5, StrikeToFlag = 2, StrikeDecayDays = 60, GainCheckMinIntervalSec = 60, GainRetryMax = 2, ForceCheckMinIntervalMinutes = 10 metadata-proven dump.cs:583820
Dùng TimeCheatingDetector + một mảng URL kiểm giờ static readonly string[] TimeCheckUrls với _timeUrlIndex xoay vòng metadata-proven dump.cs:583820
Quyết định phạm vi: đây là máy móc toàn vẹn, giá trị gameplay thấp, R1-adjacent (thời gian mạng) ⇒ KHÔNG dựng lại quyết định lane G134
NHƯNG Time_manager gate thu nhập OFFLINE, nên nó KHÔNG hoàn toàn tách khỏi kinh tế: GainToleranceSec 1800, GainToleranceSameSessionSec 300, Trust level, ledger 32 entry, FutureLeadMaxDays 1 metadata-proven dump.cs:590441; FABLE §B-4
Đồng hồ của game (G391): get_UtcNow (0x2AD32BC) = DateTime.UtcNow + SavedOffsetSec; khi đã Synced: mốc máy chủ đơn điệu _anchorServerUtc + (MonoSec − _anchorMonoSec) được dùng trong lúc một kiểm tra đang chờ (AwaitingCheck), ngoài ra chỉ khi nó muộn hơn (giờ không bao giờ lùi); kết quả kẹp không thấp hơn sàn LastTrustedUtc; Kind = Utc. Now = UtcNow + offset múi giờ (cache 60 s mono, Local); Today = Now.Date; TodayUtc = UtcNow.Date; IsUsable = TrustLevel != None; MonoSec = _mono.Elapsed. Setter LastTrustedUtc (0x2AD01C4) vũ trang sàn qua TryParseExact("o") disasm-proven G391
.cctor của Time_manager (0x2AD3AA8) từng thiếu — gốc rễ của crash save mới và các ngày năm 1: _mono Stopwatch (null ⇒ mọi MonoSec hỏng), _lastTrustedUtc, _tzCheckedMonoSec = −∞, AwaitingCheck = true, BuildUtc = 2026-08-01, Ledger, _floor / LastAcceptedServerUtc = MinValue, _sessionId = Guid N[0..8], _jumpAnchorDeviceUtc = UtcNow. Boot mới: TrustLevel = NoneUtcNow = đồng hồ máy ⇒ G437 đóng (0 ngoại lệ trên batch 18) disasm-proven + build-measured G391, G394
Time_manager.RecordGain (0x2AD2CF4, 1 472 B) = sổ cái chống gian lận cho thưởng theo thời gian: mỗi khoản cấp được ghi kèm UTC của game, mức trust lúc cấp, session id, đồng hồ đơn điệu; LedgerMax = 32 (bỏ mục cũ nhất); bắn OnGainRecorded; từ chối dưới Trust.None (cảnh báo); khi một kiểm tra giờ đang chạy thì cảnh báo-nhưng-vẫn-ghi (literal bảo caller phải gate bằng CanGrantTimedReward). Đây là kênh mà PendingCountPrefix("offline:") đếm (doc 01 §5.8) disasm-proven G377

2.7 Ranh giới R1 — dịch vụ ngoài

Claim Mức bằng chứng Nguồn / provenance
Ads / attribution / analytics / IAP / remote-config / login KHÔNG được tích hợp; thay bằng 5 interface: IAdService, IAnalyticsService, IPurchaseService, IRemoteConfigService, ILoginService quy tắc dự án quy tắc dự án (nội bộ) R1
Binding mặc định là NullService fail-closed: IsRewardedReady => false, ShowRewarded(...) → ServiceResult.NotAvailable, Purchase(...) → NotAvailable source (project lane) [restore project: NullServices.cs]
ServiceLocator giữ binding + ResetToNull(); Bind(null) tự quay về Null service source [restore project: ServiceLocator.cs]
Manager thuộc diện R1-adjacent: ServerConfig_manager (43m), OfferWall_manager (40m), Rank_manager (35m), FreeCash_manager (34m), Review_manager (5m), Notification_manager (11m), UpdateManager (21m) metadata-proven + quy tắc quy tắc dự án (nội bộ) R1; [report: codegen_lane.md] §1.3
Liveops KHÔNG ghi đè kinh tế trong 1.1.59: Firebase RemoteConfig chỉ có 5 key và chỉ chạm UpdateManager (min_required_version, latest_version, update_check_enabled, timehack_pardon, offerwall_event_onoff); PlayFab chỉ login/leaderboard, GetTitleData 0 caller; ServerConfig_manager chỉ mang ads/freecash/offerwall/interstitial/weapon_02_ad_onoff VERIFIED FABLE §U4
Lớp 1 — Tutorial: trên save mới (Stage_Value_Max = 0) chỉ SpeedRun_Btn + Setting_Btn bật. Shop / Gun / Skin / Mission / DailyCheck / Auto / Eco / DiaPig / Buff đều tắtKHÔNG do Contents_Lock_Reload điều khiển ⇒ Tutorial_manager mở dần VERIFIED (bằng chứng phủ định runtime) PROBE_RESULT.md §6; G192
Cơ chế lớp 1 (đọc từ binary, đợt 6): điều kiện vào tutorial = !_cleared && Lv_return(Power) < 1 ⇒ hàng 27 nút ẩn cho tới lần nâng Power đầu tiên; CompleteTutorial bật lại cả hàng (§2.8) disasm-proven G375
Lớp 3 — segment/liveops: OfferWall từ Stage 10, Interstitial từ 12, FreeCash 15…35 (cửa sổ hai đầu — tự ẩn khi vượt 35). Đơn vị đo là MaxStageDisplay = Stage_Value_Max/10 + 1, không phải Stage_Value_Max thô VERIFIED (đo trực tiếp) PROBE_RESULT.md §1.2, §1.3
segment = 0tắt toàn bộ bề mặt liveops; FreeCash còn bị gate kép: trạng thái AlmediaLinkSDK._almediaStatus (static 0x38) stage VERIFIED 10 §4.5; dump.cs:1108434
Cổng thứ hai của FreeCash là _almediaStatus ∈ {Eligible(1), Linked(2)}, mặc định NotAvailable(3)bản cài mới KHÔNG BAO GIỜ thấy FreeCash cho tới khi Almedia khả dụng. (Không phải AppsFlyerInit.Segment — hai thứ dùng chung offset static 0x38 nên dễ quy sai) VERIFIED enum AlmediaStatus dump.cs:1110114; 10 §4.5
Thang mở khoá nội dung chạy trên Tree_manager.Stage_Value_Max (giá trị chỉ tăng = max(Level_Now + 10·Stage_Now), ghi ở Game_Win, không bị Rebirth() reset): ngưỡng 20 / 30 / 50 / 90 / 110 / 110 / 340; Rebirth mở ở 20 VERIFIED cả số lẫn tên (đợt 9): 7 handler Content_Btn_* đọc trực tiếp — Rebirth 20 / Fish 50 / Mine 30 / Boss 90 / Pet 110 / Monster 110 / Adventure 340, không đủ thì alert bản địa hoá "mở ở stage N" với N = 3 / 6 / 4 / 10 / 12 / 12 / 35 (Stage_Value_Max đếm 10 mỗi stage) Contents_Lock_Reload RVA 0x2B4441C; Game_Win 0x2B452E4; Content_Btn_* (G395); FABLE §U1
781 file .cs thuộc 27 assembly SDK được xác định và loại khỏi phạm vi, không tính là gap asset-census [report: asset_lane.md] §7
UpdateManager.CheckForUpdate (0x2AD8358) + continuation (0x2AD9B38): SOT gọi Firebase CheckAndFixDependenciesAsync rồi rẽ theo kết quả. Bản restore route qua IPlatformService.CheckDependencies(Action<bool available>)NullPlatformService gọi callback false trên thread gọi ⇒ nhánh "phụ thuộc lỗi" của SOT chạy (log Firebase 의존성 오류: Unavailable quan sát được trên thiết bị), nhánh thành công giữ nguyên nhưng không tới được. Không bịa trạng thái disasm-proven + quyết định R1 (runtime-observed) G360, G361, G362

2.8 Hệ save/load — bản đồ restore đã chứng minh từng field (đợt 3)

Nguồn: [evidence codegen: FIELD_MAP.md] (Hex-Rays dec_ContinueLoad.c 765 dòng + disasm thô, mỗi hàng có trích dẫn), ledger G290–G330. Đây là phần đóng UNKNOWN "cấu trúc SaveData" của §5.

Claim Mức bằng chứng Nguồn / provenance
LoadData.Load (0x2AC1298..0x2AC1C3C) restore trực tiếp 22 field tiền tệ / thời gian / chống gian lận (Money@0x18 → Gamemanager+0x28, Money_Accum@0x38, Ruby@0x58, Dia@0x78 → ObscuredInt, PetCoin@0x84, TotalPlayTime@0x88, khối TimeHack_*/TimeStrike_*, SavedOffsetSec@0xB8 → Time_manager+0x10, HasSavedOffset@0xC0) rồi uỷ quyền Upgrade_manager.LoadData / Rebirth_manager.LoadData / Gun_manager.AD_Gun_OnLoaded / pet disasm-proven (máy trích + kiểm tay) FIELD_MAP §A–B
Đính chính: tiến độ lõi được restore trong Load, không phải ContinueLoad: Stage_Now + Level_Now (@0x180, một lệnh LDR/STR D0 8 byte vì hai int kề nhau ở cả hai struct) → Tree_manager+0x28/+0x2C; Stage_Value_Max@0x188 → +0x30; SentWaveRatios@0x190 → LoadSentWaveRatios; Skin_Unlock@0x1A0 / Skin_Selected_num@0x1A8 → Skin_manager. Bản dump đầu dừng ở 0x2AC1968 (chỉ là nơi nhánh null nhảy tới) nên đã kết luận sai disasm-proven FIELD_MAP §E0–E1
ContinueLoad (0x2AC2690..0x2AC338C, 831 lệnh) có 35 restore trực tiếp, mỗi hàng được chứng minh riêng bằng Hex-Rays (biến đích truy về static_fields+0xB8 của class chủ) và đối chiếu disasm thô — không dùng bảng trích tự động (bảng đó gán nhầm khối Mission_* cho Adventure_manager) disasm-proven G328, G329; FIELD_MAP mục "ContinueLoad direct restores"
Thứ tự gọi trong ContinueLoad (đúng thứ tự nhị phân): Tree_manager.Game_StartContents_Lock_Reload → fan-out StepUp / DailyGun / PlusPack / Shop_manager / Buff_manager.LoadBuffTimes / FreeCash_manager / DiaPig.Init / Setting.UI_Reload / Rank_manager.Login / Tutorial_manager.SetCleared / DailyCheck.First_reload / Fish_Tank.ReloadAll / khối Adventure / Mission_manager.ReloadTime_manager.MarkSaveLoadedCat_manager.Skin_Reload + Balance_ReloadSaveSystem.AllowSave tại 0x2AC32CC = CUỐI đường nạpOfferWall.OnSaveLoadedServerConfig.OnSaveLoadedAppsFlyerInit.SendLog_Launch disasm-proven G328
AllowSave chỉ tồn tại ở hai chỗ trong binary: cuối ContinueLoad và cuối InitNewGame — autosave (10 s) không thể ghi đè save khi restore mới đi được nửa đường disasm-proven G330
Bản cài mới: SaveSystem.TryLoadFrom có guard File.Exists cho cả bản chính lẫn .bak ⇒ trả null ⇒ Load trả null ⇒ InitNewGame disasm-proven G330
Mọi ngày giờ trong save được parse là UTC: LoadData.DateParseSafe (0x2AC3458, 356 B) — bỏ qua null/khoảng trắng, TryParseExact("yyyy-MM-dd HH:mm:ss") rồi TryParse, InvariantCulture, styles 0x80 = AssumeUniversal \| AdjustToUniversal ⇒ đổi múi giờ máy không dịch được cửa sổ vắng mặt; rồi SanitizeYear (0x2AC3904, 608 B) = guard Phật lịch Thái (±543 khi năm lưu lệch ≥ 400 so với UtcNow; chấp nhận 2020..UtcNow.Year + 1). 15 điểm gọi / 6 file (LoadData 8, OfflineProgressManager 2, Shop 3, PlusPack 1, StepUp 1, ServerConfig 1) — khi hai hàm này còn là stub default(DateTime), mọi ngày restore đều 0001-01-01; residual (1) chỉ là triệu chứng nhìn thấy được disasm-proven G372
Khối Adventure có HAI NHÁNH trên data.Adventure_Maps (0x2AC2DC0): nhánh MỚI = Ticket_Now = 20 + đóng dấu Now; nhánh RESTORE (0x2AC2E28) = nạp Lv/Exp/Ticket_Now và parse mốc nạp vé với Time_manager.Now làm mặc định ⇒ mốc hỏng làm đồng hồ bắt đầu lại từ bây giờ thay vì chờ từ năm 1. Bản dựng lại từng gộp hai nhánh (lỗi từ sửa G901) — đã tách disasm-proven G372
Hàng nút HUD (27 nút tĩnh Main_UI/Btn_list) bị TUTORIAL ẩn: Tutorial_manager.Start (0x2B47300) — if (!_cleared && Lv_return(Power) < 1) BeginTutorial() (tắt bộ 32 DisableOnTutorial = cả hàng) else CompleteTutorial(). CompleteTutorial (0x2B473B8, 328 B) là thứ duy nhất bật lại hàng: CurrentStep = Done, tắt 3 CatHand_* + ArrowGunEquip, bật mọi mục DisableOnTutorial khác null, tắt object tutorial sau cùng. Baseline đính chính: HUD tối giản trên save mớiđúng SOT (ẩn tới lần nâng Power đầu); save đã nạp mới phải thấy hàng. Caveat thứ tự Unity: Start có thể chạy trước khi Load restore Upgrade_Lv ⇒ save đã nạp có thể bị bắt đầu tutorial nhầm disasm-proven G375
SOT chịu được cuộc đua Start-vs-load — bằng thiết kế (G385, đóng G432): cổng vào tutorial đúng là !_cleared && Lv_return(Power) < 1 (56 B, _cleared private không serialize); BeginTutorial → SetStep(Upgrade) → Enter_Upgrade → MoneyCheckLoop đọc lại Lv_return(Power) mỗi 0.2 s và nhảy sang Challenge khi ≥ 1 ⇒ tutorial tự thoát ngay khi Load restore level. Bản dựng lại kẹt ở UpgradeSetStep (0x2B4767C) là stub rỗng (CurrentStep = enum mặc định 0). Máy trạng thái đã dựng đủ: SetStep, Enter_Challenge/GunOpen/GunBuy/GunEquip, OnGunButtonPressed, 5 coroutine kiểm điều kiện (CheckSpeedRunClear, CheckGunSlotActive, CheckGunPurchased, CheckGunEquipped, SetupGunEquipArrow); RequestIfNeeded (0x2B4782C) là RET trần thật disasm-proven G385
Bước mua súng của tutorial có nạp dia theo kịch bản: Enter_GunBuy: if (Dia <= 99) Get_Dia(100 − Dia) — bảo đảm lần mua đầu tiên mua được. Đây là kịch bản hướng dẫn, không phải đường kiếm tiền (R1-sạch, không chạm SDK) disasm-proven G385
Thứ tự thực thi script KHÔNG phải nguyên nhân (G383): build ship không có script Assembly-CSharp nào có m_ExecutionOrder ≠ 0 (chỉ 10 mặc định package: EventSystem −1000, I2 RealTimeTranslation −450, TMP −110/−105/−100, PlayerInput −100, InfValInputField −80, Cinemachine −1 ×2, ToggleGroup +10); bản restore khớp build-measured (globalgamemanagers SOT) G383; [report: sot-script-execution-order.json]
Thứ tự restore đầy đủ của LoadData.Load (0x2AC113C..0x2AC1C3C, 2 816 B, 91 call — đọc lại đợt 4): A tiền tệ (0x2AC12C4: Money/Money_Accum/Ruby InfVal, Dia/_getDia/_useDia ObscuredInt, PetCoin, TotalPlayTime, is_TimeHacker, OfflineFailStreak/LastDate) → B đồng hồ (0x2AC1400: SavedOffsetSec, HasSavedOffset, LastTrustedUtc, Ledger, TimeStrike*, UsedCodes_Log, rồi mới Time_manager.Init — sau khi offset/ledger đã có; ServerConfig.LoadFromSaveData; chuỗi TimeHack_*; Rank_btn.SetActive(false) khi tài khoản bị gắn cờ chỉnh giờ) → C (0x2AC1690) Upgrade_manager.LoadData, Rebirth_manager.LoadData, Rebirth_CountD súng (0x2AC16E0: chép từng phần tử, chặn theo độ dài, AD_Gun_*, Gacha_Draw_Count, AD_Gun_OnLoaded, back-fill Has_Merged qua Has_Degree_Or_Higher(1)) → E1 pet (0x2AC1880) → E2 stage/skin (0x2AC19AC) → F (0x2AC1A24: FishInventory, UpdateStatTexts, Relics + Balance_Reload nếu bảng null + Value_Reload) → cuối (0x2AC1BC0) OfflineProgressManager.instance.EvaluateAndApply(data, ContinueLoad), không có instance thì ContinueLoad(data) thẳng. Chuỗi null ⇒ "" disasm-proven G359, G361
Bài học nghiệm thu (G355 → G359): bản dựng lại từng ship Load chỉ là dispatcher 10 dòng gọi ContinueLoad — thiết bị nạp save thật ra Money = 0, Stage_Now = 0, Upgrade_Lv = [0,0,0] trong khi SOT nạp đúng (8/38 field lệch). Đồng thời DailyCheck.Show_WhenReady là coroutine return nullStartCoroutine(null) NRE cắt ngang ContinueLoad (mọi restore sau DailyCheck không chạy). Cổng lật đã báo "OPEN 0/28" sai vì chỉ kiểm tên method không rỗng, không kiểm thân Load và không đi theo StartCoroutine. Bộ sinh giờ theo dõi 41 mục (kể cả iterator return null); OfflineProgressManager.Awake rỗng cũng bị bắt (offline không thể tới). Đối chiếu 38 field trên save thật là điều kiện lật, không phải số stub quy trình (acceptance FAIL rồi FIX) G355, G359, G361; [report: batch9]
Nghiệm thu lần 2 (batch 13, save thật Stage 3-3): 37/38 field khớp file bản .sot (Stage/Level/SVM 2/2/22, Money 2 497 189.34, Ruby, Dia 100, PetCoin, Upgrade_Lv [36,36,35], Rebirth, 6 cờ gói, PurchaseCount, Dia_FirstBuy_Used, DiaPig, Mine/Fish, bộ ba PlayFab round-trip không đổi, cờ, mute); 0 ngoại lệ; map Desert Stage 3 vẽ đúng. 4 residual còn mở (§5): (1) Adventure_LastTicketCharge nạp ra 0001-01-01 — restore có guard (0x2AC2FE8) chưa "đáp"; (2) đường offline không trả gì trên bản dựng lại (SOT trả +1 627 259.5 và hiện panel DailyCheck ngày 2) — IsUsable = false ⇒ nhánh !IsUsable, lỗi ở hạ nguồn; (3) autosave không ghi: SaveSystem.Save() không tham số rỗng (xem hàng kế); (4) hàng nút HUD không render (có từ trước) quy trình (acceptance PASS-WITH-RESIDUALS) G367; [report: 20260904T210045Z-batch13]
Đường GHI save cố ý để TẮT (G370): SaveSystem.Save() (0x2ABED84) = Save(SaveData.CreateFromCurrentState()); CreateFromCurrentState (0x2AC3DD4, 6 116 B, 245 call, 112 field) còn là stub. Helper đã dựng: LoadData.ToStringSafe, Time_manager.Touch, Upgrade_manager.SaveData_return, Rebirth_manager.SaveData_return, PlusPack.SaveData_return, DiaPig.GetSaveData. 45/112 field là nghịch đảo cơ học của đường nạp, 64 cần đọc riêng. Lý do giữ Save() rỗng = fail-safe: cây ghi dở dang sẽ ghi đè save thật bằng null (12 field DateTime, Upgrade_Lv, Rebirth_Upgrade_Lv ⇒ mất prestige). Nguồn chỉ-ghi đã chốt: LastSaveTime = ToStringSafe(UtcNow), mute từ Setting, is_reviewed từ Review_manager, TutorialCleared = (step == 5), InstallDate + ServerConfig_Cache từ static. Kế hoạch: residual 2/3/4 trước → CreateFromCurrentState với cổng phía ghi (liệt kê đủ 112 field + mọi SaveData_return/GetSaveData, 0 stub) → Save() là công tắc lật; nghiệm thu = round-trip diff từng field (chỉ timestamp/dẫn xuất được khác, mỗi cái phải giải thích). Trạng thái đợt 7 (G379): cổng đọc CLOSED 3/10 callee, 109/109 field chưa phủ; 6 helper đã dựng; 50/109 nghịch đảo cơ học, 59 cần đọc riêng; bàn giao cho lane ghi riêng (sở hữu SaveData.cs + SaveSystem.cs). Câu hỏi 7 Clone — ĐÃ TRẢ LỜI (G388): snapshot = Weapon_Get_Counts@0x138, Dia_FirstBuy_Used@0x2F8, NormalReward_Get@0x290, EpicReward_Get@0x298, Adventure_Currency_Core@0x2B8, StepUp_Purchased@0x338, DailyGun_Purchase_Counts@0x348; 13 mảng khác ghi theo tham chiếu, 5 List copy-construct. Vì sao quan trọng: mảng theo tham chiếu tiếp tục đổi theo state sau khi chụp ⇒ file có thể chứa trạng thái sau thời điểm "save" (với JsonUtility serialize ngay thì hiếm, nhưng là ngữ nghĩa SOT phải giữ nguyên để round-trip khớp) disasm-proven + quyết định G370, G379, G388
Đường GHI đã dựng lại — công tắc VẪN TẮT (G388): CreateFromCurrentState (0x2AC3DD4) = 127/128 field theo đúng thứ tự store của binary (is_FirstGacha@0x210 không bao giờ được game ship ghi); chỉ 5 null-guard của chính binary (Adventure/Relic/StepUp/DailyGun/FreeCash); Dia_FirstBuy_Season = 1 hằng; InstallDate.ToString("yyyy-MM-dd"); TutorialCleared = (CurrentStep == Done); Adventure_Maps / Relics dựng lại thành cây object. SaveSystem: PlayerData_return (0x2AC5724), Save(string) (0x2AC57B8, ghi .tmp → xoay .bak), SaveEncoded (0x2AC59D0); Save() (0x2ABED84) để rỗng (thân trong comment). Kích thước IDA 6 116 vs 6 108 = nuốt đuôi Adventure_MapType_Save..ctor (lớp G518), không ảnh hưởng field disasm-proven G388
Mô hình round-trip trên save Stage 3-3: theo ngữ nghĩa SOT = 126 identity / 2 giải thích được (LastSaveTime, LastTrustedUtc đóng dấu lại); với overlay hiện tại = 117 identity / 8 STUB / 1 CLOBBER = 9 phá huỷ ⇒ đúng là lý do công tắc tắt. Cổng phía ghi (mở rộng: field + callee + getter inline + round-trip) đọc CLOSED (6/6 getter inline stub, 9 blocker); bộ sinh cũ gen_write_stubs.py nhận nhầm Save() là đã dựng — không tin OPEN của nó quy trình G388
CÔNG TẮC GHI ĐÃ BẬT trong overlay học (G392): cổng mở rộng mở trên cả 4 trục (callee 0/11, getter inline 0/6, field 0/109, round-trip 0); mô hình round-trip trên save Stage 3-3: theo đồng hồ SOT 126 identity / 2 dẫn xuất, theo bản học 127 identity / 1 dẫn xuất (LastSaveTime) / 0 phá huỷ (LastTrustedUtc giữ nguyên vì TrustLevelNone dưới R1); Save() = Save(CreateFromCurrentState()), _saveAllowed không đổi. Kỳ vọng round-trip thiết bị: chỉ LastSaveTime, TotalPlayTime, Money/Money_Accum, (SavedOffsetSec nếu có reconcile) được khác; DailyGun_Week_Key phải identity disasm-proven + quy trình G392
Build thiết bị đầu tiên có ghi sống (batch 19) — hai triệu chứng, cả hai đã hiểu (G399): (b) "không đọc được save" KHÔNG phải codeSaveSystem.cs chỉ khác batch 18 ở thân Save(); Load/TryLoadFrom/JsonData_Decoding/SavePath byte-identical. Log: "main save broken" ở t+4.9 s, lần ghi đầu t+16 s, file 60 s byte-identical với file đẩy vào ⇒ quy trình test: trên cài mới, adb push tạo files/saves/ dưới user shell trước khi app tạo (batch 15-18 là nâng cấp trên thư mục app đã tạo). Quy trình mới: cài mới → chạy 1 lần (app tạo thư mục) → force-stop → push → ls -la + md5 trước khi chạy → chạy lại; dòng "main save broken" phải vắng. (a) autosave ném trên state mới = Shop_manager.ProcessedTxnIds (static +0x40) null: SOT cấp nó + PurchaseHistory (+0x48) trong Shop_manager..cctor (0x2B36F9C @0x2B372FC / 0x2B37324) cùng CurrencyFormat (+0x68, bảng 23 tiền tệ → định dạng)ZeroDecimalCurrency (+0x70: KRW / JPY / VND / IDR / CLP / HUF); overlay cctor chỉ có Dia_FirstBuy_Used/DiaRewards ⇒ blocker codegen (đã hoàn tất, G400). Cho tới đó: autosave save mới ném mỗi 10 s và không ghi gì (fail-safe); save đã nạp không ảnh hưởng build-measured + disasm-proven G399
BATCH 20 — GAME CHƠI ĐƯỢC (G405): không còn "main save broken" (quy trình G399); autosave GHI trên đường save đã nạpNoSaveFile.json + .bak (10 020 B, xoay .tmp → .bak, chủ sở hữu là app); HOME không tạo thêm lần ghi. File giữ nguyên chưa diff. Residual còn lại (G445): (1) autosave trên save mới ném ở PlusPack.SaveData_return ← CreateFromCurrentState (null kế tiếp sau khi sửa List); (2) offline: RecordGain("offline:5,4h") rồi "routine is null"StartCoroutine ← EvaluateRoutine ⇒ khoản trả huỷ, Money delta 0, không panel (không còn mất tiền); (3) đuôi ContinueLoad vẫn chưa chạy (vé Trip / DiaPig tick năm 1) = hệ quả của (2) (continuation chưa được gọi); (4) tutorial Challenge → GunOpen ở t = 14 s, chưa Done build-measured G405
Nghiệm thu batch 20 — đường GHI trên save đã nạp (G414): VERIFIED. Round-trip 122/128 identity; xoay .bak xác nhận (lần ghi 10 s trước); đọc lại sạch file vừa ghi (0 "main save broken"), không gì bị xoá qua 5 snapshot; khác biệt đúng kỳ vọng: Money, Money_Accum, LastSaveTime; SavedOffsetSec lệch 1 ULP định dạng (vô hại) build-measured (acceptance) G414
Hai LỖI THẬT còn lại, ghi thẳng (G414): (1) thu nhập tiền ≈ ×300 SOT — ~3.2 M/s trên bản học vs ~8.6 K/s SOT từ cùng file (Money 2.50 M → 21.67 M trong 60 s; Money_Accum 41 M → 234 M) ⇒ một hệ số base-100 được áp thô (×100 / ×10 000) ở đâu đó trong tích tiền (MoneyValue_return: Upgrade / Rebirth / Skin Money_Accum / Pet / Relic / buff) hoặc ở Get_Money / tiền của quái — đang điều tra; (2) TotalPlayTime không tăng (SOT +129 s) ⇒ bộ tích thời gian chơi (Gamemanager.Update / AutoSaveRoutine) còn stub. Tự mua offline ([36,36,35] → [41,41,41] rồi [42,41,41] sau khe 6 phút) khớp dự đoán harness ⇒ nhiều khả năng đúng (sàn 30 s ⇒ SOT cũng chạy offline trên khe 6 phút); lỗi là độ lớn tiền, không phải tự mua build-measured G414
Mối nguy danh tính (G414): đọc lại 32/38 — 6 lệch đều do ContinueLoad chưa chạy trên file 5 h (null ShowAfterFrame): vé Trip, DiaPig tick, SpeedRun_Stage_Now (file 1 → 0), và bộ ba PlayFab RỖNG trong bộ nhớLogin chạy với credential rỗng = nguy cơ đúc lại danh tính (G506); chưa ghi đè trên đĩa vì lần ghi phát lại dữ liệu đã nạp. Phản chứng: trên khe 6 phút continuation chạy trước và NRE đến 3 ms sau ⇒ vị trí coroutine so với continuation phụ thuộc kết quả offline — codegen phải đối chiếu lại thứ tự state với binary (G334 ghi RunCoroutine → Loading_Off → ShowResult) build-measured + phân tích G414, G409

| 6 getter inline là stub (G389 — ĐÃ ĐIỀN đợt 9, G391) — gốc rễ thật của residual ngày tháng: Buff_manager.MoneyBuffTime/PowerBuffTime ⇒ 0f (0x2AE4170/78), FreeCash_manager.LinkRewardReceived/RewardAdRemoved ⇒ false (0x2A6307C/0x2A62158), Time_manager.LastTrustedUtc getter null / setter no-op (0x2AD016C), Time_manager.UtcNow ⇒ default (0x2AD32BC). UtcNow = năm 1 đầu độc SanitizeYear: mọi ngày 2026 lệch ≥ 400 năm ⇒ bị dịch −543 ⇒ bị loại ⇒ default — đó là nguyên nhân thật của residual (1) batch 13 (không phải guard Adventure) và gần chắc của G437 (NewUser_Reload: Today.AddDays(−1) trên DateTime mặc định ⇒ AddTicks ném, InitNewGame dừng giữa chừng trên save mới). Ghi save với UtcNow này sẽ phá 8 field ngày | disasm-proven (đường đọc) | G389, G386 | | Bỏ sót phía ĐỌC trong ContinueLoad (phát hiện nhờ mô hình round-trip, không phải diff nghiệm thu): Lv_Now (dec:418), Mission_StageClear/Rebirth/PlayMine_Value (dec:593-595), Mission_is_Luxury (dec:596), vòng Mission_ExpGet/_Luxury (dec:608-651) — bản đồ 35 field ở §2.8 thiếu các hàng này; một lần ghi trung thực sẽ xoá chúng (StageClear_Value 10 → 0 trên file thật). Phải sửa trước khi bật Save()đã restore (G391): Monster_manager.Lv_Now, Mission StageClear/Rebirth/PlayMine_Value, Mission_manager.is_Luxury (static, cặp với is_Vip), hai vòng Mission_Exp_Get/_Luxury (chặn theo cả hai độ dài) | disasm-proven | G389, G391 | | Buff bị KẸP khi nạp: Buff_manager.LoadBuffTimes = NEON Clamp(x, 0, 900) (vcltz xoá âm, vbsl/vcgt chặn ở 0x44610000 = 900f) ⇒ save cũ/sửa tay không thể hồi sinh buff vô hạn; trần 15 phút được áp ngay lúc nạp. Bẫy thứ tự tham số: ContinueLoad đọc PowerBuffTime@0x2AC trước MoneyBuffTime@0x2A8 rồi gọi LoadBuffTimes(money, power) — đảo so với thứ tự offset | disasm-proven | G291 | | Danh tính khách được ghi TRƯỚC khi đăng ký: Rank_manager.Login (0x2ABC454) — không có credential thì sinh "user_" + Random.Range(10000000, 99999999) + "@guest.com", mật khẩu Guid.NewGuid().ToString("N").Substring(0, 16), tên "User_" + n, và ghi cả ba vào save tại 0x2ABC7A8/0x2ABC7FC/0x2ABC81C — TRƯỚC lời gọi RegisterPlayFabUser (0x2ABC91C), không nằm trong callback thành công ⇒ danh tính ổn định qua các lần chạy offline, lần sau đi nhánh LOGIN. Trong bản restore hai nhánh đi qua ServiceLocator.Login.SignIn (R1) | disasm-proven | G322, G323 | | DiaPig.Init (0x2B278AC) cố ý bất đối xứng: save nullDia_Now = 0 và chỉ neo lại đồng hồ tick, bỏ qua Tick_Accumulate ⇒ save bị xoá/thiếu không thể đúc dia offline; chỉ nhánh restore mới tích luỹ. Timestamp parse DateTimeStyles.RoundtripKindchuyển về UTC (không giả định) ⇒ chuỗi mang offset không thể thổi phồng cửa sổ. Đính chính (G377): SOT không đóng dấu lại _lastTickTime khi nạp — giữ giá trị file (UTC hoá, chỉ kẹp nếu ở tương lai = chống vặn giờ tới, 0x2B27994); thứ được đóng dấu lại lúc nạp là _lastTickMono (mốc đồng hồ đơn điệu gate việc tích luỹ). Nghiệm thu DiaPig_LastTick = giá trị file là đúng | disasm-proven | G327, G377 | | Fish_Tank.ReloadAll (0x2B0142C): tank đang inactivehoãn qua _pendingReload (xếp cá cần RectTransform sống; làm khi ẩn sẽ ra vị trí rác); refill bị chặn bởi cả số cá trong Fish_manager.Inventory lẫn cỡ pool; FindEmptyPosition trả điểm cho SetPosition; SortHierarchyByGrade; text "{0}<size=25>/{1}" với trần 20; rồi StartAutoMergeChain | disasm-proven | G327 | | Chép phòng thủ ở StepUp / DailyGun / PlusPack: StepUp.LoadFromSaveData (0x2B3AFF4) chép tối đa 5 cờ mua, kẹp theo cả độ dài mảng lưu lẫn mảng mình; DailyGun (0x2B266D8): Week_Key rơi về "" (không phải null) để Week_Refresh so sánh không cần null-check, tối đa 3 đếm mua; PlusPack (0x2B2C2F4): thay Purchase_dateTime bằng new DateTime[3] trước khi chép ⇒ mảng lưu ngắn/null để lại default(DateTime) chứ không phải giá trị cũ. Ctor: StepUp.Purchased = new bool[5], Open_Place = "banner_weapon"; DailyGun.Purchase_Counts = new int[3], Week_Key = "" | disasm-proven | G309 | | Cờ mute của Setting được đóng gói: ContinueLoad ghi BGM_Mute@0x1D8 → Setting.instance+0x20 bằng một byte store, rồi SFX_Mute + Vibration_Mute@0x1D9/0x1DA → +0x21/+0x22 bằng một word store ⇒ ba cờ không độc lập về offset nguồn. Setting.UI_Reload (0x2ACD3E0): mỗi toggle là một CẶP object bật/tắt theo phủ định cờ mute, rồi ApplyBGMMute; hàng đăng nhập gate theo nền tảng trạng thái đăng nhập (Android → nút Google, iOS → Apple; DataLoad/DataSave chỉ khi đã đăng nhập); spinner luôn tắt | disasm-proven | G329, G311 | | Setting.IsSignedIn (0x2ACD654) đọc PlayGamesPlatform.Instance.IsAuthenticated() — trong bản restore đi qua ServiceLocator.Login.IsSignedIn (Null ⇒ false = không có phiên, không phải câu trả lời bịa) | disasm-proven + quyết định R1 | G312, G320 | | Gun_manager.AD_Gun_OnLoaded chỉ VÔ HIỆU HOÁ (bỏ nếu Owned_Gun_list[slot] != AD_Gun_Num, hết hạn khi RemainSec <= 0) — không nhánh nào đúc được súng quảng cáo ⇒ R1-an toàn. FreeCash_manager.LoadFromSaveData xoá cờ tường minh khi save null. Tutorial_manager.SetCleared gọi StopAllCoroutines trước CompleteTutorial (bắt buộc — tránh MoneyCheckLoop gọi lại SetStep) | disasm-proven | G291 | | ServerConfig_manager.LoadFromSaveData (0x2AC1FA8..0x2AC265C): restore InstallDateUtc (DateParseSafe "yyyy-MM-dd"), InstallDateSource, RewardAdCnt, PostLastDate, CachedConfigJson (mỗi cái ?? ""); phát hiện đổi version build → _versionChanged + log (xử lý ở nơi khác); parse JSON đã cacheAppsFlyerInit.ApplyServerSegment; thiếu ngày cài → TryGetNativeInstallDate hoặc Time_manager.UtcNow. Không có fetch mạng trên đường này ⇒ tái hiện nguyên văn. Nhãn nguồn ngày cài: LEGACY_USE_REAL_INSTALL_DATE ? "estimated" : "update" — chốt bằng CMP W8,#0 / CSEL …,EQ | disasm-proven | G326, G327 | | Khối Adventure trong ContinueLoad: Adventure_Map_Info_list không có LoadFromSave — SOT chép is_Enter / Enter_dateTime từng phần tử trong vòng lặp lồng, chặn ở cả hai cấp (số loại map, rồi số map mỗi loại) | disasm-proven | G330 | | Shop_manager .cctor (0x2B36F9C, 1936 B): cấp Dia_FirstBuy_Used = new bool[5] (static+0x28) tại 0x2B36FF4DiaRewards; Is_Dia_FirstBuy(int) (0x2B2D458) = đúng ba phép thử của binary: index < 0 ⇒ false; Length <= index ⇒ false; return !Used[index] — không có null-guard (CBZ thẳng vào helper ném) ⇒ thiếu .cctor là save mới NRE ngay lần đầu chạm UI mua-lần-đầu | disasm-proven | G350 | | Time_manager.Init (0x2AD12F4): cấp Ledger nếu null, rồi đối chiếu trust (so TrustLevel, neo offset đã lưu, reconcile máy chủ). Bản restore chỉ giữ cấp Ledger (vì Load và gain-check dereference nó); tầng trust cố ý không tái hiện (anti-cheat, ngoài phạm vi) | disasm-proven (đọc) / quyết định lane (không tái hiện) | G321 | | Gamemanager..cctor (0x2A517A4, 1160 B) mang 6 khởi tạo field: TimeCheckUrls = {google, cloudflare, bing, apple}, GainRetryDelaySec = {15f, 60f}, _lastGainCheckRealtime = float.NegativeInfinity (lần kiểm đầu luôn tới hạn; 0 sẽ âm thầm trễ một chu kỳ), _moneyCache = new Dictionary<string, InfVal>(), _moneyCacheKeys = new Queue<string>(), _textCooldowns = new Dictionary<Vector2Int, float>(). CleanupTextCooldowns (MoveNext 0x2A51D58): Time.time, ngưỡng cũ 2 s, quét mỗi 5 s | disasm-proven | G319 | | Bộ field cần so sánh khi nghiệm thu save-load (ưu tiên theo mức "sai thì im lặng và đắt"): tiến độ (Stage_Now, Level_Now, Stage_Value_Max, SpeedRun/Boss Stage_Now, Monster Wave_Now) · kinh tế (Money, Money_Accum, Ruby, Dia, PetCoin, Upgrade_Lv[3], Rebirth_Upgrade_Lv[3], Rebirth_Count) · quyền lợi (6 cờ gói Shop, PurchaseCount, Dia_FirstBuy_Used[5]) · timer có thể cấp thưởng (buff money/power, vé Trip + mốc nạp, DiaPig Dia_Now + tick cuối, Mine TicketUse_Count, Fish FreeGachaCount) · danh tính (PlayFab_Email/Password/DisplayName phải giữ nguyên — lệch nghĩa là đã đúc mới thay vì restore) · cờ (TutorialCleared, is_reviewed, mute ×3) | quy trình nghiệm thu | G329; FIELD_MAP mục "Batch-9 acceptance" |

35 restore trực tiếp của ContinueLoad (gộp theo manager; @off = offset trong SaveData; nguồn FIELD_MAP, mỗi hàng có dòng Hex-Rays):

Manager đích Field SaveData (@off) → đích
SpeedRun_manager SpeedRun_Stage_Now@0x198 → Stage_Now+0x30
Buff_manager MoneyBuffTime@0x2A8, PowerBuffTime@0x2AC → LoadBuffTimes(money, power) (kẹp 0..900)
DiaPig diaPig@0x1D0 → DiaPig.Init(data.diaPig)
Setting BGM_Mute@0x1D8 → +0x20 (byte); SFX_Mute + Vibration_Mute@0x1D9 → +0x21/+0x22 (word)
Rank_manager PlayFab_Email/Password/DisplayName@0x1E8/0x1F0/0x1F8 → +0x28/+0x30/+0x38
Tutorial_manager TutorialCleared@0x331 → SetCleared() khi true
DailyCheck LastCheck_Date@0x200 (DateParseSafe), CheckNum@0x208, TotalCheckNum@0x20C → First_reload
Fish_manager / Fish_Tank Free_GachaDate@0x218, FreeGachaCount@0x220 → +0x80, FishInventory@0x228 → Inventory rồi Fish_Tank.ReloadAll
Mine_manager TicketCharge_Date@0x238, TicketUse_Count@0x230 → +0x58, Best_Reward@0x240 → +0x68
Boss_manager / Monster_manager Boss_Stage@0x244 → Stage_Now+0x20; Wave_Now@0x248 → Wave_Now+0x20
Adventure_manager Adventure_Maps@0x2B0 (mảng map, chép lồng), Currency_Core@0x2B8, Lv@0x2C0, Ticket_Now@0x2C8 → +0x60, LastTicketCharge_Date@0x2D0 (DateParseSafe)
Mission_manager Reward_Lv@0x250 → +0x48, TreeCount_Value@0x258 → +0x50, is_SeasonStart@0x268 → +0x65, is_Vip@0x269 → static+0x0, SeasonEnd_Date@0x270, MissionStart_Date@0x278, ExpGet@0x280, ExpGet_Luxury@0x288, NormalReward_Get@0x290, EpicReward_Get@0x298, Season_Num@0x2A0 → +0x60, is_GunSeason@0x2A4 → +0x64
Review_manager is_reviewed@0x330 → +0x20

2.9 Trip / Adventure — vé, XP, bảng mở khoá (đợt 3)

Claim Mức bằng chứng Nguồn / provenance
Adventure_manager.Ticket_Charge_Interval (0x2ADAC44) = TimeSpan.FromHours(4) — xác nhận tĩnh cho quan sát "1 vé / 4 giờ" của probe. Stub default(TimeSpan) = 0 từng gây chia cho 0 ở hai method sau (Ticket_Refresh) — lỗi mặc định giá trị nổ ở nơi dùng, không ở nơi khai báo disasm-proven G321
Quy tắc hai mốc của Ticket_Refresh (0x2ADAC98): dưới trần — mốc nạp tiến đúng số khoảng 4 h nguyên đã tiêu, phần dư mang sang; tại trần — mốc nạp đặt lại về NOW, phần dư bị bỏ ⇒ vắng mặt dài không thể tích vé quá Ticket_Max disasm-proven G321
Adventure_manager.Exp_Balance_Reload (0x2ADAABC) sinh bảng XP bằng công thức: Exp_balance[i] = (20 + 3·i)·i + 50, i = 0..1450, 73, 102, 137, 178, 225, 278, 337, 402, 473, 550, 633, 722, 817, 918 (bước 20, tăng 3 mỗi cấp — bậc hai) disasm-proven (thân hàm) G311
✅ ĐÃ GIẢI (G361): mảng scene 10, 60, 100, 130, 180, 230, 280, 340, 400, 470, 550, 630, 720, 820, 920 là bảng SỐNG. Main.unity serialize Exp_balance dưới dạng hex blob (dòng 20323823) — tìm kiếm dạng danh sách YAML đã bỏ sót nó. Exp_Balance_Reload chỉ là fallback lười: hai điểm gọi duy nhất nằm trong ContinueLoad (0x2AC2F9C / 0x2AC3000), mỗi điểm được guard Exp_balance == null \|\| Length == 0. Bản dựng lại từng gọi nó vô điều kiện (sẽ ghi đè đường cong designer mỗi lần nạp) — đã sửa; kèm Last_Ticket_Charge_dateTime = Time_manager.Now (0x2AC2FE8) disasm-proven + scene-YAML (hex blob) G361; đóng G901
Adventure_Unlock_Balance = int[5] {0, 1, 3, 6, 10} (static, .cctor 0x2ADD174) và Adventure_Duration_Balance = int[5] {1, 2, 4, 8, 16} (instance, .ctor 0x2ADD07C) — xác nhận tĩnh (blob SHA MATCH) cho giá trị probe đã đọc từ bộ nhớ; hai nguồn độc lập trùng khớp metadata-proven (blob) + runtime-verified (live) G353 (TABLE.md hàng 1-2); PROBE_RESULT.md §3
Fish_manager.GachaChances ctor (0x2B01D08) = float[4] {40, 30, 20, 10}nhưng scene Main.unity:20930271 serialize [60, 30, 10, 0]. Field public serialize ⇒ giá trị scene thắng lúc chạy; ctor chỉ là mặc định khi AddComponent. Bài học: với field serialize, blob ctor không phải giá trị ship metadata-proven (blob) + scene-YAML G353 (TABLE.md hàng 6); doc 11 §6.4

2.10 Chế độ Mine (đào quặng) — luật đọc từ binary (đợt 9, G393)

Claim Mức bằng chứng Nguồn / provenance
Màn Mine dài 60 s (GameTime_Max = 60f ctor; Main là 180 s); orePerFrame = 100 (đặt quặng rải theo frame); CatSpawnPos (−100, 0, 0); quặng đặt bằng nhiễu Perlin trong vành bán kính 8..20 quanh tâm 0 (innerRadius 8, outerRadius 20; noiseScale / noiseThreshold ctor 0.15 / 0.45, scene 0.085 / 0.517 — scene thắng) disasm-proven + scene-YAML G393, G385
Cổng vé: Ticket_MaxCount_return từng là return 0 ⇒ mine không bao giờ vào được; nay đọc trần vé thật; Ticket_Reload / Game_Enter / Enter_Cor / Game_Start / Game_Cor / Game_End / Ore_Destroyed / PlaceOres / ClearOres đã dựng disasm-proven G393
Quirk SOT #1: Game_Cor gieo đồng hồ đếm ngược từ GameTime_Max_return đã cộng PlusPack, nhưng đưa cho thanh trượt max thô 60 s ⇒ một lượt có boost bắt đầu với thanh tràn quá 100 % disasm-proven G393
Quirk SOT #2: PlaceOres chỉ xáo 100 mục đầu của pool ⇒ chỉ 100 quặng đầu nhận sprite thứ hai (1_dia, 10 dia) — phần còn lại luôn là quặng thường disasm-proven G393
Bonus_AD_Show / SweepBonus_AD_Show để lại cho R1 (route qua IAdService) quyết định R1 G393
Điểm số Mine = Dia (doc 11 §6.2); Lose_PetCoin/Lose_Dia xem doc 01 §2

2.11 Chế độ Play (SpeedRun) — luật đọc từ binary (đợt 11, G406 / lane M2)

Claim Mức bằng chứng Nguồn / provenance
Ctor (0x2B42160): GameTime_Max = 60 s, CatSpawnPos (−100, 0, 0), treesPerFrame = 30, bán kính vòng 20 / 95 / 3 / 2 (scene thắng: 22 / 70 / 3 / 2); .cctor (0x2B42214) Reward_Value = 30 — bị bỏ rơi ⇒ mọi lần clear trả 0 dia disasm-proven + scene-YAML G406
Số cây = clamp(15·Stage + 20, 20, 190) (190 = pool Tree_SpeedRun, doc 11); HP / tiền = đường cong stage của Tree_manager × bậc 1 / 1.8 / 3 (bậc = Random.Range(0, 3) — không có bậc 3); bố cục vòng tròn (ring); map dùng lại Tree_manager.Ground_sprite_list[Stage_Now % 10] disasm-proven G406
Thắng = cây cuối đổ → Game_Win: Stage_Now++ (chính là thứ tutorial CheckSpeedRunClear poll) + 30 dia (×4 khi auto), quảng cáo bonus +90; thua = hết 60 s → Game_Fail disasm-proven G406
Quét (sweep) tự động khi Power_SpeedRun_return ≥ GetStageHealth(stage) × n × 193 / 100 / 24 (ToPrecision 5) — ngưỡng "mạnh gấp ~8 lần tổng máu" disasm-proven G406
R1: rewarded → ServiceLocator.Ads.ShowRewarded("SpeedRun_Bonus"); interstitial → ShowInterstitial(null) quyết định R1 G406
SpeeRun_Stage_Power_return = GetStageHealth(Stage) × 5 (Tree_manager, §2.3 doc 05) disasm-proven G395

2.12 Chế độ Raid (ẩn, chưa phát hành) — luật đọc từ binary (G406 / lane M2)

Claim Mức bằng chứng Nguồn / provenance
Ctor (0x2AFE24C): Gun_List / Gun_Index_List = {−1, −1, −1}, CatSpawnPos; .cctor (0x2AFE318) Epoch = 2024-01-01 UTC disasm-proven G406
Boss của tuần = floor(số tuần kể từ Epoch) mod 5 — 5 boss worm xoay vòng theo tuần disasm-proven G406
Súng được buff theo tuần (.rodata 0x105CA70): 1 → EXPLOSIVE, 2 → LASER, 3 → MISSILE, 4 → BLAST_SNIPER, còn lại → PIERCE disasm-proven G406
Loadout 3 ô, tối thiểu 2 súng trang bị; màn 30 s; boss hồi sinh sau mỗi lần giết với Health_Balance[Score_Now]; Score ≥ Score_Max10 dia (20 khi auto); kết thúc chỉ theo đồng hồ, panel 2 s, tự thoát; Score_Now không reset quá Score_Max (nguyên văn) disasm-proven G406
R1: banner Raid Show/Hide chưa route đượcIAdService thiếu ShowBanner/HideBanner (codegen thêm, Null no-op) quyết định R1 G406
Nhắc lại: không có nút vào Raid trong 27 nút HUD, mảng scene rỗng ⇒ nội dung chưa phát hành (doc 11 §5.5); luật trên là code có sẵn, không phải nội dung người chơi thấy VERIFIED (negative) G153, G177

2.13 Chế độ Hunt (Monster_manager) — luật đọc từ binary (đợt 12, G408 / lane M1)

Claim Mức bằng chứng Nguồn / provenance
Ctor/cctor: Clear_Reward = 30, MonsterCountPerLevel {90, 120, 160, 200, 240}, SpawnIntervalPerLevel {0.35, 0.2, 0.17, 0.12, 0.08} (chỉ số = level 0..4 trong wave — đóng UNKNOWN §5), _cachedHealthWave = −1, CatSpawnPos; Awake cấp Spawned_Monster (đóng G223) disasm-proven G408
Vào chế độ: tắt is_gaming, Tree_manager.Game_Exit, EcoMode_Off, HP_Now = HP_Max với HP_Max scene = 1 ⇒ MỘT lần rào bị xuyên = thua, UI_type.Monster, warp mèo, camera ortho 45 (40 khi thoát), màn che 2 s, một frame, panel giải thích disasm-proven + scene-YAML G408
Mỗi level: giết MonsterCountPerLevel[Lv] con, spawn mỗi SpawnIntervalPerLevel[Lv] s (0.04 s khi < 3 con sống, 0.1 s khi rỗi), đợt xâm nhập xếp hàng lại; máu = Health_Balance[Wave] scene (500, 1 500, 4 500, … ×3 ToPrecision(2) ngoài bảng); xác suất quái to = Lv × 0.2máu ×3; 5 level / wave, splash 2 s disasm-proven + scene-YAML G408
Thưởng clear level = (int)(PlusPack.Reward_Mult_Monster × 30) PetCoin, ×2 dưới Is_RewardBonus_Auto; kết thúc chỉ khi Game_Fail (HP 0) hoặc bỏ cuộc; thoát nộp điểm Wave × 10 + Lv disasm-proven G408
Quét (sweep) cần gói PlusPack Auto; ngưỡng = WaveHealth × (100 + 40·lv) / 100 × 150 / (interval × 100), tối đa 100 level disasm-proven G408
Quirk: đĩa HP vẽ bằng Shapes.Disc.set_AngRadiansEnd với giá trị độ (không phải radian); vùng spawn = ellipse clamp(aspect × 60, 30, 60) × 60, jitter ±5, xoay 45° disasm-proven G408
R1: rewarded → ServiceLocator.Ads.ShowRewarded(AD_Type.Monster_Bonus) (Granted → Bonus_Get); interstitial Hunt = 4 → ShowInterstitial(null); banner cần IAdService.ShowBanner/HideBanner. R13 blocker: PlusPack.Reward_Mult_Monster/Boss trong overlay = 0 (SOT 0x2B2B588/0x2B2B720 = Is_Active(PlusPack_Type.Reward) ? 2f : 1f; Is_Active = slot ≠ default && UtcNow < slot.AddDays(7)); Shop_manager.Is_RewardBonus_Auto = static +0x09 ‖ FreeCash.RewardAdRemovedthưởng cả hai chế độ = 0 cho tới khi codegen điền quyết định R1 + blocker G408

2.14 Chế độ Boss — luật đọc từ binary (G408 / lane M1)

Claim Mức bằng chứng Nguồn / provenance
Reward_Value = 150, GameTime_Max = 60; boss spawn lúc vào với Health_Balance[Stage] scene (50 000, 250 000, 1.2 M, … ×5 ngoài bảng); skin = Stage % 9 (9 rồng xoay) disasm-proven + scene-YAML G408
60 s, ×2 với gói PlusPack Time — nhưng slider max giữ 60 thô ⇒ cùng quirk tràn > 100 % như Mine (§2.10); hết giờ → Game_Fail disasm-proven G408
Boss.Die_Reload → Game_Win: Stage_Now++ TRƯỚC khi nộp leaderboard; thưởng (int)(Reward_Mult_Boss × 150) dia, ×3 dưới Is_RewardBonus_Auto, quảng cáo bonus +2×; Loading_Then_Start là dead code disasm-proven G408
Quét (không cần PlusPack): Power_Boss ≥ Health(stage) / max(1, round(timer × 70 / 100)) ⇒ clear một stage disasm-proven G408
R1: rewarded → ShowRewarded(AD_Type.Boss_Bonus); interstitial Boss = 3; Rank_manager.SubmitBossScore R1-out quyết định R1 G408

3. Mô hình hành vi

3.1 Bộ khung: 35 singleton, không có DI

public class X_manager : MonoBehaviour
{
    public static X_manager instance;          // field static trần
    private void Awake() { instance = this; }  // đăng ký, không kiểm trùng
    public void Balance_Reload() { … }         // sinh bảng số — NHƯNG LÀ DEAD CODE (xem dưới)
    public int[] SaveData_return() { … }       // xuất state
    public void LoadData(SaveData data) { … }  // nạp state
    public void UI_Reload() { … }              // vẽ lại UI từ state
}

Đây là kiến trúc đơn giản đến mức thô — không dependency injection, không service locator, không event bus tổng. Mọi hệ thống gọi thẳng X_manager.instance.Foo().

Vì sao nó hoạt động ở đây: game có đúng một scene (doc 08). Mọi manager tồn tại từ lúc load đến lúc thoát. Không có vòng đời phức tạp, không có scene bị unload, nên rủi ro lớn nhất của singleton (tham chiếu chết) không tồn tại.

Vì sao nó vẫn nguy hiểm: khớp nối chéo là toàn phần. Cặp Tree.Die_ReloadTree_manager.Tree_Destroyed chạm vào Particle_Polling, Tree_manager, Gamemanager, Mission_manager, và AppsFlyerInitnăm singleton cho một cây đổ (đợt 3 tách được hai hàm mà IDA đã gộp: Die_Reload 2 dòng, Tree_Destroyed 864 B — G346). Đây chính là lý do lane codegen không thể ship Gamemanager dạng overlay 70 method: nó kéo theo cả đồ thị.

Bốn quy ước method lặp lại ở hầu hết manager là thứ đáng học hơn cả bản thân singleton:

Quy ước Vai trò
Balance_Reload() sinh bảng số từ công thức — là công cụ Editor, không chạy lúc runtime
SaveData_return() / LoadData(SaveData) ranh giới lưu trữ, một cặp cho mỗi hệ thống
UI_Reload() / Value_Reload() / First_Reload() vẽ lại UI từ state; UI không giữ state
X_return() static phơi một giá trị dẫn xuất cho hệ thống khác đọc

Sự tách bạch First_Reload() (thiết lập một lần: sprite, tên, chỉ số) và Value_Reload() (cập nhật thường xuyên: số, tiến độ, trạng thái khoá) xuất hiện ở khắp nơi và có bằng chứng runtime rõ: Mission_Reward_Item.First_Reload 200× vs Value_Reload 150×; Skin_Item_UI.First_Reload 25×. Đây là mẫu tối ưu UI đơn giản và hiệu quả.

3.2 Cân bằng: hàm sinh là công cụ Editor, mảng bake là nguồn sự thật

Mọi manager có tiến trình đều có một Balance_Reload() sinh bảng số từ vài hằng số:

Upgrade_manager.Balance_Reload()  RVA 0x2B4A02C   -> Money/Power/Speed/Price_balance  (10 001 phần tử)
Rebirth_manager.Balance_Reload()  RVA 0x2B21CB4   -> Value_balance ×3 + Price_balance (20 001 phần tử)
Relic_manager.Balance_Reload()    RVA 0x2ADFE20   -> Damage/Money/CriDmg_Balance
Skin_manager.Balance_Reload()     RVA 0x2B3F2EC   -> Price_Balance
Tree_manager.Balance_Reload()     RVA 0x2B447EC   -> Stage_Health/Money_Balance
Gun_manager.Balance_Reload()      RVA 0x2B0AB4C   -> StartCoroutine(Request_Sheet())  [Google Sheet TSV]

Nhưng không cái nào chạy lúc runtime. CodeRefsTo rỗng cho cả ba cái đã kiểm (Upgrade, Rebirth, Gun); không có string literal (loại trừ Invoke/StartCoroutine theo tên); không có m_MethodName: Balance_Reload trong scene (loại trừ UnityEvent). Không có [ContextMenu] trong dump ⇒ nhiều khả năng chúng được gọi từ một custom Editor (Assembly-CSharp-Editor, không có trong build) rồi bake kết quả vào scene.

Bảng thật lúc chạy nằm ở Main.unity (số dòng ghi trong [F001: scene_balance_tables.json]), và chúng được sinh bằng lặp có CẮT với hằng số là double nhị phân — lệch tới −10.2 % ở lv 50 so với dạng đóng. Chi tiết đầy đủ, bảng sai lệch và bài học: 09-prestige-rebirth.md §3.2.

Kiểm chứng độc lập: Speed_balance[46] = 468, Money_balance[46] = 3 120 (3.12a), Price_balance[46] = 1.34×10⁸ (134b) — cả ba khớp ảnh chụp save thật của người dùng ở Lv.46.

Hệ quả cho một bản restore: giữ nguyên mảng serialize; đừng gọi Balance_Reload trong Awake. Gọi nó với một InfVal khác phiên bản (làm tròn thay vì cắt, hoặc 1.38 thập phân) sẽ sinh ra một nền kinh tế khác SOT.

Hệ quả cho một framework: nếu bảng được bake, bảng hợp đồng — kèm checksum, và hàm sinh phải tái tạo được nó trong CI. Nếu không, hàm sinh sẽ âm thầm trôi khỏi dữ liệu đã ship.

Ngoại lệ đã kiểm: Tree_manager.Degree_Health_Balance = {1, 1.8, 3, 35}Degree_Money_Balance = {1, 1.8, 3, 100} được tính trong .cctor (0x2B46094) nên chúng thực sự chạy lúc runtime. (Thứ tự theo enum degree: HYPOTHESIS.)

3.2b Ba lớp cổng — và vì sao phải tách chúng ra

Người chơi gặp ba hệ thống khoá độc lập, không phải một. Chúng chồng lên nhau và được xác nhận bằng hai phiên probe runtime độc lập:

Lớp 1 — TUTORIAL            Tutorial_manager mở dần các nút HUD
   save mới (Stage_Value_Max = 0):  CHỈ SpeedRun_Btn + Setting_Btn bật
   Shop / Gun / Skin / Mission / DailyCheck / Auto / Eco / DiaPig / Buff  -> TẮT
   và KHÔNG cái nào được Contents_Lock_Reload chạm tới

Lớp 2 — TIẾN ĐỘ            Tree_manager.Contents_Lock_Reload  (0x2B4441C)
   so Stage_Value_Max với 20 / 30 / 50 / 90 / 110 / 110 / 340
   Stage_Value_Max = max(Level_Now + 10*Stage_Now), CHỈ TĂNG, không bị Rebirth reset

Lớp 3 — SEGMENT / LIVEOPS  ServerConfig_manager (segment) + trạng thái SDK riêng của từng bề mặt
   đơn vị đo: MaxStageDisplay = Stage_Value_Max / 10 + 1     <- KHÔNG phải Stage_Value_Max thô
   OfferWall      >= 10          (bật vĩnh viễn)
   Interstitial   >= 12          (chỉ 2 điểm đặt: Rebirth, Mine)
   FreeCash       15 .. 35       (CỬA SỔ HAI ĐẦU — tự ẩn khi vượt 35)
                                 + cổng thứ hai: AlmediaLinkSDK._almediaStatus in {1,2}
                                   (mặc định 3 = NotAvailable -> máy fresh không bao giờ thấy)
   segment = 0    -> tắt sạch

Ba lớp trả lời ba câu hỏi khác nhau:

Lớp Câu hỏi nó trả lời Ai điều khiển Reset khi rebirth?
Tutorial Người chơi đã học tới đâu? Tutorial_manager không
Tiến độ Người chơi đã đi xa nhất tới đâu? Stage_Value_Max (high-water mark) không
Liveops Người chơi này thuộc nhóm nào, và có nên chào mời gì không? ServerConfig (segment) + trạng thái SDK từng bề mặt không áp dụng

Vì sao đây là bài học framework quan trọng: trộn ba lớp này vào một cờ isUnlocked là một trong những sai lầm tốn kém nhất trong game live-service. Chúng có chủ sở hữu khác nhau (designer / system / growth), nhịp thay đổi khác nhau (không bao giờ / theo tiến độ / theo cấu hình từ xa), và hậu quả khác nhau khi sai (người chơi bối rối / mất nội dung / mất doanh thu hoặc spam).

Đáng chú ý: lớp 1 vô hình với phân tích tĩnh. Nếu chỉ đọc Contents_Lock_Reload, bạn sẽ kết luận Shop/Gun/Skin mở ngay từ đầu — sai. Phải chạy một save mới thật mới thấy chúng tắt.

FreeCash là ví dụ duy nhất của cửa sổ hai đầu (15 ≤ x ≤ 35): một lời chào mời chỉ xuất hiện ở khoảng giữa game — đủ lâu để người chơi có động lực, đủ sớm để họ chưa tự lo được. Đây là một mẫu tuning đáng chép: không phải mọi thứ mở ra rồi ở đó mãi.

Và một lớp gate thứ tư mà cả ba đều không thấy: THỜI GIAN THỰC. Map của Trip xoay theo ngày trong tuần (Forest T2/T5/CN · Desert T3/T6/CN · Glacier T4/T7/CN). Không có phân tích tĩnh nào phát hiện ra điều này — nó chỉ hiện ra khi mở panel trên máy thật. Cộng với Adventure_Unlock_Balance = [0,1,3,6,10] (gate theo Trip Level, không theo stage), Trip có hai trục gate riêng, độc lập với cả ba lớp ở trên.

3.2c Đường boot & định dạng save

Luồng nạp save — offline chạy TRƯỚC, và là một continuation:

// LoadData.Load(data)
if (data == null)
    InitNewGame();
else
    OfflineProgressManager.instance.EvaluateAndApply(data, ContinueLoad);   // <-- ContinueLoad = continuation

CodeRefsTo(EvaluateAndApply) = đúng một người gọi: LoadData.Load. Nghĩa là:

  • Tiến trình offline được tính trước khi bất kỳ manager nào nạp state của nó.
  • Popup "Chào mừng trở lại!" nằm trong luồng điều khiển, không phải một hiệu ứng phụ chạy song song: phần còn lại của việc nạp save chờ nó xong rồi mới tiếp tục.
  • ContinueLoad sau đó toả ra ~48 điểm gọi: X.LoadFromSaveData(data) cho từng manager, cộng một cặp Time_manager.get_Now / DateParseSafe cho từng bộ đếm thời gian (buff, vé, daily, cooldown…). Đợt 3 đã chứng minh từng field của đường này (35 restore trực tiếp + thứ tự gọi + vị trí AllowSave) — xem §2.8[evidence codegen: FIELD_MAP.md].

Đây là một thứ tự có chủ đích, không phải tình cờ: offline phải chạy trước vì nó ghi vào Gamemanager.Moneyđẩy level upgrade lên01 §5.8); nếu manager nạp state trước thì kết quả offline sẽ bị ghi đè.

Định dạng save — không mã hoá, không salt:

SaveSystem.TryLoadFrom   0x2AC6064
JsonData_Decoding        0x2AC6228   =>  Base64( UTF8( JSON của JsonUtility ) )

Application.persistentDataPath + "/saves/"
    NoSaveFile.json        (chính)
    NoSaveFile.json.bak    (dự phòng — dùng khi bản chính parse ra null)

Và điểm đáng chú ý nhất: game dùng Anti-Cheat Toolkit (ObscuredInt, ObscuredFloat…) cho state trong bộ nhớ — nhưng SaveData khai báo kiểu thường:

public class SaveData {          // dump.cs:589019+
    public InfVal Money;         // 0x18
    public int    Dia;           // 0x78   <- int THƯỜNG, không phải ObscuredInt
    …                            // 100+ field
}

(Đã tự kiểm: 0 lần xuất hiện chuỗi Obscured trong toàn bộ khối SaveData.)

Khối restore chép thẳng từng field vào static của manager — ví dụ data.Money (@+0x18) → Gamemanager static +0x28.

Đánh đổi, nói thẳng: chống gian lận phía bộ nhớ (ACTk) + save plain-text khi nghỉ. Nó chặn được lớp tấn công phổ biến nhất (công cụ quét/sửa bộ nhớ như GameGuardian) với chi phí gần bằng 0, và không chặn được người chơi mở file save ra sửa bằng tay. Đó là một lựa chọn hợp lý cho một game idle single-player: giá trị bị đe doạ là doanh thu quảng cáo/IAP của chính người chơi đó, không phải sự công bằng với người chơi khác. Mã hoá save sẽ chỉ làm chậm kẻ quyết tâm và tăng rủi ro hỏng save cho người dùng thật. Nếu game có bảng xếp hạng có thưởng, phép tính này đổi — và CatGunner Rank_manager, nên đây là một điểm đáng chất vấn chứ không phải một hình mẫu để chép mù.

Ba tầng phòng thủ khi NẠP (đợt 3, disasm-proven — §2.8): (1) kẹp giá trị có thể cấp thưởng ngay lúc nạp (LoadBuffTimes = Clamp(0, 900)); (2) bất đối xứng null-save cho mọi thứ tích luỹ theo thời gian (DiaPig.Init: save null chỉ neo lại đồng hồ, không Tick_Accumulate); (3) chép phòng thủ cho mảng (min(độ dài lưu, độ dài mình), thay mảng mới trước khi chép, chuỗi rỗng thay null). Cộng với AllowSave đặt ở cuối ContinueLoad / InitNewGame — autosave không thể ghi đè một save đang nạp dở.

⚠ QUY TẮC KHÔI PHỤC — bắt buộc

Không bao giờ bật việc nạp save trước khi khối restore hoàn chỉnh và chính xác. Một bản restore một phần sẽ đọc save của người chơi cũ, khôi phục thiếu, rồi ghi đè save đó bằng trạng thái thiếu — phá huỷ tiến trình của họ trong im lặng. Điều đó tệ hơn việc không restore gì cả: không restore thì người chơi thấy game mới và biết ngay; restore thiếu thì họ mất nhiều giờ chơi mà không có cách nào lấy lại. Với SaveData 100+ field và ~48 điểm LoadFromSaveData, "gần đủ" là một trạng thái rất dễ rơi vào.

3.3 Súng: gacha → merge → hạn dùng

DrawRandom_Normal() / DrawRandom_Epic() / DrawGun_Degree_5(degree)
        |
        v
DrawRandomGun(degree) -> FindFirstEmptyOwned() -> Owned_Gun_list[slot] = gunNum   (16 ô, -1 = trống)
        |
        +-> OnWeaponAcquired(gunNum) -> Weapon_Get_Counts[degree]++   [thống kê cho analytics R1]
        +-> NewGun_UI_Reload(gunNum)
        |
        v
TryStartAutoMerge()  (nếu is_AutoMerge)
        AutoMerge_Limit_Degree = min(AutoMerge_Degree, Max_Collected_Degree)
        const AutoMerge_Exclude_Degree = 7   <- bậc >= 7 KHÔNG BAO GIỜ tự ghép

Ba chi tiết thiết kế:

  1. Kho cố định 16 ô với -1 là ô trống. Không List, không cấp phát. Chỉ một int[16]. FindFirstEmptyOwned() là một vòng lặp tuyến tính — trên 16 phần tử thì đó là cách nhanh nhất.

  2. Auto-merge có trần cứng bằng hằng số. AutoMerge_Exclude_Degree = 7 bảo vệ người chơi khỏi việc tự động ghép mất một khẩu hiếm. Đây là kiểu ràng buộc phải nằm trong code, không nằm trong cấu hình — một designer chỉnh nhầm sẽ phá tài khoản người chơi.

  3. Súng thưởng quảng cáo có hạn dùng theo long AD_Gun_EndUtcTicks. Nó chiếm một ô kho, hết hạn thì AD_Gun_Clear(removeFromInventory). Đây là "cho mượn sức mạnh" chứ không phải "cho sức mạnh" — một mẫu monetization gặp thường xuyên. Toàn bộ đường này là R1.

Cân bằng từ xa (đáng chú ý): Gun_manager chứa một URL Google Sheets dạng export?format=tsv&range=A2:I66 cùng coroutine Request_Sheet(). Nghĩa là bảng chỉ số súng (9 cột × 65 dòng) tải được từ xa sau khi ship. Đây là một mẫu liveops thật, rất phổ biến, và cũng là một rủi ro cần ghi nhận: bản restore có thể quan sát thấy chỉ số khác với bảng nhúng.

Ranh giới R1: Request_Sheet là truy cập mạng ra dịch vụ ngoài ⇒ phải đi qua IRemoteConfigService với binding mặc định fail-closed (không trả về giá trị từ xa nào).

3.4 Buff: hai đồng hồ, một coroutine

// Buff_manager — dump.cs:592886
private const float BUFF_DURATION = 300f;   // mỗi lần nhận: +5 phút
public  const float BUFF_MAX      = 900f;   // trần tích luỹ: 15 phút
private float moneyBuffTime;                // @0x28
private float powerBuffTime;                // @0x2C
private Coroutine buffCoroutine;
private void TryStartCoroutine();           // chỉ chạy coroutine khi có buff
public  void AddBuff(AD_Type type);
public  bool IsBuffActive(AD_Type type);
public  float GetBuffRemainingTime(AD_Type type);
public  bool IsPermanentBuff { get; }

Thiết kế đáng học:

  • Buff là thời gian còn lại, không phải danh sách instance. Hai float thay cho một List<ActiveBuff>. Nhận buff = cộng 300 giây, kẹp ở 900. Không cấp phát, không GC, không sắp xếp.
  • AddBuff cộng dồn có trần. Xem 3 quảng cáo liên tiếp = 15 phút, không phải 3 buff chồng nhau. Trần cứng ngăn người chơi tích trữ vô hạn.
  • Trần được áp cả khi NẠP save, không chỉ khi cộng: LoadBuffTimes kẹp [0, 900] bằng NEON (G291). Một save sửa tay moneyBuffTime = 1e9 vào game vẫn chỉ còn 15 phút.
  • Một coroutine duy nhất, chỉ chạy khi cần (TryStartCoroutine). Khi không có buff nào, không có gì chạy mỗi frame.
  • IsPermanentBuff là đường thoát riêng cho buff vĩnh viễn (mua bằng IAP) — nó là property, không phải một float = ∞. Runtime cho thấy nó được hỏi 103× trong 40 giây, tức nằm trên hot path và được kiểm trước mọi thứ khác.
  • AD_Type có 10 giá trị nhưng Buff_manager chỉ giữ 2 đồng hồ. Nghĩa là 8 giá trị còn lại là loại phần thưởng quảng cáo một lần (bonus, gacha, rebirth boost), không phải buff theo thời gian. Một enum, hai ngữ nghĩa — hợp lý ở đây vì tất cả đều là "người chơi xem quảng cáo để lấy X".

3.5 Nhiệm vụ: 4 loại, một API

public enum Mission_Type { TreeCount = 0, StageClear = 1, Rebirth = 2, PlayMine = 3 }

// Call-site duy nhất từ gameplay:
Mission_manager.instance.MissionValue_Increase(Mission_Type.TreeCount);   // Tree_manager.Tree_Destroyed (G346; trước ghi nhầm là Tree.Die_Reload)
Mission_manager.instance.MissionValue_Increase(Mission_Type.Rebirth);     // Rebirth()

Bốn loại nhiệm vụ cho một game có hàng chục hệ thống. Đây là một quyết định thiết kế mạnh: nhiệm vụ chỉ theo dõi những hành động cốt lõi (giết cây, qua màn, rebirth, chơi mỏ), không theo dõi mọi thứ. Kết quả: Mission_manager (54 method) hầu hết là UI và phần thưởng, không phải theo dõi.

Runtime xác nhận vòng khép kín: Tree.Die_Reload 85×Tree_DestroyedMissionValue_Increase 86×.

is_Reward_SequenceLocked (150×) cho thấy phần thưởng phải nhận theo thứ tự — người chơi không nhảy cóc lấy phần thưởng lớn. Cộng với hệ mùa (Season_Reload, is_Season_Closed), đây là bộ khung battle-pass tối giản.

3.6 Ranh giới R1 — cái đúng đắn nhất trong toàn bộ project

// [restore project: NullServices.cs]
public sealed class NullAdService : IAdService
{
    public bool IsInterstitialReady => false;
    public bool IsRewardedReady     => false;
    public void ShowRewarded(string placementId, Action<ServiceResult> onReward)
    {
        NullLog.Refused(nameof(NullAdService), nameof(ShowRewarded));
        onReward?.Invoke(ServiceResult.NotAvailable);      // TỪ CHỐI. Không bao giờ grant.
    }
}

Quy tắc (R1, nguyên văn): "Reward/purchase/entitlement calls FAIL CLOSED: return failure/denied, never success. A NullService may log and no-op. It may never fabricate progression state."

Vì sao đây là mẫu quan trọng nhất trong tài liệu này:

  • Một game idle chằng chịt các call-site quảng cáo/IAP: buff ×3, rebirth ×3, gacha, súng thưởng, offerwall, free cash, phần thưởng nhiệm vụ. Đó là 10 giá trị AD_Type.
  • Nếu một stub trả về "thành công", bản restore sẽ tự trao thưởng cho chính nó — và mọi phép đo parity sau đó đều vô nghĩa, một cách âm thầm.
  • Fail-closed nghĩa là: nếu quên bind một service, người chơi nhận ít hơn thực tế. Sai theo hướng an toàn, và sai một cách quan sát được.

Trong build hiện tại, ngay cả 46 assembly SDK còn giữ lại cũng fail-closed do cấu trúc: chúng là DLL do Cpp2IL dựng, không có thân method, nên mọi lời gọi qua chúng trả về default.

3.7 Reset theo ngày có tầng chống đổi giờ máy — mẫu chuẩn của mọi quota hằng ngày

Đọc từ Fish_manager.FreeGacha_Reload (quota 5 lượt rút cá/ngày, doc 10 §4.1b, docs/functions/ca-fish.md §6). Cùng một hình dạng dùng lại được cho điểm danh, vé Trip, quota quảng cáo — bất cứ thứ gì "mỗi ngày N lần".

void Reload()
{
    if (savedDate.Date >= Time_manager.Today) return;              // 1. chưa sang ngày mới
    if (!Time_manager.CanRolloverDaily("fish_free", 3)) return;    // 2. tầng trust chưa đồng ý
    Time_manager.RecordGain("fish_free");                          // 3. ghi sổ TRƯỚC
    count = 0;                                                     // 4. rồi mới phát
    savedDate = Time_manager.Today;                                // 5. và mới đánh dấu
}

Bốn quy tắc, xếp theo mức dễ làm sai:

  1. KHÔNG BAO GIỜ so bằng DateTime.Today. Mọi hệ theo lịch phải hỏi một nguồn giờ duy nhất (Time_manager.Today = Now.Date, với Now là giờ local đã hiệu chỉnh theo lệch đo được với máy chủ). Hai nguồn giờ trong một game là hai lịch, và chúng sẽ lệch nhau đúng vào lúc người chơi đổi múi giờ.
  2. Cổng chống gian lận là một SỔ NỢ CÓ TRẦN, không phải một cờ boolean. CanRolloverDaily(key, maxPending) = SaveLoaded && CanGrantTimedReward && PendingCount(key) < 3. Người chơi được "ứng trước" tối đa 3 lần rollover chưa xác thực; quá đó thì hệ ngừng phát cho tới khi giờ được xác nhận. Ưu điểm so với cờ: chơi offline dài ngày vẫn nhận được, chỉ có gian lận lặp lại mới bị chặn — và mỗi khoá ("fish_free", …) có sổ riêng nên một hệ bị khoá không kéo theo hệ khác.
  3. Bị chặn thì KHÔNG ghi ngày mới. Đây là toàn bộ khác biệt giữa "hoãn" và "mất": vì savedDate giữ nguyên ngày cũ, điều kiện savedDate < Today vẫn đúng ở lần gọi sau, nên lần đồng bộ tiếp theo thử lại. Ghi ngày trước khi qua cổng = người chơi mất trắng quota hôm đó, im lặng. Quy tắc tổng quát: đừng bao giờ đánh dấu "đã phát" trước khi phát.
  4. Kẹp ngày lưu về hiện tại khi đồng hồ được đồng bộ. OnSynced → savedDate = ClampFutureLocalDate(savedDate) chặn kiểu gian lận "đẩy giờ máy tới tương lai rồi trả về" — nếu không, ngày lưu nằm ở tương lai và quota đóng băng vĩnh viễn. Trần là Today.AddDays(1); khi không vượt trần thì trả saved nguyên vẹn, không phải saved.Date (giữ cả phần giờ).

Điểm gọi: hàm Reload() phải chạy ở cả hai đường — mỗi lần vẽ lại UI của bề mặt đó, khi đồng hồ vừa được đồng bộ lúc UI đang đóng. Chỉ gọi ở một chỗ là có một cửa sổ trong đó bộ đếm cũ được dùng để quyết định.

Bẫy dựng lại (G1679): hai hàm ClampFutureLocalDate/ClampFutureLocal từng là ret-stub return default(DateTime)nằm im vô hại cho tới khi một lô khác dựng đúng OnTimeSynced — lúc đó mỗi lần đồng bộ giờ sẽ ghi 0001-01-01 vào ngày lưu, tức reset quota mỗi lần. Bài học chung cho mọi dự án khôi phục: dựng đúng một hàm có thể kích hoạt một stub ở lớp khác đang nằm im. Trước khi kết thúc một lô, phải quét stub trên toàn bộ callee mà lô vừa mở đường tới.


4. Framework takeaway

  1. Bốn quy ước method cho mọi hệ thống tiến trình: Balance_Reload() sinh bảng số từ công thức — CÔNG CỤ EDITOR, bake ra asset SaveData_return() / LoadData() ranh giới lưu trữ, một cặp mỗi hệ thống First_Reload() / Value_Reload() thiết lập một lần vs cập nhật thường xuyên static X_return() phơi giá trị dẫn xuất cho hệ thống khác Bốn cái này lặp lại ở ~30 manager. Framework nên đưa chúng thành interface, không để mỗi người tự đặt tên.

  2. Cân bằng là công thức + vài hằng số, không phải bảng inspector. 7 hằng số sinh ra 10 001 phần tử. Chỉnh nhanh hơn, diff sạch hơn, và khôi phục được từ binary. Framework nên cung cấp ICurve { BigNum ValueAt(int lv); } với các hiện thực sẵn (hình học có làm tròn, tuyến tính, đa thức, có soft-cap).

  3. Kho vật phẩm cố định + giá trị lính canh (int[16], -1 = trống) thay cho List<T>. Với kích thước nhỏ, quét tuyến tính là nhanh nhất và không cấp phát.

  4. Ràng buộc bảo vệ người chơi phải là const trong code, không phải cấu hình. AutoMerge_Exclude_Degree = 7 là ví dụ: một designer chỉnh nhầm sẽ phá tài khoản.

  5. Buff = thời gian còn lại có trần, không phải danh sách instance. float remaining; const float PER_GRANT = 300; const float MAX = 900; AddBuff() => remaining = min(remaining + PER_GRANT, MAX) Không cấp phát, cộng dồn tự nhiên, trần cứng chống tích trữ. Cộng một property IsPermanent riêng.

  6. Ít loại nhiệm vụ. Bốn loại là đủ cho một game thương mại. Theo dõi hành động cốt lõi, không theo dõi mọi thứ — nếu không, mỗi hệ thống mới lại phải sửa hệ nhiệm vụ.

  7. Mọi dịch vụ ngoài nằm sau interface với binding mặc định FAIL-CLOSED. Đây là bất biến quan trọng nhất trong một game idle, vì phần thưởng quảng cáo/IAP đan xuyên toàn bộ hệ thống tiến trình. Một stub được phép log và no-op; không bao giờ được bịa progression.

  8. Tách "máy móc toàn vẹn" khỏi "gameplay" ngay từ đầu. Một nửa Gamemanager (70 method) và toàn bộ Time_manager (51 method) là chống gian lận thời gian. Trong framework, đó phải là module riêng đằng sau ITrustedTime, không trộn vào manager kinh tế — nếu không, bạn không thể kiểm thử kinh tế mà không có mạng.

  9. Kiểm tra xem cấu hình từ xa CÓ THỰC SỰ chạm vào cân bằng không — đừng giả định theo cả hai hướng. Ở đây có đủ hạ tầng cho việc đó (URL Google Sheet trong Gun_manager, Firebase RemoteConfig, PlayFab, một endpoint config riêng) và trực giác ban đầu là "số có thể bị ghi đè từ xa". Kiểm tra bằng xref cho thấy: không. RemoteConfig chỉ chạm UpdateManager; GetTitleData 0 caller; ServerConfig chỉ mang tham số quảng cáo; và Request_Sheet chỉ tới được từ một Balance_Reload đã chết. Bài học: một hạ tầng có mặt không có nghĩa nó được dùng. Ba lệnh xref rẻ hơn một giả định sai.

  10. Đừng dựng lại thứ mà runtime cho thấy không quan trọng. 731 type, chỉ 190 vào scene, 66 class chiếm phần lớn thời gian chạy. Xếp hạng bằng dữ liệu, đừng xếp hạng bằng cảm giác.

  11. PHÂN TẦNG CỔNG: đừng gộp ba loại khoá vào một cờ isUnlocked. ITutorialGate : "đã học tới đâu?" — designer sở hữu, không bao giờ đổi sau khi ship IProgressGate : "đã đi xa nhất tới đâu?" — high-water mark, không bị prestige reset ISegmentGate : "nhóm nào, có chào mời không?" — cấu hình từ xa, đổi bất cứ lúc nào ITimeGate : "hôm nay là thứ mấy?" — map Trip xoay theo ngày trong tuần Bốn lớp, bốn chủ sở hữu, bốn nhịp thay đổi, bốn kiểu hậu quả khi sai. Gộp lại là một trong những sai lầm tốn kém nhất của game live-service. Cảnh báo phương pháp: lớp Tutorial và lớp Time vô hình với phân tích tĩnh — chỉ hiện ra khi chạy một save mới thật trên máy thật.

11b. Cửa sổ chào mời hai đầu. FreeCash chỉ hiện trong khoảng Stage 15…35tự ẩn khi người chơi vượt qua. Không phải mọi thứ mở ra rồi ở đó mãi — một lời chào mời hết phù hợp thì nên biến mất.

11c. Ngưỡng đo bằng ĐƠN VỊ NGƯỜI CHƠI THẤY, không bằng biến nội bộ. Mọi cổng liveops so sánh với MaxStageDisplay = Stage_Value_Max/10 + 1 — tức số stage hiển thị, thứ mà người vận hành và người chơi cùng nói tới. Cấu hình từ xa ghi "Stage 15" và nó thật sự nghĩa là Stage 15. Nếu cấu hình dùng biến nội bộ, mọi người sẽ chỉnh sai.

11d. "Nhận thưởng theo thứ tự" là một quy tắc, không phải một ngưỡng. is_Reward_SequenceLocked không so sánh với con số nào — nó quét claimed[] và khoá chừng nào còn một mốc thấp hơn chưa nhận. Framework nên có sẵn kiểu này (SequentialClaimTrack) thay vì để mỗi người tự dựng bằng ngưỡng số.

11e. Mốc mở khoá đọc một tiến độ ĐƠN ĐIỆU riêng, không đọc tiến độ hiện tại. Stage_Value_Max = max(Level_Now + 10·Stage_Now) chỉ tăng và không bị prestige reset — nên prestige không bao giờ khoá lại nội dung người chơi đã mở. Framework nên có sẵn một HighWaterMark riêng cho việc mở khoá.

  1. Thứ tự boot là hợp đồng, không phải tình cờ. LoadData.Load chạy offline trước, rồi truyền ContinueLoad làm continuation — vì tiến trình offline ghi vào tiền và level. Framework nên phơi ra một pipeline boot có thứ tự tường minh (IBootStep với thứ hạng), không để mỗi manager tự Awake rồi hy vọng.

  2. Chống gian lận bộ nhớ ≠ chống gian lận file — chọn có ý thức và ghi lại lựa chọn. SOT bọc state runtime bằng ACTk nhưng ghi save Base64(JSON) trần. Hợp lý cho idle single-player; đáng chất vấn khi có bảng xếp hạng có thưởng. Đừng chép mẫu này mà không kiểm lại mô hình mối đe doạ.

  3. ⚠ QUY TẮC KHÔI PHỤC: không bật nạp save trước khi khối restore hoàn chỉnh. Restore một phần sẽ đọc save cũ, khôi phục thiếu, rồi ghi đèphá tiến trình người chơi trong im lặng. Tệ hơn hẳn việc không restore gì. Với 100+ field và ~48 điểm LoadFromSaveData, "gần đủ" là trạng thái rất dễ rơi vào. Framework nên có một cổng an toàn: từ chối ghi save khi số field khôi phục được < 100 % số field đọc vào.

  4. dump.cs chỉ liệt kê KHAI BÁO, không liệt kê TRUY CẬP. "Field không xuất hiện trong dump.cs" không là bằng chứng nó không được dùng — tài liệu này từng kết luận sai như vậy về Rebirth_Btn_Locked_obj. Dùng xref của IDA (CodeRefsTo / DataRefsTo + decompile + tìm offset). Cùng kỹ thuật là thứ chứng minh Balance_Reload là dead code — một kết luận không thể rút ra từ dump.cs.

  5. ⚠ CÂN BẰNG CỦA BẢN SHIP SỐNG TRONG SCENE, KHÔNG TRONG CTOR. Đây là mặt bên kia của bài học "phải chép initializer" (G1648/G1663) và nó đã làm hai kết luận trong dự án này sai:

    Field Ctor nói Scene nói Ai thắng
    Cat_manager.FormationRadius 10 2.5 scene
    Fish_manager.GachaChances (G1682) {40, 30, 20, 10} {60, 30, 10, 0} scene

    Luật đọc, theo loại field chứ không theo trực giác:

    public / [SerializeField] và CÓ trong scene|prefab -> ctor chỉ là mặc định AddComponent; SCENE thắng private KHÔNG [SerializeField] -> CTOR là giá trị thật; scene không cứu được const / static readonly -> ctor/metadata là giá trị thật

    Hệ quả thao tác cho một dự án khôi phục: chép ctor vào code là đúng (thiếu 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. Trước khi một con số cân bằng được publish, phải trả lời được "field này có serialize không, và scene ghi gì". Cách bắt lỗi rẻ nhất khi chưa đọc được scene: một quan sát tần suất trên máy — năm lượt gacha liên tiếp ra cùng một bậc là 0,6⁵ ≈ 7,8% với bảng thật nhưng 0,4⁵ ≈ 1% với bảng ctor, đủ lệch để đi đọc lại. Nợ còn treo cùng loại: Fish_Draw_Btn.Price (ctor 100, chưa đọc scene — đừng tin con số đó).


5. Negative evidence & UNKNOWN

Mục Trạng thái Bằng chứng khoá được nó
Shop_manager (107 method, manager lớn nhất) KHẢO SÁT MỘT PHẦN. Doc 10 phủ IAP; đợt 3 thêm .cctor (Dia_FirstBuy_Used, DiaRewards), Is_Dia_FirstBuy, LoadFromSaveData (G350). Runtime: is_DiaPig_Unlock 4 778×, OfferWall_ShouldShow 24×, Reload_FreeReward 14×, Is_Dia_FirstBuy 10× Disasm phần còn lại
Adventure_manager (49m), Fish_manager (38m), Monster_manager (37m), Mine_manager (31m), Boss_manager (29m), Raid_manager (26m), SpeedRun_manager (26m) KHẢO SÁT MỘT PHẦN (đợt 3): Adventure Ticket_Charge_Interval/Ticket_Refresh/Exp_Balance_Reload/Alert_Reload + 2 bảng static (§2.9); Fish ReloadAll + GachaChances; Monster 2 bảng (MonsterCountPerLevel, SpawnIntervalPerLevelchỉ số hàng chưa đọc); Raid 2 mảng -1. Phần còn lại chưa đọc Disasm phần còn lại
Thân Upgrade_manager.Value_str_Return (RVA 0x2B49134) CHƯA dựng lại — đây là hàm sinh chuỗi cho panel nâng cấp; ảnh chụp cho thấy nó không hiển thị thẳng Money_balance[Lv] Disasm RVA 0x2B49134
Thân Gun_manager.DrawRandomGun (RVA 0x2B0A278) — tỉ lệ rơi gacha UNKNOWN. Đây là số liệu monetization nhạy cảm nhất và chưa hề được đọc. Không suy đoán. Disasm RVA 0x2B0A278 + Balance_Reload
Nội dung bảng Google Sheets từ xa UNKNOWN và có thể đã thay đổi — URL là một tài liệu ngoài, không thuộc SOT Không khôi phục được từ binary
Request_Sheet có thực sự chạy lúc boot không CHƯA XÁC MINH — không thấy trong 505 method đã chạy của cửa sổ trace 40 s. Đây là bằng chứng phủ định yếu (cửa sổ ngắn, có thể chạy sau) Trace dài hơn + theo dõi mạng
Trip (Adventure_manager) — mảng cân bằng đọc trực tiếp từ bộ nhớ: Adventure_Unlock_Balance = [0,1,3,6,10] (5 map, gate theo TRIP LEVEL, không theo stage), Adventure_PetSlot_Unlock_Balance = [0,2,7], Adventure_Duration_Balance = [1,2,4,8,16] giờ, Exp_balance 15 bậc (10…920), vé 20/20 nạp 1 vé / 4 giờ VERIFIED (đọc bộ nhớ runtime) PROBE_RESULT.md §3; G178
Map Trip XOAY THEO NGÀY TRONG TUẦN: Forest T2/T5/CN · Desert T3/T6/CN · Glacier T4/T7/CN — một lớp gate thời gian mà phân tích tĩnh không nhìn thấy VERIFIED (ảnh shots/31_panel_Trip.png) PROBE_RESULT.md §2.1; G191
Trip có 4 loại tiền tệ: 3 Core (xanh lá / cam / xanh dương) + PrismCore VERIFIED (ảnh) như trên
Mission_manager.is_Reward_SequenceLocked(degree, Lv) KHÔNG phải ngưỡng số: for (i=0; i<Lv; i++) if (!claimed[i]) return true; — bị khoá chừng nào còn một mốc thấp hơn chưa nhận VERIFIED (disasm + đọc bộ nhớ) RVA 0x2A71E5C; claimed[] = bool[25]battle-pass 25 bậc; PROBE_RESULT.md §3.1
Raid không tới được kể cả ở Stage_Value_Max = 1000: quét đủ 27 nút trong Main_UI/Btn_list, không có nút Raid nào — hai lần quét runtime độc lập VERIFIED (bằng chứng phủ định, 2 lần) PROBE_RESULT.md §5; G142/G177 ⇒ nội dung chưa ship
Điều kiện mở khoá từng nội dung ĐÓNG MỘT PHẦN. Cơ chế + ngưỡng đã VERIFIED: Contents_Lock_Reload so Stage_Value_Max với 20 / 30 / 50 / 90 / 110 / 110 / 340 (Rebirth = 20). Ánh xạ ngưỡng → tên nội dung vẫn UNKNOWN (chỉ biết off_… static của class chủ). Danh mục đầy đủ do lane [liveops catalog: liveops] phụ trách FABLE §U1; map off_… sang class; lane [liveops catalog: liveops]
Cấu trúc SaveData ĐÃ ĐÓNG (đợt 3). Bản đồ restore từng field: 22 (Load) + 5 (tiến độ lõi trong Load) + 35 (ContinueLoad) đều có offset nguồn/đích và dòng Hex-Rays — xem §2.8 và FIELD_MAP.md. Còn mở: các field SaveData không được đường nạp đọc (nếu có) chưa liệt kê G329; [evidence codegen: FIELD_MAP.md]
Bảng XP Trip nào sống sau khi nạp save ĐÃ ĐÓNG (G361): mảng scene (hex blob) sống; công thức chỉ chạy khi mảng null/rỗng — §2.9
Mission_manager.HighTier_Gun_Table {16..29} dùng để làm gì UNKNOWN — giá trị đã chứng minh (G353), consumer chưa đọc DataRefsTo slot metadata của field, decompile người gọi
~~Monster_manager.MonsterCountPerLevel / SpawnIntervalPerLevel — chỉ số hàng~~ ĐÃ ĐÓNG (G408): chỉ số = level 0..4 trong wave (5 level / wave); §2.13
Thu nhập tiền trên bản học ≈ ×300 SOT (3.2 M/s vs 8.6 K/s cùng file) MỞ — LỖI THẬT (G414), nghi hệ số base-100 áp thô trong tích tiền Codegen: log từng mắt MoneyValue_return + tiền quái trên cùng frame
TotalPlayTime không tăng MỞ (G414) — bộ tích thời gian chơi còn stub Codegen
PlusPack.Reward_Mult_Monster/Boss, Is_Active, Shop_manager.Is_RewardBonus_Auto = 0/false trong overlay ⇒ thưởng Hunt/Boss = 0 MỞ — R13 (G408) Codegen
Tầng đối chiếu trust trong Time_manager.Init CỐ Ý KHÔNG TÁI HIỆN (anti-cheat, ngoài phạm vi) — không phải UNKNOWN về nội dung, là quyết định phạm vi G321
Đường GHI save DỰNG XONG, công tắc TẮT (G388) — 127/128 field; blocker: 6 getter inline + bỏ sót phía đọc (G389) ⇒ cổng CLOSED. Bật khi cổng mở rộng = 0 và round-trip thiết bị 126/128 Cổng _compile_write + round-trip thiết bị
~~Time_manager.UtcNow / LastTrustedUtc / buff / FreeCash getters còn stub~~ ĐÃ ĐÓNG (G391): đồng hồ + .cctor dựng lại (§2.6); buff getters = field +0x28/+0x2C; FreeCash = view static null-safe +0x30/+0x31
~~Bỏ sót phía đọc ContinueLoad~~ ĐÃ ĐÓNG (G391) — 6 restore bổ sung (§2.8)
~~Save mới: InitNewGame dừng giữa chừng (G437)~~ ĐÃ ĐÓNG (G394): batch 18 đường mới 0 ngoại lệ
Residual (1) — đuôi ContinueLoad chưa chạy VẪN CÒN ở batch 20 (G405) — giờ là hệ quả của "routine is null" trong EvaluateRoutine (continuation chưa được gọi); hết null này là đuôi chạy Cùng fix với offline
Đường offline ĐÃ CHẠY trên thiết bị nhưng HUỶ giữa chừng (G394): RecordGain("offline:2,2h") dưới Trust.None (2.2 h vs ~9.3 h kể từ LastSaveTimeSavedOffsetSec của save đẩy vào? lane nghiệm thu phán), rồi ArgumentNullException (key) trong Gamemanager.MoneyValue_return ← Get_Money ← ApplyToManagers sau khi đã mua nâng cấptiền GIẢM 2 497 189 → 126 356 (−2.37 M). Gốc rễ (G400): GetMoneyCacheKey stub null — đã sửa. Batch 20 (G405): không còn mất tiền; đường offline chạy tới EvaluateRoutine rồi ném "routine is null" ⇒ khoản trả huỷ. Null đã định danh (G409): OfflineProgressManager.ShowAfterFrame() còn return nullShowResult (0x2A75650) gọi StartCoroutine(ShowAfterFrame()); thân = <ShowAfterFrame>d__17.MoveNext (0x2AB6C44): yield 1 frame → bật offlinePanelUIReload(_beforeData, LastResult) (Offline_Panel_UI.Reload 0x2AB6CDC có lẽ cũng stub). Dự đoán harness cho 5.4 h (chưa xác nhận): +19 790 601.31, MoneyFinal 22 287 790.65, level [41, 41, 41] (11 nâng cấp, tiêu 212 268 000), stage giữ 2-2; nhạy ±1 s ≈ ±17 300 tiền ⇒ nghiệm thu cần elapsed float chính xác NULL ĐÃ ĐỊNH DANH — chờ build kế; dự đoán chưa xác nhận Codegen: ShowAfterFrame + Offline_Panel_UI.Reload; đối chiếu thứ tự state
~~Map kẹo ngọtStage_Now = 2~~ ĐÃ GIẢI THÍCH (G406/G1307): Tree_manager.Map_Batch chọn chủ đề theo Stage_Now % 10 = 2 = candy; SpeedRun.Map_obj (GO 655) inactive — không phải bug, là map theo stage. (Doc 11 §6.1: 10 chủ đề map = 10 stage lặp)
~~Đọc save hồi quy ở batch 19~~ ĐÃ HIỂU (G399): quy trình test (push trước khi app tạo thư mục), không phải code; quy trình mới ghi ở §2.8
Autosave trên save mới VẪN NÉM ở batch 20 (G405): null kế tiếp = PlusPack.SaveData_return ← CreateFromCurrentState; save đã nạp ghi được Codegen: PlusPack snapshot
~~Gamemanager.CanSpawnText stub~~ ĐÃ ĐÓNG (G402): thân dựng lại (lưới 1.5, X/Y, làm tròn ngân hàng — doc 06 §2.4); batch 20 thấy tờ tiền bay
Tutorial trên save thật dừng ở GunOpen (chưa Done, TutorialCleared vẫn false) MỞ (G405) — các bước súng phụ thuộc Gun_manager (tranche 1-2 đã landed G404) Build kế
Residual nạp save (G367) (1) Adventure_LastTicketChargeĐÃ SỬA, chờ nghiệm thu lại (DateParseSafe/SanitizeYear + tách hai nhánh Adventure, G372); (3) đường offline — simulator ĐÃ DỰNG XONG và khớp oracle 17 chữ số ngoài thiết bị (G382; 12 luật doc 01 §5.8); trên thiết bị vẫn chờ 9 stub R13 ở file khác (Rebirth.Money_return/Speed_return trả 0 ⇒ mọi số × 0, Firse_reload rỗng, Fish.GetTotal*, Skin.*_Accum_return — G1002); (4) hàng nút HUD — CompleteTutorial + máy trạng thái SetStep đã dựng (G375, G385); batch 17 (trước overlay G385) vẫn ở TutorialStep = Upgradechờ build kế; residual (1) gốc rễ thật = UtcNow = default (G389), chờ fix Nghiệm thu lại 38/38 sau build kế
Hệ điểm danh / retention (DailyCheck) ĐÃ ĐÓNG (G378): 13 stub đã dựng; quy tắc + bảng 70…1500 dia ở 10 §7.6. Còn mở: panel Ngày-2 trên save thật chưa nghiệm thu lại (nửa còn lại của residual offline) Nghiệm thu lại
~~Fresh save: 7 tích luỹ base-100 = 0~~ (G423) ĐÃ ĐÓNG (G374): callee là Relic_manager.Start = Value_ReloadSkin_manager.Start (§2.4) — chờ xác nhận trên build kế
Không quan sát thấy Không có DI container, không event bus tổng, không state machine tổng — chỉ 35 field static và lời gọi trực tiếp dump.cs toàn bộ manager
Không quan sát thấy Time_managerRedeemCode_manager không có instance nào trong scene — chúng được dựng lúc chạy [report: codegen_lane.md] §1.3

6. Raw evidence

  • [index: priority_static.json] — xếp hạng 731 class theo instance + method
  • [index: scene_census.json] — 30 082 tham chiếu m_Script, 190 class có mặt
  • [runtime trace: runtime_ranking.json] — 505 method đã chạy, kèm số lần
  • dump.cs:602928Upgrade_manager (hằng số + 22 method)
  • dump.cs:596607Gun_manager (gacha, auto-merge, URL từ xa)
  • dump.cs:592886Buff_manager
  • dump.cs:592186Relic_manager
  • dump.cs:601415Skin_manager
  • dump.cs:587114 / :584959Mission_Type / AD_Type
  • dump.cs:583820Gamemanager (hằng số chống gian lận)
  • [evidence codegen: rule_statics.cs.evidence] — rule statics đã dựng lại
  • [evidence codegen: Upgrade_Balance_Reload.c]Balance_Reload đã giải mã (dead code)
  • [phân tích nội bộ]Balance_Reload là dead code; bảng thật = mảng scene
  • [F001: scene_balance_tables.json] — 8 mảng balance trích từ Main.unity
  • [device evidence: user_screenshot_stage5-4.png] — xác nhận Speed_balance[46] = 468
  • [restore project: Services.cs], NullServices.cs, ServiceLocator.cs} — ranh giới R1
  • [report: asset_lane.md] §7 — 781 file SDK ngoài phạm vi
  • [liveops catalog: liveops] — danh mục màn hình + cổng mở khoá (lane riêng, liên kết khi bàn giao)
  • [evidence codegen: FIELD_MAP.md] — bản đồ restore 22 + 5 + 35 field, mỗi hàng có dòng Hex-Rays (đợt 3)
  • [evidence codegen: TABLE.md] — 22 bảng InitializeArray của Assembly-CSharp, 448/448 SHA MATCH toàn build (đợt 3)
  • [evidence codegen: CTOR_INITIALIZER_WORKLIST.md] — 39 ctor / 24 class có khởi tạo bị Cpp2IL bỏ rơi
  • Gaps: G129, G134, G136; đợt 3: G291, G296, G309, G311, G319–G330, G346, G350, G353 trong gap list (nội bộ)