SimTower saved game
From Just Solve the File Format Problem
(Difference between revisions)
Cypherpunks (Talk | contribs) (link to OpenSkyscraper document) |
Cypherpunks (Talk | contribs) (add some code to parse the format (TODO: add usage notes)) |
||
Line 9: | Line 9: | ||
* [https://raw.githubusercontent.com/fabianschuiki/OpenSkyscraper/90e6e92a2d46869a4052332e48633dd1545256e4/doc/simtower/TDT_format.txt Format description from the OpenSkyscraper project] | * [https://raw.githubusercontent.com/fabianschuiki/OpenSkyscraper/90e6e92a2d46869a4052332e48633dd1545256e4/doc/simtower/TDT_format.txt Format description from the OpenSkyscraper project] | ||
* [[Wikipedia:SimTower|Wikipedia article on SimTower]] | * [[Wikipedia:SimTower|Wikipedia article on SimTower]] | ||
+ | |||
+ | == Code == | ||
+ | <br clear="all"> | ||
+ | |||
+ | === tdt_cons.py === | ||
+ | |||
+ | <nowiki># 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 | ||
+ | )</nowiki> | ||
+ | |||
+ | === util.py === | ||
+ | |||
+ | <nowiki># 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)) | ||
+ | </nowiki> |
Revision as of 01:14, 27 September 2016
Contents |
Links
Code
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))