package main import ( "bufio" "encoding/binary" "fmt" "io" "log" "net" "sync" "time" pb "gitea.boner.be/bdnugget/goonserver/actions" "gitea.boner.be/bdnugget/goonserver/db" "google.golang.org/protobuf/proto" ) const ( port = ":6969" // Port to listen on tickRate = 600 * time.Millisecond protoVersion = 1 ) type Player struct { sync.Mutex ID int X, Y int Username string LastSeenMsgTimestamp int64 // Track the last message timestamp this player has seen } var ( players = make(map[int]*Player) actionQueue = make(map[int][]*pb.Action) // Queue to store actions for each player playerConns = make(map[int]net.Conn) // Map to store player connections mu sync.RWMutex // Add mutex for protecting shared maps chatHistory = make([]*pb.ChatMessage, 0, 100) chatMutex sync.RWMutex ) func main() { if err := db.InitDB("goonserver.db"); err != nil { log.Fatalf("Failed to initialize database: %v", err) } ln, err := net.Listen("tcp", port) if err != nil { log.Fatalf("Failed to listen on port %s: %v", port, err) } defer ln.Close() fmt.Printf("Server is listening on port %s\n", port) // Create ticker for fixed game state updates ticker := time.NewTicker(tickRate) defer ticker.Stop() // Start registration attempt cleanup goroutine go func() { ticker := time.NewTicker(time.Hour) defer ticker.Stop() for range ticker.C { db.CleanupOldAttempts() } }() // Handle incoming connections in a separate goroutine go func() { for { conn, err := ln.Accept() if err != nil { log.Printf("Failed to accept connection: %v", err) continue } go handleConnection(conn) } }() // Main game loop for range ticker.C { processActions() } } func handleConnection(conn net.Conn) { defer func() { conn.Close() log.Printf("Connection closed and cleanup complete") }() // Get client IP remoteAddr := conn.RemoteAddr().String() ip, _, err := net.SplitHostPort(remoteAddr) if err != nil { log.Printf("Failed to parse remote address: %v", err) return } // Read initial message for player ID reader := bufio.NewReader(conn) // Wait for authentication lengthBuf := make([]byte, 4) if _, err := io.ReadFull(reader, lengthBuf); err != nil { log.Printf("Failed to read auth message length: %v", err) return } messageLength := binary.BigEndian.Uint32(lengthBuf) messageBuf := make([]byte, messageLength) if _, err := io.ReadFull(reader, messageBuf); err != nil { log.Printf("Failed to read auth message: %v", err) return } batch := &pb.ActionBatch{} if err := proto.Unmarshal(messageBuf, batch); err != nil { log.Printf("Failed to unmarshal auth message: %v", err) return } if len(batch.Actions) == 0 { log.Printf("No auth action received") return } action := batch.Actions[0] var playerID int var authErr error if batch.ProtocolVersion == 0 { response := &pb.ServerMessage{ AuthSuccess: false, ErrorMessage: "Client using outdated protocol (pre-versioning)", ProtocolVersion: protoVersion, } writeMessage(conn, response) return } if batch.ProtocolVersion < protoVersion { response := &pb.ServerMessage{ AuthSuccess: false, ErrorMessage: fmt.Sprintf("Client protocol version too old (client: %d, required: %d)", batch.ProtocolVersion, protoVersion), ProtocolVersion: protoVersion, } writeMessage(conn, response) return } switch action.Type { case pb.Action_REGISTER: if err := db.CheckRegistrationLimit(ip); err != nil { response := &pb.ServerMessage{ AuthSuccess: false, ErrorMessage: err.Error(), } writeMessage(conn, response) return } playerID, authErr = db.RegisterPlayer(action.Username, action.Password) case pb.Action_LOGIN: playerID, authErr = db.AuthenticatePlayer(action.Username, action.Password) default: log.Printf("Invalid initial action type: %v", action.Type) return } // Send auth response response := &pb.ServerMessage{ PlayerId: int32(playerID), AuthSuccess: authErr == nil, } if authErr != nil { response.ErrorMessage = authErr.Error() if err := writeMessage(conn, response); err != nil { log.Printf("Failed to send auth response: %v", err) } return } // Load last known position x, y, err := db.LoadPlayerState(playerID) if err != nil { log.Printf("Error loading state for player %d: %v", playerID, err) x, y = 5, 5 // Default position } username, err := db.GetUsername(playerID) if err != nil { log.Printf("Error getting username for player %d: %v", playerID, err) return } log.Printf("Player %d (%s) authenticated successfully, checking for existing session", playerID, username) // Check for existing session and force disconnect if needed mu.Lock() existingPlayer, alreadyLoggedIn := players[playerID] if alreadyLoggedIn { log.Printf("Player %d (%s) is already logged in, forcing disconnect of old session", playerID, username) // An existing session is found - clean it up if oldConn, exists := playerConns[playerID]; exists { // Try to close the old connection oldConn.Close() delete(playerConns, playerID) } // Keep the player object but update its connection existingPlayer.X = x existingPlayer.Y = y playerConns[playerID] = conn mu.Unlock() } else { // Create a new player player := &Player{ ID: playerID, X: x, Y: y, Username: username, LastSeenMsgTimestamp: 0, // Initialize to 0 to receive all messages initially } players[playerID] = player playerConns[playerID] = conn mu.Unlock() existingPlayer = player // Announce connection addSystemMessage(fmt.Sprintf("%s connected", username)) } // Ensure player state is saved on any kind of disconnect defer func() { if p, exists := players[playerID]; exists { if err := db.SavePlayerState(playerID, p.X, p.Y); err != nil { log.Printf("Error saving state for player %d: %v", playerID, err) } } addSystemMessage(fmt.Sprintf("%s disconnected", username)) mu.Lock() delete(players, playerID) delete(playerConns, playerID) delete(actionQueue, playerID) mu.Unlock() log.Printf("Player %d (%s) disconnected", playerID, username) }() // Send initial state with correct position response = &pb.ServerMessage{ PlayerId: int32(playerID), AuthSuccess: true, Players: []*pb.PlayerState{{ PlayerId: int32(playerID), X: int32(x), Y: int32(y), Username: username, }}, ProtocolVersion: protoVersion, } // Send player ID to client if err := writeMessage(conn, response); err != nil { log.Printf("Failed to send player ID: %v", err) return } log.Printf("Player %d (%s) connected successfully", playerID, username) // Listen for incoming actions from this player for { // Read message length lengthBuf := make([]byte, 4) if _, err := io.ReadFull(reader, lengthBuf); err != nil { if err == io.EOF { log.Printf("Player %d disconnected gracefully", playerID) } else { log.Printf("Error reading message length from player %d: %v", playerID, err) } return } messageLength := binary.BigEndian.Uint32(lengthBuf) // Read message body messageBuf := make([]byte, messageLength) if _, err := io.ReadFull(reader, messageBuf); err != nil { log.Printf("Error reading message from player %d: %v", playerID, err) return } batch := &pb.ActionBatch{} if err := proto.Unmarshal(messageBuf, batch); err != nil { log.Printf("Failed to unmarshal action batch for player %d: %v", playerID, err) continue } // Update the last seen message timestamp if batch.LastSeenMessageTimestamp > 0 { existingPlayer.LastSeenMsgTimestamp = batch.LastSeenMessageTimestamp } // Queue the actions for processing if batch.PlayerId == int32(playerID) { for _, action := range batch.Actions { if action.Type == pb.Action_DISCONNECT { log.Printf("Player %d requested disconnect", playerID) return } } mu.Lock() actionQueue[playerID] = append(actionQueue[playerID], batch.Actions...) mu.Unlock() } } } func addChatMessage(playerID int32, content string) { player, exists := players[int(playerID)] if !exists { return } chatMutex.Lock() defer chatMutex.Unlock() msg := &pb.ChatMessage{ PlayerId: playerID, Username: player.Username, Content: content, Timestamp: time.Now().UnixNano(), } if len(chatHistory) >= 100 { chatHistory = chatHistory[1:] } chatHistory = append(chatHistory, msg) } func addSystemMessage(content string) { chatMutex.Lock() defer chatMutex.Unlock() msg := &pb.ChatMessage{ PlayerId: 0, // System messages use ID 0 Username: "System", Content: content, Timestamp: time.Now().UnixNano(), } if len(chatHistory) >= 100 { chatHistory = chatHistory[1:] } chatHistory = append(chatHistory, msg) } func processActions() { mu.Lock() // Make a list of players to process first, to avoid lock contention activePlayers := make(map[int]*Player) for id, p := range players { activePlayers[id] = p } activeConns := make(map[int]net.Conn) for id, conn := range playerConns { activeConns[id] = conn } activeQueues := make(map[int][]*pb.Action) for id, actions := range actionQueue { if len(actions) > 0 { activeQueues[id] = actions actionQueue[id] = nil // Clear the queue early to avoid double processing } } mu.Unlock() // Process actions without holding the global lock for playerID, actions := range activeQueues { player, exists := activePlayers[playerID] if !exists { continue } player.Lock() for _, action := range actions { switch action.Type { case pb.Action_MOVE: player.X = int(action.X) player.Y = int(action.Y) fmt.Printf("Player %d moved to (%d, %d)\n", playerID, player.X, player.Y) case pb.Action_CHAT: addChatMessage(int32(playerID), action.ChatMessage) fmt.Printf("Player %d says: %s\n", playerID, action.ChatMessage) } } player.Unlock() } // Prepare current game state currentTick := time.Now().UnixNano() / int64(tickRate) // Get recent messages for new connections chatMutex.RLock() recentMessages := chatHistory[max(0, len(chatHistory)-5):] // Get last 5 for new connections chatMutex.RUnlock() // To avoid holding locks too long, prepare player states first playerStates := make([]*pb.PlayerState, 0, len(activePlayers)) for id, p := range activePlayers { p.Lock() playerStates = append(playerStates, &pb.PlayerState{ PlayerId: int32(id), X: int32(p.X), Y: int32(p.Y), Username: p.Username, }) p.Unlock() } // Now send updates to each player for playerID, conn := range activeConns { player, exists := activePlayers[playerID] if !exists { continue } state := &pb.ServerMessage{ CurrentTick: currentTick, Players: playerStates, } // Add chat messages - only send those the player hasn't seen player.Lock() lastSeen := player.LastSeenMsgTimestamp player.Unlock() chatMutex.RLock() var newMessages []*pb.ChatMessage // For new connections, send the 5 most recent messages if lastSeen == 0 && len(recentMessages) > 0 { newMessages = recentMessages if len(newMessages) > 0 { // Update the player's timestamp to the latest message player.Lock() player.LastSeenMsgTimestamp = newMessages[len(newMessages)-1].Timestamp player.Unlock() } } else { // For existing connections, only send new messages for _, msg := range chatHistory { if msg.Timestamp > lastSeen { newMessages = append(newMessages, msg) } } // Update the player's timestamp if we sent them new messages if len(newMessages) > 0 { player.Lock() player.LastSeenMsgTimestamp = newMessages[len(newMessages)-1].Timestamp player.Unlock() } } state.ChatMessages = newMessages chatMutex.RUnlock() // Log the number of messages we're sending if len(newMessages) > 0 { log.Printf("Sending %d new messages to player %d", len(newMessages), playerID) } // Send the state to the player - do this without holding any locks if err := writeMessage(conn, state); err != nil { log.Printf("Failed to send update to player %d: %v", playerID, err) // Handle connection errors by removing the player mu.Lock() delete(players, playerID) delete(playerConns, playerID) delete(actionQueue, playerID) mu.Unlock() } } } // Helper function to write length-prefixed messages func writeMessage(conn net.Conn, msg proto.Message) error { data, err := proto.Marshal(msg) if err != nil { return err } // Write length prefix lengthBuf := make([]byte, 4) binary.BigEndian.PutUint32(lengthBuf, uint32(len(data))) if _, err := conn.Write(lengthBuf); err != nil { return err } // Write message body _, err = conn.Write(data) return err }