#!/usr/bin/env python3 # SPDX-License-Identifier: BSD-3-Clause # # Texture lump builder for Freedoom # # This script builds the TEXTURE1, TEXTURE2 and PNAMES lumps for # Freedoom - the lumps containing the texture definitions. # # Freedoom does not use deutex's built-in texture builder for several # reasons: # # Firstly, Freedoom's texture lumps need to be compatible with those # of the original Doom WADs. There are several examples of PWADs that # replace only PNAMES, or only TEXTURE1. Because the TEXTURE1 format # is tightly coupled to the ordering of the PNAMES lump, this means # that these WADs will fail in Freedoom unless the ordering is # strictly maintained. For example, if entry #3 in PNAMES is DOOR2_4, # it must also be the same in Freedoom's version of PNAMES. # # Freedoom has a single configuration file where all its textures are # defined, but doom.wad contains two separate lumps: TEXTURE1/TEXTURE2. # Similarly to the first problem, it's important that the compatible # lumps are built with the same textures in each: some WADs replace # TEXTURE1 but not TEXTURE2, with the result that many textures end # up missing. # # Finally, deutex does not allow a filename to be specified for # TEXTURE entries. That is to say, you can't do this: # # [textures] # TEXTURE1 = fdtxtr1.txt # # This is an annoying limitation that means that the different # Freedoom IWADs cannot be built in parallel by make. import collections import os.path import re import sys import struct COMMENT_RE = re.compile(r"\s*;.*") TEXTURE_NAME_RE = re.compile(r"\s*([\w-]+)\s+(-?\d+)\s+(-?\d+)") PATCH_NAME_RE = re.compile(r"\s*\*\s+([\w-]+)\s+(-?\d+)\s+(-?\d+)") Texture = collections.namedtuple("Texture", ["w", "h", "patches"]) TexturePatch = collections.namedtuple("TexturePatch", ["pname", "x", "y"]) class TextureSet(collections.OrderedDict): def __init__(self, pnames): """Initialize a new set of textures. Args: pnames: List of PNAMES to use for the textures. New patches will be added to this list as the texture set is extended. """ super(TextureSet, self).__init__() self.pnames = pnames def pname_index(self, pname): """Get the index into the PNAMES list for the given patch. If the patch is not in the list, it will be added to the list. Args: pname: Name of the patch to look up. Returns: Index into the PNAMES list where this patch can be found. """ pname = pname.upper() try: return self.pnames.index(pname) except ValueError: self.pnames.append(pname) return len(self.pnames) - 1 def add_texture(self, name, width=0, height=0): """Add a new texture to the set. If a texture is already defined with the given name, the current definition is erased (though the ordering of textures is maintained). Args: name: Name of the texture. width: Width of the texture in pixels. height: Height of the texture in pixels. """ name = name.upper() self[name] = Texture(width, height, []) def add_texture_patch(self, txname, patch, x, y): """Add a patch to the given texture. Args: txname: Name of the texture. patch: Name of the patch to add. x: X offset for the patch in pixels. y: Y offset for the patch in pixels. """ txname = txname.upper() texture = self[txname] tp = TexturePatch(self.pname_index(patch), x, y) texture.patches.append(tp) def write_texture_lump(self, filename): """Build the texture lump and output to a file. Args: filename: Path to file in which to store the resulting lump. """ with open(filename, "wb") as out: # Header indicating number of textures: out.write(struct.pack(" 8: raise NamesFileError( "Invalid name in %s: %s" % (filename, line) ) if line != "": result.append(line.upper()) return result def write_names_file(names, filename, sprites_dir): """Write a list of names to a file. Args: names: List of names to write. filename: Filename to write them to. sprites_dir: Path to directory containing sprites. If a file is found in this directory matching a patch name, that patch will not be included in the outputted list. """ with open(filename, "w") as f: for name in names: filename = "%s.png" % name.lower() sprite_path = os.path.join(sprites_dir, filename) if not os.path.exists(sprite_path): f.write("%s\n" % name) def load_compat_textures(textures, compat_file): """Pre-populate a texture set from a compatibility file. Args: textures: TextureSet to populate. compat_file: Path to text file containing list of textures. If None, do not do anything. """ if compat_file is None: return for texture in read_names_file(compat_file): textures.add_texture(texture) class TextureConfigError(Exception): pass def parse_textures(stream): """Parse texture config from the given input stream. Args: stream: Input stream from which to read lines of input. Yields: A tuple for each parsed texture, containing: Texture name Width Height List of tuples representing each patch, where each contains: Patch name X offset Y offset """ current_texture = None current_patches = [] linenum = 0 for line in sys.stdin: line = COMMENT_RE.sub("", line).strip() linenum += 1 match = TEXTURE_NAME_RE.match(line) if match: if current_texture is not None: yield current_texture current_patches = [] current_texture = ( match.group(1), # Texture name int(match.group(2)), # Width int(match.group(3)), # Height current_patches, # List of patches ) continue match = PATCH_NAME_RE.match(line) if match and current_texture: current_patches.append( ( match.group(1), # Patch name int(match.group(2)), # X offset int(match.group(3)), # Y offset ) ) continue if line != "": raise TextureConfigError( "input:%i:Invalid config line: %s" % (linenum, line) ) # Last texture: if current_texture is not None: yield current_texture def write_pnames_lump(pnames, filename): """Write a PNAMES list to a file. Args: pnames: List of strings containing patch names. filename: Output filename. """ with open(filename, "wb") as out: out.write(struct.pack("