diff --git a/app/app.go b/app/app.go index 06ab7d0..c57d171 100644 --- a/app/app.go +++ b/app/app.go @@ -1 +1,107 @@ 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/logger.go b/app/logger.go new file mode 100644 index 0000000..89688e6 --- /dev/null +++ b/app/logger.go @@ -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 main() { + logger := NewCustomLogger() + + logger.Info("This is an info message") + logger.Error("This is an error message") +} diff --git a/app/main.go b/app/main.go index 1b6ffab..68cd7a7 100644 --- a/app/main.go +++ b/app/main.go @@ -1,7 +1,10 @@ package main import ( + "encoding/json" "fmt" + "github.com/google/uuid" + netclient "hudly/client" "hudly/hypixel" "hudly/mcfetch" "log" @@ -9,8 +12,8 @@ import ( "time" ) -var key = "9634ea92-80f0-482f-aebd-b082c6ed6f19" -var uuid = "5328930e-d411-49cb-90ad-4e5c7b27dd86" +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 @@ -73,3 +76,157 @@ func demo() { } fmt.Println(fmt.Sprintf("%+v", res)) } + +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 + } + + client, err := netclient.NewClient("0.0.0.0", uuid.New().String()) + if err != nil { + log.Fatalf("Failed to create client: %v", err) + return + } + 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") +} + +// calculateRatio is a helper function to calculate ratios (e.g., WLR, KDR, etc.) +func calculateRatio(numerator, denominator int) float64 { + if denominator == 0 { + return float64(numerator) + } + return float64(numerator) / float64(denominator) +} diff --git a/client/client.go b/client/client.go index 7a79240..f8c9c66 100644 --- a/client/client.go +++ b/client/client.go @@ -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) +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 +} + +// 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("could not connect to server: %v", err) + return nil, fmt.Errorf("failed to connect to server: %w", err) } - return conn, nil + return &Client{ + Conn: conn, + ClientID: clientID, + DataChannel: make(chan string), + }, 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 - } +// 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 } -// 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) +// 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 main() { - if len(os.Args) < 2 { - fmt.Println("Usage: go run client.go ") - return +func (c *Client) ListenForData() { + go func() { + for { + var dataPacket SendDataRequestPacket + err := c.receivePacket(READ_DATA, &dataPacket) + if err != nil { + 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 + } + + // Send the received data to the channel + c.DataChannel <- dataPacket.Data + } + }() +} + +// 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, } - serverAddress := os.Args[1] - conn, err := connectToServer(serverAddress) + 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(err) - return + return fmt.Errorf("failed to encode packet: %w", 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) + return writePacket(c.Conn, byte(packetID), packetData) +} + +// 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 { + return err + } + 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 } diff --git a/go.mod b/go.mod index 7c247de..735350f 100644 --- a/go.mod +++ b/go.mod @@ -1 +1,12 @@ 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 460d46a..ac0ae2c 100644 --- a/server/server.go +++ b/server/server.go @@ -1,201 +1,97 @@ package main import ( - "bufio" + "encoding/binary" "encoding/json" "fmt" + "io" "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 + } + + // Then read the packet ID (1 byte) + var messageType byte + if err := binary.Read(conn, binary.BigEndian, &messageType); err != nil { + return 0, nil, err + } + + // 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 + } + + return PacketID(int(messageType)), data, nil } -// Handle client connection -func handleClient(client *Client) { - defer client.conn.Close() +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)) - 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)) + // 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 + } + + // Write the data + _, err := conn.Write(data) + return err } -// 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") +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 +99,332 @@ func startServer() { 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) - }() + go handleClient(conn) } } -func main() { - startServer() +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:", err) + 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) { + room := findRoom(packet.RoomCode) + if room == nil { + writePacket(conn, byte(CONNECT_RESPONSE), encodeResponsePacket(ConnectionResponsePacket{ + Success: false, + Reason: "room does not exist", + })) + return + } + + if room.Password == "" || packet.Password == room.Password { + writePacket(conn, byte(CONNECT_RESPONSE), encodeResponsePacket(ConnectionResponsePacket{ + Success: true, + Reason: "", + })) + return + } + + writePacket(conn, byte(CONNECT_RESPONSE), encodeResponsePacket(ConnectionResponsePacket{ + Success: false, + Reason: "invalid password", + })) +} + +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, + ClientID: packet.UserID, + RoomCode: packet.RoomCode, + } + + 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) { + 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 := findRoom(leavingClient.RoomCode) + if room == nil { + fmt.Println("Room not found") + return + } + + for i, client := range room.Clients { + if client == leavingClient { + // Remove the client from the list + room.Clients = append(room.Clients[:i], room.Clients[i+1:]...) + break + } + } + + // If the room is now empty or the leaving client was the creator, delete the room + if len(room.Clients) == 0 || room.Clients[0] == leavingClient { + for _, client := range room.Clients { + client.Conn.Close() + } + + delete(rooms, room.Code) + fmt.Println("Room", room.Code, "deleted") + } else { + // // Notify remaining clients that a user has left (optional) + // for _, remainingClient := range room.Clients { + // writePacket(remainingClient.Conn, byte(LEAVE_ROOM), []byte(fmt.Sprintf("User %s has left the room", leavingClient.ClientID))) + // } + } + + delete(clients, leavingClient.ClientID) + + leavingClient.Conn.Close() + 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 }