aboutsummaryrefslogtreecommitdiff
path: root/query.go
diff options
context:
space:
mode:
authorfusion32 <marcopuzziello@gmail.com>2025-08-01 19:48:26 -0300
committerfusion32 <marcopuzziello@gmail.com>2025-08-01 19:48:26 -0300
commitd84651a899a6104fce73d95b9c571627c55e5d16 (patch)
treebbaf7f7646dec22dc9fdb0f136d5ba5534b1fddf /query.go
downloadweb-d84651a899a6104fce73d95b9c571627c55e5d16.tar.gz
web-d84651a899a6104fce73d95b9c571627c55e5d16.zip
bare bones website
Diffstat (limited to 'query.go')
-rw-r--r--query.go735
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
+ }
+}