-
-
Save anonymous/1f7e51021d9cc61dbc4c22df2bef0a5c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Map Interface Module | |
Copyright 2013 Rob "N3X15" Nelson <nexis@7chan.org> | |
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 operator import itemgetter | |
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 LocationIterator: | |
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 TileIterator: | |
def __init__(self, _map): | |
self.map = _map | |
self.pos = -1 | |
self.max = len(self.map.tiles) | |
def __iter__(self): | |
return self | |
def __next__(self): | |
return self.next() | |
def next(self): | |
self.pos += 1 | |
if self.pos >= self.max: | |
raise StopIteration | |
t = self.map.tiles[self.pos] | |
#print('#{} = {}'.format(self.pos,str(t))) | |
return t | |
class AtomIterator: | |
def __init__(self, _map): | |
self.map = _map | |
self.pos = -1 | |
self.max = len(self.map.instances) | |
def __iter__(self): | |
return self | |
def __next__(self): | |
return self.next() | |
def next(self): | |
self.pos += 1 | |
if self.pos >= self.max: | |
raise StopIteration | |
t = self.map.instances[self.pos] | |
# print('#{} = {}'.format(self.pos,str(t))) | |
return t | |
class Tile(object): | |
def __init__(self, _map, master=False): | |
# : Map's copy of the tile, used for tracking. | |
self.master = master | |
self.coords = (0, 0, 0) | |
self.origID = '' | |
self.ID = -1 | |
self.instances = [] | |
self.locations = [] | |
self.frame = None | |
self.unselected_frame = None | |
self.areaSelected = True | |
self.log = logging.getLogger(__name__ + '.Tile') | |
self.map = _map | |
self._hash = None | |
self.orig_hash = None | |
def UpdateHash(self, no_map_update=False): | |
if self._hash is None: | |
# Why MD5? Because the shorter the string, the faster the comparison. | |
self._hash = hashlib.md5(str(self)).hexdigest() | |
if not no_map_update: | |
self.ID=self.map.UpdateTile(self) | |
if self.ID==-1: | |
raise Error('self.ID == -1') | |
def InvalidateHash(self): | |
if self._hash is not None: | |
self.orig_hash = self._hash | |
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. | |
''' | |
if atom is None: return | |
self.instances.remove(atom.ID) | |
self.InvalidateHash() | |
if hash: self.UpdateHash() | |
def AppendAtom(self, atom, hash=True): | |
''' | |
:param Atom atom: | |
Atom to add. | |
''' | |
if atom is None: return | |
atom.UpdateMap(self.map) | |
self.instances.append(atom.ID) | |
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.ID) | |
def copy(self, origID=False): | |
tile = self.map.CreateTile() | |
tile.ID = self.ID | |
tile.instances = [x for x in self.instances] | |
if origID: | |
tile.origID = self.origID | |
if not self._hash: | |
self.UpdateHash(no_map_update=True) | |
tile._hash = self._hash | |
return tile | |
def GetAtoms(self): | |
atoms = [] | |
for id in self.instances: | |
if id is None: | |
continue | |
a = self.map.GetInstance(id) | |
if a is None: | |
self.log.debug('Unknown instance ID {}!'.format(id)) | |
continue | |
atoms += [a] | |
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 rmLocation(self, coord, autoclean=True): | |
if coord in self.locations: | |
self.locations.remove(coord) | |
if autoclean and len(self.locations) == 0: | |
self.map.tiles[self.ID] = None # Mark ready for recovery | |
self.map._tile_idmap.pop(self.GetHash(), None) | |
def addLocation(self, coord): | |
if coord not in self.locations: | |
self.locations.append(coord) | |
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.GetAtoms()]) | |
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/open/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 | |
class MapLayer: | |
def __init__(self, z, _map, height=255, width=255): | |
self.initial_load=False | |
self.map = _map | |
self.min = (0, 0) | |
self.max = (height - 1, width - 1) | |
self.tiles = None | |
self.Resize(height, width) | |
self.z = z | |
def GetTile(self, x, y): | |
# return self.tiles[y][x] | |
t = self.map.GetTileByID(self.tiles[x, y]) | |
t.coords = (x, y, self.z) | |
return t | |
def SetTile(self, x, y, tile): | |
''' | |
:param x int: | |
:param y int: | |
:param tile Tile: | |
''' | |
''' | |
if not self.initial_load: | |
# Remove old tile. | |
oldid = self.tiles[x, y] | |
if oldid < len(self.map.instances): | |
t = self.map.tiles[oldid] | |
if t: t.rmLocation((x, y, self.z)) | |
''' | |
# Set new tile. | |
if not self.initial_load: | |
tile.ID=self.map.UpdateTile(tile) | |
self.tiles[x, y] = tile.ID | |
#self.map.tiles[tile.ID].addLocation((x, y, self.z)) | |
def SetTileID(self, x, y, newID): | |
''' | |
:param x int: | |
:param y int: | |
:param newID int: | |
''' | |
if newID is None: | |
raise Exception('newID cannot be None') | |
t = self.map.tiles[newID] | |
if t is None: | |
raise KeyError('Unknown tile #{}'.format(newID)) | |
#self.SetTile(x, y, t) | |
''' | |
if not self.initial_load: | |
# Remove old tile. | |
oldid = self.tiles[x, y] | |
if oldid < len(self.map.instances): | |
t = self.map.tiles[oldid] | |
if t: t.rmLocation((x, y, self.z)) | |
''' | |
self.tiles[x, y] = newID | |
#self.map.tiles[newID].addLocation((x, y, self.z)) | |
def Resize(self, height, width): | |
self.height = height | |
self.width = width | |
basetile = self.map.basetile; | |
if self.tiles is None: | |
self.tiles = numpy.empty((height, width), int) # object) | |
for y in xrange(height): | |
for x in xrange(width): | |
self.SetTile(x, y, basetile) | |
else: | |
self.tiles.resize(height, width) | |
# self.tiles = [[Tile(self.map) for _ in xrange(width)] for _ in xrange(height)] | |
class MapRenderFlags: | |
RENDER_STARS = 1 | |
RENDER_AREAS = 2 | |
class Map: | |
def __init__(self, tree=None, **kwargs): | |
self.zLevels = [] | |
self._instance_idmap = {} # md5 -> id | |
self._tile_idmap = {} # md5 -> id | |
self.basetile = Tile(self) | |
self.instances = [] # Atom | |
self.tiles = [] # Tile | |
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') | |
self.missing_atoms = set() | |
self.basetile.UpdateHash(); | |
def ResetTilestore(self): | |
'''For loading maps. Resets tile data to a pristine state.''' | |
self.instances = [] # Atom | |
self.tiles = [] # Tile | |
self.basetile = None | |
def GetTileByID(self, tileID): | |
t = self.tiles[tileID] | |
if t is None: | |
return None | |
t = t.copy() | |
t.master = False | |
return t | |
def GetInstance(self, atomID): | |
a=None | |
try: | |
a = self.instances[atomID] | |
except IndexError as e: | |
self.log.critical('Unable to find instance {}!') | |
raise e | |
if a is None: | |
# print('WARNING: #{0} not found'.format(atomID)) | |
return None | |
a = a.copy() | |
# a.master = False | |
return a | |
def UpdateTile(self, t): | |
''' | |
Update tile registry. | |
:param t Tile: | |
Tile to update. | |
:return Tile ID: | |
''' | |
thash = t.GetHash() | |
# if t.ID >= 0 and t.ID < len(self.tiles) and self.tiles[t.ID] is not None: | |
# self.tiles[t.ID].rmLocation(t.coords) | |
tiles_action = "-" | |
''' | |
if t in self.tiles: | |
t.ID = self.tiles.index(t) | |
else: | |
''' | |
idmap_action = "-" | |
if thash not in self._tile_idmap: | |
idmap_action = "Added" | |
t.ID = len(self.tiles) | |
self.tiles += [t.copy()] | |
self._tile_idmap[thash] = t.ID | |
tiles_action = "Added" | |
#print('Assigned ID #{} to tile {}'.format(t.ID,thash)) | |
elif self._tile_idmap[thash] != t.ID: | |
t.ID = self._tile_idmap[thash] | |
idmap_action = "Updated" | |
#print('Updated tile {1} to ID #{0}'.format(t.ID,thash)) | |
#print('Updated #{} - Tiles: {}, idmap: {}'.format(t.ID, thash, tiles_action, idmap_action)) | |
self.tiles[t.ID].addLocation(t.coords) | |
return t.ID | |
def UpdateAtom(self, a): | |
''' | |
Update tile registry. | |
:param a Atom: Tile to update. | |
''' | |
thash = a.GetHash() | |
if a.ID and len(self.instances) < a.ID and self.instances[a.ID] is not None: | |
self.instances[a.ID].rmLocation(self, a.coords) | |
if thash not in self._instance_idmap: | |
a.ID = len(self.instances) | |
self.instances += [a.copy()] | |
self._instance_idmap[thash] = a.ID | |
#print('Assigned ID #{} to atom {}'.format(a.ID,thash)) | |
else: | |
a.ID = self._instance_idmap[thash] | |
if a.coords is not None: | |
self.instances[a.ID].addLocation(a.coords) | |
return a.ID | |
def RemoveAtom(self, a): | |
''' | |
Remove atom from the entire map. | |
:param a Atom: Tile to update. | |
''' | |
thash = a.GetHash() | |
if a.ID and len(self.instances) < a.ID and self.instances[a.ID] is not None: | |
self.instances[a.ID].rmLocation(self, a.coords) | |
del self.instances[a.ID] | |
if thash in self._instance_idmap: | |
del self._instance_idmap[thash] | |
def CreateZLevel(self, height, width, z= -1): | |
zLevel = MapLayer(z if z >= 0 else len(self.zLevels), self, height, width) | |
if z >= 0: | |
self.zLevels[z] = zLevel | |
else: | |
self.zLevels.append(zLevel) | |
return zLevel | |
def Atoms(self): | |
'''Iterates over all instances in the map. | |
''' | |
return AtomIterator(self) | |
def Tiles(self): | |
'''Iterates over all tiles of the map. | |
''' | |
return TileIterator(self) | |
def Locations(self): | |
return LocationIterator(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, 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].GetTile(x, y) | |
def CopyTileAt(self, x, y, z): | |
''' | |
:param int x: | |
:param int y: | |
:param int z: | |
:rtype Tile: | |
''' | |
return self.GetTileAt(x, y, z).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].SetTile(x, y, tile) | |
def CreateTile(self): | |
''' | |
:rtype Tile: | |
''' | |
return Tile(self) | |
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, str(atom))) | |
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 range(len(self.zLevels)): | |
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].GetTile(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 | |
atom.layer = float(2) | |
if 'layer' in atom.properties: | |
atom.layer = float(atom.properties['layer'].value) | |
if atom.path.startswith('/turf/open/floor/plasteel'): | |
atom.layer = float(2.5) | |
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/open/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) | |
t=None | |
#atom = sorted(atom, key=lambda key: 'layer') | |
if len(instancePositions) == 0: | |
return | |
print(' Rendering...') | |
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), (255,0,0,0)) | |
# 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): | |
for atom in sorted(levelAtoms, reverse=True): | |
print(atom.ID) | |
if atom.ID not in instancePositions: | |
levelAtoms.remove(atom) | |
continue | |
icon = self.renderAtom(atom, basedir, skip_alpha) | |
if icon is None: | |
levelAtoms.remove(atom) | |
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 | |
icon=None # Cleanup | |
levelAtoms.remove(atom) | |
levelAtoms = None | |
instancePositions = None | |
if len(self.selectedAreas) == 0: | |
# Autocrop (only works if NOT rendering stars or areas) | |
#pic = trim(pic) # FIXME: MemoryError on /vg/. | |
pic=pic # Hack | |
else: | |
# if nSelAreas == 0: | |
# continue | |
pic = pic.crop(bbox) | |
if pic is not None: | |
# Saev | |
filedir = os.path.dirname(os.path.abspath(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, '(map)', missing=True) | |
return atom | |
return Atom(path) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment