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
shenlei f266072f67 Merge feature/ava2-items 2026-10-16 10:10:50 +09:00
shenleiandClaude Opus 5.5 90f213c4cc Import the AVA2 drop items the server lacks as ordinary 40250 items
tools/server/ava2-items stages the 478 items referenced by the AVA2 drop
tables that 203 does not have: server item_proto/item_names rows, a client
item_proto/item_list/itemdesc patch over the biolog tables, and the AVA2
icons, ground models (BitKnit gr2 via sdk/gr2) and their textures, packed
as ava2items ahead of biolog. Materials, quest items, chests, the guardian
weapons and shoes keep their meaning; items of AVA2-only systems (pets,
pendants, new rings, mounts, sashes, costume enchants, talisman items, new
skill books) become plain ITEM_NONE items until those systems are ported.
Names are translated in names_zh.tsv.

item_proto_patch is the full-record successor of biolog_proto_patch, with
--dump for checking conversions against the shipped table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 10:10:47 +09:00
shenleiandClaude Opus 5.5 d11d46c469 Fix the zh locale build after the item shapes overlay
A stray comma turned the proto patch argument list into a tuple, so the
build failed with "bad operand type for unary +" since 5dcac9cb. The AVA2
biology step also accepts the reference unpacked under AVA2/TEST instead of
only the FILES+SRC.tgz archive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 09:31:52 +09:00
54 changed files with 2846 additions and 24 deletions
+44 -7
View File
@@ -356,7 +356,7 @@ def build(en: Path, zh: Path, output: Path, client_base: Path,
[str(proto_tool), str(en / "item_proto"), str(en / "mob_proto"),
str(item_names), str(mob_names), str(locale_dir / "item_proto"),
str(locale_dir / "mob_proto")] + ([str(limits)] if limits.is_file() else [])
+ ([str(names_dir / "item_stones_server.tsv")] if (names_dir / "item_stones_server.tsv").is_file() else []),
+ ([str(names_dir / "item_stones_server.tsv")] if (names_dir / "item_stones_server.tsv").is_file() else [])
# item_shapes_server.tsv follows the stone overlay (positional): size and flags of new-use items.
+ ([str(names_dir / "item_shapes_server.tsv")] if (names_dir / "item_shapes_server.tsv").is_file() else []),
check=True, capture_output=True, text=True,
@@ -397,7 +397,10 @@ def build_biolog(output: Path, pack_tool: Path):
root = REPO / "tools/server/biolog"
ava = REPO.parent / "AVA2"
archive = ava / "2. Backup SF+SRC and SQL/FILES+SRC.tgz"
if not archive.is_file():
extracted = ava / "TEST/2. Backup SF+SRC and SQL/home/srv1/db/item_proto.txt"
if not archive.is_file() and extracted.is_file():
ava = ava / "TEST" # the reference unpacked under AVA2/TEST
elif not archive.is_file():
raise ValueError("AVA2 biology requires the local FILES+SRC.tgz reference")
proto_tool = tool("biolog_proto_patch")
if not proto_tool.is_file():
@@ -405,11 +408,14 @@ def build_biolog(output: Path, pack_tool: Path):
stage = output.parent / "biolog"
stage.mkdir(parents=True, exist_ok=True)
proto = stage / "ava2-item-proto.txt"
with tarfile.open(archive, "r:gz") as source:
member = source.extractfile("home/srv1/db/item_proto.txt")
if member is None:
raise ValueError("AVA2 item table missing")
proto.write_bytes(member.read())
if not archive.is_file():
shutil.copyfile(extracted, proto)
else:
with tarfile.open(archive, "r:gz") as source:
member = source.extractfile("home/srv1/db/item_proto.txt")
if member is None:
raise ValueError("AVA2 item table missing")
proto.write_bytes(member.read())
spec = importlib.util.spec_from_file_location("biolog_assets", root / "make_assets.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
@@ -425,6 +431,36 @@ def build_biolog(output: Path, pack_tool: Path):
return {"stages": 13, "item_templates": 29, "pack": "biolog", "first_priority": True}
def build_ava2_items(output: Path, pack_tool: Path):
"""AVA2 drop items missing on the server, layered over the biolog item table (see tools/server/ava2-items)."""
import importlib.util
root = REPO / "tools/server/ava2-items"
ava = REPO.parent / "AVA2/TEST"
reader = REPO.parent / "metin2-server/src/server/db/src/ProtoReader.cpp"
if not (ava / "1. Client DEV/pack_maker").is_dir() or not reader.is_file():
raise ValueError("AVA2 items need ../AVA2/TEST (pack_maker + server db) and ../metin2-server")
proto_tool = tool("item_proto_patch")
if not proto_tool.is_file():
raise ValueError("build item_proto_patch before generating the Chinese client")
stage = output.parent / "ava2-items"
spec = importlib.util.spec_from_file_location("ava2_items", root / "make_items.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# biolog's pack carries the current locale/zh item tables; this pack must replace them, so it goes first.
report = module.prepare(ava, reader, output.parent / "biolog/assets/locale/zh", stage,
proto_tool.resolve(), pack_tool.resolve(), wrap_description)
for extension in ("eix", "epk"):
shutil.copy2(stage / ("ava2items." + extension), output / "pack" / ("ava2items." + extension))
index = output / "pack/Index"
lines = index.read_bytes().replace(b"\r\n", b"\n").splitlines()
if b"ava2items" in lines:
del lines[lines.index(b"ava2items") - 1:lines.index(b"ava2items") + 1]
at = lines.index(b"biolog") - 1
lines[at:at] = [b"*", b"ava2items"]
index.write_bytes(b"\r\n".join(lines) + b"\r\n")
return {"items": len(report["kept"]) + len(report["locked"]), "kept": len(report["kept"]),
"locked": len(report["locked"]), "assets": report["assets"], "pack": "ava2items"}
def build_wiki(output: Path, pack_tool: Path):
"""A versioned public server catalogue; original script and proto packs stay intact."""
@@ -466,6 +502,7 @@ def main():
args.proto_tool, args.name_tables, args.pack_tool)
report = json.loads(report_path.read_text())
report["biolog"] = build_biolog(args.output, args.pack_tool)
report["ava2_items"] = build_ava2_items(args.output, args.pack_tool)
report["wiki"] = build_wiki(args.output, args.pack_tool)
report_path.write_text(json.dumps(report, ensure_ascii=False, indent=2)+"\n", encoding="utf-8")
print(f"Built {args.output / 'pack/locale_zh.eix'} (.epk), registered in pack/Index")
+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);
+4
View File
@@ -328,12 +328,16 @@ if(BUILD_TESTING AND CMAKE_SYSTEM_NAME STREQUAL CMAKE_HOST_SYSTEM_NAME)
COMMAND ${MT_WIKI_PYTHON3} ${PROJECT_SOURCE_DIR}/tests/wiki/test_boss.py)
add_test(NAME port.chest
COMMAND ${MT_WIKI_PYTHON3} ${PROJECT_SOURCE_DIR}/tests/wiki/test_chest.py)
add_test(NAME port.ava2_items
COMMAND ${MT_WIKI_PYTHON3} ${PROJECT_SOURCE_DIR}/tools/server/ava2-items/tests/test_items.py)
endif()
# Offline locale data builder: retains 40250 proto layout and gameplay fields.
add_executable(locale_proto_patch ${PROJECT_SOURCE_DIR}/tests/port/locale_proto_patch.cpp)
target_link_libraries(locale_proto_patch PRIVATE port_platform)
add_executable(biolog_proto_patch ${PROJECT_SOURCE_DIR}/tests/port/biolog_proto_patch.cpp)
target_link_libraries(biolog_proto_patch PRIVATE port_platform)
add_executable(item_proto_patch ${PROJECT_SOURCE_DIR}/tests/port/item_proto_patch.cpp)
target_link_libraries(item_proto_patch PRIVATE port_platform)
# Locale packs in the original format (scripts/build_zh_locale.py packs locale/zh with it).
add_executable(eterpack_pack ${PROJECT_SOURCE_DIR}/tests/port/eterpack_pack.cpp)
target_link_libraries(eterpack_pack PRIVATE port_platform)
+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
+80
View File
@@ -0,0 +1,80 @@
// PORT: adds or replaces whole item_proto records (all 156-byte fields); untouched records stay byte-identical.
// Row: vnum type sub size anti flags wear immune buy sell refined refine_set magic_pct
// limit0 value0 limit1 value1 apply0 value0 apply1 value1 apply2 value2 v0..v5 specular gain_socket \t name
#include "GameLib/StdAfx.h"
#include "GameLib/ItemData.h"
#include "EterBase/lzo.h"
#include <array>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <map>
#include <sstream>
#include <stdexcept>
#include <vector>
using Item=CItemData::TItemTable;
static constexpr std::array<DWORD,4> item_key{{173217,72619434,408587239,27973291}};
static std::vector<BYTE> read_file(const char* path){
std::ifstream stream(path,std::ios::binary);
if(!stream)throw std::runtime_error("cannot read proto");
return {std::istreambuf_iterator<char>(stream),std::istreambuf_iterator<char>()};
}
static DWORD word(const std::vector<BYTE>& data,size_t offset){
if(offset+4>data.size())throw std::runtime_error("truncated proto");
DWORD value;std::memcpy(&value,data.data()+offset,4);return value;
}
static void put_word(std::vector<BYTE>& data,size_t offset,DWORD value){std::memcpy(data.data()+offset,&value,4);}
static void dump(const Item& r){
std::printf("%u %u %u %u %u %u %u %u %u %u %u %u %u",r.dwVnum,r.bType,r.bSubType,r.bSize,r.dwAntiFlags,r.dwFlags,r.dwWearFlags,r.dwImmuneFlag,
r.dwIBuyItemPrice,r.dwISellItemPrice,r.dwRefinedVnum,r.wRefineSet,r.bAlterToMagicItemPct);
for(const auto& limit:r.aLimits)std::printf(" %u %d",limit.bType,int(limit.lValue));
for(const auto& apply:r.aApplies)std::printf(" %u %d",apply.bType,int(apply.lValue));
for(auto value:r.alValues)std::printf(" %d",int(value));
std::printf(" %u %u\t%.*s\n",r.bSpecular,r.bGainSocketPct,int(strnlen(r.szLocaleName,sizeof(r.szLocaleName))),r.szLocaleName);
}
int main(int argc,char** argv) {
const bool dumping=argc==3&&!std::strcmp(argv[1],"--dump");
if(argc!=4&&!dumping){std::fprintf(stderr,"usage: item_proto_patch in_proto rows.tsv out_proto | --dump proto\n");return 2;}
if(dumping)argv[1]=argv[2];
try {
static CLZO lzo; auto key=item_key;
auto input=read_file(argv[1]);
if(input.size()<20 || std::memcmp(input.data(),"MIPX",4) || word(input,4)!=1 || word(input,8)!=sizeof(Item))throw std::runtime_error("item proto stride");
CLZObject decoded;
if(!CLZO::Instance().Decompress(decoded,input.data()+20,key.data()))throw std::runtime_error("item proto decode");
if(decoded.GetSize()!=size_t(word(input,12))*sizeof(Item))throw std::runtime_error("proto count/size mismatch");
std::map<DWORD,Item> rows;
for(DWORD i=0;i<word(input,12);++i){Item r;std::memcpy(&r,decoded.GetBuffer()+i*sizeof(Item),sizeof(Item));rows[r.dwVnum]=r;}
if(dumping){for(const auto& pair:rows)dump(pair.second);return 0;}
std::ifstream file(argv[2],std::ios::binary);std::string line;size_t added=0,replaced=0;
while(std::getline(file,line)) {
if(line.empty())continue;
auto tab=line.find('\t',0);std::string numbers=line,name;
// the name is the last tab-separated column
auto last=line.rfind('\t');if(last==std::string::npos)throw std::runtime_error("item row without name");
numbers=line.substr(0,last);name=line.substr(last+1);(void)tab;
std::istringstream stream(numbers);std::array<long long,31> f{};
for(auto& value:f)if(!(stream>>value))throw std::runtime_error("item row: "+line.substr(0,40));
long long extra;if(stream>>extra)throw std::runtime_error("item row has extra columns");
if(name.size()>CItemData::ITEM_NAME_MAX_LEN)throw std::runtime_error("item name too long");
Item r{};int i=0;
r.dwVnum=f[i++];r.bType=f[i++];r.bSubType=f[i++];r.bSize=f[i++];r.dwAntiFlags=f[i++];r.dwFlags=f[i++];
r.dwWearFlags=f[i++];r.dwImmuneFlag=f[i++];r.dwIBuyItemPrice=f[i++];r.dwISellItemPrice=f[i++];
r.dwRefinedVnum=f[i++];r.wRefineSet=f[i++];r.bAlterToMagicItemPct=f[i++];
for(auto& limit:r.aLimits){limit.bType=f[i++];limit.lValue=f[i++];}
for(auto& apply:r.aApplies){apply.bType=f[i++];apply.lValue=f[i++];}
for(auto& value:r.alValues)value=f[i++];
r.bSpecular=f[i++];r.bGainSocketPct=f[i++];
std::memcpy(r.szName,name.data(),name.size());std::memcpy(r.szLocaleName,name.data(),name.size());
(rows.count(r.dwVnum)?replaced:added)++;rows[r.dwVnum]=r;
}
std::vector<BYTE> plain;for(const auto& pair:rows){const BYTE* bytes=reinterpret_cast<const BYTE*>(&pair.second);plain.insert(plain.end(),bytes,bytes+sizeof(Item));}
CLZObject encoded;if(!CLZO::Instance().CompressEncryptedMemory(encoded,plain.data(),plain.size(),key.data()))throw std::runtime_error("encode");
std::vector<BYTE> result(input.begin(),input.begin()+20);put_word(result,12,rows.size());put_word(result,16,encoded.GetSize());
result.insert(result.end(),encoded.GetBuffer(),encoded.GetBuffer()+encoded.GetSize());
CLZObject verified;if(!CLZO::Instance().Decompress(verified,result.data()+20,key.data()) || verified.GetSize()!=plain.size() || std::memcmp(verified.GetBuffer(),plain.data(),plain.size()))throw std::runtime_error("round-trip");
std::ofstream out(argv[3],std::ios::binary|std::ios::trunc);
if(!out.write(reinterpret_cast<const char*>(result.data()),result.size()))throw std::runtime_error("cannot write proto");
std::printf("items added=%zu replaced=%zu total=%zu; verified round-trip\n",added,replaced,rows.size());return 0;
}catch(const std::exception& e){std::fprintf(stderr,"%s\n",e.what());return 1;}
}
+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
+27
View File
@@ -0,0 +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. 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')
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'
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))
+233
View File
@@ -0,0 +1,233 @@
#!/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 (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','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):
src=proto_reader.read_text(encoding='latin1')
def arr(name):
m=re.search(r'(?:static\s+)?string\s+'+name+r'\[\]\s*=\s*\{(.*?)\};',src,re.S)
return re.findall(r'"([^"]+)"',m.group(1))
types=arr('arType');m=re.search(r'arSubType\[\]\s*=\s*\{(.*?)\};',src,re.S)
subs=[arr(t) if t!='0' else [] for t in re.findall(r'\b(0|arSub\d+)\b',re.sub(r'//[^\n]*','',m.group(1)))]
assert len(subs)==len(types),(len(subs),len(types))
return dict(type=types,sub=subs,anti=arr('arAntiFlag'),flag=arr('arFlag'),wear=arr('arWearrFlag'),
imm=arr('arImmune'),limit=arr('arLimitType'),apply=arr('arApplyType'))
def tokens(text):return [t.strip() for t in text.split('|') if t.strip() and t.strip() not in ('NONE','0')]
def number(text):
try:return int(text)
except ValueError:return 0
def names():
result={}
for line in (HERE/'names_zh.tsv').read_text().splitlines():
if line and not line.startswith('#'):
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 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']]
if not kept:
t,st='ITEM_NONE','0';wear=imm=[];applies=[('APPLY_NONE','0')]*3;values=['0']*6
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','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]]
refine=(c[11],c[12]) if number(c[11]) in imported else ('0','0')
limits=[(l if l in E['limit'] else 'LIMIT_NONE',str(number(v))) for l,v in zip(c[14:18:2],c[15:18:2])]
j=lambda xs:' | '.join(xs) if xs else 'NONE'
server=[c[0],c[1],t,st,c[4],j(anti),j(flags),j(wear),j(imm),c[9],c[10],refine[0],refine[1],c[13]]
for l,v in limits:server+=[l,v]
for a,v in applies:server+=[a,str(number(v))]
server+=values+[str(number(c[30])),str(number(c[31])),str(number(c[32])) if len(c)>32 and c[32] else '0']
bits=lambda xs,k:sum(1<<E[k].index(x) for x in xs)
ti=E['type'].index(t);si=E['sub'][ti].index(st) if not st.isdigit() else int(st)
client=[int(c[0]),ti,si,number(c[4]),bits(anti,'anti'),bits(flags,'flag'),bits(wear,'wear'),bits(imm,'imm'),
number(c[9]),number(c[10]),number(refine[0]),number(refine[1]),number(c[13])]
for l,v in limits:client+=[E['limit'].index(l),int(v)]
for a,v in applies:client+=[E['apply'].index(a),number(v)]
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={}
for xml in sorted(pack_maker.glob('*_create.xml')):
for archived,source in re.findall(r'ArchivedPath="([^"]+)"><!\[CDATA\[([^\]]+)\]\]>',xml.read_text(encoding='latin1')):
index.setdefault(archived.lower().replace('\\','/'),pack_maker.parent/source)
return index
def prepare(ava,proto_reader,locale,output,tool,packer,wrap=lambda text:text):
"""wrap: the locale's itemdesc line wrapping (build_zh_locale.wrap_description)."""
E=enums(proto_reader);zh=names();wanted=set(zh)
db=ava/'2. Backup SF+SRC and SQL/home/srv1/db';pack_maker=ava/'1. Client DEV/pack_maker'
rows={}
for line in (db/'item_proto.txt').read_bytes().splitlines():
cols=line.split(b'\t')
if cols[0].strip().isdigit() and int(cols[0]) in wanted:rows[int(cols[0])]=[x.decode('latin1') for x in cols]
assert rows.keys()==wanted,sorted(wanted-rows.keys())
listrows={}
for line in (pack_maker/'locale/locale/es/item_list.txt').read_bytes().splitlines():
c=[x.strip() for x in line.split(b'\t')]
if c and c[0].isdigit() and int(c[0]) in wanted:listrows[int(c[0])]=[x.decode('latin1') for x in c if x]
files=pack_index(pack_maker)
for v in wanted-listrows.keys(): # 16 items have no AVA2 item_list row; their icons ship under the vnum
listrows[v]=[str(v),'ETC','icon/item/%05d.tga'%v]
output.mkdir(parents=True,exist_ok=True)
assets=output/'assets'
if assets.exists():shutil.rmtree(assets)
server=[];client=[];report={'kept':[],'locked':[]}
for v in sorted(wanted):
s,n,kept=convert(rows[v],E,wanted)
name=zh[v];s[1]=name
server.append('\t'.join(s).encode('gbk'))
client.append(('\t'.join(map(str,n))+'\t'+name).encode('gbk'))
report['kept' if kept else 'locked'].append(v)
copied=0
for v,row in listrows.items():
for path in row[2:4]:
p=path.lower().replace('\\','/')
sources=[p]
if p.endswith('.gr2'):sources+=[s for s in MODEL_TEXTURES.get(p.rsplit('/',1)[1],())]
for q in sources:
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_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)
report['assets']=copied
(output/'items.json').write_text(json.dumps(report,indent=1)+'\n')
return report
# Textures named inside the (BitKnit-compressed) AVA2 models, read with sdk/gr2.
MODEL_TEXTURES={**{'%05d.gr2'%v:('d:/ymir work/item/weapon/weapon_7th_01.dds',) for v in (320,340,1190,2210,3230,5170,7310)},
'boss_box.gr2':('d:/ymir work/item/etc/boss_box.dds',),
'acce_01_85_004.gr2':('d:/ymir work/item/wing/acce_85_001-008.dds',),'acce_02_85_008.gr2':('d:/ymir work/item/wing/acce_85_001-008.dds',),
'acce_03_85_014.gr2':('d:/ymir work/item/wing/acce_85_011-018.dds',),'acce_04_85_018.gr2':('d:/ymir work/item/wing/acce_85_011-018.dds',)}
if __name__=='__main__':
p=argparse.ArgumentParser(description=__doc__)
for a in ('ava','proto_reader','locale','output','tool','packer'):p.add_argument(a,type=Path)
a=p.parse_args();print(json.dumps({k:len(v) if isinstance(v,list) else v for k,v in prepare(a.ava,a.proto_reader,a.locale,a.output,a.tool.resolve(),a.packer.resolve()).items()}))
+208
View File
@@ -0,0 +1,208 @@
# AVA2 drop items imported as 40250 items (vnum<TAB>zh name). 54001-54267 are named by range in make_items.py.
320 守护者之剑+0
340 守护者之刃+0
1190 守护者匕首+0
2210 守护者之弓+0
3230 守护者大刀+0
5170 守护者之铃+0
6884 稀有度转换器
7310 守护者之扇+0
9600 火之护符+0
9830 冰之护符+0
10060 土之护符+0
10290 暗之护符+0
10520 风之护符+0
10750 雷之护符+0
15440 炎晶之鞋+0
15450 海洋之鞋+0
15460 锆石之鞋+0
15470 孔雀石之鞋+0
19500 红宝石戒指+0
19520 石榴石戒指+0
19540 祖母绿戒指+0
19560 蓝宝石戒指+0
25163 缩小的头颅
25164 蜘蛛钥匙
25165 螺旋钥匙
25166 缩小的头颅+
25167 拉扎多尔入场券
25168 奈梅尔入场券
25169 九头蛇护身符
28444 破碑之石+4
28445 克首领之石+4
30400 盖亚宝石
30600 图样
30601 铠甲设计图
30602 蓝色染料
30603 红色染料
30604 灰色染料
30605 金板
30606 银板
30607 铜板
30608 钢板
30609 青铜板
30610 秘银
30611 坚韧绑带
30612 结实绳索
30614 黑色染料
30615 白色染料
30616 二氧化钛
30617 玛瑙
30618 月长石
30620 红龙之角
30621 红龙鳞片
30623 智慧之书
30624 魔法宝珠
30625 帕德玛刚玉
30626 蓝晶石
30627 神之果实
30628 神圣之花
30629 众神灵药
30630 龙之翼
33031 吊坠宝石
39070 灰色碎片
39071 白色碎片
39072 绿色碎片
39073 黄色碎片
50275 金色欧凯宝箱
50276 银色欧凯宝箱
50277 铜色欧凯宝箱
50569 秘籍·地震
50570 秘籍·星光
50571 秘籍·风暴射击
50572 秘籍·地狱打击
50573 秘籍·致命波动
50574 秘籍·流星
50575 秘籍·灵气护盾
50928 金王战利品
50929 银王战利品
50930 铜王战利品
51110 阿拉多尔宝箱
51111 奈梅尔宝箱
51112 贝兰塞陶宝箱
51115 远古守护者宝箱
51116 红龙宝箱
51117 远古巨龙宝箱
53004 鲁弗斯封印(永久)
53996 护符附魔道具
53997 护符加持道具
54705 九头蛇宝箱
54706 65级武器宝箱
54707 70级武器宝箱
54708 75级武器宝箱
54709 防具宝箱
54710 装备宝箱
54711 饰品宝箱
54712 生物学家礼包
54713 生物学家袋子
54714 生物学家袋子+
54715 强化药剂包(小)
54716 强化药剂包(中)
54717 强化药剂包(特大)
55001 宠物蛋白补充剂
55004 宠物秘籍·幼年
55005 宠物秘籍·野性
55006 宠物秘籍·勇敢
55009 宠物秘籍宝箱
55010 宠物书·石碑
55011 宠物书·怪物
55012 宠物书·首领
55013 宠物书·力量
55014 宠物书·智力
55015 宠物书·敏捷
55016 宠物书·体力
55017 宠物书·野兽
55018 宠物书·兽人
55019 宠物书·秘法
55020 宠物书·不死
55021 宠物书·恶魔
55022 宠物书·经验
55023 宠物书·掉宝
55024 宠物书·金币
55025 宠物书·半兽人
55026 宠物书·抗半兽人
55027 宠物书·技能伤害
55028 宠物书·抗技能伤害
55029 宠物书·平均伤害
55030 宠物书·抗平均伤害
55031 宠物书·抗暴击
55033 宠物技能清除剂
55035 宠物经验增幅剂
55036 宠物栏位解锁器
55038 宠物书·抗穿透
55039 宠物书·施法速度
55040 宠物书·格挡
55041 宠物书·抗毒
55042 宠物书·冰
55043 宠物书·土
55044 宠物书·暗
55101 活力药剂(小)
55102 活力药剂(中)
55103 活力药剂(大)
55104 活力药剂(特大)
55108 战斗药剂(小)
55109 战斗药剂(中)
55110 战斗药剂(大)
55111 战斗药剂(特大)
55115 力量药剂(小)
55116 力量药剂(中)
55117 力量药剂(大)
55118 力量药剂(特大)
55401 猴子蛋
55404 奈梅尔蛋
55407 阿兹雷尔蛋
71097 炼金附魔道具
71234 蓝色巴里
71254 白色冬日羊驼
71301 护符加持道具
71302 护符强化道具
71502 狮子封印(永久)
71505 野猪封印(永久)
71508 狼封印(永久)
71511 老虎封印(永久)
72064 小型扩充券
72065 中型扩充券
72066 大型扩充券
72067 强效扩充券
72068 天界扩充券
72319 金钥匙
72320 银钥匙
72346 6-7属性附魔道具
77900 药剂宝箱(小)
77901 药剂宝箱(中)
77902 药剂宝箱(大)
77903 药剂宝箱(特大)
77904 阅读宝箱
77905 护符宝箱
77906 耳环宝箱
77907 手镯宝箱
77908 项链宝箱
77909 戒指宝箱
77910 强化宝箱(小)
77911 强化宝箱(中)
77912 强化宝箱(大)
77913 炼金宝箱(小)
77914 炼金宝箱(中)
77915 炼金宝箱(大)
77916 战令宝箱
77917 任务书宝箱
77918 青铜宝箱
77919 白银宝箱
77920 黄金宝箱
77921 青铜钥匙
77922 白银钥匙
77923 黄金钥匙
77924 钥匙宝箱
80019 精致布料
80025 时装兑换券
80030 男爵夫人的宝箱
85004 领主披风(个人)
85008 大师披风(个人)
85014 王朝披风(个人)
85018 皇家披风(个人)
96001 火之护符碎片
96002 冰之护符碎片
96003 土之护符碎片
96004 暗之护符碎片
96005 风之护符碎片
96006 雷之护符碎片
1 # AVA2 drop items imported as 40250 items (vnum<TAB>zh name). 54001-54267 are named by range in make_items.py.
2 320 守护者之剑+0
3 340 守护者之刃+0
4 1190 守护者匕首+0
5 2210 守护者之弓+0
6 3230 守护者大刀+0
7 5170 守护者之铃+0
8 6884 稀有度转换器
9 7310 守护者之扇+0
10 9600 火之护符+0
11 9830 冰之护符+0
12 10060 土之护符+0
13 10290 暗之护符+0
14 10520 风之护符+0
15 10750 雷之护符+0
16 15440 炎晶之鞋+0
17 15450 海洋之鞋+0
18 15460 锆石之鞋+0
19 15470 孔雀石之鞋+0
20 19500 红宝石戒指+0
21 19520 石榴石戒指+0
22 19540 祖母绿戒指+0
23 19560 蓝宝石戒指+0
24 25163 缩小的头颅
25 25164 蜘蛛钥匙
26 25165 螺旋钥匙
27 25166 缩小的头颅+
28 25167 拉扎多尔入场券
29 25168 奈梅尔入场券
30 25169 九头蛇护身符
31 28444 破碑之石+4
32 28445 克首领之石+4
33 30400 盖亚宝石
34 30600 图样
35 30601 铠甲设计图
36 30602 蓝色染料
37 30603 红色染料
38 30604 灰色染料
39 30605 金板
40 30606 银板
41 30607 铜板
42 30608 钢板
43 30609 青铜板
44 30610 秘银
45 30611 坚韧绑带
46 30612 结实绳索
47 30614 黑色染料
48 30615 白色染料
49 30616 二氧化钛
50 30617 玛瑙
51 30618 月长石
52 30620 红龙之角
53 30621 红龙鳞片
54 30623 智慧之书
55 30624 魔法宝珠
56 30625 帕德玛刚玉
57 30626 蓝晶石
58 30627 神之果实
59 30628 神圣之花
60 30629 众神灵药
61 30630 龙之翼
62 33031 吊坠宝石
63 39070 灰色碎片
64 39071 白色碎片
65 39072 绿色碎片
66 39073 黄色碎片
67 50275 金色欧凯宝箱
68 50276 银色欧凯宝箱
69 50277 铜色欧凯宝箱
70 50569 秘籍·地震
71 50570 秘籍·星光
72 50571 秘籍·风暴射击
73 50572 秘籍·地狱打击
74 50573 秘籍·致命波动
75 50574 秘籍·流星
76 50575 秘籍·灵气护盾
77 50928 金王战利品
78 50929 银王战利品
79 50930 铜王战利品
80 51110 阿拉多尔宝箱
81 51111 奈梅尔宝箱
82 51112 贝兰塞陶宝箱
83 51115 远古守护者宝箱
84 51116 红龙宝箱
85 51117 远古巨龙宝箱
86 53004 鲁弗斯封印(永久)
87 53996 护符附魔道具
88 53997 护符加持道具
89 54705 九头蛇宝箱
90 54706 65级武器宝箱
91 54707 70级武器宝箱
92 54708 75级武器宝箱
93 54709 防具宝箱
94 54710 装备宝箱
95 54711 饰品宝箱
96 54712 生物学家礼包
97 54713 生物学家袋子
98 54714 生物学家袋子+
99 54715 强化药剂包(小)
100 54716 强化药剂包(中)
101 54717 强化药剂包(特大)
102 55001 宠物蛋白补充剂
103 55004 宠物秘籍·幼年
104 55005 宠物秘籍·野性
105 55006 宠物秘籍·勇敢
106 55009 宠物秘籍宝箱
107 55010 宠物书·石碑
108 55011 宠物书·怪物
109 55012 宠物书·首领
110 55013 宠物书·力量
111 55014 宠物书·智力
112 55015 宠物书·敏捷
113 55016 宠物书·体力
114 55017 宠物书·野兽
115 55018 宠物书·兽人
116 55019 宠物书·秘法
117 55020 宠物书·不死
118 55021 宠物书·恶魔
119 55022 宠物书·经验
120 55023 宠物书·掉宝
121 55024 宠物书·金币
122 55025 宠物书·半兽人
123 55026 宠物书·抗半兽人
124 55027 宠物书·技能伤害
125 55028 宠物书·抗技能伤害
126 55029 宠物书·平均伤害
127 55030 宠物书·抗平均伤害
128 55031 宠物书·抗暴击
129 55033 宠物技能清除剂
130 55035 宠物经验增幅剂
131 55036 宠物栏位解锁器
132 55038 宠物书·抗穿透
133 55039 宠物书·施法速度
134 55040 宠物书·格挡
135 55041 宠物书·抗毒
136 55042 宠物书·冰
137 55043 宠物书·土
138 55044 宠物书·暗
139 55101 活力药剂(小)
140 55102 活力药剂(中)
141 55103 活力药剂(大)
142 55104 活力药剂(特大)
143 55108 战斗药剂(小)
144 55109 战斗药剂(中)
145 55110 战斗药剂(大)
146 55111 战斗药剂(特大)
147 55115 力量药剂(小)
148 55116 力量药剂(中)
149 55117 力量药剂(大)
150 55118 力量药剂(特大)
151 55401 猴子蛋
152 55404 奈梅尔蛋
153 55407 阿兹雷尔蛋
154 71097 炼金附魔道具
155 71234 蓝色巴里
156 71254 白色冬日羊驼
157 71301 护符加持道具
158 71302 护符强化道具
159 71502 狮子封印(永久)
160 71505 野猪封印(永久)
161 71508 狼封印(永久)
162 71511 老虎封印(永久)
163 72064 小型扩充券
164 72065 中型扩充券
165 72066 大型扩充券
166 72067 强效扩充券
167 72068 天界扩充券
168 72319 金钥匙
169 72320 银钥匙
170 72346 6-7属性附魔道具
171 77900 药剂宝箱(小)
172 77901 药剂宝箱(中)
173 77902 药剂宝箱(大)
174 77903 药剂宝箱(特大)
175 77904 阅读宝箱
176 77905 护符宝箱
177 77906 耳环宝箱
178 77907 手镯宝箱
179 77908 项链宝箱
180 77909 戒指宝箱
181 77910 强化宝箱(小)
182 77911 强化宝箱(中)
183 77912 强化宝箱(大)
184 77913 炼金宝箱(小)
185 77914 炼金宝箱(中)
186 77915 炼金宝箱(大)
187 77916 战令宝箱
188 77917 任务书宝箱
189 77918 青铜宝箱
190 77919 白银宝箱
191 77920 黄金宝箱
192 77921 青铜钥匙
193 77922 白银钥匙
194 77923 黄金钥匙
195 77924 钥匙宝箱
196 80019 精致布料
197 80025 时装兑换券
198 80030 男爵夫人的宝箱
199 85004 领主披风(个人)
200 85008 大师披风(个人)
201 85014 王朝披风(个人)
202 85018 皇家披风(个人)
203 96001 火之护符碎片
204 96002 冰之护符碎片
205 96003 土之护符碎片
206 96004 暗之护符碎片
207 96005 风之护符碎片
208 96006 雷之护符碎片
+115
View File
@@ -0,0 +1,115 @@
import sys,unittest
from pathlib import Path
HERE=Path(__file__).resolve().parents[1];sys.path.insert(0,str(HERE))
import make_items as m
READER=HERE.parents[3]/'metin2-server/src/server/db/src/ProtoReader.cpp'
def row(vnum,t,st,anti='NONE',flags='NONE',wear='NONE',applies=('APPLY_NONE','0')*3,values=('0',)*6,refine=('0','0')):
return [str(vnum),'x',t,st,'1',anti,flags,wear,'NONE','10','20',refine[0],refine[1],'0','LEVEL','120','LIMIT_NONE','0',*applies,*values,'0','0','-1','']
@unittest.skipUnless(READER.is_file(),'needs ../metin2-server')
class ConvertTests(unittest.TestCase):
E=m.enums(READER) if READER.is_file() else None
def test_weapon_kept_without_unknown_flags(self):
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_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_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),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