From 8d01a7a7541d54123ab8f9475cd95848329e2fe4 Mon Sep 17 00:00:00 2001 From: shenlei Date: Fri, 16 Oct 2026 14:40:40 +0900 Subject: [PATCH] AVA2 unlock P8: new rings client (mt_rings, ring slots, item import) ITEM_TYPE_RINGS / c_Equipment_Rings1/2, mt_rings adds the two slots beside the shoes, the accessory tooltip and the drill / jewel / switcher / adder drop targets; make_items imports the four +0..+9 ring chains. Co-Authored-By: Claude Opus 5.5 --- src/platform/ScriptLib/PythonBoot.cpp | 2 + src/platform/ScriptLib/RingsScript.inc | 139 ++++++++++++++++++++ src/port/GameLib/ItemData.h | 2 + src/port/UserInterface/GameType.h | 2 + src/port/UserInterface/PythonItemModule.cpp | 3 + tools/server/ava2-items/make_items.py | 15 ++- tools/server/ava2-items/tests/test_items.py | 8 +- 7 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 src/platform/ScriptLib/RingsScript.inc diff --git a/src/platform/ScriptLib/PythonBoot.cpp b/src/platform/ScriptLib/PythonBoot.cpp index cc7aeb5a..053a456f 100644 --- a/src/platform/ScriptLib/PythonBoot.cpp +++ b/src/platform/ScriptLib/PythonBoot.cpp @@ -1020,6 +1020,7 @@ namespace PythonBoot #include "MountScript.inc" #include "CostumeAttrScript.inc" #include "PendantScript.inc" +#include "RingsScript.inc" static void install_script_module(const char* c_szModule, const char* c_szScript) { @@ -1108,6 +1109,7 @@ bool RunMainScript(const char* lpCmdLine, std::string* error) install_script_module("mt_mount", c_szMountScript); install_script_module("mt_costume_attr", c_szCostumeAttrScript); install_script_module("mt_pendant", c_szPendantScript); + install_script_module("mt_rings", c_szRingsScript); if (g_touch_input) install_script_module("mt_mobile_hud", c_szMobileHUDScript); } diff --git a/src/platform/ScriptLib/RingsScript.inc b/src/platform/ScriptLib/RingsScript.inc new file mode 100644 index 00000000..e5aacfc0 --- /dev/null +++ b/src/platform/ScriptLib/RingsScript.inc @@ -0,0 +1,139 @@ +// PORT: AVA2 new rings (ENABLE_NEW_RINGS), as module mt_rings. Rings are ITEM_TYPE_RINGS (36) worn in +// c_Equipment_Rings1/2 (server WEAR_RINGS_1/2, metin2-server port_rings.cpp). They are socket accessories with +// their own attribute pool, reached through the 40250 switchers / adders. This script adds the two slots to the +// inventory's equipment panel (the free spots beside the shoes on equipment_base, where AVA2 has them too), gives +// rings the accessory tooltip of AVA2 uitooltip.py (limits, applies, attributes, jewel sockets) and marks them as +// drop targets of the drill / jewel / switcher / adder items the server accepts. +static const char c_szRingsScript[] = R"PY(# -*- coding: utf-8 -*- +import item +import player +import uiInventory +import uiToolTip +import ui + +# EquipmentSlot coordinates (the shoes are at 39,145). +SLOTS=((item.EQUIPMENT_RINGS1,(2,145)),(item.EQUIPMENT_RINGS2,(75,145))) +# AVA2 CItem::CanPutInto: ring family (vnum / 10 * 10) -> jewel. +JEWEL={19220:50634,19500:50635,19520:50636,19540:50637,19560:50638} +ACCESSORY_SOCKET_MAX_SIZE=3 + + +def IsRing(vnum): + if not vnum:return False + item.SelectItem(vnum) + return item.GetItemType()==item.ITEM_TYPE_RINGS + + +def MaterialVnum(vnum): + return JEWEL.get(vnum/10*10,0) + + +def FindChild(node,name): + if node.get("name")==name:return node + for child in node.get("children",()): + found=FindChild(child,name) + if found:return found + return None + + +def PatchInventoryWindow(window): + base=FindChild(window,"Equipment_Base");slot=FindChild(window,"EquipmentSlot") + if not base or not slot:return + frames=[];cells=[] + for i,(index,(x,y)) in enumerate(SLOTS): + frames.append({"name":"RingSlotBase%d"%i,"type":"image","x":slot["x"]+x,"y":slot["y"]+y,"image":"d:/ymir work/ui/public/Slot_Base.sub"}) + cells.append({"index":index,"x":x,"y":y,"width":32,"height":32}) + base["children"]=tuple(frames)+tuple(base.get("children",())) + slot["slot"]=tuple(slot["slot"])+tuple(cells) + + +_execfile=ui.execfile + +def ExecFile(fileName,dict): + _execfile(fileName,dict) + name=fileName.replace("\\","/").split("/")[-1].lower() + if name=="inventorywindow.py" and "window" in dict:PatchInventoryWindow(dict["window"]) + +ui.execfile=ExecFile + + +_refreshEquip=uiInventory.InventoryWindow.RefreshEquipSlotWindow + +def RefreshEquipSlotWindow(self): + _refreshEquip(self) + for index,_ in SLOTS:self.wndEquip.SetItemSlot(index,player.GetItemIndex(index),0) + self.wndEquip.RefreshSlot() + +uiInventory.InventoryWindow.RefreshEquipSlotWindow=RefreshEquipSlotWindow + + +def AttrCount(slot): + return len([1 for i in xrange(player.ATTRIBUTE_SLOT_MAX_NUM) if player.GetItemAttribute(slot,i)[0]]) + + +# 40250's __CanPutAccessorySocket / __CanAddAccessorySocket / __CanChangeItemAttrList / __CanAddItemAttr only take +# weapons, armor and the armor accessories; everything else (refine scrolls, ...) keeps the 40250 checks. +RING_USE_TYPES=("USE_ADD_ACCESSORY_SOCKET","USE_PUT_INTO_ACCESSORY_SOCKET","USE_CHANGE_ATTRIBUTE","USE_ADD_ATTRIBUTE","USE_ADD_ATTRIBUTE2") + + +def CanUseOnRing(srcItemVNum,dstSlotPos,useType): + dstItemVNum=player.GetItemIndex(dstSlotPos) + # the server refuses these items on a worn item ("Fix" in CHARACTER::UseItemEx) + if player.IsEquipmentSlot(dstSlotPos):return False + cur=player.GetItemMetinSocket(dstSlotPos,0);end=player.GetItemMetinSocket(dstSlotPos,1) + if useType=="USE_ADD_ACCESSORY_SOCKET":return end0 + if useType=="USE_ADD_ATTRIBUTE":return count<4 + if useType=="USE_ADD_ATTRIBUTE2":return count==4 + return False + + +_canUse=uiInventory.InventoryWindow._InventoryWindow__CanUseSrcItemToDstItem + +def CanUseSrcItemToDstItem(self,srcItemVNum,srcSlotPos,dstSlotPos): + useType=item.GetUseType(srcItemVNum) + if srcSlotPos!=dstSlotPos and useType in RING_USE_TYPES and IsRing(player.GetItemIndex(dstSlotPos)): + return CanUseOnRing(srcItemVNum,dstSlotPos,useType) + return _canUse(self,srcItemVNum,srcSlotPos,dstSlotPos) + +uiInventory.InventoryWindow._InventoryWindow__CanUseSrcItemToDstItem=CanUseSrcItemToDstItem + + +# --- tooltip (AVA2 uitooltip.py ITEM_TYPE_RINGS branch, without rarity) --------------------------------------- +# A ring falls into the generic branch of ItemToolTip.AddItemData, which only appends the limits; the rest of the +# accessory tooltip follows them. +Tip=uiToolTip.ItemToolTip +_addItemData=Tip.AddItemData +_appendLimit=Tip._ItemToolTip__AppendLimitInformation + +def AddItemData(self,itemVnum,metinSlot,attrSlot=0,flags=0,unbindTime=0): + self.mtRing=(itemVnum,metinSlot,attrSlot) if IsRing(itemVnum) else None + try: + return _addItemData(self,itemVnum,metinSlot,attrSlot,flags,unbindTime) + finally: + self.mtRing=None + + +def AppendLimitInformation(self): + _appendLimit(self) + state=getattr(self,'mtRing',None) + if not state:return + self.mtRing=None + itemVnum,metinSlot,attrSlot=state + item.SelectItem(itemVnum) + self._ItemToolTip__AppendAffectInformation() + self._ItemToolTip__AppendAttributeInformation(attrSlot) + item.SelectItem(itemVnum) + self.AppendWearableInformation() + if metinSlot: + item.SelectItem(itemVnum) + self._ItemToolTip__AppendAccessoryMetinSlotInfo(metinSlot,MaterialVnum(itemVnum)) + item.SelectItem(itemVnum) + +Tip.AddItemData=AddItemData +Tip._ItemToolTip__AppendLimitInformation=AppendLimitInformation +)PY"; diff --git a/src/port/GameLib/ItemData.h b/src/port/GameLib/ItemData.h index cfdf2f32..8f1cd315 100644 --- a/src/port/GameLib/ItemData.h +++ b/src/port/GameLib/ItemData.h @@ -56,6 +56,8 @@ class CItemData ITEM_TYPE_SECONDARY_COIN, //32 명도전. ITEM_TYPE_RING, //33 반지 (유니크 슬롯이 아닌 순수 반지 슬롯) ITEM_TYPE_BELT, //34 벨트 + ITEM_TYPE_SHINING, //35 PORT: AVA2 shining (not ported, keeps the AVA2 numbering) + ITEM_TYPE_RINGS, //36 PORT: AVA2 new rings, worn in c_Equipment_Rings1/2 ITEM_TYPE_MAX_NUM, }; diff --git a/src/port/UserInterface/GameType.h b/src/port/UserInterface/GameType.h index 0cee29b0..61a1f0f7 100644 --- a/src/port/UserInterface/GameType.h +++ b/src/port/UserInterface/GameType.h @@ -109,6 +109,8 @@ enum EDragonSoulStepTypes const DWORD c_Costume_Slot_MountSkin = c_Equipment_Start + 26; // PORT: AVA2 mount skin, server WEAR_COSTUME_MOUNT_SKIN #endif const DWORD c_Equipment_Pendant = c_Equipment_Start + 27; // PORT: AVA2 talisman, server WEAR_PENDANT + const DWORD c_Equipment_Rings1 = c_Equipment_Start + 28; // PORT: AVA2 new rings, server WEAR_RINGS_1/2 + const DWORD c_Equipment_Rings2 = c_Equipment_Start + 29; // [주의] 숫자(32) 하드코딩 주의. 현재 서버에서 용혼석 슬롯은 32부터임. diff --git a/src/port/UserInterface/PythonItemModule.cpp b/src/port/UserInterface/PythonItemModule.cpp index a543c2ef..23e7b4ab 100644 --- a/src/port/UserInterface/PythonItemModule.cpp +++ b/src/port/UserInterface/PythonItemModule.cpp @@ -762,6 +762,9 @@ void initItem() PyModule_AddIntConstant(poModule, "ARMOR_EAR", CItemData::ARMOR_EAR); PyModule_AddIntConstant(poModule, "ARMOR_PENDANT", CItemData::ARMOR_PENDANT); // PORT: AVA2 talisman PyModule_AddIntConstant(poModule, "EQUIPMENT_PENDANT", c_Equipment_Pendant); + PyModule_AddIntConstant(poModule, "ITEM_TYPE_RINGS", CItemData::ITEM_TYPE_RINGS); // PORT: AVA2 new rings + PyModule_AddIntConstant(poModule, "EQUIPMENT_RINGS1", c_Equipment_Rings1); + PyModule_AddIntConstant(poModule, "EQUIPMENT_RINGS2", c_Equipment_Rings2); PyModule_AddIntConstant(poModule, "ITEM_APPLY_MAX_NUM", CItemData::ITEM_APPLY_MAX_NUM); PyModule_AddIntConstant(poModule, "ITEM_SOCKET_MAX_NUM", CItemData::ITEM_SOCKET_MAX_NUM); diff --git a/tools/server/ava2-items/make_items.py b/tools/server/ava2-items/make_items.py index 7af2ddc1..c1a71f86 100644 --- a/tools/server/ava2-items/make_items.py +++ b/tools/server/ava2-items/make_items.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 """Stage the AVA2 drop items that the 203 server lacks as ordinary 40250 items (server rows + client patch pack). -Only types/subtypes/flags the 40250 server understands survive; items of AVA2-only systems (pets, pendants, -new rings, weapon/sash-skin/effect costume enchants, new skill books) become plain ITEM_NONE items until those -systems are ported. Also writes mobs-server.tsv (MOUNT_MOBS) for install_items.py. Usage: see build_zh_locale.build_ava2_items and --help. +Only types/subtypes/flags the 40250 server understands (plus the ported AVA2 systems: sash, mount, costume +enchants, talismans, new rings) survive; items of the AVA2-only systems not ported yet (pets, weapon/sash-skin/effect +costume enchants, new skill books) become plain ITEM_NONE items. Also writes mobs-server.tsv (MOUNT_MOBS) for install_items.py. Usage: see build_zh_locale.build_ava2_items and --help. """ import argparse,json,re,shutil,subprocess from pathlib import Path @@ -14,6 +14,8 @@ DRAGON={21210:'暗龙铠甲',21230:'风龙铠甲',21250:'魔龙铠甲',21270:' # Talismans (P7, server port_pendant.cpp): +0..+200 per element, refine sets 440-499 (../pendant/refine.sql). PENDANT={9600:'火之护符',9830:'冰之护符',10060:'土之护符',10290:'暗之护符',10520:'风之护符',10750:'雷之护符'} PENDANT_STEPS=201 +# New rings (P8, server port_rings.cpp): +0..+9 per family, 203's refine sets 46-54. +RINGS={19500:'红宝石戒指',19520:'石榴石戒指',19540:'祖母绿戒指',19560:'蓝宝石戒指'} # Talisman attribute items: 71302 / 53996 reroll, 71301 / 53997 add (53996 / 53997 are generic on AVA2). PENDANT_SCROLLS={53996,53997,71301,71302} # Models of the armor shapes the 40250 race msm lack (registered by the client's mt_shapes script): shape -> @@ -21,9 +23,10 @@ PENDANT_SCROLLS={53996,53997,71301,71302} ARMOR_SHAPES={32:'%s_7_1'} RACE_PATHS={'warrior':('pc','pc2'),'assassin':('pc2','pc'),'sura':('pc','pc2'),'shaman':('pc2','pc')} # (male, female) # Kept with their 40250 meaning; everything else becomes ITEM_NONE (see module doc). -KEEP_TYPES={'ITEM_MATERIAL','ITEM_QUEST','ITEM_GIFTBOX','ITEM_NONE','ITEM_TREASURE_BOX','ITEM_TREASURE_KEY'} +KEEP_TYPES={'ITEM_MATERIAL','ITEM_QUEST','ITEM_GIFTBOX','ITEM_NONE','ITEM_TREASURE_BOX','ITEM_TREASURE_KEY','ITEM_NEW_RINGS'} KEEP_SUBTYPES={('ITEM_WEAPON',s) for s in ('WEAPON_SWORD','WEAPON_DAGGER','WEAPON_BOW','WEAPON_TWO_HANDED','WEAPON_BELL','WEAPON_FAN')}|{('ITEM_ARMOR','ARMOR_FOOTS'),('ITEM_ARMOR','ARMOR_BODY'),('ITEM_ARMOR','ARMOR_PENDANT'),('ITEM_METIN','METIN_NORMAL')}|{('ITEM_COSTUME',s) for s in ('COSTUME_SASH','COSTUME_MOUNT','COSTUME_MOUNT_SKIN')} -DESC={'ITEM_METIN':'可镶嵌到武器的空孔中。','ITEM_MATERIAL':'制作与升级材料。','ITEM_QUEST':'任务物品。','ITEM_GIFTBOX':'打开后可获得物品。','ITEM_TREASURE_BOX':'需要对应的钥匙才能打开。','ITEM_TREASURE_KEY':'可以打开对应的宝箱。','ITEM_WEAPON':'','ITEM_ARMOR':'','ITEM_COSTUME':'披风。可在披风工匠处合成升级,或吸收武器、铠甲的部分属性。'} +DESC={'ITEM_METIN':'可镶嵌到武器的空孔中。','ITEM_MATERIAL':'制作与升级材料。','ITEM_QUEST':'任务物品。','ITEM_GIFTBOX':'打开后可获得物品。','ITEM_TREASURE_BOX':'需要对应的钥匙才能打开。','ITEM_TREASURE_KEY':'可以打开对应的宝箱。','ITEM_WEAPON':'','ITEM_ARMOR':'','ITEM_COSTUME':'披风。可在披风工匠处合成升级,或吸收武器、铠甲的部分属性。', + 'ITEM_NEW_RINGS':'戒指。装备在两个戒指栏之一。可用钻头开孔(最多 3 孔)并镶嵌对应的宝石,可用转换道具重洗属性、用追加道具增加属性。'} # Sash system (metin2-server docs/FEATURE-AVA2-UNLOCK.md P4): effects registered by the client's mt_sash script # (files they reference are staged too), the window images, and item_scale.txt (model scale per job/sex; a row # covers vnum..vnum+4). @@ -75,6 +78,8 @@ def names(): for k in range(10):result[base+k]='%s+%d'%(n,k) for base,n in PENDANT.items(): for k in range(PENDANT_STEPS):result[base+k]='%s+%d'%(n,k) + for base,n in RINGS.items(): + for k in range(10):result[base+k]='%s+%d'%(n,k) return result def convert(cols,E,imported): diff --git a/tools/server/ava2-items/tests/test_items.py b/tools/server/ava2-items/tests/test_items.py index cd99bfdb..6fadefb0 100644 --- a/tools/server/ava2-items/tests/test_items.py +++ b/tools/server/ava2-items/tests/test_items.py @@ -74,6 +74,12 @@ class ConvertTests(unittest.TestCase): self.assertTrue(kept);self.assertEqual(s[2:4],['ITEM_USE',st]) s,c,kept=m.convert(row(71303,'ITEM_USE','USE_ADD_ATTRIBUTE'),self.E,set());self.assertFalse(kept) n=m.names();self.assertEqual([n[v] for v in (9600,9800,10950)],['火之护符+0','火之护符+200','雷之护符+200']) + def test_new_rings_kept_with_refine_chain(self): + s,c,kept=m.convert(row(19500,'ITEM_NEW_RINGS','0', + applies=('APPLY_INT','1','APPLY_RESIST_WARRIOR','1','APPLY_NONE','0'),refine=('19501','46')),self.E,{19500,19501}) + self.assertTrue(kept);self.assertEqual(s[2:4],['ITEM_NEW_RINGS','0']);self.assertEqual(c[1],36) + self.assertEqual(s[11:13],['19501','46']);self.assertEqual(s[18:22],['APPLY_INT','1','APPLY_RESIST_WARRIOR','1']) + n=m.names();self.assertEqual([n[v] for v in (19500,19509,19569)],['红宝石戒指+0','红宝石戒指+9','蓝宝石戒指+9']) def test_install_items_with_mobs(self): import subprocess,tempfile with tempfile.TemporaryDirectory() as d: @@ -102,7 +108,7 @@ class ConvertTests(unittest.TestCase): with self.assertRaises(SystemExit):m.effect_files(files) def test_names_cover_ranges(self): - n=m.names();self.assertEqual(len(n),514+6*200);self.assertEqual(n[54100],'武器外观强化石') + n=m.names();self.assertEqual(len(n),514+6*200+4*9);self.assertEqual(n[54100],'武器外观强化石') self.assertEqual([n[v] for v in (21210,21219,21279)],['暗龙铠甲+0','暗龙铠甲+9','神龙铠甲+9']) self.assertTrue(all(len(v.encode('gbk'))<=24 for v in n.values()))