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
@@ -3,9 +3,14 @@
|
||||
// session.stream().feed(), outgoing checked via take_outgoing().
|
||||
#include "../src/net/classic/classic_session.h"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <string>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
#include <vector>
|
||||
|
||||
using namespace mtnet::classic;
|
||||
@@ -43,6 +48,42 @@ static std::vector<uint8_t> drain(ClassicSession &s) {
|
||||
return out;
|
||||
}
|
||||
|
||||
static int listen_loopback(uint16_t &port) {
|
||||
int fd = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (fd < 0) {
|
||||
return -1;
|
||||
}
|
||||
int one = 1;
|
||||
::setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
|
||||
sockaddr_in addr{};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
addr.sin_port = 0;
|
||||
if (::bind(fd, reinterpret_cast<const sockaddr *>(&addr), sizeof(addr)) < 0 ||
|
||||
::listen(fd, 4) < 0) {
|
||||
::close(fd);
|
||||
return -1;
|
||||
}
|
||||
socklen_t len = sizeof(addr);
|
||||
if (::getsockname(fd, reinterpret_cast<sockaddr *>(&addr), &len) < 0) {
|
||||
::close(fd);
|
||||
return -1;
|
||||
}
|
||||
int flags = ::fcntl(fd, F_GETFL, 0);
|
||||
::fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
||||
port = ntohs(addr.sin_port);
|
||||
return fd;
|
||||
}
|
||||
|
||||
static bool accept_loopback(int listener, int &accepted) {
|
||||
pollfd pfd{listener, POLLIN, 0};
|
||||
if (::poll(&pfd, 1, 500) <= 0) {
|
||||
return false;
|
||||
}
|
||||
accepted = ::accept(listener, nullptr, nullptr);
|
||||
return accepted >= 0;
|
||||
}
|
||||
|
||||
int main() {
|
||||
ClassicSession s;
|
||||
s.set_now(0);
|
||||
@@ -53,6 +94,7 @@ int main() {
|
||||
s.on_entered_game = [&]() { entered = true; };
|
||||
|
||||
s.start_offline("admin", "123456789");
|
||||
s.set_auto_entergame_delay(1500);
|
||||
CHECK(s.stage() == mtnet::INetSession::Stage::Connecting, "start_offline -> Connecting");
|
||||
|
||||
// --- handshake: server HANDSHAKE -> client echoes it back ---
|
||||
@@ -98,6 +140,8 @@ int main() {
|
||||
ls.players[0].level = 42;
|
||||
ls.players[0].ht = 20;
|
||||
ls.players[0].change_name = 1;
|
||||
ls.players[0].addr = static_cast<int32_t>(0x0100007f); // 127.0.0.1, little-endian lAddr
|
||||
ls.players[0].port = 13002;
|
||||
ls.players[1].id = 1002;
|
||||
std::strcpy(ls.players[1].name, "Sura");
|
||||
ls.players[1].job = 4;
|
||||
@@ -110,6 +154,8 @@ int main() {
|
||||
const auto &sl = s.char_slots();
|
||||
CHECK(sl.size() == 4 && sl[0].name == "Warrior" && sl[0].level == 42 && sl[0].change_name,
|
||||
"slot 0 + forced rename flag");
|
||||
CHECK(sl[0].addr == static_cast<int32_t>(0x0100007f) && sl[0].port == 13002,
|
||||
"slot 0 game address carried");
|
||||
CHECK(sl[1].name == "Sura" && sl[1].job == 4, "slot 1");
|
||||
CHECK(sl[2].empty() && sl[3].empty(), "slots 2,3 empty");
|
||||
}
|
||||
@@ -218,6 +264,79 @@ int main() {
|
||||
CHECK(s.in_game() && entered, "in_game() + on_entered_game");
|
||||
}
|
||||
|
||||
// --- known but unsupported integration packet is not silently accepted ---
|
||||
{
|
||||
ClassicSession unsupported;
|
||||
unsupported.start_offline("admin", "123456789");
|
||||
std::vector<uint8_t> packet(sizeof(GCMatrixCard), 0);
|
||||
packet[0] = HDR_GC_MATRIX_CARD;
|
||||
feed(unsupported, packet);
|
||||
CHECK(unsupported.stage() == mtnet::INetSession::Stage::Failed,
|
||||
"unsupported integration packet -> Failed");
|
||||
CHECK(unsupported.last_error().find("does not handle") != std::string::npos,
|
||||
"unsupported integration packet -> explicit error");
|
||||
auto unsupported_headers = unsupported.parser().drain_unhandled_headers();
|
||||
CHECK(unsupported_headers.size() == 1 && unsupported_headers[0] == HDR_GC_MATRIX_CARD,
|
||||
"unsupported integration packet -> header event");
|
||||
}
|
||||
|
||||
// --- DirectEnter opens the character slot's TCP endpoint ---
|
||||
{
|
||||
uint16_t initial_port = 0;
|
||||
uint16_t slot_port = 0;
|
||||
uint16_t warp_port = 0;
|
||||
const int initial_listener = listen_loopback(initial_port);
|
||||
const int slot_listener = listen_loopback(slot_port);
|
||||
const int warp_listener = listen_loopback(warp_port);
|
||||
if (initial_listener >= 0 && slot_listener >= 0 && warp_listener >= 0) {
|
||||
ClassicSession direct;
|
||||
CHECK(direct.connect_with_login_key("127.0.0.1", initial_port, "admin", 0x12345678),
|
||||
"direct enter initial game connection");
|
||||
int initial_connection = -1;
|
||||
CHECK(accept_loopback(initial_listener, initial_connection),
|
||||
"direct enter first TCP connection accepted");
|
||||
|
||||
GCLoginSuccess login{};
|
||||
login.header = HDR_GC_LOGIN_SUCCESS_NEWSLOT;
|
||||
login.players[0].id = 2001;
|
||||
std::strcpy(login.players[0].name, "Direct");
|
||||
login.players[0].addr = static_cast<int32_t>(0x0100007f); // 127.0.0.1
|
||||
login.players[0].port = slot_port;
|
||||
feed(direct, raw(login));
|
||||
CHECK(direct.char_slots()[0].addr == static_cast<int32_t>(0x0100007f) &&
|
||||
direct.char_slots()[0].port == slot_port, "direct enter slot endpoint parsed");
|
||||
CHECK(direct.connect_direct_enter(0), "direct enter reconnect accepted");
|
||||
int slot_connection = -1;
|
||||
CHECK(accept_loopback(slot_listener, slot_connection),
|
||||
"direct enter second TCP connection targets slot endpoint");
|
||||
CHECK(direct.connect_warp("127.0.0.1", warp_port),
|
||||
"GC_WARP reconnect keeps selected slot and login key");
|
||||
int warp_connection = -1;
|
||||
CHECK(accept_loopback(warp_listener, warp_connection),
|
||||
"GC_WARP reconnect targets packet endpoint");
|
||||
if (initial_connection >= 0) {
|
||||
::close(initial_connection);
|
||||
}
|
||||
if (slot_connection >= 0) {
|
||||
::close(slot_connection);
|
||||
}
|
||||
if (warp_connection >= 0) {
|
||||
::close(warp_connection);
|
||||
}
|
||||
} else {
|
||||
std::puts("SKIP: DirectEnter TCP target test (localhost bind unavailable)");
|
||||
}
|
||||
if (initial_listener >= 0) {
|
||||
::close(initial_listener);
|
||||
}
|
||||
if (slot_listener >= 0) {
|
||||
::close(slot_listener);
|
||||
}
|
||||
if (warp_listener >= 0) {
|
||||
::close(warp_listener);
|
||||
}
|
||||
}
|
||||
|
||||
// --- GC_MOVE(3): NPC starts moving, interpolation advances on tick ---
|
||||
{
|
||||
GCMove mv{};
|
||||
@@ -248,7 +367,7 @@ int main() {
|
||||
feed(s, raw(warp));
|
||||
auto warps = s.world().drain_warps();
|
||||
CHECK(warps.size() == 1 && warps[0].x == 432100 && warps[0].y == 876500 &&
|
||||
warps[0].same_server(), "classic GC_WARP parsed and surfaced");
|
||||
!warps[0].same_server(), "classic GC_WARP always requests reconnect");
|
||||
}
|
||||
|
||||
// --- GC_CHARACTER_POINTS(16): stat block ---
|
||||
|
||||
Reference in New Issue
Block a user