Files
90CNServer/AGENTS.md
T
hanfuit b88e1a223b docs:新增远程发版脚本,Release 可用一条命令发布
Gitea 的 Release 全流程走 REST API(实测可行),不用点网页:

- docs/release/publish_release.py(纯标准库,无依赖):校验 tag → 建/更新
  Release → 上传附件 → 回读自检;支持 --dry-run / --draft / --prerelease /
  --expect-commit / --no-asset / --insecure
- 默认先比对本地 tag 与远端 tag 指向的提交,不一致直接拒绝 —— 2026.11.05
  就是 tag 落后 12 个提交,源码包是旧代码且外表看不出来
- .url 里仍是 REPLACE_WITH_YOUR_LINK 时拒绝发版(--force 可跳过)
- 附件同名先删再传,脚本可重复执行(幂等)
- docs/release/README.md 改为「一条命令发版 + 手工兜底」两套流程,附实测记录
- 根 README 发布流程、AGENTS.md 发版清单挂上脚本用法

实测:临时 tag 走完整流程 创建 201 → 上传 201 → 下载 200(内容正确)→
重复执行后只有一个同名附件 → 删除干净(release 204 / tag 已清)。
注意 Gitea 附件字段名必须是 attachment,用 file 会 500 "http: no such file"。
2026-11-05 19:44:14 +08:00

292 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 的「源码包」是一份旧代码,排查起来极难想到
- [ ] 上面两项可以交给脚本,它先比对 tag 再动手:
`python docs/release/publish_release.py --version v2026.11.xx --dry-run`(看过计划后去掉 `--dry-run`)
---
## 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 章最后一项