Guest User

monstercount

a guest
May 17th, 2026
72
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 5.71 KB | None | 0 0
  1. #!/usr/bin/env python
  2.  
  3. from struct import unpack
  4. from collections import Counter
  5. from termcolor import colored
  6. from os.path import basename, splitext
  7. from zipfile import ZipFile
  8. import argparse
  9.  
  10. EXCESSIVE_MONSTER_COUNT = 300
  11.  
  12. # Thing Types | https://doomwiki.org/wiki/Thing_types#Monsters
  13. MONSTER_TYPES = { 3004:"tr", 9:"sg", 65:"cg", 3001:"im",
  14.                   3002:"dm", 58:"sp", 3006:"ls", 3005:"ca",
  15.                   69:"hk", 3003:"br", 68:"ar", 71:"pe",
  16.                   66:"rv", 67:"mn", 64:"vi", 7:"sd",
  17.                   16:"cy", 84:"ss", 72:"ke" }
  18.  
  19. # Thing flags | https://doomwiki.org/wiki/Thing#Flags
  20. FLAG_HNTR = 1<<0 # 0x01
  21. FLAG_HMP  = 1<<1 # 0x02
  22. FLAG_UV   = 1<<2 # 0x04
  23. FLAG_DEAF = 1<<3 # 0x08
  24. FLAG_MP   = 1<<4 # 0x10
  25.  
  26. def get_monsters(wad, args):
  27.     file_id, num_lumps, directory_offset = unpack('4sii', wad.read(12))
  28.  
  29.     if file_id not in (b'PWAD', b'IWAD'):
  30.         raise ValueError("Not a valid WAD file")
  31.  
  32.     monster_data = []
  33.     map_name = ""
  34.  
  35.     if args.skill in (1, 2):
  36.         skill_flag = FLAG_HNTR
  37.     elif args.skill == 3:
  38.         skill_flag = FLAG_HMP
  39.     elif args.skill in (4, 5):
  40.         skill_flag = FLAG_UV
  41.     flag_mask = skill_flag | FLAG_MP
  42.  
  43.     for i in range(num_lumps):
  44.         wad.seek(directory_offset + 16 * i)
  45.         lump_offset, lump_size, lump_name = unpack('ii8s', wad.read(16))
  46.         lump_name = lump_name.split(b'\0', 1)[0].decode()
  47.  
  48.         if lump_name == 'THINGS':
  49.             wad.seek(lump_offset)
  50.             map_monsters = []
  51.             while lump_size >= 10:
  52.                 x, y, angle, type_id, flags = unpack('hhHHH', wad.read(10))
  53.                 if type_id in MONSTER_TYPES and (flags & flag_mask) == skill_flag:
  54.                     map_monsters.append(type_id)
  55.                 lump_size -= 10
  56.             monster_data.append((map_name, map_monsters))
  57.         else:
  58.             map_name = lump_name
  59.  
  60.     return monster_data
  61.  
  62. def tabulate(monster_data):
  63.     monster_counts = {}
  64.     for map_name, map_monsters in monster_data:
  65.         monster_counts.setdefault(map_name, Counter()).update(map_monsters)
  66.  
  67.     present_monsters = {mon for counter in monster_counts.values() for mon in counter}
  68.     headers = [MONSTER_TYPES[key] for key in MONSTER_TYPES if key in present_monsters]
  69.  
  70.     table = []
  71.     for row in sorted(monster_counts):
  72.         row_totals = [monster_counts[row].get(key, 0) for key in MONSTER_TYPES if MONSTER_TYPES[key] in headers]
  73.         table.append([row] + [str(count) for count in row_totals] + [str(sum(row_totals))])
  74.  
  75.     column_totals = [sum(int(row[i]) for row in table) for i in range(1, len(table[0]))]
  76.     table.append(["TOTAL"] + [str(count) for count in column_totals])
  77.  
  78.     table.insert(0, [""] + headers + ["TOTAL"])
  79.  
  80.     return table
  81.  
  82. def display(tabulated_data, args):
  83.     formatted_table = [["" if cell == '0' else cell for cell in row] for row in tabulated_data]
  84.  
  85.     formatted_table = formatted_table[:-1]
  86.  
  87.     colwidths = [max(len(x) for x in col) for col in zip(*formatted_table)]
  88.     formatted_table = [[x.rjust(w) for x,w in zip(row, colwidths)] for row in formatted_table]
  89.  
  90.     for row in formatted_table[:-1]:
  91.         last_col_idx = len(row) - 1
  92.         value = row[last_col_idx]
  93.         if value.strip().isdigit() and int(value) > EXCESSIVE_MONSTER_COUNT:
  94.             row[last_col_idx] = colored(value, 'red')
  95.  
  96.     for row in formatted_table:
  97.         print(" ".join(row))
  98.  
  99.     if len(formatted_table) > 24:
  100.         print(" ".join(formatted_table[0]))
  101.  
  102.     wad_count = len(tabulated_data) - 2
  103.     populated_count = sum(1 for row in tabulated_data[1:-1] if int(row[-1]) > 0)
  104.     total_monsters = int(tabulated_data[-1][-1])
  105.     avg_monsters = round(total_monsters / populated_count) if populated_count else 0
  106.     print(f"WAD count: {populated_count} Avg monsters: {avg_monsters}")
  107.  
  108. def process_wad(wad, args):
  109.     monster_data = get_monsters(wad, args)
  110.     if not monster_data:
  111.         print("No maps found.")
  112.         return
  113.     tabulated_data = tabulate(monster_data)
  114.     display(tabulated_data, args)
  115.  
  116. def main(args):
  117.     for file in args.files:
  118.         if file.lower().endswith('.wad'):
  119.             print(f"Opening {file} with -skill {args.skill}...")
  120.             with open(file, 'rb') as wad:
  121.                 process_wad(wad, args)
  122.         elif file.lower().endswith('.zip'):
  123.             with ZipFile(file) as zf:
  124.                 zip_name = splitext(basename(file))[0].lower()
  125.                 wad_name = f"{zip_name}.wad"
  126.                 namelist = zf.namelist()
  127.                 fn = (next((n for n in namelist if n.lower() == wad_name), None)
  128.                    or next((n for n in namelist if n.lower().endswith('.wad')), None))
  129.                 if fn is not None:
  130.                     print(f"Opening {fn} inside {file} with -skill {args.skill}...")
  131.                     with zf.open(fn, 'r') as wad:
  132.                         process_wad(wad, args)
  133.                 else:
  134.                     print("No .wad found inside archive")
  135.         else:
  136.             print("Unsupported file type. This script only accepts .wad and .zip files.")
  137.  
  138. def parse_arguments():
  139.     parser = argparse.ArgumentParser(description="Parses Doom wad files for monster counts")
  140.     parser.add_argument(
  141.         '-skill',
  142.         type=int,
  143.         choices=range(1, 6),
  144.         default=4,
  145.         help="skill level (1-5), 1=ITYTD, 2=HNTR, 3=HMP, 4=UV, 5=Nightmare (defaults to (4) UV)"
  146.     )
  147.     parser.add_argument(
  148.         'files',
  149.         nargs='*',
  150.         help=".wad or .zip files to process"
  151.     )
  152.     return parser.parse_args()
  153.  
  154. if __name__=='__main__':
  155.     args = parse_arguments()
  156.     if args.files:
  157.         main(args)
  158.     else:
  159.         print('Please provide at least one file to process.')
  160.  
Advertisement
Add Comment
Please, Sign In to add comment