GOingTunneling/main.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
}