40250 classic: parallel-dev checkpoint through increment 49 (W0 interface freeze)
Captures the uncommitted parallel-development work (increments 46-49) on the
40250 classic client port, folded into docs/CLIENT-GAP.md + CLIENT-GAP-FIX.md.
Increment 49 (W0 interface-freeze batch, Phase 1 prerequisite) lands the
cross-workflow shared-file scaffolding so the 4 Phase 1 workflows can run in
isolated worktrees without contention:
- entity_store.{h,cpp}: Entity gains empire/affect_flags/owner_vid/state_flags;
new mut_spawn_full() (single-shot two-packet merge, §2.3), mut_ownership()
(§2.5), mut_map_bgm() + take_bgm_dirty()/bgm_name()/bgm_volume() (§9.1),
drain_dirty() (§2.1/§2.5 bare-field-update queue); mut_char_info() now stores
empire; reset_for_map_change() clears m_dirty.
- m2_client.cpp: new bgm_changed(name, volume) signal; classic + m2dev pump
loops drain drain_dirty() -> entity_info and take_bgm_dirty() -> bgm_changed;
entity_dict() exposes the 4 new keys.
- classic/classic_parser.{h,cpp}: GC_MAIN_CHARACTER3_BGM /
GC_MAIN_CHARACTER4_BGM_VOL route bgm_name/bgm_volume to mut_map_bgm() (was
discarded); new m_pending_actor staging map (W1 fills §2.2 merge logic).
- project/bgm_director.gd (new) + game_scene.gd: BGM consumption split out of
net_world.gd so W2/W4 don't collide; §8.6/§4.8 keybind convergence (digits
1-4 -> quickslots 0-3, F1-F4 -> quickslots 4-7, Ctrl+1..9 -> _emote()).
Tests: cmake --build build clean; ctest 16/16; 15 GDScript regressions green
(netbridge, gamescene, netplay, p9, p10, p2b, p8, system_menu_ui, skill,
combat_fx, skill_fx, player_motion, char_status_ui, chat, inventory), no skips.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yvAPqPivoY7vmBbzgqK4W
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
70f200e885
commit
f32064c74a
@@ -20,6 +20,7 @@
|
||||
#include <m2_coord.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
using namespace godot;
|
||||
|
||||
@@ -36,6 +37,7 @@ void M2Client::_bind_methods() {
|
||||
&M2Client::connect_to_server);
|
||||
ClassDB::bind_method(D_METHOD("disconnect_from_server"), &M2Client::disconnect_from_server);
|
||||
ClassDB::bind_method(D_METHOD("select_character", "index"), &M2Client::select_character);
|
||||
ClassDB::bind_method(D_METHOD("enter_game", "index"), &M2Client::enter_game);
|
||||
ClassDB::bind_method(
|
||||
D_METHOD("create_character", "slot", "name", "job", "shape", "con", "int", "str", "dex"),
|
||||
&M2Client::create_character);
|
||||
@@ -363,6 +365,7 @@ void M2Client::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("get_stamina_state"), &M2Client::get_stamina_state);
|
||||
ADD_SIGNAL(MethodInfo("warp", PropertyInfo(Variant::VECTOR3, "pos"),
|
||||
PropertyInfo(Variant::BOOL, "same_server")));
|
||||
ADD_SIGNAL(MethodInfo("world_reset"));
|
||||
ADD_SIGNAL(MethodInfo("time_changed", PropertyInfo(Variant::INT, "server_epoch")));
|
||||
ADD_SIGNAL(MethodInfo("channel_changed", PropertyInfo(Variant::INT, "channel")));
|
||||
ADD_SIGNAL(MethodInfo("npc_marks_changed"));
|
||||
@@ -376,6 +379,10 @@ void M2Client::_bind_methods() {
|
||||
PropertyInfo(Variant::BOOL, "has_destination")));
|
||||
ADD_SIGNAL(MethodInfo("world_markers_changed"));
|
||||
ADD_SIGNAL(MethodInfo("mount_changed", PropertyInfo(Variant::INT, "vid")));
|
||||
// §9.1 map background music: name is the resolved track id/path, volume is
|
||||
// 0..1 or -1 when the server left it unspecified.
|
||||
ADD_SIGNAL(MethodInfo("bgm_changed", PropertyInfo(Variant::STRING, "name"),
|
||||
PropertyInfo(Variant::FLOAT, "volume")));
|
||||
}
|
||||
|
||||
static const char *phase_name(int p) {
|
||||
@@ -550,6 +557,17 @@ bool M2Client::select_character(int index) {
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool M2Client::enter_game(int index) {
|
||||
if (classic_sess) {
|
||||
const bool ok = classic_sess->connect_direct_enter(index);
|
||||
if (ok && index >= 0 && index < static_cast<int>(classic_sess->char_slots().size())) {
|
||||
selected_pid = classic_sess->char_slots()[index].id;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
return select_character(index);
|
||||
}
|
||||
|
||||
bool M2Client::create_character(int slot, const godot::String &name, int job, int shape,
|
||||
int con, int intel, int str, int dex) {
|
||||
if (classic_sess) {
|
||||
@@ -2232,6 +2250,10 @@ static Dictionary entity_dict(const mtnet::Entity &e) {
|
||||
d["guild"] = e.guild;
|
||||
d["alignment"] = (int)e.alignment;
|
||||
d["pk_mode"] = (int)e.pk_mode;
|
||||
d["empire"] = (int)e.empire;
|
||||
d["affect_flags"] = (int64_t)e.affect_flags;
|
||||
d["owner_vid"] = (int)e.owner_vid;
|
||||
d["state_flags"] = (int)e.state_flags;
|
||||
Array parts;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
parts.push_back((int)e.parts[i]);
|
||||
@@ -2734,7 +2756,15 @@ void M2Client::pump_classic() {
|
||||
}
|
||||
emit_signal("phase_changed", String(pn[idx]));
|
||||
if (classic_sess->stage() == St::Failed && stage != Stage::Failed) {
|
||||
emit_signal("login_failed", String(classic_sess->last_error().c_str()));
|
||||
// ClientVS22 sends every already-established game connection loss back
|
||||
// through SetLoginPhase(), regardless of whether SELECT was reached.
|
||||
// Authentication failures happen before this flag is set and remain
|
||||
// login_failed events.
|
||||
if (classic_sess->was_online_lost()) {
|
||||
emit_signal("disconnected", String(classic_sess->last_error().c_str()));
|
||||
} else {
|
||||
emit_signal("login_failed", String(classic_sess->last_error().c_str()));
|
||||
}
|
||||
set_stage(Stage::Failed);
|
||||
} else if (classic_sess->stage() == St::LoggingIn && stage == Stage::GameConnect) {
|
||||
set_stage(Stage::GameLogin);
|
||||
@@ -2806,6 +2836,17 @@ void M2Client::pump_classic() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// §2.5: bare field updates (ownership / affect flags) with no Spawn/Move/
|
||||
// Info change — re-mirror the node so presentation stays in sync.
|
||||
for (uint32_t vid : w.drain_dirty()) {
|
||||
if (const mtnet::Entity *e = w.get(vid)) {
|
||||
emit_signal("entity_info", (int)vid, entity_dict(*e));
|
||||
}
|
||||
}
|
||||
// §9.1: map background music.
|
||||
if (w.take_bgm_dirty()) {
|
||||
emit_signal("bgm_changed", String::utf8(w.bgm_name().c_str()), w.bgm_volume());
|
||||
}
|
||||
for (const auto &m : w.drain_chat()) {
|
||||
if (m.type == mtnet::CHAT_TYPE_WHISPER) {
|
||||
emit_signal("whisper_received", (int)m.sub, String::utf8(m.from.c_str()),
|
||||
@@ -3108,15 +3149,28 @@ void M2Client::pump_classic() {
|
||||
}
|
||||
for (const auto &warp : w.drain_warps()) {
|
||||
fmt::m2coord::Vec3 g = fmt::m2coord::position_to_godot(warp.x, warp.y, 0);
|
||||
emit_signal("warp", Vector3(g.x, g.y, g.z), warp.same_server());
|
||||
if (!warp.same_server()) {
|
||||
uint32_t addr = (uint32_t)warp.addr;
|
||||
char host[16];
|
||||
std::snprintf(host, sizeof(host), "%u.%u.%u.%u", addr & 0xFF,
|
||||
(addr >> 8) & 0xFF, (addr >> 16) & 0xFF, (addr >> 24) & 0xFF);
|
||||
warp_to_game_server(String(host), warp.port);
|
||||
// ClientVS22's RecvWarpPacket() always calls Connect(lAddr, wPort),
|
||||
// even for an address that points back to the current channel. Clear
|
||||
// the old loading/world state before the socket is replaced.
|
||||
w.reset_for_map_change();
|
||||
emit_signal("world_reset");
|
||||
emit_signal("warp", Vector3(g.x, g.y, g.z), false);
|
||||
|
||||
uint32_t addr = static_cast<uint32_t>(warp.addr);
|
||||
char host[16];
|
||||
std::snprintf(host, sizeof(host), "%u.%u.%u.%u", addr & 0xFF,
|
||||
(addr >> 8) & 0xFF, (addr >> 16) & 0xFF, (addr >> 24) & 0xFF);
|
||||
if (!classic_sess->connect_warp(std::string(host), warp.port)) {
|
||||
// connect_warp() marks the session Failed. Let the normal stage
|
||||
// bridge surface it as a post-login disconnect on the next pump.
|
||||
return;
|
||||
}
|
||||
classic_last_stage = -1;
|
||||
shop_open_seen = false;
|
||||
mall_open_seen = false;
|
||||
dead_seen.clear();
|
||||
set_stage(Stage::GameConnect);
|
||||
return;
|
||||
}
|
||||
if (w.take_time_dirty()) {
|
||||
emit_signal("time_changed", (int64_t)w.server_time());
|
||||
@@ -3280,6 +3334,17 @@ void M2Client::pump_game() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// §2.5: bare field updates (ownership / affect flags) with no Spawn/Move/
|
||||
// Info change — re-mirror the node so presentation stays in sync.
|
||||
for (uint32_t vid : w.drain_dirty()) {
|
||||
if (const mtnet::Entity *e = w.get(vid)) {
|
||||
emit_signal("entity_info", (int)vid, entity_dict(*e));
|
||||
}
|
||||
}
|
||||
// §9.1: map background music.
|
||||
if (w.take_bgm_dirty()) {
|
||||
emit_signal("bgm_changed", String::utf8(w.bgm_name().c_str()), w.bgm_volume());
|
||||
}
|
||||
for (const auto &m : w.drain_chat()) {
|
||||
if (m.type == mtnet::CHAT_TYPE_WHISPER) {
|
||||
emit_signal("whisper_received", (int)m.sub, String::utf8(m.from.c_str()),
|
||||
|
||||
Reference in New Issue
Block a user