# race_spec_test —— P2:种族规格 `.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 surf := {} func _set(p, v): if p == "gr2_path": gr2_path = v; return true if p == "weapon_gr2": weapon_gr2 = 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: _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(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")) # --- 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 9(cheongrin)的 armor(armor_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, ...)")