Implement playable Mac client and rendering validation
This commit is contained in:
+144
-28
@@ -10,6 +10,7 @@
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <list>
|
||||
|
||||
#include <godot_cpp/classes/mesh_instance3d.hpp>
|
||||
#include <godot_cpp/classes/os.hpp>
|
||||
@@ -25,10 +26,102 @@ using namespace godot;
|
||||
|
||||
namespace mtgodot {
|
||||
|
||||
namespace {
|
||||
struct ClipEntry {
|
||||
String path;
|
||||
uint64_t modified;
|
||||
size_t bytes;
|
||||
std::shared_ptr<const gr2::File> file;
|
||||
};
|
||||
// Immutable decoded clips are shared, not playback time/pose/event cursors.
|
||||
// Main-thread animation loading only; no Godot objects survive in this cache.
|
||||
std::list<ClipEntry> clip_cache;
|
||||
size_t clip_bytes = 0;
|
||||
uint64_t clip_hits = 0, clip_misses = 0;
|
||||
constexpr size_t CLIP_LIMIT = 32, CLIP_BYTE_LIMIT = 64 * 1024 * 1024;
|
||||
struct MsaEntry {
|
||||
String path;
|
||||
uint64_t modified;
|
||||
fmt::Msa metadata;
|
||||
};
|
||||
std::list<MsaEntry> msa_cache;
|
||||
uint64_t msa_hits = 0, msa_misses = 0;
|
||||
// Keep events immutable in the cache; reload copies them into each actor.
|
||||
bool cached_msa(const String &path, fmt::Msa &metadata, std::string *error) {
|
||||
const uint64_t modified = FileAccess::get_modified_time(path);
|
||||
for (auto it = msa_cache.begin(); it != msa_cache.end(); ++it) {
|
||||
if (it->path != path) continue;
|
||||
if (it->modified == modified) {
|
||||
++msa_hits;
|
||||
metadata = it->metadata;
|
||||
msa_cache.splice(msa_cache.begin(), msa_cache, it);
|
||||
return true;
|
||||
}
|
||||
msa_cache.erase(it);
|
||||
break;
|
||||
}
|
||||
++msa_misses;
|
||||
if (!fmt::parse_msa_file(std::string(path.utf8().get_data()), metadata, error)) return false;
|
||||
if (msa_cache.size() >= 128) msa_cache.pop_back();
|
||||
msa_cache.push_front({path, modified, metadata});
|
||||
return true;
|
||||
}
|
||||
std::shared_ptr<const gr2::File> cached_clip(const String &path, gr2::LoadError *err) {
|
||||
uint64_t modified = FileAccess::get_modified_time(path);
|
||||
for (auto it = clip_cache.begin(); it != clip_cache.end(); ++it) {
|
||||
if (it->path != path) continue;
|
||||
if (it->modified == modified) {
|
||||
++clip_hits;
|
||||
clip_cache.splice(clip_cache.begin(), clip_cache, it);
|
||||
return clip_cache.front().file;
|
||||
}
|
||||
clip_bytes -= it->bytes;
|
||||
clip_cache.erase(it);
|
||||
break;
|
||||
}
|
||||
++clip_misses;
|
||||
auto loaded = gr2_from_file(path, err);
|
||||
if (!loaded) return {};
|
||||
size_t bytes = 0;
|
||||
for (size_t i = 0; i < loaded->sections().size(); ++i) bytes += loaded->section_bytes(i).size();
|
||||
auto file = std::make_shared<const gr2::File>(std::move(*loaded));
|
||||
if (bytes <= CLIP_BYTE_LIMIT) {
|
||||
while (!clip_cache.empty() && (clip_cache.size() >= CLIP_LIMIT || clip_bytes + bytes > CLIP_BYTE_LIMIT)) {
|
||||
clip_bytes -= clip_cache.back().bytes;
|
||||
clip_cache.pop_back();
|
||||
}
|
||||
clip_cache.push_front({path, modified, bytes, file});
|
||||
clip_bytes += bytes;
|
||||
}
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
Dictionary Metin2AnimPlayer::get_clip_cache_stats() {
|
||||
Dictionary d;
|
||||
d["hits"] = clip_hits;
|
||||
d["misses"] = clip_misses;
|
||||
d["entries"] = static_cast<int64_t>(clip_cache.size());
|
||||
d["section_bytes"] = static_cast<int64_t>(clip_bytes);
|
||||
d["msa_hits"] = msa_hits;
|
||||
d["msa_misses"] = msa_misses;
|
||||
d["msa_entries"] = static_cast<int64_t>(msa_cache.size());
|
||||
return d;
|
||||
}
|
||||
void Metin2AnimPlayer::clear_clip_cache() {
|
||||
clip_cache.clear();
|
||||
clip_bytes = 0;
|
||||
clip_hits = clip_misses = 0;
|
||||
msa_cache.clear();
|
||||
msa_hits = msa_misses = 0;
|
||||
}
|
||||
|
||||
Metin2AnimPlayer::Metin2AnimPlayer() {}
|
||||
Metin2AnimPlayer::~Metin2AnimPlayer() = default;
|
||||
|
||||
void Metin2AnimPlayer::_bind_methods() {
|
||||
ClassDB::bind_static_method("Metin2AnimPlayer", D_METHOD("get_clip_cache_stats"), &Metin2AnimPlayer::get_clip_cache_stats);
|
||||
ClassDB::bind_static_method("Metin2AnimPlayer", D_METHOD("clear_clip_cache"), &Metin2AnimPlayer::clear_clip_cache);
|
||||
ClassDB::bind_method(D_METHOD("set_anim_path", "path"), &Metin2AnimPlayer::set_anim_path);
|
||||
ClassDB::bind_method(D_METHOD("get_anim_path"), &Metin2AnimPlayer::get_anim_path);
|
||||
ClassDB::bind_method(D_METHOD("set_model_path", "path"), &Metin2AnimPlayer::set_model_path);
|
||||
@@ -49,6 +142,7 @@ void Metin2AnimPlayer::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("selfcheck", "samples"), &Metin2AnimPlayer::selfcheck, DEFVAL(24));
|
||||
ClassDB::bind_method(D_METHOD("get_accumulation"), &Metin2AnimPlayer::get_accumulation);
|
||||
ClassDB::bind_method(D_METHOD("get_events"), &Metin2AnimPlayer::get_events);
|
||||
ClassDB::bind_method(D_METHOD("get_effect_bone_pose", "bone"), &Metin2AnimPlayer::get_effect_bone_pose);
|
||||
ClassDB::bind_method(D_METHOD("get_loop_data"), &Metin2AnimPlayer::get_loop_data);
|
||||
ClassDB::bind_method(D_METHOD("get_motion_data"), &Metin2AnimPlayer::get_motion_data);
|
||||
ClassDB::bind_static_method("Metin2AnimPlayer",
|
||||
@@ -63,6 +157,7 @@ void Metin2AnimPlayer::_bind_methods() {
|
||||
PropertyInfo(Variant::STRING, "sound"),
|
||||
PropertyInfo(Variant::VECTOR3, "pos")));
|
||||
ADD_SIGNAL(MethodInfo("playback_finished"));
|
||||
ADD_SIGNAL(MethodInfo("motion_event_detailed", PropertyInfo(Variant::DICTIONARY, "event")));
|
||||
|
||||
ADD_PROPERTY(PropertyInfo(Variant::STRING, "anim_path", PROPERTY_HINT_GLOBAL_FILE, "*.gr2,*.msa"),
|
||||
"set_anim_path", "get_anim_path");
|
||||
@@ -132,17 +227,11 @@ static String strip_dpath(const String &raw) {
|
||||
return k >= 0 ? p.substr(k) : p; // "ymir work/pc/.../x.gr2"
|
||||
}
|
||||
|
||||
String Metin2AnimPlayer::resolve_anim_gr2(const String &spec) {
|
||||
String Metin2AnimPlayer::resolve_anim_gr2(const String &spec, const fmt::Msa &m) {
|
||||
String low = spec.to_lower();
|
||||
if (!low.ends_with(".msa")) {
|
||||
return spec; // already a .gr2 path
|
||||
}
|
||||
fmt::Msa m;
|
||||
std::string e;
|
||||
if (!fmt::parse_msa_file(std::string(spec.utf8().get_data()), m, &e)) {
|
||||
UtilityFunctions::push_error(String("[Metin2AnimPlayer] .msa parse: ") + String(e.c_str()));
|
||||
return spec;
|
||||
}
|
||||
const String motion = String(m.motion_gr2.c_str());
|
||||
const String base = motion.replace("\\", "/").get_file();
|
||||
const String msa_dir = spec.get_base_dir();
|
||||
@@ -171,16 +260,45 @@ String Metin2AnimPlayer::resolve_anim_gr2(const String &spec) {
|
||||
return cand;
|
||||
}
|
||||
|
||||
static Dictionary motion_event_dict(const fmt::MotionEvent &ev) {
|
||||
Dictionary d;
|
||||
d["type"] = ev.type;
|
||||
d["start_time"] = ev.start_time;
|
||||
d["effect"] = String(ev.effect_file.c_str());
|
||||
d["sound"] = String(ev.sound_file.c_str());
|
||||
d["pos"] = Vector3(ev.position[0], ev.position[1], ev.position[2]);
|
||||
d["bone"] = String(ev.attaching_bone.c_str());
|
||||
d["attaching"] = ev.attaching;
|
||||
d["following"] = ev.following;
|
||||
d["independent"] = ev.independent;
|
||||
d["fishing_effect"] = ev.fishing_effect;
|
||||
return d;
|
||||
}
|
||||
|
||||
Dictionary Metin2AnimPlayer::get_effect_bone_pose(const String &bone) {
|
||||
Dictionary result;
|
||||
Metin2Model *model = resolve_model();
|
||||
const gr2::Skeleton *sk = model ? model->gr2_skeleton() : nullptr;
|
||||
if (!sk || !anim_file) return result;
|
||||
for (size_t i = 0; i < sk->bones.size() && i < world_buf.size(); ++i) {
|
||||
if (String(sk->bones[i].name.c_str()) == bone) {
|
||||
result["transform"] = gr2_to_godot(world_buf[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void Metin2AnimPlayer::emit_motion_event(const fmt::MotionEvent &ev) {
|
||||
emit_signal("motion_event", ev.type, String(ev.effect_file.c_str()),
|
||||
String(ev.sound_file.c_str()), Vector3(ev.position[0], ev.position[1], ev.position[2]));
|
||||
emit_signal("motion_event_detailed", motion_event_dict(ev));
|
||||
}
|
||||
|
||||
Array Metin2AnimPlayer::get_events() const {
|
||||
Array a;
|
||||
for (const fmt::MotionEvent &ev : events) {
|
||||
Dictionary d;
|
||||
d["type"] = ev.type;
|
||||
d["start_time"] = ev.start_time;
|
||||
d["effect"] = String(ev.effect_file.c_str());
|
||||
d["sound"] = String(ev.sound_file.c_str());
|
||||
d["pos"] = Vector3(ev.position[0], ev.position[1], ev.position[2]);
|
||||
a.push_back(d);
|
||||
a.push_back(motion_event_dict(ev));
|
||||
}
|
||||
return a;
|
||||
}
|
||||
@@ -280,7 +398,7 @@ void Metin2AnimPlayer::_ready() {
|
||||
}
|
||||
|
||||
void Metin2AnimPlayer::reload() {
|
||||
anim_file = std::nullopt;
|
||||
anim_file.reset();
|
||||
duration = 0.0;
|
||||
time = 0.0;
|
||||
prev_time = 0.0;
|
||||
@@ -294,16 +412,18 @@ void Metin2AnimPlayer::reload() {
|
||||
}
|
||||
|
||||
// .msa -> resolve the real motion .gr2 + pull accumulation / events.
|
||||
const bool diagnostics = OS::get_singleton()->get_environment("MTGODOT_ANIM_DIAGNOSTICS") == "1";
|
||||
String gr2_spec = anim_path;
|
||||
if (anim_path.to_lower().ends_with(".msa")) {
|
||||
fmt::Msa m;
|
||||
std::string e;
|
||||
if (fmt::parse_msa_file(std::string(anim_path.utf8().get_data()), m, &e)) {
|
||||
if (cached_msa(anim_path, m, &e)) {
|
||||
msa_metadata = m;
|
||||
accumulation = Vector3(m.accumulation[0], m.accumulation[1], m.accumulation[2]);
|
||||
events = m.events;
|
||||
gr2_spec = resolve_anim_gr2(anim_path);
|
||||
UtilityFunctions::print(vformat("[Metin2AnimPlayer] .msa -> %s accum=(%.2f %.2f %.2f) events=%d loopdata=%s",
|
||||
// Reuse the metadata just parsed; path resolution must not parse it again.
|
||||
gr2_spec = resolve_anim_gr2(anim_path, m);
|
||||
if (diagnostics) UtilityFunctions::print(vformat("[Metin2AnimPlayer] .msa -> %s accum=(%.2f %.2f %.2f) events=%d loopdata=%s",
|
||||
gr2_spec, accumulation.x, accumulation.y, accumulation.z, (int)events.size(),
|
||||
m.has_loop_data ? vformat("%d x [%.3f,%.3f]", m.motion_loop_count,
|
||||
m.loop_start_time, m.loop_end_time) : String("none")));
|
||||
@@ -314,7 +434,7 @@ void Metin2AnimPlayer::reload() {
|
||||
}
|
||||
|
||||
gr2::LoadError err;
|
||||
auto f = mtgodot::gr2_from_file(gr2_spec, &err);
|
||||
auto f = cached_clip(gr2_spec, &err);
|
||||
if (!f) {
|
||||
UtilityFunctions::push_error(String("[Metin2AnimPlayer] anim load failed [") +
|
||||
String(err.stage.c_str()) + "]: " + String(err.message.c_str()));
|
||||
@@ -330,7 +450,7 @@ void Metin2AnimPlayer::reload() {
|
||||
duration = an.duration;
|
||||
|
||||
// Diagnostic: how many anim tracks map to a model-skeleton bone by name?
|
||||
if (Metin2Model *model = resolve_model()) {
|
||||
if (Metin2Model *model = diagnostics ? resolve_model() : nullptr) {
|
||||
if (const gr2::Skeleton *sk = model->gr2_skeleton()) {
|
||||
int matched = 0;
|
||||
godot::String unmatched;
|
||||
@@ -356,7 +476,7 @@ void Metin2AnimPlayer::reload() {
|
||||
last_info = vformat("anim '%s' dur=%.3f tracks=%d anims=%d",
|
||||
String(an.name.c_str()), duration, (int)an.tracks.size(),
|
||||
(int)anim_file->file_info().animations.size());
|
||||
UtilityFunctions::print("[Metin2AnimPlayer] ", last_info);
|
||||
if (diagnostics) UtilityFunctions::print("[Metin2AnimPlayer] ", last_info);
|
||||
apply_pose(0.0);
|
||||
}
|
||||
|
||||
@@ -480,7 +600,7 @@ void Metin2AnimPlayer::_process(double delta) {
|
||||
if (prev_anim_file) {
|
||||
blend_elapsed += delta; // real seconds, independent of time_scale
|
||||
if (blend_elapsed >= blend_time) {
|
||||
prev_anim_file = std::nullopt;
|
||||
prev_anim_file.reset();
|
||||
}
|
||||
}
|
||||
double next = time + delta * time_scale;
|
||||
@@ -508,9 +628,7 @@ void Metin2AnimPlayer::dispatch_events(double from, double to) {
|
||||
double b = std::fmax(0.0, std::fmin(to, duration));
|
||||
for (const fmt::MotionEvent &ev : events) {
|
||||
if (a < ev.start_time && ev.start_time <= b) {
|
||||
emit_signal("motion_event", ev.type, String(ev.effect_file.c_str()),
|
||||
String(ev.sound_file.c_str()),
|
||||
Vector3(ev.position[0], ev.position[1], ev.position[2]));
|
||||
emit_motion_event(ev);
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -522,9 +640,7 @@ void Metin2AnimPlayer::dispatch_events(double from, double to) {
|
||||
double t = ev.start_time;
|
||||
bool hit = (a < t && t <= b) || (b > duration && t <= b - duration);
|
||||
if (hit) {
|
||||
emit_signal("motion_event", ev.type, String(ev.effect_file.c_str()),
|
||||
String(ev.sound_file.c_str()),
|
||||
Vector3(ev.position[0], ev.position[1], ev.position[2]));
|
||||
emit_motion_event(ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user