diff --git a/old/login/client.cpp b/old/login/client.cpp index d41728f..8634d43 100644 --- a/old/login/client.cpp +++ b/old/login/client.cpp @@ -4,23 +4,28 @@ This file is part of EQ2Emulator. */ + +// Debug includes #include "../common/debug.h" -#ifdef WIN32 -#define WIN32_LEAN_AND_MEAN -#include -#include -#include -#else + +// Linux network headers #include #include #include #include -#endif -#include -#include -#include -#include +// Standard library includes +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Project includes #include "net.h" #include "client.h" #include "../common/EQStream.h" @@ -33,781 +38,1088 @@ #include "../common/ConfigReader.h" #include "../common/Log.h" -extern NetConnection net; -extern LWorldList world_list; -extern ClientList client_list; -extern LoginDatabase database; -extern mapEQOpcodeManager; +// Global instances +extern NetConnection net; +extern LWorldList world_list; +extern ClientList client_list; +extern LoginDatabase database; +extern std::map EQOpcodeManager; extern ConfigReader configReader; + using namespace std; -Client::Client(EQStream* ieqnc) { - eqnc = ieqnc; - ip = eqnc->GetrIP(); - port = ntohs(eqnc->GetrPort()); - account_id = 0; - lsadmin = 0; - worldadmin = 0; - lsstatus = 0; - version = 0; - kicked = false; - verified = false; - memset(bannedreason, 0, sizeof(bannedreason)); - //worldresponse_timer = new Timer(10000); - //worldresponse_timer->Disable(); - memset(key,0,10); - LoginMode = None; - num_updates = 0; - updatetimer = new Timer(500); - updatelisttimer = new Timer(10000); - //keepalive = new Timer(5000); - //logintimer = new Timer(500); // Give time for the servers to send updates - //keepalive->Start(); - //updatetimer->Start(); - //logintimer->Disable(); - disconnectTimer = 0; - memset(ClientSession,0,25); - request_num = 0; - login_account = 0; - createRequest = 0; - playWaitTimer = NULL; - start = false; - update_position = 0; - update_packets = 0; - needs_world_list = true; - sent_character_list = false; -} -Client::~Client() { - //safe_delete(worldresponse_timer); - //safe_delete(logintimer); - safe_delete(login_account); - eqnc->Close(); - safe_delete(playWaitTimer); - safe_delete(createRequest); - safe_delete(disconnectTimer); - safe_delete(updatetimer); -} - -bool Client::Process() { - if(!start && !eqnc->CheckActive()){ - if(!playWaitTimer) - playWaitTimer = new Timer(5000); - else if(playWaitTimer->Check()){ - safe_delete(playWaitTimer); - return false; - } - return true; - } - else if(!start){ - safe_delete(playWaitTimer); - start = true; - } - - if (disconnectTimer && disconnectTimer->Check()) - { - safe_delete(disconnectTimer); - getConnection()->SendDisconnect(); - } - - if (!kicked) { - /************ Get all packets from packet manager out queue and process them ************/ - EQApplicationPacket *app = 0; - /*if(logintimer && logintimer->Check()) - { - database.LoadCharacters(GetLoginAccount()); - SendLoginAccepted(); - logintimer->Disable(); - }*/ - /*if(worldresponse_timer && worldresponse_timer->Check()) - { - FatalError(WorldDownErrorMessage); - worldresponse_timer->Disable(); - }*/ - - if(playWaitTimer != NULL && playWaitTimer->Check ( ) ) - { - SendPlayFailed(PLAY_ERROR_SERVER_TIMEOUT); - safe_delete(playWaitTimer); - } - if(!needs_world_list && updatetimer && updatetimer->Check()){ - if(updatelisttimer && updatelisttimer->Check()){ - if(num_updates >= 180){ //30 minutes - getConnection()->SendDisconnect(); - } - else{ - vector::iterator itr; - if(update_packets){ - for(itr = update_packets->begin(); itr != update_packets->end(); itr++){ - safe_delete(*itr); - } - } - safe_delete(update_packets); - update_packets = world_list.GetServerListUpdate(version); - } - num_updates++; - } - else{ - 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; - } - } - } - - while(app = eqnc->PopPacket()) - { - switch(app->GetOpcode()) - { - case OP_LoginRequestMsg:{ - DumpPacket(app); - PacketStruct* packet = 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); - - if (version == 0 || EQOpcodeManager.count(GetOpcodeVersion(version)) == 0) - { - safe_delete(packet); - packet = configReader.getStruct("LS_LoginRequest", 1208); - if (packet && packet->LoadPacketData(app->pBuffer, app->size)) { - version = packet->getType_int16_ByName("version"); - } - else - break; - } - //[7:19 PM] Kirmmin: Well, I very quickly learned that unknown3 in LS_LoginRequest packet is the same value as cl_eqversion in the eq2_defaults.ini file. - - LogWrite(LOGIN__DEBUG, 0, "Login", "New Client Version Provided: %i", version); - - if (EQOpcodeManager.count(GetOpcodeVersion(version)) == 0) { - LogWrite(LOGIN__ERROR, 0, "Login", "Incompatible client version provided: %i", version); - SendLoginDenied(); - return false; - } - - 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"); - LoginAccount* acct = database.LoadAccount(username.data.c_str(),password.data.c_str(), net.IsAllowingAccountCreation()); - if(acct){ - Client* otherclient = client_list.FindByLSID(acct->getLoginAccountID()); - if(otherclient) - otherclient->getConnection()->SendDisconnect(); // This person is already logged in, we don't want them logged in twice, kick the previous client as it might be a ghost - } - 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.", (char*)username.data.c_str()); - } - else - { - if (username.size > 0) - LogWrite(LOGIN__ERROR, 0, "Login", "%s login failed!", (char*)username.data.c_str()); - else - LogWrite(LOGIN__ERROR, 0, "Login", "[UNKNOWN USER] login failed!"); - } - - if(!acct) - SendLoginDenied(); - else{ - needs_world_list = true; - SetLoginAccount(acct); - SendLoginAccepted(); - } - } - else{ - cout << "Error bad version: " << version << endl; - SendLoginDeniedBadVersion(); - } - } - else{ - cout << "Error loading LS_LoginRequest packet: \n"; - //DumpPacket(app); - } - safe_delete(packet); - break; - } - case OP_KeymapLoadMsg:{ - // cout << "Received OP_KeymapNoneMsg\n"; - //dunno what this is for - break; - } - case OP_AllWSDescRequestMsg:{ - SendWorldList(); - needs_world_list = false; - if(!sent_character_list) { - database.LoadCharacters(GetLoginAccount(), GetVersion()); - sent_character_list = true; - } - SendCharList(); - break; - } - case OP_LsClientCrashlogReplyMsg:{ -// DumpPacket(app); - SaveErrorsToDB(app, "Crash Log", GetVersion()); - break; - } - case OP_LsClientVerifylogReplyMsg:{ -// DumpPacket(app); - SaveErrorsToDB(app, "Verify Log", GetVersion()); - break; - } - case OP_LsClientAlertlogReplyMsg:{ -// DumpPacket(app); - SaveErrorsToDB(app, "Alert Log", GetVersion()); - break; - } - case OP_LsClientBaselogReplyMsg:{ -// DumpPacket(app); - SaveErrorsToDB(app, "Base Log", GetVersion()); - break; - } - case OP_AllCharactersDescRequestMsg:{ - break; - } - case OP_CreateCharacterRequestMsg:{ - PacketStruct* packet = configReader.getStruct("CreateCharacter", GetVersion()); - - DumpPacket(app); - playWaitTimer = new 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()); - LWorld* world_server = world_list.FindByID(packet->getType_int32_ByName("server_id")); - if(!world_server) - { - DumpPacket(app->pBuffer, app->size); - cout << GetAccountName() << " attempted creation of character with an invalid server id of: " << packet->getType_int32_ByName("server_id") << "\n"; - break; - } - else - { - createRequest = packet; - ServerPacket* outpack = new ServerPacket(ServerOP_CharacterCreate, app->size+sizeof(int16)); - int16 out_version = GetVersion(); - memcpy(outpack->pBuffer, &out_version, sizeof(int16)); - memcpy(outpack->pBuffer + sizeof(int16), app->pBuffer, app->size); - uchar* tmp = outpack->pBuffer; - - if(out_version<=283) - tmp+=2; - else if(out_version == 373) { - tmp += 6; - } - else - tmp += 7; - - int32 account_id = GetAccountID(); - memcpy(tmp, &account_id, sizeof(int32)); - world_server->SendPacket(outpack); - safe_delete(outpack); - } - } - else{ - LogWrite(WORLD__ERROR, 1, "World", "Error in character creation request from account %s!", GetAccountName()); - safe_delete(packet); - } - // world_list.SendWorldChanged(create.profile.server_id, false, this); - break; - } - case OP_PlayCharacterRequestMsg:{ - int32 char_id = 0; - int32 server_id = 0; - PacketStruct* request = configReader.getStruct("LS_PlayRequest",GetVersion()); - if(request && request->LoadPacketData(app->pBuffer,app->size)){ - char_id = request->getType_int32_ByName("char_id"); - 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"); - } - LWorld* world = world_list.FindByID(server_id); - string name = database.GetCharacterName(char_id,server_id,GetAccountID()); - if(world && name.length() > 0){ - pending_play_char_id = char_id; - ServerPacket* outpack = new ServerPacket(ServerOP_UsertoWorldReq, sizeof(UsertoWorldRequest_Struct)); - UsertoWorldRequest_Struct* req = (UsertoWorldRequest_Struct*)outpack->pBuffer; - req->char_id = char_id; - req->lsaccountid = GetAccountID(); - req->worldid = server_id; - - struct in_addr in; - in.s_addr = GetIP(); - strcpy(req->ip_address, inet_ntoa(in)); - world->SendPacket(outpack); - delete outpack; - - safe_delete(playWaitTimer); - - playWaitTimer = new Timer ( 5000 ); - playWaitTimer->Start ( ); - } - else{ - cout << GetAccountName() << " sent invalid Play Request: \n"; - SendPlayFailed(PLAY_ERROR_PROBLEM); - DumpPacket(app); - } - } - safe_delete(request); - break; - } - case OP_DeleteCharacterRequestMsg:{ - PacketStruct* request = configReader.getStruct("LS_DeleteCharacterRequest", GetVersion()); - PacketStruct* response = configReader.getStruct("LS_DeleteCharacterResponse", GetVersion()); - if(request && response && request->LoadPacketData(app->pBuffer,app->size)){ - EQ2_16BitString name = request->getType_EQ2_16BitString_ByName("name"); - int32 acct_id = GetAccountID(); - int32 char_id = request->getType_int32_ByName("char_id"); - int32 server_id = request->getType_int32_ByName("server_id"); - if(database.VerifyDelete(acct_id, char_id, name.data.c_str())){ - response->setDataByName("response", 1); - GetLoginAccount()->removeCharacter((char*)name.data.c_str(), GetVersion()); - LWorld* world_server = world_list.FindByID(server_id); - if(world_server != NULL) - world_server->SendDeleteCharacter ( char_id , acct_id ); - } - else - response->setDataByName("response", 0); - response->setDataByName("server_id", server_id); - response->setDataByName("char_id", char_id); - response->setDataByName("account_id", account_id); - response->setMediumStringByName("name", (char*)name.data.c_str()); - response->setDataByName("max_characters", 10); - - EQ2Packet* outapp = response->serialize(); - QueuePacket(outapp); - - this->SendCharList(); - } - safe_delete(request); - safe_delete(response); - 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()); - } - } - delete app; - } - } - - if (!eqnc->CheckActive()) { - return false; - } - - return true; -} - -void Client::SaveErrorsToDB(EQApplicationPacket* app, char* type, int32 version){ - int32 size = 0; - z_stream zstream; - if (version >= 546) { - memcpy(&size, app->pBuffer + sizeof(int32), sizeof(int32)); - zstream.next_in = app->pBuffer + 8; - zstream.avail_in = app->size - 8; - } - else { //box set - size = 0xFFFF; - zstream.next_in = app->pBuffer + 2; - zstream.avail_in = app->size - 2; - } - size++; - char* message = new char[size]; - memset(message, 0, size); - - int zerror = 0; - - zstream.next_out = (BYTE*)message; - zstream.avail_out = size; - zstream.zalloc = Z_NULL; - zstream.zfree = Z_NULL; - zstream.opaque = Z_NULL; - - zerror = inflateInit( &zstream); - if(zerror != Z_OK) { - safe_delete_array(message); - return; - } - zerror = inflate( &zstream, 0 ); - if(message && strlen(message) > 0) - database.SaveClientLog(type, message, GetLoginAccount()->getLoginName(), GetVersion()); - safe_delete_array(message); -} - -void Client::CharacterApproved(int32 server_id,int32 char_id) +/** + * @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} { - if(createRequest && server_id == createRequest->getType_int32_ByName("server_id")){ - LWorld* world_server = world_list.FindByID(server_id); - if(!world_server) - return; - - PacketStruct* packet = configReader.getStruct("LS_CreateCharacterReply", GetVersion()); - if(packet){ - packet->setDataByName("account_id", GetAccountID()); - packet->setDataByName("unknown", 0xFFFFFFFF); - packet->setDataByName("response", CREATESUCCESS_REPLY); - packet->setMediumStringByName("name", (char*)createRequest->getType_EQ2_16BitString_ByName("name").data.c_str()); - EQ2Packet* outapp = packet->serialize(); - QueuePacket(outapp); - safe_delete(packet); - database.SaveCharacter(createRequest, GetLoginAccount(),char_id, GetVersion()); - - // refresh characters for this account - database.LoadCharacters(GetLoginAccount(), GetVersion()); - - SendCharList(); - - if (GetVersion() <= 561) - { - pending_play_char_id = char_id; - ServerPacket* outpack = new ServerPacket(ServerOP_UsertoWorldReq, sizeof(UsertoWorldRequest_Struct)); - UsertoWorldRequest_Struct* req = (UsertoWorldRequest_Struct*)outpack->pBuffer; - req->char_id = char_id; - req->lsaccountid = GetAccountID(); - req->worldid = server_id; - - struct in_addr in; - in.s_addr = GetIP(); - strcpy(req->ip_address, inet_ntoa(in)); - world_server->SendPacket(outpack); - delete outpack; - } - } - } - else{ - cout << GetAccountName() << " received invalid CharacterApproval from server: " << server_id << endl; - } - safe_delete(createRequest); + // 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(500); + updatelisttimer = std::make_unique(10000); } -void Client::CharacterRejected(int8 reason_number) +/** + * @brief Destructor - cleans up all client resources + */ +Client::~Client() { - PacketStruct* packet = configReader.getStruct("LS_CreateCharacterReply", GetVersion()); - if(createRequest && packet){ - packet->setDataByName("account_id", GetAccountID()); - int8 clientReasonNum = reason_number; - // reason numbers change and instead of updating the world server - // the login server will hold the up to date #'s -/* - switch(reason_number) - { - // these error codes seem to be removed now, they shutdown the client rather immediately - // for now we are just going to play a joke on them and say they can't create a new character. - case INVALIDRACE_REPLY: - case INVALIDGENDER_REPLY: - clientReasonNum = 8; - break; - case BADNAMELENGTH_REPLY: - clientReasonNum = 9; - break; - case NAMEINVALID_REPLY: - clientReasonNum = 10; - break; - case NAMEFILTER_REPLY: - clientReasonNum = 11; - break; - case NAMETAKEN_REPLY: - clientReasonNum = 12; - break; - case OVERLOADEDSERVER_REPLY: - clientReasonNum = 13; - break; - } -*/ - packet->setDataByName("response", clientReasonNum); - packet->setMediumStringByName("name", ""); - EQ2Packet* outapp = packet->serialize(); - QueuePacket(outapp); - safe_delete(packet); - } - /*LS_CreateCharacterReply reply(GetAccountID(), reason_number, create.profile.name.data); - EQ2Packet* outapp = reply.serialize(); - QueuePacket(outapp); - create.Clear();*/ + // 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_); + } } -void Client::SendCharList(){ - /*PacketStruct* packet = configReader.getStruct("LS_CreateCharacterReply"); - packet->setDataByName("account_id", GetAccountID()); - packet->setDataByName("response", reason_number); - packet->setDataByName("name", &create.profile.name); - EQ2Packet* outapp = packet->serialize(); - QueuePacket(outapp); - safe_delete(packet);*/ - 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); - -} -void Client::SendLoginDeniedBadVersion(){ - EQ2Packet* app = new EQ2Packet(OP_LoginReplyMsg, 0, sizeof(LS_LoginResponse)); - LS_LoginResponse* ls_response = (LS_LoginResponse*)app->pBuffer; - ls_response->reply_code = 6; - ls_response->unknown03 = 0xFFFFFFFF; - ls_response->unknown04 = 0xFFFFFFFF; - QueuePacket(app); - StartDisconnectTimer(); -} -void Client::SendLoginDenied(){ - EQ2Packet* app = new EQ2Packet(OP_LoginReplyMsg, 0, sizeof(LS_LoginResponse)); - LS_LoginResponse* ls_response = (LS_LoginResponse*)app->pBuffer; - ls_response->reply_code = 1; - // reply_codes for AoM: - /* 1 = Login rejected: Invalid username or password. Please try again. - 2 = Login rejected: Server thinks your account is currently playing; you may have to wait " - "a few minutes for it to clear, then try again - 6 = Login rejected: The client's version does not match the server's. Please re-run the patcher. - 7 = Login rejected: You have no scheduled playtimes. - 8 = Your account does not have the features required to play on this server. - 11 = The client's build does not match the server's. Please re-run the patcher. - 12 = You must update your password in order to log in. Pressing OK will op" - "en your web browser to the SOE password management page - Other Value > 1 = Login rejected for an unknown reason. - */ - ls_response->unknown03 = 0xFFFFFFFF; - ls_response->unknown04 = 0xFFFFFFFF; - QueuePacket(app); - StartDisconnectTimer(); -} - -void Client::SendLoginAccepted(int32 account_id, int8 login_response) { - PacketStruct* packet = configReader.getStruct("LS_LoginReplyMsg", GetVersion()); - int i = 0; - if (packet) - { - packet->setDataByName("account_id", account_id); - - packet->setDataByName("login_response", login_response); - - packet->setDataByName("do_not_force_soga", 1); - - // sub_level 0xFFFFFFFF = blacks out all portraits for class alignments, considered non membership - // sub_level > 0 = class alignments still required, but portraits are viewable and race selectable - // sub_level = 2 membership, you can 'create characters on time locked servers' vs standard - // sub_level = 0 forces popup on close to web browser - packet->setDataByName("sub_level", net.GetDefaultSubscriptionLevel()); - packet->setDataByName("race_flag", 0x1FFFFF); - packet->setDataByName("class_flag", 0x7FFFFFE); - packet->setMediumStringByName("username", GetAccountName()); - packet->setMediumStringByName("password", GetAccountName()); - - // unknown5 - // full support = 0x7CFF - // 1 << 12 (-4096) = missing echoes of faydwer, disables Fae and Arasai (black portraits) and kelethin as starting city - // 1 << 13 (-8192) = disables sarnak (black portraits) and gorowyn as starting city - packet->setDataByName("unknown5", net.GetExpansionFlag()); - packet->setDataByName("unknown6", 0xFF); - packet->setDataByName("unknown6", 0xFF, 1); - packet->setDataByName("unknown6", 0xFF, 2); - - // controls class access / playable characters - packet->setDataByName("unknown10", 0xFF); - - // packet->setDataByName("unknown7a", 0x0101); - // packet->setDataByName("race_unknown", 0x01); - packet->setDataByName("unknown7", net.GetEnabledRaces()); // 0x01-0xFF disable extra races FAE(16) ARASAI (17) SARNAK (18) -- with 4096/8192 flags, no visibility of portraits - packet->setDataByName("unknown7a", 0xEE); - packet->setDataByName("unknown8", net.GetCitiesFlag(), 1); // dword_1ECBA18 operand for race flag packs (sublevel 0,1,2?) -- (sublevel -1) controls starting zones omission 0xEE vs 0xCF (CF misses halas) - - /* - 1 = city of qeynos - 2 = city of freeport - 4 = city of kelethin - 8 = city of neriak - 16 = gorowyn - 32 = new halas - 64 = queens colony - 128 = outpost overlord - */ - - EQ2Packet* outapp = packet->serialize(); - QueuePacket(outapp); - safe_delete(packet); - } -} - -void Client::SendWorldList(){ - EQ2Packet* pack = world_list.MakeServerListPacket(lsadmin, version); - EQ2Packet* dupe = pack->Copy(); - DumpPacket(dupe->pBuffer,dupe->size); - QueuePacket(dupe); - - SendLoginAccepted(0, 10); // triggers a different code path in the client to set certain flags - return; -} - -void Client::QueuePacket(EQ2Packet* app){ - eqnc->EQ2QueuePacket(app); -} - -void Client::WorldResponse(int32 worldid, int8 response, char* ip_address, int32 port, int32 access_key) +/** + * @brief Main processing function for the client + * @return true if client is still active, false if should be disconnected + */ +bool Client::Process() { - LWorld* world = world_list.FindByID(worldid); - if(world == 0) { - FatalError(0); - return; - } - if(response != 1){ - if(response == PLAY_ERROR_CHAR_NOT_LOADED){ - 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((char*)pending_play_char_name.c_str(), GetVersion()); - } - } - FatalError(response); - return; - } + // Handle initial connection state + if (!start_ && !eqnc_->CheckActive()) + { + if (!playWaitTimer_) + { + playWaitTimer_ = std::make_unique(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; + } + } + } + } - PacketStruct* response_packet = configReader.getStruct("LS_PlayResponse", GetVersion()); - if(response_packet){ - safe_delete(playWaitTimer); - 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); - safe_delete(response_packet); - } - return; -} -void Client::FatalError(int8 response) { - safe_delete(playWaitTimer); - SendPlayFailed(response); + // 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; } -void Client::SendPlayFailed(int8 response){ - PacketStruct* response_packet = 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); - safe_delete(response_packet); - } +/** + * @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(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; + } } -void ClientList::Add(Client* client) { - MClientList.writelock(); - client_list[client] = true; - MClientList.releasewritelock(); +/** + * @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(); } -Client* ClientList::Get(int32 ip, int16 port) { - Client* ret = 0; - map::iterator itr; - MClientList.readlock(); - for(itr = client_list.begin(); itr != client_list.end(); itr++){ - if(itr->first->GetIP() == ip && itr->first->GetPort() == port){ - ret = itr->first; - break; - } - } - MClientList.releasereadlock(); - return ret; +/** + * @brief Processes a character creation request + * @param app The character creation packet + */ +void Client::ProcessCharacterCreation(EQApplicationPacket* app) +{ + auto packet = std::unique_ptr( + configReader.getStruct("CreateCharacter", GetVersion())); + + DumpPacket(app); + + // Start play wait timer + playWaitTimer_ = std::make_unique(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(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()); + } } -void ClientList::FindByCreateRequest(){ - Client* client = 0; - map::iterator itr; - MClientList.readlock(); - for(itr = client_list.begin(); itr != client_list.end(); itr++){ - if(itr->first->AwaitingCharCreationRequest()){ - if(!client) - client = itr->first; - else{ - client = 0;//more than 1 character waiting, dont want to send rejection to wrong one - break; - } - } - } - MClientList.releasereadlock(); - if(client) - client->CharacterRejected(UNKNOWNERROR_REPLY); +/** + * @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( + 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(ServerOP_UsertoWorldReq, + sizeof(UsertoWorldRequest_Struct)); + auto* req = reinterpret_cast(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(5000); + playWaitTimer_->Start(); + } + else + { + std::cout << GetAccountName() << " sent invalid Play Request" << std::endl; + SendPlayFailed(PLAY_ERROR_PROBLEM); + DumpPacket(app); + } + } } -Client* ClientList::FindByLSID(int32 lsaccountid) { - Client* client = 0; - map::iterator itr; - MClientList.readlock(); - for(itr = client_list.begin(); itr != client_list.end(); itr++){ - if(itr->first->GetAccountID() == lsaccountid){ - client = itr->first; - break; - } - } - MClientList.releasereadlock(); - return client; -} -void ClientList::SendPacketToAllClients(EQ2Packet* app){ - Client* client = 0; - map::iterator itr; - MClientList.readlock(); - if(client_list.size() > 0){ - for(itr = client_list.begin(); itr != client_list.end(); itr++){ - itr->first->QueuePacket(app->Copy()); - } - } - safe_delete(app); - MClientList.releasereadlock(); -} -void ClientList::Process() { - Client* client = 0; - vector erase_list; - map::iterator itr; - MClientList.readlock(); - for(itr = client_list.begin(); itr != client_list.end(); itr++){ - client = itr->first; - if(!client->Process()) - erase_list.push_back(client); - } - MClientList.releasereadlock(); - if(erase_list.size() > 0){ - vector::iterator erase_itr; - MClientList.writelock(); - for(erase_itr = erase_list.begin(); erase_itr != erase_list.end(); erase_itr++){ - client = *erase_itr; - 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); - } - MClientList.releasewritelock(); - } +/** + * @brief Processes a delete character request + * @param app The delete character packet + */ +void Client::ProcessDeleteCharacter(EQApplicationPacket* app) +{ + auto request = std::unique_ptr( + configReader.getStruct("LS_DeleteCharacterRequest", GetVersion())); + auto response = std::unique_ptr( + 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(size); + std::memset(message.get(), 0, size); + + // Setup decompression + zstream.next_out = reinterpret_cast(message.get()); + zstream.avail_out = size; + zstream.zalloc = Z_NULL; + zstream.zfree = Z_NULL; + zstream.opaque = Z_NULL; -void Client::StartDisconnectTimer() { - if (!disconnectTimer) - { - disconnectTimer = new Timer(1000); - disconnectTimer->Start(); - } + // 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( + 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(ServerOP_UsertoWorldReq, + sizeof(UsertoWorldRequest_Struct)); + auto* req = reinterpret_cast(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( + 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(OP_LoginReplyMsg, 0, sizeof(LS_LoginResponse)); + auto* ls_response = reinterpret_cast(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(OP_LoginReplyMsg, 0, sizeof(LS_LoginResponse)); + auto* ls_response = reinterpret_cast(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( + 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( + 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( + 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(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 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 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 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 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 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 erase_list; + + // Process clients and identify disconnected ones + { + std::lock_guard 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 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; + } + } +} \ No newline at end of file diff --git a/old/login/client.h b/old/login/client.h index 4d3936e..50d487f 100644 --- a/old/login/client.h +++ b/old/login/client.h @@ -4,128 +4,357 @@ This file is part of EQ2Emulator. */ -#ifndef CLIENT_H -#define CLIENT_H +#pragma once + +// Standard library includes +#include +#include +#include +#include +#include +#include +#include + +// Project includes #include "../common/linked_list.h" #include "../common/timer.h" #include "../common/TCPConnection.h" +#include "../common/PacketStruct.h" #include "login_structs.h" #include "LoginAccount.h" -#include "../common/PacketStruct.h" -#include -#include -enum eLoginMode { None, Normal, Registration }; +// Forward declarations class DelayQue; +class EQStream; +class EQ2Packet; +class EQApplicationPacket; + +/** + * @brief Login modes for client state management + */ +enum class LoginMode : std::uint8_t +{ + None, // No login state + Normal, // Normal login process + Registration // Account registration mode +}; + +/** + * @brief Represents a connected client in the login server + * + * This class manages individual client connections, handling: + * - Authentication and login process + * - Character selection and creation + * - World server communication + * - Packet processing and queuing + */ class Client { public: - Client(EQStream* ieqnc); + /** + * @brief Constructs a new client instance + * @param ieqnc The EQStream connection for this client + */ + explicit Client(EQStream* ieqnc); + + /** + * @brief Destructor - ensures proper cleanup of client resources + */ ~Client(); - void SendLoginDenied(); - void SendLoginDeniedBadVersion(); - void SendLoginAccepted(int32 account_id = 1, int8 login_response = 0); - void SendWorldList(); - void SendCharList(); - int16 AddWorldToList2(uchar* buffer, char* name, int32 id, int16* flags); - void GenerateChecksum(EQApplicationPacket* outapp); - int8 LoginKey[10]; - int8 ClientSession[25]; - bool Process(); - void SaveErrorsToDB(EQApplicationPacket* app, char* type, int32 version); - void CharacterApproved(int32 server_id,int32 char_id); - void CharacterRejected(int8 reason_number); - EQStream* getConnection() { return eqnc; } - LoginAccount* GetLoginAccount() { return login_account; } - void SetLoginAccount(LoginAccount* in_account) { - login_account = in_account; - if(in_account) - account_id = in_account->getLoginAccountID(); - } - int16 GetVersion(){ return version; } - char* GetKey() { return key; } - void SetKey(char* in_key) { strcpy(key,in_key); } - int32 GetIP() { return ip; } - int16 GetPort() { return port; } - int32 GetAccountID() { return account_id; } - const char* GetAccountName(){ return (char*)account_name.c_str(); } - void SetAccountName(const char* name){ account_name = string(name); } - void ProcessLogin(char* name, char* pass,int seq=0); - void QueuePacket(EQ2Packet* app); - void FatalError(int8 response); - void WorldResponse(int32 worldid, int8 response, char* ip_address, int32 port, int32 access_key); - bool AwaitingCharCreationRequest(){ - if(createRequest) - return true; - else - return false; - } - Timer* updatetimer; - Timer* updatelisttimer; - Timer* disconnectTimer; - //Timer* keepalive; - //Timer* logintimer; - int16 packettotal; - int32 requested_server_id; - int32 request_num; - LinkedList delay_que; - void SendPlayFailed(int8 response); - - void StartDisconnectTimer(); + + // Disable copy operations + Client(const Client&) = delete; + Client& operator=(const Client&) = delete; + + // Login response methods + /** + * @brief Sends a login denied message to the client + */ + void SendLoginDenied(); + + /** + * @brief Sends a login denied message due to version mismatch + */ + void SendLoginDeniedBadVersion(); + + /** + * @brief Sends a login accepted message to the client + * @param account_id The account ID (default: 1) + * @param login_response The login response code (default: 0) + */ + void SendLoginAccepted(std::int32_t account_id = 1, std::int8_t login_response = 0); + + // World and character list methods + /** + * @brief Sends the world server list to the client + */ + void SendWorldList(); + + /** + * @brief Sends the character list to the client + */ + void SendCharList(); + + /** + * @brief Adds a world server to the list buffer + * @param buffer Output buffer for world data + * @param name World server name + * @param id World server ID + * @param flags World server flags + * @return Size of data written to buffer + */ + std::int16_t AddWorldToList2(unsigned char* buffer, const char* name, + std::int32_t id, std::int16_t* flags); + + /** + * @brief Generates checksum for outgoing packet + * @param outapp Packet to generate checksum for + */ + void GenerateChecksum(EQApplicationPacket* outapp); + + /** + * @brief Main processing loop for the client + * @return true if client is still active, false if should be removed + */ + bool Process(); + + /** + * @brief Saves client error logs to database + * @param app Packet containing error data + * @param type Type of error log + * @param version Client version + */ + void SaveErrorsToDB(EQApplicationPacket* app, const char* type, std::int32_t version); + + // Character management methods + /** + * @brief Handles character creation approval from world server + * @param server_id World server ID + * @param char_id New character ID + */ + void CharacterApproved(std::int32_t server_id, std::int32_t char_id); + + /** + * @brief Handles character creation rejection + * @param reason_number Rejection reason code + */ + void CharacterRejected(std::int8_t reason_number); + + /** + * @brief Processes login attempt + * @param name Account name + * @param pass Account password + * @param seq Sequence number (default: 0) + */ + void ProcessLogin(const char* name, const char* pass, int seq = 0); + + /** + * @brief Queues a packet for sending to the client + * @param app Packet to queue + */ + void QueuePacket(EQ2Packet* app); + + /** + * @brief Sends a fatal error to the client + * @param response Error response code + */ + void FatalError(std::int8_t response); + + /** + * @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 for world server + */ + void WorldResponse(std::int32_t worldid, std::int8_t response, + char* ip_address, std::int32_t port, std::int32_t access_key); + + /** + * @brief Sends play failed message to client + * @param response Failure reason code + */ + void SendPlayFailed(std::int8_t response); + + /** + * @brief Starts the disconnect timer + */ + void StartDisconnectTimer(); + + // Getter methods + [[nodiscard]] EQStream* getConnection() { return eqnc_; } + [[nodiscard]] LoginAccount* GetLoginAccount() { return login_account_.get(); } + [[nodiscard]] std::int16_t GetVersion() const { return version_; } + [[nodiscard]] char* GetKey() { return key_; } + [[nodiscard]] std::int32_t GetIP() const { return ip_; } + [[nodiscard]] std::int16_t GetPort() const { return port_; } + [[nodiscard]] std::int32_t GetAccountID() const { return account_id_; } + [[nodiscard]] const char* GetAccountName() const { return account_name_.c_str(); } + [[nodiscard]] bool AwaitingCharCreationRequest() const { return createRequest_ != nullptr; } + + // Setter methods + void SetLoginAccount(LoginAccount* in_account) + { + login_account_.reset(in_account); + if (in_account) + { + account_id_ = in_account->getLoginAccountID(); + } + } + + void SetKey(const char* in_key) + { + std::strncpy(key_, in_key, sizeof(key_) - 1); + key_[sizeof(key_) - 1] = '\0'; + } + + void SetAccountName(const char* name) { account_name_ = name; } + + // Public data members (legacy compatibility) + std::int8_t LoginKey[10]{}; // Login key for authentication + std::int8_t ClientSession[25]{}; // Client session data + + // Timers + std::unique_ptr updatetimer; // Timer for periodic updates + std::unique_ptr updatelisttimer; // Timer for world list updates + std::unique_ptr disconnectTimer; // Timer for disconnect timeout + + // Request tracking + std::int16_t packettotal{0}; // Total packets processed + std::int32_t requested_server_id{0}; // Requested world server ID + std::int32_t request_num{0}; // Current request number + + // Delay queue for packet scheduling + LinkedList delay_que; + private: - string pending_play_char_name; - int32 pending_play_char_id; - int8 update_position; - int16 num_updates; - vector* update_packets; - LoginAccount* login_account; - EQStream* eqnc; - - int32 ip; - int16 port; - - int32 account_id; - string account_name; - char key[10]; - int8 lsadmin; - sint16 worldadmin; - int lsstatus; - bool kicked; - bool verified; - bool start; - bool needs_world_list; - int16 version; - char bannedreason[30]; - bool sent_character_list; - eLoginMode LoginMode; - PacketStruct* createRequest; - Timer* playWaitTimer; + // Character play state + std::string pending_play_char_name_; // Name of character pending play + std::int32_t pending_play_char_id_{0}; // ID of character pending play + + // Update state + std::int8_t update_position_{0}; // Current position in update sequence + std::int16_t num_updates_{0}; // Number of updates sent + std::vector* update_packets_{nullptr}; // Update packet queue + + // Account information + std::unique_ptr login_account_; // Login account data + EQStream* eqnc_; // Network connection stream + + // Connection details + std::int32_t ip_{0}; // Client IP address + std::int16_t port_{0}; // Client port + + // Account data + std::int32_t account_id_{0}; // Account ID in database + std::string account_name_; // Account name + char key_[10]{}; // Authentication key + std::int8_t lsadmin_{0}; // Login server admin level + std::int16_t worldadmin_{0}; // World server admin level + int lsstatus_{0}; // Login server status + + // Client state flags + bool kicked_{false}; // Client has been kicked + bool verified_{false}; // Client has been verified + bool start_{false}; // Client has started + bool needs_world_list_{true}; // Client needs world list + bool sent_character_list_{false}; // Character list has been sent + + // Version and mode + std::int16_t version_{0}; // Client version + LoginMode login_mode_{LoginMode::None}; // Current login mode + + // Character creation + char bannedreason_[30]{}; // Ban reason if applicable + PacketStruct* createRequest_{nullptr}; // Pending character creation request + std::unique_ptr playWaitTimer_; // Timer for play request timeout }; +/** + * @brief Manages all connected clients + * + * Thread-safe container for managing multiple client connections + */ class ClientList { public: - ClientList() {} - ~ClientList() {} - - void Add(Client* client); - Client* Get(int32 ip, int16 port); - Client* FindByLSID(int32 lsaccountid); - void FindByCreateRequest(); - void SendPacketToAllClients(EQ2Packet* app); - void Process(); + /** + * @brief Default constructor + */ + ClientList() = default; + + /** + * @brief Destructor + */ + ~ClientList() = default; + + // Disable copy operations + ClientList(const ClientList&) = delete; + ClientList& operator=(const ClientList&) = delete; + + /** + * @brief Adds a client to the list + * @param client Client to add + */ + void Add(Client* client); + + /** + * @brief Finds a client by IP and port + * @param ip Client IP address + * @param port Client port + * @return Pointer to client if found, nullptr otherwise + */ + [[nodiscard]] Client* Get(std::int32_t ip, std::int16_t port); + + /** + * @brief Finds a client by login server account ID + * @param lsaccountid Login server account ID + * @return Pointer to client if found, nullptr otherwise + */ + [[nodiscard]] Client* FindByLSID(std::int32_t lsaccountid); + + /** + * @brief Finds clients waiting for character creation response + */ + void FindByCreateRequest(); + + /** + * @brief Sends a packet to all connected clients + * @param app Packet to send + */ + void SendPacketToAllClients(EQ2Packet* app); + + /** + * @brief Processes all clients in the list + */ + void Process(); + private: - Mutex MClientList; - map client_list; + mutable std::mutex mutex_; // Mutex for thread safety + std::map client_list_; // Map of clients to active status }; -class DelayQue { + +/** + * @brief Delayed packet queue entry + * + * Holds a packet with an associated timer for delayed sending + */ +class DelayQue +{ public: - DelayQue(Timer* in_timer, EQApplicationPacket* in_packet){ - timer = in_timer; - packet = in_packet; - }; - Timer* timer; - EQApplicationPacket* packet; -}; -#endif + /** + * @brief Constructs a delayed queue entry + * @param in_timer Timer for delay + * @param in_packet Packet to delay + */ + DelayQue(Timer* in_timer, EQApplicationPacket* in_packet) + : timer(in_timer) + , packet(in_packet) + { + } + + Timer* timer; // Timer controlling the delay + EQApplicationPacket* packet; // Packet to be sent after delay +}; \ No newline at end of file