from common import * import array import sys import math from ctypes import create_string_buffer # Useful Stuff u32 = struct.Struct('>I') u16 = struct.Struct('>H') zero32 = u32.pack(0) 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, padH, 4): for xtile in xrange(0, padW, 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 class TileLayerExporter(LayerExporter): 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 - 1) / 16 sectorBottom = (layerY + layerHeight - 1) / 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 - sectorTop] for srcX in xrange(layerWidth): worldX = srcX + layerX sectorX = worldX / 16 destX = worldX % 16 tile = srcRow[srcX] if tile == -1: continue destSector = destRow[sectorX - sectorLeft] if destSector is None: destSector = [[-1 for j in xrange(16)] for i in xrange(16)] destRow[sectorX - sectorLeft] = destSector destSector[destY][destX] = tile # now add the created sectors to the data count = reduce(lambda x,y: x+len(y), rawSectors, 0) sectorMap = [0xFFFF 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.realBounds = (layerX, layerY, layerX+layerWidth-1, layerY+layerHeight-1) self.sectorMap = sectorMap class DoodadLayerExporter(LayerExporter): pass class PathLayerExporter(LayerExporter): pass def __init__(self, mapObj): self.map = mapObj self.tileAssociates = {} self.doodadAssociates = {} output = [] for layer in self.map.layers: if isinstance(layer, KPTileLayer) and len(layer.objects) > 0: output.append(KPMapExporter.TileLayerExporter(layer)) elif isinstance(layer, KPDoodadLayer) and len(layer.objects) > 0: output.append(KPMapExporter.DoodadLayerExporter(layer)) elif isinstance(layer, KPPathLayer): for iLayer in self.map.associateLayers: if len(iLayer.objects) > 0: tl = KPMapExporter.TileLayerExporter(iLayer) self.tileAssociates[iLayer.associate] = tl output.append(tl) if len(iLayer.doodads) > 0: dl = KPMapExporter.DoodadLayerExporter(iLayer) self.doodadAssociates[iLayer.associate] = dl output.append(dl) output.append(KPMapExporter.PathLayerExporter(layer)) self.layers = output def build(self): requiredFixUps = [] stringsToAdd = set() textures = set() tilesets = set() offsets = {None: 0xFFFFFFFF} # first off, build the sectors sectors = [] sectorIndices = {} for layer in self.layers: if isinstance(layer, self.TileLayerExporter): 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 # map all paths to unlock info unlockInfo = {} for node in self.map.pathLayer.nodes: if not node.level: continue checked = set() affected = [] self._findUnlocksForNode(node, checked, affected) level1, level2 = node.level for item, secret in affected: unlockInfo[item] = (level1, level2, secret) # now build the layers for eLayer in self.layers: layer = eLayer.layer offsets[eLayer] = len(data) offsets[layer] = len(data) if isinstance(eLayer, self.TileLayerExporter): data += u32.pack(0) # tileset name tileset = '/Maps/Texture/%s.bin' % layer.tileset tilesets.add(tileset) stringsToAdd.add(tileset) requiredFixUps.append((len(data), ('tileset', tileset))) data += zero32 # sector info data += struct.pack('>IIII', *eLayer.sectorBounds) data += struct.pack('>IIII', *eLayer.realBounds) data += ''.join(map(u16.pack, eLayer.sectorMap)) pad = (4 - (len(data) & 3)) % 4 data += ('\0' * pad) elif isinstance(eLayer, self.DoodadLayerExporter): data += u32.pack(1) # doodad list try: doodadList = layer.doodads except AttributeError: doodadList = layer.objects data += u32.pack(len(doodadList)) for doodad in doodadList: requiredFixUps.append((len(data), doodad)) data += zero32 # now pack them ... for doodad in doodadList: 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('>iiiiiiii', loopid, curveid, rFrames, typeid, rStart, rEnd, 0, 0) elif isinstance(eLayer, self.PathLayerExporter): 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('>hhiiiiii', x+12, y+12, 0, 0, 0, 0, 0, 0) # this varies if node.isStop(): # figure out the exits by direction leftExit, rightExit, upExit, downExit = None, None, None, None for exit in node.exits: start, end = exit._startNodeRef(), exit._endNodeRef() opposite = end if (start == node) else start oX, oY = opposite.position deltaX, deltaY = oX-x, oY-y angle = math.degrees(math.atan2(deltaX, deltaY)) % 360 print "Here: %d,%d Opposite %d,%d Delta: %d,%d Angle: %d" % (x,y,oX,oY,deltaX,deltaY,angle) # Left = 270, Right = 90, Up = 180, Down = 0 if angle >= 225 and angle <= 315: leftExit = exit elif angle >= 45 and angle <= 135: rightExit = exit elif angle > 135 and angle < 225: upExit = exit elif angle > 315 or angle < 45: downExit = exit exits = [leftExit, rightExit, upExit, downExit] else: # not a stop, so just dump them in exits = node.exits + [None,None,None,None] 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 in self.tileAssociates: requiredFixUps.append((current+20, self.tileAssociates[node])) if node in self.doodadAssociates: requiredFixUps.append((current+24, self.doodadAssociates[node])) if node.isStop(): if node.level: level1, level2 = node.level data += struct.pack('>ibbbb', 2, level1, level2, 0, 0) 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)) if path in self.tileAssociates: requiredFixUps.append((current+8, self.tileAssociates[path])) if path in self.doodadAssociates: requiredFixUps.append((current+12, self.doodadAssociates[path])) data += (zero32 * 4) try: unlockL1, unlockL2, isSecret = unlockInfo[path] unlockType = (2 if isSecret else 1) except KeyError: unlockL1, unlockL2, unlockType = 0, 0, 0 data += struct.pack('>bbbbfi', unlockType, unlockL1, unlockL2, 1, path.movementSpeed, path.animation) # now that we're almost done... pack the strings for string in stringsToAdd: offsets[string] = len(data) data += str(string) data += '\0' # textures texPadding = ((len(data) + 0x1F) & ~0x1F) - len(data) data += ('\0' * texPadding) texHeaderStartOffset = len(data) texDataStartOffset = texHeaderStartOffset + ((len(textures) + len(tilesets)) * 0x20) currentTexOffset = texDataStartOffset imageData = [] struct.pack_into('>ii', data, 8, len(tilesets), len(data)) for setname in tilesets: offsets[('tileset', setname)] = len(data) data += self._buildGXTexObjRGB5A3(1024, 512, offsets[setname]) 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) 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=True, 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', 0x90, 0, (0x500000 | ((height - 1) << 10) | (width - 1)), 0x10000000 + imgOffset, # (imgptr >> 5) 0, 0, 0, (((width + 3) / 4) * ((height + 3) / 4)) & 0x7FFF, 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)