速查 › 執行檔裡剩下的原始碼痕跡
執行檔裡剩下的原始碼痕跡
這個遊戲的零售執行檔裡,留著 68 條原始碼檔案路徑
(EA 自己的 64 條,微軟 C 執行期函式庫 4 條),
其中 62 條長成 e:\mvp2005pc\source\common\… 的樣子。
而真正有價值的不是那些路徑,是緊貼著它們的一整排內部名稱表 ——
那是這個遊戲的內部構造,用它自己的詞彙寫的。
為什麼原始碼路徑會留在出貨的程式裡
C 和 C++ 有一個叫 __FILE__ 的東西,編譯時會被換成當前檔案的完整路徑。
它最常見的用途是斷言:「這裡不該發生的事發生了,出事的是哪一個檔、哪一行」。
斷言訊息要印路徑,路徑就得存進二進位檔。於是每留下一條路徑, 旁邊就有一句 EA 工程師寫給自己看的話。
整個遊戲的架構,用檔案數量說話
| 子系統 | 檔數 | 裡面是什麼 |
|---|---|---|
ai/ | 22 | 打者、投手、野手、跑者、球員本體的判斷邏輯 |
geomlib/ | 21 | 幾何碰撞:圓錐、方盒、射線、球體、網格、骨架,以及它們兩兩相交的測試 |
script/ | 8 | 播放腳本:simtoendofplay、teleportactor、gosubrandom |
frontend/ | 5 | 介面與疊層 |
assetmanager/ database/ controller/
comm/ main/ interfaces/ |
各 1 | 資產、資料表、手把、連線、進入點、投打介面 |
libraries/gamelib/ | 2 | 離線表管理員與路徑清單,那是跨專案共用的函式庫 |
| (編譯器自己的) | 4 | f:\vs70builds\3077\vc\crtbld\ —— 微軟 C 執行期函式庫的原始碼 |
AI 跟幾何碰撞加起來佔了 63%。一個 2005 年的棒球遊戲, 大半的工程量花在「電腦球員要做什麼」跟「兩個東西有沒有碰到」。
⭐ 方法:從「字串挨在一起」升級到「指標真的指過去」
這一頁最重要的不是任何一條結論,是驗證方式的轉變。
舊方法為什麼不夠
看到一排名字連在一起、數量又剛好對得上某個東西,很容易就寫成「這是一張表」。 但字串挨在一起只證明編譯器把它們放在一起,不證明它們屬於同一張表。
本站把「數量剛好對得上」列為最弱的一級證據,正是為了防這個。
新方法:去找那張表本身
C 語言把「編號 → 名字」做成一個指標陣列:一格一個位址,每個位址指向一條字串。 那個陣列是真的存在於檔案裡的資料,不是推論。
做法(不反組譯,只讀資料):
- 解 PE 檔頭,把字串的檔案偏移換算成程式執行時的位址
- 全檔掃描,找哪些四位元組剛好等於那些位址
- 如果它們連續排在一起、前後被別的東西夾住 —— 那就是那張表,而且格數是數出來的
用這個方法,好幾條原本只能標「合理但沒證實」的發現, 變成格數、順序、起點全部量得出來。
量出來的內部構造
守備員的行為是一張 50 格的任務表
指標陣列在檔案偏移 0x4FDB30,50 格,
順序正好是字串區塊的完全倒序,前後被浮點常數夾住,
而那 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 這種「刻意不做事」也是一項任務。
⚠️ 一開始還寫了「CatchAThrow 跟 CatchAThrowNew 並存,
代表舊程式碼沒刪、兩套同時活著」——覆驗把這句退回成猜測:
列舉裡有兩個名字不等於有兩條活的程式碼,New 也可能只是別名。
旁邊那張疑似派送表是 48 格不是 50 格,兩種對齊方式都說得通,
覆驗者拒絕挑「讀起來比較順」的那一種。
受傷有 27 種,而且每一種都綁一個具體的比賽動作
指標陣列在 0x4fe7a8,28 格:第 0 到 26 格是分類,
第 27 格是計數哨兵 NUM_INJURY_CATEGORIES。
在兩份未改動過的執行檔上重現,同樣 27 條、同樣偏移,排除模組污染。
它不是抽象的傷病資料庫,是「傷是怎麼來的」清單:被界外球打到、被觸身球打到
(再細分頭、左右大腿、左右前臂、左右上臂、軀幹八種)、
投手被強襲球打到(再細分頭、腳、腿、手、臂、軀幹六種)、撞牆接球、飛撲接球、
靠近休息區、靠近全壘打牆、本壘觸殺、雙殺傳球、投手體力、
頭部先滑壘與腳先滑壘分開計,以及 INJURY_BATTER_PITCHER_BRAWL。
兩件事因此成立:受傷是事件驅動的,不是隨機骰——每種傷都得先有對應動作發生; 而清場衝突在引擎裡是會產生後果的完整事件,不只是動畫。
投打之間的節奏,四種對局各調一組
四組 × 五個參數。組名是 UserUser_Pitching_Batting、
UserCPU_、CPUUser_、CPUCPU_,
而每組五個成員的前綴剛好是 UU / UC / CU / CC ——
前綴縮寫對上組名全稱,是名字對名字。
五個成員是:RunnerIncreasesLead、NewBatter_WithRunners、
NewBatter_NoRunners、SameBatter_WithRunners、
SameBatter_NoRunners。也就是說,停頓長度會依「換不換打者」
和「壘上有沒有人」再細分。
共用的範圍字串 0, 5, 0.01 在整支執行檔只出現一次,
卻被引用整整 20 次(4 組 × 5 個),每次都固定落在對應參數名引用的前 9 個位元組。
結構是量到的,不是鄰接推的。
⚠️ 「單位是秒」是猜的,而且很可能不對。 二進位裡沒有任何字串寫 time 或 delay, 而真實投球間隔是 15 到 20 秒,0 到 5 不可能是完整間隔 —— 頂多是附加延遲或倍率。
連線對戰:交換的是搖桿輸入,抓包的是 CRC
除錯訊息把列舉前綴直接印出來:(%d) kTPGameComm_ReadPadInfoWait - client /
- server,名字對名字。
旁邊的除錯旋鈕叫 Input Exchange Ticks 與 Max Pad Skew ——
那正是延遲鎖步用來吸收延遲的兩個概念:提前幾個 tick 交換輸入、允許雙方偏移多少。
連線角色寫成 I'm the HOST!! / I'm the GUEST!!,
除錯選單裡甚至有一顆 Create Desync(故意製造不同步)
跟一顆 Force recovery。
而這功能真的有出貨:HOSTLAN、EASPORTSONLINE、
NETWORKGAMES、cFENetworkStatus 都在,
殺掉了「這只是主機版殘留程式碼」這個平凡解釋。
鄰居也全是網路項目:語音的連線/斷線/開關麥克風、假斷線、主客端磁碟錯誤。
⚠️ 覆驗砍掉的四句話
- 「傳的不是比賽狀態,是按鍵」講太絕。同一台狀態機裡就有
WriteBackEnd/ReadBackEnd,而 BackEnd 有ingamedata\backend.cpp佐證是賽中資料庫(名單/打線/成績)。 握手階段確實在傳資料。 - 那串箭頭不是執行順序,只是列舉的宣告序。 而且兩個字串池從第六項就分岔,只有前五項互相印證。
local: %d rem: %d解讀成「本地與遠端搖桿快取索引」是猜的: 整支執行檔remote一次都沒出現,rem也可能是 remaining。- 「這解釋了為什麼連線對戰對名單一致性極度敏感」零證據,已整句刪除—— 而且 BackEnd 交換這一步剛好反過來削弱它。
資產管理員用一個直線符號同時表達兩件事
執行檔裡有一條寫死的 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.o、models/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.big | initstay.big |
1,212,663 | init + stay(留著不卸載) |
initprgz.big | initpurge.big |
106,971 | init + purge(可以清掉) |
外層是封裝檔的殼,裡面只裝一個同名但拼完整、而且壓縮過的封裝檔。
所以結尾那個 z 就是「壓縮過」。
內層檔名把外層的縮寫展開了 —— 這是名字對名字,不是猜的。
解開 initstay.big 有 34 項,全是最基本、永遠要在的東西:
球、球棒、斷棒、影子、碰撞身體、三種細節等級的頭與身體、
手套(fglv 野手、cglv 捕手)、球棒與皮膚貼圖。
initpurge.big 11 項,全是可替換的模型外殼。
順便解開一個社群工具的謎
一份社群工具(glovebatUTILITY,壓縮檔裡的說明檔標 2005-05-06)的說明書
說手套貼圖在 inistay.big 裡。
這台機器上沒有那個檔,而且把 data 底下 383 個封裝檔、
71,081 個項目掃過,fglv 與 cglv 都是 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:HOMEDIFFICULTY 與
GG: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,0 到 WallHeight,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 條演員軌本身是對的 —— 但靠的是 Actor1 到 Actor13
這些名字,不是那個數字。這正是本站列為最弱一級的「數量剛好對得上」。
另外三個方法論等級的教訓
strings會把相鄰的二進位位元組黏進字串開頭。 有一條名字被讀成D2OUTFLYBALL,實際的名字是2OUTFLYBALL(兩出局高飛球)—— 那個D是前面一個浮點常數的位元組。同樣的錯讓兩個偏移量各多算一位元組。- 純文字搜尋看不進壓縮過的資料。 有一條差點寫成「這些是遊戲根本不讀的孤兒資料」,因為在目錄表裡 0 命中 —— 實際上那 460 個項目全部是壓縮的。
- 名字表沒人引用,不等於功能不存在。 跑者 AI 那五張名字表在零售版裡沒有任何一個指標指向它們(對照組: 隔壁的浮點常數區有 109 次引用,證明搜法有效)。 所以「引擎保留了瞬移跑者的能力」只能降級成「保留了這個名字」。
這一頁不能告訴你什麼
- 本站不反組譯。以上全部是讀資料段得到的:字串、指標陣列、 以及它們跟磁碟上檔案的對應。凡是需要看程式碼才能確定的,一律標成推論或猜測。
- 「某條路徑屬於某個 .cpp」多半是鄰接推論。
編譯器把字串放在一起不代表它們屬於同一個模組,
只有在名字本身能互相印證時(
liveplaytasks對上 Task)才算錨點。 - 這些表都是唯讀知識,不是修改教學。 本站沒有提供改動它們的工具,也沒有驗證過改了會怎樣。
📌 重點整理
- 一句話 零售執行檔裡留著 68 條原始碼路徑(EA 自己的 64 條,微軟 C 執行期函式庫 4 條),但真正有價值的不是路徑,是緊貼著它們的內部名稱表,那是這個遊戲的構造用它自己的詞彙寫的。
- 用檔案數量看架構
ai/22 個、geomlib/21 個,兩者加起來佔 63%。一個 2005 年的棒球遊戲,大半工程量花在「電腦球員要做什麼」跟「兩個東西有沒有碰到」。 - 這頁最重要的不是結論,是方法換了 從「字串挨在一起」升級到「指標真的指過去」。C 語言把編號對名字做成指標陣列,那是真的存在於檔案裡的資料,格數是數出來的。全程不反組譯,只讀資料段。
- 量出來的兩張表 守備員的行為是
0x4FDB30的 50 格任務表,最後 8 格全部以UserControl開頭,往後兩千個位元組之內同一段字串池裡就排著fielderuser.cpp與liveplaytasksuser.cpp,名字對上名字。受傷是0x4fe7a8的 28 格(27 種分類加一個計數哨兵),在兩份未改動過的執行檔上重現。每一種傷都綁一個具體動作,所以受傷是事件驅動的,不是隨機骰。 - 三個球員被寫死在執行檔裡 Gagne 護目鏡的鏡片與鏡框、Damon 的頭髮、Manny Ramirez 的頭髮,四張貼圖被單獨做成資產,還放進「永遠不卸載」那一庫。另外自訂球場改圍牆高度真的會換掉碰撞面:87 個球場 1,368 個群組裡,帶標籤的 62 個全部集中在兩個檔,正好是可自訂的那一座的日夜兩版,四種高度是四份碰撞幾何,所以那不是視覺效果,會改變全壘打判定。
- 48 條主張,29 條被自己人打掉 約六成,站得住且是新的只有 13 條。最值得記的一條「差一點」是資料檔表頭寫著
63 13、腳本剛好 13 條演員軌,看起來完美,覆驗去量datafile.big裡帶這型表頭的 325 個檔才發現那個 13 是「這個檔裡這型記錄有幾筆」,跨檔取值 1 到 93、325/325 精準吻合。兩個 13 毫無關係。 - 「搜不到」不等於「不存在」 一份 2005 年的社群工具說手套貼圖在
inistay.big,這台機器把data底下 383 個封裝檔、71,081 個項目掃過都是 0 筆。真相是檔名少打一個字母,而且它藏在第二層:initstaz.big裡壓縮著initstay.big,只掃一層那 34 個檔在統計上等於不存在。同一類的還有「純文字搜尋看不進壓縮資料」跟「strings會把前面的位元組黏進字串開頭」(D2OUTFLYBALL其實是2OUTFLYBALL)。 - 本站沒驗的 投打節奏那五個參數「單位是秒」是猜的,而且很可能不對,二進位裡沒有任何字串寫 time 或 delay。跑者 AI 那五張名字表在零售版沒有任何指標指向,所以「引擎保留了瞬移跑者的能力」只能降級成「保留了這個名字」。「某條路徑屬於某個 .cpp」多半是鄰接推論。本站不反組譯,這些表都是唯讀知識,沒有提供改動的工具,也沒有驗證過改了會怎樣。
接下來
- 二十年工具清冊 —— 社群那一半的故事
- 總設定檔 datafile —— 氣勢那張表的數值住在這裡
- FEL 版面語法 —— 難度那條路徑怎麼讀
- 封裝檔可以拆成散裝嗎 ——
|那個符號正好回答了這一課的問題 - 本站怎麼量的