From fb018e2a7dd01436924ac99c07d76aa02891a3b2 Mon Sep 17 00:00:00 2001 From: bdnugget Date: Sat, 18 Jan 2025 22:23:08 +0100 Subject: [PATCH 1/3] Add menu on esc and don't close on esc --- game/chat.go | 3 +- game/game.go | 101 ++++++++++++++++++++++++++++++++++++++------------- main.go | 1 + 3 files changed, 78 insertions(+), 27 deletions(-) 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..70d6e3a 100644 --- a/game/game.go +++ b/game/game.go @@ -16,6 +16,7 @@ type Game struct { Models []types.ModelAsset Music rl.Music Chat *Chat + MenuOpen bool } func New() *Game { @@ -59,6 +60,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,36 +180,17 @@ func (g *Game) Render() { } rl.EndMode3D() - drawFloatingMessage := func(msg *types.FloatingMessage) { - if msg == nil || time.Now().After(msg.ExpireTime) { - return - } - pos := msg.ScreenPos - text := msg.Content - textWidth := rl.MeasureText(text, 20) - - for offsetX := -2; offsetX <= 2; offsetX++ { - for offsetY := -2; offsetY <= 2; offsetY++ { - rl.DrawText(text, - int32(pos.X)-textWidth/2+int32(offsetX), - int32(pos.Y)+int32(offsetY), - 20, - rl.Black) - } - } - rl.DrawText(text, int32(pos.X)-textWidth/2, int32(pos.Y), 20, rl.Yellow) + // Draw menu if open + if g.MenuOpen { + g.DrawMenu() } - if g.Player.FloatingMessage != nil { - drawFloatingMessage(g.Player.FloatingMessage) - } - - for _, other := range g.OtherPlayers { - drawFloatingMessage(other.FloatingMessage) + // 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 +216,61 @@ 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": + 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/main.go b/main.go index dd5c18e..fbc399a 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() From d86cbe15a3dbe60da8026d52b8646adf9964df2d Mon Sep 17 00:00:00 2001 From: bdnugget Date: Sat, 18 Jan 2025 22:27:22 +0100 Subject: [PATCH 2/3] add quit channel to clean up after self --- game/game.go | 5 +- main.go | 5 +- network/network.go | 135 ++++++++++++++++++++++++--------------------- 3 files changed, 80 insertions(+), 65 deletions(-) diff --git a/game/game.go b/game/game.go index 70d6e3a..270ea19 100644 --- a/game/game.go +++ b/game/game.go @@ -17,6 +17,7 @@ type Game struct { Music rl.Music Chat *Chat MenuOpen bool + QuitChan chan struct{} // Channel to signal shutdown } func New() *Game { @@ -37,7 +38,8 @@ func New() *Game { Fovy: 45.0, Projection: rl.CameraPerspective, }, - Chat: NewChat(), + Chat: NewChat(), + QuitChan: make(chan struct{}), } game.Player.UserData = game game.Chat.userData = game @@ -259,6 +261,7 @@ func (g *Game) DrawMenu() { case "Settings": // TODO: Implement settings case "Exit Game": + close(g.QuitChan) // Signal all goroutines to shut down rl.CloseWindow() } } diff --git a/main.go b/main.go index fbc399a..9c8fb43 100644 --- a/main.go +++ b/main.go @@ -52,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) @@ -65,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..65779ba 100644 --- a/network/network.go +++ b/network/network.go @@ -56,89 +56,98 @@ 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() go func() { - for range actionTicker.C { - player.Lock() - if len(player.ActionQueue) > 0 { - actions := make([]*pb.Action, len(player.ActionQueue)) - copy(actions, player.ActionQueue) + for { + select { + case <-quitChan: + return + case <-actionTicker.C: + player.Lock() + if len(player.ActionQueue) > 0 { + actions := make([]*pb.Action, len(player.ActionQueue)) + copy(actions, player.ActionQueue) - batch := &pb.ActionBatch{ - PlayerId: playerID, - Actions: actions, - Tick: player.CurrentTick, + batch := &pb.ActionBatch{ + PlayerId: playerID, + Actions: actions, + Tick: player.CurrentTick, + } + + 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() } - - 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() } } }() 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: 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) + } } } } From b96c7ada7a74ffaae314a5d023464c58da102805 Mon Sep 17 00:00:00 2001 From: bdnugget Date: Sat, 18 Jan 2025 23:23:03 +0100 Subject: [PATCH 3/3] Menu with broken exit and settings --- game/game.go | 35 ++++++++++++++++++++++++++++++++++- go.mod | 4 ++-- go.sum | 16 ++-------------- goonserver | 2 +- network/network.go | 18 ++++++++++++++++++ types/types.go | 3 ++- 6 files changed, 59 insertions(+), 19 deletions(-) diff --git a/game/game.go b/game/game.go index 270ea19..b46441e 100644 --- a/game/game.go +++ b/game/game.go @@ -18,6 +18,7 @@ type Game struct { 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 { @@ -29,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{ @@ -40,6 +42,7 @@ func New() *Game { }, Chat: NewChat(), QuitChan: make(chan struct{}), + QuitDone: make(chan struct{}), } game.Player.UserData = game game.Chat.userData = game @@ -182,6 +185,35 @@ func (g *Game) Render() { } rl.EndMode3D() + // Draw floating messages + drawFloatingMessage := func(msg *types.FloatingMessage) { + if msg == nil || time.Now().After(msg.ExpireTime) { + return + } + pos := msg.ScreenPos + text := msg.Content + textWidth := rl.MeasureText(text, 20) + + for offsetX := -2; offsetX <= 2; offsetX++ { + for offsetY := -2; offsetY <= 2; offsetY++ { + rl.DrawText(text, + int32(pos.X)-textWidth/2+int32(offsetX), + int32(pos.Y)+int32(offsetY), + 20, + rl.Black) + } + } + rl.DrawText(text, int32(pos.X)-textWidth/2, int32(pos.Y), 20, rl.Yellow) + } + + if g.Player.FloatingMessage != nil { + drawFloatingMessage(g.Player.FloatingMessage) + } + + for _, other := range g.OtherPlayers { + drawFloatingMessage(other.FloatingMessage) + } + // Draw menu if open if g.MenuOpen { g.DrawMenu() @@ -261,7 +293,8 @@ func (g *Game) DrawMenu() { case "Settings": // TODO: Implement settings case "Exit Game": - close(g.QuitChan) // Signal all goroutines to shut down + close(g.QuitChan) + <-g.QuitDone rl.CloseWindow() } } 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/network/network.go b/network/network.go index 65779ba..feb8f4b 100644 --- a/network/network.go +++ b/network/network.go @@ -61,11 +61,26 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play 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 { select { case <-quitChan: + // Send disconnect message to server + disconnectMsg := &pb.ActionBatch{ + PlayerId: playerID, + Actions: []*pb.Action{{ + Type: pb.Action_DISCONNECT, + PlayerId: playerID, + }}, + } + writeMessage(conn, disconnectMsg) + done <- struct{}{} return case <-actionTicker.C: player.Lock() @@ -96,6 +111,9 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play for { 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 default: // Read message length (4 bytes) 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 {