Author SHA1 Message Date
shenleiandClaude Opus 5.5 8d01a7a754 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>
2026-10-16 14:40:40 +09:00
shenleiandClaude Opus 5.5 b9af78ef64 AVA2 unlock P7: talismans (护符) and their attribute items
- ARMOR_PENDANT / APPLY_ENCHANT_* 123-128 / c_Equipment_Pendant (server WEAR_PENDANT 27) constants
- mt_pendant: equipment slot under the earring, drop targets for 71302/53996 (reroll) and 71301/53997
  (add, max 5), MTPENDANT messages; enchant apply texts in mt_applies
- switchbot ignores talismans and talisman rerolls
- make_items keeps the six +0..+200 chains (1206 items) and the four attribute items
- tools/server/pendant: refine sets 440-499 (AVA2 rows) + install.sh

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 14:26:12 +09:00
shenleiandClaude Opus 5.5 a1b63c0afb AVA2 unlock P6: costume enchants (client)
Keep 54001-54078 (body/hair) and 54124-54174 (mount skin) enchants as
USE_CHANGE_ATTRIBUTE; mt_costume_attr marks matching costumes as drop
targets, shows the attribute in the tooltip and renders MTCATTR; the
switchbot no longer lists enchants as scrolls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 14:07:07 +09:00
shenleiandClaude Opus 5.5 1412806ee9 AVA2 unlock P5: mount costume / skin (COSTUME_MOUNT 2, COSTUME_MOUNT_SKIN 4, slots 25/26, mt_mount, npc_mount races)
Keeps 71502/71505/71508/71511 and skins 71234/71254 as costumes, stages the
npc_mount models of races 32012/32028, writes mobs-server.tsv and lets
install_items.py install mob rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:58:16 +09:00
shenleiandClaude Opus 5.5 4d7306792e AVA2 unlock P4: sash (mt_sash windows/slot/tooltips, sash module, CHR_EQUIPPART_SASH, item_scale)
Client side of metin2-server FEATURE-AVA2-UNLOCK P4: HEADER_CG_SASH 211 /
HEADER_GC_SASH 215, a fifth awPart WORD in the character packets (old
servers break), the sash part attached by scale per job/sex, the combine
and absorb windows in script mt_sash, and quest tools/server/sash on 20016.
The ava2items pack keeps the four grade-4 sashes plus their effects,
acce window images and item_scale.txt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:45:02 +09:00
shenleiandClaude Opus 5.5 0d2a9ebe36 AVA2 unlock P3: dragon armor 21210-21279 (+0..+9) with armor shape 32
Keep ARMOR_BODY rows, import the whole refine chain, ship AVA2's <race>_7_1
models and register shape 32 after each race msm load (mt_shapes): the race
msm are read by bare name from the root pack and cannot be overridden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 11:13:41 +09:00
shenleiandClaude Opus 5.5 178fcf48fe AVA2 unlock P1/P2: treasure boxes/keys and stone/Boss stones usable
Keep ITEM_TREASURE_BOX/KEY and METIN_NORMAL rows; APPLY_ATTBONUS_STONE 139 /
APPLY_ATTBONUS_BOSS 140 in ItemData.h, item module and tooltip (mt_applies).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 11:05:20 +09:00
51 changed files with 2332 additions and 43 deletions
+28
View File
@@ -0,0 +1,28 @@
// PORT: item bonuses added after 40250 (AVA2 numbering, server common/length.h). The packed uitooltip.py
// only knows 40250's applies; add the new ones to its AFFECT_DICT so item and socket tooltips show them.
static const char c_szAppliesScript[] = R"PY(# -*- coding: utf-8 -*-
import item
import player
import uiToolTip
L = player.AutoHuntLocalize
TEXTS = (
("APPLY_ATTBONUS_STONE", "对陨石伤害 +%d%%"),
("APPLY_ATTBONUS_BOSS", "对 Boss 伤害 +%d%%"),
("APPLY_ENCHANT_ELEC", "对雷属性怪物伤害 +%d%%"),
("APPLY_ENCHANT_FIRE", "对火属性怪物伤害 +%d%%"),
("APPLY_ENCHANT_ICE", "对冰属性怪物伤害 +%d%%"),
("APPLY_ENCHANT_WIND", "对风属性怪物伤害 +%d%%"),
("APPLY_ENCHANT_EARTH", "对土属性怪物伤害 +%d%%"),
("APPLY_ENCHANT_DARK", "对暗属性怪物伤害 +%d%%"),
)
def Text(text):
return lambda value: L(text % value)
for name, text in TEXTS:
if hasattr(item, name):
uiToolTip.ItemToolTip.AFFECT_DICT[getattr(item, name)] = Text(text)
)PY";
@@ -0,0 +1,34 @@
// PORT: armor shapes added after 40250 (AVA2's dragon armor 21210+, item value3 = 32). The race .msm files are
// read by bare name, which CEterPackManager always serves from the root pack, so a patch pack cannot replace
// them; register the extra shapes after each LoadLocalRaceData instead, as the AVA2 msm groups would. Models
// ship in the ava2items pack (metin2-client tools/server/ava2-items, ARMOR_SHAPES / RACE_PATHS).
static const char c_szArmorShapesScript[] = R"PY(# -*- coding: utf-8 -*-
import chrmgr
SHAPES = {32: "%s_7_1"}
# ShapeData PathName of each race msm (male, female).
PATHS = {"warrior": ("pc", "pc2"), "assassin": ("pc2", "pc"), "sura": ("pc", "pc2"), "shaman": ("pc2", "pc")}
_load = chrmgr.LoadLocalRaceData
def Register(name):
stem = name.lower().split(".")[0]
race, _, sex = stem.partition("_")
if race not in PATHS or sex not in ("m", "w"):
return
path = "d:/ymir work/%s/%s/" % (PATHS[race][sex == "w"], race)
for shape, model in SHAPES.items():
model = path + model % race
chrmgr.SetShapeModel(shape, model + ".gr2")
chrmgr.AppendShapeSkin(shape, 0, model + ".dds", model + ".dds")
def LoadLocalRaceData(name):
result = _load(name)
Register(name)
return result
chrmgr.LoadLocalRaceData = LoadLocalRaceData
)PY";
@@ -0,0 +1,83 @@
// PORT: AVA2 costume enchants (ENABLE_SPECIAL_COSTUME_ATTR), as module mt_costume_attr. Items 54001-54267 are
// USE_CHANGE_ATTRIBUTE rows with value0 = apply, value1 = value, value2 = target mask (1 << bonus flag); the server
// (metin2-server port_costume_attr.cpp) adds that attribute to a body / hair / mount-skin costume. The 40250
// inventory already sends every USE_CHANGE_ATTRIBUTE drop to the server; this script only marks the matching
// costumes as usable targets, shows the attribute in the enchant's tooltip and renders the "MTCATTR <code>" server
// messages that replace AVA2's locale strings 1056 / 1057 / 1061 / 1062 / 1063.
static const char c_szCostumeAttrScript[] = R"PY(# -*- coding: utf-8 -*-
import chat
import game
import item
import player
import uiInventory
import uiToolTip
L=player.AutoHuntLocalize
VNUMS=(54001,54267)
MAX_ATTR=3
# AVA2 CItem::GetBonusFlag of the costume kinds the server maps -> (mask bit, target name).
TARGETS={item.COSTUME_TYPE_BODY:(1,'时装'),item.COSTUME_TYPE_HAIR:(2,'发型'),item.COSTUME_TYPE_MOUNT_SKIN:(4,'坐骑外观')}
MESSAGES={'target':'该强化石不能用于这件物品。','none':'该强化石没有可附加的属性。','equipped':'请先卸下时装再附加属性。',
'full':'这件时装的附加属性已满(最多 3 条)。','has':'这件时装已有该属性。','done':'附加属性成功。'}
def Enchant(vnum):
"""(apply, value, mask) of a costume enchant, else None (selects vnum)."""
if not VNUMS[0]<=vnum<=VNUMS[1]:return None
item.SelectItem(vnum)
if item.GetItemType()!=item.ITEM_TYPE_USE or item.GetUseType(vnum)!="USE_CHANGE_ATTRIBUTE" or not item.GetValue(2):return None
return (item.GetValue(0),item.GetValue(1),item.GetValue(2))
def CanEnchant(enchant,dstSlot):
dstVnum=player.GetItemIndex(dstSlot)
if not dstVnum or player.IsEquipmentSlot(dstSlot):return False
item.SelectItem(dstVnum)
if item.GetItemType()!=item.ITEM_TYPE_COSTUME:return False
target=TARGETS.get(item.GetItemSubType())
if not target or not enchant[2]&(1<<target[0]):return False
attrs=[player.GetItemAttribute(dstSlot,i) for i in xrange(player.ATTRIBUTE_SLOT_MAX_NUM)]
used=[a for a in attrs if a[0]]
return len(used)<MAX_ATTR and enchant[0] not in [a[0] for a in used]
_canUse=uiInventory.InventoryWindow._InventoryWindow__CanUseSrcItemToDstItem
def CanUseSrcItemToDstItem(self,srcItemVNum,srcSlotPos,dstSlotPos):
enchant=Enchant(srcItemVNum)
if enchant is None:return _canUse(self,srcItemVNum,srcSlotPos,dstSlotPos)
return srcSlotPos!=dstSlotPos and CanEnchant(enchant,dstSlotPos)
uiInventory.InventoryWindow._InventoryWindow__CanUseSrcItemToDstItem=CanUseSrcItemToDstItem
Tip=uiToolTip.ItemToolTip
_addItemData=Tip.AddItemData
def AddItemData(self,itemVnum,metinSlot,attrSlot=0,flags=0,unbindTime=0):
result=_addItemData(self,itemVnum,metinSlot,attrSlot,flags,unbindTime)
enchant=Enchant(itemVnum)
if enchant:
self.AppendSpace(5)
affect=self._ItemToolTip__GetAffectString(enchant[0],enchant[1])
if affect:self.AppendTextLine(affect,self.POSITIVE_COLOR)
names=[name for bit,name in sorted(TARGETS.values()) if enchant[2]&(1<<bit)]
if names:self.AppendTextLine(L('适用:'+'、'.join(names)),self.CONDITION_COLOR)
self.ResizeToolTip()
return result
Tip.AddItemData=AddItemData
_command=game.GameWindow.BINARY_ServerCommand_Run
def Command(self,line):
if line.startswith('MTCATTR '):
words=line.split()
text=MESSAGES.get(words[1] if len(words)>1 else '')
if text:chat.AppendChat(chat.CHAT_TYPE_INFO,L(text))
return 1
return _command(self,line)
game.GameWindow.BINARY_ServerCommand_Run=Command
)PY";
+79
View File
@@ -0,0 +1,79 @@
// PORT: AVA2 mount costume + mount skin (ENABLE_MOUNT_COSTUME_SYSTEM / ENABLE_MOUNT_SKIN), as module mt_mount.
// The server swaps the horse race for the worn costume's value0 (metin2-server port_mount.cpp); this script
// registers the AVA2 npclist rows of the skin races (npclist.txt is read from the root pack only, so a patch
// pack cannot add them; RaceManager's __IsNewMountRace adds npc_mount to their search path), adds the mount
// and skin slots to the costume window under the sash slot, and renders the "MTMOUNT <code>" server
// messages that replace AVA2's locale strings 334 / 1097 / 1098. Models ship in the ava2items pack
// (tools/server/ava2-items, MOUNT_MOBS).
static const char c_szMountScript[] = R"PY(# -*- coding: utf-8 -*-
import chat
import chrmgr
import game
import item
import player
import ui
import uiInventory
L=player.AutoHuntLocalize
# AVA2 root/npclist.txt rows of the races MOUNT_MOBS ships.
RACES=((32012,"christmas_2017_sleddog_02"),(32028,"christmas_2020_alpaca"))
SRC_NAMES=(("christmas_2017_sleddog_02","christmas_2017_sleddog"),)
MESSAGES={'riding':'骑乘中无法更换坐骑时装。','need_mount':'需要先装备坐骑时装。','has_skin':'请先卸下坐骑外观。',
'need_horse':'需要先拥有马匹(马匹等级 1 以上)才能召唤坐骑。'}
# Costume window: left column under the sash slot (CostumeSlot coordinates; body is at 61,45).
SLOTS=((item.COSTUME_SLOT_MOUNT,(13,45)),(item.COSTUME_SLOT_MOUNT_SKIN,(13,82)))
for race,name in RACES:chrmgr.RegisterRaceName(race,name)
for name,src in SRC_NAMES:chrmgr.RegisterRaceSrcName(name,src)
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 PatchCostumeWindow(window):
base=FindChild(window,"Costume_Base");slot=FindChild(window,"CostumeSlot")
if not base or not slot:return
frames=[];slots=[]
for index,(x,y) in SLOTS:
frames.append({"name":"MountSlotBase%d"%index,"type":"image","x":slot["x"]+x,"y":slot["y"]+y,"image":"d:/ymir work/ui/public/Slot_Base.sub"})
slots.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(slots)
_execfile=ui.execfile
def ExecFile(fileName,dict):
_execfile(fileName,dict)
name=fileName.replace("\\","/").split("/")[-1].lower()
if name=="costumewindow.py" and "window" in dict:PatchCostumeWindow(dict["window"])
ui.execfile=ExecFile
_refreshCostume=uiInventory.CostumeWindow.RefreshCostumeSlot
def RefreshCostumeSlot(self):
_refreshCostume(self)
for index,_ in SLOTS:self.wndEquip.SetItemSlot(index,player.GetItemIndex(index),0)
self.wndEquip.RefreshSlot()
uiInventory.CostumeWindow.RefreshCostumeSlot=RefreshCostumeSlot
_command=game.GameWindow.BINARY_ServerCommand_Run
def Command(self,line):
if line.startswith('MTMOUNT '):
words=line.split()
text=MESSAGES.get(words[1] if len(words)>1 else '')
if text:chat.AppendChat(chat.CHAT_TYPE_INFO,L(text))
return 1
return _command(self,line)
game.GameWindow.BINARY_ServerCommand_Run=Command
)PY";
+100
View File
@@ -0,0 +1,100 @@
// PORT: AVA2 talisman (ITEM_TALISMAN_EQUIPMENT + ELEMENT_NEW_BONUSES), as module mt_pendant. Talismans are
// ITEM_ARMOR / ARMOR_PENDANT worn in c_Equipment_Pendant (server WEAR_PENDANT, metin2-server port_pendant.cpp).
// This script adds that slot to the inventory's equipment panel (the free spot under the earring on
// equipment_base), marks unequipped talismans as drop targets of the talisman attribute items (71302 / 53996
// reroll, 71301 / 53997 add up to 5) and nothing else, and renders the "MTPENDANT <code>" server messages that
// replace AVA2's locale strings 1056 / 1057 / 1067.
static const char c_szPendantScript[] = R"PY(# -*- coding: utf-8 -*-
import chat
import game
import item
import player
import ui
import uiInventory
L=player.AutoHuntLocalize
CHANGE=(71302,53996)
ADD=(71301,53997)
MAX_ATTR=5
MESSAGES={'target':'该道具只能用于护符。','equipped':'请先卸下护符再使用。','empty':'这件护符没有可重洗的属性。',
'changed':'护符属性已重新随机。','full':'这件护符的属性已满(最多 5 条)。','added':'护符追加属性成功。'}
# EquipmentSlot coordinates of the slot (the earring is at 114,84).
SLOT=(item.EQUIPMENT_PENDANT,(114,118))
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
index,(x,y)=SLOT
frame={"name":"PendantSlotBase","type":"image","x":slot["x"]+x,"y":slot["y"]+y,"image":"d:/ymir work/ui/public/Slot_Base.sub"}
base["children"]=(frame,)+tuple(base.get("children",()))
slot["slot"]=tuple(slot["slot"])+({"index":index,"x":x,"y":y,"width":32,"height":32},)
_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)
index=SLOT[0]
self.wndEquip.SetItemSlot(index,player.GetItemIndex(index),0)
self.wndEquip.RefreshSlot()
uiInventory.InventoryWindow.RefreshEquipSlotWindow=RefreshEquipSlotWindow
def IsTalisman(vnum):
if not vnum:return False
item.SelectItem(vnum)
return item.GetItemType()==item.ITEM_TYPE_ARMOR and item.GetItemSubType()==item.ARMOR_PENDANT
def AttrCount(slot):
return len([1 for i in xrange(player.ATTRIBUTE_SLOT_MAX_NUM) if player.GetItemAttribute(slot,i)[0]])
_canUse=uiInventory.InventoryWindow._InventoryWindow__CanUseSrcItemToDstItem
def CanUseSrcItemToDstItem(self,srcItemVNum,srcSlotPos,dstSlotPos):
scroll=srcItemVNum in CHANGE or srcItemVNum in ADD
talisman=IsTalisman(player.GetItemIndex(dstSlotPos))
if not scroll:
# the server refuses every other attribute / socket item on a talisman
if talisman and item.GetUseType(srcItemVNum) in self.USE_TYPE_TUPLE:return False
return _canUse(self,srcItemVNum,srcSlotPos,dstSlotPos)
if srcSlotPos==dstSlotPos or not talisman or player.IsEquipmentSlot(dstSlotPos):return False
count=AttrCount(dstSlotPos)
return count>0 if srcItemVNum in CHANGE else count<MAX_ATTR
uiInventory.InventoryWindow._InventoryWindow__CanUseSrcItemToDstItem=CanUseSrcItemToDstItem
_command=game.GameWindow.BINARY_ServerCommand_Run
def Command(self,line):
if line.startswith('MTPENDANT '):
words=line.split()
text=MESSAGES.get(words[1] if len(words)>1 else '')
if text:chat.AppendChat(chat.CHAT_TYPE_INFO,L(text))
return 1
return _command(self,line)
game.GameWindow.BINARY_ServerCommand_Run=Command
)PY";
+17
View File
@@ -37,6 +37,7 @@
#include "UserInterface/PythonQuest.h"
#include "UserInterface/PythonMessenger.h"
#include "UserInterface/PythonSafeBox.h"
#include "UserInterface/PythonSash.h"
#include "UserInterface/PythonGuild.h"
#include "UserInterface/MarkManager.h"
#include "UserInterface/GuildMarkDownloader.h"
@@ -153,6 +154,7 @@ struct GameSingletons
CPythonQuest pyQuest;
CPythonMessenger pyManager;
CPythonSafeBox pySafeBox;
CPythonSash pySash; // PORT: AVA2 sash
CPythonGuild pyGuild;
CGuildMarkManager kGuildMarkManager;
CGuildMarkDownloader kGuildMarkDownloader;
@@ -911,6 +913,7 @@ bool run_main_script_body(const char* lpCmdLine, std::string* error)
initBackground();
initMessenger();
initsafebox();
initSash();
initguild();
initServerStateChecker();
initRenderTarget(); // PORT: AVA2 model preview
@@ -1011,6 +1014,13 @@ namespace PythonBoot
#include "VipScript.inc"
#include "ModelPreviewScript.inc"
#include "BattlePassScript.inc"
#include "AppliesScript.inc"
#include "ArmorShapesScript.inc"
#include "SashScript.inc"
#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)
{
@@ -1093,6 +1103,13 @@ bool RunMainScript(const char* lpCmdLine, std::string* error)
install_script_module("mt_vip", c_szVipScript);
install_script_module("mt_preview", c_szModelPreviewScript);
install_script_module("mt_battlepass", c_szBattlePassScript);
install_script_module("mt_applies", c_szAppliesScript);
install_script_module("mt_shapes", c_szArmorShapesScript);
install_script_module("mt_sash", c_szSashScript);
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);
}
+139
View File
@@ -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";
+612
View File
@@ -0,0 +1,612 @@
// PORT: AVA2 sash (披风) windows and tooltips, as module mt_sash. The C++ side is the AVA2 sash module
// (PythonSash.cpp, CG/GC_SASH packets, PART_SASH model + item_scale.txt); this script is AVA2 root/uisash.py
// (CombineWindow/AbsorbWindow verbatim, their uiscript dicts served through ui.execfile), the glue AVA2 put
// into game.py/interfacemodule.py/uiinventory.py (ActSash/AlertSash, right click adds to the open window,
// attached slots lit, costume window sash slot) and uitooltip.py's absorption lines (CalcSashValue,
// SetSashResultItem/SetSashResultAbsItem). Server messages arrive as "MTSASH <code>" commands
// (metin2-server port_sash.cpp). Assets ship in the ava2items pack (tools/server/ava2-items, SASH_*).
static const char c_szSashScript[] = R"PY(# -*- coding: utf-8 -*-
import app
import chat
import chrmgr
import constInfo
import game
import item
import localeInfo
import mouseModule
import player
import sash
import ui
import uiInventory
import uiToolTip
L=player.AutoHuntLocalize
MESSAGES={'already_open':'披风窗口已经打开','absorb_open':'请先关闭披风吸收窗口','combine_open':'请先关闭披风合成窗口',
'locked':'物品已锁定,无法放入','grade':'只能放入相同品级的披风','max_abs':'该披风的吸收率已达上限',
'absorb_type':'只能吸收武器或铠甲','absorbed':'该披风已经吸收过物品','gold':'金币不足',
'success':'成功!','fail':'失败,作为材料的披风已消失'}
ABS_CHANGED='合成完成,吸收率变为 %d%%'
ALERT_COMBINE='合成失败时,作为材料的第二件披风将会消失。'
ALERT_ABSORB='吸收后,被吸收的物品将会消失。'
ABSORB_CHANCE='吸收率:%d%%'
ABSORB_CHANCE2='吸收率:%d%% ~ %d%%'
COMBINE_SCRIPT='''# -*- coding: utf-8 -*-
import player
import uiScriptLocale
L=player.AutoHuntLocalize
window = {
"name" : "Sash_CombineWindow",
"x" : (SCREEN_WIDTH - 215)/2, "y" : (SCREEN_HEIGHT - 270)/2,
"style" : ("movable", "float",),
"width" : 215, "height" : 270,
"children" : (
{
"name" : "board", "type" : "board", "style" : ("attach",),
"x" : 0, "y" : 0, "width" : 215, "height" : 270,
"children" : (
{
"name" : "TitleBar", "type" : "titlebar", "style" : ("attach",),
"x" : 6, "y" : 6, "width" : 200, "color" : "yellow",
"children" : (
{ "name" : "TitleName", "type" : "text", "x" : 95, "y" : 3, "text" : L("披风合成"), "text_horizontal_align" : "center" },
),
},
{
"name" : "Sash_Combine", "type" : "image", "x" : 9, "y" : 35,
"image" : uiScriptLocale.LOCALE_UISCRIPT_PATH + "acce/acce_combine.tga",
"children" : (
{
"name" : "SashSlot", "type" : "slot", "x" : 3, "y" : 3, "width" : 200, "height" : 150,
"slot" : (
{"index":0, "x":78, "y":7, "width":32, "height":32},
{"index":1, "x":78, "y":60, "width":32, "height":32},
{"index":2, "x":78, "y":115, "width":32, "height":32},
),
},
{ "name" : "Main", "type" : "text", "text" : L("主披风"), "text_horizontal_align" : "center", "x" : 85 + 12, "y" : 7 + 36 },
{ "name" : "serve", "type" : "text", "text" : L("材料披风"), "text_horizontal_align" : "center", "x" : 85 + 12, "y" : 60 + 38 },
{ "name" : "Result", "type" : "text", "text" : L("合成结果"), "text_horizontal_align" : "center", "x" : 85 + 12, "y" : 115 + 40 },
),
},
{ "name" : "NeedMoney", "type" : "text", "text" : "", "text_horizontal_align" : "center", "x" : 105, "y" : 215 },
{
"name" : "AcceptButton", "type" : "button", "x" : 40, "y" : 235, "text" : uiScriptLocale.OK,
"default_image" : "d:/ymir work/ui/public/middle_button_01.sub",
"over_image" : "d:/ymir work/ui/public/middle_button_02.sub",
"down_image" : "d:/ymir work/ui/public/middle_button_03.sub",
},
{
"name" : "CancelButton", "type" : "button", "x" : 114, "y" : 235, "text" : uiScriptLocale.CANCEL,
"default_image" : "d:/ymir work/ui/public/middle_button_01.sub",
"over_image" : "d:/ymir work/ui/public/middle_button_02.sub",
"down_image" : "d:/ymir work/ui/public/middle_button_03.sub",
},
),
},
),
}
'''
ABSORB_SCRIPT='''# -*- coding: utf-8 -*-
import player
import uiScriptLocale
L=player.AutoHuntLocalize
window = {
"name" : "Sash_AbsorbtionWindow",
"x" : (SCREEN_WIDTH - 205)/2, "y" : (SCREEN_HEIGHT - 270)/2,
"style" : ("movable", "float",),
"width" : 205, "height" : 270,
"children" : (
{
"name" : "board", "type" : "board", "style" : ("attach",),
"x" : 0, "y" : 0, "width" : 205, "height" : 270,
"children" : (
{
"name" : "TitleBar", "type" : "titlebar", "style" : ("attach",),
"x" : 6, "y" : 6, "width" : 190, "color" : "yellow",
"children" : (
{ "name" : "TitleName", "type" : "text", "x" : 95, "y" : 3, "text" : L("披风吸收"), "text_horizontal_align" : "center" },
),
},
{
"name" : "Sash_Combine", "type" : "image", "x" : 9, "y" : 35,
"image" : uiScriptLocale.LOCALE_UISCRIPT_PATH + "acce/acce_absorb.tga",
"children" : (
{
"name" : "SashSlot", "type" : "slot", "x" : 3, "y" : 3, "width" : 190, "height" : 200,
"slot" : (
{"index":0, "x":26, "y":41, "width":31, "height":31},
{"index":1, "x":125, "y":8, "width":31, "height":96},
{"index":2, "x":75, "y":126, "width":31, "height":31},
),
},
),
},
{
"name" : "AcceptButton", "type" : "button", "x" : 40, "y" : 235, "text" : uiScriptLocale.OK,
"default_image" : "d:/ymir work/ui/public/middle_button_01.sub",
"over_image" : "d:/ymir work/ui/public/middle_button_02.sub",
"down_image" : "d:/ymir work/ui/public/middle_button_03.sub",
},
{
"name" : "CancelButton", "type" : "button", "x" : 114, "y" : 235, "text" : uiScriptLocale.CANCEL,
"default_image" : "d:/ymir work/ui/public/middle_button_01.sub",
"over_image" : "d:/ymir work/ui/public/middle_button_02.sub",
"down_image" : "d:/ymir work/ui/public/middle_button_03.sub",
},
),
},
),
}
'''
SCRIPTS={'sash_combinewindow.py':COMBINE_SCRIPT,'sash_absorbwindow.py':ABSORB_SCRIPT}
# Costume window: the sash slot left of the hair slot (CostumeSlot coordinates).
COSTUME_SASH_POS=(13,8)
g_windows=None
g_effects=False
def CalcSashValue(value,abs):
# AVA2 uitooltip.CalcSashValue; the server scales applies the same way (port_sash.cpp).
if not value:return 0
valueCalc=round(value*abs)/100
valueCalc+=0.5
valueCalc=int(valueCalc)+1 if valueCalc>0 else int(valueCalc)
return 1 if (valueCalc<=0 and value>0) else valueCalc
def IsSash(itemVnum):
item.SelectItem(itemVnum)
return item.GetItemType()==item.ITEM_TYPE_COSTUME and item.GetItemSubType()==item.COSTUME_TYPE_SASH
def IsSashWindowOpen():
return bool(g_windows) and (g_windows[0].IsOpened() or g_windows[1].IsOpened())
class CombineWindow(ui.ScriptWindow):
def __init__(self):
ui.ScriptWindow.__init__(self)
self.isLoaded=0;self.PositionOut=0;self.PositionStartX=0;self.PositionStartY=0
self.tooltipItem=None
def __del__(self):
ui.ScriptWindow.__del__(self)
def Destroy(self):
self.ClearDictionary()
self.titleBar=None;self.btnAccept=None;self.btnCancel=None;self.sashSlot=None;self.needMoney=None;self.Result=None
self.tooltipItem=None;self.PositionOut=0;self.PositionStartX=0;self.PositionStartY=0
def LoadWindow(self):
if self.isLoaded:return
self.isLoaded=1
try:
pyScrLoader=ui.PythonScriptLoader()
pyScrLoader.LoadScriptFile(self,"uiscript/sash_combinewindow.py")
except:
import exception
exception.Abort("Sash_CombineWindow.LoadDialog.LoadScript")
try:
self.board=self.GetChild("board")
self.titleBar=self.GetChild("TitleBar")
self.btnAccept=self.GetChild("AcceptButton")
self.btnCancel=self.GetChild("CancelButton")
self.needMoney=self.GetChild("NeedMoney")
self.Result=self.GetChild("Result")
self.sashSlot=self.GetChild("SashSlot")
except:
import exception
exception.Abort("Sash_CombineWindow.LoadDialog.BindObject")
self.sashSlot.SetSelectEmptySlotEvent(ui.__mem_func__(self.OnSelectEmptySlot))
self.sashSlot.SetUnselectItemSlotEvent(ui.__mem_func__(self.OnSelectItemSlot))
self.sashSlot.SetUseSlotEvent(ui.__mem_func__(self.OnSelectItemSlot))
self.sashSlot.SetOverInItemEvent(ui.__mem_func__(self.OnOverInItem))
self.sashSlot.SetOverOutItemEvent(ui.__mem_func__(self.OnOverOutItem))
self.titleBar.SetCloseEvent(ui.__mem_func__(self.OnClose))
self.btnCancel.SetEvent(ui.__mem_func__(self.OnClose))
self.btnAccept.SetEvent(ui.__mem_func__(self.OnAccept))
def SetItemToolTip(self,itemTooltip):
self.tooltipItem=itemTooltip
def IsOpened(self):
return bool(self.IsShow() and self.isLoaded)
def SetPrice(self):
self.needMoney.SetText(L('合成费用:')+localeInfo.NumberToMoneyString(sash.GetPrice()))
def Open(self):
self.PositionOut=0
(self.PositionStartX,self.PositionStartY,z)=player.GetMainCharacterPosition()
self.SetPrice()
for i in xrange(sash.WINDOW_MAX_MATERIALS+1):
self.sashSlot.ClearSlot(i)
self.Show()
def Close(self):
if self.tooltipItem:self.tooltipItem.HideToolTip()
self.Hide()
def OnClose(self):
sash.SendCloseRequest()
def OnPressEscapeKey(self):
self.OnClose()
return True
def OnAccept(self):
sash.SendRefineRequest()
def OnUpdate(self):
LIMIT_RANGE=sash.LIMIT_RANGE
(x,y,z)=player.GetMainCharacterPosition()
if abs(x-self.PositionStartX)>=LIMIT_RANGE or abs(y-self.PositionStartY)>=LIMIT_RANGE:
if not self.PositionOut:
self.PositionOut+=1
self.OnClose()
def OnSelectEmptySlot(self,selectedSlotPos):
isAttached=mouseModule.mouseController.isAttached()
if not isAttached or selectedSlotPos==sash.WINDOW_MAX_MATERIALS:return
attachedSlotType=mouseModule.mouseController.GetAttachedType()
attachedSlotPos=mouseModule.mouseController.GetAttachedSlotNumber()
attachedInvenType=player.SlotTypeToInvenType(attachedSlotType)
mouseModule.mouseController.DeattachObject()
if attachedSlotType==player.SLOT_TYPE_INVENTORY and attachedInvenType==player.INVENTORY:
sash.Add(attachedInvenType,attachedSlotPos,selectedSlotPos)
def OnSelectItemSlot(self,selectedSlotPos):
if selectedSlotPos==sash.WINDOW_MAX_MATERIALS:return
mouseModule.mouseController.DeattachObject()
sash.Remove(selectedSlotPos)
def OnOverInItem(self,selectedSlotPos):
if self.tooltipItem:
if selectedSlotPos==sash.WINDOW_MAX_MATERIALS:
(isHere,iCell)=sash.GetAttachedItem(0)
if isHere:self.tooltipItem.SetSashResultItem(iCell)
else:
(isHere,iCell)=sash.GetAttachedItem(selectedSlotPos)
if isHere:self.tooltipItem.SetInventoryItem(iCell)
def OnOverOutItem(self):
if self.tooltipItem:self.tooltipItem.HideToolTip()
def Refresh(self,iAct):
self.SetPrice()
self.sashSlot.ClearSlot(sash.WINDOW_MAX_MATERIALS)
for i in xrange(sash.WINDOW_MAX_MATERIALS):
self.sashSlot.ClearSlot(i)
(isHere,iCell)=sash.GetAttachedItem(i)
if isHere:
self.sashSlot.SetItemSlot(i,player.GetItemIndex(iCell),0)
if i==int(sash.WINDOW_MAX_MATERIALS-1):
(itemVnum,MinAbs,MaxAbs)=sash.GetResultItem()
if not itemVnum:break
self.sashSlot.SetItemSlot(i+1,itemVnum,0)
break
self.sashSlot.RefreshSlot()
class AbsorbWindow(CombineWindow):
# AVA2 AbsorbWindow: CombineWindow without the price line, a two-item result tooltip.
def LoadWindow(self):
if self.isLoaded:return
self.isLoaded=1
try:
pyScrLoader=ui.PythonScriptLoader()
pyScrLoader.LoadScriptFile(self,"uiscript/sash_absorbwindow.py")
except:
import exception
exception.Abort("Sash_AbsorbtionWindow.LoadDialog.LoadScript")
try:
self.titleBar=self.GetChild("TitleBar")
self.btnAccept=self.GetChild("AcceptButton")
self.btnCancel=self.GetChild("CancelButton")
self.sashSlot=self.GetChild("SashSlot")
except:
import exception
exception.Abort("Sash_AbsorbtionWindow.LoadDialog.BindObject")
self.sashSlot.SetSelectEmptySlotEvent(ui.__mem_func__(self.OnSelectEmptySlot))
self.sashSlot.SetUnselectItemSlotEvent(ui.__mem_func__(self.OnSelectItemSlot))
self.sashSlot.SetUseSlotEvent(ui.__mem_func__(self.OnSelectItemSlot))
self.sashSlot.SetOverInItemEvent(ui.__mem_func__(self.OnOverInItem))
self.sashSlot.SetOverOutItemEvent(ui.__mem_func__(self.OnOverOutItem))
self.titleBar.SetCloseEvent(ui.__mem_func__(self.OnClose))
self.btnCancel.SetEvent(ui.__mem_func__(self.OnClose))
self.btnAccept.SetEvent(ui.__mem_func__(self.OnAccept))
def SetPrice(self):
pass
def OnOverInItem(self,selectedSlotPos):
if self.tooltipItem:
if selectedSlotPos==sash.WINDOW_MAX_MATERIALS:
(isHere1,iCell1)=sash.GetAttachedItem(0)
(isHere2,iCell2)=sash.GetAttachedItem(1)
if isHere1 and isHere2:self.tooltipItem.SetSashResultAbsItem(iCell1,iCell2)
else:
(isHere,iCell)=sash.GetAttachedItem(selectedSlotPos)
if isHere:self.tooltipItem.SetInventoryItem(iCell)
# --- tooltip (AVA2 uitooltip.py ENABLE_SASH_SYSTEM parts) -------------------------------------------------
Tip=uiToolTip.ItemToolTip
_addItemData=Tip.AddItemData
_appendAffect=Tip._ItemToolTip__AppendAffectInformation
_appendAttribute=Tip._ItemToolTip__AppendAttributeInformation
def AppendAbsorbedItem(self,itemAbsorbedVnum,absChance):
# Attack/defence and applies of the absorbed item, scaled by the absorption rate.
item.SelectItem(itemAbsorbedVnum)
itemType=item.GetItemType()
if itemType==item.ITEM_TYPE_WEAPON:
attack=[item.GetValue(3),item.GetValue(4),item.GetValue(5)]
magic=[item.GetValue(1),item.GetValue(2),item.GetValue(5)]
lines=[]
minPower,maxPower,addPower=[CalcSashValue(v,absChance) for v in attack]
if maxPower>minPower:lines.append(localeInfo.TOOLTIP_ITEM_ATT_POWER%(minPower+addPower,maxPower+addPower))
else:lines.append(localeInfo.TOOLTIP_ITEM_ATT_POWER_ONE_ARG%(minPower+addPower))
minMagic,maxMagic,addPower=[CalcSashValue(v,absChance) for v in magic]
if magic[0]>0 or magic[1]>0:
if maxMagic>minMagic:magicLine=localeInfo.TOOLTIP_ITEM_MAGIC_ATT_POWER%(minMagic+addPower,maxMagic+addPower)
else:magicLine=localeInfo.TOOLTIP_ITEM_MAGIC_ATT_POWER_ONE_ARG%(minMagic+addPower)
if item.GetItemSubType()==item.WEAPON_FAN:lines.insert(0,magicLine)
else:lines.append(magicLine)
for line in lines:self.AppendTextLine(line,self.POSITIVE_COLOR)
elif itemType==item.ITEM_TYPE_ARMOR:
defGrade=CalcSashValue(item.GetValue(1),absChance)
defBonus=CalcSashValue(item.GetValue(5)*2,absChance)
if defGrade>0:
self.AppendSpace(5)
self.AppendTextLine(localeInfo.TOOLTIP_ITEM_DEF_GRADE%(defGrade+defBonus),self.GetChangeTextLineColor(defGrade))
magicDefencePower=CalcSashValue(item.GetValue(0),absChance)
if magicDefencePower>0:
self.AppendTextLine(localeInfo.TOOLTIP_ITEM_MAGIC_DEF_POWER%magicDefencePower,self.GetChangeTextLineColor(magicDefencePower))
for i in xrange(item.ITEM_APPLY_MAX_NUM):
item.SelectItem(itemAbsorbedVnum)
(affectType,affectValue)=item.GetAffect(i)
affectValue=CalcSashValue(affectValue,absChance)
affectString=self._ItemToolTip__GetAffectString(affectType,affectValue)
if affectString and affectValue>0:
self.AppendTextLine(affectString,self.GetChangeTextLineColor(affectValue))
def AppendScaledAttributes(self,attrSlot,absChance):
if not attrSlot:return
for i in xrange(player.ATTRIBUTE_SLOT_MAX_NUM):
type,value=attrSlot[i][0],attrSlot[i][1]
if not value:continue
value=CalcSashValue(value,absChance)
affectString=self._ItemToolTip__GetAffectString(type,value)
if affectString and value>0:
self.AppendTextLine(affectString,self._ItemToolTip__GetAttributeColor(i,value))
def AddItemData(self,itemVnum,metinSlot,attrSlot=0,flags=0,unbindTime=0):
self.mtSash=None
if metinSlot and IsSash(itemVnum):
self.mtSash=(itemVnum,int(metinSlot[sash.ABSORPTION_SOCKET]),int(metinSlot[sash.ABSORBED_SOCKET]))
try:
return _addItemData(self,itemVnum,metinSlot,attrSlot,flags,unbindTime)
finally:
self.mtSash=None
def AppendAffectInformation(self):
_appendAffect(self)
state=getattr(self,'mtSash',None)
if not state:return
itemVnum,absChance,itemAbsorbedVnum=state
self.AppendTextLine(L(ABSORB_CHANCE%absChance),self.CONDITION_COLOR)
if itemAbsorbedVnum:AppendAbsorbedItem(self,itemAbsorbedVnum,absChance)
item.SelectItem(itemVnum)
def AppendAttributeInformation(self,attrSlot):
state=getattr(self,'mtSash',None)
if not state:return _appendAttribute(self,attrSlot)
AppendScaledAttributes(self,attrSlot,state[1])
item.SelectItem(state[0])
def SetSashResultItem(self,slotIndex,window_type=player.INVENTORY):
(itemVnum,MinAbs,MaxAbs)=sash.GetResultItem()
if not itemVnum:return
self.ClearToolTip()
metinSlot=[player.GetItemMetinSocket(window_type,slotIndex,i) for i in xrange(player.METIN_SOCKET_MAX_NUM)]
attrSlot=[player.GetItemAttribute(window_type,slotIndex,i) for i in xrange(player.ATTRIBUTE_SLOT_MAX_NUM)]
if not IsSash(itemVnum):return
itemDesc=item.GetItemDescription()
self._ItemToolTip__AdjustMaxWidth(attrSlot,itemDesc)
self._ItemToolTip__SetItemTitle(itemVnum,metinSlot,attrSlot)
self.AppendDescription(itemDesc,26)
self.AppendDescription(item.GetItemSummary(),26,self.CONDITION_COLOR)
self._ItemToolTip__AppendLimitInformation()
if MinAbs==MaxAbs:self.AppendTextLine(L(ABSORB_CHANCE%MinAbs),self.CONDITION_COLOR)
else:self.AppendTextLine(L(ABSORB_CHANCE2%(MinAbs,MaxAbs)),self.CONDITION_COLOR)
itemAbsorbedVnum=int(metinSlot[sash.ABSORBED_SOCKET])
if itemAbsorbedVnum:AppendAbsorbedItem(self,itemAbsorbedVnum,MaxAbs)
AppendScaledAttributes(self,attrSlot,MaxAbs)
item.SelectItem(itemVnum)
self.AppendWearableInformation()
self.ShowToolTip()
def SetSashResultAbsItem(self,slotIndex1,slotIndex2,window_type=player.INVENTORY):
itemVnumSash=player.GetItemIndex(window_type,slotIndex1)
itemVnumTarget=player.GetItemIndex(window_type,slotIndex2)
if not itemVnumSash or not itemVnumTarget:return
self.ClearToolTip()
if not IsSash(itemVnumSash):return
metinSlot=[player.GetItemMetinSocket(window_type,slotIndex1,i) for i in xrange(player.METIN_SOCKET_MAX_NUM)]
attrSlot=[player.GetItemAttribute(window_type,slotIndex2,i) for i in xrange(player.ATTRIBUTE_SLOT_MAX_NUM)]
absChance=int(metinSlot[sash.ABSORPTION_SOCKET])
itemDesc=item.GetItemDescription()
self._ItemToolTip__AdjustMaxWidth(attrSlot,itemDesc)
self._ItemToolTip__SetItemTitle(itemVnumSash,metinSlot,attrSlot)
self.AppendDescription(itemDesc,26)
self.AppendDescription(item.GetItemSummary(),26,self.CONDITION_COLOR)
item.SelectItem(itemVnumSash)
self._ItemToolTip__AppendLimitInformation()
self.AppendTextLine(L(ABSORB_CHANCE%absChance),self.CONDITION_COLOR)
AppendAbsorbedItem(self,itemVnumTarget,absChance)
AppendScaledAttributes(self,attrSlot,absChance)
item.SelectItem(itemVnumSash)
self.AppendWearableInformation()
self.ShowToolTip()
Tip.AddItemData=AddItemData
Tip._ItemToolTip__AppendAffectInformation=AppendAffectInformation
Tip._ItemToolTip__AppendAttributeInformation=AppendAttributeInformation
Tip.SetSashResultItem=SetSashResultItem
Tip.SetSashResultAbsItem=SetSashResultAbsItem
# --- uiscripts: the two sash windows and the costume window's sash slot ------------------------------------
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 PatchCostumeWindow(window):
base=FindChild(window,"Costume_Base");slot=FindChild(window,"CostumeSlot")
if not base or not slot:return
x,y=COSTUME_SASH_POS
frame={"name":"SashSlotBase","type":"image","x":slot["x"]+x,"y":slot["y"]+y,"image":"d:/ymir work/ui/public/Slot_Base.sub"}
base["children"]=(frame,)+tuple(base.get("children",()))
slot["slot"]=tuple(slot["slot"])+({"index":item.COSTUME_SLOT_SASH,"x":x,"y":y,"width":32,"height":32},)
_execfile=ui.execfile
def ExecFile(fileName,dict):
name=fileName.replace("\\","/").split("/")[-1].lower()
if name in SCRIPTS:
exec(SCRIPTS[name],dict)
return
_execfile(fileName,dict)
if name=="costumewindow.py" and "window" in dict:PatchCostumeWindow(dict["window"])
ui.execfile=ExecFile
_refreshCostume=uiInventory.CostumeWindow.RefreshCostumeSlot
def RefreshCostumeSlot(self):
_refreshCostume(self)
self.wndEquip.SetItemSlot(item.COSTUME_SLOT_SASH,player.GetItemIndex(item.COSTUME_SLOT_SASH),0)
self.wndEquip.RefreshSlot()
uiInventory.CostumeWindow.RefreshCostumeSlot=RefreshCostumeSlot
# --- inventory: right click adds to the open sash window, attached items are lit ----------------------------
_useItemSlot=uiInventory.InventoryWindow.UseItemSlot
_refreshBag=uiInventory.InventoryWindow.RefreshBagSlotWindow
def UseItemSlot(self,slotIndex):
if IsSashWindowOpen() and app.GetCursor()!=app.SELL:
cell=self._InventoryWindow__InventoryLocalSlotPosToGlobalSlotPos(slotIndex)
if not player.IsEquipmentSlot(cell):
sash.Add(player.INVENTORY,cell,255)
return
return _useItemSlot(self,slotIndex)
def AutoPotionActive(cell):
return constInfo.IS_AUTO_POTION(player.GetItemIndex(cell)) and player.GetItemMetinSocket(cell,0)!=0
def RefreshBagSlotWindow(self):
_refreshBag(self)
attached=set()
if IsSashWindowOpen():
for j in xrange(sash.WINDOW_MAX_MATERIALS):
(isHere,iCell)=sash.GetAttachedItem(j)
if isHere:attached.add(iCell)
lit=getattr(self,'mtSashLit',set())
now=set()
for i in xrange(player.INVENTORY_PAGE_SIZE):
cell=self._InventoryWindow__InventoryLocalSlotPosToGlobalSlotPos(i)
if cell in attached:
self.wndItem.ActivateSlot(i);now.add(i)
elif i in lit and not AutoPotionActive(cell):
self.wndItem.DeactivateSlot(i)
self.mtSashLit=now
self.wndItem.RefreshSlot()
uiInventory.InventoryWindow.UseItemSlot=UseItemSlot
uiInventory.InventoryWindow.RefreshBagSlotWindow=RefreshBagSlotWindow
# --- game window: ActSash/AlertSash (called by the GC_SASH packet), MTSASH messages, effects -----------------
def RegisterEffects():
global g_effects
if g_effects:return
g_effects=True
chrmgr.RegisterEffect(chrmgr.EFFECT_SASH_SUCCEDED,"","d:/ymir work/effect/etc/buff/buff_item6.mse")
chrmgr.RegisterEffect(chrmgr.EFFECT_SASH_EQUIP,"","d:/ymir work/effect/etc/buff/buff_item7.mse")
chrmgr.RegisterEffect(chrmgr.EFFECT_REFINED+chrmgr.EFFECT_SASH,"Bip01","d:/ymir work/pc/common/effect/armor/acc_01.mse")
_open=game.GameWindow.Open
_close=game.GameWindow.Close
_command=game.GameWindow.BINARY_ServerCommand_Run
def Open(self):
global g_windows
_open(self)
RegisterEffects()
g_windows=(CombineWindow(),AbsorbWindow())
for window in g_windows:
window.LoadWindow();window.Hide()
if self.interface:window.SetItemToolTip(self.interface.tooltipItem)
def Close(self):
global g_windows
if g_windows:
for window in g_windows:window.Close();window.Destroy()
g_windows=None
_close(self)
def ActSash(self,iAct,bWindow):
if not g_windows or not self.interface:return
window=g_windows[0 if bWindow else 1]
inventory=self.interface.wndInventory
if iAct==1:
if not window.IsOpened():window.Open()
if inventory and not inventory.IsShow():inventory.Show()
elif iAct==2:
if window.IsOpened():window.Close()
elif iAct==3 or iAct==4:
if window.IsOpened():window.Refresh(iAct)
if inventory:inventory.RefreshBagSlotWindow()
def AlertSash(self,bWindow):
self.PopupMessage(L(ALERT_COMBINE if bWindow else ALERT_ABSORB))
def Command(self,line):
if line.startswith('MTSASH '):
words=line.split()
if len(words)==3 and words[1]=='abs' and words[2].isdigit():text=ABS_CHANGED%int(words[2])
else:text=MESSAGES.get(words[1] if len(words)>1 else '')
if text:chat.AppendChat(chat.CHAT_TYPE_INFO,L(text))
return 1
return _command(self,line)
game.GameWindow.Open=Open
game.GameWindow.Close=Close
game.GameWindow.ActSash=ActSash
game.GameWindow.AlertSash=AlertSash
game.GameWindow.BINARY_ServerCommand_Run=Command
)PY";
+4 -2
View File
@@ -188,9 +188,11 @@ class SwitchbotWindow(ui.Window):
vnum=player.GetItemIndex(player.INVENTORY,cell)
if not vnum:continue
item.SelectItem(vnum);kind=item.GetItemType()
if kind==item.ITEM_TYPE_USE and item.GetItemSubType()==USE_CHANGE_ATTRIBUTE:
# 54001-54267 with a value2 mask are AVA2 costume enchants (mt_costume_attr), 71302 / 53996 talisman
# rerolls (mt_pendant), not switchers; talismans have their own attribute pool.
if kind==item.ITEM_TYPE_USE and item.GetItemSubType()==USE_CHANGE_ATTRIBUTE and not (54001<=vnum<=54267 and item.GetValue(2)) and vnum not in (71302,53996):
scrolls[vnum]=scrolls.get(vnum,0)+player.GetItemCount(player.INVENTORY,cell)
elif kind in (item.ITEM_TYPE_WEAPON,item.ITEM_TYPE_ARMOR) and any(player.GetItemAttribute(player.INVENTORY,cell,i)[0] for i in range(5)):
elif kind in (item.ITEM_TYPE_WEAPON,item.ITEM_TYPE_ARMOR) and not (kind==item.ITEM_TYPE_ARMOR and item.GetItemSubType()==item.ARMOR_PENDANT) and any(player.GetItemAttribute(player.INVENTORY,cell,i)[0] for i in range(5)):
items.append((cell,vnum))
selected=self.Selected();scroll=self.Scroll();state=self.bot.state
if state and state['active'] and (state['cell'],state['vnum']) in items:
@@ -760,6 +760,12 @@ bool LoadLocaleData(const char* localePath)
Tracenf("LoadLocaleData - LoadItemDesc(%s) Error", szItemDesc);
}
// PORT: AVA2 sash part scale (ENABLE_SASH_SYSTEM)
char szItemScale[256];
snprintf(szItemScale, sizeof(szItemScale), "%s/item_scale.txt", localePath);
if (!rkItemMgr.LoadItemScale(szItemScale))
Tracenf("LoadLocaleData: error while loading %s.", szItemScale);
if (!rkNPCMgr.LoadNonPlayerData(szMobProto))
{
TraceError("LoadLocaleData - LoadMobProto(%s) Error", szMobProto);
+44 -10
View File
@@ -685,21 +685,50 @@ void CGraphicThingInstance::DeformAll()
{
m_bUpdated = true;
CGrannyLODController::FDeformAll deform;
deform.mc_pWorldMatrix = &m_worldMatrix;
std::for_each(m_LODControllerVector.begin(), m_LODControllerVector.end(), deform);
D3DXMATRIX matScaled;
for (int i = 0; i < (int)m_LODControllerVector.size(); ++i)
{
CGrannyLODController* pkLOD = m_LODControllerVector[i];
if (pkLOD->isModelInstance())
pkLOD->DeformAll(__GetPartWorldMatrix(i, matScaled));
}
}
void CGraphicThingInstance::SetPartScale(int iPart, float fX, float fY, float fZ)
{
D3DXMATRIX matScale;
D3DXMatrixScaling(&matScale, fX, fY, fZ);
m_partScaleMap[iPart] = matScale;
}
void CGraphicThingInstance::ClearPartScale(int iPart)
{
m_partScaleMap.erase(iPart);
}
const D3DXMATRIX * CGraphicThingInstance::__GetPartWorldMatrix(int iPart, D3DXMATRIX & rScaled) const
{
if (m_partScaleMap.empty())
return &m_worldMatrix;
std::map<int, D3DXMATRIX>::const_iterator it = m_partScaleMap.find(iPart);
if (it == m_partScaleMap.end())
return &m_worldMatrix;
D3DXMatrixMultiply(&rScaled, &it->second, &m_worldMatrix);
return &rScaled;
}
void CGraphicThingInstance::DeformNoSkin()
{
m_bUpdated = true;
std::vector<CGrannyLODController*>::iterator i;
for (i=m_LODControllerVector.begin(); i!=m_LODControllerVector.end(); ++i)
D3DXMATRIX matScaled;
for (int i = 0; i < (int)m_LODControllerVector.size(); ++i)
{
CGrannyLODController* pkLOD=*i;
CGrannyLODController* pkLOD=m_LODControllerVector[i];
if (pkLOD->isModelInstance())
pkLOD->DeformNoSkin(&m_worldMatrix);
pkLOD->DeformNoSkin(__GetPartWorldMatrix(i, matScaled));
}
}
@@ -707,9 +736,13 @@ void CGraphicThingInstance::OnDeform()
{
m_bUpdated = true;
CGrannyLODController::FDeform deform;
deform.mc_pWorldMatrix = &m_worldMatrix;
std::for_each(m_LODControllerVector.begin(), m_LODControllerVector.end(), deform);
D3DXMATRIX matScaled;
for (int i = 0; i < (int)m_LODControllerVector.size(); ++i)
{
CGrannyLODController* pkLOD = m_LODControllerVector[i];
if (pkLOD->isModelInstance())
pkLOD->Deform(__GetPartWorldMatrix(i, matScaled));
}
}
void CGraphicThingInstance::__SetLocalTime(float fLocalTime)
@@ -938,6 +971,7 @@ void CGraphicThingInstance::OnClear()
{
stl_wipe(m_LODControllerVector);
stl_wipe_second(m_roMotionThingMap);
m_partScaleMap.clear();
for (DWORD d = 0; d < m_modelThingSetVector.size(); ++d)
m_modelThingSetVector[d].Clear();
+8
View File
@@ -34,6 +34,11 @@ class CGraphicThingInstance : public CGraphicObjectInstance
void UpdateLODLevel();
void UpdateTime();
// PORT: AVA2 ENABLE_OBJ_SCALLING for the sash part: the part is deformed with Scale * world,
// i.e. scaled about the actor origin in actor space (item_scale.txt).
void SetPartScale(int iPart, float fX, float fY, float fZ);
void ClearPartScale(int iPart);
void DeformAll(); // 모든 LOD 디폼
bool LessRenderOrder(CGraphicThingInstance* pkThingInst);
@@ -139,6 +144,9 @@ class CGraphicThingInstance : public CGraphicObjectInstance
std::vector<CGrannyLODController *> m_LODControllerVector;
std::vector<TModelThingSet> m_modelThingSetVector;
std::map<DWORD, CGraphicThing::TRef *> m_roMotionThingMap;
std::map<int, D3DXMATRIX> m_partScaleMap; // PORT: AVA2 sash scale
const D3DXMATRIX * __GetPartWorldMatrix(int iPart, D3DXMATRIX & rScaled) const;
protected:
virtual void OnUpdateCollisionData(const CStaticCollisionDataVector * pscdVector);
+1
View File
@@ -243,6 +243,7 @@ class CActorInstance : public IActorInstance, public IFlyTargetableObject
bool SetRace(DWORD eRace);
void SetHair(DWORD eHair);
void AttachSash(CItemData * pItemData, float fSpecular = 0.0f); // PORT: AVA2 sash
void SetVirtualID(DWORD dwVID);
void SetShape(DWORD eShape, float fSpecular=0.0f);
+41
View File
@@ -624,3 +624,44 @@ void CActorInstance::__ClearAttachingEffect()
}
m_AttachingEffectList.clear();
}
// PORT: AVA2 ActorInstanceAttach.cpp AttachSash (ENABLE_SASH_SYSTEM). The sash is a model part on
// "Bip01 Spine2" that plays its own first motion (the cloth sway).
void CActorInstance::AttachSash(CItemData * pItemData, float fSpecular)
{
if (!pItemData)
{
RegisterModelThing(CRaceData::PART_SASH, NULL);
SetModelInstance(CRaceData::PART_SASH, CRaceData::PART_SASH, 0);
RefreshActorInstance();
return;
}
RegisterModelThing(CRaceData::PART_SASH, pItemData->GetModelThing());
SetModelInstance(CRaceData::PART_SASH, CRaceData::PART_SASH, 0);
AttachModelInstance(CRaceData::PART_MAIN, "Bip01 Spine2", CRaceData::PART_SASH);
if (fSpecular > 0.0f)
{
SMaterialData kMaterialData;
kMaterialData.pImage = NULL;
kMaterialData.isSpecularEnable = TRUE;
kMaterialData.fSpecularPower = fSpecular;
kMaterialData.bSphereMapIndex = 1;
SetMaterialData(CRaceData::PART_SASH, NULL, kMaterialData);
}
if (CRaceData::PART_SASH >= m_LODControllerVector.size())
return;
if (CGrannyLODController* pLODController = m_LODControllerVector[CRaceData::PART_SASH])
{
if (CGrannyModelInstance* pSashModelInstance = pLODController->GetModelInstance())
{
CGraphicThing* pItemGraphicThing = pItemData->GetModelThing();
if (pItemGraphicThing && pItemGraphicThing->GetMotionCount() > 0)
if (CGrannyMotion* pItemMotion = pItemGraphicThing->GetMotionPointer(0))
pSashModelInstance->SetMotionPointer(pItemMotion);
}
}
}
+38
View File
@@ -215,6 +215,43 @@ void CItemData::SetItemTableData(TItemTable * pItemTable)
memcpy(&m_ItemTable, pItemTable, sizeof(TItemTable));
}
// PORT: AVA2 ItemData.cpp SetItemScale/GetItemScale. The position columns are dropped: AVA2 feeds
// them to SetScalePosition, which its deform never reads. JOB_WOLFMAN rows are skipped (no wolfman).
void CItemData::SetItemScale(const std::string & strJob, const std::string & strSex, const std::string & strScaleX, const std::string & strScaleY, const std::string & strScaleZ)
{
DWORD dwPos;
if (strJob == "JOB_WARRIOR")
dwPos = NRaceData::JOB_WARRIOR;
else if (strJob == "JOB_ASSASSIN")
dwPos = NRaceData::JOB_ASSASSIN;
else if (strJob == "JOB_SURA")
dwPos = NRaceData::JOB_SURA;
else if (strJob == "JOB_SHAMAN")
dwPos = NRaceData::JOB_SHAMAN;
else
return;
dwPos += 1;
if (strSex == "F")
dwPos += 5;
m_aScaleInfo[dwPos].bSet = true;
m_aScaleInfo[dwPos].fScaleX = float(atof(strScaleX.c_str()) / 100.0f);
m_aScaleInfo[dwPos].fScaleY = float(atof(strScaleY.c_str()) / 100.0f);
m_aScaleInfo[dwPos].fScaleZ = float(atof(strScaleZ.c_str()) / 100.0f);
}
bool CItemData::GetItemScale(DWORD dwPos, float & fScaleX, float & fScaleY, float & fScaleZ)
{
if (dwPos >= sizeof(m_aScaleInfo) / sizeof(m_aScaleInfo[0]) || !m_aScaleInfo[dwPos].bSet)
return false;
fScaleX = m_aScaleInfo[dwPos].fScaleX;
fScaleY = m_aScaleInfo[dwPos].fScaleY;
fScaleZ = m_aScaleInfo[dwPos].fScaleZ;
return true;
}
const CItemData::TItemTable* CItemData::GetTable() const
{
return &m_ItemTable;
@@ -452,6 +489,7 @@ void CItemData::Clear()
m_pLODModelThingVector.clear();
memset(&m_ItemTable, 0, sizeof(m_ItemTable));
memset(m_aScaleInfo, 0, sizeof(m_aScaleInfo));
}
CItemData::CItemData()
+25
View File
@@ -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,
};
@@ -95,6 +97,7 @@ class CItemData
ARMOR_FOOTS,
ARMOR_NECK,
ARMOR_EAR,
ARMOR_PENDANT, // PORT: AVA2 talisman, worn in c_Equipment_Pendant
ARMOR_NUM_TYPES
};
@@ -102,6 +105,9 @@ class CItemData
{
COSTUME_BODY, //0 갑옷(main look)
COSTUME_HAIR, //1 헤어(탈착가능)
COSTUME_MOUNT = 2, // PORT: AVA2 mount costume (value0 = horse race)
COSTUME_SASH = 3, // PORT: AVA2 sash
COSTUME_MOUNT_SKIN = 4, // PORT: AVA2 mount skin (needs a worn COSTUME_MOUNT)
COSTUME_NUM_TYPES,
};
@@ -350,6 +356,16 @@ class CItemData
APPLY_ANTI_CRITICAL_PCT, //90 크리티컬 저항
APPLY_ANTI_PENETRATE_PCT, //91 관통타격 저항
// PORT AVA2 bonus numbering, matching the server (common/length.h): 92-122 and 129-138 are not ported yet.
APPLY_ENCHANT_ELEC = 123, // talisman: attack +x% on monsters of that element
APPLY_ENCHANT_FIRE = 124,
APPLY_ENCHANT_ICE = 125,
APPLY_ENCHANT_WIND = 126,
APPLY_ENCHANT_EARTH = 127,
APPLY_ENCHANT_DARK = 128,
APPLY_ATTBONUS_STONE = 139,
APPLY_ATTBONUS_BOSS = 140,
MAX_APPLY_NUM, //
};
@@ -491,6 +507,9 @@ class CItemData
//BOOL LoadItemData(const char * c_szFileName);
void SetDefaultItemData(const char * c_szIconFileName, const char * c_szModelFileName = NULL);
void SetItemTableData(TItemTable * pItemTable);
// PORT: AVA2 item_scale.txt (sash part scale per job/sex). Slot = job + 1, + 5 when female.
void SetItemScale(const std::string & strJob, const std::string & strSex, const std::string & strScaleX, const std::string & strScaleY, const std::string & strScaleZ);
bool GetItemScale(DWORD dwPos, float & fScaleX, float & fScaleY, float & fScaleZ);
protected:
void __LoadFiles();
@@ -514,6 +533,12 @@ class CItemData
NRaceData::TAttachingDataVector m_AttachingDataVector;
DWORD m_dwVnum;
TItemTable m_ItemTable;
struct SScaleInfo
{
bool bSet;
float fScaleX, fScaleY, fScaleZ;
};
SScaleInfo m_aScaleInfo[11];
public:
static void DestroySystem();
+36
View File
@@ -402,3 +402,39 @@ CItemManager::~CItemManager()
{
Destroy();
}
// PORT: AVA2 ItemManager.cpp LoadItemScale (ENABLE_SASH_SYSTEM). Each row covers vnum .. vnum + 4;
// unlike AVA2 (MakeItemData) only vnums already in the item table are touched.
bool CItemManager::LoadItemScale(const char * c_szFileName)
{
const VOID* pvData;
CMappedFile kFile;
if (!CEterPackManager::Instance().Get(kFile, c_szFileName, &pvData))
return false;
CMemoryTextFileLoader kTextFileLoader;
kTextFileLoader.Bind(kFile.Size(), pvData);
CTokenVector kTokenVector;
for (DWORD i = 0; i < kTextFileLoader.GetLineCount(); ++i)
{
if (!kTextFileLoader.SplitLineByTab(i, &kTokenVector))
continue;
if (kTokenVector.size() < ITEMSCALE_NUM)
{
TraceError("LoadItemScale: invalid line %d (%s).", i, c_szFileName);
continue;
}
for (int j = 0; j < 5; ++j)
{
TItemMap::iterator f = m_ItemMap.find(atoi(kTokenVector[ITEMSCALE_VNUM].c_str()) + j);
if (m_ItemMap.end() == f)
continue;
f->second->SetItemScale(kTokenVector[ITEMSCALE_JOB], kTokenVector[ITEMSCALE_SEX], kTokenVector[ITEMSCALE_SCALE_X], kTokenVector[ITEMSCALE_SCALE_Y], kTokenVector[ITEMSCALE_SCALE_Z]);
}
}
return true;
}
+15
View File
@@ -13,6 +13,20 @@ class CItemManager : public CSingleton<CItemManager>
ITEMDESC_COL_SUMM,
ITEMDESC_COL_NUM,
};
// PORT: AVA2 item_scale.txt columns
enum EItemScaleColumn
{
ITEMSCALE_VNUM,
ITEMSCALE_JOB,
ITEMSCALE_SEX,
ITEMSCALE_SCALE_X,
ITEMSCALE_SCALE_Y,
ITEMSCALE_SCALE_Z,
ITEMSCALE_POSITION_X,
ITEMSCALE_POSITION_Y,
ITEMSCALE_POSITION_Z,
ITEMSCALE_NUM,
};
public:
typedef std::map<DWORD, CItemData*> TItemMap;
@@ -31,6 +45,7 @@ class CItemManager : public CSingleton<CItemManager>
/////
bool LoadItemDesc(const char* c_szFileName);
bool LoadItemScale(const char* c_szFileName); // PORT: AVA2 sash
bool LoadItemList(const char* c_szFileName);
bool LoadItemTable(const char* c_szFileName);
CItemData * MakeItemData(DWORD dwIndex);
+1
View File
@@ -23,6 +23,7 @@ class CRaceData
PART_HEAD,
PART_WEAPON_LEFT,
PART_HAIR,
PART_SASH, // PORT: AVA2 sash ("Bip01 Spine2", item vnum - 85000)
PART_MAX_NUM,
};
+11
View File
@@ -22,6 +22,15 @@ bool __IsNPCRace(unsigned race)
return false;
}
// PORT: AVA2 __IsNewMountRace — mount costume / skin races (christmas_2017_sleddog, ...) live in npc_mount.
bool __IsNewMountRace(unsigned race)
{
if (race >= 32000 && race <= 32100)
return true;
return false;
}
void __GetRaceResourcePathes(unsigned race, std::vector <std::string>& vec_stPathes)
{
if (__IsGuildRace(race))
@@ -37,6 +46,8 @@ void __GetRaceResourcePathes(unsigned race, std::vector <std::string>& vec_stPat
if (race >= 30000)
{
vec_stPathes.push_back ("d:/ymir work/npc2/");
if (__IsNewMountRace(race)) // PORT: AVA2
vec_stPathes.push_back ("d:/ymir work/npc_mount/");
vec_stPathes.push_back ("d:/ymir work/npc/");
vec_stPathes.push_back ("d:/ymir work/monster/");
vec_stPathes.push_back ("d:/ymir work/monster2/");
+6
View File
@@ -104,7 +104,13 @@ enum EDragonSoulStepTypes
const DWORD c_Costume_Slot_Hair = c_Costume_Slot_Start + 1;
const DWORD c_Costume_Slot_Count = 2;
const DWORD c_Costume_Slot_End = c_Costume_Slot_Start + c_Costume_Slot_Count;
const DWORD c_Costume_Slot_Sash = c_Equipment_Start + 24; // PORT: AVA2 sash, server WEAR_COSTUME_SASH (after WEAR_BELT)
const DWORD c_Costume_Slot_Mount = c_Equipment_Start + 25; // PORT: AVA2 mount costume, server WEAR_COSTUME_MOUNT
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부터임.
+78
View File
@@ -756,6 +756,7 @@ bool CInstanceBase::Create(const SCreateData& c_rkCreateData)
{
SetHair(c_rkCreateData.m_dwHair);
SetWeapon(c_rkCreateData.m_dwWeapon);
SetSash(c_rkCreateData.m_dwSash); // PORT: AVA2 sash
}
__Create_SetName(c_rkCreateData);
@@ -2592,6 +2593,80 @@ void CInstanceBase::SetHair(DWORD eHair)
m_GraphicThingInstance.SetHair(eHair);
}
// PORT: AVA2 InstanceBase.cpp SetSash/ChangeSash/ClearSashEffect (ENABLE_SASH_SYSTEM + ENABLE_OBJ_SCALLING).
// Differences: part 0 detaches the model (AVA2 looked up item 85000 and returned early, leaving the old
// sash on), and only the item_scale.txt scale is applied (its position columns never reached the deform).
void CInstanceBase::SetSash(DWORD dwSash)
{
if (!IsPC() && GetRace() > 8)
return;
if (IsPC() && IsPoly())
return;
m_awPart[CRaceData::PART_SASH] = dwSash;
ClearSashEffect();
m_GraphicThingInstance.ClearPartScale(CRaceData::PART_SASH);
if (!dwSash)
{
m_GraphicThingInstance.AttachSash(NULL);
return;
}
dwSash += 85000;
float fSpecular = 65.0f;
if (dwSash > 86000)
{
fSpecular += 35;
m_dwSashEffect = EFFECT_REFINED + EFFECT_SASH;
__EffectContainer_AttachEffect(m_dwSashEffect);
}
fSpecular /= 100.0f;
CItemData * pItemData;
if (!CItemManager::Instance().GetItemDataPointer(dwSash, &pItemData))
{
m_GraphicThingInstance.AttachSash(NULL);
return;
}
m_GraphicThingInstance.AttachSash(pItemData, fSpecular);
DWORD dwRace = GetRace(), dwPos = RaceToJob(dwRace), dwSex = RaceToSex(dwRace);
dwPos += 1;
if (dwSex == 0)
dwPos += 5;
float fScaleX, fScaleY, fScaleZ;
if (pItemData->GetItemScale(dwPos, fScaleX, fScaleY, fScaleZ))
m_GraphicThingInstance.SetPartScale(CRaceData::PART_SASH, fScaleX, fScaleY, fScaleZ);
}
void CInstanceBase::ChangeSash(DWORD dwSash)
{
if (!IsPC() && GetRace() > 7)
return;
if (GetPart(CRaceData::PART_SASH) == dwSash)
return;
SetSash(dwSash);
}
void CInstanceBase::ClearSashEffect()
{
if (!m_dwSashEffect)
return;
__EffectContainer_DetachEffect(m_dwSashEffect);
m_dwSashEffect = 0;
}
void CInstanceBase::ChangeHair(DWORD eHair)
{
if (!HAIR_COLOR_ENABLE)
@@ -2815,6 +2890,7 @@ bool CInstanceBase::ChangeArmor(DWORD dwArmor)
DWORD dwVID = GetVirtualID();
DWORD dwRace = GetRace();
DWORD eHair = GetPart(CRaceData::PART_HAIR);
DWORD dwSash = GetPart(CRaceData::PART_SASH); // PORT: AVA2 sash
DWORD eWeapon = GetPart(CRaceData::PART_WEAPON);
float fRot = GetRotation();
float fAdvRot = GetAdvancingRotation();
@@ -2835,6 +2911,7 @@ bool CInstanceBase::ChangeArmor(DWORD dwArmor)
SetArmor(dwArmor);
SetHair(eHair);
SetSash(dwSash); // PORT: AVA2 sash
SetWeapon(eWeapon);
SetRotation(fRot);
@@ -3102,6 +3179,7 @@ void CInstanceBase::__Initialize()
m_swordRefineEffectRight = 0;
m_swordRefineEffectLeft = 0;
m_armorRefineEffect = 0;
m_dwSashEffect = 0; // PORT: AVA2 sash
m_sAlignment = 0;
m_byPKMode = 0;
+8
View File
@@ -26,6 +26,7 @@ class CInstanceBase
DWORD m_dwWeapon;
DWORD m_dwHair;
DWORD m_dwMountVnum;
DWORD m_dwSash; // PORT: AVA2 sash part (item vnum - 85000)
short m_sAlignment;
BYTE m_byPKMode;
@@ -293,6 +294,7 @@ class CInstanceBase
EFFECT_BODYARMOR_SPECIAL, // 갑옷 4-2-1
EFFECT_BODYARMOR_SPECIAL2, // 갑옷 4-2-2
EFFECT_SASH, // PORT: AVA2 refined sash glow (acc_01.mse)
EFFECT_REFINED_NUM,
};
@@ -370,6 +372,8 @@ class CInstanceBase
EFFECT_HALLOWEEN_CANDY_EQUIP, // 할로윈 사탕 착용 순간에 발동하는 이펙트
EFFECT_HAPPINESS_RING_EQUIP, // 행복의 반지 착용 순간에 발동하는 이펙트
EFFECT_LOVE_PENDANT_EQUIP, // 행복의 반지 착용 순간에 발동하는 이펙트
EFFECT_SASH_SUCCEDED, // PORT: AVA2 sash
EFFECT_SASH_EQUIP,
EFFECT_TEMP,
EFFECT_NUM,
};
@@ -500,6 +504,8 @@ class CInstanceBase
bool ChangeArmor(DWORD dwArmor);
void ChangeWeapon(DWORD eWeapon);
void ChangeHair(DWORD eHair);
void SetSash(DWORD dwSash); // PORT: AVA2 sash
void ChangeSash(DWORD dwSash);
void ChangeGuild(DWORD dwGuildID);
DWORD GetWeaponType();
@@ -909,6 +915,7 @@ class CInstanceBase
UINT __GetRefinedEffect(CItemData* pItem);
void __ClearWeaponRefineEffect();
void __ClearArmorRefineEffect();
void ClearSashEffect(); // PORT: AVA2 sash
protected:
void __AttachSelectEffect();
@@ -977,6 +984,7 @@ class CInstanceBase
DWORD m_swordRefineEffectRight;
DWORD m_swordRefineEffectLeft;
DWORD m_armorRefineEffect;
DWORD m_dwSashEffect; // PORT: AVA2 sash
struct SMoveAfterFunc
{
@@ -77,6 +77,7 @@ void SNetworkActorData::__copy__(const SNetworkActorData& src)
m_dwArmor = src.m_dwArmor;
m_dwWeapon = src.m_dwWeapon;
m_dwHair = src.m_dwHair;
m_dwSash = src.m_dwSash; // PORT: AVA2 sash
m_dwOwnerVID = src.m_dwOwnerVID;
@@ -104,6 +105,7 @@ SNetworkActorData::SNetworkActorData()
m_dwArmor=0;
m_dwWeapon=0;
m_dwHair=0;
m_dwSash=0; // PORT: AVA2 sash
m_dwEmpireID=0;
m_dwOwnerVID=0;
@@ -356,6 +358,7 @@ CInstanceBase* CNetworkActorManager::__AppendCharacterManagerActor(SNetworkActor
kCreateData.m_dwArmor=rkNetActorData.m_dwArmor;
kCreateData.m_dwWeapon=rkNetActorData.m_dwWeapon;
kCreateData.m_dwHair=rkNetActorData.m_dwHair;
kCreateData.m_dwSash=rkNetActorData.m_dwSash; // PORT: AVA2 sash
kCreateData.m_isMain=__IsMainActorVID(dwVID);
CInstanceBase* pOldInstance = rkChrMgr.GetInstancePtr(dwVID);
@@ -473,6 +476,7 @@ void CNetworkActorManager::UpdateActor(const SNetworkUpdateActorData& c_rkNetUpd
pkInstFind->ChangeArmor(c_rkNetUpdateActorData.m_dwArmor);
pkInstFind->ChangeWeapon(c_rkNetUpdateActorData.m_dwWeapon);
pkInstFind->ChangeHair(c_rkNetUpdateActorData.m_dwHair);
pkInstFind->ChangeSash(c_rkNetUpdateActorData.m_dwSash); // PORT: AVA2 sash
pkInstFind->ChangeGuild(c_rkNetUpdateActorData.m_dwGuildID);
pkInstFind->SetAffectFlagContainer(c_rkNetUpdateActorData.m_kAffectFlags);
pkInstFind->SetMoveSpeed(c_rkNetUpdateActorData.m_dwMovSpd);
@@ -502,6 +506,7 @@ void CNetworkActorManager::UpdateActor(const SNetworkUpdateActorData& c_rkNetUpd
rkNetActorData.m_dwArmor=c_rkNetUpdateActorData.m_dwArmor;
rkNetActorData.m_dwWeapon=c_rkNetUpdateActorData.m_dwWeapon;
rkNetActorData.m_dwHair=c_rkNetUpdateActorData.m_dwHair;
rkNetActorData.m_dwSash=c_rkNetUpdateActorData.m_dwSash; // PORT: AVA2 sash
rkNetActorData.m_sAlignment=c_rkNetUpdateActorData.m_sAlignment;
rkNetActorData.m_byPKMode=c_rkNetUpdateActorData.m_byPKMode;
}
@@ -31,6 +31,7 @@ struct SNetworkActorData
DWORD m_dwArmor;
DWORD m_dwWeapon;
DWORD m_dwHair;
DWORD m_dwSash; // PORT: AVA2 sash
DWORD m_dwOwnerVID;
@@ -85,6 +86,7 @@ struct SNetworkUpdateActorData
DWORD m_dwArmor;
DWORD m_dwWeapon;
DWORD m_dwHair;
DWORD m_dwSash; // PORT: AVA2 sash
DWORD m_dwMovSpd;
DWORD m_dwAtkSpd;
short m_sAlignment;
@@ -100,6 +102,7 @@ struct SNetworkUpdateActorData
m_dwArmor=0;
m_dwWeapon=0;
m_dwHair=0;
m_dwSash=0;
m_dwMovSpd=0;
m_dwAtkSpd=0;
m_sAlignment=0;
+53
View File
@@ -154,6 +154,7 @@ enum
HEADER_CG_DRAGON_SOUL_REFINE = 205,
HEADER_CG_STATE_CHECKER = 206,
HEADER_CG_SASH = 211, // PORT: AVA2 sash (metin2-server FEATURE-AVA2-UNLOCK P4)
#ifdef __AUCTION__
HEADER_CG_AUCTION_CMD = 205,
@@ -358,6 +359,7 @@ enum
HEADER_GC_SPECIFIC_EFFECT = 208,
HEADER_GC_DRAGON_SOUL_REFINE = 209,
HEADER_GC_RESPOND_CHANNELSTATUS = 210,
HEADER_GC_SASH = 215, // PORT: AVA2 sash
HEADER_GC_KEY_AGREEMENT_COMPLETED = 0xfa, // _IMPROVED_PACKET_ENCRYPTION_
HEADER_GC_KEY_AGREEMENT = 0xfb, // _IMPROVED_PACKET_ENCRYPTION_
@@ -1285,6 +1287,7 @@ enum ECharacterEquipmentPart
CHR_EQUIPPART_WEAPON,
CHR_EQUIPPART_HEAD,
CHR_EQUIPPART_HAIR,
CHR_EQUIPPART_SASH, // PORT: AVA2 sash, item vnum - 85000
CHR_EQUIPPART_NUM,
};
@@ -2401,6 +2404,8 @@ enum SPECIAL_EFFECT
SE_EQUIP_HALLOWEEN_CANDY, // 할로윈 사탕을 착용(-_-;)한 순간에 발동하는 이펙트
SE_EQUIP_HAPPINESS_RING, // 크리스마스 행복의 반지를 착용하는 순간에 발동하는 이펙트
SE_EQUIP_LOVE_PENDANT, // 발렌타인 사랑의 팬던트(71145) 착용할 때 이펙트 (발동이펙트임, 지속이펙트 아님)
SE_EFFECT_SASH_SUCCEDED, // PORT: AVA2 sash combine success
SE_EFFECT_SASH_EQUIP,
};
typedef struct SPacketGCSpecialEffect
@@ -2812,4 +2817,52 @@ typedef struct SChannelStatus
BYTE bStatus;
} TChannelStatus;
// PORT: AVA2 sash window. HEADER_CG_SASH and HEADER_GC_SASH share the layout; the server sends the
// combine (bWindow true) and absorb windows' state, messages come as "MTSASH <code>" server commands.
enum ESashInfo
{
SASH_ABSORPTION_SOCKET = 0,
SASH_ABSORBED_SOCKET = 1,
SASH_WINDOW_MAX_MATERIALS = 2,
};
enum
{
SASH_SUBHEADER_GC_OPEN = 0,
SASH_SUBHEADER_GC_CLOSE,
SASH_SUBHEADER_GC_ADDED,
SASH_SUBHEADER_GC_REMOVED,
SASH_SUBHEADER_CG_REFINED,
SASH_SUBHEADER_CG_CLOSE = 0,
SASH_SUBHEADER_CG_ADD,
SASH_SUBHEADER_CG_REMOVE,
SASH_SUBHEADER_CG_REFINE,
};
typedef struct SPacketSash
{
BYTE header;
BYTE subheader;
bool bWindow;
DWORD dwPrice;
BYTE bPos;
TItemPos tPos;
DWORD dwItemVnum;
DWORD dwMinAbs;
DWORD dwMaxAbs;
} TPacketSash;
typedef struct SSashMaterial
{
BYTE bHere;
WORD wCell;
} TSashMaterial;
typedef struct SSashResult
{
DWORD dwItemVnum;
DWORD dwMinAbs;
DWORD dwMaxAbs;
} TSashResult;
#pragma pack(pop)
@@ -840,5 +840,8 @@ void initchrmgr()
PyModule_AddIntConstant(poModule, "EFFECT_HALLOWEEN_CANDY_EQUIP", CInstanceBase::EFFECT_HALLOWEEN_CANDY_EQUIP);
PyModule_AddIntConstant(poModule, "EFFECT_HAPPINESS_RING_EQUIP", CInstanceBase::EFFECT_HAPPINESS_RING_EQUIP);
PyModule_AddIntConstant(poModule, "EFFECT_LOVE_PENDANT_EQUIP", CInstanceBase::EFFECT_LOVE_PENDANT_EQUIP);
PyModule_AddIntConstant(poModule, "EFFECT_SASH_SUCCEDED", CInstanceBase::EFFECT_SASH_SUCCEDED); // PORT: AVA2 sash
PyModule_AddIntConstant(poModule, "EFFECT_SASH_EQUIP", CInstanceBase::EFFECT_SASH_EQUIP);
PyModule_AddIntConstant(poModule, "EFFECT_SASH", CInstanceBase::EFFECT_SASH); // refined index, EFFECT_REFINED + EFFECT_SASH
}
@@ -84,6 +84,7 @@ PyObject * chrCreateInstance(PyObject* poSelf, PyObject* poArgs)
kCreateData.m_dwArmor=8;
kCreateData.m_dwWeapon=0;
kCreateData.m_dwHair=0;
kCreateData.m_dwSash=0; // PORT: AVA2 sash
kCreateData.m_isMain=false;
PyObject* poHorse=PyDict_GetItemString(poDict, "horse");
@@ -533,6 +534,21 @@ PyObject * chrRefresh(PyObject* poSelf, PyObject* poArgs)
return Py_BuildNone();
}
// PORT: AVA2 PythonCharacterModule.cpp chrSetSash (ENABLE_SASH_SYSTEM)
PyObject * chrSetSash(PyObject* poSelf, PyObject* poArgs)
{
int dwSash;
if (!PyTuple_GetInteger(poArgs, 0, &dwSash))
return Py_BuildException();
CInstanceBase * pkInst = CPythonCharacterManager::Instance().GetSelectedInstancePtr();
if (!pkInst)
return Py_BuildNone();
pkInst->SetSash(dwSash);
return Py_BuildNone();
}
PyObject * chrMountHorse(PyObject* poSelf, PyObject* poArgs)
{
CInstanceBase * pkInst = CPythonCharacterManager::Instance().GetSelectedInstancePtr();
@@ -1211,6 +1227,7 @@ PyObject * chrtestSetRideMan(PyObject* poSelf, PyObject* poArgs)
kCreateData.m_dwRace = 0;
kCreateData.m_dwArmor = 0;
kCreateData.m_dwHair = 100;
kCreateData.m_dwSash = 0; // PORT: AVA2 sash
kCreateData.m_dwMovSpd = 100;
kCreateData.m_dwAtkSpd = 100;
kCreateData.m_dwMountVnum = imount;
@@ -1227,6 +1244,7 @@ void initchr()
{
{ "DismountHorse", chrDismountHorse, METH_VARARGS },
{ "MountHorse", chrMountHorse, METH_VARARGS },
{ "SetSash", chrSetSash, METH_VARARGS }, // PORT: AVA2 sash
{ "Destroy", chrDestroy, METH_VARARGS },
{ "Update", chrUpdate, METH_VARARGS },
@@ -1438,6 +1456,7 @@ void initchr()
PyModule_AddIntConstant(poModule, "PART_WEAPON", CRaceData::PART_WEAPON);
PyModule_AddIntConstant(poModule, "PART_HEAD", CRaceData::PART_HEAD);
PyModule_AddIntConstant(poModule, "PART_WEAPON_LEFT", CRaceData::PART_WEAPON_LEFT);
PyModule_AddIntConstant(poModule, "PART_SASH", CRaceData::PART_SASH); // PORT: AVA2 sash
/////
@@ -640,6 +640,12 @@ void initItem()
PyModule_AddIntConstant(poModule, "COSTUME_SLOT_BODY", c_Costume_Slot_Body);
PyModule_AddIntConstant(poModule, "COSTUME_SLOT_HAIR", c_Costume_Slot_Hair);
PyModule_AddIntConstant(poModule, "COSTUME_SLOT_END", c_Costume_Slot_End);
PyModule_AddIntConstant(poModule, "COSTUME_TYPE_SASH", CItemData::COSTUME_SASH); // PORT: AVA2 sash
PyModule_AddIntConstant(poModule, "COSTUME_SLOT_SASH", c_Costume_Slot_Sash);
PyModule_AddIntConstant(poModule, "COSTUME_TYPE_MOUNT", CItemData::COSTUME_MOUNT); // PORT: AVA2 mount costume
PyModule_AddIntConstant(poModule, "COSTUME_TYPE_MOUNT_SKIN", CItemData::COSTUME_MOUNT_SKIN);
PyModule_AddIntConstant(poModule, "COSTUME_SLOT_MOUNT", c_Costume_Slot_Mount);
PyModule_AddIntConstant(poModule, "COSTUME_SLOT_MOUNT_SKIN", c_Costume_Slot_MountSkin);
#endif
#ifdef ENABLE_NEW_EQUIPMENT_SYSTEM
@@ -754,6 +760,11 @@ void initItem()
PyModule_AddIntConstant(poModule, "ARMOR_FOOTS", CItemData::ARMOR_FOOTS);
PyModule_AddIntConstant(poModule, "ARMOR_NECK", CItemData::ARMOR_NECK);
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);
@@ -852,4 +863,12 @@ void initItem()
PyModule_AddIntConstant(poModule, "APPLY_RESIST_DARK", CItemData::APPLY_RESIST_DARK );
PyModule_AddIntConstant(poModule, "APPLY_ANTI_CRITICAL_PCT", CItemData::APPLY_ANTI_CRITICAL_PCT );
PyModule_AddIntConstant(poModule, "APPLY_ANTI_PENETRATE_PCT", CItemData::APPLY_ANTI_PENETRATE_PCT );
PyModule_AddIntConstant(poModule, "APPLY_ATTBONUS_STONE", CItemData::APPLY_ATTBONUS_STONE );
PyModule_AddIntConstant(poModule, "APPLY_ATTBONUS_BOSS", CItemData::APPLY_ATTBONUS_BOSS );
PyModule_AddIntConstant(poModule, "APPLY_ENCHANT_ELEC", CItemData::APPLY_ENCHANT_ELEC );
PyModule_AddIntConstant(poModule, "APPLY_ENCHANT_FIRE", CItemData::APPLY_ENCHANT_FIRE );
PyModule_AddIntConstant(poModule, "APPLY_ENCHANT_ICE", CItemData::APPLY_ENCHANT_ICE );
PyModule_AddIntConstant(poModule, "APPLY_ENCHANT_WIND", CItemData::APPLY_ENCHANT_WIND );
PyModule_AddIntConstant(poModule, "APPLY_ENCHANT_EARTH", CItemData::APPLY_ENCHANT_EARTH );
PyModule_AddIntConstant(poModule, "APPLY_ENCHANT_DARK", CItemData::APPLY_ENCHANT_DARK );
}
@@ -184,6 +184,7 @@ class CMainPacketHeaderMap : public CNetworkPacketHeaderMap
Set(HEADER_GC_HYBRIDCRYPT_SDB, CNetworkPacketHeaderMap::TPacketType(sizeof(TPacketGCHybridSDB), DYNAMIC_SIZE_PACKET));
Set(HEADER_GC_SPECIFIC_EFFECT, CNetworkPacketHeaderMap::TPacketType(sizeof(TPacketGCSpecificEffect), STATIC_SIZE_PACKET));
Set(HEADER_GC_DRAGON_SOUL_REFINE, CNetworkPacketHeaderMap::TPacketType(sizeof(TPacketGCDragonSoulRefine), STATIC_SIZE_PACKET));
Set(HEADER_GC_SASH, CNetworkPacketHeaderMap::TPacketType(sizeof(TPacketSash), STATIC_SIZE_PACKET)); // PORT: AVA2 sash
}
};
@@ -267,6 +267,12 @@ class CPythonNetworkStream : public CNetworkStream, public CSingleton<CPythonNet
// 용홍석 강화
bool SendDragonSoulRefinePacket(BYTE bRefineType, TItemPos* pos);
// PORT: AVA2 sash
bool SendSashClosePacket();
bool SendSashAddPacket(TItemPos tPos, BYTE bPos);
bool SendSashRemovePacket(BYTE bPos);
bool SendSashRefinePacket();
// Handshake
bool RecvHandshakePacket();
bool RecvHandshakeOKPacket();
@@ -545,6 +551,7 @@ class CPythonNetworkStream : public CNetworkStream, public CSingleton<CPythonNet
// 용혼석 관련
bool RecvDragonSoulRefine();
bool RecvSashPacket(bool bReturn = false); // PORT: AVA2 sash
// MiniMap Info
bool RecvNPCList();
@@ -630,6 +630,10 @@ void CPythonNetworkStream::GamePhase()
ret = RecvDragonSoulRefine();
break;
case HEADER_GC_SASH: // PORT: AVA2 sash
ret = RecvSashPacket();
break;
default:
ret = RecvDefaultPacket(header);
break;
@@ -113,6 +113,7 @@ bool CPythonNetworkStream::RecvCharacterAppendPacket()
kNetActorData.m_dwArmor=0;/*chrAddPacket.awPart[CHR_EQUIPPART_ARMOR]*/;
kNetActorData.m_dwWeapon=0;/*chrAddPacket.awPart[CHR_EQUIPPART_WEAPON]*/;
kNetActorData.m_dwHair=0;/*chrAddPacket.awPart[CHR_EQUIPPART_HAIR]*/;
kNetActorData.m_dwSash=0; // PORT: AVA2 sash
kNetActorData.m_dwMountVnum=0;/*chrAddPacket.dwMountVnum*/;
kNetActorData.m_dwLevel = 0; // 몬스터 레벨 표시 안함
@@ -160,6 +161,7 @@ bool CPythonNetworkStream::RecvCharacterAdditionalInfo()
kNetActorData.m_dwArmor=chrInfoPacket.awPart[CHR_EQUIPPART_ARMOR];
kNetActorData.m_dwWeapon=chrInfoPacket.awPart[CHR_EQUIPPART_WEAPON];
kNetActorData.m_dwHair=chrInfoPacket.awPart[CHR_EQUIPPART_HAIR];
kNetActorData.m_dwSash=chrInfoPacket.awPart[CHR_EQUIPPART_SASH]; // PORT: AVA2 sash
kNetActorData.m_dwMountVnum=chrInfoPacket.dwMountVnum;
__RecvCharacterAppendPacket(&kNetActorData);
@@ -193,6 +195,7 @@ bool CPythonNetworkStream::RecvCharacterAppendPacketNew()
kNetActorData.m_dwArmor=chrAddPacket.awPart[CHR_EQUIPPART_ARMOR];
kNetActorData.m_dwWeapon=chrAddPacket.awPart[CHR_EQUIPPART_WEAPON];
kNetActorData.m_dwHair=chrAddPacket.awPart[CHR_EQUIPPART_HAIR];
kNetActorData.m_dwSash=chrAddPacket.awPart[CHR_EQUIPPART_SASH]; // PORT: AVA2 sash
kNetActorData.m_dwStateFlags=chrAddPacket.bStateFlag;
kNetActorData.m_dwVID=chrAddPacket.dwVID;
kNetActorData.m_dwMountVnum=chrAddPacket.dwMountVnum;
@@ -221,6 +224,7 @@ bool CPythonNetworkStream::RecvCharacterUpdatePacket()
kNetUpdateActorData.m_dwArmor=chrUpdatePacket.awPart[CHR_EQUIPPART_ARMOR];
kNetUpdateActorData.m_dwWeapon=chrUpdatePacket.awPart[CHR_EQUIPPART_WEAPON];
kNetUpdateActorData.m_dwHair=chrUpdatePacket.awPart[CHR_EQUIPPART_HAIR];
kNetUpdateActorData.m_dwSash=chrUpdatePacket.awPart[CHR_EQUIPPART_SASH]; // PORT: AVA2 sash
kNetUpdateActorData.m_dwVID=chrUpdatePacket.dwVID;
kNetUpdateActorData.m_kAffectFlags.CopyData(0, sizeof(chrUpdatePacket.dwAffectFlag[0]), &chrUpdatePacket.dwAffectFlag[0]);
kNetUpdateActorData.m_kAffectFlags.CopyData(32, sizeof(chrUpdatePacket.dwAffectFlag[1]), &chrUpdatePacket.dwAffectFlag[1]);
@@ -246,6 +250,7 @@ bool CPythonNetworkStream::RecvCharacterUpdatePacketNew()
kNetUpdateActorData.m_dwArmor=chrUpdatePacket.awPart[CHR_EQUIPPART_ARMOR];
kNetUpdateActorData.m_dwWeapon=chrUpdatePacket.awPart[CHR_EQUIPPART_WEAPON];
kNetUpdateActorData.m_dwHair=chrUpdatePacket.awPart[CHR_EQUIPPART_HAIR];
kNetUpdateActorData.m_dwSash=chrUpdatePacket.awPart[CHR_EQUIPPART_SASH]; // PORT: AVA2 sash
kNetUpdateActorData.m_dwVID=chrUpdatePacket.dwVID;
kNetUpdateActorData.m_kAffectFlags.CopyData(0, sizeof(chrUpdatePacket.dwAffectFlag[0]), &chrUpdatePacket.dwAffectFlag[0]);
kNetUpdateActorData.m_kAffectFlags.CopyData(32, sizeof(chrUpdatePacket.dwAffectFlag[1]), &chrUpdatePacket.dwAffectFlag[1]);
@@ -858,6 +858,13 @@ bool CPythonNetworkStream::RecvSpecialEffect()
case SE_EQUIP_LOVE_PENDANT:
effect = CInstanceBase::EFFECT_LOVE_PENDANT_EQUIP;
break;
// PORT: AVA2 sash
case SE_EFFECT_SASH_SUCCEDED:
effect = CInstanceBase::EFFECT_SASH_SUCCEDED;
break;
case SE_EFFECT_SASH_EQUIP:
effect = CInstanceBase::EFFECT_SASH_EQUIP;
break;
default:
@@ -0,0 +1,159 @@
#include "StdAfx.h"
#include "PythonNetworkStream.h"
#include "PythonSash.h"
#include "Packet.h"
// PORT: AVA2 PythonNetworkStreamPhaseGame.cpp sash packets (ENABLE_SASH_SYSTEM) verbatim.
bool CPythonNetworkStream::RecvSashPacket(bool bReturn)
{
TPacketSash sPacket;
if (!Recv(sizeof(sPacket), &sPacket))
return bReturn;
bReturn = true;
switch (sPacket.subheader)
{
case SASH_SUBHEADER_GC_OPEN:
{
CPythonSash::Instance().Clear();
PyCallClassMemberFunc(m_apoPhaseWnd[PHASE_WINDOW_GAME], "ActSash", Py_BuildValue("(ib)", 1, sPacket.bWindow));
}
break;
case SASH_SUBHEADER_GC_CLOSE:
{
CPythonSash::Instance().Clear();
PyCallClassMemberFunc(m_apoPhaseWnd[PHASE_WINDOW_GAME], "ActSash", Py_BuildValue("(ib)", 2, sPacket.bWindow));
}
break;
case SASH_SUBHEADER_GC_ADDED:
{
CPythonSash::Instance().AddMaterial(sPacket.dwPrice, sPacket.bPos, sPacket.tPos);
if (sPacket.bPos == 1)
{
CPythonSash::Instance().AddResult(sPacket.dwItemVnum, sPacket.dwMinAbs, sPacket.dwMaxAbs);
PyCallClassMemberFunc(m_apoPhaseWnd[PHASE_WINDOW_GAME], "AlertSash", Py_BuildValue("(b)", sPacket.bWindow));
}
PyCallClassMemberFunc(m_apoPhaseWnd[PHASE_WINDOW_GAME], "ActSash", Py_BuildValue("(ib)", 3, sPacket.bWindow));
}
break;
case SASH_SUBHEADER_GC_REMOVED:
{
CPythonSash::Instance().RemoveMaterial(sPacket.dwPrice, sPacket.bPos);
if (sPacket.bPos == 0)
CPythonSash::Instance().RemoveMaterial(sPacket.dwPrice, 1);
PyCallClassMemberFunc(m_apoPhaseWnd[PHASE_WINDOW_GAME], "ActSash", Py_BuildValue("(ib)", 4, sPacket.bWindow));
}
break;
case SASH_SUBHEADER_CG_REFINED:
{
if (sPacket.dwMaxAbs == 0)
CPythonSash::Instance().RemoveMaterial(sPacket.dwPrice, 1);
else
{
CPythonSash::Instance().RemoveMaterial(sPacket.dwPrice, 0);
CPythonSash::Instance().RemoveMaterial(sPacket.dwPrice, 1);
}
PyCallClassMemberFunc(m_apoPhaseWnd[PHASE_WINDOW_GAME], "ActSash", Py_BuildValue("(ib)", 4, sPacket.bWindow));
}
break;
default:
TraceError("CPythonNetworkStream::RecvSashPacket: unknown subheader %d\n.", sPacket.subheader);
break;
}
return bReturn;
}
bool CPythonNetworkStream::SendSashClosePacket()
{
if (!__CanActMainInstance())
return true;
TItemPos tPos;
tPos.window_type = INVENTORY;
tPos.cell = 0;
TPacketSash sPacket;
sPacket.header = HEADER_CG_SASH;
sPacket.subheader = SASH_SUBHEADER_CG_CLOSE;
sPacket.dwPrice = 0;
sPacket.bPos = 0;
sPacket.tPos = tPos;
sPacket.dwItemVnum = 0;
sPacket.dwMinAbs = 0;
sPacket.dwMaxAbs = 0;
if (!Send(sizeof(sPacket), &sPacket))
return false;
return SendSequence();
}
bool CPythonNetworkStream::SendSashAddPacket(TItemPos tPos, BYTE bPos)
{
if (!__CanActMainInstance())
return true;
TPacketSash sPacket;
sPacket.header = HEADER_CG_SASH;
sPacket.subheader = SASH_SUBHEADER_CG_ADD;
sPacket.dwPrice = 0;
sPacket.bPos = bPos;
sPacket.tPos = tPos;
sPacket.dwItemVnum = 0;
sPacket.dwMinAbs = 0;
sPacket.dwMaxAbs = 0;
if (!Send(sizeof(sPacket), &sPacket))
return false;
return SendSequence();
}
bool CPythonNetworkStream::SendSashRemovePacket(BYTE bPos)
{
if (!__CanActMainInstance())
return true;
TItemPos tPos;
tPos.window_type = INVENTORY;
tPos.cell = 0;
TPacketSash sPacket;
sPacket.header = HEADER_CG_SASH;
sPacket.subheader = SASH_SUBHEADER_CG_REMOVE;
sPacket.dwPrice = 0;
sPacket.bPos = bPos;
sPacket.tPos = tPos;
sPacket.dwItemVnum = 0;
sPacket.dwMinAbs = 0;
sPacket.dwMaxAbs = 0;
if (!Send(sizeof(sPacket), &sPacket))
return false;
return SendSequence();
}
bool CPythonNetworkStream::SendSashRefinePacket()
{
if (!__CanActMainInstance())
return true;
TItemPos tPos;
tPos.window_type = INVENTORY;
tPos.cell = 0;
TPacketSash sPacket;
sPacket.header = HEADER_CG_SASH;
sPacket.subheader = SASH_SUBHEADER_CG_REFINE;
sPacket.dwPrice = 0;
sPacket.bPos = 0;
sPacket.tPos = tPos;
sPacket.dwItemVnum = 0;
sPacket.dwMinAbs = 0;
sPacket.dwMaxAbs = 0;
if (!Send(sizeof(sPacket), &sPacket))
return false;
return SendSequence();
}
+28
View File
@@ -422,6 +422,34 @@ void CPythonPlayer::__UpdateBattleStatus()
m_playerStatus.SetPoint(POINT_MAX_MAGIC_WEP, m_dwWeaponMaxMagicPower+m_dwWeaponAddPower);
m_playerStatus.SetPoint(POINT_MIN_ATK, __GetTotalAtk(m_dwWeaponMinPower, m_dwWeaponAddPower));
m_playerStatus.SetPoint(POINT_MAX_ATK, __GetTotalAtk(m_dwWeaponMaxPower, m_dwWeaponAddPower));
// PORT: AVA2 PythonPlayer.cpp (ENABLE_SASH_SYSTEM): an equipped sash that absorbed a weapon adds
// (value3|4 + value5) and magic (value1|2 + value5) x absorption% to the shown attack, as the
// server adds it in Item_GetDamage / SetPolyVarForAttack.
const TItemData * pSashData = GetItemData(TItemPos(INVENTORY, c_Costume_Slot_Sash));
if (pSashData)
{
long lAbsorbedVnum = pSashData->alSockets[SASH_ABSORBED_SOCKET];
CItemData * pAbsorbed;
if (lAbsorbedVnum != 0 && CItemManager::Instance().GetItemDataPointer(lAbsorbedVnum, &pAbsorbed))
{
if (pAbsorbed->GetType() == CItemData::ITEM_TYPE_WEAPON)
{
double dAbs = static_cast<double>(pSashData->alSockets[SASH_ABSORPTION_SOCKET]) / 100.0;
long lMinWep = static_cast<long>((pAbsorbed->GetValue(3) + pAbsorbed->GetValue(5)) * dAbs + 0.5);
long lMaxWep = static_cast<long>((pAbsorbed->GetValue(4) + pAbsorbed->GetValue(5)) * dAbs + 0.5);
long lMinMag = static_cast<long>((pAbsorbed->GetValue(1) + pAbsorbed->GetValue(5)) * dAbs + 0.5);
long lMaxMag = static_cast<long>((pAbsorbed->GetValue(2) + pAbsorbed->GetValue(5)) * dAbs + 0.5);
m_playerStatus.SetPoint(POINT_MIN_WEP, m_playerStatus.GetPoint(POINT_MIN_WEP) + lMinWep);
m_playerStatus.SetPoint(POINT_MAX_WEP, m_playerStatus.GetPoint(POINT_MAX_WEP) + lMaxWep);
m_playerStatus.SetPoint(POINT_MIN_MAGIC_WEP, m_playerStatus.GetPoint(POINT_MIN_MAGIC_WEP) + lMinMag);
m_playerStatus.SetPoint(POINT_MAX_MAGIC_WEP, m_playerStatus.GetPoint(POINT_MAX_MAGIC_WEP) + lMaxMag);
m_playerStatus.SetPoint(POINT_MIN_ATK, m_playerStatus.GetPoint(POINT_MIN_ATK) + lMinWep);
m_playerStatus.SetPoint(POINT_MAX_ATK, m_playerStatus.GetPoint(POINT_MAX_ATK) + lMaxWep);
}
}
}
}
void CPythonPlayer::SetStatus(DWORD dwType, long lValue)
+189
View File
@@ -0,0 +1,189 @@
#include "StdAfx.h"
// PORT: AVA2 PythonSash.cpp (ENABLE_SASH_SYSTEM) verbatim; the sash module is driven by mt_sash.
#include "PythonSash.h"
#include "PythonNetworkStream.h"
CPythonSash::CPythonSash()
{
Clear();
}
CPythonSash::~CPythonSash()
{
Clear();
}
void CPythonSash::Clear()
{
dwPrice = 0;
ZeroMemory(&m_vSashResult, sizeof(m_vSashResult));
m_vSashMaterials.clear();
m_vSashMaterials.resize(SASH_WINDOW_MAX_MATERIALS);
for (BYTE bPos = 0; bPos < m_vSashMaterials.size(); ++bPos)
{
TSashMaterial tMaterial;
tMaterial.bHere = 0;
tMaterial.wCell = 0;
m_vSashMaterials[bPos] = tMaterial;
}
}
void CPythonSash::AddMaterial(DWORD dwRefPrice, BYTE bPos, TItemPos tPos)
{
if (bPos >= SASH_WINDOW_MAX_MATERIALS)
return;
if (bPos == 0)
dwPrice = dwRefPrice;
TSashMaterial tMaterial;
tMaterial.bHere = 1;
tMaterial.wCell = tPos.cell;
m_vSashMaterials[bPos] = tMaterial;
}
void CPythonSash::AddResult(DWORD dwItemVnum, DWORD dwMinAbs, DWORD dwMaxAbs)
{
TSashResult tResult;
tResult.dwItemVnum = dwItemVnum;
tResult.dwMinAbs = dwMinAbs;
tResult.dwMaxAbs = dwMaxAbs;
m_vSashResult = tResult;
}
void CPythonSash::RemoveMaterial(DWORD dwRefPrice, BYTE bPos)
{
if (bPos >= SASH_WINDOW_MAX_MATERIALS)
return;
if (bPos == 1)
dwPrice = dwRefPrice;
TSashMaterial tMaterial;
tMaterial.bHere = 0;
tMaterial.wCell = 0;
m_vSashMaterials[bPos] = tMaterial;
}
bool CPythonSash::GetAttachedItem(BYTE bPos, BYTE & bHere, WORD & wCell)
{
if (bPos >= SASH_WINDOW_MAX_MATERIALS)
return false;
bHere = m_vSashMaterials[bPos].bHere;
wCell = m_vSashMaterials[bPos].wCell;
return true;
}
void CPythonSash::GetResultItem(DWORD & dwItemVnum, DWORD & dwMinAbs, DWORD & dwMaxAbs)
{
dwItemVnum = m_vSashResult.dwItemVnum;
dwMinAbs = m_vSashResult.dwMinAbs;
dwMaxAbs = m_vSashResult.dwMaxAbs;
}
PyObject * SendSashCloseRequest(PyObject * poSelf, PyObject * poArgs)
{
CPythonNetworkStream & rkNetStream = CPythonNetworkStream::Instance();
rkNetStream.SendSashClosePacket();
return Py_BuildNone();
}
PyObject * SendSashAdd(PyObject * poSelf, PyObject * poArgs)
{
BYTE bPos;
TItemPos tPos;
if (!PyTuple_GetInteger(poArgs, 0, &tPos.window_type))
return Py_BuildException();
else if (!PyTuple_GetInteger(poArgs, 1, &tPos.cell))
return Py_BuildException();
else if (!PyTuple_GetInteger(poArgs, 2, &bPos))
return Py_BuildException();
CPythonNetworkStream & rkNetStream = CPythonNetworkStream::Instance();
rkNetStream.SendSashAddPacket(tPos, bPos);
return Py_BuildNone();
}
PyObject * SendSashRemove(PyObject * poSelf, PyObject * poArgs)
{
BYTE bPos;
if (!PyTuple_GetInteger(poArgs, 0, &bPos))
return Py_BuildException();
CPythonNetworkStream & rkNetStream = CPythonNetworkStream::Instance();
rkNetStream.SendSashRemovePacket(bPos);
return Py_BuildNone();
}
PyObject * GetSashPrice(PyObject * poSelf, PyObject * poArgs)
{
return Py_BuildValue("i", CPythonSash::Instance().GetPrice());
}
PyObject * GetSashAttachedItem(PyObject * poSelf, PyObject * poArgs)
{
BYTE bPos;
if (!PyTuple_GetInteger(poArgs, 0, &bPos))
return Py_BuildException();
BYTE bHere;
WORD wCell;
bool bGet = CPythonSash::Instance().GetAttachedItem(bPos, bHere, wCell);
if (!bGet)
{
bHere = 0;
wCell = 0;
}
return Py_BuildValue("ii", bHere, wCell);
}
PyObject * GetSashResultItem(PyObject * poSelf, PyObject * poArgs)
{
DWORD dwItemVnum, dwMinAbs, dwMaxAbs;
CPythonSash::Instance().GetResultItem(dwItemVnum, dwMinAbs, dwMaxAbs);
return Py_BuildValue("iii", dwItemVnum, dwMinAbs, dwMaxAbs);
}
PyObject * SendSashRefineRequest(PyObject * poSelf, PyObject * poArgs)
{
BYTE bHere;
WORD wCell;
bool bGet = CPythonSash::Instance().GetAttachedItem(1, bHere, wCell);
if (bGet)
{
if (bHere)
{
CPythonNetworkStream & rkNetStream = CPythonNetworkStream::Instance();
rkNetStream.SendSashRefinePacket();
}
}
return Py_BuildNone();
}
void initSash()
{
static PyMethodDef functions[] = {
{"SendCloseRequest", SendSashCloseRequest, METH_VARARGS},
{"Add", SendSashAdd, METH_VARARGS},
{"Remove", SendSashRemove, METH_VARARGS},
{"GetPrice", GetSashPrice, METH_VARARGS},
{"GetAttachedItem", GetSashAttachedItem, METH_VARARGS},
{"GetResultItem", GetSashResultItem, METH_VARARGS},
{"SendRefineRequest", SendSashRefineRequest, METH_VARARGS},
{NULL, NULL, NULL},
};
PyObject* pModule = Py_InitModule("sash", functions);
PyModule_AddIntConstant(pModule, "ABSORPTION_SOCKET", SASH_ABSORPTION_SOCKET);
PyModule_AddIntConstant(pModule, "ABSORBED_SOCKET", SASH_ABSORBED_SOCKET);
PyModule_AddIntConstant(pModule, "WINDOW_MAX_MATERIALS", SASH_WINDOW_MAX_MATERIALS);
PyModule_AddIntConstant(pModule, "CLEAN_ATTR_VALUE_FIELD", 0);
PyModule_AddIntConstant(pModule, "LIMIT_RANGE", 1000);
}
+27
View File
@@ -0,0 +1,27 @@
#pragma once
// PORT: AVA2 sash window state (ENABLE_SASH_SYSTEM), fed by HEADER_GC_SASH.
#include "Packet.h"
class CPythonSash : public CSingleton<CPythonSash>
{
public:
DWORD dwPrice;
typedef std::vector<TSashMaterial> TSashMaterials;
public:
CPythonSash();
virtual ~CPythonSash();
void Clear();
void AddMaterial(DWORD dwRefPrice, BYTE bPos, TItemPos tPos);
void AddResult(DWORD dwItemVnum, DWORD dwMinAbs, DWORD dwMaxAbs);
void RemoveMaterial(DWORD dwRefPrice, BYTE bPos);
DWORD GetPrice() {return dwPrice;}
bool GetAttachedItem(BYTE bPos, BYTE & bHere, WORD & wCell);
void GetResultItem(DWORD & dwItemVnum, DWORD & dwMinAbs, DWORD & dwMaxAbs);
protected:
TSashResult m_vSashResult;
TSashMaterials m_vSashMaterials;
};
+1
View File
@@ -68,6 +68,7 @@ void initskill();
void initBackground();
void initMessenger();
void initsafebox();
void initSash(); // PORT: AVA2 sash
void initguild();
void initServerStateChecker();
void initRenderTarget(); // PORT: AVA2 CPythonRenderTargetModule
+4 -4
View File
@@ -43,7 +43,7 @@ inline constexpr int ID_MAX_NUM = 30; // Packet.h -> login[31]
inline constexpr int PASS_MAX_NUM = 16; // -> passwd[17]
inline constexpr int PRIVATE_CODE_LENGTH = 8;
inline constexpr int POINT_MAX_NUM = 255; // common/length.h:58
inline constexpr int CHR_EQUIPPART_NUM = 4; // ARMOR/WEAPON/HEAD/HAIR
inline constexpr int CHR_EQUIPPART_NUM = 5; // ARMOR/WEAPON/HEAD/HAIR + AVA2 SASH (metin2-server P4)
inline constexpr int LOGIN_STATUS_MAX_LEN = 8; // -> szStatus[9]
inline constexpr int KEY_AGREEMENT_MAX_DATA_LEN = 256;
inline constexpr int ITEM_SOCKET_MAX_NUM = 3; // common/item_length.h
@@ -645,7 +645,7 @@ struct GCCharacterAdd2 { // packet_add_char2 — name + parts + guild
uint32_t mount_vnum;
};
static_assert(sizeof(GCCharacterAdd2) ==
1 + 4 + (CHARACTER_NAME_MAX_LEN + 1) + 4 + 12 + 1 + 2 + 8 + 1 + 1 + 1 + 8 + 1 + 4 + 2 + 1 + 4); // 120
1 + 4 + (CHARACTER_NAME_MAX_LEN + 1) + 4 + 12 + 1 + 2 + 2 * CHR_EQUIPPART_NUM + 1 + 1 + 1 + 8 + 1 + 4 + 2 + 1 + 4); // 122
struct GCCharacterDel { uint8_t header; uint32_t vid; };
static_assert(sizeof(GCCharacterDel) == 5);
@@ -873,7 +873,7 @@ struct GCCharacterUpdate { // packet_update_char (header 19)
uint8_t pk_mode;
uint32_t mount_vnum;
};
static_assert(sizeof(GCCharacterUpdate) == 1 + 4 + 8 + 1 + 1 + 1 + 8 + 4 + 2 + 1 + 4); // 35
static_assert(sizeof(GCCharacterUpdate) == 1 + 4 + 2 * CHR_EQUIPPART_NUM + 1 + 1 + 1 + 8 + 4 + 2 + 1 + 4); // 37
struct GCStun { uint8_t header; uint32_t vid; }; // header 13
struct GCDead { uint8_t header; uint32_t vid; }; // header 14
@@ -1036,7 +1036,7 @@ struct GCCharAddInfo {
uint8_t pk_mode;
uint32_t mount_vnum;
};
static_assert(sizeof(GCCharAddInfo) == 1 + 4 + (CHARACTER_NAME_MAX_LEN + 1) + 8 + 1 + 4 + 4 + 2 + 1 + 4); // 54
static_assert(sizeof(GCCharAddInfo) == 1 + 4 + (CHARACTER_NAME_MAX_LEN + 1) + 2 * CHR_EQUIPPART_NUM + 1 + 4 + 4 + 2 + 1 + 4); // 56
// --- GC skills / quickslots ---
struct PlayerSkill3 { // TPlayerSkill (common/tables.h) — time_t is 32-bit on the -m32 server
+1 -1
View File
@@ -41,7 +41,7 @@ class SwitchbotTests(unittest.TestCase):
mods['net'].SendChatPacket=self.sent.append
mods['game'].GameWindow=type('GameWindow',(),dict(Open=lambda s:None,Close=lambda s:None,BINARY_ServerCommand_Run=lambda s,line:7))
grp=mods['grp'];grp.SetColor=grp.RenderBar=grp.RenderBox=lambda *a:None
it=mods['item'];it.ITEM_TYPE_WEAPON=1;it.ITEM_TYPE_ARMOR=2;it.ITEM_TYPE_USE=3;self.selected=None
it=mods['item'];it.ITEM_TYPE_WEAPON=1;it.ITEM_TYPE_ARMOR=2;it.ITEM_TYPE_USE=3;it.ARMOR_PENDANT=7;self.selected=None
it.SelectItem=lambda v:setattr(self,'selected',v);it.GetItemType=lambda:protos[self.selected][0];it.GetItemSubType=lambda:protos[self.selected][1]
it.GetItemName=lambda:'N%d'%self.selected
pl=mods['player'];pl.INVENTORY=1;pl.INVENTORY_PAGE_SIZE=45;pl.INVENTORY_PAGE_COUNT=5
+16 -7
View File
@@ -1,18 +1,27 @@
#!/usr/bin/env python3
"""Install items-server.tsv (from make_items.py) into share/conf/item_proto.txt + item_names.txt.
Rows with the same vnum are replaced, so a rerun is idempotent. The db reads the tables only at boot:
restart db and every core afterwards. Usage: install_items.py <server root> <items-server.tsv> <fresh backup dir>
restart db and every core afterwards. mobs-server.tsv (optional 4th arg) goes into mob_proto.txt + mob_names.txt
the same way. Usage: install_items.py <server root> <items-server.tsv> <fresh backup dir> [<mobs-server.tsv>]
"""
import shutil,sys
from pathlib import Path
server,rows,backup=map(Path,sys.argv[1:4])
mobrows=Path(sys.argv[4]) if len(sys.argv)>4 else None
if backup.exists():raise SystemExit('use a fresh backup directory')
items={int(l.split(b'\t',1)[0]):l for l in rows.read_bytes().splitlines() if l}
load=lambda p:{int(l.split(b'\t',1)[0]):l for l in p.read_bytes().splitlines() if l}
backup.mkdir(parents=True)
conf=server/'share/conf'
for filename,row in (('item_proto.txt',lambda v:items[v]),('item_names.txt',lambda v:str(v).encode()+b'\t'+items[v].split(b'\t')[1])):
path=conf/filename;raw=path.read_bytes();shutil.copy2(path,backup/filename)
newline=b'\r\n' if b'\r\n' in raw else b'\n'
kept=[l for l in raw.replace(b'\r\n',b'\n').split(b'\n') if l and not (l.split(b'\t',1)[0].isdigit() and int(l.split(b'\t',1)[0]) in items)]
tmp=path.with_name(filename+'.tmp');tmp.write_bytes(newline.join(kept+[row(v) for v in sorted(items)])+newline);tmp.replace(path)
def install(proto,names,rows):
for filename,row in ((proto,lambda v:rows[v]),(names,lambda v:str(v).encode()+b'\t'+rows[v].split(b'\t')[1])):
path=conf/filename;raw=path.read_bytes();shutil.copy2(path,backup/filename)
newline=b'\r\n' if b'\r\n' in raw else b'\n'
kept=[l for l in raw.replace(b'\r\n',b'\n').split(b'\n') if l and not (l.split(b'\t',1)[0].isdigit() and int(l.split(b'\t',1)[0]) in rows)]
tmp=path.with_name(filename+'.tmp');tmp.write_bytes(newline.join(kept+[row(v) for v in sorted(rows)])+newline);tmp.replace(path)
items=load(rows);install('item_proto.txt','item_names.txt',items)
print('AVA2 items installed:',len(items),'backup:',backup)
if mobrows:
mobs=load(mobrows);install('mob_proto.txt','mob_names.txt',mobs)
print('AVA2 mobs installed:',len(mobs))
+95 -9
View File
@@ -1,18 +1,54 @@
#!/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, mounts, sashes, costume enchants, talisman bonus items, new skill books) become plain ITEM_NONE
items until those systems are ported. 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
HERE=Path(__file__).resolve().parent
RANGES=((54001,54027,'时装强化石'),(54028,54078,'发型强化石'),(54079,54123,'武器外观强化石'),
(54124,54174,'坐骑外观强化石'),(54175,54219,'披风外观强化石'),(54220,54267,'特效强化石'))
# Dragon armor: +0..+9 per class (the refine chain needs every step).
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 ->
# model stem ('%s' = race); RACE_PATHS = the ShapeData PathName of each race msm.
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'}
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')}
DESC={'ITEM_MATERIAL':'制作与升级材料。','ITEM_QUEST':'任务物品。','ITEM_GIFTBOX':'打开后可获得物品。','ITEM_WEAPON':'','ITEM_ARMOR':''}
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':'披风。可在披风工匠处合成升级,或吸收武器、铠甲的部分属性。',
'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).
SASH_EFFECTS=('d:/ymir work/effect/etc/buff/buff_item6.mse','d:/ymir work/effect/etc/buff/buff_item7.mse',
'd:/ymir work/pc/common/effect/armor/acc_01.mse')
SASH_LOCALE={'locale/en/ui/acce/acce_combine.tga':'locale/zh/ui/acce/acce_combine.tga',
'locale/en/ui/acce/acce_absorb.tga':'locale/zh/ui/acce/acce_absorb.tga'}
# Per-subtype descriptions (override DESC).
# Costume enchants (P6, server port_costume_attr.cpp): only the body / hair / mount-skin ranges have a 40250
# target; AVA2 apply numbers in value0 equal 203's (P2 aligned 139/140). The rest stay locked.
ENCHANT_KEEP=((54001,54078),(54124,54174))
DESC_SUB={('ITEM_USE','USE_CHANGE_ATTRIBUTE'):'拖到未穿戴的对应时装上,附加一条固定属性(每件时装最多 3 条,不能重复)。',
('ITEM_COSTUME','COSTUME_MOUNT'):'坐骑时装。装备后召唤该坐骑(需要已拥有马匹),装备期间属性生效。',
('ITEM_COSTUME','COSTUME_MOUNT_SKIN'):'坐骑外观。需先装备坐骑时装,装备后改变坐骑外观。',
('ITEM_ARMOR','ARMOR_PENDANT'):'护符。装备在护符栏,对相应属性的怪物伤害提高(强化等级 +1 = +1%)。可用护符强化道具重洗属性,用护符加持道具追加属性(最多 5 条)。'}
# Per-vnum descriptions (override DESC_SUB).
DESC_VNUM={53996:'拖到未穿戴的护符上,重新随机全部属性。',71302:'拖到未穿戴的护符上,重新随机全部属性。',
53997:'拖到未穿戴的护符上,追加一条属性(最多 5 条)。',71301:'拖到未穿戴的护符上,追加一条属性(最多 5 条)。'}
# Mount system (P5): races of the mount skins that 203 and the 40250 client lack -> (zh name, npc_mount folder).
# Their AVA2 mob_proto rows go to mobs-server.tsv, the whole model folder into the pack; the client's mt_mount
# script registers the race names (npclist.txt is read from the root pack only).
MOUNT_MOBS={32012:('蓝色巴里','christmas_2017_sleddog'),32028:('白色冬日羊驼','christmas_2020_alpaca')}
LOCKED='该物品所属的系统尚未开放,暂时无法使用。'
def enums(proto_reader):
@@ -38,13 +74,21 @@ def names():
v,n=line.split('\t');result[int(v)]=n
for lo,hi,n in RANGES:
for v in range(lo,hi+1):result[v]=n
for base,n in DRAGON.items():
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):
"""AVA2 item_proto.txt row (str columns) -> (server row columns, client numeric fields, kept?)."""
c=[x.strip() for x in cols[:33]]
t,st=c[2],c[3]
kept=t in KEEP_TYPES or (t,st) in KEEP_SUBTYPES
kept=t in KEEP_TYPES or (t,st) in KEEP_SUBTYPES or ((t,st)==('ITEM_USE','USE_CHANGE_ATTRIBUTE') and
any(lo<=int(c[0])<=hi for lo,hi in ENCHANT_KEEP)) or (
(t,st) in (('ITEM_USE','USE_CHANGE_ATTRIBUTE'),('ITEM_USE','USE_ADD_ATTRIBUTE')) and int(c[0]) in PENDANT_SCROLLS)
anti=tokens(c[5]);flags=tokens(c[6])
if 'ITEM_STACKABLE' in anti:anti.remove('ITEM_STACKABLE');flags.append('ITEM_STACKABLE')
anti=[a for a in dict.fromkeys(anti) if a in E['anti']];flags=[f for f in dict.fromkeys(flags) if f in E['flag']]
@@ -53,7 +97,7 @@ def convert(cols,E,imported):
flags=[f for f in flags if f in ('ITEM_STACKABLE','LOG')]
else:
if t in ('ITEM_QUEST','ITEM_GIFTBOX','ITEM_NONE','ITEM_MATERIAL') and st not in E['sub'][E['type'].index(t)]:st='0'
wear=[w for w in tokens(c[7]) if w in E['wear']] if t in ('ITEM_WEAPON','ITEM_ARMOR') else []
wear=[w for w in tokens(c[7]) if w in E['wear']] if t in ('ITEM_WEAPON','ITEM_ARMOR','ITEM_METIN') else []
imm=[i for i in tokens(c[8]) if i in E['imm']]
applies=[(a,v) if a in E['apply'] else ('APPLY_NONE','0') for a,v in zip(c[18:24:2],c[19:24:2])]
values=[str(number(v)) for v in c[24:30]]
@@ -73,6 +117,30 @@ def convert(cols,E,imported):
client+=[int(v) for v in values]+[number(c[30]),number(c[31])]
return server,client,kept
def shape_models():
"""Pack paths of the ARMOR_SHAPES models (+ LODs) and skins of every race."""
out=[]
for race,dirs in RACE_PATHS.items():
for d in dict.fromkeys(dirs):
for stem in ARMOR_SHAPES.values():
out+=['d:/ymir work/%s/%s/%s%s'%(d,race,stem%race,e) for e in ('.gr2','.dds','_lod_01.gr2','_lod_02.gr2','_lod_03.gr2')]
return out
def effect_files(files):
"""SASH_EFFECTS and every file (mde/textures/models) they name, recursively; names are absolute
(d:/ymir work/...) or relative to the referring file."""
out=[];todo=list(SASH_EFFECTS)
while todo:
q=todo.pop()
if q in out:continue
if q not in files:raise SystemExit('missing AVA2 effect file %s'%q)
out.append(q)
if q.endswith(('.mse','.mde')):
for name in re.findall(rb'[\w \-:./\\]+?\.(?:dds|tga|jpg|mde|gr2|spt|msf)',files[q].read_bytes(),re.I):
name=name.decode('latin1').strip().strip('"').lower().replace('\\','/')
todo.append(name if name.startswith('d:/') else q.rsplit('/',1)[0]+'/'+name.split('/')[-1])
return out
def pack_index(pack_maker):
"""ArchivedPath (lowercase) -> source file, from the AVA2 pack manifests."""
index={}
@@ -117,17 +185,35 @@ def prepare(ava,proto_reader,locale,output,tool,packer,wrap=lambda text:text):
if q not in files:raise SystemExit('missing AVA2 asset %s for %d'%(q,v))
target=assets/q;target.parent.mkdir(parents=True,exist_ok=True)
if not target.exists():shutil.copy2(files[q],target);copied+=1
mount_files=[q for q in files for _,d in MOUNT_MOBS.values() if q.startswith('d:/ymir work/npc_mount/%s/'%d)]
if len({q.split('/')[3] for q in mount_files})!=len(MOUNT_MOBS):raise SystemExit('missing AVA2 mount models')
for q in shape_models()+effect_files(files)+mount_files:
if q not in files:raise SystemExit('missing AVA2 asset %s'%q)
target=assets/q;target.parent.mkdir(parents=True,exist_ok=True)
if not target.exists():shutil.copy2(files[q],target);copied+=1
(output/'items-server.tsv').write_bytes(b'\n'.join(server)+b'\n')
(output/'items-client.tsv').write_bytes(b'\n'.join(client)+b'\n')
mobs={}
for line in (db/'mob_proto.txt').read_bytes().splitlines():
cols=line.split(b'\t')
if cols[0].strip().isdigit() and int(cols[0]) in MOUNT_MOBS:
v=int(cols[0]);cols[1]=MOUNT_MOBS[v][0].encode('gbk');mobs[v]=b'\t'.join(cols)
assert mobs.keys()==MOUNT_MOBS.keys(),sorted(MOUNT_MOBS.keys()-mobs.keys())
(output/'mobs-server.tsv').write_bytes(b'\n'.join(mobs[v] for v in sorted(mobs))+b'\n')
target=assets/'locale/zh';target.mkdir(parents=True,exist_ok=True)
subprocess.run([str(tool),str(locale/'item_proto'),str(output/'items-client.tsv'),str(target/'item_proto')],check=True)
for source,dest in SASH_LOCALE.items():
(assets/dest).parent.mkdir(parents=True,exist_ok=True);shutil.copy2(files[source],assets/dest);copied+=1
scale=[l for l in (pack_maker/'locale/locale/en/item_scale.txt').read_bytes().splitlines()
if l[:5].isdigit() and wanted & set(range(int(l[:5]),int(l[:5])+5))]
(target/'item_scale.txt').write_bytes(b'\n'.join(scale)+b'\n')
for filename in ('item_list.txt','itemdesc.txt'):
result=[l for l in (locale/filename).read_bytes().splitlines() if not (l.split(b'\t',1)[0].isdigit() and int(l.split(b'\t',1)[0]) in wanted)]
for v in sorted(wanted):
if filename=='item_list.txt':result.append('\t'.join(listrows[v]).encode('gbk'))
else:
t=rows[v][2].strip()
desc=DESC.get(t,'') if v in report['kept'] else LOCKED
desc=DESC_VNUM.get(v) or DESC_SUB.get((t,rows[v][3].strip()),DESC.get(t,'')) if v in report['kept'] else LOCKED
result.append(('%d\t%s\t%s'%(v,zh[v],wrap(desc) if desc else '')).encode('gbk'))
(target/filename).write_bytes(b'\n'.join(result)+b'\n')
subprocess.run([str(packer),str(output/'ava2items'),str(assets),''],check=True)
-4
View File
@@ -21,10 +21,6 @@
19520 石榴石戒指+0
19540 祖母绿戒指+0
19560 蓝宝石戒指+0
21210 暗龙铠甲+0
21230 风龙铠甲+0
21250 魔龙铠甲+0
21270 神龙铠甲+0
25163 缩小的头颅
25164 蜘蛛钥匙
25165 螺旋钥匙
1 # AVA2 drop items imported as 40250 items (vnum<TAB>zh name). 54001-54267 are named by range in make_items.py.
21 19520
22 19540
23 19560
21210
21230
21250
21270
24 25163
25 25164
26 25165
+86 -6
View File
@@ -14,22 +14,102 @@ class ConvertTests(unittest.TestCase):
s,c,kept=m.convert(row(320,'ITEM_WEAPON','WEAPON_SWORD','ANTI_MUDANG | ANTI_WOLFMAN','ITEM_TUNABLE','WEAR_WEAPON',
('APPLY_ATT_SPEED','18','APPLY_ATTBONUS_BOSS','5','APPLY_NONE','0'),refine=('321','81')),self.E,{320})
self.assertTrue(kept);self.assertEqual(s[5],'ANTI_MUDANG');self.assertEqual(s[7],'WEAR_WEAPON')
self.assertEqual(s[18:22],['APPLY_ATT_SPEED','18','APPLY_NONE','0'])
self.assertEqual(s[18:22],['APPLY_ATT_SPEED','18','APPLY_ATTBONUS_BOSS','5']) # ported apply kept
self.assertEqual(s[11:13],['0','0']) # the +1 item is not imported
self.assertEqual(c[1:3],[self.E['type'].index('ITEM_WEAPON'),0]);self.assertEqual(c[4],1<<self.E['anti'].index('ANTI_MUDANG'))
self.assertEqual(len(s),33);self.assertEqual(len(c),31)
def test_costume_enchant_is_locked(self):
s,c,kept=m.convert(row(54001,'ITEM_USE','USE_CHANGE_ATTRIBUTE',flags='ITEM_STACKABLE',values=('23','10','2','0','0','0')),self.E,set())
self.assertFalse(kept);self.assertEqual(s[2:4],['ITEM_NONE','0']);self.assertEqual(s[24:30],['0']*6)
self.assertEqual(c[1:3],[0,0]);self.assertEqual(c[5],1<<self.E['flag'].index('ITEM_STACKABLE'))
def test_costume_enchant_kept_only_for_ported_targets(self):
for vnum,mask in ((54001,'2'),(54078,'4'),(54130,'16')): # body / hair / mount skin (port_costume_attr.cpp)
s,c,kept=m.convert(row(vnum,'ITEM_USE','USE_CHANGE_ATTRIBUTE',flags='ITEM_STACKABLE',values=('23','10',mask,'0','0','0')),self.E,set())
self.assertTrue(kept);self.assertEqual(s[2:4],['ITEM_USE','USE_CHANGE_ATTRIBUTE']);self.assertEqual(s[24:27],['23','10',mask])
for vnum,mask in ((54100,'8'),(54200,'32'),(54250,'64')): # weapon costume / sash skin / effect
s,c,kept=m.convert(row(vnum,'ITEM_USE','USE_CHANGE_ATTRIBUTE',flags='ITEM_STACKABLE',values=('23','10',mask,'0','0','0')),self.E,set())
self.assertFalse(kept);self.assertEqual(s[2:4],['ITEM_NONE','0']);self.assertEqual(s[24:30],['0']*6)
self.assertEqual(c[1:3],[0,0]);self.assertEqual(c[5],1<<self.E['flag'].index('ITEM_STACKABLE'))
def test_unknown_type_and_stackable_anti(self):
s,_,kept=m.convert(row(55010,'ITEM_PET','PET_SKILL','ITEM_STACKABLE','LOG','WEAR_PENDANT'),self.E,set())
self.assertFalse(kept);self.assertEqual(s[5:8],['NONE','LOG | ITEM_STACKABLE','NONE'])
def test_quest_subtype(self):
s,c,kept=m.convert(row(30400,'ITEM_QUEST','QUEST_NONE',values=('APPLY_NONE','0','0','0','0','0')),self.E,set())
self.assertTrue(kept);self.assertEqual(s[3],'0');self.assertEqual(s[24],'0')
def test_treasure_box_and_key_kept(self):
for vnum,t in ((77918,'ITEM_TREASURE_BOX'),(77921,'ITEM_TREASURE_KEY')):
s,c,kept=m.convert(row(vnum,t,'0',values=('3','0','0','0','0','0')),self.E,set())
self.assertTrue(kept);self.assertEqual(s[2:4],[t,'0']);self.assertEqual(s[24],'3') # key value0 == box value0 opens it
self.assertEqual(c[1],self.E['type'].index(t))
def test_stone_and_boss_stones_kept(self):
s,c,kept=m.convert(row(28444,'ITEM_METIN','METIN_NORMAL',flags='ITEM_STACKABLE',wear='WEAR_WEAPON',
applies=('APPLY_ATTBONUS_STONE','8','APPLY_NONE','0','APPLY_NONE','0'),values=('0','0','0','0','0','31')),self.E,set())
self.assertTrue(kept);self.assertEqual(s[7],'WEAR_WEAPON');self.assertEqual(s[18:20],['APPLY_ATTBONUS_STONE','8'])
self.assertEqual(c[17:19],[139,8]) # AVA2 numbering, shared with the server and client enums
self.assertEqual(self.E['apply'].index('APPLY_ATTBONUS_BOSS'),140);self.assertEqual(s[29],'31')
def test_dragon_armor_kept_with_refine_chain(self):
s,c,kept=m.convert(row(21210,'ITEM_ARMOR','ARMOR_BODY','ANTI_ASSASSIN | ANTI_SURA | ANTI_MUDANG | ANTI_WOLFMAN','ITEM_TUNABLE','WEAR_BODY',
('APPLY_MOV_SPEED','5','APPLY_RESIST_MAGIC','25','APPLY_MAX_HP_PCT','20'),('0','210','0','32','0','0'),('21211','81')),self.E,{21210,21211})
self.assertTrue(kept);self.assertEqual(s[3],'ARMOR_BODY');self.assertEqual(s[7],'WEAR_BODY');self.assertEqual(s[11:13],['21211','81'])
self.assertEqual(s[5],'ANTI_ASSASSIN | ANTI_SURA | ANTI_MUDANG');self.assertEqual(s[27],'32') # value3 = armor shape
models=m.shape_models();self.assertEqual(len(models),40)
self.assertIn('d:/ymir work/pc2/warrior/warrior_7_1_lod_03.gr2',models)
def test_sash_kept(self):
s,c,kept=m.convert(row(85004,'ITEM_COSTUME','COSTUME_SASH',wear='WEAR_COSTUME_BODY',
values=('4','0','0','0','0','0')),self.E,{85004})
self.assertTrue(kept);self.assertEqual(s[2:4],['ITEM_COSTUME','COSTUME_SASH']);self.assertEqual(s[24],'4') # value0 = grade
self.assertEqual(c[2],3) # COSTUME_SASH, after BODY/HAIR/MOUNT
def test_mount_and_skin_kept(self):
s,c,kept=m.convert(row(71502,'ITEM_COSTUME','COSTUME_MOUNT','ANTI_STACK',applies=('APPLY_ATTBONUS_MONSTER','30','APPLY_NONE','0','APPLY_NONE','0'),
values=('20113','0','0','0','0','0')),self.E,{71502})
self.assertTrue(kept);self.assertEqual(s[3],'COSTUME_MOUNT');self.assertEqual(s[18:20],['APPLY_ATTBONUS_MONSTER','30'])
self.assertEqual(s[24],'20113');self.assertEqual(c[2],2) # value0 = horse race
s,c,kept=m.convert(row(71234,'ITEM_COSTUME','COSTUME_MOUNT_SKIN',values=('32012','0','0','0','0','0')),self.E,{71234})
self.assertTrue(kept);self.assertEqual(s[3],'COSTUME_MOUNT_SKIN');self.assertEqual(c[2],4)
self.assertEqual(set(m.MOUNT_MOBS),{32012,32028})
def test_pendant_and_scrolls_kept(self):
s,c,kept=m.convert(row(9601,'ITEM_ARMOR','ARMOR_PENDANT',flags='ITEM_TUNABLE',wear='WEAR_PENDANT',
applies=('APPLY_ENCHANT_FIRE','1','APPLY_NONE','0','APPLY_NONE','0'),refine=('9602','441')),self.E,{9601,9602})
self.assertTrue(kept);self.assertEqual(s[3],'ARMOR_PENDANT');self.assertEqual(s[7],'NONE') # no WEAR_PENDANT flag: FindEquipCell
self.assertEqual(s[11:13],['9602','441']);self.assertEqual(s[18:20],['APPLY_ENCHANT_FIRE','1']);self.assertEqual(c[17],124)
self.assertEqual(c[2],self.E['sub'][self.E['type'].index('ITEM_ARMOR')].index('ARMOR_PENDANT'))
for vnum,st in ((53996,'USE_CHANGE_ATTRIBUTE'),(71302,'USE_CHANGE_ATTRIBUTE'),(53997,'USE_ADD_ATTRIBUTE'),(71301,'USE_ADD_ATTRIBUTE')):
s,c,kept=m.convert(row(vnum,'ITEM_USE',st,flags='ITEM_STACKABLE'),self.E,set())
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:
d=Path(d);conf=d/'share/conf';conf.mkdir(parents=True)
(conf/'item_proto.txt').write_bytes(b'VNUM\tNAME\r\n71502\told\r\n');(conf/'item_names.txt').write_bytes(b'VNUM\tNAME\r\n')
(conf/'mob_proto.txt').write_bytes(b'VNUM\tNAME\n20110\tboar\n');(conf/'mob_names.txt').write_bytes(b'20110\tboar\n')
(d/'i.tsv').write_bytes(b'71502\tnew\n');(d/'m.tsv').write_bytes(b'32012\tbari\tKING\n')
subprocess.run([sys.executable,str(HERE/'install_items.py'),str(d),str(d/'i.tsv'),str(d/'bk'),str(d/'m.tsv')],check=True,capture_output=True)
self.assertEqual((conf/'item_proto.txt').read_bytes(),b'VNUM\tNAME\r\n71502\tnew\r\n')
self.assertEqual((conf/'mob_proto.txt').read_bytes(),b'VNUM\tNAME\n20110\tboar\n32012\tbari\tKING\n')
self.assertEqual((conf/'mob_names.txt').read_bytes(),b'20110\tboar\n32012\tbari\n')
self.assertTrue((d/'bk/mob_proto.txt').is_file())
def test_sash_effect_closure(self):
import tempfile
with tempfile.TemporaryDirectory() as d:
d=Path(d);files={}
def put(name,data=b'x'):
f=d/str(len(files));f.write_bytes(data);files[name]=f
put(m.SASH_EFFECTS[0],b'MeshFileName "buff_item1.mde"\nTextureFileName "d:/ymir work/effect/a.dds"')
for q in m.SASH_EFFECTS[1:]:put(q)
put('d:/ymir work/effect/etc/buff/buff_item1.mde',b'"light010.dds"');put('d:/ymir work/effect/a.dds')
put('d:/ymir work/effect/etc/buff/light010.dds')
got=set(m.effect_files(files))
self.assertEqual(got,set(files))
del files['d:/ymir work/effect/etc/buff/light010.dds']
with self.assertRaises(SystemExit):m.effect_files(files)
def test_names_cover_ranges(self):
n=m.names();self.assertEqual(len(n),478);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()))
if __name__=='__main__':unittest.main()
+14
View File
@@ -0,0 +1,14 @@
# Talisman refine sets
AVA2 talismans (护符, metin2-server docs/FEATURE-AVA2-UNLOCK.md P7) refine +0..+200 through 201 items per element. Every step uses one of ten refine sets per element: fragment ×1 + 30031 ×1 + 33031 ×10, 50000 yang, 100 % (AVA2's own rows).
| Element | Items | Sets | Fragment |
|---|---|---|---|
| fire | 9600–9800 | 440–449 | 96001 |
| ice | 9830–10030 | 460–469 | 96002 |
| earth | 10060–10260 | 480–489 | 96003 |
| dark | 10290–10490 | 490–499 | 96004 |
| wind | 10520–10720 | 470–479 | 96005 |
| elect | 10750–10950 | 450–459 | 96006 |
`install.sh` (on the server) backs up rows 440–499, replaces them from `refine.sql`, and checks 60 rows. The db loads refine_proto at boot, so restart the db afterwards. The items themselves come from `../ava2-items`.
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh
# Installs the talisman refine sets (README.md) into player.refine_proto; the db reads them at boot, so restart the db.
set -eu
HERE=$(cd "$(dirname "$0")" && pwd)
MYSQL=${MYSQL:-mysql -umetin2 -ppassword}
BACKUP=/root/backup-pendant/$(date +%Y%m%d-%H%M%S)
mkdir -p "$BACKUP"
$MYSQL player -e 'SELECT * FROM refine_proto WHERE id BETWEEN 440 AND 499' > "$BACKUP/refine_440_499.tsv"
echo "backup: $BACKUP"
$MYSQL player < "$HERE/refine.sql"
[ "$($MYSQL -N player -e 'SELECT COUNT(*) FROM refine_proto WHERE id BETWEEN 440 AND 499 AND cost=50000')" -eq 60 ] || { echo "refine rows missing" >&2; exit 1; }
echo "done; restart the db"
+63
View File
@@ -0,0 +1,63 @@
-- AVA2 talisman refine sets 440-499 (AVA2 player.refine_proto): element fragment x1 + 30031 x1 + 33031 x10, 50000 yang, 100 %.
-- Step n of a talisman chain uses set base + n % 10 (fire 440, elect 450, ice 460, wind 470, earth 480, dark 490).
REPLACE INTO refine_proto (id,vnum0,vnum1,vnum2,vnum3,vnum4,count0,count1,count2,count3,count4,cost,src_vnum,result_vnum,prob) VALUES
(440,96001,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(441,96001,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(442,96001,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(443,96001,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(444,96001,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(445,96001,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(446,96001,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(447,96001,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(448,96001,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(449,96001,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(450,96006,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(451,96006,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(452,96006,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(453,96006,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(454,96006,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(455,96006,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(456,96006,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(457,96006,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(458,96006,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(459,96006,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(460,96002,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(461,96002,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(462,96002,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(463,96002,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(464,96002,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(465,96002,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(466,96002,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(467,96002,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(468,96002,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(469,96002,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(470,96005,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(471,96005,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(472,96005,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(473,96005,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(474,96005,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(475,96005,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(476,96005,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(477,96005,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(478,96005,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(479,96005,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(480,96003,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(481,96003,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(482,96003,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(483,96003,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(484,96003,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(485,96003,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(486,96003,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(487,96003,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(488,96003,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(489,96003,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(490,96004,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(491,96004,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(492,96004,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(493,96004,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(494,96004,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(495,96004,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(496,96004,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(497,96004,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(498,96004,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
(499,96004,30031,33031,0,0,1,1,10,0,0,50000,0,0,100);
+12
View File
@@ -0,0 +1,12 @@
# 披风 (sash) quest
AVA2 `Globales/sash.lua` ported to 王铁匠 (20016; 203 has no NPC 60003):
披风说明 / 披风合成 (`pc.open_sash(true)`) / 属性吸收 (`pc.open_sash(false)`).
Needs the sash game binary (server `port_sash.cpp`, CG 211 / GC 215) and a client with `mt_sash`.
sh install.sh # on the server, from this directory; then restart the game cores
Adds `pc.open_sash` to quest_functions, compiles only sash.quest in an isolated staging dir, copies its
objects (state/sash + 20016/chat/sash.start.{0,1,2}.*) into the live tree, and adds it to locale_list.
Backups go to /root/backup-sash/<time>.
+35
View File
@@ -0,0 +1,35 @@
#!/bin/sh
# Installs quest sash (README.md): run on the server from this directory, then restart the game cores.
# Adds pc.open_sash to quest_functions (needs the sash game binary; an older core would fail the call, not the boot),
# compiles only sash.quest in an isolated staging directory and copies its objects into the live quest tree.
set -eu
SERVER=${SERVER:-/usr/metin2/server}
LOCALE=${LOCALE:-newcibn}
QUEST="$SERVER/share/locale/$LOCALE/quest"
HERE=$(cd "$(dirname "$0")" && pwd)
BACKUP=/root/backup-sash/$(date +%Y%m%d-%H%M%S)
export LC_ALL=C
mkdir -p "$BACKUP/stage"
cp -p "$QUEST/quest_functions" "$QUEST/locale_list" "$BACKUP/"
[ -f "$QUEST/sash.quest" ] && cp -p "$QUEST/sash.quest" "$BACKUP/"
echo "backup: $BACKUP"
# A GBK trail byte 0x5C would be a backslash to qc.
iconv -f UTF-8 -t GBK "$HERE/sash.quest" > "$BACKUP/sash.gbk"
if grep -q '\\' "$BACKUP/sash.gbk"; then echo "sash.quest: backslash byte in GBK" >&2; exit 1; fi
cp "$QUEST/quest_functions" "$BACKUP/stage/"
grep -qx 'pc.open_sash' "$BACKUP/stage/quest_functions" || echo 'pc.open_sash' >> "$BACKUP/stage/quest_functions"
cp "$BACKUP/sash.gbk" "$BACKUP/stage/sash.quest"
(cd "$BACKUP/stage" && "$QUEST/qc" sash.quest) > "$BACKUP/qc.log" 2>&1 || { cat "$BACKUP/qc.log" >&2; exit 1; }
test -s "$BACKUP/stage/object/state/sash"
[ "$(cd "$BACKUP/stage/object/20016/chat" && ls | grep -c '^sash\.')" -eq 9 ] || { echo "unexpected qc output" >&2; exit 1; }
cat "$BACKUP/stage/quest_functions" > "$QUEST/quest_functions"
cat "$BACKUP/sash.gbk" > "$QUEST/sash.quest"
grep -qx 'sash.quest' "$QUEST/locale_list" || echo 'sash.quest' >> "$QUEST/locale_list"
(cd "$BACKUP/stage/object" && find . -type f) | while read -r f; do
mkdir -p "$QUEST/object/$(dirname "$f")"
cp "$BACKUP/stage/object/$f" "$QUEST/object/$f"
done
echo "done; restart the game cores"
+50
View File
@@ -0,0 +1,50 @@
-- 披风 (AVA2 Globales/sash.lua): 合成 / 属性吸收 窗口由 pc.open_sash(true/false) 打开。
-- AVA2 用 NPC 60003, 203 没有这个 NPC, 挂在 王铁匠 20016。
quest sash begin
state start begin
when 20016.chat."披风说明" begin
say_title(mob_name(20016))
say("")
say("披风可以吸收武器或防具的属性.")
say("")
say("[披风合成]")
say("放入两件同等级的披风合成, 成功后")
say("得到吸收率更高的披风, 失败则材料消失.")
say("")
say("[属性吸收]")
say("放入披风和一件武器或防具, 披风按吸收率")
say("获得其属性, 被吸收的装备会消失.")
say("")
end
when 20016.chat."披风合成" begin
say_title(mob_name(20016))
say("")
say("要打开披风合成窗口吗?")
say("")
local confirm = select("确定", "取消")
if confirm == 2 then
return
end
setskin(NOWINDOW)
pc.open_sash(true)
end
when 20016.chat."属性吸收" begin
say_title(mob_name(20016))
say("")
say("被吸收的装备会消失, 披风原有的吸收")
say("属性会被替换.")
say("要打开属性吸收窗口吗?")
say("")
local confirm = select("确定", "取消")
if confirm == 2 then
return
end
setskin(NOWINDOW)
pc.open_sash(false)
end
end
end