diff options
| author | fusion32 <marcopuzziello@gmail.com> | 2025-07-22 22:22:01 -0300 |
|---|---|---|
| committer | fusion32 <marcopuzziello@gmail.com> | 2025-07-22 22:22:01 -0300 |
| commit | 82b82dca7d621bf4530416c7c5e9fc69874eecd3 (patch) | |
| tree | 92c6bfd20e9a926854ac40cd380c6c37cc7e5ab3 | |
| parent | 29f6d814a276af83d9fb98a398ac8d22d8b6e9c1 (diff) | |
| download | querymanager-82b82dca7d621bf4530416c7c5e9fc69874eecd3.tar.gz querymanager-82b82dca7d621bf4530416c7c5e9fc69874eecd3.zip | |
improvements and bug fixes
- Add support for SIGINT and SIGTERM signals for graceful termination.
- Properly check premium account status with `EVICT_FREE_ACCOUNTS`
query.
- Properly check if character is already online before failing game
login with "too many characters" error.
- Overall cleanup.
| -rw-r--r-- | config.cfg | 8 | ||||
| -rw-r--r-- | sql/schema.sql | 88 | ||||
| -rw-r--r-- | src/connections.cc | 45 | ||||
| -rw-r--r-- | src/database.cc | 46 | ||||
| -rw-r--r-- | src/querymanager.cc | 61 | ||||
| -rw-r--r-- | src/querymanager.hh | 23 |
6 files changed, 213 insertions, 58 deletions
@@ -2,10 +2,10 @@ DatabaseFile = "tibia.db" MaxCachedStatements = 100 -# Connection -Password = "a6glaf0c" -Port = 7174 +# Connections +UpdateRate = 20 +QueryManagerPort = 7174 +QueryManagerPassword = "a6glaf0c" MaxConnections = 50 MaxConnectionIdleTime = 60s MaxConnectionPacketSize = 1M -UpdateRate = 20 diff --git a/sql/schema.sql b/sql/schema.sql index 5427ea6..c8a78ec 100644 --- a/sql/schema.sql +++ b/sql/schema.sql @@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS Worlds ( Name TEXT NOT NULL COLLATE NOCASE, Type INTEGER NOT NULL, RebootTime INTEGER NOT NULL, - Address INTEGER NOT NULL, + IPAddress INTEGER NOT NULL, Port INTEGER NOT NULL, MaxPlayers INTEGER NOT NULL, PremiumPlayerBuffer INTEGER NOT NULL, @@ -264,12 +264,94 @@ CREATE TABLE IF NOT EXISTS OnlineCharacters ( -- REMOVE(fusion): Testing Data. --============================================================================== -INSERT INTO Worlds (WorldID, Name, Type, RebootTime, Address, Port, MaxPlayers, +INSERT INTO Worlds (WorldID, Name, Type, RebootTime, IPAddress, Port, MaxPlayers, PremiumPlayerBuffer, MaxNewbies, PremiumNewbieBuffer) VALUES (1, 'Zanera', 0, 5, 0x7F000001, 7172, 1000, 100, 300, 100); +-- 111111/tibia INSERT INTO Accounts (AccountID, Email, Auth) VALUES (111111, '@tibia', X'206699cbc2fae1683118c873d746aa376049cb5923ef0980298bb7acbba527ec9e765668f7a338dffea34acf61a20efb654c1e9c62d35148dba2aeeef8dc7788'); INSERT INTO Characters (WorldID, CharacterID, AccountID, Name, Sex) - VALUES (1, 1, 1, 'Player', 1); + VALUES (1, 1, 111111, 'Gamemaster', 1), (1, 2, 111111, 'Player', 1); + +INSERT INTO CharacterRights (CharacterID, Right) + VALUES + (1, 'NOTATION'), + (1, 'NAMELOCK'), + (1, 'STATEMENT_REPORT'), + (1, 'BANISHMENT'), + (1, 'FINAL_WARNING'), + (1, 'IP_BANISHMENT'), + (1, 'KICK'), + (1, 'HOME_TELEPORT'), + (1, 'GAMEMASTER_BROADCAST'), + (1, 'ANONYMOUS_BROADCAST'), + (1, 'NO_BANISHMENT'), + (1, 'ALLOW_MULTICLIENT'), + (1, 'LOG_COMMUNICATION'), + (1, 'READ_GAMEMASTER_CHANNEL'), + (1, 'READ_TUTOR_CHANNEL'), + (1, 'HIGHLIGHT_HELP_CHANNEL'), + (1, 'SEND_BUGREPORTS'), + (1, 'NAME_INSULTING'), + (1, 'NAME_SENTENCE'), + (1, 'NAME_NONSENSICAL_LETTERS'), + (1, 'NAME_BADLY_FORMATTED'), + (1, 'NAME_NO_PERSON'), + (1, 'NAME_CELEBRITY'), + (1, 'NAME_COUNTRY'), + (1, 'NAME_FAKE_IDENTITY'), + (1, 'NAME_FAKE_POSITION'), + (1, 'STATEMENT_INSULTING'), + (1, 'STATEMENT_SPAMMING'), + (1, 'STATEMENT_ADVERT_OFFTOPIC'), + (1, 'STATEMENT_ADVERT_MONEY'), + (1, 'STATEMENT_NON_ENGLISH'), + (1, 'STATEMENT_CHANNEL_OFFTOPIC'), + (1, 'STATEMENT_VIOLATION_INCITING'), + (1, 'CHEATING_BUG_ABUSE'), + (1, 'CHEATING_GAME_WEAKNESS'), + (1, 'CHEATING_MACRO_USE'), + (1, 'CHEATING_MODIFIED_CLIENT'), + (1, 'CHEATING_HACKING'), + (1, 'CHEATING_MULTI_CLIENT'), + (1, 'CHEATING_ACCOUNT_TRADING'), + (1, 'CHEATING_ACCOUNT_SHARING'), + (1, 'GAMEMASTER_THREATENING'), + (1, 'GAMEMASTER_PRETENDING'), + (1, 'GAMEMASTER_INFLUENCE'), + (1, 'GAMEMASTER_FALSE_REPORTS'), + (1, 'KILLING_EXCESSIVE_UNJUSTIFIED'), + (1, 'DESTRUCTIVE_BEHAVIOUR'), + (1, 'SPOILING_AUCTION'), + (1, 'INVALID_PAYMENT'), + (1, 'TELEPORT_TO_CHARACTER'), + (1, 'TELEPORT_TO_MARK'), + (1, 'TELEPORT_VERTICAL'), + (1, 'TELEPORT_TO_COORDINATE'), + (1, 'LEVITATE'), + (1, 'SPECIAL_MOVEUSE'), + (1, 'MODIFY_GOSTRENGTH'), + (1, 'SHOW_COORDINATE'), + (1, 'RETRIEVE'), + (1, 'ENTER_HOUSES'), + (1, 'OPEN_NAMEDOORS'), + (1, 'INVULNERABLE'), + (1, 'UNLIMITED_MANA'), + (1, 'KEEP_INVENTORY'), + (1, 'ALL_SPELLS'), + (1, 'UNLIMITED_CAPACITY'), + (1, 'ATTACK_EVERYWHERE'), + (1, 'NO_LOGOUT_BLOCK'), + (1, 'GAMEMASTER_OUTFIT'), + (1, 'ILLUMINATE'), + (1, 'CHANGE_PROFESSION'), + (1, 'IGNORED_BY_MONSTERS'), + (1, 'SHOW_KEYHOLE_NUMBERS'), + (1, 'CREATE_OBJECTS'), + (1, 'CREATE_MONEY'), + (1, 'CREATE_MONSTERS'), + (1, 'CHANGE_SKILLS'), + (1, 'CLEANUP_FIELDS'), + (1, 'NO_STATISTICS'); diff --git a/src/connections.cc b/src/connections.cc index e78c48e..7d573ca 100644 --- a/src/connections.cc +++ b/src/connections.cc @@ -84,11 +84,11 @@ int ListenerAccept(int Listener, uint32 *OutAddr, uint16 *OutPort){ // IMPORTANT(fusion): It should be impossible to spoof the loopback // address so this comparison should be safe. We're also binding the // listening socket to the loopback address which should prevent any - // other address to show up here. + // remote addresses from showing up here. uint32 Addr = ntohl(SocketAddr.sin_addr.s_addr); uint16 Port = ntohs(SocketAddr.sin_port); if(Addr != INADDR_LOOPBACK){ - LOG_ERR("Rejecting remote connection from %08X.", Addr); + LOG_ERR("Rejecting remote connection from %08X:%d.", Addr, Port); close(Socket); continue; } @@ -317,8 +317,8 @@ void ProcessConnections(void){ } if(AssignConnection(Socket, Addr, Port) == NULL){ - LOG_ERR("Rejecting connection from %08X due to max number of" - " connections being reached (%d)", Addr, g_MaxConnections); + LOG_ERR("Rejecting connection %08X:%d due to max number of" + " connections being reached (%d)", Addr, Port, g_MaxConnections); close(Socket); } } @@ -364,12 +364,12 @@ bool InitConnections(void){ ASSERT(g_Listener == -1); ASSERT(g_Connections == NULL); - LOG("Listening port: %d", g_Port); + LOG("Query manager port: %d", g_QueryManagerPort); LOG("Max connections: %d", g_MaxConnections); - LOG("Max connection idle time: %d ms", g_MaxConnectionIdleTime); + LOG("Max connection idle time: %dms", g_MaxConnectionIdleTime); LOG("Max connection packet size: %d", g_MaxConnectionPacketSize); - g_Listener = ListenerBind((uint16)g_Port); + g_Listener = ListenerBind((uint16)g_QueryManagerPort); if(g_Listener == -1){ LOG_ERR("Failed to bind listener"); return false; @@ -491,7 +491,7 @@ void ProcessLoginQuery(TConnection *Connection, TReadBuffer *Buffer){ // TODO(fusion): Probably just disconnect on failed login attempt? Implement // write then disconnect? - if(!StringEq(g_Password, Password)){ + if(!StringEq(g_QueryManagerPassword, Password)){ LOG_WARN("Invalid login attempt from %s", Connection->RemoteAddress); SendQueryStatusFailed(Connection); return; @@ -501,15 +501,22 @@ void ProcessLoginQuery(TConnection *Connection, TReadBuffer *Buffer){ if(ApplicationType == APPLICATION_TYPE_GAME){ WorldID = GetWorldID(LoginData); if(WorldID == 0){ - LOG_WARN("Unknown world name \"%s\"", LoginData); + LOG_WARN("Rejecting connection %s from unknown game server \"%s\"", + Connection->RemoteAddress, LoginData); SendQueryStatusFailed(Connection); return; } - - LOG("Connection %s AUTHORIZED to world \"%s\" (%d)", + LOG("Connection %s AUTHORIZED to game server \"%s\" (%d)", Connection->RemoteAddress, LoginData, WorldID); + }else if(ApplicationType == APPLICATION_TYPE_LOGIN){ + LOG("Connection %s AUTHORIZED to login server", Connection->RemoteAddress); + }else if(ApplicationType == APPLICATION_TYPE_WEB){ + LOG("Connection %s AUTHORIZED to web server", Connection->RemoteAddress); }else{ - LOG("Connection %s AUTHORIZED", Connection->RemoteAddress); + LOG_WARN("Rejecting connection %s from unknown application type %d", + Connection->RemoteAddress, ApplicationType); + SendQueryStatusFailed(Connection); + return; } Connection->Authorized = true; @@ -735,8 +742,10 @@ static int LoginGameTransaction(int WorldID, int AccountID, const char *Characte return 12; } + // TODO(fusion): Probably merge these into a single query? if(!GetCharacterRight(Character->CharacterID, "ALLOW_MULTICLIENT") - && GetAccountOnlineCharacters(Account.AccountID) > 0){ + && GetAccountOnlineCharacters(Account.AccountID) > 0 + && !IsCharacterOnline(Character->CharacterID)){ return 13; } @@ -759,9 +768,15 @@ static int LoginGameTransaction(int WorldID, int AccountID, const char *Characte return -1; } + Account.PremiumDays += Account.PendingPremiumDays; + Account.PendingPremiumDays = 0; *PremiumAccountActivated = true; } + if(Account.PremiumDays > 0){ + Rights->Push(TCharacterRight{"PREMIUM_ACCOUNT"}); + } + if(!IncrementIsOnline(WorldID, Character->CharacterID)){ return -1; } @@ -843,6 +858,8 @@ void ProcessLoginGameQuery(TConnection *Connection, TReadBuffer *Buffer){ WriteBuffer.WriteString(Rights[i].Name); } + WriteBuffer.WriteFlag(PremiumAccountActivated); + SendResponse(Connection, &WriteBuffer); } @@ -1763,7 +1780,7 @@ void ProcessLoadWorldConfigQuery(TConnection *Connection, TReadBuffer *Buffer){ TWriteBuffer WriteBuffer = PrepareResponse(Connection, QUERY_STATUS_OK); WriteBuffer.Write8((uint8)WorldConfig.Type); WriteBuffer.Write8((uint8)WorldConfig.RebootTime); - WriteBuffer.Write32BE((uint32)WorldConfig.Address); + WriteBuffer.Write32BE((uint32)WorldConfig.IPAddress); WriteBuffer.Write16((uint16)WorldConfig.Port); WriteBuffer.Write16((uint16)WorldConfig.MaxPlayers); WriteBuffer.Write16((uint16)WorldConfig.PremiumPlayerBuffer); diff --git a/src/database.cc b/src/database.cc index cd32f88..3b1c51e 100644 --- a/src/database.cc +++ b/src/database.cc @@ -53,7 +53,7 @@ sqlite3_stmt *PrepareQuery(const char *Text){ if(Stmt == NULL){ if(sqlite3_prepare_v3(g_Database, Text, -1, SQLITE_PREPARE_PERSISTENT, &Stmt, NULL) != SQLITE_OK){ - LOG_ERR("Failed to prepary query: %s", sqlite3_errmsg(g_Database)); + LOG_ERR("Failed to prepare query: %s", sqlite3_errmsg(g_Database)); return NULL; } @@ -169,7 +169,7 @@ int GetWorldID(const char *WorldName){ bool GetWorldConfig(int WorldID, TWorldConfig *WorldConfig){ ASSERT(WorldConfig != NULL); sqlite3_stmt *Stmt = PrepareQuery( - "SELECT Type, RebootTime, Address, Port, MaxPlayers," + "SELECT Type, RebootTime, IPAddress, Port, MaxPlayers," " PremiumPlayerBuffer, MaxNewbies, PremiumNewbieBuffer" " FROM Worlds WHERE WorldID = ?1"); if(Stmt == NULL){ @@ -189,7 +189,7 @@ bool GetWorldConfig(int WorldID, TWorldConfig *WorldConfig){ WorldConfig->Type = sqlite3_column_int(Stmt, 0); WorldConfig->RebootTime = sqlite3_column_int(Stmt, 1); - WorldConfig->Address = sqlite3_column_int(Stmt, 2); + WorldConfig->IPAddress = sqlite3_column_int(Stmt, 2); WorldConfig->Port = sqlite3_column_int(Stmt, 3); WorldConfig->MaxPlayers = sqlite3_column_int(Stmt, 4); WorldConfig->PremiumPlayerBuffer = sqlite3_column_int(Stmt, 5); @@ -256,6 +256,33 @@ int GetAccountOnlineCharacters(int AccountID){ return sqlite3_column_int(Stmt, 0); } +bool IsCharacterOnline(int CharacterID){ + sqlite3_stmt *Stmt = PrepareQuery( + "SELECT IsOnline FROM Characters WHERE CharacterID = ?1"); + if(Stmt == NULL){ + LOG_ERR("Failed to prepare query"); + return false; + } + + if(sqlite3_bind_int(Stmt, 1, CharacterID) != SQLITE_OK){ + LOG_ERR("Failed to bind CharacterID: %s", sqlite3_errmsg(g_Database)); + return false; + } + + int ErrorCode = sqlite3_step(Stmt); + if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){ + LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(g_Database)); + return false; + } + + bool Result = false; + if(ErrorCode == SQLITE_ROW){ + Result = (sqlite3_column_int(Stmt, 0) != 0); + } + + return Result; +} + bool ActivatePendingPremiumDays(int AccountID){ sqlite3_stmt *Stmt = PrepareQuery( "UPDATE Accounts" @@ -470,7 +497,7 @@ bool GetGuildLeaderStatus(int WorldID, int CharacterID){ } bool IncrementIsOnline(int WorldID, int CharacterID){ - // NOTE(fusion): Same as `DecrementIsOnline` + // NOTE(fusion): Same as `DecrementIsOnline`. sqlite3_stmt *Stmt = PrepareQuery( "UPDATE Characters SET IsOnline = IsOnline + 1" " WHERE WorldID = ?1 AND CharacterID = ?2"); @@ -553,7 +580,7 @@ bool LogoutCharacter(int WorldID, int CharacterID, int Level, " Profession = ?4," " Residence = ?5," " LastLoginTime = ?6," - " TutorActivities = ?7" + " TutorActivities = ?7," " IsOnline = IsOnline - 1" " WHERE WorldID = ?1 AND CharacterID = ?2"); if(Stmt == NULL){ @@ -767,7 +794,7 @@ bool GetWorldInvitation(int WorldID, int CharacterID){ bool InsertLoginAttempt(int AccountID, int IPAddress, bool Failed){ sqlite3_stmt *Stmt = PrepareQuery( "INSERT INTO LoginAttempts (AccountID, IPAddress, Timestamp, Failed)" - " VALUES (?1, ?2, UNIXEPOCH(), ?4)"); + " VALUES (?1, ?2, UNIXEPOCH(), ?3)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; @@ -916,9 +943,10 @@ bool GetFreeAccountEvictions(int WorldID, DynamicArray<THouseEviction> *Eviction sqlite3_stmt *Stmt = PrepareQuery( "SELECT O.HouseID, O.OwnerID" " FROM HouseOwners AS O" - " LEFT JOIN CharacterRights AS R" - " ON R.CharacterID = O.OwnerID AND R.Right = 'PREMIUM_ACCOUNT'" - " WHERE O.WorldID = ?1 AND R.CharacterID IS NULL"); + " LEFT JOIN Characters AS C ON C.CharacterID = O.OwnerID" + " LEFT JOIN Accounts AS A ON A.AccountID = C.AccountID" + " WHERE O.WorldID = ?1" + " AND (A.PremiumEnd IS NULL OR A.PremiumEnd < UNIXEPOCH())"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; diff --git a/src/querymanager.cc b/src/querymanager.cc index 26d2855..5086032 100644 --- a/src/querymanager.cc +++ b/src/querymanager.cc @@ -1,19 +1,28 @@ #include "querymanager.hh" +// TODO(fusion): Support windows eventually? +#if OS_LINUX +# include <errno.h> +# include <signal.h> +#else +# error "Operating system not currently supported." +#endif + +// Shutdown Signal +int g_ShutdownSignal = 0; + // Time int g_MonotonicTimeMS = 0; -// Database CONFIG +// Config char g_DatabaseFile[1024] = "tibia.db"; int g_MaxCachedStatements = 100; - -// Connection CONFIG -char g_Password[30] = ""; -int g_Port = 7174; +int g_UpdateRate = 20; +int g_QueryManagerPort = 7174; +char g_QueryManagerPassword[30] = ""; int g_MaxConnections = 50; int g_MaxConnectionIdleTime = 60000; int g_MaxConnectionPacketSize = (int)MB(1); -int g_UpdateRate = 20; void LogAdd(const char *Prefix, const char *Format, ...){ char Entry[4096]; @@ -331,18 +340,18 @@ bool ReadConfig(const char *FileName){ ReadStringConfig(g_DatabaseFile, (int)sizeof(g_DatabaseFile), Val); }else if(StringEqCI(Key, "MaxCachedStatements")){ ReadIntegerConfig(&g_MaxCachedStatements, Val); - }else if(StringEqCI(Key, "Password")){ - ReadStringConfig(g_Password, (int)sizeof(g_Password), Val); - }else if(StringEqCI(Key, "Port")){ - ReadIntegerConfig(&g_Port, Val); + }else if(StringEqCI(Key, "UpdateRate")){ + ReadIntegerConfig(&g_UpdateRate, Val); + }else if(StringEqCI(Key, "QueryManagerPort")){ + ReadIntegerConfig(&g_QueryManagerPort, Val); + }else if(StringEqCI(Key, "QueryManagerPassword")){ + ReadStringConfig(g_QueryManagerPassword, (int)sizeof(g_QueryManagerPassword), Val); }else if(StringEqCI(Key, "MaxConnections")){ ReadIntegerConfig(&g_MaxConnections, Val); }else if(StringEqCI(Key, "MaxConnectionIdleTime")){ ReadDurationConfig(&g_MaxConnectionIdleTime, Val); }else if(StringEqCI(Key, "MaxConnectionPacketSize")){ ReadSizeConfig(&g_MaxConnectionPacketSize, Val); - }else if(StringEqCI(Key, "UpdateRate")){ - ReadIntegerConfig(&g_UpdateRate, Val); }else{ LOG_WARN("Unknown config \"%s\"", Key); } @@ -352,10 +361,33 @@ bool ReadConfig(const char *FileName){ return true; } +static bool SigHandler(int SigNr, sighandler_t Handler){ + struct sigaction Action = {}; + Action.sa_handler = Handler; + sigfillset(&Action.sa_mask); + if(sigaction(SigNr, &Action, NULL) == -1){ + LOG_ERR("Failed to change handler for signal %d (%s): (%d) %s", + SigNr, sigdescr_np(SigNr), errno, strerrordesc_np(errno)); + return false; + } + return true; +} + +static void ShutdownHandler(int SigNr){ + g_ShutdownSignal = SigNr; +} + int main(int argc, const char **argv){ (void)argc; (void)argv; + g_ShutdownSignal = 0; + if(!SigHandler(SIGPIPE, SIG_IGN) + || !SigHandler(SIGINT, ShutdownHandler) + || !SigHandler(SIGTERM, ShutdownHandler)){ + return EXIT_FAILURE; + } + int64 StartTime = GetClockMonotonicMS(); g_MonotonicTimeMS = 0; @@ -376,7 +408,7 @@ int main(int argc, const char **argv){ LOG("Running at %d updates per second...", g_UpdateRate); int64 UpdateInterval = 1000 / (int64)g_UpdateRate; - while(true){ + while(g_ShutdownSignal == 0){ int64 UpdateStart = GetClockMonotonicMS(); g_MonotonicTimeMS = (int)(UpdateStart - StartTime); ProcessConnections(); @@ -387,5 +419,8 @@ int main(int argc, const char **argv){ } } + LOG("Received signal %d (%s), shutting down...", + g_ShutdownSignal, sigdescr_np(g_ShutdownSignal)); + return EXIT_SUCCESS; } diff --git a/src/querymanager.hh b/src/querymanager.hh index 9b358ce..f0245d4 100644 --- a/src/querymanager.hh +++ b/src/querymanager.hh @@ -78,17 +78,15 @@ typedef size_t usize; // Time extern int g_MonotonicTimeMS; -// Database CONFIG +// Config extern char g_DatabaseFile[1024]; extern int g_MaxCachedStatements; - -// Connection CONFIG -extern char g_Password[30]; -extern int g_Port; +extern int g_UpdateRate; +extern int g_QueryManagerPort; +extern char g_QueryManagerPassword[30]; extern int g_MaxConnections; extern int g_MaxConnectionIdleTime; extern int g_MaxConnectionPacketSize; -extern int g_UpdateRate; void LogAdd(const char *Prefix, const char *Format, ...) ATTR_PRINTF(2, 3); void LogAddVerbose(const char *Prefix, const char *Function, @@ -530,14 +528,8 @@ public: //============================================================================== enum : int { APPLICATION_TYPE_GAME = 1, - - // TODO(fusion): There is definitely more application types that we don't - // know about. From looking at the config loader on the server application, - // we could perhaps infer a few, although we're gonna be limited to our own - // usage cases (GAME and WEB). - //APPLICATION_TYPE_WEB - //APPLICATION_TYPE_FORUM - //APPLICATION_TYPE_ADMIN + APPLICATION_TYPE_LOGIN = 2, + APPLICATION_TYPE_WEB = 3, }; enum : int { @@ -695,7 +687,7 @@ void ProcessConnectionQuery(TConnection *Connection); struct TWorldConfig{ int Type; int RebootTime; - int Address; + int IPAddress; int Port; int MaxPlayers; int PremiumPlayerBuffer; @@ -834,6 +826,7 @@ int GetWorldID(const char *WorldName); bool GetWorldConfig(int WorldID, TWorldConfig *WorldConfig); bool GetAccountData(int AccountID, TAccountData *Account); int GetAccountOnlineCharacters(int AccountID); +bool IsCharacterOnline(int CharacterID); bool ActivatePendingPremiumDays(int AccountID); bool GetCharacterList(int AccountID, DynamicArray<TCharacterLoginData> *Characters); int GetCharacterID(int WorldID, const char *CharacterName); |
