package assets import ( "fmt" "os" "gitea.boner.be/bdnugget/goonscape/types" rl "github.com/gen2brain/raylib-go/raylib" ) // ModelLoader handles loading and fallback for 3D models type ModelLoader struct { safeMode bool } // NewModelLoader creates a new model loader instance func NewModelLoader() *ModelLoader { return &ModelLoader{ safeMode: os.Getenv("GOONSCAPE_SAFE_MODE") == "1", } } // IsSafeMode returns if we should avoid loading external models func (ml *ModelLoader) IsSafeMode() bool { return ml.safeMode || os.Getenv("GOONSCAPE_SAFE_MODE") == "1" } // LoadModel attempts to load a model, returning a placeholder if it fails func (ml *ModelLoader) LoadModel(fileName string, fallbackShape int, fallbackColor rl.Color) (rl.Model, bool, rl.Color) { // Don't even try to load external models in safe mode if ml.IsSafeMode() { rl.TraceLog(rl.LogInfo, "Safe mode enabled, using primitive shape instead of %s", fileName) return ml.createPrimitiveShape(fallbackShape), false, fallbackColor } defer func() { // Recover from any panics during model loading if r := recover(); r != nil { rl.TraceLog(rl.LogError, "Panic in LoadModel: %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 ml.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 (ml *ModelLoader) 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 } // Helper function to load animations for a model 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) if len(idleAnims) > 0 { animSet.Idle = idleAnims rl.TraceLog(rl.LogInfo, "Loaded idle animation: %s (%d frames, %f seconds)", idlePath, idleAnims[0].FrameCount, float32(idleAnims[0].FrameCount)/60.0) } } // Load walk animations if specified if walkPath, ok := animPaths["walk"]; ok { walkAnims := rl.LoadModelAnimations(walkPath) if len(walkAnims) > 0 { animSet.Walk = walkAnims rl.TraceLog(rl.LogInfo, "Loaded walk animation: %s (%d frames, %f seconds)", walkPath, walkAnims[0].FrameCount, float32(walkAnims[0].FrameCount)/60.0) } } 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) { loader := NewModelLoader() return loader.LoadModel(fileName, fallbackShape, fallbackColor) } func LoadModels() ([]types.ModelAsset, error) { // Force safe mode for now until we fix the segfault os.Setenv("GOONSCAPE_SAFE_MODE", "1") models := make([]types.ModelAsset, 0, 3) modelLoader := NewModelLoader() // 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 // If in safe mode, create all models directly without loading if modelLoader.IsSafeMode() { // Gooner model (cube) cube := modelLoader.createPrimitiveShape(0) models = append(models, types.ModelAsset{ Model: cube, YOffset: 0.0, PlaceholderColor: goonerColor, }) // Coomer model (sphere) sphere := modelLoader.createPrimitiveShape(1) models = append(models, types.ModelAsset{ Model: sphere, YOffset: -4.0, PlaceholderColor: coomerColor, }) // Shreke model (cylinder) cylinder := modelLoader.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 = modelLoader.LoadModel("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 = modelLoader.LoadModel("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 = modelLoader.LoadModel("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) { 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 }