完善客户端功能并同步差距文档

This commit is contained in:
shenlei
2026-09-03 18:40:01 +09:00
parent f93a172296
commit 13ccb8d02d
135 changed files with 21881 additions and 1259 deletions
+97 -21
View File
@@ -14,9 +14,15 @@
# 时装(costume):Metin2 不发独立的 costume 槽包,服务器把 costume 的 vnum 直接写进
# 主角实体的 parts[] 数组(ARMOR/HAIR)。所以每个渲染槽优先取 parts[],为 0 才回退
# get_equipment()。这样 costume「自动」生效,也是渲染别的玩家用的同一条路。
#
# §4.1 修改 4(部位遮挡):解出身体 shape 后过 PartHiding —— 动物 / 怪物时装
# shape 100-103)抑制武器 + 头发;婚纱(shape 201)非捧花时抑制武器;变身
# (seam,暂无实体字段)会把身体 shape 归 0 且清武器 / 头发。刷新顺序对齐
# InstanceBase.cpp ChangeArmor:身体 → 头发 → 武器(→ 盾)。
extends Node
const RaceSpec = preload("res://ui/race_spec.gd")
const PartHiding = preload("res://part_hiding.gd")
const WEAR_BODY := 0
const WEAR_HEAD := 1
@@ -40,10 +46,12 @@ var _spec: RefCounted # RaceSpec(惰性)
var _spec_tried := false
var _last_weapon_vnum := -1
var _last_body_vnum := -1
var _last_head_vnum := -1
var _last_head_vnum := -2 # -1 是「发型被遮挡」的有效态,用 -2 当「未应用」哨兵
var _last_shield_vnum := -1
var _last_hair_part := -1
var main_getter: Callable = Callable() # func() -> int:主角 vid(取 parts 用)
var _remote_mode := false
var _remote_parts: Array = [] # 远端 PC 的 awPart[ARMOR..HAIR]
# armor vnum -> race .msm 的 shape index。默认:有 proto 用 item_proto values[3]
# (对齐客户端 __ArmorVnumToShapeSHAPE_VALUE_SLOT_INDEX=3),否则回退 =vnum。应用方可覆写。
var armor_shape_of: Callable = func(vnum: int) -> int: return _armor_shape_default(vnum)
@@ -70,6 +78,24 @@ func setup(m2client: Node, il: RefCounted, model_getter: Callable, assets := "",
if main_getter.is_valid() and int(main_getter.call()) == v:
refresh())
# 远端 PC 不读取本地 get_equipment();参考端直接消费
# GC_CHAR_ADDITIONAL_INFO / GC_CHARACTER_UPDATE 的 awPart[4]。
func setup_remote(il: RefCounted, model_getter: Callable, assets := "",
race_index := -1, parts := [], proto_node: Node = null) -> void:
client = null
item_list = il
_model_getter = model_getter
assets_root = assets
race = race_index
proto = proto_node
_remote_mode = true
_remote_parts = parts.duplicate() if parts is Array else []
while _remote_parts.size() < 4:
_remote_parts.append(0)
if _remote_parts.size() > 4:
_remote_parts = _remote_parts.slice(0, 4)
refresh()
func set_race(r: int) -> void:
race = r
_spec = null
@@ -81,36 +107,55 @@ func _on_inv_changed(window: int, _cell: int) -> void:
func refresh() -> void:
var model: Node = _model_getter.call() if _model_getter.is_valid() else null
if model == null or client == null:
if model == null or (client == null and not _remote_mode):
return
var eq: Array = client.get_equipment()
if eq.size() <= WEAR_WEAPON:
var eq: Array = client.get_equipment() if client != null and client.has_method("get_equipment") else []
if not _remote_mode and eq.size() <= WEAR_WEAPON:
return
var parts := _parts() # 主角 parts[](时装 / 别的玩家用);空 -> 全回退 eq
# 武器parts[WEAPON] 优先,为 0 回退 WEAR_WEAPON
var wpn_vnum := _eff(parts, PART_WEAPON, eq, WEAR_WEAPON)
if wpn_vnum != _last_weapon_vnum:
_last_weapon_vnum = wpn_vnum
model.set("weapon_gr2", _resolve_weapon(wpn_vnum))
if eq.size() > WEAR_SHIELD:
var sh_vnum := int(eq[WEAR_SHIELD].get("vnum", 0))
if sh_vnum != _last_shield_vnum:
_last_shield_vnum = sh_vnum
model.set("shield_gr2", _resolve_weapon(sh_vnum))
# 身体:parts[ARMOR] 优先(= 普通盔甲或时装),为 0 回退 WEAR_BODY
# --- 身体parts[ARMOR] 优先(普通盔甲或时装),为 0 回退 WEAR_BODY -------------
var body_vnum := _eff(parts, PART_ARMOR, eq, WEAR_BODY)
# 遮挡判定用的 shape(对齐 __ArmorVnumToShapeproto values[3] 非 0,否则 =vnum
var raw_shape := int(armor_shape_of.call(body_vnum)) if armor_shape_of.is_valid() else body_vnum
var is_poly := _is_poly()
if body_vnum != _last_body_vnum:
_last_body_vnum = body_vnum
_apply_body(model, body_vnum)
# 头:parts[HEAD] 优先,为 0 回退 WEAR_HEAD;有模型的头盔覆盖发型槽,否则用发型
# --- 头 / 发:动物·怪物时装(shape 100-103)或变身 -> 隐藏;否则头盔覆盖 / 发型 ----
var hair_off := PartHiding.hair_hidden(raw_shape, is_poly)
var head_vnum := _eff(parts, PART_HEAD, eq, WEAR_HEAD)
if head_vnum != _last_head_vnum:
_last_head_vnum = head_vnum
_apply_head_or_hair(model, head_vnum)
var head_key := -1 if hair_off else head_vnum
if head_key != _last_head_vnum:
_last_head_vnum = head_key
if hair_off:
model.set("hair_gr2", "")
model.set("hair_skin", "")
_last_hair_part = -1 # 恢复时强制重挂发型
else:
_apply_head_or_hair(model, head_vnum)
# --- 武器:parts[WEAPON] 优先,为 0 回退 WEAR_WEAPON;过 PartHiding 遮挡 ---------
var wpn_vnum := _eff(parts, PART_WEAPON, eq, WEAR_WEAPON)
var wpn_eff := PartHiding.effective_weapon(wpn_vnum, raw_shape, is_poly)
if wpn_eff != _last_weapon_vnum:
_last_weapon_vnum = wpn_eff
model.set("weapon_gr2", _resolve_weapon(wpn_eff))
# --- 盾(左手挂点,同武器遮挡规则)--------------------------------------------
if eq.size() > WEAR_SHIELD:
var sh_vnum := int(eq[WEAR_SHIELD].get("vnum", 0))
var sh_eff := 0 if PartHiding.weapon_hidden(raw_shape, is_poly, sh_vnum) else sh_vnum
if sh_eff != _last_shield_vnum:
_last_shield_vnum = sh_eff
model.set("shield_gr2", _resolve_weapon(sh_eff))
# §4.1 修改 4 seam40250 实体表暂无变身字段(m2_client.cpp entity dict 无 poly_race /
# polymorph)。一旦 GC_CHARACTER_UPDATE / GC_CHAR_ADD_INFO 带上变身 race,这里改成读
# client.get_entity(main_getter.call()).get("poly_race", 0) != 0 即可。
func _is_poly() -> bool:
return false
# 有效 vnum:主角 parts[part_idx] 非 0 就用它(时装 / 远端玩家),否则用装备槽
func _eff(parts: Array, part_idx: int, eq: Array, wear_slot: int) -> int:
@@ -119,6 +164,8 @@ func _eff(parts: Array, part_idx: int, eq: Array, wear_slot: int) -> int:
return int(eq[wear_slot].get("vnum", 0)) if eq.size() > wear_slot else 0
func _parts() -> Array:
if _remote_mode:
return _remote_parts
if not main_getter.is_valid() or client == null or not client.has_method("get_entity"):
return []
var vid: int = main_getter.call()
@@ -164,6 +211,8 @@ func _apply_hair_from_parts(model: Node, force := false) -> void:
model.set("hair_skin", ts)
func _hair_part() -> int:
if _remote_mode:
return int(_remote_parts[PART_HAIR]) if _remote_parts.size() > PART_HAIR else -1
if not main_getter.is_valid() or client == null or not client.has_method("get_entity"):
return -1
var vid: int = main_getter.call()
@@ -176,6 +225,10 @@ func _hair_part() -> int:
return -1
func set_hair(index: int) -> void:
if _remote_mode:
_set_remote_part(PART_HAIR, index)
refresh()
return
var model: Node = _model_getter.call() if _model_getter.is_valid() else null
if model == null:
return
@@ -189,6 +242,29 @@ func set_hair(index: int) -> void:
if ts != "":
model.set("hair_skin", ts)
func set_armor(index: int) -> void:
if not _remote_mode:
return
_set_remote_part(PART_ARMOR, index)
refresh()
func set_weapon(index: int) -> void:
if not _remote_mode:
return
_set_remote_part(PART_WEAPON, index)
refresh()
func set_head(index: int) -> void:
if not _remote_mode:
return
_set_remote_part(PART_HEAD, index)
refresh()
func _set_remote_part(part: int, value: int) -> void:
while _remote_parts.size() <= part:
_remote_parts.append(0)
_remote_parts[part] = value
# --- body ------------------------------------------------------------------
# armor vnum -> item_proto values[3](非 0);否则 =vnum(对齐 __ArmorVnumToShape