diff --git a/scripts/build_zh_locale.py b/scripts/build_zh_locale.py index 959e42c3..cd472a7b 100644 --- a/scripts/build_zh_locale.py +++ b/scripts/build_zh_locale.py @@ -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") diff --git a/src/port/CMakeLists.txt b/src/port/CMakeLists.txt index fda2583b..2c6eac70 100644 --- a/src/port/CMakeLists.txt +++ b/src/port/CMakeLists.txt @@ -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) diff --git a/tests/port/item_proto_patch.cpp b/tests/port/item_proto_patch.cpp new file mode 100644 index 00000000..a46609ac --- /dev/null +++ b/tests/port/item_proto_patch.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +using Item=CItemData::TItemTable; +static constexpr std::array item_key{{173217,72619434,408587239,27973291}}; +static std::vector 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(stream),std::istreambuf_iterator()}; +} +static DWORD word(const std::vector& 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& 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 rows; + for(DWORD i=0;i 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 plain;for(const auto& pair:rows){const BYTE* bytes=reinterpret_cast(&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 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(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;} +} diff --git a/tools/server/ava2-items/install_items.py b/tools/server/ava2-items/install_items.py new file mode 100644 index 00000000..d141956b --- /dev/null +++ b/tools/server/ava2-items/install_items.py @@ -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 +""" +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) diff --git a/tools/server/ava2-items/make_items.py b/tools/server/ava2-items/make_items.py new file mode 100644 index 00000000..e1b9234e --- /dev/null +++ b/tools/server/ava2-items/make_items.py @@ -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< 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="([^"]+)">',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()})) diff --git a/tools/server/ava2-items/names_zh.tsv b/tools/server/ava2-items/names_zh.tsv new file mode 100644 index 00000000..afeeef9e --- /dev/null +++ b/tools/server/ava2-items/names_zh.tsv @@ -0,0 +1,212 @@ +# AVA2 drop items imported as 40250 items (vnumzh 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 雷之护符碎片 diff --git a/tools/server/ava2-items/tests/test_items.py b/tools/server/ava2-items/tests/test_items.py new file mode 100644 index 00000000..200e665c --- /dev/null +++ b/tools/server/ava2-items/tests/test_items.py @@ -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<