CatGunner CookbookBardCat 1.1.59 · L1 ANALYSIS · tài liệu học
Trang chủTài liệu framework › 01 — Kinh tế & tiền tệ (Economy / Money)
Nguồn: 01-economy-money.md · Cập nhật lần cuối: 2026-09-06 23:05 +07 · sha256 nguồn 83a859e7734a

01 — Kinh tế & tiền tệ (Economy / Money)

Nguồn: chưng cất từ quá trình restore SOT com.Chodun.CatGunner 1.1.59 (tên nội bộ dự án: BardCat). Cấp bằng chứng: mọi C# trích trong tài liệu này là L1 ANALYSIS — dựng lại clean-room từ ARM64 disassembly của libil2cpp.so + quan sát runtime. Không phải source gốc. Ràng buộc: tài liệu này KHÔNG thay thế và KHÔNG làm yếu bản restore 1:1 trong [restore project: CatGunner].


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

Một game idle phải cộng dồn số tiền vượt xa double, phải nhân nó qua một chuỗi multiplier do người chơi mở khoá, phải hiển thị nó gọn trên màn hình điện thoại, và phải làm tất cả những việc đó mỗi lần một con quái chết — tức hàng trăm lần mỗi phút. CatGunner giải quyết trọn vẹn bài toán này. Đây là mô tả đủ chính xác để tái hiện trong một game idle MỚI.


2. Stable truths

Claim Mức bằng chứng Nguồn / provenance Phạm vi áp dụng
Kiểu số lớn của game là InfiniteValue.InfVal — package "Infinite Value (BigDecimal)" (Juste Tools, Asset Store id 194963), KHÔNG phải bignum tự viết package-source [report: codegen_lane.md] §5 (G007); quy tắc dự án (nội bộ) R2; [report: project_lane.md] §4 Mọi giá trị tiền / máu / sát thương
Layout serialize của InfVal = [SerializeField] string s_digits; rồi [SerializeField] int s_exponent;, cộng BigInteger _digits không serialize package-source + scene-census [report: project_lane.md] §4 ("Infinite Value serialized-layout check"); scene Main.unity serialize InfVal inline Quyết định tính tương thích khi nâng version package
SOT ship một major version CŨ HƠN v2.2.0 trên store; API format cũ (ToString(int, string[] unitsList, IFormatProvider), DisplayOption, unitsList, Parse(...,unitsList,...)) đã bị v2.2.0 gỡ bỏ disasm-proven RVA 0x44E3AB4, 0x44E3B38, 0x44E3A34; [evidence codegen: README.md] Rủi ro version-drift khi mượn package thương mại
SOT có thêm method InfVal_change() (RVA 0x44E47D4) — v2.2.0 KHÔNG có disasm-proven [evidence codegen: InfVal_change.c] Mọi call-site hiển thị số
InfVal_change() = mag = exponent + digits.ToString().Length; if (mag>=4) mag = (mag-1)%3+3; return ToString(mag); disasm-proven RVA 0x44E47D4, decode trong _evidence/InfVal/README.md Notation số lớn trên UI
Ký hiệu trên màn hình = tối đa 3 chữ số có nghĩa (kèm .NN nếu cần) + hậu tố chữ cái thường (a, aa, …). Ví dụ thật: 297a, 203.09a, 139.6a runtime-verified Hook method thật trên thiết bị; [evidence codegen: infval_change_parity_samples.json] Mọi text tiền/sát thương/dia
Chỉ có ĐÚNG MỘT formatter hiển thị: InfVal_change()29 caller trong code game; mọi overload ToString khác có 0 caller (chỉ thunk reflection IL2CPP 0x28227xx) VERIFIED (CodeRefsTo, IDA) [evidence codegen: UPPERCASE_FINDING.md] Không có formatter thứ hai
Đơn vị hiện CHỮ HOA ở text bay (3.12A) và chữ thường trên HUD (3.12a) là do ART CỦA FONT, không do code: chuỗi giống hệt nhau, không có ToUpper nào trên đường vẽ VERIFIED như trên; bảng 42 ký tự của Assets/Font/Damage.asset (đã kiểm độc lập) Không bao giờ thêm ToUpper
Gamemanager giữ tiền tệ ở static field: Money @+0x28, Money_Accum @+0x48, Ruby @+0x68 (đều InfVal), PetCoin @+0xB8 (int); DiaObscuredInt disasm-proven Static layout từ class off_5E2AE78static_fields[+0xB8]; [evidence codegen: economy_core.cs.evidence]; [report: reexport_lane.md] §5.2 Kiến trúc state kinh tế
4 sự kiện thay đổi tiền tệ: OnMoneyChanged @+0x08, OnDiaChanged @+0x10, OnRubyChanged @+0x18, OnPetCoinChanged @+0x20 (đều System.Action static) disasm-proven như trên Mẫu UI-refresh
Bốn sự kiện đó là field-like event (public static event Action X;). Cpp2IL render chúng thành accessor add/remove [CompilerGenerated] rỗng ⇒ trong bản dựng lại 14 event / 9 file từng bất động — mọi += không làm gì, HUD tiền tệ không bao giờ được làm mới (G376, R14). Nếu số dư trên HUD "không nhúc nhích", kiểm event trước khi kiểm kinh tế disasm-proven + quy tắc R14 G376; quy tắc dự án (nội bộ) R14 Bẫy khôi phục UI-refresh
Get_Money(v) = v = MoneyValue_return(v); Money += v; Money_Accum += v; OnMoneyChanged?.Invoke(); disasm-proven + runtime-verified RVA 0x2A4ECBC; fired 85× trong cửa sổ trace 40 s Đường thu nhập chính
Get_Money(v, Vector3 pos) = như trên + Money_Txt_Spawn(v.InfVal_change(), pos) disasm-proven + runtime-verified RVA 0x2A4EDD8; [runtime trace: econ_validate.js],run_econ.py} Floating text thưởng
MoneyValue_return (RVA 0x2A4E7B0) là chuỗi nhân thu nhập, KHÔNG phải hàm làm tròn disasm-proven + runtime-verified [evidence codegen: MoneyValue_return.cs.evidence] Toàn bộ progression kinh tế
Chuỗi nhân: Upgrade × Rebirth × Skin × Pet × Relic (mỗi cái là phần trăm, /100) rồi × 3 nếu Money_Buff đang bật disasm-proven như trên Công thức thu nhập
Live parity: input 20 → 152.2, input 60 → 456.6 ⇒ hệ số ổn định ×7.61 ⇒ xác nhận thuần nhân, không có cộng/offset. Nhưng state của phiên đó không được ghi lại ⇒ không tách được thành từng mắt runtime-verified [evidence codegen: moneyvalue_return_parity_samples.json] (25 mẫu) Kiểm tra đầu-cuối
Hai cặp vào/ra live ở trạng thái đó: 36.0 → 94.6860.0 → 157.80cả hai đúng ×2.63 runtime-verified như trên (MoneyValue_return ra qua thanh ghi X8) Vector Tier-A mới
Upgrade_manager.Money_return() CHÍNH LÀ con số hiển thị trên HUD: "Money Lv.13" hiện 263 runtime-verified + observed như trên Panel hiển thị thẳng ô bảng (khớp D010)
Cache LRU cho MoneyValue_return: Dictionary<string,InfVal> _moneyCache + Queue<string> _moneyCacheKeys, trần 50 phần tử, key sinh bởi GetMoneyCacheKey(value) disasm-proven RVA 0x2A4E7B0; GetMoneyCacheKey fired 86× vs MoneyValue_return 86× Tối ưu hot-path
Key cache = 7 phần (G400): GetMoneyCacheKey (0x2A4E3C8) = string.Format("{0}\|{1}\|{2}\|{3}\|{4}\|{5}\|{6}", value, Upgrade_manager.Money_return(), Rebirth_manager.Money_return(), Skin_manager.Money_Accum, Pet_manager.Pet_Money_Accum, Relic_manager.Money_Value, buffActive) — key liệt kê đúng mọi nguồn multiplier của chuỗi ⇒ cache tự vô hiệu khi bất kỳ mắt nào đổi (bổ sung cho InvalidateMoneyCache). Stub return null của nó = Dictionary.TryGetValue ném ArgumentNullException (key) ⇒ khoản trả offline trên thiết bị bị huỷ giữa chừng (G394) disasm-proven G400 Đóng UNKNOWN §6

| Lose_Money(v): Money -= v; if (Money < 0) Money = ((InfVal)0).ToPrecision(5); — có ToPrecision(5) | disasm-proven | RVA 0x2A4F6E4 | Clamp sàn | | Lose_Ruby(v): Ruby -= v; if (Ruby < 0) Ruby = (InfVal)0;KHÔNGToPrecision (khác Lose_Money) | disasm-proven | RVA 0x2A4FACC | Bất đối xứng có thật, không phải lỗi đọc | | Lose_PetCoin(int) clamp >= 0 và có call-site analytics (AppsFlyerInit.SendLog_UseGem) — thuộc R1, phải đi qua interface | disasm-proven | RVA 0x2A4FC14 | Ranh giới service | | Money_Reset() đặt cả MoneyMoney_Accum về 0 rồi bắn event | disasm-proven | RVA 0x2A4F1F8 | Rebirth / prestige | | Bộ biến đổi Dia (đợt 9, G395): Get_Dia(int) / (int, Vector3) = Dia += v; _getDia += v × 3 (ADD W8, W19, W19, LSL #1 @0x2A4F358sổ gem máy chủ đếm 3 trên mỗi dia); bắn OnDiaChanged; Save() throttle 5 s realtime qua LastSaveRealtime. Lose_Dia phản chiếu vào _useDia + log AppsFlyer, KHÔNG kẹp ở 0 (khác Lose_PetCoin, có kẹp và truyền literal 4 = DiaUse_Type.Pet). Get_Money(InfVal, Vector3, string) đã điền — thân rỗng của nó là nguyên nhân ArgumentNullException (key) làm huỷ khoản trả offline trên thiết bị (G394) | disasm-proven | G395, G394 | Tiền premium; R1 (log) | | Ore rớt dia hiển thị bằng string literal cứng "+0.1" / "+1" (@0x5E81500 / 0x5E81508), qua Gamemanager.Dia_Devide_Txt_Spawn | disasm-proven | [reconstructed: Ore.cs] | Text tiền tệ phụ | | Có cơ chế throttle text theo ô lưới không gian: Dictionary<Vector2Int,float> _textCooldowns + CanSpawnText(worldPos, cooldown); hằng số BUCKET_SIZE = 1.5, DAMAGE_TEXT_COOLDOWN = 0.12, CRITICAL_TEXT_COOLDOWN = 0.12, MONEY_TEXT_COOLDOWN = 0.2 | metadata-proven + runtime-observed | dump.cs:583820; CanSpawnText fired 287× | Chống spam floating text | | Thân CanSpawnText (0x2A507F4): ô = Vector2Int(Math.Round(x/1.5), Math.Round(y/1.5)) — X/Y, làm tròn ngân hàng, z bỏ; đóng dấu Time.time, từ chối nếu ô còn trong cooldown | disasm-proven | G402 | Đóng UNKNOWN doc 06 | | InfVal giữ 5 chữ số có nghĩa trong đường kinh tế | runtime-verified | Mẫu parity: input 20 lưu là (digits=20000, exp=-3), output 152.2 lưu là (digits=15220, exp=-2) — đúng 5 chữ số cả hai phía | Precision mặc định | | ToPrecision(n) CẮT (về phía 0), KHÔNG làm tròn — và formatter hiển thị cũng CẮT. Hai thao tác khác nhau nhưng cùng một luật làm tròn | VERIFIED (2 kênh độc lập) | code 0x44E0E5C → 0x44DF888; dữ liệu: 1 202 phần tử mảng balance trong Main.unity khớp 100 % mô hình cắt, 0 % mô hình tròn — [phân tích nội bộ] §U3, §B-1 | Tránh nhầm lẫn khi hiện thực | | Mắt Upgrade_manager.Money_return() = Money_balance[Lv] — và Money_balancemảng đã serialize trong Main.unity, KHÔNG phải bảng tính lúc chạy (Balance_Reload là dead code) | VERIFIED | RVA 0x2B4A864; Main.unity:20393431; FABLE §U4, §B-3 | Đường cong thu nhập trong vòng | | Mảng đó sinh bằng lặp có CẮT: [0]=100; [i] = trunc3([i-1] · 1.08). Trôi khỏi dạng đóng 100·1.08^lv tới −10.2 % ở lv 50 và ~6.5 lần ở lv 1000 | VERIFIED (tái tạo 0/60 lệch) | [F001: economy_reference_sim.py] mục [M] | Đừng dùng luỹ thừa | | Mắt Rebirth_manager.Money_return() = Value_balance[2][Lv] + 100, cũng là mảng scene: Value(lv) = trunc3(int(trunc3(1.05^lv·100−100))), MaxLv = 20000 | VERIFIED (tái tạo 0/60 lệch) | RVA 0x2B22434; Main.unity:20531317 | Đường cong prestige (doc 09) | | Trong 1.1.59, liveops KHÔNG ghi đè kinh tế: Firebase RemoteConfig chỉ chạm UpdateManager; PlayFab GetTitleData 0 caller; ServerConfig chỉ ads/freecash/offerwall; Gun_manager.Request_Sheet (Google Sheet TSV) chỉ tới được từ Balance_Reload đã chết | VERIFIED | FABLE §U4 | Provenance của số | | Gamemanager.MoneyCacheMaxSize = 50hằng số biên dịch, không phải phỏng đoán | metadata-proven | dump.cs:583820 | Trần LRU | | _moneyCache / _moneyCacheKeys / _textCooldowns được cấp trong Gamemanager..cctor (0x2A517A4, 1160 B) cùng TimeCheckUrls, GainRetryDelaySec = {15f, 60f}, _lastGainCheckRealtime = float.NegativeInfinity. Cpp2IL bỏ rơi khởi tạo field static ⇒ bản dựng lại thiếu .cctor sẽ NRE ở lần đầu MoneyValue_return chạm cache | disasm-proven | G319 (gap list (nội bộ)) | Bẫy khôi phục tầng kinh tế | | Có overload thứ ba Get_Money(InfVal, Vector3, string) (RVA 0x2A4F090) — cho phép truyền chuỗi hiển thị tự chọn | metadata-proven | dump.cs:583820 | API |


3. Mô hình hành vi (đủ để tái hiện)

3.1 Vì sao game idle cần bignum

Trục tăng trưởng của idle là hàm mũ: máu quái, giá nâng cấp và thu nhập đều nhân lên mỗi tầng. double giữ được ~1e308 nhưng mất chữ số có nghĩa rất sớm khi cộng dồn hai giá trị lệch bậc (ví dụ 1e30 + 1 = 1e30), khiến các phép trừ máu nhỏ biến mất. InfVal giải quyết bằng cách tách:

struct InfVal {
    [SerializeField] string        s_digits;    // các chữ số có nghĩa, dạng chuỗi (serialize được)
    [SerializeField] int           s_exponent;  // số mũ cơ số 10
    System.Numerics.BigInteger     _digits;     // dạng runtime, KHÔNG serialize
}
value = _digits * 10^s_exponent

Hai thuộc tính then chốt cho idle: 1. Precision cố định theo chữ số có nghĩa, không theo bậc — nên ToPrecision(5) là một thao tác ngữ nghĩa rõ ràng, và số 1e100 vẫn so sánh/cộng/trừ chính xác ở 5 chữ số. 2. Serialize được vào scene/prefab dưới dạng text — đó là lý do s_digitsstring. Trong SOT, scene Main.unity 481 MB serialize InfVal inline vào từng component (Enemy.Health_Now, Health_Max, Money), nên đổi tên field trong package sẽ âm thầm mất toàn bộ số liệu kinh tế — đây là kiểm tra bắt buộc trước khi nâng version.

3.2 Chuỗi nhân thu nhập (income-multiplier chain)

// L1 ANALYSIS — RVA 0x2A4E7B0. VERIFIED.
private static InfVal MoneyValue_return(InfVal value)
{
    string key = GetMoneyCacheKey(value);
    if (_moneyCache != null && _moneyCache.TryGetValue(key, out InfVal cached))
        return cached;                                   // (A) fast path

    InfVal r = value * Upgrade_manager.Money_return() / 100;   // (B) nâng cấp trong màn
    r = r * Rebirth_manager.Money_return() / 100;              //     prestige
    r = r * Skin_manager.Money_Accum       / 100;              //     skin (int %)
    r = r * Pet_manager.Pet_Money_Accum    / 100;              //     pet (InfVal %)
    r = r * Relic_manager.Money_Value      / 100;              //     relic (InfVal %)
    if (Buff_manager.instance != null &&
        Buff_manager.instance.IsBuffActive(AD_Type.Money_Buff))
        r = r * 3;                                             // (C) buff xem quảng cáo: ×3

    if (_moneyCache != null)                                   // (D) LRU insert
    {
        if (_moneyCache.Count >= 50)
            _moneyCache.Remove(_moneyCacheKeys.Dequeue());
        _moneyCache[key] = r;
        _moneyCacheKeys.Enqueue(key);
    }
    return r;
}

Đặc điểm thiết kế đáng học:

  • Thuần nhân, không cộng. Mỗi nguồn multiplier là một phần trăm (/100). 100 % nghĩa là "không đổi", nên một hệ thống chưa mở khoá trả về 100 và tự động trung tính. Không cần branch "nếu chưa có pet thì bỏ qua". Đây là lý do parity runtime cho một hệ số hằng số (×7.61) trên mọi input: f(x) = k·x, k = tích các multiplier tại thời điểm đó.
  • Thứ tự cố định, mỗi bước chia 100 ngay — giữ độ lớn trung gian nhỏ, tránh thổi phồng exponent.
  • Buff quảng cáo tách khỏi chuỗi phần trăm: nó là ×3 cứng ở cuối, không phải "+200 %". Đây là ranh giới R1 — trong framework tái sử dụng, cờ buff phải đến từ một service có thể fail-closed, không được bịa.

3.3 Cache LRU tiền

Cache đánh vào giá trị input, không phải trạng thái người chơi. Vì thu nhập theo bậc màn chỉ có một tập nhỏ giá trị gốc lặp đi lặp lại (mỗi loại quái một mức tiền), 50 slot là quá đủ: runtime cho MoneyValue_return chạy 86 lần trong 40 giây với GetMoneyCacheKey cũng 86 lần (key luôn được tính) nhưng chuỗi nhân phía sau hầu như luôn hit.

CẢNH BÁO tính đúng đắn (quan trọng khi tái dùng): cache này phải bị invalidate mỗi khi bất kỳ multiplier nào đổi. SOT có đúng hàm đó: Gamemanager.InvalidateMoneyCache() (RVA 0x2A4E324) xoá cả _moneyCache_moneyCacheKeys. Nếu framework của bạn copy cache mà quên gọi invalidate sau khi mua upgrade, người chơi sẽ trả tiền mà thu nhập không tăng cho tới khi key bị đẩy khỏi LRU — một bug rất khó truy.

3.4 Tiền tệ và event

Money        InfVal   tiền chính, tiêu được          -> OnMoneyChanged
Money_Accum  InfVal   tổng tích luỹ, KHÔNG bị trừ    -> OnMoneyChanged (chung event)
Ruby         InfVal   tiền tệ cứng phụ                -> OnRubyChanged
Dia          ObscuredInt (ACTk)  tiền tệ premium      -> OnDiaChanged
PetCoin      int      tiền tệ hệ pet                  -> OnPetCoinChanged

Money_Accum chỉ tăng — nó là cơ sở cho các mốc thành tựu/prestige và không bị Lose_Money chạm tới. Đây là tách biệt "wallet vs lifetime earnings" kinh điển của idle.

Sự bất đối xứng đã kiểm chứng giữa Lose_Money (clamp bằng ((InfVal)0).ToPrecision(5)) và Lose_Ruby (clamp bằng (InfVal)0 trần trụi) là thật trong binary, không phải nhiễu đọc. Nó cho thấy một chi tiết thực tế: khi số 0 phải tham gia phép nhân/so sánh tiếp theo với precision 5, giữ precision giúp kết quả sau đó không bị hạ bậc. Framework nên chuẩn hoá một chỗ duy nhất.

3.5 Notation hiển thị

// L1 ANALYSIS — RVA 0x44E47D4
public readonly string InfVal_change()
{
    int mag = exponent + digits.ToString().Length;   // tổng bậc độ lớn
    if (mag >= 4)
        mag = (mag - 1) % 3 + 3;                      // ép về 3..5, canh theo nhóm nghìn
    return ToString(mag);                              // ToString(int maxDisplayedDigits)
}

Ý nghĩa: magsố chữ số tối đa được hiển thị. Với số < 10 000 thì hiển thị nguyên. Từ 10 000 trở lên, (mag-1)%3+3 đưa về chu kỳ 3 → luôn ra 3, 4 hoặc 5 chữ số, canh đúng theo mốc nghìn — nhờ vậy chuỗi hiển thị luôn "3 chữ số có nghĩa + đơn vị".

Đơn vị là bảng chữ cái: a, b, …, z, aa, ab, … với bước 3 bậc (unitStep = 3), tương đương AlphabeticFormatter trong package v2.2.0.

Mẫu ground-truth (bắt trực tiếp từ SOT đang chạy):

digits × 10^exp Kết quả trên màn hình
100 × 10^0 100
500 × 10^-1 50
10000 × 10^-2 100
761 × 10^0 761
297 × 10^3 297a
45660 × 10^-2 (= 456.6) 456
15220 × 10^-2 (= 152.2) 152
1 × 10^-11 scale lớn 203.09a, 139.6a, 144.77a

Truncate vs round — trạng thái hiện tại. Trong 14 cặp bắt được, đúng một cặp phân biệt được hai giả thuyết: (digits=45660, exponent=-2) = 456.6 → "456". Làm tròn nửa-lên sẽ cho "457". Vậy formatter hiển thị ToString(int) CẮT. Đây là kết luận dựa trên một mẫu phân biệt — đủ để hiện thực, chưa đủ để coi là bất biến đã kiểm chứng rộng.

ĐÍNH CHÍNH (đợt sau): cả hai thao tác đều CẮT. - InfVal.ToPrecision(n)CẮT về phía 0 (dùng trong số học: clamp Lose_Money, bảng cân bằng, ToPrecision(5) cho thưởng rebirth). VERIFIED hai kênh: code 0x44E0E5C → ToExponent 0x44DF888 → set_exponent (chia BigInteger), và dữ liệu — 1 202 phần tử bảng balance trong Main.unity khớp 100 % mô hình cắt, 0 % mô hình làm tròn. ([phân tích nội bộ] §U3, §B-1) - InfVal.ToString(int) / InfVal_change()CẮT, để hiển thị.

Một phát biểu trước đó trong PRESTIGE_RULES.md rằng ToPrecision làm tròn đã bị supersede. Đây cũng là lý do bản mô hình tham chiếu đầu tiên tính sai bảng cân bằng — xem §5.7.

✅ ĐÃ CHẠY VÀ PASS: InfVal.ParityTest.cs đã chạy trong Editor (EditMode) — 14/14 PASS, 0 failed. Bằng chứng: [report: InfVal-parity-20260904T125826Z.xml] (result="Passed" total="14" passed="14" failed="0"; lần chạy trước đó …125608Z.xml là 13/14, nên đây là một lần sửa thật, không phải test rỗng). Cầu nối InfVal do đó lên mức test-verified, không còn là "đã bàn giao kèm test". Vẫn nên mở rộng bộ mẫu ở giá trị lớn (đơn vị sau z). Không được chấp nhận một formatter chưa tái hiện đúng từng mẫu này. Mô hình tham chiếu Python (§5) hiện thực đúng cả hai thao tác và pass 14/14.

3.5.1 Bằng chứng MỚI từ save thật của người dùng — HOA THƯỜNG vs HOA HOA

Ảnh chụp màn hình từ save đã tiến xa của chính người dùng (Stage 5-4, Power/Speed/Money đều Lv.46):

  • File: [device evidence: user_screenshot_stage5-4.png] sha256 04ef8221dc6000c0bd064fc9ea6c0281fb88f8118ea0c538f1d5b83399dc7641 (1 114 008 B)
  • Mức bằng chứng: quan sát trực tiếp trên SOT đang chạy (screenshot), đã đọc lại và xác nhận độc lập.

Quan sát được (đọc trực tiếp từ ảnh):

Vị trí trên màn hình Chuỗi hiển thị Kiểu chữ đơn vị
Text sát thương bay lên 3.12A, 6.24A HOA
Text tiền bay lên (màu xanh lá) 149.75A HOA
Số dư tiền (góc trên phải) 134.58b thường
Panel nâng cấp — Power Lv.46 3.12a thường
Panel nâng cấp — Money Lv.46 3.12a thường
Panel nâng cấp — giá (cả 3 cột) 134b thường
Panel nâng cấp — Speed Lv.46 468 (không có đơn vị)

Phát hiện ban đầu: cùng một giá trị 3.12 × 10³ hiển thị là 3.12a trong panel nâng cấp và 3.12A trong text sát thương bay lên. Chữ số giống hệt nhau; chỉ khác kiểu chữ của đơn vị.

✅ ĐÃ GIẢI QUYẾT: đó là ART CỦA FONT, không phải formatter

Giả thuyết "có hai formatter" hoặc "có một .ToUpper() ở đâu đó" đã bị bác bỏ. Chuỗi là chữ thường ở cả hai chỗ; chỉ có hình vẽ của glyph là chữ hoa.

Bằng chứng 1 — chỉ có ĐÚNG MỘT formatter (CodeRefsTo, IDA):

Method của InfVal RVA Số caller trong code game
InfVal_change() 0x44E47D4 29
ToString(int, string[] unitsList, IFormatProvider) 0x44E3AB4 0 (chỉ thunk reflection 0x2822784)
ToString(int, DisplayOption, IFormatProvider) 0x44E3B38 0
ToString(int) 0x44E3A34 0 caller game (chỉ được InfVal_change gọi nội bộ @0x44E4874)
ToString(string[] unitsList, IFormatProvider) 0x44E3BBC 0
ToString(DisplayOption, IFormatProvider) 0x44E3CC0 0

Các địa chỉ 0x28227xxthunk gọi bằng reflection của IL2CPP, không phải code Assembly-CSharp. ⇒ Không có formatter thứ hai, không có unitsList viết hoa. Mọi con số hiển thị trong game — tiền trên HUD text sát thương — đều đi qua cùng một InfVal_change().

Chính chuỗi sát thương cũng được dựng bằng nó: Cat.Balance_Reload gọi InfVal_change tại 0x2AE59500x2AE5A84Cat.Damage_str (@0x90) / Cat.CriDmg_str (@0x98), rồi Bullet chuyển tiếp sang Gamemanager.Damage_ColliderDamage_Txt_Spawn / Critical_Txt_Spawn.

Bằng chứng 2 — không có ToUpper nào trên đường vẽ text. Text bay lên không dùng TextMeshPro; nó là một hệ mesh bitmap-font tự viết:

DamageTextManager.ShowDamageText(key, content, pos)
  -> DamageTextObject.InitSprites(Font, string content)   0x2A544C8
       -> per char: String.get_Chars
       -> DamageTextSprite.InitChar(fontType, font, ch)   0x2A55EB8
       -> DamageTextSpriteManager.GetSpriteInfo(...)      0x2A560EC
       -> Font.GetCharacterInfo

Cả bốn method đã được decompile: không có biến đổi ký tự, không ToUpper, không case-folding. Ký tự đi qua nguyên vẹn.

Bằng chứng 3 — bảng ký tự của font tự nó chứng minh. Assets/Font/Damage.asset định nghĩa đúng 42 codepoint (đã kiểm độc lập bằng cách đếm index: trong file asset đã export):

+ . 0 1 2 3 4 5 6 7 8 9 B K M T a b c d e f g h i j k l m n o p q r s t u v w x y z

Tức có lowercase a–z (97–122) — đúng là các chữ cái đơn vị mà InfVal_change sinh ra — cộng chữ số, +, ., và các đơn vị short-scale kiểu cũ B K M T. Vậy 'a' phân giải sang một glyph có thật; artwork của glyph đó trong Assets/Texture2D/Damage.png được vẽ thành chữ "A" hoa. Bảng y hệt trong Damage (Cri).asset, Dia.asset, Money_Cri.asset (đều n=42, cùng tập ký tự) — cả bốn đều là font của text bay. HUD dùng TMP + TTF thường (NotoSans / MPLUSRounded / Mitme), nên hiển thị chữ thường.

HỆ QUẢ CHO BẢN RESTORE — quan trọng: - TUYỆT ĐỐI KHÔNG thêm .ToUpper() ở bất kỳ đâu. Chuỗi của SOT là chữ thường. Viết hoa trong code vừa sai, vừa — một khi art font gốc được phục hồi — không tạo ra khác biệt nhìn thấy được; nó chỉ là một lời khai sai nằm trong code. - Độ trung thực phụ thuộc vào việc giữ nguyên 4 font asset và atlas của chúng (xem 08 §5 về G012 — chúng là bitmap font, không phải TTF hỏng). - Bộ 14 cặp parity là đủ. Không cần thêm "case viết hoa" nào, vì không có chuỗi viết hoa nào được sinh ra. Kiểm tra đúng cho tầng đó là so sánh render với atlas font, không phải test parity chuỗi.

3.5.2 Đối chiếu chéo với ảnh chụp (Lv.46) — 4/4 KHỚP sau khi sửa mô hình

Ảnh chụp cho bốn điểm kiểm tra. Ở lần đối chiếu đầu, mô hình của tài liệu này (dựa trên công thức 100·1.08^lv + làm tròn) khớp 1/3. Sau khi phát hiện bảng thật là mảng serialize trong scene sinh bằng lặp có CẮT, cả bốn đều khớp:

Ô trên màn hình Mảng scene (Main.unity) Màn hình Mô hình CŨ (sai)
Speed Lv.46 Speed_balance[46] = 468 468 ✅ 468
Power Lv.46 Power_balance[46] = 3 120 3.12a 3.5a
Money Lv.46 Money_balance[46] = 3 120 3.12a 3.5a
Giá Lv.46 Price_balance[46] = 1.34×10⁸ 134b 136b

Hai kết luận trước đây đã bị supersede:

  • ~~"panel nâng cấp hiển thị một giá trị dẫn xuất, không phải ô bảng"~~ → SAI. Panel hiển thị đúng Money_balance[Lv]. Sự "lệch" là lỗi của mô hình, không phải hành vi của panel.
  • ~~"lệch giá 136b vs 134b chưa giải thích được"~~ → ĐÃ GIẢI THÍCH. Mảng scene cho đúng 134b.

Bài học: khi mô hình lệch với thực tế quan sát được, giả thuyết đầu tiên phải là mô hình sai, không phải thực tế phức tạp hơn ta tưởng. Ở đây, việc thêm một lời giải thích cho ô lệch ("chắc panel hiển thị thứ khác") suýt che mất một lỗi nền tảng ảnh hưởng tới toàn bộ đường cong kinh tế.

3.5.3 Corroboration hình ảnh cho buff ×3

Ảnh cũng cho thấy hai biểu tượng buff ×3 (lửa và tiền) đang hoạt động ở cạnh trái màn hình. Đây là corroboration hình ảnh cho nhánh Money_Buff trong MoneyValue_return (if (IsBuffActive(AD_Type.Money_Buff)) r = r * 3) — nó xác nhận buff ×3 tồn tại và hiển thị được cho người chơi. Nó không chứng minh giá trị hệ số là 3; giá trị đó đến từ disasm.

3.6 Quy tắc fail-closed cho phần thưởng

Money_Buff ×3 và mọi nguồn thu nhập gắn với quảng cáo/IAP đi qua ranh giới R1:

// [restore project: Services] — mặc định là NullService
public sealed class NullAdService : IAdService {
    public bool IsRewardedReady => false;
    public void ShowRewarded(string placementId, Action<ServiceResult> onReward) {
        onReward?.Invoke(ServiceResult.NotAvailable);   // TỪ CHỐI, không bao giờ grant
    }
}

Nguyên tắc: một service chưa được bind KHÔNG BAO GIỜ được phép trả về "đã trao thưởng". Một stub được phép log và no-op; nó không bao giờ được bịa ra trạng thái progression. Trong một framework idle, đây phải là mặc định compile-time, không phải quy ước.


4. Framework takeaway

Những gì một framework idle tái sử dụng nên phơi ra:

  1. IBigNumber là kiểu tiền tệ duy nhất, serialize-được dưới dạng text. Không dùng double ở bất kỳ đâu trong đường thu nhập. Bắt buộc struct (không box) và có ToPrecision(int) để chuẩn hoá.

  2. IMultiplierSource trả về phần trăm, mặc định 100. csharp public interface IMultiplierSource { BigNum PercentFor(StatKind kind); } // 100 == neutral Pipeline thu nhập là sources.Aggregate(baseValue, (acc, s) => acc * s.PercentFor(kind) / 100). Thêm một hệ thống mới (relic, pet, guild…) = đăng ký thêm một source — không sửa công thức.

  3. Multiplier "sự kiện" (buff, ad, event) là một tầng riêng, nhân sau, và bắt buộc đi qua service fail-closed. Đừng trộn nó vào danh sách phần trăm; nó có vòng đời và tính tin cậy khác hẳn.

  4. Cache theo giá trị input + LRU trần nhỏ, kèm một InvalidateAll() bắt buộc gọi khi bất kỳ source nào đổi. Framework nên tự động invalidate bằng cách cho IMultiplierSource bắn event, thay vì trông chờ call-site nhớ gọi (đây là chỗ SOT dựa vào kỷ luật con người).

  5. Tách Wallet (tiêu được) khỏi LifetimeEarned (chỉ tăng), dùng chung một event thay đổi.

  6. Formatter là một contract có test parity, không phải một hàm tiện ích. Notation "3 chữ số có nghĩa + đơn vị chữ cái, bước 3 bậc" nên là formatter mặc định; và framework phải kèm bộ test cặp (giá trị → chuỗi) để bất kỳ thay đổi nào cũng bị bắt. Quy tắc truncate-vs-round phải được khai báo tường minh, không để implicit. Một formatter duy nhất cho toàn game — SOT có đúng một (InfVal_change, 29 caller); mọi overload khác 0 caller. Nhiều formatter = nhiều nguồn sự thật cho cùng một con số.

6b. KIỂU CHỮ SỐNG TRONG FONT ATLAS, KHÔNG SỐNG TRONG CODE. Đừng bao giờ thêm ToUpper. Cùng một chuỗi "3.12a" hiện là 3.12A ở text bay và 3.12a trên HUD — chỉ vì hai renderer dùng hai font khác nhau, và font bitmap của text bay vẽ glyph a thành chữ "A". Nếu bạn "sửa" bằng .ToUpper() trong code, bạn sẽ: (a) làm sai chuỗi thật, (b) tạo một khác biệt không nhìn thấy được một khi art đúng được dùng, (c) để lại một lời khai sai trong code. Quy tắc framework: layer trình bày quyết định hình dạng chữ; layer dữ liệu chỉ trả chuỗi.

  1. Throttle floating text ở tầng framework (CanSpawnText + cooldown cleanup), vì mỗi viên đạn trúng đều muốn sinh text.

  2. Bảng chỉ số theo hạng nên sinh bằng HÀM MŨ + LÀM TRÒN THEO CHỮ SỐ CÓ NGHĨA, và mỗi trục chỉ số được một CƠ SỐ riêng. Mẫu đọc được ở Fish_manager.Balance_Reload (13 hạng cá, docs/functions/ca-fish.md §3.1): csharp for (int i = 0; i < gradeCount; i++) { Damage = RoundToSignificant(4f * Mathf.Pow(3f, i), 2); // trục chính, cơ số 3 CriPer = Clamp(RoundToSignificant( Mathf.Pow(2.5f, i), 2), 0, 100); // trục phụ, cơ số 2.5 CriVal = RoundToSignificant(8f * Mathf.Pow(2.5f, i), 2); } grade 0 1 2 3 4 5 6 7 8 9 10 11 12 Dmg 4 12 36 110 320 970 2900 8700 26000 79000 240000 710000 2100000 CriVal 8 20 50 120 310 780 2000 4900 12000 31000 76000 190000 480000 Ba điều đáng chép: - RoundToSignificant(x, 2) chứ không phải số thô. Bảng đọc lên như bảng do người viết (110, 320, 970) chứ không phải 108, 324, 972. Chi phí: một hàm 6 dòng (floor(log10)scale = 10^(digits−exp−1) → hai lần Math.Round). Lưu ý làm tròn ToEven (125 → 120) — phải khai báo tường minh, giống quy tắc truncate-vs-round ở mục 6. - Hai cơ số khác nhau tách hai trục ra theo thời gian. 3^12 ≈ 531 000 so với 2.5^12 ≈ 60 000: sau 12 bậc trục chính bỏ xa trục phụ ~9 lần. Nhà thiết kế chọn tốc độ tương đối giữa các chỉ số bằng một con số duy nhất mỗi trục, không phải bằng 13 ô bảng. - Hàm sinh là công cụ Editor; mảng bake trong scene là nguồn sự thật (§5.7, doc 07 §3.2). Cách rẻ nhất để đối chiếu mà không mở scene: chọn một hồ sơ save có các hạng khác nhau rồi so TỔNG hiển thị trên UI. Với [0,0,1,2,5] bảng trên cho Damage tổng 1026 (+1.02a%) và CriVal tổng 866 + 125 = 991% — hai tổng độc lập, khớp cả hai là đủ đóng câu hỏi.

  3. Chỉ số phụ nên có TRẦN, và trần nên nằm GIỮA bảng. CriPer chạm 100 ngay từ hạng 6 — tức một nửa bảng hạng không đóng góp thêm một điểm tỉ lệ chí mạng nào. Đây không phải sơ suất: - nó ngăn những con số vô nghĩa (“tỉ lệ chí mạng 400%”) mà vẫn giữ công thức sinh gọn; - nó rút gọn không gian quyết định theo thời gian: đầu game người chơi cân ba chỉ số, cuối game chỉ còn một trục (sát thương). Trò chơi tự đơn giản hoá đúng lúc người chơi đã có nhiều thứ khác để nghĩ; - hệ quả kỹ thuật: Clamp phải nằm trong hàm sinh để bảng bake đã sạch, và lặp lại lần nữa ở chỗ hiển thị tổng (Clamp(sum, 0, 100)), vì tổng của nhiều thực thể vẫn vượt trần dù mỗi thực thể đã bị chặn. SOT làm đúng cả hai chỗ.

  4. ⚠ MỌI BẢNG SỐ PHẢI ĐƯỢC ĐỌC TỪ NƠI NÓ THẬT SỰ SỐNG LÚC CHẠY — thường là SCENE, không phải ctor. Mục 8 nói hàm sinh là công cụ Editor và mảng bake là nguồn sự thật; mục này là hệ quả nghiêm khắc hơn của nó, và nó đã làm hai kết luận trong dự án này sai:

    Bảng Ctor / .cctor nói Scene nói Ai thắng lúc chạy
    Cat_manager.FormationRadius 10 2.5 scene
    Fish_manager.GachaChances (G1682) {40, 30, 20, 10} {60, 30, 10, 0} scene

    Luật đọc theo loại field: public/[SerializeField] có mặt trong scene ⇒ scene thắng, ctor chỉ là mặc định của AddComponent; private không [SerializeField]ctor là giá trị thật; const/static readonly ⇒ ctor/metadata là thật. Với Fish_info_list ở mục 8 thì hai nguồn khớp nhau — nên bài học không phải "đừng tin ctor" mà là "phải kiểm loại field trước khi tin bất kỳ con số cân bằng nào".

    Chi phí của việc bỏ qua: một bảng tỉ lệ gacha sai được viết vào tài liệu và publish. Cái bắt được nó không phải phân tích tĩnh mà là một quan sát tần suất trên máy — năm lượt liên tiếp ra cùng bậc thấp nhất là 0,6⁵ ≈ 7,8% với bảng thật nhưng 0,4⁵ ≈ 1% với bảng ctor. Framework takeaway: cạnh mỗi bảng tỉ lệ nên có một phép thử tần suất rẻ để bảng tài liệu và bảng chạy thật không âm thầm trôi khỏi nhau. Xem thêm doc 07 §4 mục 16.


5. Module kinh tế CHẠY ĐỘC LẬP — đặc tả

Mục tiêu: tầng kinh tế phải chạy được mà không cần Unity. Không MonoBehaviour, không Transform, không Time.deltaTime, không singleton kéo theo scene. Lý do không chỉ là sạch sẽ:

  • Kiểm thử được. Đường thu nhập là chỗ dễ sai nhất và tốn kém nhất khi sai. Nếu nó thuần logic, bạn chạy hàng triệu bước mô phỏng trong vài giây, không cần mở Editor.
  • Cân bằng được. Designer cần trả lời "bao lâu tới mốc X?" trước khi ship, không phải sau.
  • Chuyển được. Cùng một tầng chạy trên server (chống gian lận), trong công cụ cân bằng, và trong game.

5.1 Ranh giới module

┌─────────────────────── EconomyCore (thuần logic, 0 phụ thuộc engine) ────────────────────────┐
│                                                                                              │
│  BigNum            số thập phân theo chữ số có nghĩa   (mô hình hoá InfiniteValue.InfVal)    │
│                    { digits: BigInteger, exponent: int, precision = 5 }                      │
│                    + - * /int, so sánh, ToPrecision(n)  [CẮT về phía 0]                      │
│                    + ToDisplayString()                  [CẮT + đơn vị chữ cái]               │
│                                                                                              │
│  IMultiplierSource PercentFor(kind) -> BigNum          (100 == trung tính)                   │
│  IncomeChain       tích các source, rồi buff; cache LRU 50; Invalidate()                      │
│  Wallet            Money, Money_Accum, Ruby, PetCoin  + 4 event thay đổi                     │
│  PrestigeLayer     ResetSet / PersistSet / RewardCurve / PriceCurve   (doc 09)               │
│                                                                                              │
└──────────────────────────────────────────────────────────────────────────────────────────────┘
        ▲                                    ▲                                   ▲
        │ đọc                                │ đọc                               │ đọc
  UnityAdapter                         SimulatorHost                       ServerValidator
  (MonoBehaviour mỏng:                 (chạy hàng loạt,                    (tính lại thu nhập
   bind event -> UI,                    không đồ hoạ)                       để kiểm tra client)
   gọi Get_Money khi quái chết)

Quy tắc ranh giới, rút thẳng từ SOT:

Quy tắc Bằng chứng từ SOT
Tiền tệ và event là static, không gắn instance scene Gamemanager.Money/Money_Accum/Ruby/PetCoin + 4 Action đều static
Chuỗi nhân chỉ đọc phần trăm, không biết hệ thống nào cấp MoneyValue_return gọi 5 getter static trả InfVal/int
Vị trí (Vector3) là dữ liệu mờ với tầng kinh tế — chỉ chuyển tiếp cho lớp hiển thị Get_Money(v, Pos) chỉ dùng Pos để gọi Money_Txt_Spawn
Nguồn duy nhất chạm engine là floating text và analytics Money_Txt_Spawn, AppsFlyerInit.SendLog_UseGem — cả hai tách được ra sau interface

Trong SOT, Gamemanager một MonoBehaviour, nhưng phần kinh tế bên trong nó đã gần như thuần static rồi — nó chỉ cần một MonoBehaviour để có Awake/coroutine cho autosave và anti-cheat. Bài học: kinh tế đã muốn độc lập; chỉ cần cắt đúng chỗ.

5.2 Mô hình dữ liệu tối thiểu

BigNum      : { digits: BigInteger, exponent: int }      value = digits * 10^exponent
              precision = 5 chữ số có nghĩa (bằng chứng: mẫu parity 20 -> (20000,-3), 152.2 -> (15220,-2))
              serialize: (string digits, int exponent)   <- khớp s_digits / s_exponent của SOT

Wallet      : Money: BigNum          tiêu được          -> OnMoneyChanged
              MoneyAccum: BigNum     chỉ tăng           -> OnMoneyChanged (dùng chung event)
              Ruby: BigNum           tiền tệ prestige   -> OnRubyChanged
              PetCoin: int                              -> OnPetCoinChanged
              (Dia: ObscuredInt trong SOT               -> OnDiaChanged)

IncomeChain : sources: IMultiplierSource[]   (thứ tự CỐ ĐỊNH: Upgrade, Rebirth, Skin, Pet, Relic)
              buffMultiplier: int = 3        (chỉ khi Money_Buff bật)
              cache: LRU<string, BigNum>     trần 50

5.3 Hợp đồng hành vi (tóm tắt — chi tiết ở §3 và doc 09)

GetMoney(v)          : v' = Chain(v); Money += v'; MoneyAccum += v'; fire(OnMoneyChanged); return v'
GetMoney(v, pos)     : v' = GetMoney(v); SpawnText(v'.ToDisplayString(), pos)
LoseMoney(v)         : Money -= v; if (Money < 0) Money = BigNum.Zero.ToPrecision(5); fire()
MoneyReset()         : Money = 0; MoneyAccum = 0; fire()
GetRuby(v)/LoseRuby  : Ruby ±= v; if (Ruby < 0) Ruby = 0;  [KHÔNG ToPrecision]  fire()
GetPetCoin/LosePetCoin: PetCoin ±= v; clamp >= 0; fire()   [LosePetCoin có call-site analytics R1]

Chain(v)             : key = CacheKey(v); nếu hit -> trả về
                       r = v
                       for s in sources: r = r * s.Percent / 100
                       if (moneyBuff) r = r * 3
                       nếu cache đầy (>=50): bỏ phần tử cũ nhất
                       cache[key] = r; return r
Chain.Invalidate()   : xoá cache — BẮT BUỘC khi bất kỳ source nào đổi

5.4 Đặc tả SIMULATOR

Simulator là một host không đồ hoạ chạy EconomyCore theo bước rời rạc:

SimConfig {
    baseDrop            BigNum      tiền gốc mỗi lần giết
    sources             danh sách phần trăm ban đầu
    upgradeCurve        (lv) -> percent    [đệ quy 1.08 + ToPrecision(3), MaxLv 10000]
    rebirthCurve        (lv) -> percent    [floor(1.05^lv*100-100) @3, MaxLv 20000]
    rebirthPriceCurve   (lv) -> Ruby       [DiscardDecimal(3*(lv+1)^1.6 * C(lv)) @3]
    rewardCurve         (level, stage, relic%) -> Ruby   [n^2.5 * relic%/100, 0 nếu stage<=1]
}

SimRun:
    lặp:  GetMoney(baseDrop)  cho tới khi đạt mốc / hết ngân sách bước
    ghi:  số bước, tiền tích luỹ, hệ số tổng hợp, tỉ lệ cache hit
    tuỳ chọn: mô phỏng trọn chu kỳ prestige (mua node -> rebirth -> mua node -> …)

Simulator phải tự kiểm bằng vector THẬT trước khi ai đó tin kết quả cân bằng của nó.

5.5 Bộ vector kiểm thử

Hai hạng, không được trộn lẫn:

Hạng A — oracle bắt LIVE từ SOT đang chạy (giá trị quan sát được, không phải suy ra):

Vector Giá trị Nguồn
MoneyValue_return(20) 152.2 — lưu là (15220, -2) moneyvalue_return_parity_samples.json
MoneyValue_return(60) 456.6 — lưu là (45660, -2) như trên
Hệ số phiên đó ×7.61state không ghi lại ⇒ không tách được suy ra từ hai mẫu trên
MoneyValue_return(60.0) 157.80 — lưu là (15780, -2) như trên
Từng mắt ở phiên đó Upgrade = 263, Rebirth = 100, Skin/Pet/Relic = 100, buff off×2.63 như trên — phân rã được
14 cặp InfVal_change (100,0)→"100", (500,-1)→"50", (761,0)→"761", (297,3)→"297a", (215,3)→"215a", (45660,-2)→"456", (15220,-2)→"152", (10000,-2)→"100", (12500,-2)→"125", (125,0)→"125", (705,0)→"705", (951,0)→"951", (0,0)→"0", (0,-4)→"0" infval_change_parity_samples.json

Các mẫu còn lại trong file parity có _digitsBigInteger mà bộ bắt không đọc được giá trị (trường bị ghi là 1). Chúng bị loại khỏi bộ vector — không suy đoán.

Hạng B — MẢNG SCENE THẬT (nguồn sự thật lúc chạy; xem cảnh báo ở §5.7):

Trích từ Main.unity ra [F001: scene_balance_tables.json] (8 mảng × 60 phần tử). Mô hình tham chiếu tái tạo chúng 0/360 lệch.

Vector Giá trị (mảng scene) Nguồn
Money_balance[0,1,2,10] 100, 108, 116, 210 Main.unity:20393431
Money_balance[46] 3 120 → hiển thị 3.12a (khớp ảnh chụp) như trên
Money_balance[50] 4 210 (dạng đóng 4 690.16 — lệch −10.2 %) như trên
Price_balance[1] 68, KHÔNG phải 69 (1.38 là double nhị phân) Main.unity:20353425
Price_balance[46] 1.34×10⁸134b (khớp ảnh chụp) như trên
Speed_balance[46] 468 (khớp ảnh chụp) Main.unity:20413434
Hệ số prestige lv 1 / 10 / 50 ×1.05 / ×1.62 / ×11.40 Main.unity:20531317
Rebirth Price(lv=0/1/9/46) 3 / 9 / 119 / 1 420 Ruby Main.unity:20571320

Hạng C — DẪN XUẤT từ công thức disasm (không có mảng để đối chiếu):

Vector Giá trị Nguồn
reward(Level=0, Stage=2)n=10 316.22 Ruby (ToPrecision(5) cắt) RVA 0x2B22F18
reward(Level=0, Stage=10)n=50 17 677 Ruby như trên
Cổng Stage_Now <= 1 0 Ruby như trên
Boost is_UpgradeRebirth ×3 như trên

5.6 Mô hình tham chiếu chạy được

[F001: economy_reference_sim.py]

Python thuần, không phụ thuộc gì (chỉ thư viện chuẩn). Hiện thực hoá BigNum, formatter, chuỗi nhân, cache LRU, ví tiền và toàn bộ tầng prestige; tự kiểm với 70 vector thuộc cả ba hạng, trong đó có việc tái tạo 8 mảng balance thật của scene (0/360 lệch) và 4 giá trị đọc từ ảnh chụp save thật.

$ python3 economy_reference_sim.py
[A] InfVal_change() — 14 cặp parity bắt từ thiết bị            14/14 OK
[B] MoneyValue_return — parity runtime (x7.61, đầu-cuối)         2/2  OK
[B2] MoneyValue_return ĐÃ PHÂN RÃ theo từng mắt (x2.63)          4/4  OK
[C] Bất biến: chuỗi THUẦN NHÂN (f(60)/f(20) == 3.0)             OK
[D] Cache LRU trần 50 + invalidate khi multiplier đổi           4/4  OK
[E] Get_Money / Lose_Money / Money_Reset + event                5/5  OK
[F] Lose_Ruby clamp KHÁC Lose_Money (không ToPrecision)         OK
[G] Bảng prestige — mảng scene, lặp có CẮT                      6/6  OK
[H] Bảng upgrade — mảng scene, lặp có CẮT (KHÔNG phải 1.08^Lv)  8/8  OK
[I] Giá node prestige (Ruby) — mảng scene                       6/6  OK
[J] Thưởng rebirth n^2.5 + cổng Stage + boost x3                7/7  OK
[K] Một chu kỳ Rebirth() đầy đủ — RESET vs PERSIST              5/5  OK
[M] Tái tạo 8 MẢNG BALANCE SCENE THẬT                     0/360 lệch  OK
[N] 4 giá trị đọc từ ảnh chụp save thật (Lv.46)                 4/4  OK
KẾT QUẢ: 70/70 PASS

là mô hình tham chiếu, không phải bản restore. Bản restore trong [restore project: CatGunner] dùng package InfiniteValue.InfVal thật; file Python chỉ mô hình hoá ngữ nghĩa để kiểm chứng và mô phỏng.

Một kết quả đáng chú ý mà simulator làm hiện ra ngay (mục [K]): với Stage 10 / upgrade Money lv 25, rebirth trả 17 677 Ruby, mua được 40 level node Money, và mắt Rebirth trong chuỗi thu nhập nhảy từ 100 % lên 703 % — trong khi mắt Upgrade rơi từ 653 % về 100 %. Đó là toàn bộ vòng lặp idle, đo được bằng số, không cần mở Editor.

5.7 ⚠ Nguồn sự thật của bảng cân bằng — đọc trước khi hiện thực

Balance_Reload — hàm sinh bảng cân bằng — là DEAD CODE trong build 1.1.59 (CodeRefsTo = [] cho Upgrade_manager 0x2B4A02C, Rebirth_manager 0x2B21CB4, Gun_manager 0x2B0AB4C; không string literal; không UnityEvent trong scene). Bảng thật lúc chạy là mảng đã serialize trong Main.unity, sinh bằng lặp có CẮT với hằng số là double nhị phân.

# ĐÚNG — tái tạo mảng scene, 0/360 lệch trên 8 mảng
v = 100
for i in range(1, n):
    v = trunc3(v * 1.08)          # cắt 3 chữ số có nghĩa MỖI BƯỚC

# SAI — lệch −10.2 % ở lv 50 và ~6.5 LẦN ở lv 1000
v = 100 * (1.08 ** lv)

Simulator standalone phải ăn mảng scene, hoặc lặp-cắt y hệt. Không được dùng luỹ thừa. Chi tiết đầy đủ + bảng sai lệch: 09-prestige-rebirth.md §3.2.

5.8 Mô hình kinh tế THỨ HAI: OFFLINE — tách hẳn, phải hiện thực riêng

Tầng mô tả ở trên là kinh tế online. Có một mô hình thứ hai, không phải một nhánh của cái thứ nhất, và người chơi thật nhận phần lớn tiền qua nó.

Sự thật cốt lõi (disasm-proven): OfflineProgressSimulator.ApplyToManagers (RVA 0x2AB7398) ghi thẳng vào Gamemanager.Money / Money_Accum, rồi chỉ gọi Get_Money((InfVal)0) — một khoản cấp bằng 0 với mục đích duy nhất là bắn OnMoneyChanged để làm mới UI. ⇒ Thu nhập offline KHÔNG đi qua MoneyValue_return.

// ApplyToManagers — RVA 0x2AB7398
Gamemanager.Money       = result.<InfVal @+56>;   // GHI THẲNG static field
Gamemanager.Money_Accum = result.<InfVal @+88>;   // GHI THẲNG
Gamemanager.InvalidateMoneyCache();
Gamemanager.Get_Money((InfVal)0);                 // cấp 0 — chỉ để bắn OnMoneyChanged
Upgrade_manager.Firse_reload(new int[3]{power, speed, money});   // áp level đã mô phỏng
Tree_manager.Contents_Lock_Reload();

Multiplier nào được áp (call census đầy đủ OfflineProgressSimulator, kể cả lớp coroutine/lambda lồng nhau <RunCoroutine>d__13, <>c, <>c__DisplayClass*):

Multiplier Online (MoneyValue_return) Offline (ĐÍNH CHÍNH đợt 7 — G382/G1001)
Upgrade money (Money_balance[lv]) (GetMoneyBalance)
Rebirth money (Rebirth_manager.Money_return) (qua SafeManagerCall lambda b__13_2)
Skin_manager.Money_Accum — đọc trong MoveNext (0x2ABA4DC..0x2ABA754, null-guard ⇒ 100), nhân vào cả CalcFarmMoneyPerSec lẫn CalcMoneyPerStage
Pet_manager.Pet_Money_Accum (như trên, ép (int))
Relic_manager.Money_Value (như trên)
Buff_manager Money_Buff ×3 KHÔNGthứ DUY NHẤT vắng mặt offline
đường cong stage / degree (qua giá trị Money của quái) (GetStageMoney, AvgDegreeMoney)

⚠ ĐÍNH CHÍNH (đợt 7): bản trước của bảng này (và OFFLINE_ECONOMY.md §(c)) ghi Skin / Pet / Relic không được áp offline — sai. Điều tra call-census ban đầu bỏ sót các lần đọc trong <RunCoroutine>d__13.MoveNext; khi dựng lại toàn bộ simulator (G382) chúng lộ ra. skinDmg / skinSpd / fishDmg / relicDmg / relicCriDmg cũng vào chuỗi sát thương trong CalcClearInfo. Mục CORRECTION đã được nối thêm vào OFFLINE_ECONOMY.md (không sửa lịch sử).

Cửa sổ thời gianComputeElapsed (RVA 0x2A75258):

total  = (now - last).TotalSeconds              # không cap, chỉ để hiển thị "đã vắng bao lâu"
capped = min(total, EffectiveMaxOfflineSeconds())
if (total < 0) capped = 0                       # đồng hồ bị vặn lùi -> KHÔNG cấp gì
return (capped >= 30.0)                         # SÀN 30 GIÂY, dưới đó không cấp gì cả
  • CapEffectiveMaxOfflineSeconds() (RVA 0x2A74B2C): ```csharp return MaxOfflineSeconds
    • (Relic_manager.instance != null ? Relic_manager.Offline_Value * 60f : 0f); ``MaxOfflineSeconds = 28800(8 giờ). **Offline_Value(Relic static @0x64) tính bằng PHÚT** — hàm nhân60fđể ra giây. Một relic "+30" nghĩa là **+30 phút** offline (⇒ cap30 600 s), không phải +30 giây. BaselineOffline_Value = 0⇒ **đúng 8 giờ**. Chú ý guardRelic_manager.instance != null`: chưa có manager thì không cộng gì.
  • Toàn bộ đường này còn bị gate bởi Time_manager.IsUsable + guard pending-gain (chống gian lận thời gian).
  • Đợt 3 — đã dựng lại và đối chiếu (G332, G334):
  • OfflineProgressManager..ctor (0x2A756F0): MaxOfflineSeconds = 28800f, LoadingFrameBudgetMs = 60. Con số 28 800 vì thế có hai nguồn độc lập trùng khớp (mặc định ctor + giá trị scene Main.unity:20446002).
  • ComputeElapsed trả HAI kết quả riêng, và sự tách bạch là bắt buộc: elapsed = giá trị đã cap (thứ simulator trả tiền cho), absent = khoảng cách thật theo đồng hồ (chỉ để hiện "bạn đã vắng N giờ"). Gộp hai cái ⇒ hoặc trả thừa, hoặc báo sai.
  • Ba nhánh trust đọc được (tái hiện, không kích hoạt): PendingCountPrefix("offline:") > 2 ⇒ từ chối thẳng; nhánh tin cậy parse LastTrustedUtc theo định dạng "o", rơi về LastSaveTime; nhánh không tin cậy dùng DateTime.UtcNow thô với LastSaveTime.
  • Evaluate (0x2A754F0): null-guard data/instance → ComputeElapsed → chụp _beforeData cho panel trước/sau → LastResult = OfflineProgressSimulator.Run(data, elapsed, absent). (Hàm này có 0 caller — đường thật là EvaluateAndApply → EvaluateRoutine.)
  • EvaluateRoutine (0x2A75714) gọi ComputeElapsed HAI LẦN và cả hai đều cần thiết. Máy trạng thái (Hex-Rays thất bại, đọc từ disasm thô): 0 → yield null → 1 → Time_manager.WaitForCheck(5s) → 2 → RunCoroutine → 3. Lần 1 (state 0) là cổng rẻ: không nợ gì thì thoát trước khi hiện UI hay chờ đồng bộ giờ, kết quả bị bỏ. Lần 2 (state 2) chạy sau WaitForCheck(5f) — sau khi reconcile giờ máy chủ có thể đã về — và giá trị của lần này mới được trả. Một optimizer "xoá lời gọi trùng" sẽ âm thầm trả tiền theo giờ thiết bị chưa đồng bộ, mở lại đúng cửa sổ chỉnh giờ mà WaitForCheck tồn tại để đóng.
  • Khoản được ghi log qua Time_manager.RecordGain dưới dạng string.Format("offline:{0:F1}h", elapsed / 3600f); tiền tố "offline:" chính là thứ PendingCountPrefix("offline:") đếm ⇒ dòng log và bộ đếm chống lạm dụng dùng chung một kênh. Đóng UNKNOWN "ngưỡng và tiền tố của guard" trong OFFLINE_ECONOMY.md.
  • Đối chiếu đã thực hiện: economy_reference_sim.py (MIN 30, MAX 28 800, relic phút×60, đồng hồ lùi ⇒ 0) khớp chính xác disasm — không có gì phải sửa ở cả hai phía.
  • Bảng của simulator (blob InitializeArray, SHA MATCH — G353): DegreeHealthMult = double[4] {1, 1.8, 3, 35}, DegreeMoneyMult = double[4] {1, 1.8, 3, 100} (xác nhận độc lập hai mảng Tree_manager..cctor), và DegreeRates = float[5,4]cùng blob với Tree_Grid.degreeRates: {80,20,0,0} {50,45,5,0} {20,55,25,0} {5,35,60,0} {0,15,85,0} (mỗi hàng = 100 %, hàng = Level_Now 0..4 — doc 05 §3.6, doc 11 §5.2).

Trần tiến độ: offline không đẩy stage vượt Stage_Value_Max — nó chỉ dồn nâng cấp. Quan sát trực tiếp trên máy: panel "Chào mừng trở lại!" ghi "Tiến trình offline không vượt quá Stage tối đa!"; sau 11m36s vắng mặt, Stage 1-1 → 1-1 Max! nhưng Lv 3/4/5 → 14/14/13.

Simulator TỰ MUA nâng cấp trong lúc offline (BuyAllAffordable 0x2AB9840 / TryBuy 0x2AB9974), nên offline cũng làm tăng level Power/Speed/Money — không chỉ tăng tiền.

Bài học framework — HAI mô hình kinh tế vẫn là hai mô hình, nhưng khác nhau ở CHỖ KHÁC so với đã nghĩ: - Khác biệt về multiplier chỉ là buff quảng cáo ×3 (online có, offline không). - Khác biệt thật nằm ở mô hình sinh tiền: online = tiền rơi từ từng quái bị bắn; offline = mô phỏng clear wave (thời gian combat, kill rate, tự mua nâng cấp, cày lại wave cao nhất) — xem 12 luật bên dưới. - Dùng MoneyValue_return × thời gian cho offline vẫn sai (không có mô hình clear/kill-rate); dùng mô hình offline cho online cũng sai. Mô hình tham chiếu (§5.6) phải thêm các số hạng skin/pet/relic vào offline_income_factor (G1001) và không có mô hình clear-wave ⇒ nó không cho ra được con số tiền (gap đã ghi); harness của lane offline (_evidence/Offline/Simulator/harness/) là bản đầy đủ.

12 luật của simulator offline — VERIFIED (đợt 7, G382 / G1003; OfflineProgressSimulator dựng lại trọn 27 RVA, harness Python độc lập tái tạo oracle .sot đúng 17 chữ số):

  1. Vòng lặp chạy khi timeLeft > 0.05 s, tối đa 30 000 bước, ngân sách 60 ms mỗi frame rồi yield.
  2. Mỗi bước: BuyAllAffordablemua level THẤP NHẤT trước, hoà thì Power > Speed > Money, giá tra ở level hiện tại; có thay đổi thì tính lại ClearInfo / tốc độ.
  3. clearTime = CombatTime × 1.3 + 5 s; nếu còn đủ thời gian: +CalcMoneyPerStage, tiến wave (5 level/stage) nhưng KHÔNG BAO GIỜ vượt Stage_Value_Max — sim cày lại wave cao nhất từng đạt.
  4. Nếu một lần clear không còn vừa: +perSec × timeLeft rồi dừng.
  5. Wave không thể clear (CombatTime = +∞, tức > 180 s): cày thu nhập/giây cho tới khi nâng cấp rẻ nhất mua được (trần 1 năm), mua, thử lại.
  6. Cây thường = clamp(10·stage + 30, 30, 100) × (level + 1); HP cây = HP stage × Σ_{i<3} DegreeRates[level, i]/100 × DegreeHealthMult[i]; tiền tương tự với DegreeMoneyMult; level 4 thêm một cây boss (HP ×35, tiền ×100).
  7. Sát thương mỗi súng = Gun.Damage × Power/100 × Rebirth/100 → cắt, × Skin/100 → cắt, × (Fish+100)/100 → cắt, × Relic/100 → cắt; chí mạng = dmg × (CriVal+125)/100 → cắt, × RelicCri/100 → cắt; kỳ vọng = dmg·(1−criRate) + crit·criRate. Chuỗi sát thương CẮT ở mỗi bước phần trăm.
  8. atkSpd = clamp(Speed_balance[lv] + RebirthSpd + SkinSpd − 30·stage − 200, 100, 10000); giây/phát = clamp(AtkSpeed / atkSpd × 100, 0.05, 2); một phát giết MultiTargetCount cây (Laser/Pierce 10, Explosive/Missile/BlastSniper 5, Shotgun max(pellets, 1), Single 1) nếu one-shot, ngược lại ceil(HP / kỳ vọng) phát; sàn 0.1 / petSpeed (giảm nửa dưới mốc cao nhất).
  9. CombatTime = max(0.05/petSpeed × số cây, số cây / killRate) (+ HP boss / DPS); +∞ nếu > 180 s.
  10. Tiền/giây = stageMoney × avgDegreeMoney × MoneyBal/100 × Rebirth/100 × Skin/100 × Pet/100 × Relic/100 × killRateKHÔNG cắt (phần lẻ sống sót: .5021456285 của oracle).
  11. ToInfVal bão hoà ở 1e15.
  12. ApplyToManagers ghi thẳng Gamemanager.Money / Money_Accum, InvalidateMoneyCache, Get_Money(0) để bắn OnMoneyChanged, Firse_reload(levels), Tree_manager Stage_Now / Level_Now / Stage_Value_Max, Contents_Lock_Reload.

Ví dụ với số thật (save Stage 2-2, mốc cao nhất 22, Upgrade_Lv [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) — harness và game gốc cho cùng kết quả:

Bước Điều xảy ra Số
Combat một wave 82.317 s ⇒ clearTime = 82.317 × 1.3 + 5 112.012 s / wave
2 lần clear ở Money lv 35 722 844 mỗi lần ⇒ tiền vượt 3 900 000 +1 445 688
BuyAllAffordable Money là level thấp nhất (35 < 36) và rẻ nhất ⇒ 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đây là nơi phần lẻ sinh ra +179 261.502
Wave 2-3 = 23 > 22 ⇒ không tiến, cày lại 2-2 stage 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] khớp 17/17 chữ số với game gốc

Thời điểm được trả (mốc tin cậy 20:06:05.377Z + 802.993 s = 20:19:28.370Z) trùng độc lập với dấu thời gian autosave của game gốc — elapsed được kiểm chứng chéo, không phải khớp cưỡng.

Bẫy R13 còn chặn kết quả trên thiết bị (G1002): Rebirth_manager.Money_return / Speed_return (0x2B22434 / 0x2B22368) còn trả default(InfVal) = 0không phải ngoại lệ nên SafeManagerCall trả 0 thay vì fallback 100 ⇒ mọi con số offline × 0; Upgrade_manager.Firse_reload (0x2B49FAC) rỗng ⇒ không đẩy được level; Fish_manager.GetTotal* (0x2B01760/84/A8), Skin_manager.*_Accum_return (0x2B3F6AC/738/7C4) trả 0 (chỉ đúng với bể trống / skin 0). Chín stub nhỏ này là lý do khoản trả offline trên thiết bị vẫn đang chờ.

Đợt 8 (G385): 9 blocker ĐÃ ĐIỀNRebirth_manager.Speed_return = Upgrade_list[1].Value_return() + 100, Money_return = Upgrade_list[2] + 100 (chỉ số chứng minh bằng bounds-check inline); Skin_manager.*_Accum_return = tổng Skin_list[i].Damage/Speed/Money trên các skin đã mở (chỉ phần bonus, base 100 cộng ở nơi dùng); Fish_manager.GetTotal* = accessor lười gate bằng _cacheDirty (+0x90) — kéo theo RecalculateTotals (0x2B018C0) và Fish_manager..ctor (_cacheDirty = true là bẫy R13: để false thì tổng cá không bao giờ tính; Inventory = new List<int>, is_AutoMerge = true); Upgrade_manager.Firse_reload; CalcPrice dạng đóng (doc 07 §2.2). Khoản trả offline trên thiết bị vẫn chưa quan sát được (batch 17 trước overlay này; save mới còn bị UtcNow = default chặn InitNewGame — G386/G389).

⚠ BẪY KHÔI PHỤC (retraction của một phát biểu trước đó)

Bản trước của tài liệu này viết rằng "nếu nhánh fallback chạy, thu nhập offline sẽ cao hơn tới 10 %". Rút lại claim đó: nhánh fallback KHÔNG THỂ chạy trong build 1.1.59.

OfflineProgressSimulator.GetMoneyBalance (RVA 0x2AB9018):

int i = Mathf.Clamp(lv, 0, 10000);
var inst = Upgrade_manager.instance;
if (inst != null && inst.Money_balance != null && i < inst.Money_balance.Length)
    return inst.Money_balance[i];                                  // MẢNG SCENE (đường thật)
return ((InfVal)(Math.Pow(1.08, i) * 100.0)).ToPrecision(3);       // fallback dạng đóng

Ba lý do nó không tới được: 1. Nhánh biên là bất khả thi. Money_balance trong Main.unityđúng 10 001 phần tử (đã tự đếm giữa dòng 20393431 và 20413434 ⇒ xác nhận), còn chỉ số bị kẹp về [0, 10000]. Vậy i < Length luôn đúng. 2. instance đã được đặt trước khi caller chạy. Chuỗi gọi duy nhất là LoadData.Load → EvaluateAndApply (0x2A75174) → EvaluateRoutine → RunCoroutine; Upgrade_manager.Awake (0x2B49EE0) đặt instance, và cả hai cùng nằm trong một scene duy nhất, nên mọi Awake hoàn tất trong lúc load scene, trước đường save-load. (OfflineProgressManager.Evaluate 0x2A754F00 caller — dead.) 3. Money_balance được serialize trong scene ⇒ không bao giờ null khi component tồn tại.

⇒ Không có lạm phát nào xảy ra trong app đã ship. Giữ điều này lại như một bẫy khôi phục: nếu một bản restore tách manager sang một bootstrap scene khiến việc đánh giá offline chạy trước Awake, fallback dạng đóng sẽ âm thầm kích hoạt và thổi phồng thu nhập (chênh ~10 % ở lv 50, ~6.5 lần ở lv 1000 — xem §5.7). Quy tắc: luôn đọc mảng đã serialize. Cùng dạng fallback tồn tại ở GetPrice (0x2AB9A48), GetPowerBalance (0x2AB87C4), GetSpeedBalance (0x2AB88D4).

6. Negative evidence & UNKNOWN

Mục Trạng thái Bằng chứng khoá được nó
ToString(int) truncate hay round ĐÃ CHỐT: CẮT. Xác nhận thêm bằng ToPrecision (2 kênh) và bằng parity test in-Editor 14/14 PASS Mở rộng bộ mẫu ở giá trị lớn (đơn vị sau z)
Đường code viết HOA đơn vị ở text bay ĐÃ ĐÓNG: không có đường code nào.art của font — một formatter duy nhất, không ToUpper, glyph lowercase a–z được vẽ thành chữ hoa. Giả thuyết "formatter thứ hai" bị bác bỏ UPPERCASE_FINDING.md; G141
Panel nâng cấp hiển thị giá trị gì ĐÃ ĐÓNG. Hiển thị đúng Money_balance[Lv] = 3 120 → 3.12a. Kết luận cũ ("giá trị dẫn xuất") là SAI, do mô hình làm tròn của chính tài liệu này Main.unity:20393431; sim mục [N]
Lệch giá nâng cấp 136b vs 134b tại Lv.46 ĐÃ ĐÓNG. Mảng scene cho đúng 1.34×10⁸134b. Lệch là lỗi của mô hình làm tròn cũ, không phải của SOT Main.unity:20353425; sim mục [N]
Phân rã hệ số ×7.61 (mẫu cũ) KHÔNG THỂ tách — phiên bắt đó không ghi state. Vẫn hợp lệ như kiểm tra đầu-cuối. (đã có cách khác: xem dòng dưới)
Số hạng Skin / Pet / Relic ĐÓNG 2/3 (đợt 4, tĩnh): Relic — Value_Reload (0x2ADF8F0) baseline 100 + đường cong seed 105 ×1.05/level (3 chữ số) −100, bậc ×1/×1.5/×2 (G361); Skin — Skin_Value_Reload (0x2B3F068) cộng dồn base-100 (G359). Pet vẫn UNKNOWN; đo runtime trên save có sở hữu vẫn còn nợ Decompile Pet_manager; đo lại trên save đã tiến triển (G185)
Điểm gãy ×2.0 (giá upgrade, lv ≥ 5000) trong mảng scene UNKNOWN — mới đối chiếu tới lv 59 Đọc Main.unity ở idx 4999–5002
Mô hình kinh tế offline (mô hình thứ hai) ĐÃ ĐÓNG (đợt 7, G382): OfflineProgressSimulator dựng lại trọn (27 RVA, 42/42 start_ea khớp) và harness độc lập tái tạo oracle .sot đúng 17 chữ số (+1 627 259.5021456285) — 12 luật ở §5.8. Còn mở: khoản trả trên thiết bị chờ 9 stub R13 (Rebirth.Money_return/Speed_return, Firse_reload, Fish.GetTotal*, Skin.*_Accum_return) — G1002 Codegen điền 9 stub → nghiệm thu lại residual (2)
Precision của InfVal.op_Implicit(double) (0x44DF694) UNKNOWN — ảnh hưởng chữ số cuối ở mọi chỗ có double (vd √n trong thưởng rebirth) Decompile 0x44DF694 + op_Multiply
Thứ tự đơn vị sau z (aa, ab, … hay A, B) ở bậc rất lớn UNKNOWN Capture on-device ở giá trị lớn hơn; mẫu hiện tại dừng ở a
GetMoneyCacheKey sinh key chính xác thế nào ĐÃ ĐÓNG (G400): 7 phần value\|Upgrade\|Rebirth\|Skin\|Pet\|Relic\|buffActive (§2)
45 method Gamemanager còn lại (anti-cheat / time / save / network) CHƯA dựng lại — cố ý; là integrity machinery, ngoài phạm vi R1-adjacent G129, G134
Version InfVal chính xác mà SOT ship UNKNOWN — chỉ biết "cũ hơn 2.2.0, có API unitsList/DisplayOption" Metadata strings / store archive
Money_Accum có bị Rebirth reset không ĐÃ CHỐT: CÓ. Rebirth() gọi Money_Reset(), vốn xoá cả Money lẫn Money_Accum disasm RVA 0x2B22AE00x2A4F1F8; xem doc 09 §3.1
Có bảng cân bằng từ xa ghi đè lúc chạy không ĐÃ ĐÓNG: KHÔNG (trong 1.1.59). Firebase RemoteConfig chỉ chạm UpdateManager; PlayFab GetTitleData 0 caller; ServerConfig chỉ ads/freecash/offerwall. Gun_manager.Request_Sheet (Google Sheet TSV cho Gun_list) tồn tại nhưng chỉ tới được từ Balance_Reload đã chết FABLE §U4
Balance_Reload có chạy lúc runtime không ĐÃ ĐÓNG: KHÔNG. Dead code — CodeRefsTo = [], không string literal, không UnityEvent. Bảng thật = mảng serialize trong scene FABLE §U4, §B-3
Bản học: thu nhập ≈ ×300 SOT (3.2 M/s vs 8.6 K/s cùng file, batch 20) MỞ — LỖI THẬT của bản dựng lại (G414), nghi một hệ số base-100 áp thô (×100/×10 000) trong tích MoneyValue_return hoặc tiền quái; không phải cơ chế SOT — mọi số trong doc này vẫn là của SOT Log 6 mắt + tiền quái cùng frame trên .learning; so .sot
Không quan sát thấy Không có đường cộng (+) nào trong MoneyValue_return — 25 mẫu parity đều khớp k·x thuần nhân moneyvalue_return_parity_samples.json

7. Raw evidence

  • [evidence codegen: economy_core.cs.evidence] — layout static + 8 method tiền tệ
  • [evidence codegen: MoneyValue_return.cs.evidence] — chuỗi nhân + LRU
  • [evidence codegen: moneyvalue_return_parity_samples.json] — 25 mẫu runtime
  • [disasm: Gamemanager_economy.c], MoneyValue_return.c, GetMoneyCacheKey.c}
  • [evidence codegen: README.md], InfVal_change.c, InfVal.CatGunnerBridge.cs, InfVal.ParityTest.cs}
  • [evidence codegen: UPPERCASE_FINDING.md] — chỉ có một formatter; chữ hoa là art của font
  • [evidence codegen: text_spawn.cs.evidence] — 5 spawner text nổi + throttle
  • [report: InfVal-parity-20260904T125826Z.xml] — parity in-Editor 14/14 PASS
  • [liveops catalog: PROBE_RESULT.md] §4 — phân rã MoneyValue_return (×2.63)
  • [evidence codegen: infval_change_parity_samples.json], parity_pairs_smallvalue.json}
  • [runtime trace: econ_validate.js], run_econ.py, run_mvr.py, mvr_samples.json, infval_samples.json}
  • [reconstructed: Ore.cs] — call-site Dia_Devide_Txt_Spawn
  • [phân tích nội bộ] — U1–U4 + B-1…B-13 (nguồn của bản đính chính bảng cân bằng)
  • [F001: scene_balance_tables.json] — 8 mảng balance trích từ Main.unity
  • [evidence codegen: PRESTIGE_RULES.md], Rebirth_Balance_Reload.c, Upgrade_Balance_Reload.c} — hàm sinh (dead code; một phần bị FABLE_ANALYSIS supersede)
  • dump.cs:583820 — khai báo Gamemanager đầy đủ (field, const, RVA)
  • [F001: economy_reference_sim.py] — mô hình tham chiếu chạy được, 70/70 PASS
  • [device evidence: user_screenshot_stage5-4.png] — save thật của người dùng (Stage 5-4, Lv.46), sha256 04ef8221dc6000c0bd064fc9ea6c0281fb88f8118ea0c538f1d5b83399dc7641
  • Tài liệu liên quan: 09-prestige-rebirth.md
  • Gaps liên quan: G007, G007a, G012 (diễn giải lại), G122, G129, G141, G186 trong gap list (nội bộ)