Implement 40250 classic client port
This commit is contained in:
@@ -0,0 +1,393 @@
|
||||
<!-- generated-by: gsd-doc-writer -->
|
||||
# Android 真机测试指南
|
||||
|
||||
本文说明如何在 Android 真机上安装 `mtgodot-poc`、生成并导入资源包、验证资源挂载,以及进行 40250 客户端测试。
|
||||
|
||||
## 1. 当前测试范围与已知前置条件
|
||||
|
||||
当前 Android 导出目标是 `arm64-v8a`,APK 包名为 `org.internal.mtgodotpoc`,最低 Android SDK 为 24。APK 只包含代码和 Godot 工程资源;Metin2 的 `assets/` 与 `bgm/` 不打进 APK,测试时必须单独导入 `assets.zip`。
|
||||
|
||||
当前仓库的资源部署存在一个需要特别注意的限制:
|
||||
|
||||
- `export-android.sh --install` 会把资源推到 `/sdcard/Android/data/org.internal.mtgodotpoc/files/assets.zip`。
|
||||
- `project/asset_pack.gd` 首先查找 `user://assets.zip`,当前没有显式把上述外部目录加入候选路径。
|
||||
- 因此,Debug 真机测试应把外部目录中的压缩包再复制到应用私有 `files/` 目录,确保客户端的 `user://assets.zip` 能命中。
|
||||
|
||||
另外,40250 classic 网络栈当前由进程环境变量 `MT_PROTOCOL=classic` 选择。Android 通过 Activity 启动 APK 时不能直接继承 Mac 终端的环境变量;如果 APK 尚未增加 Android 侧的协议配置入口或将 classic 设为移动端默认值,则只能完成 APK、资源和 UI 验证,不能据此宣称已经完成真实 40250 登录验收。
|
||||
|
||||
公会功能按当前项目范围暂缓;龙魂玩法不属于目标 40250 服务器功能,不纳入 Android 验收。
|
||||
|
||||
## 2. 主机环境
|
||||
|
||||
以下版本来自当前 Android Gradle 配置和构建脚本:
|
||||
|
||||
| 项目 | 要求 |
|
||||
|---|---|
|
||||
| 主机 | macOS,Apple Silicon 构建脚本已按当前机器验证 |
|
||||
| Godot | `4.7.1.stable`,并安装对应 Android 导出模板 |
|
||||
| Android SDK | compile/target SDK `36`,Build Tools `36.1.0` |
|
||||
| Android NDK | `29.0.14206865` |
|
||||
| Java | Gradle 配置最低 Java 17;当前脚本默认使用 OpenJDK 21 |
|
||||
| Android 手机 | `arm64-v8a`,建议使用 Vulkan 设备 |
|
||||
| 磁盘 | 至少预留 APK、资源包和临时构建空间;当前 `assets.zip` 约 2.1–2.2 GB |
|
||||
|
||||
进入仓库并设置环境变量。路径按本机安装位置调整:
|
||||
|
||||
```bash
|
||||
cd /Users/shenlei/Work/mt/mtgodot-poc
|
||||
|
||||
export ANDROID_SDK_ROOT="$HOME/Library/Android/sdk"
|
||||
export ANDROID_HOME="$ANDROID_SDK_ROOT"
|
||||
export ANDROID_NDK_ROOT="$ANDROID_SDK_ROOT/ndk/29.0.14206865"
|
||||
export JAVA_HOME="/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home"
|
||||
export GODOT="/opt/homebrew/bin/godot"
|
||||
export ADB="$ANDROID_SDK_ROOT/platform-tools/adb"
|
||||
```
|
||||
|
||||
先确认工具存在:
|
||||
|
||||
```bash
|
||||
"$GODOT" --version
|
||||
java -version
|
||||
"$ADB" version
|
||||
test -f "$ANDROID_NDK_ROOT/build/cmake/android.toolchain.cmake"
|
||||
```
|
||||
|
||||
如果 Android SDK/NDK 尚未安装,可在 Android Studio 的 SDK Manager 中安装,或使用 `sdkmanager` 安装与上表一致的版本。不要只安装一个较旧 NDK 后直接构建;Gradle 模板要求的 NDK 版本以 `project/android/build/config.gradle` 为准。
|
||||
|
||||
Godot 还需要安装与 `4.7.1` 完全匹配的 Android 导出模板。可在 Godot 的“编辑器设置 → 导出 → 导出模板”中安装。
|
||||
|
||||
首次导出前生成 Debug keystore:
|
||||
|
||||
```bash
|
||||
./gen-debug-keystore.sh
|
||||
```
|
||||
|
||||
## 3. 连接 Android 真机
|
||||
|
||||
在手机上完成以下设置:
|
||||
|
||||
1. 设置 → 关于手机 → 连续点击版本号,开启开发者选项。
|
||||
2. 开发者选项中开启“USB 调试”。
|
||||
3. USB 连接模式选择“文件传输/MTP”。
|
||||
4. 首次连接时,在手机上确认“允许 USB 调试”。
|
||||
|
||||
在 Mac 上检查设备:
|
||||
|
||||
```bash
|
||||
"$ADB" kill-server
|
||||
"$ADB" start-server
|
||||
"$ADB" devices -l
|
||||
```
|
||||
|
||||
期望看到:
|
||||
|
||||
```text
|
||||
<设备序列号> device
|
||||
```
|
||||
|
||||
状态为 `unauthorized` 时,解锁手机并接受 RSA 授权;没有设备时,优先检查 USB 线、USB 模式、开发者选项和手机是否允许该电脑调试。
|
||||
|
||||
## 4. 生成资源包
|
||||
|
||||
资源包必须从与 APK 同一份工作树生成。执行:
|
||||
|
||||
```bash
|
||||
./pack-assets.sh
|
||||
```
|
||||
|
||||
该脚本会:
|
||||
|
||||
1. 执行 `bake_asset_index.gd`,生成 `assets/asset_index.txt`。
|
||||
2. 将 `assets/` 和 `bgm/` 打成 `build/export/assets.zip`。
|
||||
3. 使用 `zip -0` 原样存储,不压缩 `.dds`、`.gr2` 等大型资源。
|
||||
4. 排除 `.git`、`.DS_Store` 和 `assets/.gdignore`。
|
||||
|
||||
检查资源包:
|
||||
|
||||
```bash
|
||||
ls -lh build/export/assets.zip
|
||||
unzip -l build/export/assets.zip | rg 'assets/asset_index\.txt|assets/|bgm/' | head -n 20
|
||||
```
|
||||
|
||||
不要手工解压到 APK,也不要把完整 `assets/` 强行放进 `res://`。客户端启动时通过 `ProjectSettings.load_resource_pack()` 挂载 zip,挂载成功后资源以 `res://assets/...` 和 `res://bgm/...` 访问。
|
||||
|
||||
资源发生变化后必须重新执行 `./pack-assets.sh`。只重新导出 APK 不会更新已经生成的 `assets.zip`。
|
||||
|
||||
## 5. 构建 APK
|
||||
|
||||
建议先单独构建,不要在第一次测试时直接使用 `--install`:
|
||||
|
||||
```bash
|
||||
./export-android.sh Debug
|
||||
```
|
||||
|
||||
构建成功后应得到:
|
||||
|
||||
```text
|
||||
build/export/mtgodot-poc.apk
|
||||
```
|
||||
|
||||
检查 APK 和 native library:
|
||||
|
||||
```bash
|
||||
ls -lh build/export/mtgodot-poc.apk
|
||||
unzip -l build/export/mtgodot-poc.apk | rg 'lib/arm64-v8a/.*\.so'
|
||||
```
|
||||
|
||||
应至少包含 `lib/arm64-v8a/` 下的 Godot Android 库和 `libmtgodot` 扩展库。
|
||||
|
||||
## 6. 安装 APK 并导入资源
|
||||
|
||||
### 6.1 安装 APK
|
||||
|
||||
```bash
|
||||
"$ADB" install -r build/export/mtgodot-poc.apk
|
||||
```
|
||||
|
||||
`-r` 会保留应用数据。若要完全清理测试数据,可选执行:
|
||||
|
||||
```bash
|
||||
"$ADB" shell pm clear org.internal.mtgodotpoc
|
||||
```
|
||||
|
||||
清理后必须重新导入资源包。
|
||||
|
||||
### 6.2 先推送到 Android 外部目录
|
||||
|
||||
```bash
|
||||
PKG=org.internal.mtgodotpoc
|
||||
EXT_DIR="/sdcard/Android/data/$PKG/files"
|
||||
|
||||
"$ADB" shell mkdir -p "$EXT_DIR"
|
||||
"$ADB" push build/export/assets.zip "$EXT_DIR/assets.zip"
|
||||
"$ADB" shell ls -lh "$EXT_DIR/assets.zip"
|
||||
```
|
||||
|
||||
### 6.3 Debug 测试复制到 Godot `user://`
|
||||
|
||||
当前 `Debug` APK 可使用 `run-as` 把外部文件复制到应用私有 `files/` 目录:
|
||||
|
||||
```bash
|
||||
"$ADB" shell run-as "$PKG" sh -c \
|
||||
"cp '$EXT_DIR/assets.zip' files/assets.zip"
|
||||
|
||||
"$ADB" shell run-as "$PKG" ls -lh files/assets.zip
|
||||
```
|
||||
|
||||
最后一条命令能看到约 2.1–2.2 GB 的文件,才继续启动客户端。这个步骤是当前代码路径下的关键步骤;只执行 `adb push` 到 `/sdcard/Android/data/.../files/`,不能替代它。
|
||||
|
||||
如果 `run-as` 报告应用不可调试,确认安装的是 `Debug` APK,而不是 Release APK。Release 流程需要先修改资源查找/部署实现,使客户端明确支持外部目录,不能假设 `run-as` 可用。
|
||||
|
||||
### 6.4 资源更新
|
||||
|
||||
客户端退出后,只更新资源包时执行:
|
||||
|
||||
```bash
|
||||
./pack-assets.sh
|
||||
|
||||
"$ADB" push build/export/assets.zip "$EXT_DIR/assets.zip"
|
||||
"$ADB" shell run-as "$PKG" sh -c \
|
||||
"cp '$EXT_DIR/assets.zip' files/assets.zip"
|
||||
```
|
||||
|
||||
资源挂载只在 `client_main.gd::_ready()` 中执行,因此更新后必须重启客户端,不能只切回前台:
|
||||
|
||||
```bash
|
||||
"$ADB" shell am force-stop "$PKG"
|
||||
```
|
||||
|
||||
## 7. 启动客户端并确认资源挂载
|
||||
|
||||
清空旧日志后启动:
|
||||
|
||||
```bash
|
||||
"$ADB" logcat -c
|
||||
"$ADB" shell am start -n \
|
||||
org.internal.mtgodotpoc/com.godot.game.GodotApp
|
||||
```
|
||||
|
||||
实时查看日志:
|
||||
|
||||
```bash
|
||||
"$ADB" logcat -v time -s godot GodotError AndroidRuntime
|
||||
```
|
||||
|
||||
资源挂载成功的关键日志是:
|
||||
|
||||
```text
|
||||
[AssetPack] mounted .../assets.zip
|
||||
```
|
||||
|
||||
以下日志表示资源没有被找到或挂载失败:
|
||||
|
||||
```text
|
||||
[AssetPack] 找不到 assets.zip
|
||||
[AssetPack] load_resource_pack 失败
|
||||
[client] 资源目录不存在
|
||||
```
|
||||
|
||||
启动验收至少包含:
|
||||
|
||||
- 登录界面正常出现,而不是黑屏或立即退出。
|
||||
- 登录背景、控件和字体资源正常显示。
|
||||
- 选人页能加载职业模型、背景和界面资源。
|
||||
- 进入游戏后能看到地图、地形、建筑、树木或角色模型。
|
||||
- 日志中没有持续出现资源文件缺失、`FileAccess` 打开失败或 native library 加载失败。
|
||||
|
||||
可保存截图作为测试证据:
|
||||
|
||||
```bash
|
||||
mkdir -p build/export/android-evidence
|
||||
"$ADB" exec-out screencap -p > build/export/android-evidence/launch.png
|
||||
```
|
||||
|
||||
## 8. 40250 服务器连接配置
|
||||
|
||||
### 8.1 手机网络要求
|
||||
|
||||
手机必须能直接访问认证服/游戏服的 IP 和端口:
|
||||
|
||||
- 手机与局域网测试服连接到同一网络,或服务器开放公网访问。
|
||||
- 客户端不能使用 `127.0.0.1` 或 `localhost` 指向 Mac;在手机上它们指向手机自身。
|
||||
- 防火墙和 FreeBSD jail 的端口映射必须允许手机来源地址访问。
|
||||
- 服务器端口以实际 `CONFIG` 为准,不要照抄示例端口。
|
||||
|
||||
### 8.2 `serverlist.txt`
|
||||
|
||||
客户端启动时会优先读取 `res://serverlist.txt`;没有该文件时使用 `project/net/serverinfo.gd` 中的内置默认值。真实 40250 测试应在导出前准备 `project/serverlist.txt`,格式为 TSV:
|
||||
|
||||
```text
|
||||
# 名称\t认证地址\t认证端口\t游戏地址\t游戏端口\t频道列表\t端口步长\t会徽端口
|
||||
龙魂40250\t<服务器IP>\t<auth-port>\t<服务器IP>\t<channel1-port>\t1,2,3,4\t1\t0
|
||||
```
|
||||
|
||||
例如 40250 仓库脚本中的默认 channel1 端口与当前客户端内置默认值并不相同,必须以目标 FreeBSD 服务器的 `CONFIG` 和实际监听结果为准。
|
||||
|
||||
<!-- VERIFY: 真实 40250 服务器 IP、auth/game 端口、频道数量和防火墙策略必须由当前 FreeBSD 部署确认。 -->
|
||||
|
||||
### 8.3 classic 协议选择限制
|
||||
|
||||
当前 native 代码仅在进程环境变量满足以下条件时切换 40250 classic 后端:
|
||||
|
||||
```text
|
||||
MT_PROTOCOL=classic
|
||||
```
|
||||
|
||||
在 Mac 上运行 Godot 时可以通过环境变量设置;但 Android APK 是由 Activity 启动的,普通的 `adb shell am start` 不会把 Mac 的环境变量传入 APK。因此,真实 Android 40250 登录测试前必须确认以下任一项已经完成:
|
||||
|
||||
1. Android 版本把 classic 设置为默认网络后端;
|
||||
2. 客户端增加可持久化的协议配置并在 Android 导出中设为 `classic`;或
|
||||
3. Android 测试包提供协议选择入口。
|
||||
|
||||
不要把下面的命令当作可靠解决方案:
|
||||
|
||||
```bash
|
||||
adb shell MT_PROTOCOL=classic am start ...
|
||||
```
|
||||
|
||||
它设置的是 `am` 命令进程的环境,不等于给已启动的 Android Activity 设置环境变量。
|
||||
|
||||
## 9. 真机功能验收清单
|
||||
|
||||
以下项目按实际 40250 测试账号、角色和 NPC 条件逐项记录:
|
||||
|
||||
### 启动与网络
|
||||
|
||||
- [ ] APK 安装成功,包名为 `org.internal.mtgodotpoc`。
|
||||
- [ ] `assets.zip` 已复制到应用私有 `files/`,大小与主机一致。
|
||||
- [ ] 日志出现 `[AssetPack] mounted`。
|
||||
- [ ] 登录界面资源完整显示。
|
||||
- [ ] 服务器/频道检测能得到预期结果。
|
||||
- [ ] 认证成功后进入选人页。
|
||||
- [ ] 选择角色后进入游戏场景。
|
||||
|
||||
### 世界与基础操作
|
||||
|
||||
- [ ] 地图、地形、建筑、树木、角色和纹理正常加载。
|
||||
- [ ] 点击地面移动,角色位置与服务器同步。
|
||||
- [ ] 普通攻击、目标选择、技能和动作没有异常断线。
|
||||
- [ ] 聊天发送与接收正常。
|
||||
- [ ] 切换前后台后连接和场景状态符合预期;断线时能看到重连提示。
|
||||
|
||||
### 已接入的常用功能
|
||||
|
||||
- [ ] 背包打开、物品使用、丢弃、移动和快捷栏操作。
|
||||
- [ ] NPC 对话、脚本选项、输入框和确认框。
|
||||
- [ ] 普通商店查看、切换货架和购买。
|
||||
- [ ] 私人商店物品选择、定价、开设和撤收。
|
||||
- [ ] 交易发起、物品/金币添加、确认和取消。
|
||||
- [ ] 仓库密码、存入、取出和物品移动。
|
||||
- [ ] 商城物品列表和取出。
|
||||
- [ ] 组队邀请、接受、离队、成员状态和治疗入口。
|
||||
- [ ] ESC 系统菜单、音量、镜头、雾效和显示设置持久化。
|
||||
|
||||
### 明确不纳入本轮
|
||||
|
||||
- 公会相关功能:按当前范围暂缓。
|
||||
- 龙魂:目标 40250 服务器没有该功能,不进行功能验收。
|
||||
- 真实 FreeBSD + MySQL 全流程验收:需要服务器环境和测试账号,不能用本机离线测试结果替代。
|
||||
|
||||
## 10. 测试证据保存
|
||||
|
||||
建议每次测试保留 APK、资源包校验值、设备信息和日志:
|
||||
|
||||
```bash
|
||||
mkdir -p build/export/android-evidence
|
||||
|
||||
shasum -a 256 build/export/mtgodot-poc.apk \
|
||||
build/export/assets.zip \
|
||||
> build/export/android-evidence/artifacts.sha256
|
||||
|
||||
"$ADB" shell getprop ro.product.model \
|
||||
> build/export/android-evidence/device-model.txt
|
||||
"$ADB" shell getprop ro.build.version.release \
|
||||
> build/export/android-evidence/android-version.txt
|
||||
"$ADB" logcat -d -v time -s godot GodotError AndroidRuntime \
|
||||
> build/export/android-evidence/logcat.txt
|
||||
```
|
||||
|
||||
测试记录至少填写:构建时间、Git commit、APK SHA-256、`assets.zip` SHA-256、手机型号、Android 版本、服务器地址/端口、账号、测试结果和失败日志。
|
||||
|
||||
## 11. 常见问题
|
||||
|
||||
| 现象 | 处理 |
|
||||
|---|---|
|
||||
| `adb devices` 没有设备 | 开启 USB 调试、接受 RSA、切换 MTP、检查数据线;必要时重新执行 `adb kill-server` / `adb start-server` |
|
||||
| 状态是 `unauthorized` | 解锁手机并接受“允许 USB 调试”,然后重新执行 `adb devices` |
|
||||
| APK 安装失败 | 检查设备是否为 arm64;卸载旧包后再安装;查看 `adb install` 的具体错误 |
|
||||
| `run-as` 报不可调试 | 安装 `Debug` APK;Release APK 不能依赖 `run-as` 导入资源 |
|
||||
| 日志找不到 `assets.zip` | 确认外部 push 成功,再执行 `run-as ... cp ... files/assets.zip`,然后重启应用 |
|
||||
| `load_resource_pack 失败` | 检查压缩包完整性、磁盘空间、是否由 `./pack-assets.sh` 生成,以及 zip 内是否存在 `assets/asset_index.txt` |
|
||||
| 登录连接超时 | 手机不能使用 `localhost`;检查服务器 IP、端口、Wi-Fi/VPN、防火墙和 FreeBSD jail 映射 |
|
||||
| 登录协议不对或立即断线 | 确认 Android 包实际选择了 `classic`,仅在 Mac 终端设置 `MT_PROTOCOL` 不会自动传入 APK |
|
||||
| 黑屏或 Vulkan 崩溃 | 保存完整 `logcat`;当前 Android 移动 Vulkan 的 shader/GPU-skin 真机验证仍需在目标设备上进行 |
|
||||
| 更新资源后画面不变 | 资源包挂载发生在启动阶段,执行 `am force-stop` 后重新启动;必要时清理应用数据并重新导入 |
|
||||
|
||||
## 12. 推荐的最短测试流程
|
||||
|
||||
首次测试按以下顺序执行:
|
||||
|
||||
```bash
|
||||
cd /Users/shenlei/Work/mt/mtgodot-poc
|
||||
|
||||
./pack-assets.sh
|
||||
./export-android.sh Debug
|
||||
|
||||
export ADB="${ANDROID_SDK_ROOT:-$HOME/Library/Android/sdk}/platform-tools/adb"
|
||||
PKG=org.internal.mtgodotpoc
|
||||
EXT_DIR="/sdcard/Android/data/$PKG/files"
|
||||
|
||||
"$ADB" install -r build/export/mtgodot-poc.apk
|
||||
"$ADB" shell mkdir -p "$EXT_DIR"
|
||||
"$ADB" push build/export/assets.zip "$EXT_DIR/assets.zip"
|
||||
"$ADB" shell run-as "$PKG" sh -c \
|
||||
"cp '$EXT_DIR/assets.zip' files/assets.zip"
|
||||
"$ADB" shell run-as "$PKG" ls -lh files/assets.zip
|
||||
|
||||
"$ADB" logcat -c
|
||||
"$ADB" shell am start -n "$PKG/com.godot.game.GodotApp"
|
||||
"$ADB" logcat -v time -s godot GodotError AndroidRuntime
|
||||
```
|
||||
|
||||
只有在看到 `[AssetPack] mounted`、登录界面资源正常、并确认 Android 包使用正确的 40250 classic 后端后,才进入真实服务器功能验收。
|
||||
@@ -0,0 +1,648 @@
|
||||
# 迁移到 40250 服务端 —— 协议差异与移植方案
|
||||
|
||||
> 目标:网络层从 **m2dev fork 协议** 换成 **40250 经典 Metin2 协议**,连
|
||||
> `Work/mt/40250/Server/`。现有 m2dev 那套**保留**为可切换后端(`MT_PROTOCOL=m2dev|classic`),
|
||||
> Godot 场景 / GDScript / UI / 渲染保持既有接口与布局;为接住 classic 的状态/命令,只做必要的
|
||||
> bridge、状态显示和资源映射补充。
|
||||
>
|
||||
> 参考源:
|
||||
> - 40250 客户端:`/Users/shenlei/Work/mt/40250/ClientVS22/source/`
|
||||
> (`UserInterface/`、`EterLib/NetStream.{h,cpp}`、`EterBase/cipher.{h,cpp}`)
|
||||
> - 40250 服务端:`/Users/shenlei/Work/mt/40250/Server/metin2/src/server/`
|
||||
> (`game/src/packet.h`、`game/src/packet_info.cpp`、`game/src/desc.cpp`(握手/阶段/cipher)、
|
||||
> `game/src/input.cpp`(`CInputHandshake` + sequence 校验)、`game/src/input_auth.cpp`、`common/`)。
|
||||
> auth 服 = 同一个 game 二进制跑 auth 模式(无独立源码目录);运行时目录
|
||||
> `Server/metin2/server/{auth, game99, channel1..4, db}`,各带 `CONFIG`。
|
||||
> - m2dev(现有移植的对照):`/Users/shenlei/Work/mt/m2dev-client-src-main/src/UserInterface/`
|
||||
|
||||
> **当前范围调整(2026-09-01)**:本阶段只收口客户端本地协议、状态桥接、UI 首版和离线回归。
|
||||
> 真实 FreeBSD + MySQL 40250 服务端的完整登录 / 功能验收暂缓,不作为本阶段交付阻塞;公会相关
|
||||
> 功能暂缓。当前目标 40250 服务端不提供龙魂玩法,因此龙魂不纳入本阶段支持范围;代码中的兼容
|
||||
> 分支不代表该服务端可用。
|
||||
|
||||
---
|
||||
|
||||
## 0. 一句话结论
|
||||
|
||||
| 层 | 改动 | 量级 |
|
||||
|---|---|---|
|
||||
| **网络层** `extension/src/net/` | classic 栈从零写(cipher / 分帧 / 握手 / ~200 结构体 / dispatch),m2dev 栈挪进子目录冻结 | **大**(~2–4 周)|
|
||||
| `EntityStore` | 拆出 mutation API,两个 parser 共用世界模型 | 中(1–2 天)|
|
||||
| `M2Client` | 加 `INetSession` seam,方法转发,按 `MT_PROTOCOL` 选后端 | 小(1 天)|
|
||||
| 渲染 / `formats` / `libgr2` / Godot 场景 / `app_flow` / HUD / UI 窗口 | 保持既有接口;按 40250 复核并补状态、命令和动作资源映射(不重写渲染底座)| 小—中 |
|
||||
| **前置**:跑起 40250 服务端 | 独立一摊(**FreeBSD** game/auth/db + MySQL + proto 表 + config;见 §7.1;仓库已提供可执行脚本)| 中 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 封包头
|
||||
|
||||
### 1.1 宽度与风格
|
||||
|
||||
| | m2dev fork | 40250 classic |
|
||||
|---|---|---|
|
||||
| 头字段 | `uint16_t`(小端),十六进制段(`CG_MOVE = 0x0301`)| **`BYTE`(1 字节)**,十进制(`HEADER_CG_MOVE = 7`)|
|
||||
| 定义位置 | `wire.h`(我们自己维护)| 客户端 `UserInterface/Packet.h`(L30 起匿名 enum,CG/GC/GG 混一起)**与**服务端 `game/src/packet.h` 手工保持一致 |
|
||||
|
||||
> ⚠️ 客户端 `Packet.h` 的 header enum 有 `#if defined(GAIDEN)` 分支,**40250 客户端构建未定义
|
||||
> `GAIDEN`**(`UserInterface.vcxproj` PreprocessorDefinitions 只有 `WIN32;_DEBUG;_WINDOWS;USE_LOD;
|
||||
> _CRT_SECURE_NO_WARNINGS`)。非 GAIDEN 分支里同一 header 值的名字和服务端不同:
|
||||
> 服务端 `HEADER_GC_MAIN_CHARACTER=113` ↔ 客户端非 GAIDEN 是 `HEADER_GC_MAIN_CHARACTER2_EMPIRE=113`
|
||||
> (`HEADER_GC_MAIN_CHARACTER=15` 在客户端指旧包);`SKILL_LEVEL` / `REFINE_INFORMATION` /
|
||||
> `TARGET_CREATE` 等也有 `_OLD` / `_NEW` / `2_` 后缀错位。**移植时以服务端 `game/src/packet.h`
|
||||
> 的值 + 名为准**(它就是线上实际值),结构体按「服务端实际 `Packet()` 发的那个」核(见 §4.4)。
|
||||
|
||||
### 1.2 40250 头表(服务端 `packet.h` 权威值)
|
||||
|
||||
**控制段(保留头,两向都有)**
|
||||
```
|
||||
0xff GC/CG_HANDSHAKE 0xfe CG_PONG / GC_BINDUDP
|
||||
0xfd CG_CLIENT_VERSION / GC_PHASE 0xfc CG_TIME_SYNC / GC_TIME_SYNC (0xfc 的 GC 侧: 服务端
|
||||
packet.h 叫 HEADER_GC_TIME_SYNC,客户端 Packet.h 叫
|
||||
HEADER_GC_HANDSHAKE_OK —— 同一字节;CG 侧无 0xfa)
|
||||
0xfb GC/CG_KEY_AGREEMENT 0xfa GC_KEY_AGREEMENT_COMPLETED
|
||||
0xf1 CG_CLIENT_VERSION2
|
||||
```
|
||||
(客户端值:`Packet.h` L162-167 CG、L362-367 GC。GC 0xfc `HEADER_GC_HANDSHAKE_OK` 注释写「252」。)
|
||||
|
||||
**CG(客户端→服务端)**
|
||||
```
|
||||
1 LOGIN 2 ATTACK 3 CHAT 4 CHARACTER_CREATE 5 CHARACTER_DELETE
|
||||
6 CHARACTER_SELECT 7 MOVE 8 SYNC_POSITION 10 ENTERGAME
|
||||
11 ITEM_USE 12 ITEM_DROP 13 ITEM_MOVE 15 ITEM_PICKUP
|
||||
16 QUICKSLOT_ADD 17 QUICKSLOT_DEL 18 QUICKSLOT_SWAP 19 WHISPER 20 ITEM_DROP2
|
||||
26 ON_CLICK 27 EXCHANGE 28 CHARACTER_POSITION 29 SCRIPT_ANSWER
|
||||
30 QUEST_INPUT_STRING 31 QUEST_CONFIRM
|
||||
50 SHOP 51 FLY_TARGETING 52 USE_SKILL 53 ADD_FLY_TARGETING 54 SHOOT 55 MYSHOP
|
||||
60 ITEM_USE_TO_ITEM 61 TARGET 64 TEXT 65 WARP 66 SCRIPT_BUTTON 67 MESSENGER
|
||||
69 MALL_CHECKOUT 70 SAFEBOX_CHECKIN 71 SAFEBOX_CHECKOUT
|
||||
72 PARTY_INVITE 73 PARTY_INVITE_ANSWER 74 PARTY_REMOVE 75 PARTY_SET_STATE 76 PARTY_USE_SKILL
|
||||
77 SAFEBOX_ITEM_MOVE 78 PARTY_PARAMETER
|
||||
80 GUILD 81 ANSWER_MAKE_GUILD 82 FISHING 83 ITEM_GIVE 90 EMPIRE 96 REFINE
|
||||
100 MARK_LOGIN 101 MARK_CRCLIST 102 MARK_UPLOAD 104 MARK_IDXLIST 105 HACK 106 CHANGE_NAME
|
||||
109 LOGIN2 110 DUNGEON 111 LOGIN3 112 GUILD_SYMBOL_UPLOAD 113 SYMBOL_CRC
|
||||
114 SCRIPT_SELECT_ITEM 116 LOGIN5_OPENID 202 PASSPOD_ANSWER 203 HS_ACK 204 XTRAP_ACK
|
||||
205 DRAGON_SOUL_REFINE 206 STATE_CHECKER
|
||||
```
|
||||
|
||||
**GC(服务端→客户端)**
|
||||
```
|
||||
1 CHARACTER_ADD 2 CHARACTER_DEL 3 MOVE 4 CHAT 5 SYNC_POSITION
|
||||
6 LOGIN_SUCCESS3(=3格,PLAYER_PER_ACCOUNT3) 7 LOGIN_FAILURE 32 LOGIN_SUCCESS4(=4格)
|
||||
8 CHARACTER_CREATE_SUCCESS 9 CREATE_FAILURE 10 DELETE_SUCCESS 11 DELETE_WRONG_SOCIAL_ID
|
||||
12 ATTACK 13 STUN 14 DEAD 15 MAIN_CHARACTER_OLD
|
||||
16 CHARACTER_POINTS 17 CHARACTER_POINT_CHANGE 18 CHANGE_SPEED
|
||||
19 CHARACTER_UPDATE 24 CHARACTER_UPDATE_NEW
|
||||
20 ITEM_DEL 21 ITEM_SET 22 ITEM_USE 23 ITEM_DROP 25 ITEM_UPDATE
|
||||
26 ITEM_GROUND_ADD 27 ITEM_GROUND_DEL
|
||||
28 QUICKSLOT_ADD 29 QUICKSLOT_DEL 30 QUICKSLOT_SWAP 31 ITEM_OWNERSHIP
|
||||
34 WHISPER 36 MOTION 37 PARTS 38 SHOP 39 SHOP_SIGN
|
||||
40 DUEL_START 41 PVP 42 EXCHANGE 43 CHARACTER_POSITION 44 PING
|
||||
45 SCRIPT 46 QUEST_CONFIRM 61 MOUNT 62 OWNERSHIP 63 TARGET 65 WARP
|
||||
69 ADD_FLY_TARGETING 70 CREATE_FLY 71 FLY_TARGETING 72 SKILL_LEVEL_OLD 76 SKILL_LEVEL
|
||||
74 MESSENGER 75 GUILD 77 PARTY_INVITE 78 PARTY_ADD 79 PARTY_UPDATE 80 PARTY_REMOVE
|
||||
81 QUEST_INFO 82 REQUEST_MAKE_GUILD 83 PARTY_PARAMETER
|
||||
85 SAFEBOX_SET 86 SAFEBOX_DEL 87 SAFEBOX_WRONG_PASSWORD 88 SAFEBOX_SIZE
|
||||
89 FISHING 90 EMPIRE 91 PARTY_LINK 92 PARTY_UNLINK 95 REFINE_INFORMATION_OLD
|
||||
99 VIEW_EQUIP 100 MARK_BLOCK 102 MARK_IDXLIST 106 TIME 107 CHANGE_NAME 110 DUNGEON
|
||||
111 WALK_MODE 112 SKILL_GROUP 113 MAIN_CHARACTER 114 SEPCIAL_EFFECT / (USE_POTION 注释掉)
|
||||
115 NPC_POSITION 116 MATRIX_CARD 118 LOGIN_KEY 119 REFINE_INFORMATION 121 CHANNEL
|
||||
122 MALL_OPEN 123 TARGET_UPDATE 124 TARGET_DELETE 125 TARGET_CREATE
|
||||
126 AFFECT_ADD 127 AFFECT_REMOVE 128 MALL_SET 129 MALL_DEL 130 LAND_LIST
|
||||
131 LOVER_INFO 132 LOVE_POINT_UPDATE 133 SYMBOL_DATA 134 DIG_MOTION 135 DAMAGE_INFO
|
||||
136 CHAR_ADDITIONAL_INFO 137 MAIN_CHARACTER3_BGM 138 MAIN_CHARACTER4_BGM_VOL
|
||||
150 AUTH_SUCCESS 151 PANAMA_PACK 152 HYBRIDCRYPT_KEYS 153 HYBRIDCRYPT_SDB 154 AUTH_SUCCESS_OPENID
|
||||
200 ROULETTE 202 REQUEST_PASSPOD 203 REQUEST_PASSPOD_FAILED 204 HS_REQUEST 205 XTRAP_CS1_REQUEST
|
||||
208 SPECIFIC_EFFECT 209 DRAGON_SOUL_REFINE 210 RESPOND_CHANNELSTATUS
|
||||
```
|
||||
|
||||
> 逐结构体细节实现期直接看 `game/src/packet.h`(EUC-KR 编码,注释乱码无碍)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 分帧(framing)
|
||||
|
||||
### 2.1 m2dev fork
|
||||
每个包 `[u16 header LE][u16 length LE][payload]`。长度永远显式。无 sequence 字节。
|
||||
|
||||
### 2.2 40250 classic —— 静态尺寸表 + sequence 字节 + 动态包
|
||||
|
||||
- **收**:读 1 字节 header → 查尺寸表(`CPacketInfo::Set(header, sizeof(TPacketXxx), name, bSeq)`,见
|
||||
`game/src/packet_info.cpp`)→ 读定长 body。
|
||||
- **CG 的 sequence 字节**:`bSeq=true` 的包尾部多 1 字节 `BYTE sequence`。服务端 `Set()` 里
|
||||
`if (bSeq) element->iSize += sizeof(BYTE)`(尺寸表已含 +1);客户端 `NetStream.cpp` 发包后调
|
||||
`SendSequence()`:`m_bUseSequence` 为真时追加 `m_kVec_bSequenceTable[m_iSequence++ % SEQUENCE_TABLE_SIZE]`
|
||||
(静态表 `s_bSequenceTable[SEQUENCE_TABLE_SIZE]`,`NetStream.cpp` L881)。开关由 Python 调
|
||||
`net.SetPacketSequenceMode()`(`PythonNetworkStreamModule.cpp:1601`,对 `rns` 和
|
||||
`rkAccountConnector` 都置 true)—— 具体时机在 `root/*.py`(本 dump 没带),实现期抓包定位。
|
||||
服务端逐包校验、错了断线。
|
||||
- **`bSeq=true` 覆盖几乎所有 CG 游戏内包**(Login/Move/Attack/Chat/ItemMove/Shop/Party/Quest/Pong/…;
|
||||
连 `CG_LOGIN`/`LOGIN2`/`LOGIN3` 和 `CG_TIME_SYNC` 都是 `true`,见 `packet_info.cpp:148-150,137`)。
|
||||
`bSeq=false` 的全集(`packet_info.cpp`):`TEXT` `HANDSHAKE` `MARK_LOGIN` `MARK_IDXLIST` `MARK_CRCLIST`
|
||||
`MARK_UPLOAD` `KEY_AGREEMENT` `GUILD_SYMBOL_UPLOAD` `SYMBOL_CRC` `DRAGON_SOUL_REFINE` `STATE_CHECKER`。
|
||||
(`HS_ACK` / `XTRAP_ACK` 在 40250 服务端 `packet_info.cpp:227-228` 是**注释掉的**,根本没登记。)
|
||||
客户端只在 `m_bUseSequence` 打开后才真正追加尾字节,未开时即便 `bSeq=true` 也不加。
|
||||
- **GC 不带 sequence 字节**。
|
||||
- **动态尺寸包**:客户端接收侧靠 `CNetworkPacketHeaderMap`(`CMainPacketHeaderMap` 构造函数,
|
||||
`PythonNetworkStream.cpp` L29 起)里的 `Set(header, TPacketType(sizeof(TPacketXxx), DYNAMIC_SIZE_PACKET))`
|
||||
标记区分;`CheckPacket()` 命中动态项时先 `Peek(TDynamicSizePacketHeader{BYTE header; WORD size;})`
|
||||
拿 `size` = 整包字节数,再 `Peek(size)`。发送侧则看结构体自带的长度字段
|
||||
(`command_chat` 叫 `WORD length`、`command_whisper` 叫 `WORD wSize`、GC 侧 `packet_*` 叫 `WORD size`,语义相同)。
|
||||
**客户端 GC 动态包全集**(`CMainPacketHeaderMap`,其余 GC 全部 `STATIC_SIZE_PACKET`):
|
||||
`GC_CHAT` `GC_SCRIPT` `GC_SHOP` `GC_GUILD` `GC_MESSENGER` `GC_QUEST_INFO` `GC_DUEL_START`
|
||||
`GC_SYNC_POSITION` `GC_DUNGEON` `GC_NPC_POSITION` `GC_LAND_LIST` `GC_HYBRIDCRYPT_KEYS` `GC_HYBRIDCRYPT_SDB`。
|
||||
**CG 动态包**:只有 `CG_CHAT`(`command_chat`)和 `CG_WHISPER`(`command_whisper`)。
|
||||
**易错的静态包**:`GC_MALL_OPEN`/`SET`/`DEL`、`GC_SAFEBOX_SET`、`GC_PARTY_ADD`/`UPDATE`、`GC_WHISPER`、
|
||||
`GC_AFFECT_ADD` 都是 `STATIC_SIZE_PACKET`(定长,`sizeof` 取满缓冲)。CG 侧 `CG_MARK_UPLOAD`
|
||||
(`{BYTE header; DWORD gid; BYTE image[16*12*4];}`)、`CG_SHOP`(`{BYTE header; BYTE subheader;}`)、
|
||||
`CG_GUILD`(`{BYTE byHeader; BYTE bySubHeader;}`)也都是定长子头分发,不是动态包。
|
||||
|
||||
### 2.3 影响
|
||||
`net_stream.cpp` 完全重写:新 `classic_stream.cpp` 维护 header→size 表(从 `packet.h` + `packet_info.cpp`
|
||||
镜像一份到 `wire_classic.h` 的 `static_assert` + 一个 `packet_size(header)` 函数),发 CG 时自动追加
|
||||
sequence 字节并推进本地序列表,动态包读 `size` 前缀。
|
||||
|
||||
---
|
||||
|
||||
## 3. 加密(cipher)
|
||||
|
||||
### 3.1 m2dev fork
|
||||
libsodium `crypto_kx`(X25519),一轮:`GC_KEY_CHALLENGE{server_pk, challenge}` →
|
||||
`CG_KEY_RESPONSE{client_pk, HMAC(challenge, tx_key)}` → `GC_KEY_COMPLETE{AEAD 加密的 session token, nonce}`。
|
||||
`crypto_kx_*_session_keys` 出**两把方向密钥** `m_tx_key` / `m_rx_key`(**不是**两向同密钥);激活后每向
|
||||
一条 `crypto_stream_xchacha20`(固定方向 nonce:`0x01` S→C、`0x02` C→S)+ 递增字节计数原地 XOR。
|
||||
KEY_COMPLETE 的 token 用一次性 `crypto_aead_xchacha20poly1305_ietf`。`secure_cipher.{h,cpp}`
|
||||
(近乎照搬客户端 `EterBase/SecureCipher`)。
|
||||
|
||||
### 3.2 40250 classic —— `_IMPROVED_PACKET_ENCRYPTION_`(`EterBase/cipher.{h,cpp}`)
|
||||
|
||||
1. **密钥协商**:Crypto++ `DH2`(Unified DH)——`DH2KeyAgreement` 里 `DH dh_; DH2 dh2_(dh_);`,
|
||||
同一组 DH 域参数下各生成「静态 + 临时」两对密钥,`dh2_.Agree(shared_, spriv, epriv, otherPub…)`
|
||||
一次得共享密钥 `SecByteBlock shared_`(`cipher.cpp` L123-133, 336, 428-441)。
|
||||
握手时间同步收敛后,流程是**服务端先手**:
|
||||
1. 服务端 `DESC::SendKeyAgreement()`(`desc.cpp:726`):`cipher_.Prepare(packet.data, &data_length)`
|
||||
生成本端 blob → 发 `HEADER_GC_KEY_AGREEMENT`
|
||||
`TPacketKeyAgreement{BYTE bHeader; WORD wAgreedLength; WORD wDataLength; BYTE data[MAX_DATA_LEN=256];}`
|
||||
(定长 261B,客户端 `Packet.h` L2722,客户端把它登记为 `STATIC_SIZE_PACKET`)。
|
||||
2. 客户端 `RecvKeyAgreementPacket()`(`PythonNetworkStreamPhaseHandshake.cpp:208`):`Prepare()` 生成本端
|
||||
blob → **`Activate(pkt.wAgreedLength, pkt.data, pkt.wDataLength)` 用服务端的数据**(内部
|
||||
`m_cipher.Activate(true, …)`,`Agree()` 得 `SecByteBlock shared_`)→ 再发 `HEADER_CG_KEY_AGREEMENT`
|
||||
带自己的 blob。
|
||||
3. 服务端收 `HEADER_CG_KEY_AGREEMENT`(`input.cpp:647`):**先** `SendKeyAgreementCompleted()`
|
||||
(发 `HEADER_GC_KEY_AGREEMENT_COMPLETED` `TPacketKeyAgreementCompleted{BYTE bHeader; BYTE data[3];}`)
|
||||
+ `ProcessOutput()` 刷 socket,**再** `IsCipherPrepared()` 检查(否则「maybe a Hacker」
|
||||
`DelayedDisconnect(5)`)→ `FinishHandshake(wAgreedLength, data, wDataLength)` = `cipher_.Activate(false, …)`
|
||||
(`desc.cpp:755`)成功则 `g_bAuthServer ? SetPhase(PHASE_AUTH) : SetPhase(PHASE_LOGIN)`(`input.cpp:665-668`),
|
||||
失败 `PHASE_CLOSE`。
|
||||
4. 客户端 `RecvKeyAgreementCompletedPacket()` → `ActivateCipher()` = `m_cipher.set_activated(true)`。
|
||||
—— `Agree()` 完成到 `set_activated(true)` 之间有一小段窗口:密钥算出但还没开始真正加解密。
|
||||
注意服务端是**先发 COMPLETED 再自己 FinishHandshake/切阶段**(顺序有意如此,帮客户端先进加密态)。
|
||||
2. **对称加密**:`SetUp(polarity)`(`cipher.cpp` L205)里 `hint_0 = shared[shared[0] % size]`、
|
||||
`hint_1 = shared[shared[1] % size]`,各 `BlockCipherAlgorithm::Pick(hint)` → `selector = hint %
|
||||
kMaxAlgorithms`(**`kMaxAlgorithms = 14`**,enum 里 `kDefault=0` 占一位)。`selector` 1–13 依次是
|
||||
`RC6/MARS/Twofish/Serpent/CAST256/IDEA/3DES(DES_EDE2)/Camellia/SEED/RC5/Blowfish/TEA/SHACAL2`;
|
||||
`selector == 0`(`kDefault`)或落到 `default:` → `Twofish`(`kAES/Rijndael` 那位是注释掉的)。
|
||||
**CTR 模式**(`CTR_Mode<T>::Encryption/Decryption`),key/iv 从 `shared` 切片。两个密码实例按
|
||||
`polarity` 决定收/发各用哪个 —— **客户端 polarity = `true`**
|
||||
(`EterLib/NetStream.cpp:3004` `return m_cipher.Activate(true, agreed_length, buffer, length);`),
|
||||
服务端用 `false`。无 padding(CTR 流)。
|
||||
`Encrypt/Decrypt` = `SymmetricCipher::ProcessData` 原地。
|
||||
3. **`_IMPROVED_PACKET_ENCRYPTION_` 是编译期 `#define`,不是运行时开关**:
|
||||
- 服务端 `common/service.h:7` `#define _IMPROVED_PACKET_ENCRYPTION_` —— **shipped 40250 服务端默认开**。
|
||||
- 客户端 `eterBase/ServiceDefs.h:4` 同样 `#define` —— 也默认开。
|
||||
- 关掉走老路:4-DWORD key 的 `TEA_Encrypt`(服务端 `desc.cpp` non-improved 分支 + `GetEncryptionKey()`)
|
||||
/ 客户端 `SetSecurityMode`。**要「明文起步」必须重编服务端**(把 `service.h` 那行注释掉),
|
||||
没有 `CONFIG` token 能关(见 §7.2)。有源码,可行。
|
||||
- 另有一层 `packet_encode`/`packet_decode`(`desc.cpp:479`)在 cipher 之外,始终生效。
|
||||
|
||||
### 3.3 影响
|
||||
最硬的一块。需要 **Crypto++**(`DH`/`DH2` + `CTR_Mode<T>` 13 个算法)。方案:
|
||||
- vendored Crypto++(子模块,CMake 跨 macOS/iOS/Android 能编)—— 体积大但省事;或
|
||||
- 只挪需要的算法(DH2 + Twofish/RC5/... CTR)—— 工作量大。
|
||||
- 版本 **Crypto++ 8.4.0**:服务端 `src/extern/cryptopp_8_4_0.tar.gz`(+ 预编 `src/extern/lib/libcryptopp.a`),
|
||||
客户端 `ClientVS22/extern/include/cryptopp/`(`config_ver.h` `CRYPTOPP_VERSION 840`)。移植对齐这个版本。
|
||||
|
||||
`secure_cipher.cpp` 不动(m2dev 用),新 `classic_cipher.{h,cpp}` 端口 `EterBase/cipher.cpp`。
|
||||
|
||||
---
|
||||
|
||||
## 4. 握手 / 阶段流
|
||||
|
||||
### 4.1 阶段枚举
|
||||
服务端 `enum EPhase`(`packet.h:850`):`PHASE_CLOSE=0, HANDSHAKE=1, LOGIN=2, SELECT=3, LOADING=4,
|
||||
GAME=5, DEAD=6, CLIENT_CONNECTING=7, DBCLIENT=8, P2P=9, AUTH=10, TEEN=11, PASSPOD=12`。
|
||||
客户端匿名 enum(`Packet.h:1124`)前 7 个(0–6)一致,尾部是 `DBCLIENT_CONNECTING / DBCLIENT`——
|
||||
**只有 0–6 会经 `GC_PHASE` 到达客户端**,`PHASE_AUTH` 等是服务端内部态。
|
||||
正常流:CLOSE→HANDSHAKE→(AUTH,仅 `g_bAuthServer`)→LOGIN→SELECT→LOADING→GAME。
|
||||
切阶段由 `HEADER_GC_PHASE`(0xfd) `TPacketGCPhase{BYTE header; BYTE phase;}` 驱动(客户端 `RecvPhasePacket()`)。
|
||||
|
||||
### 4.2 HANDSHAKE
|
||||
客户端 `PythonNetworkStreamPhaseHandshake.cpp`;**服务端 `desc.cpp` + `input.cpp`(`CInputHandshake`)明文都在**。
|
||||
|
||||
- `TPacketGCHandshake`(=`TPacketCGHandshake`,同布局):`{BYTE bHeader; DWORD dwHandshake; DWORD dwTime;
|
||||
LONG lDelta;}`(服务端 `packet.h` `packet_header_handshake`;客户端字段名 `header`)。`bSeq=false`
|
||||
(`packet_info.cpp:136`)。
|
||||
- 连上 → 服务端 `DESC::StartHandshake` → `SendHandshake(get_dword_time(), 0)`,`pack.bHeader =
|
||||
HEADER_GC_HANDSHAKE`(0xff)(`desc.cpp:637-658`)。
|
||||
- 客户端 **HandShake 阶段**收到 `HEADER_GC_HANDSHAKE`(`PythonNetworkStreamPhaseHandshake.cpp` L37-62 内联):
|
||||
`ELTimer_SetServerMSec(dwTime + lDelta)` → `dwTime += 2*lDelta`、`lDelta = 0` →
|
||||
**header 不变(仍 0xff)原样回发**,不追加 sequence。
|
||||
- 服务端 `CInputProcessor::Handshake`(`input.cpp:195`):先校验 `p->dwHandshake == GetHandshake()`
|
||||
(不符 `PHASE_CLOSE`)。在 `PHASE_HANDSHAKE` 里调 `HandshakeProcess(dwTime, lDelta, false)`
|
||||
(`desc.cpp:662`):
|
||||
- `bias = get_dword_time() - (dwTime + lDelta)`;**`0 ≤ bias ≤ 50` → 收敛**,`m_bHandshaking=false` 返回 true。
|
||||
- 否则 `lNewDelta = (dwCurTime - dwTime) / 2`(<0 时用 `(dwCurTime - m_dwHandshakeSentTime)/2`),
|
||||
`++m_iHandshakeRetry > HANDSHAKE_RETRY_LIMIT` → `PHASE_CLOSE`;否则 `SendHandshake(dwCurTime, lNewDelta)`
|
||||
再来一轮。
|
||||
- **收敛后**(`input.cpp:207-217`):
|
||||
- `#ifdef _IMPROVED_PACKET_ENCRYPTION_`(shipped 默认)→ `d->SendKeyAgreement()`,**这里先不切阶段**;
|
||||
阶段推进发生在之后收到 `HEADER_CG_KEY_AGREEMENT` 时(`input.cpp:665-668`,`FinishHandshake` 成功后
|
||||
`SetPhase(PHASE_AUTH|PHASE_LOGIN)`)。完整密钥协商流程见 §3.2。
|
||||
- `#else`(明文/老 TEA)→ 立即 `g_bAuthServer ? SetPhase(PHASE_AUTH) : SetPhase(PHASE_LOGIN)`。
|
||||
- **握手期之后的时钟再同步**:SELECT/GAME 里服务端再发 `HEADER_GC_HANDSHAKE`,客户端不在 handshake
|
||||
phase → `PythonNetworkStreamPhaseSelect.cpp` L96-102 的 `RecvHandshakePacket()`:回发时 header 改成
|
||||
`HEADER_CG_TIME_SYNC`(0xfc) 且 `SendSequence()`(`bSeq=true`,`packet_info.cpp:137`)。服务端此路
|
||||
`HandshakeProcess(…, true)`(`bInfiniteRetry=true`,`input.cpp:222`),收敛时回发 **1 字节
|
||||
`HEADER_GC_TIME_SYNC`(0xfc)**(`desc.cpp:676-679`)→ 客户端 `RecvHandshakeOKPacket()` 读
|
||||
`TPacketGCBlank{BYTE header;}` 定终值。
|
||||
—— 注意 0xfc 这个值:**客户端叫 `HEADER_GC_HANDSHAKE_OK`,服务端叫 `HEADER_GC_TIME_SYNC`**,同一字节。
|
||||
- 期间可能夹 `HEADER_GC_KEY_AGREEMENT` / `_COMPLETED`(见 §3)、`HEADER_GC_BINDUDP`、`HEADER_GC_PING`、
|
||||
`HEADER_GC_HYBRIDCRYPT_KEYS` / `_SDB`(动态包)。`CInputHandshake::Analyze`(`input.cpp:276`)还会
|
||||
处理 `HEADER_CG_TEXT`(webadmin `IS_SERVER_UP` 等文本命令)和 header `10`(旧探活忽略)。
|
||||
|
||||
### 4.3 LOGIN(`PythonNetworkStreamPhaseLogin.cpp`)
|
||||
- **两条路**:
|
||||
- `SendLoginPacket` → `HEADER_CG_LOGIN` `TPacketCGLogin{BYTE header, char login[31], char passwd[17]}`
|
||||
(明文账密,服务端直连模式)。
|
||||
- `SendLoginPacketNew` → `HEADER_CG_LOGIN2` `command_login2` `{BYTE header; char name[31];
|
||||
DWORD login_key; DWORD adwClientKey[4];}` —— `login_key` 来自**独立 auth 服**(同 game 二进制跑
|
||||
auth 模式)先前发的 `HEADER_GC_LOGIN_KEY`(118);`adwClientKey` = 客户端 `extern DWORD g_adwEncryptKey[4]`。
|
||||
`SendLoginPacketNew` 还会 `SetSecurityMode(true, g_adwEncryptKey, g_adwDecryptKey)` 起老 TEA 层。
|
||||
- `HEADER_CG_LOGIN3` `command_login3` `{header; name[31]; pwd[17]; DWORD adwClientKey[4];}`
|
||||
(account server 直登 + 客户端 key)。
|
||||
- 回:**40250 服务端实际只发 `HEADER_GC_LOGIN_SUCCESS_NEWSLOT`(=**32**)**
|
||||
(`DESC::SendLoginSuccessPacket` `desc.cpp:925-959`:`p.bHeader = HEADER_GC_LOGIN_SUCCESS_NEWSLOT`,
|
||||
`Packet(&p, sizeof(TPacketGCLoginSuccess))`)。结构体 `packet_login_success` /
|
||||
`TPacketGCLoginSuccess`(`packet.h:892`):`{BYTE bHeader; TSimplePlayer players[PLAYER_PER_ACCOUNT=**4**];
|
||||
DWORD guild_id[4]; char guild_name[4][GUILD_NAME_MAX_LEN+1=13]; DWORD handle; DWORD random_key;}`
|
||||
—— 客户端非 GAIDEN 里 header 32 走 `HEADER_GC_LOGIN_SUCCESS4` → `TPacketGCLoginSuccess4`(`[4]`,同布局)。
|
||||
`TSimplePlayer`(`common/tables.h:347`,`pack(1)`,**63 字节**)== 客户端 `TSimplePlayerInformation`。
|
||||
header **6**(`HEADER_GC_LOGIN_SUCCESS` / 客户端 `_SUCCESS3`,`players[3]`)是旧格式,shipped 40250 不发。
|
||||
失败 `HEADER_GC_LOGIN_FAILURE`(7) `{BYTE header; char szStatus[LOGIN_STATUS_MAX_LEN+1=9];}`
|
||||
(`LOGIN_FAILURE_* enum`:1 ALREADY / 2 ID_NOT_EXIST / 3 WRONG_PASS / 4 FALSE / 7 FULL)。
|
||||
`handle`/`random_key` 供 `CG_MARK_LOGIN` 用(会徽下载)。
|
||||
- 频道状态:`HEADER_GC_RESPOND_CHANNELSTATUS`(210)。
|
||||
- `GC_PHASE(PHASE_SELECT)` → 选人阶段。
|
||||
|
||||
### 4.4 SELECT / LOADING / GAME
|
||||
- SELECT:`CG_CHARACTER_SELECT`(6) `command_player_select` `{BYTE header; BYTE player_index;}` →
|
||||
`GC_PHASE(PHASE_LOADING)` + **header 113 的「主角包」**。服务端 `char.cpp:1607` 发的是
|
||||
`TPacketGCMainCharacter`(服务端 `packet.h` L1043)=
|
||||
`{BYTE header; DWORD dwVID; WORD wRaceNum; char szName[CHARACTER_NAME_MAX_LEN+1=25]; long lx,ly,lz;
|
||||
BYTE empire; BYTE skill_group;}` —— 客户端非 GAIDEN 里这个 header 走 `case HEADER_GC_MAIN_CHARACTER2_EMPIRE`
|
||||
读 `TPacketGCMainCharacter2_EMPIRE`(同布局)。注意 `wRaceNum` 是种族不是地图号,szName 在坐标**前**,
|
||||
尾部是 `empire` + `skill_group` 两个 BYTE。开了 SUPPORT_BGM 时改发 `_BGM`(137) / `_BGM_VOL`(138) 变体
|
||||
(多 BGM 名字段)。地图名另经加载流程 → `CG_ENTERGAME`(10) → `GC_PHASE(PHASE_GAME)`。
|
||||
- 建/删号:`CG_CHARACTER_CREATE`(4) —— 客户端 `command_player_create` / `TPacketCGCreateCharacter`
|
||||
`{BYTE header; BYTE index; char name[CHARACTER_NAME_MAX_LEN+1=25]; WORD job; BYTE shape; BYTE CON;
|
||||
BYTE INT; BYTE STR; BYTE DEX;}`(服务端侧同一包叫 `TPacketCGPlayerCreate`)。
|
||||
`CG_CHARACTER_DELETE`(5) `command_player_delete` / `TPacketCGDestroyCharacter`
|
||||
`{BYTE header; BYTE index; char szPrivateCode[PRIVATE_CODE_LENGTH=8];}`。
|
||||
→ `GC_CHARACTER_CREATE_SUCCESS`(8) `command_player_create_success` / `TPacketGCPlayerCreateSuccess`
|
||||
`{BYTE header; BYTE bAccountCharacterSlot; TSimplePlayerInformation kSimplePlayerInfomation;}`
|
||||
/ `_CREATE_FAILURE`(9) / `_DELETE_SUCCESS`(10) `{header, BYTE account_index}` /
|
||||
`_DELETE_WRONG_SOCIAL_ID`(11)。
|
||||
|
||||
> 与 m2dev 对照:m2dev 是 `CG_CHARACTER_CREATE=0x0201`;40250 是 `HEADER_CG_CHARACTER_CREATE=4`。
|
||||
> create 包里 `job` 两边**都是 `WORD`(u16)**;差异在头值、结构体名、字段序(40250 顺序是
|
||||
> name→job→shape→CON/INT/STR/DEX),按 40250 `command_player_create` 重推。**只有摘要结构
|
||||
> `TSimplePlayerInformation.byJob` 是 BYTE**(见 §5),别和 create 包的 `WORD job` 混。
|
||||
|
||||
---
|
||||
|
||||
## 5. 关键结构体差异(抽样,实现期逐个对 `game/src/packet.h`)
|
||||
|
||||
| 语义 | m2dev fork | 40250 classic |
|
||||
|---|---|---|
|
||||
| 包头 | `uint16_t header; uint16_t length;` | `BYTE header;`(动态包再加 `WORD size;`)|
|
||||
| `TItemPos` | `{u8 window_type; u16 cell;}` | `{BYTE window_type; WORD cell;}`(同布局,但不少老包里 cell 是 `BYTE`,按包看)|
|
||||
| `TPlayerItemAttribute` | `{u8 bType; i16 sValue;}` (3B) | 同 |
|
||||
| `TSimplePlayerInformation`(客户端 `Packet.h` L1176)| fork 版 | `{DWORD dwID; char szName[CHARACTER_NAME_MAX_LEN+1=25]; BYTE byJob; BYTE byLevel; DWORD dwPlayMinutes; BYTE byST,byHT,byDX,byIQ; WORD wMainPart; BYTE bChangeName; WORD wHairPart; BYTE bDummy[4]; long x,y; LONG lAddr; WORD wPort; BYTE bySkillGroup;}` —— **摘要里 `byJob` 是 BYTE**(但 `command_player_create.job` 是 `WORD`,别混),名字 25,无 wMapIndex/guild(guild 在 `TPacketGCLoginSuccess3/4` 尾部并列数组)|
|
||||
| 移动 `CG_MOVE`(`command_move`)| fork | `{BYTE bHeader; BYTE bFunc; BYTE bArg; BYTE bRot; long lX; long lY; DWORD dwTime;}` —— **实测一致** |
|
||||
| 聊天 | fork `[hdr][len][type][...]` | CG `command_chat{BYTE header; WORD length; BYTE type;}` + `char szChat[]`(`Packet.h` L597);GC `packet_chatting{BYTE header; WORD size; BYTE type; DWORD dwVID; BYTE bEmpire;}` + 文本(L1425)。都是动态包,长度字段一个叫 `length` 一个叫 `size`,语义相同 |
|
||||
| 商店 | fork `GC_SHOP`(0x0810) sub-header + `SHOP_GC_START_EX` | `HEADER_GC_SHOP`(38) `TPacketGCShop{BYTE header; WORD size; BYTE subheader;}`(动态)。`EPacketShopSubHeaders`(`Packet.h` L1864):`GC_START`=0 … `GC_START_EX`=10、`NOT_ENOUGH_MONEY_EX`=11 —— **子头值与 fork 一致**,商店层大体可搬 |
|
||||
| 交易 | fork `GC_EXCHANGE` sub-header | `TPacketExchange{BYTE header; BYTE subheader; …}` —— **定长,无 `WORD size`**(不是所有 sub-header 包都是动态包,逐个看)|
|
||||
| 帮会 / messenger / quest | fork 各自 | `GC_GUILD` / `GC_MESSENGER` / `GC_QUEST_INFO` 是动态包(`[BYTE header][WORD size][...]`,见 §2.2 权威清单),子头在 size 之后。`GC_SCRIPT`(任务对话)也是动态 |
|
||||
| 龙魂精炼 | 不纳入本阶段 | 目标 40250 服务端不提供该玩法;客户端兼容代码保留但不宣称可用,也不纳入当前验收 |
|
||||
|
||||
`wire.h` ~2000 行结构体按 40250 重写成 `wire_classic.h`。**所有 wire 结构体是 `#pragma pack(1)`**
|
||||
(客户端 `Packet.h` L435–2813 的 `pack(push)/pack(1)…pack(pop)` 段;文件开头那段 `pack(8)` 是无关的
|
||||
Hackshield 传输缓冲,不涉及协议)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 双后端架构
|
||||
|
||||
```
|
||||
extension/src/net/
|
||||
├─ entity_store.{h,cpp} # 共享世界模型。apply() 拆成:
|
||||
│ # - 状态 + mutation 方法(spawn_entity / set_vitals /
|
||||
│ # set_inv_item / party_set / shop_set / ...)—— 协议无关
|
||||
│ # - m2dev_parser.cpp : 现有 apply() 挪过来,调 mutation
|
||||
│ # - classic_parser.cpp: 新写,按 §1/§5 解 40250 包,调同一批 mutation
|
||||
├─ i_net_session.h # 新接口:
|
||||
│ connect(host, port) / login(id, pw) / select_char(i) / enter_game()
|
||||
│ send_move/attack/chat/item_move/use_skill/... (或 send(Intent, args))
|
||||
│ pump() → 产出 GC 包 → parser → entity_store
|
||||
│ 信号源: 让 M2Client 照旧 emit(session 只回调 "有 N 个事件待抽干")
|
||||
├─ m2dev/ # 现有文件整体挪入, namespace mtnet::m2dev, 不改逻辑
|
||||
│ wire.h · secure_cipher.{h,cpp} · net_stream.{h,cpp} · game_client.h · auth_client.h ·
|
||||
│ mark_client.h · mark_image.{h,cpp} · byte_buffer.h · m2dev_parser.cpp(从 entity_store 拆)
|
||||
│ 实现 INetSession
|
||||
├─ classic/ # 新, namespace mtnet::classic
|
||||
│ wire_classic.h · classic_cipher.{h,cpp}(Crypto++ DH2 + CTR)· classic_stream.{h,cpp}
|
||||
│ (1B 头 + size 表 + sequence 字节 + 动态包)· classic_session.cpp(HANDSHAKE→…→GAME 状态机)
|
||||
│ · classic_parser.cpp · 实现 INetSession
|
||||
└─ m2_client.{h,cpp} # 持有 std::unique_ptr<INetSession>;
|
||||
构造时按 OS.get_environment("MT_PROTOCOL")(默认 "classic" 或先 "m2dev")选
|
||||
m2dev::Session / classic::Session; 所有 game->send_* / dispatch 改成 session->
|
||||
```
|
||||
|
||||
- **CTest**:`net_entity_test` / `net_loopback_test` 现有的绑 m2dev 后端,保持绿。
|
||||
classic 侧新增 `net_classic_test`(合成包 + MockServer 走 classic 分帧/cipher)。
|
||||
- **GDScript / 场景 / UI / 渲染**:保留原有 `M2Client` 方法和既有信号兼容性;为 40250
|
||||
特有的 classic 回包增加必要 signal/状态桥接和 UI 处理,不改渲染底座。
|
||||
|
||||
---
|
||||
|
||||
## 7. 待确认 / 暂缓项
|
||||
|
||||
1. **【暂缓】跑起 40250 服务端** —— `Server/metin2/src/server/`(顶层 `Makefile` → `libthecore libpoly libgame
|
||||
liblua libsql libserverkey game db`)。**这套是 FreeBSD-only**:`CC = clang++-devel`、`-m32`
|
||||
32 位、`-lmd`、静态链 `/usr/local/lib/mysql/libmysqlclient.a` + 预编 `extern/lib/libcryptopp.a`
|
||||
+ `libIL.a`,`GAMEDIR = /usr/metin2/server/share/bin`(`game/src/Makefile` `SVN_VERSION = 40250`)。
|
||||
Linux 上跑要么改 Makefile/补依赖,要么 FreeBSD VM/jail。还要 MySQL(`Server/metin2_mysql_dump` /
|
||||
`Server/mysql`)、`item_proto`/`mob_proto` 表、运行时 `Server/metin2/server/{game99,auth}` 的 `CONFIG`。
|
||||
本仓库的 `tools/40250/{build_server,setup_mysql,prepare_runtime,start_server,stop_server,run_classic_e2e}.sh`
|
||||
已把这一步固化为可执行 runbook;当前 macOS 机器仍没有 FreeBSD/MySQL server。该环境搭建和
|
||||
live 结果回填暂不属于本阶段交付,后续恢复公会 / 真服验收时再执行。
|
||||
2. **明文起步要重编服务端**:`_IMPROVED_PACKET_ENCRYPTION_` 是 `common/service.h:7` 的编译期 `#define`,
|
||||
**没有 `CONFIG` token 可关**(`config.cpp` 里只有 `test_server` / `distribution_test_server` /
|
||||
`g_bAuthServer` 这类,与加密无关)。第一里程碑:注释掉 `service.h` 那行重编 game,客户端同理,
|
||||
shipped 40250 默认直接走 DH2/CTR;明文只作为需要重编 game 的诊断分支,不是运行配置项。
|
||||
3. **auth 服务器是否独立端口**:`g_bAuthServer` 分支决定握手收敛后进 `PHASE_AUTH` 还是直接 `PHASE_LOGIN`
|
||||
(`input.cpp:207-217`)。auth 服和 game 服可能不同端口(运行时 `server/auth` vs `server/game99`)。
|
||||
决定用 `CG_LOGIN` 还是 `CG_LOGIN2/3` + `GC_LOGIN_KEY`。
|
||||
4. **Crypto++ 引入方式**:vendored 子模块 vs 抽子集。对齐服务端的 `cryptopp_8_4_0`。
|
||||
5. **sequence 表**:客户端静态表 `s_bSequenceTable[SEQUENCE_TABLE_SIZE=32768]`(`EterLib/NetStream.cpp`
|
||||
L881;`SendSequence()` L834 循环取值 `m_iSequence % 32768`,`SetPacketSequenceMode(true)` L829 开启)。
|
||||
服务端对应表 `game/src/sequence.cpp` `gc_abSequence[SEQUENCE_MAX_NUM=32768]`;校验在 `input.cpp`
|
||||
L121-162:`m_pPacketInfo->IsSequence(bHeader)` 时读包尾 1 字节与 `lpDesc->GetSequence()` 比,不符
|
||||
→ `SetPhase(PHASE_CLOSE)` 断线,符 → `push_seq` + `SetNextSequence()`。
|
||||
**两张表必须逐字节一致**,端口时整表 + 开启时机照抄。
|
||||
6. **HYBRIDCRYPT**(`GC_HYBRIDCRYPT_KEYS/SDB`, 152/153,动态包)—— 是**加密资源包(EterPack)的密钥下发**:
|
||||
客户端 `RecvHybridCryptKeyPacket`/`SDBPacket` → `CEterPackManager::RetrieveHybridCryptPackKeys/PackSDB`
|
||||
(SDB = Supplementary Data Blocks),让客户端能解密 hybrid 加密的 `.eix/.epk`。**与反外挂无关**。
|
||||
我们的移植走散文件 / 自研 mtpack,不吃官方加密包 → **直接忽略**(收到丢弃即可)。
|
||||
7. **root/\*.py + uiscript**:这个 dump 只有 client 源码,没带 `root/` pack。UI 窗口对照 40250 版
|
||||
uiscript 是**独立线**,不阻塞协议移植(现有窗口先照跑)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 建议推进顺序
|
||||
|
||||
1. **【暂缓】真服前置**:在 FreeBSD VM/jail 按 `tools/40250/README.md` 起 40250 game+auth+db,
|
||||
并用官方客户端或 `net_classic_e2e_host` 登入验证;不作为当前交付门槛。
|
||||
2. `INetSession` 接口 + `EntityStore` mutation 拆分 + m2dev 挪入 `net/m2dev/`(现有测试保持绿)。
|
||||
3. `classic/classic_stream`:1B 头 + size 表 + sequence + 动态包 + shipped DH2/CTR,打通
|
||||
HANDSHAKE→LOGIN→SELECT→GAME 空跑。
|
||||
4. `classic_parser`:`GC_LOGIN_SUCCESS` / `GC_MAIN_CHARACTER` / `GC_CHARACTER_ADD` / `GC_MOVE` /
|
||||
`GC_CHARACTER_POINTS` —— 能进游戏、看见自己和别人动。
|
||||
5. `classic_cipher`:Crypto++ DH2 + CTR,接 `KEY_AGREEMENT` 流。
|
||||
6. 铺开其余 GC/CG(物品 / 战斗 / 商店 / 组队 / 任务 …),对照 m2dev 已做的功能清单逐个搬;
|
||||
公会与龙魂不列入当前阶段。
|
||||
7. `MT_PROTOCOL` 切换、`net_classic_test`;真服 e2e 暂缓。
|
||||
8. UI 窗口对 40250 uiscript 复核(独立)。
|
||||
|
||||
> 进度按 `docs/CLIENT-GAP.md` 的功能清单同步,标注 classic/m2dev 各自完成度。
|
||||
|
||||
### 进度(branch `feat/40250-classic-net`)
|
||||
|
||||
- **前置(step 1)**:40250 FreeBSD 服务端 —— 暂缓(见 §7.1),不阻塞当前本地协议 / UI 交付。
|
||||
- **step 3a — wire 层** ✅:`extension/src/net/classic/wire_classic.h`(`namespace mtnet::classic`,
|
||||
1B 头 enum 全表,命名跟服务端 `packet.h` + `#pragma pack(1)` 结构体 + `static_assert` +
|
||||
`packet_size_{cg,gc}(h)`(**整包尺寸含 1B 头、不含 seq 字节** —— 对齐 `CPacketInfo::Set(…, sizeof(…))`)+
|
||||
`is_sequence_cg` / `is_dynamic_{cg,gc}`)。覆盖 control / handshake / key-agreement / login(1/2/3) /
|
||||
login_success(32 主用, 6 legacy) / select / create / delete / main_character(113) /
|
||||
character_add(1,2) / move(CG,GC) / points / point_change / chat。`TSimplePlayer` = **63 字节**。
|
||||
40250 客户端/服务端实际会用到的结构体已随 parser 补齐;仅 vendor-only 或服务端未登记的包不伪造。
|
||||
- **step 3b — sequence 表** ✅:`extension/src/net/classic/sequence_table.h` —— 32768 字节
|
||||
`SEQUENCE_TABLE`,逐字节 = `NetStream.cpp:881` `s_bSequenceTable` = 服务端 `sequence.cpp`
|
||||
`gc_abSequence[]`(脚本校验过)。
|
||||
- **step 3c — 分帧层** ✅:`extension/src/net/classic/classic_stream.{h,cpp}`(在 `mtnet` 库里)。
|
||||
非阻塞 TCP + `feed()`/`take_outgoing()` 无 socket 测试路径。base 处理 HANDSHAKE 回显
|
||||
(`time += 2*delta`、header 不变、无 seq)、`GC_TIME_SYNC` blank、`GC_PHASE`、`GC_PING→CG_PONG`、
|
||||
`BINDUDP`;`set_sequence_mode(true)` 后对 bSeq CG 包追加 `SEQUENCE_TABLE[i++]`;动态包读
|
||||
`TDynamicSizePacketHeader.size`。收到 `GC_KEY_AGREEMENT` 后由 `classic_cipher` 完成 DH2/CTR
|
||||
协商,并支持完成包与后续密文同一 read 的 pipeline。
|
||||
CTest `net.classic_wire` + `net.classic_stream` 绿。
|
||||
- **step 2 — `INetSession` seam + `EntityStore` mutation API** ✅(部分):
|
||||
`extension/src/net/i_net_session.h`(`connect / disconnect / select_char / enter_game / pump /
|
||||
stage / world`,per-intent `send_*` 随后加)。`EntityStore` 加了协议无关的
|
||||
`mut_spawn / mut_spawn_main / mut_despawn / mut_move / mut_set_points / mut_set_point`
|
||||
(包住原 `touch()` / `start_move()`,m2dev `apply()` 一行没动,`net.entity_store` 仍绿)。
|
||||
m2dev 文件的物理挪目录(`net/m2dev/`)延后(纯改名,无功能收益)。
|
||||
- **step 4 — `classic_parser` + `classic_session`** ✅:
|
||||
- `classic/classic_parser.{h,cpp}` —— 40250 GC 包体 → `EntityStore::mut_*`。已接
|
||||
`GC_LOGIN_SUCCESS`(32/6) / `GC_LOGIN_FAILURE` / `GC_LOGIN_KEY` / `GC_MAIN_CHARACTER`(113) /
|
||||
`GC_CHARACTER_ADD`(1) / `GC_CHARACTER_DEL`(2) / `GC_MOVE`(3) / `GC_CHARACTER_POINTS`(16) /
|
||||
`GC_CHARACTER_POINT_CHANGE`(17,含 `int32_t header` 怪癖) / `GC_CHAT`(4,dynamic)。
|
||||
`CharSlot` 列表 + handle/random_key。
|
||||
- `classic/classic_session.{h,cpp}`(实现 `INetSession`)—— 驱动
|
||||
HANDSHAKE→LOGIN(`CG_LOGIN`, shipped server 的 DH2/CTR)→SELECT→LOADING→GAME。`GC_PHASE(LOGIN)` 时开 sequence mode
|
||||
并发登录;`GC_PHASE(SELECT)` 抛 `on_char_list`;`select_char(slot)`→`CG_CHARACTER_SELECT`;
|
||||
`PHASE_LOADING` 后 1.5s 自动 `CG_ENTERGAME`(可配);`PHASE_GAME`→`on_entered_game`。
|
||||
`start_offline()` 测试钩子跳过 socket。
|
||||
- CTest `net.classic_session` —— 合成服务端字节走完整流程:握手回显、CG_LOGIN + seq[0]、
|
||||
4 格角色列表、选人 + seq[1]、主角/NPC spawn、自动 ENTERGAME + seq[2]、进游戏、
|
||||
GC_MOVE 插值、points/point_change/del。全量 ctest **13/13**。
|
||||
- **step 4b — per-intent `send_*`** ✅:`INetSession` + `ClassicSession` 加了
|
||||
`send_move`(`bRot = deg/5`)/ `send_attack` / `send_chat`(dynamic:`[hdr][WORD len][type]text\0`,
|
||||
`len = 4 + strlen + 1`)/ `send_target` / `send_character_position` / `send_click_npc` /
|
||||
`send_script_answer` / `send_item_use` / `send_item_move` / `send_item_drop`(count>0 走 DROP2)/
|
||||
`send_item_pickup` / `send_use_skill`(target 非 0 时先发 `CG_FLY_TARGETING`)。全部
|
||||
`Stage::InGame` 门控,走 `classic_stream` 的 seq 追加。`wire_classic.h` 补了对应 CG 结构体 +
|
||||
`packet_size_cg` 表项。`EntityStore::mut_move` 改收「已解码角度」(m2dev `rot*360/256` vs
|
||||
classic `rot*5` 两种编码,放到 parser 里转)。`net.classic_session` 扩展验证
|
||||
move/attack/target/chat/item_move 的字节布局 + seq[3..6] 连续性 + 非 InGame 拒绝。ctest 13/13。
|
||||
- **step 7(前半)— `M2Client` 按 `MT_PROTOCOL` 选后端** ✅:`connect_to_server()` 里
|
||||
`OS::get_environment("MT_PROTOCOL") == "classic"` → 建 `ClassicSession` 直连 game host(无独立
|
||||
auth 服),`set_stage(GameConnect)`;否则原 `AuthClient`+`GameClient` 路径**一行没动**。
|
||||
`_process()` 加 `pump_classic()` 分支(stage→`phase_changed`/`stage_changed`/`char_list`/
|
||||
`entered_game` + 复用世界镜像子集:`entity_spawned/despawned/moved/main_set/info` + `chat` +
|
||||
`vitals_changed` + `points_changed`——即 `classic_parser` 目前产出的那些)。热路径方法
|
||||
(`move` / `attack` / `set_target` / `say` / `character_position` / `click_npc` / `script_answer` /
|
||||
`use_skill` / `move_item` / `use_item` / `drop_item{,_count}` / `pickup_item` / `select_character`)
|
||||
加 `if (classic_sess) return classic_sess->send_*(...)` 前置分支。读方法
|
||||
(`get_entity` / `get_entities` / `get_main_vid` / `get_points`)经新 `active_world()` 取
|
||||
`classic_sess ? &classic_sess->world() : &game->world()`。guild/cube/marks 已接入 classic:
|
||||
公会/战争/技能走 `CG_GUILD`,会徽走带当前 handle/random_key 的独立 mark 连接,Cube 走
|
||||
40250 的 `/cube r_info`、`/cube add/delete`、`/cube make` 命令;classic 下原来因 `game == null`
|
||||
而 no-op 的 M2Client 分支已消除。仍不伪造 vendor-only HackShield/XTrap 响应和未在 40250
|
||||
源码登记的可选包。
|
||||
ctest 13/13,Godot editor import 干净,抽样 GDScript 测试通过,iOS/Android 绿。
|
||||
- **step 5 — `classic_cipher`(`_IMPROVED_PACKET_ENCRYPTION_`)** ✅(mac + iOS + Android 全绿):
|
||||
- **Crypto++ 8.4.0 vendored**:`extension/third_party/cryptopp/`(从 `ClientVS22/extern/include/
|
||||
cryptopp` 拷 393 个 `.h/.cpp/.S`,非 submodule)+ 自写 `CMakeLists.txt`(glob `.cpp`,剔除
|
||||
test/bench/validat/regtest/adhoc/fips,`CRYPTOPP_DISABLE_ASM/SSSE3/AESNI` 全平台纯 C++)。
|
||||
`mt3p::cryptopp`,链进 `mtnet`。mac 编译干净。
|
||||
- `classic/classic_cipher.{h,cpp}` —— 近乎照搬 `EterBase/cipher.cpp`:`DH2KeyAgreement`
|
||||
(RFC 5114 1024-bit MODP + `DH2` 静态+临时密钥对,`prepare()` 生成 `spub||epub` blob,`agree()`
|
||||
出 `SecByteBlock shared`)+ `BlockCipherAlgorithm::Pick(hint % 14)` 选块密码(默认 Twofish)+
|
||||
`set_up(polarity)` 从 `shared` 切 key/iv 建两个 `CTR_Mode<T>` 实例(`polarity` 决定收发各用哪个)。
|
||||
- `classic_stream` 接进去:`GC_KEY_AGREEMENT`(0xFB) → `prepare()` + `activate(polarity, …)` +
|
||||
回发 `CG_KEY_AGREEMENT`(明文);`GC_KEY_AGREEMENT_COMPLETED`(0xFA) → `set_activated(true)` +
|
||||
立刻解密缓冲区里 server 可能 pipeline 的密文尾巴。激活后 `emit_bytes()` 对所有出站字节
|
||||
(含 seq 字节)CTR 加密、`decrypt_appended()` 对每个 recv/feed chunk 解密。`set_polarity(true)`
|
||||
= 客户端。
|
||||
- CTest `net.classic_cipher`(DH2 双端协商 + 加解密对称往返 + CTR 连续性 + 会话隔离)、
|
||||
`net.classic_encstream`(`ClassicStream` 全流程:握手 → KEY_AGREEMENT 交换 → COMPLETED →
|
||||
加密 GC_PHASE 解出 + 出站 CG_MOVE 服务端能解 + pipelined COMPLETED+密文一个 feed)。
|
||||
mac ctest 15/15。
|
||||
- **交叉编译**:iOS arm64 ✅、Android arm64 ✅(Android 需把 NDK 的
|
||||
`sources/android/cpufeatures/cpu-features.c` 编进 `cryptopp` + 加到 include path ——
|
||||
`cpu.cpp` 无条件 `#include "cpu-features.h"`;已在 `cryptopp/CMakeLists.txt` 的 `if(ANDROID)` 里处理)。
|
||||
`.so` 从 46 MB → 59 MB。Godot editor import 干净,ctest 15/15。
|
||||
- **step 4c — `classic_parser` 铺开 GC(items)** ✅:接了 `GC_ITEM_SET`(21) / `GC_ITEM_DEL`(20) /
|
||||
`GC_ITEM_UPDATE`(25) / `GC_ITEM_GROUND_ADD`(26) / `GC_ITEM_GROUND_DEL`(27)。40250 的
|
||||
`TPlayerItemAttribute` 是 3 字节 `pack(1)`(`{BYTE type; short value}`),跟 `EntityStore::Item.attrs`
|
||||
的 stride 不同 → parser 里逐元素解包。`EntityStore` 加 `mut_item_set / mut_item_del /
|
||||
mut_item_update / mut_ground_add / mut_ground_del`(包 `mut_slot()` + `m_inv_changes` /
|
||||
`m_ground`)。`M2Client`:`get_inventory / get_equipment / get_item / get_ground_items` 改走
|
||||
`active_world()`;`pump_classic()` 补 `drain_inv` → `inventory_changed`、`drain_ground` →
|
||||
`ground_item_added/removed`。`net.classic_session` 扩展验证 item_set/update/del + ground add/del。
|
||||
ctest 15/15,iOS/Android 绿。
|
||||
- **step 4d — `classic_parser` GC 实体状态簇** ✅:`GC_CHARACTER_UPDATE`(19,装备外观/公会/善恶/骑乘) /
|
||||
`GC_CHANGE_SPEED`(18) / `GC_CHARACTER_POSITION`(43) / `GC_WALK_MODE`(111) / `GC_STUN`(13) /
|
||||
`GC_DEAD`(14) / `GC_MOTION`(36) / `GC_TARGET`(63,目标血条 %)。`EntityStore` 加 `mut_char_update /
|
||||
mut_set_position / mut_change_speed / mut_walk_mode / mut_stun / mut_dead / mut_motion / mut_target`
|
||||
(包 `m_ents` / `m_motions` / `m_target_*` / `m_mount_changes`)。`pump_classic()` 补
|
||||
`drain_motions`→`motion`、`drain_damage`→`damage`、`drain_mount_changes`→`mount_changed`、
|
||||
`take_target_dirty`→`target_info`。`get_target()` 走 `active_world()`。`net.classic_session`
|
||||
扩展验证。ctest 15/15,iOS/Android 绿。
|
||||
- **step 4e — `classic_parser` HUD 簇** ✅:`GC_SKILL_LEVEL`(76,`TPlayerSkill` 6B/条,`time_t`
|
||||
32 位) / `GC_QUICKSLOT_ADD/DEL/SWAP`(28/29/30) / `GC_AFFECT_ADD/REMOVE`(126/127,`TPacketAffectElement`
|
||||
21B) / `GC_DAMAGE_INFO`(135) / `GC_CHAR_ADDITIONAL_INFO`(136,补 spawn 的 name/parts/level/guild) /
|
||||
`GC_WHISPER`(34,dynamic —— 客户端 map 登记 STATIC 但 `wSize` 恰在 `TDynamicSizePacketHeader.size`
|
||||
偏移,走 dynamic 路径 ok) / `GC_SYNC_POSITION`(5,dynamic)。`EntityStore` 加 `mut_skill_levels /
|
||||
mut_quickslot_{set,del,swap} / mut_affect_{add,remove} / mut_damage / mut_char_info /
|
||||
mut_snap_position / mut_chat / mut_whisper`。`GC_CHAT`/`GC_WHISPER` 改走 `EntityStore::m_chat`
|
||||
队列(跟 m2dev 一致,`pump_classic` 的 `drain_chat` 统一处理),删掉 parser 的 `on_chat` 回调。
|
||||
`pump_classic()` 补 skills/quickslots/affects 的 drain;`get_skills/get_quickslots/get_affects/
|
||||
get_skill_group` 走 `active_world()`。ctest 15/15,iOS/Android 绿。
|
||||
- **step 4f — `classic_parser` party** ✅:`GC_PARTY_INVITE`(77,`leader_vid`) / `GC_PARTY_ADD`(78) /
|
||||
`GC_PARTY_UPDATE`(79,role/hp%/affects[7]) / `GC_PARTY_REMOVE`(80) / `GC_PARTY_LINK`(91) /
|
||||
`GC_PARTY_UNLINK`(92) / `GC_PARTY_PARAMETER`(83,分配模式)。`EntityStore` 加 `mut_party_{invite,add,
|
||||
update,remove,link,unlink,parameter}`。`pump_classic()` 补 `drain_party_invites`→`party_invite_ask`、
|
||||
`party_dirty`→`party_changed`。`get_party` / `get_party_distribute_mode` 走 `active_world()`。
|
||||
ctest 15/15,iOS/Android 绿。
|
||||
- **step 4g — `classic_parser` NPC shop** ✅:`GC_SHOP`(38,dynamic 子头) —— `START`(0,单货架
|
||||
`packet_shop_start{u32 owner_vid; packet_shop_item[40]}`,`packet_shop_item`=43B) /
|
||||
`START_EX`(10,多货架 `{u32 vid; u8 tab_count} + tab_count×{name[32]; coin_type; item[40]}`) /
|
||||
`END`(1) / 错误子头(`NOT_ENOUGH_MONEY`/`SOLDOUT`/`INVENTORY_FULL`/`INVALID_POS`)。
|
||||
`EntityStore` 加 `mut_shop_open(vid, tabs) / mut_shop_close() / mut_shop_error(code)`。
|
||||
`pump_classic()` 补 `shop_dirty`→`shop_opened`/`shop_closed`、`drain_shop_errors`→`shop_error`。
|
||||
`is_shop_open` / `get_shop_items` / `get_shop` 走 `active_world()`。ctest 15/15,iOS/Android 绿。
|
||||
- **step 4h — `classic_parser` quest / NPC 对话** ✅:`GC_SCRIPT`(45,dynamic,`{skin; u16 src_size;
|
||||
text}`) / `GC_QUEST_CONFIRM`(46,STATIC 74B) / `GC_QUEST_INFO`(81,dynamic,`{u16 index; u8 flag}` +
|
||||
flag 驱动 title/clock/counter/icon 串)。`EntityStore` 加 `mut_script / mut_quest_confirm /
|
||||
mut_quest_info`。`pump_classic()` 补 `drain_scripts`→`script_dialog`、`drain_confirms`→
|
||||
`quest_confirm_ask`、`drain_quest_changes`→`quest_info`。`get_quests` 走 `active_world()`。
|
||||
ctest 15/15,iOS/Android 绿。至此 `click_npc`→`GC_SCRIPT` 对话闭环。
|
||||
- **step 4i — classic P8 social / storage** ✅:
|
||||
- `GC_MESSENGER` / `CG_MESSENGER`:好友快照、上线/下线与手机状态,以及按 40250 客户端格式发送加好友/删好友。
|
||||
- `GC_EXCHANGE` / `CG_EXCHANGE`:交易开始、双方物品/金币/接受状态、结束/错误提示,以及全部客户端操作。
|
||||
- `GC_SAFEBOX_*` / `CG_SAFEBOX_*`:仓库开启、资金、物品快照/删除、密码错误、存入/取出/移动。
|
||||
- `GC_MALL_*` / `CG_MALL_CHECKOUT`:商城开启、物品快照/删除、取出;均接入 `EntityStore`、
|
||||
`M2Client` 读接口、信号和 classic session intent。`net.classic_wire` / `net.classic_session` 覆盖
|
||||
包尺寸、字段布局、序列字节和状态变更;macOS `mtgodot` 构建通过。
|
||||
- **step 4j — classic P9 / remaining fixed packets** ✅:补齐 `GC_WARP` 跨频道重连、`GC_TIME` /
|
||||
`GC_CHANNEL`、NPC/土地/目标标记、恋人、观察者、坐骑/飞行、PVP、`GC_DUEL_START` 对手 VID 列表、装备查看、挖矿/钓鱼、炼化、
|
||||
技能冷却和特殊效果;并消费登录安全包、BGM 主角包、`CHARACTER_ADD2/UPDATE2`。龙魂兼容代码不纳入目标服务端支持声明。
|
||||
不具备 40250 vendor runtime 的 HackShield/XTrap 请求只做定长消费,不伪造响应。
|
||||
- **step 4k — Cube + private shop semantics** ✅:classic Cube 使用 40250 `cmd_general.cpp` 的
|
||||
`/cube r_info`、`/cube r_info <start> <count>`、`/cube add <cube_index> <inventory_index>`、
|
||||
`/cube delete <cube_index>`、`/cube make`;UI 提供背包→24 格材料槽的添加/删除。个人摊位收摊
|
||||
使用空 `CG_MYSHOP`,与 40250 `CHARACTER::OpenMyShop()` 的关闭语义一致。
|
||||
- **step 4l — UI/uiscript复核** ✅:好友、交易、仓库、商城、NPC 商店、会徽、Cube、炼化和个人摊位
|
||||
窗口均通过既有 M2Client 信号/intent 接入;公会窗口暂缓,龙魂窗口不纳入目标服务端支持范围。Cube UI 的旧“直接按配方 index 制作”
|
||||
已改为先装载材料槽再制作,避免把 classic index 当成服务端参数;PVP/决斗状态也会在世界标签中
|
||||
刷新,`MobView.set_display_name()` 对无内置名字节点的真模型提供同样的名字牌回退。
|
||||
- **step 4m — 公会完整状态与会徽原生协议** ⏸️ 暂缓:公会在线/离线、公告列表、
|
||||
邀请、战争积分等 GC 子包,并接入 `EntityStore`、`M2Client` 和公会窗口;公告发布/刷新/删除、
|
||||
邀请接受/拒绝都有对应 CG 路径;已有代码保留但公会整体不纳入当前阶段。会徽独立连接也已改为 40250 的单字节头、`bufSize` 分片、CRC、
|
||||
符号包和上传布局;并按原客户端在首个 `GC_HANDSHAKE` 后发送 `CG_MARK_LOGIN` 的时序处理,
|
||||
兼容随后是否进入可选 DH2/CTR;`net.classic_mark` 固定了下载与上传的关键字节协议。
|
||||
- **step 4n — 握手重试与时钟同步时序** ✅:`PHASE_HANDSHAKE` 中的重试仍回发
|
||||
`GC_HANDSHAKE`(0xff),进入 LOGIN/SELECT/GAME 后收到的后续握手才改发带 sequence 的
|
||||
`CG_TIME_SYNC`(0xfc);会徽独立连接保持首个握手后的 `CG_MARK_LOGIN` 路径,避免把两种连接的
|
||||
控制包时序混用。`net.classic_stream` 和 `net.classic_mark` 已分别覆盖这两个边界。
|
||||
- **step 4o — 好友邀请确认闭环** ✅:40250 的 `GC_CHAT/COMMAND` 命令
|
||||
`messenger_auth <name>` 不再静默丢弃,现会产生 `friend_invite_ask`;好友窗口显示接受/拒绝
|
||||
对话框,并通过 `/messenger_auth y|n <name>` 回发原生服务端命令。`net.entity_store` 与
|
||||
`p8_test.gd` 覆盖邀请入队、弹窗和两种回答;`netbridge_test.gd` 同时校验 P8/P9、会徽和精炼
|
||||
API 都实际绑定到 Godot;公会与龙魂不作为当前交付验收项。
|
||||
- **step 4p — 40250 命令总线补齐** ✅:`PartyRequest`/`PartyRequestDenied`、`CloseSafebox`/
|
||||
`CloseMall`、`ShowMeSafeboxPassword`/`ShowMeMallPassword`、`RefineSuceeded`/`RefineFailed`、
|
||||
`OpenPrivateShop`、`MyShopPriceList` 和服务端回传的 `setblockmode` 已从静默 no-op 改为
|
||||
`EntityStore` typed event;M2Client 暴露对应 signal,仓库/商城会弹密码框,错误密码会提示,
|
||||
精炼结果会回显,个人摊位可由服务端命令打开并使用价格回填,游戏设置会按服务端屏蔽掩码同步。
|
||||
`net.entity_store` 覆盖这些命令及仓库关闭状态。
|
||||
- **step 4q — 其余有状态的 ServerCommand** ✅:40250 客户端原生处理的
|
||||
`ObserverMode`/`ObserverCount`、`StoneDetect`、`StartStaminaConsume`/`StopStaminaConsume`、
|
||||
`sms`/`nosms`、`mobile_auth` 和 `combo` 已进入 `EntityStore` 状态及 typed event;
|
||||
M2Client 提供观战/体力/手机/连击 getter 和对应 signal(体力按服务端下发速率递减,探石保留
|
||||
转换后的角度),因此 classic 不会在这些回包上静默丢状态。`ConsoleEnable`、`BettingMoney`、
|
||||
`ObserverTeamInfo` 仍保持 40250 原客户端的无可见副作用语义;不伪造它们不存在的 Godot 功能。
|
||||
- **step 4r — GC_MOTION 与表情命令** ✅:`GC_MOTION` 不再只由 M2Client 发出后被场景丢弃,
|
||||
NetWorld 会驱动本地/远端实体,并按 40250 `CRaceMotionData` 的职业变体选择真实 `.msa`。
|
||||
`kiss`、`french_kiss`、`slap`、`clap`、`cheer1/2`、`dance1..6`、`dig_motion` 及其余
|
||||
`congratulation/forgive/angry/.../joy` ServerCommand 也映射到同一 motion 流;`gift` 会触发
|
||||
可见通知。40250 源码只提供 `Gift_Show()` 通知入口,没有随命令携带奖励或领取 CG,因此不虚构
|
||||
礼物奖励数据协议。
|
||||
- **step 4s — 版本门禁与无阵营账号** ✅:补齐账号未绑定阵营时的 `CG_EMPIRE` 选择入口,并在
|
||||
收到 `GC_MAIN_CHARACTER`、`GC_MAIN_CHARACTER3_BGM` 或 `GC_MAIN_CHARACTER4_BGM_VOL` 后按
|
||||
40250 原客户端时序发送一次 `CG_CLIENT_VERSION`(`1215955205`)及 sequence。服务端
|
||||
`input_main.cpp` 会在首次 `CG_MOVE` 前拒绝没有版本上报的连接;classic session 现在覆盖这个
|
||||
门禁,离线测试同时锁定版本字段和序号推进。
|
||||
- **step 4t — 强制改名 UI 闭环** ✅:`SimplePlayer.change_name` 已透传到 classic / m2dev
|
||||
角色槽位;选人页在该标志存在时按 40250 原版行为拦截“开始”并打开改名窗,发送
|
||||
`CG_CHANGE_NAME`,再由 `GC_CHANGE_NAME` 刷新角色名并清除强制改名标志。`AppFlow` 已接通请求、
|
||||
成功和失败回传,`char_create_delete_test.gd` 覆盖短名拒绝、发送参数和成功状态。强制改名的
|
||||
重名 / 非法名错误码与真实服务端验收暂缓。
|
||||
- **step 4u — 创建公会 UI 闭环** ⏸️ 暂缓:`GC_REQUEST_MAKE_GUILD` 现在会触发公会窗的创建名称输入,
|
||||
按 40250 原版流程校验空名、控制字符和 12 字上限,再调用 `CG_ANSWER_MAKE_GUILD`;发送失败会
|
||||
保留窗口以便重试。已有 `guild_creation_ui_test.gd` 覆盖服务端请求、名称校验和合法名称应答。
|
||||
公会完整页面、服务端敏感词 / 重名错误回包、实际建会状态和权限暂不纳入本阶段。
|
||||
- **step 4v — 任务选魔石窗口复核** ✅:确认 `[SELECT_ITEM]` 已从 `QuestDialog` 接到
|
||||
`SelectItemUI`,按原 `uiselectitem.RefreshSlot` 过滤魔石和 grade、将选择窗格映射回背包格,
|
||||
选择时发送 `CG_SCRIPT_SELECT_ITEM`,关闭时发送 0;`select_item_ui_test.gd` 已覆盖过滤和选择。
|
||||
原版 CJK IME、完整资源视觉及真实脚本业务回包验收暂缓。
|
||||
- **step 4w — 队伍角色 / 能量 / 私店招牌复核** ✅:按 40250 `CParty::Update` 修正
|
||||
`party_ui.gd` 的角色 6 为 `HASTE`(迅捷),并补齐 `affects[7]` 槽位语义;攻击/坦克/迅捷/辅助/
|
||||
宗师/防御分别按 Leadership 10/20/20/25/35/40 门控,组队治疗按 Leadership 18 门控,
|
||||
`p8_test.gd` 覆盖低等级禁用路径。Cube 的 `POINT_ENERGY` 已由 HUD 快照显示;`GC_SHOP_SIGN`
|
||||
已由 `NetWorld` 提供 `ShopSign` Label3D fallback,`GC_SYMBOL_DATA` 已接会徽下载链路;个人摊位
|
||||
已支持标准 Godot 拖放与点击选取双路径,Cube 已校验 `cube_open(npc)` 前置、配方材料、金币和
|
||||
制作状态。缺失共享 `.sub` 贴图时使用现有 fallback,不阻断窗口逻辑;live FreeBSD + MySQL E2E 暂缓。
|
||||
- **step 4x — P8 交互边界与目标服务端范围** ✅:NPC/石头/传送点点选统一进入
|
||||
`click_npc`,服务端 `cube_open` 才打开 Cube;商店购买/出售支持数量控件和错误回显,交易栏位按
|
||||
服务端已占用槽位分配并拒绝重复/超限,仓库/商城拒绝满背包或满仓位,密码弹窗可正常确认/取消。
|
||||
40250 目标服务端没有龙魂玩法,`GameScene` 不再创建龙魂入口、不消费其背包右键或快捷键;
|
||||
`ds_refine` 兼容 API 仍保留。
|
||||
- **本机回归结果**:C++/协议 CTest 16/16 通过(包含 `net.loopback_flow`);本次抽样的
|
||||
`project/*_test.gd` 共 38 个脚本全部通过;系统设置测试已使用 headless 临时配置并对缺失可选
|
||||
贴图安全降级。当前沙箱禁止 loopback bind,因此 `channel_status_test` 明确跳过本机 mock;
|
||||
有网络权限时仍执行完整请求/响应断言,不是 40250 classic 协议失败。
|
||||
- **当前收尾项**:协议 / 状态链路、classic UI 交互和离线回归测试已完成;真实 FreeBSD + MySQL
|
||||
binary E2E 暂缓,公会功能暂缓,龙魂因目标 40250 服务端不提供而排除。`tools/40250/` 的
|
||||
build/setup/start/stop/probe 入口保留,待后续恢复真服验收时使用。
|
||||
+66
-24
@@ -8,10 +8,14 @@
|
||||
> 渲染细节另见 `PARITY-GAP.md`,动画、材质和资产管线另见 `BACKLOG.md`,网络实现另见
|
||||
> `CLIENT-PORT.md`。
|
||||
>
|
||||
> 更新日期:2026-08-31(增量 38)
|
||||
> 更新日期:2026-09-01(增量 45)
|
||||
>
|
||||
> **状态列图例**:✅ 达到本文末“1:1 完成判据” · 🟡 有首版 / 部分语义 · ⬜ 未实现 ·
|
||||
> ❌ 明确不做(超出 m2dev-client 参考范围,除非另立需求)。
|
||||
|
||||
> **当前 40250 范围调整(增量 45)**:真实 FreeBSD + MySQL 服务端的完整登录 / 功能验收暂缓,
|
||||
> 不作为本阶段交付门槛;公会相关功能暂缓。当前目标 40250 服务端不提供龙魂玩法,龙魂不纳入本阶段
|
||||
> 支持范围,已有兼容代码不代表服务端可用;运行时龙魂入口已禁用,但保留 `ds_refine` 等协议兼容代码。
|
||||
>
|
||||
> ### 不做清单(❌)
|
||||
>
|
||||
@@ -21,9 +25,38 @@
|
||||
> - **反作弊(anti-cheat)**:`CG_HACK`(0x0B03) 客户端异常上报,以及桌面外壳的
|
||||
> `ProcessCRC` / `ProcessScanner` / 内存扫描等。自用客户端不需要,**标注为不做**,
|
||||
> 除非服务端强制校验才回头做最小上报。
|
||||
> - **龙魂精炼(当前 40250 目标服务端)**:目标服务端不提供该玩法,本阶段不实现 / 不验收;
|
||||
> 客户端保留的兼容代码不计入当前支持范围。
|
||||
>
|
||||
> ### 变更记录
|
||||
>
|
||||
> - **2026-09-01 增量 45(P8 交互边界与龙魂运行时范围)**:补齐 NPC 点击路由、Cube NPC
|
||||
> 前置 / 配方材料 / 金币校验与制作状态、商店购买 / 出售数量和错误反馈、交换重复 / 满槽 /
|
||||
> 金币边界、仓库 / 商城密码与满包回滚、个人摊位标准 Godot 拖放路径;缺失可选 `.sub` 贴图
|
||||
> 安全降级。龙魂入口运行时禁用,公会和真实服务器验收继续按当前范围暂缓。
|
||||
> - **2026-09-01 增量 44(交付范围调整)**:真实 FreeBSD + MySQL 完整验收、公会相关功能均
|
||||
> 暂缓;当前目标 40250 服务端不提供龙魂玩法,龙魂从本阶段支持范围排除。保留本地协议 / 状态 / UI
|
||||
> 首版与离线回归结果,后续恢复服务端条件后再单独推进 deferred 项。
|
||||
> - **2026-09-01 增量 43(队伍角色 / 能量 / 私店招牌复核)**:对照 40250 服务端
|
||||
> `CParty::Update` 修正队伍角色 6 为 `HASTE`(迅捷),补齐 `affects[7]` 的服务端槽位语义,
|
||||
> 并在 `party_ui.gd` 按 Leadership 10/20/25/35/40 门控攻击/坦克/迅捷/辅助/宗师/防御,
|
||||
> Leadership 18 门控组队治疗;`p8_test.gd` 覆盖低 Leadership 的禁用路径。Cube HUD 已有
|
||||
> `POINT_ENERGY` 快照显示;`GC_SHOP_SIGN` 已解析并在 `NetWorld` 以 `ShopSign` Label3D
|
||||
> 作为无 `.sub` 资源 fallback,`GC_SYMBOL_DATA` 也已有会徽下载链路。剩余仍是原版资源、真
|
||||
> `.sub` 拖放 / 视觉和 Cube NPC 交互;live FreeBSD + MySQL E2E 暂缓。
|
||||
> - **2026-09-01 增量 42(选魔石窗口闭环复核)**:确认 `GC_SCRIPT` 的 `[SELECT_ITEM]` 已由
|
||||
> `QuestDialog` 转发到 `SelectItemUI`;选择窗按原 `uiselectitem.RefreshSlot` 过滤魔石 / grade、
|
||||
> 映射背包格并发送 `CG_SCRIPT_SELECT_ITEM`,关闭发送 0,`select_item_ui_test.gd` 已覆盖。仍需
|
||||
> 完整原版资源、图标和 CJK 输入环境下的视觉验收。
|
||||
> - **2026-09-01 增量 41(创建公会 UI 闭环)**:`GC_REQUEST_MAKE_GUILD` 触发公会创建窗口,
|
||||
> 对应 `CG_ANSWER_MAKE_GUILD` 已接入名称输入、12 字上限、空名 / 控制字符校验和发送失败提示;
|
||||
> 新增 `guild_creation_ui_test.gd` 覆盖请求、校验和合法名称应答。服务端敏感词 / 重名错误及实际建会
|
||||
> 状态仍需真服验收。
|
||||
> - **2026-09-01 增量 40(强制改名 UI 闭环)**:`SimplePlayer.change_name` 已从 classic / m2dev
|
||||
> 角色列表透传到 `M2Client.char_list`;选人页按 40250 原版语义,在角色带该标志时点击“开始”
|
||||
> 弹出改名窗,校验名称后发送 `CG_CHANGE_NAME`,成功的 `GC_CHANGE_NAME` 刷新槽位并清除标志,
|
||||
> `AppFlow` 接通请求、成功和失败回传,新增 `char_create_delete_test.gd` 回归覆盖。P1 行不再把
|
||||
> 改名 UI 列为未实现;仍需真服验证强制改名、重名 / 非法名错误码和服务端状态。
|
||||
> - **2026-08-31 增量 30(EterGrnLib 对齐复核)**:查代码确认以下已落地,表内相关行同步。
|
||||
> ① **材质 blend / two-sided 改读 GR2 材质信息**(`metin2_model::decide_blend/decide_two_sided`
|
||||
> 优先 `gr2::MaterialInfo.alpha_blend/two_sided` = `dump_materials` 端口 `Material.cpp:233`,
|
||||
@@ -63,17 +96,18 @@
|
||||
> - **2026-08-31 增量 37(组队成员信息板 + `CG_PARTY_SET_STATE`)**:
|
||||
> ① **C++**:`wire.h` 加 `CGPartySetState{u16 hdr,u16 len,u32 pid,u8 role,u8 on}`(断言 10)
|
||||
> + `EPartyRole` 枚举(NORMAL 0/LEADER 1/ATTACKER 2/TANKER 3/BUFFER 4/SKILL_MASTER 5/
|
||||
> BERSERKER 6/DEFENDER 7);`game_client.send_party_set_state(pid,role,on)`;
|
||||
> HASTE 6/DEFENDER 7);`game_client.send_party_set_state(pid,role,on)`;
|
||||
> `M2Client.party_set_state(pid,role,on)` + bind;`get_party()` dict 补 `state`(完整角色字节)
|
||||
> 和 `affects[7]`。
|
||||
> ② **`party_ui.gd`** 从「名字 + 一条 HP」重做成 1:1 迁移 `assets/root/uiparty.py`
|
||||
> `PartyMemberInfoBoard` + `partymemberinfoboard.py` 布局:每员 strip = 角色状态按钮
|
||||
> (队长可点 → 弹角色菜单 普通/攻击/坦克/狂战/辅助/宗师/防御 → `party_set_state`;普通 =
|
||||
> (队长可点 → 弹角色菜单 普通/攻击/坦克/迅捷/辅助/宗师/防御 → `party_set_state`;普通 =
|
||||
> 清当前角色 `on=false`;+ 踢出 → `party_leave` = `SendPartyRemovePacket`)+ 名字(+★队长)
|
||||
> + HP gauge + `affects[7]` 非零 → 附加效果 chip(tooltip 带值,槽位名暂定)。点名字 →
|
||||
> `set_target(vid)`。顶部:EXP 分配开关(`party_set_distribute` 0 不均分 / 1 均分)+
|
||||
> 组队治疗(`party_use_skill(PARTY_SKILL_HEAL=1, 0)`)。队长判定 = `get_party()` 里本地
|
||||
> `get_main_vid()` 那条的 `leader`。角色菜单按党技能等级门控(Tanker≥10…Defender≥40)暂略。
|
||||
> `get_main_vid()` 那条的 `leader`。角色菜单按 40250 服务端 Leadership 门控:攻击≥10、坦克/迅捷≥20、
|
||||
> 辅助≥25、宗师≥35、防御≥40;治疗要求 Leadership≥18。
|
||||
> ③ **修 `p8_test` 隐患**:`_init` 原来 `_run()` 未 `await`,`_run` 第一个 `await process_frame`
|
||||
> 之后的所有断言(含增量 34 的 `shop_ex` 多货架)从未真正生效 —— 改成 `await _run()` 后
|
||||
> 暴露 `shop_ui.refresh()` 的 `queue_free` 延迟 bug(旧 row 当帧未消失 → `get_child_count`
|
||||
@@ -224,6 +258,11 @@
|
||||
> `get_observers()` 与 `land_areas_changed` / `observer_event` 已接入;小地图现在消费观战者
|
||||
> 状态并绘制紫色菱形标记。实体、回环、Godot 桥接和 `p9_test.gd` 覆盖。缺失 header 清单由
|
||||
> 5 项降为 **1 项**(仅剩 `CG_TEXT`,参考端未发现实际发送方)。
|
||||
> - **2026-09-01 增量 39(`CG_TEXT` wire 尺寸覆盖)**:按 40250 `TPacketCGText` /
|
||||
> `CInputHandshake::Analyze` 补齐 `CG_TEXT` 的 1 字节固定前缀和 classic `packet_size_cg()`
|
||||
> 表项,并加入 wire 回归断言。该包后续文本由服务端按换行特殊解析,仅作为握手阶段管理通道;
|
||||
> Cube 继续使用已验证的 `CG_CHAT`/`CHAT_TYPE_COMMAND`,不把管理命令暴露为普通游戏 API。
|
||||
> 主网络层缺失 header 清单由 1 项降为 0 项。
|
||||
> - **2026-08-31 增量 19(钓鱼动作消费)**:`NetWorld` 和 `NetPlay` 现在消费
|
||||
> `fishing_event` 的 START / STOP / REACT / SUCCESS / FAIL 子头,记录方向并尝试驱动远端及
|
||||
> 本地主角的钓鱼动作;FISH 子头按参考客户端语义保留为鱼获物品事件,不误当作角色 VID。
|
||||
@@ -272,7 +311,7 @@
|
||||
> - **2026-08-31 增量 3**:两个 ⬜ 项落地首版 —— **私人商店 / 道具商城**
|
||||
> (`CG_MYSHOP` / `GC_MALL_OPEN|SET|DEL` / `CG_MALL_CHECKOUT` + `private_shop_ui` /
|
||||
> `mall_ui`)、**Cube 제작**(`GC_CHAT/COMMAND` 的 `cube …` 总线 → `CubeState` +
|
||||
> `cube_ui`)。仍缺能量条、原版拖放 UI 细节。
|
||||
> `cube_ui`)。能量值已由 `POINT_ENERGY` 快照接入 HUD;仍缺原版拖放 UI 细节。
|
||||
> **拍卖行**经核查 m2dev-client 无此系统 → 标注 ❌ 不做(见上「不做清单」)。
|
||||
> **反作弊**(`CG_HACK` + `ProcessCRC`/`ProcessScanner`)→ 标注 ❌ 不做。
|
||||
> **「协议覆盖审计」段**改为逐条清单:脚本核对 `wire.h` vs `Packet.h`,主网络层
|
||||
@@ -291,7 +330,7 @@
|
||||
## 结论
|
||||
|
||||
`mtgodot-poc` 已经不是早期“只能登录、移动和攻击”的骨架:登录到 PHASE_GAME、基础战斗、
|
||||
背包 / 装备、技能 UI、任务、聊天、组队 / 好友、商店 / 交易 / 仓库、公会、精炼、龙魂、
|
||||
背包 / 装备、技能 UI、任务、聊天、组队 / 好友、商店 / 交易 / 仓库、精炼,
|
||||
地图和部分特效都已有可运行首版。
|
||||
|
||||
当前的主要差距已从“有没有功能”转为:
|
||||
@@ -313,7 +352,7 @@
|
||||
- 背包、基础装备换模、地面物品、技能窗口和快捷栏读取;
|
||||
- 聊天、私聊、基础任务对话和任务日志;
|
||||
- 组队、好友、情侣名 / 爱意值状态、NPC 商店、交易、仓库;
|
||||
- 公会、公会技能、公会战、会徽上 / 下载、精炼、龙魂精炼;
|
||||
- 公会、公会技能、公会战、会徽上 / 下载、精炼(公会相关验收暂缓);
|
||||
- 地形、建筑、水体、**NPC / 怪物真模型(vnum→npclist.txt→目录)**、小地图、频道、昼夜和天气首版;
|
||||
- GR2 模型 / 动画、换装首版、武器 / 盾挂点、距离 LOD / 淡入、角色接触阴影、
|
||||
`item_proto` 驱动的球面高光和 `.mse` 粒子首版。
|
||||
@@ -324,7 +363,7 @@
|
||||
|
||||
| 优先级 | 状态 | 模块 | 当前实现 | 需要迁移的原客户端语义 |
|
||||
|---|---|---|---|---|
|
||||
| P1 | 🟡 | 选人 / 建号 / 删号 / 改名 | `CG_CHARACTER_CREATE`(0x0201) / `CG_CHARACTER_DELETE`(0x0202) / `CG_CHANGE_NAME`(0x010B) / `GC_PLAYER_CREATE_SUCCESS`(0x020C) / `GC_PLAYER_CREATE_FAILURE`(0x020D) / `GC_PLAYER_DELETE_SUCCESS`(0x020E) / `GC_PLAYER_DELETE_WRONG_SOCIAL_ID`(0x020F) / `GC_CHANGE_NAME`(0x010C) 全部有尺寸断言;`GameClient` 发送接口、角色槽位补丁和 `drain_*_events()`;`M2Client` 同名建删改 API、4 个建删结果信号及 `char_name_changed`;`net.loopback_flow` 覆盖建删和改名名称字节 / pid 广播。**真机验证(2026-08-31,`net_e2e MT_E2E_CHARCREATE=1`)**:建号、`GC_PLAYER_CREATE_FAILURE`、`GC_EMPIRE` 已验。选人页有建号 / 删号弹窗与国家国旗 | `CG_CHARACTER_DELETE` 破坏性,真机未验;改名 UI / 改名卡消耗和重复名错误提示未接;建号缺外形 / 发型选择与属性再分配 UI(现发 `shape=0` + 职业基础四维);缺 `SELECT_EMPIRE` 选国界面;原版选人台座、镜头动画、`OnCreateFailure` 完整错误码文案 |
|
||||
| P1 | 🟡 | 选人 / 建号 / 删号 / 改名 | `CG_CHARACTER_CREATE`(0x0201) / `CG_CHARACTER_DELETE`(0x0202) / `CG_CHANGE_NAME`(0x010B) / `GC_PLAYER_CREATE_SUCCESS`(0x020C) / `GC_PLAYER_CREATE_FAILURE`(0x020D) / `GC_PLAYER_DELETE_SUCCESS`(0x020E) / `GC_PLAYER_DELETE_WRONG_SOCIAL_ID`(0x020F) / `GC_CHANGE_NAME`(0x010C) 全部有尺寸断言;`GameClient` 发送接口、角色槽位补丁和 `drain_*_events()`;`M2Client` 同名建删改 API、4 个建删结果信号及 `char_name_changed`;`net.loopback_flow` 覆盖建删和改名名称字节 / pid 广播。**真机验证(2026-08-31,`net_e2e MT_E2E_CHARCREATE=1`)**:建号、`GC_PLAYER_CREATE_FAILURE`、`GC_EMPIRE` 已验。选人页有建号 / 删号弹窗、强制改名弹窗与国家国旗 | `CG_CHARACTER_DELETE` 破坏性,真机未验;强制改名、重名 / 非法名错误码和服务端状态未真服验收;建号缺外形 / 发型选择与属性再分配 UI(现发 `shape=0` + 职业基础四维);原版选人台座、镜头动画、`OnCreateFailure` 完整错误码文案 |
|
||||
| P0 | 🟡 | 技能施放协议 | `CG_USE_SKILL`、`CG_FLY_TARGETING`、`CG_ADD_FLY_TARGETING` 与 `CG_SHOOT` 已有尺寸断言、原生发送接口和 loopback 字节验证;服务端 `GC_FLY_TARGETING` / `GC_ADD_FLY_TARGETING` 已进入 `EntityStore`,通过 `M2Client.fly_targeting` 暴露,且已补入 `GameClient` 世界分派;`GC_CHANGE_SKILL_GROUP` 会更新 `get_skill_group()` 并使技能快照失效;快捷栏先发 `use_skill(skill_id,target_vid)`,再同步 `CG_MOVE` 技能动作;弓技能会在 `.msa` FLY 事件发送 `CG_SHOOT`。真服单会话已发送 `CG_USE_SKILL` / `CG_FLY_TARGETING` / `CG_ADD_FLY_TARGETING` / `CG_SHOOT`,均无断连 / unknown | 仍需实现范围 / 多目标选择器来自动驱动 `ADD_FLY_TARGETING`;冷却、目标合法性、真实弹道和客户端表现也须按技能类型细分;尚未在真服观察到对应 GC 飞行目标推送 |
|
||||
| P0 | 🟡 | 快捷栏持久化 | 已覆盖 36 槽(4 页 × 9 格)、F1–F4 切页和 1–9 激活;拖放 / 清除 / 交换会分别发送 `CG_QUICKSLOT_ADD/DEL/SWAP`,服务器 `GC_*` 可完整恢复;技能冷却读取对应 `.msk` 的 `CoolTimeFormula` | 仍需补命令 / 表情快捷槽、物品拖放来源、鼠标物品态及原 UI 的完整快捷栏交互;冷却还未叠加原客户端的施法速度与所有特殊技能规则 |
|
||||
| P0 | 🟡 | 物品操作 | 已有移动、使用、丢弃、拾取;Shift 选“来源→目标”发 `ITEM_USE_TO_ITEM`,Ctrl+右键向当前目标 `ITEM_GIVE`,Alt+右键以数量框发 `ITEM_DROP2`;`ITEM_OWNERSHIP` 会实时更新地面名条;`GC_VIEW_EQUIP` 已保存 11 格检查快照并弹出装备查看窗 | 仍需复刻原鼠标物品态、操作确认 / 错误提示、宝石专用规则,以及装备查看窗的原版槽位图标、3D 纸娃娃与交互 |
|
||||
@@ -333,14 +372,15 @@
|
||||
| P0 | 🟡 | PVP / 决斗 / 阵营 | `GC_PVP` 和 `DUEL_START` 已进入状态层,并以 `pvp_changed` / `duel_started` 信号提供给 Godot;挑战 / 战斗 / 复仇关系会在世界实体上显示标签;实体已有部分 empire / alignment / pk 字段 | 仍需 PK 模式上行、原版名字颜色、可攻击判定、阵营规则和原版目标板 / 决斗 UI |
|
||||
| P1 | 🟡 | Actor 战斗运行时 | 有骨骼动画、基础 combo / 硬直 / 相机抖动;动作切换按 Granny `EaseInCurve(0,0,1,1)` 的 `2w²−w³` 逐骨 crossfade;角色 mesh 支持距离 LOD、15% 迟滞和约 0.18s 新旧 mesh 淡入(`LodGhost` + `lod_fade` uniform);右手武器 / 左手盾按 `weaponComposite[0] = weaponInvWorld[0]·weaponLocal[0]` grip 复合挂 `equip_right_hand`(探针实测 Δ≤0.06 m,非标准米级偏移文件夹断回退);`.msa` motion event 已分派声音、EffectPosition 特效、震屏和屏闪(FLY / WARP 留钩子);玩家 / 怪物强制实时投影并附脚底接触阴影 Decal;实体 `attack_speed` 与本地 `POINT_ATT_SPEED` 已按原版 100 基准影响攻击请求间隔 | 仍需用真实 `.msa` motion duration、武器 / 坐骑规则和服务端合法性校准攻击节奏;另补 combo 取消窗口、精确受击 / 击退、角色碰撞、同步碰撞、武器拖尾、剩余 motion event / 骨骼挂点语义、GPU skin LOD、双持 / 左手武器和目标锁定相机;`03150` 类偏移网格需 per-weapon 偏移数据或修 .gr2 |
|
||||
| P1 | 🟡 | 飞行物与命中特效 | `GC_FLY_TARGETING` / `GC_ADD_FLY_TARGETING` 保存 shooter 的目标队列和 VID / 坐标;`GC_CREATE_FLY` 可消费队列并生成 tween 发光球 | 对齐 `FlyingObjectManager`:真实技能弹道模型、命中时机 / 命中特效、消失条件、声音与 EffectLib 特效 |
|
||||
| P1 | 🟡 | 任务 / NPC | 支持 `[ENTER]`、`[CLEAR]`、`[NEXT]`、`[DONE]`、基础 `[QUESTION]`、`[INPUT]` 文本框和任务日志;`M2Client.script_select_item()` / `quest_cancel()` 已可发送原协议应答,Escape 会走取消包;真服单会话已发送 `CG_SCRIPT_SELECT_ITEM`(8B) 与 `CG_QUEST_CANCEL`(4B),取消触发 1 条 quest-change 且无断连 / unknown | 补选道具网格、原版 CJK IME / 输入校验、QuestButton、立绘、完整 EventManager 标签、世界箭头和屏幕边缘指示;选道具包本轮未出现脚本业务回包 |
|
||||
| P1 | 🟡 | 任务 / NPC | 支持 `[ENTER]`、`[CLEAR]`、`[NEXT]`、`[DONE]`、基础 `[QUESTION]`、`[INPUT]`、`[SELECT_ITEM]` 文本流程和任务日志;`SelectItemUI` 按原 `uiselectitem.RefreshSlot` 过滤魔石 / grade、映射背包格并发送 `CG_SCRIPT_SELECT_ITEM`,`quest_cancel()` 发送取消包;真服单会话已发送 `CG_SCRIPT_SELECT_ITEM`(8B) 与 `CG_QUEST_CANCEL`(4B),取消触发 1 条 quest-change 且无断连 / unknown | 补原版 CJK IME / 输入校验、QuestButton、立绘、完整 EventManager 标签、世界箭头和屏幕边缘指示;选道具业务回包与完整资源视觉仍需真服 / 资源环境验收 |
|
||||
| P1 | 🟡 | 商店 / 交易 / 仓库 | 普通 NPC 商店、交易状态和仓库列表已有首版;**`SHOP_GC_START_EX` 多货架已解析(`m_shop_tabs`,`get_shop()`),`shop_ui` 带货架按钮 + `tabIdx*40+slot` 买位置** | 补图标格网格、数量输入、交易回滚提示、仓库密码 / 改密码、完整拖放规则、`coin_type` 非金币货币显示 |
|
||||
| P1 | 🟡 | 私人商店 / 道具商城 | **首版**:`CG_MYSHOP`(0x0802) `CGMyShopHead`(38) + `MyShopItem`(13)×N 有尺寸断言 + `GameClient::send_open_private_shop` / `send_close_private_shop`(=`SHOP_CG_END`) + loopback 字节级;`GC_MALL_OPEN`(0x0841)/`GC_MALL_SET`(0x0842)/`GC_MALL_DEL`(0x0843) 路由进 `EntityStore`(`m_mall[135]` + `mall_open/size/slot/dirty`),`CG_MALL_CHECKOUT`(0x0840) `CGMallCheckout`(8) 发送;`M2Client` `open/close_private_shop`、`get_mall_items`/`mall_checkout` + `mall_opened`/`mall_changed` 信号;`ui/private_shop_ui.gd`(**已重做成 `PrivateShopBuilder.itemStock` 模型:40 格 grid + 拿起 / 落位 / 逐件价格弹窗 / 点占用格撤下 / 개설按格号排序 + display_pos + 上限 39**)、`ui/mall_ui.gd`(列表 + 取出)。**真机验证(2026-08-31,`net_e2e MT_E2E_MYSHOP=1`)**:`CG_MYSHOP` 38B head + 13B item×1 被真服接受,无断连 / 无 shop error,`SHOP_CG_END` 关店正常。`CG_MALL_CHECKOUT` 送出不断连,但 mall 需点 NPC 才 open。他人开店 `SHOP_GC_START_EX` 多货架已解析(见增量 34)。⬜ `SYMBOL_DATA`/`GC_SHOP_SIGN` 头顶招牌 3D、真 `.sub` 版 `privateshopbuilder.py` uiscript、鼠标真拖放物品态 |
|
||||
| P1 | 🟡 | 私人商店 / 道具商城 | **首版**:`CG_MYSHOP`(0x0802) `CGMyShopHead`(38) + `MyShopItem`(13)×N 有尺寸断言 + `GameClient::send_open_private_shop` / `send_close_private_shop`(=`SHOP_CG_END`) + loopback 字节级;`GC_MALL_OPEN`(0x0841)/`GC_MALL_SET`(0x0842)/`GC_MALL_DEL`(0x0843) 路由进 `EntityStore`(`m_mall[135]` + `mall_open/size/slot/dirty`),`CG_MALL_CHECKOUT`(0x0840) `CGMallCheckout`(8) 发送;`M2Client` `open/close_private_shop`、`get_mall_items`/`mall_checkout` + `mall_opened`/`mall_changed` 信号;`ui/private_shop_ui.gd`(**已重做成 `PrivateShopBuilder.itemStock` 模型:40 格 grid + 拿起 / 落位 / 逐件价格弹窗 / 点占用格撤下 / 개설按格号排序 + display_pos + 上限 39**)、`ui/mall_ui.gd`(列表 + 取出)。**真机验证(2026-08-31,`net_e2e MT_E2E_MYSHOP=1`)**:`CG_MYSHOP` 38B head + 13B item×1 被真服接受,无断连 / 无 shop error,`SHOP_CG_END` 关店正常。`CG_MALL_CHECKOUT` 送出不断连,但 mall 需点 NPC 才 open。他人开店 `SHOP_GC_START_EX` 多货架已解析(见增量 34)。`GC_SHOP_SIGN` 已在 `NetWorld` 提供 `ShopSign` Label3D fallback,`GC_SYMBOL_DATA` 已接会徽下载;仍缺真 `.sub` 版 `privateshopbuilder.py` uiscript、资源视觉和鼠标真拖放物品态 |
|
||||
| — | ❌ | 拍卖行 / auction house | 不做 | m2dev-client 无此系统(`0x08xx` 段只有 shop/myshop/safebox/mall,全源码 / 协议 / 资产零 `auction` 命中)。需自定义协议 + 服务端支持才能实现,**标注为不做**,除非后续明确提需求 |
|
||||
| P1 | 🟡 | Cube (제작) / 能量系统 | **首版**:Cube 走 `GC_CHAT`/`CHAT_TYPE_COMMAND` 文本总线(对齐 `ServerCommand()`),`EntityStore::apply_server_command` 解析 `cube open/close/info/success/fail/r_list/m_info`(配方 `v,c` 列表 + `@`/`&`/`|`/`/` 分层材料 + 金币)→ `CubeState` + `drain_cube_events()`;`GameClient::send_cube_make/material_info/result_list`(`/cube make|mInfo|rList`);`M2Client` `get_cube`/`cube_make`/`cube_request_*` + `cube_opened`/`cube_closed`/`cube_changed`/`cube_result` 信号;`ui/cube_ui.gd`(配方列表 + 材料/金币 + 제작)。`net.entity_store` + `net.loopback_flow` 覆盖。真机(2026-08-31)`/cube rList` 送出不断连,但服务器只在玩家站到 cube NPC 前才回应 → `cube open=0`,需 NPC 交互完整验。⬜ 能量条(`POINT_ENERGY` / affect)、原 `uiCube.py` 材料格 3D 图标 / 拖放 / 결과 애니、NPC 前完整验 send 命令字 |
|
||||
| P1 | 🟡 | 组队 | 邀请、接受、离队;**成员信息板(`party_ui.gd`,1:1 迁移 `uiparty.PartyMemberInfoBoard`):每员角色状态按钮(队长弹菜单 → `CG_PARTY_SET_STATE` 0x0704 分配 攻/坦/狂/辅/宗/防 或踢人)+ 名字(★队长) + HP gauge + `affects[7]` 附加效果 chip;顶部 EXP 分配开关(`party_set_distribute`)+ 组队治疗(`party_use_skill(1,0)`)**;真服单会话 `CG_PARTY_USE_SKILL`(9B) 后仍在线;小地图 / Atlas 绿色队员点 + 队长环 | 补角色菜单按党技能等级门控(Tanker≥10…Defender≥40)、`affects[7]` 精确槽位语义、真版 `.sub` 图标 / 颜色、队伍技能冷却 / 权限、离线颜色语义 |
|
||||
| P1 | 🟡 | Cube (제작) / 能量系统 | **首版**:Cube 走 `GC_CHAT`/`CHAT_TYPE_COMMAND` 文本总线(对齐 `ServerCommand()`),`EntityStore::apply_server_command` 解析 `cube open/close/info/success/fail/r_list/m_info`(配方 `v,c` 列表 + `@`/`&`/`|`/`/` 分层材料 + 金币)→ `CubeState` + `drain_cube_events()`;`GameClient::send_cube_make/material_info/result_list`(`/cube make|mInfo|rList`);`M2Client` `get_cube`/`cube_make`/`cube_request_*` + `cube_opened`/`cube_closed`/`cube_changed`/`cube_result` 信号;`ui/cube_ui.gd`(配方列表 + 材料/金币 + 제작)。`net.entity_store` + `net.loopback_flow` 覆盖。真机(2026-08-31)`/cube rList` 送出不断连,但服务器只在玩家站到 cube NPC 前才回应 → `cube open=0`,需 NPC 交互完整验。`POINT_ENERGY` 已由 HUD 快照显示;仍缺原 `uiCube.py` 材料格 3D 图标 / 拖放 / 결과 애니、NPC 前完整验 send 命令字 |
|
||||
| P1 | 🟡 | 组队 | 邀请、接受、离队;**成员信息板(`party_ui.gd`,1:1 迁移 `uiparty.PartyMemberInfoBoard`):每员角色状态按钮(队长弹菜单 → `CG_PARTY_SET_STATE` 0x0704 分配 攻/坦/迅捷/辅/宗/防 或踢人)+ 名字(★队长) + HP gauge + `affects[7]` 附加效果 chip;顶部 EXP 分配开关(`party_set_distribute`)+ 组队治疗(`party_use_skill(1,0)`)**;角色 / 治疗控件已按 40250 服务端 Leadership 规则本地门控(攻击≥10、坦克/迅捷≥20、辅助≥25、宗师≥35、防御≥40,治疗≥18);真服单会话 `CG_PARTY_USE_SKILL`(9B) 后仍在线;小地图 / Atlas 绿色队员点 + 队长环 | 仍需真版 `.sub` 图标 / 颜色、队伍技能冷却 / 权限、离线颜色语义和真实两账号组队验收 |
|
||||
| P1 | 🟡 | 好友 / 情侣 | 好友列表、上线状态和私聊入口已有;`GC_LOVER_INFO`(70B) / `GC_LOVE_POINT_UPDATE`(5B) 已有尺寸断言、`EntityStore::LoverInfo` 状态、`M2Client.get_lover()` / `lover_changed`,`love_ui` 常驻显示伴侣名和 0–100 爱意值;`net.entity_store` 与 `love_ui_test.gd` 覆盖 | 补好友分组和完整 messenger 行为;情侣仍需完整交互 / 状态图标与 affect 表现、断线 / 切图生命周期、原版文案和真服务器端到端验收 |
|
||||
| P1 | 🟡 | 公会 | 信息、成员、技能、公会战、会徽上 / 下载和部分操作已有首版;`GC_REQUEST_MAKE_GUILD`(0x0731) 已解析并以 `M2Client.guild_make_requested` 通知;`CG_SYMBOL_CRC` / `GC_SYMBOL_DATA` 已支持原始符号文件下载 | 补完整权限 / 等级页、公告与日志、成员管理、创建公会 UI / 应答流程、公会战应答、积分板、领地和公会建筑 |
|
||||
| P1 | 🟡 | 公会(暂缓) | 信息、成员、技能、公会战、会徽上 / 下载和部分操作已有首版;`GC_REQUEST_MAKE_GUILD`(0x0731) 已解析并以 `M2Client.guild_make_requested` 通知;公会窗已弹出创建名称输入并发送 `CG_ANSWER_MAKE_GUILD`;`CG_SYMBOL_CRC` / `GC_SYMBOL_DATA` 已支持原始符号文件下载 | 本阶段暂缓完整权限 / 等级页、公告与日志、成员管理、公会战应答、积分板、领地和公会建筑;恢复公会范围后再做完整真服验收 |
|
||||
| — | ❌ | 龙魂精炼 | 当前目标 40250 服务端不提供该玩法;本阶段不纳入支持或验收,已有兼容代码仅保留作协议参考 | 若未来服务端增加该玩法,再另立协议、UI 和验收需求 |
|
||||
| P1 | 🟡 | 世界玩法 | 有 `CG_WARP` 主动请求、`GC_WARP` 同服 / 跨服处理、`CG_FISHING` / `GC_FISHING` 钓鱼方向与状态事件、`NetWorld` / `NetPlay` 钓鱼动作分发、`ChatUI` 鱼获 / 失败日志、`CG_DUNGEON` / `GC_DUNGEON` 副本请求与状态事件、`GC_DIG_MOTION` 挖矿动作广播、`GC_LAND_LIST` 领地状态、`GC_OBSERVER_ADD/REMOVE/MOVE` 观战者状态、频道、时间、NPC marker、天气粒子和 mount_vnum 字段;真服登录洪流已收到并解析 `GC_LAND_LIST`(436B),单会话发送 `CG_FISHING`(5B) 后收到“Please choose a Fishing Pole.”业务拒绝且连接保持;同服 `GC_WARP` 直接挪玩家,跨服 `addr≠0` 由 `M2Client` 复用 login key 直连目标 game server,断点重连也走该快路径 | 补完整钓鱼 UI / 鱼获背包落地、副本计时 / 入口 UI、坐骑真模型 / 骑乘动作 / 移速、地形贴花、DungeonBlock 和地图天气配置;`CG_DUNGEON` / `CG_WARP` 本轮为避免副作用未发送,跨服换图仍需真服端到端验收与场景生命周期清理 |
|
||||
| P1 | 🟡 | 小地图 / Atlas | 可画玩家、实体、NPC、任务标记、观战者紫色菱形和 `GC_LAND_LIST` 领地边框;支持 `set_scale()` / 鼠标滚轮缩放;Atlas 支持底图、地图名称、玩家坐标、拖动和坐标查询 | 补原 minimap / Atlas 贴图细节、提示、队员 / NPC 分类、世界箭头、地图驱动天气配置和原版窗口视觉 |
|
||||
| P1 | 🟡 | EffectLib | `.mse` Particle 已映射到 `GPUParticles3D` | 粒子 `.dds` 目前被程序化径向渐变替代,`.mde` 用 box 占位;需补 EffectMesh、SimpleLight、TexAni、MovingType、骨骼挂点和完整时间轴 |
|
||||
@@ -384,7 +424,7 @@
|
||||
故当前总数从 24 降为 22 个 header;增量 7 再补任务选择 / 取消上行,增量 8 补队伍技能上行,
|
||||
增量 9 补飞行目标广播,增量 10 补技能组切换,增量 11 补角色改名,增量 12 补公会创建请求,
|
||||
增量 13 补主动传送,增量 14 补挖矿动作,增量 15 补会徽符号下载,增量 16 补钓鱼协议,
|
||||
增量 17 补副本协议,增量 18 补领地与观战者,当前为 **1 个 header**。
|
||||
增量 17 补副本协议,增量 18 补领地与观战者,增量 39 补 `CG_TEXT`,当前为 **0 个 header**。
|
||||
|
||||
排除仅命名不同、功能已覆盖的别名(`GC_CHAR_ADDITIONAL_INFO`=`GC_CHAR_ADD_INFO`、
|
||||
`GC_TARGET`=`GC_TARGET_INFO`、`GC_TARGET_CREATE_NEW`=`GC_TARGET_CREATE`、
|
||||
@@ -392,13 +432,13 @@
|
||||
`GC_SEPCIAL_EFFECT`=`GC_SPECIAL_EFFECT`、`GC_REFINE_INFORMATION_NEW`=`GC_REFINE_INFO_NEW`),
|
||||
以及 `CG_STATE_CHECKER` / `GC_RESPOND_CHANNELSTATUS`(`project/net/channel_status.gd` 单独实现)
|
||||
和 `CG_HACK`(反作弊,见「不做清单」),
|
||||
**当前主网络层仍缺 1 个 header**(脚本核对 `wire.h` vs `Packet.h`):
|
||||
**当前主网络层仍缺 0 个 header**(脚本核对 `wire.h` vs `Packet.h`):
|
||||
|
||||
### 账号 / 命令通道
|
||||
|
||||
| header | 值 | 说明 |
|
||||
|---|---|---|
|
||||
| `CG_TEXT` | 0x0011 | 简易命令通道(Cube 现借 `CG_CHAT`/`CHAT_TYPE_COMMAND` 实现,非原路径) |
|
||||
| `CG_TEXT` | 0x0011 | 已补 classic wire 尺寸表;这是握手阶段的管理命令,固定前缀为 1 字节,后续文本直到换行由服务端特殊解析。Cube 仍借 `CG_CHAT`/`CHAT_TYPE_COMMAND` 实现,不伪造管理命令入口 |
|
||||
|
||||
### 已声明但待核(不计入上面 1 个)
|
||||
|
||||
@@ -407,8 +447,9 @@
|
||||
自动触发语义、姿态动作链和服务端位置纠偏回包验收。
|
||||
- `CG_SCRIPT_SELECT_ITEM`(0x0903) / `CG_QUEST_CANCEL`(0x0906):wire、发送接口和
|
||||
loopback 字节测试已覆盖;真服单会话已分别发送 8B / 4B,取消触发 1 条 quest-change 且无
|
||||
unknown;基础 `[INPUT]` / Escape 取消已接入,仍缺选道具网格、原版输入校验和有脚本时的任务
|
||||
流程验收。
|
||||
unknown;基础 `[INPUT]` / `[SELECT_ITEM]` / Escape 取消已接入,选择窗的背包魔石网格、
|
||||
grade 过滤和选择回包已由 `select_item_ui_test.gd` 覆盖;仍缺原版输入校验、CJK IME 和有脚本时
|
||||
的真实任务流程验收。
|
||||
- `CG_PARTY_USE_SKILL`(0x0705):wire、发送接口和 loopback 字节测试已覆盖;真服单会话已发送
|
||||
9B 且保持在线 / 无 unknown,但无队伍技能业务回包;仍缺冷却 / 权限、UI 入口和有效队伍真服
|
||||
验收。
|
||||
@@ -420,10 +461,11 @@
|
||||
`M2Client.skill_group_changed` 已覆盖;本轮发现并修复 `GameClient` 未分派该 header 的 bug;
|
||||
仍缺技能组选择 UI、服务端权限和真服推送验收。
|
||||
- `CG_CHANGE_NAME`(0x010B) / `GC_CHANGE_NAME`(0x010C):wire、角色槽位更新、
|
||||
`M2Client.change_name()` 和 `char_name_changed` 已覆盖;仍缺改名 UI、改名卡消耗 / 重名错误
|
||||
文案和真服验收。
|
||||
- `GC_REQUEST_MAKE_GUILD`(0x0731):无负载 wire 解析和 `guild_make_requested` 信号已覆盖;仍缺
|
||||
创建公会窗口、名称校验 / 应答和真服验收。
|
||||
`M2Client.change_name()` 和 `char_name_changed` 已覆盖;选人页按 `change_name` 标志接入
|
||||
原版“点击开始先改名”流程,并对重名 / 非法名错误码给出状态文案;仍缺真服验收。
|
||||
- `GC_REQUEST_MAKE_GUILD`(0x0731):无负载 wire 解析和 `guild_make_requested` 信号已覆盖;公会窗已
|
||||
完成创建窗口、12 字上限、空名 / 控制字符校验和 `CG_ANSWER_MAKE_GUILD` 应答;仍缺服务端敏感词 /
|
||||
重名错误回包、实际建会状态和真服验收。
|
||||
- `CG_SYMBOL_CRC`(0x0723) / `GC_SYMBOL_DATA`(0x0732):16B / 8B wire、`MarkClient` CRC 请求、
|
||||
变长数据接收和 `M2Client.guild_symbol_ready` 已覆盖;仍缺本地符号缓存失效策略、UI 预览和
|
||||
真服验收。
|
||||
@@ -464,7 +506,7 @@
|
||||
|
||||
1. 任务完整标签 / 输入 / 选道具 / 指引;
|
||||
2. 商店 START_EX、仓库密码、商城、私人商店、Cube(拍卖行 m2dev-client 无,除非另起协议);
|
||||
3. 组队角色、公会完整页面、情侣完整交互、领地与公会建筑;
|
||||
3. 组队角色、情侣完整交互、领地与公会建筑;公会完整页面暂缓;
|
||||
4. 钓鱼、副本、坐骑、观战者和 Atlas。
|
||||
|
||||
### M3:UI 与表现 1:1
|
||||
@@ -483,7 +525,7 @@
|
||||
|
||||
## 1:1 完成判据
|
||||
|
||||
一个功能只有同时满足以下条件才标记为完成:
|
||||
一个功能只有同时满足以下条件才标记为完成。当前 40250 阶段对已明确暂缓的真服、公会和龙魂项不作完成承诺:
|
||||
|
||||
1. 原客户端涉及的 CG / GC 包和 sub-header 全部覆盖,并有尺寸断言;
|
||||
2. 数据状态能正确恢复、重连、切图和清理;
|
||||
|
||||
Reference in New Issue
Block a user