“基洛夫空艇已就绪”
如果你曾在凌晨两点偷偷打开电脑,把音量调到最低,造出第一个基洛夫空艇然后飞过对面主基地——那么,你对这张地图的感情一定很特别。

《红色警戒2》是很多人的即时战略游戏启蒙。光棱坦克的折射光线、磁暴步兵的电弧、超时空兵一闪一闪消失在敌方矿车旁边……这些画面至今记忆犹新。
但你有没有好奇过:这些承载了无数战术和回忆的经典地图,其底层文件究竟是如何构成的?除了使用官方地图编辑器,能否直接解析甚至生成 .map 文件?为了解答这个问题,我进行了一次深入的 逆向工程,并最终将整个过程打包成一个可以让 AI 理解的“技能”。
第一个发现:.map 文件本质是 INI
没错,用任何文本编辑器打开一个红警2的 .map 文件,你会看到结构清晰的文本内容。

[Basic]
Name=Desert Dominion
NewINIFormat=4
Official=no
GameMode=standard
MultiplayerOnly=1
[Map]
Size=0,0,80,80
LocalSize=2,4,76,72
Theater=DESERT
[Waypoints]
; 玩家出生点:value = Y * 1000 + X
0=15015 ; 玩家1:X=15, Y=15
1=65065 ; 玩家2:X=65, Y=65
就是这么直接。NewINIFormat=4 告诉游戏引擎这是 RA2/YR 的格式;Theater=DESERT 选择了沙漠地表主题(还有 TEMPERATE、SNOW、URBAN 等)。出生点的编码方式简单得让人感动:Y × 1000 + X,将二维坐标压缩成一个整数。玩家1在 (15, 15),那就是 15015。
建筑、单位、树木的放置同样使用纯文本描述:
[Structures]
; ID=所属方,类型,血量,X,Y,朝向,标签,...
0=Neutral,CAOILD,256,40,40,0,none,0,1,0,0,,,1,0
[Terrain]
; 格子编号=树木类型
30030=TREE01
31030=TREE05
CAOILD 是油井,TREE01 是一种树。格式直白明了。
读到这里你可能会想:所以地图文件就是个配置表,解析不就是读 INI 吗?事情没那么简单。
二进制段:地图数据的核心
继续往下翻,你会遇到完全不同的内容:
[IsoMapPack5]
1=8gDuAP8AAAAAAAAAAAAAAAEAAAAAAAAAAAAAAgAAAAAAAAAAAAADAAAA
2=AAAAAAAAAABQAAAAAAAAAAAAAGAAAAAAAAAAAAAAcAAAAAAAAAAAAAAC
3=AAAAAAAAAAAAAAoAAAAAAAAAAAAACwAAAAAAAAAAAAAMAAAAAAAAAAAAAA
...(数百行 Base64)
[OverlayPack]
1=/v8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/
2=AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/
...(更多 Base64)
这些 Base64 字符串背后,藏着地图真正的核心数据。让我们逐一拆解。
IsoMapPack5:每个瓦片11字节
红警2采用45度等距视角(Isometric),地图由菱形网格构成。地图上的每一格地面都被编码成一个11字节的结构体:
int16 X // 瓦片 X 坐标
int16 Y // 瓦片 Y 坐标
int32 TileIndex // 地形类型索引
uint8 TileSubIndex // 子瓦片索引
uint8 Level // 高度层级 (0-14)
uint8 IceGrowth // 冰冻生长状态
一张 80×80 的标准地图有 6400 个格子,原始数据就是 70,400 字节,外加4字节终止符。用 Python 生成这个结构非常直观:
for y in range(height):
for x in range(width):
raw.extend(struct.pack('<hh', x, y)) # X, Y
raw.extend(struct.pack('<i', tile_index)) # 地形类型
raw.append(0) # SubIndex
raw.append(0) # Level(高度)
raw.append(0) # IceGrowth
OverlayPack:262,144字节的固定网格
覆盖物(矿石、宝石、围墙、铁轨等)使用一套完全不同的体系。无论你的地图实际多大,覆盖物网格始终固定为 512×512 = 262,144 个格子。每个格子一个字节,表示覆盖物类型。
此外,还有同样大小的 OverlayDataPack,每个格子一个字节表示“帧”(对于矿石来说,就是密度,0-11,数值越高越富)。两个数组加起来就是 512KB 的原始数据。
overlay_pack = bytearray([0xFF] * 262144) # 0xFF = 无覆盖物
overlay_data = bytearray([0x00] * 262144)
# 放置一块矿:类型 105 (TIB01),密度 10
idx = x + 512 * y
overlay_pack[idx] = 0x69 # 105
overlay_data[idx] = 10 # 帧 10 = 高密度矿
Westwood 的两种自研压缩算法
如此庞大的原始数据不能直接写入文本文件。Westwood 在90年代为其游戏引擎实现了两种压缩方案:
| 数据段 |
压缩算法 |
特点 |
| IsoMapPack5, PreviewPack |
LZO1X |
解压极快,适合实时加载 |
| OverlayPack, OverlayDataPack |
Format80/LCW |
Westwood 自研,基于位操作的指令集 |
然后,所有数据会经过一层统一的 Pack Section 编码:
原始数据
→ 分块(每块 ≤8192 字节)
→ 逐块压缩(LZO1X 或 Format80)
→ 每块前加4字节头:[uint16 压缩长度][uint16 原始长度]
→ 拼接所有块
→ Base64 编码
→ 按70字符换行,加行号
最终,就变成了你在 .map 文件中看到的 1=ABC..., 2=DEF... 的格式。
Format80(又称LCW)是Westwood从《命令与征服》时代就开始使用的压缩格式。它并非通用压缩算法,更像一个微型虚拟机的指令集——解压器逐字节读取“指令”,并执行相应操作:
10cccccc → 拷贝接下来的 c 个字面字节(c=0 表示结束)
11111110 cc cc ww → 用字节 w 填充 c 个位置(RLE 压缩)
0cccpppp pppppppp → 从偏移 p 处回拷 c+3 个字节(短回引用)
11cccccc pppp pppp → 从绝对位置 p 拷贝 c+3 个字节
11111111 cccc cccc pppp pppp → 长绝对引用
我实现了一个简单但有效的编码器——仅使用字面拷贝和RLE填充,未做回引用匹配。因为覆盖物数据大部分是 0xFF(空格子),RLE已经足够高效:
def encode(data: bytes) -> bytes:
result = bytearray()
pos = 0
while pos < len(data):
# 检测连续相同字节(RLE 机会)
run_byte = data[pos]
run_end = pos + 1
while run_end < len(data) and data[run_end] == run_byte:
run_end += 1
run_len = run_end - pos
if run_len >= 4:
# 填充指令:0xFE + 长度(uint16) + 填充值
result.append(0xFE)
result.extend(struct.pack('<H', min(65535, run_len)))
result.append(run_byte)
pos = run_end
else:
# 收集字面字节,最多 63 个
lit_start = pos
while pos < len(data) and (pos - lit_start) < 63:
# 遇到 4+ 相同字节就中断
if (pos + 3 < len(data)
and data[pos] == data[pos+1] == data[pos+2] == data[pos+3]):
break
pos += 1
chunk_len = pos - lit_start
if chunk_len > 0:
result.append(0x80 | chunk_len) # 字面拷贝指令
result.extend(data[lit_start:pos])
result.append(0x80) # 结束标记
return bytes(result)
一个262,144字节、全部为 0xFF 的覆盖物网格,压缩后只有几百字节。
LZO1X的取巧方案:不压缩也是一种压缩
IsoMapPack5 使用的是 LZO1X 压缩。标准做法是安装 python-lzo 这个C扩展库。但为了让工具实现零依赖运行,我研究了一个巧妙的方案。
翻阅 LZO1X 格式规范,我发现它允许一种 “first literal”指令,可以直接搬运原始字节而不进行任何压缩。其格式极其简单:
[长度 + 17] [原始字节...] [0x11, 0x00, 0x00]
第一个字节是数据长度加17(解压器读取后减17即知要拷贝多少字节),最后三个字节是结束标记。唯一的限制是单次最多处理238字节。
因此,策略就是:把数据切成238字节的小块,每块都使用这种仅包含字面数据的编码。
MAX_LITERAL = 238
def compress(data: bytes) -> bytes:
if _HAS_LZO:
return _lzo.compress(data, 1, False) # 有 C 库就用真压缩
# 没有 C 库:literal-only 编码
result = bytearray()
result.append(len(data) + 17) # 长度标记
result.extend(data) # 原始字节
result.extend(b'\x11\x00\x00') # 结束标记
return bytes(result)
这样生成的文件会稍大一些,但游戏引擎能够正确识别并解压。这就足够了。为什么Westwood当年选择LZO1X而不是zlib?很可能是因为在2000年的硬件条件下,LZO的解压速度更快——对于需要实时加载的游戏来说,速度比压缩率更重要。
设计决策:让AI与脚本各司其职
解决了技术解析问题后,接下来需要设计一个实用的工具。这里有一个关键决策:什么该让AI做,什么该让脚本做?
答案很清晰:
| 角色 |
擅长 |
负责 |
| AI |
创意决策、平衡性判断、理解自然语言 |
设计地图布局、放置资源、规划路线 |
| 脚本 |
精确计算、二进制编码、格式合规 |
压缩算法、打包编码、文件生成 |
因此,我将整个项目打包成了一个 Claude Code Skill:

ra2-map-generator/
├── SKILL.md ← AI的“说明书”:格式规范、设计指南
├── scripts/
│ ├── generate_map.py ← 主引擎:组装完整 .map 文件
│ ├── pack_section.py ← Pack Section 编码器
│ ├── format80.py ← Format80/LCW 压缩
│ └── lzo_mini.py ← LZO1X 压缩(含 fallback)
├── references/
│ ├── REFERENCE.md ← 地图格式完整规范
│ ├── structures.md ← 建筑/单位/步兵 ID 大全
│ ├── overlays.md ← 覆盖物类型索引
│ └── theaters.md ← 地表主题文档
└── assets/templates/ ← 模板地图(1v1 / 4人)
SKILL.md 是核心——它既是AI的触发条件,也是工作流程指南和格式速查表。AI读完就知道:
Y * 1000 + X 是坐标编码公式。
- 覆盖物类型 105 是矿石,28 是宝石。
- “帧” 0-11 控制资源密度。
- 如何调用脚本、传递参数。
实战演示:从需求到可玩地图
让我们走一遍完整流程。需求是:“生成一张沙漠主题的 1v1 地图”。
AI 的工作
AI 根据 SKILL.md 的指导,做出以下决策:
- 参数确定:80×80 尺寸,DESERT 主题,2 位玩家。
- 布局设计:对称布局,玩家1在左上 (15,15),玩家2在右下 (65,65)。
- 资源规划:每位玩家附近放置矿石和宝石,地图中央放置额外的争夺资源。
接着,AI 会生成局部 INI(文本段落)和描述覆盖物放置的 JSON 数据。
脚本的工作
python3 -m scripts.generate_map \
--ini partial_map.ini \
--width 80 --height 80 \
--theater DESERT \
--overlay-json overlays.json \
--output Desert_Dominion.map
脚本接手后执行以下步骤:
- 解析文本 INI,保留 AI 编写的 [Basic]、[Map]、[Waypoints] 等段落。
- 生成 IsoMapPack5:6400个瓦片 × 11字节 → LZO1X压缩 → Base64编码。
- 生成 OverlayPack 和 OverlayDataPack:2 × 262,144字节 → Format80压缩 → Base64编码。
- 生成预览图:160×100像素的BGR888纯色图 → LZO1X压缩 → Base64编码。
- 补全必要的空段落(如 Triggers、Events 等,以确保游戏兼容性)。
- 按照红警2引擎预期的24个段落的顺序组装最终文件。
最终产物
生成 Desert_Dominion.map 文件,大小约179KB,包含24个标准段落。将其复制到红警2的 Maps/ 目录下,启动遭遇战模式,就能在地图列表中找到 “Desert Dominion”。选择,开局,熟悉的“正在建造基地”语音响起。
资源生成细节:曼哈顿距离衰减
资源平衡是地图设计的灵魂。在脚本中,矿石的生成采用了曼哈顿距离衰减算法,使得矿场分布更自然:
def generate_ore_cluster(center_x, center_y, radius=3, density=8):
ores = []
for dy in range(-radius, radius + 1):
for dx in range(-radius, radius + 1):
dist = abs(dx) + abs(dy) # 曼哈顿距离
if dist <= radius:
frame = max(0, min(11, density - dist))
if frame > 0:
ores.append({
'x': center_x + dx,
'y': center_y + dy,
'type': 0x69, # TIB01 (矿石)
'frame': frame, # 越靠近中心密度越高
})
return ores
矿场中心最富(frame = density),向外逐格递减。这样生成的矿场呈现出中间富、边缘贫的自然过渡,与游戏原版地图的风格保持一致。宝石(GEM01, 类型 0x1C)的生成逻辑类似,只是默认半径更小、基础密度更低,以体现其稀有性。
扩展可能性
目前生成的是平坦地形(所有瓦片 TileIndex=0, Level=0)。但IsoMapPack5的11字节结构体中,高度(Level)和地形类型(TileIndex)字段都是可用的。这意味着未来可以轻松扩展:
- 高度变化:利用 Level (1-14) 创建山丘与高地。
- 悬崖与斜坡:使用特定的 TileIndex(如100-120代表悬崖,200-250代表斜坡)。
- 水域与道路:TileIndex 5-8 代表水面,50-70 代表铺装路面。
地图可以从简单的“一马平川”进化为拥有地形起伏、河流分割、高地争夺的复杂战场。
更有价值的是 Agent Skill 这一范式本身。SKILL.md + scripts/ + references/ 的三件套结构,本质上是为AI注入了某个垂直领域的专业能力:
SKILL.md:定义工作流程、触发条件、格式速查。
scripts/:处理AI不擅长的精确计算(二进制编码、压缩算法)。
references/:提供领域知识库(所有ID、编号映射)。
这套架构完全可以复用于其他领域,如Minecraft地图生成、城市规划模型、电路板布局等。核心思想始终是:让AI负责创意与决策,让脚本保障精确与执行。
结语
《红色警戒2》发行于2000年。二十多年后,我们通过逆向工程解析其地图格式,并利用AI技术来自动生成新地图。
Westwood 工程师当年设计的这套混合格式——INI文本夹杂二进制数据、分块压缩的Pack Section、262,144字节的固定覆盖物网格——充满了那个时代特有的工程美学:不追求极致的通用性,但在严格的约束条件下做到了高度的实用性。
逆向这些格式的过程,本身就是一次对经典游戏开发智慧的“考古”。你会发现,2000年的游戏工程师面临的挑战与今天的开发者并无本质不同:如何在有限的空间内编码足够的信息,如何在性能与灵活性之间权衡,如何设计一个能被社区和工具扩展的格式。他们选择了LZO而非zlib,选择了INI而非纯二进制容器,每个选择的背后都有其时代背景与工程考量。
而现在,通过结构化的知识注入,AI也能理解并运用这些古老的“规则”了。
建造完毕。
本文技术实现与项目思路,旨在探讨经典游戏文件格式分析与AI辅助生成的结合,欢迎在 云栈社区 进行更多技术交流。项目完整代码已基于 MIT 协议开源,仅需 Python 3.8+ 标准库即可运行,零外部依赖。