完善客户端功能并同步差距文档
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
// M2 §3.5 —— playersettingmodule.py 的 ComboAttackNew 调用 -> PC 连击段表。
|
||||
// 照 GameLib/RaceData.cpp CRaceData::ReserveComboAttack / RegisterComboAttack 语义。
|
||||
#include <combo_table.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
using namespace fmt;
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(c, msg) \
|
||||
do { \
|
||||
if (!(c)) { \
|
||||
std::fprintf(stderr, "FAIL: %s\n", (msg)); \
|
||||
++g_fail; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
// warrior_m/_w 共用的 __LoadGameWarriorEx 片段 + assassin 的 DUALHAND(男限),
|
||||
// 外加一个非 Ex 的 def 用来验证块复位。缩进 / tab 混用照原文。
|
||||
static const char *kPy = R"PY(
|
||||
COMBO_TYPE_1 = 0
|
||||
COMBO_TYPE_2 = 1
|
||||
COMBO_TYPE_3 = 2
|
||||
COMBO_INDEX_1 = 0
|
||||
COMBO_INDEX_6 = 5
|
||||
|
||||
def __LoadGameWarriorEx(race, path):
|
||||
chrmgr.SelectRace(race)
|
||||
|
||||
chrmgr.ReserveComboAttackNew(chr.MOTION_MODE_GENERAL, COMBO_TYPE_1, 1)
|
||||
chrmgr.RegisterComboAttackNew(chr.MOTION_MODE_GENERAL, COMBO_TYPE_1, COMBO_INDEX_1, chr.MOTION_COMBO_ATTACK_1)
|
||||
|
||||
## Combo Type 1
|
||||
chrmgr.ReserveComboAttackNew(chr.MOTION_MODE_ONEHAND_SWORD, COMBO_TYPE_1, 4)
|
||||
chrmgr.RegisterComboAttackNew(chr.MOTION_MODE_ONEHAND_SWORD, COMBO_TYPE_1, COMBO_INDEX_1, chr.MOTION_COMBO_ATTACK_1)
|
||||
chrmgr.RegisterComboAttackNew(chr.MOTION_MODE_ONEHAND_SWORD, COMBO_TYPE_1, COMBO_INDEX_2, chr.MOTION_COMBO_ATTACK_2)
|
||||
chrmgr.RegisterComboAttackNew(chr.MOTION_MODE_ONEHAND_SWORD, COMBO_TYPE_1, COMBO_INDEX_3, chr.MOTION_COMBO_ATTACK_3)
|
||||
chrmgr.RegisterComboAttackNew(chr.MOTION_MODE_ONEHAND_SWORD, COMBO_TYPE_1, COMBO_INDEX_4, chr.MOTION_COMBO_ATTACK_4)
|
||||
## Combo Type 2 —— 段号跳到 5 / 7(非连续,验证按下标写而非追加)
|
||||
chrmgr.ReserveComboAttackNew(chr.MOTION_MODE_ONEHAND_SWORD, COMBO_TYPE_2, 5)
|
||||
chrmgr.RegisterComboAttackNew(chr.MOTION_MODE_ONEHAND_SWORD, COMBO_TYPE_2, COMBO_INDEX_1, chr.MOTION_COMBO_ATTACK_1)
|
||||
chrmgr.RegisterComboAttackNew(chr.MOTION_MODE_ONEHAND_SWORD, COMBO_TYPE_2, COMBO_INDEX_2, chr.MOTION_COMBO_ATTACK_2)
|
||||
chrmgr.RegisterComboAttackNew(chr.MOTION_MODE_ONEHAND_SWORD, COMBO_TYPE_2, COMBO_INDEX_3, chr.MOTION_COMBO_ATTACK_3)
|
||||
chrmgr.RegisterComboAttackNew(chr.MOTION_MODE_ONEHAND_SWORD, COMBO_TYPE_2, COMBO_INDEX_4, chr.MOTION_COMBO_ATTACK_5)
|
||||
chrmgr.RegisterComboAttackNew(chr.MOTION_MODE_ONEHAND_SWORD, COMBO_TYPE_2, COMBO_INDEX_5, chr.MOTION_COMBO_ATTACK_7)
|
||||
# 越界的 Register(下标 5 >= size 5):照参考端 AssertLog + return,忽略
|
||||
chrmgr.RegisterComboAttackNew(chr.MOTION_MODE_ONEHAND_SWORD, COMBO_TYPE_2, 5, chr.MOTION_COMBO_ATTACK_8)
|
||||
# 对已存在 key 的重复 Reserve:map::insert 不覆盖,原向量保留
|
||||
chrmgr.ReserveComboAttackNew(chr.MOTION_MODE_ONEHAND_SWORD, COMBO_TYPE_2, 99)
|
||||
|
||||
def __LoadGameAssassinEx(race, path):
|
||||
chrmgr.SelectRace(race)
|
||||
chrmgr.ReserveComboAttackNew(chr.MOTION_MODE_DUALHAND_SWORD, COMBO_TYPE_3, 6)
|
||||
chrmgr.RegisterComboAttackNew(chr.MOTION_MODE_DUALHAND_SWORD, COMBO_TYPE_3, COMBO_INDEX_6, chr.MOTION_COMBO_ATTACK_8)
|
||||
|
||||
def __LoadGameNPC():
|
||||
# 块外的调用不该落到任何职业里
|
||||
chrmgr.ReserveComboAttackNew(chr.MOTION_MODE_GENERAL, COMBO_TYPE_1, 3)
|
||||
)PY";
|
||||
|
||||
static void check_vec(const std::vector<uint16_t> *v, std::initializer_list<uint16_t> want, const char *msg) {
|
||||
if (!v) { std::fprintf(stderr, "FAIL: %s (nullptr)\n", msg); ++g_fail; return; }
|
||||
if (v->size() != want.size()) { std::fprintf(stderr, "FAIL: %s (size %zu != %zu)\n", msg, v->size(), want.size()); ++g_fail; return; }
|
||||
size_t i = 0;
|
||||
for (uint16_t w : want) {
|
||||
if ((*v)[i] != w) { std::fprintf(stderr, "FAIL: %s ([%zu]=%u != %u)\n", msg, i, (*v)[i], w); ++g_fail; return; }
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
// ---- key 编码:RaceData.h MAKE_COMBO_KEY -------------------------------
|
||||
CHECK(make_combo_key(COMBO_MODE_ONEHAND_SWORD, 1) == ((2u << 16) | 1u), "make_combo_key");
|
||||
CHECK(combo_key_mode(make_combo_key(5, 2)) == 5 && combo_key_type(make_combo_key(5, 2)) == 2, "combo_key split");
|
||||
|
||||
// ---- 解析 ------------------------------------------------------------
|
||||
PlayerComboTables tbl;
|
||||
std::string err;
|
||||
if (!parse_player_combo_tables(kPy, tbl, &err)) {
|
||||
std::fprintf(stderr, "parse failed: %s\n", err.c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
const ComboTable &w = tbl.klass(COMBO_CLASS_WARRIOR);
|
||||
const ComboTable &a = tbl.klass(COMBO_CLASS_ASSASSIN);
|
||||
const ComboTable &s = tbl.klass(COMBO_CLASS_SURA);
|
||||
|
||||
// GENERAL / type1 —— 单段普攻连击(NAME_COMBO_ATTACK_1 = 14)
|
||||
check_vec(w.get(COMBO_MODE_GENERAL, 0), {14}, "warrior GENERAL type1 = [14]");
|
||||
|
||||
// ONEHAND_SWORD / type1 —— 连续 4 段 14..17
|
||||
check_vec(w.get(COMBO_MODE_ONEHAND_SWORD, 0), {14, 15, 16, 17}, "warrior 1H type1 = [14,15,16,17]");
|
||||
|
||||
// ONEHAND_SWORD / type2 —— 5 段,末两段跳到 18 / 20(COMBO_ATTACK_5 / _7)
|
||||
check_vec(w.get(COMBO_MODE_ONEHAND_SWORD, 1), {14, 15, 16, 18, 20}, "warrior 1H type2 = [14,15,16,18,20]");
|
||||
|
||||
// 越界 Register 被忽略;重复 Reserve 不覆盖(size 仍是 5,不是 99)
|
||||
CHECK(w.get(COMBO_MODE_ONEHAND_SWORD, 1)->size() == 5, "dup Reserve ignored / OOB Register ignored");
|
||||
|
||||
// type3 从没 Reserve 过 -> GetComboDataPointer 应为 nullptr
|
||||
CHECK(w.get(COMBO_MODE_ONEHAND_SWORD, 2) == nullptr, "warrior 1H type3 absent -> nullptr");
|
||||
|
||||
// 职业隔离:DUALHAND 只在 assassin 块里;warrior/sura 没有
|
||||
check_vec(a.get(COMBO_MODE_DUALHAND_SWORD, 2), {0, 0, 0, 0, 0, 21}, "assassin DUAL type3 idx5 = COMBO_ATTACK_8(21)");
|
||||
CHECK(w.get(COMBO_MODE_DUALHAND_SWORD, 2) == nullptr, "warrior has no DUALHAND table");
|
||||
CHECK(s.empty(), "sura block absent in snippet -> empty table");
|
||||
|
||||
// 块外(__LoadGameNPC)的 Reserve 不该落进任何职业
|
||||
size_t total = 0;
|
||||
for (int c = 0; c < COMBO_CLASS_COUNT; ++c) total += tbl.klass(c).key_count();
|
||||
CHECK(total == 4, "only the 4 in-Ex-block combo keys registered (2 warrior 1H + 1 warrior GENERAL + 1 assassin DUAL)");
|
||||
|
||||
// 越界职业索引 -> 空表,不崩
|
||||
CHECK(tbl.klass(-1).empty() && tbl.klass(99).empty(), "out-of-range class -> empty");
|
||||
|
||||
// ---- 可选:真实 playersettingmodule.py(设了 M2_ASSETS 才跑)----------
|
||||
if (const char *root = std::getenv("M2_ASSETS")) {
|
||||
const std::string path = std::string(root) + "/root/playersettingmodule.py";
|
||||
PlayerComboTables live;
|
||||
std::string lerr;
|
||||
if (parse_player_combo_tables_file(path, live, &lerr)) {
|
||||
check_vec(live.klass(COMBO_CLASS_WARRIOR).get(COMBO_MODE_ONEHAND_SWORD, 0),
|
||||
{14, 15, 16, 17}, "live warrior 1H type1");
|
||||
// sura ONEHAND type3 末段回到 COMBO_ATTACK_4 (17)
|
||||
const auto *sv = live.klass(COMBO_CLASS_SURA).get(COMBO_MODE_ONEHAND_SWORD, 2);
|
||||
CHECK(sv && sv->size() == 6 && sv->back() == 17, "live sura 1H type3 tail = COMBO_ATTACK_4");
|
||||
// shaman FAN type1 = 4 连续段
|
||||
check_vec(live.klass(COMBO_CLASS_SHAMAN).get(COMBO_MODE_FAN, 0),
|
||||
{14, 15, 16, 17}, "live shaman FAN type1");
|
||||
std::printf("live playersettingmodule.py: OK\n");
|
||||
} else {
|
||||
std::fprintf(stderr, "live parse skipped: %s\n", lerr.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (g_fail == 0) {
|
||||
std::printf("PASS: formats_combo_table_test (§3.5 PC 连击段表)\n");
|
||||
}
|
||||
return g_fail == 0 ? 0 : 1;
|
||||
}
|
||||
@@ -199,12 +199,22 @@ Group DirectionalLight
|
||||
Group Fog
|
||||
{
|
||||
foglevel 6
|
||||
IsDensity 1
|
||||
Color 0.69 0.74 0.83 1.0
|
||||
}
|
||||
Group Filter
|
||||
{
|
||||
Enable 1
|
||||
Color 0.1 0.2 0.3 0.4
|
||||
AlphaSrc 5
|
||||
AlphaDest 6
|
||||
}
|
||||
Group SkyBox
|
||||
{
|
||||
BTextureRenderMode 1
|
||||
Scale 3500.0 3500.0 3500.0
|
||||
CloudTextureFileName "d:/ymir work/environment/clouds_zone01.tga"
|
||||
FrontFaceFileName "d:/ymir work/environment/skybox/front.dds"
|
||||
List Gradient
|
||||
{
|
||||
0.09 0.28 0.65 0.0
|
||||
@@ -221,6 +231,12 @@ Group SkyBox
|
||||
CHECK(e.dir_light.bg_enable && e.dir_light.ch_enable, "env bg/ch enable");
|
||||
CHECK(std::fabs(e.dir_light.bg_diffuse[1] - 0.97f) < 1e-3f, "env bg diffuse");
|
||||
CHECK(e.fog.enable && e.fog.fog_level == 6, "env fog (foglevel form)");
|
||||
CHECK(e.fog.density_fog, "env density fog flag");
|
||||
CHECK(e.filter.enable && e.filter.alpha_src == 5 && e.filter.alpha_dest == 6,
|
||||
"env screen filter fields");
|
||||
CHECK(std::fabs(e.filter.color[2] - 0.3f) < 1e-3f, "env screen filter color");
|
||||
CHECK(e.sky.texture_render_mode && e.sky.face_textures[0].find("front.dds") != std::string::npos,
|
||||
"env skybox texture mode + front face");
|
||||
CHECK(std::fabs(e.sky.scale[0] - 3500.0f) < 1e-1f, "env skybox scale");
|
||||
CHECK(e.sky.cloud_texture.find("clouds_zone01.tga") != std::string::npos, "env cloud tex path");
|
||||
CHECK(e.sky.gradient.size() == 3, "env List Gradient rows");
|
||||
@@ -452,6 +468,17 @@ static void test_live(const std::string& assets) {
|
||||
CHECK(env.script_type == "EnvrionmentData", "live msenv script_type");
|
||||
CHECK(env.fog.enable && env.sky.gradient.size() >= 8, "live msenv fog + gradient list");
|
||||
|
||||
fmt::Environment trent;
|
||||
CHECK(fmt::parse_environment_file(
|
||||
assets + "/season2/season2/metin2_map_trent02_a/trent02.msenv", trent, &err),
|
||||
("live trent02.msenv: " + err).c_str());
|
||||
CHECK(trent.dir_light.ch_enable && trent.dir_light.ch_ambient[0] > 0.39f,
|
||||
"live trent character light");
|
||||
CHECK(!trent.filter.enable && trent.filter.alpha_src == 2 && trent.filter.alpha_dest == 2,
|
||||
"live trent filter fields");
|
||||
CHECK(trent.sky.cloud_color.size() == 2 && trent.lens_flare.enable,
|
||||
"live trent cloud gradient + lens flare");
|
||||
|
||||
// Property CRC 注册表
|
||||
fmt::PropertyRegistry reg;
|
||||
CHECK(reg.scan(assets + "/Property", &err), ("live Property scan: " + err).c_str());
|
||||
|
||||
@@ -30,5 +30,202 @@ Group LoopData
|
||||
std::fprintf(stderr, "LoopData fields were not preserved\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// A motion with no ComboInputData / AttackingData leaves the flags clear.
|
||||
if (msa.has_combo_input || msa.has_attacking_data) {
|
||||
std::fprintf(stderr, "combat groups reported present when absent\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// CLIENT-GAP §3.3 / §3.5 —— real onehand_sword/combo_01.msa field values.
|
||||
const std::string combo = R"(
|
||||
ScriptType MotionData
|
||||
MotionFileName "d:/ymir work/pc/warrior/onehand_sword/combo_01.gr2"
|
||||
MotionDuration 1.000000
|
||||
Accumulation 0.00 -131.76 0.00
|
||||
Group ComboInputData
|
||||
{
|
||||
PreInputTime 0.167094
|
||||
DirectInputTime 0.533333
|
||||
InputLimitTime 0.602564
|
||||
LinkTime 0.058889
|
||||
}
|
||||
Group AttackingData
|
||||
{
|
||||
AttackingType 2
|
||||
HittingType 2
|
||||
StiffenTime 0.0
|
||||
InvisibleTime 0.100000
|
||||
ExternalForce 8.000000
|
||||
AttackingStartTime 0.192308
|
||||
AttackingEndTime 0.315385
|
||||
}
|
||||
)";
|
||||
fmt::Msa cm;
|
||||
if (!fmt::parse_msa(combo, cm, &err)) {
|
||||
std::fprintf(stderr, "combo parse failed: %s\n", err.c_str());
|
||||
return 1;
|
||||
}
|
||||
const bool combo_ok = cm.has_combo_input &&
|
||||
std::fabs(cm.combo_pre_input_time - 0.167094f) < 1e-5f &&
|
||||
std::fabs(cm.combo_direct_input_time - 0.533333f) < 1e-5f &&
|
||||
std::fabs(cm.combo_input_limit_time - 0.602564f) < 1e-5f &&
|
||||
std::fabs(cm.combo_link_time - 0.058889f) < 1e-5f;
|
||||
const bool atk_ok = cm.has_attacking_data && cm.attacking_type == 2 && cm.hitting_type == 2 &&
|
||||
std::fabs(cm.attack_start_time - 0.192308f) < 1e-5f &&
|
||||
std::fabs(cm.attack_end_time - 0.315385f) < 1e-5f &&
|
||||
std::fabs(cm.invisible_time - 0.1f) < 1e-5f &&
|
||||
std::fabs(cm.external_force - 8.0f) < 1e-5f;
|
||||
if (!combo_ok || !atk_ok) {
|
||||
std::fprintf(stderr, "ComboInputData / AttackingData fields were not preserved\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// §3.5 修改 1 —— 老式内联写法(无 HitDataCount):单个命中窗,含 List HitPosition 采样。
|
||||
const std::string inl = R"(
|
||||
ScriptType MotionData
|
||||
MotionFileName "d:/ymir work/pc/warrior/general/attack.gr2"
|
||||
MotionDuration 1.0
|
||||
Group AttackingData
|
||||
{
|
||||
AttackingType 1
|
||||
HittingType 2
|
||||
StiffenTime 0.0
|
||||
InvisibleTime 0.5
|
||||
ExternalForce 0.0
|
||||
AttackingStartTime 0.456410
|
||||
AttackingEndTime 0.597436
|
||||
AttackingBone "equip_right_hand"
|
||||
WeaponLength 0.0
|
||||
List HitPosition
|
||||
{
|
||||
0.456410 1.0 2.0 3.0 4.0 5.0 6.0
|
||||
0.464744 7.0 8.0 9.0 10.0 11.0 12.0
|
||||
}
|
||||
}
|
||||
)";
|
||||
fmt::Msa im;
|
||||
if (!fmt::parse_msa(inl, im, &err)) {
|
||||
std::fprintf(stderr, "inline attacking parse failed: %s\n", err.c_str());
|
||||
return 1;
|
||||
}
|
||||
if (im.hit_windows.size() != 1 || im.hit_windows[0].samples.size() != 2 ||
|
||||
im.hit_windows[0].bone_name != "equip_right_hand" ||
|
||||
std::fabs(im.hit_windows[0].start_time - 0.456410f) > 1e-5f ||
|
||||
std::fabs(im.hit_windows[0].end_time - 0.597436f) > 1e-5f ||
|
||||
std::fabs(im.attack_start_time - 0.456410f) > 1e-5f ||
|
||||
std::fabs(im.hit_windows[0].samples[1].time - 0.464744f) > 1e-5f ||
|
||||
std::fabs(im.hit_windows[0].samples[1].last_pos[0] - 7.0f) > 1e-5f ||
|
||||
std::fabs(im.hit_windows[0].samples[1].pos[2] - 12.0f) > 1e-5f) {
|
||||
std::fprintf(stderr, "inline HitData window / HitPosition samples not preserved\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// §3.5 修改 1 —— 新式写法:AttackType + MotionType + HitDataCount N + Group HitData0N。
|
||||
const std::string multi = R"(
|
||||
ScriptType MotionData
|
||||
MotionFileName "d:/ymir work/pc/warrior/twohand_sword/combo_05.gr2"
|
||||
MotionDuration 1.6
|
||||
Group AttackingData
|
||||
{
|
||||
AttackType 0
|
||||
HittingType 2
|
||||
StiffenTime 0.0
|
||||
InvisibleTime 0.2
|
||||
ExternalForce 5.0
|
||||
HitLimitCount 3
|
||||
MotionType 2
|
||||
HitDataCount 2
|
||||
Group HitData00
|
||||
{
|
||||
AttackingStartTime 0.455475
|
||||
AttackingEndTime 0.759124
|
||||
AttackingBone "equip_right_hand"
|
||||
WeaponLength 150.0
|
||||
List HitPosition
|
||||
{
|
||||
0.455475 54.5 -30.5 71.1 89.9 105.7 19.4
|
||||
}
|
||||
}
|
||||
Group HitData01
|
||||
{
|
||||
AttackingStartTime 0.9
|
||||
AttackingEndTime 1.1
|
||||
AttackingBone "equip_left_hand"
|
||||
WeaponLength 120.0
|
||||
}
|
||||
}
|
||||
)";
|
||||
fmt::Msa mm;
|
||||
if (!fmt::parse_msa(multi, mm, &err)) {
|
||||
std::fprintf(stderr, "multi HitData parse failed: %s\n", err.c_str());
|
||||
return 1;
|
||||
}
|
||||
if (mm.attacking_type != 0 || mm.motion_type != 2 || mm.hit_limit_count != 3 ||
|
||||
mm.hit_windows.size() != 2 ||
|
||||
std::fabs(mm.hit_windows[0].start_time - 0.455475f) > 1e-5f ||
|
||||
mm.hit_windows[0].samples.size() != 1 ||
|
||||
mm.hit_windows[1].bone_name != "equip_left_hand" ||
|
||||
std::fabs(mm.hit_windows[1].end_time - 1.1f) > 1e-5f ||
|
||||
mm.hit_windows[1].samples.size() != 0 ||
|
||||
std::fabs(mm.attack_start_time - 0.455475f) > 1e-5f) {
|
||||
std::fprintf(stderr, "multi HitData windows not preserved\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- 可选:真实 PC .msa(设了 M2_ASSETS 才跑)----------------------------
|
||||
if (const char* root = std::getenv("M2_ASSETS")) {
|
||||
const std::string base = std::string(root) + "/PC/ymir work/pc/warrior/";
|
||||
fmt::Msa a1;
|
||||
if (fmt::parse_msa_file(base + "general/attack.msa", a1, &err)) {
|
||||
if (a1.hit_windows.size() != 1 || a1.hit_windows[0].bone_name != "equip_right_hand" ||
|
||||
std::fabs(a1.hit_windows[0].start_time - 0.456410f) > 1e-4f ||
|
||||
std::fabs(a1.hit_windows[0].end_time - 0.597436f) > 1e-4f ||
|
||||
a1.hit_windows[0].samples.size() != 17) {
|
||||
std::fprintf(stderr, "live general/attack.msa window/samples wrong "
|
||||
"(windows=%zu samples=%zu)\n", a1.hit_windows.size(),
|
||||
a1.hit_windows.empty() ? 0 : a1.hit_windows[0].samples.size());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
fmt::Msa c5;
|
||||
if (fmt::parse_msa_file(base + "twohand_sword/combo_05.msa", c5, &err)) {
|
||||
if (c5.hit_windows.size() != 1 || c5.motion_type != 2 ||
|
||||
std::fabs(c5.hit_windows[0].weapon_length - 150.0f) > 1e-3f ||
|
||||
std::fabs(c5.hit_windows[0].start_time - 0.455475f) > 1e-4f ||
|
||||
c5.hit_windows[0].samples.size() != 37) {
|
||||
std::fprintf(stderr, "live twohand_sword/combo_05.msa wrong "
|
||||
"(windows=%zu samples=%zu)\n", c5.hit_windows.size(),
|
||||
c5.hit_windows.empty() ? 0 : c5.hit_windows[0].samples.size());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
// 真正的双持双命中窗(右手窗 + 左手窗)。
|
||||
const std::string adh = std::string(root) + "/PC/ymir work/pc/assassin/dualhand_sword/combo_01.msa";
|
||||
fmt::Msa dh;
|
||||
if (fmt::parse_msa_file(adh, dh, &err)) {
|
||||
if (dh.hit_windows.size() != 2 ||
|
||||
dh.hit_windows[0].bone_name != "Bip01 R Hand" ||
|
||||
dh.hit_windows[1].bone_name != "Bip01 L Hand" ||
|
||||
dh.hit_windows[0].samples.size() != 13 ||
|
||||
dh.hit_windows[1].samples.size() != 13 ||
|
||||
std::fabs(dh.hit_windows[1].start_time - 0.605128f) > 1e-4f) {
|
||||
std::fprintf(stderr, "live assassin dualhand combo_01.msa two-window wrong "
|
||||
"(windows=%zu)\n", dh.hit_windows.size());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
fmt::Msa pb;
|
||||
if (fmt::parse_msa_file(base + "skill/palbang.msa", pb, &err)) {
|
||||
// AttackingData 后还有 Group MotionEventData —— List 修复后二者都要解析到。
|
||||
if (pb.hit_windows.size() != 1 || pb.hit_limit_count != 18 ||
|
||||
std::fabs(pb.hit_windows[0].weapon_length - 130.0f) > 1e-3f ||
|
||||
pb.events.size() != 3) {
|
||||
std::fprintf(stderr, "live skill/palbang.msa wrong "
|
||||
"(windows=%zu events=%zu)\n", pb.hit_windows.size(), pb.events.size());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user