#!/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, 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,'特效强化石')) # Dragon armor: +0..+9 per class (the refine chain needs every step). DRAGON={21210:'暗龙铠甲',21230:'风龙铠甲',21250:'魔龙铠甲',21270:'神龙铠甲'} # 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'} KEEP_SUBTYPES={('ITEM_WEAPON',s) for s in ('WEAPON_SWORD','WEAPON_DAGGER','WEAPON_BOW','WEAPON_TWO_HANDED','WEAPON_BELL','WEAPON_FAN')}|{('ITEM_ARMOR','ARMOR_FOOTS'),('ITEM_ARMOR','ARMOR_BODY'),('ITEM_METIN','METIN_NORMAL')}|{('ITEM_COSTUME','COSTUME_SASH')} DESC={'ITEM_METIN':'可镶嵌到武器的空孔中。','ITEM_MATERIAL':'制作与升级材料。','ITEM_QUEST':'任务物品。','ITEM_GIFTBOX':'打开后可获得物品。','ITEM_TREASURE_BOX':'需要对应的钥匙才能打开。','ITEM_TREASURE_KEY':'可以打开对应的宝箱。','ITEM_WEAPON':'','ITEM_ARMOR':'','ITEM_COSTUME':'披风。可在披风工匠处合成升级,或吸收武器、铠甲的部分属性。'} # Sash system (metin2-server docs/FEATURE-AVA2-UNLOCK.md P4): effects registered by the client's mt_sash script # (files they reference are staged too), the window images, and item_scale.txt (model scale per job/sex; a row # covers vnum..vnum+4). SASH_EFFECTS=('d:/ymir work/effect/etc/buff/buff_item6.mse','d:/ymir work/effect/etc/buff/buff_item7.mse', 'd:/ymir work/pc/common/effect/armor/acc_01.mse') SASH_LOCALE={'locale/en/ui/acce/acce_combine.tga':'locale/zh/ui/acce/acce_combine.tga', 'locale/en/ui/acce/acce_absorb.tga':'locale/zh/ui/acce/acce_absorb.tga'} LOCKED='该物品所属的系统尚未开放,暂时无法使用。' def enums(proto_reader): 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) 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','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< 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 for q in shape_models()+effect_files(files): if q not in files:raise SystemExit('missing AVA2 asset %s'%q) target=assets/q;target.parent.mkdir(parents=True,exist_ok=True) if not target.exists():shutil.copy2(files[q],target);copied+=1 (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 source,dest in SASH_LOCALE.items(): (assets/dest).parent.mkdir(parents=True,exist_ok=True);shutil.copy2(files[source],assets/dest);copied+=1 scale=[l for l in (pack_maker/'locale/locale/en/item_scale.txt').read_bytes().splitlines() if l[:5].isdigit() and wanted & set(range(int(l[:5]),int(l[:5])+5))] (target/'item_scale.txt').write_bytes(b'\n'.join(scale)+b'\n') for filename in ('item_list.txt','itemdesc.txt'): result=[l for l in (locale/filename).read_bytes().splitlines() if not (l.split(b'\t',1)[0].isdigit() and int(l.split(b'\t',1)[0]) in wanted)] for v in sorted(wanted): if filename=='item_list.txt':result.append('\t'.join(listrows[v]).encode('gbk')) else: t=rows[v][2].strip() desc=DESC.get(t,'') if v in report['kept'] else LOCKED 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()}))