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) func replaceCorruptedRune(msg string) string { runes := []rune(msg) for i, r := range runes { if r == '�' { runes[i] = '§' } } 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 { 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 } } 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.") } } }