git-svn-id: https://crawl-ref.svn.sourceforge.net/svnroot/crawl-ref/trunk@4363 c06c8d41-db1a-0410-9941-cceddc491573
3II6B6W4PVF3OD6LIZVK4HH4NOS3VDS52XYB52SUJWAMATF6ZIBAC #!/usr/local/bin/python## Tool for examining saved games.import osimport sysimport optparsetry: import crawl.tagsexcept ImportError:print "You need to put crawl-ref/source/python in your PYTHONPATH; I'll try."try: sys.path.append(os.path.join(os.path.dirname(sys.argv[0]), "../python"))except: passimport crawl.tagsdef process_zip(opts, fn):"""Process one or more sub-files from a .zip saved game."""if fn.lower().endswith('zip'):import zipfilefrom cStringIO import StringIOzip = zipfile.ZipFile(fn, 'r')for n in zip.namelist():# Ignore non-tag files for nowif ( n.endswith('.kil') or n.endswith('.nts') or n.endswith('.tc')or n.endswith('.tut') or n.endswith('.st')):#print "Skipping %s" % ncontinueprint "Reading %s" % ntry:save = crawl.tags.TaggedFile(StringIO(zip.read(n)))except Exception, e:print " Failed (not a tag file?)"continueprocess_file(opts, save)def process_file(opts, save):"""Process a file; a portion of a saved game.Pass a TaggedFile instance"""# Not much going on here yetTAG_YOU = crawl.tags.tags_enum.s2i['TAG_YOU']if TAG_YOU in save.tags:print "Dumping YOU's quiver:"you = save.tags[TAG_YOU]you.quiver.dump()def main(args):parser = optparse.OptionParser()parser.add_option("-p", dest='player', help='If .zip, process the player (.sav) portion',action='store_true')opts, args = parser.parse_args(args)for fn in args:if fn.lower().endswith('zip'):process_zip(opts, fn)else:process_file(opts, crawl.tags.TaggedFile(fn))if __name__=='__main__':main(sys.argv[1:])
#!/usr/local/bin/python## Module for reading and examining saved games.#import osimport structimport StringIOimport binfile# ----------------------------------------------------------------------# Some constants and things# ----------------------------------------------------------------------NUM_MONSTER_SPELL_SLOTS = 6NUM_MONSTER_SLOTS = 10MONS_PLAYER_GHOST = 400MONS_PANDEMONIUM_DEMON = 401class enum(object):def __init__(self, values):self.s2i = {}self.i2s = {}cur = 0for val in values.split():if '=' in val:val,cur = val.split('=')cur = int(cur)self.s2i[val] = curself.i2s[cur] = valcur += 1def s(self, i): return self.i2s.get(i, str(i))TAGS = """TAG_NO_TAGTAG_YOUTAG_YOU_ITEMSTAG_YOU_DUNGEONTAG_LEVELTAG_LEVEL_ITEMSTAG_LEVEL_MONSTERSTAG_GHOSTTAG_LEVEL_ATTITUDETAG_LOST_MONSTERSTAG_LEVEL_TILES"""TAGS_NAMES = dict( enumerate(TAGS.split()) )TAGS_NUMS = dict( (b,a) for (a,b) in enumerate(TAGS.split()) )tags_enum = enum(TAGS)object_class_type = enum("""WEAPONS MISSILES ARMOUR WANDS FOOD UNKNOWN_I SCROLLSJEWELLERY POTIONS UNKNOWN_II BOOKS STAVES ORBS MISC CORPSE GOLD GEMUNASSIGNED=100 RANDOM=255""")weapon_type = enum("""club mace flail dagger morningstar sling=13 bow crossbow handxbowblowgun=42 longbow=45""")missile_type = enum("""MI_STONE MI_ARROW MI_BOLT MI_DART MI_NEEDLE MI_LARGE_ROCKMI_SLING_BULLET MI_JAVELIN MI_THROWING_NET""")ammo_t = enum("THROW BOW SLING CROSSBOW HANDXBOW BLOWGUN")# ----------------------------------------------------------------------# unmarshall helpers# ----------------------------------------------------------------------def stream_container(f, reader):return [ reader(f) for x in xrange(f.stream1('I')) ]def stream_array(f, count_type='B', item='B', limit=None):num = f.stream1(count_type)if limit is not None and num > limit:print "Warning: big array! (%d > %d)" % (num, limit)if len(item) == 1:return f.stream('%d%s' % (num, item))else:return [ f.stream(item) for x in xrange(num) ]def stream_map(f, key_type, value_type):length = f.stream1('I')assert type(key_type)==strif len(key_type) == 1:def key_reader(): return f.stream1(key_type)else:def key_reader(): return f.stream(key_type)if type(value_type) == str:if len(value_type) == 1:def value_reader(): return f.stream1(value_type)else:def value_reader(): return f.stream(value_type)else:def value_reader(): return value_type()return dict( (key_reader(), value_reader())for i in xrange(length) )def assert_end(f):cur = f.tell()f.seek(0, os.SEEK_END)end = f.tell()if cur != end:print " !! cur %d != end %d" % (cur,end)# ----------------------------------------------------------------------# Misc sub data structures# ----------------------------------------------------------------------def CrawlValue(f):type_, flags = f.stream('BB')if type_ == 1: return bool(f.stream1('B')) # BOOLelif type_ == 2: return f.stream1('B') # BYTEelif type_ == 3: return f.stream1('H') # SHORTelif type_ == 4: return f.stream1('I') # LONGelif type_ == 5: return f.stream1('f') # FLOATelif type_ == 6: return f.streamString2('f') # STRINGelif type_ == 7: return f.stream('HH') # COORDelif type_ == 8: return Item(f) # ITEMelif type_ == 9: return CrawlHashTable(f) # HASHelif type_ == 10: return CrawlVector(f) # VECclass CrawlHashTable(object):def __init__(self, f):self.size = f.stream1('B')if self.size == 0: returnself.typeflags = f.stream('BB')contents = [ (f.streamString2(), CrawlValue(f)) for x in xrange(self.size) ]class CrawlVector(object):def __init__(self, f):self.size = f.stream1('B')if self.size == 0: returnself.max, self.type, self.flags = f.stream('BBB')contents = [ CrawlValue(f) for x in xrange(self.size) ]class Item(object):def __init__(self, f):self.base_type, self.sub_type = f.stream('BB')self.plusses = f.stream('HH')self.special, self.quantity = f.stream('IH')self.data2 = f.stream('BHHI')self.unused = f.stream('HH')self.slot = f.stream1('B')self.origs = f.stream('HH')self.inscrip = f.streamString2()self.props = CrawlHashTable(f)def __str__(self):ret = "%s" % object_class_type.s(self.base_type)if ret == 'WEAPONS':ret += '/%s' % weapon_type.s(self.sub_type)elif ret == 'MISSILES':ret += '/%s' % missile_type.s(self.sub_type)else:ret += '/%d' % self.sub_typereturn retKC_NCATEGORIES = 3class PlaceInfo(object):def __init__(self, f):self.data1 = f.stream('IIII')self.mon_kill_exp = f.stream('II')self.mon_kill_num = f.stream('%dI' % KC_NCATEGORIES)self.data2 = f.stream('6I')self.data3 = f.stream('6f')def mon_spells(f): return f.stream('%dH' % NUM_MONSTER_SPELL_SLOTS)def mon_resist(f): return f.stream('12B')class Ghost(object):def __init__(self, f):self.name = f.streamString2()self.data1 = f.stream('10HBH')self.resist = mon_resist(f)self.data2 = f.stream('BBH')self.spells = mon_spells(f)class Monster(object):def __init__(self, f):def mon_enchant(f): return f.stream('5H')self.data1 = f.stream('6B')self.pos = f.stream('BB')self.targ = f.stream('BB')self.data2 = f.stream('II')self.ench = [ mon_enchant(f) for x in xrange(f.stream1('H')) ]self.ench_count = f.stream1('B')self.type = f.stream1('H')self.data3 = f.stream('HHHH')self.inv = f.stream('%dH' % NUM_MONSTER_SLOTS)self.spells = spells(f)self.god = f.stream1('B')if self.type in (MONS_PLAYER_GHOST, MONS_PANDEMONIUM_DEMON):self.ghost = Ghost(f)class Quiver(object):def __init__(self, f):self.cooky = f.stream1('H')self.last_weapon = Item(f)self.last_used_type = f.stream1('I')num_last_used = f.stream1('I')self.last_used_of_type = [ Item(f) for i in range(num_last_used) ]def dump(self):print 'last weapon:', self.last_weaponfor i,item in enumerate(self.last_used_of_type):print ' %s: %d %s' % (ammo_t.s(i), item.quantity, item)def Coord(f): return f.stream('HH')class MapMarker(object):def __init__(self, f):self.mark_type = mark_type = f.stream1('H')if mark_type == 0: # map_feature_markerself.read_base(f)self.feat = f.stream1('H')elif mark_type == 1: # map_lua_markerself.read_base(f)self.initialized = f.stream1('B')if self.initialized:chunk = f.streamString2()assert False, "Don't know how much else to read"elif mark_type == 2: # map_corruption_markerself.read_base(f)self.duration, self.radius = f.stream('HH')elif mark_type == 3: # map_wiz_props_markerself.read_base(f)self.props = [ (f.streamString2(), f.streamString2())for x in xrange(f.stream1('H')) ]else:assert "Unknown map marker type %d" % mark_typedef read_base(self, f):self.coord = Coord(f)# ----------------------------------------------------------------------# Generic tagged file# ----------------------------------------------------------------------class TaggedFile(object):def __init__(self, fn):f = binfile.reader(fn)f.byteorder = '>'self.f = fself.major, self.minor = f.stream('bb')self.tags = dict( self._gen_tags() )def _gen_tags(self):while True:try:tag_id, size = self.f.stream('HI')except struct.error:returndata = self.f.file.read(size)tag_name = TAGS_NAMES[tag_id]try: constructor = TAG_TO_CLASS[tag_name]except KeyError:print " Found %s (currently unsupported)" % tag_nameelse:print " Found %s (parsing)" % tag_namesub_reader = binfile.reader(StringIO.StringIO(data))sub_reader.byteorder = '>'data = constructor(sub_reader, self.minor)yield (tag_id, data)class TagBase(object): passclass TagLEVEL_MONSTERS(TagBase):def __init__(self, f, minor):self.mons_alloc = stream_array(f, 'B', 'H')def TagGHOST(f, minor):return [ Ghost(f) for x in xrange(f.stream1('H')) ]class TagLOST_MONSTERS(TagBase):def __init__(self, f, minor):def follower():return (Monster(f), [Item(f) for x in xrange(NUM_MONSTER_SLOTS)])def follower_list():return [follower() for x in xrange(f.stream1('H'))]def item_list():return [Item(f) for x in xrange(f.stream1('H'))]# level_id -> follower_listself.lost_monst = stream_map(f, 'BIB', follower_list)self.lost_item = stream_map(f, 'BIB', item_list)assert_end(f.file)class TagYOU_DUNGEON(TagBase):def __init__(self, f, minor):self.ucreatures = stream_array(f, 'H', 'B')self.c_things = stream_array(f, 'B', 'II')self.s_things = stream_array(f, 'H', '%dB' % len(self.c_things))# branch_type -> level_id(branchtype, depth, leveltype)self.stair_level = stream_map(f, 'I', 'BIB')# level_pos -> shop_typeself.shops_present = stream_map(f, 'IIBIB', 'I')# level_pos -> god_typeself.altars_present = stream_map(f, 'IIBIB', 'I')# level_pos -> portal_typeself.portals_present = stream_map(f, 'IIBIB', 'I')# level_id -> stringself.level_annotations = stream_map(f, 'BIB', f.streamString2)self.place_info = PlaceInfo(f)self.more_place_info = [PlaceInfo(f) for x in xrange(f.stream1('H'))]self.uniq_map_tags = stream_container(f, f.__class__.streamString2)self.uniq_map_names = stream_container(f, f.__class__.streamString2)assert_end(f.file)class TagYOU_ITEMS(TagBase):def __init__(self, f, minor):ninv = f.stream1('B')self.inv = [Item(f) for x in range(ninv)]ntype, nsubtype = f.stream('BB')self.item_desc = [ f.stream('%dB' % nsubtype) for x in range(ntype) ]ident_w, ident_h = f.stream('BB')self.identified = [ f.stream('%dB' % ident_h) for x in range(ident_w) ]self.uniques = stream_array(f)self.books = stream_array(f)self.unrandart = stream_array(f, 'H', 'B')assert_end(f.file)class TagYOU(TagBase):def __init__(self, f, minor):self.name = f.streamString2()self.data1 = f.stream('BBBBBH') # last is pet_targetself.data2 = f.stream('8B') # last is level_typeself.level_type_name = f.streamString2()self.data3 = f.stream('5B') # speciesself.hp, self.hunger = f.stream('HH')self.equip = stream_array(f)self.magic = f.stream('BB')self.stats = f.stream('BBB')self.regen = f.stream('BBH')self.xp, self.gold = f.stream('II')self.data4 = f.stream('BBI')self.max_stats = f.stream('BBB')self.hp_magic2 = f.stream('HHHH')self.pos = f.stream('HH')self.class_name = f.streamString2()self.burden = f.stream1('H')self.spells = stream_array(f)self.spell_letters = stream_array(f)self.abil_letters = stream_array(f, 'B', 'H')self.skills = stream_array(f, 'B', 'BBIB')self.durations = stream_array(f, 'B', 'I')self.attributes = stream_array(f)self.quiver_old = stream_array(f)self.sacrifice = stream_array(f, 'B', 'I')self.mutation = stream_array(f, 'H', 'BB')self.penance = stream_array(f)self.worshipped = stream_array(f)self.gifts = f.stream('%dH' % len(self.worshipped))self.data5 = f.stream('4B')self.elapsed_time = f.stream1('f')self.wizard = f.stream1('B')self.game_start = f.streamString2()self.real_time, self.num_turns = f.stream('II')self.data6 = f.stream('HHB')self.beheldby = f.stream('%dB' % f.stream1('B'))if minor >= 2:self.piety_hysteresis = f.stream1('B')if minor >= 3:self.quiver = Quiver(f)cur = f.file.tell()f.file.seek(0, os.SEEK_END)cur -= f.file.tell()assert cur == 0, "%d bytes off" % curclass TagLEVEL_ATTITUDE(TagBase):def __init__(self, f, minor):self.monsters = stream_array(f, 'H', 'BH')assert_end(f.file)class TagLEVEL_ITEMS(TagBase):def __init__(self, f, minor):self.traps = stream_array(f, 'H', 'BBB')num_items = f.stream1('H')self.items = [Item(f) for x in range(num_items)]assert_end(f.file)def gen_run_length_decode(f, value_type, expected):while expected > 0:run = f.stream1('B')value = f.stream1(value_type)expected -= runassert expected >= 0for i in xrange(run):yield valueclass TagLEVEL(TagBase):def __init__(self, f, minor):self.colours = f.stream('BB')self.level_flags = f.stream('I')self.time = f.stream('f')self.gx,self.gy = f.stream('HH')self.turns = f.stream('I')self.grid = [ f.stream('BHHHHH')for i in xrange(self.gx)for j in xrange(self.gy) ]expected = self.gx * self.gyself.grid_colours = list(gen_run_length_decode(f, 'B', expected))self.clouds = stream_array(f, 'H', 'BBBHBH', limit=1000)self.shops = stream_array(f, 'B', 'BBBBBBBB', limit=15)self.sanctuary = Coord(f)self.sanctuary_time = f.stream1('B')self.markers = [ MapMarker(f) for x in xrange(f.stream1('H')) ]assert_end(f.file)TAG_TO_CLASS = {'TAG_YOU' : TagYOU,'TAG_YOU_ITEMS': TagYOU_ITEMS,'TAG_YOU_DUNGEON': TagYOU_DUNGEON,'TAG_LEVEL': TagLEVEL,'TAG_LEVEL_ITEMS': TagLEVEL_ITEMS,'TAG_LEVEL_MONSTERS': TagLEVEL_MONSTERS,'TAG_GHOST': TagGHOST,'TAG_LEVEL_ATTITUDE': TagLEVEL_ATTITUDE,'TAG_LOST_MONSTERS': TagLOST_MONSTERS,# TAG_LEVEL_TILES}if __name__ == '__main__':testfile = '/Users/pld/src/crawl-ref/play/saves/Hunter-501.sav'x = TaggedFile(testfile)q = x.tags[TAGS_NUMS['TAG_YOU']].quiverq.dump()
import structclass reader(object):"""Wrapper around a read-only binary file"""def __init__(self, fileorname):"""fileorname may be a file-like object or a filename"""self.byteorder = '=' # can also be '<' or '>'if hasattr(fileorname, 'read'):self.file = fileornameelse:self.file = file(fileorname, 'rb')def read(self,len): return self.file.read(len)def stream(self, fmt, len=0):"""Unpack fmt from f and return results."""fmt = self.byteorder + fmt # don't enforce alignment!if len == 0: len = struct.calcsize(fmt)data = self.file.read(len)return struct.unpack(fmt, data)def stream1(self, fmt, len=0):"""Unpack fmt from f and return just one result."""(data,) = self.stream(fmt,len)return datadef _streamStringLow(self, len):if len <= 0: return ''str = self.file.read(len-1)char = self.file.read(1)if ord(char)==0: return strelse: return str+chardef streamCooky(self):l = list(self.stream("4c"))l.reverse()return ''.join(l)def streamString4(self):"""Stream string prefixed with 4 bytes of length"""(strlen,) = self.stream("I",4)if strlen >= 2048:print "Bad string len: %#x" % strlenassert strlen < 2048return self._streamStringLow(strlen)def streamString2(self):"""Stream string prefixed with 2 bytes of length"""(strlen,) = self.stream("H",2)assert strlen < 2048return self._streamStringLow(strlen)def streamString1(self):"""Stream string prefixed with 1 byte of length"""(strlen,) = self.stream("B",1)assert strlen < 2048return self._streamStringLow(strlen)class writer(object):"""Wrapper around a write-only binary file"""def __init__(self, fileorname):"""fileorname may be a file-like object or a filename"""if hasattr(fileorname, 'write'):self.file = fileornameelse:self.file = file(fileorname, 'wb')def write(self,data): return self.file.write(data)def stream(self, fmt, *args):fmt = '='+fmt # add the "no alignment" flag to the fmtself.file.write(struct.pack(fmt, *args))# for symmetry with readerdef stream1(self, fmt, *args):fmt = '='+fmt # add the "no alignment" flag to the fmtself.file.write(struct.pack(fmt, *args))def streamCooky(self, cooky):assert len(cooky)==4l = list(cooky)l.reverse()self.stream("4c", *l)def streamString1(self, str):"""Stream string prefixed with 1 byte of length"""#print "writing %s" % str# Game doesn't handle 0-length null stringsself.file.write(struct.pack("B",len(str)+1))self.file.write(str)self.file.write("\0")def streamString2(self, str):"""Stream string prefixed with 2 bytes of length"""#print "writing %s" % str# Game doesn't handle 0-length null stringsself.file.write(struct.pack("H",len(str)+1))self.file.write(str)self.file.write("\0")def streamString4(self, str):"""Stream string prefixed with 4 bytes of length"""if str=='':self.file.write(struct.pack("I",0))else:#print "writing %s" % strself.file.write(struct.pack("I",len(str)+1))self.file.write(str)self.file.write("\0")