#include #include #include #include int main() { const std::string text = R"( ScriptType MotionData MotionFileName "d:/ymir work/pc/test.gr2" MotionDuration 2.0 Group LoopData { MotionLoopCount 2 LoopCancelEnable 1 LoopStartTime 0.25 LoopEndTime 1.75 } )"; fmt::Msa msa; std::string err; if (!fmt::parse_msa(text, msa, &err)) { std::fprintf(stderr, "parse failed: %s\n", err.c_str()); return 1; } const bool ok = msa.has_loop_data && msa.motion_loop_count == 2 && msa.loop_cancel_enable && std::fabs(msa.loop_start_time - 0.25f) < 1e-6f && std::fabs(msa.loop_end_time - 1.75f) < 1e-6f; if (!ok) { 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; }