1125 lines
33 KiB
C++
1125 lines
33 KiB
C++
/*
|
|
EQ2Emulator: Everquest II Server Emulator
|
|
Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net)
|
|
|
|
This file is part of EQ2Emulator.
|
|
*/
|
|
|
|
// Debug includes
|
|
#include "../common/debug.h"
|
|
|
|
// Linux network headers
|
|
#include <sys/socket.h>
|
|
#include <netinet/in.h>
|
|
#include <arpa/inet.h>
|
|
#include <unistd.h>
|
|
|
|
// Standard library includes
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
#include <cstring>
|
|
#include <iomanip>
|
|
#include <iostream>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
// Project includes
|
|
#include "net.h"
|
|
#include "client.h"
|
|
#include "../common/EQStream.h"
|
|
#include "../common/packet_dump.h"
|
|
#include "../common/packet_functions.h"
|
|
#include "../common/emu_opcodes.h"
|
|
#include "../common/MiscFunctions.h"
|
|
#include "LWorld.h"
|
|
#include "LoginDatabase.h"
|
|
#include "../common/ConfigReader.h"
|
|
#include "../common/Log.h"
|
|
|
|
// Global instances
|
|
extern NetConnection net;
|
|
extern LWorldList world_list;
|
|
extern ClientList client_list;
|
|
extern LoginDatabase database;
|
|
extern std::map<std::int16_t, OpcodeManager*> EQOpcodeManager;
|
|
extern ConfigReader configReader;
|
|
|
|
using namespace std;
|
|
|
|
/**
|
|
* @brief Constructs a new Client instance
|
|
* @param ieqnc The EQStream connection for this client
|
|
*/
|
|
Client::Client(EQStream* ieqnc)
|
|
: eqnc_{ieqnc}
|
|
, ip_{ieqnc->GetrIP()}
|
|
, port_{ntohs(ieqnc->GetrPort())}
|
|
, account_id_{0}
|
|
, lsadmin_{0}
|
|
, worldadmin_{0}
|
|
, lsstatus_{0}
|
|
, version_{0}
|
|
, kicked_{false}
|
|
, verified_{false}
|
|
, login_mode_{LoginMode::None}
|
|
, num_updates_{0}
|
|
, request_num_{0}
|
|
, createRequest_{nullptr}
|
|
, playWaitTimer_{nullptr}
|
|
, start_{false}
|
|
, update_position_{0}
|
|
, update_packets_{nullptr}
|
|
, needs_world_list_{true}
|
|
, sent_character_list_{false}
|
|
{
|
|
// Clear critical buffers
|
|
std::memset(bannedreason_, 0, sizeof(bannedreason_));
|
|
std::memset(key_, 0, sizeof(key_));
|
|
std::memset(ClientSession, 0, sizeof(ClientSession));
|
|
|
|
// Initialize timers
|
|
updatetimer = std::make_unique<Timer>(500);
|
|
updatelisttimer = std::make_unique<Timer>(10000);
|
|
}
|
|
|
|
/**
|
|
* @brief Destructor - cleans up all client resources
|
|
*/
|
|
Client::~Client()
|
|
{
|
|
// Close the connection
|
|
if (eqnc_)
|
|
{
|
|
eqnc_->Close();
|
|
}
|
|
|
|
// Clean up character creation request
|
|
safe_delete(createRequest_);
|
|
|
|
// Clean up update packets
|
|
if (update_packets_)
|
|
{
|
|
for (auto* packet : *update_packets_)
|
|
{
|
|
safe_delete(packet);
|
|
}
|
|
safe_delete(update_packets_);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Main processing function for the client
|
|
* @return true if client is still active, false if should be disconnected
|
|
*/
|
|
bool Client::Process()
|
|
{
|
|
// Handle initial connection state
|
|
if (!start_ && !eqnc_->CheckActive())
|
|
{
|
|
if (!playWaitTimer_)
|
|
{
|
|
playWaitTimer_ = std::make_unique<Timer>(5000);
|
|
}
|
|
else if (playWaitTimer_->Check())
|
|
{
|
|
playWaitTimer_.reset();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
else if (!start_)
|
|
{
|
|
playWaitTimer_.reset();
|
|
start_ = true;
|
|
}
|
|
|
|
// Check disconnect timer
|
|
if (disconnectTimer && disconnectTimer->Check())
|
|
{
|
|
disconnectTimer.reset();
|
|
getConnection()->SendDisconnect();
|
|
}
|
|
|
|
// Process packets if not kicked
|
|
if (!kicked_)
|
|
{
|
|
// Handle play wait timeout
|
|
if (playWaitTimer_ != nullptr && playWaitTimer_->Check())
|
|
{
|
|
SendPlayFailed(PLAY_ERROR_SERVER_TIMEOUT);
|
|
playWaitTimer_.reset();
|
|
}
|
|
|
|
// Process world list updates
|
|
if (!needs_world_list_ && updatetimer && updatetimer->Check())
|
|
{
|
|
if (updatelisttimer && updatelisttimer->Check())
|
|
{
|
|
// Disconnect after 30 minutes of updates
|
|
if (num_updates_ >= 180)
|
|
{
|
|
getConnection()->SendDisconnect();
|
|
}
|
|
else
|
|
{
|
|
// Clean up old update packets
|
|
if (update_packets_)
|
|
{
|
|
for (auto* packet : *update_packets_)
|
|
{
|
|
safe_delete(packet);
|
|
}
|
|
}
|
|
safe_delete(update_packets_);
|
|
|
|
// Get new update packets
|
|
update_packets_ = world_list.GetServerListUpdate(version_);
|
|
}
|
|
num_updates_++;
|
|
}
|
|
else
|
|
{
|
|
// Send update packets in sequence
|
|
if (!update_packets_)
|
|
{
|
|
update_packets_ = world_list.GetServerListUpdate(version_);
|
|
}
|
|
else
|
|
{
|
|
if (update_position_ < update_packets_->size())
|
|
{
|
|
QueuePacket(update_packets_->at(update_position_)->serialize());
|
|
update_position_++;
|
|
}
|
|
else
|
|
{
|
|
update_position_ = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process incoming packets
|
|
EQApplicationPacket* app = nullptr;
|
|
while ((app = eqnc_->PopPacket()))
|
|
{
|
|
switch (app->GetOpcode())
|
|
{
|
|
case OP_LoginRequestMsg:
|
|
{
|
|
ProcessLoginRequest(app);
|
|
break;
|
|
}
|
|
|
|
case OP_KeymapLoadMsg:
|
|
{
|
|
// Keymap message - currently unused
|
|
break;
|
|
}
|
|
|
|
case OP_AllWSDescRequestMsg:
|
|
{
|
|
ProcessWorldListRequest();
|
|
break;
|
|
}
|
|
|
|
case OP_LsClientCrashlogReplyMsg:
|
|
{
|
|
SaveErrorsToDB(app, "Crash Log", GetVersion());
|
|
break;
|
|
}
|
|
|
|
case OP_LsClientVerifylogReplyMsg:
|
|
{
|
|
SaveErrorsToDB(app, "Verify Log", GetVersion());
|
|
break;
|
|
}
|
|
|
|
case OP_LsClientAlertlogReplyMsg:
|
|
{
|
|
SaveErrorsToDB(app, "Alert Log", GetVersion());
|
|
break;
|
|
}
|
|
|
|
case OP_LsClientBaselogReplyMsg:
|
|
{
|
|
SaveErrorsToDB(app, "Base Log", GetVersion());
|
|
break;
|
|
}
|
|
|
|
case OP_AllCharactersDescRequestMsg:
|
|
{
|
|
// Character description request - handled elsewhere
|
|
break;
|
|
}
|
|
|
|
case OP_CreateCharacterRequestMsg:
|
|
{
|
|
ProcessCharacterCreation(app);
|
|
break;
|
|
}
|
|
|
|
case OP_PlayCharacterRequestMsg:
|
|
{
|
|
ProcessPlayCharacter(app);
|
|
break;
|
|
}
|
|
|
|
case OP_DeleteCharacterRequestMsg:
|
|
{
|
|
ProcessDeleteCharacter(app);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
{
|
|
const char* name = app->GetOpcodeName();
|
|
if (name)
|
|
{
|
|
LogWrite(OPCODE__DEBUG, 1, "Opcode", "%s Received %04X (%i)",
|
|
name, app->GetRawOpcode(), app->GetRawOpcode());
|
|
}
|
|
else
|
|
{
|
|
LogWrite(OPCODE__DEBUG, 1, "Opcode", "Received %04X (%i)",
|
|
app->GetRawOpcode(), app->GetRawOpcode());
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
delete app;
|
|
}
|
|
}
|
|
|
|
// Check if connection is still active
|
|
if (!eqnc_->CheckActive())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief Processes a login request packet
|
|
* @param app The login request packet
|
|
*/
|
|
void Client::ProcessLoginRequest(EQApplicationPacket* app)
|
|
{
|
|
DumpPacket(app);
|
|
|
|
// Try loading with version 1 structure
|
|
auto packet = std::unique_ptr<PacketStruct>(configReader.getStruct("LS_LoginRequest", 1));
|
|
|
|
if (packet && packet->LoadPacketData(app->pBuffer, app->size))
|
|
{
|
|
version_ = packet->getType_int16_ByName("version");
|
|
LogWrite(LOGIN__DEBUG, 0, "Login", "Classic Client Version Provided: %i", version_);
|
|
|
|
// Check if we need to try the newer structure
|
|
if (version_ == 0 || EQOpcodeManager.count(GetOpcodeVersion(version_)) == 0)
|
|
{
|
|
packet.reset(configReader.getStruct("LS_LoginRequest", 1208));
|
|
if (packet && packet->LoadPacketData(app->pBuffer, app->size))
|
|
{
|
|
version_ = packet->getType_int16_ByName("version");
|
|
}
|
|
else
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
LogWrite(LOGIN__DEBUG, 0, "Login", "New Client Version Provided: %i", version_);
|
|
|
|
// Verify client version is supported
|
|
if (EQOpcodeManager.count(GetOpcodeVersion(version_)) == 0)
|
|
{
|
|
LogWrite(LOGIN__ERROR, 0, "Login", "Incompatible client version provided: %i", version_);
|
|
SendLoginDenied();
|
|
return;
|
|
}
|
|
|
|
// Process login credentials
|
|
if (EQOpcodeManager.count(GetOpcodeVersion(version_)) > 0 && getConnection())
|
|
{
|
|
getConnection()->SetClientVersion(GetVersion());
|
|
|
|
EQ2_16BitString username = packet->getType_EQ2_16BitString_ByName("username");
|
|
EQ2_16BitString password = packet->getType_EQ2_16BitString_ByName("password");
|
|
|
|
// Load account from database
|
|
LoginAccount* acct = database.LoadAccount(username.data.c_str(),
|
|
password.data.c_str(),
|
|
net.IsAllowingAccountCreation());
|
|
|
|
// Check for duplicate login
|
|
if (acct)
|
|
{
|
|
Client* otherclient = client_list.FindByLSID(acct->getLoginAccountID());
|
|
if (otherclient)
|
|
{
|
|
// Kick the previous client (might be a ghost)
|
|
otherclient->getConnection()->SendDisconnect();
|
|
}
|
|
}
|
|
|
|
// Process successful login
|
|
if (acct)
|
|
{
|
|
SetAccountName(username.data.c_str());
|
|
database.UpdateAccountIPAddress(acct->getLoginAccountID(), getConnection()->GetrIP());
|
|
database.UpdateAccountClientDataVersion(acct->getLoginAccountID(), version_);
|
|
LogWrite(LOGIN__INFO, 0, "Login", "%s successfully logged in.",
|
|
username.data.c_str());
|
|
|
|
needs_world_list_ = true;
|
|
SetLoginAccount(acct);
|
|
SendLoginAccepted();
|
|
}
|
|
else
|
|
{
|
|
// Login failed
|
|
if (username.size > 0)
|
|
{
|
|
LogWrite(LOGIN__ERROR, 0, "Login", "%s login failed!",
|
|
username.data.c_str());
|
|
}
|
|
else
|
|
{
|
|
LogWrite(LOGIN__ERROR, 0, "Login", "[UNKNOWN USER] login failed!");
|
|
}
|
|
SendLoginDenied();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
std::cout << "Error bad version: " << version_ << std::endl;
|
|
SendLoginDeniedBadVersion();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
std::cout << "Error loading LS_LoginRequest packet" << std::endl;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Processes a world list request
|
|
*/
|
|
void Client::ProcessWorldListRequest()
|
|
{
|
|
SendWorldList();
|
|
needs_world_list_ = false;
|
|
|
|
if (!sent_character_list_)
|
|
{
|
|
database.LoadCharacters(GetLoginAccount(), GetVersion());
|
|
sent_character_list_ = true;
|
|
}
|
|
|
|
SendCharList();
|
|
}
|
|
|
|
/**
|
|
* @brief Processes a character creation request
|
|
* @param app The character creation packet
|
|
*/
|
|
void Client::ProcessCharacterCreation(EQApplicationPacket* app)
|
|
{
|
|
auto packet = std::unique_ptr<PacketStruct>(
|
|
configReader.getStruct("CreateCharacter", GetVersion()));
|
|
|
|
DumpPacket(app);
|
|
|
|
// Start play wait timer
|
|
playWaitTimer_ = std::make_unique<Timer>(15000);
|
|
playWaitTimer_->Start();
|
|
|
|
LogWrite(WORLD__INFO, 1, "World", "Character creation request from account %s",
|
|
GetAccountName());
|
|
|
|
if (packet->LoadPacketData(app->pBuffer, app->size, GetVersion() <= 561 ? false : true))
|
|
{
|
|
DumpPacket(app->pBuffer, app->size);
|
|
packet->setDataByName("account_id", GetAccountID());
|
|
|
|
// Find the target world server
|
|
LWorld* world_server = world_list.FindByID(packet->getType_int32_ByName("server_id"));
|
|
if (!world_server)
|
|
{
|
|
DumpPacket(app->pBuffer, app->size);
|
|
std::cout << GetAccountName() << " attempted creation of character with an invalid server id of: "
|
|
<< packet->getType_int32_ByName("server_id") << "\n";
|
|
return;
|
|
}
|
|
|
|
// Store creation request and forward to world server
|
|
createRequest_ = packet.release();
|
|
|
|
auto outpack = std::make_unique<ServerPacket>(ServerOP_CharacterCreate,
|
|
app->size + sizeof(std::int16_t));
|
|
std::int16_t out_version = GetVersion();
|
|
std::memcpy(outpack->pBuffer, &out_version, sizeof(std::int16_t));
|
|
std::memcpy(outpack->pBuffer + sizeof(std::int16_t), app->pBuffer, app->size);
|
|
|
|
// Adjust pointer based on version
|
|
unsigned char* tmp = outpack->pBuffer;
|
|
if (out_version <= 283)
|
|
{
|
|
tmp += 2;
|
|
}
|
|
else if (out_version == 373)
|
|
{
|
|
tmp += 6;
|
|
}
|
|
else
|
|
{
|
|
tmp += 7;
|
|
}
|
|
|
|
// Add account ID
|
|
std::int32_t account_id = GetAccountID();
|
|
std::memcpy(tmp, &account_id, sizeof(std::int32_t));
|
|
|
|
world_server->SendPacket(outpack.get());
|
|
}
|
|
else
|
|
{
|
|
LogWrite(WORLD__ERROR, 1, "World", "Error in character creation request from account %s!",
|
|
GetAccountName());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Processes a play character request
|
|
* @param app The play character packet
|
|
*/
|
|
void Client::ProcessPlayCharacter(EQApplicationPacket* app)
|
|
{
|
|
std::int32_t char_id = 0;
|
|
std::int32_t server_id = 0;
|
|
|
|
auto request = std::unique_ptr<PacketStruct>(
|
|
configReader.getStruct("LS_PlayRequest", GetVersion()));
|
|
|
|
if (request && request->LoadPacketData(app->pBuffer, app->size))
|
|
{
|
|
char_id = request->getType_int32_ByName("char_id");
|
|
|
|
// Handle version-specific server ID retrieval
|
|
if (GetVersion() <= 283)
|
|
{
|
|
server_id = database.GetServer(GetAccountID(), char_id,
|
|
request->getType_EQ2_16BitString_ByName("name").data);
|
|
}
|
|
else
|
|
{
|
|
server_id = request->getType_int32_ByName("server_id");
|
|
}
|
|
|
|
// Find world server and character
|
|
LWorld* world = world_list.FindByID(server_id);
|
|
std::string name = database.GetCharacterName(char_id, server_id, GetAccountID());
|
|
|
|
if (world && !name.empty())
|
|
{
|
|
pending_play_char_id_ = char_id;
|
|
|
|
// Create user to world request
|
|
auto outpack = std::make_unique<ServerPacket>(ServerOP_UsertoWorldReq,
|
|
sizeof(UsertoWorldRequest_Struct));
|
|
auto* req = reinterpret_cast<UsertoWorldRequest_Struct*>(outpack->pBuffer);
|
|
req->char_id = char_id;
|
|
req->lsaccountid = GetAccountID();
|
|
req->worldid = server_id;
|
|
|
|
// Set IP address
|
|
struct in_addr in;
|
|
in.s_addr = GetIP();
|
|
std::strcpy(req->ip_address, inet_ntoa(in));
|
|
|
|
world->SendPacket(outpack.get());
|
|
|
|
// Start play wait timer
|
|
playWaitTimer_.reset();
|
|
playWaitTimer_ = std::make_unique<Timer>(5000);
|
|
playWaitTimer_->Start();
|
|
}
|
|
else
|
|
{
|
|
std::cout << GetAccountName() << " sent invalid Play Request" << std::endl;
|
|
SendPlayFailed(PLAY_ERROR_PROBLEM);
|
|
DumpPacket(app);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Processes a delete character request
|
|
* @param app The delete character packet
|
|
*/
|
|
void Client::ProcessDeleteCharacter(EQApplicationPacket* app)
|
|
{
|
|
auto request = std::unique_ptr<PacketStruct>(
|
|
configReader.getStruct("LS_DeleteCharacterRequest", GetVersion()));
|
|
auto response = std::unique_ptr<PacketStruct>(
|
|
configReader.getStruct("LS_DeleteCharacterResponse", GetVersion()));
|
|
|
|
if (request && response && request->LoadPacketData(app->pBuffer, app->size))
|
|
{
|
|
EQ2_16BitString name = request->getType_EQ2_16BitString_ByName("name");
|
|
std::int32_t acct_id = GetAccountID();
|
|
std::int32_t char_id = request->getType_int32_ByName("char_id");
|
|
std::int32_t server_id = request->getType_int32_ByName("server_id");
|
|
|
|
// Verify deletion
|
|
if (database.VerifyDelete(acct_id, char_id, name.data.c_str()))
|
|
{
|
|
response->setDataByName("response", 1);
|
|
GetLoginAccount()->removeCharacter(name.data.c_str(), GetVersion());
|
|
|
|
// Notify world server
|
|
LWorld* world_server = world_list.FindByID(server_id);
|
|
if (world_server != nullptr)
|
|
{
|
|
world_server->SendDeleteCharacter(char_id, acct_id);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
response->setDataByName("response", 0);
|
|
}
|
|
|
|
// Build response
|
|
response->setDataByName("server_id", server_id);
|
|
response->setDataByName("char_id", char_id);
|
|
response->setDataByName("account_id", account_id_);
|
|
response->setMediumStringByName("name", name.data.c_str());
|
|
response->setDataByName("max_characters", 10);
|
|
|
|
EQ2Packet* outapp = response->serialize();
|
|
QueuePacket(outapp);
|
|
|
|
// Refresh character list
|
|
SendCharList();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Saves client error logs to the database
|
|
* @param app Packet containing error data
|
|
* @param type Type of error log
|
|
* @param version Client version
|
|
*/
|
|
void Client::SaveErrorsToDB(EQApplicationPacket* app, const char* type, std::int32_t version)
|
|
{
|
|
std::int32_t size = 0;
|
|
z_stream zstream{};
|
|
|
|
// Extract size and data based on version
|
|
if (version >= 546)
|
|
{
|
|
std::memcpy(&size, app->pBuffer + sizeof(std::int32_t), sizeof(std::int32_t));
|
|
zstream.next_in = app->pBuffer + 8;
|
|
zstream.avail_in = app->size - 8;
|
|
}
|
|
else
|
|
{
|
|
// Box set version
|
|
size = 0xFFFF;
|
|
zstream.next_in = app->pBuffer + 2;
|
|
zstream.avail_in = app->size - 2;
|
|
}
|
|
|
|
size++;
|
|
auto message = std::make_unique<char[]>(size);
|
|
std::memset(message.get(), 0, size);
|
|
|
|
// Setup decompression
|
|
zstream.next_out = reinterpret_cast<Bytef*>(message.get());
|
|
zstream.avail_out = size;
|
|
zstream.zalloc = Z_NULL;
|
|
zstream.zfree = Z_NULL;
|
|
zstream.opaque = Z_NULL;
|
|
|
|
// Decompress message
|
|
int zerror = inflateInit(&zstream);
|
|
if (zerror != Z_OK)
|
|
{
|
|
return;
|
|
}
|
|
|
|
zerror = inflate(&zstream, 0);
|
|
inflateEnd(&zstream);
|
|
|
|
// Save to database if message is valid
|
|
if (message && std::strlen(message.get()) > 0)
|
|
{
|
|
database.SaveClientLog(type, message.get(), GetLoginAccount()->getLoginName(), GetVersion());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Handles character creation approval from world server
|
|
* @param server_id World server ID
|
|
* @param char_id New character ID
|
|
*/
|
|
void Client::CharacterApproved(std::int32_t server_id, std::int32_t char_id)
|
|
{
|
|
if (createRequest_ && server_id == createRequest_->getType_int32_ByName("server_id"))
|
|
{
|
|
LWorld* world_server = world_list.FindByID(server_id);
|
|
if (!world_server)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Send success response
|
|
auto packet = std::unique_ptr<PacketStruct>(
|
|
configReader.getStruct("LS_CreateCharacterReply", GetVersion()));
|
|
|
|
if (packet)
|
|
{
|
|
packet->setDataByName("account_id", GetAccountID());
|
|
packet->setDataByName("unknown", 0xFFFFFFFF);
|
|
packet->setDataByName("response", CREATESUCCESS_REPLY);
|
|
packet->setMediumStringByName("name",
|
|
createRequest_->getType_EQ2_16BitString_ByName("name").data.c_str());
|
|
|
|
EQ2Packet* outapp = packet->serialize();
|
|
QueuePacket(outapp);
|
|
|
|
// Save character to database
|
|
database.SaveCharacter(createRequest_, GetLoginAccount(), char_id, GetVersion());
|
|
|
|
// Refresh character list
|
|
database.LoadCharacters(GetLoginAccount(), GetVersion());
|
|
SendCharList();
|
|
|
|
// Auto-enter world for older clients
|
|
if (GetVersion() <= 561)
|
|
{
|
|
pending_play_char_id_ = char_id;
|
|
|
|
auto outpack = std::make_unique<ServerPacket>(ServerOP_UsertoWorldReq,
|
|
sizeof(UsertoWorldRequest_Struct));
|
|
auto* req = reinterpret_cast<UsertoWorldRequest_Struct*>(outpack->pBuffer);
|
|
req->char_id = char_id;
|
|
req->lsaccountid = GetAccountID();
|
|
req->worldid = server_id;
|
|
|
|
struct in_addr in;
|
|
in.s_addr = GetIP();
|
|
std::strcpy(req->ip_address, inet_ntoa(in));
|
|
|
|
world_server->SendPacket(outpack.get());
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
std::cout << GetAccountName() << " received invalid CharacterApproval from server: "
|
|
<< server_id << std::endl;
|
|
}
|
|
|
|
safe_delete(createRequest_);
|
|
}
|
|
|
|
/**
|
|
* @brief Handles character creation rejection
|
|
* @param reason_number Rejection reason code
|
|
*/
|
|
void Client::CharacterRejected(std::int8_t reason_number)
|
|
{
|
|
auto packet = std::unique_ptr<PacketStruct>(
|
|
configReader.getStruct("LS_CreateCharacterReply", GetVersion()));
|
|
|
|
if (createRequest_ && packet)
|
|
{
|
|
packet->setDataByName("account_id", GetAccountID());
|
|
packet->setDataByName("response", reason_number);
|
|
packet->setMediumStringByName("name", "");
|
|
|
|
EQ2Packet* outapp = packet->serialize();
|
|
QueuePacket(outapp);
|
|
}
|
|
|
|
safe_delete(createRequest_);
|
|
}
|
|
|
|
/**
|
|
* @brief Sends the character list to the client
|
|
*/
|
|
void Client::SendCharList()
|
|
{
|
|
LogWrite(LOGIN__INFO, 0, "Login", "[%s] sending character list.", GetAccountName());
|
|
|
|
LS_CharSelectList list;
|
|
list.loadData(GetAccountID(), GetLoginAccount()->charlist, GetVersion());
|
|
|
|
EQ2Packet* outapp = list.serialize(GetVersion());
|
|
DumpPacket(outapp->pBuffer, outapp->size);
|
|
QueuePacket(outapp);
|
|
}
|
|
|
|
/**
|
|
* @brief Sends login denied due to bad version
|
|
*/
|
|
void Client::SendLoginDeniedBadVersion()
|
|
{
|
|
auto app = std::make_unique<EQ2Packet>(OP_LoginReplyMsg, 0, sizeof(LS_LoginResponse));
|
|
auto* ls_response = reinterpret_cast<LS_LoginResponse*>(app->pBuffer);
|
|
|
|
ls_response->reply_code = 6; // Version mismatch
|
|
ls_response->unknown03 = 0xFFFFFFFF;
|
|
ls_response->unknown04 = 0xFFFFFFFF;
|
|
|
|
QueuePacket(app.release());
|
|
StartDisconnectTimer();
|
|
}
|
|
|
|
/**
|
|
* @brief Sends login denied message
|
|
*/
|
|
void Client::SendLoginDenied()
|
|
{
|
|
auto app = std::make_unique<EQ2Packet>(OP_LoginReplyMsg, 0, sizeof(LS_LoginResponse));
|
|
auto* ls_response = reinterpret_cast<LS_LoginResponse*>(app->pBuffer);
|
|
|
|
// Reply codes for AoM:
|
|
// 1 = Invalid username or password
|
|
// 2 = Account already playing
|
|
// 6 = Version mismatch
|
|
// 7 = No scheduled playtimes
|
|
// 8 = Missing features
|
|
// 11 = Build mismatch
|
|
// 12 = Must update password
|
|
ls_response->reply_code = 1;
|
|
ls_response->unknown03 = 0xFFFFFFFF;
|
|
ls_response->unknown04 = 0xFFFFFFFF;
|
|
|
|
QueuePacket(app.release());
|
|
StartDisconnectTimer();
|
|
}
|
|
|
|
/**
|
|
* @brief Sends login accepted message
|
|
* @param account_id Account ID
|
|
* @param login_response Login response code
|
|
*/
|
|
void Client::SendLoginAccepted(std::int32_t account_id, std::int8_t login_response)
|
|
{
|
|
auto packet = std::unique_ptr<PacketStruct>(
|
|
configReader.getStruct("LS_LoginReplyMsg", GetVersion()));
|
|
|
|
if (packet)
|
|
{
|
|
packet->setDataByName("account_id", account_id);
|
|
packet->setDataByName("login_response", login_response);
|
|
packet->setDataByName("do_not_force_soga", 1);
|
|
|
|
// Set subscription and expansion flags
|
|
packet->setDataByName("sub_level", net.GetDefaultSubscriptionLevel());
|
|
packet->setDataByName("race_flag", 0x1FFFFF);
|
|
packet->setDataByName("class_flag", 0x7FFFFFE);
|
|
packet->setMediumStringByName("username", GetAccountName());
|
|
packet->setMediumStringByName("password", GetAccountName());
|
|
|
|
// Expansion flags
|
|
packet->setDataByName("unknown5", net.GetExpansionFlag());
|
|
packet->setDataByName("unknown6", 0xFF);
|
|
packet->setDataByName("unknown6", 0xFF, 1);
|
|
packet->setDataByName("unknown6", 0xFF, 2);
|
|
|
|
// Class access
|
|
packet->setDataByName("unknown10", 0xFF);
|
|
|
|
// Race and city flags
|
|
packet->setDataByName("unknown7", net.GetEnabledRaces());
|
|
packet->setDataByName("unknown7a", 0xEE);
|
|
packet->setDataByName("unknown8", net.GetCitiesFlag(), 1);
|
|
|
|
EQ2Packet* outapp = packet->serialize();
|
|
QueuePacket(outapp);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Sends the world server list
|
|
*/
|
|
void Client::SendWorldList()
|
|
{
|
|
EQ2Packet* pack = world_list.MakeServerListPacket(lsadmin_, version_);
|
|
EQ2Packet* dupe = pack->Copy();
|
|
|
|
DumpPacket(dupe->pBuffer, dupe->size);
|
|
QueuePacket(dupe);
|
|
|
|
// Trigger special client code path
|
|
SendLoginAccepted(0, 10);
|
|
}
|
|
|
|
/**
|
|
* @brief Queues a packet for sending
|
|
* @param app Packet to queue
|
|
*/
|
|
void Client::QueuePacket(EQ2Packet* app)
|
|
{
|
|
eqnc_->EQ2QueuePacket(app);
|
|
}
|
|
|
|
/**
|
|
* @brief Handles world server response for play request
|
|
* @param worldid World server ID
|
|
* @param response Response code
|
|
* @param ip_address World server IP
|
|
* @param port World server port
|
|
* @param access_key Access key
|
|
*/
|
|
void Client::WorldResponse(std::int32_t worldid, std::int8_t response,
|
|
char* ip_address, std::int32_t port, std::int32_t access_key)
|
|
{
|
|
LWorld* world = world_list.FindByID(worldid);
|
|
if (world == nullptr)
|
|
{
|
|
FatalError(0);
|
|
return;
|
|
}
|
|
|
|
if (response != 1)
|
|
{
|
|
// Handle character not loaded error
|
|
if (response == PLAY_ERROR_CHAR_NOT_LOADED)
|
|
{
|
|
std::string pending_play_char_name = database.GetCharacterName(
|
|
pending_play_char_id_, worldid, GetAccountID());
|
|
|
|
if (database.VerifyDelete(GetAccountID(), pending_play_char_id_,
|
|
pending_play_char_name.c_str()))
|
|
{
|
|
GetLoginAccount()->removeCharacter(pending_play_char_name.c_str(), GetVersion());
|
|
}
|
|
}
|
|
FatalError(response);
|
|
return;
|
|
}
|
|
|
|
// Send successful play response
|
|
auto response_packet = std::unique_ptr<PacketStruct>(
|
|
configReader.getStruct("LS_PlayResponse", GetVersion()));
|
|
|
|
if (response_packet)
|
|
{
|
|
playWaitTimer_.reset();
|
|
|
|
response_packet->setDataByName("response", 1);
|
|
response_packet->setSmallStringByName("server", ip_address);
|
|
response_packet->setDataByName("port", port);
|
|
response_packet->setDataByName("account_id", GetAccountID());
|
|
response_packet->setDataByName("access_code", access_key);
|
|
|
|
EQ2Packet* outapp = response_packet->serialize();
|
|
QueuePacket(outapp);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Sends a fatal error to the client
|
|
* @param response Error response code
|
|
*/
|
|
void Client::FatalError(std::int8_t response)
|
|
{
|
|
playWaitTimer_.reset();
|
|
SendPlayFailed(response);
|
|
}
|
|
|
|
/**
|
|
* @brief Sends play failed message
|
|
* @param response Failure reason code
|
|
*/
|
|
void Client::SendPlayFailed(std::int8_t response)
|
|
{
|
|
auto response_packet = std::unique_ptr<PacketStruct>(
|
|
configReader.getStruct("LS_PlayResponse", GetVersion()));
|
|
|
|
if (response_packet)
|
|
{
|
|
response_packet->setDataByName("response", response);
|
|
response_packet->setSmallStringByName("server", "");
|
|
response_packet->setDataByName("port", 0);
|
|
response_packet->setDataByName("account_id", GetAccountID());
|
|
response_packet->setDataByName("access_code", 0);
|
|
|
|
EQ2Packet* outapp = response_packet->serialize();
|
|
QueuePacket(outapp);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Starts the disconnect timer
|
|
*/
|
|
void Client::StartDisconnectTimer()
|
|
{
|
|
if (!disconnectTimer)
|
|
{
|
|
disconnectTimer = std::make_unique<Timer>(1000);
|
|
disconnectTimer->Start();
|
|
}
|
|
}
|
|
|
|
// ClientList implementation
|
|
|
|
/**
|
|
* @brief Adds a client to the list
|
|
* @param client Client to add
|
|
*/
|
|
void ClientList::Add(Client* client)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
client_list_[client] = true;
|
|
}
|
|
|
|
/**
|
|
* @brief Finds a client by IP and port
|
|
* @param ip Client IP
|
|
* @param port Client port
|
|
* @return Client pointer or nullptr
|
|
*/
|
|
Client* ClientList::Get(std::int32_t ip, std::int16_t port)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
|
|
for (auto& [client, active] : client_list_)
|
|
{
|
|
if (client->GetIP() == ip && client->GetPort() == port)
|
|
{
|
|
return client;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
/**
|
|
* @brief Finds clients waiting for character creation
|
|
*/
|
|
void ClientList::FindByCreateRequest()
|
|
{
|
|
Client* found_client = nullptr;
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
|
|
for (auto& [client, active] : client_list_)
|
|
{
|
|
if (client->AwaitingCharCreationRequest())
|
|
{
|
|
if (!found_client)
|
|
{
|
|
found_client = client;
|
|
}
|
|
else
|
|
{
|
|
// More than one waiting, don't send rejection
|
|
found_client = nullptr;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (found_client)
|
|
{
|
|
found_client->CharacterRejected(UNKNOWNERROR_REPLY);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Finds a client by login server account ID
|
|
* @param lsaccountid Account ID
|
|
* @return Client pointer or nullptr
|
|
*/
|
|
Client* ClientList::FindByLSID(std::int32_t lsaccountid)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
|
|
for (auto& [client, active] : client_list_)
|
|
{
|
|
if (client->GetAccountID() == lsaccountid)
|
|
{
|
|
return client;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
/**
|
|
* @brief Sends a packet to all connected clients
|
|
* @param app Packet to send
|
|
*/
|
|
void ClientList::SendPacketToAllClients(EQ2Packet* app)
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
|
|
if (!client_list_.empty())
|
|
{
|
|
for (auto& [client, active] : client_list_)
|
|
{
|
|
client->QueuePacket(app->Copy());
|
|
}
|
|
}
|
|
}
|
|
|
|
safe_delete(app);
|
|
}
|
|
|
|
/**
|
|
* @brief Processes all clients in the list
|
|
*/
|
|
void ClientList::Process()
|
|
{
|
|
std::vector<Client*> erase_list;
|
|
|
|
// Process clients and identify disconnected ones
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
|
|
for (auto& [client, active] : client_list_)
|
|
{
|
|
if (!client->Process())
|
|
{
|
|
erase_list.push_back(client);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove disconnected clients
|
|
if (!erase_list.empty())
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
|
|
for (Client* client : erase_list)
|
|
{
|
|
struct in_addr in;
|
|
in.s_addr = client->getConnection()->GetRemoteIP();
|
|
|
|
net.numclients--;
|
|
|
|
LogWrite(LOGIN__INFO, 0, "Login",
|
|
"Removing client from ip: %s on port %i, Account Name: %s",
|
|
inet_ntoa(in),
|
|
ntohs(client->getConnection()->GetRemotePort()),
|
|
client->GetAccountName());
|
|
|
|
client->getConnection()->Close();
|
|
net.UpdateWindowTitle();
|
|
client_list_.erase(client);
|
|
delete client;
|
|
}
|
|
}
|
|
} |