491 lines
15 KiB
Go
491 lines
15 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/veandco/go-sdl2/sdl"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const GameVersion = "0.0.1"
|
|
|
|
type ServerConfig struct {
|
|
MapWidth uint32 `json:"map_width"`
|
|
MapHeight uint32 `json:"map_height"`
|
|
BlastRadius uint8 `json:"blast_radius"`
|
|
|
|
MaxEnergy uint32 `json:"max_energy"`
|
|
MaxAmmunition uint32 `json:"max_ammunition"`
|
|
MaxShields uint32 `json:"max_shields"`
|
|
|
|
NormalShotCost uint32 `json:"normal_shot_cost"`
|
|
SuperShotCost uint32 `json:"super_shot_cost"`
|
|
ReloadCost uint32 `json:"reload_cost"`
|
|
MovementCost uint32 `json:"movement_cost"`
|
|
DiggingCost uint32 `json:"digging_cost"`
|
|
ShootDiggingCostBonus uint32 `json:"shoot_digging_cost_bonus"`
|
|
|
|
ShootCooldown uint32 `json:"shoot_cooldown"`
|
|
RechargeCooldownOwn uint32 `json:"recharge_cooldown_own"`
|
|
DiggingCooldown uint32 `json:"digging_cooldown"`
|
|
RechargeCooldownOpponent uint32 `json:"recharge_cooldown_opponent"`
|
|
RepairCooldown uint32 `json:"repair_cooldown"`
|
|
MovementCooldown uint32 `json:"movement_cooldown"`
|
|
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"`
|
|
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"`
|
|
KeyBindOffset uint16 `json:"key_bind_offset"`
|
|
ServerConfig ServerConfig `json:"server_config"`
|
|
}
|
|
|
|
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,
|
|
Client: true,
|
|
Address: "192.168.1.8:5074",
|
|
JoyStickDeadZone: 8000,
|
|
DoAllKeymapsPlayers: false,
|
|
DoJoyStickPlayers: true,
|
|
DoKeymapPlayer: true,
|
|
RecentlyDugBlocksClearInterval: 20,
|
|
KeyBindOffset: 0,
|
|
ServerConfig: ServerConfig{
|
|
MapWidth: 1000,
|
|
MapHeight: 1000,
|
|
BlastRadius: 5,
|
|
|
|
MaxEnergy: 3520,
|
|
MaxAmmunition: 6,
|
|
MaxShields: 100,
|
|
|
|
NormalShotCost: 7,
|
|
SuperShotCost: 80,
|
|
ReloadCost: 4,
|
|
MovementCost: 1,
|
|
DiggingCost: 3,
|
|
ShootDiggingCostBonus: 1,
|
|
|
|
ShootCooldown: 8,
|
|
RechargeCooldownOwn: 0,
|
|
DiggingCooldown: 4,
|
|
RechargeCooldownOpponent: 6,
|
|
RepairCooldown: 4,
|
|
MovementCooldown: 2,
|
|
MovementCooldownNoEnergy: 4,
|
|
DiggingCooldownNoEnergy: 8,
|
|
ReloadCooldown: 16,
|
|
ReloadWait: 16,
|
|
},
|
|
}
|
|
|
|
// Check if the file exists
|
|
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
|
// File does not exist, create it with the default config
|
|
data, err := json.MarshalIndent(config, "", " ")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal config: %v", err)
|
|
}
|
|
|
|
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 := os.ReadFile(filename)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read config file: %v", err)
|
|
}
|
|
|
|
err = json.Unmarshal(data, config)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal config: %v", err)
|
|
}
|
|
fmt.Println("Config file loaded.")
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
var config *Config
|
|
|
|
var gameMap = &GameMap{}
|
|
|
|
var serverConfig ServerConfig
|
|
|
|
var mapWindow *sdl.Window
|
|
var mapSurface *sdl.Surface
|
|
var mapRendererRect *sdl.Rect
|
|
|
|
var netPlayerMapper = map[*net.TCPConn]*Player{}
|
|
|
|
var clientInitialized = false
|
|
|
|
var worldLock sync.Mutex
|
|
|
|
var totalRenderTime, totalGameLogicCatchUp, totalNormalGameLogic, totalTicking, totalScalingTime, totalRemotePlayerUpdate, tickCount, totalFrameTime, frameCount uint64
|
|
|
|
func main() {
|
|
configX, err := loadOrCreateConfig("config.json")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
serverConfig = configX.ServerConfig
|
|
config = configX
|
|
|
|
if !(config.Server && !config.DoKeymapPlayer && !config.DoJoyStickPlayers && !config.DoAllKeymapsPlayers && !config.MapWindow) {
|
|
initializeSDL()
|
|
defer sdl.Quit()
|
|
}
|
|
mapRendererRect = &sdl.Rect{X: 0, Y: 0, W: int32(serverConfig.MapWidth), H: int32(serverConfig.MapHeight)}
|
|
players := make(map[uint32]*Player)
|
|
initPlayerColors()
|
|
bullets := make(map[uint32]*Bullet)
|
|
bulletParticles := make(map[uint32]*BulletParticle)
|
|
bases := make(map[uint32]*Base)
|
|
|
|
if config.Client && config.Server {
|
|
panic("You can' t run client and server in the same instance")
|
|
}
|
|
|
|
if !config.Client {
|
|
gameMap.createGameMap(true)
|
|
createPlayers(getNeededPlayers(), playerColors, keyMaps, joyMaps, gameMap, players, bases)
|
|
bases = createBases(players, gameMap)
|
|
} else {
|
|
// Create a connection to the server
|
|
addr, err := net.ResolveTCPAddr("tcp", config.Address)
|
|
|
|
if err != nil {
|
|
log.Fatal("Error resolving address " + config.Address + ": " + err.Error())
|
|
}
|
|
|
|
conn, err := net.DialTCP("tcp", nil, addr)
|
|
if err != nil {
|
|
log.Fatalf("Failed to connect to server: %v", err)
|
|
}
|
|
defer func(conn *net.TCPConn) {
|
|
_ = conn.Close()
|
|
}(conn)
|
|
|
|
go handleConnectionClient(conn, players, bases, bullets, bulletParticles)
|
|
for !clientInitialized {
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
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
|
|
addr, err := net.ResolveTCPAddr("tcp", config.Address)
|
|
|
|
if err != nil {
|
|
log.Fatal("Error resolving address " + config.Address + ": " + err.Error())
|
|
}
|
|
listen, err := net.ListenTCP("tcp", addr)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
// close listener
|
|
defer func(listen *net.TCPListener) {
|
|
_ = listen.Close()
|
|
}(listen)
|
|
go func() {
|
|
for {
|
|
conn, err := listen.AcceptTCP()
|
|
if err != nil {
|
|
var opErr *net.OpError
|
|
if errors.As(err, &opErr) && 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)
|
|
}
|
|
}()
|
|
}
|
|
|
|
if config.MapWindow {
|
|
mapWindow, mapSurface = setupMapWindowAndSurface()
|
|
}
|
|
running := true
|
|
|
|
// Delta time management
|
|
var prevTime = sdl.GetTicks64()
|
|
const maxDeltaTime uint64 = 1000 / 60 // max 60 FPS, ~16ms per frame
|
|
|
|
for running {
|
|
currentTime := sdl.GetTicks64()
|
|
deltaTime := currentTime - prevTime
|
|
prevTime = currentTime
|
|
worldLock.Lock()
|
|
|
|
totalGameLogicCatchUp += profileSection(func() {
|
|
// Catch up in case of a large delta time
|
|
for deltaTime > maxDeltaTime {
|
|
deltaTime -= maxDeltaTime
|
|
|
|
// 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)
|
|
if !running {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
playersMutex.RUnlock()
|
|
|
|
// Render logic
|
|
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)
|
|
})
|
|
}
|
|
|
|
frameEnd := sdl.GetTicks64()
|
|
totalFrameTime += frameEnd - currentTime
|
|
frameCount++
|
|
|
|
// Log profiling information every 1000 frames
|
|
if frameCount%config.ProfilerInterval == 0 && config.ProfilerOn {
|
|
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)
|
|
}
|
|
}
|
|
|
|
// Separate function to handle game logic
|
|
func runGameLogic(players map[uint32]*Player, gameMap *GameMap, bases map[uint32]*Base, bullets map[uint32]*Bullet, bulletParticles map[uint32]*BulletParticle) {
|
|
// Tick world
|
|
|
|
if config.Server {
|
|
//update remote player maps
|
|
totalRemotePlayerUpdate += profileSection(func() {
|
|
var wgX sync.WaitGroup
|
|
playerUpdateMutex.Lock()
|
|
playersMutex.RLock()
|
|
for _, player := range players {
|
|
if player.local || !player.initialized {
|
|
continue
|
|
}
|
|
wgX.Add(6)
|
|
go player.updateRemotePlayerBases(bases, players, &wgX)
|
|
go player.updateRemotePlayerMap(&wgX)
|
|
go player.updateRemotePlayerBullets(bullets, &wgX)
|
|
go player.updateRemotePlayerBulletParticles(bulletParticles, &wgX)
|
|
go player.updateRemotePlayerPlayers(players, &wgX)
|
|
playerX := player
|
|
go func() {
|
|
success := playerX.sendPlayerUpdate(&wgX)
|
|
if !success {
|
|
if playerX.connection != nil {
|
|
_ = (*playerX.connection).Close()
|
|
}
|
|
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()
|
|
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) {
|
|
if !player.local {
|
|
return
|
|
}
|
|
player.window, player.logicalSurface = setupWindowAndSurface(playerIndex)
|
|
logicalColor := sdl.MapRGBA(player.logicalSurface.Format, 101, 101, 0, 255)
|
|
_ = 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.HUDSurface, player.HUDSurfaceRect, player.HUDSurfaceTargetRect = setupHUDSurface()
|
|
initHud(player.HUDSurface)
|
|
player.camera = &sdl.Rect{X: 0, Y: 0, W: config.CameraW, H: config.CameraH}
|
|
}
|
|
|
|
func doPlayerFrame(playerIndex uint8, player *Player, players map[uint32]*Player, gameMap *GameMap, bases map[uint32]*Base, bullets map[uint32]*Bullet, bulletParticles map[uint32]*BulletParticle) bool {
|
|
running := true
|
|
var (
|
|
shouldContinue bool
|
|
)
|
|
frameStart := sdl.GetTicks64()
|
|
|
|
// Profile handleEvents
|
|
player.totalHandleEventsTime += profileSection(func() {
|
|
running = handleEvents(player.window, player.logicalSurface)
|
|
keyboard := sdl.GetKeyboardState()
|
|
shouldContinue = handleInput(keyboard, bullets, player, gameMap, players)
|
|
running = running && shouldContinue
|
|
})
|
|
|
|
// Profile rendering (aggregate all renderings)
|
|
player.totalRenderTime += profileSection(func() {
|
|
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
|
|
renderHud(player, player.HUDSurface)
|
|
_ = player.playSurface.BlitScaled(player.playSurfaceRect, player.logicalSurface, player.playSurfaceTargetRect)
|
|
_ = player.HUDSurface.BlitScaled(player.HUDSurfaceRect, player.logicalSurface, player.HUDSurfaceTargetRect)
|
|
})
|
|
|
|
// Profile window adjustments
|
|
player.totalScalingTime += profileSection(func() {
|
|
adjustWindow(player.window, player.logicalSurface)
|
|
})
|
|
|
|
frameEnd := sdl.GetTicks64()
|
|
player.totalFrameTime += frameEnd - frameStart
|
|
player.frameCount++
|
|
|
|
// Log profiling information every 1000 frames
|
|
if player.frameCount%config.ProfilerInterval == 0 && config.ProfilerOn && playerIndex == 0 {
|
|
logProfilingInfo(player.totalHandleEventsTime, player.totalRenderTime, player.totalScalingTime, player.totalFrameTime, player.frameCount)
|
|
resetProfilingCounters(&player.totalHandleEventsTime, &player.totalRenderTime, &player.totalScalingTime, &player.totalFrameTime, &player.frameCount)
|
|
}
|
|
return running
|
|
}
|