SimTower saved game
From Just Solve the File Format Problem
Revision as of 01:23, 27 September 2016 by Cypherpunks (Talk | contribs)
Contents |
Links
Code
The following code can parse the format; and even for those who don't know Python it should mostly be understandable as a format description.
Integers are named as (sign)(endian)Int(bits)
, like ULInt32. TDT uses little-endian. Exact size and signedness wasn't verified for most fields.
- This requires the Construct package for Python (tested with Construct 2.5.2 and Python 3.5.2).
- It's meant to handle binary-identical roundtripping even with bad values.
tdt_cons.py
# To the extent possible under law, the author(s) have dedicated all copyright # and related and neighboring rights to this software to the public domain # worldwide. This software is distributed without any warranty. # # See <http://creativecommons.org/publicdomain/zero/1.0/>. import construct as _cns import util as _util #### File header File_Header_Struct = _cns.Struct('file_header', _cns.Anchor('hdr_start_'), # to debug, change Anchor to Probe # SimTower doesn't seem to care about the first byte, but the second # must be 0x24. Larger values produce a "wrong version" warning. _cns.Padding(1), _cns.Const(_cns.ULInt8('version'), 0x24), _cns.ULInt8('stars'), # 1-5 stars, or 6 for "tower" _util.Unknown('_hdr3', 1), _cns.ULInt32('cash_balance'), # units of $100 _cns.ULInt32('other_income'), _cns.ULInt32('construction_costs'), _cns.ULInt32('last_quarter_cash'), _cns.ULInt16('frame_time'), _cns.ULInt32('day'), _util.Unknown('_hdr26', 2), _cns.ULInt8('lobby_height'), _util.Unknown('_hdr29', 9), _cns.ULInt16('viewport_x'), _cns.ULInt16('viewport_y'), _util.Unknown('_hdr42', 14), _cns.ULInt8('named_people_count'), _util.Unknown('_hdr57', 503), _cns.Anchor('hdr_end_'), _util.Assert(lambda ctx: ctx.hdr_end_ == ctx.hdr_start_ + 560), ) #### Tenants Tenant_Type = _util.NumToSym(_cns.ULInt8('type'), { 0:'floor', 3:'hotel_single', 4:'hotel_double', 5:'hotal_suite', 6:'restaurant', 7:'office', 9:'condo', 10:'shop', 11:'parking_space', 12:'fast_food', 13:'medical', 14:'security', 15:'housekeeping', 18:'movie_theater_floor2', 19:'movie_theater', 20:'recycling_floor2', 21:'recycling', 24:'lobby', 29:'hall_floor2', 30:'hall', 33:'metro', 34:'movie_screen_floor2', 35:'movie_screen', 36:'cathedral_floor5', 37:'cathedral_floor4', 38:'cathedral_floor3', 39:'cathedral_floor2', 40:'cathedral', 44:'parking_ramp', 45:'metro_tunnel', 48:'burned', }) Tenant_Struct = _cns.Struct('tenant', # Horizontal positions are given with half-open ranges [left,right) # in units of 8 pixels. _cns.ULInt16('left_edge'), _cns.ULInt16('right_edge'), _cns.Rename('type', Tenant_Type), _cns.ULInt8('status'), _cns.ULInt8('pertype_idx'), _util.Unknown('_unk7', 1), _cns.ULInt32('people_offset'), _cns.ULInt8('id'), _util.Unknown('_unk13', 3), _cns.ULInt8('rent_class'), _util.Unknown('_unk17', 1), ) assert(Tenant_Struct.sizeof() == 18) #### Floors def _floor_below_grade(raw_floor): return raw_floor < 10 def floor_id_from_raw(raw_floor): # Raw floor values {0..9} are basements: {-10..-1} or {B10..B1}. # Values {10..119} are floors {1..110}. if _floor_below_grade(raw_floor): return raw_floor-10 else: return raw_floor-9 def _floor_could_be_lobby(raw_floor): return floor_id_from_raw(raw_floor) in (1, 15, 30, 45, 60, 75, 90) Floor_Struct = _cns.Struct('floor', _cns.ULInt16('tenant_count'), _cns.ULInt16('left_edge'), _cns.ULInt16('right_edge'), _cns.Rename('tenants', _cns.Array(lambda ctx: ctx.tenant_count, Tenant_Struct)), _cns.Array(94, _cns.ULInt16('tenant_id_to_index')), ) #### People Person_Struct = _cns.Struct('person', _cns.ULInt8('tenant_floor'), _cns.ULInt8('tenant_index'), _cns.ULInt16('number_in_tenant'), _cns.Rename('tenant_type', Tenant_Type), _cns.ULInt8('status'), _cns.SLInt8('current_floor'), _util.Unknown('_unk6', 5), _cns.ULInt16('stress'), _cns.ULInt16('eval'), ) assert(Person_Struct.sizeof() == 16) #### Retail Retail_Struct = _cns.Struct('retail', _cns.ULInt8('floor'), _util.Unknown('_unk1', 10), _cns.ULInt8('type'), _util.Unknown('_unk12', 6), ) assert(Retail_Struct.sizeof() == 18) #### Stairs Stairs_Type = _util.NumToSym(_cns.ULInt8('type'), {0:'escalator', 1:'standard'}) Stairs_Struct = _cns.Struct('stairs', _util.Bool(_cns.ULInt8('present')), _cns.Rename('type', Stairs_Type), _cns.ULInt16('xpos'), _cns.ULInt8('bottom_floor'), _util.Unknown('_unk5', 5), ) assert(Stairs_Struct.sizeof() == 10) #### Elevators Elevator_Type = _util.NumToSym(_cns.ULInt8('type'), {0:'express', 1:'standard', 2:'service'}) def _potential_floors_served(ctx): if ctx.type == 'express': count = 0 for i in range(ctx.bottom_floor, ctx.top_floor + 1): if _floor_below_grade(i) or _floor_could_be_lobby(i): count += 1 else: count = 1 + (ctx.top_floor - ctx.bottom_floor) return count Elevator_Struct = _cns.Struct('elevator', _cns.Anchor('start_'), _util.Bool(_cns.ULInt8('present')), _cns.Rename('type', Elevator_Type), # Capacity > 42 may crash SimTower. # 42 seems to work for non-express elevators. _cns.ULInt8('capacity'), _cns.ULInt8('car_count'), # There are 4 "schedule" arrays of size 14: 7 weekday periods, followed # by 7 weekend periods. The GUI shows 6 periods per day; the 7th is # not used. The entire first array looks unused too. The regex # '\x01{14}......\x05......\x05' can reliably find elevator data. _cns.Array(14, _cns.ULInt8('#unknown_sched')), _cns.If(lambda ctx: ctx.present, _util.Assert(lambda ctx: ctx['#unknown_sched'] == [1]*14)), _cns.Array(14, _cns.ULInt8('response_distances')), _cns.Array(14, _cns.ULInt8('express_modes')), _cns.Array(14, _cns.ULInt8('departure_delays')), _util.Bool(_cns.ULInt8('shaft_visible')), _util.Unknown('_unk60', 1), _cns.ULInt16('left_edge'), _cns.ULInt8('top_floor'), _cns.ULInt8('bottom_floor'), _cns.Value('potential_floors_served_', _potential_floors_served), # For each of 120 floors, value is 1 if served, otherwise 0. # Adding express elevator stops on improper floors (above ground and # not a multiple of 15) may crash SimTower, probably because there's # an array of size potential_floors_served. _cns.Array(120, _cns.ULInt8('floor_stops')), # Resting floor for each car. _cns.Array(8, _cns.ULInt8('resting_floors')), _cns.Anchor('fixed_end_'), _util.Assert(lambda ctx: ctx.fixed_end_ == ctx.start_ + 194), _cns.If(lambda ctx: ctx.present, _util.AnonEmbed( _util.Unknown('_unk194', 3488), # Information about people waiting for elevators # is likely to be in the following array. _cns.Array(lambda ctx: ctx.potential_floors_served_, _util.Unknown('_unk3682', 324)), )), _cns.Anchor('end_'), ) #### Finances/population block Finances_Struct = _cns.Struct('finances', _cns.Array(10, _cns.ULInt32('tenant_populations')), _cns.ULInt32('tower_population'), _cns.Array(10, _cns.ULInt32('tenant_incomes')), _cns.ULInt32('tower_income'), _cns.Array(10, _cns.ULInt32('tenant_maintenance')), _cns.ULInt32('tower_maintenance'), ) #### Overall file structure Tower_Struct = _cns.Struct('tower', _cns.Embedded(File_Header_Struct), _cns.Rename('floors', _cns.Array(120, Floor_Struct)), _cns.ULInt32('people_count'), _cns.Rename('people', _cns.Array(lambda ctx: ctx.people_count, Person_Struct)), _cns.Rename('retail', _cns.Array(512, Retail_Struct)), _cns.Anchor('elevators_start_'), _cns.Rename('elevators', _cns.Array(24, Elevator_Struct)), _cns.Anchor('elevators_end_'), _util.Unknown('_unk_after_elev', 88), _cns.Anchor('finances_start_'), _cns.Embedded(Finances_Struct), _cns.Anchor('finances_end_'), _util.Unknown('_unk_after_finpop', 1102), _cns.Anchor('stairs_start_'), _cns.Rename('stairs', _cns.Array(64, Stairs_Struct)), _cns.Anchor('stairs_end_'), _util.Unknown('_unk_after_stairs', 17242), _cns.Rename('named_people', _cns.Array(lambda ctx: ctx.named_people_count, _util.ZStr(None, 16))), _cns.Terminator # must reach end of file )
util.py
# To the extent possible under law, the author(s) have dedicated all copyright # and related and neighboring rights to this software to the public domain # worldwide. This software is distributed without any warranty. # # See <http://creativecommons.org/publicdomain/zero/1.0/>. import construct as _cns #### Utility functions Unknown = _cns.Field class Assert(_cns.Construct): def __init__(self, fn): _cns.Construct.__init__(self, None) self.fn = fn def _sizeof(self, context): return 0 def _assert(self, stream, context): assert self.fn(context) def _parse(self, stream, context): self._assert(stream, context) def _build(self, obj, stream, context): self._assert(stream, context) def AnonEmbed(*subcons): return _cns.Embedded(_cns.Struct(None, *subcons)) def NumToSym(subcon, decoding): return _cns.MappingAdapter(subcon, decoding = decoding, encoding = dict((v,k) for (k,v) in decoding.items()), decdefault = _cns.Pass, encdefault = _cns.Pass) def Bool(subcon): return NumToSym(subcon, {0:False, 1:True}) class _PaddedBytes(bytes): def __new__(cls, raw=b'', padding=None): b = super(_PaddedBytes, cls).__new__(cls, raw) b.padding = padding return b class _ZStrAdapter(_cns.StringAdapter): def __init__(self, subcon): _cns.StringAdapter.__init__(self, subcon) def _decode(self, obj, context): obj = _PaddedBytes(*obj.split(b'\0', 1)) return _cns.StringAdapter._decode(self, obj, context) def _encode(self, obj, context): out = _cns.StringAdapter._encode(self, obj, context) out += b'\0' try: if obj.padding is not None: out += obj.padding except AttributeError: pass pad = self._sizeof(context) - len(out) if pad < 0: raise ValueError("string too long") out += b'\0' * pad return out def ZStr(name, length, encoding=None): fld = _cns.Field(name, length) return _ZStrAdapter(_cns.StringAdapter(fld, encoding=encoding))