From 6de95810cd41e610f36dffc777ae372d0c229bf9 Mon Sep 17 00:00:00 2001 From: tritonality Date: Thu, 24 Jul 2025 17:51:30 +0900 Subject: [PATCH 1/5] Add complete world queue system with login server integration - Add world queue manager with account reservation tracking - Fix out of scope queue_manager call in login server client disconnect - Implement RemovePlayerFromAllQueues method for proper queue cleanup - Add ServerOP_RemoveFromQueue packet handling in world server - Restore original working architecture from queue-systemRC branch --- .gitignore | 2 +- common/CMakeLists.txt | 2 + common/queue_packets.h | 54 + common/ruletypes.h | 11 + loginserver/client.cpp | 4 +- loginserver/client.h | 22 +- loginserver/client_manager.cpp | 18 + loginserver/client_manager.h | 6 + loginserver/database.cpp | 2 +- loginserver/login_server.h | 3 +- loginserver/server_manager.cpp | 62 +- loginserver/server_manager.h | 7 +- loginserver/world_server.cpp | 299 ++- loginserver/world_server.h | 16 +- utils/scripts/queue_system/README.md | 728 ++++++++ .../queue-system-test-interactive.sh | 1660 +++++++++++++++++ .../queue_system/queue-system-test-simple | 236 +++ utils/sql/db_update_manifest.txt | 1 + .../2025_07_16_login_queue_system.sql | 39 + world/CMakeLists.txt | 4 + world/account_reservation_manager.cpp | 275 +++ world/account_reservation_manager.h | 69 + world/client.cpp | 17 +- world/client.h | 1 + world/cliententry.cpp | 9 +- world/clientlist.cpp | 40 +- world/clientlist.h | 7 +- world/login_server.cpp | 61 +- world/login_server.h | 32 +- world/login_server_list.cpp | 8 +- world/main.cpp | 8 +- world/world_queue.cpp | 983 ++++++++++ world/world_queue.h | 180 ++ world/zoneserver.cpp | 3 + 34 files changed, 4824 insertions(+), 45 deletions(-) create mode 100644 common/queue_packets.h create mode 100644 utils/scripts/queue_system/README.md create mode 100755 utils/scripts/queue_system/queue-system-test-interactive.sh create mode 100755 utils/scripts/queue_system/queue-system-test-simple create mode 100644 utils/sql/git/required/2025_07_16_login_queue_system.sql create mode 100644 world/account_reservation_manager.cpp create mode 100644 world/account_reservation_manager.h create mode 100644 world/world_queue.cpp create mode 100644 world/world_queue.h diff --git a/.gitignore b/.gitignore index 8e585f1ef..619187f0d 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,7 @@ log/ logs/ vcpkg/ perl/ - +compile_commands.json .idea/* *cbp diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index c6e1de58e..5cc4d06e3 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -456,7 +456,9 @@ SET(common_headers server_event_scheduler.h serverinfo.h servertalk.h + queue_packets.h server_reload_types.h + queue_packets.h sha1.h shareddb.h skills.h diff --git a/common/queue_packets.h b/common/queue_packets.h new file mode 100644 index 000000000..03c613ad5 --- /dev/null +++ b/common/queue_packets.h @@ -0,0 +1,54 @@ +#ifndef EQEMU_QUEUE_PACKETS_H +#define EQEMU_QUEUE_PACKETS_H + +#include "types.h" + +#pragma pack(1) + +// Queue-related opcodes (separate from main servertalk.h to avoid full rebuilds) +#define ServerOP_QueueAutoConnect 0x4013 +#define ServerOP_QueueDirectUpdate 0x4016 // World->Login: Send pre-built server list packet to specific client +#define ServerOP_QueueBatchUpdate 0x4018 // World->Login: Batch queue position updates for multiple clients +#define ServerOP_RemoveFromQueue 0x4019 // Login->World: Remove single disconnected client from queue + +// Queue-related packet structures +struct ServerQueueAutoConnect_Struct { + uint32 loginserver_account_id; + uint32 world_id; + uint32 from_id; + uint32 to_id; + uint32 ip_address; + char ip_addr_str[64]; + char forum_name[31]; + char client_key[11]; // Unique key of the client that was authorized (10 chars + null terminator) +}; + +struct ServerQueuePositionQuery_Struct { + uint32 loginserver_account_id; // Which account to query position for +}; + +struct ServerQueuePositionResponse_Struct { + uint32 loginserver_account_id; // Account this response is for + uint32 queue_position; // 0 = not queued, >0 = position in queue +}; + +struct ServerQueueDirectUpdate_Struct { + uint32 ls_account_id; // Account identifier for lookup + uint32 ip_address; + uint32 queue_position; // New queue position (0 = not queued) + uint32 estimated_wait; // Estimated wait time in seconds +}; + +struct ServerQueueBatchUpdate_Struct { + uint32 update_count; // Number of queue updates in this batch + // Followed by update_count instances of ServerQueueDirectUpdate_Struct + // Use: ServerQueueDirectUpdate_Struct* updates = (ServerQueueDirectUpdate_Struct*)((char*)packet_data + sizeof(ServerQueueBatchUpdate_Struct)); +}; + +struct ServerQueueRemoval_Struct { + uint32 ls_account_id; // Account to remove from queue when client disconnects +}; + +#pragma pack() + +#endif // EQEMU_QUEUE_PACKETS_H \ No newline at end of file diff --git a/common/ruletypes.h b/common/ruletypes.h index 326db3acf..d43738aca 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -185,6 +185,17 @@ RULE_BOOL (AlKabor, ReducedMonkAC, true, "AK behavior is true. Monks had a low RULE_BOOL (AlKabor, BlockProjectileCorners, true, "AK behavior is true. If an NPC was in a corner, arrows and bolts would not hit them.") RULE_BOOL (AlKabor, BlockProjectileWalls, true, "AK behavior is true. If an NPC was walled, then arrows and bolts had to be fired from an angle parallel to the wall in order to hit them. (if this is true, corners will also block)") RULE_BOOL (AlKabor, GreenmistHack, true, "Greenmist recourse didn't work on AK. The spell data is messed up so it's not properly fixable without modifying the client. This enables a partial workaround that is not AKurate but provides some benefit to players using this weapon.") +RULE_INT (AlKabor, PlayerPopulationCap, 1200, "Max Players allowed in the Server. Will exclude offline Bazaar trader characters.") +RULE_INT (AlKabor, TestPopulationOffset, 0, "Test population offset for queue testing") +RULE_INT (AlKabor, IPDatabaseSyncInterval, 300, "Interval in seconds between database synchronization of IP reservation data") +RULE_INT (AlKabor, MaxPlayersOnline, 1200, "Maximum number of players allowed online before queue is activated") +RULE_BOOL (AlKabor, EnableQueue, true, "Enable the login queue system when server population is at capacity") +RULE_BOOL (AlKabor, QueueBypassGMLevel, true, "Allow GMs (status >= 80) to bypass the queue") +RULE_BOOL (AlKabor, EnableQueueLogging, true, "Enable detailed logging for queue system operations") +RULE_BOOL (AlKabor, FreezeQueue, false, "Freeze queue advancement - players remain at current positions") +RULE_BOOL (AlKabor, EnableQueuePersistence, true, "Enable saving queue state to database for crash recovery") +RULE_INT (AlKabor, DefaultGracePeriod, 60, "Default grace period in seconds for IP reservations when not specified") +RULE_INT (AlKabor, IPCleanupInterval, 5, "Interval in seconds between IP reservation cleanup cycles") RULE_CATEGORY_END() RULE_CATEGORY( Map ) diff --git a/loginserver/client.cpp b/loginserver/client.cpp index 06f94eb9d..664698501 100644 --- a/loginserver/client.cpp +++ b/loginserver/client.cpp @@ -36,6 +36,8 @@ Client::Client(std::shared_ptr c, LSClientVersion v) m_sent_session_info = false; m_play_server_id = 0; m_play_sequence_id = 0; + m_queue_server_id = 0; + m_queue_position = 0; } bool Client::Process() @@ -358,7 +360,7 @@ void Client::Handle_Play(const char* data) } if (data) { - server.server_manager->SendUserToWorldRequest(data, m_account_id, m_connection->GetRemoteIP()); + server.server_manager->SendUserToWorldRequest(data, m_account_id, m_connection->GetRemoteIP(), false, m_key); } } diff --git a/loginserver/client.h b/loginserver/client.h index 8b752ac56..eecf6c10c 100644 --- a/loginserver/client.h +++ b/loginserver/client.h @@ -141,7 +141,23 @@ class Client */ unsigned int GetMacClientVersion() const { return m_client_mac_version; } - + /** + * Queue position management + */ + void SetQueuePosition(uint32 server_id, uint32 position) { + m_queue_server_id = server_id; + m_queue_position = position; + } + + uint32 GetQueueServerID() const { return m_queue_server_id; } + uint32 GetQueuePosition() const { return m_queue_position; } + + void ClearQueuePosition() { + m_queue_server_id = 0; + m_queue_position = 0; + } + + bool HasQueuePosition() const { return m_queue_server_id > 0 && m_queue_position > 0; } private: Saltme m_salt; @@ -157,6 +173,10 @@ class Client unsigned int m_play_server_id; unsigned int m_play_sequence_id; std::string m_key; + + // Queue position tracking for this client + uint32 m_queue_server_id; + uint32 m_queue_position; }; #endif diff --git a/loginserver/client_manager.cpp b/loginserver/client_manager.cpp index bb564ef5b..93dee1e06 100644 --- a/loginserver/client_manager.cpp +++ b/loginserver/client_manager.cpp @@ -110,7 +110,16 @@ void ClientManager::ProcessDisconnect() std::shared_ptr c = (*iter)->GetConnection(); if (c->CheckState(CLOSED)) { c->ReleaseFromUse(); + // Get client account ID before deletion for queue removal + uint32 account_id = (*iter)->GetAccountID(); + LogInfo("Client disconnected from the server, removing client."); + + // Remove from queue on all world servers if account ID is valid + if ((*iter)->HasQueuePosition() && account_id > 0 && server.server_manager) { + server.server_manager->RemovePlayerFromAllQueues(account_id); + } + delete (*iter); iter = clients.erase(iter); } @@ -163,3 +172,12 @@ Client *ClientManager::GetClient(unsigned int account_id) return cur; } +Client *ClientManager::GetClientByKey(const std::string& key) +{ + for (Client* client : clients) { + if (client && client->GetKey() == key) { + return client; + } + } + return nullptr; +} diff --git a/loginserver/client_manager.h b/loginserver/client_manager.h index 5c763552c..8f51e7122 100644 --- a/loginserver/client_manager.h +++ b/loginserver/client_manager.h @@ -62,6 +62,12 @@ class ClientManager * Gets a client (if exists) by their account id. */ Client *GetClient(unsigned int account_id); + + /** + * Gets a client by their unique key (for multi-client authorization matching). + */ + Client *GetClientByKey(const std::string& key); + private: /** diff --git a/loginserver/database.cpp b/loginserver/database.cpp index 4e1c91578..f88b00129 100644 --- a/loginserver/database.cpp +++ b/loginserver/database.cpp @@ -47,7 +47,7 @@ Database::Database(std::string user, std::string pass, std::string host, std::st exit(1); } else { - Log(Logs::General, Logs::Status, "Using database '%s' at %s:%d", m_database, host, port); + Log(Logs::General, Logs::Status, "Using database '%s' at %s:%d", m_database, host.c_str(), atoi(port.c_str())); } } diff --git a/loginserver/login_server.h b/loginserver/login_server.h index 0f81114ac..ff34610dc 100644 --- a/loginserver/login_server.h +++ b/loginserver/login_server.h @@ -38,7 +38,8 @@ struct LoginServer Options options; ServerManager *server_manager; ClientManager *client_manager; - + // TODO: Revist auto-connect logic + // void ProcessQueueAutoConnect(uint16_t opcode, const EQ::Net::Packet& p); }; #endif diff --git a/loginserver/server_manager.cpp b/loginserver/server_manager.cpp index bfe446407..78de4097b 100644 --- a/loginserver/server_manager.cpp +++ b/loginserver/server_manager.cpp @@ -22,11 +22,20 @@ #include "../common/eqemu_logsys.h" #include "../common/ip_util.h" +#include +#include +#include +#include +#include +#include "../common/queue_packets.h" extern EQEmuLogSys LogSys; extern LoginServer server; extern bool run_server; +// Global cache for server effective population by server ID +extern std::map g_server_populations; + ServerManager::ServerManager() { int listen_port = server.config.GetVariableInt("client_configuration", "listen_port", 5998); @@ -191,6 +200,13 @@ EQApplicationPacket* ServerManager::CreateServerListPacket(Client* c) slsf->flags = 0x1; slsf->worldid = (*iter)->GetServerId(); slsf->usercount = (*iter)->GetStatus(); + // Check if client has a queue position for this server - show queue position + if (c->HasQueuePosition() && c->GetQueueServerID() == (*iter)->GetServerId()) { + slsf->usercount = c->GetQueuePosition(); + } else if (g_server_populations.find((*iter) ->GetServerId()) != g_server_populations.end()) /* INF Only uses it if it's initated so won't impact TAKP */{ + slsf->usercount = g_server_populations[(*iter)->GetServerId()]; + } + data_ptr += sizeof(ServerListServerFlags_Struct); ++iter; } @@ -199,7 +215,7 @@ EQApplicationPacket* ServerManager::CreateServerListPacket(Client* c) return outapp; } -void ServerManager::SendUserToWorldRequest(const char* server_id, unsigned int client_account_id, uint32 ip) +void ServerManager::SendUserToWorldRequest(const char* server_id, unsigned int client_account_id, uint32 ip, bool is_auto_connect, const std::string& client_key) { auto iter = m_world_servers.begin(); bool found = false; @@ -214,6 +230,16 @@ void ServerManager::SendUserToWorldRequest(const char* server_id, unsigned int c utwr->lsaccountid = client_account_id; utwr->ip = ip; + + // Encode auto-connect status in FromID field + // 0 = manual PLAY request, 1 = auto-connect request + utwr->FromID = is_auto_connect ? 1 : 0; + utwr->ToID = 0; // Not used + + // Store client key in forum_name field (repurposing for queue system) + strncpy(utwr->forum_name, client_key.c_str(), sizeof(utwr->forum_name) - 1); + utwr->forum_name[sizeof(utwr->forum_name) - 1] = '\0'; + (*iter)->GetConnection()->Send(ServerOP_UsertoWorldReq, outapp); found = true; @@ -266,4 +292,36 @@ void ServerManager::DestroyServerByName(std::string l_name, std::string s_name, ++iter; } -} \ No newline at end of file +} + +void ServerManager::RemovePlayerFromAllQueues(uint32 ls_account_id) +{ + if (ls_account_id == 0) { + return; + } + + // Create removal packet once - reuse for all world servers + auto removal_pack = new ServerPacket(ServerOP_RemoveFromQueue, sizeof(ServerQueueRemoval_Struct)); + ServerQueueRemoval_Struct* removal = (ServerQueueRemoval_Struct*)removal_pack->pBuffer; + removal->ls_account_id = ls_account_id; + + uint32 packets_sent = 0; + + // Send removal packet to all connected world servers + auto iter = m_world_servers.begin(); + while (iter != m_world_servers.end()) { + if ((*iter)->GetConnection()) { + // Send to this world server + (*iter)->GetConnection()->SendPacket(removal_pack); + packets_sent++; + } + ++iter; + } + + // Clean up packet after sending to all servers + delete removal_pack; + + if (packets_sent > 0) { + LogInfo("Sent queue removal for disconnected client [{}] to [{}] world servers", ls_account_id, packets_sent); + } +} diff --git a/loginserver/server_manager.h b/loginserver/server_manager.h index 0e20996aa..7ec0e310e 100644 --- a/loginserver/server_manager.h +++ b/loginserver/server_manager.h @@ -45,7 +45,7 @@ class ServerManager /** * Sends a request to world to see if the client is banned or suspended. */ - void SendUserToWorldRequest(const char* ServerIP, unsigned int client_account_id, uint32 ip); + void SendUserToWorldRequest(const char* ServerIP, unsigned int client_account_id, uint32 ip, bool is_auto_connect = false, const std::string& client_key = ""); /** * Creates a server list packet for the older client. @@ -62,6 +62,11 @@ class ServerManager */ void DestroyServerByName(std::string l_name, std::string s_name, WorldServer *ignore = nullptr); + /** + * Removes a player from all world server queues when they disconnect + */ + void RemovePlayerFromAllQueues(uint32 ls_account_id); + private: /** * Retrieves a server(if exists) by ip address diff --git a/loginserver/world_server.cpp b/loginserver/world_server.cpp index a58bee038..a046b5b41 100644 --- a/loginserver/world_server.cpp +++ b/loginserver/world_server.cpp @@ -20,10 +20,19 @@ #include "login_types.h" #include "../common/eqemu_logsys.h" #include "../common/ip_util.h" +#include "../common/queue_packets.h" // Queue-specific opcodes and structures +#include +#include +#include +#include +#include extern EQEmuLogSys LogSys; extern LoginServer server; +// Global cache for server effective population by server ID +std::map g_server_populations; + WorldServer::WorldServer(std::shared_ptr c) { m_connection = c; @@ -41,6 +50,10 @@ WorldServer::WorldServer(std::shared_ptr c) c->OnMessage(ServerOP_LSStatus, std::bind(&WorldServer::ProcessLSStatus, this, std::placeholders::_1, std::placeholders::_2)); c->OnMessage(ServerOP_UsertoWorldResp, std::bind(&WorldServer::ProcessUsertoWorldResp, this, std::placeholders::_1, std::placeholders::_2)); c->OnMessage(ServerOP_LSAccountUpdate, std::bind(&WorldServer::ProcessLSAccountUpdate, this, std::placeholders::_1, std::placeholders::_2)); + c->OnMessage(ServerOP_QueueAutoConnect, std::bind(&WorldServer::ProcessQueueAutoConnect, this, std::placeholders::_1, std::placeholders::_2)); + c->OnMessage(ServerOP_QueueDirectUpdate, std::bind(&WorldServer::ProcessQueueDirectUpdate, this, std::placeholders::_1, std::placeholders::_2)); + c->OnMessage(ServerOP_QueueBatchUpdate, std::bind(&WorldServer::ProcessQueueBatchUpdate, this, std::placeholders::_1, std::placeholders::_2)); + c->OnMessage(ServerOP_WorldListUpdate, std::bind(&WorldServer::ProcessWorldListUpdate, this, std::placeholders::_1, std::placeholders::_2)); } WorldServer::~WorldServer() @@ -105,15 +118,20 @@ void WorldServer::ProcessUsertoWorldResp(uint16_t opcode, const EQ::Net::Packet& LogInfo("User-To-World Response received."); UsertoWorldResponse*user_to_world_response = (UsertoWorldResponse*)p.Data(); + LogInfo("DEBUG: Received response [{}] for LS account [{}]", user_to_world_response->response, user_to_world_response->lsaccountid); LogInfo("Trying to find client with user id of [{0}].", user_to_world_response->lsaccountid); Client* c = server.client_manager->GetClient(user_to_world_response->lsaccountid); if (c && (c->GetClientVersion() == cv_old)) { in_addr in{}; in.s_addr = c->GetConnection()->GetRemoteIP(); - std::string client_addr = inet_ntoa(in); + char client_addr_buf[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &in, client_addr_buf, INET_ADDRSTRLEN); + std::string client_addr = client_addr_buf; if (user_to_world_response->response > 0) { + // Send client auth - use real account name for all connections SendClientAuth(client_addr, c->GetAccountName(), c->GetKey(), c->GetAccountID(), c->GetMacClientVersion()); + LogInfo("Called SendClientAuth with account name [{}]", c->GetAccountName()); } switch (user_to_world_response->response) { @@ -144,12 +162,30 @@ void WorldServer::ProcessUsertoWorldResp(uint16_t opcode, const EQ::Net::Packet& c->FatalError("Error IP Limit Exceeded: \n\nYou have exceeded the maximum number of allowed IP addresses for this account."); break; } + case -6: { // Queue response - player should be queued + // TODO Dialog box for queue response? + LogInfo("QUEUE RESPONSE: Player [{}] should be queued by world server", user_to_world_response->lsaccountid); + // World server handles queue addition - login server just acknowledges + // Client will see updated queue position via ServerOP_QueueDirectUpdate packets + // For old clients, don't send any response - they'll stay on server select + // and see queue updates via server list refreshes + return; // Return early - no play response sent = "nothing happens" = stay on server select + } + case -7: { // Queue toggle - player removed from queue, stay on server select + LogInfo("QUEUE TOGGLE: Player [{}] removed from queue - staying on server select", user_to_world_response->lsaccountid); + // Player was removed from queue, no action needed + // Return early without sending play response - client stays on server select cleanly + return; // Return early - no play response sent = stay on server select + } } LogInfo("Found client with user id of {0} and account name of {1}.", user_to_world_response->lsaccountid, c->GetAccountName().c_str()); EQApplicationPacket* outapp = new EQApplicationPacket(OP_PlayEverquestRequest, 17); strncpy((char*)&outapp->pBuffer[1], c->GetKey().c_str(), c->GetKey().size()); - c->SendPlayResponse(outapp); + // Send PlayResponse for all successful connections (auto-connect and manual) + // Auto-connect should work exactly like manual PLAY when world server approves + LogInfo("Sending PlayResponse for approved connection (account: {})", c->GetAccountName()); + c->SendPlayResponse(outapp); delete outapp; } else if (c) { @@ -163,10 +199,15 @@ void WorldServer::ProcessUsertoWorldResp(uint16_t opcode, const EQ::Net::Packet& in_addr in{}; in.s_addr = c->GetConnection()->GetRemoteIP(); - std::string client_addr = inet_ntoa(in); + char client_addr_buf[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &in, client_addr_buf, INET_ADDRSTRLEN); + std::string client_addr = client_addr_buf; if (user_to_world_response->response > 0) { per->Allowed = 1; + + // Send client auth - use real account name for all connections SendClientAuth(client_addr, c->GetAccountName(), c->GetKey(), c->GetAccountID()); + LogInfo("Called SendClientAuth with account name [{}] (newer client)", c->GetAccountName()); } switch (user_to_world_response->response) { @@ -198,12 +239,33 @@ void WorldServer::ProcessUsertoWorldResp(uint16_t opcode, const EQ::Net::Packet& per->Message = LS::ErrStr::IP_ADDR_MAX; break; } + case -6: { // Queue response - player should be queued + // Don't set per->Allowed = 1 - player shouldn't connect yet + per->Message = 0; // No error message needed - queue handled via server list + LogInfo("QUEUE RESPONSE: Player [{}] should be queued by world server", user_to_world_response->lsaccountid); + // World server handles queue addition - login server just acknowledges + // Client will see updated queue position via ServerOP_QueueDirectUpdate packets + // Send response with Allowed = 0 to keep client on server select + // break; + return; + } + case -7: { // Queue toggle - player removed from queue, stay on server select + // Don't set per->Allowed = 1 - player shouldn't connect yet + per->Message = 0; // No error message needed - same as queue response + LogInfo("QUEUE TOGGLE: Player [{}] removed from queue - staying on server select", user_to_world_response->lsaccountid); + // Player was removed from queue, no action needed + // Return early without sending play response - client stays on server select cleanly + return; // Return early - no play response sent = stay on server select + } } LogInfo("Sending play response with following data, allowed {} , sequence {} , server number {} , message {} ", per->Allowed, per->Sequence, per->ServerNumber, per->Message); - c->SendPlayResponse(outapp); + // Send PlayResponse for all successful connections (auto-connect and manual) + // Auto-connect should work exactly like manual PLAY when world server approves + LogInfo("Sending PlayResponse for approved connection (newer client, account: {})", c->GetAccountName()); + c->SendPlayResponse(outapp); delete outapp; } else { @@ -466,6 +528,9 @@ void WorldServer::Handle_LSStatus(ServerLSStatus_Struct *s) m_players_online = s->num_players; m_zones_booted = s->num_zones; m_server_status = s->status; + + // Update the global cache with the current population + g_server_populations[GetServerId()] = s->num_players; } void WorldServer::SendClientAuth(std::string ip, std::string account, std::string key, unsigned int account_id, uint8 version) @@ -504,4 +569,230 @@ void WorldServer::SendClientAuth(std::string ip, std::string account, std::strin ); safe_delete(outapp); +} +void WorldServer::ProcessQueueAutoConnect(uint16_t opcode, const EQ::Net::Packet& p) +{ + if (p.Length() < sizeof(ServerQueueAutoConnect_Struct)) { + LogError("Received ServerOP_QueueAutoConnect packet that was too small"); + return; + } + + ServerQueueAutoConnect_Struct* sqac = (ServerQueueAutoConnect_Struct*)p.Data(); + + QueueDebugLog(1, "Processing auto-connect for LS account [{}] from world server [{}]", + sqac->loginserver_account_id, GetServerLongName()); + + // Find the specific client connection that was authorized using the client key to avoid connecting the wrong client in the event that the client has multiple connections to the login server. + Client* target_client = nullptr; + if (server.client_manager) { + if (strlen(sqac->client_key) > 0) { + // Use the specific client key to find the exact authorized connection + target_client = server.client_manager->GetClientByKey(sqac->client_key); + QueueDebugLog(1, "AUTO-CONNECT: Using client key [{}] to find authorized connection", sqac->client_key); + } + else { + // No client key provided - this should not happen with proper queue system + LogError("AUTO-CONNECT: No client key provided for LS account [{}] - cannot auto-connect safely", sqac->loginserver_account_id); + return; + } + } + + if (!target_client) { + LogInfo("Auto-connect failed: Client with key [{}] for LS account [{}] no longer connected to login server", + sqac->client_key, sqac->loginserver_account_id); + return; + } + + // Check if client is still connected to login server + if (target_client->GetConnection()->CheckState(CLOSED)) { + LogInfo("Auto-connect failed: Client connection is closed for LS account [{}]", sqac->loginserver_account_id); + return; + } + + // Verify this is the correct account (safety check) + if (target_client->GetAccountID() != sqac->loginserver_account_id) { + LogError("AUTO-CONNECT: Client key mismatch! Found client has account [{}] but expected [{}]", + target_client->GetAccountID(), sqac->loginserver_account_id); + return; + } + + // Log which specific client connection we're targeting + in_addr in{}; + in.s_addr = target_client->GetConnection()->GetRemoteIP(); + uint16_t port = target_client->GetConnection()->GetRemotePort(); + char client_ip_buf[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &in, client_ip_buf, INET_ADDRSTRLEN); + QueueDebugLog(1, "AUTO-CONNECT: Successfully targeting specific authorized client connection {}:{} for account [{}]", + client_ip_buf, ntohs(port), sqac->loginserver_account_id); + + // Use ServerManager to automatically send player to world server + if (server.server_manager) { + QueueDebugLog(1, "AUTO-CONNECT: Sending player [{}] (Client IP: {}) to world server [{}] automatically", + sqac->loginserver_account_id, sqac->ip_addr_str, GetServerLongName()); + + std::string world_server_ip = GetRemoteIP(); + server.server_manager->SendUserToWorldRequest( + world_server_ip.c_str(), + sqac->loginserver_account_id, + sqac->ip_address, + true, // is_auto_connect + sqac->client_key // client_key + ); + + QueueDebugLog(1, "Auto-connect request sent successfully for account [{}] to world server [{}]", + sqac->loginserver_account_id, world_server_ip); + } else { + LogError("Auto-connect failed: ServerManager not available"); + } +} + +void WorldServer::ProcessQueueDirectUpdate(uint16_t opcode, const EQ::Net::Packet& p) +{ + QueueDebugLog(1, "ProcessQueueDirectUpdate called with opcode 0x{:X}, packet size {}", opcode, p.Length()); + + if (p.Length() < sizeof(ServerQueueDirectUpdate_Struct)) { + LogError("Received ServerOP_QueueDirectUpdate packet that was too small"); + return; + } + if (server.client_manager) { + server.client_manager->UpdateServerList(); + QueueDebugLog(1, "Sent server list updates to all connected clients"); + } + ServerQueueDirectUpdate_Struct* direct_update = (ServerQueueDirectUpdate_Struct*)p.Data(); + + QueueDebugLog(1, "Received queue direct update for LS account [{}] position [{}] wait [{}]s", + direct_update->ls_account_id, direct_update->queue_position, direct_update->estimated_wait); + + // Find target client by account ID + Client* target_client = nullptr; + + if (server.client_manager && direct_update->ls_account_id != 0) { + target_client = server.client_manager->GetClient(direct_update->ls_account_id); + } + + if (target_client) { + // Check if player is no longer queued (position 0 = removed from queue) + if (direct_update->queue_position == 0) { + // Clear queue position from client + target_client->ClearQueuePosition(); + LogInfo("Player [{}] removed from queue", direct_update->ls_account_id); + } else { + // Store queue position in client object + target_client->SetQueuePosition(GetServerId(), direct_update->queue_position); + LogInfo("Player [{}] queue position updated to [{}]", + direct_update->ls_account_id, direct_update->queue_position); + } + + QueueDebugLog(1, "Updated client account [{}] with queue position [{}]", + direct_update->ls_account_id, direct_update->queue_position); + + // EFFICIENT: Send targeted update only to this specific client + target_client->SendServerListPacket(); + QueueDebugLog(1, "Sent targeted server list update to client [{}]", direct_update->ls_account_id); + } else { + QueueDebugLog(1, "Client account [{}] not found - likely disconnected", + direct_update->ls_account_id); + } + + +} + +void WorldServer::ProcessQueueBatchUpdate(uint16_t opcode, const EQ::Net::Packet& p) +{ + QueueDebugLog(1, "ProcessQueueBatchUpdate called with opcode 0x{:X}, packet size {}", opcode, p.Length()); + + if (p.Length() < sizeof(ServerQueueBatchUpdate_Struct)) { + LogError("Received ServerOP_QueueBatchUpdate packet that was too small"); + return; + } + + ServerQueueBatchUpdate_Struct* batch_header = (ServerQueueBatchUpdate_Struct*)p.Data(); + uint32 update_count = batch_header->update_count; + + // Validate packet size + size_t expected_size = sizeof(ServerQueueBatchUpdate_Struct) + (update_count * sizeof(ServerQueueDirectUpdate_Struct)); + if (p.Length() < expected_size) { + LogError("Received ServerOP_QueueBatchUpdate packet with invalid size. Expected: {}, Got: {}", + expected_size, p.Length()); + return; + } + + if (update_count == 0) { + QueueDebugLog(1, "Received empty batch update"); + return; + } + + QueueDebugLog(1, "Processing batch queue update with [{}] player updates", update_count); + + // Get update array after header + ServerQueueDirectUpdate_Struct* updates = (ServerQueueDirectUpdate_Struct*)((char*)p.Data() + sizeof(ServerQueueBatchUpdate_Struct)); + + uint32 processed_updates = 0; + uint32 failed_updates = 0; + + // Process each update in the batch + for (uint32 i = 0; i < update_count; ++i) { + ServerQueueDirectUpdate_Struct* update = &updates[i]; + + QueueDebugLog(1, "Processing batch update [{}]: LS account [{}] position [{}] wait [{}]s", + i + 1, update->ls_account_id, update->queue_position, update->estimated_wait); + + // Find target client + Client* target_client = nullptr; + if (server.client_manager && update->ls_account_id != 0) { + target_client = server.client_manager->GetClient(update->ls_account_id); + } + + if (target_client) { + // Process the update (same logic as individual updates) + if (update->queue_position == 0) { + // Clear queue position from client + target_client->ClearQueuePosition(); + QueueDebugLog(1, "Batch update [{}]: Player [{}] removed from queue", i + 1, update->ls_account_id); + } else { + // Store queue position in client object + target_client->SetQueuePosition(GetServerId(), update->queue_position); + QueueDebugLog(1, "Batch update [{}]: Player [{}] queue position updated to [{}]", + i + 1, update->ls_account_id, update->queue_position); + } + + // Send targeted update to this specific client + target_client->SendServerListPacket(); + processed_updates++; + } else { + QueueDebugLog(1, "Batch update [{}]: Client account [{}] not found - likely disconnected", + i + 1, update->ls_account_id); + failed_updates++; + } + } + + LogInfo("Processed batch queue update: [{}] successful, [{}] failed, [{}] total", + processed_updates, failed_updates, update_count); +} + + +void WorldServer::ProcessWorldListUpdate(uint16_t opcode, const EQ::Net::Packet& p) +{ + QueueDebugLog(1, "ProcessWorldListUpdate called - opcode: 0x{:X}, packet size: {}", opcode, p.Length()); + + // Check if packet contains population data + if (p.Length() >= sizeof(uint32)) { + uint32 new_population = *((uint32*)p.Data()); + LogInfo("Received ServerOP_WorldListUpdate from world server [{}] with population: {} - updating cache and pushing to clients", + GetServerLongName(), new_population); + + // Update the global cache immediately with the new population + g_server_populations[GetServerId()] = new_population; + } else { + QueueDebugLog(1, "Received ServerOP_WorldListUpdate from world server [{}] (legacy format) - pushing server list updates to all clients", + GetServerLongName()); + } + + // Push server list updates to all connected clients immediately + if (server.client_manager) { + server.client_manager->UpdateServerList(); + LogInfo("Pushed server list updates to all connected login clients"); + } else { + QueueDebugLog(1, "server.client_manager is null - cannot push updates"); + } } \ No newline at end of file diff --git a/loginserver/world_server.h b/loginserver/world_server.h index a02b37f41..c0cee6af8 100644 --- a/loginserver/world_server.h +++ b/loginserver/world_server.h @@ -22,8 +22,15 @@ #include "../common/net/servertalk_server_connection.h" #include "../common/servertalk.h" #include "../common/packet_dump.h" +#include "../../world/world_queue.h" // For shared QUEUE_DEBUG_LEVEL #include #include +#include "../common/emu_opcodes.h" + +// Forward declarations +class EQApplicationPacket; +class Client; +struct UsertoWorldResponse; /** * World server class, controls the connected server processing. @@ -114,14 +121,18 @@ class WorldServer */ void Handle_LSStatus(ServerLSStatus_Struct *server_login_status); -\ /** * Informs world that there is a client incoming with the following data. */ void SendClientAuth(std::string ip, std::string account, std::string key, unsigned int account_id, uint8 version = 0); - + void ProcessQueueAutoConnect(uint16_t opcode, const EQ::Net::Packet& p); + void ProcessQueueDirectUpdate(uint16_t opcode, const EQ::Net::Packet& p); + void ProcessQueueBatchUpdate(uint16_t opcode, const EQ::Net::Packet& p); + void ProcessWorldListUpdate(uint16_t opcode, const EQ::Net::Packet& p); private: + bool RuleB_Get(const std::string& rule_name, bool default_value); + /** * Packet processing functions: */ @@ -153,4 +164,3 @@ class WorldServer }; #endif - diff --git a/utils/scripts/queue_system/README.md b/utils/scripts/queue_system/README.md new file mode 100644 index 000000000..c4475eca2 --- /dev/null +++ b/utils/scripts/queue_system/README.md @@ -0,0 +1,728 @@ +# ๐Ÿ”„ Queue System + +## ๐ŸŽฏ **Overview** + +The queue system provides server capacity management through a **World Server-centric architecture** where all queue decisions are made centrally by `QueueManager` on the world server. The system includes automatic queue advancement, grace period management for disconnections, and real-time population tracking. + +### **๐Ÿ—๏ธ Key Architectural Components** +- **๐ŸŒ QueueManager** (`world/world_queue.cpp`) - Central decision authority for all queue operations +- **๐Ÿ  AccountRezMgr** (`world/account_reservation_manager.cpp`) - Grace period & population tracking + + +### **๐Ÿ”„ Core Flow** +1. **Client** clicks PLAY โ†’ **Login Server** receives request +2. **Login Server** queries **World Server** via `ServerOP_UsertoWorldReq` +3. **QueueManager** makes capacity decision (admit/queue/bypass) +4. **World Server** responds with decision via `ServerOP_UsertoWorldResp` +5. **Auto-Connect System** advances queue and triggers connections automatically + +### **โšก Key Features** +- **Account ID-Based** - No IP tracking, pure account reservation system +- **Self-Repairing** - Automatically recovers from crashes and validates state +- **Real-Time Updates** - Queue positions update live via batch packets +- **Grace Periods** - Recently disconnected players bypass queue temporarily +- **GM Bypass** - Configurable admin level queue exemptions + +### **๐Ÿ”„ Backward Compatibility** +- **Unknown Opcodes Ignored** - Old servers silently drop `ServerOP_Queue*` packets +- **Standard Login Flow** - Non-queue servers process `ServerOP_UsertoWorldReq/Resp` normally +- **No Breaking Changes** - All existing login functionality preserved +- **Graceful Degradation** - Queue-enabled login servers work with old world servers + + +--- + +## ๐Ÿ—๏ธ **System Architecture & Flow Diagrams** + +### **World Server-Centric Architecture** + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ CLIENTS โ”‚ +โ”‚ โ”‚ +โ”‚ โ€ข PLAY btn โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ OP_PlayEverquestRequest + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ LOGIN SERVER โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Client Requests: โ”‚ โ”‚ +โ”‚ โ”‚ โ€ข Handle_Play โ”‚ โ”‚ +โ”‚ โ”‚ โ€ข SendUserToWorldRequest โ”‚ โ”‚ +โ”‚ โ”‚ โ€ข ProcessUsertoWorldResp โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ–ผ ServerOP_UsertoWorldReq โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ WORLD SERVER โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ QueueManager โ”‚โ—„โ”€โ”€โ–บโ”‚ AccountRezMgr โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ (Self-Repair) โ”‚ โ”‚ +โ”‚ โ”‚ โ€ข Queue Logic โ”‚ โ”‚ โ€ข Population โ”‚ โ”‚ +โ”‚ โ”‚ โ€ข Bypass Rules โ”‚ โ”‚ โ€ข Grace Periods โ”‚ โ”‚ +โ”‚ โ”‚ โ€ข Auto-Connect โ”‚ โ”‚ โ€ข Reservations โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ–ผ Decision Authority โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”‚ +โ”‚ โ”‚ EvaluateConnectionRequest: โ”‚โ”‚ +โ”‚ โ”‚ โ€ข Capacity check โ”‚โ”‚ +โ”‚ โ”‚ โ€ข GM/Grace bypasses โ”‚โ”‚ +โ”‚ โ”‚ โ€ข Queue addition โ”‚โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ–ผ Response Packets โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ€ข ServerOP_UsertoWorldResp + โ”‚ โ€ข ServerOP_QueueAutoConnect + โ”‚ โ€ข ServerOP_QueueBatchUpdate + โ”‚ โ€ข ServerOP_WorldListUpdate + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ LOGIN SERVER โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Response Handling: โ”‚ โ”‚ +โ”‚ โ”‚ โ€ข Success (1) โ†’ SendClientAuth โ”‚ โ”‚ +โ”‚ โ”‚ โ€ข Queue (-6) โ†’ Stay on server select โ”‚ โ”‚ +โ”‚ โ”‚ โ€ข Auto-connect โ†’ Trigger PLAY โ”‚ โ”‚ +โ”‚ โ”‚ โ€ข Batch updates โ†’ Update queue pos โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ–ผ Client Updates โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ CLIENTS โ”‚ +โ”‚ โ”‚ +โ”‚ Server List โ”‚ +โ”‚ Queue Pos โ”‚ +โ”‚ Updates โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### **Connection Flow Integration** + +``` +โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +๐ŸŽฎ **COMPLETE LOGIN FLOW** (Client PLAY button to world entry) +โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +๐ŸŽฎ *Client clicks PLAY on server* +โ†“ +โ””โ”€๐Ÿ“๐Ÿ“จ `loginserver/client.cpp` + โ””โ”€ OP_PlayEverquestRequest received + โ””โ”€ [Client::Handle_Play] + โ””โ”€ โ” m_client_status == cs_logged_in? + โ””โ”€ โœ… Continue + โ””โ”€ โŒ LogError + return +โ†“ +๐Ÿ“๐Ÿ“จ `loginserver/server_manager.cpp` +โ””โ”€ [SendUserToWorldRequest] + โ”œโ”€ Find target world server by IP + โ”œโ”€ Create UsertoWorldRequest packet + โ”œโ”€ utwr->lsaccountid = client_account_id + โ”œโ”€ utwr->ip = client_ip + โ”œโ”€ utwr->FromID = is_auto_connect ? 1 : 0 // Manual PLAY = 0 + โ”œโ”€ utwr->forum_name = client_key (for queue targeting) + โ””โ”€ Send: ServerOP_UsertoWorldReq โ†’ World server + โ†“ + ๐Ÿ“๐ŸŒ `world/login_server.cpp` + โ””โ”€ *OnMessage(ServerOP_UsertoWorldReq)* + โ””โ”€ [ProcessUsertoWorldReq] + โ”œโ”€ ๐Ÿ” **Standard validation checks** (bans, suspensions, IP limits, etc.) + โ”‚ โ””โ”€ โŒ If failed โ†’ Various error responses (-1 to -5) + โ”‚ โ””โ”€ โœ… If passed โ†’ Continue to capacity check + โ”œโ”€ ๐Ÿ“Š **CAPACITY CHECK:** + โ”‚ โ”œโ”€ effective_population = GetWorldPop() // queue_manager.EffectivePopulation() + โ”‚ โ”œโ”€ queue_cap = RuleI(Quarm, PlayerPopulationCap) + โ”‚ โ””โ”€ โ” effective_population >= queue_cap? + โ”‚ โ”œโ”€ โŒ Under capacity โ†’ response = 1 โœ… (SUCCESS) + โ”‚ โ””โ”€ โœ… At capacity โ†’ **QUEUE MANAGER EVALUATION:** + โ”‚ โ””โ”€ ๐ŸŽฏ [queue_manager.EvaluateConnectionRequest] + โ”‚ โ”œโ”€ โ” Auto-connect (FromID == 1)? + โ”‚ โ”‚ โ””โ”€ โœ… AutoConnect โ†’ Override capacity (return true) + โ”‚ โ”œโ”€ โ” Already queued? + โ”‚ โ”‚ โ””โ”€ โœ… QueueToggle โ†’ Remove from queue, response = -7 + โ”‚ โ”œโ”€ โ” GM bypass (status >= 80)? + โ”‚ โ”‚ โ””โ”€ โœ… GMBypass โ†’ Override capacity (return true) + โ”‚ โ”œโ”€ โ” Grace period whitelist? + โ”‚ โ”‚ โ””โ”€ โœ… GraceBypass โ†’ Extend grace, override capacity + โ”‚ โ””โ”€ โŒ No bypass โ†’ QueuePlayer: + โ”‚ โ”œโ”€ [AddToQueue] โ†’ Add to queue + โ”‚ โ”œโ”€ response = -6 (QUEUE) + โ”‚ โ””โ”€ return false (respect capacity) + โ””โ”€ ๐Ÿ“ก Send: ServerOP_UsertoWorldResp โ†’ Login server + โ†“ + ๐Ÿ“๐Ÿ“จ `loginserver/world_server.cpp` + โ””โ”€ *OnMessage(ServerOP_UsertoWorldResp)* + โ””โ”€ [ProcessUsertoWorldResp] + โ”œโ”€ Find client by lsaccountid + โ””โ”€ โ” response code? + โ”œโ”€ โœ… **1 (SUCCESS):** + โ”‚ โ”œโ”€ [SendClientAuth] โ†’ ServerOP_LSClientAuth โ†’ World server + โ”‚ โ”‚ โ†“ + โ”‚ โ”‚ ๐Ÿ“๐ŸŒ `world/login_server.cpp` + โ”‚ โ”‚ โ””โ”€ *OnMessage(ServerOP_LSClientAuth)* + โ”‚ โ”‚ โ””โ”€ [ProcessLSClientAuth] + โ”‚ โ”‚ โ”œโ”€ [client_list.CLEAdd] โ†’ Player enters world + โ”‚ โ”‚ โ””โ”€ [m_account_rez_mgr.AddReservation] โ†’ Track connection + โ”‚ โ””โ”€ ๐ŸŽฎ **CLIENT ENTERS WORLD** + โ”œโ”€ โŒ **-6 (QUEUE):** + โ”‚ โ”œโ”€ LogInfo("Player should be queued") + โ”‚ โ”œโ”€ return early (no play response) + โ”‚ โ””โ”€ ๐ŸŽฎ **CLIENT STAYS ON SERVER SELECT** + โ”‚ โ””โ”€ Sees queue position via server list updates + โ”œโ”€ โŒ **-7 (QUEUE TOGGLE):** + โ”‚ โ”œโ”€ LogInfo("Player removed from queue") + โ”‚ โ”œโ”€ return early (no play response) + โ”‚ โ””โ”€ ๐ŸŽฎ **CLIENT STAYS ON SERVER SELECT** + โ””โ”€ โŒ **Other errors:** + โ”œโ”€ Standard validation failures (-1 to -5) + โ””โ”€ ๐ŸŽฎ **CLIENT GETS ERROR MESSAGE** +``` + +### **QueueAdvancementTimer (3000ms) - Main Queue Processing** + +``` +โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +๐Ÿ”„ **QUEUE ADVANCEMENT FLOW** (Every 3 seconds in world server) +โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +๐Ÿ“๐ŸŒ `world/main.cpp` +โ”œโ”€ โฐ Timer QueueManagerTimer(3000) // 3 second interval +โ”œโ”€ QueueManagerTimer.Start() in main() +โ””โ”€ โฐ [QueueManagerTimer.Check] (3s interval) + โ””โ”€ ๐ŸŽฏ [queue_manager.ProcessAdvancementTimer] + โ”œโ”€ ๐Ÿ“Š Update server_population table (effective_population) + โ”œโ”€ ๐Ÿ“ก SendWorldListUpdate() โ†’ Login server (if population changed) + โ”‚ โ””โ”€ Updates g_server_populations[server_id] cache + โ”‚ โ””โ”€ Pushes to ALL connected login clients immediately + โ”œโ”€ ๐ŸŽฏ [UpdateQueuePositions] โ†’ MAIN QUEUE LOGIC + โ”‚ โ”œโ”€ ๐Ÿงน Check m_queue_paused and RuleB(Quarm, FreezeQueue) + โ”‚ โ”œโ”€ ๐Ÿ“Š current_population = EffectivePopulation() + โ”‚ โ”œโ”€ ๐ŸŽฏ available_slots = max_capacity - current_population + โ”‚ โ”œโ”€ ๐Ÿ” Queued player at position 1: + โ”‚ โ”‚ โ””โ”€ โ” available_slots > 0? + โ”‚ โ”‚ โ””โ”€ โœ… [AutoConnectQueuedPlayer] + โ”‚ โ”‚ โ”œโ”€ ๐Ÿ  [m_account_rez_mgr.AddRez] โ†’ Create reservation FIRST + โ”‚ โ”‚ โ”‚ โ””โ”€ 30-second grace period for capacity bypass + โ”‚ โ”‚ โ””โ”€ ๐Ÿ“ก [SendQueueAutoConnect] โ†’ ServerOP_QueueAutoConnect โ†’ Login server + โ”‚ โ”‚ โ†“ + โ”‚ โ”‚ ๐Ÿ“๐Ÿ“จ `loginserver/world_server.cpp` + โ”‚ โ”‚ โ””โ”€ [ProcessQueueAutoConnect] + โ”‚ โ”‚ โ””โ”€ Contains: ls_account_id, ip, client_key + โ”‚ โ”‚ โ””โ”€ ๐Ÿ—‘๏ธ Remove from queue after auto-connect + โ”‚ โ””โ”€ ๐Ÿ“ข [SendQueuedClientsUpdate] โ†’ ServerOP_QueueBatchUpdate โ†’ Login server + โ”‚ โ”œโ”€ Recalculates positions: position = index + 1 + โ”‚ โ”œโ”€ Recalculates wait times: wait = position * 60 + โ”‚ โ””โ”€ Single packet with all queue updates + โ”‚ โ†“ + โ”‚ ๐Ÿ“๐Ÿ“จ `loginserver/world_server.cpp` + โ”‚ โ””โ”€ *OnMessage(ServerOP_QueueBatchUpdate)* + โ”‚ โ””โ”€ [ProcessQueueBatchUpdate] + โ”‚ โ”œโ”€ Parse batch header + update array + โ”‚ โ”œโ”€ For each update in batch: + โ”‚ โ”‚ โ”œโ”€ ๐Ÿ” Find client by ls_account_id + โ”‚ โ”‚ โ”œโ”€ Update client queue position + โ”‚ โ”‚ โ””โ”€ ๐Ÿ“ฑ [target_client->SendServerListPacket] + โ”‚ โ”‚ โ””โ”€ ๐Ÿ”„ Rebuilds server list with NEW position + โ”‚ โ”‚ โ””โ”€ ๐Ÿ“ค **๐ŸŽฏ USER SEES UPDATE INSTANTLY** + โ”‚ โ””โ”€ Log: "Processed [X] successful, [Y] failed" + โ”œโ”€ ๐Ÿงน [m_account_rez_mgr.PeriodicMaintenance] + โ”‚ โ”œโ”€ Scan all account reservations for expiry + โ”‚ โ”œโ”€ Remove expired grace periods (30s default) + โ”‚ โ”œโ”€ Sync to database every 5 minutes + โ”‚ โ””โ”€ ๐Ÿ”ง **SELF-REPAIR:** Auto-correct inconsistent states + โ””โ”€ ๐Ÿ”„ [CheckForExternalChanges] + โ”œโ”€ Check for RuleI(Quarm, TestPopulationOffset) changes + โ”œโ”€ Check RefreshQueue flag in tblloginserversettings + โ”œโ”€ If test offset changed โ†’ SendWorldListUpdate() + โ””โ”€ If RefreshQueue=1 โ†’ RestoreQueueFromDatabase() +``` +### **Real-time Population Synchronization** +``` +โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +๐Ÿ“Š **POPULATION TRACKING & SYNC** (Every 3 seconds + event-driven) +โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +๐Ÿ“๐ŸŒ world_queue.cpp [QueueManager::EffectivePopulation] +โ”œโ”€ ๐Ÿ  Account Reservations: m_account_rez_mgr.Total() +โ”‚ โ”œโ”€ ๐Ÿ‘ฅ Active connections (in-world players) +โ”‚ โ””โ”€ โฐ Grace period connections (recently disconnected, 30s) +โ”œโ”€ ๐Ÿงช Test Offset: m_cached_test_offset +โ”‚ โ”œโ”€ ๐Ÿ”ฌ RuleI(Quarm, TestPopulationOffset) for simulation +โ”‚ โ””โ”€ ๐Ÿ“Š Cached value updated via CheckForExternalChanges() +โ””โ”€ ๐Ÿ“Š effective_population = reservations + test_offset + +๐Ÿ“๐ŸŒ ProcessAdvancementTimer [Every 3 seconds] +โ”œโ”€ ๐Ÿ’พ DB Update: server_population table +โ”‚ โ””โ”€ INSERT/UPDATE effective_population, last_updated = NOW() +โ”œโ”€ ๐Ÿ“ก Real-time Login Server Sync: +โ”‚ โ”œโ”€ โ” effective_population != last_sent_population? +โ”‚ โ”‚ โ””โ”€ โœ… [SendWorldListUpdate] โ†’ ServerOP_WorldListUpdate โ†’ Login server +โ”‚ โ”‚ โ†“ +โ”‚ โ”‚ ๐Ÿ“๐Ÿ“จ `loginserver/world_server.cpp` +โ”‚ โ”‚ โ””โ”€ [ProcessWorldListUpdate] +โ”‚ โ”‚ โ”œโ”€ ๐ŸŽฏ g_server_populations[server_id] = new_population +โ”‚ โ”‚ โ””โ”€ ๐Ÿ“ก server.client_manager->UpdateServerList() +โ”‚ โ”‚ โ””โ”€ Pushes to ALL connected login clients +โ”‚ โ”‚ โ””โ”€ Users see updated server population instantly +โ”‚ โ””โ”€ ๐Ÿ“Š last_sent_population = effective_population (cache) +โ””โ”€ ๐Ÿ”„ Backup: active_ip_connections table (5min snapshots) + โ””โ”€ ๐Ÿ’พ AccountRezMgr crash recovery data +``` + + + + +### **AccountRezMgr Grace Period Management (Self-Repairing)** +``` +โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +๐Ÿ”ง **ACCOUNT RESERVATION SELF-REPAIR** (Every 3 seconds via PeriodicMaintenance) +โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +๐Ÿ“๐ŸŒ account_reservation_manager.cpp +โ””โ”€ ๐ŸŽฏ [m_account_rez_mgr.PeriodicMaintenance] (called by ProcessAdvancementTimer) + โ”œโ”€ ๐Ÿ” Scan all account reservations in m_reservations map + โ”‚ โ””โ”€ For each reservation: + โ”‚ โ”œโ”€ โ” Connection still active? + โ”‚ โ”‚ โ””โ”€ โœ… Refresh last_seen timestamp โ†’ Keep alive + โ”‚ โ”œโ”€ โ” Grace period (30s default) expired? + โ”‚ โ”‚ โ””โ”€ ๐Ÿ—‘๏ธ Remove reservation โ†’ Update population count + โ”‚ โ”œโ”€ โ” Raid detected (extended protection)? + โ”‚ โ”‚ โ””โ”€ โฐ Extend grace period (longer timeout) + โ”‚ โ””โ”€ ๐Ÿ”ง **SELF-REPAIR CHECKS:** + โ”‚ โ”œโ”€ Validate reservation timestamps + โ”‚ โ”œโ”€ Check for orphaned reservations + โ”‚ โ”œโ”€ Correct inconsistent IP mappings + โ”‚ โ””โ”€ Verify account ID validity + โ”œโ”€ ๐Ÿ’พ Database Sync (every 5 minutes): + โ”‚ โ”œโ”€ Batch write to active_ip_connections table + โ”‚ โ”œโ”€ Remove expired entries from database + โ”‚ โ””โ”€ ๐Ÿ”ง Cross-validate memory vs database state + โ”œโ”€ ๐Ÿ“Š Population Impact: + โ”‚ โ”œโ”€ Immediate EffectivePopulation() updates + โ”‚ โ”œโ”€ Real-time capacity calculations + โ”‚ โ””โ”€ Auto-trigger SendWorldListUpdate() if changed +``` + +## ๐Ÿ’พ **Event-Driven Database Mirroring** + +``` +๐Ÿ”„ **On Queue Add:** +โ””โ”€ ๐Ÿ“ Memory: m_queued_players[account_id] = entry +โ””โ”€ ๐Ÿ’พ DB: Update server_population SET effective_population = X (immediate) +โ””โ”€ ๐Ÿ“‹ Log: "ADD_TO_QUEUE pos=1 wait=60s account=[12345] (memory + DB)" + +๐Ÿ”„ **On Queue Remove:** +โ””โ”€ ๐Ÿ—‘๏ธ Memory: m_queued_players.erase(account_id) +โ””โ”€ ๐Ÿ’พ DB: Update server_population SET effective_population = X (immediate) +โ””โ”€ ๐Ÿ“‹ Log: "REMOVE_FROM_QUEUE account=[12345] (memory + DB)" + +๐Ÿ”„ **On Auto-Connect Success:** +โ””โ”€ โณ Memory: Move to m_pending_connections +โ””โ”€ ๐Ÿ’พ DB: Update server_population (pending now counts as population) +โ””โ”€ ๐Ÿ“ก ServerOP_QueueAutoConnect โ†’ Login server +โ””โ”€ ๐Ÿ“‹ Log: "AUTO_CONNECT account=[12345] client_key=[abc123]" + +โšก **Result:** Database always synchronized, zero queue data loss! ๐ŸŽฏ +``` + +## ๐Ÿ“‹ **Implementation Details & Code Examples** + + +### **Startup Lifecycle & Global Object Safety** +```cpp +// world/main.cpp +#include "world_queue.h" + +// world/world_queue.h +extern QueueManager queue_manager; +class QueueManager { + mutable AccountRezMgr m_account_rez_mgr; + // Global object - always valid, no null checks needed +}; + +// world/main.cpp - Timer declaration +Timer QueueManagerTimer(3000); + +// world/world_queue.cpp - Direct usage anywhere +queue_manager.ProcessAdvancementTimer(); // Always safe + +// โœ… Global Safety Guarantees: +// - extern declaration makes queue_manager available everywhere +// - Global object lifetime = entire program execution +// - No initialization dependencies or startup order issues +// - Mutable AccountRezMgr handles internal state management +``` +## ๐Ÿ”ง **Core Components** +### **class QueueManager** (`world/world_queue.cpp/.h`) +**Primary queue coordinator - World server queue management and population control** +```cpp +class QueueManager { +public: + // Primary queue operations + void AddToQueue(uint32 ls_account_id, const std::string& account_name, + uint32 server_id, const std::string& client_key); + void RemoveFromQueue(uint32 ls_account_id, uint32 server_id = 0); + uint32 GetQueuePosition(uint32 ls_account_id, uint32 server_id) const; + + // Population decision making + uint32 GetEffectivePopulation(); + uint32 GetWorldPop() const; + bool ShouldQueuePlayer() const; + + // Queue advancement & maintenance + void UpdateQueuePositions(); + void PeriodicMaintenance(); + void CheckForExternalChanges(); + +private: + std::queue m_queue; + std::map m_pending_connections; // account_id -> timestamp + Timer m_advancement_timer; + Timer m_maintenance_timer; +}; +``` + +### **class AccountRezMgr** (`world/account_reservation_manager.cpp/.h`) +**Supporting component - Account connection tracking for QueueManager (Self-Repairing)** + +```cpp +class AccountRezMgr { +public: + // Population data for QueueManager + uint32 Total() const; // All reservations (active + grace period) + uint32 ActiveCount() const; // Active connections only + + // Reservation management (supports queue bypass) + void AddReservation(uint32 account_id, uint32 ip_address); + void RemoveReservation(uint32 account_id); + bool HasReservation(uint32 account_id) const; + + // Grace period support + void StartGracePeriod(uint32 account_id); + void RefreshReservation(uint32 account_id); + + // Maintenance (Self-Repairing) + void CleanupStaleConnections(); + void SyncAllConnectionsToDatabase(); + +private: + std::unordered_map m_reservations; + Timer m_cleanup_timer; + Timer m_sync_timer; +}; +``` + +**Supporting Role for QueueManager:** +- **Population Reporting** - Provides base player count for queue decisions +- **Reservation Tracking** - Maintains connection state that QueueManager uses +- **Queue Bypass Implementation** - Handles grace periods for recently disconnected players +- **Database Persistence** - Ensures QueueManager has reliable population data +- **Connection Lifecycle** - Manages the full player connection experience +- **Self-Repairing** - Auto-corrects inconsistent states, recovers from crashes, validates DB integrity + +--- + +### **Data Flow Code Examples:** + +**1. Receives Capacity Check Request** +```cpp +// world/login_server.cpp - ProcessUsertoWorldReq +if (effective_population >= queue_cap) { + ConnectionRequest request = {}; + request.account_id = id; + request.is_auto_connect = (utwr->FromID == 1); + + bool should_override = queue_manager.EvaluateConnectionRequest(request, queue_cap, utwrs, nullptr); +} +``` + +**2. Queries AccountRezMgr for Population** +```cpp +// world/world_queue.cpp - EffectivePopulation +uint32 QueueManager::EffectivePopulation() { + uint32 base_population = m_account_rez_mgr.Total(); + uint32 test_offset = m_cached_test_offset; + uint32 effective_population = base_population + test_offset; + return effective_population; +} +``` + +**3. Makes Queue/Admit Decision** +```cpp +// world/world_queue.cpp - EvaluateConnectionRequest +if (request.is_auto_connect) { + decision = QueueDecisionOutcome::AutoConnect; +} else if (IsAccountQueued(request.account_id)) { + decision = QueueDecisionOutcome::QueueToggle; +} else if (RuleB(Quarm, QueueBypassGMLevel) && request.status >= 80) { + decision = QueueDecisionOutcome::GMBypass; +} else if (m_account_rez_mgr.IsAccountInGraceWhitelist(request.account_id)) { + decision = QueueDecisionOutcome::GraceBypass; +} else { + decision = QueueDecisionOutcome::QueuePlayer; +} +``` + +**4. Sends Response to Login Server** +```cpp +// world/world_queue.cpp - EvaluateConnectionRequest +switch (decision) { + case QueueDecisionOutcome::AutoConnect: + case QueueDecisionOutcome::GMBypass: + return true; // Override capacity - allow connection + + case QueueDecisionOutcome::GraceBypass: + m_account_rez_mgr.IncreaseGraceDuration(request.account_id, 30); + return true; // Allow connection with extended grace + + case QueueDecisionOutcome::QueuePlayer: + response->response = -6; // Queue response + return false; + + case QueueDecisionOutcome::QueueToggle: + response->response = -7; // Toggle response + return false; +} +``` + +**5. Processes Queue Advancement via Timer** +```cpp +// world/main.cpp + world/world_queue.cpp +if (QueueAdvancementTimer.Check()) { + queue_manager.UpdateQueuePositions(); +} + +void QueueManager::UpdateQueuePositions() { + for (auto& client : m_queued_clients) { + if (current_position == 1 && available_slots > 0) { + // Create reservation FIRST, then auto-connect + AutoConnectQueuedPlayer(client); + accounts_to_remove.push_back(client.w_accountid); + } + } +} +``` + +**6. Sends Auto-Connect Requests to Login Server** +```cpp +// world/world_queue.cpp - AutoConnectQueuedPlayer + SendQueueAutoConnect +void QueueManager::AutoConnectQueuedPlayer(const QueuedClient& qclient) { + // Add reservation FIRST - grants capacity bypass + m_account_rez_mgr.AddRez(qclient.w_accountid, qclient.ip_address, 30); + + // Then send auto-connect packet to login server + SendQueueAutoConnect(qclient); +} + +void QueueManager::SendQueueAutoConnect(const QueuedClient& client) { + auto pack = new ServerPacket(ServerOP_QueueAutoConnect, sizeof(ServerQueueAutoConnect_Struct)); + ServerQueueAutoConnect_Struct* sqac = (ServerQueueAutoConnect_Struct*)pack->pBuffer; + + sqac->loginserver_account_id = client.ls_account_id; + sqac->ip_address = client.ip_address; + strncpy(sqac->client_key, client.authorized_client_key.c_str(), sizeof(sqac->client_key) - 1); + + loginserver->SendPacket(pack); // โ†’ ServerOP_QueueAutoConnect โ†’ ProcessQueueAutoConnect +} +``` + +### **Implementation Examples:** + +**1. Master Queue Controller - Queue Decision Logic** +```cpp +// loginserver/client.cpp - Handle_Play (entry point) +void Client::Handle_Play(const char* data) { + if(m_client_status != cs_logged_in) { + LogError("Client sent a play request when they either were not logged in, discarding."); + return; + } + + if (data) { + server.server_manager->SendUserToWorldRequest(data, m_account_id, + m_connection->GetRemoteIP(), false, m_key); + } +} + +// loginserver/server_manager.cpp - SendUserToWorldRequest +void ServerManager::SendUserToWorldRequest(const char* server_id, unsigned int client_account_id, + uint32 ip, bool is_auto_connect, const std::string& client_key) { + // Find target world server and send ServerOP_UsertoWorldReq packet + UsertoWorldRequest* utwr = (UsertoWorldRequest*)outapp.Data(); + utwr->lsaccountid = client_account_id; + utwr->ip = ip; + utwr->FromID = is_auto_connect ? 1 : 0; // 0 = manual PLAY, 1 = auto-connect + strncpy(utwr->forum_name, client_key.c_str(), sizeof(utwr->forum_name) - 1); + + (*iter)->GetConnection()->Send(ServerOP_UsertoWorldReq, outapp); +} + +// world/login_server.cpp - ProcessUsertoWorldReq (main decision point) +void LoginServer::ProcessUsertoWorldReq(uint16_t opcode, EQ::Net::Packet& p) { + // ... status checks, bans, IP limits ... + + uint32 effective_population = GetWorldPop(); + uint32 queue_cap = RuleI(Quarm, PlayerPopulationCap); + + if (effective_population >= queue_cap) { + // Build connection request for centralized evaluation + ConnectionRequest request = {}; + request.account_id = id; + request.ls_account_id = utwr->lsaccountid; + request.ip_address = utwr->ip; + request.status = status; + request.is_auto_connect = (utwr->FromID == 1); + request.forum_name = utwr->forum_name; + + // CENTRALIZED DECISION: Let queue manager handle ALL queue logic + bool should_override_capacity = queue_manager.EvaluateConnectionRequest(request, queue_cap, utwrs, nullptr); + + if (should_override_capacity) { + LogInfo("APPROVED bypass for account [{}] - allowing connection", id); + } else { + LogInfo("QueueManager decision for account [{}] - response code [{}]", id, utwrs->response); + } + } +} +``` + +**2. Capacity Authority - Population Calculation** +```cpp +// world/world_queue.cpp - GetWorldPop calls EffectivePopulation +uint32 GetWorldPop() { + return queue_manager.EffectivePopulation(); +} + +uint32 QueueManager::EffectivePopulation() { + // Get base population from account reservation manager + uint32 base_population = m_account_rez_mgr.Total(); + + // Add test population offset for simulation + uint32 test_offset = m_cached_test_offset; + + // Calculate final effective population + uint32 effective_population = base_population + test_offset; + + QueueDebugLog(2, "Account reservations: {}, test offset: {}, effective total: {}", + base_population, test_offset, effective_population); + + return effective_population; +} +``` + +**5. Login Server Communication - Queue Update Packets** +```cpp +// world/world_queue.cpp - SendQueueAutoConnect +void QueueManager::SendQueueAutoConnect(const QueuedClient& client) { + auto pack = new ServerPacket(ServerOP_QueueAutoConnect, sizeof(ServerQueueAutoConnect_Struct)); + ServerQueueAutoConnect_Struct* sqac = (ServerQueueAutoConnect_Struct*)pack->pBuffer; + + sqac->loginserver_account_id = client.ls_accountid; + sqac->ip_address = client.ip; + strncpy(sqac->ip_addr_str, client.ip_str.c_str(), sizeof(sqac->ip_addr_str) - 1); + strncpy(sqac->client_key, client.authorized_client_key.c_str(), sizeof(sqac->client_key) - 1); + + if (loginserver.Connected()) { + loginserver.SendPacket(pack); + QueueDebugLog(1, "Sent ServerOP_QueueAutoConnect for LS account [{}]", client.ls_accountid); + } + + safe_delete(pack); +} + +// world/world_queue.cpp - SendQueuedClientsUpdate +void QueueManager::SendQueuedClientsUpdate() { + for (const auto& client : m_queued_clients) { + auto pack = new ServerPacket(ServerOP_QueueDirectUpdate, sizeof(ServerQueueDirectUpdate_Struct)); + ServerQueueDirectUpdate_Struct* sqdu = (ServerQueueDirectUpdate_Struct*)pack->pBuffer; + + sqdu->ls_account_id = client.ls_accountid; + sqdu->queue_position = client.position; + sqdu->estimated_wait = client.estimated_wait; + + if (loginserver.Connected()) { + loginserver.SendPacket(pack); + } + + safe_delete(pack); + } + + QueueDebugLog(1, "Sent queue position updates for {} clients", m_queued_clients.size()); +} +``` + +--- +### **Database Schema & Event-Driven Synchronization** + +#### **Table Structures** +```sql +-- Real-time population tracking (QueueManager writes) +server_population: + - server_id, effective_population, last_updated + - Updated every 15s for login server display + +-- Account reservation backup (AccountRezMgr writes) +active_ip_connections: + - account_id, ip_address, last_seen, grace_period, is_in_raid + - Updated every 5min for crash recovery + +-- Queue configuration (Runtime toggles) +rule_values: + - Quarm:EnableQueue, Quarm:PlayerPopulationCap + - Quarm:TestPopulationOffset, Quarm:QueueBypassGMLevel +``` + +#### **Event-Driven Synchronization Patterns** + +**Queue Operations (Immediate Sync)** +``` +๐Ÿ”„ **On Queue Add:** +โ””โ”€ ๐Ÿ“ Memory: m_queued_players[account_id] = entry +โ””โ”€ ๐Ÿ’พ DB: Update server_population SET effective_population = X (immediate) +โ””โ”€ ๐Ÿ“‹ Log: "ADD_TO_QUEUE pos=1 wait=60s account=[12345] (memory + DB)" + +๐Ÿ”„ **On Queue Remove:** +โ””โ”€ ๐Ÿ—‘๏ธ Memory: m_queued_players.erase(account_id) +โ””โ”€ ๐Ÿ’พ DB: Update server_population SET effective_population = X (immediate) +โ””โ”€ ๐Ÿ“‹ Log: "REMOVE_FROM_QUEUE account=[12345] (memory + DB)" + +๐Ÿ”„ **On Auto-Connect Success:** +โ””โ”€ โณ Memory: Move to m_pending_connections +โ””โ”€ ๐Ÿ’พ DB: Update server_population (pending now counts as population) +โ””โ”€ ๐Ÿ“ก ServerOP_QueueAutoConnect โ†’ Login server +โ””โ”€ ๐Ÿ“‹ Log: "AUTO_CONNECT account=[12345] client_key=[abc123]" + +โšก **Result:** Database always synchronized, zero queue data loss! ๐ŸŽฏ +``` + +**Account Reservation Sync** +``` +๐Ÿ  **On Reservation Add:** +โ””โ”€ ๐Ÿ“ Memory: m_reservations[account_id] = {ip, timestamp, grace_period} +โ””โ”€ ๐Ÿ’พ DB: INSERT/UPDATE active_ip_connections (every 5min batch) +โ””โ”€ ๐Ÿ“Š Immediate population count update + +๐Ÿ—‘๏ธ **On Reservation Remove:** +โ””โ”€ ๐Ÿงน Memory: m_reservations.erase(account_id) +โ””โ”€ ๐Ÿ’พ DB: Mark for deletion in next sync cycle +โ””โ”€ ๐Ÿ“‰ Immediate population count update + +โฐ **Grace Period Events:** +โ””โ”€ ๐Ÿ• Memory: Update grace_period timestamps +โ””โ”€ ๐Ÿ’พ DB: Batch sync every 5min (crash recovery data) +โ””โ”€ ๐Ÿ“Š Population changes update immediately +``` + + + + diff --git a/utils/scripts/queue_system/queue-system-test-interactive.sh b/utils/scripts/queue_system/queue-system-test-interactive.sh new file mode 100755 index 000000000..24c1aa165 --- /dev/null +++ b/utils/scripts/queue_system/queue-system-test-interactive.sh @@ -0,0 +1,1660 @@ +#!/bin/bash + +# EQMacEmu Queue System Interactive Test Script +# Comprehensive testing environment for queue system and auto-connect functionality +# +# IMPORTANT: Account-Based Population Tracking +# This script measures "Server Reservations" (unique account IDs with active reservations) +# NOT "Active Connections" (currently connected clients) +# +# Server Reservations (account-based tracking) includes: +# - Players on character select screen +# - Players creating characters +# - Players loading into zones +# - Players actively in zones +# - Players temporarily disconnected but within grace period +# +# This is the correct measure for queue decisions because: +# 1. Each account represents one server reservation/slot regardless of connection state +# 2. Prevents players from losing queue position due to crashes/disconnects +# 3. Accurately reflects server resource commitment vs just connection count +# 4. Handles zoning, loading, and temporary disconnections gracefully +# 5. More accurate than IP-based tracking (multiple accounts behind NAT, dynamic IPs) +# +# NEW DATABASE SCHEMA: +# - server_population: Updated every 15 seconds with total account reservation count (for real-time reporting) +# - active_account_connections: Updated every 5 minutes with detailed account reservation data (for crash recovery) +# Contains: account_id, ip_address, last_seen, grace_period, is_in_raid +# - Grace periods: 60s (normal players), 600s (raid members) +# +# WORLD SERVER ID ISSUE FIX: +# In test environments with frequent server restarts, world servers may get new ServerIDs each time +# they restart, leaving orphaned queue entries tied to old server IDs. This script now: +# - Uses smarter auto-detection based on server names and queue history +# - Detects and warns about multiple server IDs in queue +# - Provides cleanup tools to remove orphaned entries (option 9) +# - Shows which server ID is currently active vs old/orphaned entries +# +# The script provides tools to monitor both tables and understand the dual-update system. + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +ORANGE='\033[38;5;208m' +PURPLE='\033[0;35m' +WHITE='\033[1;37m' +NC='\033[0m' # No Color + +# ============================================================================ +# USER CONFIGURATION - Modify these settings for your setup +# ============================================================================ + +# Path to eqemu_config.json (modify if your config lives elsewhere) +CONFIG_PATH="$HOME/quick-quarm/bin/eqemu_config.json" + +# Attempt to read DB credentials from config using jq; fallback to defaults +if [[ -f "$CONFIG_PATH" ]]; then + # Read database settings using native tools (no jq required) + if command -v jq >/dev/null 2>&1; then + # Use jq if available (faster and more reliable) + DB_HOST=$(jq -r '.database.host' "$CONFIG_PATH") + DB_USER=$(jq -r '.database.username' "$CONFIG_PATH") + DB_PASS=$(jq -r '.database.password' "$CONFIG_PATH") + DB_NAME=$(jq -r '.database.db' "$CONFIG_PATH") + WORLD_SERVER_NAME=$(jq -r '.longname' "$CONFIG_PATH") + WORLD_SERVER_SHORT_NAME=$(jq -r '.shortname' "$CONFIG_PATH") + else + # Use native bash tools to parse JSON - extract database section first + # Get the database section between "database": { and the closing } + DB_SECTION=$(sed -n '/"database"[[:space:]]*:[[:space:]]*{/,/^[[:space:]]*}/p' "$CONFIG_PATH") + + # Extract database values from the database section + DB_HOST=$(echo "$DB_SECTION" | grep -o '"host"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"host"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') + DB_USER=$(echo "$DB_SECTION" | grep -o '"username"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"username"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') + DB_PASS=$(echo "$DB_SECTION" | grep -o '"password"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"password"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') + DB_NAME=$(echo "$DB_SECTION" | grep -o '"db"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"db"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') + + # Extract server names from root level (not in database section) + WORLD_SERVER_NAME=$(grep -o '"longname"[[:space:]]*:[[:space:]]*"[^"]*"' "$CONFIG_PATH" | head -1 | sed 's/.*"longname"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') + WORLD_SERVER_SHORT_NAME=$(grep -o '"shortname"[[:space:]]*:[[:space:]]*"[^"]*"' "$CONFIG_PATH" | head -1 | sed 's/.*"shortname"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') + fi + + # Set defaults for any empty values + DB_HOST=${DB_HOST:-"localhost"} + DB_USER=${DB_USER:-"quarm"} + DB_PASS=${DB_PASS:-"quarm"} + DB_NAME=${DB_NAME:-"quarm"} + WORLD_SERVER_NAME=${WORLD_SERVER_NAME:-"Quick Quarm EQ"} + WORLD_SERVER_SHORT_NAME=${WORLD_SERVER_SHORT_NAME:-"QuickQuarm"} + + # Validate that we got valid values (not null or empty) + if [ "$WORLD_SERVER_NAME" = "null" ] || [ -z "$WORLD_SERVER_NAME" ]; then + WORLD_SERVER_NAME="Quick Quarm EQ" # Fallback + fi + if [ "$WORLD_SERVER_SHORT_NAME" = "null" ] || [ -z "$WORLD_SERVER_SHORT_NAME" ]; then + WORLD_SERVER_SHORT_NAME="QuickQuarm" # Fallback + fi +else + # Fallback / manual override + DB_HOST="localhost" + DB_USER="quarm" + DB_PASS="quarm" + DB_NAME="quarm" + WORLD_SERVER_NAME="Quick Quarm EQ" + WORLD_SERVER_SHORT_NAME="QuickQuarm" +fi + +# Server details - Now automatically read from eqemu_config.json +# These values are read from your bin/eqemu_config.json "longname" and "shortname" fields +# If config file is not found or jq is not installed, defaults to "Quick Quarm EQ" / "QuickQuarm" + +# Alternative: Auto-detect first available server (set to "true" to enable) +AUTO_DETECT_SERVER="true" + +# ============================================================================ +# END USER CONFIGURATION +# ============================================================================ + +# Function to check database prerequisites +check_database_prerequisites() { + print_step "Checking database prerequisites for queue system..." + + # Check if SQL migration file exists + if [ ! -f "$SQL_MIGRATION_PATH" ]; then + print_error "Required SQL migration file not found:" + print_error " Expected: $SQL_MIGRATION_PATH" + print_error " This file is required to create queue system tables." + return 1 + fi + + print_info "โœ“ SQL migration file found: $SQL_MIGRATION_PATH" + + # Check if required tables exist + local required_tables=("tblLoginQueue" "server_population" "tblloginserversettings") + local missing_tables=() + + for table in "${required_tables[@]}"; do + if ! mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e "DESCRIBE $table;" >/dev/null 2>&1; then + missing_tables+=("$table") + fi + done + + if [ ${#missing_tables[@]} -gt 0 ]; then + print_error "Missing required database tables: ${missing_tables[*]}" + print_error "Please run the SQL migration first:" + print_error " mysql -h$DB_HOST -u$DB_USER -p$DB_PASS $DB_NAME < \"$SQL_MIGRATION_PATH\"" + echo + print_info "You can also run the migration automatically:" + echo -n "Run SQL migration now? [y/N]: " + read run_migration + if [[ "$run_migration" =~ ^[Yy]$ ]]; then + print_step "Running SQL migration..." + if mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" < "$SQL_MIGRATION_PATH" 2>/dev/null; then + print_info "โœ“ SQL migration completed successfully" + else + print_error "โœ— SQL migration failed - please run manually" + return 1 + fi + else + return 1 + fi + else + print_info "โœ“ All required database tables exist" + fi + + # Check if queue rules exist with new naming convention + local queue_rules=("Quarm.PlayerPopulationCap" "Quarm.EnableQueue" "Quarm.TestPopulationOffset") + local missing_rules=() + + for rule in "${queue_rules[@]}"; do + local rule_exists=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT COUNT(*) FROM rule_values WHERE rule_name='$rule';" 2>/dev/null) + if [ "$rule_exists" = "0" ]; then + missing_rules+=("$rule") + fi + done + + if [ ${#missing_rules[@]} -gt 0 ]; then + print_error "Missing queue system rules: ${missing_rules[*]}" + print_error "These will be created automatically when you run setup option (10)." + print_info "Note: Queue system uses new Quarm.* rule naming convention as of 2025" + else + print_info "โœ“ Queue system rules found with correct naming convention" + fi + + print_info "โœ“ Database prerequisites check completed" + return 0 +} + +print_header() { + echo -e "${BLUE}============================================${NC}" + echo -e "${BLUE} EQMacEmu Queue System Test Infrastructure${NC}" + echo -e "${BLUE}============================================${NC}" + echo +} + +print_step() { + echo -e "${GREEN}[STEP]${NC} $1" +} + +print_info() { + echo -e "${YELLOW}[INFO]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Execute SQL command with validation +execute_sql() { + local sql_command="$1" + local result=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e "$sql_command" 2>&1) + local exit_code=$? + + if [ $exit_code -ne 0 ]; then + echo -e "${RED}[ERROR]${NC} SQL execution failed: $result" + return 1 + fi + + echo "$result" + return 0 +} + +# Trigger queue refresh by setting database flag +trigger_queue_refresh() { + mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e " +INSERT INTO tblloginserversettings (type, value, category, description) +VALUES ('RefreshQueue', '1', 'options', 'Trigger queue refresh - auto-reset by system') +ON DUPLICATE KEY UPDATE value = '1'; +" 2>/dev/null + + if [ $? -eq 0 ]; then + echo -e "${GREEN}[INFO]${NC} Queue refresh triggered - live system will sync within seconds" + else + echo -e "${YELLOW}[WARN]${NC} Failed to trigger queue refresh" + fi +} + +# Function to find the current world server ID +find_world_server_id() { + local quiet_flag="$1" + + if [ "$quiet_flag" != "quiet" ]; then + print_info "Auto-detecting world server..." + fi + + # Get the most recent server registration regardless of name + # This is more reliable than name matching + local latest_server_id=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e " + SELECT ServerID + FROM tblWorldServerRegistration + ORDER BY ServerID DESC + LIMIT 1;" 2>/dev/null) + + if [ -n "$latest_server_id" ] && [ "$latest_server_id" -ne 0 ]; then + # Get the server name for verification + local server_name=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e " + SELECT ServerLongName + FROM tblWorldServerRegistration + WHERE ServerID = $latest_server_id;" 2>/dev/null) + + if [ "$quiet_flag" != "quiet" ]; then + print_info "Found current server '$server_name' with ID: $latest_server_id" + fi + + echo "$latest_server_id" + return 0 + else + if [ "$quiet_flag" != "quiet" ]; then + print_error "No world server registrations found" + fi + echo "0" + return 1 + fi +} + +# Function to show available world servers (for troubleshooting) +show_available_servers() { + print_info "Available world servers in database:" + mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e "SELECT ServerID, serverLongName, serverShortName FROM tblWorldServerRegistration;" 2>/dev/null + echo + print_info "If no servers are listed, make sure your world server is running and has registered with the login server." + print_info "Check your eqemu_config.json 'longname' and 'shortname' settings." +} + +# Function to check if rule exists and update it +set_rule() { + local rule_name="$1" + local rule_value="$2" + + # Check if rule exists + local exists=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -se "SELECT COUNT(*) FROM rule_values WHERE ruleset_id=1 AND rule_name='$rule_name';" 2>/dev/null) + + if [ "$exists" = "1" ]; then + execute_sql "UPDATE rule_values SET rule_value='$rule_value' WHERE ruleset_id=1 AND rule_name='$rule_name';" + print_info "Updated rule $rule_name = $rule_value" + else + execute_sql "INSERT INTO rule_values (ruleset_id, rule_name, rule_value, notes) VALUES (1, '$rule_name', '$rule_value', 'Test queue system');" + print_info "Created rule $rule_name = $rule_value" + fi +} + +# Function to create test accounts +create_test_accounts() { + print_step "Creating test accounts for queue simulation..." + + for i in {1..10}; do + local account_name="queuetest$i" + local exists=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -se "SELECT COUNT(*) FROM tblLoginServerAccounts WHERE AccountName='$account_name';" 2>/dev/null) + + if [ "$exists" = "0" ]; then + execute_sql "INSERT INTO tblLoginServerAccounts (AccountName, AccountPassword, AccountEmail, LastLoginDate, LastIPAddress) VALUES ('$account_name', SHA('testpass'), 'test$i@test.com', NOW(), '127.0.0.1');" + print_info "Created test account: $account_name" + fi + done +} + +# Function to setup queue environment +setup_queue_environment() { + print_step "Setting up queue test environment..." + + # Set very low population cap for testing + set_rule "Quarm.PlayerPopulationCap" "5" + + # Enable queue system + set_rule "Quarm.EnableQueue" "true" + set_rule "Quarm.EnableQueueLogging" "true" + set_rule "Quarm.QueueEstimatedWaitPerPlayer" "30" + + print_info "Queue environment configured:" + print_info " - Population cap: 5 players" + print_info " - Queue system: Enabled" + print_info " - Wait time: 30 seconds per position" +} + +# Function to simulate population +simulate_population() { + local count="$1" + print_step "Simulating $count online players..." + + # This would typically be done by having actual clients connected + # For testing, we can temporarily modify the status reporting + print_info "To simulate population, you'll need to:" + print_info " 1. Connect $count test clients to the world server" + print_info " 2. Or use the simulate-population.sh script" + print_info " 3. Current simulation creates accounts ready for queue testing" +} + +# Function to create fake queue entries for testing +create_fake_queue() { + print_step "Creating simulated queue entries..." + + # Get world server ID (assuming first/only server for testing) + local world_server_id=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -se "SELECT ServerID FROM tblWorldServerRegistration ORDER BY ServerID LIMIT 1;" 2>/dev/null) + + if [ -z "$world_server_id" ]; then + print_error "No world server found in database. Make sure world server is registered." + return 1 + fi + + print_info "Using world server ID: $world_server_id" + + # Clear existing queue entries + execute_sql "DELETE FROM tblLoginQueue WHERE world_server_id = $world_server_id;" + + # Create queue entries for test accounts + for i in {1..8}; do + local account_id=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -se "SELECT LoginServerID FROM tblLoginServerAccounts WHERE AccountName='queuetest$i';" 2>/dev/null) + + if [ -n "$account_id" ]; then + local position=$((10 - i)) # Positions 9, 8, 7, 6, 5, 4, 3, 2 + local wait_time=$((position * 30)) + + execute_sql "INSERT INTO tblLoginQueue (account_id, world_server_id, queue_position, estimated_wait, ip_address, queued_timestamp, last_updated) VALUES ($account_id, $world_server_id, $position, $wait_time, INET_ATON('127.0.0.1'), NOW(), NOW());" + print_info "Added queuetest$i to queue at position $position" + fi + done + + print_info "Queue simulation created with 8 players in positions 2-9" + print_info "Position 1 is reserved for your test account" +} + +# Function to set population offset +set_population_offset() { + print_step "Setting Population Offset..." + + # Get current offset value + local current_offset=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT rule_value FROM rule_values WHERE rule_name IN ('Quarm.TestPopulationOffset', 'Quarm:TestPopulationOffset') LIMIT 1;" 2>/dev/null) + current_offset=${current_offset:-0} + + print_info "Current Test Population Offset: $current_offset" + echo + echo "Enter new population offset value (0-1200):" + echo " 0 = No offset (real population only)" + echo " 100 = Add 100 fake players" + echo " 500 = Add 500 fake players" + echo + echo -n "New offset value: " + read new_offset + + # Validate input + if [[ ! "$new_offset" =~ ^[0-9]+$ ]]; then + print_error "Invalid input. Please enter a number." + return 1 + fi + + if [ "$new_offset" -lt 0 ] || [ "$new_offset" -gt 1200 ]; then + print_error "Offset must be between 0 and 1200." + return 1 + fi + + # Update the database - try both naming conventions + execute_sql "UPDATE rule_values SET rule_value='$new_offset' WHERE rule_name='Quarm.TestPopulationOffset';" + execute_sql "UPDATE rule_values SET rule_value='$new_offset' WHERE rule_name='Quarm:TestPopulationOffset';" + + # Create the rule if it doesn't exist + execute_sql "INSERT IGNORE INTO rule_values (ruleset_id, rule_name, rule_value, notes) VALUES (1, 'Quarm:TestPopulationOffset', '$new_offset', 'Test population offset for queue testing');" + + print_info "Population offset updated: $current_offset โ†’ $new_offset" + print_info "The world server will update the total population count within ~15 seconds." + + if [ "$new_offset" -gt 0 ]; then + print_info "This will add $new_offset fake players to the server population for testing." + else + print_info "Test offset disabled - showing real population only." + fi +} + +# Function to show current queue status +show_queue_status() { + print_step "Current Queue Status:" + + local world_server_id=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -se "SELECT ServerID FROM tblWorldServerRegistration ORDER BY ServerID LIMIT 1;" 2>/dev/null) + + if [ -z "$world_server_id" ]; then + print_error "No world server found in database." + print_info "Make sure both servers are running." + return 1 + fi + + echo + echo "Position | Account | Acc ID | ETA | Timestamp" + echo "---------|-------------|--------|-----------|-------------------" + + mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -se " + SELECT + lq.queue_position, + lsa.AccountName, + lq.estimated_wait, + lq.last_updated + FROM tblLoginQueue lq + JOIN tblLoginServerAccounts lsa ON lq.account_id = lsa.LoginServerID + WHERE lq.world_server_id = $world_server_id + ORDER BY lq.queue_position; + " | while read position account wait_time timestamp; do + printf "%-8s | %-11s | %-9s | %s\n" "$position" "$account" "${wait_time}s" "$timestamp" + done + + echo + local queue_count=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -se "SELECT COUNT(*) FROM tblLoginQueue WHERE world_server_id = $world_server_id;" 2>/dev/null) + echo "Total players in queue: $queue_count" +} + +# Function for live queue monitoring +live_queue_monitor() { + print_step "Starting Live Queue Monitor..." + print_info "Press Ctrl+C to return to main menu" + echo + + # Clear entire screen and reset cursor position + clear + + # Initialize queue mode (1 = Dynamic, 2 = Chrono) + local queue_mode=1 + local notification="" + + # Hide cursor for smoother redraws + tput civis + + # Save the original trap and set local trap for monitor + local original_trap=$(trap -p INT) + trap 'echo -e "\n${YELLOW}[INFO]${NC} Returning to main menu..."; restore_main_trap; tput cnorm; return 0' INT + + # Test database connection first + if ! mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e "SELECT 1;" >/dev/null 2>&1; then + print_error "Cannot connect to database. Check connection settings at top of script." + restore_main_trap + tput cnorm + return 1 + fi + + while true; do + # Clear screen properly using clear command instead of escape sequences + clear + + echo -e "${BLUE}=== LIVE QUEUE MONITOR (Press Ctrl+C to exit) ===${NC}" + + # Show notification if any (only once) + if [ -n "$notification" ]; then + echo -e "${YELLOW}${notification}${NC}" + notification="" # Clear so it shows only once + fi + echo + + # Get current population and settings + local real_population=$(get_real_player_count) + local client_count=$(get_client_count) + local detailed_account_count=$(get_detailed_account_count) + local test_offset=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT rule_value FROM rule_values WHERE rule_name = 'Quarm:TestPopulationOffset';" 2>/dev/null) + local max_players=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT rule_value FROM rule_values WHERE rule_name = 'Quarm:PlayerPopulationCap';" 2>/dev/null) + local queue_enabled=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT rule_value FROM rule_values WHERE rule_name = 'Quarm:EnableQueue';" 2>/dev/null) + + # If colon notation doesn't work, try dot notation as fallback + if [ -z "$test_offset" ]; then + test_offset=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT rule_value FROM rule_values WHERE rule_name = 'Quarm.TestPopulationOffset';" 2>/dev/null) + fi + if [ -z "$max_players" ]; then + max_players=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT rule_value FROM rule_values WHERE rule_name = 'Quarm.PlayerPopulationCap';" 2>/dev/null) + fi + if [ -z "$queue_enabled" ]; then + queue_enabled=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT rule_value FROM rule_values WHERE rule_name = 'Quarm.EnableQueue';" 2>/dev/null) + fi + + # Set defaults if values are empty + real_population=${real_population:-0} + client_count=${client_count:-0} + detailed_account_count=${detailed_account_count:-0} + test_offset=${test_offset:-0} + max_players=${max_players:-1200} + queue_enabled=${queue_enabled:-false} + + # The real_population already includes test offset from world server, so use it directly + local effective_population=$real_population + + # Calculate how much of the population is from test offset vs real accounts + local actual_accounts=$((real_population - test_offset)) + if [ $actual_accounts -lt 0 ]; then + actual_accounts=0 + fi + + # Determine queue status + local queue_status="DISABLED" + local queue_active="INACTIVE" + if [ "$queue_enabled" = "true" ]; then + queue_status="ENABLED" + if [ $effective_population -ge $max_players ]; then + queue_active="ACTIVE (queuing new connections)" + else + queue_active="INACTIVE (server has capacity)" + fi + fi + + # Show population stats with both metrics + echo -e "${GREEN}Server Status:${NC}" + echo " Server Capacity: $max_players" + echo " Queue System: $queue_status" + echo " Queue Status: $queue_active" + + # Show queue configuration + echo -e "${YELLOW}Queue Configuration:${NC}" + + # Check if any of our expected rules exist + local rule_count=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e " + SELECT COUNT(*) FROM rule_values + WHERE rule_name IN ( + 'Quarm.EnableQueue', + 'Quarm.PlayerPopulationCap', + 'Quarm.TestPopulationOffset', + 'Quarm.QueueBypassGMLevel', + 'Quarm.QueueEstimatedWaitPerPlayer' + );" 2>/dev/null) + + # Also check for colon-based naming convention + local rule_count_colon=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e " + SELECT COUNT(*) FROM rule_values + WHERE rule_name IN ( + 'Quarm:EnableQueue', + 'Quarm:PlayerPopulationCap', + 'Quarm:TestPopulationOffset', + 'Quarm:QueueBypassGMLevel', + 'Quarm:QueueEstimatedWaitPerPlayer' + );" 2>/dev/null) + + if [ "$rule_count" -gt 0 ]; then + mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e " + SELECT + CONCAT(' ', + CASE + WHEN rule_name = 'Quarm.EnableQueue' THEN 'Queue Enabled: ' + WHEN rule_name = 'Quarm.PlayerPopulationCap' THEN 'Max Players: ' + WHEN rule_name = 'Quarm.TestPopulationOffset' THEN 'Test Offset: ' + WHEN rule_name = 'Quarm.QueueBypassGMLevel' THEN 'GM Bypass: ' + WHEN rule_name = 'Quarm.QueueEstimatedWaitPerPlayer' THEN 'Wait Per Player: ' + ELSE CONCAT(SUBSTRING(rule_name, 7), ': ') + END, + rule_value, + CASE + WHEN rule_name = 'Quarm.QueueEstimatedWaitPerPlayer' THEN ' seconds' + ELSE '' + END + ) + FROM rule_values + WHERE rule_name IN ( + 'Quarm.EnableQueue', + 'Quarm.PlayerPopulationCap', + 'Quarm.TestPopulationOffset', + 'Quarm.QueueBypassGMLevel', + 'Quarm.QueueEstimatedWaitPerPlayer' + ) + ORDER BY FIELD(rule_name, + 'Quarm.EnableQueue', + 'Quarm.PlayerPopulationCap', + 'Quarm.TestPopulationOffset', + 'Quarm.QueueBypassGMLevel', + 'Quarm.QueueEstimatedWaitPerPlayer' + ); + " 2>/dev/null + elif [ "$rule_count_colon" -gt 0 ]; then + mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e " + SELECT + CONCAT(' ', + CASE + WHEN rule_name = 'Quarm:EnableQueue' THEN 'Queue Enabled: ' + WHEN rule_name = 'Quarm:PlayerPopulationCap' THEN 'Max Players: ' + WHEN rule_name = 'Quarm:TestPopulationOffset' THEN 'Test Offset: ' + WHEN rule_name = 'Quarm:QueueBypassGMLevel' THEN 'GM Bypass: ' + WHEN rule_name = 'Quarm:QueueEstimatedWaitPerPlayer' THEN 'Wait Per Player: ' + ELSE CONCAT(SUBSTRING(rule_name, 7), ': ') + END, + rule_value, + CASE + WHEN rule_name = 'Quarm:QueueEstimatedWaitPerPlayer' THEN ' seconds' + ELSE '' + END + ) + FROM rule_values + WHERE rule_name IN ( + 'Quarm:EnableQueue', + 'Quarm:PlayerPopulationCap', + 'Quarm:TestPopulationOffset', + 'Quarm:QueueBypassGMLevel', + 'Quarm:QueueEstimatedWaitPerPlayer' + ) + ORDER BY FIELD(rule_name, + 'Quarm:EnableQueue', + 'Quarm:PlayerPopulationCap', + 'Quarm:TestPopulationOffset', + 'Quarm:QueueBypassGMLevel', + 'Quarm:QueueEstimatedWaitPerPlayer' + ); + " 2>/dev/null + else + echo " Queue rules not found in database" + fi + + # Show average wait time as N/A + echo " Average Wait Time: N/A" + + # Display test offset for easier reading + echo + echo -e "${GREEN}Test Offset:${NC}" + echo " +$test_offset" + + # Show commands at the top so they don't get scrolled away + echo + echo -e "${PURPLE}Commands:${NC}" + echo -e "${PURPLE} [${WHITE}6${PURPLE}] Delete queue [${WHITE}8${PURPLE}] Toggle queue mode [${WHITE}p${PURPLE}] Set population${NC}" + echo -e "${PURPLE} [${WHITE}s${PURPLE}] Lower pop (-1) [${WHITE}w${PURPLE}] Increase pop (+1) [${WHITE}q${PURPLE}] Quit${NC}" + echo -e "${PURPLE} [${WHITE}x${PURPLE}] Rescan server${NC}" +if [ "$queue_mode" = "1" ]; then + echo -e "${PURPLE} Queue mode: ${ORANGE}**(1) Dynamic - Rotate RAID, NORM, GRP, NEWB**${PURPLE} | (2) Chrono - Time based${NC}" +else + echo -e "${PURPLE} Queue mode: (1) Dynamic - Rotate RAID, NORM, GRP, NEWB | ${ORANGE}**(2) Chrono - Time based**${PURPLE}${NC}" +fi + + # Check for queue entries + echo + local queue_count=0 + + if mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e "DESCRIBE tblLoginQueue;" >/dev/null 2>&1; then + queue_count=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT COUNT(*) FROM tblLoginQueue;" 2>/dev/null) + queue_count=${queue_count:-0} + + echo -e "${YELLOW}Current Queue Entries:${NC}" + + # Just find server IDs that actually have queue data - much simpler! + local servers_with_queues=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e " + SELECT DISTINCT world_server_id + FROM tblLoginQueue + ORDER BY world_server_id DESC;" 2>/dev/null) + + if [ -n "$servers_with_queues" ]; then + # Use the most recent server ID (highest number) as primary display + local primary_server_id=$(echo "$servers_with_queues" | head -1) + echo -e "${BLUE}Showing queue entries for server ID: $primary_server_id${NC}" + + # Show how many servers have queue data + local server_count=$(echo "$servers_with_queues" | wc -l) + if [ "$server_count" -gt 1 ]; then + echo -e "${YELLOW}๐Ÿ“ Note: Found queue entries on $server_count servers: $(echo $servers_with_queues | tr '\n' ' ')${NC}" + echo -e "${YELLOW} Displaying entries from most recent server ($primary_server_id). Use option [6] to clean up old servers.${NC}" + fi + else + echo -e "${BLUE}No queue entries found in database${NC}" + primary_server_id="" + fi + + # Debug information + local total_entries=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e " + SELECT COUNT(*) FROM tblLoginQueue;" 2>/dev/null) + local current_entries=0 + if [ -n "$primary_server_id" ]; then + current_entries=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e " + SELECT COUNT(*) FROM tblLoginQueue WHERE world_server_id = $primary_server_id;" 2>/dev/null) + fi + + echo "Debug: Total queue entries: ${total_entries:-0} | For display server: ${current_entries:-0}" + + echo "Position | Account | Acc ID | ETA | Flag | Last Online | In Queue | Server | IP Address" + echo "---------|-------------|--------|-----------|------|---------------------|----------|--------|---------------" + + if [ $queue_count -gt 0 ]; then + # Show queue entries for the server we determined has entries + local query_filter="" + if [ -n "$primary_server_id" ]; then + query_filter="WHERE lq.world_server_id = $primary_server_id" + fi + + # Simplified queue entry display - don't try complex account lookups in the loop + mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e " + SELECT + lq.queue_position, + COALESCE(a.name, CONCAT('Account-', lq.account_id)) as account_name, + lq.account_id, + 'N/A', + 'NORM' as flag, + 'unknown' as last_online, + TIME_FORMAT(SEC_TO_TIME(TIMESTAMPDIFF(SECOND, lq.last_updated, NOW())), '%i:%s') as in_queue, + lq.world_server_id, + CONCAT( + (lq.ip_address & 0xFF), '.', + ((lq.ip_address >> 8) & 0xFF), '.', + ((lq.ip_address >> 16) & 0xFF), '.', + ((lq.ip_address >> 24) & 0xFF) + ) as ip_addr + FROM tblLoginQueue lq + LEFT JOIN account a ON lq.account_id = a.id + $query_filter + ORDER BY lq.world_server_id, lq.queue_position LIMIT 20; + " 2>/dev/null | while IFS=$'\t' read position account acc_id wait_time flag last_online in_queue server_id ip_addr; do + printf "%-8s | %-11s | %-6s | %-9s | %-4s | %-19s | %-8s | %-6s | %s\n" \ + "$position" "$account" "$acc_id" "$wait_time" "$flag" "$last_online" "$in_queue" "$server_id" "$ip_addr" + done + + # Show raw debug info if enabled - always show when requested + if [ "${show_raw_queue:-false}" = "true" ]; then + echo + echo -e "${BLUE}Raw queue entries in database (all servers):${NC}" + if [ "$queue_count" -gt 0 ]; then + mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" --table -e "SELECT * FROM tblLoginQueue ORDER BY world_server_id, queue_position LIMIT 20;" 2>/dev/null + else + echo " (no queue entries in database)" + fi + fi + + # Check if we have entries but they're not showing in the formatted display + local displayed_count=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e " + SELECT COUNT(*) FROM tblLoginQueue lq $query_filter;" 2>/dev/null) + displayed_count=${displayed_count:-0} # Default to 0 if empty + + if [ "$displayed_count" -eq 0 ]; then + # No entries for current server, but entries exist elsewhere + if [ "$queue_count" -gt 0 ]; then + echo -e "${YELLOW} (no entries for display server $primary_server_id - $queue_count total entries may be on other servers)${NC}" + echo -e "${YELLOW} Tip: Use option [6] to clean up old queue entries or check login server logs${NC}" + else + echo -e "${YELLOW} (no queue entries found in database)${NC}" + fi + fi + else + echo " (empty queue)" + fi + else + echo -e "${YELLOW}Queue table not found - queue system may not be configured${NC}" + fi + + # Show refresh info + echo + echo -e "${BLUE}Last updated: $(date '+%H:%M:%S') | Refreshing every 3 seconds...${NC}" + + # Wait for input with timeout (3 seconds) + # Use read with timeout and handle key presses + read -t 3 -n 1 key 2>/dev/null + + case "$key" in + # REMOVED: Cases "1" through "5" - force login, boot from queue, move up/down queue, send dialog msg + "6") + # Delete entire queue + echo + echo -e "${YELLOW}Available servers with queue entries:${NC}" + mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" --table -e " + SELECT + lq.world_server_id as 'Server ID', + COUNT(*) as 'Queue Count', + MAX(lq.last_updated) as 'Last Updated' + FROM tblLoginQueue lq + GROUP BY lq.world_server_id + ORDER BY lq.last_updated DESC;" 2>/dev/null + + echo + echo -n -e "${YELLOW}Enter server ID to clear (or 'all' for all servers): ${NC}" + read server_choice + + if [ "$server_choice" = "all" ]; then + echo + echo -e "${RED}WARNING: This will delete the entire queue for ALL world servers!${NC}" + echo -n -e "${YELLOW}Are you absolutely sure? [y/N]: ${NC}" + read -n 1 confirm + echo + if [[ "$confirm" =~ ^[Yy]$ ]]; then + local total_queue_count=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT COUNT(*) FROM tblLoginQueue;" 2>/dev/null) + + if [ "$total_queue_count" -gt 0 ]; then + execute_sql "DELETE FROM tblLoginQueue;" + local delete_result=$? + + if [ $delete_result -eq 0 ]; then + notification="${GREEN}Deleted entire queue: removed $total_queue_count players from all servers${NC}" + trigger_queue_refresh + else + notification="${RED}Failed to delete queue - MySQL error code: $delete_result${NC}" + fi + else + notification="${YELLOW}Queue is already empty - nothing to delete${NC}" + fi + else + notification="${BLUE}Queue deletion cancelled${NC}" + fi + elif [ -n "$server_choice" ] && [[ "$server_choice" =~ ^[0-9]+$ ]]; then + echo + local server_queue_count=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT COUNT(*) FROM tblLoginQueue WHERE world_server_id = $server_choice;" 2>/dev/null) + + if [ "$server_queue_count" -gt 0 ]; then + echo -e "${YELLOW}Delete queue for server $server_choice ($server_queue_count players) - Are you sure? [y/N]: ${NC}" + read -n 1 confirm + echo + if [[ "$confirm" =~ ^[Yy]$ ]]; then + execute_sql "DELETE FROM tblLoginQueue WHERE world_server_id = $server_choice;" + local delete_result=$? + + if [ $delete_result -eq 0 ]; then + notification="${GREEN}Deleted queue for server $server_choice: removed $server_queue_count players${NC}" + trigger_queue_refresh + else + notification="${RED}Failed to delete queue for server $server_choice${NC}" + fi + else + notification="${BLUE}Queue deletion cancelled${NC}" + fi + else + notification="${YELLOW}Server $server_choice has no queue entries${NC}" + fi + else + notification="${YELLOW}Queue deletion cancelled - invalid server choice${NC}" + fi + ;; + "8") + # Toggle queue mode + echo + if [ "$queue_mode" = "1" ]; then + echo -n -e "${YELLOW}Change to Chrono mode? [y/N]: ${NC}" + else + echo -n -e "${YELLOW}Change to Dynamic mode? [y/N]: ${NC}" + fi + read -n 1 confirm + echo + if [[ "$confirm" =~ ^[Yy]$ ]]; then + if [ "$queue_mode" = "1" ]; then + queue_mode=2 + notification="${GREEN}Queue mode changed to: (2) Chrono - Time based${NC}" + else + queue_mode=1 + notification="${GREEN}Queue mode changed to: (1) Dynamic - Rotate RAID, NORM, GRP, NEWB${NC}" + fi + else + notification="${BLUE}Queue mode change cancelled${NC}" + fi + ;; + "p"|"P") + # Set population offset + echo + local current_offset=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT rule_value FROM rule_values WHERE rule_name = 'Quarm:TestPopulationOffset' LIMIT 1;" 2>/dev/null) + current_offset=${current_offset:-0} + + echo -e "${YELLOW}Current population offset: $current_offset${NC}" + echo -n -e "${YELLOW}Enter new population offset (0-9999): ${NC}" + read new_offset + + if [[ "$new_offset" =~ ^[0-9]+$ ]] && [ "$new_offset" -ge 0 ] && [ "$new_offset" -le 9999 ]; then + mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e " + INSERT INTO rule_values (ruleset_id, rule_name, rule_value) + VALUES (1, 'Quarm:TestPopulationOffset', '$new_offset') + ON DUPLICATE KEY UPDATE rule_value = '$new_offset'; + " 2>/dev/null + + # Verify the update + local verified_offset=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT rule_value FROM rule_values WHERE rule_name = 'Quarm:TestPopulationOffset';" 2>/dev/null) + + notification="${GREEN}Population offset set: $current_offset โ†’ $new_offset${NC}" + notification+="\n${BLUE}Verified in database: Quarm:TestPopulationOffset = $verified_offset${NC}" + notification+="\n${YELLOW}World server will use new value within 5-15 seconds${NC}" + elif [ -n "$new_offset" ]; then + notification="${RED}Invalid input: '$new_offset'. Please enter a number between 0 and 9999.${NC}" + else + notification="${YELLOW}Set population cancelled - no value entered${NC}" + fi + ;; + "q"|"Q") + echo + echo -e "${YELLOW}[INFO]${NC} Returning to main menu..." + restore_main_trap + tput cnorm + return 0 + ;; + "s"|"S") + # Decrease test offset by 1 (lower population) + local current_offset=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT rule_value FROM rule_values WHERE rule_name = 'Quarm:TestPopulationOffset' LIMIT 1;" 2>/dev/null) + current_offset=${current_offset:-0} + + if [ "$current_offset" -gt 0 ]; then + local new_offset=$((current_offset - 1)) + mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e " + INSERT INTO rule_values (ruleset_id, rule_name, rule_value) + VALUES (1, 'Quarm:TestPopulationOffset', '$new_offset') + ON DUPLICATE KEY UPDATE rule_value = '$new_offset'; + " 2>/dev/null + + # Verify the update + local verified_offset=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT rule_value FROM rule_values WHERE rule_name = 'Quarm:TestPopulationOffset';" 2>/dev/null) + + notification="${GREEN}Population offset decreased: $current_offset โ†’ $new_offset${NC}" + notification+="\n${BLUE}Verified in database: Quarm:TestPopulationOffset = $verified_offset${NC}" + notification+="\n${YELLOW}World server will use new value within 5-15 seconds${NC}" + else + notification="${YELLOW}Population offset already at minimum (0)${NC}" + fi + ;; + "w"|"W") + # Increase test offset by 1 (higher population) + local current_offset=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT rule_value FROM rule_values WHERE rule_name = 'Quarm:TestPopulationOffset' LIMIT 1;" 2>/dev/null) + current_offset=${current_offset:-0} + + if [ "$current_offset" -lt 9999 ]; then # Reasonable maximum + local new_offset=$((current_offset + 1)) + mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e " + INSERT INTO rule_values (ruleset_id, rule_name, rule_value) + VALUES (1, 'Quarm:TestPopulationOffset', '$new_offset') + ON DUPLICATE KEY UPDATE rule_value = '$new_offset'; + " 2>/dev/null + + # Verify the update + local verified_offset=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT rule_value FROM rule_values WHERE rule_name = 'Quarm:TestPopulationOffset';" 2>/dev/null) + + notification="${GREEN}Population offset increased: $current_offset โ†’ $new_offset${NC}" + notification+="\n${BLUE}Verified in database: Quarm:TestPopulationOffset = $verified_offset${NC}" + notification+="\n${YELLOW}World server will use new value within 5-15 seconds${NC}" + else + notification="${YELLOW}Population offset already at maximum (9999)${NC}" + fi + ;; + "r"|"R") + # Toggle raw queue display + if [ "${show_raw_queue:-false}" = "true" ]; then + show_raw_queue="false" + notification="${GREEN}Raw queue display: OFF${NC}" + else + show_raw_queue="true" + notification="${GREEN}Raw queue display: ON${NC}" + fi + ;; + "x"|"X") + # Rescan server + echo + echo -e "${YELLOW}Rescanning server registration...${NC}" + + # Get current server information + local latest_server=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e " + SELECT ServerID, ServerLongName, ServerShortName, ServerLastLoginDate + FROM tblWorldServerRegistration + ORDER BY ServerID DESC + LIMIT 1;" 2>/dev/null) + + if [ -n "$latest_server" ]; then + echo "$latest_server" | while IFS=$'\t' read server_id long_name short_name last_login; do + echo -e "${GREEN}โœ“ Current server found:${NC}" + echo " Server ID: $server_id" + echo " Long Name: $long_name" + echo " Short Name: $short_name" + echo " Last Registration: $last_login" + done + + # Show queue data for this server + local queue_entries=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e " + SELECT COUNT(*) FROM tblLoginQueue WHERE world_server_id = ( + SELECT ServerID FROM tblWorldServerRegistration ORDER BY ServerID DESC LIMIT 1 + );" 2>/dev/null) + echo " Queue entries: ${queue_entries:-0}" + + notification="${GREEN}Server rescan complete - current server ID: $(echo "$latest_server" | cut -f1)${NC}" + else + notification="${RED}Server rescan failed - no server registrations found${NC}" + fi + ;; + # Legacy support for old key mappings - updated mappings + "f"|"F") key="1"; continue ;; + "b"|"B") key="2"; continue ;; + "e"|"E") key="3"; continue ;; + "d"|"D") key="4"; continue ;; + "m"|"M") key="8"; continue ;; + # "r"|"R") key="r"; continue ;; # Removed - causes infinite loop, 'r' case exists above + *) + # No key pressed or other key, continue refresh cycle + ;; + esac + done +} + +# Function to restore main trap after live monitor +restore_main_trap() { + trap 'echo -e "\n${YELLOW}[INFO]${NC} Goodbye!"; tput cnorm; exit 0' INT +} + +# Function to advance the queue (simulate someone entering the server) +advance_queue() { + print_step "Advancing queue (simulating player entry)..." + + local world_server_id=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_NAME" -se "SELECT ServerID FROM tblWorldServerRegistration ORDER BY ServerID LIMIT 1;" 2>/dev/null) + + if [ -z "$world_server_id" ]; then + print_error "No world server found in database." + print_info "Make sure both servers are running." + return 1 + fi + + # Remove the player at position 1 + execute_sql "DELETE FROM tblLoginQueue WHERE world_server_id = $world_server_id AND queue_position = 1;" + + # Move everyone else up one position + execute_sql "UPDATE tblLoginQueue SET queue_position = GREATEST(1, queue_position - 1), estimated_wait = GREATEST(30, (queue_position - 1) * 30), last_updated = NOW() WHERE world_server_id = $world_server_id AND queue_position > 1;" + + print_info "Queue advanced - position 1 entered server, all others moved up" +} + +# Function to add a test account to the queue +add_test_account_to_queue() { + print_step "Adding account to queue..." + + echo -n "Enter account name: " + read account_name + + if [ -z "$account_name" ]; then + print_error "Account name cannot be empty" + return 1 + fi + + local world_server_id=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_NAME" -se "SELECT ServerID FROM tblWorldServerRegistration ORDER BY ServerID LIMIT 1;" 2>/dev/null) + + if [ -z "$world_server_id" ]; then + print_error "No world server found in database." + print_info "Make sure both servers are running." + return 1 + fi + + # Get account ID + local account_id=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -se "SELECT LoginServerID FROM tblLoginServerAccounts WHERE AccountName = '$account_name';" 2>/dev/null) + + if [ -z "$account_id" ]; then + print_error "Account '$account_name' not found in database." + return 1 + fi + + # Get next available position + local next_position=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -se "SELECT COALESCE(MAX(queue_position), 0) + 1 FROM tblLoginQueue WHERE world_server_id = $world_server_id;" 2>/dev/null) + + # Calculate estimated wait time (60 seconds per position ahead) + local estimated_wait=$((next_position * 60)) + + # Add to queue + execute_sql "INSERT IGNORE INTO tblLoginQueue (account_id, world_server_id, queue_position, estimated_wait, ip_address, queued_timestamp, last_updated) VALUES ($account_id, $world_server_id, $next_position, $estimated_wait, INET_ATON('127.0.0.1'), NOW(), NOW());" + + if [ $? -eq 0 ]; then + print_info "Account '$account_name' added to queue at position $next_position (estimated wait: $estimated_wait seconds)" + else + print_error "Failed to add account to queue" + fi +} + +# Function to move account to front of queue (for testing auto-connect) +move_to_front() { + print_step "Moving account to front of queue..." + + echo -n "Enter account name: " + read account_name + + if [ -z "$account_name" ]; then + print_error "Account name cannot be empty" + return 1 + fi + + local world_server_id=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_NAME" -se "SELECT ServerID FROM tblWorldServerRegistration ORDER BY ServerID LIMIT 1;" 2>/dev/null) + + if [ -z "$world_server_id" ]; then + print_error "No world server found in database." + print_info "Make sure both servers are running." + return 1 + fi + + # Get account ID + local account_id=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -se "SELECT LoginServerID FROM tblLoginServerAccounts WHERE AccountName = '$account_name';" 2>/dev/null) + + if [ -z "$account_id" ]; then + print_error "Account '$account_name' not found in database." + return 1 + fi + + # Check if account is in queue + local current_position=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -se "SELECT queue_position FROM tblLoginQueue WHERE account_id = $account_id AND world_server_id = $world_server_id;" 2>/dev/null) + + if [ -z "$current_position" ]; then + print_error "Account '$account_name' is not in the queue." + return 1 + fi + + # Move everyone at position 1 and above up by 1 + execute_sql "UPDATE tblLoginQueue SET queue_position = queue_position + 1, estimated_wait = queue_position * 60, last_updated = NOW() WHERE world_server_id = $world_server_id AND queue_position >= 1 AND account_id != $account_id;" + + # Move target account to position 1 + execute_sql "UPDATE tblLoginQueue SET queue_position = 1, estimated_wait = 30, last_updated = NOW() WHERE account_id = $account_id AND world_server_id = $world_server_id;" + + print_info "Account '$account_name' moved to position 1 (ready for auto-connect testing)" +} + +# Function to clean up test environment +cleanup() { + print_step "Cleaning up test environment..." + + local world_server_id=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_NAME" -se "SELECT ServerID FROM tblWorldServerRegistration ORDER BY ServerID LIMIT 1;" 2>/dev/null) + + if [ -n "$world_server_id" ]; then + # Clear queue + execute_sql "DELETE FROM tblLoginQueue WHERE world_server_id = $world_server_id;" + + # Reset rules to defaults + set_rule "Quarm.PlayerPopulationCap" "1200" + set_rule "Quarm.EnableQueue" "true" + set_rule "Quarm.TestPopulationOffset" "0" + set_rule "Quarm.QueueBypassGMLevel" "true" + set_rule "Quarm.QueueEstimatedWaitPerPlayer" "60" + set_rule "Quarm.EnableQueueLogging" "true" + + # Remove test accounts (optional - comment out if you want to keep them) + # execute_sql "DELETE FROM tblLoginServerAccounts WHERE AccountName LIKE 'queuetest%';" + fi + + print_info "Test environment cleaned up" +} + +# Function to get IP-based population count from server_population table +# Returns the number of active IP reservations tracked by the world server +get_real_player_count() { + mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT current_population FROM server_population WHERE server_id = 1 LIMIT 1;" 2>/dev/null || echo "0" +} + +# Function to get active clients from tblLoginActiveAccounts +get_client_count() { + # Active clients = people who have logged in and are tracked by login server + # This is NOT the same as IP reservations - this tracks authentication sessions + local active_clients=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e " + SELECT COUNT(*) FROM tblLoginActiveAccounts; + " 2>/dev/null) + + echo "${active_clients:-0}" +} + +# Function to get detailed account tracking count +get_detailed_account_count() { + local detailed_count=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e " + SELECT COUNT(*) FROM active_account_connections; + " 2>/dev/null) + + echo "${detailed_count:-0}" +} + +# Function to show population comparison +show_population_comparison() { + print_step "Population Tracking Comparison" + + local client_count=$(get_client_count) + local account_reservations=$(get_real_player_count) + local detailed_account_count=$(get_detailed_account_count) + + echo + print_info "Population Metrics:" + print_info " Account Reservations (Real-time): $account_reservations (account-based tracking)" + print_info " Account Reservations (Detailed): $detailed_account_count (with grace period data)" + echo + + # Show differences and what they mean + if [ "$client_count" -eq "$account_reservations" ]; then + print_info "โœ… Client Count and Account Reservations match - no players in grace periods" + elif [ "$account_reservations" -gt "$client_count" ]; then + local grace_players=$((account_reservations - client_count)) + print_info "โ„น๏ธ $grace_players player(s) in grace period (disconnected but reservation held)" + print_info " These are players who disconnected but their account reservation is still active" + print_info " Grace periods: 60s (normal), 600s (raid members)" + else + local missing=$((client_count - account_reservations)) + print_info "โš ๏ธ $missing client(s) not tracked by account system - possible tracking issue" + print_info " This may indicate the account tracking system needs investigation" + fi + + # Show detailed vs real-time difference + if [ "$account_reservations" != "$detailed_account_count" ]; then + local diff=$((account_reservations - detailed_account_count)) + echo + if [ "$diff" -gt 0 ]; then + print_info "๐Ÿ“Š Real-time count is $diff higher than detailed count" + print_info " Real-time updates every 15s, detailed syncs every 5min" + else + print_info "๐Ÿ“Š Detailed count is $((-diff)) higher than real-time count" + print_info " May indicate recent disconnections not yet synced" + fi + fi +} + +# Function to debug population tracking +debug_population_tracking() { + print_step "Debugging Population Tracking System..." + + echo + print_info "1. Checking if server_population table exists..." + if mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e "DESCRIBE server_population;" >/dev/null 2>&1; then + print_info "โœ“ server_population table exists" + + echo + print_info "2. Table structure:" + mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" --table -e "DESCRIBE server_population;" 2>/dev/null + + echo + print_info "3. Current table contents:" + mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" --table -e "SELECT * FROM server_population;" 2>/dev/null + + local row_count=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT COUNT(*) FROM server_population;" 2>/dev/null) + if [ "$row_count" = "0" ]; then + print_error "Table is empty! This means the world server isn't updating it." + print_info "Possible causes:" + print_info " - World server not running" + print_info " - Population tracking code not compiled in" + print_info " - Database connection error in world server" + print_info " - LoginServer::SendStatus() not running (every 15 seconds)" + else + print_info "โœ“ Table has $row_count row(s)" + + local last_updated=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT last_updated FROM server_population WHERE server_id = 1;" 2>/dev/null) + if [ -n "$last_updated" ]; then + print_info "โœ“ Last updated: $last_updated" + + # Check how old the last update is + local seconds_ago=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT TIMESTAMPDIFF(SECOND, last_updated, NOW()) FROM server_population WHERE server_id = 1;" 2>/dev/null) + if [ "$seconds_ago" -gt 60 ]; then + print_error "โš  Last update was $seconds_ago seconds ago (should update every ~15 seconds)" + print_info "World server may not be running or population tracking is broken" + else + print_info "โœ“ Update is recent ($seconds_ago seconds ago)" + fi + fi + fi + else + print_error "โœ— server_population table does not exist!" + print_info "You may need to run database migrations or update your schema." + fi + + echo + print_info "4. Checking account tracking detail table (active_account_connections)..." + if mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e "DESCRIBE active_account_connections;" >/dev/null 2>&1; then + print_info "โœ“ active_account_connections table exists" + + local account_count=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT COUNT(*) FROM active_account_connections;" 2>/dev/null) + print_info "โœ“ Contains $account_count account reservations" + + if [ "$account_count" -gt 0 ]; then + echo + print_info "Recent account reservations:" + mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" --table -e " + SELECT + account_id as 'Account ID', + INET_NTOA(ip_address) as 'IP Address', + last_seen as 'Last Seen', + grace_period as 'Grace (s)', + CASE WHEN is_in_raid = 1 THEN 'Yes' ELSE 'No' END as 'Raid' + FROM active_account_connections + ORDER BY last_seen DESC + LIMIT 10; + " 2>/dev/null + fi + else + print_error "โœ— active_account_connections table does not exist!" + print_info "This table stores detailed account reservation data and is required for crash recovery." + fi + + echo + print_info "5. Testing manual population update..." + local test_population=999 + if mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e "INSERT INTO server_population (server_id, current_population) VALUES (1, $test_population) ON DUPLICATE KEY UPDATE current_population = $test_population, last_updated = NOW();" 2>/dev/null; then + print_info "โœ“ Manual update successful" + local new_value=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT current_population FROM server_population WHERE server_id = 1;" 2>/dev/null) + if [ "$new_value" = "$test_population" ]; then + print_info "โœ“ Value updated correctly to $new_value" + # Reset to 0 + mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e "UPDATE server_population SET current_population = 0 WHERE server_id = 1;" 2>/dev/null + print_info "โœ“ Reset to 0 for normal operation" + else + print_error "โœ— Value not updated correctly (got: $new_value, expected: $test_population)" + fi + else + print_error "โœ— Manual update failed - database permission issue" + fi +} + +# Function to show detailed account tracker status +show_account_tracker_status() { + print_step "Account Tracker Status and Reservations" + + # Get summary counts + local server_pop=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT COALESCE(current_population, 0) FROM server_population WHERE server_id = 1;" 2>/dev/null) + local account_detail_count=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT COUNT(*) FROM active_account_connections;" 2>/dev/null) + + echo + print_info "Population Summary:" + print_info " Current Population (server_population): ${server_pop:-Unknown}" + print_info " Detailed Account Reservations (active_account_connections): ${account_detail_count:-0}" + + if [ -n "$server_pop" ] && [ -n "$account_detail_count" ] && [ "$server_pop" != "$account_detail_count" ]; then + print_info " Note: Counts may differ - server_population updates every 15s, account details sync every 5min" + fi + + # Show detailed account connections if any exist + if [ "$account_detail_count" -gt 0 ]; then + echo + print_info "Active Account Reservations:" + mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" --table -e " + SELECT + account_id as 'Account ID', + INET_NTOA(ip_address) as 'IP Address', + last_seen as 'Last Seen', + grace_period as 'Grace Period (s)', + CASE WHEN is_in_raid = 1 THEN 'Yes' ELSE 'No' END as 'In Raid', + TIMESTAMPDIFF(SECOND, last_seen, NOW()) as 'Seconds Ago' + FROM active_account_connections + ORDER BY last_seen DESC; + " 2>/dev/null + + # Show grace period statistics + echo + print_info "Grace Period Breakdown:" + mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" --table -e " + SELECT + grace_period as 'Grace Period (s)', + COUNT(*) as 'Count', + CASE + WHEN grace_period = 60 THEN 'Normal Players' + WHEN grace_period = 600 THEN 'Raid Members' + ELSE 'Custom' + END as 'Type' + FROM active_account_connections + GROUP BY grace_period + ORDER BY grace_period; + " 2>/dev/null + else + echo + print_info "No active account reservations found." + print_info "This could mean:" + print_info " - No players are currently connected" + print_info " - World server hasn't synced the detailed table yet (syncs every 5 minutes)" + print_info " - Account tracking system is not working properly" + fi +} + +# Function to clear account tracking test data +clear_account_test_data() { + print_step "Clearing Account Tracking Test Data" + + echo + print_info "This will clear all account reservation data from active_account_connections table." + print_info "Use this for testing or to reset the account tracking system." + print_info "" + echo -n "Are you sure? [y/N]: " + read confirm + + if [[ "$confirm" =~ ^[Yy]$ ]]; then + local count_before=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT COUNT(*) FROM active_account_connections;" 2>/dev/null) + + execute_sql "DELETE FROM active_account_connections;" + + if [ $? -eq 0 ]; then + print_info "โœ… Cleared $count_before account reservations from test data" + print_info "Note: Real player connections will be re-added by the world server" + trigger_queue_refresh + else + print_error "โŒ Failed to clear account test data" + fi + else + print_info "Cancelled - no data cleared" + fi +} + +# Function to toggle GM bypass queue +toggle_gm_bypass() { + print_step "Toggling GM Bypass Queue..." + + local current_gm_bypass=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT rule_value FROM rule_values WHERE rule_name = 'Quarm:QueueBypassGMLevel';" 2>/dev/null) + + # If rule not found, create it with default value 'true' + if [ -z "$current_gm_bypass" ]; then + print_info "GM Bypass rule not found. Creating with default value 'true'..." + current_gm_bypass="true" + fi + + if [ "$current_gm_bypass" = "true" ]; then + new_gm_bypass="false" + print_info "Disabling GM Bypass Queue." + else + new_gm_bypass="true" + print_info "Enabling GM Bypass Queue." + fi + + set_rule "Quarm:QueueBypassGMLevel" "$new_gm_bypass" + + print_info "GM Bypass Queue is now: $new_gm_bypass" +} + +# Function to toggle queue freeze +toggle_freeze_queue() { + print_step "Toggling Queue Freeze..." + + local current_freeze=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT rule_value FROM rule_values WHERE rule_name = 'Quarm:FreezeQueue';" 2>/dev/null) + + # If rule not found, create it with default value 'false' + if [ -z "$current_freeze" ]; then + print_info "Freeze Queue rule not found. Creating with default value 'false'..." + current_freeze="false" + fi + + if [ "$current_freeze" = "true" ]; then + new_freeze="false" + print_info "Unfreezing queue - players will now advance in queue normally." + else + new_freeze="true" + print_info "Freezing queue - players will stay at their current positions." + fi + + set_rule "Quarm:FreezeQueue" "$new_freeze" + + print_info "Queue freeze is now: $new_freeze" + if [ "$new_freeze" = "true" ]; then + print_info "Note: Players can still be added/removed from queue, but positions won't advance." + else + print_info "Note: Queue positions will now advance every 30 seconds as normal." + fi +} + +# Function to delete entire queue +delete_entire_queue() { + print_step "Delete Entire Queue" + + echo + echo -e "${RED}WARNING: This will delete the entire queue for all players!${NC}" + echo -n -e "${YELLOW}Are you absolutely sure? [y/N]: ${NC}" + read -n 1 confirm + echo + + if [[ "$confirm" =~ ^[Yy]$ ]]; then + local world_server_id=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT ServerID FROM tblWorldServerRegistration ORDER BY ServerID LIMIT 1;" 2>/dev/null) + if [ -n "$world_server_id" ]; then + print_info "Found world server ID: $world_server_id" + local queue_count=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e "SELECT COUNT(*) FROM tblLoginQueue WHERE world_server_id=$world_server_id;" 2>/dev/null) + print_info "Current queue has $queue_count players" + + execute_sql "DELETE FROM tblLoginQueue WHERE world_server_id=$world_server_id;" + local delete_result=$? + + if [ $delete_result -eq 0 ]; then + print_info "Cleared queue for world server $world_server_id: removed $queue_count players" + trigger_queue_refresh + else + print_error "Failed to clear queue for world server $world_server_id" + fi + else + print_error "No world server found in database" + print_error "Make sure your world server is running and registered" + print_error "Check database connection settings at top of script" + return 1 + fi + else + print_info "Queue deletion cancelled" + return 0 + fi +} + +# Function to clean up orphaned queue entries from old/dead servers +cleanup_orphaned_queue_entries() { + print_step "Cleaning up orphaned queue entries from old server restarts..." + + # Get the current active world server ID + local current_server_id=$(find_world_server_id) + + if [ -z "$current_server_id" ]; then + print_error "Cannot determine current world server ID - cannot clean up safely" + return 1 + fi + + print_info "Current active server ID: $current_server_id" + + # Show orphaned entries before cleanup + echo + print_info "Checking for orphaned queue entries..." + + local orphaned_count=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e " + SELECT COUNT(*) + FROM tblLoginQueue q + WHERE q.world_server_id != $current_server_id;" 2>/dev/null) + + if [ "$orphaned_count" -gt 0 ]; then + print_info "Found $orphaned_count orphaned queue entries from old server restarts:" + + mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" --table -e " + SELECT + q.world_server_id as 'Old Server ID', + COUNT(*) as 'Orphaned Entries', + MAX(q.last_updated) as 'Last Updated' + FROM tblLoginQueue q + WHERE q.world_server_id != $current_server_id + GROUP BY q.world_server_id + ORDER BY q.world_server_id;" 2>/dev/null + + echo + echo -n -e "${YELLOW}Remove these orphaned entries? [y/N]: ${NC}" + read confirm + + if [[ "$confirm" =~ ^[Yy]$ ]]; then + # Delete orphaned entries + execute_sql "DELETE FROM tblLoginQueue WHERE world_server_id != $current_server_id;" + + if [ $? -eq 0 ]; then + print_info "โœ… Successfully removed $orphaned_count orphaned queue entries" + print_info "Queue now only contains entries for current server ID: $current_server_id" + trigger_queue_refresh + else + print_error "โŒ Failed to remove orphaned entries" + fi + else + print_info "Cleanup cancelled - orphaned entries remain" + fi + else + print_info "โœ… No orphaned queue entries found - all entries are for current server ID: $current_server_id" + fi +} + +# Main menu +show_menu() { + echo + echo "Queue System Interactive Test Options:" + echo "1. Live queue monitor" + echo "2. Set population offset" + echo "3. Debug population tracking" + echo "4. Show account tracker status" + echo "5. Clear account tracking test data" + echo "6. Delete queue (all players)" + echo "7. Toggle GM bypass queue" + echo "8. Toggle freeze queue" + echo "9. Cleanup orphaned queue entries" + echo "10. Exit" + echo + echo "Press Ctrl+C to exit anytime" + echo +} + +# Main execution +main() { + print_header + + # Set trap for Ctrl+C to exit gracefully + trap 'echo -e "\n${YELLOW}[INFO]${NC} Goodbye!"; exit 0' INT + + while true; do + show_menu + echo -n "Choose option [1-10]: " + read choice + + case $choice in + 1) + live_queue_monitor + ;; + 2) + set_population_offset + ;; + 3) + debug_population_tracking + ;; + 4) + show_account_tracker_status + ;; + 5) + clear_account_test_data + ;; + 6) + delete_entire_queue + ;; + 7) + toggle_gm_bypass + ;; + 8) + toggle_freeze_queue + ;; + 9) + cleanup_orphaned_queue_entries + ;; + 10) + print_info "Goodbye!" + exit 0 + ;; + *) + print_error "Invalid option. Please choose 1-10." + ;; + esac + done +} + +# Check if database is accessible +if ! mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" -e "USE $DB_NAME;" 2>/dev/null; then + print_error "Cannot connect to database. Please check connection details." + print_info "Edit this script to update DB_HOST, DB_USER, DB_PASS, DB_NAME variables" + exit 1 +fi + +# Show startup configuration +print_header +print_info "Configuration loaded:" +print_info " Config file: $CONFIG_PATH" +print_info " Database: $DB_HOST/$DB_NAME (user: $DB_USER)" +print_info " Server names: '$WORLD_SERVER_NAME' / '$WORLD_SERVER_SHORT_NAME'" +print_info " Auto-detect server: $AUTO_DETECT_SERVER" +echo + +# Run main menu +main + diff --git a/utils/scripts/queue_system/queue-system-test-simple b/utils/scripts/queue_system/queue-system-test-simple new file mode 100755 index 000000000..703e916f7 --- /dev/null +++ b/utils/scripts/queue_system/queue-system-test-simple @@ -0,0 +1,236 @@ +#!/bin/bash + +# Path to eqemu_config.json (modify if your config lives elsewhere) +CONFIG_PATH="$HOME/quick-quarm/bin/eqemu_config.json" + +# Attempt to read DB credentials from config using jq; fallback to defaults +if command -v jq >/dev/null 2>&1 && [[ -f "$CONFIG_PATH" ]]; then + DB_HOST=$(jq -r '.database.host' "$CONFIG_PATH") + DB_USER=$(jq -r '.database.username' "$CONFIG_PATH") + DB_PASS=$(jq -r '.database.password' "$CONFIG_PATH") + DB_NAME=$(jq -r '.database.db' "$CONFIG_PATH") +else + # Fallback / manual override + DB_HOST="localhost" + DB_USER="quarm" + DB_PASS="quarm" + DB_NAME="quarm" +fi + +# Check command line arguments +if [ $# -eq 0 ]; then + echo "Usage: $0 [value]" + echo "" + echo "Commands:" + echo " status - Show current server status and population (both tables)" + echo " simulate - Set test population offset (0 to disable)" + echo " capacity - Set server capacity limit (Quarm:PlayerPopulationCap)" + echo " enable - Enable queue system" + echo " disable - Disable queue system" + echo " gmbypass - Enable/disable GM bypass of queue (default: on)" + echo " iptracker - Show detailed IP reservation list with grace periods" + echo " ipcount - Show active IP connection count only" + echo " cleartestdata - Clear IP reservation test data" + echo "" + echo "Database Schema:" + echo " server_population - Total count (updated every 15s for real-time)" + echo " active_ip_connections - Detailed reservations (synced every 5min for crash recovery)" + echo "" + exit 1 +fi + +COMMAND=$1 +VALUE=$2 + +case $COMMAND in + "status") + echo "=== Server Status ===" + + # Get IP reservations (real population) + IP_RESERVATIONS=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e " + SELECT COUNT(*) FROM active_ip_connections; + " 2>/dev/null) + + # Get population from database (this is the effective population sent to login server) + EFFECTIVE_POPULATION=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e " + SELECT COALESCE(effective_population, 0) FROM server_population WHERE server_id = 1; + " 2>/dev/null) + + if [ -z "$EFFECTIVE_POPULATION" ]; then + echo "โŒ server_population table not found or empty!" + echo " Run: mysql -u quarm -pquarm quarm < source/EQMacEmu/utils/sql/git/required/2025_07_16_login_queue_system.sql" + EFFECTIVE_POPULATION="0" + fi + + echo "๐Ÿ”— Real IP Reservations: $IP_RESERVATIONS" + + # Get test offset + TEST_OFFSET=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e " + SELECT COALESCE(rule_value, '0') FROM rule_values + WHERE rule_name = 'Quarm:TestPopulationOffset'; + " 2>/dev/null) + + if [ "$TEST_OFFSET" != "0" ] && [ -n "$TEST_OFFSET" ]; then + echo "๐Ÿงช Test Population Offset: +$TEST_OFFSET" + echo "๐Ÿ“ˆ Effective Population: $EFFECTIVE_POPULATION (includes $IP_RESERVATIONS real + $TEST_OFFSET test)" + else + echo "๐Ÿ“ˆ Effective Population: $EFFECTIVE_POPULATION (same as real IP reservations)" + fi + + # Get capacity + CAPACITY=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e " + SELECT COALESCE(rule_value, 'Not Set') FROM rule_values + WHERE rule_name = 'Quarm:PlayerPopulationCap'; + " 2>/dev/null) + echo "๐ŸŽฏ Server Capacity: $CAPACITY" + + # Get legacy max players (main.cpp queue was removed) + LEGACY_MAX=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e " + SELECT COALESCE(rule_value, 'Not Set') FROM rule_values + WHERE rule_name = 'Quarm:MaxPlayersOnline'; + " 2>/dev/null) + echo "๐Ÿ›๏ธ Legacy Max Online: $LEGACY_MAX (main.cpp queue removed)" + + # Get queue status + QUEUE_ENABLED=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e " + SELECT COALESCE(rule_value, 'false') FROM rule_values + WHERE rule_name = 'Quarm:EnableQueue'; + " 2>/dev/null) + echo "๐Ÿšฅ Queue System: $QUEUE_ENABLED" + + # Get GM bypass status + GM_BYPASS=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e " + SELECT COALESCE(rule_value, 'true') FROM rule_values + WHERE rule_name = 'Quarm:QueueBypassGMLevel'; + " 2>/dev/null) + echo "๐Ÿ‘‘ GM Queue Bypass: $GM_BYPASS" + + ;; + + "iptracker") + echo "=== IP Tracker Status ===" + + # Show active IP connections with details + mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e " + SELECT + INET_NTOA(ip_address) as 'IP Address', + account_id as 'Account ID', + last_seen as 'Last Seen', + grace_period as 'Grace Period (s)', + CASE WHEN is_in_raid = 1 THEN 'Yes' ELSE 'No' END as 'In Raid' + FROM active_ip_connections + ORDER BY last_seen DESC; + " 2>/dev/null + + IP_COUNT=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e " + SELECT COUNT(*) FROM active_ip_connections; + " 2>/dev/null) + + echo "" + echo "Total Active IP Connections: $IP_COUNT" + ;; + + "ipcount") + IP_COUNT=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sN -e " + SELECT COUNT(*) FROM active_ip_connections; + " 2>/dev/null) + echo "Active IP Connections: $IP_COUNT" + ;; + + "cleartestdata") + echo "๐Ÿงน Clearing test IP data..." + mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e " + DELETE FROM active_ip_connections; + " 2>/dev/null + echo "โœ… Test IP data cleared" + ;; + + "simulate") + if [ -z "$VALUE" ]; then + echo "Error: simulate command requires a number" + exit 1 + fi + + echo "Setting test population offset to: $VALUE" + mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e " + INSERT INTO rule_values (ruleset_id, rule_name, rule_value) + VALUES (1, 'Quarm:TestPopulationOffset', '$VALUE') + ON DUPLICATE KEY UPDATE rule_value = '$VALUE'; + " 2>/dev/null + + if [ "$VALUE" -eq 0 ]; then + echo "โœ… Population simulation disabled" + else + echo "โœ… Population simulation enabled: +$VALUE players" + fi + ;; + + "capacity") + if [ -z "$VALUE" ]; then + echo "Error: capacity command requires a number" + exit 1 + fi + + echo "Setting server capacity to: $VALUE" + mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e " + INSERT INTO rule_values (ruleset_id, rule_name, rule_value) + VALUES (1, 'Quarm:PlayerPopulationCap', '$VALUE') + ON DUPLICATE KEY UPDATE rule_value = '$VALUE'; + " 2>/dev/null + echo "โœ… Server capacity set to $VALUE" + ;; + + "enable") + echo "Enabling queue system..." + mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e " + INSERT INTO rule_values (ruleset_id, rule_name, rule_value) + VALUES (1, 'Quarm:EnableQueue', 'true') + ON DUPLICATE KEY UPDATE rule_value = 'true'; + " 2>/dev/null + echo "โœ… Queue system enabled" + ;; + + "disable") + echo "Disabling queue system..." + mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e " + INSERT INTO rule_values (ruleset_id, rule_name, rule_value) + VALUES (1, 'Quarm:EnableQueue', 'false') + ON DUPLICATE KEY UPDATE rule_value = 'false'; + " 2>/dev/null + echo "โœ… Queue system disabled" + ;; + + "gmbypass") + if [ -z "$VALUE" ]; then + echo "Error: gmbypass command requires 'on' or 'off'" + exit 1 + fi + + if [ "$VALUE" = "on" ]; then + echo "Enabling GM bypass..." + mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e " + INSERT INTO rule_values (ruleset_id, rule_name, rule_value) + VALUES (1, 'Quarm:QueueBypassGMLevel', 'true') + ON DUPLICATE KEY UPDATE rule_value = 'true'; + " 2>/dev/null + echo "โœ… GM bypass enabled" + elif [ "$VALUE" = "off" ]; then + echo "Disabling GM bypass..." + mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e " + INSERT INTO rule_values (ruleset_id, rule_name, rule_value) + VALUES (1, 'Quarm:QueueBypassGMLevel', 'false') + ON DUPLICATE KEY UPDATE rule_value = 'false'; + " 2>/dev/null + echo "โœ… GM bypass disabled" + else + echo "Error: gmbypass requires 'on' or 'off'" + exit 1 + fi + ;; + + *) + echo "Error: Unknown command '$COMMAND'" + echo "Run '$0' without arguments for usage help" + exit 1 + ;; +esac \ No newline at end of file diff --git a/utils/sql/db_update_manifest.txt b/utils/sql/db_update_manifest.txt index 8b7c1de5e..2eaf64d3e 100644 --- a/utils/sql/db_update_manifest.txt +++ b/utils/sql/db_update_manifest.txt @@ -316,6 +316,7 @@ 9070|2015_01_28_quest_debug_log_category.sql|SELECT * FROM `logsys_categories` WHERE `log_category_description` LIKE 'Quest Debug'|empty| 9074|2015_02_01_logsys_packet_logs.sql|SELECT * FROM `logsys_categories` WHERE `log_category_description` LIKE 'Packet: Server -> Client'|empty| 9075|2015_02_02_logsys_packet_logs_with_dump.sql|SELECT * FROM `logsys_categories` WHERE `log_category_description` LIKE 'Packet: Server -> Client With Dump'|empty| +9076|2025_07_16_login_queue_system.sql|SHOW TABLES LIKE 'tblLoginQueue'|empty| # Upgrade conditions: # This won't be needed after this system is implemented, but it is used database that are not diff --git a/utils/sql/git/required/2025_07_16_login_queue_system.sql b/utils/sql/git/required/2025_07_16_login_queue_system.sql new file mode 100644 index 000000000..c66acb17e --- /dev/null +++ b/utils/sql/git/required/2025_07_16_login_queue_system.sql @@ -0,0 +1,39 @@ +-- Login Queue System Database Update +-- Add queue persistence table for maintaining queue positions across restarts +-- This enables queue persistence during emergency maintenance with 100+ queued players + +CREATE TABLE IF NOT EXISTS tblLoginQueue ( + account_id INT UNSIGNED NOT NULL, + world_server_id INT UNSIGNED NOT NULL, + queue_position INT UNSIGNED NOT NULL, + estimated_wait INT UNSIGNED NOT NULL, + ip_address INT UNSIGNED NOT NULL, + queued_timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (account_id, world_server_id), + INDEX idx_world_position (world_server_id, queue_position), + INDEX idx_timestamp (queued_timestamp) +) ENGINE=InnoDB; + +-- Server Population Tracking Table +-- Real-time population data updated by LoginServer::SendStatus() +-- Used for monitoring and debugging queue decisions +CREATE TABLE IF NOT EXISTS server_population ( + server_id INT NOT NULL DEFAULT 1, + effective_population INT NOT NULL DEFAULT 0, + last_updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (server_id) +) ENGINE=InnoDB; + +-- Initialize server_population table with default row +INSERT IGNORE INTO server_population (server_id, effective_population) VALUES (1, 0); + +-- Initialize RefreshQueue setting with default value +INSERT INTO tblloginserversettings (type, value, category, description, defaults) +VALUES ('RefreshQueue', '0', 'options', 'Trigger queue refresh - auto-reset by system', '0') +ON DUPLICATE KEY UPDATE value = '0', description = 'Trigger queue refresh - auto-reset by system'; + +-- Initialize pop_count setting to enable population display in server list +INSERT INTO tblloginserversettings (type, value, category, description, defaults) +VALUES ('pop_count', '1', 'display', 'Show population counts in server list', '1') +ON DUPLICATE KEY UPDATE value = '1', description = 'Show population counts in server list'; \ No newline at end of file diff --git a/world/CMakeLists.txt b/world/CMakeLists.txt index 675404a96..4e346eb2a 100644 --- a/world/CMakeLists.txt +++ b/world/CMakeLists.txt @@ -1,6 +1,7 @@ CMAKE_MINIMUM_REQUIRED(VERSION 3.12) SET(world_sources + account_reservation_manager.cpp client.cpp cliententry.cpp clientlist.cpp @@ -20,6 +21,7 @@ SET(world_sources world_event_scheduler.cpp world_config.cpp world_console_connection.cpp + world_queue.cpp world_server_cli.cpp worlddb.cpp world_boot.cpp @@ -28,6 +30,7 @@ SET(world_sources ) SET(world_headers + account_reservation_manager.h client.h cliententry.h clientlist.h @@ -47,6 +50,7 @@ SET(world_headers wguild_mgr.h world_config.h world_console_connection.h + world_queue.h world_tcp_connection.h world_server_cli.h worlddb.h diff --git a/world/account_reservation_manager.cpp b/world/account_reservation_manager.cpp new file mode 100644 index 000000000..0497a0ef1 --- /dev/null +++ b/world/account_reservation_manager.cpp @@ -0,0 +1,275 @@ +#include "worlddb.h" +#include "account_reservation_manager.h" +#include "../common/eqemu_logsys.h" +#include "../common/rulesys.h" +#include "clientlist.h" +#include "world_queue.h" + +#include +#include +#include +#include + +extern ClientList client_list; +extern class WorldDatabase database; + + +AccountRezMgr::AccountRezMgr() : m_last_cleanup(0), m_last_database_sync(0) { +} + +void AccountRezMgr::AddRez(uint32 account_id, uint32 ip_address, uint32 grace_period_seconds) { + // Default grace period if none specified or database not ready + extern bool database_ready; + if (grace_period_seconds == 0) { + if (database_ready) { + grace_period_seconds = RuleI(Quarm, DefaultGracePeriod); + } else { + QueueDebugLog(1, "Database not ready - using default grace period for account [{}]", account_id); + grace_period_seconds = 60; + } + } + + QueueDebugLog(1, "AccountRezMgr: Adding reservation - account_id: {}, ip_address: {}, grace_period: {}s", + account_id, ip_address, grace_period_seconds); + // Set last_seen to current time for new reservations + m_account_reservations[account_id] = PlayerInfo(account_id, time(nullptr), grace_period_seconds, false, ip_address); + LogConnectionChange(account_id, "registered"); + + UpdateGraceWhitelistStatus(account_id); +} + +void AccountRezMgr::RemoveRez(uint32 account_id) { + auto it = m_account_reservations.find(account_id); + if (it != m_account_reservations.end()) { + m_account_reservations.erase(it); + LogConnectionChange(account_id, "removed"); + } +} + +void AccountRezMgr::UpdateLastSeen(uint32 account_id) +{ + auto it = m_account_reservations.find(account_id); + if (it != m_account_reservations.end()) { + it->second.last_seen = time(nullptr); + + UpdateGraceWhitelistStatus(account_id); + } +} + +bool AccountRezMgr::CheckGracePeriod(uint32 account_id, uint32 current_time) { + if (current_time == 0) { + current_time = time(nullptr); + } + + auto it = m_account_reservations.find(account_id); + if (it == m_account_reservations.end()) { + return true; + } + + uint32 time_since_last_seen = current_time - it->second.last_seen; + bool exceeded_grace_period = time_since_last_seen > it->second.grace_period; + + return exceeded_grace_period; +} + +uint32 AccountRezMgr::GetRemainingGracePeriod(uint32 account_id, uint32 current_time) const { + if (current_time == 0) { + current_time = time(nullptr); + } + + auto it = m_account_reservations.find(account_id); + if (it == m_account_reservations.end()) { + return 0; + } + + uint32 time_since_last_seen = current_time - it->second.last_seen; + if (time_since_last_seen >= it->second.grace_period) { + return 0; + } + + return it->second.grace_period - time_since_last_seen; +} +void AccountRezMgr::CleanupStaleConnections() { + const uint32 CONNECTION_GRACE_SECONDS = 15; // TODO: Make a rule for this? + uint32 current_time = time(nullptr); + std::vector accounts_to_remove; + + // First pass: identify what needs to be updated or removed + for (auto& pair : m_account_reservations) { + uint32 account_id = pair.first; + + bool has_world_connection = client_list.ActiveConnection(account_id); + + if (has_world_connection) { + UpdateLastSeen(account_id); + QueueDebugLog(2, "AccountRezMgr: kept_active_char connection for account [{}] - total active accounts: {}", + account_id, m_account_reservations.size()); + } else { + // Check if this is a new reservation that hasn't had time to connect + uint32 time_since_creation = current_time - pair.second.last_seen; + + if (pair.second.last_seen == 0 || time_since_creation < CONNECTION_GRACE_SECONDS) { + QueueDebugLog(2, "AccountRezMgr: Account [{}] not connected to World, waiting for connection (age: {}s)", + account_id, time_since_creation); + continue; // Skip grace period logic for new reservations + } + + QueueDebugLog(2, "AccountRezMgr: Account [{}] doesn't have world connection - adding to grace whitelist", account_id); + + UpdateGraceWhitelistStatus(account_id); + + if (CheckGracePeriod(account_id, current_time)) { + LogConnectionChange(account_id, "cleanup_grace_expired"); + accounts_to_remove.push_back(account_id); + } else { + uint32 time_since_last_seen = current_time - pair.second.last_seen; + uint32 time_remaining = pair.second.grace_period - time_since_last_seen; + QueueDebugLog(2, "AccountRezMgr: Account [{}] in grace period, keeping reservation. Time remaining: [{}] seconds", account_id, time_remaining); + } + } + } + for (uint32 account_id : accounts_to_remove) { + RemoveRez(account_id); + } +} + +void AccountRezMgr::PeriodicMaintenance() { + uint32 current_time = time(nullptr); + + CleanupStaleConnections(); + if (ShouldPerformDatabaseSync()) { + m_last_database_sync = current_time; + QueueDebugLog(2, "AccountRezMgr: Full reservation list synced to database for crash recovery"); + } +} + + +void AccountRezMgr::LogConnectionChange(uint32 account_id, const std::string& action) const { + QueueDebugLog(1, "AccountRezMgr: {} connection for account [{}] - total active accounts: {}", + action, account_id, m_account_reservations.size()); +} + +std::string AccountRezMgr::AccountToString(uint32 account_id) const { + return fmt::format("Account[{}]", account_id); +} + +bool AccountRezMgr::ShouldPerformCleanup() const { + return (time(nullptr) - m_last_cleanup) >= RuleI(Quarm, IPCleanupInterval); +} + +bool AccountRezMgr::ShouldPerformDatabaseSync() const { + return (time(nullptr) - m_last_database_sync) >= RuleI(Quarm, IPDatabaseSyncInterval); +} + +bool AccountRezMgr::IsAccountInGraceWhitelist(uint32 account_id) { + uint32 current_time = time(nullptr); + + CleanupExpiredGraceWhitelist(); + + auto it = m_grace_whitelist.find(account_id); + if (it != m_grace_whitelist.end()) { + QueueDebugLog(2, "AccountRezMgr: Account [{}] found in grace whitelist (expires in {}s)", + account_id, it->second - current_time); + return true; + } + + return false; +} + +void AccountRezMgr::UpdateGraceWhitelistStatus(uint32 account_id) { + auto it = m_account_reservations.find(account_id); + if (it == m_account_reservations.end()) { + return; + } + + const PlayerInfo& info = it->second; + uint32 current_time = time(nullptr); + uint32 expires_at = info.last_seen + info.grace_period; + + m_grace_whitelist[account_id] = expires_at; + + QueueDebugLog(2, "AccountRezMgr: Account [{}] found in whitelist (expires: {})", + account_id, expires_at); +} + +void AccountRezMgr::RemoveFromGraceWhitelist(uint32 account_id) { + auto it = m_grace_whitelist.find(account_id); + if (it != m_grace_whitelist.end()) { + m_grace_whitelist.erase(it); + QueueDebugLog(1, "AccountRezMgr: Account [{}] removed from grace whitelist", account_id); + } +} + +void AccountRezMgr::IncreaseGraceDuration(uint32 account_id, uint32 grace_duration_seconds) { + // Only extend grace for accounts that already have reservations + auto it = m_account_reservations.find(account_id); + if (it == m_account_reservations.end()) { + QueueDebugLog(1, "AccountRezMgr: Cannot extend grace for account [{}] - no existing reservation", account_id); + return; // Fail silently - account needs a reservation first + } + + uint32 expires_at = time(nullptr) + grace_duration_seconds; + m_grace_whitelist[account_id] = expires_at; + QueueDebugLog(1, "AccountRezMgr: Account [{}] grace period extended for [{}] seconds (has existing reservation)", account_id, grace_duration_seconds); +} + +void AccountRezMgr::CleanupExpiredGraceWhitelist() { + uint32 current_time = time(nullptr); + + for (auto it = m_grace_whitelist.begin(); it != m_grace_whitelist.end(); ) { + if (it->second <= current_time) { + QueueDebugLog(2, "AccountRezMgr: Account [{}] grace whitelist expired, removed", it->first); + it = m_grace_whitelist.erase(it); + } else { + ++it; + } + } +} +// TODO: Implement database sync/restore methods +/* +void AccountRezMgr::SyncConnectionToDatabase(uint32 account_id, const PlayerInfo& info) { + std::string query = fmt::format( + "INSERT INTO active_account_connections (account_id, ip_address, last_seen, grace_period, is_in_raid) " + "VALUES ({}, {}, FROM_UNIXTIME({}), {}, {}) " + "ON DUPLICATE KEY UPDATE " + "ip_address = VALUES(ip_address), " + "last_seen = VALUES(last_seen), " + "grace_period = VALUES(grace_period), " + "is_in_raid = VALUES(is_in_raid)", + account_id, info.ip_address, info.last_seen, info.grace_period, info.is_in_raid ? 1 : 0 + ); + + auto result = database.QueryDatabase(query); + if (!result.Success()) { + LogError("Failed to sync account connection to database: {}", result.ErrorMessage()); + } +} + +void AccountRezMgr::RemoveConnectionFromDatabase(uint32 account_id) { + std::string query = fmt::format( + "DELETE FROM active_account_connections WHERE account_id = {}", + account_id + ); + + auto result = database.QueryDatabase(query); + if (!result.Success()) { + LogError("Failed to remove account connection from database: {}", result.ErrorMessage()); + } +} + +void AccountRezMgr::SyncAllConnectionsToDatabase() { + // Clear existing entries + auto clear_result = database.QueryDatabase("DELETE FROM active_account_connections"); + if (!clear_result.Success()) { + LogError("Failed to clear active_account_connections table: {}", clear_result.ErrorMessage()); + return; + } + + // Sync all current connections + for (const auto& pair : m_account_reservations) { + SyncConnectionToDatabase(pair.first, pair.second); + } + + LogInfo("AccountRezMgr: Synced {} connections to database", m_account_reservations.size()); +} +*/ diff --git a/world/account_reservation_manager.h b/world/account_reservation_manager.h new file mode 100644 index 000000000..74bae89ac --- /dev/null +++ b/world/account_reservation_manager.h @@ -0,0 +1,69 @@ +#ifndef ACCOUNT_RESERVATION_MANAGER_H +#define ACCOUNT_RESERVATION_MANAGER_H + +#include "../common/types.h" +#include +#include +#include +#include +#include + +// Forward declarations + +struct PlayerInfo { + uint32 account_id; + uint32 last_seen; + bool is_in_raid; + uint32 grace_period; + uint32 ip_address; // Keep IP for logging purposes + PlayerInfo(uint32 acct_id = 0, uint32 last_seen_time = 0, uint32 grace_period_seconds = 60, bool raid_status = false, uint32 ip_addr = 0) + : account_id(acct_id) + , last_seen(last_seen_time ? last_seen_time : time(nullptr)) + , is_in_raid(raid_status) // TODO: Flag this to calculate longer grace period window + , grace_period(grace_period_seconds) + , ip_address(ip_addr) {} +}; + +class AccountRezMgr { +public: + AccountRezMgr(); + + void AddRez(uint32 account_id, uint32 ip_address = 0, uint32 grace_period_seconds = 0); + void RemoveRez(uint32 account_id); + void PeriodicMaintenance(); + void UpdateLastSeen(uint32 account_id); + bool CheckGracePeriod(uint32 account_id, uint32 current_time = 0); + uint32 GetRemainingGracePeriod(uint32 account_id, uint32 current_time = 0) const; + uint32 Total() const { return m_account_reservations.size(); } + // uint32 EffectivePopulation(); // Single source of truth for world server population (now handles DB sync) + + // Constants and static queries + static const std::string queue_enablement_query; + // static const std::string population_sync_query_format; // REMOVED DATABASE SYNC + + // In-memory grace whitelist management (no database needed - same process) + bool IsAccountInGraceWhitelist(uint32 account_id); // Checks and auto-cleans expired entries + void IncreaseGraceDuration(uint32 account_id, uint32 grace_duration_seconds); // Add to in-memory whitelist + void RemoveFromGraceWhitelist(uint32 account_id); // Remove from in-memory whitelist + void CleanupExpiredGraceWhitelist(); // Clean up expired entries from in-memory map + void UpdateGraceWhitelistStatus(uint32 account_id); // Updates whitelist when reservation changes + +private: + // Account tracking data + std::map m_account_reservations; // account_id -> PlayerInfo + uint32 m_last_cleanup; // Last cleanup timestamp + uint32 m_last_database_sync; // Last database sync timestamp + bool m_queue_enabled; // Cached queue enablement status + + // In-memory grace whitelist - no database needed since both managers are in same process + std::map m_grace_whitelist; // account_id -> expires_at timestamp + + // Helper methods + void CleanupStaleConnections(); + std::string AccountToString(uint32 account_id) const; + void LogConnectionChange(uint32 account_id, const std::string& action) const; + bool ShouldPerformCleanup() const; + bool ShouldPerformDatabaseSync() const; +}; + +#endif // ACCOUNT_RESERVATION_MANAGER_H \ No newline at end of file diff --git a/world/client.cpp b/world/client.cpp index fc7de8840..2f94ebba2 100644 --- a/world/client.cpp +++ b/world/client.cpp @@ -30,6 +30,7 @@ #include "zonelist.h" #include "clientlist.h" #include "wguild_mgr.h" +#include "world_queue.h" // Add for QueueManager #include "char_create_data.h" #include "../common/repositories/player_event_logs_repository.h" #include "../common/events/player_event_logs.h" @@ -72,7 +73,8 @@ extern EQ::Random emu_random; extern uint32 numclients; extern volatile bool RunLoops; extern volatile bool UCSServerAvailable_; - +extern uint32 numzones; +extern LoginServer* loginserver; Client::Client(EQStreamInterface* ieqs) : autobootup_timeout(RuleI(World, ZoneAutobootTimeoutMS)), connect(1000), @@ -93,6 +95,7 @@ Client::Client(EQStreamInterface* ieqs) char_id = 0; zone_waiting_for_bootup = 0; enter_world_triggered = false; + is_graceful_disconnect = false; // Will be set to true for graceful m_ClientVersionBit = 0; numclients++; } @@ -102,6 +105,8 @@ Client::~Client() { cle->SetOnline(CLE_Status::Offline); } + // if (is_graceful_disconnect) {AccountTracker::Safe_RemoveReservation(cle->AccountID(), is_graceful_disconnect);} + // ^ Commented out for now. This gives a grace period even if the client disconnects gracefully consistency. numclients--; //let the stream factory know were done with this stream @@ -291,6 +296,15 @@ bool Client::HandleSendLoginInfoPacket(const EQApplicationPacket *app) { SendExpansionInfo(); SendCharInfo(); database.LoginIP(cle->AccountID(), long2ip(GetIP())); + + // Register this account as active for queue population tracking + // Skip if account already has a reservation (auto-connecting people get theirs earlier) + if (loginserver && !queue_manager.m_account_rez_mgr.IsAccountInGraceWhitelist(cle->AccountID())) { + queue_manager.m_account_rez_mgr.AddRez(cle->AccountID(), GetIP(), 6); + LogInfo("Added account reservation for account [{}] (normal connection)", cle->AccountID()); + } else if (queue_manager.m_account_rez_mgr.IsAccountInGraceWhitelist(cle->AccountID())) { + LogInfo("Account [{}] already has reservation (auto-connect/grace period)", cle->AccountID()); + } } } else { @@ -820,6 +834,7 @@ bool Client::HandlePacket(const EQApplicationPacket *app) { } case OP_WorldLogout: { + is_graceful_disconnect = true; eqs->Close(); return true; } diff --git a/world/client.h b/world/client.h index f66e04eb5..ebed72d25 100644 --- a/world/client.h +++ b/world/client.h @@ -79,6 +79,7 @@ class Client { Timer autobootup_timeout; uint32 zone_waiting_for_bootup; bool enter_world_triggered; + bool is_graceful_disconnect; // Track if this was a graceful logout vs network issue EQ::versions::ClientVersion m_ClientVersion; uint32 m_ClientVersionBit; diff --git a/world/cliententry.cpp b/world/cliententry.cpp index fca00bdef..1907f4582 100644 --- a/world/cliententry.cpp +++ b/world/cliententry.cpp @@ -28,7 +28,6 @@ #include "../common/guilds.h" #include "../common/strings.h" -extern uint32 numplayers; extern LoginServerList loginserverlist; extern ZSList zoneserver_list; extern ClientList client_list; @@ -96,7 +95,7 @@ ClientListEntry::ClientListEntry(uint32 in_id, ZoneServer *iZS, ServerClientList ClientListEntry::~ClientListEntry() { if (RunLoops) { - Camp(); // updates zoneserver's numplayers + Camp(); // updates zoneserver population tracking client_list.RemoveCLEReferances(this); } SetOnline(CLE_Status::Offline); @@ -123,12 +122,12 @@ void ClientListEntry::SetOnline(CLE_Status iOnline) ); if (iOnline >= CLE_Status::Online && pOnline < CLE_Status::Online) { - numplayers++; + // Population tracking now handled by queue system } else if (iOnline < CLE_Status::Online && pOnline >= CLE_Status::Online) { - numplayers--; + // Population tracking now handled by queue system } - if (iOnline != CLE_Status::Online || pOnline < CLE_Status::Online) { + if (iOnline != CLE_Status::Online || pOnline < CLE_Status::Online) { pOnline = iOnline; } if (iOnline < CLE_Status::Zoning) { diff --git a/world/clientlist.cpp b/world/clientlist.cpp index ec2f84e68..02802a304 100644 --- a/world/clientlist.cpp +++ b/world/clientlist.cpp @@ -22,6 +22,7 @@ #include "zonelist.h" #include "client.h" #include "worlddb.h" +#include "login_server.h" #include "../common/strings.h" #include "../common/guilds.h" #include "../common/races.h" @@ -35,11 +36,11 @@ #include "../zone/string_ids.h" #include "../common/zone_store.h" #include +#include "world_queue.h" // For queue_manager global extern WebInterfaceList web_interface; extern ZSList zoneserver_list; -uint32 numplayers = 0; //this really wants to be a member variable of ClientList... ClientList::ClientList() : CLStale_timer(10000) @@ -270,6 +271,11 @@ void ClientList::DisconnectByIP(uint32 iIP) { zoneserver_list.SendPacket(pack); safe_delete(pack); } + // TODO: Add kick-to-queue functionality + uint32 account_id = countCLEIPs->AccountID(); + queue_manager.m_account_rez_mgr.RemoveRez(account_id); + LogInfo("Removed account reservation for IP-limited account [{}] - no grace period bypass", account_id); + countCLEIPs->SetOnline(CLE_Status::Offline); iterator.RemoveCurrent(); continue; @@ -445,7 +451,7 @@ void ClientList::SendCLEList(const int16& admin, const char* to, WorldTCPConnect iterator.Advance(); x++; } - fmt::format_to(std::back_inserter(out), "{}{} CLEs in memory. {} CLEs listed. numplayers = {}.", newline, x, y, numplayers); + fmt::format_to(std::back_inserter(out), "{}{} CLEs in memory. {} CLEs listed. server_population = {}.", newline, x, y, GetWorldPop()); out.push_back(0); connection->SendEmoteMessageRaw(to, 0, AccountStatus::Player, Chat::NPCQuestSay, out.data()); } @@ -1377,9 +1383,20 @@ bool ClientList::IsAccountInGame(uint32 iLSID) { return false; } - +// Current only used as a fallback for for QueueManager::EffectivePopulation. Proper usage is to use GetWorldPop() int ClientList::GetClientCount() { - return(numplayers); + int count = 0; + LinkedListIterator iterator(clientlist); + + iterator.Reset(); + while(iterator.MoreElements()) { + if (iterator.GetData()->Online() >= CLE_Status::Zoning) { + count++; + } + iterator.Advance(); + } + + return count; } void ClientList::GetClients(const char *zone_name, std::vector &res) { @@ -1720,3 +1737,18 @@ void ClientList::OnTick(EQ::Timer* t) web_interface.SendEvent(out); } +std::string ClientList::GetClientKeyByLSID(uint32 iLSID) { + LinkedListIterator iterator(clientlist); + iterator.Reset(); + + while (iterator.MoreElements()) { + ClientListEntry* entry = iterator.GetData(); + if (entry && entry->LSID() == iLSID) { + return entry->GetLSKey(); + } + iterator.Advance(); + } + + return ""; // Return empty string if not found +} + diff --git a/world/clientlist.h b/world/clientlist.h index a9df1996c..aadc27ed6 100644 --- a/world/clientlist.h +++ b/world/clientlist.h @@ -9,8 +9,10 @@ #include "../common/servertalk.h" #include "../common/event/timer.h" #include "../common/net/console_server_connection.h" +#include "world_queue.h" // For server population management #include #include +#include class Client; class ZoneServer; @@ -72,9 +74,10 @@ class ClientList { bool ActiveConnection(uint32 iAccID); bool ActiveConnection(uint32 iAccID, uint32 iCharID); bool IsAccountInGame(uint32 iLSID); + std::string GetClientKeyByLSID(uint32 iLSID); // Get client key for queue authorization matching - - int GetClientCount(); + int GetClientCount(); // Fallback population counter for startup + void GetClients(const char *zone_name, std::vector &into); bool WhoAllFilter(ClientListEntry* client, Who_All_Struct* whom, int16 admin, int whomlen); diff --git a/world/login_server.cpp b/world/login_server.cpp index 6cff271fe..e3d3994ec 100644 --- a/world/login_server.cpp +++ b/world/login_server.cpp @@ -28,6 +28,7 @@ #include "../common/packet_dump.h" #include "../common/strings.h" #include "../common/eqemu_logsys.h" +#include "../common/queue_packets.h" // For ServerOP_RemoveFromQueue and ServerQueueRemoval_Struct #include "login_server.h" #include "login_server_list.h" #include "zoneserver.h" @@ -36,12 +37,16 @@ #include "clientlist.h" #include "cliententry.h" #include "world_config.h" +#include "world_queue.h" // For queue_manager and QueueDebugLog extern ZSList zoneserver_list; extern ClientList client_list; -extern uint32 numzones; -extern uint32 numplayers; extern volatile bool RunLoops; +extern std::mutex ipMutex; +extern std::unordered_set ipWhitelist; + +// Global pointer for other files to access the primary LoginServer instance +LoginServer* loginserver = nullptr; LoginServer::LoginServer(const char* iAddress, uint16 iPort, const char* Account, const char* Password, uint8 Type) { @@ -51,11 +56,20 @@ LoginServer::LoginServer(const char* iAddress, uint16 iPort, const char* Account m_login_password = Password; m_can_account_update = false; m_is_legacy = Type == 1; + + // Set global pointer to first LoginServer instance for other files to access + if (!loginserver) { + loginserver = this; + } + Connect(); } LoginServer::~LoginServer() { - + // Clear global pointer if it was pointing to this instance + if (loginserver == this) { + loginserver = nullptr; + } } void LoginServer::ProcessUsertoWorldReq(uint16_t opcode, EQ::Net::Packet& p) @@ -92,7 +106,7 @@ void LoginServer::ProcessUsertoWorldReq(uint16_t opcode, EQ::Net::Packet& p) } int32 x = Config->MaxClients; - if ((int32)numplayers >= x && x != -1 && x != 255 && status < 80) + if ((int32)client_list.GetClientCount() >= x && x != -1 && x != 255 && status < 80) utwrs->response = -3; if (status == -1) @@ -168,6 +182,35 @@ void LoginServer::ProcessLSAccountUpdate(uint16_t opcode, EQ::Net::Packet& p) { m_can_account_update = true; } +void LoginServer::ProcessQueueRemoval(uint16_t opcode, EQ::Net::Packet& p) { + LogNetcode("Received ServerPacket from LS OpCode {:#04x}", opcode); + + if (p.Length() < sizeof(ServerQueueRemoval_Struct)) { + LogError("Invalid ServerOP_RemoveFromQueue packet size. Expected: {}, Got: {}", + sizeof(ServerQueueRemoval_Struct), p.Length()); + return; + } + + auto removal = (ServerQueueRemoval_Struct*)p.Data(); + if (removal->ls_account_id == 0) { + LogError("Invalid ls_account_id (0) in ServerOP_RemoveFromQueue packet"); + return; + } + + // Convert LS account ID to world account ID + uint32 world_account_id = database.GetAccountIDFromLSID(removal->ls_account_id); + if (world_account_id == 0) { + QueueDebugLog(1, "No world account found for LS account {}", removal->ls_account_id); + return; + } + + QueueDebugLog(1, "Processing queue removal for LS account {} (world account {})", + removal->ls_account_id, world_account_id); + + // Remove from queue using the global queue_manager + queue_manager.RemoveFromQueue(world_account_id); +} + bool LoginServer::Connect() { char errbuf[1024]; @@ -219,6 +262,7 @@ bool LoginServer::Connect() { m_legacy_client->OnMessage(ServerOP_SystemwideMessage, std::bind(&LoginServer::ProcessSystemwideMessage, this, std::placeholders::_1, std::placeholders::_2)); m_legacy_client->OnMessage(ServerOP_LSRemoteAddr, std::bind(&LoginServer::ProcessLSRemoteAddr, this, std::placeholders::_1, std::placeholders::_2)); m_legacy_client->OnMessage(ServerOP_LSAccountUpdate, std::bind(&LoginServer::ProcessLSAccountUpdate, this, std::placeholders::_1, std::placeholders::_2)); + m_legacy_client->OnMessage(ServerOP_RemoveFromQueue, std::bind(&LoginServer::ProcessQueueRemoval, this, std::placeholders::_1, std::placeholders::_2)); } else { m_client.reset(new EQ::Net::ServertalkClient(m_loginserver_address, m_loginserver_port, false, "World", "")); @@ -244,6 +288,7 @@ bool LoginServer::Connect() { m_client->OnMessage(ServerOP_SystemwideMessage, std::bind(&LoginServer::ProcessSystemwideMessage, this, std::placeholders::_1, std::placeholders::_2)); m_client->OnMessage(ServerOP_LSRemoteAddr, std::bind(&LoginServer::ProcessLSRemoteAddr, this, std::placeholders::_1, std::placeholders::_2)); m_client->OnMessage(ServerOP_LSAccountUpdate, std::bind(&LoginServer::ProcessLSAccountUpdate, this, std::placeholders::_1, std::placeholders::_2)); + m_client->OnMessage(ServerOP_RemoveFromQueue, std::bind(&LoginServer::ProcessQueueRemoval, this, std::placeholders::_1, std::placeholders::_2)); } return true; } @@ -309,13 +354,13 @@ void LoginServer::SendStatus() { if (WorldConfig::get()->Locked) lss->status = -2; - else if (numzones <= 0) + else if (zoneserver_list.GetZoneCount() <= 0) lss->status = -1; else - lss->status = numplayers > 0 ? numplayers : 0; + lss->status = client_list.GetClientCount() > 0 ? client_list.GetClientCount() : 0; - lss->num_zones = numzones; - lss->num_players = numplayers; + lss->num_zones = zoneserver_list.GetZoneCount(); + lss->num_players = client_list.GetClientCount(); SendPacket(pack); delete pack; } diff --git a/world/login_server.h b/world/login_server.h index e879e15ae..1132dd330 100644 --- a/world/login_server.h +++ b/world/login_server.h @@ -27,7 +27,9 @@ #include "../common/net/servertalk_client_connection.h" #include "../common/net/servertalk_legacy_client_connection.h" #include "../common/event/timer.h" +// #include "account_reservation_manager.h" // Moved to QueueManager #include +#include class LoginServer { public: @@ -57,7 +59,7 @@ class LoginServer { return false; } bool CanUpdate() { return m_can_account_update; } - + private: void ProcessUsertoWorldReq(uint16_t opcode, EQ::Net::Packet& p); void ProcessLSClientAuth(uint16_t opcode, EQ::Net::Packet& p); @@ -65,6 +67,14 @@ class LoginServer { void ProcessSystemwideMessage(uint16_t opcode, EQ::Net::Packet& p); void ProcessLSRemoteAddr(uint16_t opcode, EQ::Net::Packet& p); void ProcessLSAccountUpdate(uint16_t opcode, EQ::Net::Packet& p); + void ProcessQueuePositionQuery(uint16_t opcode, EQ::Net::Packet& p); + void ProcessQueueBatchRemoval(uint16_t opcode, EQ::Net::Packet& p); + void ProcessQueueRemoval(uint16_t opcode, EQ::Net::Packet& p); + + /** + * Smart wait time calculation based on queue dynamics and server capacity + */ + uint32 CalculateSmartWaitTime(uint32 queue_position, uint32 current_population, uint32 max_capacity) const; std::unique_ptr m_client; std::unique_ptr m_legacy_client; @@ -75,6 +85,22 @@ class LoginServer { std::string m_login_account; std::string m_login_password; bool m_can_account_update; - bool m_is_legacy; + uint8 m_is_legacy; + + // Centralized account-based reservation tracking // Removed + // AccountRezMgr m_account_rez_mgr; // Removed + + // Queue authorization tracking + struct QueueAuthorization { + uint32 account_id; + uint32 timestamp; + uint32 timeout_seconds; + QueueAuthorization(uint32 id, uint32 ts, uint32 timeout) + : account_id(id), timestamp(ts), timeout_seconds(timeout) {} + bool IsExpired() const { + return (time(nullptr) - timestamp) > timeout_seconds; + } + }; + std::vector m_authorized_accounts; }; -#endif +#endif \ No newline at end of file diff --git a/world/login_server_list.cpp b/world/login_server_list.cpp index 24bf6ff65..bc37c1c19 100644 --- a/world/login_server_list.cpp +++ b/world/login_server_list.cpp @@ -36,12 +36,8 @@ #include "clientlist.h" #include "world_config.h" -extern ZSList zoneserver_list; -extern LoginServerList loginserverlist; -extern ClientList client_list; -extern uint32 numzones; -extern uint32 numplayers; -extern volatile bool RunLoops; +extern ZSList zoneserver_list; +extern ClientList client_list; LoginServerList::LoginServerList() { } diff --git a/world/main.cpp b/world/main.cpp index 81a1060db..6148e6e94 100644 --- a/world/main.cpp +++ b/world/main.cpp @@ -60,6 +60,7 @@ #include #include +#include // For mkdir #endif @@ -85,6 +86,7 @@ #include "../common/events/player_event_logs.h" #include "../common/skill_caps.h" #include "../common/ip_util.h" +#include "world_queue.h" SkillCaps skill_caps; ZoneStore zone_store; @@ -108,6 +110,8 @@ WebInterfaceList web_interface; WorldContentService content_service; PathManager path; PlayerEventLogs player_event_logs; +QueueManager queue_manager; +bool database_ready = false; void CatchSignal(int sig_num); @@ -170,6 +174,8 @@ int main(int argc, char** argv) { Timer player_event_log_process(1000); player_event_log_process.Start(1000); + Timer QueueManagerTimer(3000); + QueueManagerTimer.Start(); // global loads LogInfo("Loading launcher list.."); @@ -466,7 +472,7 @@ int main(int argc, char** argv) { std::string window_title = fmt::format( "World [{}] Clients [{}]", Config->LongName, - client_list.GetClientCount() + GetWorldPop() ); UpdateWindowTitle(window_title); diff --git a/world/world_queue.cpp b/world/world_queue.cpp new file mode 100644 index 000000000..1e6540fd5 --- /dev/null +++ b/world/world_queue.cpp @@ -0,0 +1,983 @@ +#include "world_queue.h" +#include "worlddb.h" // For WorldDatabase +#include "login_server.h" // For LoginServer global +#include "world_config.h" // For WorldConfig +#include "clientlist.h" // For ClientList +#include "../common/eqemu_logsys.h" +#include "../common/servertalk.h" +#include "../common/queue_packets.h" // Queue-specific opcodes and structures +#include "../common/rulesys.h" // For RuleB and RuleI macros +#include "../common/ip_util.h" // For IpUtil::IsIpInPrivateRfc1918 +#include +#include + +extern LoginServer* loginserver; +extern WorldDatabase database; +extern ClientList client_list; +extern bool database_ready; // Add extern declaration for database_ready flag + +struct QueuedClient { + uint32 w_accountid; + uint32 queue_position; + uint32 estimated_wait; + uint32 ip_address; + uint32 queued_timestamp; + uint32 last_updated; + uint32 last_seen; + + uint32 ls_account_id; + uint32 from_id; + std::string ip_str; + std::string forum_name; + + std::string authorized_client_key; + + QueuedClient() : w_accountid(0), queue_position(0), estimated_wait(0), ip_address(0), + queued_timestamp(0), last_updated(0), last_seen(0), ls_account_id(0), + from_id(0) {} + + QueuedClient(uint32 world_acct_id, uint32 pos, uint32 wait, uint32 ip = 0, + uint32 ls_acct_id = 0, uint32 f_id = 0, + const std::string& ip_string = "", const std::string& forum = "", + const std::string& auth_key = "") + : w_accountid(world_acct_id), queue_position(pos), estimated_wait(wait), ip_address(ip), + queued_timestamp(time(nullptr)), last_updated(time(nullptr)), last_seen(time(nullptr)), + ls_account_id(ls_acct_id), from_id(f_id), + ip_str(ip_string), forum_name(forum), + authorized_client_key(auth_key) {} +}; + +struct QueueNotification { + uint32 ls_account_id; + uint32 ip_address; + + QueueNotification(uint32 ls_id, uint32 ip) : ls_account_id(ls_id), ip_address(ip) {} +}; + +QueueManager::QueueManager() + : m_queue_paused(false), m_cached_test_offset(0), m_world_server_id(1) +{ +} + +QueueManager::~QueueManager() +{ + QueueDebugLog(1, "QueueManager destroyed."); +} +uint32 QueueManager::EffectivePopulation() +{ +// TODO: Add bypass logic for trader + GM accounts + + uint32 account_reservations = m_account_rez_mgr.Total(); + uint32 test_offset = m_cached_test_offset; + uint32 effective_population = account_reservations + test_offset; + + QueueDebugLog(2, "Account reservations: {}, test offset: {}, effective total: {}", + account_reservations, test_offset, effective_population); + + return effective_population; +} + +void QueueManager::AddToQueue(uint32 world_account_id, uint32 position, uint32 estimated_wait, uint32 ip_address, + uint32 ls_account_id, uint32 from_id, + const char* ip_str, const char* forum_name, const char* client_key) +{ + in_addr addr; + addr.s_addr = ip_address; + std::string ip_str_log = inet_ntoa(addr); + + // Calculate position if not provided (0 = auto-calculate) + if (position == 0) { + position = m_queued_clients.size() + 1; + } + + // TODO: Proper average calcuation here + uint32 wait_per_player = 60; + estimated_wait = position * wait_per_player; + + QueuedClient new_entry(world_account_id, position, estimated_wait, ip_address, + ls_account_id, from_id, + ip_str ? ip_str : ip_str_log, + forum_name ? forum_name : "", + client_key ? client_key : ""); + + m_queued_clients.push_back(new_entry); + + SaveQueueDBEntry(world_account_id, position, estimated_wait, ip_address); + LogQueueAction("ADD_TO_QUEUE", world_account_id, + fmt::format("pos={} wait={}s ip={} ls_id={} (memory + DB)", position, estimated_wait, ip_str_log, ls_account_id)); + + SendQueuedClientsUpdate(); + +} + +void QueueManager::RemoveFromQueue(const std::vector& account_ids) +{ + if (account_ids.empty()) { + return; + } + + std::vector removed_accounts; + std::vector notification_data; // Clear struct instead of pair + + for (uint32 account_id : account_ids) { + auto it = std::find_if(m_queued_clients.begin(), m_queued_clients.end(), + [account_id](const QueuedClient& qclient) { + return qclient.w_accountid == account_id; + }); + + if (it == m_queued_clients.end()) { + uint32 world_account_id = GetWorldAccountFromLS(account_id); + if (world_account_id != account_id) { + it = std::find_if(m_queued_clients.begin(), m_queued_clients.end(), + [world_account_id](const QueuedClient& qclient) { + return qclient.w_accountid == world_account_id; + }); + } + } + + if (it != m_queued_clients.end()) { + in_addr addr; + addr.s_addr = it->ip_address; + std::string ip_str = inet_ntoa(addr); + + uint32 account_id_to_remove = it->w_accountid; + uint32 ls_account_id_to_notify = it->ls_account_id; + uint32 ip_address_to_notify = it->ip_address; + + removed_accounts.push_back(account_id_to_remove); + notification_data.emplace_back(ls_account_id_to_notify, ip_address_to_notify); + + m_queued_clients.erase(it); + + LogQueueAction("REMOVE", account_id_to_remove, + fmt::format("IP: {} (batch removal)", ip_str)); + } + } + + if (removed_accounts.empty()) { + LogDebug("RemoveFromQueue: No valid accounts found to remove from queue"); + return; + } + + for (uint32 account_id : removed_accounts) { + RemoveQueueDBEntry(account_id); + } + + if (loginserver && loginserver->Connected()) { + uint32 notifications_sent = 0; + for (const auto& notification : notification_data) { + uint32 ls_account_id = notification.ls_account_id; + uint32 ip_address = notification.ip_address; + + if (ls_account_id > 0) { + SendQueueRemoval(ls_account_id); + notifications_sent++; + } + } + + if (notifications_sent > 0) { + QueueDebugLog(1, "Sent [{}] queue removal notifications to disconnected players", notifications_sent); + } + } + + if (!m_queued_clients.empty()) { + SendQueuedClientsUpdate(); // Sends updated queue positions to each player + QueueDebugLog(1, "Removed [{}] players from queue, sent position updates to [{}] remaining clients", + removed_accounts.size(), m_queued_clients.size()); + } else { + QueueDebugLog(1, "Removed [{}] players from queue - queue is now empty", removed_accounts.size()); + } +} +void QueueManager::UpdateQueuePositions() +{ + if (m_queued_clients.empty()) { + return; + } + + // Don't update queue positions if server is down/locked + if (m_queue_paused) { + QueueDebugLog(2, "Queue updates paused due to server status - [{}] players remain queued", m_queued_clients.size()); + return; + } + + // Check if queue is manually frozen via rule + if (RuleB(AlKabor, FreezeQueue)) { + QueueDebugLog(2, "Queue updates frozen by rule - [{}] players remain queued with frozen positions", m_queued_clients.size()); + return; + } + + // Get server capacity for capacity decisions + uint32 max_capacity = RuleI(AlKabor, PlayerPopulationCap); + uint32 current_population = EffectivePopulation(); + + // Calculate available slots for auto-connects + uint32 available_slots = (current_population < max_capacity) ? (max_capacity - current_population) : 0; + uint32 auto_connects_initiated = 0; + + // Store accounts to remove after auto-connect (to avoid modifying vector during iteration) + std::vector accounts_to_remove; + + // Process players at front of queue for auto-connect + // Use normal iteration since we'll remove accounts after the loop + for (int i = 0; i < static_cast(m_queued_clients.size()); ++i) { + QueuedClient& qclient = m_queued_clients[i]; + uint32 current_position = i + 1; // Position is index + 1 + + // Only process players at position 1 (front of queue) + if (current_position == 1 && auto_connects_initiated < available_slots) { + in_addr addr; + addr.s_addr = qclient.ip_address; + std::string ip_str = inet_ntoa(addr); + + LogQueueAction("FRONT_OF_QUEUE", qclient.w_accountid, + fmt::format("IP: {} reached position [{}] - auto-connecting with grace whitelist (total: {}/{}, slot {}/{})", + ip_str, current_position, current_population, max_capacity, auto_connects_initiated + 1, available_slots)); + + // Send auto-connect request to login server + if (qclient.ls_account_id > 0) { + AutoConnectQueuedPlayer(qclient); + // Mark for removal from queue after auto-connect + accounts_to_remove.push_back(qclient.w_accountid); + } + + auto_connects_initiated++; + } else if (current_position == 1) { + // Server at capacity - log but don't auto-connect + in_addr addr; + addr.s_addr = qclient.ip_address; + std::string ip_str = inet_ntoa(addr); + + LogQueueAction("WAITING_CAPACITY", qclient.w_accountid, + fmt::format("IP: {} at position [{}] waiting for capacity (total: {}/{}, used slots: {}/{})", + ip_str, current_position, current_population, max_capacity, auto_connects_initiated, available_slots)); + } + } + + for (uint32 account_id : accounts_to_remove) { + RemoveFromQueue(account_id); + QueueDebugLog(1, "Removed account [{}] from queue after auto-connect", account_id); + } + + if (auto_connects_initiated > 0) { + QueueDebugLog(1, "Auto-connected [{}] players from queue to grace whitelist - [{}] players remain queued", + auto_connects_initiated, m_queued_clients.size()); + + SendQueuedClientsUpdate(); + } +} + +bool QueueManager::EvaluateConnectionRequest(const ConnectionRequest& request, uint32 max_capacity, + UsertoWorldResponse* response, Client* client) +{ + QueueDecisionOutcome decision = QueueDecisionOutcome::QueuePlayer; // Defaults to queueing + + // 1. Auto-connects always bypass (shouldn't get -6, but just in case) + if (request.is_auto_connect) { + QueueDebugLog(1, "QueueManager - AUTO_CONNECT: Account [{}] bypassing queue evaluation", + request.account_id); + decision = QueueDecisionOutcome::AutoConnect; + } + // 2. Check if player is already queued (queue toggle behavior) + else if (IsAccountQueued(request.account_id)) { + uint32 queue_position = GetQueuePosition(request.account_id); + QueueDebugLog(1, "QueueManager - QUEUE_TOGGLE: Account [{}] already queued at position [{}] - toggling off", + request.account_id, queue_position); + decision = QueueDecisionOutcome::QueueToggle; + } + // 3. Check GM bypass rules - this is where we override world server's decision + else if (RuleB(AlKabor, QueueBypassGMLevel) && request.status >= 80) { + QueueDebugLog(1, "QueueManager - GM_BYPASS: Account [{}] (status: {}) overriding world server capacity decision", + request.account_id, request.status); + decision = QueueDecisionOutcome::GMBypass; + } + // 4. Check grace period whitelist (disconnected players still within grace period) + else if (m_account_rez_mgr.IsAccountInGraceWhitelist(request.account_id)) { + QueueDebugLog(1, "QueueManager - GRACE_BYPASS: Account [{}] in grace period whitelist - overriding capacity check", + request.account_id); + + decision = QueueDecisionOutcome::GraceBypass; + } + // 5. Default case - no bypass conditions met, queue the player + else { + QueueDebugLog(1, "QueueManager - QUEUE_PLAYER: Account [{}] at capacity with no bypass conditions - adding to queue", + request.account_id); + + decision = QueueDecisionOutcome::QueuePlayer; + } + switch (decision) { + case QueueDecisionOutcome::AutoConnect: + case QueueDecisionOutcome::GMBypass: + return true; + + case QueueDecisionOutcome::GraceBypass: + // Extend grace period for players who already have reservations but are looping char select/server select + m_account_rez_mgr.IncreaseGraceDuration(request.account_id, 30); + return true; + + case QueueDecisionOutcome::QueueToggle: + RemoveFromQueue(request.account_id); + QueueDebugLog(1, "QUEUE TOGGLE: Player clicked PLAY while queued - removed account [{}] from server", request.account_id); + if (response) { + response->response = -7; // Queue toggle response code for login server + } + return false; + + case QueueDecisionOutcome::QueuePlayer: + // Add to queue for this server + { + // Use the client key from the login server request (passed via forum_name field) + std::string client_key = request.forum_name ? request.forum_name : ""; + + AddToQueue( + request.world_account_id, // world_account_id (primary key) + 0, // position (0 = auto-calculate) + 0, // estimated_wait (auto-calculated) + request.ip_address, // ip_address + request.ls_account_id, // ls_account_id + response ? response->FromID : 0, // from_id + request.ip_str, // ip_str + request.forum_name, // forum_name + client_key.c_str() // authorized_client_key (use forum_name as client_key) + ); + + uint32 queue_position = m_queued_clients.size(); // Position just added + uint32 estimated_wait = queue_position * 60; // TODO: Calcualte avg wait + + QueueDebugLog(1, "Added account [{}] to queue at position [{}] with estimated wait [{}] seconds (client key: {})", + request.world_account_id, queue_position, estimated_wait, + client_key.empty() ? "NONE" : "present"); + + if (response) { + response->response = -6; // Queue response code for login server + } + return false; // Don't override -6, player should remain queued + } + } + + // Should never reach here, but default to not overriding + return false; +} + +bool QueueManager::IsAccountQueued(uint32 account_id) const +{ + // First check if it's a direct world account ID lookup + auto it = std::find_if(m_queued_clients.begin(), m_queued_clients.end(), + [account_id](const QueuedClient& qclient) { + return qclient.w_accountid == account_id; + }); + if (it != m_queued_clients.end()) { + return true; + } + + // If not found, check if it's an LS account ID that needs mapping using encapsulated method + uint32 world_account_id = GetWorldAccountFromLS(account_id); + if (world_account_id != account_id) { + auto world_it = std::find_if(m_queued_clients.begin(), m_queued_clients.end(), + [world_account_id](const QueuedClient& qclient) { + return qclient.w_accountid == world_account_id; + }); + return world_it != m_queued_clients.end(); + } + + return false; +} + + +uint32 QueueManager::GetQueuePosition(uint32 account_id) const +{ + // First check if it's a direct world account ID lookup + auto it = std::find_if(m_queued_clients.begin(), m_queued_clients.end(), + [account_id](const QueuedClient& qclient) { + return qclient.w_accountid == account_id; + }); + if (it != m_queued_clients.end()) { + return std::distance(m_queued_clients.begin(), it) + 1; // Position = index + 1 + } + + // If not found, check if it's an LS account ID that needs mapping using encapsulated method + uint32 world_account_id = GetWorldAccountFromLS(account_id); + if (world_account_id != account_id) { // Only search if we got a different mapping + auto world_it = std::find_if(m_queued_clients.begin(), m_queued_clients.end(), + [world_account_id](const QueuedClient& qclient) { + return qclient.w_accountid == world_account_id; + }); + if (world_it != m_queued_clients.end()) { + return std::distance(m_queued_clients.begin(), world_it) + 1; // Position = index + 1 + } + } + + return 0; +} + +uint32 QueueManager::GetTotalQueueSize() const +{ + return static_cast(m_queued_clients.size()); +} +void QueueManager::CheckForExternalChanges() // Handles test offset changes and queue refresh flags +{ + static bool first_run = true; + static const std::string test_offset_query = "SELECT rule_value FROM rule_values WHERE rule_name = 'Quarm:TestPopulationOffset' LIMIT 1"; + uint32 current_test_offset = QuerySingleUint32(test_offset_query, 0); + + QueueDebugLog(1, "Current test_offset: {}, cached_test_offset: {}", current_test_offset, m_cached_test_offset); + + if (first_run || m_cached_test_offset != current_test_offset) { + if (!first_run) { + QueueDebugLog(2, "Test offset changed from [{}] to [{}] - pushing server list updates to all login clients", + m_cached_test_offset, current_test_offset); + } + + m_cached_test_offset = current_test_offset; + + if (!first_run) { + // Get current effective population using the standard method + uint32 effective_population = EffectivePopulation(); + + // Send server list update to all connected login server clients + if (loginserver && loginserver->Connected()) { + QueueDebugLog(1, "Login server is connected - sending ServerOP_WorldListUpdate packet"); + + SendWorldListUpdate(effective_population); + QueueDebugLog(2, "Sent ServerOP_WorldListUpdate to login server for test offset change with population: {}", effective_population); + } else { + QueueDebugLog(1, "Login server NOT connected - cannot send update packet"); + } + } + + first_run = false; + } + + static const std::string refresh_queue_query = + "SELECT value FROM tblloginserversettings WHERE type = 'RefreshQueue' ORDER BY value DESC LIMIT 1"; + static const std::string reset_queue_flag_query = + "UPDATE tblloginserversettings SET value = '0' WHERE type = 'RefreshQueue'"; + + auto results = database.QueryDatabase(refresh_queue_query); + if (results.Success() && results.RowCount() > 0) { + auto row = results.begin(); + std::string flag_value = row[0] ? row[0] : "0"; + + if (flag_value != "0") { + bool should_refresh = (flag_value == "1" || flag_value == "true"); + + if (should_refresh) { + QueueDebugLog(1, "Queue refresh flag detected - refreshing queue from database"); + RestoreQueueFromDatabase(); + QueueDebugLog(1, "Queue refreshed from database - clients updated"); + } else { + QueueDebugLog(2, "RefreshQueue flag value = '{}' - resetting to 0", flag_value); + } + + auto reset_result = database.QueryDatabase(reset_queue_flag_query); + + if (reset_result.Success()) { + QueueDebugLog(2, "RefreshQueue flag reset to 0"); + } else { + LogError("Failed to reset RefreshQueue flag: {}", reset_result.ErrorMessage()); + } + } + } else { + if (!results.Success()) { + LogError("CheckForExternalChanges: Query failed - {}", results.ErrorMessage()); + } else { + LogDebug("CheckForExternalChanges: No RefreshQueue flag found in database"); + } + } +} + +void QueueManager::ClearAllQueues() // Unused for now +{ + if (!m_queued_clients.empty()) { + uint32 count = GetTotalQueueSize(); + QueueDebugLog(1, "Clearing all queue entries for world server - removing [{}] players", count); + + // 1. Clear memory + m_queued_clients.clear(); + + // 2. Clear database immediately (event-driven) + auto clear_query = fmt::format("DELETE FROM tblLoginQueue WHERE world_server_id = {}", m_world_server_id); + auto result = database.QueryDatabase(clear_query); + + if (result.Success()) { + QueueDebugLog(1, "Queue cleared - [{}] players removed (memory + DB)", count); + } else { + LogError("Queue cleared from memory but failed to clear database: {}", result.ErrorMessage()); + } + } +} + +uint32 QueueManager::GetWorldAccountFromLS(uint32 ls_account_id) const +{ + uint32 world_account_id = database.GetAccountIDFromLSID(ls_account_id); + if (world_account_id == 0) { + // For new accounts that don't have world accounts yet, use LS account ID as fallback + LogDebug("No world account mapping for LS account [{}] - using LS account ID as fallback", ls_account_id); + return ls_account_id; + } + return world_account_id; +} + +uint32 QueueManager::GetLSAccountFromWorld(uint32 world_account_id) const +{ + auto query = fmt::format("SELECT lsaccount_id FROM account WHERE id = {} LIMIT 1", world_account_id); + uint32 ls_account_id = QuerySingleUint32(query, 0); + + if (ls_account_id > 0) { + return ls_account_id; + } + + // Fallback: assume world_account_id is actually the LS account ID for new accounts + LogDebug("No LS account mapping for world account [{}] - using world account ID as fallback", world_account_id); + return world_account_id; +} + +bool QueueManager::QueryDB(const std::string& query, const std::string& operation_desc) const +{ + // Safety check: ensure database is ready before accessing it + if (!database_ready) { + QueueDebugLog(2, "Database not ready - skipping operation: {}", operation_desc); + return false; + } + + auto results = database.QueryDatabase(query); + if (!results.Success()) { + LogError("Failed to {}: {}", operation_desc, results.ErrorMessage()); + return false; + } + return true; +} + +uint32 QueueManager::QuerySingleUint32(const std::string& query, uint32 default_value) const +{ + if (!ValidateDatabaseReady()) { + return default_value; + } + + auto results = database.QueryDatabase(query); + if (results.Success() && results.RowCount() > 0) { + auto row = results.begin(); + if (row[0]) { + try { + return static_cast(std::stoul(row[0])); + } catch (const std::exception&) { + LogError("Failed to parse uint32 from query result: {}", row[0]); + } + } + } + return default_value; +} + +void QueueManager::LogQueueAction(const std::string& action, uint32 account_id, const std::string& details) const +{ + if (details.empty()) { + QueueDebugLog(1, "QueueManager - {}: Account [{}]", action, account_id); + } else { + QueueDebugLog(1, "QueueManager - {}: Account [{}] - {}", action, account_id, details); + } +} + +void QueueManager::SendQueuedClientsUpdate() const { + QueueDebugLog(1, "SendQueuedClientsUpdate called - checking queue state..."); + + if (m_queued_clients.empty()) { + QueueDebugLog(1, "No queued players to update"); + return; + } + + if (!loginserver || !loginserver->Connected()) { + LogError("Login server not available - cannot send queue updates"); + return; + } + + QueueDebugLog(1, "Found [{}] queued players, login server available - sending batch update", m_queued_clients.size()); + + std::vector updates; + updates.reserve(m_queued_clients.size()); + + uint32 valid_updates = 0; + uint32 skipped_invalid = 0; + + // Collect all valid queue updates with dynamic position calculation + for (size_t i = 0; i < m_queued_clients.size(); ++i) { + const QueuedClient& qclient = m_queued_clients[i]; + uint32 dynamic_position = i + 1; // Position = index + 1 + + // Create update entry + ServerQueueDirectUpdate_Struct update = {}; + update.ls_account_id = qclient.ls_account_id; + update.queue_position = dynamic_position; // Use calculated position + update.estimated_wait = dynamic_position * 60; // Use calculated wait time + + updates.push_back(update); + valid_updates++; + + QueueDebugLog(1, "Added to batch: account [{}] (LS: {}) position [{}] wait [{}]s", + qclient.w_accountid, qclient.ls_account_id, dynamic_position, update.estimated_wait); + } + + if (valid_updates == 0) { + QueueDebugLog(1, "No valid queue updates to send"); + return; + } + + // Create batch packet: header + array of updates + size_t packet_size = sizeof(ServerQueueBatchUpdate_Struct) + (valid_updates * sizeof(ServerQueueDirectUpdate_Struct)); + auto batch_pack = new ServerPacket(ServerOP_QueueBatchUpdate, packet_size); + + // Set update count + ServerQueueBatchUpdate_Struct* batch_header = (ServerQueueBatchUpdate_Struct*)batch_pack->pBuffer; + batch_header->update_count = valid_updates; + + // Copy update array after header + ServerQueueDirectUpdate_Struct* update_array = (ServerQueueDirectUpdate_Struct*)(batch_pack->pBuffer + sizeof(ServerQueueBatchUpdate_Struct)); + memcpy(update_array, updates.data(), valid_updates * sizeof(ServerQueueDirectUpdate_Struct)); + + QueueDebugLog(1, "Created batch packet - opcode: 0x{:X}, size: {}, update count: {}", + ServerOP_QueueBatchUpdate, packet_size, valid_updates); + + // Send single batch packet to login server + loginserver->SendPacket(batch_pack); + delete batch_pack; + + QueueDebugLog(1, "Sent batch queue update with [{}] player updates to login server (skipped: {})", + valid_updates, skipped_invalid); + + QueueDebugLog(1, "SendQueuedClientsUpdate completed - batch sent: {}, skipped: {}, total queued: {}", + valid_updates, skipped_invalid, m_queued_clients.size()); +} + +void QueueManager::AutoConnectQueuedPlayer(const QueuedClient& qclient) +{ + if (!loginserver || !loginserver->Connected()) { + LogError("Cannot auto-connect queued player - login server not available or not connected"); + return; + } + + QueueDebugLog(1, "AUTO-CONNECT: Sending auto-connect trigger for LS account [{}]", qclient.ls_account_id); + + // Add to grace whitelist before sending auto-connect trigger - direct object access + QueueDebugLog(1, "AUTO-CONNECT: Added account [{}] to grace whitelist for population cap bypass", qclient.w_accountid); + m_account_rez_mgr.AddRez(qclient.w_accountid, qclient.ip_address, 30); + + // Send auto-connect packet to login server + SendQueueAutoConnect(qclient); + QueueDebugLog(1, "AUTO-CONNECT: Sent ServerOP_QueueAutoConnect to loginserver for account [{}]", qclient.ls_account_id); +} + +void QueueManager::SendQueueAutoConnect(const QueuedClient& qclient) +{ + if (!ValidateLoginServerConnection(ServerOP_QueueAutoConnect)) { + return; + } + + // Convert IP to string if needed + in_addr addr; + addr.s_addr = qclient.ip_address; + char ip_str_buf[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &addr, ip_str_buf, INET_ADDRSTRLEN); + + // Send auto-connect signal to login server to trigger the client connection + auto autoconnect_pack = new ServerPacket(ServerOP_QueueAutoConnect, sizeof(ServerQueueAutoConnect_Struct)); + ServerQueueAutoConnect_Struct* sqac = (ServerQueueAutoConnect_Struct*)autoconnect_pack->pBuffer; + + sqac->loginserver_account_id = qclient.ls_account_id; + sqac->world_id = 0; // Not used + sqac->from_id = qclient.from_id; + sqac->to_id = 0; // Not used + sqac->ip_address = qclient.ip_address; + + strncpy(sqac->ip_addr_str, ip_str_buf, sizeof(sqac->ip_addr_str) - 1); + sqac->ip_addr_str[sizeof(sqac->ip_addr_str) - 1] = '\0'; + + strncpy(sqac->forum_name, qclient.forum_name.c_str(), sizeof(sqac->forum_name) - 1); + sqac->forum_name[sizeof(sqac->forum_name) - 1] = '\0'; + + // CRITICAL: Include the authorized client key for specific client targeting + strncpy(sqac->client_key, qclient.authorized_client_key.c_str(), sizeof(sqac->client_key) - 1); + sqac->client_key[sizeof(sqac->client_key) - 1] = '\0'; + + loginserver->SendPacket(autoconnect_pack); + delete autoconnect_pack; + + QueueDebugLog(2, "Sent ServerOP_QueueAutoConnect for LS account {} with client key", qclient.ls_account_id); +} +// DATABASE QUEUE OPERATIONS +bool QueueManager::ValidateDatabaseReady() const +{ + if (!database_ready) { + QueueDebugLog(1, "Database not ready - skipping operation"); + return false; + } + return true; +} +void QueueManager::SaveQueueDBEntry(uint32 account_id, uint32 queue_position, uint32 estimated_wait, uint32 ip_address) +{ + if (!ValidateDatabaseReady()) { + return; + } + + auto query = fmt::format( + "REPLACE INTO tblLoginQueue (account_id, world_server_id, queue_position, estimated_wait, ip_address) " + "VALUES ({}, {}, {}, {}, {})", + account_id, m_world_server_id, queue_position, estimated_wait, ip_address + ); + + QueryDB(query, fmt::format("save queue entry for account {} on world server {}", account_id, m_world_server_id)); +} + +void QueueManager::RemoveQueueDBEntry(uint32 account_id) +{ + if (!ValidateDatabaseReady()) { + return; + } + + auto query = fmt::format( + "DELETE FROM tblLoginQueue WHERE account_id = {} AND world_server_id = {}", + account_id, m_world_server_id + ); + + QueryDB(query, fmt::format("remove queue entry for account {} on world server {}", account_id, m_world_server_id)); +} + +bool QueueManager::LoadQueueEntries(std::vector>& queue_entries) +{ + auto query = fmt::format( + "SELECT account_id, queue_position, estimated_wait, ip_address " + "FROM tblLoginQueue WHERE world_server_id = {} " + "ORDER BY queue_position ASC", + m_world_server_id + ); + + auto results = database.QueryDatabase(query); // Use world server global database + if (!results.Success()) { + LogError("Failed to load queue entries for world server {}", m_world_server_id); + return false; + } + + queue_entries.clear(); + for (auto row = results.begin(); row != results.end(); ++row) { + uint32 account_id = atoi(row[0]); + uint32 queue_position = atoi(row[1]); + uint32 estimated_wait = atoi(row[2]); + uint32 ip_address = atoi(row[3]); + + queue_entries.emplace_back(account_id, queue_position, estimated_wait, ip_address); + } + + QueueDebugLog(2, "Loaded {} queue entries for world server {}", queue_entries.size(), m_world_server_id); + return true; +} + +void QueueManager::ProcessAdvancementTimer() +{ + uint32 queue_size = GetTotalQueueSize(); + LogDebug("Queue Status: {} players currently in queue", queue_size); + + // Update server_population table for monitoring/debugging + uint32 effective_population = EffectivePopulation(); + static const std::string server_population_query_template = + "INSERT INTO server_population (server_id, effective_population) " + "VALUES (1, {}) ON DUPLICATE KEY UPDATE " + "effective_population = {}, last_updated = NOW()"; + auto query = fmt::format(fmt::runtime(server_population_query_template), effective_population, effective_population); + auto result = database.QueryDatabase(query); + if (!result.Success()) { + LogDebug("Failed to update server_population table: {}", result.ErrorMessage()); + } + + // Send real-time update to login server cache ONLY if population changed + // + if (loginserver && loginserver->Connected()) { + static uint32 last_sent_population = UINT32_MAX; // Track last sent value + + if (effective_population != last_sent_population) { + SendWorldListUpdate(effective_population); + LogDebug("Sent real-time ServerOP_WorldListUpdate with population: {} (changed from {})", + effective_population, last_sent_population); + + last_sent_population = effective_population; // Update tracking variable + } + } + // Auto connect Q player #1. Qplayers are shown their # in the queue + UpdateQueuePositions(); + // Check for stale connections + m_account_rez_mgr.PeriodicMaintenance(); + // Sync queue with database + CheckForExternalChanges(); +} + +void QueueManager::RestoreQueueFromDatabase() +{ + // Check if queue persistence is enabled + if (!RuleB(AlKabor, EnableQueuePersistence)) { + QueueDebugLog(2, "Queue persistence disabled - clearing old queue entries for world server [{}]", m_world_server_id); + auto clear_query = fmt::format("DELETE FROM tblLoginQueue WHERE world_server_id = {}", m_world_server_id); + database.QueryDatabase(clear_query); // Use global database + return; + } + + QueueDebugLog(1, "Restoring queue from database for world server [{}]", m_world_server_id); + + // Load queue entries from database + std::vector> queue_entries; + if (!LoadQueueEntries(queue_entries)) { + LogError("Failed to load queue entries from database"); + return; + } + + // Restore queue entries to memory + m_queued_clients.clear(); + uint32 restored_count = 0; + + for (const auto& entry_tuple : queue_entries) { + uint32 world_account_id = std::get<0>(entry_tuple); + uint32 queue_position = std::get<1>(entry_tuple); + uint32 estimated_wait = std::get<2>(entry_tuple); + uint32 ip_address = std::get<3>(entry_tuple); + + // Skip entries with invalid world account IDs + if (world_account_id == 0) { + QueueDebugLog(2, "QueueManager - SKIP: Invalid world account ID [0] during restoration"); + continue; + } + + QueuedClient entry; + entry.w_accountid = world_account_id; + entry.queue_position = queue_position; + entry.estimated_wait = estimated_wait; + entry.ip_address = ip_address; + entry.queued_timestamp = time(nullptr); // Current time for restored entries + entry.last_updated = time(nullptr); + + // For restored entries, we don't have LS account ID or extended connection details + entry.ls_account_id = 0; // Unknown for restored entries + entry.from_id = 0; + entry.ip_str = ""; + entry.forum_name = ""; + + // Use vector push_back instead of map indexing (consistent with vector declaration) + m_queued_clients.push_back(entry); + restored_count++; + + QueueDebugLog(2, "QueueManager - RESTORE: World account [{}] at position [{}] with wait [{}] - persistent queue entry restored", + world_account_id, queue_position, estimated_wait); + } + + if (restored_count > 0) { + QueueDebugLog(1, "Restored [{}] persistent queue entries from database for world server [{}]", + restored_count, m_world_server_id); + QueueDebugLog(2, "NOTE: Restored entries use world account IDs only - LS account mapping will be established when players reconnect"); + } else { + QueueDebugLog(2, "No queue entries to restore for world server [{}]", m_world_server_id); + } + + // Send immediate update to login server after queue restore + if (loginserver && loginserver->Connected()) { + uint32 effective_population = EffectivePopulation(); + SendWorldListUpdate(effective_population); + QueueDebugLog(1, "Sent ServerOP_WorldListUpdate to login server after queue restore - population: {}", effective_population); + } else { + QueueDebugLog(1, "Login server not connected - cannot send queue restore update"); + } +} + +// Connection validation helper to reduce code duplication +bool QueueManager::ValidateLoginServerConnection(uint16 opcode) const +{ + if (!loginserver || !loginserver->Connected()) { + if (opcode != 0) { + QueueDebugLog(2, "Cannot send opcode 0x{:X} - login server not connected", opcode); + } else { + QueueDebugLog(2, "Login server not connected - skipping packet send"); + } + return false; + } + return true; +} + +// ServerPacket helper methods to reduce code duplication +void QueueManager::SendWorldListUpdate(uint32 effective_population) +{ + if (!ValidateLoginServerConnection(ServerOP_WorldListUpdate)) {return;} + + auto update_pack = new ServerPacket(ServerOP_WorldListUpdate, sizeof(uint32)); + *((uint32*)update_pack->pBuffer) = effective_population; + loginserver->SendPacket(update_pack); + delete update_pack; + + QueueDebugLog(2, "Sent ServerOP_WorldListUpdate with population: {}", effective_population); +} + +void QueueManager::SendQueuedClientUpdate(uint32 ls_account_id, uint32 queue_position, uint32 estimated_wait, uint32 ip_address) +{ + if (!ValidateLoginServerConnection(ServerOP_QueueDirectUpdate)) {return;} + + auto update_pack = new ServerPacket(ServerOP_QueueDirectUpdate, sizeof(ServerQueueDirectUpdate_Struct)); + ServerQueueDirectUpdate_Struct* update = (ServerQueueDirectUpdate_Struct*)update_pack->pBuffer; + + update->ls_account_id = ls_account_id; + update->queue_position = queue_position; + update->estimated_wait = estimated_wait; + update->ip_address = ip_address; + + loginserver->SendPacket(update_pack); + delete update_pack; + + QueueDebugLog(2, "Sent ServerOP_QueueDirectUpdate for LS account {} - position: {}, wait: {}s", + ls_account_id, queue_position, estimated_wait); +} + +void QueueManager::SendQueueRemoval(uint32 ls_account_id) +{ + if (!ValidateLoginServerConnection(ServerOP_QueueDirectUpdate)) {return;} + + auto removal_pack = new ServerPacket(ServerOP_QueueDirectUpdate, sizeof(ServerQueueDirectUpdate_Struct)); + ServerQueueDirectUpdate_Struct* removal = (ServerQueueDirectUpdate_Struct*)removal_pack->pBuffer; + + removal->ls_account_id = ls_account_id; + removal->queue_position = 0; // Position 0 = removed from queue + removal->estimated_wait = 0; + removal->ip_address = 0; + + loginserver->SendPacket(removal_pack); + delete removal_pack; + + QueueDebugLog(2, "Sent queue removal for LS account {}", ls_account_id); +} + +// Helper methods for common packet sending patterns - simplified overloads +template +void QueueManager::SendLoginServerPacket(uint16 opcode, const T& data) +{ + if (!ValidateLoginServerConnection(opcode)) { return; } + + auto packet = new ServerPacket(opcode, sizeof(T)); + *((T*)packet->pBuffer) = data; + loginserver->SendPacket(packet); + delete packet; + + QueueDebugLog(2, "Sent packet opcode 0x{:X}, size: {}", opcode, sizeof(T)); +} + +void QueueManager::SendLoginServerPacket(uint16 opcode, uint32 value) +{ + if (!ValidateLoginServerConnection(opcode)) { return; } + + auto packet = new ServerPacket(opcode, sizeof(uint32)); + *((uint32*)packet->pBuffer) = value; + loginserver->SendPacket(packet); + delete packet; + + QueueDebugLog(2, "Sent packet opcode 0x{:X} with value: {}", opcode, value); +} + +void QueueManager::SendLoginServerPacket(uint16 opcode) +{ + if (!ValidateLoginServerConnection(opcode)) { return; } + + auto packet = new ServerPacket(opcode, 0); + loginserver->SendPacket(packet); + delete packet; + + QueueDebugLog(2, "Sent packet opcode 0x{:X} (no data)", opcode); +} \ No newline at end of file diff --git a/world/world_queue.h b/world/world_queue.h new file mode 100644 index 000000000..89c247b23 --- /dev/null +++ b/world/world_queue.h @@ -0,0 +1,180 @@ +#ifndef WORLD_QUEUE_H +#define WORLD_QUEUE_H + +#include "../common/types.h" +#include "account_reservation_manager.h" // Add AccountRezMgr +#include +#include +#include +#include +#include // Added for std::set +#include // Added for std::unique_ptr +#include // Added for std::optional +#include // Added for template metaprogramming + +// Queue debug system - shared across all queue-related files +// 0 = off, 1 = important events only, 2 = verbose/noisy operations +#ifndef QUEUE_DEBUG_LEVEL +#define QUEUE_DEBUG_LEVEL 1 // TODO: Need to refine which msgs are @ which level +#endif + +#define QueueDebugLog(level, fmt, ...) \ + do { if (QUEUE_DEBUG_LEVEL >= level) LogInfo(fmt, ##__VA_ARGS__); } while(0) + +struct QueuedClient; +class WorldDatabase; +class AccountRezMgr; +class Client; +struct UsertoWorldResponse; +class QueueManager; +extern QueueManager queue_manager; // Global object - always valid, no null checks needed + +namespace EQ { + namespace Net { + class Packet; + } +} + +enum class QueueDecisionOutcome { + AutoConnect, // Auto-connect bypass + QueueToggle, // Player was queued, remove them (toggle off) + GMBypass, // GM bypass - override capacity + GraceBypass, // Grace period bypass - override capacity + QueuePlayer // Normal queueing - respect capacity decision +}; + +struct ConnectionRequest { + uint32 account_id; + uint32 ls_account_id; + uint32 ip_address; + int16 status; // Account status (GM level, etc.) + bool is_auto_connect; // Is this an auto-connect vs manual PLAY? + bool is_mule; // Is this a mule account? + const char* ip_str; + const char* forum_name; + uint32 world_account_id; +}; + +class QueueManager { +public: + QueueManager(); // No longer needs WorldServer pointer + ~QueueManager(); + + /** + * Core queue operations + */ + void AddToQueue(uint32 world_account_id, uint32 position, uint32 estimated_wait, uint32 ip_address, + uint32 ls_account_id, uint32 from_id, + const char* ip_str, const char* forum_name, const char* client_key = nullptr); + void RemoveFromQueue(const std::vector& account_ids); + void RemoveFromQueue(uint32 account_id) { RemoveFromQueue(std::vector{account_id}); } // Single account overload + void UpdateQueuePositions(); + + /** + * Connection decision logic - handles -6 queue responses from world server + * Returns true if player should bypass queue (override -6), false if should be queued + */ + bool EvaluateConnectionRequest(const ConnectionRequest& request, uint32 max_capacity, + UsertoWorldResponse* response = nullptr, Client* client = nullptr); + + /** + * Queue queries + */ + bool IsAccountQueued(uint32 account_id) const; + uint32 GetQueuePosition(uint32 account_id) const; + uint32 GetTotalQueueSize() const; + + /** + * Population management + */ + uint32 EffectivePopulation(); // World population + test offset + + /** + * Queue state management + */ + void SetPaused(bool paused) { m_queue_paused = paused; } + bool IsPaused() const { return m_queue_paused; } + + /** + * Persistence operations + */ + // void SyncQueueToDatabase(); + void RestoreQueueFromDatabase(); + void CheckForExternalChanges(); // NEW: Check if database changed externally + + void ProcessAdvancementTimer(); // Enhanced queue management - handles population updates, DB sync, and advancement + + /** + * Database queue operations - moved from loginserver + */ + void SaveQueueDBEntry(uint32 account_id, uint32 queue_position, uint32 estimated_wait, uint32 ip_address); + void RemoveQueueDBEntry(uint32 account_id); + bool LoadQueueEntries(std::vector>& queue_entries); + + /** + * Debugging and management + */ + void ClearAllQueues(); + + /** + * Auto-connect functionality + */ + void AutoConnectQueuedPlayer(const QueuedClient& qclient); + + /** + * Targeted broadcast system - send updates only to queued clients + */ + void SendQueuedClientsUpdate() const; + + // Account reservation manager - true POD safety (always callable) + mutable AccountRezMgr m_account_rez_mgr; + +private: + // Database safety helper - checks if database is ready for operations + bool ValidateDatabaseReady() const; + + // Queue data + std::vector m_queued_clients; // Ordered queue - position = index + 1 + std::map m_last_seen; // account_id -> timestamp of last login server connection + bool m_queue_paused; // Queue updates paused + // std::string m_freeze_reason; + + // Cached test offset for efficient population calculation + uint32 m_cached_test_offset; + + // World server ID for database operations + uint32 m_world_server_id; + + // Helper functions + void LogQueueAction(const std::string& action, uint32 account_id, const std::string& details = "") const; + + // Encapsulated database operations + uint32 GetWorldAccountFromLS(uint32 ls_account_id) const; // Wrapper for GetAccountIDFromLSID with fallback logic + uint32 GetLSAccountFromWorld(uint32 world_account_id) const; // Reverse mapping for dialogs and notifications + + // Database query helpers + bool QueryDB(const std::string& query, const std::string& operation_desc) const; + uint32 QuerySingleUint32(const std::string& query, uint32 default_value = 0) const; + + // ServerPacket helper methods to reduce code duplication + void SendWorldListUpdate(uint32 effective_population); + void SendQueuedClientUpdate(uint32 ls_account_id, uint32 queue_position, uint32 estimated_wait, uint32 ip_address); + void SendQueueRemoval(uint32 ls_account_id); + void SendQueueAutoConnect(const QueuedClient& qclient); + + // Helper methods for common packet sending patterns - simplified overloads + template + void SendLoginServerPacket(uint16 opcode, const T& data); + void SendLoginServerPacket(uint16 opcode, uint32 value); + void SendLoginServerPacket(uint16 opcode); + + // Connection validation helper + bool ValidateLoginServerConnection(uint16 opcode = 0) const; +}; + +// Global population accessor function +inline uint32 GetWorldPop() { + return queue_manager.EffectivePopulation(); +} + +#endif // WORLD_QUEUE_H \ No newline at end of file diff --git a/world/zoneserver.cpp b/world/zoneserver.cpp index 28dd7b1b0..46d1d3829 100644 --- a/world/zoneserver.cpp +++ b/world/zoneserver.cpp @@ -39,6 +39,7 @@ #include "../common/zone_store.h" #include "../common/patches/patches.h" #include "../common/skill_caps.h" +#include "world_queue.h" // For queue_manager global #include "../common/server_reload_types.h" extern ClientList client_list; @@ -1031,6 +1032,8 @@ void ZoneServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) { ClientListEntry* cle = client_list.FindCLEByAccountID(skp->AccountID); if (cle) { cle->SetOnline(CLE_Status::Offline); + LogInfo("Removing account reservation for kicked account [{}]", cle->AccountID()); + queue_manager.m_account_rez_mgr.RemoveRez(cle->AccountID()); } zoneserver_list.SendPacket(pack); From 0774d419382df93c4529c6bdae96b1d3c79ae3b3 Mon Sep 17 00:00:00 2001 From: Edalyn Date: Thu, 24 Jul 2025 11:32:19 -0400 Subject: [PATCH 2/5] Change AlKabor -> World, exclude admins from reservations --- common/ruletypes.h | 21 ++++++++++----------- world/account_reservation_manager.cpp | 6 +++--- world/client.cpp | 2 +- world/world_queue.cpp | 12 +++++++----- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/common/ruletypes.h b/common/ruletypes.h index d43738aca..cc7b76737 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -126,6 +126,16 @@ RULE_BOOL( World, DontBootDynamics, false, "If true, dynamic zones will not boot RULE_BOOL(World, EnableDevTools, true, "Enable or Disable the Developer Tools globally (Most of the time you want this enabled)") RULE_BOOL(World, UseOldShadowKnightClassExport, true, "Disable to have Shadowknight show as Shadow Knight (live-like)") RULE_STRING(World, MOTD, "", "Server MOTD sent on login, change from empty to have this be used instead of variables table 'motd' value") +RULE_INT(World, TestPopulationOffset, 0, "Test population offset for queue testing") +RULE_INT(World, IPDatabaseSyncInterval, 300, "Interval in seconds between database synchronization of IP reservation data") +RULE_INT(World, MaxPlayersOnline, 1200, "Maximum number of players allowed online before queue is activated") +RULE_BOOL(World, EnableQueue, true, "Enable the login queue system when server population is at capacity") +RULE_BOOL(World, QueueBypassGMLevel, true, "Allow GMs (status >= 80) to bypass the queue") +RULE_BOOL(World, EnableQueueLogging, true, "Enable detailed logging for queue system operations") +RULE_BOOL(World, FreezeQueue, false, "Freeze queue advancement - players remain at current positions") +RULE_BOOL(World, EnableQueuePersistence, true, "Enable saving queue state to database for crash recovery") +RULE_INT(World, DefaultGracePeriod, 60, "Default grace period in seconds for IP reservations when not specified") +RULE_INT(World, IPCleanupInterval, 5, "Interval in seconds between IP reservation cleanup cycles") RULE_CATEGORY_END() RULE_CATEGORY( Zone ) @@ -185,17 +195,6 @@ RULE_BOOL (AlKabor, ReducedMonkAC, true, "AK behavior is true. Monks had a low RULE_BOOL (AlKabor, BlockProjectileCorners, true, "AK behavior is true. If an NPC was in a corner, arrows and bolts would not hit them.") RULE_BOOL (AlKabor, BlockProjectileWalls, true, "AK behavior is true. If an NPC was walled, then arrows and bolts had to be fired from an angle parallel to the wall in order to hit them. (if this is true, corners will also block)") RULE_BOOL (AlKabor, GreenmistHack, true, "Greenmist recourse didn't work on AK. The spell data is messed up so it's not properly fixable without modifying the client. This enables a partial workaround that is not AKurate but provides some benefit to players using this weapon.") -RULE_INT (AlKabor, PlayerPopulationCap, 1200, "Max Players allowed in the Server. Will exclude offline Bazaar trader characters.") -RULE_INT (AlKabor, TestPopulationOffset, 0, "Test population offset for queue testing") -RULE_INT (AlKabor, IPDatabaseSyncInterval, 300, "Interval in seconds between database synchronization of IP reservation data") -RULE_INT (AlKabor, MaxPlayersOnline, 1200, "Maximum number of players allowed online before queue is activated") -RULE_BOOL (AlKabor, EnableQueue, true, "Enable the login queue system when server population is at capacity") -RULE_BOOL (AlKabor, QueueBypassGMLevel, true, "Allow GMs (status >= 80) to bypass the queue") -RULE_BOOL (AlKabor, EnableQueueLogging, true, "Enable detailed logging for queue system operations") -RULE_BOOL (AlKabor, FreezeQueue, false, "Freeze queue advancement - players remain at current positions") -RULE_BOOL (AlKabor, EnableQueuePersistence, true, "Enable saving queue state to database for crash recovery") -RULE_INT (AlKabor, DefaultGracePeriod, 60, "Default grace period in seconds for IP reservations when not specified") -RULE_INT (AlKabor, IPCleanupInterval, 5, "Interval in seconds between IP reservation cleanup cycles") RULE_CATEGORY_END() RULE_CATEGORY( Map ) diff --git a/world/account_reservation_manager.cpp b/world/account_reservation_manager.cpp index 0497a0ef1..af568765e 100644 --- a/world/account_reservation_manager.cpp +++ b/world/account_reservation_manager.cpp @@ -22,7 +22,7 @@ void AccountRezMgr::AddRez(uint32 account_id, uint32 ip_address, uint32 grace_pe extern bool database_ready; if (grace_period_seconds == 0) { if (database_ready) { - grace_period_seconds = RuleI(Quarm, DefaultGracePeriod); + grace_period_seconds = RuleI(World, DefaultGracePeriod); } else { QueueDebugLog(1, "Database not ready - using default grace period for account [{}]", account_id); grace_period_seconds = 60; @@ -154,11 +154,11 @@ std::string AccountRezMgr::AccountToString(uint32 account_id) const { } bool AccountRezMgr::ShouldPerformCleanup() const { - return (time(nullptr) - m_last_cleanup) >= RuleI(Quarm, IPCleanupInterval); + return (time(nullptr) - m_last_cleanup) >= RuleI(World, IPCleanupInterval); } bool AccountRezMgr::ShouldPerformDatabaseSync() const { - return (time(nullptr) - m_last_database_sync) >= RuleI(Quarm, IPDatabaseSyncInterval); + return (time(nullptr) - m_last_database_sync) >= RuleI(World, IPDatabaseSyncInterval); } bool AccountRezMgr::IsAccountInGraceWhitelist(uint32 account_id) { diff --git a/world/client.cpp b/world/client.cpp index 2f94ebba2..c837b55b5 100644 --- a/world/client.cpp +++ b/world/client.cpp @@ -299,7 +299,7 @@ bool Client::HandleSendLoginInfoPacket(const EQApplicationPacket *app) { // Register this account as active for queue population tracking // Skip if account already has a reservation (auto-connecting people get theirs earlier) - if (loginserver && !queue_manager.m_account_rez_mgr.IsAccountInGraceWhitelist(cle->AccountID())) { + if (loginserver && cle->Admin() < QuestTroupe && !queue_manager.m_account_rez_mgr.IsAccountInGraceWhitelist(cle->AccountID())) { queue_manager.m_account_rez_mgr.AddRez(cle->AccountID(), GetIP(), 6); LogInfo("Added account reservation for account [{}] (normal connection)", cle->AccountID()); } else if (queue_manager.m_account_rez_mgr.IsAccountInGraceWhitelist(cle->AccountID())) { diff --git a/world/world_queue.cpp b/world/world_queue.cpp index 1e6540fd5..76ce693f8 100644 --- a/world/world_queue.cpp +++ b/world/world_queue.cpp @@ -9,8 +9,10 @@ #include "../common/rulesys.h" // For RuleB and RuleI macros #include "../common/ip_util.h" // For IpUtil::IsIpInPrivateRfc1918 #include -#include +#ifndef _WINDOWS +#include +#endif extern LoginServer* loginserver; extern WorldDatabase database; extern ClientList client_list; @@ -201,13 +203,13 @@ void QueueManager::UpdateQueuePositions() } // Check if queue is manually frozen via rule - if (RuleB(AlKabor, FreezeQueue)) { + if (RuleB(World, FreezeQueue)) { QueueDebugLog(2, "Queue updates frozen by rule - [{}] players remain queued with frozen positions", m_queued_clients.size()); return; } // Get server capacity for capacity decisions - uint32 max_capacity = RuleI(AlKabor, PlayerPopulationCap); + uint32 max_capacity = RuleI(World, MaxPlayersOnline); uint32 current_population = EffectivePopulation(); // Calculate available slots for auto-connects @@ -285,7 +287,7 @@ bool QueueManager::EvaluateConnectionRequest(const ConnectionRequest& request, u decision = QueueDecisionOutcome::QueueToggle; } // 3. Check GM bypass rules - this is where we override world server's decision - else if (RuleB(AlKabor, QueueBypassGMLevel) && request.status >= 80) { + else if (RuleB(World, QueueBypassGMLevel) && request.status >= QuestTroupe) { QueueDebugLog(1, "QueueManager - GM_BYPASS: Account [{}] (status: {}) overriding world server capacity decision", request.account_id, request.status); decision = QueueDecisionOutcome::GMBypass; @@ -809,7 +811,7 @@ void QueueManager::ProcessAdvancementTimer() void QueueManager::RestoreQueueFromDatabase() { // Check if queue persistence is enabled - if (!RuleB(AlKabor, EnableQueuePersistence)) { + if (!RuleB(World, EnableQueuePersistence)) { QueueDebugLog(2, "Queue persistence disabled - clearing old queue entries for world server [{}]", m_world_server_id); auto clear_query = fmt::format("DELETE FROM tblLoginQueue WHERE world_server_id = {}", m_world_server_id); database.QueryDatabase(clear_query); // Use global database From 1697f4a51bf3446fcdfbce77a33c1ca2f28c5827 Mon Sep 17 00:00:00 2001 From: Edalyn Date: Thu, 24 Jul 2025 11:33:40 -0400 Subject: [PATCH 3/5] Windows build fix --- loginserver/server_manager.cpp | 2 ++ loginserver/world_server.cpp | 2 ++ 2 files changed, 4 insertions(+) diff --git a/loginserver/server_manager.cpp b/loginserver/server_manager.cpp index 78de4097b..734db0535 100644 --- a/loginserver/server_manager.cpp +++ b/loginserver/server_manager.cpp @@ -23,7 +23,9 @@ #include "../common/eqemu_logsys.h" #include "../common/ip_util.h" #include +#ifndef _WINDOWS #include +#endif #include #include #include diff --git a/loginserver/world_server.cpp b/loginserver/world_server.cpp index a046b5b41..cb3037ace 100644 --- a/loginserver/world_server.cpp +++ b/loginserver/world_server.cpp @@ -22,7 +22,9 @@ #include "../common/ip_util.h" #include "../common/queue_packets.h" // Queue-specific opcodes and structures #include +#ifndef _WINDOWS #include +#endif #include #include #include From 60bca0f67215d2405209d8184713d8c8fdd0329b Mon Sep 17 00:00:00 2001 From: Edalyn Date: Thu, 24 Jul 2025 16:17:34 -0400 Subject: [PATCH 4/5] Client key field in utws --- common/servertalk.h | 1 + loginserver/server_manager.cpp | 5 ++--- world/world_queue.cpp | 3 ++- world/world_queue.h | 1 + 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/common/servertalk.h b/common/servertalk.h index 7bd33eb40..7a7263c76 100644 --- a/common/servertalk.h +++ b/common/servertalk.h @@ -674,6 +674,7 @@ struct UsertoWorldRequest { uint32 ToID; char IPAddr[64]; char forum_name[31]; + char client_key[31]; }; struct UsertoWorldResponse { diff --git a/loginserver/server_manager.cpp b/loginserver/server_manager.cpp index 734db0535..170e4234d 100644 --- a/loginserver/server_manager.cpp +++ b/loginserver/server_manager.cpp @@ -238,9 +238,8 @@ void ServerManager::SendUserToWorldRequest(const char* server_id, unsigned int c utwr->FromID = is_auto_connect ? 1 : 0; utwr->ToID = 0; // Not used - // Store client key in forum_name field (repurposing for queue system) - strncpy(utwr->forum_name, client_key.c_str(), sizeof(utwr->forum_name) - 1); - utwr->forum_name[sizeof(utwr->forum_name) - 1] = '\0'; + strncpy(utwr->client_key, client_key.c_str(), sizeof(utwr->client_key) - 1); + utwr->client_key[sizeof(utwr->client_key) - 1] = '\0'; (*iter)->GetConnection()->Send(ServerOP_UsertoWorldReq, outapp); found = true; diff --git a/world/world_queue.cpp b/world/world_queue.cpp index 76ce693f8..eb4ee8099 100644 --- a/world/world_queue.cpp +++ b/world/world_queue.cpp @@ -328,7 +328,7 @@ bool QueueManager::EvaluateConnectionRequest(const ConnectionRequest& request, u // Add to queue for this server { // Use the client key from the login server request (passed via forum_name field) - std::string client_key = request.forum_name ? request.forum_name : ""; + std::string client_key = request.client_key ? request.client_key : ""; AddToQueue( request.world_account_id, // world_account_id (primary key) @@ -856,6 +856,7 @@ void QueueManager::RestoreQueueFromDatabase() entry.from_id = 0; entry.ip_str = ""; entry.forum_name = ""; + entry.authorized_client_key = ""; // Use vector push_back instead of map indexing (consistent with vector declaration) m_queued_clients.push_back(entry); diff --git a/world/world_queue.h b/world/world_queue.h index 89c247b23..790adc2e1 100644 --- a/world/world_queue.h +++ b/world/world_queue.h @@ -52,6 +52,7 @@ struct ConnectionRequest { bool is_mule; // Is this a mule account? const char* ip_str; const char* forum_name; + const char* client_key; uint32 world_account_id; }; From 62df6b96600ec644bd392084ac05c7984dd8256f Mon Sep 17 00:00:00 2001 From: Edalyn Date: Fri, 25 Jul 2025 19:52:49 -0400 Subject: [PATCH 5/5] Update servertalk_server_connection.cpp --- common/net/servertalk_server_connection.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/common/net/servertalk_server_connection.cpp b/common/net/servertalk_server_connection.cpp index b039c9a9d..25a44921b 100644 --- a/common/net/servertalk_server_connection.cpp +++ b/common/net/servertalk_server_connection.cpp @@ -34,7 +34,9 @@ void EQ::Net::ServertalkServerConnection::Send(uint16_t opcode, EQ::Net::Packet req.PutUInt32(i, req_in->FromID); i += 4; req.PutUInt32(i, req_in->ToID); i += 4; req.PutData(i, req_in->IPAddr, 64); i += 64; - + req.PutData(i, req_in->forum_name, 31); i += 31; + req.PutData(i, req_in->client_key, 31); i += 31; + EQ::Net::DynamicPacket out; out.PutUInt16(0, ServerOP_UsertoWorldResp); out.PutUInt16(2, req.Length() + 4); @@ -56,6 +58,7 @@ void EQ::Net::ServertalkServerConnection::Send(uint16_t opcode, EQ::Net::Packet req.PutUInt16(i, req_in->is_world_admin); i += 2; req.PutUInt32(i, req_in->ip_address); i += 4; req.PutUInt8(i, req_in->is_client_from_local_network); i += 1; + req.PutData(i, req_in->forum_name, 31); i += 31; EQ::Net::DynamicPacket out; out.PutUInt16(0, ServerOP_LSClientAuth);