Compare commits

..

No commits in common. "7be859d58fe719749ae4cde92ebc11ab87250319" and "220a45147551126767edd556a56cdb182872ad48" have entirely different histories.

7 changed files with 309 additions and 161 deletions

View File

@ -85,19 +85,3 @@ func LoadModels() ([]types.ModelAsset, error) {
func LoadMusic(filename string) (rl.Music, error) { func LoadMusic(filename string) (rl.Music, error) {
return rl.LoadMusicStream(filename), nil 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)
}

View File

@ -2,6 +2,7 @@ package game
import ( import (
"fmt" "fmt"
"sync"
"time" "time"
"gitea.boner.be/bdnugget/goonscape/types" "gitea.boner.be/bdnugget/goonscape/types"
@ -25,6 +26,7 @@ type Chat struct {
cursorPos int cursorPos int
scrollOffset int scrollOffset int
userData interface{} userData interface{}
mutex sync.RWMutex
} }
func NewChat() *Chat { func NewChat() *Chat {
@ -49,6 +51,9 @@ func (c *Chat) AddMessage(playerID int32, content string) {
} }
func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) { func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
c.mutex.Lock()
defer c.mutex.Unlock()
// Convert protobuf messages to our local type // Convert protobuf messages to our local type
for _, msg := range messages { for _, msg := range messages {
localMsg := types.ChatMessage{ localMsg := types.ChatMessage{
@ -94,6 +99,9 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
} }
func (c *Chat) Draw(screenWidth, screenHeight int32) { func (c *Chat) Draw(screenWidth, screenHeight int32) {
c.mutex.RLock()
defer c.mutex.RUnlock()
// Calculate chat window width based on screen width // Calculate chat window width based on screen width
chatWindowWidth := screenWidth - (chatMargin * 2) chatWindowWidth := screenWidth - (chatMargin * 2)

View File

@ -1,7 +1,8 @@
package game package game
import ( import (
"os" "fmt"
"sync"
"time" "time"
"gitea.boner.be/bdnugget/goonscape/assets" "gitea.boner.be/bdnugget/goonscape/assets"
@ -19,9 +20,10 @@ type Game struct {
Music rl.Music Music rl.Music
Chat *Chat Chat *Chat
MenuOpen bool MenuOpen bool
QuitChan chan struct{} // Channel to signal shutdown quitChan chan struct{}
loginScreen *LoginScreen loginScreen *LoginScreen
isLoggedIn bool isLoggedIn bool
cleanupOnce sync.Once
} }
func New() *Game { func New() *Game {
@ -36,7 +38,7 @@ func New() *Game {
Projection: rl.CameraPerspective, Projection: rl.CameraPerspective,
}, },
Chat: NewChat(), Chat: NewChat(),
QuitChan: make(chan struct{}), quitChan: make(chan struct{}),
loginScreen: NewLoginScreen(), loginScreen: NewLoginScreen(),
} }
game.Chat.userData = game game.Chat.userData = game
@ -44,15 +46,32 @@ func New() *Game {
} }
func (g *Game) LoadAssets() error { func (g *Game) LoadAssets() error {
var err error var loadErr error
g.Models, err = assets.LoadModels() defer func() {
if err != nil { if r := recover(); r != nil {
return err 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)
} }
g.Music, err = assets.LoadMusic("resources/audio/GoonScape2.mp3") // Verify model loading
if err != nil { for i, model := range g.Models {
return err 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)
} }
return nil return nil
@ -77,11 +96,9 @@ func (g *Game) Update(deltaTime float32) {
} }
g.AssignModelToPlayer(g.Player) 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 g.isLoggedIn = true
return
} }
g.loginScreen.Draw()
return return
} }
@ -274,8 +291,20 @@ func (g *Game) Render() {
} }
func (g *Game) Cleanup() { func (g *Game) Cleanup() {
assets.UnloadModels(g.Models) g.cleanupOnce.Do(func() {
assets.UnloadMusic(g.Music) // 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)
}
}
})
} }
func (g *Game) HandleInput() { func (g *Game) HandleInput() {
@ -355,10 +384,7 @@ func (g *Game) DrawMenu() {
} }
func (g *Game) Shutdown() { 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) { func (g *Game) HandleServerMessages(messages []*pb.ChatMessage) {
@ -373,3 +399,7 @@ func (g *Game) AssignModelToPlayer(player *types.Player) {
player.Model = modelAsset.Model player.Model = modelAsset.Model
player.Texture = modelAsset.Texture player.Texture = modelAsset.Texture
} }
func (g *Game) QuitChan() <-chan struct{} {
return g.quitChan
}

82
main.go
View File

@ -3,7 +3,10 @@ package main
import ( import (
"flag" "flag"
"log" "log"
"os"
"os/signal"
"strings" "strings"
"syscall"
"gitea.boner.be/bdnugget/goonscape/game" "gitea.boner.be/bdnugget/goonscape/game"
"gitea.boner.be/bdnugget/goonscape/network" "gitea.boner.be/bdnugget/goonscape/network"
@ -11,11 +14,24 @@ import (
) )
func main() { func main() {
defer func() {
if r := recover(); r != nil {
log.Printf("Recovered from panic in main: %v", r)
}
}()
// Parse command line flags // Parse command line flags
verbose := flag.Bool("v", false, "Also show info logs (spammy)")
local := flag.Bool("local", false, "Connect to local server") local := flag.Bool("local", false, "Connect to local server")
addr := flag.String("addr", "", "Server address (host or host:port)") addr := flag.String("addr", "", "Server address (host or host:port)")
flag.Parse() flag.Parse()
if *verbose {
rl.SetTraceLogLevel(rl.LogTrace)
} else {
rl.SetTraceLogLevel(rl.LogWarning)
}
// Set server address based on flags // Set server address based on flags
if *local { if *local {
if *addr != "" { if *addr != "" {
@ -32,29 +48,63 @@ func main() {
rl.InitWindow(1024, 768, "GoonScape") rl.InitWindow(1024, 768, "GoonScape")
rl.SetExitKey(0) rl.SetExitKey(0)
defer rl.CloseWindow()
rl.InitAudioDevice() rl.InitAudioDevice()
defer rl.CloseAudioDevice()
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()
}()
rl.SetTargetFPS(60) rl.SetTargetFPS(60)
game := game.New() rl.PlayMusicStream(gameInstance.Music)
if err := game.LoadAssets(); err != nil { rl.SetMusicVolume(gameInstance.Music, 0.5)
log.Fatalf("Failed to load assets: %v", err)
// 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()
} }
defer game.Cleanup() }()
rl.PlayMusicStream(game.Music)
rl.SetMusicVolume(game.Music, 0.5)
// Keep game loop in main thread for Raylib
for !rl.WindowShouldClose() { for !rl.WindowShouldClose() {
deltaTime := rl.GetFrameTime() deltaTime := rl.GetFrameTime()
rl.UpdateMusicStream(game.Music) rl.UpdateMusicStream(gameInstance.Music)
game.Update(deltaTime)
game.Render()
}
// Wait for clean shutdown func() {
<-game.QuitChan 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:
}
}
} }

View File

@ -7,6 +7,7 @@ import (
"io" "io"
"log" "log"
"net" "net"
"sync"
"time" "time"
"gitea.boner.be/bdnugget/goonscape/types" "gitea.boner.be/bdnugget/goonscape/types"
@ -91,19 +92,32 @@ 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{}) { func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Player, otherPlayers map[int32]*types.Player, quitChan <-chan struct{}) {
reader := bufio.NewReader(conn) 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) actionTicker := time.NewTicker(types.ClientTickRate)
defer actionTicker.Stop() defer actionTicker.Stop()
defer conn.Close()
defer close(player.QuitDone)
// Create a channel to signal when goroutines are done // Create error channel for goroutine communication
errChan := make(chan error, 1)
done := make(chan struct{}) done := make(chan struct{})
// Create a set of current players to track disconnects // Start message sending goroutine
currentPlayers := make(map[int32]bool)
go func() { 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)
}
}()
for { for {
select { select {
case <-quitChan: case <-quitChan:
@ -118,23 +132,23 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
writeMessage(conn, disconnectMsg) writeMessage(conn, disconnectMsg)
done <- struct{}{} done <- struct{}{}
return return
case <-done:
return
case <-actionTicker.C: case <-actionTicker.C:
player.Lock() player.Lock()
if len(player.ActionQueue) > 0 { if len(player.ActionQueue) > 0 {
actions := make([]*pb.Action, len(player.ActionQueue)) actions := make([]*pb.Action, len(player.ActionQueue))
copy(actions, player.ActionQueue) copy(actions, player.ActionQueue)
batch := &pb.ActionBatch{ batch := &pb.ActionBatch{
PlayerId: playerID, PlayerId: playerID,
Actions: actions, Actions: actions,
Tick: player.CurrentTick, Tick: player.CurrentTick,
} }
player.ActionQueue = player.ActionQueue[:0] player.ActionQueue = player.ActionQueue[:0]
player.Unlock() player.Unlock()
if err := writeMessage(conn, batch); err != nil { if err := writeMessage(conn, batch); err != nil {
log.Printf("Failed to send actions to server: %v", err) errChan <- err
return return
} }
} else { } else {
@ -144,32 +158,29 @@ 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 { for {
select { select {
case <-quitChan: case <-quitChan:
done := make(chan struct{})
go func() {
<-done
close(player.QuitDone)
}()
select {
case <-done:
time.Sleep(100 * time.Millisecond)
case <-time.After(1 * time.Second):
log.Println("Shutdown timed out")
}
return return
default: default:
// Read message length (4 bytes)
lengthBuf := make([]byte, 4) lengthBuf := make([]byte, 4)
if _, err := io.ReadFull(reader, lengthBuf); err != nil { if _, err := io.ReadFull(reader, lengthBuf); err != nil {
log.Printf("Failed to read message length: %v", err) if err != io.EOF {
errChan <- fmt.Errorf("failed to read message length: %v", err)
}
return return
} }
messageLength := binary.BigEndian.Uint32(lengthBuf) messageLength := binary.BigEndian.Uint32(lengthBuf)
// Read the full message
messageBuf := make([]byte, messageLength) messageBuf := make([]byte, messageLength)
if _, err := io.ReadFull(reader, messageBuf); err != nil { if _, err := io.ReadFull(reader, messageBuf); err != nil {
log.Printf("Failed to read message body: %v", err) log.Printf("Failed to read message body: %v", err)
@ -197,7 +208,6 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
player.Unlock() player.Unlock()
for _, state := range serverMessage.Players { for _, state := range serverMessage.Players {
currentPlayers[state.PlayerId] = true
if state.PlayerId == playerID { if state.PlayerId == playerID {
player.Lock() player.Lock()
// Update initial position if not set // Update initial position if not set
@ -222,7 +232,7 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
// Remove players that are no longer in the server state // Remove players that are no longer in the server state
for id := range otherPlayers { for id := range otherPlayers {
if !currentPlayers[id] { if id != playerID {
delete(otherPlayers, id) delete(otherPlayers, id)
} }
} }
@ -232,6 +242,23 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
} }
} }
} }
}()
// 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,
}},
}
writeMessage(conn, disconnectMsg)
case err := <-errChan:
log.Printf("Network error: %v", err)
}
} }
// Helper function to write length-prefixed messages // Helper function to write length-prefixed messages
@ -252,3 +279,50 @@ func writeMessage(conn net.Conn, msg proto.Message) error {
_, err = conn.Write(data) _, err = conn.Write(data)
return err 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
}

View File

@ -1,12 +1,34 @@
package types package types
import ( import (
"sync"
"time" "time"
pb "gitea.boner.be/bdnugget/goonserver/actions" pb "gitea.boner.be/bdnugget/goonserver/actions"
rl "github.com/gen2brain/raylib-go/raylib" 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) { func (p *Player) MoveTowards(target Tile, deltaTime float32, mapGrid [][]Tile) {
p.Lock() p.Lock()
defer p.Unlock() defer p.Unlock()
@ -74,6 +96,8 @@ func NewPlayer(state *pb.PlayerState) *Player {
IsMoving: false, IsMoving: false,
AnimationFrame: 0, AnimationFrame: 0,
LastAnimUpdate: time.Now(), LastAnimUpdate: time.Now(),
LastUpdateTime: time.Now(),
InterpolationProgress: 1.0,
} }
} }

View File

@ -1,7 +1,6 @@
package types package types
import ( import (
"sync"
"time" "time"
pb "gitea.boner.be/bdnugget/goonserver/actions" pb "gitea.boner.be/bdnugget/goonserver/actions"
@ -14,27 +13,6 @@ type Tile struct {
Walkable bool 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 { type AnimationSet struct {
Idle []rl.ModelAnimation Idle []rl.ModelAnimation
Walk []rl.ModelAnimation Walk []rl.ModelAnimation