Files
mtgodot-poc/formats/tests/msa_test.cpp
T

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;
}