07-progression-managers.md · Cập nhật lần cuối: 2026-09-06 23:04 +07 · sha256 nguồn bc4b07b3f1c607 — Tiến trình & bộ khung Manager
Nguồn: chưng cất từ restore SOT
com.Chodun.CatGunner1.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-CSharp có 731 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 |
Có 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_manager và RedeemCode_manager có 0 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_objs có 5 phần tử nhưng UI_type có 6 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ờ nó | 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 |
SaveData có 100+ 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.59 — CodeRefsTo = [] (đã 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 — và để 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…) |
Có 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 9×, Upgrade 5× 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 |
Có 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:I66 ⇒ 9 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_Type có 10 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 = 0 ⇒ Cat.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 Load ⇒ hai đườ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_Vip là static (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 = None ⇒ UtcNow = đồ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ắt và KHÔ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 = 0 ⇒ tắt toàn bộ bề mặt liveops; FreeCash còn bị gate kép: trạng thái AlmediaLinkSDK._almediaStatus (static 0x38) và 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-Raysdec_ContinueLoad.c765 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úcSaveData" 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_Start → Contents_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.Reload → Time_manager.MarkSaveLoaded → Cat_manager.Skin_Reload + Balance_Reload → SaveSystem.AllowSave tại 0x2AC32CC = CUỐI đường nạp → OfferWall.OnSaveLoaded → ServerConfig.OnSaveLoaded → AppsFlyerInit.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 là đú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 ở Upgrade vì SetStep (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_Count → D 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 null ⇒ StartCoroutine(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 và 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ì TrustLevel ở None 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 code — SaveSystem.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) và 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ạp — NoSaveFile.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 null ⇒ Dia_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.RoundtripKind và chuyể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 inactive ⇒ hoã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 và 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 đã cache → AppsFlyerInit.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 có .cctor (0x2B36F9C, 1936 B): cấp Dia_FirstBuy_Used = new bool[5] (static+0x28) tại 0x2B36FF4 và DiaRewards; 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..14 ⇒ 50, 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_Max ⇒ 10 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 được — IAdService 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.2 ⇒ má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.RewardAdRemoved ⇒ thưở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_Reload → Tree_manager.Tree_Destroyed
chạm vào Particle_Polling, Tree_manager, Gamemanager, Mission_manager, và AppsFlyerInit — nă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_ReloadtrongAwake. Gọi nó với một InfVal khác phiên bản (làm tròn thay vì cắt, hoặc1.38thậ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 là 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} và
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.
ContinueLoadsau đó toả ra ~48 điểm gọi:X.LoadFromSaveData(data)cho từng manager, cộng một cặpTime_manager.get_Now/DateParseSafecho 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 và[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 và đẩy level upgrade lên (§01 §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 có
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
SaveData100+ field và ~48 điểmLoadFromSaveData, "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ế:
-
Kho cố định 16 ô với
-1là ô trống. KhôngList, không cấp phát. Chỉ mộtint[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. -
Auto-merge có trần cứng bằng hằng số.
AutoMerge_Exclude_Degree = 7bả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. -
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_Sheetlà truy cập mạng ra dịch vụ ngoài ⇒ phải đi quaIRemoteConfigServicevớ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
floatthay cho mộtList<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. AddBuffcộ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:
LoadBuffTimeskẹp[0, 900]bằng NEON (G291). Một save sửa taymoneyBuffTime = 1e9và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. IsPermanentBufflà đường thoát riêng cho buff vĩnh viễn (mua bằng IAP) — nó là property, không phải mộtfloat= ∞. 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_Typecó 10 giá trị nhưngBuff_managerchỉ 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_Destroyed → MissionValue_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:
- 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ớiNowlà 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ờ. - 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. - 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ì
savedDategiữ nguyên ngày cũ, điều kiệnsavedDate < Todayvẫ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. - 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ảsavednguyên vẹn, không phảisaved.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 đó, và 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/ClampFutureLocaltừng là ret-stubreturn default(DateTime)và nằm im vô hại cho tới khi một lô khác dựng đúngOnTimeSynced— lúc đó mỗi lần đồng bộ giờ sẽ ghi0001-01-01và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
-
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ácBố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. -
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). -
Kho vật phẩm cố định + giá trị lính canh (
int[16],-1= trống) thay choList<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. -
Ràng buộc bảo vệ người chơi phải là
consttrong code, không phải cấu hình.AutoMerge_Exclude_Degree = 7là ví dụ: một designer chỉnh nhầm sẽ phá tài khoản. -
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 propertyIsPermanentriêng. -
Í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ụ.
-
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.
-
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 sauITrustedTime, 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. -
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ạmUpdateManager;GetTitleData0 caller; ServerConfig chỉ mang tham số quảng cáo; vàRequest_Sheetchỉ tới được từ mộtBalance_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. -
Đừ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.
-
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ầnBố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…35 và tự ẩ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á.
-
Thứ tự boot là hợp đồng, không phải tình cờ.
LoadData.Loadchạy offline trước, rồi truyềnContinueLoadlà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 (IBootStepvới thứ hạng), không để mỗi manager tựAwakerồi hy vọng. -
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ạ. -
⚠ 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. -
⚠
dump.cschỉ 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 minhBalance_Reloadlà dead code — một kết luận không thể rút ra từdump.cs. -
⚠ 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.FormationRadius102.5scene 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ậtHệ 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ưng0,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(ctor100, 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, SpawnIntervalPerLevel — chỉ 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ừ LastSaveTime — SavedOffsetSec 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ấp ⇒ tiề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 null — ShowResult (0x2A75650) gọi StartCoroutine(ShowAfterFrame()); thân = <ShowAfterFrame>d__17.MoveNext (0x2AB6C44): yield 1 frame → bật offlinePanelUI → Reload(_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ọt ở Stage_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 = Upgrade ⇒ chờ 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_Reload và Skin_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_manager và RedeemCode_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ếum_Script, 190 class có mặt[runtime trace: runtime_ranking.json]— 505 method đã chạy, kèm số lầndump.cs:602928—Upgrade_manager(hằng số + 22 method)dump.cs:596607—Gun_manager(gacha, auto-merge, URL từ xa)dump.cs:592886—Buff_managerdump.cs:592186—Relic_managerdump.cs:601415—Skin_managerdump.cs:587114/:584959—Mission_Type/AD_Typedump.cs:583820—Gamemanager(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_Reloadlà 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ậnSpeed_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ảngInitializeArraycủ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ộ)