From 0d2a9ebe361cf138efee707ca9ea4af0afba36b3 Mon Sep 17 00:00:00 2001 From: shenlei Date: Fri, 16 Oct 2026 11:13:41 +0900 Subject: [PATCH] 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 _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 --- src/platform/ScriptLib/ArmorShapesScript.inc | 34 ++++++++++++++++++++ src/platform/ScriptLib/PythonBoot.cpp | 2 ++ tools/server/ava2-items/make_items.py | 23 ++++++++++++- tools/server/ava2-items/names_zh.tsv | 4 --- tools/server/ava2-items/tests/test_items.py | 11 ++++++- 5 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 src/platform/ScriptLib/ArmorShapesScript.inc diff --git a/src/platform/ScriptLib/ArmorShapesScript.inc b/src/platform/ScriptLib/ArmorShapesScript.inc new file mode 100644 index 00000000..eb0eb5b0 --- /dev/null +++ b/src/platform/ScriptLib/ArmorShapesScript.inc @@ -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"; diff --git a/src/platform/ScriptLib/PythonBoot.cpp b/src/platform/ScriptLib/PythonBoot.cpp index 3bbbe85b..8927d15d 100644 --- a/src/platform/ScriptLib/PythonBoot.cpp +++ b/src/platform/ScriptLib/PythonBoot.cpp @@ -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); } diff --git a/tools/server/ava2-items/make_items.py b/tools/server/ava2-items/make_items.py index 70b178e1..9b6b0f52 100644 --- a/tools/server/ava2-items/make_items.py +++ b/tools/server/ava2-items/make_items.py @@ -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) diff --git a/tools/server/ava2-items/names_zh.tsv b/tools/server/ava2-items/names_zh.tsv index afeeef9e..cea1187c 100644 --- a/tools/server/ava2-items/names_zh.tsv +++ b/tools/server/ava2-items/names_zh.tsv @@ -21,10 +21,6 @@ 19520 石榴石戒指+0 19540 祖母绿戒指+0 19560 蓝宝石戒指+0 -21210 暗龙铠甲+0 -21230 风龙铠甲+0 -21250 魔龙铠甲+0 -21270 神龙铠甲+0 25163 缩小的头颅 25164 蜘蛛钥匙 25165 螺旋钥匙 diff --git a/tools/server/ava2-items/tests/test_items.py b/tools/server/ava2-items/tests/test_items.py index 3faad9d1..9bd5e607 100644 --- a/tools/server/ava2-items/tests/test_items.py +++ b/tools/server/ava2-items/tests/test_items.py @@ -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()