From 5bf962a18d63af77622b7181c7681cd56311569d Mon Sep 17 00:00:00 2001 From: bdnugget <1001337108312v3@gmail.com> Date: Wed, 16 Apr 2025 10:47:46 +0200 Subject: [PATCH] Fix segfaults by avoiding models and animations for now --- assets/assets.go | 275 +++++++++++++++++++++++++++++++++++++++-------- game/game.go | 88 ++++++++++++--- main.go | 67 +++++++++--- types/player.go | 1 + types/types.go | 13 +-- 5 files changed, 370 insertions(+), 74 deletions(-) diff --git a/assets/assets.go b/assets/assets.go index 0fea9ae..43322b6 100644 --- a/assets/assets.go +++ b/assets/assets.go @@ -1,6 +1,9 @@ package assets import ( + "fmt" + "os" + "gitea.boner.be/bdnugget/goonscape/types" rl "github.com/gen2brain/raylib-go/raylib" ) @@ -9,6 +12,11 @@ import ( func loadModelAnimations(animPaths map[string]string) (types.AnimationSet, error) { var animSet types.AnimationSet + // Only try to load animations if environment variable isn't set + if os.Getenv("GOONSCAPE_DISABLE_ANIMATIONS") == "1" { + return animSet, nil + } + // Load idle animations if specified if idlePath, ok := animPaths["idle"]; ok { idleAnims := rl.LoadModelAnimations(idlePath) @@ -32,56 +40,235 @@ func loadModelAnimations(animPaths map[string]string) (types.AnimationSet, error return animSet, nil } +// ValidateModel checks if a model is valid and properly loaded +func ValidateModel(model rl.Model) error { + if model.Meshes == nil { + return fmt.Errorf("model has nil meshes") + } + if model.Meshes.VertexCount <= 0 { + return fmt.Errorf("model has invalid vertex count") + } + return nil +} + +// CompletelyAvoidExternalModels determines if we should avoid loading external models +func CompletelyAvoidExternalModels() bool { + return os.Getenv("GOONSCAPE_SAFE_MODE") == "1" +} + +// SafeLoadModel attempts to load a model, returning a placeholder if it fails +func SafeLoadModel(fileName string, fallbackShape int, fallbackColor rl.Color) (rl.Model, bool, rl.Color) { + // Don't even try to load external models in safe mode + if CompletelyAvoidExternalModels() { + rl.TraceLog(rl.LogInfo, "Safe mode enabled, using primitive shape instead of %s", fileName) + return createPrimitiveShape(fallbackShape), false, fallbackColor + } + + defer func() { + // Recover from any panics during model loading + if r := recover(); r != nil { + rl.TraceLog(rl.LogError, "Panic in SafeLoadModel: %v", r) + } + }() + + // Try to load the model + model := rl.LoadModel(fileName) + + // Check if the model is valid + if model.Meshes == nil || model.Meshes.VertexCount <= 0 { + rl.TraceLog(rl.LogWarning, "Failed to load model %s, using placeholder", fileName) + return createPrimitiveShape(fallbackShape), false, fallbackColor + } + + // For real models, return zero color since we don't need it + return model, true, rl.Color{} +} + +// createPrimitiveShape creates a simple shape without loading external models +func createPrimitiveShape(shapeType int) rl.Model { + var mesh rl.Mesh + + switch shapeType { + case 0: // Cube + mesh = rl.GenMeshCube(1.0, 2.0, 1.0) + case 1: // Sphere + mesh = rl.GenMeshSphere(1.0, 8, 8) + case 2: // Cylinder + mesh = rl.GenMeshCylinder(0.8, 2.0, 8) + case 3: // Cone + mesh = rl.GenMeshCone(1.0, 2.0, 8) + default: // Default to cube + mesh = rl.GenMeshCube(1.0, 2.0, 1.0) + } + + model := rl.LoadModelFromMesh(mesh) + return model +} + func LoadModels() ([]types.ModelAsset, error) { - // 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"}) + // Force safe mode for now until we fix the segfault + os.Setenv("GOONSCAPE_SAFE_MODE", "1") - // Apply transformations - transform := rl.MatrixIdentity() - transform = rl.MatrixMultiply(transform, rl.MatrixRotateY(180*rl.Deg2rad)) - transform = rl.MatrixMultiply(transform, rl.MatrixRotateX(-90*rl.Deg2rad)) - transform = rl.MatrixMultiply(transform, rl.MatrixScale(1.0, 1.0, 1.0)) - goonerModel.Transform = transform + models := make([]types.ModelAsset, 0, 3) - // 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"}) - coomerModel.Transform = transform + // Use environment variable to completely disable model loading + safeMode := CompletelyAvoidExternalModels() - // 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", - // }) + // Colors for the different models + goonerColor := rl.Color{R: 255, G: 200, B: 200, A: 255} // Pinkish + coomerColor := rl.Color{R: 200, G: 230, B: 255, A: 255} // Light blue + shrekeColor := rl.Color{R: 180, G: 255, B: 180, A: 255} // Light green - return []types.ModelAsset{ - { - Model: goonerModel, - Animation: append(goonerAnims.Idle, goonerAnims.Walk...), - AnimFrames: int32(len(goonerAnims.Idle) + len(goonerAnims.Walk)), - Animations: goonerAnims, - YOffset: 0.0, - }, - { - Model: coomerModel, - Animation: append(coomerAnims.Idle, coomerAnims.Walk...), - AnimFrames: int32(len(coomerAnims.Idle) + len(coomerAnims.Walk)), - Animations: coomerAnims, - YOffset: -4.0, - }, - {Model: shrekeModel, Texture: shrekeTexture}, - }, nil + // If in safe mode, create all models directly without loading + if safeMode { + // Gooner model (cube) + cube := createPrimitiveShape(0) + models = append(models, types.ModelAsset{ + Model: cube, + YOffset: 0.0, + PlaceholderColor: goonerColor, + }) + + // Coomer model (sphere) + sphere := createPrimitiveShape(1) + models = append(models, types.ModelAsset{ + Model: sphere, + YOffset: -4.0, + PlaceholderColor: coomerColor, + }) + + // Shreke model (cylinder) + cylinder := createPrimitiveShape(2) + models = append(models, types.ModelAsset{ + Model: cylinder, + YOffset: 0.0, + PlaceholderColor: shrekeColor, + }) + + return models, nil + } + + // The rest of the function with normal model loading + // Load Goonion model with error handling + var goonerModel rl.Model + var success bool + var modelColor rl.Color + + goonerModel, success, modelColor = SafeLoadModel("resources/models/gooner/walk_no_y_transform.glb", 0, goonerColor) + + // Create animations only if model was loaded successfully + var goonerAnims types.AnimationSet + if success { + goonerAnims, _ = loadModelAnimations(map[string]string{ + "idle": "resources/models/gooner/idle_no_y_transform.glb", + "walk": "resources/models/gooner/walk_no_y_transform.glb", + }) + + // Apply transformations + transform := rl.MatrixIdentity() + transform = rl.MatrixMultiply(transform, rl.MatrixRotateY(180*rl.Deg2rad)) + transform = rl.MatrixMultiply(transform, rl.MatrixRotateX(-90*rl.Deg2rad)) + transform = rl.MatrixMultiply(transform, rl.MatrixScale(1.0, 1.0, 1.0)) + goonerModel.Transform = transform + } + + // Always add a model (real or placeholder) + models = append(models, types.ModelAsset{ + Model: goonerModel, + Animation: append(goonerAnims.Idle, goonerAnims.Walk...), + AnimFrames: int32(len(goonerAnims.Idle) + len(goonerAnims.Walk)), + Animations: goonerAnims, + YOffset: 0.0, + PlaceholderColor: modelColor, + }) + + // Coomer model with safe loading - using a sphere shape + var coomerModel rl.Model + coomerModel, success, modelColor = SafeLoadModel("resources/models/coomer/idle_notransy.glb", 1, coomerColor) + + if success { + // Only load animations if the model loaded successfully + coomerAnims, _ := loadModelAnimations(map[string]string{ + "idle": "resources/models/coomer/idle_notransy.glb", + "walk": "resources/models/coomer/unsteadywalk_notransy.glb", + }) + + // Apply transformations + transform := rl.MatrixIdentity() + transform = rl.MatrixMultiply(transform, rl.MatrixRotateY(180*rl.Deg2rad)) + transform = rl.MatrixMultiply(transform, rl.MatrixRotateX(-90*rl.Deg2rad)) + transform = rl.MatrixMultiply(transform, rl.MatrixScale(1.0, 1.0, 1.0)) + coomerModel.Transform = transform + + models = append(models, types.ModelAsset{ + Model: coomerModel, + Animation: append(coomerAnims.Idle, coomerAnims.Walk...), + AnimFrames: int32(len(coomerAnims.Idle) + len(coomerAnims.Walk)), + Animations: coomerAnims, + YOffset: -4.0, + PlaceholderColor: rl.Color{}, // Not a placeholder + }) + } else { + // Add a placeholder with different shape/color + models = append(models, types.ModelAsset{ + Model: coomerModel, + YOffset: -4.0, + PlaceholderColor: modelColor, + }) + } + + // Shreke model with safe loading - using a cylinder shape + var shrekeModel rl.Model + shrekeModel, success, modelColor = SafeLoadModel("resources/models/shreke.obj", 2, shrekeColor) + + if success { + // Only proceed with texture if model loaded + shrekeTexture := rl.LoadTexture("resources/models/shreke.png") + if shrekeTexture.ID <= 0 { + rl.TraceLog(rl.LogWarning, "Failed to load shreke texture") + } else { + rl.SetMaterialTexture(shrekeModel.Materials, rl.MapDiffuse, shrekeTexture) + + models = append(models, types.ModelAsset{ + Model: shrekeModel, + Texture: shrekeTexture, + YOffset: 0.0, + PlaceholderColor: rl.Color{}, // Not a placeholder + }) + } + } else { + // Add another placeholder with different shape/color + models = append(models, types.ModelAsset{ + Model: shrekeModel, + YOffset: 0.0, + PlaceholderColor: modelColor, + }) + } + + if len(models) == 0 { + return nil, fmt.Errorf("failed to load any models") + } + + return models, nil } func LoadMusic(filename string) (rl.Music, error) { - return rl.LoadMusicStream(filename), nil + defer func() { + // Recover from any panics during music loading + if r := recover(); r != nil { + rl.TraceLog(rl.LogError, "Panic in LoadMusic: %v", r) + } + }() + + // Skip loading music if environment variable is set + if os.Getenv("GOONSCAPE_DISABLE_AUDIO") == "1" { + rl.TraceLog(rl.LogInfo, "Audio disabled, skipping music loading") + return rl.Music{}, fmt.Errorf("audio disabled") + } + + music := rl.LoadMusicStream(filename) + if music.Stream.Buffer == nil { + return music, fmt.Errorf("failed to load music: %s", filename) + } + return music, nil } diff --git a/game/game.go b/game/game.go index 9df3433..acf6154 100644 --- a/game/game.go +++ b/game/game.go @@ -170,8 +170,18 @@ func (g *Game) DrawPlayer(player *types.Player, model rl.Model) { player.Lock() defer player.Unlock() + // Check for invalid model + if model.Meshes == nil || model.Meshes.VertexCount <= 0 { + // Don't try to draw invalid models + return + } + grid := GetMapGrid() modelIndex := int(player.ID) % len(g.Models) + if modelIndex < 0 || modelIndex >= len(g.Models) { + // Prevent out of bounds access + modelIndex = 0 + } modelAsset := g.Models[modelIndex] const defaultHeight = 8.0 // Default height above tile, fine tune per model in types.ModelAsset @@ -185,16 +195,25 @@ func (g *Game) DrawPlayer(player *types.Player, model rl.Model) { if modelAsset.Animations.Idle != nil || modelAsset.Animations.Walk != nil { if player.IsMoving && len(modelAsset.Animations.Walk) > 0 { anim := modelAsset.Animations.Walk[0] // Use first walk animation - player.AnimationFrame = player.AnimationFrame % anim.FrameCount - rl.UpdateModelAnimation(model, anim, player.AnimationFrame) + if anim.FrameCount > 0 { + player.AnimationFrame = player.AnimationFrame % anim.FrameCount + rl.UpdateModelAnimation(model, anim, player.AnimationFrame) + } } else if len(modelAsset.Animations.Idle) > 0 { anim := modelAsset.Animations.Idle[0] // Use first idle animation - player.AnimationFrame = player.AnimationFrame % anim.FrameCount - rl.UpdateModelAnimation(model, anim, player.AnimationFrame) + if anim.FrameCount > 0 { + player.AnimationFrame = player.AnimationFrame % anim.FrameCount + rl.UpdateModelAnimation(model, anim, player.AnimationFrame) + } } } - rl.DrawModel(model, playerPos, 16, rl.White) + // Use placeholder color if it's set, otherwise use white + var drawColor rl.Color = rl.White + if player.PlaceholderColor.A > 0 { + drawColor = player.PlaceholderColor + } + rl.DrawModel(model, playerPos, 16, drawColor) // Draw floating messages and path indicators if player.FloatingMessage != nil { @@ -228,20 +247,43 @@ func (g *Game) DrawPlayer(player *types.Player, model rl.Model) { func (g *Game) Render() { rl.BeginDrawing() + defer func() { + // This defer will catch any panics that might occur during rendering + // and ensure EndDrawing gets called to maintain proper graphics state + if r := recover(); r != nil { + rl.TraceLog(rl.LogError, "Panic during rendering: %v", r) + } + rl.EndDrawing() + }() + rl.ClearBackground(rl.RayWhite) if !g.isLoggedIn { g.loginScreen.Draw() - rl.EndDrawing() return } rl.BeginMode3D(g.Camera) g.DrawMap() - g.DrawPlayer(g.Player, g.Player.Model) + + // Draw player only if valid + if g.Player != nil && g.Player.Model.Meshes != nil { + g.DrawPlayer(g.Player, g.Player.Model) + } + + // Draw other players with defensive checks for _, other := range g.OtherPlayers { + if other == nil { + continue + } + + // Make sure model is assigned if other.Model.Meshes == nil { g.AssignModelToPlayer(other) + // Skip this frame if assignment failed + if other.Model.Meshes == nil { + continue + } } g.DrawPlayer(other, other.Model) } @@ -268,12 +310,14 @@ func (g *Game) Render() { rl.DrawText(text, int32(pos.X)-textWidth/2, int32(pos.Y), 20, rl.Yellow) } - if g.Player.FloatingMessage != nil { + if g.Player != nil && g.Player.FloatingMessage != nil { drawFloatingMessage(g.Player.FloatingMessage) } for _, other := range g.OtherPlayers { - drawFloatingMessage(other.FloatingMessage) + if other != nil && other.FloatingMessage != nil { + drawFloatingMessage(other.FloatingMessage) + } } // Draw menu if open @@ -282,12 +326,11 @@ func (g *Game) Render() { } // Only draw chat if menu is not open - if !g.MenuOpen { + if !g.MenuOpen && g.Chat != nil { g.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight())) } rl.DrawFPS(10, 10) - rl.EndDrawing() } func (g *Game) Cleanup() { @@ -392,12 +435,33 @@ func (g *Game) HandleServerMessages(messages []*pb.ChatMessage) { } func (g *Game) AssignModelToPlayer(player *types.Player) { + if player == nil { + return + } + + // Defensive check for empty models array + if len(g.Models) == 0 { + rl.TraceLog(rl.LogWarning, "No models available to assign to player") + return + } + modelIndex := int(player.ID) % len(g.Models) + if modelIndex < 0 || modelIndex >= len(g.Models) { + // Prevent out of bounds access + modelIndex = 0 + } + modelAsset := g.Models[modelIndex] - // Just use the original model - don't try to copy it + // Validate model before assigning + if modelAsset.Model.Meshes == nil { + rl.TraceLog(rl.LogWarning, "Trying to assign invalid model to player %d", player.ID) + return + } + player.Model = modelAsset.Model player.Texture = modelAsset.Texture + player.PlaceholderColor = modelAsset.PlaceholderColor } func (g *Game) QuitChan() <-chan struct{} { diff --git a/main.go b/main.go index 165291c..3ec1a57 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "os/signal" "strings" "syscall" + "time" "gitea.boner.be/bdnugget/goonscape/game" "gitea.boner.be/bdnugget/goonscape/network" @@ -14,9 +15,12 @@ import ( ) func main() { + // Set up panic recovery at the top level defer func() { if r := recover(); r != nil { - log.Printf("Recovered from panic in main: %v", r) + log.Printf("Recovered from fatal panic in main: %v", r) + // Give the user a chance to see the error + time.Sleep(5 * time.Second) } }() @@ -46,26 +50,61 @@ func main() { network.SetServerAddr(*addr) } + // Initialize window with error handling + rl.SetConfigFlags(rl.FlagMsaa4xHint) // Enable MSAA for smoother rendering rl.InitWindow(1024, 768, "GoonScape") - rl.SetExitKey(0) - rl.InitAudioDevice() - gameInstance := game.New() - if err := gameInstance.LoadAssets(); err != nil { - log.Printf("Failed to load assets: %v", err) - return + rl.SetExitKey(0) + + // Initialize audio with error handling + if !rl.IsAudioDeviceReady() { + rl.InitAudioDevice() + if !rl.IsAudioDeviceReady() { + log.Println("Warning: Failed to initialize audio device, continuing without audio") + } + } + + // Use a maximum of 3 attempts to load assets + var gameInstance *game.Game + var loadErr error + maxAttempts := 3 + + for attempt := 1; attempt <= maxAttempts; attempt++ { + gameInstance = game.New() + loadErr = gameInstance.LoadAssets() + if loadErr == nil { + break + } + + log.Printf("Attempt %d/%d: Failed to load assets: %v", attempt, maxAttempts, loadErr) + if attempt < maxAttempts { + log.Println("Retrying...") + gameInstance.Cleanup() // Cleanup before retrying + time.Sleep(500 * time.Millisecond) + } + } + + if loadErr != nil { + log.Printf("Failed to load assets after %d attempts. Starting with default assets.", maxAttempts) } defer func() { - gameInstance.Cleanup() + if gameInstance != nil { + gameInstance.Cleanup() + } rl.CloseWindow() - rl.CloseAudioDevice() + if rl.IsAudioDeviceReady() { + rl.CloseAudioDevice() + } }() rl.SetTargetFPS(60) - rl.PlayMusicStream(gameInstance.Music) - rl.SetMusicVolume(gameInstance.Music, 0.5) + // Play music if available + if gameInstance.Music.Stream.Buffer != nil { + rl.PlayMusicStream(gameInstance.Music) + rl.SetMusicVolume(gameInstance.Music, 0.5) + } // Handle OS signals for clean shutdown sigChan := make(chan os.Signal, 1) @@ -80,7 +119,11 @@ func main() { // Keep game loop in main thread for Raylib for !rl.WindowShouldClose() { deltaTime := rl.GetFrameTime() - rl.UpdateMusicStream(gameInstance.Music) + + // Update music if available + if gameInstance.Music.Stream.Buffer != nil { + rl.UpdateMusicStream(gameInstance.Music) + } func() { defer func() { diff --git a/types/player.go b/types/player.go index cfd8473..1ab5a94 100644 --- a/types/player.go +++ b/types/player.go @@ -27,6 +27,7 @@ type Player struct { LastAnimUpdate time.Time LastUpdateTime time.Time InterpolationProgress float32 + PlaceholderColor rl.Color } func (p *Player) MoveTowards(target Tile, deltaTime float32, mapGrid [][]Tile) { diff --git a/types/types.go b/types/types.go index f15c8af..a8b09c2 100644 --- a/types/types.go +++ b/types/types.go @@ -22,12 +22,13 @@ type AnimationSet struct { } type ModelAsset struct { - Model rl.Model - Texture rl.Texture2D - Animation []rl.ModelAnimation // Keep this for compatibility - AnimFrames int32 // Keep this for compatibility - Animations AnimationSet // New field for organized animations - YOffset float32 // Additional height offset (added to default 8.0) + Model rl.Model + Texture rl.Texture2D + Animation []rl.ModelAnimation // Keep this for compatibility + AnimFrames int32 // Keep this for compatibility + Animations AnimationSet // New field for organized animations + YOffset float32 // Additional height offset (added to default 8.0) + PlaceholderColor rl.Color } type ChatMessage struct {