Update networking stack

This commit is contained in:
Bruno Rybársky 2024-09-01 20:13:53 +02:00
parent b3c789303b
commit 589fd61902
13 changed files with 1335 additions and 1235 deletions

@ -181,7 +181,9 @@ func (bullet *Bullet) tick(gameMap *GameMap,
} }
playersMutex.RUnlock() playersMutex.RUnlock()
if collisionResult != 0 || hitPlayer { if collisionResult != 0 || hitPlayer {
bullet.explode(gameMap, bulletParticleMap) if !config.Client {
bullet.explode(gameMap, bulletParticleMap)
}
delete(bulletMap, bullet.id) delete(bulletMap, bullet.id)
} else { } else {
bullet.posX = nextX bullet.posX = nextX

465
clientboundpacket.go Normal file

@ -0,0 +1,465 @@
package main
import (
"encoding/binary"
"fmt"
"image/color"
"log"
"net"
"os"
)
func sendPlayerStartResponse(major, minor, patch uint8, conn *net.TCPConn) error {
data := make([]byte, 3)
data[0] = major
data[1] = minor
data[2] = patch
return sendPacket(0, data, conn)
}
func receivePlayerStartResponse(data []byte) (major, minor, patch uint8, err error) {
if len(data) < 3 {
return 0, 0, 0, fmt.Errorf("insufficient data length, got %d instead of 3", len(data))
}
major = data[0]
minor = data[1]
patch = data[2]
return major, minor, patch, nil
}
func sendServerInfo(
playerID, playerColorID, maxEnergy, maxAmmunition, maxShields, mapWidth, mapHeight, normalShotCost,
superShotCost, reloadCost, movementCost, diggingCost, shootDiggingBonus, shootCooldown, rechargeCooldown,
rechargeOpponentCooldown, repairCooldown, diggingCooldown, movementCooldown, movementCooldownNoEnergy,
diggingCooldownNoEnergy, reloadCooldown uint32, blastRadius uint8, reloadWait uint32, conn *net.TCPConn) error {
data := make([]byte, 93) // Adjusted size to accommodate playerID and playerColorID
binary.LittleEndian.PutUint32(data[0:4], playerID)
binary.LittleEndian.PutUint32(data[4:8], playerColorID)
binary.LittleEndian.PutUint32(data[8:12], maxEnergy)
binary.LittleEndian.PutUint32(data[12:16], maxAmmunition)
binary.LittleEndian.PutUint32(data[16:20], maxShields)
binary.LittleEndian.PutUint32(data[20:24], mapWidth)
binary.LittleEndian.PutUint32(data[24:28], mapHeight)
binary.LittleEndian.PutUint32(data[28:32], normalShotCost)
binary.LittleEndian.PutUint32(data[32:36], superShotCost)
binary.LittleEndian.PutUint32(data[36:40], reloadCost)
binary.LittleEndian.PutUint32(data[40:44], movementCost)
binary.LittleEndian.PutUint32(data[44:48], diggingCost)
binary.LittleEndian.PutUint32(data[48:52], shootDiggingBonus)
binary.LittleEndian.PutUint32(data[52:56], shootCooldown)
binary.LittleEndian.PutUint32(data[56:60], rechargeCooldown)
binary.LittleEndian.PutUint32(data[60:64], rechargeOpponentCooldown)
binary.LittleEndian.PutUint32(data[64:68], repairCooldown)
binary.LittleEndian.PutUint32(data[68:72], diggingCooldown)
binary.LittleEndian.PutUint32(data[72:76], movementCooldown)
binary.LittleEndian.PutUint32(data[76:80], movementCooldownNoEnergy)
binary.LittleEndian.PutUint32(data[80:84], diggingCooldownNoEnergy)
binary.LittleEndian.PutUint32(data[84:88], reloadCooldown)
data[88] = blastRadius
binary.LittleEndian.PutUint32(data[89:93], reloadWait)
return sendPacket(5, data, conn)
}
func receiveServerInfo(data []byte) (
playerID, playerColorID, maxEnergy, maxAmmunition, maxShields, mapWidth, mapHeight, normalShotCost, superShotCost,
reloadCost, movementCost, diggingCost, shootDiggingBonus, shootCooldown, rechargeCooldown, rechargeOpponentCooldown,
repairCooldown, diggingCooldown, movementCooldown, movementCooldownNoEnergy, diggingCooldownNoEnergy, reloadCooldown uint32,
blastRadius uint8, reloadWait uint32, err error) {
if len(data) < 93 {
return 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, fmt.Errorf("insufficient data length, got %d instead of 93", len(data))
}
playerID = binary.LittleEndian.Uint32(data[0:4])
playerColorID = binary.LittleEndian.Uint32(data[4:8])
maxEnergy = binary.LittleEndian.Uint32(data[8:12])
maxAmmunition = binary.LittleEndian.Uint32(data[12:16])
maxShields = binary.LittleEndian.Uint32(data[16:20])
mapWidth = binary.LittleEndian.Uint32(data[20:24])
mapHeight = binary.LittleEndian.Uint32(data[24:28])
normalShotCost = binary.LittleEndian.Uint32(data[28:32])
superShotCost = binary.LittleEndian.Uint32(data[32:36])
reloadCost = binary.LittleEndian.Uint32(data[36:40])
movementCost = binary.LittleEndian.Uint32(data[40:44])
diggingCost = binary.LittleEndian.Uint32(data[44:48])
shootDiggingBonus = binary.LittleEndian.Uint32(data[48:52])
shootCooldown = binary.LittleEndian.Uint32(data[52:56])
rechargeCooldown = binary.LittleEndian.Uint32(data[56:60])
rechargeOpponentCooldown = binary.LittleEndian.Uint32(data[60:64])
repairCooldown = binary.LittleEndian.Uint32(data[64:68])
diggingCooldown = binary.LittleEndian.Uint32(data[68:72])
movementCooldown = binary.LittleEndian.Uint32(data[72:76])
movementCooldownNoEnergy = binary.LittleEndian.Uint32(data[76:80])
diggingCooldownNoEnergy = binary.LittleEndian.Uint32(data[80:84])
reloadCooldown = binary.LittleEndian.Uint32(data[84:88])
blastRadius = data[88]
reloadWait = binary.LittleEndian.Uint32(data[89:93])
return playerID, playerColorID, maxEnergy, maxAmmunition, maxShields, mapWidth, mapHeight, normalShotCost, superShotCost,
reloadCost, movementCost, diggingCost, shootDiggingBonus, shootCooldown, rechargeCooldown, rechargeOpponentCooldown,
repairCooldown, diggingCooldown, movementCooldown, movementCooldownNoEnergy, diggingCooldownNoEnergy, reloadCooldown,
blastRadius, reloadWait, nil
}
func sendPositionUpdate(posX, posY uint32, orientation uint8, conn *net.TCPConn) error {
data := make([]byte, 9)
binary.LittleEndian.PutUint32(data[0:4], posX)
binary.LittleEndian.PutUint32(data[4:8], posY)
data[8] = orientation
return sendPacket(1, data, conn)
}
func receivePositionUpdate(data []byte) (posX, posY uint32, orientation uint8, err error) {
if len(data) < 9 {
return 0, 0, 0, fmt.Errorf("insufficient data length, got %d instead of 9", len(data))
}
posX = binary.LittleEndian.Uint32(data[0:4])
posY = binary.LittleEndian.Uint32(data[4:8])
orientation = data[8]
return posX, posY, orientation, nil
}
func sendOtherPlayer(playerID, posX, posY uint32, orientation uint8, conn *net.TCPConn) error {
data := make([]byte, 13)
binary.LittleEndian.PutUint32(data[0:4], playerID)
binary.LittleEndian.PutUint32(data[4:8], posX)
binary.LittleEndian.PutUint32(data[8:12], posY)
data[12] = orientation
return sendPacket(2, data, conn)
}
func receiveOtherPlayer(data []byte) (playerID, posX, posY uint32, orientation uint8, err error) {
if len(data) < 13 {
return 0, 0, 0, 0, fmt.Errorf("insufficient data length, got %d instead of 13", len(data))
}
playerID = binary.LittleEndian.Uint32(data[0:4])
posX = binary.LittleEndian.Uint32(data[4:8])
posY = binary.LittleEndian.Uint32(data[8:12])
orientation = data[12]
return playerID, posX, posY, orientation, nil
}
func sendBullet(posX, posY uint32, direction, red, green, blue, alpha uint8, bulletID uint32, isSuper bool, ownerID uint32, conn *net.TCPConn) error {
data := make([]byte, 22)
binary.LittleEndian.PutUint32(data[0:4], posX)
binary.LittleEndian.PutUint32(data[4:8], posY)
data[8] = direction
data[9] = red
data[10] = green
data[11] = blue
data[12] = alpha
binary.LittleEndian.PutUint32(data[13:17], bulletID)
data[17] = boolToByte(isSuper)
binary.LittleEndian.PutUint32(data[18:22], ownerID)
return sendPacket(3, data, conn)
}
func receiveBullet(data []byte) (posX, posY uint32, direction, red, green, blue, alpha uint8, bulletID uint32, isSuper bool, ownerID uint32, err error) {
if len(data) < 22 {
return 0, 0, 0, 0, 0, 0, 0, 0, false, 0, fmt.Errorf("insufficient data length, got %d instead of 21", len(data))
}
posX = binary.LittleEndian.Uint32(data[0:4])
posY = binary.LittleEndian.Uint32(data[4:8])
direction = data[8]
red = data[9]
green = data[10]
blue = data[11]
alpha = data[12]
bulletID = binary.LittleEndian.Uint32(data[13:17])
isSuper = byteToBool(data[17])
ownerID = binary.LittleEndian.Uint32(data[18:22])
return posX, posY, direction, red, green, blue, alpha, bulletID, isSuper, ownerID, nil
}
func sendBulletParticle(posX, posY, expirationTimer uint32, red, green, blue, alpha uint8, bulletParticleID uint32, conn *net.TCPConn) error {
data := make([]byte, 20)
binary.LittleEndian.PutUint32(data[0:4], posX)
binary.LittleEndian.PutUint32(data[4:8], posY)
binary.LittleEndian.PutUint32(data[8:12], expirationTimer)
data[12] = red
data[13] = green
data[14] = blue
data[15] = alpha
binary.LittleEndian.PutUint32(data[16:20], bulletParticleID)
return sendPacket(4, data, conn)
}
func receiveBulletParticle(data []byte) (posX, posY, expirationTimer uint32, red, green, blue, alpha uint8, bulletParticleID uint32, err error) {
if len(data) < 20 {
return 0, 0, 0, 0, 0, 0, 0, 0, fmt.Errorf("insufficient data length, got %d instead of 20", len(data))
}
posX = binary.LittleEndian.Uint32(data[0:4])
posY = binary.LittleEndian.Uint32(data[4:8])
expirationTimer = binary.LittleEndian.Uint32(data[8:12])
red = data[12]
green = data[13]
blue = data[14]
alpha = data[15]
bulletParticleID = binary.LittleEndian.Uint32(data[16:20])
return posX, posY, expirationTimer, red, green, blue, alpha, bulletParticleID, nil
}
func sendPlayerUpdate(energy, ammo, shields uint32, conn *net.TCPConn) error {
data := make([]byte, 12)
binary.LittleEndian.PutUint32(data[0:4], energy)
binary.LittleEndian.PutUint32(data[4:8], ammo)
binary.LittleEndian.PutUint32(data[8:12], shields)
return sendPacket(6, data, conn)
}
func receivePlayerUpdate(data []byte) (energy, ammo, shields uint32, err error) {
if len(data) < 12 {
return 0, 0, 0, fmt.Errorf("insufficient data length, got %d instead of 12", len(data))
}
energy = binary.LittleEndian.Uint32(data[0:4])
ammo = binary.LittleEndian.Uint32(data[4:8])
shields = binary.LittleEndian.Uint32(data[8:12])
return energy, ammo, shields, nil
}
func sendBaseLocation(posX, posY uint32, ownerID, playerColorID uint32, conn *net.TCPConn) error {
data := make([]byte, 16)
binary.LittleEndian.PutUint32(data[0:4], posX)
binary.LittleEndian.PutUint32(data[4:8], posY)
binary.LittleEndian.PutUint32(data[8:12], ownerID)
binary.LittleEndian.PutUint32(data[12:16], playerColorID)
return sendPacket(7, data, conn)
}
func receiveBaseLocation(data []byte) (posX, posY uint32, ownerID, playerColorID uint32, err error) {
if len(data) < 16 {
return 0, 0, 0, 0, fmt.Errorf("insufficient data length, got %d instead of 16", len(data))
}
posX = binary.LittleEndian.Uint32(data[0:4])
posY = binary.LittleEndian.Uint32(data[4:8])
ownerID = binary.LittleEndian.Uint32(data[8:12])
playerColorID = binary.LittleEndian.Uint32(data[12:16])
return posX, posY, ownerID, playerColorID, nil
}
func sendTileUpdate(posX, posY uint32, kind uint8, conn *net.TCPConn) error {
data := make([]byte, 9)
binary.LittleEndian.PutUint32(data[0:4], posX)
binary.LittleEndian.PutUint32(data[4:8], posY)
data[8] = kind
return sendPacket(8, data, conn)
}
func receiveTileUpdate(data []byte) (posX, posY uint32, kind uint8, err error) {
if len(data) < 9 {
return 0, 0, 0, fmt.Errorf("insufficient data length, got %d instead of 9", len(data))
}
posX = binary.LittleEndian.Uint32(data[0:4])
posY = binary.LittleEndian.Uint32(data[4:8])
kind = data[8]
return posX, posY, kind, nil
}
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)
for {
packetID, _, packetData, err := receivePacket(conn)
if err != nil {
log.Printf("Error receiving packet: %v", err)
os.Exit(0)
return
}
switch packetID {
case 0:
major, minor, patch, err := receivePlayerStartResponse(packetData)
if err != nil {
log.Printf("Error receiving player start response: %v", err)
}
versionArray := getCurrentVersion()
if versionArray[0] != major || versionArray[1] != minor || versionArray[2] != patch {
log.Fatalf("Wrong version tried to connect with %d.%d.%d to %d.%d.%d", major, minor, patch, versionArray[0], versionArray[1], versionArray[2])
}
case 1:
if clientInitialized {
posX, posY, orientation, err := receivePositionUpdate(packetData)
if err != nil {
log.Printf("Error receiving player location: %v", err)
}
player.gameObject.baseRect.X = int32(posX)
player.gameObject.baseRect.Y = int32(posY)
player.gameObject.orientation = orientation
player.gameObject.adjustBaseRect()
}
case 2:
playerID, posX, posY, orientation, err := receiveOtherPlayer(packetData)
if err != nil {
log.Printf("Error receiving player response: %v", err)
}
playersMutex.RLock()
existingPlayer := players[playerID]
playersMutex.RUnlock()
if existingPlayer == nil {
lastPlayerID = playerID
playerColor := playerID % uint32(len(playerColors))
createPlayer(false, playerColors[playerColor], playerColor, nil, KeyMap{}, JoyMap{}, nil, gameMap, players, bases)
playersMutex.RLock()
newPlayer := players[playerID]
playersMutex.RUnlock()
newPlayer.gameObject.orientation = orientation
newPlayer.gameObject.adjustBaseRect()
newPlayer.gameObject.baseRect.X = int32(posX)
newPlayer.gameObject.baseRect.Y = int32(posY)
} else {
existingPlayer.gameObject.orientation = orientation
existingPlayer.gameObject.adjustBaseRect()
existingPlayer.gameObject.baseRect.X = int32(posX)
existingPlayer.gameObject.baseRect.Y = int32(posY)
}
case 3:
posX, posY, direction, red, green, blue, alpha, bulletID, isSuper, ownerID, err := receiveBullet(packetData)
if err != nil {
log.Printf("Error receiving bullet response: %v", err)
}
inColor := color.RGBA{
R: red,
G: green,
B: blue,
A: alpha,
}
existingBullet := bullets[bulletID]
if existingBullet == nil {
bulletMutex.Lock()
bullets[bulletID] = &Bullet{
posX: int32(posX),
posY: int32(posY),
direction: direction,
color: inColor,
super: isSuper,
id: bulletID,
ownerID: ownerID,
}
bulletMutex.Unlock()
} else {
existingBullet.direction = direction
existingBullet.color = inColor
existingBullet.super = isSuper
existingBullet.posX = int32(posX)
existingBullet.posY = int32(posY)
}
case 4:
posX, posY, expirationTimer, red, green, blue, alpha, bulletParticleID, err := receiveBulletParticle(packetData)
if err != nil {
log.Printf("Error receiving bullet particle: %v", err)
}
inColor := color.RGBA{
R: red,
G: green,
B: blue,
A: alpha,
}
bulletParticleMutex.Lock()
existingBulletParticle := bulletParticles[bulletParticleID]
if existingBulletParticle == nil {
bulletParticles[bulletParticleID] = &BulletParticle{
posX: int32(posX),
posY: int32(posY),
expirationTimer: expirationTimer,
color: inColor,
id: bulletParticleID,
}
} else {
existingBulletParticle.color = inColor
existingBulletParticle.posX = int32(posX)
existingBulletParticle.posY = int32(posY)
existingBulletParticle.expirationTimer = expirationTimer
}
bulletParticleMutex.Unlock()
case 5:
playerID, playerColorID, maxEnergy, maxAmmunition, maxShields, mapWidth, mapHeight, normalShotCost, superShotCost, reloadCost, movementCost,
diggingCost, shootDiggingBonus, shootCooldown, rechargeCooldown, rechargeOpponentCooldown, repairCooldown, diggingCooldown, movementCooldown,
movementCooldownNoEnergy, diggingCooldownNoEnergy, reloadCooldown, blastRadius, reloadWait, err := receiveServerInfo(packetData)
if err != nil {
log.Printf("Error receiving server info: %v", err)
}
serverConfig = ServerConfig{
MapWidth: mapWidth,
MapHeight: mapHeight,
BlastRadius: blastRadius,
MaxEnergy: maxEnergy,
MaxAmmunition: maxAmmunition,
MaxShields: maxShields,
NormalShotCost: normalShotCost,
SuperShotCost: superShotCost,
ReloadCost: reloadCost,
MovementCost: movementCost,
DiggingCost: diggingCost,
ShootDiggingCostBonus: shootDiggingBonus,
ShootCooldown: shootCooldown,
RechargeCooldownOwn: rechargeCooldown,
DiggingCooldown: diggingCooldown,
RechargeCooldownOpponent: rechargeOpponentCooldown,
RepairCooldown: repairCooldown,
MovementCooldown: movementCooldown,
MovementCooldownNoEnergy: movementCooldownNoEnergy,
DiggingCooldownNoEnergy: diggingCooldownNoEnergy,
ReloadCooldown: reloadCooldown,
ReloadWait: reloadWait,
}
gameMap.createGameMap(false)
lastPlayerID = playerID
createPlayer(true, playerColors[playerColorID], playerColorID, conn, keyMaps[config.KeyBindOffset], JoyMap{}, nil, gameMap, players, bases)
playersMutex.RLock()
player = players[playerID]
playersMutex.RUnlock()
log.Printf("Got server info, now initializing\n")
clientInitialized = true
case 6:
if clientInitialized {
energy, ammo, shields, err := receivePlayerUpdate(packetData)
if err != nil {
log.Printf("Error receiving player update: %v", err)
}
player.energy = energy
player.ammunition = ammo
player.shields = shields
}
case 7:
posX, posY, ownerID, playerColorID, err := receiveBaseLocation(packetData)
if err != nil {
log.Printf("Error receiving base location: %v", err)
}
baseID := ownerID
baseMutex.RLock()
existingBase := bases[baseID]
baseMutex.RUnlock()
inColor := playerColors[playerColorID].body
if existingBase == nil {
baseMutex.Lock()
bases[baseID] = createBase(gameMap,
inColor,
posX,
posY,
ownerID,
uint32(float64(player.gameObject.baseRect.W)*1.5),
)
baseMutex.Unlock()
} else {
existingBase.gameObject.baseRect.X = int32(posX)
existingBase.gameObject.baseRect.Y = int32(posY)
existingBase.gameObject.colors[0] = inColor
}
case 8:
posX, posY, kind, err := receiveTileUpdate(packetData)
if err != nil {
log.Printf("Error receiving tile update: %v", err)
}
if posX > 0 && posX < gameMap.width && posY > 0 && posY < gameMap.height {
gameMap.tiles[posX][posY] = kind
}
}
}
}

@ -76,16 +76,16 @@ func (gameObject *GameObject) render(camera *sdl.Rect, surface *sdl.Surface) {
gameObject.inView = true gameObject.inView = true
if config.Debug { if config.Debug {
baseRectFinal := adjustRectToCamera(gameObject.baseRect, camera) baseRectFinal := adjustRectToCamera(gameObject.baseRect, camera)
surface.FillRect(baseRectFinal, sdl.MapRGBA(surface.Format, 255, 20, 10, 64)) _ = surface.FillRect(baseRectFinal, sdl.MapRGBA(surface.Format, 255, 20, 10, 64))
if !gameObject.collisionRect.Empty() { if !gameObject.collisionRect.Empty() {
surface.FillRect(adjustRectToCamera(gameObject.collisionRect, camera), sdl.MapRGBA(surface.Format, 40, 192, 255, 64)) _ = surface.FillRect(adjustRectToCamera(gameObject.collisionRect, camera), sdl.MapRGBA(surface.Format, 40, 192, 255, 64))
} }
} }
if config.RenderGameObjects { if config.RenderGameObjects {
for _, coloredRect := range gameObject.visualRects[gameObject.orientation] { for _, coloredRect := range gameObject.visualRects[gameObject.orientation] {
finalRect := gameObject.adjustRectToCamera(coloredRect.rect, camera) finalRect := gameObject.adjustRectToCamera(coloredRect.rect, camera)
r, g, b, a := coloredRect.color.RGBA() r, g, b, a := coloredRect.color.RGBA()
surface.FillRect(finalRect, sdl.MapRGBA(surface.Format, uint8(r), uint8(g), uint8(b), uint8(a))) _ = surface.FillRect(finalRect, sdl.MapRGBA(surface.Format, uint8(r), uint8(g), uint8(b), uint8(a)))
} }
} }
} else { } else {

1
go.mod

@ -5,5 +5,4 @@ go 1.21
require ( require (
github.com/aquilax/go-perlin v1.1.0 github.com/aquilax/go-perlin v1.1.0
github.com/veandco/go-sdl2 v0.4.40 github.com/veandco/go-sdl2 v0.4.40
google.golang.org/protobuf v1.34.2
) )

@ -99,8 +99,8 @@ func handleWindowResize(window *sdl.Window, logicalSurface *sdl.Surface) {
srcRect := &sdl.Rect{X: 0, Y: 0, W: logicalSurface.W, H: logicalSurface.H} srcRect := &sdl.Rect{X: 0, Y: 0, W: logicalSurface.W, H: logicalSurface.H}
dstRect := &sdl.Rect{X: letterboxX, Y: letterboxY, W: newWidth, H: newHeight} dstRect := &sdl.Rect{X: letterboxX, Y: letterboxY, W: newWidth, H: newHeight}
logicalSurface.BlitScaled(srcRect, windowSurface, dstRect) _ = logicalSurface.BlitScaled(srcRect, windowSurface, dstRect)
window.UpdateSurface() _ = window.UpdateSurface()
} }
func adjustWindow(window *sdl.Window, logicalSurface *sdl.Surface) { func adjustWindow(window *sdl.Window, logicalSurface *sdl.Surface) {
@ -133,15 +133,15 @@ func adjustWindow(window *sdl.Window, logicalSurface *sdl.Surface) {
letterboxY := (windowHeight - newHeight) / 2 letterboxY := (windowHeight - newHeight) / 2
// Fill background // Fill background
windowSurface.FillRect(nil, sdl.MapRGBA(logicalSurface.Format, 80, 20, 10, 255)) _ = windowSurface.FillRect(nil, sdl.MapRGBA(logicalSurface.Format, 80, 20, 10, 255))
// Set source and destination rectangles // Set source and destination rectangles
srcRect := &sdl.Rect{X: 0, Y: 0, W: logicalSurface.W, H: logicalSurface.H} srcRect := &sdl.Rect{X: 0, Y: 0, W: logicalSurface.W, H: logicalSurface.H}
dstRect := &sdl.Rect{X: letterboxX, Y: letterboxY, W: newWidth, H: newHeight} dstRect := &sdl.Rect{X: letterboxX, Y: letterboxY, W: newWidth, H: newHeight}
// Perform the scaled blit // Perform the scaled blit
logicalSurface.BlitScaled(srcRect, windowSurface, dstRect) _ = logicalSurface.BlitScaled(srcRect, windowSurface, dstRect)
// Update the window surface // Update the window surface
window.UpdateSurface() _ = window.UpdateSurface()
} }

8
hud.go

@ -48,13 +48,13 @@ func initHud(surface *sdl.Surface) {
sdl.MapRGBA(surface.Format, 40, 243, 243, 255), sdl.MapRGBA(surface.Format, 40, 243, 243, 255),
} }
HUDColor := sdl.MapRGBA(surface.Format, 101, 101, 101, 255) HUDColor := sdl.MapRGBA(surface.Format, 101, 101, 101, 255)
surface.FillRect(nil, HUDColor) _ = surface.FillRect(nil, HUDColor)
for letterIndex, letter := range rects { for letterIndex, letter := range rects {
for _, letterRect := range letter { for _, letterRect := range letter {
offsetRect := letterRect offsetRect := letterRect
offsetRect.Y += letterOffsets[letterIndex] offsetRect.Y += letterOffsets[letterIndex]
surface.FillRect(&offsetRect, letterColors[letterIndex]) _ = surface.FillRect(&offsetRect, letterColors[letterIndex])
} }
} }
} }
@ -62,7 +62,7 @@ func initHud(surface *sdl.Surface) {
func renderHud(player *Player, surface *sdl.Surface) { func renderHud(player *Player, surface *sdl.Surface) {
for _, letterOffset := range letterOffsets { for _, letterOffset := range letterOffsets {
surface.FillRect(&sdl.Rect{X: 16, Y: letterOffset - 1, W: 90, H: 7}, 0) _ = surface.FillRect(&sdl.Rect{X: 16, Y: letterOffset - 1, W: 90, H: 7}, 0)
} }
HUDValues := []int32{ HUDValues := []int32{
@ -72,7 +72,7 @@ func renderHud(player *Player, surface *sdl.Surface) {
} }
for HUDValueIndex, HUDValue := range HUDValues { for HUDValueIndex, HUDValue := range HUDValues {
surface.FillRect( _ = surface.FillRect(
&sdl.Rect{ &sdl.Rect{
X: 17, X: 17,
Y: letterOffsets[HUDValueIndex], Y: letterOffsets[HUDValueIndex],

74
main.go

@ -2,23 +2,22 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"github.com/veandco/go-sdl2/sdl" "github.com/veandco/go-sdl2/sdl"
"io/ioutil"
"log" "log"
"net" "net"
"os" "os"
"runtime/pprof"
"sync" "sync"
"time" "time"
) )
const GameVersion = "TunnEElineningnegbfbf Through the wiAldWest" const GameVersion = "0.0.1"
type ServerConfig struct { type ServerConfig struct {
MapWidth uint32 `json:"map_width"` MapWidth uint32 `json:"map_width"`
MapHeight uint32 `json:"map_height"` MapHeight uint32 `json:"map_height"`
BlastRadius uint32 `json:"blast_radius"` BlastRadius uint8 `json:"blast_radius"`
MaxEnergy uint32 `json:"max_energy"` MaxEnergy uint32 `json:"max_energy"`
MaxAmmunition uint32 `json:"max_ammunition"` MaxAmmunition uint32 `json:"max_ammunition"`
@ -120,14 +119,14 @@ func loadOrCreateConfig(filename string) (*Config, error) {
return nil, fmt.Errorf("failed to marshal config: %v", err) return nil, fmt.Errorf("failed to marshal config: %v", err)
} }
err = ioutil.WriteFile(filename, data, 0644) err = os.WriteFile(filename, data, 0644)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to write config file: %v", err) return nil, fmt.Errorf("failed to write config file: %v", err)
} }
fmt.Println("Config file created with default values.") fmt.Println("Config file created with default values.")
} else { } else {
// File exists, load the config // File exists, load the config
data, err := ioutil.ReadFile(filename) data, err := os.ReadFile(filename)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to read config file: %v", err) return nil, fmt.Errorf("failed to read config file: %v", err)
} }
@ -172,17 +171,6 @@ func main() {
initializeSDL() initializeSDL()
defer sdl.Quit() defer sdl.Quit()
} }
if config.ProfilerOn {
f, err := os.Create("cpuprofile")
if err != nil {
log.Fatal("could not create CPU profile: ", err)
}
defer f.Close() // error handling omitted for example
if err := pprof.StartCPUProfile(f); err != nil {
log.Fatal("could not start CPU profile: ", err)
}
defer pprof.StopCPUProfile()
}
mapRendererRect = &sdl.Rect{X: 0, Y: 0, W: int32(serverConfig.MapWidth), H: int32(serverConfig.MapHeight)} mapRendererRect = &sdl.Rect{X: 0, Y: 0, W: int32(serverConfig.MapWidth), H: int32(serverConfig.MapHeight)}
players := make(map[uint32]*Player) players := make(map[uint32]*Player)
initPlayerColors() initPlayerColors()
@ -210,7 +198,9 @@ func main() {
if err != nil { if err != nil {
log.Fatalf("Failed to connect to server: %v", err) log.Fatalf("Failed to connect to server: %v", err)
} }
defer conn.Close() defer func(conn *net.TCPConn) {
_ = conn.Close()
}(conn)
go handleConnectionClient(conn, players, bases, bullets, bulletParticles) go handleConnectionClient(conn, players, bases, bullets, bulletParticles)
for !clientInitialized { for !clientInitialized {
@ -238,12 +228,15 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
// close listener // close listener
defer listen.Close() defer func(listen *net.TCPListener) {
_ = listen.Close()
}(listen)
go func() { go func() {
for { for {
conn, err := listen.AcceptTCP() conn, err := listen.AcceptTCP()
if err != nil { if err != nil {
if opErr, ok := err.(*net.OpError); ok && opErr.Err.Error() == "use of closed network connection" { var opErr *net.OpError
if errors.As(err, &opErr) && opErr.Err.Error() == "use of closed network connection" {
log.Println("Listener closed, stopping server.") log.Println("Listener closed, stopping server.")
return return
} }
@ -349,25 +342,24 @@ func runGameLogic(players map[uint32]*Player, gameMap *GameMap, bases map[uint32
//update remote player maps //update remote player maps
totalRemotePlayerUpdate += profileSection(func() { totalRemotePlayerUpdate += profileSection(func() {
var wgX sync.WaitGroup var wgX sync.WaitGroup
var wgX2 sync.WaitGroup
playerUpdateMutex.Lock() playerUpdateMutex.Lock()
playersMutex.RLock() playersMutex.RLock()
for _, player := range players { for _, player := range players {
if player.local { if player.local || !player.initialized {
continue continue
} }
wgX.Add(6) wgX.Add(6)
go player.updateRemotePlayerBases(bases, players, &wgX)
go player.updateRemotePlayerMap(&wgX) go player.updateRemotePlayerMap(&wgX)
go player.updateRemotePlayerBases(bases, &wgX)
go player.updateRemotePlayerBullets(bullets, &wgX) go player.updateRemotePlayerBullets(bullets, &wgX)
go player.updateRemotePlayerBulletParticles(bulletParticles, &wgX) go player.updateRemotePlayerBulletParticles(bulletParticles, &wgX)
go player.updateRemotePlayerPlayers(players, &wgX) go player.updateRemotePlayerPlayers(players, &wgX)
playerX := player playerX := player
go func() { go func() {
fail := playerX.sendInfoToPlayer(&wgX) success := playerX.sendPlayerUpdate(&wgX)
if fail { if !success {
if playerX.connection != nil { if playerX.connection != nil {
(*playerX.connection).Close() _ = (*playerX.connection).Close()
} }
baseMutex.Lock() baseMutex.Lock()
if bases[playerX.playerID] != nil { if bases[playerX.playerID] != nil {
@ -382,28 +374,6 @@ func runGameLogic(players map[uint32]*Player, gameMap *GameMap, bases map[uint32
} }
playersMutex.RUnlock() playersMutex.RUnlock()
wgX.Wait() wgX.Wait()
playersMutex.Lock()
for _, player := range players {
wgX2.Add(1)
playerX := player
go func() {
fail := playerX.sendUpdatesToPlayer(players, &wgX2)
if fail {
if playerX.connection != nil {
(*playerX.connection).Close()
}
baseMutex.Lock()
if bases[playerX.playerID] != nil {
bases[playerX.playerID].delete(bases)
}
baseMutex.Unlock()
delete(players, playerX.playerID)
}
}()
}
playersMutex.Unlock()
wgX2.Wait()
playerUpdateMutex.Unlock() playerUpdateMutex.Unlock()
}) })
} }
@ -443,11 +413,11 @@ func initPlayer(playerIndex uint8, player *Player) {
} }
player.window, player.logicalSurface = setupWindowAndSurface(playerIndex) player.window, player.logicalSurface = setupWindowAndSurface(playerIndex)
logicalColor := sdl.MapRGBA(player.logicalSurface.Format, 101, 101, 0, 255) logicalColor := sdl.MapRGBA(player.logicalSurface.Format, 101, 101, 0, 255)
player.logicalSurface.FillRect(nil, logicalColor) _ = player.logicalSurface.FillRect(nil, logicalColor)
player.playSurface, player.playSurfaceRect, player.playSurfaceTargetRect = setupPlaySurface() player.playSurface, player.playSurfaceRect, player.playSurfaceTargetRect = setupPlaySurface()
playColor := sdl.MapRGBA(player.playSurface.Format, 101, 0, 101, 255) playColor := sdl.MapRGBA(player.playSurface.Format, 101, 0, 101, 255)
player.playSurface.FillRect(nil, playColor) _ = player.playSurface.FillRect(nil, playColor)
player.HUDSurface, player.HUDSurfaceRect, player.HUDSurfaceTargetRect = setupHUDSurface() player.HUDSurface, player.HUDSurfaceRect, player.HUDSurfaceTargetRect = setupHUDSurface()
initHud(player.HUDSurface) initHud(player.HUDSurface)
@ -498,8 +468,8 @@ func doPlayerFrame(playerIndex uint8, player *Player, players map[uint32]*Player
player.tick(bullets) player.tick(bullets)
player.gameObject.prevBaseRect = player.gameObject.baseRect player.gameObject.prevBaseRect = player.gameObject.baseRect
renderHud(player, player.HUDSurface) renderHud(player, player.HUDSurface)
player.playSurface.BlitScaled(player.playSurfaceRect, player.logicalSurface, player.playSurfaceTargetRect) _ = player.playSurface.BlitScaled(player.playSurfaceRect, player.logicalSurface, player.playSurfaceTargetRect)
player.HUDSurface.BlitScaled(player.HUDSurfaceRect, player.logicalSurface, player.HUDSurfaceTargetRect) _ = player.HUDSurface.BlitScaled(player.HUDSurfaceRect, player.logicalSurface, player.HUDSurfaceTargetRect)
}) })
// Profile window adjustments // Profile window adjustments

119
netCode.go Normal file

@ -0,0 +1,119 @@
package main
import (
"encoding/binary"
"errors"
"hash/crc32"
"net"
"strconv"
"strings"
)
// sendPacket sends a packet with the given packetID and data over the provided TCP connection.
func sendPacket(packetID uint8, data []byte, conn *net.TCPConn) error {
// Create a buffer with the packet ID and data
tempBuffer := append([]byte{packetID}, data...)
// Calculate the checksum of the packet ID + data
checksum := crc32.ChecksumIEEE(tempBuffer)
// Convert the checksum to a 4-byte slice
checksumBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(checksumBytes, checksum)
// Prepend the checksum to the beginning of the buffer
tempBuffer = append(checksumBytes, tempBuffer...)
// Calculate the total length (checksum + packet ID + data)
packetLength := uint32(len(tempBuffer))
// Prepare the output buffer with the length field
outBuffer := make([]byte, 4)
binary.LittleEndian.PutUint32(outBuffer, packetLength)
// Append the tempBuffer (checksum + packet ID + data) to outBuffer
outBuffer = append(outBuffer, tempBuffer...)
// Send the packet over the connection
writeLen, err := conn.Write(outBuffer)
if err != nil {
return err
}
if writeLen != len(outBuffer) {
return errors.New("write length does not match output buffer length")
}
return nil
}
// receivePacket receives a packet from the provided TCP connection and verifies it.
func receivePacket(conn *net.TCPConn) (uint8, uint32, []byte, error) {
// Step 1: Read the packet length (4 bytes)
lengthBuffer := make([]byte, 4)
n, err := conn.Read(lengthBuffer)
if err != nil {
return 0, 0, nil, err
}
if n != len(lengthBuffer) {
return 0, 0, nil, errors.New("read length does not match read length")
}
packetLength := binary.LittleEndian.Uint32(lengthBuffer)
// Step 2: Allocate a buffer to hold the rest of the packet (checksum + packetID + data)
packetBuffer := make([]byte, packetLength)
n, err = conn.Read(packetBuffer)
if err != nil {
return 0, 0, nil, err
}
if n != int(packetLength) {
return 0, 0, nil, errors.New("read length does not match packet length")
}
// Ensure we've read enough bytes
if uint32(len(packetBuffer)) != packetLength {
return 0, 0, nil, errors.New("incomplete packet received")
}
// Step 3: Extract the checksum (4 bytes)
receivedChecksum := binary.LittleEndian.Uint32(packetBuffer[:4])
// Step 4: Extract the packet ID (1 byte)
packetID := packetBuffer[4]
// Step 5: Extract the data
data := packetBuffer[5:]
// Step 6: Verify the checksum
calculatedChecksum := crc32.ChecksumIEEE(append([]byte{packetID}, data...))
if receivedChecksum != calculatedChecksum {
return 0, 0, nil, errors.New("checksum mismatch")
}
// Return the packet ID and data
return packetID, uint32(len(data)), data, nil
}
func boolToByte(b bool) byte {
if b {
return 1
}
return 0
}
func byteToBool(b byte) bool {
return b == 1
}
func getCurrentVersion() []uint8 {
versionArrayString := strings.Split(GameVersion, ".")
if len(versionArrayString) < 3 {
return []uint8{}
}
var versionArray []uint8
for i := 0; i < len(versionArrayString); i++ {
intVersion, _ := strconv.Atoi(versionArrayString[i])
versionArray = append(versionArray, uint8(intVersion))
}
return versionArray
}

1192
player.go

File diff suppressed because it is too large Load Diff

@ -379,7 +379,9 @@ func handleInput(keyboard []uint8, bullets map[uint32]*Bullet, player *Player, g
// Handle movement after the loop // Handle movement after the loop
if moveUp || moveDown || moveLeft || moveRight { if moveUp || moveDown || moveLeft || moveRight {
if player.tryMove(gameMap, shoot, players) { if player.tryMove(gameMap, shoot, players) {
player.energy -= serverConfig.MovementCost if player.energy > serverConfig.MovementCost {
player.energy -= serverConfig.MovementCost
}
} }
} }

316
playerutil.go Normal file

@ -0,0 +1,316 @@
package main
import (
"github.com/veandco/go-sdl2/sdl"
"math/rand"
"net"
)
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+int(config.KeyBindOffset))%(len(keyMaps)-1)]
addedKeyboardPlayers++
} else {
joyStickIndex := i - uint8(addedKeyboardPlayers)
joyMap = joyMaps[joyStickIndex]
joyStick = sdl.JoystickOpen(int(joyStickIndex))
}
unusedColor := getUnusedColor(playerColors, players)
createPlayer(
true,
playerColors[unusedColor],
unusedColor,
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, thisPlayerColorIndex uint32, 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,
playerColorID: thisPlayerColorIndex,
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
}

236
serverboundpacket.go Normal file

@ -0,0 +1,236 @@
package main
import (
"encoding/binary"
"fmt"
"github.com/veandco/go-sdl2/sdl"
"log"
"net"
"sync"
)
func sendPlayerStartRequest(major, minor, patch uint8, conn *net.TCPConn) error {
data := make([]byte, 3)
data[0] = major
data[1] = minor
data[2] = patch
return sendPacket(0, data, conn)
}
func receivePlayerStartRequest(data []byte) (major, minor, patch uint8, err error) {
if len(data) < 3 {
return 0, 0, 0, fmt.Errorf("insufficient data length, got %d instead of 3", len(data))
}
major = data[0]
minor = data[1]
patch = data[2]
return major, minor, patch, nil
}
func sendPlayerLocation(posX, posY uint32, orientation uint8, conn *net.TCPConn) error {
data := make([]byte, 9)
binary.LittleEndian.PutUint32(data[0:4], posX)
binary.LittleEndian.PutUint32(data[4:8], posY)
data[8] = orientation
return sendPacket(1, data, conn)
}
func receivePlayerLocation(data []byte) (posX, posY uint32, orientation uint8, err error) {
if len(data) < 9 {
return 0, 0, 0, fmt.Errorf("insufficient data length, got %d instead of 9", len(data))
}
posX = binary.LittleEndian.Uint32(data[0:4])
posY = binary.LittleEndian.Uint32(data[4:8])
orientation = data[8]
return posX, posY, orientation, nil
}
func sendDigBlock(posX, posY uint32, isShooting bool, conn *net.TCPConn) error {
data := make([]byte, 9)
binary.LittleEndian.PutUint32(data[0:4], posX)
binary.LittleEndian.PutUint32(data[4:8], posY)
data[8] = boolToByte(isShooting)
return sendPacket(2, data, conn)
}
func receiveDigBlock(data []byte) (posX, posY uint32, isShooting bool, err error) {
if len(data) < 9 {
return 0, 0, false, fmt.Errorf("insufficient data length, got %d instead of 9", len(data))
}
posX = binary.LittleEndian.Uint32(data[0:4])
posY = binary.LittleEndian.Uint32(data[4:8])
isShooting = byteToBool(data[8])
return posX, posY, isShooting, nil
}
func sendShoot(isSuper bool, conn *net.TCPConn) error {
data := make([]byte, 1)
data[0] = boolToByte(isSuper)
return sendPacket(3, data, conn)
}
func receiveShoot(data []byte) (isSuper bool, err error) {
if len(data) < 1 {
return false, fmt.Errorf("insufficient data length, got %d instead of 1", len(data))
}
isSuper = byteToBool(data[0])
return isSuper, nil
}
func handleRequest(conn *net.TCPConn, players map[uint32]*Player, bullets map[uint32]*Bullet, bases map[uint32]*Base) {
defer func(conn *net.TCPConn) {
_ = conn.Close()
}(conn)
for {
packetID, _, packetData, err := receivePacket(conn)
if err != nil {
return
}
switch packetID {
//goland:noinspection GoDeferInLoop,GoDeferInLoop
case 0:
versionArray := getCurrentVersion()
major, minor, patch, err := receivePlayerStartRequest(packetData)
if err != nil {
continue
}
if versionArray[0] != major || versionArray[1] != minor || versionArray[2] != patch {
log.Printf("Incorrect version tried to connect with %d.%d.%d from %s to %d.%d.%d", versionArray[0], versionArray[1], versionArray[2], conn.RemoteAddr().String(), major, minor, patch)
return
}
log.Printf("Connected from %s with %d.%d.%d", conn.RemoteAddr().String(), major, minor, patch)
unusedColor := getUnusedColor(playerColors, players)
newPlayerID := createPlayer(false, playerColors[unusedColor], unusedColor, 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
success := player.sendVersionBackToPlayer()
if !success {
return
}
log.Printf("Sent version to %d", newPlayerID)
success = player.sendServerInfoToPlayer()
if !success {
log.Printf("Server info not sent to %d", newPlayerID)
return
}
log.Printf("Sent server info to %d", newPlayerID)
var wgX sync.WaitGroup
wgX.Add(1)
success = player.sendPlayerUpdate(&wgX)
if !success {
log.Printf("Server info not sent to %d", newPlayerID)
return
}
log.Printf("Sent info to %d", newPlayerID)
success = player.sendLocationToPlayer()
if !success {
log.Printf("Server location not sent to %d", newPlayerID)
return
}
log.Printf("Sent location to %d", newPlayerID)
player.initialized = true
case 1:
player := netPlayerMapper[conn]
if player == nil {
log.Fatalf("Can't find player for connection")
}
newX, newY, orientation, err := receivePlayerLocation(packetData)
if err != nil {
log.Printf("Error receiving player location: %s", err)
}
oldX := uint32(player.gameObject.baseRect.X)
oldY := uint32(player.gameObject.baseRect.Y)
// Update player orientation
player.gameObject.orientation = 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+uint32(player.gameObject.baseRect.W); x++ {
for y := newY; y < newY+uint32(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: int32(newX),
Y: int32(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 2:
player := netPlayerMapper[conn]
if player == nil {
log.Fatalf("Can't find player for connection")
}
posX, posY, isShooting, err := receiveDigBlock(packetData)
if err != nil {
log.Printf("Error receiving dig block: %s", err)
}
res := player.digBlock(posX, posY, gameMap, isShooting)
if res == 1 {
if player.energy > serverConfig.DiggingCost {
player.energy -= serverConfig.DiggingCost
}
}
case 3:
player := netPlayerMapper[conn]
if player == nil {
log.Fatalf("Can't find player for connection")
}
isSuper, err := receiveShoot(packetData)
if err != nil {
log.Printf("Error receiving dig block: %s", err)
}
player.shoot(isSuper, bullets)
}
}
}

@ -1,135 +0,0 @@
syntax = "proto3";
package goingtunneling;
option go_package = "./proto";
message Position {
int32 posX = 1;
int32 posY = 2;
}
message Player {
uint32 playerID = 1;
PlayerLocation location = 2;
}
message PlayerLocation {
Position position = 1;
uint32 orientation = 2;
}
message Bullet {
Position position = 1;
uint32 direction = 2;
Color color = 3;
uint32 id = 4;
bool super = 5;
uint32 ownerID = 6;
}
message Color {
uint32 red = 1;
uint32 green = 2;
uint32 blue = 3;
uint32 alpha = 4;
}
message BulletParticle {
Position position = 1;
uint32 expirationTimer = 2;
Color color = 3;
uint32 id = 4;
}
message ServerInfo {
uint32 maxEnergy = 1;
uint32 maxAmmunition = 2;
uint32 maxShields = 3;
uint32 mapWidth = 4;
uint32 mapHeight = 5;
uint32 normalShotCost = 6;
uint32 superShotCost = 7;
uint32 reloadCost = 8;
uint32 movementCost = 9;
uint32 diggingCost = 10;
uint32 shootDiggingBonus = 11;
uint32 shootCooldown = 12;
uint32 rechargeCooldown = 13;
uint32 rechargeOpponentCooldown = 14;
uint32 repairCooldown = 15;
uint32 diggingCooldown = 16;
uint32 movementCooldown = 17;
uint32 movementCooldownNoEnergy = 18;
uint32 diggingCooldownNoEnergy = 19;
uint32 reloadCooldown = 20;
uint32 blastRadius = 21;
uint32 playerID = 22;
uint32 playerColorID = 23;
uint32 reloadWait = 24;
}
message PlayerUpdate {
uint32 energy = 1;
uint32 ammo = 2;
uint32 shields = 3;
}
message BaseLocation {
Position position = 1;
Player owner = 2;
Color color = 3;
}
message TileUpdate {
Position position = 1;
uint32 kind = 2;
}
message WorldUpdate {
repeated Player players = 1;
repeated BaseLocation base = 2;
repeated Bullet bullets = 3;
repeated BulletParticle bulletParticles = 4;
repeated TileUpdate tileUpdate = 5;
}
message ClientBound {
oneof clientBoundMessage {
ServerInfo serverInfo = 1;
PlayerUpdate playerUpdate = 2;
PlayerLocation playerLocationUpdate = 3;
WorldUpdate worldUpdate = 4;
PlayerStartResponse playerStartResponse = 5;
}
}
message DigBlock {
Position position = 1;
bool isShooting = 2;
}
message Shoot {
bool super = 1;
}
message PlayerAction {
oneof playerAction {
DigBlock digBlock = 1;
Shoot shoot = 2;
}
}
message PlayerStartRequest {
string version = 1;
}
message PlayerStartResponse {
string version = 1;
}
message ServerBound {
oneof serverBoundMessage {
PlayerLocation playerPosition = 1;
PlayerAction playerAction = 2;
PlayerStartRequest playerStartRequest = 3;
}
}