freedoom/scripts/fix-map-names

241 lines
7.4 KiB
Python
Executable File

#!/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("<I", fhand.read(4))
fhand.seek(directory_offset)
# The first lump in the directory, which should be the 0 byte map
# name one.
lump_data_offset, lump_size, lump_name = struct.unpack(
"<II8s", fhand.read(16)
)
if not isinstance(lump_name, str):
# lump_name is bytes in Python 3.
lump_name = lump_name.decode("UTF-8")
# Get rid of the null suffix.
lump_name = lump_name.partition("\0")[0]
if lump_size:
# The first lump should be 0 bytes.
error(
"First lump size non-zero with "
+ str(lump_size)
+ " bytes"
)
elif not (args.force or map_name_re.match(lump_name)):
# A sanity check to make sure we read the right part.
error("Actual name unexpected")
elif expected_name != lump_name:
# The name is not what we thought it should be.
fix_needed = True
fixes_needed += 1
if fix_needed and not args.test:
# Seek to the lump name and the overwrite the lump name with
# the expected name.
fhand.seek(directory_offset + 8)
fhand.write(struct.pack("8s", expected_name.encode("UTF-8")))
except IOError as err:
# Probably the WAD file couldn't be open for read (test) or read and
# write (default).
error(
"Unable to open for read"
+ ("" if args.test else " and write")
+ ": "
+ str(err)
)
except struct.error as err:
# This is probably the reason since seek silently succeeds even when
# the location is not possible, but then unpack fails due to the short
# read.
error("File too small: " + str(err))
except Exception as err:
# This was probably explicitly thrown by this script.
error(str(err))
if (last_error or fix_needed) and not args.quiet:
# Map None to "".
expected_name, lump_name, last_error = [
x if x else "" for x in (expected_name, lump_name, last_error)
]
if not header_shown:
print(output_line % ("WAD", "Expected", "Actual", "Error"))
print(output_line % ("---", "--------", "------", "-----"))
header_shown = True
print(output_line % (wad, expected_name, lump_name, last_error))
# Summarize what happened, and then exit with the appropriate exit code.
def summarize():
if not args.quiet:
if fixes_needed:
print(
"\n%s %d WADs with the incorrect map name."
% ("Found" if args.test else "Fixed", fixes_needed)
)
else:
print("\nAll WADs had the correct map name.")
if error_count:
print("There were %d errors." % error_count)
sys.exit(1 if (error_count or (args.test and fixes_needed)) else 0)
# Main
parse_args()
process_paths()
summarize()