SimTower saved game

From Just Solve the File Format Problem
(Difference between revisions)
Jump to: navigation, search
(Usage example: add section to change elevator capacity)
m (tdt_cons.py: fix comment)
Line 47: Line 47:
  
 
File_Header_Struct = _cns.Struct('file_header',
 
File_Header_Struct = _cns.Struct('file_header',
_cns.Anchor('hdr_start_'), # to debug, change Anchor to Probe
+
# 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
 
# SimTower doesn't seem to care about the first byte, but the second

Revision as of 01:53, 27 September 2016

File Format
Name SimTower saved game
Ontology
Extension(s) .tdt

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

Personal tools
Namespaces

Variants
Actions
Navigation
Toolbox