教學 › 問題排除

遊戲當掉了怎麼辦

裝了模組之後遊戲開不起來、或玩到一半跳回桌面 —— 這是這款遊戲最常見的問題。這頁教你先判斷是哪一種當機, 再對症下藥,而不是亂試一通。

難度

★☆☆ 只是檢查

時間

約 5 分鐘

會改到檔案嗎

不會,全程唯讀

🛟 動手之前:先完整備份整個遊戲資料夾

把整個遊戲資料夾複製一份到別的地方包括裡面存放紀錄的資料夾。不要挑檔案,整包複製最省事也最保險。

「紀錄」是指這些(它們跟遊戲檔混在同一個資料夾裡):

dir /s /b "〔你的遊戲資料夾〕\*.sav"
find "〔你的遊戲資料夾〕" -name "*.sav"

上面第一行 Windows、第二行 Mac。 本站每一支工具都會自己備份它要動的那一個檔, 但那只保得住那一個檔 —— 整包備份保的是「你玩到現在的一切」。 完整做法看備份與還原 SOP

⚡ 只想趕快做完?照這五步

  1. 下載這頁的診斷腳本,從「下載」資料夾拖到桌面,開命令提示字元切到桌面。
    cd %USERPROFILE%\Desktop
  2. 打下面這行,結尾留一個空格,再把遊戲安裝資料夾拖進來(不是 data 資料夾,是它的上一層),然後按 Enter。
    python mvp_check_setup.py 
  3. 看【1】:印「⚠ 只有 2 GB(未開啟大位址支援)」而你又裝了不少模組,這是候選原因之一 —— 但本站量到成因的那次跳出(中文版配大球場)卡的不是這道 2 GB 的牆,是遊戲自己切的那塊 64 MB 記憶體池,單獨開大位址旗標照樣跳出(見執行檔上的六個旋鈕)。再看【2】列出最大的幾個檔。
  4. 對照症狀挑一類:裝了很多模組之後才開始當看 ① 記憶體、換了某個模組立刻當看 ② 模組、改完檔案打不開看 ③ 自己改壞、一直都會當看 ④ 系統設定。
  5. 照本站教學改壞的,一行還原;手動改的就把當初的 .bak 改名回原檔名。
    python mvp_hide_runner_speed.py "…\ingame.big" --restore

Mac 把 python 換成 python3。診斷腳本全程唯讀,不會修改任何檔案。 想知道 2 GB 上限是怎麼回事、為什麼要先驗下載的那包完不完整,往下讀; 只想複習,跳到📌 重點整理

先跑診斷

與其猜,不如先看一眼實際狀況。這支腳本會讀出三件事: 執行檔能用多少記憶體、遊戲資料量多大、關鍵檔案在不在。

⬇ 下載診斷腳本 mvp_check_setup.py

零相依、全程唯讀不修改任何檔案、附完整原始碼。需要 Python 3.7 以上(見第 0 課)。

⚠️ 按了下載之後,檔案多半在「下載」資料夾不是桌面 —— 開檔案總管左邊點「下載」,把那個 .py 拖到桌面再往下做。 沒做的話會看到 can't open file

a把腳本放到桌面,開命令提示字元切到桌面(cd %USERPROFILE%\Desktop)。

b打下面這段,然後把遊戲安裝資料夾拖進來(不是 data 資料夾,是它的上一層):

python mvp_check_setup.py 
你會看到類似這樣
【1】執行檔能用多少記憶體
  檔案  mvp2005.exe(6.6 MB)
  類型  32 位元 程式
  上限  ⚠ 只有 2 GB(未開啟大位址支援)
        這是當機的候選原因之一。本站在自己那顆執行檔上量到的是:
        只掛大位址旗標沒有感覺,卡住的是遊戲自己切的那塊記憶體池。

【2】遊戲資料量
  總量      1.3 GB(902 個檔案)
  封裝檔    224 個 .big
  最大的幾個:
      202.4 MB  audio/cd/spch_pbp/pbpdat.big
      165.0 MB  models.big

【3】關鍵檔案
  ✅ data/database/attrib.dat          球員屬性
  ✅ data/frontend/ingame.big          比賽中介面

⚠️ 2026-09-05 訂正:這一段原本是拼出來的 —— 它把本站測試機的檔案大小配上原版才會出現的警告, 世界上沒有一台機器會印出那份輸出。現在換成 在剛安裝好的官方英文版上實際跑出來的那一份(節錄)。 你自己的數字會不一樣:裝過模組的機器檔案更多更大, 執行檔被動過大位址旗標的話【1】會印「✅ 最多 4 GB」。

出錯了?對照這張表

畫面上的訊息原因你要做的
can't open file '…\mvp_check_setup.py' 檔案還在「下載」資料夾,不在桌面 開檔案總管左邊點「下載」,把那個 .py 拖到桌面,再跑一次。 這是本站最常見的第一個卡點。
印出一整段說明就停住,沒有【1】 指令後面沒有接資料夾(結束碼 1 打完 python mvp_check_setup.py 結尾留一個空格, 再把遊戲安裝資料夾拖進來
✗ 找不到這個資料夾 路徑打錯,或路徑有空格卻沒加引號(結束碼 1 用拖曳取得路徑(第 0 課操作二
✗ 這個資料夾裡沒有 .exe
✗ 找不到 data 資料夾
選到上一層或下一層了 同時看得到 datamvp2005.exe 的那一層
✗ 讀取途中出錯 掃描到一半檔案被移走或鎖住:雲端同步、防毒掃描, 或外接碟中途斷線(結束碼 2 關掉那些程式、確認碟還在,再跑一次。 這支全程唯讀,中斷不會留下半截檔案
已中止。本次檢查全程唯讀 你按了 Ctrl+C(結束碼 130 什麼都沒動到,想重跑就直接再跑一次

對照你的當機症狀

症狀最可能的原因往下看
裝了很多模組之後開始當
(尤其大型球場包)
記憶體撞到上限① 記憶體
換了某個模組之後立刻那個模組本身有問題,或裝錯位置② 模組
改完檔案之後打不開檔案寫壞了③ 檔案
一直都會當,跟模組無關顯示設定或系統相容性④ 系統

① 記憶體撞到上限

這是模組玩家最常被提起的一種。原因是個二十年前的技術限制:

32 位元程式預設只能用 2 GB 記憶體

MVP Baseball 2005 是 32 位元程式。這類程式在預設情況下, 無論你的電腦有多少記憶體,它最多只能用 2 GB

原版遊戲用不到這個量,但當你換上高解析度球場、大量球員臉皮、 高音質音檔之後,就有機會撞到天花板 —— 然後遊戲直接消失。

怎麼知道自己有沒有中

診斷腳本的【1】會告訴你。有兩種結果:

腳本顯示意思
✅ 最多 4 GB(已開啟大位址支援)這道 2 GB 的牆你的執行檔已經處理過了。但記憶體還有第二層天花板 —— 遊戲自己切的那塊 64 MB 記憶體池,旗標開了它也不會變大(見中文版配大球場就跳出)。那一層也排除之後,再往 ② ③ ④ 找
⚠ 只有 2 GB(未開啟大位址支援)如果你裝了不少模組,這是候選原因之一 —— 但本站量到成因的那次跳出(中文版配大球場)卡的不是它,是遊戲自己切的那塊 64 MB 記憶體池

技術上這是程式檔頭裡一個叫「大位址感知」的旗標。腳本只是把它讀出來,沒有動它。

可以怎麼處理

不動執行檔的做法(先試這個):

調整執行檔的做法: 模組社群長年使用一種小工具,把那個旗標打開,讓遊戲可以用到 4 GB。 這類工具由第三方開發,本站不提供也不轉載, 請自行尋找並評估來源;動手前務必先備份執行檔

⚠️ 2026-09-05 訂正:這一段本來只寫到上面為止,那樣寫有兩個問題

一、不必去外面找那支工具。本站後來自己寫了一支零相依的腳本做同一件事 (mvp_modernize.py --4gb),預設唯讀、要加 --apply 才會寫、 會自己備份、隨時可以 --restore 退回去,下載與完整原始碼在 讓它在 2026 年跑起來那一課。

二、單獨開這個旗標不見得有感覺。執行檔上的六個旋鈕那一課量到的是: 只掛大位址旗標,中文版配大球場照樣跳出 —— 真正卡住的是遊戲自己跟系統要的那塊 64 MB 記憶體池, 那是另一層天花板,旗標開了它也不會變大。 對照實驗與怎麼把池子放大到 128 MB 見 中文版配大球場就跳出

說清楚:這跟破解無關

調整的是「這個程式最多能用多少記憶體」,屬於相容性設定, 與任何防盜拷或授權驗證機制無關。 本站不提供、不教學、也不討論任何規避保護措施的方法。

⚠️ 2026-08-28 訂正:本頁的診斷腳本有一個從來不會亮的檢查

這支腳本原本會提醒你「有幾個球場檔超過 80 MB」。 那個數字沒有依據 —— 它來自本站另一份內部筆記裡的一句推算, 而那句推算本身也沒有實測。

本站把手上所有球場檔量了一遍:

來源檔數最大中位數
剛安裝好的官方英文版873.99 MB3.24 MB
剛安裝好的官方繁體中文版873.99 MB3.24 MB
2023 那包社群球場8715.14 MB7.57 MB
標「10MB」的那包(最肥的一份)8318.44 MB4.73 MB
全部來源合計69218.44 MB

692 個檔裡最大的是 18.44 MB,而門檻寫 80 MB —— 是它的 4.3 倍。 所以這個檢查從來沒有亮過,而且不可能亮。 比沒有這個檢查更糟的是:它會讓你以為「檢查過了,球場沒問題」。

現在改成兩件事:直接印出最大的那個球場檔,並附上原版的基準(3.99 MB), 超過 10 MB 才提醒 —— 而且明說 10 是社群傳了二十年的說法,不是本站量到的邊界, 措辭是「值得清一下」不是「這會當機」。真正該做的是 清掉沒人指向的位元組

⭐ 這個錯的形狀值得記:一個永遠不會觸發的檢查,跟沒有檢查不一樣 —— 它會發出「安全」的訊號。寫門檻之前要先量一次真實資料的分布, 確認門檻落在資料範圍之內

①-b 你下載的那包根本沒下載完

裝模組裝到一半出事的時候,第一個該懷疑的不是遊戲,是那個壓縮檔。 本站手上有一批 2010 年的社群大頭照包,18 個檔。跑兩種檢查:

檢查一:大小對不對檢查二:內容解得開嗎
其中一包❌ 只有應有大小的 24.4%❌ 一個檔都取不出來
另一包✅ 精確吻合❌ 49 個檔裡 13 個資料損毀
其餘 16 包

兩種完全不同的壞法,而且第二種靠檔案大小看不出來。 第二包的大小跟它自己宣告的一個位元組都不差, 解開之後才發現有 13 張圖是壞的 —— 如果你只裝了它然後遊戲當掉,很容易怪到遊戲或別的模組頭上。

怎麼自己檢查(兩件事都要做)

一、大小對不對。大部分解壓縮軟體都有「測試」功能, 在檔案上按右鍵就找得到。它會告訴你檔案有沒有缺一截。

二、內容解得開嗎。同一個「測試」功能也會逐檔驗一次, 壞掉的會被點名。大小過了不代表內容過了,一定要看它跑完說什麼。

兩個都過了才動手裝。沒過的話重新下載那一包, 不要試圖「先裝再說」—— 半個模組進到遊戲裡, 之後很難分辨當機是哪一步造成的。

本站不提供任何模組下載,也不指名任何下載站。 這一節講的是拿到檔案之後怎麼確認它是完整的

為什麼這一節排在這麼前面

因為它是最便宜的檢查:花三十秒, 就能排除掉一整類「怎麼查都查不出原因」的當機。

而且那包缺了四分之三的檔案是 2010 年沒下載完的, 在資料夾裡躺了十幾年 —— 沒有人發現,因為沒有人檢查過檔案不會自己告訴你它壞了,你要去問它。

② 模組造成的當機

②-b 語音檔頭與聲音對不上

播報語音是成對的:一個檔頭(.hdr)配一個聲音(.dat)。 這一對在剛安裝好的原版裡是完美的一比一

檔頭聲音 有檔頭沒聲音有聲音沒檔頭
剛安裝好的英文版2,106 / 4,5802,106 / 4,580 00
剛安裝好的中文版2,106 / 4,5802,106 / 4,580 00
一台裝過台灣模組的機器3,637 / 12,4593,658 / 12,528 12102

原版兩個方向都是零,所以只要出現孤兒,就一定是後來裝的東西造成的。 這也是這條檢查能當診斷用的前提 —— 基準線是乾淨的

兩種孤兒,嚴重程度不一樣

未解 本站沒有實測「有檔頭沒聲音」 會不會真的讓遊戲跳出 —— 那要在一台會重現的機器上試。 這裡能確定的是:原版沒有這種東西

③ 自己改壞了

如果是照本站教學改檔案之後出問題,每篇教學都有一行指令還原

python mvp_hide_runner_speed.py "…\ingame.big" --restore

手動改的話,把當初的 .bak 備份檔改名回原檔名即可。診斷腳本的【4】會告訴你它找到幾個備份檔,但路徑最多只列 8 個,其餘收成一行 …(還有 N 個)

最常見的改壞方式

用「整包重新打包」的方式存回 .big。這會讓檔案大幅縮水而且不會報錯, 等你發現時已經覆蓋了。原因與正確做法見 BIGF 格式的 append 鐵律

判斷方法很簡單:存回後的 .big 檔案大小不應該變小。變小很多就是中了。

④ 系統與顯示設定

跟模組無關、原版就會當的話,通常是這款 2005 年遊戲跟現代系統的相容性問題:

📋 還是解決不了?照這個格式回報

把診斷腳本的輸出整段貼上,比只說「會當機」有用得多:

作業系統:Windows 11 / Windows 10 / 其他 ___
當機時機:開遊戲就當 / 進比賽當 / 玩一陣子才當 / 特定畫面當
最近改過什麼:___
診斷腳本輸出:(整段貼上,尤其是【1】和【2】)
裝了哪些模組:___
原版(未裝模組)會不會當:會 / 不會 / 沒試過

四個有名有姓的雷

上面四大類是從症狀分類。這一節不一樣 —— 這是本站十幾年下來實際踩過、有具體成因的四個。 如果你的狀況對得上,可以直接跳過診斷。

① 遊戲資料夾路徑不可以有中文

症狀:遊戲載入資源時直接掛掉,或根本開不起來。

成因:這是 2005 年的 32 位元程式,處理檔案路徑時用的是舊的編碼方式。 中文路徑在非中文系統環境下會變成亂碼,程式就找不到檔案。

解法:把遊戲裝在純英文路徑,例如 C:\Games\MVP2005\資料夾名稱裡也不要有空格比較保險。

這一條也適用於你放模組的資料夾。

② 大容量硬碟存不了球員名冊

症狀:改完名冊存檔失敗,或存了之後遊戲讀不到。

成因推測 本站沒有查出確切原因。可能跟大容量硬碟的磁區大小跟 2005 年的程式預期不同有關, 但本站沒有實測驗證過這個解釋

解法:把遊戲裝在 SSD 或容量比較小的硬碟上。 這是實際有效的做法,即使成因還沒查清楚。

誠實講:「換個硬碟就好了」是經驗,不是解釋。本站不把經驗寫成機制。

③ 王朝模式玩久了跳出

症狀:單場比賽沒問題,王朝模式跑幾個球季之後開始當。

成因:跟上面 「① 記憶體撞到上限」是同一件事 —— 32 位元程式有記憶體上限,賽季資料越積越多就會撞到。

解法:那個旗標本站自己有腳本 (mvp_modernize.py --4gb,零相依、原始碼全公開、可 --restore 還原, 在讓它在 2026 年跑起來那一課), 不必去外面找來源不明的工具。動手前務必先備份執行檔。

⚠️ 2026-09-05 訂正:這一格原本只寫「找第三方工具把旗標打開」。 但執行檔上的六個旋鈕那一課量到的是 —— 單獨掛這個旗標,中文版配大球場照樣跳出, 真正卡住的是遊戲自己切的那塊 64 MB 記憶體池,兩層天花板不一樣, 池子怎麼放大見中文版配大球場就跳出。 至於王朝模式玩久了跳出是不是同一個池子,本站還沒實測過

說清楚 那個旗標是相容性設定,跟任何防盜拷或授權驗證機制無關。

④ 老闆模式的主場視角會跳出 —— 至今無解

症狀:老闆模式(經營模式)切到主場觀眾席視角就當掉。

成因未解 本站不知道。踩了十幾年,沒有查出原因,也沒有找到繞過的方法。

解法沒有。目前只能避開那個視角。

這一條寫出來是因為:站上其他每一頁都在講「解開了什麼」, 但有些東西就是沒解開。 一個只寫成功案例的站,會讓你以為每個問題都有答案。

📌 重點整理

這支腳本在做什麼

沒讀過程式碼也沒關係,這一段把它的流程講完。

它做的事只有一句話:把四件跟當機有關的事實讀出來印給你看,一個位元組都不寫。 先確認你給的路徑存不存在(拖成 mvp2005.exe 也接受,它會自己退到上一層), 再依序跑【1】執行檔的記憶體上限、【2】data 的資料量、 【3】四個關鍵檔案在不在、【4】你自己的備份檔有幾個。 每一段遇到值得注意的事就先收進一張清單,全部跑完才一次印出來, 而不是邊跑邊喊;後者會讓結論散在四段輸出中間,讀者得自己往回翻。

整份程式只用得到讀取類的呼叫(read_bytesstatglobrglobiterdir),沒有任何一處寫入。 它的定位是動手之前的體檢,體檢本身不該動到病人

哪一段做什麼為什麼要有它
read_pe_info() 讀執行檔開頭的 PE 檔頭,取出 Machine(給哪一種處理器跑的) 與 Characteristics(一整排開關)這兩個欄位。 記憶體上限不是猜出來的,它就寫在 Characteristics 的 0x0020 那一個位元上。 只解析開頭 4096 個位元組,是因為要用的欄位全部落在那個範圍內。 這一格原本寫「不必為了看兩個數字把整個執行檔搬進記憶體」,那句話是錯的。 程式那一行是 exe.read_bytes()[:4096],先把整個檔案讀進記憶體, 再切出前面 4096 個位元組,整個檔案還是進來了。本站在中文版配大球場就跳出那一課 量過的那顆剛安裝好的英文原版 mvp2005.exe(6,972,001 bytes)上, 用 Python 內建的 tracemalloc 量(macOS · Python 3.9.6, 兩邊的量測腳本除了這一行以外一模一樣):這一行的記憶體峰值是 6,979,611 bytes;改寫成 with open(exe, 'rb') as f: data = f.read(4096) 是 10,549 bytes,兩種寫法拿到的 4096 個位元組完全一樣(sha256 相同)。 這兩個數字只有在同一支量測腳本裡才可以互相比較:tracemalloc 的峰值連量測腳本自己配置的記憶體都算進去,尾數會隨著你怎麼量浮動幾百個位元組, 而 open() 收到的是 Path 還是字串又會再差一千多個位元組。 站得住的是量級,兩者差了約 660 倍。 體檢腳本只跑一次,這個差別不影響使用,所以程式碼維持原樣,改的是說明。
human() 把位元組數換成 KB、MB、GB。 bytes 印整數加千分位,其他單位印一位小數。 這份報告是給沒在算位元組的人看的。 「165.0 MB」一眼就懂,「172,992,803 bytes」要停下來數位數。
main() 開頭那幾行 取路徑、確認存不存在、拖進來的如果是檔案就退到它的上一層, 然後準備一張空的「值得注意」清單。 很多人是把 mvp2005.exe 拖進終端機, 而不是拖資料夾。與其回一句「路徑錯了」,不如退一層, 那本來就是他想指的地方。
main() 的【1】 在資料夾裡找 .exe,分三層挑出主程式 (精確檔名、名字裡有 mvp、第一個),讀出位元數與記憶體上限。 改過的懶人包常常多塞幾個 .exe 進來。 挑錯一個,整段結論就是在講別的檔案,所以除了分三層挑, 還會把「報告的是哪一個、其他有哪些」一起印出來。
main() 的【2】 走遍 data 底下所有檔,算總量、數 .big、 列出最大的五個,並且點出疑似備份的子資料夾。 備份資料夾會讓總量與 .big 個數看起來多一倍, 不講清楚,讀者會以為自己的遊戲爆掉了。 所以旁邊會附上「未改過的遊戲通常是 224 個 .big」當對照。
【2】裡的球場檔那一小段 單獨看 data\stadium\,印出球場檔的個數與最大的那一個, 並附上剛安裝好的原版基準(87 個檔,最大 3.99 MB); 超過 10 MB 的另外列成一張清單,清單最多列五個, 實際有幾個要看清單上一行的數字。本站測試機那份 data 就是前面表裡最大 15.14 MB 的那一份,有 24 個超過,清單只列出前五個。 這裡有一段訂正紀錄留在程式碼裡。 門檻原本寫 80 MB,而本站掃過的 692 個球場檔最大只有 18.44 MB, 那個檢查永遠不會亮,只是讓人以為「檢查過了沒問題」。 現在的 10 MB 是社群傳了二十年的說法,不是本站量到的當機邊界, 所以措辭是「值得清一下」而不是「這會當機」。
main() 的【3】 檢查四個關鍵檔案在不在:球員屬性、賽程、比賽中介面、選單介面。 這四個少了就一定不對,而原因通常只有兩種, 指錯資料夾,或是下載的那一包根本沒下載完。 它只看在不在、不開內容,所以「檔案還在但已經壞了」這一段驗不出來。
main() 的【4】 找出本站工具留下的備份檔 (認得的副檔名寫在下方完整原始碼的 BAK_SUFFIXES 那一份清單裡, .bak.speedbak.locbak.hudcolorbak.modernizebak 這一類), 並且把遊戲原廠自己附的那一個另外列。 找到幾個會印在標題那一行,但路徑最多只列 8 個, 超過的收成一行 …(還有 N 個) (上限 8 寫死在腳本裡,見下方完整原始碼的 baks[:8])。 名字取成 x.bak資料夾、以及指向不存在檔案的捷徑, 都不算備份 —— 腳本用 is_file() 擋掉了。 本站每一支工具用自己的副檔名, 同時跑兩支時備份才不會互相覆蓋,所以只找 *.bak 會看不見其他那些。 ⚠️ 2026-09-05 訂正:這一格原本寫「找出四種副檔名」, 而那份清單當時漏掉二十種 —— 只改過 .locbak.hudcolorbak 這一類的人,【4】會對他說「沒有找到任何你自己的備份檔」。 清單已經補齊,這裡也不再寫死幾種(多一課會寫檔的教學通常就多一種)。 要一次看全部的備份,用備份與還原 SOP那一課的檢查器。 原廠那個 data\igshapes\igcrsr.fsh.bak 要排掉, 否則等於對一台從沒備份過的機器說「這是好習慣」。
main() 最後的結論 把一路收集到的「值得注意」編號印出來, 沒有就說沒有,最後再聲明一次全程唯讀。 結束碼分成「跑完了」(0)、 「沒給路徑或路徑不存在」(1)與「掃描途中讀取出錯」(2), 按 Ctrl+C 中斷是 130;發現幾項值得注意不會改變它。 這是體檢不是測試,不該讓批次檔因為有一項值得注意就中斷。

安全網在這一支身上是反過來的:它沒有備份、也沒有還原功能, 因為它從頭到尾沒有要寫任何東西。做不到的事也一起講: 遊戲跑起來實際吃掉多少記憶體它量不到(【1】讀的是「最多准用多少」,不是「現在用了多少」), 模組好不好、相不相容它也判斷不了。

想先確認【4】那一段可不可信,多打一個旗標就好:

python mvp_check_setup.py --selftest
不讀你的遊戲資料夾,只在記憶體裡拿假的路徑字串把「哪些檔算備份」那條規則驗一次, 不建暫存檔、也不碰磁碟。四項全綠才代表【4】的判斷還照著那份清單走; 有一項紅了它會用結束碼 1 收場。 四項裡有兩項是故意種下的餌(清單砍短之後 .ratingsbak 必須立刻看不見、 原廠那一個連反斜線與大寫的寫法都要挑得出來), 沒有餌的話,一條「什麼都算備份」的爛規則也會全綠。

完整原始碼

下面就是你剛才下載的那一支,一個字都沒有不同(本站有自動檢查在守這件事)。不必下載也能先看過一遍。

展開 / 收合完整原始碼(541 行)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
mvp_check_setup.py
檢查 EA MVP Baseball 2005 的安裝狀況,診斷當機原因。

    python3 mvp_check_setup.py "遊戲安裝資料夾"

例如:
    python3 mvp_check_setup.py "C:\\Program Files (x86)\\EA SPORTS\\MVP Baseball 2005"

這支腳本**全程唯讀**,不會修改任何檔案,只是把幾項資訊讀出來給你看:

  1. 執行檔能用多少記憶體(32 位元程式預設上限 2 GB,是當機的候選原因之一;
     另一層是遊戲自己切的記憶體池,本站那顆執行檔上量到的是 64 MB)
  2. 遊戲資料總量,以及最大的幾個檔案
  3. 常見的安裝問題(檔案缺失、資料夾位置不對)

── 輸入 ─────────────────────────────────────────────
  一個路徑,指到遊戲的安裝資料夾(裡面看得到 .exe 與 data 資料夾的那一層)。
  直接把 mvp2005.exe 拖進來也接受,腳本會自己退到它的上一層。
  以 -- 開頭的參數一律當旗標略過,所以路徑排在旗標後面也讀得到。
  唯一認得的旗標是 --selftest:不讀你的遊戲資料夾,只在記憶體裡把
  【4】那條「哪些檔算備份」的規則驗一次(含兩個故意種下的餌)。
  它拒絕在 python3 -O 底下跑,理由寫在 self_test() 的說明裡。

── 輸出 ─────────────────────────────────────────────
  只印到畫面,不產生任何檔案:
    【1】主程式是幾位元、記憶體上限是 2 GB 還是 4 GB
    【2】data 的總量、.big 個數、最大的五個檔;球場檔另外算一次,印個數與最大的那一個,
        超過 10 MB 的最多再列五個(只有這幾項,沒有中位數也沒有分級)
    【3】四個關鍵檔案在不在
    【4】腳本找到的備份檔(總數會印,路徑最多列 8 個;原廠自己附的那一個另外列,不算你的)
  最後把一路收集到的「值得注意」彙整成一張清單。
  結束碼:0 = 跑完了,1 = 沒給路徑或路徑不存在(或 --selftest 有一項紅了),
          2 = 掃描途中讀取出錯,或是在 python3 -O 底下跑 --selftest(它會拒跑),
          130 = 你按了 Ctrl+C。
  「發現 N 項值得注意」不會改變結束碼,那是給人看的,不是給批次檔判斷的。

── 安全網在哪 ───────────────────────────────────────
  這支不需要備份,也沒有還原功能,因為它沒有可以出錯的動作:
  整份只有讀取用的呼叫(read_bytes、stat、exists、is_dir、glob、rglob、iterdir 這一類),
  沒有任何 open(..., 'w')、沒有刪除、沒有搬移,也不連網。
  --selftest 也一樣:它餵給規則的是假的路徑字串,不建暫存檔、不碰磁碟。
  中途按 Ctrl+C 停掉也不會留下半截檔案,因為根本沒有檔案被打開來寫。
  它的定位是「動手之前的體檢」,體檢本身不該動到病人。

── 做不到的事 ───────────────────────────────────────
  · 量不到遊戲跑起來實際吃掉多少記憶體。【1】讀的是執行檔檔頭裡的一個旗標,
    那是「最多准用多少」,不是「現在用了多少」。
  · 判斷不了模組好不好、相不相容。它只量檔案的大小與在不在。
  · 【3】只看檔案在不在,不開內容,所以「檔案還在但已經壞了」這裡驗不出來。
  · 【4】認得的是本站工具留下的那些副檔名(下面 BAK_SUFFIXES 那份清單)。
    你自己手動複製、改名成別的樣子的備份(例如 attrib 複本.dat),這裡看不到。
  · 球場檔那個 10 MB 是社群傳了二十年的說法,不是本站量到的當機邊界。
    超過只代表值得清一下,不代表一定會當(這一點程式碼裡也有寫)。

無外部相依,Python 3.7 以上即可。
授權:MIT(見檔尾完整條款)。本站教學文字另採 CC BY 4.0。
"""
#
# ─────────────────────────────────────────────────────────
#  法律與免責(每一支本站腳本都帶著這一段)
#
#  · 本工具與 Electronic Arts 無任何官方關聯,也未經其授權或背書。
#    MVP Baseball 2005 為 Electronic Arts 之作品與商標。
#  · 本工具為原創程式碼,**不含任何 EA 的程式碼或資產**。
#  · 本工具不提供、不教學、也不包含任何規避技術保護措施的功能。
#  · 使用者應僅對自己合法取得的遊戲副本使用本工具,並自行承擔風險。
#    使用前請自行確認你與遊戲發行商之間的使用者授權合約(EULA)。
#  · 本工具按「現狀」提供,不附任何明示或默示的擔保。
#  · 授權:MIT(見檔尾)。教學文字另採 CC BY 4.0。
#  · 回報與下架:https://toniliumvp.github.io/MVPBaseball/report.html
#    三條管道,其中「直接向 GitHub 提出」不需經過維護者;
#    留言區那條不需要任何帳號。管道有變動只會改那一頁。
# ─────────────────────────────────────────────────────────

import struct
import sys
from pathlib import Path

# Windows 主控台預設編碼存不下 ✓ ✗ 這類符號,先把輸出轉成 UTF-8。
try:
    sys.stdout.reconfigure(encoding='utf-8', errors='replace')
    sys.stderr.reconfigure(encoding='utf-8', errors='replace')
except (AttributeError, ValueError):
    pass

# PE Characteristics 旗標
# (前兩個是 Characteristics 裡的位元,後兩個是 Machine 欄位的值,
#  數值都來自 PE 格式本身,不是本站定義的。)
# 0x0020 = 大位址支援。32 位元程式開了它,位址空間上限從 2 GB 變成 4 GB。
# 0x0100 = 這支程式只跑得動 32 位元機器。目前這份程式碼沒有讀這個位元。
# 0x014C / 0x8664 是 Machine 欄位最常見的兩個值:x86 與 x64。
IMAGE_FILE_LARGE_ADDRESS_AWARE = 0x0020
IMAGE_FILE_32BIT_MACHINE = 0x0100
MACHINE_I386 = 0x014C
MACHINE_AMD64 = 0x8664


def read_pe_info(exe: Path):
    """讀 PE 檔頭,回傳 (machine, characteristics)。不是 PE 就回 None。

    Windows 執行檔的檔頭是一串固定位移的欄位,這裡用到五個(這個數目是照這個函式自己的
    程式碼數的,下面五個欄位各對應一次讀取。其中三個是數字,用小端序讀,也就是低位的
    位元組放前面;另外兩個是 MZ 與 PE 那兩塊招牌,直接比對位元組,不牽涉小端序):

      位移 0x00   2 bytes   'MZ',1980 年代 DOS 留下來的招牌
      位移 0x3C   4 bytes   PE 檔頭從檔案的哪個位移開始
      PE + 0      4 bytes   'PE' 後面接兩個 0x00
      PE + 4      2 bytes   Machine,給哪一種處理器跑的
      PE + 22     2 bytes   Characteristics,一整排開關,
                            大位址支援就是其中一個位元

    只解析開頭 4096 個位元組:上面那些欄位全部落在這個範圍內。
    ⚠️ 2026-09-03 訂正:這裡原本寫「不必為了看兩個數字把整個執行檔搬進
    記憶體」,那句話是錯的。下面那一行是先用 read_bytes() 把整個檔案讀進
    記憶體,再切出前面 4096 個位元組,整個檔案還是進來了。
    本站在那顆剛安裝好的全新英文原版 mvp2005.exe(6,972,001 bytes)上,
    用 Python 內建的 tracemalloc 量(macOS · Python 3.9.6,兩邊的量測腳本除了
    這一行以外一模一樣):這一行的記憶體峰值是 6,979,611 bytes;改寫成
    with open(exe, 'rb') as f: data = f.read(4096) 是 10,549 bytes,
    兩種寫法拿到的 4096 個位元組完全一樣(sha256 相同)。
    ⚠️ 這兩個數字只有在同一支量測腳本裡才可以互相比較。tracemalloc 的峰值
    連量測腳本自己配置的記憶體都算進去,尾數會隨著你怎麼量浮動幾百個位元組;
    而 open() 收到的是 Path 還是字串又會再差一千多個位元組(傳字串量到的是
    8,989)。站得住的是量級:兩者差了約 660 倍。
    體檢腳本只跑一次,這個差別不影響使用,所以程式碼維持原樣,改的是說明。
    """
    try:
        data = exe.read_bytes()[:4096]
    except OSError:
        # 讀不到就當成「格式讀不出來」處理:同名的資料夾、斷掉的捷徑、
        # 沒有讀取權限都走這裡。體檢工具不該把 traceback 丟給讀者。
        return None
    # 沒有 MZ 招牌就不是 Windows 執行檔。硬解下去只會解出一堆假數字。
    if data[:2] != b'MZ':
        return None
    # 檔案短到連 0x3C 那個欄位都放不下,多半是空檔或沒下載完的半截檔。
    if len(data) < 0x40:
        return None
    # '<I' = 小端序、4 個位元組、無號整數。它的值是 PE 檔頭的位移,不是固定的。
    pe_off = struct.unpack('<I', data[0x3C:0x40])[0]
    # 兩種壞法都要擋:指標指到我們沒讀進來的範圍外,或指到的位置不是 'PE' 招牌。
    # 24 是「從 PE 檔頭起算,要讀到 Characteristics 至少需要這麼多位元組」。
    if pe_off + 24 > len(data) or data[pe_off:pe_off + 4] != b'PE\x00\x00':
        return None
    # '<H' = 小端序、2 個位元組、無號整數。兩個欄位都在 PE 檔頭的固定位移上。
    machine = struct.unpack('<H', data[pe_off + 4:pe_off + 6])[0]
    chars = struct.unpack('<H', data[pe_off + 22:pe_off + 24])[0]
    return machine, chars


def human(n: int) -> str:
    """把位元組數換成人看得懂的單位。1 KB 一律當 1024 bytes 算。"""
    # 逐級除以 1024,除到數字小於 1024 或者已經升到 GB 為止。
    # 不再往上升是因為這個遊戲的資料夾不會到 TB,多一級只是多一個永遠走不到的分支。
    for unit in ('bytes', 'KB', 'MB', 'GB'):
        if n < 1024 or unit == 'GB':
            # bytes 只有在小於 1024 時才會用到,那時印整數就夠,小數點沒有意義;
            # 其他單位印一位小數,「165.0 MB」比「172,992,803 bytes」好讀。
            return f'{n:,.0f} {unit}' if unit == 'bytes' else f'{n:.1f} {unit}'
        n /= 1024
    # 上面那圈跑到 'GB' 時條件一定成立,所以這一行實務上走不到,留著當保險。
    return f'{n:.1f} GB'


# 本站每一支會寫遊戲檔的工具,第一次寫入前都會留一份備份,而且各用各的副檔名
# (.playerbak、.ratingsbak、.portraitbak …),這樣同時跑兩支工具備份不會互相蓋掉。
# 這份清單漏一種,那一支工具留的備份在【4】就完全看不見,而且不會有任何提示 ——
# 讀者會以為自己沒備份。2026-09-05 補齊之前,這裡只有四種,而當時站上已經有 25 種。
# ⚠️ 這份清單要跟「檢查備份夠不夠」那一課的 mvp_check_backup.py 裡那份對得上。
#    兩邊都是逐字寫出的字面字串,所以可以用機器逐條比對,不必用眼睛看。
#    這支腳本要能單獨下載後直接跑(無外部相依),所以是各留一份、靠檢查器對齊,
#    不是 import 對方。多一課會寫檔的教學通常就多一種,所以這裡不寫死「幾種」。
#    順序不影響任何判斷,排成字母序只是給人好找。
BAK_SUFFIXES = (
    '.bak', '.audiobak', '.autobak', '.chantbak', '.datafilebak',
    '.exepebak', '.facebak', '.facetexbak', '.feltoolbak', '.fontbak',
    '.hudcolorbak', '.introbak', '.locbak', '.logobak', '.menutextbak',
    '.modernizebak', '.packbak', '.playerbak', '.portraitbak',
    '.ratingsbak', '.screenbak', '.shrinkbak', '.speedbak', '.unibak',
    '.unpackbak',
)

# ⚠️ EA 自己就在遊戲裡附了一個 .bak。把它算成使用者的備份,
#    等於對一台從沒備份過的全新機器說「這是好習慣」——
#    而這支腳本的用途正是動手前的體檢,方向剛好相反。
# 寫成「斜線 + 小寫」的相對路徑,比對前會把待測路徑也轉成同一種寫法。
SHIPPED_BAKS = {'data/igshapes/igcrsr.fsh.bak'}


def classify_baks(rels, suffixes=BAK_SUFFIXES, shipped_set=SHIPPED_BAKS):
    """把相對路徑分成三堆,回傳 (你自己的備份, 遊戲本來就附的, 不是備份檔)。

    只吃路徑字串、不碰檔案系統,所以 --selftest 可以直接餵假路徑進來驗這條規則。
    比對前先統一成「斜線 + 小寫」:Windows 用反斜線分隔,而且檔名不分大小寫,
    不先統一的話同一份清單在三種作業系統上會對不起來。
    """
    mine, shipped, other = [], [], []
    for rel in rels:
        key = str(rel).replace('\\', '/').lower()
        base = key.rsplit('/', 1)[-1]
        # 用最後一個點之後的部分當副檔名,跟 pathlib 的 .suffix 同一種切法
        # (igcrsr.fsh.bak 取到的是 .bak,不是 .fsh.bak)。
        suf = base[base.rfind('.'):] if '.' in base else ''
        if suf not in suffixes:
            other.append(rel)
        elif key in shipped_set:
            shipped.append(rel)
        else:
            mine.append(rel)
    return mine, shipped, other


def self_test():
    """把【4】那條「哪些檔算備份」的規則當場驗一次,全綠回 True。

    全程只在記憶體裡跑假路徑:不建檔、不刪檔、也不讀你的遊戲資料夾。
    四項裡有兩項是餌 —— 沒有餌的話,一個「什麼都算備份」的爛規則也會全綠。

    在 python3 -O 底下不跑,直接以結束碼 2 收工(不回傳 True/False)。
    """
    # ⚠️ python3 -O 會把整份程式裡的 assert 拿掉。本站其他工具的自我測試是用
    #    assert 判斷的,在 -O 底下那些餌一條都不會執行,螢幕上照樣一路 ✅ ——
    #    那是一片假的綠燈,比沒有測試更危險。
    #    這一支目前用的是 if/else 不是 assert,所以 -O 還不會讓它假綠;擋掉是為了
    #    跟全站的自我測試一致,而且哪天這裡改寫成 assert,守門已經在了,
    #    不必指望那時候有人記得補。
    #    這裡直接結束整支程式,不回傳 —— 「測試沒跑」跟「測試沒過」是兩回事,
    #    用 1(有一項紅了)會把它們混在一起。
    if sys.flags.optimize:
        print('--selftest 不能在 python3 -O 底下跑:-O 會把 assert 整段拿掉,'
              '哪天這裡改用 assert 判斷就會變成一片假的綠燈。請拿掉 -O 再跑一次。')
        sys.exit(2)
    print('【0】自我測試(不碰任何檔案)')
    ok = True

    # 一、清單裡的每一種副檔名都要認得出來
    probes = ['data/database/attrib.dat' + s for s in BAK_SUFFIXES]
    mine, _, other = classify_baks(probes)
    if len(mine) == len(BAK_SUFFIXES) and not other:
        print(f'   ✅ 清單裡 {len(BAK_SUFFIXES)} 種副檔名全部認得')
    else:
        ok = False
        print(f'   ❌ 有 {len(other)} 種沒認出來:{other}')

    # 二、陰性對照:沒在清單裡的副檔名不可以被算成備份。
    #     少了這一條,上面那一項用「什麼都算備份」也會過。
    mine, _, other = classify_baks(['data/database/attrib.dat.neverheardofbak',
                                    'data/database/attrib.dat'])
    if not mine and len(other) == 2:
        print('   ✅ 陰性對照:認不得的副檔名沒有被算成備份')
    else:
        ok = False
        print(f'   ❌ 陰性對照失敗,被算成備份的有:{mine}')

    # 三、餌:把清單砍到只剩 .bak,.ratingsbak 就必須從「你的備份」掉出來。
    #     這是 2026-08-29 真的發生過的漏洞形態 —— 清單漏一種,
    #     那一支工具留的備份就完全看不見,而且是靜默的。
    short, _, _ = classify_baks(['data/database/attrib.dat.ratingsbak'],
                                suffixes=('.bak',))
    full, _, _ = classify_baks(['data/database/attrib.dat.ratingsbak'])
    if not short and len(full) == 1:
        print('   ✅ 餌一:清單砍短之後,.ratingsbak 立刻看不見了(規則真的由清單決定)')
    else:
        ok = False
        print(f'   ❌ 餌一沒抓到(砍短={short} / 完整={full})')

    # 四、餌:遊戲本來就附的那一個要被挑出來,連反斜線與大寫的寫法都要挑得出來;
    #     但同名檔放在別的資料夾就是你自己的備份,不可以一起被吃掉。
    mine, shipped, _ = classify_baks(['data/igshapes/igcrsr.fsh.bak',
                                      'DATA\\IGSHAPES\\IGCRSR.FSH.BAK',
                                      'mymods/igcrsr.fsh.bak'])
    if len(shipped) == 2 and len(mine) == 1:
        print('   ✅ 餌二:原廠那一個(含反斜線/大寫寫法)沒被算成你的,同名副本仍算你的')
    else:
        ok = False
        print(f'   ❌ 餌二失敗(原廠={len(shipped)} 個 / 你的={len(mine)} 個)')

    print('   全綠' if ok else '   有項目沒過')
    return ok

def main():
    """依序跑完四段檢查,把結果印出來,最後彙整成一張「值得注意」的清單。"""
    # 只把「不是以 -- 開頭」的參數當成路徑。這樣以後要加旗標,
    # 不會因為旗標排在前面就把路徑吃掉。
    args = [a for a in sys.argv[1:] if not a.startswith('--')]
    # --selftest 不需要路徑:它驗的是規則,不是你的遊戲。
    if any(a.lower() == '--selftest' for a in sys.argv[1:]):
        return 0 if self_test() else 1
    if not args:
        # 沒給路徑就把檔頭那份說明當用法提示印出來,並用 1 表示「什麼都沒做」。
        print(__doc__)
        return 1

    # expanduser 是為了讓 ~/... 這種寫法也能用(Mac 與 Linux 常見)。
    root = Path(args[0]).expanduser()
    if not root.exists():
        print(f'✗ 找不到這個資料夾:{root}')
        return 1
    # 有人會直接把 mvp2005.exe 拖進終端機,而不是拖資料夾。
    # 與其怪他打錯,不如退到它的上一層,那本來就是他想指的地方。
    if root.is_file():                                   # 拖到 exe 也接受
        root = root.parent

    print(f'遊戲資料夾  {root}\n')
    # 一路把「值得注意」收在這個清單裡,最後一起印。
    # 邊跑邊印的話,結論會散在四段輸出中間,讀者得自己往回翻。
    problems = []

    # ── 1. 執行檔記憶體上限 ────────────────────────────
    # MVP 2005 是 32 位元程式,沒開大位址支援的話,
    # 不管電腦插了多少記憶體都只准用 2 GB。
    # ⚠️ 「模組疊多了就是會撞到這個天花板」這句話本站沒有量到。量到的是:
    #    在本站那顆執行檔上,只掛大位址旗標沒有感覺 —— 卡住的是遊戲自己切的
    #    那塊記憶體池(量到 64 MB),那是另一層天花板。
    #    所以這一段給的是候選原因,不是結論。
    print('【1】執行檔能用多少記憶體')
    exes = sorted(root.glob('*.exe'))
    # 挑主程式分三層,由準到寬:精確檔名 → 名字裡有 mvp → 第一個 .exe。
    # 分三層是因為改過的懶人包常常多塞幾個 .exe 進來,
    # 挑錯一個,底下整段結論就是在講別的檔案。
    # 優先精確檔名,避免挑到 "mvp2005 - 備份.exe" 這類副本
    main_exe = next((e for e in exes if e.name.lower() == 'mvp2005.exe'), None)
    if not main_exe:
        main_exe = next((e for e in exes if 'mvp' in e.name.lower()), exes[0] if exes else None)
    # 不只一個 .exe 時,要講清楚報告的是哪一個,
    # 否則讀者會拿這一段結論去對照他心裡想的另一個檔。
    if len(exes) > 1:
        others = [e.name for e in exes if e != main_exe]
        print(f'  註    資料夾裡有 {len(exes)} 個 .exe,以下報告的是 {main_exe.name if main_exe else "?"}')
        print(f'        其他:{", ".join(others[:3])}{"…" if len(others) > 3 else ""}')
    if not main_exe:
        print('  ✗ 這個資料夾裡沒有 .exe,可能不是遊戲安裝資料夾')
        problems.append('找不到遊戲執行檔')
    else:
        info = read_pe_info(main_exe)
        # 大小也可能讀不到:斷掉的捷徑就是這樣,stat 會跟著連結走過去然後撲空。
        # 這裡讀不到不該讓整支停下來 —— 後面三段還是有東西可以給讀者看。
        try:
            size_txt = human(main_exe.stat().st_size)
        except OSError:
            size_txt = '大小讀不到'
        print(f'  檔案  {main_exe.name}({size_txt})')
        if not info:
            print('  ✗ 讀不出程式格式(檔案可能損毀,或指到的是資料夾、斷掉的捷徑、沒有讀取權限的檔)')
            problems.append('執行檔格式異常')
        else:
            machine, chars = info
            # Machine 說的是「給哪種處理器跑的」。MVP 2005 是 2005 年的遊戲,
            # 正常情況會是 0x014C(x86);認不得的值就把原始數字印出來,不要亂猜。
            bits = '32 位元' if machine == MACHINE_I386 else '64 位元' if machine == MACHINE_AMD64 else f'未知(0x{machine:04X})'
            # Characteristics 是一整排開關,用 & 取出 0x0020 那一個位元。
            # 它就是大位址支援,開了之後 32 位元程式的位址空間上限從 2 GB 變成 4 GB。
            # 這是相容性旗標,跟任何防拷或授權驗證機制無關。
            large = bool(chars & IMAGE_FILE_LARGE_ADDRESS_AWARE)
            print(f'  類型  {bits} 程式')
            if machine == MACHINE_I386:
                if large:
                    print('  上限  ✅ 最多 4 GB(已開啟大位址支援)')
                    print('        這台的執行檔已經處理過記憶體上限問題。')
                else:
                    print('  上限  ⚠ 只有 2 GB(未開啟大位址支援)')
                    print('        這是當機的候選原因之一。本站在自己那顆執行檔上量到的是:')
                    print('        只掛大位址旗標沒有感覺,卡住的是遊戲自己切的那塊記憶體池。')
                    problems.append('執行檔記憶體上限只有 2 GB')
            else:
                print('  上限  (非 32 位元程式,不受 2 GB 限制)')

    # ── 2. 遊戲資料量 ──────────────────────────────────
    # 這一段量的一律是「磁碟上的位元組數」,不是封裝檔解開之後的內容大小。
    # 兩種算法在大檔上差不多,在小檔上差很多,所以口徑要先講清楚。
    print('\n【2】遊戲資料量')
    data_dir = root / 'data'
    if not data_dir.is_dir():
        print(f'  ✗ 找不到 data 資料夾')
        print('    確認你選的是遊戲安裝資料夾(裡面應該有 data\\ 和 .exe)')
        problems.append('找不到 data 資料夾')
    else:
        # rglob('*') 會一路走進所有子資料夾,包含使用者自己放在 data 底下的備份。
        # 那正是下面要特別點出備份資料夾的原因:總量會被灌水。
        files = [f for f in data_dir.rglob('*') if f.is_file()]
        total = sum(f.stat().st_size for f in files)
        # .big 是這個遊戲的封裝檔,一個 .big 裡面包著很多筆內容。
        # 副檔名先轉小寫再比:有的檔案系統不分大小寫、有的分,
        # 不先統一的話 .BIG 這種寫法會被漏掉。
        bigs = [f for f in files if f.suffix.lower() == '.big']
        print(f'  總量      {human(total)}({len(files):,} 個檔案)')
        print(f'  封裝檔    {len(bigs)} 個 .big')
        # 疑似備份子資料夾會讓數字看起來偏高,提示一下
        # 只看 data 底下的第一層,而且是靠資料夾名字猜的,中英文各列幾個常見寫法。
        # 猜錯的代價很小(多印一行註記),漏掉的代價比較大(讀者會以為自己的遊戲爆掉了)。
        bak_dirs = [d for d in data_dir.iterdir()
                    if d.is_dir() and any(k in d.name for k in ('備份', '原版', 'Backup', 'backup', '副本'))]
        if bak_dirs:
            bak_files = sum(1 for d in bak_dirs for f in d.rglob('*.big'))
            shown = ', '.join(d.name[:22] + ('…' if len(d.name) > 22 else '')
                              for d in bak_dirs[:2])
            if len(bak_dirs) > 2:
                shown += f' 等 {len(bak_dirs)} 個'
            print(f'  註        其中 {bak_files} 個 .big 在疑似備份的子資料夾內'
                  f'({shown})')
            print(f'            未改過的遊戲通常是 224 個 .big')
        big5 = sorted(bigs, key=lambda f: -f.stat().st_size)[:5]
        if big5:
            print('  最大的幾個:')
            for f in big5:
                print(f'    {human(f.stat().st_size):>10}  {f.relative_to(data_dir)}')
        # 球場檔比原版大很多的,值得拿清孤兒那支腳本看一下。
        #
        # ⚠️ 2026-08-28 訂正:這裡原本寫「超過 80 MB」。那個數字沒有依據 ——
        #    本站掃過 692 個球場檔(全部來源合計,不寫死來源數),
        #    最大的一個是 18.44 MB,80 MB 是它的 4.3 倍。
        #    也就是說這個檢查從來沒有亮過,而且不可能亮 ——
        #    它只是讓人以為「檢查過了沒問題」。
        #
        # 現在用的基準是量出來的:剛安裝好的原版 87 個球場檔,最大 3.99 MB。
        # 超過 10 MB 就提醒 —— 10 這個數字是社群傳了二十年的說法,不是本站量到的門檻,
        # 所以措辭是「值得看一下」不是「這會當機」。
        PRISTINE_MAX_MB = 3.99      # 剛安裝好的原版 87 個檔裡最大的那個
        FOLKLORE_MB = 10            # 社群傳的數字,不是實測門檻
        # 球場檔全部放在 data\stadium\ 底下,副檔名是 .big。
        # 沒有這個資料夾也不算錯(有人只裝了部分內容),那就整段跳過。
        stad = data_dir / 'stadium'
        if stad.is_dir():
            bigs_st = sorted(stad.glob('*.big'), key=lambda f: -f.stat().st_size)
            over = [f for f in bigs_st if f.stat().st_size > FOLKLORE_MB * 1024 * 1024]
            if bigs_st:
                top = bigs_st[0]
                print(f'\n  球場檔 {len(bigs_st)} 個,最大的是 '
                      f'{human(top.stat().st_size)}  {top.name}')
                print(f'    (剛安裝好的原版 87 個檔,最大 {PRISTINE_MAX_MB} MB)')
            if over:
                print(f'  ⚠ 有 {len(over)} 個超過 {FOLKLORE_MB} MB:')
                for f in over[:5]:
                    print(f'    {human(f.stat().st_size):>10}  {f.name}')
                print('    這不代表一定會當機 —— 社群傳的「太大會跳出」沒有人量過邊界。')
                print('    但變大的原因多半是改檔留下的舊版本,可以用清孤兒那支腳本清掉,')
                print('    內容一個位元組都不會少。')
                problems.append(f'{len(over)} 個球場檔超過 {FOLKLORE_MB} MB(值得清一下,不一定是問題)')

    # ── 3. 關鍵檔案是否齊全 ────────────────────────────
    # 這四個是「少了就一定不對」的檔:兩個資料庫、兩個介面封裝檔。
    # 缺了通常只有兩種原因,指錯資料夾,或是下載的那一包根本沒下載完。
    # 注意這裡只看檔案在不在,不開內容,所以「在但是壞的」這一段驗不出來。
    print('\n【3】關鍵檔案')
    expect = [
        ('data/database/attrib.dat', '球員屬性'),
        ('data/database/schedule.big', '賽程'),
        ('data/frontend/ingame.big', '比賽中介面'),
        ('data/frontend/frontend.big', '選單介面'),
    ]
    for rel, desc in expect:
        p = root / rel
        mark = '✅' if p.exists() else '✗ '
        print(f'  {mark} {rel:<34}{desc}')
        if not p.exists():
            problems.append(f'缺少 {rel}')

    # ── 4. 備份檔提醒 ──────────────────────────────────
    # 走一次整棵樹,用副檔名比對(副檔名先轉小寫,.BAK 這種寫法也認得)。
    # 只認 .bak 的話,其他二十幾種副檔名的備份會完全看不見,而且不會有任何提示。
    print()
    found = sorted(f for f in root.rglob('*')
                   if f.suffix.lower() in BAK_SUFFIXES and f.is_file())
    baks, shipped, _ = classify_baks([f.relative_to(root) for f in found])
    if baks:
        print(f'【4】找到 {len(baks)} 個你自己的備份檔')
        for b in baks[:8]:
            print(f'  {b}')
        if len(baks) > 8:
            print(f'  …(還有 {len(baks) - 8} 個)')
        print('  這是好習慣。改壞了可以用它們還原。')
    else:
        print('【4】沒有找到任何你自己的備份檔')
        print('  如果你還沒改過任何東西,這是正常的。')
        print('  如果你改過,代表備份不在遊戲資料夾裡,或是你自己取的名字這裡認不得')
        print('  (只認得下面 BAK_SUFFIXES 那份清單裡的副檔名)—— 動手前請先自己複製一份。')
    if shipped:
        names = ', '.join(str(b) for b in shipped)
        print(f'  (另外有 {len(shipped)} 個是遊戲本來就附的,不算你的備份:{names})')

    # ── 結論 ───────────────────────────────────────────
    # 這張清單只印給人看,不影響結束碼:這支是體檢不是測試,
    # 不該讓批次檔因為「有一項值得注意」就中斷。
    print('\n' + '=' * 46)
    if not problems:
        print('沒有發現明顯問題。')
        print('如果還是會當機,可能是模組相容性或顯示設定的問題。')
    else:
        print(f'發現 {len(problems)} 項值得注意:')
        for i, p in enumerate(problems, 1):
            print(f'  {i}. {p}')
    print('\n本次檢查全程唯讀,沒有修改任何檔案。')
    return 0


if __name__ == '__main__':
    try:
        sys.exit(main())
    except KeyboardInterrupt:
        # 這支從頭到尾沒有打開任何檔案來寫,也沒有搬移或刪除,
        # 所以中途停掉就只是少印幾行,遊戲資料夾裡什麼都沒有動到。
        # 結束碼用 130(=128+SIGINT),跟其他幾種結束方式分得開。
        print('\n\n已中止。本次檢查全程唯讀,沒有動到任何檔案。')
        sys.exit(130)
    except OSError as e:
        # 掃描途中檔案被移走或鎖住會走到這裡:雲端同步資料夾、防毒隔離、
        # 外接碟被拔掉都算。這支全程唯讀,中斷不會留下壞檔;
        # 但要給讀者一句看得懂的話,而不是一整串 traceback。
        # 結束碼用 2,跟「路徑給錯了」的 1 分開,批次檔才判斷得出來。
        print(f'\n✗ 讀取途中出錯:{e}')
        print('  可能的原因:遊戲資料夾正在被別的程式動(雲端同步、防毒掃描),')
        print('  或是外接碟中途斷線。關掉那些程式、確認碟還在,再跑一次。')
        print('  本次檢查全程唯讀,沒有修改任何檔案。')
        sys.exit(2)

# ─────────────────────────────────────────────────────────
#  MIT License
#
#  Copyright (c) 2026 toni
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in
#  all copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
#  THE SOFTWARE.
# ─────────────────────────────────────────────────────────

📋 回報範本

卡住或做成了,複製下面這段填一填:

當機診斷 status: 跑完了 / 跑不起來
我的當機屬於哪一種(①記憶體 ②模組 ③自己改壞 ④系統設定): ___
診斷腳本印出來的重點(整段複製): ___
遊戲現在開得起來嗎: 開得起來 / 開不起來
我已經試過的: ___

接下來