From b8a6b688886dcb5ff4fab2d4895a8f03eab452bd Mon Sep 17 00:00:00 2001 From: illyum Date: Wed, 16 Oct 2024 18:45:19 -0600 Subject: [PATCH] Seperate into different files WORKING --- app.go | 219 +++++++++++++++ app/app.go | 762 ---------------------------------------------------- cache.go | 101 +++++++ display.go | 66 +++++ hypixel.go | 217 +++++++++++++++ logger.go | 49 ++++ main.go | 500 +--------------------------------- mcplayer.go | 101 +++++++ 8 files changed, 757 insertions(+), 1258 deletions(-) create mode 100644 app.go delete mode 100644 app/app.go create mode 100644 cache.go create mode 100644 display.go create mode 100644 hypixel.go create mode 100644 logger.go create mode 100644 mcplayer.go diff --git a/app.go b/app.go new file mode 100644 index 0000000..4f10dba --- /dev/null +++ b/app.go @@ -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) +} diff --git a/app/app.go b/app/app.go deleted file mode 100644 index 41b430f..0000000 --- a/app/app.go +++ /dev/null @@ -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 == '�' { - 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() -} diff --git a/cache.go b/cache.go new file mode 100644 index 0000000..9c3c744 --- /dev/null +++ b/cache.go @@ -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) + } + } +} diff --git a/display.go b/display.go new file mode 100644 index 0000000..6fa7722 --- /dev/null +++ b/display.go @@ -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) + } +} diff --git a/hypixel.go b/hypixel.go new file mode 100644 index 0000000..6d1deee --- /dev/null +++ b/hypixel.go @@ -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) + }() +} diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..e5357f8 --- /dev/null +++ b/logger.go @@ -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) diff --git a/main.go b/main.go index b53cd3c..4eaf902 100644 --- a/main.go +++ b/main.go @@ -2,308 +2,12 @@ package main import ( "bufio" - "encoding/json" - "fmt" - rl "github.com/gen2brain/raylib-go/raylib" - "io" "log" - "net/http" "os" - "path/filepath" - "strconv" - "strings" - "sync" "time" ) -type HypixelPlayerAchievements struct { - 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) +const KEY = "5039a811-1b27-4db6-a7f0-c8dd28eeebcd" func replaceCorruptedRune(msg string) string { runes := []rune(msg) @@ -315,109 +19,6 @@ func replaceCorruptedRune(msg string) string { 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) { file, err := os.Open(path) if err != nil { @@ -439,100 +40,7 @@ func tailFile(path string, lineCh chan<- string) { } } -func start_demo() { - path := os.Getenv("USERPROFILE") - path = filepath.Join(path, ".lunarclient", "offline", "multiver", "logs", "latest.log") - - 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.") - } - } +func main() { + app := NewApp(KEY) + app.Start() } diff --git a/mcplayer.go b/mcplayer.go new file mode 100644 index 0000000..3206333 --- /dev/null +++ b/mcplayer.go @@ -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") + } +}