Compare commits

...

2 Commits

8 changed files with 445 additions and 102 deletions

View File

@ -1,6 +1,9 @@
package assets package assets
import ( import (
"fmt"
"os"
"gitea.boner.be/bdnugget/goonscape/types" "gitea.boner.be/bdnugget/goonscape/types"
rl "github.com/gen2brain/raylib-go/raylib" rl "github.com/gen2brain/raylib-go/raylib"
) )
@ -9,6 +12,11 @@ import (
func loadModelAnimations(animPaths map[string]string) (types.AnimationSet, error) { func loadModelAnimations(animPaths map[string]string) (types.AnimationSet, error) {
var animSet types.AnimationSet 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 // Load idle animations if specified
if idlePath, ok := animPaths["idle"]; ok { if idlePath, ok := animPaths["idle"]; ok {
idleAnims := rl.LoadModelAnimations(idlePath) idleAnims := rl.LoadModelAnimations(idlePath)
@ -32,10 +40,129 @@ func loadModelAnimations(animPaths map[string]string) (types.AnimationSet, error
return animSet, nil 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) { func LoadModels() ([]types.ModelAsset, error) {
// Goonion model and animations // Force safe mode for now until we fix the segfault
goonerModel := rl.LoadModel("resources/models/gooner/walk_no_y_transform.glb") os.Setenv("GOONSCAPE_SAFE_MODE", "1")
goonerAnims, _ := loadModelAnimations(map[string]string{"idle": "resources/models/gooner/idle_no_y_transform.glb", "walk": "resources/models/gooner/walk_no_y_transform.glb"})
models := make([]types.ModelAsset, 0, 3)
// Use environment variable to completely disable model loading
safeMode := CompletelyAvoidExternalModels()
// 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 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 // Apply transformations
transform := rl.MatrixIdentity() transform := rl.MatrixIdentity()
@ -43,45 +170,105 @@ func LoadModels() ([]types.ModelAsset, error) {
transform = rl.MatrixMultiply(transform, rl.MatrixRotateX(-90*rl.Deg2rad)) transform = rl.MatrixMultiply(transform, rl.MatrixRotateX(-90*rl.Deg2rad))
transform = rl.MatrixMultiply(transform, rl.MatrixScale(1.0, 1.0, 1.0)) transform = rl.MatrixMultiply(transform, rl.MatrixScale(1.0, 1.0, 1.0))
goonerModel.Transform = transform goonerModel.Transform = transform
}
// Coomer model (ready for animations) // Always add a model (real or placeholder)
coomerModel := rl.LoadModel("resources/models/coomer/idle_notransy.glb") models = append(models, types.ModelAsset{
// 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
// 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",
// })
return []types.ModelAsset{
{
Model: goonerModel, Model: goonerModel,
Animation: append(goonerAnims.Idle, goonerAnims.Walk...), Animation: append(goonerAnims.Idle, goonerAnims.Walk...),
AnimFrames: int32(len(goonerAnims.Idle) + len(goonerAnims.Walk)), AnimFrames: int32(len(goonerAnims.Idle) + len(goonerAnims.Walk)),
Animations: goonerAnims, Animations: goonerAnims,
YOffset: 0.0, 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, Model: coomerModel,
Animation: append(coomerAnims.Idle, coomerAnims.Walk...), Animation: append(coomerAnims.Idle, coomerAnims.Walk...),
AnimFrames: int32(len(coomerAnims.Idle) + len(coomerAnims.Walk)), AnimFrames: int32(len(coomerAnims.Idle) + len(coomerAnims.Walk)),
Animations: coomerAnims, Animations: coomerAnims,
YOffset: -4.0, YOffset: -4.0,
}, PlaceholderColor: rl.Color{}, // Not a placeholder
{Model: shrekeModel, Texture: shrekeTexture}, })
}, nil } 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) { 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
} }

View File

@ -2,20 +2,11 @@ package main
import "time" import "time"
// Game world constants
const ( const (
MapWidth = 50 // Server-related constants
MapHeight = 50 ServerTickRate = 600 * time.Millisecond // RuneScape-style tick rate
TileSize = 32 ClientTickRate = 50 * time.Millisecond // Client runs at higher rate for smooth rendering
TileHeight = 2.0 MaxTickDesync = 5 // Max ticks behind before forcing resync
TickRate = 600 * time.Millisecond // Server tick rate (600ms) DefaultPort = "6969" // Default server port
serverAddr = "localhost:6969"
// RuneScape-style tick rate (600ms)
ServerTickRate = 600 * time.Millisecond
// Client might run at a higher tick rate for smooth rendering
ClientTickRate = 50 * time.Millisecond
// Maximum number of ticks we can get behind before forcing a resync
MaxTickDesync = 5
) )

View File

@ -2,6 +2,7 @@ package game
import ( import (
"fmt" "fmt"
"log"
"sync" "sync"
"time" "time"
@ -54,6 +55,12 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()
if len(messages) == 0 {
return
}
log.Printf("Processing %d chat messages", len(messages))
// 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{
@ -69,6 +76,7 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
c.messages = c.messages[1:] c.messages = c.messages[1:]
} }
c.messages = append(c.messages, localMsg) c.messages = append(c.messages, localMsg)
log.Printf("Added chat message from %s: %s", msg.Username, msg.Content)
// Scroll to latest message if it's not already visible // Scroll to latest message if it's not already visible
visibleMessages := int((chatHeight - inputHeight) / messageHeight) visibleMessages := int((chatHeight - inputHeight) / messageHeight)
@ -92,6 +100,9 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
ExpireTime: time.Now().Add(6 * time.Second), ExpireTime: time.Now().Add(6 * time.Second),
} }
otherPlayer.Unlock() otherPlayer.Unlock()
log.Printf("Added floating message to other player %d", msg.PlayerId)
} else {
log.Printf("Could not find other player %d to add floating message", msg.PlayerId)
} }
} }
} }

View File

@ -24,6 +24,7 @@ type Game struct {
loginScreen *LoginScreen loginScreen *LoginScreen
isLoggedIn bool isLoggedIn bool
cleanupOnce sync.Once cleanupOnce sync.Once
frameCounter int // For periodic logging
} }
func New() *Game { func New() *Game {
@ -129,7 +130,28 @@ func (g *Game) Update(deltaTime float32) {
g.Player.MoveTowards(g.Player.TargetPath[0], deltaTime, GetMapGrid()) g.Player.MoveTowards(g.Player.TargetPath[0], deltaTime, GetMapGrid())
} }
// Periodically log information about other players
g.frameCounter++
if g.frameCounter%300 == 0 {
rl.TraceLog(rl.LogInfo, "There are %d other players", len(g.OtherPlayers))
for id, other := range g.OtherPlayers {
rl.TraceLog(rl.LogInfo, "Other player ID: %d, Position: (%f, %f, %f), Has model: %v",
id, other.PosActual.X, other.PosActual.Y, other.PosActual.Z, other.Model.Meshes != nil)
}
}
// Process other players
for _, other := range g.OtherPlayers { for _, other := range g.OtherPlayers {
if other == nil {
continue
}
// Make sure other players have models assigned
if other.Model.Meshes == nil {
g.AssignModelToPlayer(other)
}
// Update other player movement
if len(other.TargetPath) > 0 { if len(other.TargetPath) > 0 {
other.MoveTowards(other.TargetPath[0], deltaTime, GetMapGrid()) other.MoveTowards(other.TargetPath[0], deltaTime, GetMapGrid())
} }
@ -167,11 +189,21 @@ func (g *Game) DrawMap() {
} }
func (g *Game) DrawPlayer(player *types.Player, model rl.Model) { func (g *Game) DrawPlayer(player *types.Player, model rl.Model) {
player.Lock() // No need for lock in rendering, we'll use a "take snapshot" approach
defer player.Unlock() // This avoids potential deadlocks and makes the rendering more consistent
// Check for invalid model
if model.Meshes == nil || model.Meshes.VertexCount <= 0 {
// Don't try to draw invalid models
return
}
grid := GetMapGrid() grid := GetMapGrid()
modelIndex := int(player.ID) % len(g.Models) 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] modelAsset := g.Models[modelIndex]
const defaultHeight = 8.0 // Default height above tile, fine tune per model in types.ModelAsset const defaultHeight = 8.0 // Default height above tile, fine tune per model in types.ModelAsset
@ -185,16 +217,25 @@ func (g *Game) DrawPlayer(player *types.Player, model rl.Model) {
if modelAsset.Animations.Idle != nil || modelAsset.Animations.Walk != nil { if modelAsset.Animations.Idle != nil || modelAsset.Animations.Walk != nil {
if player.IsMoving && len(modelAsset.Animations.Walk) > 0 { if player.IsMoving && len(modelAsset.Animations.Walk) > 0 {
anim := modelAsset.Animations.Walk[0] // Use first walk animation anim := modelAsset.Animations.Walk[0] // Use first walk animation
player.AnimationFrame = player.AnimationFrame % anim.FrameCount if anim.FrameCount > 0 {
rl.UpdateModelAnimation(model, anim, player.AnimationFrame) currentFrame := player.AnimationFrame % anim.FrameCount
rl.UpdateModelAnimation(model, anim, currentFrame)
}
} else if len(modelAsset.Animations.Idle) > 0 { } else if len(modelAsset.Animations.Idle) > 0 {
anim := modelAsset.Animations.Idle[0] // Use first idle animation anim := modelAsset.Animations.Idle[0] // Use first idle animation
player.AnimationFrame = player.AnimationFrame % anim.FrameCount if anim.FrameCount > 0 {
rl.UpdateModelAnimation(model, anim, player.AnimationFrame) currentFrame := player.AnimationFrame % anim.FrameCount
rl.UpdateModelAnimation(model, anim, currentFrame)
}
} }
} }
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 // Draw floating messages and path indicators
if player.FloatingMessage != nil { if player.FloatingMessage != nil {
@ -228,20 +269,43 @@ func (g *Game) DrawPlayer(player *types.Player, model rl.Model) {
func (g *Game) Render() { func (g *Game) Render() {
rl.BeginDrawing() 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) rl.ClearBackground(rl.RayWhite)
if !g.isLoggedIn { if !g.isLoggedIn {
g.loginScreen.Draw() g.loginScreen.Draw()
rl.EndDrawing()
return return
} }
rl.BeginMode3D(g.Camera) rl.BeginMode3D(g.Camera)
g.DrawMap() g.DrawMap()
// Draw player only if valid
if g.Player != nil && g.Player.Model.Meshes != nil {
g.DrawPlayer(g.Player, g.Player.Model) g.DrawPlayer(g.Player, g.Player.Model)
}
// Draw other players with defensive checks
for _, other := range g.OtherPlayers { for _, other := range g.OtherPlayers {
if other == nil {
continue
}
// Make sure model is assigned
if other.Model.Meshes == nil { if other.Model.Meshes == nil {
g.AssignModelToPlayer(other) g.AssignModelToPlayer(other)
// Skip this frame if assignment failed
if other.Model.Meshes == nil {
continue
}
} }
g.DrawPlayer(other, other.Model) g.DrawPlayer(other, other.Model)
} }
@ -268,13 +332,15 @@ func (g *Game) Render() {
rl.DrawText(text, int32(pos.X)-textWidth/2, int32(pos.Y), 20, rl.Yellow) 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) drawFloatingMessage(g.Player.FloatingMessage)
} }
for _, other := range g.OtherPlayers { for _, other := range g.OtherPlayers {
if other != nil && other.FloatingMessage != nil {
drawFloatingMessage(other.FloatingMessage) drawFloatingMessage(other.FloatingMessage)
} }
}
// Draw menu if open // Draw menu if open
if g.MenuOpen { if g.MenuOpen {
@ -282,12 +348,11 @@ func (g *Game) Render() {
} }
// Only draw chat if menu is not open // 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())) g.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight()))
} }
rl.DrawFPS(10, 10) rl.DrawFPS(10, 10)
rl.EndDrawing()
} }
func (g *Game) Cleanup() { func (g *Game) Cleanup() {
@ -392,12 +457,36 @@ func (g *Game) HandleServerMessages(messages []*pb.ChatMessage) {
} }
func (g *Game) AssignModelToPlayer(player *types.Player) { func (g *Game) AssignModelToPlayer(player *types.Player) {
modelIndex := int(player.ID) % len(g.Models) 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
}
// Make sure model index is positive for consistent player appearances
// Use abs value of ID to ensure consistent appearance for negative IDs
modelIndex := abs(int(player.ID)) % len(g.Models)
if modelIndex < 0 || modelIndex >= len(g.Models) {
// Prevent out of bounds access
modelIndex = 0
}
rl.TraceLog(rl.LogInfo, "Assigning model %d to player %d", modelIndex, player.ID)
modelAsset := g.Models[modelIndex] 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.Model = modelAsset.Model
player.Texture = modelAsset.Texture player.Texture = modelAsset.Texture
player.PlaceholderColor = modelAsset.PlaceholderColor
} }
func (g *Game) QuitChan() <-chan struct{} { func (g *Game) QuitChan() <-chan struct{} {

57
main.go
View File

@ -7,6 +7,7 @@ import (
"os/signal" "os/signal"
"strings" "strings"
"syscall" "syscall"
"time"
"gitea.boner.be/bdnugget/goonscape/game" "gitea.boner.be/bdnugget/goonscape/game"
"gitea.boner.be/bdnugget/goonscape/network" "gitea.boner.be/bdnugget/goonscape/network"
@ -14,9 +15,12 @@ import (
) )
func main() { func main() {
// Set up panic recovery at the top level
defer func() { defer func() {
if r := recover(); r != nil { 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) network.SetServerAddr(*addr)
} }
// Initialize window with error handling
rl.SetConfigFlags(rl.FlagMsaa4xHint | rl.FlagWindowResizable) // Enable MSAA and make window resizable
rl.InitWindow(1024, 768, "GoonScape") rl.InitWindow(1024, 768, "GoonScape")
rl.SetExitKey(0)
rl.InitAudioDevice()
gameInstance := game.New() rl.SetExitKey(0)
if err := gameInstance.LoadAssets(); err != nil {
log.Printf("Failed to load assets: %v", err) // Initialize audio with error handling
return 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() { defer func() {
if gameInstance != nil {
gameInstance.Cleanup() gameInstance.Cleanup()
}
rl.CloseWindow() rl.CloseWindow()
if rl.IsAudioDeviceReady() {
rl.CloseAudioDevice() rl.CloseAudioDevice()
}
}() }()
rl.SetTargetFPS(60) rl.SetTargetFPS(60)
// Play music if available
if gameInstance.Music.Stream.Buffer != nil {
rl.PlayMusicStream(gameInstance.Music) rl.PlayMusicStream(gameInstance.Music)
rl.SetMusicVolume(gameInstance.Music, 0.5) rl.SetMusicVolume(gameInstance.Music, 0.5)
}
// Handle OS signals for clean shutdown // Handle OS signals for clean shutdown
sigChan := make(chan os.Signal, 1) sigChan := make(chan os.Signal, 1)
@ -80,7 +119,11 @@ func main() {
// Keep game loop in main thread for Raylib // Keep game loop in main thread for Raylib
for !rl.WindowShouldClose() { for !rl.WindowShouldClose() {
deltaTime := rl.GetFrameTime() deltaTime := rl.GetFrameTime()
// Update music if available
if gameInstance.Music.Stream.Buffer != nil {
rl.UpdateMusicStream(gameInstance.Music) rl.UpdateMusicStream(gameInstance.Music)
}
func() { func() {
defer func() { defer func() {

View File

@ -18,10 +18,11 @@ import (
const protoVersion = 1 const protoVersion = 1
var serverAddr = "boner.be:6969" var serverAddr = "boner.be:6969" // Default server address
func SetServerAddr(addr string) { func SetServerAddr(addr string) {
serverAddr = addr serverAddr = addr
log.Printf("Server address set to: %s", serverAddr)
} }
func ConnectToServer(username, password string, isRegistering bool) (net.Conn, int32, error) { func ConnectToServer(username, password string, isRegistering bool) (net.Conn, int32, error) {
@ -175,22 +176,33 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
lengthBuf := make([]byte, 4) lengthBuf := make([]byte, 4)
if _, err := io.ReadFull(reader, lengthBuf); err != nil { if _, err := io.ReadFull(reader, lengthBuf); err != nil {
if err != io.EOF { if err != io.EOF {
log.Printf("Network read error: %v", err)
errChan <- fmt.Errorf("failed to read message length: %v", err) errChan <- fmt.Errorf("failed to read message length: %v", err)
} else {
log.Printf("Connection closed by server")
} }
return return
} }
messageLength := binary.BigEndian.Uint32(lengthBuf) messageLength := binary.BigEndian.Uint32(lengthBuf)
// Sanity check message size to prevent potential memory issues
if messageLength > 1024*1024 { // 1MB max message size
log.Printf("Message size too large: %d bytes", messageLength)
errChan <- fmt.Errorf("message size too large: %d bytes", messageLength)
return
}
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)
errChan <- fmt.Errorf("failed to read message body: %v", err)
return return
} }
var serverMessage pb.ServerMessage var serverMessage pb.ServerMessage
if err := proto.Unmarshal(messageBuf, &serverMessage); err != nil { if err := proto.Unmarshal(messageBuf, &serverMessage); err != nil {
log.Printf("Failed to unmarshal server message: %v", err) log.Printf("Failed to unmarshal server message: %v", err)
continue continue // Skip this message but don't quit
} }
player.Lock() player.Lock()
@ -207,7 +219,11 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
} }
player.Unlock() player.Unlock()
// Process player states
validPlayerIds := make(map[int32]bool)
for _, state := range serverMessage.Players { for _, state := range serverMessage.Players {
validPlayerIds[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
@ -223,21 +239,26 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
continue continue
} }
// Update or create other players
if otherPlayer, exists := otherPlayers[state.PlayerId]; exists { if otherPlayer, exists := otherPlayers[state.PlayerId]; exists {
otherPlayer.UpdatePosition(state, types.ServerTickRate) otherPlayer.UpdatePosition(state, types.ServerTickRate)
} else { } else {
log.Printf("Creating new player with ID: %d", state.PlayerId)
otherPlayers[state.PlayerId] = types.NewPlayer(state) otherPlayers[state.PlayerId] = types.NewPlayer(state)
} }
} }
// Remove players that are no longer in the server state // Remove players no longer in the server state
for id := range otherPlayers { for id := range otherPlayers {
if id != playerID { if id != playerID && !validPlayerIds[id] {
log.Printf("Removing player with ID: %d", id)
delete(otherPlayers, id) delete(otherPlayers, id)
} }
} }
// Handle chat messages
if handler, ok := player.UserData.(types.ChatMessageHandler); ok && len(serverMessage.ChatMessages) > 0 { if handler, ok := player.UserData.(types.ChatMessageHandler); ok && len(serverMessage.ChatMessages) > 0 {
log.Printf("Received %d chat messages from server", len(serverMessage.ChatMessages))
handler.HandleServerMessages(serverMessage.ChatMessages) handler.HandleServerMessages(serverMessage.ChatMessages)
} }
} }

View File

@ -9,7 +9,7 @@ import (
) )
type Player struct { type Player struct {
sync.RWMutex sync.RWMutex // Keep this for network operations
Model rl.Model Model rl.Model
Texture rl.Texture2D Texture rl.Texture2D
PosActual rl.Vector3 PosActual rl.Vector3
@ -27,12 +27,11 @@ type Player struct {
LastAnimUpdate time.Time LastAnimUpdate time.Time
LastUpdateTime time.Time LastUpdateTime time.Time
InterpolationProgress float32 InterpolationProgress float32
PlaceholderColor rl.Color
} }
func (p *Player) MoveTowards(target Tile, deltaTime float32, mapGrid [][]Tile) { func (p *Player) MoveTowards(target Tile, deltaTime float32, mapGrid [][]Tile) {
p.Lock() // No need for lock here as this is called from a single thread (game loop)
defer p.Unlock()
targetPos := rl.Vector3{ targetPos := rl.Vector3{
X: float32(target.X * TileSize), X: float32(target.X * TileSize),
Y: mapGrid[target.X][target.Y].Height * TileHeight, Y: mapGrid[target.X][target.Y].Height * TileHeight,
@ -52,7 +51,7 @@ func (p *Player) MoveTowards(target Tile, deltaTime float32, mapGrid [][]Tile) {
oldFrame := p.AnimationFrame oldFrame := p.AnimationFrame
p.AnimationFrame += int32(deltaTime * 60) p.AnimationFrame += int32(deltaTime * 60)
rl.TraceLog(rl.LogInfo, "Walk frame update: %d -> %d (delta: %f)", rl.TraceLog(rl.LogDebug, "Walk frame update: %d -> %d (delta: %f)",
oldFrame, p.AnimationFrame, deltaTime) oldFrame, p.AnimationFrame, deltaTime)
} else { } else {
wasMoving := p.IsMoving wasMoving := p.IsMoving
@ -64,7 +63,7 @@ func (p *Player) MoveTowards(target Tile, deltaTime float32, mapGrid [][]Tile) {
oldFrame := p.AnimationFrame oldFrame := p.AnimationFrame
p.AnimationFrame += int32(deltaTime * 60) p.AnimationFrame += int32(deltaTime * 60)
rl.TraceLog(rl.LogInfo, "Idle frame update: %d -> %d (delta: %f)", rl.TraceLog(rl.LogDebug, "Idle frame update: %d -> %d (delta: %f)",
oldFrame, p.AnimationFrame, deltaTime) oldFrame, p.AnimationFrame, deltaTime)
} }
@ -115,6 +114,7 @@ func (p *Player) UpdatePosition(state *pb.PlayerState, tickRate time.Duration) {
} }
func (p *Player) ForceResync(state *pb.PlayerState) { func (p *Player) ForceResync(state *pb.PlayerState) {
// Keep this lock since it's called from the network goroutine
p.Lock() p.Lock()
defer p.Unlock() defer p.Unlock()

View File

@ -28,6 +28,7 @@ type ModelAsset struct {
AnimFrames int32 // Keep this for compatibility AnimFrames int32 // Keep this for compatibility
Animations AnimationSet // New field for organized animations Animations AnimationSet // New field for organized animations
YOffset float32 // Additional height offset (added to default 8.0) YOffset float32 // Additional height offset (added to default 8.0)
PlaceholderColor rl.Color
} }
type ChatMessage struct { type ChatMessage struct {