Update PlayerLoginReply struct for TOB

- TOB Struct is different from the old one
- Moved specifics into their respective structs
- Added variant for choosing the struct to use
- Added setters to abstract the client operations and reduce duplicate code
- Pass at cleanup on GenerateRandomLoginKey to make it easier to modify
This commit is contained in:
Knightly 2026-04-19 08:48:14 -10:00
parent 767f04731b
commit faf5fe1093
5 changed files with 133 additions and 149 deletions

View File

@ -219,7 +219,7 @@ const char* EQ::versions::MobVersionName(MobVersion mob_version)
case MobVersion::OfflineRoF2:
return "Offline RoF2";
case MobVersion::OfflineTOB:
return "Offline Steam Latest";
return "Offline TOB";
default:
return "Invalid Version";
};
@ -505,7 +505,7 @@ uint32 EQ::expansions::ConvertExpansionToExpansionBit(Expansion expansion)
return bitLS;
case Expansion::TOB:
return bitTOB;
default:
return bitEverQuest;
}

View File

@ -302,20 +302,13 @@ void Client::SendPlayResponse(EQApplicationPacket *outapp)
void Client::GenerateRandomLoginKey()
{
m_key.clear();
int count = 0;
while (count < 10) {
static const char key_selection[] =
{
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', '0', '1', '2', '3', '4', '5',
'6', '7', '8', '9'
};
static constexpr std::string_view key_selection = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
constexpr size_t key_length = 10;
m_key.append((const char *) &key_selection[m_random.Int(0, 35)], 1);
count++;
m_key.clear();
m_key.reserve(key_length);
for (size_t i = 0; i < key_length; ++i) {
m_key += key_selection[m_random.Int(0, key_selection.size() - 1)];
}
}
@ -362,55 +355,30 @@ void Client::SendFailedLogin()
m_stored_username.clear();
m_stored_password.clear();
if (m_client_version == cv_tob) {
// unencrypted
LoginBaseMessage h{};
h.sequence = m_login_base_message.sequence; // login (3)
h.encrypt_type = m_login_base_message.encrypt_type;
// unencrypted
LoginBaseMessage h{};
h.sequence = m_login_base_message.sequence;
h.encrypt_type = m_login_base_message.encrypt_type;
h.unk3 = m_login_base_message.unk3;
// encrypted
PlayerLoginReplyTOB r{};
r.base_reply.success = false;
r.base_reply.error_str_id = 105; // Error - The username and/or password were not valid
PlayerLoginReply r = m_client_version == cv_tob ? PlayerLoginReply(PlayerLoginReplyTOB{}) : PlayerLoginReply(PlayerLoginReplyOld{});
r.set_error_code(LS::ErrStr::ERROR_INVALID_CREDS);
// We don't care what key we send, just that it exists and is 10 characters so that we do not shift
r.set_key("InvalidKey");
char encrypted_buffer[80] = { 0 };
auto rc = eqcrypt_block((const char*)&r, sizeof(r), encrypted_buffer, 1);
if (rc == nullptr) {
LogDebug("Failed to encrypt eqcrypt block for failed login");
}
constexpr int outsize = sizeof(LoginBaseMessage) + sizeof(encrypted_buffer);
EQApplicationPacket outapp(OP_LoginAccepted, outsize);
outapp.WriteData(&h, sizeof(h));
outapp.WriteData(&encrypted_buffer, sizeof(encrypted_buffer));
m_connection->QueuePacket(&outapp);
char encrypted_buffer[80] = { 0 };
auto rc = eqcrypt_block(r.data(), r.size(), encrypted_buffer, 1);
if (rc == nullptr) {
LogDebug("Failed to encrypt eqcrypt block for failed login");
}
else {
// unencrypted
LoginBaseMessage h{};
h.sequence = m_login_base_message.sequence; // login (3)
h.encrypt_type = m_login_base_message.encrypt_type;
// encrypted
PlayerLoginReply r{};
r.base_reply.success = false;
r.base_reply.error_str_id = 105; // Error - The username and/or password were not valid
constexpr int outsize = sizeof(LoginBaseMessage) + sizeof(encrypted_buffer);
EQApplicationPacket outapp(OP_LoginAccepted, outsize);
outapp.WriteData(&h, sizeof(h));
outapp.WriteData(&encrypted_buffer, sizeof(encrypted_buffer));
char encrypted_buffer[80] = { 0 };
auto rc = eqcrypt_block((const char*)&r, sizeof(r), encrypted_buffer, 1);
if (rc == nullptr) {
LogDebug("Failed to encrypt eqcrypt block for failed login");
}
m_connection->QueuePacket(&outapp);
constexpr int outsize = sizeof(LoginBaseMessage) + sizeof(encrypted_buffer);
EQApplicationPacket outapp(OP_LoginAccepted, outsize);
outapp.WriteData(&h, sizeof(h));
outapp.WriteData(&encrypted_buffer, sizeof(encrypted_buffer));
m_connection->QueuePacket(&outapp);
}
m_client_status = cs_failed_to_login;
}
@ -496,91 +464,44 @@ void Client::DoSuccessfulLogin(LoginAccountsRepository::LoginAccounts &a)
m_account_name = a.account_name;
m_loginserver_name = a.source_loginserver;
if (m_client_version == cv_tob) {
// unencrypted
LoginBaseMessage h{};
h.sequence = m_login_base_message.sequence;
h.compressed = false;
h.encrypt_type = m_login_base_message.encrypt_type;
h.unk3 = m_login_base_message.unk3;
// unencrypted
LoginBaseMessage h{};
h.sequence = m_login_base_message.sequence;
h.compressed = false;
h.encrypt_type = m_login_base_message.encrypt_type;
h.unk3 = m_login_base_message.unk3;
// not serializing any of the variable length strings so just use struct directly
PlayerLoginReplyTOB r{};
r.base_reply.success = true;
r.base_reply.error_str_id = 101; // No Error
r.unk1 = 0;
r.unk2 = 0;
r.lsid = a.id;
r.failed_attempts = 0;
r.show_player_count = server.options.IsShowPlayerCountEnabled();
r.unk3 = 0;
r.unk4 = 0;
memcpy(r.key, m_key.c_str(), m_key.size());
// not serializing any of the variable length strings so just use struct directly
PlayerLoginReply r = m_client_version == cv_tob ? PlayerLoginReply(PlayerLoginReplyTOB{}) : PlayerLoginReply(PlayerLoginReplyOld{});
//todo: needs to be fixed
//SendExpansionPacketData(r);
r.set_success(true);
r.set_error_code(LS::ErrStr::ERROR_NONE);
r.set_lsid(a.id);
r.set_show_player_count(server.options.IsShowPlayerCountEnabled());
r.set_key(m_key);
char encrypted_buffer[80] = { 0 };
auto rc = eqcrypt_block((const char*)&r, sizeof(r), encrypted_buffer, 1);
if (rc == nullptr) {
LogDebug("Failed to encrypt eqcrypt block");
}
constexpr int outsize = sizeof(LoginBaseMessage) + sizeof(encrypted_buffer);
auto outapp = std::make_unique<EQApplicationPacket>(OP_LoginAccepted, outsize);
outapp->WriteData(&h, sizeof(h));
outapp->WriteData(&encrypted_buffer, sizeof(encrypted_buffer));
m_connection->QueuePacket(outapp.get());
if (m_client_version != cv_tob) {
SendExpansionPacketData(r.old());
}
else {
// unencrypted
LoginBaseMessage h{};
h.sequence = m_login_base_message.sequence;
h.compressed = false;
h.encrypt_type = m_login_base_message.encrypt_type;
h.unk3 = m_login_base_message.unk3;
// not serializing any of the variable length strings so just use struct directly
PlayerLoginReply r{};
r.base_reply.success = true;
r.base_reply.error_str_id = 101; // No Error
r.unk1 = 0;
r.unk2 = 0;
r.lsid = a.id;
r.failed_attempts = 0;
r.show_player_count = server.options.IsShowPlayerCountEnabled();
r.offer_min_days = 99;
r.offer_min_views = -1;
r.offer_cooldown_minutes = 0;
r.web_offer_number = 0;
r.web_offer_min_days = 99;
r.web_offer_min_views = -1;
r.web_offer_cooldown_minutes = 0;
memcpy(r.key, m_key.c_str(), m_key.size());
char encrypted_buffer[80] = { 0 };
SendExpansionPacketData(r);
char encrypted_buffer[80] = { 0 };
auto rc = eqcrypt_block((const char*)&r, sizeof(r), encrypted_buffer, 1);
if (rc == nullptr) {
LogDebug("Failed to encrypt eqcrypt block");
}
constexpr int outsize = sizeof(LoginBaseMessage) + sizeof(encrypted_buffer);
auto outapp = std::make_unique<EQApplicationPacket>(OP_LoginAccepted, outsize);
outapp->WriteData(&h, sizeof(h));
outapp->WriteData(&encrypted_buffer, sizeof(encrypted_buffer));
m_connection->QueuePacket(outapp.get());
auto rc = eqcrypt_block(r.data(), r.size(), encrypted_buffer, 1);
if (rc == nullptr) {
LogDebug("Failed to encrypt eqcrypt block");
}
constexpr int outsize = sizeof(LoginBaseMessage) + sizeof(encrypted_buffer);
auto outapp = std::make_unique<EQApplicationPacket>(OP_LoginAccepted, outsize);
outapp->WriteData(&h, sizeof(h));
outapp->WriteData(&encrypted_buffer, sizeof(encrypted_buffer));
m_connection->QueuePacket(outapp.get());
m_client_status = cs_logged_in;
}
void Client::SendExpansionPacketData(PlayerLoginReply &plrs)
void Client::SendExpansionPacketData(PlayerLoginReplyOld &plrs)
{
SerializeBuffer buf;
//from eqlsstr_us.txt id of each expansion, excluding 'Everquest'

View File

@ -38,7 +38,7 @@ public:
// Titanium uses the encrypted data block to contact the expansion (You own xxx:) and the max expansions (of yyy)
// Rof uses a separate data packet specifically for the expansion data
// Live, as of July 2021 uses a similar but slightly different seperate data packet
void SendExpansionPacketData(PlayerLoginReply &plrs);
void SendExpansionPacketData(PlayerLoginReplyOld &plrs);
void SendPlayToWorld(const char *data);
void SendServerListPacket(uint32 seq);
void SendPlayResponse(EQApplicationPacket *outapp);

View File

@ -194,7 +194,7 @@ ClientManager::ClientManager()
if (!m_tob_ops->LoadOpcodes(opcodes_path.c_str())) {
LogError(
"ClientManager fatal error: couldn't load opcodes for Steam Latest file [{}]",
"ClientManager fatal error: couldn't load opcodes for TOB file [{}]",
server.config.GetVariableString("client_configuration", "tob_opcodes", "login_opcodes.conf")
);
@ -204,7 +204,7 @@ ClientManager::ClientManager()
m_tob_stream->OnNewConnection(
[this](std::shared_ptr<EQ::Net::EQStream> stream) {
LogInfo(
"New Steam Latest client connection from [{}:{}]",
"New TOB client connection from [{}:{}]",
long2ip(stream->GetRemoteIP()),
stream->GetRemotePort()
);

View File

@ -20,6 +20,7 @@
#include "common/types.h"
#include <string>
#include <variant>
#pragma pack(push)
#pragma pack(1)
@ -45,7 +46,7 @@ struct LoginHandShakeReply {
};
// variable length, can use directly if not serializing strings
struct PlayerLoginReply {
struct PlayerLoginReplyOld {
// base header excluded to make struct data easier to encrypt
//LoginBaseMessage base_header;
LoginBaseReplyMessage base_reply;
@ -65,21 +66,80 @@ struct PlayerLoginReply {
int32_t web_offer_cooldown_minutes; // minimum minutes between offers (based on last offer time in client eqls ini) (default: 0)
char username[1]; // variable length, if not empty client attempts to re-login to server select when quitting from char select and sends this in a struct
char unknown[1]; // variable length, password unlikely? client doesn't send this on re-login from char select
};
struct PlayerLoginReplyTOB
{
void set_success(bool v) { base_reply.success = v; }
void set_error_code(int32_t v) { base_reply.error_str_id = v; }
void set_lsid(int32_t v) { lsid = v; }
void set_show_player_count(bool v) { show_player_count = v; }
void set_hardcoded_success_values() {
offer_min_days = 99;
offer_min_views = -1;
web_offer_min_days = 99;
web_offer_min_views = -1;
}
};
static_assert(sizeof(PlayerLoginReplyOld) == 58, "PlayerLoginReplyOld struct size does not match expected size");
static_assert(std::is_trivially_copyable_v<PlayerLoginReplyOld>);
static_assert(std::is_standard_layout_v<PlayerLoginReplyOld>);
struct PlayerLoginReplyTOB {
LoginBaseReplyMessage base_reply;
int8_t unk1; // (default: 0)
int8_t unk2; // (default: 0)
int32_t lsid; // (default: -1)
char key[11]; // client reads until null (variable length)
int32_t failed_attempts;
bool show_player_count; // admin flag, enables admin button and shows server player counts (default: false)
int32_t unk3; // guess, needs more investigation (default: 0)
int32_t unk4; // guess, needs more investigation (default: 0)
char username[1]; // variable length, if not empty client attempts to re-login to server select when quitting from char select and sends this in a struct
char unknown[1]; // variable length, password unlikely? client doesn't send this on re-login from char select
int8_t unk1 = 0;
int8_t unk2 = 0;
int32_t lsid = -1;
char key[11] = {};
int32_t failed_attempts = 0;
int32_t display_error_str_id = 0;
int32_t unk3 = 0;
bool show_player_count = false;
char username[1] = {};
char unk4[1] = {};
void set_success(bool v) { base_reply.success = v; }
void set_error_code(int32_t v) { display_error_str_id = v; base_reply.error_str_id = v; }
void set_lsid(int32_t v) { lsid = v; }
void set_show_player_count(bool v) { show_player_count = v; }
void set_hardcoded_success_values() {}
};
static_assert(sizeof(PlayerLoginReplyTOB) == 38, "PlayerLoginReplyTOB struct size does not match expected size");
static_assert(std::is_trivially_copyable_v<PlayerLoginReplyTOB>);
static_assert(std::is_standard_layout_v<PlayerLoginReplyTOB>);
class PlayerLoginReply {
std::variant<PlayerLoginReplyOld, PlayerLoginReplyTOB> v_;
public:
PlayerLoginReply(PlayerLoginReplyOld s) : v_(s) {}
PlayerLoginReply(PlayerLoginReplyTOB s) : v_(s) {}
void set_success(bool val) {
std::visit([val](auto& s) { s.set_success(val); }, v_);
}
void set_error_code(int32_t val) {
std::visit([val](auto& s) { s.set_error_code(val); }, v_);
}
void set_lsid(int32_t val) {
std::visit([val](auto& s) { s.set_lsid(val); }, v_);
}
void set_show_player_count(bool val) {
std::visit([val](auto& s) { s.set_show_player_count(val); }, v_);
}
void set_key(std::string_view s) {
std::visit([&](auto& st) {
const size_t n = s.copy(st.key, sizeof(st.key) - 1);
st.key[n] = '\0';
}, v_);
}
PlayerLoginReplyOld& old() { return std::get<PlayerLoginReplyOld>(v_); }
const PlayerLoginReplyOld& old() const { return std::get<PlayerLoginReplyOld>(v_); }
char* data() noexcept {
return std::visit([](auto& s) { return reinterpret_cast<char*>(&s); }, v_);
}
size_t size() const noexcept {
return std::visit([](auto const& s) { return sizeof(s); }, v_);
}
};
// variable length, for reference
@ -195,11 +255,14 @@ namespace LS {
namespace ErrStr {
constexpr static int ERROR_NONE = 101; // No Error
constexpr static int ERROR_UNKNOWN = 102; // Error - Unknown Error Occurred
constexpr static int ERROR_INVALID_CREDS = 105; // Error - Invalid Account Name or Password
constexpr static int ERROR_ACTIVE_CHARACTER = 111; // Error 1018: You currently have an active character on that EverQuest Server, please allow a minute for synchronization and try again.
constexpr static int ERROR_PASSWORD_RESET = 112; // Require password reset
constexpr static int ERROR_SERVER_UNAVAILABLE = 326; // That server is currently unavailable. Please check the EverQuest webpage for current server status and try again later.
constexpr static int ERROR_ACCOUNT_SUSPENDED = 337; // This account is currently suspended. Please contact customer service for more information.
constexpr static int ERROR_ACCOUNT_BANNED = 338; // This account is currently banned. Please contact customer service for more information.
constexpr static int ERROR_WORLD_MAX_CAPACITY = 339; // The world server is currently at maximum capacity and not allowing further logins until the number of players online decreases. Please try again later.
constexpr static int ERROR_REQUIRE_2FA = 342; // This account requires two-factor authentication.
};
}