From 6feabc0249807c7bbbbe486dd679832572c0fa68 Mon Sep 17 00:00:00 2001 From: ZomgPonies Date: Sat, 21 Jun 2014 05:17:42 -0400 Subject: [PATCH] For some reason this keeps disappearing --- tools/BYONDTools/byond/__init__.py | 6 +- tools/BYONDTools/byond/basetypes.py | 172 ++-- tools/BYONDTools/byond/map/__init__.py | 791 ++++++++++++++++++ tools/BYONDTools/byond/map/format/__init__.py | 13 + tools/BYONDTools/byond/map/format/base.py | 33 + tools/BYONDTools/byond/map/format/dmm.py | 490 +++++++++++ tools/BYONDTools/byond/objtree.py | 24 +- tools/BYONDTools/byond/utils.py | 4 + 8 files changed, 1461 insertions(+), 72 deletions(-) create mode 100644 tools/BYONDTools/byond/map/__init__.py create mode 100644 tools/BYONDTools/byond/map/format/__init__.py create mode 100644 tools/BYONDTools/byond/map/format/base.py create mode 100644 tools/BYONDTools/byond/map/format/dmm.py diff --git a/tools/BYONDTools/byond/__init__.py b/tools/BYONDTools/byond/__init__.py index b46c624fe03..e73138bee18 100644 --- a/tools/BYONDTools/byond/__init__.py +++ b/tools/BYONDTools/byond/__init__.py @@ -5,8 +5,8 @@ Created on Sep 21, 2013 ''' import os -from .map import Map, Tile, MapRenderFlags -from .objtree import ObjectTree +from byond.map import Map, Tile, MapRenderFlags +from byond.objtree import ObjectTree def GetFilesFromDME(dmefile='baystation12.dme', ext='.dm'): filesInDME=[] @@ -30,7 +30,7 @@ def GetFilesFromDME(dmefile='baystation12.dme', ext='.dm'): if c == '"': inString = not inString if not inString: - filepath = os.path.join(rootdir, filename) + filepath = os.path.join(rootdir, filename.replace('\\',os.sep)) if filepath.endswith(ext): filesInDME += [filepath] filename = '' diff --git a/tools/BYONDTools/byond/basetypes.py b/tools/BYONDTools/byond/basetypes.py index 658a9c4840e..60f369154bc 100644 --- a/tools/BYONDTools/byond/basetypes.py +++ b/tools/BYONDTools/byond/basetypes.py @@ -10,7 +10,48 @@ OBJ_LAYER = 3 MOB_LAYER = 4 FLY_LAYER = 5 -import re +# @formatter:off +COLORS = { + 'aqua': '#00FFFF', + 'black': '#000000', + 'blue': '#0000FF', + 'brown': '#A52A2A', + 'cyan': '#00FFFF', + 'fuchsia': '#FF00FF', + 'gold': '#FFD700', + 'gray': '#808080', + 'green': '#008000', + 'grey': '#808080', + 'lime': '#00FF00', + 'magenta': '#FF00FF', + 'maroon': '#800000', + 'navy': '#000080', + 'olive': '#808000', + 'purple': '#800080', + 'red': '#FF0000', + 'silver': '#C0C0C0', + 'teal': '#008080', + 'white': '#FFFFFF', + 'yellow': '#FFFF00' +} +# @formatter:on + +_COLOR_LOOKUP = {} + +def BYOND2RGBA(colorstring, alpha=255): + colorstring = colorstring.strip() + if colorstring[0] == '#': + colorstring = colorstring[1:] + r, g, b = colorstring[:2], colorstring[2:4], colorstring[4:] + r, g, b = [int(n, 16) for n in (r, g, b)] + return (r, g, b, alpha) + else: + return _COLOR_LOOKUP[colorstring] + +for name, color in COLORS.items(): + _COLOR_LOOKUP[name] = BYOND2RGBA(color) + +import re, hashlib from .utils import eval_expr REGEX_TABS = re.compile('^(?P\t*)') class BYONDValue: @@ -18,28 +59,28 @@ class BYONDValue: Handles numbers and unhandled types like lists. """ def __init__(self, string, filename='', line=0, typepath='/', **kwargs): - #: The actual value. + # : The actual value. self.value = string - #: Filename this was found in + # : Filename this was found in self.filename = filename - #: Line of the originating file. + # : Line of the originating file. self.line = line - #: Typepath of the value. + # : Typepath of the value. self.type = typepath - #: Has this value been inherited? + # : Has this value been inherited? self.inherited = kwargs.get('inherited', False) - #: Is this a declaration? (/var) + # : Is this a declaration? (/var) self.declaration = kwargs.get('declaration', False) - #: Anything special? (global, const, etc.) + # : Anything special? (global, const, etc.) self.special = kwargs.get('special', None) - #: If a list, what's the size? + # : If a list, what's the size? self.size = kwargs.get('size', None) def copy(self): @@ -47,6 +88,8 @@ class BYONDValue: return BYONDValue(self.value, self.filename, self.line, self.type, declaration=self.declaration, inherited=self.inherited, special=self.special) def __str__(self): + if self.value is None: + return 'null' return '{0}'.format(self.value) def __repr__(self): @@ -104,16 +147,16 @@ class BYONDString(BYONDValue): class PropertyFlags: '''Collection of flags that affect :func:`Atom.setProperty` behavior.''' - #: Property being set should be saved to the map + # : Property being set should be saved to the map MAP_SPECIFIED = 1 - #: Property being set should be handled as a string + # : Property being set should be handled as a string STRING = 2 - #: Property being set should be handled as a file reference + # : Property being set should be handled as a file reference FILEREF = 4 - #: Property being set should be handled as a value + # : Property being set should be handled as a value VALUE = 8 class Atom: @@ -128,61 +171,76 @@ class Atom: The line in the aforementioned file. ''' - #: Prints all inherited properties, not just the ones that are mapSpecified. + # : Prints all inherited properties, not just the ones that are mapSpecified. FLAG_INHERITED_PROPERTIES = 1 - #: writeMap2 prints old_ids instead of the actual IID. + # : writeMap2 prints old_ids instead of the actual IID. FLAG_USE_OLD_ID = 2 def __init__(self, path, filename='', line=0, **kwargs): global TURF_LAYER, AREA_LAYER, OBJ_LAYER, MOB_LAYER - #: Absolute path of this atom + # : Absolute path of this atom self.path = path - #: Vars of this atom, including inherited vars. + # : Vars of this atom, including inherited vars. self.properties = {} - #: List of var names that were specified by the map, if atom was loaded from a :class:`byond.map.Map`. + # : List of var names that were specified by the map, if atom was loaded from a :class:`byond.map.Map`. self.mapSpecified = [] - #: Child atoms and procs. + # : Child atoms and procs. self.children = {} - #: The parent of this atom. + # : The parent of this atom. self.parent = None - #: The file this atom originated from. + # : The file this atom originated from. self.filename = filename - #: Line from the originating file. + # : Line from the originating file. self.line = line - #: Instance ID (maps only). Used internally, do NOT change. + # : Instance ID (maps only). Used internally, do NOT change. self.id = None - #: Instance ID that was read from the map. + # : Instance ID that was read from the map. self.old_id = None - #: Used internally. - self.ob_inherited=False + # : Used internally. + self.ob_inherited = False - #: Loaded from map, but missing in the code. (Maps only) - self.missing=kwargs.get('missing',False) + # : Loaded from map, but missing in the code. (Maps only) + self.missing = kwargs.get('missing', False) - #if not self.missing and path == '/area/engine/engineering': + # if not self.missing and path == '/area/engine/engineering': # raise Exception('God damnit') + + self._hash = None + + def UpdateHash(self): + if self._hash is None: + self._hash = hashlib.md5(str(self)).hexdigest() + + def InvalidateHash(self): + self._hash = None + + def GetHash(self): + self.UpdateHash() + return self._hash + def copy(self): ''' Make a copy of this atom, without dangling references. :returns byond.basetypes.Atom ''' - new_node = Atom(self.path,self.filename,self.line,missing=self.missing) + new_node = Atom(self.path, self.filename, self.line, missing=self.missing) new_node.properties = self.properties.copy() new_node.mapSpecified = self.mapSpecified new_node.id = self.id new_node.old_id = self.old_id + new_node.UpdateHash() # new_node.parent = self.parent return new_node @@ -250,10 +308,12 @@ class Atom: self.properties[index] = BYONDFileRef(value) else: self.properties[index] = BYONDValue(value) + + self.UpdateHash() def InheritProperties(self): if self.ob_inherited: return - #debugInheritance=self.path in ('/area','/obj','/mob','/atom/movable','/atom') + # debugInheritance=self.path in ('/area','/obj','/mob','/atom/movable','/atom') if self.parent: if not self.parent.ob_inherited: self.parent.InheritProperties() @@ -262,26 +322,26 @@ class Atom: if key not in self.properties: self.properties[key] = value self.properties[key].inherited = True - #if debugInheritance:print(' {0}[{2}] -> {1}'.format(self.parent.path,self.path,key)) - #assert 'name' in self.properties - self.ob_inherited=True + # if debugInheritance:print(' {0}[{2}] -> {1}'.format(self.parent.path,self.path,key)) + # assert 'name' in self.properties + self.ob_inherited = True for k in self.children.iterkeys(): self.children[k].InheritProperties() - + def __ne__(self, atom): return not self.__eq__(atom) def __eq__(self, atom): if atom == None: return False - if self.mapSpecified != atom.mapSpecified: - return False + # if self.mapSpecified != atom.mapSpecified: + # return False if self.path != atom.path: return False return self.properties == atom.properties - def handle_math(self,expr): - if isinstance(expr,str): + def handle_math(self, expr): + if isinstance(expr, str): return eval_expr(expr) return expr @@ -291,12 +351,12 @@ class Atom: myLayer = 0 otherLayer = 0 try: - myLayer = self.handle_math(self.getProperty('layer',myLayer)) + myLayer = self.handle_math(self.getProperty('layer', myLayer)) except ValueError: print('Failed to parse {0} as float.'.format(self.properties['layer'].value)) pass try: - otherLayer = self.handle_math(other.getProperty('layer',otherLayer)) + otherLayer = self.handle_math(other.getProperty('layer', otherLayer)) except ValueError: print('Failed to parse {0} as float.'.format(other.properties['layer'].value)) pass @@ -308,34 +368,24 @@ class Atom: myLayer = 0 otherLayer = 0 try: - myLayer = self.handle_math(self.getProperty('layer',myLayer)) + myLayer = self.handle_math(self.getProperty('layer', myLayer)) except ValueError: print('Failed to parse {0} as float.'.format(self.properties['layer'].value)) pass try: - otherLayer = self.handle_math(other.getProperty('layer',otherLayer)) + otherLayer = self.handle_math(other.getProperty('layer', otherLayer)) except ValueError: print('Failed to parse {0} as float.'.format(other.properties['layer'].value)) pass return myLayer < otherLayer - - def MapSerialize(self, flags=0): - atomContents = [] - # print(repr(self.mapSpecified)) - if (flags & Atom.FLAG_INHERITED_PROPERTIES): - for key, val in self.properties.items(): - atomContents += ['{0} = {1}'.format(key, val)] - else: - for i in range(len(self.mapSpecified)): - key = self.mapSpecified[i] - if key in self.properties: - val = self.properties[key] - atomContents += ['{0} = {1}'.format(key, val)] - if len(atomContents) > 0: - return self.path + '{' + '; '.join(atomContents) + '}' - else: - return self.path + def __str__(self): + + atomContents = [] + for key, val in self.properties.items(): + atomContents += ['{0}={1}'.format(key, val)] + return '{}{{{}}}'.format(self.path, ';'.join(atomContents)) + def dumpPropInfo(self, name): o = '{0}: '.format(name) if name not in self.properties: @@ -391,7 +441,7 @@ class Proc(Atom): self.definition = False self.origpath = '' - def figureOutName(self,path): + def figureOutName(self, path): name = path.split('(')[0] return name.split('/')[-1] diff --git a/tools/BYONDTools/byond/map/__init__.py b/tools/BYONDTools/byond/map/__init__.py new file mode 100644 index 00000000000..4907ceb4da3 --- /dev/null +++ b/tools/BYONDTools/byond/map/__init__.py @@ -0,0 +1,791 @@ +""" +Map Interface Module + +Copyright 2013 Rob "N3X15" Nelson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" +import os, itertools, sys, numpy, logging, hashlib +from byond.map.format import GetMapFormat, Load as LoadMapFormats +from byond.DMI import DMI +from byond.directions import SOUTH, IMAGE_INDICES +from byond.basetypes import Atom, BYONDString, BYONDValue, BYONDFileRef, BYOND2RGBA +# from byond.objtree import ObjectTree +from PIL import Image, ImageChops + +# Cache +_icons = {} +_dmis = {} + +LoadMapFormats() + +# From StackOverflow +def trim(im): + bg = Image.new(im.mode, im.size, im.getpixel((0, 0))) + diff = ImageChops.difference(im, bg) + diff = ImageChops.add(diff, diff, 2.0, -100) + bbox = diff.getbbox() + if bbox: + return im.crop(bbox) + +# Bytes +def tint_image(image, tint_color): + return ImageChops.multiply(image, Image.new('RGBA', image.size, tint_color)) + +class TileIterator: + def __init__(self, _map): + self.map = _map + self.x = -1 + self.y = 0 + self.z = 0 + + self.max_z = len(self.map.zLevels) + + def __iter__(self): + return self + + def __next__(self): + return self.next() + + def next(self): + self.x += 1 + + zLev = self.map.zLevels[self.z] + + if self.x >= zLev.width: + self.y += 1 + self.x = 0 + + if self.y >= zLev.height: + self.z += 1 + self.y = 0 + + if self.z >= self.max_z: + raise StopIteration + + t = self.map.GetTileAt(self.x, self.y, self.z) + #print('{} = {}'.format((self.x,self.y,self.z),str(t))) + return t + +class Tile(object): + def __init__(self, _map): + self.origID = '' + self.ID = 0 + self.instances = [] + self.frame = None + self.unselected_frame = None + self.areaSelected = True + self.log = logging.getLogger(__name__ + '.Tile') + self.map = _map + self._hash = None + + def UpdateHash(self): + if self._hash is None: + self._hash = hashlib.md5(str(self)).hexdigest() + + def InvalidateHash(self): + self._hash = None + + def GetHash(self): + self.UpdateHash() + return self._hash + + def RemoveAtom(self, atom, hash=True): + ''' + :param Atom atom: + Atom to remove. Raises ValueError if not found. + ''' + self.instances.remove(atom) + self.InvalidateHash() + if hash: self.UpdateHash() + + def AppendAtom(self, atom, hash=True): + ''' + :param Atom atom: + Atom to add. + ''' + self.instances.append(atom) + self.InvalidateHash() + if hash: self.UpdateHash() + + def CountAtom(self, atom): + ''' + :param Atom atom: + Atom to count. + :return int: Count of atoms + ''' + return self.instances.count(atom) + + def copy(self, origID=False): + tile = self.map.CreateTile() + tile.instances = self.instances + if origID: + tile.origID = self.origID + + tile.UpdateHash() + return tile + + def GetAtoms(self): + atoms = [] + for atom in self.instances: + if atom is None: continue + atoms += [atom] + return atoms + + def SortAtoms(self): + return sorted(self.GetAtoms(), reverse=True) + + def GetAtom(self, idx): + return self.map.getInstance(self.instances[idx]) + + def GetInstances(self): + return self.instances + + def __str__(self): + return self._serialize() + + def __ne__(self, tile): + return not self.__eq__(tile) + + def __eq__(self, other): + return other and ((other._hash and self._hash and self._hash == other._hash) or (len(self.instances) == len(other.instances) and self.instances == other.instances)) + # else: + # return all(self.instances[i] == other.instances[i] for i in xrange(len(self.instances))) + + def _serialize(self): + return ','.join([str(i) for i in self.GetInstances()]) + + def RenderToMapTile(self, passnum, basedir, renderflags, **kwargs): + img = Image.new('RGBA', (96, 96)) + self.offset = (32, 32) + foundAPixelOffset = False + render_types = kwargs.get('render_types', ()) + skip_alpha = kwargs.get('skip_alpha', False) + # for atom in sorted(self.GetAtoms(), reverse=True): + for atom in self.SortAtoms(): + if len(render_types) > 0: + found = False + for path in render_types: + if atom.path.startswith(path): + found = True + if not found: + continue + + aid = atom.id + # Ignore /areas. They look like ass. + if atom.path.startswith('/area'): + if not (renderflags & MapRenderFlags.RENDER_AREAS): + continue + + # We're going to turn space black for smaller images. + if atom.path == '/turf/space': + if not (renderflags & MapRenderFlags.RENDER_STARS): + continue + + if 'icon' not in atom.properties: + logging.critical('UNKNOWN ICON IN {0} (atom #{1})'.format(self.origID, aid)) + logging.info(atom.MapSerialize()) + logging.info(atom.MapSerialize(Atom.FLAG_INHERITED_PROPERTIES)) + continue + + dmi_file = atom.properties['icon'].value + + if 'icon_state' not in atom.properties: + # Grab default icon_state ('') if we can't find the one defined. + atom.properties['icon_state'] = BYONDString("") + + state = atom.properties['icon_state'].value + + direction = SOUTH + if 'dir' in atom.properties: + try: + direction = int(atom.properties['dir'].value) + except ValueError: + logging.critical('FAILED TO READ dir = ' + repr(atom.properties['dir'].value)) + continue + + icon_key = '{0}|{1}|{2}'.format(dmi_file, state, direction) + frame = None + pixel_x = 0 + pixel_y = 0 + if icon_key in _icons: + frame, pixel_x, pixel_y = _icons[icon_key] + else: + dmi_path = os.path.join(basedir, dmi_file) + dmi = None + if dmi_path in _dmis: + dmi = _dmis[dmi_path] + else: + try: + dmi = DMI(dmi_path) + dmi.loadAll() + _dmis[dmi_path] = dmi + except Exception as e: + print(str(e)) + for prop in ['icon', 'icon_state', 'dir']: + print('\t{0}'.format(atom.dumpPropInfo(prop))) + pass + if dmi.img is None: + logging.warning('Unable to open {0}!'.format(dmi_path)) + continue + + if dmi.img.mode not in ('RGBA', 'P'): + logging.warn('{} is mode {}!'.format(dmi_file, dmi.img.mode)) + + if direction not in IMAGE_INDICES: + logging.warn('Unrecognized direction {} on atom {} in tile {}!'.format(direction, atom.MapSerialize(), self.origID)) + direction = SOUTH # DreamMaker property editor shows dir = 2. WTF? + + frame = dmi.getFrame(state, direction, 0) + if frame == None: + # Get the error/default state. + frame = dmi.getFrame("", direction, 0) + + if frame == None: + continue + + if frame.mode != 'RGBA': + frame = frame.convert("RGBA") + + pixel_x = 0 + if 'pixel_x' in atom.properties: + pixel_x = int(atom.properties['pixel_x'].value) + + pixel_y = 0 + if 'pixel_y' in atom.properties: + pixel_y = int(atom.properties['pixel_y'].value) + + _icons[icon_key] = (frame, pixel_x, pixel_y) + + # Handle BYOND alpha and coloring + c_frame = frame + alpha = int(atom.getProperty('alpha', 255)) + if skip_alpha: + alpha = 255 + color = atom.getProperty('color', '#FFFFFF') + if alpha != 255 or color != '#FFFFFF': + c_frame = tint_image(frame, BYOND2RGBA(color, alpha)) + img.paste(c_frame, (32 + pixel_x, 32 - pixel_y), c_frame) # Add to the top of the stack. + if pixel_x != 0 or pixel_y != 0: + if passnum == 0: return # Wait for next pass + foundAPixelOffset = True + + if passnum == 1 and not foundAPixelOffset: + return None + if not self.areaSelected: + # Fade out unselected tiles. + bands = list(img.split()) + # Excluding alpha band + for i in range(3): + bands[i] = bands[i].point(lambda x: x * 0.4) + img = Image.merge(img.mode, bands) + + return img + +vTile = numpy.vectorize(Tile) + +class MapLayer: + def __init__(self, _map, height=255, width=255): + self.map = _map + self.min = (1, 1) + self.max = (height, width) + self.height = height + self.width = width + self.tiles = numpy.empty((height, width), object) + + self.tiles[:, :] = vTile(self) + + def Resize(self,height,width): + self.height=height + self.width=width + self.tiles.resize(height,width) + +class MapRenderFlags: + RENDER_STARS = 1 + RENDER_AREAS = 2 + +class Map: + def __init__(self, tree=None, **kwargs): + self.zLevels = [] + self.DMIs = {} + self.tree = tree + self.generatedTexAtlas = False + self.selectedAreas = () + self.whitelistTypes = None + self.forgiving_atom_lookups = kwargs.get('forgiving_atom_lookups', False) + + self.log = logging.getLogger(__name__ + '.Map') + + def CreateZLevel(self, height, width, z= -1): + zLevel = MapLayer(self, height, width) + if z >= 0: + self.zLevels[z] = zLevel + else: + self.zLevels.append(zLevel) + return zLevel + + def Tiles(self): + return TileIterator(self) + + def Load(self, filename, **kwargs): + _, ext = os.path.splitext(filename) + fmt=kwargs.get('format','dmm2' if ext=='dmm2' else 'dmm') + reader = GetMapFormat(self, fmt) + reader.Load(filename, **kwargs) + + def Save(self, filename, **kwargs): + _, ext = os.path.splitext(filename) + fmt=kwargs.get('format','dmm2' if ext=='dmm2' else 'dmm') + reader = GetMapFormat(self, kwargs.get('format',fmt)) + reader.Save(filename, **kwargs) + + def writeMap2(self, filename, flags=0): + self.filename = filename + tileFlags = 0 + atomFlags = 0 + if flags & Map.WRITE_OLD_IDS: + tileFlags |= Tile.FLAG_USE_OLD_ID + atomFlags |= Atom.FLAG_USE_OLD_ID + padding = len(self.tileTypes[-1].ID2String()) + with open(filename, 'w') as f: + f.write('// Atom Instances\n') + for atom in self.instances: + f.write('{0} = {1}\n'.format(atom.id, atom.MapSerialize(atomFlags))) + f.write('// Tiles\n') + for tile in self.tileTypes: + f.write('{0}\n'.format(tile.MapSerialize2(tileFlags, padding))) + f.write('// Layout\n') + for z in self.zLevels.keys(): + f.write('\n(1,1,{0}) = {{"\n'.format(z)) + zlevel = self.zLevels[z] + for y in xrange(zlevel.height): + for x in xrange(zlevel.width): + tile = zlevel.GetTileAt(x, y) + if flags & Map.WRITE_OLD_IDS: + f.write(tile.origID) + else: + f.write(tile.ID2String(padding)) + f.write("\n") + f.write('"}\n') + + def GetTileAt(self, x, y, z): + ''' + :param int x: + :param int y: + :param int z: + :rtype Tile: + ''' + if z < len(self.zLevels): + return self.zLevels[z].tiles[x, y] + + def CopyTileAt(self, x, y, z): + ''' + :param int x: + :param int y: + :param int z: + :rtype Tile: + ''' + if z < len(self.zLevels): + return self.zLevels[z].tiles[x, y].copy() + + def SetTileAt(self, x, y, z, tile): + ''' + :param int x: + :param int y: + :param int z: + ''' + if z < len(self.zLevels): + self.zLevels[z].tiles[x, y] = tile + + def CreateTile(self): + ''' + :rtype Tile: + ''' + return Tile(self) + + def generateTexAtlas(self, basedir, renderflags=0): + if self.generatedTexAtlas: + return + print('--- Generating texture atlas...') + self._icons = {} + self._dmis = {} + self.generatedTexAtlas = True + for tid in xrange(len(self.tileTypes)): + tile = self.tileTypes[tid] + img = Image.new('RGBA', (96, 96)) + tile.offset = (32, 32) + tile.areaSelected = True + tile.render_deferred = False + for atom in sorted(tile.GetAtoms(), reverse=True): + + aid = atom.id + # Ignore /areas. They look like ass. + if atom.path.startswith('/area'): + if not (renderflags & MapRenderFlags.RENDER_AREAS): + continue + + # We're going to turn space black for smaller images. + if atom.path == '/turf/space': + if not (renderflags & MapRenderFlags.RENDER_STARS): + continue + + if 'icon' not in atom.properties: + print('CRITICAL: UNKNOWN ICON IN {0} (atom #{1})'.format(tile.origID, aid)) + print(atom.MapSerialize()) + print(atom.MapSerialize(Atom.FLAG_INHERITED_PROPERTIES)) + continue + + dmi_file = atom.properties['icon'].value + + if 'icon_state' not in atom.properties: + # Grab default icon_state ('') if we can't find the one defined. + atom.properties['icon_state'] = BYONDString("") + + state = atom.properties['icon_state'].value + + direction = SOUTH + if 'dir' in atom.properties: + try: + direction = int(atom.properties['dir'].value) + except ValueError: + print('FAILED TO READ dir = ' + repr(atom.properties['dir'].value)) + continue + + icon_key = '{0}:{1}[{2}]'.format(dmi_file, state, direction) + frame = None + pixel_x = 0 + pixel_y = 0 + if icon_key in self._icons: + frame, pixel_x, pixel_y = self._icons[icon_key] + else: + dmi_path = os.path.join(basedir, dmi_file) + dmi = None + if dmi_path in self._dmis: + dmi = self._dmis[dmi_path] + else: + try: + dmi = self.loadDMI(dmi_path) + self._dmis[dmi_path] = dmi + except Exception as e: + print(str(e)) + for prop in ['icon', 'icon_state', 'dir']: + print('\t{0}'.format(atom.dumpPropInfo(prop))) + pass + + if dmi.img is None: + self.log.warn('Unable to open {0}!'.format(dmi_path)) + continue + + if dmi.img.mode not in ('RGBA', 'P'): + self.log.warn('{} is mode {}!'.format(dmi_file, dmi.img.mode)) + + if direction not in IMAGE_INDICES: + self.log.warn('Unrecognized direction {} on atom {} in tile {}!'.format(direction, atom.MapSerialize(), tile.origID)) + direction = SOUTH # DreamMaker property editor shows dir = 2. WTF? + + frame = dmi.getFrame(state, direction, 0) + if frame == None: + # Get the error/default state. + frame = dmi.getFrame("", direction, 0) + + if frame == None: + continue + + if frame.mode != 'RGBA': + frame = frame.convert("RGBA") + + pixel_x = 0 + if 'pixel_x' in atom.properties: + pixel_x = int(atom.properties['pixel_x'].value) + + pixel_y = 0 + if 'pixel_y' in atom.properties: + pixel_y = int(atom.properties['pixel_y'].value) + + self._icons[icon_key] = (frame, pixel_x, pixel_y) + img.paste(frame, (32 + pixel_x, 32 - pixel_y), frame) # Add to the top of the stack. + if pixel_x != 0 or pixel_y != 0: + tile.render_deferred = True + tile.frame = img + + # Fade out unselected tiles. + bands = list(img.split()) + # Excluding alpha band + for i in range(3): + bands[i] = bands[i].point(lambda x: x * 0.4) + tile.unselected_frame = Image.merge(img.mode, bands) + + self.tileTypes[tid] = tile + + def renderAtom(self, atom, basedir, skip_alpha=False): + if 'icon' not in atom.properties: + logging.critical('UNKNOWN ICON IN ATOM #{0} ({1})'.format(atom.id, atom.path)) + logging.info(atom.MapSerialize()) + logging.info(atom.MapSerialize(Atom.FLAG_INHERITED_PROPERTIES)) + return None + # else: + # logging.info('Icon found for #{}.'.format(atom.id)) + + dmi_file = atom.properties['icon'].value + + if dmi_file is None: + return None + + # Grab default icon_state ('') if we can't find the one defined. + state = atom.getProperty('icon_state', '') + + direction = SOUTH + if 'dir' in atom.properties: + try: + direction = int(atom.properties['dir'].value) + except ValueError: + logging.critical('FAILED TO READ dir = ' + repr(atom.properties['dir'].value)) + return None + + icon_key = '{0}|{1}|{2}'.format(dmi_file, state, direction) + frame = None + pixel_x = 0 + pixel_y = 0 + if icon_key in _icons: + frame, pixel_x, pixel_y = _icons[icon_key] + else: + dmi_path = os.path.join(basedir, dmi_file) + dmi = None + if dmi_path in _dmis: + dmi = _dmis[dmi_path] + else: + try: + dmi = DMI(dmi_path) + dmi.loadAll() + _dmis[dmi_path] = dmi + except Exception as e: + print(str(e)) + for prop in ['icon', 'icon_state', 'dir']: + print('\t{0}'.format(atom.dumpPropInfo(prop))) + pass + if dmi.img is None: + logging.warning('Unable to open {0}!'.format(dmi_path)) + return None + + if dmi.img.mode not in ('RGBA', 'P'): + logging.warn('{} is mode {}!'.format(dmi_file, dmi.img.mode)) + + if direction not in IMAGE_INDICES: + logging.warn('Unrecognized direction {} on atom {}!'.format(direction, atom.MapSerialize())) + direction = SOUTH # DreamMaker property editor shows dir = 2. WTF? + + frame = dmi.getFrame(state, direction, 0) + if frame == None: + # Get the error/default state. + frame = dmi.getFrame("", direction, 0) + + if frame == None: + return None + + if frame.mode != 'RGBA': + frame = frame.convert("RGBA") + + pixel_x = 0 + if 'pixel_x' in atom.properties: + pixel_x = int(atom.properties['pixel_x'].value) + + pixel_y = 0 + if 'pixel_y' in atom.properties: + pixel_y = int(atom.properties['pixel_y'].value) + + _icons[icon_key] = (frame, pixel_x, pixel_y) + + # Handle BYOND alpha and coloring + c_frame = frame + alpha = int(atom.getProperty('alpha', 255)) + if skip_alpha: + alpha = 255 + color = atom.getProperty('color', '#FFFFFF') + if alpha != 255 or color != '#FFFFFF': + c_frame = tint_image(frame, BYOND2RGBA(color, alpha)) + return c_frame + + def generateImage(self, filename_tpl, basedir='.', renderflags=0, z=None, **kwargs): + ''' + Instead of generating on a tile-by-tile basis, this creates a large canvas and places + each atom on it after sorting layers. This resolves the pixel_(x,y) problem. + ''' + if z is None: + for z in self.zLevels.keys(): + self.generateImage(filename_tpl, basedir, renderflags, z, **kwargs) + return + self.selectedAreas = () + skip_alpha = False + render_types = () + if 'area' in kwargs: + self.selectedAreas = kwargs['area'] + if 'render_types' in kwargs: + render_types = kwargs['render_types'] + if 'skip_alpha' in kwargs: + skip_alpha = kwargs['skip_alpha'] + + print('Checking z-level {0}...'.format(z)) + instancePositions = {} + for y in range(self.zLevels[z].height): + for x in range(self.zLevels[z].width): + t = self.zLevels[z].GetTileAt(x, y) + # print('*** {},{}'.format(x,y)) + if t is None: + continue + if len(self.selectedAreas) > 0: + renderThis = True + for atom in t.GetAtoms(): + if atom.path.startswith('/area'): + if atom.path not in self.selectedAreas: + renderThis = False + if not renderThis: continue + for atom in t.GetAtoms(): + if atom is None: continue + iid = atom.id + if atom.path.startswith('/area'): + if atom.path not in self.selectedAreas: + continue + + # Check for render restrictions + if len(render_types) > 0: + found = False + for path in render_types: + if atom.path.startswith(path): + found = True + if not found: + continue + + # Ignore /areas. They look like ass. + if atom.path.startswith('/area'): + if not (renderflags & MapRenderFlags.RENDER_AREAS): + continue + + # We're going to turn space black for smaller images. + if atom.path == '/turf/space': + if not (renderflags & MapRenderFlags.RENDER_STARS): + continue + + if iid not in instancePositions: + instancePositions[iid] = [] + + # pixel offsets + ''' + pixel_x = int(atom.getProperty('pixel_x', 0)) + pixel_y = int(atom.getProperty('pixel_y', 0)) + t_o_x = int(round(pixel_x / 32)) + t_o_y = int(round(pixel_y / 32)) + pos = (x + t_o_x, y + t_o_y) + ''' + pos = (x, y) + + instancePositions[iid].append(pos) + + if len(instancePositions) == 0: + return + + levelAtoms = [] + for iid in instancePositions: + levelAtoms += [self.getInstance(iid)] + + pic = Image.new('RGBA', ((self.zLevels[z].width + 2) * 32, (self.zLevels[z].height + 2) * 32), "black") + + # Bounding box, used for cropping. + bbox = [99999, 99999, 0, 0] + + # Replace {z} with current z-level. + filename = filename_tpl.replace('{z}', str(z)) + + pastes = 0 + for atom in sorted(levelAtoms, reverse=True): + if atom.id not in instancePositions: + continue + icon = self.renderAtom(atom, basedir, skip_alpha) + if icon is None: + continue + for x, y in instancePositions[atom.id]: + new_bb = self.getBBoxForAtom(x, y, atom, icon) + # print('{0},{1} = {2}'.format(x, y, new_bb)) + # Adjust cropping bounds + if new_bb[0] < bbox[0]: + bbox[0] = new_bb[0] + if new_bb[1] < bbox[1]: + bbox[1] = new_bb[1] + if new_bb[2] > bbox[2]: + bbox[2] = new_bb[2] + if new_bb[3] > bbox[3]: + bbox[3] = new_bb[3] + pic.paste(icon, new_bb, icon) + pastes += 1 + + if len(self.selectedAreas) == 0: + # Autocrop (only works if NOT rendering stars or areas) + pic = trim(pic) + else: + # if nSelAreas == 0: + # continue + pic = pic.crop(bbox) + + if pic is not None: + # Saev + filedir = os.path.dirname(filename) + if not os.path.isdir(filedir): + os.makedirs(filedir) + print(' -> {} ({}x{}) - {} objects'.format(filename, pic.size[0], pic.size[1], pastes)) + pic.save(filename, 'PNG') + + def getBBoxForAtom(self, x, y, atom, icon): + icon_width, icon_height = icon.size + pixel_x = int(atom.getProperty('pixel_x', 0)) + pixel_y = int(atom.getProperty('pixel_y', 0)) + + return self.tilePosToBBox(x, y, pixel_x, pixel_y, icon_height, icon_width) + + def tilePosToBBox(self, tile_x, tile_y, pixel_x, pixel_y, icon_height, icon_width): + # Tile Pos + X = tile_x * 32 + Y = tile_y * 32 + + # pixel offsets + X += pixel_x + Y -= pixel_y + + # BYOND coordinates -> PIL coords. + # BYOND uses LOWER left. + # PIL uses UPPER left + X += 0 + Y += 32 - icon_height + + return ( + X, + Y, + X + icon_width, + Y + icon_height + ) + + # So we can read a map without parsing the tree. + def GetAtom(self, path): + if self.tree is not None: + atom = self.tree.GetAtom(path) + if atom is None and self.forgiving_atom_lookups: + self.missing_atoms.add(path) + return Atom(path, self.filename, missing=True) + return atom + return Atom(path) + diff --git a/tools/BYONDTools/byond/map/format/__init__.py b/tools/BYONDTools/byond/map/format/__init__.py new file mode 100644 index 00000000000..8c13b21e15f --- /dev/null +++ b/tools/BYONDTools/byond/map/format/__init__.py @@ -0,0 +1,13 @@ +import glob, os +from .base import * + +def Load(): + print('Loading Map Format Modules...') + for f in glob.glob(os.path.dirname(__file__) + "/*.py"): + modName = 'byond.map.format.' + os.path.basename(f)[:-3] + print(' Loading module ' + modName) + mod = __import__(modName) + for attr in dir(mod): + if not attr.startswith('_'): + #print(' {} = {}'.format(attr,getattr(mod, attr))) + globals()[attr] = getattr(mod, attr) \ No newline at end of file diff --git a/tools/BYONDTools/byond/map/format/base.py b/tools/BYONDTools/byond/map/format/base.py new file mode 100644 index 00000000000..2906b46cb4d --- /dev/null +++ b/tools/BYONDTools/byond/map/format/base.py @@ -0,0 +1,33 @@ + +# Decorator +class MapFormat(object): + all = {} + def __init__(self, ext, _id=None): + self.id = _id + self.extension = ext + def __call__(self, c): + if self.id is None: + fname_p = c.__name__ + self.id = fname_p + print('Adding Map Format "{}" (.{})...'.format(self.id,self.extension)) + if self.extension in MapFormat.all: + print('!!! *.{} FILES ARE ALREADY HANDLED BY {}!'.format(self.extension,self.id)) + MapFormat.all[self.extension] = c + return c + +def GetMapFormat(_map,ext): + f = MapFormat.all.get(ext.lstrip('.'),None) + if f is None: + print('Unable to find MapFormat for {}.'.format(ext)) + return f(_map) + +class BaseMapFormat: + def __init__(self, _map): + self.map = _map + self.missing_atoms = set() + + def Load(self, filename, **kwargs): + return + + def Save(self, filename, **kwargs): + return \ No newline at end of file diff --git a/tools/BYONDTools/byond/map/format/dmm.py b/tools/BYONDTools/byond/map/format/dmm.py new file mode 100644 index 00000000000..eb62c9bf0bd --- /dev/null +++ b/tools/BYONDTools/byond/map/format/dmm.py @@ -0,0 +1,490 @@ +from byond.basetypes import * +from byond.utils import getElapsed +# from byond.map import Tile, MapLayer +from byond.map.format.base import BaseMapFormat, MapFormat +import os, sys, logging, itertools, shutil, collections, math +from time import clock + +ID_ENCODING_TABLE = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' +IET_SIZE = len(ID_ENCODING_TABLE) + +def chunker(iterable, chunksize): + """ + Return elements from the iterable in `chunksize`-ed lists. The last returned + chunk may be smaller (if length of collection is not divisible by `chunksize`). + + >>> print list(chunker(xrange(10), 3)) + [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]] + """ + i = iter(iterable) + while True: + wrapped_chunk = [list(itertools.islice(i, int(chunksize)))] + if not wrapped_chunk[0]: + break + yield wrapped_chunk.pop() + +def DMMSortAlg(v): + t = (v.upper(),) + l = [c.isupper() for c in v] + l.reverse() + return t + tuple(l) + +@MapFormat('dmm') +class DMMFormat(BaseMapFormat): + def __init__(self, map): + BaseMapFormat.__init__(self, map) + + self.tileTypes = [] + self.instances = [] + self.oldID2NewID = {} + self.log = logging.getLogger('DMMFormat') + + self.filename = 'BUILT-IN?' + + # Number of duplicates found when loading. + self.duplicates = 0 + + # Caches + self.tileChunk2ID = {} + self.atomCache = {} + + self.atomBorders = { + '{':'}', + '"':'"', + '(':')' + } + nit = self.atomBorders.copy() + for start, stop in self.atomBorders.items(): + if start != stop: + nit[stop] = None + self.atomBorders = nit + self.idlen = 0 + + self.dump_inherited = False + + def Load(self, filename, **kwargs): + if not os.path.isfile(filename): + self.log.warn('File ' + filename + " does not exist.") + self.filename = filename + with open(filename, 'r') as f: + print('Reading tile types from {0}...'.format(self.filename)) + self.consumeTiles(f) + print('Reading tile positions...') + self.consumeTileMap(f) + + def consumeDataValue(self, value, lineNumber): + data = None + if value[0] in ('"', "'"): + quote = value[0] + if quote == '"': + data = BYONDString(value[1:-1], self.filename, lineNumber) + elif quote == "'": + data = BYONDFileRef(value[1:-1], self.filename, lineNumber) + elif value == 'null': + data = BYONDValue(None, self.filename, lineNumber) + else: + data = BYONDValue(value, self.filename, lineNumber) + return data + def consumeTileMap(self, f): + zLevel = None + y = 0 + z = 0 + inZLevel = False + width = 0 + height = 0 + while True: + line = f.readline() + if line == '': + return + # (1,1,1) = {" + if line.startswith('('): + coordChunk = line[1:line.index(')')].split(',') + # print(repr(coordChunk)) + z = int(coordChunk[2]) + inZLevel = True + y = 0 + width = 0 + height = 0 + continue + if line.strip() == '"}': + inZLevel = False + if height == 0: + height = y + # self.map.zLevels[z] = zLevel + zLevel = None + # self.log.info('Added map layer {0} ({1}x{2})'.format(z, height, width)) + print(' * Added map layer {0} ({1}x{2})'.format(z, height, width)) + continue + if inZLevel: + if zLevel is None: + zLevel = self.map.CreateZLevel(height, width) # , z-1) + if width == 0: + width = len(line) / self.idlen + # print('Width detected as {}.'.format(width)) + zLevel.Resize(width, width) + if width > 255: + logging.warn("Line is {} blocks long!".format(width)) + x = 0 + for chunk in chunker(line.strip(), self.idlen): + chunk = self.String2ID(''.join(chunk)) + tid = self.oldID2NewID[chunk] + zLevel.tiles[x, y] = self.tileTypes[tid].copy(origID=True) + x += 1 + y += 1 + + def consumeTiles(self, f): + index = 0 + self.duplicates = 0 + lineNumber = 0 + self.tileChunk2ID = {} + while True: + line = f.readline() + lineNumber += 1 + if line.startswith('"'): + t = self.consumeTile(line, lineNumber) + t.ID = index + t.UpdateHash() + self.tileTypes += [t] + self.idlen = max(self.idlen, len(self.ID2String(t.ID))) + self.oldID2NewID[t.origID] = t.ID + index += 1 + # No longer needed, 2fast. + # if((index % 100) == 0): + # print(index) + else: + self.log.info('-- {} tiles loaded, {} duplicates discarded'.format(index, self.duplicates)) + return + + def consumeTileAtoms(self, line, lineNumber): + instances = [] + atom_chunks = self.SplitAtoms(line) + + for atom_chunk in atom_chunks: + if atom_chunk in self.atomCache: + instances += [self.atomCache[atom_chunk]] + else: + atom = self.consumeAtom(atom_chunk, lineNumber) + self.atomCache[atom_chunk] = atom + instances += [atom] + + return instances + + def SplitProperties(self, string): + o = [] + buf = [] + inString = False + for chunk in string.split(';'): + if not inString: + if '"' in chunk: + inString = False + pos = 0 + while(True): + pos = chunk.find('"', pos) + if pos == -1: + break + pc = '' + if pos > 0: + pc = chunk[pos - 1] + if pc != '\\': + inString = not inString + pos += 1 + if not inString: + o += [chunk] + else: + buf += [chunk] + else: + o += [chunk] + else: + if '"' in chunk: + o += [';'.join(buf + [chunk])] + inString = False + buf = [] + else: + buf += [chunk] + return o + + def SplitAtoms(self, string): + ignoreLevel = [] + + o = [] + buf = '' + + string = string.rstrip() + line_len = len(string) + for i in xrange(line_len): + c = string[i] + pc = '' + if i > 0: + pc = string[i - 1] + + if c in self.atomBorders and pc != '\\': + end = self.atomBorders[c] + if end == c: # Just toggle. + if len(ignoreLevel) > 0: + if ignoreLevel[-1] == c: + ignoreLevel.pop() + else: + ignoreLevel.append(c) + else: + if end == None: + if len(ignoreLevel) > 0: + if ignoreLevel[-1] == c: + ignoreLevel.pop() + else: + ignoreLevel.append(end) + if c == ',' and len(ignoreLevel) == 0: + o += [buf] + buf = '' + else: + buf += c + + if len(ignoreLevel) > 0: + print(repr(ignoreLevel)) + sys.exit() + return o + [buf] + + def consumeAtom(self, line, lineNumber=0): + if '{' not in line: + atom = line.strip() + if atom.endswith('/'): + self.log.warn('{file}:{line}: Malformed atom: {data} has ending slash. Stripping slashes from right side.'.format(file=self.filename, line=lineNumber, data=atom)) + atom = atom.rstrip('/') + currentAtom = self.map.GetAtom(atom) + if currentAtom is not None: + return currentAtom + else: + self.log.critical('{file}:{line}: Failed to consumeAtom({data}): Unable to locate atom.'.format(file=self.filename, line=lineNumber, data=line)) + return None + chunks = line.split('{') + if len(chunks) < 2: + self.log.error('{file}:{line}: Something went wrong in consumeAtom(). line={data}'.format(file=self.filename, line=lineNumber, data=line)) + atom = chunks[0].strip() + if atom.endswith('/'): + self.log.warn('{file}:{line}: Malformed atom: {data} has ending slash. Stripping slashes from right side.'.format(file=self.filename, line=lineNumber, data=atom)) + atom = atom.rstrip('/') + currentAtom = self.map.GetAtom(atom) + if currentAtom is not None: + currentAtom = currentAtom.copy() + else: + return None + if chunks[1].endswith('}'): + chunks[1] = chunks[1][:-1] + property_chunks = self.SplitProperties(chunks[1]) + mapSupplied = [] + for chunk in property_chunks: + if chunk.endswith('}'): + chunk = chunk[:-1] + pparts = chunk.split('=', 1) + key = pparts[0].strip() + value = pparts[1].strip() + if key == '': + self.log.warn('Ignoring property with blank name. (given {0})'.format(chunk)) + continue + data = self.consumeDataValue(value, lineNumber) + if key not in currentAtom.mapSpecified: + mapSupplied += [key] + currentAtom.properties[key] = data + + currentAtom.mapSpecified = mapSupplied + + # Compare to base + if True: # TODO: not (self.readFlags & Map.READ_NO_BASE_COMP): + base_atom = self.map.GetAtom(currentAtom.path) + assert base_atom != None + # for key in base_atom.properties.keys(): + # val = base_atom.properties[key] + # if key not in currentAtom.properties: + # currentAtom.properties[key] = val + for key in currentAtom.properties.iterkeys(): + val = currentAtom.properties[key].value + if key in base_atom.properties and val == base_atom.properties[key].value: + if key in currentAtom.mapSpecified: + currentAtom.mapSpecified.remove(key) + # print('Removed {0} from atom: Equivalent to base atom!') + + return currentAtom + + def consumeTile(self, line, lineNumber): + t = self.map.CreateTile() + t.origID = self.consumeTileID(line) + tileChunk = line.strip()[line.index('(') + 1:-1] + + if tileChunk in self.tileChunk2ID: + tid = self.tileChunk2ID[tileChunk] + print('{} duplicate of {}! Installing redirect...'.format(t.origID, tid)) + self.oldID2NewID[t.origID] = tid + self.duplicates += 1 + return self.tileTypes[tid] + self.tileChunk2ID[tileChunk]=t.origID + t.instances = self.consumeTileAtoms(tileChunk, lineNumber) + if t.origID == 0: + print('{} -> {}'.format(t.origID,tileChunk)) + print('AKA {}'.format(str(t))) + return t + + def consumeTileID(self, line): + e = line.index('"', 1) + return self.String2ID(line[1:e]) + + def SerializeAtom(self, atom): + atomContents = [] + # print(repr(self.mapSpecified)) + if self.dump_inherited: + for key, val in atom.properties.items(): + atomContents += ['{0} = {1}'.format(key, val)] + else: + for i in range(len(atom.mapSpecified)): + key = atom.mapSpecified[i] + if key in atom.properties: + val = atom.properties[key] + atomContents += ['{0} = {1}'.format(key, val)] + if len(atomContents) > 0: + return atom.path + '{' + '; '.join(atomContents) + '}' + else: + return atom.path + + def SerializeTile(self, tile): + # "aat" = (/obj/structure/grille,/obj/structure/window/reinforced{dir = 8},/obj/structure/window/reinforced{dir = 1},/obj/structure/window/reinforced,/obj/structure/cable{d1 = 2; d2 = 4; icon_state = "2-4"; tag = ""},/turf/simulated/floor/plating,/area/security/prison) + atoms = [] + if tile.origID == 0: + print(str(tile)) + print('len = {}'.format(len(tile.instances))) + for i in xrange(len(tile.instances)): + atom = tile.instances[i] + if atom.path != '': + atoms += [self.SerializeAtom(atom)] + + return '({atoms})'.format(atoms=','.join(atoms)) + + def ID2String(self, _id, pad=0): + o = '' + while(_id >= len(ID_ENCODING_TABLE)): + i = _id % IET_SIZE + o = ID_ENCODING_TABLE[i] + o + _id -= i + _id /= IET_SIZE + o = ID_ENCODING_TABLE[_id] + o + if pad > len(o): + o = o.rjust(pad, ID_ENCODING_TABLE[0]) + return o + + def String2ID(self, _id): + o = 0 + for i, c in enumerate(reversed(_id)): + o += ID_ENCODING_TABLE.index(c) * (IET_SIZE ** i) + return o + + def AssignTID(self, tile): + ''' + :param tile Tile: + Tile to assign ID to. + ''' + if tile not in self.tileTypes: + self.tileTypes.append(tile) + tile.ID = self.tileTypes.index(tile) + return self.ID2String(tile) + + def GetTID(self, tile): + # print('GetTID: origID: {}'.format(repr(tile.origID))) + if self.serialize_cleanly and tile.origID != '': + return tile.origID + else: + return tile.ID + + def Save(self, filename, **kwargs): + self.filename = filename + + self.tileTypes = [] + examined = [] + hashMap = {} + self.typeMap = {} + self.type2TID = {} + self.instances = [] + + self.serialize_cleanly = kwargs.get('clean', True) + self.dump_inherited = kwargs.get('inherited', False) + + #print('"aaa" = {}'.format(self.map.GetTileAt(0,0,0))) + + # Preprocess and assign IDs. + idlen = 0 + it = self.map.Tiles() + lz = -1 + last_str = None + start = None + maxid=0 + for tile in it: + #print(str(tile)) + if lz != it.z: + if start: + print(' -> Took {}'.format(getElapsed(start))) + print(' * Consolidating level {}...'.format(it.z+1)) + start = clock() + lz = it.z + strt = tile.GetHash() # str(tile) + dbg = False + #if strt == 'e18826df83665059358c177f43f6ac72': + # dbg=True + if last_str == strt: + #if dbg: print('Continuing: Same as last examined.') + continue + if strt in examined: + #if dbg: print('Continuing: Hash already examined.') + last_str = strt + continue + # tile.ID = len(examined) + examined += [strt] + tid = self.GetTID(tile) + if tid in self.typeMap: + if hashMap.get(strt, None) == tid: + if dbg: print('Continuing: TID {} in typeMap, hashes match.'.format(tid)) + continue + tile.origID = '' + tid = len(self.typeMap) + while tid in self.typeMap: + tid+=1 + #print('{} assigned to new TID {}'.format(strt,tid)) + maxid=max(maxid,tid) + + #if tid == 0: print('<{},{},{}> {}: {}'.format(it.x, it.y, it.z, tid, str(tile))) + self.typeMap[tid] = (strt,self.SerializeTile(tile)) + last_str = strt + hashMap[strt]=tid + + ############################################################################ + ## HACK ALERT BECAUSE SOMETHING'S HORRIBLY WRONG AND I DON'T KNOW WHAT IT IS + ############################################################################ + self.typeMap[0]=(self.typeMap[0][0],'(/turf/space,/area)') + ### /hack + + idlen=len(self.ID2String(maxid)) + tmpfile = filename + '.tmp' + print('Opening {} for write...'.format(tmpfile)) + print('idlen = {}'.format(idlen)) + print('"aaa" = {}'.format(self.typeMap[0][1])) + start = clock() + with open(tmpfile, 'w') as f: + for tid in sorted(self.typeMap.keys()): + stid=self.ID2String(tid, idlen) + strt,serdata=self.typeMap[tid] + f.write('"{}" = {}\n'.format(stid, serdata)) + self.type2TID[strt] = stid + print(' Wrote types in {}...'.format(getElapsed(start))) + lap = clock() + for z in xrange(len(self.map.zLevels)): + print(' Writing z={}...'.format(z)) + f.write('\n(1,1,{0}) = {{"\n'.format(z+1)) + zlevel = self.map.zLevels[z] + for y in xrange(zlevel.height): + for x in xrange(zlevel.width): + tile = self.map.GetTileAt(x, y, z) + thash = tile.GetHash() + f.write(self.type2TID[thash]) + f.write("\n") + f.write('"}\n') + print(' Wrote tiles in {}...'.format(getElapsed(lap))) + if os.path.isfile(filename): + os.remove(filename) + os.rename(tmpfile, filename) + print('-> {} in {}'.format(filename, getElapsed(start))) + diff --git a/tools/BYONDTools/byond/objtree.py b/tools/BYONDTools/byond/objtree.py index 394c6f1d17a..abeb798e0aa 100644 --- a/tools/BYONDTools/byond/objtree.py +++ b/tools/BYONDTools/byond/objtree.py @@ -22,7 +22,7 @@ def debug(filename, line, path, message): class OTRCache: # : Only used for obliterating outdated data. - VERSION = [28, 4, 2014] + VERSION = [18, 6, 2014] def __init__(self, filename): self.filename = filename @@ -93,13 +93,13 @@ class ObjectTree: 'atom_defaults.dm' ) def __init__(self, **options): - #: All atoms, in a list. + # : All atoms, in a list. self.Atoms = {} - #: All atoms, in a tree-node structure. + # : All atoms, in a tree-node structure. self.Tree = Atom('') - #: Skip loading from .OTR? + # : Skip loading from .OTR? self.skip_otr = False self.LoadedStdLib = False @@ -193,7 +193,7 @@ class ObjectTree: if c == '"': inString = not inString if not inString: - filepath = os.path.join(rootdir, filename) + filepath = os.path.join(rootdir, filename.replace('\\',os.sep)) if filepath.endswith(ext): ToRead += [filepath] filename = '' @@ -583,9 +583,13 @@ class ObjectTree: line = decl = self.lineBeforePreprocessing.strip() else: decl = line.strip() - if '[' in line: + if '[' in line and not 'list(' in line: line_split, arr_decl = line.split('[', 1) - str_size = arr_decl[:arr_decl.index(']')] + try: + str_size = arr_decl[:arr_decl.index(']')] + except ValueError: + logging.warn('{}:{}: MALFORMED CODE: Unable to find ]'.format(filename, ln)) + logging.info(line) size = -1 if str_size != '': try: @@ -626,6 +630,8 @@ class ObjectTree: 'size':size } if typepath != '/': + if value == 'null': + return (name, BYONDValue(None, filename, ln, typepath, **kwargs)) return (name, BYONDValue(value, filename, ln, typepath, **kwargs)) elif value and value[0] == '"': return (name, BYONDString(value[1:-1], filename, ln, **kwargs)) @@ -636,6 +642,8 @@ class ObjectTree: return (name, BYONDValue(float(value), filename, ln, typepath, **kwargs)) except ValueError: pass + elif value == 'null': + return (name, BYONDValue(None, filename, ln, typepath, **kwargs)) return (name, BYONDValue(value, filename, ln, typepath, **kwargs)) def MakeTree(self): @@ -662,7 +670,7 @@ class ObjectTree: if '(' in path_item: cNode.children[path_item] = Proc('/'.join([''] + cpath), []) else: - cNode.children[path_item] = Atom('/'.join([''] + cpath),atom.filename,atom.line) + cNode.children[path_item] = Atom('/'.join([''] + cpath), atom.filename, atom.line) cNode.children[path_item].parent = cNode parent_type = cNode.children[path_item].getProperty('parent_type') if parent_type is not None: diff --git a/tools/BYONDTools/byond/utils.py b/tools/BYONDTools/byond/utils.py index 8f65a20e970..d42707bd793 100644 --- a/tools/BYONDTools/byond/utils.py +++ b/tools/BYONDTools/byond/utils.py @@ -1,4 +1,5 @@ import hashlib, ast, os +from time import clock import operator as op def md5sum(filename): @@ -26,6 +27,9 @@ def eval_expr(expr): """ return eval_(ast.parse(expr).body[0].value) # Module(body=[Expr(value=...)]) +def getElapsed(start): + return '%d:%02d:%02d.%03d' % reduce(lambda ll, b : divmod(ll[0], b) + ll[1:], [((clock() - start) * 1000,), 1000, 60, 60]) + def eval_(node): if isinstance(node, ast.Num): # return node.n