Files
metin2-client/tools/server/ava2-items/make_items.py
T
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

148 lines
9.2 KiB
Python

#!/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','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_METIN','METIN_NORMAL')}
DESC={'ITEM_METIN':'可镶嵌到武器的空孔中。','ITEM_MATERIAL':'制作与升级材料。','ITEM_QUEST':'任务物品。','ITEM_GIFTBOX':'打开后可获得物品。','ITEM_TREASURE_BOX':'需要对应的钥匙才能打开。','ITEM_TREASURE_KEY':'可以打开对应的宝箱。','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','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 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()}))