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() ||