aboutsummaryrefslogtreecommitdiff
path: root/src/main.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.cc')
-rw-r--r--src/main.cc709
1 files changed, 709 insertions, 0 deletions
diff --git a/src/main.cc b/src/main.cc
new file mode 100644
index 0000000..637da2f
--- /dev/null
+++ b/src/main.cc
@@ -0,0 +1,709 @@
+#include "common.hh"
+
+#include <errno.h>
+#include <signal.h>
+
+int64 g_StartTimeMS = 0;
+int g_ShutdownSignal = 0;
+TConfig g_Config = {};
+
+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);
+
+ // NOTE(fusion): Trim trailing whitespace.
+ int Length = (int)strlen(Entry);
+ while(Length > 0 && isspace(Entry[Length - 1])){
+ Entry[Length - 1] = 0;
+ Length -= 1;
+ }
+
+ if(Length > 0){
+ char TimeString[128];
+ StringBufFormatTime(TimeString, "%Y-%m-%d %H:%M:%S", (int)time(NULL));
+ fprintf(stdout, "%s [%s] %s\n", TimeString, Prefix, Entry);
+ fflush(stdout);
+ }
+}
+
+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);
+
+ // NOTE(fusion): Trim trailing whitespace.
+ int Length = (int)strlen(Entry);
+ while(Length > 0 && isspace(Entry[Length - 1])){
+ Entry[Length - 1] = 0;
+ Length -= 1;
+ }
+
+ if(Length > 0){
+ (void)File;
+ (void)Line;
+ char TimeString[128];
+ StringBufFormatTime(TimeString, "%Y-%m-%d %H:%M:%S", (int)time(NULL));
+ fprintf(stdout, "%s [%s] %s: %s\n", TimeString, Prefix, Function, Entry);
+ fflush(stdout);
+ }
+}
+
+struct tm GetLocalTime(time_t t){
+ struct tm result;
+#if COMPILER_MSVC
+ localtime_s(&result, &t);
+#else
+ localtime_r(&t, &result);
+#endif
+ return result;
+}
+
+struct tm GetGMTime(time_t t){
+ struct tm result;
+#if COMPILER_MSVC
+ gmtime_s(&result, &t);
+#else
+ gmtime_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
+ // NOTE(fusion): The coarse monotonic clock has a larger resolution but is
+ // supposed to be faster, even avoiding system calls in some cases. It should
+ // be fine for millisecond precision which is what we're using.
+ struct timespec Time;
+ clock_gettime(CLOCK_MONOTONIC_COARSE, &Time);
+ return ((int64)Time.tv_sec * 1000)
+ + ((int64)Time.tv_nsec / 1000000);
+#endif
+}
+
+int GetMonotonicUptime(void){
+ return (int)((GetClockMonotonicMS() - g_StartTimeMS) / 1000);
+}
+
+bool StringEmpty(const char *String){
+ return String[0] == 0;
+}
+
+bool StringEq(const char *A, const char *B){
+ int Index = 0;
+ while(A[Index] != 0 && A[Index] == B[Index]){
+ Index += 1;
+ }
+ return A[Index] == B[Index];
+}
+
+bool StringEqCI(const char *A, const char *B){
+ int Index = 0;
+ while(A[Index] != 0 && tolower(A[Index]) == tolower(B[Index])){
+ Index += 1;
+ }
+ return tolower(A[Index]) == tolower(B[Index]);
+}
+
+bool StringCopy(char *Dest, int DestCapacity, const char *Src){
+ int SrcLength = (Src != NULL ? (int)strlen(Src) : 0);
+ return StringCopyN(Dest, DestCapacity, Src, SrcLength);
+}
+
+bool StringCopyN(char *Dest, int DestCapacity, const char *Src, int SrcLength){
+ ASSERT(DestCapacity > 0);
+ bool Result = (SrcLength < DestCapacity);
+ if(Result && SrcLength > 0){
+ memcpy(Dest, Src, SrcLength);
+ Dest[SrcLength] = 0;
+ }else{
+ Dest[0] = 0;
+ }
+ return Result;
+}
+
+bool StringFormat(char *Dest, int DestCapacity, const char *Format, ...){
+ va_list ap;
+ va_start(ap, Format);
+ int Written = vsnprintf(Dest, DestCapacity, Format, ap);
+ va_end(ap);
+ return Written >= 0 && Written < DestCapacity;
+}
+
+bool StringFormatTime(char *Dest, int DestCapacity, const char *Format, int Timestamp){
+ struct tm tm = GetLocalTime((time_t)Timestamp);
+ int Result = (int)strftime(Dest, DestCapacity, Format, &tm);
+
+ // NOTE(fusion): `strftime` will return ZERO if it's unable to fit the result
+ // in the supplied buffer, which is annoying because ZERO may not represent a
+ // failure if the result is an empty string.
+ ASSERT(Result >= 0 && Result < DestCapacity);
+ if(Result == 0){
+ memset(Dest, 0, DestCapacity);
+ }
+
+ return Result != 0;
+}
+
+void StringClear(char *Dest, int DestCapacity){
+ ASSERT(DestCapacity > 0);
+ memset(Dest, 0, DestCapacity);
+}
+
+uint32 StringHash(const char *String){
+ // FNV1a 32-bits
+ uint32 Hash = 0x811C9DC5U;
+ for(int i = 0; String[i] != 0; i += 1){
+ Hash ^= (uint32)String[i];
+ Hash *= 0x01000193U;
+ }
+ return Hash;
+}
+
+bool StringEscape(char *Dest, int DestCapacity, const char *Src){
+ int WritePos = 0;
+ for(int ReadPos = 0; Src[ReadPos] != 0 && WritePos < DestCapacity; ReadPos += 1){
+ int EscapeCh = -1;
+ switch(Src[ReadPos]){
+ case '\a': EscapeCh = 'a'; break;
+ case '\b': EscapeCh = 'b'; break;
+ case '\t': EscapeCh = 't'; break;
+ case '\n': EscapeCh = 'n'; break;
+ case '\v': EscapeCh = 'v'; break;
+ case '\f': EscapeCh = 'f'; break;
+ case '\r': EscapeCh = 'r'; break;
+ case '\"': EscapeCh = '\"'; break;
+ case '\'': EscapeCh = '\''; break;
+ case '\\': EscapeCh = '\\'; break;
+ }
+
+ if(EscapeCh != -1){
+ if((WritePos + 1) <= DestCapacity){
+ Dest[WritePos] = '\\';
+ WritePos += 1;
+ }
+
+ if((WritePos + 1) <= DestCapacity){
+ Dest[WritePos] = EscapeCh;
+ WritePos += 1;
+ }
+ }else{
+ if((WritePos + 1) <= DestCapacity){
+ Dest[WritePos] = Src[ReadPos];
+ WritePos += 1;
+ }
+ }
+ }
+
+ if(WritePos < DestCapacity){
+ Dest[WritePos] = 0;
+ return true;
+ }else{
+ Dest[DestCapacity - 1] = 0;
+ return false;
+ }
+}
+
+int UTF8SequenceSize(uint8 LeadingByte){
+ if((LeadingByte & 0x80) == 0){
+ return 1;
+ }else if((LeadingByte & 0xE0) == 0xC0){
+ return 2;
+ }else if((LeadingByte & 0xF0) == 0xE0){
+ return 3;
+ }else if((LeadingByte & 0xF8) == 0xF0){
+ return 4;
+ }else{
+ return 0;
+ }
+}
+
+bool UTF8IsTrailingByte(uint8 Byte){
+ return (Byte & 0xC0) == 0x80;
+}
+
+int UTF8EncodedSize(int Codepoint){
+ if(Codepoint < 0){
+ return 0;
+ }else if(Codepoint <= 0x7F){
+ return 1;
+ }else if(Codepoint <= 0x07FF){
+ return 2;
+ }else if(Codepoint <= 0xFFFF){
+ return 3;
+ }else if(Codepoint <= 0x10FFFF){
+ return 4;
+ }else{
+ return 0;
+ }
+}
+
+int UTF8FindNextLeadingByte(const char *Src, int SrcLength){
+ int Offset = 0;
+ while(Offset < SrcLength){
+ // NOTE(fusion): Allow the first byte to be a leading byte, in case we
+ // just want to advance from one leading byte to another.
+ if(Offset > 0 && !UTF8IsTrailingByte(Src[Offset])){
+ break;
+ }
+ Offset += 1;
+ }
+ return Offset;
+}
+
+int UTF8DecodeOne(const uint8 *Src, int SrcLength, int *OutCodepoint){
+ if(SrcLength <= 0){
+ return 0;
+ }
+
+ int Size = UTF8SequenceSize(Src[0]);
+ if(Size <= 0 || Size > SrcLength){
+ return 0;
+ }
+
+ for(int i = 1; i < Size; i += 1){
+ if(!UTF8IsTrailingByte(Src[i])){
+ return 0;
+ }
+ }
+
+ int Codepoint = 0;
+ switch(Size){
+ case 1:{
+ Codepoint = (int)Src[0];
+ break;
+ }
+
+ case 2:{
+ Codepoint = ((int)(Src[0] & 0x1F) << 6)
+ | ((int)(Src[1] & 0x3F) << 0);
+ break;
+ }
+
+ case 3:{
+ Codepoint = ((int)(Src[0] & 0x0F) << 12)
+ | ((int)(Src[1] & 0x3F) << 6)
+ | ((int)(Src[2] & 0x3F) << 0);
+ break;
+ }
+
+ case 4:{
+ Codepoint = ((int)(Src[0] & 0x07) << 18)
+ | ((int)(Src[1] & 0x3F) << 12)
+ | ((int)(Src[2] & 0x3F) << 6)
+ | ((int)(Src[3] & 0x3F) << 0);
+ break;
+ }
+ }
+
+ if(OutCodepoint){
+ *OutCodepoint = Codepoint;
+ }
+
+ return Size;
+}
+
+int UTF8EncodeOne(uint8 *Dest, int DestCapacity, int Codepoint){
+ int Size = UTF8EncodedSize(Codepoint);
+ if(Size > 0 && Size <= DestCapacity){
+ switch(Size){
+ case 1:{
+ Dest[0] = (uint8)Codepoint;
+ break;
+ }
+
+ case 2:{
+ Dest[0] = (uint8)(0xC0 | (0x1F & (Codepoint >> 6)));
+ Dest[1] = (uint8)(0x80 | (0x3F & (Codepoint >> 0)));
+ break;
+ }
+
+ case 3:{
+ Dest[0] = (uint8)(0xE0 | (0x0F & (Codepoint >> 12)));
+ Dest[1] = (uint8)(0x80 | (0x3F & (Codepoint >> 6)));
+ Dest[2] = (uint8)(0x80 | (0x3F & (Codepoint >> 0)));
+ break;
+ }
+
+ case 4:{
+ Dest[0] = (uint8)(0xF0 | (0x07 & (Codepoint >> 18)));
+ Dest[1] = (uint8)(0x80 | (0x3F & (Codepoint >> 12)));
+ Dest[2] = (uint8)(0x80 | (0x3F & (Codepoint >> 6)));
+ Dest[3] = (uint8)(0x80 | (0x3F & (Codepoint >> 0)));
+ break;
+ }
+ }
+ }
+
+ return Size;
+}
+
+// IMPORTANT(fusion): This function WON'T handle null-termination. It'll rather
+// convert any characters, INCLUDING the null-terminator, contained in the src
+// string. Invalid or NON-LATIN1 codepoints are translated into '?'.
+int UTF8ToLatin1(char *Dest, int DestCapacity, const char *Src, int SrcLength){
+ int ReadPos = 0;
+ int WritePos = 0;
+ while(ReadPos < SrcLength){
+ int Codepoint = -1;
+ int Size = UTF8DecodeOne((uint8*)(Src + ReadPos), (SrcLength - ReadPos), &Codepoint);
+ if(Size > 0){
+ ReadPos += Size;
+ }else{
+ ReadPos += UTF8FindNextLeadingByte((Src + ReadPos), (SrcLength - ReadPos));
+ }
+
+ if(WritePos < DestCapacity){
+ if(Codepoint >= 0 && Codepoint <= 0xFF){
+ Dest[WritePos] = (char)Codepoint;
+ }else{
+ Dest[WritePos] = '?';
+ }
+ }
+ WritePos += 1;
+ }
+
+ return WritePos;
+}
+
+// IMPORTANT(fusion): This function WON'T handle null-termination. It'll rather
+// convert any characters, INCLUDING the null-terminator, contained in the src
+// string. Note that LATIN1 characters translates directly into UNICODE codepoints.
+int Latin1ToUTF8(char *Dest, int DestCapacity, const char *Src, int SrcLength){
+ int WritePos = 0;
+ for(int ReadPos = 0; ReadPos < SrcLength; ReadPos += 1){
+ WritePos += UTF8EncodeOne((uint8*)(Dest + WritePos),
+ (DestCapacity - WritePos), (uint8)Src[ReadPos]);
+ }
+ return WritePos;
+}
+
+bool ParseBoolean(bool *Dest, const char *String){
+ ASSERT(Dest && String);
+ *Dest = StringEqCI(String, "true")
+ || StringEqCI(String, "on")
+ || StringEqCI(String, "yes");
+ return *Dest
+ || StringEqCI(String, "false")
+ || StringEqCI(String, "off")
+ || StringEqCI(String, "no");
+}
+
+bool ParseInteger(int *Dest, const char *String){
+ ASSERT(Dest && String);
+ const char *StringEnd;
+ *Dest = (int)strtol(String, (char**)&StringEnd, 0);
+ return StringEnd > String;
+}
+
+bool ParseDuration(int *Dest, const char *String){
+ ASSERT(Dest && String);
+ const char *Suffix;
+ *Dest = (int)strtol(String, (char**)&Suffix, 0);
+ if(Suffix == String){
+ return false;
+ }
+
+ while(Suffix[0] != 0 && isspace(Suffix[0])){
+ Suffix += 1;
+ }
+
+ if(Suffix[0] == 'S' || Suffix[0] == 's'){
+ *Dest *= (1);
+ }else if(Suffix[0] == 'M' || Suffix[0] == 'm'){
+ *Dest *= (60);
+ }else if(Suffix[0] == 'H' || Suffix[0] == 'h'){
+ *Dest *= (60 * 60);
+ }
+
+ return true;
+}
+
+bool ParseSize(int *Dest, const char *String){
+ ASSERT(Dest && String);
+ const char *Suffix;
+ *Dest = (int)strtol(String, (char**)&Suffix, 0);
+ if(Suffix == String){
+ 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 ParseString(char *Dest, int DestCapacity, const char *String){
+ ASSERT(Dest && DestCapacity > 0 && String);
+ int StringStart = 0;
+ int StringEnd = (int)strlen(String);
+ if(StringEnd >= 2){
+ if((String[0] == '"' && String[StringEnd - 1] == '"')
+ || (String[0] == '\'' && String[StringEnd - 1] == '\'')
+ || (String[0] == '`' && String[StringEnd - 1] == '`')){
+ StringStart += 1;
+ StringEnd -= 1;
+ }
+ }
+
+ return StringCopyN(Dest, DestCapacity,
+ &String[StringStart], (StringEnd - StringStart));
+}
+
+void ParseMotd(char *Dest, int DestCapacity, const char *String){
+ char *Motd = (char*)alloca(DestCapacity);
+ ParseString(Motd, DestCapacity, String);
+ if(Motd[0] != 0){
+ StringFormat(Dest, DestCapacity, "%u\n%s", StringHash(Motd), Motd);
+ }
+}
+
+bool ReadConfig(const char *FileName, TConfig *Config){
+ 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){
+ const int MaxLineSize = 1024;
+ char Line[MaxLineSize];
+ 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(!StringBufCopyN(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(!StringBufCopyN(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, "LoginPort")){
+ ParseInteger(&Config->LoginPort, Val);
+ }else if(StringEqCI(Key, "ConnectionTimeout")){
+ ParseDuration(&Config->ConnectionTimeout, Val);
+ }else if(StringEqCI(Key, "MaxConnections")){
+ ParseInteger(&Config->MaxConnections, Val);
+ }else if(StringEqCI(Key, "MaxStatusRecords")){
+ ParseInteger(&Config->MaxStatusRecords, Val);
+ }else if(StringEqCI(Key, "MinStatusInterval")){
+ ParseDuration(&Config->MinStatusInterval, Val);
+ }else if(StringEqCI(Key, "QueryManagerHost")){
+ ParseStringBuf(Config->QueryManagerHost, Val);
+ }else if(StringEqCI(Key, "QueryManagerPort")){
+ ParseInteger(&Config->QueryManagerPort, Val);
+ }else if(StringEqCI(Key, "QueryManagerPassword")){
+ ParseStringBuf(Config->QueryManagerPassword, Val);
+ }else if(StringEqCI(Key, "StatusWorld")){
+ ParseStringBuf(Config->StatusWorld, Val);
+ }else if(StringEqCI(Key, "URL")){
+ ParseStringBuf(Config->Url, Val);
+ }else if(StringEqCI(Key, "Location")){
+ ParseStringBuf(Config->Location, Val);
+ }else if(StringEqCI(Key, "ServerType")){
+ ParseStringBuf(Config->ServerType, Val);
+ }else if(StringEqCI(Key, "ServerVersion")){
+ ParseStringBuf(Config->ServerVersion, Val);
+ }else if(StringEqCI(Key, "ClientVersion")){
+ ParseStringBuf(Config->ClientVersion, Val);
+ }else if(StringEqCI(Key, "MOTD")){
+ ParseMotd(Config->Motd, sizeof(Config->Motd), Val);
+ }else{
+ LOG_WARN("Unknown config \"%s\"", Key);
+ }
+ }
+
+ fclose(File);
+ 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;
+ //WakeConnections?
+}
+
+int main(int argc, const char **argv){
+ (void)argc;
+ (void)argv;
+
+ g_StartTimeMS = GetClockMonotonicMS();
+ g_ShutdownSignal = 0;
+ if(!SigHandler(SIGPIPE, SIG_IGN)
+ || !SigHandler(SIGINT, ShutdownHandler)
+ || !SigHandler(SIGTERM, ShutdownHandler)){
+ return EXIT_FAILURE;
+ }
+
+ // Service Config
+ g_Config.LoginPort = 7171;
+ g_Config.ConnectionTimeout = 5; // seconds
+ g_Config.MaxConnections = 10;
+ g_Config.MaxStatusRecords = 1024;
+ g_Config.MinStatusInterval = 300; // seconds
+ StringBufCopy(g_Config.QueryManagerHost, "127.0.0.1");
+ g_Config.QueryManagerPort = 7173;
+ StringBufCopy(g_Config.QueryManagerPassword, "");
+
+ // Service Info
+ StringBufCopy(g_Config.StatusWorld, "");
+ StringBufCopy(g_Config.Url, "");
+ StringBufCopy(g_Config.Location, "");
+ StringBufCopy(g_Config.ServerType, "");
+ StringBufCopy(g_Config.ServerVersion, "");
+ StringBufCopy(g_Config.ClientVersion, "");
+ StringBufCopy(g_Config.Motd, "");
+
+ LOG("Tibia Login v0.2");
+ if(!ReadConfig("config.cfg", &g_Config)){
+ return EXIT_FAILURE;
+ }
+
+ LOG("Login port: %d", g_Config.LoginPort);
+ LOG("Connection timeout: %ds", g_Config.ConnectionTimeout);
+ LOG("Max connections: %d", g_Config.MaxConnections);
+ LOG("Max status records: %d", g_Config.MaxStatusRecords);
+ LOG("Min status interval: %ds", g_Config.MinStatusInterval);
+ LOG("Query manager host: \"%s\"", g_Config.QueryManagerHost);
+ LOG("Query manager port: %d", g_Config.QueryManagerPort);
+ LOG("Status world: \"%s\"", g_Config.StatusWorld);
+ LOG("URL: \"%s\"", g_Config.Url);
+ LOG("Location: \"%s\"", g_Config.Location);
+ LOG("Server type: \"%s\"", g_Config.ServerType);
+ LOG("Server version: \"%s\"", g_Config.ServerVersion);
+ LOG("Client version: \"%s\"", g_Config.ClientVersion);
+ LOG("MOTD: \"%s\"", g_Config.Motd);
+
+
+ { // NOTE(fusion): Print MOTD preview with escape codes.
+ char MotdPreview[30];
+ if(StringBufEscape(MotdPreview, g_Config.Motd)){
+ LOG("MOTD: \"%s\"", MotdPreview);
+ }else{
+ LOG("MOTD: \"%s...\"", MotdPreview);
+ }
+ }
+
+ atexit(ExitQuery);
+ atexit(ExitConnections);
+ if(!InitQuery() || !InitConnections()){
+ return EXIT_FAILURE;
+ }
+
+ LOG("Running...");
+ while(g_ShutdownSignal == 0){
+ ProcessConnections();
+ }
+
+ LOG("Received signal %d (%s), shutting down...",
+ g_ShutdownSignal, sigdescr_np(g_ShutdownSignal));
+ return EXIT_SUCCESS;
+}
+