- add MeshPort peer-to-peer overlay networking SDK, C++ integration, and Python bindings - add item actions quick use/split/delete/lock overlay window (mt_item_actions) - add shop name range and NPC name visibility settings to system display options - support battlepass ticket activation and month validation in mt_battlepass - support boss tracking teleport destination button in mt_bosses - support asynchronous account registration cancellation and pending login cancellation - update AVA2 item conversion for attr lock scrolls and battlepass tickets - update .gitignore for local build outputs and caches
170 lines
10 KiB
Python
170 lines
10 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,tarfile
|
|
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
|
|
result.update({50348:'属性锁定符',50349:'属性解锁符'})
|
|
for month in range(1,13):result[25149+month]='%d月战令票'%month
|
|
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 (int(c[0]) in (50348,50349) and (t,st)==('ITEM_USE','USE_CHANGE_ATTRIBUTE'))
|
|
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={}
|
|
proto=db/'item_proto.txt'
|
|
if proto.is_file():raw=proto.read_bytes()
|
|
else:
|
|
# The checked-out reference normally contains the original archive,
|
|
# not somebody else's AVA2/TEST extraction directory.
|
|
with tarfile.open(ava/'2. Backup SF+SRC and SQL/FILES+SRC.tgz','r:gz') as source:
|
|
member=source.extractfile('home/srv1/db/item_proto.txt')
|
|
if member is None:raise ValueError('AVA2 archive has no item_proto.txt')
|
|
raw=member.read()
|
|
for line in raw.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]
|
|
# The archived server DB omits the lock materials even though its gameplay
|
|
# code registers them. AVA2's client DumpProto ships their actual definitions.
|
|
if not {50348,50349}<=rows.keys():
|
|
dump=ava/'3. Source BIN/Tools/DumpProto/Release/item_proto.txt'
|
|
if not dump.is_file():dump=ava.parent/'3. Source BIN/Tools/DumpProto/Release/item_proto.txt'
|
|
for line in dump.read_bytes().splitlines():
|
|
cols=line.split(b'\t')
|
|
if cols[0] in (b'50348',b'50349'):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
|
|
if v==50348:desc='装备有5条普通属性时,可锁定其中一条;最多锁定2组。每次消耗1张。'
|
|
elif v==50349:desc='解除装备上的属性锁定,每次消耗1张。'
|
|
elif 25150<=v<=25161:desc='仅限对应月份激活战令,确认后消耗;激活后开始累计任务进度。'
|
|
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()}))
|