Initial unworking app, working demo

This commit is contained in:
illyum 2024-10-16 18:28:20 -06:00
commit 5fcea16c2c
11 changed files with 1378 additions and 0 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@ -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

9
.idea/HypixelStuff.iml generated Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/HypixelStuff.iml" filepath="$PROJECT_DIR$/.idea/HypixelStuff.iml" />
</modules>
</component>
</project>

BIN
HypixelStuff.7z Normal file

Binary file not shown.

BIN
HypixelStuff.exe Normal file

Binary file not shown.

760
app/app.go Normal file
View File

@ -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 == '<27>' {
runes[i] = '§'
}
}
return string(runes)
}
func (a *App) onFileEmit(line string) {
msg := strings.TrimSpace(line)
if len(msg) < 34 {
return
}
submsg := msg[33:]
if len(submsg) != 0 {
LogBuf.Add(submsg)
}
OnlinePrefix := "[CHAT] ONLINE: "
PartyListSeparatorLinePrefix := "[CHAT] -----------------------------------------------------"
PartyMemberCountPrefix := "[CHAT] Party Members ("
PartyLeaderPrefix := "[CHAT] Party Leader: "
PartyListMembersPrefix := "[CHAT] Party Members: "
if strings.HasPrefix(submsg, OnlinePrefix) { // Online Message
newsubmsg := strings.TrimPrefix(submsg, OnlinePrefix)
players := strings.Split(newsubmsg, ",")
for _, player := range players {
playerName := strings.TrimSpace(player)
playerUUID, err := FetchMCPlayer(playerName)
if err != nil {
log.Fatalf("Error fetching UUID: %v", err)
return
}
fmt.Printf("UUID of player %s: %s\n", playerName, playerUUID)
cachedPlayer := CachedUuid{playerUUID.UUID, playerName, playerName, time.Now()}
a.UUIDCache.Add(&cachedPlayer)
//names, err := GetNameFromUUID(playerUUID)
//if err != nil {
// log.Fatalf("Error fetching names from UUID: %v", err)
//}
//fmt.Printf("Name history for UUID %s: %v\n", playerUUID, names)
}
} else if strings.HasPrefix(submsg, PartyListSeparatorLinePrefix) { // Party List
last, _ := LogBuf.GetSecondToLast()
// TODO: Check if moderators
if !strings.HasPrefix(last, PartyListMembersPrefix) {
return
}
PartyMembersMsg := strings.TrimPrefix(last, PartyListMembersPrefix)
for _, player := range strings.Split(PartyMembersMsg, ",") {
playerName := strings.TrimSpace(strings.TrimSuffix(player, " ?"))
if strings.HasPrefix(playerName, "[") {
playerName = strings.Split(playerName, " ")[1]
}
print("Found Player: '")
print(playerName)
print("'\n")
a.CurrentDisplayTable.AddPlayerN(playerName)
//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()
}

11
go.mod Normal file
View File

@ -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
)

8
go.sum Normal file
View File

@ -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=

538
main.go Normal file
View File

@ -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 == '<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.")
}
}
}

36
notes.md Normal file
View File

@ -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
```

BIN
raylib.dll Normal file

Binary file not shown.