539 lines
13 KiB
Go
539 lines
13 KiB
Go
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 == '<27>' {
|
||
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.")
|
||
}
|
||
}
|
||
}
|