From eeb52c5f1ce5b600b5e82aad080dc8e4d5ce6c73 Mon Sep 17 00:00:00 2001 From: Steven Elliott Date: Sat, 5 Jun 2021 17:55:41 -0400 Subject: [PATCH] build: Add test-vanilla-compliance The test-vanilla-compliance build target (also invoked by the "test" build target) and script can be used to test the level WAD files for vanilla compliance. Also, a subset of the errors found can be fixed by the "fix" build target. To summarize: make test # Run all tests make test-vanilla-compliance # Run test-vanilla-compliance make fix # Run all fixes make fix-vanilla-compliance # Run test-vanilla-compliance -f --- Makefile | 37 +- README.adoc | 5 + scripts/fix-map-names | 240 ------------ scripts/test-vanilla-compliance | 667 ++++++++++++++++++++++++++++++++ 4 files changed, 702 insertions(+), 247 deletions(-) delete mode 100755 scripts/fix-map-names create mode 100755 scripts/test-vanilla-compliance diff --git a/Makefile b/Makefile index bcd3cf74..d8a5a31d 100644 --- a/Makefile +++ b/Makefile @@ -145,22 +145,45 @@ clean: $(MAKE) -C lumps/textures clean $(MAKE) -C manual clean -# Test targets all of which are a dependency of "test". +# Test targets some of which are a dependency of "test". -# Test that WAD files have the expected map names. +# Test that the level WAD files have the expected map names. test-map-names: - scripts/fix-map-names -t levels + scripts/test-vanilla-compliance -n levels + +# Test that the level WAD files have vanilla compliance. This is a superset of +# the "test-map-names" build target. +test-vanilla-compliance: + scripts/test-vanilla-compliance levels # Run all tests. Add test-* targets above, and then as a dependency here. -test: test-map-names +test: test-vanilla-compliance @echo @echo "All tests passed." -# Non-test targets that run scripts in the "scripts" directory. +# Fix targets some of which are a dependency of "fix". -# Fix the map names. +# Fix the level WAD files so that they have the expected map names. fix-map-names: - scripts/fix-map-names levels + scripts/test-vanilla-compliance -fn levels + +# Fix the level WAD files so that they have vanilla compliance. This is a +# superset of the "fix-map-names" build target. +fix-vanilla-compliance: + scripts/test-vanilla-compliance -f levels + +# TODO: I'm not sure we want to run this routinely, but I thought I'd put it +# here for completeness. Currently it makes a lot of changes to buildcfg.txt +# that don't have an obvious impact. Consequently "fix" does not depend on this +# target. Just delete this TODO and target if we don't want this. Maybe add a +# proper description in any case. +fix-gfx-offsets: + scripts/fix-gfx-offsets sprites/*.png + +# Run all fixes. Add fix-* targets above, and then as a dependency here. +fix: fix-vanilla-compliance + @echo + @echo "All fixable errors fixed." # Rebuild the nodes for the level WADs. By default this invokes "ZenNode" on # all 100 level WADs. Override the "NODE_BUILDER" prefixed variables to diff --git a/README.adoc b/README.adoc index 5c9eddea..7ca6e877 100644 --- a/README.adoc +++ b/README.adoc @@ -162,6 +162,11 @@ It is sensible to also heed the following guidelines: is an engine with strict adherence to vanilla Doom limits and bugs, and working in it assures that levels can be played with any _Doom_ engine. + * Use a Doom editor to check for errors. In + http://eureka-editor.sourceforge.net/[Eureka] it's possible to + check for errors with the Check / All menu, or by pressing `F9`. + * If possible run `make test` and fix any errors found. Note that + some of the errors can be fixed by `make fix`. === Graphics diff --git a/scripts/fix-map-names b/scripts/fix-map-names deleted file mode 100755 index 1d12d98d..00000000 --- a/scripts/fix-map-names +++ /dev/null @@ -1,240 +0,0 @@ -#!/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("", "THINGS", "LINEDEFS", "SIDEDEFS", + "VERTEXES", "SEGS", "SSECTORS", "NODES", "SECTORS", "REJECT", + "BLOCKMAP") + +# The expected number of lumps. +standard_lump_names_len = len(standard_lump_names) + +# Used to phrase message correctly. Future is used for test mode, and past for +# fix mode. +tense = "" + +# The total number of errors or fixes returned by do_* methods. +total_do_count = 0 + +# A count of unexpected errors. These are errors other than what this script +# intends to check for, such as the file not being readable. These errors +# suggest that something is fundamentally wrong, and that the results should +# not be trusted. +unexpected_count = 0 + +# Maps that can have secret exits. A secret exit in maps other than these is +# an error. +may_have_secret_exit = { "E1M3", "E2M5", "E3M6", "E4M2", "MAP15", "MAP31" } + +# Number of WADs that had errors or fixes seen as indicated by the do_* methods. +wad_error_fix_count = 0 + +# Things. Specifies what is allowed for things. + +# Allowed values for Ultimate Doom (Doom 1 in doom.wad) from this list of +# editor numbers. See +# https://zdoom.org/wiki/Standard_editor_numbers +allowed_things_doom = { + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, + 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, 62, 63, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2010, 2011, + 2012, 2013, 2014, 2015, 2018, 2019, 2022, 2023, 2024, 2025, 2026, 2028, + 2035, 2045, 2046, 2047, 2048, 2049, 3001, 3002, 3003, 3004, 3005, 3006 } + +# Doom 2 editor numbers are Doom 1 (the above) with additional Doom 2 monsters. +allowed_things_doom2 = allowed_things_doom.union(set(range(64, 90))) + +# Allowed things as a function of the doom version. +allowed_things_all = { 1: allowed_things_doom, 2: allowed_things_doom2 } + +# A reference to the current allowed things in allowed_things_all. It's set +# with each map change. +allowed_things = None + +# A mask of the allowed spawn flag bits. See +# https://zdoom.org/wiki/Thing#Spawn_flags +allowed_things_spawn_flags = 0x1f # [0, 32) + +# Linedefs. Specifies what is allowed for linedefs. + +# A mask of the allowed linedef flag bits. See +# https://zdoom.org/wiki/Linedef#Linedef_flags +allowed_linedefs_flags = 0x1ff # [0, 512) + +# The allowed linedef types. This is the "Reg", meaning regular, linedef types +# taken from this documentation: +# https://soulsphere.org/projects/boomref/boomref.txt +allowed_linedefs_types = set(range(142)) # Exclude 78? + +# The subset of allowed_linedefs_types that leads to secret maps. +allowed_linedefs_types_to_secret = { 51, 124 } + +# Sectors. Specifies what is allowed for sectors. + +# The allowed sector types. See +# https://doomwiki.org/wiki/Sector#Doom +allowed_sectors_types = { 0, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17 } + +# Classes + +# The error exception class for this script. +class TestException(Exception): + pass + +# Functions. Alphabetical. + +# The following do_* methods take action (test or fix). For them the term "region" +# means a contiguous set of bytes in a WAD file that consists of some structure, +# and "item" means one instance of that repeated structure. + +# Process a directory item. +def do_directory(messages, number, offset, size, name): + # We don't have an expectation for lump names for IWADs. For --name-only + # only process the first lump (the name lump at number == 0). + first_lump_only = args.name_only or args.fix + if (not args.iwad) and ((not first_lump_only) or (number == 0)): + fix_name = get_expected_lump_name(number) + if fix_name and (name != fix_name): + orig_name = name + if number == 0: + fixable = ", but " + tense + " fixed with " + fix_name + if args.fix: + name = fix_name + allowed = " is not allowed" + else: + fixable = " and can not be fixed" + allowed = " is at the wrong index" if name in standard_lump_names \ + else " is not allowed" + messages.append("Lump name %s%s%s." % (orig_name, allowed, fixable)) + # It's not clear where to put this error message that concerns the + # entire directory (not just one entry), so add it to the first entry. + if (not first_lump_only) and (number == 0) and ( + dir_count != standard_lump_names_len): + messages.append("Expected %d lumps, but found %d lumps." % ( + standard_lump_names_len, dir_count)) + + return (offset, size, name) + + +# Process a LINEDEFS item. +def do_linedef(messages, number, bvert, evert, flags, linet, stag, rsdef, lsdef): + # Flag bit 0 through 8 are allowed. + if option_linedef_flags: + fix_flags = allowed_linedefs_flags & flags + if flags != fix_flags: + messages.append("Flags %d is not allowed, but %s fixed with %d." % ( + flags, tense, fix_flags)) + if args.fix: + flags = fix_flags + + # There's no obvious automated way of fixing line types. + if (linet not in allowed_linedefs_types) and not args.fix: + messages.append("Type %s is not allowed and can not be fixed." % linet) + + # Only allow secret exits on the correct maps since that behavior is hard + # coded in the engine. A possible fix would be to map to the equivalent + # non-secret exit type, but it's probably better to evaluate it and replace + # it in order to avoid multiple exits. + if (linet in allowed_linedefs_types_to_secret) and not is_may_have_secret_exit: + messages.append("Type %s is a secret exit, but secret exits are only " + "allowed on maps %s. Not fixable." % ( + linet, may_have_secret_exit)) + + return (bvert, evert, flags, linet, stag, rsdef, lsdef) + + +# Process (do) a region. A region can be either a lump, or some other non-lump +# array of structures (the directory). Note that unlike the other do_* +# methods this method process the entire region, not just one item in it. +def do_region(region_name, map_name, wad, fhand, lump_offset, lump_size): + info = region_info.get(region_name) + if info is None: + # Nothing is supported for this region type. + return 0 + old_pos = fhand.tell() + fhand.seek(lump_offset) + unpack_format, field_names_part, row_format_part, do_item = info + unpack_size = struct.calcsize(unpack_format) + field_names = ("Region", "Map", "Num") + field_names_part + ( + " Fixes" if args.fix else " Errors",) + row_format = "%" + str(len(region_name)) + "s %5s %5d " + \ + row_format_part + "%s" + header_format = row_format.replace("d", "s") + header = header_format % field_names + + count = 0 # Count of errors or fixes. + offset = 0 + num = 0 + first = len(region_header_shown) == 0 + while offset < lump_size: + bytes = fhand.read(unpack_size) + fields = wad_unpack(unpack_format, bytes) + messages = [] + fields = do_item(messages, num, *fields) + if args.fix and len(messages) > 0: + fhand.seek(fhand.tell() - unpack_size) + bytes = wad_pack(unpack_format, *fields) + fhand.write(bytes) + if args.verbose or len(messages) > 0: + if region_name not in region_header_shown: + region_header_shown[region_name] = set() + shown = region_header_shown[region_name] + if wad not in shown: + if not first: + print("") + print("Process %s for %s in %s" % (region_name, map_name, wad)) + print(header) + shown.add(wad) + row = (region_name, map_name, num) + fields + ( + get_combined_message(messages),) + print(row_format % row) + count += len(messages) + offset += unpack_size + num += 1 + fhand.seek(old_pos) + return count + + +# Process a SECTORS item. +def do_sector(messages, number, fhght, chght, ftext, ctext, light, typ, tag): + # There's no obvious automated way of fixing sector types. + if (typ not in allowed_sectors_types) and not args.fix: + messages.append("Type %s is not allowed and can not be fixed." % typ) + + return (fhght, chght, ftext, ctext, light, typ, tag) + + +# Process a THINGS item. +def do_thing(messages, number, x, y, angle, typ, spawn_flags): + # In the following sections each check one field within the thing. They are + # in file order. + + # Make sure that the angle is in range [0, 360) and that it is a + # multiple of 45. The "//" rounds down, so the " + 22" makes it round + # off to the nearing multiple of 45. The "% 360" gets it to range [0, 360). + if option_thing_angle: + fix_angle = (45 * ((angle + 22) // 45)) % 360 + if angle != fix_angle: + messages.append("Angle %d is not allowed, but %s fixed with %d." % ( + angle, tense, fix_angle)) + if args.fix: + angle = fix_angle + + # Make sure the thing type is allowed for the Doom version. + if typ not in allowed_things: + prefix = "Type %d is not allowed for Doom %s, but " % ( + typ, doom_version) + if typ == benign_type: + # Don't fix if already benign. + if not args.fix: + messages.append("%sis already the benign type, so it will not " + "be fixed." % prefix) + else: + messages.append("%s%s fixed with begin type %d." % ( + prefix, tense, benign_type)) + if args.fix: + # This seems to be a benign and non-visible type, so it's not a full + # fix (since proper deleting is hard) but this makes it easier to + # find all the offending things by searching for one type. + # (benign_type). + typ = benign_type + + # Make sure the spawn flags limited to the lowest 5 bits. + if option_thing_spawn_flags: + fix_spawn_flags = 0x1f & spawn_flags + if spawn_flags != fix_spawn_flags: + messages.append("Spawn flags %d is not allowed, but %s fixed with " + "%d." % (spawn_flags, tense, fix_spawn_flags)) + if args.fix: + spawn_flags = fix_spawn_flags + + return (x, y, angle, typ, spawn_flags) + + +# Combine a list of messages by comma separating them, and and then prefixing +# with " ERRORS: " or " FIXES: " depending on the mode. +def get_combined_message(messages): + if len(messages) == 0: + return "" + return (" FIXES: " if args.fix else " ERRORS: " ) + ", ".join(messages) + + +# Get the doom version. 1 for Doom 1 or 2 for Doom 2 +def get_doom_version(map_name): + return 2 if map_name.startswith("MAP") else 1 + + +# Get the expected lump name at a specified index. +def get_expected_lump_name(index): + if index == 0: + # Strip of the directory, upper case, remove ".wad". + name = os.path.basename(current_wad).upper() + if name.endswith(".WAD"): + name = name[:-4] + + # For IWADS guess that the first name should be the first map. + if args.iwad and ("DOOM" in name): + return "MAP01" if ("2" in name) else "E1M1" + + # 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 + elif index < standard_lump_names_len: + return standard_lump_names[index] + else: + return None + + +# Update globals that depend on the map name. This is called for each map +# change. +def handle_map_change(map_name): + global doom_version + global allowed_things + global is_may_have_secret_exit + + new_doom_version = get_doom_version(map_name) + if new_doom_version != doom_version: + doom_version = new_doom_version + allowed_things = allowed_things_all[doom_version] + is_may_have_secret_exit = map_name in may_have_secret_exit + + +# Initialization that can't be done in global variables. +def init(): + global tense + + # This is done here beacuse the function names are not yet visible in the + # global variables. + region_info["THINGS"] += (do_thing,) + region_info["LINEDEFS"] += (do_linedef,) + region_info["SECTORS"] += (do_sector,) + region_info["directory"] += (do_directory,) + + # Tense as in "was" for the past and "can be" for the future. + tense = "was" if args.fix else "can be" + + # It's fine if this is incorrect for Doom 1. It will be overridden when + # the first map is seen. + handle_map_change("MAP01") + +# Main enty point. +def main(): + parse_args() + init() + process_paths() + summarize() + + +# 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", + "--fix", + action="store_true", + help="Fix. Fix a subset of the errors.") + parser.add_argument( + "-l", + "--follow-symlinks", + action="store_true", + help="Follow symbolic links (dangerous).") + parser.add_argument( + "-i", + "--iwad", + action="store_true", + help="IWAD. Allow processing of IWADs in addition to PWADs. This is " + + "mostly for internal use. When combined with verbose (-v) it can " + + "be used to generate lists of allowed values.") + parser.add_argument( + "-n", + "--name-only", + action="store_true", + help="Only consider the map name (first lump), and nothing else.") + parser.add_argument( + "-r", + "--recursive", + action="store_true", + help="Recurse into sub-directories.") + parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="Verbose. Output every item processed.") + 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): + global current_wad + + if os.path.islink(path) and not args.follow_symlinks: + # Symlink. Avoid following by default in case it points somewhere it + # shouldn't. + print("Ignoring symlink \"%s\". Use -l to follow symlinks." % path) + elif 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(os.path.join(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"): + current_wad = path + 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 wad_error_fix_count + global dir_count + global processed_wad_count + global total_do_count + global unexpected_count + + if os.path.basename(wad).lower() in ignored_wads: + # A known WAD that should not be processed. + return + + # This is after the above since it is only a count of WADs that were + # processed. + processed_wad_count += 1 + + try: + wad_do_count = 0 # The number of errors or fixes. + map_name = "???" # Used for logging. May be "???" for IWADs. + with open(wad, "r+b" if args.fix else "rb") as fhand: + magic, dir_count, dir_offset = wad_unpack("<4s2i", + fhand.read(12)) + is_iwad = magic == "IWAD" + if not (magic == "PWAD" or (args.iwad and is_iwad)): + raise TestException("Not an allowed Doom file header magic. " + "magic=" + magic) + # Directory at offset 0x8 in the header. + fhand.seek(dir_offset) + + lump_index = 0 # zero based + # Process the lumps. For --name-only only the first lump needs to + # be processed in order to get the map name. + if args.name_only: + lump_index_limit = 1 + else: + lump_index_limit = dir_count + while lump_index < lump_index_limit: + # The first lump in the directory, which should be the 0 byte map + # name one. + lump_offset, lump_size, lump_name = wad_unpack("<2i8s", + fhand.read(16)) + + # Get the map name both so that the Doom version can be + # determined, and for logging. + if is_iwad: + # Limited error checking, and no fixing, for IWADs. + name_lump = map_name_re.match(lump_name) + else: + # A particular order and sizes are expected for levels. + name_lump = lump_index == 0 + if name_lump: + map_name = lump_name + + # This tells us what items are allowed. + if name_lump and not args.name_only: + handle_map_change(map_name) + + # Verify the contents of lumps. + wad_do_count += do_region(lump_name, map_name, wad, fhand, + lump_offset, lump_size) + + lump_index += 1 + + # Consider the directory, which is traditionally after all of the + # lumps, now that the lumps have been processed. "16" is the size + # of each directory entry. + wad_do_count += do_region("directory", map_name, wad, fhand, + dir_offset, dir_count * 16) + + except IOError as err: + # Probably the WAD file couldn't be open for read (test) or read and + # write (default). + print("Unable to open for read%s: %s: " % ( + " and write" if args.fix else "", err)) + unexpected_count += 1 + 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. + print("WAD file %s is too small: %s" % (wad, err)) + unexpected_count += 1 + except TestException as err: + print(str(err)) + unexpected_count += 1 + + if wad_do_count > 0 or args.verbose: + print("\nFilename for map %s in the above is %s. It has %d %s" % ( + map_name, wad, wad_do_count, "fixes." if args.fix else "errors.")) + if wad_do_count > 0: + total_do_count += wad_do_count + wad_error_fix_count += 1 + + +# Summarize what happened, and then exit with the appropriate exit code. +def summarize(): + no_fixes = "did not have fixable errors" if args.fix else "were correct" + end = "fixes." if args.fix else "errors." + if wad_error_fix_count == 0: + # A bit confusing in the fix case since only fixes that can + # actually be done are counted, but hopefully people will figure it out. + print("\nAll %d WADs %s. There are no %s" % ( + processed_wad_count, no_fixes, end)) + else: + print("\n%d of %d WAD files have %s There are a total of %d %s" % ( + wad_error_fix_count, processed_wad_count, end, total_do_count, end)) + if unexpected_count > 0: + print("There were %d unexpected errors, so the result may not be " + "valid." % unexpected_count) + sys.exit(1 if (unexpected_count or (total_do_count and not args.fix)) else 0) + + +# A version of wad_pack() suitable for WAD files. Convert strings to bytes +# before packing. +def wad_pack(format, *fields): + if "s" in format: + new_fields = [] + for field in fields: + new_fields.append(field.encode("UTF-8") if type(field) == str else field) + else: + new_fields = fields + return struct.pack(format, *new_fields) + + +# A version of wad_unpack() suitable for WAD files. Convert strings to UTF-8 +# (superset of ASCII), and strip off any trailing nulls. +def wad_unpack(format, buffer): + raw_fields = struct.unpack(format, buffer) + if "s" in format: + fields = [] + for field in raw_fields: + # Split by null and take the first non-null result in order to + # strip trailing nulls. + fields.append(field.decode("UTF-8").partition("\0")[0] + if type(field) == bytes else field) + return tuple(fields) + else: + return raw_fields + + +# So that this script may be accessed as a module. +if __name__ == "__main__": + main()