# 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