From aa31732158d3e457dfe630f3b2e56f53de89e2b1 Mon Sep 17 00:00:00 2001 From: fusion32 Date: Tue, 7 Oct 2025 18:50:02 -0300 Subject: make connections properly handle async queries This moves queries from `src/connections.cc` to `src/query.cc` and adds a few missing details. The only thing left is to review query processing and tie database operations to the `TDatabase` struct. --- src/query.cc | 1856 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 1836 insertions(+), 20 deletions(-) (limited to 'src/query.cc') diff --git a/src/query.cc b/src/query.cc index d1042e1..9d21833 100644 --- a/src/query.cc +++ b/src/query.cc @@ -4,8 +4,8 @@ struct TQueryQueue{ pthread_mutex_t Mutex; - pthread_cond_t EmptyCond; - pthread_cond_t FullCond; + pthread_cond_t WorkAvailable; + pthread_cond_t RoomAvailable; uint32 ReadPos; uint32 WritePos; uint32 MaxQueries; @@ -27,17 +27,21 @@ static TQueryQueue *g_QueryQueue; TQuery *QueryNew(void){ TQuery *Query = (TQuery*)calloc(1, sizeof(TQuery)); AtomicStore(&Query->RefCount, 1); - Query->BufferSize = g_MaxConnectionPacketSize; + Query->BufferSize = g_Config.QueryBufferSize; Query->Buffer = (uint8*)calloc(1, Query->BufferSize); + Query->Request = TReadBuffer{}; + Query->Response = TWriteBuffer{}; return Query; } void QueryDone(TQuery *Query){ - int RefCount = AtomicFetchAdd(&Query->RefCount, -1); - ASSERT(RefCount >= 1); - if(RefCount == 1){ - free(Query->Buffer); - free(Query); + if(Query != NULL){ + int RefCount = AtomicFetchAdd(&Query->RefCount, -1); + ASSERT(RefCount >= 1); + if(RefCount == 1){ + free(Query->Buffer); + free(Query); + } } } @@ -64,13 +68,13 @@ void QueryEnqueue(TQuery *Query){ while(NumQueries >= MaxQueries){ LOG_WARN("Execution stalled: queue is full (%u / %u)...", NumQueries, MaxQueries); - pthread_cond_wait(&g_QueryQueue->FullCond, &g_QueryQueue->Mutex); + pthread_cond_wait(&g_QueryQueue->RoomAvailable, &g_QueryQueue->Mutex); NumQueries = g_QueryQueue->WritePos - g_QueryQueue->ReadPos; MaxQueries = g_QueryQueue->MaxQueries; } if(NumQueries == 0){ - pthread_cond_signal(&g_QueryQueue->EmptyCond); + pthread_cond_signal(&g_QueryQueue->WorkAvailable); } g_QueryQueue->Queries[g_QueryQueue->WritePos % MaxQueries] = Query; @@ -86,14 +90,14 @@ TQuery *QueryDequeue(AtomicInt *Running){ pthread_mutex_lock(&g_QueryQueue->Mutex); uint32 NumQueries = g_QueryQueue->WritePos - g_QueryQueue->ReadPos; while(NumQueries == 0 && AtomicLoad(Running)){ - pthread_cond_wait(&g_QueryQueue->EmptyCond, &g_QueryQueue->Mutex); + pthread_cond_wait(&g_QueryQueue->WorkAvailable, &g_QueryQueue->Mutex); NumQueries = g_QueryQueue->WritePos - g_QueryQueue->ReadPos; } if(NumQueries > 0 && AtomicLoad(Running)){ uint32 MaxQueries = g_QueryQueue->MaxQueries; if(NumQueries == MaxQueries){ - pthread_cond_signal(&g_QueryQueue->FullCond); + pthread_cond_signal(&g_QueryQueue->RoomAvailable); } Query = g_QueryQueue->Queries[g_QueryQueue->ReadPos % MaxQueries]; @@ -119,10 +123,53 @@ static void *WorkerThread(void *Data){ LOG("%d: Running...", Worker->WorkerID); while(TQuery *Query = QueryDequeue(&Worker->Running)){ - //TODO + Query->QueryType = Query->Request.Read8(); + //bool (*ProcessQuery)(TDatabase*, TQuery*) = NULL; switch(Query->QueryType){ + case QUERY_INTERNAL_RESOLVE_WORLD: ProcessInternalResolveWorld(Database, Query); break; + case QUERY_CHECK_ACCOUNT_PASSWORD: ProcessCheckAccountPassword(Database, Query); break; + case QUERY_LOGIN_ACCOUNT: ProcessLoginAccount(Database, Query); break; + case QUERY_LOGIN_ADMIN: ProcessLoginAdmin(Database, Query); break; + case QUERY_LOGIN_GAME: ProcessLoginGame(Database, Query); break; + case QUERY_LOGOUT_GAME: ProcessLogoutGame(Database, Query); break; + case QUERY_SET_NAMELOCK: ProcessSetNamelock(Database, Query); break; + case QUERY_BANISH_ACCOUNT: ProcessBanishAccount(Database, Query); break; + case QUERY_SET_NOTATION: ProcessSetNotation(Database, Query); break; + case QUERY_REPORT_STATEMENT: ProcessReportStatement(Database, Query); break; + case QUERY_BANISH_IP_ADDRESS: ProcessBanishIpAddress(Database, Query); break; + case QUERY_LOG_CHARACTER_DEATH: ProcessLogCharacterDeath(Database, Query); break; + case QUERY_ADD_BUDDY: ProcessAddBuddy(Database, Query); break; + case QUERY_REMOVE_BUDDY: ProcessRemoveBuddy(Database, Query); break; + case QUERY_DECREMENT_IS_ONLINE: ProcessDecrementIsOnline(Database, Query); break; + case QUERY_FINISH_AUCTIONS: ProcessFinishAuctions(Database, Query); break; + case QUERY_TRANSFER_HOUSES: ProcessTransferHouses(Database, Query); break; + case QUERY_EVICT_FREE_ACCOUNTS: ProcessEvictFreeAccounts(Database, Query); break; + case QUERY_EVICT_DELETED_CHARACTERS: ProcessEvictDeletedCharacters(Database, Query); break; + case QUERY_EVICT_EX_GUILDLEADERS: ProcessEvictExGuildleaders(Database, Query); break; + case QUERY_INSERT_HOUSE_OWNER: ProcessInsertHouseOwner(Database, Query); break; + case QUERY_UPDATE_HOUSE_OWNER: ProcessUpdateHouseOwner(Database, Query); break; + case QUERY_DELETE_HOUSE_OWNER: ProcessDeleteHouseOwner(Database, Query); break; + case QUERY_GET_HOUSE_OWNERS: ProcessGetHouseOwners(Database, Query); break; + case QUERY_GET_AUCTIONS: ProcessGetAuctions(Database, Query); break; + case QUERY_START_AUCTION: ProcessStartAuction(Database, Query); break; + case QUERY_INSERT_HOUSES: ProcessInsertHouses(Database, Query); break; + case QUERY_CLEAR_IS_ONLINE: ProcessClearIsOnline(Database, Query); break; + case QUERY_CREATE_PLAYERLIST: ProcessCreatePlayerlist(Database, Query); break; + case QUERY_LOG_KILLED_CREATURES: ProcessLogKilledCreatures(Database, Query); break; + case QUERY_LOAD_PLAYERS: ProcessLoadPlayers(Database, Query); break; + case QUERY_EXCLUDE_FROM_AUCTIONS: ProcessExcludeFromAuctions(Database, Query); break; + case QUERY_CANCEL_HOUSE_TRANSFER: ProcessCancelHouseTransfer(Database, Query); break; + case QUERY_LOAD_WORLD_CONFIG: ProcessLoadWorldConfig(Database, Query); break; + case QUERY_CREATE_ACCOUNT: ProcessCreateAccount(Database, Query); break; + case QUERY_CREATE_CHARACTER: ProcessCreateCharacter(Database, Query); break; + case QUERY_GET_ACCOUNT_SUMMARY: ProcessGetAccountSummary(Database, Query); break; + case QUERY_GET_CHARACTER_PROFILE: ProcessGetCharacterProfile(Database, Query); break; + case QUERY_GET_WORLDS: ProcessGetWorlds(Database, Query); break; + case QUERY_GET_ONLINE_CHARACTERS: ProcessGetOnlineCharacters(Database, Query); break; + case QUERY_GET_KILL_STATISTICS: ProcessGetKillStatistics(Database, Query); break; default:{ - // + QueryFailed(Query); + break; } } @@ -144,9 +191,9 @@ bool InitQuery(void){ // in flight. g_QueryQueue = (TQueryQueue*)calloc(1, sizeof(TQueryQueue)); pthread_mutex_init(&g_QueryQueue->Mutex, NULL); - pthread_cond_init(&g_QueryQueue->EmptyCond, NULL); - pthread_cond_init(&g_QueryQueue->FullCond, NULL); - g_QueryQueue->MaxQueries = 2 * g_MaxConnections; + pthread_cond_init(&g_QueryQueue->WorkAvailable, NULL); + pthread_cond_init(&g_QueryQueue->RoomAvailable, NULL); + g_QueryQueue->MaxQueries = 2 * g_Config.MaxConnections; g_QueryQueue->Queries = (TQuery**)calloc(g_QueryQueue->MaxQueries, sizeof(TQuery*)); g_NumWorkers = 1; @@ -173,6 +220,10 @@ void ExitQuery(void){ AtomicStore(&g_Workers[i].Running, 0); } + if(g_QueryQueue != NULL){ + pthread_cond_broadcast(&g_QueryQueue->WorkAvailable); + } + for(int i = 0; i < g_NumWorkers; i += 1){ // IMPORTANT(fusion): The `WorkerID` will be set to -1 if we fail // to spawn its thread. @@ -187,8 +238,8 @@ void ExitQuery(void){ if(g_QueryQueue != NULL){ pthread_mutex_destroy(&g_QueryQueue->Mutex); - pthread_cond_destroy(&g_QueryQueue->EmptyCond); - pthread_cond_destroy(&g_QueryQueue->FullCond); + pthread_cond_destroy(&g_QueryQueue->WorkAvailable); + pthread_cond_destroy(&g_QueryQueue->RoomAvailable); // TODO(fusion): Abort queries instead? uint32 MaxQueries = g_QueryQueue->MaxQueries; @@ -203,9 +254,36 @@ void ExitQuery(void){ } } -// Queries +// Query Request +//============================================================================== +TWriteBuffer QueryBeginRequest(TQuery *Query, int QueryType){ + Query->Request = TReadBuffer{}; + TWriteBuffer WriteBuffer = TWriteBuffer(Query->Buffer, Query->BufferSize); + WriteBuffer.Write8((uint8)QueryType); + return WriteBuffer; +} + +bool QueryFinishRequest(TQuery *Query, TWriteBuffer WriteBuffer){ + ASSERT(WriteBuffer.Buffer == Query->Buffer + && WriteBuffer.Size == Query->BufferSize + && WriteBuffer.Position >= 1); + bool Result = !WriteBuffer.Overflowed(); + if(Result){ + Query->Request = TReadBuffer(WriteBuffer.Buffer, WriteBuffer.Position); + } + return Result; +} + +bool QueryInternalResolveWorld(TQuery *Query, const char *World){ + TWriteBuffer WriteBuffer = QueryBeginRequest(Query, QUERY_INTERNAL_RESOLVE_WORLD); + WriteBuffer.WriteString(World); + return QueryFinishRequest(Query, WriteBuffer); +} + +// Query Response //============================================================================== TWriteBuffer *QueryBeginResponse(TQuery *Query, int Status){ + Query->QueryStatus = Status; Query->Response = TWriteBuffer(Query->Buffer, Query->BufferSize); Query->Response.Write16(0); Query->Response.Write8((uint8)Status); @@ -246,3 +324,1741 @@ void QueryFailed(TQuery *Query){ QueryFinishResponse(Query); } +// Query Processing +//============================================================================== +void ProcessInternalResolveWorld(TDatabase *Database, TQuery *Query); +void ProcessCheckAccountPassword(TDatabase *Database, TQuery *Query); +void ProcessLoginAccount(TDatabase *Database, TQuery *Query); +void ProcessLoginAdmin(TDatabase *Database, TQuery *Query); +void ProcessLoginGame(TDatabase *Database, TQuery *Query); +void ProcessLogoutGame(TDatabase *Database, TQuery *Query); +void ProcessSetNamelock(TDatabase *Database, TQuery *Query); +void ProcessBanishAccount(TDatabase *Database, TQuery *Query); +void ProcessSetNotation(TDatabase *Database, TQuery *Query); +void ProcessReportStatement(TDatabase *Database, TQuery *Query); +void ProcessBanishIpAddress(TDatabase *Database, TQuery *Query); +void ProcessLogCharacterDeath(TDatabase *Database, TQuery *Query); +void ProcessAddBuddy(TDatabase *Database, TQuery *Query); +void ProcessRemoveBuddy(TDatabase *Database, TQuery *Query); +void ProcessDecrementIsOnline(TDatabase *Database, TQuery *Query); +void ProcessFinishAuctions(TDatabase *Database, TQuery *Query); +void ProcessTransferHouses(TDatabase *Database, TQuery *Query); +void ProcessEvictFreeAccounts(TDatabase *Database, TQuery *Query); +void ProcessEvictDeletedCharacters(TDatabase *Database, TQuery *Query); +void ProcessEvictExGuildleaders(TDatabase *Database, TQuery *Query); +void ProcessInsertHouseOwner(TDatabase *Database, TQuery *Query); +void ProcessUpdateHouseOwner(TDatabase *Database, TQuery *Query); +void ProcessDeleteHouseOwner(TDatabase *Database, TQuery *Query); +void ProcessGetHouseOwners(TDatabase *Database, TQuery *Query); +void ProcessGetAuctions(TDatabase *Database, TQuery *Query); +void ProcessStartAuction(TDatabase *Database, TQuery *Query); +void ProcessInsertHouses(TDatabase *Database, TQuery *Query); +void ProcessClearIsOnline(TDatabase *Database, TQuery *Query); +void ProcessCreatePlayerlist(TDatabase *Database, TQuery *Query); +void ProcessLogKilledCreatures(TDatabase *Database, TQuery *Query); +void ProcessLoadPlayers(TDatabase *Database, TQuery *Query); +void ProcessExcludeFromAuctions(TDatabase *Database, TQuery *Query); +void ProcessCancelHouseTransfer(TDatabase *Database, TQuery *Query); +void ProcessLoadWorldConfig(TDatabase *Database, TQuery *Query); +void ProcessCreateAccount(TDatabase *Database, TQuery *Query); +void ProcessCreateCharacter(TDatabase *Database, TQuery *Query); +void ProcessGetAccountSummary(TDatabase *Database, TQuery *Query); +void ProcessGetCharacterProfile(TDatabase *Database, TQuery *Query); +void ProcessGetWorlds(TDatabase *Database, TQuery *Query); +void ProcessGetOnlineCharacters(TDatabase *Database, TQuery *Query); +void ProcessGetKillStatistics(TDatabase *Database, TQuery *Query); + +// TODO(fusion): These are the old query processing functions. The new ones will +// be very similar, except that we want a way to tell whether there was a database +// failure, such as a connection reset, so we can automatically retry them up to +// a certain amount of times before failing. +// bool ProcessX(Database, Query){ +// // Execute database queries, returning false on failure. The +// // response should only be written at the very end when we know +// // the query SUCCEEDED, to keep the request data intact if a retry +// // is needed (because they share the query buffer). +// } +// +// ... +// +// void DatabaseCheckpoint(Database){ +// // Check whether there was a database error or if the database is +// // still connected and make sure it is ready for processing a query. +// } +// +// ... +// +// int NumAttempts = MaxAttempts; +// DatabaseCheckpoint(Database); +// while(!ProcessX(Database, Query)){ +// if(Attempts <= 0){ +// QueryFailed(Query); +// break; +// } +// DatabaseCheckpoint(Database); +// NumAttempts -= 1; +// } + + +#if 0 +// Connection Queries +//============================================================================== +void CompoundBanishment(TBanishmentStatus Status, int *Days, bool *FinalWarning){ + // TODO(fusion): We might want to add all these constants as config values. + ASSERT(Days != NULL && FinalWarning != NULL); + if(Status.FinalWarning){ + *FinalWarning = false; + *Days = 0; // permanent + }else if(Status.TimesBanished > 5 || *FinalWarning){ + *FinalWarning = true; + if(*Days < 30){ + *Days = 30; + }else{ + *Days *= 2; + } + } +} + + +void ProcessLoginQuery(TConnection *Connection, TReadBuffer *Buffer){ + char Password[30]; + char LoginData[30]; + int ApplicationType = Buffer->Read8(); + Buffer->ReadString(Password, sizeof(Password)); + if(ApplicationType == APPLICATION_TYPE_GAME){ + Buffer->ReadString(LoginData, sizeof(LoginData)); + } + + // TODO(fusion): Probably just disconnect on failed login attempt? Implement + // write then disconnect? + if(!StringEq(g_QueryManagerPassword, Password)){ + LOG_WARN("Invalid login attempt from %s", Connection->RemoteAddress); + SendQueryStatusFailed(Connection); + return; + } + + int WorldID = 0; + if(ApplicationType == APPLICATION_TYPE_GAME){ + WorldID = GetWorldID(LoginData); + if(WorldID == 0){ + LOG_WARN("Rejecting connection %s from unknown game server \"%s\"", + Connection->RemoteAddress, LoginData); + SendQueryStatusFailed(Connection); + return; + } + LOG("Connection %s AUTHORIZED to game server \"%s\" (%d)", + Connection->RemoteAddress, LoginData, WorldID); + }else if(ApplicationType == APPLICATION_TYPE_LOGIN){ + LOG("Connection %s AUTHORIZED to login server", Connection->RemoteAddress); + }else if(ApplicationType == APPLICATION_TYPE_WEB){ + LOG("Connection %s AUTHORIZED to web server", Connection->RemoteAddress); + }else{ + LOG_WARN("Rejecting connection %s from unknown application type %d", + Connection->RemoteAddress, ApplicationType); + SendQueryStatusFailed(Connection); + return; + } + + Connection->Authorized = true; + Connection->ApplicationType = ApplicationType; + Connection->WorldID = WorldID; + SendQueryStatusOk(Connection); +} + +static int CheckAccountPasswordTransaction(int AccountID, const char *Password, int IPAddress){ + TransactionScope Tx("CheckAccountPassword"); + if(!Tx.Begin()){ + return -1; + } + + TAccount Account; + if(!GetAccountData(AccountID, &Account)){ + return -1; + } + + if(Account.AccountID == 0){ + return 1; + } + + if(!TestPassword(Account.Auth, sizeof(Account.Auth), Password)){ + return 2; + } + + if(GetAccountFailedLoginAttempts(Account.AccountID, 5 * 60) > 10){ + return 3; + } + + if(GetIPAddressFailedLoginAttempts(IPAddress, 30 * 60) > 20){ + return 4; + } + + if(!Tx.Commit()){ + return -1; + } + + return 0; +} + +void ProcessCheckAccountPasswordQuery(TConnection *Connection, TReadBuffer *Buffer){ + char Password[30]; + char IPString[16]; + int AccountID = (int)Buffer->Read32(); + Buffer->ReadString(Password, sizeof(Password)); + Buffer->ReadString(IPString, sizeof(IPString)); + + int IPAddress = 0; + if(!ParseIPAddress(IPString, &IPAddress)){ + SendQueryStatusFailed(Connection); + return; + } + + // NOTE(fusion): Similar to `ProcessLoginAccountQuery`. + int Result = CheckAccountPasswordTransaction(AccountID, Password, IPAddress); + InsertLoginAttempt(AccountID, IPAddress, (Result != 0)); + if(Result == -1){ + SendQueryStatusFailed(Connection); + }else if(Result != 0){ + SendQueryStatusError(Connection, Result); + }else{ + SendQueryStatusOk(Connection); + } +} + +int LoginAccountTransaction(int AccountID, const char *Password, int IPAddress, + DynamicArray *Characters, int *PremiumDays){ + TransactionScope Tx("LoginAccount"); + if(!Tx.Begin()){ + return -1; + } + + TAccount Account; + if(!GetAccountData(AccountID, &Account)){ + return -1; + } + + if(Account.AccountID == 0){ + return 1; + } + + if(!TestPassword(Account.Auth, sizeof(Account.Auth), Password)){ + return 2; + } + + if(GetAccountFailedLoginAttempts(Account.AccountID, 5 * 60) > 10){ + return 3; + } + + if(GetIPAddressFailedLoginAttempts(IPAddress, 30 * 60) > 20){ + return 4; + } + + if(IsAccountBanished(Account.AccountID)){ + return 5; + } + + if(IsIPBanished(IPAddress)){ + return 6; + } + + if(!GetCharacterEndpoints(Account.AccountID, Characters)){ + return -1; + } + + if(!Tx.Commit()){ + return -1; + } + + *PremiumDays = Account.PremiumDays + Account.PendingPremiumDays; + return 0; +} + +void ProcessLoginAccountQuery(TConnection *Connection, TReadBuffer *Buffer){ + char Password[30]; + char IPString[16]; + int AccountID = (int)Buffer->Read32(); + Buffer->ReadString(Password, sizeof(Password)); + Buffer->ReadString(IPString, sizeof(IPString)); + + int IPAddress = 0; + if(!ParseIPAddress(IPString, &IPAddress)){ + SendQueryStatusFailed(Connection); + return; + } + + int PremiumDays = 0; + DynamicArray Characters; + int Result = LoginAccountTransaction(AccountID, Password, + IPAddress, &Characters, &PremiumDays); + + // NOTE(fusion): Similar to `ProcessLoginGameQuery` except we don't modify + // any tables inside the login transaction. + // TODO(fusion): Maybe have different login attempt tables or types? + InsertLoginAttempt(AccountID, IPAddress, (Result != 0)); + + if(Result == -1){ + SendQueryStatusFailed(Connection); + return; + } + + if(Result != 0){ + SendQueryStatusError(Connection, Result); + return; + } + + TWriteBuffer WriteBuffer = PrepareResponse(Connection, QUERY_STATUS_OK); + int NumCharacters = std::min(Characters.Length(), UINT8_MAX); + WriteBuffer.Write8((uint8)NumCharacters); + for(int i = 0; i < NumCharacters; i += 1){ + WriteBuffer.WriteString(Characters[i].Name); + WriteBuffer.WriteString(Characters[i].WorldName); + WriteBuffer.Write32BE((uint32)Characters[i].WorldAddress); + WriteBuffer.Write16((uint16)Characters[i].WorldPort); + } + WriteBuffer.Write16((uint16)PremiumDays); + SendResponse(Connection, &WriteBuffer); +} + +void ProcessLoginAdminQuery(TConnection *Connection, TReadBuffer *Buffer){ + // TODO(fusion): I thought for a second this could be the query used with + // the login server but it doesn't take a password or ip address for basic + // checks. Even if it's used in combination with `CheckAccountPassword`, + // it doesn't make sense to split what should have been a single query which + // is what the new `LoginAccount` query does. + SendQueryStatusFailed(Connection); +} + +static int LoginGameTransaction(int WorldID, int AccountID, const char *CharacterName, + const char *Password, int IPAddress, bool PrivateWorld, bool GamemasterRequired, + TCharacterLoginData *Character, DynamicArray *Buddies, + DynamicArray *Rights, bool *PremiumAccountActivated){ + TransactionScope Tx("LoginGame"); + if(!Tx.Begin()){ + return -1; + } + + if(!GetCharacterLoginData(CharacterName, Character)){ + return -1; + } + + if(Character->CharacterID == 0){ + return 1; + } + + if(Character->Deleted){ + return 2; + } + + if(Character->WorldID != WorldID){ + return 3; + } + + if(PrivateWorld){ + if(!GetWorldInvitation(WorldID, Character->CharacterID)){ + return 4; + } + } + + TAccount Account; + if(!GetAccountData(AccountID, &Account)){ + return -1; + } + + if(Account.AccountID == 0 || Account.AccountID != Character->AccountID){ + // NOTE(fusion): This is correct, there is no error code 5. + return 15; + } + + if(Account.Deleted){ + return 8; + } + + if(!TestPassword(Account.Auth, sizeof(Account.Auth), Password)){ + return 6; + } + + if(GetAccountFailedLoginAttempts(Account.AccountID, 5 * 60) > 10){ + return 7; + } + + if(GetIPAddressFailedLoginAttempts(IPAddress, 30 * 60) > 20){ + return 9; + } + + if(IsAccountBanished(Account.AccountID)){ + return 10; + } + + if(IsCharacterNamelocked(Character->CharacterID)){ + return 11; + } + + if(IsIPBanished(IPAddress)){ + return 12; + } + + // TODO(fusion): Probably merge these into a single operation? + if(!GetCharacterRight(Character->CharacterID, "ALLOW_MULTICLIENT") + && GetAccountOnlineCharacters(Account.AccountID) > 0 + && !IsCharacterOnline(Character->CharacterID)){ + return 13; + } + + if(GamemasterRequired){ + if(!GetCharacterRight(Character->CharacterID, "GAMEMASTER_OUTFIT")){ + return 14; + } + } + + if(!GetBuddies(WorldID, Account.AccountID, Buddies)){ + return -1; + } + + if(!GetCharacterRights(Character->CharacterID, Rights)){ + return -1; + } + + if(Account.PremiumDays == 0 && Account.PendingPremiumDays > 0){ + if(!ActivatePendingPremiumDays(Account.AccountID)){ + return -1; + } + + Account.PremiumDays += Account.PendingPremiumDays; + Account.PendingPremiumDays = 0; + *PremiumAccountActivated = true; + } + + if(Account.PremiumDays > 0){ + Rights->Push(TCharacterRight{"PREMIUM_ACCOUNT"}); + } + + if(!IncrementIsOnline(WorldID, Character->CharacterID)){ + return -1; + } + + if(!Tx.Commit()){ + return -1; + } + + return 0; +} + +void ProcessLoginGameQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + char CharacterName[30]; + char Password[30]; + char IPString[16]; + int AccountID = (int)Buffer->Read32(); + Buffer->ReadString(CharacterName, sizeof(CharacterName)); + Buffer->ReadString(Password, sizeof(Password)); + Buffer->ReadString(IPString, sizeof(IPString)); + bool PrivateWorld = Buffer->ReadFlag(); + Buffer->ReadFlag(); // "PremiumAccountRequired" unused + bool GamemasterRequired = Buffer->ReadFlag(); + + int IPAddress = 0; + if(!ParseIPAddress(IPString, &IPAddress)){ + SendQueryStatusFailed(Connection); + return; + } + + TCharacterLoginData Character; + DynamicArray Buddies; + DynamicArray Rights; + bool PremiumAccountActivated = false; + int Result = LoginGameTransaction(Connection->WorldID, AccountID, + CharacterName, Password, IPAddress, PrivateWorld, + GamemasterRequired, &Character, &Buddies, &Rights, + &PremiumAccountActivated); + + // IMPORTANT(fusion): We need to insert login attempts outside the login game + // transaction or we could end up not having it recorded at all due to rollbacks. + // It is also the reason the whole transaction had to be pulled to its own function. + // IMPORTANT(fusion): Don't return if we fail to insert the login attempt as the + // result of the whole operation was already determined by the transaction function. + InsertLoginAttempt(AccountID, IPAddress, (Result != 0)); + + if(Result == -1){ + SendQueryStatusFailed(Connection); + return; + } + + if(Result != 0){ + SendQueryStatusError(Connection, Result); + return; + } + + TWriteBuffer WriteBuffer = PrepareResponse(Connection, QUERY_STATUS_OK); + WriteBuffer.Write32((uint32)Character.CharacterID); + WriteBuffer.WriteString(Character.Name); + WriteBuffer.Write8((uint8)Character.Sex); + WriteBuffer.WriteString(Character.Guild); + WriteBuffer.WriteString(Character.Rank); + WriteBuffer.WriteString(Character.Title); + + int NumBuddies = std::min(Buddies.Length(), UINT8_MAX); + WriteBuffer.Write8((uint8)NumBuddies); + for(int i = 0; i < NumBuddies; i += 1){ + WriteBuffer.Write32((uint32)Buddies[i].CharacterID); + WriteBuffer.WriteString(Buddies[i].Name); + } + + int NumRights = std::min(Rights.Length(), UINT8_MAX); + WriteBuffer.Write8((uint8)NumRights); + for(int i = 0; i < NumRights; i += 1){ + WriteBuffer.WriteString(Rights[i].Name); + } + + WriteBuffer.WriteFlag(PremiumAccountActivated); + + SendResponse(Connection, &WriteBuffer); +} + +void ProcessLogoutGameQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + char Profession[30]; + char Residence[30]; + int CharacterID = (int)Buffer->Read32(); + int Level = Buffer->Read16(); + Buffer->ReadString(Profession, sizeof(Profession)); + Buffer->ReadString(Residence, sizeof(Residence)); + int LastLoginTime = (int)Buffer->Read32(); + int TutorActivities = Buffer->Read16(); + + if(!LogoutCharacter(Connection->WorldID, CharacterID, Level, + Profession, Residence, LastLoginTime, TutorActivities)){ + SendQueryStatusFailed(Connection); + return; + } + + SendQueryStatusOk(Connection); +} + +void ProcessSetNamelockQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + char CharacterName[30]; + char IPString[16]; + char Reason[200]; + char Comment[200]; + int GamemasterID = (int)Buffer->Read32(); + Buffer->ReadString(CharacterName, sizeof(CharacterName)); + Buffer->ReadString(IPString, sizeof(IPString)); + Buffer->ReadString(Reason, sizeof(Reason)); + Buffer->ReadString(Comment, sizeof(Comment)); + + int IPAddress = 0; + if(!StringEmpty(IPString) && !ParseIPAddress(IPString, &IPAddress)){ + SendQueryStatusFailed(Connection); + return; + } + + TransactionScope Tx("SetNamelock"); + if(!Tx.Begin()){ + SendQueryStatusFailed(Connection); + return; + } + + int CharacterID = GetCharacterID(Connection->WorldID, CharacterName); + if(CharacterID == 0){ + SendQueryStatusError(Connection, 1); + return; + } + + // TODO(fusion): Might be `NO_BANISHMENT`. + if(GetCharacterRight(CharacterID, "NAMELOCK")){ + SendQueryStatusError(Connection, 2); + return; + } + + TNamelockStatus Status = GetNamelockStatus(CharacterID); + if(Status.Namelocked){ + SendQueryStatusError(Connection, (Status.Approved ? 4 : 3)); + return; + } + + if(!InsertNamelock(CharacterID, IPAddress, GamemasterID, Reason, Comment)){ + SendQueryStatusFailed(Connection); + return; + } + + if(!Tx.Commit()){ + SendQueryStatusFailed(Connection); + return; + } + + SendQueryStatusOk(Connection); +} + +void ProcessBanishAccountQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + char CharacterName[30]; + char IPString[16]; + char Reason[200]; + char Comment[200]; + int GamemasterID = (int)Buffer->Read32(); + Buffer->ReadString(CharacterName, sizeof(CharacterName)); + Buffer->ReadString(IPString, sizeof(IPString)); + Buffer->ReadString(Reason, sizeof(Reason)); + Buffer->ReadString(Comment, sizeof(Comment)); + bool FinalWarning = Buffer->ReadFlag(); + + int IPAddress = 0; + if(!StringEmpty(IPString) && !ParseIPAddress(IPString, &IPAddress)){ + SendQueryStatusFailed(Connection); + return; + } + + TransactionScope Tx("BanishAccount"); + if(!Tx.Begin()){ + SendQueryStatusFailed(Connection); + return; + } + + int CharacterID = GetCharacterID(Connection->WorldID, CharacterName); + if(CharacterID == 0){ + SendQueryStatusError(Connection, 1); + return; + } + + // TODO(fusion): Might be `NO_BANISHMENT`. + if(GetCharacterRight(CharacterID, "BANISHMENT")){ + SendQueryStatusError(Connection, 2); + return; + } + + TBanishmentStatus Status = GetBanishmentStatus(CharacterID); + if(Status.Banished){ + SendQueryStatusError(Connection, 3); + return; + } + + int BanishmentID = 0; + int Days = 7; + CompoundBanishment(Status, &Days, &FinalWarning); + if(!InsertBanishment(CharacterID, IPAddress, GamemasterID, + Reason, Comment, FinalWarning, Days * 86400, &BanishmentID)){ + SendQueryStatusFailed(Connection); + return; + } + + if(!Tx.Commit()){ + SendQueryStatusFailed(Connection); + return; + } + + TWriteBuffer WriteBuffer = PrepareResponse(Connection, QUERY_STATUS_OK); + WriteBuffer.Write32((uint32)BanishmentID); + WriteBuffer.Write8(Days > 0 ? Days : 0xFF); + WriteBuffer.WriteFlag(FinalWarning); + SendResponse(Connection, &WriteBuffer); +} + +void ProcessSetNotationQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + char CharacterName[30]; + char IPString[16]; + char Reason[200]; + char Comment[200]; + int GamemasterID = Buffer->Read32(); + Buffer->ReadString(CharacterName, sizeof(CharacterName)); + Buffer->ReadString(IPString, sizeof(IPString)); + Buffer->ReadString(Reason, sizeof(Reason)); + Buffer->ReadString(Comment, sizeof(Comment)); + + int IPAddress = 0; + if(!StringEmpty(IPString) && !ParseIPAddress(IPString, &IPAddress)){ + SendQueryStatusFailed(Connection); + return; + } + + TransactionScope Tx("SetNotation"); + if(!Tx.Begin()){ + SendQueryStatusFailed(Connection); + return; + } + + int CharacterID = GetCharacterID(Connection->WorldID, CharacterName); + if(CharacterID == 0){ + SendQueryStatusError(Connection, 1); + return; + } + + // TODO(fusion): Might be `NO_BANISHMENT`. + if(GetCharacterRight(CharacterID, "NOTATION")){ + SendQueryStatusError(Connection, 2); + return; + } + + int BanishmentID = 0; + if(GetNotationCount(CharacterID) >= 5){ + int BanishmentDays = 7; + bool FinalWarning = false; + TBanishmentStatus Status = GetBanishmentStatus(CharacterID); + CompoundBanishment(Status, &BanishmentDays, &FinalWarning); + if(!InsertBanishment(CharacterID, IPAddress, 0, "Excessive Notations", + "", FinalWarning, BanishmentDays, &BanishmentID)){ + SendQueryStatusFailed(Connection); + return; + } + } + + if(!InsertNotation(CharacterID, IPAddress, GamemasterID, Reason, Comment)){ + SendQueryStatusFailed(Connection); + return; + } + + if(!Tx.Commit()){ + SendQueryStatusFailed(Connection); + return; + } + + TWriteBuffer WriteBuffer = PrepareResponse(Connection, QUERY_STATUS_OK); + WriteBuffer.Write32((uint32)BanishmentID); + SendResponse(Connection, &WriteBuffer); +} + +void ProcessReportStatementQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + char CharacterName[30]; + char Reason[200]; + char Comment[200]; + int ReporterID = Buffer->Read32(); + Buffer->ReadString(CharacterName, sizeof(CharacterName)); + Buffer->ReadString(Reason, sizeof(Reason)); + Buffer->ReadString(Comment, sizeof(Comment)); + int BanishmentID = Buffer->Read32(); + int StatementID = Buffer->Read32(); + int NumStatements = Buffer->Read16(); + + if(StatementID == 0){ + LOG_ERR("Missing reported statement id"); + SendQueryStatusFailed(Connection); + return; + } + + if(NumStatements == 0){ + LOG_ERR("Missing report statements"); + SendQueryStatusFailed(Connection); + return; + } + + TStatement *ReportedStatement = NULL; + TStatement *Statements = (TStatement*)alloca(NumStatements * sizeof(TStatement)); + for(int i = 0; i < NumStatements; i += 1){ + Statements[i].StatementID = (int)Buffer->Read32(); + Statements[i].Timestamp = (int)Buffer->Read32(); + Statements[i].CharacterID = (int)Buffer->Read32(); + Buffer->ReadString(Statements[i].Channel, sizeof(Statements[i].Channel)); + Buffer->ReadString(Statements[i].Text, sizeof(Statements[i].Text)); + + if(Statements[i].StatementID == StatementID){ + if(ReportedStatement != NULL){ + LOG_WARN("Reported statement (%d, %d, %d) appears multiple times", + Connection->WorldID, Statements[i].Timestamp, + Statements[i].StatementID); + } + ReportedStatement = &Statements[i]; + } + } + + if(ReportedStatement == NULL){ + LOG_ERR("Missing reported statement"); + SendQueryStatusFailed(Connection); + return; + } + + TransactionScope Tx("ReportStatement"); + if(!Tx.Begin()){ + SendQueryStatusFailed(Connection); + return; + } + + int CharacterID = GetCharacterID(Connection->WorldID, CharacterName); + if(CharacterID == 0){ + SendQueryStatusError(Connection, 1); + return; + }else if(ReportedStatement->CharacterID != CharacterID){ + LOG_ERR("Reported statement character mismatch"); + SendQueryStatusFailed(Connection); + return; + } + + if(IsStatementReported(Connection->WorldID, ReportedStatement)){ + SendQueryStatusError(Connection, 2); + return; + } + + if(!InsertStatements(Connection->WorldID, NumStatements, Statements)){ + SendQueryStatusFailed(Connection); + return; + } + + if(!InsertReportedStatement(Connection->WorldID, ReportedStatement, + BanishmentID, ReporterID, Reason, Comment)){ + SendQueryStatusFailed(Connection); + return; + } + + if(!Tx.Commit()){ + SendQueryStatusFailed(Connection); + return; + } + + SendQueryStatusOk(Connection); +} + +void ProcessBanishIPAddressQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + char CharacterName[30]; + char IPString[16]; + char Reason[200]; + char Comment[200]; + int GamemasterID = Buffer->Read16(); + Buffer->ReadString(CharacterName, sizeof(CharacterName)); + Buffer->ReadString(IPString, sizeof(IPString)); + Buffer->ReadString(Reason, sizeof(Reason)); + Buffer->ReadString(Comment, sizeof(Comment)); + + int IPAddress = 0; + if(!ParseIPAddress(IPString, &IPAddress)){ + SendQueryStatusFailed(Connection); + return; + } + + TransactionScope Tx("BanishIP"); + if(!Tx.Begin()){ + SendQueryStatusFailed(Connection); + return; + } + + int CharacterID = GetCharacterID(Connection->WorldID, CharacterName); + if(CharacterID == 0){ + SendQueryStatusError(Connection, 1); + return; + } + + // TODO(fusion): Might be `NO_BANISHMENT`. + if(GetCharacterRight(CharacterID, "IP_BANISHMENT")){ + SendQueryStatusError(Connection, 2); + return; + } + + // IMPORTANT(fusion): It is not a good idea to ban an IP address, specially + // V4 addresses, as they may be dynamically assigned or represent the address + // of a public ISP router that manages multiple clients. + int BanishmentDays = 3; + if(!InsertIPBanishment(CharacterID, IPAddress, GamemasterID, + Reason, Comment, BanishmentDays * 86400)){ + SendQueryStatusFailed(Connection); + return; + } + + if(!Tx.Commit()){ + SendQueryStatusFailed(Connection); + return; + } + + SendQueryStatusOk(Connection); +} + +void ProcessLogCharacterDeathQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + char Remark[30]; + int CharacterID = (int)Buffer->Read32(); + int Level = Buffer->Read16(); + int OffenderID = (int)Buffer->Read32(); + Buffer->ReadString(Remark, sizeof(Remark)); + bool Unjustified = Buffer->ReadFlag(); + int Timestamp = (int)Buffer->Read32(); + if(!InsertCharacterDeath(Connection->WorldID, CharacterID, Level, + OffenderID, Remark, Unjustified, Timestamp)){ + SendQueryStatusFailed(Connection); + return; + } + + SendQueryStatusOk(Connection); +} + +void ProcessAddBuddyQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + int AccountID = (int)Buffer->Read32(); + int BuddyID = (int)Buffer->Read32(); + if(!InsertBuddy(Connection->WorldID, AccountID, BuddyID)){ + SendQueryStatusFailed(Connection); + return; + } + + SendQueryStatusOk(Connection); +} + +void ProcessRemoveBuddyQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + int AccountID = (int)Buffer->Read32(); + int BuddyID = (int)Buffer->Read32(); + if(!DeleteBuddy(Connection->WorldID, AccountID, BuddyID)){ + SendQueryStatusFailed(Connection); + return; + } + + SendQueryStatusOk(Connection); +} + +void ProcessDecrementIsOnlineQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + int CharacterID = (int)Buffer->Read32(); + if(!DecrementIsOnline(Connection->WorldID, CharacterID)){ + SendQueryStatusFailed(Connection); + return; + } + + SendQueryStatusOk(Connection); +} + +void ProcessFinishAuctionsQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + DynamicArray Auctions; + if(!FinishHouseAuctions(Connection->WorldID, &Auctions)){ + SendQueryStatusFailed(Connection); + return; + } + + TWriteBuffer WriteBuffer = PrepareResponse(Connection, QUERY_STATUS_OK); + int NumAuctions = std::min(Auctions.Length(), UINT16_MAX); + WriteBuffer.Write16((uint16)NumAuctions); + for(int i = 0; i < NumAuctions; i += 1){ + WriteBuffer.Write16((uint16)Auctions[i].HouseID); + WriteBuffer.Write32((uint32)Auctions[i].BidderID); + WriteBuffer.WriteString(Auctions[i].BidderName); + WriteBuffer.Write32((uint32)Auctions[i].BidAmount); + } + SendResponse(Connection, &WriteBuffer); +} + +void ProcessTransferHousesQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + DynamicArray Transfers; + if(!FinishHouseTransfers(Connection->WorldID, &Transfers)){ + SendQueryStatusFailed(Connection); + return; + } + + TWriteBuffer WriteBuffer = PrepareResponse(Connection, QUERY_STATUS_OK); + int NumTransfers = std::min(Transfers.Length(), UINT16_MAX); + WriteBuffer.Write16((uint16)NumTransfers); + for(int i = 0; i < NumTransfers; i += 1){ + WriteBuffer.Write16((uint16)Transfers[i].HouseID); + WriteBuffer.Write32((uint32)Transfers[i].NewOwnerID); + WriteBuffer.WriteString(Transfers[i].NewOwnerName); + WriteBuffer.Write32((uint32)Transfers[i].Price); + } + SendResponse(Connection, &WriteBuffer); +} + +void ProcessEvictFreeAccountsQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + DynamicArray Evictions; + if(!GetFreeAccountEvictions(Connection->WorldID, &Evictions)){ + SendQueryStatusFailed(Connection); + return; + } + + TWriteBuffer WriteBuffer = PrepareResponse(Connection, QUERY_STATUS_OK); + int NumEvictions = std::min(Evictions.Length(), UINT16_MAX); + WriteBuffer.Write16((uint16)NumEvictions); + for(int i = 0; i < NumEvictions; i += 1){ + WriteBuffer.Write16((uint16)Evictions[i].HouseID); + WriteBuffer.Write32((uint32)Evictions[i].OwnerID); + } + SendResponse(Connection, &WriteBuffer); +} + +void ProcessEvictDeletedCharactersQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + DynamicArray Evictions; + if(!GetDeletedCharacterEvictions(Connection->WorldID, &Evictions)){ + SendQueryStatusFailed(Connection); + return; + } + + TWriteBuffer WriteBuffer = PrepareResponse(Connection, QUERY_STATUS_OK); + int NumEvictions = std::min(Evictions.Length(), UINT16_MAX); + WriteBuffer.Write16((uint16)NumEvictions); + for(int i = 0; i < NumEvictions; i += 1){ + WriteBuffer.Write16((uint16)Evictions[i].HouseID); + } + SendResponse(Connection, &WriteBuffer); +} + +void ProcessEvictExGuildleadersQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + // NOTE(fusion): This is a bit different from the other eviction functions. + // The server doesn't maintain guild information for characters so it will + // send a list of guild houses with their owners and we're supposed to check + // whether the owner is still a guild leader. I don't think we should check + // any other information as the server is authoritative on house information. + DynamicArray Evictions; + int NumGuildHouses = Buffer->Read16(); + for(int i = 0; i < NumGuildHouses; i += 1){ + int HouseID = Buffer->Read16(); + int OwnerID = (int)Buffer->Read32(); + if(!GetGuildLeaderStatus(Connection->WorldID, OwnerID)){ + Evictions.Push(HouseID); + } + } + + TWriteBuffer WriteBuffer = PrepareResponse(Connection, QUERY_STATUS_OK); + int NumEvictions = std::min(Evictions.Length(), UINT16_MAX); + WriteBuffer.Write16((uint16)NumEvictions); + for(int i = 0; i < NumEvictions; i += 1){ + WriteBuffer.Write16((uint16)Evictions[i]); + } + SendResponse(Connection, &WriteBuffer); +} + +void ProcessInsertHouseOwnerQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + int HouseID = Buffer->Read16(); + int OwnerID = (int)Buffer->Read32(); + int PaidUntil = (int)Buffer->Read32(); + if(!InsertHouseOwner(Connection->WorldID, HouseID, OwnerID, PaidUntil)){ + SendQueryStatusFailed(Connection); + return; + } + + SendQueryStatusOk(Connection); +} + +void ProcessUpdateHouseOwnerQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + int HouseID = Buffer->Read16(); + int OwnerID = (int)Buffer->Read32(); + int PaidUntil = (int)Buffer->Read32(); + if(!UpdateHouseOwner(Connection->WorldID, HouseID, OwnerID, PaidUntil)){ + SendQueryStatusFailed(Connection); + return; + } + + SendQueryStatusOk(Connection); +} + +void ProcessDeleteHouseOwnerQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + int HouseID = Buffer->Read16(); + if(!DeleteHouseOwner(Connection->WorldID, HouseID)){ + SendQueryStatusFailed(Connection); + return; + } + + SendQueryStatusOk(Connection); +} + +void ProcessGetHouseOwnersQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + DynamicArray Owners; + if(!GetHouseOwners(Connection->WorldID, &Owners)){ + SendQueryStatusFailed(Connection); + return; + } + + TWriteBuffer WriteBuffer = PrepareResponse(Connection, QUERY_STATUS_OK); + int NumOwners = std::min(Owners.Length(), UINT16_MAX); + WriteBuffer.Write16((uint16)NumOwners); + for(int i = 0; i < NumOwners; i += 1){ + WriteBuffer.Write16((uint16)Owners[i].HouseID); + WriteBuffer.Write32((uint32)Owners[i].OwnerID); + WriteBuffer.WriteString(Owners[i].OwnerName); + WriteBuffer.Write32((uint32)Owners[i].PaidUntil); + } + SendResponse(Connection, &WriteBuffer); +} + +void ProcessGetAuctionsQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + DynamicArray Auctions; + if(!GetHouseAuctions(Connection->WorldID, &Auctions)){ + SendQueryStatusFailed(Connection); + return; + } + + TWriteBuffer WriteBuffer = PrepareResponse(Connection, QUERY_STATUS_OK); + int NumAuctions = std::min(Auctions.Length(), UINT16_MAX); + WriteBuffer.Write16((uint16)NumAuctions); + for(int i = 0; i < NumAuctions; i += 1){ + WriteBuffer.Write16((uint16)Auctions[i]); + } + SendResponse(Connection, &WriteBuffer); +} + +void ProcessStartAuctionQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + int HouseID = Buffer->Read16(); + if(!StartHouseAuction(Connection->WorldID, HouseID)){ + SendQueryStatusFailed(Connection); + return; + } + + SendQueryStatusOk(Connection); +} + +void ProcessInsertHousesQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + TransactionScope Tx("InsertHouses"); + if(!Tx.Begin()){ + SendQueryStatusFailed(Connection); + return; + } + + if(!DeleteHouses(Connection->WorldID)){ + SendQueryStatusFailed(Connection); + return; + } + + int NumHouses = Buffer->Read16(); + if(NumHouses > 0){ +G THouse *Houses = (THouse*)alloca(NumHouses * sizeof(THouse)); + for(int i = 0; i < NumHouses; i += 1){ + Houses[i].HouseID = Buffer->Read16(); + Buffer->ReadString(Houses[i].Name, sizeof(Houses[i].Name)); + Houses[i].Rent = (int)Buffer->Read32(); + Buffer->ReadString(Houses[i].Description, sizeof(Houses[i].Description)); + Houses[i].Size = Buffer->Read16(); + Houses[i].PositionX = Buffer->Read16(); + Houses[i].PositionY = Buffer->Read16(); + Houses[i].PositionZ = Buffer->Read8(); + Buffer->ReadString(Houses[i].Town, sizeof(Houses[i].Town)); + Houses[i].GuildHouse = Buffer->ReadFlag(); + } + + if(!InsertHouses(Connection->WorldID, NumHouses, Houses)){ + SendQueryStatusFailed(Connection); + return; + } + } + + if(!Tx.Commit()){ + SendQueryStatusFailed(Connection); + return; + } + + SendQueryStatusOk(Connection); +} + +void ProcessClearIsOnlineQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + int NumAffectedCharacters; + if(!ClearIsOnline(Connection->WorldID, &NumAffectedCharacters)){ + SendQueryStatusFailed(Connection); + return; + } + + TWriteBuffer WriteBuffer = PrepareResponse(Connection, QUERY_STATUS_OK); + WriteBuffer.Write16((uint16)NumAffectedCharacters); + SendResponse(Connection, &WriteBuffer); +} + +void ProcessCreatePlayerlistQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + TransactionScope Tx("OnlineList"); + if(!Tx.Begin()){ + SendQueryStatusFailed(Connection); + return; + } + + if(!DeleteOnlineCharacters(Connection->WorldID)){ + SendQueryStatusFailed(Connection); + return; + } + + // TODO(fusion): I think `NumCharacters` may be used to signal that the + // server is going OFFLINE, in which case we'd have to add an `Online` + // column to `Worlds` and update it here. + + bool NewRecord = false; + int NumCharacters = Buffer->Read16(); + if(NumCharacters != 0xFFFF && NumCharacters > 0){ + TOnlineCharacter *Characters = (TOnlineCharacter*)alloca(NumCharacters * sizeof(TOnlineCharacter)); + for(int i = 0; i < NumCharacters; i += 1){ + Buffer->ReadString(Characters[i].Name, sizeof(Characters[i].Name)); + Characters[i].Level = Buffer->Read16(); + Buffer->ReadString(Characters[i].Profession, sizeof(Characters[i].Profession)); + } + + if(!InsertOnlineCharacters(Connection->WorldID, NumCharacters, Characters)){ + SendQueryStatusFailed(Connection); + return; + } + + if(!CheckOnlineRecord(Connection->WorldID, NumCharacters, &NewRecord)){ + SendQueryStatusFailed(Connection); + return; + } + } + + if(!Tx.Commit()){ + SendQueryStatusFailed(Connection); + return; + } + + TWriteBuffer WriteBuffer = PrepareResponse(Connection, QUERY_STATUS_OK); + WriteBuffer.WriteFlag(NewRecord); + SendResponse(Connection, &WriteBuffer); +} + +void ProcessLogKilledCreaturesQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + int NumStats = Buffer->Read16(); + TKillStatistics *Stats = (TKillStatistics*)alloca(NumStats * sizeof(TKillStatistics)); + for(int i = 0; i < NumStats; i += 1){ + Buffer->ReadString(Stats[i].RaceName, sizeof(Stats[i].RaceName)); + Stats[i].PlayersKilled = (int)Buffer->Read32(); + Stats[i].TimesKilled = (int)Buffer->Read32(); + } + + if(NumStats > 0){ + TransactionScope Tx("LogKilledCreatures"); + if(!Tx.Begin()){ + SendQueryStatusFailed(Connection); + return; + } + + if(!MergeKillStatistics(Connection->WorldID, NumStats, Stats)){ + SendQueryStatusFailed(Connection); + return; + } + + if(!Tx.Commit()){ + SendQueryStatusFailed(Connection); + return; + } + } + + SendQueryStatusOk(Connection); +} + +void ProcessLoadPlayersQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + // IMPORTANT(fusion): The server expect 10K entries at most. It is probably + // some shared hard coded constant. + int NumEntries; + TCharacterIndexEntry Entries[10000]; + int MinimumCharacterID = (int)Buffer->Read32(); + if(!GetCharacterIndexEntries(Connection->WorldID, + MinimumCharacterID, NARRAY(Entries), &NumEntries, Entries)){ + SendQueryStatusFailed(Connection); + return; + } + + TWriteBuffer WriteBuffer = PrepareResponse(Connection, QUERY_STATUS_OK); + WriteBuffer.Write32((uint32)NumEntries); + for(int i = 0; i < NumEntries; i += 1){ + WriteBuffer.WriteString(Entries[i].Name); + WriteBuffer.Write32((uint32)Entries[i].CharacterID); + } + SendResponse(Connection, &WriteBuffer); +} + +void ProcessExcludeFromAuctionsQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + TransactionScope Tx("ExcludeFromAuctions"); + if(!Tx.Begin()){ + SendQueryStatusFailed(Connection); + return; + } + + int CharacterID = (int)Buffer->Read32(); + bool Banish = Buffer->ReadFlag(); + int ExclusionDays = 7; + int BanishmentID = 0; + if(Banish){ + int BanishmentDays = 7; + bool FinalWarning = false; + TBanishmentStatus Status = GetBanishmentStatus(CharacterID); + CompoundBanishment(Status, &BanishmentDays, &FinalWarning); + if(!InsertBanishment(CharacterID, 0, 0, "Spoiling Auction", + "", FinalWarning, BanishmentDays * 86400, &BanishmentID)){ + SendQueryStatusFailed(Connection); + return; + } + } + + if(!ExcludeFromAuctions(Connection->WorldID, + CharacterID, ExclusionDays * 86400, BanishmentID)){ + SendQueryStatusFailed(Connection); + return; + } + + if(!Tx.Commit()){ + SendQueryStatusFailed(Connection); + return; + } + + SendQueryStatusOk(Connection); +} + +void ProcessCancelHouseTransferQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + // TODO(fusion): Not sure what this is used for. Maybe house transfer rows + // are kept permanently and this query is used to delete/flag it, in case + // the it didn't complete. We might need to refine `FinishHouseTransfers`. + //int HouseID = Buffer->Read16(); + SendQueryStatusOk(Connection); +} + +void ProcessLoadWorldConfigQuery(TConnection *Connection, TReadBuffer *Buffer){ + if(Connection->ApplicationType != APPLICATION_TYPE_GAME){ + SendQueryStatusFailed(Connection); + return; + } + + TWorldConfig WorldConfig = {}; + if(!GetWorldConfig(Connection->WorldID, &WorldConfig)){ + SendQueryStatusFailed(Connection); + return; + } + + TWriteBuffer WriteBuffer = PrepareResponse(Connection, QUERY_STATUS_OK); + WriteBuffer.Write8((uint8)WorldConfig.Type); + WriteBuffer.Write8((uint8)WorldConfig.RebootTime); + WriteBuffer.Write32BE((uint32)WorldConfig.IPAddress); + WriteBuffer.Write16((uint16)WorldConfig.Port); + WriteBuffer.Write16((uint16)WorldConfig.MaxPlayers); + WriteBuffer.Write16((uint16)WorldConfig.PremiumPlayerBuffer); + WriteBuffer.Write16((uint16)WorldConfig.MaxNewbies); + WriteBuffer.Write16((uint16)WorldConfig.PremiumNewbieBuffer); + SendResponse(Connection, &WriteBuffer); +} + +void ProcessCreateAccountQuery(TConnection *Connection, TReadBuffer *Buffer){ + // TODO(fusion): We'd ideally want to automatically generate an account number + // and return it in case of success but that would also require a more robust + // website infrastructure with verification e-mails, etc... + char Email[100]; + char Password[30]; + int AccountID = (int)Buffer->Read32(); + Buffer->ReadString(Email, sizeof(Email)); + Buffer->ReadString(Password, sizeof(Password)); + + // NOTE(fusion): Inputs should be checked before hand. + if(AccountID <= 0 || StringEmpty(Email) || StringEmpty(Password)){ + SendQueryStatusFailed(Connection); + return; + } + + uint8 Auth[64]; + if(!GenerateAuth(Password, Auth, sizeof(Auth))){ + SendQueryStatusFailed(Connection); + return; + } + + TransactionScope Tx("CreateAccount"); + if(!Tx.Begin()){ + SendQueryStatusFailed(Connection); + return; + } + + if(AccountNumberExists(AccountID)){ + SendQueryStatusError(Connection, 1); + return; + } + + if(AccountEmailExists(Email)){ + SendQueryStatusError(Connection, 2); + return; + } + + if(!CreateAccount(AccountID, Email, Auth, sizeof(Auth))){ + SendQueryStatusFailed(Connection); + return; + } + + if(!Tx.Commit()){ + SendQueryStatusFailed(Connection); + return; + } + + SendQueryStatusOk(Connection); +} + +void ProcessCreateCharacterQuery(TConnection *Connection, TReadBuffer *Buffer){ + char WorldName[30]; + char CharacterName[30]; + Buffer->ReadString(WorldName, sizeof(WorldName)); + int AccountID = (int)Buffer->Read32(); + Buffer->ReadString(CharacterName, sizeof(CharacterName)); + int Sex = Buffer->Read8(); + + // NOTE(fusion): Inputs should be checked before hand. + if(AccountID <= 0 || (Sex != 1 && Sex != 2) + || StringEmpty(WorldName) + || StringEmpty(CharacterName)){ + SendQueryStatusFailed(Connection); + return; + } + + TransactionScope Tx("CreateCharacter"); + if(!Tx.Begin()){ + SendQueryStatusFailed(Connection); + return; + } + + int WorldID = GetWorldID(WorldName); + if(WorldID == 0){ + SendQueryStatusError(Connection, 1); + return; + } + + if(!AccountNumberExists(AccountID)){ + SendQueryStatusError(Connection, 2); + return; + } + + if(CharacterNameExists(CharacterName)){ + SendQueryStatusError(Connection, 3); + return; + } + + if(!CreateCharacter(WorldID, AccountID, CharacterName, Sex)){ + SendQueryStatusFailed(Connection); + return; + } + + if(!Tx.Commit()){ + SendQueryStatusFailed(Connection); + return; + } + + SendQueryStatusOk(Connection); +} + +void ProcessGetAccountSummaryQuery(TConnection *Connection, TReadBuffer *Buffer){ + int AccountID = (int)Buffer->Read32(); + + if(AccountID <= 0){ + SendQueryStatusFailed(Connection); + return; + } + + TAccount Account; + if(!GetAccountData(AccountID, &Account)){ + SendQueryStatusFailed(Connection); + return; + } + + if(Account.AccountID != AccountID){ + SendQueryStatusFailed(Connection); + return; + } + + DynamicArray Characters; + if(!GetCharacterSummaries(AccountID, &Characters)){ + SendQueryStatusFailed(Connection); + return; + } + + TWriteBuffer WriteBuffer = PrepareResponse(Connection, QUERY_STATUS_OK); + WriteBuffer.WriteString(Account.Email); + WriteBuffer.Write16((uint16)Account.PremiumDays); + WriteBuffer.Write16((uint16)Account.PendingPremiumDays); + WriteBuffer.WriteFlag(Account.Deleted); + int NumCharacters = std::min(Characters.Length(), UINT8_MAX); + WriteBuffer.Write8((uint8)NumCharacters); + for(int i = 0; i < NumCharacters; i += 1){ + WriteBuffer.WriteString(Characters[i].Name); + WriteBuffer.WriteString(Characters[i].World); + WriteBuffer.Write16((uint16)Characters[i].Level); + WriteBuffer.WriteString(Characters[i].Profession); + WriteBuffer.WriteFlag(Characters[i].Online); + WriteBuffer.WriteFlag(Characters[i].Deleted); + } + SendResponse(Connection, &WriteBuffer); +} + +void ProcessGetCharacterProfileQuery(TConnection *Connection, TReadBuffer *Buffer){ + char CharacterName[30]; + Buffer->ReadString(CharacterName, sizeof(CharacterName)); + + if(StringEmpty(CharacterName)){ + SendQueryStatusFailed(Connection); + return; + } + + TCharacterProfile Character; + if(!GetCharacterProfile(CharacterName, &Character)){ + SendQueryStatusFailed(Connection); + return; + } + + if(!StringEqCI(Character.Name, CharacterName)){ + SendQueryStatusError(Connection, 1); + return; + } + + TWriteBuffer WriteBuffer = PrepareResponse(Connection, QUERY_STATUS_OK); + WriteBuffer.WriteString(Character.Name); + WriteBuffer.WriteString(Character.World); + WriteBuffer.Write8((uint8)Character.Sex); + WriteBuffer.WriteString(Character.Guild); + WriteBuffer.WriteString(Character.Rank); + WriteBuffer.WriteString(Character.Title); + WriteBuffer.Write16((uint16)Character.Level); + WriteBuffer.WriteString(Character.Profession); + WriteBuffer.WriteString(Character.Residence); + WriteBuffer.Write32((uint32)Character.LastLogin); + WriteBuffer.Write16((uint16)Character.PremiumDays); + WriteBuffer.WriteFlag(Character.Online); + WriteBuffer.WriteFlag(Character.Deleted); + SendResponse(Connection, &WriteBuffer); +} + +void ProcessGetWorldsQuery(TConnection *Connection, TReadBuffer *Buffer){ + DynamicArray Worlds; + if(!GetWorlds(&Worlds)){ + SendQueryStatusFailed(Connection); + return; + } + + TWriteBuffer WriteBuffer = PrepareResponse(Connection, QUERY_STATUS_OK); + int NumWorlds = std::min(Worlds.Length(), UINT8_MAX); + WriteBuffer.Write8((uint8)NumWorlds); + for(int i = 0; i < NumWorlds; i += 1){ + WriteBuffer.WriteString(Worlds[i].Name); + WriteBuffer.Write8((uint8)Worlds[i].Type); + WriteBuffer.Write16((uint16)Worlds[i].NumPlayers); + WriteBuffer.Write16((uint16)Worlds[i].MaxPlayers); + WriteBuffer.Write16((uint16)Worlds[i].OnlineRecord); + WriteBuffer.Write32((uint32)Worlds[i].OnlineRecordTimestamp); + } + SendResponse(Connection, &WriteBuffer); +} + +void ProcessGetOnlineCharactersQuery(TConnection *Connection, TReadBuffer *Buffer){ + char WorldName[30]; + Buffer->ReadString(WorldName, sizeof(WorldName)); + + int WorldID = GetWorldID(WorldName); + if(WorldID == 0){ + SendQueryStatusFailed(Connection); + return; + } + + DynamicArray Characters; + if(!GetOnlineCharacters(WorldID, &Characters)){ + SendQueryStatusFailed(Connection); + return; + } + + TWriteBuffer WriteBuffer = PrepareResponse(Connection, QUERY_STATUS_OK); + int NumCharacters = std::min(Characters.Length(), UINT16_MAX); + WriteBuffer.Write16((uint16)NumCharacters); + for(int i = 0; i < NumCharacters; i += 1){ + WriteBuffer.WriteString(Characters[i].Name); + WriteBuffer.Write16((uint16)Characters[i].Level); + WriteBuffer.WriteString(Characters[i].Profession); + } + SendResponse(Connection, &WriteBuffer); +} + +void ProcessGetKillStatisticsQuery(TConnection *Connection, TReadBuffer *Buffer){ + char WorldName[30]; + Buffer->ReadString(WorldName, sizeof(WorldName)); + + int WorldID = GetWorldID(WorldName); + if(WorldID == 0){ + SendQueryStatusFailed(Connection); + return; + } + + DynamicArray Stats; + if(!GetKillStatistics(WorldID, &Stats)){ + SendQueryStatusFailed(Connection); + return; + } + + TWriteBuffer WriteBuffer = PrepareResponse(Connection, QUERY_STATUS_OK); + int NumStats = std::min(Stats.Length(), UINT16_MAX); + WriteBuffer.Write16((uint16)NumStats); + for(int i = 0; i < NumStats; i += 1){ + WriteBuffer.WriteString(Stats[i].RaceName); + WriteBuffer.Write32((uint32)Stats[i].PlayersKilled); + WriteBuffer.Write32((uint32)Stats[i].TimesKilled); + } + SendResponse(Connection, &WriteBuffer); +} + +void ProcessConnectionQuery(TConnection *Connection){ + TReadBuffer Buffer(Connection->Buffer, Connection->RWSize); + int Query = Buffer.Read8(); + if(!Connection->Authorized){ + if(Query == QUERY_LOGIN){ + ProcessLoginQuery(Connection, &Buffer); + }else{ + LOG_ERR("Unauthorized query %d from %s", QueryType, Connection->RemoteAddress); + CloseConnection(Connection); + } + return; + } + + switch(Query){ + case QUERY_CHECK_ACCOUNT_PASSWORD: ProcessCheckAccountPasswordQuery(Connection, &Buffer); break; + case QUERY_LOGIN_ACCOUNT: ProcessLoginAccountQuery(Connection, &Buffer); break; + case QUERY_LOGIN_ADMIN: ProcessLoginAdminQuery(Connection, &Buffer); break; + case QUERY_LOGIN_GAME: ProcessLoginGameQuery(Connection, &Buffer); break; + case QUERY_LOGOUT_GAME: ProcessLogoutGameQuery(Connection, &Buffer); break; + case QUERY_SET_NAMELOCK: ProcessSetNamelockQuery(Connection, &Buffer); break; + case QUERY_BANISH_ACCOUNT: ProcessBanishAccountQuery(Connection, &Buffer); break; + case QUERY_SET_NOTATION: ProcessSetNotationQuery(Connection, &Buffer); break; + case QUERY_REPORT_STATEMENT: ProcessReportStatementQuery(Connection, &Buffer); break; + case QUERY_BANISH_IP_ADDRESS: ProcessBanishIPAddressQuery(Connection, &Buffer); break; + case QUERY_LOG_CHARACTER_DEATH: ProcessLogCharacterDeathQuery(Connection, &Buffer); break; + case QUERY_ADD_BUDDY: ProcessAddBuddyQuery(Connection, &Buffer); break; + case QUERY_REMOVE_BUDDY: ProcessRemoveBuddyQuery(Connection, &Buffer); break; + case QUERY_DECREMENT_IS_ONLINE: ProcessDecrementIsOnlineQuery(Connection, &Buffer); break; + case QUERY_FINISH_AUCTIONS: ProcessFinishAuctionsQuery(Connection, &Buffer); break; + case QUERY_TRANSFER_HOUSES: ProcessTransferHousesQuery(Connection, &Buffer); break; + case QUERY_EVICT_FREE_ACCOUNTS: ProcessEvictFreeAccountsQuery(Connection, &Buffer); break; + case QUERY_EVICT_DELETED_CHARACTERS: ProcessEvictDeletedCharactersQuery(Connection, &Buffer); break; + case QUERY_EVICT_EX_GUILDLEADERS: ProcessEvictExGuildleadersQuery(Connection, &Buffer); break; + case QUERY_INSERT_HOUSE_OWNER: ProcessInsertHouseOwnerQuery(Connection, &Buffer); break; + case QUERY_UPDATE_HOUSE_OWNER: ProcessUpdateHouseOwnerQuery(Connection, &Buffer); break; + case QUERY_DELETE_HOUSE_OWNER: ProcessDeleteHouseOwnerQuery(Connection, &Buffer); break; + case QUERY_GET_HOUSE_OWNERS: ProcessGetHouseOwnersQuery(Connection, &Buffer); break; + case QUERY_GET_AUCTIONS: ProcessGetAuctionsQuery(Connection, &Buffer); break; + case QUERY_START_AUCTION: ProcessStartAuctionQuery(Connection, &Buffer); break; + case QUERY_INSERT_HOUSES: ProcessInsertHousesQuery(Connection, &Buffer); break; + case QUERY_CLEAR_IS_ONLINE: ProcessClearIsOnlineQuery(Connection, &Buffer); break; + case QUERY_CREATE_PLAYERLIST: ProcessCreatePlayerlistQuery(Connection, &Buffer); break; + case QUERY_LOG_KILLED_CREATURES: ProcessLogKilledCreaturesQuery(Connection, &Buffer); break; + case QUERY_LOAD_PLAYERS: ProcessLoadPlayersQuery(Connection, &Buffer); break; + case QUERY_EXCLUDE_FROM_AUCTIONS: ProcessExcludeFromAuctionsQuery(Connection, &Buffer); break; + case QUERY_CANCEL_HOUSE_TRANSFER: ProcessCancelHouseTransferQuery(Connection, &Buffer); break; + case QUERY_LOAD_WORLD_CONFIG: ProcessLoadWorldConfigQuery(Connection, &Buffer); break; + case QUERY_CREATE_ACCOUNT: ProcessCreateAccountQuery(Connection, &Buffer); break; + case QUERY_CREATE_CHARACTER: ProcessCreateCharacterQuery(Connection, &Buffer); break; + case QUERY_GET_ACCOUNT_SUMMARY: ProcessGetAccountSummaryQuery(Connection, &Buffer); break; + case QUERY_GET_CHARACTER_PROFILE: ProcessGetCharacterProfileQuery(Connection, &Buffer); break; + case QUERY_GET_WORLDS: ProcessGetWorldsQuery(Connection, &Buffer); break; + case QUERY_GET_ONLINE_CHARACTERS: ProcessGetOnlineCharactersQuery(Connection, &Buffer); break; + case QUERY_GET_KILL_STATISTICS: ProcessGetKillStatisticsQuery(Connection, &Buffer); break; + default:{ + LOG_ERR("Unknown query %d from %s", Query, Connection->RemoteAddress); + SendQueryStatusFailed(Connection); + break; + } + } +} +#endif -- cgit v1.2.3