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, 云资产清单, 安全意识培训, 客户端加密, 游戏模组, 逆向工具, 逆向工程, 音频系统