diff --git a/assets/assets.go b/assets/assets.go index 0fea9ae..4368cbf 100644 --- a/assets/assets.go +++ b/assets/assets.go @@ -85,3 +85,19 @@ func LoadModels() ([]types.ModelAsset, error) { func LoadMusic(filename string) (rl.Music, error) { return rl.LoadMusicStream(filename), nil } + +func UnloadModels(models []types.ModelAsset) { + for _, model := range models { + if model.Animation != nil { + for i := int32(0); i < model.AnimFrames; i++ { + rl.UnloadModelAnimation(model.Animation[i]) + } + } + rl.UnloadModel(model.Model) + rl.UnloadTexture(model.Texture) + } +} + +func UnloadMusic(music rl.Music) { + rl.UnloadMusicStream(music) +} diff --git a/game/chat.go b/game/chat.go index f6b3ce9..9aced0d 100644 --- a/game/chat.go +++ b/game/chat.go @@ -2,7 +2,6 @@ package game import ( "fmt" - "sync" "time" "gitea.boner.be/bdnugget/goonscape/types" @@ -26,7 +25,6 @@ type Chat struct { cursorPos int scrollOffset int userData interface{} - mutex sync.RWMutex } func NewChat() *Chat { @@ -51,9 +49,6 @@ func (c *Chat) AddMessage(playerID int32, content string) { } func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) { - c.mutex.Lock() - defer c.mutex.Unlock() - // Convert protobuf messages to our local type for _, msg := range messages { localMsg := types.ChatMessage{ @@ -99,9 +94,6 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) { } func (c *Chat) Draw(screenWidth, screenHeight int32) { - c.mutex.RLock() - defer c.mutex.RUnlock() - // Calculate chat window width based on screen width chatWindowWidth := screenWidth - (chatMargin * 2) diff --git a/game/game.go b/game/game.go index 9df3433..2a26f9a 100644 --- a/game/game.go +++ b/game/game.go @@ -1,8 +1,7 @@ package game import ( - "fmt" - "sync" + "os" "time" "gitea.boner.be/bdnugget/goonscape/assets" @@ -20,10 +19,9 @@ type Game struct { Music rl.Music Chat *Chat MenuOpen bool - quitChan chan struct{} + QuitChan chan struct{} // Channel to signal shutdown loginScreen *LoginScreen isLoggedIn bool - cleanupOnce sync.Once } func New() *Game { @@ -38,7 +36,7 @@ func New() *Game { Projection: rl.CameraPerspective, }, Chat: NewChat(), - quitChan: make(chan struct{}), + QuitChan: make(chan struct{}), loginScreen: NewLoginScreen(), } game.Chat.userData = game @@ -46,32 +44,15 @@ func New() *Game { } func (g *Game) LoadAssets() error { - var loadErr error - defer func() { - if r := recover(); r != nil { - loadErr = fmt.Errorf("panic during asset loading: %v", r) - // Cleanup any partially loaded assets - g.Cleanup() - } - }() - - // Load models with better error handling - g.Models, loadErr = assets.LoadModels() - if loadErr != nil { - return fmt.Errorf("failed to load models: %v", loadErr) + var err error + g.Models, err = assets.LoadModels() + if err != nil { + return err } - // Verify model loading - for i, model := range g.Models { - if model.Model.Meshes == nil { - return fmt.Errorf("model %d failed to load properly", i) - } - } - - // Load music with better error handling - g.Music, loadErr = assets.LoadMusic("resources/audio/GoonScape2.mp3") - if loadErr != nil { - return fmt.Errorf("failed to load music: %v", loadErr) + g.Music, err = assets.LoadMusic("resources/audio/GoonScape2.mp3") + if err != nil { + return err } return nil @@ -96,9 +77,11 @@ func (g *Game) Update(deltaTime float32) { } g.AssignModelToPlayer(g.Player) - go network.HandleServerCommunication(conn, playerID, g.Player, g.OtherPlayers, g.quitChan) + go network.HandleServerCommunication(conn, playerID, g.Player, g.OtherPlayers, g.QuitChan) g.isLoggedIn = true + return } + g.loginScreen.Draw() return } @@ -291,20 +274,8 @@ func (g *Game) Render() { } func (g *Game) Cleanup() { - g.cleanupOnce.Do(func() { - // Stop music first - if g.Music.Stream.Buffer != nil { - rl.StopMusicStream(g.Music) - rl.UnloadMusicStream(g.Music) - } - - // Unload textures - for _, model := range g.Models { - if model.Texture.ID > 0 { - rl.UnloadTexture(model.Texture) - } - } - }) + assets.UnloadModels(g.Models) + assets.UnloadMusic(g.Music) } func (g *Game) HandleInput() { @@ -384,7 +355,10 @@ func (g *Game) DrawMenu() { } func (g *Game) Shutdown() { - close(g.quitChan) + close(g.QuitChan) + <-g.Player.QuitDone + rl.CloseWindow() + os.Exit(0) } func (g *Game) HandleServerMessages(messages []*pb.ChatMessage) { @@ -399,7 +373,3 @@ func (g *Game) AssignModelToPlayer(player *types.Player) { player.Model = modelAsset.Model player.Texture = modelAsset.Texture } - -func (g *Game) QuitChan() <-chan struct{} { - return g.quitChan -} diff --git a/main.go b/main.go index 165291c..7b1f06f 100644 --- a/main.go +++ b/main.go @@ -3,10 +3,7 @@ package main import ( "flag" "log" - "os" - "os/signal" "strings" - "syscall" "gitea.boner.be/bdnugget/goonscape/game" "gitea.boner.be/bdnugget/goonscape/network" @@ -14,24 +11,11 @@ import ( ) func main() { - defer func() { - if r := recover(); r != nil { - log.Printf("Recovered from panic in main: %v", r) - } - }() - // Parse command line flags - verbose := flag.Bool("v", false, "Also show info logs (spammy)") local := flag.Bool("local", false, "Connect to local server") addr := flag.String("addr", "", "Server address (host or host:port)") flag.Parse() - if *verbose { - rl.SetTraceLogLevel(rl.LogTrace) - } else { - rl.SetTraceLogLevel(rl.LogWarning) - } - // Set server address based on flags if *local { if *addr != "" { @@ -48,63 +32,29 @@ func main() { rl.InitWindow(1024, 768, "GoonScape") rl.SetExitKey(0) + defer rl.CloseWindow() + rl.InitAudioDevice() - - gameInstance := game.New() - if err := gameInstance.LoadAssets(); err != nil { - log.Printf("Failed to load assets: %v", err) - return - } - - defer func() { - gameInstance.Cleanup() - rl.CloseWindow() - rl.CloseAudioDevice() - }() + defer rl.CloseAudioDevice() rl.SetTargetFPS(60) - rl.PlayMusicStream(gameInstance.Music) - rl.SetMusicVolume(gameInstance.Music, 0.5) + game := game.New() + if err := game.LoadAssets(); err != nil { + log.Fatalf("Failed to load assets: %v", err) + } + defer game.Cleanup() - // Handle OS signals for clean shutdown - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) - go func() { - <-sigChan - if gameInstance != nil { - gameInstance.Shutdown() - } - }() + rl.PlayMusicStream(game.Music) + rl.SetMusicVolume(game.Music, 0.5) - // Keep game loop in main thread for Raylib for !rl.WindowShouldClose() { deltaTime := rl.GetFrameTime() - rl.UpdateMusicStream(gameInstance.Music) - - func() { - defer func() { - if r := recover(); r != nil { - log.Printf("Recovered from panic in game update: %v", r) - } - }() - gameInstance.Update(deltaTime) - }() - - func() { - defer func() { - if r := recover(); r != nil { - log.Printf("Recovered from panic in game render: %v", r) - } - }() - gameInstance.Render() - }() - - // Check if game requested shutdown - select { - case <-gameInstance.QuitChan(): - return - default: - } + rl.UpdateMusicStream(game.Music) + game.Update(deltaTime) + game.Render() } + + // Wait for clean shutdown + <-game.QuitChan } diff --git a/network/network.go b/network/network.go index cf749dd..21e57ef 100644 --- a/network/network.go +++ b/network/network.go @@ -7,7 +7,6 @@ import ( "io" "log" "net" - "sync" "time" "gitea.boner.be/bdnugget/goonscape/types" @@ -92,32 +91,19 @@ func ConnectToServer(username, password string, isRegistering bool) (net.Conn, i func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Player, otherPlayers map[int32]*types.Player, quitChan <-chan struct{}) { reader := bufio.NewReader(conn) - defer func() { - if r := recover(); r != nil { - log.Printf("Recovered from panic in HandleServerCommunication: %v", r) - } - conn.Close() - if player.QuitDone != nil { - close(player.QuitDone) - } - }() actionTicker := time.NewTicker(types.ClientTickRate) defer actionTicker.Stop() + defer conn.Close() + defer close(player.QuitDone) - // Create error channel for goroutine communication - errChan := make(chan error, 1) + // Create a channel to signal when goroutines are done done := make(chan struct{}) - // Start message sending goroutine - go func() { - defer func() { - if r := recover(); r != nil { - log.Printf("Recovered from panic in message sender: %v", r) - errChan <- fmt.Errorf("message sender panic: %v", r) - } - }() + // Create a set of current players to track disconnects + currentPlayers := make(map[int32]bool) + go func() { for { select { case <-quitChan: @@ -132,23 +118,23 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play writeMessage(conn, disconnectMsg) done <- struct{}{} return - case <-done: - 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, } + player.ActionQueue = player.ActionQueue[:0] player.Unlock() if err := writeMessage(conn, batch); err != nil { - errChan <- err + log.Printf("Failed to send actions to server: %v", err) return } } else { @@ -158,106 +144,93 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play } }() - // Main message receiving loop - go func() { - defer func() { - if r := recover(); r != nil { - log.Printf("Recovered from panic in message receiver: %v", r) - errChan <- fmt.Errorf("message receiver panic: %v", r) - } - }() + for { + select { + case <-quitChan: + done := make(chan struct{}) + go func() { + <-done + close(player.QuitDone) + }() - for { select { - case <-quitChan: + case <-done: + time.Sleep(100 * time.Millisecond) + case <-time.After(1 * time.Second): + log.Println("Shutdown timed out") + } + return + 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 - default: - lengthBuf := make([]byte, 4) - if _, err := io.ReadFull(reader, lengthBuf); err != nil { - if err != io.EOF { - errChan <- fmt.Errorf("failed to read message length: %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 + } + + 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 } - return } - messageLength := binary.BigEndian.Uint32(lengthBuf) + } + player.Unlock() - 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) + for _, state := range serverMessage.Players { + currentPlayers[state.PlayerId] = true + 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 } - 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 - } - } - } - player.Unlock() - - 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 - } - - if otherPlayer, exists := otherPlayers[state.PlayerId]; exists { - otherPlayer.UpdatePosition(state, types.ServerTickRate) - } else { - otherPlayers[state.PlayerId] = types.NewPlayer(state) - } - } - - // Remove players that are no longer in the server state - for id := range otherPlayers { - if id != playerID { - delete(otherPlayers, id) - } - } - - if handler, ok := player.UserData.(types.ChatMessageHandler); ok && len(serverMessage.ChatMessages) > 0 { - handler.HandleServerMessages(serverMessage.ChatMessages) + if otherPlayer, exists := otherPlayers[state.PlayerId]; exists { + otherPlayer.UpdatePosition(state, types.ServerTickRate) + } else { + otherPlayers[state.PlayerId] = types.NewPlayer(state) } } - } - }() - // Wait for error or quit signal - select { - case <-quitChan: - // Send disconnect message - disconnectMsg := &pb.ActionBatch{ - PlayerId: playerID, - Actions: []*pb.Action{{ - Type: pb.Action_DISCONNECT, - PlayerId: playerID, - }}, + // Remove players that are no longer in the server state + for id := range otherPlayers { + if !currentPlayers[id] { + delete(otherPlayers, id) + } + } + + if handler, ok := player.UserData.(types.ChatMessageHandler); ok && len(serverMessage.ChatMessages) > 0 { + handler.HandleServerMessages(serverMessage.ChatMessages) + } } - writeMessage(conn, disconnectMsg) - case err := <-errChan: - log.Printf("Network error: %v", err) } } @@ -279,50 +252,3 @@ func writeMessage(conn net.Conn, msg proto.Message) error { _, err = conn.Write(data) return err } - -type Connection struct { - conn net.Conn - playerID int32 - quitChan chan struct{} - quitDone chan struct{} - closeOnce sync.Once -} - -func NewConnection(username, password string, isRegistering bool) (*Connection, error) { - conn, playerID, err := ConnectToServer(username, password, isRegistering) - if err != nil { - return nil, err - } - return &Connection{ - conn: conn, - playerID: playerID, - quitChan: make(chan struct{}), - quitDone: make(chan struct{}), - }, nil -} - -func (c *Connection) Close() { - c.closeOnce.Do(func() { - close(c.quitChan) - // Wait with timeout for network cleanup - select { - case <-c.quitDone: - // Clean shutdown completed - case <-time.After(500 * time.Millisecond): - log.Println("Network cleanup timed out") - } - c.conn.Close() - }) -} - -func (c *Connection) PlayerID() int32 { - return c.playerID -} - -func (c *Connection) Start(player *types.Player, otherPlayers map[int32]*types.Player) { - go HandleServerCommunication(c.conn, c.playerID, player, otherPlayers, c.quitChan) -} - -func (c *Connection) QuitChan() <-chan struct{} { - return c.quitChan -} diff --git a/types/player.go b/types/player.go index cfd8473..747e39f 100644 --- a/types/player.go +++ b/types/player.go @@ -1,34 +1,12 @@ package types import ( - "sync" "time" pb "gitea.boner.be/bdnugget/goonserver/actions" rl "github.com/gen2brain/raylib-go/raylib" ) -type Player struct { - sync.RWMutex - Model rl.Model - Texture rl.Texture2D - PosActual rl.Vector3 - PosTile Tile - TargetPath []Tile - Speed float32 - ActionQueue []*pb.Action - ID int32 - QuitDone chan struct{} - CurrentTick int64 - UserData interface{} - FloatingMessage *FloatingMessage - IsMoving bool - AnimationFrame int32 - LastAnimUpdate time.Time - LastUpdateTime time.Time - InterpolationProgress float32 -} - func (p *Player) MoveTowards(target Tile, deltaTime float32, mapGrid [][]Tile) { p.Lock() defer p.Unlock() @@ -90,14 +68,12 @@ func NewPlayer(state *pb.PlayerState) *Player { Y: float32(state.Y * TileHeight), Z: float32(state.Y * TileSize), }, - PosTile: Tile{X: int(state.X), Y: int(state.Y)}, - Speed: 50.0, - ID: state.PlayerId, - IsMoving: false, - AnimationFrame: 0, - LastAnimUpdate: time.Now(), - LastUpdateTime: time.Now(), - InterpolationProgress: 1.0, + PosTile: Tile{X: int(state.X), Y: int(state.Y)}, + Speed: 50.0, + ID: state.PlayerId, + IsMoving: false, + AnimationFrame: 0, + LastAnimUpdate: time.Now(), } } diff --git a/types/types.go b/types/types.go index f15c8af..49aa718 100644 --- a/types/types.go +++ b/types/types.go @@ -1,6 +1,7 @@ package types import ( + "sync" "time" pb "gitea.boner.be/bdnugget/goonserver/actions" @@ -13,6 +14,27 @@ type Tile struct { Walkable bool } +type Player struct { + sync.Mutex + PosActual rl.Vector3 + PosTile Tile + TargetPath []Tile + ActionQueue []*pb.Action + Speed float32 + Model rl.Model + Texture rl.Texture2D + ID int32 + CurrentTick int64 + LastUpdateTime time.Time + LastAnimUpdate time.Time + InterpolationProgress float32 + UserData interface{} + FloatingMessage *FloatingMessage + QuitDone chan struct{} + AnimationFrame int32 + IsMoving bool +} + type AnimationSet struct { Idle []rl.ModelAnimation Walk []rl.ModelAnimation