jaenster/d2-dedicated-server
GitHub: jaenster/d2-dedicated-server
一个用 Zig 编写的自托管、云原生暗黑破坏神 II 1.14d 专用游戏服务器及 Battle.net 大厅领域服务器,旨在作为现代版 PvPGN 替代方案。
Stars: 3 | Forks: 0
# d2-dedicated-server — 无头、云原生的暗黑破坏神 II 1.14d 专用游戏服务器 + 大厅领域服务器
一个**自托管、开源的暗黑破坏神 II 专用游戏服务器**,适用于官方零售版 **1.14d**,
外加一个全新干净的 **Battle.net realm server** — 一个现代的、**云原生的 PvPGN
替代品**,你可以使用 Docker / Kubernetes 来运行。全部使用 **Zig** 编写。
它将单文件的暗黑破坏神 II 1.14d `Game.exe` 变成了一个 **无头专用游戏
服务器**,就像旧版本使用分离的 DLL 所做的那样 — 只不过在 1.14d 中,整个
服务器引擎(`Fog::QServer` + `D2Game::Game::Server`)被静态链接在
`Game.exe` 内部。我们没有重新实现它;而是从注入的
Zig DLL 中**驱动真实的引擎**。内置的大厅领域服务器(`realmd`)取代了 **PvPGN**(bnetd + d2cs + d2dbs),
因此**未经修改的官方零售版客户端**可以直接登录并进行完整的游戏 — 无需任何客户端 Mod。
专为**容器和 Kubernetes** 打造:Windows 的 `Game.exe` 运行在 wine 下,完全
无头 — 没有 GUI,没有 X,没有显示器 — 并且 `realmd` 作为一个仅有几 MB 的静态二进制文件发布。
一个具备容量感知的**游戏服务器集群**会注册到大厅领域服务器;状态数据存储在
**Postgres + Redis**(或仅文件系统)中;健康/就绪探针和优雅
关闭使其成为一流的**云原生**工作负载。日志流式传输到 **stdout**
(可选 JSON)— `docker logs` / journald / `kubectl logs` 可以直接正常工作。一个
`docker compose up` 就可以在本地运行整个技术栈;清单文件位于 [`deploy/`](deploy/)。
## 组件
```
unmodified 1.14d client (GUI)
| BNCS / MCP / BNFTP
v
┌─────────────────────────────────┐ gs-link (6115) ┌──────────────────────┐
│ realmd (native Zig binary) │ <───────────────────── │ headless Game.exe │
│ bnetd 6112 login + version MPQ │ │ + d2gs.dll (injected)│
│ d2cs 6113 realm / create-join │ char save (d2dbs) │ drives QServer/D2Game│
│ d2dbs 6114 character saves │ <───────────────────── │ listens on :4000 │
│ gslink 6115 GS dispatch │ └──────────────────────┘
└─────────────────────────────────┘ ^
^ │ game traffic (:4000)
└────────────────── the client connects directly ───┘
```
- **realm server** (`src/realm/server/`,原生;`realmd` 二进制文件):一个单一的二进制文件
取代了 pvpgn 的 bnetd + d2cs + d2dbs,外加一个供注入的服务器连接的 GS-link。
可插拔的持久化(fs/redis/pg)能够在重启后存活;支持多实例。参见
[`src/realm/server/README.md`](src/realm/server/README.md)。
- **game server** (`src/d2gs.zig` + `src/engine/` + `src/realm/client/`):注入的
DLL,它将 `Game.exe` 作为无头专用服务器启动,并通过引擎的 realm callback table 将其桥接到 realmd。
参见 [`src/engine/README.md`](src/engine/README.md)。
- **shared realm contract** (`src/realm/shared/`):两端共同导入的
d2cs↔d2gs 通信协议(`realm_shared` 模块),因此客户端和服务器在通信上天然保持一致。
## 架构
完整的模型位于 [`docs/architecture/`](docs/architecture/)(LikeC4)中,并且可以
使用 `npx likec4 start docs/architecture` 进行实时浏览。渲染视图:
**全景图** — 原生 D2 模块 + d2gs 项目组成部分:

**GS 集群** — realmd 的 `gslink` 维护着一个包含*多个*游戏服务器的注册表。一个 GS
向外连接(`:6115`),自报其公共的 `:4000` + `gsid` + 容量
(ADDRINFO);`CREATE` 会路由到负载最小的 GS,`JOIN` 会路由到所拥有的 `gsid`。
持久化外观会分发给 `fs` / `redis`(临时)/ `pg`(持久):

**Kubernetes 拓扑** — 无状态的 realmd 位于 LoadBalancer(受 `/readyz` 控制)
之后,带有 Redis + Postgres 后端,以及一个 GS StatefulSet,其 Pod 在
其 Node IP 上绑定 `hostPort 4000`。玩家通过 LB 登录,然后*直接*
与 `nodeIP:4000` 进行游戏流量通信:

## 注入工作原理
```
Game.exe --(loads)--> dbghelp.dll (our proxy) --(--loaddll)--> d2gs.dll [+ your mod DLLs]
|
DllMain spawns serverThread:
bootstrapRealmServer() + realm callbacks
loop: HandleAnyIncomingPacket
+ TickAllGames + DispatchAndCleanup
```
- **传递:** `Game.exe` 为其崩溃处理程序加载 `dbghelp.dll`。我们的代理
转发了真实的导出函数,并对通过
`--loaddll ` 传递的 DLL 进行了 `LoadLibrary` — 这就是 `d2gs.dll` 进入的方式。不需要对磁盘上的 `Game.exe` 打补丁。
- **无头模式:** `--headless` 字节补丁(`src/runtime/headless.zig`)存根了
渲染器/媒体加载器并隐藏了窗口,因此主机可以在没有显示器的情况下存活。
- **服务器 tick:** 镜像了引擎自己的 `QSERVER_CoopThreadMain` — 排空入站
数据包,tick 所有游戏,然后**刷新排队的出站数据包**(
`DispatchAndCleanup` 步骤使得加入的客户端能够真正取得进展)。
## 加载 Mod / 服务器修改
代理会加载你通过 `--loaddll` 传递的**任何** DLL,该参数可重复使用。一个服务器 mod
只是另一个在它的 `DllMain` 中 hook/patch 引擎的注入 DLL:
```
wine Game.exe ... --loaddll Z:\path\d2gs.dll --loaddll Z:\path\yourmod.dll --d2gs ...
```
每个 DLL 都在进程内运行,拥有在其固定地址(镜像
基址 `0x00400000`,无 ASLR)上对引擎的完全访问权限。`d2gs.dll` 只是第一个这样的 DLL。
在**容器**中,将 mod DLL 放在 `/mods` 目录下(每个都在 `d2gs.dll` 之后被 `--loaddll` 加载)
或者在 `D2GS_EXTRA_DLLS` 中列出它们;通过将其挂载到 `/moddata` —
在启动时,它会合并到位于可写工作目录中的只读游戏安装目录中,从而覆盖额外的**数据**(mod MPQ,
或带有 `D2GS_EXTRA_ARGS="-direct -txt"` 的松散 `data/` 目录树)。
## 构建
```
zig build # -> zig-out/bin/{dbghelp.dll, d2gs.dll, ver-IX86-1.dll} (x86-windows)
# + zig-out/bin/realmd (native host binary)
```
## 运行完整技术栈
### 快速指南:使用 Docker Compose 的云端技术栈
与你在 Kubernetes 上运行的相同的 `realmd` 镜像和后端,都集中在一个主机上 — `realmd`
带有 **Postgres**(持久角色存档)+ **Redis**(临时会话/游戏)。完整
文件位于 [`deploy/compose.yaml`](deploy/compose.yaml)(它还有一个受配置文件控制的 `gs`
游戏服务器服务);核心部分仅是:
```
services:
redis:
image: redis:7-alpine
postgres:
image: postgres:16-alpine
environment: { POSTGRES_USER: realmd, POSTGRES_PASSWORD: realmd, POSTGRES_DB: realmd }
realmd:
build: { context: ., dockerfile: deploy/Dockerfile, target: realmd }
depends_on: [redis, postgres]
environment:
REALMD_DURABLE_STORE: pg # character saves
REALMD_EPHEMERAL_STORE: redis # sessions + games (native TTL)
REALMD_REDIS_ADDR: redis:6379
REALMD_PG_DSN: postgres://realmd:realmd@postgres:5432/realmd
REALMD_LOG_JSON: "1"
ports: ["6112:6112", "6113:6113", "6114:6114", "6115:6115", "18080:8080"]
```
```
docker compose -f deploy/compose.yaml up --build
curl localhost:18080/readyz # 200 once Postgres + Redis are reachable
# 同时在 in-compose 中运行 headless game server(需要你的 D2 1.14d 安装):
D2GS_GAME_SRC=/path/to/d2-1.14d docker compose -f deploy/compose.yaml --profile gs up --build
```
`gs` 服务受配置文件控制,因为游戏文件是专有的(通过
`D2GS_GAME_SRC` 挂载它们)。对于特定于机器的微调,请保留一个被 gitignored 的
`deploy/compose.local.yaml` 并添加 `-f deploy/compose.local.yaml`。
### 手动(原生 realmd + wine GS)
```
# 1) realm server(原生;data dir 包含 accounts/chars/games)
REALMD_DATA_DIR=./realmd-data ./zig-out/bin/realmd
# 2) headless game server(wine),向 realmd 的 gs-link 注册
wine Game.exe -w -nosound --headless --loaddll Z:\...\d2gs.dll \
--d2gs --d2gs-boot --realm --create-games \
--d2cs 127.0.0.1:6115 --d2dbs 127.0.0.1:6114
# 3) 一个 real client(将其 bnet gateway 指向 realmd,然后正常登录)
wine Game.exe -w -skiptobnet --loaddll Z:\...\d2gs.dll --d2gs --bypass-checkrev
```
`./run.sh` 会构建 DLL 并为纯注入的情况组装一个 wine 测试目录。完整的创建+加入流程有一个端到端的测试:
[`tools/realmd-test/e2e-game.sh`](tools/realmd-test/e2e-game.sh)(启动 realmd + GS,
驱动两个客户端进行创建 + 加入,断言两个角色都已加载;需要 wine +
通过 `E2E_GAME_SRC` 提供的真实 1.14d 安装)。
## 标志(传递给 Game.exe,由我们的 DLL 读取)
| 标志 | 效果 |
|-|-|
| `--loaddll ` | (代理) LoadLibrary 一个注入的 DLL;可重复 |
| `--d2gs` | 附加 + 记录日志;安装崩溃/停止/多实例保护 |
| `--headless` | 应用生存/无显示器补丁 |
| `--d2gs-boot` | 运行引擎引导 + tick 循环(专用服务器) |
| `--realm` | 在 realm 模式下进行引导(注册 realm callback table) |
| `--create-games` | 加载数据表以便引擎可以创建游戏 |
| `--realmd ` | 连接到一个 realm server,推导 gs-link (`:6115`) + d2dbs (`:6114`);DNS 可用。环境变量:`REALMD_HOST` |
| `--d2cs ` | 连接到 realmd 的 gs-link 以进行创建/加入调度(覆盖 `--realmd`) |
| `--d2dbs ` | 从 realmd 的 d2dbs 获取角色存档(覆盖 `--realmd`) |
| `--gs-addr ` | 客户端拨号连接此 GS 游戏的公共地址(自报告给 realmd)。环境变量:`D2GS_GS_ADDR` |
| `--max-games ` | 此 GS 向 realmd 播报的容量。环境变量:`D2GS_MAX_GAMES` |
| `--bypass-checkrev` | (客户端) 跳过 bnet 版本检查 |
| `--screenshot` | (客户端) 每 3 秒截取一次屏幕截图(有头调试) |
| `--auto-login ` | (客户端) 驱动登录 → 角色选择 → 创建游戏 |
| `--auto-join ` | (客户端) 驱动登录 → 角色选择 → 加入游戏 |
| `--pkttrace` | 记录每个 `:4000` 客户端↔GS 数据包 ID(详细模式) |
| `--suppress-halts` | 吞掉引擎断言而不是退出(调试用) |
## 布局
```
src/
dbghelp.zig dbghelp.dll proxy — injection foothold (--loaddll loader)
d2gs.zig d2gs.dll entry — DllMain, flag parsing, server thread + tick loop
log.zig logger (stdout + file)
realm/ the realm link — both ends + their shared contract [README]
shared/ d2cs<->d2gs wire protocol (the realm_shared module)
client/ GS-side clients of the realm (d2cs/d2dbs, join context) [README]
server/ realm server / realmd binary: bnetd+d2cs+d2dbs+gs-link,
pluggable store (fs/redis/pg), health, graceful shutdown [README]
engine/ bindings into Game.exe's own engine + realm callback table [README]
runtime/ in-process machinery: byte-patches, hooks, fastcall, diagnostics [README]
test/ client-driving test harnesses (auto-login/join, screenshots) [README]
checkrev/ CheckRevision.dll producer for the version-check MPQ [README]
deploy/
Dockerfile multi-target image: `--target realmd` (scratch) | `--target gs` (wine)
compose.yaml local stack — realmd + Postgres + Redis (+ `gs` profile)
realmd.yaml k8s — realmd Deployment + redis/pg + probes + LoadBalancer
gs.yaml k8s — game-server StatefulSet (hostPort 4000 + node IP)
gs-entrypoint.sh headless-wine launcher (assembles game dir, loads mods)
docs/architecture/ LikeC4 model (diablo2.c4 + cloud.c4) + exported diagrams (img/)
```
每个 `src/*` 目录都有自己的 `README.md`。其他文档:
[`REALM.md`](REALM.md), [`REALMD.md`](REALMD.md), [`VERIFY.md`](VERIFY.md), [`LEGAL.md`](LEGAL.md)。
## 状态
**正在工作**(已在 wine 下的未修改零售版 1.14d `Game.exe` 上测试):
- ✅ 注入(`dbghelp` 代理 → `--loaddll` → `DllMain`)+ 无头生存。
- ✅ 专用服务器启动,监听 `:4000`,稳定地进行 tick。
- ✅ `realmd`:真实客户端登录,通过版本检查(BNFTP MPQ +
CheckRevision),选择 realm,列出 + 加载角色。
- ✅ 创建 + 加入一个调度到无头 GS 的游戏。
- ✅ **角色在游戏世界中出生**(从 d2dbs 加载,完整的生命/法力,可游玩)。
- ✅ **多人游戏** — 两个真实客户端在一个游戏中,对彼此可见。
**待完善之处 / 后续计划:**
- ⏳ 默认编译了详细的加入诊断(`joindiag`);`pkttrace` 受控制。
- ⏳ 在同一个 wineprefix 中的两个有头客户端可能会在第二个客户端启动时
触发 bnet 网关列表解析器(间歇性发生);e2e 测试会重试。
- ⏳ 增强跨重启和多个并发游戏的稳定性;使用正确的引擎初始化 hook 替换
固定的初始化延迟。
## 许可证与法律
代码:[MIT](LICENSE)。此处不分发任何 Blizzard 游戏文件 — 请自带你自己的
合法暗黑破坏神 II 副本。非官方版本,与 Blizzard 无关。参见
[`LEGAL.md`](LEGAL.md)。
标签:Wine, Zig, 子域名突变, 搜索引擎查询, 暗黑破坏神2, 游戏服务器, 版权保护, 请求拦截