装备校验链/快捷栏/公会/好友情侣/交易仓库五项差距修复(增量 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
This commit is contained in:
+130
-33
@@ -1,13 +1,20 @@
|
||||
# equip_rules.gd —— 装备穿戴合法性校验(纯函数校验器)。
|
||||
#
|
||||
# 语义顺序对齐服务器 `char_item.cpp::CanEquipNow`:
|
||||
# antiflag(性别 / 职业 / 帝国) → wearable_flag 与目标 wear 槽匹配 → limit(等级 / 属性)。
|
||||
# 语义顺序 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` 前置门(`PythonItemModule` 只暴露 `IsWearableFlag` /
|
||||
# `GetLimit`,真正的仲裁在服务器)。这里按 §3.9 `__CanShot` 的方式在客户端防御式重建一道前置
|
||||
# 门——精确的 tie-break 顺序、PCBANG / REAL_TIME 类 limit、已穿同槽替换等留待接服务器比对(seam)。
|
||||
# 40250 客户端本身**不做** `CanEquipNow` 前置门(真正的仲裁在服务器)。这里按 §3.9
|
||||
# `__CanShot` 的方式在客户端防御式重建一道前置门,被拦下时给出与服务器一致的拒绝码。
|
||||
extends RefCounted
|
||||
|
||||
# --- EItemAntiFlag(ItemData.h,classic 排列:帝国位在 1<<9..1<<11)---
|
||||
@@ -65,10 +72,18 @@ 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
|
||||
@@ -103,6 +118,8 @@ static func job_of_race(race: int) -> int:
|
||||
|
||||
|
||||
# 物品默认落在哪个 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))
|
||||
@@ -163,47 +180,91 @@ static func slot_accepts(item: Dictionary, wear: int) -> bool:
|
||||
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}
|
||||
|
||||
|
||||
# 主校验。
|
||||
# item : proto 字典 —— {type, sub_type, anti_flags, wear_flags, limits:[{type,value}]}
|
||||
# wear : 目标 EWearPositions(或新装备槽 19..23);传 -1 用 default_wear_for(item)
|
||||
# ctx : {race, sex, job, empire, level, st, dx, ht, iq}
|
||||
# —— sex/job 缺省时由 race 推;属性 st/dx/ht/iq 缺省或 < 0 = 不判(seam)
|
||||
# 返回 {ok: bool, code: String, need: int}。code 见下方常量注释;need = 失败 limit 的要求值。
|
||||
# 主校验(顺序 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 target := wear if wear >= 0 else default_wear_for(item)
|
||||
if target < 0:
|
||||
return _fail("NOT_EQUIPPABLE")
|
||||
|
||||
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 empire := int(ctx.get("empire", 0))
|
||||
var worn: Dictionary = ctx.get("worn", {})
|
||||
|
||||
# 1) anti-flag:性别
|
||||
if sex == SEX_FEMALE and (anti & ANTIFLAG_FEMALE):
|
||||
return _fail("ANTI_SEX")
|
||||
if sex == SEX_MALE and (anti & ANTIFLAG_MALE):
|
||||
return _fail("ANTI_SEX")
|
||||
# 2) anti-flag:职业
|
||||
# ---- 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")
|
||||
# 3) anti-flag:帝国
|
||||
var emp_bit := int(EMPIRE_ANTIFLAG.get(empire, 0))
|
||||
if emp_bit != 0 and (anti & emp_bit):
|
||||
return _fail("ANTI_EMPIRE")
|
||||
|
||||
# 4) wearable_flag 与目标槽匹配
|
||||
if not slot_accepts(item, target):
|
||||
return _fail("NOT_WEARABLE_HERE")
|
||||
|
||||
# 5) limit(等级 / 属性)—— 逐条 GetLimit
|
||||
# 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))
|
||||
@@ -226,7 +287,43 @@ static func can_equip(item: Dictionary, wear: int, ctx: Dictionary) -> Dictionar
|
||||
if _stat(ctx, "ht") >= 0 and _stat(ctx, "ht") < v:
|
||||
return _fail("LIMIT_CON", v)
|
||||
_:
|
||||
pass # PCBANG / REAL_TIME* → 服务器判(seam)
|
||||
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}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user