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_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 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 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 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()
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 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 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]
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 decobj = zlib.decompressobj() indexdata = decobj.decompress(df.read())
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]