速查 › 執行檔裡剩下的原始碼痕跡

執行檔裡剩下的原始碼痕跡

這個遊戲的零售執行檔裡,留著 68 條原始碼檔案路徑 (EA 自己的 64 條,微軟 C 執行期函式庫 4 條), 其中 62 條長成 e:\mvp2005pc\source\common\… 的樣子。 而真正有價值的不是那些路徑,是緊貼著它們的一整排內部名稱表 —— 那是這個遊戲的內部構造,用它自己的詞彙寫的。

為什麼原始碼路徑會留在出貨的程式裡

C 和 C++ 有一個叫 __FILE__ 的東西,編譯時會被換成當前檔案的完整路徑。 它最常見的用途是斷言:「這裡不該發生的事發生了,出事的是哪一個檔、哪一行」。

斷言訊息要印路徑,路徑就得存進二進位檔。於是每留下一條路徑, 旁邊就有一句 EA 工程師寫給自己看的話。

整個遊戲的架構,用檔案數量說話

子系統檔數裡面是什麼
ai/22 打者、投手、野手、跑者、球員本體的判斷邏輯
geomlib/21 幾何碰撞:圓錐、方盒、射線、球體、網格、骨架,以及它們兩兩相交的測試
script/8 播放腳本:simtoendofplayteleportactorgosubrandom
frontend/5介面與疊層
assetmanager/ database/ controller/ comm/ main/ interfaces/ 各 1資產、資料表、手把、連線、進入點、投打介面
libraries/gamelib/2 離線表管理員與路徑清單,那是跨專案共用的函式庫
(編譯器自己的)4 f:\vs70builds\3077\vc\crtbld\ —— 微軟 C 執行期函式庫的原始碼

AI 跟幾何碰撞加起來佔了 63%。一個 2005 年的棒球遊戲, 大半的工程量花在「電腦球員要做什麼」跟「兩個東西有沒有碰到」。

⭐ 方法:從「字串挨在一起」升級到「指標真的指過去」

這一頁最重要的不是任何一條結論,是驗證方式的轉變。

舊方法為什麼不夠

看到一排名字連在一起、數量又剛好對得上某個東西,很容易就寫成「這是一張表」。 但字串挨在一起只證明編譯器把它們放在一起,不證明它們屬於同一張表。

本站把「數量剛好對得上」列為最弱的一級證據,正是為了防這個。

新方法:去找那張表本身

C 語言把「編號 → 名字」做成一個指標陣列:一格一個位址,每個位址指向一條字串。 那個陣列是真的存在於檔案裡的資料,不是推論。

做法(不反組譯,只讀資料):

  1. 解 PE 檔頭,把字串的檔案偏移換算成程式執行時的位址
  2. 全檔掃描,找哪些四位元組剛好等於那些位址
  3. 如果它們連續排在一起、前後被別的東西夾住 —— 那就是那張表,而且格數是數出來的

用這個方法,好幾條原本只能標「合理但沒證實」的發現, 變成格數、順序、起點全部量得出來

量出來的內部構造

守備員的行為是一張 50 格的任務表

指標陣列在檔案偏移 0x4FDB3050 格, 順序正好是字串區塊的完全倒序,前後被浮點常數夾住, 而那 50 條字串在整支執行檔裡各自只被引用一次

第 0 格是 Not In Use,第 1 格是 Manual, 最後 8 格全部以 UserControl 開頭。 而這一區後方 156 個位元組是 fieldergamecannon.cpp,再往後兩千個位元組之內, 同一段字串池裡依序排著 fielderuser.cpp(+432)/ liveplaytasksuser.cpp(+1716)/ liveplaytasks.cpp(+2080)—— liveplaytasks 對上 Task,而 user 版自成一個檔, 反過來佐證那 8 格是玩家操控時才跑的。 (2026-09-05 訂正:原本把 156 這個距離寫成落在後面那三個檔上, 實際落點是 fieldergamecannon.cpp;距離都是從最後一條字串 Not In Use 的起點數起。三個檔同在一段字串池這件事不受影響。)

內容看得出顆粒度:接球分成六種(一般、撞牆、撞牆跳躍、接傳球、接不到的球…), 傳球有 ThrowToTarget / ThrowToFielder / DelayedThrow / FakeRundownThrow,中繼有 GotoCutoffPosition / ConsiderCutThrow,還有 LookRunnerBack / HoldThePlay / GiveUpBase 這種「刻意不做事」也是一項任務

⚠️ 一開始還寫了「CatchAThrowCatchAThrowNew 並存, 代表舊程式碼沒刪、兩套同時活著」——覆驗把這句退回成猜測: 列舉裡有兩個名字不等於有兩條活的程式碼,New 也可能只是別名。 旁邊那張疑似派送表是 48 格不是 50 格,兩種對齊方式都說得通, 覆驗者拒絕挑「讀起來比較順」的那一種

受傷有 27 種,而且每一種都綁一個具體的比賽動作

指標陣列在 0x4fe7a828 格:第 0 到 26 格是分類, 第 27 格是計數哨兵 NUM_INJURY_CATEGORIES在兩份未改動過的執行檔上重現,同樣 27 條、同樣偏移,排除模組污染。

它不是抽象的傷病資料庫,是「傷是怎麼來的」清單:被界外球打到、被觸身球打到 (再細分頭、左右大腿、左右前臂、左右上臂、軀幹八種)、 投手被強襲球打到(再細分頭、腳、腿、手、臂、軀幹六種)、撞牆接球、飛撲接球、 靠近休息區、靠近全壘打牆、本壘觸殺、雙殺傳球、投手體力、 頭部先滑壘與腳先滑壘分開計,以及 INJURY_BATTER_PITCHER_BRAWL

兩件事因此成立:受傷是事件驅動的,不是隨機骰——每種傷都得先有對應動作發生; 而清場衝突在引擎裡是會產生後果的完整事件,不只是動畫。

投打之間的節奏,四種對局各調一組

四組 × 五個參數。組名是 UserUser_Pitching_BattingUserCPU_CPUUser_CPUCPU_, 而每組五個成員的前綴剛好是 UU / UC / CU / CC —— 前綴縮寫對上組名全稱,是名字對名字。

五個成員是:RunnerIncreasesLeadNewBatter_WithRunnersNewBatter_NoRunnersSameBatter_WithRunnersSameBatter_NoRunners。也就是說,停頓長度會依「換不換打者」 和「壘上有沒有人」再細分

共用的範圍字串 0, 5, 0.01 在整支執行檔只出現一次, 卻被引用整整 20 次(4 組 × 5 個),每次都固定落在對應參數名引用的前 9 個位元組。 結構是量到的,不是鄰接推的。

⚠️ 「單位是秒」是猜的,而且很可能不對。 二進位裡沒有任何字串寫 time 或 delay, 而真實投球間隔是 15 到 20 秒,0 到 5 不可能是完整間隔 —— 頂多是附加延遲或倍率。

連線對戰:交換的是搖桿輸入,抓包的是 CRC

除錯訊息把列舉前綴直接印出來:(%d) kTPGameComm_ReadPadInfoWait - client / - server名字對名字。 旁邊的除錯旋鈕叫 Input Exchange TicksMax Pad Skew —— 那正是延遲鎖步用來吸收延遲的兩個概念:提前幾個 tick 交換輸入、允許雙方偏移多少。

連線角色寫成 I'm the HOST!! / I'm the GUEST!!, 除錯選單裡甚至有一顆 Create Desync(故意製造不同步) 跟一顆 Force recovery

而這功能真的有出貨HOSTLANEASPORTSONLINENETWORKGAMEScFENetworkStatus 都在, 殺掉了「這只是主機版殘留程式碼」這個平凡解釋。 鄰居也全是網路項目:語音的連線/斷線/開關麥克風、假斷線、主客端磁碟錯誤。

⚠️ 覆驗砍掉的四句話

資產管理員用一個直線符號同時表達兩件事

執行檔裡有一條寫死的 models.big|c%03d.%s,還有一條通用樣板 %s|%s| 左邊是磁碟上的路徑(可以有目錄),右邊是封裝檔內部的成員名。

這解釋了為什麼 %s.ord%s/%s.ord 會同時存在: 帶斜線的是給散裝檔用的,不帶斜線的是要放到 | 右邊的

驗證:把這台測試機 data 資料夾底下每一個封裝檔(開頭是 BIGF 的 383 個) 的目錄表全部列出來,共 71,081 筆含路徑分隔符號的是 0 筆 —— 跟「| 右邊是扁平名字」一致。 而 models.big 的 4,211 筆裡有 2,682 筆是 c001.fsh/.ord/.orl 這種三件套, 左邊檔名對上右邊成員名。

⚠️ 這個「0 筆」只成立在第一層。 下面「資產是以庫為單位整批載入卸載的」那一節挖開的第二層裡, initstay.big 的 34 個成員與 initpurge.big 的 11 個成員 全部帶斜線models/bat.omodels/textures/cglv.fsh…), 合計 45 個反例。所以只能說「本站量到的第一層目錄項目沒有分隔符號」, 不能說「| 右邊永遠是扁平名字」。

(2026-09-05 訂正:這一段原本寫 71,105 筆,而同一頁後面那張卡又寫 387 個封裝檔、 71,202 個項目 —— 兩個口徑互不相容,而且兩個都重跑不出來。現在統一成當下量得到的值: 這台測試機 data 底下 383 個 BIGF 封裝檔、71,081 筆目錄項目。 這是這台機器的數字,資料夾裡多放一份備份就會變;定性結論不受影響。)

資產是以「庫」為單位整批載入卸載的

____________________ LOADING BANK %s ____________________
____________________ UN-LOADING BANK %s ____________________
banklist

而磁碟上剛好有兩個名字對得上這個概念的檔:

外層裡面唯一那一項大小怎麼讀
initstaz.biginitstay.big 1,212,663init + stay(留著不卸載)
initprgz.biginitpurge.big 106,971init + purge(可以清掉)

外層是封裝檔的殼,裡面只裝一個同名但拼完整、而且壓縮過的封裝檔。 所以結尾那個 z 就是「壓縮過」。 內層檔名把外層的縮寫展開了 —— 這是名字對名字,不是猜的。

解開 initstay.big 有 34 項,全是最基本、永遠要在的東西: 球、球棒、斷棒、影子、碰撞身體、三種細節等級的頭與身體、 手套(fglv 野手、cglv 捕手)、球棒與皮膚貼圖。 initpurge.big 11 項,全是可替換的模型外殼。

順便解開一個社群工具的謎

一份社群工具(glovebatUTILITY,壓縮檔裡的說明檔標 2005-05-06)的說明書 說手套貼圖在 inistay.big 裡。 這台機器上沒有那個檔,而且把 data 底下 383 個封裝檔、 71,081 個項目掃過,fglvcglv 都是 0 筆。

真相是檔名少打了一個字母,而且它們藏在第二層: 實際的檔名叫 initstaz.big,要先解開外層才看得到裡面壓縮著的 initstay.big只掃一層的話,那 34 個檔案在統計上等於不存在。

三個球員被寫死在執行檔裡

initstay.big 的 34 項裡,有四張貼圖是指名道姓的:

檔名是什麼
models/textures/gagnegoggleslens.fsh護目鏡的鏡片
models/textures/gagnegogglesframe.fsh護目鏡的鏡框(分成兩張)
models/textures/johndamonhair.fsh某位球員的頭髮
models/textures/mannyramirezhair.fsh某位球員的頭髮

其他球員的外觀全部走通用系統(臉皮代號),只有這三個人的特徵被單獨做成資產、 而且放進「永遠不卸載」那一庫

難度是主客各一份

fesubscreendifficulty.cpp 附近有四條元素路徑,兩兩成對, 主隊一組、客隊一組,各自帶自己的隊徽節點與 DIFFICULTY 節點。

覆驗用了一個完全不碰執行檔的第二證據:直接去讀版面檔 fes_teamselect.fel,裡面 GG:HOMEDIFFICULTYGG:AWAYDIFFICULTY 各自內含一份四選項選單, 對應到文字表的四個字串(新秀/職業/全明星/MVP)。

路徑寫法也順帶印證本站對 版面檔的解讀: SG 是子畫面群組、GR 是群組、ANIM… 是動畫節點,層級用斜線串起來。

王朝模式的「氣勢」是一整張可調表

MinMomentum / MaxMomentum / InitialMomentum, 加上六個對稱參數:親自打、模擬中途介入、整場模擬三種贏法各有自己的氣勢影響值, 輸也各有一份。

那三分法正是王朝模式解決一場比賽的三條路,各自對氣勢的影響是分開調的。 表裡還有曲線與分級查表的開關。

覆驗把證據從「執行檔字串排列」升級到總設定檔裡一筆真實記錄: 名稱表與數值表相鄰兩行,對起來就拿到原廠實際數值。

自訂球場改圍牆高度,真的會換掉碰撞面

把 87 個球場的碰撞檔全部掃過,1,368 個群組裡帶標籤的 62 個 全部集中在兩個檔(每檔 31 個:Seat 15、Scoreboard 8、 Restaurant 4、WallHeight 4), 而那兩個檔是同一座球場的日、夜兩版 —— 正好就是可自訂的那一座。 (2026-09-05 訂正:原本寫「只有兩個用了標籤」,那個「兩個」數的是檔案不是群組, 跟前半句的單位對不起來;帶標籤的群組是 62 個。結論不變。)

那座球場的碰撞檔裡,WallHeight,0WallHeight,3 各有自己的三角形;擋球面那一份也是四組各自獨立。 四種圍牆高度不是換模型而已,碰撞幾何是四份。 實務意義:自訂球場改圍牆高度會改變全壘打判定,不是視覺效果。

⚠️ 覆驗說:結論可以留,但原本舉的那個理由其實是最弱的一條,已換成上面這個寫法。

老實說:48 條主張,29 條被自己人打掉

這一輪把整份執行檔分六個區塊逐一開採,每一條發現都另外跑一輪 專門去推翻它,同時回頭查站上既有的頁面是不是早就寫過。

結果條數意思
站得住且是新的13寫在上面
被推翻29約六成
站上早就寫過6包括編譯器版本、動畫三件套、資產編號樣板

最值得記的一條「差一點」

有一條主張寫「一個腳本 63 個欄位、剛好 13 條演員軌」, 而資料檔表頭寫的就是 63 13。看起來完美。

覆驗去量了 datafile.big 的 460 個成員:帶著這型(63 欄位)表頭的有 325 個檔那個 13 是「這個檔裡這型記錄有幾筆」, 跨檔取值 1 到 93,325/325 精準吻合。而演員軌永遠是 13。 兩個 13 毫無關係。 (2026-09-05 訂正:原本寫「量了 437 個檔、419/437 精準吻合」, 這兩個數字重跑不出來,也說不清當時掃的是哪一批檔;現在換成寫得出範圍、 重跑得出來的值。「跨檔取值 1 到 93」與整段結論不變。)

13 條演員軌本身是對的 —— 但靠的是 Actor1Actor13 這些名字,不是那個數字。這正是本站列為最弱一級的「數量剛好對得上」。

另外三個方法論等級的教訓

這一頁不能告訴你什麼

📌 重點整理

接下來