Metin2 game client (P0–P11) + mobile asset pipeline
Networked client on the existing Godot 4.7 + libgr2 renderer:
- net: m2dev wire protocol (libsodium KX + XChaCha20), auth/select/game
phases, EntityStore world model, ~all GC/CG headers. char create/delete,
private shop / mall / cube, SHOP_GC_START_EX, guild, party (+ CG_PARTY_SET_STATE),
quests, dragon soul, refine, safebox, exchange.
- UI: in-game windows migrated 1:1 from the reference uiscript/root .py —
char status (/stat), inventory+equipment, select-item ([SELECT_ITEM] quest
token), system-option + game-option + ESC system menu, private-shop 39-grid,
party info board, shop tabs, atlas, minimap, quickbar, chat, …
- EterGrnLib polish: GR2 material blend/two-sided, LOD crossfade, motion-event
dispatch, contact shadow, ray-AABB picking, weapon grip pre-transform.
Portable asset IO (A1) — all extension/libgr2/formats/mtproto reads routed
through godot::FileAccess (res:// PCK works on iOS/Android); standalone-lib
*_path() kept for the non-Godot CTests. AssetResolver + PropertyRegistry
switched to a baked index (bake_asset_index.gd) instead of std::filesystem.
Mobile builds: build-{android,ios}.sh, export-android.sh, pack-assets.sh,
gen-debug-keystore.sh. Assets ship as a zip mounted at runtime by
project/asset_pack.gd (adb push now; HTTP download is a drop-in later).
ctest 10/10, 34 GDScript suites, macOS/iOS/Android all build.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EJxkHiNKS4kybHS3XKyAJ
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
# oracle —— 独立 Windows 工程(不在主 CMake 树内)。见 ../docs/steps/00-oracle.md、RUNBOOK.md
|
||||
#
|
||||
# cmake -S oracle -B oracle/build -DGRANNY_SDK=<泄露 SDK 根>
|
||||
# cmake --build oracle/build --config Release
|
||||
#
|
||||
# GRANNY_SDK 默认指向本仓库同级的 MobileSource 里的 Granny-3D-SDK-main。
|
||||
# 链 granny2_x64.lib(import lib);运行时需要对应的 granny2_x64.dll 在 PATH / exe 目录。
|
||||
# ⚠ 这个 DLL 是 SDK 2.9.12。资产由 2.4.0.7 导出(M0 T2 实测)。容器 + 组合公式在 2.4–2.11
|
||||
# 稳定,但 B 样条边界 / 少数曲线细节可能有差异 —— 先跑 selfcheck(T7)确认坐标约定,
|
||||
# 再决定是否要换成 2.4.x 的 runtime DLL(见 GRANNY-VERSION.md)。
|
||||
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(xrender_oracle C)
|
||||
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
|
||||
set(GRANNY_SDK "${CMAKE_SOURCE_DIR}/../../MobileSource/Cross Platform/Granny-3D-SDK-main"
|
||||
CACHE PATH "泄露 Granny SDK 根(含 include/ 和 lib/win64/)")
|
||||
|
||||
if(NOT EXISTS "${GRANNY_SDK}/include/granny.h")
|
||||
message(FATAL_ERROR "找不到 ${GRANNY_SDK}/include/granny.h —— 传 -DGRANNY_SDK=<SDK 根>")
|
||||
endif()
|
||||
|
||||
add_executable(oracle oracle.c)
|
||||
target_include_directories(oracle PRIVATE "${GRANNY_SDK}/include")
|
||||
|
||||
if(WIN32)
|
||||
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
target_link_libraries(oracle PRIVATE "${GRANNY_SDK}/lib/win64/granny2_x64.lib")
|
||||
set(_dll "${GRANNY_SDK}/lib/win64/granny2_x64.dll")
|
||||
else()
|
||||
target_link_libraries(oracle PRIVATE "${GRANNY_SDK}/lib/win32/granny2.lib")
|
||||
set(_dll "${GRANNY_SDK}/lib/win32/granny2.dll")
|
||||
endif()
|
||||
# 把 DLL 拷到 exe 旁边,方便直接跑 / Wine
|
||||
add_custom_command(TARGET oracle POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_dll}" "$<TARGET_FILE_DIR:oracle>")
|
||||
else()
|
||||
message(WARNING
|
||||
"非 Windows:SDK 只提供 win32/win64/wii/ps3/xenon 的 granny2 预编译库。\n"
|
||||
" 选项 A:在 Windows 机器 / VM 上 build+run。\n"
|
||||
" 选项 B:Wine(oracle 只做文件 IO + 数学 + 写二进制,不碰 GPU,Wine 够用)——\n"
|
||||
" 用 MinGW 交叉编译或直接在 Wine 里跑 MSVC,链 granny2_x64.lib。\n"
|
||||
" 选项 C(stretch):从 SDK source/ 移植 winxx/ 平台层出一个 POSIX libgranny。\n"
|
||||
" 见 RUNBOOK.md。")
|
||||
endif()
|
||||
@@ -0,0 +1,45 @@
|
||||
# oracle dump 二进制格式
|
||||
|
||||
> `oracle.c`(`oracle dump`)产出。libgr2 侧对拍读取器(`tools/oracle_diff/`)必须与本文件一致。
|
||||
> `oracle dump` 把**层① 世界矩阵**和**mesh N 蒙皮顶点**写进**同一个文件**:先文件头,
|
||||
> 再 `kind=1` 段,紧接着 `kind=3` 段(`--mesh` 未命中时 `vertex_count=0`)。全部小端。
|
||||
|
||||
## 文件头(所有 dump 共用)
|
||||
|
||||
```
|
||||
magic "XORC" 4B
|
||||
version u32 1
|
||||
kind u32 1=层① 2=层② 3=蒙皮顶点 1
|
||||
coord u32 坐标约定编码: 1
|
||||
bit0 手系 (0=左手 1=右手)
|
||||
bit1 单位 (0=原始 1=米)
|
||||
bit2 根变换已 apply
|
||||
granny_ver char[16] 选定 granny2.dll 版本串
|
||||
model_path len-prefixed str
|
||||
anim_path len-prefixed str
|
||||
t f32 采样时刻
|
||||
```
|
||||
|
||||
## kind=1/2 段(骨骼世界矩阵,紧跟文件头)
|
||||
|
||||
```
|
||||
bone_count u32
|
||||
matrices f32[bone_count][16] GrannyGetWorldPose4x4Array 直出(骨骼空间→世界),行主序 Granny 原生
|
||||
```
|
||||
|
||||
> ⚠ 不是 `GrannyGetWorldPoseComposite4x4Array` —— 那个返回 `InverseWorld4x4·world` = **蒙皮矩阵**(bind pose 时 ≈ I)。
|
||||
> `GrannyBuildWorldPose` 的 `Offset4x4` 传的是 model 的 `InitialPlacement`(`GrannyGetModelInitialPlacement4x4`),
|
||||
> 与 libgr2 `skeleton.initial_placement` 一致;不传的话非单位 IP 的模型(如 assassin 双 root)会差一个平移。
|
||||
|
||||
## kind=3 段(mesh N 蒙皮顶点,紧跟 kind=1 段)
|
||||
|
||||
```
|
||||
vertex_count u32 (--mesh 未命中 → 0,无后续)
|
||||
positions f32[vertex_count][3] GrannyDeformVertices(PositionNormal) 后的位置
|
||||
```
|
||||
|
||||
## 坐标约定
|
||||
|
||||
**M0/oracle 全程 raw**:`coord` = 手系按选定 DLL 实际输出、单位=原始、根变换未 apply。
|
||||
libgr2 的 `gr2::Mat4` 声明同一约定(见 `docs/steps/M0-gr2-reader.md` 「约定」)。
|
||||
basis fix / 单位缩放是 M1 渲染时才做的事,不进 dump。
|
||||
@@ -0,0 +1,53 @@
|
||||
# Granny 版本判定(oracle T1)
|
||||
|
||||
## 已知(M0 T2)
|
||||
|
||||
- 资产由 **Granny Standard Exporter, SDK version 2.4.0.7** 导出(从解压后的 gr2 内嵌串读到)。
|
||||
- 9166 个 gr2 里 **9155 个文件格式 v6、11 个 v7**(magic:v6=`GRNFileMV_Old`,v7=`GRNFileMV_32Bit_LittleEndian`)。
|
||||
- libgr2 自研,`bind_pose_self_check` 全量 2013/2017 多骨 skeleton < 1e-3(`warrior_cheongrin` 6.1e-5)。
|
||||
|
||||
## 候选 runtime
|
||||
|
||||
| DLL | 来源 | 状态 |
|
||||
|---|---|---|
|
||||
| `granny2_x64.dll` 2.9.12 | 泄露 SDK `lib/win64/` | **先用这个**(SDK 自带、能直接链)。容器 + 组合公式在 2.4–2.11 稳定。 |
|
||||
| granny2.dll 2.4.x | 需另找(对应 2.4.0.7 exporter 的 runtime) | 若 2.9.12 在曲线 / B 样条边界上和 libgr2 分歧且 Blender 仲裁站 libgr2 这边,换这个。 |
|
||||
| granny2.dll 2.11.x | m2dev-client-src 头文件写的版本 | 兜底候选。 |
|
||||
|
||||
## 判定步骤(跑 `oracle probe`)
|
||||
|
||||
对每个候选 DLL,跑:
|
||||
|
||||
```
|
||||
oracle probe "assets/PC/ymir work/pc/warrior/warrior_cheongrin.gr2"
|
||||
oracle probe "assets/PC/ymir work/pc/warrior/action/dance_1.gr2"
|
||||
oracle probe "assets/Zone/ymir work/zone/.../<某静态件>.gr2"
|
||||
```
|
||||
|
||||
AC(选定 DLL 下):
|
||||
|
||||
- `warrior_cheongrin.gr2`:`Skeletons=1`、`Meshes=5`、`Animations=0`、`Models=1`、
|
||||
`Materials=4`、`Textures=2`、skeleton 0 = 75 骨、骨骼名以 `Bip01` 开头
|
||||
—— 与 libgr2(`gr2dump`)**逐字段一致**。
|
||||
- `dance_1.gr2`:`Animations=1`、`Duration≈28.333334`、`TrackGroups=1`。
|
||||
- 静态件:`Skeletons=0` 或 1、`Meshes≥1`。
|
||||
- 3+ 文件全过;其余候选 DLL 的失败表现记进本表。
|
||||
|
||||
## 选定结果 —— ✅ 已跑(2026-08-29)
|
||||
|
||||
- **选定:`granny2_x64.dll` 2.9.12.0**(泄露 SDK `lib/win64/`)。
|
||||
- 环境:macOS 26.4 / Apple Silicon / wine-stable 11.0(Rosetta)/ MinGW-w64 交叉编译(`build-wine.sh`)。
|
||||
- **`oracle probe` vs `gr2dump` 逐字段一致**:
|
||||
|
||||
| 文件 | Textures | Materials | Skeletons | Meshes | Models | Animations | skel0 骨骼数 | 骨骼名/父索引 |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| `warrior_cheongrin.gr2` | 2=2 | 4=4 | 1=1 | 5=5 | 1=1 | 0=0 | 75=75 | 逐项一致(Bip01…)|
|
||||
| `assassin.gr2` | 3=3 | 6=6 | 1=1 | 17=17 | 1=1 | 0=0 | 95=95 | 逐项一致(含双 root:`assasin_low` + `Bip01`)|
|
||||
| `dance_1.gr2` | — | — | 0 | 0 | 0 | 1=1 | — | Duration 28.333334,TrackGroups 1 |
|
||||
|
||||
- `FromFileName` 连非法编码的 Korean 路径都逐字节一致(libgr2 读原始字节 = Granny)。
|
||||
|
||||
**结论**:2.4 导出的资产在 2.9.12 runtime 下 `GrannyGetFileInfo` 完全正常,libgr2 与之逐字段对齐。
|
||||
容器 + 组合公式在 2.4–2.9 稳定这一假设成立,**不需要换 2.4.x runtime**。
|
||||
|
||||
`oracle.exe` + `granny2_x64.dll` 不进仓库(体积 + 版权);`build-wine.sh` 一条命令重建。
|
||||
@@ -0,0 +1,43 @@
|
||||
# oracle/ —— Windows 真值源工具
|
||||
|
||||
**独立 Windows/MSVC 工程,不在主 CMake 树内。** 施工文档:[`../docs/steps/00-oracle.md`](../docs/steps/00-oracle.md)
|
||||
|
||||
## 两条轨
|
||||
|
||||
| 轨 | 产物 | 状态 |
|
||||
|---|---|---|
|
||||
| **Oracle-Lite**(只用 `granny2.dll` C API) | `granny_probe.exe` · `oracle dump` CLI · 最小 D3D9 渲染器 | 必做,关键路径 |
|
||||
| **Oracle-Full**(+ 抽出的 `EterGrnLib`) | `oracle_etergrn` + 层② dump | 延后,demo 不需要 |
|
||||
|
||||
## 目录
|
||||
|
||||
```
|
||||
oracle/
|
||||
oracle.c ★ Oracle-Lite 全部:probe(T1)+ dump(T3 层① / T5 蒙皮顶点)
|
||||
CMakeLists.txt Windows/MSVC 工程,链泄露 SDK 的 granny2_x64.lib
|
||||
RUNBOOK.md 怎么 build+run(原生 Windows / Wine / stretch: POSIX 移植)
|
||||
GRANNY-VERSION.md 版本判定 + 候选对比(待跑 probe 填实)
|
||||
FORMAT.md dump 二进制布局 + 坐标约定
|
||||
vendor/ 锁定的 granny2 DLL(T1 选定后 git-track)
|
||||
render/ 独立最小 D3D9 → PNG(T6,不依赖 EterGrnLib;待写)
|
||||
selfcheck.* T7 自检(Windows CI runner;逻辑见 RUNBOOK.md「selfcheck」)
|
||||
full/ Oracle-Full:oracle_etergrn + 层② dump(T2/T4,延后)
|
||||
```
|
||||
|
||||
## 状态
|
||||
|
||||
- [x] `oracle.c` —— probe + dump(层① 世界矩阵 + mesh N 蒙皮顶点)
|
||||
- [x] **macOS + Wine 跑通**(`build-wine.sh` = MinGW-w64 交叉编译 + wine-stable)。Granny **2.9.12.0**。
|
||||
- [x] **T1** DLL 选定 + probe vs `gr2dump` 逐字段一致(`GRANNY-VERSION.md`)
|
||||
- [x] **T3/T5** 层① dump + 蒙皮顶点 dump 跑通;**M2 门禁·主对拍 PASS**:
|
||||
- `warrior_cheongrin` bind + `dance_1` @ t={0, 4.7, 11.333, 20}:骨骼矩阵 `max|Δ| ≤ 4.1e-5`、蒙皮顶点 `max‖Δ‖ ≤ 6.5e-5`
|
||||
- `assassin`(双 root,非单位 InitialPlacement)bind:`3.1e-5 / 4.6e-5`
|
||||
- 对拍工具 `tools/oracle_diff`(libgr2 侧读 `.bin` 逐字段比)
|
||||
- [ ] T8 noise_floor.json(跑同 dump 100 次 + 换相邻 DLL 一次,取 `max|Δ|`)
|
||||
- [ ] T6 D3D9 确定态截图 harness(M1 视觉对拍用)
|
||||
- [ ] Oracle-Full(层②,`EterGrnLib`)—— 延后
|
||||
|
||||
## 输出
|
||||
|
||||
- dump → `../test/oracle_dumps/`(格式见 `FORMAT.md`)
|
||||
- 噪声地板 → `../test/noise_floor.json`(T8,M2 用它设 ε)
|
||||
@@ -0,0 +1,88 @@
|
||||
# oracle 运行手册(Oracle-Lite)
|
||||
|
||||
`oracle.c` 只用 `granny2` 的公开 C API(`granny.h`):文件 IO + 数学 + 写二进制,**不碰 GPU / D3D / EterGrnLib**。
|
||||
所以除了原生 Windows,Wine 也能跑。
|
||||
|
||||
## 依赖
|
||||
|
||||
- `granny.h` + `granny2_x64.lib` + `granny2_x64.dll` —— 泄露 SDK 里就有:
|
||||
`MobileSource/Cross Platform/Granny-3D-SDK-main/{include,lib/win64}/`
|
||||
(32 位版在 `lib/win32/`。)
|
||||
- 这套 DLL 是 **SDK 2.9.12**。资产由 **2.4.0.7** 导出(M0 T2 从解压后的 gr2 里读到)。
|
||||
容器格式 + 组合公式(`BuildCompositeTransform4x4` / `ColumnMatrixMultiply4x3` / B 样条系数)
|
||||
在 2.4–2.11 稳定,libgr2 的 self-check 6.1e-5 已印证。差异风险在 B 样条边界 / 少数曲线细节。
|
||||
→ 先跑 `selfcheck`(下)确认坐标约定和 t=0 == bind pose,再决定是否换 2.4.x runtime(见 `GRANNY-VERSION.md`)。
|
||||
|
||||
## A. 原生 Windows(MSVC)
|
||||
|
||||
```bat
|
||||
cmake -S oracle -B oracle\build -DGRANNY_SDK="C:\path\to\Granny-3D-SDK-main"
|
||||
cmake --build oracle\build --config Release
|
||||
oracle\build\Release\oracle.exe probe "assets\PC\ymir work\pc\warrior\warrior_cheongrin.gr2"
|
||||
```
|
||||
|
||||
## B. macOS + Wine —— ✅ 已验证(macOS 26.4 / Apple Silicon)
|
||||
|
||||
```sh
|
||||
brew install mingw-w64
|
||||
# wine-stable cask 需要 sudo 装 gstreamer 依赖;无 sudo 时直接解包用:
|
||||
tar -xf "$(ls ~/Library/Caches/Homebrew/downloads/*wine-stable*.tar.xz)" -C /tmp/wine
|
||||
xattr -rc "/tmp/wine/Wine Stable.app"
|
||||
WINE="/tmp/wine/Wine Stable.app/Contents/Resources/wine/bin/wine"
|
||||
|
||||
# 编译(脚本封装了 MinGW 的坑:granny.h 的 __int64 关键字)
|
||||
oracle/build-wine.sh # → oracle/oracle.exe + oracle/granny2_x64.dll
|
||||
|
||||
# ⚠ WINEPREFIX 不能在 /tmp(Wine 拒绝在非属主目录建 prefix)
|
||||
export WINEPREFIX="$HOME/.wine-oracle" WINEDEBUG=-all
|
||||
"$WINE" wineboot --init # 首次,Rosetta 下 3–8 分钟
|
||||
|
||||
cd oracle
|
||||
GR2='Z:\Users\me\...\assets\PC\ymir work\pc\warrior\warrior_cheongrin.gr2'
|
||||
"$WINE" oracle.exe probe "$GR2"
|
||||
"$WINE" oracle.exe dump "$GR2" - 0.0 \
|
||||
-o 'Z:\Users\me\...\xrender-poc\test\oracle_dumps\cheongrin_bind_t0.bin' --mesh 4
|
||||
"$WINE" oracle.exe dump "$GR2" 'Z:\...\action\dance_1.gr2' 11.333 \
|
||||
-o 'Z:\...\test\oracle_dumps\cheongrin_dance1_t11.333.bin' --mesh 4
|
||||
```
|
||||
|
||||
坑:① MinGW GCC 不认 `granny.h` 里的 `__int64` → `build-wine.sh` 用 `-D__int64="long long"`;
|
||||
② Wine prefix 不能放 `/tmp`(非属主);③ wine-stable 首启会刷一大堆 MoltenVK 日志到 stderr,无害,`WINEDEBUG=-all` 挡不住它,grep 掉即可。
|
||||
|
||||
## C.(stretch)从 SDK source/ 移植 POSIX libgranny
|
||||
|
||||
`Granny-3D-SDK-main/source/` 有全套源码(62k 行)+ CMakeLists,但重度 Windows 化
|
||||
(`winxx/win64/*.cpp`、`winxx_granny_windows.cpp`、`WINDOWS_IGNORE_PACKING_MISMATCH`)。
|
||||
`wii/`、`wiiu/`、`ps3/` 目录里有 `ansi_*` 的通用实现可参考。真要做是几小时的平台层移植,
|
||||
不在关键路径 —— 先走 A / B。
|
||||
|
||||
## 对拍(`tools/oracle_diff`)
|
||||
|
||||
```sh
|
||||
build/tools/oracle_diff <model.gr2> <anim.gr2|-> <dump.bin> [--t <sec>] [--mesh N]
|
||||
```
|
||||
|
||||
libgr2 侧用 `gr2::sample_pose` / `bind_pose` + 手写 LBS 算一遍,报:
|
||||
- **gate 1**:骨骼世界矩阵 vs oracle 层① `max|Δ|`
|
||||
- **gate 2**:mesh N 蒙皮顶点 vs oracle `max‖Δ‖`
|
||||
- verdict:阈值 1e-3(noise_floor 就绪前)
|
||||
|
||||
**已跑结果**(Granny 2.9.12.0 vs libgr2):
|
||||
|
||||
| 用例 | mat max\|Δ\| | vtx max‖Δ‖ |
|
||||
|---|---|---|
|
||||
| `warrior_cheongrin` bind | 1.5e-5 | 4.6e-5 |
|
||||
| `warrior_cheongrin` + `dance_1` @ t=0/4.7/11.333/20 | ≤ 4.1e-5 | ≤ 6.5e-5 |
|
||||
| `assassin` bind(双 root + 非单位 InitialPlacement)| 3.1e-5 | 4.6e-5 |
|
||||
|
||||
全部 ≈ float32 累积误差 → **libgr2 的类型树 / 世界姿势 / OldCurve 求值 / CPU LBS 与真 Granny 数值一致**。
|
||||
|
||||
> 关键:`oracle.c` 的 `GrannyBuildWorldPose` 要把 model 的 `InitialPlacement` 当 `Offset4x4` 传
|
||||
> (`GrannyGetModelInitialPlacement4x4`),否则非单位 InitialPlacement 的模型(如 assassin)会差一个平移。
|
||||
> libgr2 的 `skeleton.initial_placement` 做同一件事。踩过一次。
|
||||
|
||||
## 产出
|
||||
|
||||
- dump → `test/oracle_dumps/`(布局见 `FORMAT.md`)
|
||||
- 噪声地板 → `test/noise_floor.json`(`oracle dump` 跑同 `(model,anim,t)` 100 次 + 换一版
|
||||
相邻 DLL 一次,取矩阵 / 顶点的 `max|Δ|`;M2 的 ε = 这个 ×10)
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# 在 macOS 上用 MinGW-w64 交叉编译 oracle.exe(链泄露 SDK 的 granny2_x64.lib),
|
||||
# 用 Wine 跑。见 RUNBOOK.md。实测 macOS 26.4 / Apple Silicon / wine-stable 11.0 / Granny 2.9.12.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
SDK="${GRANNY_SDK:-../../MobileSource/Cross Platform/Granny-3D-SDK-main}"
|
||||
[ -f "$SDK/include/granny.h" ] || { echo "找不到 $SDK/include/granny.h(传 GRANNY_SDK=<SDK 根>)"; exit 1; }
|
||||
|
||||
CC="${MINGW_CC:-x86_64-w64-mingw32-gcc}"
|
||||
command -v "$CC" >/dev/null || { echo "没装 MinGW:brew install mingw-w64"; exit 1; }
|
||||
|
||||
# 关键:granny.h 在 _WIN64 分支里 `#define GRANNY_64BIT_TYPE __int64`,
|
||||
# MinGW GCC 默认不认 __int64 关键字 → 用 -D__int64="long long" 顶掉。
|
||||
"$CC" -O2 -std=gnu11 -fms-extensions -D'__int64=long long' \
|
||||
-I "$SDK/include" \
|
||||
oracle.c "$SDK/lib/win64/granny2_x64.lib" \
|
||||
-o oracle.exe
|
||||
|
||||
cp "$SDK/lib/win64/granny2_x64.dll" ./
|
||||
echo "built: $(pwd)/oracle.exe (+ granny2_x64.dll)"
|
||||
echo
|
||||
echo "跑(Wine):"
|
||||
echo " export WINEPREFIX=\$HOME/.wine-oracle # ⚠ 别放 /tmp:Wine 拒绝在非属主目录建 prefix"
|
||||
echo " wine wineboot --init # 首次,Rosetta 下较慢"
|
||||
echo " wine oracle.exe probe 'Z:\\path\\to\\warrior_cheongrin.gr2'"
|
||||
echo " wine oracle.exe dump 'Z:\\...\\model.gr2' 'Z:\\...\\anim.gr2' <t> -o 'Z:\\...\\out.bin' --mesh N"
|
||||
echo
|
||||
echo "对拍:build/tools/oracle_diff <model.gr2> <anim.gr2|-> <out.bin> --mesh N"
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
/* oracle —— Windows 真值源(Oracle-Lite)。见 ../docs/steps/00-oracle.md
|
||||
*
|
||||
* oracle probe <file.gr2>
|
||||
* → 打 GrannyGetFileInfo 的 count / FromFileName / 每 skeleton 骨骼数 + 前几个骨骼名
|
||||
*
|
||||
* oracle dump <model.gr2> <anim.gr2|-> <t> -o out.bin [--mesh N]
|
||||
* → 层① 世界矩阵(GrannyGetWorldPoseComposite4x4Array 直出)+ mesh N 蒙皮顶点
|
||||
* → 二进制布局见 ../FORMAT.md;libgr2 侧对拍读取器必须与之一致
|
||||
*
|
||||
* 只用 granny2 的公开 C API(granny.h)。链 granny2_x64.lib(见 CMakeLists.txt / RUNBOOK.md)。
|
||||
* 不碰 D3D、不碰 EterGrnLib —— 那是 Oracle-Full(延后)。
|
||||
*/
|
||||
#include <granny.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define ORC_MAGIC "XORC"
|
||||
#define ORC_VERSION 1u
|
||||
#define KIND_LAYER1 1u
|
||||
#define KIND_SKINV 3u
|
||||
|
||||
static void die(const char *msg) { fprintf(stderr, "oracle: %s\n", msg); exit(2); }
|
||||
|
||||
static const char *granny_version_string(void) {
|
||||
/* 运行期版本(链接到的 granny2.dll 实际报的)。编译期宏是 GrannyProduct*Version。*/
|
||||
const char *rt = GrannyGetVersionString();
|
||||
return rt ? rt : "unknown";
|
||||
}
|
||||
|
||||
/* ── probe ─────────────────────────────────────────────────────────── */
|
||||
static int cmd_probe(const char *path) {
|
||||
granny_file *f = GrannyReadEntireFile(path);
|
||||
if (!f) { fprintf(stderr, "probe: cannot read %s\n", path); return 1; }
|
||||
granny_file_info *fi = GrannyGetFileInfo(f);
|
||||
if (!fi) { fprintf(stderr, "probe: GrannyGetFileInfo NULL\n"); return 1; }
|
||||
|
||||
printf("file : %s\n", path);
|
||||
printf("granny runtime : %s\n", granny_version_string());
|
||||
printf("FromFileName : %s\n", fi->FromFileName ? fi->FromFileName : "(null)");
|
||||
printf("Textures=%d Materials=%d Skeletons=%d VertexDatas=%d TriTopologies=%d "
|
||||
"Meshes=%d Models=%d TrackGroups=%d Animations=%d\n",
|
||||
fi->TextureCount, fi->MaterialCount, fi->SkeletonCount, fi->VertexDataCount,
|
||||
fi->TriTopologyCount, fi->MeshCount, fi->ModelCount, fi->TrackGroupCount,
|
||||
fi->AnimationCount);
|
||||
|
||||
for (int s = 0; s < fi->SkeletonCount; ++s) {
|
||||
granny_skeleton *sk = fi->Skeletons[s];
|
||||
printf(" skeleton %d: %d bones : ", s, sk->BoneCount);
|
||||
for (int b = 0; b < sk->BoneCount && b < 6; ++b)
|
||||
printf("[%d]%s(p=%d) ", b, sk->Bones[b].Name, sk->Bones[b].ParentIndex);
|
||||
printf("%s\n", sk->BoneCount > 6 ? "..." : "");
|
||||
}
|
||||
for (int a = 0; a < fi->AnimationCount; ++a)
|
||||
printf(" animation %d: '%s' Duration=%.6f TimeStep=%.6f TrackGroups=%d\n",
|
||||
a, fi->Animations[a]->Name, fi->Animations[a]->Duration,
|
||||
fi->Animations[a]->TimeStep, fi->Animations[a]->TrackGroupCount);
|
||||
|
||||
GrannyFreeFile(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ── dump helpers ──────────────────────────────────────────────────── */
|
||||
static void wr_u32(FILE *o, uint32_t v) { fwrite(&v, 4, 1, o); }
|
||||
static void wr_f32(FILE *o, float v) { fwrite(&v, 4, 1, o); }
|
||||
static void wr_str(FILE *o, const char *s) {
|
||||
uint32_t n = (uint32_t)(s ? strlen(s) : 0);
|
||||
wr_u32(o, n);
|
||||
if (n) fwrite(s, 1, n, o);
|
||||
}
|
||||
|
||||
/* 与 ../FORMAT.md「文件头」一致 */
|
||||
static void write_header(FILE *o, uint32_t kind, uint32_t coord,
|
||||
const char *model_path, const char *anim_path, float t) {
|
||||
fwrite(ORC_MAGIC, 1, 4, o);
|
||||
wr_u32(o, ORC_VERSION);
|
||||
wr_u32(o, kind);
|
||||
wr_u32(o, coord);
|
||||
char gv[16]; memset(gv, 0, sizeof gv);
|
||||
{ const char *v = granny_version_string(); size_t n = strlen(v);
|
||||
memcpy(gv, v, n < 15 ? n : 15); }
|
||||
fwrite(gv, 1, 16, o);
|
||||
wr_str(o, model_path);
|
||||
wr_str(o, anim_path ? anim_path : "");
|
||||
wr_f32(o, t);
|
||||
}
|
||||
|
||||
/* ── dump ──────────────────────────────────────────────────────────── */
|
||||
static int cmd_dump(int argc, char **argv) {
|
||||
if (argc < 4) die("dump <model.gr2> <anim.gr2|-> <t> -o out.bin [--mesh N]");
|
||||
const char *model_path = argv[0];
|
||||
const char *anim_path = strcmp(argv[1], "-") == 0 ? NULL : argv[1];
|
||||
float t = (float)atof(argv[2]);
|
||||
const char *out_path = NULL;
|
||||
int mesh_index = 0;
|
||||
for (int i = 3; i < argc; ++i) {
|
||||
if (!strcmp(argv[i], "-o") && i + 1 < argc) out_path = argv[++i];
|
||||
else if (!strcmp(argv[i], "--mesh") && i + 1 < argc) mesh_index = atoi(argv[++i]);
|
||||
}
|
||||
if (!out_path) die("dump: 缺 -o out.bin");
|
||||
|
||||
granny_file *mf = GrannyReadEntireFile(model_path);
|
||||
if (!mf) die("dump: model 读失败");
|
||||
granny_file_info *mfi = GrannyGetFileInfo(mf);
|
||||
if (!mfi || mfi->ModelCount < 1) die("dump: model 无 Models[0]");
|
||||
|
||||
granny_model_instance *mi = GrannyInstantiateModel(mfi->Models[0]);
|
||||
if (!mi) die("dump: GrannyInstantiateModel 失败");
|
||||
granny_skeleton *skel = GrannyGetSourceSkeleton(mi);
|
||||
int bone_count = skel->BoneCount;
|
||||
|
||||
granny_file *af = NULL;
|
||||
if (anim_path) {
|
||||
af = GrannyReadEntireFile(anim_path);
|
||||
if (!af) die("dump: anim 读失败");
|
||||
granny_file_info *afi = GrannyGetFileInfo(af);
|
||||
if (!afi || afi->AnimationCount < 1) die("dump: anim 无 Animations[0]");
|
||||
granny_control *ctrl = GrannyPlayControlledAnimation(0.0f, afi->Animations[0], mi);
|
||||
if (!ctrl) die("dump: GrannyPlayControlledAnimation 失败");
|
||||
}
|
||||
|
||||
GrannySetModelClock(mi, t);
|
||||
|
||||
/* Offset4x4 = model 的 InitialPlacement(客户端渲染时也这么给)。
|
||||
* libgr2 侧 gr2::sample_pose 用 skeleton.initial_placement 做同一件事。 */
|
||||
granny_real32 placement[16];
|
||||
GrannyGetModelInitialPlacement4x4(mfi->Models[0], placement);
|
||||
|
||||
granny_local_pose *lp = GrannyNewLocalPose(bone_count);
|
||||
granny_world_pose *wp = GrannyNewWorldPose(bone_count);
|
||||
/* 先填 rest(无 track 的骨骼用它);有动画再让 SampleModelAnimations 覆盖动的那些。 */
|
||||
GrannyBuildRestLocalPose(skel, 0, bone_count, lp);
|
||||
if (af) GrannySampleModelAnimations(mi, 0, bone_count, lp);
|
||||
GrannyBuildWorldPose(skel, 0, bone_count, lp, placement, wp);
|
||||
|
||||
/* 层① = 每骨「骨骼空间 → 世界」的原始世界矩阵(GrannyGetWorldPose4x4Array)。
|
||||
* 注意:GrannyGetWorldPoseComposite4x4Array 返回的是 InverseWorld4x4·world = 蒙皮矩阵,
|
||||
* 不是这里要的。 */
|
||||
granny_matrix_4x4 *world = GrannyGetWorldPose4x4Array(wp);
|
||||
|
||||
/* coord:raw(手系按 DLL 输出、单位=原始、根变换未 apply)。见 FORMAT.md。 */
|
||||
const uint32_t coord = 0u;
|
||||
|
||||
FILE *o = fopen(out_path, "wb");
|
||||
if (!o) die("dump: 打不开输出文件");
|
||||
write_header(o, KIND_LAYER1, coord, model_path, anim_path, t);
|
||||
wr_u32(o, (uint32_t)bone_count);
|
||||
for (int b = 0; b < bone_count; ++b)
|
||||
fwrite(&world[b][0][0], 4, 16, o);
|
||||
|
||||
/* kind=3:mesh_index 的蒙皮顶点位置。照 SDK tutorial(wii_basic.cpp CreateMesh/Render):
|
||||
* deformer(输入=mesh 自带顶点类型, 输出=PNT332)
|
||||
* 矩阵缓冲 = GrannyGetWorldPoseComposite4x4Array(= InverseWorld·world = 蒙皮矩阵)
|
||||
* 索引 = GrannyGetMeshBindingToBoneIndices(mesh 骨骼槽 → model 骨骼) */
|
||||
if (mfi->MeshCount > mesh_index) {
|
||||
granny_mesh *mesh = mfi->Meshes[mesh_index];
|
||||
int vcount = GrannyGetMeshVertexCount(mesh);
|
||||
|
||||
typedef struct { float p[3], n[3], uv[2]; } pnt332;
|
||||
pnt332 *dst = (pnt332 *)malloc(sizeof(pnt332) * vcount);
|
||||
|
||||
granny_matrix_4x4 *composite = GrannyGetWorldPoseComposite4x4Array(wp);
|
||||
granny_mesh_binding *mb = GrannyNewMeshBinding(mesh, skel, skel);
|
||||
granny_int32x const *toBone = GrannyGetMeshBindingToBoneIndices(mb);
|
||||
|
||||
granny_mesh_deformer *def = GrannyNewMeshDeformer(
|
||||
GrannyGetMeshVertexType(mesh), GrannyPNT332VertexType,
|
||||
GrannyDeformPositionNormal, GrannyDontAllowUncopiedTail);
|
||||
if (def) {
|
||||
GrannyDeformVertices(def, toBone, (const granny_real32 *)composite,
|
||||
vcount, GrannyGetMeshVertices(mesh), dst);
|
||||
} else {
|
||||
fprintf(stderr, "dump: no deformer for mesh %d vertex type\n", mesh_index);
|
||||
}
|
||||
|
||||
wr_u32(o, (uint32_t)vcount);
|
||||
for (int v = 0; v < vcount; ++v)
|
||||
fwrite(dst[v].p, 4, 3, o);
|
||||
|
||||
if (def) GrannyFreeMeshDeformer(def);
|
||||
GrannyFreeMeshBinding(mb);
|
||||
free(dst);
|
||||
} else {
|
||||
wr_u32(o, 0);
|
||||
}
|
||||
|
||||
fclose(o);
|
||||
printf("dump: wrote %s bones=%d t=%.6f granny=%s\n",
|
||||
out_path, bone_count, t, granny_version_string());
|
||||
|
||||
GrannyFreeWorldPose(wp);
|
||||
GrannyFreeLocalPose(lp);
|
||||
GrannyFreeModelInstance(mi);
|
||||
if (af) GrannyFreeFile(af);
|
||||
GrannyFreeFile(mf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc < 2) {
|
||||
fprintf(stderr,
|
||||
"usage:\n"
|
||||
" oracle probe <file.gr2>\n"
|
||||
" oracle dump <model.gr2> <anim.gr2|-> <t> -o out.bin [--mesh N]\n");
|
||||
return 2;
|
||||
}
|
||||
if (!strcmp(argv[1], "probe")) {
|
||||
if (argc < 3) die("probe <file.gr2>");
|
||||
return cmd_probe(argv[2]);
|
||||
}
|
||||
if (!strcmp(argv[1], "dump")) return cmd_dump(argc - 2, argv + 2);
|
||||
die("未知子命令(probe / dump)");
|
||||
return 2;
|
||||
}
|
||||
Executable
+116
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env bash
|
||||
# M2 门禁·主:对测试资产集跑 oracle dump + oracle_diff,汇总 test/m2-numeric.json。
|
||||
# 前置:oracle/build-wine.sh 已产出 oracle.exe;WINEPREFIX 已 init(见 RUNBOOK.md);
|
||||
# oracle_diff 已构建(cmake -B build-tools -DMTGODOT_BUILD_EXTENSION=OFF -DMTGODOT_BUILD_TOOLS=ON)。
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
REPO="$(pwd)"
|
||||
ASSETS="$(cd "${MT_ASSETS:-$REPO/assets}" && pwd)"
|
||||
DUMPS="$REPO/test/oracle_dumps"
|
||||
DIFF=""
|
||||
for c in "$REPO/build-tools/tools/oracle_diff" "$REPO/build/tools/oracle_diff"; do
|
||||
[ -x "$c" ] && DIFF="$c" && break
|
||||
done
|
||||
[ -n "$DIFF" ] || { echo "oracle_diff not built — run: cmake -B build-tools -DMTGODOT_BUILD_EXTENSION=OFF -DMTGODOT_BUILD_TOOLS=ON && cmake --build build-tools"; exit 2; }
|
||||
mkdir -p "$DUMPS"
|
||||
|
||||
WINE="${WINE:-/tmp/wine/Wine Stable.app/Contents/Resources/wine/bin/wine}"
|
||||
export WINEPREFIX="${WINEPREFIX:-$HOME/.wine-oracle}" WINEDEBUG="${WINEDEBUG:--all}"
|
||||
|
||||
# Z: 盘映射到 /
|
||||
winpath() { printf 'Z:%s' "$(printf '%s' "$1" | sed 's#/#\\#g')"; }
|
||||
|
||||
# 用例:<model 相对 assets> | <anim 相对 assets,或 -> | <t> | <mesh idx>
|
||||
CASES=$(cat <<'EOF'
|
||||
PC/ymir work/pc/warrior/warrior_cheongrin.gr2|-|0|4
|
||||
PC/ymir work/pc/warrior/warrior_cheongrin_lod_01.gr2|-|0|4
|
||||
PC/ymir work/pc/warrior/warrior_cheongrin_lod_02.gr2|-|0|4
|
||||
PC/ymir work/pc/warrior/warrior_cheongrin_lod_03.gr2|-|0|4
|
||||
PC/ymir work/pc/warrior/warrior_lord.gr2|-|0|1
|
||||
PC/ymir work/pc/warrior/warrior_lord_lod_01.gr2|-|0|1
|
||||
PC/ymir work/pc/warrior/warrior_lord_lod_02.gr2|-|0|1
|
||||
PC/ymir work/pc/warrior/warrior_lord_lod_03.gr2|-|0|1
|
||||
PC/ymir work/pc/assassin/assassin.gr2|-|0|16
|
||||
PC/ymir work/pc/shaman/shaman_lord.gr2|-|0|0
|
||||
metin2_patch_dragon_rock_mobs/ymir work/monster2/redthief2_soldier2/redthief2_soldier2_lod_01.gr2|-|0|4
|
||||
monster2/ymir work/monster2/snakeman_bow/snakeman_bow_lod_01.gr2|-|0|0
|
||||
monster2/ymir work/monster2/snakeman_bow/snakeman_bow_lod_01.gr2|-|0|1
|
||||
metin2_patch_easter1/ymir work/pc/warrior/warrior_rabbit1_backup.gr2|-|0|0
|
||||
Zone/ymir work/zone/oxevent/ox_01.gr2|-|0|0
|
||||
PC/ymir work/pc/warrior/warrior_cheongrin.gr2|PC/ymir work/pc/warrior/action/dance_1.gr2|0|4
|
||||
PC/ymir work/pc/warrior/warrior_cheongrin.gr2|PC/ymir work/pc/warrior/action/dance_1.gr2|4.7|4
|
||||
PC/ymir work/pc/warrior/warrior_cheongrin.gr2|PC/ymir work/pc/warrior/action/dance_1.gr2|11.333|4
|
||||
PC/ymir work/pc/warrior/warrior_cheongrin.gr2|PC/ymir work/pc/warrior/action/dance_1.gr2|20|4
|
||||
PC/ymir work/pc/warrior/warrior_cheongrin.gr2|PC/ymir work/pc/warrior/general/attack.gr2|0.3|4
|
||||
PC/ymir work/pc/warrior/warrior_cheongrin.gr2|PC/ymir work/pc/warrior/general/run.gr2|0.5|4
|
||||
PC/ymir work/pc/warrior/warrior_lord.gr2|PC/ymir work/pc/warrior/action/dance_1.gr2|10|1
|
||||
PC/ymir work/pc/warrior/warrior_lord.gr2|PC/ymir work/pc/warrior/general/wait.gr2|1.0|1
|
||||
EOF
|
||||
)
|
||||
|
||||
key_for() { echo "$1|$2|$3|$4" | md5 -q | cut -c1-12; }
|
||||
|
||||
# ── 1. dump(并行,4 个一批)──────────────────────────────────────────
|
||||
run_dump() {
|
||||
local model="$1" anim="$2" t="$3" mesh="$4" out="$5"
|
||||
local wm; wm="$(winpath "$ASSETS/$model")"
|
||||
local wa="-"; [ "$anim" != "-" ] && wa="$(winpath "$ASSETS/$anim")"
|
||||
local wo; wo="$(winpath "$out")"
|
||||
"$WINE" "$REPO/oracle/oracle.exe" dump "$wm" "$wa" "$t" -o "$wo" --mesh "$mesh" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
echo "== oracle dump =="
|
||||
i=0
|
||||
while IFS='|' read -r model anim t mesh; do
|
||||
[ -z "$model" ] && continue
|
||||
k=$(key_for "$model" "$anim" "$t" "$mesh")
|
||||
out="$DUMPS/suite_$k.bin"
|
||||
run_dump "$model" "$anim" "$t" "$mesh" "$out" &
|
||||
i=$((i+1)); [ $((i % 4)) -eq 0 ] && wait
|
||||
done <<< "$CASES"
|
||||
wait
|
||||
|
||||
# ── 2. diff + 汇总 ─────────────────────────────────────────────────────
|
||||
echo "== oracle_diff =="
|
||||
JSON="$REPO/test/m2-numeric.json"
|
||||
{
|
||||
echo '{'
|
||||
echo ' "granny": "2.9.12.0",'
|
||||
echo ' "threshold": {"mat": 1e-3, "vtx": 1e-3},'
|
||||
echo ' "cases": ['
|
||||
} > "$JSON"
|
||||
|
||||
total=0; passed=0; first=1
|
||||
worst_mat=0; worst_vtx=0
|
||||
while IFS='|' read -r model anim t mesh; do
|
||||
[ -z "$model" ] && continue
|
||||
k=$(key_for "$model" "$anim" "$t" "$mesh")
|
||||
bin="$DUMPS/suite_$k.bin"
|
||||
total=$((total+1))
|
||||
if [ ! -s "$bin" ]; then
|
||||
line=" {\"model\":\"$(basename "$model")\",\"anim\":\"$(basename "$anim")\",\"t\":$t,\"mesh\":$mesh,\"status\":\"NO_DUMP\"}"
|
||||
else
|
||||
amodel="$ASSETS/$model"; aanim="-"; [ "$anim" != "-" ] && aanim="$ASSETS/$anim"
|
||||
v=$("$DIFF" "$amodel" "$aanim" "$bin" --mesh "$mesh" 2>/dev/null | tail -1)
|
||||
m=$(echo "$v" | sed -n 's/.*mat \([0-9.e+-]*\) <.*/\1/p')
|
||||
vt=$(echo "$v" | sed -n 's/.*vtx \([0-9.e+-]*\) <.*/\1/p')
|
||||
st="FAIL"; echo "$v" | grep -q PASS && { st="PASS"; passed=$((passed+1)); }
|
||||
awk -v m="$m" -v w="$worst_mat" 'BEGIN{exit !(m+0>w+0)}' && worst_mat=$m
|
||||
awk -v m="$vt" -v w="$worst_vtx" 'BEGIN{exit !(m+0>w+0)}' && worst_vtx=$vt
|
||||
printf ' [%-4s] %-40s %-14s mat=%-10s vtx=%s\n' "$st" "$(basename "$model")" "$(basename "$anim") @${t}" "$m" "$vt"
|
||||
line=" {\"model\":\"$(basename "$model")\",\"anim\":\"$(basename "$anim")\",\"t\":$t,\"mesh\":$mesh,\"mat\":$m,\"vtx\":$vt,\"status\":\"$st\"}"
|
||||
fi
|
||||
[ $first -eq 1 ] && first=0 || echo ',' >> "$JSON"
|
||||
printf '%s' "$line" >> "$JSON"
|
||||
done <<< "$CASES"
|
||||
|
||||
{
|
||||
echo ''
|
||||
echo ' ],'
|
||||
echo " \"summary\": {\"total\": $total, \"passed\": $passed, \"worst_mat\": $worst_mat, \"worst_vtx\": $worst_vtx}"
|
||||
echo '}'
|
||||
} >> "$JSON"
|
||||
|
||||
echo
|
||||
echo "== $passed/$total PASS worst mat=$worst_mat vtx=$worst_vtx → $JSON =="
|
||||
[ "$passed" -eq "$total" ]
|
||||
Reference in New Issue
Block a user