commit 5fcea16c2cc1504ea290660cfe86018474e072a2 Author: illyum Date: Wed Oct 16 18:28:20 2024 -0600 Initial unworking app, working demo diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/HypixelStuff.iml b/.idea/HypixelStuff.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/HypixelStuff.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..8a99dfa --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/HypixelStuff.7z b/HypixelStuff.7z new file mode 100644 index 0000000..2af89dd Binary files /dev/null and b/HypixelStuff.7z differ diff --git a/HypixelStuff.exe b/HypixelStuff.exe new file mode 100644 index 0000000..2966419 Binary files /dev/null and b/HypixelStuff.exe differ diff --git a/app/app.go b/app/app.go new file mode 100644 index 0000000..836fabe --- /dev/null +++ b/app/app.go @@ -0,0 +1,760 @@ +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) + 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) { + cachedPlayer, isAlive := app.UUIDCache.Get(UUID) + + if isAlive { + fmt.Printf("Cached player: %+v\n", cachedPlayer) + } else { + playerdat, err := FetchMCPlayer(UUID) + if err != nil { + fmt.Println("Error fetching player:", err) + } else { + app.UUIDCache.Add(&CachedUuid{ + CleanUuid: strings.ReplaceAll(playerdat.UUID, "-", ""), + Uuid: playerdat.UUID, + PlayerName: playerdat.Username, + TimeFetched: time.Now(), + }) + + fmt.Printf("Fetched and cached player: %+v\n", playerdat) + } + } +} + +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) + + //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) + + // 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() + + res, err := app.API.FetchPlayer(UUID) + if err != nil { + fmt.Println(err) + return + } + fmt.Printf("%+v\n", res.Player) + app.CurrentDisplayTable.AddPlayer(res.Player) + app.CurrentDisplayTable.Draw() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e159e1d --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module HypixelStuff + +go 1.23.0 + +require github.com/gen2brain/raylib-go/raylib v0.0.0-20241014163942-bf5ef1835077 + +require ( + github.com/ebitengine/purego v0.7.1 // indirect + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect + golang.org/x/sys v0.20.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..72e14cd --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA= +github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= +github.com/gen2brain/raylib-go/raylib v0.0.0-20241014163942-bf5ef1835077 h1:DEJVMa/7rYR4xLwpuSoMGgcaAKXW+64VASWtBWusQNM= +github.com/gen2brain/raylib-go/raylib v0.0.0-20241014163942-bf5ef1835077/go.mod h1:BaY76bZk7nw1/kVOSQObPY1v1iwVE1KHAGMfvI6oK1Q= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/main.go b/main.go new file mode 100644 index 0000000..b53cd3c --- /dev/null +++ b/main.go @@ -0,0 +1,538 @@ +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.") + } + } +} diff --git a/notes.md b/notes.md new file mode 100644 index 0000000..73c0b08 --- /dev/null +++ b/notes.md @@ -0,0 +1,36 @@ +rl.SetWindowState(rl.FlagWindowUndecorated) + + +//[07:41:31] [Client thread/INFO]: [CHAT] {"server":"mini208R","gametype":"HOUSING","mode":"dynamic","map":"Base"} + +https://playerdb.co/api/player/minecraft/illyum +https://playerdb.co/ + + +[CHAT] ONLINE: angryhacks + +### When to look up stats +- on private message / dm +- Party Invite +- Guild Invite +- Friend Invite +- When you invite someone to party +- When party leader/other member invites party +- When party joins (/stream open) +- Duel Request (that specific duel's stats) + +### Trackers +- Tips? +- WDR Reports? + +### Other Stuff To Add +If you screenshot a leaderboard, it will check the screenshots folder and try to detect if a screenshot has a leaderboard in it, then it will load the leaderboard in and check for those people's stats and display them off to the side (daily only since other lb are on api) + + + +## Building: +```bash +set CGO_ENABLED=0 +go build -ldflags="-s -w" +upx --best --lzma HypixelStuff.exe +``` diff --git a/raylib.dll b/raylib.dll new file mode 100644 index 0000000..6424378 Binary files /dev/null and b/raylib.dll differ