feat(game-server): scaffold dedicated server toward player join
Build Game Server / build (push) Successful in 17s
Build Game Server / build (push) Successful in 17s
R&D branch for an authoritative Unreal game server (Prospect.Unreal).
The connection handshake already reaches PlayerController spawn; this
commit pushes it toward real players joining:
- Program.cs: target the Cycle station map + game mode (configurable via
PROSPECT_MAP/GAMEMODE/PORT), proper host loop, and initialise a
GameSession so NMT_Join -> Login no longer fails ("GameSession is null").
- UWorld.WelcomePlayer: send the world's real map/game mode to the client
instead of the UE ThirdPerson template.
- Dockerfile.gameserver + .gitea/workflows/game-server.yml: build/push a
separate image (the-cycle-game) on the game-server branch, ntfy notify.
- GAMESERVER.md: honest state + roadmap (player spawn + movement
replication need live client testing; AI/loot intentionally deferred).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using Prospect.Unreal.Core;
|
||||
using Prospect.Unreal.Core;
|
||||
using Prospect.Unreal.Net.Actors;
|
||||
using Prospect.Unreal.Runtime;
|
||||
using Serilog;
|
||||
|
||||
@@ -7,10 +8,10 @@ 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) =>
|
||||
@@ -18,37 +19,55 @@ internal static class Program
|
||||
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();
|
||||
|
||||
Logger.Information("Starting Prospect.Server.Game");
|
||||
|
||||
// Prospect:
|
||||
// Map: /Game/Maps/MP/Station/Station_P
|
||||
// GameMode: /Script/Prospect/YGameMode_Station
|
||||
|
||||
var worldUrl = new FUrl
|
||||
{
|
||||
Map = "/Game/ThirdPersonCPP/Maps/ThirdPersonExampleMap"
|
||||
};
|
||||
|
||||
// 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);
|
||||
world.Listen();
|
||||
|
||||
|
||||
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())
|
||||
{
|
||||
world.Tick(TickRate);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Logger.Information("Shutting down");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user