Created
March 11, 2026 16:25
-
-
Save othercat/6a3913981a98e09fa3d9846c830a7544 to your computer and use it in GitHub Desktop.
清理仙剑1 MAP.mkf 末尾空 chunk 并对齐 GOP.mkf
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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