Metin2 game client (P0–P11) + mobile asset pipeline
Networked client on the existing Godot 4.7 + libgr2 renderer:
- net: m2dev wire protocol (libsodium KX + XChaCha20), auth/select/game
phases, EntityStore world model, ~all GC/CG headers. char create/delete,
private shop / mall / cube, SHOP_GC_START_EX, guild, party (+ CG_PARTY_SET_STATE),
quests, dragon soul, refine, safebox, exchange.
- UI: in-game windows migrated 1:1 from the reference uiscript/root .py —
char status (/stat), inventory+equipment, select-item ([SELECT_ITEM] quest
token), system-option + game-option + ESC system menu, private-shop 39-grid,
party info board, shop tabs, atlas, minimap, quickbar, chat, …
- EterGrnLib polish: GR2 material blend/two-sided, LOD crossfade, motion-event
dispatch, contact shadow, ray-AABB picking, weapon grip pre-transform.
Portable asset IO (A1) — all extension/libgr2/formats/mtproto reads routed
through godot::FileAccess (res:// PCK works on iOS/Android); standalone-lib
*_path() kept for the non-Godot CTests. AssetResolver + PropertyRegistry
switched to a baked index (bake_asset_index.gd) instead of std::filesystem.
Mobile builds: build-{android,ios}.sh, export-android.sh, pack-assets.sh,
gen-debug-keystore.sh. Assets ship as a zip mounted at runtime by
project/asset_pack.gd (adb push now; HTTP download is a drop-in later).
ctest 10/10, 34 GDScript suites, macOS/iOS/Android all build.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EJxkHiNKS4kybHS3XKyAJ
This commit is contained in:
@@ -1,15 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <godot_cpp/classes/array_mesh.hpp>
|
||||
#include <godot_cpp/classes/image.hpp>
|
||||
#include <godot_cpp/classes/image_texture.hpp>
|
||||
#include <godot_cpp/classes/node3d.hpp>
|
||||
#include <godot_cpp/classes/ref.hpp>
|
||||
#include <godot_cpp/templates/hash_map.hpp>
|
||||
#include <godot_cpp/variant/array.hpp>
|
||||
#include <godot_cpp/variant/packed_string_array.hpp>
|
||||
#include <godot_cpp/variant/string.hpp>
|
||||
|
||||
#include <gr2/gr2.h>
|
||||
|
||||
#include "gr2_bridge.h"
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
@@ -35,8 +39,17 @@ public:
|
||||
~Metin2Model() override;
|
||||
|
||||
void _ready() override;
|
||||
void _process(double delta) override;
|
||||
|
||||
// --- inspector properties ---
|
||||
// §2.10 LOD: loads <base>_lod_01/02/03.gr2 (same 75-bone skeleton, decimated
|
||||
// meshes) and swaps the rendered mesh by camera distance. `lod_distances` =
|
||||
// [d1,d2,d3]; >d3 -> LOD3. Skinning is unaffected (shared skeleton).
|
||||
void set_lod_enabled(bool v);
|
||||
bool get_lod_enabled() const { return lod_enabled; }
|
||||
void set_lod_distances(const godot::PackedFloat32Array &d);
|
||||
godot::PackedFloat32Array get_lod_distances() const { return lod_dist; }
|
||||
int get_lod_level() const { return lod_level; }
|
||||
void set_gr2_path(const godot::String &p);
|
||||
godot::String get_gr2_path() const { return gr2_path; }
|
||||
|
||||
@@ -58,9 +71,56 @@ public:
|
||||
void set_use_gr2_materials(bool v);
|
||||
bool get_use_gr2_materials() const { return use_gr2_materials; }
|
||||
|
||||
// Sphere-map specular power (PARITY §2.7 / BACKLOG B9). In the original client
|
||||
// this is per skin-part, driven by the equipped body-armor item_proto
|
||||
// `bSpecular / 100`; 0 = flat (the base body). No equipment layer here yet, so
|
||||
// this is a manual hook — 0 keeps output identical to before.
|
||||
void set_specular_power(double p);
|
||||
double get_specular_power() const { return specular_power; }
|
||||
|
||||
// Explicit per-surface texture path override (index -> absolute .dds path).
|
||||
void set_surface_texture(int surface, const godot::String &path);
|
||||
|
||||
// When gr2_path is a .msm: [{index, model (abs .gr2), target_skin}, ...].
|
||||
godot::Array get_hair_options() const { return hair_options; }
|
||||
|
||||
// Attach a hair .gr2 whose skeleton bone names match the base. Its mesh is
|
||||
// merged into the CPU-skin path (skinned by the base skeleton's matrices,
|
||||
// bones remapped by name). "" detaches. GPU-skin path ignores hair for now.
|
||||
void set_hair_gr2(const godot::String &p);
|
||||
godot::String get_hair_gr2() const { return hair_gr2; }
|
||||
|
||||
// SourceSkin -> TargetSkin recolour (PARITY §2.4 / BACKLOG D3). The client's
|
||||
// `.msm` HairData ships one hair .gr2 with a `SourceSkin` (the texture baked
|
||||
// into the gr2 material) and a per-colour `TargetSkin` dds; `SetMaterialImage
|
||||
// Pointer(part, SourceSkin, load(TargetSkin))` swaps it. Here: when set, the
|
||||
// hair mesh uses this dds as its albedo instead of the gr2's sibling texture.
|
||||
// Absolute path (an entry's resolved `target_skin` from get_hair_options()).
|
||||
void set_hair_skin(const godot::String &p);
|
||||
godot::String get_hair_skin() const { return hair_skin; }
|
||||
|
||||
// Attach a rigid weapon .gr2 to a base-skeleton bone (PARITY §2.1 / BACKLOG
|
||||
// C5). The client links the weapon model instance to `equip_right_hand`
|
||||
// (`playersettingmodule.py`, warrior) and drives it with that bone's world
|
||||
// matrix (`ModelInstanceUpdate.cpp:148` GetBoneMatrixPointer). Here the weapon
|
||||
// mesh is a child MeshInstance3D whose transform = the bone's world pose from
|
||||
// gr2::sample_pose each frame. "" detaches. Path may be relative to the base
|
||||
// gr2 ("d:/ymir work/item/weapon/00040.gr2") or absolute.
|
||||
void set_weapon_gr2(const godot::String &p);
|
||||
godot::String get_weapon_gr2() const { return weapon_gr2; }
|
||||
void set_weapon_bone(const godot::String &b);
|
||||
godot::String get_weapon_bone() const { return weapon_bone; }
|
||||
// Off-hand shield (rigid, attaches to equip_left_hand like the weapon).
|
||||
void set_shield_gr2(const godot::String &p);
|
||||
godot::String get_shield_gr2() const { return shield_gr2; }
|
||||
void set_shield_bone(const godot::String &b);
|
||||
godot::String get_shield_bone() const { return shield_bone; }
|
||||
|
||||
// Fed by Metin2AnimPlayer after each gr2::sample_pose: base-skeleton world
|
||||
// matrices (world_pose output). Repositions attached rigid parts (weapon +
|
||||
// shield). No-op for a slot with no gr2 or an unresolved bone name.
|
||||
void update_weapon_pose(const std::vector<gr2::Mat4> &world_pose);
|
||||
|
||||
// Rebuild the subtree from the current properties.
|
||||
void reload();
|
||||
|
||||
@@ -71,6 +131,9 @@ public:
|
||||
const gr2::Skeleton *gr2_skeleton() const;
|
||||
godot::Skeleton3D *skeleton_node() const { return skel; }
|
||||
const gr2::FileInfo *gr2_fileinfo() const;
|
||||
// Latest full-affine deformer matrices. Initialized to bind pose on reload
|
||||
// and refreshed by either skinning path; attachments can share this pose.
|
||||
const std::vector<gr2::Mat4> ¤t_skin_matrices() const { return current_skin; }
|
||||
|
||||
// CPU linear-blend skinning: rewrite ArrayMesh vertex regions from the
|
||||
// per-bone deformer matrices (gr2::sample_pose's `skin` output). Debug/
|
||||
@@ -79,6 +142,13 @@ public:
|
||||
bool has_cpu_skin_mesh() const { return cpu_mesh.is_valid(); }
|
||||
void enable_cpu_skin(bool on);
|
||||
|
||||
// GPU linear-blend skinning: LBS in a custom vertex shader with the FULL
|
||||
// per-bone matrices uploaded as a float texture (no Skeleton3D -> shear kept).
|
||||
// Keeps the static build_mesh() output; just swaps materials + feeds the
|
||||
// bone texture each frame. MTGODOT_GPUSKIN=1 route.
|
||||
void enable_gpu_skin(bool on);
|
||||
void gpu_skin(const std::vector<gr2::Mat4> &skin);
|
||||
|
||||
// libgr2 sanity probe (kept from M0').
|
||||
godot::String probe_gr2(const godot::String &path) const;
|
||||
|
||||
@@ -93,8 +163,47 @@ private:
|
||||
bool flip_winding = false;
|
||||
|
||||
godot::String material_mode = "metin2"; // "metin2" (ShaderMaterial) | "standard"
|
||||
bool use_gr2_materials = false; // drive textures from gr2 material names (unverified)
|
||||
bool use_gr2_materials = true; // gr2 MaterialBindings -> texture (else: filename heuristic only)
|
||||
double specular_power = 0.0; // PARITY §2.7; 0 = disabled
|
||||
godot::Ref<godot::Texture2D> sphere_map; // shared sphere map, lazy-loaded
|
||||
godot::Ref<godot::Texture2D> _load_sphere_map(); // "ymir work/special/spheremap.jpg"
|
||||
godot::PackedStringArray surface_tex_override;
|
||||
godot::String resolved_gr2_dir; // dir of the actually-loaded .gr2 (for .msm)
|
||||
godot::Array hair_options; // from .msm HairData
|
||||
|
||||
godot::String hair_gr2; // attached hair .gr2 (or "")
|
||||
godot::String hair_skin; // TargetSkin dds override (or "")
|
||||
std::shared_ptr<std::optional<gr2::File>> hair_file; // loaded hair gr2
|
||||
std::vector<mtgodot::RenderPart> hair_parts;
|
||||
std::vector<int> hair_bone_remap; // hair skel idx -> base skel idx (by name)
|
||||
godot::Ref<godot::Material> hair_mat;
|
||||
godot::Ref<godot::ImageTexture> hair_tex; // resolved hair albedo (for GPU path)
|
||||
int gpu_base_surf = 0; // base surfaces before appended GPU hair
|
||||
void _load_hair();
|
||||
void _build_gpu_mesh(); // base + remapped hair surfaces -> mi->mesh (GPU-skin path, §2.2)
|
||||
|
||||
godot::String weapon_gr2; // attached weapon .gr2 (or "")
|
||||
godot::String weapon_bone = "equip_right_hand"; // base-skeleton attach bone
|
||||
std::shared_ptr<std::optional<gr2::File>> weapon_file; // loaded weapon gr2
|
||||
godot::MeshInstance3D *weapon_mi = nullptr; // child of this node
|
||||
int weapon_bone_idx = -1; // base skeleton index of weapon_bone
|
||||
// weapon's own bone[0] grip transform (invWorld · local); folded in so the mesh
|
||||
// aligns to the hand even when it is authored offset from its bone (client:
|
||||
// GrannySampleModelAnimationsAccelerated on the weapon skeleton). Identity when
|
||||
// the weapon has no skeleton or a ~identity bone.
|
||||
gr2::Mat4 weapon_pre{ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 };
|
||||
godot::String shield_gr2;
|
||||
godot::String shield_bone = "Bip01 L Hand"; // PC skeletons have no equip_left_hand
|
||||
std::shared_ptr<std::optional<gr2::File>> shield_file;
|
||||
godot::MeshInstance3D *shield_mi = nullptr;
|
||||
int shield_bone_idx = -1;
|
||||
gr2::Mat4 shield_pre{ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 };
|
||||
void _load_weapon();
|
||||
void _load_shield();
|
||||
// shared rigid-attach loader used by _load_weapon / _load_shield.
|
||||
void _load_attach(const godot::String &gr2_rel, const godot::String &bone_name,
|
||||
std::shared_ptr<std::optional<gr2::File>> &slot_file, godot::MeshInstance3D *&slot_mi,
|
||||
int &slot_bone_idx, gr2::Mat4 &slot_pre, const char *node_name, const char *label);
|
||||
|
||||
std::shared_ptr<std::optional<gr2::File>> file; // shared_ptr so accessor stays valid
|
||||
std::vector<gr2::MaterialInfo> materials;
|
||||
@@ -103,12 +212,37 @@ private:
|
||||
godot::String last_info;
|
||||
godot::HashMap<godot::String, godot::Ref<godot::ImageTexture>> tex_cache;
|
||||
|
||||
// §2.10 LOD
|
||||
bool lod_enabled = true;
|
||||
int lod_level = 0; // 0 = full model, 1..3 = _lod_0N
|
||||
godot::PackedFloat32Array lod_dist;
|
||||
std::vector<std::shared_ptr<std::optional<gr2::File>>> lod_files; // [0]=_lod_01 ...
|
||||
std::vector<std::vector<mtgodot::RenderPart>> lod_parts; // parallel to lod_files
|
||||
// LOD crossfade: a frozen ghost of the outgoing level fades out while the new
|
||||
// mesh fades in (LODController::BlendRenderWithOneTexture). ~0.18 s.
|
||||
godot::MeshInstance3D *lod_prev_mi = nullptr;
|
||||
double lod_fade_t = -1.0; // <0 = not fading
|
||||
std::vector<godot::Ref<godot::Material>> lod_ghost_mats;
|
||||
void _end_lod_fade();
|
||||
std::vector<mtgodot::RenderPart> base_parts;
|
||||
void _load_lods(const godot::String &loaded_spec);
|
||||
void _set_lod(int n);
|
||||
const gr2::FileInfo *active_fi() const; // base or current LOD (mesh/material data)
|
||||
|
||||
std::vector<mtgodot::RenderPart> parts; // surface s -> (gr2 mesh, tri_group, material)
|
||||
std::vector<godot::Ref<godot::Material>> surf_mats; // resolved once; re-assigned each cpu_skin frame
|
||||
godot::Ref<godot::ArrayMesh> cpu_mesh; // rebuilt each frame in cpu_skin mode
|
||||
std::vector<int> cpu_surf_mesh; // surface -> gr2 mesh index
|
||||
bool gpu_skin_active = false;
|
||||
std::vector<gr2::Mat4> current_skin;
|
||||
godot::Ref<godot::Image> bones_img; // RGBAF 3 x bone_count
|
||||
godot::Ref<godot::ImageTexture> bones_tex;
|
||||
|
||||
void _clear_children();
|
||||
void _apply_materials();
|
||||
godot::String _guess_texture_dir() const;
|
||||
// Resolve a "d:/ymir work/..." path referenced from `base` (.msm/.msa) to an
|
||||
// absolute file. Passes existing absolute paths through.
|
||||
static godot::String resolve_rel_gr2(const godot::String &base, const godot::String &spec);
|
||||
godot::Ref<godot::ImageTexture> _load_dds(const godot::String &path);
|
||||
godot::Ref<godot::ImageTexture> _resolve_texture(const godot::String &dir,
|
||||
const godot::String &stem, const godot::String &surface_name,
|
||||
|
||||
Reference in New Issue
Block a user