14-luu-game-va-offline.md · Cập nhật lần cuối: 2026-09-05 08:45 +07 · sha256 nguồn 6e4f44cc758514 — 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 và 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 |
ContinueLoad có 35 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_Start → Contents_Lock_Reload → fan-out 15 hệ (StepUp, DailyGun, PlusPack, Shop, Buff, FreeCash, DiaPig, Setting, Rank, Tutorial, DailyCheck, Fish_Tank, Adventure, Mission) → MarkSaveLoaded → Cat_manager.Skin_Reload + Balance_Reload → SaveSystem.AllowSave → OfferWall / ServerConfig OnSaveLoaded → log launch |
AllowSave ở cuố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 RoundtripKind và chuyể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> và 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_Vip là static (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 và 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 = UtcNow — thiế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 và 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:") > 2 ⇒ từ 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 và 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êu ⇒ tiề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-2 — dự đ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ật và nghiệ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 × killRate — KHÔ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.Loadkhi 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:
Loadchỉ là dispatcher 10 dòng (SOT: 91 call), và một coroutinereturn nulllàmStartCoroutineném NRE cắt ngangContinueLoad. Kiểm toán stub phải đọc thân hàm và đi theoStartCoroutine; đ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:AllowSavenằm sau chỗ NRE nên autosave chưa kịp ghi đè — sửa NRE mà không sửaLoadthì sẽ ghi đè. - Gọi hàm sinh bảng vô điều kiện trên đường nạp.
Exp_Balance_Reloadchỉ đượ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
eventthà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 ⇒
ContinueLoadkhô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).
Startcủa Unity chạy trước khi save được nạp. Quyết định "vào tutorial hay không" trongStartđọcUpgrade_Lvchư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_*choAdventure_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àWaitForChecktồ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.Initcố ý 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()là bước cuối của cả hai đường (InitNewGamevàContinueLoad), 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 thaynull. - [ ] Timestamp: parse
RoundtripKind, épToUniversalTime()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ỏiabsent(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/.cctorcủ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(soTrustLevel, 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.UtcNowtrả 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
SaveDatakhô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
../framework/07-progression-managers.md§2.8 — bảng 35 field + ba tầng phòng thủ, §3.2c — định dạng save và quy tắc khôi phục../framework/01-economy-money.md§5.8 — kinh tế offline là mô hình thứ hai../framework/09-prestige-rebirth.md§4b — offline và prestige, bảngDegree*../framework/08-architecture-and-scale.md§3.6 — vì sao ctor bị bỏ rơi và hàm bị gộp- 10 — Tiến trình & mở khoá — vé Trip, XP Trip,
Game_Win