diff options
| author | fusion32 <marcopuzziello@gmail.com> | 2025-08-01 19:48:26 -0300 |
|---|---|---|
| committer | fusion32 <marcopuzziello@gmail.com> | 2025-08-01 19:48:26 -0300 |
| commit | d84651a899a6104fce73d95b9c571627c55e5d16 (patch) | |
| tree | bbaf7f7646dec22dc9fdb0f136d5ba5534b1fddf /query.go | |
| download | web-d84651a899a6104fce73d95b9c571627c55e5d16.tar.gz web-d84651a899a6104fce73d95b9c571627c55e5d16.zip | |
bare bones website
Diffstat (limited to 'query.go')
| -rw-r--r-- | query.go | 735 |
1 files changed, 735 insertions, 0 deletions
diff --git a/query.go b/query.go new file mode 100644 index 0000000..baab921 --- /dev/null +++ b/query.go @@ -0,0 +1,735 @@ +package main + +import ( + "encoding/binary" + "net" + "strings" + "sync" + "time" +) + +// TQueryManagerConnection +// ============================================================================== +const ( + APPLICATION_TYPE_GAME = 1 + APPLICATION_TYPE_LOGIN = 2 + APPLICATION_TYPE_WEB = 3 +) + +const ( + QUERY_STATUS_OK = 0 + QUERY_STATUS_ERROR = 1 + QUERY_STATUS_FAILED = 3 +) + +const ( + // TODO(fusion): There are newly created queries to support basic account + // management. A production ready website would need even more queries to + // allow account activation, recovery, deletion, password change, character + // deletion, etc... + + QUERY_LOGIN = 0 + QUERY_CHECK_ACCOUNT_PASSWORD = 10 + QUERY_CREATE_ACCOUNT = 100 + QUERY_CREATE_CHARACTER = 101 + QUERY_GET_ACCOUNT_SUMMARY = 102 + QUERY_GET_CHARACTER_PROFILE = 103 + QUERY_GET_WORLDS = 150 + QUERY_GET_ONLINE_CHARACTERS = 151 + QUERY_GET_KILL_STATISTICS = 152 +) + +type ( + TWorld struct { + Name string + Type string + NumPlayers int + MaxPlayers int + OnlineRecord int + OnlineRecordTime string + } + + TAccountSummary struct { + AccountID int + Email string + PremiumDays int + PendingPremiumDays int + Deleted bool + Characters []TCharacterSummary + } + + TCharacterSummary struct { + Name string + World string + Level int + Profession string + Online bool + Deleted bool + } + + TCharacterProfile struct { + Name string + World string + Sex int + Guild string + Rank string + Title string + Level int + Profession string + Residence string + LastLogin string + PremiumDays int + Online bool + Deleted bool + } + + TKillStatistics struct { + RaceName string + TimesKilled int + PlayersKilled int + } + + TOnlineCharacter struct { + Name string + Level int + Profession string + } + + TAccountCacheEntry struct { + AccountID int + Result int + Data TAccountSummary + LastAccess time.Time + } + + TCharacterCacheEntry struct { + CharacterName string + Result int + Data TCharacterProfile + LastAccess time.Time + } + + TKillStatisticsCacheEntry struct { + World string + Data []TKillStatistics + RefreshTime time.Time + } + + TOnlineCharactersCacheEntry struct { + World string + Data []TOnlineCharacter + RefreshTime time.Time + } + + TQueryManagerConnection struct { + Handle net.Conn + } +) + +func (Connection *TQueryManagerConnection) Connect() bool { + if Connection.Handle != nil { + g_LogErr.Print("Already connected") + return false + } + + var Err error + QueryManagerAddress := JoinHostPort(g_QueryManagerHost, g_QueryManagerPort) + Connection.Handle, Err = net.Dial("tcp4", QueryManagerAddress) + if Err != nil { + g_LogErr.Print(Err) + return false + } + + var LoginBuffer [1024]byte + WriteBuffer := Connection.PrepareQuery(QUERY_LOGIN, LoginBuffer[:]) + WriteBuffer.Write8(APPLICATION_TYPE_WEB) + WriteBuffer.WriteString(g_QueryManagerPassword) + Status, _ := Connection.ExecuteQuery(false, &WriteBuffer) + if Status != QUERY_STATUS_OK { + Connection.Disconnect() + g_LogErr.Printf("Failed to login to query manager (%v)", Status) + return false + } + + return true +} + +func (Connection *TQueryManagerConnection) Disconnect() { + if Connection.Handle != nil { + if Err := Connection.Handle.Close(); Err != nil { + g_LogErr.Print(Err) + } + Connection.Handle = nil + } +} + +func (Connection *TQueryManagerConnection) PrepareQuery(QueryType int, Buffer []byte) TWriteBuffer { + WriteBuffer := TWriteBuffer{Buffer: Buffer, Position: 0} + WriteBuffer.Write16(0) // Request Size + WriteBuffer.Write8(uint8(QueryType)) + return WriteBuffer +} + +func (Connection *TQueryManagerConnection) ExecuteQuery(AutoReconnect bool, WriteBuffer *TWriteBuffer) (Status int, ReadBuffer TReadBuffer) { + // IMPORTANT(fusion): Different from the C++ version, there is no connection + // buffer, and the response is read into the same buffer used by `WriteBuffer`, + // to avoid moving data around when reconnecting in the middle of a query. + // TODO(fusion): Maybe join `TWriteBuffer` and `TReadBuffer` into `TQueryBuffer` + // to avoid confusion on how this function operates? + if WriteBuffer == nil || WriteBuffer.Position <= 2 { + panic("write buffer is empty") + } + + RequestSize := WriteBuffer.Position - 2 + if RequestSize < 0xFFFF { + WriteBuffer.Rewrite16(0, uint16(RequestSize)) + } else { + WriteBuffer.Rewrite16(0, 0xFFFF) + WriteBuffer.Insert32(2, uint32(RequestSize)) + } + + Status = QUERY_STATUS_FAILED + if WriteBuffer.Overflowed() { + g_LogErr.Print("Write buffer overflowed") + return + } + + const MaxAttempts = 2 + Buffer := WriteBuffer.Buffer + WriteSize := WriteBuffer.Position + for Attempt := 1; true; Attempt += 1 { + if Connection.Handle == nil && (!AutoReconnect || !Connection.Connect()) { + return + } + + if _, Err := Connection.Handle.Write(Buffer[:WriteSize]); Err != nil { + Connection.Disconnect() + if Attempt >= MaxAttempts { + g_LogErr.Printf("Failed to write request: %v", Err) + return + } + continue + } + + var Help [4]byte + if _, Err := Connection.Handle.Read(Help[:2]); Err != nil { + Connection.Disconnect() + if Attempt >= MaxAttempts { + g_LogErr.Printf("Failed to read response size: %v", Err) + return + } + continue + } + + ResponseSize := int(binary.LittleEndian.Uint16(Help[:2])) + if ResponseSize == 0xFFFF { + if _, Err := Connection.Handle.Read(Help[:]); Err != nil { + Connection.Disconnect() + g_LogErr.Printf("Failed to read response extended size: %v", Err) + return + } + + ResponseSize = int(binary.LittleEndian.Uint32(Help[:])) + } + + if ResponseSize <= 0 || ResponseSize > len(Buffer) { + Connection.Disconnect() + g_LogErr.Printf("Invalid response size %v (BufferSize: %v)", + ResponseSize, len(Buffer)) + return + } + + if _, Err := Connection.Handle.Read(Buffer[:ResponseSize]); Err != nil { + Connection.Disconnect() + g_LogErr.Printf("Failed to read response: %v", Err) + return + } + + ReadBuffer = TReadBuffer{ + Buffer: Buffer, + Position: 0, + } + Status = int(ReadBuffer.Read8()) + return + } + + // NOTE(fusion): The compiler complains there is no return statement here + // but the loop above can only exit by returning from the function which + // make anything after it UNREACHABLE. + return +} + +func (Connection *TQueryManagerConnection) CheckAccountPassword(AccountID int, Password, IPAddress string) (Result int) { + var Buffer [1024]byte + WriteBuffer := Connection.PrepareQuery(QUERY_CHECK_ACCOUNT_PASSWORD, Buffer[:]) + WriteBuffer.Write32(uint32(AccountID)) + WriteBuffer.WriteString(Password) + WriteBuffer.WriteString(IPAddress) + Status, ReadBuffer := Connection.ExecuteQuery(true, &WriteBuffer) + Result = -1 + switch Status { + case QUERY_STATUS_OK: + Result = 0 + case QUERY_STATUS_ERROR: + ErrorCode := int(ReadBuffer.Read8()) + if ErrorCode >= 1 && ErrorCode <= 4 { + Result = ErrorCode + } else { + g_LogErr.Printf("Invalid error code %v", ErrorCode) + } + default: + g_LogErr.Printf("Request failed (%v)", Status) + } + return +} + +func (Connection *TQueryManagerConnection) CreateAccount(AccountID int, Email string, Password string) (Result int) { + var Buffer [1024]byte + WriteBuffer := Connection.PrepareQuery(QUERY_CREATE_ACCOUNT, Buffer[:]) + WriteBuffer.Write32(uint32(AccountID)) + WriteBuffer.WriteString(Email) + WriteBuffer.WriteString(Password) + Status, ReadBuffer := Connection.ExecuteQuery(true, &WriteBuffer) + Result = -1 + switch Status { + case QUERY_STATUS_OK: + Result = 0 + case QUERY_STATUS_ERROR: + ErrorCode := int(ReadBuffer.Read8()) + if ErrorCode >= 1 && ErrorCode <= 2 { + Result = ErrorCode + } else { + g_LogErr.Printf("Invalid error code %v", ErrorCode) + } + default: + g_LogErr.Printf("Request failed (%v)", Status) + } + return +} + +func (Connection *TQueryManagerConnection) CreateCharacter(World string, AccountID int, Name string, Sex int) (Result int) { + var Buffer [1024]byte + WriteBuffer := Connection.PrepareQuery(QUERY_CREATE_CHARACTER, Buffer[:]) + WriteBuffer.WriteString(World) + WriteBuffer.Write32(uint32(AccountID)) + WriteBuffer.WriteString(Name) + WriteBuffer.Write8(uint8(Sex)) + Status, ReadBuffer := Connection.ExecuteQuery(true, &WriteBuffer) + Result = -1 + switch Status { + case QUERY_STATUS_OK: + Result = 0 + case QUERY_STATUS_ERROR: + ErrorCode := int(ReadBuffer.Read8()) + if ErrorCode >= 1 && ErrorCode <= 3 { + Result = ErrorCode + } else { + g_LogErr.Printf("Invalid error code %v", ErrorCode) + } + default: + g_LogErr.Printf("Request failed (%v)", Status) + } + return +} + +func (Connection *TQueryManagerConnection) GetAccountSummary(AccountID int) (Result int, Account TAccountSummary) { + var Buffer [16384]byte + WriteBuffer := Connection.PrepareQuery(QUERY_GET_ACCOUNT_SUMMARY, Buffer[:]) + WriteBuffer.Write32(uint32(AccountID)) + Status, ReadBuffer := Connection.ExecuteQuery(true, &WriteBuffer) + Result = -1 + switch Status { + case QUERY_STATUS_OK: + Result = 0 + Account.AccountID = AccountID + Account.Email = ReadBuffer.ReadString() + Account.PremiumDays = int(ReadBuffer.Read16()) + Account.PendingPremiumDays = int(ReadBuffer.Read16()) + Account.Deleted = ReadBuffer.ReadFlag() + NumCharacters := int(ReadBuffer.Read8()) + if NumCharacters > 0 { + Account.Characters = make([]TCharacterSummary, NumCharacters) + for Index := range Account.Characters { + Account.Characters[Index].Name = ReadBuffer.ReadString() + Account.Characters[Index].World = ReadBuffer.ReadString() + Account.Characters[Index].Level = int(ReadBuffer.Read16()) + Account.Characters[Index].Profession = ReadBuffer.ReadString() + Account.Characters[Index].Online = ReadBuffer.ReadFlag() + Account.Characters[Index].Deleted = ReadBuffer.ReadFlag() + } + } + case QUERY_STATUS_ERROR: + ErrorCode := int(ReadBuffer.Read8()) + if ErrorCode >= 1 && ErrorCode <= 4 { + Result = ErrorCode + } else { + g_LogErr.Printf("Invalid error code %v", ErrorCode) + } + default: + g_LogErr.Printf("Request failed (%v)", Status) + } + return +} + +func (Connection *TQueryManagerConnection) GetCharacterProfile(CharacterName string) (Result int, Character TCharacterProfile) { + var Buffer [16384]byte + WriteBuffer := Connection.PrepareQuery(QUERY_GET_CHARACTER_PROFILE, Buffer[:]) + WriteBuffer.WriteString(CharacterName) + Status, ReadBuffer := Connection.ExecuteQuery(true, &WriteBuffer) + Result = -1 + switch Status { + case QUERY_STATUS_OK: + Result = 0 + Character.Name = ReadBuffer.ReadString() + Character.World = ReadBuffer.ReadString() + Character.Sex = int(ReadBuffer.Read8()) + Character.Guild = ReadBuffer.ReadString() + Character.Rank = ReadBuffer.ReadString() + Character.Title = ReadBuffer.ReadString() + Character.Level = int(ReadBuffer.Read16()) + Character.Profession = ReadBuffer.ReadString() + Character.Residence = ReadBuffer.ReadString() + Character.LastLogin = TimestampString(int(ReadBuffer.Read32())) + Character.PremiumDays = int(ReadBuffer.Read16()) + Character.Online = ReadBuffer.ReadFlag() + Character.Deleted = ReadBuffer.ReadFlag() + case QUERY_STATUS_ERROR: + ErrorCode := int(ReadBuffer.Read8()) + if ErrorCode == 1 { + Result = ErrorCode + } else { + g_LogErr.Printf("Invalid error code %v", ErrorCode) + } + default: + g_LogErr.Printf("Request failed (%v)", Status) + } + return +} + +func (Connection *TQueryManagerConnection) GetWorlds() (Result int, Worlds []TWorld) { + var Buffer [16384]byte + WriteBuffer := Connection.PrepareQuery(QUERY_GET_WORLDS, Buffer[:]) + Status, ReadBuffer := Connection.ExecuteQuery(true, &WriteBuffer) + Result = -1 + switch Status { + case QUERY_STATUS_OK: + Result = 0 + NumWorlds := int(ReadBuffer.Read8()) + if NumWorlds > 0 { + Worlds = make([]TWorld, NumWorlds) + for Index := range Worlds { + Worlds[Index].Name = ReadBuffer.ReadString() + Worlds[Index].Type = WorldTypeString(int(ReadBuffer.Read8())) + Worlds[Index].NumPlayers = int(ReadBuffer.Read16()) + Worlds[Index].MaxPlayers = int(ReadBuffer.Read16()) + Worlds[Index].OnlineRecord = int(ReadBuffer.Read16()) + Worlds[Index].OnlineRecordTime = TimestampString(int(ReadBuffer.Read32())) + } + } + default: + g_LogErr.Printf("Request failed (%v)", Status) + } + return +} + +func (Connection *TQueryManagerConnection) GetOnlineCharacters(World string) (Result int, Characters []TOnlineCharacter) { + var Buffer [65536]byte + WriteBuffer := Connection.PrepareQuery(QUERY_GET_ONLINE_CHARACTERS, Buffer[:]) + WriteBuffer.WriteString(World) + Status, ReadBuffer := Connection.ExecuteQuery(true, &WriteBuffer) + Result = -1 + switch Status { + case QUERY_STATUS_OK: + Result = 0 + NumCharacters := int(ReadBuffer.Read16()) + if NumCharacters > 0 { + Characters = make([]TOnlineCharacter, NumCharacters) + for Index := 0; Index < NumCharacters; Index += 1 { + Characters[Index].Name = ReadBuffer.ReadString() + Characters[Index].Level = int(ReadBuffer.Read16()) + Characters[Index].Profession = ReadBuffer.ReadString() + } + } + default: + g_LogErr.Printf("Request failed (%v)", Status) + } + return +} + +func (Connection *TQueryManagerConnection) GetKillStatistics(World string) (Result int, Stats []TKillStatistics) { + var Buffer [65536]byte + WriteBuffer := Connection.PrepareQuery(QUERY_GET_KILL_STATISTICS, Buffer[:]) + WriteBuffer.WriteString(World) + Status, ReadBuffer := Connection.ExecuteQuery(true, &WriteBuffer) + Result = -1 + switch Status { + case QUERY_STATUS_OK: + Result = 0 + NumStats := int(ReadBuffer.Read16()) + if NumStats > 0 { + Stats = make([]TKillStatistics, NumStats) + for Index := 0; Index < NumStats; Index += 1 { + Stats[Index].RaceName = ReadBuffer.ReadString() + Stats[Index].PlayersKilled = int(ReadBuffer.Read32()) + Stats[Index].TimesKilled = int(ReadBuffer.Read32()) + } + } + default: + g_LogErr.Printf("Request failed (%v)", Status) + } + return +} + +// Query Subsystem +// ============================================================================== +var ( + g_QueryManagerMutex sync.Mutex + g_QueryManagerConnection TQueryManagerConnection + + g_AccountCache []TAccountCacheEntry + g_CharacterCache []TCharacterCacheEntry + g_WorldCache []TWorld + g_WorldCacheRefreshTime time.Time + g_OnlineCharactersCache []TOnlineCharactersCacheEntry + g_KillStatisticsCache []TKillStatisticsCacheEntry +) + +func InitQuery() bool { + g_Log.Printf("QueryManagerHost: %v", g_QueryManagerHost) + g_Log.Printf("QueryManagerPort: %v", g_QueryManagerPort) + g_Log.Printf("MaxCachedAccounts: %v", g_MaxCachedAccounts) + g_Log.Printf("MaxCachedCharacters: %v", g_MaxCachedCharacters) + g_Log.Printf("CharacterRefreshInterval: %v", g_CharacterRefreshInterval) + g_Log.Printf("WorldsRefreshInterval: %v", g_WorldsRefreshInterval) + g_Log.Printf("OnlineCharactersRefreshInterval: %v", g_OnlineCharactersRefreshInterval) + g_Log.Printf("KillStatisticsRefreshInterval: %v", g_KillStatisticsRefreshInterval) + + Result := g_QueryManagerConnection.Connect() + if !Result { + g_LogErr.Print("Failed to connect to query manager") + } + return Result +} + +func ExitQuery() { + g_QueryManagerConnection.Disconnect() +} + +func CheckAccountPassword(AccountID int, Password, IPAddress string) int { + g_QueryManagerMutex.Lock() + defer g_QueryManagerMutex.Unlock() + return g_QueryManagerConnection.CheckAccountPassword(AccountID, Password, IPAddress) +} + +func CreateAccount(AccountID int, Email string, Password string) int { + g_QueryManagerMutex.Lock() + defer g_QueryManagerMutex.Unlock() + return g_QueryManagerConnection.CreateAccount(AccountID, Email, Password) +} + +func CreateCharacter(World string, AccountID int, Name string, Sex int) int { + g_QueryManagerMutex.Lock() + defer g_QueryManagerMutex.Unlock() + return g_QueryManagerConnection.CreateCharacter(World, AccountID, Name, Sex) +} + +func GetAccountSummary(AccountID int) (Result int, Account TAccountSummary) { + g_QueryManagerMutex.Lock() + defer g_QueryManagerMutex.Unlock() + + if g_AccountCache == nil { + g_AccountCache = make([]TAccountCacheEntry, g_MaxCachedAccounts) + } + + var Entry *TAccountCacheEntry + LeastRecentlyUsedIndex := 0 + LeastRecentlyUsedTime := g_AccountCache[0].LastAccess + for Index := 0; Index < len(g_AccountCache); Index += 1 { + Current := &g_AccountCache[Index] + + if Current.LastAccess.Before(LeastRecentlyUsedTime) { + LeastRecentlyUsedIndex = Index + LeastRecentlyUsedTime = Current.LastAccess + } + + if Current.AccountID == AccountID { + Entry = Current + break + } + } + + if Entry == nil { + Result, Account = g_QueryManagerConnection.GetAccountSummary(AccountID) + if Result == 0 { + Entry = &g_AccountCache[LeastRecentlyUsedIndex] + Entry.AccountID = AccountID + Entry.Data = Account + Entry.LastAccess = time.Now() + } + } else { + Result = 0 + Account = Entry.Data + Entry.LastAccess = time.Now() + } + + return +} + +// NOTE(fusion): Account data is different from other cached data because it +// shouldn't change over time unless we do it ourselves, in which case we know +// exactly when to invalidate cached data. +func InvalidateAccountCachedData(AccountID int) { + g_QueryManagerMutex.Lock() + defer g_QueryManagerMutex.Unlock() + for Index := 0; Index < len(g_AccountCache); Index += 1 { + if g_AccountCache[Index].AccountID == AccountID { + g_AccountCache[Index] = TAccountCacheEntry{} + break + } + } +} + +func GetCharacterProfile(CharacterName string) (Result int, Character TCharacterProfile) { + g_QueryManagerMutex.Lock() + defer g_QueryManagerMutex.Unlock() + + if g_CharacterCache == nil { + g_CharacterCache = make([]TCharacterCacheEntry, g_MaxCachedCharacters) + } + + var Entry *TCharacterCacheEntry + LeastRecentlyUsedIndex := 0 + LeastRecentlyUsedTime := g_CharacterCache[0].LastAccess + for Index := 0; Index < len(g_CharacterCache); Index += 1 { + Current := &g_CharacterCache[Index] + + if time.Since(Current.LastAccess) >= g_CharacterRefreshInterval { + *Current = TCharacterCacheEntry{} + } + + if Current.LastAccess.Before(LeastRecentlyUsedTime) { + LeastRecentlyUsedIndex = Index + LeastRecentlyUsedTime = Current.LastAccess + } + + if strings.EqualFold(Current.CharacterName, CharacterName) { + Entry = Current + break + } + } + + if Entry == nil { + Result, Character = g_QueryManagerConnection.GetCharacterProfile(CharacterName) + Entry = &g_CharacterCache[LeastRecentlyUsedIndex] + Entry.CharacterName = CharacterName + Entry.Result = Result + Entry.Data = Character + Entry.LastAccess = time.Now() + } else { + Result = Entry.Result + Character = Entry.Data + Entry.LastAccess = time.Now() + } + + return +} + +func GetWorlds() []TWorld { + g_QueryManagerMutex.Lock() + defer g_QueryManagerMutex.Unlock() + if time.Until(g_WorldCacheRefreshTime) <= 0 { + // IMPORTANT(fusion): `GetWorlds` will return a FRESH slice. This will + // prevent race conditions regarding any previous world slice, assuming + // we're only reading from them. + Result, Worlds := g_QueryManagerConnection.GetWorlds() + if Result == 0 { + g_WorldCache = Worlds + g_WorldCacheRefreshTime = time.Now().Add(g_WorldsRefreshInterval) + } + } + return g_WorldCache +} + +func GetWorld(World string) *TWorld { + Worlds := GetWorlds() + for Index := range Worlds { + if strings.EqualFold(Worlds[Index].Name, World) { + return &Worlds[Index] + } + } + return nil +} + +func GetOnlineCharacters(World string) []TOnlineCharacter { + g_QueryManagerMutex.Lock() + defer g_QueryManagerMutex.Unlock() + + var Entry *TOnlineCharactersCacheEntry + for Index := 0; Index < len(g_OnlineCharactersCache); Index += 1 { + Current := &g_OnlineCharactersCache[Index] + if time.Until(Current.RefreshTime) <= 0 { + g_OnlineCharactersCache = SwapAndPop(g_OnlineCharactersCache, Index) + Index -= 1 + continue + } + + if strings.EqualFold(Current.World, World) { + Entry = Current + break + } + } + + if Entry == nil { + Result, Characters := g_QueryManagerConnection.GetOnlineCharacters(World) + if Result == 0 { + g_OnlineCharactersCache = append(g_OnlineCharactersCache, TOnlineCharactersCacheEntry{}) + Entry = &g_OnlineCharactersCache[len(g_OnlineCharactersCache)-1] + Entry.World = World + Entry.Data = Characters + Entry.RefreshTime = time.Now().Add(g_OnlineCharactersRefreshInterval) + } + } + + if Entry != nil { + return Entry.Data + } else { + return nil + } +} + +func GetKillStatistics(World string) []TKillStatistics { + g_QueryManagerMutex.Lock() + defer g_QueryManagerMutex.Unlock() + + var Entry *TKillStatisticsCacheEntry + for Index := 0; Index < len(g_KillStatisticsCache); Index += 1 { + Current := &g_KillStatisticsCache[Index] + if time.Until(Current.RefreshTime) <= 0 { + g_KillStatisticsCache = SwapAndPop(g_KillStatisticsCache, Index) + Index -= 1 + continue + } + + if strings.EqualFold(Current.World, World) { + Entry = Current + break + } + } + + if Entry == nil { + Result, Stats := g_QueryManagerConnection.GetKillStatistics(World) + if Result == 0 { + g_KillStatisticsCache = append(g_KillStatisticsCache, TKillStatisticsCacheEntry{}) + Entry = &g_KillStatisticsCache[len(g_KillStatisticsCache)-1] + Entry.World = World + Entry.Data = Stats + Entry.RefreshTime = time.Now().Add(g_KillStatisticsRefreshInterval) + } + } + + if Entry != nil { + return Entry.Data + } else { + return nil + } +} |
