查看源码下载文档
CHAPTER / 15

战场之下:平台库、资源工具与现代移植

红警的玩法代码之所以能够用“画一块图”“读一个资源”“等一段时间”表达行为,是因为下面还有一整套 Westwood 自有库。它们承担从操作系统到游戏世界的转换:把磁盘字节变成图像和声音,把系统计时变成可查询的时钟,把内存和显存统一成可绘制缓冲区。本章追踪这些层次,也解释为什么这个项目无法仅靠换一个 C++ 编译器恢复。

分析基线同 第 01 章。下文的接口行为来自固定提交源码;恢复路线和实验门槛是工程建议,没有冒充已完成的移植结果。

15.1 两套平台库:相近接口,不同硬件现实

WWFLAT32 的总库包含音频、描述符管理、文件、键盘、MCGA/SVGA 图形原语、内存、调色板、形状、瓦片、定时器、视频、文本窗口与 WSA。WIN32LIB 则以 DRAWBUFF、Windows 音频、定时器、通信等实现承担相近职责。主程序根据 WIN32 选择其一,不能把两树所有 CPP 放到同一个构建目标中。DOS 总库清单Windows 总库清单

模块组 主要职责 需要避免的误读
MEM、DOS 的 DESCMGMT 分配、释放、内存标志;DOS 保护模式描述符与实模式协作 不能把旧 DPMI 接口理解成现代统一虚拟内存 API
DOS 的 MCGAPRIM/SVGAPRIM/VIDEO、Win32 的 DRAWBUFFMISC/DDRAW 像素、矩形、拷贝、视口、显示模式和表面管理 图形操作大量由 CPU 及汇编完成,DirectDraw 并非现代 3D 引擎
PALETTE/SHAPE/TILE/FONT/WSA/IFF 调色板、形状帧、地块、字形、动画与图片格式 多种资源格式有不同解码路径,不能都当普通位图
DIPTHONG 固定二字符字典文本编码与解码 它压缩文字,不是语音合成模块
KEYBOARD/TIMER 输入缓存、按键与时钟 系统 tick 不等于战斗模拟帧
AUDIO/PLAYCD 样本播放、流式音频、CD 播放相关接口 DOS SOS 与 Windows DirectSound 分支不同
DOS 的 FILE、Win32 的 RAWFILECODE 中的文件类 原始读写、路径、缓存、游戏资源包适配 同名类有历史版本,实际游戏清单选择十分重要
MONO/PROFILE/EXAMPLE 调试显示、性能测量与例程 存在测试程序不代表全仓库有现代自动测试套件
WINDOWS/WW_WIN 游戏库自己的文本窗口与打印控制 WW_WIN/WINDOWS.CPP 并不是 Windows 主消息循环
WINCOMM 串口、调制解调器及 Windows 注册表配置 不等于 IPX 或整套互联网多人逻辑
MOVIE 另立工程的 MPEG 播放封装 不在当前 win32lib.libLIBS 列表中

三个容易从名字猜错的例子值得打开函数体确认:DIPTHONGDip_Text 将常见字符对编码;WW_WIN/WINDOWS.CPP 提供 Window_Print、滚动和分页;WinModemClass::Serial_Port_Open 则用 CreateFile(...FILE_FLAG_OVERLAPPED) 打开串口并创建读写事件。它们分别对应文本、文本窗口和串口,名称相近不等于职能相同。字符对编码文本窗口打印串口异步打开

MOVIE 还展示了子工程与主工程衔接不完整的问题:它使用独立 Watcom 11 路径及 DirectX Media 头文件,默认生成 mpeg.lib;主游戏链接却要求 mpgdll.lib。二者名称不同,不能因为看到了电影源文件,就断言缺失的导入库已经可以直接替代。需要继续核对导出方式、符号、调用约定及原 DLL 包装边界。电影子工程配置默认库目标

15.2 历史副本与“真正编进游戏的是哪一份”

仓库内的 OLDOLDMEMMSVCNEWOK.BAK 是历史演进的痕迹,不是清晰的现代版本管理分支。SRCDEBUGINCLUDE 也不只是手工维护目录:DRAWBUFF/MAKEFILE 在生成子库时,会把头文件和汇编声明复制到 INCLUDE,把 CPP 和 ASM 复制到 SRCDEBUG复制与归档步骤

本次对照发现:MISC/DDRAW.CPPSRCDEBUG/DDRAW.CPP 相同,而 AUDIO/SOUNDIO.CPPSRCDEBUG/SOUNDIO.CPP 不同,TIMER/TIMER.HINCLUDE/TIMER.H 也不同。这种混合状态意味着不能简单宣布“所有副本都可删除”,也不能把 SRCDEBUG 一律视作最新实现。可靠次序是:主目标清单 → 子库对象清单 → include 搜索顺序 → 符号定义及接口匹配

例如游戏的 TECHFILES 明确编译 CODE/RAWFILE.CPP;Windows 总库当前 LIBS 不含 rawfile.lib,虽然安装/更新列表仍有对应项。读文件路径时应先读游戏实际使用的这份,不能自动采用 WIN32LIB/RAWFILE 中同名文件。游戏技术库的文件模块总库与安装列表的差异

15.3 内存:保留接口,让实现随平台变化

Windows Alloc(bytes, flags) 的主路径其实是 malloc:失败时调用 Memory_Error,指定 MEM_CLEAR 时清零。若启用 MEM_CHECK,会额外申请前后保护区域,存入指针和大小标记,便于发现越界写入。全局 new/new[] 又转调 Allocdelete/delete[] 转调 FreeWin32 分配实现全局分配运算符

同一分配文件仍保留非 Win32 路径:优先申请保护模式内存,必要时用 DPMI int 0x31 服务申请 DOS 内存、保存选择子。Windows 版的 DPMI_Lock/Unlock 则是空实现。这是移植中典型的接口保留:上层不用重写每个调用点,但底层原本的“锁住实模式可访问内存”概念已不再对应相同动作。DOS 回退分配空的 DPMI 兼容函数

这层“通用内存分配”与游戏对象的固定容量池是两个概念:前者向操作系统取得字节,后者组织单位、建筑等对象的身份与生命周期。移植时若同时更换分配器、对象池和序列化,很难定位第一次状态差异来自哪里,宜分开验证。

15.4 时间:操作系统节拍与模拟帧必须分开

Windows WinTimerClasstimeBeginPeriod 请求精度、用 timeSetEvent 注册周期回调。回调进入 Update_Tick_Count,递增 SysTicksUserTicks;销毁时取消事件并恢复计时精度。计时器生命周期回调和计数更新

TimerClass::Time() 通过“当前 tick 减去启动基准”累积时间;停止会把 Started 清为 0,启动使用 当前 tick + 1 避免与停止标记冲突。倒计时类返回 max(DelayTime - 已用时间, 0)。因此多个计时器主要保存基准和剩余值,不需要每个对象注册一个 Windows 系统事件。累计计时与暂停倒计时

但是战斗时间不是由这个回调直接“驱动每个坦克”。游戏另有 FrameTimerClass,其返回值就是全局模拟帧 FrameSystemTimerClass 才读平台时钟。一个基于帧的 90 tick 倒计时,要推进 90 个模拟帧;系统时间经过多久,还取决于速度、暂停与主循环调度。把两者统统替换成毫秒会改变玩法和同步语义。两种时钟源

DOS 库则加载或取得实模式定时器镜像,再调用 Install_Timer_Interrupt。它与 Windows 多媒体定时器实现不同,却试图向上提供相近的 tick 查询。DOS 定时器安装

15.5 图形库:统一缓冲区,仍要处理显存的特殊性

GraphicBufferClass::Init 根据标志选择 DirectDraw 表面或普通内存:有 GBC_VIDEOMEM/GBC_VISIBLE 时走 DD_Init;否则使用外部缓冲区或 new BYTE[Size]。这让游戏绘图代码可在屏幕页、后台页和内存图片上使用相近接口。缓冲区分支

但它不是彻底屏蔽平台的抽象。Lock() 对普通内存立即成功;对 DirectDraw 表面要检查指针、游戏焦点和嵌套锁次数,获取表面地址及行间跨度。如果表面丢失,调用恢复函数并有限重试。这里的 Pitch 被存成“实际一行跨度减去 Width”,不是现代 API 中常见的完整行字节数;迁移时照搬同名变量尤其容易画错。表面锁定与 Pitch 语义

Set_Video_Mode 创建 DirectDraw 对象、取得全屏独占级别、设置显示模式,再建立 256 色调色板。软件缓冲区里主要保存颜色索引,显示时通过调色板解释颜色;这也是重映射、阵营着色和渐变效果的基础之一。显示初始化

大量绘图细节在汇编里:BITBLIT.ASM 先裁剪源矩形,再裁剪目标矩形,避免把视口外的数据当像素复制;GBUFFER.INC 手工声明 C++ 对象字段布局,用 DD 表示地址与整数。这里既有性能优化,也有 ABI 耦合。把宿主改成 64 位后,如果 C++ 指针变成 8 字节而汇编仍按旧偏移读字段,结果不是“稍慢”,而是读取错误地址。矩形拷贝裁剪汇编字段布局

一种稳妥的现代化设计是先保留 8 位索引帧缓冲和原绘图规则,只在最终呈现时转为现代窗口纹理。它能把“画得对不对”与“现代 API 怎么显示”分成两项实验。进一步改写图形原语或提高逻辑分辨率,应在像素对照基线建立之后进行。具体战场渲染顺序见 第 11 章

15.6 字节流:文件、压缩与加密可以像积木一样组合

CODE/RAWFILE.CPP 处理底层文件访问。Win32 用 ReadFile/WriteFile,DOS 则走 DOS 文件函数;BiasLength 还会限制读取范围,让一段大文件区间表现为独立子文件。这对 MIX 包中的资源很自然:上层请求“读取某张图”,下层可把它映射到包内的偏移和长度。带偏置的读取

文件之上有两种通用数据流:

  • Straw:消费者向当前段 Get,当前段向上游取得字节并加工,属于拉取式接口。
  • Pipe:调用者向当前段 Put,当前段加工后推给下游;缓冲转换还需要 Flush 输出尾部数据。

两者都有链连接维护,基类通常只转发操作,具体压缩、加密、校验由派生段实现。Straw 连接与读取Pipe 连接、写入与刷新职责

例如 LCWStraw::Get 先消耗已经解压的缓存;不足时读入含压缩长度和原始长度的块头,再读完整压缩块并调用 LCW_Uncomp。调用者想要 7 字节,不代表压缩算法也每次只解压 7 字节。LZWStrawLZOStraw 提供相似分块接口,内部算法不同。它们出现在技术库中,不代表每一种资源都会经过三种压缩。LCW 分块读取LZO 分块处理

一个可以手算的 LCW 例子。 按本实现的解码分支,构造字节序列 83 41 42 43 00 03 8083 指示直接拷贝 3 字节,得到 ABC00 03 指示从当前输出位置向后退 3 字节,再复制 3 字节,得到 ABCABC80 结束。这是依据源码构造的教学输入,没有把它冒充游戏现有资源或本次已执行测试。它解释了这类压缩如何用“之前出现过的内容”代替重复字节。LCW 的短回拷、原样拷贝和结束码

MIX 的加密和校验也复用这套链。构造函数识别扩展标志,若头部加密,接上 PKStraw;它先用 PKey 还原 Blowfish 密钥,再让内部 BlowStraw 处理后续块。缓存 MIX 数据时,若带摘要则接入 SHAStraw,读取后与文件内 20 字节摘要比较。不应把这一描述扩大成“整个包体都加密”或“所有资源都必然校验”;源码分别控制头部加密与缓存时的摘要路径。MIX 头部选择密钥解包数据缓存摘要比较

移植这些解码器时,必须额外建立损坏输入测试。例如 LCW_Uncomp 的长度参数在此实现中没有名字,也没有被使用;它靠操作码和结束标记前进。现代工具应明确输入上限、输出上限、回拷范围及截断处理,保持合法数据结果一致,同时避免把格式外数据当作可信内存。解码函数签名及访问方式

15.7 资源工具:游戏内容是如何进入运行时的

TOOLS 中有压缩、动画、图标、字体、音频和打包相关 EXE,但不是每个 EXE 都提供完整可重建源码。本次能直接深入跟踪的两项是 AUDIOMAKMIX/MIXFILE.CPP;其余工具不能只凭文件名编造完整行为。

AUDIOMAK 接受 WAV/VOC,输出 .AUDConvert_WAV 解析 RIFF 格式块,记录采样率、单双声道和 8/16 位等标志;启用压缩时,以不超过 2048 字节的段调用 Compress_Frame,写出压缩长度、原始长度、魔数和压缩内容。音频头与压缩类别WAV 转换及分块输出

MIXFILE 则读取一个文件名清单,为每个文件建立记录,计算名字的 CRC 标识,确定内容的偏移和长度,再将索引按 CRC 排序。它写出的索引记录包括 CRC/Offset/Size,随后写文件内容。这不是把文件系统目录结构原样塞进 ZIP,也没有在这段写入路径中加入扩展加密头。清单读取名字标识索引排序与输出

于是可以串起一条具体的内容生产链:录制声音 → WAV → AUDIOMAK 转成分块音频 → 清单工具打入 MIX → 游戏按大写资源名计算标识 → 在 MIX 索引中二分查找 → 得到缓存指针或磁盘偏移 → 音频系统按格式播放。工具与游戏之间的真正契约是名字规范、字节布局和解码语义,而不是彼此必须使用同一种编译器。运行时 MIX 二分查找

这也给出一个最适合初学者的恢复实验:只制作两个很小的自有资源,生成 MIX,再用独立读取器按名字提取,比较字节一致性。这样可以学习完整资产链,而无须先修复整个战场引擎。

15.8 VQ、IPX 与两个启动器:不能忽略的外部子系统

VQA32 主要组织电影播放:配置、任务调度、加载、绘制、音频、字幕及解码汇编。VQM32 是它的配套工具库,含 IFF、调色板、LCW、音频编解码、字体、CRC、MIX 等。两者有不少功能与主平台库重叠,因为它们是相对独立的历史库,而非所有技术都共享同一个现代公共模块。VQA32 对象清单VQM32 配套对象

VQA_Play 可看出它有自己的播放状态:首次准备绘图与音频、设定电影时钟;暂停保存时间并停音;恢复后继续“加载一帧、尝试画一帧”,并通过 User_Update 与宿主协作。电影播放器的循环不是战斗 AI 循环,不能用电影 DrawRate 推算坦克逻辑更新速度。初始化与暂停电影加载与绘制循环

IPX 顶层目录保存了 Win32 调用 16 位 DLL 的 thunk 桥。DllMain 连接 THIPX16.DLLTHIPX32.DLL.THK 文件描述参数方向和跨边界的结构;批处理用 thunk 工具、Watcom、ml 和链接器生成 DLL/导入库。这与游戏中事件重传、命令同步等上层网络规则是不同层。当前主项目还定义 WINSOCK_IPX,所以不能把历史 thunk 的存在解释成每次联机都必经它。DLL 桥接入口跨位宽参数描述桥接构建步骤

两个启动器也不同:LAUNCH 的 16 位汇编压缩自身占用、检查低端内存与磁盘、设置 DOS/4GW 环境,再执行 GAME.DATLAUNCHER 则读同名 .lcf,查补丁,启动配置指定的进程,等待退出,再看是否需要应用补丁并重启。后者不负责地图更新或战斗运算。DOS 启动准备DOS 执行游戏Windows 配置与补丁循环

这种老工具也应按实现评估可靠性。例如 Create_Process 保存 CreateProcess 的返回值却最终无条件返回 TRUE;因此“函数报告成功”不足以证明子进程已经启动。阅读保留工程时,既要理解作者意图,也要看到尚未处理完整的错误路径。进程创建实现

15.9 一条可验证的恢复与移植路线

可以选择“尽量还原历史构建”或“保存原规则、替换平台层”两条路线。前者有助于获得行为参考,后者更适合持续维护;两者都需要明确资源版本与编译宏。以下门槛强调能够证明什么,避免以“出现主菜单”替代“游戏正确”。

阶段 要完成的工作 可判定的通过条件
0:固定基线 保留原树,列出源文件选择、宏、外部库、资源哈希、结构布局 每个目标知道编译哪些文件;区分缺输入与待生成产物
1:纯算法与资源 隔离 CRC、LCW、颜色转换、MIX 读写;使用自有小样本 已知字节结果正确;截断、错误长度、边界回拷能受控失败
2:平台最小壳 实现文件、时钟、输入、8 位帧缓冲、声音输出接口 能显示调色板测试图;锁定、行跨度、焦点变化正确;系统计时与模拟帧分离
3:单机最小场景 固定规则、地图、随机种子和输入事件,运行少量单位 每帧状态摘要可重复;移动、伤害、生产等达到预先定义结果
4:行为与格式兼容 对照合法获得的匹配发行版或可信参考;验证存取档和播放 能解释每项差异;不能仅靠“加载没崩溃”判兼容
5:多人同步 保留命令与模拟语义,替换所需传输适配 双端同输入同帧状态一致;延迟与丢包不会悄悄改变事件顺序
6:扩展与发布 再加入扩展包、电影、语言、现代显示及工具 明确功能矩阵、已知差异、资源要求与构建可复现范围

其中 ABI 是贯穿各阶段的重点:32/64 位指针、旧 bool、结构体打包、整数与字节序、C++ 名字修饰、寄存器/栈调用约定,以及汇编手写字段偏移都要分别核对。将旧代码整理成 CMake 工程可以改善依赖管理,但不会自动解决这些语义问题。

本次没有安装历史工具链,也没有运行随附二进制。可复查成果是源码分层、编译条件和实验设计;真正的编译恢复应另行输出命令、日志、工具版本和可重现样本。

源码阅读路线

先读 CODE/MAKEFILEWIN32LIB/MAKEFILEDRAWBUFF/MAKEFILE,掌握实际输入;再读 MEM/ALLOC.CPPTIMER/TIMERINI.CPPMISC/DDRAW.CPP,看三个平台适配范例;之后沿 RAWFILE → Straw/Pipe → LCW/PKStraw → MIXFILE 理解字节流;最后对照 TOOLS/MIXTOOLS/AUDIOMAKWINVQ/VQA32/TASK.CPP 与两个启动器,把运行时和制作流水线接起来。

尚未确认的边界

未验证历史 .LIB/.EXE 与源码逐一对应,未重建完整音视频生产工具,未恢复 IPX 16/32 位桥,也未测量现代替换层的像素、音频或时间误差。库内某些历史模块虽有实现,是否在某个实际发行构建被链接进入,还需链接 MAP、符号引用或对应发行产物补证。

红色警戒初代 · 源码剖析18 章正文 / 5 份附录
输入关键词,检索 24 份完整文档
全文检索 · 点击结果直达对应小节