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 } 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 conn.Close() // 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 } player := &Player{ ID: playerID, X: x, Y: y, Username: username, } // Prevent multiple logins mu.Lock() for _, p := range players { if p.Username == username { mu.Unlock() response := &pb.ServerMessage{ AuthSuccess: false, ErrorMessage: "Account already logged in", } writeMessage(conn, response) return } } players[playerID] = player playerConns[playerID] = conn mu.Unlock() // 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, } addSystemMessage(fmt.Sprintf("%s connected", username)) // Ensure player state is saved on any kind of disconnect defer func() { if err := db.SavePlayerState(playerID, player.X, player.Y); err != nil { log.Printf("Error saving state for player %d: %v", playerID, err) } addSystemMessage(fmt.Sprintf("%s disconnected", player.Username)) mu.Lock() delete(players, playerID) delete(playerConns, playerID) delete(actionQueue, playerID) mu.Unlock() log.Printf("Player %d disconnected", playerID) }() // Send player ID to client if err := writeMessage(conn, response); err != nil { log.Printf("Failed to send player ID: %v", err) return } fmt.Printf("Player %d connected\n", playerID) // 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 } // 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 } } actionQueue[playerID] = append(actionQueue[playerID], batch.Actions...) } } } 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() defer mu.Unlock() // Update players based on queued actions for playerID, actions := range actionQueue { player := players[playerID] 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() actionQueue[playerID] = nil // Clear the action queue after processing } // Prepare and broadcast the current game state currentTick := time.Now().UnixNano() / int64(tickRate) state := &pb.ServerMessage{ CurrentTick: currentTick, Players: make([]*pb.PlayerState, 0, len(players)), } // Convert players to PlayerState for id, p := range players { p.Lock() state.Players = append(state.Players, &pb.PlayerState{ PlayerId: int32(id), X: int32(p.X), Y: int32(p.Y), Username: p.Username, }) p.Unlock() } // Add chat messages to the state chatMutex.RLock() state.ChatMessages = chatHistory[max(0, len(chatHistory)-5):] // Only send last 5 messages chatMutex.RUnlock() // Send to each connected player for _, conn := range playerConns { if err := writeMessage(conn, state); err != nil { log.Printf("Failed to send update: %v", err) } } } // 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 }