diff --git a/game/chat.go b/game/chat.go index ea0dba1..df01004 100644 --- a/game/chat.go +++ b/game/chat.go @@ -53,6 +53,7 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) { for _, msg := range messages { localMsg := types.ChatMessage{ PlayerID: msg.PlayerId, + Username: msg.Username, Content: msg.Content, Time: time.Unix(0, msg.Timestamp), } @@ -117,7 +118,7 @@ func (c *Chat) Draw(screenWidth, screenHeight int32) { for i := startIdx; i < endIdx; i++ { msg := c.messages[i] - text := fmt.Sprintf("[%d]: %s", msg.PlayerID, msg.Content) + text := fmt.Sprintf("%s: %s", msg.Username, msg.Content) rl.DrawText(text, int32(chatX)+5, int32(messageY), 20, rl.White) messageY += messageHeight } diff --git a/game/game.go b/game/game.go index b926291..5c4f001 100644 --- a/game/game.go +++ b/game/game.go @@ -5,6 +5,7 @@ import ( "time" "gitea.boner.be/bdnugget/goonscape/assets" + "gitea.boner.be/bdnugget/goonscape/network" "gitea.boner.be/bdnugget/goonscape/types" pb "gitea.boner.be/bdnugget/goonserver/actions" rl "github.com/gen2brain/raylib-go/raylib" @@ -19,19 +20,13 @@ type Game struct { Chat *Chat MenuOpen bool QuitChan chan struct{} // Channel to signal shutdown + loginScreen *LoginScreen + isLoggedIn bool } func New() *Game { InitWorld() game := &Game{ - Player: &types.Player{ - PosActual: rl.NewVector3(5*types.TileSize, 0, 5*types.TileSize), - PosTile: GetTile(5, 5), - Speed: 50.0, - TargetPath: []types.Tile{}, - UserData: nil, - QuitDone: make(chan struct{}), - }, OtherPlayers: make(map[int32]*types.Player), Camera: rl.Camera3D{ Position: rl.NewVector3(0, 10, 10), @@ -40,10 +35,10 @@ func New() *Game { Fovy: 45.0, Projection: rl.CameraPerspective, }, - Chat: NewChat(), - QuitChan: make(chan struct{}), + Chat: NewChat(), + QuitChan: make(chan struct{}), + loginScreen: NewLoginScreen(), } - game.Player.UserData = game game.Chat.userData = game return game } @@ -64,6 +59,35 @@ func (g *Game) LoadAssets() error { } func (g *Game) Update(deltaTime float32) { + if !g.isLoggedIn { + username, password, isRegistering, submitted := g.loginScreen.Update() + if submitted { + conn, playerID, err := network.ConnectToServer(username, password, isRegistering) + if err != nil { + g.loginScreen.SetError(err.Error()) + return + } + + // Assign model based on player ID + modelIndex := int(playerID) % len(g.Models) + g.Player = &types.Player{ + Speed: 50.0, + TargetPath: []types.Tile{}, + UserData: g, + QuitDone: make(chan struct{}), + ID: playerID, + Model: g.Models[modelIndex].Model, + Texture: g.Models[modelIndex].Texture, + } + + go network.HandleServerCommunication(conn, playerID, g.Player, g.OtherPlayers, g.QuitChan) + g.isLoggedIn = true + return + } + g.loginScreen.Draw() + return + } + // Handle ESC for menu if rl.IsKeyPressed(rl.KeyEscape) { g.MenuOpen = !g.MenuOpen @@ -174,11 +198,23 @@ func (g *Game) Render() { rl.BeginDrawing() rl.ClearBackground(rl.RayWhite) + if !g.isLoggedIn { + g.loginScreen.Draw() + rl.EndDrawing() + return + } + rl.BeginMode3D(g.Camera) g.DrawMap() g.DrawPlayer(g.Player, g.Player.Model) for id, other := range g.OtherPlayers { - g.DrawPlayer(other, g.Models[int(id)%len(g.Models)].Model) + if other.Model.Meshes == nil { + // Assign model based on player ID for consistency + modelIndex := int(id) % len(g.Models) + other.Model = g.Models[modelIndex].Model + other.Texture = g.Models[modelIndex].Texture + } + g.DrawPlayer(other, other.Model) } rl.EndMode3D() @@ -312,3 +348,7 @@ func (g *Game) Shutdown() { rl.CloseWindow() os.Exit(0) } + +func (g *Game) HandleServerMessages(messages []*pb.ChatMessage) { + g.Chat.HandleServerMessages(messages) +} diff --git a/game/login.go b/game/login.go new file mode 100644 index 0000000..a36a0d9 --- /dev/null +++ b/game/login.go @@ -0,0 +1,185 @@ +package game + +import ( + rl "github.com/gen2brain/raylib-go/raylib" +) + +type LoginScreen struct { + username string + password string + errorMessage string + isRegistering bool + focusedField int // 0 = username, 1 = password +} + +func NewLoginScreen() *LoginScreen { + return &LoginScreen{} +} + +func (l *LoginScreen) Draw() { + screenWidth := float32(rl.GetScreenWidth()) + screenHeight := float32(rl.GetScreenHeight()) + + // Draw background + rl.DrawRectangle(0, 0, int32(screenWidth), int32(screenHeight), rl.RayWhite) + + // Draw title + title := "GoonScape" + if l.isRegistering { + title += " - Register" + } else { + title += " - Login" + } + titleSize := int32(40) + titleWidth := rl.MeasureText(title, titleSize) + rl.DrawText(title, int32(screenWidth/2)-titleWidth/2, 100, titleSize, rl.Black) + + // Draw input fields + inputWidth := float32(200) + inputHeight := float32(30) + inputX := screenWidth/2 - inputWidth/2 + + // Username field + rl.DrawRectangleRec(rl.Rectangle{ + X: inputX, Y: 200, + Width: inputWidth, Height: inputHeight, + }, rl.LightGray) + rl.DrawText(l.username, int32(inputX)+5, 205, 20, rl.Black) + if l.focusedField == 0 { + rl.DrawRectangleLinesEx(rl.Rectangle{ + X: inputX - 2, Y: 198, + Width: inputWidth + 4, Height: inputHeight + 4, + }, 2, rl.Blue) + } + + // Password field + rl.DrawRectangleRec(rl.Rectangle{ + X: inputX, Y: 250, + Width: inputWidth, Height: inputHeight, + }, rl.LightGray) + masked := "" + for range l.password { + masked += "*" + } + rl.DrawText(masked, int32(inputX)+5, 255, 20, rl.Black) + if l.focusedField == 1 { + rl.DrawRectangleLinesEx(rl.Rectangle{ + X: inputX - 2, Y: 248, + Width: inputWidth + 4, Height: inputHeight + 4, + }, 2, rl.Blue) + } + + // Draw error message + if l.errorMessage != "" { + msgWidth := rl.MeasureText(l.errorMessage, 20) + rl.DrawText(l.errorMessage, int32(screenWidth/2)-msgWidth/2, 300, 20, rl.Red) + } + + // Draw buttons + buttonWidth := float32(100) + buttonHeight := float32(30) + buttonY := float32(350) + + // Login/Register button + actionBtn := rl.Rectangle{ + X: screenWidth/2 - buttonWidth - 10, + Y: buttonY, + Width: buttonWidth, + Height: buttonHeight, + } + rl.DrawRectangleRec(actionBtn, rl.Blue) + actionText := "Login" + if l.isRegistering { + actionText = "Register" + } + actionWidth := rl.MeasureText(actionText, 20) + rl.DrawText(actionText, + int32(actionBtn.X+actionBtn.Width/2)-actionWidth/2, + int32(actionBtn.Y+5), + 20, rl.White) + + // Switch mode button + switchBtn := rl.Rectangle{ + X: screenWidth/2 + 10, + Y: buttonY, + Width: buttonWidth, + Height: buttonHeight, + } + rl.DrawRectangleRec(switchBtn, rl.DarkGray) + switchText := "Register" + if l.isRegistering { + switchText = "Login" + } + switchWidth := rl.MeasureText(switchText, 20) + rl.DrawText(switchText, + int32(switchBtn.X+switchBtn.Width/2)-switchWidth/2, + int32(switchBtn.Y+5), + 20, rl.White) +} + +func (l *LoginScreen) Update() (string, string, bool, bool) { + // Handle input field focus + if rl.IsMouseButtonPressed(rl.MouseLeftButton) { + mousePos := rl.GetMousePosition() + screenWidth := float32(rl.GetScreenWidth()) + inputWidth := float32(200) + inputX := screenWidth/2 - inputWidth/2 + + // Check username field + if mousePos.X >= inputX && mousePos.X <= inputX+inputWidth && + mousePos.Y >= 200 && mousePos.Y <= 230 { + l.focusedField = 0 + } + // Check password field + if mousePos.X >= inputX && mousePos.X <= inputX+inputWidth && + mousePos.Y >= 250 && mousePos.Y <= 280 { + l.focusedField = 1 + } + + // Check buttons + buttonWidth := float32(100) + if mousePos.Y >= 350 && mousePos.Y <= 380 { + // Action button + if mousePos.X >= screenWidth/2-buttonWidth-10 && + mousePos.X <= screenWidth/2-10 { + return l.username, l.password, l.isRegistering, true + } + // Switch mode button + if mousePos.X >= screenWidth/2+10 && + mousePos.X <= screenWidth/2+buttonWidth+10 { + l.isRegistering = !l.isRegistering + } + } + } + + // Handle text input + key := rl.GetCharPressed() + for key > 0 { + if l.focusedField == 0 && len(l.username) < 12 { + l.username += string(key) + } else if l.focusedField == 1 && len(l.password) < 20 { + l.password += string(key) + } + key = rl.GetCharPressed() + } + + // Handle backspace + if rl.IsKeyPressed(rl.KeyBackspace) { + if l.focusedField == 0 && len(l.username) > 0 { + l.username = l.username[:len(l.username)-1] + } else if l.focusedField == 1 && len(l.password) > 0 { + l.password = l.password[:len(l.password)-1] + } + } + + // Handle tab to switch fields + if rl.IsKeyPressed(rl.KeyTab) { + l.focusedField = (l.focusedField + 1) % 2 + } + + return "", "", false, false +} + +func (l *LoginScreen) SetError(msg string) { + l.errorMessage = msg +} diff --git a/goonserver b/goonserver index be32dec..27da845 160000 --- a/goonserver +++ b/goonserver @@ -1 +1 @@ -Subproject commit be32dec2025e02db94cd2d79293a39cfb808bbaf +Subproject commit 27da845b11f968aa77139c871becc6ce4c688038 diff --git a/main.go b/main.go index 9c8fb43..7b1f06f 100644 --- a/main.go +++ b/main.go @@ -11,20 +11,21 @@ import ( ) func main() { - local := flag.Bool("local", false, "Use local server instead of remote") - addr := flag.String("addr", "boner.be:6969", "Server address (hostname:port or hostname)") + // Parse command line flags + local := flag.Bool("local", false, "Connect to local server") + addr := flag.String("addr", "", "Server address (host or host:port)") flag.Parse() - if *local && *addr != "boner.be:6969" { - log.Fatal("Cannot use both -local and -addr flags") - } - + // Set server address based on flags if *local { + if *addr != "" { + log.Fatal("Cannot use -local and -addr together") + } network.SetServerAddr("localhost:6969") } else if *addr != "" { - // If only hostname is provided, append default port + // If port is not specified, append default port if !strings.Contains(*addr, ":") { - *addr = *addr + ":6969" + *addr += ":6969" } network.SetServerAddr(*addr) } @@ -32,36 +33,24 @@ func main() { rl.InitWindow(1024, 768, "GoonScape") rl.SetExitKey(0) defer rl.CloseWindow() + rl.InitAudioDevice() defer rl.CloseAudioDevice() + rl.SetTargetFPS(60) + game := game.New() if err := game.LoadAssets(); err != nil { log.Fatalf("Failed to load assets: %v", err) } defer game.Cleanup() - conn, playerID, err := network.ConnectToServer() - if err != nil { - log.Fatalf("Failed to connect to server: %v", err) - } - defer conn.Close() - - game.Player.ID = playerID - modelIndex := int(playerID) % len(game.Models) - game.Player.Model = game.Models[modelIndex].Model - game.Player.Texture = game.Models[modelIndex].Texture - - go network.HandleServerCommunication(conn, playerID, game.Player, game.OtherPlayers, game.QuitChan) - rl.PlayMusicStream(game.Music) rl.SetMusicVolume(game.Music, 0.5) - rl.SetTargetFPS(60) for !rl.WindowShouldClose() { - rl.UpdateMusicStream(game.Music) deltaTime := rl.GetFrameTime() - + rl.UpdateMusicStream(game.Music) game.Update(deltaTime) game.Render() } diff --git a/network/network.go b/network/network.go index 8b21bb6..c68953d 100644 --- a/network/network.go +++ b/network/network.go @@ -3,56 +3,89 @@ package network import ( "bufio" "encoding/binary" + "fmt" "io" "log" "net" "time" - "gitea.boner.be/bdnugget/goonscape/game" "gitea.boner.be/bdnugget/goonscape/types" pb "gitea.boner.be/bdnugget/goonserver/actions" + rl "github.com/gen2brain/raylib-go/raylib" "google.golang.org/protobuf/proto" ) +const protoVersion = 1 + var serverAddr = "boner.be:6969" func SetServerAddr(addr string) { serverAddr = addr } -func ConnectToServer() (net.Conn, int32, error) { +func ConnectToServer(username, password string, isRegistering bool) (net.Conn, int32, error) { conn, err := net.Dial("tcp", serverAddr) if err != nil { log.Printf("Failed to dial server: %v", err) return nil, 0, err } - log.Println("Connected to server. Waiting for player ID...") - reader := bufio.NewReader(conn) + log.Println("Connected to server. Authenticating...") - // Read message length (4 bytes) + // Send auth message + authAction := &pb.Action{ + Type: pb.Action_LOGIN, + Username: username, + Password: password, + } + if isRegistering { + authAction.Type = pb.Action_REGISTER + } + + authBatch := &pb.ActionBatch{ + Actions: []*pb.Action{authAction}, + ProtocolVersion: protoVersion, + } + + if err := writeMessage(conn, authBatch); err != nil { + conn.Close() + return nil, 0, fmt.Errorf("failed to send auth: %v", err) + } + + // Read server response + reader := bufio.NewReader(conn) lengthBuf := make([]byte, 4) if _, err := io.ReadFull(reader, lengthBuf); err != nil { - log.Printf("Failed to read message length: %v", err) - return nil, 0, err + conn.Close() + return nil, 0, fmt.Errorf("failed to read auth response: %v", err) } messageLength := binary.BigEndian.Uint32(lengthBuf) - // Read the full message messageBuf := make([]byte, messageLength) if _, err := io.ReadFull(reader, messageBuf); err != nil { - log.Printf("Failed to read message body: %v", err) - return nil, 0, err + conn.Close() + return nil, 0, fmt.Errorf("failed to read auth response body: %v", err) } var response pb.ServerMessage if err := proto.Unmarshal(messageBuf, &response); err != nil { - log.Printf("Failed to unmarshal server response: %v", err) - return nil, 0, err + conn.Close() + return nil, 0, fmt.Errorf("failed to unmarshal auth response: %v", err) + } + + if response.ProtocolVersion > protoVersion { + conn.Close() + return nil, 0, fmt.Errorf("server requires newer protocol version (server: %d, client: %d)", + response.ProtocolVersion, protoVersion) + } + + if !response.AuthSuccess { + conn.Close() + return nil, 0, fmt.Errorf(response.ErrorMessage) } playerID := response.GetPlayerId() - log.Printf("Successfully connected with player ID: %d", playerID) + log.Printf("Successfully authenticated with player ID: %d", playerID) return conn, playerID, nil } @@ -162,6 +195,17 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play for _, state := range serverMessage.Players { if state.PlayerId == playerID { + player.Lock() + // Update initial position if not set + if player.PosActual.X == 0 && player.PosActual.Z == 0 { + player.PosActual = rl.Vector3{ + X: float32(state.X * types.TileSize), + Y: 0, + Z: float32(state.Y * types.TileSize), + } + player.PosTile = types.Tile{X: int(state.X), Y: int(state.Y)} + } + player.Unlock() continue } @@ -172,8 +216,8 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play } } - if g, ok := player.UserData.(*game.Game); ok && len(serverMessage.ChatMessages) > 0 { - g.Chat.HandleServerMessages(serverMessage.ChatMessages) + if handler, ok := player.UserData.(types.ChatMessageHandler); ok && len(serverMessage.ChatMessages) > 0 { + handler.HandleServerMessages(serverMessage.ChatMessages) } } } diff --git a/types/types.go b/types/types.go index c47a0c4..3598281 100644 --- a/types/types.go +++ b/types/types.go @@ -39,6 +39,7 @@ type ModelAsset struct { type ChatMessage struct { PlayerID int32 + Username string Content string Time time.Time } @@ -49,6 +50,10 @@ type FloatingMessage struct { ScreenPos rl.Vector2 // Store the screen position for 2D rendering } +type ChatMessageHandler interface { + HandleServerMessages([]*pb.ChatMessage) +} + const ( MapWidth = 50 MapHeight = 50