# 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))