Files
mtgodot-poc/oracle/oracle.c
T
shenandClaude Sonnet 5 47baf6c0c6 Metin2 game client (P0–P11) + mobile asset pipeline
Networked client on the existing Godot 4.7 + libgr2 renderer:
- net: m2dev wire protocol (libsodium KX + XChaCha20), auth/select/game
  phases, EntityStore world model, ~all GC/CG headers. char create/delete,
  private shop / mall / cube, SHOP_GC_START_EX, guild, party (+ CG_PARTY_SET_STATE),
  quests, dragon soul, refine, safebox, exchange.
- UI: in-game windows migrated 1:1 from the reference uiscript/root .py —
  char status (/stat), inventory+equipment, select-item ([SELECT_ITEM] quest
  token), system-option + game-option + ESC system menu, private-shop 39-grid,
  party info board, shop tabs, atlas, minimap, quickbar, chat, …
- EterGrnLib polish: GR2 material blend/two-sided, LOD crossfade, motion-event
  dispatch, contact shadow, ray-AABB picking, weapon grip pre-transform.

Portable asset IO (A1) — all extension/libgr2/formats/mtproto reads routed
through godot::FileAccess (res:// PCK works on iOS/Android); standalone-lib
*_path() kept for the non-Godot CTests. AssetResolver + PropertyRegistry
switched to a baked index (bake_asset_index.gd) instead of std::filesystem.

Mobile builds: build-{android,ios}.sh, export-android.sh, pack-assets.sh,
gen-debug-keystore.sh. Assets ship as a zip mounted at runtime by
project/asset_pack.gd (adb push now; HTTP download is a drop-in later).

ctest 10/10, 34 GDScript suites, macOS/iOS/Android all build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EJxkHiNKS4kybHS3XKyAJ
2026-08-31 20:02:12 +09:00

216 lines
9.3 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* oracle —— Windows 真值源(Oracle-Lite)。见 ../docs/steps/00-oracle.md
*
* oracle probe <file.gr2>
* → 打 GrannyGetFileInfo 的 count / FromFileName / 每 skeleton 骨骼数 + 前几个骨骼名
*
* oracle dump <model.gr2> <anim.gr2|-> <t> -o out.bin [--mesh N]
* → 层① 世界矩阵(GrannyGetWorldPoseComposite4x4Array 直出)+ mesh N 蒙皮顶点
* → 二进制布局见 ../FORMAT.md;libgr2 侧对拍读取器必须与之一致
*
* 只用 granny2 的公开 C APIgranny.h)。链 granny2_x64.lib(见 CMakeLists.txt / RUNBOOK.md)。
* 不碰 D3D、不碰 EterGrnLib —— 那是 Oracle-Full(延后)。
*/
#include <granny.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#define ORC_MAGIC "XORC"
#define ORC_VERSION 1u
#define KIND_LAYER1 1u
#define KIND_SKINV 3u
static void die(const char *msg) { fprintf(stderr, "oracle: %s\n", msg); exit(2); }
static const char *granny_version_string(void) {
/* 运行期版本(链接到的 granny2.dll 实际报的)。编译期宏是 GrannyProduct*Version。*/
const char *rt = GrannyGetVersionString();
return rt ? rt : "unknown";
}
/* ── probe ─────────────────────────────────────────────────────────── */
static int cmd_probe(const char *path) {
granny_file *f = GrannyReadEntireFile(path);
if (!f) { fprintf(stderr, "probe: cannot read %s\n", path); return 1; }
granny_file_info *fi = GrannyGetFileInfo(f);
if (!fi) { fprintf(stderr, "probe: GrannyGetFileInfo NULL\n"); return 1; }
printf("file : %s\n", path);
printf("granny runtime : %s\n", granny_version_string());
printf("FromFileName : %s\n", fi->FromFileName ? fi->FromFileName : "(null)");
printf("Textures=%d Materials=%d Skeletons=%d VertexDatas=%d TriTopologies=%d "
"Meshes=%d Models=%d TrackGroups=%d Animations=%d\n",
fi->TextureCount, fi->MaterialCount, fi->SkeletonCount, fi->VertexDataCount,
fi->TriTopologyCount, fi->MeshCount, fi->ModelCount, fi->TrackGroupCount,
fi->AnimationCount);
for (int s = 0; s < fi->SkeletonCount; ++s) {
granny_skeleton *sk = fi->Skeletons[s];
printf(" skeleton %d: %d bones : ", s, sk->BoneCount);
for (int b = 0; b < sk->BoneCount && b < 6; ++b)
printf("[%d]%s(p=%d) ", b, sk->Bones[b].Name, sk->Bones[b].ParentIndex);
printf("%s\n", sk->BoneCount > 6 ? "..." : "");
}
for (int a = 0; a < fi->AnimationCount; ++a)
printf(" animation %d: '%s' Duration=%.6f TimeStep=%.6f TrackGroups=%d\n",
a, fi->Animations[a]->Name, fi->Animations[a]->Duration,
fi->Animations[a]->TimeStep, fi->Animations[a]->TrackGroupCount);
GrannyFreeFile(f);
return 0;
}
/* ── dump helpers ──────────────────────────────────────────────────── */
static void wr_u32(FILE *o, uint32_t v) { fwrite(&v, 4, 1, o); }
static void wr_f32(FILE *o, float v) { fwrite(&v, 4, 1, o); }
static void wr_str(FILE *o, const char *s) {
uint32_t n = (uint32_t)(s ? strlen(s) : 0);
wr_u32(o, n);
if (n) fwrite(s, 1, n, o);
}
/* 与 ../FORMAT.md「文件头」一致 */
static void write_header(FILE *o, uint32_t kind, uint32_t coord,
const char *model_path, const char *anim_path, float t) {
fwrite(ORC_MAGIC, 1, 4, o);
wr_u32(o, ORC_VERSION);
wr_u32(o, kind);
wr_u32(o, coord);
char gv[16]; memset(gv, 0, sizeof gv);
{ const char *v = granny_version_string(); size_t n = strlen(v);
memcpy(gv, v, n < 15 ? n : 15); }
fwrite(gv, 1, 16, o);
wr_str(o, model_path);
wr_str(o, anim_path ? anim_path : "");
wr_f32(o, t);
}
/* ── dump ──────────────────────────────────────────────────────────── */
static int cmd_dump(int argc, char **argv) {
if (argc < 4) die("dump <model.gr2> <anim.gr2|-> <t> -o out.bin [--mesh N]");
const char *model_path = argv[0];
const char *anim_path = strcmp(argv[1], "-") == 0 ? NULL : argv[1];
float t = (float)atof(argv[2]);
const char *out_path = NULL;
int mesh_index = 0;
for (int i = 3; i < argc; ++i) {
if (!strcmp(argv[i], "-o") && i + 1 < argc) out_path = argv[++i];
else if (!strcmp(argv[i], "--mesh") && i + 1 < argc) mesh_index = atoi(argv[++i]);
}
if (!out_path) die("dump: 缺 -o out.bin");
granny_file *mf = GrannyReadEntireFile(model_path);
if (!mf) die("dump: model 读失败");
granny_file_info *mfi = GrannyGetFileInfo(mf);
if (!mfi || mfi->ModelCount < 1) die("dump: model 无 Models[0]");
granny_model_instance *mi = GrannyInstantiateModel(mfi->Models[0]);
if (!mi) die("dump: GrannyInstantiateModel 失败");
granny_skeleton *skel = GrannyGetSourceSkeleton(mi);
int bone_count = skel->BoneCount;
granny_file *af = NULL;
if (anim_path) {
af = GrannyReadEntireFile(anim_path);
if (!af) die("dump: anim 读失败");
granny_file_info *afi = GrannyGetFileInfo(af);
if (!afi || afi->AnimationCount < 1) die("dump: anim 无 Animations[0]");
granny_control *ctrl = GrannyPlayControlledAnimation(0.0f, afi->Animations[0], mi);
if (!ctrl) die("dump: GrannyPlayControlledAnimation 失败");
}
GrannySetModelClock(mi, t);
/* Offset4x4 = model 的 InitialPlacement(客户端渲染时也这么给)。
* libgr2 侧 gr2::sample_pose 用 skeleton.initial_placement 做同一件事。 */
granny_real32 placement[16];
GrannyGetModelInitialPlacement4x4(mfi->Models[0], placement);
granny_local_pose *lp = GrannyNewLocalPose(bone_count);
granny_world_pose *wp = GrannyNewWorldPose(bone_count);
/* 先填 rest(无 track 的骨骼用它);有动画再让 SampleModelAnimations 覆盖动的那些。 */
GrannyBuildRestLocalPose(skel, 0, bone_count, lp);
if (af) GrannySampleModelAnimations(mi, 0, bone_count, lp);
GrannyBuildWorldPose(skel, 0, bone_count, lp, placement, wp);
/* 层① = 每骨「骨骼空间 → 世界」的原始世界矩阵(GrannyGetWorldPose4x4Array)。
* 注意:GrannyGetWorldPoseComposite4x4Array 返回的是 InverseWorld4x4·world = 蒙皮矩阵,
* 不是这里要的。 */
granny_matrix_4x4 *world = GrannyGetWorldPose4x4Array(wp);
/* coordraw(手系按 DLL 输出、单位=原始、根变换未 apply)。见 FORMAT.md。 */
const uint32_t coord = 0u;
FILE *o = fopen(out_path, "wb");
if (!o) die("dump: 打不开输出文件");
write_header(o, KIND_LAYER1, coord, model_path, anim_path, t);
wr_u32(o, (uint32_t)bone_count);
for (int b = 0; b < bone_count; ++b)
fwrite(&world[b][0][0], 4, 16, o);
/* kind=3mesh_index 的蒙皮顶点位置。照 SDK tutorialwii_basic.cpp CreateMesh/Render):
* deformer(输入=mesh 自带顶点类型, 输出=PNT332)
* 矩阵缓冲 = GrannyGetWorldPoseComposite4x4Array= InverseWorld·world = 蒙皮矩阵)
* 索引 = GrannyGetMeshBindingToBoneIndicesmesh 骨骼槽 → model 骨骼) */
if (mfi->MeshCount > mesh_index) {
granny_mesh *mesh = mfi->Meshes[mesh_index];
int vcount = GrannyGetMeshVertexCount(mesh);
typedef struct { float p[3], n[3], uv[2]; } pnt332;
pnt332 *dst = (pnt332 *)malloc(sizeof(pnt332) * vcount);
granny_matrix_4x4 *composite = GrannyGetWorldPoseComposite4x4Array(wp);
granny_mesh_binding *mb = GrannyNewMeshBinding(mesh, skel, skel);
granny_int32x const *toBone = GrannyGetMeshBindingToBoneIndices(mb);
granny_mesh_deformer *def = GrannyNewMeshDeformer(
GrannyGetMeshVertexType(mesh), GrannyPNT332VertexType,
GrannyDeformPositionNormal, GrannyDontAllowUncopiedTail);
if (def) {
GrannyDeformVertices(def, toBone, (const granny_real32 *)composite,
vcount, GrannyGetMeshVertices(mesh), dst);
} else {
fprintf(stderr, "dump: no deformer for mesh %d vertex type\n", mesh_index);
}
wr_u32(o, (uint32_t)vcount);
for (int v = 0; v < vcount; ++v)
fwrite(dst[v].p, 4, 3, o);
if (def) GrannyFreeMeshDeformer(def);
GrannyFreeMeshBinding(mb);
free(dst);
} else {
wr_u32(o, 0);
}
fclose(o);
printf("dump: wrote %s bones=%d t=%.6f granny=%s\n",
out_path, bone_count, t, granny_version_string());
GrannyFreeWorldPose(wp);
GrannyFreeLocalPose(lp);
GrannyFreeModelInstance(mi);
if (af) GrannyFreeFile(af);
GrannyFreeFile(mf);
return 0;
}
int main(int argc, char **argv) {
if (argc < 2) {
fprintf(stderr,
"usage:\n"
" oracle probe <file.gr2>\n"
" oracle dump <model.gr2> <anim.gr2|-> <t> -o out.bin [--mesh N]\n");
return 2;
}
if (!strcmp(argv[1], "probe")) {
if (argc < 3) die("probe <file.gr2>");
return cmd_probe(argv[2]);
}
if (!strcmp(argv[1], "dump")) return cmd_dump(argc - 2, argv + 2);
die("未知子命令(probe / dump");
return 2;
}