def load_charname(self, book=None) -> None: """ Read our character name; this is a bit dependant on the book number, which is why we pass it in here. If the book number is not passed in, we will scan for map files and then use the first map's book number. If there are no maps in the slot, then we'll raise a LoadException """ # First figure out which book number we are, if we don't have it yet if not book: if not self.maps_loaded: self.load_maps() if len(self.maps) > 0: book = self.maps[0].book else: raise LoadException( 'Could not auto-detect which book version to use for charname') # Now do the actual loading if not os.path.exists(self.char_loc): raise LoadException(f'"char" file not found in {self.char_loc}') df = Savefile(self.char_loc) df.open_r() if book == 1: df.readint() else: df.readuchar() self.charname = df.readstr().decode('UTF-8') self.char_loaded = True df.close()
def load(filename, book=None, req_book=None): """ Static method to load a savename file. This will open the file once and read in a bit of data to determine whether this is a Book 1 character file or a Book 2 character file, and then call the appropriate constructor and return the object. The individual Book constructors expect to be passed in an """ df = Savefile(filename) # First figure out what format to load, if needed if book is None: try: df.open_r() name = df.readstr() date = df.readstr() time = df.readstr() map_or_version = df.readstr() df.close() except (IOError, struct.error), e: raise LoadException(str(e)) if map_or_version.startswith('book3'): book = 3 elif map_or_version in B1Constants.maps: book = 1 else: book = 2
def load(filename, book=None, req_book=None): """ Static method to load a character file. This will open the file once and read in a bit of data to determine whether this is a Book 1 character file or a Book 2 character file, and then call the appropriate constructor and return the object. The individual Book constructors expect to be passed in an """ df = Savefile(filename) # First figure out what format to load, if needed if book is None: # The initial "zero" padding in Book 1 is four bytes, and only one byte in # Book 2. Since the next bit of data is the character name, as a string, # if the second byte of the file is 00, we'll assume that it's a Book 1 file, # and Book 2 otherwise. try: df.open_r() initital = df.readuchar() second = df.readuchar() df.close() except (IOError, struct.error), e: raise LoadException(str(e)) if second == 0: book = 1 else: book = 2
def __init__(self, datadir, eschalondata): # Wall graphic groupings for i in range(101): self.wall_gfx_group[i] = self.GFX_SET_A for i in range(101, 161): self.wall_gfx_group[i] = self.GFX_SET_B for i in range(161, 251): self.wall_gfx_group[i] = self.GFX_SET_C for i in range(251, 256): self.wall_gfx_group[i] = self.GFX_SET_TREE # Wall object types for i in (range(127) + range(132, 142) + range(143, 153) + range(154, 161) + range(214, 251)): self.wall_types[i] = self.TYPE_OBJ for i in range(161, 214): self.wall_types[i] = self.TYPE_WALL for i in (range(251, 256) + range(127, 132) + [142, 153]): self.wall_types[i] = self.TYPE_TREE # Restricted entities (only one direction) self.restrict_ents = [50, 55, 58, 66, 67, 71] # Book 1 specific vars (just the PAK structure stuff) self.unknownh1 = -1 self.unknownh2 = -1 self.numfiles = -1 self.compressed_idx_size = -1 self.unknowni1 = -1 self.fileindex = {} self.zeroindex = -1 # Book 1 specific caches self.itemcache = None # Graphics PAK file self.pakloc = os.path.join(eschalondata.gamedir, 'gfx.pak') if os.path.isfile(self.pakloc): self.df = Savefile(self.pakloc) else: self.df = None # Set our loaded status self.loaded = False # Finally call the parent constructor super(B1Gfx, self).__init__(datadir, eschalondata)
def load(filename, book=None, req_book=None): """ Static method to load a map file. This will open the file once and read in a bit of data to determine whether this is a Book 1 map file or a Book 1 map file, and then call the appropriate constructor and return the object. """ df = Savefile(filename) # Book 1 files start with 10 strings, Book 2 with 9, and Book 3 with # more. To see what kind of file we have, read 11 strings and check # whether the last two are ASCII-only if book is None: try: df.open_r() strings = [] for i in range(11): strings.append(df.readstr()) df.close() except (IOError, struct.error), e: raise LoadException(str(e)) if not Map.is_ascii(strings[9]): book = 2 elif not Map.is_ascii(strings[10]): book = 1 else: book = 3
def get_mapinfo(filename=None, map_df=None): """ Given a filename or a passed filehandle, loads the first few bits of information from a map file, and will return a tuple containing the Eschalon Book the map belongs to, the internal "map name" of the map, and a Savefile object pointing to the map. Will raise a LoadException if it encounters errors. Book 1 files start with 10 strings Book 2 files start with 9 strings, followed by a uchar whose value will always be 1 (the "loadhook" var, presumably) Book 3 files start with 12 strings, the first of which is a version, which so far is always 0.992. So, to figure out dynamically what kind of file we're loading: 1) Read 9 strings, remember the first one 2) Read the next uchar - if it's 1, then we're editing Book 2 3) If the first string is "0.992", then we're editing Book 3 4) Otherwise, we're editing Book 1 Theoretically, that way this works even if a Book 2 map happens to use a mapname of 0.992, in an effort to be cheeky. """ if filename is not None: df = Savefile(filename) elif map_df is not None: df = map_df else: raise LoadException('One of filename or map_df must be passed in') stringlist = [] try: df.open_r() for i in range(9): stringlist.append(df.readstr()) nextbyte = df.readuchar() df.close() except (IOError, struct.error) as e: raise LoadException(str(e)) if nextbyte == 1: detected_book = 2 detected_mapname = stringlist[0] # TODO: We're checking for a blank string here to cover up # for some invalid data that older versions of the unofficial # pre-1.0.0 builds. By the time 1.1.0 rolls around, or so, # we should get rid of that. elif stringlist[0] == '0.992' or stringlist[0] == '': detected_book = 3 detected_mapname = stringlist[1] else: detected_book = 1 detected_mapname = stringlist[1] return detected_book, detected_mapname, df
def replicate(self): newsn = Savename.load(self.df.filename, self.book) if self.book == 1: newsn = B1Savename(Savefile(self.df.filename)) elif self.book == 2: newsn = B2Savename(Savefile(self.df.filename)) elif self.book == 3: newsn = B3Savename(Savefile(self.df.filename)) # Single vals (no need to do actual replication) newsn.savename = self.savename newsn.savedate = self.savedate newsn.savetime = self.savetime newsn.mapname = self.mapname newsn.totalsecs = self.totalsecs newsn.totalturns = self.totalturns newsn.totaldays = self.totaldays newsn.coloration = self.coloration newsn.quicktravel = self.quicktravel # Lists that need copying for val in self.options: newsn.options.append(val) for val in self.narratives: newsn.narratives.append(val) for val in self.quests: newsn.quests.append(val) for val in self.npcs: newsn.npcs.append(val) # Call out to the subclass replication function self._sub_replicate(newsn) # Now return our duplicated object return newsn
def __init__(self, datadir, eschalondata): # Wall graphic groupings for i in range(101): self.wall_gfx_group[i] = self.GFX_SET_A for i in range(101, 161): self.wall_gfx_group[i] = self.GFX_SET_B for i in range(161, 251): self.wall_gfx_group[i] = self.GFX_SET_C for i in range(251, 256): self.wall_gfx_group[i] = self.GFX_SET_TREE # Wall object types for i in (list(range(127)) + list(range(132, 142)) + list(range(143, 153)) + list(range(154, 161)) + list(range(214, 251))): self.wall_types[i] = self.TYPE_OBJ for i in range(161, 214): self.wall_types[i] = self.TYPE_WALL for i in (list(range(251, 256)) + list(range(127, 132)) + [142, 153]): self.wall_types[i] = self.TYPE_TREE # Restricted entities (only one direction) self.restrict_ents = [50, 55, 58, 66, 67, 71] # Book 1 specific vars (just the PAK structure stuff) self.unknownh1 = -1 self.unknownh2 = -1 self.numfiles = -1 self.compressed_idx_size = -1 self.unknowni1 = -1 self.fileindex = {} self.zeroindex = -1 # Book 1 specific caches self.itemcache = None # Graphics PAK file self.pakloc = os.path.join(eschalondata.gamedir, 'gfx.pak') if os.path.isfile(self.pakloc): self.df = Savefile(self.pakloc) else: self.df = None # Set our loaded status self.loaded = False # Finally call the parent constructor super(B1Gfx, self).__init__(datadir, eschalondata)
def load(filename, book=None, req_book=None): """ Static method to load a savename file. This will open the file once and read in a bit of data to determine whether this is a Book 1 character file or a Book 2 character file, and then call the appropriate constructor and return the object. The individual Book constructors expect to be passed in an """ df = Savefile(filename) # First figure out what format to load, if needed if book is None: try: df.open_r() name = df.readstr().decode('UTF-8') date = df.readstr().decode('UTF-8') time = df.readstr().decode('UTF-8') map_or_version = df.readstr().decode('UTF-8') df.close() except (IOError, struct.error) as e: LOG.error("Failed to load book", exc_info=True) raise LoadException(e) from e if map_or_version.startswith(b'book3'): book = 3 elif map_or_version in B1Constants.maps: book = 1 else: book = 2 # See if we're required to conform to a specific book if (req_book is not None and book != req_book): raise LoadException( 'This utility can only load Book %d Character files; this file is from Book %d' % (req_book, book)) # Now actually return the object if book == 1: c.switch_to_book(1) return B1Savename(df) elif book == 2: c.switch_to_book(2) return B2Savename(df) else: c.switch_to_book(3) return B3Savename(df)
def new(filename, book, map_df=None, ent_df=None): """ Sets up a new, blank Map object with the given book. Will raise a LoadException if we're passed a book we don't know about. Optionally pass in a datafile object to load our data from. """ if map_df is None: df = Savefile(filename) else: df = map_df if book == 1: c.switch_to_book(1) return B1Map(df, ent_df) elif book == 2: c.switch_to_book(2) return B2Map(df, ent_df) elif book == 3: c.switch_to_book(3) return B3Map(df, ent_df) else: raise LoadException('Unknown book version specified: %d' % (book))
def get_mapinfo(filename=None, map_df=None): """ Given a filename or a passed filehandle, loads the first few bits of information from a map file, and will return a tuple containing the Eschalon Book the map belongs to, the internal "map name" of the map, and a Savefile object pointing to the map. Will raise a LoadException if it encounters errors. Book 1 files start with 10 strings Book 2 files start with 9 strings, followed by a uchar whose value will always be 1 (the "loadhook" var, presumably) Book 3 files start with 12 strings, the first of which is a version, which so far is always 0.992. So, to figure out dynamically what kind of file we're loading: 1) Read 9 strings, remember the first one 2) Read the next uchar - if it's 1, then we're editing Book 2 3) If the first string is "0.992", then we're editing Book 3 4) Otherwise, we're editing Book 1 Theoretically, that way this works even if a Book 2 map happens to use a mapname of 0.992, in an effort to be cheeky. """ if filename is not None: df = Savefile(filename) elif map_df is not None: df = map_df else: raise LoadException('One of filename or map_df must be passed in') stringlist = [] try: df.open_r() for i in range(9): stringlist.append(df.readstr()) nextbyte = df.readuchar() df.close() except (IOError, struct.error), e: raise LoadException(str(e))
class B1Gfx(Gfx): """ Grphics structure for Book 1 """ book = 1 wall_types: Dict[Any, Any] = {} wall_gfx_group: Dict[Any, Any] = {} tilebuf_mult = 1 item_dim = 42 item_cols = 10 item_rows = 24 tile_width = 52 tile_height = 26 floor_cols = 6 floor_rows = 32 decal_cols = 6 decal_rows = 32 obj_a_width = 52 obj_a_height = 52 obj_a_cols = 6 obj_a_rows = 16 obj_a_offset = 1 obj_b_width = 52 obj_b_height = 78 obj_b_cols = 6 obj_b_rows = 10 obj_b_offset = 101 obj_c_width = 52 obj_c_height = 78 obj_c_cols = 6 obj_c_rows = 10 obj_c_offset = 161 obj_d_width = 52 obj_d_height = 130 obj_d_cols = 5 obj_d_rows = 1 obj_d_offset = 251 walldecal_cols = 6 walldecal_rows = 10 GFX_SET_A = 1 GFX_SET_B = 2 GFX_SET_C = 3 GFX_SET_TREE = 4 def __init__(self, datadir, eschalondata): # Wall graphic groupings for i in range(101): self.wall_gfx_group[i] = self.GFX_SET_A for i in range(101, 161): self.wall_gfx_group[i] = self.GFX_SET_B for i in range(161, 251): self.wall_gfx_group[i] = self.GFX_SET_C for i in range(251, 256): self.wall_gfx_group[i] = self.GFX_SET_TREE # Wall object types for i in (list(range(127)) + list(range(132, 142)) + list(range(143, 153)) + list(range(154, 161)) + list(range(214, 251))): self.wall_types[i] = self.TYPE_OBJ for i in range(161, 214): self.wall_types[i] = self.TYPE_WALL for i in (list(range(251, 256)) + list(range(127, 132)) + [142, 153]): self.wall_types[i] = self.TYPE_TREE # Restricted entities (only one direction) self.restrict_ents = [50, 55, 58, 66, 67, 71] # Book 1 specific vars (just the PAK structure stuff) self.unknownh1 = -1 self.unknownh2 = -1 self.numfiles = -1 self.compressed_idx_size = -1 self.unknowni1 = -1 self.fileindex = {} self.zeroindex = -1 # Book 1 specific caches self.itemcache = None # Graphics PAK file self.pakloc = os.path.join(eschalondata.gamedir, 'gfx.pak') if os.path.isfile(self.pakloc): self.df = Savefile(self.pakloc) else: self.df = None # Set our loaded status self.loaded = False # Finally call the parent constructor super(B1Gfx, self).__init__(datadir, eschalondata) def readfile(self, filename: str) -> object: """ Reads a given filename out of the PAK. """ if self.loaded: filepath = os.path.join( self.eschalondata.gamedir, 'packedgraphics', filename) if os.path.isfile(filepath): return open(filepath, 'rb').read() if filename in self.fileindex: self.df.open_r() self.df.seek(self.zeroindex + self.fileindex[filename].abs_index) # On Windows, we need to specify bufsize or memory gets clobbered filedata = zlib.decompress( self.df.read(self.fileindex[filename].size_compressed), 15, self.fileindex[filename].size_real) self.df.close() return filedata else: raise LoadException( 'Filename %s not found in archive' % (filename)) else: raise LoadException('PAK Index has not been loaded') def initialread(self): """ Read in the main file index. """ df = self.df if df is None: self.loaded = True return df.open_r() header = df.read(4) if (header != b'!PAK'): df.close() raise LoadException('Invalid PAK header') # Initial Values self.unknownh1 = df.readshort() self.unknownh2 = df.readshort() self.numfiles = df.readint() self.compressed_idx_size = df.readint() self.unknowni1 = df.readint() # Now load in the index decobj = zlib.decompressobj() indexdata = decobj.decompress(df.read()) self.zeroindex = df.tell() - len(decobj.unused_data) for i in range(self.numfiles): index = PakIndex(indexdata[:272]) indexdata = indexdata[272:] self.fileindex[index.filename] = index # Close and clean up df.close() self.loaded = True def get_item(self, item, size=None, gdk=True): if (self.itemcache is None): self.itemcache = GfxCache(self.readfile( 'items_mastersheet.png'), 42, 42, 10) return self.itemcache.getimg(item.pictureid + 1, size, gdk) def get_floor(self, floornum, size=None, gdk=False): if (floornum == 0): return None if (self.floorcache is None): self.floorcache = GfxCache(self.readfile( 'iso_tileset_base.png'), 52, 26, 6) return self.floorcache.getimg(floornum, size, gdk) def get_decal(self, decalnum, size=None, gdk=False): if (decalnum == 0): return None if (self.decalcache is None): self.decalcache = GfxCache(self.readfile( 'iso_tileset_base_decals.png'), 52, 26, 6) return self.decalcache.getimg(decalnum, size, gdk) # Returns a tuple, first item is the surface, second is the extra height to add while drawing def get_object(self, objnum, size=None, gdk=False, treeset=0): """ Note that we ignore the treeset flag in book 1 """ if (objnum == 0): return (None, 0, 0) try: gfxgroup = self.wall_gfx_group[objnum] except KeyError: return (None, 0, 0) if gfxgroup == self.GFX_SET_A: if (self.objcache1 is None): self.objcache1 = GfxCache(self.readfile( 'iso_tileset_obj_a.png'), 52, 52, 6) return (self.objcache1.getimg(objnum, size, gdk), 1, 0) elif gfxgroup == self.GFX_SET_B: if (self.objcache2 is None): self.objcache2 = GfxCache(self.readfile( 'iso_tileset_obj_b.png'), 52, 78, 6) return (self.objcache2.getimg(objnum - 100, size, gdk), 2, 0) elif gfxgroup == self.GFX_SET_C: if (self.objcache3 is None): self.objcache3 = GfxCache(self.readfile( 'iso_tileset_obj_c.png'), 52, 78, 6) return (self.objcache3.getimg(objnum - 160, size, gdk), 2, 0) else: if (self.objcache4 is None): self.objcache4 = GfxCache( self.readfile('iso_trees.png'), 52, 130, 5) if (objnum in self.treemap): return (self.objcache4.getimg(self.treemap[objnum], size, gdk), 4, 0) else: return (None, 4, 0) def get_object_decal(self, decalnum, size=None, gdk=False): if (decalnum == 0): return None if (self.objdecalcache is None): self.objdecalcache = GfxCache(self.readfile( 'iso_tileset_obj_decals.png'), 52, 78, 6) return self.objdecalcache.getimg(decalnum, size, gdk) def get_flame(self, size=None, gdk=False): """ Grabs the flame graphic, so it's clear when viewing maps. I provide my own image here instead of using the game's because the file bundled with the game doesn't have transparency information, and I don't feel like doing a conversion. """ if (self.flamecache is None): with open(os.path.join(self.datadir, 'torch_single.png'), 'rb') as df: flamedata = df.read() self.flamecache = B1GfxEntCache(flamedata, 1, 1) if (size is None): size = self.tile_width return self.flamecache.getimg(1, int(size * self.flamecache.size_scale), gdk) def get_entity(self, entnum, direction, size=None, gdk=False): entity = self.eschalondata.get_entity(entnum) if not entity: return None entnum = entity.gfxfile if (entnum not in self.entcache): filename = 'mo%d.png' % (entnum) if (entnum in self.restrict_ents): self.entcache[entnum] = B1GfxEntCache( self.readfile(filename), 2, 1) else: self.entcache[entnum] = B1GfxEntCache(self.readfile(filename)) cache = self.entcache[entnum] if (size is None): size = self.tile_width return cache.getimg(direction, int(size * cache.size_scale), gdk) def get_avatar(self, avatarnum): if avatarnum < 0 or avatarnum > 7: return None if avatarnum not in self.avatarcache: if avatarnum == 7: if os.path.exists(os.path.join(self.eschalondata.gamedir, 'mypic.png')): self.avatarcache[avatarnum] = GdkPixbuf.Pixbuf.new_from_file( os.path.join(self.eschalondata.gamedir, 'mypic.png')) else: return None else: self.avatarcache[avatarnum] = GfxCache( self.readfile('{}.png'.format(avatarnum)), 60, 60, 1).pixbuf return self.avatarcache[avatarnum]
def set_df_ent(self): try: self.df_ent = Savefile( self.df.filename[:self.df.filename.rindex('.map')] + '.ent') except ValueError: self.df_ent = Savefile('')
def replicate(self): if self.df_ent is None: new_df_ent = None else: new_df_ent = Savefile(self.df_ent.filename, self.df_ent.stringdata) if self.book == 1: newmap = B1Map(Savefile(self.df.filename, self.df.stringdata), new_df_ent) elif self.book == 2: newmap = B2Map(Savefile(self.df.filename, self.df.stringdata), new_df_ent) elif self.book == 3: newmap = B3Map(Savefile(self.df.filename, self.df.stringdata), new_df_ent) # Single vals (no need to do actual replication) newmap.mapname = self.mapname newmap.music1 = self.music1 newmap.music2 = self.music2 newmap.skybox = self.skybox newmap.atmos_sound_day = self.atmos_sound_day newmap.color_r = self.color_r newmap.color_g = self.color_g newmap.color_b = self.color_b newmap.color_a = self.color_a newmap.extradata = self.extradata newmap.tree_set = self.tree_set newmap.parallax_x = self.parallax_x newmap.parallax_y = self.parallax_y # Copy tiles for i in range(200): for j in range(100): newmap.tiles[i][j] = self.tiles[i][j].replicate() # At this point, tilecontents and entities have been replicated as well; # loop through our list to repopulate from the new objects, so that # our referential comparisons still work on the new copy. for entity in self.entities: if (entity is None): newmap.entities.append(None) else: if (entity.y < len(newmap.tiles) and entity.x < len(newmap.tiles[entity.y])): newmap.entities.append( newmap.tiles[entity.y][entity.x].entity) else: newmap.entities.append(entity.replicate()) tilecontentidxtemp = {} for tilecontent in self.tilecontents: if (tilecontent is None): newmap.tilecontents.append(None) else: if (tilecontent.y < len(newmap.tiles) and tilecontent.x < len(newmap.tiles[tilecontent.y])): key = '%d%02d' % (tilecontent.y, tilecontent.x) if (key in tilecontentidxtemp): tilecontentidxtemp[key] += 1 else: tilecontentidxtemp[key] = 0 newmap.tilecontents.append(newmap.tiles[tilecontent.y][ tilecontent.x].tilecontents[tilecontentidxtemp[key]]) else: newmap.tilecontents.append(tilecontent.replicate()) # Call out to superclass replication self._sub_replicate(newmap) # Now return our duplicated object return newmap
class Map(object): """ The base Map class. """ DIR_NO_CHANGE = 0x00 DIR_N = 0x01 DIR_NE = 0x02 DIR_E = 0x04 DIR_SE = 0x08 DIR_S = 0x10 DIR_SW = 0x20 DIR_W = 0x40 DIR_NW = 0x80 DIR_NOT_ADJACENT = 0xFF DELTA_TO_DIRECTIONS = { (0, 0): DIR_NO_CHANGE, (0, -2): DIR_N, (1, 0): DIR_E, (0, 2): DIR_S, (-1, 0): DIR_W} DELTA_TO_DIRECTIONS_EVEN = { (-1, -1): DIR_NW, (0, -1): DIR_NE, (0, 1): DIR_SE, (-1, 1): DIR_SW} DELTA_TO_DIRECTIONS_ODD = { (0, -1): DIR_NW, (1, -1): DIR_NE, (1, 1): DIR_SE, (0, 1): DIR_SW} DIRECTIONS_TO_DELTA = { DIR_NO_CHANGE: (0, 0), DIR_N: (0, -2), DIR_E: (1, 0), DIR_S: (0, 2), DIR_W: (-1, 0)} DIRECTIONS_TO_DELTA_EVEN = { DIR_NW: (-1, -1), DIR_NE: (0, -1), DIR_SE: (0, 1), DIR_SW: (-1, 1)} DIRECTIONS_TO_DELTA_ODD = { DIR_NW: (0, -1), DIR_NE: (1, -1), DIR_SE: (1, 1), DIR_SW: (0, 1)} def __init__(self, df, ent_df): """ A fresh object. """ # Everything else follows... self.df = None self.df_ent = None self.filename_ent = '' self.mapname = '' self.music1 = '' self.music2 = '' self.skybox = '' self.atmos_sound_day = '' # Not entirely sure about the alpha channel, which # is always zero, but it seems to make sense self.color_r = 255 self.color_g = 255 self.color_b = 255 self.color_a = 0 self.parallax_x = 0 self.parallax_y = 0 self.extradata = '' # Note that book 1 doesn't actually have this, but for sanity's # sake we're putting it in the base class self.tree_set = 0 self.cursqcol = 0 self.cursqrow = 0 self.tiles = [] for i in range(200): self.tiles.append([]) for j in range(100): self.tiles[i].append(Tile.new(c.book, j, i)) self.tilecontents = [] self.entities = [] self.df = df if ent_df is None: self.set_df_ent() else: self.df_ent = ent_df # Also, we'll keep track of "big graphic" mappings self.big_gfx_mappings = BigGraphicMappings(self) def set_savegame(self, savegame): """ Sets the savegame flags as-requested. """ for row in self.tiles: for tile in row: tile.savegame = savegame for entity in self.entities: entity.savegame = savegame for tilecontent in self.tilecontents: tilecontent.savegame = savegame def check_map_extension(self): """ Force the map to have a .map extension. Note that our "Save As" logic might not warn on overwriting, now, because of this. """ if self.df.filename[-4:].lower() != '.map': self.df.filename = '%s.map' % self.df.filename def set_df_ent(self): try: self.df_ent = Savefile( self.df.filename[:self.df.filename.rindex('.map')] + '.ent') except ValueError: self.df_ent = Savefile('') def get_opq_path(self): """ Returns the path to the opq file (only valid if we're a savegame). This is the level minimap, and must be present in the savegame dir. """ return self.df.filename[:self.df.filename.rindex('.map')] + '.opq' def has_opq_file(self): """ Returns true if we have a .opq file in our savegame dir, or if we're a global map file. Returns false if we're a savegame and an .opq is not found. """ if self.is_savegame() and not os.path.exists(self.get_opq_path()): return False else: return True def replicate(self): if self.df_ent is None: new_df_ent = None else: new_df_ent = Savefile(self.df_ent.filename, self.df_ent.stringdata) if self.book == 1: newmap = B1Map(Savefile(self.df.filename, self.df.stringdata), new_df_ent) elif self.book == 2: newmap = B2Map(Savefile(self.df.filename, self.df.stringdata), new_df_ent) elif self.book == 3: newmap = B3Map(Savefile(self.df.filename, self.df.stringdata), new_df_ent) # Single vals (no need to do actual replication) newmap.mapname = self.mapname newmap.music1 = self.music1 newmap.music2 = self.music2 newmap.skybox = self.skybox newmap.atmos_sound_day = self.atmos_sound_day newmap.color_r = self.color_r newmap.color_g = self.color_g newmap.color_b = self.color_b newmap.color_a = self.color_a newmap.extradata = self.extradata newmap.tree_set = self.tree_set newmap.parallax_x = self.parallax_x newmap.parallax_y = self.parallax_y # Copy tiles for i in range(200): for j in range(100): newmap.tiles[i][j] = self.tiles[i][j].replicate() # At this point, tilecontents and entities have been replicated as well; # loop through our list to repopulate from the new objects, so that # our referential comparisons still work on the new copy. for entity in self.entities: if entity is None: newmap.entities.append(None) else: if entity.y < len(newmap.tiles) and entity.x < len(newmap.tiles[entity.y]): newmap.entities.append( newmap.tiles[entity.y][entity.x].entity) else: newmap.entities.append(entity.replicate()) tilecontentidxtemp = {} for tilecontent in self.tilecontents: if tilecontent is None: newmap.tilecontents.append(None) else: if tilecontent.y < len(newmap.tiles) and tilecontent.x < len(newmap.tiles[tilecontent.y]): key = '%d%02d' % (tilecontent.y, tilecontent.x) if key in tilecontentidxtemp: tilecontentidxtemp[key] += 1 else: tilecontentidxtemp[key] = 0 newmap.tilecontents.append( newmap.tiles[tilecontent.y][tilecontent.x].tilecontents[tilecontentidxtemp[key]]) else: newmap.tilecontents.append(tilecontent.replicate()) # Call out to superclass replication self._sub_replicate(newmap) # Now return our duplicated object return newmap def _sub_replicate(self, newmap): """ Stub for superclasses to override, to replicate specific vars """ pass def set_tile_savegame(self): """ Sets the savegame flag appropriately for all tiles """ savegame = self.is_savegame() for row in self.tiles: for tile in row: tile.savegame = savegame def addtile(self): """ Add a new tile, assuming that the tiles are stored in a left-to-right, top-to-bottom format in the map. """ self.tiles[self.cursqrow][self.cursqcol].read(self.df) self.cursqcol += 1 if self.cursqcol == 100: self.cursqcol = 0 self.cursqrow += 1 def addtilecontent(self): """ Add a tilecontent. """ try: tilecontent = Tilecontent.new(c.book, self.is_savegame()) tilecontent.read(self.df) # Note that once we start deleting tilecontents, you'll have to update both constructs here. # Something along the lines of this should do: # self.map.tiles[y][x].tilecontents.remove(tilecontent) # self.tilecontents.remove(tilecontent) # ... does that object then get put into a garbage collector or something? Do we have to # set that to None at some point, manually? self.tilecontents.append(tilecontent) if 0 <= tilecontent.x < 100 and 0 <= tilecontent.y < 200: self.tiles[tilecontent.y][tilecontent.x].addtilecontent( tilecontent) return True except FirstItemLoadException as e: return False def deltilecontent(self, x, y, idx): """ Deletes a tilecontent, both from the associated tile, and our internal list. """ tile = self.tiles[y][x] tilecontent = tile.tilecontents[idx] if tilecontent is not None: self.tilecontents.remove(tilecontent) self.tiles[y][x].deltilecontent(tilecontent) def addentity(self): """ Add an entity. """ try: entity = Entity.new(c.book, self.is_savegame()) entity.read(self.df_ent) if self.tiles[entity.y][entity.x].entity is not None: # TODO: Support this better, perhaps? LOG.warn( 'Two entities on a single tile, discarding all but the original') else: self.entities.append(entity) if 0 <= entity.x < 100 and 0 <= entity.y < 200: self.tiles[entity.y][entity.x].addentity(entity) return True except FirstItemLoadException as e: return False def delentity(self, x, y): """ Deletes an entity, both from the associated tile, and our internal list. """ tile = self.tiles[y][x] ent = tile.entity if ent is not None: self.entities.remove(ent) tile.delentity() def rgb_color(self): return (self.color_r << 24) + (self.color_g << 16) + (self.color_b << 8) + 0xFF def coords_relative(self, x, y, dir): """ Static method to return coordinates for the tile relative to the given coords. 1 = N, 2 = NE, etc """ if dir == self.DIR_N: if y < 2: return None else: return x, y - 2 elif dir == self.DIR_NE: if (y % 2) == 0: if y > 0: return x, y - 1 else: return None elif x < 99: return x + 1, y - 1 else: return None elif dir == self.DIR_E: if x < 99: return x + 1, y else: return None elif dir == self.DIR_SE: if (y % 2) == 0: return x, y + 1 elif x < 99 and y < 199: return x + 1, y + 1 else: return None elif dir == self.DIR_S: if y < 198: return x, y + 2 else: return None elif dir == self.DIR_SW: if (y % 2) == 1: if y < 199: return x, y + 1 else: return None elif x > 0: return x - 1, y + 1 else: return None elif dir == self.DIR_W: if x > 0: return x - 1, y else: return None elif dir == self.DIR_NW: if (y % 2) == 1: return x, y - 1 elif y > 0 and x > 0: return x - 1, y - 1 else: return None else: return None def tile_relative(self, x, y, dir): """ Returns a tile object relative to the given coords. """ coords = self.coords_relative(x, y, dir) if coords: return self.tiles[coords[1]][coords[0]] else: return None def _convert_savegame(self, savegame): """ Does the grunt work of converting ourself to a savegame or global file. """ for col in self.tiles: for tile in col: tile._convert_savegame(savegame) for entity in self.entities: entity._convert_savegame(savegame) self.set_savegame(savegame) def convert_savegame(self, savegame=True): """ Converts ourself to a savegame or global map. This will clear out the filenames from our Savefile objects, so they can't be accidentally overwritten without effort. """ if (savegame and self.is_global()) or (self.is_savegame() and not savegame): self.df.set_filename('') if self.df_ent is not None: self.df_ent.set_filename('') self._convert_savegame(savegame) return True else: raise Exception('No conversion to perform') def get_item_names(self): """ Returns a list of tuples which describe all the item names found on the map. Used at the moment to doublecheck item names once a map is converted from savegame to global. Elements of the tuple: 1) Tile X 2) Tile Y 3) Item Name """ retlist = [] for (y, row) in enumerate(self.tiles): for (x, tile) in enumerate(row): for tilecontent in tile.tilecontents: for item in tilecontent.items: if item.item_name != '': retlist.append((x, y, item.item_name)) return retlist def get_invalid_global_items(self): """ Returns a list of items on this map which do not appear to be valid global item names. Returns a list of tuples where each element contains the following: 1) Tile X 2) Tile Y 3) Item Name """ itemdict = c.eschalondata.get_itemdict() invalid_items = [] for itemtuple in self.get_item_names(): itemname = itemtuple[2] itemname_lower = itemname.lower() if (itemname_lower != 'empty' and itemname_lower != 'random' and itemname not in itemdict): invalid_items.append(itemtuple) return invalid_items @staticmethod def new(filename, book, map_df=None, ent_df=None): """ Sets up a new, blank Map object with the given book. Will raise a LoadException if we're passed a book we don't know about. Optionally pass in a datafile object to load our data from. """ if map_df is None: df = Savefile(filename) else: df = map_df if book == 1: c.switch_to_book(1) return B1Map(df, ent_df) elif book == 2: c.switch_to_book(2) return B2Map(df, ent_df) elif book == 3: c.switch_to_book(3) return B3Map(df, ent_df) else: raise LoadException('Unknown book version specified: %d' % book) @staticmethod def get_mapinfo(filename=None, map_df=None): """ Given a filename or a passed filehandle, loads the first few bits of information from a map file, and will return a tuple containing the Eschalon Book the map belongs to, the internal "map name" of the map, and a Savefile object pointing to the map. Will raise a LoadException if it encounters errors. Book 1 files start with 10 strings Book 2 files start with 9 strings, followed by a uchar whose value will always be 1 (the "loadhook" var, presumably) Book 3 files start with 12 strings, the first of which is a version, which so far is always 0.992. So, to figure out dynamically what kind of file we're loading: 1) Read 9 strings, remember the first one 2) Read the next uchar - if it's 1, then we're editing Book 2 3) If the first string is "0.992", then we're editing Book 3 4) Otherwise, we're editing Book 1 Theoretically, that way this works even if a Book 2 map happens to use a mapname of 0.992, in an effort to be cheeky. """ if filename is not None: df = Savefile(filename) elif map_df is not None: df = map_df else: raise LoadException('One of filename or map_df must be passed in') stringlist = [] try: df.open_r() for i in range(9): stringlist.append(df.readstr()) nextbyte = df.readuchar() df.close() except (IOError, struct.error) as e: raise LoadException(str(e)) if nextbyte == 1: detected_book = 2 detected_mapname = stringlist[0] # TODO: We're checking for a blank string here to cover up # for some invalid data that older versions of the unofficial # pre-1.0.0 builds. By the time 1.1.0 rolls around, or so, # we should get rid of that. elif stringlist[0] == '0.992' or stringlist[0] == '': detected_book = 3 detected_mapname = stringlist[1] else: detected_book = 1 detected_mapname = stringlist[1] return detected_book, detected_mapname, df @staticmethod def load(filename, req_book=None): """ Static method to load a map file. This will open the file once and read in a bit of data to determine which Eschalon game the mapfile comes from, and calls the appropriate constructor to return the object. If req_book is passed in, it will raise a LoadException if the detected Book number doesn't match. This will also raise a LoadException if it's unable to determine the version (generally due to being passed something that's not a map file). Note that this method does not actually read in the entire map file. It does "preload" the map name, however, so that it can be easily referenced in lists. Use .read() on the resulting map object to actually read in the map data. """ # Get some information about the filename (detected_book, detected_mapname, df) = Map.get_mapinfo(filename) # See if we're required to conform to a specific book if req_book is not None and detected_book != req_book: raise LoadException('This utility can only load Book %d map files; this file is from Book %d' % ( req_book, detected_book)) # Now actually return the object if detected_book == 1: c.switch_to_book(1) return B1Map(df) elif detected_book == 2: c.switch_to_book(2) return B2Map(df) elif detected_book == 3: c.switch_to_book(3) return B3Map(df) else: raise LoadException( 'Unknown book version found for "%s"; perhaps it is not an Eschalon map file' % filename) # Find directions from one coordinate set to another @staticmethod def directions_between_coords(x1, y1, x2, y2): if y1 % 2 == 0: map = dict(list(Map.DELTA_TO_DIRECTIONS.items()) + list(Map.DELTA_TO_DIRECTIONS_EVEN.items())) else: map = dict(list(Map.DELTA_TO_DIRECTIONS.items()) + list(Map.DELTA_TO_DIRECTIONS_ODD.items())) xdiff = x2 - x1 ydiff = y2 - y1 # Base case - adjacent tile if (xdiff, ydiff) in map: if map[(xdiff, ydiff)] == 0: return [] else: return [map[(xdiff, ydiff)]] # Not adjacent - recur # Looping through cardinal directions first would produce # shorter lists of directions, but it's probably not worth the # extra code complexity for coords in map: # Don't allow direction 0, DIR_NO_CHANGE if map[coords] == 0: continue # Does this direction get us closer? newx = x1 + coords[0] newy = y1 + coords[1] if abs(x2 - newx) <= abs(xdiff) and abs(y2 - newy) <= abs(ydiff): return [map[coords]] + Map.directions_between_coords(newx, newy, x2, y2) # Should never happen raise Exception("Couldn't find a direction from " + str(x1) + "," + str(y1) + " to " + str(x2) + "," + str(y2)) # Follow a set of directions from a coordinate set @staticmethod def follow_directions_from_coord(x, y, directions): for direction in directions: if direction in Map.DIRECTIONS_TO_DELTA: x += Map.DIRECTIONS_TO_DELTA[direction][0] y += Map.DIRECTIONS_TO_DELTA[direction][1] elif y % 2 == 0 and direction in Map.DIRECTIONS_TO_DELTA_EVEN: x += Map.DIRECTIONS_TO_DELTA_EVEN[direction][0] y += Map.DIRECTIONS_TO_DELTA_EVEN[direction][1] elif y % 2 == 1 and direction in Map.DIRECTIONS_TO_DELTA_ODD: x += Map.DIRECTIONS_TO_DELTA_ODD[direction][0] y += Map.DIRECTIONS_TO_DELTA_ODD[direction][1] else: raise Exception("Unknown direction " + hex(direction)) return x, y
import zlib from eschalon.savefile import Savefile, LoadException from struct import unpack class PakIndex(object): """ A class to hold information on an individual file in the pak. """ def __init__(self, data): (self.size_compressed, self.abs_index, self.size_real, self.unknowni1) = unpack('<IIII', data[:16]) self.filename = data[16:] self.filename = self.filename[:self.filename.index("\x00")] df = Savefile(sys.argv[1]) df.open_r() header = df.read(4) if (header != '!PAK'): df.close() raise LoadException('Invalid PAK header') # Initial Values unknownh1 = df.readshort() unknownh2 = df.readshort() numfiles = df.readint() compressed_idx_size = df.readint() unknowni1 = df.readint() # Now load in the index
def load_charname(self, book=None): """ Read our character name; this is a bit dependant on the book number, which is why we pass it in here. If the book number is not passed in, we will scan for map files and then use the first map's book number. If there are no maps in the slot, then we'll raise a LoadException """ # First figure out which book number we are, if we don't have it yet if not book: if not self.maps_loaded: self.load_maps() if len(self.maps) > 0: book = self.maps[0].book else: raise LoadException( 'Could not auto-detect which book version to use for charname' ) # Now do the actual loading if not os.path.exists(self.char_loc): raise LoadException('"char" file not found in %s' % (directory)) df = Savefile(self.char_loc) df.open_r() if book == 1: df.readint() else: df.readuchar() self.charname = df.readstr() self.char_loaded = True df.close()
class B1Gfx(Gfx): """ Grphics structure for Book 1 """ book = 1 wall_types = {} wall_gfx_group = {} tilebuf_mult = 1 item_dim = 42 item_cols = 10 item_rows = 24 tile_width = 52 tile_height = 26 floor_cols = 6 floor_rows = 32 decal_cols = 6 decal_rows = 32 obj_a_width = 52 obj_a_height = 52 obj_a_cols = 6 obj_a_rows = 16 obj_a_offset = 1 obj_b_width = 52 obj_b_height = 78 obj_b_cols = 6 obj_b_rows = 10 obj_b_offset = 101 obj_c_width = 52 obj_c_height = 78 obj_c_cols = 6 obj_c_rows = 10 obj_c_offset = 161 obj_d_width = 52 obj_d_height = 130 obj_d_cols = 5 obj_d_rows = 1 obj_d_offset = 251 walldecal_cols = 6 walldecal_rows = 10 GFX_SET_A = 1 GFX_SET_B = 2 GFX_SET_C = 3 GFX_SET_TREE = 4 def __init__(self, datadir, eschalondata): # Wall graphic groupings for i in range(101): self.wall_gfx_group[i] = self.GFX_SET_A for i in range(101, 161): self.wall_gfx_group[i] = self.GFX_SET_B for i in range(161, 251): self.wall_gfx_group[i] = self.GFX_SET_C for i in range(251, 256): self.wall_gfx_group[i] = self.GFX_SET_TREE # Wall object types for i in (range(127) + range(132, 142) + range(143, 153) + range(154, 161) + range(214, 251)): self.wall_types[i] = self.TYPE_OBJ for i in range(161, 214): self.wall_types[i] = self.TYPE_WALL for i in (range(251, 256) + range(127, 132) + [142, 153]): self.wall_types[i] = self.TYPE_TREE # Restricted entities (only one direction) self.restrict_ents = [50, 55, 58, 66, 67, 71] # Book 1 specific vars (just the PAK structure stuff) self.unknownh1 = -1 self.unknownh2 = -1 self.numfiles = -1 self.compressed_idx_size = -1 self.unknowni1 = -1 self.fileindex = {} self.zeroindex = -1 # Book 1 specific caches self.itemcache = None # Graphics PAK file self.pakloc = os.path.join(eschalondata.gamedir, 'gfx.pak') if os.path.isfile(self.pakloc): self.df = Savefile(self.pakloc) else: self.df = None # Set our loaded status self.loaded = False # Finally call the parent constructor super(B1Gfx, self).__init__(datadir, eschalondata) def readfile(self, filename): """ Reads a given filename out of the PAK. """ if self.loaded: filepath = os.path.join(self.eschalondata.gamedir, 'packedgraphics', filename) if os.path.isfile(filepath): return open(filepath, 'rb').read() if (filename in self.fileindex): self.df.open_r() self.df.seek(self.zeroindex + self.fileindex[filename].abs_index) # On Windows, we need to specify bufsize or memory gets clobbered filedata = zlib.decompress( self.df.read(self.fileindex[filename].size_compressed), 15, self.fileindex[filename].size_real) self.df.close() return filedata else: raise LoadException('Filename %s not found in archive' % (filename)) else: raise LoadException('PAK Index has not been loaded') def initialread(self): """ Read in the main file index. """ df = self.df if df is None: self.loaded = True return df.open_r() header = df.read(4) if (header != '!PAK'): df.close() raise LoadException('Invalid PAK header') # Initial Values self.unknownh1 = df.readshort() self.unknownh2 = df.readshort() self.numfiles = df.readint() self.compressed_idx_size = df.readint() self.unknowni1 = df.readint() # Now load in the index decobj = zlib.decompressobj() indexdata = decobj.decompress(df.read()) self.zeroindex = df.tell() - len(decobj.unused_data) decobj = None for i in range(self.numfiles): index = PakIndex(indexdata[:272]) indexdata = indexdata[272:] self.fileindex[index.filename] = index # Close and clean up df.close() self.loaded = True def get_item(self, item, size=None, gdk=True): if (self.itemcache is None): self.itemcache = GfxCache(self.readfile('items_mastersheet.png'), 42, 42, 10) return self.itemcache.getimg(item.pictureid+1, size, gdk) def get_floor(self, floornum, size=None, gdk=False): if (floornum == 0): return None if (self.floorcache is None): self.floorcache = GfxCache(self.readfile('iso_tileset_base.png'), 52, 26, 6) return self.floorcache.getimg(floornum, size, gdk) def get_decal(self, decalnum, size=None, gdk=False): if (decalnum == 0): return None if (self.decalcache is None): self.decalcache = GfxCache(self.readfile('iso_tileset_base_decals.png'), 52, 26, 6) return self.decalcache.getimg(decalnum, size, gdk) # Returns a tuple, first item is the surface, second is the extra height to add while drawing def get_object(self, objnum, size=None, gdk=False, treeset=0): """ Note that we ignore the treeset flag in book 1 """ if (objnum == 0): return (None, 0, 0) try: gfxgroup = self.wall_gfx_group[objnum] except KeyError: return (None, 0, 0) if gfxgroup == self.GFX_SET_A: if (self.objcache1 is None): self.objcache1 = GfxCache(self.readfile('iso_tileset_obj_a.png'), 52, 52, 6) return (self.objcache1.getimg(objnum, size, gdk), 1, 0) elif gfxgroup == self.GFX_SET_B: if (self.objcache2 is None): self.objcache2 = GfxCache(self.readfile('iso_tileset_obj_b.png'), 52, 78, 6) return (self.objcache2.getimg(objnum-100, size, gdk), 2, 0) elif gfxgroup == self.GFX_SET_C: if (self.objcache3 is None): self.objcache3 = GfxCache(self.readfile('iso_tileset_obj_c.png'), 52, 78, 6) return (self.objcache3.getimg(objnum-160, size, gdk), 2, 0) else: if (self.objcache4 is None): self.objcache4 = GfxCache(self.readfile('iso_trees.png'), 52, 130, 5) if (objnum in self.treemap): return (self.objcache4.getimg(self.treemap[objnum], size, gdk), 4, 0) else: return (None, 4, 0) def get_object_decal(self, decalnum, size=None, gdk=False): if (decalnum == 0): return None if (self.objdecalcache is None): self.objdecalcache = GfxCache(self.readfile('iso_tileset_obj_decals.png'), 52, 78, 6) return self.objdecalcache.getimg(decalnum, size, gdk) def get_flame(self, size=None, gdk=False): """ Grabs the flame graphic, so it's clear when viewing maps. I provide my own image here instead of using the game's because the file bundled with the game doesn't have transparency information, and I don't feel like doing a conversion. """ if (self.flamecache is None): df = open(os.path.join(self.datadir, 'torch_single.png'), 'rb') flamedata = df.read() df.close() self.flamecache = B1GfxEntCache(flamedata, 1, 1) if (size is None): size = self.tile_width return self.flamecache.getimg(1, int(size*self.flamecache.size_scale), gdk) def get_entity(self, entnum, direction, size=None, gdk=False): entity = self.eschalondata.get_entity(entnum) if not entity: return None entnum = entity.gfxfile if (entnum not in self.entcache): filename = 'mo%d.png' % (entnum) if (entnum in self.restrict_ents): self.entcache[entnum] = B1GfxEntCache(self.readfile(filename), 2, 1) else: self.entcache[entnum] = B1GfxEntCache(self.readfile(filename)) cache = self.entcache[entnum] if (size is None): size = self.tile_width return cache.getimg(direction, int(size*cache.size_scale), gdk) def get_avatar(self, avatarnum): if (avatarnum < 0 or avatarnum > 7): return None if (avatarnum not in self.avatarcache): if (avatarnum == 7): if (os.path.exists(os.path.join(self.eschalondata.gamedir, 'mypic.png'))): self.avatarcache[avatarnum] = gtk.gdk.pixbuf_new_from_file(os.path.join(self.eschalondata.gamedir, 'mypic.png')) else: return None else: self.avatarcache[avatarnum] = GfxCache(self.readfile('%d.png' % (avatarnum)), 60, 60, 1).pixbuf return self.avatarcache[avatarnum]
def replicate(self): newchar = Character.load(self.df.filename, self.book) if self.book == 1: newchar = B1Character(Savefile(self.df.filename)) elif self.book == 2: newchar = B2Character(Savefile(self.df.filename)) elif self.book == 3: newchar = B3Character(Savefile(self.df.filename)) # Single vals (no need to do actual replication) #newchar.book = self.book newchar.inv_rows = self.inv_rows newchar.inv_cols = self.inv_cols newchar.name = self.name newchar.strength = self.strength newchar.dexterity = self.dexterity newchar.endurance = self.endurance newchar.speed = self.speed newchar.intelligence = self.intelligence newchar.wisdom = self.wisdom newchar.perception = self.perception newchar.concentration = self.concentration newchar.maxhp = self.maxhp newchar.maxmana = self.maxmana newchar.curhp = self.curhp newchar.curmana = self.curmana newchar.experience = self.experience newchar.level = self.level newchar.gold = self.gold newchar.torches = self.torches newchar.torchused = self.torchused newchar.curinvcol = self.curinvcol newchar.curinvrow = self.curinvrow newchar.orientation = self.orientation newchar.xpos = self.xpos newchar.ypos = self.ypos newchar.picid = self.picid newchar.extra_att_points = self.extra_att_points newchar.extra_skill_points = self.extra_skill_points # Lists that need copying for val in self.spells: newchar.spells.append(val) for val in self.fxblock: newchar.fxblock.append(val) for val in self.statuses: newchar.statuses.append(val) # More complex lists that need copying for val in self.readyslots: newchar.readyslots.append([val[0], val[1]]) # Dicts that need copying for key, val in self.skills.iteritems(): newchar.skills[key] = val # Objects that need copying for i in range(self.inv_rows): for j in range(self.inv_cols): newchar.inventory[i][j] = self.inventory[i][j].replicate() for i in range(self.ready_rows * self.ready_cols): newchar.readyitems[i] = self.readyitems[i].replicate() newchar.quiver = self.quiver.replicate() newchar.helm = self.helm.replicate() newchar.cloak = self.cloak.replicate() newchar.amulet = self.amulet.replicate() newchar.torso = self.torso.replicate() newchar.weap_prim = self.weap_prim.replicate() newchar.belt = self.belt.replicate() newchar.gauntlet = self.gauntlet.replicate() newchar.legs = self.legs.replicate() newchar.ring1 = self.ring1.replicate() newchar.ring2 = self.ring2.replicate() newchar.shield = self.shield.replicate() newchar.feet = self.feet.replicate() # Call out to the subclass replication function self._sub_replicate(newchar) # Now return our duplicated object return newchar