Metin2 game client (P0–P11) + mobile asset pipeline

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

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

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

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

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EJxkHiNKS4kybHS3XKyAJ
This commit is contained in:
shen
2026-08-31 20:02:12 +09:00
co-authored by Claude Sonnet 5
parent f4917a2b3b
commit 47baf6c0c6
414 changed files with 69568 additions and 385 deletions
+202
View File
@@ -0,0 +1,202 @@
// map_probe — SHINSOO-WORLD-RENDERING.md W0 的 load report。
//
// map_probe <assets_root> [map_rel] map_rel 默认 OutdoorA1/metin2_map_a1
//
// 扫一张地图:setting.txt / TextureSet / 每个区块的 6 类文件 / areadata
// 逐项打印「成功 / 失败 + 原因」,末尾给汇总。不加载 .msenv / PropertyW0 剩余项)。
#include "area_data.h"
#include "asset_resolver.h"
#include "environment.h"
#include "m2_coord.h"
#include "map_setting.h"
#include "property.h"
#include "splat.h"
#include "terrain_files.h"
#include "texture_set.h"
#include <cstdio>
#include <cstdlib>
#include <map>
#include <set>
#include <string>
using namespace fmt;
static int g_ok = 0, g_bad = 0;
static void line(const char* tag, bool ok, const std::string& detail) {
std::printf(" [%s] %-28s %s\n", ok ? "OK " : "ERR", tag, detail.c_str());
ok ? ++g_ok : ++g_bad;
}
int main(int argc, char** argv) {
if (argc < 2) {
std::fprintf(stderr, "usage: map_probe <assets_root> [map_rel]\n");
return 2;
}
std::string assets = argv[1];
std::string map_rel = argc > 2 ? argv[2] : "OutdoorA1/metin2_map_a1";
std::string map = assets + "/" + map_rel;
std::printf("== map_probe: %s ==\n", map.c_str());
std::string err;
MapSetting s;
if (!parse_map_setting_file(map + "/setting.txt", s, &err)) {
line("setting.txt", false, err);
return 1;
}
line("setting.txt", true,
"MapSize " + std::to_string(s.map_size_x) + "x" + std::to_string(s.map_size_y) +
" CellScale " + std::to_string(s.cell_scale) + " HeightScale " +
std::to_string(s.height_scale) + " env=" + s.environment);
// TextureSetsetting 里是 `textureset\metin2_A1.txt`,实盘在 assets/textureset/textureset/ 且小写
{
std::string rel = s.texture_set;
for (auto& c : rel)
if (c == '\\') c = '/';
for (auto& c : rel) c = (char)std::tolower((unsigned char)c);
std::string p = assets + "/textureset/" + rel;
TextureSet ts;
if (parse_texture_set_file(p, ts, &err))
line("TextureSet", true,
std::to_string(ts.layers.size()) + " layers (+1 empty = runtime " +
std::to_string(ts.runtime_count()) + ")");
else
line("TextureSet", false, p + " : " + err);
}
// 每个区块
int chunks_found = 0, hm_ok = 0, tile_ok = 0, attr_ok = 0, water_ok = 0, area_ok = 0;
long total_objs = 0;
std::map<uint32_t, int> crc_hist;
for (int tx = 0; tx < s.map_size_x; ++tx) {
for (int ty = 0; ty < s.map_size_y; ++ty) {
std::string dir = map + "/" + m2coord::tile_dir(tx, ty);
std::string e;
HeightMap hm;
TileMap tm;
AttrMap am;
WaterMap wm;
AreaData ad;
bool any = false;
if (load_height_map(dir + "/height.raw", hm, &e)) { ++hm_ok; any = true; }
if (load_tile_map(dir + "/tile.raw", tm, &e)) ++tile_ok;
if (load_attr_map(dir + "/attr.atr", am, &e)) ++attr_ok;
if (load_water_map(dir + "/water.wtr", wm, &e)) ++water_ok;
if (parse_area_data_file(dir + "/areadata.txt", ad, &e)) {
++area_ok;
total_objs += (long)ad.objects.size();
for (auto& o : ad.objects) crc_hist[o.crc]++;
}
if (any) ++chunks_found;
}
}
int n = s.chunk_count();
line("chunk dirs", chunks_found == n,
std::to_string(chunks_found) + "/" + std::to_string(n));
line("height.raw", hm_ok == n, std::to_string(hm_ok) + "/" + std::to_string(n) + " as 131x131");
line("tile.raw", tile_ok == n, std::to_string(tile_ok) + "/" + std::to_string(n) + " as 258x258");
line("attr.atr", attr_ok == n, std::to_string(attr_ok) + "/" + std::to_string(n) + " magic 2634");
line("water.wtr", water_ok == n, std::to_string(water_ok) + "/" + std::to_string(n) + " magic 5426");
line("areadata.txt", area_ok == n, std::to_string(area_ok) + "/" + std::to_string(n) + " parsed");
std::printf(" ---- AreaData: %ld objects, %zu distinct Property CRCs ----\n",
total_objs, crc_hist.size());
// splat(区块 000000
{
TileMap tm;
std::string e;
if (load_tile_map(map + "/000000/tile.raw", tm, &e)) {
SplatSet ss;
build_splat(tm, 18, ss);
std::string used;
for (auto& L : ss.layers)
used += std::to_string(L.layer) + "(" + std::to_string(L.coverage) + ") ";
line("splat 000000", !ss.layers.empty(),
std::to_string(ss.layers.size()) + " used layers: " + used);
}
}
// .msenv(虚拟路径 environment\<name> -> ETC/ymir work/environment/<name>
Environment env;
{
std::string p = assets + "/ETC/ymir work/environment/" + s.environment;
for (auto& c : p) c = (c == '\\') ? '/' : (char)std::tolower((unsigned char)c);
// 只小写文件名部分即可,这里整体小写对 macOS/实盘均成立
std::string p2 = assets + "/ETC/ymir work/environment/" + s.environment;
std::string lp = p2;
for (auto& c : lp) c = (char)std::tolower((unsigned char)c);
if (parse_environment_file(lp, env, &err))
line(".msenv", true,
s.environment + " : fog=" + (env.fog.enable ? "on" : "off") + " gradient=" +
std::to_string(env.sky.gradient.size()) + " rows, cloudtex=" +
env.sky.cloud_texture);
else
line(".msenv", false, lp + " : " + err);
}
// Property CRC 注册表
PropertyRegistry reg;
if (reg.scan(assets + "/Property", &err)) {
std::string bytype;
for (auto& [k, v] : reg.type_counts) bytype += k + "=" + std::to_string(v) + " ";
line("Property registry", reg.parse_failed == 0,
std::to_string(reg.by_crc.size()) + " CRCs (" + std::to_string(reg.files_scanned) +
" files, " + std::to_string(reg.parse_failed) + " failed, " +
std::to_string(reg.crc_collisions) + " collisions) [" + bytype + "]");
// areadata 的 CRC 覆盖率
long hit = 0, total = 0;
std::set<uint32_t> missing;
for (auto& [crc, cnt] : crc_hist) {
total++;
if (reg.find(crc)) hit++;
else missing.insert(crc);
}
line("AreaData CRC coverage", missing.empty(),
std::to_string(hit) + "/" + std::to_string(total) + " distinct CRCs resolve" +
(missing.empty() ? "" : " (missing e.g. " + std::to_string(*missing.begin()) + ")"));
} else {
line("Property registry", false, err);
}
// AssetResolver
AssetResolver res;
if (res.build(assets, AssetResolver::default_priority(), &err)) {
line("AssetResolver", true,
std::to_string(res.files_indexed) + " files, " +
std::to_string(res.by_ymir.size()) + " ymir keys, " +
std::to_string(res.ymir_conflicts) + " conflicts");
// TextureSet 图层贴图解析率
TextureSet ts2;
std::string rel = s.texture_set, e3;
for (auto& c : rel) c = (c == '\\') ? '/' : (char)std::tolower((unsigned char)c);
if (parse_texture_set_file(assets + "/textureset/" + rel, ts2, &e3)) {
int miss = 0;
for (auto& L : ts2.layers)
if (res.resolve(L.texture, nullptr).empty()) ++miss;
line("TextureSet -> assets", miss == 0,
std::to_string(ts2.layers.size() - miss) + "/" +
std::to_string(ts2.layers.size()) + " layer textures resolve");
}
// Property 引用的模型文件解析率(buildingfile / treefile / effectfile
int pref = 0, pmiss = 0;
for (auto& [crc, p] : reg.by_crc) {
for (const char* k : {"buildingfile", "treefile", "effectfile", "modelfile"}) {
std::string v = p.get(k);
if (v.empty()) continue;
++pref;
if (res.resolve(v, nullptr).empty()) ++pmiss;
}
}
line("Property models -> assets", true,
std::to_string(pref - pmiss) + "/" + std::to_string(pref) +
" resolve (misses incl. .spt trees / missing packs)");
} else {
line("AssetResolver", false, err);
}
std::printf("\n== summary: %d ok, %d err ==\n", g_ok, g_bad);
return g_bad ? 1 : 0;
}