diff --git a/game/chat.go b/game/chat.go index 98f5abe..ea0dba1 100644 --- a/game/chat.go +++ b/game/chat.go @@ -168,13 +168,12 @@ func (c *Chat) Update() (string, bool) { c.inputBuffer = c.inputBuffer[:0] c.cursorPos = 0 c.isTyping = false - return message, true } c.isTyping = false } - if rl.IsKeyPressed(rl.KeyEscape) { + if rl.IsKeyPressed(rl.KeyEscape) && c.isTyping { c.inputBuffer = c.inputBuffer[:0] c.cursorPos = 0 c.isTyping = false diff --git a/game/game.go b/game/game.go index 8220586..b46441e 100644 --- a/game/game.go +++ b/game/game.go @@ -16,6 +16,9 @@ type Game struct { Models []types.ModelAsset Music rl.Music Chat *Chat + MenuOpen bool + QuitChan chan struct{} // Channel to signal shutdown + QuitDone chan struct{} // New channel to signal when cleanup is complete } func New() *Game { @@ -27,6 +30,7 @@ func New() *Game { Speed: 50.0, TargetPath: []types.Tile{}, UserData: nil, + QuitDone: make(chan struct{}), }, OtherPlayers: make(map[int32]*types.Player), Camera: rl.Camera3D{ @@ -36,7 +40,9 @@ func New() *Game { Fovy: 45.0, Projection: rl.CameraPerspective, }, - Chat: NewChat(), + Chat: NewChat(), + QuitChan: make(chan struct{}), + QuitDone: make(chan struct{}), } game.Player.UserData = game game.Chat.userData = game @@ -59,6 +65,17 @@ func (g *Game) LoadAssets() error { } func (g *Game) Update(deltaTime float32) { + // Handle ESC for menu + if rl.IsKeyPressed(rl.KeyEscape) { + g.MenuOpen = !g.MenuOpen + return + } + + // Don't process other inputs if menu is open + if g.MenuOpen { + return + } + if message, sent := g.Chat.Update(); sent { g.Player.Lock() g.Player.ActionQueue = append(g.Player.ActionQueue, &pb.Action{ @@ -168,6 +185,7 @@ func (g *Game) Render() { } rl.EndMode3D() + // Draw floating messages drawFloatingMessage := func(msg *types.FloatingMessage) { if msg == nil || time.Now().After(msg.ExpireTime) { return @@ -196,8 +214,17 @@ func (g *Game) Render() { drawFloatingMessage(other.FloatingMessage) } + // Draw menu if open + if g.MenuOpen { + g.DrawMenu() + } + + // Only draw chat if menu is not open + if !g.MenuOpen { + g.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight())) + } + rl.DrawFPS(10, 10) - g.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight())) rl.EndDrawing() } @@ -223,3 +250,63 @@ func (g *Game) HandleInput() { } } } + +func (g *Game) DrawMenu() { + screenWidth := float32(rl.GetScreenWidth()) + screenHeight := float32(rl.GetScreenHeight()) + + // Semi-transparent background + rl.DrawRectangle(0, 0, int32(screenWidth), int32(screenHeight), rl.ColorAlpha(rl.Black, 0.7)) + + // Menu title + title := "Menu" + titleSize := int32(40) + titleWidth := rl.MeasureText(title, titleSize) + rl.DrawText(title, int32(screenWidth/2)-titleWidth/2, 100, titleSize, rl.White) + + // Menu buttons + buttonWidth := float32(200) + buttonHeight := float32(40) + buttonY := float32(200) + buttonSpacing := float32(60) + + menuItems := []string{"Resume", "Settings", "Exit Game"} + for _, item := range menuItems { + buttonRect := rl.Rectangle{ + X: screenWidth/2 - buttonWidth/2, + Y: buttonY, + Width: buttonWidth, + Height: buttonHeight, + } + + // Check mouse hover + mousePoint := rl.GetMousePosition() + mouseHover := rl.CheckCollisionPointRec(mousePoint, buttonRect) + + // Draw button + if mouseHover { + rl.DrawRectangleRec(buttonRect, rl.ColorAlpha(rl.White, 0.3)) + if rl.IsMouseButtonPressed(rl.MouseLeftButton) { + switch item { + case "Resume": + g.MenuOpen = false + case "Settings": + // TODO: Implement settings + case "Exit Game": + close(g.QuitChan) + <-g.QuitDone + rl.CloseWindow() + } + } + } + + // Draw button text + textSize := int32(20) + textWidth := rl.MeasureText(item, textSize) + textX := int32(buttonRect.X+buttonRect.Width/2) - textWidth/2 + textY := int32(buttonRect.Y + buttonRect.Height/2 - float32(textSize)/2) + rl.DrawText(item, textX, textY, textSize, rl.White) + + buttonY += buttonSpacing + } +} diff --git a/go.mod b/go.mod index 53e5898..285c671 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.23.0 require ( gitea.boner.be/bdnugget/goonserver v0.0.0-20250113131525-49e23114973c github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b - google.golang.org/protobuf v1.36.2 + google.golang.org/protobuf v1.36.3 ) require ( @@ -14,4 +14,4 @@ require ( golang.org/x/sys v0.29.0 // indirect ) -// replace gitea.boner.be/bdnugget/goonserver => ./goonserver +replace gitea.boner.be/bdnugget/goonserver => ./goonserver diff --git a/go.sum b/go.sum index 4152fb6..c8e48ea 100644 --- a/go.sum +++ b/go.sum @@ -1,24 +1,12 @@ -gitea.boner.be/bdnugget/goonserver v0.0.0-20250113131525-49e23114973c h1:TO14y5QeQXn6sLCv6vORVdjnMn5hP/Vd+60UjqcrtFA= -gitea.boner.be/bdnugget/goonserver v0.0.0-20250113131525-49e23114973c/go.mod h1:inR1bKrr/vcTba+G1KzmmY6vssMq9oGNOk836VwPa4c= -github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE= -github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= -github.com/gen2brain/raylib-go/raylib v0.0.0-20240930075631-c66f9e2942fe h1:mInjrbJkUglTM7tBmXG+epnPCE744aj15J7vjJwM4gs= -github.com/gen2brain/raylib-go/raylib v0.0.0-20240930075631-c66f9e2942fe/go.mod h1:BaY76bZk7nw1/kVOSQObPY1v1iwVE1KHAGMfvI6oK1Q= github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b h1:JJfspevP3YOXcSKVABizYOv++yMpTJIdPUtoDzF/RWw= github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b/go.mod h1:BaY76bZk7nw1/kVOSQObPY1v1iwVE1KHAGMfvI6oK1Q= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= -golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU= -google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= diff --git a/goonserver b/goonserver index b73d8de..b44cdab 160000 --- a/goonserver +++ b/goonserver @@ -1 +1 @@ -Subproject commit b73d8de85149e8f6d6fe1e8d69562a0a26f7df23 +Subproject commit b44cdab6114914ba2bd66049541447d3dd1f9fcb diff --git a/main.go b/main.go index dd5c18e..9c8fb43 100644 --- a/main.go +++ b/main.go @@ -30,6 +30,7 @@ func main() { } rl.InitWindow(1024, 768, "GoonScape") + rl.SetExitKey(0) defer rl.CloseWindow() rl.InitAudioDevice() defer rl.CloseAudioDevice() @@ -51,7 +52,7 @@ func main() { game.Player.Model = game.Models[modelIndex].Model game.Player.Texture = game.Models[modelIndex].Texture - go network.HandleServerCommunication(conn, playerID, game.Player, game.OtherPlayers) + go network.HandleServerCommunication(conn, playerID, game.Player, game.OtherPlayers, game.QuitChan) rl.PlayMusicStream(game.Music) rl.SetMusicVolume(game.Music, 0.5) @@ -64,4 +65,7 @@ func main() { game.Update(deltaTime) game.Render() } + + // Wait for clean shutdown + <-game.QuitChan } diff --git a/network/network.go b/network/network.go index 02da330..feb8f4b 100644 --- a/network/network.go +++ b/network/network.go @@ -56,89 +56,116 @@ func ConnectToServer() (net.Conn, int32, error) { return conn, playerID, nil } -func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Player, otherPlayers map[int32]*types.Player) { - // Create a buffered reader for the connection +func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Player, otherPlayers map[int32]*types.Player, quitChan <-chan struct{}) { reader := bufio.NewReader(conn) actionTicker := time.NewTicker(types.ClientTickRate) defer actionTicker.Stop() + defer conn.Close() + defer close(player.QuitDone) + + // Create a channel to signal when goroutines are done + done := make(chan struct{}) go func() { - for range actionTicker.C { - player.Lock() - if len(player.ActionQueue) > 0 { - actions := make([]*pb.Action, len(player.ActionQueue)) - copy(actions, player.ActionQueue) - - batch := &pb.ActionBatch{ + for { + select { + case <-quitChan: + // Send disconnect message to server + disconnectMsg := &pb.ActionBatch{ PlayerId: playerID, - Actions: actions, - Tick: player.CurrentTick, + Actions: []*pb.Action{{ + Type: pb.Action_DISCONNECT, + PlayerId: playerID, + }}, } + writeMessage(conn, disconnectMsg) + done <- struct{}{} + return + case <-actionTicker.C: + player.Lock() + if len(player.ActionQueue) > 0 { + actions := make([]*pb.Action, len(player.ActionQueue)) + copy(actions, player.ActionQueue) - player.ActionQueue = player.ActionQueue[:0] - player.Unlock() + batch := &pb.ActionBatch{ + PlayerId: playerID, + Actions: actions, + Tick: player.CurrentTick, + } - if err := writeMessage(conn, batch); err != nil { - log.Printf("Failed to send actions to server: %v", err) - return + player.ActionQueue = player.ActionQueue[:0] + player.Unlock() + + if err := writeMessage(conn, batch); err != nil { + log.Printf("Failed to send actions to server: %v", err) + return + } + } else { + player.Unlock() } - } else { - player.Unlock() } } }() for { - // Read message length (4 bytes) - lengthBuf := make([]byte, 4) - if _, err := io.ReadFull(reader, lengthBuf); err != nil { - log.Printf("Failed to read message length: %v", err) + select { + case <-quitChan: + <-done // Wait for action goroutine to finish + close(done) + time.Sleep(100 * time.Millisecond) // Give time for disconnect message to be sent return - } - 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 - } - - var serverMessage pb.ServerMessage - if err := proto.Unmarshal(messageBuf, &serverMessage); err != nil { - log.Printf("Failed to unmarshal server message: %v", err) - continue - } - - player.Lock() - player.CurrentTick = serverMessage.CurrentTick - - tickDiff := serverMessage.CurrentTick - player.CurrentTick - if tickDiff > types.MaxTickDesync { - for _, state := range serverMessage.Players { - if state.PlayerId == playerID { - player.ForceResync(state) - break - } + default: + // Read message length (4 bytes) + lengthBuf := make([]byte, 4) + if _, err := io.ReadFull(reader, lengthBuf); err != nil { + log.Printf("Failed to read message length: %v", err) + return } - } - player.Unlock() + messageLength := binary.BigEndian.Uint32(lengthBuf) - for _, state := range serverMessage.Players { - if state.PlayerId == playerID { + // 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 + } + + var serverMessage pb.ServerMessage + if err := proto.Unmarshal(messageBuf, &serverMessage); err != nil { + log.Printf("Failed to unmarshal server message: %v", err) continue } - if otherPlayer, exists := otherPlayers[state.PlayerId]; exists { - otherPlayer.UpdatePosition(state, types.ServerTickRate) - } else { - otherPlayers[state.PlayerId] = types.NewPlayer(state) - } - } + player.Lock() + player.CurrentTick = serverMessage.CurrentTick - if g, ok := player.UserData.(*game.Game); ok && len(serverMessage.ChatMessages) > 0 { - g.Chat.HandleServerMessages(serverMessage.ChatMessages) + tickDiff := serverMessage.CurrentTick - player.CurrentTick + if tickDiff > types.MaxTickDesync { + for _, state := range serverMessage.Players { + if state.PlayerId == playerID { + player.ForceResync(state) + break + } + } + } + player.Unlock() + + for _, state := range serverMessage.Players { + if state.PlayerId == playerID { + continue + } + + if otherPlayer, exists := otherPlayers[state.PlayerId]; exists { + otherPlayer.UpdatePosition(state, types.ServerTickRate) + } else { + otherPlayers[state.PlayerId] = types.NewPlayer(state) + } + } + + if g, ok := player.UserData.(*game.Game); ok && len(serverMessage.ChatMessages) > 0 { + g.Chat.HandleServerMessages(serverMessage.ChatMessages) + } } } } diff --git a/types/types.go b/types/types.go index 6062703..c47a0c4 100644 --- a/types/types.go +++ b/types/types.go @@ -27,8 +27,9 @@ type Player struct { CurrentTick int64 LastUpdateTime time.Time InterpolationProgress float32 - UserData interface{} // Used to store reference to game + UserData interface{} FloatingMessage *FloatingMessage + QuitDone chan struct{} } type ModelAsset struct {