#!/usr/bin/env python3 from argparse import ArgumentParser from lxml import etree from pathlib import Path from PIL import Image import sys def main(): parser = ArgumentParser(description="Cleanup rc3.world maps") parser.add_argument("map", help="map in .tmx format") parser.add_argument("-o", "--output", help="where to write modified map") parser.add_argument( "-i", "--in-place", help="replace map with modified version", action="store_true", ) parser.add_argument( "--fix-copyright", help="add mapCopyright/tilesetCopyright properties", action="store_true", ) parser.add_argument( "--separate-collisions", help="move collisions into a separate 'collisions' layer", action="store_true", ) parser.add_argument( "--split-tilesets", help="split tilesets bigger than 4096x4096", action="store_true", ) args = parser.parse_args() output = args.output if args.in_place: output = args.map if not output: print("Output unspecified.", file=sys.stderr) sys.exit(1) tree = etree.parse(args.map) map = tree.getroot() if args.fix_copyright: fix_copyright(map) if args.separate_collisions: separate_collisions(map) if args.split_tilesets: split_tilesets(map) tree.write(output) def fix_copyright(map): if map.find("properties/property[@name='mapCopyright']") is None: license = input("Map license? [CC0] ") or "CC0" properties = map.find("properties") if properties is None: properties = etree.SubElement(map, "properties") etree.SubElement(properties, "property", name="mapCopyright", value=license) for tileset in map.findall("tileset"): if tileset.find("properties/property[@name='tilesetCopyright']") is None: license = ( input("Tileset '%s' license? [CC0] " % tileset.attrib["name"]) or "CC0" ) properties = tileset.find("properties") if properties is None: properties = etree.SubElement(tileset, "properties") etree.SubElement( properties, "property", name="tilesetCopyright", value=license ) def separate_collisions(map): # find tiles with collides property colliding_tiles = set() firstgid = 1 tilecount = 0 for tileset in map.findall("tileset"): if tileset.attrib["name"] == "collisions": continue firstgid = int(tileset.attrib["firstgid"]) tilecount = int(tileset.attrib["tilecount"]) for tile in tileset.findall("tile"): properties = tile.find("properties") if properties is None: continue for collision_property in properties.findall("property[@name='collides']"): properties.remove(collision_property) if collision_property.attrib["value"] != "true": continue colliding_tiles.add(firstgid + int(tile.attrib["id"])) # create/find collisions tileset (single white tile) collisions_tileset = map.find("tileset[@name='collisions']") if collisions_tileset is None: collisions_tileset = etree.Element( "tileset", firstgid=str(firstgid + tilecount), name="collisions", tilewidth="32", tileheight="32", tilecount="1", columns="1", ) tileset.addnext(collisions_tileset) etree.SubElement( etree.SubElement(collisions_tileset, "properties"), "property", name="tilesetCopyright", value="not copyrightable", ) image_source = Path("imgs") / "collisions.png" image_source.parent.mkdir(parents=True, exist_ok=True) Image.new("1", (32, 32), (1)).save(image_source) etree.SubElement( collisions_tileset, "image", source=str(image_source), width="32", height="32", ) etree.SubElement( etree.SubElement( etree.SubElement(collisions_tileset, "tile", id="0"), "properties" ), "property", name="collides", type="bool", value="true", ) collisions_gid = int(collisions_tileset.attrib["firstgid"]) # merge collisions collisions = [0] * int(map.attrib["width"]) * int(map.attrib["height"]) for layer in map.findall("layer"): if layer.attrib["name"] == "collisions": continue data = [int(x) for x in layer.find("data[@encoding='csv']").text.split(",")] for i, gid in enumerate(data): if gid in colliding_tiles: collisions[i] = collisions_gid # create/find collisions layer and save collision data collisions_data = map.find("layer[@name='collisions']/data") if collisions_data is None: collisions_layer = etree.Element( "layer", id=map.attrib["nextlayerid"], name="collisions", width=map.attrib["width"], height=map.attrib["height"], ) map.find("layer").addprevious(collisions_layer) map.attrib["nextlayerid"] = str(int(map.attrib["nextlayerid"]) + 1) collisions_data = etree.SubElement( collisions_layer, "data", encoding="csv", ) collisions_data.text = ",".join([str(x) for x in collisions]) def split_tilesets(map): # we need the offset of the tileset inside the map element to insert new tilesets correctly for offset, tileset in enumerate(map): if tileset.tag != "tileset": continue image = tileset.find("image") if int(image.attrib["width"]) <= 4096 and int(image.attrib["height"]) <= 4096: continue dosplit = input("Split tileset '%s'? [yN] " % tileset.attrib["name"]) if dosplit.lower() != "y": continue properties = tileset.find("properties") firstgid = int(tileset.attrib["firstgid"]) tilewidth = int(tileset.attrib["tilewidth"]) tileheight = int(tileset.attrib["tileheight"]) image_source = Path(image.attrib["source"]) with Image.open(image_source) as im: for i, x in enumerate(range(0, im.width, 4096)): for j, y in enumerate(range(0, im.height, 4096)): croped_source = image_source.with_stem( "%s_%02d%02d" % (image_source.stem, i, j) ) im.crop( (x, y, min(x + 4096, im.width), min(y + 4096, im.height)) ).save(croped_source) width = min(4096, im.width - x) height = min(4096, im.height - y) columns = width // tilewidth tilecount = columns * (height // tileheight) tileset_croped = etree.Element( "tileset", name="%s (%d, %d)" % (tileset.attrib["name"], i, j), firstgid=str(firstgid), tilewidth=str(tilewidth), tileheight=str(tileheight), tilecount=str(tilecount), columns=str(columns), ) map.insert(offset + i + j, tileset_croped) firstgid += tilecount if properties is not None: tileset_croped.append(properties) etree.SubElement( tileset_croped, "image", source=str(croped_source), width=str(width), height=str(height), ) map.remove(tileset) if __name__ == "__main__": main()