This commit is contained in:
illyum 2024-10-22 18:49:14 -06:00
parent b8a6b68888
commit f489e0c57a
31 changed files with 1302 additions and 863 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# IDE Generated Files
.vscode/
.jb/
.cache/
.idea/
# Executables
*.exe

View File

@ -1,9 +0,0 @@
<?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>

2
.idea/modules.xml generated
View File

@ -2,7 +2,7 @@
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/HypixelStuff.iml" filepath="$PROJECT_DIR$/.idea/HypixelStuff.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/hudly.iml" filepath="$PROJECT_DIR$/.idea/hudly.iml" />
</modules>
</component>
</project>

Binary file not shown.

Binary file not shown.

219
app.go
View File

@ -1,219 +0,0 @@
package main
import (
"fmt"
rl "github.com/gen2brain/raylib-go/raylib"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
)
type App struct {
LogPath string
API *HypixelAPI
UUIDCache *UUIDCache
CurrentDisplayTable DisplayTable
}
func NewApp(apiKey string) *App {
path := os.Getenv("USERPROFILE")
logPath := filepath.Join(path, ".lunarclient", "offline", "multiver", "logs", "latest.log")
return &App{
LogPath: logPath,
API: NewHypixelAPI(apiKey),
UUIDCache: NewUUIDCache(120 * time.Minute),
CurrentDisplayTable: &BedwarsDisplayTable{},
}
}
func (a *App) Start() {
rl.SetConfigFlags(rl.FlagWindowTransparent | rl.FlagWindowUndecorated)
screenWidth := 800
screenHeight := 600
rl.InitWindow(int32(screenWidth), int32(screenHeight), "Raylib in Go")
defer rl.CloseWindow()
windowPosition := rl.NewVector2(500, 200)
rl.SetWindowPosition(int(int32(windowPosition.X)), int(int32(windowPosition.Y)))
var panOffset rl.Vector2
var dragWindow bool = false
const LowFps = 8
rl.SetTargetFPS(LowFps)
lineCh := make(chan string)
go tailFile(a.LogPath, lineCh)
ch := make(chan HypixelPlayerResponse)
var wg sync.WaitGroup
go func() {
wg.Wait()
close(ch)
}()
sx := 10
sy := 10
for !rl.WindowShouldClose() {
mousePosition := rl.GetMousePosition()
windowPosition := rl.GetWindowPosition()
screenMousePosition := rl.Vector2{
X: windowPosition.X + mousePosition.X,
Y: windowPosition.Y + mousePosition.Y,
}
if rl.IsMouseButtonPressed(rl.MouseLeftButton) && !dragWindow {
rl.SetTargetFPS(int32(rl.GetMonitorRefreshRate(0)))
dragWindow = true
panOffset.X = screenMousePosition.X - float32(windowPosition.X)
panOffset.Y = screenMousePosition.Y - float32(windowPosition.Y)
}
if dragWindow {
newWindowPositionX := int(screenMousePosition.X - panOffset.X)
newWindowPositionY := int(screenMousePosition.Y - panOffset.Y)
rl.SetWindowPosition(newWindowPositionX, newWindowPositionY)
if rl.IsMouseButtonReleased(rl.MouseLeftButton) {
dragWindow = false
rl.SetTargetFPS(LowFps)
}
}
select {
case line := <-lineCh:
a.onFileEmit(replaceCorruptedRune(line))
default:
}
rl.BeginDrawing()
rl.ClearBackground(rl.Color{0, 0, 0, 100})
yOffset := sy
for _, player := range a.CurrentDisplayTable.GetPlayers() {
playerName := player.DisplayName
bedwarsLevel := player.Achievements.BedwarsStar
bedwarsWins := player.Stats.Bedwars.Wins
rl.DrawText(fmt.Sprintf("Player: %s", playerName), int32(sx), int32(yOffset), 20, rl.Black)
yOffset += 25
rl.DrawText(fmt.Sprintf("Bedwars Level: %d", bedwarsLevel), int32(sx), int32(yOffset), 18, rl.DarkGray)
yOffset += 25
rl.DrawText(fmt.Sprintf("Bedwars Wins: %d", bedwarsWins), int32(sx), int32(yOffset), 18, rl.DarkGray)
yOffset += 40
}
rl.EndDrawing()
}
}
func (a *App) onFileEmit(line string) {
msg := strings.TrimSpace(line)
if len(msg) < 34 {
return
}
submsg := msg[33:]
if len(submsg) != 0 {
LogBuf.Add(submsg)
}
OnlinePrefix := "[CHAT] ONLINE: "
PartyListSeparatorLinePrefix := "[CHAT] -----------------------------------------------------"
PartyMemberCountPrefix := "[CHAT] Party Members ("
PartyLeaderPrefix := "[CHAT] Party Leader: "
PartyListMembersPrefix := "[CHAT] Party Members: "
if strings.HasPrefix(submsg, OnlinePrefix) { // Online Message
newsubmsg := strings.TrimPrefix(submsg, OnlinePrefix)
players := strings.Split(newsubmsg, ",")
for _, player := range players {
playerName := strings.TrimSpace(player)
playerUUID, err := FetchMCPlayer(playerName)
if err != nil {
log.Fatalf("Error fetching UUID: %v", err)
return
}
fmt.Printf("UUID of player %s: %s\n", playerName, playerUUID)
cachedPlayer := CachedUuid{playerUUID.UUID, playerName, playerName, time.Now()}
a.UUIDCache.Add(&cachedPlayer)
//names, err := GetNameFromUUID(playerUUID)
//if err != nil {
// log.Fatalf("Error fetching names from UUID: %v", err)
//}
//fmt.Printf("Name history for UUID %s: %v\n", playerUUID, names)
}
} else if strings.HasPrefix(submsg, PartyListSeparatorLinePrefix) { // Party List
last, _ := LogBuf.GetSecondToLast()
// TODO: Check if moderators
if !strings.HasPrefix(last, PartyListMembersPrefix) {
return
}
PartyMembersMsg := strings.TrimPrefix(last, PartyListMembersPrefix)
for _, player := range strings.Split(PartyMembersMsg, ",") {
playerName := strings.TrimSpace(strings.TrimSuffix(player, " ?"))
if strings.HasPrefix(playerName, "[") {
playerName = strings.Split(playerName, " ")[1]
}
print("Found Player: '")
print(playerName)
print("'\n")
a.CurrentDisplayTable.AddPlayerN(playerName, a)
//playerName := strings.TrimSpace(player)
//playerUUID, err := GetUUIDFromName(playerName)
//if err != nil {
// log.Fatalf("Error fetching UUID: %v", err)
// return
//}
//fmt.Printf("UUID of player %s: %s\n", playerName, playerUUID)
//cachedPlayer := CachedUuid{playerUUID, playerName, playerName, time.Now()}
//UuidCache.Add(&cachedPlayer)
}
// Parse Party Leader
leaders_msg, err := LogBuf.GetLineStepsBack(2)
if err != nil {
println("Unable to find party leader message")
return
}
PartyLeaderMsg := strings.TrimPrefix(leaders_msg, PartyLeaderPrefix)
playerName := strings.TrimSpace(strings.TrimSuffix(PartyLeaderMsg, " ?"))
if strings.HasPrefix(playerName, "[") {
playerName = strings.Split(playerName, " ")[1]
}
print("Found Leader: '")
print(playerName)
print("'\n")
a.CurrentDisplayTable.AddPlayerN(playerName, a)
// Parse Party Count
party_count_msg, err := LogBuf.GetLineStepsBack(4)
if err != nil {
println("Unable to find party count message")
return
}
PartyCountMsg := strings.TrimPrefix(party_count_msg, PartyMemberCountPrefix)
count_str := strings.TrimSuffix(PartyCountMsg, ")")
count, err := strconv.Atoi(count_str)
if err != nil {
println("Unable to parse party count message - Invalid number used")
return
}
print("Expected ")
print(count)
print(" party members\n")
}
println(submsg)
}

1
app/app.go Normal file
View File

@ -0,0 +1 @@
package main

69
app/config/config.go Normal file
View File

@ -0,0 +1,69 @@
package config
import (
"os"
"path"
"runtime"
)
type FileConfig struct {
Path string
Name string
Type string
}
func GetDefaultConfigPath() string {
var filePath string
switch os_name := runtime.GOOS; os_name {
case "windows":
appData := os.Getenv("APPDATA")
filePath = path.Join(appData, "hudly", ".conf")
case "darwin":
homePath := os.Getenv("HOME")
filePath = path.Join(homePath, "Library", "Application Support", "hudly", ".conf")
case "linux":
homePath := os.Getenv("HOME")
filePath = path.Join(homePath, ".config", "hudly", ".conf")
default:
panic("unknown operating system")
}
return filePath
}
type Config struct {
ConfigVersion int
PlayerName string
PlayerUUID string
AltNames []string
AltUUIDs []string
AppFeaturesConfig AppFeaturesConfig
}
type AppFeaturesConfig struct {
CheckGuildChat bool
CheckOfficerChat bool
CheckPartyChat bool
CheckPartyList bool
CheckLobbyMessages bool
CheckQueueMessages bool
CheckInGameMessages bool
}
func GetDefaultConfig() *Config {
return &Config{
ConfigVersion: 1,
PlayerName: "",
PlayerUUID: "",
AltNames: []string{},
AltUUIDs: []string{},
AppFeaturesConfig: AppFeaturesConfig{
CheckGuildChat: true,
CheckOfficerChat: false,
CheckPartyChat: true,
CheckPartyList: true,
CheckLobbyMessages: false,
CheckQueueMessages: true,
CheckInGameMessages: true,
},
}
}

75
app/main.go Normal file
View File

@ -0,0 +1,75 @@
package main
import (
"fmt"
"hudly/hypixel"
"hudly/mcfetch"
"log"
"os"
"time"
)
var key = "9634ea92-80f0-482f-aebd-b082c6ed6f19"
var uuid = "5328930e-d411-49cb-90ad-4e5c7b27dd86"
func demo() {
// Ensure a username is provided as a command-line argument
if len(os.Args) < 2 {
log.Fatal("Please provide a Minecraft username as a command-line argument.")
}
// Get the username from the command-line arguments
username := os.Args[1]
thing := hypixel.NewAPIKey(key)
api := hypixel.NewAPI(*thing)
thing.UsesLeft = 11
// Create a MemoryCache instance
memCache := &mcfetch.MemoryCache{}
memCache.Init()
// Create a channel to receive the result
resultChan := make(chan map[string]interface{})
errorChan := make(chan error)
// Create an AsyncPlayerFetcher for asynchronous data fetching with MemoryCache
asyncFetcher := mcfetch.NewAsyncPlayerFetcher(
username, // Minecraft username or UUID
memCache, // Pass the memory cache instance
2, // Number of retries
2*time.Second, // Retry delay
5*time.Second, // Request timeout
)
// Start asynchronous data fetching
asyncFetcher.FetchPlayerData(resultChan, errorChan)
// Non-blocking code execution (do something else while waiting)
fmt.Println("Fetching data asynchronously...")
var userID string
// Block until we receive data or an error
select {
case data := <-resultChan:
fmt.Printf("Player data: %+v\n", data)
// Check if "uuid" exists and is not nil
if uuid, ok := data["id"].(string); ok {
userID = uuid
} else {
fmt.Println(fmt.Sprintf("%+v", data))
log.Fatal("UUID not found or invalid for player")
}
case err := <-errorChan:
log.Fatal(err)
}
// Use the Hypixel API to get additional player data
res, err := api.GetPlayerResponse(userID)
if err != nil {
panic(err)
}
fmt.Println(fmt.Sprintf("%+v", res))
}

101
cache.go
View File

@ -1,101 +0,0 @@
package main
import (
"sync"
"time"
)
type CachedUuid struct {
CleanUuid string
Uuid string
PlayerName string
TimeFetched time.Time
}
type UUIDCache struct {
mu sync.RWMutex
lifetimeLimit time.Duration
uuidMap map[string]*CachedUuid
cleanUuidMap map[string]*CachedUuid
playerNameMap map[string]*CachedUuid
}
func NewUUIDCache(lifetime time.Duration) *UUIDCache {
return &UUIDCache{
lifetimeLimit: lifetime,
uuidMap: make(map[string]*CachedUuid),
cleanUuidMap: make(map[string]*CachedUuid),
playerNameMap: make(map[string]*CachedUuid),
}
}
func (c *UUIDCache) Add(cachedUuid *CachedUuid) {
c.mu.Lock()
defer c.mu.Unlock()
c.uuidMap[cachedUuid.Uuid] = cachedUuid
c.cleanUuidMap[cachedUuid.CleanUuid] = cachedUuid
c.playerNameMap[cachedUuid.PlayerName] = cachedUuid
}
func (c *UUIDCache) GetByUuid(uuid string) (*CachedUuid, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
val, ok := c.uuidMap[uuid]
return val, ok
}
func (c *UUIDCache) GetByCleanUuid(cleanUuid string) (*CachedUuid, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
val, ok := c.cleanUuidMap[cleanUuid]
return val, ok
}
func (c *UUIDCache) GetByPlayerName(playerName string) (*CachedUuid, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
val, ok := c.playerNameMap[playerName]
return val, ok
}
func (c *UUIDCache) Get(id string) (*CachedUuid, bool) {
val, ok := c.GetByPlayerName(id)
if ok {
return val, ok
}
val, ok = c.GetByUuid(id)
if ok {
return val, ok
}
val, ok = c.GetByCleanUuid(id)
return val, ok
}
func (c *UUIDCache) Delete(uuid string) {
c.mu.Lock()
defer c.mu.Unlock()
if cachedUuid, ok := c.uuidMap[uuid]; ok {
delete(c.uuidMap, cachedUuid.Uuid)
delete(c.cleanUuidMap, cachedUuid.CleanUuid)
delete(c.playerNameMap, cachedUuid.PlayerName)
}
}
func (c *UUIDCache) Clean() {
c.mu.Lock()
defer c.mu.Unlock()
now := time.Now()
for uuid, cachedUuid := range c.uuidMap {
if now.Sub(cachedUuid.TimeFetched) > c.lifetimeLimit {
delete(c.uuidMap, uuid)
delete(c.cleanUuidMap, cachedUuid.CleanUuid)
delete(c.playerNameMap, cachedUuid.PlayerName)
}
}
}

75
client/client.go Normal file
View File

@ -0,0 +1,75 @@
package client
import (
"bufio"
"fmt"
"net"
"os"
"strings"
)
// Connect to the chat server
func connectToServer(address string) (net.Conn, error) {
conn, err := net.Dial("tcp", address)
if err != nil {
return nil, fmt.Errorf("could not connect to server: %v", err)
}
return conn, nil
}
// Read input from the terminal and send it to the server
func readInputAndSend(conn net.Conn) {
reader := bufio.NewReader(os.Stdin)
for {
fmt.Print("> ")
text, _ := reader.ReadString('\n')
text = strings.TrimSpace(text)
// Send the command to the server
_, err := conn.Write([]byte(text + "\n"))
if err != nil {
fmt.Println("Error sending message:", err)
return
}
// If the user types 'quit', exit the program
if text == "quit" {
fmt.Println("Goodbye!")
return
}
}
}
// Listen for incoming messages from the server
func listenForMessages(conn net.Conn) {
reader := bufio.NewReader(conn)
for {
message, err := reader.ReadString('\n')
if err != nil {
fmt.Println("Disconnected from server.")
return
}
fmt.Print(message)
}
}
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: go run client.go <server-address>")
return
}
serverAddress := os.Args[1]
conn, err := connectToServer(serverAddress)
if err != nil {
fmt.Println(err)
return
}
defer conn.Close()
// Start a goroutine to listen for incoming messages from the server
go listenForMessages(conn)
// Read input from the terminal and send it to the server
readInputAndSend(conn)
}

View File

@ -1,66 +0,0 @@
package main
import (
"fmt"
"strings"
"time"
)
type DisplayTable interface {
AddPlayer(player HypixelPlayer)
AddPlayerN(player string, app *App)
Draw()
GetPlayers() []HypixelPlayer
}
type BedwarsDisplayTable struct {
players []HypixelPlayer
}
func (b *BedwarsDisplayTable) GetPlayers() []HypixelPlayer {
return b.players
}
func (b *BedwarsDisplayTable) AddPlayer(player HypixelPlayer) {
b.players = append(b.players, player)
}
func (b *BedwarsDisplayTable) AddPlayerN(player string, app *App) {
cachedPlayer, isAlive := app.UUIDCache.Get(player)
if isAlive {
fetchedPlayer, err := app.API.FetchPlayer(cachedPlayer.Uuid)
if err != nil {
fmt.Println("Error fetching player from Hypixel API:", err)
return
}
b.AddPlayer(fetchedPlayer.Player)
} else {
playerData, err := FetchMCPlayer(player)
if err != nil {
fmt.Println("Error fetching player data:", err)
return
}
app.UUIDCache.Add(&CachedUuid{
CleanUuid: strings.ReplaceAll(playerData.UUID, "-", ""),
Uuid: playerData.UUID,
PlayerName: playerData.Username,
TimeFetched: time.Now(),
})
fetchedPlayer, err := app.API.FetchPlayer(playerData.UUID)
if err != nil {
fmt.Println("Error fetching player from Hypixel API:", err)
return
}
b.AddPlayer(fetchedPlayer.Player)
}
}
func (b *BedwarsDisplayTable) Draw() {
fmt.Println("=== Bedwars Player Table ===")
for _, player := range b.players {
fmt.Printf("Player: %s, Score: %d\n", player.DisplayName, player.Achievements.BedwarsStar)
}
}

12
go.mod
View File

@ -1,11 +1 @@
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
)
module hudly

8
go.sum
View File

@ -1,8 +0,0 @@
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=

View File

@ -1,217 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"runtime"
"sync"
"time"
)
func errorContext(skip int) string {
pc, file, line, ok := runtime.Caller(skip)
if !ok {
return "unknown"
}
fn := runtime.FuncForPC(pc).Name()
return fmt.Sprintf("%s:%d %s", file, line, fn)
}
func APITraceError(format string, args ...interface{}) error {
traceInfo := errorContext(2)
return fmt.Errorf("[%s] "+format, append([]interface{}{traceInfo}, args...)...)
}
type HypixelPlayerAchievements struct {
BedwarsStar int `json:"bedwars_level"`
}
type HypixelPlayerBedwarsStats struct {
Experience int `json:"Experience"`
Winstreak int `json:"winstreak"`
Wins int `json:"wins_bedwars"`
Losses int `json:"losses_bedwars"`
GamesPlayed int `json:"games_played_bedwars"`
WinLossRatio float32 // wins / gamesplayed
Kills int `json:"kills_bedwars"`
Deaths int `json:"deaths_bedwars"`
KillDeathRatio float32 // kills / deaths
FinalKills int `json:"final_kills_bedwars"`
FinalDeaths int `json:"final_deaths_bedwars"`
FinalKillDeathRatio float32 // final kills / final deaths
BedsBroken int `json:"beds_broken_bedwars"`
BedsLost int `json:"beds_lost_bedwars"`
BedsBrokenLostRatio float32 // beds broken / beds lost
}
func (stats *HypixelPlayerBedwarsStats) CalculateRatios() {
// WLR
if stats.GamesPlayed > 0 {
stats.WinLossRatio = float32(stats.Wins) / float32(stats.GamesPlayed)
} else {
stats.WinLossRatio = float32(stats.Wins)
}
// KDR
if stats.Deaths > 0 {
stats.KillDeathRatio = float32(stats.Kills) / float32(stats.Deaths)
} else {
stats.KillDeathRatio = float32(stats.Kills)
}
// FKDR
if stats.FinalDeaths > 0 {
stats.FinalKillDeathRatio = float32(stats.FinalKills) / float32(stats.FinalDeaths)
} else {
stats.FinalKillDeathRatio = float32(stats.FinalKills)
}
// BBLR
if stats.BedsLost > 0 {
stats.BedsBrokenLostRatio = float32(stats.BedsBroken) / float32(stats.BedsLost)
} else {
stats.BedsBrokenLostRatio = float32(stats.BedsBroken)
}
}
func (stats *HypixelPlayerBedwarsStats) IsWinstreakDisabled() bool {
return stats.Winstreak == -1
}
type HypixelPlayerStats struct {
Bedwars HypixelPlayerBedwarsStats `json:"Bedwars"`
}
type HypixelPlayer struct {
DisplayName string `json:"displayname"`
Achievements HypixelPlayerAchievements `json:"achievements"`
Stats HypixelPlayerStats `json:"stats"`
MonthlyRankColor string `json:"monthlyRankColor"`
RankPlusColor string `json:"rankPlusColor"`
NetworkExp json.Number `json:"networkExp"`
}
type HypixelPlayerResponse struct {
Success bool `json:"success"`
Player HypixelPlayer `json:"player"`
}
type HypixelAPIKey struct {
Value string
Uses int
RateLimit time.Duration
LimitUsesLeft int
}
func NewHypixelAPIKey(key string) *HypixelAPIKey {
return &HypixelAPIKey{
Value: key,
Uses: 0,
RateLimit: time.Millisecond * 1,
LimitUsesLeft: -1,
}
}
type HypixelAPI struct {
BaseURL string
APIKey HypixelAPIKey
}
func NewHypixelAPI(apiKey string) *HypixelAPI {
// TODO: Ensure valid key before creating
key := NewHypixelAPIKey(apiKey)
return &HypixelAPI{
BaseURL: "https://api.hypixel.net/v2/",
APIKey: *key,
}
}
func (h *HypixelAPI) FetchPlayer(PlayerUUID string) (HypixelPlayerResponse, error) {
// TODO: Handle nicked players
if len(PlayerUUID) != 32 && len(PlayerUUID) != 36 {
return HypixelPlayerResponse{}, fmt.Errorf("invalid Player UUID! please pass in a valid UUID (Not a name!)")
}
RequestUrl := h.BaseURL + "player?uuid=" + PlayerUUID
client := &http.Client{
Timeout: 10 * time.Second,
}
req, err := http.NewRequest("GET", RequestUrl, nil)
if err != nil {
return HypixelPlayerResponse{}, APITraceError("failed to create HTTP request: %v", err)
}
req.Header.Set("API-Key", h.APIKey.Value)
resp, err := client.Do(req)
if err != nil {
return HypixelPlayerResponse{}, APITraceError("failed to send HTTP request: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return HypixelPlayerResponse{}, APITraceError("received non-200 response: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return HypixelPlayerResponse{}, APITraceError("failed to read response body: %v", err)
}
var playerResponse HypixelPlayerResponse
err = json.Unmarshal(body, &playerResponse)
if err != nil {
return HypixelPlayerResponse{}, APITraceError("failed to unmarshal JSON: %v", err)
}
if !playerResponse.Success {
return HypixelPlayerResponse{}, APITraceError("API returned unsuccessful response")
}
playerResponse.Player.Stats.Bedwars.CalculateRatios()
return playerResponse, nil
}
func (h *HypixelAPI) FetchPlayerAsync(PlayerUUID string, resultChan chan<- HypixelPlayerResponse, errorChan chan<- error) {
go func() {
result, err := h.FetchPlayer(PlayerUUID)
if err != nil {
errorChan <- err
return
}
resultChan <- result
}()
}
func (h *HypixelAPI) FetchPlayersAsync(PlayerUUIDs []string, resultChan chan<- HypixelPlayerResponse, errorChan chan<- error) {
var wg sync.WaitGroup
for _, uuid := range PlayerUUIDs {
wg.Add(1)
go func(uuid string) {
defer wg.Done()
result, err := h.FetchPlayer(uuid)
if err != nil {
errorChan <- err
return
}
resultChan <- result
}(uuid)
}
go func() {
wg.Wait()
close(resultChan)
close(errorChan)
}()
}

106
hypixel/api.go Normal file
View File

@ -0,0 +1,106 @@
package hypixel
import (
"encoding/json"
"io"
"log"
"net/http"
"strconv"
"time"
)
const BASE_URL = "https://api.hypixel.net/v2/"
const PLAYER_ENDPOINT_URL = "player?"
type PlayerResponse struct {
Success bool `json:"success"`
Cause string `json:"cause,omitempty"`
Player Player `json:"player"`
}
type HypixelApi struct {
Key APIKey
}
func NewAPI(apiKey APIKey) *HypixelApi {
return &HypixelApi{
Key: apiKey,
}
}
func (api *HypixelApi) GetPlayer(uuid string) (*Player, error) {
_, err := api.canDoRequest()
if err != nil {
return nil, err
}
_, err = api.GetPlayerResponse(uuid)
if err != nil {
return nil, err
}
return nil, nil
}
func (api *HypixelApi) GetPlayerResponse(uuid string) (*PlayerResponse, error) {
_, err := api.canDoRequest()
if err != nil {
return nil, err
}
url := BASE_URL + PLAYER_ENDPOINT_URL + "uuid=" + uuid
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Add("API-Key", raw_key)
playerResponse, err := api.doPlayerRequest(req)
if !playerResponse.Success {
return nil, NewSomeError(playerResponse.Cause, url)
}
return playerResponse, nil
}
func (api *HypixelApi) canDoRequest() (bool, error) {
return true, nil
if api.Key.UsesLeft < 1 {
return false, NewAPIKeyRateLimitedException(raw_key, api.Key.UsesLeft, "Key throttle")
}
return true, nil
}
func (api *HypixelApi) doPlayerRequest(request *http.Request) (*PlayerResponse, error) {
res, err := http.DefaultClient.Do(request)
if err != nil {
return nil, err
}
usesLeft, err := strconv.Atoi(res.Header.Get("ratelimit-remaining"))
resetStr := res.Header.Get("ratelimit-reset")
resetInt, err := strconv.ParseInt(resetStr, 10, 64)
if err != nil {
log.Fatalf("Error parsing ratelimit-reset: %v", err)
}
resetTime := time.Unix(resetInt, 0)
api.Key.UsesLeft = usesLeft
api.Key.ResetDelay = resetTime.Sub(time.Now())
api.Key.TotalUses++
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
defer res.Body.Close()
var playerResponse PlayerResponse
err = json.Unmarshal(body, &playerResponse)
if err != nil {
return nil, err
}
return &playerResponse, nil
}

73
hypixel/key.go Normal file
View File

@ -0,0 +1,73 @@
package hypixel
import (
"encoding/json"
"io"
"log"
"net/http"
"strconv"
"time"
)
var raw_key = ""
type APIKey struct {
TotalUses int
UsesLeft int
ResetDelay time.Duration
}
func NewAPIKey(key string) *APIKey {
raw_key = key
k := &APIKey{TotalUses: 0, UsesLeft: 0, ResetDelay: 0}
_, err := k.TestKey()
if err != nil {
return k
}
return k
}
func (api *APIKey) TestKey() (bool, error) {
url := BASE_URL + PLAYER_ENDPOINT_URL + "uuid=" + "d245a6e2-349d-405a-b801-48f06d39c9a9" // tqrm
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return false, err
}
req.Header.Add("API-Key", raw_key)
res, err := http.DefaultClient.Do(req)
if err != nil {
return false, err
}
usesLeft, err := strconv.Atoi(res.Header.Get("ratelimit-remaining"))
resetStr := res.Header.Get("ratelimit-reset")
resetInt, err := strconv.ParseInt(resetStr, 10, 64)
if err != nil {
log.Fatalf("Error parsing ratelimit-reset: %v", err)
}
resetTime := time.Unix(resetInt, 0)
api.UsesLeft = usesLeft
api.ResetDelay = resetTime.Sub(time.Now())
api.TotalUses++
body, err := io.ReadAll(res.Body)
if err != nil {
return false, err
}
defer res.Body.Close()
var playerResponse PlayerResponse
err = json.Unmarshal(body, &playerResponse)
if err != nil {
return false, err
}
if playerResponse.Success {
return true, nil
}
return false, NewSomeError(url, playerResponse.Cause)
}

49
hypixel/player.go Normal file
View File

@ -0,0 +1,49 @@
package hypixel
import "encoding/json"
type Player struct {
ID string `json:"id"`
UUID string `json:"uuid"`
PlayerName string `json:"playername"`
DisplayName string `json:"displayname"`
NetworkExp json.Number `json:"networkExp"`
KarmaExp json.Number `json:"karma"`
AchievementPoints json.Number `json:"achievementPoints"`
UserLanguage string `json:"userLanguage"`
RankPlusColor string `json:"rankPlusColor"`
Achievements Achievements `json:"achievements"`
Stats Stats `json:"stats"`
}
type Achievements struct {
BedwarsLevel int `json:"bedwars_level"`
}
type Stats struct {
Bedwars Bedwars `json:"Bedwars"`
}
type Bedwars struct {
Experience json.Number `json:"Experience"`
CurrentWinstreak int `json:"winstreak"` // If this isn't here, they have winstreak disabled (flag)
Wins int `json:"wins_bedwars"`
Losses int `json:"losses_bedwars"`
WLR float64
Kills int `json:"kills_bedwars"`
Deaths int `json:"deaths_bedwars"`
KDR float64
FinalKills int `json:"final_kills_bedwars"`
FinalDeaths int `json:"final_deaths_bedwars"`
FKDR float64
BedsBroken int `json:"beds_broken_bedwars"`
BedsLost int `json:"beds_lost_bedwars"`
BBLR float64
}
// TODO: set defaults (kdr, fkdr, bblr, wlr)

248
hypixel/response_errors.go Normal file
View File

@ -0,0 +1,248 @@
package hypixel
import (
"errors"
"fmt"
"time"
)
// Errors:
// SomeError - Couldn't reach the endpoint (maybe internet issues or blocked from hypxiel? or cloudflare is down, etc)
// APIKeyInvalid - Thrown when the key is invalid
// APIKeyRateLimited - Thrown when you're being rate limited (reached your call limit)
// PlayerNotFoundError - When hypxiel can't find the target player
// MalformedUuid - When hypixel returns a malformed UUID error
// MalformedPlayer - When hypixel returns a malformed player error
// PlayerNotFound - When a target player isn't found (usually by uuid)
// TimeOutError - When the api call takes longer than the specified timeout to return
// JsonUnmartialingError - When custom unmartialing functions throw errors (NOT a wrapper for generic json unmartial error)
// UnknownError - When something that isn't already here happens
// SomeError is thrown when the endpoint cannot be reached (e.g., internet issues, blocked from Hypixel, or Cloudflare is down)
type SomeError struct {
URL string
Cause string
}
// NewSomeError creates a new instance of SomeError
func NewSomeError(url, cause string) *SomeError {
return &SomeError{
URL: url,
Cause: cause,
}
}
// Error returns the error message for SomeError
func (e *SomeError) Error() string {
return fmt.Sprintf("Unable to reach endpoint: URL: '%s', Cause: '%s'", e.URL, e.Cause)
}
// IsSomeError checks if the error is a SomeError
func IsSomeError(err error) bool {
var targetErr *SomeError
return errors.As(err, &targetErr)
}
// APIKeyInvalidException is thrown when the HypixelApi key is invalid
type APIKeyInvalidException struct {
APIKey string
Cause string
}
// NewAPIKeyInvalidException creates a new instance of APIKeyInvalidException
func NewAPIKeyInvalidException(apiKey, cause string) *APIKeyInvalidException {
return &APIKeyInvalidException{
APIKey: apiKey,
Cause: cause,
}
}
// PlayerNotFoundException is thrown when the HypixelApi cannot find the target player
type PlayerNotFoundException struct {
URL string
PlayerID string
Cause string
}
// NewPlayerNotFoundException helper function to create PlayerNotFoundException error
func NewPlayerNotFoundException(url, id, cause string) *PlayerNotFoundException {
return &PlayerNotFoundException{
URL: url,
PlayerID: id,
Cause: cause,
}
}
// Error returns the error message for PlayerNotFoundException
func (e *PlayerNotFoundException) Error() string {
return fmt.Sprintf("Player not found: Player ID: '%s', fetch cause: '%s', fetched at URL: '%s'", e.PlayerID, e.Cause, e.URL)
}
// IsPlayerNotFoundException checks if the error is a PlayerNotFoundException error
func IsPlayerNotFoundException(err error) bool {
var targetErr *PlayerNotFoundException
return errors.As(err, &targetErr)
}
// Error returns the error message for APIKeyInvalidException
func (e *APIKeyInvalidException) Error() string {
return fmt.Sprintf("HypixelApi key invalid: Key: '%s', Cause: '%s'", e.APIKey, e.Cause)
}
// IsAPIKeyInvalidException checks if the error is an APIKeyInvalidException
func IsAPIKeyInvalidException(err error) bool {
var targetErr *APIKeyInvalidException
return errors.As(err, &targetErr)
}
// APIKeyRateLimitedException is thrown when the HypixelApi key has reached its call limit
type APIKeyRateLimitedException struct {
APIKey string
RateLimit int
Cause string
}
// NewAPIKeyRateLimitedException creates a new instance of APIKeyRateLimitedException
func NewAPIKeyRateLimitedException(apiKey string, rateLimit int, cause string) *APIKeyRateLimitedException {
return &APIKeyRateLimitedException{
APIKey: apiKey,
RateLimit: rateLimit,
Cause: cause,
}
}
// Error returns the error message for APIKeyRateLimitedException
func (e *APIKeyRateLimitedException) Error() string {
return fmt.Sprintf("HypixelApi key rate limited: Key: '%s', Rate Limit: '%d', Cause: '%s'", e.APIKey, e.RateLimit, e.Cause)
}
// IsAPIKeyRateLimitedException checks if the error is an APIKeyRateLimitedException
func IsAPIKeyRateLimitedException(err error) bool {
var targetErr *APIKeyRateLimitedException
return errors.As(err, &targetErr)
}
// MalformedUUIDException is thrown when Hypixel returns a malformed UUID error
type MalformedUUIDException struct {
UUID string
Cause string
}
// NewMalformedUUIDException creates a new instance of MalformedUUIDException
func NewMalformedUUIDException(uuid, cause string) *MalformedUUIDException {
return &MalformedUUIDException{
UUID: uuid,
Cause: cause,
}
}
// Error returns the error message for MalformedUUIDException
func (e *MalformedUUIDException) Error() string {
return fmt.Sprintf("Malformed UUID: UUID: '%s', Cause: '%s'", e.UUID, e.Cause)
}
// IsMalformedUUIDException checks if the error is a MalformedUUIDException
func IsMalformedUUIDException(err error) bool {
var targetErr *MalformedUUIDException
return errors.As(err, &targetErr)
}
// MalformedPlayerException is thrown when Hypixel returns a malformed player error
type MalformedPlayerException struct {
PlayerData string
Cause string
}
// NewMalformedPlayerException creates a new instance of MalformedPlayerException
func NewMalformedPlayerException(playerData, cause string) *MalformedPlayerException {
return &MalformedPlayerException{
PlayerData: playerData,
Cause: cause,
}
}
// Error returns the error message for MalformedPlayerException
func (e *MalformedPlayerException) Error() string {
return fmt.Sprintf("Malformed player data: Data: '%s', Cause: '%s'", e.PlayerData, e.Cause)
}
// IsMalformedPlayerException checks if the error is a MalformedPlayerException
func IsMalformedPlayerException(err error) bool {
var targetErr *MalformedPlayerException
return errors.As(err, &targetErr)
}
// TimeOutException is thrown when the HypixelApi call takes longer than the specified timeout to return
type TimeOutException struct {
Duration time.Duration
URL string
Cause string
}
// NewTimeOutException creates a new instance of TimeOutException
func NewTimeOutException(duration time.Duration, url, cause string) *TimeOutException {
return &TimeOutException{
Duration: duration,
URL: url,
Cause: cause,
}
}
// Error returns the error message for TimeOutException
func (e *TimeOutException) Error() string {
return fmt.Sprintf("Request timed out after '%s' when accessing URL '%s': Cause: '%s'", e.Duration, e.URL, e.Cause)
}
// IsTimeOutException checks if the error is a TimeOutException
func IsTimeOutException(err error) bool {
var targetErr *TimeOutException
return errors.As(err, &targetErr)
}
// JSONUnmarshallingException is thrown when custom unmarshalling functions encounter errors
type JSONUnmarshallingException struct {
Data string
Cause string
}
// NewJSONUnmarshallingException creates a new instance of JSONUnmarshallingException
func NewJSONUnmarshallingException(data, cause string) *JSONUnmarshallingException {
return &JSONUnmarshallingException{
Data: data,
Cause: cause,
}
}
// Error returns the error message for JSONUnmarshallingException
func (e *JSONUnmarshallingException) Error() string {
return fmt.Sprintf("JSON unmarshalling error: Data: '%s', Cause: '%s'", e.Data, e.Cause)
}
// IsJSONUnmarshallingException checks if the error is a JSONUnmarshallingException
func IsJSONUnmarshallingException(err error) bool {
var targetErr *JSONUnmarshallingException
return errors.As(err, &targetErr)
}
// UnknownErrorException is thrown when an unspecified error occurs
type UnknownErrorException struct {
Cause string
}
// NewUnknownErrorException creates a new instance of UnknownErrorException
func NewUnknownErrorException(cause string) *UnknownErrorException {
return &UnknownErrorException{
Cause: cause,
}
}
// Error returns the error message for UnknownErrorException
func (e *UnknownErrorException) Error() string {
return fmt.Sprintf("Unknown error occurred: Cause: '%s'", e.Cause)
}
// IsUnknownErrorException checks if the error is an UnknownErrorException
func IsUnknownErrorException(err error) bool {
var targetErr *UnknownErrorException
return errors.As(err, &targetErr)
}

5
hypixel/tst.go Normal file
View File

@ -0,0 +1,5 @@
package hypixel
func DoThing() int {
return 1
}

View File

@ -1,49 +0,0 @@
package main
import "fmt"
type LogBuffer struct {
strings []string
size int
}
func NewLogBuffer(size int) *LogBuffer {
return &LogBuffer{
strings: make([]string, 0, size),
size: size,
}
}
func (l *LogBuffer) Add(s string) {
if len(l.strings) == l.size {
l.strings = l.strings[1:]
}
l.strings = append(l.strings, s)
}
func (l *LogBuffer) Get() []string {
return l.strings
}
func (l *LogBuffer) GetLast() (string, error) {
if len(l.strings) == 0 {
return "", fmt.Errorf("log buffer is empty")
}
return l.strings[len(l.strings)-1], nil
}
func (l *LogBuffer) GetSecondToLast() (string, error) {
if len(l.strings) < 2 {
return "", fmt.Errorf("log buffer does not have enough lines")
}
return l.strings[len(l.strings)-2], nil
}
func (l *LogBuffer) GetLineStepsBack(x int) (string, error) {
if x < 0 || x >= len(l.strings) {
return "", fmt.Errorf("log buffer does not have enough lines to step back %d times", x)
}
return l.strings[len(l.strings)-1-x], nil
}
var LogBuf = NewLogBuffer(10)

46
main.go
View File

@ -1,46 +0,0 @@
package main
import (
"bufio"
"log"
"os"
"time"
)
const KEY = "5039a811-1b27-4db6-a7f0-c8dd28eeebcd"
func replaceCorruptedRune(msg string) string {
runes := []rune(msg)
for i, r := range runes {
if r == '<27>' {
runes[i] = '§'
}
}
return string(runes)
}
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 main() {
app := NewApp(KEY)
app.Start()
}

View File

@ -0,0 +1,67 @@
package mcfetch
import (
"encoding/json"
"errors"
"net/http"
"time"
)
// AsyncPlayerFetcher is responsible for fetching Minecraft player data asynchronously
type AsyncPlayerFetcher struct {
playerName string
retries int
retryDelay time.Duration
timeout time.Duration
cache ICache
}
// NewAsyncPlayerFetcher creates a new AsyncPlayerFetcher with an abstract cache (ICache)
func NewAsyncPlayerFetcher(playerName string, cache ICache, retries int, retryDelay time.Duration, timeout time.Duration) *AsyncPlayerFetcher {
cache.Init()
return &AsyncPlayerFetcher{
playerName: playerName,
retries: retries,
retryDelay: retryDelay,
timeout: timeout,
cache: cache,
}
}
// FetchPlayerData fetches the player data asynchronously
func (pf *AsyncPlayerFetcher) FetchPlayerData(resultChan chan map[string]interface{}, errorChan chan error) {
go func() {
cachedData, found := pf.cache.Get(pf.playerName)
if found {
resultChan <- cachedData
return
}
var data map[string]interface{}
for i := 0; i < pf.retries; i++ {
resp, err := pf.makeRequest(pf.playerName)
if err == nil {
defer resp.Body.Close()
if err := json.NewDecoder(resp.Body).Decode(&data); err == nil {
pf.cache.Set(pf.playerName, data)
pf.cache.Sync()
resultChan <- data
return
}
}
time.Sleep(pf.retryDelay)
}
errorChan <- errors.New("Failed to fetch player data after retries")
}()
}
// makeRequest performs the HTTP request to Mojang API
func (pf *AsyncPlayerFetcher) makeRequest(playerName string) (*http.Response, error) {
client := http.Client{Timeout: pf.timeout}
url := "https://api.mojang.com/users/profiles/minecraft/" + playerName
resp, err := client.Get(url)
if err != nil || resp.StatusCode != http.StatusOK {
return nil, errors.New("Request failed")
}
return resp, nil
}

154
mcfetch/cache.go Normal file
View File

@ -0,0 +1,154 @@
package mcfetch
import (
"encoding/json"
"io"
"log"
"os"
"sync"
)
type ICache interface {
Init()
Load()
Get(key string) (map[string]interface{}, bool)
Set(key string, data map[string]interface{})
Save()
Sync()
Purge()
Clear()
}
// MemoryCache implementation
type MemoryCache struct {
cache map[string]interface{}
mu sync.RWMutex
}
// Init initializes the cache (no-op for MemoryCache)
func (c *MemoryCache) Init() {
c.cache = make(map[string]interface{})
}
// Load loads the cache (no-op for MemoryCache)
func (c *MemoryCache) Load() {}
// Get retrieves an item from the cache
func (c *MemoryCache) Get(key string) (map[string]interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
value, found := c.cache[key]
if !found {
return nil, false
}
return value.(map[string]interface{}), true
}
// Set stores an item in the cache
func (c *MemoryCache) Set(key string, data map[string]interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
c.cache[key] = data
}
// Save saves the cache (no-op for MemoryCache)
func (c *MemoryCache) Save() {}
// Sync syncs the cache (no-op for MemoryCache)
func (c *MemoryCache) Sync() {}
// Purge will be implemented later
func (c *MemoryCache) Purge() {}
// Clear clears the cache
func (c *MemoryCache) Clear() {
c.mu.Lock()
defer c.mu.Unlock()
c.cache = make(map[string]interface{})
}
// JsonFileCache implementation
type JsonFileCache struct {
filename string
cache map[string]interface{}
mu sync.RWMutex
}
// Init initializes the cache
func (c *JsonFileCache) Init() {
c.cache = make(map[string]interface{})
}
// Load loads the cache from a JSON file
func (c *JsonFileCache) Load() {
c.mu.Lock()
defer c.mu.Unlock()
file, err := os.Open(c.filename)
if err != nil {
log.Println("Error opening file:", err)
return
}
defer file.Close()
byteValue, err := io.ReadAll(file)
if err != nil {
log.Println("Error reading file:", err)
return
}
err = json.Unmarshal(byteValue, &c.cache)
if err != nil {
log.Println("Error unmarshalling JSON:", err)
}
}
// Get retrieves an item from the cache
func (c *JsonFileCache) Get(key string) (map[string]interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
value, found := c.cache[key]
if !found {
return nil, false
}
return value.(map[string]interface{}), true
}
// Set stores an item in the cache
func (c *JsonFileCache) Set(key string, data map[string]interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
c.cache[key] = data
}
// Save saves the cache to a JSON file
func (c *JsonFileCache) Save() {
c.mu.Lock()
defer c.mu.Unlock()
byteValue, err := json.MarshalIndent(c.cache, "", " ")
if err != nil {
log.Println("Error marshalling JSON:", err)
return
}
err = os.WriteFile(c.filename, byteValue, 0644)
if err != nil {
log.Println("Error writing file:", err)
}
}
// Sync is the same as Save for the JsonFileCache
func (c *JsonFileCache) Sync() {
c.Save()
}
// Purge will be implemented later
func (c *JsonFileCache) Purge() {}
// Clear clears the cache
func (c *JsonFileCache) Clear() {
c.mu.Lock()
defer c.mu.Unlock()
c.cache = make(map[string]interface{})
}

43
mcfetch/notes.md Normal file
View File

@ -0,0 +1,43 @@
## Example Usage:
```go
package main
import (
"fmt"
"log"
"time"
"mcfetch"
)
func main() {
// Create a channel to receive the result
resultChan := make(chan map[string]interface{})
errorChan := make(chan error)
// Create an AsyncPlayerFetcher for asynchronous data fetching
asyncFetcher := mcfetch.NewAsyncPlayerFetcher(
"Notch", // Minecraft username or UUID
3, // Number of retries
2*time.Second, // Retry delay
5*time.Second, // Request timeout
)
// Start asynchronous data fetching
asyncFetcher.FetchPlayerDataAsync(resultChan, errorChan)
// Non-blocking code execution (do something else while waiting)
fmt.Println("Fetching data asynchronously...")
// Block until we receive data or an error
select {
case data := <-resultChan:
fmt.Printf("Player data: %+v\n", data)
case err := <-errorChan:
log.Fatal(err)
}
}
```

63
mcfetch/player_fetcher.go Normal file
View File

@ -0,0 +1,63 @@
package mcfetch
import (
"encoding/json"
"errors"
"net/http"
"time"
)
// PlayerFetcher is responsible for fetching Minecraft player data synchronously
type PlayerFetcher struct {
playerName string
retries int
retryDelay time.Duration
timeout time.Duration
cache ICache
}
// NewPlayerFetcher creates a new PlayerFetcher with an abstract cache (ICache)
func NewPlayerFetcher(playerName string, cache ICache, retries int, retryDelay time.Duration, timeout time.Duration) *PlayerFetcher {
cache.Init()
return &PlayerFetcher{
playerName: playerName,
retries: retries,
retryDelay: retryDelay,
timeout: timeout,
cache: cache,
}
}
// FetchPlayerData fetches the player data synchronously
func (pf *PlayerFetcher) FetchPlayerData() (map[string]interface{}, error) {
cachedData, found := pf.cache.Get(pf.playerName)
if found {
return cachedData, nil
}
var data map[string]interface{}
for i := 0; i < pf.retries; i++ {
resp, err := pf.makeRequest(pf.playerName)
if err == nil {
defer resp.Body.Close()
if err := json.NewDecoder(resp.Body).Decode(&data); err == nil {
pf.cache.Set(pf.playerName, data)
pf.cache.Sync()
return data, nil
}
}
time.Sleep(pf.retryDelay)
}
return nil, errors.New("Failed to fetch player data after retries")
}
// makeRequest performs the HTTP request to Mojang API
func (pf *PlayerFetcher) makeRequest(playerName string) (*http.Response, error) {
client := http.Client{Timeout: pf.timeout}
url := "https://api.mojang.com/users/profiles/minecraft/" + playerName
resp, err := client.Get(url)
if err != nil || resp.StatusCode != http.StatusOK {
return nil, errors.New("Request failed")
}
return resp, nil
}

36
mcfetch/tools.go Normal file
View File

@ -0,0 +1,36 @@
package mcfetch
import (
"regexp"
"strings"
)
// IsValidUsername checks if the username is valid
func IsValidUsername(username string) bool {
if len(username) < 3 || len(username) > 16 {
return false
}
validChars := "abcdefghijklmnopqrstuvwxyz1234567890_"
for _, char := range strings.ToLower(username) {
if !strings.ContainsRune(validChars, char) {
return false
}
}
return true
}
// IsValidUUID checks if a UUID is valid
func IsValidUUID(uuid string) bool {
matched, _ := regexp.MatchString("^[0-9a-fA-F]{32}$", strings.ReplaceAll(uuid, "-", ""))
return matched
}
// UndashUUID removes dashes from UUIDs
func UndashUUID(uuid string) string {
return strings.ReplaceAll(uuid, "-", "")
}
// DashUUID adds dashes to UUIDs at standard positions
func DashUUID(uuid string) string {
return uuid[:8] + "-" + uuid[8:12] + "-" + uuid[12:16] + "-" + uuid[16:20] + "-" + uuid[20:]
}

View File

@ -1,101 +0,0 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"regexp"
)
type Property struct {
Name string `json:"name"`
Value string `json:"value"`
Signature string `json:"signature"`
}
type MCPlayer struct {
Username string
UUID string
AvatarURL string
SkinTexture string
Properties []Property
}
func isValidUUID(id string) bool {
r := regexp.MustCompile(`^[a-fA-F0-9]{32}$|^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$`)
return r.MatchString(id)
}
func isValidUsername(name string) bool {
r := regexp.MustCompile(`^[a-zA-Z0-9_]{3,16}$`)
return r.MatchString(name)
}
func fetchFromPlayerDB(id string) (*MCPlayer, error) {
url := fmt.Sprintf("https://playerdb.co/api/player/minecraft/%s", id)
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var result map[string]interface{}
err = json.Unmarshal(body, &result)
if err != nil {
return nil, err
}
if result["success"].(bool) == false {
return nil, fmt.Errorf("player not found (playerdb): %s", id)
}
data := result["data"].(map[string]interface{})
player := data["player"].(map[string]interface{})
properties := player["properties"].([]interface{})
mcPlayer := &MCPlayer{
Username: player["username"].(string),
UUID: player["id"].(string),
AvatarURL: player["avatar"].(string),
SkinTexture: player["skin_texture"].(string),
}
for _, p := range properties {
prop := p.(map[string]interface{})
mcPlayer.Properties = append(mcPlayer.Properties, Property{
Name: prop["name"].(string),
Value: prop["value"].(string),
Signature: prop["signature"].(string),
})
}
return mcPlayer, nil
}
func FetchMCPlayer(id string) (*MCPlayer, error) {
// TODO: Implement mojang API as the fallback
// TODO: Catch Nicks (propagate nick catcher through to HypixelAPI)
if isValidUUID(id) {
player, err := fetchFromPlayerDB(id)
if err != nil {
return nil, err
}
return player, nil
} else if isValidUsername(id) {
player, err := fetchFromPlayerDB(id)
if err != nil {
return nil, err
}
return player, nil
} else {
return nil, errors.New("invalid input: not a valid UUID or username")
}
}

View File

@ -1,36 +1,6 @@
rl.SetWindowState(rl.FlagWindowUndecorated)
## Hypixel
The key is stored in a KEYFILE (encrypted)
//[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
```
## Proxy Stuff:
- Player's Pings
- Server TPS

Binary file not shown.

223
server/server.go Normal file
View File

@ -0,0 +1,223 @@
package main
import (
"bufio"
"encoding/json"
"fmt"
"math/rand"
"net"
"strings"
"sync"
"time"
)
type Client struct {
conn net.Conn
username string
room *Room
}
type Room struct {
code string
password string
clients map[*Client]bool
lock sync.Mutex
}
var (
rooms = make(map[string]*Room)
mu sync.Mutex
)
// Helper function to generate a 4-hexadecimal room code
func generateRoomCode() string {
rand.Seed(time.Now().UnixNano())
return fmt.Sprintf("%04x", rand.Intn(0xFFFF))
}
// Handle client connection
func handleClient(client *Client) {
defer client.conn.Close()
reader := bufio.NewReader(client.conn)
for {
line, err := reader.ReadString('\n')
if err != nil {
if client.room != nil {
leaveRoom(client)
}
fmt.Println("Client disconnected:", client.conn.RemoteAddr())
return
}
processCommand(client, strings.TrimSpace(line))
}
}
// Process client commands
func processCommand(client *Client, input string) {
parts := strings.SplitN(input, " ", 2)
if len(parts) < 1 {
return
}
cmd := strings.ToUpper(parts[0])
args := ""
if len(parts) > 1 {
args = parts[1]
}
switch cmd {
case "CREATE":
createRoom(client, args)
case "JOIN":
joinRoom(client, args)
case "LEAVE":
leaveRoom(client)
case "MESSAGE":
sendMessage(client, args)
case "SYNC":
handleSync(client, args)
default:
client.conn.Write([]byte("Unknown command\n"))
}
}
// Create a room with an optional password
func createRoom(client *Client, args string) {
mu.Lock()
defer mu.Unlock()
password := ""
if args != "" {
password = args // Treat all input after CREATE as a password
}
roomCode := generateRoomCode() // Room code is generated automatically
room := &Room{
code: roomCode,
password: password,
clients: make(map[*Client]bool),
}
rooms[roomCode] = room
client.conn.Write([]byte(fmt.Sprintf("Room created with code: %s\n", roomCode)))
}
// Join a room using its 4-hexadecimal code and optional password
func joinRoom(client *Client, args string) {
parts := strings.SplitN(args, " ", 2)
roomCode := parts[0]
password := ""
if len(parts) == 2 {
password = parts[1]
}
mu.Lock()
room, exists := rooms[roomCode]
mu.Unlock()
if !exists {
client.conn.Write([]byte("Room not found\n"))
return
}
if room.password != "" && room.password != password {
client.conn.Write([]byte("Incorrect password\n"))
return
}
room.lock.Lock()
room.clients[client] = true
client.room = room
room.lock.Unlock()
client.conn.Write([]byte(fmt.Sprintf("Joined room: %s\n", roomCode)))
broadcastMessage(client.room, fmt.Sprintf("%s has joined the room\n", client.username))
}
// Leave the current room
func leaveRoom(client *Client) {
if client.room == nil {
client.conn.Write([]byte("You are not in any room\n"))
return
}
client.room.lock.Lock()
delete(client.room.clients, client)
client.room.lock.Unlock()
broadcastMessage(client.room, fmt.Sprintf("%s has left the room\n", client.username))
client.conn.Write([]byte("You have left the room\n"))
client.room = nil
}
// Send a message to all clients in the current room
func sendMessage(client *Client, message string) {
if client.room == nil {
client.conn.Write([]byte("You are not in any room\n"))
return
}
timestamp := time.Now().Format("2006-01-02 15:04:05")
formattedMessage := fmt.Sprintf("[%s] %s: %s\n", timestamp, client.username, message)
broadcastMessage(client.room, formattedMessage)
}
// Broadcast a message to all clients in the room
func broadcastMessage(room *Room, message string) {
room.lock.Lock()
defer room.lock.Unlock()
for client := range room.clients {
client.conn.Write([]byte(message))
}
}
// Handle the SYNC command, expecting a JSON payload
func handleSync(client *Client, payload string) {
var data map[string]interface{}
err := json.Unmarshal([]byte(payload), &data)
if err != nil {
client.conn.Write([]byte("Invalid JSON\n"))
return
}
// You can process the JSON payload here as needed
client.conn.Write([]byte("Sync received\n"))
}
// Start the server
func startServer() {
listener, err := net.Listen("tcp", ":5518")
if err != nil {
fmt.Println("Error starting server:", err)
return
}
defer listener.Close()
fmt.Println("Server started on port 5518")
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("Error accepting connection:", err)
continue
}
go func() {
conn.Write([]byte("Enter your username: "))
username, _ := bufio.NewReader(conn).ReadString('\n')
username = strings.TrimSpace(username)
client := &Client{
conn: conn,
username: username,
}
conn.Write([]byte(fmt.Sprintf("Welcome %s!\n", username)))
handleClient(client)
}()
}
}
func main() {
startServer()
}