Fix 40250 classic login and world loading
This commit is contained in:
@@ -12,6 +12,9 @@ ClassicSession::ClassicSession() : m_parser(m_world) {
|
||||
return on_packet(h, b, l);
|
||||
};
|
||||
m_stream.on_error = [this](const std::string &e) {
|
||||
if (m_auth_transition) {
|
||||
return;
|
||||
}
|
||||
m_last_error = e;
|
||||
set_stage(Stage::Failed);
|
||||
if (on_error) {
|
||||
@@ -19,7 +22,8 @@ ClassicSession::ClassicSession() : m_parser(m_world) {
|
||||
}
|
||||
};
|
||||
m_stream.on_state_change = [this](ClassicStream::State s) {
|
||||
if (s == ClassicStream::State::Offline && m_stage != Stage::Failed &&
|
||||
if (s == ClassicStream::State::Offline && !m_auth_transition &&
|
||||
m_stage != Stage::Failed &&
|
||||
m_stage != Stage::Offline) {
|
||||
m_last_error = m_stream.last_error().empty() ? "disconnected" : m_stream.last_error();
|
||||
set_stage(Stage::Failed);
|
||||
@@ -27,6 +31,29 @@ ClassicSession::ClassicSession() : m_parser(m_world) {
|
||||
// handshake packets flow next; stay in Connecting until GC_PHASE(LOGIN)
|
||||
}
|
||||
};
|
||||
m_auth_stream.on_phase = [this](uint8_t p) { on_auth_phase(p); };
|
||||
m_auth_stream.on_packet = [this](uint8_t h, const uint8_t *b, uint32_t l) {
|
||||
return on_auth_packet(h, b, l);
|
||||
};
|
||||
m_auth_stream.on_error = [this](const std::string &e) {
|
||||
if (m_auth_transition) {
|
||||
return;
|
||||
}
|
||||
m_last_error = e;
|
||||
set_stage(Stage::Failed);
|
||||
if (on_error) {
|
||||
on_error(e);
|
||||
}
|
||||
};
|
||||
m_auth_stream.on_state_change = [this](ClassicStream::State s) {
|
||||
if (s == ClassicStream::State::Offline && !m_auth_transition &&
|
||||
m_use_auth && !m_auth_succeeded && m_stage != Stage::Failed &&
|
||||
m_stage != Stage::Offline) {
|
||||
m_last_error = m_auth_stream.last_error().empty() ? "auth peer closed" :
|
||||
m_auth_stream.last_error();
|
||||
set_stage(Stage::Failed);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void ClassicSession::set_stage(Stage s) {
|
||||
@@ -42,6 +69,12 @@ void ClassicSession::set_stage(Stage s) {
|
||||
void ClassicSession::start_offline(const std::string &id, const std::string &pw) {
|
||||
m_id = id;
|
||||
m_pw = pw;
|
||||
m_use_auth = false;
|
||||
m_auth_succeeded = false;
|
||||
m_game_started = true;
|
||||
m_authenticated_game = false;
|
||||
m_auth_login_key = 0;
|
||||
m_auth_login_sent = false;
|
||||
m_login_sent = false;
|
||||
m_entergame_sent = false;
|
||||
m_version_sent = false;
|
||||
@@ -50,13 +83,61 @@ void ClassicSession::start_offline(const std::string &id, const std::string &pw)
|
||||
// 0xFF. The normal game client switches to CG_TIME_SYNC only after the
|
||||
// server has advanced into the login/select side of the connection.
|
||||
m_stream.set_time_sync_mode(false);
|
||||
m_auth_stream.set_time_sync_mode(false);
|
||||
set_stage(Stage::Connecting);
|
||||
}
|
||||
|
||||
bool ClassicSession::connect(const std::string &game_host, uint16_t game_port,
|
||||
const std::string &id, const std::string &pw) {
|
||||
start_offline(id, pw);
|
||||
if (!m_stream.connect(game_host, game_port)) {
|
||||
m_game_host = game_host;
|
||||
m_game_port = game_port;
|
||||
m_authenticated_game = false;
|
||||
m_auth_transition = true;
|
||||
const bool connected = m_stream.connect(game_host, game_port);
|
||||
m_auth_transition = false;
|
||||
if (!connected) {
|
||||
m_last_error = m_stream.last_error();
|
||||
set_stage(Stage::Failed);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ClassicSession::connect(const std::string &auth_host, uint16_t auth_port,
|
||||
const std::string &game_host, uint16_t game_port, const std::string &id,
|
||||
const std::string &pw) {
|
||||
start_offline(id, pw);
|
||||
m_use_auth = true;
|
||||
m_game_started = false;
|
||||
m_auth_succeeded = false;
|
||||
m_authenticated_game = false;
|
||||
m_auth_host = auth_host;
|
||||
m_auth_port = auth_port;
|
||||
m_game_host = game_host;
|
||||
m_game_port = game_port;
|
||||
m_auth_transition = true;
|
||||
const bool connected = m_auth_stream.connect(auth_host, auth_port);
|
||||
m_auth_transition = false;
|
||||
if (!connected) {
|
||||
m_last_error = m_auth_stream.last_error();
|
||||
set_stage(Stage::Failed);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ClassicSession::connect_with_login_key(const std::string &game_host, uint16_t game_port,
|
||||
const std::string &id, uint32_t login_key) {
|
||||
start_offline(id, "");
|
||||
m_game_host = game_host;
|
||||
m_game_port = game_port;
|
||||
m_auth_login_key = login_key;
|
||||
m_authenticated_game = true;
|
||||
m_auth_transition = true;
|
||||
const bool connected = m_stream.connect(game_host, game_port);
|
||||
m_auth_transition = false;
|
||||
if (!connected) {
|
||||
m_last_error = m_stream.last_error();
|
||||
set_stage(Stage::Failed);
|
||||
return false;
|
||||
@@ -65,12 +146,40 @@ bool ClassicSession::connect(const std::string &game_host, uint16_t game_port,
|
||||
}
|
||||
|
||||
void ClassicSession::disconnect() {
|
||||
m_auth_transition = true;
|
||||
m_auth_stream.disconnect();
|
||||
m_stream.disconnect();
|
||||
m_auth_transition = false;
|
||||
set_stage(Stage::Offline);
|
||||
}
|
||||
|
||||
void ClassicSession::pump() {
|
||||
m_stream.process();
|
||||
if (m_use_auth && !m_auth_succeeded && m_stage != Stage::Failed) {
|
||||
m_auth_stream.process();
|
||||
}
|
||||
if (m_use_auth && m_auth_succeeded && !m_game_started && m_stage != Stage::Failed) {
|
||||
m_auth_transition = true;
|
||||
m_auth_stream.disconnect();
|
||||
m_auth_transition = false;
|
||||
m_authenticated_game = true;
|
||||
m_game_started = true;
|
||||
m_login_sent = false;
|
||||
m_entergame_sent = false;
|
||||
m_version_sent = false;
|
||||
m_stream.set_time_sync_mode(false);
|
||||
m_auth_transition = true;
|
||||
const bool connected = m_stream.connect(m_game_host, m_game_port);
|
||||
m_auth_transition = false;
|
||||
if (!connected) {
|
||||
m_last_error = m_stream.last_error();
|
||||
set_stage(Stage::Failed);
|
||||
return;
|
||||
}
|
||||
set_stage(Stage::Connecting);
|
||||
}
|
||||
if (m_game_started && m_stage != Stage::Failed) {
|
||||
m_stream.process();
|
||||
}
|
||||
// auto CG_ENTERGAME a beat into PHASE_LOADING (the m2dev client waits ~1.5s;
|
||||
// sending it too early makes the server drop us mid spawn-burst).
|
||||
if (m_stage == Stage::Loading && !m_entergame_sent && m_entergame_delay > 0 &&
|
||||
@@ -84,6 +193,10 @@ void ClassicSession::send_login() {
|
||||
if (m_login_sent) {
|
||||
return;
|
||||
}
|
||||
if (m_authenticated_game) {
|
||||
send_login_by_key();
|
||||
return;
|
||||
}
|
||||
// docs §2.2: the client turns sequence mode on around here; CG_LOGIN is a
|
||||
// bSeq packet and the server expects the trailing byte from packet #1.
|
||||
m_stream.set_sequence_mode(true);
|
||||
@@ -96,6 +209,78 @@ void ClassicSession::send_login() {
|
||||
m_login_sent = true;
|
||||
}
|
||||
|
||||
void ClassicSession::send_auth_login() {
|
||||
if (m_auth_login_sent) {
|
||||
return;
|
||||
}
|
||||
m_auth_stream.set_sequence_mode(true);
|
||||
CGLogin3 p{};
|
||||
p.header = HDR_CG_LOGIN3;
|
||||
std::strncpy(p.login, m_id.c_str(), sizeof(p.login) - 1);
|
||||
std::strncpy(p.passwd, m_pw.c_str(), sizeof(p.passwd) - 1);
|
||||
// The stock connector sends four client keys here. Zero is valid for this
|
||||
// client, provided the same values are used in the later CG_LOGIN2 packet.
|
||||
m_auth_stream.send_fixed(&p, sizeof(p));
|
||||
m_auth_login_sent = true;
|
||||
}
|
||||
|
||||
void ClassicSession::send_login_by_key() {
|
||||
// The game side must use the ticket returned by the auth server; sending
|
||||
// CG_LOGIN here would hit the server's locale/version gate.
|
||||
m_stream.set_sequence_mode(true);
|
||||
CGLogin2 p{};
|
||||
p.header = HDR_CG_LOGIN2;
|
||||
std::strncpy(p.login, m_id.c_str(), sizeof(p.login) - 1);
|
||||
p.login_key = m_auth_login_key;
|
||||
m_stream.send_fixed(&p, sizeof(p));
|
||||
m_login_sent = true;
|
||||
}
|
||||
|
||||
void ClassicSession::on_auth_phase(uint8_t phase) {
|
||||
if (phase == PHASE_AUTH) {
|
||||
set_stage(Stage::LoggingIn);
|
||||
send_auth_login();
|
||||
} else if (phase == PHASE_CLOSE) {
|
||||
m_last_error = "auth server closed phase";
|
||||
set_stage(Stage::Failed);
|
||||
}
|
||||
}
|
||||
|
||||
bool ClassicSession::on_auth_packet(uint8_t header, const uint8_t *body, uint32_t len) {
|
||||
if (header == HDR_GC_AUTH_SUCCESS) {
|
||||
if (len + 1u < sizeof(GCAuthSuccess)) {
|
||||
m_last_error = "short GC_AUTH_SUCCESS";
|
||||
set_stage(Stage::Failed);
|
||||
return false;
|
||||
}
|
||||
GCAuthSuccess p{};
|
||||
p.header = header;
|
||||
std::memcpy(reinterpret_cast<uint8_t *>(&p) + 1, body, sizeof(p) - 1);
|
||||
m_auth_login_key = p.login_key;
|
||||
if (p.result == 0 || m_auth_login_key == 0) {
|
||||
m_last_error = "auth rejected credentials";
|
||||
set_stage(Stage::Failed);
|
||||
return false;
|
||||
}
|
||||
m_auth_succeeded = true;
|
||||
return true;
|
||||
}
|
||||
if (header == HDR_GC_LOGIN_FAILURE) {
|
||||
if (len + 1u < sizeof(GCLoginFailure)) {
|
||||
m_last_error = "short auth GC_LOGIN_FAILURE";
|
||||
} else {
|
||||
const char *status = reinterpret_cast<const char *>(body);
|
||||
m_last_error = "auth login failed: " +
|
||||
std::string(status, strnlen(status, sizeof(GCLoginFailure) - 1));
|
||||
}
|
||||
set_stage(Stage::Failed);
|
||||
return false;
|
||||
}
|
||||
// Optional Panama/hybrid-crypt packets are already framed and can be
|
||||
// ignored by this asset-loader client.
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ClassicSession::create_character(int slot, const std::string &name, int job, int shape,
|
||||
int con, int intel, int str, int dex) {
|
||||
if (m_stage != Stage::CharSelect || slot < 0 || slot >= m_parser.slot_count() ||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
// ClassicSession — MT_PROTOCOL=classic: drives ClassicStream through
|
||||
// HANDSHAKE -> LOGIN -> SELECT -> LOADING -> GAME and parses GC packets into a
|
||||
// shared EntityStore. See docs/CLIENT-40250-PORT.md §4 / §8.
|
||||
// ClassicSession — MT_PROTOCOL=classic: drives ClassicStream through the
|
||||
// 40250 auth-server -> game-server flow and parses GC packets into a shared
|
||||
// EntityStore. See docs/CLIENT-40250-PORT.md §4 / §8.
|
||||
//
|
||||
// The stock 40250 server uses _IMPROVED_PACKET_ENCRYPTION_; ClassicStream
|
||||
// negotiates that DH2 + CTR session before the first login packet.
|
||||
@@ -27,6 +27,13 @@ public:
|
||||
// --- INetSession ---
|
||||
bool connect(const std::string &game_host, uint16_t game_port, const std::string &id,
|
||||
const std::string &pw) override;
|
||||
// Full 40250 flow: auth CG_LOGIN3 -> ticket, then game CG_LOGIN2 -> chars.
|
||||
bool connect(const std::string &auth_host, uint16_t auth_port,
|
||||
const std::string &game_host, uint16_t game_port, const std::string &id,
|
||||
const std::string &pw);
|
||||
// Reconnect directly to a game channel using a previously issued auth ticket.
|
||||
bool connect_with_login_key(const std::string &game_host, uint16_t game_port,
|
||||
const std::string &id, uint32_t login_key);
|
||||
void disconnect() override;
|
||||
bool select_char(int slot) override;
|
||||
bool enter_game() override;
|
||||
@@ -133,36 +140,57 @@ public:
|
||||
// --- accessors ---
|
||||
const std::vector<CharSlot> &char_slots() const { return m_parser.char_slots(); }
|
||||
ClassicStream &stream() { return m_stream; } // for tests / trace
|
||||
ClassicStream &auth_stream() { return m_auth_stream; } // auth-flow tests / trace
|
||||
ClassicParser &parser() { return m_parser; }
|
||||
uint32_t login_key() const { return m_auth_login_key; }
|
||||
uint32_t now_ms() const { return m_now; }
|
||||
void set_now(uint32_t ms) {
|
||||
m_now = ms;
|
||||
m_world.set_now(ms);
|
||||
}
|
||||
// auto-send CG_ENTERGAME this many ms after entering PHASE_LOADING (0 = never,
|
||||
// caller drives enter_game()). Default mirrors the m2dev client's ~1.5s.
|
||||
// caller drives enter_game()). The 40250 test account sends a large map-load
|
||||
// burst; waiting for that burst to drain avoids racing the server's spawn data.
|
||||
void set_auto_entergame_delay(uint32_t ms) { m_entergame_delay = ms; }
|
||||
void set_wire_trace(bool on) { m_stream.set_wire_trace(on); }
|
||||
void set_wire_trace(bool on) {
|
||||
m_stream.set_wire_trace(on);
|
||||
m_auth_stream.set_wire_trace(on);
|
||||
}
|
||||
|
||||
private:
|
||||
void set_stage(Stage s);
|
||||
void on_phase(uint8_t phase);
|
||||
void on_auth_phase(uint8_t phase);
|
||||
bool on_packet(uint8_t header, const uint8_t *body, uint32_t len);
|
||||
bool on_auth_packet(uint8_t header, const uint8_t *body, uint32_t len);
|
||||
void send_login();
|
||||
void send_auth_login();
|
||||
void send_login_by_key();
|
||||
bool send_client_version();
|
||||
|
||||
EntityStore m_world;
|
||||
ClassicStream m_stream;
|
||||
ClassicStream m_auth_stream;
|
||||
ClassicParser m_parser;
|
||||
|
||||
Stage m_stage = Stage::Offline;
|
||||
std::string m_last_error;
|
||||
std::string m_id, m_pw;
|
||||
std::string m_auth_host, m_game_host;
|
||||
uint16_t m_auth_port = 0;
|
||||
uint16_t m_game_port = 0;
|
||||
uint32_t m_auth_login_key = 0;
|
||||
uint32_t m_now = 0;
|
||||
uint32_t m_loading_since = 0;
|
||||
uint32_t m_entergame_delay = 1500;
|
||||
uint32_t m_entergame_delay = 8000;
|
||||
bool m_entergame_sent = false;
|
||||
bool m_login_sent = false;
|
||||
bool m_auth_login_sent = false;
|
||||
bool m_use_auth = false;
|
||||
bool m_auth_succeeded = false;
|
||||
bool m_game_started = false;
|
||||
bool m_authenticated_game = false;
|
||||
bool m_auth_transition = false;
|
||||
bool m_version_sent = false;
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <arpa/inet.h>
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
@@ -27,6 +28,17 @@ void set_nonblocking(int fd) {
|
||||
int fl = fcntl(fd, F_GETFL, 0);
|
||||
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
|
||||
}
|
||||
|
||||
bool plausible_character_add(const uint8_t *p, size_t available) {
|
||||
if (available < sizeof(GCCharacterAdd) || p[0] != HDR_GC_CHARACTER_ADD) {
|
||||
return false;
|
||||
}
|
||||
GCCharacterAdd packet{};
|
||||
std::memcpy(&packet, p, sizeof(packet));
|
||||
return packet.vid != 0 && packet.race != 0 && packet.type <= 8 &&
|
||||
std::isfinite(packet.angle) && std::fabs(packet.angle) <= 360.0f &&
|
||||
packet.moving_speed <= 255 && packet.attack_speed <= 255;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ClassicStream::~ClassicStream() {
|
||||
@@ -93,10 +105,15 @@ void ClassicStream::disconnect() {
|
||||
m_seq_idx = 0;
|
||||
m_seq_on = false;
|
||||
m_handshake_seen = false;
|
||||
m_phase = PHASE_HANDSHAKE;
|
||||
m_cipher.clean_up();
|
||||
set_state(State::Offline);
|
||||
}
|
||||
|
||||
void ClassicStream::discard_recv(size_t n) {
|
||||
m_recv.discard(n);
|
||||
}
|
||||
|
||||
void ClassicStream::decrypt_appended(size_t n) {
|
||||
if (n == 0 || !m_cipher.activated()) {
|
||||
return;
|
||||
@@ -234,7 +251,7 @@ bool ClassicStream::handle_control(uint8_t header, bool &consumed) {
|
||||
if (!m_recv.peek(&hs, sizeof(hs))) {
|
||||
return true; // need more
|
||||
}
|
||||
m_recv.discard(sizeof(hs));
|
||||
discard_recv(sizeof(hs));
|
||||
consumed = true;
|
||||
// mirror EterLib/PythonNetworkStreamPhaseHandshake.cpp + desc.cpp:
|
||||
m_server_time_base = hs.time + static_cast<uint32_t>(hs.delta);
|
||||
@@ -267,7 +284,7 @@ bool ClassicStream::handle_control(uint8_t header, bool &consumed) {
|
||||
if (!m_recv.peek(&b, sizeof(b))) {
|
||||
return true;
|
||||
}
|
||||
m_recv.discard(sizeof(b));
|
||||
discard_recv(sizeof(b));
|
||||
consumed = true;
|
||||
if (on_handshake_ok) {
|
||||
on_handshake_ok();
|
||||
@@ -279,8 +296,9 @@ bool ClassicStream::handle_control(uint8_t header, bool &consumed) {
|
||||
if (!m_recv.peek(&p, sizeof(p))) {
|
||||
return true;
|
||||
}
|
||||
m_recv.discard(sizeof(p));
|
||||
discard_recv(sizeof(p));
|
||||
consumed = true;
|
||||
m_phase = p.phase;
|
||||
if (m_trace) {
|
||||
std::fprintf(stderr, "[classic] phase -> %u\n", p.phase);
|
||||
}
|
||||
@@ -294,7 +312,7 @@ bool ClassicStream::handle_control(uint8_t header, bool &consumed) {
|
||||
if (!m_recv.peek(&b, sizeof(b))) {
|
||||
return true;
|
||||
}
|
||||
m_recv.discard(sizeof(b));
|
||||
discard_recv(sizeof(b));
|
||||
consumed = true;
|
||||
uint8_t pong = HDR_CG_PONG; // 0xFE
|
||||
send_fixed(&pong, 1); // CG_PONG is bSeq=true (sizeof(BYTE)+seq)
|
||||
@@ -305,7 +323,7 @@ bool ClassicStream::handle_control(uint8_t header, bool &consumed) {
|
||||
if (!m_recv.peek(&u, sizeof(u))) {
|
||||
return true;
|
||||
}
|
||||
m_recv.discard(sizeof(u));
|
||||
discard_recv(sizeof(u));
|
||||
consumed = true;
|
||||
return true; // UDP not used
|
||||
}
|
||||
@@ -321,7 +339,7 @@ bool ClassicStream::handle_control(uint8_t header, bool &consumed) {
|
||||
if (!m_recv.peek(&kac, sizeof(kac))) {
|
||||
return true;
|
||||
}
|
||||
m_recv.discard(sizeof(kac));
|
||||
discard_recv(sizeof(kac));
|
||||
consumed = true;
|
||||
m_cipher.set_activated(true);
|
||||
// the server may have pipelined encrypted bytes right behind this;
|
||||
@@ -347,7 +365,7 @@ bool ClassicStream::handle_key_agreement() {
|
||||
if (!m_recv.peek(&pkt, sizeof(pkt))) {
|
||||
return true; // need the whole 261-byte packet
|
||||
}
|
||||
m_recv.discard(sizeof(pkt));
|
||||
discard_recv(sizeof(pkt));
|
||||
|
||||
// generate our own DH2 public blob
|
||||
KeyAgreement out{};
|
||||
@@ -391,7 +409,7 @@ void ClassicStream::dispatch() {
|
||||
// zero padding between packets (cipher block alignment on the m2dev side;
|
||||
// harmless to skip here too).
|
||||
if (header == 0) {
|
||||
m_recv.discard(1);
|
||||
discard_recv(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -430,7 +448,7 @@ void ClassicStream::dispatch() {
|
||||
disconnect();
|
||||
return;
|
||||
}
|
||||
m_recv.discard(raw.bytes);
|
||||
discard_recv(raw.bytes);
|
||||
continue;
|
||||
case RawPacketStatus::Error:
|
||||
m_last_error = "raw packet framing error";
|
||||
@@ -464,8 +482,11 @@ void ClassicStream::dispatch() {
|
||||
m_recv.discard(sizeof(DynHead));
|
||||
uint32_t body_len = dh.size - sizeof(DynHead);
|
||||
const uint8_t *body = m_recv.read_ptr();
|
||||
if (m_trace) {
|
||||
std::fprintf(stderr, "[classic] recv hdr=%u n=%u dynamic=1\n", header, dh.size);
|
||||
}
|
||||
bool ok = !on_packet || on_packet(header, body, body_len);
|
||||
m_recv.discard(body_len);
|
||||
discard_recv(body_len);
|
||||
if (!ok) {
|
||||
disconnect();
|
||||
return;
|
||||
@@ -477,6 +498,31 @@ void ClassicStream::dispatch() {
|
||||
int total = packet_size_gc(header);
|
||||
if (total <= 0) {
|
||||
m_last_error = "unknown/unsupported GC header " + std::to_string(header);
|
||||
// Some 40250 deployments append one legacy entity packet variant that
|
||||
// is not present in the checked-in header map. It has no length field,
|
||||
// but the following normal GC_CHARACTER_ADD stream is self-describing.
|
||||
// During PHASE_LOADING, recover only at a strongly validated character
|
||||
// boundary; this keeps a deployment-specific spawn packet from aborting
|
||||
// the whole login while leaving malformed encrypted data fatal elsewhere.
|
||||
if (m_phase == PHASE_LOADING) {
|
||||
const uint8_t *bytes = m_recv.read_ptr();
|
||||
bool recovered = false;
|
||||
for (size_t i = 1; i + sizeof(GCCharacterAdd) <= m_recv.readable(); ++i) {
|
||||
if (!plausible_character_add(bytes + i, m_recv.readable() - i)) {
|
||||
continue;
|
||||
}
|
||||
if (m_trace) {
|
||||
std::fprintf(stderr, "[classic] recovering loading stream at +%zu\n", i);
|
||||
}
|
||||
discard_recv(i);
|
||||
m_last_error.clear();
|
||||
recovered = true;
|
||||
break;
|
||||
}
|
||||
if (recovered) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (on_error) {
|
||||
on_error(m_last_error);
|
||||
}
|
||||
@@ -487,8 +533,11 @@ void ClassicStream::dispatch() {
|
||||
return;
|
||||
}
|
||||
const uint8_t *p = m_recv.read_ptr();
|
||||
if (m_trace) {
|
||||
std::fprintf(stderr, "[classic] recv hdr=%u n=%d dynamic=0\n", header, total);
|
||||
}
|
||||
bool ok = !on_packet || on_packet(header, p + 1, static_cast<uint32_t>(total - 1));
|
||||
m_recv.discard(static_cast<size_t>(total));
|
||||
discard_recv(static_cast<size_t>(total));
|
||||
if (!ok) {
|
||||
disconnect();
|
||||
return;
|
||||
|
||||
@@ -107,6 +107,7 @@ private:
|
||||
void append_sequence_if_needed(uint8_t header);
|
||||
bool handle_key_agreement();
|
||||
void decrypt_appended(size_t n); // decrypt the last n bytes just written to m_recv
|
||||
void discard_recv(size_t n);
|
||||
|
||||
int m_sock = -1;
|
||||
State m_state = State::Offline;
|
||||
@@ -123,6 +124,7 @@ private:
|
||||
bool m_handshake_seen = false;
|
||||
bool m_time_sync_mode = false;
|
||||
bool m_trace = false;
|
||||
uint8_t m_phase = PHASE_HANDSHAKE;
|
||||
|
||||
// server clock offset from the last handshake exchange (informational).
|
||||
uint32_t m_server_time_base = 0;
|
||||
|
||||
@@ -455,18 +455,26 @@ void M2Client::connect_to_server(const String &auth_host, int auth_port, const S
|
||||
have_cfg = true;
|
||||
suspended = false;
|
||||
|
||||
// MT_PROTOCOL=classic -> 40250 backend: direct connect to the game host,
|
||||
// with the same post-login feature surface as the shared M2Client API.
|
||||
// MT_PROTOCOL=classic -> 40250 backend: authenticate on auth_host first,
|
||||
// then connect to game_host with the returned login ticket.
|
||||
if (OS::get_singleton()->get_environment("MT_PROTOCOL") == "classic") {
|
||||
classic_sess = std::make_unique<mtnet::classic::ClassicSession>();
|
||||
// The 40250 server expects CG_ENTERGAME shortly after the loading phase
|
||||
// starts. Keep the live client in the same timing window as m2dev while
|
||||
// allowing a deployment-specific override for unusually large bursts.
|
||||
const String enter_delay = OS::get_singleton()->get_environment("MT_CLASSIC_ENTER_DELAY");
|
||||
classic_sess->set_auto_entergame_delay(enter_delay.is_empty() ? 1500 :
|
||||
static_cast<uint32_t>(MAX(0, enter_delay.to_int())));
|
||||
last_login_key = 0;
|
||||
classic_list_emitted = false;
|
||||
classic_last_stage = -1;
|
||||
classic_last_empire = -1;
|
||||
shop_open_seen = false;
|
||||
mall_open_seen = false;
|
||||
last_game_phase = -1;
|
||||
if (!classic_sess->connect(std::string(g_host.utf8().get_data()), (uint16_t)g_port,
|
||||
std::string(id.utf8().get_data()), std::string(pw.utf8().get_data()))) {
|
||||
if (!classic_sess->connect(std::string(auth_host.utf8().get_data()), (uint16_t)auth_port,
|
||||
std::string(g_host.utf8().get_data()), (uint16_t)g_port,
|
||||
std::string(id.utf8().get_data()), std::string(pw.utf8().get_data()))) {
|
||||
emit_signal("login_failed", String(classic_sess->last_error().c_str()));
|
||||
set_stage(Stage::Failed);
|
||||
classic_sess.reset();
|
||||
@@ -2646,8 +2654,16 @@ void M2Client::warp_to_game_server(const String &host, int port) {
|
||||
mall_open_seen = false;
|
||||
dead_seen.clear();
|
||||
classic_sess = std::make_unique<mtnet::classic::ClassicSession>();
|
||||
if (!classic_sess->connect(std::string(host.utf8().get_data()), (uint16_t)port,
|
||||
std::string(cfg_id.utf8().get_data()), std::string(cfg_pw.utf8().get_data()))) {
|
||||
bool connected = false;
|
||||
if (last_login_key != 0) {
|
||||
connected = classic_sess->connect_with_login_key(std::string(host.utf8().get_data()),
|
||||
(uint16_t)port, std::string(cfg_id.utf8().get_data()), last_login_key);
|
||||
} else {
|
||||
connected = classic_sess->connect(std::string(cfg_auth_host.utf8().get_data()),
|
||||
(uint16_t)cfg_auth_port, std::string(host.utf8().get_data()), (uint16_t)port,
|
||||
std::string(cfg_id.utf8().get_data()), std::string(cfg_pw.utf8().get_data()));
|
||||
}
|
||||
if (!connected) {
|
||||
emit_signal("login_failed", String(classic_sess->last_error().c_str()));
|
||||
set_stage(Stage::Failed);
|
||||
classic_sess.reset();
|
||||
@@ -2691,6 +2707,9 @@ void M2Client::pump_classic() {
|
||||
|
||||
classic_sess->set_now((uint32_t)Time::get_singleton()->get_ticks_msec());
|
||||
classic_sess->pump();
|
||||
if (classic_sess->login_key() != 0) {
|
||||
last_login_key = classic_sess->login_key();
|
||||
}
|
||||
const int empire = (int)classic_sess->parser().empire();
|
||||
if (empire != classic_last_empire) {
|
||||
classic_last_empire = empire;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// are driven by the Godot UI or the documented command surface.
|
||||
//
|
||||
// net_classic_e2e <host> <port> <id> <password> [slot] [timeout_seconds]
|
||||
// MT_CLASSIC_AUTH=1 selects the 40250 auth(11000) -> game(<port>) flow.
|
||||
//
|
||||
// Exit status 0 means the socket handshake, DH2/CTR negotiation, login,
|
||||
// character list, character selection and PHASE_GAME were all reached.
|
||||
@@ -70,9 +71,18 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
|
||||
mtnet::classic::ClassicSession session;
|
||||
session.set_auto_entergame_delay(1500);
|
||||
const int enter_delay_ms = std::getenv("MT_CLASSIC_ENTER_DELAY") ?
|
||||
std::atoi(std::getenv("MT_CLASSIC_ENTER_DELAY")) : 8000;
|
||||
session.set_auto_entergame_delay(enter_delay_ms);
|
||||
session.set_wire_trace(std::getenv("MT_NET_TRACE") != nullptr);
|
||||
if (!session.connect(host, static_cast<uint16_t>(port), id, password)) {
|
||||
const bool use_auth = std::getenv("MT_CLASSIC_AUTH") != nullptr;
|
||||
const int auth_port = std::getenv("MT_CLASSIC_AUTH_PORT") ?
|
||||
std::atoi(std::getenv("MT_CLASSIC_AUTH_PORT")) : 11000;
|
||||
const bool connected = use_auth ?
|
||||
session.connect(host, static_cast<uint16_t>(auth_port), host, static_cast<uint16_t>(port),
|
||||
id, password) :
|
||||
session.connect(host, static_cast<uint16_t>(port), id, password);
|
||||
if (!connected) {
|
||||
std::fprintf(stderr, "FAIL connect: %s\n", session.last_error().c_str());
|
||||
return 2;
|
||||
}
|
||||
@@ -99,8 +109,9 @@ int main(int argc, char **argv) {
|
||||
std::printf("[e2e] character slots=%zu\n", slots.size());
|
||||
for (size_t i = 0; i < slots.size(); ++i) {
|
||||
const auto &slot = slots[i];
|
||||
std::printf(" [%zu] %s id=%u level=%u\n", i, slot.name.c_str(), slot.id,
|
||||
slot.level);
|
||||
std::printf(" [%zu] %s id=%u level=%u pos=(%d,%d) main_part=%u hair_part=%u change_name=%d\n",
|
||||
i, slot.name.c_str(), slot.id, slot.level, slot.x, slot.y, slot.main_part,
|
||||
slot.hair_part, (int)slot.change_name);
|
||||
}
|
||||
const int slot = choose_slot(slots, requested_slot);
|
||||
if (slot < 0) {
|
||||
@@ -130,6 +141,18 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
std::printf("PASS PHASE_GAME main_vid=%u entities=%zu inventory_slots=%d points_level=%d\n",
|
||||
world.main_vid(), world.size(), inventory_count, world.points().level());
|
||||
if (const mtnet::Entity *me = world.get(world.main_vid())) {
|
||||
std::printf(" main race=%u pos_cm=(%.0f,%.0f,%.0f) parts=[%u,%u,%u,%u] name=%s\n",
|
||||
me->race, me->x, me->y, me->z, me->parts[0], me->parts[1], me->parts[2],
|
||||
me->parts[3], me->name.c_str());
|
||||
}
|
||||
for (int wear = 0; wear < mtnet::WEAR_MAX_NUM; ++wear) {
|
||||
const mtnet::Item &item = world.equip_slot(wear);
|
||||
if (!item.empty()) {
|
||||
std::printf(" equip wear=%d vnum=%u count=%u\n", wear, item.vnum,
|
||||
(unsigned)item.count);
|
||||
}
|
||||
}
|
||||
session.disconnect();
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user