Files
mtgodot-poc/formats/environment.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

205 lines
7.8 KiB
C++

#include "environment.h"
#include "m2_tokvec.h" // read_file
#include <cctype>
#include <cstdlib>
#include <cstring>
#include <sstream>
namespace fmt {
namespace {
bool ieq(const std::string& a, const char* b) {
if (a.size() != std::strlen(b)) return false;
for (size_t i = 0; i < a.size(); ++i)
if (std::tolower((unsigned char)a[i]) != std::tolower((unsigned char)b[i])) return false;
return true;
}
std::vector<std::string> tok_line(const std::string& line) {
std::vector<std::string> out;
size_t i = 0, n = line.size();
while (i < n) {
char c = line[i];
if (c == '#' || (c == '/' && i + 1 < n && line[i + 1] == '/')) break;
if (std::isspace((unsigned char)c)) { ++i; continue; }
if (c == '{' || c == '}') { out.emplace_back(1, c); ++i; continue; }
if (c == '"') {
++i;
std::string s;
while (i < n && line[i] != '"') s += line[i++];
if (i < n) ++i;
out.push_back(s);
continue;
}
std::string s;
while (i < n && !std::isspace((unsigned char)line[i]) && line[i] != '{' && line[i] != '}')
s += line[i++];
out.push_back(s);
}
return out;
}
struct Block {
std::string name;
bool is_list = false;
std::vector<std::vector<std::string>> lines; // kv lines (lines[k][0] = key)
std::vector<std::vector<float>> rows; // List rows
std::vector<Block> children;
const std::vector<std::string>* kv(const char* key) const {
for (auto& l : lines)
if (!l.empty() && ieq(l[0], key)) return &l;
return nullptr;
}
const Block* child(const char* nm) const {
for (auto& c : children)
if (ieq(c.name, nm)) return &c;
return nullptr;
}
float f(const char* key, int idx, float def = 0) const {
auto* l = kv(key);
return (l && (int)l->size() > idx) ? (float)std::atof((*l)[idx].c_str()) : def;
}
int i(const char* key, int def = 0) const {
auto* l = kv(key);
return (l && l->size() > 1) ? std::atoi((*l)[1].c_str()) : def;
}
std::string s(const char* key, const std::string& def = "") const {
auto* l = kv(key);
return (l && l->size() > 1) ? (*l)[1] : def;
}
};
bool parse_tree(const std::string& text, Block& root, std::string* err) {
std::istringstream in(text);
std::string line;
std::vector<Block*> stack{&root};
std::string pending; // name of a Group/List awaiting '{'
bool pending_list = false;
while (std::getline(in, line)) {
if (!line.empty() && line.back() == '\r') line.pop_back();
auto t = tok_line(line);
for (size_t k = 0; k < t.size(); ++k) {
const std::string& x = t[k];
if (x == "{") {
Block b;
b.name = pending;
b.is_list = pending_list;
stack.back()->children.push_back(std::move(b));
stack.push_back(&stack.back()->children.back());
pending.clear();
pending_list = false;
} else if (x == "}") {
if (stack.size() <= 1) { if (err) *err = "msenv: 多余的 }"; return false; }
stack.pop_back();
} else if (ieq(x, "Group") || ieq(x, "List")) {
pending_list = ieq(x, "List");
pending = (k + 1 < t.size()) ? t[k + 1] : "";
++k;
} else {
// 一行剩余 token 作为一条记录
std::vector<std::string> rec(t.begin() + k, t.end());
if (stack.back()->is_list) {
std::vector<float> row;
for (auto& v : rec) row.push_back((float)std::atof(v.c_str()));
stack.back()->rows.push_back(std::move(row));
} else {
stack.back()->lines.push_back(std::move(rec));
}
break; // 行内其余 token 已并入本记录
}
}
}
if (stack.size() != 1) { if (err) *err = "msenv: 块未闭合"; return false; }
return true;
}
Rgba rgba(const std::vector<std::string>* l, int off = 1) {
Rgba c{{0, 0, 0, 1}};
if (l)
for (int k = 0; k < 4 && off + k < (int)l->size(); ++k)
c[k] = (float)std::atof((*l)[off + k].c_str());
return c;
}
Rgba row_rgba(const std::vector<float>& r) {
Rgba c{{0, 0, 0, 0}};
for (int k = 0; k < 4 && k < (int)r.size(); ++k) c[k] = r[k];
return c;
}
} // namespace
bool parse_environment(const std::string& text, Environment& out, std::string* err) {
Block root;
if (!parse_tree(text, root, err)) return false;
out = Environment{};
if (auto* l = root.kv("ScriptType")) out.script_type = l->size() > 1 ? (*l)[1] : "";
out.script_version = root.f("ScriptVersion", 1);
if (const Block* dl = root.child("DirectionalLight")) {
if (auto* d = dl->kv("Direction"))
for (int k = 0; k < 3 && k + 1 < (int)d->size(); ++k)
out.dir_light.direction[k] = (float)std::atof((*d)[k + 1].c_str());
if (const Block* bg = dl->child("Background")) {
out.dir_light.bg_enable = bg->i("Enable") != 0;
out.dir_light.bg_diffuse = rgba(bg->kv("Diffuse"));
out.dir_light.bg_ambient = rgba(bg->kv("Ambient"));
}
if (const Block* ch = dl->child("Character")) {
out.dir_light.ch_enable = ch->i("Enable") != 0;
out.dir_light.ch_diffuse = rgba(ch->kv("Diffuse"));
out.dir_light.ch_ambient = rgba(ch->kv("Ambient"));
}
}
if (const Block* m = root.child("Material")) {
out.material.diffuse = rgba(m->kv("Diffuse"));
out.material.ambient = rgba(m->kv("Ambient"));
out.material.emissive = rgba(m->kv("Emissive"));
}
if (const Block* fg = root.child("Fog")) {
out.fog.fog_level = fg->i("foglevel", 0);
out.fog.near_distance = fg->f("NearDistance", 1);
out.fog.far_distance = fg->f("FarDistance", 1);
out.fog.color = rgba(fg->kv("Color"));
out.fog.enable = (fg->kv("Enable") && fg->i("Enable") != 0) || out.fog.fog_level > 0;
}
if (const Block* sb = root.child("SkyBox")) {
for (int k = 0; k < 3; ++k) out.sky.scale[k] = sb->f("Scale", k + 1);
out.sky.gradient_level_upper = sb->i("GradientLevelUpper");
out.sky.gradient_level_lower = sb->i("GradientLevelLower");
for (int k = 0; k < 2; ++k) {
out.sky.cloud_scale[k] = sb->f("CloudScale", k + 1);
out.sky.cloud_texture_scale[k] = sb->f("CloudTextureScale", k + 1);
out.sky.cloud_speed[k] = sb->f("CloudSpeed", k + 1);
}
out.sky.cloud_height = sb->f("CloudHeight", 1);
out.sky.cloud_texture = sb->s("CloudTextureFileName");
if (const Block* cc = sb->child("CloudColor"))
for (auto& r : cc->rows) out.sky.cloud_color.push_back(row_rgba(r));
if (const Block* gr = sb->child("Gradient"))
for (auto& r : gr->rows) out.sky.gradient.push_back(row_rgba(r));
}
if (const Block* lf = root.child("LensFlare")) {
out.lens_flare.enable = lf->i("Enable") != 0;
out.lens_flare.brightness_color = rgba(lf->kv("BrightnessColor"));
out.lens_flare.max_brightness = lf->f("MaxBrightness", 1);
out.lens_flare.main_flare_enable = lf->i("MainFlareEnable") != 0;
out.lens_flare.main_flare_texture = lf->s("MainFlareTextureFileName");
out.lens_flare.main_flare_size = lf->f("MainFlareSize", 1);
}
return true;
}
bool parse_environment_file(const std::string& path, Environment& out, std::string* err) {
std::string text;
if (!read_file(path, text)) { if (err) *err = "打不开 " + path; return false; }
return parse_environment(text, out, err);
}
} // namespace fmt