Skip to content

Instantly share code, notes, and snippets.

@othercat
Created March 11, 2026 16:25
Show Gist options
  • Select an option

  • Save othercat/6a3913981a98e09fa3d9846c830a7544 to your computer and use it in GitHub Desktop.

Select an option

Save othercat/6a3913981a98e09fa3d9846c830a7544 to your computer and use it in GitHub Desktop.
清理仙剑1 MAP.mkf 末尾空 chunk 并对齐 GOP.mkf
#!/usr/bin/env python3
"""
fix_mkf_pair.py - 清理 MAP.mkf 末尾空 chunk 并对齐 GOP.mkf
功能:
1. 检测 MAP.mkf 末尾连续的空 chunk(大小为0),将其清除
2. 对齐 GOP.mkf 的 chunk 数量与清理后的 MAP.mkf 一致
- GOP 不足则追加空 chunk
- GOP 末尾多余的空 chunk 则截掉
原则:
- 只清理末尾连续的空 chunk,中间的空 chunk 不动(保持编号不变)
- 清理前会检查:如果 SSS.mkf 中有场景引用了即将被删除的地图编号,则拒绝清理
用法:
python fix_mkf_pair.py MAP.mkf GOP.mkf [SSS.mkf]
指定 SSS.mkf 可进行安全校验(推荐)。
不指定则跳过校验直接清理。
输出:
MAP_fixed.mkf / GOP_fixed.mkf(同目录)
或使用 --in-place 直接覆盖(自动备份 .bak)
"""
import struct
import sys
import os
import shutil
# ============================================================
# MKF 基础操作
# ============================================================
def read_mkf_offsets(filepath: str) -> list[int]:
"""读取 MKF 文件的完整偏移表"""
with open(filepath, "rb") as f:
raw = f.read(4)
if len(raw) < 4:
raise ValueError(f"{filepath}: 文件过小,无法读取 MKF 头")
first_offset = struct.unpack("<I", raw)[0]
num_entries = first_offset // 4
if num_entries < 2:
raise ValueError(f"{filepath}: 偏移表条目数异常 ({num_entries})")
f.seek(0)
raw_offsets = f.read(first_offset)
if len(raw_offsets) < first_offset:
raise ValueError(f"{filepath}: 文件不完整,偏移表截断")
return list(struct.unpack(f"<{num_entries}I", raw_offsets))
def get_chunk_count(offsets: list[int]) -> int:
return len(offsets) - 1
def read_chunks(filepath: str, offsets: list[int]) -> list[bytes]:
"""读取 MKF 文件的所有 chunk 数据"""
chunks = []
with open(filepath, "rb") as f:
for i in range(get_chunk_count(offsets)):
start = offsets[i]
end = offsets[i + 1]
size = end - start
if size > 0:
f.seek(start)
data = f.read(size)
if len(data) != size:
raise ValueError(
f"{filepath}: chunk {i} 读取不完整 "
f"(期望 {size}, 实际 {len(data)})"
)
chunks.append(data)
else:
chunks.append(b"")
return chunks
def build_mkf(chunks: list[bytes]) -> bytes:
"""从 chunk 列表重建 MKF 文件"""
num_entries = len(chunks) + 1
header_size = num_entries * 4
offsets = []
current = header_size
for c in chunks:
offsets.append(current)
current += len(c)
offsets.append(current)
header = struct.pack(f"<{num_entries}I", *offsets)
body = b"".join(chunks)
return header + body
# ============================================================
# 末尾空 chunk 检测
# ============================================================
def count_trailing_empty(chunks: list[bytes]) -> int:
"""计算末尾连续空 chunk 的数量"""
count = 0
for i in range(len(chunks) - 1, -1, -1):
if len(chunks[i]) == 0:
count += 1
else:
break
return count
# ============================================================
# SSS.mkf 安全校验
# ============================================================
def read_scene_map_nums(sss_path: str) -> list[int]:
"""从 SSS.mkf chunk 1 中读取所有场景的 wMapNum"""
offsets = read_mkf_offsets(sss_path)
if get_chunk_count(offsets) < 2:
raise ValueError(f"{sss_path}: chunk 数量不足,无法读取场景数据")
with open(sss_path, "rb") as f:
start = offsets[1]
end = offsets[2]
size = end - start
f.seek(start)
data = f.read(size)
# 每条 SCENE = 8 字节: wMapNum(2) + wScriptOnEnter(2)
# + wScriptOnTeleport(2) + wEventObjectIndex(2)
scene_count = len(data) // 8
map_nums = []
for i in range(scene_count):
wMapNum = struct.unpack_from("<H", data, i * 8)[0]
map_nums.append(wMapNum)
return map_nums
def check_safety(map_nums: list[int], max_valid_chunk: int) -> list[int]:
"""
检查是否有场景引用了即将被删除的地图编号。
max_valid_chunk: 清理后保留的最大 chunk 编号(0-based),
即 chunk 0 ~ max_valid_chunk-1 会被保留。
返回冲突的场景索引列表。
"""
conflicts = []
for i, mnum in enumerate(map_nums):
if mnum >= max_valid_chunk and mnum != 0:
conflicts.append((i, mnum))
return conflicts
# ============================================================
# 主流程
# ============================================================
def main():
if len(sys.argv) < 3:
print("用法: python fix_mkf_pair.py <MAP.mkf> <GOP.mkf> [SSS.mkf] [--in-place]")
print()
print("示例:")
print(" python fix_mkf_pair.py MAP.mkf GOP.mkf SSS.mkf")
print(" python fix_mkf_pair.py MAP.mkf GOP.mkf SSS.mkf --in-place")
print(" python fix_mkf_pair.py MAP.mkf GOP.mkf (跳过安全校验)")
sys.exit(1)
map_path = sys.argv[1]
gop_path = sys.argv[2]
sss_path = None
in_place = False
for arg in sys.argv[3:]:
if arg == "--in-place":
in_place = True
else:
sss_path = arg
# 检查文件存在
for path in [map_path, gop_path]:
if not os.path.isfile(path):
print(f"错误: 文件不存在: {path}")
sys.exit(1)
if sss_path and not os.path.isfile(sss_path):
print(f"错误: 文件不存在: {sss_path}")
sys.exit(1)
# ── 第1步:读取 MAP.mkf ──
print(f"═══ 第1步:分析 MAP.mkf ═══")
print(f" 文件: {map_path}")
map_offsets = read_mkf_offsets(map_path)
map_chunks = read_chunks(map_path, map_offsets)
map_count = len(map_chunks)
print(f" chunk 数量: {map_count}")
trailing_empty = count_trailing_empty(map_chunks)
print(f" 末尾连续空 chunk 数量: {trailing_empty}")
if trailing_empty == 0:
print(f" MAP.mkf 末尾没有空 chunk,无需清理。")
target_count = map_count
else:
target_count = map_count - trailing_empty
print(f" 清理后目标 chunk 数量: {target_count}")
# ── 第2步:安全校验(如果提供了 SSS.mkf)──
if sss_path and trailing_empty > 0:
print(f"\n═══ 第2步:SSS.mkf 安全校验 ═══")
print(f" 文件: {sss_path}")
map_nums = read_scene_map_nums(sss_path)
scene_count = len(map_nums)
print(f" 场景记录数: {scene_count}")
conflicts = check_safety(map_nums, target_count)
if conflicts:
print(f"\n ❌ 发现 {len(conflicts)} 个场景引用了即将被删除的地图编号:")
for scene_idx, mnum in conflicts:
print(f" Scene[{scene_idx}] (场景#{scene_idx+1}) → wMapNum={mnum}")
print(f"\n 这些地图编号 >= {target_count},清理后将不存在。")
print(f" 拒绝清理。请先修复 SSS.mkf 中的场景引用。")
sys.exit(1)
else:
print(f" ✅ 所有场景的 wMapNum 都在安全范围内 (< {target_count})")
elif sss_path is None and trailing_empty > 0:
print(f"\n ⚠️ 未提供 SSS.mkf,跳过安全校验。")
# ── 第3步:清理 MAP.mkf ──
print(f"\n═══ 第3步:处理 MAP.mkf ═══")
if trailing_empty > 0:
map_chunks_new = map_chunks[:target_count]
print(f" 删除末尾 {trailing_empty} 个空 chunk: {map_count} → {target_count}")
else:
map_chunks_new = map_chunks
print(f" 无需修改,保持 {map_count} 个 chunk")
# ── 第4步:对齐 GOP.mkf ──
print(f"\n═══ 第4步:对齐 GOP.mkf ═══")
print(f" 文件: {gop_path}")
gop_offsets = read_mkf_offsets(gop_path)
gop_chunks = read_chunks(gop_path, gop_offsets)
gop_count = len(gop_chunks)
print(f" 当前 chunk 数量: {gop_count}")
print(f" 目标 chunk 数量: {target_count}")
if gop_count == target_count:
print(f" ✅ 已一致,无需修改。")
gop_chunks_new = gop_chunks
elif gop_count < target_count:
diff = target_count - gop_count
gop_chunks_new = gop_chunks + [b""] * diff
print(f" 追加 {diff} 个空 chunk: {gop_count} → {target_count}")
else:
# GOP 比目标多,尝试截掉末尾空 chunk
gop_trailing = count_trailing_empty(gop_chunks)
can_remove = gop_count - target_count
if can_remove <= gop_trailing:
gop_chunks_new = gop_chunks[:target_count]
print(f" 截掉末尾 {can_remove} 个空 chunk: {gop_count} → {target_count}")
else:
print(f" ❌ GOP.mkf 比目标多 {gop_count - target_count} 个 chunk,")
print(f" 但末尾只有 {gop_trailing} 个空 chunk 可安全删除。")
print(f" 无法自动对齐,请手动检查。")
sys.exit(1)
# ── 第5步:写入 ──
print(f"\n═══ 第5步:写入文件 ═══")
if in_place:
map_output = map_path
gop_output = gop_path
# 备份
if trailing_empty > 0:
bak = map_path + ".bak"
shutil.copy2(map_path, bak)
print(f" 备份: {map_path} → {bak}")
if gop_count != target_count:
bak = gop_path + ".bak"
shutil.copy2(gop_path, bak)
print(f" 备份: {gop_path} → {bak}")
else:
dirname = os.path.dirname(map_path)
map_output = os.path.join(dirname, "MAP_fixed.mkf")
gop_output = os.path.join(dirname, "GOP_fixed.mkf")
# 写入 MAP
map_changed = trailing_empty > 0
if map_changed:
map_data = build_mkf(map_chunks_new)
with open(map_output, "wb") as f:
f.write(map_data)
print(f" MAP → {map_output} ({len(map_data)} 字节, {len(map_chunks_new)} chunks)")
else:
if not in_place:
shutil.copy2(map_path, map_output)
print(f" MAP 未修改")
# 写入 GOP
gop_changed = gop_count != target_count
if gop_changed:
gop_data = build_mkf(gop_chunks_new)
with open(gop_output, "wb") as f:
f.write(gop_data)
print(f" GOP → {gop_output} ({len(gop_data)} 字节, {len(gop_chunks_new)} chunks)")
else:
if not in_place:
shutil.copy2(gop_path, gop_output)
print(f" GOP 未修改")
# ── 第6步:验证 ──
print(f"\n═══ 第6步:验证 ═══")
v_map = read_mkf_offsets(map_output if map_changed or not in_place else map_path)
v_gop = read_mkf_offsets(gop_output if gop_changed or not in_place else gop_path)
v_map_count = get_chunk_count(v_map)
v_gop_count = get_chunk_count(v_gop)
print(f" MAP chunk 数量: {v_map_count}")
print(f" GOP chunk 数量: {v_gop_count}")
if v_map_count == v_gop_count:
print(f" ✅ MAP 与 GOP chunk 数量一致: {v_map_count}")
else:
print(f" ❌ 数量不一致! MAP={v_map_count}, GOP={v_gop_count}")
sys.exit(1)
# 验证原有数据完整性
if map_changed:
v_map_chunks = read_chunks(map_output, v_map)
for i in range(target_count):
if v_map_chunks[i] != map_chunks_new[i]:
print(f" ❌ MAP chunk {i} 数据不一致!")
sys.exit(1)
print(f" ✅ MAP 所有 {target_count} 个 chunk 数据完整")
if gop_changed:
v_gop_chunks = read_chunks(gop_output, v_gop)
orig_preserved = min(gop_count, target_count)
for i in range(orig_preserved):
if v_gop_chunks[i] != gop_chunks[i]:
print(f" ❌ GOP chunk {i} 数据不一致!")
sys.exit(1)
print(f" ✅ GOP 原有 {orig_preserved} 个 chunk 数据完整")
# ── 总结 ──
print(f"\n═══ 总结 ═══")
print(f" MAP: {map_count} → {v_map_count} chunks"
+ (f" (删除末尾 {trailing_empty} 个空 chunk)" if trailing_empty > 0 else " (未修改)"))
if gop_count < target_count:
print(f" GOP: {gop_count} → {v_gop_count} chunks (追加 {target_count - gop_count} 个空 chunk)")
elif gop_count > target_count:
print(f" GOP: {gop_count} → {v_gop_count} chunks (截掉 {gop_count - target_count} 个空 chunk)")
else:
print(f" GOP: {gop_count} → {v_gop_count} chunks (未修改)")
print(f" ✅ 完成!")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment