1565 lines
47 KiB
Go
1565 lines
47 KiB
Go
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()
|
|
}
|
|
}
|
|
}
|
|
}
|