#!/usr/bin/env python3 # SPDX-License-Identifier: BSD-3-Clause # # fix-map-names - Fix map names in WAD files # # Fix the map name, which is the name of the first lump. If the "-t" option is # passed for test mode then incorrect map names are displayed, but not fixed. # # This script can be invoked with make target "fix-map-names" (no "-t" option) # or make target "test-map-names" ("-t" option). Make target "test" # ("-t" option) will run this and any other test. # Imports import argparse import os import re import struct import sys # Globals args = {} # Command line arguments. error_count = 0 fixes_needed = 0 freedoom_1_re = re.compile(r"^C(\d)M(\d)$") # FD #1 maps freedoom_dm_re = re.compile(r"^DM(\d\d)$") # FD DM maps header_shown = False ignored_wads = set(["dummy.wad", "test_levels.wad"]) last_error = None map_name_re = re.compile(r"^((E\dM\d)|(MAP\d\d))$") output_line = "%-17s %-9s %-7s %s" # Functions # Handle error 'msg'. Pass None to reset 'last_error'. def error(msg): global error_count global last_error last_error = msg if msg: error_count += 1 # Given WAD path 'wad' return the expected map name as a function of the # filename. def get_expected_map_name(wad): # Strip of the directory, upper case, remove ".wad". name = os.path.basename(wad).upper() if name.endswith(".WAD"): name = name[:-4] # Convert from Freedoom name to Doom names. name = freedoom_1_re.sub(r"E\1M\2", name) name = freedoom_dm_re.sub(r"MAP\1", name) if map_name_re.match(name): return name else: return None # Parse the command line arguments and store the result in 'args'. def parse_args(): global args parser = argparse.ArgumentParser( description="Fix map names in WAD files.", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) # The following is sorted by long argument. parser.add_argument( "-f", "--force", action="store_true", help="Force. Fix map name regardless of the existing map name.", ) parser.add_argument( "-q", "--quiet", action="store_true", help="Quiet (minimum output)." ) parser.add_argument( "-r", "--recursive", action="store_true", help="Recurse into directories.", ) parser.add_argument( "-t", "--test", action="store_true", help="Test mode. Don't make any changes.", ) parser.add_argument( "paths", metavar="PATH", nargs="+", help="WAD paths, files and directories.", ) args = parser.parse_args() return args # Process path 'path' which is at depth 'depth'. If 'depth' is 0 then this is # a top level path passed in on the command line. def process_path(path, depth): if os.path.isdir(path): # Directory. If not recursive then only consider this directory if it # was specified explicitly. if args.recursive or not depth: path_list = os.listdir(path) path_list.sort() for base in path_list: process_path(path + "/" + base, depth + 1) else: # File. Only process WAD files that were specified explicitly # (depth 0), or that have the expected suffix. if (not depth) or path.lower().endswith(".wad"): process_wad(path) # Process the paths passed in on the command line. def process_paths(): for path in args.paths: process_path(path, 0) # Process WAD path 'wad'. def process_wad(wad): global header_shown global last_error global fixes_needed if os.path.basename(wad).lower() in ignored_wads: # A known WAD that should not be processed. return try: # Reset everything. error(None) lump_name = None fix_needed = False expected_name = get_expected_map_name(wad) if not expected_name: raise Exception("Unable to get the expected name") with open(wad, "rb" if args.test else "r+b") as fhand: magic = fhand.read(4) if not isinstance(magic, str): # magic is bytes in Python 3. magic = magic.decode("UTF-8") if not magic == "PWAD": raise Exception("Not a PWAD. magic=" + magic) # Directory at offset 0x8 in the header. fhand.seek(0x08) directory_offset, = struct.unpack("