- 装备校验: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
332 lines
13 KiB
GDScript
332 lines
13 KiB
GDScript
# 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
|
||
|
||
# --- EItemAntiFlag(ItemData.h,classic 排列:帝国位在 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
|
||
|
||
# --- EWearPositions(0..10)+ 新装备系统槽(GameType.h,19..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
|
||
|
||
# --- EUniqueSubTypes(item_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
|
||
|
||
# --- 职业 / 性别 <- race(common 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/3(GC_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_cell(RING1 空则 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 否则 RING2;UNIQUE→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", {})
|
||
|
||
# ---- CanEquipNow(char_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(等级 / 属性)—— 逐条 GetLimit;PCBANG / 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")
|
||
|
||
# ---- FindEquipCell(item.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_sex(char_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))
|