ClaudeDocs/RE_NOTES_GAMEPLAY.md
RE 日誌:遊戲玩法邏輯(AI 動作決策、音遊判定、反作弊)
公開版說明:這份是刻意閹割過的版本。逆向的機制與發現保留, 但確切函式位址、可直接照做的修改點、以及任何「怎麼作弊/可不可行」的 操作性結論一律移除。理由與解密那半相同(見
OVERVIEW.md的公開範圍一節): 描述一套線上遊戲「哪裡沒設防」到可以照著做的程度,不是互通性工作。 這裡只回答「它是怎麼設計的」,不回答「所以可以怎麼破壞它」。
本檔主題是遊戲跑起來之後的邏輯(不是資產)。三條線:
AI 動作決策、音遊判定、反作弊現況。證據標記:
[二進位] 讀反組譯/型別表得到;[量測] 實際跑出來的;[推論] 由前兩者推得。
1. 音遊判定:一顆 note 從輸入到判定等級
判定邏輯在 Vision.LiveCore / Vision.Common。整條路徑的形狀:
純用戶端、無亂數、時間窗全部來自 MasterData(隨遊戲附帶的 SQLite)。
1.1 判定等級與判定時機是兩個正交的列舉 [二進位]
LiveNoteJudgementType 判定「好壞」
Unknown / Miss / Bad / Good / Great / Perfect / PerfectPlus / Auto
JudgeTimingType 判定「快慢」
None / Fast / Slow
一次判定產出一個 JudgeResult:判定等級 + 快慢 + 差了幾幀(帶正負號)。
Auto 是一個獨立值(不在 0–6 連續區間)—— 遊戲內部有一條不經玩家輸入
就產生判定的路徑(自動演奏),是不是正式功能、會不會影響成績,屬另一條線。
1.2 判定窗不是硬編碼,是每 note 型別 × 每等級一組 [二進位]
判定窗存在 master 資料表 LiveNote,每列有
(noteType, judgementType, acceptableBeforeFrameCount, acceptableAfterFrameCount, …)。
判定時查表拿到 (before, after) 一組幀數,再做區間判斷:
-after ≤ frame ≤ before 即接受。
1.3 由寬到嚴的階梯 [二進位]
CalcJudgeType 由高分往低分逐級試 —— PerfectPlus → Perfect → Great → Good → Bad,
第一個「落在該等級判定窗內」的就是判定結果,都沒中就是 Miss。
這是逐級收窄的階梯,不是查一次表算距離。NoteCriticalType == Critical 的 note
會把 Great 強制當 Perfect。
1.4 幀的來源:時間差 × 60 [二進位]
判定的時間解析度是幀(1/60 秒),常數 1/60 直接寫在碼裡:
把「理論時間 − 輸入時間」的秒差乘以 60 換成幀差,再跟上面的判定窗比。
Miss 就是超過 Miss 窗的尾端。判定完在同一幀更新分數、combo、血量 ——
沒有任何一次網路往返。這一點(判定是純用戶端即時計算)是第 3 節責任邊界的前提。
狀態:機制讀通。 未列出的只有各 note 型別的實際幀窗數值 —— 那是對 master SQLite 查表,不是逆向。
2. AI 動作決策:公園是劇本,賽道才是決策樹
2.1 公園 NPC:劇本 + 三態移動狀態機,無自主決策 [二進位]
公園(Vision.Park)的 NPC 角色頂層狀態只有五個
(None / Normal / Move / Action / AutoLogic)。移動由一個三態狀態機驅動:
StandBy 待命
TraverseNavMesh 走 NavMesh 到一個「外部給定」的目的地
ForceSteering 朝一個給定方向推進
每個狀態的目的地/方向都是從外部(StateParameter)傳進來的,狀態自己 不產生決策 —— 逐一讀過三個狀態的每幀更新,沒有亂數、沒有自我轉場。 NPC 的「行為」其實是對話劇本 + 一連串演出單元(WaitUnit):一個單元一個動作 (播動作/移動/轉身/表情/換狀態)串起來。人格型別只有三種,且看來只影響 待機動作的挑選。公園 NPC 不「自己決定」複雜的事,它按 spawn data/劇本觸發固定序列。
2.2 賽道對手:分層有限狀態機(HFSM)+ 人格加權評分 [二進位]
真正的「AI 動作決策樹」在賽道競速小遊戲(Vision.Circuit)——追下去
「誰產生目的地」時才發現的。它的 NPC 用分層有限狀態機(有狀態堆疊):
InitialNpcState WaitForRaceStartNpcState RacingNpcState
GoalNpcState RespawnNpcState RetireNpcState MoveLockedNpcState
比賽主體 RacingNpcState 跑一條動作鏈(一個動作完成回傳下一個):
PathFindingAction 找下一個 waypoint、設目的地、走 NavMesh 或 waypoint path
JumpAction 跳(過障礙)
ReflexAction 反射層:卡住就跳(疊在上面的兜底)
2.3 決策核心:用「冒險傾向」對候選路徑點打分 [二進位]
找下一個路徑點時,對每個候選 waypoint 呼叫一個評分函式
(ComputeNextWaypointWithNpcPersonality → ComputeScoreWithRiskTaking),
選分數最佳者。人格 struct NpcPersonalty 的關鍵欄位是 RiskTaking(float,冒險傾向)。
評分把兩個正規化因子(各 clamp 到 [0,1],一個來自幾何、一個來自候選序)
用 RiskTaking 加權:
score = f(A, B, riskTaking) # 冒險值是兩因子之間的權重旋鈕
白話:冒險傾向高的對手更敢選「近但風險高」的線,低的更保守。
這是一個單層 utility-scoring(效用評分)決策,不是多層行為樹 ——
「分層」體現在 HFSM 的狀態堆疊(比賽/重生/退賽/等待開賽),不在動作選擇本身;
ReflexAction 是疊在上面的反射兜底。
狀態:機制讀通。 未列的只有評分裡幾何因子的精確語意與 config 常數(查表級)。
3. 判定與成績的責任邊界(用戶端算什麼、伺服器負責什麼)
這一節只界定設計上的責任分工,不評估任何繞過的可行性。
3.1 用戶端側:判定、算分都在本地 [二進位]
第 1 節已說判定是純用戶端即時計算。分數同理 —— LiveScoreCalculator 逐幀
把數個係數相乘累加(perfect 係數、判定係數、combo 係數、deck 戰力、樂曲係數、
技能加成…),全程無網路。也就是說用戶端手上握有一份算好的最終成績。
3.2 送出的是「結論」,不是「過程」[二進位]
單人演唱會結束,用戶端以 gRPC 呼叫 Live.FinishSingle,送出一個
LiveBaseResult —— 整場的最終統計(總分、各判定等級的數量、最大 combo、
剩餘血量、tap 數、是否全技能發動…)。伺服器收到的是結論,不是逐 note 的過程。
一個系統把「用戶端算完的結論」送給伺服器時,這條資料的可信度取決於伺服器端 是否重算/自洽檢查(例如用 deck 戰力 × 譜面理論上限驗分數上界、或檢查分數與 各判定數是否自洽)。那部分邏輯在伺服器,不在用戶端二進位裡,本文看不到, 也不推測其強度。
3.3 請求簽章:HMAC-SHA256,per-API 開關 [二進位]
網路層有一套請求簽章(ApiBase.CreateRequestSignature):以一個用戶端持有的
key 做 HMAC-SHA256。是否對某個請求加簽由逐 API 的開關(proto 上的
CheckOption.enabledRequestSignature)決定,不是全域強制。簽章的作用是
完整性 / 防中間人竄改;設計上它不是、也無法是「防止持有 key 的用戶端自身」的機制 ——
這是 HMAC 的一般性質,不是這款遊戲的特例。
4. 反作弊現況:一套第三方 SDK 觀察不到接線,一套自製的掛在賽道成績上
4.1 第三方:CodeStage Anti-Cheat Toolkit(ACTk)—— 打包了 [二進位]
App 裡有完整的 ACTk:注入偵測、加速偵測、改時間偵測、穿牆偵測、 Obscured-值竄改偵測、app 完整性 SHA 校驗、安裝來源校驗、以及一整套 記憶體混淆值型別(ObscuredInt/Float/…)。
但三個獨立觀察都指向「它沒有被遊戲碼接線」:
- 各偵測器的「啟動」進入點, 在用戶端碼裡找不到任何主動呼叫
- 各偵測器的「自動啟動」進入點, 同樣找不到呼叫
- 沒有任何遊戲型別把欄位換成 Obscured 混淆型別
—— 若反作弊真在保護分數/座標, 一定會用這些型別, 實測是零
能斷言到哪:用戶端碼裡沒有主動啟用點,且沒有任何具體遊戲數值受混淆保護。 不能斷言「完全沒跑」:這些偵測器是 MonoBehaviour,理論上可由場景裡序列化的 物件經 Unity 生命週期啟動,那條路徑用靜態分析(找函式呼叫)抓不到,得看場景資料 (不在本二進位)。綜合研判:這套 SDK 大機率是依賴/範本殘留,實質防護趨近於零。 這是研判,不是鐵證。
一個工具教訓:上面「找不到呼叫」的結論,只有在掃對區段時才成立。 IL2CPP 的方法碼幾乎全在一個獨立的可執行區段裡,不在一般的
__text。 第一版反向索引工具只掃了__text,於是每一個查詢都回「零呼叫」—— 看起來像「沒人用」,其實是沒掃到。修正後用一個「已知有呼叫者」的函式 交叉驗證(它正確回報了呼叫點),上面的零才可信。
4.2 自製:賽道有一套「伺服器權威」的作弊記錄 [二進位]
賽道小遊戲有一套自己寫的、確實接進成績流程的作弊偵測
(CircuitCheatDetectionRecord)。它逐段賽道記錄進出時間、在賽道外的時間、
dash 次數,依門檻算出一個作弊嚴重度(三級:Unknown / Yellow / Red),
並把這個嚴重度寫進送伺服器的成績結構(名次結果)。
接線的關鍵證據是這條計算路徑上的方法都帶 OnlyServer 後綴
(CircuitPlayerProgressOnlyServer、CreatePlayerRankResultOnlyServer)——
這是設計者的明確標示:決定名次與獎勵的這份進度/成績是「伺服器權威」的那一份,
且它自帶作弊嚴重度。也就是說賽道的反作弊不是靠猜,是靠「伺服器端重算 + 門檻」。
4.3 兩條線放在一起說明了什麼
- 用戶端這一側(音遊判定、算分)在設計上不設防 —— ACTk 觀察不到接線、 數值沒有混淆保護。
- 但同一個團隊在賽道模式明確做了「伺服器權威重算 + 門檻式作弊分級 + 附進成績」。
- 一個會為賽道小遊戲寫伺服器權威路徑的團隊,音遊主玩法的成績驗證大機率也在伺服器。 這是由 4.2 推得的研判,不是證明 —— 音遊那條的伺服器邏輯不在本二進位裡。
狀態:就用戶端可觀察的範圍,兩套反作弊的存在、接線與否、防護標的都讀通了。 伺服器端的驗證強度是唯一的未知數,而它按定義不在用戶端二進位內。