package main import ( "bufio" "bytes" "encoding/binary" "github.com/veandco/go-sdl2/sdl" tunnelerProto "goingtunneling/proto" "google.golang.org/protobuf/proto" "image/color" "io" "log" "math/rand" "net" "os" "sync" ) var lastPlayerID = uint32(0) type Player struct { local bool connection *net.Conn playerColors PlayerColors 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 gameObject *GameObject playerID uint32 knownGameMap *GameMap gameMapUpdates []TileUpdate 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 } type TileUpdate struct { PosX uint32 PosY uint32 Kind uint8 } 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) { // Common part: Rendering player player.gameObject.baseRect = sdl.Rect{X: player.gameObject.baseRect.X, Y: player.gameObject.baseRect.Y, W: 7, H: 7} 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) digBlock(posX, posY int32, gameMap *GameMap, isShooting bool) uint8 { collisionAtDigSite := gameMap.checkCollision(posX, posY) if collisionAtDigSite == 0 { return 0 } else if collisionAtDigSite == 1 && (player.digCooldown == 0 || isShooting) { player.digCooldown = serverConfig.DiggingCooldown if config.Client { player.sendDigToServer(uint32(posX), uint32(posY), isShooting) return 2 } gameMap.tiles[posX][posY] = 0 return 1 } return 2 } 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.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) } } func (player *Player) tryMove(gameMap *GameMap, isShooting bool, players map[uint32]*Player) (moved bool) { if player.movementCooldown > 0 { return false } ranOutOfEnergy := (isShooting && player.energy <= serverConfig.DiggingCost+serverConfig.ShootDiggingCostBonus) || player.energy <= serverConfig.DiggingCost if ranOutOfEnergy { isShooting = false } shouldPenalizeDigging := false stopped := false switch player.gameObject.orientation { case 0: // Up collisionRect := sdl.Rect{ X: player.gameObject.baseRect.X, Y: player.gameObject.baseRect.Y - 1, W: 5, H: 7, } for _, opponent := range players { if opponent != player && opponent.gameObject.baseRect.HasIntersection(&collisionRect) && opponent.shields <= serverConfig.MaxShields { return false } } for x := player.gameObject.baseRect.X; x < player.gameObject.baseRect.X+5 && !stopped; x++ { for y := player.gameObject.baseRect.Y - 1; y < player.gameObject.baseRect.Y+7; y++ { digResult := player.digBlock(x, y, gameMap, isShooting) if digResult == 1 { shouldPenalizeDigging = true if !isShooting { stopped = true break } } else if digResult != 0 { stopped = true break } } } if !stopped { moved = true player.gameObject.baseRect.Y-- } case 1: // Right collisionRect := sdl.Rect{ X: player.gameObject.baseRect.X + 1, Y: player.gameObject.baseRect.Y, W: 7, H: 5, } for _, opponent := range players { if opponent != player && opponent.gameObject.baseRect.HasIntersection(&collisionRect) { return false } } for y := player.gameObject.baseRect.Y + 1; y < player.gameObject.baseRect.Y+6 && !stopped; y++ { for x := player.gameObject.baseRect.X + 1; x < player.gameObject.baseRect.X+8; x++ { digResult := player.digBlock(x, y, gameMap, isShooting) if digResult == 1 { shouldPenalizeDigging = true if !isShooting { stopped = true break } } else if digResult != 0 { stopped = true break } } } if !stopped { moved = true player.gameObject.baseRect.X++ } case 2: // Down collisionRect := sdl.Rect{ X: player.gameObject.baseRect.X, Y: player.gameObject.baseRect.Y + 1, W: 5, H: 7, } for _, opponent := range players { if opponent != player && opponent.gameObject.baseRect.HasIntersection(&collisionRect) { return false } } for x := player.gameObject.baseRect.X; x < player.gameObject.baseRect.X+5 && !stopped; x++ { for y := player.gameObject.baseRect.Y + 1; y < player.gameObject.baseRect.Y+8; y++ { digResult := player.digBlock(x, y, gameMap, isShooting) if digResult == 1 { shouldPenalizeDigging = true if !isShooting { stopped = true break } } else if digResult != 0 { stopped = true break } } } if !stopped { moved = true player.gameObject.baseRect.Y++ } case 3: // Left collisionRect := sdl.Rect{ X: player.gameObject.baseRect.X - 1, Y: player.gameObject.baseRect.Y, W: 7, H: 5, } for _, opponent := range players { if opponent != player && opponent.gameObject.baseRect.HasIntersection(&collisionRect) { return false } } for y := player.gameObject.baseRect.Y + 1; y < player.gameObject.baseRect.Y+6 && !stopped; y++ { for x := player.gameObject.baseRect.X - 1; x < player.gameObject.baseRect.X+6; x++ { digResult := player.digBlock(x, y, gameMap, isShooting) if digResult == 1 { shouldPenalizeDigging = true if !isShooting { stopped = true break } } else if digResult != 0 { stopped = true break } } } if !stopped { moved = true player.gameObject.baseRect.X-- } case 4: // Up-Right collisionRect := sdl.Rect{ X: player.gameObject.baseRect.X + 1, Y: player.gameObject.baseRect.Y - 1, W: 7, H: 7, } for _, opponent := range players { if opponent != player && opponent.gameObject.baseRect.HasIntersection(&collisionRect) { return false } } for x := player.gameObject.baseRect.X + 1; x < player.gameObject.baseRect.X+8 && !stopped; x++ { for y := player.gameObject.baseRect.Y - 1; y < player.gameObject.baseRect.Y+6; y++ { digResult := player.digBlock(x, y, gameMap, isShooting) if digResult == 1 { shouldPenalizeDigging = true if !isShooting { stopped = true break } } else if digResult != 0 { stopped = true break } } } if !stopped { moved = true player.gameObject.baseRect.X++ player.gameObject.baseRect.Y-- } case 5: // Up-Left collisionRect := sdl.Rect{ X: player.gameObject.baseRect.X - 1, Y: player.gameObject.baseRect.Y - 1, W: 7, H: 7, } for _, opponent := range players { if opponent != player && opponent.gameObject.baseRect.HasIntersection(&collisionRect) { return false } } for x := player.gameObject.baseRect.X - 1; x < player.gameObject.baseRect.X+6 && !stopped; x++ { for y := player.gameObject.baseRect.Y - 1; y < player.gameObject.baseRect.Y+6; y++ { digResult := player.digBlock(x, y, gameMap, isShooting) if digResult == 1 { shouldPenalizeDigging = true if !isShooting { stopped = true break } } else if digResult != 0 { stopped = true break } } } if !stopped { moved = true player.gameObject.baseRect.X-- player.gameObject.baseRect.Y-- } case 6: // Down-Right collisionRect := sdl.Rect{ X: player.gameObject.baseRect.X + 1, Y: player.gameObject.baseRect.Y + 1, W: 7, H: 7, } for _, opponent := range players { if opponent != player && opponent.gameObject.baseRect.HasIntersection(&collisionRect) { return false } } for x := player.gameObject.baseRect.X + 1; x < player.gameObject.baseRect.X+8 && !stopped; x++ { for y := player.gameObject.baseRect.Y + 1; y < player.gameObject.baseRect.Y+8; y++ { digResult := player.digBlock(x, y, gameMap, isShooting) if digResult == 1 { shouldPenalizeDigging = true if !isShooting { stopped = true break } } else if digResult != 0 { stopped = true break } } } if !stopped { moved = true player.gameObject.baseRect.X++ player.gameObject.baseRect.Y++ } case 7: // Down-Left collisionRect := sdl.Rect{ X: player.gameObject.baseRect.X - 1, Y: player.gameObject.baseRect.Y + 1, W: 7, H: 7, } for _, opponent := range players { if opponent != player && opponent.gameObject.baseRect.HasIntersection(&collisionRect) { return false } } for x := player.gameObject.baseRect.X - 1; x < player.gameObject.baseRect.X+6 && !stopped; x++ { for y := player.gameObject.baseRect.Y + 1; y < player.gameObject.baseRect.Y+8; y++ { digResult := player.digBlock(x, y, gameMap, isShooting) if digResult == 1 { shouldPenalizeDigging = true if !isShooting { stopped = true break } } else if digResult != 0 { stopped = true break } } } if !stopped { moved = true player.gameObject.baseRect.X-- player.gameObject.baseRect.Y++ } } if config.Client { player.sendPositionToServer() } // Penalties and cooldown handling if shouldPenalizeDigging { if isShooting && player.ammunition < serverConfig.MaxAmmunition && rand.Intn(2) == 0 { player.ammunition++ } if ranOutOfEnergy { player.digCooldown = serverConfig.DiggingCooldownNoEnergy } player.energy -= serverConfig.DiggingCost if isShooting { player.energy -= serverConfig.ShootDiggingCostBonus } } if moved { if ranOutOfEnergy { player.movementCooldown = serverConfig.MovementCooldownNoEnergy } else { player.movementCooldown = serverConfig.MovementCooldown } } return } 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) } 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 switch player.gameObject.orientation { case 0: // Up shootY = player.gameObject.baseRect.Y - 1 shootX = player.gameObject.baseRect.X + 2 break case 1: // Right shootY = player.gameObject.baseRect.Y + 3 shootX = player.gameObject.baseRect.X + 8 break case 2: // Down shootX = player.gameObject.baseRect.X + 2 shootY = player.gameObject.baseRect.Y + 8 break case 3: // Left shootY = player.gameObject.baseRect.Y + 3 shootX = player.gameObject.baseRect.X - 2 break case 4: // Up-Right shootY = player.gameObject.baseRect.Y shootX = player.gameObject.baseRect.X + 5 break case 5: // Up-Left shootY = player.gameObject.baseRect.Y shootX = player.gameObject.baseRect.X - 1 break case 6: // Down-Right shootY = player.gameObject.baseRect.Y + 5 shootX = player.gameObject.baseRect.X + 5 break case 7: // Down-Left shootY = player.gameObject.baseRect.Y + 5 shootX = player.gameObject.baseRect.X - 1 break } 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-- } if config.Client { player.sendShootToServer(super) } else { // Set bullet color bulletColor := player.playerColors.body // Create and add the bullet bullets[bulletLastID] = &Bullet{ posX: shootX, posY: shootY, direction: player.gameObject.orientation, color: bulletColor, super: super, id: bulletLastID, } 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 bullets[bulletLastID] = &Bullet{ posX: x, posY: y, direction: uint8(rand.Intn(8)), color: bulletColor, super: true, id: bulletLastID, } bulletLastID++ } } } func createPlayers(amount uint8, playerColors []PlayerColors, keyMaps []KeyMap, joyMaps []JoyMap, gameMap *GameMap, players map[uint32]*Player) { joyStickCount := sdl.NumJoysticks() if amount > uint8(len(keyMaps)+len(joyMaps)) || amount > uint8(len(keyMaps)+joyStickCount) { panic("Too many players, not enough inputs") } if amount >= uint8(len(playerColors)) { panic("Too many players, not enough colors") } addedKeyboardPlayers := 0 for i := uint8(0); i < amount; i++ { var keyMap KeyMap var joyMap JoyMap var joyStick *sdl.Joystick if (config.DoAllKeymapsPlayers && i <= uint8(len(keyMaps))) || (config.DoKeymapPlayer && i == 0) || (uint8(joyStickCount) <= i) { keyMap = keyMaps[addedKeyboardPlayers] addedKeyboardPlayers++ } else { joyStickIndex := i - uint8(addedKeyboardPlayers) joyMap = joyMaps[joyStickIndex] joyStick = sdl.JoystickOpen(int(joyStickIndex)) } createPlayer( true, playerColors[len(players)%len(playerColors)], nil, keyMap, joyMap, joyStick, gameMap, players, ) } } func closeThings(players map[uint32]*Player) { for _, player := range players { if player.joyStick != nil { player.joyStick.Close() } if player.window != nil { player.window.Destroy() } if player.logicalSurface != nil { player.logicalSurface.Free() } if player.playSurface != nil { player.playSurface.Free() } if player.HUDSurface != nil { player.HUDSurface.Free() } } } func createPlayer(local bool, thisPlayerColors PlayerColors, conn *net.Conn, keyMap KeyMap, joyMap JoyMap, joyStick *sdl.Joystick, gameMap *GameMap, players map[uint32]*Player) uint32 { coordsAreValid := false var posX, posY int32 maxTries := 1000 for !coordsAreValid && maxTries >= 0 { maxTries-- posX = int32(16 + rand.Intn(int(gameMap.width-43))) posY = int32(16 + rand.Intn(int(gameMap.height-43))) coordsAreValid = true for _, player := range players { distance := (player.gameObject.baseRect.X-posX)*(player.gameObject.baseRect.X-posX) + (player.gameObject.baseRect.Y-posY)*(player.gameObject.baseRect.Y-posY) if distance < 300*300 { // Check if distance is less than 300 units coordsAreValid = false break } } if posX < 16 || posX > int32(gameMap.width-36-7) || posY < 16 || posY > int32(gameMap.height-36-7) { coordsAreValid = false break } } if maxTries < 0 { panic("Could not place all players, increase map size") } gameObject := &GameObject{} gameObject.baseRect = sdl.Rect{ X: posX, Y: posY, W: 7, H: 7, } gameObject.addColor(thisPlayerColors.tracks) gameObject.addColor(thisPlayerColors.body) gameObject.addColor(thisPlayerColors.cannon) gameObject.orientation = 0 // Up gameObject.addColoredRect(0, 1, 1, 6, 0) gameObject.addColoredRect(4, 1, 1, 6, 0) gameObject.addColoredRect(1, 2, 3, 4, 1) gameObject.addColoredRect(2, 0, 1, 4, 2) gameObject.orientation = 1 // Right gameObject.addColoredRect(1, 1, 6, 1, 0) gameObject.addColoredRect(1, 5, 6, 1, 0) gameObject.addColoredRect(2, 2, 4, 3, 1) gameObject.addColoredRect(4, 3, 4, 1, 2) gameObject.orientation = 2 // Down gameObject.addColoredRect(0, 1, 1, 6, 0) gameObject.addColoredRect(4, 1, 1, 6, 0) gameObject.addColoredRect(1, 2, 3, 4, 1) gameObject.addColoredRect(2, 4, 1, 4, 2) gameObject.orientation = 3 // Left gameObject.addColoredRect(1, 1, 6, 1, 0) gameObject.addColoredRect(1, 5, 6, 1, 0) gameObject.addColoredRect(2, 2, 4, 3, 1) gameObject.addColoredRect(0, 3, 4, 1, 2) gameObject.orientation = 4 // Up-Right gameObject.addColoredRect(3, 0, 1, 1, 0) gameObject.addColoredRect(2, 1, 1, 1, 0) gameObject.addColoredRect(1, 2, 1, 1, 0) gameObject.addColoredRect(0, 3, 1, 1, 0) gameObject.addColoredRect(6, 3, 1, 1, 0) gameObject.addColoredRect(5, 4, 1, 1, 0) gameObject.addColoredRect(4, 5, 1, 1, 0) gameObject.addColoredRect(3, 6, 1, 1, 0) gameObject.addColoredRect(3, 1, 1, 1, 1) gameObject.addColoredRect(2, 2, 2, 1, 1) gameObject.addColoredRect(1, 3, 2, 1, 1) gameObject.addColoredRect(4, 3, 2, 1, 1) gameObject.addColoredRect(2, 4, 3, 1, 1) gameObject.addColoredRect(3, 5, 1, 1, 1) gameObject.addColoredRect(5, 1, 1, 1, 2) gameObject.addColoredRect(4, 2, 1, 1, 2) gameObject.addColoredRect(3, 3, 1, 1, 2) // Up-Left orientation (Y-axis reflection) gameObject.orientation = 5 // Up-Left gameObject.addColoredRect(3, 0, 1, 1, 0) gameObject.addColoredRect(4, 1, 1, 1, 0) gameObject.addColoredRect(5, 2, 1, 1, 0) gameObject.addColoredRect(6, 3, 1, 1, 0) gameObject.addColoredRect(0, 3, 1, 1, 0) gameObject.addColoredRect(1, 4, 1, 1, 0) gameObject.addColoredRect(2, 5, 1, 1, 0) gameObject.addColoredRect(3, 6, 1, 1, 0) gameObject.addColoredRect(3, 1, 1, 1, 1) gameObject.addColoredRect(3, 2, 2, 1, 1) gameObject.addColoredRect(4, 3, 2, 1, 1) gameObject.addColoredRect(1, 3, 2, 1, 1) gameObject.addColoredRect(2, 4, 3, 1, 1) gameObject.addColoredRect(3, 5, 1, 1, 1) gameObject.addColoredRect(1, 1, 1, 1, 2) gameObject.addColoredRect(2, 2, 1, 1, 2) gameObject.addColoredRect(3, 3, 1, 1, 2) // Down-Right orientation (X-axis reflection) gameObject.orientation = 6 // Down-Right gameObject.addColoredRect(3, 6, 1, 1, 0) gameObject.addColoredRect(2, 5, 1, 1, 0) gameObject.addColoredRect(1, 4, 1, 1, 0) gameObject.addColoredRect(0, 3, 1, 1, 0) gameObject.addColoredRect(6, 3, 1, 1, 0) gameObject.addColoredRect(5, 2, 1, 1, 0) gameObject.addColoredRect(4, 1, 1, 1, 0) gameObject.addColoredRect(3, 0, 1, 1, 0) gameObject.addColoredRect(3, 5, 1, 1, 1) gameObject.addColoredRect(2, 4, 2, 1, 1) gameObject.addColoredRect(1, 3, 2, 1, 1) gameObject.addColoredRect(4, 3, 2, 1, 1) gameObject.addColoredRect(2, 2, 3, 1, 1) gameObject.addColoredRect(3, 1, 1, 1, 1) gameObject.addColoredRect(5, 5, 1, 1, 2) gameObject.addColoredRect(4, 4, 1, 1, 2) gameObject.addColoredRect(3, 3, 1, 1, 2) // Down-Left orientation (XY reflection) gameObject.orientation = 7 // Down-Left gameObject.addColoredRect(3, 6, 1, 1, 0) gameObject.addColoredRect(4, 5, 1, 1, 0) gameObject.addColoredRect(5, 4, 1, 1, 0) gameObject.addColoredRect(6, 3, 1, 1, 0) gameObject.addColoredRect(0, 3, 1, 1, 0) gameObject.addColoredRect(1, 2, 1, 1, 0) gameObject.addColoredRect(2, 1, 1, 1, 0) gameObject.addColoredRect(3, 0, 1, 1, 0) gameObject.addColoredRect(3, 1, 1, 1, 1) gameObject.addColoredRect(2, 2, 3, 1, 1) gameObject.addColoredRect(1, 3, 2, 1, 1) gameObject.addColoredRect(4, 3, 2, 1, 1) gameObject.addColoredRect(3, 4, 2, 1, 1) gameObject.addColoredRect(3, 5, 1, 1, 1) gameObject.addColoredRect(1, 5, 1, 1, 2) gameObject.addColoredRect(2, 4, 1, 1, 2) gameObject.addColoredRect(3, 3, 1, 1, 2) gameObject.orientation = 0 if !local && (keyMap.exit != keyMap.shoot || joyMap.exitButton != joyMap.shootButton) { panic("Input assigned to remote player") } knownGameMap := GameMap{width: gameMap.width, height: gameMap.height, tiles: make([][]uint8, serverConfig.MapWidth)} for i := uint32(0); i < serverConfig.MapWidth; i++ { knownGameMap.tiles[i] = make([]uint8, serverConfig.MapHeight) } players[lastPlayerID] = &Player{ playerColors: thisPlayerColors, keyMap: keyMap, joyMap: joyMap, joyStick: joyStick, shields: serverConfig.MaxShields, energy: serverConfig.MaxEnergy, gameObject: gameObject, local: local, connection: conn, playerID: lastPlayerID, knownBulletParticles: make(map[uint32]*BulletParticle), knownBases: make(map[uint32]*Base), knownPlayers: make(map[uint32]*Player), knownBullets: make(map[uint32]*Bullet), knownGameMap: &knownGameMap, } lastPlayerID++ return lastPlayerID - 1 } func sendMessageRaw(data []byte, conn *net.Conn) (fail bool) { if conn != nil { // Send the length of the message first length := uint32(len(data)) lengthBuf := new(bytes.Buffer) if err := binary.Write(lengthBuf, binary.BigEndian, length); err != nil { if config.Server { log.Printf("Failed to write message length: %v", err) fail = true } else { log.Fatalf("Failed to write message length: %v", err) } } // Send the length followed by the message itself if _, err := (*conn).Write(lengthBuf.Bytes()); err != nil { if config.Server { log.Printf("Failed to write message length to connection: %v", err) fail = true } else { log.Fatalf("Failed to write message length to connection: %v", err) } } if _, err := (*conn).Write(data); err != nil { if config.Server { log.Printf("Failed to write message to connection: %v", err) fail = true } else { log.Fatalf("Failed to write message to connection: %v", err) } } } return } func (player *Player) sendMessage(data []byte) bool { return sendMessageRaw(data, player.connection) } func (player *Player) updateRemotePlayerMap() { playerUpdateMutex.Lock() defer playerUpdateMutex.Unlock() 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() player.gameMapUpdates = append(player.gameMapUpdates, TileUpdate{ PosX: uint32(x), PosY: uint32(y), Kind: gameMap.tiles[x][y], }) 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) { playerUpdateMutex.Lock() defer playerUpdateMutex.Unlock() player.knownPlayers = make(map[uint32]*Player) 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 { player.knownPlayers[playerLoop.playerID] = playerLoop break } } } } func (player *Player) updateRemotePlayerBullets(bullets map[uint32]*Bullet) { playerUpdateMutex.Lock() defer playerUpdateMutex.Unlock() player.knownBullets = make(map[uint32]*Bullet) 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) { player.knownBullets[bullet.id] = bullet } } } func (player *Player) updateRemotePlayerBulletParticles(bulletParticles map[uint32]*BulletParticle) { playerUpdateMutex.Lock() defer playerUpdateMutex.Unlock() player.knownBulletParticles = make(map[uint32]*BulletParticle) 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) { player.knownBulletParticles[bulletParticle.id] = bulletParticle } } } func (player *Player) updateRemotePlayerBases(bases map[uint32]*Base) { playerUpdateMutex.Lock() defer playerUpdateMutex.Unlock() player.knownBases = make(map[uint32]*Base) 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)) { player.knownBases[base.ownerID] = base break } } } } func (player *Player) sendVersionBackToPlayer() bool { if player.connection != nil && config.Server { response := tunnelerProto.ClientBound{ ClientBoundMessage: &tunnelerProto.ClientBound_PlayerStartResponse{ PlayerStartResponse: &tunnelerProto.PlayerStartResponse{ Version: config.Version, }, }, } out, err := proto.Marshal(&response) if err != nil { panic("Error serializing version") } return player.sendMessage(out) } return false } func (player *Player) sendServerInfoToPlayer(players map[uint32]*Player) bool { if player.connection != nil && config.Server { response := tunnelerProto.ClientBound{ ClientBoundMessage: &tunnelerProto.ClientBound_ServerInfo{ ServerInfo: &tunnelerProto.ServerInfo{ MaxEnergy: serverConfig.MaxEnergy, MaxAmmunition: serverConfig.MaxAmmunition, MaxShields: serverConfig.MaxShields, MapWidth: serverConfig.MapWidth, MapHeight: serverConfig.MapHeight, NormalShotCost: serverConfig.NormalShotCost, SuperShotCost: serverConfig.SuperShotCost, ReloadCost: serverConfig.ReloadCost, MovementCost: serverConfig.MovementCost, DiggingCost: serverConfig.DiggingCost, ShootDiggingBonus: serverConfig.ShootDiggingCostBonus, ShootCooldown: serverConfig.ShootCooldown, RechargeCooldown: serverConfig.RechargeCooldownOwn, RechargeOpponentCooldown: serverConfig.RechargeCooldownOpponent, RepairCooldown: serverConfig.RepairCooldown, DiggingCooldown: serverConfig.DiggingCooldown, MovementCooldown: serverConfig.MovementCooldown, MovementCooldownNoEnergy: serverConfig.MovementCooldownNoEnergy, DiggingCooldownNoEnergy: serverConfig.DiggingCooldownNoEnergy, ReloadCooldown: serverConfig.ReloadCooldown, BlastRadius: serverConfig.BlastRadius, PlayerID: player.playerID, PlayerColorID: uint32((len(players) - 1) % len(playerColors)), }, }, } out, err := proto.Marshal(&response) if err != nil { panic("Error serializing server info") } return player.sendMessage(out) } return false } func (player *Player) sendInfoToPlayer() bool { if player.connection != nil && config.Server { response := tunnelerProto.ClientBound{ ClientBoundMessage: &tunnelerProto.ClientBound_PlayerUpdate{ PlayerUpdate: &tunnelerProto.PlayerUpdate{ Energy: player.energy, Ammo: player.ammunition, Shields: player.shields, }, }, } out, err := proto.Marshal(&response) if err != nil { panic("Error serializing info") } return player.sendMessage(out) } return false } func (player *Player) sendLocationToPlayer() bool { if player.connection != nil && config.Server { response := tunnelerProto.ClientBound{ ClientBoundMessage: &tunnelerProto.ClientBound_PlayerLocationUpdate{ PlayerLocationUpdate: &tunnelerProto.PlayerLocation{ Position: &tunnelerProto.Position{ PosX: player.gameObject.baseRect.X, PosY: player.gameObject.baseRect.Y, }, Orientation: uint32(player.gameObject.orientation), }, }, } out, err := proto.Marshal(&response) if err != nil { panic("Error serializing location") } return player.sendMessage(out) } return false } var playerUpdateMutex sync.Mutex func (player *Player) sendUpdatesToPlayer(players map[uint32]*Player) bool { playerUpdateMutex.Lock() defer playerUpdateMutex.Unlock() if player.connection != nil && config.Server { var playersToSend []*tunnelerProto.Player var basesToSend []*tunnelerProto.BaseLocation var bulletsToSend []*tunnelerProto.Bullet var bulletParticlesToSend []*tunnelerProto.BulletParticle var tileUpdatesToSend []*tunnelerProto.TileUpdate for playerID, knownPlayer := range player.knownPlayers { if playerID == player.playerID { continue } playersToSend = append(playersToSend, &tunnelerProto.Player{ PlayerID: playerID, Location: &tunnelerProto.PlayerLocation{ Position: &tunnelerProto.Position{ PosX: knownPlayer.gameObject.baseRect.X, PosY: knownPlayer.gameObject.baseRect.Y, }, Orientation: uint32(knownPlayer.gameObject.orientation), }, }) } for _, knownBase := range player.knownBases { r, g, b, a := players[knownBase.ownerID].playerColors.body.RGBA() basesToSend = append(basesToSend, &tunnelerProto.BaseLocation{ Position: &tunnelerProto.Position{ PosX: knownBase.gameObject.baseRect.X, PosY: knownBase.gameObject.baseRect.Y, }, Owner: &tunnelerProto.Player{ PlayerID: knownBase.ownerID, Location: nil, }, Color: &tunnelerProto.Color{ Red: r, Green: g, Blue: b, Alpha: a, }, }) } for _, knownBullet := range player.knownBullets { bulletRed, bulletGreen, bulletBlue, bulletAlpha := knownBullet.color.RGBA() bulletsToSend = append(bulletsToSend, &tunnelerProto.Bullet{ Position: &tunnelerProto.Position{ PosX: knownBullet.posX, PosY: knownBullet.posY, }, Direction: uint32(knownBullet.direction), Color: &tunnelerProto.Color{ Red: bulletRed, Green: bulletGreen, Blue: bulletBlue, Alpha: bulletAlpha, }, Id: knownBullet.id, }) } for _, knownBulletParticle := range player.knownBulletParticles { bulletParticleRed, bulletParticleGreen, bulletParticleBlue, bulletParticleAlpha := knownBulletParticle.color.RGBA() bulletParticlesToSend = append(bulletParticlesToSend, &tunnelerProto.BulletParticle{ Position: &tunnelerProto.Position{ PosX: knownBulletParticle.posX, PosY: knownBulletParticle.posY, }, ExpirationTimer: knownBulletParticle.expirationTimer, Color: &tunnelerProto.Color{ Red: bulletParticleRed, Green: bulletParticleGreen, Blue: bulletParticleBlue, Alpha: bulletParticleAlpha, }, Id: knownBulletParticle.id, }) } for _, tileUpdate := range player.gameMapUpdates { tileUpdatesToSend = append(tileUpdatesToSend, &tunnelerProto.TileUpdate{ Position: &tunnelerProto.Position{ PosX: int32(tileUpdate.PosX), PosY: int32(tileUpdate.PosY), }, Kind: uint32(tileUpdate.Kind), }) } response := tunnelerProto.ClientBound{ ClientBoundMessage: &tunnelerProto.ClientBound_WorldUpdate{ WorldUpdate: &tunnelerProto.WorldUpdate{ Players: playersToSend, Base: basesToSend, Bullets: bulletsToSend, BulletParticles: bulletParticlesToSend, TileUpdate: tileUpdatesToSend, }, }, } out, err := proto.Marshal(&response) if err != nil { panic("Error serializing updates") } return player.sendMessage(out) } return false } func (player *Player) sendPositionToServer() bool { if player.connection != nil && config.Client { response := tunnelerProto.ServerBound{ ServerBoundMessage: &tunnelerProto.ServerBound_PlayerPosition{ PlayerPosition: &tunnelerProto.PlayerLocation{ Position: &tunnelerProto.Position{ PosX: player.gameObject.baseRect.X, PosY: player.gameObject.baseRect.Y, }, Orientation: uint32(player.gameObject.orientation), }, }, } out, err := proto.Marshal(&response) if err != nil { panic("Error serializing location") } return player.sendMessage(out) } return false } func sendVersionToServer(connection *net.Conn) { if connection != nil && config.Client { response := tunnelerProto.ServerBound{ ServerBoundMessage: &tunnelerProto.ServerBound_PlayerStartRequest{ PlayerStartRequest: &tunnelerProto.PlayerStartRequest{ Version: config.Version, }, }, } out, err := proto.Marshal(&response) if err != nil { panic("Error serializing version") } sendMessageRaw(out, connection) } } func (player *Player) sendDigToServer(posX, posY uint32, isShooting bool) { if player.connection != nil && config.Client { response := tunnelerProto.ServerBound{ ServerBoundMessage: &tunnelerProto.ServerBound_PlayerAction{ PlayerAction: &tunnelerProto.PlayerAction{ PlayerAction: &tunnelerProto.PlayerAction_DigBlock{ DigBlock: &tunnelerProto.DigBlock{ Position: &tunnelerProto.Position{ PosX: int32(posX), PosY: int32(posY), }, IsShooting: isShooting, }, }, }, }, } out, err := proto.Marshal(&response) if err != nil { panic("Error serializing dig") } player.sendMessage(out) } } func (player *Player) sendShootToServer(isSuper bool) { if player.connection != nil && config.Client { response := tunnelerProto.ServerBound{ ServerBoundMessage: &tunnelerProto.ServerBound_PlayerAction{ PlayerAction: &tunnelerProto.PlayerAction{ PlayerAction: &tunnelerProto.PlayerAction_Shoot{ Shoot: &tunnelerProto.Shoot{ Super: isSuper, }, }, }, }, } out, err := proto.Marshal(&response) if err != nil { panic("Error serializing shot") } player.sendMessage(out) } } func handleRequest(conn net.Conn, players map[uint32]*Player, bullets map[uint32]*Bullet, bases map[uint32]*Base) { defer conn.Close() r := bufio.NewReader(conn) for { // Read the length of the incoming message (4 bytes) lengthBuf := make([]byte, 4) _, err := io.ReadFull(r, lengthBuf) if err != nil { log.Printf("Failed to read message length: %v", err) return } // Convert length to an integer length := binary.BigEndian.Uint32(lengthBuf) // Read the actual message data messageBuf := make([]byte, length) _, err = io.ReadFull(r, messageBuf) if err != nil { log.Printf("Failed to read message data: %v", err) return } // Unmarshal the protobuf message var msg tunnelerProto.ServerBound if err := proto.Unmarshal(messageBuf, &msg); err != nil { log.Printf("Failed to unmarshal protobuf message: %v", err) return } switch msg.GetServerBoundMessage().(type) { case *tunnelerProto.ServerBound_PlayerStartRequest: clientVersion := msg.GetPlayerStartRequest().Version if clientVersion != config.Version { log.Fatalf("Wrong client version, a client connected with %s to %s", clientVersion, config.Version) return } newPlayerID := createPlayer(false, playerColors[len(players)%len(playerColors)], &conn, KeyMap{}, JoyMap{}, nil, gameMap, players) defer delete(players, newPlayerID) player := players[newPlayerID] bases[newPlayerID] = createBase(gameMap, player.playerColors.body, uint32(player.gameObject.baseRect.X-14), uint32(player.gameObject.baseRect.Y-14), player.playerID, uint32(float64(player.gameObject.baseRect.W)*1.5)) defer bases[newPlayerID].delete(bases) netPlayerMapper[&conn] = player fail := player.sendVersionBackToPlayer() if fail { return } log.Printf("Sent version to %d", newPlayerID) fail = player.sendServerInfoToPlayer(players) if fail { return } log.Printf("Sent server info to %d", newPlayerID) fail = player.sendInfoToPlayer() if fail { return } log.Printf("Sent info to %d", newPlayerID) fail = player.sendLocationToPlayer() if fail { return } log.Printf("Sent location to %d", newPlayerID) case *tunnelerProto.ServerBound_PlayerPosition: player := netPlayerMapper[&conn] if player == nil { return } newPosition := msg.GetPlayerPosition() newX := newPosition.Position.PosX newY := newPosition.Position.PosY oldX := player.gameObject.baseRect.X oldY := player.gameObject.baseRect.Y if ((newX > oldX && newX-oldX <= 2) || (newX < oldX && oldX-newX <= 2) || newX == oldX) && (newY > oldY && newY-oldY <= 2) || (newY < oldY && oldY-newY <= 2 || newY == oldY) { player.gameObject.baseRect = sdl.Rect{ X: newX, Y: newY, W: player.gameObject.baseRect.W, H: player.gameObject.baseRect.H, } } player.gameObject.orientation = uint8(newPosition.Orientation) case *tunnelerProto.ServerBound_PlayerAction: player := netPlayerMapper[&conn] if player == nil { return } playerAction := msg.GetPlayerAction() if playerAction == nil { return } switch action := playerAction.PlayerAction.(type) { case *tunnelerProto.PlayerAction_DigBlock: position := action.DigBlock.Position isShooting := action.DigBlock.IsShooting //TODO:FIX && player.ammunition < serverConfig.MaxAmmunition player.digBlock(position.PosX, position.PosY, gameMap, isShooting) case *tunnelerProto.PlayerAction_Shoot: isSuper := action.Shoot.Super player.shoot(isSuper, bullets) } } } } func handleConnectionClient(conn *net.Conn, players map[uint32]*Player, bases map[uint32]*Base, bullets map[uint32]*Bullet, bulletParticles map[uint32]*BulletParticle) { var player *Player sendVersionToServer(conn) r := bufio.NewReader(*conn) for { // Read the length of the incoming message (4 bytes) lengthBuf := make([]byte, 4) _, err := io.ReadFull(r, lengthBuf) if err != nil { log.Printf("Failed to read message length: %v", err) os.Exit(0) } // Convert length to an integer length := binary.BigEndian.Uint32(lengthBuf) // Read the actual message data messageBuf := make([]byte, length) _, err = io.ReadFull(r, messageBuf) if err != nil { log.Printf("Failed to read message data: %v", err) os.Exit(0) } var msg tunnelerProto.ClientBound if err := proto.Unmarshal(messageBuf, &msg); err != nil { log.Printf("Failed to unmarshal protobuf message: %v", err) } switch msg.GetClientBoundMessage().(type) { case *tunnelerProto.ClientBound_PlayerStartResponse: serverVersion := msg.GetPlayerStartResponse().Version if serverVersion != config.Version { log.Fatalf("Wrong server version, connected with %s to %s", config.Version, serverVersion) return } else { log.Printf("Connected to %s, running version %s with version %s\n", config.Address, serverVersion, config.Version) } case *tunnelerProto.ClientBound_ServerInfo: serverInfo := msg.GetServerInfo() serverConfig = ServerConfig{ MapWidth: serverInfo.MapWidth, MapHeight: serverInfo.MapHeight, BlastRadius: serverInfo.BlastRadius, MaxEnergy: serverInfo.MaxEnergy, MaxAmmunition: serverInfo.MaxAmmunition, MaxShields: serverInfo.MaxShields, NormalShotCost: serverInfo.NormalShotCost, SuperShotCost: serverInfo.SuperShotCost, ReloadCost: serverInfo.ReloadCost, MovementCost: serverInfo.MovementCost, DiggingCost: serverInfo.DiggingCost, ShootDiggingCostBonus: serverInfo.ShootDiggingBonus, ShootCooldown: serverInfo.ShootCooldown, RechargeCooldownOwn: serverInfo.RechargeCooldown, DiggingCooldown: serverInfo.DiggingCooldown, RechargeCooldownOpponent: serverInfo.RechargeOpponentCooldown, RepairCooldown: serverInfo.RepairCooldown, MovementCooldown: serverInfo.MovementCooldown, MovementCooldownNoEnergy: serverInfo.MovementCooldownNoEnergy, DiggingCooldownNoEnergy: serverInfo.DiggingCooldownNoEnergy, ReloadCooldown: serverInfo.ReloadCooldown, } gameMap.createGameMap(false) lastPlayerID = serverInfo.PlayerID createPlayer(true, playerColors[serverInfo.PlayerColorID], conn, keyMaps[0], JoyMap{}, nil, gameMap, players) player = players[serverInfo.PlayerID] log.Printf("Got server info, now initializing\n") clientInitialized = true case *tunnelerProto.ClientBound_PlayerUpdate: if clientInitialized { playerUpdate := msg.GetPlayerUpdate() player.energy = playerUpdate.Energy player.ammunition = playerUpdate.Ammo player.shields = playerUpdate.Shields } case *tunnelerProto.ClientBound_PlayerLocationUpdate: if clientInitialized { playerLocationUpdate := msg.GetPlayerLocationUpdate() player.gameObject.baseRect.X = playerLocationUpdate.Position.PosX player.gameObject.baseRect.Y = playerLocationUpdate.Position.PosY player.gameObject.orientation = uint8(playerLocationUpdate.Orientation) } case *tunnelerProto.ClientBound_WorldUpdate: if clientInitialized { worldUpdate := msg.GetWorldUpdate() worldLock.Lock() for _, playerLoop := range worldUpdate.Players { playerID := playerLoop.PlayerID existingPlayer := players[playerID] if existingPlayer == nil { lastPlayerID = playerID createPlayer(false, playerColors[playerID%uint32(len(playerColors))], nil, KeyMap{}, JoyMap{}, nil, gameMap, players) newPlayer := players[playerID] newPlayer.gameObject.orientation = uint8(playerLoop.Location.Orientation) newPlayer.gameObject.baseRect.X = playerLoop.Location.Position.PosX newPlayer.gameObject.baseRect.Y = playerLoop.Location.Position.PosY } else { existingPlayer.gameObject.orientation = uint8(playerLoop.Location.Orientation) existingPlayer.gameObject.baseRect.X = playerLoop.Location.Position.PosX existingPlayer.gameObject.baseRect.Y = playerLoop.Location.Position.PosY } } for _, base := range worldUpdate.Base { baseID := base.Owner.PlayerID existingBase := bases[baseID] inColor := color.RGBA{ R: uint8(base.Color.Red), G: uint8(base.Color.Green), B: uint8(base.Color.Blue), A: uint8(base.Color.Alpha), } if existingBase == nil { bases[baseID] = createBase(gameMap, inColor, uint32(base.Position.PosX), uint32(base.Position.PosY), base.Owner.PlayerID, uint32(int32(float64(player.gameObject.baseRect.W)*1.5)), ) } else { existingBase.gameObject.baseRect.X = base.Position.PosX existingBase.gameObject.baseRect.Y = base.Position.PosY existingBase.gameObject.colors[0] = inColor } } for _, bullet := range worldUpdate.Bullets { bulletID := bullet.Id inColor := color.RGBA{ R: uint8(bullet.Color.Red), G: uint8(bullet.Color.Green), B: uint8(bullet.Color.Blue), A: uint8(bullet.Color.Alpha), } existingBullet := bullets[bulletID] if existingBullet == nil { bullets[bulletID] = &Bullet{ posX: bullet.Position.PosX, posY: bullet.Position.PosY, direction: uint8(bullet.Direction), color: inColor, super: bullet.Super, id: bulletID, } } else { existingBullet.direction = uint8(bullet.Direction) existingBullet.color = inColor existingBullet.super = bullet.Super existingBullet.posX = bullet.Position.PosX existingBullet.posY = bullet.Position.PosY } } for _, bulletParticle := range worldUpdate.BulletParticles { bulletParticleID := bulletParticle.Id inColor := color.RGBA{ R: uint8(bulletParticle.Color.Red), G: uint8(bulletParticle.Color.Green), B: uint8(bulletParticle.Color.Blue), A: uint8(bulletParticle.Color.Alpha), } existingBulletParticle := bulletParticles[bulletParticleID] if existingBulletParticle == nil { bulletParticles[bulletParticleID] = &BulletParticle{ posX: bulletParticle.Position.PosX, posY: bulletParticle.Position.PosY, expirationTimer: bulletParticle.ExpirationTimer, color: inColor, id: bulletParticleID, } } else { existingBulletParticle.color = inColor existingBulletParticle.posX = bulletParticle.Position.PosX existingBulletParticle.posY = bulletParticle.Position.PosY existingBulletParticle.expirationTimer = bulletParticle.ExpirationTimer } } for _, tileUpdate := range worldUpdate.TileUpdate { posX := uint32(tileUpdate.Position.PosX) posY := uint32(tileUpdate.Position.PosY) kind := uint8(tileUpdate.Kind) if posX > 0 && posX < gameMap.width && posY > 0 && posY < gameMap.height { gameMap.tiles[posX][posY] = kind } } worldLock.Unlock() } } } }