Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f266072f67 | ||
|
|
90f213c4cc | ||
|
|
d11d46c469 |
@@ -356,7 +356,7 @@ def build(en: Path, zh: Path, output: Path, client_base: Path,
|
||||
[str(proto_tool), str(en / "item_proto"), str(en / "mob_proto"),
|
||||
str(item_names), str(mob_names), str(locale_dir / "item_proto"),
|
||||
str(locale_dir / "mob_proto")] + ([str(limits)] if limits.is_file() else [])
|
||||
+ ([str(names_dir / "item_stones_server.tsv")] if (names_dir / "item_stones_server.tsv").is_file() else []),
|
||||
+ ([str(names_dir / "item_stones_server.tsv")] if (names_dir / "item_stones_server.tsv").is_file() else [])
|
||||
# item_shapes_server.tsv follows the stone overlay (positional): size and flags of new-use items.
|
||||
+ ([str(names_dir / "item_shapes_server.tsv")] if (names_dir / "item_shapes_server.tsv").is_file() else []),
|
||||
check=True, capture_output=True, text=True,
|
||||
@@ -397,7 +397,10 @@ def build_biolog(output: Path, pack_tool: Path):
|
||||
root = REPO / "tools/server/biolog"
|
||||
ava = REPO.parent / "AVA2"
|
||||
archive = ava / "2. Backup SF+SRC and SQL/FILES+SRC.tgz"
|
||||
if not archive.is_file():
|
||||
extracted = ava / "TEST/2. Backup SF+SRC and SQL/home/srv1/db/item_proto.txt"
|
||||
if not archive.is_file() and extracted.is_file():
|
||||
ava = ava / "TEST" # the reference unpacked under AVA2/TEST
|
||||
elif not archive.is_file():
|
||||
raise ValueError("AVA2 biology requires the local FILES+SRC.tgz reference")
|
||||
proto_tool = tool("biolog_proto_patch")
|
||||
if not proto_tool.is_file():
|
||||
@@ -405,11 +408,14 @@ def build_biolog(output: Path, pack_tool: Path):
|
||||
stage = output.parent / "biolog"
|
||||
stage.mkdir(parents=True, exist_ok=True)
|
||||
proto = stage / "ava2-item-proto.txt"
|
||||
with tarfile.open(archive, "r:gz") as source:
|
||||
member = source.extractfile("home/srv1/db/item_proto.txt")
|
||||
if member is None:
|
||||
raise ValueError("AVA2 item table missing")
|
||||
proto.write_bytes(member.read())
|
||||
if not archive.is_file():
|
||||
shutil.copyfile(extracted, proto)
|
||||
else:
|
||||
with tarfile.open(archive, "r:gz") as source:
|
||||
member = source.extractfile("home/srv1/db/item_proto.txt")
|
||||
if member is None:
|
||||
raise ValueError("AVA2 item table missing")
|
||||
proto.write_bytes(member.read())
|
||||
spec = importlib.util.spec_from_file_location("biolog_assets", root / "make_assets.py")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
@@ -425,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."""
|
||||
@@ -466,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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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()}))
|
||||
@@ -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 雷之护符碎片
|
||||
|
@@ -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()
|
||||
Reference in New Issue
Block a user