From 589fd619025b5150b0ca8602dab6ce5a8dab82e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Ryb=C3=A1rsky?= Date: Sun, 1 Sep 2024 20:13:53 +0200 Subject: [PATCH] Update networking stack --- bullet.go | 4 +- clientboundpacket.go | 465 ++++++++++++++++ gameobject.go | 6 +- go.mod | 1 - graphics.go | 10 +- hud.go | 8 +- main.go | 74 +-- netCode.go | 119 +++++ player.go | 1192 ++++++------------------------------------ playerData.go | 4 +- playerutil.go | 316 +++++++++++ serverboundpacket.go | 236 +++++++++ tuneller.proto | 135 ----- 13 files changed, 1335 insertions(+), 1235 deletions(-) create mode 100644 clientboundpacket.go create mode 100644 netCode.go create mode 100644 playerutil.go create mode 100644 serverboundpacket.go delete mode 100644 tuneller.proto diff --git a/bullet.go b/bullet.go index 2fbb8e0..8e58ffb 100644 --- a/bullet.go +++ b/bullet.go @@ -181,7 +181,9 @@ func (bullet *Bullet) tick(gameMap *GameMap, } playersMutex.RUnlock() if collisionResult != 0 || hitPlayer { - bullet.explode(gameMap, bulletParticleMap) + if !config.Client { + bullet.explode(gameMap, bulletParticleMap) + } delete(bulletMap, bullet.id) } else { bullet.posX = nextX diff --git a/clientboundpacket.go b/clientboundpacket.go new file mode 100644 index 0000000..b6a6512 --- /dev/null +++ b/clientboundpacket.go @@ -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 + } + } + } +} diff --git a/gameobject.go b/gameobject.go index d917fd0..f1adbfc 100644 --- a/gameobject.go +++ b/gameobject.go @@ -76,16 +76,16 @@ func (gameObject *GameObject) render(camera *sdl.Rect, surface *sdl.Surface) { gameObject.inView = true if config.Debug { 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() { - 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 { for _, coloredRect := range gameObject.visualRects[gameObject.orientation] { finalRect := gameObject.adjustRectToCamera(coloredRect.rect, camera) 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 { diff --git a/go.mod b/go.mod index 25614f7..1cf8b4a 100644 --- a/go.mod +++ b/go.mod @@ -5,5 +5,4 @@ go 1.21 require ( github.com/aquilax/go-perlin v1.1.0 github.com/veandco/go-sdl2 v0.4.40 - google.golang.org/protobuf v1.34.2 ) diff --git a/graphics.go b/graphics.go index bc55945..1e8350c 100644 --- a/graphics.go +++ b/graphics.go @@ -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} dstRect := &sdl.Rect{X: letterboxX, Y: letterboxY, W: newWidth, H: newHeight} - logicalSurface.BlitScaled(srcRect, windowSurface, dstRect) - window.UpdateSurface() + _ = logicalSurface.BlitScaled(srcRect, windowSurface, dstRect) + _ = window.UpdateSurface() } func adjustWindow(window *sdl.Window, logicalSurface *sdl.Surface) { @@ -133,15 +133,15 @@ func adjustWindow(window *sdl.Window, logicalSurface *sdl.Surface) { letterboxY := (windowHeight - newHeight) / 2 // 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 srcRect := &sdl.Rect{X: 0, Y: 0, W: logicalSurface.W, H: logicalSurface.H} dstRect := &sdl.Rect{X: letterboxX, Y: letterboxY, W: newWidth, H: newHeight} // Perform the scaled blit - logicalSurface.BlitScaled(srcRect, windowSurface, dstRect) + _ = logicalSurface.BlitScaled(srcRect, windowSurface, dstRect) // Update the window surface - window.UpdateSurface() + _ = window.UpdateSurface() } diff --git a/hud.go b/hud.go index dec0ef0..ff4c864 100644 --- a/hud.go +++ b/hud.go @@ -48,13 +48,13 @@ func initHud(surface *sdl.Surface) { sdl.MapRGBA(surface.Format, 40, 243, 243, 255), } HUDColor := sdl.MapRGBA(surface.Format, 101, 101, 101, 255) - surface.FillRect(nil, HUDColor) + _ = surface.FillRect(nil, HUDColor) for letterIndex, letter := range rects { for _, letterRect := range letter { offsetRect := letterRect 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) { 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{ @@ -72,7 +72,7 @@ func renderHud(player *Player, surface *sdl.Surface) { } for HUDValueIndex, HUDValue := range HUDValues { - surface.FillRect( + _ = surface.FillRect( &sdl.Rect{ X: 17, Y: letterOffsets[HUDValueIndex], diff --git a/main.go b/main.go index 0d37e4e..4b2f3d1 100644 --- a/main.go +++ b/main.go @@ -2,23 +2,22 @@ package main import ( "encoding/json" + "errors" "fmt" "github.com/veandco/go-sdl2/sdl" - "io/ioutil" "log" "net" "os" - "runtime/pprof" "sync" "time" ) -const GameVersion = "TunnEElineningnegbfbf Through the wiAldWest" +const GameVersion = "0.0.1" type ServerConfig struct { MapWidth uint32 `json:"map_width"` MapHeight uint32 `json:"map_height"` - BlastRadius uint32 `json:"blast_radius"` + BlastRadius uint8 `json:"blast_radius"` MaxEnergy uint32 `json:"max_energy"` 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) } - err = ioutil.WriteFile(filename, data, 0644) + err = os.WriteFile(filename, data, 0644) if err != nil { return nil, fmt.Errorf("failed to write config file: %v", err) } fmt.Println("Config file created with default values.") } else { // File exists, load the config - data, err := ioutil.ReadFile(filename) + data, err := os.ReadFile(filename) if err != nil { return nil, fmt.Errorf("failed to read config file: %v", err) } @@ -172,17 +171,6 @@ func main() { initializeSDL() 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)} players := make(map[uint32]*Player) initPlayerColors() @@ -210,7 +198,9 @@ func main() { if err != nil { 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) for !clientInitialized { @@ -238,12 +228,15 @@ func main() { log.Fatal(err) } // close listener - defer listen.Close() + defer func(listen *net.TCPListener) { + _ = listen.Close() + }(listen) go func() { for { conn, err := listen.AcceptTCP() 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.") return } @@ -349,25 +342,24 @@ func runGameLogic(players map[uint32]*Player, gameMap *GameMap, bases map[uint32 //update remote player maps totalRemotePlayerUpdate += profileSection(func() { var wgX sync.WaitGroup - var wgX2 sync.WaitGroup playerUpdateMutex.Lock() playersMutex.RLock() for _, player := range players { - if player.local { + if player.local || !player.initialized { continue } wgX.Add(6) + go player.updateRemotePlayerBases(bases, players, &wgX) go player.updateRemotePlayerMap(&wgX) - go player.updateRemotePlayerBases(bases, &wgX) go player.updateRemotePlayerBullets(bullets, &wgX) go player.updateRemotePlayerBulletParticles(bulletParticles, &wgX) go player.updateRemotePlayerPlayers(players, &wgX) playerX := player go func() { - fail := playerX.sendInfoToPlayer(&wgX) - if fail { + success := playerX.sendPlayerUpdate(&wgX) + if !success { if playerX.connection != nil { - (*playerX.connection).Close() + _ = (*playerX.connection).Close() } baseMutex.Lock() if bases[playerX.playerID] != nil { @@ -382,28 +374,6 @@ func runGameLogic(players map[uint32]*Player, gameMap *GameMap, bases map[uint32 } playersMutex.RUnlock() 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() }) } @@ -443,11 +413,11 @@ func initPlayer(playerIndex uint8, player *Player) { } player.window, player.logicalSurface = setupWindowAndSurface(playerIndex) 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() 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() initHud(player.HUDSurface) @@ -498,8 +468,8 @@ func doPlayerFrame(playerIndex uint8, player *Player, players map[uint32]*Player player.tick(bullets) player.gameObject.prevBaseRect = player.gameObject.baseRect renderHud(player, player.HUDSurface) - player.playSurface.BlitScaled(player.playSurfaceRect, player.logicalSurface, player.playSurfaceTargetRect) - player.HUDSurface.BlitScaled(player.HUDSurfaceRect, player.logicalSurface, player.HUDSurfaceTargetRect) + _ = player.playSurface.BlitScaled(player.playSurfaceRect, player.logicalSurface, player.playSurfaceTargetRect) + _ = player.HUDSurface.BlitScaled(player.HUDSurfaceRect, player.logicalSurface, player.HUDSurfaceTargetRect) }) // Profile window adjustments diff --git a/netCode.go b/netCode.go new file mode 100644 index 0000000..398969c --- /dev/null +++ b/netCode.go @@ -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 +} diff --git a/player.go b/player.go index 1f6c494..6f17526 100644 --- a/player.go +++ b/player.go @@ -1,18 +1,10 @@ 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" ) @@ -21,16 +13,17 @@ 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 + local bool + connection *net.TCPConn + playerColors PlayerColors + playerColorID uint32 + keyMap KeyMap + joyMap JoyMap + joyStick *sdl.Joystick + camera *sdl.Rect + energy uint32 + ammunition uint32 + shields uint32 digCooldown uint32 shootCooldown uint32 @@ -44,14 +37,14 @@ type Player struct { 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 + + previousEnergy uint32 + previousAmmunition uint32 + previousShields uint32 window *sdl.Window @@ -70,12 +63,7 @@ type Player struct { totalFrameTime uint64 totalScalingTime uint64 frameCount uint64 -} - -type TileUpdate struct { - PosX uint32 - PosY uint32 - Kind uint8 + initialized bool } func (player *Player) track(camera *sdl.Rect) { @@ -129,8 +117,8 @@ func (player *Player) tick(bullets map[uint32]*Bullet) { } } -func (player *Player) digBlock(posX, posY int32, gameMap *GameMap, isShooting bool) uint8 { - collisionAtDigSite := gameMap.checkCollision(posX, posY) +func (player *Player) digBlock(posX, posY uint32, gameMap *GameMap, isShooting bool) uint8 { + collisionAtDigSite := gameMap.checkCollision(int32(posX), int32(posY)) if collisionAtDigSite == 0 { return 0 } else if collisionAtDigSite == 1 && (player.digCooldown == 0 || isShooting) { @@ -147,7 +135,7 @@ func (player *Player) digBlock(posX, posY int32, gameMap *GameMap, isShooting bo } if config.Client { - player.sendDigToServer(uint32(posX), uint32(posY), isShooting) + player.sendDigToServer(posX, posY, isShooting) } gameMap.tiles[posX][posY] = 0 return 1 @@ -156,6 +144,9 @@ func (player *Player) digBlock(posX, posY int32, gameMap *GameMap, isShooting bo } func (player *Player) tryMove(gameMap *GameMap, isShooting bool, players map[uint32]*Player) (moved bool) { + if config.Client { + defer player.sendPositionToServer() + } player.gameObject.adjustBaseRect() if player.movementCooldown > 0 { return false @@ -199,7 +190,7 @@ func (player *Player) tryMove(gameMap *GameMap, isShooting bool, players map[uin // 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) + digResult := player.digBlock(uint32(x), uint32(y), gameMap, isShooting) if digResult == 1 { // Successfully dug a block if !isShooting { stopped = true // Stop movement if not shooting @@ -237,9 +228,6 @@ func (player *Player) tryMove(gameMap *GameMap, isShooting bool, players map[uin player.gameObject.baseRect.X += dxT player.gameObject.baseRect.Y += dyT moved = true - if config.Client { - player.sendPositionToServer() - } } // Send the updated position to the server @@ -374,362 +362,8 @@ func (player *Player) explode(bullets map[uint32]*Bullet) { } } -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)) - } - 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 @@ -763,8 +397,15 @@ func (player *Player) updateRemotePlayerMap(wgX *sync.WaitGroup) { 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] + kind := gameMap.tiles[x][y] + err := sendTileUpdate( + uint32(x), + uint32(y), + kind, + player.connection) + if err == nil { + player.knownGameMap.tiles[x][y] = gameMap.tiles[x][y] + } lck.Unlock() } } @@ -789,7 +430,15 @@ func (player *Player) updateRemotePlayerPlayers(players map[uint32]*Player, wgX for _, playerLoopRect := range playerLoop.gameObject.getCurrentRects() { if player.camera.HasIntersection(playerLoop.gameObject.adjustRectWorld(playerLoopRect.rect)) && playerLoop.shields <= serverConfig.MaxShields { - player.knownPlayers[playerLoop.playerID] = playerLoop + if player.knownPlayers[playerLoop.playerID] == nil { + _ = sendOtherPlayer( + playerLoop.playerID, + uint32(player.gameObject.baseRect.X), + uint32(player.gameObject.baseRect.Y), + player.gameObject.orientation, + player.connection) + player.knownPlayers[playerLoop.playerID] = playerLoop + } break } } @@ -815,7 +464,22 @@ func (player *Player) updateRemotePlayerBullets(bullets map[uint32]*Bullet, wgX H: config.CameraH, } if player.camera.HasIntersection(bulletRect) { - player.knownBullets[bullet.id] = bullet + if player.knownBullets[bullet.id] == nil { + r, g, b, a := bullet.color.RGBA() + _ = sendBullet( + uint32(bullet.posX), + uint32(bullet.posY), + bullet.direction, + uint8(r), + uint8(g), + uint8(b), + uint8(a), + bullet.id, + bullet.super, + bullet.ownerID, + player.connection) + player.knownBullets[bullet.id] = bullet + } } } bulletMutex.RUnlock() @@ -839,15 +503,31 @@ func (player *Player) updateRemotePlayerBulletParticles(bulletParticles map[uint H: config.CameraH, } if player.camera.HasIntersection(bulletRect) { - player.knownBulletParticles[bulletParticle.id] = bulletParticle + if player.knownBullets[bulletParticle.id] == nil { + r, g, b, a := bulletParticle.color.RGBA() + _ = sendBulletParticle( + uint32(bulletParticle.posX), + uint32(bulletParticle.posY), + bulletParticle.expirationTimer, + uint8(r), + uint8(g), + uint8(b), + uint8(a), + bulletParticle.id, + player.connection) + player.knownBulletParticles[bulletParticle.id] = bulletParticle + } } } bulletParticleMutex.RUnlock() } -func (player *Player) updateRemotePlayerBases(bases map[uint32]*Base, wgX *sync.WaitGroup) { +func (player *Player) updateRemotePlayerBases(bases map[uint32]*Base, players map[uint32]*Player, wgX *sync.WaitGroup) { defer wgX.Done() player.knownBases = make(map[uint32]*Base) + if player.connection == nil || !config.Server { + return + } baseMutex.RLock() for _, base := range bases { player.camera = &sdl.Rect{ @@ -858,7 +538,15 @@ func (player *Player) updateRemotePlayerBases(bases map[uint32]*Base, wgX *sync. } for _, baseRect := range base.gameObject.getCurrentRects() { if player.camera.HasIntersection(base.gameObject.adjustRectWorld(baseRect.rect)) { - player.knownBases[base.ownerID] = base + if player.knownPlayers[base.ownerID] == nil { + _ = sendBaseLocation( + uint32(base.gameObject.baseRect.X), + uint32(base.gameObject.baseRect.Y), + base.ownerID, + players[base.ownerID].playerColorID, + player.connection) + player.knownBases[base.ownerID] = base + } break } } @@ -866,684 +554,122 @@ func (player *Player) updateRemotePlayerBases(bases map[uint32]*Base, wgX *sync. baseMutex.RUnlock() } -func (player *Player) sendInfoToPlayer(wgX *sync.WaitGroup) bool { +func (player *Player) sendPlayerUpdate(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, - }, - }, + if player.previousEnergy != player.energy || + player.previousShields != player.shields || + player.previousAmmunition != player.ammunition { + err := sendPlayerUpdate(player.energy, player.ammunition, player.shields, player.connection) + player.previousEnergy = player.energy + player.previousShields = player.shields + player.previousAmmunition = player.ammunition + return err == nil + } else { + return true } - 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() +func (player *Player) sendServerInfoToPlayer() bool { 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) + err := sendServerInfo( + player.playerID, + player.playerColorID, + serverConfig.MaxEnergy, + serverConfig.MaxAmmunition, + serverConfig.MaxShields, + serverConfig.MapWidth, + serverConfig.MapHeight, + serverConfig.NormalShotCost, + serverConfig.SuperShotCost, + serverConfig.ReloadCost, + serverConfig.MovementCost, + serverConfig.DiggingCost, + serverConfig.ShootDiggingCostBonus, + serverConfig.ShootCooldown, + serverConfig.RechargeCooldownOwn, + serverConfig.RechargeCooldownOpponent, + serverConfig.RepairCooldown, + serverConfig.DiggingCooldown, + serverConfig.MovementCooldown, + serverConfig.MovementCooldownNoEnergy, + serverConfig.DiggingCooldownNoEnergy, + serverConfig.ReloadCooldown, + serverConfig.BlastRadius, + serverConfig.ReloadWait, + player.connection) + return err == nil } return false } func (player *Player) sendVersionBackToPlayer() bool { if player.connection != nil && config.Server { - 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) + + versionArray := getCurrentVersion() + err := sendPlayerStartResponse( + versionArray[0], + versionArray[1], + versionArray[2], + player.connection) + return err == nil } return false } func (player *Player) sendLocationToPlayer() bool { if player.connection != nil && config.Server { - 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) + err := sendPositionUpdate( + uint32(player.gameObject.baseRect.X), + uint32(player.gameObject.baseRect.Y), + player.gameObject.orientation, + player.connection) + return err == nil } return false } func (player *Player) sendPositionToServer() bool { if player.connection != nil && config.Client { - 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) + err := sendPlayerLocation( + uint32(player.gameObject.baseRect.X), + uint32(player.gameObject.baseRect.Y), + player.gameObject.orientation, + player.connection) + return err == nil } return false } func sendVersionToServer(connection *net.TCPConn) { if connection != nil && config.Client { - 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) + versionArray := getCurrentVersion() + _ = sendPlayerStartRequest( + versionArray[0], + versionArray[1], + versionArray[2], + connection) + + return } } func (player *Player) sendDigToServer(posX, posY uint32, isShooting bool) { if player.connection != nil && config.Client { - 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) + _ = sendDigBlock( + posX, + posY, + isShooting, + player.connection) } } 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[config.KeyBindOffset], 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() - } - } + _ = sendShoot( + isSuper, + player.connection) } } diff --git a/playerData.go b/playerData.go index 52ec7d9..cc8e411 100644 --- a/playerData.go +++ b/playerData.go @@ -379,7 +379,9 @@ func handleInput(keyboard []uint8, bullets map[uint32]*Bullet, player *Player, g // Handle movement after the loop if moveUp || moveDown || moveLeft || moveRight { if player.tryMove(gameMap, shoot, players) { - player.energy -= serverConfig.MovementCost + if player.energy > serverConfig.MovementCost { + player.energy -= serverConfig.MovementCost + } } } diff --git a/playerutil.go b/playerutil.go new file mode 100644 index 0000000..d2828b5 --- /dev/null +++ b/playerutil.go @@ -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 +} diff --git a/serverboundpacket.go b/serverboundpacket.go new file mode 100644 index 0000000..230cfac --- /dev/null +++ b/serverboundpacket.go @@ -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) + } + } +} diff --git a/tuneller.proto b/tuneller.proto deleted file mode 100644 index b578b80..0000000 --- a/tuneller.proto +++ /dev/null @@ -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; - } -} \ No newline at end of file