SimTower saved game

From Just Solve the File Format Problem
(Difference between revisions)
Jump to: navigation, search
(add some code to parse the format (TODO: add usage notes))
(add notes)
Line 11: Line 11:
  
 
== Code ==
 
== 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 <code>(sign)(endian)Int(bits)</code>, 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.
 
<br clear="all">
 
<br clear="all">
  

Revision as of 01:23, 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.


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

Personal tools
Namespaces

Variants
Actions
Navigation
Toolbox