Author SHA1 Message Date
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
7 changed files with 527 additions and 0 deletions
+31
View File
@@ -431,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."""
@@ -472,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")
+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)
+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;}
}
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env python3
"""Install items-server.tsv (from make_items.py) into share/conf/item_proto.txt + item_names.txt.
Rows with the same vnum are replaced, so a rerun is idempotent. The db reads the tables only at boot:
restart db and every core afterwards. Usage: install_items.py <server root> <items-server.tsv> <fresh backup dir>
"""
import shutil,sys
from pathlib import Path
server,rows,backup=map(Path,sys.argv[1:4])
if backup.exists():raise SystemExit('use a fresh backup directory')
items={int(l.split(b'\t',1)[0]):l for l in rows.read_bytes().splitlines() if l}
backup.mkdir(parents=True)
conf=server/'share/conf'
for filename,row in (('item_proto.txt',lambda v:items[v]),('item_names.txt',lambda v:str(v).encode()+b'\t'+items[v].split(b'\t')[1])):
path=conf/filename;raw=path.read_bytes();shutil.copy2(path,backup/filename)
newline=b'\r\n' if b'\r\n' in raw else b'\n'
kept=[l for l in raw.replace(b'\r\n',b'\n').split(b'\n') if l and not (l.split(b'\t',1)[0].isdigit() and int(l.split(b'\t',1)[0]) in items)]
tmp=path.with_name(filename+'.tmp');tmp.write_bytes(newline.join(kept+[row(v) for v in sorted(items)])+newline);tmp.replace(path)
print('AVA2 items installed:',len(items),'backup:',backup)
+147
View File
@@ -0,0 +1,147 @@
#!/usr/bin/env python3
"""Stage the AVA2 drop items that the 203 server lacks as ordinary 40250 items (server rows + client patch pack).
Only types/subtypes/flags the 40250 server understands survive; items of AVA2-only systems (pets, pendants,
new rings, mounts, sashes, costume enchants, talisman bonus items, new skill books) become plain ITEM_NONE
items until those systems are ported. Usage: see build_zh_locale.build_ava2_items and --help.
"""
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,'特效强化石'))
# Kept with their 40250 meaning; everything else becomes ITEM_NONE (see module doc).
KEEP_TYPES={'ITEM_MATERIAL','ITEM_QUEST','ITEM_GIFTBOX','ITEM_NONE'}
KEEP_SUBTYPES={('ITEM_WEAPON',s) for s in ('WEAPON_SWORD','WEAPON_DAGGER','WEAPON_BOW','WEAPON_TWO_HANDED','WEAPON_BELL','WEAPON_FAN')}|{('ITEM_ARMOR','ARMOR_FOOTS')}
DESC={'ITEM_MATERIAL':'制作与升级材料。','ITEM_QUEST':'任务物品。','ITEM_GIFTBOX':'打开后可获得物品。','ITEM_WEAPON':'','ITEM_ARMOR':''}
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
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
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') 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 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
(output/'items-server.tsv').write_bytes(b'\n'.join(server)+b'\n')
(output/'items-client.tsv').write_bytes(b'\n'.join(client)+b'\n')
target=assets/'locale/zh';target.mkdir(parents=True,exist_ok=True)
subprocess.run([str(tool),str(locale/'item_proto'),str(output/'items-client.tsv'),str(target/'item_proto')],check=True)
for filename in ('item_list.txt','itemdesc.txt'):
result=[l for l in (locale/filename).read_bytes().splitlines() if not (l.split(b'\t',1)[0].isdigit() and int(l.split(b'\t',1)[0]) in wanted)]
for v in sorted(wanted):
if filename=='item_list.txt':result.append('\t'.join(listrows[v]).encode('gbk'))
else:
t=rows[v][2].strip()
desc=DESC.get(t,'') if v in report['kept'] else LOCKED
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()}))
+212
View File
@@ -0,0 +1,212 @@
# 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
21210 暗龙铠甲+0
21230 风龙铠甲+0
21250 魔龙铠甲+0
21270 神龙铠甲+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 21210 暗龙铠甲+0
25 21230 风龙铠甲+0
26 21250 魔龙铠甲+0
27 21270 神龙铠甲+0
28 25163 缩小的头颅
29 25164 蜘蛛钥匙
30 25165 螺旋钥匙
31 25166 缩小的头颅+
32 25167 拉扎多尔入场券
33 25168 奈梅尔入场券
34 25169 九头蛇护身符
35 28444 破碑之石+4
36 28445 克首领之石+4
37 30400 盖亚宝石
38 30600 图样
39 30601 铠甲设计图
40 30602 蓝色染料
41 30603 红色染料
42 30604 灰色染料
43 30605 金板
44 30606 银板
45 30607 铜板
46 30608 钢板
47 30609 青铜板
48 30610 秘银
49 30611 坚韧绑带
50 30612 结实绳索
51 30614 黑色染料
52 30615 白色染料
53 30616 二氧化钛
54 30617 玛瑙
55 30618 月长石
56 30620 红龙之角
57 30621 红龙鳞片
58 30623 智慧之书
59 30624 魔法宝珠
60 30625 帕德玛刚玉
61 30626 蓝晶石
62 30627 神之果实
63 30628 神圣之花
64 30629 众神灵药
65 30630 龙之翼
66 33031 吊坠宝石
67 39070 灰色碎片
68 39071 白色碎片
69 39072 绿色碎片
70 39073 黄色碎片
71 50275 金色欧凯宝箱
72 50276 银色欧凯宝箱
73 50277 铜色欧凯宝箱
74 50569 秘籍·地震
75 50570 秘籍·星光
76 50571 秘籍·风暴射击
77 50572 秘籍·地狱打击
78 50573 秘籍·致命波动
79 50574 秘籍·流星
80 50575 秘籍·灵气护盾
81 50928 金王战利品
82 50929 银王战利品
83 50930 铜王战利品
84 51110 阿拉多尔宝箱
85 51111 奈梅尔宝箱
86 51112 贝兰塞陶宝箱
87 51115 远古守护者宝箱
88 51116 红龙宝箱
89 51117 远古巨龙宝箱
90 53004 鲁弗斯封印(永久)
91 53996 护符附魔道具
92 53997 护符加持道具
93 54705 九头蛇宝箱
94 54706 65级武器宝箱
95 54707 70级武器宝箱
96 54708 75级武器宝箱
97 54709 防具宝箱
98 54710 装备宝箱
99 54711 饰品宝箱
100 54712 生物学家礼包
101 54713 生物学家袋子
102 54714 生物学家袋子+
103 54715 强化药剂包(小)
104 54716 强化药剂包(中)
105 54717 强化药剂包(特大)
106 55001 宠物蛋白补充剂
107 55004 宠物秘籍·幼年
108 55005 宠物秘籍·野性
109 55006 宠物秘籍·勇敢
110 55009 宠物秘籍宝箱
111 55010 宠物书·石碑
112 55011 宠物书·怪物
113 55012 宠物书·首领
114 55013 宠物书·力量
115 55014 宠物书·智力
116 55015 宠物书·敏捷
117 55016 宠物书·体力
118 55017 宠物书·野兽
119 55018 宠物书·兽人
120 55019 宠物书·秘法
121 55020 宠物书·不死
122 55021 宠物书·恶魔
123 55022 宠物书·经验
124 55023 宠物书·掉宝
125 55024 宠物书·金币
126 55025 宠物书·半兽人
127 55026 宠物书·抗半兽人
128 55027 宠物书·技能伤害
129 55028 宠物书·抗技能伤害
130 55029 宠物书·平均伤害
131 55030 宠物书·抗平均伤害
132 55031 宠物书·抗暴击
133 55033 宠物技能清除剂
134 55035 宠物经验增幅剂
135 55036 宠物栏位解锁器
136 55038 宠物书·抗穿透
137 55039 宠物书·施法速度
138 55040 宠物书·格挡
139 55041 宠物书·抗毒
140 55042 宠物书·冰
141 55043 宠物书·土
142 55044 宠物书·暗
143 55101 活力药剂(小)
144 55102 活力药剂(中)
145 55103 活力药剂(大)
146 55104 活力药剂(特大)
147 55108 战斗药剂(小)
148 55109 战斗药剂(中)
149 55110 战斗药剂(大)
150 55111 战斗药剂(特大)
151 55115 力量药剂(小)
152 55116 力量药剂(中)
153 55117 力量药剂(大)
154 55118 力量药剂(特大)
155 55401 猴子蛋
156 55404 奈梅尔蛋
157 55407 阿兹雷尔蛋
158 71097 炼金附魔道具
159 71234 蓝色巴里
160 71254 白色冬日羊驼
161 71301 护符加持道具
162 71302 护符强化道具
163 71502 狮子封印(永久)
164 71505 野猪封印(永久)
165 71508 狼封印(永久)
166 71511 老虎封印(永久)
167 72064 小型扩充券
168 72065 中型扩充券
169 72066 大型扩充券
170 72067 强效扩充券
171 72068 天界扩充券
172 72319 金钥匙
173 72320 银钥匙
174 72346 6-7属性附魔道具
175 77900 药剂宝箱(小)
176 77901 药剂宝箱(中)
177 77902 药剂宝箱(大)
178 77903 药剂宝箱(特大)
179 77904 阅读宝箱
180 77905 护符宝箱
181 77906 耳环宝箱
182 77907 手镯宝箱
183 77908 项链宝箱
184 77909 戒指宝箱
185 77910 强化宝箱(小)
186 77911 强化宝箱(中)
187 77912 强化宝箱(大)
188 77913 炼金宝箱(小)
189 77914 炼金宝箱(中)
190 77915 炼金宝箱(大)
191 77916 战令宝箱
192 77917 任务书宝箱
193 77918 青铜宝箱
194 77919 白银宝箱
195 77920 黄金宝箱
196 77921 青铜钥匙
197 77922 白银钥匙
198 77923 黄金钥匙
199 77924 钥匙宝箱
200 80019 精致布料
201 80025 时装兑换券
202 80030 男爵夫人的宝箱
203 85004 领主披风(个人)
204 85008 大师披风(个人)
205 85014 王朝披风(个人)
206 85018 皇家披风(个人)
207 96001 火之护符碎片
208 96002 冰之护符碎片
209 96003 土之护符碎片
210 96004 暗之护符碎片
211 96005 风之护符碎片
212 96006 雷之护符碎片
@@ -0,0 +1,35 @@
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_NONE','0'])
self.assertEqual(s[11:13],['0','0']) # the +1 item is not imported
self.assertEqual(c[1:3],[self.E['type'].index('ITEM_WEAPON'),0]);self.assertEqual(c[4],1<<self.E['anti'].index('ANTI_MUDANG'))
self.assertEqual(len(s),33);self.assertEqual(len(c),31)
def test_costume_enchant_is_locked(self):
s,c,kept=m.convert(row(54001,'ITEM_USE','USE_CHANGE_ATTRIBUTE',flags='ITEM_STACKABLE',values=('23','10','2','0','0','0')),self.E,set())
self.assertFalse(kept);self.assertEqual(s[2:4],['ITEM_NONE','0']);self.assertEqual(s[24:30],['0']*6)
self.assertEqual(c[1:3],[0,0]);self.assertEqual(c[5],1<<self.E['flag'].index('ITEM_STACKABLE'))
def test_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_names_cover_ranges(self):
n=m.names();self.assertEqual(len(n),478);self.assertEqual(n[54100],'武器外观强化石')
self.assertTrue(all(len(v.encode('gbk'))<=24 for v in n.values()))
if __name__=='__main__':unittest.main()