Fix a few network issues

This commit is contained in:
Bruno Rybársky 2024-08-31 15:35:19 +02:00
parent d310509035
commit 0ba96ca478
7 changed files with 694 additions and 576 deletions

58
base.go

@ -3,8 +3,12 @@ package main
import (
"github.com/veandco/go-sdl2/sdl"
"image/color"
"strconv"
"sync"
)
var baseMutex sync.RWMutex
type Base struct {
ownerID uint32
openingWidth int32
@ -12,8 +16,9 @@ type Base struct {
}
func (base *Base) tick(players map[uint32]*Player) {
playersMutex.RLock()
for playerID, player := range players {
if player.gameObject.baseRect.HasIntersection(&base.gameObject.baseRect) {
if player.gameObject.baseRect.HasIntersection(base.gameObject.baseRect) {
if player.rechargeCooldown == 0 && player.energy < serverConfig.MaxEnergy {
if serverConfig.MaxEnergy-player.energy < 4 {
player.energy++
@ -32,6 +37,7 @@ func (base *Base) tick(players map[uint32]*Player) {
}
}
}
playersMutex.RUnlock()
}
func (base *Base) render(camera *sdl.Rect, surface *sdl.Surface, bases map[uint32]*Base) {
@ -42,14 +48,6 @@ func (base *Base) render(camera *sdl.Rect, surface *sdl.Surface, bases map[uint3
}
func (base *Base) build(gameMap *GameMap) {
borderWidth := base.gameObject.baseRect.W - base.openingWidth
borderWidthA := borderWidth / 2
if borderWidth < 0 {
panic("Bad border width")
}
if base.gameObject.baseRect.H < 9 {
panic("Bad border height")
}
if gameMap.width-uint32(base.gameObject.baseRect.X)-uint32(base.gameObject.baseRect.W) <= 0 {
panic("Bad base x location")
}
@ -57,30 +55,32 @@ func (base *Base) build(gameMap *GameMap) {
panic("Bad base y location")
}
if base.gameObject.baseRect.X < 0 || base.gameObject.baseRect.Y < 0 {
panic("Bad base negative location")
panic("Bad base negative location " + strconv.Itoa(int(base.gameObject.baseRect.X)) + " - " + strconv.Itoa(int(base.gameObject.baseRect.Y)))
}
for x := base.gameObject.baseRect.X; x < base.gameObject.baseRect.X+base.gameObject.baseRect.W+1; x++ {
for y := base.gameObject.baseRect.Y; y < base.gameObject.baseRect.Y+base.gameObject.baseRect.H+1; y++ {
base.gameObject.adjustBaseRect()
for x := base.gameObject.baseRect.X; x < base.gameObject.baseRect.X+base.gameObject.baseRect.W; x++ {
for y := base.gameObject.baseRect.Y; y < base.gameObject.baseRect.Y+base.gameObject.baseRect.H; y++ {
gameMap.tiles[x][y] = 0
}
}
for y := base.gameObject.baseRect.Y; y < base.gameObject.baseRect.Y+base.gameObject.baseRect.H; y++ {
gameMap.tiles[base.gameObject.baseRect.X][y] = 4
gameMap.tiles[base.gameObject.baseRect.X+base.gameObject.baseRect.W][y] = 4
for _, rectT := range base.gameObject.getCurrentRects() {
rect := sdl.Rect{
X: rectT.rect.X + base.gameObject.baseRect.X,
Y: rectT.rect.Y + base.gameObject.baseRect.Y,
W: rectT.rect.W,
H: rectT.rect.H,
}
for x := rect.X; x < rect.X+rect.W; x++ {
for y := rect.Y; y < rect.Y+rect.H; y++ {
gameMap.tiles[x][y] = 4
}
for x := base.gameObject.baseRect.X; x < base.gameObject.baseRect.X+borderWidthA; x++ {
gameMap.tiles[x][base.gameObject.baseRect.Y] = 4
gameMap.tiles[x][base.gameObject.baseRect.Y+base.gameObject.baseRect.H] = 4
}
for x := base.gameObject.baseRect.X + borderWidthA + base.openingWidth; x < base.gameObject.baseRect.X+base.gameObject.baseRect.W+1; x++ {
gameMap.tiles[x][base.gameObject.baseRect.Y] = 4
gameMap.tiles[x][base.gameObject.baseRect.Y+base.gameObject.baseRect.H] = 4
}
}
func createBase(gameMap *GameMap, baseColor color.Color, posX, posY, ownerID, openingWidth uint32) *Base {
gameObject := &GameObject{}
gameObject.baseRect = sdl.Rect{
gameObject.baseRect = &sdl.Rect{
X: int32(posX),
Y: int32(posY),
W: 35,
@ -97,12 +97,12 @@ func createBase(gameMap *GameMap, baseColor color.Color, posX, posY, ownerID, op
panic("Bad border width")
}
gameObject.addColor(baseColor)
gameObject.addColoredRect(0, 0, 1, gameObject.baseRect.H, 0)
gameObject.addColoredRect(gameObject.baseRect.W, 0, 1, gameObject.baseRect.H, 0)
gameObject.addColoredRect(0, 0, 1, gameObject.baseRect.H-1, 0)
gameObject.addColoredRect(gameObject.baseRect.W, 0, 1, gameObject.baseRect.H-1, 0)
gameObject.addColoredRect(0, 0, borderWidthA, 1, 0)
gameObject.addColoredRect(borderWidthA+int32(openingWidth), 0, borderWidthB, 1, 0)
gameObject.addColoredRect(0, gameObject.baseRect.H, borderWidthA, 1, 0)
gameObject.addColoredRect(borderWidthA+int32(openingWidth), gameObject.baseRect.H, borderWidthB, 1, 0)
gameObject.addColoredRect(0, gameObject.baseRect.H-1, borderWidthA, 1, 0)
gameObject.addColoredRect(borderWidthA+int32(openingWidth), gameObject.baseRect.H-1, borderWidthB, 1, 0)
base := &Base{
gameObject: gameObject,
ownerID: ownerID,
@ -118,12 +118,16 @@ func (base *Base) delete(bases map[uint32]*Base) {
gameMap.tiles[x][y] = 0
}
}
baseMutex.Lock()
delete(bases, base.ownerID)
baseMutex.Unlock()
}
func createBases(players map[uint32]*Player, gameMap *GameMap) map[uint32]*Base {
bases := map[uint32]*Base{}
playersMutex.RLock()
for ownerID, player := range players {
baseMutex.Lock()
bases[ownerID] = createBase(gameMap,
player.playerColors.body,
uint32(player.gameObject.baseRect.X-14),
@ -131,6 +135,8 @@ func createBases(players map[uint32]*Player, gameMap *GameMap) map[uint32]*Base
ownerID,
uint32(float64(player.gameObject.baseRect.W)*1.5),
)
baseMutex.Unlock()
}
playersMutex.RUnlock()
return bases
}

@ -4,11 +4,15 @@ import (
"github.com/veandco/go-sdl2/sdl"
"image/color"
"math/rand"
"sync"
)
var bulletLastID = uint32(0)
var bulletParticleLastID = uint32(0)
var bulletMutex sync.RWMutex
var bulletParticleMutex sync.RWMutex
type Bullet struct {
posX, posY int32
direction uint8
@ -159,6 +163,7 @@ func (bullet *Bullet) tick(gameMap *GameMap,
H: 1,
}
hitPlayer := false
playersMutex.RLock()
for _, player := range players {
if player.playerID == bullet.ownerID {
continue
@ -174,6 +179,7 @@ func (bullet *Bullet) tick(gameMap *GameMap,
}
}
}
playersMutex.RUnlock()
if collisionResult != 0 || hitPlayer {
bullet.explode(gameMap, bulletParticleMap)
delete(bulletMap, bullet.id)

@ -6,9 +6,10 @@ import (
)
type GameObject struct {
baseRect sdl.Rect
prevBaseRect sdl.Rect
borderRect sdl.Rect
baseRect *sdl.Rect
prevBaseRect *sdl.Rect
borderRect *sdl.Rect
collisionRect *sdl.Rect
orientation uint8
visualRects [][]*ColoredRect
colors []color.Color
@ -38,6 +39,13 @@ func (gameObject *GameObject) adjustRectWorld(offset *sdl.Rect) *sdl.Rect {
}
}
func (gameObject *GameObject) adjustColoredRectWorld(offset *ColoredRect) *ColoredRect {
return &ColoredRect{
color: offset.color,
rect: gameObject.adjustRectWorld(offset.rect),
}
}
func (gameObject *GameObject) adjustRectToCamera(offset *sdl.Rect, camera *sdl.Rect) *sdl.Rect {
return &sdl.Rect{
X: gameObject.baseRect.X + offset.X - camera.X,
@ -47,20 +55,31 @@ func (gameObject *GameObject) adjustRectToCamera(offset *sdl.Rect, camera *sdl.R
}
}
func (gameObject *GameObject) adjustBaseRect() {
first := true
oldX, oldY := gameObject.baseRect.X, gameObject.baseRect.Y
for _, rect := range gameObject.getCurrentRects() {
if first {
gameObject.baseRect = gameObject.adjustRectWorld(rect.rect)
first = false
} else {
}
newRect := gameObject.baseRect.Union(gameObject.adjustRectWorld(rect.rect))
gameObject.baseRect = &newRect
gameObject.baseRect.X, gameObject.baseRect.Y = oldX, oldY
}
}
func (gameObject *GameObject) render(camera *sdl.Rect, surface *sdl.Surface) {
if camera.HasIntersection(&gameObject.baseRect) {
if camera.HasIntersection(gameObject.baseRect) {
gameObject.inView = true
if config.Debug {
gameObject.borderRect = sdl.Rect{
X: gameObject.baseRect.X - 1,
Y: gameObject.baseRect.Y - 1,
W: gameObject.baseRect.W + 2,
H: gameObject.baseRect.H + 2,
}
borderRectFinal := adjustRectToCamera(&gameObject.borderRect, camera)
baseRectFinal := adjustRectToCamera(&gameObject.baseRect, camera)
surface.FillRect(borderRectFinal, sdl.MapRGBA(surface.Format, 20, 192, 128, 64))
baseRectFinal := adjustRectToCamera(gameObject.baseRect, camera)
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))
}
}
if config.RenderGameObjects {
for _, coloredRect := range gameObject.visualRects[gameObject.orientation] {

144
main.go

@ -8,10 +8,13 @@ import (
"log"
"net"
"os"
"runtime/pprof"
"sync"
"time"
)
const GameVersion = "TunnEElineningnegbfbf Through the wiAldWest"
type ServerConfig struct {
MapWidth uint32 `json:"map_width"`
MapHeight uint32 `json:"map_height"`
@ -37,24 +40,26 @@ type ServerConfig struct {
MovementCooldownNoEnergy uint32 `json:"movement_cooldown_no_energy"`
DiggingCooldownNoEnergy uint32 `json:"digging_cooldown_no_energy"`
ReloadCooldown uint32 `json:"reload_cooldown"`
ReloadWait uint32 `json:"reload_wait"`
}
type Config struct {
Debug bool `json:"debug"`
MapWindow bool `json:"map_window"`
MapUpdateInterval uint16 `json:"map_update_interval"`
RenderGameObjects bool `json:"render_game_objects"`
ProfilerOn bool `json:"profiler_on"`
ProfilerInterval uint64 `json:"profiler_interval"`
Server bool `json:"server"`
CameraW int32 `json:"camera_w"`
CameraH int32 `json:"camera_h"`
Version string `json:"version"`
Client bool `json:"client"`
Address string `json:"address"`
JoyStickDeadZone int16 `json:"joystick_dead_zone"`
DoAllKeymapsPlayers bool `json:"do_all_keymaps_players"`
DoJoyStickPlayers bool `json:"do_joystick_players"`
DoKeymapPlayer bool `json:"do_keymap_player"`
RecentlyDugBlocksClearInterval uint16 `json:"recently_dug_blocks_clear_interval"`
ServerConfig ServerConfig `json:"server_config"`
}
@ -62,19 +67,20 @@ func loadOrCreateConfig(filename string) (*Config, error) {
config := &Config{
Debug: false,
MapWindow: false,
MapUpdateInterval: 999,
RenderGameObjects: true,
ProfilerOn: false,
ProfilerInterval: 100,
Server: false,
CameraW: 76,
CameraH: 76,
Version: "69420",
Client: true,
Address: "192.168.1.8:5074",
JoyStickDeadZone: 8000,
DoAllKeymapsPlayers: false,
DoJoyStickPlayers: true,
DoKeymapPlayer: true,
RecentlyDugBlocksClearInterval: 20,
ServerConfig: ServerConfig{
MapWidth: 1000,
MapHeight: 1000,
@ -100,6 +106,7 @@ func loadOrCreateConfig(filename string) (*Config, error) {
MovementCooldownNoEnergy: 4,
DiggingCooldownNoEnergy: 8,
ReloadCooldown: 16,
ReloadWait: 16,
},
}
@ -149,6 +156,8 @@ var clientInitialized = false
var worldLock sync.Mutex
var totalRenderTime, totalGameLogicCatchUp, totalNormalGameLogic, totalTicking, totalScalingTime, totalRemotePlayerUpdate, tickCount, totalFrameTime, frameCount uint64
func main() {
initializeSDL()
defer sdl.Quit()
@ -158,6 +167,17 @@ func main() {
}
serverConfig = configX.ServerConfig
config = configX
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()
@ -171,7 +191,7 @@ func main() {
if !config.Client {
gameMap.createGameMap(true)
createPlayers(getNeededPlayers(), playerColors, keyMaps, joyMaps, gameMap, players)
createPlayers(getNeededPlayers(), playerColors, keyMaps, joyMaps, gameMap, players, bases)
bases = createBases(players, gameMap)
} else {
// Create a connection to the server
@ -195,9 +215,11 @@ func main() {
defer closeThings(players)
playersMutex.Lock()
for playerIndex, player := range players {
initPlayer(uint8(playerIndex), player)
}
playersMutex.Unlock()
if config.Server {
// Create a connection to the server
@ -216,7 +238,12 @@ func main() {
for {
conn, err := listen.AcceptTCP()
if err != nil {
log.Fatal(err)
if opErr, ok := err.(*net.OpError); ok && opErr.Err.Error() == "use of closed network connection" {
log.Println("Listener closed, stopping server.")
return
}
log.Println("Error accepting connection:", err)
continue
}
go handleRequest(conn, players, bullets, bases)
}
@ -227,7 +254,6 @@ func main() {
mapWindow, mapSurface = setupMapWindowAndSurface()
}
running := true
var totalRenderTime, totalScalingTime, totalFrameTime, frameCount uint64
// Delta time management
var prevTime = sdl.GetTicks64()
@ -239,6 +265,7 @@ func main() {
prevTime = currentTime
worldLock.Lock()
totalGameLogicCatchUp += profileSection(func() {
// Catch up in case of a large delta time
for deltaTime > maxDeltaTime {
deltaTime -= maxDeltaTime
@ -246,10 +273,13 @@ func main() {
// Run multiple logic ticks if deltaTime is too large
runGameLogic(players, gameMap, bases, bullets, bulletParticles)
}
})
totalNormalGameLogic += profileSection(func() {
// Run logic for the remaining delta time
runGameLogic(players, gameMap, bases, bullets, bulletParticles)
})
playersMutex.RLock()
for playerIndex, player := range players {
if player.local {
running = doPlayerFrame(uint8(playerIndex), player, players, gameMap, bases, bullets, bulletParticles)
@ -258,25 +288,34 @@ func main() {
}
}
}
playersMutex.RUnlock()
// Render logic
if config.MapWindow {
if config.MapWindow && frameCount%uint64(config.MapUpdateInterval) == 0 {
// Profile rendering (aggregate all renderings)
totalRenderTime += profileSection(func() {
running = running && handleEvents(mapWindow, mapSurface)
gameMap.render(mapRendererRect, mapSurface)
bulletMutex.RLock()
for _, bullet := range bullets {
(*bullet).render(mapRendererRect, mapSurface, bullets)
}
bulletMutex.RUnlock()
baseMutex.RLock()
for _, base := range bases {
(*base).render(mapRendererRect, mapSurface, bases)
}
baseMutex.RUnlock()
playersMutex.RLock()
for _, playerLoop := range players {
(*playerLoop).render(mapRendererRect, mapSurface, players)
}
playersMutex.RUnlock()
bulletParticleMutex.RLock()
for _, bulletParticle := range bulletParticles {
bulletParticle.render(mapRendererRect, mapSurface, bulletParticles)
}
bulletParticleMutex.RUnlock()
})
totalScalingTime += profileSection(func() {
adjustWindow(mapWindow, mapSurface)
@ -289,8 +328,8 @@ func main() {
// Log profiling information every 1000 frames
if frameCount%config.ProfilerInterval == 0 && config.ProfilerOn {
logMapProfilingInfo(totalRenderTime, totalScalingTime, totalFrameTime, frameCount)
resetMapProfilingCounters(&totalRenderTime, &totalScalingTime, &totalFrameTime, &frameCount)
logMapProfilingInfo(totalRenderTime, totalScalingTime, totalFrameTime, totalGameLogicCatchUp, totalNormalGameLogic, totalTicking, totalRemotePlayerUpdate, frameCount, tickCount)
resetMapProfilingCounters(&totalRenderTime, &totalScalingTime, &totalFrameTime, &totalGameLogicCatchUp, &totalNormalGameLogic, &totalTicking, &totalRemotePlayerUpdate, &frameCount, &tickCount)
}
worldLock.Unlock()
enforceFrameRate(currentTime, 60)
@ -303,51 +342,94 @@ func runGameLogic(players map[uint32]*Player, gameMap *GameMap, bases map[uint32
if config.Server {
//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 {
continue
}
go player.updateRemotePlayerMap()
go player.updateRemotePlayerBases(bases)
go player.updateRemotePlayerBullets(bullets)
go player.updateRemotePlayerBulletParticles(bulletParticles)
go player.updateRemotePlayerPlayers(players)
fail := player.sendInfoToPlayer()
wgX.Add(6)
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 {
if player.connection != nil {
(*player.connection).Close()
if playerX.connection != nil {
(*playerX.connection).Close()
}
delete(players, player.playerID)
continue
}
fail = player.sendUpdatesToPlayer(players)
if fail {
if player.connection != nil {
(*player.connection).Close()
}
delete(players, player.playerID)
continue
baseMutex.Lock()
if bases[playerX.playerID] != nil {
bases[playerX.playerID].delete(bases)
}
baseMutex.Unlock()
playersMutex.TryLock()
delete(players, playerX.playerID)
playersMutex.Unlock()
}
}()
}
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()
})
}
totalTicking += profileSection(func() {
playerUpdateMutex.Lock()
bulletMutex.RLock()
for _, bullet := range bullets {
(*bullet).tick(gameMap, bulletParticles, bullets, players)
}
bulletMutex.RUnlock()
playersMutex.RLock()
for _, player := range players {
if player.local {
continue
}
(*player).tick(bullets)
}
playersMutex.RUnlock()
baseMutex.RLock()
for _, base := range bases {
(*base).tick(players)
}
baseMutex.RUnlock()
bulletParticleMutex.RLock()
for _, bulletParticle := range bulletParticles {
bulletParticle.tick(bulletParticles)
}
bulletParticleMutex.RUnlock()
playerUpdateMutex.Unlock()
})
tickCount++
}
func initPlayer(playerIndex uint8, player *Player) {
@ -387,18 +469,26 @@ func doPlayerFrame(playerIndex uint8, player *Player, players map[uint32]*Player
player.track(player.camera)
gameMap.render(player.camera, player.playSurface)
bulletMutex.RLock()
for _, bullet := range bullets {
(*bullet).render(player.camera, player.playSurface, bullets)
}
bulletMutex.RUnlock()
baseMutex.RLock()
for _, base := range bases {
(*base).render(player.camera, player.playSurface, bases)
}
baseMutex.RUnlock()
playersMutex.RLock()
for _, playerLoop := range players {
(*playerLoop).render(player.camera, player.playSurface, players)
}
playersMutex.RUnlock()
bulletParticleMutex.RLock()
for _, bulletParticle := range bulletParticles {
bulletParticle.render(player.camera, player.playSurface, bulletParticles)
}
bulletParticleMutex.RUnlock()
player.tick(bullets)
player.gameObject.prevBaseRect = player.gameObject.baseRect

848
player.go

File diff suppressed because it is too large Load Diff

@ -18,14 +18,20 @@ func logProfilingInfo(handleEventsTime, renderTime, scaleTime, frameTime, frameC
fmt.Printf("Average render time: %f ms\n", float64(renderTime)/floatFrame)
fmt.Printf("Average scaling time: %f ms\n", float64(scaleTime)/floatFrame)
fmt.Printf("Average frame time: %f ms\n", float64(frameTime)/floatFrame)
fmt.Print("\n")
}
func logMapProfilingInfo(renderTime, scaleTime, frameTime, frameCount uint64) {
func logMapProfilingInfo(renderTime, scaleTime, frameTime, totalGameLogicCatchUp, totalNormalGameLogic, totalTicking, totalRemotePlayerUpdate, tickCount, frameCount uint64) {
floatFrame := float64(frameCount)
floatTick := float64(tickCount)
fmt.Printf("Average map render time: %f ms\n", float64(renderTime)/floatFrame)
fmt.Printf("Average map scaling time: %f ms\n", float64(scaleTime)/floatFrame)
fmt.Printf("Average full render time: %f ms\n", float64(frameTime)/floatFrame)
fmt.Printf("Average total gamelogic catching up time: %f ms\n", float64(totalGameLogicCatchUp)/floatFrame)
fmt.Printf("Average normal gamelogic time: %f ms\n", float64(totalNormalGameLogic)/floatFrame)
fmt.Printf("Average ticking time: %f ms\n", float64(totalTicking)/floatTick)
fmt.Printf("Average remote player update time: %f ms\n", float64(totalRemotePlayerUpdate)/floatTick)
fmt.Print("\n")
}
@ -37,11 +43,16 @@ func resetProfilingCounters(handleEventsTime, renderTime, scaleTime, frameTime *
*frameCount = 0
}
func resetMapProfilingCounters(renderTime, scaleTime, frameTime *uint64, frameCount *uint64) {
func resetMapProfilingCounters(renderTime, scaleTime, frameTime, totalGameLogicCatchUp, totalNormalGameLogic, totalTicking, totalRemotePlayerUpdate *uint64, frameCount, tickCount *uint64) {
*renderTime = 0
*scaleTime = 0
*frameTime = 0
*frameCount = 0
*totalGameLogicCatchUp = 0
*totalNormalGameLogic = 0
*totalTicking = 0
*totalRemotePlayerUpdate = 0
*tickCount = 0
}
func enforceFrameRate(frameStart uint64, targetFPS int) {

@ -23,6 +23,7 @@ message Bullet {
Color color = 3;
uint32 id = 4;
bool super = 5;
uint32 ownerID = 6;
}
message Color {
@ -63,6 +64,7 @@ message ServerInfo {
uint32 blastRadius = 21;
uint32 playerID = 22;
uint32 playerColorID = 23;
uint32 reloadWait = 24;
}
message PlayerUpdate {