change how guild data is stored
This commit is contained in:
parent
0ff6217227
commit
27a0df43b3
@ -2,6 +2,8 @@
|
|||||||
-- the database in some partial state. Also, notice we don't use `IF NOT EXISTS`
|
-- the database in some partial state. Also, notice we don't use `IF NOT EXISTS`
|
||||||
-- because it could also leave the database in a bad state if for example, some
|
-- because it could also leave the database in a bad state if for example, some
|
||||||
-- old version of a table already existed.
|
-- old version of a table already existed.
|
||||||
|
--==============================================================================
|
||||||
|
|
||||||
BEGIN;
|
BEGIN;
|
||||||
|
|
||||||
-- IMPORTANT(fusion): PosgreSQL doesn't have a defined `NOCASE` collation like
|
-- IMPORTANT(fusion): PosgreSQL doesn't have a defined `NOCASE` collation like
|
||||||
@ -78,9 +80,6 @@ CREATE TABLE Characters (
|
|||||||
AccountID INTEGER NOT NULL,
|
AccountID INTEGER NOT NULL,
|
||||||
Name TEXT NOT NULL COLLATE NOCASE,
|
Name TEXT NOT NULL COLLATE NOCASE,
|
||||||
Sex SMALLINT NOT NULL,
|
Sex SMALLINT NOT NULL,
|
||||||
Guild TEXT NOT NULL COLLATE NOCASE DEFAULT '',
|
|
||||||
Rank TEXT NOT NULL COLLATE NOCASE DEFAULT '',
|
|
||||||
Title TEXT NOT NULL DEFAULT '',
|
|
||||||
Level SMALLINT NOT NULL DEFAULT 0,
|
Level SMALLINT NOT NULL DEFAULT 0,
|
||||||
Profession TEXT NOT NULL DEFAULT '',
|
Profession TEXT NOT NULL DEFAULT '',
|
||||||
Residence TEXT NOT NULL DEFAULT '',
|
Residence TEXT NOT NULL DEFAULT '',
|
||||||
@ -93,7 +92,6 @@ CREATE TABLE Characters (
|
|||||||
);
|
);
|
||||||
CREATE INDEX CharactersWorldIndex ON Characters(WorldID, IsOnline);
|
CREATE INDEX CharactersWorldIndex ON Characters(WorldID, IsOnline);
|
||||||
CREATE INDEX CharactersAccountIndex ON Characters(AccountID, IsOnline);
|
CREATE INDEX CharactersAccountIndex ON Characters(AccountID, IsOnline);
|
||||||
CREATE INDEX CharactersGuildIndex ON Characters(Guild, Rank);
|
|
||||||
|
|
||||||
-- NOTE(fusion): It seems `RIGHT` is a reserved keyword and trying to use
|
-- NOTE(fusion): It seems `RIGHT` is a reserved keyword and trying to use
|
||||||
-- it as a column name will generate an error.
|
-- it as a column name will generate an error.
|
||||||
@ -136,6 +134,46 @@ CREATE TABLE LoginAttempts (
|
|||||||
CREATE INDEX LoginAttemptsAccountIndex ON LoginAttempts(AccountID, Timestamp);
|
CREATE INDEX LoginAttemptsAccountIndex ON LoginAttempts(AccountID, Timestamp);
|
||||||
CREATE INDEX LoginAttemptsAddressIndex ON LoginAttempts(IPAddress, Timestamp);
|
CREATE INDEX LoginAttemptsAddressIndex ON LoginAttempts(IPAddress, Timestamp);
|
||||||
|
|
||||||
|
-- Guild Tables
|
||||||
|
--==============================================================================
|
||||||
|
CREATE TABLE Guilds (
|
||||||
|
WorldID INTEGER NOT NULL,
|
||||||
|
GuildID INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY,
|
||||||
|
Name TEXT NOT NULL COLLATE NOCASE,
|
||||||
|
LeaderID INTEGER NOT NULL,
|
||||||
|
Created TIMESTAMPTZ NOT NULL,
|
||||||
|
PRIMARY KEY (GuildID),
|
||||||
|
UNIQUE (Name),
|
||||||
|
UNIQUE (LeaderID)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE GuildRanks (
|
||||||
|
GuildID INTEGER NOT NULL,
|
||||||
|
Rank SMALLINT NOT NULL,
|
||||||
|
Name TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (GuildID, Rank)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE GuildMembers (
|
||||||
|
GuildID INTEGER NOT NULL,
|
||||||
|
CharacterID INTEGER NOT NULL,
|
||||||
|
Rank SMALLINT NOT NULL,
|
||||||
|
Title TEXT NOT NULL,
|
||||||
|
Joined TIMESTAMPTZ NOT NULL,
|
||||||
|
PRIMARY KEY (CharacterID)
|
||||||
|
);
|
||||||
|
CREATE INDEX GuildMembersGuildIndex ON GuildMembers(GuildID, Rank);
|
||||||
|
|
||||||
|
CREATE TABLE GuildInvites (
|
||||||
|
GuildID INTEGER NOT NULL,
|
||||||
|
CharacterID INTEGER NOT NULL,
|
||||||
|
RecruiterID INTEGER NOT NULL,
|
||||||
|
Timestamp TIMESTAMPTZ NOT NULL,
|
||||||
|
PRIMARY KEY (GuildID, CharacterID)
|
||||||
|
);
|
||||||
|
CREATE INDEX GuildInvitesCharacterIndex ON GuildInvites(CharacterID);
|
||||||
|
CREATE INDEX GuildInvitesRecruiterIndex ON GuildInvites(RecruiterID);
|
||||||
|
|
||||||
-- House Tables
|
-- House Tables
|
||||||
--==============================================================================
|
--==============================================================================
|
||||||
CREATE TABLE Houses (
|
CREATE TABLE Houses (
|
||||||
|
|||||||
54
postgres/z-002-migrate-v02-to-v03.sql
Normal file
54
postgres/z-002-migrate-v02-to-v03.sql
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
-- NOTE(fusion): This file contains the migration script from v0.2 to v0.3. It's
|
||||||
|
-- put inside a transaction to avoid errors from leaving the database in some
|
||||||
|
-- partial state.
|
||||||
|
-- These changes are already present in the latest `schema.sql`, so trying to
|
||||||
|
-- apply them on a newly created database will result in errors.
|
||||||
|
--==============================================================================
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
DROP INDEX CharactersGuildIndex;
|
||||||
|
ALTER TABLE Characters DROP COLUMN Guild;
|
||||||
|
ALTER TABLE Characters DROP COLUMN Rank;
|
||||||
|
ALTER TABLE Characters DROP COLUMN Title;
|
||||||
|
|
||||||
|
CREATE TABLE Guilds (
|
||||||
|
WorldID INTEGER NOT NULL,
|
||||||
|
GuildID INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY,
|
||||||
|
Name TEXT NOT NULL COLLATE NOCASE,
|
||||||
|
LeaderID INTEGER NOT NULL,
|
||||||
|
Created TIMESTAMPTZ NOT NULL,
|
||||||
|
PRIMARY KEY (GuildID),
|
||||||
|
UNIQUE (Name),
|
||||||
|
UNIQUE (LeaderID)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE GuildRanks (
|
||||||
|
GuildID INTEGER NOT NULL,
|
||||||
|
Rank SMALLINT NOT NULL,
|
||||||
|
Name TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (GuildID, Rank)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE GuildMembers (
|
||||||
|
GuildID INTEGER NOT NULL,
|
||||||
|
CharacterID INTEGER NOT NULL,
|
||||||
|
Rank SMALLINT NOT NULL,
|
||||||
|
Title TEXT NOT NULL,
|
||||||
|
Joined TIMESTAMPTZ NOT NULL,
|
||||||
|
PRIMARY KEY (CharacterID)
|
||||||
|
);
|
||||||
|
CREATE INDEX GuildMembersGuildIndex ON GuildMembers(GuildID, Rank);
|
||||||
|
|
||||||
|
CREATE TABLE GuildInvites (
|
||||||
|
GuildID INTEGER NOT NULL,
|
||||||
|
CharacterID INTEGER NOT NULL,
|
||||||
|
RecruiterID INTEGER NOT NULL,
|
||||||
|
Timestamp TIMESTAMPTZ NOT NULL,
|
||||||
|
PRIMARY KEY (GuildID, CharacterID)
|
||||||
|
);
|
||||||
|
CREATE INDEX GuildInvitesCharacterIndex ON GuildInvites(CharacterID);
|
||||||
|
CREATE INDEX GuildInvitesRecruiterIndex ON GuildInvites(RecruiterID);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
@ -1,5 +1,7 @@
|
|||||||
-- NOTE(fusion): This file contains sample initial data. It's wrapped into a
|
-- NOTE(fusion): This file contains sample initial data. It's put inside a
|
||||||
-- transaction to avoid errors from leaving the database in some partial state.
|
-- transaction to avoid errors from leaving the database in some partial state.
|
||||||
|
--==============================================================================
|
||||||
|
|
||||||
BEGIN;
|
BEGIN;
|
||||||
|
|
||||||
-- 111111/tibia
|
-- 111111/tibia
|
||||||
@ -57,9 +57,6 @@ CREATE TABLE Characters (
|
|||||||
AccountID INTEGER NOT NULL,
|
AccountID INTEGER NOT NULL,
|
||||||
Name TEXT NOT NULL COLLATE NOCASE,
|
Name TEXT NOT NULL COLLATE NOCASE,
|
||||||
Sex INTEGER NOT NULL,
|
Sex INTEGER NOT NULL,
|
||||||
Guild TEXT NOT NULL COLLATE NOCASE DEFAULT '',
|
|
||||||
Rank TEXT NOT NULL COLLATE NOCASE DEFAULT '',
|
|
||||||
Title TEXT NOT NULL DEFAULT '',
|
|
||||||
Level INTEGER NOT NULL DEFAULT 0,
|
Level INTEGER NOT NULL DEFAULT 0,
|
||||||
Profession TEXT NOT NULL DEFAULT '',
|
Profession TEXT NOT NULL DEFAULT '',
|
||||||
Residence TEXT NOT NULL DEFAULT '',
|
Residence TEXT NOT NULL DEFAULT '',
|
||||||
@ -72,7 +69,6 @@ CREATE TABLE Characters (
|
|||||||
);
|
);
|
||||||
CREATE INDEX CharactersWorldIndex ON Characters(WorldID, IsOnline);
|
CREATE INDEX CharactersWorldIndex ON Characters(WorldID, IsOnline);
|
||||||
CREATE INDEX CharactersAccountIndex ON Characters(AccountID, IsOnline);
|
CREATE INDEX CharactersAccountIndex ON Characters(AccountID, IsOnline);
|
||||||
CREATE INDEX CharactersGuildIndex ON Characters(Guild, Rank);
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
-- TODO(fusion): Have group rights instead of adding individual rights to characters?
|
-- TODO(fusion): Have group rights instead of adding individual rights to characters?
|
||||||
@ -123,6 +119,46 @@ CREATE TABLE LoginAttempts (
|
|||||||
CREATE INDEX LoginAttemptsAccountIndex ON LoginAttempts(AccountID, Timestamp);
|
CREATE INDEX LoginAttemptsAccountIndex ON LoginAttempts(AccountID, Timestamp);
|
||||||
CREATE INDEX LoginAttemptsAddressIndex ON LoginAttempts(IPAddress, Timestamp);
|
CREATE INDEX LoginAttemptsAddressIndex ON LoginAttempts(IPAddress, Timestamp);
|
||||||
|
|
||||||
|
-- Guild Tables
|
||||||
|
--==============================================================================
|
||||||
|
CREATE TABLE Guilds (
|
||||||
|
WorldID INTEGER NOT NULL,
|
||||||
|
GuildID INTEGER NOT NULL,
|
||||||
|
Name TEXT NOT NULL COLLATE NOCASE,
|
||||||
|
LeaderID INTEGER NOT NULL,
|
||||||
|
Created INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (GuildID),
|
||||||
|
UNIQUE (Name),
|
||||||
|
UNIQUE (LeaderID)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE GuildRanks (
|
||||||
|
GuildID INTEGER NOT NULL,
|
||||||
|
Rank INTEGER NOT NULL,
|
||||||
|
Name TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (GuildID, Rank)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE GuildMembers (
|
||||||
|
GuildID INTEGER NOT NULL,
|
||||||
|
CharacterID INTEGER NOT NULL,
|
||||||
|
Rank INTEGER NOT NULL,
|
||||||
|
Title TEXT NOT NULL,
|
||||||
|
Joined INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (CharacterID)
|
||||||
|
);
|
||||||
|
CREATE INDEX GuildMembersGuildIndex ON GuildMembers(GuildID, Rank);
|
||||||
|
|
||||||
|
CREATE TABLE GuildInvites (
|
||||||
|
GuildID INTEGER NOT NULL,
|
||||||
|
CharacterID INTEGER NOT NULL,
|
||||||
|
RecruiterID INTEGER NOT NULL,
|
||||||
|
Timestamp INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (GuildID, CharacterID)
|
||||||
|
);
|
||||||
|
CREATE INDEX GuildInvitesCharacterIndex ON GuildInvites(CharacterID);
|
||||||
|
CREATE INDEX GuildInvitesRecruiterIndex ON GuildInvites(RecruiterID);
|
||||||
|
|
||||||
-- House Tables
|
-- House Tables
|
||||||
--==============================================================================
|
--==============================================================================
|
||||||
CREATE TABLE Houses (
|
CREATE TABLE Houses (
|
||||||
|
|||||||
@ -2,10 +2,10 @@
|
|||||||
-- must be manually executed as `sqlite3 -bail -echo tibia.db < migration.sql`
|
-- must be manually executed as `sqlite3 -bail -echo tibia.db < migration.sql`
|
||||||
-- because the original schema didn't have a `Patches` table which is necessary
|
-- because the original schema didn't have a `Patches` table which is necessary
|
||||||
-- with the new automatic patching system. Future migration scripts can be placed
|
-- with the new automatic patching system. Future migration scripts can be placed
|
||||||
-- in `patches/` for automatic execution but not this one, unfortunately.
|
-- in `patches/` for automatic execution but not this one, unfortunately. For
|
||||||
|
-- more details see `sqlite/README.txt`.
|
||||||
-- These changes are already present in the latest `schema.sql`, so trying to
|
-- These changes are already present in the latest `schema.sql`, so trying to
|
||||||
-- apply it to a newly created database will result in errors. For more details
|
-- apply them to a newly created database will result in errors.
|
||||||
-- see `sqlite/README.txt`.
|
|
||||||
--==============================================================================
|
--==============================================================================
|
||||||
|
|
||||||
BEGIN;
|
BEGIN;
|
||||||
|
|||||||
50
sqlite/z-002-migrate-v02-to-v03.sql
Normal file
50
sqlite/z-002-migrate-v02-to-v03.sql
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
-- NOTE(fusion): This file contains the migration script from v0.2 to v0.3. It
|
||||||
|
-- can be executed automatically as a patch if placed at `sqlite/patches`. For
|
||||||
|
-- more details see `sqlite/README.txt`.
|
||||||
|
-- These changes are already present in the latest `schema.sql`, so trying to
|
||||||
|
-- apply them to a newly created database will result in errors.
|
||||||
|
--==============================================================================
|
||||||
|
|
||||||
|
DROP INDEX CharactersGuildIndex;
|
||||||
|
ALTER TABLE Characters DROP COLUMN Guild;
|
||||||
|
ALTER TABLE Characters DROP COLUMN Rank;
|
||||||
|
ALTER TABLE Characters DROP COLUMN Title;
|
||||||
|
|
||||||
|
CREATE TABLE Guilds (
|
||||||
|
WorldID INTEGER NOT NULL,
|
||||||
|
GuildID INTEGER NOT NULL,
|
||||||
|
Name TEXT NOT NULL COLLATE NOCASE,
|
||||||
|
LeaderID INTEGER NOT NULL,
|
||||||
|
Created INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (GuildID),
|
||||||
|
UNIQUE (Name),
|
||||||
|
UNIQUE (LeaderID)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE GuildRanks (
|
||||||
|
GuildID INTEGER NOT NULL,
|
||||||
|
Rank INTEGER NOT NULL,
|
||||||
|
Name TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (GuildID, Rank)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE GuildMembers (
|
||||||
|
GuildID INTEGER NOT NULL,
|
||||||
|
CharacterID INTEGER NOT NULL,
|
||||||
|
Rank INTEGER NOT NULL,
|
||||||
|
Title TEXT NOT NULL,
|
||||||
|
Joined INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (CharacterID)
|
||||||
|
);
|
||||||
|
CREATE INDEX GuildMembersGuildIndex ON GuildMembers(GuildID, Rank);
|
||||||
|
|
||||||
|
CREATE TABLE GuildInvites (
|
||||||
|
GuildID INTEGER NOT NULL,
|
||||||
|
CharacterID INTEGER NOT NULL,
|
||||||
|
RecruiterID INTEGER NOT NULL,
|
||||||
|
Timestamp INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (GuildID, CharacterID)
|
||||||
|
);
|
||||||
|
CREATE INDEX GuildInvitesCharacterIndex ON GuildInvites(CharacterID);
|
||||||
|
CREATE INDEX GuildInvitesRecruiterIndex ON GuildInvites(RecruiterID);
|
||||||
|
|
||||||
@ -1771,8 +1771,7 @@ bool GetCharacterID(TDatabase *Database, int WorldID, const char *CharacterName,
|
|||||||
bool GetCharacterLoginData(TDatabase *Database, const char *CharacterName, TCharacterLoginData *Character){
|
bool GetCharacterLoginData(TDatabase *Database, const char *CharacterName, TCharacterLoginData *Character){
|
||||||
ASSERT(Database != NULL && CharacterName != NULL && Character != NULL);
|
ASSERT(Database != NULL && CharacterName != NULL && Character != NULL);
|
||||||
const char *Stmt = PrepareQuery(Database,
|
const char *Stmt = PrepareQuery(Database,
|
||||||
"SELECT WorldID, CharacterID, AccountID, Name,"
|
"SELECT WorldID, CharacterID, AccountID, Name, Sex, Deleted"
|
||||||
" Sex, Guild, Rank, Title, Deleted"
|
|
||||||
" FROM Characters WHERE Name = $1::TEXT");
|
" FROM Characters WHERE Name = $1::TEXT");
|
||||||
if(Stmt == NULL){
|
if(Stmt == NULL){
|
||||||
LOG_ERR("Failed to prepare query");
|
LOG_ERR("Failed to prepare query");
|
||||||
@ -1797,10 +1796,7 @@ bool GetCharacterLoginData(TDatabase *Database, const char *CharacterName, TChar
|
|||||||
Character->AccountID = GetResultInt(Result, 0, 2);
|
Character->AccountID = GetResultInt(Result, 0, 2);
|
||||||
StringBufCopy(Character->Name, GetResultText(Result, 0, 3));
|
StringBufCopy(Character->Name, GetResultText(Result, 0, 3));
|
||||||
Character->Sex = GetResultInt(Result, 0, 4);
|
Character->Sex = GetResultInt(Result, 0, 4);
|
||||||
StringBufCopy(Character->Guild, GetResultText(Result, 0, 5));
|
Character->Deleted = GetResultBool(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;
|
return true;
|
||||||
@ -1809,7 +1805,7 @@ bool GetCharacterLoginData(TDatabase *Database, const char *CharacterName, TChar
|
|||||||
bool GetCharacterProfile(TDatabase *Database, const char *CharacterName, TCharacterProfile *Character){
|
bool GetCharacterProfile(TDatabase *Database, const char *CharacterName, TCharacterProfile *Character){
|
||||||
ASSERT(Database != NULL && CharacterName != NULL && Character != NULL);
|
ASSERT(Database != NULL && CharacterName != NULL && Character != NULL);
|
||||||
const char *Stmt = PrepareQuery(Database,
|
const char *Stmt = PrepareQuery(Database,
|
||||||
"SELECT C.Name, W.Name, C.Sex, C.Guild, C.Rank, C.Title, C.Level,"
|
"SELECT C.CharacterID, C.Name, W.Name, C.Sex, C.Level,"
|
||||||
" C.Profession, C.Residence, C.LastLoginTime, C.IsOnline,"
|
" C.Profession, C.Residence, C.LastLoginTime, C.IsOnline,"
|
||||||
" C.Deleted, GREATEST(A.PremiumEnd - CURRENT_TIMESTAMP, '0')"
|
" C.Deleted, GREATEST(A.PremiumEnd - CURRENT_TIMESTAMP, '0')"
|
||||||
" FROM Characters AS C"
|
" FROM Characters AS C"
|
||||||
@ -1837,19 +1833,17 @@ bool GetCharacterProfile(TDatabase *Database, const char *CharacterName, TCharac
|
|||||||
|
|
||||||
memset(Character, 0, sizeof(TCharacterProfile));
|
memset(Character, 0, sizeof(TCharacterProfile));
|
||||||
if(PQntuples(Result) > 0){
|
if(PQntuples(Result) > 0){
|
||||||
StringBufCopy(Character->Name, GetResultText(Result, 0, 0));
|
Character->CharacterID = GetResultInt(Result, 0, 0);
|
||||||
StringBufCopy(Character->World, GetResultText(Result, 0, 1));
|
StringBufCopy(Character->Name, GetResultText(Result, 0, 1));
|
||||||
Character->Sex = GetResultInt(Result, 0, 2);
|
StringBufCopy(Character->World, GetResultText(Result, 0, 2));
|
||||||
StringBufCopy(Character->Guild, GetResultText(Result, 0, 3));
|
Character->Sex = GetResultInt(Result, 0, 3);
|
||||||
StringBufCopy(Character->Rank, GetResultText(Result, 0, 4));
|
Character->Level = GetResultInt(Result, 0, 4);
|
||||||
StringBufCopy(Character->Title, GetResultText(Result, 0, 5));
|
StringBufCopy(Character->Profession, GetResultText(Result, 0, 5));
|
||||||
Character->Level = GetResultInt(Result, 0, 6);
|
StringBufCopy(Character->Residence, GetResultText(Result, 0, 6));
|
||||||
StringBufCopy(Character->Profession, GetResultText(Result, 0, 7));
|
Character->LastLogin = GetResultTimestamp(Result, 0, 7);
|
||||||
StringBufCopy(Character->Residence, GetResultText(Result, 0, 8));
|
Character->Online = GetResultBool(Result, 0, 8);
|
||||||
Character->LastLogin = GetResultTimestamp(Result, 0, 9);
|
Character->Deleted = GetResultBool(Result, 0, 9);
|
||||||
Character->Online = GetResultBool(Result, 0, 10);
|
Character->PremiumDays = RoundSecondsToDays(GetResultInterval(Result, 0, 10));
|
||||||
Character->Deleted = GetResultBool(Result, 0, 11);
|
|
||||||
Character->PremiumDays = RoundSecondsToDays(GetResultInterval(Result, 0, 12));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -1915,8 +1909,8 @@ bool GetGuildLeaderStatus(TDatabase *Database, int WorldID, int CharacterID, boo
|
|||||||
ASSERT(Database != NULL && GuildLeader != NULL);
|
ASSERT(Database != NULL && GuildLeader != NULL);
|
||||||
// NOTE(fusion): Same as `DecrementIsOnline`.
|
// NOTE(fusion): Same as `DecrementIsOnline`.
|
||||||
const char *Stmt = PrepareQuery(Database,
|
const char *Stmt = PrepareQuery(Database,
|
||||||
"SELECT Guild, Rank FROM Characters"
|
"SELECT 1 FROM Guilds"
|
||||||
" WHERE WorldID = $1::INTEGER AND CharacterID = $2::INTEGER");
|
" WHERE WorldID = $1::INTEGER AND LeaderID = $2::INTEGER");
|
||||||
if(Stmt == NULL){
|
if(Stmt == NULL){
|
||||||
LOG_ERR("Failed to prepare query");
|
LOG_ERR("Failed to prepare query");
|
||||||
return false;
|
return false;
|
||||||
@ -1934,16 +1928,8 @@ bool GetGuildLeaderStatus(TDatabase *Database, int WorldID, int CharacterID, boo
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
*GuildLeader = false;
|
*GuildLeader = (PQntuples(Result) > 0);
|
||||||
if(PQntuples(Result) > 0){
|
return true;
|
||||||
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){
|
bool IncrementIsOnline(TDatabase *Database, int WorldID, int CharacterID){
|
||||||
@ -2341,6 +2327,46 @@ bool GetIPAddressFailedLoginAttempts(TDatabase *Database, int IPAddress, int Tim
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Guild Tables
|
||||||
|
//==============================================================================
|
||||||
|
bool GetCharacterGuildData(TDatabase *Database, int CharacterID, TCharacterGuildData *GuildData){
|
||||||
|
ASSERT(Database != NULL && GuildData != NULL);
|
||||||
|
const char *Stmt = PrepareQuery(Database,
|
||||||
|
"SELECT G.GuildID, R.Rank, G.Name, R.Name, M.Title"
|
||||||
|
" FROM Characters AS C"
|
||||||
|
" LEFT JOIN GuildMembers AS M ON M.CharacterID = C.CharacterID"
|
||||||
|
" LEFT JOIN Guilds AS G ON G.GuildID = M.GuildID"
|
||||||
|
" LEFT JOIN GuildRanks AS R"
|
||||||
|
" ON R.GuildID = M.GuildID AND R.Rank = M.Rank"
|
||||||
|
" 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(GuildData, 0, sizeof(TCharacterGuildData));
|
||||||
|
if(PQntuples(Result) > 0){
|
||||||
|
GuildData->GuildID = GetResultInt(Result, 0, 0);
|
||||||
|
GuildData->Rank = GetResultInt(Result, 0, 1);
|
||||||
|
StringBufCopy(GuildData->GuildName, GetResultText(Result, 0, 2));
|
||||||
|
StringBufCopy(GuildData->RankName, GetResultText(Result, 0, 3));
|
||||||
|
StringBufCopy(GuildData->Title, GetResultText(Result, 0, 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// House Tables
|
// House Tables
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
bool FinishHouseAuctions(TDatabase *Database, int WorldID, DynamicArray<THouseAuction> *Auctions){
|
bool FinishHouseAuctions(TDatabase *Database, int WorldID, DynamicArray<THouseAuction> *Auctions){
|
||||||
|
|||||||
@ -1022,8 +1022,7 @@ bool GetCharacterID(TDatabase *Database, int WorldID, const char *CharacterName,
|
|||||||
bool GetCharacterLoginData(TDatabase *Database, const char *CharacterName, TCharacterLoginData *Character){
|
bool GetCharacterLoginData(TDatabase *Database, const char *CharacterName, TCharacterLoginData *Character){
|
||||||
ASSERT(Database != NULL && CharacterName != NULL && Character != NULL);
|
ASSERT(Database != NULL && CharacterName != NULL && Character != NULL);
|
||||||
sqlite3_stmt *Stmt = PrepareQuery(Database,
|
sqlite3_stmt *Stmt = PrepareQuery(Database,
|
||||||
"SELECT WorldID, CharacterID, AccountID, Name,"
|
"SELECT WorldID, CharacterID, AccountID, Name, Sex, Deleted"
|
||||||
" Sex, Guild, Rank, Title, Deleted"
|
|
||||||
" FROM Characters WHERE Name = ?1");
|
" FROM Characters WHERE Name = ?1");
|
||||||
if(Stmt == NULL){
|
if(Stmt == NULL){
|
||||||
LOG_ERR("Failed to prepare query");
|
LOG_ERR("Failed to prepare query");
|
||||||
@ -1049,10 +1048,7 @@ bool GetCharacterLoginData(TDatabase *Database, const char *CharacterName, TChar
|
|||||||
Character->AccountID = sqlite3_column_int(Stmt, 2);
|
Character->AccountID = sqlite3_column_int(Stmt, 2);
|
||||||
StringBufCopy(Character->Name, (const char*)sqlite3_column_text(Stmt, 3));
|
StringBufCopy(Character->Name, (const char*)sqlite3_column_text(Stmt, 3));
|
||||||
Character->Sex = sqlite3_column_int(Stmt, 4);
|
Character->Sex = sqlite3_column_int(Stmt, 4);
|
||||||
StringBufCopy(Character->Guild, (const char*)sqlite3_column_text(Stmt, 5));
|
Character->Deleted = (sqlite3_column_int(Stmt, 5) != 0);
|
||||||
StringBufCopy(Character->Rank, (const char*)sqlite3_column_text(Stmt, 6));
|
|
||||||
StringBufCopy(Character->Title, (const char*)sqlite3_column_text(Stmt, 7));
|
|
||||||
Character->Deleted = (sqlite3_column_int(Stmt, 8) != 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -1061,7 +1057,7 @@ bool GetCharacterLoginData(TDatabase *Database, const char *CharacterName, TChar
|
|||||||
bool GetCharacterProfile(TDatabase *Database, const char *CharacterName, TCharacterProfile *Character){
|
bool GetCharacterProfile(TDatabase *Database, const char *CharacterName, TCharacterProfile *Character){
|
||||||
ASSERT(Database != NULL && CharacterName != NULL && Character != NULL);
|
ASSERT(Database != NULL && CharacterName != NULL && Character != NULL);
|
||||||
sqlite3_stmt *Stmt = PrepareQuery(Database,
|
sqlite3_stmt *Stmt = PrepareQuery(Database,
|
||||||
"SELECT C.Name, W.Name, C.Sex, C.Guild, C.Rank, C.Title, C.Level,"
|
"SELECT C.CharacterID, C.Name, W.Name, C.Sex, C.Level,"
|
||||||
" C.Profession, C.Residence, C.LastLoginTime, C.IsOnline,"
|
" C.Profession, C.Residence, C.LastLoginTime, C.IsOnline,"
|
||||||
" C.Deleted, MAX(A.PremiumEnd - UNIXEPOCH(), 0)"
|
" C.Deleted, MAX(A.PremiumEnd - UNIXEPOCH(), 0)"
|
||||||
" FROM Characters AS C"
|
" FROM Characters AS C"
|
||||||
@ -1090,19 +1086,17 @@ bool GetCharacterProfile(TDatabase *Database, const char *CharacterName, TCharac
|
|||||||
|
|
||||||
memset(Character, 0, sizeof(TCharacterProfile));
|
memset(Character, 0, sizeof(TCharacterProfile));
|
||||||
if(ErrorCode == SQLITE_ROW){
|
if(ErrorCode == SQLITE_ROW){
|
||||||
StringBufCopy(Character->Name, (const char*)sqlite3_column_text(Stmt, 0));
|
Character->CharacterID = sqlite3_column_int(Stmt, 0);
|
||||||
StringBufCopy(Character->World, (const char*)sqlite3_column_text(Stmt, 1));
|
StringBufCopy(Character->Name, (const char*)sqlite3_column_text(Stmt, 1));
|
||||||
Character->Sex = sqlite3_column_int(Stmt, 2);
|
StringBufCopy(Character->World, (const char*)sqlite3_column_text(Stmt, 2));
|
||||||
StringBufCopy(Character->Guild, (const char*)sqlite3_column_text(Stmt, 3));
|
Character->Sex = sqlite3_column_int(Stmt, 3);
|
||||||
StringBufCopy(Character->Rank, (const char*)sqlite3_column_text(Stmt, 4));
|
Character->Level = sqlite3_column_int(Stmt, 4);
|
||||||
StringBufCopy(Character->Title, (const char*)sqlite3_column_text(Stmt, 5));
|
StringBufCopy(Character->Profession, (const char*)sqlite3_column_text(Stmt, 5));
|
||||||
Character->Level = sqlite3_column_int(Stmt, 6);
|
StringBufCopy(Character->Residence, (const char*)sqlite3_column_text(Stmt, 6));
|
||||||
StringBufCopy(Character->Profession, (const char*)sqlite3_column_text(Stmt, 7));
|
Character->LastLogin = sqlite3_column_int(Stmt, 7);
|
||||||
StringBufCopy(Character->Residence, (const char*)sqlite3_column_text(Stmt, 8));
|
Character->Online = (sqlite3_column_int(Stmt, 8) != 0);
|
||||||
Character->LastLogin = sqlite3_column_int(Stmt, 9);
|
Character->Deleted = (sqlite3_column_int(Stmt, 9) != 0);
|
||||||
Character->Online = (sqlite3_column_int(Stmt, 10) != 0);
|
Character->PremiumDays = RoundSecondsToDays(sqlite3_column_int(Stmt, 10));
|
||||||
Character->Deleted = (sqlite3_column_int(Stmt, 11) != 0);
|
|
||||||
Character->PremiumDays = RoundSecondsToDays(sqlite3_column_int(Stmt, 12));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -1168,8 +1162,8 @@ bool GetGuildLeaderStatus(TDatabase *Database, int WorldID, int CharacterID, boo
|
|||||||
ASSERT(Database != NULL && GuildLeader != NULL);
|
ASSERT(Database != NULL && GuildLeader != NULL);
|
||||||
// NOTE(fusion): Same as `DecrementIsOnline`.
|
// NOTE(fusion): Same as `DecrementIsOnline`.
|
||||||
sqlite3_stmt *Stmt = PrepareQuery(Database,
|
sqlite3_stmt *Stmt = PrepareQuery(Database,
|
||||||
"SELECT Guild, Rank FROM Characters"
|
"SELECT 1 FROM Guilds"
|
||||||
" WHERE WorldID = ?1 AND CharacterID = ?2");
|
" WHERE WorldID = ?1 AND LeaderID = ?2");
|
||||||
if(Stmt == NULL){
|
if(Stmt == NULL){
|
||||||
LOG_ERR("Failed to prepare query");
|
LOG_ERR("Failed to prepare query");
|
||||||
return false;
|
return false;
|
||||||
@ -1188,15 +1182,7 @@ bool GetGuildLeaderStatus(TDatabase *Database, int WorldID, int CharacterID, boo
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
*GuildLeader = false;
|
*GuildLeader = (ErrorCode == SQLITE_ROW);
|
||||||
if(ErrorCode == SQLITE_ROW){
|
|
||||||
const char *Guild = (const char*)sqlite3_column_text(Stmt, 0);
|
|
||||||
const char *Rank = (const char*)sqlite3_column_text(Stmt, 1);
|
|
||||||
if(Guild != NULL && !StringEmpty(Guild) && Rank != NULL && StringEqCI(Rank, "Leader")){
|
|
||||||
*GuildLeader = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1592,6 +1578,47 @@ bool GetIPAddressFailedLoginAttempts(TDatabase *Database, int IPAddress, int Tim
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Guild Tables
|
||||||
|
//==============================================================================
|
||||||
|
bool GetCharacterGuildData(TDatabase *Database, int CharacterID, TCharacterGuildData *GuildData){
|
||||||
|
ASSERT(Database != NULL && GuildData != NULL);
|
||||||
|
sqlite3_stmt *Stmt = PrepareQuery(Database,
|
||||||
|
"SELECT G.GuildID, R.Rank, G.Name, R.Name, M.Title"
|
||||||
|
" FROM Characters AS C"
|
||||||
|
" LEFT JOIN GuildMembers AS M ON M.CharacterID = C.CharacterID"
|
||||||
|
" LEFT JOIN Guilds AS G ON G.GuildID = M.GuildID"
|
||||||
|
" LEFT JOIN GuildRanks AS R"
|
||||||
|
" ON R.GuildID = M.GuildID AND R.Rank = M.Rank"
|
||||||
|
" WHERE C.CharacterID = ?1");
|
||||||
|
if(Stmt == NULL){
|
||||||
|
LOG_ERR("Failed to prepare query");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AutoStmtReset StmtReset(Stmt);
|
||||||
|
if(sqlite3_bind_int(Stmt, 1, CharacterID) != SQLITE_OK){
|
||||||
|
LOG_ERR("Failed to bind CharacterID: %s", sqlite3_errmsg(Database->Handle));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ErrorCode = sqlite3_step(Stmt);
|
||||||
|
if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){
|
||||||
|
LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(GuildData, 0, sizeof(TCharacterGuildData));
|
||||||
|
if(ErrorCode == SQLITE_ROW){
|
||||||
|
GuildData->GuildID = sqlite3_column_int(Stmt, 0);
|
||||||
|
GuildData->Rank = sqlite3_column_int(Stmt, 1);
|
||||||
|
StringBufCopy(GuildData->GuildName, (const char*)sqlite3_column_text(Stmt, 2));
|
||||||
|
StringBufCopy(GuildData->RankName, (const char*)sqlite3_column_text(Stmt, 3));
|
||||||
|
StringBufCopy(GuildData->Title, (const char*)sqlite3_column_text(Stmt, 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// House Tables
|
// House Tables
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
bool FinishHouseAuctions(TDatabase *Database, int WorldID, DynamicArray<THouseAuction> *Auctions){
|
bool FinishHouseAuctions(TDatabase *Database, int WorldID, DynamicArray<THouseAuction> *Auctions){
|
||||||
|
|||||||
20
src/query.cc
20
src/query.cc
@ -679,6 +679,9 @@ static void LoginGameTx(TDatabase *Database, TQuery *Query,
|
|||||||
QUERY_ERROR_IF(!GamemasterOutfit, 14);
|
QUERY_ERROR_IF(!GamemasterOutfit, 14);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TCharacterGuildData GuildData;
|
||||||
|
QUERY_STOP_IF(!GetCharacterGuildData(Database, Character.CharacterID, &GuildData));
|
||||||
|
|
||||||
DynamicArray<TAccountBuddy> Buddies;
|
DynamicArray<TAccountBuddy> Buddies;
|
||||||
QUERY_STOP_IF(!GetBuddies(Database, Query->WorldID, Account.AccountID, &Buddies));
|
QUERY_STOP_IF(!GetBuddies(Database, Query->WorldID, Account.AccountID, &Buddies));
|
||||||
|
|
||||||
@ -704,9 +707,9 @@ static void LoginGameTx(TDatabase *Database, TQuery *Query,
|
|||||||
Response->Write32((uint32)Character.CharacterID);
|
Response->Write32((uint32)Character.CharacterID);
|
||||||
Response->WriteString(Character.Name);
|
Response->WriteString(Character.Name);
|
||||||
Response->Write8((uint8)Character.Sex);
|
Response->Write8((uint8)Character.Sex);
|
||||||
Response->WriteString(Character.Guild);
|
Response->WriteString(GuildData.GuildName);
|
||||||
Response->WriteString(Character.Rank);
|
Response->WriteString(GuildData.RankName);
|
||||||
Response->WriteString(Character.Title);
|
Response->WriteString(GuildData.Title);
|
||||||
|
|
||||||
int NumBuddies = std::min<int>(Buddies.Length(), UINT8_MAX);
|
int NumBuddies = std::min<int>(Buddies.Length(), UINT8_MAX);
|
||||||
Response->Write8((uint8)NumBuddies);
|
Response->Write8((uint8)NumBuddies);
|
||||||
@ -1478,15 +1481,18 @@ void ProcessGetCharacterProfile(TDatabase *Database, TQuery *Query){
|
|||||||
|
|
||||||
TCharacterProfile Character;
|
TCharacterProfile Character;
|
||||||
QUERY_STOP_IF(!GetCharacterProfile(Database, CharacterName, &Character));
|
QUERY_STOP_IF(!GetCharacterProfile(Database, CharacterName, &Character));
|
||||||
QUERY_ERROR_IF(!StringEqCI(Character.Name, CharacterName), 1);
|
QUERY_ERROR_IF(Character.CharacterID == 0, 1);
|
||||||
|
|
||||||
|
TCharacterGuildData GuildData;
|
||||||
|
QUERY_STOP_IF(!GetCharacterGuildData(Database, Character.CharacterID, &GuildData));
|
||||||
|
|
||||||
TWriteBuffer *Response = QueryBeginResponse(Query, QUERY_STATUS_OK);
|
TWriteBuffer *Response = QueryBeginResponse(Query, QUERY_STATUS_OK);
|
||||||
Response->WriteString(Character.Name);
|
Response->WriteString(Character.Name);
|
||||||
Response->WriteString(Character.World);
|
Response->WriteString(Character.World);
|
||||||
Response->Write8((uint8)Character.Sex);
|
Response->Write8((uint8)Character.Sex);
|
||||||
Response->WriteString(Character.Guild);
|
Response->WriteString(GuildData.GuildName);
|
||||||
Response->WriteString(Character.Rank);
|
Response->WriteString(GuildData.RankName);
|
||||||
Response->WriteString(Character.Title);
|
Response->WriteString(GuildData.Title);
|
||||||
Response->Write16((uint16)Character.Level);
|
Response->Write16((uint16)Character.Level);
|
||||||
Response->WriteString(Character.Profession);
|
Response->WriteString(Character.Profession);
|
||||||
Response->WriteString(Character.Residence);
|
Response->WriteString(Character.Residence);
|
||||||
|
|||||||
@ -809,7 +809,7 @@ int main(int argc, const char **argv){
|
|||||||
g_Config.MaxConnections = 25;
|
g_Config.MaxConnections = 25;
|
||||||
g_Config.MaxConnectionIdleTime = 60 * 5; // seconds
|
g_Config.MaxConnectionIdleTime = 60 * 5; // seconds
|
||||||
|
|
||||||
LOG("Tibia Query Manager v0.2 (%s)", DATABASE_SYSTEM_NAME);
|
LOG("Tibia Query Manager v0.3 (%s)", DATABASE_SYSTEM_NAME);
|
||||||
if(!ReadConfig("config.cfg", &g_Config)){
|
if(!ReadConfig("config.cfg", &g_Config)){
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -884,19 +884,22 @@ struct TCharacterLoginData{
|
|||||||
int AccountID;
|
int AccountID;
|
||||||
char Name[30];
|
char Name[30];
|
||||||
int Sex;
|
int Sex;
|
||||||
char Guild[30];
|
|
||||||
char Rank[30];
|
|
||||||
char Title[30];
|
|
||||||
bool Deleted;
|
bool Deleted;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct TCharacterGuildData{
|
||||||
|
int GuildID;
|
||||||
|
int Rank;
|
||||||
|
char GuildName[30];
|
||||||
|
char RankName[30];
|
||||||
|
char Title[30];
|
||||||
|
};
|
||||||
|
|
||||||
struct TCharacterProfile{
|
struct TCharacterProfile{
|
||||||
|
int CharacterID;
|
||||||
char Name[30];
|
char Name[30];
|
||||||
char World[30];
|
char World[30];
|
||||||
int Sex;
|
int Sex;
|
||||||
char Guild[30];
|
|
||||||
char Rank[30];
|
|
||||||
char Title[30];
|
|
||||||
int Level;
|
int Level;
|
||||||
char Profession[30];
|
char Profession[30];
|
||||||
char Residence[30];
|
char Residence[30];
|
||||||
@ -1048,6 +1051,9 @@ bool InsertLoginAttempt(TDatabase *Database, int AccountID, int IPAddress, bool
|
|||||||
bool GetAccountFailedLoginAttempts(TDatabase *Database, int AccountID, int TimeWindow, int *FailedAttempts);
|
bool GetAccountFailedLoginAttempts(TDatabase *Database, int AccountID, int TimeWindow, int *FailedAttempts);
|
||||||
bool GetIPAddressFailedLoginAttempts(TDatabase *Database, int IPAddress, int TimeWindow, int *FailedAttempts);
|
bool GetIPAddressFailedLoginAttempts(TDatabase *Database, int IPAddress, int TimeWindow, int *FailedAttempts);
|
||||||
|
|
||||||
|
// NOTE(fusion): Guild Tables
|
||||||
|
bool GetCharacterGuildData(TDatabase *Database, int CharacterID, TCharacterGuildData *GuildData);
|
||||||
|
|
||||||
// NOTE(fusion): House Tables
|
// NOTE(fusion): House Tables
|
||||||
bool FinishHouseAuctions(TDatabase *Database, int WorldID, DynamicArray<THouseAuction> *Auctions);
|
bool FinishHouseAuctions(TDatabase *Database, int WorldID, DynamicArray<THouseAuction> *Auctions);
|
||||||
bool FinishHouseTransfers(TDatabase *Database, int WorldID, DynamicArray<THouseTransfer> *Transfers);
|
bool FinishHouseTransfers(TDatabase *Database, int WorldID, DynamicArray<THouseTransfer> *Transfers);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user