package main import ( "encoding/json" "fmt" "github.com/veandco/go-sdl2/sdl" "io/ioutil" "log" "net" "os" "sync" "time" ) type ServerConfig struct { MapWidth uint32 `json:"map_width"` MapHeight uint32 `json:"map_height"` BlastRadius uint32 `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"` } type Config struct { Debug bool `json:"debug"` MapWindow bool `json:"map_window"` 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"` ServerConfig ServerConfig `json:"server_config"` } func loadOrCreateConfig(filename string) (*Config, error) { config := &Config{ Debug: false, MapWindow: false, 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, 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, }, } // 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 = ioutil.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) 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 func main() { initializeSDL() defer sdl.Quit() configX, err := loadOrCreateConfig("config.json") if err != nil { log.Fatal(err) } serverConfig = configX.ServerConfig config = configX 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 = 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 conn.Close() go handleConnectionClient(conn, players, bases, bullets, bulletParticles) for !clientInitialized { time.Sleep(100 * time.Millisecond) } } defer closeThings(players) for playerIndex, player := range players { initPlayer(uint8(playerIndex), player) } 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 listen.Close() go func() { for { conn, err := listen.AcceptTCP() if err != nil { log.Fatal(err) } go handleRequest(conn, players, bullets, bases) } }() } if config.MapWindow { mapWindow, mapSurface = setupMapWindowAndSurface() } running := true var totalRenderTime, totalScalingTime, totalFrameTime, frameCount uint64 // 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() // 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) } // Run logic for the remaining delta time runGameLogic(players, gameMap, bases, bullets, bulletParticles) for playerIndex, player := range players { if player.local { running = doPlayerFrame(uint8(playerIndex), player, players, gameMap, bases, bullets, bulletParticles) if !running { break } } } // Render logic if config.MapWindow { // Profile rendering (aggregate all renderings) totalRenderTime += profileSection(func() { running = running && handleEvents(mapWindow, mapSurface) gameMap.render(mapRendererRect, mapSurface) for _, bullet := range bullets { (*bullet).render(mapRendererRect, mapSurface, bullets) } for _, base := range bases { (*base).render(mapRendererRect, mapSurface, bases) } for _, playerLoop := range players { (*playerLoop).render(mapRendererRect, mapSurface, players) } for _, bulletParticle := range bulletParticles { bulletParticle.render(mapRendererRect, mapSurface, bulletParticles) } }) 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, frameCount) resetMapProfilingCounters(&totalRenderTime, &totalScalingTime, &totalFrameTime, &frameCount) } 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 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() if fail { if player.connection != nil { (*player.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 } } } playerUpdateMutex.Lock() for _, bullet := range bullets { (*bullet).tick(gameMap, bulletParticles, bullets, players) } for _, player := range players { if player.local { continue } (*player).tick(bullets) } for _, base := range bases { (*base).tick(players) } for _, bulletParticle := range bulletParticles { bulletParticle.tick(bulletParticles) } playerUpdateMutex.Unlock() } 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) for _, bullet := range bullets { (*bullet).render(player.camera, player.playSurface, bullets) } for _, base := range bases { (*base).render(player.camera, player.playSurface, bases) } for _, playerLoop := range players { (*playerLoop).render(player.camera, player.playSurface, players) } for _, bulletParticle := range bulletParticles { bulletParticle.render(player.camera, player.playSurface, bulletParticles) } 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 }