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:
shen
2026-08-31 20:02:12 +09:00
co-authored by Claude Sonnet 5
parent f4917a2b3b
commit 47baf6c0c6
414 changed files with 69568 additions and 385 deletions
+25
View File
@@ -0,0 +1,25 @@
# libgr2 —— gr2 v6 只读读取器(M0)。零第三方依赖。
add_library(libgr2 STATIC
src/gr2_file.cpp # T1 header + section table + fixup
src/gr2_decompress.cpp # T2 section 解压分派
src/oodle1.c # T2 Granny Oodle1 解码(端口自泄露 SDK
src/gr2_typetree.cpp # T3 自描述类型树遍历
src/gr2_fileinfo.cpp # T4 FileInfo 汇总
src/gr2_skeleton.cpp # T5 骨架 + bind pose 自洽检查
src/gr2_mesh.cpp # T6 顶点 / 索引 / 三角组 / BoneBindings
src/gr2_anim.cpp # T7a/T7b 曲线分类 + 解码子集
src/gr2_material.cpp # 材质视图(附加):root.Materials → 贴图文件名
)
add_library(xrender::libgr2 ALIAS libgr2)
target_include_directories(libgr2
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_compile_features(libgr2 PUBLIC cxx_std_20)
if(BUILD_TESTING)
add_executable(libgr2_loader_errors_test tests/loader_errors.cpp)
target_link_libraries(libgr2_loader_errors_test PRIVATE xrender::libgr2)
add_test(NAME libgr2.loader_errors COMMAND libgr2_loader_errors_test)
endif()
+42
View File
@@ -0,0 +1,42 @@
# libgr2
自研 gr2 v6/v7 **只读**读取器。零第三方依赖。施工文档 [`../docs/steps/M0-gr2-reader.md`](../docs/steps/M0-gr2-reader.md)。
## 能做什么
- 容器:magic / header / section / pointer-fixup(懒解引用,不物化 32 位指针)
- section 解压:Oodle1`oodle1.c`
- 自描述类型树遍历(不硬编码 struct 布局)
- `FileInfo`skeletons / meshes / materials / textures / models / animations
- 骨架 + `granny_transform` → 4x4 组合(`BuildCompositeTransform4x4` 语义)
- 蒙皮网格:顶点(按顶点格式自描述解)/ 索引 / 三角组 / BoneBindings(多 skeleton 取最优匹配)
- 材质:`granny_material` 链(直接 `.Texture` 或递归 `.Maps[].Map`)→ `granny_texture.FromFileName`
`Mesh.material_textures``MaterialBindings` 平行,`tri_group.material_index` 索引它
- 动画曲线(`OldCurveType` B 样条,degree 0/1/2)解码 + `gr2::sample_pose`(跨文件按骨骼名 retarget
## 验证
- 全量 9166 个 Metin2 `.gr2`:**0 崩溃、0 非退化谓词失败**(`tools/gr2fuzz`
- **对拍真 Granny 2.9.12**:骨骼世界矩阵 ≤ 4.6e-5、蒙皮顶点 ≤ 6.5e-5`tools/oracle_diff``test/m2-numeric.json`23 用例含 v7 / 双 root / 刚体 / 多动画帧)
- 坏输入(空 / 错 magic / 截断 / 越界索引)一律干净报错,不崩
## 明确不做(Metin2 语料里 0 出现;要「任意 gr2」才需要)
| 缺口 | 现状 | 补的成本 |
|---|---|---|
| **writer** | 无(设计上只读)| 大,PoC 不需要 |
| Oodle0 解压(`Format==1`| `decompress_status=1`,不崩 | 中,同族算法变体 |
| BitKnit 解压(`Format==4`| `decompress_status=4`,不崩 | 大,无公开实现;退路:Windows 侧离线转未压缩 |
| 压缩曲线(`curve2` / `CurveData*` 变体)| `classify_curve``Identity`/`Unknown``read_old_curve` 跳过 | 中,`granny_curve.cpp``Da*` 分支 |
| B 样条 degree ≥ 3 | `eval` 退化到 degree 2 | 小,加 `CubicCoefficients` |
| 64 位 / big-endian gr2 | `load` 直接拒(`only 32-bit little-endian`| 中 |
| 多 skeleton 文件里刚体挂件的 skeleton 归属 | mesh binding 取「悬空最少」的 skeleton`oracle_diff` 对这种 mesh 会误判(不是 libgr2 的问题)| 需要 FileInfo 暴露 per-model skeleton 索引 |
## ⚠ 许可
`src/oodle1.c` 是从泄露的 Granny SDK **直接端口**的解码路径(约 450 行)。
- **内部研究 / 不发布 / 非商用**:可用(和「拿泄露 SDK 当字节级规格」同一风险类别,全项目都这么做)。
- **任何对外发布 / 商用**:必须 clean-room 重写(照公开的「granny2 Oodle1」算法描述从零写,不看这份端口)。
其余代码(容器 / 类型树 / 骨架 / 网格 / 曲线 / 姿势)是照泄露 SDK **头文件的 struct 定义和公式注释**自己写的,不是复制实现。
+97
View File
@@ -0,0 +1,97 @@
// libgr2 —— gr2 v6 只读读取器(demo 子集)。
// 施工文档:docs/steps/M0-gr2-reader.md
#pragma once
#include "types.h"
#include <cstdint>
#include <cstddef>
#include <memory>
#include <optional>
#include <span>
#include <string>
#include <vector>
namespace gr2 {
enum class Magic { Unknown, Bit32_LE, Bit32_BE, Bit64_LE, Bit64_BE };
struct LoadError {
std::string stage; // "header" / "section" / "typetree" / "fileinfo" / ...
std::string message;
};
// T3:通用类型树遍历(gr2dump 打印成员用;不硬编码 struct 布局)
struct MemberDump { std::string name; int member_type; int array_width; };
class File;
std::vector<MemberDump> dump_root_members(const File&);
// 材质视图(类型树遍历 root.Materials → granny_material)。
// 只读文件名,不解码贴图。diffuse_texture 取 Usage 含 "diffuse" 的 map 的
// 贴图,退而取第一张;all_textures 是从该材质可达的全部 FromFileName。
struct MaterialInfo {
std::string name;
std::string diffuse_texture;
std::vector<std::string> all_textures;
// Metin2/EterGrnLib 渲染态推断(gr2 不带 D3DRS,靠名字 + map 数 + ExtendedData):
int map_count = 0; // granny_material.MapCount(可达子材质的 Texture 数)
bool alpha_blend = false;// Name 以 "Blend" 开头且 map_count>1EterGrnLib Material.cpp:233
bool two_sided = false; // 名字含 2side/twoside/double/leaf/tree/ivy/fenceExtendedData "Two-sided" 的近似)
};
std::vector<MaterialInfo> dump_materials(const File&);
// 已解析、fixup 后的文件。析构释放所有 section 缓冲。
class File {
public:
static std::optional<File> load(const uint8_t* bytes, size_t len, LoadError* err);
static std::optional<File> load_path(const std::string& path, LoadError* err);
// T1
Magic magic() const { return magic_; }
uint32_t format_version()const { return version_; } // 期望 6
uint32_t total_size() const { return total_size_; }
const std::vector<SectionInfo>& sections() const { return sections_; }
// T4 —— 完整汇总(内部会按需触发 T5/T6/T7a)
const FileInfo& file_info() const { return info_; }
// 展开后的 section 字节(T2 产出)。空 section 返回空 span。调试 / 校验用。
std::span<const uint8_t> section_bytes(size_t i) const;
// T5 —— bind pose 自洽检查(不依赖 oracle)。返回 max|world_bind·invBind I|
// 阈值策略见 M0-gr2-reader.md T5noise_floor 前用 1e-3
double bind_pose_self_check(int skeleton = 0) const;
// M2 便捷封装(同文件内的 skeleton + animation)。跨文件(model gr2 + anim gr2
// 用自由函数 gr2::sample_pose(skeleton, animation, t, ...)。anim/skeleton 是 file_info() 索引。
bool sample_pose(int anim, float t, int skeleton,
std::vector<Mat4>& world_pose, std::vector<Mat4>& skin) const;
bool bind_pose(int skeleton, std::vector<Mat4>& world_pose, std::vector<Mat4>& skin) const;
private:
File() = default;
Magic magic_ = Magic::Unknown;
uint32_t version_ = 0;
uint32_t total_size_ = 0;
std::vector<SectionInfo> sections_;
FileInfo info_;
// 内部:section 缓冲、root object 指针、类型树 —— 放 .cpp 的 Impl
struct Impl;
std::shared_ptr<Impl> impl_;
friend std::vector<MemberDump> dump_root_members(const File&);
friend std::vector<MaterialInfo> dump_materials(const File&);
};
// ── M2:姿势采样(model 的 skeleton + anim 的 tracks 可来自不同 gr2)──────
// Granny 语义、行主序、平移在 elem 12..14
// world[i] = Composite(local[i]) · (parent<0 ? sk.initial_placement : world[parent])
// skin[i] = sk.bones[i].inverse_world · world[i]
// tracks 按骨骼名匹配 sk;无匹配的骨骼用 sk 的 bind LocalTransform。
void sample_pose(const Skeleton& sk, const Animation& an, float t,
std::vector<Mat4>& world, std::vector<Mat4>& skin);
void bind_pose(const Skeleton& sk, std::vector<Mat4>& world, std::vector<Mat4>& skin);
// 打印辅助
const char* magic_name(Magic);
const char* compression_name(uint32_t section_format); // 0=none 1=Oodle0 2=Oodle1
} // namespace gr2
+127
View File
@@ -0,0 +1,127 @@
// libgr2 —— 对外 POD 视图。不泄露内部指针 / SDK 类型。
// 规格来源:MobileSource/Cross Platform/Granny-3D-SDK-main/source/
// granny_file_format.h / granny_data_type_definition.h / granny_transform.h / granny_file_info.h
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include <span>
#include <array>
namespace gr2 {
using Mat4 = std::array<float, 16>; // 行主序,Granny 原生。坐标约定见 docs/steps/M0-gr2-reader.md「约定」
// ── 骨架 ──────────────────────────────────────────────────────────────
struct Bone {
std::string name;
int32_t parent = -1; // < 自身索引,或 -1 = 根
Mat4 local_transform{}; // granny_transform SRT → 4x4(组合顺序照 granny_transform.cpp
Mat4 inverse_world{}; // InverseWorld4x4,来自文件
uint32_t lt_flags = 0; // granny_transform.Flags:哪部分非单位
};
struct Skeleton {
std::vector<Bone> bones;
Mat4 initial_placement{1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1}; // 绑定它的 model 的 InitialPlacement
};
// ── 网格 ──────────────────────────────────────────────────────────────
enum class VertexKind { PNT332, PNT3322, PNT332_Skinned, PNT3322_Skinned, Unknown };
struct Vertex { // demo 统一打包结构
float pos[3];
float normal[3];
float uv0[2];
float uv1[2]; // PNT3322 才有效
uint8_t bone_index[4] = {0}; // *_Skinned 才有效
uint8_t bone_weight[4] = {0}; // 归一 u8
};
struct TriGroup {
int32_t material_index;
int32_t tri_first; // 三角序号
int32_t tri_count;
};
struct Mesh {
std::string name;
VertexKind kind = VertexKind::Unknown;
std::vector<Vertex> vertices;
std::vector<uint32_t> indices; // 三角形列表
std::vector<TriGroup> tri_groups;
std::vector<int32_t> bone_bindings; // mesh 局部骨骼槽 → skeleton 骨骼索引
uint32_t source_bone_weight_slots = 0; // 顶点类型声明值;审计 >4/8 权重
uint32_t source_bone_index_slots = 0; // packed UInt32 记为 4
// 与 granny_mesh.MaterialBindings 平行:每项 = 解析出的漫反射贴图文件名
// granny_texture.FromFileName 原样,通常是 "D:\Ymir Work\...\x.dds";空 = 未解析)。
// tri_groups[i].material_index 索引本表。
std::vector<std::string> material_textures;
bool rigid = false; // GrannyMeshIsRigid
};
// ── 动画 ──────────────────────────────────────────────────────────────
// 曲线子类型:T7a 分类。Metin2 = Granny 2.4,全是 OldCurveTypegranny_curve.cpp):
// {Int32 Degree; RefToArray Knots(Real32[]); RefToArray Controls(Real32[])}
// 无压缩变体。label 形如 "Old(d=2,dim=4)"。
enum class CurveFormat { Keyframes32f, /* T7a 跑 fuzz 后补齐 */ Unknown };
// T7b:解出的曲线(B 样条:knots 时间轴 + controls 控制点,行优先 [knot][dim]
struct Curve {
int32_t degree = 0;
uint32_t dim = 0; // 3=vec, 4=quat, 9=3x3 scaleshear
std::vector<float> knots; // KnotCount 个
std::vector<float> controls; // KnotCount * dim 个
bool empty() const { return knots.empty() || dim == 0; }
// t 处求值,写 dim 个 float 到 outidentity 由调用方给)。
void eval(float t, float* out) const;
};
struct BoneTrack {
std::string bone_name;
int32_t bone_index = -1; // 映射到 skeleton(未映射 = -1
std::string pos_type = "none";
std::string rot_type = "none";
std::string scale_type = "none";
Curve position; // dim 3
Curve orientation; // dim 4(四元数 x,y,z,w
Curve scale_shear; // dim 93x3)或 dim 3
};
struct Animation {
std::string name;
float duration = 0.0f;
std::vector<BoneTrack> tracks;
// t 处每个 track 的局部 4x4granny_transform 组合语义,行主序)。
// out 按 tracks 顺序;bone_index 见各 track。
void sample_local(float t, std::vector<Mat4>& out) const;
};
// 顶层 granny_materialdriven by root Materials[])。name + 解析出的漫反射贴图。
struct Material {
std::string name;
std::string diffuse_texture; // granny_texture.FromFileName 原样
};
// ── FileInfo 汇总(T4)────────────────────────────────────────────────
struct FileInfo {
std::string from_file_name;
std::vector<Skeleton> skeletons;
std::vector<Mesh> meshes;
std::vector<Animation> animations;
std::vector<Material> materials;
uint32_t material_count = 0;
uint32_t texture_count = 0;
uint32_t model_count = 0;
};
// ── section 表(T1gr2dump --sections 用)────────────────────────────
struct SectionInfo {
uint32_t compression; // 0=none 1/2=Oodle0/1 4=BitKnit
uint32_t data_size; // 压缩后
uint32_t expanded_size; // 展开后
uint32_t pointer_fixup_count;
int decompress_status; // 0=ok,非 0=解压失败/未实现(T2)
};
} // namespace gr2
+270
View File
@@ -0,0 +1,270 @@
// M0 T7a —— 曲线子类型分类(gr2fuzz 直方图依赖)。T7b(解码)见文末。
// 见 docs/steps/M0-gr2-reader.md
// 规格:granny_track_group.cpp (TrackGroupType/TransformTrackType/Curve2Type) /
// granny_curve.cpp (CurveData*Type,均以 CurveDataHeaderType 开头) /
// granny_curve.h (curve_data_header{ u8 Format; u8 Degree })
#include "internal.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#include <cmath>
#define ADBG(...) do { if (getenv("GR2_DEBUG")) fprintf(stderr, "[anim] " __VA_ARGS__); } while (0)
namespace gr2 {
using detail::Ref;
using detail::TypeMember;
// Metin2 = Granny 2.4:曲线是 OldCurveTypegranny_curve.cpp):
// {Int32 Degree; RefToArray Knots(Real32[]); RefToArray Controls(Real32[])} —— 20B,无压缩变体。
// curve_obj 指向 transform_track 里 inline 的那个 curve 对象本体。
// 新版(2.6+)是 curve2{variant CurveData},本函数一并识别、打标签。
std::string File::Impl::classify_curve_obj(Ref curve_obj, Ref curve_type) {
if (!curve_obj.valid() || !curve_type.valid()) return "none";
const auto& ms = parse_type(curve_type);
if (ms.empty()) return "Unknown";
// 老格式:第一个成员是 "Degree"
if (ms[0].name == "Degree") {
const TypeMember* mD = &ms[0];
const TypeMember* mK = find_member(ms, "Knots");
const TypeMember* mC = find_member(ms, "Controls");
int32_t degree = rd_i32(member_slot(curve_obj, *mD));
uint32_t kn = mK ? uint32_t(std::max(0, rd_i32(member_slot(curve_obj, *mK)))) : 0;
uint32_t cn = mC ? uint32_t(std::max(0, rd_i32(member_slot(curve_obj, *mC)))) : 0;
uint32_t dim = kn ? cn / kn : 0;
char buf[64];
std::snprintf(buf, sizeof buf, "Old(d=%d,dim=%u)", degree, dim);
return buf;
}
// 新格式:curve2{variant CurveData}
if (ms[0].name == "CurveData") {
Ref type_ref = follow(member_slot(curve_obj, ms[0])); // variant.Type
if (!type_ref.valid()) return "Identity";
const auto& dm = parse_type(type_ref);
if (dm.empty()) return "Unknown";
const std::string& n = dm[0].name;
const char* pfx = "CurveDataHeader_";
return (n.rfind(pfx, 0) == 0) ? n.substr(std::strlen(pfx)) : n;
}
return "Unknown";
}
// 读一条 OldCurveType 曲线({Int32 Degree; RefToArray Knots; RefToArray Controls})到 Curve。
void File::Impl::read_old_curve(Ref curve_obj, Ref curve_type, Curve& out) {
if (!curve_obj.valid() || !curve_type.valid()) return;
const auto& ms = parse_type(curve_type);
if (ms.empty() || ms[0].name != "Degree") return; // 新格式暂不解
const TypeMember* mD = &ms[0];
const TypeMember* mK = find_member(ms, "Knots");
const TypeMember* mC = find_member(ms, "Controls");
if (!mK || !mC) return;
out.degree = rd_i32(member_slot(curve_obj, *mD));
auto read_farr = [&](const TypeMember& m, std::vector<float>& dst) {
Ref slot = member_slot(curve_obj, m);
int32_t cnt = rd_i32(slot);
if (cnt <= 0 || cnt > (1 << 24)) return;
Ref base = follow({slot.section, slot.offset + 4});
const uint8_t* p = at(base, size_t(cnt) * 4);
if (!p) return;
dst.resize(cnt);
for (int32_t k = 0; k < cnt; ++k) std::memcpy(&dst[k], p + size_t(k) * 4, 4);
};
read_farr(*mK, out.knots);
read_farr(*mC, out.controls);
out.dim = out.knots.empty() ? 0 : uint32_t(out.controls.size() / out.knots.size());
if (out.dim && out.knots.size() * out.dim != out.controls.size()) out.dim = 0; // 不一致 → 弃
// 有些资产的曲线控制点本身是 NaN(如 redthief_general/*_damage.gr2 的 finger track)。
// 不 sanitize 会把 NaN 灌进整条蒙皮链。→ 整条曲线弃(eval 返回空,调用方回退到 bind)。
for (float v : out.knots) if (!std::isfinite(v)) { out.dim = 0; return; }
for (float v : out.controls) if (!std::isfinite(v)) { out.dim = 0; return; }
}
// animation: {String Name; Real32 Duration; Real32 TimeStep; Real32 Oversampling;
// ArrayOfRefs TrackGroups; ...}
// track_group: {String Name; RefToArray VectorTracks; RefToArray TransformTracks; ...}
// transform_track: {String Name; Int32 Flags; Inline OrientationCurve(curve2);
// Inline PositionCurve(curve2); Inline ScaleShearCurve(curve2)}
bool File::Impl::extract_animation(Ref anim_obj, Ref anim_type, const Skeleton* skel, Animation& out) {
if (!anim_obj.valid() || !anim_type.valid()) return false;
const auto& ams = parse_type(anim_type);
if (const TypeMember* m = find_member(ams, "Name"))
out.name = rd_str(member_slot(anim_obj, *m));
if (const TypeMember* m = find_member(ams, "Duration"))
out.duration = rd_f32(member_slot(anim_obj, *m));
const TypeMember* mTG = find_member(ams, "TrackGroups");
if (!mTG) return true;
auto groups = read_array_of_refs(anim_obj, *mTG);
const auto& tgms = parse_type(mTG->ref_type);
const TypeMember* mTT = find_member(tgms, "TransformTracks");
if (!mTT) return true;
const auto& ttms = parse_type(mTT->ref_type);
if (getenv("GR2_DEBUG")) {
fprintf(stderr, "[anim] transform_track type members=%zu:\n", ttms.size());
for (auto& m : ttms)
fprintf(stderr, "[anim] %-20s type=%d aw=%d off=%u size=%u reftype=(%d,%u)\n",
m.name.c_str(), m.type, m.array_width, m.offset, m.size,
m.ref_type.section, m.ref_type.offset);
}
const TypeMember* mName = find_member(ttms, "Name");
const TypeMember* mOri = find_member(ttms, "OrientationCurve");
const TypeMember* mPos = find_member(ttms, "PositionCurve");
const TypeMember* mScl = find_member(ttms, "ScaleShearCurve");
for (Ref g : groups) {
if (!g.valid()) continue;
Ref base; uint32_t count = 0, stride = 0;
if (!read_ref_to_array(g, *mTT, base, count, stride) || count == 0) continue;
for (uint32_t i = 0; i < count; ++i) {
Ref t = {base.section, base.offset + i * stride};
BoneTrack bt;
if (mName) bt.bone_name = rd_str(member_slot(t, *mName));
if (mPos) {
bt.pos_type = classify_curve_obj(member_slot(t, *mPos), mPos->ref_type);
read_old_curve(member_slot(t, *mPos), mPos->ref_type, bt.position);
}
if (mOri) {
bt.rot_type = classify_curve_obj(member_slot(t, *mOri), mOri->ref_type);
read_old_curve(member_slot(t, *mOri), mOri->ref_type, bt.orientation);
}
if (mScl) {
bt.scale_type = classify_curve_obj(member_slot(t, *mScl), mScl->ref_type);
read_old_curve(member_slot(t, *mScl), mScl->ref_type, bt.scale_shear);
}
if (skel) for (size_t k = 0; k < skel->bones.size(); ++k)
if (skel->bones[k].name == bt.bone_name) { bt.bone_index = int32_t(k); break; }
out.tracks.push_back(std::move(bt));
}
}
return true;
}
// ── T7b:曲线解码 ─────────────────────────────────────────────────────
// Metin2 曲线全是 OldCurveTypeB 样条:Degree + Knots[] + Controls[knot*dim])。
// 求值照 granny_bspline.cpp SampleBSpline{0,1,2}xN + granny_bspline_inlines.h
// {Linear,Quadratic}Coefficients。degree 0=常量,1=线性,2=二次 B 样条。
// KnotIndex = "第一个 > t 的 knot"granny CurveFindKnot),窗口越界按端点 clamp。
//
// dim==4(四元数):blend 之前对本 span 参与的 degree+1 个控制点做
// EnsureQuaternionContinuitygranny_math.cpp)—— 逐点若与前一点点积 < 0 则整体取反。
// 少了这一步,跨半球(q vs -q)的控制点线性混合会收缩到近零,归一后得到一个
// 合法但方向错约 180° 的旋转(表情动作里脖子/头会折叠塌陷)。
void Curve::eval(float t, float* out) const {
const uint32_t n = (uint32_t)knots.size();
if (n == 0 || dim == 0) return;
auto Kc = [&](int i) -> float {
i = i < 0 ? 0 : (i >= (int)n ? (int)n - 1 : i);
return knots[(uint32_t)i];
};
if (n == 1 || degree <= 0) { // 常量
for (uint32_t d = 0; d < dim; ++d) out[d] = controls[d];
return;
}
if (t <= knots[0]) t = knots[0];
if (t >= knots[n - 1]) t = knots[n - 1];
int i = (int)n - 1;
for (uint32_t k = 0; k < n; ++k) if (knots[k] > t) { i = (int)k; break; }
if (i < 1) i = 1;
const int span = (degree == 1) ? 2 : 3; // 本 span 的控制点数 = degree+1
const int first = (degree == 1) ? (i - 1) : (i - 2);
// 拷本 span 的控制点(越界按端点 clamp),四元数做半球连续化
float cp[3 * 4]; // 最多 3 点 × dim4
for (int s = 0; s < span; ++s) {
int ci = first + s;
ci = ci < 0 ? 0 : (ci >= (int)n ? (int)n - 1 : ci);
for (uint32_t d = 0; d < dim; ++d) cp[s * dim + d] = controls[(uint32_t)ci * dim + d];
}
if (dim == 4) {
for (int s = 1; s < span; ++s) {
const float* p = &cp[(s - 1) * 4];
float* q = &cp[s * 4];
if (p[0]*q[0] + p[1]*q[1] + p[2]*q[2] + p[3]*q[3] < 0.0f)
for (int d = 0; d < 4; ++d) q[d] = -q[d];
}
}
if (degree == 1) {
float denom = Kc(i) - Kc(i - 1);
float c0 = denom != 0.0f ? (t - Kc(i - 1)) / denom : 0.0f;
c0 = c0 < 0 ? 0 : (c0 > 1 ? 1 : c0);
float c1 = 1.0f - c0;
for (uint32_t d = 0; d < dim; ++d)
out[d] = c1 * cp[0 * dim + d] + c0 * cp[1 * dim + d];
} else { // degree >= 2 → 二次(Metin2 无 degree 3
float ti_2 = Kc(i - 2), ti_1 = Kc(i - 1), ti = Kc(i), ti1 = Kc(i + 1);
auto fdiv = [](float a, float b) { return b != 0.0f ? a / b : 0.0f; };
float L0 = fdiv(t - ti_1, ti - ti_1);
float L1_1 = fdiv(t - ti_2, ti - ti_2);
float L1_2 = fdiv(t - ti_1, ti1 - ti_1);
float tmp = (L1_1 + L0) - L0 * L1_1;
float c0 = L0 * L1_2; // ci
float c1 = tmp - c0; // ci_1
float c2 = 1.0f - tmp; // ci_2
for (uint32_t d = 0; d < dim; ++d)
out[d] = c2 * cp[0 * dim + d] + c1 * cp[1 * dim + d] + c0 * cp[2 * dim + d];
}
if (dim == 4) { // 四元数:归一
float m = std::sqrt(out[0]*out[0] + out[1]*out[1] + out[2]*out[2] + out[3]*out[3]);
if (m > 1e-12f) for (int d = 0; d < 4; ++d) out[d] /= m;
}
}
// t 处每个 track 的局部 4x4BuildCompositeTransform4x4 语义:C 上 3x3 = transpose(R3*SS3)
// 平移在 elem 12..14)。position 缺省 (0,0,0)orientation 缺省单位四元数,scaleshear 缺省 I。
void Animation::sample_local(float t, std::vector<Mat4>& out) const {
out.assign(tracks.size(), Mat4{1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1});
for (size_t ti = 0; ti < tracks.size(); ++ti) {
const BoneTrack& tr = tracks[ti];
float pos[3] = {0, 0, 0};
float ori[4] = {0, 0, 0, 1};
float ss[9] = {1,0,0, 0,1,0, 0,0,1};
if (!tr.position.empty()) tr.position.eval(t, pos);
if (!tr.orientation.empty()) tr.orientation.eval(t, ori);
bool have_ss = false;
if (!tr.scale_shear.empty()) {
if (tr.scale_shear.dim == 9) { tr.scale_shear.eval(t, ss); have_ss = true; }
else if (tr.scale_shear.dim == 3) {
float s[3]; tr.scale_shear.eval(t, s);
ss[0] = s[0]; ss[4] = s[1]; ss[8] = s[2]; have_ss = true;
}
}
// R3 (行主序) from quat
const float xx = ori[0]*ori[0], yy = ori[1]*ori[1], zz = ori[2]*ori[2];
const float xy = ori[0]*ori[1], xz = ori[0]*ori[2], yz = ori[1]*ori[2];
const float wx = ori[3]*ori[0], wy = ori[3]*ori[1], wz = ori[3]*ori[2];
float r3[9] = {
1 - 2*(yy+zz), 2*(xy-wz), 2*(xz+wy),
2*(xy+wz), 1 - 2*(xx+zz), 2*(yz-wx),
2*(xz-wy), 2*(yz+wx), 1 - 2*(xx+yy)
};
float u3[9];
if (have_ss) {
for (int r = 0; r < 3; ++r)
for (int c = 0; c < 3; ++c)
u3[r*3+c] = r3[r*3+0]*ss[0*3+c] + r3[r*3+1]*ss[1*3+c] + r3[r*3+2]*ss[2*3+c];
} else {
for (int k = 0; k < 9; ++k) u3[k] = r3[k];
}
Mat4& m = out[ti];
m = Mat4{};
m[0] = u3[0]; m[1] = u3[3]; m[2] = u3[6]; m[3] = 0;
m[4] = u3[1]; m[5] = u3[4]; m[6] = u3[7]; m[7] = 0;
m[8] = u3[2]; m[9] = u3[5]; m[10] = u3[8]; m[11] = 0;
m[12] = pos[0]; m[13] = pos[1]; m[14] = pos[2]; m[15] = 1;
}
}
} // namespace gr2
+58
View File
@@ -0,0 +1,58 @@
// M0 T2 —— section 解压分派。见 docs/steps/M0-gr2-reader.md
#include "internal.h"
#include "oodle1.h"
#include <cstring>
namespace gr2 {
// src 指向文件里该 section 的压缩字节(长度 s.data_size)。
// 本函数会做一份可写的、尾部留 32B 零的副本(Oodle1 解码器会写它、读过界)。
bool File::Impl::decompress_section(const detail::GrnSection& s, const uint8_t* src,
detail::Section& out, LoadError* err) {
out.info = {s.compression, s.data_size, s.expanded_data_size, s.pointer_fixup_count, 0};
if (s.expanded_data_size == 0) { // 空 section
out.data.clear();
return true;
}
switch (s.compression) {
case 0: { // NoCompression
out.data.assign(src, src + s.data_size);
out.data.resize(s.expanded_data_size);
return true;
}
case 1: // Oodle0 —— 同一族解码器的变体,暂未实现(T2 后续;样本里极少)
out.info.decompress_status = 1;
if (err) *err = {"section", "Oodle0 not implemented (M0 T2)"};
return false;
case 2: { // Oodle1
// 4 字节对齐的可写副本 + 32B 零尾
std::vector<uint8_t> buf;
buf.reserve(s.data_size + 32);
buf.assign(src, src + s.data_size);
buf.resize(s.data_size + 32, 0);
out.data.assign(s.expanded_data_size, 0);
int rc = gr2_oodle1_decompress(/*byte_reversed=*/0,
s.data_size, buf.data(),
s.first16bit, s.first8bit, s.expanded_data_size,
out.data.data());
out.info.decompress_status = rc;
if (rc != 0) {
if (err) *err = {"section", "Oodle1 decompress failed"};
return false;
}
return true;
}
case 4: // BitKnit —— 无公开实现(全样本 0 个)
out.info.decompress_status = 4;
if (err) *err = {"section", "BitKnit unsupported (M0 T2)"};
return false;
default:
out.info.decompress_status = -1;
if (err) *err = {"section", "unknown compression format"};
return false;
}
}
} // namespace gr2
+205
View File
@@ -0,0 +1,205 @@
// 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
+119
View File
@@ -0,0 +1,119 @@
// M0 T4 —— FileInfo 汇总(驱动 T5 骨架 / T6 网格 / T7a 曲线分类)。
// 见 docs/steps/M0-gr2-reader.md
// 规格:granny_file_info.cpp (FileInfoType) / granny_animation.cpp (AnimationType)
#include "internal.h"
#include <cstdio>
#include <cstdlib>
namespace gr2 {
using detail::Ref;
using detail::TypeMember;
bool File::Impl::build_fileinfo(FileInfo& out, LoadError* err) {
if (!root_obj.valid() || !root_type.valid()) {
if (err) *err = {"fileinfo", "no root object / type"};
return false;
}
const auto& rms = parse_type(root_type);
if (rms.empty()) {
if (err) *err = {"fileinfo", "root type has no members"};
return false;
}
auto count_of = [&](const char* name) -> uint32_t {
const TypeMember* m = find_member(rms, name);
if (!m) return 0;
int32_t c = rd_i32(member_slot(root_obj, *m));
return (c > 0 && c < (1 << 24)) ? uint32_t(c) : 0;
};
if (const TypeMember* m = find_member(rms, "FromFileName"))
out.from_file_name = rd_str(member_slot(root_obj, *m));
out.texture_count = count_of("Textures");
out.material_count = count_of("Materials");
out.model_count = count_of("Models");
// ── 顶层材质表(调试 / 备用查找)────────────────────────────────────
if (const TypeMember* mMa = find_member(rms, "Materials")) {
auto refs = read_array_of_refs(root_obj, *mMa);
const auto& mams = parse_type(mMa->ref_type);
const TypeMember* mMN = find_member(mams, "Name");
for (Ref r : refs) {
if (!r.valid()) continue;
Material mat;
if (mMN) mat.name = rd_str(member_slot(r, *mMN));
mat.diffuse_texture = material_texture_name(r, mMa->ref_type, 0);
out.materials.push_back(std::move(mat));
}
}
// ── 骨架(T5)────────────────────────────────────────────────────
skels_cache.clear();
skel_refs.clear();
if (const TypeMember* mSk = find_member(rms, "Skeletons")) {
auto refs = read_array_of_refs(root_obj, *mSk);
for (Ref r : refs) {
Skeleton sk;
if (extract_skeleton(r, mSk->ref_type, sk)) {
skels_cache.push_back(std::move(sk));
skel_refs.push_back(r);
}
}
}
// ── 模型 → 骨架关联:把 model.InitialPlacement 写进对应 skeleton ─────
// (很多 Metin2 骨架把 model 空间偏移放在 InitialPlacement 而非 root 骨骼的
// LocalTransform;不带上它,bind pose 自洽检查会对这些文件失败)
if (const TypeMember* mMo = find_member(rms, "Models")) {
auto refs = read_array_of_refs(root_obj, *mMo);
const auto& moms = parse_type(mMo->ref_type);
const TypeMember* mSkRef = find_member(moms, "Skeleton");
const TypeMember* mIP = find_member(moms, "InitialPlacement");
for (Ref r : refs) {
if (!r.valid() || !mSkRef) continue;
Ref sref = read_reference(r, *mSkRef);
for (size_t k = 0; k < skel_refs.size(); ++k) {
if (skel_refs[k].section == sref.section && skel_refs[k].offset == sref.offset) {
if (mIP) {
uint32_t fl = 0;
transform_to_composite(member_slot(r, *mIP), skels_cache[k].initial_placement, fl);
}
break;
}
}
}
}
out.skeletons = skels_cache;
// ── 网格(T6)────────────────────────────────────────────────────
if (const TypeMember* mMe = find_member(rms, "Meshes")) {
auto refs = read_array_of_refs(root_obj, *mMe);
for (Ref r : refs) {
Mesh me;
bool ok = extract_mesh(r, mMe->ref_type, skels_cache, me);
if (getenv("GR2_DEBUG"))
fprintf(stderr, "[gr2] mesh ref (%d,%u) -> %s name=%s verts=%zu idx=%zu\n",
r.section, r.offset, ok ? "OK" : "FAIL", me.name.c_str(),
me.vertices.size(), me.indices.size());
if (ok) out.meshes.push_back(std::move(me));
}
}
// ── 动画(T7a:名字 + 时长 + 每 track 的曲线子类型)─────────────────
if (const TypeMember* mAn = find_member(rms, "Animations")) {
auto refs = read_array_of_refs(root_obj, *mAn);
const Skeleton* sk0 = skels_cache.empty() ? nullptr : &skels_cache[0];
for (Ref r : refs) {
if (!r.valid()) continue;
Animation an;
if (extract_animation(r, mAn->ref_type, sk0, an))
out.animations.push_back(std::move(an));
}
}
return true;
}
} // namespace gr2
+123
View File
@@ -0,0 +1,123 @@
// 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
+300
View File
@@ -0,0 +1,300 @@
// M0 T6 —— 顶点 / 索引 / 三角组 / BoneBindings。见 docs/steps/M0-gr2-reader.md
// 规格:granny_mesh.cpp (MeshType/BoneBindingType) / granny_vertex_data.cpp (VertexDataType
// + 各顶点格式) / granny_tri_topology.cpp (TriTopologyType/TriMaterialGroupType)
// 顶点格式是自描述的(Vertices 是 ReferenceToVariantArray,带 data_type_definition*),
// 按成员名(Position/Normal/TextureCoordinates0/1/BoneWeights/BoneIndices)取分量,不硬编码布局。
#include "internal.h"
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#define MDBG(...) do { if (getenv("GR2_DEBUG")) fprintf(stderr, "[mesh] " __VA_ARGS__); } while (0)
namespace gr2 {
using detail::Ref;
using detail::TypeMember;
namespace {
// member_type → (元素是浮点?, 元素字节数, 是否归一化整型)
struct Elem { bool is_float; int bytes; bool normalized; bool is_signed; };
Elem elem_of(int mt) {
switch (mt) {
case detail::MT_Real32: return {true, 4, false, true};
case detail::MT_Real16: return {false, 2, false, true}; // 半精度,少见,按需扩展
case detail::MT_Int8: return {false, 1, false, true};
case detail::MT_UInt8: return {false, 1, false, false};
case detail::MT_BinormInt8: return {false, 1, true, true};
case detail::MT_NormUInt8: return {false, 1, true, false};
case detail::MT_Int16: return {false, 2, false, true};
case detail::MT_UInt16: return {false, 2, false, false};
case detail::MT_BinormInt16: return {false, 2, true, true};
case detail::MT_NormUInt16: return {false, 2, true, false};
case detail::MT_Int32: return {false, 4, false, true};
case detail::MT_UInt32: return {false, 4, false, false};
default: return {false, 0, false, false};
}
}
float read_scalar(const uint8_t* p, const Elem& e) {
switch (e.bytes) {
case 4:
if (e.is_float) { float v; std::memcpy(&v, p, 4); return v; }
if (e.is_signed) { int32_t v; std::memcpy(&v, p, 4); return float(v); }
{ uint32_t v; std::memcpy(&v, p, 4); return float(v); }
case 2: {
if (e.is_signed) { int16_t v; std::memcpy(&v, p, 2); return e.normalized ? v / 32767.0f : float(v); }
uint16_t v; std::memcpy(&v, p, 2); return e.normalized ? v / 65535.0f : float(v);
}
case 1: {
if (e.is_signed) { int8_t v = (int8_t)p[0]; return e.normalized ? v / 127.0f : float(v); }
return e.normalized ? p[0] / 255.0f : float(p[0]);
}
default: return 0.0f;
}
}
} // namespace
// mesh: {String Name; Reference PrimaryVertexData; RefToArray MorphTargets;
// Reference PrimaryTopology; RefToArray MaterialBindings; RefToArray BoneBindings; ...}
bool File::Impl::extract_mesh(Ref mesh_obj, Ref mesh_type,
const std::vector<Skeleton>& skels, Mesh& out) {
if (!mesh_obj.valid() || !mesh_type.valid()) return false;
const auto& mms = parse_type(mesh_type);
if (getenv("GR2_DEBUG")) {
fprintf(stderr, "[mesh] mesh_type=(%d,%u) members=%zu:\n",
mesh_type.section, mesh_type.offset, mms.size());
for (auto& m : mms)
fprintf(stderr, "[mesh] %-20s type=%d aw=%d off=%u size=%u reftype=(%d,%u)\n",
m.name.c_str(), m.type, m.array_width, m.offset, m.size,
m.ref_type.section, m.ref_type.offset);
}
if (const TypeMember* mName = find_member(mms, "Name"))
out.name = rd_str(member_slot(mesh_obj, *mName));
// ── 顶点 ──────────────────────────────────────────────────────────
const TypeMember* mVD = find_member(mms, "PrimaryVertexData");
if (!mVD) { MDBG("no PrimaryVertexData member\n"); return false; }
MDBG("PrimaryVertexData m.offset=%u ref_type=(%d,%u)\n",
mVD->offset, mVD->ref_type.section, mVD->ref_type.offset);
Ref vd = read_reference(mesh_obj, *mVD);
if (!vd.valid()) { MDBG("PrimaryVertexData ref -> NULL (slot %d,%u)\n",
member_slot(mesh_obj, *mVD).section, member_slot(mesh_obj, *mVD).offset); return false; }
const auto& vdms = parse_type(mVD->ref_type);
const TypeMember* mVerts = find_member(vdms, "Vertices");
if (!mVerts) { MDBG("no Vertices member (vdms=%zu)\n", vdms.size()); return false; }
// ReferenceToVariantArray on-disk (32-bit): {def* Type; int32 Count; void* Ptr}
// (granny_data_type_definition.cpp: Ptr32 + int32 + Ptr32)
Ref vslot = member_slot(vd, *mVerts);
Ref vtype = follow({vslot.section, vslot.offset + 0});
int32_t vcount = rd_i32({vslot.section, vslot.offset + 4});
Ref vdata = follow({vslot.section, vslot.offset + 8});
MDBG("Vertices count=%d vtype=(%d,%u) vdata=(%d,%u)\n",
vcount, vtype.section, vtype.offset, vdata.section, vdata.offset);
if (vcount <= 0 || vcount > (1 << 24) || !vtype.valid() || !vdata.valid()) return false;
const auto& vfmt = parse_type(vtype);
uint32_t stride = 0;
for (const auto& m : vfmt) stride += m.size;
MDBG("vfmt members=%zu stride=%u\n", vfmt.size(), stride);
if (stride == 0) return false;
struct Comp { const TypeMember* m; Elem e; };
auto comp = [&](const char* n) -> Comp {
const TypeMember* m = find_member(vfmt, n);
if (!m) return {nullptr, {}};
return {m, elem_of(m->type)};
};
Comp cPos = comp("Position");
Comp cNrm = comp("Normal");
Comp cUv0 = comp("TextureCoordinates0");
Comp cUv1 = comp("TextureCoordinates1");
Comp cBW = comp("BoneWeights");
Comp cBI = comp("BoneIndices");
const bool skinned = (cBW.m && cBI.m);
out.rigid = !skinned;
if (skinned) {
out.source_bone_weight_slots = uint32_t(cBW.m->array_width > 0 ? cBW.m->array_width : 1);
out.source_bone_index_slots = (cBI.e.bytes == 4 && cBI.m->array_width <= 1)
? 4u
: uint32_t(cBI.m->array_width > 0 ? cBI.m->array_width : 1);
}
if (skinned && cUv1.m) out.kind = VertexKind::PNT3322_Skinned;
else if (skinned) out.kind = VertexKind::PNT332_Skinned;
else if (cUv1.m) out.kind = VertexKind::PNT3322;
else if (cPos.m && cNrm.m) out.kind = VertexKind::PNT332;
else out.kind = VertexKind::Unknown;
if ((uint64_t)vdata.offset + (uint64_t)vcount * stride > sections[vdata.section].data.size())
return false;
out.vertices.resize(vcount);
for (int32_t i = 0; i < vcount; ++i) {
const uint8_t* base = sections[vdata.section].data.data() + vdata.offset + size_t(i) * stride;
Vertex& v = out.vertices[i];
auto rd = [&](const Comp& c, float* dst, int n) {
if (!c.m || !c.e.bytes) return;
int aw = c.m->array_width <= 0 ? 1 : c.m->array_width;
n = n < aw ? n : aw;
for (int k = 0; k < n; ++k)
dst[k] = read_scalar(base + c.m->offset + size_t(k) * c.e.bytes, c.e);
};
rd(cPos, v.pos, 3);
rd(cNrm, v.normal, 3);
rd(cUv0, v.uv0, 2);
rd(cUv1, v.uv1, 2);
if (skinned) {
float w[4] = {0,0,0,0}, bi[4] = {0,0,0,0};
rd(cBW, w, 4);
// BoneIndices 可能是 UInt8[4] 或 UInt32[N]PWN* 系列是单 uint32 packed 4×u8
if (cBI.e.bytes == 1) {
rd(cBI, bi, 4);
} else if (cBI.e.bytes == 4 && cBI.e.is_float == false) {
uint32_t packed;
std::memcpy(&packed, base + cBI.m->offset, 4);
for (int k = 0; k < 4; ++k) bi[k] = float((packed >> (k * 8)) & 0xff);
}
for (int k = 0; k < 4; ++k) {
v.bone_index[k] = uint8_t(std::lround(bi[k]));
float wn = cBW.e.normalized ? w[k] : w[k]; // 已在 read_scalar 归一
v.bone_weight[k] = uint8_t(std::lround(std::fmin(std::fmax(wn, 0.0f), 1.0f) * 255.0f));
}
}
}
// ── 索引 + 三角组 ────────────────────────────────────────────────
const TypeMember* mTopo = find_member(mms, "PrimaryTopology");
if (mTopo) {
Ref tt = read_reference(mesh_obj, *mTopo);
if (tt.valid()) {
const auto& tms = parse_type(mTopo->ref_type);
if (const TypeMember* mG = find_member(tms, "Groups")) {
Ref gb; uint32_t gc = 0, gs = 0;
if (read_ref_to_array(tt, *mG, gb, gc, gs) && gc) {
const auto& gmT = parse_type(mG->ref_type);
const TypeMember* mMI = find_member(gmT, "MaterialIndex");
const TypeMember* mTF = find_member(gmT, "TriFirst");
const TypeMember* mTC = find_member(gmT, "TriCount");
for (uint32_t i = 0; i < gc; ++i) {
Ref g = {gb.section, gb.offset + i * gs};
TriGroup tg{};
if (mMI) tg.material_index = rd_i32(member_slot(g, *mMI));
if (mTF) tg.tri_first = rd_i32(member_slot(g, *mTF));
if (mTC) tg.tri_count = rd_i32(member_slot(g, *mTC));
out.tri_groups.push_back(tg);
}
}
}
auto load_idx = [&](const char* name, int elem_bytes) -> bool {
const TypeMember* mI = find_member(tms, name);
if (!mI) return false;
Ref ib; uint32_t ic = 0, is = 0;
if (!read_ref_to_array(tt, *mI, ib, ic, is) || ic == 0) return false;
const uint8_t* p = at(ib, size_t(ic) * elem_bytes);
if (!p) return false;
out.indices.resize(ic);
for (uint32_t i = 0; i < ic; ++i) {
if (elem_bytes == 2) { uint16_t v; std::memcpy(&v, p + i*2, 2); out.indices[i] = v; }
else { uint32_t v; std::memcpy(&v, p + i*4, 4); out.indices[i] = v; }
}
return true;
};
if (!load_idx("Indices", 4)) load_idx("Indices16", 2);
}
}
// ── MaterialBindings:与 tri_group.material_index 平行的贴图名表 ───
// granny_mesh.MaterialBindings = RefToArray of {Reference Material}。
if (const TypeMember* mMB = find_member(mms, "MaterialBindings")) {
Ref mb; uint32_t mc = 0, ms = 0;
if (read_ref_to_array(mesh_obj, *mMB, mb, mc, ms) && mc) {
const auto& bT = parse_type(mMB->ref_type);
const TypeMember* mMat = find_member(bT, "Material");
out.material_textures.reserve(mc);
for (uint32_t i = 0; i < mc; ++i) {
Ref e = {mb.section, mb.offset + i * ms};
std::string tn;
if (mMat) {
Ref mat = read_reference(e, *mMat);
tn = material_texture_name(mat, mMat->ref_type, 0);
}
out.material_textures.push_back(std::move(tn));
}
}
}
// ── BoneBindingsmesh 局部骨骼槽 → skeleton 骨骼索引 ─────────────
// 文件可能带多个 skeleton(本体 + 武器挂点等)。对每个 skeleton 试解析,
// 取"悬空最少"的那个。
if (const TypeMember* mBB = find_member(mms, "BoneBindings")) {
Ref bb; uint32_t bc = 0, bs = 0;
if (read_ref_to_array(mesh_obj, *mBB, bb, bc, bs) && bc) {
const auto& bbT = parse_type(mBB->ref_type);
const TypeMember* mBN = find_member(bbT, "BoneName");
std::vector<std::string> names(bc);
for (uint32_t i = 0; i < bc; ++i) {
Ref e = {bb.section, bb.offset + i * bs};
names[i] = mBN ? rd_str(member_slot(e, *mBN)) : std::string();
}
std::vector<int32_t> best(bc, -1);
int best_dangling = int(bc) + 1;
for (const auto& sk : skels) {
std::vector<int32_t> cur(bc, -1);
int dangling = 0;
for (uint32_t i = 0; i < bc; ++i) {
for (size_t k = 0; k < sk.bones.size(); ++k)
if (sk.bones[k].name == names[i]) { cur[i] = int32_t(k); break; }
if (cur[i] < 0) ++dangling;
}
if (dangling < best_dangling) { best_dangling = dangling; best = cur; }
if (dangling == 0) break;
}
out.bone_bindings = std::move(best);
}
}
return true;
}
// granny_material {String Name; int MapCount; material_map* Maps; texture* Texture; ...}
// granny_material_map {String Usage; material* Material}
// granny_texture {String FromFileName; ...}
std::string File::Impl::material_texture_name(Ref mat_obj, Ref mat_type, int depth) {
if (!mat_obj.valid() || !mat_type.valid() || depth > 8) return {};
const auto& mms = parse_type(mat_type);
// 1) 直接挂 texture
if (const TypeMember* mTex = find_member(mms, "Texture")) {
Ref tex = read_reference(mat_obj, *mTex);
if (tex.valid()) {
const auto& tms = parse_type(mTex->ref_type);
if (const TypeMember* mFN = find_member(tms, "FromFileName")) {
std::string s = rd_str(member_slot(tex, *mFN));
if (!s.empty()) return s;
}
}
}
// 2) 递归 Maps[].Map(复合材质:granny_material_map = {String Usage; material* Map}
if (const TypeMember* mMaps = find_member(mms, "Maps")) {
Ref b; uint32_t c = 0, s = 0;
if (read_ref_to_array(mat_obj, *mMaps, b, c, s) && c) {
const auto& mapT = parse_type(mMaps->ref_type);
const TypeMember* mSub = find_member(mapT, "Map");
if (!mSub) mSub = find_member(mapT, "Material");
for (uint32_t i = 0; i < c && mSub; ++i) {
Ref e = {b.section, b.offset + i * s};
Ref sub = read_reference(e, *mSub);
std::string r = material_texture_name(sub, mSub->ref_type, depth + 1);
if (!r.empty()) return r;
}
}
}
return {};
}
} // namespace gr2
+225
View File
@@ -0,0 +1,225 @@
// M0 T5 —— 骨架提取 + bind pose 自洽检查。见 docs/steps/M0-gr2-reader.md
// 规格:granny_transform.cpp BuildCompositeTransform4x4 / granny_math.cpp
// MatrixEqualsQuaternion3x3 + MatrixMultiply3x3 /
// granny_matrix_operations.cpp ColumnMatrixMultiply4x3Impl /
// granny_world_pose.cpp BuildWorldPoseNoCompositeLOD + granny_bone_operations.cpp
// BuildFullWorldPoseOnly_Generic
#include "internal.h"
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
namespace gr2 {
using detail::Ref;
using detail::TypeMember;
namespace {
// 行主序 4x4R = A * BColumnMatrixMultiply4x3Impl 语义,平移在第 3 行 elem 12..14
Mat4 mul4x3(const Mat4& A, const Mat4& B) {
Mat4 R{};
for (int i = 0; i < 3; ++i) {
for (int k = 0; k < 3; ++k)
R[i * 4 + k] = A[i * 4 + 0] * B[0 * 4 + k]
+ A[i * 4 + 1] * B[1 * 4 + k]
+ A[i * 4 + 2] * B[2 * 4 + k];
R[i * 4 + 3] = 0.0f;
}
for (int k = 0; k < 3; ++k)
R[12 + k] = A[12] * B[0 * 4 + k]
+ A[13] * B[1 * 4 + k]
+ A[14] * B[2 * 4 + k]
+ B[12 + k];
R[15] = 1.0f;
return R;
}
// granny_math.cpp MatrixEqualsQuaternion3x3(行主序 3x3
void quat_to_m3(const float q[4], float d[9]) {
const float xx = q[0] * q[0], yy = q[1] * q[1], zz = q[2] * q[2];
const float xy = q[0] * q[1], xz = q[0] * q[2], yz = q[1] * q[2];
const float wx = q[3] * q[0], wy = q[3] * q[1], wz = q[3] * q[2];
d[0] = 1 - 2 * (yy + zz); d[1] = 2 * (xy - wz); d[2] = 2 * (xz + wy);
d[3] = 2 * (xy + wz); d[4] = 1 - 2 * (xx + zz); d[5] = 2 * (yz - wx);
d[6] = 2 * (xz - wy); d[7] = 2 * (yz + wx); d[8] = 1 - 2 * (xx + yy);
}
void m3_mul(const float a[9], const float b[9], float r[9]) {
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j)
r[i * 3 + j] = a[i * 3 + 0] * b[0 * 3 + j]
+ a[i * 3 + 1] * b[1 * 3 + j]
+ a[i * 3 + 2] * b[2 * 3 + j];
}
} // namespace
// granny_transform (68B: u32 Flags; f32 Position[3]; f32 Orientation[4]; f32 ScaleShear[3][3])
// → 行主序 4x4,上 3x3 = transpose(U3),平移在 elem 12..14。BuildCompositeTransform4x4 语义。
void File::Impl::transform_to_composite(Ref tf, Mat4& out, uint32_t& flags) const {
const uint8_t* p = at(tf, 68);
out = Mat4{};
out[0] = out[5] = out[10] = out[15] = 1.0f;
flags = 0;
if (!p) return;
auto f32 = [&](size_t off) { float v; std::memcpy(&v, p + off, 4); return v; };
flags = rd_u32(tf);
float pos[3] = { f32(4), f32(8), f32(12) };
float ori[4] = { f32(16), f32(20), f32(24), f32(28) };
float ss[9];
for (int i = 0; i < 9; ++i) ss[i] = f32(32 + size_t(i) * 4);
float q3[9];
quat_to_m3(ori, q3);
float u3[9];
if (flags & 0x4 /* HasScaleShear */) m3_mul(q3, ss, u3);
else std::memcpy(u3, q3, sizeof u3);
// C 的上 3x3 = transpose(U3)
out[0] = u3[0]; out[1] = u3[3]; out[2] = u3[6]; out[3] = 0;
out[4] = u3[1]; out[5] = u3[4]; out[6] = u3[7]; out[7] = 0;
out[8] = u3[2]; out[9] = u3[5]; out[10] = u3[8]; out[11] = 0;
out[12] = pos[0]; out[13] = pos[1]; out[14] = pos[2]; out[15] = 1;
}
// skeleton: {String Name; RefToArray Bones(bone[]); int32 LODType; VariantRef ExtendedData}
// bone: {String Name; int32 ParentIndex; Transform LocalTransform;
// Real32[16] InverseWorldTransform; Real32 LODError; VariantRef ExtendedData}
// 全程按类型树取偏移,不硬编码。
bool File::Impl::extract_skeleton(Ref skel_obj, Ref skel_type, Skeleton& out) {
out.bones.clear();
if (!skel_obj.valid() || !skel_type.valid()) return false;
const auto& sms = parse_type(skel_type);
const TypeMember* mBones = find_member(sms, "Bones");
if (!mBones) return false;
Ref bones_base;
uint32_t bone_count = 0, stride = 0;
if (!read_ref_to_array(skel_obj, *mBones, bones_base, bone_count, stride)) return false;
if (bone_count == 0) return false;
if (bone_count > 8192 || stride == 0) return false;
const auto& bms = parse_type(mBones->ref_type);
const TypeMember* mName = find_member(bms, "Name");
const TypeMember* mParent = find_member(bms, "ParentIndex");
const TypeMember* mLocal = find_member(bms, "LocalTransform");
if (!mLocal) mLocal = find_member(bms, "Transform");
const TypeMember* mInvW = find_member(bms, "InverseWorldTransform");
if (!mInvW) mInvW = find_member(bms, "InverseWorld4x4");
if (!mLocal || !mInvW) return false;
out.bones.resize(bone_count);
for (uint32_t i = 0; i < bone_count; ++i) {
Ref b = { bones_base.section, bones_base.offset + i * stride };
Bone& bo = out.bones[i];
if (mName) bo.name = rd_str(member_slot(b, *mName));
if (mParent) bo.parent = rd_i32(member_slot(b, *mParent));
uint32_t flags = 0;
transform_to_composite(member_slot(b, *mLocal), bo.local_transform, flags);
bo.lt_flags = flags;
const uint8_t* iw = at(member_slot(b, *mInvW), 64);
if (iw) for (int k = 0; k < 16; ++k) std::memcpy(&bo.inverse_world[k], iw + k * 4, 4);
else bo.inverse_world = Mat4{};
}
return true;
}
// ── M2:姿势采样(自由函数 = 主 primitive;File::* 是便捷封装)──────────
namespace {
void accumulate_world(const Skeleton& sk, const std::vector<Mat4>& local,
std::vector<Mat4>& world, std::vector<Mat4>& skin) {
const size_t n = sk.bones.size();
const Mat4 I{1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1};
world.assign(n, I);
skin.assign(n, I);
for (size_t i = 0; i < n; ++i) {
int32_t par = sk.bones[i].parent;
const Mat4& parent_w = (par < 0 || (size_t)par >= i) ? sk.initial_placement : world[par];
world[i] = mul4x3((i < local.size()) ? local[i] : sk.bones[i].local_transform, parent_w);
skin[i] = mul4x3(sk.bones[i].inverse_world, world[i]);
}
}
} // namespace
void bind_pose(const Skeleton& sk, std::vector<Mat4>& world, std::vector<Mat4>& skin) {
std::vector<Mat4> local(sk.bones.size());
for (size_t i = 0; i < sk.bones.size(); ++i) local[i] = sk.bones[i].local_transform;
accumulate_world(sk, local, world, skin);
}
void sample_pose(const Skeleton& sk, const Animation& an, float t,
std::vector<Mat4>& world, std::vector<Mat4>& skin) {
std::vector<Mat4> track_local;
an.sample_local(t, track_local);
std::vector<Mat4> local(sk.bones.size());
for (size_t i = 0; i < sk.bones.size(); ++i) local[i] = sk.bones[i].local_transform; // 默认 = bind
// track 按骨骼名匹配(bone_index 若已在 anim 提取时对某个 skeleton 映射过,可能不是这个 sk)
for (size_t ti = 0; ti < an.tracks.size() && ti < track_local.size(); ++ti) {
const BoneTrack& tr = an.tracks[ti];
int bi = -1;
for (size_t k = 0; k < sk.bones.size(); ++k)
if (sk.bones[k].name == tr.bone_name) { bi = int(k); break; }
if (bi >= 0) local[bi] = track_local[ti];
}
accumulate_world(sk, local, world, skin);
}
bool File::bind_pose(int skeleton, std::vector<Mat4>& world, std::vector<Mat4>& skin) const {
if (!impl_ || skeleton < 0 || (size_t)skeleton >= impl_->skels_cache.size()) return false;
gr2::bind_pose(impl_->skels_cache[skeleton], world, skin);
return true;
}
bool File::sample_pose(int anim, float t, int skeleton,
std::vector<Mat4>& world, std::vector<Mat4>& skin) const {
if (!impl_ || skeleton < 0 || (size_t)skeleton >= impl_->skels_cache.size()) return false;
if (anim < 0 || (size_t)anim >= info_.animations.size())
return bind_pose(skeleton, world, skin);
gr2::sample_pose(impl_->skels_cache[skeleton], info_.animations[anim], t, world, skin);
return true;
}
double File::Impl::skeleton_self_check(int skeleton) const {
if (skeleton < 0 || (size_t)skeleton >= skels_cache.size()) return 1e9;
const auto& sk = skels_cache[skeleton];
const size_t n = sk.bones.size();
if (n == 0) return 1e9;
std::vector<Mat4> world(n);
double dev = 0.0;
size_t worst = 0;
for (size_t i = 0; i < n; ++i) {
int32_t par = sk.bones[i].parent;
if (par < 0 || (size_t)par >= i) {
// 根:world = local * InitialPlacementBuildWorldPose 的 Offset4x4
world[i] = mul4x3(sk.bones[i].local_transform, sk.initial_placement);
} else {
world[i] = mul4x3(sk.bones[i].local_transform, world[par]);
}
// E = InverseWorld * world ≈ I
Mat4 E = mul4x3(sk.bones[i].inverse_world, world[i]);
double bd = 0.0;
for (int k = 0; k < 16; ++k) {
float want = (k % 5 == 0) ? 1.0f : 0.0f;
bd = std::max(bd, (double)std::fabs(E[k] - want));
}
if (bd > dev) { dev = bd; worst = i; }
}
if (getenv("GR2_DEBUG")) {
fprintf(stderr, "[skel] self-check dev=%.5f worst bone %zu '%s' parent=%d\n",
dev, worst, sk.bones[worst].name.c_str(), sk.bones[worst].parent);
const Mat4& E = mul4x3(sk.bones[worst].inverse_world, world[worst]);
fprintf(stderr, "[skel] E[worst]:");
for (float x : E) fprintf(stderr, " %.3f", x);
fprintf(stderr, "\n");
}
return dev;
}
} // namespace gr2
+204
View File
@@ -0,0 +1,204 @@
// M0 T1fixup 索引)+ T3(自描述类型树遍历)。见 docs/steps/M0-gr2-reader.md
// 规格:granny_file_format.hfixup+ granny_data_type_definition.h/.cpp(成员类型 + stride
#include "internal.h"
#include <cstring>
namespace gr2 {
using detail::Ref;
using detail::TypeMember;
using detail::kTypeDefEntrySize;
// ── member_type → 文件里的单元字节数(32 位指针)──────────────────────
// 序数见 internal.h MemberType。InlineMember 需递归(这里在 member_unit_size 处理)。
static const int8_t kUnit32[detail::MT_OnePastLast] = {
/* End */ 0,
/* Inline */ 0, /* 特判 */
/* Reference */ 4,
/* RefToArray */ 8, /* int32 + ptr */
/* ArrayOfRefs */ 8,
/* VariantRef */ 8, /* ptr + ptr */
/* Removed */ 0,
/* RefToVariantArr*/ 12,
/* String */ 4,
/* Transform */ 68, /* u32 flags + f32[3] + f32[4] + f32[3][3] */
/* Real32 */ 4,
/* Int8 */ 1,
/* UInt8 */ 1,
/* BinormInt8 */ 1,
/* NormUInt8 */ 1,
/* Int16 */ 2,
/* UInt16 */ 2,
/* BinormInt16 */ 2,
/* NormUInt16 */ 2,
/* Int32 */ 4,
/* UInt32 */ 4,
/* Real16 */ 2,
/* EmptyRef */ 4,
};
// ── section 内指针 + 边界检查 ────────────────────────────────────────
const uint8_t* File::Impl::at(Ref r, size_t need) const {
if (r.section < 0 || (size_t)r.section >= sections.size()) return nullptr;
const auto& d = sections[r.section].data;
if ((size_t)r.offset + need > d.size()) return nullptr;
return d.data() + r.offset;
}
uint32_t File::Impl::rd_u32(Ref r) const {
const uint8_t* p = at(r, 4);
if (!p) return 0;
return uint32_t(p[0]) | (uint32_t(p[1]) << 8) | (uint32_t(p[2]) << 16) | (uint32_t(p[3]) << 24);
}
float File::Impl::rd_f32(Ref r) const {
uint32_t u = rd_u32(r);
float f;
std::memcpy(&f, &u, 4);
return f;
}
// 解一个"指针槽":查 fixup 表;查不到 => NULL(返回 invalid Ref
Ref File::Impl::follow(Ref slot) const {
if (slot.section < 0) return {};
auto it = fixups.find((uint64_t(uint32_t(slot.section)) << 32) | slot.offset);
if (it == fixups.end()) return {};
return it->second;
}
std::string File::Impl::rd_str(Ref slot) const {
Ref s = follow(slot);
const uint8_t* p = at(s, 1);
if (!p) return {};
const auto& d = sections[s.section].data;
size_t maxn = d.size() - s.offset;
size_t n = 0;
while (n < maxn && p[n]) ++n;
return std::string((const char*)p, n);
}
// ── T1:建 fixup 索引 ────────────────────────────────────────────────
// grn_pointer_fixup 数组在文件里(PointerFixupArrayOffset 从文件起点算,Count 项,每 12B)。
// 每项 { u32 FromOffset(本 section 展开数据内偏移); u32 DestSection; u32 DestOffset }。
void File::Impl::build_fixups(const uint8_t* file, size_t file_len) {
auto le32 = [&](size_t off) -> uint32_t {
if (off + 4 > file_len) return 0;
return uint32_t(file[off]) | (uint32_t(file[off + 1]) << 8) |
(uint32_t(file[off + 2]) << 16) | (uint32_t(file[off + 3]) << 24);
};
fixups.clear();
for (size_t si = 0; si < raw.size(); ++si) {
const GrnSection& gs = raw[si];
size_t base = gs.pointer_fixup_off;
for (uint32_t k = 0; k < gs.pointer_fixup_count; ++k) {
size_t p = base + size_t(k) * 12;
if (p + 12 > file_len) break;
uint32_t from = le32(p + 0);
uint32_t dsec = le32(p + 4);
uint32_t doff = le32(p + 8);
if (dsec >= sections.size()) continue;
fixups[(uint64_t(uint32_t(si)) << 32) | from] =
Ref{int32_t(dsec), doff};
}
}
}
// ── T3:走 data_type_definition 数组 ─────────────────────────────────
uint32_t File::Impl::member_unit_size(const TypeMember& m) {
if (m.type == detail::MT_Inline) return object_size(m.ref_type);
if (m.type <= 0 || m.type >= detail::MT_OnePastLast) return 0;
return (uint32_t)kUnit32[m.type];
}
uint32_t File::Impl::object_size(Ref type_ref) {
const auto& ms = parse_type(type_ref);
uint32_t sz = 0;
for (const auto& m : ms) sz += m.size;
return sz;
}
const std::vector<TypeMember>& File::Impl::parse_type(Ref type_ref) {
uint64_t key = (uint64_t(uint32_t(type_ref.section)) << 32) | type_ref.offset;
auto it = type_cache.find(key);
if (it != type_cache.end()) return it->second;
std::vector<TypeMember> out;
// 先占位(防递归 Inline 死循环)
auto& slot = type_cache[key];
Ref cur = type_ref;
uint32_t inst_off = 0;
for (int guard = 0; guard < 4096; ++guard) {
const uint8_t* e = at(cur, kTypeDefEntrySize);
if (!e) break;
int32_t type = (int32_t)rd_u32(cur);
if (type == detail::MT_End) break;
TypeMember m;
m.type = type;
// Name @ +4(指针槽)
m.name = rd_str({cur.section, cur.offset + 4});
// ReferenceType @ +8(指针槽 → data_type_definition 数组)
m.ref_type = follow({cur.section, cur.offset + 8});
// ArrayWidth @ +12
m.array_width = (int32_t)rd_u32({cur.section, cur.offset + 12});
uint32_t width = (m.array_width <= 0) ? 1u : (uint32_t)m.array_width;
m.offset = inst_off;
m.size = member_unit_size(m) * width;
inst_off += m.size;
out.push_back(std::move(m));
cur.offset += kTypeDefEntrySize;
}
slot = std::move(out);
return slot;
}
const TypeMember* File::Impl::find_member(const std::vector<TypeMember>& ms, const char* name) const {
for (const auto& m : ms) if (m.name == name) return &m;
return nullptr;
}
// ArrayOfReferences 成员:{int32 count; ptr → count 个指针槽}
std::vector<Ref> File::Impl::read_array_of_refs(Ref obj, const TypeMember& m) const {
std::vector<Ref> out;
Ref slot = member_slot(obj, m);
int32_t count = rd_i32(slot);
if (count <= 0 || count > (1 << 24)) return out;
Ref arr = follow({slot.section, slot.offset + 4}); // 指针槽在 count 之后
if (!arr.valid()) return out;
out.reserve(count);
for (int32_t i = 0; i < count; ++i) {
Ref elem = follow({arr.section, arr.offset + uint32_t(i) * 4});
out.push_back(elem); // 可能 invalidNULL 项)
}
return out;
}
// ReferenceToArray 成员:{int32 count; ptr → count 个连续对象}
bool File::Impl::read_ref_to_array(Ref obj, const TypeMember& m,
Ref& base, uint32_t& count, uint32_t& stride) {
Ref slot = member_slot(obj, m);
int32_t c = rd_i32(slot);
base = {};
count = 0;
stride = m.ref_type.valid() ? object_size(m.ref_type) : 0;
if (c <= 0 || c > (1 << 24)) return c == 0; // 空数组不算错
base = follow({slot.section, slot.offset + 4});
if (!base.valid()) return false;
count = uint32_t(c);
return true;
}
// ── gr2dump 用:打印 root object 的类型成员(不硬编码 struct 布局)─────
std::vector<MemberDump> dump_root_members(const File& f) {
std::vector<MemberDump> out;
if (!f.impl_ || !f.impl_->root_type.valid()) return out;
const auto& ms = f.impl_->parse_type(f.impl_->root_type);
out.reserve(ms.size());
for (const auto& m : ms)
out.push_back({m.name, m.type, m.array_width});
return out;
}
} // namespace gr2
+135
View File
@@ -0,0 +1,135 @@
// libgr2 内部。SDK struct 镜像从 granny_file_format.h / granny_data_type_definition.h 抄。
#pragma once
#include "gr2/gr2.h"
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
namespace gr2 {
namespace detail {
// ── granny_file_format.h 镜像 ──────────────────────────────────────────
#pragma pack(push, 1)
struct GrnFileMagic { // grn_file_magic_value
uint32_t magic[4];
uint32_t header_size;
uint32_t header_format;
uint32_t reserved[2];
};
struct GrnReference { uint32_t section; uint32_t offset; };
struct GrnPointerFixup { uint32_t from_offset; GrnReference to; }; // 12B
struct GrnSection { // grn_section —— 44B
uint32_t compression;
uint32_t data_offset, data_size;
uint32_t expanded_data_size;
uint32_t internal_alignment;
uint32_t first16bit, first8bit;
uint32_t pointer_fixup_off, pointer_fixup_count;
uint32_t mixed_fixup_off, mixed_fixup_count;
};
#pragma pack(pop)
struct Section {
std::vector<uint8_t> data; // 展开后
SectionInfo info;
};
// section 内的位置。不物化指针(文件是 32 位指针,本进程 64 位),
// 全程用 (section, offset) + fixup 表按需解引用。
struct Ref {
int32_t section = -1;
uint32_t offset = 0;
bool valid() const { return section >= 0; }
};
// ── granny_data_type_definition.hmember_type 枚举(序数不可变)────────
enum MemberType {
MT_End = 0, MT_Inline, MT_Reference, MT_RefToArray, MT_ArrayOfRefs,
MT_VariantRef, MT_Removed, MT_RefToVariantArray, MT_String, MT_Transform,
MT_Real32, MT_Int8, MT_UInt8, MT_BinormInt8, MT_NormUInt8,
MT_Int16, MT_UInt16, MT_BinormInt16, MT_NormUInt16,
MT_Int32, MT_UInt32, MT_Real16, MT_EmptyRef, MT_OnePastLast
};
// data_type_definition 在文件里的字节数(32 位指针):
// Type(4) Name(ptr4) ReferenceType(ptr4) ArrayWidth(4) Extra[3](12) Ignored(ptr4)
constexpr uint32_t kTypeDefEntrySize = 32;
struct TypeMember {
std::string name;
int32_t type = MT_End;
int32_t array_width = 0; // 0 => 1
Ref ref_type; // Inline/Reference/... 的元素类型
uint32_t offset = 0; // 在对象实例里的字节偏移(打包,无对齐 pad)
uint32_t size = 0; // 本 member 的总字节数
};
} // namespace detail
struct File::Impl {
std::vector<detail::Section> sections; // 展开后数据 + info
std::vector<detail::GrnSection> raw; // 原始 section 头(fixup 用)
using Ref = detail::Ref;
using TypeMember = detail::TypeMember;
using GrnSection = detail::GrnSection;
// (section<<32 | from_offset) -> 目标 Ref
std::unordered_map<uint64_t, Ref> fixups;
Ref root_obj; // grn_file_header.RootObject
Ref root_type; // grn_file_header.RootObjectTypeDefinition
bool typetree_ready = false;
// 类型解析缓存:typedef 的 Ref -> 成员列表
std::unordered_map<uint64_t, std::vector<TypeMember>> type_cache;
// ── T2 / T1 ──
bool decompress_section(const GrnSection&, const uint8_t* src, detail::Section& out, LoadError*);
void build_fixups(const uint8_t* file, size_t file_len); // T1
// ── T3 ──
const uint8_t* at(Ref r, size_t need = 0) const; // section 内指针 + 边界检查
Ref follow(Ref slot) const; // 解一个"指针槽"
const std::vector<TypeMember>& parse_type(Ref type_ref); // 走 data_type_definition 数组
uint32_t object_size(Ref type_ref);
uint32_t member_unit_size(const TypeMember&);
const TypeMember* find_member(const std::vector<TypeMember>& ms, const char* name) const;
// 便捷读取
uint32_t rd_u32(Ref r) const;
int32_t rd_i32(Ref r) const { return (int32_t)rd_u32(r); }
float rd_f32(Ref r) const;
std::string rd_str(Ref slot) const; // 解引用一个 String 槽
// 成员访问(obj = 对象实例的 Ref,m = 其类型的某个成员)
Ref member_slot(Ref obj, const TypeMember& m) const { return {obj.section, obj.offset + m.offset}; }
Ref read_reference(Ref obj, const TypeMember& m) const { return follow(member_slot(obj, m)); }
// ArrayOfReferences: {int32 count; ptr → [count] 个指针槽 → 各自的对象}
std::vector<Ref> read_array_of_refs(Ref obj, const TypeMember& m) const;
// ReferenceToArray: {int32 count; ptr → [count] 个连续对象(elem_type)}
bool read_ref_to_array(Ref obj, const TypeMember& m, Ref& base, uint32_t& count, uint32_t& stride);
// ── T4/T5/T6/T7a ──
bool build_fileinfo(FileInfo& out, LoadError*);
bool extract_skeleton(Ref skel_obj, Ref skel_type, Skeleton& out);
bool extract_mesh(Ref mesh_obj, Ref mesh_type, const std::vector<Skeleton>& skels, Mesh& out);
// granny_material 链:直接 .Texture.FromFileName,或递归 .Maps[].Material。返回 dds 文件名(原样)。
std::string material_texture_name(Ref mat_obj, Ref mat_type, int depth);
bool extract_animation(Ref anim_obj, Ref anim_type, const Skeleton* skel, Animation& out);
// 曲线子类型分类(T7a)。curve_obj 指向 inline 曲线对象本体,curve_type 是它的类型。
std::string classify_curve_obj(Ref curve_obj, Ref curve_type);
void read_old_curve(Ref curve_obj, Ref curve_type, Curve& out); // T7b
double skeleton_self_check(int skeleton) const;
// granny_transform (68B) → 行主序 4x4BuildCompositeTransform4x4 语义)
void transform_to_composite(Ref tf, Mat4& out, uint32_t& flags) const;
std::vector<Skeleton> skels_cache; // self-check 复用
std::vector<Ref> skel_refs; // 与 skels_cache 平行:各 skeleton 对象的 Refmodel 关联用)
mutable FileInfo* info_cache = nullptr; // 指向 File::info_
};
} // namespace gr2
+611
View File
@@ -0,0 +1,611 @@
/* Granny "Oodle1" section decompressor (M0 T2).
*
* Ported (decode path only) from the leaked Granny 2.9.12 SDK:
* granny_oodle1_compression.cpp + radlz.c + radarith.c + arithbit.c
* Used as algorithm spec; RAD's build machinery / compressor are NOT included.
* See docs/steps/M0-gr2-reader.md T2.
*/
#include "oodle1.h"
#include <stdlib.h>
#include <string.h>
typedef uint8_t U8;
typedef uint16_t U16;
typedef uint32_t U32;
typedef int32_t S32;
typedef uintptr_t UINTa;
#define radassert(x) ((void)0)
/* ================================================================= */
/* arithbit.c — byte-aligned range decoder */
/* ================================================================= */
typedef struct ARITHBITS {
U32 low;
U32 range;
U8 *ptr;
U32 underflow; /* temp between Get()/Remove() to skip re-divide */
U8 *start; /* low bit used as scratch */
} ARITHBITS;
static void ArithBitsGetStart(ARITHBITS *ab, const void *ptr) {
U32 buf;
ab->ptr = (U8 *)ptr + 1;
buf = ((const U8 *)ptr)[0];
ab->start = (U8 *)((UINTa)ptr + (buf & 1));
ab->low = buf >> 1;
ab->range = 1u << 7;
}
static void ArithBitsDecRenorm(ARITHBITS *ab) {
U32 range = ab->range;
if (range <= 0x800000u) {
U32 low = ab->low;
U32 buf = (U32)((UINTa)ab->start & 1u);
U8 *ptr = ab->ptr;
do {
low = (low + low + buf) << 7;
buf = *ptr++;
low |= (buf >> 1);
buf &= 1u;
range <<= 8;
} while (range <= 0x800000u);
ab->low = low;
ab->start = (U8 *)(((UINTa)ab->start & ~(UINTa)1) + buf);
ab->range = range;
ab->ptr = ptr;
}
}
static U32 ArithBitsGet(ARITHBITS *ab, U32 scale) {
U32 tmp;
ArithBitsDecRenorm(ab);
ab->underflow = ab->range / scale;
tmp = ab->low / ab->underflow;
return (tmp >= scale) ? (scale - 1) : tmp;
}
static void ArithBitsRemove(ARITHBITS *ab, U32 start, U32 range, U32 scale) {
U32 tmp = ab->underflow * start;
ab->low -= tmp;
if ((start + range) < scale) ab->range = ab->underflow * range;
else ab->range -= tmp;
}
static U32 ArithBitsGetValue(ARITHBITS *ab, U32 scale) {
U32 tmp, div, start;
ArithBitsDecRenorm(ab);
div = ab->range / scale;
start = ab->low / div;
if (start >= scale) start = scale - 1;
tmp = div * start;
ab->low -= tmp;
if ((start + 1) < scale) ab->range = div;
else ab->range -= tmp;
return start;
}
static U32 ArithBitsGetBits(ARITHBITS *ab, U32 bits, U32 scale) {
U32 tmp;
ArithBitsDecRenorm(ab);
ab->underflow = ab->range >> bits;
tmp = ab->low / ab->underflow;
return (tmp >= scale) ? (scale - 1) : tmp;
}
static U32 ArithBitsGetBitsValue(ARITHBITS *ab, U32 bits, U32 scale) {
U32 tmp, div, start;
ArithBitsDecRenorm(ab);
div = ab->range >> bits;
start = ab->low / div;
if (start >= scale) start = scale - 1;
tmp = div * start;
ab->low -= tmp;
if ((start + 1) < scale) ab->range = div;
else ab->range -= tmp;
return start;
}
/* ================================================================= */
/* radarith.c — adaptive arithmetic modeller (decode path) */
/* ================================================================= */
#define NORM_BITS 14
#define NORM_COUNT (1 << NORM_BITS)
#define ADJ_SUMS (NORM_COUNT << 1)
#define OVERFLOW_COUNT ((NORM_BITS / 2) - 1) /* 6 */
#define COUNTTYPE U16
#define COUNTS_SIZE(v) (sizeof(COUNTTYPE) * (((v) + 1 + 1 + 3) & ~3u))
#define VALUES_SIZE(v) COUNTS_SIZE(v)
typedef struct ARITHDATA {
COUNTTYPE singles_tot;
COUNTTYPE update_tot;
COUNTTYPE update_max;
COUNTTYPE update_range;
COUNTTYPE rescale_tot;
COUNTTYPE singles_length;
COUNTTYPE summed_length;
COUNTTYPE unique_count;
COUNTTYPE *values;
COUNTTYPE *single_counts;
U32 table_walks[NORM_BITS];
COUNTTYPE summed_counts[OVERFLOW_COUNT]; /* deliberately overflows into the gap before single_counts */
} ARITHDATA;
typedef ARITHDATA *ARITH;
#define Arith_was_escaped(val) (((UINTa)(val)) > 65536u)
#define Arith_set_decompressed_symbol(val, v) (*((U16 *)(val)) = (U16)(v))
static U32 Arith_decompress_alloc_size(U32 unique_values) {
return (U32)(sizeof(ARITHDATA) + COUNTS_SIZE(unique_values)
+ COUNTS_SIZE(unique_values) + VALUES_SIZE(unique_values));
}
static ARITH Arith_open(void *ptr, void *compress_temp_buf, U32 max_value, U32 unique_values) {
ARITH a = (ARITH)ptr;
U32 u;
(void)compress_temp_buf; /* always NULL on the decode path */
if (!a) return a;
memset(a, 0, Arith_decompress_alloc_size(unique_values));
a->single_counts = (COUNTTYPE *)((char *)a + (sizeof(ARITHDATA) + COUNTS_SIZE(unique_values)));
a->values = (COUNTTYPE *)((char *)a->single_counts + COUNTS_SIZE(unique_values));
a->unique_count = (COUNTTYPE)unique_values;
a->singles_tot = 4;
a->summed_counts[0] = ADJ_SUMS;
a->summed_counts[1] = NORM_COUNT + ADJ_SUMS;
a->summed_counts[2] = NORM_COUNT + ADJ_SUMS;
a->summed_counts[3] = NORM_COUNT + ADJ_SUMS;
a->summed_counts[4] = NORM_COUNT + ADJ_SUMS;
a->summed_counts[5] = NORM_COUNT + ADJ_SUMS;
a->single_counts[0] = 4;
a->update_tot = 8;
a->update_range = 4;
u = max_value * 32;
if (u < 256) u = 256;
else if (u > 15160) u = 15160;
a->rescale_tot = (COUNTTYPE)u;
u = max_value * 2;
if (u < 128) u = 128;
else if (u >= (((U32)a->rescale_tot >> 1) - 32)) u = (U32)(((U32)a->rescale_tot >> 1) - 32);
a->update_max = (COUNTTYPE)u;
return a;
}
static void rescale_decompress(ARITH a) {
unsigned long i;
U32 max = 0, pos = (U32)-1;
a->single_counts[0] >>= 1;
a->singles_tot = a->single_counts[0];
for (i = 1; i <= a->singles_length; i++) {
while (a->single_counts[i] <= 1) {
if (i < a->singles_length) {
a->single_counts[i] = a->single_counts[a->singles_length];
a->single_counts[a->singles_length] = 0;
a->values[i] = a->values[a->singles_length];
--a->singles_length;
} else {
a->single_counts[i] = 0;
--a->singles_length;
goto done;
}
}
a->single_counts[i] >>= 1;
a->singles_tot = (COUNTTYPE)(a->singles_tot + a->single_counts[i]);
if (a->single_counts[i] > max) { max = a->single_counts[i]; pos = (U32)i; }
}
done:
if (max && a->singles_length) {
U32 j = a->singles_length;
if (pos != j) {
U32 t;
t = a->single_counts[j]; a->single_counts[j] = a->single_counts[pos]; a->single_counts[pos] = (COUNTTYPE)t;
t = a->values[j]; a->values[j] = a->values[pos]; a->values[pos] = (COUNTTYPE)t;
}
}
if ((a->singles_length != a->unique_count) && (a->single_counts[0] == 0)) {
++a->single_counts[0];
++a->singles_tot;
}
}
static void update_counts(ARITH a) {
U32 i, tot, adj;
adj = (NORM_COUNT * 8) / a->singles_tot;
tot = ((((U32)a->single_counts[0]) * adj) >> 3) + ADJ_SUMS;
a->summed_counts[0] = ADJ_SUMS;
i = 1;
for (;;) {
a->summed_counts[i] = (COUNTTYPE)tot;
if (i > a->singles_length) break;
tot += (((U32)a->single_counts[i]) * adj) >> 3;
++i;
}
tot = (U32)a->update_range << 1;
if (tot > a->update_max) {
a->update_tot = (COUNTTYPE)(a->singles_tot + a->update_max);
} else {
a->update_range = (COUNTTYPE)tot;
a->update_tot = (COUNTTYPE)(a->singles_tot + tot);
}
a->summed_length = a->singles_length;
a->summed_counts[a->summed_length + 1] = NORM_COUNT + ADJ_SUMS;
}
static void build_walk_table(U32 *wtable, U32 v) {
U32 num = 0, *m = wtable;
++v;
do {
v = (v + 1) >> 1;
++num;
*m++ = v;
} while (v > 4);
num -= 1;
if (num) {
memmove(wtable + NORM_BITS - 1 - num + 1, wtable + 1, num * sizeof(wtable[0]));
}
wtable[1] = wtable[0];
wtable[0] = num;
}
static UINTa Arith_decompress(ARITH a, ARITHBITS *ab) {
U32 offset;
S32 index;
if (a->singles_tot >= a->update_tot) {
if (a->update_tot >= a->rescale_tot) rescale_decompress(a);
update_counts(a);
a->summed_counts[a->summed_length + 2] = NORM_COUNT + ADJ_SUMS;
a->summed_counts[a->summed_length + 3] = NORM_COUNT + ADJ_SUMS;
a->summed_counts[a->summed_length + 4] = NORM_COUNT + ADJ_SUMS;
a->summed_counts[a->summed_length + 5] = NORM_COUNT + ADJ_SUMS;
a->summed_counts[a->summed_length + 6] = NORM_COUNT + ADJ_SUMS;
build_walk_table(a->table_walks, a->summed_length);
}
offset = ArithBitsGetBits(ab, NORM_BITS, NORM_COUNT) + ADJ_SUMS;
{
index = (S32)a->table_walks[1];
#define check_level(lev) \
if (a->summed_counts[index] > (COUNTTYPE)offset) \
index -= (S32)a->table_walks[NORM_BITS - 1 - (lev)]; \
else \
index += (S32)a->table_walks[NORM_BITS - 1 - (lev)];
switch (a->table_walks[0]) {
case 11: check_level(10) /* fallthrough */
case 10: check_level(9) /* fallthrough */
case 9: check_level(8) /* fallthrough */
case 8: check_level(7) /* fallthrough */
case 7: check_level(6) /* fallthrough */
case 6: check_level(5) /* fallthrough */
case 5: check_level(4) /* fallthrough */
case 4: check_level(3) /* fallthrough */
case 3: check_level(2) /* fallthrough */
case 2: check_level(1) /* fallthrough */
case 1: check_level(0) /* fallthrough */
case 0: break;
default: break;
}
#undef check_level
if (a->summed_counts[index] > (COUNTTYPE)offset) index -= 2;
else index += 2;
if (a->summed_counts[index] > (COUNTTYPE)offset) {
if (a->summed_counts[index - 1] > (COUNTTYPE)offset) index -= 2;
else --index;
} else {
if (a->summed_counts[index + 1] <= (COUNTTYPE)offset) ++index;
}
}
ArithBitsRemove(ab, a->summed_counts[index] - ADJ_SUMS,
a->summed_counts[index + 1] - a->summed_counts[index], NORM_COUNT);
++a->single_counts[index];
++a->singles_tot;
if (index <= 0) {
if (a->singles_length == a->summed_length) {
new_index:
index = ++a->singles_length;
a->single_counts[index] += 2;
a->singles_tot += 2;
if (a->singles_length == a->unique_count) {
a->singles_tot = (COUNTTYPE)(a->singles_tot - a->single_counts[0]);
a->single_counts[0] = 0;
}
return (UINTa)&a->values[index];
} else {
if (ArithBitsGetBitsValue(ab, 1, 2)) {
index = (S32)(ArithBitsGetValue(ab, a->singles_length - a->summed_length)
+ a->summed_length + 1);
a->single_counts[index] += 2;
a->singles_tot += 2;
} else {
goto new_index;
}
}
}
return a->values[index];
}
/* ================================================================= */
/* radlz.c — LZ decoder (decode path) */
/* ================================================================= */
#define LOW_BITS 2
#define MED_BITS 8
#define TOP_BITS 8
#define LARGEST_POSSIBLE_OFFSET ((1 << (LOW_BITS + MED_BITS + TOP_BITS)) - 1)
#define GRANNY_OFFSET LARGEST_POSSIBLE_OFFSET
#define MAX_LENS 64
#define LMAX (1 << LOW_BITS) /* 4 */
#define MMAX (1 << MED_BITS) /* 256 */
#define GETT(v) ((v) >> (LOW_BITS + MED_BITS))
#define ADDRESS_MASK 3
static const U32 long_lengths[4] = { MAX_LENS * 2, MAX_LENS * 3, MAX_LENS * 4, MAX_LENS * 8 };
typedef struct LZ_HEADER {
U32 max_offset_and_byte;
U32 uniq_offset_and_byte;
U32 uniq_lens;
} LZ_HEADER;
typedef struct LZDDATA {
ARITH bytes[ADDRESS_MASK + 1];
ARITH lens[MAX_LENS + 1];
ARITH offsl, offst;
ARITH offsm[256];
U32 max_bytes, max_offs, max_offsL, max_offsM, max_offsT;
U32 bytes_decompressed;
U32 last_len;
} LZDDATA;
typedef LZDDATA *LZD;
static void get_max_offsets(U32 max_offset, U32 *low_max, U32 *med_max, U32 *high_max) {
*low_max = (max_offset >= LMAX) ? LMAX : (max_offset + 1);
*med_max = ((max_offset >> LOW_BITS) >= MMAX) ? MMAX : ((max_offset >> LOW_BITS) + 1);
*high_max = GETT(max_offset) + 1;
}
static void copy_bytes(void *d, const void *s, U32 length) {
U8 *dest = (U8 *)d;
const U8 *src = (const U8 *)s;
/* LZ back-refs always have src < dest, so the fast path never triggers here;
* byte copy is required for correct RLE-style overlap. */
if (length >= 4 && (src - dest) >= 4) {
do {
length -= 4;
((U32 *)dest)[0] = ((const U32 *)src)[0];
dest += 4; src += 4;
} while (length > 4);
if (length == 0) return;
}
do { length--; *dest++ = *src++; } while (length);
}
static U32 LZ_decompress_alloc_size(U32 max_byte_value, U32 uniq_byte_values, U32 max_offset) {
U32 size, h, low_max, med_max, high_max;
(void)max_byte_value;
size = (U32)sizeof(LZDDATA);
get_max_offsets(max_offset, &low_max, &med_max, &high_max);
h = Arith_decompress_alloc_size(uniq_byte_values);
size += h * (ADDRESS_MASK + 1);
size += Arith_decompress_alloc_size(MAX_LENS + 1) * (MAX_LENS + 1);
size += Arith_decompress_alloc_size(low_max);
size += high_max * Arith_decompress_alloc_size(med_max);
size += Arith_decompress_alloc_size(high_max);
return size;
}
static LZD LZ_decompress_open_from_header(void *ptr, const LZ_HEADER *h) {
U32 i, j, size, uniq = 0;
U8 *addr;
LZD l = (LZD)ptr;
l->max_bytes = h->max_offset_and_byte & 511;
l->max_offs = h->max_offset_and_byte >> 9;
get_max_offsets(l->max_offs, &l->max_offsL, &l->max_offsM, &l->max_offsT);
l->last_len = 0;
l->bytes_decompressed = 0;
j = h->uniq_offset_and_byte & 511;
addr = (U8 *)(l + 1);
size = Arith_decompress_alloc_size(j);
for (i = 0; i <= ADDRESS_MASK; i++) {
l->bytes[i] = Arith_open(addr, 0, l->max_bytes - 1, j);
addr += size;
}
for (j = 0; j < 4; j++) {
uniq = (h->uniq_lens >> ((3 - j) * 8)) & 255;
size = Arith_decompress_alloc_size(uniq);
for (i = 0; i < (MAX_LENS / 4); i++) {
l->lens[(j * (MAX_LENS / 4)) + i] = Arith_open(addr, 0, MAX_LENS, uniq);
addr += size;
}
}
for (i = (MAX_LENS / 4) * 4; i <= MAX_LENS; i++) {
l->lens[i] = Arith_open(addr, 0, MAX_LENS, uniq); /* inherits last uniq/size */
addr += size;
}
size = Arith_decompress_alloc_size(l->max_offsM);
for (i = 0; i < l->max_offsT; i++) {
l->offsm[i] = Arith_open(addr, 0, l->max_offsM - 1, l->max_offsM);
addr += size;
}
l->offsl = Arith_open(addr, 0, l->max_offsL - 1, l->max_offsL);
l->offst = Arith_open(addr + Arith_decompress_alloc_size(l->max_offsL),
0, l->max_offsT - 1, GETT(h->uniq_offset_and_byte >> 9) + 1);
return l;
}
static U32 LZ_decompress(LZD l, ARITHBITS *ab, U8 *output) {
UINTa v;
U32 escaped;
v = Arith_decompress(l->lens[l->last_len], ab);
if (Arith_was_escaped(v)) {
escaped = ArithBitsGetValue(ab, MAX_LENS + 1);
Arith_set_decompressed_symbol(v, escaped);
v = escaped;
}
l->last_len = (U32)v;
if (v) {
U32 len, off, max_ofs;
UINTa m;
max_ofs = l->max_offs;
if (max_ofs > l->bytes_decompressed) max_ofs = l->bytes_decompressed;
len = (v >= (MAX_LENS - 3)) ? long_lengths[v - (MAX_LENS - 3)] : (U32)(v + 1);
v = Arith_decompress(l->offsl, ab);
if (Arith_was_escaped(v)) {
escaped = ArithBitsGetValue(ab, l->max_offsL);
Arith_set_decompressed_symbol(v, escaped);
v = escaped;
}
off = (U32)(v + 1);
v = Arith_decompress(l->offst, ab);
if (Arith_was_escaped(v)) {
escaped = ArithBitsGetValue(ab, GETT(max_ofs) + 1);
Arith_set_decompressed_symbol(v, escaped);
v = escaped;
}
m = Arith_decompress(l->offsm[v], ab);
if (Arith_was_escaped(m)) {
U32 offsm_used;
if (max_ofs >= (MMAX << LOW_BITS)) offsm_used = MMAX;
else offsm_used = (max_ofs >> LOW_BITS) + 1;
escaped = ArithBitsGetValue(ab, offsm_used);
Arith_set_decompressed_symbol(m, escaped);
m = escaped;
}
off = (U32)(off + ((U32)m << LOW_BITS) + ((U32)v << (LOW_BITS + MED_BITS)));
l->bytes_decompressed += len;
copy_bytes(output, output - off, len);
return len;
} else {
v = Arith_decompress(l->bytes[((UINTa)output) & ADDRESS_MASK], ab);
if (Arith_was_escaped(v)) {
escaped = ArithBitsGetValue(ab, l->max_bytes);
Arith_set_decompressed_symbol(v, escaped);
v = escaped;
}
*output = (U8)v;
++l->bytes_decompressed;
return 1;
}
}
/* ================================================================= */
/* granny_oodle1_compression.cpp — the wrapper */
/* ================================================================= */
#define ALIGN32(x) (((x) + 3u) & ~3u)
static void reverse32(void *p, U32 nbytes) {
U8 *b = (U8 *)p;
U32 i;
for (i = 0; i + 4 <= nbytes; i += 4) {
U8 t0 = b[i], t1 = b[i + 1];
b[i] = b[i + 3];
b[i + 1] = b[i + 2];
b[i + 2] = t1;
b[i + 3] = t0;
}
}
int gr2_oodle1_decompress(int file_is_byte_reversed,
U32 comp_size, void *comp_bytes,
U32 stop0, U32 stop1, U32 stop2, void *out) {
LZ_HEADER headers[3];
ARITHBITS ab;
U32 temp_size, size, b;
U32 stops[3];
void *temp;
U8 *to;
/* hanging-bit fix: zero-pad up to 32-bit alignment (caller also over-allocates). */
{
U32 rounded = ALIGN32(comp_size) - comp_size;
while (rounded--) ((U8 *)comp_bytes)[comp_size + rounded] = 0;
}
if (comp_size < sizeof(headers)) return -1;
memcpy(headers, comp_bytes, sizeof(headers));
if (file_is_byte_reversed) reverse32(headers, sizeof(headers));
ArithBitsGetStart(&ab, (const U8 *)comp_bytes + sizeof(headers));
temp_size = LZ_decompress_alloc_size(255, 256, GRANNY_OFFSET);
temp = malloc(temp_size);
if (!temp) return -2;
size = 0;
stops[0] = stop0; stops[1] = stop1; stops[2] = stop2;
to = (U8 *)out;
for (b = 0; b < 3; ++b) {
U32 stop = stops[b];
LZD lz = LZ_decompress_open_from_header(temp, &headers[b]);
U32 guard = 0;
while (size < stop) {
U32 len = LZ_decompress(lz, &ab, to);
if (len == 0 || size + len > stop2 + 512) { free(temp); return -3; } /* corruption guard */
size += len;
to += len;
if (++guard > stop2 + 16) { free(temp); return -4; }
}
if (size != stop) { free(temp); return -5; }
}
free(temp);
return 0;
}
int gr2_oodle1_decompress_chunk(int file_is_byte_reversed,
U32 comp_size, void *comp_bytes,
U32 decompressed_size, void *out) {
LZ_HEADER header;
ARITHBITS ab;
U32 temp_size, size;
void *temp;
U8 *to;
if (comp_size < sizeof(header)) return -1;
memcpy(&header, comp_bytes, sizeof(header));
if (file_is_byte_reversed) reverse32(&header, sizeof(header));
ArithBitsGetStart(&ab, (const U8 *)comp_bytes + sizeof(header));
temp_size = LZ_decompress_alloc_size(255, 256, GRANNY_OFFSET);
temp = malloc(temp_size);
if (!temp) return -2;
size = 0;
to = (U8 *)out;
{
LZD lz = LZ_decompress_open_from_header(temp, &header);
while (size < decompressed_size) {
U32 len = LZ_decompress(lz, &ab, to);
if (len == 0) { free(temp); return -3; }
size += len;
to += len;
}
}
free(temp);
return (size == decompressed_size) ? 0 : -5;
}
+31
View File
@@ -0,0 +1,31 @@
// Granny "Oodle1" section 解压(M0 T2)。
// 端口自泄露 SDK 的 granny_oodle1_compression.cpp + radlz.c + radarith.c + arithbit.c
// (只移植解码路径,作算法规格 —— 不含压缩,不含 RAD 的构建机制)。
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
// comp_bytes:可写缓冲,长度 >= comp_size + 32,尾部已清零(解码器会读过界)。
// 起始地址必须 4 字节对齐。
// stop0/1/2 section 的 First16Bit / First8Bit / ExpandedDataSize(累积边界)。
// out :至少 stop2 字节。
// 返回 0 成功,非 0 失败。
int gr2_oodle1_decompress(int file_is_byte_reversed,
uint32_t comp_size,
void* comp_bytes,
uint32_t stop0, uint32_t stop1, uint32_t stop2,
void* out);
// 单块版(NoCompression 之外走不到;保留对称)
int gr2_oodle1_decompress_chunk(int file_is_byte_reversed,
uint32_t comp_size,
void* comp_bytes,
uint32_t decompressed_size,
void* out);
#ifdef __cplusplus
}
#endif
+63
View File
@@ -0,0 +1,63 @@
#include <gr2/gr2.h>
#include <cstdint>
#include <cstdio>
#include <string>
#include <vector>
namespace {
void wr32(std::vector<uint8_t> &bytes, size_t off, uint32_t value) {
bytes[off + 0] = uint8_t(value);
bytes[off + 1] = uint8_t(value >> 8);
bytes[off + 2] = uint8_t(value >> 16);
bytes[off + 3] = uint8_t(value >> 24);
}
std::vector<uint8_t> granny_header(size_t size, uint32_t section_count) {
std::vector<uint8_t> bytes(size, 0);
const uint32_t magic[] = { 0xCAB067B8, 0x0FB16DF8, 0x7E8C7284, 0x1E00195E };
for (int i = 0; i < 4; ++i) {
wr32(bytes, size_t(i) * 4, magic[i]);
}
constexpr size_t h = 32;
wr32(bytes, h + 0, 6); // Version
wr32(bytes, h + 4, uint32_t(size));
wr32(bytes, h + 12, 40); // section table starts at byte 72
wr32(bytes, h + 16, section_count);
// Root type/object remain section 0, offset 0. With no section this must be
// rejected by the FileInfo stage rather than returned as an empty File.
return bytes;
}
bool expect_failure(const std::vector<uint8_t> &bytes, const char *stage) {
gr2::LoadError err;
auto file = gr2::File::load(bytes.data(), bytes.size(), &err);
if (file || err.stage != stage) {
std::fprintf(stderr, "expected failure stage '%s', got file=%d stage='%s' message='%s'\n",
stage, file.has_value(), err.stage.c_str(), err.message.c_str());
return false;
}
return true;
}
} // namespace
int main() {
bool ok = true;
ok &= expect_failure(std::vector<uint8_t>(80, 0), "header");
ok &= expect_failure(granny_header(80, 0), "fileinfo");
auto unsupported = granny_header(160, 1);
constexpr size_t section = 72;
wr32(unsupported, section + 0, 4); // unsupported BitKnit
wr32(unsupported, section + 4, uint32_t(unsupported.size()));
wr32(unsupported, section + 12, 1); // non-empty expanded section
ok &= expect_failure(unsupported, "section");
auto bad_offset = granny_header(160, 1);
wr32(bad_offset, section + 4, UINT32_MAX);
ok &= expect_failure(bad_offset, "section");
return ok ? 0 : 1;
}