# 15｜战场之下：平台库、资源工具与现代移植

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

分析基线同 [第 01 章](01-repository-history-and-build.md)。下文的接口行为来自固定提交源码；恢复路线和实验门槛是工程建议，没有冒充已完成的移植结果。

## 15.1 两套平台库：相近接口，不同硬件现实

`WWFLAT32` 的总库包含音频、描述符管理、文件、键盘、MCGA/SVGA 图形原语、内存、调色板、形状、瓦片、定时器、视频、文本窗口与 WSA。`WIN32LIB` 则以 `DRAWBUFF`、Windows 音频、定时器、通信等实现承担相近职责。主程序根据 `WIN32` 选择其一，不能把两树所有 CPP 放到同一个构建目标中。[DOS 总库清单](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WWFLAT32/MAKEFILE#L69-L98)；[Windows 总库清单](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/MAKEFILE#L72-L98)

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

三个容易从名字猜错的例子值得打开函数体确认：`DIPTHONG` 的 `Dip_Text` 将常见字符对编码；`WW_WIN/WINDOWS.CPP` 提供 `Window_Print`、滚动和分页；`WinModemClass::Serial_Port_Open` 则用 `CreateFile(...FILE_FLAG_OVERLAPPED)` 打开串口并创建读写事件。它们分别对应文本、文本窗口和串口，名称相近不等于职能相同。[字符对编码](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/DIPTHONG/DIPTHONG.CPP#L117-L155)；[文本窗口打印](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/WW_WIN/WINDOWS.CPP#L414-L453)；[串口异步打开](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/WINCOMM/WINCOMM.CPP#L390-L428)

`MOVIE` 还展示了子工程与主工程衔接不完整的问题：它使用独立 Watcom 11 路径及 DirectX Media 头文件，默认生成 `mpeg.lib`；主游戏链接却要求 `mpgdll.lib`。二者名称不同，不能因为看到了电影源文件，就断言缺失的导入库已经可以直接替代。需要继续核对导出方式、符号、调用约定及原 DLL 包装边界。[电影子工程配置](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/MOVIE/MAKEFILE#L44-L78)；[默认库目标](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/MOVIE/MAKEFILE#L110-L118)

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

仓库内的 `OLD`、`OLDMEM`、`MSVC`、`NEW`、`OK`、`.BAK` 是历史演进的痕迹，不是清晰的现代版本管理分支。`SRCDEBUG` 和 `INCLUDE` 也不只是手工维护目录：`DRAWBUFF/MAKEFILE` 在生成子库时，会把头文件和汇编声明复制到 `INCLUDE`，把 CPP 和 ASM 复制到 `SRCDEBUG`。[复制与归档步骤](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/DRAWBUFF/MAKEFILE#L166-L189)

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

例如游戏的 `TECHFILES` 明确编译 `CODE/RAWFILE.CPP`；Windows 总库当前 `LIBS` 不含 `rawfile.lib`，虽然安装/更新列表仍有对应项。读文件路径时应先读游戏实际使用的这份，不能自动采用 `WIN32LIB/RAWFILE` 中同名文件。[游戏技术库的文件模块](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/MAKEFILE#L373-L399)；[总库与安装列表的差异](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/MAKEFILE#L80-L119)

## 15.3 内存：保留接口，让实现随平台变化

Windows `Alloc(bytes, flags)` 的主路径其实是 `malloc`：失败时调用 `Memory_Error`，指定 `MEM_CLEAR` 时清零。若启用 `MEM_CHECK`，会额外申请前后保护区域，存入指针和大小标记，便于发现越界写入。全局 `new/new[]` 又转调 `Alloc`，`delete/delete[]` 转调 `Free`。[Win32 分配实现](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/MEM/ALLOC.CPP#L150-L186)；[全局分配运算符](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/MEM/NEWDEL.CPP#L63-L125)

同一分配文件仍保留非 Win32 路径：优先申请保护模式内存，必要时用 DPMI `int 0x31` 服务申请 DOS 内存、保存选择子。Windows 版的 `DPMI_Lock/Unlock` 则是空实现。这是移植中典型的接口保留：上层不用重写每个调用点，但底层原本的“锁住实模式可访问内存”概念已不再对应相同动作。[DOS 回退分配](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/MEM/ALLOC.CPP#L188-L248)；[空的 DPMI 兼容函数](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/MEM/ALLOC.CPP#L100-L119)

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

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

Windows `WinTimerClass` 用 `timeBeginPeriod` 请求精度、用 `timeSetEvent` 注册周期回调。回调进入 `Update_Tick_Count`，递增 `SysTicks` 与 `UserTicks`；销毁时取消事件并恢复计时精度。[计时器生命周期](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/TIMER/TIMERINI.CPP#L99-L158)；[回调和计数更新](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/TIMER/TIMERINI.CPP#L184-L230)

`TimerClass::Time()` 通过“当前 tick 减去启动基准”累积时间；停止会把 `Started` 清为 0，启动使用 `当前 tick + 1` 避免与停止标记冲突。倒计时类返回 `max(DelayTime - 已用时间, 0)`。因此多个计时器主要保存基准和剩余值，不需要每个对象注册一个 Windows 系统事件。[累计计时与暂停](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/TIMER/TIMER.CPP#L127-L178)；[倒计时](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/TIMER/TIMERDWN.CPP#L93-L122)

但是战斗时间不是由这个回调直接“驱动每个坦克”。游戏另有 `FrameTimerClass`，其返回值就是全局模拟帧 `Frame`；`SystemTimerClass` 才读平台时钟。一个基于帧的 90 tick 倒计时，要推进 90 个模拟帧；系统时间经过多久，还取决于速度、暂停与主循环调度。把两者统统替换成毫秒会改变玩法和同步语义。[两种时钟源](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/JSHELL.H#L332-L369)

DOS 库则加载或取得实模式定时器镜像，再调用 `Install_Timer_Interrupt`。它与 Windows 多媒体定时器实现不同，却试图向上提供相近的 tick 查询。[DOS 定时器安装](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WWFLAT32/TIMER/TIMERINI.CPP#L89-L138)

## 15.5 图形库：统一缓冲区，仍要处理显存的特殊性

`GraphicBufferClass::Init` 根据标志选择 DirectDraw 表面或普通内存：有 `GBC_VIDEOMEM/GBC_VISIBLE` 时走 `DD_Init`；否则使用外部缓冲区或 `new BYTE[Size]`。这让游戏绘图代码可在屏幕页、后台页和内存图片上使用相近接口。[缓冲区分支](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/DRAWBUFF/GBUFFER.CPP#L307-L337)

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

`Set_Video_Mode` 创建 DirectDraw 对象、取得全屏独占级别、设置显示模式，再建立 256 色调色板。软件缓冲区里主要保存颜色索引，显示时通过调色板解释颜色；这也是重映射、阵营着色和渐变效果的基础之一。[显示初始化](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/MISC/DDRAW.CPP#L454-L499)

大量绘图细节在汇编里：`BITBLIT.ASM` 先裁剪源矩形，再裁剪目标矩形，避免把视口外的数据当像素复制；`GBUFFER.INC` 手工声明 C++ 对象字段布局，用 `DD` 表示地址与整数。这里既有性能优化，也有 ABI 耦合。把宿主改成 64 位后，如果 C++ 指针变成 8 字节而汇编仍按旧偏移读字段，结果不是“稍慢”，而是读取错误地址。[矩形拷贝裁剪](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/DRAWBUFF/BITBLIT.ASM#L88-L167)；[汇编字段布局](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/DRAWBUFF/GBUFFER.INC#L35-L51)

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

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

`CODE/RAWFILE.CPP` 处理底层文件访问。Win32 用 `ReadFile/WriteFile`，DOS 则走 DOS 文件函数；`BiasLength` 还会限制读取范围，让一段大文件区间表现为独立子文件。这对 MIX 包中的资源很自然：上层请求“读取某张图”，下层可把它映射到包内的偏移和长度。[带偏置的读取](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/RAWFILE.CPP#L531-L575)

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

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

两者都有链连接维护，基类通常只转发操作，具体压缩、加密、校验由派生段实现。[Straw 连接与读取](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/STRAW.CPP#L93-L139)；[Pipe 连接、写入与刷新职责](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/PIPE.CPP#L92-L151)

例如 `LCWStraw::Get` 先消耗已经解压的缓存；不足时读入含压缩长度和原始长度的块头，再读完整压缩块并调用 `LCW_Uncomp`。调用者想要 7 字节，不代表压缩算法也每次只解压 7 字节。`LZWStraw`、`LZOStraw` 提供相似分块接口，内部算法不同。它们出现在技术库中，不代表每一种资源都会经过三种压缩。[LCW 分块读取](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/LCWSTRAW.CPP#L126-L178)；[LZO 分块处理](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/LZOSTRAW.CPP#L159-L179)

**一个可以手算的 LCW 例子。** 按本实现的解码分支，构造字节序列 `83 41 42 43 00 03 80`：`83` 指示直接拷贝 3 字节，得到 `ABC`；`00 03` 指示从当前输出位置向后退 3 字节，再复制 3 字节，得到 `ABCABC`；`80` 结束。这是依据源码构造的教学输入，没有把它冒充游戏现有资源或本次已执行测试。它解释了这类压缩如何用“之前出现过的内容”代替重复字节。[LCW 的短回拷、原样拷贝和结束码](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/LCW.CPP#L72-L109)

MIX 的加密和校验也复用这套链。构造函数识别扩展标志，若头部加密，接上 `PKStraw`；它先用 `PKey` 还原 Blowfish 密钥，再让内部 `BlowStraw` 处理后续块。缓存 MIX 数据时，若带摘要则接入 `SHAStraw`，读取后与文件内 20 字节摘要比较。不应把这一描述扩大成“整个包体都加密”或“所有资源都必然校验”；源码分别控制头部加密与缓存时的摘要路径。[MIX 头部选择](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/MIXFILE.CPP#L188-L230)；[密钥解包](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/PKSTRAW.CPP#L171-L224)；[数据缓存摘要比较](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/MIXFILE.CPP#L398-L449)

移植这些解码器时，必须额外建立损坏输入测试。例如 `LCW_Uncomp` 的长度参数在此实现中没有名字，也没有被使用；它靠操作码和结束标记前进。现代工具应明确输入上限、输出上限、回拷范围及截断处理，保持合法数据结果一致，同时避免把格式外数据当作可信内存。[解码函数签名及访问方式](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/LCW.CPP#L72-L92)

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

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

`AUDIOMAK` 接受 WAV/VOC，输出 `.AUD`。`Convert_WAV` 解析 RIFF 格式块，记录采样率、单双声道和 8/16 位等标志；启用压缩时，以不超过 2048 字节的段调用 `Compress_Frame`，写出压缩长度、原始长度、魔数和压缩内容。[音频头与压缩类别](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/TOOLS/AUDIOMAK/AUDIOMAK.CPP#L106-L129)；[WAV 转换及分块输出](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/TOOLS/AUDIOMAK/AUDIOMAK.CPP#L495-L590)

`MIXFILE` 则读取一个文件名清单，为每个文件建立记录，计算名字的 CRC 标识，确定内容的偏移和长度，再将索引按 CRC 排序。它写出的索引记录包括 `CRC/Offset/Size`，随后写文件内容。这不是把文件系统目录结构原样塞进 ZIP，也没有在这段写入路径中加入扩展加密头。[清单读取](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/TOOLS/MIX/MIXFILE.CPP#L274-L313)；[名字标识](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/TOOLS/MIX/MIXFILE.CPP#L370-L386)；[索引排序与输出](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/TOOLS/MIX/MIXFILE.CPP#L480-L568)

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

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

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

`VQA32` 主要组织电影播放：配置、任务调度、加载、绘制、音频、字幕及解码汇编。`VQM32` 是它的配套工具库，含 IFF、调色板、LCW、音频编解码、字体、CRC、MIX 等。两者有不少功能与主平台库重叠，因为它们是相对独立的历史库，而非所有技术都共享同一个现代公共模块。[VQA32 对象清单](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WINVQ/VQA32/MAKEFILE#L58-L75)；[VQM32 配套对象](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WINVQ/VQM32/MAKEFILE#L49-L86)

从 `VQA_Play` 可看出它有自己的播放状态：首次准备绘图与音频、设定电影时钟；暂停保存时间并停音；恢复后继续“加载一帧、尝试画一帧”，并通过 `User_Update` 与宿主协作。电影播放器的循环不是战斗 AI 循环，不能用电影 DrawRate 推算坦克逻辑更新速度。[初始化与暂停](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WINVQ/VQA32/TASK.CPP#L269-L321)；[电影加载与绘制循环](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WINVQ/VQA32/TASK.CPP#L323-L415)

`IPX` 顶层目录保存了 Win32 调用 16 位 DLL 的 thunk 桥。`DllMain` 连接 `THIPX16.DLL` 与 `THIPX32.DLL`；`.THK` 文件描述参数方向和跨边界的结构；批处理用 thunk 工具、Watcom、`ml` 和链接器生成 DLL/导入库。这与游戏中事件重传、命令同步等上层网络规则是不同层。当前主项目还定义 `WINSOCK_IPX`，所以不能把历史 thunk 的存在解释成每次联机都必经它。[DLL 桥接入口](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/IPX/THIPX32C.CPP#L73-L99)；[跨位宽参数描述](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/IPX/THIPX.THK#L49-L82)；[桥接构建步骤](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/IPX/MAKETH32.BAT#L1-L7)

两个启动器也不同：`LAUNCH` 的 16 位汇编压缩自身占用、检查低端内存与磁盘、设置 DOS/4GW 环境，再执行 `GAME.DAT`；`LAUNCHER` 则读同名 `.lcf`，查补丁，启动配置指定的进程，等待退出，再看是否需要应用补丁并重启。后者不负责地图更新或战斗运算。[DOS 启动准备](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/LAUNCH/LAUNCH.ASM#L132-L185)；[DOS 执行游戏](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/LAUNCH/LAUNCH.ASM#L254-L289)；[Windows 配置与补丁循环](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/LAUNCHER/MAIN.CPP#L90-L167)

这种老工具也应按实现评估可靠性。例如 `Create_Process` 保存 `CreateProcess` 的返回值却最终无条件返回 TRUE；因此“函数报告成功”不足以证明子进程已经启动。阅读保留工程时，既要理解作者意图，也要看到尚未处理完整的错误路径。[进程创建实现](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/LAUNCHER/PROCESS.CPP#L30-L58)

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

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

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

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

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

### 源码阅读路线

先读 `CODE/MAKEFILE` → `WIN32LIB/MAKEFILE` → `DRAWBUFF/MAKEFILE`，掌握实际输入；再读 `MEM/ALLOC.CPP`、`TIMER/TIMERINI.CPP`、`MISC/DDRAW.CPP`，看三个平台适配范例；之后沿 `RAWFILE → Straw/Pipe → LCW/PKStraw → MIXFILE` 理解字节流；最后对照 `TOOLS/MIX`、`TOOLS/AUDIOMAK`、`WINVQ/VQA32/TASK.CPP` 与两个启动器，把运行时和制作流水线接起来。

### 尚未确认的边界

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