diff --git a/2 b/2 new file mode 100644 index 0000000..4602617 --- /dev/null +++ b/2 @@ -0,0 +1,5 @@ +INFO: 2025/02/10 14:51:58 main.go:15: Starting GoonScape client +INFO: 2025/02/10 14:51:58 main.go:38: Initializing window +INFO: 2025/02/10 14:51:58 main.go:55: Loading game assets +INFO: 2025/02/10 14:51:58 game.go:54: Loading game assets +INFO: 2025/02/10 14:51:58 assets.go:51: Loading models diff --git a/assets/assets.go b/assets/assets.go index 4368cbf..69479ea 100644 --- a/assets/assets.go +++ b/assets/assets.go @@ -1,10 +1,25 @@ package assets import ( + "fmt" + "sync" + + "gitea.boner.be/bdnugget/goonscape/logging" "gitea.boner.be/bdnugget/goonscape/types" rl "github.com/gen2brain/raylib-go/raylib" ) +var ( + assetMutex sync.RWMutex + loadedModels map[string]types.ModelAsset + audioMutex sync.Mutex + audioInitialized bool +) + +func init() { + loadedModels = make(map[string]types.ModelAsset) +} + // Helper function to load animations for a model func loadModelAnimations(animPaths map[string]string) (types.AnimationSet, error) { var animSet types.AnimationSet @@ -33,9 +48,28 @@ func loadModelAnimations(animPaths map[string]string) (types.AnimationSet, error } func LoadModels() ([]types.ModelAsset, error) { + logging.Info.Println("Loading models") + assetMutex.Lock() + defer assetMutex.Unlock() + + if len(loadedModels) > 0 { + logging.Info.Println("Returning cached models") + models := make([]types.ModelAsset, 0, len(loadedModels)) + for _, model := range loadedModels { + models = append(models, model) + } + return models, nil + } + // Goonion model and animations goonerModel := rl.LoadModel("resources/models/gooner/walk_no_y_transform.glb") - goonerAnims, _ := loadModelAnimations(map[string]string{"idle": "resources/models/gooner/idle_no_y_transform.glb", "walk": "resources/models/gooner/walk_no_y_transform.glb"}) + goonerAnims, err := loadModelAnimations(map[string]string{ + "idle": "resources/models/gooner/idle_no_y_transform.glb", + "walk": "resources/models/gooner/walk_no_y_transform.glb", + }) + if err != nil { + return nil, err + } // Apply transformations transform := rl.MatrixIdentity() @@ -46,25 +80,30 @@ func LoadModels() ([]types.ModelAsset, error) { // Coomer model (ready for animations) coomerModel := rl.LoadModel("resources/models/coomer/idle_notransy.glb") - // coomerTexture := rl.LoadTexture("resources/models/coomer.png") - // rl.SetMaterialTexture(coomerModel.Materials, rl.MapDiffuse, coomerTexture) - // When you have animations, add them like: - coomerAnims, _ := loadModelAnimations(map[string]string{"idle": "resources/models/coomer/idle_notransy.glb", "walk": "resources/models/coomer/unsteadywalk_notransy.glb"}) + coomerAnims, err := loadModelAnimations(map[string]string{ + "idle": "resources/models/coomer/idle_notransy.glb", + "walk": "resources/models/coomer/unsteadywalk_notransy.glb", + }) + if err != nil { + return nil, err + } coomerModel.Transform = transform // Shreke model (ready for animations) - shrekeModel := rl.LoadModel("resources/models/shreke.obj") - shrekeTexture := rl.LoadTexture("resources/models/shreke.png") - rl.SetMaterialTexture(shrekeModel.Materials, rl.MapDiffuse, shrekeTexture) - // When you have animations, add them like: - // shrekeAnims, _ := loadModelAnimations("resources/models/shreke.glb", - // map[string]string{ - // "idle": "resources/models/shreke_idle.glb", - // "walk": "resources/models/shreke_walk.glb", - // }) + shrekeModel := rl.LoadModel("resources/models/shreke/Animation_Slow_Orc_Walk_withSkin.glb") + shrekeAnims, err := loadModelAnimations(map[string]string{ + "idle": "resources/models/shreke/Animation_Slow_Orc_Walk_withSkin.glb", + "walk": "resources/models/shreke/Animation_Excited_Walk_M_withSkin.glb", + }) + if err != nil { + return nil, err + } + shrekeModel.Transform = transform - return []types.ModelAsset{ + // Store loaded models + models := []types.ModelAsset{ { + Name: "gooner", Model: goonerModel, Animation: append(goonerAnims.Idle, goonerAnims.Walk...), AnimFrames: int32(len(goonerAnims.Idle) + len(goonerAnims.Walk)), @@ -78,15 +117,47 @@ func LoadModels() ([]types.ModelAsset, error) { Animations: coomerAnims, YOffset: -4.0, }, - {Model: shrekeModel, Texture: shrekeTexture}, - }, nil + { + Model: shrekeModel, + Animation: append(shrekeAnims.Idle, shrekeAnims.Walk...), + AnimFrames: int32(len(shrekeAnims.Idle) + len(shrekeAnims.Walk)), + Animations: shrekeAnims, + YOffset: 0.0, + }, + } + + for _, model := range models { + loadedModels[model.Name] = model + } + + return models, nil } func LoadMusic(filename string) (rl.Music, error) { - return rl.LoadMusicStream(filename), nil + logging.Info.Printf("Loading music from %s", filename) + audioMutex.Lock() + defer audioMutex.Unlock() + + if !rl.IsAudioDeviceReady() { + err := fmt.Errorf("audio device not initialized") + logging.Error.Println(err) + return rl.Music{}, err + } + + music := rl.LoadMusicStream(filename) + if music.CtxType == 0 { + err := fmt.Errorf("failed to load music stream") + logging.Error.Println(err) + return rl.Music{}, err + } + logging.Info.Println("Music loaded successfully") + return music, nil } func UnloadModels(models []types.ModelAsset) { + assetMutex.Lock() + defer assetMutex.Unlock() + for _, model := range models { if model.Animation != nil { for i := int32(0); i < model.AnimFrames; i++ { @@ -95,6 +166,7 @@ func UnloadModels(models []types.ModelAsset) { } rl.UnloadModel(model.Model) rl.UnloadTexture(model.Texture) + delete(loadedModels, model.Name) } } diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..9351e8e --- /dev/null +++ b/config/config.go @@ -0,0 +1,7 @@ +package config + +type Config struct { + PlayMusic bool +} + +var Current = Config{PlayMusic: true} diff --git a/game/chat.go b/game/chat.go index 9aced0d..78b4289 100644 --- a/game/chat.go +++ b/game/chat.go @@ -2,6 +2,7 @@ package game import ( "fmt" + "sync" "time" "gitea.boner.be/bdnugget/goonscape/types" @@ -19,6 +20,7 @@ const ( ) type Chat struct { + sync.RWMutex messages []types.ChatMessage inputBuffer []rune isTyping bool @@ -49,6 +51,8 @@ func (c *Chat) AddMessage(playerID int32, content string) { } func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) { + c.Lock() + defer c.Unlock() // Convert protobuf messages to our local type for _, msg := range messages { localMsg := types.ChatMessage{ @@ -80,13 +84,14 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) { ExpireTime: time.Now().Add(6 * time.Second), } game.Player.Unlock() - } else if otherPlayer, exists := game.OtherPlayers[msg.PlayerId]; exists { - otherPlayer.Lock() - otherPlayer.FloatingMessage = &types.FloatingMessage{ + } else if otherPlayer, exists := game.OtherPlayers.Load(msg.PlayerId); exists { + other := otherPlayer.(*types.Player) + other.Lock() + other.FloatingMessage = &types.FloatingMessage{ Content: msg.Content, ExpireTime: time.Now().Add(6 * time.Second), } - otherPlayer.Unlock() + other.Unlock() } } } @@ -94,6 +99,8 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) { } func (c *Chat) Draw(screenWidth, screenHeight int32) { + c.RLock() + defer c.RUnlock() // Calculate chat window width based on screen width chatWindowWidth := screenWidth - (chatMargin * 2) diff --git a/game/context.go b/game/context.go new file mode 100644 index 0000000..a9522b3 --- /dev/null +++ b/game/context.go @@ -0,0 +1,38 @@ +package game + +import ( + "context" + "sync" +) + +type GameContext struct { + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup + assetsLock sync.RWMutex +} + +func NewGameContext() *GameContext { + ctx, cancel := context.WithCancel(context.Background()) + return &GameContext{ + ctx: ctx, + cancel: cancel, + } +} + +func (gc *GameContext) Shutdown() { + gc.cancel() + gc.wg.Wait() +} + +func (gc *GameContext) LoadAssets(fn func() error) error { + gc.assetsLock.Lock() + defer gc.assetsLock.Unlock() + return fn() +} + +func (gc *GameContext) UnloadAssets(fn func()) { + gc.assetsLock.Lock() + defer gc.assetsLock.Unlock() + fn() +} diff --git a/game/game.go b/game/game.go index e3bf9c4..e01db52 100644 --- a/game/game.go +++ b/game/game.go @@ -1,33 +1,44 @@ package game import ( + "fmt" "os" + "sync" "time" + "sync/atomic" + "gitea.boner.be/bdnugget/goonscape/assets" + "gitea.boner.be/bdnugget/goonscape/config" + "gitea.boner.be/bdnugget/goonscape/logging" "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" ) +var audioMutex sync.Mutex +var audioInitOnce sync.Once + type Game struct { + ctx *GameContext Player *types.Player - OtherPlayers map[int32]*types.Player + OtherPlayers sync.Map // Using sync.Map for concurrent access Camera rl.Camera3D Models []types.ModelAsset Music rl.Music Chat *Chat - MenuOpen bool + MenuOpen atomic.Bool QuitChan chan struct{} // Channel to signal shutdown loginScreen *LoginScreen - isLoggedIn bool + isLoggedIn atomic.Bool } func New() *Game { InitWorld() game := &Game{ - OtherPlayers: make(map[int32]*types.Player), + ctx: NewGameContext(), + OtherPlayers: sync.Map{}, Camera: rl.Camera3D{ Position: rl.NewVector3(0, 10, 10), Target: rl.NewVector3(0, 0, 0), @@ -44,22 +55,38 @@ func New() *Game { } func (g *Game) LoadAssets() error { + audioMutex.Lock() + defer audioMutex.Unlock() + + logging.Info.Println("Loading game assets") var err error + + // Load models first g.Models, err = assets.LoadModels() if err != nil { + logging.Error.Printf("Failed to load models: %v", err) return err } - g.Music, err = assets.LoadMusic("resources/audio/GoonScape2.mp3") - if err != nil { - return err + // Load music only if enabled + if config.Current.PlayMusic { + logging.Info.Println("Loading music stream") + g.Music = rl.LoadMusicStream("resources/audio/GoonScape2.mp3") + if g.Music.CtxType == 0 { + logging.Error.Println("Failed to load music stream") + return fmt.Errorf("failed to load music stream") + } + logging.Info.Println("Music stream loaded successfully") + } else { + logging.Info.Println("Music disabled by config") } + logging.Info.Println("Assets loaded successfully") return nil } func (g *Game) Update(deltaTime float32) { - if !g.isLoggedIn { + if !g.isLoggedIn.Load() { username, password, isRegistering, submitted := g.loginScreen.Update() if submitted { conn, playerID, err := network.ConnectToServer(username, password, isRegistering) @@ -77,8 +104,8 @@ func (g *Game) Update(deltaTime float32) { } g.AssignModelToPlayer(g.Player) - go network.HandleServerCommunication(conn, playerID, g.Player, g.OtherPlayers, g.QuitChan) - g.isLoggedIn = true + go network.HandleServerCommunication(conn, playerID, g.Player, &g.OtherPlayers, g.QuitChan) + g.isLoggedIn.Store(true) return } g.loginScreen.Draw() @@ -87,12 +114,12 @@ func (g *Game) Update(deltaTime float32) { // Handle ESC for menu if rl.IsKeyPressed(rl.KeyEscape) { - g.MenuOpen = !g.MenuOpen + g.MenuOpen.Store(!g.MenuOpen.Load()) return } // Don't process other inputs if menu is open - if g.MenuOpen { + if g.MenuOpen.Load() { return } @@ -109,14 +136,20 @@ func (g *Game) Update(deltaTime float32) { g.HandleInput() if len(g.Player.TargetPath) > 0 { - g.Player.MoveTowards(g.Player.TargetPath[0], deltaTime, GetMapGrid()) + g.Player.Lock() + if len(g.Player.TargetPath) > 0 { + g.Player.MoveTowards(g.Player.TargetPath[0], deltaTime, GetMapGrid()) + } + g.Player.Unlock() } - for _, other := range g.OtherPlayers { + g.OtherPlayers.Range(func(key, value any) bool { + other := value.(*types.Player) if len(other.TargetPath) > 0 { other.MoveTowards(other.TargetPath[0], deltaTime, GetMapGrid()) } - } + return true + }) UpdateCamera(&g.Camera, g.Player.PosActual, deltaTime) } @@ -153,6 +186,11 @@ func (g *Game) DrawPlayer(player *types.Player) { player.Lock() defer player.Unlock() + if player.Model.Meshes == nil { + logging.Error.Println("Player model not initialized") + return + } + grid := GetMapGrid() modelIndex := int(player.ID) % len(g.Models) modelAsset := g.Models[modelIndex] @@ -210,24 +248,36 @@ func (g *Game) DrawPlayer(player *types.Player) { } func (g *Game) Render() { - rl.BeginDrawing() - rl.ClearBackground(rl.RayWhite) + if !rl.IsWindowReady() { + logging.Error.Println("Window not ready for rendering") + return + } - if !g.isLoggedIn { + rl.BeginDrawing() + defer func() { + if rl.IsWindowReady() { + rl.EndDrawing() + } + }() + + if !g.isLoggedIn.Load() { g.loginScreen.Draw() - rl.EndDrawing() return } rl.BeginMode3D(g.Camera) g.DrawMap() g.DrawPlayer(g.Player) - for _, other := range g.OtherPlayers { + + g.OtherPlayers.Range(func(key, value any) bool { + other := value.(*types.Player) if other.Model.Meshes == nil { g.AssignModelToPlayer(other) } g.DrawPlayer(other) - } + return true + }) + rl.EndMode3D() // Draw floating messages @@ -255,27 +305,41 @@ func (g *Game) Render() { drawFloatingMessage(g.Player.FloatingMessage) } - for _, other := range g.OtherPlayers { + g.OtherPlayers.Range(func(key, value any) bool { + other := value.(*types.Player) drawFloatingMessage(other.FloatingMessage) - } + return true + }) // Draw menu if open - if g.MenuOpen { + if g.MenuOpen.Load() { g.DrawMenu() } // Only draw chat if menu is not open - if !g.MenuOpen { + if !g.MenuOpen.Load() { g.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight())) } rl.DrawFPS(10, 10) - rl.EndDrawing() } func (g *Game) Cleanup() { - assets.UnloadModels(g.Models) - assets.UnloadMusic(g.Music) + // Unload models + if g.Models != nil { + assets.UnloadModels(g.Models) + } + + // Stop and unload music if enabled + if config.Current.PlayMusic && g.Music.CtxType != 0 { + rl.StopMusicStream(g.Music) + rl.UnloadMusicStream(g.Music) + } + + // Close audio device if it's ready + if rl.IsAudioDeviceReady() { + rl.CloseAudioDevice() + } } func (g *Game) HandleInput() { @@ -334,7 +398,7 @@ func (g *Game) DrawMenu() { if rl.IsMouseButtonPressed(rl.MouseLeftButton) { switch item { case "Resume": - g.MenuOpen = false + g.MenuOpen.Store(false) case "Settings": // TODO: Implement settings case "Exit Game": @@ -369,7 +433,37 @@ func (g *Game) AssignModelToPlayer(player *types.Player) { modelIndex := int(player.ID) % len(g.Models) modelAsset := g.Models[modelIndex] - // Just use the original model - don't try to copy it player.Model = modelAsset.Model player.Texture = modelAsset.Texture + player.AnimationFrame = 0 +} + +func (g *Game) Run() { + if config.Current.PlayMusic { + audioInitOnce.Do(func() { + logging.Info.Println("Initializing audio device") + rl.InitAudioDevice() + if !rl.IsAudioDeviceReady() { + logging.Error.Println("Failed to initialize audio device") + } + }) + defer func() { + logging.Info.Println("Closing audio device") + rl.CloseAudioDevice() + }() + } + + logging.Info.Println("Starting game loop") + for !rl.WindowShouldClose() { + deltaTime := rl.GetFrameTime() + if config.Current.PlayMusic { + rl.UpdateMusicStream(g.Music) + } + g.Update(deltaTime) + g.Render() + } + logging.Info.Println("Game loop ended") + + logging.Info.Println("Closing quit channel") + close(g.QuitChan) } diff --git a/game/pathfinding.go b/game/pathfinding.go index ce59e31..ba3b751 100644 --- a/game/pathfinding.go +++ b/game/pathfinding.go @@ -83,6 +83,7 @@ func heuristic(a, b types.Tile) float32 { } func distance(a, b types.Tile) float32 { + _, _ = a, b return 1.0 // uniform cost for now } diff --git a/go.mod b/go.mod index db5f35f..487bc72 100644 --- a/go.mod +++ b/go.mod @@ -10,8 +10,8 @@ require ( require ( github.com/ebitengine/purego v0.8.2 // indirect - golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3 // indirect + golang.org/x/sys v0.30.0 // indirect ) replace gitea.boner.be/bdnugget/goonserver => ./goonserver diff --git a/go.sum b/go.sum index c8e48ea..f8d4b9f 100644 --- a/go.sum +++ b/go.sum @@ -6,7 +6,11 @@ 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-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= +golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3 h1:qNgPs5exUA+G0C96DrPwNrvLSj7GT/9D+3WMWUcUg34= +golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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/logging/logging.go b/logging/logging.go new file mode 100644 index 0000000..2120c98 --- /dev/null +++ b/logging/logging.go @@ -0,0 +1,16 @@ +package logging + +import ( + "log" + "os" +) + +var ( + Info *log.Logger + Error *log.Logger +) + +func init() { + Info = log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile) + Error = log.New(os.Stderr, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile) +} diff --git a/main.go b/main.go index 7b1f06f..6094a82 100644 --- a/main.go +++ b/main.go @@ -5,17 +5,27 @@ import ( "log" "strings" + "gitea.boner.be/bdnugget/goonscape/config" "gitea.boner.be/bdnugget/goonscape/game" + "gitea.boner.be/bdnugget/goonscape/logging" "gitea.boner.be/bdnugget/goonscape/network" rl "github.com/gen2brain/raylib-go/raylib" ) func main() { + logging.Info.Println("Starting GoonScape client") + + // Raylib log level warn + rl.SetTraceLogLevel(rl.LogWarning) // Parse command line flags local := flag.Bool("local", false, "Connect to local server") addr := flag.String("addr", "", "Server address (host or host:port)") + noMusic := flag.Bool("no-music", false, "Disable music playback") flag.Parse() + // Set config before any game initialization + config.Current.PlayMusic = !*noMusic + // Set server address based on flags if *local { if *addr != "" { @@ -30,31 +40,40 @@ func main() { network.SetServerAddr(*addr) } + logging.Info.Println("Initializing window") rl.InitWindow(1024, 768, "GoonScape") - rl.SetExitKey(0) - defer rl.CloseWindow() + defer func() { + logging.Info.Println("Closing window") + rl.CloseWindow() + }() - rl.InitAudioDevice() + // Initialize audio device first + if !rl.IsAudioDeviceReady() { + rl.InitAudioDevice() + if !rl.IsAudioDeviceReady() { + log.Fatal("Failed to initialize audio device") + } + } defer rl.CloseAudioDevice() - rl.SetTargetFPS(60) - game := game.New() + logging.Info.Println("Loading game assets") if err := game.LoadAssets(); err != nil { log.Fatalf("Failed to load assets: %v", err) } - defer game.Cleanup() + defer func() { + logging.Info.Println("Cleaning up game resources") + game.Cleanup() + }() - rl.PlayMusicStream(game.Music) - rl.SetMusicVolume(game.Music, 0.5) - - for !rl.WindowShouldClose() { - deltaTime := rl.GetFrameTime() - rl.UpdateMusicStream(game.Music) - game.Update(deltaTime) - game.Render() + if config.Current.PlayMusic { + logging.Info.Println("Starting music playback") + rl.PlayMusicStream(game.Music) + rl.SetMusicVolume(game.Music, 0.5) } - // Wait for clean shutdown - <-game.QuitChan + rl.SetTargetFPS(60) + logging.Info.Println("Starting game loop") + game.Run() + logging.Info.Println("Game exited cleanly") } diff --git a/network/network.go b/network/network.go index 21e57ef..e712551 100644 --- a/network/network.go +++ b/network/network.go @@ -2,16 +2,18 @@ package network import ( "bufio" + "context" "encoding/binary" "fmt" "io" "log" "net" + "sync" "time" + "gitea.boner.be/bdnugget/goonscape/logging" "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" ) @@ -19,18 +21,84 @@ const protoVersion = 1 var serverAddr = "boner.be:6969" +type NetworkManager struct { + ctx context.Context + conn net.Conn + reader *bufio.Reader + writer *bufio.Writer + mu sync.Mutex +} + +func NewNetworkManager(ctx context.Context) *NetworkManager { + return &NetworkManager{ + ctx: ctx, + } +} + func SetServerAddr(addr string) { serverAddr = addr } -func ConnectToServer(username, password string, isRegistering bool) (net.Conn, int32, error) { - conn, err := net.Dial("tcp", serverAddr) +func (nm *NetworkManager) Connect(addr string) error { + nm.mu.Lock() + defer nm.mu.Unlock() + + var err error + nm.conn, err = net.Dial("tcp", addr) if err != nil { - log.Printf("Failed to dial server: %v", err) - return nil, 0, err + return err } - log.Println("Connected to server. Authenticating...") + nm.reader = bufio.NewReader(nm.conn) + nm.writer = bufio.NewWriter(nm.conn) + + go nm.readLoop() + return nil +} + +func (nm *NetworkManager) readLoop() { + for { + select { + case <-nm.ctx.Done(): + return + default: + // Read and process messages + } + } +} + +func (nm *NetworkManager) Send(message proto.Message) error { + nm.mu.Lock() + defer nm.mu.Unlock() + + data, err := proto.Marshal(message) + if err != nil { + return err + } + + // Write length prefix + lengthBuf := make([]byte, 4) + binary.BigEndian.PutUint32(lengthBuf, uint32(len(data))) + if _, err := nm.writer.Write(lengthBuf); err != nil { + return err + } + + // Write message body + if _, err := nm.writer.Write(data); err != nil { + return err + } + + return nm.writer.Flush() +} + +func ConnectToServer(username, password string, isRegistering bool) (net.Conn, int32, error) { + logging.Info.Println("Attempting to connect to server at", serverAddr) + conn, err := net.Dial("tcp", serverAddr) + if err != nil { + logging.Error.Printf("Failed to dial server: %v", err) + return nil, 0, err + } + logging.Info.Println("Connected to server. Authenticating...") // Send auth message authAction := &pb.Action{ @@ -81,7 +149,7 @@ func ConnectToServer(username, password string, isRegistering bool) (net.Conn, i if !response.AuthSuccess { conn.Close() - return nil, 0, fmt.Errorf(response.ErrorMessage) + return nil, 0, fmt.Errorf("authentication failed: %s", response.ErrorMessage) } playerID := response.GetPlayerId() @@ -89,145 +157,63 @@ func ConnectToServer(username, password string, isRegistering bool) (net.Conn, i return conn, playerID, nil } -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{}) - - // Create a set of current players to track disconnects - currentPlayers := make(map[int32]bool) - - 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() - 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 { - log.Printf("Failed to send actions to server: %v", err) - return - } - } else { - player.Unlock() - } - } - } +func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Player, otherPlayers *sync.Map, quitChan <-chan struct{}) { + defer func() { + logging.Info.Println("Closing connection and cleaning up for player", playerID) + conn.Close() + close(player.QuitDone) }() + reader := bufio.NewReader(conn) + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + for { select { 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 - default: - // Read message length (4 bytes) + case <-ticker.C: + // Read message length lengthBuf := make([]byte, 4) if _, err := io.ReadFull(reader, lengthBuf); err != nil { log.Printf("Failed to read message length: %v", err) - return + continue } messageLength := binary.BigEndian.Uint32(lengthBuf) - // Read the full message + // Read message body messageBuf := make([]byte, messageLength) if _, err := io.ReadFull(reader, messageBuf); err != nil { log.Printf("Failed to read message body: %v", err) - return + continue } + // Process server message 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 - } - } - } - player.Unlock() - + // Update player states 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() + if state == nil { + logging.Error.Println("Received nil player state") continue } - if otherPlayer, exists := otherPlayers[state.PlayerId]; exists { - otherPlayer.UpdatePosition(state, types.ServerTickRate) + if state.PlayerId == playerID { + player.UpdatePosition(state, types.ServerTickRate) + } else if existing, ok := otherPlayers.Load(state.PlayerId); ok { + existing.(*types.Player).UpdatePosition(state, types.ServerTickRate) } else { - otherPlayers[state.PlayerId] = types.NewPlayer(state) + newPlayer := types.NewPlayer(state) + otherPlayers.Store(state.PlayerId, newPlayer) } } - // 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 { + // Handle chat messages + if handler, ok := player.UserData.(types.ChatMessageHandler); ok { handler.HandleServerMessages(serverMessage.ChatMessages) } } diff --git a/resources/models/shreke/Animation_Excited_Walk_M_withSkin.glb b/resources/models/shreke/Animation_Excited_Walk_M_withSkin.glb new file mode 100644 index 0000000..e5340ef Binary files /dev/null and b/resources/models/shreke/Animation_Excited_Walk_M_withSkin.glb differ diff --git a/resources/models/shreke/Animation_Running_withSkin.glb b/resources/models/shreke/Animation_Running_withSkin.glb new file mode 100644 index 0000000..9234323 Binary files /dev/null and b/resources/models/shreke/Animation_Running_withSkin.glb differ diff --git a/resources/models/shreke/Animation_Slow_Orc_Walk_withSkin.glb b/resources/models/shreke/Animation_Slow_Orc_Walk_withSkin.glb new file mode 100644 index 0000000..297827a Binary files /dev/null and b/resources/models/shreke/Animation_Slow_Orc_Walk_withSkin.glb differ diff --git a/resources/models/shreke/Animation_Unsteady_Walk_withSkin.glb b/resources/models/shreke/Animation_Unsteady_Walk_withSkin.glb new file mode 100644 index 0000000..861ce85 Binary files /dev/null and b/resources/models/shreke/Animation_Unsteady_Walk_withSkin.glb differ diff --git a/resources/models/shreke/Animation_Walking_withSkin.glb b/resources/models/shreke/Animation_Walking_withSkin.glb new file mode 100644 index 0000000..152c079 Binary files /dev/null and b/resources/models/shreke/Animation_Walking_withSkin.glb differ diff --git a/segfault.txt b/segfault.txt new file mode 100644 index 0000000..a92a0a3 --- /dev/null +++ b/segfault.txt @@ -0,0 +1,126 @@ +INFO: 2025/02/10 15:53:21 main.go:16: Starting GoonScape client +INFO: 2025/02/10 15:53:21 main.go:43: Initializing window +INFO: 2025/02/10 15:53:21 main.go:60: Loading game assets +INFO: 2025/02/10 15:53:21 game.go:61: Loading game assets +INFO: 2025/02/10 15:53:21 assets.go:51: Loading models +INFO: 2025/02/10 15:53:21 game.go:81: Music disabled by config +INFO: 2025/02/10 15:53:21 game.go:84: Assets loaded successfully +INFO: 2025/02/10 15:53:21 main.go:76: Starting game loop +INFO: 2025/02/10 15:53:21 game.go:456: Starting game loop +malloc(): unsorted double linked list corrupted +SIGABRT: abort +PC=0x78a790604c4c m=0 sigcode=18446744073709551610 +signal arrived during cgo execution + +goroutine 1 gp=0xc0000061c0 m=0 mp=0xaf8e20 [syscall, locked to thread]: +runtime.cgocall(0x6da9a0, 0xc00002da50) + /usr/lib/go/src/runtime/cgocall.go:167 +0x5b fp=0xc00002da28 sp=0xc00002d9f0 pc=0x46d0fb +github.com/gen2brain/raylib-go/raylib._Cfunc_EndDrawing() + _cgo_gotypes.go:2539 +0x3f fp=0xc00002da50 sp=0xc00002da28 pc=0x51cdbf +github.com/gen2brain/raylib-go/raylib.EndDrawing(...) + /home/bd/.cache/go/mod/github.com/gen2brain/raylib-go/raylib@v0.0.0-20250109172833-6dbba4f81a9b/rcore.go:464 +gitea.boner.be/bdnugget/goonscape/game.(*Game).Render.func1() + /home/bd/Projects/go/goonscape/game/game.go:259 +0x36 fp=0xc00002da68 sp=0xc00002da50 pc=0x6b6fb6 +runtime.deferreturn() + /usr/lib/go/src/runtime/panic.go:605 +0x5e fp=0xc00002daf8 sp=0xc00002da68 pc=0x4396fe +gitea.boner.be/bdnugget/goonscape/game.(*Game).Render(0xc0001ae000) + /home/bd/Projects/go/goonscape/game/game.go:265 +0x405 fp=0xc00002dbf8 sp=0xc00002daf8 pc=0x6b27a5 +gitea.boner.be/bdnugget/goonscape/game.(*Game).Run(0xc0001ae000) + /home/bd/Projects/go/goonscape/game/game.go:463 +0x105 fp=0xc00002dcc0 sp=0xc00002dbf8 pc=0x6b3e85 +main.main() + /home/bd/Projects/go/goonscape/main.go:77 +0xb85 fp=0xc00002df50 sp=0xc00002dcc0 pc=0x6b8345 +runtime.main() + /usr/lib/go/src/runtime/proc.go:272 +0x28b fp=0xc00002dfe0 sp=0xc00002df50 pc=0x43d84b +runtime.goexit({}) + /usr/lib/go/src/runtime/asm_amd64.s:1700 +0x1 fp=0xc00002dfe8 sp=0xc00002dfe0 pc=0x47ac81 + +goroutine 2 gp=0xc000006c40 m=nil [force gc (idle)]: +runtime.gopark(0xaedc20?, 0xaf8e20?, 0x0?, 0x0?, 0x0?) + /usr/lib/go/src/runtime/proc.go:424 +0xce fp=0xc0000567a8 sp=0xc000056788 pc=0x47356e +runtime.goparkunlock(...) + /usr/lib/go/src/runtime/proc.go:430 +runtime.forcegchelper() + /usr/lib/go/src/runtime/proc.go:337 +0xb3 fp=0xc0000567e0 sp=0xc0000567a8 pc=0x43db93 +runtime.goexit({}) + /usr/lib/go/src/runtime/asm_amd64.s:1700 +0x1 fp=0xc0000567e8 sp=0xc0000567e0 pc=0x47ac81 +created by runtime.init.7 in goroutine 1 + /usr/lib/go/src/runtime/proc.go:325 +0x1a + +goroutine 3 gp=0xc000007180 m=nil [GC sweep wait]: +runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x0?) + /usr/lib/go/src/runtime/proc.go:424 +0xce fp=0xc00006af80 sp=0xc00006af60 pc=0x47356e +runtime.goparkunlock(...) + /usr/lib/go/src/runtime/proc.go:430 +runtime.bgsweep(0xc000080000) + /usr/lib/go/src/runtime/mgcsweep.go:277 +0x94 fp=0xc00006afc8 sp=0xc00006af80 pc=0x428714 +runtime.gcenable.gowrap1() + /usr/lib/go/src/runtime/mgc.go:204 +0x25 fp=0xc00006afe0 sp=0xc00006afc8 pc=0x41ce25 +runtime.goexit({}) + /usr/lib/go/src/runtime/asm_amd64.s:1700 +0x1 fp=0xc00006afe8 sp=0xc00006afe0 pc=0x47ac81 +created by runtime.gcenable in goroutine 1 + /usr/lib/go/src/runtime/mgc.go:204 +0x66 + +goroutine 4 gp=0xc000007340 m=nil [GC scavenge wait]: +runtime.gopark(0xc000080000?, 0x8e5b60?, 0x1?, 0x0?, 0xc000007340?) + /usr/lib/go/src/runtime/proc.go:424 +0xce fp=0xc000064f78 sp=0xc000064f58 pc=0x47356e +runtime.goparkunlock(...) + /usr/lib/go/src/runtime/proc.go:430 +runtime.(*scavengerState).park(0xaf8060) + /usr/lib/go/src/runtime/mgcscavenge.go:425 +0x49 fp=0xc000064fa8 sp=0xc000064f78 pc=0x426149 +runtime.bgscavenge(0xc000080000) + /usr/lib/go/src/runtime/mgcscavenge.go:653 +0x3c fp=0xc000064fc8 sp=0xc000064fa8 pc=0x4266bc +runtime.gcenable.gowrap2() + /usr/lib/go/src/runtime/mgc.go:205 +0x25 fp=0xc000064fe0 sp=0xc000064fc8 pc=0x41cdc5 +runtime.goexit({}) + /usr/lib/go/src/runtime/asm_amd64.s:1700 +0x1 fp=0xc000064fe8 sp=0xc000064fe0 pc=0x47ac81 +created by runtime.gcenable in goroutine 1 + /usr/lib/go/src/runtime/mgc.go:205 +0xa5 + +goroutine 18 gp=0xc000104700 m=nil [finalizer wait]: +runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x0?) + /usr/lib/go/src/runtime/proc.go:424 +0xce fp=0xc000184e20 sp=0xc000184e00 pc=0x47356e +runtime.runfinq() + /usr/lib/go/src/runtime/mfinal.go:193 +0x145 fp=0xc000184fe0 sp=0xc000184e20 pc=0x41bea5 +runtime.goexit({}) + /usr/lib/go/src/runtime/asm_amd64.s:1700 +0x1 fp=0xc000184fe8 sp=0xc000184fe0 pc=0x47ac81 +created by runtime.createfing in goroutine 1 + /usr/lib/go/src/runtime/mfinal.go:163 +0x3d + +goroutine 19 gp=0xc0001048c0 m=nil [chan receive]: +runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x0?) + /usr/lib/go/src/runtime/proc.go:424 +0xce fp=0xc00006bf18 sp=0xc00006bef8 pc=0x47356e +runtime.chanrecv(0xc0001140e0, 0x0, 0x1) + /usr/lib/go/src/runtime/chan.go:639 +0x3bc fp=0xc00006bf90 sp=0xc00006bf18 pc=0x40c89c +runtime.chanrecv1(0x0?, 0x0?) + /usr/lib/go/src/runtime/chan.go:489 +0x12 fp=0xc00006bfb8 sp=0xc00006bf90 pc=0x40c4d2 +runtime.unique_runtime_registerUniqueMapCleanup.func1(...) + /usr/lib/go/src/runtime/mgc.go:1781 +runtime.unique_runtime_registerUniqueMapCleanup.gowrap1() + /usr/lib/go/src/runtime/mgc.go:1784 +0x2f fp=0xc00006bfe0 sp=0xc00006bfb8 pc=0x41fe4f +runtime.goexit({}) + /usr/lib/go/src/runtime/asm_amd64.s:1700 +0x1 fp=0xc00006bfe8 sp=0xc00006bfe0 pc=0x47ac81 +created by unique.runtime_registerUniqueMapCleanup in goroutine 1 + /usr/lib/go/src/runtime/mgc.go:1779 +0x96 + +rax 0x0 +rbx 0x4829 +rcx 0x78a790604c4c +rdx 0x6 +rdi 0x4829 +rsi 0x4829 +rbp 0x78a7902fc740 +rsp 0x7ffd63425870 +r8 0xffffffff +r9 0x0 +r10 0x8 +r11 0x246 +r12 0x7ffd634259d0 +r13 0x6 +r14 0x7ffd634259d0 +r15 0x7ffd634259d0 +rip 0x78a790604c4c +rflags 0x246 +cs 0x33 +fs 0x0 +gs 0x0 +exit status 2 diff --git a/types/player.go b/types/player.go index 747e39f..2ee798f 100644 --- a/types/player.go +++ b/types/player.go @@ -1,12 +1,34 @@ package types import ( + "sync" "time" pb "gitea.boner.be/bdnugget/goonserver/actions" rl "github.com/gen2brain/raylib-go/raylib" ) +type Player struct { + sync.RWMutex + PosActual rl.Vector3 + PosTile Tile + TargetPath []Tile + ActionQueue []*pb.Action + Speed float32 + ID int32 + IsMoving bool + AnimationFrame int32 + LastAnimUpdate time.Time + LastUpdateTime time.Time + InterpolationProgress float32 + FloatingMessage *FloatingMessage + QuitDone chan struct{} + UserData interface{} + Model rl.Model + Texture rl.Texture2D + Username string +} + func (p *Player) MoveTowards(target Tile, deltaTime float32, mapGrid [][]Tile) { p.Lock() defer p.Unlock() diff --git a/types/types.go b/types/types.go index 49aa718..d84119b 100644 --- a/types/types.go +++ b/types/types.go @@ -1,7 +1,6 @@ package types import ( - "sync" "time" pb "gitea.boner.be/bdnugget/goonserver/actions" @@ -14,27 +13,6 @@ 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 @@ -50,6 +28,7 @@ type ModelAsset struct { AnimFrames int32 // Keep this for compatibility Animations AnimationSet // New field for organized animations YOffset float32 // Additional height offset (added to default 8.0) + Name string } type ChatMessage struct { @@ -69,6 +48,13 @@ type ChatMessageHandler interface { HandleServerMessages([]*pb.ChatMessage) } +type PlayerState struct { + PlayerId int32 + X int32 + Y int32 + Username string +} + const ( MapWidth = 50 MapHeight = 50