# 13 联机、帧同步与录像：几台电脑怎样打同一场战争

红警联机最值得理解的地方是：每台电脑都运行完整战场模拟，网络主要交换“哪位玩家在第几帧命令哪些单位做什么”。坦克逐步移动、子弹飞行、伤害结算和电脑玩家决策由各台机器依照相同规则推演。由此可以在很低的带宽下容纳大量单位，但所有机器必须保持相同的初始状态、命令顺序和随机数调用轨迹。本章把这一设计称为**确定性锁步**：这是对实际代码的架构归纳，不意味着每一帧都必须同时收到所有人的一个独立包。

本文分析固定提交中的实现，涉及 `COMM_PROTOCOL_MULTI_E_COMP`、`WINSOCK_IPX`、`FIXIT_MULTI_SAVE` 等条件分支；不把这些分支混称为所有发行版本都启用的唯一协议。作为切入点，`Queue_AI()` 将普通战役及遭遇战送入本地队列处理，将调制解调器、IPX、Internet 等模式送入联机队列，录像回放则优先走独立入口。三者共用事件执行机制。[源码：模式分流](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L355-L379)

## 13.1 玩家操作如何变成可复制的命令

`EventClass` 是网络层与游戏规则之间的协议。它拥有事件类型、执行帧 `Frame`、来源阵营 `ID`、是否已经执行的标志，以及按类型复用的 `Data` 联合体。事件不是鼠标屏幕坐标：`MEGAMISSION` 包含单位、任务、目标和目的地；生产包含对象类别及类型编号；放置建筑包含类别和地图格。这一抽象使不同分辨率、不同观察位置的玩家能够共享同一套战场命令。[源码：事件结构与载荷](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/EVENT.H#L56-L179)

需要直接读字段而不是只读注释：当前 `Frame` 是 **26 位**，尽管附近遗留注释写着 27 位；阵营编号占 5 位，执行标志占 1 位。`TARGET`/`xTargetClass` 表示可还原的游戏对象或地图目标标识，不是把一台电脑的裸内存地址发送给另一台电脑。对象身份的基础见[对象模型章节](03-object-model-and-memory.md)。

`Queue_Mission()` 构造事件并加入 `OutList`。`OutList` 是本机产生、等待调度的命令；`DoList` 是已经获得执行时间、等待执行的命令。单机的 `Queue_AI_Normal()` 直接搬运，随后可录像、执行并清理；联机必须先给本地命令分配未来帧，装入发送包，同时把同一事件放入本机 `DoList`。所以本机操作者也受到统一延迟约束，并不会先立即改动坦克、再让远端追赶。[源码：入队](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L241-L247)、[单机执行通路](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L406-L442)、[联机本地入执行队列](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L2745-L2772)

到达执行时间以后，`EventClass::Execute()` 才翻译命令为游戏操作。例如 `PRODUCE` 调用阵营的 `Begin_Production()`；`PLACE` 调用 `Place_Object()`；移动/攻击的大任务经过 `Assign_Mission()`、`Assign_Target()`、`Assign_Destination()` 等进入单位状态机。**发出移动指令不等于立刻写入最终坐标**，寻路、转向和每帧推进仍由单位逻辑完成。[源码：生产与放置](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/EVENT.CPP#L611-L627)、[任务与目标赋值](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/EVENT.CPP#L732-L765)

## 13.2 用未来执行帧换取共同的现在

最直接的锁步实现可以“发一帧、等一帧”，但那会把每次网络往返都变成停顿。这里用 `MaxAhead` 允许机器在可控范围内前进：发送时给命令标注未来执行帧，给网络传递留出空间。普通协议使用 `Frame + delay`；多帧压缩协议还向上取整到 `FrameSendRate` 的整数倍。每个复合包以 `FRAMEINFO` 开头，携带该帧的状态校验值、累计命令数和延迟。[源码：封包与调度帧](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L2390-L2456)

`FRAMEINFO` 和 `FRAMESYNC` 必须区分：前者是常规心跳及命令包头，携带运行状态校验；后者用于初始握手或等待中的重新通报，携带场景校验，不附带普通命令。`Frame - Delay` 用来推回发送者的游戏进度。开始游戏以及载入多人存档时，会清空相关计数、发送同步消息并等待其他机器准备好。普通载入用当前战场校验比较存档；紧急存档分支可把场景校验置零，这是一条明确的历史恢复例外。[源码：帧信息定义](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/EVENT.H#L185-L218)、[握手与载入](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L636-L688)、[同步包](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L1735-L1764)

能不能继续由 `Can_Advance()` 的两个条件共同决定：

```text
所有连接的 已收到命令数 >= 对方宣称已发送命令数
且 当前 Frame < 所有对方进度的最小值 + MaxAhead
```

只比较帧号是不够的。假设对方发送移动命令的可靠包暂时丢失，但后发的无确认心跳先到了，帧号会显示“对方已往前走”。如果本机据此继续，可能错过移动命令的执行时刻。累计命令数让本机知道仍缺数据；`Process_Receive_Packet()` 从包头更新宣称的数量，从成功拆出的普通事件更新实际收到的数量，并排除心跳本身。[源码：推进判定](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L2090-L2132)、[收到包后的计数](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L1805-L1918)

这里的等待并不是回滚预测。代码会等数据、重发帧同步、弹出重连对话框或处理超时；如果普通命令已经错过约定帧，`Execute_DoList()` 报 `TXT_PACKET_TOO_LATE` 并返回失败。对所读主通路而言，没有现代 rollback 那种保存最近数帧、猜测输入、再倒退重算的步骤。不要把 `FRAMESYNC` 名字理解成“下载其他玩家的完整世界来修复”。[源码：等待重发](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L960-L975)、[迟到命令处理](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L3312-L3338)

## 13.3 实例：给三辆坦克下达同一移动命令

以下数字是按公式构造的教学例子，不是一次运行测量。设当前逻辑帧为 120，`FrameSendRate=3`，`MaxAhead=9`，玩家 A 选中坦克 U1、U2、U3，要求它们去同一地图格 C。

1. 输入层为三辆坦克分别产生任务事件，进入 A 的 `OutList`。此时没有三份完整坦克状态需要传输。
2. 本帧符合发送周期，封包将三个事件的执行帧设为 `ceil((120+9)/3)×3=129`，把 A 的阵营 ID 写入事件，并复制到 A 本机的 `DoList`。
3. 如果这三个连续事件都是 `MEGAMISSION`，且任务、目标、目的地相同，压缩器只保存一次公共任务数据，随后列出其余单位的 `Whom`，并附重复数量。
4. B 收到后恢复出三个完整事件，记录 A 的进度及命令数；A、B 都不能因为自己的包先到，就改变公共的执行顺序。
5. 双方在允许推进且到达 129 帧时按同样顺序执行事件，提交三辆坦克的任务/目的地。后续逻辑更新持续推进实际移动；某些路径准备可以在目的地赋值时立即发生。

这里还有一个容易忽略的时序细节：本快照的主循环先运行 `Logic.AI()`，然后运行 `Queue_AI()`，之后才 `Frame++`。因而在 129 帧队列阶段赋予的新任务，主要由后续逻辑轮次消费；不能把本例写成“先执行 129 帧命令，再计算同一轮 `Logic.AI()`”。但这不表示事件执行期间绝不寻路：`DriveClass::Assign_Destination()` 会在条件满足时立即调用 `Start_Of_Move()`，后者可以进入 `Basic_Path()`；应区分调用栈内的路径准备与之后持续发生的位移更新。[源码：即时移动准备](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/DRIVE.CPP#L618-L627)、[路径生成条件](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/DRIVE.CPP#L892-L947)。详细主循环见[运行时章节](02-runtime-and-game-loop.md)。[源码：主循环两阶段](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONQUER.CPP#L2248-L2296)、[递增帧](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONQUER.CPP#L2377-L2381)

## 13.4 同一帧也必须有同一顺序

“所有人都收到命令了”仍不等于确定性。如果 A 的坦克开火与 B 的坦克撤退在同一帧到达，按网络到达顺序执行可能导致不同结果。`Execute_DoList()` 先按阵营顺序遍历，再扫描属于该阵营的事件。代码明确利用各机器共同的 House 顺序来建立执行次序；传输层还要保持同一连接可靠命令的顺序。[源码：固定阵营执行次序](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L3276-L3317)

由此能看到确定性的三个层次：同一个场景和规则产生相同起点；共同的帧号与阵营次序产生相同外部输入序列；单位、AI、碰撞与随机数计算产生相同后续状态。工程上，这也解释了为什么“只在自己画面上多调用一次游戏随机函数”可能毁掉整局联机，而“只改变本机光标外观”通常不该进入共享战场逻辑。后一句是设计约束的推论，不表示本项目所有视觉路径都已经经过我们运行验证。

在 `TIMING_FIX` 条件下，代码还处理动态增大 `MaxAhead` 的危险窗口：时间参数改变前生成的某些命令仍带旧延迟，但机器可能按新延迟放宽推进。实现把落在指定窗口内的普通事件重新安排到窗口末尾，避免迟到。这说明协议演进牵动了事件调度，不能只修改一个全局延迟数字。[源码：延迟变更修正](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L3252-L3274)

## 13.5 节省的是结构重复，不是坦克运动精度

压缩协议没有把坦克坐标降低精度来省流量；它省掉事件间重复的协议字段。`EventLength[]` 描述各类有效载荷长度；公共帧号和玩家 ID 已在包头提供，普通事件只需类型加对应联合体片段。`ADDPLAYER` 另有变长载荷，不能按照普通固定大小事件处理。[源码：按事件长度封装](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L2608-L2630)、[变长事件](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L2827-L2855)

`MEGAMISSION` 连续重复编码特别适合 RTS：玩家一次框选几十辆坦克，目的地通常相同。第一项存完整任务，后面的项只存不同单位身份。接收方必须重新展开为逐单位事件，保证执行层不需要理解“网络上这次压缩了十辆车”。这里仅对实际出现的 `MEGAMISSION` 分支作结论，不能凭名字把带编队速度参数的 `MEGAMISSION_F` 也算进同一重复编码规则。[源码：相同任务的比较](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L2632-L2678)、[写入压缩结果](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L2791-L2825)、[接收端逐单位展开](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L3109-L3173)

发送策略也区分“消息必须到”与“进度通报可以被更新的进度替代”。含命令的包请求 ACK；没有命令的心跳可以不要求 ACK。代码有根据发送积压限制每次命令数的逻辑，但随后明确对 Internet、modem、null-modem 分支覆盖该上限。分析时应保留这个覆盖，不能把上方注释里的队列节流条件宣称为全部联机方式的统一行为。[源码：发送容量与确认策略](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L1615-L1699)

## 13.6 网络速度、逻辑帧率和屏幕刷新不是同一个量

`Frame` 表示模拟的离散步数；连接超时及响应时间使用系统 tick；`FrameSendRate` 表示每几帧发一次复合包，名称中的 rate 在这里不是每秒多少包。多帧协议下，不符合发送周期时队列处理会提前返回，但主循环仍继续经过各轮逻辑。[源码：发送周期分支](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L773-L798)

每 128 帧，机器报告自身处理耗时；主机角色负责产生统一的 `TIMING` 事件。它不是负责所有战斗计算的权威服务器。`Generate_Real_Timing_Event()` 根据最慢参与者所需的系统 tick 和玩家选择的速度，取较低的目标帧率；把往返响应时间换算为单程逻辑帧数，再取整到发送周期，且至少为发送周期的三倍。因此网络变差可能加大命令延迟，机器处理慢可能降低共同节奏。[源码：周期调整](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L742-L769)、[目标帧率及 MaxAhead 公式](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L1377-L1462)

画面刷新另有工作机会。主循环等待计时的 `Sync_Delay()` 仍处理输入、渲染和回调，所以不能用“每秒逻辑推进几次”简单等同“显示器每秒刷新几次”。同样，代码中大量 60 tick 换算不能单独证明游戏固定运行在 60 个逻辑帧每秒；速度设置、协议分支和处理能力都会影响实际节奏。[源码：等待期仍有界面工作](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONQUER.CPP#L2086-L2115)

## 13.7 从事件协议一路到 UDP、IPX 与串口

| 层次 | 主要职责 | 代码入口 |
|---|---|---|
| 游戏语义 | 任务、生产、部署、出售等命令 | `EventClass` |
| 确定性调度 | 未来帧、累计命令计数、状态校验 | `QUEUE.CPP` |
| 连接管理 | 向一个或全部已连接玩家发消息 | `IPXManagerClass` / `NullModemClass` |
| 可靠传输 | 包 ID、ACK、重发、重复包处理、顺序交付 | `ConnectionClass` |
| 平台通道 | UDP/IPX 数据报或串口帧收发 | `WSPUDP.CPP` / `WSPIPX.CPP` / `NULLCONN.CPP` |

多人队列通过 `ConnManClass*` 选择具体连接管理器，Internet 分支也复用名为 `Ipx` 的管理对象。`IPXManagerClass::Send_Private_Message()` 默认向所有连接发送同一包，所以不要从 `Ipx` 这个遗留变量名推断 Internet 数据必然采用 IPX，也不要把这里的“private”理解成加密。[源码：管理器选择](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L603-L628)、[向各连接发送](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/IPXMGR.CPP#L827-L861)

`ConnectionClass` 通过 ACK 和超时重发实现可靠性。尤其需要纠正文件头的旧说法：虽然 `CONNECT.H` 注释曾形容底层不保证顺序，实际 `Get_Packet()` 对 `PACKET_DATA_ACK` 只交付 `LastReadID+1`，而无确认包可以直接取出。可靠命令与不可靠心跳采用不同交付规则，正是帧同步还需要独立累计命令数的原因。[源码：实际顺序交付](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONNECT.CPP#L480-L537)、[确认与重发维护](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONNECT.CPP#L594-L658)

在 `WINSOCK_IPX` 分支，`IPXConnClass::Send_To()` 转交 `PacketTransport->WriteTo()`。Internet 初始化可选 `UDPInterfaceClass`，其 socket 是 `AF_INET/SOCK_DGRAM`；IPX 实现则使用 `AF_NS/SOCK_DGRAM/NSPROTO_IPX`。串口路径把长度、两个 magic 值、数据校验及结束字符包在应用消息外面，然后交给串口写入。这是多套平台通路共享同一游戏协议的实例。[源码：传输抽象](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/IPXCONN.CPP#L611-L633)、[Internet/IPX 选择](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/INIT.CPP#L1199-L1258)、[UDP socket](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/WSPUDP.CPP#L146-L175)、[IPX socket](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/WSPIPX.CPP#L185-L219)、[串口外层帧](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/NULLCONN.CPP#L163-L215)

WOL 相关代码还承担大厅玩家信息、地址和开局准备：它从玩家信息生成 `Session.Players`，设置主机地址，然后启动底层收发并调用 `Init_Network()`。这些服务集成不等于战斗中每个坦克状态都由 WOL 服务器计算。本章确认的是客户端调用与分层，未验证旧在线服务现在能否使用，也未将仓库中的接口定义视为完整服务端源码。[源码：WOL 玩家信息与开局](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/WOL_GSUP.CPP#L3280-L3343)

## 13.8 不同步是怎样发现的：一个很不“纯”的校验函数

`Compute_Game_CRC()` 组合步兵位置、朝向、任务、目标，车辆和建筑部分字段，舰船字段，阵营资金/电力，以及地图层、逻辑层对象的顺序和身份。它不是对全部内存的完整快照求密码学哈希：很多字段并未逐个纳入，组合算法 `Add_CRC()` 是移位加法。函数名字包含 CRC，不应据此宣称它使用某种标准 CRC 多项式或能无碰撞证明两局状态完全相同。[源码：采样字段](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L3793-L3868)、[组合算法](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L3895-L3908)

最出人意料的是最后的 `Add_CRC(&GameCRC, Scen.RandomNumber)`。`RandomClass::operator int()` 会调用取随机数运算符，后者更新 `Seed`；它**消耗一个新的随机数**，不是读取 `Seed` 字段！代码旁被注释掉的旧写法才是直接读取种子。随机数生成器按线性递推更新状态并提取 15 位；区间抽样还可能因为拒绝超范围结果而多次取数。因此调用次数、调用次序和调用参数都可能影响后续战斗。[源码：校验取随机数](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L3870-L3874)、[隐式转换及常量](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/RANDOM.H#L55-L83)、[随机递推与区间抽样](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/RANDOM.CPP#L89-L181)

这是阅读源码的一个实用教训：看似“仅用于诊断”的函数，在这个项目里也可能参与模拟状态变化。移植时若擅自删除校验或在某台机器多调用一次，会改变随机序列。网络每帧先计算校验，并在 32 项环形历史中保存；收到 `FRAMEINFO` 后按对应历史帧比较，且有初始跳过期、恰好在约定帧执行、`Delay<32` 等条件。不同步时可以输出诊断并提示退出或继续，但“继续”涉及断开连接等处理，并不是自动修复分歧。[源码：历史记录](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L636-L662)、[校验条件与不同步处理](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L3441-L3505)

## 13.9 录像重放是再计算，而且还保存了观看视角

项目确实有录像与回放代码，不能说“老红警完全没有录像机制”。但也不能据此承诺发行版提供现代回放浏览器：一处启用 `Session.Record/Play` 的命令选项位于 `CHEAT_KEYS` 条件内；`Session` 默认文件名为 `RECORD.BIN`，另有 attract 演示逻辑。[源码：调试条件内的开关](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/INIT.CPP#L2104-L2138)、[录像文件名](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/SESSION.CPP#L180-L188)

录像包含三个不同层次：启动头记录 Session、场景、初始随机种子、特殊选项与游戏选项；`Do_Record_Playback()` 记录/恢复视口位置、当前选择、编组与编队数据；`Queue_Record()` 在执行前保存本轮待执行事件数量及 `EventClass` 原始结构。它不是逐帧视频，也不是只有鼠标输入；准确说是“可重建初始环境的配置，加命令流及部分界面状态”。[源码：启动头](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/INIT.CPP#L3482-L3524)、[视角与选择状态](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONQUER.CPP#L5094-L5150)、[事件记录](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L3602-L3625)

回放从记录中恢复事件、重置执行标志、放回 `DoList`，再调用同一个 `Execute_DoList()`；它会遵循多人首帧和发送周期的特殊节奏，文件读取失败则结束。`Init_Random()` 的回放分支从记录种子初始化随机状态，真实的伤害与移动仍重新运行，因此规则或执行次序变化可能使旧录像偏离；保存格式直接依赖 `sizeof(EventClass)`，也没有看到跨版本回放迁移层。[源码：回放循环](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L3702-L3773)、[随机初始化](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/INIT.CPP#L2437-L2465)

还有一个值得保留的边界：普通单机队列没有在同处调用游戏 CRC，而回放队列会调用；CRC 又会消耗随机数。源码中的记录注释与调试入口多次指向多人用途。因此，**“存在战役分支”不足以证明所有单机录像都可确定性正确重放**，这一点需实际构建、记录与回放对照后再下结论。存档则保存当前世界并恢复对象，详见[下一章](14-save-load-and-binary-formats.md)。

## 13.10 继续阅读路线与未验证部分

推荐按 `EVENT.H → Queue_Mission → Queue_AI_Multiplayer → Build_Send_Packet → Can_Advance → Execute_DoList → EventClass::Execute` 跟踪一条命令，再读 `CONNECT.CPP` 看可靠交付，最后读 `Queue_Record/Queue_Playback` 和 `Do_Record_Playback` 看为什么同一事件边界能支持录像。

本章已据函数体确认事件延迟、顺序、计数门槛、状态校验、传输分层和录放数据流；没有实际完成跨机联机、网络丢包注入、超长局计数回绕或录像兼容性实验。具体发行版本启用了哪些宏、外部在线服务的现状、各平台结构打包差异以及单机录像是否存在上述随机偏差，均不能用静态阅读替代运行结论。
