package main import ( "github.com/veandco/go-sdl2/sdl" "image/color" "math/rand" "net" "sync" ) var playerUpdateMutex sync.Mutex var lastPlayerID = uint32(0) var playersMutex sync.RWMutex type Player struct { local bool connection *net.TCPConn playerColors PlayerColors playerColorID uint32 keyMap KeyMap joyMap JoyMap joyStick *sdl.Joystick camera *sdl.Rect energy uint32 ammunition uint32 shields uint32 digCooldown uint32 shootCooldown uint32 repairCooldown uint32 rechargeCooldown uint32 movementCooldown uint32 reloadCooldown uint32 reloadWait uint32 gameObject *GameObject playerID uint32 knownGameMap *GameMap knownBullets map[uint32]*Bullet knownBulletParticles map[uint32]*BulletParticle knownBases map[uint32]*Base knownPlayers map[uint32]*Player previousEnergy uint32 previousAmmunition uint32 previousShields uint32 window *sdl.Window logicalSurface *sdl.Surface playSurface *sdl.Surface HUDSurface *sdl.Surface playSurfaceRect *sdl.Rect HUDSurfaceRect *sdl.Rect playSurfaceTargetRect *sdl.Rect HUDSurfaceTargetRect *sdl.Rect totalHandleEventsTime uint64 totalRenderTime uint64 totalFrameTime uint64 totalScalingTime uint64 frameCount uint64 initialized bool } func (player *Player) track(camera *sdl.Rect) { camera.X = player.gameObject.baseRect.X - 37 camera.Y = player.gameObject.baseRect.Y - 38 } func (player *Player) render(camera *sdl.Rect, surface *sdl.Surface, players map[uint32]*Player) { if player.shields > 0 && player.shields <= serverConfig.MaxShields && player.gameObject.baseRect.X >= 0 && player.gameObject.baseRect.Y >= 0 { player.gameObject.render(camera, surface) if !player.gameObject.inView && !config.Server { delete(players, player.playerID) } } } func (player *Player) tick(bullets map[uint32]*Bullet) { if player.digCooldown > 0 { player.digCooldown-- } if player.shootCooldown > 0 { player.shootCooldown-- } if player.repairCooldown > 0 { player.repairCooldown-- } if player.rechargeCooldown > 0 { player.rechargeCooldown-- } if player.movementCooldown > 0 { player.movementCooldown-- } if player.reloadCooldown > 0 { player.reloadCooldown-- } else if player.ammunition < serverConfig.MaxAmmunition && player.shootCooldown == 0 && player.reloadWait == 0 && player.energy > serverConfig.ReloadCost { player.ammunition++ player.reloadCooldown = serverConfig.ReloadCooldown player.energy -= serverConfig.ReloadCost } if player.shields <= 0 { player.shields = serverConfig.MaxShields + 1 player.explode(bullets) } if player.reloadWait > 0 { player.reloadWait-- } } func (player *Player) digBlock(posX, posY uint32, gameMap *GameMap, isShooting bool) uint8 { collisionAtDigSite := gameMap.checkCollision(int32(posX), int32(posY)) if collisionAtDigSite == 0 { return 0 } else if collisionAtDigSite == 1 && (player.digCooldown == 0 || isShooting) { if player.energy > serverConfig.DiggingCost { if isShooting && player.energy > serverConfig.ShootDiggingCostBonus { player.energy -= serverConfig.ShootDiggingCostBonus } player.digCooldown = serverConfig.DiggingCooldown } else { player.digCooldown = serverConfig.DiggingCooldownNoEnergy } if isShooting && player.ammunition < serverConfig.MaxAmmunition && rand.Intn(2) == 0 { player.ammunition++ } if config.Client { player.sendDigToServer(posX, posY, isShooting) } gameMap.tiles[posX][posY] = 0 return 1 } return 2 } func (player *Player) tryMove(gameMap *GameMap, isShooting bool, players map[uint32]*Player) (moved bool) { if config.Client { defer player.sendPositionToServer() } player.gameObject.adjustBaseRect() if player.movementCooldown > 0 { return false } ranOutOfEnergy := (isShooting && player.energy <= serverConfig.DiggingCost+serverConfig.ShootDiggingCostBonus) || player.energy <= serverConfig.DiggingCost if ranOutOfEnergy { isShooting = false } // Define movement deltas based on orientation movementDeltas := []struct { dx, dy int32 }{ {0, -1}, {1, 0}, {0, 1}, {-1, 0}, // Up, Right, Down, Left {1, -1}, {-1, -1}, {1, 1}, {-1, 1}, // Up-Right, Up-Left, Down-Right, Down-Left } dx, dy := movementDeltas[player.gameObject.orientation].dx, movementDeltas[player.gameObject.orientation].dy // Set collision rectangle player.gameObject.collisionRect = &sdl.Rect{ X: player.gameObject.baseRect.X + oNeg(dx), Y: player.gameObject.baseRect.Y + oNeg(dy), W: player.gameObject.baseRect.W + abs(dx), H: player.gameObject.baseRect.H + abs(dy), } // Check for player collision playersMutex.RLock() for _, opponent := range players { if opponent != player && opponent.gameObject.baseRect.HasIntersection(player.gameObject.collisionRect) { playersMutex.RUnlock() return false } } playersMutex.RUnlock() // Initialize flags for movement and digging status stopped := false // Check collision and dig at the target position for x := player.gameObject.baseRect.X + oNeg(dx); x < player.gameObject.baseRect.X+dx+player.gameObject.baseRect.W; x++ { for y := player.gameObject.baseRect.Y + oNeg(dy); y < player.gameObject.baseRect.Y+dy+player.gameObject.baseRect.H; y++ { digResult := player.digBlock(uint32(x), uint32(y), gameMap, isShooting) if digResult == 1 { // Successfully dug a block if !isShooting { stopped = true // Stop movement if not shooting } } else if digResult == 2 { // Block could not be dug (e.g., solid block) stopped = true } if stopped || !(digResult == 0 || (digResult == 1 && isShooting)) { stopped = true break } } if stopped { break } } // Move player if there was no obstruction if !stopped { var dxT, dyT int32 if dx > 1 { dxT = 1 } else if dx < -1 { dxT = -1 } else { dxT = dx } if dy > 1 { dyT = 1 } else if dy < -1 { dyT = -1 } else { dyT = dy } player.gameObject.baseRect.X += dxT player.gameObject.baseRect.Y += dyT moved = true } // Send the updated position to the server // Apply movement cooldown if moved { if ranOutOfEnergy { player.movementCooldown = serverConfig.MovementCooldownNoEnergy } else { player.movementCooldown = serverConfig.MovementCooldown } } return } func abs(dx int32) int32 { if dx < 0 { return -dx } else { return dx } } func oNeg(dx int32) int32 { if dx < 0 { return dx } else { return 0 } } func (player *Player) getRGBAColor(colorIndex uint8, format *sdl.PixelFormat) uint32 { var selectedColor color.Color switch colorIndex { case 0: selectedColor = player.playerColors.tracks case 1: selectedColor = player.playerColors.body case 2: selectedColor = player.playerColors.cannon } var r, g, b, a uint8 if selectedColor != nil { rt, gt, bt, at := selectedColor.RGBA() r, g, b, a = uint8(rt), uint8(gt), uint8(bt), uint8(at) } return sdl.MapRGBA(format, r, g, b, a) } type Point struct { X int32 Y int32 } func (player *Player) shoot(super bool, bullets map[uint32]*Bullet) { if (super && (player.energy <= serverConfig.SuperShotCost || player.ammunition < serverConfig.MaxAmmunition)) || (!super && (player.energy <= serverConfig.NormalShotCost || player.ammunition < 1)) { return } if player.shootCooldown == 0 { var shootX, shootY int32 offsets := []Point{ {X: 2, Y: 0}, // 0: Up {X: 6, Y: 2}, // 1: Right {X: 2, Y: 6}, // 2: Down {X: 0, Y: 2}, // 3: Left {X: 6, Y: 0}, // 4: Up-Right {X: 0, Y: 0}, // 5: Up-Left {X: 5, Y: 5}, // 6: Down-Right {X: 0, Y: 6}, // 7: Down-Left } // Access the offset based on the player's orientation shootX = player.gameObject.baseRect.X + offsets[player.gameObject.orientation].X shootY = player.gameObject.baseRect.Y + offsets[player.gameObject.orientation].Y player.shootCooldown = serverConfig.ShootCooldown // Set cooldown and decrease energy if super { player.energy -= serverConfig.SuperShotCost player.ammunition = 0 } else { player.energy -= serverConfig.NormalShotCost player.ammunition-- } player.reloadWait = serverConfig.ReloadWait if config.Client { player.sendShootToServer(super) } else { // Set bullet color bulletColor := player.playerColors.body bulletMutex.Lock() // Create and add the bullet bullets[bulletLastID] = &Bullet{ posX: shootX, posY: shootY, direction: player.gameObject.orientation, color: bulletColor, super: super, id: bulletLastID, ownerID: player.playerID, } bulletMutex.Unlock() bulletLastID++ } } } func (player *Player) explode(bullets map[uint32]*Bullet) { // Set bullet color bulletColor := player.playerColors.body for x := player.gameObject.baseRect.X - int32(serverConfig.BlastRadius); x < int32(serverConfig.BlastRadius)*2+1; x++ { for y := player.gameObject.baseRect.Y - int32(serverConfig.BlastRadius); y < int32(serverConfig.BlastRadius)*2+1; y++ { // Create and add the bullet bulletMutex.Lock() bullets[bulletLastID] = &Bullet{ posX: x, posY: y, direction: uint8(rand.Intn(8)), color: bulletColor, super: true, id: bulletLastID, ownerID: player.playerID, } bulletMutex.Unlock() bulletLastID++ } } } func (player *Player) updateRemotePlayerMap(wgX *sync.WaitGroup) { defer wgX.Done() fromX := player.gameObject.baseRect.X - (config.CameraW / 2) - 1 fromY := player.gameObject.baseRect.Y - (config.CameraH / 2) - 1 toX := player.gameObject.baseRect.X + (config.CameraW / 2) + 2 toY := player.gameObject.baseRect.Y + (config.CameraH / 2) + 1 if uint32(toX) > serverConfig.MapWidth { toX = int32(serverConfig.MapWidth) } if uint32(toY) > serverConfig.MapHeight { toY = int32(serverConfig.MapHeight) } if fromX < 0 { fromX = 0 } if fromY < 0 { fromY = 0 } // Create a WaitGroup to wait for all goroutines to finish var wg sync.WaitGroup var lck sync.Mutex // Process columns instead of individual pixels for x := fromX; x < toX; x++ { // Increment the WaitGroup counter wg.Add(1) // Launch a goroutine for each column go func(x int32) { defer wg.Done() // Decrement the counter when the goroutine completes for y := fromY; y < toY; y++ { if player.knownGameMap.tiles[x][y] != gameMap.tiles[x][y] { lck.Lock() kind := gameMap.tiles[x][y] err := sendTileUpdate( uint32(x), uint32(y), kind, player.connection) if err == nil { player.knownGameMap.tiles[x][y] = gameMap.tiles[x][y] } lck.Unlock() } } }(x) } // Wait for all goroutines to finish wg.Wait() } func (player *Player) updateRemotePlayerPlayers(players map[uint32]*Player, wgX *sync.WaitGroup) { defer wgX.Done() player.knownPlayers = make(map[uint32]*Player) playersMutex.RLock() for _, playerLoop := range players { player.camera = &sdl.Rect{ X: player.gameObject.baseRect.X - (config.CameraW / 2), Y: player.gameObject.baseRect.Y - (config.CameraH / 2), W: config.CameraW, H: config.CameraH, } for _, playerLoopRect := range playerLoop.gameObject.getCurrentRects() { if player.camera.HasIntersection(playerLoop.gameObject.adjustRectWorld(playerLoopRect.rect)) && playerLoop.shields <= serverConfig.MaxShields { if player.knownPlayers[playerLoop.playerID] == nil { _ = sendOtherPlayer( playerLoop.playerID, uint32(player.gameObject.baseRect.X), uint32(player.gameObject.baseRect.Y), player.gameObject.orientation, player.connection) player.knownPlayers[playerLoop.playerID] = playerLoop } break } } } playersMutex.RUnlock() } func (player *Player) updateRemotePlayerBullets(bullets map[uint32]*Bullet, wgX *sync.WaitGroup) { defer wgX.Done() player.knownBullets = make(map[uint32]*Bullet) bulletMutex.RLock() for _, bullet := range bullets { bulletRect := &sdl.Rect{ X: bullet.posX, Y: bullet.posY, W: 1, H: 1, } player.camera = &sdl.Rect{ X: player.gameObject.baseRect.X - (config.CameraW / 2), Y: player.gameObject.baseRect.Y - (config.CameraH / 2), W: config.CameraW, H: config.CameraH, } if player.camera.HasIntersection(bulletRect) { if player.knownBullets[bullet.id] == nil { r, g, b, a := bullet.color.RGBA() _ = sendBullet( uint32(bullet.posX), uint32(bullet.posY), bullet.direction, uint8(r), uint8(g), uint8(b), uint8(a), bullet.id, bullet.super, bullet.ownerID, player.connection) player.knownBullets[bullet.id] = bullet } } } bulletMutex.RUnlock() } func (player *Player) updateRemotePlayerBulletParticles(bulletParticles map[uint32]*BulletParticle, wgX *sync.WaitGroup) { defer wgX.Done() player.knownBulletParticles = make(map[uint32]*BulletParticle) bulletParticleMutex.RLock() for _, bulletParticle := range bulletParticles { bulletRect := &sdl.Rect{ X: bulletParticle.posX, Y: bulletParticle.posY, W: 1, H: 1, } player.camera = &sdl.Rect{ X: player.gameObject.baseRect.X - (config.CameraW / 2), Y: player.gameObject.baseRect.Y - (config.CameraH / 2), W: config.CameraW, H: config.CameraH, } if player.camera.HasIntersection(bulletRect) { if player.knownBullets[bulletParticle.id] == nil { r, g, b, a := bulletParticle.color.RGBA() _ = sendBulletParticle( uint32(bulletParticle.posX), uint32(bulletParticle.posY), bulletParticle.expirationTimer, uint8(r), uint8(g), uint8(b), uint8(a), bulletParticle.id, player.connection) player.knownBulletParticles[bulletParticle.id] = bulletParticle } } } bulletParticleMutex.RUnlock() } func (player *Player) updateRemotePlayerBases(bases map[uint32]*Base, players map[uint32]*Player, wgX *sync.WaitGroup) { defer wgX.Done() player.knownBases = make(map[uint32]*Base) if player.connection == nil || !config.Server { return } baseMutex.RLock() for _, base := range bases { player.camera = &sdl.Rect{ X: player.gameObject.baseRect.X - (config.CameraW / 2), Y: player.gameObject.baseRect.Y - (config.CameraH / 2), W: config.CameraW, H: config.CameraH, } for _, baseRect := range base.gameObject.getCurrentRects() { if player.camera.HasIntersection(base.gameObject.adjustRectWorld(baseRect.rect)) { if player.knownPlayers[base.ownerID] == nil { _ = sendBaseLocation( uint32(base.gameObject.baseRect.X), uint32(base.gameObject.baseRect.Y), base.ownerID, players[base.ownerID].playerColorID, player.connection) player.knownBases[base.ownerID] = base } break } } } baseMutex.RUnlock() } func (player *Player) sendPlayerUpdate(wgX *sync.WaitGroup) bool { defer wgX.Done() if player.connection != nil && config.Server { if player.previousEnergy != player.energy || player.previousShields != player.shields || player.previousAmmunition != player.ammunition { err := sendPlayerUpdate(player.energy, player.ammunition, player.shields, player.connection) player.previousEnergy = player.energy player.previousShields = player.shields player.previousAmmunition = player.ammunition return err == nil } else { return true } } return false } func (player *Player) sendServerInfoToPlayer() bool { if player.connection != nil && config.Server { err := sendServerInfo( player.playerID, player.playerColorID, serverConfig.MaxEnergy, serverConfig.MaxAmmunition, serverConfig.MaxShields, serverConfig.MapWidth, serverConfig.MapHeight, serverConfig.NormalShotCost, serverConfig.SuperShotCost, serverConfig.ReloadCost, serverConfig.MovementCost, serverConfig.DiggingCost, serverConfig.ShootDiggingCostBonus, serverConfig.ShootCooldown, serverConfig.RechargeCooldownOwn, serverConfig.RechargeCooldownOpponent, serverConfig.RepairCooldown, serverConfig.DiggingCooldown, serverConfig.MovementCooldown, serverConfig.MovementCooldownNoEnergy, serverConfig.DiggingCooldownNoEnergy, serverConfig.ReloadCooldown, serverConfig.BlastRadius, serverConfig.ReloadWait, player.connection) return err == nil } return false } func (player *Player) sendVersionBackToPlayer() bool { if player.connection != nil && config.Server { versionArray := getCurrentVersion() err := sendPlayerStartResponse( versionArray[0], versionArray[1], versionArray[2], player.connection) return err == nil } return false } func (player *Player) sendLocationToPlayer() bool { if player.connection != nil && config.Server { err := sendPositionUpdate( uint32(player.gameObject.baseRect.X), uint32(player.gameObject.baseRect.Y), player.gameObject.orientation, player.connection) return err == nil } return false } func (player *Player) sendPositionToServer() bool { if player.connection != nil && config.Client { err := sendPlayerLocation( uint32(player.gameObject.baseRect.X), uint32(player.gameObject.baseRect.Y), player.gameObject.orientation, player.connection) return err == nil } return false } func sendVersionToServer(connection *net.TCPConn) { if connection != nil && config.Client { versionArray := getCurrentVersion() _ = sendPlayerStartRequest( versionArray[0], versionArray[1], versionArray[2], connection) return } } func (player *Player) sendDigToServer(posX, posY uint32, isShooting bool) { if player.connection != nil && config.Client { _ = sendDigBlock( posX, posY, isShooting, player.connection) } } func (player *Player) sendShootToServer(isSuper bool) { if player.connection != nil && config.Client { _ = sendShoot( isSuper, player.connection) } }