Hotdry.

Article

SimTower 保存文件格式逆向工程:二进制结构与解析实战

深入解析经典管理游戏 SimTower 的 TDT 保存文件格式,涵盖文件头、楼层数据、租户结构、电梯调度与财务统计的二进制表示方法。

2026-05-01systems

在模拟经营游戏的历史长河中,SimTower(1993 年由日本开发者 Yoot Sait o 制作、Maxis 发行)占据着独特地位。这款以摩天大楼管理为核心玩法的游戏,其保存文件采用纯二进制格式存储,蕴含着丰富的逆向工程价值。与现代游戏普遍使用 JSON 或 XML 等文本格式不同,SimTower 的.tdt文件以紧凑的二进制结构记录了建筑物的完整状态,包括楼层布局、租户信息、人员流动、财务数据乃至电梯调度参数。对这一格式进行逆向分析,不仅能够帮助我们理解 1990 年代游戏的数据持久化设计思路,更能为现代系统工程师提供处理定长与变长混合数据结构、端序问题以及版本兼容性挑战的实战参考。

文件格式总体架构

SimTower 的保存文件使用.tdt扩展名,整体采用小端序(little-endian)编码,这与当年运行在 Windows 平台上的程序特征一致。文件版本标记为0x24(十进制 36),位于文件头部特定偏移位置,程序通过这一标记判断文件格式的兼容性。当玩家尝试加载版本号不匹配的存档时,游戏会弹出 "wrong version" 警告,这为逆向工程师提供了定位文件结构起始点的关键线索。

整个文件可以划分为若干逻辑区块:文件头(560 字节)、楼层数据块(120 个楼层结构)、人员数据块、零售业数据块、电梯数据块、财务统计块、楼梯数据块以及命名人员块。这种区块划分方式体现了游戏开发者在设计时的清晰思路 —— 将不同类型的数据分别组织,便于读写操作和状态管理。值得注意的是,许多区块的大小在设计时已经固定(如 120 个楼层、64 个楼梯、24 部电梯),这种硬编码的数组大小在当年的游戏开发中相当常见,既简化了代码逻辑,也保证了数据读取的确定性。

文件头设计与全局状态

文件头部固定占用 560 字节,其中包含了游戏全局状态的完整快照。前两个字节尤为重要:第一个字节游戏似乎并不关心其具体值,但第二个字节必须为0x24,否则游戏会拒绝加载。第三个字节表示建筑的星级评定(1-5 星,或 6 代表完整的 TOWER 级别),这一信息直接对应游戏中玩家建造的大楼规模。

财务数据在文件头中以 32 位无符号整数形式存储,但这里存在一个关键细节:游戏 UI 显示的金额实际上是内部存储值的 100 倍。游戏手册中也提到了这个 "奇怪" 的设计 —— 大部分界面会向玩家 "撒谎",只有财务窗口(Finance Window)显示的是真实数值。这种处理方式在 1990 年代的游戏中并不罕见,通常是为了在有限的存储空间内保持足够的精度,同时简化显示逻辑。在进行逆向解析时,开发者需要记住将读取到的现金余额乘以 100 才能得到真实的美元数值。

时间系统在文件头中通过两个字段表示:frameTime记录当前帧数(用于游戏内时间的实时推进),currentDay记录自游戏开始以来经过的天数(带符号 32 位整数,可为负值)。时间的推进并非线性的 —— 游戏将一天划分为 7 个时段,每个时段的帧流逝速度各不相同。上午 7 点至中午 12 点每帧代表 45 秒,而午间 12 点至 12 点半每帧仅代表 4.5 秒,这种时间流的非线性设计旨在加速玩家不活跃时段的模拟过程。天数计数器在第 2300 帧处发生日期进位,且游戏运行超过 1000 年后会发生整数溢出,这与许多早期游戏面临的 "千年虫" 问题如出一辙。

视口位置(viewport_x 和 viewport_y)记录了玩家当前看到的屏幕区域,以像素为单位。此外,文件头还包含大堂高度(lobby_height)以及命名人员计数(named_people_count)等信息,为后续区块的解析提供必要的上下文参数。

楼层与租户数据结构

文件头之后是 120 个楼层结构的连续排列,每个楼层结构本身是变长的,这增加了解析的复杂性。每个楼层以 16 位无符号整数开始,表示该楼层上租户条目的数量。随后是左右边缘坐标(同样以 16 位无符号整数存储),描述该楼层上所有租户的水平范围。

租户(tenant)结构是理解 SimTower 数据组织方式的核心。每个租户占用 18 字节,包含以下关键字段:左右边缘坐标(各 2 字节)、租户类型(1 字节)、状态字节、类型索引(1 字节)、人员偏移量(4 字节)、租户 ID(1 字节)、租金等级(1 字节)等。租户类型以数值形式存储,通过查表可以转换为游戏中的实际类型:0 代表普通楼层,3-5 代表酒店单人间、双人间和套房,6 代表餐厅,7 代表办公室,9 代表公寓,10 代表商店,11 代表停车位,12 代表快餐店,13 代表医疗中心,14 代表安保,15 代表客房服务,18-19 代表电影院,20-21 代表回收站,24 代表大堂,29-30 代表宴会厅,31 代表地铁站,34-35 代表影院,36-40 代表大教堂,44 代表停车坡道,45 代表地铁隧道,48 代表烧毁区域。

租户的水平位置以 8 像素为单位进行测量,使用半开区间 [right, left) 表示占用范围。这意味着如果一个租户的左边缘为 0x96、右边缘为 0x9F,则该租户实际占用从第 38 个单位到第 40 个单位的位置。租户在数组中按左边缘坐标升序排列,这种排序方式便于渲染时的空间计算。值得注意的是,电梯、楼梯和自动扶梯并不被视为租户,因此它们的数据存储在文件后部的专门区块中,与租户数据分离。

每个楼层还包含一个 94 元素的索引映射数组(indexMap),用于处理租户在楼层内的重排序问题。当游戏中需要引用某个楼层的特定租户时,实际访问的是floors[floor].tenants[floors[floor].indexMap[index]]这样的双重索引结构。这种设计为游戏提供了灵活的租户排序能力,而无需物理移动底层数据。

楼层编号采用独特的映射方式:原始值 0-9 对应地下楼层 B10 至 B1,原始值 10 对应一楼,原始值 110 对应第 100 层。游戏最多支持 110 个可见楼层加上额外的楼层缓冲区域,总计 120 个楼层数据结构预留。

人员与零售业数据

人员数据块紧跟在楼层数据之后,以 32 位无符号整数开头指示人员总数,随后是每个人员的详细结构(每条记录 16 字节)。人员结构中包含租户楼层索引、租户在楼层内的索引、人员在租户中的编号、租户类型、当前状态、当前所在楼层(负值表示在楼外)、压力值(stress)和评估值(eval)等字段。这些数据共同构成了游戏中 NPC 行为的物理基础。

零售业数据块用于存储快餐店、餐厅和商店的详细信息,共包含 512 个固定大小的记录,每个记录 18 字节。虽然大多数记录可能处于未使用状态(通过楼层字段的负值来表示无效条目),但游戏预先分配了足够的空间以支持大规模的商户组合。零售类型通过单独的查找表定义,例如餐厅包括英式酒吧、法式餐厅、中餐馆、寿司店和牛排馆,快餐包括日式荞麦面、中式咖啡馆、汉堡店、冰淇淋店和咖啡店,商店则涵盖男装店、花店、书店、药店、银行、邮局等多种业态。

电梯系统的复杂结构

电梯数据结构是整个文件格式中最复杂的部分,每部电梯占用 194 字节的固定头部(如果电梯存在)或仅 12 字节的空标记(如果电梯不存在)。电梯类型包括三种:快速电梯(express)、标准电梯(standard)和服务电梯(service)。快速电梯仅在特定楼层停靠(通常是每 15 层一个大堂),而标准电梯和服务电梯可以在更多楼层停靠。

电梯容量是一个需要特别关注的参数。根据逆向工程的结果,容量值大于 42 可能导致游戏崩溃,而 42 对于非快速电梯似乎是安全的上限。这一限制很可能源于游戏内部数组的大小预分配,当超出预期容量时会导致缓冲区溢出。电梯数据还包含四个 14 元素的调度数组,分别对应工作日(7 个时段)和周末(7 个时段)的响应距离、快递模式(express mode)和出发延迟配置。

每部电梯还维护一个 120 元素的停靠楼层数组,对应游戏中的 120 个楼层位置。对于快速电梯,如果在非大堂楼层(地面以上且不是 15 的倍数)添加停靠点,同样可能引发游戏崩溃,因为底层数组的大小是基于预期服务楼层数量计算的。每部电梯还有 8 个轿厢的休息楼层配置,用于模拟电梯在非活跃期间返回基楼的行为。

财务统计与游戏经济

财务统计块位于电梯数据之后,包含三类数组:租户人口(10 个类型)、租户收入(10 个类型)、租户维护成本(10 个类型),以及对应的总 tower 值。这 10 种收入类型依次为:办公室、单人间、双人间、酒店套房、商店、快餐店、餐厅、宴会厅、剧院和公寓。10 种维护成本类型包括:大堂、电梯、快速电梯、服务电梯、自动扶梯、停车坡道、回收站、地铁站、客房服务和安保。

这些数据与游戏财务窗口中显示的信息直接对应,为玩家提供建筑物经营状况的全面概览。通过分析这些数值的排列方式,可以推断出游戏的收入计算模型和成本分摊机制。值得注意的是,这些统计数据似乎是从底层交易数据实时汇总而来,而非独立存储的原始值,这意味着修改这些值可能不会影响游戏实际的经济模拟逻辑。

楼梯与命名人员

楼梯数据块包含 64 个固定宽度的记录,每个记录 10 字节。与电梯类似,每个楼梯条目包含存在标志、类型(自动扶梯或普通楼梯)、水平位置和基准楼层等信息。楼梯的水平位置同样以楼层单位而非像素为单位进行测量,与租户位置的表示方式保持一致。

文件末尾是命名人员区域,每个命名占用 16 字节的固定宽度字符串空间(使用 C 风格字符串,即以 null 字符结尾)。最大名称长度为 15 个字符,超出部分会被截断。这些命名与具体租户的关联方式目前仍不完全清楚,可能是通过其他数据结构中间接引用的。

逆向工程的工程实践

对 SimTower 文件格式的逆向工程展示了处理复古游戏数据结构的典型方法论。首先是 magic number 识别 —— 版本字节0x24作为文件格式的标识符,为解析器提供了初始的版本兼容性检查点。其次是结构体边界验证,通过计算偏移量并使用断言检查各区块的实际边界是否与预期相符,可以快速发现解析逻辑中的错误。以头部为例,解析器会验证头部结束位置是否恰好等于起始位置加上 560 字节,这种检查在处理变长数据时尤为重要。

端序处理是小端序格式解析的关键,考虑到现代计算机普遍采用小端序(x86/x64 架构),这种格式与当代系统的兼容性较好,但在跨平台场景下仍需注意字节序转换。数组大小的硬编码体现了 1990 年代游戏开发的典型风格 —— 预先确定最大容量可以简化内存管理,但也限制了后续扩展的灵活性。

对于希望在现代系统中复现或扩展 SimTower 功能的开发者,可以使用 Python 的 Construct 库构建解析器,实现文件内容的二进制解析与重建。值得注意的是,游戏对大部分数据字段并不进行严格校验,这意味着修改某些非关键字段可能会产生意想不到的模拟效果,例如负数日期值会导致时间显示异常。这种宽松的验证机制既是当年开发中的务实选择,也为逆向工程师提供了更多的探索空间。


参考资料

systems