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:
@@ -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()
|
||||
@@ -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 定义和公式注释**自己写的,不是复制实现。
|
||||
@@ -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>1(EterGrnLib Material.cpp:233)
|
||||
bool two_sided = false; // 名字含 2side/twoside/double/leaf/tree/ivy/fence(ExtendedData "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 T5(noise_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
|
||||
@@ -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,全是 OldCurveType(granny_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 到 out(identity 由调用方给)。
|
||||
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 9(3x3)或 dim 3
|
||||
};
|
||||
|
||||
struct Animation {
|
||||
std::string name;
|
||||
float duration = 0.0f;
|
||||
std::vector<BoneTrack> tracks;
|
||||
// t 处每个 track 的局部 4x4(granny_transform 组合语义,行主序)。
|
||||
// out 按 tracks 顺序;bone_index 见各 track。
|
||||
void sample_local(float t, std::vector<Mat4>& out) const;
|
||||
};
|
||||
|
||||
// 顶层 granny_material(driven 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 表(T1,gr2dump --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
|
||||
@@ -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:曲线是 OldCurveType(granny_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 曲线全是 OldCurveType(B 样条: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 个控制点做
|
||||
// EnsureQuaternionContinuity(granny_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 的局部 4x4(BuildCompositeTransform4x4 语义: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
|
||||
@@ -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
|
||||
@@ -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 个已知 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> 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── BoneBindings:mesh 局部骨骼槽 → 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
|
||||
@@ -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 {
|
||||
|
||||
// 行主序 4x4:R = A * B(ColumnMatrixMultiply4x3Impl 语义,平移在第 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 * InitialPlacement(BuildWorldPose 的 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
|
||||
@@ -0,0 +1,204 @@
|
||||
// M0 T1(fixup 索引)+ T3(自描述类型树遍历)。见 docs/steps/M0-gr2-reader.md
|
||||
// 规格:granny_file_format.h(fixup)+ 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); // 可能 invalid(NULL 项)
|
||||
}
|
||||
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
|
||||
@@ -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.h:member_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) → 行主序 4x4(BuildCompositeTransform4x4 语义)
|
||||
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 对象的 Ref(model 关联用)
|
||||
|
||||
mutable FileInfo* info_cache = nullptr; // 指向 File::info_
|
||||
};
|
||||
|
||||
} // namespace gr2
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user