起因:同步 gmweb 时看到 18112 被占,先判断成「启动器在跑」、又改口成「残留 pythonw serve.py」,两次都错 —— 实际是多调用模式下的 gmweb 子进程,进程名就叫 DNF90启动器.exe。 - 排查文档新增小节「18112 上跑的可能是启动器本身」:给出命令行三态判别表 (带 serve.py 参数 = gmweb 服务 / 只有 exe 路径且占 19527 = 主进程 / 父 pid 不存在 = 遗留服务),并附 Get-CimInstance 取证命令 - 明确推论:该进程跑的是启动那一刻的 serve.py;静态文件读盘即生效, 但路由不会,症状是「页面新版、接口旧版,按钮点了没反应」 - AGENTS.md 8.3 同步补上「改 serve.py 后必须让 18112 进程重启」的部署动作
290 lines
16 KiB
Markdown
290 lines
16 KiB
Markdown
# AGENTS.md
|
||
|
||
> **给接续开发的 AI 的入口文件。** 先读完这一页再看代码。
|
||
> 这里只放三样东西:**哪些规则不能破**、**命令怎么跑**、**改完怎么验证**。
|
||
> 背景与完整说明看 [README.md](README.md),历史踩过的坑看 [docs/踩坑记录.md](docs/踩坑记录.md)。
|
||
|
||
---
|
||
|
||
## 0. 三十秒认识这个仓库
|
||
|
||
《地下城与勇士》90 级版本的**本地单机模拟器**,非私服、不联网、不开服。monorepo,四个进程协作:
|
||
|
||
| 组件 | 目录 | 技术栈 | 端口 / 产物 |
|
||
|------|------|--------|------------|
|
||
| 整合启动器 | `launcher/` | Python 3.15 **32 位** + pywebview + PyInstaller | `127.0.0.1:19527`,产物 `launcher/dist/DNF90启动器.exe` |
|
||
| 服务端 | `server/` | Go 1.26.5(模块 `longheng.io/server`) | `127.0.0.1:18111`(Admin),产物 `DNF90Server.exe` 等三个 |
|
||
| 游戏内 GM 控制台 | `gmweb/` | 纯 HTML/CSS/JS + Python 标准库 `serve.py` | `127.0.0.1:18112`,反代 18111 并注入 token |
|
||
| 客户端补丁 | `client-patch/` | C++ / MSVC vcxproj | 产物 `90CN.dll`,接管游戏面板弹 WebView2 窗口 |
|
||
|
||
其余:`pvf/`(PVF 解析,Go,GPL-3.0)、`loadout-tool/`(换装工具,C#)、`gmtool/`(旧版 GM 工具,仅参考)。
|
||
**仓库只存源码**:exe / dll / 美术 / PVF 资源都不在库里,成品走网盘。
|
||
|
||
当前版本 **v2026.11.05**,远程 `origin` = `https://git.hanfuit.com/Lingbao/90CNServer.git`,分支 `main`。
|
||
|
||
---
|
||
|
||
## 1. 开工前必读(按顺序,别跳)
|
||
|
||
1. 本文件(硬规则 + 验证方法)
|
||
2. [docs/踩坑记录.md](docs/踩坑记录.md) —— **至少读 P-001 / P-002 / P-011**,这三条是「不知道就会重新踩一遍」的
|
||
3. 然后按要改的地方选读:
|
||
|
||
| 要改什么 | 读哪份 |
|
||
|---------|--------|
|
||
| 启动器 / GM 工具 / 打包 | [docs/开发文档.md](docs/开发文档.md) |
|
||
| 服务端 GM 接口、拍卖行、背包 | [docs/服务端开发文档.md](docs/服务端开发文档.md) |
|
||
| 客户端补丁 90CN.dll、CRC 自校验 | [docs/客户端补丁与CRC校验.md](docs/客户端补丁与CRC校验.md) |
|
||
| PVF 解析、离线物品名 | [docs/PVF解析开发文档.md](docs/PVF解析开发文档.md) |
|
||
| 发版 | [docs/release/README.md](docs/release/README.md) + [发布说明模板](docs/release/发布说明模板.md) |
|
||
|
||
---
|
||
|
||
## 2. 硬规则(不是代码风格,违反了就出故障)
|
||
|
||
**R1|`90CN.dll` 与 `DNF90Server.exe` 是一对,必须同批构建、同批发布。**
|
||
服务端编译期把 DLL 的 SHA-256 写进常量 `boundDllSHA256`,DLL 一换指纹就不配套,
|
||
客户端能启动但**登录阶段被踢**,服务端日志没有明显报错。只改其中一半是最常见的翻车方式。
|
||
|
||
**R2|改完 DLL 必须先补 CRC32,再重编服务端。**
|
||
`client-patch/tools/build_90cn.ps1` 第三步会自动补;带 `-SkipCrc` 出来的 DLL 不要发布。
|
||
自校验失败的表现是客户端弹框退出(详见 P-003 / P-004:CRC 种子别手填、槽位只在 `.data`/`.rdata` 扫)。
|
||
|
||
**R3|仓库是唯一真源,`Z:\90server` 只是部署副本。**
|
||
调试可以直接在部署目录改,但**改完必须回灌仓库**,并用 `diff -rq` 核对(P-011 就是这么漏过
|
||
`90CN_web.ini` 和整个 `gmweb/` 的)。提交前 `git status` 确认没有该进的 ini / 静态资源留在外面。
|
||
|
||
**R4|`gmweb/` 只能写 IE11 兼容代码。**
|
||
游戏内嵌的是老 WebView 控件:不用 flex、不用 CSS 变量、不用 `transform`。布局统一
|
||
`float` + `box-sizing:border-box`;垂直居中用 `position:absolute; top:50%` + `-ms-transform:translateY(-50%)`。
|
||
改完 CSS/JS **必须把 `?v=YYYYMMDDx` 加一版**,否则 IE11 缓存不刷新。
|
||
|
||
**R5|静态响应统一 `Cache-Control: no-store`。**
|
||
启动器侧已在 `server.py` 的 `_send` 里加,新增路由时别绕过它;否则 WebView2 会缓存旧页面,
|
||
现象是「改了没生效」,会浪费大量时间。
|
||
|
||
**R6|单文件打包会改变 `sys.executable` 的含义。**
|
||
源码运行时它是 `python.exe`,`--onefile` 打包后**是启动器自己**。任何 `Popen([sys.executable, ...])`
|
||
的地方打包前都要重新推一遍;拉起子进程后要**校验副作用**(端口真在监听)而不是校验返回值。
|
||
这就是 P-001 的整条成因链,别再犯。
|
||
|
||
**R7|不提交编译产物。**
|
||
`*.exe`、`*.dll`、`launcher/dist/`、`launcher/build/`、`server/build_verify/`、`__pycache__/`、
|
||
`launcher/dnf_items.json`、`*.pvf`、`*.log` 已在 `.gitignore`。不要为了「方便别人」把它们塞进库。
|
||
|
||
**R8|版本号三处一致**:`launcher/server.py` 的 `CURRENT_VERSION`、发布包根目录 `更新日志.txt`、
|
||
客户端的版本显示。改一漏二会出现「首页还是旧版本号」。
|
||
|
||
**R9|品牌与声明不能动。**
|
||
「本模拟器资源禁止倒卖,请到官方微信公众号或社群下载」「禁止用于开服」这类声明必须保留;
|
||
`pvf/` 源自 pvfine,**GPL-3.0 有传染性**,改它之前先确认许可证影响。
|
||
|
||
---
|
||
|
||
## 3. 环境与构建
|
||
|
||
作者机器上的路径(其他环境自行替换,逻辑不依赖盘符):
|
||
|
||
```
|
||
Python 3.15 32 位 C:\Python315\python.exe # 32 位是硬要求,客户端/工具链都是 32 位
|
||
Go ≥ 1.26.5
|
||
MSVC Visual Studio(client-patch 用 vcxproj)
|
||
部署目录 Z:\90server # 只是副本
|
||
桌面发布包 90CN灵宝小竹整合20261105更新补丁\
|
||
```
|
||
|
||
```bash
|
||
# 服务端(三个 exe)
|
||
cd server
|
||
go env -w GOPROXY=https://goproxy.cn,direct
|
||
go env -w GOSUMDB=sum.golang.google.cn
|
||
go build -buildvcs=false -trimpath -o DNF90Server.exe ./cmd/server/dnf90
|
||
go build -buildvcs=false -trimpath -o DNF90Control.exe ./cmd/server/control
|
||
go build -buildvcs=false -trimpath -o DNF90Doctor.exe ./cmd/server/doctor
|
||
|
||
# 启动器(单文件)
|
||
cd launcher
|
||
C:/Python315/python.exe -m pip install pywebview pythonnet clr-loader pyinstaller
|
||
C:/Python315/python.exe -m PyInstaller --clean --noconfirm "DNF90启动器.spec"
|
||
# → launcher/dist/DNF90启动器.exe
|
||
|
||
# 客户端补丁(Release|Win32,自动补 CRC)
|
||
client-patch\tools\build_90cn.bat
|
||
|
||
# PVF 物品名导出
|
||
cd pvf && go build ./cmd/pvfexport/ && pvfexport -pvf Script.pvf -out dnf_items.json -compact
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 代码地图
|
||
|
||
```
|
||
launcher/
|
||
├── server.py 2809 行,本地 HTTP 服务 + 全部 GM 逻辑 + 窗口启动(核心,占 80% 工作量)
|
||
│ ├── 202-360 日志(_logf) / 单实例互斥 / 多调用模式 run_script_instead()
|
||
│ ├── 369-430 start_gmweb():拉起 18112 + 就绪校验 + 进程内兜底 ← 高危区,见 P-001
|
||
│ ├── 551-686 更新日志:仓库 Release + 本地 txt 合并去重
|
||
│ └── 2481-2800 main() / webview2_installed() / show_window() / 窗口清理
|
||
├── index.html 234 行,六页骨架(开始/GM工具/一键换装/游戏设置/游戏更新/关于我们)
|
||
├── shell.js 804 行,页面路由、更新日志渲染、二维码灯箱
|
||
├── shell.css 447 行,页面样式(WebView2 环境,可用现代 CSS)
|
||
└── qr/ 四张二维码:alipay.png / wechat.png / gzh.jpg / qq.png
|
||
|
||
gmweb/ 游戏内控制台,共约 1100 行
|
||
├── serve.py 187 行,静态托管 + /api/* 反代 18111 + 注入 admin token + /qr/<name> 白名单
|
||
├── index.html / style.css / app.js IE11 约束,别用现代语法
|
||
└── qr/ 同上四张
|
||
|
||
client-patch/
|
||
├── 90CN.cpp 客户端补丁主体(极大,改前先搜函数名定位)
|
||
├── 90CN_web.ini width=800 height=720 close_action=1 ← 内嵌 GM 页尺寸,改这里不是改代码
|
||
└── tools/ build_90cn.ps1(构建+补CRC)/ patch_dll_crc.py(info|verify|patch)
|
||
|
||
server/cmd/server/ dnf90(主体)/ control(控制台)/ doctor(自检)/ launcher / logtool / 等
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 改完怎么验证(**别只看代码,必须跑**)
|
||
|
||
```bash
|
||
# 进程与端口(改启动器后必查:正常只有 19527 + 18112 两个监听)
|
||
tasklist | findstr DNF90
|
||
netstat -ano | findstr "127.0.0.1:19527 127.0.0.1:18112 127.0.0.1:18111"
|
||
|
||
# 启动器现场日志(出问题先看这个,比猜快得多)
|
||
type 启动器日志.txt # exe 同目录;应有 pid / argv / 端口 / 多调用模式
|
||
|
||
# DLL 自校验状态
|
||
C:/Python315/python.exe client-patch/tools/patch_dll_crc.py verify Z:/90server/Client/90CN.dll
|
||
C:/Python315/python.exe client-patch/tools/patch_dll_crc.py info Z:/90server/Client/90CN.dll
|
||
|
||
# 仓库 ↔ 部署目录一致性
|
||
diff -rq gmweb Z:/90server/gmweb
|
||
diff -q client-patch/90CN_web.ini Z:/90server/Client/90CN_web.ini
|
||
```
|
||
|
||
**看界面的推荐做法**(比肉眼读像素可靠,本项目一直这么验):
|
||
|
||
```bash
|
||
# 1) 起静态服务;2) 注入脚本切到目标页;3) 无头浏览器截图/量尺寸
|
||
cd gmweb && C:/Python315/python.exe -m http.server 18198
|
||
# 页面里注入:document.getElementById('pane-about').className='pane on';
|
||
# (启动器则是给对应 .shell-page 加 active)
|
||
msedge --headless=new --disable-gpu --hide-scrollbars --window-size=800,720 \
|
||
--screenshot=shot.png --virtual-time-budget=3000 http://127.0.0.1:18198/_t.html
|
||
# 量尺寸:把 getBoundingClientRect 结果写进 document.title,再用 --dump-dom | grep title
|
||
```
|
||
|
||
尺寸口径:GM 页内嵌窗口 **800×720**;启动器窗口 **1204×717**。改完两边都看一眼有没有滚动条。
|
||
|
||
**分场景清单**
|
||
|
||
| 改了什么 | 必须确认 |
|
||
|---------|---------|
|
||
| 启动器页面/样式 | 打包 → 杀旧进程 → 启动 → 六页都能切、无滚动条、`启动器日志.txt` 无异常 |
|
||
| 启动器 Python 逻辑 | 先跑源码再打包:`cd launcher && DNF90_SERVER_ROOT=Z:/90server DNF90_NO_ELEVATE=1 python server.py`(`SERVER_ROOT` 靠 EXE 同级有 `Server/` 识别,脱离部署目录时用环境变量指定);确认没有新增 `sys.executable` 陷阱 |
|
||
| gmweb 页面 | `?v=` 加一版;800×720 截图三列/四页无溢出;`/api/*` 仍能通 |
|
||
| 服务端接口 | `go build` 通过;`DNF90Doctor.exe` 自检;启动器「GM工具」页能读到数据 |
|
||
| 客户端补丁 | `build_90cn.bat` 全流程(含补 CRC)→ **重编服务端** → 两边一起替换 → 登录验证 |
|
||
| 发版 | 按 [docs/release/README.md](docs/release/README.md) 七步走,见第 6 节清单 |
|
||
|
||
---
|
||
|
||
## 6. 发版检查清单(缺一项就会有人装不上)
|
||
|
||
- [ ] `Client\90CN.dll` 与 `Server\runtime\bin\DNF90Server.exe` **成对且指纹一致**
|
||
- [ ] `gmweb\` 整个目录(含 `qr\` 四张图)
|
||
- [ ] `Client\90CN_web.ini`(缺它回退 1040 宽、`close_action=0`)
|
||
- [ ] 启动器 exe + `更新日志.txt`
|
||
- [ ] 版本号三处一致(`CURRENT_VERSION` / 客户端显示 / 更新日志)
|
||
- [ ] **干净目录**(非中文、无空格)实跑一遍再对外发
|
||
- [ ] Release 说明用模板;附件传 `docs/release/网盘下载.url`(别直传十几 G 的包)
|
||
- [ ] **打 tag 前先确认它指向「实际发布的那个提交」**:`git log --oneline -1 v2026.11.xx`,
|
||
再用 `git ls-remote --tags origin` 看远端解析到的 `^{}` 是不是同一个 sha。
|
||
tag 与代码错位时,Release 的「源码包」是一份旧代码,排查起来极难想到
|
||
|
||
---
|
||
|
||
## 7. 当前状态(2026-11-05)
|
||
|
||
- 版本 `v2026.11.05`;`main` 与 `origin/main` 同步
|
||
- **tag 基线**:`v2026.11.05` 指向 `3b240ec`(annotated),就是本次补丁包对应的代码;
|
||
之后的提交(如 `cc54603` 一键换装)**不在**这个 tag 里,发下一版要另打 `v2026.11.06`
|
||
- Release `2026.11.05更新`(id=3)绑此 tag,附件 `90CN灵宝小竹整合20261105更新补丁.rar`
|
||
- 启动器:六页(开始/GM工具/一键换装/游戏设置/游戏更新[当前|历史]/关于我们),关于页三列二维码
|
||
- gmweb:四页,关于页三列(赞助我们 353 | 官方QQ群 178 | 团队公众号 179,面板宽 712)
|
||
- 已修复:启动器打包后自我套娃开窗(P-001),带单实例互斥 + 运行日志 + WebView2 固定数据目录
|
||
- 已知待办:release 附件 `.url` 里的网盘链接换代时,要同步改 `发布说明模板.md` 表格;`gmtool/` 为旧版遗留,可择机归档
|
||
|
||
---
|
||
|
||
## 8. 提交约定
|
||
|
||
1. 提交信息格式:`范围:做了什么`,例如 `启动器:游戏更新拆分为当前/历史双子页,新增关于我们页`
|
||
2. 一次提交一件事;改了接口就同步改对应文档(服务端接口 → `docs/服务端开发文档.md`)
|
||
3. 只提交源码与文档;产物走 `.gitignore`
|
||
4. 新踩的坑,追加到 `docs/踩坑记录.md`(编号延续 P-0xx,格式照抄现有条目)
|
||
|
||
### 8.1 推送前先 rebase(强制,不是建议)
|
||
|
||
`main` 是**多人共享**的分支,别人(包括另一个 AI)随时可能已经推过。本地 commit 后直接 `push`
|
||
会被拒(non-fast-forward);这时最容易发生的坏操作是「先 reset 再重来」,改动就丢了。
|
||
|
||
固定顺序,别跳步:
|
||
|
||
```bash
|
||
git status -sb # ① 工作区必须干净;看 ahead/behind
|
||
git pull --rebase # ② 把本地提交挪到远端最新之上,有冲突当场解决
|
||
git push origin main # ③ 推
|
||
```
|
||
|
||
- 同步别人的改动时用 `git merge --ff-only origin/main` 或 `git pull --rebase`,**不要** `git reset --hard`
|
||
- rebase 遇到冲突:改完 `git add <文件>` → `git rebase --continue`;搞不定就 `git rebase --abort` 回到原样再来
|
||
- 推送后用 `git log --oneline -3` 确认自己的提交在最新,而不是被顶回旧位置
|
||
|
||
> 真实案例:`cc54603`(别人的一键换装)就是直接推的 `main`,本地因未同步而分叉;
|
||
> 好在工作区干净才能 `--ff-only` 快进过去,否则就得手工合。
|
||
|
||
### 8.2 多人 / 多 AI 协作走分支,别直推 `main`
|
||
|
||
| 场景 | 做法 |
|
||
|---|---|
|
||
| 改动跨多个文件、要评审、或不确定是否上线 | 建分支 `feat/xxx` / `fix/xxx` → 推送 → 发合并请求(Pull Request),由维护者合并 |
|
||
| 同时有两人在改同一个目录(尤其 `gmweb/`) | **必须走分支**,否则后推的人一定覆盖前者 |
|
||
| 单人、改动确定、改完立刻部署验证 | 可以直推 `main`,但仍要遵守 8.1 |
|
||
| 只改文档 | 可以直推 `main`,仍要遵守 8.1 |
|
||
|
||
分支命名:`feat/换装方案`、`fix/启动器端口` 这类「类型/主题」,合并后删掉分支。
|
||
|
||
### 8.3 推完不等于生效
|
||
|
||
仓库改了,`Z:\90server` 和桌面补丁包**还是旧的**(见 R3)。凡是改了 `gmweb/`、启动器或客户端的提交,
|
||
推完要:同步部署副本 → `diff -rq` 核对 → 该重打包的重打包 → 通知实际在玩的人重启启动器。
|
||
|
||
**改 `serve.py` 尤其要注意**:静态文件(`index.html` / `loadout.js` / `.css`)每次请求读盘、替换即生效,
|
||
但 `serve.py` 的路由是**进程启动时注册**的。抓 18112 的那个进程(进程名可能就叫 `DNF90启动器.exe`,
|
||
看命令行有没有 `serve.py --port 18112`)不重启,就会出现「页面是新版、接口是旧版」——
|
||
按钮点着没反应,最难查。所以:同步完先看 18112 是谁在听,该关就关,再重新打开启动器。
|
||
|
||
> 真实案例:`cc54603` 推了 gmweb 一键换装,但部署目录没同步,游戏里看不到新功能;
|
||
> 同时 18112 被一个残留 `serve.py` 占着,启动器看到端口已占用就跳过启动,
|
||
> 于是「改了、推了、也重启了,还是旧页面」(见 P-007、P-008)。
|
||
|
||
---
|
||
|
||
## 9. 不要做的事
|
||
|
||
- ❌ 不要「为了省事」只替换 DLL 或只替换服务端 exe —— 见 R1
|
||
- ❌ 不要用 `!important` 硬压 gmweb 的样式问题 —— 先看是哪个选择器赢了(P-005)
|
||
- ❌ 不要用现代 CSS/JS 写 `gmweb/` —— 见 R4
|
||
- ❌ 不要把 `Z:\90server` 当工作区改完就完事 —— 见 R3
|
||
- ❌ 不要在启动器里加「打包后会指向自己」的子进程调用而不做校验 —— 见 R6
|
||
- ❌ 不要提交 exe/dll/`dnf_items.json`/PVF 资源
|
||
- ❌ 不要把本机测试用的掉落改动带进对外发布的服务端
|
||
- ❌ 不要公开分发或倒卖本仓库代码与资源(`pvf/` 除外,但受 GPL-3.0 约束)
|
||
- ❌ 不要在没 `git pull --rebase` 的情况下直接 `push` —— 见 8.1
|
||
- ❌ 不要在 push 被拒后用 `git reset --hard` 或强推 `--force` 覆盖别人的提交 —— 先 `pull --rebase`
|
||
- ❌ 不要在 tag 指向错误提交的情况下发 Release —— 见第 6 章最后一项
|