AVA2 unlock P3: dragon armor 21210-21279 (+0..+9) with armor shape 32

Keep ARMOR_BODY rows, import the whole refine chain, ship AVA2's <race>_7_1
models and register shape 32 after each race msm load (mt_shapes): the race
msm are read by bare name from the root pack and cannot be overridden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-10-16 11:13:41 +09:00
co-authored by Claude Opus 5.5
parent 178fcf48fe
commit 0d2a9ebe36
5 changed files with 68 additions and 6 deletions
@@ -0,0 +1,34 @@
// PORT: armor shapes added after 40250 (AVA2's dragon armor 21210+, item value3 = 32). The race .msm files are
// read by bare name, which CEterPackManager always serves from the root pack, so a patch pack cannot replace
// them; register the extra shapes after each LoadLocalRaceData instead, as the AVA2 msm groups would. Models
// ship in the ava2items pack (metin2-client tools/server/ava2-items, ARMOR_SHAPES / RACE_PATHS).
static const char c_szArmorShapesScript[] = R"PY(# -*- coding: utf-8 -*-
import chrmgr
SHAPES = {32: "%s_7_1"}
# ShapeData PathName of each race msm (male, female).
PATHS = {"warrior": ("pc", "pc2"), "assassin": ("pc2", "pc"), "sura": ("pc", "pc2"), "shaman": ("pc2", "pc")}
_load = chrmgr.LoadLocalRaceData
def Register(name):
stem = name.lower().split(".")[0]
race, _, sex = stem.partition("_")
if race not in PATHS or sex not in ("m", "w"):
return
path = "d:/ymir work/%s/%s/" % (PATHS[race][sex == "w"], race)
for shape, model in SHAPES.items():
model = path + model % race
chrmgr.SetShapeModel(shape, model + ".gr2")
chrmgr.AppendShapeSkin(shape, 0, model + ".dds", model + ".dds")
def LoadLocalRaceData(name):
result = _load(name)
Register(name)
return result
chrmgr.LoadLocalRaceData = LoadLocalRaceData
)PY";
+2
View File
@@ -1012,6 +1012,7 @@ namespace PythonBoot
#include "ModelPreviewScript.inc"
#include "BattlePassScript.inc"
#include "AppliesScript.inc"
#include "ArmorShapesScript.inc"
static void install_script_module(const char* c_szModule, const char* c_szScript)
{
@@ -1095,6 +1096,7 @@ bool RunMainScript(const char* lpCmdLine, std::string* error)
install_script_module("mt_preview", c_szModelPreviewScript);
install_script_module("mt_battlepass", c_szBattlePassScript);
install_script_module("mt_applies", c_szAppliesScript);
install_script_module("mt_shapes", c_szArmorShapesScript);
if (g_touch_input)
install_script_module("mt_mobile_hud", c_szMobileHUDScript);
}
+22 -1
View File
@@ -9,9 +9,15 @@ 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_METIN','METIN_NORMAL')}
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')}
DESC={'ITEM_METIN':'可镶嵌到武器的空孔中。','ITEM_MATERIAL':'制作与升级材料。','ITEM_QUEST':'任务物品。','ITEM_GIFTBOX':'打开后可获得物品。','ITEM_TREASURE_BOX':'需要对应的钥匙才能打开。','ITEM_TREASURE_KEY':'可以打开对应的宝箱。','ITEM_WEAPON':'','ITEM_ARMOR':''}
LOCKED='该物品所属的系统尚未开放,暂时无法使用。'
@@ -38,6 +44,8 @@ def names():
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):
@@ -73,6 +81,15 @@ def convert(cols,E,imported):
client+=[int(v) for v in values]+[number(c[30]),number(c[31])]
return server,client,kept
def shape_models():
"""Pack paths of the ARMOR_SHAPES models (+ LODs) and skins of every race."""
out=[]
for race,dirs in RACE_PATHS.items():
for d in dict.fromkeys(dirs):
for stem in ARMOR_SHAPES.values():
out+=['d:/ymir work/%s/%s/%s%s'%(d,race,stem%race,e) for e in ('.gr2','.dds','_lod_01.gr2','_lod_02.gr2','_lod_03.gr2')]
return out
def pack_index(pack_maker):
"""ArchivedPath (lowercase) -> source file, from the AVA2 pack manifests."""
index={}
@@ -117,6 +134,10 @@ def prepare(ava,proto_reader,locale,output,tool,packer,wrap=lambda text:text):
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():
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)
-4
View File
@@ -21,10 +21,6 @@
19520 石榴石戒指+0
19540 祖母绿戒指+0
19560 蓝宝石戒指+0
21210 暗龙铠甲+0
21230 风龙铠甲+0
21250 魔龙铠甲+0
21270 神龙铠甲+0
25163 缩小的头颅
25164 蜘蛛钥匙
25165 螺旋钥匙
1 # AVA2 drop items imported as 40250 items (vnum<TAB>zh name). 54001-54267 are named by range in make_items.py.
21 19520
22 19540
23 19560
21210
21230
21250
21270
24 25163
25 25164
26 25165
+10 -1
View File
@@ -39,8 +39,17 @@ class ConvertTests(unittest.TestCase):
self.assertTrue(kept);self.assertEqual(s[7],'WEAR_WEAPON');self.assertEqual(s[18:20],['APPLY_ATTBONUS_STONE','8'])
self.assertEqual(c[17:19],[139,8]) # AVA2 numbering, shared with the server and client enums
self.assertEqual(self.E['apply'].index('APPLY_ATTBONUS_BOSS'),140);self.assertEqual(s[29],'31')
def test_dragon_armor_kept_with_refine_chain(self):
s,c,kept=m.convert(row(21210,'ITEM_ARMOR','ARMOR_BODY','ANTI_ASSASSIN | ANTI_SURA | ANTI_MUDANG | ANTI_WOLFMAN','ITEM_TUNABLE','WEAR_BODY',
('APPLY_MOV_SPEED','5','APPLY_RESIST_MAGIC','25','APPLY_MAX_HP_PCT','20'),('0','210','0','32','0','0'),('21211','81')),self.E,{21210,21211})
self.assertTrue(kept);self.assertEqual(s[3],'ARMOR_BODY');self.assertEqual(s[7],'WEAR_BODY');self.assertEqual(s[11:13],['21211','81'])
self.assertEqual(s[5],'ANTI_ASSASSIN | ANTI_SURA | ANTI_MUDANG');self.assertEqual(s[27],'32') # value3 = armor shape
models=m.shape_models();self.assertEqual(len(models),40)
self.assertIn('d:/ymir work/pc2/warrior/warrior_7_1_lod_03.gr2',models)
def test_names_cover_ranges(self):
n=m.names();self.assertEqual(len(n),478);self.assertEqual(n[54100],'武器外观强化石')
n=m.names();self.assertEqual(len(n),514);self.assertEqual(n[54100],'武器外观强化石')
self.assertEqual([n[v] for v in (21210,21219,21279)],['暗龙铠甲+0','暗龙铠甲+9','神龙铠甲+9'])
self.assertTrue(all(len(v.encode('gbk'))<=24 for v in n.values()))
if __name__=='__main__':unittest.main()