From 4d7306792ee5172fc7f8116366bbc3e5c22a5044 Mon Sep 17 00:00:00 2001 From: shenlei Date: Fri, 16 Oct 2026 13:45:02 +0900 Subject: [PATCH] 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 --- src/platform/ScriptLib/PythonBoot.cpp | 5 + src/platform/ScriptLib/SashScript.inc | 612 ++++++++++++++++++ .../UserInterface/PythonApplication.cpp | 6 + src/port/EterGrnLib/ThingInstance.cpp | 54 +- src/port/EterGrnLib/ThingInstance.h | 8 + src/port/GameLib/ActorInstance.h | 1 + src/port/GameLib/ActorInstanceAttach.cpp | 41 ++ src/port/GameLib/ItemData.cpp | 38 ++ src/port/GameLib/ItemData.h | 10 + src/port/GameLib/ItemManager.cpp | 36 ++ src/port/GameLib/ItemManager.h | 15 + src/port/GameLib/RaceData.h | 1 + src/port/UserInterface/GameType.h | 1 + src/port/UserInterface/InstanceBase.cpp | 78 +++ src/port/UserInterface/InstanceBase.h | 8 + .../UserInterface/NetworkActorManager.cpp | 5 + src/port/UserInterface/NetworkActorManager.h | 3 + src/port/UserInterface/Packet.h | 53 ++ .../PythonCharacterManagerModule.cpp | 3 + .../UserInterface/PythonCharacterModule.cpp | 19 + src/port/UserInterface/PythonItemModule.cpp | 2 + .../UserInterface/PythonNetworkStream.cpp | 1 + src/port/UserInterface/PythonNetworkStream.h | 7 + .../PythonNetworkStreamPhaseGame.cpp | 4 + .../PythonNetworkStreamPhaseGameActor.cpp | 5 + .../PythonNetworkStreamPhaseGameItem.cpp | 7 + .../PythonNetworkStreamPhaseGameSash.cpp | 159 +++++ src/port/UserInterface/PythonPlayer.cpp | 28 + src/port/UserInterface/PythonSash.cpp | 189 ++++++ src/port/UserInterface/PythonSash.h | 27 + src/port/UserInterface/StdAfx.h | 1 + tests/port/classic/wire_classic.h | 8 +- tools/server/ava2-items/make_items.py | 35 +- tools/server/ava2-items/tests/test_items.py | 19 + tools/server/sash/README.md | 12 + tools/server/sash/install.sh | 35 + tools/server/sash/sash.quest | 50 ++ 37 files changed, 1568 insertions(+), 18 deletions(-) create mode 100644 src/platform/ScriptLib/SashScript.inc create mode 100644 src/port/UserInterface/PythonNetworkStreamPhaseGameSash.cpp create mode 100644 src/port/UserInterface/PythonSash.cpp create mode 100644 src/port/UserInterface/PythonSash.h create mode 100644 tools/server/sash/README.md create mode 100755 tools/server/sash/install.sh create mode 100644 tools/server/sash/sash.quest diff --git a/src/platform/ScriptLib/PythonBoot.cpp b/src/platform/ScriptLib/PythonBoot.cpp index 8927d15d..4c2fdc93 100644 --- a/src/platform/ScriptLib/PythonBoot.cpp +++ b/src/platform/ScriptLib/PythonBoot.cpp @@ -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 @@ -1013,6 +1016,7 @@ namespace PythonBoot #include "BattlePassScript.inc" #include "AppliesScript.inc" #include "ArmorShapesScript.inc" +#include "SashScript.inc" static void install_script_module(const char* c_szModule, const char* c_szScript) { @@ -1097,6 +1101,7 @@ bool RunMainScript(const char* lpCmdLine, std::string* error) 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); if (g_touch_input) install_script_module("mt_mobile_hud", c_szMobileHUDScript); } diff --git a/src/platform/ScriptLib/SashScript.inc b/src/platform/ScriptLib/SashScript.inc new file mode 100644 index 00000000..71e414f8 --- /dev/null +++ b/src/platform/ScriptLib/SashScript.inc @@ -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 " 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"; diff --git a/src/platform/UserInterface/PythonApplication.cpp b/src/platform/UserInterface/PythonApplication.cpp index f8a9ffec..a110bbb9 100644 --- a/src/platform/UserInterface/PythonApplication.cpp +++ b/src/platform/UserInterface/PythonApplication.cpp @@ -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); diff --git a/src/port/EterGrnLib/ThingInstance.cpp b/src/port/EterGrnLib/ThingInstance.cpp index f6b7ec8b..7dc2d48f 100644 --- a/src/port/EterGrnLib/ThingInstance.cpp +++ b/src/port/EterGrnLib/ThingInstance.cpp @@ -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::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::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(); diff --git a/src/port/EterGrnLib/ThingInstance.h b/src/port/EterGrnLib/ThingInstance.h index e02e6b92..c718e198 100644 --- a/src/port/EterGrnLib/ThingInstance.h +++ b/src/port/EterGrnLib/ThingInstance.h @@ -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 m_LODControllerVector; std::vector m_modelThingSetVector; std::map m_roMotionThingMap; + std::map m_partScaleMap; // PORT: AVA2 sash scale + + const D3DXMATRIX * __GetPartWorldMatrix(int iPart, D3DXMATRIX & rScaled) const; protected: virtual void OnUpdateCollisionData(const CStaticCollisionDataVector * pscdVector); diff --git a/src/port/GameLib/ActorInstance.h b/src/port/GameLib/ActorInstance.h index 9b5a857e..a4717c5b 100644 --- a/src/port/GameLib/ActorInstance.h +++ b/src/port/GameLib/ActorInstance.h @@ -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); diff --git a/src/port/GameLib/ActorInstanceAttach.cpp b/src/port/GameLib/ActorInstanceAttach.cpp index 0d8b5c81..976788d8 100644 --- a/src/port/GameLib/ActorInstanceAttach.cpp +++ b/src/port/GameLib/ActorInstanceAttach.cpp @@ -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); + } + } +} diff --git a/src/port/GameLib/ItemData.cpp b/src/port/GameLib/ItemData.cpp index 65566340..b7fd5393 100644 --- a/src/port/GameLib/ItemData.cpp +++ b/src/port/GameLib/ItemData.cpp @@ -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() diff --git a/src/port/GameLib/ItemData.h b/src/port/GameLib/ItemData.h index 8ae3e0fb..935581ad 100644 --- a/src/port/GameLib/ItemData.h +++ b/src/port/GameLib/ItemData.h @@ -102,6 +102,7 @@ class CItemData { COSTUME_BODY, //0 갑옷(main look) COSTUME_HAIR, //1 헤어(탈착가능) + COSTUME_SASH = 3, // PORT: AVA2 sash (2 is AVA2 COSTUME_MOUNT) COSTUME_NUM_TYPES, }; @@ -495,6 +496,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(); @@ -518,6 +522,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(); diff --git a/src/port/GameLib/ItemManager.cpp b/src/port/GameLib/ItemManager.cpp index f0d76fab..ee25d324 100644 --- a/src/port/GameLib/ItemManager.cpp +++ b/src/port/GameLib/ItemManager.cpp @@ -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; +} diff --git a/src/port/GameLib/ItemManager.h b/src/port/GameLib/ItemManager.h index b7100a7e..b7366b63 100644 --- a/src/port/GameLib/ItemManager.h +++ b/src/port/GameLib/ItemManager.h @@ -13,6 +13,20 @@ class CItemManager : public CSingleton 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 TItemMap; @@ -31,6 +45,7 @@ class CItemManager : public CSingleton ///// 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); diff --git a/src/port/GameLib/RaceData.h b/src/port/GameLib/RaceData.h index 23586030..7f72b1a7 100644 --- a/src/port/GameLib/RaceData.h +++ b/src/port/GameLib/RaceData.h @@ -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, }; diff --git a/src/port/UserInterface/GameType.h b/src/port/UserInterface/GameType.h index 7c8fa39f..76a9ca83 100644 --- a/src/port/UserInterface/GameType.h +++ b/src/port/UserInterface/GameType.h @@ -104,6 +104,7 @@ 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) #endif diff --git a/src/port/UserInterface/InstanceBase.cpp b/src/port/UserInterface/InstanceBase.cpp index e05944fc..4e3c7d10 100644 --- a/src/port/UserInterface/InstanceBase.cpp +++ b/src/port/UserInterface/InstanceBase.cpp @@ -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; diff --git a/src/port/UserInterface/InstanceBase.h b/src/port/UserInterface/InstanceBase.h index c7a15498..5ea032a0 100644 --- a/src/port/UserInterface/InstanceBase.h +++ b/src/port/UserInterface/InstanceBase.h @@ -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 { diff --git a/src/port/UserInterface/NetworkActorManager.cpp b/src/port/UserInterface/NetworkActorManager.cpp index 7b7806cf..b96e7b9f 100644 --- a/src/port/UserInterface/NetworkActorManager.cpp +++ b/src/port/UserInterface/NetworkActorManager.cpp @@ -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; } diff --git a/src/port/UserInterface/NetworkActorManager.h b/src/port/UserInterface/NetworkActorManager.h index 577feba8..420be5b4 100644 --- a/src/port/UserInterface/NetworkActorManager.h +++ b/src/port/UserInterface/NetworkActorManager.h @@ -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; diff --git a/src/port/UserInterface/Packet.h b/src/port/UserInterface/Packet.h index b88ad3c4..03b28cca 100644 --- a/src/port/UserInterface/Packet.h +++ b/src/port/UserInterface/Packet.h @@ -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 " 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) diff --git a/src/port/UserInterface/PythonCharacterManagerModule.cpp b/src/port/UserInterface/PythonCharacterManagerModule.cpp index c87a89d3..784b2ab9 100644 --- a/src/port/UserInterface/PythonCharacterManagerModule.cpp +++ b/src/port/UserInterface/PythonCharacterManagerModule.cpp @@ -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 } diff --git a/src/port/UserInterface/PythonCharacterModule.cpp b/src/port/UserInterface/PythonCharacterModule.cpp index ad76433f..70714a41 100644 --- a/src/port/UserInterface/PythonCharacterModule.cpp +++ b/src/port/UserInterface/PythonCharacterModule.cpp @@ -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 ///// diff --git a/src/port/UserInterface/PythonItemModule.cpp b/src/port/UserInterface/PythonItemModule.cpp index 4e6fb4da..48f6897e 100644 --- a/src/port/UserInterface/PythonItemModule.cpp +++ b/src/port/UserInterface/PythonItemModule.cpp @@ -640,6 +640,8 @@ 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); #endif #ifdef ENABLE_NEW_EQUIPMENT_SYSTEM diff --git a/src/port/UserInterface/PythonNetworkStream.cpp b/src/port/UserInterface/PythonNetworkStream.cpp index 540408a5..2cd30362 100644 --- a/src/port/UserInterface/PythonNetworkStream.cpp +++ b/src/port/UserInterface/PythonNetworkStream.cpp @@ -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 } }; diff --git a/src/port/UserInterface/PythonNetworkStream.h b/src/port/UserInterface/PythonNetworkStream.h index 18b8cb01..ed660051 100644 --- a/src/port/UserInterface/PythonNetworkStream.h +++ b/src/port/UserInterface/PythonNetworkStream.h @@ -267,6 +267,12 @@ class CPythonNetworkStream : public CNetworkStream, public CSingletonalSockets[SASH_ABSORBED_SOCKET]; + CItemData * pAbsorbed; + if (lAbsorbedVnum != 0 && CItemManager::Instance().GetItemDataPointer(lAbsorbedVnum, &pAbsorbed)) + { + if (pAbsorbed->GetType() == CItemData::ITEM_TYPE_WEAPON) + { + double dAbs = static_cast(pSashData->alSockets[SASH_ABSORPTION_SOCKET]) / 100.0; + long lMinWep = static_cast((pAbsorbed->GetValue(3) + pAbsorbed->GetValue(5)) * dAbs + 0.5); + long lMaxWep = static_cast((pAbsorbed->GetValue(4) + pAbsorbed->GetValue(5)) * dAbs + 0.5); + long lMinMag = static_cast((pAbsorbed->GetValue(1) + pAbsorbed->GetValue(5)) * dAbs + 0.5); + long lMaxMag = static_cast((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) diff --git a/src/port/UserInterface/PythonSash.cpp b/src/port/UserInterface/PythonSash.cpp new file mode 100644 index 00000000..30bdfa1e --- /dev/null +++ b/src/port/UserInterface/PythonSash.cpp @@ -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); +} diff --git a/src/port/UserInterface/PythonSash.h b/src/port/UserInterface/PythonSash.h new file mode 100644 index 00000000..3bd285bd --- /dev/null +++ b/src/port/UserInterface/PythonSash.h @@ -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 +{ + public: + DWORD dwPrice; + typedef std::vector 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; +}; diff --git a/src/port/UserInterface/StdAfx.h b/src/port/UserInterface/StdAfx.h index 9865982f..0438de5b 100644 --- a/src/port/UserInterface/StdAfx.h +++ b/src/port/UserInterface/StdAfx.h @@ -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 diff --git a/tests/port/classic/wire_classic.h b/tests/port/classic/wire_classic.h index 33aecb36..e7a30de0 100644 --- a/tests/port/classic/wire_classic.h +++ b/tests/port/classic/wire_classic.h @@ -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 diff --git a/tools/server/ava2-items/make_items.py b/tools/server/ava2-items/make_items.py index 9b6b0f52..83d8216a 100644 --- a/tools/server/ava2-items/make_items.py +++ b/tools/server/ava2-items/make_items.py @@ -1,7 +1,7 @@ #!/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 +new rings, mounts, 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. """ import argparse,json,re,shutil,subprocess @@ -17,8 +17,15 @@ ARMOR_SHAPES={32:'%s_7_1'} RACE_PATHS={'warrior':('pc','pc2'),'assassin':('pc2','pc'),'sura':('pc','pc2'),'shaman':('pc2','pc')} # (male, female) # Kept with their 40250 meaning; everything else becomes ITEM_NONE (see module doc). KEEP_TYPES={'ITEM_MATERIAL','ITEM_QUEST','ITEM_GIFTBOX','ITEM_NONE','ITEM_TREASURE_BOX','ITEM_TREASURE_KEY'} -KEEP_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_METIN','METIN_NORMAL')} -DESC={'ITEM_METIN':'可镶嵌到武器的空孔中。','ITEM_MATERIAL':'制作与升级材料。','ITEM_QUEST':'任务物品。','ITEM_GIFTBOX':'打开后可获得物品。','ITEM_TREASURE_BOX':'需要对应的钥匙才能打开。','ITEM_TREASURE_KEY':'可以打开对应的宝箱。','ITEM_WEAPON':'','ITEM_ARMOR':''} +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_METIN','METIN_NORMAL')}|{('ITEM_COSTUME','COSTUME_SASH')} +DESC={'ITEM_METIN':'可镶嵌到武器的空孔中。','ITEM_MATERIAL':'制作与升级材料。','ITEM_QUEST':'任务物品。','ITEM_GIFTBOX':'打开后可获得物品。','ITEM_TREASURE_BOX':'需要对应的钥匙才能打开。','ITEM_TREASURE_KEY':'可以打开对应的宝箱。','ITEM_WEAPON':'','ITEM_ARMOR':'','ITEM_COSTUME':'披风。可在披风工匠处合成升级,或吸收武器、铠甲的部分属性。'} +# 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'} LOCKED='该物品所属的系统尚未开放,暂时无法使用。' def enums(proto_reader): @@ -90,6 +97,21 @@ def shape_models(): 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={} @@ -134,7 +156,7 @@ 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 - for q in shape_models(): + for q in shape_models()+effect_files(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 @@ -142,6 +164,11 @@ def prepare(ava,proto_reader,locale,output,tool,packer,wrap=lambda text:text): (output/'items-client.tsv').write_bytes(b'\n'.join(client)+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): diff --git a/tools/server/ava2-items/tests/test_items.py b/tools/server/ava2-items/tests/test_items.py index 9bd5e607..8d301ae0 100644 --- a/tools/server/ava2-items/tests/test_items.py +++ b/tools/server/ava2-items/tests/test_items.py @@ -46,6 +46,25 @@ class ConvertTests(unittest.TestCase): 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_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),514);self.assertEqual(n[54100],'武器外观强化石') diff --git a/tools/server/sash/README.md b/tools/server/sash/README.md new file mode 100644 index 00000000..6fc2b5c7 --- /dev/null +++ b/tools/server/sash/README.md @@ -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/