Files
mtgodot-poc/project/player_trade_exchange_system.gd
T
shenandshen 66d217b313 feat(client): 完成40250客户端核心功能1:1对齐与桥梁高度采样修复
- 桥梁与静态物体高度采样修复:
  - 严格对齐 40250 CMapOutdoor::GetHeight 与 CAttributeInstance::GetHeight
  - 解析 .mdatr 中的 AttributeHeight 网格,使用 is_in_triangle_2d 准确计算桥面多边形平面方程
  - sample_height 查询邻近区块并返回 fMAX(fObjectHeight, fTerrainHeight),彻底解决走上桥面穿透掉入水底/河床的问题
  - 新增 test_bridge_height_parity.gd 自动化对拍测试
- 40250 怪物击杀经验动效:
  - 1:1 实现 FLY_EXP(0) / FLY_HP / FLY_SP 粒子轨迹与爆炸吸附
- 40250 客户端全系统功能对齐(Batches 1-31):
  - 包含公会、交易、骑乘、变身、钓鱼、采矿、商城、信件、结婚、地牢等 134 套对拍系统与自动化回归测试
- 文档沉淀:
  - 新增 docs/CLIENT-PARITY-AUDIT-AND-FIX-GUIDE.md 客户端对拍缺陷发现与修复工程指南
2026-09-19 08:51:25 -07:00

344 lines
10 KiB
GDScript

# player_trade_exchange_system.gd —— 40250 官方玩家面对面安全交易双向锁定系统 1:1
# 严格对照:
# metin2/src/server/game/src/exchange.h
# metin2/src/server/game/src/exchange.cpp
# metin2/src/server/game/src/char.cpp (OpenExchange, SetExchange, etc.)
extends RefCounted
const EXCHANGE_ITEM_MAX_NUM := 12 # 官方交易面板 12 格 (4x3 布局)
const EXCHANGE_MAX_DISTANCE := 1000 # 官方交易最大距离 1000
# 玩家交易单边状态
class TradeSide:
var player_id: int
var player_name: String
var is_ready: bool = false
var offered_gold: int = 0
# 12 个格位: display_pos (0..11) -> item_data Dictionary or null
var display_slots: Array = []
# 关联背包位置: display_pos -> inv_slot
var inv_positions: Dictionary = {}
func _init(id: int, name: String) -> void:
player_id = id
player_name = name
is_ready = false
offered_gold = 0
display_slots.clear()
for i in range(EXCHANGE_ITEM_MAX_NUM):
display_slots.append(null)
inv_positions.clear()
func count_items() -> int:
var c := 0
for it in display_slots:
if it != null:
c += 1
return c
var side_a: TradeSide = null
var side_b: TradeSide = null
var is_active: bool = false
# 模拟玩家的背包与金币引用(便于原子化结算)
# player_data = { "id": int, "name": int, "gold": int, "inv": Array, "max_inv_slots": int }
var player_a_ref: Dictionary = {}
var player_b_ref: Dictionary = {}
func _init() -> void:
side_a = null
side_b = null
is_active = false
# 开启双向交易会话 (1:1 OpenExchange)
func start_exchange(player_a: Dictionary, player_b: Dictionary, distance: float = 200.0) -> Dictionary:
var res := { "success": false, "error_code": "OK", "message": "" }
if distance > EXCHANGE_MAX_DISTANCE:
res["error_code"] = "TOO_FAR"
res["message"] = "双方距离太远,无法进行交易。"
return res
if player_a.get("id") == player_b.get("id"):
res["error_code"] = "CANNOT_TRADE_SELF"
res["message"] = "无法与自己进行交易。"
return res
# 检查状态
if player_a.get("is_dead", false) or player_b.get("is_dead", false):
res["error_code"] = "PLAYER_DEAD"
res["message"] = "处于死亡状态的玩家无法交易。"
return res
if player_a.get("is_in_safebox", false) or player_b.get("is_in_safebox", false) \
or player_a.get("is_in_shop", false) or player_b.get("is_in_shop", false):
res["error_code"] = "BUSY_WINDOW"
res["message"] = "一方正处于其他交互窗口中,无法交易。"
return res
player_a_ref = player_a
player_b_ref = player_b
side_a = TradeSide.new(player_a["id"], player_a.get("name", "PlayerA"))
side_b = TradeSide.new(player_b["id"], player_b.get("name", "PlayerB"))
is_active = true
res["success"] = true
res["message"] = "交易窗口已开启。"
return res
# 获取玩家所属的交易边
func _get_side(player_id: int) -> TradeSide:
if side_a != null and side_a.player_id == player_id:
return side_a
elif side_b != null and side_b.player_id == player_id:
return side_b
return null
# 获取交易对手边
func _get_other_side(player_id: int) -> TradeSide:
if side_a != null and side_a.player_id == player_id:
return side_b
elif side_b != null and side_b.player_id == player_id:
return side_a
return null
func _get_player_ref(player_id: int) -> Dictionary:
if player_a_ref.get("id") == player_id:
return player_a_ref
elif player_b_ref.get("id") == player_id:
return player_b_ref
return {}
# 放入物品到交易栏 (1:1 CExchange::AddItem)
# 核心防调包规则:一旦修改物品,双方锁定状态立即重置为 false!
func add_item(player_id: int, inv_slot: int, display_pos: int) -> Dictionary:
var res := { "success": false, "error_code": "OK", "message": "" }
if not is_active:
res["error_code"] = "TRADE_NOT_ACTIVE"
return res
var side := _get_side(player_id)
var other := _get_other_side(player_id)
var player_data := _get_player_ref(player_id)
if side == null or other == null or player_data.is_empty():
res["error_code"] = "INVALID_PARTICIPANT"
return res
if display_pos < 0 or display_pos >= EXCHANGE_ITEM_MAX_NUM:
res["error_code"] = "INVALID_DISPLAY_POS"
return res
if side.display_slots[display_pos] != null:
res["error_code"] = "SLOT_ALREADY_OCCUPIED"
return res
var inv: Array = player_data.get("inv", [])
if inv_slot < 0 or inv_slot >= inv.size() or inv[inv_slot] == null:
res["error_code"] = "ITEM_NOT_FOUND"
return res
var it: Dictionary = inv[inv_slot]
# 1:1 防交易检查: ANTI_GIVE 标记 / 灵魂绑定 / 装备中
if it.get("anti_give", false) or it.get("is_soulbound", false) or it.get("is_equipped", false):
res["error_code"] = "ITEM_UNTRADEABLE"
res["message"] = "该物品受法则限制,不可交易或转移。"
return res
# 放入交易槽
side.display_slots[display_pos] = it.duplicate(true)
side.inv_positions[display_pos] = inv_slot
# 40250 官方核心安全机制: 重置双方锁定状态!
side.is_ready = false
other.is_ready = false
res["success"] = true
res["message"] = "物品已上架交易栏,双方就绪状态已重置。"
return res
# 从交易栏取回物品 (1:1 CExchange::RemoveItem)
# 同样重置双方锁定状态!
func remove_item(player_id: int, display_pos: int) -> Dictionary:
var res := { "success": false, "error_code": "OK" }
if not is_active:
res["error_code"] = "TRADE_NOT_ACTIVE"
return res
var side := _get_side(player_id)
var other := _get_other_side(player_id)
if side == null or other == null:
res["error_code"] = "INVALID_PARTICIPANT"
return res
if display_pos < 0 or display_pos >= EXCHANGE_ITEM_MAX_NUM or side.display_slots[display_pos] == null:
res["error_code"] = "SLOT_EMPTY"
return res
side.display_slots[display_pos] = null
side.inv_positions.erase(display_pos)
# 40250 官方安全机制: 重置双方锁定状态!
side.is_ready = false
other.is_ready = false
res["success"] = true
return res
# 设置出价金币 (1:1 CExchange::AddGold)
# 同样重置双方锁定状态!
func set_gold(player_id: int, amount: int) -> Dictionary:
var res := { "success": false, "error_code": "OK", "message": "" }
if not is_active:
res["error_code"] = "TRADE_NOT_ACTIVE"
return res
var side := _get_side(player_id)
var other := _get_other_side(player_id)
var player_data := _get_player_ref(player_id)
if side == null or other == null or player_data.is_empty():
res["error_code"] = "INVALID_PARTICIPANT"
return res
if amount < 0:
res["error_code"] = "INVALID_GOLD"
return res
if player_data.get("gold", 0) < amount:
res["error_code"] = "INSUFFICIENT_GOLD"
res["message"] = "身上金币不足,无法出价。"
return res
side.offered_gold = amount
# 40250 官方安全机制: 重置双方锁定状态!
side.is_ready = false
other.is_ready = false
res["success"] = true
res["message"] = "出价金额已更新,双方就绪状态已重置。"
return res
# 点击锁定/同意 (1:1 CExchange::Accept)
# 双方均锁定后自动触发原子化结算
func set_ready(player_id: int, ready_state: bool) -> Dictionary:
var res := {
"success": false,
"trade_completed": false,
"error_code": "OK",
"message": ""
}
if not is_active:
res["error_code"] = "TRADE_NOT_ACTIVE"
return res
var side := _get_side(player_id)
var other := _get_other_side(player_id)
if side == null or other == null:
res["error_code"] = "INVALID_PARTICIPANT"
return res
side.is_ready = ready_state
res["success"] = true
# 若双方均已 Ready 锁定,执行原子化交付完成交易 (CExchange::Done)
if side_a.is_ready and side_b.is_ready:
var done_res = _execute_atomic_trade()
if done_res["success"]:
res["trade_completed"] = true
res["message"] = "交易成功!双方物品与金币已安全过户。"
else:
res["trade_completed"] = false
res["error_code"] = done_res["error_code"]
res["message"] = done_res["message"]
else:
res["message"] = "已就绪锁定,等待对方确认。"
return res
# 背包剩余空位计算
func _get_free_inv_slots(player_data: Dictionary) -> int:
var inv: Array = player_data.get("inv", [])
var free_count := 0
for it in inv:
if it == null:
free_count += 1
return free_count
# 原子化事务交付执行 (1:1 CExchange::Done & CheckSpace)
func _execute_atomic_trade() -> Dictionary:
var res := { "success": false, "error_code": "OK", "message": "" }
# 1. 再次校验金币是否充足
if player_a_ref.get("gold", 0) < side_a.offered_gold:
res["error_code"] = "PLAYER_A_LESS_GOLD"
res["message"] = "一方金币不足,交易终止。"
return res
if player_b_ref.get("gold", 0) < side_b.offered_gold:
res["error_code"] = "PLAYER_B_LESS_GOLD"
res["message"] = "一方金币不足,交易终止。"
return res
# 2. 空间校验 (CheckSpace):
# 玩家 A 需容纳 B 给予的所有物品;玩家 B 需容纳 A 给予的所有物品
# 考虑双方移出物品后腾出的背包空位
var a_items_count := side_a.count_items()
var b_items_count := side_b.count_items()
var a_free := _get_free_inv_slots(player_a_ref) + a_items_count
var b_free := _get_free_inv_slots(player_b_ref) + b_items_count
if a_free < b_items_count:
res["error_code"] = "PLAYER_A_INSUFFICIENT_SPACE"
res["message"] = "玩家 A 背包空间不足,无法接收对方物品。"
return res
if b_free < a_items_count:
res["error_code"] = "PLAYER_B_INSUFFICIENT_SPACE"
res["message"] = "玩家 B 背包空间不足,无法接收对方物品。"
return res
# 3. 执行金币转移
player_a_ref["gold"] = player_a_ref.get("gold", 0) - side_a.offered_gold + side_b.offered_gold
player_b_ref["gold"] = player_b_ref.get("gold", 0) - side_b.offered_gold + side_a.offered_gold
# 4. 执行物品转移
# 从 A 背包移除 A 出售的物品
var inv_a: Array = player_a_ref.get("inv", [])
for dpos in side_a.inv_positions:
var inv_slot: int = side_a.inv_positions[dpos]
inv_a[inv_slot] = null
# 从 B 背包移除 B 出售的物品
var inv_b: Array = player_b_ref.get("inv", [])
for dpos in side_b.inv_positions:
var inv_slot: int = side_b.inv_positions[dpos]
inv_b[inv_slot] = null
# 将 A 的物品放入 B 的背包空位
for it in side_a.display_slots:
if it != null:
for i in range(inv_b.size()):
if inv_b[i] == null:
inv_b[i] = it.duplicate(true)
break
# 将 B 的物品放入 A 的背包空位
for it in side_b.display_slots:
if it != null:
for i in range(inv_a.size()):
if inv_a[i] == null:
inv_a[i] = it.duplicate(true)
break
# 5. 关闭会话
is_active = false
res["success"] = true
return res
# 取消交易 (1:1 CExchange::Cancel)
func cancel_trade() -> void:
is_active = false
side_a = null
side_b = null