oLdpZ/dcss-remastered
GitHub: oLdpZ/dcss-remastered
该项目通过 DLL 代理钩子为 Dungeon Crawl Stone Soup 原生桌面版添加自适应区域音乐、分层音效及动态视觉后期处理,全程无需重新编译游戏引擎。
Stars: 0 | Forks: 0
# DCSS Remastered 音频
**为 Dungeon Crawl Stone Soup 带来动态的、感知区域的地图音乐和分层音效——直接添加到原版*预编译*的 Windows Tiles 二进制文件中,无需源代码,也无需重新编译。**
DCSS(Tiles 版,Windows 平台)自带了一个极其简陋的音频后端:它每次只能通过 WinMM 的 `sndPlaySoundW` 调用播放单个 WAV 文件。本项目将这个单通道、仅限事件触发的后端转变为一个完整的**自适应原声音乐**系统——包含针对不同分支的音乐淡入淡出、基于 HP(生命值)的音频闪避以及重叠的音效——**且完全无需修改游戏源码。**
上游游戏源码位于 **[crawl/crawl](https://github.com/crawl/crawl)**。本仓库是一个独立的附加组件;它**不包含**任何 Crawl 的源代码或二进制文件。
## 本项目的不同之处
其他 DCSS 音频项目要么 (a) 需要在启用 `SOUND` 支持的情况下重新编译游戏引擎(如 BindTheEarth、Crawler's Sound Patch),要么 (b) 在 WebTiles 之上的**浏览器**中运行(如 SoundSupport 扩展模块)。这两种方式都很棒——但它们都没有为**原生的、已编译好的桌面版二进制程序**添加自适应原声音乐。
而本项目正是利用 `crawl.exe` 上的 **DLL 代理钩子**实现了这一点——就公开项目而言,这种方法在 DCSS 领域似乎是**独一无二的**。
| 项目 | 运行环境 | 需重新编译? | 区域音乐 | 实现方式 |
|---|---|:---:|:---:|---|
| BindTheEarth / Crawler's Sound Patch | 原生 | ✅ 是 | ❌ | 内置 `SOUND` 系统 |
| SoundSupport (WebTiles 扩展) | 浏览器 (WebTiles) | ❌ | ✅ | JS 扩展模块 |
| **DCSS Remastered Audio (本项目)** | **原生 `crawl.exe`** | ❌ | ✅ | **`winmm.dll` 代理 + 外部混音器** |
## 工作原理
```
crawl.exe (stock binary, unmodified)
│ sndPlaySoundW("....wav")
▼
winmm.dll ◄── proxy DLL (this project, C / x86 / MSVC)
│ │ forwards every unrelated WinMM export to the
│ │ real winmm.dll; intercepts sndPlaySoundW
│ ▼
│ real winmm (SysWOW64) ← native passthrough fallback
▼
\\.\pipe\dcss_audio (named pipe)
│
▼
Audio Director (Python + pygame-ce)
multi-channel mixer:
• per-branch music with crossfade
• HP ducking (music dips on low-HP warning)
• overlapping SFX groups
```
有两种机制负责为 Director 提供数据:
1. **即时 SFX** — Crawl 自身的 `sound +=` 规则(位于 `config/remaster.rc` 中)将日志消息(`You hit`、`open the door`、`climb downwards` 等)映射到微型 WAV 文件的路径。代理程序识别到该路径后,会通过管道转发一个 token。
2. **状态变化(音乐 / 闪避)** — 一个沙盒化的 **Lua `ready()` 钩子**会监控 `you.branch()`、`you.hp()` 以及新游戏过渡事件,随后调用 `crawl.playsound(".../state__branch_.wav")`。这些是**标记路径**,而非真实的音频文件——代理程序会读取*文件名*,将其映射为 `music` / `duck` / `unduck` 命令,Director 随之执行相应的音乐淡入淡出。游戏以为它播放了一段 WAV 音频;但实际上是 Director 替换了背景音乐。
Director 始终具备原生直接透传的后备方案,因此如果程序未运行,音频也能实现平滑降级。
## 仓库结构
```
remaster/
├── proxy/ winmm_proxy.c — DLL forwarder + sndPlaySoundW hook (C, x86, MSVC)
│ build.ps1 / deploy.ps1
├── director/ director.py — orchestrator
│ audio_engine.py — pygame-ce multi-channel mixer
│ pipe_server.py — named-pipe server
│ router.py — path→token→action mapping
│ soundmap.json — event/branch → audio config
│ tests/ — TDD unit tests for the router
├── config/ remaster.rc — sound rules + Lua ready() hook
│ gen_init.py — wires remaster.rc into init.txt
├── tools/ fetch_music.py — downloads CC-BY music (not bundled)
│ make_sfx.py — synthesizes CC0 SFX
│ make_markers.py — generates marker WAVs
│ dump_imports.py — reverse-engineering helper
└── play-remaster.ps1 — one-click launcher (Director + game lifecycle)
```
音频资源和编译好的 `winmm.dll` 特意**没有提交**到仓库中(见 `.gitignore`)——音乐需通过下载获取,而 SFX 和标记文件则在本地生成。
## 安装说明
```
# 1. Install the Director's dependencies
pip install -r director/requirements.txt
# 2. Generate SFX and marker WAVs
python tools/make_sfx.py
python tools/make_markers.py
# 3. Fetch the per-branch music (Kevin MacLeod, CC-BY — see CREDITS.txt)
python tools/fetch_music.py
# 4. Build the proxy DLL (x86 — must match crawl.exe) and deploy it next to crawl.exe
powershell -File proxy/build.ps1
powershell -File proxy/deploy.ps1
# 5. Wire remaster.rc into the game's init.txt
python config/gen_init.py
```
然后使用 **`play-remaster.ps1`**(或 `Play DCSS Remastered.bat` 快捷方式)启动:它会启动 Audio Director,运行游戏,并在退出时自动关闭 Director。
## 逆向工程说明
以下是对原版二进制文件进行分析得出的结论——如果你打算将此方案移植到其他版本,这些信息会非常有用:
- Tiles 版本使用 **WinMM `sndPlaySoundW`** 后端(而非 SDL_mixer):原生仅支持单通道 WAV 格式。这单独的一个调用就是整个钩子的注入点。
- `crawl.exe` 是 **x86** 架构,因此代理 DLL 必须是 x86 架构,且真正的 `winmm.dll` 必须从 **`SysWOW64`**(而不是 `System32`)中复制。
- DLL 导出转发必须使用
`#pragma comment(linker, "/EXPORT:name=winmm_orig.name")` —— MSVC **不会**将 `.def` 语法解析为转发。
- Crawl 的 Lua 配置是**沙盒化**的(无法使用 `io`/`os` 库);与外部进程通信的唯一途径是 `crawl.playsound()` ——这正是标记路径技巧所利用的机制。
- 启动时的警告信息 *"sound will have no effect on this build"*(声音在此版本中无效)具有误导性——实际上音频通过此路径可以完美播放。
## 图形层(视频重塑)
**在上述音频重塑的基础上,添加了动态屏幕分级、色调/去饱和度调整、晕影以及“视觉增强效果”(闪光/震动/泛光脉冲)——理念相同,同样遵循免重编译的限制条件,只不过是应用于视频渲染路径而非音频路径。**
通过上述提到的 token,音频 Director 已经能够获知游戏的当前状态(分支、HP、事件)。图形层复用了这一核心逻辑:**单个 Director 进程可同时驱动音频和视频**,它会将当前的视觉目标参数写入一个微小的共享内存块,由第二个代理 DLL 逐帧读取这些数据,并将其渲染为全屏的后期处理画面。
### 架构
```
crawl.exe (stock binary, unmodified)
│ wglMakeCurrent / glDraw* / ... (OpenGL calls)
│ gdi32!SwapBuffers(hdc) ← frame presentation
▼
opengl32.dll ◄── proxy DLL (this project, C / x86 / MSVC)
│ │ forwards every OpenGL export to the real
│ │ opengl32.dll (367 forwarders, generated);
│ ▼ IAT-hooks gdi32!SwapBuffers in crawl.exe's
│ real opengl32 (SysWOW64) import table
│
▼ (on every hooked SwapBuffers call)
dcss_gfx_state (named shared-memory block, 108 bytes)
▲
│ written once per Director tick
Director (Python) — same process as the audio mixer
VisualRouter: game token → VisualState → shared memory
config: director/visualmap.json (grades, modifiers, events)
│
▼
postprocess.c (GLSL fragment shader, runs inside crawl.exe's
own GL context, right after the hooked SwapBuffers call):
capture back buffer → tint · desaturate · vignette
+ flash / shake / bloom envelopes + unstable/low-HP/death pulses
→ draw full-screen quad → let the real SwapBuffers present it
```
**逆向工程说明:** Tiles 二进制文件**不会**直接调用 `wglSwapBuffers`——它通过 **`gdi32!SwapBuffers`** 呈现帧,这是 GDI 通用实现下的 OpenGL 应用程序翻转后缓冲区的标准方式。这就是为什么钩子的目标指向 `gdi32.dll` 中 `SwapBuffers` 的导入地址表(IAT)条目,而不是 `opengl32.dll` 内部的原因。`crawl.exe` 直接导入了 `OPENGL32.dll`(这使得音频层中经典的 DLL 代理技巧在此处再次奏效),并且在不修补二进制文件的情况下,对 `SwapBuffers` 进行 IAT 钩子拦截是注入后期处理流程的唯一可靠切入点。
### 仓库结构(新增内容)
```
remaster/
├── gfx/
│ ├── gl_proxy.c — OPENGL32.dll proxy: forwards every GL entry point,
│ │ installs the SwapBuffers IAT hook, drives the frame
│ ├── gl_forwarders.h — generated export-forwarding table (gen_forwarders.py)
│ ├── iat_hook.c/.h — import-table scanning + hook installation
│ ├── postprocess.c/.h — GLSL post-process pass (tint/desaturate/vignette/juice)
│ ├── shmem.c/.h — reads the dcss_gfx_state shared-memory block
│ ├── shared_state.h — GfxState struct (must match gfx_state.py's PACK_FORMAT)
│ ├── build.ps1 — builds opengl32.dll (x86 MSVC)
│ ├── deploy.ps1 — copies opengl32.dll next to crawl.exe
│ └── harness/ — headless GL test harness (build.ps1 + gl_harness.c),
│ exercises pp_init()/pp_draw() without launching the game
├── director/
│ ├── gfx_state.py — VisualState + PACK_FORMAT (Python mirror of GfxState)
│ ├── visual_router.py — VisualRouter: token → VisualState, master enable/intensity
│ └── visualmap.json — grades per branch, HP/unstable modifiers, event pulses
```
### 构建与部署
```
# Build the proxy DLL (needs a real 32-bit opengl32.dll from SysWOW64 on PATH/in the
# VS dev prompt environment — build.ps1 sets this up)
powershell -File gfx/build.ps1
# Copy opengl32.dll next to crawl.exe (fails harmlessly if the game is running and
# has the DLL locked — just close the game and re-run)
powershell -File gfx/deploy.ps1
# Optional: headless sanity check without launching the game
powershell -File gfx/harness/build.ps1
gfx/harness/gl_harness.exe # should print "PP_INIT: OK"
```
Director(`director/director.py`)是由 `play-remaster.ps1` 为处理音频而启动的同一个进程——图形处理无需单独的启动器。
### 调优
所有的视觉行为——各分支的颜色分级、色调、去饱和度/晕影强度、低 HP 与不稳定分支的修饰参数、事件脉冲的颜色/强度——都保存在 **`director/visualmap.json`** 中。只需编辑此 JSON 文件并**重启 Director**;无需重新编译 DLL,因为代理程序仅从共享内存中读取数值,其自身绝不嵌入任何游戏状态逻辑。
### 紧急关闭开关
在启动前设置环境变量 **`DCSS_GFX_OFF=1`** 即可完全禁用视频重塑(音频重塑不受影响)——这是经人工验证的直接透传机制。
### 平滑降级
所有故障模式都会回退到**原版、未修改的渲染路径**——绝不会导致崩溃:
- **Director 未运行 / 缺少共享内存** — `shmem_get()` 返回 `NULL`,`pp_draw` 不进行任何绘制。
- **设置了 `DCSS_GFX_OFF=1`** — 代理程序将完全跳过钩子安装及后期处理流程。
- **`visualmap.json` 中 `master.enable = 0`** — `VisualRouter.__init__` 会将 `master_enable = 0` 写入 `VisualState`,而 `pp_draw` 在触及任何 GL 状态之前,就会因其第一个检查项(`!st->master_enable`)提前返回。
- **着色器编译/链接失败** — `pp_init()` 会在编译片段着色器后检查 `GL_COMPILE_STATUS`,并在链接程序后检查 `GL_LINK_STATUS`;任何一步失败都会返回 0,此时 `pp_draw` 对该进程将永久变为空操作。
- **持续出现 GL 错误** — 每次绘制完成后,`pp_draw` 都会在循环中通过 `glGetError()` 清空错误队列(在状态恢复之后执行,以确保清空操作本身不会干扰渲染)。如果连续 60 帧持续出现非零错误,模块将通过 `OutputDebugStringA` 记录一次日志,并设置永久的 `g_disabled` 标志;随后的每一帧都将直接透传。这可以防止驱动程序或状态问题无限期地持续下去。
## 致谢与授权许可
- **音乐** — Kevin MacLeod([incompetech.com](https://incompetech.com)),
采用知识共享 **CC-BY 4.0** 许可。每首曲目的署名详见 [`CREDITS.txt`](CREDITS.txt))。
此处不提供分发;需通过 `fetch_music.py` 进行下载。
- **音效** — 为本项目程序化合成(`tools/make_sfx.py`),
以 **CC0 / 公共领域** 许许发布。
- **Dungeon Crawl Stone Soup** 是一个独立的 GPL-2.0 项目 — [crawl/crawl](https://github.com/crawl/crawl)。
本附加组件不包含该项目的任何代码或二进制文件,仅通过 WinMM ABI 和命名管道与原版游戏进行通信。
本附加组件的自带代码按“原样”提供,仅供**个人非商业用途**使用 —
详见 [`LICENSE`](LICENSE)。
标签:DLL代理, Hook注入, rizin, 云资产清单, 安全意识培训, 客户端加密, 游戏模组, 逆向工具, 逆向工程, 音频系统