745 lines
21 KiB
Go
745 lines
21 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"io"
|
|
"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
|
|
OnlinePeak int
|
|
OnlinePeakTimestamp int
|
|
LastStartup int
|
|
LastShutdown int
|
|
}
|
|
|
|
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 int
|
|
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 := io.ReadFull(Connection.Handle, 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 := io.ReadFull(Connection.Handle, 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 := io.ReadFull(Connection.Handle, 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 = 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].OnlinePeak = int(ReadBuffer.Read16())
|
|
Worlds[Index].OnlinePeakTimestamp = int(ReadBuffer.Read32())
|
|
Worlds[Index].LastStartup = int(ReadBuffer.Read32())
|
|
Worlds[Index].LastShutdown = 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("WorldRefreshInterval: %v", g_WorldRefreshInterval)
|
|
|
|
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]
|
|
|
|
// NOTE(fusion): Account data itself shouldn't change over time unless
|
|
// we do it ourselves, in which case `InvalidateAccountCachedData` is
|
|
// used to invalidate the cache entry. The problem is that the account
|
|
// summary also includes character data which will change, depending on
|
|
// activities on the game server.
|
|
if time.Since(Current.LastAccess) >= g_CharacterRefreshInterval {
|
|
*Current = TAccountCacheEntry{}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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_WorldRefreshInterval)
|
|
}
|
|
}
|
|
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_WorldRefreshInterval)
|
|
}
|
|
}
|
|
|
|
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_WorldRefreshInterval)
|
|
}
|
|
}
|
|
|
|
if Entry != nil {
|
|
return Entry.Data
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|