diff --git a/bwmap.py b/bwmap.py new file mode 100644 index 0000000..3db4e6b --- /dev/null +++ b/bwmap.py @@ -0,0 +1,507 @@ +import sys +import struct + +# sorry not sorry +P = struct.pack +U = struct.unpack + +# notes: +# SCMDraft 2 requires, at bare minimum: +scmdraft_req = b"ERA ~DIM ~TILE~UNIT~PUNI~IOWN~THG2~SPRP~WAV ~MASK~STR ~OWNR~SIDE~FORC".split(b"~") + +# Starcraft 1.18+ Melee requires, at bare minimum: +# absolutely required: +# VER TYPE OWNR SIDE COLR ERA DIM MTXM UNIT THG2 SPRP FORC +# not needed at all: +# IVER IOWN ISOM TILE DD2 +# not required for Melee mode (non-UMS): +# PUPx UPGx PUNI MASK MRGN WAV PTEx UNIx TECx TRIG MBRF UPRP UPUS SWNM +# special cases: +# VCOD partly: 34 19 00 00 00 00 00 00 00 00 00 00 00 00 00 00 1F 29 7D A6 D7 B0 00 BB CC 31 24 ED 17 4C 13 0B +# STR kinda (can be empty for melee) +melee_req = b"VER ~TYPE~VCOD~OWNR~SIDE~COLR~ERA ~DIM ~MTXM~UNIT~THG2~STR ~SPRP~FORC".split(b"~") + +# TODO: try more things to add to this list. +ums_req = melee_req + b"MRGN~PTEx~PUPx~TECx~TRIG~UNIx~UPGx~UPRP".split(b"~") + +both_req = set(melee_req + scmdraft_req) + +known_keys = [ # in the order we want to write them. + # boring filetype stuff + b"TYPE", + b"VER ", + b"IVER", + b"IVE2", + b"VCOD", + + b"SPRP", # scenario name and description + b"COLR", # player colors + b"FORC", # player forces and settings + b"SIDE", # player races + b"IOWN", # editor player types + b"OWNR", # ingame player types + + b"DIM ", # map dimensions + b"ERA ", # tileset + b"MTXM", # ingame tiles + b"TILE", # editor tiles + b"ISOM", # editor isometric tiles + b"MASK", # fog of war per tile + b"DD2 ", # editor doodads + b"UNIT", # placed units + b"THG2", # placed sprites (e.g. doodad) + + b"MRGN", # locations + + b"UNIS", # unit settings (a bunch of stuff, including names) + b"UNIx", # broodwar override + + b"PUNI", # build unit restrictions + + b"PTEC", # tech restrictions + b"PTEx", # broodwar override + + b"UPGR", # upgrade restrictions (and defaults, max levels...) + b"PUPx", # broodwar override + + b"TECS", # tech settings (costs and times) + b"TECx", # broodwar override + + b"UPGS", # upgrade settings (costs and times) + b"UPGx", # broodwar override + + b"UPRP", # properties of spawn-with-properties + b"UPUS", # booleans of which properties are used + + b"MBRF", # mission briefings (triggers without conditions) + b"TRIG", # triggers + + b"SWNM", # switch names + b"WAV ", # wav paths (editor only) + b"STR ", # string data +] + +skippable_keys = set(known_keys).symmetric_difference(both_req) + +vcod_valid = b"\ +\x34\x19\xCA\x77\x99\xDC\x68\x71\x0A\x60\xBF\xC3\xA7\xE7\x75\xA7\ +\x1F\x29\x7D\xA6\xD7\xB0\x3A\xBB\xCC\x31\x24\xED\x17\x4C\x13\x0B\ +\x65\x20\xA2\xB7\x91\xBD\x18\x6B\x8D\xC3\x5D\xDD\xE2\x7A\xD5\x37\ +\xF6\x59\x64\xD4\x63\x9A\x12\x0F\x43\x5C\x2E\x46\xE3\x74\xF8\x2A\ +\x08\x6A\x37\x06\x37\xF6\xD6\x3B\x0E\x94\x63\x16\x45\x67\x5C\xEC\ +\xD7\x7B\xF7\xB7\x1A\xFC\xD4\x9E\x73\xFA\x3F\x8C\x2E\xC0\xE1\x0F\ +\xD1\x74\x09\x07\x95\xE3\x64\xD7\x75\x16\x68\x74\x99\xA7\x4F\xDA\ +\xD5\x20\x18\x1F\xE7\xE6\xA0\xBE\xA6\xB6\xE3\x1F\xCA\x0C\xEF\x70\ +\x31\xD5\x1A\x31\x4D\xB8\x24\x35\xE3\xF8\xC7\x7D\xE1\x1A\x58\xDE\ +\xF4\x05\x27\x43\xBA\xAC\xDB\x07\xDC\x69\xBE\x0A\xA8\x8F\xEC\x49\ +\xD7\x58\x16\x3F\xE5\xDB\xC1\x8A\x41\xCF\xC0\x05\x9D\xCA\x1C\x72\ +\xA2\xB1\x5F\xA5\xC4\x23\x70\x9B\x84\x04\xE1\x14\x80\x7B\x90\xDA\ +\xFA\xDB\x69\x06\xA3\xF3\x0F\x40\xBE\xF3\xCE\xD4\xE3\xC9\xCB\xD7\ +\x5A\x40\x01\x34\xF2\x68\x14\xF8\x38\x8E\xC5\x1A\xFE\xD6\x3D\x4B\ +\x53\x05\x05\xFA\x34\x10\x45\x8E\xDD\x91\x69\xFE\xAF\xE0\xEE\xF0\ +\xF3\x48\x7E\xDD\x9F\xAD\xDC\x75\x62\x7A\xAC\xE5\x31\x1B\x62\x67\ +\x20\xCD\x36\x4D\xE0\x98\x21\x74\xFB\x09\x79\x71\x36\x67\xCD\x7F\ +\x77\x5F\xD6\x3C\xA2\xA2\xA6\xC6\x1A\xE3\xCE\x6A\x4E\xCD\xA9\x6C\ +\x86\xBA\x9D\x3B\xB5\xF4\x76\xFD\xF8\x44\xF0\xBC\x2E\xE9\x6E\x29\ +\x23\x25\x2F\x6B\x08\xAB\x27\x44\x7A\x12\xCC\x99\xED\xDC\xF2\x75\ +\xC5\x3C\x38\x7E\xF7\x1C\x1B\xC5\xD1\x2D\x94\x65\x06\xC9\x48\xDD\ +\xBE\x32\x2D\xAC\xB5\xC9\x32\x81\x66\x4A\xD8\x34\x35\x3F\x15\xDF\ +\xB2\xEE\xEB\xB6\x04\xF6\x4D\x96\x35\x42\x94\x9C\x62\x8A\xD3\x61\ +\x52\xA8\x7B\x6F\xDC\x61\xFC\xF4\x6C\x14\x2D\xFE\x99\xEA\xA4\x0A\ +\xE8\xD9\xFE\x13\xD0\x48\x44\x59\x80\x66\xF3\xE3\x34\xD9\x8D\x19\ +\x16\xD7\x63\xFE\x30\x18\x7E\x3A\x9B\x8D\x0F\xB1\x12\xF0\xF5\x8C\ +\x0A\x78\x58\xDB\x3E\x63\xB8\x8C\x3A\xAA\xF3\x8E\x37\x8A\x1A\x2E\ +\x5C\x31\xF9\xEF\xE3\x6D\xE3\x7E\x9B\xBD\x3E\x13\xC6\x44\xC0\xB9\ +\xBC\x3A\xDA\x90\xA4\xAD\xB0\x74\xF8\x57\x27\x89\x47\xE6\x3F\x37\ +\xE4\x42\x79\x5A\xDF\x43\x8D\xEE\xB4\x0A\x49\xE8\x3C\xC3\x88\x1A\ +\x88\x01\x6B\x76\x8A\xC3\xFD\xA3\x16\x7A\x4E\x56\xA7\x7F\xCB\xBA\ +\x02\x5E\x1C\xEC\xB0\xB9\xC9\x76\x1E\x82\xB1\x39\x3E\xC9\x57\xC5\ +\x19\x24\x38\x4C\x5D\x2F\x54\xB8\x6F\x5D\x57\x8E\x30\xA1\x0A\x52\ +\x6D\x18\x71\x5E\x13\x06\xC3\x59\x1F\xDC\x3E\x62\xDC\xDA\xB5\xEB\ +\x1B\x91\x95\xF9\xA7\x91\xD5\xDA\x33\x53\xCE\x6B\xF5\x00\x70\x01\ +\x7F\xD8\xEE\xE8\xC0\x0A\xF1\xCE\x63\xEB\xB6\xD3\x78\xEF\xCC\xA5\ +\xAA\x5D\xBC\xA4\x96\xAB\xF2\xD2\x61\xFF\xEA\x9A\xA8\x6A\xED\xA2\ +\xBD\x3E\xED\x61\x39\xC1\x82\x92\x16\x36\x23\xB1\xB0\xA0\x24\xE5\ +\x05\x9B\xA7\xAA\x0D\x12\x9B\x33\x83\x92\x20\xDA\x25\xB0\xEC\xFC\ +\x24\xD0\x38\x23\xFC\x95\xF2\x74\x80\x73\xE5\x19\x97\x50\x7D\x44\ +\x45\x93\x44\xDB\xA2\xAD\x1D\x69\x44\x14\xEE\xE7\x2C\x7F\x87\xFF\ +\x38\x9E\x32\xF1\x4D\xBC\x29\xDA\x42\x27\x26\xFE\xC1\xD2\x2B\xA9\ +\xF6\x42\x7A\x0E\xCB\xE8\x7C\xD1\x0F\x5B\xEC\x56\x69\xB7\x61\x31\ +\xB4\x6D\xF9\x25\x40\x34\x79\x6D\xFA\x53\xA7\x0B\xFA\xA4\x82\xCE\ +\xC3\x45\x49\x61\x0D\x45\x2C\x8F\x28\x49\x60\xF7\xF3\x7D\xC9\x1E\ +\x0F\xD0\x89\xC1\x26\x52\xF8\xD3\x4D\x8F\x35\x14\xBA\x9D\x5F\x0B\ +\x07\xA9\x4A\x00\xF7\x22\x26\x2F\x3E\x67\xFB\x1F\xA1\x9C\x11\xC6\ +\x69\x4F\x5D\x66\x58\x34\x15\x90\x6C\xE5\x54\x46\xAF\x5F\x63\xD6\ +\x8A\x0C\x95\xDF\xBD\x0D\xE4\xAF\xBF\x40\x40\x4C\xA3\xF6\x51\x71\ +\x29\xED\x26\xF8\x85\x28\x22\xD5\xBF\xBE\xCF\xFA\x28\xC5\x7F\x51\ +\xB8\x06\x63\x07\xEC\xBD\x8F\x29\xFA\x55\x7E\x71\x1A\x40\x32\x66\ +\xE8\xD4\xDE\x9D\xD4\x5E\xFC\x93\x7A\x3D\xD5\x3B\xCD\x75\x2E\x80\ +\x0A\x4F\x74\x87\x1B\xCC\x8F\xEA\x9A\xA9\xDB\x7C\x16\x53\xE5\xEF\ +\xAB\x78\xC1\x6E\xA4\x72\x89\x5A\x98\x2C\x70\x50\xFB\xA1\xDF\x1F\ +\x6B\xB7\xD9\x44\x07\x80\x82\x56\xFD\xBF\xC0\x83\x0E\x49\xD0\x5B\ +\x1E\x68\x6A\x0E\x9A\xC2\x0B\x2F\x8E\x43\xA0\xE1\x99\x0C\xF6\xB2\ +\xE0\x7A\x1C\x5E\x2C\xC8\xA0\x45\x3C\x0B\xE9\x88\xAC\xB9\x96\xC6\ +\x74\xAE\x83\x2A\xBB\x13\xFA\x65\xEB\x4F\x1F\xA6\xB0\x8A\x8A\xE1\ +\x81\xE9\xB8\xB9\xD5\x55\x15\x4E\x45\xF2\xAD\x9B\x3E\xC2\x35\x7E\ +\x5F\x92\x2E\x72\xB6\x5B\x68\x23\x6E\xC6\x45\x0E\xE9\x3B\x87\xD4\ +\xF4\x41\xC0\xE3\xA8\x05\x44\xBE\xE4\x0F\x8A\x13\x1A\xC4\x37\xF4\ +\x5A\x40\x55\xEF\x9D\x79\x1D\x4B\x4A\x79\x3A\x9C\x76\x85\x37\xCC\ +\x82\x3D\x0F\xB6\x60\xA6\x93\x7E\xBD\x5C\xC2\xC4\x72\xC7\x7F\x90\ +\x4D\x1B\x96\x10\x13\x05\x68\x68\x35\xC0\x7B\xFF\x46\x85\x43\x2A\ +\x01\x04\x05\x06\x02\x01\x05\x02\x00\x03\x07\x07\x05\x04\x06\x03\ +" + +mrgn_default = bytearray(b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" * 255) +# set up the "Anywhere" location correctly. this is always location 64. +mrgn_default[63*20 + 9] = 0x10 +mrgn_default[63*20 + 13] = 0x10 +mrgn_default[63*20 + 16] = 0x03 +mrgn_default = bytes(mrgn_default) + +def readit(f): + f.seek(0, 2) + fs = f.tell() + f.seek(0) + + src = {} + + while f.tell() < fs: + k = f.read(4) + + try: + name = k.decode('ascii') + except UnicodeDecodeError: + print("! skipping invalid section name:", k) + size = U(" 0: + f.read(size) + continue + + size = U(" 0 and type(strings[0]) != bytes: + strings = [str(s).encode("CP1252") for s in strings] + + flat = b"\0" + b"\0".join(strings) + + offsets = [] + o = count * 2 + 3 + for s in strings: + offsets += [o] + o += len(s) + 1 + + out = P("= 12: + print("# ignoring start location for player", pi) + placed_starts[pi] = True + + k = b"ERA " + if k in src: + for ki, v in enumerate(src[k]): + era = U("force mapping + 0,0,0,0, # strings for forces + 1,1,1,1, # bitmasks: randstart, ally, allyvic, vis, unused... + ) + src[b"FORC"] = [b] + assert len(b) == 20, b + + if b"IOWN" not in src: + # FIXME: possibly invalid. + b = b"" + for i in range(8): + b += b"\6" if placed_starts[i] else b"\0" + b += b"\0\0\0\7" + src[b"IOWN"] = [b] + assert len(b) == 12, b + + if b"MRGN" not in src: + src[b"MRGN"] = [mrgn_default] + + if b"THG2" not in src: + src[b"THG2"] = [b""] + + if b"SPRP" not in src: + src[b"SPRP"] = [b"\0\0\0\0"] + + if b"WAV " not in src: + src[b"WAV "] = [b"\0\0\0\0" * 512] + + #if b"ISOM" not in src: + # size = (w // 2 + 1) * (h + 1) * 4 + # src[b"ISOM"] = [b'\0' * size] + + if b"PUNI" not in src: + src[b"PUNI"] = [b"\1" * 5700] + + done = {} + for k in known_keys: + if k in src: + for v in src[k]: + W(k, v) + done[k] = True + + for k, v in src.items(): + if not done[k]: + print("# deferred write:", k) + W(k, v) + + for k in known_keys: + if k not in skippable_keys and k not in wroteonce: + print("# never wrote section:", k) + + for k in scmdraft_req: + if k not in wroteonce: + print("# map might not open in SCMDraft 2. missing", k) + for k in melee_req: + if k not in wroteonce: + print("# map might not open in StarCraft (Melee). missing", k) + for k in ums_req: + if k not in wroteonce: + print("# map might not open in StarCraft (Use Map Settings). missing", k) + +def run(args): + for fn in args: + with open(fn, 'rb') as f: + src = readit(f) + + if src is None: + print("! failed to rewrite", fn) # FIXME: reuse of terminology. + ofn = fn.replace('.chk', '') + '.x.chk' + assert fn != ofn, ofn + rewrite(src) + with open(ofn, 'wb') as f: + writeit(f, src) + +if __name__ == '__main__': + ret = run(sys.argv[1:]) + sys.exit(ret)