Files
mtgodot-poc/project/equip_rules.gd
T
shenleiandClaude Code 8092ff44e2 装备校验链/快捷栏/公会/好友情侣/交易仓库五项差距修复(增量 129)
- 装备校验:can_equip 按 CanEquipNow→FindEquipCell→EquipItem 精确重排,
  新增 find_equip_cell() 1:1 复刻(RING/UNIQUE 占用翻转、COSTUME/BELT/DS),
  帝国 antiflag 降级为 tooltip 提示,LIMIT_PCBANG/REAL_TIME* 语义与倒计时文案
- 快捷栏:死亡全槽禁用+激活守卫、变身(affect 220)技能置灰、
  物品槽失配置灰(服务器回收/换武器后刷新)
- 公会:GUILD_AUTH_* 权限位门控踢人/公告/技能升级,/gskillup、
  被宣战应答弹窗(/war·/nowar)、资金存取行(40250 服务端已禁用仍保留 1:1)
- 好友/情侣:messenger 三固定组(好友/公会/情侣),lover_login/logout/
  near/far/divorce 命令事件→在线/在身边状态与离婚隐藏
- 交易/仓库:ExchangeState.last_notice(ALREADY/LESS_GOLD)结束浮层提示;
  仓库改密 /safebox_change_password 三段输入;关窗补 /safebox_close·/mall_close;
  仓库金钱经核实 40250 无 wire 协议,UI 定界只读
- app_flow:重连时 guild_marks_ready 重复连接加 is_connected 守卫
- 相机 look_at 零向量守卫(首帧/传送落点)
- 测试:equip/love/guild 断言更新,79 项中 78 绿(game_camera_test 为
  HEAD 既有失败);真服冒烟 LIVE_SMOKE PASS(192.168.21.203 全链路)
- docs/CLIENT-GAP.md 同步增量 129

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018kUFvQGYusuY3dkfGitmAe
2026-09-07 21:43:30 +09:00

332 lines
13 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.
# equip_rules.gd —— 装备穿戴合法性校验(纯函数校验器)。
#
# 语义顺序 1:1 对齐服务器真实链(已核对 40250 源码):
# `char_item.cpp:7462 CanEquipNow`JOB antiflag → limit(LEVEL/STR/INT/DEX/CON)
# → WEARABLE_UNIQUE 同组去重(结婚戒指另需已婚,客户端不判)。
# `char_item.cpp:6100 EquipItem`(拿到 `item.cpp:472 FindEquipCell` 的槽位后):
# 燕尾服+骑乘禁止 → 变身中除 WEAR_ARROW 一律禁止 → `FN_check_item_sex`(性别)
# → 骑乘物品已骑乘禁止 → 攻击/施法后 1.5s 冷却(客户端不判)。
# **帝国 antiflag 在装备路径上从不检查**(只在掉落/商店显示层)——已移出装备门,
# 见 empire_warning(),供 tooltip 提示用。
# LIMIT_PCBANG / LIMIT_REAL_TIME* 不是装备门禁(item.cpp:1899 PCBANG 仅标记;
# REAL_TIME* 由 socket0/socket1 驱动到期,客户端职责只有 tooltip 倒计时)。
#
# 参考枚举 1:1 自 `REF/GameLib/ItemData.h``EItemAntiFlag` / `EItemWearableFlag` /
# `EWearPositions` / `ELimitTypes` / `EItemType` / `EArmorSubTypes` / `ECostumeSubTypes`)。
# 40250 客户端本身**不做** `CanEquipNow` 前置门(真正的仲裁在服务器)。这里按 §3.9
# `__CanShot` 的方式在客户端防御式重建一道前置门,被拦下时给出与服务器一致的拒绝码。
extends RefCounted
# --- EItemAntiFlagItemData.hclassic 排列:帝国位在 1<<9..1<<11---
const ANTIFLAG_FEMALE := 1 << 0
const ANTIFLAG_MALE := 1 << 1
const ANTIFLAG_WARRIOR := 1 << 2
const ANTIFLAG_ASSASSIN := 1 << 3
const ANTIFLAG_SURA := 1 << 4
const ANTIFLAG_SHAMAN := 1 << 5
const ANTIFLAG_EMPIRE_A := 1 << 9
const ANTIFLAG_EMPIRE_B := 1 << 10
const ANTIFLAG_EMPIRE_R := 1 << 11
# --- EItemWearableFlag ---
const WEARABLE_BODY := 1 << 0
const WEARABLE_HEAD := 1 << 1
const WEARABLE_FOOTS := 1 << 2
const WEARABLE_WRIST := 1 << 3
const WEARABLE_WEAPON := 1 << 4
const WEARABLE_NECK := 1 << 5
const WEARABLE_EAR := 1 << 6
const WEARABLE_UNIQUE := 1 << 7
const WEARABLE_SHIELD := 1 << 8
const WEARABLE_ARROW := 1 << 9
# --- EWearPositions0..10+ 新装备系统槽(GameType.h19..23---
const WEAR_BODY := 0
const WEAR_HEAD := 1
const WEAR_FOOTS := 2
const WEAR_WRIST := 3
const WEAR_WEAPON := 4
const WEAR_NECK := 5
const WEAR_EAR := 6
const WEAR_UNIQUE1 := 7
const WEAR_UNIQUE2 := 8
const WEAR_ARROW := 9
const WEAR_SHIELD := 10
const WEAR_MAX_NUM := 11
const WEAR_COSTUME_BODY := 19
const WEAR_COSTUME_HAIR := 20
const WEAR_RING1 := 21
const WEAR_RING2 := 22
const WEAR_BELT := 23
# --- ELimitTypes ---
const LIMIT_NONE := 0
const LIMIT_LEVEL := 1
const LIMIT_STR := 2
const LIMIT_DEX := 3
const LIMIT_INT := 4
const LIMIT_CON := 5
const LIMIT_PCBANG := 6
const LIMIT_REAL_TIME := 7
# --- EItemType(相关项)+ 子类 ---
const ITEM_TYPE_WEAPON := 1
const ITEM_TYPE_ARMOR := 2
const ITEM_TYPE_UNIQUE := 16
const ITEM_TYPE_TOTEM := 26
const ITEM_TYPE_COSTUME := 28
const ITEM_TYPE_DS := 29
const ITEM_TYPE_SPECIAL_DS := 30
const ITEM_TYPE_RING := 33
const ITEM_TYPE_BELT := 34
# --- EUniqueSubTypesitem_length.h:骑乘/坐骑物品,服务器 IsRideItem---
const UNIQUE_SPECIAL_RIDE := 2
const UNIQUE_SPECIAL_MOUNT_RIDE := 3
const ARMOR_BODY := 0
const ARMOR_HEAD := 1
const ARMOR_SHIELD := 2
const ARMOR_WRIST := 3
const ARMOR_FOOTS := 4
const ARMOR_NECK := 5
const ARMOR_EAR := 6
const COSTUME_BODY := 0
const COSTUME_HAIR := 1
# --- 职业 / 性别 <- racecommon length.h `MAIN_RACE_*`job = race % 4;性别见下表)---
const JOB_WARRIOR := 0
const JOB_ASSASSIN := 1
const JOB_SURA := 2
const JOB_SHAMAN := 3
const SEX_MALE := 0
const SEX_FEMALE := 1
# race 0..7 = WARRIOR_M / ASSASSIN_W / SURA_M / SHAMAN_W / WARRIOR_W / ASSASSIN_M / SURA_W / SHAMAN_M
const SEX_BY_RACE := [SEX_MALE, SEX_FEMALE, SEX_MALE, SEX_FEMALE, SEX_FEMALE, SEX_MALE, SEX_FEMALE, SEX_MALE]
const JOB_ANTIFLAG := [ANTIFLAG_WARRIOR, ANTIFLAG_ASSASSIN, ANTIFLAG_SURA, ANTIFLAG_SHAMAN]
# 帝国 1/2/3GC_EMPIRE)→ 对应 anti 位
const EMPIRE_ANTIFLAG := {1: ANTIFLAG_EMPIRE_A, 2: ANTIFLAG_EMPIRE_B, 3: ANTIFLAG_EMPIRE_R}
static func sex_of_race(race: int) -> int:
return int(SEX_BY_RACE[race]) if race >= 0 and race < SEX_BY_RACE.size() else SEX_MALE
static func job_of_race(race: int) -> int:
return (race % 4) if race >= 0 else JOB_WARRIOR
# 物品默认落在哪个 wear 槽(type / sub_type 决定)。不可穿戴 → -1。
# 戒指无穿戴表时默认 RING1;有穿戴表请用 find_equip_cellRING1 空则 1 否则 2
# 与服务器 item.cpp:472 一致)。
static func default_wear_for(item: Dictionary) -> int:
var itype := int(item.get("type", -1))
var sub := int(item.get("sub_type", -1))
match itype:
ITEM_TYPE_WEAPON:
return WEAR_WEAPON
ITEM_TYPE_COSTUME:
return WEAR_COSTUME_HAIR if sub == COSTUME_HAIR else WEAR_COSTUME_BODY
ITEM_TYPE_RING:
return WEAR_RING1
ITEM_TYPE_BELT:
return WEAR_BELT
ITEM_TYPE_ARMOR:
match sub:
ARMOR_HEAD: return WEAR_HEAD
ARMOR_SHIELD: return WEAR_SHIELD
ARMOR_WRIST: return WEAR_WRIST
ARMOR_FOOTS: return WEAR_FOOTS
ARMOR_NECK: return WEAR_NECK
ARMOR_EAR: return WEAR_EAR
_: return WEAR_BODY
return -1
static func is_equippable(item: Dictionary) -> bool:
return default_wear_for(item) >= 0
# wear 槽 → 需要的 wearable_flag 位(新装备槽 19..23 无 wearable_flag → 0)。
static func wear_to_wearable(wear: int) -> int:
match wear:
WEAR_BODY: return WEARABLE_BODY
WEAR_HEAD: return WEARABLE_HEAD
WEAR_FOOTS: return WEARABLE_FOOTS
WEAR_WRIST: return WEARABLE_WRIST
WEAR_WEAPON: return WEARABLE_WEAPON
WEAR_NECK: return WEARABLE_NECK
WEAR_EAR: return WEARABLE_EAR
WEAR_UNIQUE1, WEAR_UNIQUE2: return WEARABLE_UNIQUE
WEAR_ARROW: return WEARABLE_ARROW
WEAR_SHIELD: return WEARABLE_SHIELD
return 0
# 目标槽能否接受这件物品:新装备槽按 item type,旧 11 槽按 wearable_flag 位。
static func slot_accepts(item: Dictionary, wear: int) -> bool:
var itype := int(item.get("type", -1))
match wear:
WEAR_COSTUME_BODY, WEAR_COSTUME_HAIR:
return itype == ITEM_TYPE_COSTUME
WEAR_RING1, WEAR_RING2:
return itype == ITEM_TYPE_RING
WEAR_BELT:
return itype == ITEM_TYPE_BELT
var need := wear_to_wearable(wear)
if need == 0:
return false
return (int(item.get("wear_flags", 0)) & need) != 0
# 服务器 `item.cpp:472 CItem::FindEquipCell` 的 1:1 复刻。
# worn : 当前穿戴表 {wear_cell: vnum}(客户端来自 get_equipment())。
# else-if 优先级:BODY→HEAD→FOOTS→WRIST→WEAPON→SHIELD→NECK→EAR→ARROW
# RING→RING1 空则 RING1 否则 RING2UNIQUE→UNIQUE1 空则 UNIQUE1 否则 UNIQUE2。
# 龙魂石(DS/SPECIAL_DS)服务器返回 WEAR_MAX_NUM+sub,但客户端走
# dragon_soul_ui 的专用协议路径,这里返回 -1 不进装备门。
static func find_equip_cell(item: Dictionary, worn: Dictionary = {}) -> int:
var itype := int(item.get("type", -1))
var sub := int(item.get("sub_type", -1))
var flags := int(item.get("wear_flags", 0))
if (flags == 0 or itype == ITEM_TYPE_TOTEM) and itype != ITEM_TYPE_COSTUME \
and itype != ITEM_TYPE_DS and itype != ITEM_TYPE_SPECIAL_DS \
and itype != ITEM_TYPE_RING and itype != ITEM_TYPE_BELT:
return -1
if itype == ITEM_TYPE_DS or itype == ITEM_TYPE_SPECIAL_DS:
return -1 # 见上:客户端 DS 专用路径
if itype == ITEM_TYPE_COSTUME:
return WEAR_COSTUME_HAIR if sub == COSTUME_HAIR else WEAR_COSTUME_BODY
if itype == ITEM_TYPE_RING:
return WEAR_RING2 if worn.has(WEAR_RING1) else WEAR_RING1
if itype == ITEM_TYPE_BELT:
return WEAR_BELT
if flags & WEARABLE_BODY:
return WEAR_BODY
if flags & WEARABLE_HEAD:
return WEAR_HEAD
if flags & WEARABLE_FOOTS:
return WEAR_FOOTS
if flags & WEARABLE_WRIST:
return WEAR_WRIST
if flags & WEARABLE_WEAPON:
return WEAR_WEAPON
if flags & WEARABLE_SHIELD:
return WEAR_SHIELD
if flags & WEARABLE_NECK:
return WEAR_NECK
if flags & WEARABLE_EAR:
return WEAR_EAR
if flags & WEARABLE_ARROW:
return WEAR_ARROW
if flags & WEARABLE_UNIQUE:
return WEAR_UNIQUE2 if worn.has(WEAR_UNIQUE1) else WEAR_UNIQUE1
return -1
# 骑乘 / 坐骑物品(服务器 `item.cpp:1695 IsRideItem`UNIQUE + RIDE / MOUNT_RIDE)。
static func is_ride_item(item: Dictionary) -> bool:
return int(item.get("type", -1)) == ITEM_TYPE_UNIQUE \
and (int(item.get("sub_type", -1)) == UNIQUE_SPECIAL_RIDE \
or int(item.get("sub_type", -1)) == UNIQUE_SPECIAL_MOUNT_RIDE)
# 帝国 antiflag:服务器装备路径不检查(显示层语义)。返回冲突的 anti 位(0 = 无冲突),
# tooltip 用它给出提示,不作为装备门。
static func empire_warning(item: Dictionary, empire: int) -> int:
var bit := int(EMPIRE_ANTIFLAG.get(empire, 0))
if bit == 0:
return 0
return int(item.get("anti_flags", 0)) & bit
static func _fail(code: String, need := 0) -> Dictionary:
return {"ok": false, "code": code, "need": need}
# 主校验(顺序 1:1 = 服务器 CanEquipNow → FindEquipCell → EquipItem 后置检查)。
# item : proto 字典 —— {type, sub_type, anti_flags, wear_flags, limits:[{type,value}], vnum?}
# wear : 目标槽;传 -1 用 find_equip_cell(有 ctx.worn 时含 RING/UNIQUE 占用翻转)
# ctx : {race, sex, job, empire, level, st, dx, ht, iq,
# worn: {cell: vnum}, # 当前穿戴(RING/UNIQUE 占用判断)
# polymorphed: bool, # 变身中(POINT_POLYMORPH / affect 220
# riding: bool} # 骑乘中
# —— sex/job 缺省时由 race 推;属性 st/dx/ht/iq 缺省或 < 0 = 不判
# 返回 {ok: bool, code: String, need: int}。need = 失败 limit 的要求值。
static func can_equip(item: Dictionary, wear: int, ctx: Dictionary) -> Dictionary:
if item.is_empty():
return _fail("NO_ITEM")
var anti := int(item.get("anti_flags", 0))
var race := int(ctx.get("race", 0))
var sex := int(ctx.get("sex", sex_of_race(race)))
var job := int(ctx.get("job", job_of_race(race)))
var worn: Dictionary = ctx.get("worn", {})
# ---- CanEquipNowchar_item.cpp:7462----
# 1) anti-flag:职业(服务器只查 JOB;性别在拿到槽位后查,见 5))
if job >= 0 and job < JOB_ANTIFLAG.size() and (anti & int(JOB_ANTIFLAG[job])):
return _fail("ANTI_JOB")
# 2) limit(等级 / 属性)—— 逐条 GetLimitPCBANG / REAL_TIME* 不是门禁(见文件头)
for lim in item.get("limits", []):
var t := int(lim.get("type", 0))
var v := int(lim.get("value", 0))
if v <= 0:
continue
match t:
LIMIT_LEVEL:
if int(ctx.get("level", 0)) < v:
return _fail("LIMIT_LEVEL", v)
LIMIT_STR:
if _stat(ctx, "st") >= 0 and _stat(ctx, "st") < v:
return _fail("LIMIT_STR", v)
LIMIT_DEX:
if _stat(ctx, "dx") >= 0 and _stat(ctx, "dx") < v:
return _fail("LIMIT_DEX", v)
LIMIT_INT:
if _stat(ctx, "iq") >= 0 and _stat(ctx, "iq") < v:
return _fail("LIMIT_INT", v)
LIMIT_CON:
if _stat(ctx, "ht") >= 0 and _stat(ctx, "ht") < v:
return _fail("LIMIT_CON", v)
_:
pass
# 3) WEARABLE_UNIQUE 同组去重:UNIQUE1/UNIQUE2 已有同组物品则拒绝。
# 服务器用 IsSameSpecialGroup;穿戴表(worn: cell→vnum)拿不到被占物品的
# proto,这里退化为同 vnum 判定——只拦下确定的重复,其余交服务器权威。
# 结婚戒指另需已婚,客户端不判。
if int(item.get("wear_flags", 0)) & WEARABLE_UNIQUE and item.get("vnum", 0) is int:
for cell in [WEAR_UNIQUE1, WEAR_UNIQUE2]:
if int(worn.get(cell, 0)) == int(item.get("vnum", 0)) \
and int(item.get("vnum", 0)) != 0:
return _fail("UNIQUE_DUP")
# ---- FindEquipCellitem.cpp:472----
var target := find_equip_cell(item, worn) if wear < 0 else wear
if target < 0:
return _fail("NOT_EQUIPPABLE")
# 显式指定槽时校验物品与槽匹配(服务器对显式 cell 不做这层校验,属客户端防御)。
if wear >= 0 and not slot_accepts(item, target):
return _fail("NOT_WEARABLE_HERE")
# ---- EquipItem 后置检查(char_item.cpp:6122+----
# 4) 燕尾服(vnum 11901..11904)骑乘中不可穿
if target == WEAR_BODY and bool(ctx.get("riding", false)):
var vnum := int(item.get("vnum", 0))
if vnum >= 11901 and vnum <= 11904:
return _fail("RIDE_TUXEDO")
# 5) 变身中除 WEAR_ARROW 一律禁止
if bool(ctx.get("polymorphed", false)) and target != WEAR_ARROW:
return _fail("POLYMORPH")
# 6) FN_check_item_sexchar_item.cpp:189
if sex == SEX_FEMALE and (anti & ANTIFLAG_FEMALE):
return _fail("ANTI_SEX")
if sex == SEX_MALE and (anti & ANTIFLAG_MALE):
return _fail("ANTI_SEX")
# 7) 骑乘物品已骑乘禁止(item.cpp:1695 IsRideItem
if bool(ctx.get("riding", false)) and is_ride_item(item):
return _fail("RIDE_ITEM")
# 攻击/施法后 1.5s 冷却(char_item.cpp:6143)客户端不判,交服务器。
return {"ok": true, "code": "", "need": 0}
static func _stat(ctx: Dictionary, key: String) -> int:
return int(ctx.get(key, -1))