querymanager/src/query.cc
fusion32 d36b26307d prevent password brute force (#4)
Prevent blocked IP addresses and accounts from returning anything
other than a blocked error message. Previously, further attempts
would first check the password which enabled brute force attacks
to use the "invalid account/password" message as an oracle.
2025-10-30 01:03:01 -03:00

1578 lines
56 KiB
C++

#include "querymanager.hh"
#include <pthread.h>
enum : int {
WORKER_STATUS_SPAWNING = 0,
WORKER_STATUS_ACTIVE,
WORKER_STATUS_DONE,
};
struct TQueryQueue{
pthread_mutex_t Mutex;
pthread_cond_t WorkAvailable;
pthread_cond_t RoomAvailable;
uint32 ReadPos;
uint32 WritePos;
uint32 MaxQueries;
TQuery **Queries;
};
struct TWorker{
int WorkerID;
AtomicInt Status;
AtomicInt Stop;
pthread_t Thread;
};
static int g_NumWorkers;
static TWorker *g_Workers;
static TQueryQueue *g_QueryQueue;
// Query Queue and Workers
//==============================================================================
const char *QueryName(int QueryType){
const char *Name = "";
switch(QueryType){
case QUERY_LOGIN: Name = "LOGIN"; break;
case QUERY_INTERNAL_RESOLVE_WORLD: Name = "INTERNAL_RESOLVE_WORLD"; break;
case QUERY_CHECK_ACCOUNT_PASSWORD: Name = "CHECK_ACCOUNT_PASSWORD"; break;
case QUERY_LOGIN_ACCOUNT: Name = "LOGIN_ACCOUNT"; break;
case QUERY_LOGIN_ADMIN: Name = "LOGIN_ADMIN"; break;
case QUERY_LOGIN_GAME: Name = "LOGIN_GAME"; break;
case QUERY_LOGOUT_GAME: Name = "LOGOUT_GAME"; break;
case QUERY_SET_NAMELOCK: Name = "SET_NAMELOCK"; break;
case QUERY_BANISH_ACCOUNT: Name = "BANISH_ACCOUNT"; break;
case QUERY_SET_NOTATION: Name = "SET_NOTATION"; break;
case QUERY_REPORT_STATEMENT: Name = "REPORT_STATEMENT"; break;
case QUERY_BANISH_IP_ADDRESS: Name = "BANISH_IP_ADDRESS"; break;
case QUERY_LOG_CHARACTER_DEATH: Name = "LOG_CHARACTER_DEATH"; break;
case QUERY_ADD_BUDDY: Name = "ADD_BUDDY"; break;
case QUERY_REMOVE_BUDDY: Name = "REMOVE_BUDDY"; break;
case QUERY_DECREMENT_IS_ONLINE: Name = "DECREMENT_IS_ONLINE"; break;
case QUERY_FINISH_AUCTIONS: Name = "FINISH_AUCTIONS"; break;
case QUERY_TRANSFER_HOUSES: Name = "TRANSFER_HOUSES"; break;
case QUERY_EVICT_FREE_ACCOUNTS: Name = "EVICT_FREE_ACCOUNTS"; break;
case QUERY_EVICT_DELETED_CHARACTERS: Name = "EVICT_DELETED_CHARACTERS"; break;
case QUERY_EVICT_EX_GUILDLEADERS: Name = "EVICT_EX_GUILDLEADERS"; break;
case QUERY_INSERT_HOUSE_OWNER: Name = "INSERT_HOUSE_OWNER"; break;
case QUERY_UPDATE_HOUSE_OWNER: Name = "UPDATE_HOUSE_OWNER"; break;
case QUERY_DELETE_HOUSE_OWNER: Name = "DELETE_HOUSE_OWNER"; break;
case QUERY_GET_HOUSE_OWNERS: Name = "GET_HOUSE_OWNERS"; break;
case QUERY_GET_AUCTIONS: Name = "GET_AUCTIONS"; break;
case QUERY_START_AUCTION: Name = "START_AUCTION"; break;
case QUERY_INSERT_HOUSES: Name = "INSERT_HOUSES"; break;
case QUERY_CLEAR_IS_ONLINE: Name = "CLEAR_IS_ONLINE"; break;
case QUERY_CREATE_PLAYERLIST: Name = "CREATE_PLAYERLIST"; break;
case QUERY_LOG_KILLED_CREATURES: Name = "LOG_KILLED_CREATURES"; break;
case QUERY_LOAD_PLAYERS: Name = "LOAD_PLAYERS"; break;
case QUERY_EXCLUDE_FROM_AUCTIONS: Name = "EXCLUDE_FROM_AUCTIONS"; break;
case QUERY_CANCEL_HOUSE_TRANSFER: Name = "CANCEL_HOUSE_TRANSFER"; break;
case QUERY_LOAD_WORLD_CONFIG: Name = "LOAD_WORLD_CONFIG"; break;
case QUERY_CREATE_ACCOUNT: Name = "CREATE_ACCOUNT"; break;
case QUERY_CREATE_CHARACTER: Name = "CREATE_CHARACTER"; break;
case QUERY_GET_ACCOUNT_SUMMARY: Name = "GET_ACCOUNT_SUMMARY"; break;
case QUERY_GET_CHARACTER_PROFILE: Name = "GET_CHARACTER_PROFILE"; break;
case QUERY_GET_WORLDS: Name = "GET_WORLDS"; break;
case QUERY_GET_ONLINE_CHARACTERS: Name = "GET_ONLINE_CHARACTERS"; break;
case QUERY_GET_KILL_STATISTICS: Name = "GET_KILL_STATISTICS"; break;
default: Name = "UNKNOWN"; break;
}
return Name;
}
TQuery *QueryNew(void){
TQuery *Query = (TQuery*)calloc(1, sizeof(TQuery));
AtomicStore(&Query->RefCount, 1);
Query->BufferSize = g_Config.QueryBufferSize;
Query->Buffer = (uint8*)calloc(1, Query->BufferSize);
Query->Request = TReadBuffer{};
Query->Response = TWriteBuffer{};
return Query;
}
void QueryDone(TQuery *Query){
if(Query != NULL){
int RefCount = AtomicFetchAdd(&Query->RefCount, -1);
ASSERT(RefCount >= 1);
if(RefCount == 1){
free(Query->Buffer);
free(Query);
}
}
}
int QueryRefCount(TQuery *Query){
return AtomicLoad(&Query->RefCount);
}
void QueryEnqueue(TQuery *Query){
ASSERT(g_QueryQueue != NULL);
ASSERT(Query != NULL);
// IMPORTANT(fusion): A query object should be referenced by a connection
// and a query queue/worker at most. Anything else, we're gonna have a bad
// time.
int RefCount = 1;
if(!AtomicCompareExchange(&Query->RefCount, &RefCount, 2)){
LOG_ERR("Query already have %d references", RefCount);
return;
}
pthread_mutex_lock(&g_QueryQueue->Mutex);
uint32 NumQueries = g_QueryQueue->WritePos - g_QueryQueue->ReadPos;
uint32 MaxQueries = g_QueryQueue->MaxQueries;
while(NumQueries >= MaxQueries){
LOG_WARN("Execution stalled: queue is full (%u / %u)...",
NumQueries, MaxQueries);
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->WorkAvailable);
}
g_QueryQueue->Queries[g_QueryQueue->WritePos % MaxQueries] = Query;
g_QueryQueue->WritePos += 1;
pthread_mutex_unlock(&g_QueryQueue->Mutex);
}
TQuery *QueryDequeue(AtomicInt *Stop){
ASSERT(g_QueryQueue != NULL);
ASSERT(Stop != NULL);
TQuery *Query = NULL;
pthread_mutex_lock(&g_QueryQueue->Mutex);
uint32 NumQueries = g_QueryQueue->WritePos - g_QueryQueue->ReadPos;
while(NumQueries == 0 && !AtomicLoad(Stop)){
pthread_cond_wait(&g_QueryQueue->WorkAvailable, &g_QueryQueue->Mutex);
NumQueries = g_QueryQueue->WritePos - g_QueryQueue->ReadPos;
}
if(NumQueries > 0 && !AtomicLoad(Stop)){
uint32 MaxQueries = g_QueryQueue->MaxQueries;
if(NumQueries == MaxQueries){
pthread_cond_signal(&g_QueryQueue->RoomAvailable);
}
Query = g_QueryQueue->Queries[g_QueryQueue->ReadPos % MaxQueries];
g_QueryQueue->ReadPos += 1;
}
pthread_mutex_unlock(&g_QueryQueue->Mutex);
return Query;
}
static void *WorkerThread(void *Data){
ASSERT(Data != NULL);
TWorker *Worker = (TWorker*)Data;
if(AtomicLoad(&Worker->Stop)){
LOG_WARN("Worker#%d: Stopping on entry...", Worker->WorkerID);
AtomicStore(&Worker->Status, WORKER_STATUS_DONE);
return NULL;
}
TDatabase *Database = DatabaseOpen();
if(Database == NULL){
LOG_ERR("Worker#%d: Failed to connect to database", Worker->WorkerID);
AtomicStore(&Worker->Status, WORKER_STATUS_DONE);
return NULL;
}
LOG("Worker#%d: ACTIVE...", Worker->WorkerID);
AtomicStore(&Worker->Status, WORKER_STATUS_ACTIVE);
while(TQuery *Query = QueryDequeue(&Worker->Stop)){
void (*ProcessQuery)(TDatabase*, TQuery*) = NULL;
Query->QueryType = Query->Request.Read8();
switch(Query->QueryType){
case QUERY_INTERNAL_RESOLVE_WORLD: ProcessQuery = ProcessInternalResolveWorld; break;
case QUERY_CHECK_ACCOUNT_PASSWORD: ProcessQuery = ProcessCheckAccountPassword; break;
case QUERY_LOGIN_ACCOUNT: ProcessQuery = ProcessLoginAccount; break;
case QUERY_LOGIN_GAME: ProcessQuery = ProcessLoginGame; break;
case QUERY_LOGOUT_GAME: ProcessQuery = ProcessLogoutGame; break;
case QUERY_SET_NAMELOCK: ProcessQuery = ProcessSetNamelock; break;
case QUERY_BANISH_ACCOUNT: ProcessQuery = ProcessBanishAccount; break;
case QUERY_SET_NOTATION: ProcessQuery = ProcessSetNotation; break;
case QUERY_REPORT_STATEMENT: ProcessQuery = ProcessReportStatement; break;
case QUERY_BANISH_IP_ADDRESS: ProcessQuery = ProcessBanishIpAddress; break;
case QUERY_LOG_CHARACTER_DEATH: ProcessQuery = ProcessLogCharacterDeath; break;
case QUERY_ADD_BUDDY: ProcessQuery = ProcessAddBuddy; break;
case QUERY_REMOVE_BUDDY: ProcessQuery = ProcessRemoveBuddy; break;
case QUERY_DECREMENT_IS_ONLINE: ProcessQuery = ProcessDecrementIsOnline; break;
case QUERY_FINISH_AUCTIONS: ProcessQuery = ProcessFinishAuctions; break;
case QUERY_TRANSFER_HOUSES: ProcessQuery = ProcessTransferHouses; break;
case QUERY_EVICT_FREE_ACCOUNTS: ProcessQuery = ProcessEvictFreeAccounts; break;
case QUERY_EVICT_DELETED_CHARACTERS: ProcessQuery = ProcessEvictDeletedCharacters; break;
case QUERY_EVICT_EX_GUILDLEADERS: ProcessQuery = ProcessEvictExGuildleaders; break;
case QUERY_INSERT_HOUSE_OWNER: ProcessQuery = ProcessInsertHouseOwner; break;
case QUERY_UPDATE_HOUSE_OWNER: ProcessQuery = ProcessUpdateHouseOwner; break;
case QUERY_DELETE_HOUSE_OWNER: ProcessQuery = ProcessDeleteHouseOwner; break;
case QUERY_GET_HOUSE_OWNERS: ProcessQuery = ProcessGetHouseOwners; break;
case QUERY_GET_AUCTIONS: ProcessQuery = ProcessGetAuctions; break;
case QUERY_START_AUCTION: ProcessQuery = ProcessStartAuction; break;
case QUERY_INSERT_HOUSES: ProcessQuery = ProcessInsertHouses; break;
case QUERY_CLEAR_IS_ONLINE: ProcessQuery = ProcessClearIsOnline; break;
case QUERY_CREATE_PLAYERLIST: ProcessQuery = ProcessCreatePlayerlist; break;
case QUERY_LOG_KILLED_CREATURES: ProcessQuery = ProcessLogKilledCreatures; break;
case QUERY_LOAD_PLAYERS: ProcessQuery = ProcessLoadPlayers; break;
case QUERY_EXCLUDE_FROM_AUCTIONS: ProcessQuery = ProcessExcludeFromAuctions; break;
case QUERY_CANCEL_HOUSE_TRANSFER: ProcessQuery = ProcessCancelHouseTransfer; break;
case QUERY_LOAD_WORLD_CONFIG: ProcessQuery = ProcessLoadWorldConfig; break;
case QUERY_CREATE_ACCOUNT: ProcessQuery = ProcessCreateAccount; break;
case QUERY_CREATE_CHARACTER: ProcessQuery = ProcessCreateCharacter; break;
case QUERY_GET_ACCOUNT_SUMMARY: ProcessQuery = ProcessGetAccountSummary; break;
case QUERY_GET_CHARACTER_PROFILE: ProcessQuery = ProcessGetCharacterProfile; break;
case QUERY_GET_WORLDS: ProcessQuery = ProcessGetWorlds; break;
case QUERY_GET_ONLINE_CHARACTERS: ProcessQuery = ProcessGetOnlineCharacters; break;
case QUERY_GET_KILL_STATISTICS: ProcessQuery = ProcessGetKillStatistics; break;
}
Query->QueryStatus = QUERY_STATUS_PENDING;
if(ProcessQuery != NULL && DatabaseCheckpoint(Database)){
// NOTE(fusion): A minimum of 1 attempt is ASSUMED.
int Attempts = g_Config.QueryMaxAttempts;
while(true){
ProcessQuery(Database, Query);
if(Query->QueryStatus != QUERY_STATUS_PENDING
|| Attempts <= 0
|| !DatabaseCheckpoint(Database)){
break;
}
Attempts -= 1;
// NOTE(fusion): This one is important because we want to know
// whether some query is failing too often, in which case there
// may be a problem with it.
LOG_WARN("Worker#%d: Query %s failed, retrying...",
Worker->WorkerID, QueryName(Query->QueryType));
}
}
if(Query->QueryStatus == QUERY_STATUS_PENDING){
QueryFailed(Query);
}
QueryDone(Query);
WakeConnections();
}
LOG("Worker#%d: DONE...", Worker->WorkerID);
DatabaseClose(Database);
AtomicStore(&Worker->Status, WORKER_STATUS_DONE);
return NULL;
}
bool InitQuery(void){
ASSERT(g_QueryQueue == NULL);
ASSERT(g_Workers == NULL);
// IMPORTANT(fusion): We'd ideally have a single query per connection at any
// given time but, in reality, connections could be reset while their queries
// are still in a query queue/worker, increasing the maximum number of queries
// in flight.
g_QueryQueue = (TQueryQueue*)calloc(1, sizeof(TQueryQueue));
pthread_mutex_init(&g_QueryQueue->Mutex, NULL);
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 = g_Config.QueryWorkerThreads;
if(g_NumWorkers > DatabaseMaxConcurrency()){
g_NumWorkers = DatabaseMaxConcurrency();
}
g_Workers = (TWorker*)calloc(g_NumWorkers, sizeof(TWorker));
for(int i = 0; i < g_NumWorkers; i += 1){
TWorker *Worker = &g_Workers[i];
Worker->WorkerID = i;
AtomicStore(&Worker->Status, WORKER_STATUS_SPAWNING);
AtomicStore(&Worker->Stop, 0);
int ErrorCode = pthread_create(&Worker->Thread, NULL, WorkerThread, Worker);
if(ErrorCode != 0){
LOG_ERR("Failed to spawn worker thread %d: (%d) %s",
i, ErrorCode, strerrordesc_np(ErrorCode));
return false;
}
}
while(true){
int NumWorkersSpawning = 0;
int NumWorkersActive = 0;
int NumWorkersDone = 0;
for(int i = 0; i < g_NumWorkers; i += 1){
int Status = AtomicLoad(&g_Workers[i].Status);
if(Status == WORKER_STATUS_SPAWNING){
NumWorkersSpawning += 1;
}else if(Status == WORKER_STATUS_ACTIVE){
NumWorkersActive += 1;
}else if(Status == WORKER_STATUS_DONE){
NumWorkersDone += 1;
}
}
if(NumWorkersSpawning > 0){
LOG("Waiting on worker threads... (SPAWNING=%d, ACTIVE=%d, DONE=%d)",
NumWorkersSpawning, NumWorkersActive, NumWorkersDone);
SleepMS(500);
continue;
}
if(NumWorkersDone > 0){
LOG_ERR("%d worker thread%s failed to initialize",
NumWorkersDone, (NumWorkersDone == 1 ? "" : "s"));
return false;
}
ASSERT(NumWorkersActive == g_NumWorkers);
break;
}
return true;
}
void ExitQuery(void){
if(g_Workers != NULL){
ASSERT(g_QueryQueue != NULL);
for(int i = 0; i < g_NumWorkers; i += 1){
AtomicStore(&g_Workers[i].Stop, 1);
}
pthread_cond_broadcast(&g_QueryQueue->WorkAvailable);
for(int i = 0; i < g_NumWorkers; i += 1){
// IMPORTANT(fusion): There is no "invalid" pthread handle so this
// is non-standard behaviour. Nevertheless the game server uses it
// and it seems to be consistent on Linux, which is what matters at
// the end of the day.
if(g_Workers[i].Thread != 0){
pthread_join(g_Workers[i].Thread, NULL);
}
}
free(g_Workers);
}
if(g_QueryQueue != NULL){
pthread_mutex_destroy(&g_QueryQueue->Mutex);
pthread_cond_destroy(&g_QueryQueue->WorkAvailable);
pthread_cond_destroy(&g_QueryQueue->RoomAvailable);
// TODO(fusion): Abort queries instead?
uint32 MaxQueries = g_QueryQueue->MaxQueries;
for(uint32 ReadPos = g_QueryQueue->ReadPos;
ReadPos != g_QueryQueue->WritePos;
ReadPos += 1){
QueryDone(g_QueryQueue->Queries[ReadPos % MaxQueries]);
}
free(g_QueryQueue->Queries);
free(g_QueryQueue);
}
}
// 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){
ASSERT(Status != QUERY_STATUS_PENDING);
Query->QueryStatus = Status;
Query->Response = TWriteBuffer(Query->Buffer, Query->BufferSize);
Query->Response.Write16(0);
Query->Response.Write8((uint8)Status);
return &Query->Response;
}
bool QueryFinishResponse(TQuery *Query){
TWriteBuffer *Response = &Query->Response;
if(Response->Position <= 2){
LOG_ERR("Invalid response size");
return false;
}
int PayloadSize = Response->Position - 2;
if(PayloadSize < 0xFFFF){
Response->Rewrite16(0, (uint16)PayloadSize);
}else{
Response->Rewrite16(0, 0xFFFF);
Response->Insert32(2, (uint32)PayloadSize);
}
return !Response->Overflowed();
}
void QueryOk(TQuery *Query){
QueryBeginResponse(Query, QUERY_STATUS_OK);
QueryFinishResponse(Query);
}
void QueryError(TQuery *Query, int ErrorCode){
TWriteBuffer *Response = QueryBeginResponse(Query, QUERY_STATUS_ERROR);
Response->Write8((uint8)ErrorCode);
QueryFinishResponse(Query);
}
void QueryFailed(TQuery *Query){
QueryBeginResponse(Query, QUERY_STATUS_FAILED);
QueryFinishResponse(Query);
}
// Query Helpers
//==============================================================================
static 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;
}
}
}
// Query Processing
//==============================================================================
// IMPORTANT(fusion): Query processing functions are expected to signal their status
// by updating `Query->QueryStatus`. A query that is still `PENDING` at the end of
// processing is considered unfinished and may be retried.
// The query response should only be written at the very end, when we know it has
// SUCCEEDED, so that request data is kept intact (because they share the same query
// buffer) if the caller decides to retry it.
// Since using `QueryBeginResponse` automatically sets `Query->QueryStatus`, it
// should be unlikely that the request data is modified while keeping a `PENDING`
// status.
// NOTE(fusion): These should be used with query functions to reduce clutter.
#define QUERY_STOP_IF(Cond) \
do{ \
if(Cond){ \
Query->QueryStatus = QUERY_STATUS_PENDING; \
return; \
} \
}while(0)
#define QUERY_ERROR_IF(Cond, ErrorCode) \
do{ \
if(Cond){ \
QueryError(Query, ErrorCode); \
return; \
} \
}while(0)
#define QUERY_FAIL_IF(Cond) \
do{ \
if(Cond){ \
QueryFailed(Query); \
return; \
} \
}while(0)
void ProcessInternalResolveWorld(TDatabase *Database, TQuery *Query){
TReadBuffer Request = Query->Request;
char World[30];
Request.ReadString(World, sizeof(World));
int WorldID;
QUERY_STOP_IF(!GetWorldID(Database, World, &WorldID));
QUERY_FAIL_IF(WorldID <= 0);
Query->WorldID = WorldID;
QueryOk(Query);
}
static void CheckAccountPasswordTx(TDatabase *Database, TQuery *Query,
int AccountID, const char *Password, int IPAddress){
TransactionScope Tx("CheckAccountPassword");
QUERY_STOP_IF(!Tx.Begin(Database));
// IMPORTANT(fusion): Disallow blocked IP addresses and accounts early to
// prevent error messages from being used to brute force passwords.
int FailedLoginAttempts;
QUERY_STOP_IF(!GetIPAddressFailedLoginAttempts(Database, IPAddress, 30 * 60, &FailedLoginAttempts));
QUERY_ERROR_IF(FailedLoginAttempts > 20, 4);
QUERY_STOP_IF(!GetAccountFailedLoginAttempts(Database, AccountID, 5 * 60, &FailedLoginAttempts));
QUERY_ERROR_IF(FailedLoginAttempts > 10, 3);
TAccount Account;
QUERY_STOP_IF(!GetAccountData(Database, AccountID, &Account));
QUERY_ERROR_IF(Account.AccountID == 0, 1);
QUERY_ERROR_IF(!TestPassword(Account.Auth, sizeof(Account.Auth), Password), 2);
QUERY_STOP_IF(!Tx.Commit());
QueryOk(Query);
}
void ProcessCheckAccountPassword(TDatabase *Database, TQuery *Query){
TReadBuffer Request = Query->Request;
char Password[30];
char IPString[16];
int AccountID = (int)Request.Read32();
Request.ReadString(Password, sizeof(Password));
Request.ReadString(IPString, sizeof(IPString));
int IPAddress = 0;
QUERY_FAIL_IF(!ParseIPAddress(&IPAddress, IPString));
// NOTE(fusion): Same as `ProcessLoginGame`.
CheckAccountPasswordTx(Database, Query, AccountID, Password, IPAddress);
if(Query->QueryStatus != QUERY_STATUS_PENDING){
InsertLoginAttempt(Database, AccountID, IPAddress,
(Query->QueryStatus != QUERY_STATUS_OK));
}
}
static void LoginAccountTx(TDatabase *Database, TQuery *Query,
int AccountID, const char *Password, int IPAddress){
TransactionScope Tx("LoginAccount");
QUERY_STOP_IF(!Tx.Begin(Database));
// IMPORTANT(fusion): Disallow blocked IP addresses and accounts early to
// prevent error messages from being used to brute force passwords.
int FailedLoginAttempts;
QUERY_STOP_IF(!GetIPAddressFailedLoginAttempts(Database, IPAddress, 30 * 60, &FailedLoginAttempts));
QUERY_ERROR_IF(FailedLoginAttempts > 20, 4);
QUERY_STOP_IF(!GetAccountFailedLoginAttempts(Database, AccountID, 5 * 60, &FailedLoginAttempts));
QUERY_ERROR_IF(FailedLoginAttempts > 10, 3);
TAccount Account;
QUERY_STOP_IF(!GetAccountData(Database, AccountID, &Account));
QUERY_ERROR_IF(Account.AccountID == 0, 1);
QUERY_ERROR_IF(!TestPassword(Account.Auth, sizeof(Account.Auth), Password), 2);
bool IsBanished;
QUERY_STOP_IF(!IsAccountBanished(Database, Account.AccountID, &IsBanished));
QUERY_ERROR_IF(IsBanished, 5);
QUERY_STOP_IF(!IsIPBanished(Database, IPAddress, &IsBanished));
QUERY_ERROR_IF(IsBanished, 6);
DynamicArray<TCharacterEndpoint> Characters;
QUERY_STOP_IF(!GetCharacterEndpoints(Database, Account.AccountID, &Characters));
QUERY_STOP_IF(!Tx.Commit());
TWriteBuffer *Response = QueryBeginResponse(Query, QUERY_STATUS_OK);
int NumCharacters = std::min<int>(Characters.Length(), UINT8_MAX);
Response->Write8((uint8)NumCharacters);
for(int i = 0; i < NumCharacters; i += 1){
Response->WriteString(Characters[i].Name);
Response->WriteString(Characters[i].WorldName);
int WorldAddress;
if(ResolveHostName(Characters[i].WorldHost, &WorldAddress)){
Response->Write32BE(WorldAddress);
Response->Write16(Characters[i].WorldPort);
}else{
LOG_ERR("Failed to resolve world \"%s\" host name \"%s\" for character \"%s\"",
Characters[i].WorldName, Characters[i].WorldHost, Characters[i].Name);
Response->Write32BE(0);
Response->Write16(0);
}
}
Response->Write16((uint16)(Account.PremiumDays + Account.PendingPremiumDays));
QueryFinishResponse(Query);
}
void ProcessLoginAccount(TDatabase *Database, TQuery *Query){
TReadBuffer Request = Query->Request;
char Password[30];
char IPString[16];
int AccountID = (int)Request.Read32();
Request.ReadString(Password, sizeof(Password));
Request.ReadString(IPString, sizeof(IPString));
int IPAddress = 0;
QUERY_FAIL_IF(!ParseIPAddress(&IPAddress, IPString));
// NOTE(fusion): Same as `ProcessLoginGame`.
LoginAccountTx(Database, Query, AccountID, Password, IPAddress);
if(Query->QueryStatus != QUERY_STATUS_PENDING){
InsertLoginAttempt(Database, AccountID, IPAddress,
(Query->QueryStatus != QUERY_STATUS_OK));
}
}
static void LoginGameTx(TDatabase *Database, TQuery *Query,
int AccountID, const char *CharacterName, const char *Password,
int IPAddress, bool PrivateWorld, bool GamemasterRequired){
TransactionScope Tx("LoginGame");
QUERY_STOP_IF(!Tx.Begin(Database));
// IMPORTANT(fusion): Disallow blocked IP addresses and accounts early to
// prevent error messages from being used to brute force passwords.
int FailedLoginAttempts;
QUERY_STOP_IF(!GetIPAddressFailedLoginAttempts(Database, IPAddress, 30 * 60, &FailedLoginAttempts));
QUERY_ERROR_IF(FailedLoginAttempts > 20, 9);
QUERY_STOP_IF(!GetAccountFailedLoginAttempts(Database, AccountID, 5 * 60, &FailedLoginAttempts));
QUERY_ERROR_IF(FailedLoginAttempts > 10, 7);
TCharacterLoginData Character;
QUERY_STOP_IF(!GetCharacterLoginData(Database, CharacterName, &Character));
QUERY_ERROR_IF(Character.CharacterID == 0, 1);
QUERY_ERROR_IF(Character.Deleted, 2);
QUERY_ERROR_IF(Character.WorldID != Query->WorldID, 3);
if(PrivateWorld){
bool Invited;
QUERY_STOP_IF(!GetWorldInvitation(Database, Query->WorldID, Character.CharacterID, &Invited));
QUERY_ERROR_IF(!Invited, 4);
}
TAccount Account;
QUERY_STOP_IF(!GetAccountData(Database, AccountID, &Account));
// NOTE(fusion): This is correct, there is no error code 5.
QUERY_ERROR_IF(Account.AccountID == 0 || Account.AccountID != Character.AccountID, 15);
QUERY_ERROR_IF(Account.Deleted, 8);
QUERY_ERROR_IF(!TestPassword(Account.Auth, sizeof(Account.Auth), Password), 6);
bool IsBanished;
QUERY_STOP_IF(!IsAccountBanished(Database, Account.AccountID, &IsBanished));
QUERY_ERROR_IF(IsBanished, 10);
QUERY_STOP_IF(!IsCharacterNamelocked(Database, Character.CharacterID, &IsBanished));
QUERY_ERROR_IF(IsBanished, 11);
QUERY_STOP_IF(!IsIPBanished(Database, IPAddress, &IsBanished));
QUERY_ERROR_IF(IsBanished, 12);
// TODO(fusion): Probably merge these into a single operation?
bool MultiClient;
QUERY_STOP_IF(!GetCharacterRight(Database, Character.CharacterID, "ALLOW_MULTICLIENT", &MultiClient));
if(!MultiClient){
int OnlineCharacters;
QUERY_STOP_IF(!GetAccountOnlineCharacters(Database, Account.AccountID, &OnlineCharacters));
if(OnlineCharacters > 0){
bool IsOnline;
QUERY_STOP_IF(!IsCharacterOnline(Database, Character.CharacterID, &IsOnline));
QUERY_ERROR_IF(!IsOnline, 13);
}
}
if(GamemasterRequired){
bool GamemasterOutfit;
QUERY_STOP_IF(!GetCharacterRight(Database, Character.CharacterID, "GAMEMASTER_OUTFIT", &GamemasterOutfit));
QUERY_ERROR_IF(!GamemasterOutfit, 14);
}
TCharacterGuildData GuildData;
QUERY_STOP_IF(!GetCharacterGuildData(Database, Character.CharacterID, &GuildData));
DynamicArray<TAccountBuddy> Buddies;
QUERY_STOP_IF(!GetBuddies(Database, Query->WorldID, Account.AccountID, &Buddies));
DynamicArray<TCharacterRight> Rights;
QUERY_STOP_IF(!GetCharacterRights(Database, Character.CharacterID, &Rights));
bool PremiumAccountActivated = false;
if(Account.PremiumDays == 0 && Account.PendingPremiumDays > 0){
QUERY_STOP_IF(!ActivatePendingPremiumDays(Database, Account.AccountID));
Account.PremiumDays += Account.PendingPremiumDays;
Account.PendingPremiumDays = 0;
PremiumAccountActivated = true;
}
if(Account.PremiumDays > 0){
Rights.Push(TCharacterRight{"PREMIUM_ACCOUNT"});
}
QUERY_STOP_IF(!IncrementIsOnline(Database, Query->WorldID, Character.CharacterID));
QUERY_STOP_IF(!Tx.Commit());
TWriteBuffer *Response = QueryBeginResponse(Query, QUERY_STATUS_OK);
Response->Write32((uint32)Character.CharacterID);
Response->WriteString(Character.Name);
Response->Write8((uint8)Character.Sex);
Response->WriteString(GuildData.GuildName);
Response->WriteString(GuildData.RankName);
Response->WriteString(GuildData.Title);
int NumBuddies = std::min<int>(Buddies.Length(), UINT8_MAX);
Response->Write8((uint8)NumBuddies);
for(int i = 0; i < NumBuddies; i += 1){
Response->Write32((uint32)Buddies[i].CharacterID);
Response->WriteString(Buddies[i].Name);
}
int NumRights = std::min<int>(Rights.Length(), UINT8_MAX);
Response->Write8((uint8)NumRights);
for(int i = 0; i < NumRights; i += 1){
Response->WriteString(Rights[i].Name);
}
Response->WriteFlag(PremiumAccountActivated);
QueryFinishResponse(Query);
}
void ProcessLoginGame(TDatabase *Database, TQuery *Query){
TReadBuffer Request = Query->Request;
char CharacterName[30];
char Password[30];
char IPString[16];
int AccountID = (int)Request.Read32();
Request.ReadString(CharacterName, sizeof(CharacterName));
Request.ReadString(Password, sizeof(Password));
Request.ReadString(IPString, sizeof(IPString));
bool PrivateWorld = Request.ReadFlag();
Request.ReadFlag(); // "PremiumAccountRequired" unused
bool GamemasterRequired = Request.ReadFlag();
int IPAddress;
QUERY_FAIL_IF(!ParseIPAddress(&IPAddress, IPString));
// 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.
LoginGameTx(Database, Query, AccountID, CharacterName,
Password, IPAddress, PrivateWorld, GamemasterRequired);
if(Query->QueryStatus != QUERY_STATUS_PENDING){
InsertLoginAttempt(Database, AccountID, IPAddress,
(Query->QueryStatus != QUERY_STATUS_OK));
}
}
void ProcessLogoutGame(TDatabase *Database, TQuery *Query){
TReadBuffer Request = Query->Request;
char Profession[30];
char Residence[30];
int CharacterID = (int)Request.Read32();
int Level = Request.Read16();
Request.ReadString(Profession, sizeof(Profession));
Request.ReadString(Residence, sizeof(Residence));
int LastLoginTime = (int)Request.Read32();
int TutorActivities = Request.Read16();
QUERY_STOP_IF(!LogoutCharacter(Database, Query->WorldID, CharacterID,
Level, Profession, Residence, LastLoginTime, TutorActivities));
QueryOk(Query);
}
void ProcessSetNamelock(TDatabase *Database, TQuery *Query){
TReadBuffer Request = Query->Request;
char CharacterName[30];
char IPString[16];
char Reason[200];
char Comment[200];
int GamemasterID = (int)Request.Read32();
Request.ReadString(CharacterName, sizeof(CharacterName));
Request.ReadString(IPString, sizeof(IPString));
Request.ReadString(Reason, sizeof(Reason));
Request.ReadString(Comment, sizeof(Comment));
int IPAddress = 0;
QUERY_FAIL_IF(!StringEmpty(IPString) && !ParseIPAddress(&IPAddress, IPString));
TransactionScope Tx("SetNamelock");
QUERY_STOP_IF(!Tx.Begin(Database));
int CharacterID;
QUERY_STOP_IF(!GetCharacterID(Database, Query->WorldID, CharacterName, &CharacterID));
QUERY_ERROR_IF(CharacterID == 0, 1);
// TODO(fusion): Might be `NO_BANISHMENT`.
bool Namelock;
QUERY_STOP_IF(!GetCharacterRight(Database, CharacterID, "NAMELOCK", &Namelock));
QUERY_ERROR_IF(Namelock, 2);
TNamelockStatus Status;
QUERY_STOP_IF(!GetNamelockStatus(Database, CharacterID, &Status));
QUERY_ERROR_IF(Status.Namelocked, (Status.Approved ? 4 : 3));
QUERY_STOP_IF(!InsertNamelock(Database, CharacterID, IPAddress, GamemasterID, Reason, Comment));
QUERY_STOP_IF(!Tx.Commit());
QueryOk(Query);
}
void ProcessBanishAccount(TDatabase *Database, TQuery *Query){
TReadBuffer Request = Query->Request;
char CharacterName[30];
char IPString[16];
char Reason[200];
char Comment[200];
int GamemasterID = (int)Request.Read32();
Request.ReadString(CharacterName, sizeof(CharacterName));
Request.ReadString(IPString, sizeof(IPString));
Request.ReadString(Reason, sizeof(Reason));
Request.ReadString(Comment, sizeof(Comment));
bool FinalWarning = Request.ReadFlag();
int IPAddress = 0;
QUERY_FAIL_IF(!StringEmpty(IPString) && !ParseIPAddress(&IPAddress, IPString));
TransactionScope Tx("BanishAccount");
QUERY_STOP_IF(!Tx.Begin(Database));
int CharacterID;
QUERY_STOP_IF(!GetCharacterID(Database, Query->WorldID, CharacterName, &CharacterID));
QUERY_ERROR_IF(CharacterID == 0, 1);
// TODO(fusion): Might be `NO_BANISHMENT`.
bool Banishment;
QUERY_STOP_IF(!GetCharacterRight(Database, CharacterID, "BANISHMENT", &Banishment));
QUERY_ERROR_IF(Banishment, 2);
TBanishmentStatus Status;
QUERY_STOP_IF(!GetBanishmentStatus(Database, CharacterID, &Status));
QUERY_ERROR_IF(Status.Banished, 3);
int Days = 7;
int BanishmentID = 0;
CompoundBanishment(Status, &Days, &FinalWarning);
QUERY_STOP_IF(!InsertBanishment(Database, CharacterID, IPAddress,
GamemasterID, Reason, Comment, FinalWarning, Days * 86400,
&BanishmentID));
QUERY_STOP_IF(!Tx.Commit());
TWriteBuffer *Response = QueryBeginResponse(Query, QUERY_STATUS_OK);
Response->Write32((uint32)BanishmentID);
Response->Write8(Days > 0 ? Days : 0xFF);
Response->WriteFlag(FinalWarning);
QueryFinishResponse(Query);
}
void ProcessSetNotation(TDatabase *Database, TQuery *Query){
TReadBuffer Request = Query->Request;
char CharacterName[30];
char IPString[16];
char Reason[200];
char Comment[200];
int GamemasterID = (int)Request.Read32();
Request.ReadString(CharacterName, sizeof(CharacterName));
Request.ReadString(IPString, sizeof(IPString));
Request.ReadString(Reason, sizeof(Reason));
Request.ReadString(Comment, sizeof(Comment));
int IPAddress = 0;
QUERY_FAIL_IF(!StringEmpty(IPString) && !ParseIPAddress(&IPAddress, IPString));
TransactionScope Tx("SetNotation");
QUERY_STOP_IF(!Tx.Begin(Database));
int CharacterID;
QUERY_STOP_IF(!GetCharacterID(Database, Query->WorldID, CharacterName, &CharacterID));
QUERY_ERROR_IF(CharacterID == 0, 1);
// TODO(fusion): Might be `NO_BANISHMENT`.
bool Notation;
QUERY_STOP_IF(!GetCharacterRight(Database, CharacterID, "NOTATION", &Notation));
QUERY_ERROR_IF(Notation, 2);
int Notations = 0;
int BanishmentID = 0;
QUERY_STOP_IF(!GetNotationCount(Database, CharacterID, &Notations));
if(Notations >= 5){
int BanishmentDays = 7;
bool FinalWarning = false;
TBanishmentStatus Status = {};
QUERY_STOP_IF(!GetBanishmentStatus(Database, CharacterID, &Status));
CompoundBanishment(Status, &BanishmentDays, &FinalWarning);
QUERY_STOP_IF(!InsertBanishment(Database, CharacterID, IPAddress,
0, "Excessive Notations", "", FinalWarning, BanishmentDays,
&BanishmentID));
}
QUERY_STOP_IF(!InsertNotation(Database, CharacterID, IPAddress, GamemasterID, Reason, Comment));
QUERY_STOP_IF(!Tx.Commit());
TWriteBuffer *Response = QueryBeginResponse(Query, QUERY_STATUS_OK);
Response->Write32((uint32)BanishmentID);
QueryFinishResponse(Query);
}
void ProcessReportStatement(TDatabase *Database, TQuery *Query){
TReadBuffer Request = Query->Request;
char CharacterName[30];
char Reason[200];
char Comment[200];
int ReporterID = (int)Request.Read32();
Request.ReadString(CharacterName, sizeof(CharacterName));
Request.ReadString(Reason, sizeof(Reason));
Request.ReadString(Comment, sizeof(Comment));
int BanishmentID = (int)Request.Read32();
int StatementID = (int)Request.Read32();
int NumStatements = (int)Request.Read16();
if(StatementID == 0){
LOG_ERR("Missing statement id");
QueryFailed(Query);
return;
}
if(NumStatements == 0){
LOG_ERR("Missing statement context");
QueryFailed(Query);
return;
}
TStatement *ReportedStatement = NULL;
TStatement *Statements = (TStatement*)alloca(NumStatements * sizeof(TStatement));
for(int i = 0; i < NumStatements; i += 1){
Statements[i].StatementID = (int)Request.Read32();
Statements[i].Timestamp = (int)Request.Read32();
Statements[i].CharacterID = (int)Request.Read32();
Request.ReadString(Statements[i].Channel, sizeof(Statements[i].Channel));
Request.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",
Query->WorldID, Statements[i].Timestamp, Statements[i].StatementID);
}
ReportedStatement = &Statements[i];
}
}
if(ReportedStatement == NULL){
LOG_ERR("Missing reported statement");
QueryFailed(Query);
return;
}
TransactionScope Tx("ReportStatement");
QUERY_STOP_IF(!Tx.Begin(Database));
int CharacterID;
QUERY_STOP_IF(!GetCharacterID(Database, Query->WorldID, CharacterName, &CharacterID));
QUERY_ERROR_IF(CharacterID == 0, 1);
if(ReportedStatement->CharacterID != CharacterID){
LOG_ERR("Reported statement character mismatch");
QueryFailed(Query);
return;
}
bool IsReported;
QUERY_STOP_IF(!IsStatementReported(Database, Query->WorldID, ReportedStatement, &IsReported));
QUERY_ERROR_IF(IsReported, 2);
QUERY_STOP_IF(!InsertStatements(Database, Query->WorldID, NumStatements, Statements));
QUERY_STOP_IF(!InsertReportedStatement(Database, Query->WorldID, ReportedStatement,
BanishmentID, ReporterID, Reason, Comment));
QUERY_STOP_IF(!Tx.Commit());
QueryOk(Query);
}
void ProcessBanishIpAddress(TDatabase *Database, TQuery *Query){
TReadBuffer Request = Query->Request;
char CharacterName[30];
char IPString[16];
char Reason[200];
char Comment[200];
int GamemasterID = (int)Request.Read16();
Request.ReadString(CharacterName, sizeof(CharacterName));
Request.ReadString(IPString, sizeof(IPString));
Request.ReadString(Reason, sizeof(Reason));
Request.ReadString(Comment, sizeof(Comment));
int IPAddress;
QUERY_FAIL_IF(!ParseIPAddress(&IPAddress, IPString));
TransactionScope Tx("BanishIP");
QUERY_STOP_IF(!Tx.Begin(Database));
int CharacterID;
QUERY_STOP_IF(!GetCharacterID(Database, Query->WorldID, CharacterName, &CharacterID));
QUERY_ERROR_IF(CharacterID == 0, 1);
// TODO(fusion): Might be `NO_BANISHMENT`.
bool IPBanishment;
QUERY_STOP_IF(!GetCharacterRight(Database, CharacterID, "IP_BANISHMENT", &IPBanishment));
QUERY_ERROR_IF(IPBanishment, 2);
// IMPORTANT(fusion): It is not a good idea to ban IP addresses, specially V4,
// as they may be dynamically assigned or represent the address of a public ISP
// router that manages multiple clients.
int BanishmentDays = 3;
QUERY_STOP_IF(!InsertIPBanishment(Database, CharacterID, IPAddress,
GamemasterID, Reason, Comment, BanishmentDays * 86400));
QUERY_STOP_IF(!Tx.Commit());
QueryOk(Query);
}
void ProcessLogCharacterDeath(TDatabase *Database, TQuery *Query){
TReadBuffer Request = Query->Request;
char Remark[30];
int CharacterID = (int)Request.Read32();
int Level = (int)Request.Read16();
int OffenderID = (int)Request.Read32();
Request.ReadString(Remark, sizeof(Remark));
bool Unjustified = Request.ReadFlag();
int Timestamp = (int)Request.Read32();
QUERY_STOP_IF(!InsertCharacterDeath(Database, Query->WorldID, CharacterID,
Level, OffenderID, Remark, Unjustified, Timestamp));
QueryOk(Query);
}
void ProcessAddBuddy(TDatabase *Database, TQuery *Query){
TReadBuffer Request = Query->Request;
int AccountID = (int)Request.Read32();
int BuddyID = (int)Request.Read32();
QUERY_STOP_IF(!InsertBuddy(Database, Query->WorldID, AccountID, BuddyID));
QueryOk(Query);
}
void ProcessRemoveBuddy(TDatabase *Database, TQuery *Query){
TReadBuffer Request = Query->Request;
int AccountID = (int)Request.Read32();
int BuddyID = (int)Request.Read32();
QUERY_STOP_IF(!DeleteBuddy(Database, Query->WorldID, AccountID, BuddyID));
QueryOk(Query);
}
void ProcessDecrementIsOnline(TDatabase *Database, TQuery *Query){
TReadBuffer Request = Query->Request;
int CharacterID = (int)Request.Read32();
QUERY_STOP_IF(!DecrementIsOnline(Database, Query->WorldID, CharacterID));
QueryOk(Query);
}
void ProcessFinishAuctions(TDatabase *Database, TQuery *Query){
DynamicArray<THouseAuction> Auctions;
QUERY_STOP_IF(!FinishHouseAuctions(Database, Query->WorldID, &Auctions));
TWriteBuffer *Response = QueryBeginResponse(Query, QUERY_STATUS_OK);
int NumAuctions = std::min<int>(Auctions.Length(), UINT16_MAX);
Response->Write16((uint16)NumAuctions);
for(int i = 0; i < NumAuctions; i += 1){
Response->Write16((uint16)Auctions[i].HouseID);
Response->Write32((uint32)Auctions[i].BidderID);
Response->WriteString(Auctions[i].BidderName);
Response->Write32((uint32)Auctions[i].BidAmount);
}
QueryFinishResponse(Query);
}
void ProcessTransferHouses(TDatabase *Database, TQuery *Query){
DynamicArray<THouseTransfer> Transfers;
QUERY_STOP_IF(!FinishHouseTransfers(Database, Query->WorldID, &Transfers));
TWriteBuffer *Response = QueryBeginResponse(Query, QUERY_STATUS_OK);
int NumTransfers = std::min<int>(Transfers.Length(), UINT16_MAX);
Response->Write16((uint16)NumTransfers);
for(int i = 0; i < NumTransfers; i += 1){
Response->Write16((uint16)Transfers[i].HouseID);
Response->Write32((uint32)Transfers[i].NewOwnerID);
Response->WriteString(Transfers[i].NewOwnerName);
Response->Write32((uint32)Transfers[i].Price);
}
QueryFinishResponse(Query);
}
void ProcessEvictFreeAccounts(TDatabase *Database, TQuery *Query){
DynamicArray<THouseEviction> Evictions;
QUERY_STOP_IF(!GetFreeAccountEvictions(Database, Query->WorldID, &Evictions));
TWriteBuffer *Response = QueryBeginResponse(Query, QUERY_STATUS_OK);
int NumEvictions = std::min<int>(Evictions.Length(), UINT16_MAX);
Response->Write16((uint16)NumEvictions);
for(int i = 0; i < NumEvictions; i += 1){
Response->Write16((uint16)Evictions[i].HouseID);
Response->Write32((uint32)Evictions[i].OwnerID);
}
QueryFinishResponse(Query);
}
void ProcessEvictDeletedCharacters(TDatabase *Database, TQuery *Query){
DynamicArray<THouseEviction> Evictions;
QUERY_STOP_IF(!GetDeletedCharacterEvictions(Database, Query->WorldID, &Evictions));
TWriteBuffer *Response = QueryBeginResponse(Query, QUERY_STATUS_OK);
int NumEvictions = std::min<int>(Evictions.Length(), UINT16_MAX);
Response->Write16((uint16)NumEvictions);
for(int i = 0; i < NumEvictions; i += 1){
Response->Write16((uint16)Evictions[i].HouseID);
}
QueryFinishResponse(Query);
}
void ProcessEvictExGuildleaders(TDatabase *Database, TQuery *Query){
// 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<int> Evictions;
TReadBuffer Request = Query->Request;
int NumGuildHouses = (int)Request.Read16();
for(int i = 0; i < NumGuildHouses; i += 1){
int HouseID = (int)Request.Read16();
int OwnerID = (int)Request.Read32();
bool IsGuildLeader;
QUERY_STOP_IF(!GetGuildLeaderStatus(Database, Query->WorldID, OwnerID, &IsGuildLeader));
if(IsGuildLeader){
Evictions.Push(HouseID);
}
}
TWriteBuffer *Response = QueryBeginResponse(Query, QUERY_STATUS_OK);
int NumEvictions = std::min<int>(Evictions.Length(), UINT16_MAX);
Response->Write16((uint16)NumEvictions);
for(int i = 0; i < NumEvictions; i += 1){
Response->Write16((uint16)Evictions[i]);
}
QueryFinishResponse(Query);
}
void ProcessInsertHouseOwner(TDatabase *Database, TQuery *Query){
TReadBuffer Request = Query->Request;
int HouseID = (int)Request.Read16();
int OwnerID = (int)Request.Read32();
int PaidUntil = (int)Request.Read32();
QUERY_STOP_IF(!InsertHouseOwner(Database, Query->WorldID, HouseID, OwnerID, PaidUntil));
QueryOk(Query);
}
void ProcessUpdateHouseOwner(TDatabase *Database, TQuery *Query){
TReadBuffer Request = Query->Request;
int HouseID = (int)Request.Read16();
int OwnerID = (int)Request.Read32();
int PaidUntil = (int)Request.Read32();
QUERY_STOP_IF(!UpdateHouseOwner(Database, Query->WorldID, HouseID, OwnerID, PaidUntil));
QueryOk(Query);
}
void ProcessDeleteHouseOwner(TDatabase *Database, TQuery *Query){
TReadBuffer Request = Query->Request;
int HouseID = (int)Request.Read16();
QUERY_STOP_IF(!DeleteHouseOwner(Database, Query->WorldID, HouseID));
QueryOk(Query);
}
void ProcessGetHouseOwners(TDatabase *Database, TQuery *Query){
DynamicArray<THouseOwner> Owners;
QUERY_STOP_IF(!GetHouseOwners(Database, Query->WorldID, &Owners));
TWriteBuffer *Response = QueryBeginResponse(Query, QUERY_STATUS_OK);
int NumOwners = std::min<int>(Owners.Length(), UINT16_MAX);
Response->Write16((uint16)NumOwners);
for(int i = 0; i < NumOwners; i += 1){
Response->Write16((uint16)Owners[i].HouseID);
Response->Write32((uint32)Owners[i].OwnerID);
Response->WriteString(Owners[i].OwnerName);
Response->Write32((uint32)Owners[i].PaidUntil);
}
QueryFinishResponse(Query);
}
void ProcessGetAuctions(TDatabase *Database, TQuery *Query){
DynamicArray<int> Auctions;
QUERY_STOP_IF(!GetHouseAuctions(Database, Query->WorldID, &Auctions));
TWriteBuffer *Response = QueryBeginResponse(Query, QUERY_STATUS_OK);
int NumAuctions = std::min<int>(Auctions.Length(), UINT16_MAX);
Response->Write16((uint16)NumAuctions);
for(int i = 0; i < NumAuctions; i += 1){
Response->Write16((uint16)Auctions[i]);
}
QueryFinishResponse(Query);
}
void ProcessStartAuction(TDatabase *Database, TQuery *Query){
TReadBuffer Request = Query->Request;
int HouseID = (int)Request.Read16();
QUERY_STOP_IF(!StartHouseAuction(Database, Query->WorldID, HouseID));
QueryOk(Query);
}
void ProcessInsertHouses(TDatabase *Database, TQuery *Query){
TReadBuffer Request = Query->Request;
TransactionScope Tx("InsertHouses");
QUERY_STOP_IF(!Tx.Begin(Database));
QUERY_STOP_IF(!DeleteHouses(Database, Query->WorldID));
int NumHouses = (int)Request.Read16();
if(NumHouses > 0){
THouse *Houses = (THouse*)alloca(NumHouses * sizeof(THouse));
for(int i = 0; i < NumHouses; i += 1){
Houses[i].HouseID = (int)Request.Read16();
Request.ReadString(Houses[i].Name, sizeof(Houses[i].Name));
Houses[i].Rent = (int)Request.Read32();
Request.ReadString(Houses[i].Description, sizeof(Houses[i].Description));
Houses[i].Size = (int)Request.Read16();
Houses[i].PositionX = (int)Request.Read16();
Houses[i].PositionY = (int)Request.Read16();
Houses[i].PositionZ = (int)Request.Read8();
Request.ReadString(Houses[i].Town, sizeof(Houses[i].Town));
Houses[i].GuildHouse = Request.ReadFlag();
}
QUERY_STOP_IF(!InsertHouses(Database, Query->WorldID, NumHouses, Houses));
}
QUERY_STOP_IF(!Tx.Commit());
QueryOk(Query);
}
void ProcessClearIsOnline(TDatabase *Database, TQuery *Query){
int NumAffectedCharacters;
QUERY_STOP_IF(!ClearIsOnline(Database, Query->WorldID, &NumAffectedCharacters));
TWriteBuffer *Response = QueryBeginResponse(Query, QUERY_STATUS_OK);
Response->Write16((uint16)NumAffectedCharacters);
QueryFinishResponse(Query);
}
void ProcessCreatePlayerlist(TDatabase *Database, TQuery *Query){
TReadBuffer Request = Query->Request;
TransactionScope Tx("OnlineList");
QUERY_STOP_IF(!Tx.Begin(Database));
QUERY_STOP_IF(!DeleteOnlineCharacters(Database, Query->WorldID));
bool NewPeak = false;
int NumCharacters = (int)Request.Read16();
if(NumCharacters != 0xFFFF && NumCharacters > 0){
TOnlineCharacter *Characters = (TOnlineCharacter*)alloca(NumCharacters * sizeof(TOnlineCharacter));
for(int i = 0; i < NumCharacters; i += 1){
Request.ReadString(Characters[i].Name, sizeof(Characters[i].Name));
Characters[i].Level = (int)Request.Read16();
Request.ReadString(Characters[i].Profession, sizeof(Characters[i].Profession));
}
QUERY_STOP_IF(!InsertOnlineCharacters(Database, Query->WorldID, NumCharacters, Characters));
QUERY_STOP_IF(!CheckOnlinePeak(Database, Query->WorldID, NumCharacters, &NewPeak));
}
if(NumCharacters == 0xFFFF){
QUERY_STOP_IF(!CheckWorldShutdownTime(Database, Query->WorldID));
}else{
QUERY_STOP_IF(!CheckWorldStartupTime(Database, Query->WorldID));
}
QUERY_STOP_IF(!Tx.Commit());
TWriteBuffer *Response = QueryBeginResponse(Query, QUERY_STATUS_OK);
Response->WriteFlag(NewPeak);
QueryFinishResponse(Query);
}
void ProcessLogKilledCreatures(TDatabase *Database, TQuery *Query){
TReadBuffer Request = Query->Request;
int NumStats = (int)Request.Read16();
TKillStatistics *Stats = (TKillStatistics*)alloca(NumStats * sizeof(TKillStatistics));
for(int i = 0; i < NumStats; i += 1){
Request.ReadString(Stats[i].RaceName, sizeof(Stats[i].RaceName));
Stats[i].PlayersKilled = (int)Request.Read32();
Stats[i].TimesKilled = (int)Request.Read32();
}
if(NumStats > 0){
TransactionScope Tx("LogKilledCreatures");
QUERY_STOP_IF(!Tx.Begin(Database));
QUERY_STOP_IF(!MergeKillStatistics(Database, Query->WorldID, NumStats, Stats));
QUERY_STOP_IF(!Tx.Commit());
}
QueryOk(Query);
}
void ProcessLoadPlayers(TDatabase *Database, TQuery *Query){
// IMPORTANT(fusion): The server expect 10K entries at most. It is probably
// some shared hard coded constant.
int NumEntries;
TCharacterIndexEntry Entries[10000];
TReadBuffer Request = Query->Request;
int MinimumCharacterID = (int)Request.Read32();
QUERY_STOP_IF(!GetCharacterIndexEntries(Database, Query->WorldID,
MinimumCharacterID, NARRAY(Entries), &NumEntries, Entries));
TWriteBuffer *Response = QueryBeginResponse(Query, QUERY_STATUS_OK);
Response->Write32((uint32)NumEntries);
for(int i = 0; i < NumEntries; i += 1){
Response->WriteString(Entries[i].Name);
Response->Write32((uint32)Entries[i].CharacterID);
}
QueryFinishResponse(Query);
}
void ProcessExcludeFromAuctions(TDatabase *Database, TQuery *Query){
TReadBuffer Request = Query->Request;
TransactionScope Tx("ExcludeFromAuctions");
QUERY_STOP_IF(!Tx.Begin(Database));
int CharacterID = (int)Request.Read32();
bool Banish = Request.ReadFlag();
int ExclusionDays = 7;
int BanishmentID = 0;
if(Banish){
int BanishmentDays = 7;
bool FinalWarning = false;
TBanishmentStatus Status;
QUERY_STOP_IF(!GetBanishmentStatus(Database, CharacterID, &Status));
CompoundBanishment(Status, &BanishmentDays, &FinalWarning);
QUERY_STOP_IF(!InsertBanishment(Database, CharacterID, 0,
0, "Spoiling Auction", "", FinalWarning, BanishmentDays * 86400,
&BanishmentID));
}
QUERY_STOP_IF(!ExcludeFromAuctions(Database, Query->WorldID,
CharacterID, ExclusionDays * 86400, BanishmentID));
QUERY_STOP_IF(!Tx.Commit());
QueryOk(Query);
}
void ProcessCancelHouseTransfer(TDatabase *Database, TQuery *Query){
// 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();
(void)Database;
QueryOk(Query);
}
void ProcessLoadWorldConfig(TDatabase *Database, TQuery *Query){
TWorldConfig WorldConfig = {};
QUERY_STOP_IF(!GetWorldConfig(Database, Query->WorldID, &WorldConfig));
QUERY_FAIL_IF(WorldConfig.WorldID == 0);
int IPAddress;
QUERY_FAIL_IF(!ResolveHostName(WorldConfig.HostName, &IPAddress));
TWriteBuffer *Response = QueryBeginResponse(Query, QUERY_STATUS_OK);
Response->Write8((uint8)WorldConfig.Type);
Response->Write8((uint8)WorldConfig.RebootTime);
Response->Write32BE((uint32)IPAddress);
Response->Write16((uint16)WorldConfig.Port);
Response->Write16((uint16)WorldConfig.MaxPlayers);
Response->Write16((uint16)WorldConfig.PremiumPlayerBuffer);
Response->Write16((uint16)WorldConfig.MaxNewbies);
Response->Write16((uint16)WorldConfig.PremiumNewbieBuffer);
QueryFinishResponse(Query);
}
void ProcessCreateAccount(TDatabase *Database, TQuery *Query){
// 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];
TReadBuffer Request = Query->Request;
int AccountID = (int)Request.Read32();
Request.ReadString(Email, sizeof(Email));
Request.ReadString(Password, sizeof(Password));
// NOTE(fusion): Inputs should be checked before hand.
QUERY_FAIL_IF(AccountID <= 0);
QUERY_FAIL_IF(StringEmpty(Email));
QUERY_FAIL_IF(StringEmpty(Password));
uint8 Auth[64];
QUERY_FAIL_IF(!GenerateAuth(Password, Auth, sizeof(Auth)));
TransactionScope Tx("CreateAccount");
QUERY_STOP_IF(!Tx.Begin(Database));
bool AccountExists;
QUERY_STOP_IF(!AccountNumberExists(Database, AccountID, &AccountExists));
QUERY_ERROR_IF(AccountExists, 1);
QUERY_STOP_IF(!AccountEmailExists(Database, Email, &AccountExists));
QUERY_ERROR_IF(AccountExists, 2);
QUERY_STOP_IF(!CreateAccount(Database, AccountID, Email, Auth, sizeof(Auth)));
QUERY_STOP_IF(!Tx.Commit());
QueryOk(Query);
}
void ProcessCreateCharacter(TDatabase *Database, TQuery *Query){
char WorldName[30];
char CharacterName[30];
TReadBuffer Request = Query->Request;
Request.ReadString(WorldName, sizeof(WorldName));
int AccountID = (int)Request.Read32();
Request.ReadString(CharacterName, sizeof(CharacterName));
int Sex = (int)Request.Read8();
// NOTE(fusion): Inputs should be checked before hand.
QUERY_FAIL_IF(AccountID <= 0);
QUERY_FAIL_IF(Sex != 1 && Sex != 2);
QUERY_FAIL_IF(StringEmpty(WorldName));
QUERY_FAIL_IF(StringEmpty(CharacterName));
TransactionScope Tx("CreateCharacter");
QUERY_STOP_IF(!Tx.Begin(Database));
int WorldID;
QUERY_STOP_IF(!GetWorldID(Database, WorldName, &WorldID));
QUERY_ERROR_IF(WorldID == 0, 1);
bool AccountExists;
QUERY_STOP_IF(!AccountNumberExists(Database, AccountID, &AccountExists));
QUERY_ERROR_IF(!AccountExists, 2);
bool CharacterExists;
QUERY_STOP_IF(!CharacterNameExists(Database, CharacterName, &CharacterExists));
QUERY_ERROR_IF(CharacterExists, 3);
QUERY_STOP_IF(!CreateCharacter(Database, WorldID, AccountID, CharacterName, Sex));
QUERY_STOP_IF(!Tx.Commit());
QueryOk(Query);
}
void ProcessGetAccountSummary(TDatabase *Database, TQuery *Query){
TReadBuffer Request = Query->Request;
int AccountID = (int)Request.Read32();
QUERY_FAIL_IF(AccountID <= 0);
TAccount Account;
QUERY_STOP_IF(!GetAccountData(Database, AccountID, &Account));
QUERY_FAIL_IF(Account.AccountID == 0 || Account.AccountID != AccountID);
DynamicArray<TCharacterSummary> Characters;
QUERY_STOP_IF(!GetCharacterSummaries(Database, AccountID, &Characters));
TWriteBuffer *Response = QueryBeginResponse(Query, QUERY_STATUS_OK);
Response->WriteString(Account.Email);
Response->Write16((uint16)Account.PremiumDays);
Response->Write16((uint16)Account.PendingPremiumDays);
Response->WriteFlag(Account.Deleted);
int NumCharacters = std::min<int>(Characters.Length(), UINT8_MAX);
Response->Write8((uint8)NumCharacters);
for(int i = 0; i < NumCharacters; i += 1){
Response->WriteString(Characters[i].Name);
Response->WriteString(Characters[i].World);
Response->Write16((uint16)Characters[i].Level);
Response->WriteString(Characters[i].Profession);
Response->WriteFlag(Characters[i].Online);
Response->WriteFlag(Characters[i].Deleted);
}
QueryFinishResponse(Query);
}
void ProcessGetCharacterProfile(TDatabase *Database, TQuery *Query){
char CharacterName[30];
TReadBuffer Request = Query->Request;
Request.ReadString(CharacterName, sizeof(CharacterName));
QUERY_FAIL_IF(StringEmpty(CharacterName));
TCharacterProfile Character;
QUERY_STOP_IF(!GetCharacterProfile(Database, CharacterName, &Character));
QUERY_ERROR_IF(Character.CharacterID == 0, 1);
TCharacterGuildData GuildData;
QUERY_STOP_IF(!GetCharacterGuildData(Database, Character.CharacterID, &GuildData));
TWriteBuffer *Response = QueryBeginResponse(Query, QUERY_STATUS_OK);
Response->WriteString(Character.Name);
Response->WriteString(Character.World);
Response->Write8((uint8)Character.Sex);
Response->WriteString(GuildData.GuildName);
Response->WriteString(GuildData.RankName);
Response->WriteString(GuildData.Title);
Response->Write16((uint16)Character.Level);
Response->WriteString(Character.Profession);
Response->WriteString(Character.Residence);
Response->Write32((uint32)Character.LastLogin);
Response->Write16((uint16)Character.PremiumDays);
Response->WriteFlag(Character.Online);
Response->WriteFlag(Character.Deleted);
QueryFinishResponse(Query);
}
void ProcessGetWorlds(TDatabase *Database, TQuery *Query){
DynamicArray<TWorld> Worlds;
QUERY_STOP_IF(!GetWorlds(Database, &Worlds));
TWriteBuffer *Response = QueryBeginResponse(Query, QUERY_STATUS_OK);
int NumWorlds = std::min<int>(Worlds.Length(), UINT8_MAX);
Response->Write8((uint8)NumWorlds);
for(int i = 0; i < NumWorlds; i += 1){
Response->WriteString(Worlds[i].Name);
Response->Write8((uint8)Worlds[i].Type);
Response->Write16((uint16)Worlds[i].NumPlayers);
Response->Write16((uint16)Worlds[i].MaxPlayers);
Response->Write16((uint16)Worlds[i].OnlinePeak);
Response->Write32((uint32)Worlds[i].OnlinePeakTimestamp);
Response->Write32((uint32)Worlds[i].LastStartup);
Response->Write32((uint32)Worlds[i].LastShutdown);
}
QueryFinishResponse(Query);
}
void ProcessGetOnlineCharacters(TDatabase *Database, TQuery *Query){
char WorldName[30];
TReadBuffer Request = Query->Request;
Request.ReadString(WorldName, sizeof(WorldName));
int WorldID;
QUERY_STOP_IF(!GetWorldID(Database, WorldName, &WorldID));
QUERY_FAIL_IF(WorldID == 0);
DynamicArray<TOnlineCharacter> Characters;
QUERY_STOP_IF(!GetOnlineCharacters(Database, WorldID, &Characters));
TWriteBuffer *Response = QueryBeginResponse(Query, QUERY_STATUS_OK);
int NumCharacters = std::min<int>(Characters.Length(), UINT16_MAX);
Response->Write16((uint16)NumCharacters);
for(int i = 0; i < NumCharacters; i += 1){
Response->WriteString(Characters[i].Name);
Response->Write16((uint16)Characters[i].Level);
Response->WriteString(Characters[i].Profession);
}
QueryFinishResponse(Query);
}
void ProcessGetKillStatistics(TDatabase *Database, TQuery *Query){
char WorldName[30];
TReadBuffer Request = Query->Request;
Request.ReadString(WorldName, sizeof(WorldName));
int WorldID;
QUERY_STOP_IF(!GetWorldID(Database, WorldName, &WorldID));
QUERY_FAIL_IF(WorldID == 0);
DynamicArray<TKillStatistics> Stats;
QUERY_STOP_IF(!GetKillStatistics(Database, WorldID, &Stats));
TWriteBuffer *Response = QueryBeginResponse(Query, QUERY_STATUS_OK);
int NumStats = std::min<int>(Stats.Length(), UINT16_MAX);
Response->Write16((uint16)NumStats);
for(int i = 0; i < NumStats; i += 1){
Response->WriteString(Stats[i].RaceName);
Response->Write32((uint32)Stats[i].PlayersKilled);
Response->Write32((uint32)Stats[i].TimesKilled);
}
QueryFinishResponse(Query);
}