#!/usr/bin/env python # cube-map version 1.0 # by Steven Elliott (aka xoltra) # # Part of the goal of this script is to make it easier to implement # experimental editing commands. If they turn out to be popular maybe they # can be incorporated into Cube's editing facilities. Currently the only # editing that this script supports is global deleting and replacing of # entities by type. # # This script should be more modular. Possibly there should be a CubeMap # class in a cubemap module like there is a for gzip. # # Cube loads maps in load_world() in worldio.cpp; which can be compared to # this script. # # This script is subject to the same open source license as Cube (ZLIB like # license). See the "LICENSE" section in the cube_source/readme.txt file in # the Cube source for details. # # Examples: # # Show the headers, entities and cubes for douze and tartech: # cube-map -s. -e. -c. douze.cgz tartech.cgz # # Show the headers for all maps: # cube-map -s. *.cgz # # Delete the rockets and bullets and replace the quad with a health boost # in douze: # cube-map -w -o douze-mod.cgz -d 'rockets bullets' -r 'quad->boost' \ # douze.cgz # # Generate an image of douze with the playerstarts and quad marked: # cube-map -i douze.ppm -0 -20 -9 15 -q 'playerstart quad' douze.cgz # # For images it is recommended that -0 and -9 (min and max floor height) be # set correctly for the range of floor heights of the map for the best image # quality. But doing so is not crucial since The Gimp (also recommended) can # adjust the brightness and contrast (Layer/Colors/Brightness-Constrast) of # the image. You can zoom in on the image in The Gimp, iteratively rerun this # script and then revert the image (File/Revert). Finally, notice that the X # and Y coordinates of the cursor displayed by The Gimp in its status bar are # consistent with the X and Y coordinates of that point within Cube. # # see "cube-map -h" for more information. import os import sys from optparse import OptionParser import re from struct import * from gzip import GzipFile from tempfile import NamedTemporaryFile # The following structures were taken from cube.h. In each one the portion to # the right of the '//' documents the attribute letter used by this script as # well as what the attribute means. #### Header #### # struct header # { # // n - name of the map (not in the header) # char head[4]; // m - magic header, should be "CUBE" # int version; // v - version of the map, 5 in 2005 # int headersize; // s - sizeof(header) (should be header_len) # int sfactor; // f - log of one side of map, so 2**sf per side # int numents; // e - number of entities in the map # char maptitle[128]; // t - title of the map (mapmsg) # uchar texlists[3][256]; // N/A # int waterlevel; // w - water level of the map (waterlevel) # int reserved[15]; // N/A # }; # Cross reference the attributes to the unpacked index and the format # specifier as well as a description of each column. header_xref = { "n" : [-1, "%-15s", "name of the map (not in the header)"], "m" : [ 0, "%-4s", "magic header, should be \"CUBE\""], "v" : [ 1, "%2d", "version of the map, 5 in 2005"], "s" : [ 2, "%3d", "sizeof(header) (should be header_len)"], "f" : [ 3, "%2d", "log of one side of map, so 2**f per side"], "e" : [ 4, "%4d", "number of entities in the map"], "t" : [ 5, "%-50s", "title of the map (mapmsg)"], "w" : [ 7, "%7d", "water level of the map (waterlevel)"] } header_xref_keys = header_xref.keys() header_attrs_default = "nfet" # Layout of the header. Should be kept in sync with the above. header_fmt = "<4siiii128s768si15i" header_len = calcsize(header_fmt) # A convenience for common indexes header_version_idx = header_xref["v"][0] header_sfactor_idx = header_xref["f"][0] header_numents_idx = header_xref["e"][0] #### Entities #### # struct persistent_entity # { # short x; // x - x-coordinate of the entity # short y; // y - y-coordinate of the entity # short z; // z - z-coordinate of the entity # short attr1; // 1 - first attribute of the entity # uchar type; // t - type of the entity # uchar attr2; // 2 - second attribute of the entity # uchar attr3; // 3 - third attribute of the entity # uchar attr4; // 4 - fourth attribute of the entity # }; # Cross reference the attributes to the unpacked index and the format # specifier as well as a description of each column. # Cross reference the entity letter to it's index, format, color and desc. entity_xref = { "n" : [-1, "%-15s", "name of the map (not in the entity)"], "i" : [-1, "%4d", "index of the entity (not in the entity)"], "x" : [ 0, "%3d", "x-coordinate of the entity "], "y" : [ 1, "%3d", "y-coordinate of the entity "], "z" : [ 2, "%3d", "z-coordinate of the entity "], "1" : [ 3, "%3d", "first attribute of the entity"], "t" : [ 4, "%2d", "type of the entity"], "2" : [ 5, "%3d", "second attribute of the entity"], "3" : [ 6, "%3d", "third attribute of the entity"], "4" : [ 7, "%3d", "fourth attribute of the entity"] } entity_xref_keys = entity_xref.keys() entity_attrs_default = "ixyzt1234" # Layout of the entity. Should be kept in sync with the above. entity_fmt = "4h4B" entity_len = calcsize(entity_fmt) # Used to decode the entity type, also from cube.h. # Relates the index to the name and color (RGB - "\xRR\xGG\xBB"). entity_type_decode = [ ["notused", "\x00\x00\x00"], # entity slot not in use in map ["light", "\xe0\xe0\xff"], # light, attr1 = radius, attr2 = intens ["playerstart", "\x00\x00\xff"], # attr1 = angle ["shells", "\xd0\x70\x20"], # shotgun shells ["bullets", "\x00\xff\x00"], # chaingun bullets ["rockets", "\x85\x61\x14"], # rockets ["rounds", "\x88\x88\xfb"], # rifle rounds ["health", "\xff\x8e\x8e"], # ordinary health ["boost", "\xff\x50\x50"], # health boost ["greenarmour", "\x04\x98\x00"], # green armour ["yellowarmour", "\xa6\x9a\x05"], # yellow armour ["quad", "\xff\xff\x00"], # quad ["teleport", "\xe0\xff\xe0"], # attr1 = idx ["teledest", "\xff\xe0\xe0"], # attr1 = angle, attr2 = idx ["mapmodel", "\x00\x00\xff"], # attr1 = angle, attr2 = idx ["monster", "\x45\x90\x91"], # attr1 = angle, attr2 = monstertype ["carrot", "\xff\x6c\x00"], # attr1 = tag, attr2 = type ["jumppad", "\x80\xb7\x64"], # attr = [zpush, ypush, xpush] ["maxenttypes", "\x00\x00\x00"]] entity_type_max = len(entity_type_decode) - 1 # Pixels of entites that may later be written to an image entity_pixel_list = [] # A convenience for common indexes entity_type_idx = entity_xref["t"][0] entity_x_idx = entity_xref["x"][0] entity_y_idx = entity_xref["y"][0] #### Cubes #### # struct cube // Not really in cube.h. Subset of sqr that is in map file. # { # char floor; // f - floor height in cubes # char ceil; // c - ceil height in cubes # uchar wtex; // | - wall texture ids # uchar ftex; // _ - floor texture ids # uchar ctex; // - - ceil texture ids # uchar vdelta; // v - vertex delta, used for heightfield cubes # uchar utex; // u - upper wall tex id # uchar tag; // g - used by triggers # }; # specifier as well as a description of each column. cube_xref = { "n" : [-1, "%-15s", "name of the map (not in the cube)"], "i" : [-1, "%6d", "index of the cube (not in the cube)"], "x" : [-1, "%4d", "x-coordinate of the cube (not in the cube)"], "y" : [-1, "%4d", "y-coordinate of the cube (not in the cube)"], "t" : [-1, "%3d", "type of the cube"], "f" : [ 0, "%3d", "floor height in cubes"], "c" : [ 1, "%3d", "ceil height in cubes"], "|" : [ 2, "%3d", "wall texture ids"], "_" : [ 3, "%3d", "floor texture ids"], "-" : [ 4, "%3d", "ceil texture ids"], "v" : [ 5, "%3d", "vertex delta, used for heightfield cubes"], "u" : [ 6, "%3d", "upper wall tex id"], "g" : [ 7, "%3d", "used by triggers"] } cube_xref_keys = cube_xref.keys() cube_attrs_default = "ixytfcg" cube_solid_fmt = "2B" cube_solid_len = calcsize(cube_solid_fmt) cube_fmt = "2b6B" cube_len = calcsize(cube_fmt) # Used to decode the cube type, also from cube.h. cube_type_decode = [ "solid", # entirely solid cube [only specifies wtex] "corner", # half full corner of a wall "fhf", # floor heightfield using neighbour vdelta values "chf", # idem ceiling "space", # entirely empty cube "semisolid", # generated by mipmapping "maxtype"] cube_type_max = len(cube_type_decode) - 1 # A convenience for common indexes cube_type_idx = cube_xref["t"][0] cube_wtex_idx = cube_xref["|"][0] cube_vdelta_idx = cube_xref["v"][0] #### Common Functions #### # Look for the null terminater in a C-style string and truncate it. def c_string_to_python(c_str): null_idx = c_str.find("\x00") if null_idx == -1: return c_str else: return c_str[:null_idx] # Convert an entity to an integer, if possible def entity_to_int(entity, error_handler, action): # Try to convert them to integers, if possible. If not look them up. If # they still can't be found (the result should be an integer) then call # the error handler. Allow weird values to be specified numerically. entity_names = [e_decode[0] for e_decode in entity_type_decode] try: entity = int(entity) except ValueError: if entity in entity_names: entity = entity_names.index(entity) if type(entity) != int: error_handler("cannot %s unknown entity '%s'" % (action, entity)) return None return entity # Replace the header of a given map with the bytes specified. def set_header_bytes(m_fname, h_bytes): # This is a very inefficient way of replacing the header of a cube map. # It is done this way as a result of the fact that the GzipFile class does # not support working with files in read+write mode. Also it does not # support "rewind"ing files opened in write mode. Both of these things # can be done with ordiray file handles in python. old_hand = GzipFile(filename=m_fname) temp_hand = NamedTemporaryFile() new_hand = GzipFile(fileobj=temp_hand) # Copy old to new. We don't care about the header in the old one. old_hand.seek(header_len) new_hand.write(h_bytes) new_hand.write(old_hand.read()) # Close stuff. The temp file is not closed yet to keep the contests. old_hand.close() new_hand.close() temp_hand.flush() # Move the temporary file over m_fname os.rename(temp_hand.name, m_fname) try: temp_hand.close() except OSError: # Ignore it not being able to delete the file. pass # Write the header for a PPM file. def write_ppm_header(i_fobj, size, comment): i_fobj.write("P6\n" + "# COMMENT: %s\n" % comment + "%d %d\n" % (size, size) + "255\n") # Write a list of pixels into a PPM file. def add_ppm_pixels(i_fobj, size, pixel_list): # Go the beginning and then move past the header in order to get the # start of the binary data. i_fobj.seek(0) for discard in range(4): i_fobj.readline() data_start = i_fobj.tell() pixel_list.sort() # Sort first by pixel, then by color. Much faster. for pixel, color in pixel_list: i_fobj.seek(3 * pixel + data_start) i_fobj.write(color) # Convert a string of 'RRGGBB' form to a color. Could be enhanced to take # color names, such as "red". It could also be more robust. def color_str_to_color(color_str): r, g, b = color_str[0:2], color_str[2:4], color_str[4:6] return eval ("\"\\x%s\\x%s\\x%s\"" % (r, g, b)) #### Usage #### usage = "usage: %%prog [-h] [-n] [-w [-o out_fname] \ [-d entity1 [entity2 ... ]] [-r entity_from->entity_to]] [-s header_attrs] \ [-e entity_attrs] [-c cube_attrs] [-i image_fname [-x cube_image_attr] \ [-0 image_min] [-9 image_max] [-q entity1 [entity2 ... ]] \ [-f image_color_override] [-l image_color_solid]] map1 [map2 ... ]\n\ where header_attrs can be '.' (default of '%s') or any combination of:\n" % \ header_attrs_default header_xref_items = header_xref.items() header_xref_items.sort() for header_item in header_xref_items: attr_letter, (attr_col, attr_fmt, attr_desc) = header_item usage += "\t%s - %s\n" % (attr_letter, attr_desc) usage += \ "where entity_attrs can be '.' (default of '%s') or any combination of:\n" % \ entity_attrs_default entity_xref_items = entity_xref.items() entity_xref_items.sort() for entity_item in entity_xref_items: attr_letter, (attr_col, attr_fmt, attr_desc) = entity_item usage += "\t%s - %s\n" % (attr_letter, attr_desc) usage += \ "where cube_attrs can be '.' (default of '%s') or any combination of:\n" % \ cube_attrs_default cube_xref_items = cube_xref.items() cube_xref_items.sort() for cube_item in cube_xref_items: attr_letter, (attr_col, attr_fmt, attr_desc) = cube_item usage += "\t%s - %s\n" % (attr_letter, attr_desc) usage = usage.rstrip() # get rid of the final \n parser = OptionParser(usage=usage) parser.add_option("-n", "--numeric", action="store_true", dest="numeric", help="display numeric attributes numerically") parser.add_option("-s", "--header-attrs", dest="header_attrs", metavar="header_attrs", help="attributes of the header that are to be displayed") parser.add_option("-e", "--entity-attrs", dest="entity_attrs", metavar="entity_attrs", help="attributes of the entities that are to be displayed") parser.add_option("-c", "--cube-attrs", dest="cube_attrs", metavar="cube_attrs", help="attributes of the cubes that are to be displayed") parser.add_option("-w", "--write-mode", action="store_true", dest="write_mode", help="allow the maps to be outputted") parser.add_option("-o", "--out-fname", dest="out_fname", metavar="out_fname", help="output filename (default: -out.cgz)") parser.add_option("-d", "--entities-delete", dest="entities_delete", metavar="entities_delete", help="entity deletion, separated by white space \ (ex: -d 'rockets health', applied before replace)") parser.add_option("-r", "--entity-replace", dest="entity_replace", metavar="entity_replace", help="entity replacement (ex: -r 'boost->quad', make sure \ to quote '>')") parser.add_option("-i", "--image-file", dest="image_fname", metavar="image_fname", help="name of the image file") parser.add_option("-x", "--image-cube-attr", dest="image_cube_attr", metavar="image_cube_attr", help="cube attribute that will be plotted (default: \ f - floor height)", default="f") parser.add_option("-0", "--image-min", dest="image_min", metavar="image_min", type=int, help="value that corresponds to black for the \ image_cube_attr", default=-128) parser.add_option("-9", "--image-max", dest="image_max", metavar="image_max", type=int, help="value that corresponds to white for the \ image_cube_attr", default=128) parser.add_option("-q", "--image-entities", dest="image_entities", metavar="image_entities", default="playerstart shells \ bullets rockets rounds health boost greenarmour yellowarmour quad teleport \ teledest monster carrot", help="entities to be included in the image, separated by \ white space (default: playerstart shells bullets rockets rounds health boost \ greenarmour yellowarmour quad teleport teledest monster carrot)") parser.add_option("-f", "--image-color-override", dest="image_color_override", metavar="image_color_override", help="override the color of the entities plotted \ (RGB - 'RRGGBB' format)") parser.add_option("-l", "--image-color-solid", dest="image_color_solid", metavar="image_color_solid", default="405040", help="color of solid cubes (default: 405040) \ (RGB - 'RRGGBB' format)") (options, args) = parser.parse_args() if len(args) < 1: parser.error("at least one map is required") if (not options.write_mode) and options.entities_delete: parser.error("-w must be secified for -d") if (not options.write_mode) and options.entity_replace: parser.error("-w must be secified for -r") # Is there a better way of deterimining if non-boolean options are set than # to use exception handling? What about default values? if options.header_attrs == ".": options.header_attrs = header_attrs_default if options.header_attrs: for attr_letter in options.header_attrs: if not attr_letter in header_xref_keys: parser.error("uknown header_attr '%s'" % attr_letter) if options.entity_attrs == ".": options.entity_attrs = entity_attrs_default if options.entity_attrs: for attr_letter in options.entity_attrs: if not attr_letter in entity_xref_keys: parser.error("uknown entity_attr '%s'" % attr_letter) if options.cube_attrs == ".": options.cube_attrs = cube_attrs_default if options.cube_attrs: if not options.entity_attrs: # Entities have to be processed in order to get to the cubes. options.entity_attrs = "0" for attr_letter in options.cube_attrs: if not attr_letter in cube_xref_keys: parser.error("uknown cube_attr '%s'" % attr_letter) if options.entities_delete: entities_delete = [] for entity_delete in options.entities_delete.split(): entities_delete.append(entity_to_int(entity_delete, parser.error, "delete (-d)")) if not options.entity_attrs: # Entities have to be processed in order to change them. options.entity_attrs = "0" if options.entity_replace: entity_from, entity_to = re.search("(\S+)\s*->\s*(\S+)", options.entity_replace).groups() entity_from = entity_to_int(entity_from, parser.error, "replace (-r) from") entity_to = entity_to_int(entity_to, parser.error, "replace (-r) to") if not options.entity_attrs: # Entities have to be processed in order to change them. options.entity_attrs = "0" try: if options.image_fname: image_fobj = open(options.image_fname, "w+b") if not options.entity_attrs: # Entities have to be processed in order to generate the image. options.entity_attrs = "0" if not options.cube_attrs: # Cubes have to be processed in order to change them. options.cube_attrs = "0" except IOError, err: sys.stderr.write("uname to open the image file ('%s') for write" % options.image_fname) sys.exit(1) if not options.image_cube_attr in cube_xref_keys: parser.error("uknown cube_attr '%s' for image" % options.image_cube_attr) image_cube_idx = cube_xref[options.image_cube_attr][0] image_entities = [] for image_entity in options.image_entities.split(): image_entities.append(entity_to_int(image_entity, parser.error, "image entity (-q)")) if options.image_color_override: image_color_override = color_str_to_color(options.image_color_override) image_color_solid = color_str_to_color(options.image_color_solid) #### Map Processing #### bad_maps = False first_section = True for map_fname in args: try: if (map_fname.find(".cgz") == -1): map_hand = None map_hand = open(map_fname, "rb") else: map_hand = GzipFile(filename=map_fname) map_name = map_fname.replace(".cgz", "") header_bytes = map_hand.read(header_len) if options.write_mode: if (not options.out_fname) or ("-out.cgz" in options.out_fname): options.out_fname = "%s-out.cgz" % map_name if map_fname == options.out_fname: parser.error("writing to a map being read ('%s') is not \ supported" % map_fname) out_hand = GzipFile(filename=options.out_fname, mode="wb") out_hand.write(header_bytes) except IOError, err: bad_maps = True sys.stderr.write("%s: %s\n" % (map_fname, err)) continue if len(header_bytes) != header_len: bad_maps = True sys.stderr.write("%s: short read of header. got %d of %d bytes\n" % (map_name, len(header_bytes), header_len)) map_hand.close() continue # Convert the binary data to a tuple based on header_fmt. header_items = unpack(header_fmt, header_bytes) if header_items[header_version_idx] < 5: sys.stderr.write("%s: warning, not reliable for older maps\n" % map_name) if header_items[header_version_idx] < 4: # Ignore the last 64 bytes of the header for old maps. Allow # the water level to be invalid in this case. map_hand.seek(-64) # Set the following globals since it may be helpful later in # reading entities or cubes. The different naming convention for # "sfactor" and "numents" is based on Cube source code. sfactor = header_items[header_sfactor_idx] linear_size = 2 ** sfactor num_cubes = linear_size * linear_size numents = numents_out = header_items[header_numents_idx] # The handling of white space and appending of strings is inefficient # in the following, but it should be good enough. if options.header_attrs: # White space between sections. if options.header_attrs != "0": if first_section and (options.header_attrs != "0"): first_section = False else: if ((options.entity_attrs and (options.entity_attrs != "0")) or (options.cube_attrs and (options.cube_attrs != "0"))): print out_line = "" for attr_letter in options.header_attrs: attr_col, attr_fmt, attr_desc = header_xref[attr_letter] if attr_letter == "n": out_line += attr_fmt % map_name + " " continue attr_value = header_items[attr_col] if attr_fmt.find("s") != -1: attr_value = c_string_to_python(attr_value) out_line += attr_fmt % attr_value + " " sys.stdout.write(out_line.rstrip() + "\n") if options.entity_attrs: # White space between sections. if options.entity_attrs != "0": if first_section and (options.entity_attrs != "0"): first_section = False else: print # Don't do any IO error checking in the following. Assume doing it # on the header is sufficient. for entity_idx in xrange(numents): entity_bytes = map_hand.read(entity_len) entity_items = unpack(entity_fmt, entity_bytes) entity_type = entity_items[entity_type_idx] if options.write_mode: if (options.entities_delete and entity_type in entities_delete): numents_out -= 1 continue # Don't write this entity. if (options.entity_replace and (entity_type == entity_from)): entity_items_array = [] entity_items_array.extend(entity_items) entity_items_array[entity_type_idx] = entity_to entity_items = entity_items_array entity_bytes = pack(entity_fmt, *entity_items) out_hand.write(entity_bytes) if options.image_fname: # If we are generating an image then for the entities # specified add them to the list of pixels. Use the colors in # entity_type_decode, unless they are overridden with # image_color_override. if options.image_entities and (entity_type in image_entities): if options.image_color_override: entity_color = image_color_override else: entity_color = entity_type_decode[entity_type][1] entity_pixel_list.append( (entity_items[entity_x_idx] + linear_size * entity_items[entity_y_idx], entity_color)) # Process the entities without priting them. if options.entity_attrs == "0": continue out_line = "" for attr_letter in options.entity_attrs: attr_col, attr_fmt, attr_desc = entity_xref[attr_letter] if attr_letter == "n": out_line += attr_fmt % map_name + " " continue elif attr_letter == "i": out_line += attr_fmt % entity_idx + " " continue attr_value = entity_items[attr_col] if (attr_letter == "t") and (not options.numeric): out_line += "%-12s " % \ ((attr_value <= entity_type_max) and entity_type_decode[attr_value][0] or "????") continue if attr_fmt.find("s") != -1: attr_value = c_string_to_python(attr_value) out_line += attr_fmt % attr_value + " " sys.stdout.write(out_line.rstrip() + "\n") if options.cube_attrs: # White space between sections. if options.cube_attrs != "0": if first_section and (options.cube_attrs != "0"): first_section = False else: print # Prepare the image file. if options.image_fname: write_ppm_header(image_fobj, linear_size, "image of %s" % map_name) # For cubes the out file, if any, is written to as soon as the map # file is read since there is no cube processing at this time. # Process the cubes without printing them. if options.cube_attrs == "0": print_cubes = False if options.image_fname: options.cube_attrs = options.image_cube_attr else: print_cubes = True cube_idx = 0 while cube_idx < num_cubes: cube_type_bytes = map_hand.read(1) if len(cube_type_bytes): cube_type, = unpack("B", cube_type_bytes) else: break if options.write_mode: out_hand.write(cube_type_bytes) if cube_type == 255: # The previous cube is to be repeated a specified number of # times cube_repeat_bytes = map_hand.read(1) if options.write_mode: out_hand.write(cube_repeat_bytes) cube_repeat, = unpack("B", cube_repeat_bytes) if options.image_fname: image_fobj.write(attr_color * cube_repeat) if print_cubes: print "previous cube repeated %s times" % cube_repeat cube_idx += cube_repeat continue elif cube_type == 0: # solid cube_solid_bytes = map_hand.read(cube_solid_len) if options.write_mode: out_hand.write(cube_solid_bytes) cube_solid_items = unpack(cube_solid_fmt, cube_solid_bytes) # Kludge to make it look like a default cube. cube_items = [0] * 8 cube_items[cube_wtex_idx] = cube_solid_items[0] cube_items[cube_vdelta_idx] = cube_solid_items[1] else: # default cube_bytes = map_hand.read(cube_len) if options.write_mode: out_hand.write(cube_bytes) cube_items = unpack(cube_fmt, cube_bytes) # Process the cubes without priting them. if options.cube_attrs == "0": continue out_line = "" for attr_letter in options.cube_attrs: attr_col, attr_fmt, attr_desc = cube_xref[attr_letter] if attr_letter == "t": if options.numeric: out_line += attr_fmt % cube_type + " " else: out_line += "%-9s " % \ ((cube_type <= cube_type_max) and cube_type_decode[cube_type] or "????") continue if attr_letter == "n": out_line += attr_fmt % map_name + " " continue elif attr_letter == "i": out_line += attr_fmt % cube_idx + " " continue elif attr_letter == "x": out_line += attr_fmt % (cube_idx % linear_size) + " " continue elif attr_letter == "y": out_line += attr_fmt % (cube_idx / linear_size) + " " continue attr_value = cube_items[attr_col] if options.image_fname and (attr_col == image_cube_idx): if cube_type == 0: # solid: attr_color = image_color_solid else: attr_bright = 0xff * \ ((attr_value - options.image_min) / float(options.image_max - options.image_min)) if attr_bright < 0: attr_bright = 0 elif attr_bright > 0xff: attr_bright = 0xff attr_color = pack("3B", attr_bright, attr_bright, attr_bright) image_fobj.write(attr_color) out_line += attr_fmt % attr_value + " " if print_cubes: sys.stdout.write(out_line.rstrip() + "\n") cube_idx += 1 # The following is here until we have cube processing to do the rest. if options.write_mode: out_hand.write(map_hand.read()) out_hand.close() # If need be adjust the header of the new map to compensate for # deleted entities. if numents != numents_out: header_items_array = [] header_items_array.extend(header_items) header_items_array[header_numents_idx] = numents_out header_items = header_items_array header_bytes = pack(header_fmt, *header_items) set_header_bytes(options.out_fname, header_bytes) map_hand.close() if options.image_fname: # Write out the entity pixels, if any if len(entity_pixel_list): add_ppm_pixels(image_fobj, linear_size, entity_pixel_list) image_fobj.close() if bad_maps: sys.exit(1)