CatGunner CookbookBardCat 1.1.59 · L1 ANALYSIS · tài liệu học
Trang chủCookbook › 14 — Lưu game & thu nhập offline (nạp save an toàn, trả tiền vắng mặt đúng luật)
Nguồn: 14-luu-game-va-offline.md · Cập nhật lần cuối: 2026-09-05 08:45 +07 · sha256 nguồn 6e4f44cc7585

14 — Lưu game & thu nhập offline (nạp save an toàn, trả tiền vắng mặt đúng luật)

Nguồn framework: docs/framework/07-progression-managers.md §2.8, §3.2c (hệ save/load), docs/framework/01-economy-money.md §5.8 và docs/framework/09-prestige-rebirth.md §4b (kinh tế offline), docs/framework/08-architecture-and-scale.md §3.6 (phương pháp). Cấp bằng chứng: L1 ANALYSIS — dựng lại clean-room từ disasm + metadata + quan sát runtime; không phải source gốc. Mỗi dòng bên dưới mang nhãn bằng chứng riêng và mã G-id trong ledger dự án.

Mục tiêu

Một game idle sống bằng hai khoảnh khắc mà người chơi không nhìn thấy code: lúc nạp save (họ quay lại sau vài giờ) và lúc tính tiền vắng mặt (họ được trả bao nhiêu). Sai ở đây là sai im lặng và không đảo được: save bị ghi đè thiếu, buff vô hạn hồi sinh từ file, tiền offline thổi phồng do chỉnh giờ. Trang này gom toàn bộ luật mà game gốc áp ở hai khoảnh khắc đó — từng field, từng thứ tự gọi, từng chỗ kẹp — để bạn chép đúng phần phòng thủ chứ không chỉ phần "đọc JSON".

Game gốc làm gì

Định dạng và đường nạp

Điều game làm Con số / hằng số Mức bằng chứng Nguồn
Save là Base64(UTF8(JSON của JsonUtility)), không mã hoá, không salt; bản chính NoSaveFile.json + dự phòng .bak trong persistentDataPath/saves/ 2 file disasm-proven doc 07 §3.2c
Chống gian lận chỉ trong bộ nhớ (ObscuredInt của Anti-Cheat Toolkit); SaveData khai báo kiểu thường (public int Dia) 0 lần Obscured trong SaveData metadata-proven doc 07 §2.1
Đường boot: LoadData.Load(data)data == null ? InitNewGame() : OfflineProgressManager.EvaluateAndApply(data, ContinueLoad)offline tính TRƯỚC, ContinueLoad là continuation 1 người gọi duy nhất disasm-proven doc 07 §3.2c
Bản cài mới: SaveSystem.TryLoadFrom có guard File.Exists cho cả bản chính lẫn .bak ⇒ null ⇒ InitNewGame disasm-proven doc 07 §2.8 (G330)
LoadData.Load restore trực tiếp 22 field tiền tệ/thời gian/chống gian lận tiến độ lõi (Stage_Now + Level_Now bằng một lệnh 8 byte vì hai int kề nhau ở cả hai struct, Stage_Value_Max, skin) rồi uỷ quyền Upgrade / Rebirth / Gun / Pet 22 + 5 disasm-proven doc 07 §2.8
ContinueLoad35 restore trực tiếp — mỗi field được chứng minh riêng bằng decompiler, không dùng bảng trích tự động (bảng đó từng gán nhầm cả khối Mission_* cho Adventure_manager) 35 disasm-proven doc 07 §2.8 (G328, G329)
Thứ tự trong ContinueLoad: Game_StartContents_Lock_Reload → fan-out 15 hệ (StepUp, DailyGun, PlusPack, Shop, Buff, FreeCash, DiaPig, Setting, Rank, Tutorial, DailyCheck, Fish_Tank, Adventure, Mission) → MarkSaveLoadedCat_manager.Skin_Reload + Balance_ReloadSaveSystem.AllowSave → OfferWall / ServerConfig OnSaveLoaded → log launch AllowSavecuối disasm-proven doc 07 §2.8 (G328)
AllowSave chỉ tồn tại ở hai chỗ: cuối ContinueLoad và cuối InitNewGame ⇒ autosave 10 s không thể ghi đè save khi restore mới đi được nửa đường 2 disasm-proven doc 07 §2.8 (G330)

Ba tầng phòng thủ khi nạp

Điều game làm Con số / hằng số Mức bằng chứng Nguồn
Kẹp buff ngay lúc nạp: Buff_manager.LoadBuffTimes = Clamp(x, 0, 900) (NEON) ⇒ save cũ/sửa tay không hồi sinh buff vô hạn [0, 900] s disasm-proven doc 07 §2.8 (G291)
Bẫy thứ tự tham số: ContinueLoad đọc PowerBuffTime trước MoneyBuffTime rồi gọi LoadBuffTimes(money, power) — đảo so với thứ tự offset trong SaveData disasm-proven G291
Bất đối xứng null-save của DiaPig.Init: save null ⇒ Dia_Now = 0 và chỉ neo lại đồng hồ tick, bỏ qua Tick_Accumulate ⇒ save bị xoá không đúc được dia offline; timestamp parse RoundtripKindchuyển về UTC (không giả định) disasm-proven doc 07 §2.8 (G327)
Chép phòng thủ: StepUp chép ≤ 5 cờ, kẹp theo cả độ dài mảng lưu lẫn mảng mình; DailyGun.Week_Key rơi về "" (không null); PlusPack thay Purchase_dateTime = new DateTime[3] trước khi chép ⇒ mảng lưu ngắn để lại default(DateTime) chứ không phải giá trị cũ 5 / 3 / 3 disasm-proven doc 07 §2.8 (G309)
Gun_manager.AD_Gun_OnLoaded chỉ vô hiệu hoá súng quảng cáo (khác ô ⇒ bỏ; hết hạn ⇒ bỏ) — không nhánh nào cấp súng; FreeCash_manager xoá cờ khi save null disasm-proven G291
Gun_manager.Awake chuẩn hoá kho súng về đúng 16 ô -1 và chép ô cũ sang ⇒ bản cài mới không lỗi index 16 disasm-proven G296
Shop_manager có static ctor cấp Dia_FirstBuy_Used = new bool[5]; Is_Dia_FirstBuy = ba phép thử index < 0, Length <= index, !Used[index] — thiếu ctor là save mới lỗi ở lần đầu chạm UI mua-lần-đầu 5 disasm-proven G350
Danh tính khách ghi TRƯỚC khi đăng ký: Rank_manager.Login sinh user_<8 số>@guest.com + mật khẩu 16 ký tự + tên User_<n>ghi vào save trước lời gọi đăng ký (không nằm trong callback thành công) ⇒ danh tính ổn định qua các lần chạy offline disasm-proven G322, G323
Cờ mute của Setting đóng gói: BGM một byte, SFX + Vibration một word — ba cờ không độc lập về offset nguồn; UI_Reload mỗi toggle là một cặp object bật/tắt theo phủ định cờ 3 cờ, 2 store disasm-proven G329, G311
Fish_Tank.ReloadAll: tank ẩn ⇒ hoãn (_pendingReload, xếp cá cần RectTransform sống); refill ≤ min(kho, pool 20); Fish_Tank không có singleton — đi qua Fish_manager.instance.fishTank 20 disasm-proven G327, G330
Mission_manager.is_Vipstatic (không phải instance) — restore bằng một word store dùng chung với is_SeasonStart disasm-proven G329
Tutorial_manager.SetCleared gọi StopAllCoroutines trước CompleteTutorial — bắt buộc, tránh MoneyCheckLoop gọi lại SetStep disasm-proven G291
Mọi ngày giờ trong save được parse là UTC: DateParseSafe — bỏ null/khoảng trắng, TryParseExact("yyyy-MM-dd HH:mm:ss") rồi TryParse, InvariantCulture, AssumeUniversal \| AdjustToUniversal ⇒ đổi múi giờ máy không dịch được cửa sổ vắng mặt; rồi SanitizeYear = guard Phật lịch Thái (±543 khi năm lệch ≥ 400; chấp nhận 2020..năm nay + 1) 15 điểm gọi / 6 file disasm-proven doc 07 §2.8 (G372)
Khối Trip có hai nhánh: save mới ⇒ cấp 20 vé + đóng dấu bây giờ; restore ⇒ nạp Lv/Exp/vé và parse mốc nạp với "bây giờ" làm mặc định ⇒ mốc hỏng làm đồng hồ bắt đầu lại, không chờ từ năm 1 20 disasm-proven doc 07 §2.8 (G372)
Sổ cái thưởng theo thời gian (Time_manager.RecordGain): mỗi khoản cấp ghi UTC + mức trust lúc cấp + session id + đồng hồ đơn điệu; giữ 32 mục; từ chối khi trust = None; đang kiểm giờ thì cảnh báo nhưng vẫn ghi (caller phải gate bằng CanGrantTimedReward); bắn OnGainRecorded 32 disasm-proven doc 07 §2.6 (G377)
Đính chính DiaPig: khi nạp, _lastTickTime giữ giá trị file (UTC hoá, chỉ kẹp nếu ở tương lai — chống vặn giờ tới); thứ được đóng dấu lại là _lastTickMono (mốc đơn điệu gate tích luỹ). Nghiệm thu "tick cuối = giá trị file" là đúng disasm-proven G377
Hàng 27 nút HUD bị tutorial ẩn: Tutorial_manager.Start — chưa xong tutorial Power Lv < 1 ⇒ tắt bộ 32 object; ngược lại CompleteTutorial bật lại tất cả. Caveat: Start có thể chạy trước khi save được nạp ⇒ save cũ có thể bị bắt đầu tutorial nhầm 27 / 32 disasm-proven doc 07 §2.8 (G375)
Đường khởi tạo save mới cũng phải đặt baseline base-100: Relic_manager.Start = Value_Reload; Skin_manager.Start = UI_First_Reload + Skin_Value_Reload + Cat.Skin_Reload; skin 0 luôn mở 100 disasm-proven doc 07 §2.4 (G374)
Đồng hồ của game: UtcNow = giờ máy + offset đã lưu; khi đã đồng bộ, dùng mốc máy chủ đơn điệu (anchor + thời gian đơn điệu trôi) trong lúc một kiểm tra đang chờ, 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 mốc tin cậy cuối; Now = UTC + múi giờ (cache 60 s), Today = Now.Date, IsUsable = trust ≠ None disasm-proven doc 07 §2.6 (G391)
Static ctor của đồng hồ: Stopwatch đơn điệu, AwaitingCheck = true mặc định, BuildUtc = 2026-08-01, session id, mốc nhảy = UtcNowthiếu nó là mọi ngày thành năm 1 và save mới crash disasm-proven doc 07 §2.6 (G391)
Save() của game throttle 5 s thời gian thực; QuitGame = save rồi thoát 5 s disasm-proven doc 01 §2, doc 07 §2.1 (G395)

Thu nhập offline — cửa sổ thời gian

Điều game làm Con số / hằng số Mức bằng chứng Nguồn
Sàn: vắng dưới 30 s ⇒ không trả gì (không farm được bằng cách bật/tắt app) 30 s disasm-proven doc 01 §5.8 (G332)
Trần: 28 800 s + Relic Offline_Value (phút) × 60 — 28 800 có hai nguồn độc lập (mặc định ctor giá trị scene) 8 h (+ relic) disasm-proven + scene-YAML doc 09 §4b (G332)
Đồng hồ bị vặn lùi (total < 0) ⇒ trả 0, không quấn vòng 0 disasm-proven G332
ComputeElapsed trả hai giá trị: elapsed (đã cap — cái được trả tiền) và absent (khoảng thật — chỉ để hiện "bạn đã vắng N giờ"). Gộp hai cái ⇒ trả thừa hoặc báo sai 2 out-param disasm-proven G332
Ba nhánh trust: PendingCountPrefix("offline:") > 2từ chối; nhánh tin cậy parse LastTrustedUtc định dạng "o" rơi về LastSaveTime; nhánh không tin cậy dùng DateTime.UtcNow thô ngưỡng 2 disasm-proven (tái hiện, không kích hoạt) G332
EvaluateRoutine gọi ComputeElapsed HAI LẦN, cả hai cần thiết: lần 1 = cổng rẻ (không nợ ⇒ thoát trước UI/đồng bộ giờ, kết quả bỏ); lần 2 chạy sau Time_manager.WaitForCheck(5 s) — sau reconcile giờ máy chủ — và giá trị lần này mới được trả 5 s disasm-proven doc 01 §5.8 (G334)
Khoản trả được log qua Time_manager.RecordGain("offline:{0:F1}h")cùng tiền tố "offline:" mà guard chống lạm dụng đếm ⇒ log và bộ đếm dùng chung một kênh disasm-proven G334
Mô hình tham chiếu Python của dự án (MIN 30 / MAX 28 800 / relic phút × 60 / lùi ⇒ 0) khớp chính xác disasm — đã so, không giả định 0 lệch VERIFIED G332
Simulator dùng bảng riêng: DegreeHealthMult {1, 1.8, 3, 35}, DegreeMoneyMult {1, 1.8, 3, 100} (bằng đúng hai mảng bậc cây của Tree_manager) và DegreeRates 5×4 (cùng blob với Tree_Grid.degreeRates — xem 05) 4 + 4 + 20 metadata-proven (blob SHA) G353
Offline không đi qua chuỗi nhân online: ghi thẳng Money/Money_Accum; áp Upgrade + Rebirth + Skin + Pet + Relic + đường cong stage/bậc; chỉ bỏ buff quảng cáo ×3; tự mua nâng cấp; không đẩy stage vượt Stage_Value_Max. Đính chính: bản trước ghi Skin/Pet/Relic bị bỏ — sai (G382) VERIFIED (simulator dựng trọn + oracle 17 chữ số) doc 01 §5.8, doc 09 §4b (G382)

Nghiệm thu trên save thật — đã kiểm vs còn mở (đợt 5, trung thực)

Bản dựng lại nạp save thật Stage 3-3 (cùng file được đẩy vào game gốc để so):

Kết quả Trạng thái Nguồn
37/38 field khớp file game gốc: Stage/Level/mốc 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, bộ đếm Mine/Fish, cờ, mute; danh tính khách round-trip không đổi; 0 ngoại lệ; map Desert Stage 3 vẽ đúng ĐÃ KIỂM G367
Thanh mua-được của 3 panel nâng cấp đo với tiền thật 47.7 / 48.5 / 57.2 % ĐÃ KIỂM G371
Đồng hồ của game (UtcNow + .cctor thiếu) — gốc rễ của ngày năm 1 và crash save mới ĐÃ SỬA và KIỂM: build 18 đường save mới 0 ngoại lệ G391, G394
Mốc nạp vé Trip / tick DiaPig vẫn năm 1 — nửa sau của việc nạp vẫn chưa chạy ở build 20, giờ vì "routine is null" ở bước offline (continuation chưa được gọi); hết null này là đuôi chạy HỆ QUẢ của null offline — chờ G405
Đường offline: simulator đã dựng trọn và harness độc lập tái tạo khoản trả của game gốc từ cùng save đúng 17 chữ số (+1 627 259.5021456285, mục "Kinh tế offline" bên dưới) ĐÃ KIỂM ngoài thiết bị G382
Khoản trả offline trên thiết bị (build 18): đường offline đã chạy (ghi sổ offline:2,2h) và tự mua nâng cấp, rồi huỷ giữa chừng vì một key tiền null (Get_Money có tham số chuỗi còn rỗng) sau khi đã tiêutiền GIẢM 2 497 189 → 126 356. Gốc rễ: key cache tiền stub null — đã sửa. Build 20: đường offline chạy tới cuối rồi ném "routine is null" — null đã định danh (G409): coroutine hiện panel kết quả (ShowAfterFrame: chờ 1 frame → bật panel → nạp số trước/sau) còn là stub. Dự đoán mô phỏng cho 5.4 h vắng mặt: +19 790 601, level → 41/41/41 (11 nâng cấp), stage giữ 2-2dự đoán, chưa xác nhận; nhạy ±1 s ≈ ±17 300 tiền CHẠY TỚI CUỐI, CÒN 1 NULL (đã định danh) G405, G409
Đường ghi save (công tắc bật, G392): build 20 — ghi được trên save thậtnghiệm thu round-trip: 122/128 giống, xoay .bak xác nhận, đọc lại sạch file vừa ghi, không gì bị xoá qua 5 lần chụp; khác biệt đúng kỳ vọng (tiền, tiền tích luỹ, mốc save). Trên game mới còn lỗi: autosave ném ở snapshot PlusPack VERIFIED trên save thật (122/128); còn lỗi trên game mới G405, G414
Hai lỗi THẬT tìm ra nhờ round-trip (G414): (1) tốc độ kiếm tiền trên bản học ≈ ×300 game gốc (3.2 M/s vs 8.6 K/s cùng file) — nghi một hệ số base-100 áp thô trong tích tiền; (2) TotalPlayTime đứng yên (game gốc +129 s). Tự mua nâng cấp offline ([36,36,35] → [41,41,41]) khớp dự đoán mô phỏng ⇒ nhiều khả năng đúng — lỗi là độ lớn tiền, không phải tự mua MỞ — ĐANG SỬA G414
Nguy cơ danh tính: khi nửa sau nạp bị bỏ qua, bộ ba tài khoản khách rỗng trong bộ nhớ ⇒ đăng nhập có thể đúc lại danh tính; chưa ghi đè đĩa chỉ vì lần ghi phát lại dữ liệu đã nạp. Nghiệm thu save phải kiểm bộ ba trong bộ nhớ, không chỉ trên đĩa MỞ — hệ quả của null offline G414
(lịch sử) Trước khi bật: CreateFromCurrentState = 127/128 field (1 field game gốc không bao giờ ghi); 7 mảng chụp snapshot (Clone: đếm súng theo bậc, cờ mua-lần-đầu, 2 bảng nhận thưởng pass, Core Trip, cờ StepUp, đếm DailyGun) vs 13 mảng ghi theo tham chiếu — quan trọng vì mảng tham chiếu tiếp tục đổi sau khi chụp; ghi file .tmp rồi xoay .bak. Mô hình round-trip trên save thật: theo game gốc 126 giống / 2 giải thích được (2 dấu thời gian); với bản dựng lại hiện tại 9 field bị phá ⇒ đúng là lý do chưa bật DỰNG XONG, TẮT theo quyết định G388
Bỏ sót phía đọc (Lv_Now, 4 bộ đếm nhiệm vụ, cờ Luxury, vòng ExpGet) — phát hiện nhờ mô hình round-trip ĐÃ SỬA (6 restore bổ sung) G389, G391
Hàng nút HUD trên save đã nạp — render (build 18), build 20 thêm bàn tay tutorial trên panel nâng cấp đầu; tutorial tiến Challenge → GunOpen (t = 14 s), chưa Done ĐÃ KIỂM (một phần) G394, G405
Map kẹo ngọt ở save Stage 3 ĐÃ GIẢI THÍCH — không phải bug: chủ đề map = Stage_Now % 10 (stage 2 = candy); 10 chủ đề lặp mỗi 10 stage G406
Panel điểm danh ngày 2 trên save thật CHƯA THẤY (build 17: không panel trong 262 s) G386
Đạn không gây sát thương (G424) — hằng hướng-camera thiếu pitch 40° ⇒ chia cho 0 ⇒ đạn ở vô cực. Build 20: 104/104 va chạm đi trọn chuỗi, cây đổ, tiền + lá rơi, thanh máu, hàng nút HUD + bàn tay tutorial — GAME CHƠI ĐƯỢC (xem 04) ĐÃ ĐÓNG (thiết bị) G400, G405
Số bay (sát thương / tiền) — CanSpawnText dựng lại (lưới 1.5 đơn vị, chỉ X/Y, làm tròn ngân hàng; cooldown 0.2 s tiền / 0.12 s sát thương); build 20 thấy tờ tiền bay ĐÃ ĐÓNG G402, G405
Đính chính baseline: trên save mới, HUD tối giản (hàng nút ẩn tới lần nâng Power đầu) là đúng SOT — kỳ vọng khác nhau theo loại save ĐÃ KIỂM (đọc binary) G375

Vì sao đường GHI cố ý tắt (G370): Save() = Save(CreateFromCurrentState()); hàm tạo snapshot lớn (245 lời gọi, 112 field) còn là stub. 45 field là nghịch đảo cơ học của đường nạp, 64 cần đọc riêng. Một cây ghi dở dang sẽ ghi đè save thật bằng null (12 field ngày giờ, level nâng cấp, level prestige ⇒ mất prestige) — tệ hơn không ghi. Kế hoạch: sửa 3 residual nhìn thấy được trước, rồi cây ghi với cổng phía ghi riêng (đủ 112 field + mọi SaveData_return, 0 stub), rồi mới bật Save(); nghiệm thu = nạp save thật → ép ghi → diff từng field với file gốc (chỉ timestamp/dẫn xuất được khác, mỗi cái phải giải thích).

Kinh tế offline — 12 luật đã VERIFIED và một ví dụ với số thật (đợt 7, G382)

Đây là mô hình kinh tế thứ hai của game (doc 01 §5.8 / doc 09 §4b) — nay đọc trọn từ binary và kiểm chứng bằng oracle: một harness Python độc lập (mô phỏng chính xác số học InfVal + float32) chạy trên save thật cho ra cùng 17 chữ số với game gốc. Vì R6a (kinh tế phải chạy độc lập), đây là mẫu để bạn viết simulator của mình.

# Luật Nguồn
1 Vòng lặp khi còn > 0.05 s, tối đa 30 000 bước, ngân sách 60 ms/frame rồi yield disasm-proven (G1003)
2 Mỗi bước: tự mua nâng cấp — level thấp nhất trước, hoà thì Power > Speed > Money, giá ở level hiện tại disasm-proven
3 clearTime = combat × 1.3 + 5 s; vừa thời gian ⇒ cộng tiền một wave, tiến wave (5 level/stage) nhưng không bao giờ vượt mốc cao nhất (Stage_Value_Max) — cày lại wave cao nhất disasm-proven
4 Không còn vừa một lần clear ⇒ cộng tiền/giây × thời gian còn lại, dừng disasm-proven
5 Wave không thể clear (combat > 180 s) ⇒ cày tiền/giây tới khi mua được nâng cấp rẻ nhất (trần 1 năm), mua, thử lại disasm-proven
6 Số cây thường = clamp(10·stage + 30, 30, 100) × (level + 1); HP/tiền cây = stage × trọng số bậc (degreeRates × DegreeHealthMult/DegreeMoneyMult); level 4 thêm một cây boss (HP ×35, tiền ×100) disasm-proven
7 Sát thương = Damage × Power/100 × Rebirth/100 → cắt, × Skin/100 → cắt, × (Fish+100)/100 → cắt, × Relic/100 → cắt; chí mạng × (CriVal+125)/100 → cắt, × RelicCri/100 → cắt; kỳ vọng = trộn theo tỉ lệ crit. Cắt ở mỗi bước % disasm-proven
8 Tốc độ đánh = clamp(Speed[lv] + RebirthSpd + SkinSpd − 30·stage − 200, 100, 10000); giây/phát = clamp(AtkSpeed/atkSpd × 100, 0.05, 2); one-shot giết nhiều cây theo loại súng (Laser/Pierce 10, nổ/missile/sniper 5, shotgun = số viên, đơn 1) disasm-proven
9 combat = max(0.05/petSpeed × số cây, số cây / killRate) (+ HP boss / DPS); > 180 s ⇒ vô cực disasm-proven
10 Tiền/giây = tiền stage × bậc TB × Money/100 × Rebirth/100 × Skin/100 × Pet/100 × Relic/100 × killRateKHÔNG cắt (phần lẻ .5021456285 của oracle sinh từ đây) disasm-proven
11 Giá trị bão hoà ở 1e15 disasm-proven
12 Áp kết quả: ghi thẳng Money/Money_Accum, xoá cache, Get_Money(0) để HUD làm mới, đẩy level nâng cấp, stage/level/mốc, Contents_Lock_Reload disasm-proven

Ví dụ với số thật (save Stage 2-2, mốc 22, nâng cấp [36, 36, 35], Money 2 497 189.34, súng lục Damage 100 / AtkSpeed 0.6, không skin/pet/relic/cá, vắng mặt 802.99 s):

Bước Điều xảy ra Số
Combat một wave 82.317 s ⇒ clearTime = 82.317 × 1.3 + 5 112.012 s
2 lần clear ở Money lv 35 722 844 mỗi lần ⇒ vượt 3 900 000 +1 445 688
Tự mua Money là level thấp nhất (35 < 36) ⇒ mua Money lv 36 −3 900 000
5 lần clear ở lv 36 780 462 mỗi lần +3 902 310
Đuôi 18.907 s 9 481.1678 /s × 18.907 s — nơi phần lẻ sinh ra +179 261.502
Wave 2-3 = 23 > mốc 22 ⇒ không tiến, cày lại 2-2 giữ nguyên
Kết quả kiếm 5 527 259.50, tiêu 3 900 000 ⇒ +1 627 259.5021456285; Money 4 124 448.8461326313; level [36, 36, 36] 17/17 chữ số = game gốc

Thời điểm được trả suy ra từ elapsed (mốc tin cậy + 802.993 s = 20:19:28.370Z) trùng độc lập với dấu autosave của game gốc ⇒ elapsed được kiểm chéo, không phải khớp cưỡng (G1005). Hai điều đáng học: prestige và bộ sưu tập vẫn sinh lợi khi tắt game (chỉ buff quảng cáo thì không — offline không phải kênh bán buff), và cày lại wave cao nhất nghĩa là offline không bao giờ "chơi hộ" tiến độ mới — người chơi phải tự mở stage kế.

Công thức / pseudocode

// ---- L1 ANALYSIS (doc 07 §2.8, G328/G330) — thứ tự nạp save ----
Load(data):                                                           // thứ tự đầy đủ, 91 call (G359/G361)
    if data == null: InitNewGame(); AllowSave(); return
    A  tiền tệ  : Money, Money_Accum, Ruby (InfVal); Dia, _getDia, _useDia (Obscured); PetCoin; TotalPlayTime; is_TimeHacker; OfflineFail*
    B  đồng hồ  : SavedOffsetSec, HasSavedOffset, LastTrustedUtc, Ledger, TimeStrike*, UsedCodes_Log
                  RỒI MỚI Time_manager.Init()   // sau khi offset/ledger đã có
                  ServerConfig.LoadFromSaveData(); TimeHack_* ; Rank_btn.SetActive(false) nếu bị gắn cờ chỉnh giờ
    C  nâng cấp : Upgrade_manager.LoadData(); Rebirth_manager.LoadData(); Rebirth_Count
    D  súng     : chép TỪNG PHẦN TỬ, chặn theo độ dài; AD_Gun_*; Gacha_Draw_Count; AD_Gun_OnLoaded(); Has_Merged back-fill
    E1 pet      : LoadLockGroups(); Pet_Reload()
    E2 stage    : Stage_Now + Level_Now (1 lệnh 8 byte), Stage_Value_Max, LoadSentWaveRatios(); Skin_Unlock, Selected_num, Skin_Value_Reload()
    F  cá/relic : FishInventory, UpdateStatTexts(); Relics (+ Balance_Reload NẾU bảng null) ; Value_Reload()
    OfflineProgressManager.EvaluateAndApply(data, ContinueLoad)        // offline TRƯỚC, rồi mới… (không có instance ⇒ ContinueLoad thẳng)

ContinueLoad(data):                                                   // 35 restore trực tiếp + fan-out
    Tree_manager.Game_Start(); Contents_Lock_Reload()
    StepUp/DailyGun/PlusPack/Shop/FreeCash .LoadFromSaveData(data)     // chép phòng thủ (min độ dài, mảng mới trước, "" thay null)
    Buff_manager.LoadBuffTimes(money, power)                           // Clamp(0, 900) — chú ý: đọc power TRƯỚC money
    DiaPig.Init(data.diaPig)                                           // null ⇒ chỉ neo đồng hồ, KHÔNG tích luỹ
    Setting.UI_Reload(); Rank_manager.Login(); Tutorial_manager.SetCleared() if TutorialCleared
    DailyCheck.First_reload(…); Fish_Tank.ReloadAll()                  // tank ẩn ⇒ hoãn
    Adventure: Lv, Exp, Ticket_Now, mốc nạp vé, Maps (vòng lặp lồng, chặn cả hai cấp)
    Mission_manager.Reload(…)                                          // is_Vip là STATIC
    Time_manager.MarkSaveLoaded(); Cat_manager.Skin_Reload(); Cat_manager.Balance_Reload()
    SaveSystem.AllowSave()                                             // <-- CUỐI đường nạp, không sớm hơn
    OfferWall.OnSaveLoaded(); ServerConfig.OnSaveLoaded(); [R1] log launch
// ---- L1 ANALYSIS (doc 01 §5.8, G332/G334) — cửa sổ offline ----
ComputeElapsed(data, out elapsed, out absent):
    if PendingCountPrefix("offline:") > 2: return false               // chống lạm dụng
    now  = trusted ? Time_manager.UtcNow : DateTime.UtcNow
    last = trusted ? parse(LastTrustedUtc, "o") ?? LastSaveTime : LastSaveTime
    total  = (now - last).TotalSeconds                                 // absent  = total (hiển thị)
    capped = min(total, 28800 + Relic.Offline_Value * 60)              // elapsed = capped (trả tiền)
    if total < 0: capped = 0                                           // đồng hồ lùi
    return capped >= 30                                                // sàn 30 s

EvaluateRoutine():
    if !ComputeElapsed(…): ShowResult(); return                        // lần 1: cổng rẻ, kết quả BỎ
    yield Time_manager.WaitForCheck(5f)                                // chờ reconcile giờ máy chủ
    UI_manager.Loading_On()
    if !ComputeElapsed(…, out elapsed, out absent): Loading_Off(); ShowResult(); return   // lần 2: giá trị ĐƯỢC TRẢ
    Time_manager.RecordGain(string.Format("offline:{0:F1}h", elapsed / 3600f))
    yield OfflineProgressSimulator.RunCoroutine(data, elapsed, absent)
    ApplyToManagers(); Loading_Off(); ShowResult()
// ---- Gợi ý framework (không phải SOT) ----
ISaveSlot     : Load() -> Restore(all systems, ORDER declared) -> AllowSave()   // AllowSave là bước cuối, tự động
IRestoreGuard : Clamp(timers), NullSaveResetsAccumulators, DefensiveCopy(min length, fresh array, "" not null)
IOfflineClock : (elapsedCapped, absentTrue) tách bạch; gate rẻ trước UI; tính lại SAU khi đồng bộ giờ

Bẫy đã gặp

  • Bật nạp save khi restore mới xong một nửa. Save của người chơi cũ được đọc, khôi phục thiếu, rồi autosave 10 s ghi đè bằng trạng thái thiếu — mất nhiều giờ chơi trong im lặng. Đây là lý do dự án giữ một "cổng lật" (đếm số method còn rỗng trên đường nạp) và chỉ bật SaveSystem.Load khi cổng về 0 (G330).
  • Cổng lật báo "mở" mà thật ra chưa (G355 → G359). Nghiệm thu trên save thật: 8/38 field nạp ra 0 (tiền, stage, level nâng cấp) trong khi game gốc nạp đúng cùng file. Hai nguyên nhân: Load chỉ là dispatcher 10 dòng (SOT: 91 call), và một coroutine return null làm StartCoroutine ném NRE cắt ngang ContinueLoad. Kiểm toán stub phải đọc thân hàm và đi theo StartCoroutine; điều kiện lật là 38/38 field khớp trên save KHÔNG mới, không phải số stub. May mắn duy nhất: AllowSave nằm sau chỗ NRE nên autosave chưa kịp ghi đè — sửa NRE mà không sửa Load thì sẽ ghi đè.
  • Gọi hàm sinh bảng vô điều kiện trên đường nạp. Exp_Balance_Reload chỉ được gọi khi mảng scene null/rỗng; gọi luôn là ghi đè đường cong designer mỗi lần nạp (G361). Và sửa chỗ đó đã gộp nhầm hai nhánh (mới / restore) của khối Trip — sau mỗi sửa trên đường nạp, đọc lại các rẽ nhánh quanh nó (G372).
  • Một helper parse ngày còn stub = mọi ngày trong save đều năm 1. Nghiệm thu chỉ thấy một triệu chứng (mốc vé Trip); đếm điểm gọi của helper (15) trước khi sửa tại chỗ (G372).
  • Sự kiện thay đổi tiền tệ bất động. Công cụ decompile render event thành accessor rỗng ⇒ mọi += không làm gì, HUD số dư không bao giờ làm mới mà không có ngoại lệ nào (14 event / 9 file). Nếu HUD "đứng im", kiểm event trước khi kiểm kinh tế (G376).
  • Một ngoại lệ trước continuation nuốt cả nửa sau của việc nạp. Bước offline ném ⇒ ContinueLoad không chạy ⇒ 8 field ngày còn năm 1, trông hệt "lỗi restore ngày". Khi nhiều field cùng ở mặc định, tìm điểm ném sớm nhất trước khi sửa từng field (G400).
  • "Hồi quy" có thể là quy trình test. Đẩy file save vào trước khi app tạo thư mục (cài mới) ⇒ app không đọc được. Quy trình: cài → chạy 1 lần → dừng → đẩy file → kiểm md5 → chạy lại (G399).
  • Start của Unity chạy trước khi save được nạp. Quyết định "vào tutorial hay không" trong Start đọc Upgrade_Lv chưa restore ⇒ save cũ bị bắt đầu tutorial; đường nạp phải gọi lại (SetCleared → CompleteTutorial) (G375).
  • Tin bảng trích tự động cho đường nạp. Bộ trích thanh ghi gán nhầm cả khối Mission_* cho Adventure_manager; siết chặt chỉ làm nó giấu lỗi (10 hàng → 2 hàng). Đổi công cụ (decompiler), chứng minh từng hàng (G328, G329).
  • Đặt AllowSave "đâu đó gần cuối". Binary chỉ có hai vị trí; sớm hơn một dòng là mở cửa sổ ghi đè.
  • Quên rằng ctor cũng là code. _moneyCache, _textCooldowns, Dia_FirstBuy_Used, GameTime_Max = 180 đều là khởi tạo field bị công cụ decompile bỏ rơi; thiếu chúng là NRE — hoặc tệ hơn, màn thua ở frame 1 mà không ném gì (G306, G319, G350; doc 08 §3.6).
  • Xoá "lời gọi trùng" ComputeElapsed. Lần 2 mới là lần sau đồng bộ giờ; bỏ nó = trả tiền theo giờ thiết bị chưa kiểm — chính cửa sổ chỉnh giờ mà WaitForCheck tồn tại để đóng (G334).
  • Cho save null đi chung nhánh với save thật ở mọi thứ tích luỹ theo thời gian. DiaPig.Init cố ý bất đối xứng; chép "đối xứng cho gọn" là tặng dia miễn phí cho người xoá save (G327).
  • Parse timestamp không ép về UTC. Chuỗi mang offset bị hiểu là giờ địa phương ⇒ khoảng vắng bị thổi phồng vài giờ.
  • Danh tính khách chỉ lưu khi đăng ký thành công. SOT lưu trước; đổi sang "chỉ lưu khi thành công" là mỗi lần chạy offline đúc một danh tính mới (G323).

Checklist khi tự làm

  • [ ] AllowSave()bước cuối của cả hai đường (InitNewGameContinueLoad), không có chỗ thứ ba.
  • [ ] Mọi timer có thể cấp thưởng (buff, vé, tick tiền) được kẹp ngay lúc nạp, không chỉ lúc cộng.
  • [ ] Mọi bộ tích luỹ theo thời gian có nhánh save null riêng: reset + neo lại đồng hồ, không tích luỹ.
  • [ ] Chép mảng từ save: min(độ dài lưu, độ dài mình); cấp mảng mới trước khi chép; chuỗi rỗng thay null.
  • [ ] Timestamp: parse RoundtripKind, ép ToUniversalTime() khi chưa phải UTC.
  • [ ] Offline: sàn (30 s) + trần (8 h + relic) + đồng hồ lùi ⇒ 0; tách elapsed (trả) khỏi absent (hiển thị).
  • [ ] Offline: cổng rẻ trước UI, tính lại sau khi đồng bộ giờ; log và bộ đếm chống lạm dụng dùng chung tiền tố.
  • [ ] Có một bộ field nghiệm thu save-load (tiến độ, kinh tế, quyền lợi, timer, danh tính, cờ) để diff hai bản trước khi tin đường nạp.
  • [ ] Đọc .ctor / .cctor của mọi class trước khi cho rằng field "tự có".
  • [ ] Danh tính khách: quyết định tường minh lưu trước hay sau khi đăng ký, và ghi lý do.

Điều chưa biết (UNKNOWN)

  • Tầng đối chiếu trust trong Time_manager.Init (so TrustLevel, neo offset, reconcile máy chủ) — cố ý không tái hiện trong bản restore (anti-cheat, ngoài phạm vi); nội dung đã đọc nhưng không phải bằng chứng cho game của bạn (G321).
  • Số học mô phỏng offline (CalcFarmMoneyPerSec, CalcMoneyPerStage, BuyAllAffordable) — chưa dựng lại; mới biết đầu vào và các bảng (doc 09 §5).
  • ~~Bảng XP Trip nào sống sau khi nạp save~~ — ĐÃ GIẢI (G361): mảng scene (lưu dạng hex blob) sống; hàm sinh chỉ chạy khi mảng trống.
  • ~~Nghiệm thu save-load 38/38~~ — ĐÃ CHẠY: 37/38 với 4 residual có tên (bảng "đã kiểm vs còn mở" ở trên, G367).
  • Đường ghi save (112 field) — MỞ theo quyết định (fail-safe, G370); chưa có cây ghi nào được flip.
  • ~~Save mới: 7 tích luỹ base-100 = 0~~ — ĐÃ ĐÓNG (G374): Relic_manager.Start / Skin_manager.Start đặt baseline; chờ build kế xác nhận.
  • ~~Simulator offline chưa dựng~~ — ĐÃ DỰNG TRỌN và khớp oracle 17 chữ số ngoài thiết bị (G382). Còn chờ: khoản trả trên thiết bị (9 stub R13 ở file khác, G1002) và nghiệm thu lại residual (2).
  • ~~Đường ghi save đang làm~~ — dựng xong (G388), 7 Clone đã ghim; chưa bật vì 6 getter inline + bỏ sót phía đọc (G389). Bảng trên.
  • Tại sao một getter nhỏ lại quan trọng: Time_manager.UtcNow trả ngày mặc định là đủ để guard Phật lịch loại mọi ngày trong save và làm save mới dừng khởi tạo — bài học "getter inline là stub vô hình" (doc 08 §3.6).
  • ~~Hệ điểm danh / retention (DailyCheck)~~ — ĐÃ DỰNG XONG (G378): quy tắc + bảng 70…1500 dia ở 11 §7; panel Ngày-2 trên save thật chưa nghiệm thu lại.
  • Các field SaveData không được đường nạp đọc (nếu có) — chưa liệt kê; bản đồ hiện có phủ 22 + 5 + 35 field mà binary ghi vào manager.
  • Nghiệm thu chéo: lần 1 FAIL 8/38 (đã sửa — G355/G359); lần 2 37/38 + 4 residual (G367). Chưa có lần nào 38/38.

Đọc thêm