aboutsummaryrefslogtreecommitdiff
path: root/src/database_postgres.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/database_postgres.cc')
-rw-r--r--src/database_postgres.cc1731
1 files changed, 1637 insertions, 94 deletions
diff --git a/src/database_postgres.cc b/src/database_postgres.cc
index 136f85d..c79e921 100644
--- a/src/database_postgres.cc
+++ b/src/database_postgres.cc
@@ -279,6 +279,12 @@ public:
}
};
+static int ResultAffectedRows(PGresult *Result){
+ int AffectedRows = 0;
+ ParseInteger(&AffectedRows, PQcmdTuples(Result));
+ return AffectedRows;
+}
+
static bool GetResultBool(PGresult *Result, int Row, int Col){
bool Value = false;
int Format = PQfformat(Result, Col);
@@ -353,13 +359,13 @@ static int GetResultInt(PGresult *Result, int Row, int Col){
case INT8OID:{
ASSERT(PQgetlength(Result, Row, Col) == 8);
- int64 Temp = (int64)BufferRead64BE((const uint8*)PQgetvalue(Result, Row, Col));
- if(Temp < INT_MIN || Temp > INT_MAX){
+ int64 Value64 = (int64)BufferRead64BE((const uint8*)PQgetvalue(Result, Row, Col));
+ if(Value64 < INT_MIN || Value64 > INT_MAX){
LOG_WARN("Lossy conversion of column (%d) %s from INT8 to INT4",
Col, PQfname(Result, Col));
}
- Value = (int)Temp;
+ Value = (int)Value64;
break;
}
@@ -439,7 +445,7 @@ static int GetResultByteA(PGresult *Result, int Row, int Col, uint8 *Buffer, int
}
}else if(Format == 1){ // BINARY FORMAT
Size = PQgetlength(Result, Row, Col);
- if(Size > 0 && Size < BufferSize){
+ if(Size > 0 && Size <= BufferSize){
memcpy(Buffer, PQgetvalue(Result, Row, Col), Size);
}
}
@@ -462,13 +468,13 @@ static int GetResultIPAddress(PGresult *Result, int Row, int Col){
switch(Type){
case INT8OID:{
ASSERT(PQgetlength(Result, Row, Col) == 8);
- int64 Temp = (int64)BufferRead64BE((const uint8*)PQgetvalue(Result, Row, Col));
- if(Temp < INT_MIN || Temp > INT_MAX){
+ int64 IPAddress64 = (int64)BufferRead64BE((const uint8*)PQgetvalue(Result, Row, Col));
+ if(IPAddress64 < INT_MIN || IPAddress64 > INT_MAX){
LOG_WARN("Lossy conversion of column (%d) %s from INT8 to IPV4",
Col, PQfname(Result, Col));
}
- IPAddress = (int)Temp;
+ IPAddress = (int)IPAddress64;
break;
}
@@ -576,13 +582,13 @@ static int GetResultTimestamp(PGresult *Result, int Row, int Col){
switch(Type){
case INT8OID:{
ASSERT(PQgetlength(Result, Row, Col) == 8);
- int64 Temp = (int64)BufferRead64BE((const uint8*)PQgetvalue(Result, Row, Col));
- if(Temp < INT_MIN || Temp > INT_MAX){
+ int64 Timestamp64 = (int64)BufferRead64BE((const uint8*)PQgetvalue(Result, Row, Col));
+ if(Timestamp64 < INT_MIN || Timestamp64 > INT_MAX){
LOG_WARN("Lossy conversion of column (%d) %s from INT8 to TIMESTAMP",
Col, PQfname(Result, Col));
}
- Timestamp = (int)Temp;
+ Timestamp = (int)Timestamp64;
break;
}
@@ -845,13 +851,13 @@ static int GetResultInterval(PGresult *Result, int Row, int Col){
switch(Type){
case INT8OID:{
ASSERT(PQgetlength(Result, Row, Col) == 8);
- int64 Temp = (int64)BufferRead64BE((const uint8*)PQgetvalue(Result, Row, Col));
- if(Temp < INT_MIN || Temp > INT_MAX){
+ int64 Interval64 = (int64)BufferRead64BE((const uint8*)PQgetvalue(Result, Row, Col));
+ if(Interval64 < INT_MIN || Interval64 > INT_MAX){
LOG_WARN("Lossy conversion of column (%d) %s from INT8 to INTERVAL",
Col, PQfname(Result, Col));
}
- Interval = (int)Temp;
+ Interval = (int)Interval64;
break;
}
@@ -1262,12 +1268,6 @@ TDatabase *DatabaseOpen(void){
return Database;
}
-int DatabaseChanges(TDatabase *Database){
- ASSERT(Database != NULL);
- // TODO?
- return 0;
-}
-
bool DatabaseCheckpoint(TDatabase *Database){
ASSERT(Database != NULL);
bool Result = true;
@@ -1421,8 +1421,7 @@ bool AccountExists(TDatabase *Database, int AccountID, const char *Email, bool *
bool AccountNumberExists(TDatabase *Database, int AccountID, bool *Exists){
ASSERT(Database != NULL && Exists != NULL);
const char *Stmt = PrepareQuery(Database,
- "SELECT 1 FROM Accounts"
- " WHERE AccountID = $1::INTEGER");
+ "SELECT 1 FROM Accounts WHERE AccountID = $1::INTEGER");
if(Stmt == NULL){
LOG_ERR("Failed to prepare query");
return false;
@@ -1498,7 +1497,7 @@ bool GetAccountData(TDatabase *Database, int AccountID, TAccount *Account){
ASSERT(Database != NULL && Account != NULL);
const char *Stmt = PrepareQuery(Database,
"SELECT AccountID, Email, Auth,"
- " (PremiumEnd - CURRENT_TIMESTAMP),"
+ " GREATEST(PremiumEnd - CURRENT_TIMESTAMP, '0'),"
" PendingPremiumDays, Deleted"
" FROM Accounts WHERE AccountID = $1::INTEGER");
if(Stmt == NULL){
@@ -1526,7 +1525,7 @@ bool GetAccountData(TDatabase *Database, int AccountID, TAccount *Account){
if(GetResultByteA(Result, 0, 2, Auth, sizeof(Auth)) == sizeof(Auth)){
memcpy(Account->Auth, Auth, sizeof(Auth));
}
- Account->PremiumDays = GetResultInterval(Result, 0, 3);
+ Account->PremiumDays = RoundSecondsToDays(GetResultInterval(Result, 0, 3));
Account->PendingPremiumDays = GetResultInt(Result, 0, 4);
Account->Deleted = GetResultBool(Result, 0, 5);
}
@@ -1566,250 +1565,1794 @@ bool GetAccountOnlineCharacters(TDatabase *Database, int AccountID, int *OnlineC
return true;
}
-bool IsCharacterOnline(TDatabase *Database, int CharacterID, bool *Result){
- return false;
+bool IsCharacterOnline(TDatabase *Database, int CharacterID, bool *Online){
+ ASSERT(Database != NULL && Online != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "SELECT IsOnline FROM Characters WHERE CharacterID = $1::INTEGER");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 1, 1);
+ ParamInt(&Params, CharacterID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ *Online = (PQntuples(Result) > 0 && GetResultInt(Result, 0, 0) != 0);
+ return true;
}
bool ActivatePendingPremiumDays(TDatabase *Database, int AccountID){
- return false;
+ ASSERT(Database != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "UPDATE Accounts"
+ " SET PremiumEnd = GREATEST(PremiumEnd, CURRENT_TIMESTAMP)"
+ " + MAKE_INTERVAL(days => PendingPremiumDays),"
+ " PendingPremiumDays = 0"
+ " WHERE AccountID = $1::INTEGER AND PendingPremiumDays > 0");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 1, 1);
+ ParamInt(&Params, AccountID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_COMMAND_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ return true;
}
bool GetCharacterEndpoints(TDatabase *Database, int AccountID, DynamicArray<TCharacterEndpoint> *Characters){
- return false;
+ ASSERT(Database != NULL && Characters != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "SELECT C.Name, W.Name, W.Host, W.Port"
+ " FROM Characters AS C"
+ " INNER JOIN Worlds AS W ON W.WorldID = C.WorldID"
+ " WHERE C.AccountID = $1::INTEGER");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 1, 1);
+ ParamInt(&Params, AccountID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ int NumRows = PQntuples(Result);
+ for(int Row = 0; Row < NumRows; Row += 1){
+ int WorldAddress;
+ const char *CharacterName = GetResultText(Result, Row, 0);
+ const char *WorldName = GetResultText(Result, Row, 1);
+ const char *HostName = GetResultText(Result, Row, 2);
+ if(HostName == NULL || !ResolveHostName(HostName, &WorldAddress)){
+ LOG_ERR("Failed to resolve world \"%s\" host name \"%s\" for character \"%s\"",
+ WorldName, HostName, CharacterName);
+ continue;
+ }
+
+ TCharacterEndpoint Character = {};
+ StringBufCopy(Character.Name, CharacterName);
+ StringBufCopy(Character.WorldName, WorldName);
+ Character.WorldAddress = WorldAddress;
+ Character.WorldPort = GetResultInt(Result, Row, 3);
+ Characters->Push(Character);
+
+ }
+
+ return true;
}
bool GetCharacterSummaries(TDatabase *Database, int AccountID, DynamicArray<TCharacterSummary> *Characters){
- return false;
+ ASSERT(Database != NULL && Characters != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "SELECT C.Name, W.Name, C.Level, C.Profession, C.IsOnline, C.Deleted"
+ " FROM Characters AS C"
+ " LEFT JOIN Worlds AS W ON W.WorldID = C.WorldID"
+ " WHERE C.AccountID = $1::INTEGER");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 1, 1);
+ ParamInt(&Params, AccountID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ int NumRows = PQntuples(Result);
+ for(int Row = 0; Row < NumRows; Row += 1){
+ TCharacterSummary Character = {};
+ StringBufCopy(Character.Name, GetResultText(Result, Row, 0));
+ StringBufCopy(Character.World, GetResultText(Result, Row, 1));
+ Character.Level = GetResultInt(Result, Row, 2);
+ StringBufCopy(Character.Profession, GetResultText(Result, Row, 3));
+ Character.Online = GetResultBool(Result, Row, 4);
+ Character.Deleted = GetResultBool(Result, Row, 5);;
+ Characters->Push(Character);
+ }
+
+ return true;
}
-bool CharacterNameExists(TDatabase *Database, const char *Name, bool *Result){
- return false;
+bool CharacterNameExists(TDatabase *Database, const char *Name, bool *Exists){
+ ASSERT(Database != NULL && Exists != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "SELECT 1 FROM Characters WHERE Name = $1::TEXT");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 1, 1);
+ ParamText(&Params, Name);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ *Exists = (PQntuples(Result) > 0);
+ return true;
}
bool CreateCharacter(TDatabase *Database, int WorldID, int AccountID, const char *Name, int Sex){
- return false;
+ ASSERT(Database != NULL && Name != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "INSERT INTO Characters (WorldID, AccountID, Name, Sex)"
+ " VALUES ($1::INTEGER, $2::INTEGER, $3::TEXT, $4::INTEGER)");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 4, 1);
+ ParamInt(&Params, WorldID);
+ ParamInt(&Params, AccountID);
+ ParamText(&Params, Name);
+ ParamInt(&Params, Sex);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_COMMAND_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ return true;
}
bool GetCharacterID(TDatabase *Database, int WorldID, const char *CharacterName, int *CharacterID){
- return false;
+ ASSERT(Database != NULL && CharacterName != NULL && CharacterID != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "SELECT CharacterID FROM Characters"
+ " WHERE WorldID = $1::INTEGER AND Name = $2::TEXT");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 2, 1);
+ ParamInt(&Params, WorldID);
+ ParamText(&Params, CharacterName);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ *CharacterID = (PQntuples(Result) > 0 ? GetResultInt(Result, 0, 0) : 0);
+ return true;
}
bool GetCharacterLoginData(TDatabase *Database, const char *CharacterName, TCharacterLoginData *Character){
- return false;
+ ASSERT(Database != NULL && CharacterName != NULL && Character != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "SELECT WorldID, CharacterID, AccountID, Name,"
+ " Sex, Guild, Rank, Title, Deleted"
+ " FROM Characters WHERE Name = $1::TEXT");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 1, 1);
+ ParamText(&Params, CharacterName);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ memset(Character, 0, sizeof(TCharacterLoginData));
+ if(PQntuples(Result) > 0){
+ Character->WorldID = GetResultInt(Result, 0, 0);
+ Character->CharacterID = GetResultInt(Result, 0, 1);
+ Character->AccountID = GetResultInt(Result, 0, 2);
+ StringBufCopy(Character->Name, GetResultText(Result, 0, 3));
+ Character->Sex = GetResultInt(Result, 0, 4);
+ StringBufCopy(Character->Guild, GetResultText(Result, 0, 5));
+ StringBufCopy(Character->Rank, GetResultText(Result, 0, 6));
+ StringBufCopy(Character->Title, GetResultText(Result, 0, 7));
+ Character->Deleted = GetResultBool(Result, 0, 8);
+ }
+
+ return true;
}
bool GetCharacterProfile(TDatabase *Database, const char *CharacterName, TCharacterProfile *Character){
- return false;
+ ASSERT(Database != NULL && CharacterName != NULL && Character != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "SELECT C.Name, W.Name, C.Sex, C.Guild, C.Rank, C.Title, C.Level,"
+ " C.Profession, C.Residence, C.LastLoginTime, C.IsOnline,"
+ " C.Deleted, GREATEST(A.PremiumEnd - CURRENT_TIMESTAMP, '0')"
+ " FROM Characters AS C"
+ " LEFT JOIN Worlds AS W ON W.WorldID = C.WorldID"
+ " LEFT JOIN Accounts AS A ON A.AccountID = C.AccountID"
+ " LEFT JOIN CharacterRights AS R"
+ " ON R.CharacterID = C.CharacterID"
+ " AND R.Name = 'NO_STATISTICS'"
+ " WHERE C.Name = $1::TEXT AND R.Right IS NULL");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 1, 1);
+ ParamText(&Params, CharacterName);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ memset(Character, 0, sizeof(TCharacterProfile));
+ if(PQntuples(Result) > 0){
+ StringBufCopy(Character->Name, GetResultText(Result, 0, 0));
+ StringBufCopy(Character->World, GetResultText(Result, 0, 1));
+ Character->Sex = GetResultInt(Result, 0, 2);
+ StringBufCopy(Character->Guild, GetResultText(Result, 0, 3));
+ StringBufCopy(Character->Rank, GetResultText(Result, 0, 4));
+ StringBufCopy(Character->Title, GetResultText(Result, 0, 5));
+ Character->Level = GetResultInt(Result, 0, 6);
+ StringBufCopy(Character->Profession, GetResultText(Result, 0, 7));
+ StringBufCopy(Character->Residence, GetResultText(Result, 0, 8));
+ Character->LastLogin = GetResultTimestamp(Result, 0, 9);
+ Character->Online = GetResultBool(Result, 0, 10);
+ Character->Deleted = GetResultBool(Result, 0, 11);
+ Character->PremiumDays = RoundSecondsToDays(GetResultInterval(Result, 0, 12));
+ }
+
+ return true;
}
-bool GetCharacterRight(TDatabase *Database, int CharacterID, const char *Right, bool *Result){
- return false;
+bool GetCharacterRight(TDatabase *Database, int CharacterID, const char *Right, bool *HasRight){
+ ASSERT(Database != NULL && Right != NULL && HasRight != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "SELECT 1 FROM CharacterRights"
+ " WHERE CharacterID = $1::INTEGER AND Name = $2::TEXT");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 2, 1);
+ ParamInt(&Params, CharacterID);
+ ParamText(&Params, Right);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ *HasRight = (PQntuples(Result) > 0);
+ return true;
}
bool GetCharacterRights(TDatabase *Database, int CharacterID, DynamicArray<TCharacterRight> *Rights){
- return false;
+ ASSERT(Database != NULL && Rights != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "SELECT Name FROM CharacterRights WHERE CharacterID = $1::INTEGER");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 1, 1);
+ ParamInt(&Params, CharacterID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ int NumRows = PQntuples(Result);
+ for(int Row = 0; Row < NumRows; Row += 1){
+ TCharacterRight Right = {};
+ StringBufCopy(Right.Name, GetResultText(Result, Row, 0));
+ Rights->Push(Right);
+ }
+
+ return true;
}
-bool GetGuildLeaderStatus(TDatabase *Database, int WorldID, int CharacterID, bool *Result){
+bool GetGuildLeaderStatus(TDatabase *Database, int WorldID, int CharacterID, bool *GuildLeader){
+ ASSERT(Database != NULL && GuildLeader != NULL);
+ // NOTE(fusion): Same as `DecrementIsOnline`.
+ const char *Stmt = PrepareQuery(Database,
+ "SELECT Guild, Rank FROM Characters"
+ " WHERE WorldID = $1::INTEGER AND CharacterID = $2::INTEGER");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 2, 1);
+ ParamInt(&Params, WorldID);
+ ParamInt(&Params, CharacterID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ *GuildLeader = false;
+ if(PQntuples(Result) > 0){
+ const char *Guild = GetResultText(Result, 0, 0);
+ const char *Rank = GetResultText(Result, 0, 1);
+ if(Guild != NULL && !StringEmpty(Guild) && Rank != NULL && StringEqCI(Rank, "Leader")){
+ *GuildLeader = true;
+ }
+ }
+
return false;
}
bool IncrementIsOnline(TDatabase *Database, int WorldID, int CharacterID){
- return false;
+ ASSERT(Database != NULL);
+ // NOTE(fusion): Same as `DecrementIsOnline`.
+ const char *Stmt = PrepareQuery(Database,
+ "UPDATE Characters SET IsOnline = IsOnline + 1"
+ " WHERE WorldID = $1::INTEGER AND CharacterID = $2::INTEGER");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 2, 1);
+ ParamInt(&Params, WorldID);
+ ParamInt(&Params, CharacterID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_COMMAND_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ return true;
}
bool DecrementIsOnline(TDatabase *Database, int WorldID, int CharacterID){
- return false;
+ ASSERT(Database != NULL);
+ // NOTE(fusion): A character is uniquely identified by its id. The world id
+ // check is purely to avoid a world from modifying a character from another
+ // world.
+ const char *Stmt = PrepareQuery(Database,
+ "UPDATE Characters SET IsOnline = IsOnline - 1"
+ " WHERE WorldID = $1::INTEGER AND CharacterID = $2::INTEGER");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 2, 1);
+ ParamInt(&Params, WorldID);
+ ParamInt(&Params, CharacterID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_COMMAND_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ return true;
}
bool ClearIsOnline(TDatabase *Database, int WorldID, int *NumAffectedCharacters){
- return false;
+ ASSERT(Database != NULL && NumAffectedCharacters != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "UPDATE Characters SET IsOnline = 0"
+ " WHERE WorldID = $1::INTEGER AND IsOnline != 0");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 1, 1);
+ ParamInt(&Params, WorldID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_COMMAND_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ *NumAffectedCharacters = ResultAffectedRows(Result);
+ return true;
}
bool LogoutCharacter(TDatabase *Database, int WorldID, int CharacterID, int Level,
const char *Profession, const char *Residence, int LastLoginTime, int TutorActivities){
- return false;
+ ASSERT(Database != NULL && Profession != NULL && Residence != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "UPDATE Characters"
+ " SET Level = $3::INTEGER,"
+ " Profession = $4::TEXT,"
+ " Residence = $5::TEXT,"
+ " LastLoginTime = $6::TIMESTAMPTZ,"
+ " TutorActivities = $7::INTEGER,"
+ " IsOnline = IsOnline - 1"
+ " WHERE WorldID = $1::INTEGER AND CharacterID = $2::INTEGER");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 7, 1);
+ ParamInt(&Params, WorldID);
+ ParamInt(&Params, CharacterID);
+ ParamInt(&Params, Level);
+ ParamText(&Params, Profession);
+ ParamText(&Params, Residence);
+ ParamTimestamp(&Params, LastLoginTime);
+ ParamInt(&Params, TutorActivities);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_COMMAND_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ return true;
}
bool GetCharacterIndexEntries(TDatabase *Database, int WorldID, int MinimumCharacterID,
int MaxEntries, int *NumEntries, TCharacterIndexEntry *Entries){
- return false;
+ ASSERT(Database != NULL && MaxEntries > 0 && NumEntries != NULL && Entries != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "SELECT CharacterID, Name FROM Characters"
+ " WHERE WorldID = $1::INTEGER AND CharacterID >= $2::INTEGER"
+ " ORDER BY CharacterID ASC LIMIT $3::INTEGER");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 3, 1);
+ ParamInt(&Params, WorldID);
+ ParamInt(&Params, MinimumCharacterID);
+ ParamInt(&Params, MaxEntries);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ int NumRows = PQntuples(Result);
+ if(NumRows > MaxEntries){
+ LOG_WARN("Query returned too many rows (expected %d, got %d)", MaxEntries, NumRows);
+ NumRows = MaxEntries;
+ }
+
+ for(int Row = 0; Row < NumRows; Row += 1){
+ Entries[Row].CharacterID = GetResultInt(Result, Row, 0);
+ StringBufCopy(Entries[Row].Name, GetResultText(Result, Row, 1));
+ }
+
+ *NumEntries = NumRows;
+ return true;
}
bool InsertCharacterDeath(TDatabase *Database, int WorldID, int CharacterID, int Level,
int OffenderID, const char *Remark, bool Unjustified, int Timestamp){
- return false;
+ ASSERT(Database != NULL && Remark != NULL);
+ // NOTE(fusion): Same as `DecrementIsOnline`.
+ const char *Stmt = PrepareQuery(Database,
+ "INSERT INTO CharacterDeaths (CharacterID, Level,"
+ " OffenderID, Remark, Unjustified, Timestamp)"
+ " SELECT $2::INTEGER, $3::INTEGER, $4::INTEGER,"
+ "$5::TEXT, $6::BOOLEAN, $7::TIMESTAMPTZ"
+ " FROM Characters"
+ " WHERE WorldID = $1::INTEGER AND CharacterID = $2::INTEGER");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 7, 1);
+ ParamInt(&Params, WorldID);
+ ParamInt(&Params, CharacterID);
+ ParamInt(&Params, Level);
+ ParamInt(&Params, OffenderID);
+ ParamText(&Params, Remark);
+ ParamBool(&Params, Unjustified);
+ ParamTimestamp(&Params, Timestamp);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_COMMAND_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ return true;
}
bool InsertBuddy(TDatabase *Database, int WorldID, int AccountID, int BuddyID){
- return false;
+ ASSERT(Database != NULL);
+ // NOTE(fusion): Same as `DecrementIsOnline`.
+ // NOTE(fusion): Use the `DO NOTHING` conflict resolution to make duplicate
+ // row errors appear as successful insertions.
+ const char *Stmt = PrepareQuery(Database,
+ "INSERT INTO Buddies (WorldID, AccountID, BuddyID)"
+ " SELECT $1::INTEGER, $2::INTEGER, $3::INTEGER FROM Characters"
+ " WHERE WorldID = $1::INTEGER AND CharacterID = $3::INTEGER"
+ " ON CONFLICT DO NOTHING");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 3, 1);
+ ParamInt(&Params, WorldID);
+ ParamInt(&Params, AccountID);
+ ParamInt(&Params, BuddyID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_COMMAND_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ return true;
}
bool DeleteBuddy(TDatabase *Database, int WorldID, int AccountID, int BuddyID){
- return false;
+ ASSERT(Database != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "DELETE FROM Buddies"
+ " WHERE WorldID = $1::INTEGER"
+ " AND AccountID = $2::INTEGER"
+ " AND BuddyID = $3::INTEGER");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 3, 1);
+ ParamInt(&Params, WorldID);
+ ParamInt(&Params, AccountID);
+ ParamInt(&Params, BuddyID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_COMMAND_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ return true;
}
bool GetBuddies(TDatabase *Database, int WorldID, int AccountID, DynamicArray<TAccountBuddy> *Buddies){
- return false;
+ ASSERT(Database != NULL && Buddies != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "SELECT B.BuddyID, C.Name"
+ " FROM Buddies AS B"
+ " INNER JOIN Characters AS C"
+ " ON C.WorldID = B.WorldID AND C.CharacterID = B.BuddyID"
+ " WHERE B.WorldID = $1::INTEGER AND B.AccountID = $2::INTEGER");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 2, 1);
+ ParamInt(&Params, WorldID);
+ ParamInt(&Params, AccountID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ int NumRows = PQntuples(Result);
+ for(int Row = 0; Row < NumRows; Row += 1){
+ TAccountBuddy Buddy = {};
+ Buddy.CharacterID = GetResultInt(Result, Row, 0);
+ StringBufCopy(Buddy.Name, GetResultText(Result, Row, 1));
+ Buddies->Push(Buddy);
+ }
+
+ return true;
}
-bool GetWorldInvitation(TDatabase *Database, int WorldID, int CharacterID, bool *Result){
- return false;
+bool GetWorldInvitation(TDatabase *Database, int WorldID, int CharacterID, bool *Invited){
+ ASSERT(Database != NULL && Invited != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "SELECT 1 FROM WorldInvitations"
+ " WHERE WorldID = $1::INTEGER AND CharacterID = $2::INTEGER");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 2, 1);
+ ParamInt(&Params, WorldID);
+ ParamInt(&Params, CharacterID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ *Invited = (PQntuples(Result) > 0);
+ return true;
}
bool InsertLoginAttempt(TDatabase *Database, int AccountID, int IPAddress, bool Failed){
- return false;
-}
+ ASSERT(Database != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "INSERT INTO LoginAttempts (AccountID, IPAddress, Timestamp, Failed)"
+ " VALUES ($1::INTEGER, $2::INET, CURRENT_TIMESTAMP, $3::BOOLEAN)");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
-bool GetAccountFailedLoginAttempts(TDatabase *Database, int AccountID, int TimeWindow, int *Result){
- return false;
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 3, 1);
+ ParamInt(&Params, AccountID);
+ ParamIPAddress(&Params, IPAddress);
+ ParamBool(&Params, Failed);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_COMMAND_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ return true;
}
-bool GetIPAddressFailedLoginAttempts(TDatabase *Database, int IPAddress, int TimeWindow, int *Result){
- return false;
+bool GetAccountFailedLoginAttempts(TDatabase *Database, int AccountID, int TimeWindow, int *FailedAttempts){
+ ASSERT(Database != NULL && FailedAttempts != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "SELECT COUNT(*) FROM LoginAttempts"
+ " WHERE AccountID = $1::INTEGER"
+ " AND (CURRENT_TIMESTAMP - Timestamp) <= $2::INTERVAL"
+ " AND Failed");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 2, 1);
+ ParamInt(&Params, AccountID);
+ ParamInterval(&Params, TimeWindow);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ if(PQntuples(Result) == 0){
+ LOG_ERR("Query returned no rows");
+ return false;
+ }
+
+ *FailedAttempts = GetResultInt(Result, 0, 0);
+ return true;
}
+bool GetIPAddressFailedLoginAttempts(TDatabase *Database, int IPAddress, int TimeWindow, int *FailedAttempts){
+ ASSERT(Database != NULL && FailedAttempts != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "SELECT COUNT(*) FROM LoginAttempts"
+ " WHERE IPAddress = $1::INET"
+ " AND (CURRENT_TIMESTAMP - Timestamp) <= $2::INTERVAL"
+ " AND Failed");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 2, 1);
+ ParamIPAddress(&Params, IPAddress);
+ ParamInterval(&Params, TimeWindow);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ if(PQntuples(Result) == 0){
+ LOG_ERR("Query returned no rows");
+ return false;
+ }
+
+ *FailedAttempts = GetResultInt(Result, 0, 0);
+ return true;
+}
// House Tables
//==============================================================================
bool FinishHouseAuctions(TDatabase *Database, int WorldID, DynamicArray<THouseAuction> *Auctions){
- return false;
+ ASSERT(Database != NULL && Auctions != NULL);
+ // TODO(fusion): If the application crashes while processing finished auctions,
+ // non processed auctions will be lost but with no other side-effects. It could
+ // be an inconvenience but it's not a big problem.
+ const char *Stmt = PrepareQuery(Database,
+ "DELETE FROM HouseAuctions"
+ " WHERE WorldID = $1::INTEGER"
+ " AND FinishTime IS NOT NULL"
+ " AND FinishTime <= CURRENT_TIMESTAMP"
+ " RETURNING HouseID, BidderID, BidAmount, FinishTime,"
+ " (SELECT Name FROM Characters WHERE CharacterID = BidderID)");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 1, 1);
+ ParamInt(&Params, WorldID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ int NumRows = PQntuples(Result);
+ for(int Row = 0; Row < NumRows; Row += 1){
+ THouseAuction Auction = {};
+ Auction.HouseID = GetResultInt(Result, Row, 0);
+ Auction.BidderID = GetResultInt(Result, Row, 1);
+ Auction.BidAmount = GetResultInt(Result, Row, 2);
+ Auction.FinishTime = GetResultTimestamp(Result, Row, 3);
+ StringBufCopy(Auction.BidderName, GetResultText(Result, Row, 4));
+ Auctions->Push(Auction);
+ }
+
+ return true;
}
bool FinishHouseTransfers(TDatabase *Database, int WorldID, DynamicArray<THouseTransfer> *Transfers){
- return false;
+ ASSERT(Database != NULL && Transfers != NULL);
+ // TODO(fusion): Same as `FinishHouseAuctions` but with house transfers.
+ const char *Stmt = PrepareQuery(Database,
+ "DELETE FROM HouseTransfers"
+ " WHERE WorldID = $1::INTEGER"
+ " RETURNING HouseID, NewOwnerID, Price,"
+ " (SELECT Name FROM Characters WHERE CharacterID = NewOwnerID)");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 1, 1);
+ ParamInt(&Params, WorldID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ int NumRows = PQntuples(Result);
+ for(int Row = 0; Row < NumRows; Row += 1){
+ THouseTransfer Transfer = {};
+ Transfer.HouseID = GetResultInt(Result, Row, 0);
+ Transfer.NewOwnerID = GetResultInt(Result, Row, 1);
+ Transfer.Price = GetResultInt(Result, Row, 2);
+ StringBufCopy(Transfer.NewOwnerName, GetResultText(Result, Row, 4));
+ Transfers->Push(Transfer);
+ }
+
+ return true;
}
bool GetFreeAccountEvictions(TDatabase *Database, int WorldID, DynamicArray<THouseEviction> *Evictions){
- return false;
+ ASSERT(Database != NULL && Evictions != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "SELECT O.HouseID, O.OwnerID"
+ " FROM HouseOwners AS O"
+ " LEFT JOIN Characters AS C ON C.CharacterID = O.OwnerID"
+ " LEFT JOIN Accounts AS A ON A.AccountID = C.AccountID"
+ " WHERE O.WorldID = $1::INTEGER"
+ " AND (A.PremiumEnd IS NULL OR A.PremiumEnd < CURRENT_TIMESTAMP)");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 1, 1);
+ ParamInt(&Params, WorldID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ int NumRows = PQntuples(Result);
+ for(int Row = 0; Row < NumRows; Row += 1){
+ THouseEviction Eviction = {};
+ Eviction.HouseID = GetResultInt(Result, Row, 0);
+ Eviction.OwnerID = GetResultInt(Result, Row, 1);
+ Evictions->Push(Eviction);
+ }
+
+ return true;
}
bool GetDeletedCharacterEvictions(TDatabase *Database, int WorldID, DynamicArray<THouseEviction> *Evictions){
- return false;
+ ASSERT(Database != NULL && Evictions != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "SELECT O.HouseID, O.OwnerID"
+ " FROM HouseOwners AS O"
+ " LEFT JOIN Characters AS C ON C.CharacterID = O.OwnerID"
+ " WHERE O.WorldID = $1::INTEGER"
+ " AND (C.CharacterID IS NULL OR C.Deleted)");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 1, 1);
+ ParamInt(&Params, WorldID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ int NumRows = PQntuples(Result);
+ for(int Row = 0; Row < NumRows; Row += 1){
+ THouseEviction Eviction = {};
+ Eviction.HouseID = GetResultInt(Result, Row, 0);
+ Eviction.OwnerID = GetResultInt(Result, Row, 1);
+ Evictions->Push(Eviction);
+ }
+
+ return true;
}
bool InsertHouseOwner(TDatabase *Database, int WorldID, int HouseID, int OwnerID, int PaidUntil){
- return false;
+ ASSERT(Database != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "INSERT INTO HouseOwners (WorldID, HouseID, OwnerID, PaidUntil)"
+ " VALUES ($1::INTEGER, $2::INTEGER, $3::INTEGER, $4::TIMESTAMPTZ)");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 4, 1);
+ ParamInt(&Params, WorldID);
+ ParamInt(&Params, HouseID);
+ ParamInt(&Params, OwnerID);
+ ParamTimestamp(&Params, PaidUntil);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_COMMAND_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ return true;
}
bool UpdateHouseOwner(TDatabase *Database, int WorldID, int HouseID, int OwnerID, int PaidUntil){
- return false;
+ ASSERT(Database != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "UPDATE HouseOwners"
+ " SET OwnerID = $3::INTEGER, PaidUntil = $4::TIMESTAMPTZ"
+ " WHERE WorldID = $1::INTEGER AND HouseID = $2::INTEGER");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 4, 1);
+ ParamInt(&Params, WorldID);
+ ParamInt(&Params, HouseID);
+ ParamInt(&Params, OwnerID);
+ ParamTimestamp(&Params, PaidUntil);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_COMMAND_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ return true;
}
bool DeleteHouseOwner(TDatabase *Database, int WorldID, int HouseID){
- return false;
+ ASSERT(Database != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "DELETE FROM HouseOwners"
+ " WHERE WorldID = $1::INTEGER AND HouseID = $2::INTEGER");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 2, 1);
+ ParamInt(&Params, WorldID);
+ ParamInt(&Params, HouseID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_COMMAND_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ return true;
}
bool GetHouseOwners(TDatabase *Database, int WorldID, DynamicArray<THouseOwner> *Owners){
- return false;
+ ASSERT(Database != NULL && Owners != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "SELECT O.HouseID, O.OwnerID, C.Name, O.PaidUntil"
+ " FROM HouseOwners AS O"
+ " LEFT JOIN Characters AS C ON C.CharacterID = O.OwnerID"
+ " WHERE O.WorldID = $1::INTEGER");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 1, 1);
+ ParamInt(&Params, WorldID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ int NumRows = PQntuples(Result);
+ for(int Row = 0; Row < NumRows; Row += 1){
+ THouseOwner Owner = {};
+ Owner.HouseID = GetResultInt(Result, Row, 0);
+ Owner.OwnerID = GetResultInt(Result, Row, 1);
+ StringBufCopy(Owner.OwnerName, GetResultText(Result, Row, 2));
+ Owner.PaidUntil = GetResultTimestamp(Result, Row, 3);
+ Owners->Push(Owner);
+ }
+
+ return true;
}
bool GetHouseAuctions(TDatabase *Database, int WorldID, DynamicArray<int> *Auctions){
- return false;
+ ASSERT(Database != NULL && Auctions != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "SELECT HouseID FROM HouseAuctions WHERE WorldID = $1::INTEGER");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 1, 1);
+ ParamInt(&Params, WorldID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ int NumRows = PQntuples(Result);
+ for(int Row = 0; Row < NumRows; Row += 1){
+ Auctions->Push(GetResultInt(Result, Row, 0));
+ }
+
+ return true;
}
bool StartHouseAuction(TDatabase *Database, int WorldID, int HouseID){
- return false;
+ ASSERT(Database != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "INSERT INTO HouseAuctions (WorldID, HouseID)"
+ " VALUES ($1::INTEGER, $2::INTEGER)");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 2, 1);
+ ParamInt(&Params, WorldID);
+ ParamInt(&Params, HouseID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_COMMAND_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ return true;
}
bool DeleteHouses(TDatabase *Database, int WorldID){
- return false;
+ ASSERT(Database != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "DELETE FROM Houses WHERE WorldID = $1::INTEGER");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 1, 1);
+ ParamInt(&Params, WorldID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_COMMAND_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ return true;
}
bool InsertHouses(TDatabase *Database, int WorldID, int NumHouses, THouse *Houses){
- return false;
+ ASSERT(Database != NULL && NumHouses > 0 && Houses != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "INSERT INTO Houses (WorldID, HouseID, Name, Rent, Description,"
+ " Size, PositionX, PositionY, PositionZ, Town, GuildHouse)"
+ " VALUES ($1::INTEGER, $2::INTEGER, $3::TEXT, $4::INTEGER, $5::TEXT,"
+ " $6::INTEGER, $7::INTEGER, $8::INTEGER, $9::INTEGER, $10::TEXT,"
+ " $11::BOOLEAN)");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ // TODO(fusion): We could use pipelining to speed up multiple inserts but I'm
+ // not sure the complexity increase would warrant any speed-ups.
+ ParamBuffer Params = {};
+ for(int i = 0; i < NumHouses; i += 1){
+ ParamBegin(&Params, 11, 1);
+ ParamInt(&Params, WorldID);
+ ParamInt(&Params, Houses[i].HouseID);
+ ParamText(&Params, Houses[i].Name);
+ ParamInt(&Params, Houses[i].Rent);
+ ParamText(&Params, Houses[i].Description);
+ ParamInt(&Params, Houses[i].Size);
+ ParamInt(&Params, Houses[i].PositionX);
+ ParamInt(&Params, Houses[i].PositionY);
+ ParamInt(&Params, Houses[i].PositionZ);
+ ParamText(&Params, Houses[i].Town);
+ ParamBool(&Params, Houses[i].GuildHouse);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_COMMAND_OK){
+ LOG_ERR("Failed to insert house %d: %s",
+ Houses[i].HouseID,
+ PQerrorMessage(Database->Handle));
+ return false;
+ }
+ }
+
+ return true;
}
bool ExcludeFromAuctions(TDatabase *Database, int WorldID, int CharacterID, int Duration, int BanishmentID){
- return false;
-}
+ ASSERT(Database != NULL);
+ // NOTE(fusion): Same as `DecrementIsOnline`.
+ const char *Stmt = PrepareQuery(Database,
+ "INSERT INTO HouseAuctionExclusions (CharacterID, Issued, Until, BanishmentID)"
+ " SELECT $2::INTEGER, CURRENT_TIMESTAMP, (CURRENT_TIMESTAMP + $3::INTERVAL), $4::INTEGER"
+ " FROM Characters"
+ " WHERE WorldID = $1::INTEGER AND CharacterID = $2::INTEGER");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 4, 1);
+ ParamInt(&Params, WorldID);
+ ParamInt(&Params, CharacterID);
+ ParamInterval(&Params, Duration);
+ ParamInt(&Params, BanishmentID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_COMMAND_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+ return true;
+}
// Banishment Tables
//==============================================================================
-bool IsCharacterNamelocked(TDatabase *Database, int CharacterID, bool *Result){
- return false;
+bool IsCharacterNamelocked(TDatabase *Database, int CharacterID, bool *Namelocked){
+ ASSERT(Database != NULL && Namelocked != NULL);
+ TNamelockStatus Status;
+ if(!GetNamelockStatus(Database, CharacterID, &Status)){
+ return false;
+ }
+
+ *Namelocked = Status.Namelocked && !Status.Approved;
+ return true;
}
bool GetNamelockStatus(TDatabase *Database, int CharacterID, TNamelockStatus *Status){
- return false;
+ ASSERT(Database != NULL && Status != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "SELECT Approved FROM Namelocks WHERE CharacterID = $1::INTEGER");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 1, 1);
+ ParamInt(&Params, CharacterID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ memset(Status, 0, sizeof(TNamelockStatus));
+ Status->Namelocked = (PQntuples(Result) > 0);
+ if(Status->Namelocked){
+ Status->Approved = GetResultBool(Result, 0, 0);
+ }
+
+ return true;
}
bool InsertNamelock(TDatabase *Database, int CharacterID, int IPAddress,
int GamemasterID, const char *Reason, const char *Comment){
- return false;
+ ASSERT(Database != NULL && Reason != NULL && Comment != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "INSERT INTO Namelocks (CharacterID, IPAddress, GamemasterID, Reason, Comment)"
+ " VALUES ($1::INTEGER, $2::INET, $3::INTEGER, $4::TEXT, $5::TEXT)");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 5, 1);
+ ParamInt(&Params, CharacterID);
+ ParamIPAddress(&Params, IPAddress);
+ ParamInt(&Params, GamemasterID);
+ ParamText(&Params, Reason);
+ ParamText(&Params, Comment);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_COMMAND_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ return true;
}
-bool IsAccountBanished(TDatabase *Database, int AccountID, bool *Result){
- return false;
+bool IsAccountBanished(TDatabase *Database, int AccountID, bool *Banished){
+ ASSERT(Database != NULL && Banished != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "SELECT 1 FROM Banishments"
+ " WHERE AccountID = $1::INTEGER"
+ " AND (Until = Issued OR Until > CURRENT_TIMESTAMP)");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 1, 1);
+ ParamInt(&Params, AccountID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ *Banished = (PQntuples(Result) > 0);
+ return true;
}
bool GetBanishmentStatus(TDatabase *Database, int CharacterID, TBanishmentStatus *Status){
- return false;
+ ASSERT(Database != NULL && Status != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "SELECT B.FinalWarning, (B.Until = B.Issued OR B.Until > CURRENT_TIMESTAMP)"
+ " FROM Banishments AS B"
+ " LEFT JOIN Characters AS C ON C.AccountID = B.AccountID"
+ " WHERE C.CharacterID = $1::INTEGER");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 1, 1);
+ ParamInt(&Params, CharacterID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ int NumRows = 0;
+ memset(Status, 0, sizeof(TBanishmentStatus));
+ for(int Row = 0; Row < NumRows; Row += 1){
+ Status->TimesBanished += 1;
+
+ if(GetResultBool(Result, Row, 0)){
+ Status->FinalWarning = true;
+ }
+
+ if(GetResultBool(Result, Row, 1)){
+ Status->Banished = true;
+ }
+ }
+
+ return true;
}
bool InsertBanishment(TDatabase *Database, int CharacterID, int IPAddress, int GamemasterID,
const char *Reason, const char *Comment, bool FinalWarning, int Duration, int *BanishmentID){
- return false;
+ ASSERT(Database != NULL && Reason != NULL && Comment != NULL && BanishmentID != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "INSERT INTO Banishments (AccountID, IPAddress, GamemasterID,"
+ " Reason, Comment, FinalWarning, Issued, Until)"
+ " SELECT AccountID, $2::INET, $3::INTEGER, $4::TEXT, $5::TEXT,"
+ " $6::BOOLEAN, CURRENT_TIMESTAMP, (CURRENT_TIMESTAMP + $7::INTERVAL)"
+ " FROM Characters WHERE CharacterID = $1::INTEGER"
+ " RETURNING BanishmentID");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 7, 1);
+ ParamInt(&Params, CharacterID);
+ ParamIPAddress(&Params, IPAddress);
+ ParamInt(&Params, GamemasterID);
+ ParamText(&Params, Reason);
+ ParamText(&Params, Comment);
+ ParamBool(&Params, FinalWarning);
+ ParamInterval(&Params, Duration);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ *BanishmentID = (PQntuples(Result) > 0 ? GetResultInt(Result, 0, 0) : 0);
+ return true;
}
-bool GetNotationCount(TDatabase *Database, int CharacterID, int *Result){
- return false;
+bool GetNotationCount(TDatabase *Database, int CharacterID, int *Notations){
+ ASSERT(Database != NULL && Notations != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "SELECT COUNT(*) FROM Notations WHERE CharacterID = $1::INTEGER");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 1, 1);
+ ParamInt(&Params, CharacterID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ if(PQntuples(Result) == 0){
+ LOG_ERR("Query returned no rows");
+ return false;
+ }
+
+ *Notations = GetResultInt(Result, 0, 0);
+ return true;
}
bool InsertNotation(TDatabase *Database, int CharacterID, int IPAddress,
int GamemasterID, const char *Reason, const char *Comment){
- return false;
+ ASSERT(Database != NULL && Reason != NULL && Comment != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "INSERT INTO Notations (CharacterID, IPAddress, GamemasterID, Reason, Comment)"
+ " VALUES ($1::INTEGER, $2::INET, $3::INTEGER, $4::TEXT, $5::TEXT)");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 5, 1);
+ ParamInt(&Params, CharacterID);
+ ParamIPAddress(&Params, IPAddress);
+ ParamInt(&Params, GamemasterID);
+ ParamText(&Params, Reason);
+ ParamText(&Params, Comment);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_COMMAND_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ return true;
}
-bool IsIPBanished(TDatabase *Database, int IPAddress, bool *Result){
- return false;
+bool IsIPBanished(TDatabase *Database, int IPAddress, bool *Banished){
+ ASSERT(Database != NULL && Banished != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "SELECT 1 FROM IPBanishments"
+ " WHERE IPAddress = $1::INET"
+ " AND (Until = Issued OR Until > CURRENT_TIMESTAMP)");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 1, 1);
+ ParamIPAddress(&Params, IPAddress);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ *Banished = (PQntuples(Result) > 0);
+ return true;
}
bool InsertIPBanishment(TDatabase *Database, int CharacterID, int IPAddress,
int GamemasterID, const char *Reason, const char *Comment, int Duration){
- return false;
+ ASSERT(Database != NULL && Reason != NULL && Comment != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "INSERT INTO IPBanishments (CharacterID, IPAddress,"
+ " GamemasterID, Reason, Comment, Issued, Until)"
+ " VALUES ($1::INTEGER, $2::INET, $3::INTEGER, $4::TEXT,"
+ " $5::TEXT, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP + $6::INTERVAL)");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 6, 1);
+ ParamInt(&Params, CharacterID);
+ ParamIPAddress(&Params, IPAddress);
+ ParamInt(&Params, GamemasterID);
+ ParamText(&Params, Reason);
+ ParamText(&Params, Comment);
+ ParamInterval(&Params, Duration);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_COMMAND_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ return true;
}
-bool IsStatementReported(TDatabase *Database, int WorldID, TStatement *Statement, bool *Result){
- return false;
+bool IsStatementReported(TDatabase *Database, int WorldID, TStatement *Statement, bool *Reported){
+ ASSERT(Database != NULL && Statement != NULL && Reported != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "SELECT 1 FROM Statements"
+ " WHERE WorldID = $1::INTEGER"
+ " AND Timestamp = $2::TIMESTAMPTZ"
+ " AND StatementID = $3::INTEGER");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 3, 1);
+ ParamInt(&Params, WorldID);
+ ParamTimestamp(&Params, Statement->Timestamp);
+ ParamInt(&Params, Statement->StatementID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ *Reported = (PQntuples(Result) > 0);
+ return true;
}
bool InsertStatements(TDatabase *Database, int WorldID, int NumStatements, TStatement *Statements){
- return false;
+ // NOTE(fusion): Use the `DO NOTHING` conflict resolution because different
+ // reports may include the same statements for context and I assume it's not
+ // uncommon to see overlaps.
+ ASSERT(Database != NULL && NumStatements > 0 && Statements != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "INSERT INTO Statements (WorldID, Timestamp, StatementID, CharacterID, Channel, Text)"
+ " VALUES ($1::INTEGER, $2::TIMESTAMPTZ, $3::INTEGER, $4::INTEGER, $5::TEXT, $6::TEXT)"
+ " ON CONFLICT DO NOTHING");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ for(int i = 0; i < NumStatements; i += 1){
+ if(Statements[i].StatementID == 0){
+ LOG_WARN("Skipping statement without id");
+ continue;
+ }
+
+ ParamBegin(&Params, 6, 1);
+ ParamInt(&Params, WorldID);
+ ParamTimestamp(&Params, Statements[i].Timestamp);
+ ParamInt(&Params, Statements[i].StatementID);
+ ParamInt(&Params, Statements[i].CharacterID);
+ ParamText(&Params, Statements[i].Channel);
+ ParamText(&Params, Statements[i].Text);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_COMMAND_OK){
+ LOG_ERR("Failed to insert statement %d: %s",
+ Statements[i].StatementID,
+ PQerrorMessage(Database->Handle));
+ return false;
+ }
+ }
+
+ return true;
}
bool InsertReportedStatement(TDatabase *Database, int WorldID, TStatement *Statement,
int BanishmentID, int ReporterID, const char *Reason, const char *Comment){
- return false;
-}
+ ASSERT(Database != NULL && Statement != NULL && Reason != NULL && Comment != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "INSERT INTO ReportedStatements (WorldID, Timestamp, StatementID,"
+ " CharacterID, BanishmentID, ReporterID, Reason, Comment)"
+ " VALUES ($1::INTEGER, $2::TIMESTAMPTZ, $3::INTEGER, $4::INTEGER,"
+ " $5::INTEGER, $6::INTEGER, $7::TEXT, $8::TEXT)");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 8, 1);
+ ParamInt(&Params, WorldID);
+ ParamTimestamp(&Params, Statement->Timestamp);
+ ParamInt(&Params, Statement->StatementID);
+ ParamInt(&Params, Statement->CharacterID);
+ ParamInt(&Params, BanishmentID);
+ ParamInt(&Params, ReporterID);
+ ParamText(&Params, Reason);
+ ParamText(&Params, Comment);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_COMMAND_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+ return true;
+}
// Info Tables
//==============================================================================
bool GetKillStatistics(TDatabase *Database, int WorldID, DynamicArray<TKillStatistics> *Stats){
- return false;
+ ASSERT(Database != NULL && Stats != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "SELECT RaceName, TimesKilled, PlayersKilled"
+ " FROM KillStatistics WHERE WorldID = $1::INTEGER");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 1, 1);
+ ParamInt(&Params, WorldID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ int NumRows = PQntuples(Result);
+ for(int Row = 0; Row < NumRows; Row += 1){
+ TKillStatistics Entry = {};
+ StringBufCopy(Entry.RaceName, GetResultText(Result, Row, 0));
+ Entry.TimesKilled = GetResultInt(Result, Row, 1);
+ Entry.PlayersKilled = GetResultInt(Result, Row, 2);
+ Stats->Push(Entry);
+ }
+
+ return true;
}
bool MergeKillStatistics(TDatabase *Database, int WorldID, int NumStats, TKillStatistics *Stats){
- return false;
+ ASSERT(Database != NULL && NumStats > 0 && Stats != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "INSERT INTO KillStatistics (WorldID, RaceName, TimesKilled, PlayersKilled)"
+ " VALUES ($1::INTEGER, $2::TEXT, $3::INTEGER, $4::INTEGER)"
+ " ON CONFLICT DO UPDATE SET TimesKilled = TimesKilled + EXCLUDED.TimesKilled,"
+ " PlayersKilled = PlayersKilled + EXCLUDED.PlayersKilled");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ for(int i = 0; i < NumStats; i += 1){
+ ParamBegin(&Params, 4, 1);
+ ParamInt(&Params, WorldID);
+ ParamText(&Params, Stats[i].RaceName);
+ ParamInt(&Params, Stats[i].TimesKilled);
+ ParamInt(&Params, Stats[i].PlayersKilled);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_COMMAND_OK){
+ LOG_ERR("Failed to merge \"%s\" stats: %s",
+ Stats[i].RaceName,
+ PQerrorMessage(Database->Handle));
+ return false;
+ }
+ }
+
+ return true;
}
bool GetOnlineCharacters(TDatabase *Database, int WorldID, DynamicArray<TOnlineCharacter> *Characters){
- return false;
+ ASSERT(Database != NULL && Characters != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "SELECT Name, Level, Profession"
+ " FROM OnlineCharacters WHERE WorldID = $1::INTEGER");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 1, 1);
+ ParamInt(&Params, WorldID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_TUPLES_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ int NumRows = PQntuples(Result);
+ for(int Row = 0; Row < NumRows; Row += 1){
+ TOnlineCharacter Character = {};
+ StringBufCopy(Character.Name, GetResultText(Result, Row, 0));
+ Character.Level = GetResultInt(Result, Row, 1);
+ StringBufCopy(Character.Profession, GetResultText(Result, Row, 2));
+ Characters->Push(Character);
+ }
+
+ return true;
}
bool DeleteOnlineCharacters(TDatabase *Database, int WorldID){
- return false;
+ ASSERT(Database != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "DELETE FROM OnlineCharacters WHERE WorldID = $1::INTEGER");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 1, 1);
+ ParamInt(&Params, WorldID);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_COMMAND_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ return true;
}
bool InsertOnlineCharacters(TDatabase *Database, int WorldID,
int NumCharacters, TOnlineCharacter *Characters){
- return false;
+ ASSERT(Database != NULL && Characters != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "INSERT INTO OnlineCharacters (WorldID, Name, Level, Profession)"
+ " VALUES ($1::INTEGER, $2::TEXT, $3::INTEGER, $4::TEXT)");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ for(int i = 0; i < NumCharacters; i += 1){
+ ParamBegin(&Params, 4, 1);
+ ParamInt(&Params, WorldID);
+ ParamText(&Params, Characters[i].Name);
+ ParamInt(&Params, Characters[i].Level);
+ ParamText(&Params, Characters[i].Profession);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_COMMAND_OK){
+ LOG_ERR("Failed to insert character \"%s\": %s",
+ Characters[i].Name,
+ PQerrorMessage(Database->Handle));
+ return false;
+ }
+ }
+
+ return true;
}
bool CheckOnlineRecord(TDatabase *Database, int WorldID, int NumCharacters, bool *NewRecord){
- return false;
+ ASSERT(Database != NULL && NewRecord != NULL);
+ const char *Stmt = PrepareQuery(Database,
+ "UPDATE Worlds SET OnlineRecord = $2::INTEGER,"
+ " OnlineRecordTimestamp = CURRENT_TIMESTAMP"
+ " WHERE WorldID = $1::INTEGER AND OnlineRecord < $2::INTEGER");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ ParamBuffer Params = {};
+ ParamBegin(&Params, 2, 1);
+ ParamInt(&Params, WorldID);
+ ParamInt(&Params, NumCharacters);
+ PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
+ Params.Values, Params.Lengths, Params.Formats, 1);
+ AutoResultClear ResultGuard(Result);
+ if(PQresultStatus(Result) != PGRES_COMMAND_OK){
+ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
+ return false;
+ }
+
+ *NewRecord = (ResultAffectedRows(Result) > 0);
+ return true;
}
#endif //DATABASE_POSTGRESQL