01-economy-money.md · Cập nhật lần cuối: 2026-09-06 23:05 +07 · sha256 nguồn 83a859e7734a01 — Kinh tế & tiền tệ (Economy / Money)
Nguồn: chưng cất từ quá trình restore SOT
com.Chodun.CatGunner1.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ủalibil2cpp.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() có 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); Dia là ObscuredInt |
disasm-proven | Static layout từ class off_5E2AE78 → static_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.68 và 60.0 → 157.80 — cả 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ÔNG có ToPrecision (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ả Money và Money_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 @0x2A4F358 — sổ 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_balance là mả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 = 50 là hằ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_digits là string.
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ề100và 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à
×3cứ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()(RVA0x2A4E324) xoá cả_moneyCachevà_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: mag là số 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: clampLose_Money, bảng cân bằng,ToPrecision(5)cho thưởng rebirth). VERIFIED hai kênh: code0x44E0E5C → ToExponent 0x44DF888 → set_exponent(chia BigInteger), và dữ liệu — 1 202 phần tử bảng balance trongMain.unitykhớ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.mdrằngToPrecisionlà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.xmllà 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ị sauz). 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]sha25604ef8221dc6000c0bd064fc9ea6c0281fb88f8118ea0c538f1d5b83399dc7641(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ỉ 0x28227xx là thunk 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 và 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
0x2AE5950 và 0x2AE5A84 → Cat.Damage_str (@0x90) / Cat.CriDmg_str (@0x98), rồi Bullet chuyển
tiếp sang Gamemanager.Damage_Collider → Damage_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:
-
IBigNumberlà kiểu tiền tệ duy nhất, serialize-được dưới dạng text. Không dùngdoubleở bất kỳ đâu trong đường thu nhập. Bắt buộc struct (không box) và cóToPrecision(int)để chuẩn hoá. -
IMultiplierSourcetrả về phần trăm, mặc định 100.csharp public interface IMultiplierSource { BigNum PercentFor(StatKind kind); } // 100 == neutralPipeline 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. -
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.
-
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 choIMultiplierSourcebắ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). -
Tách
Wallet(tiêu được) khỏiLifetimeEarned(chỉ tăng), dùng chung một event thay đổi. -
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.
-
Throttle floating text ở tầng framework (
CanSpawnText+ cooldown cleanup), vì mỗi viên đạn trúng đều muốn sinh text. -
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 480000Ba đ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ải108,324,972. Chi phí: một hàm 6 dòng (floor(log10)→scale = 10^(digits−exp−1)→ hai lầnMath.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 000so với2.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 choDamagetổng1026(+1.02a%) vàCriValtổng866 + 125 = 991%— hai tổng độc lập, khớp cả hai là đủ đóng câu hỏi. -
Chỉ số phụ nên có TRẦN, và trần nên nằm GIỮA bảng.
CriPerchạm100ngay 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:Clampphả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ỗ. -
⚠ 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 / .cctornóiScene nói Ai thắng lúc chạy Cat_manager.FormationRadius102.5scene 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ủaAddComponent;privatekhông[SerializeField]⇒ ctor là giá trị thật;const/static readonly⇒ ctor/metadata là thật. VớiFish_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ưng0,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 là 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.61 — state 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ó
_digitslàBigIntegermà 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
Nó 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]) |
có | có (GetMoneyBalance) |
Rebirth money (Rebirth_manager.Money_return) |
có | có (qua SafeManagerCall lambda b__13_2) |
Skin_manager.Money_Accum |
có | CÓ — đọc trong MoveNext (0x2ABA4DC..0x2ABA754, null-guard ⇒ 100), nhân vào cả CalcFarmMoneyPerSec lẫn CalcMoneyPerStage |
Pet_manager.Pet_Money_Accum |
có | CÓ (như trên, ép (int)) |
Relic_manager.Money_Value |
có | CÓ (như trên) |
Buff_manager Money_Buff ×3 |
có | KHÔNG — thứ DUY NHẤT vắng mặt offline |
| đường cong stage / degree | (qua giá trị Money của quái) |
có (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 / relicCriDmgcũng vào chuỗi sát thương trongCalcClearInfo. Mục CORRECTION đã được nối thêm vàoOFFLINE_ECONOMY.md(không sửa lịch sử).
Cửa sổ thời gian — ComputeElapsed (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ả
- Cap —
EffectiveMaxOfflineSeconds()(RVA0x2A74B2C): ```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ì.
- (Relic_manager.instance != null ? Relic_manager.Offline_Value * 60f : 0f);
- 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ị sceneMain.unity:20446002).ComputeElapsedtrả 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 parseLastTrustedUtctheo định dạng"o", rơi vềLastSaveTime; nhánh không tin cậy dùngDateTime.UtcNowthô vớiLastSaveTime. Evaluate(0x2A754F0): null-guard data/instance →ComputeElapsed→ chụp_beforeDatacho 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ọiComputeElapsedHAI 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 sauWaitForCheck(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àWaitForChecktồn tại để đóng.- Khoản được ghi log qua
Time_manager.RecordGaindưới dạngstring.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" trongOFFLINE_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ảngTree_manager..cctor), vàDegreeRates = float[5,4]— cùng blob vớiTree_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_Now0..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àooffline_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ố):
- 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ồiyield. - Mỗi bước:
BuyAllAffordable— mua 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ạiClearInfo/ tốc độ. 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ượtStage_Value_Max— sim cày lại wave cao nhất từng đạt.- Nếu một lần clear không còn vừa:
+perSec × timeLeftrồi dừng. - 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. - 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ớiDegreeMoneyMult; level 4 thêm một cây boss (HP ×35, tiền ×100). - 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. 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ếtMultiTargetCountcây (Laser/Pierce 10, Explosive/Missile/BlastSniper 5, Shotgunmax(pellets, 1), Single 1) nếu one-shot, ngược lạiceil(HP / kỳ vọng)phát; sàn0.1 / petSpeed(giảm nửa dưới mốc cao nhất).CombatTime = max(0.05/petSpeed × số cây, số cây / killRate)(+ HP boss / DPS);+∞nếu > 180 s.- Tiền/giây =
stageMoney × avgDegreeMoney × MoneyBal/100 × Rebirth/100 × Skin/100 × Pet/100 × Relic/100 × killRate— KHÔNG cắt (phần lẻ sống sót:.5021456285của oracle). ToInfValbão hoà ở 1e15.ApplyToManagersghi thẳngGamemanager.Money / Money_Accum,InvalidateMoneyCache,Get_Money(0)để bắnOnMoneyChanged,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) = 0 — khô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ỀN — Rebirth_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 có đú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 0x2A754F0 có 0 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. Là 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 0x2B22AE0 → 0x2A4F1F8; 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-siteDia_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áoGamemanagerđầ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), sha25604ef8221dc6000c0bd064fc9ea6c0281fb88f8118ea0c538f1d5b83399dc7641- 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ộ)