完善客户端功能并同步差距文档
This commit is contained in:
+133
-26
@@ -45,6 +45,7 @@
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
|
||||
using namespace godot;
|
||||
|
||||
@@ -90,9 +91,14 @@ void Metin2World::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("sample_height", "gx_m", "gz_m"), &Metin2World::sample_height);
|
||||
ClassDB::bind_method(D_METHOD("sample_attribute", "gx_m", "gz_m"), &Metin2World::sample_attribute);
|
||||
ClassDB::bind_method(D_METHOD("is_blocked", "gx_m", "gz_m"), &Metin2World::is_blocked);
|
||||
ClassDB::bind_method(D_METHOD("get_fishing_rotation", "gx_m", "gz_m", "heading_deg"),
|
||||
&Metin2World::get_fishing_rotation);
|
||||
ClassDB::bind_method(D_METHOD("can_fishing_position", "gx_m", "gz_m", "heading_deg"),
|
||||
&Metin2World::can_fishing_position);
|
||||
ClassDB::bind_method(D_METHOD("load_dds", "path"), &Metin2World::load_dds);
|
||||
ClassDB::bind_method(D_METHOD("chunk_dir", "tx", "ty"), &Metin2World::chunk_dir);
|
||||
ClassDB::bind_method(D_METHOD("get_load_report"), &Metin2World::get_load_report);
|
||||
ClassDB::bind_method(D_METHOD("get_ambience_sources"), &Metin2World::get_ambience_sources);
|
||||
ClassDB::bind_method(D_METHOD("bake_asset_index", "out_path"), &Metin2World::bake_asset_index);
|
||||
|
||||
ADD_PROPERTY(PropertyInfo(Variant::STRING, "assets_root", PROPERTY_HINT_GLOBAL_DIR),
|
||||
@@ -248,6 +254,7 @@ bool Metin2World::build_chunk(int tx, int ty) {
|
||||
|
||||
MeshInstance3D *pm = memnew(MeshInstance3D);
|
||||
pm->set_name(Pn > 1 ? vformat("Terrain_%d_%d", pi, pj) : String("Terrain"));
|
||||
pm->set_layer_mask(1u << 0); // Background light visibility layer.
|
||||
pm->set_mesh(pmesh);
|
||||
if (Pn > 1 && terrain_patch_view > 0.0f) {
|
||||
// 硬剔除(无半透明淡出),避免远景地形变透明
|
||||
@@ -299,6 +306,7 @@ bool Metin2World::build_chunk(int tx, int ty) {
|
||||
continue;
|
||||
MeshInstance3D *w = memnew(MeshInstance3D);
|
||||
w->set_name("Water");
|
||||
w->set_layer_mask(1u << 0);
|
||||
w->set_mesh(p.mesh);
|
||||
croot->add_child(w);
|
||||
++water_pieces;
|
||||
@@ -314,6 +322,8 @@ bool Metin2World::build_chunk(int tx, int ty) {
|
||||
ck.root = croot;
|
||||
if (objects_enabled && registry_ok && resolver)
|
||||
place_chunk_objects(tx, ty, croot, ck.objects, ck.trees);
|
||||
if (registry_ok)
|
||||
place_chunk_ambience(tx, ty);
|
||||
objects_placed += ck.objects;
|
||||
trees_placed += ck.trees;
|
||||
chunks.push_back(std::move(ck));
|
||||
@@ -400,6 +410,7 @@ void Metin2World::place_chunk_objects(int tx, int ty, Node3D *root, int &n_obj,
|
||||
|
||||
MeshInstance3D *mi = memnew(MeshInstance3D);
|
||||
mi->set_name(String(p->name.c_str()) + "_" + String::num_uint64(o.crc));
|
||||
mi->set_layer_mask(1u << 0); // Background light visibility layer.
|
||||
mi->set_mesh(mesh);
|
||||
mi->set_transform(xform);
|
||||
// ShadowFlag: 客户端把 isShadowFlag 物体丢进动态阴影贴图(= 我们的实时投影);
|
||||
@@ -462,6 +473,7 @@ void Metin2World::place_chunk_objects(int tx, int ty, Node3D *root, int &n_obj,
|
||||
mm->set_instance_transform(i, kv.second.xforms[i]);
|
||||
MultiMeshInstance3D *mmi = memnew(MultiMeshInstance3D);
|
||||
mmi->set_name(String("Trees_") + String(kv.first.c_str()).get_file().get_basename());
|
||||
mmi->set_layer_mask(1u << 0);
|
||||
mmi->set_multimesh(mm);
|
||||
// §3.3: 树影已烘焙进 shadowmap.dds;proxy 几何的实时投影形状也不对 -> 默认不投。
|
||||
mmi->set_cast_shadows_setting(tree_shadows ? GeometryInstance3D::SHADOW_CASTING_SETTING_ON
|
||||
@@ -474,6 +486,46 @@ void Metin2World::place_chunk_objects(int tx, int ty, Node3D *root, int &n_obj,
|
||||
}
|
||||
}
|
||||
|
||||
void Metin2World::place_chunk_ambience(int tx, int ty) {
|
||||
// Area ambience is a separate file in ClientVS22. The .pra property owns
|
||||
// play type / interval / sound names; Audio owns the runtime instances.
|
||||
const std::string mdir = std::string(map_dir().utf8().get_data());
|
||||
fmt::AreaAmbienceData ad;
|
||||
std::string e;
|
||||
const std::string base = mdir + "/" + fmt::m2coord::tile_dir(tx, ty) + "/";
|
||||
bool loaded = fmt::parse_area_ambience_file(base + "areaambiencedata.txt", ad, &e);
|
||||
if (!loaded)
|
||||
loaded = fmt::parse_area_ambience_file(base + "AreaAmbienceData.txt", ad, &e);
|
||||
if (!loaded)
|
||||
return; // 老地图可能没有环境声文件。
|
||||
|
||||
for (size_t i = 0; i < ad.objects.size(); ++i) {
|
||||
const fmt::AreaAmbienceObject &o = ad.objects[i];
|
||||
const fmt::Property *p = registry.find(o.crc);
|
||||
if (!p || p->type != fmt::PropertyType::Ambience)
|
||||
continue;
|
||||
AmbienceSource source;
|
||||
source.tile_x = tx;
|
||||
source.tile_y = ty;
|
||||
source.object_index = (int)i;
|
||||
const fmt::m2coord::Vec3 g = fmt::m2coord::position_to_godot(o.x, o.y, o.z);
|
||||
source.position = Vector3((float)g.x, (float)g.y, (float)g.z);
|
||||
source.range_cm = std::max(0, (int)std::lround(o.range));
|
||||
source.max_volume_area_percentage = (float)std::strtof(
|
||||
p->get("maxvolumeareapercentage", "0").c_str(), nullptr);
|
||||
source.play_interval = (float)std::strtof(
|
||||
p->get("playinterval", "0").c_str(), nullptr);
|
||||
source.play_interval_variation = (float)std::strtof(
|
||||
p->get("playintervalvariation", "0").c_str(), nullptr);
|
||||
source.play_type = p->get("playtype", "LOOP").c_str();
|
||||
const std::string sounds = p->get("ambiencesoundvector");
|
||||
if (!sounds.empty())
|
||||
source.sounds.push_back(sounds);
|
||||
if (!source.sounds.empty() && source.range_cm > 0)
|
||||
ambience_sources.push_back(std::move(source));
|
||||
}
|
||||
}
|
||||
|
||||
bool Metin2World::load_map() {
|
||||
unload_map();
|
||||
|
||||
@@ -487,39 +539,35 @@ bool Metin2World::load_map() {
|
||||
}
|
||||
setting_ok = true;
|
||||
|
||||
// splat 前置:TextureSet + AssetResolver(整盘扫描,一次)
|
||||
// AssetResolver 是地图对象、环境声属性和 splat 共用的资源索引。
|
||||
// 即使关闭 splat,也要保留这条链路,避免环境声依赖渲染开关。
|
||||
splat_ready = false;
|
||||
if (splat_enabled) {
|
||||
const std::string root = std::string(assets_root.utf8().get_data());
|
||||
resolver = std::make_shared<fmt::AssetResolver>();
|
||||
std::string re;
|
||||
// asset_index.txt 存在就装载(PCK/移动端必走),否则扫盘(桌面开发)。
|
||||
if (!resolver->build_or_load(root, fmt::AssetResolver::default_priority(), &re)) {
|
||||
UtilityFunctions::push_warning(String("[Metin2World] AssetResolver: ") + re.c_str());
|
||||
resolver.reset();
|
||||
}
|
||||
if (splat_enabled && resolver) {
|
||||
std::string ts_rel = setting.texture_set;
|
||||
for (char &c : ts_rel) {
|
||||
if (c == '\\')
|
||||
c = '/';
|
||||
c = (char)std::tolower((unsigned char)c);
|
||||
}
|
||||
const std::string root = std::string(assets_root.utf8().get_data());
|
||||
std::string tse;
|
||||
if (fmt::parse_texture_set_file(root + "/textureset/" + ts_rel, texture_set, &tse)) {
|
||||
resolver = std::make_shared<fmt::AssetResolver>();
|
||||
std::string re;
|
||||
// asset_index.txt 存在就装载(PCK/移动端必走),否则扫盘(桌面开发)。
|
||||
if (resolver->build_or_load(root, fmt::AssetResolver::default_priority(), &re)) {
|
||||
splat_ready = true;
|
||||
} else {
|
||||
UtilityFunctions::push_warning(String("[Metin2World] AssetResolver: ") + re.c_str());
|
||||
resolver.reset();
|
||||
}
|
||||
} else {
|
||||
if (fmt::parse_texture_set_file(root + "/textureset/" + ts_rel, texture_set, &tse))
|
||||
splat_ready = true;
|
||||
else
|
||||
UtilityFunctions::push_warning(String("[Metin2World] TextureSet: ") + tse.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Property CRC 注册表(一次;splat 已建 resolver)
|
||||
// Property CRC 注册表(对象和环境声共用;不依赖 objects_enabled)。
|
||||
registry_ok = false;
|
||||
if (objects_enabled) {
|
||||
const std::string root(assets_root.utf8().get_data());
|
||||
std::string re;
|
||||
// resolver 已建(splat 阶段):用它的文件清单,避免再 std::filesystem 扫盘
|
||||
// (PCK 里扫不了)。没 resolver 时退回目录递归。
|
||||
{
|
||||
// resolver 已建:用它的文件清单,避免再 std::filesystem 扫盘(PCK 里扫不了)。
|
||||
bool ok = resolver
|
||||
? registry.scan_list(root, resolver->all_rel(), &re)
|
||||
: registry.scan(root + "/Property", &re);
|
||||
@@ -554,12 +602,9 @@ bool Metin2World::load_map() {
|
||||
if (rp.empty())
|
||||
rp = std::string(assets_root.utf8().get_data()) +
|
||||
"/ETC/ymir work/environment/" + setting.environment;
|
||||
// 小写文件名
|
||||
for (size_t i = rp.rfind('/') + 1; i < rp.size(); ++i)
|
||||
rp[i] = (char)std::tolower((unsigned char)rp[i]);
|
||||
std::string ee;
|
||||
if (fmt::parse_environment_file(rp, env, &ee)) {
|
||||
apply_environment(env, this);
|
||||
apply_environment(env, this, resolver.get());
|
||||
env_ok = true;
|
||||
} else {
|
||||
UtilityFunctions::push_warning(String("[Metin2World] .msenv: ") + ee.c_str());
|
||||
@@ -588,6 +633,9 @@ void Metin2World::unload_chunk(int idx) {
|
||||
--chunks_built;
|
||||
if (c.root)
|
||||
c.root->queue_free();
|
||||
ambience_sources.erase(std::remove_if(ambience_sources.begin(), ambience_sources.end(),
|
||||
[&c](const AmbienceSource &s) { return s.tile_x == c.tx && s.tile_y == c.ty; }),
|
||||
ambience_sources.end());
|
||||
chunks.erase(chunks.begin() + idx);
|
||||
}
|
||||
|
||||
@@ -637,11 +685,12 @@ void Metin2World::unload_map() {
|
||||
while (!chunks.empty())
|
||||
unload_chunk((int)chunks.size() - 1);
|
||||
stream_queue.clear();
|
||||
ambience_sources.clear();
|
||||
if (objects_root) {
|
||||
objects_root->queue_free();
|
||||
objects_root = nullptr;
|
||||
}
|
||||
for (const char *nm : {"Sun", "WorldEnv"})
|
||||
for (const char *nm : {"Sun", "CharacterLight", "WorldEnv"})
|
||||
if (Node *n = get_node_or_null(NodePath(nm)))
|
||||
n->queue_free();
|
||||
water_pieces = 0;
|
||||
@@ -736,6 +785,42 @@ int Metin2World::sample_attribute(double gx_m, double gz_m) const {
|
||||
return c->am->data[size_t(ay) * fmt::ATTRMAP_XY + ax];
|
||||
}
|
||||
|
||||
double Metin2World::get_fishing_rotation(double gx_m, double gz_m, double heading_deg) const {
|
||||
// InstanceBaseMotion.cpp uses c_fFishingDistance=600.0f and
|
||||
// ELPlainCoord_GetRotatedPixelPosition (sin(rot), cos(rot)). The POC
|
||||
// world frame is the MapCoord frame, where heading -> world yaw is
|
||||
// heading + 90 degrees; using the same conversion keeps the probe in
|
||||
// the coordinate frame used by player_controller / MapCoord.
|
||||
constexpr double DISTANCE_M = 6.0;
|
||||
constexpr double WATER_ATTRIBUTE = 1 << 1; // PRTerrainLib::ATTRIBUTE_WATER
|
||||
constexpr double DEG_TO_RAD = 3.14159265358979323846 / 180.0;
|
||||
auto normalized = [](double d) {
|
||||
d = std::fmod(d, 360.0);
|
||||
return d < 0.0 ? d + 360.0 : d;
|
||||
};
|
||||
auto water_at = [&](double candidate_heading) {
|
||||
const double yaw = (candidate_heading + 90.0) * DEG_TO_RAD;
|
||||
const double x = gx_m + std::sin(yaw) * DISTANCE_M;
|
||||
const double z = gz_m + std::cos(yaw) * DISTANCE_M;
|
||||
return (sample_attribute(x, z) & int(WATER_ATTRIBUTE)) != 0;
|
||||
};
|
||||
|
||||
for (int step = 0; step <= 18; ++step) {
|
||||
const double offset = double(step * 10);
|
||||
const double right = heading_deg + offset;
|
||||
if (water_at(right))
|
||||
return normalized(right);
|
||||
const double left = heading_deg - offset;
|
||||
if (water_at(left))
|
||||
return normalized(left);
|
||||
}
|
||||
return -1.0;
|
||||
}
|
||||
|
||||
bool Metin2World::can_fishing_position(double gx_m, double gz_m, double heading_deg) const {
|
||||
return get_fishing_rotation(gx_m, gz_m, heading_deg) >= 0.0;
|
||||
}
|
||||
|
||||
Dictionary Metin2World::get_load_report() const {
|
||||
Dictionary d;
|
||||
d["map_path"] = map_path;
|
||||
@@ -756,6 +841,7 @@ Dictionary Metin2World::get_load_report() const {
|
||||
d["objects_placed"] = objects_placed;
|
||||
d["objects_skipped"] = objects_skipped;
|
||||
d["objects_missing_model"] = objects_missing_model;
|
||||
d["ambience_sources"] = (int)ambience_sources.size();
|
||||
d["trees_placed"] = trees_placed;
|
||||
d["tree_species"] = tree_species;
|
||||
d["objects_mdatr_pending"] = objects_mdatr_pending; // 有 .mdatr 但未建碰撞
|
||||
@@ -768,6 +854,27 @@ Dictionary Metin2World::get_load_report() const {
|
||||
return d;
|
||||
}
|
||||
|
||||
Array Metin2World::get_ambience_sources() const {
|
||||
Array out;
|
||||
for (const AmbienceSource &s : ambience_sources) {
|
||||
Dictionary d;
|
||||
d["key"] = String::num_int64(s.tile_x) + ":" + String::num_int64(s.tile_y) + ":" +
|
||||
String::num_int64(s.object_index);
|
||||
d["position"] = s.position;
|
||||
d["range_cm"] = s.range_cm;
|
||||
d["max_volume_area_percentage"] = s.max_volume_area_percentage;
|
||||
d["play_interval"] = s.play_interval;
|
||||
d["play_interval_variation"] = s.play_interval_variation;
|
||||
d["play_type"] = s.play_type;
|
||||
Array sounds;
|
||||
for (const std::string &sound : s.sounds)
|
||||
sounds.push_back(String(sound.c_str()));
|
||||
d["sounds"] = sounds;
|
||||
out.push_back(d);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Dictionary Metin2World::get_perf() const {
|
||||
Dictionary d;
|
||||
Performance *pf = Performance::get_singleton();
|
||||
|
||||
Reference in New Issue
Block a user