Files
mtgodot-poc/libgr2/src/gr2_file.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

206 lines
8.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.
// M0 T1 —— header + section table + fixup。见 docs/steps/M0-gr2-reader.md
// 规格:MobileSource/Cross Platform/Granny-3D-SDK-main/source/granny_file_format.h + .cpp
//
// 本次实现范围:magic 识别 + grn_file_header + section 表解析(--sections 用)。
// section 解压(Oodle0/1)是 T2;类型树 / FileInfo 是 T3/T4 —— 仍是 stub。
#include "internal.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
namespace gr2 {
namespace {
// 小端读取 + 边界检查
inline bool rd32(const uint8_t* p, size_t len, size_t off, uint32_t& out) {
if (off + 4 > len) return false;
out = uint32_t(p[off]) | (uint32_t(p[off + 1]) << 8) |
(uint32_t(p[off + 2]) << 16) | (uint32_t(p[off + 3]) << 24);
return true;
}
// granny_file_format.cpp 里的 5 个已知 magicLE u32 序列)
struct KnownMagic { uint32_t v[4]; Magic tag; const char* name; };
constexpr KnownMagic kMagics[] = {
// GRNFileMV_Old —— Metin2 的 gr2v6 era)用这个
{{0xCAB067B8, 0x0FB16DF8, 0x7E8C7284, 0x1E00195E}, Magic::Bit32_LE, "Old(32LE)"},
{{0xC06CDE29, 0x2B53A4BA, 0xA5B7F525, 0xEEE266F6}, Magic::Bit32_LE, "32Bit_LittleEndian"},
{{0xB595110E, 0x4BB5A56A, 0x502828EB, 0x04B37825}, Magic::Bit32_BE, "32Bit_BigEndian"},
{{0x5E499BE5, 0x141F636F, 0xA9EB131E, 0xC4EDBE90}, Magic::Bit64_LE, "64Bit_LittleEndian"},
{{0xE3D49531, 0x624FDC20, 0x3AD036CC, 0x89FF82B1}, Magic::Bit64_BE, "64Bit_BigEndian"},
};
constexpr size_t kMagicStructSize = 32; // grn_file_magic_value4*u32 magic + HeaderSize + HeaderFormat + 2*u32 reserved
constexpr size_t kSectionSize = 44; // grn_section11 * u32
} // namespace
const char* magic_name(Magic m) {
switch (m) {
case Magic::Bit32_LE: return "32-bit LE";
case Magic::Bit32_BE: return "32-bit BE";
case Magic::Bit64_LE: return "64-bit LE";
case Magic::Bit64_BE: return "64-bit BE";
default: return "unknown";
}
}
const char* compression_name(uint32_t f) {
switch (f) {
case 0: return "none";
case 1: return "Oodle0";
case 2: return "Oodle1";
default: return "?";
}
}
std::optional<File> File::load(const uint8_t* bytes, size_t len, LoadError* err) {
if (err) *err = {};
auto fail = [&](const char* stage, const char* msg) {
if (err) *err = {stage, msg};
return std::nullopt;
};
if (!bytes || len < kMagicStructSize + 32)
return fail("header", "file too small");
// ── magic ────────────────────────────────────────────────────────────
uint32_t m[4];
for (int i = 0; i < 4; ++i) rd32(bytes, len, size_t(i) * 4, m[i]);
Magic magic = Magic::Unknown;
for (const auto& k : kMagics) {
if (m[0] == k.v[0] && m[1] == k.v[1] && m[2] == k.v[2] && m[3] == k.v[3]) {
magic = k.tag;
break;
}
}
if (magic == Magic::Unknown)
return fail("header", "not a Granny file (unknown magic)");
if (magic != Magic::Bit32_LE)
return fail("header", "only 32-bit little-endian supported (M0 range)");
uint32_t header_size = 0, header_format = 0;
rd32(bytes, len, 16, header_size);
rd32(bytes, len, 20, header_format); // 注意:这不是 section 压缩,section 压缩看每段 Format
// ── grn_file_header(在 magic 结构之后)─────────────────────────────
const size_t H = kMagicStructSize; // header 起点
// grn_file_header: Version(0) TotalSize(4) CRC(8) SectionArrayOffset(12)
// SectionArrayCount(16) RootObjectTypeDefinition{Section(20) Offset(24)}
// RootObject{Section(28) Offset(32)} TypeTag(36) ...
uint32_t version = 0, total_size = 0, sec_off = 0, sec_count = 0;
uint32_t rt_sec = 0, rt_off = 0, ro_sec = 0, ro_off = 0;
if (!rd32(bytes, len, H + 0, version) ||
!rd32(bytes, len, H + 4, total_size) ||
!rd32(bytes, len, H + 12, sec_off) ||
!rd32(bytes, len, H + 16, sec_count) ||
!rd32(bytes, len, H + 20, rt_sec) ||
!rd32(bytes, len, H + 24, rt_off) ||
!rd32(bytes, len, H + 28, ro_sec) ||
!rd32(bytes, len, H + 32, ro_off))
return fail("header", "truncated grn_file_header");
// ── section 表 ──────────────────────────────────────────────────────
// SectionArrayOffset 相对 grn_file_header 起点(H
const size_t sec_base = H + sec_off;
if (sec_count > 64)
return fail("section", "implausible section count");
if (sec_base + size_t(sec_count) * kSectionSize > len)
return fail("section", "section array out of range");
// ── AC 自检(不致命,仅 GR2_DEBUG 打日志)──────────────────────────
if (std::getenv("GR2_DEBUG")) {
if (version != 6 && version != 7)
std::fprintf(stderr, "gr2: warn: format_version=%u (期望 6/7)\n", version);
if (total_size != len)
std::fprintf(stderr, "gr2: warn: total_size=%u != file size %zu\n", total_size, len);
}
File out;
out.magic_ = magic;
out.version_ = version;
out.total_size_ = total_size;
out.impl_ = std::make_shared<Impl>();
out.impl_->sections.resize(sec_count);
out.impl_->raw.resize(sec_count);
out.sections_.reserve(sec_count);
// ── 逐 section:读头 + T2 展开 ─────────────────────────────────────
for (uint32_t i = 0; i < sec_count; ++i) {
const size_t s = sec_base + size_t(i) * kSectionSize;
uint32_t f[11];
for (int j = 0; j < 11; ++j) rd32(bytes, len, s + size_t(j) * 4, f[j]);
// f: Format DataOffset DataSize ExpandedDataSize InternalAlignment
// First16Bit First8Bit PtrFixupOff PtrFixupCount MixedFixupOff MixedFixupCount
detail::GrnSection gs{};
gs.compression = f[0];
gs.data_offset = f[1];
gs.data_size = f[2];
gs.expanded_data_size = f[3];
gs.internal_alignment = f[4];
gs.first16bit = f[5];
gs.first8bit = f[6];
gs.pointer_fixup_off = f[7];
gs.pointer_fixup_count= f[8];
gs.mixed_fixup_off = f[9];
gs.mixed_fixup_count = f[10];
if (size_t(gs.data_offset) > len || size_t(gs.data_size) > len - size_t(gs.data_offset))
return fail("section", "section data out of range");
out.impl_->raw[i] = gs;
LoadError sec_err;
if (!out.impl_->decompress_section(gs, bytes + gs.data_offset,
out.impl_->sections[i], &sec_err)) {
if (err) {
*err = sec_err;
err->message += " (section " + std::to_string(i) + ")";
}
return std::nullopt;
}
out.sections_.push_back(out.impl_->sections[i].info);
}
// ── T1:fixup 索引(需要展开后的 section 数据 + 原始 section 头)──────
out.impl_->build_fixups(bytes, len);
out.impl_->root_type = { int32_t(rt_sec), rt_off };
out.impl_->root_obj = { int32_t(ro_sec), ro_off };
// ── T3/T4:类型树 + FileInfo 汇总。失败必须传播;返回一个看似成功但
// FileInfo 为空的对象会让上层把损坏文件误判成合法的空模型。
LoadError fi_err;
if (!out.impl_->build_fileinfo(out.info_, &fi_err)) {
if (err) *err = fi_err;
return std::nullopt;
}
(void)header_size; (void)header_format;
return out;
}
std::optional<File> File::load_path(const std::string& path, LoadError* err) {
FILE* f = std::fopen(path.c_str(), "rb");
if (!f) { if (err) *err = {"open", "cannot open " + path}; return std::nullopt; }
std::fseek(f, 0, SEEK_END);
long n = std::ftell(f);
std::fseek(f, 0, SEEK_SET);
std::vector<uint8_t> buf(n > 0 ? size_t(n) : 0);
if (!buf.empty()) {
size_t rd = std::fread(buf.data(), 1, buf.size(), f);
if (rd != buf.size()) { std::fclose(f); if (err) *err = {"open", "short read"}; return std::nullopt; }
}
std::fclose(f);
return load(buf.data(), buf.size(), err);
}
double File::bind_pose_self_check(int skeleton) const {
return impl_ ? impl_->skeleton_self_check(skeleton) : 1e9;
}
std::span<const uint8_t> File::section_bytes(size_t i) const {
if (!impl_ || i >= impl_->sections.size()) return {};
const auto& d = impl_->sections[i].data;
return {d.data(), d.size()};
}
} // namespace gr2