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,204 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user