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))