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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b9af78ef64
commit
8d01a7a754
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 end<ACCESSORY_SOCKET_MAX_SIZE
|
||||
if useType=="USE_PUT_INTO_ACCESSORY_SOCKET":return srcItemVNum==MaterialVnum(dstItemVNum) and cur<end
|
||||
# 71151 / 76023 / 71152 / 76024 are for weapons / armor
|
||||
if srcItemVNum in (71151,76023,71152,76024):return False
|
||||
count=AttrCount(dstSlotPos)
|
||||
if useType=="USE_CHANGE_ATTRIBUTE":return count>0
|
||||
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";
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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부터임.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user