SimTower saved game
From Just Solve the File Format Problem
(Difference between revisions)
Cypherpunks (Talk | contribs) m (→tdt_cons.py: fix comment) |
Cypherpunks (Talk | contribs) (→File Format: add image) |
||
Line 3: | Line 3: | ||
|subcat=Saved Games | |subcat=Saved Games | ||
|extensions={{ext|tdt}} | |extensions={{ext|tdt}} | ||
+ | |image=Simtower.png | ||
}} | }} | ||
Revision as of 03:01, 27 September 2016
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.
Usage example
If both files below are installed to the current directory, you can run the following at the command line to make all elevators carry 42 people:
$ python3 >>> import tdt_cons >>> f = open('SIMTOWER/TDT/TOWER.TDT','rb') >>> tower = tdt_cons.Tower_Struct.parse_stream(f) >>> for el in tower.elevators: el.capacity = 42 ... >>> f = open('SIMTOWER/TDT/TOWEROUT.TDT','wb') >>> tdt_cons.Tower_Struct.build_stream(tower, f) >>>
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', # Anchor records the current file position, for sanity checks (Assert). # Put _cns.Probe() between fields to dump data for debugging. _cns.Anchor('hdr_start_'), # 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))