diff --git a/app/app.go b/app/app.go deleted file mode 100644 index c57d171..0000000 --- a/app/app.go +++ /dev/null @@ -1,107 +0,0 @@ -package main - -import ( - "github.com/haleyrom/skia-go" - "image/color" - "log" - "runtime" - - "github.com/go-gl/gl/v3.3-core/gl" - "github.com/go-gl/glfw/v3.3/glfw" -) - -func init() { - // GLFW event handling must run on the main OS thread - runtime.LockOSThread() -} - -func main() { - // Initialize GLFW - if err := glfw.Init(); err != nil { - log.Fatalf("failed to initialize GLFW: %v", err) - } - defer glfw.Terminate() - - // Create GLFW window - glfw.WindowHint(glfw.ContextVersionMajor, 3) - glfw.WindowHint(glfw.ContextVersionMinor, 3) - glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile) - glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True) - - window, err := glfw.CreateWindow(800, 600, "GLFW + Skia Demo", nil, nil) - if err != nil { - log.Fatalf("failed to create GLFW window: %v", err) - } - window.MakeContextCurrent() - - // Initialize OpenGL - if err := gl.Init(); err != nil { - log.Fatalf("failed to initialize OpenGL: %v", err) - } - - // Set up Skia surface - glInterface := skia.NewNativeGrGlinterface() - if glInterface == nil { - log.Fatalf("failed to create Skia OpenGL interface") - } - grContext := skia.NewGLGrContext(glInterface) - if grContext == nil { - log.Fatalf("failed to create Skia GrContext") - } - - // Get framebuffer info - var framebufferInfo skia.GrGlFramebufferinfo - var fboID int32 - gl.GetIntegerv(gl.FRAMEBUFFER_BINDING, &fboID) - framebufferInfo.FFBOID = uint32(fboID) - framebufferInfo.FFormat = gl.RGBA8 - - width, height := window.GetSize() - renderTarget := skia.NewGlGrBackendrendertarget(int32(width), int32(height), 1, 8, &framebufferInfo) - if renderTarget == nil { - log.Fatalf("failed to create Skia render target") - } - - surface := skia.NewSurfaceBackendRenderTarget(grContext, renderTarget, skia.GR_SURFACE_ORIGIN_BOTTOM_LEFT, skia.SK_COLORTYPE_RGBA_8888, nil, nil) - if surface == nil { - log.Fatalf("failed to create Skia surface") - } - defer surface.Unref() - - c := color.RGBA{R: 255, G: 100, B: 50, A: 255} - f := skia.NewColor4f(c) - colorWhite := f.ToColor() - - for !window.ShouldClose() { - gl.ClearColor(0.3, 0.3, 0.3, 1.0) - gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) - - canvas := surface.GetCanvas() - - paint := skia.NewPaint() - paint.SetColor(0xffff0000) - - // Clear the canvas with white color - canvas.Clear(colorWhite) - - // Create a rectangle and draw it - rect := skia.Rect{ - Left: 100, - Top: 100, - Right: 300, - Bottom: 300, - } - canvas.DrawRect(&rect, paint) - grContext.Flush() - - // Swap OpenGL buffers - window.SwapBuffers() - - // Poll for GLFW events - glfw.PollEvents() - } - - // Cleanup - surface.Unref() - grContext.Unref() -} diff --git a/app/demo.go b/app/demo.go new file mode 100644 index 0000000..20c2edc --- /dev/null +++ b/app/demo.go @@ -0,0 +1,8 @@ +package main + +var key = "ccebff0f-939a-4afe-b5b3-30a7a665ee38" + +func main() { + var demoApp = NewDemoApp(key) + demoApp.Start() +} diff --git a/app/logbuf.go b/app/logbuf.go new file mode 100644 index 0000000..7a27afc --- /dev/null +++ b/app/logbuf.go @@ -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) diff --git a/app/logger.go b/app/logger.go index 89688e6..1f02f91 100644 --- a/app/logger.go +++ b/app/logger.go @@ -52,7 +52,7 @@ func (c *CustomLogger) Error(msg string) { } // Example usage of the custom logger -func main() { +func showcase() { logger := NewCustomLogger() logger.Info("This is an info message") diff --git a/app/main.go b/app/main.go index 68cd7a7..99be150 100644 --- a/app/main.go +++ b/app/main.go @@ -1,6 +1,7 @@ package main import ( + "bufio" "encoding/json" "fmt" "github.com/google/uuid" @@ -9,218 +10,36 @@ import ( "hudly/mcfetch" "log" "os" + "os/exec" + "path/filepath" + "runtime" + "strings" "time" ) -var key = "ccebff0f-939a-4afe-b5b3-30a7a665ee38" -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)) +type PlayerWrapper struct { + Player hypixel.Player `json:"player"` } -func main() { - var cmd string - fmt.Print(" | CREATE\n | JOIN\nEnter Choice:\n>") - fmt.Scanln(&cmd) - - if cmd != "CREATE" && cmd != "JOIN" { - fmt.Println("Invalid command.") - return +func replaceCorruptedRune(msg string) string { + runes := []rune(msg) + for i, r := range runes { + if r == '�' { + runes[i] = '§' + } } + return string(runes) +} - client, err := netclient.NewClient("0.0.0.0", uuid.New().String()) - if err != nil { - log.Fatalf("Failed to create client: %v", err) - return +func clearTerminal() { + var cmd *exec.Cmd + if runtime.GOOS == "windows" { + cmd = exec.Command("cmd", "/c", "cls") + } else { + cmd = exec.Command("clear") } - var code string - if cmd == "CREATE" { - var err error - code, err = client.CreateRoom("") - if err != nil { - log.Fatal(err) - return - } - fmt.Println("Created room:", code) - - err = client.JoinRoom(code, "password") - if err != nil { - log.Fatal(err) - return - } - println("Connected to room") - } else if cmd == "JOIN" { - var password string - fmt.Print("Enter Room Code:\n>") - fmt.Scanln(&code) - - fmt.Print("Enter Room Password:\n>") - fmt.Scanln(&password) - - err := client.JoinRoom(code, password) - if err != nil { - log.Fatal(err) - return - } - fmt.Println("Joined room:", code) - } - - client.ListenForData() - // Handle received data in a separate goroutine - go func() { - for data := range client.DataChannel { - fmt.Printf("Received data: %s\n", data) - } - }() - - if cmd == "CREATE" { - println("Sleeping...") - time.Sleep(time.Second * 20) - println("Continuing!") - - thing := hypixel.NewAPIKey(key) - api := hypixel.NewAPI(*thing) - - memCache := &mcfetch.MemoryCache{} - memCache.Init() - - resultChan := make(chan map[string]interface{}) - errorChan := make(chan error) - - players := []string{"illyum", "ergopunch"} - stuff := []map[string]interface{}{} // List to hold player JSON objects - - println("Getting UUIDs") - for _, player := range players { - asyncFetcher := mcfetch.NewAsyncPlayerFetcher( - player, - memCache, - 2, - 2*time.Second, - 5*time.Second, - ) - asyncFetcher.FetchPlayerData(resultChan, errorChan) - } - - var userID string - for i := 0; i < len(players); i++ { - select { - case data := <-resultChan: - 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") - } - - // Fetch player stats - res, err := api.GetPlayerResponse(userID) - if err != nil { - log.Fatalf("Failed to get player data: %v", err) - } - - // Calculate derived stats - res.Player.Stats.Bedwars.WLR = calculateRatio(res.Player.Stats.Bedwars.Wins, res.Player.Stats.Bedwars.Losses) - res.Player.Stats.Bedwars.KDR = calculateRatio(res.Player.Stats.Bedwars.Kills, res.Player.Stats.Bedwars.Deaths) - res.Player.Stats.Bedwars.FKDR = calculateRatio(res.Player.Stats.Bedwars.FinalKills, res.Player.Stats.Bedwars.FinalDeaths) - res.Player.Stats.Bedwars.BBLR = calculateRatio(res.Player.Stats.Bedwars.BedsBroken, res.Player.Stats.Bedwars.BedsLost) - - // Convert player struct to JSON - playerJSON, err := json.Marshal(res) - if err != nil { - log.Fatalf("Failed to marshal player data: %v", err) - } - - // Append the player JSON to the stuff list - var playerMap map[string]interface{} - json.Unmarshal(playerJSON, &playerMap) - stuff = append(stuff, playerMap) - - case err := <-errorChan: - log.Fatal(err) - } - } - println("UUID Done!") - println("Sending data...") - - // Send the list of JSON objects to the client - message, err := json.Marshal(stuff) - if err != nil { - log.Fatalf("Failed to marshal stuff: %v", err) - } - err = client.SendData(string(message)) - if err != nil { - log.Printf("Error sending data: %v", err) - } - fmt.Println("Sent stuff:", stuff) - println("Sending Done!") - } - - select { - case <-client.DataChannel: // This blocks the main goroutine until data is received - fmt.Println("Data received, exiting...") - } - - fmt.Println("Closing app") + cmd.Stdout = os.Stdout + cmd.Run() } // calculateRatio is a helper function to calculate ratios (e.g., WLR, KDR, etc.) @@ -230,3 +49,361 @@ func calculateRatio(numerator, denominator int) float64 { } return float64(numerator) / float64(denominator) } + +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 { + return nil, err + } + 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, ",") + for _, player := range players { + playerName := strings.TrimSpace(player) + plr, err := app.FetchMCPlayer(playerName) + res_name := plr.Name + res_uuid := plr.UUID + if err != nil { + log.Fatalf("Error fetching UUID: %v", err) + return + } + fmt.Printf("UUID of player %s: %s\n", res_name, res_uuid) + + //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, _ := 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 { + // Fetch player stats + res, err := app.API.GetPlayerResponse(user.UUID) + if err != nil { + log.Fatalf("Failed to get player data: %v", err) + } + + // Calculate derived stats + res.Player.Stats.Bedwars.WLR = calculateRatio(res.Player.Stats.Bedwars.Wins, res.Player.Stats.Bedwars.Losses) + res.Player.Stats.Bedwars.KDR = calculateRatio(res.Player.Stats.Bedwars.Kills, res.Player.Stats.Bedwars.Deaths) + res.Player.Stats.Bedwars.FKDR = calculateRatio(res.Player.Stats.Bedwars.FinalKills, res.Player.Stats.Bedwars.FinalDeaths) + res.Player.Stats.Bedwars.BBLR = calculateRatio(res.Player.Stats.Bedwars.BedsBroken, res.Player.Stats.Bedwars.BedsLost) + + // Convert player struct to JSON + playerJSON, err := json.Marshal(res) + if err != nil { + log.Fatalf("Failed to marshal player data: %v", err) + } + + // Append the player JSON to the stuff list + var playerMap map[string]interface{} + json.Unmarshal(playerJSON, &playerMap) + app.PartyBuilder = append(app.PartyBuilder, playerMap) + + // Send the list of JSON objects to the client + 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!") + } +} + +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("0.0.0.0", 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() + // Handle received data in a separate goroutine + go func() { + for data := range app.Client.DataChannel { + fmt.Printf("Received data: %s\n", data) + } + }() + + 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) + } + + // Pass the extracted players to DisplayPlayers + 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("|----------------------|------------|------------|") +} diff --git a/app/config/config.go b/config/config.go similarity index 100% rename from app/config/config.go rename to config/config.go diff --git a/go.mod b/go.mod index 735350f..5fa8ab3 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,6 @@ module hudly go 1.23.0 require ( - github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 - github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a github.com/google/uuid v1.6.0 - github.com/haleyrom/skia-go v0.0.0-20240328095045-3f321a6a6e4d ) -require github.com/mattn/go-pointer v0.0.0-20190911064623-a0a44394634f // indirect diff --git a/server/server.go b/server/server.go index ac0ae2c..aa3398e 100644 --- a/server/server.go +++ b/server/server.go @@ -199,12 +199,14 @@ func handleClient(conn net.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 } @@ -213,6 +215,7 @@ func handleConnectRequest(conn net.Conn, packet ConnectionRequestPacket) { Success: true, Reason: "", })) + fmt.Println("Invalid password") return } @@ -220,6 +223,7 @@ func handleConnectRequest(conn net.Conn, packet ConnectionRequestPacket) { Success: false, Reason: "invalid password", })) + fmt.Println("Connected user to room ") } func handleCreateRoomRequest(conn net.Conn, packet CreateRoomRequestPacket) { @@ -297,6 +301,7 @@ func handleJoinRequest(conn net.Conn, packet JoinRequestPacket) { } func handleSendData(conn net.Conn, packet SendDataRequestPacket) { + fmt.Println("Incoming send data request") mu.Lock() client, exists := clients[packet.UserID] mu.Unlock()