Compare commits
7 Commits
630877cd34
...
8211938879
Author | SHA1 | Date | |
---|---|---|---|
8211938879 | |||
53cc9bca6b | |||
a1aeb71512 | |||
ef3732c53c | |||
7be859d58f | |||
fde07e3e48 | |||
f966d538d3 |
5
2
Normal file
5
2
Normal file
@ -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
|
12
README.md
12
README.md
@ -122,3 +122,15 @@ The project uses Protocol Buffers for network communication. If you modify the `
|
|||||||
```bash
|
```bash
|
||||||
protoc --go_out=. goonserver/actions/actions.proto
|
protoc --go_out=. goonserver/actions/actions.proto
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Fixing the models
|
||||||
|
This is a note/reminder to myself. The models and animations are generated with [meshy.ai (referral link, give me credits pls)](https://www.meshy.ai/?utm_source=referral-program&utm_medium=link&utm_content=TUZLRK). Somehow the bones armature things are not in the same root as the model and it becomes like a weird pudding when using the animation. I had to manually fix this in Blender by doing:
|
||||||
|
- Selected the char1 mesh thing
|
||||||
|
- Alt + P (the right Alt kek) => Clear Parent and Keep Transform
|
||||||
|
- Tried to do the same for Armature, they should both be in the Scene collection as siblings, so straight from root
|
||||||
|
- Select char1 and press Ctrl + A => All Transforms
|
||||||
|
- Repeat for Armature
|
||||||
|
- Select char1 and make sure it still has the Armature Modifier attached (via purple wrench icon), I remove it and reapply it just to be sure
|
||||||
|
|
||||||
|
When exporting it, I make sure Transform +Y is up direction is turned off, otherwise my gooner walks on the walls instead of the floor lmao
|
||||||
|
|
||||||
|
306
assets/assets.go
306
assets/assets.go
@ -2,87 +2,28 @@ package assets
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"sync"
|
||||||
|
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/logging"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ModelLoader handles loading and fallback for 3D models
|
var (
|
||||||
type ModelLoader struct {
|
assetMutex sync.RWMutex
|
||||||
safeMode bool
|
loadedModels map[string]types.ModelAsset
|
||||||
}
|
audioMutex sync.Mutex
|
||||||
|
audioInitialized bool
|
||||||
|
)
|
||||||
|
|
||||||
// NewModelLoader creates a new model loader instance
|
func init() {
|
||||||
func NewModelLoader() *ModelLoader {
|
loadedModels = make(map[string]types.ModelAsset)
|
||||||
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
|
// Helper function to load animations for a model
|
||||||
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)
|
||||||
@ -106,84 +47,29 @@ 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) {
|
|
||||||
loader := NewModelLoader()
|
|
||||||
return loader.LoadModel(fileName, fallbackShape, fallbackColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
func LoadModels() ([]types.ModelAsset, error) {
|
func LoadModels() ([]types.ModelAsset, error) {
|
||||||
// Force safe mode for now until we fix the segfault
|
logging.Info.Println("Loading models")
|
||||||
os.Setenv("GOONSCAPE_SAFE_MODE", "1")
|
assetMutex.Lock()
|
||||||
|
defer assetMutex.Unlock()
|
||||||
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,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
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
|
return models, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// The rest of the function with normal model loading
|
// Goonion model and animations
|
||||||
// Load Goonion model with error handling
|
goonerModel := rl.LoadModel("resources/models/gooner/walk_no_y_transform.glb")
|
||||||
var goonerModel rl.Model
|
goonerAnims, err := loadModelAnimations(map[string]string{
|
||||||
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",
|
"idle": "resources/models/gooner/idle_no_y_transform.glb",
|
||||||
"walk": "resources/models/gooner/walk_no_y_transform.glb",
|
"walk": "resources/models/gooner/walk_no_y_transform.glb",
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Apply transformations
|
// Apply transformations
|
||||||
transform := rl.MatrixIdentity()
|
transform := rl.MatrixIdentity()
|
||||||
@ -191,105 +77,99 @@ 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
|
||||||
}
|
|
||||||
|
|
||||||
// Always add a model (real or placeholder)
|
// Coomer model (ready for animations)
|
||||||
models = append(models, types.ModelAsset{
|
coomerModel := rl.LoadModel("resources/models/coomer/idle_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/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
|
||||||
|
|
||||||
|
// Store loaded models
|
||||||
|
models := []types.ModelAsset{
|
||||||
|
{
|
||||||
|
Name: "gooner",
|
||||||
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 = 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,
|
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
|
},
|
||||||
})
|
{
|
||||||
} 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,
|
Model: shrekeModel,
|
||||||
Texture: shrekeTexture,
|
Animation: append(shrekeAnims.Idle, shrekeAnims.Walk...),
|
||||||
|
AnimFrames: int32(len(shrekeAnims.Idle) + len(shrekeAnims.Walk)),
|
||||||
|
Animations: shrekeAnims,
|
||||||
YOffset: 0.0,
|
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 {
|
for _, model := range models {
|
||||||
return nil, fmt.Errorf("failed to load any models")
|
loadedModels[model.Name] = model
|
||||||
}
|
}
|
||||||
|
|
||||||
return models, nil
|
return models, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadMusic(filename string) (rl.Music, error) {
|
func LoadMusic(filename string) (rl.Music, error) {
|
||||||
defer func() {
|
logging.Info.Printf("Loading music from %s", filename)
|
||||||
// Recover from any panics during music loading
|
audioMutex.Lock()
|
||||||
if r := recover(); r != nil {
|
defer audioMutex.Unlock()
|
||||||
rl.TraceLog(rl.LogError, "Panic in LoadMusic: %v", r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Skip loading music if environment variable is set
|
if !rl.IsAudioDeviceReady() {
|
||||||
if os.Getenv("GOONSCAPE_DISABLE_AUDIO") == "1" {
|
err := fmt.Errorf("audio device not initialized")
|
||||||
rl.TraceLog(rl.LogInfo, "Audio disabled, skipping music loading")
|
logging.Error.Println(err)
|
||||||
return rl.Music{}, fmt.Errorf("audio disabled")
|
return rl.Music{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
music := rl.LoadMusicStream(filename)
|
music := rl.LoadMusicStream(filename)
|
||||||
if music.Stream.Buffer == nil {
|
if music.CtxType == 0 {
|
||||||
return music, fmt.Errorf("failed to load music: %s", filename)
|
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
|
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++ {
|
||||||
|
rl.UnloadModelAnimation(model.Animation[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rl.UnloadModel(model.Model)
|
||||||
|
rl.UnloadTexture(model.Texture)
|
||||||
|
delete(loadedModels, model.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnloadMusic(music rl.Music) {
|
||||||
|
rl.UnloadMusicStream(music)
|
||||||
|
}
|
||||||
|
7
config/config.go
Normal file
7
config/config.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
PlayMusic bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var Current = Config{PlayMusic: true}
|
31
constants.go
31
constants.go
@ -2,33 +2,20 @@ package main
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
// Game world constants
|
|
||||||
const (
|
const (
|
||||||
// Server-related constants
|
|
||||||
ServerTickRate = 600 * time.Millisecond // RuneScape-style tick rate
|
|
||||||
ClientTickRate = 50 * time.Millisecond // Client runs at higher rate for smooth rendering
|
|
||||||
MaxTickDesync = 5 // Max ticks behind before forcing resync
|
|
||||||
DefaultPort = "6969" // Default server port
|
|
||||||
|
|
||||||
// Map constants
|
|
||||||
MapWidth = 50
|
MapWidth = 50
|
||||||
MapHeight = 50
|
MapHeight = 50
|
||||||
TileSize = 32
|
TileSize = 32
|
||||||
TileHeight = 2.0
|
TileHeight = 2.0
|
||||||
)
|
TickRate = 600 * time.Millisecond // Server tick rate (600ms)
|
||||||
|
serverAddr = "localhost:6969"
|
||||||
|
|
||||||
// UI constants
|
// RuneScape-style tick rate (600ms)
|
||||||
const (
|
ServerTickRate = 600 * time.Millisecond
|
||||||
ChatMargin = 10
|
|
||||||
ChatHeight = 200
|
|
||||||
MessageHeight = 20
|
|
||||||
InputHeight = 30
|
|
||||||
MaxMessages = 50
|
|
||||||
)
|
|
||||||
|
|
||||||
// Environment variable names
|
// Client might run at a higher tick rate for smooth rendering
|
||||||
const (
|
ClientTickRate = 50 * time.Millisecond
|
||||||
EnvSafeMode = "GOONSCAPE_SAFE_MODE"
|
|
||||||
EnvDisableAnimations = "GOONSCAPE_DISABLE_ANIMATIONS"
|
// Maximum number of ticks we can get behind before forcing a resync
|
||||||
EnvDisableAudio = "GOONSCAPE_DISABLE_AUDIO"
|
MaxTickDesync = 5
|
||||||
)
|
)
|
||||||
|
97
game/chat.go
97
game/chat.go
@ -2,7 +2,6 @@ package game
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -11,24 +10,28 @@ import (
|
|||||||
rl "github.com/gen2brain/raylib-go/raylib"
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Local UI constants (these could be moved to a centralized constants package later)
|
|
||||||
const (
|
const (
|
||||||
|
maxMessages = 50
|
||||||
|
chatMargin = 10 // Margin from screen edges
|
||||||
|
chatHeight = 200
|
||||||
|
messageHeight = 20
|
||||||
|
inputHeight = 30
|
||||||
runeLimit = 256
|
runeLimit = 256
|
||||||
)
|
)
|
||||||
|
|
||||||
type Chat struct {
|
type Chat struct {
|
||||||
|
sync.RWMutex
|
||||||
messages []types.ChatMessage
|
messages []types.ChatMessage
|
||||||
inputBuffer []rune
|
inputBuffer []rune
|
||||||
isTyping bool
|
isTyping bool
|
||||||
cursorPos int
|
cursorPos int
|
||||||
scrollOffset int
|
scrollOffset int
|
||||||
userData interface{}
|
userData interface{}
|
||||||
mutex sync.RWMutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewChat() *Chat {
|
func NewChat() *Chat {
|
||||||
return &Chat{
|
return &Chat{
|
||||||
messages: make([]types.ChatMessage, 0, types.MaxChatMessages),
|
messages: make([]types.ChatMessage, 0, maxMessages),
|
||||||
inputBuffer: make([]rune, 0, runeLimit),
|
inputBuffer: make([]rune, 0, runeLimit),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -40,7 +43,7 @@ func (c *Chat) AddMessage(playerID int32, content string) {
|
|||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(c.messages) >= types.MaxChatMessages {
|
if len(c.messages) >= maxMessages {
|
||||||
c.messages = c.messages[1:]
|
c.messages = c.messages[1:]
|
||||||
}
|
}
|
||||||
c.messages = append(c.messages, msg)
|
c.messages = append(c.messages, msg)
|
||||||
@ -48,23 +51,10 @@ func (c *Chat) AddMessage(playerID int32, content string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
|
func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
|
||||||
c.mutex.Lock()
|
c.Lock()
|
||||||
defer c.mutex.Unlock()
|
defer c.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 {
|
||||||
// Skip invalid messages
|
|
||||||
if msg == nil {
|
|
||||||
log.Printf("Warning: Received nil chat message")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
localMsg := types.ChatMessage{
|
localMsg := types.ChatMessage{
|
||||||
PlayerID: msg.PlayerId,
|
PlayerID: msg.PlayerId,
|
||||||
Username: msg.Username,
|
Username: msg.Username,
|
||||||
@ -74,54 +64,34 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
|
|||||||
|
|
||||||
// Only add if it's not already in our history
|
// Only add if it's not already in our history
|
||||||
if len(c.messages) == 0 || c.messages[len(c.messages)-1].Time.UnixNano() < msg.Timestamp {
|
if len(c.messages) == 0 || c.messages[len(c.messages)-1].Time.UnixNano() < msg.Timestamp {
|
||||||
if len(c.messages) >= types.MaxChatMessages {
|
if len(c.messages) >= maxMessages {
|
||||||
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((types.ChatHeight - types.InputHeight) / types.MessageHeight)
|
visibleMessages := int((chatHeight - inputHeight) / messageHeight)
|
||||||
if len(c.messages) > visibleMessages {
|
if len(c.messages) > visibleMessages {
|
||||||
c.scrollOffset = len(c.messages) - visibleMessages
|
c.scrollOffset = len(c.messages) - visibleMessages
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add floating message to the player
|
// Add floating message to the player
|
||||||
if game, ok := c.userData.(*Game); ok && game != nil {
|
if game, ok := c.userData.(*Game); ok {
|
||||||
// Make sure each game component exists before using it
|
if msg.PlayerId == game.Player.ID {
|
||||||
if game.PlayerManager == nil {
|
game.Player.Lock()
|
||||||
log.Printf("Warning: PlayerManager is nil when processing chat message")
|
game.Player.FloatingMessage = &types.FloatingMessage{
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if msg.PlayerId == game.PlayerManager.LocalPlayer.ID {
|
|
||||||
// Check if local player exists
|
|
||||||
if game.PlayerManager.LocalPlayer == nil {
|
|
||||||
log.Printf("Warning: Local player is nil when trying to add floating message")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
game.PlayerManager.LocalPlayer.Lock()
|
|
||||||
game.PlayerManager.LocalPlayer.FloatingMessage = &types.FloatingMessage{
|
|
||||||
Content: msg.Content,
|
Content: msg.Content,
|
||||||
ExpireTime: time.Now().Add(6 * time.Second),
|
ExpireTime: time.Now().Add(6 * time.Second),
|
||||||
}
|
}
|
||||||
game.PlayerManager.LocalPlayer.Unlock()
|
game.Player.Unlock()
|
||||||
} else {
|
} else if otherPlayer, exists := game.OtherPlayers.Load(msg.PlayerId); exists {
|
||||||
// The other player might not be in our list yet, handle safely
|
other := otherPlayer.(*types.Player)
|
||||||
player := game.PlayerManager.GetPlayer(msg.PlayerId)
|
other.Lock()
|
||||||
if player == nil {
|
other.FloatingMessage = &types.FloatingMessage{
|
||||||
log.Printf("Could not find other player %d to add floating message (player not in game yet)", msg.PlayerId)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
player.Lock()
|
|
||||||
player.FloatingMessage = &types.FloatingMessage{
|
|
||||||
Content: msg.Content,
|
Content: msg.Content,
|
||||||
ExpireTime: time.Now().Add(6 * time.Second),
|
ExpireTime: time.Now().Add(6 * time.Second),
|
||||||
}
|
}
|
||||||
player.Unlock()
|
other.Unlock()
|
||||||
log.Printf("Added floating message to other player %d", msg.PlayerId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -129,20 +99,19 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Chat) Draw(screenWidth, screenHeight int32) {
|
func (c *Chat) Draw(screenWidth, screenHeight int32) {
|
||||||
c.mutex.RLock()
|
c.RLock()
|
||||||
defer c.mutex.RUnlock()
|
defer c.RUnlock()
|
||||||
|
|
||||||
// Calculate chat window width based on screen width
|
// Calculate chat window width based on screen width
|
||||||
chatWindowWidth := screenWidth - (types.ChatMargin * 2)
|
chatWindowWidth := screenWidth - (chatMargin * 2)
|
||||||
|
|
||||||
// Draw chat window background
|
// Draw chat window background
|
||||||
chatX := float32(types.ChatMargin)
|
chatX := float32(chatMargin)
|
||||||
chatY := float32(screenHeight - types.ChatHeight - types.ChatMargin)
|
chatY := float32(screenHeight - chatHeight - chatMargin)
|
||||||
rl.DrawRectangle(int32(chatX), int32(chatY), chatWindowWidth, types.ChatHeight, rl.ColorAlpha(rl.Black, 0.5))
|
rl.DrawRectangle(int32(chatX), int32(chatY), chatWindowWidth, chatHeight, rl.ColorAlpha(rl.Black, 0.5))
|
||||||
|
|
||||||
// Draw messages from oldest to newest
|
// Draw messages from oldest to newest
|
||||||
messageY := chatY + 5
|
messageY := chatY + 5
|
||||||
visibleMessages := int((types.ChatHeight - types.InputHeight) / types.MessageHeight)
|
visibleMessages := int((chatHeight - inputHeight) / messageHeight)
|
||||||
|
|
||||||
// Auto-scroll to bottom if no manual scrolling has occurred
|
// Auto-scroll to bottom if no manual scrolling has occurred
|
||||||
if c.scrollOffset == 0 {
|
if c.scrollOffset == 0 {
|
||||||
@ -164,12 +133,12 @@ func (c *Chat) Draw(screenWidth, screenHeight int32) {
|
|||||||
}
|
}
|
||||||
text := fmt.Sprintf("%s: %s", msg.Username, msg.Content)
|
text := fmt.Sprintf("%s: %s", msg.Username, msg.Content)
|
||||||
rl.DrawText(text, int32(chatX)+5, int32(messageY), 20, color)
|
rl.DrawText(text, int32(chatX)+5, int32(messageY), 20, color)
|
||||||
messageY += types.MessageHeight
|
messageY += messageHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw input field
|
// Draw input field
|
||||||
inputY := chatY + float32(types.ChatHeight-types.InputHeight)
|
inputY := chatY + float32(chatHeight-inputHeight)
|
||||||
rl.DrawRectangle(int32(chatX), int32(inputY), chatWindowWidth, types.InputHeight, rl.ColorAlpha(rl.White, 0.3))
|
rl.DrawRectangle(int32(chatX), int32(inputY), chatWindowWidth, inputHeight, rl.ColorAlpha(rl.White, 0.3))
|
||||||
if c.isTyping {
|
if c.isTyping {
|
||||||
inputText := string(c.inputBuffer)
|
inputText := string(c.inputBuffer)
|
||||||
rl.DrawText(inputText, int32(chatX)+5, int32(inputY)+5, 20, rl.White)
|
rl.DrawText(inputText, int32(chatX)+5, int32(inputY)+5, 20, rl.White)
|
||||||
@ -187,7 +156,7 @@ func (c *Chat) Update() (string, bool) {
|
|||||||
if !c.isTyping {
|
if !c.isTyping {
|
||||||
wheelMove := rl.GetMouseWheelMove()
|
wheelMove := rl.GetMouseWheelMove()
|
||||||
if wheelMove != 0 {
|
if wheelMove != 0 {
|
||||||
maxScroll := max(0, len(c.messages)-int((types.ChatHeight-types.InputHeight)/types.MessageHeight))
|
maxScroll := max(0, len(c.messages)-int((chatHeight-inputHeight)/messageHeight))
|
||||||
c.scrollOffset = clamp(c.scrollOffset-int(wheelMove), 0, maxScroll)
|
c.scrollOffset = clamp(c.scrollOffset-int(wheelMove), 0, maxScroll)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,107 +0,0 @@
|
|||||||
package game
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/types"
|
|
||||||
rl "github.com/gen2brain/raylib-go/raylib"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PlayerManager handles all player-related operations
|
|
||||||
type PlayerManager struct {
|
|
||||||
LocalPlayer *types.Player
|
|
||||||
OtherPlayers map[int32]*types.Player
|
|
||||||
mutex sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPlayerManager creates a new player manager
|
|
||||||
func NewPlayerManager() *PlayerManager {
|
|
||||||
return &PlayerManager{
|
|
||||||
OtherPlayers: make(map[int32]*types.Player),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPlayer returns the player with the given ID, or the local player if ID matches
|
|
||||||
func (pm *PlayerManager) GetPlayer(id int32) *types.Player {
|
|
||||||
pm.mutex.RLock()
|
|
||||||
defer pm.mutex.RUnlock()
|
|
||||||
|
|
||||||
if pm.LocalPlayer != nil && pm.LocalPlayer.ID == id {
|
|
||||||
return pm.LocalPlayer
|
|
||||||
}
|
|
||||||
|
|
||||||
return pm.OtherPlayers[id]
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddPlayer adds a player to the manager
|
|
||||||
func (pm *PlayerManager) AddPlayer(player *types.Player) {
|
|
||||||
pm.mutex.Lock()
|
|
||||||
defer pm.mutex.Unlock()
|
|
||||||
|
|
||||||
pm.OtherPlayers[player.ID] = player
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemovePlayer removes a player from the manager
|
|
||||||
func (pm *PlayerManager) RemovePlayer(id int32) {
|
|
||||||
pm.mutex.Lock()
|
|
||||||
defer pm.mutex.Unlock()
|
|
||||||
|
|
||||||
delete(pm.OtherPlayers, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AssetManager handles all game assets
|
|
||||||
type AssetManager struct {
|
|
||||||
Models []types.ModelAsset
|
|
||||||
Music rl.Music
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAssetManager creates a new asset manager
|
|
||||||
func NewAssetManager() *AssetManager {
|
|
||||||
return &AssetManager{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetModelForPlayer returns the appropriate model for a player
|
|
||||||
func (am *AssetManager) GetModelForPlayer(playerID int32) (types.ModelAsset, bool) {
|
|
||||||
if len(am.Models) == 0 {
|
|
||||||
return types.ModelAsset{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple model assignment based on player ID
|
|
||||||
modelIndex := int(playerID) % len(am.Models)
|
|
||||||
return am.Models[modelIndex], true
|
|
||||||
}
|
|
||||||
|
|
||||||
// UIManager manages all user interface components
|
|
||||||
type UIManager struct {
|
|
||||||
Chat *Chat
|
|
||||||
LoginScreen *LoginScreen
|
|
||||||
IsLoggedIn bool
|
|
||||||
MenuOpen bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewUIManager creates a new UI manager
|
|
||||||
func NewUIManager() *UIManager {
|
|
||||||
return &UIManager{
|
|
||||||
Chat: NewChat(),
|
|
||||||
LoginScreen: NewLoginScreen(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleChatInput processes chat input and returns messages to send
|
|
||||||
func (ui *UIManager) HandleChatInput() (string, bool) {
|
|
||||||
return ui.Chat.Update()
|
|
||||||
}
|
|
||||||
|
|
||||||
// DrawUI renders all UI components
|
|
||||||
func (ui *UIManager) DrawUI(screenWidth, screenHeight int32) {
|
|
||||||
if !ui.IsLoggedIn {
|
|
||||||
ui.LoginScreen.Draw()
|
|
||||||
} else {
|
|
||||||
if ui.MenuOpen {
|
|
||||||
// Draw menu
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw chat always when logged in
|
|
||||||
ui.Chat.Draw(screenWidth, screenHeight)
|
|
||||||
}
|
|
||||||
}
|
|
38
game/context.go
Normal file
38
game/context.go
Normal file
@ -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()
|
||||||
|
}
|
427
game/game.go
427
game/game.go
@ -1,225 +1,157 @@
|
|||||||
package game
|
package game
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"fmt"
|
||||||
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/assets"
|
"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/network"
|
||||||
"gitea.boner.be/bdnugget/goonscape/types"
|
"gitea.boner.be/bdnugget/goonscape/types"
|
||||||
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
||||||
rl "github.com/gen2brain/raylib-go/raylib"
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var audioMutex sync.Mutex
|
||||||
|
var audioInitOnce sync.Once
|
||||||
|
|
||||||
type Game struct {
|
type Game struct {
|
||||||
// Component-based architecture
|
ctx *GameContext
|
||||||
PlayerManager *PlayerManager
|
Player *types.Player
|
||||||
AssetManager *AssetManager
|
OtherPlayers sync.Map // Using sync.Map for concurrent access
|
||||||
UIManager *UIManager
|
|
||||||
|
|
||||||
// Core game state
|
|
||||||
Camera rl.Camera3D
|
Camera rl.Camera3D
|
||||||
quitChan chan struct{}
|
Models []types.ModelAsset
|
||||||
cleanupOnce sync.Once
|
Music rl.Music
|
||||||
frameCounter int // For periodic logging
|
Chat *Chat
|
||||||
|
MenuOpen atomic.Bool
|
||||||
// Legacy fields for backward compatibility
|
QuitChan chan struct{} // Channel to signal shutdown
|
||||||
Player *types.Player // Use PlayerManager.LocalPlayer instead
|
loginScreen *LoginScreen
|
||||||
OtherPlayers map[int32]*types.Player // Use PlayerManager.OtherPlayers instead
|
isLoggedIn atomic.Bool
|
||||||
Models []types.ModelAsset // Use AssetManager.Models instead
|
|
||||||
Music rl.Music // Use AssetManager.Music instead
|
|
||||||
Chat *Chat // Use UIManager.Chat instead
|
|
||||||
MenuOpen bool // Use UIManager.MenuOpen instead
|
|
||||||
loginScreen *LoginScreen // Use UIManager.LoginScreen instead
|
|
||||||
isLoggedIn bool // Use UIManager.IsLoggedIn instead
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() *Game {
|
func New() *Game {
|
||||||
// Create managers
|
InitWorld()
|
||||||
playerManager := NewPlayerManager()
|
game := &Game{
|
||||||
assetManager := NewAssetManager()
|
ctx: NewGameContext(),
|
||||||
uiManager := NewUIManager()
|
OtherPlayers: sync.Map{},
|
||||||
|
|
||||||
g := &Game{
|
|
||||||
PlayerManager: playerManager,
|
|
||||||
AssetManager: assetManager,
|
|
||||||
UIManager: uiManager,
|
|
||||||
Camera: rl.Camera3D{
|
Camera: rl.Camera3D{
|
||||||
Position: rl.NewVector3(0.0, 20.0, 0.0),
|
Position: rl.NewVector3(0, 10, 10),
|
||||||
Target: rl.NewVector3(0.0, 0.0, 0.0),
|
Target: rl.NewVector3(0, 0, 0),
|
||||||
Up: rl.NewVector3(0.0, 1.0, 0.0),
|
Up: rl.NewVector3(0, 1, 0),
|
||||||
Fovy: 45.0,
|
Fovy: 45.0,
|
||||||
Projection: rl.CameraPerspective,
|
Projection: rl.CameraPerspective,
|
||||||
},
|
},
|
||||||
quitChan: make(chan struct{}),
|
Chat: NewChat(),
|
||||||
// Initialize empty maps to avoid nil references
|
QuitChan: make(chan struct{}),
|
||||||
OtherPlayers: make(map[int32]*types.Player),
|
loginScreen: NewLoginScreen(),
|
||||||
}
|
}
|
||||||
|
game.Chat.userData = game
|
||||||
// Initialize legacy fields (for backward compatibility)
|
return game
|
||||||
g.Player = g.PlayerManager.LocalPlayer
|
|
||||||
g.OtherPlayers = g.PlayerManager.OtherPlayers
|
|
||||||
g.Models = g.AssetManager.Models
|
|
||||||
g.Music = g.AssetManager.Music
|
|
||||||
g.Chat = g.UIManager.Chat
|
|
||||||
g.MenuOpen = g.UIManager.MenuOpen
|
|
||||||
g.loginScreen = g.UIManager.LoginScreen
|
|
||||||
g.isLoggedIn = g.UIManager.IsLoggedIn
|
|
||||||
|
|
||||||
// Set up inter-component references
|
|
||||||
g.Chat.userData = g // Pass game instance to chat for callbacks
|
|
||||||
|
|
||||||
// Initialize world
|
|
||||||
InitWorld()
|
|
||||||
|
|
||||||
return g
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) LoadAssets() error {
|
func (g *Game) LoadAssets() error {
|
||||||
return SafeExecute(func() error {
|
audioMutex.Lock()
|
||||||
// Load models
|
defer audioMutex.Unlock()
|
||||||
|
|
||||||
|
logging.Info.Println("Loading game assets")
|
||||||
var err error
|
var err error
|
||||||
models, err := assets.LoadModels()
|
|
||||||
|
// Load models first
|
||||||
|
g.Models, err = assets.LoadModels()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Warning: Failed to load models: %v", err)
|
logging.Error.Printf("Failed to load models: %v", err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
g.AssetManager.Models = models
|
|
||||||
|
|
||||||
// Update legacy field
|
// Load music only if enabled
|
||||||
g.Models = models
|
if config.Current.PlayMusic {
|
||||||
|
logging.Info.Println("Loading music stream")
|
||||||
// Try to load music
|
g.Music = rl.LoadMusicStream("resources/audio/GoonScape2.mp3")
|
||||||
music, err := assets.LoadMusic("resources/audio/music.mp3")
|
if g.Music.CtxType == 0 {
|
||||||
if err != nil {
|
logging.Error.Println("Failed to load music stream")
|
||||||
log.Printf("Warning: Failed to load music: %v", err)
|
return fmt.Errorf("failed to load music stream")
|
||||||
|
}
|
||||||
|
logging.Info.Println("Music stream loaded successfully")
|
||||||
} else {
|
} else {
|
||||||
g.AssetManager.Music = music
|
logging.Info.Println("Music disabled by config")
|
||||||
// Update legacy field
|
|
||||||
g.Music = music
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logging.Info.Println("Assets loaded successfully")
|
||||||
return nil
|
return nil
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) Update(deltaTime float32) {
|
func (g *Game) Update(deltaTime float32) {
|
||||||
// Legacy code to maintain compatibility
|
if !g.isLoggedIn.Load() {
|
||||||
if !g.UIManager.IsLoggedIn {
|
username, password, isRegistering, submitted := g.loginScreen.Update()
|
||||||
// Handle login
|
if submitted {
|
||||||
username, password, isRegistering, doAuth := g.UIManager.LoginScreen.Update()
|
|
||||||
// Update legacy fields
|
|
||||||
g.isLoggedIn = g.UIManager.IsLoggedIn
|
|
||||||
|
|
||||||
if doAuth {
|
|
||||||
conn, playerID, err := network.ConnectToServer(username, password, isRegistering)
|
conn, playerID, err := network.ConnectToServer(username, password, isRegistering)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.UIManager.LoginScreen.SetError(err.Error())
|
g.loginScreen.SetError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
g.PlayerManager.LocalPlayer = &types.Player{
|
g.Player = &types.Player{
|
||||||
Speed: 50.0,
|
Speed: 50.0,
|
||||||
TargetPath: []types.Tile{},
|
TargetPath: []types.Tile{},
|
||||||
ActionQueue: []*pb.Action{},
|
UserData: g,
|
||||||
QuitDone: make(chan struct{}),
|
QuitDone: make(chan struct{}),
|
||||||
ID: playerID,
|
ID: playerID,
|
||||||
}
|
}
|
||||||
g.AssignModelToPlayer(g.PlayerManager.LocalPlayer)
|
g.AssignModelToPlayer(g.Player)
|
||||||
|
|
||||||
// Update the legacy Player field
|
go network.HandleServerCommunication(conn, playerID, g.Player, &g.OtherPlayers, g.QuitChan)
|
||||||
g.Player = g.PlayerManager.LocalPlayer
|
g.isLoggedIn.Store(true)
|
||||||
|
|
||||||
// Set user data to allow chat message handling
|
|
||||||
g.PlayerManager.LocalPlayer.UserData = g
|
|
||||||
|
|
||||||
go network.HandleServerCommunication(conn, playerID, g.PlayerManager.LocalPlayer, g.PlayerManager.OtherPlayers, g.quitChan)
|
|
||||||
g.UIManager.IsLoggedIn = true
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
g.loginScreen.Draw()
|
||||||
// Skip update logic if player is not initialized yet
|
|
||||||
if g.PlayerManager.LocalPlayer == nil {
|
|
||||||
log.Printf("Warning: LocalPlayer is nil during update, skipping")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle ESC for menu
|
// Handle ESC for menu
|
||||||
if rl.IsKeyPressed(rl.KeyEscape) {
|
if rl.IsKeyPressed(rl.KeyEscape) {
|
||||||
g.UIManager.MenuOpen = !g.UIManager.MenuOpen
|
g.MenuOpen.Store(!g.MenuOpen.Load())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't process other inputs if menu is open
|
// Don't process other inputs if menu is open
|
||||||
if g.UIManager.MenuOpen {
|
if g.MenuOpen.Load() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle chat updates
|
if message, sent := g.Chat.Update(); sent {
|
||||||
if message, sent := g.UIManager.Chat.Update(); sent {
|
g.Player.Lock()
|
||||||
g.PlayerManager.LocalPlayer.Lock()
|
g.Player.ActionQueue = append(g.Player.ActionQueue, &pb.Action{
|
||||||
g.PlayerManager.LocalPlayer.ActionQueue = append(g.PlayerManager.LocalPlayer.ActionQueue, &pb.Action{
|
|
||||||
Type: pb.Action_CHAT,
|
Type: pb.Action_CHAT,
|
||||||
ChatMessage: message,
|
ChatMessage: message,
|
||||||
PlayerId: g.PlayerManager.LocalPlayer.ID,
|
PlayerId: g.Player.ID,
|
||||||
})
|
})
|
||||||
g.PlayerManager.LocalPlayer.Unlock()
|
g.Player.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process player input
|
|
||||||
g.HandleInput()
|
g.HandleInput()
|
||||||
|
|
||||||
// Update local player movement
|
if len(g.Player.TargetPath) > 0 {
|
||||||
if g.PlayerManager.LocalPlayer.TargetPath != nil && len(g.PlayerManager.LocalPlayer.TargetPath) > 0 {
|
g.Player.Lock()
|
||||||
g.PlayerManager.LocalPlayer.MoveTowards(g.PlayerManager.LocalPlayer.TargetPath[0], deltaTime, GetMapGrid())
|
if len(g.Player.TargetPath) > 0 {
|
||||||
|
g.Player.MoveTowards(g.Player.TargetPath[0], deltaTime, GetMapGrid())
|
||||||
|
}
|
||||||
|
g.Player.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Periodically log information about other players
|
g.OtherPlayers.Range(func(key, value any) bool {
|
||||||
g.frameCounter++
|
other := value.(*types.Player)
|
||||||
if g.frameCounter%300 == 0 {
|
if len(other.TargetPath) > 0 {
|
||||||
rl.TraceLog(rl.LogInfo, "There are %d other players", len(g.PlayerManager.OtherPlayers))
|
other.MoveTowards(other.TargetPath[0], deltaTime, GetMapGrid())
|
||||||
for id, other := range g.PlayerManager.OtherPlayers {
|
|
||||||
if other != nil {
|
|
||||||
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)
|
|
||||||
} else {
|
|
||||||
rl.TraceLog(rl.LogInfo, "Other player ID: %d is nil", id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
// Process other players
|
UpdateCamera(&g.Camera, g.Player.PosActual, deltaTime)
|
||||||
for _, other := range g.PlayerManager.OtherPlayers {
|
|
||||||
if other == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if other.TargetPath != nil && len(other.TargetPath) > 0 {
|
|
||||||
target := other.TargetPath[0]
|
|
||||||
other.MoveTowards(target, deltaTime, GetMapGrid())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign model if needed
|
|
||||||
if other.Model.Meshes == nil {
|
|
||||||
g.AssignModelToPlayer(other)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update camera position
|
|
||||||
UpdateCamera(&g.Camera, g.PlayerManager.LocalPlayer.PosActual, deltaTime)
|
|
||||||
|
|
||||||
// Update music if available
|
|
||||||
if g.AssetManager.Music.Stream.Buffer != nil {
|
|
||||||
rl.UpdateMusicStream(g.AssetManager.Music)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update legacy fields
|
|
||||||
g.Player = g.PlayerManager.LocalPlayer
|
|
||||||
g.OtherPlayers = g.PlayerManager.OtherPlayers
|
|
||||||
g.Models = g.AssetManager.Models
|
|
||||||
g.Music = g.AssetManager.Music
|
|
||||||
g.MenuOpen = g.UIManager.MenuOpen
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) DrawMap() {
|
func (g *Game) DrawMap() {
|
||||||
@ -250,22 +182,17 @@ func (g *Game) DrawMap() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) DrawPlayer(player *types.Player, model rl.Model) {
|
func (g *Game) DrawPlayer(player *types.Player) {
|
||||||
// No need for lock in rendering, we'll use a "take snapshot" approach
|
player.Lock()
|
||||||
// This avoids potential deadlocks and makes the rendering more consistent
|
defer player.Unlock()
|
||||||
|
|
||||||
// Check for invalid model
|
if player.Model.Meshes == nil {
|
||||||
if model.Meshes == nil || model.Meshes.VertexCount <= 0 {
|
logging.Error.Println("Player model not initialized")
|
||||||
// Don't try to draw invalid models
|
|
||||||
return
|
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
|
||||||
@ -279,25 +206,16 @@ 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
|
||||||
if anim.FrameCount > 0 {
|
player.AnimationFrame = player.AnimationFrame % anim.FrameCount
|
||||||
currentFrame := player.AnimationFrame % anim.FrameCount
|
rl.UpdateModelAnimation(player.Model, anim, player.AnimationFrame)
|
||||||
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
|
||||||
if anim.FrameCount > 0 {
|
player.AnimationFrame = player.AnimationFrame % anim.FrameCount
|
||||||
currentFrame := player.AnimationFrame % anim.FrameCount
|
rl.UpdateModelAnimation(player.Model, anim, player.AnimationFrame)
|
||||||
rl.UpdateModelAnimation(model, anim, currentFrame)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use placeholder color if it's set, otherwise use white
|
rl.DrawModel(player.Model, playerPos, 16, rl.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 {
|
||||||
@ -330,47 +248,36 @@ func (g *Game) DrawPlayer(player *types.Player, model rl.Model) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) Render() {
|
func (g *Game) Render() {
|
||||||
|
if !rl.IsWindowReady() {
|
||||||
|
logging.Error.Println("Window not ready for rendering")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
rl.BeginDrawing()
|
rl.BeginDrawing()
|
||||||
defer func() {
|
defer func() {
|
||||||
// This defer will catch any panics that might occur during rendering
|
if rl.IsWindowReady() {
|
||||||
// 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.EndDrawing()
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
rl.ClearBackground(rl.RayWhite)
|
if !g.isLoggedIn.Load() {
|
||||||
|
g.loginScreen.Draw()
|
||||||
if !g.UIManager.IsLoggedIn {
|
|
||||||
g.UIManager.LoginScreen.Draw()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rl.BeginMode3D(g.Camera)
|
rl.BeginMode3D(g.Camera)
|
||||||
g.DrawMap()
|
g.DrawMap()
|
||||||
|
g.DrawPlayer(g.Player)
|
||||||
|
|
||||||
// Draw player only if valid
|
g.OtherPlayers.Range(func(key, value any) bool {
|
||||||
if g.PlayerManager.LocalPlayer != nil && g.PlayerManager.LocalPlayer.Model.Meshes != nil {
|
other := value.(*types.Player)
|
||||||
g.DrawPlayer(g.PlayerManager.LocalPlayer, g.PlayerManager.LocalPlayer.Model)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw other players with defensive checks
|
|
||||||
for _, other := range g.PlayerManager.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)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
rl.EndMode3D()
|
rl.EndMode3D()
|
||||||
|
|
||||||
// Draw floating messages
|
// Draw floating messages
|
||||||
@ -394,68 +301,61 @@ 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.PlayerManager.LocalPlayer != nil && g.PlayerManager.LocalPlayer.FloatingMessage != nil {
|
if g.Player.FloatingMessage != nil {
|
||||||
drawFloatingMessage(g.PlayerManager.LocalPlayer.FloatingMessage)
|
drawFloatingMessage(g.Player.FloatingMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, other := range g.PlayerManager.OtherPlayers {
|
g.OtherPlayers.Range(func(key, value any) bool {
|
||||||
if other != nil && other.FloatingMessage != nil {
|
other := value.(*types.Player)
|
||||||
drawFloatingMessage(other.FloatingMessage)
|
drawFloatingMessage(other.FloatingMessage)
|
||||||
}
|
return true
|
||||||
}
|
})
|
||||||
|
|
||||||
// Draw menu if open
|
// Draw menu if open
|
||||||
if g.UIManager.MenuOpen {
|
if g.MenuOpen.Load() {
|
||||||
g.DrawMenu()
|
g.DrawMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only draw chat if menu is not open
|
// Only draw chat if menu is not open
|
||||||
if !g.UIManager.MenuOpen && g.UIManager.Chat != nil {
|
if !g.MenuOpen.Load() {
|
||||||
g.UIManager.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)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) Cleanup() {
|
func (g *Game) Cleanup() {
|
||||||
g.cleanupOnce.Do(func() {
|
// Unload models
|
||||||
// Cleanup models
|
if g.Models != nil {
|
||||||
for _, model := range g.AssetManager.Models {
|
assets.UnloadModels(g.Models)
|
||||||
rl.UnloadModel(model.Model)
|
|
||||||
if model.Texture.ID > 0 {
|
|
||||||
rl.UnloadTexture(model.Texture)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unload music
|
// Stop and unload music if enabled
|
||||||
if g.AssetManager.Music.Stream.Buffer != nil {
|
if config.Current.PlayMusic && g.Music.CtxType != 0 {
|
||||||
rl.UnloadMusicStream(g.AssetManager.Music)
|
rl.StopMusicStream(g.Music)
|
||||||
|
rl.UnloadMusicStream(g.Music)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only close the channel if it hasn't been closed yet
|
// Close audio device if it's ready
|
||||||
select {
|
if rl.IsAudioDeviceReady() {
|
||||||
case <-g.quitChan:
|
rl.CloseAudioDevice()
|
||||||
// Channel already closed, do nothing
|
|
||||||
default:
|
|
||||||
close(g.quitChan)
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) HandleInput() {
|
func (g *Game) HandleInput() {
|
||||||
clickedTile, clicked := g.GetTileAtMouse()
|
clickedTile, clicked := g.GetTileAtMouse()
|
||||||
if clicked {
|
if clicked {
|
||||||
path := FindPath(GetTile(g.PlayerManager.LocalPlayer.PosTile.X, g.PlayerManager.LocalPlayer.PosTile.Y), clickedTile)
|
path := FindPath(GetTile(g.Player.PosTile.X, g.Player.PosTile.Y), clickedTile)
|
||||||
if len(path) > 1 {
|
if len(path) > 1 {
|
||||||
g.PlayerManager.LocalPlayer.Lock()
|
g.Player.Lock()
|
||||||
g.PlayerManager.LocalPlayer.TargetPath = path[1:]
|
g.Player.TargetPath = path[1:]
|
||||||
g.PlayerManager.LocalPlayer.ActionQueue = append(g.PlayerManager.LocalPlayer.ActionQueue, &pb.Action{
|
g.Player.ActionQueue = append(g.Player.ActionQueue, &pb.Action{
|
||||||
Type: pb.Action_MOVE,
|
Type: pb.Action_MOVE,
|
||||||
X: int32(clickedTile.X),
|
X: int32(clickedTile.X),
|
||||||
Y: int32(clickedTile.Y),
|
Y: int32(clickedTile.Y),
|
||||||
PlayerId: g.PlayerManager.LocalPlayer.ID,
|
PlayerId: g.Player.ID,
|
||||||
})
|
})
|
||||||
g.PlayerManager.LocalPlayer.Unlock()
|
g.Player.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -498,7 +398,7 @@ func (g *Game) DrawMenu() {
|
|||||||
if rl.IsMouseButtonPressed(rl.MouseLeftButton) {
|
if rl.IsMouseButtonPressed(rl.MouseLeftButton) {
|
||||||
switch item {
|
switch item {
|
||||||
case "Resume":
|
case "Resume":
|
||||||
g.UIManager.MenuOpen = false
|
g.MenuOpen.Store(false)
|
||||||
case "Settings":
|
case "Settings":
|
||||||
// TODO: Implement settings
|
// TODO: Implement settings
|
||||||
case "Exit Game":
|
case "Exit Game":
|
||||||
@ -519,38 +419,51 @@ func (g *Game) DrawMenu() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) Shutdown() {
|
func (g *Game) Shutdown() {
|
||||||
// Use the cleanup method which has channel-closing safety
|
close(g.QuitChan)
|
||||||
g.Cleanup()
|
<-g.Player.QuitDone
|
||||||
|
rl.CloseWindow()
|
||||||
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) HandleServerMessages(messages []*pb.ChatMessage) {
|
func (g *Game) HandleServerMessages(messages []*pb.ChatMessage) {
|
||||||
// Check if Chat is properly initialized
|
g.Chat.HandleServerMessages(messages)
|
||||||
if g.UIManager != nil && g.UIManager.Chat != nil {
|
|
||||||
g.UIManager.Chat.HandleServerMessages(messages)
|
|
||||||
} else {
|
|
||||||
log.Printf("Warning: Cannot handle server messages, Chat is not initialized")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) AssignModelToPlayer(player *types.Player) {
|
func (g *Game) AssignModelToPlayer(player *types.Player) {
|
||||||
if player == nil {
|
modelIndex := int(player.ID) % len(g.Models)
|
||||||
return
|
modelAsset := g.Models[modelIndex]
|
||||||
}
|
|
||||||
|
|
||||||
modelAsset, found := g.AssetManager.GetModelForPlayer(player.ID)
|
|
||||||
if !found {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
player.Model = modelAsset.Model
|
player.Model = modelAsset.Model
|
||||||
player.PlaceholderColor = modelAsset.PlaceholderColor
|
player.Texture = modelAsset.Texture
|
||||||
|
player.AnimationFrame = 0
|
||||||
// Initialize animations if available
|
|
||||||
if len(modelAsset.Animations.Idle) > 0 || len(modelAsset.Animations.Walk) > 0 {
|
|
||||||
player.InitializeAnimations(modelAsset.Animations)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) QuitChan() <-chan struct{} {
|
func (g *Game) Run() {
|
||||||
return g.quitChan
|
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)
|
||||||
}
|
}
|
||||||
|
@ -1,157 +1,92 @@
|
|||||||
package game
|
package game
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"container/heap"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/types"
|
"gitea.boner.be/bdnugget/goonscape/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Node represents a node in the A* pathfinding algorithm
|
|
||||||
type Node struct {
|
type Node struct {
|
||||||
Tile types.Tile
|
Tile types.Tile
|
||||||
Parent *Node
|
Parent *Node
|
||||||
G, H, F float32 // G = cost from start, H = heuristic to goal, F = G + H
|
G, H, F float32
|
||||||
}
|
}
|
||||||
|
|
||||||
// PriorityQueue implements a min-heap for nodes ordered by F value
|
|
||||||
type PriorityQueue []*Node
|
|
||||||
|
|
||||||
// Implement the heap.Interface for PriorityQueue
|
|
||||||
func (pq PriorityQueue) Len() int { return len(pq) }
|
|
||||||
|
|
||||||
func (pq PriorityQueue) Less(i, j int) bool {
|
|
||||||
return pq[i].F < pq[j].F
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pq PriorityQueue) Swap(i, j int) {
|
|
||||||
pq[i], pq[j] = pq[j], pq[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pq *PriorityQueue) Push(x interface{}) {
|
|
||||||
item := x.(*Node)
|
|
||||||
*pq = append(*pq, item)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pq *PriorityQueue) Pop() interface{} {
|
|
||||||
old := *pq
|
|
||||||
n := len(old)
|
|
||||||
item := old[n-1]
|
|
||||||
*pq = old[0 : n-1]
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to check if tile is in priority queue
|
|
||||||
func isInQueue(queue *PriorityQueue, tile types.Tile) (bool, *Node) {
|
|
||||||
for _, node := range *queue {
|
|
||||||
if node.Tile.X == tile.X && node.Tile.Y == tile.Y {
|
|
||||||
return true, node
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindPath implements A* pathfinding algorithm with a priority queue
|
|
||||||
func FindPath(start, end types.Tile) []types.Tile {
|
func FindPath(start, end types.Tile) []types.Tile {
|
||||||
// Initialize open and closed sets
|
openList := []*Node{}
|
||||||
openSet := &PriorityQueue{}
|
closedList := make(map[[2]int]bool)
|
||||||
heap.Init(openSet)
|
|
||||||
|
|
||||||
closedSet := make(map[[2]int]bool)
|
startNode := &Node{Tile: start, G: 0, H: heuristic(start, end)}
|
||||||
|
|
||||||
// Create start node and add to open set
|
|
||||||
startNode := &Node{
|
|
||||||
Tile: start,
|
|
||||||
Parent: nil,
|
|
||||||
G: 0,
|
|
||||||
H: heuristic(start, end),
|
|
||||||
}
|
|
||||||
startNode.F = startNode.G + startNode.H
|
startNode.F = startNode.G + startNode.H
|
||||||
heap.Push(openSet, startNode)
|
openList = append(openList, startNode)
|
||||||
|
|
||||||
// Main search loop
|
for len(openList) > 0 {
|
||||||
for openSet.Len() > 0 {
|
current := openList[0]
|
||||||
// Get node with lowest F score
|
currentIndex := 0
|
||||||
current := heap.Pop(openSet).(*Node)
|
for i, node := range openList {
|
||||||
|
if node.F < current.F {
|
||||||
|
current = node
|
||||||
|
currentIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openList = append(openList[:currentIndex], openList[currentIndex+1:]...)
|
||||||
|
closedList[[2]int{current.Tile.X, current.Tile.Y}] = true
|
||||||
|
|
||||||
// If we reached the goal, reconstruct and return the path
|
|
||||||
if current.Tile.X == end.X && current.Tile.Y == end.Y {
|
if current.Tile.X == end.X && current.Tile.Y == end.Y {
|
||||||
return reconstructPath(current)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add current to closed set
|
|
||||||
closedSet[[2]int{current.Tile.X, current.Tile.Y}] = true
|
|
||||||
|
|
||||||
// Check all neighbors
|
|
||||||
for _, neighbor := range GetNeighbors(current.Tile) {
|
|
||||||
// Skip if in closed set or not walkable
|
|
||||||
if !neighbor.Walkable || closedSet[[2]int{neighbor.X, neighbor.Y}] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate tentative G score
|
|
||||||
tentativeG := current.G + distance(current.Tile, neighbor)
|
|
||||||
|
|
||||||
// Check if in open set
|
|
||||||
inOpen, existingNode := isInQueue(openSet, neighbor)
|
|
||||||
|
|
||||||
// If not in open set or better path found
|
|
||||||
if !inOpen || tentativeG < existingNode.G {
|
|
||||||
// Create or update the node
|
|
||||||
var neighborNode *Node
|
|
||||||
if inOpen {
|
|
||||||
neighborNode = existingNode
|
|
||||||
} else {
|
|
||||||
neighborNode = &Node{
|
|
||||||
Tile: neighbor,
|
|
||||||
Parent: current,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update scores
|
|
||||||
neighborNode.G = tentativeG
|
|
||||||
neighborNode.H = heuristic(neighbor, end)
|
|
||||||
neighborNode.F = neighborNode.G + neighborNode.H
|
|
||||||
neighborNode.Parent = current
|
|
||||||
|
|
||||||
// Add to open set if not already there
|
|
||||||
if !inOpen {
|
|
||||||
heap.Push(openSet, neighborNode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No path found
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// reconstructPath builds the path from goal node to start
|
|
||||||
func reconstructPath(node *Node) []types.Tile {
|
|
||||||
path := []types.Tile{}
|
path := []types.Tile{}
|
||||||
current := node
|
node := current
|
||||||
|
for node != nil {
|
||||||
// Follow parent pointers back to start
|
path = append([]types.Tile{node.Tile}, path...)
|
||||||
for current != nil {
|
node = node.Parent
|
||||||
path = append([]types.Tile{current.Tile}, path...)
|
|
||||||
current = current.Parent
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Path found: %v\n", path)
|
fmt.Printf("Path found: %v\n", path)
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
// heuristic estimates cost from current to goal (Manhattan distance)
|
neighbors := GetNeighbors(current.Tile)
|
||||||
|
for _, neighbor := range neighbors {
|
||||||
|
if !neighbor.Walkable || closedList[[2]int{neighbor.X, neighbor.Y}] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tentativeG := current.G + distance(current.Tile, neighbor)
|
||||||
|
inOpen := false
|
||||||
|
var existingNode *Node
|
||||||
|
for _, node := range openList {
|
||||||
|
if node.Tile.X == neighbor.X && node.Tile.Y == neighbor.Y {
|
||||||
|
existingNode = node
|
||||||
|
inOpen = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !inOpen || tentativeG < existingNode.G {
|
||||||
|
newNode := &Node{
|
||||||
|
Tile: neighbor,
|
||||||
|
Parent: current,
|
||||||
|
G: tentativeG,
|
||||||
|
H: heuristic(neighbor, end),
|
||||||
|
}
|
||||||
|
newNode.F = newNode.G + newNode.H
|
||||||
|
if !inOpen {
|
||||||
|
openList = append(openList, newNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func heuristic(a, b types.Tile) float32 {
|
func heuristic(a, b types.Tile) float32 {
|
||||||
return float32(abs(a.X-b.X) + abs(a.Y-b.Y))
|
return float32(abs(a.X-b.X) + abs(a.Y-b.Y))
|
||||||
}
|
}
|
||||||
|
|
||||||
// distance calculates cost between adjacent tiles
|
|
||||||
func distance(a, b types.Tile) float32 {
|
func distance(a, b types.Tile) float32 {
|
||||||
|
_, _ = a, b
|
||||||
return 1.0 // uniform cost for now
|
return 1.0 // uniform cost for now
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNeighbors returns walkable tiles adjacent to the given tile
|
|
||||||
func GetNeighbors(tile types.Tile) []types.Tile {
|
func GetNeighbors(tile types.Tile) []types.Tile {
|
||||||
directions := [][2]int{
|
directions := [][2]int{
|
||||||
{1, 0}, {-1, 0}, {0, 1}, {0, -1},
|
{1, 0}, {-1, 0}, {0, 1}, {0, -1},
|
||||||
@ -170,7 +105,6 @@ func GetNeighbors(tile types.Tile) []types.Tile {
|
|||||||
return neighbors
|
return neighbors
|
||||||
}
|
}
|
||||||
|
|
||||||
// abs returns the absolute value of x
|
|
||||||
func abs(x int) int {
|
func abs(x int) int {
|
||||||
if x < 0 {
|
if x < 0 {
|
||||||
return -x
|
return -x
|
||||||
|
@ -1,36 +1,9 @@
|
|||||||
package game
|
package game
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"runtime/debug"
|
|
||||||
|
|
||||||
rl "github.com/gen2brain/raylib-go/raylib"
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SafeExecute runs a function and recovers from panics
|
|
||||||
func SafeExecute(action func() error) (err error) {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
stack := debug.Stack()
|
|
||||||
log.Printf("Recovered from panic: %v\nStack trace:\n%s", r, stack)
|
|
||||||
err = fmt.Errorf("recovered from panic: %v", r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return action()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SafeExecuteVoid runs a void function and recovers from panics
|
|
||||||
func SafeExecuteVoid(action func()) {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
stack := debug.Stack()
|
|
||||||
log.Printf("Recovered from panic: %v\nStack trace:\n%s", r, stack)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
action()
|
|
||||||
}
|
|
||||||
|
|
||||||
func RayIntersectsBox(ray rl.Ray, boxMin, boxMax rl.Vector3) bool {
|
func RayIntersectsBox(ray rl.Ray, boxMin, boxMax rl.Vector3) bool {
|
||||||
tmin := (boxMin.X - ray.Position.X) / ray.Direction.X
|
tmin := (boxMin.X - ray.Position.X) / ray.Direction.X
|
||||||
tmax := (boxMax.X - ray.Position.X) / ray.Direction.X
|
tmax := (boxMax.X - ray.Position.X) / ray.Direction.X
|
||||||
|
4
go.mod
4
go.mod
@ -10,8 +10,8 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/ebitengine/purego v0.8.2 // indirect
|
github.com/ebitengine/purego v0.8.2 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
|
golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3 // indirect
|
||||||
golang.org/x/sys v0.29.0 // indirect
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace gitea.boner.be/bdnugget/goonserver => ./goonserver
|
replace gitea.boner.be/bdnugget/goonserver => ./goonserver
|
||||||
|
8
go.sum
8
go.sum
@ -4,9 +4,9 @@ github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b h1:JJfs
|
|||||||
github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b/go.mod h1:BaY76bZk7nw1/kVOSQObPY1v1iwVE1KHAGMfvI6oK1Q=
|
github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b/go.mod h1:BaY76bZk7nw1/kVOSQObPY1v1iwVE1KHAGMfvI6oK1Q=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
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-20250207012021-f9890c6ad9f3 h1:qNgPs5exUA+G0C96DrPwNrvLSj7GT/9D+3WMWUcUg34=
|
||||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
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.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
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 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
||||||
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit 00aa3022292b8f4eec6c01522b6a91cf6769155b
|
Subproject commit f9ec811b10bbab54e843199eb68156e9e7c143cc
|
16
logging/logging.go
Normal file
16
logging/logging.go
Normal file
@ -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)
|
||||||
|
}
|
136
main.go
136
main.go
@ -3,38 +3,28 @@ package main
|
|||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/config"
|
||||||
"gitea.boner.be/bdnugget/goonscape/game"
|
"gitea.boner.be/bdnugget/goonscape/game"
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/logging"
|
||||||
"gitea.boner.be/bdnugget/goonscape/network"
|
"gitea.boner.be/bdnugget/goonscape/network"
|
||||||
rl "github.com/gen2brain/raylib-go/raylib"
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Set up panic recovery at the top level
|
logging.Info.Println("Starting GoonScape client")
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
log.Printf("Recovered from fatal panic in main: %v", r)
|
|
||||||
// Give the user a chance to see the error
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
|
// Raylib log level warn
|
||||||
|
rl.SetTraceLogLevel(rl.LogWarning)
|
||||||
// Parse command line flags
|
// Parse command line flags
|
||||||
verbose := flag.Bool("v", false, "Also show info logs (spammy)")
|
|
||||||
local := flag.Bool("local", false, "Connect to local server")
|
local := flag.Bool("local", false, "Connect to local server")
|
||||||
addr := flag.String("addr", "", "Server address (host or host:port)")
|
addr := flag.String("addr", "", "Server address (host or host:port)")
|
||||||
|
noMusic := flag.Bool("no-music", false, "Disable music playback")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *verbose {
|
// Set config before any game initialization
|
||||||
rl.SetTraceLogLevel(rl.LogTrace)
|
config.Current.PlayMusic = !*noMusic
|
||||||
} else {
|
|
||||||
rl.SetTraceLogLevel(rl.LogWarning)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set server address based on flags
|
// Set server address based on flags
|
||||||
if *local {
|
if *local {
|
||||||
@ -50,104 +40,40 @@ func main() {
|
|||||||
network.SetServerAddr(*addr)
|
network.SetServerAddr(*addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize window with error handling
|
logging.Info.Println("Initializing window")
|
||||||
rl.SetConfigFlags(rl.FlagMsaa4xHint | rl.FlagWindowResizable) // Enable MSAA and make window resizable
|
|
||||||
rl.InitWindow(1024, 768, "GoonScape")
|
rl.InitWindow(1024, 768, "GoonScape")
|
||||||
|
defer func() {
|
||||||
|
logging.Info.Println("Closing window")
|
||||||
|
rl.CloseWindow()
|
||||||
|
}()
|
||||||
|
|
||||||
rl.SetExitKey(0)
|
// Initialize audio device first
|
||||||
|
|
||||||
// Initialize audio with error handling
|
|
||||||
if !rl.IsAudioDeviceReady() {
|
if !rl.IsAudioDeviceReady() {
|
||||||
rl.InitAudioDevice()
|
rl.InitAudioDevice()
|
||||||
if !rl.IsAudioDeviceReady() {
|
if !rl.IsAudioDeviceReady() {
|
||||||
log.Println("Warning: Failed to initialize audio device, continuing without audio")
|
log.Fatal("Failed to initialize audio device")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
defer rl.CloseAudioDevice()
|
||||||
|
|
||||||
// Use a maximum of 3 attempts to load assets
|
game := game.New()
|
||||||
var gameInstance *game.Game
|
logging.Info.Println("Loading game assets")
|
||||||
var loadErr error
|
if err := game.LoadAssets(); err != nil {
|
||||||
maxAttempts := 3
|
log.Fatalf("Failed to load assets: %v", err)
|
||||||
|
|
||||||
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 {
|
logging.Info.Println("Cleaning up game resources")
|
||||||
gameInstance.Cleanup()
|
game.Cleanup()
|
||||||
}
|
|
||||||
rl.CloseWindow()
|
|
||||||
if rl.IsAudioDeviceReady() {
|
|
||||||
rl.CloseAudioDevice()
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
if config.Current.PlayMusic {
|
||||||
|
logging.Info.Println("Starting music playback")
|
||||||
|
rl.PlayMusicStream(game.Music)
|
||||||
|
rl.SetMusicVolume(game.Music, 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
rl.SetTargetFPS(60)
|
rl.SetTargetFPS(60)
|
||||||
|
logging.Info.Println("Starting game loop")
|
||||||
// Play music if available
|
game.Run()
|
||||||
if gameInstance.Music.Stream.Buffer != nil {
|
logging.Info.Println("Game exited cleanly")
|
||||||
rl.PlayMusicStream(gameInstance.Music)
|
|
||||||
rl.SetMusicVolume(gameInstance.Music, 0.5)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle OS signals for clean shutdown
|
|
||||||
sigChan := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
|
||||||
go func() {
|
|
||||||
<-sigChan
|
|
||||||
if gameInstance != nil {
|
|
||||||
gameInstance.Shutdown()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Keep game loop in main thread for Raylib
|
|
||||||
for !rl.WindowShouldClose() {
|
|
||||||
deltaTime := rl.GetFrameTime()
|
|
||||||
|
|
||||||
// Update music if available
|
|
||||||
if gameInstance.Music.Stream.Buffer != nil {
|
|
||||||
rl.UpdateMusicStream(gameInstance.Music)
|
|
||||||
}
|
|
||||||
|
|
||||||
func() {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
log.Printf("Recovered from panic in game update: %v", r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
gameInstance.Update(deltaTime)
|
|
||||||
}()
|
|
||||||
|
|
||||||
func() {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
log.Printf("Recovered from panic in game render: %v", r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
gameInstance.Render()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Check if game requested shutdown
|
|
||||||
select {
|
|
||||||
case <-gameInstance.QuitChan():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package network
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@ -10,69 +11,67 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/logging"
|
||||||
"gitea.boner.be/bdnugget/goonscape/types"
|
"gitea.boner.be/bdnugget/goonscape/types"
|
||||||
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
||||||
rl "github.com/gen2brain/raylib-go/raylib"
|
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
const protoVersion = 1
|
const protoVersion = 1
|
||||||
|
|
||||||
var serverAddr = "boner.be:6969" // Default server address
|
var serverAddr = "boner.be:6969"
|
||||||
var lastSeenMessageTimestamp int64 = 0 // Track the last message timestamp seen by this client
|
|
||||||
|
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) {
|
func SetServerAddr(addr string) {
|
||||||
serverAddr = addr
|
serverAddr = addr
|
||||||
log.Printf("Server address set to: %s", serverAddr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MessageHandler handles reading and writing protobuf messages
|
func (nm *NetworkManager) Connect(addr string) error {
|
||||||
type MessageHandler struct {
|
nm.mu.Lock()
|
||||||
conn net.Conn
|
defer nm.mu.Unlock()
|
||||||
reader *bufio.Reader
|
|
||||||
|
var err error
|
||||||
|
nm.conn, err = net.Dial("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMessageHandler creates a new message handler
|
nm.reader = bufio.NewReader(nm.conn)
|
||||||
func NewMessageHandler(conn net.Conn) *MessageHandler {
|
nm.writer = bufio.NewWriter(nm.conn)
|
||||||
return &MessageHandler{
|
|
||||||
conn: conn,
|
go nm.readLoop()
|
||||||
reader: bufio.NewReader(conn),
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nm *NetworkManager) readLoop() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-nm.ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
// Read and process messages
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadMessage reads a single message from the network
|
func (nm *NetworkManager) Send(message proto.Message) error {
|
||||||
func (mh *MessageHandler) ReadMessage() (*pb.ServerMessage, error) {
|
nm.mu.Lock()
|
||||||
// Read message length
|
defer nm.mu.Unlock()
|
||||||
lengthBuf := make([]byte, 4)
|
|
||||||
if _, err := io.ReadFull(mh.reader, lengthBuf); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read message length: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
messageLength := binary.BigEndian.Uint32(lengthBuf)
|
data, err := proto.Marshal(message)
|
||||||
|
|
||||||
// Sanity check message size
|
|
||||||
if messageLength > 1024*1024 { // 1MB max message size
|
|
||||||
return nil, fmt.Errorf("message size too large: %d bytes", messageLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read message body
|
|
||||||
messageBuf := make([]byte, messageLength)
|
|
||||||
if _, err := io.ReadFull(mh.reader, messageBuf); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read message body: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal the message
|
|
||||||
var message pb.ServerMessage
|
|
||||||
if err := proto.Unmarshal(messageBuf, &message); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to unmarshal message: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &message, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteMessage writes a protobuf message to the network
|
|
||||||
func (mh *MessageHandler) WriteMessage(msg proto.Message) error {
|
|
||||||
data, err := proto.Marshal(msg)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -80,163 +79,26 @@ func (mh *MessageHandler) WriteMessage(msg proto.Message) error {
|
|||||||
// Write length prefix
|
// Write length prefix
|
||||||
lengthBuf := make([]byte, 4)
|
lengthBuf := make([]byte, 4)
|
||||||
binary.BigEndian.PutUint32(lengthBuf, uint32(len(data)))
|
binary.BigEndian.PutUint32(lengthBuf, uint32(len(data)))
|
||||||
if _, err := mh.conn.Write(lengthBuf); err != nil {
|
if _, err := nm.writer.Write(lengthBuf); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write message body
|
// Write message body
|
||||||
_, err = mh.conn.Write(data)
|
if _, err := nm.writer.Write(data); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateGameState processes a server message and updates game state
|
return nm.writer.Flush()
|
||||||
func UpdateGameState(serverMessage *pb.ServerMessage, player *types.Player, otherPlayers map[int32]*types.Player) {
|
|
||||||
// Safety check for nil inputs
|
|
||||||
if serverMessage == nil {
|
|
||||||
log.Printf("Warning: Received nil server message")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if player == nil {
|
|
||||||
log.Printf("Warning: Local player is nil when updating game state")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if otherPlayers == nil {
|
|
||||||
log.Printf("Warning: otherPlayers map is nil when updating game state")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
playerID := player.ID
|
|
||||||
|
|
||||||
player.Lock()
|
|
||||||
player.CurrentTick = serverMessage.CurrentTick
|
|
||||||
|
|
||||||
tickDiff := serverMessage.CurrentTick - player.CurrentTick
|
|
||||||
if tickDiff > types.MaxTickDesync {
|
|
||||||
for _, state := range serverMessage.Players {
|
|
||||||
if state != nil && state.PlayerId == playerID {
|
|
||||||
player.ForceResync(state)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
player.Unlock()
|
|
||||||
|
|
||||||
// Process player states
|
|
||||||
validPlayerIds := make(map[int32]bool)
|
|
||||||
for _, state := range serverMessage.Players {
|
|
||||||
// Skip invalid player states
|
|
||||||
if state == nil {
|
|
||||||
log.Printf("Warning: Received nil player state")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
validPlayerIds[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()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update or create other players
|
|
||||||
if otherPlayer, exists := otherPlayers[state.PlayerId]; exists {
|
|
||||||
if otherPlayer != nil {
|
|
||||||
otherPlayer.UpdatePosition(state, types.ServerTickRate)
|
|
||||||
} else {
|
|
||||||
// Replace nil player with a new one
|
|
||||||
log.Printf("Replacing nil player with ID: %d", state.PlayerId)
|
|
||||||
otherPlayers[state.PlayerId] = types.NewPlayer(state)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Printf("Creating new player with ID: %d", state.PlayerId)
|
|
||||||
otherPlayers[state.PlayerId] = types.NewPlayer(state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove players no longer in the server state
|
|
||||||
for id := range otherPlayers {
|
|
||||||
if id != playerID && !validPlayerIds[id] {
|
|
||||||
log.Printf("Removing player with ID: %d", id)
|
|
||||||
delete(otherPlayers, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle chat messages with safety checks
|
|
||||||
if handler, ok := player.UserData.(types.ChatMessageHandler); ok && handler != nil && len(serverMessage.ChatMessages) > 0 {
|
|
||||||
log.Printf("Received %d chat messages from server", len(serverMessage.ChatMessages))
|
|
||||||
|
|
||||||
// Make sure we have valid chat messages
|
|
||||||
validMessages := make([]*pb.ChatMessage, 0, len(serverMessage.ChatMessages))
|
|
||||||
for _, msg := range serverMessage.ChatMessages {
|
|
||||||
if msg != nil {
|
|
||||||
validMessages = append(validMessages, msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(validMessages) > 0 {
|
|
||||||
// Use a separate goroutine to handle messages to prevent blocking
|
|
||||||
// network handling if there's an issue with chat processing
|
|
||||||
go func(msgs []*pb.ChatMessage) {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
log.Printf("Recovered from panic in chat message handler: %v", r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
handler.HandleServerMessages(msgs)
|
|
||||||
}(validMessages)
|
|
||||||
|
|
||||||
// Update the last seen message timestamp to the most recent message
|
|
||||||
lastMsg := validMessages[len(validMessages)-1]
|
|
||||||
lastSeenMessageTimestamp = lastMsg.Timestamp
|
|
||||||
log.Printf("Updated last seen message timestamp to %d", lastSeenMessageTimestamp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConnectToServer(username, password string, isRegistering bool) (net.Conn, int32, error) {
|
func ConnectToServer(username, password string, isRegistering bool) (net.Conn, int32, error) {
|
||||||
log.Printf("Connecting to server at %s...", serverAddr)
|
logging.Info.Println("Attempting to connect to server at", serverAddr)
|
||||||
|
conn, err := net.Dial("tcp", serverAddr)
|
||||||
var err error
|
if err != nil {
|
||||||
var conn net.Conn
|
logging.Error.Printf("Failed to dial server: %v", err)
|
||||||
|
return nil, 0, err
|
||||||
// Try connecting with a timeout
|
|
||||||
connChan := make(chan net.Conn, 1)
|
|
||||||
errChan := make(chan error, 1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
c, e := net.Dial("tcp", serverAddr)
|
|
||||||
if e != nil {
|
|
||||||
errChan <- e
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
connChan <- c
|
logging.Info.Println("Connected to server. Authenticating...")
|
||||||
}()
|
|
||||||
|
|
||||||
// Wait for connection with timeout
|
|
||||||
select {
|
|
||||||
case conn = <-connChan:
|
|
||||||
// Connection successful, continue
|
|
||||||
case err = <-errChan:
|
|
||||||
return nil, 0, fmt.Errorf("failed to dial server: %v", err)
|
|
||||||
case <-time.After(5 * time.Second):
|
|
||||||
return nil, 0, fmt.Errorf("connection timeout after 5 seconds")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Connected to server. Authenticating...")
|
|
||||||
|
|
||||||
// Create a message handler
|
|
||||||
msgHandler := NewMessageHandler(conn)
|
|
||||||
|
|
||||||
// Send auth message
|
// Send auth message
|
||||||
authAction := &pb.Action{
|
authAction := &pb.Action{
|
||||||
@ -253,23 +115,31 @@ func ConnectToServer(username, password string, isRegistering bool) (net.Conn, i
|
|||||||
ProtocolVersion: protoVersion,
|
ProtocolVersion: protoVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := msgHandler.WriteMessage(authBatch); err != nil {
|
if err := writeMessage(conn, authBatch); err != nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
return nil, 0, fmt.Errorf("failed to send auth: %v", err)
|
return nil, 0, fmt.Errorf("failed to send auth: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set a read deadline for authentication
|
|
||||||
conn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
|
||||||
|
|
||||||
// Read server response
|
// Read server response
|
||||||
response, err := msgHandler.ReadMessage()
|
reader := bufio.NewReader(conn)
|
||||||
if err != nil {
|
lengthBuf := make([]byte, 4)
|
||||||
|
if _, err := io.ReadFull(reader, lengthBuf); err != nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
return nil, 0, fmt.Errorf("failed to read auth response: %v", err)
|
return nil, 0, fmt.Errorf("failed to read auth response: %v", err)
|
||||||
}
|
}
|
||||||
|
messageLength := binary.BigEndian.Uint32(lengthBuf)
|
||||||
|
|
||||||
// Clear read deadline after authentication
|
messageBuf := make([]byte, messageLength)
|
||||||
conn.SetReadDeadline(time.Time{})
|
if _, err := io.ReadFull(reader, messageBuf); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, 0, fmt.Errorf("failed to read auth response body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var response pb.ServerMessage
|
||||||
|
if err := proto.Unmarshal(messageBuf, &response); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, 0, fmt.Errorf("failed to unmarshal auth response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if response.ProtocolVersion > protoVersion {
|
if response.ProtocolVersion > protoVersion {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
@ -279,261 +149,92 @@ func ConnectToServer(username, password string, isRegistering bool) (net.Conn, i
|
|||||||
|
|
||||||
if !response.AuthSuccess {
|
if !response.AuthSuccess {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
return nil, 0, fmt.Errorf(response.ErrorMessage)
|
return nil, 0, fmt.Errorf("authentication failed: %s", response.ErrorMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
playerID := response.GetPlayerId()
|
playerID := response.GetPlayerId()
|
||||||
log.Printf("Successfully authenticated with player ID: %d", playerID)
|
log.Printf("Successfully authenticated with player ID: %d", playerID)
|
||||||
|
|
||||||
// Reset the lastSeenMessageTimestamp when reconnecting
|
|
||||||
lastSeenMessageTimestamp = 0
|
|
||||||
|
|
||||||
return conn, playerID, nil
|
return conn, playerID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Player, otherPlayers map[int32]*types.Player, quitChan <-chan struct{}) {
|
func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Player, otherPlayers *sync.Map, quitChan <-chan struct{}) {
|
||||||
msgHandler := NewMessageHandler(conn)
|
|
||||||
|
|
||||||
// Create channels for coordinating goroutines
|
|
||||||
errChan := make(chan error, 1)
|
|
||||||
done := make(chan struct{})
|
|
||||||
|
|
||||||
// Create a WaitGroup to track both sender and receiver goroutines
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(2) // One for sender, one for receiver
|
|
||||||
|
|
||||||
// Set up a deferred cleanup function
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
logging.Info.Println("Closing connection and cleaning up for player", playerID)
|
||||||
log.Printf("Recovered from panic in HandleServerCommunication: %v", r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the done channel to signal both goroutines to exit
|
|
||||||
close(done)
|
|
||||||
|
|
||||||
// Wait for both goroutines to finish
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
// Close the connection
|
|
||||||
conn.Close()
|
conn.Close()
|
||||||
|
|
||||||
// Close the player's QuitDone channel if it exists
|
|
||||||
if player.QuitDone != nil {
|
|
||||||
select {
|
|
||||||
case <-player.QuitDone: // Check if it's already closed
|
|
||||||
// Already closed, do nothing
|
|
||||||
default:
|
|
||||||
close(player.QuitDone)
|
close(player.QuitDone)
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
actionTicker := time.NewTicker(types.ClientTickRate)
|
reader := bufio.NewReader(conn)
|
||||||
defer actionTicker.Stop()
|
ticker := time.NewTicker(100 * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
// Add a heartbeat ticker to detect connection issues
|
|
||||||
heartbeatTicker := time.NewTicker(5 * time.Second)
|
|
||||||
defer heartbeatTicker.Stop()
|
|
||||||
|
|
||||||
lastMessageTime := time.Now()
|
|
||||||
|
|
||||||
// Start message sending goroutine
|
|
||||||
go func() {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
log.Printf("Recovered from panic in message sender: %v", r)
|
|
||||||
select {
|
|
||||||
case errChan <- fmt.Errorf("message sender panic: %v", r):
|
|
||||||
default:
|
|
||||||
// Channel already closed or full, just log
|
|
||||||
log.Printf("Unable to send error: %v", r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
wg.Done() // Mark this goroutine as done
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-quitChan:
|
|
||||||
// Send disconnect message to server
|
|
||||||
disconnectMsg := &pb.ActionBatch{
|
|
||||||
PlayerId: playerID,
|
|
||||||
Actions: []*pb.Action{{
|
|
||||||
Type: pb.Action_DISCONNECT,
|
|
||||||
PlayerId: playerID,
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to send disconnect message, ignoring errors
|
|
||||||
_ = msgHandler.WriteMessage(disconnectMsg)
|
|
||||||
|
|
||||||
// No need to signal done channel here, the main goroutine handles this
|
|
||||||
return
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
case <-heartbeatTicker.C:
|
|
||||||
// If no message has been sent for a while, send a heartbeat
|
|
||||||
timeSinceLastMessage := time.Since(lastMessageTime)
|
|
||||||
if timeSinceLastMessage > 5*time.Second {
|
|
||||||
// Send an empty batch as a heartbeat
|
|
||||||
emptyBatch := &pb.ActionBatch{
|
|
||||||
PlayerId: playerID,
|
|
||||||
LastSeenMessageTimestamp: lastSeenMessageTimestamp,
|
|
||||||
}
|
|
||||||
if err := msgHandler.WriteMessage(emptyBatch); err != nil {
|
|
||||||
log.Printf("Failed to send heartbeat: %v", err)
|
|
||||||
select {
|
|
||||||
case errChan <- err:
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lastMessageTime = time.Now()
|
|
||||||
}
|
|
||||||
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,
|
|
||||||
LastSeenMessageTimestamp: lastSeenMessageTimestamp,
|
|
||||||
}
|
|
||||||
player.ActionQueue = player.ActionQueue[:0]
|
|
||||||
player.Unlock()
|
|
||||||
|
|
||||||
if err := msgHandler.WriteMessage(batch); err != nil {
|
|
||||||
select {
|
|
||||||
case errChan <- err:
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lastMessageTime = time.Now()
|
|
||||||
} else {
|
|
||||||
player.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Main message receiving loop
|
|
||||||
go func() {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
log.Printf("Recovered from panic in message receiver: %v", r)
|
|
||||||
select {
|
|
||||||
case errChan <- fmt.Errorf("message receiver panic: %v", r):
|
|
||||||
default:
|
|
||||||
// Channel already closed or full, just log
|
|
||||||
log.Printf("Unable to send error: %v", r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
wg.Done() // Mark this goroutine as done
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-quitChan:
|
case <-quitChan:
|
||||||
return
|
return
|
||||||
case <-done:
|
case <-ticker.C:
|
||||||
return
|
// Read message length
|
||||||
default:
|
lengthBuf := make([]byte, 4)
|
||||||
serverMessage, err := msgHandler.ReadMessage()
|
if _, err := io.ReadFull(reader, lengthBuf); err != nil {
|
||||||
if err != nil {
|
log.Printf("Failed to read message length: %v", err)
|
||||||
if err, ok := err.(net.Error); ok && err.Timeout() {
|
continue
|
||||||
log.Printf("Network timeout: %v", err)
|
|
||||||
} else if err != io.EOF {
|
|
||||||
log.Printf("Network read error: %v", err)
|
|
||||||
select {
|
|
||||||
case errChan <- err:
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
messageLength := binary.BigEndian.Uint32(lengthBuf)
|
||||||
|
|
||||||
|
// Read message body
|
||||||
|
messageBuf := make([]byte, messageLength)
|
||||||
|
if _, err := io.ReadFull(reader, messageBuf); err != nil {
|
||||||
|
log.Printf("Failed to read message body: %v", err)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update player states
|
||||||
|
for _, state := range serverMessage.Players {
|
||||||
|
if state == nil {
|
||||||
|
logging.Error.Println("Received nil player state")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
} else {
|
||||||
log.Printf("Connection closed by server")
|
newPlayer := types.NewPlayer(state)
|
||||||
|
otherPlayers.Store(state.PlayerId, newPlayer)
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process the server message
|
// Handle chat messages
|
||||||
UpdateGameState(serverMessage, player, otherPlayers)
|
if handler, ok := player.UserData.(types.ChatMessageHandler); ok {
|
||||||
|
handler.HandleServerMessages(serverMessage.ChatMessages)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
|
||||||
// Wait for error or quit signal
|
|
||||||
select {
|
|
||||||
case <-quitChan:
|
|
||||||
log.Printf("Received quit signal, sending disconnect message")
|
|
||||||
// The cleanup will happen in the deferred function
|
|
||||||
return
|
|
||||||
case err := <-errChan:
|
|
||||||
log.Printf("Network error: %v", err)
|
|
||||||
// The cleanup will happen in the deferred function
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to write length-prefixed messages
|
// Helper function to write length-prefixed messages
|
||||||
func writeMessage(conn net.Conn, msg proto.Message) error {
|
func writeMessage(conn net.Conn, msg proto.Message) error {
|
||||||
msgHandler := NewMessageHandler(conn)
|
data, err := proto.Marshal(msg)
|
||||||
return msgHandler.WriteMessage(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Connection struct {
|
|
||||||
conn net.Conn
|
|
||||||
playerID int32
|
|
||||||
quitChan chan struct{}
|
|
||||||
quitDone chan struct{}
|
|
||||||
closeOnce sync.Once
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewConnection(username, password string, isRegistering bool) (*Connection, error) {
|
|
||||||
conn, playerID, err := ConnectToServer(username, password, isRegistering)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
|
||||||
return &Connection{
|
|
||||||
conn: conn,
|
|
||||||
playerID: playerID,
|
|
||||||
quitChan: make(chan struct{}),
|
|
||||||
quitDone: make(chan struct{}),
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Connection) Close() {
|
// Write length prefix
|
||||||
c.closeOnce.Do(func() {
|
lengthBuf := make([]byte, 4)
|
||||||
select {
|
binary.BigEndian.PutUint32(lengthBuf, uint32(len(data)))
|
||||||
case <-c.quitChan: // Check if it's already closed
|
if _, err := conn.Write(lengthBuf); err != nil {
|
||||||
// Already closed, do nothing
|
return err
|
||||||
default:
|
|
||||||
close(c.quitChan)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait with timeout for network cleanup
|
// Write message body
|
||||||
select {
|
_, err = conn.Write(data)
|
||||||
case <-c.quitDone:
|
return err
|
||||||
// Clean shutdown completed
|
|
||||||
case <-time.After(500 * time.Millisecond):
|
|
||||||
log.Println("Network cleanup timed out")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the connection is closed
|
|
||||||
c.conn.Close()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Connection) PlayerID() int32 {
|
|
||||||
return c.playerID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Connection) Start(player *types.Player, otherPlayers map[int32]*types.Player) {
|
|
||||||
go HandleServerCommunication(c.conn, c.playerID, player, otherPlayers, c.quitChan)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Connection) QuitChan() <-chan struct{} {
|
|
||||||
return c.quitChan
|
|
||||||
}
|
}
|
||||||
|
BIN
resources/models/shreke/Animation_Excited_Walk_M_withSkin.glb
Normal file
BIN
resources/models/shreke/Animation_Excited_Walk_M_withSkin.glb
Normal file
Binary file not shown.
BIN
resources/models/shreke/Animation_Running_withSkin.glb
Normal file
BIN
resources/models/shreke/Animation_Running_withSkin.glb
Normal file
Binary file not shown.
BIN
resources/models/shreke/Animation_Slow_Orc_Walk_withSkin.glb
Normal file
BIN
resources/models/shreke/Animation_Slow_Orc_Walk_withSkin.glb
Normal file
Binary file not shown.
BIN
resources/models/shreke/Animation_Unsteady_Walk_withSkin.glb
Normal file
BIN
resources/models/shreke/Animation_Unsteady_Walk_withSkin.glb
Normal file
Binary file not shown.
BIN
resources/models/shreke/Animation_Walking_withSkin.glb
Normal file
BIN
resources/models/shreke/Animation_Walking_withSkin.glb
Normal file
Binary file not shown.
126
segfault.txt
Normal file
126
segfault.txt
Normal file
@ -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
|
114
types/player.go
114
types/player.go
@ -8,91 +8,31 @@ import (
|
|||||||
rl "github.com/gen2brain/raylib-go/raylib"
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AnimationController manages animation state and updates
|
|
||||||
type AnimationController struct {
|
|
||||||
animations AnimationSet
|
|
||||||
currentAnimation string // "idle" or "walk"
|
|
||||||
frame int32
|
|
||||||
lastUpdate time.Time
|
|
||||||
frameCount int32
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAnimationController creates a new animation controller
|
|
||||||
func NewAnimationController(animations AnimationSet) *AnimationController {
|
|
||||||
return &AnimationController{
|
|
||||||
animations: animations,
|
|
||||||
currentAnimation: "idle",
|
|
||||||
frame: 0,
|
|
||||||
lastUpdate: time.Now(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update updates the animation state based on movement
|
|
||||||
func (ac *AnimationController) Update(deltaTime float32, isMoving bool) {
|
|
||||||
// Set the current animation based on movement
|
|
||||||
newAnimation := "idle"
|
|
||||||
if isMoving {
|
|
||||||
newAnimation = "walk"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset frame counter when animation changes
|
|
||||||
if ac.currentAnimation != newAnimation {
|
|
||||||
ac.frame = 0
|
|
||||||
ac.currentAnimation = newAnimation
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the frame
|
|
||||||
ac.frame += int32(deltaTime * 60)
|
|
||||||
|
|
||||||
// Determine which animation set to use
|
|
||||||
var frames []rl.ModelAnimation
|
|
||||||
if ac.currentAnimation == "walk" && len(ac.animations.Walk) > 0 {
|
|
||||||
frames = ac.animations.Walk
|
|
||||||
} else if len(ac.animations.Idle) > 0 {
|
|
||||||
frames = ac.animations.Idle
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have frames, ensure we loop properly
|
|
||||||
if len(frames) > 0 && frames[0].FrameCount > 0 {
|
|
||||||
ac.frame = ac.frame % frames[0].FrameCount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAnimFrame returns the current animation frame
|
|
||||||
func (ac *AnimationController) GetAnimFrame() int32 {
|
|
||||||
return ac.frame
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCurrentAnimation returns the current animation type
|
|
||||||
func (ac *AnimationController) GetCurrentAnimation() string {
|
|
||||||
return ac.currentAnimation
|
|
||||||
}
|
|
||||||
|
|
||||||
type Player struct {
|
type Player struct {
|
||||||
sync.RWMutex // Keep this for network operations
|
sync.RWMutex
|
||||||
Model rl.Model
|
|
||||||
Texture rl.Texture2D
|
|
||||||
PosActual rl.Vector3
|
PosActual rl.Vector3
|
||||||
PosTile Tile
|
PosTile Tile
|
||||||
TargetPath []Tile
|
TargetPath []Tile
|
||||||
Speed float32
|
|
||||||
ActionQueue []*pb.Action
|
ActionQueue []*pb.Action
|
||||||
|
Speed float32
|
||||||
ID int32
|
ID int32
|
||||||
QuitDone chan struct{}
|
|
||||||
CurrentTick int64
|
|
||||||
UserData interface{}
|
|
||||||
FloatingMessage *FloatingMessage
|
|
||||||
IsMoving bool
|
IsMoving bool
|
||||||
AnimationFrame int32
|
AnimationFrame int32
|
||||||
LastAnimUpdate time.Time
|
LastAnimUpdate time.Time
|
||||||
LastUpdateTime time.Time
|
LastUpdateTime time.Time
|
||||||
InterpolationProgress float32
|
InterpolationProgress float32
|
||||||
PlaceholderColor rl.Color
|
FloatingMessage *FloatingMessage
|
||||||
AnimController *AnimationController
|
QuitDone chan struct{}
|
||||||
|
UserData interface{}
|
||||||
|
Model rl.Model
|
||||||
|
Texture rl.Texture2D
|
||||||
|
Username string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Player) MoveTowards(target Tile, deltaTime float32, mapGrid [][]Tile) {
|
func (p *Player) MoveTowards(target Tile, deltaTime float32, mapGrid [][]Tile) {
|
||||||
// No need for lock here as this is called from a single thread (game loop)
|
p.Lock()
|
||||||
|
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,
|
||||||
@ -103,29 +43,29 @@ func (p *Player) MoveTowards(target Tile, deltaTime float32, mapGrid [][]Tile) {
|
|||||||
distance := rl.Vector3Length(direction)
|
distance := rl.Vector3Length(direction)
|
||||||
|
|
||||||
if distance > 1.0 {
|
if distance > 1.0 {
|
||||||
|
wasMoving := p.IsMoving
|
||||||
p.IsMoving = true
|
p.IsMoving = true
|
||||||
} else {
|
|
||||||
p.IsMoving = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update animation if controller exists
|
if !wasMoving {
|
||||||
if p.AnimController != nil {
|
|
||||||
p.AnimController.Update(deltaTime, p.IsMoving)
|
|
||||||
p.AnimationFrame = p.AnimController.GetAnimFrame()
|
|
||||||
} else {
|
|
||||||
// Legacy animation update for backward compatibility
|
|
||||||
if p.IsMoving {
|
|
||||||
if !p.IsMoving {
|
|
||||||
p.AnimationFrame = 0
|
p.AnimationFrame = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
oldFrame := p.AnimationFrame
|
||||||
p.AnimationFrame += int32(deltaTime * 60)
|
p.AnimationFrame += int32(deltaTime * 60)
|
||||||
|
rl.TraceLog(rl.LogInfo, "Walk frame update: %d -> %d (delta: %f)",
|
||||||
|
oldFrame, p.AnimationFrame, deltaTime)
|
||||||
} else {
|
} else {
|
||||||
wasMoving := p.IsMoving
|
wasMoving := p.IsMoving
|
||||||
|
p.IsMoving = false
|
||||||
|
|
||||||
if wasMoving {
|
if wasMoving {
|
||||||
p.AnimationFrame = 0
|
p.AnimationFrame = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
oldFrame := p.AnimationFrame
|
||||||
p.AnimationFrame += int32(deltaTime * 60)
|
p.AnimationFrame += int32(deltaTime * 60)
|
||||||
}
|
rl.TraceLog(rl.LogInfo, "Idle frame update: %d -> %d (delta: %f)",
|
||||||
|
oldFrame, p.AnimationFrame, deltaTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
if distance > 0 {
|
if distance > 0 {
|
||||||
@ -156,16 +96,9 @@ func NewPlayer(state *pb.PlayerState) *Player {
|
|||||||
IsMoving: false,
|
IsMoving: false,
|
||||||
AnimationFrame: 0,
|
AnimationFrame: 0,
|
||||||
LastAnimUpdate: time.Now(),
|
LastAnimUpdate: time.Now(),
|
||||||
LastUpdateTime: time.Now(),
|
|
||||||
InterpolationProgress: 1.0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitializeAnimations sets up the animation controller for the player
|
|
||||||
func (p *Player) InitializeAnimations(animations AnimationSet) {
|
|
||||||
p.AnimController = NewAnimationController(animations)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Player) UpdatePosition(state *pb.PlayerState, tickRate time.Duration) {
|
func (p *Player) UpdatePosition(state *pb.PlayerState, tickRate time.Duration) {
|
||||||
p.Lock()
|
p.Lock()
|
||||||
defer p.Unlock()
|
defer p.Unlock()
|
||||||
@ -180,7 +113,6 @@ 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()
|
||||||
|
|
||||||
|
@ -28,7 +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
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChatMessage struct {
|
type ChatMessage struct {
|
||||||
@ -48,6 +48,13 @@ type ChatMessageHandler interface {
|
|||||||
HandleServerMessages([]*pb.ChatMessage)
|
HandleServerMessages([]*pb.ChatMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PlayerState struct {
|
||||||
|
PlayerId int32
|
||||||
|
X int32
|
||||||
|
Y int32
|
||||||
|
Username string
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MapWidth = 50
|
MapWidth = 50
|
||||||
MapHeight = 50
|
MapHeight = 50
|
||||||
@ -59,12 +66,3 @@ const (
|
|||||||
ClientTickRate = 50 * time.Millisecond
|
ClientTickRate = 50 * time.Millisecond
|
||||||
MaxTickDesync = 5
|
MaxTickDesync = 5
|
||||||
)
|
)
|
||||||
|
|
||||||
// UI constants
|
|
||||||
const (
|
|
||||||
ChatMargin = 10
|
|
||||||
ChatHeight = 200
|
|
||||||
MessageHeight = 20
|
|
||||||
InputHeight = 30
|
|
||||||
MaxChatMessages = 50
|
|
||||||
)
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user