diff options
| author | fusion32 <marcopuzziello@gmail.com> | 2025-07-10 16:57:09 -0300 |
|---|---|---|
| committer | fusion32 <marcopuzziello@gmail.com> | 2025-07-10 16:57:09 -0300 |
| commit | 8dbd1d39e3dbd1276e2351070a0645c5e973da9e (patch) | |
| tree | f70eaa92e3e7def84bbf27b2b1550b23767541a6 /src/querymanager.cc | |
| download | querymanager-8dbd1d39e3dbd1276e2351070a0645c5e973da9e.tar.gz querymanager-8dbd1d39e3dbd1276e2351070a0645c5e973da9e.zip | |
bare bones query manager
This is a bare bones query manager that will respond just enough
queries to get the server running. The overall structure of the
tables should be almost set but we'd need to handle all queries to
get the server running properly.
It uses the SQLite3 3.50.2 amalgamation for a database backend so
we can completely avoid having to spin up yet another service for a
separate database system, effectively turning the query manager into
the database itself. This also means that other services such as the
login server and website must go through it to access the data but
it shouldn't be too difficult to replicate the querying mechanism
from the server.
This design choice was made with a single game server in mind. The
networking protocol is NOT encrypted at all and won't accept remote
connections. For a fully multi-world distributed infrastructure, a
distributed database system like PostgreSQL and MySQL should be
considered.
Diffstat (limited to 'src/querymanager.cc')
| -rw-r--r-- | src/querymanager.cc | 358 |
1 files changed, 358 insertions, 0 deletions
diff --git a/src/querymanager.cc b/src/querymanager.cc new file mode 100644 index 0000000..57b3ef9 --- /dev/null +++ b/src/querymanager.cc @@ -0,0 +1,358 @@ +#include "querymanager.hh" + +// Time +int g_MonotonicTimeMS = 0; + +// Database CONFIG +char g_DatabaseFile[1024] = "tibia.db"; +int g_MaxCachedStatements = 100; + +// Connection CONFIG +char g_Password[30] = ""; +int g_Port = 7174; +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]; + va_list ap; + va_start(ap, Format); + vsnprintf(Entry, sizeof(Entry), Format, ap); + va_end(ap); + + if(Entry[0] != 0){ + struct tm LocalTime = GetLocalTime(time(NULL)); + fprintf(stdout, "%04d/%02d/%02d %02d:%02d:%02d [%s] %s\n", + LocalTime.tm_year + 1900, LocalTime.tm_mon + 1, LocalTime.tm_mday, + LocalTime.tm_hour, LocalTime.tm_min, LocalTime.tm_sec, + Prefix, Entry); + } +} + +void LogAddVerbose(const char *Prefix, const char *Function, + const char *File, int Line, const char *Format, ...){ + char Entry[4096]; + va_list ap; + va_start(ap, Format); + vsnprintf(Entry, sizeof(Entry), Format, ap); + va_end(ap); + + if(Entry[0] != 0){ + (void)File; + (void)Line; + struct tm LocalTime = GetLocalTime(time(NULL)); + fprintf(stdout, "%04d/%02d/%02d %02d:%02d:%02d [%s] %s: %s\n", + LocalTime.tm_year + 1900, LocalTime.tm_mon + 1, LocalTime.tm_mday, + LocalTime.tm_hour, LocalTime.tm_min, LocalTime.tm_sec, + Prefix, Function, Entry); + } +} + +struct tm GetLocalTime(time_t t){ + struct tm result; +#if COMPILER_MSVC + localtime_s(&result, &t); +#else + localtime_r(&t, &result); +#endif + return result; +} + +int64 GetClockMonotonicMS(void){ +#if OS_WINDOWS + LARGE_INTEGER Counter, Frequency; + QueryPerformanceCounter(&Counter); + QueryPerformanceFrequency(&Frequency); + return (int64)((Counter.QuadPart * 1000) / Frequency.QuadPart); +#else + struct timespec Time; + clock_gettime(CLOCK_MONOTONIC, &Time); + return ((int64)Time.tv_sec * 1000) + + ((int64)Time.tv_nsec / 1000000); +#endif +} + +void SleepMS(int64 DurationMS){ +#if OS_WINDOWS + Sleep((DWORD)DurationMS); +#else + struct timespec Duration; + Duration.tv_sec = (time_t)(DurationMS / 1000); + Duration.tv_nsec = (long)((DurationMS % 1000) * 1000000); + nanosleep(&Duration, NULL); +#endif +} + +bool StringEq(const char *A, const char *B){ + int Index = 0; + while(true){ + if(A[Index] != B[Index]){ + return false; + }else if(A[Index] == 0){ + return true; + } + Index += 1; + } +} + +bool StringEqCI(const char *A, const char *B){ + int Index = 0; + while(true){ + if(tolower(A[Index]) != tolower(B[Index])){ + return false; + }else if(A[Index] == 0){ + return true; + } + Index += 1; + } +} + +bool StringCopyN(char *Dest, int DestCapacity, const char *Src, int SrcLength){ + ASSERT(DestCapacity > 0); + bool Result = (SrcLength < DestCapacity); + if(Result){ + memcpy(Dest, Src, SrcLength); + Dest[SrcLength] = 0; + }else{ + Dest[0] = 0; + } + return true; +} + +bool StringCopy(char *Dest, int DestCapacity, const char *Src){ + return StringCopyN(Dest, DestCapacity, Src, (int)strlen(Src)); +} + +bool ReadBooleanConfig(bool *Dest, const char *Val){ + ASSERT(Dest && Val); + *Dest = StringEqCI(Val, "true"); + return *Dest || StringEqCI(Val, "false"); +} + +bool ReadIntegerConfig(int *Dest, const char *Val){ + ASSERT(Dest && Val); + const char *ValEnd; + *Dest = (int)strtol(Val, (char**)&ValEnd, 0); + return ValEnd > Val; +} + +bool ReadDurationConfig(int *Dest, const char *Val){ + ASSERT(Dest && Val); + const char *Suffix; + *Dest = (int)strtol(Val, (char**)&Suffix, 0); + if(Suffix == Val){ + return false; + } + + while(Suffix[0] != 0 && isspace(Suffix[0])){ + Suffix += 1; + } + + if(Suffix[0] == 'S' || Suffix[0] == 's'){ + *Dest *= (1000); + }else if(Suffix[0] == 'M' || Suffix[0] == 'm'){ + *Dest *= (60 * 1000); + }else if(Suffix[0] == 'H' || Suffix[0] == 'h'){ + *Dest *= (60 * 60 * 1000); + } + + return true; +} + +bool ReadSizeConfig(int *Dest, const char *Val){ + ASSERT(Dest && Val); + const char *Suffix; + *Dest = (int)strtol(Val, (char**)&Suffix, 0); + if(Suffix == Val){ + return false; + } + + while(Suffix[0] != 0 && isspace(Suffix[0])){ + Suffix += 1; + } + + if(Suffix[0] == 'K' || Suffix[0] == 'k'){ + *Dest *= (1024); + }else if(Suffix[0] == 'M' || Suffix[0] == 'm'){ + *Dest *= (1024 * 1024); + } + + return true; +} + +bool ReadStringConfig(char *Dest, int DestCapacity, const char *Val){ + ASSERT(Dest && DestCapacity > 0 && Val); + int ValStart = 0; + int ValEnd = (int)strlen(Val); + if(ValEnd >= 2){ + if((Val[0] == '"' && Val[ValEnd - 1] == '"') + || (Val[0] == '\'' && Val[ValEnd - 1] == '\'') + || (Val[0] == '`' && Val[ValEnd - 1] == '`')){ + ValStart += 1; + ValEnd -= 1; + } + } + + return StringCopyN(Dest, DestCapacity, + &Val[ValStart], (ValEnd - ValStart)); +} + +bool ReadConfig(const char *FileName){ + FILE *File = fopen(FileName, "rb"); + if(File == NULL){ + LOG_ERR("Failed to open config file \"%s\"", FileName); + return false; + } + + bool EndOfFile = false; + for(int LineNumber = 1; !EndOfFile; LineNumber += 1){ + char Line[1024]; + int MaxLineSize = (int)sizeof(Line); + int LineSize = 0; + int KeyStart = -1; + int EqualPos = -1; + while(true){ + int ch = fgetc(File); + if(ch == EOF || ch == '\n'){ + if(ch == EOF){ + EndOfFile = true; + } + break; + } + + if(LineSize < MaxLineSize){ + Line[LineSize] = (char)ch; + } + + if(KeyStart == -1 && !isspace(ch)){ + KeyStart = LineSize; + } + + if(EqualPos == -1 && ch == '='){ + EqualPos = LineSize; + } + + LineSize += 1; + } + + // NOTE(fusion): Check line size limit. + if(LineSize > MaxLineSize){ + LOG_WARN("%s:%d: Exceeded line size limit of %d characters", + FileName, LineNumber, MaxLineSize); + continue; + } + + // NOTE(fusion): Check empty line or comment. + if(KeyStart == -1 || Line[KeyStart] == '#'){ + continue; + } + + // NOTE(fusion): Check assignment. + if(EqualPos == -1){ + LOG_WARN("%s:%d: No assignment found on non empty line", + FileName, LineNumber); + continue; + } + + // NOTE(fusion): Check empty key. + int KeyEnd = EqualPos; + while(KeyEnd > KeyStart && isspace(Line[KeyEnd - 1])){ + KeyEnd -= 1; + } + + if(KeyStart == KeyEnd){ + LOG_WARN("%s:%d: Empty key", FileName, LineNumber); + continue; + } + + // NOTE(fusion): Check empty value. + int ValStart = EqualPos + 1; + int ValEnd = LineSize; + while(ValStart < ValEnd && isspace(Line[ValStart])){ + ValStart += 1; + } + + while(ValEnd > ValStart && isspace(Line[ValEnd - 1])){ + ValEnd -= 1; + } + + if(ValStart == ValEnd){ + LOG_WARN("%s:%d: Empty value", FileName, LineNumber); + continue; + } + + // NOTE(fusion): Parse KV pair. + char Key[256]; + if(!StringCopyN(Key, (int)sizeof(Key), &Line[KeyStart], (KeyEnd - KeyStart))){ + LOG_WARN("%s:%d: Exceeded key size limit of %d characters", + FileName, LineNumber, (int)(sizeof(Key) - 1)); + continue; + } + + char Val[256]; + if(!StringCopyN(Val, (int)sizeof(Val), &Line[ValStart], (ValEnd - ValStart))){ + LOG_WARN("%s:%d: Exceeded value size limit of %d characters", + FileName, LineNumber, (int)(sizeof(Val) - 1)); + continue; + } + + if(StringEqCI(Key, "DatabaseFile")){ + 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, "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); + } + } + + fclose(File); + return true; +} + +int main(int argc, const char **argv){ + (void)argc; + (void)argv; + + int64 StartTime = GetClockMonotonicMS(); + g_MonotonicTimeMS = 0; + + LOG("Tibia Query Manager v0.1"); + if(!ReadConfig("config.cfg")){ + return EXIT_FAILURE; + } + + atexit(ExitDatabase); + atexit(ExitConnections); + if(!InitDatabase() || !InitConnections()){ + return EXIT_FAILURE; + } + + LOG("Running at %d updates per second...", g_UpdateRate); + int64 UpdateInterval = 1000 / (int64)g_UpdateRate; + while(true){ + int64 UpdateStart = GetClockMonotonicMS(); + g_MonotonicTimeMS = (int)(UpdateStart - StartTime); + ProcessConnections(); + int64 UpdateEnd = GetClockMonotonicMS(); + int64 NextUpdate = UpdateStart + UpdateInterval; + if(NextUpdate > UpdateEnd){ + SleepMS(NextUpdate - UpdateEnd); + } + } + + return EXIT_SUCCESS; +} |
