├── .gitignore ├── README.md ├── data ├── texture_descriptions.json └── thing_types.json ├── requirements.txt └── wadzilla.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | output.zil 3 | *.wad 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wadzilla 2 | [Current Status and FAQ](https://github.com/scottvr/wadzilla/wiki/Current-status-and-FAQ) Please see this if you actually intend to use the tool. If you are only here for the amusement of or to show support for the ludicrous idea, you may still like to read it, but it isn't critically relevant before you undertake your endeavor. 3 | 4 | Wadzilla is a tool designed to facilitate the conversion of Doom WAD files into ZIL files, suitable for creating interactive fiction in the style of Infocom games. 5 | 6 | Quite possibly it was actually designed to facilitate the long-standing "Doom on Everything" initiative. I am aware that it is a ludicrous tool. 7 | 8 | A wiki page explaining the motivation and some history of Zork and Doom is here: https://github.com/scottvr/wadzilla/wiki 9 | 10 | ## Overview 11 | 12 | Wadzilla extracts information from Doom WAD files, including map geometry, texture descriptions and thing types, and utilizes it to generate text that describes the rooms and objects in ZIL code format, so that the map can be explored within the Zork runtime environment. 13 | 14 | ## Usage 15 | 16 | To use Wadzilla, execute the following command: 17 | 18 | ```bash 19 | python wadzilla.py --basewad /path/to/doom1.wad 20 | ``` 21 | 22 | Mods (PWADs) can also be specified. the argument syntax for specifying IWAD and PWAD is based on the Doom game's command-line options in homage; apologies for their sorta non-intuitive (to me) option names but... y'know, tribute. 23 | 24 | See the output from --help for more usage info. 25 | 26 | Note that Wadzilla is still in embryonic stage and under active development. If it is committed to main, then it should be functional, just note that it may change minute to minute for as long as I still have interest in working on it. after larger milestone changes, I am tagging releases though, so you can always use those commits for stable reproducable results across different machines and whatnot. 27 | 28 | ## Dependencies 29 | 30 | Hat tip to sachahjkl for motivating me to create a requirements.txt for the Python libs. They are still dependencies, but if you run 31 | ``` bash 32 | pip install -r requirements.txt 33 | ``` 34 | they will be installed simply and with versions known to be compatible with what I have used in development. 35 | 36 | - Python 3.x 37 | - Requests 38 | - BeautifulSoup4 39 | - an IWAD file, such as doom1.wad from id Software. the Doom shareware WAD is freely distributable per John Carmack, and it is also readily available all over the Internet, so I won't waste bits by including it here. archive.org will have it, as well as many other places. 40 | 41 | ## Optionally 42 | - a patch level (mod/PWAD file) 43 | - If you actually want to PLAY your Doom IF, you will need to compile it and run it on a z-machine. 44 | 45 | A video explaining this can be found here: https://m.youtube.com/watch?v=JpaBCb6qCCo 46 | 47 | A website linking to many tools that can be used in the process is here: https://eblong.com/infocom/ 48 | 49 | if you are after the ultimate goal of *playing Doom inside of Zork* you will need the decompiled Zork ZIL to which you can use Wadzilla to add a portal into E1M1 in a specified Zork room. See the usage info for how. Then of course, the aforementioned compilation and execution on a z-machine is needed. 50 | 51 | ## Additionally 52 | Descriptions for Things found in the wad will either be just the item number (not very descriptive) or the string from the file data/thing_types.json, which contains a dict mapping the item id (decimal) to a string. By default if the file does not exist, the script will attempt to create a dict populated with data it finds in the Doom Wiki (https://doomwiki.org/wiki/Thing_types_by_number) table. 53 | 54 | You can edit this file for your needs, or even create it manually. (Oh yeah, because Wadzilla should work with Doom 2, Hexen, etc WADs equally.) Also, if you want to have the script scrape a table you know exists in the aforementioned Doom Wiki doc (as in the example below, for the Strife game), you can alter the function to search for the table header like so: 55 | 56 | ``` python 57 | # Using the wikitable on that page for the Things in the Doom-engine game "Strife" 58 | def scrape_thing_types(url): 59 | response = requests.get(url) 60 | soup = BeautifulSoup(response.text, 'html.parser') 61 | 62 | thing_dict = {} 63 | tables = soup.find_all('table', class_='wikitable') 64 | 65 | for table in tables: 66 | # Check if the table header matches the one for "Strife" 67 | header = table.find_previous('h2') 68 | if header and 'Strife' in header.get_text(): 69 | for row in table.find_all('tr')[1:]: # Skip the header row 70 | cols = row.find_all('td') 71 | if len(cols) >= 9: # Ensure there are enough columns 72 | type_id = cols[0].get_text(strip=True) 73 | description = cols[8].get_text(strip=True) 74 | if type_id.isdigit(): 75 | thing_dict[int(type_id)] = description 76 | 77 | return thing_dict 78 | ``` 79 | 80 | Just as I finished writing this section I realized I should probably just add a variable to contain the section header name (string to match) and perhaps even allow it passed as a command-line option argument. TBD. 81 | 82 | ## LICENSES 83 | wadzilla is um, MIT licensed I guess. I'll put something proper here before anyone learns of Wadzilla's existence. 84 | -------------------------------------------------------------------------------- /data/texture_descriptions.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /data/thing_types.json: -------------------------------------------------------------------------------- 1 | { 2 | "1": "Player1 start", 3 | "2": "Player 2 start", 4 | "3": "Player 3 start", 5 | "4": "Player 4 start", 6 | "5": "Blue keycard", 7 | "6": "Yellow keycard", 8 | "7": "Spiderdemon", 9 | "8": "Backpack", 10 | "9": "Shotgun guy", 11 | "10": "Bloody mess", 12 | "11": "Deathmatch start", 13 | "12": "Bloody mess", 14 | "13": "Red keycard", 15 | "14": "Teleportlanding", 16 | "15": "Dead player", 17 | "16": "Cyberdemon", 18 | "17": "Energy cell pack", 19 | "18": "Dead former human", 20 | "19": "Dead former sergeant", 21 | "20": "Dead imp", 22 | "21": "Dead demon", 23 | "22": "Dead cacodemon", 24 | "23": "Dead lost soul (invisible)", 25 | "24": "Pool of blood and flesh", 26 | "25": "Impaled human", 27 | "26": "Twitching impaled human", 28 | "27": "Skull on a pole", 29 | "28": "Five skulls \"shish kebab\"", 30 | "29": "Pile of skulls and candles", 31 | "30": "Tall green pillar", 32 | "31": "Short green pillar", 33 | "32": "Tall red pillar", 34 | "33": "Short red pillar", 35 | "34": "Candle", 36 | "35": "Candelabra", 37 | "36": "Short green pillar with beating heart", 38 | "37": "Short red pillar with skull", 39 | "38": "Red skull key", 40 | "39": "Yellow skull key", 41 | "40": "Blue skull key", 42 | "41": "Evil eye", 43 | "42": "Floating skull", 44 | "43": "Burnt tree", 45 | "44": "Tall blue firestick", 46 | "45": "Tall green firestick", 47 | "46": "Tall red firestick", 48 | "47": "Brown stump", 49 | "48": "Tall techno column", 50 | "49": "Hanging victim, twitching", 51 | "50": "Hanging victim, arms out", 52 | "51": "Hanging victim, one-legged", 53 | "52": "Hanging pair of legs", 54 | "53": "Hanging leg", 55 | "54": "Large brown tree", 56 | "55": "Short blue firestick", 57 | "56": "Short green firestick", 58 | "57": "Short red firestick", 59 | "58": "Spectre", 60 | "59": "Hanging victim, arms out", 61 | "60": "Hanging pair of legs", 62 | "61": "Hanging victim, one-legged", 63 | "62": "Hanging leg", 64 | "63": "Hanging victim, twitching", 65 | "64": "Arch-vile", 66 | "65": "Heavy weapon dude", 67 | "66": "Revenant", 68 | "67": "Mancubus", 69 | "68": "Arachnotron", 70 | "69": "Hell knight", 71 | "70": "Burning barrel", 72 | "71": "Pain elemental", 73 | "72": "Commander Keen", 74 | "73": "Hanging victim, guts removed", 75 | "74": "Hanging victim, guts and brain removed", 76 | "75": "Hanging torso, looking down", 77 | "76": "Hanging torso, open skull", 78 | "77": "Hanging torso, looking up", 79 | "78": "Hanging torso, brain removed", 80 | "79": "Pool of blood", 81 | "80": "Pool of blood", 82 | "81": "Pool of brains", 83 | "82": "Super shotgun", 84 | "83": "Megasphere", 85 | "84": "Wolfenstein SS", 86 | "85": "Tall techno floor lamp", 87 | "86": "Short techno floor lamp", 88 | "87": "Spawn spot", 89 | "88": "Romero's head", 90 | "89": "Monster spawner", 91 | "2001": "Shotgun", 92 | "2002": "Chaingun", 93 | "2003": "Rocket launcher", 94 | "2004": "Plasma gun", 95 | "2005": "Chainsaw", 96 | "2006": "BFG9000", 97 | "2007": "Clip", 98 | "2008": "4 shotgun shells", 99 | "2010": "Rocket", 100 | "2011": "Stimpack", 101 | "2012": "Medikit", 102 | "2013": "Supercharge", 103 | "2014": "Health bonus", 104 | "2015": "Armor bonus", 105 | "2018": "Armor(green)", 106 | "2019": "Megaarmor(blue)", 107 | "2022": "Invulnerability", 108 | "2023": "Berserk", 109 | "2024": "Partial invisibility", 110 | "2025": "Radiation shielding suit", 111 | "2026": "Computer area map", 112 | "2028": "Floor lamp", 113 | "2035": "Exploding barrel", 114 | "2045": "Light amplification visor", 115 | "2046": "Box of rockets", 116 | "2047": "Energy cell", 117 | "2048": "Box of bullets", 118 | "2049": "Box of shotgun shells", 119 | "3001": "Imp", 120 | "3002": "Demon", 121 | "3003": "Baron of Hell", 122 | "3004": "Zombieman", 123 | "3005": "Cacodemon", 124 | "3006": "Lost soul" 125 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4==4.11.1 2 | requests==2.31.0 3 | -------------------------------------------------------------------------------- /wadzilla.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | import requests 5 | from bs4 import BeautifulSoup 6 | import struct 7 | import argparse 8 | 9 | 10 | 11 | class WADFile: 12 | def __init__(self, filename): 13 | self.filename = filename 14 | self.lumps = {} 15 | self.read_wad() 16 | 17 | def read_wad(self): 18 | with open(self.filename, 'rb') as f: 19 | header = f.read(12) 20 | wad_type, num_lumps, dir_offset = struct.unpack('4sII', header) 21 | f.seek(dir_offset) 22 | 23 | for _ in range(num_lumps): 24 | lump_data = f.read(16) 25 | lump_offset, lump_size, lump_name = struct.unpack('II8s', lump_data) 26 | lump_name = lump_name.strip(b'\x00').decode('ascii') 27 | self.lumps[lump_name] = (lump_offset, lump_size) 28 | 29 | def read_lump(self, lump_name): 30 | if lump_name in self.lumps: 31 | offset, size = self.lumps[lump_name] 32 | with open(self.filename, 'rb') as f: 33 | f.seek(offset) 34 | return f.read(size) 35 | else: 36 | raise ValueError(f'Lump {lump_name} not found in WAD.') 37 | 38 | def adjust_coordinates(self, existing_zil_map): 39 | # Calculate offset to align room 0's starting location with existing ZIL map 40 | # and adjust coordinates of all rooms accordingly 41 | 42 | portal_room_id = len(existing_zil_map) 43 | portal_coordinates = (x, y) 44 | 45 | existing_zil_map[portal_room_id] = f"ROOM {portal_room_id}: A mysterious atmosphere fills the room. " \ 46 | f"A glowing green disc appears on the floor." 47 | 48 | self.rooms[0].add_portal(portal_coordinates) 49 | 50 | 51 | 52 | def parse_vertexes(data): 53 | vertexes = [] 54 | for i in range(0, len(data), 4): 55 | x, y = struct.unpack('hh', data[i:i+4]) 56 | vertexes.append((x, y)) 57 | return vertexes 58 | 59 | def parse_linedefs(data): 60 | linedefs = [] 61 | for i in range(0, len(data), 14): 62 | v1, v2, flags, types, tag, right_sidedef, left_sidedef = struct.unpack('hhhhhhh', data[i:i+14]) 63 | linedefs.append((v1, v2, flags, types, tag, right_sidedef, left_sidedef)) 64 | return linedefs 65 | 66 | def parse_sidedefs(data): 67 | sidedefs = [] 68 | for i in range(0, len(data), 30): 69 | x_offset, y_offset, upper_tex, lower_tex, middle_tex, sector_id = struct.unpack('hh8s8s8sh', data[i:i+30]) 70 | sidedefs.append((x_offset, y_offset, upper_tex.strip(b'\x00').decode(), lower_tex.strip(b'\x00').decode(), middle_tex.strip(b'\x00').decode(), sector_id)) 71 | return sidedefs 72 | 73 | def parse_sectors(data): 74 | sectors = [] 75 | for i in range(0, len(data), 26): 76 | floor_height, ceiling_height, floor_tex, ceiling_tex, light_level, sector_type, tag = struct.unpack('hh8s8shhh', data[i:i+26]) 77 | sectors.append((floor_height, ceiling_height, floor_tex.strip(b'\x00').decode(), ceiling_tex.strip(b'\x00').decode(), light_level, sector_type, tag)) 78 | return sectors 79 | 80 | def parse_things(data): 81 | things = [] 82 | for i in range(0, len(data), 10): 83 | x, y, angle, type, flags = struct.unpack('hhhhH', data[i:i+10]) 84 | things.append((x, y, type)) 85 | return things 86 | 87 | def point_in_polygon(x, y, polygon): 88 | num = len(polygon) 89 | j = num - 1 90 | c = False 91 | for i in range(num): 92 | if ((polygon[i][1] > y) != (polygon[j][1] > y)) and \ 93 | (x < polygon[i][0] + (polygon[j][0] - polygon[i][0]) * (y - polygon[i][1]) / (polygon[j][1] - polygon[i][1])): 94 | c = not c 95 | j = i 96 | return c 97 | 98 | def scrape_texture_descriptions(url): 99 | # stub until I arrive at a way to describe the textures. The script will just use the texture names for now. 100 | # response = requests.get(url) 101 | # soup = BeautifulSoup(response.text, 'html.parser') 102 | 103 | texture_dict = {} 104 | # for table in soup.find_all('table', class_='wikitable'): 105 | # for row in table.find_all('tr')[1:]: 106 | # cols = row.find_all('td') 107 | # if len(cols) >= 2: 108 | # texture_name = cols[0].get_text(strip=True) 109 | # description = cols[1].get_text(strip=True) 110 | # texture_dict[texture_name] = description 111 | 112 | return texture_dict 113 | 114 | def scrape_thing_types(url): 115 | response = requests.get(url) 116 | soup = BeautifulSoup(response.text, 'html.parser') 117 | 118 | thing_dict = {} 119 | tables = soup.find_all('table', class_='wikitable') 120 | 121 | for table in tables: 122 | # Check if the table header matches the one for "Doom, Doom II, Final Doom" 123 | header = table.find_previous('h2') 124 | if header and 'Doom, Doom II, Final Doom' in header.get_text(): 125 | for row in table.find_all('tr')[1:]: # Skip the header row 126 | cols = row.find_all('td') 127 | if len(cols) >= 9: 128 | type_id = cols[0].get_text(strip=True) 129 | description = cols[8].get_text(strip=True) 130 | if type_id.isdigit(): 131 | thing_dict[int(type_id)] = description 132 | 133 | return thing_dict 134 | 135 | class Room: 136 | def __init__(self, sector_id, sector_data, offset=(0, 0)): 137 | # Initialize Room instance with optional offset for coordinates 138 | self.sector_id = sector_id 139 | self.floor_height, self.ceiling_height, self.floor_tex, self.ceiling_tex, self.light_level, self.type, self.tag = sector_data 140 | self.vertexes = set() 141 | self.linedefs = [] 142 | self.things = [] 143 | self.wall_textures = [] 144 | self.offset = offset 145 | self.portal_coordinates = None # Portal entrance coordinates 146 | 147 | def add_vertex(self, vertex): 148 | # Add vertex to room, adjusting coordinates based on offset 149 | self.vertexes.add((vertex[0] + self.offset[0], vertex[1] + self.offset[1])) 150 | 151 | def add_portal(self, coordinates): 152 | # Add portal entrance coordinates to the room 153 | self.portal_coordinates = coordinates 154 | 155 | def add_vertex(self, vertex): 156 | self.vertexes.add(vertex) 157 | 158 | def add_linedef(self, linedef, sidedefs): 159 | self.linedefs.append(linedef) 160 | v1, v2, flags, types, tag, right_sidedef, left_sidedef = linedef 161 | 162 | right_sidedef_data = sidedefs[right_sidedef] 163 | left_sidedef_data = sidedefs[left_sidedef] if left_sidedef != -1 else None 164 | 165 | self.wall_textures.append({ 166 | 'right': { 167 | 'upper': right_sidedef_data[2] if right_sidedef_data[2] != '-' else None, 168 | 'lower': right_sidedef_data[3] if right_sidedef_data[3] != '-' else None, 169 | 'middle': right_sidedef_data[4] if right_sidedef_data[4] != '-' else None 170 | }, 171 | 'left': { 172 | 'upper': left_sidedef_data[2] if left_sidedef_data and left_sidedef_data[2] != '-' else None, 173 | 'lower': left_sidedef_data[3] if left_sidedef_data and left_sidedef_data[3] != '-' else None, 174 | 'middle': left_sidedef_data[4] if left_sidedef_data and left_sidedef_data[4] != '-' else None 175 | } 176 | }) 177 | 178 | def add_thing(self, thing): 179 | self.things.append(thing) 180 | 181 | def describe_zil(self, texture_descriptions, thing_type_descriptions): 182 | description = f"