import os
import struct
import StringIO
import binfile
TAG_MAJOR_VERSION = 5
TAG_MINOR_VERSION = 4
NUM_MONSTER_SPELL_SLOTS = 6
NUM_MONSTER_SLOTS = 10
MONS_PLAYER_GHOST = 400
MONS_PANDEMONIUM_DEMON = 401
class enum(object):
def __init__(self, values):
self.s2i = {}
self.i2s = {}
cur = 0
for val in values.split():
if '=' in val:
val,cur = val.split('=')
cur = int(cur)
self.s2i[val] = cur
self.i2s[cur] = val
cur += 1
def s(self, i): return self.i2s.get(i, str(i))
TAGS = """
TAG_NO_TAG
TAG_YOU
TAG_YOU_ITEMS
TAG_YOU_DUNGEON
TAG_LEVEL
TAG_LEVEL_ITEMS
TAG_LEVEL_MONSTERS
TAG_GHOST
TAG_LEVEL_ATTITUDE
TAG_LOST_MONSTERS
TAG_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 SCROLLS
JEWELLERY POTIONS UNKNOWN_II BOOKS STAVES ORBS MISC CORPSE GOLD GEM
UNASSIGNED=100 RANDOM=255""")
weapon_type = enum("""club mace flail dagger morningstar sling=13 bow crossbow handxbow
blowgun=42 longbow=45""")
missile_type = enum("""MI_STONE MI_ARROW MI_BOLT MI_DART MI_NEEDLE MI_LARGE_ROCK
MI_SLING_BULLET MI_JAVELIN MI_THROWING_NET""")
ammo_t = enum("THROW BOW SLING CROSSBOW HANDXBOW BLOWGUN")
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)==str
if 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)
def CrawlValue(f):
type_, flags = f.stream('BB')
if type_ == 1: return bool(f.stream1('B')) elif type_ == 2: return f.stream1('B') elif type_ == 3: return f.stream1('H') elif type_ == 4: return f.stream1('I') elif type_ == 5: return f.stream1('f') elif type_ == 6: return f.streamString2('f') elif type_ == 7: return f.stream('HH') elif type_ == 8: return Item(f) elif type_ == 9: return CrawlHashTable(f) elif type_ == 10: return CrawlVector(f)
class CrawlHashTable(object):
def __init__(self, f):
self.size = f.stream1('B')
if self.size == 0: return
self.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: return
self.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_type
return ret
KC_NCATEGORIES = 3
class 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_weapon
for 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, minor):
if minor >= 4:
expected_size = f.stream1('I')
num_read = -f.file.tell()
self.mark_type = mark_type = f.stream1('H')
if mark_type == 0: self.read_base(f)
self.feat = f.stream1('H')
elif mark_type == 1: self.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: self.read_base(f)
self.duration, self.radius = f.stream('HH')
elif mark_type == 3: self.read_base(f)
self.props = [ (f.streamString2(), f.streamString2())
for x in xrange(f.stream1('H')) ]
else:
assert "Unknown map marker type %d" % mark_type
num_read += f.file.tell()
if minor >= 4:
assert num_read == expected_size
def read_base(self, f):
self.coord = Coord(f)
class TaggedFile(object):
def __init__(self, fn):
f = binfile.reader(fn)
f.byteorder = '>'
self.f = f
self.major, self.minor = f.stream('bb')
print ' version %d.%d' % (self.major, self.minor)
if (self.major != TAG_MAJOR_VERSION or
self.minor > TAG_MINOR_VERSION):
print " WARNING: Cannot handle this version!"
self.tags = dict( self._gen_tags() )
def _gen_tags(self):
while True:
try:
tag_id, size = self.f.stream('HI')
except struct.error:
return
data = self.f.file.read(size)
tag_name = TAGS_NAMES[tag_id]
try: constructor = TAG_TO_CLASS[tag_name]
except KeyError:
print " Skipping %s" % tag_name
else:
print " Parsing %s" % tag_name
sub_reader = binfile.reader(StringIO.StringIO(data))
sub_reader.byteorder = '>'
data = constructor(sub_reader, self.minor)
yield (tag_id, data)
class TagBase(object): pass
class 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'))]
self.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))
self.stair_level = stream_map(f, 'I', 'BIB')
self.shops_present = stream_map(f, 'IIBIB', 'I')
self.altars_present = stream_map(f, 'IIBIB', 'I')
self.portals_present = stream_map(f, 'IIBIB', 'I')
self.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') self.data2 = f.stream('8B') self.level_type_name = f.streamString2()
self.data3 = f.stream('5B') self.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" % cur
class 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 -= run
assert expected >= 0
for i in xrange(run):
yield value
class 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.gy
self.grid_colours = list(gen_run_length_decode(f, 'B', expected))
self.cloud_no = f.stream1('H')
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')
if minor >= 4:
cooky = f.stream1('I')
assert cooky == 0x17742C32
self.markers = [ MapMarker(f, minor) 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,
}
if __name__ == '__main__':
testfile = '/Users/pld/src/crawl-ref/play/saves/Hunter-501.sav'
x = TaggedFile(testfile)
q = x.tags[TAGS_NUMS['TAG_YOU']].quiver
q.dump()