OpenRakis/Cryogenic
GitHub: OpenRakis/Cryogenic
基于 Spice86 的《沙丘》(1992) DOS 游戏 C# 渐进式重实现项目,通过混合执行原始汇编与重写的托管代码来实现跨平台运行。
Stars: 50 | Forks: 3



# Cryogenic
[](https://github.com/OpenRakis/Cryogenic/actions/workflows/pr-validation.yml)
[](https://github.com/OpenRakis/Cryogenic/actions/workflows/release.yml)
[](https://github.com/OpenRakis/Cryogenic/actions/workflows/static.yml)
[](https://github.com/OpenRakis/Cryogenic/security/code-scanning)
[](LICENSE)
[](https://github.com/OpenRakis/Cryogenic/releases/latest)
[](https://dotnet.microsoft.com/)
基于 [Spice86](https://github.com/OpenRakis/Spice86) 运行的 Cryo's Dune (CD 版本, 1992) 的 C# 重实现。
**[项目主页](https://openrakis.github.io/Cryogenic/)**
## 目录
- [项目简介](#what-this-is)
- [当前状态](#current-state)
- [工作原理](#how-it-works)
- [代码结构](#code-structure)
- [前置条件](#prerequisites)
- [构建与运行](#build-and-run)
- [截图](#screenshots)
- [贡献](#contributing)
- [资源](#resources)
- [许可证](#license)
## 项目简介
Cryogenic 将 DOS 可执行文件 `DNCDPRG.EXE` 中的 x86 汇编例程替换为 C# 方法。[Spice86](https://github.com/OpenRakis/Spice86) 运行原始二进制文件,并在注册地址将执行重定向到 C# 重写方法。汇编代码和 C# 代码共享相同的模拟内存,因此可以在不破坏程序其余部分的情况下逐个引入重写函数。
### 支持的可执行文件
所需的 `DNCDPRG.EXE` (Dune CD 版本 3.7) 的 SHA256:
```
5f30aeb84d67cf2e053a83c09c2890f010f2e25ee877ebec58ea15c5b30cfff9
```
游戏文件(`DNCDPRG.EXE`, `DUNE.DAT`)受版权保护,必须单独获取。
## 当前状态
游戏可以通过混合 ASM/.NET 执行完全游玩,并支持声音和音乐。
正在进行的工作是将更多的汇编例程转换为 C#。每个转换后的例程都注册为重写函数,并通过运行游戏进行测试。
## 工作原理
1. `Program.cs` 配置命令行参数,并使用预期的 SHA256 校验和调用 `Spice86.Program.RunWithOverrides`。
2. `DuneCdOverrideSupplier` 实例化 `Overrides.Overrides`,后者调用 `DefineOverrides()`。
3. `DefineOverrides()` 使用 `DefineFunction`(替换 CALL 目标)和 `DoOnTopOfInstruction`(内联挂钩)在特定的 segment:offset 地址注册 C# 方法。
4. 在运行时,当模拟 CPU 到达注册地址时,Spice86 执行 C# 方法而不是原始汇编代码。方法通过匹配原始指令的 `NearRet()` 或 `FarRet()` 返回。
5. 游戏状态通过类型化访问器类(`globalsOnDs`, `globalsOnCsSegment0x2538`)读取和写入,这些类直接映射到 DS 和 CS 段基址处的模拟内存。
### 内存段
`Overrides` 类声明了在注册重写时使用的五个段字段:
| 字段 | 值 | 内容 | 已注册的重写 |
|-------|-------|----------|---------------------|
| `cs1` | `0x1000` | 主游戏代码(`DNCDPRG.EXE` 加载于此) | 跨越大多数重写文件的约 80 个函数 |
| `cs2` | `0xD000` | DNVGA — VGA 图形驱动程序 | `VgaDriverCode.cs` 中的 32 个函数,`StaticDefinitions.cs` 中的 1 个 |
| `cs3` | `0xE000` | DNPCS2 / DNSBP — PCM 音频驱动程序 | 无(已声明但在当前重写中未使用) |
| `cs4` | `0xE000` | 保留用于 MIDI 驱动程序内存转储挂钩 | 偏移量 0x02DC 和 0x03EE 处的 2 个内存转储内联挂钩 |
| `cs5` | `0x0800` | 中断处理程序(替换默认 0xF000 的自定义段) | 无(声明用于地址引用) |
`cs3` 和 `cs4` 共享地址 `0xE000`。在 `DriverLoadToolbox` 中,PCM 驱动程序(DNPCS2, DNSBP)加载于 `DRIVER2_SEGMENT = 0xE000`,音乐驱动程序(DNMID)加载于 `DRIVER3_SEGMENT = 0xF000`。`MT32DriverCode.cs` 中的 MT-32 重写使用硬编码的 `0xF000`(3 个函数),而不是任何 `cs` 字段。
### 驱动程序重映射
`DriverLoadToolbox` 临时移除内存分配器的段限制,以便将驱动程序加载到固定地址:
| 驱动程序 | 名称 | 重映射到 | 用途 |
|--------|------|-------------|---------|
| DNVGA | VGA 图形 | `0xD000` | 显示、位块传输、调色板、鼠标光标 |
| DNPCS2 | PC Speaker variant 2 | `0xE000` | PCM 音效 |
| DNSBP | Sound Blaster Pro | `0xE000` | PCM 音效 |
| DNMID | MIDI | `0xF000` | 音乐播放(MT-32, AdLib) |
驱动程序 DN386, DNADL, DNADP, DNADG, DNSDB 未被重映射。在每个驱动程序加载后,`ResetAllocator` 恢复原始分配器状态。重映射挂钩注入在 CS1:E57B(`RemapDrivers`)和 CS1:E593(`ResetAllocator`)。驱动程序函数表在 CS1:E589(`ReadDriverFunctionTable`)自动检测。
## 代码结构
```
src/Cryogenic/
├── Program.cs Entry point; configures args, launches Spice86
├── DuneCdOverrideSupplier.cs Implements IOverrideSupplier; creates Overrides instance
├── DriverLoadToolbox.cs Remaps driver segments to 0xD000/0xE000/0xF000
├── Overrides/
│ ├── Overrides.cs Defines segment fields (cs1–cs5), registers all overrides
│ ├── VgaDriverCode.cs 23 VGA functions: mode setting, blitting, palette, mouse cursor
│ ├── MenuCode.cs 14 menu-type constants, 2 menu state overrides
│ ├── DialoguesCode.cs 3 dialogue functions: counter, init, hex-digit conversion
│ ├── MapCode.cs 5 map/cursor functions and click-handler address constants
│ ├── DisplayCode.cs 11 framebuffer and font selection functions
│ ├── VideoCode.cs 3 HNM video playback functions
│ ├── HnmCode.cs 1 HNM file I/O function (disk read into buffer)
│ ├── TimeCode.cs 2 day/night cycle and hour-of-day functions
│ ├── TimerCode.cs 1 PIT 8254 timer configuration function
│ ├── ScriptedSceneCode.cs 2 cutscene sequence data functions
│ ├── DatastructuresCode.cs 2 memory structure functions (index-to-pointer, sprite lookup)
│ ├── InitCode.cs 1 VGA state initialization function
│ ├── MT32DriverCode.cs 3 Roland MT-32 MIDI driver functions at segment 0xF000
│ ├── UnknownCode.cs 20 partially understood functions (bit ops, memcpy, state flags)
│ └── StaticDefinitions.cs 137+ symbolic names for unoverridden functions (tracing only)
├── Globals/ Typed accessors added manually (Extra* files)
└── Generated/ Auto-generated memory accessors (do not edit)
```
### 重写文件详解
**VgaDriverCode.cs** — 替换加载在段 0xD000 的 DNVGA 驱动程序中的函数。涵盖 VGA 模式设置(`VgaFunc00SetMode`)、帧缓冲区位块传输(`VgaFunc05Blit`)、矩形复制(`VgaFunc12CopyRectangle`)、像素写入(`VgaFunc21SetPixel`)、调色板加载(`LoadPaletteInVgaDac`)、鼠标光标绘制/恢复(`VgaFunc03DrawMouseCursor`, `VgaFunc04RestoreImageUnderMouseCursor`)、缓冲区填充(`VgaFunc08FillWithZeroFor64000AtES`)、纹理生成(`VgaFunc36GenerateTextureOutBP`)、地图块复制(`CopyMapBlock`)以及 VGA 回扫同步(`WaitForRetrace`)。
**MenuCode.cs** — 将 14 个菜单类型常量定义为十六进制偏移量(例如 `MENU_TYPE_DIALOGUE = 0x1F7E`, `MENU_TYPE_GLOBE = 0x204A`, `MENU_TYPE_BOOK = 0x2032`)。包含两个重写:一个用于 CS1:D316 处的菜单动画状态转换,另一个用于 CS1:D41B 处的当前菜单类型设置。
**DialoguesCode.cs** — 三个函数:在 DS:47A8(CS1:A1E8)递增对话计数器,初始化对话状态包括视频索引和面部缩放计时(CS1:C85B),以及将 AL 的低半字节转换为 ASCII 十六进制数字(CS1:A8B1)。
**MapCode.cs** — 定义五种地图模式(平面地图、 globe、游戏内、部队移动、扑翼机)的点击处理程序地址。五个函数设置活动点击处理程序,初始化地图光标类型,并执行从 DS:46E3 开始的 8 字节内存复制。
**DisplayCode.cs** — 管理三个帧缓冲区(前端位于 DBD6,后端位于 DC32,文本位于 DBD8)。包含三个字体选择函数(介绍、菜单、书籍),用于设置字符集地址,以及坐标查找、缓冲区清除和寄存器入栈/出栈辅助程序。
**VideoCode.cs** — 三个用于 HNM 视频播放的函数:从 DS:33A3 处的表进行资源标志查找,播放索引同步,以及通过 DBE7 处的完成标志进行检查。
**HnmCode.cs** — 一个通过 DOS 文件管理器从磁盘读取 HNM 视频数据的函数,更新读取偏移量和缓冲区指针。
**TimeCode.cs** — 从 DS:0002 处的游戏经过时间字的低 4 位提取当前小时。计算下一个可见阳光的日期。
**TimerCode.cs** — 使用控制字节 0x36 将 16 位计数器值写入 PIT 通道 0(端口 0x43/0x40)。主要在游戏退出时调用。
**ScriptedSceneCode.cs** — 从 DS:4854 处的场景序列数据读取 16 位命令并推进序列指针。
**DatastructuresCode.cs** — 通过添加基址偏移量将索引表转换为指针表。通过索引从 DS:DBB0 处的表检索精灵图表资源指针。
**MT32DriverCode.cs** — 段 0xF000 处用于 Roland MT-32 输出的三个函数:主入口点、向端口 0x330 进行 MIDI 字节写入,以及一个未使用的存根。
**UnknownCode.cs** — 20 个具有观察到的行为但用途不明确的函数。包括 DBC8 上的位测试操作、多寄存器移位、8 字节内存复制、状态标志设置器、10 字节结构构建器、DS:47F8 处的 0x5C 字节填充以及三个空操作。
**StaticDefinitions.cs** — 在其地址注册 137+ 个符号函数名(例如 `play_intro`, `open_dune_dat`, `allocator_init`),用于 Spice86 的跟踪输出。这些未被重写;它们在执行日志中提供可读的名称。
## 前置条件
1. [.NET 10 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/10.0)
2. 来自 Dune CD 版本 3.7 的 `DNCDPRG.EXE` 和 `DUNE.DAT`(受版权保护;需单独获取)
验证可执行文件校验和:
```
# Linux/Mac
sha256sum DNCDPRG.EXE
# PowerShell
Get-FileHash DNCDPRG.EXE -Algorithm SHA256
```
## 构建与运行
### 构建
```
git clone https://github.com/OpenRakis/Cryogenic
cd Cryogenic/src
dotnet build
```
### 运行(无音频)
将 `DUNE.DAT` 和 `DNCDPRG.EXE` 放在同一目录下,然后:
```
cd Cryogenic/src
dotnet run --Exe /path/to/DNCDPRG.EXE --UseCodeOverride true -p 4096
```
必须使用 `--UseCodeOverride true`。如果没有它,将不会执行任何 C# 重写。
### 带音频运行
OPL3 音乐和 Sound Blaster PCM:
```
cd Cryogenic/src/Cryogenic
dotnet publish
bin/Release/net10.0/publish/Cryogenic --Exe /path/to/DNCDPRG.EXE --Cycles 8000 --UseCodeOverride true -p 4096 -a "ADP330 SBP2227"
```
## 截图

*扑翼机*

*Chani 对话*

*香料运输界面*

*哈克南场景*

*厄拉科斯地图*

*召唤沙虫*
## 贡献
有关设置说明、编码约定和重写实现工作流,请参阅 [CONTRIBUTING.md](CONTRIBUTING.md)。
贡献领域:
- **逆向工程** — 分析汇编代码,实现 C# 重写,在 `DefineOverrides()` 中注册它们
- **文档** — 添加 XML 文档注释,记录数据结构,解释函数行为
- **测试** — 运行游戏,将行为与原始版本进行比较,报告差异
- **代码质量** — 重构现有重写,改进命名,消除重复
## 资源
- [项目主页](https://openrakis.github.io/Cryogenic/)
- [GitHub 仓库](https://github.com/OpenRakis/Cryogenic)
- [Spice86](https://github.com/OpenRakis/Spice86) — 本项目使用的模拟器和逆向工程工具包
- [发布版本](https://github.com/OpenRakis/Cryogenic/releases)
- [维基百科上的 Dune (1992)](https://en.wikipedia.org/wiki/Dune_(video_game))
## 许可证
Apache License 2.0。详见 [LICENSE](LICENSE)。
版权所有 2021–2024 Kevin Ferrare 及贡献者。
Dune 版权归 Cryo Interactive Entertainment 所有。本项目不隶属于或受 Cryo Interactive Entertainment 或任何版权所有者认可。标签:DOS游戏, Dune, .NET 10, Spice86, 开源游戏, 怀旧游戏, 沙盒, 游戏逆向工程, 程序重构, 重制版