#pragma once #include #include #include #include #include #include #include #include #include #include namespace mtgodot { class Metin2Model; // Drives a Metin2Model each frame by sampling a (possibly separate) animation // .gr2 with libgr2, then skinning it with the resulting matrices: // // sample_pose(model.skeleton, anim, t) -> world[i], skin[i] // skin[i] = inverse_world[i] * world[i] (gr2 deformer matrix per bone) // // The skinning itself is done in Metin2Model (NOT via Skeleton3D — Godot's // built-in skinning orthonormalizes the bone matrix and drops the shear Granny // bakes onto some rig bones; see docs/MIDREVIEW.md §4): // - default : cpu_skin(skin[]) — CPU LBS, ArrayMesh rebuilt/frame // - MTGODOT_GPUSKIN=1 : gpu_skin(skin[]) — full 4x3 matrices in a float // texture, LBS in the SRC_SKIN vertex shader class Metin2AnimPlayer : public godot::Node3D { GDCLASS(Metin2AnimPlayer, godot::Node3D) public: Metin2AnimPlayer(); ~Metin2AnimPlayer() override; void _ready() override; void _process(double delta) override; void set_anim_path(const godot::String &p); godot::String get_anim_path() const { return anim_path; } void set_model_path(const godot::NodePath &p); godot::NodePath get_model_path() const { return model_path; } void set_playing(bool v) { playing = v; } bool get_playing() const { return playing; } void set_loop(bool v) { loop = v; } bool get_loop() const { return loop; } void set_time_scale(double s) { time_scale = s; } double get_time_scale() const { return time_scale; } // Crossfade duration (s) applied when anim_path changes while a clip is // already playing (PARITY §2.9). 0 = hard cut (old behaviour). void set_blend_time(double s) { blend_time = s < 0.0 ? 0.0 : s; } double get_blend_time() const { return blend_time; } void set_time(double t); double get_time() const { return time; } double get_duration() const { return duration; } void reload(); godot::String get_info() const { return last_info; } // .msa metadata (empty / zero when anim_path is a raw .gr2). godot::Vector3 get_accumulation() const { return accumulation; } godot::Array get_events() const; // [{type,start_time,effect,sound,pos}, ...] // Full affine pose in model GR2 coordinates, shared by CPU/GPU and blending. godot::Dictionary get_effect_bone_pose(const godot::String &bone); godot::Dictionary get_loop_data() const; godot::Dictionary get_motion_data() const; // {duration,next_combo,attack_start_time,...} static godot::Dictionary get_clip_cache_stats(); static void clear_clip_cache(); // CLIENT-GAP §3.5 — parse the PC combo tables out of playersettingmodule.py // (chrmgr.ReserveComboAttackNew / RegisterComboAttackNew → CRaceData). Returns // { class_idx:int -> { combo_key:int -> PackedInt32Array(段号 14..21) } } // with class_idx 0=warrior 1=assassin 2=sura 3=shaman and // combo_key = (motion_mode << 16) | combo_type. Empty Dictionary on failure. static godot::Dictionary parse_combo_tables(const godot::String &py_path); // (motion_mode << 16) | combo_type — mirrors RaceData.h MAKE_COMBO_KEY. static int make_combo_key(int motion_mode, int combo_type); // NaN-scan every animation in anim_path across [0,dur]; returns a report string. godot::String selfcheck(int samples = 24); protected: static void _bind_methods(); private: void emit_motion_event(const fmt::MotionEvent &ev); godot::String anim_path; godot::NodePath model_path; bool playing = true; bool loop = true; // whole-clip playback mode; independent from .msa LoopData double time_scale = 1.0; double time = 0.0; double duration = 0.0; godot::String last_info; std::shared_ptr anim_file; std::vector world_buf; std::vector skin_buf; // Crossfade state: the outgoing clip is kept and sampled at a frozen time, // its pose blended into the incoming clip for `blend_time` seconds. double blend_time = 0.15; std::shared_ptr prev_anim_file; double prev_anim_time = 0.0; double prev_anim_duration = 0.0; bool prev_anim_loop = true; double blend_elapsed = 0.0; std::vector prev_world_buf, prev_skin_buf, blend_world_buf; godot::Vector3 accumulation; // .msa Accumulation (root motion) std::vector events; fmt::Msa msa_metadata; int next_event = 0; // index into `events`, for _process dispatch double prev_time = 0.0; Metin2Model *resolve_model() const; void apply_pose(double t); void dispatch_events(double from, double to); // .msa path -> real motion .gr2 path; passes plain .gr2 paths through. static godot::String resolve_anim_gr2(const godot::String &spec, const fmt::Msa &metadata); }; } // namespace mtgodot