// M2 T1 —— .msa 解析(照 EterGrnLib/Util.cpp CGrannyMotion::LoadMotionData 的字段)。 #include "msa.h" #include "textscript.h" #include namespace fmt { static void vec3(const std::vector* l, float* out) { if (!l) return; for (int k = 0; k < 3 && k + 1 < (int)l->size(); ++k) out[k] = float(std::atof((*l)[k + 1].c_str())); } static void loop_data(const Node& root, Msa& out) { if (const Node* loop = root.group("LoopData")) { out.has_loop_data = true; out.motion_loop_count = loop->inum("MotionLoopCount", -1); out.loop_cancel_enable = loop->inum("LoopCancelEnable", 0) != 0; out.loop_start_time = loop->num("LoopStartTime"); out.loop_end_time = loop->num("LoopEndTime"); } } // `List HitPosition { t lastXYZ XYZ t lastXYZ XYZ ... }` —— 7 个一组切成采样点。 static void hit_positions(const Node& win, std::vector& out) { const Node* lst = win.group("HitPosition"); if (!lst || lst->lines.empty()) return; const std::vector& t = lst->lines[0]; for (size_t i = 0; i + 7 <= t.size(); i += 7) { Msa::HitSample s; s.time = float(std::atof(t[i].c_str())); for (int k = 0; k < 3; ++k) s.last_pos[k] = float(std::atof(t[i + 1 + k].c_str())); for (int k = 0; k < 3; ++k) s.pos[k] = float(std::atof(t[i + 4 + k].c_str())); out.push_back(s); } } // 一个命中窗(NRaceData::THitData)。老式写法里 win == AttackingData 组本身。 static Msa::HitWindow hit_window(const Node& win) { Msa::HitWindow w; w.start_time = win.num("AttackingStartTime"); w.end_time = win.num("AttackingEndTime"); w.bone_name = win.str("AttackingBone"); w.weapon_length = win.num("WeaponLength"); hit_positions(win, w.samples); return w; } // Group ComboInputData / Group AttackingData —— 照 EterGrnLib CGrannyMotion::LoadMotionData // 与 GameLib NRaceData::LoadMotionAttackData 的字段名读取;缺组时保持默认值。 static void combat_data(const Node& root, Msa& out) { if (const Node* c = root.group("ComboInputData")) { out.has_combo_input = true; out.combo_pre_input_time = c->num("PreInputTime"); out.combo_direct_input_time = c->num("DirectInputTime"); out.combo_input_limit_time = c->num("InputLimitTime"); out.combo_link_time = c->num("LinkTime"); } if (const Node* a = root.group("AttackingData")) { out.has_attacking_data = true; out.attacking_type = a->inum("AttackType", a->inum("AttackingType", 0)); out.motion_type = a->inum("MotionType", out.attacking_type); out.hitting_type = a->inum("HittingType", 0); out.stiffen_time = a->num("StiffenTime"); out.invisible_time = a->num("InvisibleTime"); out.external_force = a->num("ExternalForce"); out.hit_limit_count = a->inum("HitLimitCount", 0); // 新式:HitDataCount N + Group HitData00..0N;老式:无 HitDataCount,组本身即窗。 bool has_children = false; for (const Node& g : a->groups) { const std::string& n = g.name; if (n.size() >= 7 && (n.compare(0, 7, "HitData") == 0 || n.compare(0, 7, "hitdata") == 0)) { out.hit_windows.push_back(hit_window(g)); has_children = true; } } if (!has_children) out.hit_windows.push_back(hit_window(*a)); if (!out.hit_windows.empty()) { out.attack_start_time = out.hit_windows.front().start_time; out.attack_end_time = out.hit_windows.front().end_time; } } } bool parse_msa(const std::string& text, Msa& out, std::string* err) { Node root; if (!parse_textscript(text, root, err)) return false; out = Msa{}; out.motion_gr2 = root.str("MotionFileName"); out.duration = root.num("MotionDuration"); vec3(root.find("Accumulation"), out.accumulation); loop_data(root, out); combat_data(root, out); if (const Node* med = root.group("MotionEventData")) { int count = med->inum("MotionEventDataCount", int(med->groups.size())); (void)count; for (const Node& ev : med->groups) { MotionEvent e; e.type = ev.inum("MotionEventType"); e.start_time = ev.num("StartingTime"); e.effect_file = ev.str("EffectFileName"); e.sound_file = ev.str("SoundFileName"); vec3(ev.find("EffectPosition"), e.position); out.events.push_back(std::move(e)); } } if (out.motion_gr2.empty()) { if (err) *err = "msa: 缺 MotionFileName"; return false; } return true; } bool parse_msa_file(const std::string& path, Msa& out, std::string* err) { Node root; if (!parse_textscript_file(path, root, err)) return false; // 复用上面逻辑:重解析文本更简单,这里直接从 root 走一遍 out = Msa{}; out.motion_gr2 = root.str("MotionFileName"); out.duration = root.num("MotionDuration"); vec3(root.find("Accumulation"), out.accumulation); loop_data(root, out); combat_data(root, out); if (const Node* med = root.group("MotionEventData")) for (const Node& ev : med->groups) { MotionEvent e; e.type = ev.inum("MotionEventType"); e.start_time = ev.num("StartingTime"); e.effect_file = ev.str("EffectFileName"); e.sound_file = ev.str("SoundFileName"); vec3(ev.find("EffectPosition"), e.position); out.events.push_back(std::move(e)); } if (out.motion_gr2.empty()) { if (err) *err = "msa: 缺 MotionFileName (" + path + ")"; return false; } return true; } } // namespace fmt