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:
shen
2026-08-31 20:02:12 +09:00
co-authored by Claude Sonnet 5
parent f4917a2b3b
commit 47baf6c0c6
414 changed files with 69568 additions and 385 deletions
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env bash
# M2 门禁·主:对测试资产集跑 oracle dump + oracle_diff,汇总 test/m2-numeric.json。
# 前置:oracle/build-wine.sh 已产出 oracle.exeWINEPREFIX 已 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" ]