Files
mtgodot-poc/formats/tests/msa_test.cpp
T
shenandshen c93894313a fix: 装备属性面板避让逻辑 + 多项功能更新
- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧
- inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域
- 包含其他累积的功能开发和测试文件
2026-09-21 16:38:59 -07:00

348 lines
10 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;
}
}
{
// 40250 RaceMotionDataEvent has dedicated payloads for FLY,
// SCREEN_WAVING and SPECIAL_ATTACKING. Keep the no-payload event
// types in the same snapshot so they cannot be silently dropped.
const std::string events = R"(MotionFileName "events.gr2"
Group MotionEventData
{
Group Event00
{
MotionEventType 2
StartingTime 0.10
DuringTime 0.20
Power 300
AffectingRange 200
}
Group Event01
{
MotionEventType 4
StartingTime 0.30
DuringTime 0.40
EnableHitProcess 0
AttackType 1
HittingType 2
StiffenTime 0.05
InvisibleTime 0.10
ExternalForce 20
HitLimitCount 3
CollisionType 4
SphereDataCount 2
Group SphereData00
{
Radius 100
Position 1 2 3
}
Group SphereData01
{
Radius 200
Position 4 5 6
}
}
Group Event02
{
MotionEventType 6
StartingTime 0.50
AttachingEnable 1
AttachingBoneName "Bip01 R Hand"
FlyFileName "d:/ymir work/pc/warrior/effect/geompung.msf"
FlyPosition 7 8 9
}
Group Event03
{
MotionEventType 7
StartingTime 0.60
}
Group Event04
{
MotionEventType 8
StartingTime 0.70
}
Group Event05
{
MotionEventType 9
StartingTime 0.80
}
}
)";
fmt::Msa parsed;
std::string error;
if (!fmt::parse_msa(events, parsed, &error) || parsed.events.size() != 6) {
std::fprintf(stderr, "motion event payload parse failed: %s\n", error.c_str());
return 1;
}
const fmt::MotionEvent *wave = &parsed.events[0];
const fmt::MotionEvent *attack = &parsed.events[1];
const fmt::MotionEvent *fly = &parsed.events[2];
if (wave->type != 2 || std::fabs(wave->duration_time - 0.2f) > 1e-6f ||
wave->power != 300 || wave->affecting_range != 200 ||
attack->type != 4 || attack->enable_hit_process || attack->attack_type != 1 ||
attack->hitting_type != 2 || attack->collision_type != 4 ||
attack->collision_spheres.size() != 2 ||
std::fabs(attack->collision_spheres[1].radius - 200.0f) > 1e-6f ||
attack->collision_spheres[1].position[2] != 6.0f ||
fly->type != 6 || fly->fly_file.find("geompung.msf") == std::string::npos ||
!fly->fly_attaching || fly->fly_attaching_bone != "Bip01 R Hand" ||
fly->fly_position[0] != 7.0f || parsed.events[3].type != 7 ||
parsed.events[4].type != 8 || parsed.events[5].type != 9) {
std::fprintf(stderr, "40250 motion event payload fields were 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;
}