from common import * import array import sys from ctypes import create_string_buffer def RGB5A3Encode(tex): tex = tex.toImage() w, h = tex.width(), tex.height() padW = (w + 3) & ~3 padH = (h + 3) & ~3 destBuffer = create_string_buffer(padW * padH * 2) shortstruct = struct.Struct('>H') sspack = shortstruct.pack_into offset = 0 for ytile in xrange(0, padW, 4): for xtile in xrange(0, padH, 4): for ypixel in xrange(ytile, ytile + 4): for xpixel in xrange(xtile, xtile + 4): if xpixel >= w or ypixel >= h: rgbDAT = 0x7FFF else: pixel = tex.pixel(xpixel, ypixel) a = pixel >> 24 r = (pixel >> 16) & 0xFF g = (pixel >> 8) & 0xFF b = pixel & 0xFF if a < 245: #RGB4A3 alpha = a/32 red = r/16 green = g/16 blue = b/16 rgbDAT = (blue) | (green << 4) | (red << 8) | (alpha << 12) else: # RGB555 red = r/8 green = g/8 blue = b/8 rgbDAT = (blue) | (green << 5) | (red << 10) | (0x8000) # 0rrrrrgggggbbbbb sspack(destBuffer, offset, rgbDAT) offset += 2 return destBuffer.raw class KPMapExporter: class LayerExporter: def __init__(self, layer): self.layer = layer def buildSectors(self, sectors, indices): # we'll use the cache held by the layer: why reinvent the wheel? layer = self.layer layer.updateCache() cache = layer.cache # first off, get all the info and figure out the sector bounds layerX, layerY = layer.cacheBasePos layerWidth, layerHeight = layer.cacheSize sectorLeft = layerX / 16 sectorTop = layerY / 16 sectorRight = (layerX + layerWidth) / 16 sectorBottom = (layerY + layerHeight) / 16 rawSectors = [] for i in xrange(sectorBottom - sectorTop + 1): rawSectors.append([None for j in xrange(sectorRight - sectorLeft + 1)]) # copy every tile index over for srcY in xrange(layerHeight): srcRow = cache[srcY] worldY = srcY + layerY sectorY = worldY / 16 destY = worldY % 16 destRow = rawSectors[sectorY] for srcX in xrange(layerWidth): worldX = srcX + layerX sectorX = worldX / 16 destX = worldX % 16 tile = srcRow[srcX] if tile == -1: continue destSector = destRow[sectorX] if destSector is None: destSector = [[-1 for j in xrange(16)] for i in xrange(16)] destRow[sectorX] = destSector destSector[destY][destX] = tile # now add the created sectors to the data count = reduce(lambda x,y: x+len(y), rawSectors, 0) sectorMap = [-1 for i in xrange(count)] destIdx = 0 for srcRow in rawSectors: for sector in srcRow: if sector is not None: # see if it's a duplicate or not sectorKey = '|'.join(map(lambda x: ','.join(map(str, x)), sector)) try: sectorMap[destIdx] = indices[sectorKey] except KeyError: indices[sectorKey] = len(sectors) sectorMap[destIdx] = len(sectors) sectors.append(sector) destIdx += 1 self.sectorBounds = (sectorLeft, sectorTop, sectorRight, sectorBottom) self.sectorMap = sectorMap def __init__(self, mapObj): self.map = mapObj self.layers = map(KPMapExporter.LayerExporter, self.map.layers) def build(self): u32 = struct.Struct('>I') u16 = struct.Struct('>H') zero32 = u32.pack(0) requiredFixUps = [] stringsToAdd = set() textures = set() offsets = {None: 0xFFFFFFFF} # first off, build the sectors sectors = [] sectorIndices = {} for layer in self.layers: if isinstance(layer.layer, KPTileLayer): layer.buildSectors(sectors, sectorIndices) sectorData = self._packSectorData(sectors) # now that we've got that, we can pack the first part of the file data = bytearray(struct.pack('>IIII', len(self.layers), 16 + len(sectorData), 0, 0)) # list of layer pointers goes here.. or will, later data += sectorData for layer in self.layers: requiredFixUps.append((len(data), layer)) data += zero32 # now build the layers for eLayer in self.layers: layer = eLayer.layer offsets[eLayer] = len(data) offsets[layer] = len(data) if isinstance(layer, KPTileLayer): data += u32.pack(0) # tileset name stringsToAdd.add(layer.tileset) requiredFixUps.append((len(data), layer.tileset)) data += zero32 # sector info data += struct.pack('>IIII', *eLayer.sectorBounds) data += ''.join(map(u16.pack, eLayer.sectorMap)) pad = (4 - (len(data) & 3)) % 4 data += ('\0' * pad) elif isinstance(layer, KPDoodadLayer): data += u32.pack(1) # doodad list data += u32.pack(len(layer.objects)) for doodad in layer.objects: requiredFixUps.append((len(data), doodad)) data += zero32 # now pack them ... for doodad in layer.objects: offsets[doodad] = len(data) x, y = doodad.position w, h = doodad.size data += struct.pack('>fffffii', x, y, w, h, doodad.angle, 0, len(doodad.animations)) texture = doodad.source[1] textures.add(texture) requiredFixUps.append((len(data) - 8, texture)) for anim in doodad.animations: rLoop, rCurve, rFrames, rType, rStart, rEnd = anim loopid = self.ANIM_LOOPS.index(rLoop) curveid = self.ANIM_CURVES.index(rCurve) typeid = self.ANIM_TYPES.index(rType) data += struct.pack('>iififf', loopid, curveid, rFrames, typeid, rStart, rEnd) elif isinstance(layer, KPPathLayer): data += u32.pack(2) # lists current = len(data) nodeArray = current + 16 pathArray = nodeArray + (len(layer.nodes) * 4) data += struct.pack('>IIII', len(layer.nodes), nodeArray, len(layer.paths), pathArray) for node in layer.nodes: requiredFixUps.append((len(data), node)) data += zero32 for path in layer.paths: requiredFixUps.append((len(data), path)) data += zero32 # now do the actual structs for node in layer.nodes: offsets[node] = len(data) x, y = node.position current = len(data) data += struct.pack('>hhiiii', x, y, 0, 0, 0, 0) exits = node.exits + [None,None,None,None] # TODO requiredFixUps.append((current+4, exits[0])) requiredFixUps.append((current+8, exits[1])) requiredFixUps.append((current+12, exits[2])) requiredFixUps.append((current+16, exits[3])) if node.isStop(): if node.level: level1, level2 = node.level data += struct.pack('>ibbbb', 2, level1, level2, 0, 0) typeid = 2 elif node.mapChange: data += u32.pack(3) destMap = node.mapChange requiredFixUps.append((len(data), destMap)) stringsToAdd.add(destMap) data += struct.pack('>ibbbb', 0, node.mapID, node.foreignID, node.transition, 0) else: data += u32.pack(1) else: data += u32.pack(0) for path in layer.paths: offsets[path] = len(data) start = path._startNodeRef() end = path._endNodeRef() current = len(data) requiredFixUps.append((current, start)) requiredFixUps.append((current+4, end)) requiredFixUps.append((current+8, path.linkedLayer)) data += (zero32 * 3) data += struct.pack('>fi', path.movementSpeed, path.animation) # unlocking paths/nodes! unlocks = [] us = struct.Struct('>bbbbi') for node in self.map.pathLayer.nodes: if not node.level: continue checked = set() affected = [] self._findUnlocksForNode(node, checked, affected, True) level1, level2 = node.level for item, secret in affected: unlocks.append(us.pack(secret, level1, level2, 0, offsets[item])) struct.pack_into('>ii', data, 8, len(unlocks), len(data)) data += ''.join(unlocks) # now that we're almost done... pack the strings for string in stringsToAdd: offsets[string] = len(data) data += str(string) data += '\0' # textures texHeaderStartOffset = len(data) texHeaderEndOffset = texHeaderStartOffset + (len(textures) * 0x20) texDataStartOffset = (texHeaderEndOffset + 0x1F) & ~0x1F texPadding = texDataStartOffset - texHeaderEndOffset currentTexOffset = texDataStartOffset imageData = [] for tex in textures: offsets[tex] = len(data) data += self._buildGXTexObjRGB5A3(tex.width(), tex.height(), currentTexOffset) converted = RGB5A3Encode(tex) imageData.append(converted) currentTexOffset += len(converted) data += ('\0' * texPadding) for piece in imageData: data += piece # to finish up, correct every offset for offset, target in requiredFixUps: u32.pack_into(data, offset, offsets[target]) return data ANIM_LOOPS = ['Contiguous', 'Loop', 'Reversible Loop'] ANIM_CURVES = ['Linear', 'Sinusoidial', 'Cosinoidial'] ANIM_TYPES = ['X Position', 'Y Position', 'Angle', 'X Scale', 'Y Scale', 'Opacity'] def _findUnlocksForNode(self, node, checked, affected, isFirstBranch=False, secret=None): if node in checked: return checked.add(node) for path in node.exits: if path not in checked: checked.add(path) self._findUnlocksForPath(path, node, checked, affected, isFirstBranch, secret) def _findUnlocksForPath(self, path, sourceNode, checked, affected, isFirstBranch, secret=None): start, end = path._startNodeRef(), path._endNodeRef() if start == sourceNode: destNode = end if isFirstBranch and path.unlocks != 1: return else: destNode = start if isFirstBranch and path.unlocks != 2: return if secret is None: secret = path.secret affected.append((path, secret)) if not destNode.isStop(): self._findUnlocksForNode(destNode, checked, affected, False, secret) def _buildGXTexObjRGB5A3(self, width, height, imgOffset): # Format: RGB5A3 (5) # Wrap: CLAMP (0) return struct.pack('>IIIIIIIHH', 0x10, 0, (0x20000 | ((height - 1) << 10) | (width - 1)), imgOffset, # (imgptr >> 5) 0, 0, 0, (((width + 3) / 4) * ((height + 3) / 4)) & 0x1FFFF, 0x0202 ) def _packSectorData(self, sectors): rowStruct = struct.Struct('>16h') output = [] for sector in sectors: for row in sector: output.append(rowStruct.pack(*row)) return ''.join(output)