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:
File diff suppressed because it is too large
Load Diff
@@ -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"}
|
||||
|
||||
@@ -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/MMPT(156 / 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 声明逐个对照后修正:
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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")
|
||||
|
||||
Executable
+63
@@ -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"
|
||||
Executable
+100
@@ -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())
|
||||
Reference in New Issue
Block a user