port 2D steps 3-4: strict 40250 asset gate with a provenance manifest

tools/asset_manifest.py records path/size/sha256 of pack/Index, every .eix/.epk
and Eternexus/root/*.msm (audit/assets/40250-client.json). The gate script sets
MT_ASSETS_STRICT=1, verifies the manifest, runs the native pack/proto tests and
four Godot tests, and fails on a missing client, a mismatch or a skip.
MT_40250_CLIENT now comes from the environment for the native tests too.
remote_player_test accepts both weighted WAIT variants (wait/wait_1).
PORT-PLAN §5 records the 2D status.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-23 01:41:38 +09:00
co-authored by Claude Opus 5
parent 240d2827c6
commit a9b31dec67
7 changed files with 1323 additions and 2 deletions
File diff suppressed because it is too large Load Diff
+1
View File
@@ -491,3 +491,4 @@
{"time":"2026-09-23T10:00:00Z","event":"port_round","unit":"EterPack/*, EterBase/{tea,lzo,Timer,Stl,Random}.cpp + platform EterBase/{CRC32,FileBase,MappedFile,Debug,Utils}","ported":["EterPack/EterPack.cpp","EterPack/EterPackManager.cpp","EterPack/EterPackCursor.cpp","EterPack/EterPackPolicy_CSHybridCrypt.cpp","EterBase/tea.cpp","EterBase/lzo.cpp","EterBase/Timer.cpp","EterBase/Stl.cpp","EterBase/Random.cpp"],"status_note":"mirror copies + platform FileBase/MappedFile/CRC32/Debug/Utils; not runtime-reachable until the asset_io pack backend (PORT-PLAN §5 step 5), so port-map statuses stay TODO","divergent":[],"needs_live":[],"corrections":["2R registered_packs 136 was wrong: 124 Index pairs -> 119 RegisterPack successes, 103 distinct packs + root (PORT-PLAN, memory)"],"findings":["libc++ unordered_multimap also keeps first-registered-wins: 0 wrong winners over 2282 overrides","exactly the 4 short-layout SECURITY files fail CEterPackManager::Get","pack/Index spells Etc for ETC.eix: CEterPack::CreateIndexFile fopens directly, so the bootstrap must resolve the on-disk dbname"],"keys":"EterPack TEA keys generated at configure time into the build tree (EterPackKeys.generated.h), never committed","tests":["extension/tests/port_eterpack_test.cpp PASS (macOS, 52605 files / 2023459178 bytes)","script/port_gate.sh macos/android/ios/windows PASS"]}
{"time":"2026-09-23T12:00:00Z","event":"port_round","unit":"platform UserInterface PackInitialize + EterBase/FileLoader.cpp + pack:// asset_io backend (PORT-PLAN §5 step 5b)","ported":["EterBase/FileLoader.cpp"],"deleted":[],"divergent":[],"needs_live":[],"status_note":"PackInitialize (platform, on-disk dbname resolution, no LogBoxf) + mtpack40250 backend + Metin2Pack node; no runtime reader uses pack:// until the proto/GDScript switch, so port-map statuses stay TODO","tests":["extension/tests/port_eterpack_test.cpp via PackSingletons+PackInitialize(\"pack\"): 103 packs, oracle order, PASS"],"commit":"5b"}
{"time":"2026-09-23T12:30:00Z","event":"port_round","unit":"proto reader -> 40250 item_proto/mob_proto via CLZO + pack://locale/<lang>/ (PORT-PLAN §5 step 2 + step 5 runtime switch)","ported":["EterPack/EterPack.cpp (16 fns)","EterPack/EterPackManager.cpp (14)","EterPack/EterPackPolicy_CSHybridCrypt.cpp (1)","EterBase/lzo.cpp (11)","EterBase/tea.cpp (2)","EterBase/Stl.cpp (2)"],"adapted":["UserInterface/UserInterface.cpp PackInitialize","EterBase/FileLoader.cpp (5)","EterBase/MappedFile.cpp (9)","EterBase/FileBase.cpp (5)","EterBase/CRC32.cpp GetCRC32"],"deleted":["extension/src/proto/proto.cpp m2dev reader: XChaCha20 + MCOZ, 236-byte item / 335-byte mob strides"],"divergent":[],"needs_live":[],"status_note":"statuses set only for the functions a clang coverage run of proto_test + port_eterpack_test hit; the rest stay TODO. SMobTable is a local #pragma pack(1) mirror in proto.cpp (static_assert 255) until batch 2P ports GameLib/PythonNonPlayer. name = cp1252(szLocaleName) as CItemData::GetName / CPythonNonPlayer::GetMonsterName","tests":["extension/tests/proto_test.cpp: 5743 items / 1334 mobs from pack://locale/en PASS","extension/tests/proto_item_layout_test.cpp 156/255 strides, 236/335 rejected PASS","godot netbridge/equip_model/remote_player/mob_view PASS"],"commit":"2D-proto"}
{"time":"2026-09-23T13:30:00Z","event":"asset_gate","unit":"PORT-PLAN §5 steps 3-4: MT_ASSETS_STRICT + provenance manifest + script/run_40250_asset_gate.sh","manifest":"audit/assets/40250-client.json (pack/Index + 214 .eix/.epk + 8 Eternexus/root/*.msm = 223 files, 1407474045 bytes, sha256)","env":{"MT_40250_CLIENT":"40250 Client dir (native tests, AssetRoot.client_path, gate)","MT_ASSETS":"m2dev loose dev override only","M2_ASSETS":"no longer used by tests"},"fixes":["remote_player_test accepted only twohand_sword/wait.msa; WAIT picks wait/wait_1 by weight, so it failed ~1/3 of runs"],"tests":["script/run_40250_asset_gate.sh macOS PASS (manifest, ctest port.eterpack|proto.*, godot netbridge/equip_model/remote_player/mob_view)","gate with MT_40250_CLIENT=/nonexistent exits 1"],"commit":"2D-gate"}
+30
View File
@@ -349,6 +349,32 @@ extension/third_party/cpython-2.7.18/ # 静态库(2P 批次
5. 资源根切换按 2R 第 6 条执行:移植 `EterPack``CLZObject`/TEA 已经在第 2 步)和 `EterPackManager`,接上
`asset_io` 的 pack 后端,读取方逐个改用 40250 路径,跑严格门禁,更新本节状态。
状态(2026-09-23):
| 步骤 | 状态 | 提交 |
| --- | --- | --- |
| 1. msm 路径 | 完成:`equip_model.gd` 先查 `root/<cls>_<m/w>.msm`m2dev 的 `root/msm/` 作后备;8 个种族都能从 `Eternexus/root` 找到 | 240d2827 |
| 2. proto | 完成:`proto.cpp` 用移植的 `CLZO`/TEA 读 MIPX/MMPT156 / 255 字节),m2dev 的 XChaCha20 / 236 / 335 读取已删除;名字按 `GetName`/`GetMonsterName``szLocaleName`cp1252)。`SMobTable` 暂时在 `proto.cpp` 里放一份 `#pragma pack(1)` 镜像(`static_assert == 255`),2P 移植 `GameLib/PythonNonPlayer` 后改用正式镜像 | 232d0461 |
| 3. 严格模式 | 完成(见下) | 本批最后一个提交 |
| 4. 门禁脚本 | 完成:`script/run_40250_asset_gate.sh` 在 macOS 上 PASS | 同上 |
| 5. 资源根 | 部分完成:pack 后端(`PackInitialize` + `asset_io``pack://` + `Metin2Pack` 节点)已接入,proto 经 `pack://locale/<lang>/` 读取(5a 1f1004da、5b 5a26e93f)。模型、贴图、地图和 root/locale 文本仍读 m2dev `assets/`,由 2V 切片逐个切换 | |
环境变量(取代步骤 3 里“两者都透传”的过渡方案):
- `MT_40250_CLIENT`40250 的 `Client` 目录(`pack/``Eternexus/`)。native 测试(CMake 缓存变量默认读同名环境变量,
再退回 `${MT_40250_SOURCE}/../../Client`)、`AssetRoot.client_path()` 和门禁脚本都用它;
- `MT_ASSETS`:只表示 m2dev 散文件目录(开发期覆盖);
- `M2_ASSETS`:测试不再使用;
- `MT_ASSETS_STRICT=1`:native 测试在资源缺失时返回 1(不是 ctest 的跳过码 77),门禁脚本把 Godot 测试输出里的
`(skip` 也算失败。
来源清单:`tools/asset_manifest.py generate|verify <Client>` 记录 `pack/Index`、全部 `.eix/.epk`
`Eternexus/root/*.msm` 的路径、大小、sha256,结果在 `audit/assets/40250-client.json`223 个文件,1,407,474,045
字节)。文件缺失、内容变化或清单外多出的文件都会让 verify 失败。
已知问题(不是本批引入的):完整 Android 链接在 API 24 上失败,原因是 `extension/src/net/text_codec.cpp` 用了 iconv
本批的 mtproto、`pack40250_node``asset_io``proto_node` 都能在 Android 上编译。
## 6. 进度与 port-map 基线
实时数字用 `port_map.py status` 查看。
@@ -370,6 +396,10 @@ extension/third_party/cpython-2.7.18/ # 静态库(2P 批次
| platform | 2091 | 0 | |
| 合计 | 10609 | 0 | |
2D 之后(2026-09-23):logic 46 个 `PORTED`EterPack、EterPackManager、lzo、tea、Stl),platform 21 个 `ADAPTED`
PackInitialize、FileLoader、MappedFile、FileBase、CRC32)。只标了 proto_test + port_eterpack_test 覆盖率运行实际执行到
的函数。
与基线前的数字(logic 3285、python 4890、platform 2229,共 10404)相比的变化:
- 函数清单(`port_map.cpp_functions`)用 platform 骨架的 clang 声明逐个对照后修正:
+4 -1
View File
@@ -140,7 +140,10 @@ if(BUILD_TESTING AND CMAKE_SYSTEM_NAME STREQUAL CMAKE_HOST_SYSTEM_NAME)
# Against the real 40250 Client/pack next to the reference source; skipped (77) when it is missing,
# failed instead under MT_ASSETS_STRICT=1.
set(MT_40250_CLIENT "${MT_40250_SOURCE}/../../Client" CACHE PATH "40250 Client directory (pack/, Eternexus/)")
set(MT_40250_CLIENT "$ENV{MT_40250_CLIENT}" CACHE PATH "40250 Client directory (pack/, Eternexus/)")
if(NOT MT_40250_CLIENT)
set(MT_40250_CLIENT "${MT_40250_SOURCE}/../../Client")
endif()
add_executable(port_eterpack_test ${CMAKE_CURRENT_SOURCE_DIR}/../../tests/port_eterpack_test.cpp)
target_link_libraries(port_eterpack_test PRIVATE port_platform)
add_test(NAME port.eterpack COMMAND $<TARGET_FILE:port_eterpack_test> ${MT_40250_CLIENT})
+3 -1
View File
@@ -63,7 +63,9 @@ func _run() -> void:
await process_frame
if proto:
_ck(pv.get_motion_mode() == 3, "remote weapon 3159 -> TWOHAND_SWORD mode")
_ck(String(pv.anim.get("anim_path")).contains("/twohand_sword/wait.msa"),
# WAIT 按权重随机选 wait.msa / wait_1.msa(见 motion_registry_test、player_motion_test),两者都是双手握姿。
var wait_re := RegEx.create_from_string("/twohand_sword/wait(_\\d+)?\\.msa$")
_ck(wait_re.search(String(pv.anim.get("anim_path"))) != null,
"remote heavy-weapon wait uses two-hand grip pose")
_ck(pv.remote_equip._weapon_refine_fx.size() == 1,
"remote +9 heavy weapon creates one persistent refine effect")
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# 40250 asset gate (docs/PORT-PLAN.md 2D steps 3-4): with MT_ASSETS_STRICT=1, verify the provenance manifest
# (audit/assets/40250-client.json), then run the native pack/proto tests and the Godot tests that read the
# 40250 client. Missing assets, a manifest mismatch, a skipped test or a failed one all fail the gate; nothing is
# reported as PASS without having run.
#
# MT_40250_CLIENT=<.../Server Client TMP4/Client> script/run_40250_asset_gate.sh
#
# Uses the build tree $MT_BUILD_DIR (default build/, configured with BUILD_TESTING=ON) and $GODOT (default godot).
set -uo pipefail
cd "$(dirname "$0")/.."
REPO="$PWD"
export MT_ASSETS_STRICT=1
CLIENT="${MT_40250_CLIENT:-$REPO/../40250/Server Client TMP4/Client}"
BUILD="${MT_BUILD_DIR:-build}"
GODOT="${GODOT:-godot}"
LOG="${MT_ASSET_GATE_LOG:-$BUILD/40250-asset-gate.log}"
fail=0
: >"$LOG" 2>/dev/null || LOG=/dev/null
step() { # name, command...
local name="$1"; shift
if "$@" >>"$LOG" 2>&1; then
echo "PASS $name"
else
echo "FAIL $name (see $LOG)"
fail=1
fi
}
if [ ! -f "$CLIENT/pack/Index" ]; then
echo "FAIL no 40250 client at '$CLIENT' (set MT_40250_CLIENT)"
exit 1
fi
CLIENT="$(cd "$CLIENT" && pwd)"
export MT_40250_CLIENT="$CLIENT"
step "manifest audit/assets/40250-client.json" python3 tools/asset_manifest.py verify "$CLIENT"
native() {
cmake -S . -B "$BUILD" -DBUILD_TESTING=ON "-DMT_40250_CLIENT=$CLIENT" \
&& cmake --build "$BUILD" --target port_eterpack_test proto_test proto_item_layout_test mtgodot -j 8 \
&& ctest --test-dir "$BUILD" -R '^(port\.eterpack|proto\.)' --output-on-failure --no-tests=error
} # under MT_ASSETS_STRICT=1 the tests exit 1, not the ctest skip code 77, when the client is missing
step "native pack/proto tests" native
godot_test() { # script: runs headless; a "(skip" line is a failure under the gate
local out
out="$(cd project && "$GODOT" --headless --path . -s "$1" 2>&1)"
local rc=$?
printf '%s\n' "$out"
[ $rc -eq 0 ] && ! grep -q "(skip" <<<"$out" && grep -q "^PASS" <<<"$out"
}
for t in netbridge_test.gd equip_model_test.gd remote_player_test.gd mob_view_test.gd; do
step "godot $t" godot_test "$t"
done
if [ $fail -ne 0 ]; then
echo "40250 asset gate: FAIL"
exit 1
fi
echo "40250 asset gate: PASS"
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""Provenance manifest of the 40250 client assets the tests and the runtime read (docs/PORT-PLAN.md 2D step 3).
Records path (relative to the Client directory) + size + sha256 for pack/Index, every .eix/.epk in pack/ and the
loose Eternexus/root/*.msm race specs. `verify` fails on a missing, changed or untracked file, so a strict
run proves it read exactly the bytes the manifest names.
tools/asset_manifest.py generate <Client dir> [-o audit/assets/40250-client.json]
tools/asset_manifest.py verify <Client dir> [-m audit/assets/40250-client.json]
"""
import argparse
import hashlib
import json
import os
import sys
DEFAULT = os.path.join(os.path.dirname(__file__), "..", "audit", "assets", "40250-client.json")
def sha256(path):
h = hashlib.sha256()
with open(path, "rb") as f:
for block in iter(lambda: f.read(1 << 20), b""):
h.update(block)
return h.hexdigest()
def tracked(client):
files = ["pack/Index"]
pack = os.path.join(client, "pack")
files += sorted("pack/" + n for n in os.listdir(pack) if n.lower().endswith((".eix", ".epk")))
root = os.path.join(client, "Eternexus", "root")
if os.path.isdir(root):
files += sorted("Eternexus/root/" + n for n in os.listdir(root) if n.lower().endswith(".msm"))
return files
def entry(client, rel):
p = os.path.join(client, rel)
return {"path": rel, "size": os.path.getsize(p), "sha256": sha256(p)}
def generate(args):
files = [entry(args.client, rel) for rel in tracked(args.client)]
out = {
"schema": 1,
"source": "40250 Server Client TMP4/Client",
"note": "path relative to the Client directory; regenerate with tools/asset_manifest.py generate",
"files": files,
}
os.makedirs(os.path.dirname(os.path.abspath(args.output)), exist_ok=True)
with open(args.output, "w") as f:
json.dump(out, f, indent=1)
f.write("\n")
print("%d files, %d bytes -> %s" % (len(files), sum(e["size"] for e in files), args.output))
return 0
def verify(args):
try:
with open(args.manifest) as f:
want = json.load(f)["files"]
except (OSError, ValueError, KeyError) as e:
print("asset_manifest: cannot read manifest %s: %s" % (args.manifest, e), file=sys.stderr)
return 1
if not os.path.isfile(os.path.join(args.client, "pack", "Index")):
print("asset_manifest: no pack/Index under %s" % args.client, file=sys.stderr)
return 1
bad = 0
for e in want:
p = os.path.join(args.client, e["path"])
if not os.path.isfile(p):
print("missing %s" % e["path"], file=sys.stderr)
bad += 1
elif os.path.getsize(p) != e["size"] or sha256(p) != e["sha256"]:
print("changed %s" % e["path"], file=sys.stderr)
bad += 1
extra = sorted(set(tracked(args.client)) - {e["path"] for e in want})
for rel in extra:
print("untracked %s" % rel, file=sys.stderr)
bad += len(extra)
print("%d files checked, %d mismatch(es)" % (len(want), bad))
return 1 if bad else 0
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
sub = ap.add_subparsers(dest="cmd", required=True)
g = sub.add_parser("generate")
g.add_argument("client")
g.add_argument("-o", "--output", default=DEFAULT)
v = sub.add_parser("verify")
v.add_argument("client")
v.add_argument("-m", "--manifest", default=DEFAULT)
args = ap.parse_args()
return generate(args) if args.cmd == "generate" else verify(args)
if __name__ == "__main__":
sys.exit(main())