// M0 T1 —— header + section table + fixup。见 docs/reference/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 #include #include 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 个已知 magic(LE u32 序列) struct KnownMagic { uint32_t v[4]; Magic tag; const char* name; }; constexpr KnownMagic kMagics[] = { // GRNFileMV_Old —— Metin2 的 gr2(v6 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_value:4*u32 magic + HeaderSize + HeaderFormat + 2*u32 reserved constexpr size_t kSectionSize = 44; // grn_section:11 * 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::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(); 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::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 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 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