Files
mtgodot-poc/project/race_spec_test.gd
T

141 lines
5.9 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# race_spec_test —— P2:种族规格 `<race>.msm` 解析 + equip_model 身体换装。
# godot --headless --path project --script race_spec_test.gd
extends SceneTree
const RaceSpec = preload("res://ui/race_spec.gd")
const EquipModel = preload("res://ui/equip_model.gd")
class FakeClient extends Node:
signal inventory_changed(window: int, cell: int)
signal entity_main_set(vid: int)
var equip := []
func _init():
for i in 11: equip.append({"vnum": 0, "count": 0, "wear": i})
func get_equipment() -> Array: return equip
func set_wear(idx, vnum):
equip[idx] = {"vnum": vnum, "count": 1, "wear": idx}
inventory_changed.emit(2, 90 + idx)
class StubModel extends Node3D:
var gr2_path := ""
var weapon_gr2 := ""
var hair_gr2 := ""
var hair_skin := ""
var surf := {}
func _set(p, v):
if p == "gr2_path": gr2_path = v; return true
if p == "weapon_gr2": weapon_gr2 = v; return true
if p == "hair_gr2": hair_gr2 = v; return true
if p == "hair_skin": hair_skin = v; return true
return false
func set_surface_texture(s, path): surf[s] = path
var _fail := 0
func _ck(c: bool, m: String) -> void:
if not c:
_fail += 1
printerr("FAIL: " + m)
func _init() -> void:
call_deferred("_run_and_finish")
func _run_and_finish() -> void:
await _run()
if _fail == 0:
print("PASS: race_spec_test (msm parse + body swap)")
quit(0)
else:
printerr("%d check(s) failed" % _fail)
quit(1)
func _run() -> void:
var assets := AssetRoot.path()
# 找一个真实的 warrior race spec
var candidates := [
assets.path_join("PC/ymir work/pc/warrior/warrior.msm"),
assets.path_join("season1/season1/pc/warrior.msm"),
assets.path_join("pc2/ymir work/pc2/warrior/warrior.msm"),
]
var spec_path := ""
for c in candidates:
if FileAccess.file_exists(c):
spec_path = c
break
if spec_path == "":
print(" (skip: no warrior.msm)")
return
var rs := RaceSpec.new()
_ck(rs.load_file(spec_path), "race spec parsed: %s" % spec_path)
_ck(not rs.base_model.begins_with(String.chr(1)), "quoted tokens do not retain lexer sentinel")
_ck(rs.shapes.size() >= 5, "shapes parsed (%d)" % rs.shapes.size())
_ck(rs.base_model.to_lower().contains("warrior_novice"), "base_model = warrior_novice (%s)" % rs.base_model)
var s0: Dictionary = rs.shape(0)
_ck(s0.get("model", "").to_lower() == "warrior_novice.gr2", "shape 0 model = warrior_novice.gr2 (%s)" % s0.get("model"))
# shape 9 是 cheongrin(见 warrior.msm
var s9: Dictionary = rs.shape(9)
if not s9.is_empty():
_ck(s9.get("model", "").to_lower().contains("cheongrin"), "shape 9 model = warrior_cheongrin (%s)" % s9.get("model"))
var h3: Dictionary = rs.hair(3)
if not h3.is_empty():
_ck(h3.get("model", "").contains("hair"), "hair 3 model has 'hair' (%s)" % h3.get("model"))
_ck(h3.get("target_skin", "").contains("red"), "hair 3 target_skin = red variant (%s)" % h3.get("target_skin"))
# 用户出错的角色用 HairIndex 0;它不是推测的默认路径,必须由 MSM
# 明确解析到 hair_1_1,且保持可换的 TargetSkin。
var h0: Dictionary = rs.hair(0)
_ck(h0.get("path", "") == rs.hair_path and rs.hair_path != "",
"empty SpecialPath inherits HairData PathName")
_ck(h0.get("model", "").to_lower() == "hair/hair_1_1.gr2",
"hair 0 model = hair/hair_1_1.gr2 (%s)" % h0.get("model", ""))
_ck(h0.get("target_skin", "").to_lower().contains("warrior_hair_01"),
"hair 0 target skin is warrior_hair_01 (%s)" % h0.get("target_skin", ""))
# --- equip_model 用 race_spec 换身体 ---
var fc := FakeClient.new()
var model := StubModel.new()
get_root().add_child(fc)
get_root().add_child(model)
var em: Node = EquipModel.new()
get_root().add_child(em)
em.setup(fc, null, func() -> Node: return model, assets, 0) # race 0 = warrior
# 装一件 shape 9cheongrin)的 armorarmor_shape_of 默认 vnum==shape
fc.set_wear(0, 9)
await process_frame
if not rs.shape(9).is_empty():
_ck(model.gr2_path.to_lower().contains("cheongrin") and FileAccess.file_exists(model.gr2_path),
"body swap -> real warrior_cheongrin.gr2 (%s)" % model.gr2_path)
# 换 shape 0(回 novice)—— 同模型不同 TargetSkin -> surface 0 覆盖
fc.set_wear(0, 1) # shape 1: novice.gr2 + TargetSkin blue
await process_frame
if not rs.shape(1).is_empty():
_ck(model.gr2_path.to_lower().contains("novice"), "shape 1 -> novice body")
_ck(model.surf.has(0), "shape 1 recolour -> set_surface_texture(0, ...)")
# 标准发型有两段编号:0..5 是 hair_1 的换色,1001..1012 才是
# hair_2..4 的不同几何。它们不是道具 vnum,远端/选角不能把它们
# 丢给 item_list 后静默回落到 PlayerView 的 hair_1_1 默认模型。
var remote_model := StubModel.new()
get_root().add_child(remote_model)
var remote: Node = EquipModel.new()
get_root().add_child(remote)
remote.setup_remote(null, func() -> Node: return remote_model, assets, 0,
[0, 0, 0, 1001])
await process_frame
var h1001: Dictionary = rs.hair(1001)
_ck(not h1001.is_empty(), "hair 1001 exists in warrior.msm")
var remote_h1001: Dictionary = remote._spec_hair(1001)
_ck(not remote_h1001.is_empty(), "remote actor resolves hair 1001 (%s)" % remote_h1001)
_ck(remote._hair_part() == 1001, "remote actor preserves hair part 1001 (%s)" % str(remote._remote_parts))
var remote_hair_model: String = remote._resolve_spec_asset(remote_h1001, String(remote_h1001.get("model", "")))
_ck(remote_hair_model != "", "hair 1001 source model path resolves (%s)" % remote_hair_model)
# setup_remote refreshes synchronously; this direct retry distinguishes a
# setup ordering bug from asset/spec resolution failure.
remote._apply_hair_from_parts(remote_model, true)
if not h1001.is_empty():
_ck(remote_model.hair_gr2.to_lower().ends_with(String(h1001.get("model", "")).to_lower()),
"standard hair 1001 resolves MSM geometry (%s)" % remote_model.hair_gr2)
_ck(remote_model.hair_skin.to_lower().ends_with(String(h1001.get("target_skin", "")).to_lower()),
"standard hair 1001 resolves MSM skin (%s)" % remote_model.hair_skin)