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 playerUpdateMutex sync.Mutex var lastPlayerID = uint32(0) var playersMutex sync.RWMutex type Player struct { local bool connection *net.TCPConn 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 reloadWait 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) { 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 int32, gameMap *GameMap, isShooting bool) uint8 { collisionAtDigSite := gameMap.checkCollision(posX, 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(uint32(posX), uint32(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) { 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(x, 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 if config.Client { player.sendPositionToServer() } } // 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) } 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-- } 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 getUnusedColor(colors []PlayerColors, players map[uint32]*Player) uint32 { for i, c := range colors { foundPlayerWithColor := false var colorCompare []uint8 clrR, clrG, clrB, clrA := c.tracks.RGBA() colorCompare = append(colorCompare, uint8(clrR), uint8(clrG), uint8(clrB), uint8(clrA)) clrR, clrG, clrB, clrA = c.body.RGBA() colorCompare = append(colorCompare, uint8(clrR), uint8(clrG), uint8(clrB), uint8(clrA)) clrR, clrG, clrB, clrA = c.cannon.RGBA() colorCompare = append(colorCompare, uint8(clrR), uint8(clrG), uint8(clrB), uint8(clrA)) playersMutex.RLock() for _, player := range players { var playerColorsCompare []uint8 plrR, plrG, plrB, plrA := player.playerColors.tracks.RGBA() playerColorsCompare = append(playerColorsCompare, uint8(plrR), uint8(plrG), uint8(plrB), uint8(plrA)) plrR, plrG, plrB, plrA = player.playerColors.body.RGBA() playerColorsCompare = append(playerColorsCompare, uint8(plrR), uint8(plrG), uint8(plrB), uint8(plrA)) plrR, plrG, plrB, plrA = player.playerColors.cannon.RGBA() playerColorsCompare = append(playerColorsCompare, uint8(plrR), uint8(plrG), uint8(plrB), uint8(plrA)) foundPlayerWithColor = false if len(playerColorsCompare) == len(colorCompare) { for i, c := range colorCompare { if playerColorsCompare[i] == c { foundPlayerWithColor = true } else { foundPlayerWithColor = false break } } if foundPlayerWithColor { break } } } playersMutex.RUnlock() if !foundPlayerWithColor { return uint32(i) } } return uint32(len(colors) - 1) } func createPlayers(amount uint8, playerColors []PlayerColors, keyMaps []KeyMap, joyMaps []JoyMap, gameMap *GameMap, players map[uint32]*Player, bases map[uint32]*Base) { 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[getUnusedColor(playerColors, players)], nil, keyMap, joyMap, joyStick, gameMap, players, bases, ) } } func closeThings(players map[uint32]*Player) { playersMutex.Lock() 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() } } playersMutex.Unlock() } func createPlayer(local bool, thisPlayerColors PlayerColors, conn *net.TCPConn, keyMap KeyMap, joyMap JoyMap, joyStick *sdl.Joystick, gameMap *GameMap, players map[uint32]*Player, bases map[uint32]*Base) uint32 { coordsAreValid := false var posX, posY uint32 maxTries := 1000 baseSize := uint32(36) // Since the base is 36x36 minDistance := uint32(200) // Minimum distance between bases maxDistance := uint32(500) // Maximum distance between bases for !coordsAreValid && maxTries >= 0 { maxTries-- posX = uint32(16 + rand.Intn(int(gameMap.width-baseSize-16))) posY = uint32(16 + rand.Intn(int(gameMap.height-baseSize-16))) coordsAreValid = true baseMutex.RLock() for _, base := range bases { basePosX := uint32(base.gameObject.baseRect.X) basePosY := uint32(base.gameObject.baseRect.Y) // Calculate the distance between the edges of the bases distanceX := max(0, max(basePosX-posX-baseSize, posX-basePosX-baseSize)) distanceY := max(0, max(basePosY-posY-baseSize, posY-basePosY-baseSize)) distanceSquared := distanceX*distanceX + distanceY*distanceY if distanceSquared < minDistance*minDistance && distanceSquared > maxDistance*maxDistance { coordsAreValid = false break } } baseMutex.RUnlock() // Edge clamping to ensure the base is within the map boundaries if posX < 16 || posX > gameMap.width-baseSize-16 || posY < 16 || posY > gameMap.height-baseSize-16 { coordsAreValid = false break } } if maxTries < 0 { panic("Could not place all players, increase map size") } gameObject := &GameObject{} gameObject.baseRect = &sdl.Rect{ X: int32(posX), Y: int32(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(0, 0, 6, 1, 0) gameObject.addColoredRect(0, 4, 6, 1, 0) gameObject.addColoredRect(1, 1, 4, 3, 1) gameObject.addColoredRect(3, 2, 4, 1, 2) gameObject.orientation = 2 // Down gameObject.addColoredRect(0, 0, 1, 6, 0) gameObject.addColoredRect(4, 0, 1, 6, 0) gameObject.addColoredRect(1, 1, 3, 4, 1) gameObject.addColoredRect(2, 3, 1, 4, 2) gameObject.orientation = 3 // Left gameObject.addColoredRect(1, 0, 6, 1, 0) gameObject.addColoredRect(1, 4, 6, 1, 0) gameObject.addColoredRect(2, 1, 4, 3, 1) gameObject.addColoredRect(0, 2, 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 gameObject.adjustBaseRect() 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) } playersMutex.Lock() 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, } playersMutex.Unlock() lastPlayerID++ return lastPlayerID - 1 } func sendMessageRaw(data []byte, conn *net.TCPConn) (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) updateTile(x, y uint32) { player.gameMapUpdates = append(player.gameMapUpdates, TileUpdate{ PosX: x, PosY: y, Kind: gameMap.tiles[x][y], }) } func (player *Player) updateRemotePlayerMap(wgX *sync.WaitGroup) { defer wgX.Done() player.gameMapUpdates = nil 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.updateTile(uint32(x), uint32(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, 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 { 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) { 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) { player.knownBulletParticles[bulletParticle.id] = bulletParticle } } bulletParticleMutex.RUnlock() } func (player *Player) updateRemotePlayerBases(bases map[uint32]*Base, wgX *sync.WaitGroup) { defer wgX.Done() player.knownBases = make(map[uint32]*Base) 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)) { player.knownBases[base.ownerID] = base break } } } baseMutex.RUnlock() } func (player *Player) sendInfoToPlayer(wgX *sync.WaitGroup) bool { defer wgX.Done() 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) sendUpdatesToPlayer(players map[uint32]*Player, wgX *sync.WaitGroup) bool { defer wgX.Done() 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 { playersMutex.RLock() r, g, b, a := players[knownBase.ownerID].playerColors.body.RGBA() playersMutex.RUnlock() 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, OwnerID: knownBullet.ownerID, }) } 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) 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, ReloadWait: serverConfig.ReloadWait, 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) sendVersionBackToPlayer() bool { if player.connection != nil && config.Server { response := tunnelerProto.ClientBound{ ClientBoundMessage: &tunnelerProto.ClientBound_PlayerStartResponse{ PlayerStartResponse: &tunnelerProto.PlayerStartResponse{ Version: GameVersion, }, }, } out, err := proto.Marshal(&response) if err != nil { panic("Error serializing version") } 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 } 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.TCPConn) { if connection != nil && config.Client { response := tunnelerProto.ServerBound{ ServerBoundMessage: &tunnelerProto.ServerBound_PlayerStartRequest{ PlayerStartRequest: &tunnelerProto.PlayerStartRequest{ Version: GameVersion, }, }, } 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.TCPConn, 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 != GameVersion { log.Fatalf("Wrong client version, a client connected with %s to %s", clientVersion, GameVersion) return } newPlayerID := createPlayer(false, playerColors[getUnusedColor(playerColors, players)], conn, KeyMap{}, JoyMap{}, nil, gameMap, players, bases) defer delete(players, newPlayerID) playersMutex.RLock() player := players[newPlayerID] playersMutex.RUnlock() baseMutex.Lock() 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)) baseMutex.Unlock() 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) var wgX sync.WaitGroup wgX.Add(1) fail = player.sendInfoToPlayer(&wgX) 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 // Update player orientation player.gameObject.orientation = uint8(newPosition.Orientation) player.gameObject.adjustBaseRect() // Check if the new position is adjacent to the old position isAdjacentX := (newX > oldX && newX-oldX <= 1) || (newX < oldX && oldX-newX <= 1) || newX == oldX isAdjacentY := (newY > oldY && newY-oldY <= 1) || (newY < oldY && oldY-newY <= 1) || newY == oldY // Ensure the player is not moving while on cooldown and is moving to an adjacent position if isAdjacentX && isAdjacentY && player.movementCooldown == 0 { // Terrain collision check: Loop over all tiles in the new bounding box area canMove := true for x := newX; x < newX+player.gameObject.baseRect.W; x++ { for y := newY; y < newY+player.gameObject.baseRect.H; y++ { if gameMap.tiles[x][y] != 0 { // Assumes 0 is passable, any other value is impassable player.knownGameMap.tiles[x][y] = 0 canMove = false } } if !canMove { break } } // If all tiles in the area are passable, update the player's position if canMove { player.gameObject.baseRect = &sdl.Rect{ X: newX, Y: newY, W: player.gameObject.baseRect.W, H: player.gameObject.baseRect.H, } // Deduct energy if the player has moved if newX != oldX || newY != oldY { if player.energy > serverConfig.MovementCost { player.energy -= serverConfig.MovementCost player.movementCooldown = serverConfig.MovementCooldown } else { player.movementCooldown = serverConfig.MovementCooldownNoEnergy } } } else { // If any part of the new area is impassable, revert to the old position player.sendLocationToPlayer() } } else { // If the movement is invalid (not adjacent or on cooldown), revert to the old position player.sendLocationToPlayer() } 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 posX := position.PosX posY := position.PosY res := player.digBlock(posX, posY, gameMap, isShooting) if res == 1 { if player.energy > serverConfig.DiggingCost { player.energy -= serverConfig.DiggingCost } } case *tunnelerProto.PlayerAction_Shoot: isSuper := action.Shoot.Super player.shoot(isSuper, bullets) } } } } func handleConnectionClient(conn *net.TCPConn, 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 != GameVersion { log.Fatalf("Wrong server version, connected with %s to %s", GameVersion, serverVersion) return } else { log.Printf("Connected to %s, running version %s with version %s\n", config.Address, serverVersion, GameVersion) } 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, ReloadWait: serverInfo.ReloadWait, } gameMap.createGameMap(false) lastPlayerID = serverInfo.PlayerID createPlayer(true, playerColors[serverInfo.PlayerColorID], conn, keyMaps[0], JoyMap{}, nil, gameMap, players, bases) playersMutex.RLock() player = players[serverInfo.PlayerID] playersMutex.RUnlock() 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) player.gameObject.adjustBaseRect() } case *tunnelerProto.ClientBound_WorldUpdate: if clientInitialized { worldUpdate := msg.GetWorldUpdate() worldLock.Lock() for _, playerLoop := range worldUpdate.Players { playerID := playerLoop.PlayerID playersMutex.RLock() existingPlayer := players[playerID] playersMutex.RUnlock() if existingPlayer == nil { lastPlayerID = playerID createPlayer(false, playerColors[playerID%uint32(len(playerColors))], nil, KeyMap{}, JoyMap{}, nil, gameMap, players, bases) playersMutex.RLock() newPlayer := players[playerID] playersMutex.RUnlock() newPlayer.gameObject.orientation = uint8(playerLoop.Location.Orientation) newPlayer.gameObject.adjustBaseRect() 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.adjustBaseRect() 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 baseMutex.RLock() existingBase := bases[baseID] baseMutex.RUnlock() 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 { baseMutex.Lock() 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)), ) baseMutex.Unlock() } 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 { bulletMutex.Lock() bullets[bulletID] = &Bullet{ posX: bullet.Position.PosX, posY: bullet.Position.PosY, direction: uint8(bullet.Direction), color: inColor, super: bullet.Super, id: bulletID, ownerID: bullet.OwnerID, } bulletMutex.Unlock() } 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), } bulletParticleMutex.RLock() existingBulletParticle := bulletParticles[bulletParticleID] bulletParticleMutex.RUnlock() if existingBulletParticle == nil { bulletParticleMutex.Lock() bulletParticles[bulletParticleID] = &BulletParticle{ posX: bulletParticle.Position.PosX, posY: bulletParticle.Position.PosY, expirationTimer: bulletParticle.ExpirationTimer, color: inColor, id: bulletParticleID, } bulletParticleMutex.Unlock() } 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() } } } }