258 lines
8.2 KiB
C++
258 lines
8.2 KiB
C++
#include <msa.h>
|
|
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <string>
|
|
|
|
int main() {
|
|
{
|
|
fmt::Msa effects;
|
|
std::string error;
|
|
if (!fmt::parse_msa(R"(MotionFileName "test.gr2"
|
|
Group MotionEventData
|
|
{
|
|
Group Event00
|
|
{
|
|
MotionEventType 1
|
|
StartingTime 0.25
|
|
EffectFileName "d:/ymir work/pc/test.mse"
|
|
AttachingBoneName "equip_right_hand"
|
|
AttachingEnable 1
|
|
FollowingEnable 1
|
|
IndependentFlag 1
|
|
EffectPosition 10 20 30
|
|
}
|
|
}
|
|
)", effects, &error) || effects.events.size() != 1 ||
|
|
!effects.events[0].attaching || !effects.events[0].following ||
|
|
!effects.events[0].independent || effects.events[0].attaching_bone != "equip_right_hand" ||
|
|
effects.events[0].position[2] != 30) {
|
|
std::fprintf(stderr, "effect attachment metadata lost\n");
|
|
return 1;
|
|
}
|
|
}
|
|
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;
|
|
}
|