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 }