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

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

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

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

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

513 lines
21 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.
// W0 —— A1 地图格式解析单测。SHINSOO §9-W0 / §11.1。
//
// 无参数:跑内联字符串单测(不需要资源)。
// 环境变量 M2_ASSETS 指向 Metin2 资产目录(默认 <repo>/assets):额外跑真实 A1 的 W0 验收断言。
#include "area_data.h"
#include "asset_resolver.h"
#include "environment.h"
#include "m2_coord.h"
#include "m2_tokvec.h"
#include "map_setting.h"
#include "property.h"
#include "splat.h"
#include "spt.h"
#include "terrain_files.h"
#include "terrain_mesh.h"
#include "texture_set.h"
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <string>
static int g_fail = 0;
#define CHECK(cond, msg) \
do { \
if (!(cond)) { \
std::fprintf(stderr, "FAIL: %s (%s:%d)\n", (msg), __FILE__, __LINE__); \
++g_fail; \
} \
} while (0)
static void test_tokvec() {
const std::string t = R"(
ScriptType MapSetting
MapSize 4 5
Start Object000
1.5 -2.0 3.0
569394331
End Object
# comment
Start Object000
9 9 9
End Object
)";
fmt::TokVecMap tv;
std::string err;
CHECK(fmt::parse_tokvec(t, tv, &err), "tokvec parse");
CHECK(tv.str("scripttype") == "MapSetting", "tokvec top-level str");
const auto* ms = tv.find("MapSize"); // 大小写不敏感
CHECK(ms && ms->size() == 2 && (*ms)[1] == "5", "tokvec MapSize tokens");
const auto* o0 = tv.find("object000");
CHECK(o0 && o0->size() == 4, "tokvec block flattens all lines");
CHECK(o0 && (*o0)[3] == "569394331", "tokvec block token order");
CHECK(tv.dup_count == 1, "tokvec dup detected");
}
static void test_coord() {
using namespace fmt::m2coord;
Vec3 g = position_to_godot(100.0, 200.0, 300.0);
CHECK(std::fabs(g.x - 1.0) < 1e-9, "coord x");
CHECK(std::fabs(g.y - 3.0) < 1e-9, "coord y = z*0.01");
CHECK(std::fabs(g.z + 2.0) < 1e-9, "coord z = -y*0.01");
CHECK(tile_id(1, 3) == 1003u, "tile_id");
CHECK(tile_dir(0, 0) == "000000", "tile_dir 0");
CHECK(tile_dir(1, 3) == "001003", "tile_dir 1,3");
int tx = -1, ty = -1;
CHECK(parse_tile_dir("003004", tx, ty) && tx == 3 && ty == 4, "parse_tile_dir");
CHECK(!parse_tile_dir("00300", tx, ty), "parse_tile_dir rejects len!=6");
CHECK(CHUNK_CM == 25600, "chunk cm");
// ypr_basis = 原始 D3DXMatrixRotationYawPitchRoll 端口,在 **Metin2 Z-up 空间**。
Mat3 y90 = ypr_basis(90.0, 0, 0); // yaw 绕 Metin2 Y(未转轴)
// m 是 row-major,基向量 = 列:new_x = (m[0],m[3],m[6])
CHECK(std::fabs(y90.m[0]) < 1e-6 && std::fabs(y90.m[6] + 1.0) < 1e-6,
"ypr yaw=90: local +X -> -Z (Metin2 space)");
Mat3 id = ypr_basis(0, 0, 0);
CHECK(std::fabs(id.m[0] - 1) < 1e-9 && std::fabs(id.m[4] - 1) < 1e-9 &&
std::fabs(id.m[8] - 1) < 1e-9,
"ypr zero = identity");
// object_basis_godot = ypr_basis 共轭到 Godot Y-up。放置物体必须用这个。
Mat3 og0 = object_basis_godot(0, 0, 0);
CHECK(std::fabs(og0.m[0] - 1) < 1e-9 && std::fabs(og0.m[4] - 1) < 1e-9 &&
std::fabs(og0.m[8] - 1) < 1e-9,
"object_basis_godot zero = identity");
// areadata 单值旋转 = roll = 朝向。共轭后必须是绕 Godot +Y 的 heading
// 不是绕 Godot Z 的翻滚(修复前的 bug)。
Mat3 rl90 = object_basis_godot(0, 0, 90.0);
CHECK(std::fabs(rl90.m[1]) < 1e-6 && std::fabs(rl90.m[4] - 1.0) < 1e-6 &&
std::fabs(rl90.m[7]) < 1e-6,
"object_basis_godot roll=90: Godot +Y axis fixed (heading, no roll)");
CHECK(std::fabs(rl90.m[0]) < 1e-6 && std::fabs(rl90.m[6] + 1.0) < 1e-6,
"object_basis_godot roll=90: local +X -> Godot -Z");
// 正交且右手(det=+1):取一个混合角
Mat3 m = object_basis_godot(20.0, -35.0, 110.0);
double det = m.m[0] * (m.m[4] * m.m[8] - m.m[5] * m.m[7]) -
m.m[1] * (m.m[3] * m.m[8] - m.m[5] * m.m[6]) +
m.m[2] * (m.m[3] * m.m[7] - m.m[4] * m.m[6]);
CHECK(std::fabs(det - 1.0) < 1e-6, "object_basis_godot det = +1");
double col0 = m.m[0] * m.m[0] + m.m[3] * m.m[3] + m.m[6] * m.m[6];
CHECK(std::fabs(col0 - 1.0) < 1e-6, "object_basis_godot col0 unit");
}
static void test_map_setting() {
const std::string t = R"(
ScriptType MapSetting
CellScale 200
HeightScale 0.500000
ViewRadius 128
MapSize 4 5
BasePosition 409600 896000
TextureSet textureset\metin2_A1.txt
Environment A1.msenv
)";
fmt::MapSetting s;
std::string err;
CHECK(fmt::parse_map_setting(t, s, &err), "map_setting parse");
CHECK(s.cell_scale == 200, "cell_scale");
CHECK(std::fabs(s.height_scale - 0.5) < 1e-9, "height_scale");
CHECK(s.map_size_x == 4 && s.map_size_y == 5, "map_size");
CHECK(s.chunk_count() == 20, "chunk_count");
CHECK(s.base_position_x == 409600 && s.base_position_y == 896000, "base_position");
CHECK(s.environment == "A1.msenv", "environment");
}
static void test_texture_set() {
const std::string t = R"(
TextureSet
TextureCount 2
Start Texture001
"d:\ymir work\terrainmaps\b\field\field 01.dds"
5.0 5.0 0.0 0.0 0 0 0
End Texture001
Start Texture002
"d:\ymir work\terrainmaps\b\field\field 02.dds"
6.0 6.0 0.0 0.0 1 10 20
End Texture002
)";
fmt::TextureSet ts;
std::string err;
CHECK(fmt::parse_texture_set(t, ts, &err), "texture_set parse");
CHECK(ts.declared_count == 2, "declared_count");
CHECK(ts.layers.size() == 2, "layers");
CHECK(ts.runtime_count() == 3, "runtime_count = layers + 1 (index0 empty)");
CHECK(ts.layers[1].splat_enabled && ts.layers[1].height_end == 20, "layer fields");
CHECK(ts.layers[0].texture.find("field 01.dds") != std::string::npos, "quoted path w/ space");
}
static void test_area_data() {
const std::string t = R"(
AreaDataFile
Start Object000
20801.746094 -13522.107422 17875.500000
569394331
0.000000#0.000000#180.000000
-40.000000
End Object
Start Object001
17069.0 -11315.7 17926.9
865570388
0.000000#15.000000#225.000000
End Object
ObjectCount 2
)";
fmt::AreaData a;
std::string err;
CHECK(fmt::parse_area_data(t, a, &err), "area_data parse");
CHECK(a.declared_count == 2, "declared");
CHECK(a.objects.size() == 2, "objects");
CHECK(a.objects[0].crc == 569394331u, "crc");
CHECK(std::fabs(a.objects[0].roll - 180.0f) < 1e-3f, "ypr roll");
CHECK(std::fabs(a.objects[0].height_bias + 40.0f) < 1e-3f, "height_bias");
CHECK(a.objects[0].x > 0 && a.objects[0].y < 0, "global cm: x>0 (east), y<0 (south)");
CHECK(std::fabs(a.objects[1].pitch - 15.0f) < 1e-3f && a.objects[1].height_bias == 0.0f,
"ypr without height_bias");
}
static void test_environment() {
const std::string t = R"(
ScriptType EnvrionmentData
ScriptVersion 1.0000
Group DirectionalLight
{
Direction 0.35 0.56 -0.75
Group Background
{
Enable 1
Diffuse 1.0 0.97 0.97 1.0
Ambient 0.0 0.0 0.0 1.0
}
Group Character
{
Enable 1
Ambient 0.15 0.15 0.15 1.0
}
}
Group Fog
{
foglevel 6
Color 0.69 0.74 0.83 1.0
}
Group SkyBox
{
Scale 3500.0 3500.0 3500.0
CloudTextureFileName "d:/ymir work/environment/clouds_zone01.tga"
List Gradient
{
0.09 0.28 0.65 0.0
0.23 0.40 0.72 0.0
0.37 0.49 0.72 0.0
}
}
)";
fmt::Environment e;
std::string err;
CHECK(fmt::parse_environment(t, e, &err), ("env parse: " + err).c_str());
CHECK(e.script_type == "EnvrionmentData", "env script_type (misspelled, accepted)");
CHECK(std::fabs(e.dir_light.direction[2] + 0.75f) < 1e-3f, "env dir light Direction z");
CHECK(e.dir_light.bg_enable && e.dir_light.ch_enable, "env bg/ch enable");
CHECK(std::fabs(e.dir_light.bg_diffuse[1] - 0.97f) < 1e-3f, "env bg diffuse");
CHECK(e.fog.enable && e.fog.fog_level == 6, "env fog (foglevel form)");
CHECK(std::fabs(e.sky.scale[0] - 3500.0f) < 1e-1f, "env skybox scale");
CHECK(e.sky.cloud_texture.find("clouds_zone01.tga") != std::string::npos, "env cloud tex path");
CHECK(e.sky.gradient.size() == 3, "env List Gradient rows");
CHECK(std::fabs(e.sky.gradient[2][0] - 0.37f) < 1e-3f, "env List row values");
}
static void test_property() {
const std::string prb = "YPRT\n274490822\nbuildingfile\t\"d:/ymir work/zone/x.gr2\"\n"
"propertyname\t\"rock_pillar_02\"\npropertytype\t\"Building\"\n"
"shadowflag\t\"1\"\n";
fmt::Property p;
std::string err;
CHECK(fmt::parse_property(prb, p, &err), ("property parse: " + err).c_str());
CHECK(p.crc == 274490822u, "property crc");
CHECK(p.type == fmt::PropertyType::Building, "property type Building");
CHECK(p.name == "rock_pillar_02", "property name");
CHECK(p.get("buildingfile").find("x.gr2") != std::string::npos, "property buildingfile");
CHECK(p.get("shadowflag") == "1", "property shadowflag");
fmt::Property bad;
CHECK(!fmt::parse_property("NOPE\n123\n", bad, &err), "property rejects non-YPRT");
}
static void test_terrain_mesh() {
// 合成一张 131x131 heightmap:平坦 + 一个已知斜坡
fmt::HeightMap hm;
hm.raw.assign(size_t(fmt::HEIGHTMAP_RAW_XY) * fmt::HEIGHTMAP_RAW_XY, 1000);
// 在 (sx,sy)=(10,10) 抬高
auto set = [&](int sx, int sy, uint16_t v) {
hm.raw[(sy + 1) * fmt::HEIGHTMAP_RAW_XY + (sx + 1)] = v;
};
set(10, 10, 2000);
fmt::TerrainMesh tm;
fmt::build_terrain_mesh(hm, 0, 0, 0.5, tm);
CHECK(tm.vertex_count() == 129 * 129, "terrain vert count");
CHECK((int)tm.positions.size() == 129 * 129 * 3, "terrain positions size");
CHECK((int)tm.indices.size() == 128 * 128 * 6, "terrain index count");
// 平坦区顶点高度 = 1000 * 0.5 * 0.01 = 5.0 m
size_t o = (size_t(0) * 129 + 0) * 3;
CHECK(std::fabs(tm.positions[o + 1] - 5.0f) < 1e-4f, "terrain flat height 5m");
// 平坦区法线朝上
CHECK(std::fabs(tm.normals[o + 1] - 1.0f) < 1e-4f, "terrain flat normal up");
// 抬高点:1000 高度差 * 0.5 * 0.01 = 5m 峰
size_t p = (size_t(10) * 129 + 10) * 3;
CHECK(std::fabs(tm.positions[p + 1] - 10.0f) < 1e-4f, "terrain bump height 10m");
// 峰邻域法线不再朝正上
size_t q = (size_t(10) * 129 + 9) * 3;
CHECK(tm.normals[q + 1] < 0.999f, "terrain slope normal tilted");
// 区块原点:tile(1,2) 的 (0,0) 顶点应在 (128*2, _, 256*2) m
fmt::TerrainMesh tm2;
fmt::build_terrain_mesh(hm, 1, 2, 0.5, tm2);
CHECK(std::fabs(tm2.positions[0] - 256.0f) < 1e-3f, "terrain chunk origin X = tile_x*256");
CHECK(std::fabs(tm2.positions[2] - 512.0f) < 1e-3f, "terrain chunk origin Z = tile_y*256");
// GetHeight 三角插值:格心
double h = fmt::terrain_height_at(hm, 100.0, 100.0, 0.5); // cell (0,0), xdist=ydist=100
CHECK(std::fabs(h - 500.0) < 1.0, "terrain_height_at flat = 500cm");
}
static void test_splat() {
// 3 层棋盘:texel 值 1 / 2 / 3
fmt::TileMap tile;
const int S = fmt::SPLAT_RAW_XY;
tile.raw.assign(size_t(S) * S, 1);
for (int y = 0; y < S; ++y)
for (int x = 0; x < S; ++x) {
if (x > S / 2) tile.raw[y * S + x] = 2;
if (x > S / 2 && y > S / 2) tile.raw[y * S + x] = 3;
}
fmt::SplatSet ss;
fmt::build_splat(tile, 4, ss);
CHECK(ss.layers.size() == 3, "splat: 3 used layers");
CHECK(ss.layers[0].layer == 1 && ss.layers[2].layer == 3, "splat: layers sorted by index");
// 图层 1 在左半区应满覆盖
CHECK(ss.layers[0].alpha[10 * S + 10] == 0xFF, "splat L1 covers its region");
CHECK(ss.layers[0].alpha[10 * S + (S - 10)] == 0x00, "splat L1 zero outside (no bleed there)");
// 图层 2 在右半区覆盖;边界 1px 会羽化进 L3 区
CHECK(ss.layers[1].alpha[10 * S + (S - 10)] == 0xFF, "splat L2 covers its region");
CHECK(ss.layers[2].coverage > 0, "splat L3 has coverage");
// 未出现的图层不生成
fmt::TileMap flat;
flat.raw.assign(size_t(S) * S, 1);
fmt::SplatSet ss2;
fmt::build_splat(flat, 8, ss2);
CHECK(ss2.layers.size() == 1, "splat: only used layers emitted");
}
static void test_asset_resolver_norm() {
using fmt::AssetResolver;
CHECK(AssetResolver::normalize("d:\\ymir work\\Tree\\A.spt") == "ymir work/tree/a.spt",
"resolver normalize backslash+drive+case");
CHECK(AssetResolver::normalize("D:/ymir work/env/./x/../y.tga") == "ymir work/env/y.tga",
"resolver normalize . and ..");
CHECK(AssetResolver::normalize("/foo//bar") == "foo/bar", "resolver normalize // and lead /");
}
static void test_spt_tokens() {
std::string b;
const uint32_t h = 0;
const uint32_t n = 13;
b.append(reinterpret_cast<const char*>(&h), sizeof(h));
b.append(reinterpret_cast<const char*>(&n), sizeof(n));
b.append("__IdvSpt_02_", n);
b.push_back('\0');
b += "\\\\PagodaTreeBark.tga";
b.push_back('\0');
b += "CompositeShadowMapB1.tgaQF";
b.push_back('\0');
b += "CompositeMapB1#N";
fmt::SptInfo si;
CHECK(fmt::sniff_spt(b, si) && si.ok, "spt inline magic");
CHECK(si.composite_texture == "CompositeMapB1.dds", "spt composite atlas token");
CHECK(si.self_shadow_texture == "CompositeShadowMapB1.dds", "spt shadow atlas token");
}
// ---- live A1 断言(M2_ASSETS 设了才跑)----
static void test_live(const std::string& assets) {
const std::string map = assets + "/OutdoorA1/metin2_map_a1";
std::string err;
fmt::MapSetting s;
CHECK(fmt::parse_map_setting_file(map + "/setting.txt", s, &err),
("live setting.txt: " + err).c_str());
CHECK(s.map_size_x == 4 && s.map_size_y == 5, "live MapSize 4x5");
CHECK(s.chunk_count() == 20, "live 20 chunks");
// 20 个区块目录都在
int found = 0;
for (int tx = 0; tx < s.map_size_x; ++tx)
for (int ty = 0; ty < s.map_size_y; ++ty) {
fmt::HeightMap hm;
std::string e2;
if (fmt::load_height_map(map + "/" + fmt::m2coord::tile_dir(tx, ty) + "/height.raw",
hm, &e2))
++found;
else
std::fprintf(stderr, " tile %s: %s\n",
fmt::m2coord::tile_dir(tx, ty).c_str(), e2.c_str());
}
CHECK(found == 20, "live: all 20 height.raw load as 131x131");
fmt::TileMap tm;
CHECK(fmt::load_tile_map(map + "/000000/tile.raw", tm, &err),
("live tile.raw 258x258: " + err).c_str());
fmt::AttrMap am;
CHECK(fmt::load_attr_map(map + "/000000/attr.atr", am, &err),
("live attr.atr magic 2634: " + err).c_str());
fmt::WaterMap wm;
CHECK(fmt::load_water_map(map + "/000000/water.wtr", wm, &err),
("live water.wtr magic 5426: " + err).c_str());
// TextureSet:解析路径 textureset\metin2_A1.txt(大小写不敏感 -> 实盘小写)
fmt::TextureSet ts;
bool got_ts = fmt::parse_texture_set_file(
assets + "/textureset/textureset/metin2_a1.txt", ts, &err);
CHECK(got_ts, ("live textureset: " + err).c_str());
if (got_ts) CHECK(ts.layers.size() == 17, "live TextureSet 17 layers");
fmt::AreaData ad;
CHECK(fmt::parse_area_data_file(map + "/000000/areadata.txt", ad, &err),
("live areadata: " + err).c_str());
CHECK(ad.declared_count == 23, "live areadata ObjectCount 23");
CHECK(ad.objects.size() == 23, "live areadata parsed 23");
if (!ad.objects.empty())
CHECK(ad.objects[0].crc == 569394331u, "live areadata Object000 CRC 569394331");
// 坐标系:000000 在原点附近,x,y 幅度 < 一个区块(全局 == 本地在此区块巧合)
bool o000_ok = true;
for (auto& o : ad.objects)
if (o.x < 0 || o.x >= fmt::m2coord::CHUNK_CM || o.y > 0 || o.y <= -fmt::m2coord::CHUNK_CM)
o000_ok = false;
CHECK(o000_ok, "live 000000 areadata within chunk 0 (global cm, x>=0 y<=0)");
// 别的区块要落在该区块的全局范围 —— 001003 的 x 应 > 25600
fmt::AreaData ad13;
if (fmt::parse_area_data_file(map + "/001003/areadata.txt", ad13, &err) && !ad13.objects.empty()) {
bool shifted = false;
for (auto& o : ad13.objects)
if (o.x >= fmt::m2coord::CHUNK_CM) shifted = true;
CHECK(shifted, "live: 001003 objects are GLOBAL cm (x > 25600), not chunk-local");
}
// PARITY §3.7 —— 相邻区块共享边高度 / 顶点位置一致(无接缝)
{
fmt::HeightMap h00, h10;
std::string e3;
bool ok00 = fmt::load_height_map(map + "/000000/height.raw", h00, &e3);
bool ok10 = fmt::load_height_map(map + "/001000/height.raw", h10, &e3);
CHECK(ok00 && ok10, "seam: load 000000 + 001000 height.raw");
if (ok00 && ok10) {
int bad = 0;
for (int j = 0; j < fmt::TerrainMesh::VERTS_XY; ++j)
if (h00.at(128, j) != h10.at(0, j)) // 000000 右缘 vs 001000 左缘
++bad;
CHECK(bad == 0, "seam: 000000 col-128 heights == 001000 col-0 (border samples)");
fmt::TerrainMesh m00, m10;
fmt::build_terrain_mesh(h00, 0, 0, s.height_scale, m00);
fmt::build_terrain_mesh(h10, 1, 0, s.height_scale, m10);
const int N = fmt::TerrainMesh::VERTS_XY;
int pbad = 0;
for (int j = 0; j < N; ++j) {
size_t a = (size_t(j) * N + (N - 1)) * 3; // m00 右缘
size_t b = (size_t(j) * N + 0) * 3; // m10 左缘
for (int k = 0; k < 3; ++k)
if (std::fabs(m00.positions[a + k] - m10.positions[b + k]) > 1e-3f)
++pbad;
}
CHECK(pbad == 0, "seam: 000000/001000 shared-edge vertex positions match");
}
}
// .spt 嗅探(几何不可读,仅确认 magic + 抽贴图名 —— W4 结论 c 的程序化确认)
{
fmt::SptInfo si;
bool got = fmt::sniff_spt_file(
assets + "/Tree/ymir work/tree/b1_pagodatree_rt.spt", si);
CHECK(got && si.ok, "live .spt magic __IdvSpt");
CHECK(si.magic.rfind("__IdvSpt", 0) == 0, "live .spt magic string");
CHECK(!si.texture_refs.empty(), "live .spt has embedded texture refs");
CHECK(si.composite_texture == "CompositeMapB1.dds", "live .spt composite atlas");
CHECK(si.self_shadow_texture == "CompositeShadowMapB1.dds", "live .spt shadow atlas");
}
// EnvironmentA1.msenv 由虚拟路径解析
fmt::Environment env;
CHECK(fmt::parse_environment_file(assets + "/ETC/ymir work/environment/a1.msenv", env, &err),
("live a1.msenv: " + err).c_str());
CHECK(env.script_type == "EnvrionmentData", "live msenv script_type");
CHECK(env.fog.enable && env.sky.gradient.size() >= 8, "live msenv fog + gradient list");
// Property CRC 注册表
fmt::PropertyRegistry reg;
CHECK(reg.scan(assets + "/Property", &err), ("live Property scan: " + err).c_str());
CHECK(reg.by_crc.size() > 1000, "live Property registry > 1000 entries");
const fmt::Property* p0 = reg.find(569394331u);
CHECK(p0 != nullptr, "live Property CRC 569394331 resolves");
if (p0) CHECK(p0->type == fmt::PropertyType::Tree, "live CRC 569394331 = Tree (Pagoda1)");
// areadata 的每个 CRC 都能在注册表命中
int miss = 0;
for (auto& o : ad.objects)
if (!reg.find(o.crc)) ++miss;
CHECK(miss == 0, "live: every 000000 areadata CRC found in Property registry");
// AssetResolver
fmt::AssetResolver res;
CHECK(res.build(assets, fmt::AssetResolver::default_priority(), &err),
("live AssetResolver build: " + err).c_str());
CHECK(res.files_indexed > 10000, "live resolver indexed > 10000 files");
std::string r;
CHECK(!res.resolve("d:/ymir work/environment/clouds_zone01.tga", &r).empty(),
("live resolve cloud tga: " + r).c_str());
CHECK(!res.resolve("d:\\ymir work\\tree\\b1_pagodatree_rt.spt", &r).empty(),
("live resolve pagoda spt: " + r).c_str());
CHECK(res.resolve("d:/ymir work/does/not/exist.xxx", &r).empty(),
"live resolve missing -> empty");
// TextureSet 里的图层贴图路径应可解析
int tex_miss = 0;
for (auto& L : ts.layers)
if (res.resolve(L.texture, nullptr).empty()) ++tex_miss;
CHECK(tex_miss == 0, "live: all TextureSet layer textures resolve");
}
int main() {
test_tokvec();
test_coord();
test_map_setting();
test_texture_set();
test_area_data();
test_environment();
test_property();
test_terrain_mesh();
test_asset_resolver_norm();
test_spt_tokens();
if (const char* a = std::getenv("M2_ASSETS"); a && *a) {
std::fprintf(stderr, "-- live A1 asserts (M2_ASSETS=%s) --\n", a);
test_live(a);
} else {
std::fprintf(stderr, "-- M2_ASSETS unset: skipping live A1 asserts --\n");
}
if (g_fail) {
std::fprintf(stderr, "%d check(s) failed\n", g_fail);
return 1;
}
std::fprintf(stderr, "all checks passed\n");
return 0;
}