存档解析
约 1062 字大约 4 分钟
2026-01-13
免责声明:本教程仅用于学习丝之歌游戏的代码,请勿用于商业用途
本文将主要介绍如何解析游戏的存档数据
想要查看更多的关于修改的内容,可以看存档修改
原理解析
游戏存档是一个二进制文件,后缀为 .dat
从 .dat 最终得到 JSON ,实际上经过了 二进制 → 提取字符串 → 解密 → JSON 的过程
提示
实际上游戏存档的解析功能可以由AI实现,关键点在于了解游戏是如何加密的,提供游戏的二进制数据和解析规则即可
下面是详细的解析过程,使用js代码实现
1. 把 .dat 读成字节数组
- 选择文件后触发
datFile的 change 事件 file.arrayBuffer()读到原始二进制,然后new Uint8Array(buffer)变成字节数组raw
document.getElementById("datFile").addEventListener("change", async (ev) => {
clearStatus(els.datStatus);
clearStatus(els.datMeta);
lastDatMeta = null;
els.extractedEncoded.value = "";
els.datJson.value = "";
try {
const file = ev.target.files && ev.target.files[0];
if (!file) return;
const buffer = await file.arrayBuffer();
const raw = new Uint8Array(buffer);
const extracted = extractBinaryFormatterString(raw);
lastDatMeta = extracted.meta;
els.extractedEncoded.value = extracted.encoded;
const cfg = getCryptoConfig();
const jsonText = decryptText(extracted.encoded, cfg);
els.datJson.value = tryPrettyJson(jsonText);
setStatus(
els.datMeta,
"ok",
[
`文件大小: ${raw.length} bytes`,
`header: ${extracted.meta.header.length} bytes`,
`stringStart: ${extracted.meta.stringStart}`,
`stringEnd: ${extracted.meta.stringEnd}`,
`footer: ${extracted.meta.footer.length} bytes`,
].join("\n")
);
setStatus(els.datStatus, "ok", "导入解析完成");
} catch (e) {
setStatus(els.datStatus, "err", String(e && e.message ? e.message : e));
}
});2. 从二进制里截取编码串(Base64文本)
extractBinaryFormatterString(raw)
function extractBinaryFormatterString(raw) {
for (let i = 0; i < raw.length; i++) {
if (raw[i] !== STRING_TOKEN) continue;
const strlenOffset = i + 5;
if (strlenOffset >= raw.length) continue;
const lenInfo = read7bitEncodedInt(raw, strlenOffset);
const strlen = lenInfo.length;
const stringStart = lenInfo.offset;
const stringEnd = stringStart + strlen;
if (stringEnd > raw.length) continue;
const utf8Bytes = raw.slice(stringStart, stringEnd);
const decoded = new TextDecoder("utf-8").decode(utf8Bytes);
return {
encoded: decoded,
meta: {
header: raw.slice(0, strlenOffset),
footer: raw.slice(stringEnd),
strlenOffset,
stringStart,
stringEnd,
},
};
}
throw new Error("未找到 BinaryFormatter 字符串对象(0x06)");
}- 做法是:
- 从头扫描字节,找到 token
0x06 - 计算
strlenOffset = i + 5,从这个位置开始读取“7-bit 变长整数”作为字符串长度:read7bitEncodedInt
function read7bitEncodedInt(raw, pos) { let result = 0; let shift = 0; let offset = pos; while (true) { const b = raw[offset++]; result |= (b & 0x7f) << shift; if ((b & 0x80) === 0) break; shift += 7; if (shift > 35) throw new Error("7-bit length overflow"); } return { length: result, offset }; }- 按长度切出那段 UTF-8 字节,再
TextDecoder("utf-8")解码成 JS 字符串decoded,就是需要的“编码串”
const utf8Bytes = raw.slice(stringStart, stringEnd); const decoded = new TextDecoder("utf-8").decode(utf8Bytes); - 从头扫描字节,找到 token
- 同时它还保存了
header和footer,用于后面导出时保持文件其它结构不变
return {
encoded: decoded,
meta: {
header: raw.slice(0, strlenOffset),
footer: raw.slice(stringEnd),
strlenOffset,
stringStart,
stringEnd,
},
};3. 把“编码串”解密成 JSON 文本
- 先从界面读取当前配置(算法、key、编码方式):
getCryptoConfig()
function getCryptoConfig() {
const algo = document.getElementById("cryptoAlgo").value;
const keyText = document.getElementById("cryptoKey").value;
const encoding = document.getElementById("cipherEncoding").value;
return { algo, keyText, encoding };
}decryptText(extracted.encoded, cfg)
function decryptText(cipherText, cfg) {
if (cfg.algo === "none") {
if (cfg.encoding === "base64") return decodeBase64ToUtf8(cipherText);
if (cfg.encoding === "base64url") return decodeBase64ToUtf8(base64UrlToBase64(cipherText));
if (cfg.encoding === "hex") return utf8FromHex(cipherText);
throw new Error("未知输入编码");
}
if (!window.CryptoJS) throw new Error("CryptoJS library is missing.");
if (cfg.algo === "aes-ecb-pkcs7") {
const ciphertextWordArray = parseCiphertextToWordArray(cipherText, cfg.encoding);
const key = CryptoJS.enc.Utf8.parse(cfg.keyText);
const decrypted = CryptoJS.AES.decrypt({ ciphertext: ciphertextWordArray }, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
});
return decrypted.toString(CryptoJS.enc.Utf8);
}
throw new Error("未支持的解密方式");
}- 如果选择的是 AES-ECB/Pkcs7:
- 先把 Base64/Hex/Base64URL 文本还原成“密文字节”(CryptoJS 的 WordArray):
parseCiphertextToWordArray
function parseCiphertextToWordArray(cipherText, encoding) { if (encoding === "base64") return CryptoJS.enc.Base64.parse(cipherText); if (encoding === "base64url") return CryptoJS.enc.Base64.parse(base64UrlToBase64(cipherText)); if (encoding === "hex") return CryptoJS.enc.Hex.parse(cipherText); throw new Error("未知输入编码"); }- 再用
CryptoJS.AES.decrypt+ECB+Pkcs7+ key 解出 UTF-8 明文字符串
if (cfg.algo === "aes-ecb-pkcs7") { const ciphertextWordArray = parseCiphertextToWordArray(cipherText, cfg.encoding); const key = CryptoJS.enc.Utf8.parse(cfg.keyText); const decrypted = CryptoJS.AES.decrypt({ ciphertext: ciphertextWordArray }, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7, }); return decrypted.toString(CryptoJS.enc.Utf8); } - 先把 Base64/Hex/Base64URL 文本还原成“密文字节”(CryptoJS 的 WordArray):
- 解密出来的结果在这里被命名为
jsonText
const cfg = getCryptoConfig();
const jsonText = decryptText(extracted.encoded, cfg);
els.datJson.value = tryPrettyJson(jsonText);4. 把 JSON 文本变成“可编辑、格式化”的 JSON
- 这里严格来说分两步:
JSON.parse(jsonText)验证它确实是合法 JSONJSON.stringify(obj, null, 2)变成带缩进的格式
- 代码里封装成
tryPrettyJson(text)
function tryPrettyJson(text) {
const obj = JSON.parse(text);
return JSON.stringify(obj, null, 2);
}- 最后把格式化后的 JSON 放进右侧文本框
datJson
els.datJson.value = tryPrettyJson(jsonText);总结
- 按照 BinaryFormatter 结构把
.dat里藏着“加密后的编码字符串”取出来 - 按照选择的编码(Base64/Hex)还原密文
- 用 AES+key 解密得到 JSON 字符串
- JSON.parse 成对象并格式化显示
