using Prospect.Unreal.Core; using Prospect.Unreal.Net.Actors; using Prospect.Unreal.Runtime; using Serilog; namespace Prospect.Server.Game; internal static class Program { private const float TickRate = (1000.0f / 60.0f) / 1000.0f; private static readonly ILogger Logger = Log.ForContext(typeof(Program)); private static readonly PeriodicTimer Tick = new PeriodicTimer(TimeSpan.FromSeconds(TickRate)); public static async Task Main() { Console.CancelKeyPress += (_, e) => { Tick.Dispose(); e.Cancel = true; }; Log.Logger = new LoggerConfiguration() .MinimumLevel.Verbose() .Enrich.FromLogContext() .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] ({SourceContext,-52}) {Message:lj}{NewLine}{Exception}") .CreateLogger(); // The Cycle: Frontier authoritative game server. // We start on the station map (the simplest shared space to get two players spawned // and moving); the raid maps (Bright Sands, …) come once spawn + movement replication // work end to end. AI and loot are intentionally out of scope for now. var map = Environment.GetEnvironmentVariable("PROSPECT_MAP") ?? "/Game/Maps/MP/Station/Station_P"; var gameMode = Environment.GetEnvironmentVariable("PROSPECT_GAMEMODE") ?? "/Script/Prospect/YGameMode_Station"; var port = int.TryParse(Environment.GetEnvironmentVariable("PROSPECT_PORT"), out var p) ? p : 7777; Logger.Information("Starting Prospect.Server.Game — map={Map} gameMode={GameMode} port={Port}", map, gameMode, port); var worldUrl = new FUrl { Map = map, Port = port }; worldUrl.Options.Add($"game={gameMode}"); await using (var world = new ProspectWorld()) { world.SetGameInstance(new UGameInstance()); world.SetGameMode(worldUrl); // The game mode needs a GameSession, otherwise NMT_Join -> SpawnPlayActor -> Login // fails with "GameSession is null" and the player is never spawned. var authGameMode = world.GetAuthGameMode(); if (authGameMode != null && authGameMode.GameSession == null) { authGameMode.GameSession = new AGameSession(); } world.InitializeActorsForPlay(worldUrl, true); if (!world.Listen()) { Logger.Fatal("Failed to start listening (port {Port} already in use?)", port); return; } Logger.Information("Server listening on :{Port}. Waiting for players…", port); while (await Tick.WaitForNextTickAsync()) { try { world.Tick(TickRate); } catch (Exception ex) { // A misaligned/incompatible client packet must not kill the whole server // (R&D: the client's exact net version isn't matched yet). Log and continue. Logger.Error(ex, "Tick error (bad packet?) — continuing"); } } } Logger.Information("Shutting down"); } }