---
title: TH04 逆向工程：DOS 弹幕图案 — REA
url: https://rea.tools/zh-CN/showcase/th04/
---

[首页](https://rea.tools/zh-CN/) / [案例展示](https://rea.tools/zh-CN/showcase/) / TH04

个案研究·东方4：莲花地故事

# 重建TH04 子弹图案。

一圈弹幕从一段简短的角度计算开始。用 REA 检查原始 DOS 函数，再把指令对应到可读的 C++ 代码。

PC-98·16位DOS · C++和汇编

[查看TH04重建](https://github.com/N0zoM1z0/th04)

从原始指令到经过验证的源码

1. 01 · 原版游戏

   **MAIN.EXE**

   该 PC-98 可执行文件及其弹幕角度辅助函数。
2. 02 · 用 REA 检查

   **指令 + 引用**

   验证加载的 DOS 代码，再从可执行文件中读取角度计算。
3. 03 · 阅读 C++

   **等分 → 瞄准 → 旋转**

   把每个操作对应到维护中的重建源码。
4. 04 · 验证源码

   **2,139 字节匹配**

   已记录的编译器检查覆盖了包含这个辅助函数的弹幕生成代码。

REA 返回原始代码和分析证据。TH04 项目提供维护中的源码，以及编译器检查结果。

## 环形弹幕如何瞄准玩家

Elly 的攻击请求一个初始数量为 16 的自机狙环形弹幕。辅助函数计算每颗子弹的方向。同一圈弹幕既可以朝向固定方向，也可以转向玩家。

你的编程助手

使用REA查找TH04如何创建一个子弹环并将其瞄准玩家。 解释计算并显示代码。

一个示例提示，原始可执行文件和重建签出可在本地使用。

TH04 用 256 个角度单位表示顺时针转一整圈。16 颗子弹之间各隔 16 个单位：`0, 16, 32, …, 240`。再加上玩家方向，就能让所有方向一起旋转。

![两幅图展示 16 颗子弹的环。固定环从角度 0 开始。加上玩家方向 40 后，每个方向都旋转相同角度，高亮的第一颗子弹指向玩家。两者都保持 16 个角度单位的间距，即 22.5 度。](https://rea.tools/assets/localized/zh-CN/figures/th04-bullet-ring.svg)

图中演示还原出的角度计算：子弹数量为 16，玩家方向为 40，模板旋转角为 0。[查看大图](https://rea.tools/assets/localized/zh-CN/figures/th04-bullet-ring.svg)

### 还原源码中的发射模板

Elly 的环形攻击 · 源码节选

```
bullet_template.angle = 0;
bullet_template.group = BG_RING_AIMED;
bullet_template.count = 16;
bullet_template_tune();
```

这几行代码在难度与性能调整之前选定弹幕图案。[阅读该攻击的源码](https://github.com/N0zoM1z0/th04/blob/0d72e9801a5c14c10384efcbb6734ceccb74ee63/src/main/boss/elly_phase_ring16.cpp#L26)。

## REA 返回什么

我们检查设置每颗子弹速度和角度的函数 `bullet_velocity_and_angle_set()`。REA 返回指令、伪代码和引用，其中三部分解释了这种弹幕如何生成。

1. #### 在阅读其代码之前检查DOS映像

   编程助手→ REA

   ```
   open_binary
   {"path": "MAIN.EXE"}

   inspect_native_load_image
   {}
   ```

   REA →编程助手 · 选定字段

   ```
   status: "verified"
   format: "dos-mz"
   load_segment: 4096
   header_bytes: 6144
   module_bytes: 150114
   ```

   这项检查确认加载的代码和重定位记录对应提供的可执行文件。上方路径为便于显示而缩短。
2. #### 查看环形弹幕的乘除计算

   编程助手→ REA · `analyze_function`

   ```
   {"procedure": "0x2cfc8"}
   ```

   REA →编程助手·指令摘录

   ```
   0x2d0eb: MOV AX, SI
   0x2d0ed: SHL AX, 0x8
   0x2d0f0: MOV DL, byte ptr [0x53af]
   0x2d0f9: IDIV BX
   ```

   `SI` 保存子弹索引。移位将它乘以 256，再除以从 `DS:53AF` 读取的数量。重建源码把这个字段命名为 `bullet_template.count`。完整指令序列见下文。
3. #### 找到弹幕转向玩家的那一段

   同一函数的结果 · 瞄准部分摘录

   ```
   0x2d190: SUB AX, word ptr [0x53a6]
   0x2d198: SUB AX, word ptr [0x53a4]
   0x2d19d: CALLF 0x1000:1da8
   0x2d1a2: ADD word ptr [BP + -0x2], AX
   ```

   对应的维护版 C++

   ```
   group_angle_offset += iatan2(
     player_pos.cur.y - bullet_template.origin.y,
     player_pos.cur.x - bullet_template.origin.x
   );
   ```

   指令操作数显示了两个坐标差，以及一个计算方向的辅助函数。它的返回值加到相对环形角度上。源码名称让这些地址的含义更易读；REA 则帮助助手核对地址背后的原始操作。

## 从汇编到可读C++

点击每一步，对照原始指令和角度计算。这份摘要展示自机狙环形弹幕的路径；完整源码还处理散射、叠层和随机模式。

REA · 原始 16 位指令

```
0x2cfcf: MOV SI, word ptr [BP + 0x4]
0x2d0eb: MOV AX, SI
0x2d0ed: SHL AX, 0x8
0x2d0f0: MOV DL, byte ptr [0x53af]
0x2d0f4: MOV DH, 0x0
0x2d0f6: PUSH DX
0x2d0f7: CWD
0x2d0f8: POP BX
0x2d0f9: IDIV BX
0x2d0fb: MOV word ptr [BP + -0x2], AX
0x2d18d: MOV AX, [0x4650]
0x2d190: SUB AX, word ptr [0x53a6]
0x2d194: PUSH AX
0x2d195: MOV AX, [0x464e]
0x2d198: SUB AX, word ptr [0x53a4]
0x2d19c: PUSH AX
0x2d19d: CALLF 0x1000:1da8
0x2d1a2: ADD word ptr [BP + -0x2], AX
0x2d1b9: MOV AL, byte ptr [BP + -0x2]
0x2d1bc: ADD AL, byte ptr [0x53ad]
0x2d1c0: MOV [0xbcc8], AL
```

可读 C++ · 自机狙环形弹幕摘要

```
unsigned char aimed_ring_angle(
    int index, int count,
    int player_direction,
    unsigned char rotation)
{
    int angle = index;
    angle = (angle * 256) / count;
    angle += player_direction;
    angle += rotation;
    return (unsigned char)angle;
}
```

**01 · 读取子弹索引。** 栈参数加载到 `SI`。在 16 颗子弹的环中，索引从 0 到 15。

**02 · 在整圈中等分方向。** `SHL AX,8` 将索引乘以 256。数量先零扩展为一个字，再由 `IDIV` 用乘积除以数量。数量为 16 时，每个索引前进 16 个角度单位。

**03 · 加上玩家方向。** 两次减法得到玩家坐标与原点坐标的差。方向辅助函数在 `AX` 中返回角度；加上这个值，就能把整圈弹幕转向玩家。

**04 · 加上模板旋转角。** `ADD AL,[0x53ad]` 加上模板中的角度。Elly 的初始模板将它设为零；其他弹幕可以用同一个辅助函数进行旋转。

**05 · 用一个字节保存最终角度。** 存储指令把 `AL` 写入子弹生成角度字段。值超过 255 后回绕，与无符号字节转换一致。周围源码还会根据这个角度和速率计算速度分量。

汇编节选展示自机狙环形分支、瞄准计算和最终存储。上面的 C++ 是便于阅读的摘要。下方的编译器检查针对链接项目中维护的源码。

阅读维护中的环形弹幕与瞄准源码

环宏 · TH04分支，摘录

```
angle_offset = (i * BULLET_ANGLE_FULL_TURN) / count;
if (i >= (count - 1)) {
    group_complete = true;
}
goto aim_or_no_aim;
```

角度和速度 · 源摘录

```
aim:
    group_angle_offset += iatan2(
        (player_pos.cur.y - bullet_template.origin.y),
        (player_pos.cur.x - bullet_template.origin.x)
    );

no_aim:
    vector2_near(
        bullet_template.velocity,
        (group_angle_offset + bullet_template.angle),
        speed
    );
    bullet_spawn_angle = (group_angle_offset + bullet_template.angle);
```

[环形弹幕宏](https://github.com/N0zoM1z0/th04/blob/0d72e9801a5c14c10384efcbb6734ceccb74ee63/src/main/bullet/add_impl.hpp#L4) · [瞄准与速度源码](https://github.com/N0zoM1z0/th04/blob/0d72e9801a5c14c10384efcbb6734ceccb74ee63/src/main/bullet/add.cpp#L422) · [角度表示](https://github.com/N0zoM1z0/th04/blob/0d72e9801a5c14c10384efcbb6734ceccb74ee63/src/main/bullet/types.hpp#L6)

## 检查维护中的源码

TH04 项目记录的检查使用指定的 Turbo C++ 4.0J 工具链，编译包含这个辅助函数的弹幕生成代码。

### 2,139匹配字节

编译的子弹生成代码与原始可执行文件的2,139字节部分相匹配。

### 两个干净的构建

两个单独的构建产生相同的匹配字节。 记录的输入哈希值与此处显示的维护源匹配。

记录于 2026 年 10 月 3 日。[阅读编译器检查](https://github.com/N0zoM1z0/th04/blob/0d72e9801a5c14c10384efcbb6734ceccb74ee63/docs/reconstruction/product/TH04_SEMANTIC_BULLET_GENERATION_V1236.md#validation)。

目标身份和分析范围

|  |  |
| --- | --- |
| 原始目标 | `MAIN.EXE`,156,258字节,DOS MZ |
| 辅助函数入口 | `0x2cfc8`（位于 REA 的加载映像中） |
| 原始文件偏移量 | `0x1e7c8`，返回 `address_to_file_offset` |
| 已记录的代码检查 | Notion Desktop 7.6.1 · JavaScript · REA |

编译器检查覆盖完整的弹幕添加代码段，包括这个辅助函数。检查还比较它在 map 文件中的贡献，以及重叠的重定位项。

REA 验证了加载段为 `0x1000` 的静态 DOS 加载映像。地址描述的是分析映像。函数结果在一个 517 字节范围内报告了两段代码，共 503 字节；上方节选的指令都位于这些已报告的代码范围内。

源码名称和编译器检查来自 TH04 检查点 `0d72e980`。较早的重建分析使用项目自身的脚本；这个页面新增了用 REA 对同一原始目标的检查。游戏文件的身份由仓库清单确定，独立原始转储的来源仍未确定。

图中说明的是角度计算。完整游戏的控制、时序和 PC-98 硬件行为，在重建项目中有各自的运行时检查。

## 完整的TH04重建

项目用维护中的 C++ 和汇编构建游戏的四个 DOS 程序。除了弹幕生成，源码还覆盖关卡、Boss、渲染、输入、菜单、结局和分数处理。

[探索 TH04 代码仓库](https://github.com/N0zoM1z0/th04)

- [弹幕生成源码](https://github.com/N0zoM1z0/th04/blob/0d72e9801a5c14c10384efcbb6734ceccb74ee63/src/main/bullet/add.cpp#L292)完整的辅助函数、其他弹幕类型与模板调整。
- [DOS构建和运行指南](https://github.com/N0zoM1z0/th04/blob/0d72e9801a5c14c10384efcbb6734ceccb74ee63/docs/DOS_BUILD.md)构建源代码并在项目的PC-98仿真器工作流中进行测试。
- [当前项目状态](https://github.com/N0zoM1z0/th04/blob/0d72e9801a5c14c10384efcbb6734ceccb74ee63/docs/RE_HANDOFF.md)DOS验证和单独的原生x64移植版。
