Files
metin2-client/libgr2/src/gr2_material.cpp
T
shenandClaude Sonnet 5 47baf6c0c6 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
2026-08-31 20:02:12 +09:00

124 lines
4.7 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// libgr2 —— 材质视图(附加,不改动既有解析)。
// root.Materials (ArrayOfReferences) → granny_material:
// Name (String)
// Texture (Reference → granny_texture{ FromFileName (String) })
// Maps (RefToArray → granny_material_map{ Usage (String); Material (Reference → granny_material) })
// diffuse 取 Usage 含 "diffuse" 的 map,退而取第一张贴图。
#include "gr2/gr2.h"
#include "internal.h"
#include <algorithm>
#include <cctype>
namespace gr2 {
using detail::TypeMember;
using Ref = detail::Ref;
namespace {
std::string lower(std::string s) {
for (auto& c : s) c = char(std::tolower((unsigned char)c));
return s;
}
// helpers 用 `auto&` 模板参数,避免命名私有的 File::Impl(只有 dump_materials 是 friend)。
// 从一个 granny_texture 对象读 FromFileName。
std::string read_texture_name(auto& im, Ref tex_obj, Ref tex_type) {
if (!tex_obj.valid() || !tex_type.valid()) return {};
const auto& ms = im.parse_type(tex_type);
if (const TypeMember* m = im.find_member(ms, "FromFileName"))
return im.rd_str(im.member_slot(tex_obj, *m));
return {};
}
struct WalkState {
MaterialInfo* out;
std::vector<std::string> encounter; // 按遇到顺序
std::string diffuse; // 首个 Usage~=diffuse 的贴图
};
// 递归遍历一个 granny_material,收集 FromFileName。
void walk_material(auto& im, Ref mat_obj, Ref mat_type, int depth,
WalkState& st, bool parent_is_diffuse) {
if (depth > 4 || !mat_obj.valid() || !mat_type.valid()) return;
const auto& ms = im.parse_type(mat_type);
if (st.out->name.empty()) {
if (const TypeMember* mn = im.find_member(ms, "Name"))
st.out->name = im.rd_str(im.member_slot(mat_obj, *mn));
}
if (const TypeMember* mt = im.find_member(ms, "Texture")) {
Ref tex = im.read_reference(mat_obj, *mt);
std::string fn = read_texture_name(im, tex, mt->ref_type);
if (!fn.empty()) {
st.encounter.push_back(fn);
if (parent_is_diffuse && st.diffuse.empty()) st.diffuse = fn;
}
}
if (const TypeMember* mm = im.find_member(ms, "Maps")) {
Ref base;
uint32_t count = 0, stride = 0;
if (im.read_ref_to_array(mat_obj, *mm, base, count, stride) && count && stride) {
const auto& map_ms = im.parse_type(mm->ref_type);
const TypeMember* mu = im.find_member(map_ms, "Usage");
const TypeMember* msub = im.find_member(map_ms, "Material");
for (uint32_t i = 0; i < count && i < 64; ++i) {
Ref map_obj{ base.section, base.offset + i * stride };
bool is_diffuse = parent_is_diffuse;
if (mu) {
std::string u = lower(im.rd_str(im.member_slot(map_obj, *mu)));
is_diffuse = u.find("diffuse") != std::string::npos || u.empty();
}
if (msub) {
Ref sub = im.read_reference(map_obj, *msub);
walk_material(im, sub, msub->ref_type, depth + 1, st, is_diffuse);
}
}
}
}
}
} // namespace
std::vector<MaterialInfo> dump_materials(const File& f) {
std::vector<MaterialInfo> out;
if (!f.impl_ || !f.impl_->root_type.valid() || !f.impl_->root_obj.valid()) return out;
File::Impl& im = *f.impl_;
const auto& root_ms = im.parse_type(im.root_type);
const TypeMember* mats = im.find_member(root_ms, "Materials");
if (!mats) return out;
std::vector<Ref> mat_refs = im.read_array_of_refs(im.root_obj, *mats);
out.reserve(mat_refs.size());
for (Ref mr : mat_refs) {
MaterialInfo mi;
WalkState st{ &mi, {}, {} };
walk_material(im, mr, mats->ref_type, 0, st, /*parent_is_diffuse=*/true);
mi.diffuse_texture = !st.diffuse.empty() ? st.diffuse
: (st.encounter.empty() ? std::string{} : st.encounter.front());
mi.map_count = (int)st.encounter.size();
mi.all_textures = st.encounter;
std::sort(mi.all_textures.begin(), mi.all_textures.end());
mi.all_textures.erase(std::unique(mi.all_textures.begin(), mi.all_textures.end()),
mi.all_textures.end());
// 渲染态推断(EterGrnLib Material.cpp 的名字/map 逻辑)
std::string ln = lower(mi.name);
mi.alpha_blend = ln.rfind("blend", 0) == 0 && mi.map_count > 1;
for (const char* kw : {"2side", "twoside", "two_side", "double", "leaf", "tree",
"ivy", "fence", "grass", "branch", "flag"})
if (ln.find(kw) != std::string::npos) { mi.two_sided = true; break; }
out.push_back(std::move(mi));
}
return out;
}
} // namespace gr2