SimTower saved game
From Just Solve the File Format Problem
(Difference between revisions)
Cypherpunks (Talk | contribs) (→File Format: add image) |
Cypherpunks (Talk | contribs) m (→util.py: remove trailing newline) |
||
| Line 375: | Line 375: | ||
def ZStr(name, length, encoding=None): | def ZStr(name, length, encoding=None): | ||
fld = _cns.Field(name, length) | fld = _cns.Field(name, length) | ||
| − | return _ZStrAdapter(_cns.StringAdapter(fld, encoding=encoding)) | + | return _ZStrAdapter(_cns.StringAdapter(fld, encoding=encoding))</nowiki> |
| − | </nowiki> | + | |
Latest revision as of 03:03, 27 September 2016
Contents |
[edit] Links
[edit] 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.
[edit] 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)
>>>
[edit] 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
)
[edit] 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))