速查 › 壓縮格式
QFS / RefPack 壓縮
從 .big 封裝檔取出來的東西,很多還包著一層壓縮。 這是 EA 自家的 RefPack(也常被叫做 QFS)。解開它不難, 而且要壓回去時有個技巧,可以完全不用寫壓縮器。
怎麼辨識
看第二個位元組是不是 0xFB。是就是 RefPack。
10 FB 01 48 D7 E5 ... ← 壓縮過
44 41 54 45 2C 54 ... ← 沒壓縮(這是「DATE,T」的 ASCII)
同一個封裝檔裡壓縮和未壓縮可以混在一起。
這是 EA 出廠就有的:剛安裝好的原版 207 個封裝檔裡有 106 個是混合的 ——
例如 ingame.big 的 44 項中,43 項壓縮、1 項(alib_ingamelogos.fel)是純文字。
所以工具要逐項判斷,不能假設「這個 .big 裡全都壓縮」。
我們改過這一段
這裡原本舉的例子是「schedule.big 的 9 個賽程表中,6 個壓縮、3 個是純文字」。
那是本站測試機的狀況 —— 拿剛安裝好的原版一驗:
原版那 9 個項目全部都是壓縮的,一個純文字都沒有。
差別是本站這台的 schedule.big 被工具改寫過。那 3 個純文字項目,
正好是換賽程年份那一課會動到的
mlb162_1/2/3.dat。
順帶學到一件事:原本壓縮的項目,被工具改寫之後可能變成未壓縮存回去。 規則本身沒錯,錯的是拿一個被改過的檔當例子。
檔頭
byte[0] 旗標 & 0x01 決定檔頭長度
byte[1] 0xFB 辨識碼
旗標 & 0x01 == 0 → 檔頭 5 bytes:byte[2..4] 是解壓後大小(3 bytes,big-endian)
旗標 & 0x01 == 1 → 檔頭 10 bytes:壓縮後大小 + 解壓後大小(各 4 bytes,big-endian)
遊戲裡絕大多數是前者,所以你最常看到的開頭就是 10 FB 加 3 個位元組的大小。
實際上本站在兩份安裝上量到的是一個 10 bytes 的檔頭都沒有:
剛安裝好的原版 14,816 個壓縮項目、本站測試機 36,620 個,第一個位元組全部是 0x10。
所以上面那一行 10 bytes 的寫法,本站沒有樣本可以驗 —— 它不是本站量到的。
四種指令
解壓就是一直讀指令、往輸出區塞資料。指令用第一個位元組的值域來區分:
| byte0 範圍 | 指令 | 指令長度 | 複製長度/資料量 | 回看距離 |
|---|---|---|---|---|
| < 0x80 | 短距離複製 | 2 bytes | 3 – 10 | 1 – 0x400 |
| 0x80 – 0xBF | 中距離複製 | 3 bytes | 4 – 67 | 1 – 0x4000 |
| 0xC0 – 0xDF | 長距離複製 | 4 bytes | 5 – 1028 | 1 – 0x20000 |
| 0xE0 – 0xFB | 純資料 | 1 byte | 4 – 112(每 4 遞增) | — |
| ≥ 0xFC | 結束 | 1 byte | 0 – 3(尾端資料) | — |
後兩欄只有複製指令兩欄都有值。純資料與結束不往回看,
它們那一格填的是一次帶出多少位元組,不是距離 —— 對照下面純資料指令的算式
n = ((byte0 & 0x1F) << 2) + 4,byte0 落在 0xE0–0xFB 時剛好是 4 到 112、每 4 遞增。
每種複製指令都同時可以帶 0 到 3 個「順便寫出去」的位元組。實際算式:
# 2-byte 指令
n = byte0 & 0x03
length = ((byte0 & 0x1C) >> 2) + 3
offset = ((byte0 & 0x60) << 3) + byte1 + 1
# 3-byte 指令
n = (byte1 >> 6) & 0x03
length = (byte0 & 0x3F) + 4
offset = ((byte1 & 0x3F) << 8) + byte2 + 1
# 4-byte 指令
n = byte0 & 0x03
length = ((byte0 & 0x0C) << 6) + byte3 + 5
offset = ((byte0 & 0x10) << 12) + (byte1 << 8) + byte2 + 1
# 純資料指令
n = ((byte0 & 0x1F) << 2) + 4
「回看距離」的意思是:往已經解出來的資料倒退 N 個位元組,從那裡複製過來。 這也是為什麼解壓一定要從頭循序做,不能跳著解。
壓回去的技巧:純資料編碼
要把改好的內容壓回 RefPack,直覺是得寫一個壓縮器 —— 找重複字串、選最佳指令。 那很慢也容易寫錯。
但格式允許你完全不做匹配搜尋
純資料指令(0xE0 系列)可以表達任意內容。
整份資料全部用純資料指令輸出,格式一樣合法,遊戲照樣讀得懂。
def qfs_compress_literal(data):
n = len(data)
out = bytearray([0x10, 0xFB, (n >> 16) & 0xFF, (n >> 8) & 0xFF, n & 0xFF])
tail = n % 4 # 0~3,交給結束指令帶走
body = n - tail
pos = 0
while pos < body:
chunk = min(112, body - pos) # 必為 4 的倍數,上限 112
out.append(0xE0 | ((chunk - 4) // 4)) # 0xE0~0xFB,不會撞到 0xFC
out += data[pos:pos + chunk]
pos += chunk
out.append(0xFC | tail) # 結束標記
out += data[pos:]
return bytes(out)
代價與效益
| 做法 | 47 個檔耗時 (本站測試機的 ingame.big,原版那顆是 44 項) | 檔案大小(單例) | 風險 |
|---|---|---|---|
| 完整壓縮器(貪婪匹配) | > 120 秒 | 11.6 KB | 壓縮器寫錯就毀檔 |
| 純資料編碼 | 0.11 秒 | 85 KB | 幾乎沒有,邏輯只有十行 |
換來的檔案大一些,但對本站測試機那顆 2.5 MB 的封裝檔來說只多 3%,而且速度差了一千倍。 本站所有教學腳本都用這個做法。
驗證方式:272 種不同長度的資料各壓一次,逐一檢查每個控制位元組是否落在規格允許的值域, 再用另一套獨立寫的解壓器解回來比對 —— 全部通過。
寫回封裝檔時仍要遵守 append 鐵律
壓好之後別忘了:接到 .big 檔尾、只改目錄與檔頭, 不要整包重新打包。原因見 BIGF 格式頁。
實測案例
⚠️ 下面第一列的 fes_ingameinfobar.fel,以及上面「怎麼辨識」那段
10 FB 01 48 D7 E5(就是它的前六個位元組),都取自本站測試機的
ingame.big。剛安裝好的原版沒有這個項目 ——
它是社群模組在 44 項之外多加的三個之一(另外兩個是 fes_ingameboxscore2.fel
與 fes_ingamepauseinfo1.fel)。a140_1.dat 那一列同理:
本站測試機是 13,138 解成 57,304,原版是 11,582 解成 54,581。
mini1_en.ffn 與 mlbspr_1.dat 兩列則是兩份安裝上逐位元組相同的。
| 檔案 | 壓縮後 | 解壓後 | 比例 |
|---|---|---|---|
| fes_ingameinfobar.fel | 11,617 | 84,183 | 7.2× |
| mini1_en.ffn(字型) | 12,329 | 133,216 | 10.8× |
| a140_1.dat(賽程) | 13,138 | 57,304 | 4.4× |
| mlbspr_1.dat(春訓) | 2,272 | 8,749 | 3.9× |
📌 重點整理
- 一句話 從
.big取出來的東西很多還包著一層 EA 自家的 RefPack(也叫 QFS),解開它不難,而且要壓回去時有個技巧,可以完全不用寫壓縮器。 - 怎麼辨識,以及不能做的假設 看第二個位元組是不是
0xFB,是就是 RefPack。同一個封裝檔裡壓縮和未壓縮可以混在一起,這是 EA 出廠就有的:剛安裝好的原版 207 個封裝檔裡有 106 個是混合的,例如ingame.big的 44 項中 43 項壓縮、1 項是純文字。所以工具要逐項判斷,不能假設「這個.big裡全都壓縮」。 - 本站改過這一段 這裡原本舉的例子是「
schedule.big的 9 個賽程表中 6 個壓縮、3 個是純文字」,那是本站測試機的狀況。拿剛安裝好的原版一驗,那 9 個項目全部都是壓縮的,一個純文字都沒有,而那 3 個純文字項目正好是換賽程年份那一課會動到的檔。規則本身沒錯,錯的是拿一個被改過的檔當例子,順帶學到:原本壓縮的項目被工具改寫之後,可能變成未壓縮存回去。 - 格式本身 檔頭看第一個位元組的旗標,
& 0x01是 0 就 5 bytes、是 1 就 10 bytes(10 bytes 那一種本站沒有樣本可以驗:兩份安裝合計 51,436 個壓縮項目,第一個位元組全部是0x10),遊戲裡絕大多數是前者,所以最常看到的開頭就是10 FB加 3 個位元組的大小。解壓的部分是四種指令加一個結束標記,用第一個位元組的值域區分:< 0x80短距離、0x80到0xBF中距離、0xC0到0xDF長距離、0xE0到0xFB純資料、≥ 0xFC結束。因為複製指令是往已經解出來的資料倒退取用,所以解壓一定要從頭循序做,不能跳著解。 - 不用寫壓縮器的技巧 純資料指令(
0xE0系列)可以表達任意內容,整份資料全部用純資料指令輸出,格式一樣合法。代價與效益差距很大:完整壓縮器 47 個檔(本站測試機的ingame.big,原版那顆是 44 項)要 120 秒以上、單例 11.6 KB,純資料編碼只要 0.11 秒、單例 85 KB。檔案是大了一些,但對本站測試機那顆 2.5 MB 的封裝檔來說只多 3%,速度差了一千倍,而且邏輯只有十行、幾乎不會寫錯。本站所有教學腳本都用這個做法。 - 壓好之後還有一條鐵律 接到
.big檔尾、只改目錄與檔頭,不要整包重新打包。實測的壓縮比可以拿來對照自己的結果:fes_ingameinfobar.fel11,617 變 84,183(7.2 倍)、字型mini1_en.ffn12,329 變 133,216(10.8 倍)、賽程a140_1.dat13,138 變 57,304(4.4 倍)、春訓mlbspr_1.dat2,272 變 8,749(3.9 倍)。第一列與賽程那一列只在本站測試機成立:剛安裝好的原版沒有fes_ingameinfobar.fel,a140_1.dat是 11,582 變 54,581;字型與春訓兩列則是兩份安裝相同。 - 本站沒驗的 這一頁列出的驗證全部停在檔案層面:272 種不同長度的資料各壓一次,逐一檢查每個控制位元組是否落在規格允許的值域,再用另一套獨立寫的解壓器解回來比對,全部通過。頁面上沒有列出任何「在遊戲裡跑起來」的測試。另外,檔頭那一行「旗標 & 0x01 == 1 → 10 bytes」本站在兩份安裝上都取不到樣本,不是量到的。那次訂正也是一個提醒:本站測試機上的檔案跟剛安裝好的原版不一樣,看到跟本頁對不起來的數字,先確認你手上那份被工具改寫過沒有。