#include "metin2_model.h" #include "dxt.h" #include "gr2_bridge.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace godot; namespace mtgodot { Metin2Model::Metin2Model() { file = std::make_shared>(); } Metin2Model::~Metin2Model() = default; void Metin2Model::_bind_methods() { ClassDB::bind_method(D_METHOD("set_gr2_path", "path"), &Metin2Model::set_gr2_path); ClassDB::bind_method(D_METHOD("get_gr2_path"), &Metin2Model::get_gr2_path); ClassDB::bind_method(D_METHOD("set_texture_dir", "path"), &Metin2Model::set_texture_dir); ClassDB::bind_method(D_METHOD("get_texture_dir"), &Metin2Model::get_texture_dir); ClassDB::bind_method(D_METHOD("set_unit_scale", "s"), &Metin2Model::set_unit_scale); ClassDB::bind_method(D_METHOD("get_unit_scale"), &Metin2Model::get_unit_scale); ClassDB::bind_method(D_METHOD("set_flip_z", "v"), &Metin2Model::set_flip_z); ClassDB::bind_method(D_METHOD("get_flip_z"), &Metin2Model::get_flip_z); ClassDB::bind_method(D_METHOD("set_flip_winding", "v"), &Metin2Model::set_flip_winding); ClassDB::bind_method(D_METHOD("get_flip_winding"), &Metin2Model::get_flip_winding); ClassDB::bind_method(D_METHOD("reload"), &Metin2Model::reload); ClassDB::bind_method(D_METHOD("get_info"), &Metin2Model::get_info); ClassDB::bind_method(D_METHOD("probe_gr2", "path"), &Metin2Model::probe_gr2); ADD_PROPERTY(PropertyInfo(Variant::STRING, "gr2_path", PROPERTY_HINT_GLOBAL_FILE, "*.gr2"), "set_gr2_path", "get_gr2_path"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "texture_dir", PROPERTY_HINT_GLOBAL_DIR), "set_texture_dir", "get_texture_dir"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "unit_scale", PROPERTY_HINT_RANGE, "0.0001,1,0.0001"), "set_unit_scale", "get_unit_scale"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "flip_z"), "set_flip_z", "get_flip_z"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "flip_winding"), "set_flip_winding", "get_flip_winding"); } void Metin2Model::set_gr2_path(const String &p) { gr2_path = p; if (is_inside_tree()) { reload(); } } void Metin2Model::set_texture_dir(const String &p) { texture_dir = p; if (is_inside_tree()) { reload(); } } void Metin2Model::set_unit_scale(double s) { unit_scale = s; if (is_inside_tree()) { reload(); } } void Metin2Model::set_flip_z(bool v) { flip_z = v; if (is_inside_tree()) { reload(); } } void Metin2Model::set_flip_winding(bool v) { flip_winding = v; if (is_inside_tree()) { reload(); } } void Metin2Model::_ready() { if (!gr2_path.is_empty()) { reload(); } } void Metin2Model::_clear_children() { skel = nullptr; mi = nullptr; TypedArray kids = get_children(); for (int i = 0; i < kids.size(); ++i) { Node *n = Object::cast_to(kids[i]); if (n) { remove_child(n); n->queue_free(); } } } void Metin2Model::reload() { _clear_children(); *file = std::nullopt; last_info = ""; if (gr2_path.is_empty()) { return; } gr2::LoadError err; const std::string p(gr2_path.utf8().get_data()); auto loaded = gr2::File::load_path(p, &err); if (!loaded) { UtilityFunctions::push_error(String("[Metin2Model] gr2 load failed [") + String(err.stage.c_str()) + "]: " + String(err.message.c_str()) + " (" + gr2_path + ")"); return; } *file = std::move(loaded); const gr2::FileInfo &fi = (*file)->file_info(); set_transform(make_conv((float)unit_scale, flip_z)); // Skeleton (optional). const gr2::Skeleton *sk = fi.skeletons.empty() ? nullptr : &fi.skeletons[0]; if (sk) { skel = build_skeleton(*sk); if (skel) { add_child(skel); skel->set_owner(get_owner() ? get_owner() : this); } } // Mesh. AABB bounds; Ref mesh = build_mesh(fi, flip_winding, bounds); if (mesh->get_surface_count() > 0) { mi = memnew(MeshInstance3D); mi->set_name("MeshInstance3D"); mi->set_mesh(mesh); add_child(mi); mi->set_owner(get_owner() ? get_owner() : this); if (skel && sk) { mi->set_skeleton_path(mi->get_path_to(skel)); mi->set_skin(build_skin(*sk)); } mi->set_custom_aabb(bounds.grow(0.01)); } _apply_materials(); int vtot = 0; for (const auto &m : fi.meshes) { vtot += (int)m.vertices.size(); } last_info = vformat("gr2 v%d | bones=%d meshes=%d surfaces=%d verts=%d | bounds=%s", (int)(*file)->format_version(), sk ? (int)sk->bones.size() : 0, (int)fi.meshes.size(), mesh->get_surface_count(), vtot, String(bounds)); UtilityFunctions::print("[Metin2Model] ", last_info); } String Metin2Model::_guess_texture_dir() const { if (!texture_dir.is_empty()) { return texture_dir; } return gr2_path.get_base_dir(); } void Metin2Model::_apply_materials() { if (!mi || !file || !*file) { return; } const gr2::FileInfo &fi = (*file)->file_info(); const String dir = _guess_texture_dir(); const String stem = gr2_path.get_file().get_basename(); // Cache: texture path -> ImageTexture. auto load_tex = [&](const String &path) -> Ref { Ref t; if (!FileAccess::file_exists(path)) { return t; } mtgodot::Image dds = mtgodot::load_dds_path(path.utf8().get_data()); if (!dds.ok()) { return t; } PackedByteArray bytes; bytes.resize((int)dds.rgba.size()); memcpy(bytes.ptrw(), dds.rgba.data(), dds.rgba.size()); Ref img = godot::Image::create_from_data( dds.w, dds.h, false, godot::Image::FORMAT_RGBA8, bytes); if (img.is_null()) { return t; } img->generate_mipmaps(); t = ImageTexture::create_from_image(img); return t; }; Ref tex_main = load_tex(dir.path_join(stem + String(".dds"))); Ref tex_face = load_tex(dir.path_join(stem + String("_face.dds"))); if (tex_face.is_null()) { tex_face = load_tex(dir.path_join(String("warrior_face.dds"))); } Ref mesh = mi->get_mesh(); for (int s = 0; s < mesh->get_surface_count(); ++s) { String sname = mesh->surface_get_name(s).to_lower(); Ref tex = (sname.find("face") != -1 && tex_face.is_valid()) ? tex_face : tex_main; Ref mat; mat.instantiate(); mat->set_cull_mode(BaseMaterial3D::CULL_DISABLED); // M1: winding unverified if (tex.is_valid()) { mat->set_texture(BaseMaterial3D::TEXTURE_ALBEDO, tex); } else { mat->set_albedo(Color(0.8, 0.8, 0.82)); } mi->set_surface_override_material(s, mat); } } const gr2::Skeleton *Metin2Model::gr2_skeleton() const { if (!file || !*file) { return nullptr; } const gr2::FileInfo &fi = (*file)->file_info(); return fi.skeletons.empty() ? nullptr : &fi.skeletons[0]; } String Metin2Model::get_info() const { return last_info; } String Metin2Model::probe_gr2(const String &path) const { gr2::LoadError err; const std::string p(path.utf8().get_data()); auto f = gr2::File::load_path(p, &err); if (!f) { return vformat("gr2 load FAILED [%s]: %s", String(err.stage.c_str()), String(err.message.c_str())); } return vformat("gr2 OK: format_version=%d sections=%d", (int)f->format_version(), (int)f->sections().size()); } } // namespace mtgodot