Fix 40250 classic login and world loading

This commit is contained in:
shen
2026-09-02 07:03:25 +08:00
parent 8f61990a83
commit 44bdcb0781
17 changed files with 446 additions and 45 deletions
+188 -3
View File
@@ -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() ||
+34 -6
View File
@@ -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;
};
+60 -11
View File
@@ -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;
+25 -6
View File
@@ -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;
+27 -4
View File
@@ -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;
}