Merge pull request 'Merge demo into main branch' (#2) from dev/demo into master

Reviewed-on: http://localhost:3001/illyum/hudly/pulls/2
This commit is contained in:
illyum 2024-10-30 03:43:53 -06:00
commit bc6c2c5988
15 changed files with 2944 additions and 335 deletions

76
README.md Normal file
View File

@ -0,0 +1,76 @@
//[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:
#### Windows
```bash
set CGO_ENABLED=0
go build -ldflags="-s -w"
upx --best --lzma hudly.exe
```
#### Mac (intel x86_64)
```bash
set GOOS=darwin
set GOARCH=amd64
set CGO_ENABLED=0
go build -ldflags="-s -w"
upx --best --lzma --force-macos hudly
```
NOTE: macOS is currently not supported
#### Mac (apple silicon ARM)
```bash
set GOOS=darwin
set GOARCH=arm64
set CGO_ENABLED=0
go build -ldflags="-s -w"
upx --best --lzma --force-macos hudly
```
NOTE: macOS is currently not supported
# TODOS/Limitation
- (api_key) Incorrect structure (api headers don't exist if key is invalid) so you get the wrong error code
- (build) Requires google's UUID library (too big for my liking)
- (client) No keep-alive implemented
- (client) No room closure detection
- (client) You can't see and send data (sender needs to have 2 clients, 1 to host and 1 to read)
- (client/config) Hard coded ip address / port
- (config) No Config (hard code key)
- (demo) Only in-memory uuid cache
- (demo) Lunar Client ONLY (default log location only)
- (demo) Requires working key to function
- (demo) Windows client sender ONLY (not correct log path locator)
- (demo) does NOT show nicked players (doesn't crash)
- (gui) Just terminal for now
- (hypixel_api) No cache
- (player) Only bedwars stats
- (server) Terrible status messages
- (server/server-config) Hard coded port
- (uuid cache) no lifetime (probably isn't needed but still)

1647
SINGLEFILE.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1 +0,0 @@
package main

8
app/demo.go Normal file
View File

@ -0,0 +1,8 @@
package main
var key = "f6999283-43ba-413e-a04d-32dbde98f423"
func main() {
var demoApp = NewDemoApp(key)
demoApp.Start()
}

50
app/logbuf.go Normal file
View File

@ -0,0 +1,50 @@
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)

60
app/logger.go Normal file
View File

@ -0,0 +1,60 @@
package main
import (
"fmt"
"log"
"os"
"runtime"
"time"
)
var AppLogger = NewCustomLogger()
// CustomLogger wraps the standard logger
type CustomLogger struct {
logger *log.Logger
}
// NewCustomLogger initializes the custom logger
func NewCustomLogger() *CustomLogger {
// Create a logger that writes to stdout with no flags
return &CustomLogger{
logger: log.New(os.Stdout, "", 0), // we will handle formatting manually
}
}
// logFormat retrieves the function name, file, and line number
func logFormat() string {
// Use the runtime.Caller to retrieve caller info
pc, file, line, ok := runtime.Caller(2)
if !ok {
file = "unknown"
line = 0
}
// Get function name
funcName := runtime.FuncForPC(pc).Name()
// Format timestamp
timestamp := time.Now().Format("2006-01-02 15:04:05")
// Return formatted log prefix (timestamp, file, line, and function name)
return fmt.Sprintf("%s - %s:%d - %s: ", timestamp, file, line, funcName)
}
// Info logs informational messages
func (c *CustomLogger) Info(msg string) {
c.logger.Println(logFormat() + "INFO: " + msg)
}
// Error logs error messages
func (c *CustomLogger) Error(msg string) {
c.logger.Println(logFormat() + "ERROR: " + msg)
}
// Example usage of the custom logger
func showcase() {
logger := NewCustomLogger()
logger.Info("This is an info message")
logger.Error("This is an error message")
}

View File

@ -1,75 +1,404 @@
package main
import (
"bufio"
"encoding/json"
"fmt"
"github.com/google/uuid"
netclient "hudly/client"
"hudly/hypixel"
"hudly/mcfetch"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"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.")
type PlayerWrapper struct {
Player hypixel.Player `json:"player"`
}
// Get the username from the command-line arguments
username := os.Args[1]
func replaceCorruptedRune(msg string) string {
runes := []rune(msg)
for i, r := range runes {
if r == '<27>' {
runes[i] = '§'
}
}
return string(runes)
}
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
func clearTerminal() {
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
cmd = exec.Command("cmd", "/c", "cls")
} else {
fmt.Println(fmt.Sprintf("%+v", data))
log.Fatal("UUID not found or invalid for player")
cmd = exec.Command("clear")
}
cmd.Stdout = os.Stdout
cmd.Run()
}
case err := <-errorChan:
log.Fatal(err)
func calcRatio(numerator, denominator int) float64 {
if denominator == 0 {
return float64(numerator)
}
return float64(numerator) / float64(denominator)
}
// Use the Hypixel API to get additional player data
res, err := api.GetPlayerResponse(userID)
type DemoApp struct {
Client *netclient.Client
API *hypixel.HypixelApi
MemCache *mcfetch.MemoryCache
LogBuf *LogBuffer
PartyBuilder []map[string]interface{}
}
func NewDemoApp(key string) *DemoApp {
var api_key = hypixel.NewAPIKey(key)
app := &DemoApp{
API: hypixel.NewAPI(*api_key),
MemCache: &mcfetch.MemoryCache{},
LogBuf: NewLogBuffer(10),
PartyBuilder: []map[string]interface{}{},
}
app.MemCache.Init()
return app
}
func (app *DemoApp) FetchMCPlayer(name string) (*mcfetch.FetchedPlayerResult, error) {
asyncFetcher := mcfetch.NewPlayerFetcher(
name,
app.MemCache,
2,
2*time.Second,
5*time.Second,
)
data, err := asyncFetcher.FetchPlayerData()
if err != nil {
panic(err)
return nil, err
}
fmt.Println(fmt.Sprintf("%+v", res))
return data, nil
}
func (app *DemoApp) onFileEmit(line string) {
msg := strings.TrimSpace(line)
if len(msg) < 34 {
return
}
submsg := msg[33:]
if len(submsg) != 0 {
app.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, ",")
var online []mcfetch.CacheResult
for _, player := range players {
playerName := strings.TrimSpace(player)
plr, err := app.FetchMCPlayer(playerName)
res_name := plr.Name
res_uuid := plr.UUID
if err != nil {
fmt.Println(fmt.Sprintf("Error fetching UUID: %v", err))
continue
}
fmt.Printf("UUID of player %s: %s\n", res_name, res_uuid)
res_player := mcfetch.CacheResult{
UUID: plr.UUID,
Name: plr.Name,
}
online = append(online, res_player)
//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)
}
app.sendPartyList(online)
} else if strings.HasPrefix(submsg, PartyListSeparatorLinePrefix) { // Party List
last, _ := app.LogBuf.GetSecondToLast()
// TODO: Check if moderators
if !strings.HasPrefix(last, PartyListMembersPrefix) {
return
}
PartyMembersMsg := strings.TrimPrefix(last, PartyListMembersPrefix)
var ppl []mcfetch.CacheResult
for _, player := range strings.Split(PartyMembersMsg, ",") {
playerName := strings.TrimSpace(strings.TrimSuffix(player, " ?"))
if strings.HasPrefix(playerName, "[") {
playerName = strings.Split(playerName, " ")[1]
}
plr, err := app.FetchMCPlayer(playerName)
if err != nil {
log.Fatalf("Error fetching Player: %v", err)
continue
}
res_name := plr.Name
res_uuid := plr.UUID
res_player := mcfetch.CacheResult{
UUID: plr.UUID,
Name: plr.Name,
}
fmt.Printf("UUID of player %s: %s\n", res_name, res_uuid)
ppl = append(ppl, res_player)
//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 := app.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]
}
plr, err := app.FetchMCPlayer(playerName)
if err != nil {
log.Fatalf("Error fetching Player: %v", err)
}
res_name := plr.Name
res_uuid := plr.UUID
res_player := mcfetch.CacheResult{
UUID: plr.UUID,
Name: plr.Name,
}
fmt.Printf("UUID of player %s: %s\n", res_name, res_uuid)
ppl = append(ppl, res_player)
// 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")
app.sendPartyList(ppl)
return
}
println(submsg)
}
func (app *DemoApp) 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 (app *DemoApp) sendPartyList(ppl []mcfetch.CacheResult) {
for _, user := range ppl {
res, err := app.API.GetPlayerResponse(user.UUID)
if err != nil {
log.Fatalf("Failed to get player data: %v", err)
}
res.Player.Stats.Bedwars.WLR = calcRatio(res.Player.Stats.Bedwars.Wins, res.Player.Stats.Bedwars.Losses)
res.Player.Stats.Bedwars.KDR = calcRatio(res.Player.Stats.Bedwars.Kills, res.Player.Stats.Bedwars.Deaths)
res.Player.Stats.Bedwars.FKDR = calcRatio(res.Player.Stats.Bedwars.FinalKills, res.Player.Stats.Bedwars.FinalDeaths)
res.Player.Stats.Bedwars.BBLR = calcRatio(res.Player.Stats.Bedwars.BedsBroken, res.Player.Stats.Bedwars.BedsLost)
playerJSON, err := json.Marshal(res)
if err != nil {
log.Fatalf("Failed to marshal player data: %v", err)
}
var playerMap map[string]interface{}
json.Unmarshal(playerJSON, &playerMap)
app.PartyBuilder = append(app.PartyBuilder, playerMap)
message, err := json.Marshal(app.PartyBuilder)
if err != nil {
log.Fatalf("Failed to marshal stuff: %v", err)
}
err = app.Client.SendData(string(message))
if err != nil {
log.Printf("Error sending data: %v", err)
}
fmt.Println("Sent stuff:", app.PartyBuilder)
println("Sending Done!")
}
// Clear buffer so it only sends the current party list, not previous party lists
app.PartyBuilder = []map[string]interface{}{}
}
func (app *DemoApp) Start() {
var cmd string
fmt.Print(" | CREATE\n | JOIN\nEnter Choice:\n>")
fmt.Scanln(&cmd)
if cmd != "CREATE" && cmd != "JOIN" {
fmt.Println("Invalid command.")
return
}
var err error
app.Client, err = netclient.NewClient("chat.itzilly.com", uuid.New().String())
if err != nil {
log.Fatalf("Failed to create client: %v", err)
return
}
if cmd == "CREATE" {
app.CreateRoom()
} else if cmd == "JOIN" {
err := app.JoinRoom()
if err != nil {
return
}
}
fmt.Printf("[DEV] Joined Branches\n")
app.Client.ListenForData()
for {
select {
case data, ok := <-app.Client.DataChannel:
if !ok {
fmt.Println("Data channel closed, exiting...")
return
}
app.HandleData(data)
}
}
fmt.Println("Closing app")
}
func (app *DemoApp) CreateRoom() {
var err error
code, err := app.Client.CreateRoom("")
if err != nil {
log.Fatal(err)
return
}
fmt.Println("Created room:", code)
err = app.Client.JoinRoom(code, "password")
if err != nil {
log.Fatal(err)
return
}
println("Connected to room")
path := os.Getenv("USERPROFILE")
logPath := filepath.Join(path, ".lunarclient", "offline", "multiver", "logs", "latest.log")
fmt.Println("Reading log file from:", logPath)
lineCh := make(chan string)
go app.tailFile(logPath, lineCh)
// TODO: Do this in a different goroutine so that you can still listen to data from your own client
for {
select {
case line := <-lineCh:
app.onFileEmit(replaceCorruptedRune(line))
}
}
}
func (app *DemoApp) JoinRoom() error {
var code string
var password string
fmt.Print("Enter Room Code:\n>")
fmt.Scanln(&code)
fmt.Print("Enter Room Password:\n>")
fmt.Scanln(&password)
err := app.Client.JoinRoom(code, password)
if err != nil {
log.Fatal(err)
return err
}
fmt.Println("Joined room:", code)
return nil
}
func (app *DemoApp) HandleData(data string) {
var playerWrappers []PlayerWrapper
err := json.Unmarshal([]byte(data), &playerWrappers)
if err != nil {
fmt.Println("Error unmarshalling data:", err)
return
}
var players []*hypixel.Player
for _, wrapper := range playerWrappers {
players = append(players, &wrapper.Player)
}
app.DisplayPlayers(players)
}
func (app *DemoApp) DisplayPlayers(players []*hypixel.Player) {
clearTerminal()
fmt.Printf("| %-20s | %-10s | %-10s |\n", "Player Name", "Bedwars Level", "FKDR")
fmt.Println("|----------------------|------------|------------|")
for _, player := range players {
fmt.Printf("| %-20s | %-10d | %10.3f |\n",
player.DisplayName,
player.Achievements.BedwarsLevel,
player.Stats.Bedwars.FKDR)
}
fmt.Println("|----------------------|------------|------------|")
}

View File

@ -1,75 +1,239 @@
package client
import (
"bufio"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"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
const PORT = ":5518"
type PacketID int
const (
CONNECT_REQUEST PacketID = iota
CONNECT_RESPONSE
JOIN_REQUEST
JOIN_RESPONSE
CREATE_REQUEST
CREATE_RESPONSE
READ_DATA
SEND_DATA_REQUEST
SEND_DATA_RESPONSE
LEAVE_ROOM
)
// Client represents a client connection to the server.
type Client struct {
Conn net.Conn
ClientID string
RoomCode string
DataChannel chan string
}
// Read input from the terminal and send it to the server
func readInputAndSend(conn net.Conn) {
reader := bufio.NewReader(os.Stdin)
// NewClient creates a new client and connects to the server.
func NewClient(serverAddr, clientID string) (*Client, error) {
conn, err := net.Dial("tcp", serverAddr+PORT)
if err != nil {
return nil, fmt.Errorf("failed to connect to server: %w", err)
}
return &Client{
Conn: conn,
ClientID: clientID,
DataChannel: make(chan string),
}, nil
}
// CreateRoom creates a new room on the server.
func (c *Client) CreateRoom(password string) (string, error) {
request := CreateRoomRequestPacket{
UserID: c.ClientID,
Password: password,
}
if err := c.sendPacket(CREATE_REQUEST, request); err != nil {
return "", err
}
var response CreateRoomResponsePacket
if err := c.receivePacket(CREATE_RESPONSE, &response); err != nil {
return "", err
}
if !response.Success {
return "", errors.New(response.Reason)
}
c.RoomCode = response.RoomCode
return response.RoomCode, nil
}
// JoinRoom joins an existing room on the server.
func (c *Client) JoinRoom(roomCode, password string) error {
request := JoinRequestPacket{
UserID: c.ClientID,
RoomCode: roomCode,
Password: password,
}
if err := c.sendPacket(JOIN_REQUEST, request); err != nil {
return err
}
var response JoinRequestResponsePacket
if err := c.receivePacket(JOIN_RESPONSE, &response); err != nil {
return err
}
if !response.Success {
return errors.New(response.Reason)
}
c.RoomCode = roomCode
return nil
}
func (c *Client) ListenForData() {
go func() {
for {
fmt.Print("> ")
text, _ := reader.ReadString('\n')
text = strings.TrimSpace(text)
// Send the command to the server
_, err := conn.Write([]byte(text + "\n"))
var dataPacket SendDataRequestPacket
err := c.receivePacket(READ_DATA, &dataPacket)
if err != nil {
fmt.Println("Error sending message:", err)
if err == io.EOF {
fmt.Println("Connection closed by the server")
close(c.DataChannel)
return
}
fmt.Printf("Error receiving data: %v\n", err)
close(c.DataChannel)
return
}
// If the user types 'quit', exit the program
if text == "quit" {
fmt.Println("Goodbye!")
return
}
// Send the received data to the channel
c.DataChannel <- dataPacket.Data
}
}()
}
// Listen for incoming messages from the server
func listenForMessages(conn net.Conn) {
reader := bufio.NewReader(conn)
for {
message, err := reader.ReadString('\n')
// SendData sends a message to all other clients in the room.
func (c *Client) SendData(data string) error {
request := SendDataRequestPacket{
UserID: c.ClientID,
Data: data,
}
if err := c.sendPacket(SEND_DATA_REQUEST, request); err != nil {
return fmt.Errorf("failed to send data packet: %w", err)
}
var response SendDataResponsePacket
if err := c.receivePacket(SEND_DATA_RESPONSE, &response); err != nil {
return fmt.Errorf("failed to receive response for sent data: %w", err)
}
if !response.Success {
return errors.New("server failed to process the data request: " + response.Reason)
}
return nil
}
// LeaveRoom disconnects the client from the current room.
func (c *Client) LeaveRoom() error {
return c.sendPacket(LEAVE_ROOM, nil)
}
// Close closes the connection to the server.
func (c *Client) Close() {
c.Conn.Close()
}
// sendPacket sends a packet with a given ID and data to the server.
func (c *Client) sendPacket(packetID PacketID, data interface{}) error {
packetData, err := json.Marshal(data)
if err != nil {
fmt.Println("Disconnected from server.")
return
}
fmt.Print(message)
return fmt.Errorf("failed to encode packet: %w", err)
}
return writePacket(c.Conn, byte(packetID), packetData)
}
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)
// receivePacket reads a response from the server for a given packet ID.
func (c *Client) receivePacket(expected PacketID, v interface{}) error {
packetID, data, err := readPacket(c.Conn)
if err != nil {
fmt.Println(err)
return
return err
}
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)
if packetID != expected {
return fmt.Errorf("unexpected packet ID: got %v, want %v", packetID, expected)
}
return json.Unmarshal(data, v)
}
// readPacket reads a packet from the connection.
func readPacket(conn net.Conn) (PacketID, []byte, error) {
var length uint32
if err := binary.Read(conn, binary.BigEndian, &length); err != nil {
return 0, nil, err
}
var messageType byte
if err := binary.Read(conn, binary.BigEndian, &messageType); err != nil {
return 0, nil, err
}
data := make([]byte, length-5)
if _, err := io.ReadFull(conn, data); err != nil {
return 0, nil, err
}
return PacketID(messageType), data, nil
}
// writePacket writes a packet to the connection.
func writePacket(conn net.Conn, messageType byte, data []byte) error {
length := uint32(5 + len(data))
if err := binary.Write(conn, binary.BigEndian, length); err != nil {
return err
}
if err := binary.Write(conn, binary.BigEndian, messageType); err != nil {
return err
}
_, err := conn.Write(data)
return err
}
type CreateRoomRequestPacket struct {
UserID string
Password string
}
type CreateRoomResponsePacket struct {
Success bool
Reason string
RoomCode string
}
type JoinRequestPacket struct {
UserID string
RoomCode string
Password string
}
type JoinRequestResponsePacket struct {
Success bool
Reason string
CurrentData string
}
type SendDataRequestPacket struct {
UserID string
Data string
}
type SendDataResponsePacket struct {
Success bool
Reason string
}

View File

@ -1,6 +1,9 @@
package config
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path"
"runtime"
@ -67,3 +70,45 @@ func GetDefaultConfig() *Config {
},
}
}
// SaveConfig saves the given config struct to the file in JSON format
func SaveConfig(config *Config, filePath string) error {
// Convert the config struct to JSON
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
return fmt.Errorf("failed to serialize config: %v", err)
}
// Write the JSON data to a file
err = ioutil.WriteFile(filePath, data, 0644)
if err != nil {
return fmt.Errorf("failed to write config to file: %v", err)
}
return nil
}
// LoadConfig loads the config from the given file path
func LoadConfig(filePath string) (*Config, error) {
// Check if the file exists
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return nil, fmt.Errorf("config file does not exist: %s", filePath)
}
// Read the file content
data, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %v", err)
}
// Create a Config object
config := &Config{}
// Deserialize the JSON data into the Config object
err = json.Unmarshal(data, config)
if err != nil {
return nil, fmt.Errorf("failed to deserialize config: %v", err)
}
return config, nil
}

7
go.mod
View File

@ -1 +1,8 @@
module hudly
go 1.23.0
require (
github.com/google/uuid v1.6.0
)

View File

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

View File

@ -28,24 +28,27 @@ func NewAsyncPlayerFetcher(playerName string, cache ICache, retries int, retryDe
}
}
// FetchPlayerData fetches the player data asynchronously
func (pf *AsyncPlayerFetcher) FetchPlayerData(resultChan chan map[string]interface{}, errorChan chan error) {
// FetchPlayerData fetches the player data asynchronously using channels
func (pf *AsyncPlayerFetcher) FetchPlayerData(resultChan chan *FetchedPlayerResult, errorChan chan error) {
go func() {
cachedData, found := pf.cache.Get(pf.playerName)
if found {
resultChan <- cachedData
resultChan <- (*FetchedPlayerResult)(cachedData)
return
}
var data map[string]interface{}
// If not in cache, make request to Mojang API
var player FetchedPlayerResult
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)
// Decode the response into FetchedPlayerResult
if err := json.NewDecoder(resp.Body).Decode(&player); err == nil {
// Store the result in the cache and return the data
pf.cache.Set(pf.playerName, (*CacheResult)(&player))
pf.cache.Sync()
resultChan <- data
resultChan <- &player
return
}
}

View File

@ -8,11 +8,16 @@ import (
"sync"
)
type CacheResult struct {
UUID string `json:"id"`
Name string `json:"name"`
}
type ICache interface {
Init()
Load()
Get(key string) (map[string]interface{}, bool)
Set(key string, data map[string]interface{})
Get(key string) (*CacheResult, bool)
Set(key string, data *CacheResult)
Save()
Sync()
Purge()
@ -21,31 +26,31 @@ type ICache interface {
// MemoryCache implementation
type MemoryCache struct {
cache map[string]interface{}
cache map[string]*CacheResult
mu sync.RWMutex
}
// Init initializes the cache (no-op for MemoryCache)
func (c *MemoryCache) Init() {
c.cache = make(map[string]interface{})
c.cache = make(map[string]*CacheResult)
}
// 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) {
func (c *MemoryCache) Get(key string) (*CacheResult, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
value, found := c.cache[key]
if !found {
return nil, false
}
return value.(map[string]interface{}), true
return value, true
}
// Set stores an item in the cache
func (c *MemoryCache) Set(key string, data map[string]interface{}) {
func (c *MemoryCache) Set(key string, data *CacheResult) {
c.mu.Lock()
defer c.mu.Unlock()
c.cache[key] = data
@ -64,19 +69,19 @@ func (c *MemoryCache) Purge() {}
func (c *MemoryCache) Clear() {
c.mu.Lock()
defer c.mu.Unlock()
c.cache = make(map[string]interface{})
c.cache = make(map[string]*CacheResult)
}
// JsonFileCache implementation
type JsonFileCache struct {
filename string
cache map[string]interface{}
cache map[CacheResult]interface{}
mu sync.RWMutex
}
// Init initializes the cache
func (c *JsonFileCache) Init() {
c.cache = make(map[string]interface{})
c.cache = make(map[CacheResult]interface{})
}
// Load loads the cache from a JSON file
@ -104,21 +109,22 @@ func (c *JsonFileCache) Load() {
}
// 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 {
func (c *JsonFileCache) Get(key string) (*CacheResult, bool) {
//c.mu.RLock()
//defer c.mu.RUnlock()
//value, found := c.cache[key]
//if !found {
// return nil, false
//}
//return value, true
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
func (c *JsonFileCache) Set(key string, data *CacheResult) {
//c.mu.Lock()
//defer c.mu.Unlock()
//c.cache[key] = data
}
// Save saves the cache to a JSON file
@ -150,5 +156,5 @@ func (c *JsonFileCache) Purge() {}
func (c *JsonFileCache) Clear() {
c.mu.Lock()
defer c.mu.Unlock()
c.cache = make(map[string]interface{})
c.cache = make(map[CacheResult]interface{})
}

View File

@ -16,6 +16,11 @@ type PlayerFetcher struct {
cache ICache
}
type FetchedPlayerResult struct {
UUID string `json:"id"`
Name string `json:"name"`
}
// 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()
@ -29,21 +34,21 @@ func NewPlayerFetcher(playerName string, cache ICache, retries int, retryDelay t
}
// 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
}
func (pf *PlayerFetcher) FetchPlayerData() (*FetchedPlayerResult, error) {
//cachedData, found := pf.cache.Get(pf.playerName)
//if found {
// return &FetchedPlayerResult{}, nil
//}
var data map[string]interface{}
var player FetchedPlayerResult
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
if err := json.NewDecoder(resp.Body).Decode(&player); err == nil {
// pf.cache.Set(pf.playerName, player)
// pf.cache.Sync()
return &player, nil
}
}
time.Sleep(pf.retryDelay)

View File

@ -1,201 +1,99 @@
package main
import (
"bufio"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"log"
"math/rand"
"net"
"strings"
"sync"
"time"
)
const PORT = ":5518"
type PacketID int
const (
CONNECT_REQUEST PacketID = iota
CONNECT_RESPONSE
JOIN_REQUEST
JOIN_RESPONSE
CREATE_REQUEST
CREATE_RESPONSE
READ_DATA
SEND_DATA_REQUEST
SEND_DATA_RESPONSE
LEAVE_ROOM
)
type Client struct {
conn net.Conn
username string
room *Room
Conn net.Conn
ClientID string
RoomCode string
}
type Room struct {
code string
password string
clients map[*Client]bool
lock sync.Mutex
Code string
Password string
Clients []*Client
}
var (
rooms = make(map[string]*Room)
mu sync.Mutex
)
var rooms = map[string]*Room{}
var clients = map[string]*Client{}
var 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))
func readPacket(conn net.Conn) (PacketID, []byte, error) {
// First, read the length of the packet (4 bytes)
var length uint32
if err := binary.Read(conn, binary.BigEndian, &length); err != nil {
return 0, nil, err
}
// 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))
}
// Then read the packet ID (1 byte)
var messageType byte
if err := binary.Read(conn, binary.BigEndian, &messageType); err != nil {
return 0, nil, err
}
// Process client commands
func processCommand(client *Client, input string) {
parts := strings.SplitN(input, " ", 2)
if len(parts) < 1 {
return
// Read the remaining data (length - 5 bytes, since 4 bytes for length and 1 byte for messageType)
data := make([]byte, length-5)
if _, err := io.ReadFull(conn, data); err != nil {
return 0, nil, err
}
cmd := strings.ToUpper(parts[0])
args := ""
if len(parts) > 1 {
args = parts[1]
return PacketID(int(messageType)), data, nil
}
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"))
func writePacket(conn net.Conn, messageType byte, data []byte) error {
// Calculate the total length of the packet
// 4 bytes for length, 1 byte for messageType
length := uint32(5 + len(data))
// Write the length and the message type
if err := binary.Write(conn, binary.BigEndian, length); err != nil {
return err
}
if err := binary.Write(conn, binary.BigEndian, messageType); err != nil {
return err
}
// 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
// Write the data
_, err := conn.Write(data)
return err
}
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")
func main() {
listener, err := net.Listen("tcp", PORT)
if err != nil {
fmt.Println("Error starting server:", err)
return
}
defer listener.Close()
fmt.Println("Server started on port 5518")
fmt.Println("Server started on port", PORT)
for {
conn, err := listener.Accept()
@ -203,21 +101,338 @@ func startServer() {
fmt.Println("Error accepting connection:", err)
continue
}
go handleClient(conn)
}
}
go func() {
conn.Write([]byte("Enter your username: "))
username, _ := bufio.NewReader(conn).ReadString('\n')
username = strings.TrimSpace(username)
type ConnectionRequestPacket struct {
UserID string
RoomCode string
Password string
}
type ConnectionResponsePacket struct {
Success bool
Reason string
}
type CreateRoomRequestPacket struct {
UserID string
Password string
}
type CreateRoomResponsePacket struct {
Success bool
Reason string
RoomCode string
}
type JoinRequestPacket struct {
UserID string
Password string
RoomCode string
}
type JoinRequestResponsePacket struct {
Success bool
Reason string
CurrentData string
}
type SendDataRequestPacket struct {
UserID string
Data string
}
type ForwardDataPacket struct {
Data string
}
type SendDataResponsePacket struct {
Success bool
Reason string
}
func handleClient(conn net.Conn) {
// defer conn.Close()
for {
var packetId PacketID
packetId, data, err := readPacket(conn)
if err != nil {
fmt.Println("Error reading packet (handleclient) :", err)
handleLeaveRoom(conn)
return
}
switch packetId {
case CONNECT_REQUEST:
var packet ConnectionRequestPacket
if err := json.Unmarshal(data, &packet); err != nil {
fmt.Println("Error decoding connection request:", err)
return
}
handleConnectRequest(conn, packet)
case CREATE_REQUEST:
var packet CreateRoomRequestPacket
if err := json.Unmarshal(data, &packet); err != nil {
fmt.Println("Error decoding connection request:", err)
return
}
handleCreateRoomRequest(conn, packet)
case JOIN_REQUEST:
var packet JoinRequestPacket
if err := json.Unmarshal(data, &packet); err != nil {
fmt.Println("Error decoding connection request:", err)
return
}
handleJoinRequest(conn, packet)
case SEND_DATA_REQUEST:
var packet SendDataRequestPacket
if err := json.Unmarshal(data, &packet); err != nil {
fmt.Println("Error decoding connection request:", err)
return
}
handleSendData(conn, packet)
case LEAVE_ROOM:
handleLeaveRoom(conn)
}
}
}
func handleConnectRequest(conn net.Conn, packet ConnectionRequestPacket) {
fmt.Println("Incoming connection request")
room := findRoom(packet.RoomCode)
if room == nil {
writePacket(conn, byte(CONNECT_RESPONSE), encodeResponsePacket(ConnectionResponsePacket{
Success: false,
Reason: "room does not exist",
}))
fmt.Println("Attempted room does not exist")
return
}
if room.Password == "" || packet.Password == room.Password {
writePacket(conn, byte(CONNECT_RESPONSE), encodeResponsePacket(ConnectionResponsePacket{
Success: true,
Reason: "",
}))
fmt.Println("Invalid password")
return
}
writePacket(conn, byte(CONNECT_RESPONSE), encodeResponsePacket(ConnectionResponsePacket{
Success: false,
Reason: "invalid password",
}))
fmt.Println("Connected user to room ")
}
func handleCreateRoomRequest(conn net.Conn, packet CreateRoomRequestPacket) {
if !isValidPassword(packet.Password) {
writePacket(conn, byte(CREATE_RESPONSE), encodeResponsePacket(CreateRoomResponsePacket{
Success: false,
Reason: "invalid password",
RoomCode: "",
}))
return
}
code := generateRandomRoomCode()
room := &Room{
Code: code,
Password: packet.Password,
Clients: []*Client{},
}
mu.Lock()
rooms[code] = room
mu.Unlock()
writePacket(conn, byte(CREATE_RESPONSE), encodeResponsePacket(CreateRoomResponsePacket{
Success: true,
Reason: "",
RoomCode: code,
}))
}
func handleJoinRequest(conn net.Conn, packet JoinRequestPacket) {
room := findRoom(packet.RoomCode)
if room == nil {
writePacket(conn, byte(JOIN_RESPONSE), encodeResponsePacket(JoinRequestResponsePacket{
Success: false,
Reason: "room not found",
}))
return
}
if !isValidPassword(packet.Password) {
writePacket(conn, byte(JOIN_RESPONSE), encodeResponsePacket(JoinRequestResponsePacket{
Success: false,
Reason: "invalid password",
}))
return
}
if room.Password == "" || room.Password == packet.Password {
client := &Client{
conn: conn,
username: username,
Conn: conn,
ClientID: packet.UserID,
RoomCode: packet.RoomCode,
}
conn.Write([]byte(fmt.Sprintf("Welcome %s!\n", username)))
handleClient(client)
}()
mu.Lock()
room.Clients = append(room.Clients, client)
clients[packet.UserID] = client
mu.Unlock()
writePacket(conn, byte(JOIN_RESPONSE), encodeResponsePacket(JoinRequestResponsePacket{
Success: true,
Reason: "",
CurrentData: "{}",
// TODO: Send current data
}))
return
}
writePacket(conn, byte(JOIN_RESPONSE), encodeResponsePacket(JoinRequestResponsePacket{
Success: false,
Reason: "invalid password",
}))
return
}
func handleSendData(conn net.Conn, packet SendDataRequestPacket) {
fmt.Println("Incoming send data request")
mu.Lock()
client, exists := clients[packet.UserID]
mu.Unlock()
if !exists || client.RoomCode == "" {
writePacket(conn, byte(SEND_DATA_RESPONSE), encodeResponsePacket(SendDataResponsePacket{
Success: false,
Reason: "client not in a room",
}))
return
}
room := findRoom(client.RoomCode)
if room == nil {
writePacket(conn, byte(SEND_DATA_RESPONSE), encodeResponsePacket(SendDataResponsePacket{
Success: false,
Reason: "room not found",
}))
return
}
dataPacket := encodeResponsePacket(ForwardDataPacket{
Data: packet.Data,
})
mu.Lock()
for _, roomClient := range room.Clients {
// if roomClient.ClientID != packet.UserID {
// // Send data to all other clients except the sender
// writePacket(roomClient.Conn, byte(READ_DATA), dataPacket)
// }
writePacket(roomClient.Conn, byte(READ_DATA), dataPacket)
}
mu.Unlock()
writePacket(conn, byte(SEND_DATA_RESPONSE), encodeResponsePacket(SendDataResponsePacket{
Success: true,
Reason: "",
}))
}
func handleLeaveRoom(conn net.Conn) {
mu.Lock()
defer mu.Unlock()
var leavingClient *Client
for _, client := range clients {
if client.Conn == conn {
leavingClient = client
break
}
}
if leavingClient == nil {
fmt.Println("Client not found")
return
}
room := rooms[leavingClient.RoomCode]
if room == nil {
fmt.Println("Room not found")
return
}
for i, client := range room.Clients {
if client == leavingClient {
room.Clients = append(room.Clients[:i], room.Clients[i+1:]...)
break
}
}
func main() {
startServer()
// Delete the room if its empty
if len(room.Clients) == 0 {
delete(rooms, room.Code)
fmt.Println("Room", room.Code, "deleted")
}
delete(clients, leavingClient.ClientID)
done := make(chan struct{})
go func() {
leavingClient.Conn.Close()
close(done)
}()
select {
case <-done:
case <-time.After(5 * time.Second):
log.Fatalln("Timeout while closing leaving client's connection")
}
fmt.Println("Client", leavingClient.ClientID, "left the room and connection closed")
}
func findRoom(code string) *Room {
mu.Lock()
defer mu.Unlock()
return rooms[code]
}
func encodeResponsePacket(packet interface{}) []byte {
data, err := json.Marshal(packet)
if err != nil {
fmt.Println("Error encoding response packet:", err)
return nil
}
return data
}
func generateRandomRoomCode() string {
validChars := "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
code := make([]byte, 4)
for i := range code {
code[i] = validChars[rand.Intn(len(validChars))]
}
return string(code)
}
func isValidPassword(password string) bool {
validChars := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789?!()[]{}<>-_"
charMap := make(map[rune]bool)
for _, char := range validChars {
charMap[char] = true
}
for _, char := range password {
if !charMap[char] {
return false
}
}
return true
}