Seperate into different files WORKING
This commit is contained in:
parent
f58a252c61
commit
b8a6b68888
219
app.go
Normal file
219
app.go
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
LogPath string
|
||||||
|
API *HypixelAPI
|
||||||
|
UUIDCache *UUIDCache
|
||||||
|
CurrentDisplayTable DisplayTable
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApp(apiKey string) *App {
|
||||||
|
path := os.Getenv("USERPROFILE")
|
||||||
|
logPath := filepath.Join(path, ".lunarclient", "offline", "multiver", "logs", "latest.log")
|
||||||
|
|
||||||
|
return &App{
|
||||||
|
LogPath: logPath,
|
||||||
|
API: NewHypixelAPI(apiKey),
|
||||||
|
UUIDCache: NewUUIDCache(120 * time.Minute),
|
||||||
|
CurrentDisplayTable: &BedwarsDisplayTable{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) Start() {
|
||||||
|
|
||||||
|
rl.SetConfigFlags(rl.FlagWindowTransparent | rl.FlagWindowUndecorated)
|
||||||
|
screenWidth := 800
|
||||||
|
screenHeight := 600
|
||||||
|
rl.InitWindow(int32(screenWidth), int32(screenHeight), "Raylib in Go")
|
||||||
|
defer rl.CloseWindow()
|
||||||
|
|
||||||
|
windowPosition := rl.NewVector2(500, 200)
|
||||||
|
rl.SetWindowPosition(int(int32(windowPosition.X)), int(int32(windowPosition.Y)))
|
||||||
|
|
||||||
|
var panOffset rl.Vector2
|
||||||
|
var dragWindow bool = false
|
||||||
|
|
||||||
|
const LowFps = 8
|
||||||
|
rl.SetTargetFPS(LowFps)
|
||||||
|
|
||||||
|
lineCh := make(chan string)
|
||||||
|
go tailFile(a.LogPath, lineCh)
|
||||||
|
|
||||||
|
ch := make(chan HypixelPlayerResponse)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(ch)
|
||||||
|
}()
|
||||||
|
|
||||||
|
sx := 10
|
||||||
|
sy := 10
|
||||||
|
|
||||||
|
for !rl.WindowShouldClose() {
|
||||||
|
mousePosition := rl.GetMousePosition()
|
||||||
|
windowPosition := rl.GetWindowPosition()
|
||||||
|
screenMousePosition := rl.Vector2{
|
||||||
|
X: windowPosition.X + mousePosition.X,
|
||||||
|
Y: windowPosition.Y + mousePosition.Y,
|
||||||
|
}
|
||||||
|
|
||||||
|
if rl.IsMouseButtonPressed(rl.MouseLeftButton) && !dragWindow {
|
||||||
|
rl.SetTargetFPS(int32(rl.GetMonitorRefreshRate(0)))
|
||||||
|
dragWindow = true
|
||||||
|
panOffset.X = screenMousePosition.X - float32(windowPosition.X)
|
||||||
|
panOffset.Y = screenMousePosition.Y - float32(windowPosition.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dragWindow {
|
||||||
|
newWindowPositionX := int(screenMousePosition.X - panOffset.X)
|
||||||
|
newWindowPositionY := int(screenMousePosition.Y - panOffset.Y)
|
||||||
|
|
||||||
|
rl.SetWindowPosition(newWindowPositionX, newWindowPositionY)
|
||||||
|
|
||||||
|
if rl.IsMouseButtonReleased(rl.MouseLeftButton) {
|
||||||
|
dragWindow = false
|
||||||
|
rl.SetTargetFPS(LowFps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case line := <-lineCh:
|
||||||
|
a.onFileEmit(replaceCorruptedRune(line))
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.BeginDrawing()
|
||||||
|
rl.ClearBackground(rl.Color{0, 0, 0, 100})
|
||||||
|
|
||||||
|
yOffset := sy
|
||||||
|
for _, player := range a.CurrentDisplayTable.GetPlayers() {
|
||||||
|
playerName := player.DisplayName
|
||||||
|
bedwarsLevel := player.Achievements.BedwarsStar
|
||||||
|
bedwarsWins := player.Stats.Bedwars.Wins
|
||||||
|
|
||||||
|
rl.DrawText(fmt.Sprintf("Player: %s", playerName), int32(sx), int32(yOffset), 20, rl.Black)
|
||||||
|
yOffset += 25
|
||||||
|
rl.DrawText(fmt.Sprintf("Bedwars Level: %d", bedwarsLevel), int32(sx), int32(yOffset), 18, rl.DarkGray)
|
||||||
|
yOffset += 25
|
||||||
|
rl.DrawText(fmt.Sprintf("Bedwars Wins: %d", bedwarsWins), int32(sx), int32(yOffset), 18, rl.DarkGray)
|
||||||
|
yOffset += 40
|
||||||
|
}
|
||||||
|
rl.EndDrawing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) onFileEmit(line string) {
|
||||||
|
msg := strings.TrimSpace(line)
|
||||||
|
if len(msg) < 34 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submsg := msg[33:]
|
||||||
|
if len(submsg) != 0 {
|
||||||
|
LogBuf.Add(submsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
OnlinePrefix := "[CHAT] ONLINE: "
|
||||||
|
PartyListSeparatorLinePrefix := "[CHAT] -----------------------------------------------------"
|
||||||
|
PartyMemberCountPrefix := "[CHAT] Party Members ("
|
||||||
|
PartyLeaderPrefix := "[CHAT] Party Leader: "
|
||||||
|
PartyListMembersPrefix := "[CHAT] Party Members: "
|
||||||
|
|
||||||
|
if strings.HasPrefix(submsg, OnlinePrefix) { // Online Message
|
||||||
|
newsubmsg := strings.TrimPrefix(submsg, OnlinePrefix)
|
||||||
|
players := strings.Split(newsubmsg, ",")
|
||||||
|
for _, player := range players {
|
||||||
|
playerName := strings.TrimSpace(player)
|
||||||
|
playerUUID, err := FetchMCPlayer(playerName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error fetching UUID: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("UUID of player %s: %s\n", playerName, playerUUID)
|
||||||
|
cachedPlayer := CachedUuid{playerUUID.UUID, playerName, playerName, time.Now()}
|
||||||
|
a.UUIDCache.Add(&cachedPlayer)
|
||||||
|
|
||||||
|
//names, err := GetNameFromUUID(playerUUID)
|
||||||
|
//if err != nil {
|
||||||
|
// log.Fatalf("Error fetching names from UUID: %v", err)
|
||||||
|
//}
|
||||||
|
//fmt.Printf("Name history for UUID %s: %v\n", playerUUID, names)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if strings.HasPrefix(submsg, PartyListSeparatorLinePrefix) { // Party List
|
||||||
|
last, _ := LogBuf.GetSecondToLast()
|
||||||
|
// TODO: Check if moderators
|
||||||
|
if !strings.HasPrefix(last, PartyListMembersPrefix) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
PartyMembersMsg := strings.TrimPrefix(last, PartyListMembersPrefix)
|
||||||
|
for _, player := range strings.Split(PartyMembersMsg, ",") {
|
||||||
|
playerName := strings.TrimSpace(strings.TrimSuffix(player, " ?"))
|
||||||
|
if strings.HasPrefix(playerName, "[") {
|
||||||
|
playerName = strings.Split(playerName, " ")[1]
|
||||||
|
}
|
||||||
|
print("Found Player: '")
|
||||||
|
print(playerName)
|
||||||
|
print("'\n")
|
||||||
|
a.CurrentDisplayTable.AddPlayerN(playerName, a)
|
||||||
|
|
||||||
|
//playerName := strings.TrimSpace(player)
|
||||||
|
//playerUUID, err := GetUUIDFromName(playerName)
|
||||||
|
//if err != nil {
|
||||||
|
// log.Fatalf("Error fetching UUID: %v", err)
|
||||||
|
// return
|
||||||
|
//}
|
||||||
|
//fmt.Printf("UUID of player %s: %s\n", playerName, playerUUID)
|
||||||
|
//cachedPlayer := CachedUuid{playerUUID, playerName, playerName, time.Now()}
|
||||||
|
//UuidCache.Add(&cachedPlayer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse Party Leader
|
||||||
|
leaders_msg, err := LogBuf.GetLineStepsBack(2)
|
||||||
|
if err != nil {
|
||||||
|
println("Unable to find party leader message")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
PartyLeaderMsg := strings.TrimPrefix(leaders_msg, PartyLeaderPrefix)
|
||||||
|
playerName := strings.TrimSpace(strings.TrimSuffix(PartyLeaderMsg, " ?"))
|
||||||
|
if strings.HasPrefix(playerName, "[") {
|
||||||
|
playerName = strings.Split(playerName, " ")[1]
|
||||||
|
}
|
||||||
|
print("Found Leader: '")
|
||||||
|
print(playerName)
|
||||||
|
print("'\n")
|
||||||
|
a.CurrentDisplayTable.AddPlayerN(playerName, a)
|
||||||
|
|
||||||
|
// Parse Party Count
|
||||||
|
party_count_msg, err := LogBuf.GetLineStepsBack(4)
|
||||||
|
if err != nil {
|
||||||
|
println("Unable to find party count message")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
PartyCountMsg := strings.TrimPrefix(party_count_msg, PartyMemberCountPrefix)
|
||||||
|
count_str := strings.TrimSuffix(PartyCountMsg, ")")
|
||||||
|
count, err := strconv.Atoi(count_str)
|
||||||
|
if err != nil {
|
||||||
|
println("Unable to parse party count message - Invalid number used")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
print("Expected ")
|
||||||
|
print(count)
|
||||||
|
print(" party members\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
println(submsg)
|
||||||
|
}
|
762
app/app.go
762
app/app.go
@ -1,762 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
rl "github.com/gen2brain/raylib-go/raylib"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"runtime"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func errorContext(skip int) string {
|
|
||||||
pc, file, line, ok := runtime.Caller(skip)
|
|
||||||
if !ok {
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn := runtime.FuncForPC(pc).Name()
|
|
||||||
return fmt.Sprintf("%s:%d %s", file, line, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func APITraceError(format string, args ...interface{}) error {
|
|
||||||
traceInfo := errorContext(2)
|
|
||||||
return fmt.Errorf("[%s] "+format, append([]interface{}{traceInfo}, args...)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
type HypixelPlayerAchievements struct {
|
|
||||||
BedwarsStar int `json:"bedwars_level"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HypixelPlayerBedwarsStats struct {
|
|
||||||
Experience int `json:"Experience"`
|
|
||||||
Winstreak int `json:"winstreak"`
|
|
||||||
Wins int `json:"wins_bedwars"`
|
|
||||||
Losses int `json:"losses_bedwars"`
|
|
||||||
GamesPlayed int `json:"games_played_bedwars"`
|
|
||||||
WinLossRatio float32 // wins / gamesplayed
|
|
||||||
|
|
||||||
Kills int `json:"kills_bedwars"`
|
|
||||||
Deaths int `json:"deaths_bedwars"`
|
|
||||||
KillDeathRatio float32 // kills / deaths
|
|
||||||
|
|
||||||
FinalKills int `json:"final_kills_bedwars"`
|
|
||||||
FinalDeaths int `json:"final_deaths_bedwars"`
|
|
||||||
FinalKillDeathRatio float32 // final kills / final deaths
|
|
||||||
|
|
||||||
BedsBroken int `json:"beds_broken_bedwars"`
|
|
||||||
BedsLost int `json:"beds_lost_bedwars"`
|
|
||||||
BedsBrokenLostRatio float32 // beds broken / beds lost
|
|
||||||
}
|
|
||||||
|
|
||||||
func (stats *HypixelPlayerBedwarsStats) CalculateRatios() {
|
|
||||||
// WLR
|
|
||||||
if stats.GamesPlayed > 0 {
|
|
||||||
stats.WinLossRatio = float32(stats.Wins) / float32(stats.GamesPlayed)
|
|
||||||
} else {
|
|
||||||
stats.WinLossRatio = float32(stats.Wins)
|
|
||||||
}
|
|
||||||
|
|
||||||
// KDR
|
|
||||||
if stats.Deaths > 0 {
|
|
||||||
stats.KillDeathRatio = float32(stats.Kills) / float32(stats.Deaths)
|
|
||||||
} else {
|
|
||||||
stats.KillDeathRatio = float32(stats.Kills)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FKDR
|
|
||||||
if stats.FinalDeaths > 0 {
|
|
||||||
stats.FinalKillDeathRatio = float32(stats.FinalKills) / float32(stats.FinalDeaths)
|
|
||||||
} else {
|
|
||||||
stats.FinalKillDeathRatio = float32(stats.FinalKills)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BBLR
|
|
||||||
if stats.BedsLost > 0 {
|
|
||||||
stats.BedsBrokenLostRatio = float32(stats.BedsBroken) / float32(stats.BedsLost)
|
|
||||||
} else {
|
|
||||||
stats.BedsBrokenLostRatio = float32(stats.BedsBroken)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (stats *HypixelPlayerBedwarsStats) IsWinstreakDisabled() bool {
|
|
||||||
return stats.Winstreak == -1
|
|
||||||
}
|
|
||||||
|
|
||||||
type HypixelPlayerStats struct {
|
|
||||||
Bedwars HypixelPlayerBedwarsStats `json:"Bedwars"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HypixelPlayer struct {
|
|
||||||
DisplayName string `json:"displayname"`
|
|
||||||
Achievements HypixelPlayerAchievements `json:"achievements"`
|
|
||||||
Stats HypixelPlayerStats `json:"stats"`
|
|
||||||
MonthlyRankColor string `json:"monthlyRankColor"`
|
|
||||||
RankPlusColor string `json:"rankPlusColor"`
|
|
||||||
NetworkExp json.Number `json:"networkExp"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HypixelPlayerResponse struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Player HypixelPlayer `json:"player"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HypixelAPIKey struct {
|
|
||||||
Value string
|
|
||||||
Uses int
|
|
||||||
RateLimit time.Duration
|
|
||||||
LimitUsesLeft int
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHypixelAPIKey(key string) *HypixelAPIKey {
|
|
||||||
return &HypixelAPIKey{
|
|
||||||
Value: key,
|
|
||||||
Uses: 0,
|
|
||||||
RateLimit: time.Millisecond * 1,
|
|
||||||
LimitUsesLeft: -1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type HypixelAPI struct {
|
|
||||||
BaseURL string
|
|
||||||
APIKey HypixelAPIKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHypixelAPI(apiKey string) *HypixelAPI {
|
|
||||||
// TODO: Ensure valid key before creating
|
|
||||||
key := NewHypixelAPIKey(apiKey)
|
|
||||||
return &HypixelAPI{
|
|
||||||
BaseURL: "https://api.hypixel.net/v2/",
|
|
||||||
APIKey: *key,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HypixelAPI) FetchPlayer(PlayerUUID string) (HypixelPlayerResponse, error) {
|
|
||||||
// TODO: Handle nicked players
|
|
||||||
if len(PlayerUUID) != 32 && len(PlayerUUID) != 36 {
|
|
||||||
return HypixelPlayerResponse{}, fmt.Errorf("invalid Player UUID! please pass in a valid UUID (Not a name!)")
|
|
||||||
}
|
|
||||||
RequestUrl := h.BaseURL + "player?uuid=" + PlayerUUID
|
|
||||||
|
|
||||||
client := &http.Client{
|
|
||||||
Timeout: 10 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", RequestUrl, nil)
|
|
||||||
if err != nil {
|
|
||||||
return HypixelPlayerResponse{}, APITraceError("failed to create HTTP request: %v", err)
|
|
||||||
}
|
|
||||||
req.Header.Set("API-Key", h.APIKey.Value)
|
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return HypixelPlayerResponse{}, APITraceError("failed to send HTTP request: %v", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return HypixelPlayerResponse{}, APITraceError("received non-200 response: %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return HypixelPlayerResponse{}, APITraceError("failed to read response body: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var playerResponse HypixelPlayerResponse
|
|
||||||
err = json.Unmarshal(body, &playerResponse)
|
|
||||||
if err != nil {
|
|
||||||
return HypixelPlayerResponse{}, APITraceError("failed to unmarshal JSON: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !playerResponse.Success {
|
|
||||||
return HypixelPlayerResponse{}, APITraceError("API returned unsuccessful response")
|
|
||||||
}
|
|
||||||
|
|
||||||
playerResponse.Player.Stats.Bedwars.CalculateRatios()
|
|
||||||
|
|
||||||
return playerResponse, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HypixelAPI) FetchPlayerAsync(PlayerUUID string, resultChan chan<- HypixelPlayerResponse, errorChan chan<- error) {
|
|
||||||
go func() {
|
|
||||||
result, err := h.FetchPlayer(PlayerUUID)
|
|
||||||
if err != nil {
|
|
||||||
errorChan <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resultChan <- result
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HypixelAPI) FetchPlayersAsync(PlayerUUIDs []string, resultChan chan<- HypixelPlayerResponse, errorChan chan<- error) {
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
for _, uuid := range PlayerUUIDs {
|
|
||||||
wg.Add(1)
|
|
||||||
|
|
||||||
go func(uuid string) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
result, err := h.FetchPlayer(uuid)
|
|
||||||
if err != nil {
|
|
||||||
errorChan <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resultChan <- result
|
|
||||||
}(uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(resultChan)
|
|
||||||
close(errorChan)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
type CachedUuid struct {
|
|
||||||
CleanUuid string
|
|
||||||
Uuid string
|
|
||||||
PlayerName string
|
|
||||||
TimeFetched time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type UUIDCache struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
lifetimeLimit time.Duration
|
|
||||||
uuidMap map[string]*CachedUuid
|
|
||||||
cleanUuidMap map[string]*CachedUuid
|
|
||||||
playerNameMap map[string]*CachedUuid
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUUIDCache(lifetime time.Duration) *UUIDCache {
|
|
||||||
return &UUIDCache{
|
|
||||||
lifetimeLimit: lifetime,
|
|
||||||
uuidMap: make(map[string]*CachedUuid),
|
|
||||||
cleanUuidMap: make(map[string]*CachedUuid),
|
|
||||||
playerNameMap: make(map[string]*CachedUuid),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *UUIDCache) Add(cachedUuid *CachedUuid) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
c.uuidMap[cachedUuid.Uuid] = cachedUuid
|
|
||||||
c.cleanUuidMap[cachedUuid.CleanUuid] = cachedUuid
|
|
||||||
c.playerNameMap[cachedUuid.PlayerName] = cachedUuid
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *UUIDCache) GetByUuid(uuid string) (*CachedUuid, bool) {
|
|
||||||
c.mu.RLock()
|
|
||||||
defer c.mu.RUnlock()
|
|
||||||
val, ok := c.uuidMap[uuid]
|
|
||||||
return val, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *UUIDCache) GetByCleanUuid(cleanUuid string) (*CachedUuid, bool) {
|
|
||||||
c.mu.RLock()
|
|
||||||
defer c.mu.RUnlock()
|
|
||||||
val, ok := c.cleanUuidMap[cleanUuid]
|
|
||||||
return val, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *UUIDCache) GetByPlayerName(playerName string) (*CachedUuid, bool) {
|
|
||||||
c.mu.RLock()
|
|
||||||
defer c.mu.RUnlock()
|
|
||||||
val, ok := c.playerNameMap[playerName]
|
|
||||||
return val, ok
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *UUIDCache) Get(id string) (*CachedUuid, bool) {
|
|
||||||
val, ok := c.GetByPlayerName(id)
|
|
||||||
if ok {
|
|
||||||
return val, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
val, ok = c.GetByUuid(id)
|
|
||||||
if ok {
|
|
||||||
return val, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
val, ok = c.GetByCleanUuid(id)
|
|
||||||
return val, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *UUIDCache) Delete(uuid string) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
if cachedUuid, ok := c.uuidMap[uuid]; ok {
|
|
||||||
delete(c.uuidMap, cachedUuid.Uuid)
|
|
||||||
delete(c.cleanUuidMap, cachedUuid.CleanUuid)
|
|
||||||
delete(c.playerNameMap, cachedUuid.PlayerName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *UUIDCache) Clean() {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
for uuid, cachedUuid := range c.uuidMap {
|
|
||||||
if now.Sub(cachedUuid.TimeFetched) > c.lifetimeLimit {
|
|
||||||
delete(c.uuidMap, uuid)
|
|
||||||
delete(c.cleanUuidMap, cachedUuid.CleanUuid)
|
|
||||||
delete(c.playerNameMap, cachedUuid.PlayerName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Property struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
Signature string `json:"signature"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MCPlayer struct {
|
|
||||||
Username string
|
|
||||||
UUID string
|
|
||||||
AvatarURL string
|
|
||||||
SkinTexture string
|
|
||||||
Properties []Property
|
|
||||||
}
|
|
||||||
|
|
||||||
func isValidUUID(id string) bool {
|
|
||||||
r := regexp.MustCompile(`^[a-fA-F0-9]{32}$|^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$`)
|
|
||||||
return r.MatchString(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isValidUsername(name string) bool {
|
|
||||||
r := regexp.MustCompile(`^[a-zA-Z0-9_]{3,16}$`)
|
|
||||||
return r.MatchString(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchFromPlayerDB(id string) (*MCPlayer, error) {
|
|
||||||
url := fmt.Sprintf("https://playerdb.co/api/player/minecraft/%s", id)
|
|
||||||
|
|
||||||
resp, err := http.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var result map[string]interface{}
|
|
||||||
err = json.Unmarshal(body, &result)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if result["success"].(bool) == false {
|
|
||||||
return nil, fmt.Errorf("player not found (playerdb): %s", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
data := result["data"].(map[string]interface{})
|
|
||||||
player := data["player"].(map[string]interface{})
|
|
||||||
properties := player["properties"].([]interface{})
|
|
||||||
|
|
||||||
mcPlayer := &MCPlayer{
|
|
||||||
Username: player["username"].(string),
|
|
||||||
UUID: player["id"].(string),
|
|
||||||
AvatarURL: player["avatar"].(string),
|
|
||||||
SkinTexture: player["skin_texture"].(string),
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range properties {
|
|
||||||
prop := p.(map[string]interface{})
|
|
||||||
mcPlayer.Properties = append(mcPlayer.Properties, Property{
|
|
||||||
Name: prop["name"].(string),
|
|
||||||
Value: prop["value"].(string),
|
|
||||||
Signature: prop["signature"].(string),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return mcPlayer, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func FetchMCPlayer(id string) (*MCPlayer, error) {
|
|
||||||
// TODO: Implement mojang API as the fallback
|
|
||||||
// TODO: Catch Nicks (propagate nick catcher through to HypixelAPI)
|
|
||||||
if isValidUUID(id) {
|
|
||||||
player, err := fetchFromPlayerDB(id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return player, nil
|
|
||||||
} else if isValidUsername(id) {
|
|
||||||
player, err := fetchFromPlayerDB(id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return player, nil
|
|
||||||
} else {
|
|
||||||
return nil, errors.New("invalid input: not a valid UUID or username")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type DisplayTable interface {
|
|
||||||
AddPlayer(player HypixelPlayer)
|
|
||||||
AddPlayerN(player string, app *App)
|
|
||||||
Draw()
|
|
||||||
GetPlayers() []HypixelPlayer
|
|
||||||
}
|
|
||||||
|
|
||||||
type BedwarsDisplayTable struct {
|
|
||||||
players []HypixelPlayer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *BedwarsDisplayTable) GetPlayers() []HypixelPlayer {
|
|
||||||
return b.players
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *BedwarsDisplayTable) AddPlayer(player HypixelPlayer) {
|
|
||||||
b.players = append(b.players, player)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *BedwarsDisplayTable) AddPlayerN(player string, app *App) {
|
|
||||||
cachedPlayer, isAlive := app.UUIDCache.Get(player)
|
|
||||||
|
|
||||||
if isAlive {
|
|
||||||
fetchedPlayer, err := app.API.FetchPlayer(cachedPlayer.Uuid)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error fetching player from Hypixel API:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
b.AddPlayer(fetchedPlayer.Player)
|
|
||||||
} else {
|
|
||||||
playerData, err := FetchMCPlayer(player)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error fetching player data:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
app.UUIDCache.Add(&CachedUuid{
|
|
||||||
CleanUuid: strings.ReplaceAll(playerData.UUID, "-", ""),
|
|
||||||
Uuid: playerData.UUID,
|
|
||||||
PlayerName: playerData.Username,
|
|
||||||
TimeFetched: time.Now(),
|
|
||||||
})
|
|
||||||
|
|
||||||
fetchedPlayer, err := app.API.FetchPlayer(playerData.UUID)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error fetching player from Hypixel API:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
b.AddPlayer(fetchedPlayer.Player)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *BedwarsDisplayTable) Draw() {
|
|
||||||
fmt.Println("=== Bedwars Player Table ===")
|
|
||||||
for _, player := range b.players {
|
|
||||||
fmt.Printf("Player: %s, Score: %d\n", player.DisplayName, player.Achievements.BedwarsStar)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type App struct {
|
|
||||||
LogPath string
|
|
||||||
API *HypixelAPI
|
|
||||||
UUIDCache *UUIDCache
|
|
||||||
CurrentDisplayTable DisplayTable
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewApp(apiKey string) *App {
|
|
||||||
path := os.Getenv("USERPROFILE")
|
|
||||||
logPath := filepath.Join(path, ".lunarclient", "offline", "multiver", "logs", "latest.log")
|
|
||||||
|
|
||||||
return &App{
|
|
||||||
LogPath: logPath,
|
|
||||||
API: NewHypixelAPI(apiKey),
|
|
||||||
UUIDCache: NewUUIDCache(120 * time.Minute),
|
|
||||||
CurrentDisplayTable: &BedwarsDisplayTable{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type LogBuffer struct {
|
|
||||||
strings []string
|
|
||||||
size int
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLogBuffer(size int) *LogBuffer {
|
|
||||||
return &LogBuffer{
|
|
||||||
strings: make([]string, 0, size),
|
|
||||||
size: size,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *LogBuffer) Add(s string) {
|
|
||||||
if len(l.strings) == l.size {
|
|
||||||
l.strings = l.strings[1:]
|
|
||||||
}
|
|
||||||
l.strings = append(l.strings, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *LogBuffer) Get() []string {
|
|
||||||
return l.strings
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *LogBuffer) GetLast() (string, error) {
|
|
||||||
if len(l.strings) == 0 {
|
|
||||||
return "", fmt.Errorf("log buffer is empty")
|
|
||||||
}
|
|
||||||
return l.strings[len(l.strings)-1], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *LogBuffer) GetSecondToLast() (string, error) {
|
|
||||||
if len(l.strings) < 2 {
|
|
||||||
return "", fmt.Errorf("log buffer does not have enough lines")
|
|
||||||
}
|
|
||||||
return l.strings[len(l.strings)-2], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *LogBuffer) GetLineStepsBack(x int) (string, error) {
|
|
||||||
if x < 0 || x >= len(l.strings) {
|
|
||||||
return "", fmt.Errorf("log buffer does not have enough lines to step back %d times", x)
|
|
||||||
}
|
|
||||||
return l.strings[len(l.strings)-1-x], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var LogBuf = NewLogBuffer(10)
|
|
||||||
|
|
||||||
func replaceCorruptedRune(msg string) string {
|
|
||||||
runes := []rune(msg)
|
|
||||||
for i, r := range runes {
|
|
||||||
if r == '<27>' {
|
|
||||||
runes[i] = '§'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return string(runes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) onFileEmit(line string) {
|
|
||||||
msg := strings.TrimSpace(line)
|
|
||||||
if len(msg) < 34 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
submsg := msg[33:]
|
|
||||||
if len(submsg) != 0 {
|
|
||||||
LogBuf.Add(submsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
OnlinePrefix := "[CHAT] ONLINE: "
|
|
||||||
PartyListSeparatorLinePrefix := "[CHAT] -----------------------------------------------------"
|
|
||||||
PartyMemberCountPrefix := "[CHAT] Party Members ("
|
|
||||||
PartyLeaderPrefix := "[CHAT] Party Leader: "
|
|
||||||
PartyListMembersPrefix := "[CHAT] Party Members: "
|
|
||||||
|
|
||||||
if strings.HasPrefix(submsg, OnlinePrefix) { // Online Message
|
|
||||||
newsubmsg := strings.TrimPrefix(submsg, OnlinePrefix)
|
|
||||||
players := strings.Split(newsubmsg, ",")
|
|
||||||
for _, player := range players {
|
|
||||||
playerName := strings.TrimSpace(player)
|
|
||||||
playerUUID, err := FetchMCPlayer(playerName)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error fetching UUID: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("UUID of player %s: %s\n", playerName, playerUUID)
|
|
||||||
cachedPlayer := CachedUuid{playerUUID.UUID, playerName, playerName, time.Now()}
|
|
||||||
a.UUIDCache.Add(&cachedPlayer)
|
|
||||||
|
|
||||||
//names, err := GetNameFromUUID(playerUUID)
|
|
||||||
//if err != nil {
|
|
||||||
// log.Fatalf("Error fetching names from UUID: %v", err)
|
|
||||||
//}
|
|
||||||
//fmt.Printf("Name history for UUID %s: %v\n", playerUUID, names)
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if strings.HasPrefix(submsg, PartyListSeparatorLinePrefix) { // Party List
|
|
||||||
last, _ := LogBuf.GetSecondToLast()
|
|
||||||
// TODO: Check if moderators
|
|
||||||
if !strings.HasPrefix(last, PartyListMembersPrefix) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
PartyMembersMsg := strings.TrimPrefix(last, PartyListMembersPrefix)
|
|
||||||
for _, player := range strings.Split(PartyMembersMsg, ",") {
|
|
||||||
playerName := strings.TrimSpace(strings.TrimSuffix(player, " ?"))
|
|
||||||
if strings.HasPrefix(playerName, "[") {
|
|
||||||
playerName = strings.Split(playerName, " ")[1]
|
|
||||||
}
|
|
||||||
print("Found Player: '")
|
|
||||||
print(playerName)
|
|
||||||
print("'\n")
|
|
||||||
a.CurrentDisplayTable.AddPlayerN(playerName, a)
|
|
||||||
|
|
||||||
//playerName := strings.TrimSpace(player)
|
|
||||||
//playerUUID, err := GetUUIDFromName(playerName)
|
|
||||||
//if err != nil {
|
|
||||||
// log.Fatalf("Error fetching UUID: %v", err)
|
|
||||||
// return
|
|
||||||
//}
|
|
||||||
//fmt.Printf("UUID of player %s: %s\n", playerName, playerUUID)
|
|
||||||
//cachedPlayer := CachedUuid{playerUUID, playerName, playerName, time.Now()}
|
|
||||||
//UuidCache.Add(&cachedPlayer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse Party Leader
|
|
||||||
leaders_msg, err := LogBuf.GetLineStepsBack(2)
|
|
||||||
if err != nil {
|
|
||||||
println("Unable to find party leader message")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
PartyLeaderMsg := strings.TrimPrefix(leaders_msg, PartyLeaderPrefix)
|
|
||||||
playerName := strings.TrimSpace(strings.TrimSuffix(PartyLeaderMsg, " ?"))
|
|
||||||
if strings.HasPrefix(playerName, "[") {
|
|
||||||
playerName = strings.Split(playerName, " ")[1]
|
|
||||||
}
|
|
||||||
print("Found Leader: '")
|
|
||||||
print(playerName)
|
|
||||||
print("'\n")
|
|
||||||
a.CurrentDisplayTable.AddPlayerN(playerName, a)
|
|
||||||
|
|
||||||
// Parse Party Count
|
|
||||||
party_count_msg, err := LogBuf.GetLineStepsBack(4)
|
|
||||||
if err != nil {
|
|
||||||
println("Unable to find party count message")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
PartyCountMsg := strings.TrimPrefix(party_count_msg, PartyMemberCountPrefix)
|
|
||||||
count_str := strings.TrimSuffix(PartyCountMsg, ")")
|
|
||||||
count, err := strconv.Atoi(count_str)
|
|
||||||
if err != nil {
|
|
||||||
println("Unable to parse party count message - Invalid number used")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
print("Expected ")
|
|
||||||
print(count)
|
|
||||||
print(" party members\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
println(submsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) Start() {
|
|
||||||
|
|
||||||
rl.SetConfigFlags(rl.FlagWindowTransparent | rl.FlagWindowUndecorated)
|
|
||||||
screenWidth := 800
|
|
||||||
screenHeight := 600
|
|
||||||
rl.InitWindow(int32(screenWidth), int32(screenHeight), "Raylib in Go")
|
|
||||||
defer rl.CloseWindow()
|
|
||||||
|
|
||||||
windowPosition := rl.NewVector2(500, 200)
|
|
||||||
rl.SetWindowPosition(int(int32(windowPosition.X)), int(int32(windowPosition.Y)))
|
|
||||||
|
|
||||||
var panOffset rl.Vector2
|
|
||||||
var dragWindow bool = false
|
|
||||||
|
|
||||||
const LowFps = 8
|
|
||||||
rl.SetTargetFPS(LowFps)
|
|
||||||
|
|
||||||
lineCh := make(chan string)
|
|
||||||
go tailFile(a.LogPath, lineCh)
|
|
||||||
|
|
||||||
ch := make(chan HypixelPlayerResponse)
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(ch)
|
|
||||||
}()
|
|
||||||
|
|
||||||
sx := 10
|
|
||||||
sy := 10
|
|
||||||
|
|
||||||
for !rl.WindowShouldClose() {
|
|
||||||
mousePosition := rl.GetMousePosition()
|
|
||||||
windowPosition := rl.GetWindowPosition()
|
|
||||||
screenMousePosition := rl.Vector2{
|
|
||||||
X: windowPosition.X + mousePosition.X,
|
|
||||||
Y: windowPosition.Y + mousePosition.Y,
|
|
||||||
}
|
|
||||||
|
|
||||||
if rl.IsMouseButtonPressed(rl.MouseLeftButton) && !dragWindow {
|
|
||||||
rl.SetTargetFPS(int32(rl.GetMonitorRefreshRate(0)))
|
|
||||||
dragWindow = true
|
|
||||||
panOffset.X = screenMousePosition.X - float32(windowPosition.X)
|
|
||||||
panOffset.Y = screenMousePosition.Y - float32(windowPosition.Y)
|
|
||||||
}
|
|
||||||
|
|
||||||
if dragWindow {
|
|
||||||
newWindowPositionX := int(screenMousePosition.X - panOffset.X)
|
|
||||||
newWindowPositionY := int(screenMousePosition.Y - panOffset.Y)
|
|
||||||
|
|
||||||
rl.SetWindowPosition(newWindowPositionX, newWindowPositionY)
|
|
||||||
|
|
||||||
if rl.IsMouseButtonReleased(rl.MouseLeftButton) {
|
|
||||||
dragWindow = false
|
|
||||||
rl.SetTargetFPS(LowFps)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case line := <-lineCh:
|
|
||||||
a.onFileEmit(replaceCorruptedRune(line))
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
rl.BeginDrawing()
|
|
||||||
rl.ClearBackground(rl.Color{0, 0, 0, 100})
|
|
||||||
|
|
||||||
yOffset := sy
|
|
||||||
for _, player := range a.CurrentDisplayTable.GetPlayers() {
|
|
||||||
playerName := player.DisplayName
|
|
||||||
bedwarsLevel := player.Achievements.BedwarsStar
|
|
||||||
bedwarsWins := player.Stats.Bedwars.Wins
|
|
||||||
|
|
||||||
rl.DrawText(fmt.Sprintf("Player: %s", playerName), int32(sx), int32(yOffset), 20, rl.Black)
|
|
||||||
yOffset += 25
|
|
||||||
rl.DrawText(fmt.Sprintf("Bedwars Level: %d", bedwarsLevel), int32(sx), int32(yOffset), 18, rl.DarkGray)
|
|
||||||
yOffset += 25
|
|
||||||
rl.DrawText(fmt.Sprintf("Bedwars Wins: %d", bedwarsWins), int32(sx), int32(yOffset), 18, rl.DarkGray)
|
|
||||||
yOffset += 40
|
|
||||||
}
|
|
||||||
rl.EndDrawing()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func tailFile(path string, lineCh chan<- string) {
|
|
||||||
file, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to open file: %v", err)
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
file.Seek(0, 2)
|
|
||||||
|
|
||||||
reader := bufio.NewReader(file)
|
|
||||||
|
|
||||||
for {
|
|
||||||
line, err := reader.ReadString('\n')
|
|
||||||
if err != nil {
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
lineCh <- line
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const KEY = "5039a811-1b27-4db6-a7f0-c8dd28eeebcd"
|
|
||||||
const UUID = "5328930e-d411-49cb-90ad-4e5c7b27dd86"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
app := NewApp(KEY)
|
|
||||||
app.Start()
|
|
||||||
}
|
|
101
cache.go
Normal file
101
cache.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CachedUuid struct {
|
||||||
|
CleanUuid string
|
||||||
|
Uuid string
|
||||||
|
PlayerName string
|
||||||
|
TimeFetched time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type UUIDCache struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
lifetimeLimit time.Duration
|
||||||
|
uuidMap map[string]*CachedUuid
|
||||||
|
cleanUuidMap map[string]*CachedUuid
|
||||||
|
playerNameMap map[string]*CachedUuid
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUUIDCache(lifetime time.Duration) *UUIDCache {
|
||||||
|
return &UUIDCache{
|
||||||
|
lifetimeLimit: lifetime,
|
||||||
|
uuidMap: make(map[string]*CachedUuid),
|
||||||
|
cleanUuidMap: make(map[string]*CachedUuid),
|
||||||
|
playerNameMap: make(map[string]*CachedUuid),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UUIDCache) Add(cachedUuid *CachedUuid) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.uuidMap[cachedUuid.Uuid] = cachedUuid
|
||||||
|
c.cleanUuidMap[cachedUuid.CleanUuid] = cachedUuid
|
||||||
|
c.playerNameMap[cachedUuid.PlayerName] = cachedUuid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UUIDCache) GetByUuid(uuid string) (*CachedUuid, bool) {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
val, ok := c.uuidMap[uuid]
|
||||||
|
return val, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UUIDCache) GetByCleanUuid(cleanUuid string) (*CachedUuid, bool) {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
val, ok := c.cleanUuidMap[cleanUuid]
|
||||||
|
return val, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UUIDCache) GetByPlayerName(playerName string) (*CachedUuid, bool) {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
val, ok := c.playerNameMap[playerName]
|
||||||
|
return val, ok
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UUIDCache) Get(id string) (*CachedUuid, bool) {
|
||||||
|
val, ok := c.GetByPlayerName(id)
|
||||||
|
if ok {
|
||||||
|
return val, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
val, ok = c.GetByUuid(id)
|
||||||
|
if ok {
|
||||||
|
return val, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
val, ok = c.GetByCleanUuid(id)
|
||||||
|
return val, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UUIDCache) Delete(uuid string) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
if cachedUuid, ok := c.uuidMap[uuid]; ok {
|
||||||
|
delete(c.uuidMap, cachedUuid.Uuid)
|
||||||
|
delete(c.cleanUuidMap, cachedUuid.CleanUuid)
|
||||||
|
delete(c.playerNameMap, cachedUuid.PlayerName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UUIDCache) Clean() {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
for uuid, cachedUuid := range c.uuidMap {
|
||||||
|
if now.Sub(cachedUuid.TimeFetched) > c.lifetimeLimit {
|
||||||
|
delete(c.uuidMap, uuid)
|
||||||
|
delete(c.cleanUuidMap, cachedUuid.CleanUuid)
|
||||||
|
delete(c.playerNameMap, cachedUuid.PlayerName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
66
display.go
Normal file
66
display.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DisplayTable interface {
|
||||||
|
AddPlayer(player HypixelPlayer)
|
||||||
|
AddPlayerN(player string, app *App)
|
||||||
|
Draw()
|
||||||
|
GetPlayers() []HypixelPlayer
|
||||||
|
}
|
||||||
|
|
||||||
|
type BedwarsDisplayTable struct {
|
||||||
|
players []HypixelPlayer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BedwarsDisplayTable) GetPlayers() []HypixelPlayer {
|
||||||
|
return b.players
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BedwarsDisplayTable) AddPlayer(player HypixelPlayer) {
|
||||||
|
b.players = append(b.players, player)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BedwarsDisplayTable) AddPlayerN(player string, app *App) {
|
||||||
|
cachedPlayer, isAlive := app.UUIDCache.Get(player)
|
||||||
|
|
||||||
|
if isAlive {
|
||||||
|
fetchedPlayer, err := app.API.FetchPlayer(cachedPlayer.Uuid)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error fetching player from Hypixel API:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.AddPlayer(fetchedPlayer.Player)
|
||||||
|
} else {
|
||||||
|
playerData, err := FetchMCPlayer(player)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error fetching player data:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UUIDCache.Add(&CachedUuid{
|
||||||
|
CleanUuid: strings.ReplaceAll(playerData.UUID, "-", ""),
|
||||||
|
Uuid: playerData.UUID,
|
||||||
|
PlayerName: playerData.Username,
|
||||||
|
TimeFetched: time.Now(),
|
||||||
|
})
|
||||||
|
|
||||||
|
fetchedPlayer, err := app.API.FetchPlayer(playerData.UUID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error fetching player from Hypixel API:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.AddPlayer(fetchedPlayer.Player)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BedwarsDisplayTable) Draw() {
|
||||||
|
fmt.Println("=== Bedwars Player Table ===")
|
||||||
|
for _, player := range b.players {
|
||||||
|
fmt.Printf("Player: %s, Score: %d\n", player.DisplayName, player.Achievements.BedwarsStar)
|
||||||
|
}
|
||||||
|
}
|
217
hypixel.go
Normal file
217
hypixel.go
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func errorContext(skip int) string {
|
||||||
|
pc, file, line, ok := runtime.Caller(skip)
|
||||||
|
if !ok {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn := runtime.FuncForPC(pc).Name()
|
||||||
|
return fmt.Sprintf("%s:%d %s", file, line, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func APITraceError(format string, args ...interface{}) error {
|
||||||
|
traceInfo := errorContext(2)
|
||||||
|
return fmt.Errorf("[%s] "+format, append([]interface{}{traceInfo}, args...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
type HypixelPlayerAchievements struct {
|
||||||
|
BedwarsStar int `json:"bedwars_level"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HypixelPlayerBedwarsStats struct {
|
||||||
|
Experience int `json:"Experience"`
|
||||||
|
Winstreak int `json:"winstreak"`
|
||||||
|
Wins int `json:"wins_bedwars"`
|
||||||
|
Losses int `json:"losses_bedwars"`
|
||||||
|
GamesPlayed int `json:"games_played_bedwars"`
|
||||||
|
WinLossRatio float32 // wins / gamesplayed
|
||||||
|
|
||||||
|
Kills int `json:"kills_bedwars"`
|
||||||
|
Deaths int `json:"deaths_bedwars"`
|
||||||
|
KillDeathRatio float32 // kills / deaths
|
||||||
|
|
||||||
|
FinalKills int `json:"final_kills_bedwars"`
|
||||||
|
FinalDeaths int `json:"final_deaths_bedwars"`
|
||||||
|
FinalKillDeathRatio float32 // final kills / final deaths
|
||||||
|
|
||||||
|
BedsBroken int `json:"beds_broken_bedwars"`
|
||||||
|
BedsLost int `json:"beds_lost_bedwars"`
|
||||||
|
BedsBrokenLostRatio float32 // beds broken / beds lost
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stats *HypixelPlayerBedwarsStats) CalculateRatios() {
|
||||||
|
// WLR
|
||||||
|
if stats.GamesPlayed > 0 {
|
||||||
|
stats.WinLossRatio = float32(stats.Wins) / float32(stats.GamesPlayed)
|
||||||
|
} else {
|
||||||
|
stats.WinLossRatio = float32(stats.Wins)
|
||||||
|
}
|
||||||
|
|
||||||
|
// KDR
|
||||||
|
if stats.Deaths > 0 {
|
||||||
|
stats.KillDeathRatio = float32(stats.Kills) / float32(stats.Deaths)
|
||||||
|
} else {
|
||||||
|
stats.KillDeathRatio = float32(stats.Kills)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FKDR
|
||||||
|
if stats.FinalDeaths > 0 {
|
||||||
|
stats.FinalKillDeathRatio = float32(stats.FinalKills) / float32(stats.FinalDeaths)
|
||||||
|
} else {
|
||||||
|
stats.FinalKillDeathRatio = float32(stats.FinalKills)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BBLR
|
||||||
|
if stats.BedsLost > 0 {
|
||||||
|
stats.BedsBrokenLostRatio = float32(stats.BedsBroken) / float32(stats.BedsLost)
|
||||||
|
} else {
|
||||||
|
stats.BedsBrokenLostRatio = float32(stats.BedsBroken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stats *HypixelPlayerBedwarsStats) IsWinstreakDisabled() bool {
|
||||||
|
return stats.Winstreak == -1
|
||||||
|
}
|
||||||
|
|
||||||
|
type HypixelPlayerStats struct {
|
||||||
|
Bedwars HypixelPlayerBedwarsStats `json:"Bedwars"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HypixelPlayer struct {
|
||||||
|
DisplayName string `json:"displayname"`
|
||||||
|
Achievements HypixelPlayerAchievements `json:"achievements"`
|
||||||
|
Stats HypixelPlayerStats `json:"stats"`
|
||||||
|
MonthlyRankColor string `json:"monthlyRankColor"`
|
||||||
|
RankPlusColor string `json:"rankPlusColor"`
|
||||||
|
NetworkExp json.Number `json:"networkExp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HypixelPlayerResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Player HypixelPlayer `json:"player"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HypixelAPIKey struct {
|
||||||
|
Value string
|
||||||
|
Uses int
|
||||||
|
RateLimit time.Duration
|
||||||
|
LimitUsesLeft int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHypixelAPIKey(key string) *HypixelAPIKey {
|
||||||
|
return &HypixelAPIKey{
|
||||||
|
Value: key,
|
||||||
|
Uses: 0,
|
||||||
|
RateLimit: time.Millisecond * 1,
|
||||||
|
LimitUsesLeft: -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type HypixelAPI struct {
|
||||||
|
BaseURL string
|
||||||
|
APIKey HypixelAPIKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHypixelAPI(apiKey string) *HypixelAPI {
|
||||||
|
// TODO: Ensure valid key before creating
|
||||||
|
key := NewHypixelAPIKey(apiKey)
|
||||||
|
return &HypixelAPI{
|
||||||
|
BaseURL: "https://api.hypixel.net/v2/",
|
||||||
|
APIKey: *key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HypixelAPI) FetchPlayer(PlayerUUID string) (HypixelPlayerResponse, error) {
|
||||||
|
// TODO: Handle nicked players
|
||||||
|
if len(PlayerUUID) != 32 && len(PlayerUUID) != 36 {
|
||||||
|
return HypixelPlayerResponse{}, fmt.Errorf("invalid Player UUID! please pass in a valid UUID (Not a name!)")
|
||||||
|
}
|
||||||
|
RequestUrl := h.BaseURL + "player?uuid=" + PlayerUUID
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", RequestUrl, nil)
|
||||||
|
if err != nil {
|
||||||
|
return HypixelPlayerResponse{}, APITraceError("failed to create HTTP request: %v", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("API-Key", h.APIKey.Value)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return HypixelPlayerResponse{}, APITraceError("failed to send HTTP request: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return HypixelPlayerResponse{}, APITraceError("received non-200 response: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return HypixelPlayerResponse{}, APITraceError("failed to read response body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var playerResponse HypixelPlayerResponse
|
||||||
|
err = json.Unmarshal(body, &playerResponse)
|
||||||
|
if err != nil {
|
||||||
|
return HypixelPlayerResponse{}, APITraceError("failed to unmarshal JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !playerResponse.Success {
|
||||||
|
return HypixelPlayerResponse{}, APITraceError("API returned unsuccessful response")
|
||||||
|
}
|
||||||
|
|
||||||
|
playerResponse.Player.Stats.Bedwars.CalculateRatios()
|
||||||
|
|
||||||
|
return playerResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HypixelAPI) FetchPlayerAsync(PlayerUUID string, resultChan chan<- HypixelPlayerResponse, errorChan chan<- error) {
|
||||||
|
go func() {
|
||||||
|
result, err := h.FetchPlayer(PlayerUUID)
|
||||||
|
if err != nil {
|
||||||
|
errorChan <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resultChan <- result
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HypixelAPI) FetchPlayersAsync(PlayerUUIDs []string, resultChan chan<- HypixelPlayerResponse, errorChan chan<- error) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
for _, uuid := range PlayerUUIDs {
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
go func(uuid string) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
result, err := h.FetchPlayer(uuid)
|
||||||
|
if err != nil {
|
||||||
|
errorChan <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resultChan <- result
|
||||||
|
}(uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(resultChan)
|
||||||
|
close(errorChan)
|
||||||
|
}()
|
||||||
|
}
|
49
logger.go
Normal file
49
logger.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type LogBuffer struct {
|
||||||
|
strings []string
|
||||||
|
size int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogBuffer(size int) *LogBuffer {
|
||||||
|
return &LogBuffer{
|
||||||
|
strings: make([]string, 0, size),
|
||||||
|
size: size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LogBuffer) Add(s string) {
|
||||||
|
if len(l.strings) == l.size {
|
||||||
|
l.strings = l.strings[1:]
|
||||||
|
}
|
||||||
|
l.strings = append(l.strings, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LogBuffer) Get() []string {
|
||||||
|
return l.strings
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LogBuffer) GetLast() (string, error) {
|
||||||
|
if len(l.strings) == 0 {
|
||||||
|
return "", fmt.Errorf("log buffer is empty")
|
||||||
|
}
|
||||||
|
return l.strings[len(l.strings)-1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LogBuffer) GetSecondToLast() (string, error) {
|
||||||
|
if len(l.strings) < 2 {
|
||||||
|
return "", fmt.Errorf("log buffer does not have enough lines")
|
||||||
|
}
|
||||||
|
return l.strings[len(l.strings)-2], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LogBuffer) GetLineStepsBack(x int) (string, error) {
|
||||||
|
if x < 0 || x >= len(l.strings) {
|
||||||
|
return "", fmt.Errorf("log buffer does not have enough lines to step back %d times", x)
|
||||||
|
}
|
||||||
|
return l.strings[len(l.strings)-1-x], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var LogBuf = NewLogBuffer(10)
|
500
main.go
500
main.go
@ -2,308 +2,12 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
rl "github.com/gen2brain/raylib-go/raylib"
|
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HypixelPlayerAchievements struct {
|
const KEY = "5039a811-1b27-4db6-a7f0-c8dd28eeebcd"
|
||||||
BedwarsStar int `json:"bedwars_level"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HypixelPlayerBedwarsStats struct {
|
|
||||||
Wins int `json:"wins_bedwars"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HypixelPlayerStats struct {
|
|
||||||
Bedwars HypixelPlayerBedwarsStats `json:"Bedwars"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HypixelPlayer struct {
|
|
||||||
Achievements HypixelPlayerAchievements `json:"achievements"`
|
|
||||||
Stats HypixelPlayerStats `json:"stats"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HypixelPlayerResponse struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Player HypixelPlayer `json:"player"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const URL_ = "https://api.hypixel.net/player?key=5039a811-1b27-4db6-a7f0-c8dd28eeebcd&uuid="
|
|
||||||
|
|
||||||
type CachedUuid struct {
|
|
||||||
CleanUuid string
|
|
||||||
Uuid string
|
|
||||||
PlayerName string
|
|
||||||
TimeFetched time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type Cache struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
uuidMap map[string]*CachedUuid
|
|
||||||
cleanUuidMap map[string]*CachedUuid
|
|
||||||
playerNameMap map[string]*CachedUuid
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCache() *Cache {
|
|
||||||
return &Cache{
|
|
||||||
uuidMap: make(map[string]*CachedUuid),
|
|
||||||
cleanUuidMap: make(map[string]*CachedUuid),
|
|
||||||
playerNameMap: make(map[string]*CachedUuid),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cache) Add(cachedUuid *CachedUuid) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
c.uuidMap[cachedUuid.Uuid] = cachedUuid
|
|
||||||
c.cleanUuidMap[cachedUuid.CleanUuid] = cachedUuid
|
|
||||||
c.playerNameMap[cachedUuid.PlayerName] = cachedUuid
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cache) GetByUuid(uuid string) (*CachedUuid, bool) {
|
|
||||||
c.mu.RLock()
|
|
||||||
defer c.mu.RUnlock()
|
|
||||||
val, ok := c.uuidMap[uuid]
|
|
||||||
return val, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cache) GetByCleanUuid(cleanUuid string) (*CachedUuid, bool) {
|
|
||||||
c.mu.RLock()
|
|
||||||
defer c.mu.RUnlock()
|
|
||||||
val, ok := c.cleanUuidMap[cleanUuid]
|
|
||||||
return val, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cache) GetByPlayerName(playerName string) (*CachedUuid, bool) {
|
|
||||||
c.mu.RLock()
|
|
||||||
defer c.mu.RUnlock()
|
|
||||||
val, ok := c.playerNameMap[playerName]
|
|
||||||
return val, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cache) Delete(uuid string) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
if cachedUuid, ok := c.uuidMap[uuid]; ok {
|
|
||||||
delete(c.uuidMap, cachedUuid.Uuid)
|
|
||||||
delete(c.cleanUuidMap, cachedUuid.CleanUuid)
|
|
||||||
delete(c.playerNameMap, cachedUuid.PlayerName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var UuidCache = NewCache()
|
|
||||||
|
|
||||||
type UUIDResponse struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetUUIDFromName(name string) (string, error) {
|
|
||||||
println(fmt.Sprintf("Player Name to search: '%s'", name))
|
|
||||||
url := fmt.Sprintf("https://api.mojang.com/users/profiles/minecraft/%s", name)
|
|
||||||
|
|
||||||
resp, err := http.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return "", fmt.Errorf("UUID error: failed to fetch UUID for name %s, status code: %d", name, resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
var uuidResponse UUIDResponse
|
|
||||||
err = json.Unmarshal(body, &uuidResponse)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return uuidResponse.ID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
//func GetNameFromUUID(uuid string) ([]string, error) {
|
|
||||||
// uuid = strings.ReplaceAll(uuid, "-", "")
|
|
||||||
//
|
|
||||||
// url := fmt.Sprintf("https://api.mojang.com/user/profiles/%s/names", uuid)
|
|
||||||
//
|
|
||||||
// resp, err := http.Get(url)
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
// defer resp.Body.Close()
|
|
||||||
//
|
|
||||||
// if resp.StatusCode != http.StatusOK {
|
|
||||||
// return nil, fmt.Errorf("error: failed to fetch name for UUID %s, status code: %d", uuid, resp.StatusCode)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// body, err := io.ReadAll(resp.Body)
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// var nameHistory []NameHistory
|
|
||||||
// err = json.Unmarshal(body, &nameHistory)
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// var names []string
|
|
||||||
// for _, history := range nameHistory {
|
|
||||||
// names = append(names, history.Name)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return names, nil
|
|
||||||
//}
|
|
||||||
|
|
||||||
func fetchHypixelData(url string, ch chan<- HypixelPlayerResponse, wg *sync.WaitGroup) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
resp, err := http.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Error fetching data:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
log.Printf("Error: failed to fetch data, status code: %d\n", resp.StatusCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Error reading response body:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var playerResponse HypixelPlayerResponse
|
|
||||||
err = json.Unmarshal(body, &playerResponse)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Error unmarshalling JSON:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ch <- playerResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
type StatsDisplayPlayer struct {
|
|
||||||
Name string
|
|
||||||
Player HypixelPlayer
|
|
||||||
}
|
|
||||||
|
|
||||||
type StatsDisplayMode int
|
|
||||||
|
|
||||||
const (
|
|
||||||
BEDWARS StatsDisplayMode = iota
|
|
||||||
)
|
|
||||||
|
|
||||||
type StatsDisplay struct {
|
|
||||||
Players []StatsDisplayPlayer
|
|
||||||
Mode StatsDisplayMode
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStatsDisplay() *StatsDisplay {
|
|
||||||
return &StatsDisplay{
|
|
||||||
Players: make([]StatsDisplayPlayer, 0),
|
|
||||||
Mode: BEDWARS,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *StatsDisplay) AddPlayer(playername string) {
|
|
||||||
playeruuid, err := GetUUIDFromName(playername)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error fetching UUID for player %s: %v\n", playername, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
hypixelApiUrl := URL_ + playeruuid
|
|
||||||
|
|
||||||
ch := make(chan HypixelPlayerResponse)
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
wg.Add(1)
|
|
||||||
go fetchHypixelData(hypixelApiUrl, ch, &wg)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(ch)
|
|
||||||
}()
|
|
||||||
|
|
||||||
for playerResponse := range ch {
|
|
||||||
if playerResponse.Success {
|
|
||||||
fmt.Printf("Player Bedwars Level: %d\n", playerResponse.Player.Achievements.BedwarsStar)
|
|
||||||
fmt.Printf("Player Bedwars Wins: %d\n", playerResponse.Player.Stats.Bedwars.Wins)
|
|
||||||
|
|
||||||
d.Players = append(d.Players, StatsDisplayPlayer{Player: playerResponse.Player, Name: playername})
|
|
||||||
} else {
|
|
||||||
fmt.Println("Failed to fetch player data.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var StatsDisplayApp = NewStatsDisplay()
|
|
||||||
|
|
||||||
type LogBuffer struct {
|
|
||||||
strings []string
|
|
||||||
size int
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLogBuffer(size int) *LogBuffer {
|
|
||||||
return &LogBuffer{
|
|
||||||
strings: make([]string, 0, size),
|
|
||||||
size: size,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *LogBuffer) Add(s string) {
|
|
||||||
if len(l.strings) == l.size {
|
|
||||||
l.strings = l.strings[1:]
|
|
||||||
}
|
|
||||||
l.strings = append(l.strings, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *LogBuffer) Get() []string {
|
|
||||||
return l.strings
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *LogBuffer) GetLast() (string, error) {
|
|
||||||
if len(l.strings) == 0 {
|
|
||||||
return "", fmt.Errorf("log buffer is empty")
|
|
||||||
}
|
|
||||||
return l.strings[len(l.strings)-1], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *LogBuffer) GetSecondToLast() (string, error) {
|
|
||||||
if len(l.strings) < 2 {
|
|
||||||
return "", fmt.Errorf("log buffer does not have enough lines")
|
|
||||||
}
|
|
||||||
return l.strings[len(l.strings)-2], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *LogBuffer) GetLineStepsBack(x int) (string, error) {
|
|
||||||
if x < 0 || x >= len(l.strings) {
|
|
||||||
return "", fmt.Errorf("log buffer does not have enough lines to step back %d times", x)
|
|
||||||
}
|
|
||||||
return l.strings[len(l.strings)-1-x], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var LogBuf = NewLogBuffer(10)
|
|
||||||
|
|
||||||
func replaceCorruptedRune(msg string) string {
|
func replaceCorruptedRune(msg string) string {
|
||||||
runes := []rune(msg)
|
runes := []rune(msg)
|
||||||
@ -315,109 +19,6 @@ func replaceCorruptedRune(msg string) string {
|
|||||||
return string(runes)
|
return string(runes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func onFileEmit(line string) {
|
|
||||||
msg := strings.TrimSpace(line)
|
|
||||||
if len(msg) < 34 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
submsg := msg[33:]
|
|
||||||
if len(submsg) != 0 {
|
|
||||||
LogBuf.Add(submsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
OnlinePrefix := "[CHAT] ONLINE: "
|
|
||||||
PartyListSeparatorLinePrefix := "[CHAT] -----------------------------------------------------"
|
|
||||||
PartyMemberCountPrefix := "[CHAT] Party Members ("
|
|
||||||
PartyLeaderPrefix := "[CHAT] Party Leader: "
|
|
||||||
PartyListMembersPrefix := "[CHAT] Party Members: "
|
|
||||||
|
|
||||||
if strings.HasPrefix(submsg, OnlinePrefix) { // Online Message
|
|
||||||
newsubmsg := strings.TrimPrefix(submsg, OnlinePrefix)
|
|
||||||
players := strings.Split(newsubmsg, ",")
|
|
||||||
for _, player := range players {
|
|
||||||
playerName := strings.TrimSpace(player)
|
|
||||||
playerUUID, err := GetUUIDFromName(playerName)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error fetching UUID: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("UUID of player %s: %s\n", playerName, playerUUID)
|
|
||||||
cachedPlayer := CachedUuid{playerUUID, playerName, playerName, time.Now()}
|
|
||||||
UuidCache.Add(&cachedPlayer)
|
|
||||||
|
|
||||||
//names, err := GetNameFromUUID(playerUUID)
|
|
||||||
//if err != nil {
|
|
||||||
// log.Fatalf("Error fetching names from UUID: %v", err)
|
|
||||||
//}
|
|
||||||
//fmt.Printf("Name history for UUID %s: %v\n", playerUUID, names)
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if strings.HasPrefix(submsg, PartyListSeparatorLinePrefix) { // Party List
|
|
||||||
last, _ := LogBuf.GetSecondToLast()
|
|
||||||
// TODO: Check if moderators
|
|
||||||
if !strings.HasPrefix(last, PartyListMembersPrefix) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
PartyMembersMsg := strings.TrimPrefix(last, PartyListMembersPrefix)
|
|
||||||
for _, player := range strings.Split(PartyMembersMsg, ",") {
|
|
||||||
playerName := strings.TrimSpace(strings.TrimSuffix(player, " ?"))
|
|
||||||
if strings.HasPrefix(playerName, "[") {
|
|
||||||
playerName = strings.Split(playerName, " ")[1]
|
|
||||||
}
|
|
||||||
print("Found Player: '")
|
|
||||||
print(playerName)
|
|
||||||
print("'\n")
|
|
||||||
StatsDisplayApp.AddPlayer(playerName)
|
|
||||||
|
|
||||||
//playerName := strings.TrimSpace(player)
|
|
||||||
//playerUUID, err := GetUUIDFromName(playerName)
|
|
||||||
//if err != nil {
|
|
||||||
// log.Fatalf("Error fetching UUID: %v", err)
|
|
||||||
// return
|
|
||||||
//}
|
|
||||||
//fmt.Printf("UUID of player %s: %s\n", playerName, playerUUID)
|
|
||||||
//cachedPlayer := CachedUuid{playerUUID, playerName, playerName, time.Now()}
|
|
||||||
//UuidCache.Add(&cachedPlayer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse Party Leader
|
|
||||||
leaders_msg, err := LogBuf.GetLineStepsBack(2)
|
|
||||||
if err != nil {
|
|
||||||
println("Unable to find party leader message")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
PartyLeaderMsg := strings.TrimPrefix(leaders_msg, PartyLeaderPrefix)
|
|
||||||
playerName := strings.TrimSpace(strings.TrimSuffix(PartyLeaderMsg, " ?"))
|
|
||||||
if strings.HasPrefix(playerName, "[") {
|
|
||||||
playerName = strings.Split(playerName, " ")[1]
|
|
||||||
}
|
|
||||||
print("Found Leader: '")
|
|
||||||
print(playerName)
|
|
||||||
print("'\n")
|
|
||||||
StatsDisplayApp.AddPlayer(playerName)
|
|
||||||
|
|
||||||
// Parse Party Count
|
|
||||||
party_count_msg, err := LogBuf.GetLineStepsBack(4)
|
|
||||||
if err != nil {
|
|
||||||
println("Unable to find party count message")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
PartyCountMsg := strings.TrimPrefix(party_count_msg, PartyMemberCountPrefix)
|
|
||||||
count_str := strings.TrimSuffix(PartyCountMsg, ")")
|
|
||||||
count, err := strconv.Atoi(count_str)
|
|
||||||
if err != nil {
|
|
||||||
println("Unable to parse party count message - Invalid number used")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
print("Expected ")
|
|
||||||
print(count)
|
|
||||||
print(" party members\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
println(submsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func tailFile(path string, lineCh chan<- string) {
|
func tailFile(path string, lineCh chan<- string) {
|
||||||
file, err := os.Open(path)
|
file, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -439,100 +40,7 @@ func tailFile(path string, lineCh chan<- string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func start_demo() {
|
func main() {
|
||||||
path := os.Getenv("USERPROFILE")
|
app := NewApp(KEY)
|
||||||
path = filepath.Join(path, ".lunarclient", "offline", "multiver", "logs", "latest.log")
|
app.Start()
|
||||||
|
|
||||||
rl.SetConfigFlags(rl.FlagWindowTransparent | rl.FlagWindowUndecorated)
|
|
||||||
screenWidth := 800
|
|
||||||
screenHeight := 600
|
|
||||||
rl.InitWindow(int32(screenWidth), int32(screenHeight), "Raylib in Go")
|
|
||||||
defer rl.CloseWindow()
|
|
||||||
|
|
||||||
windowPosition := rl.NewVector2(500, 200)
|
|
||||||
rl.SetWindowPosition(int(int32(windowPosition.X)), int(int32(windowPosition.Y)))
|
|
||||||
|
|
||||||
var panOffset rl.Vector2
|
|
||||||
var dragWindow bool = false
|
|
||||||
|
|
||||||
const LowFps = 8
|
|
||||||
rl.SetTargetFPS(LowFps)
|
|
||||||
|
|
||||||
lineCh := make(chan string)
|
|
||||||
go tailFile(path, lineCh)
|
|
||||||
|
|
||||||
ch := make(chan HypixelPlayerResponse)
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(ch)
|
|
||||||
}()
|
|
||||||
|
|
||||||
sx := 10
|
|
||||||
sy := 10
|
|
||||||
|
|
||||||
for !rl.WindowShouldClose() {
|
|
||||||
mousePosition := rl.GetMousePosition()
|
|
||||||
windowPosition := rl.GetWindowPosition()
|
|
||||||
screenMousePosition := rl.Vector2{
|
|
||||||
X: windowPosition.X + mousePosition.X,
|
|
||||||
Y: windowPosition.Y + mousePosition.Y,
|
|
||||||
}
|
|
||||||
|
|
||||||
if rl.IsMouseButtonPressed(rl.MouseLeftButton) && !dragWindow {
|
|
||||||
rl.SetTargetFPS(int32(rl.GetMonitorRefreshRate(0)))
|
|
||||||
dragWindow = true
|
|
||||||
panOffset.X = screenMousePosition.X - float32(windowPosition.X)
|
|
||||||
panOffset.Y = screenMousePosition.Y - float32(windowPosition.Y)
|
|
||||||
}
|
|
||||||
|
|
||||||
if dragWindow {
|
|
||||||
newWindowPositionX := int(screenMousePosition.X - panOffset.X)
|
|
||||||
newWindowPositionY := int(screenMousePosition.Y - panOffset.Y)
|
|
||||||
|
|
||||||
rl.SetWindowPosition(newWindowPositionX, newWindowPositionY)
|
|
||||||
|
|
||||||
if rl.IsMouseButtonReleased(rl.MouseLeftButton) {
|
|
||||||
dragWindow = false
|
|
||||||
rl.SetTargetFPS(LowFps)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case line := <-lineCh:
|
|
||||||
onFileEmit(replaceCorruptedRune(line))
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
rl.BeginDrawing()
|
|
||||||
rl.ClearBackground(rl.Color{0, 0, 0, 100})
|
|
||||||
|
|
||||||
yOffset := sy
|
|
||||||
for _, player := range StatsDisplayApp.Players {
|
|
||||||
playerName := player.Name
|
|
||||||
bedwarsLevel := player.Player.Achievements.BedwarsStar
|
|
||||||
bedwarsWins := player.Player.Stats.Bedwars.Wins
|
|
||||||
|
|
||||||
rl.DrawText(fmt.Sprintf("Player: %s", playerName), int32(sx), int32(yOffset), 20, rl.Black)
|
|
||||||
yOffset += 25
|
|
||||||
rl.DrawText(fmt.Sprintf("Bedwars Level: %d", bedwarsLevel), int32(sx), int32(yOffset), 18, rl.DarkGray)
|
|
||||||
yOffset += 25
|
|
||||||
rl.DrawText(fmt.Sprintf("Bedwars Wins: %d", bedwarsWins), int32(sx), int32(yOffset), 18, rl.DarkGray)
|
|
||||||
yOffset += 40
|
|
||||||
}
|
|
||||||
|
|
||||||
rl.EndDrawing()
|
|
||||||
}
|
|
||||||
|
|
||||||
close(lineCh)
|
|
||||||
|
|
||||||
for playerResponse := range ch {
|
|
||||||
if playerResponse.Success {
|
|
||||||
fmt.Printf("Player Bedwars Level: %d\n", playerResponse.Player.Achievements.BedwarsStar)
|
|
||||||
fmt.Printf("Player Bedwars Wins: %d\n", playerResponse.Player.Stats.Bedwars.Wins)
|
|
||||||
} else {
|
|
||||||
fmt.Println("Failed to fetch player data.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
101
mcplayer.go
Normal file
101
mcplayer.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Property struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Signature string `json:"signature"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MCPlayer struct {
|
||||||
|
Username string
|
||||||
|
UUID string
|
||||||
|
AvatarURL string
|
||||||
|
SkinTexture string
|
||||||
|
Properties []Property
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidUUID(id string) bool {
|
||||||
|
r := regexp.MustCompile(`^[a-fA-F0-9]{32}$|^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$`)
|
||||||
|
return r.MatchString(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidUsername(name string) bool {
|
||||||
|
r := regexp.MustCompile(`^[a-zA-Z0-9_]{3,16}$`)
|
||||||
|
return r.MatchString(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchFromPlayerDB(id string) (*MCPlayer, error) {
|
||||||
|
url := fmt.Sprintf("https://playerdb.co/api/player/minecraft/%s", id)
|
||||||
|
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result map[string]interface{}
|
||||||
|
err = json.Unmarshal(body, &result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if result["success"].(bool) == false {
|
||||||
|
return nil, fmt.Errorf("player not found (playerdb): %s", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := result["data"].(map[string]interface{})
|
||||||
|
player := data["player"].(map[string]interface{})
|
||||||
|
properties := player["properties"].([]interface{})
|
||||||
|
|
||||||
|
mcPlayer := &MCPlayer{
|
||||||
|
Username: player["username"].(string),
|
||||||
|
UUID: player["id"].(string),
|
||||||
|
AvatarURL: player["avatar"].(string),
|
||||||
|
SkinTexture: player["skin_texture"].(string),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range properties {
|
||||||
|
prop := p.(map[string]interface{})
|
||||||
|
mcPlayer.Properties = append(mcPlayer.Properties, Property{
|
||||||
|
Name: prop["name"].(string),
|
||||||
|
Value: prop["value"].(string),
|
||||||
|
Signature: prop["signature"].(string),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return mcPlayer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchMCPlayer(id string) (*MCPlayer, error) {
|
||||||
|
// TODO: Implement mojang API as the fallback
|
||||||
|
// TODO: Catch Nicks (propagate nick catcher through to HypixelAPI)
|
||||||
|
if isValidUUID(id) {
|
||||||
|
player, err := fetchFromPlayerDB(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return player, nil
|
||||||
|
} else if isValidUsername(id) {
|
||||||
|
player, err := fetchFromPlayerDB(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return player, nil
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("invalid input: not a valid UUID or username")
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user