From 2682bc5d1d864341aaeb42a449db73c3ecd16d70 Mon Sep 17 00:00:00 2001 From: "Juan J. Martinez" Date: Wed, 30 Dec 2020 19:07:31 +0000 Subject: Initial import --- tools/map.py | 385 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 385 insertions(+) create mode 100755 tools/map.py (limited to 'tools/map.py') diff --git a/tools/map.py b/tools/map.py new file mode 100755 index 0000000..5c6e27e --- /dev/null +++ b/tools/map.py @@ -0,0 +1,385 @@ +#!/usr/bin/env python3 + +from argparse import ArgumentParser +from collections import defaultdict +from os import path +import json +import os +import subprocess +import struct +import sys +import tempfile +import traceback + +__version__ = "1.0" + +DEF_ROOM_WIDTH = 32 +DEF_ROOM_HEIGHT = 24 +DEF_BITS = 8 + +DEF_MAP_CONF = "map_conf.json" + +""" +Format: + + 2 bytes: map data length (0 for empty map; no more data included) + 1 byte: entities length (1 is just the terminator 0xff) + map length bytes: map data (n-bit per tile) H x W x n (may be compressed) + i bytes: entity data (0xff for end) + +Expected layers: Map and Entities +""" + + +def apultra_compress(data): + with tempfile.NamedTemporaryFile() as fd: + fd.write(bytearray(data)) + fd.flush() + + ap_name = fd.name + ".ap" + subprocess.call(["apultra", "-v", fd.name, ap_name], stdout=sys.stderr) + + with open(ap_name, "rb") as fd: + out = fd.read() + os.unlink(ap_name) + + return [int(byte) for byte in out] + + +def find_name(data, name): + for item in data: + if item.get("name").lower() == name.lower(): + return item + raise ValueError("%r not found" % name) + + +def find_id(data, id): + for item in data: + if item.get("id") == id: + return item + raise ValueError("id %r not found", id) + + +def get_property(obj, name, default): + props = obj.get("properties", {}) + + if isinstance(props, dict): + return props.get(name, default) + + for p in props: + if p["name"] == name: + return p["value"] + + return default + + +def main(): + + parser = ArgumentParser(description="Map importer", + epilog="Copyright (C) 2020 Juan J Martinez ", + ) + + parser.add_argument( + "--version", action="version", version="%(prog)s " + __version__) + parser.add_argument( + "--room-width", dest="rw", default=DEF_ROOM_WIDTH, type=int, + help="room width (default: %s)" % DEF_ROOM_WIDTH) + parser.add_argument( + "--room-height", dest="rh", default=DEF_ROOM_HEIGHT, type=int, + help="room height (default: %s)" % DEF_ROOM_HEIGHT) + parser.add_argument("--max-ents", dest="max_ents", default=0, type=int, + help="max entities per room (default: unlimited)") + parser.add_argument("--max-bytes", dest="max_bytes", default=0, type=int, + help="max bytes per room (default: unlimited)") + parser.add_argument("-b", dest="bin", action="store_true", + help="output binary data (default: C code)") + parser.add_argument("-d", dest="dir", default=".", type=str, + help="directory to generate the bin files (default: .)") + parser.add_argument("-c", dest="conf", default=DEF_MAP_CONF, type=str, + help="JSON configuration file (default: %s)" % DEF_MAP_CONF) + parser.add_argument("--aplib", dest="aplib", action="store_true", + help="APLIB compressed") + parser.add_argument("-r", dest="reverse", action="store_true", + help="Reverse map order") + parser.add_argument("-q", dest="quiet", action="store_true", + help="Don't output stats on stderr") + parser.add_argument("map_json", help="Map to import") + parser.add_argument("id", help="variable name") + + args = parser.parse_args() + + with open(args.conf, "rt") as fd: + conf = json.load(fd) + + et_names = [d["name"] for d in conf["entities"]] + et_weigths = dict((d["name"], d["w"]) for d in conf["entities"]) + et_bytes = dict((d["name"], d["bytes"]) for d in conf["entities"]) + + with open(args.map_json, "rt") as fd: + data = json.load(fd) + + mh = data.get("height", 0) + mw = data.get("width", 0) + + if mh < args.rh or mh % args.rh: + parser.error("Map size height not multiple of the room size") + if mw < args.rw or mw % args.rw: + parser.error("Map size witdh not multiple of the room size") + + tilewidth = data["tilewidth"] + tileheight = data["tileheight"] + + tile_layer = find_name(data["layers"], "Map")["data"] + + def_tileset = find_name(data["tilesets"], "default") + firstgid = def_tileset.get("firstgid") + + out = [] + for y in range(0, mh, args.rh): + for x in range(0, mw, args.rw): + block = [] + for j in range(args.rh): + for i in range(args.rw): + block.append(tile_layer[x + i + (y + j) * mw] - firstgid) + + # pack + current = [] + for i in range(0, args.rh * args.rw, 8 // DEF_BITS): + tiles = [] + for k in range(8 // DEF_BITS): + tiles.append(block[i + k]) + + b = 0 + pos = 8 + for k in range(8 // DEF_BITS): + pos -= DEF_BITS + b |= (tiles[k] & ((2**DEF_BITS) - 1)) << pos + + current.append(b) + + out.append(current) + + # track empty maps + empty = [] + for i, block in enumerate(out): + if all([byte == 0xff for byte in block]): + empty.append(i) + + if args.aplib: + compressed = [] + for i, block in enumerate(out): + if i in empty: + compressed.append(None) + continue + compressed.append(apultra_compress(block)) + out = compressed + + # add the map header + for i in range(len(out)): + if out[i] is None: + continue + size = len(out[i]) + + # ents size placeholder 0 + out[i] = [size & 0xff, size >> 8, 0] + out[i] + + entities_layer = find_name(data["layers"], "Entities") + if len(entities_layer): + map_ents = defaultdict(list) + map_ents_w = defaultdict(int) + map_ents_bytes = defaultdict(int) + map_ents_names = set() + + def check_bytes(name): + if name not in map_ents_names: + # update the entity size in bytes count per map + try: + map_ents_bytes[m] += et_bytes[name] + map_ents_names.add(name) + except KeyError: + parser.error("max_bytes: no 'bytes' found for %r" % name) + + try: + objs = sorted( + entities_layer["objects"], key=lambda o: et_names.index(o["name"].lower())) + except ValueError: + parser.error("map has an unnamed object") + + for obj in objs: + name = obj["name"].lower() + m = ((obj["x"] // tilewidth) // args.rw) \ + + (((obj["y"] // tileheight) // args.rh) * (mw // args.rw)) + x = obj["x"] % (args.rw * tilewidth) + y = obj["y"] % (args.rh * tileheight) + + if name == "blocked": + if obj["width"] > obj["height"]: + if y == 0: + # up blocked + out[m][1] |= (1 << 4) + else: + # down blocked + out[m][1] |= (1 << 5) + else: + if x == 0: + # left blocked + out[m][1] |= (1 << 6) + else: + # tight blocked + out[m][1] |= (1 << 7) + continue + + t = et_names.index(name) + + # MSB is direction + + param = int(get_property(obj, "param", 0)) + if param == 1: + t |= 128 + + if args.max_ents: + # update the entity count per map + try: + map_ents_w[m] += et_weigths[name] + except KeyError: + parser.error("max_ents: no 'w' found for %r" % name) + + if args.max_bytes: + check_bytes(name) + + special = None + + # specials + if get_property(obj, "fixed", None) is not None: + if obj["width"] >= obj["height"]: + special = obj["width"] - tilewidth + if not param: + x += special + special //= tilewidth + # flag horizonal + special |= 128 + else: + special = obj["height"] - tileheight + if not param: + y += special + special //= tileheight + + if name == "elevator": + try: + respawn = json.loads(get_property(obj, "respawn", "[]")) + if args.max_bytes: + for name in respawn: + check_bytes(name) + special = [et_names.index(et) for et in respawn] + assert(len(special) < 255) + except Exception as ex: + parser.error("Error parsing respawn: %s" % ex) + + # terminator + special.append(0xff) + # size + special = [len(special), ] + special + + map_ents[m].extend([t, x, y]) + if special is not None: + if isinstance(special, (tuple, list)): + map_ents[m].extend(special) + else: + map_ents[m].append(special) + + if args.max_ents: + for i, weight in map_ents_w.items(): + if weight > args.max_ents: + parser.error("map %i has %d entities, max is %d" % + (i, weight, args.max_ents)) + + if args.max_bytes: + for i, byts in map_ents_bytes.items(): + if byts > args.max_bytes: + parser.error("map %i entities are %d bytes, max is %d" % + (i, byts, args.max_bytes)) + + # append the entities to the map data + for i in range(len(out)): + if not out[i]: + continue + elif map_ents[i]: + out[i].extend(map_ents[i]) + out[i][2] += len(map_ents[i]) + # terminator + out[i].append(0xff) + out[i][2] += 1 + + if args.reverse: + out.reverse() + + if args.bin: + for i, block in enumerate(out): + filename = path.join(args.dir, "%s%02d.bin" % (args.id, i)) + with open(filename, "wb") as fd: + if i in empty: + fd.write(struct.pack("