Server: add basic replay recording

Signed-off-by: Paul Chote <pchote@users.noreply.github.com>
This commit is contained in:
Clément Bœsch
2020-10-09 23:44:19 +01:00
committed by abcdefg30
parent dd18829def
commit e5da58e2b4
6 changed files with 105 additions and 4 deletions

View File

@@ -72,10 +72,10 @@ namespace OpenRA
return om; return om;
} }
static string TimestampedFilename(bool includemilliseconds = false) public static string TimestampedFilename(bool includemilliseconds = false, string extra = "")
{ {
var format = includemilliseconds ? "yyyy-MM-ddTHHmmssfffZ" : "yyyy-MM-ddTHHmmssZ"; var format = includemilliseconds ? "yyyy-MM-ddTHHmmssfffZ" : "yyyy-MM-ddTHHmmssZ";
return ModData.Manifest.Id + "-" + DateTime.UtcNow.ToString(format, CultureInfo.InvariantCulture); return ModData.Manifest.Id + extra + "-" + DateTime.UtcNow.ToString(format, CultureInfo.InvariantCulture);
} }
static void JoinInner(OrderManager om) static void JoinInner(OrderManager om)

View File

@@ -10,6 +10,7 @@
#endregion #endregion
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using OpenRA.FileFormats; using OpenRA.FileFormats;
@@ -85,6 +86,14 @@ namespace OpenRA.Network
writer.Write(data); writer.Write(data);
} }
public void ReceiveFrame(int clientID, int frame, byte[] data)
{
var ms = new MemoryStream(4 + data.Length);
ms.WriteArray(BitConverter.GetBytes(frame));
ms.WriteArray(data);
Receive(clientID, ms.GetBuffer());
}
bool disposed; bool disposed;
public void Dispose() public void Dispose()

View File

@@ -18,9 +18,11 @@ using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using OpenRA.FileFormats;
using OpenRA.Network; using OpenRA.Network;
using OpenRA.Primitives; using OpenRA.Primitives;
using OpenRA.Support; using OpenRA.Support;
using OpenRA.Traits;
namespace OpenRA.Server namespace OpenRA.Server
{ {
@@ -70,6 +72,10 @@ namespace OpenRA.Server
volatile ActionQueue delayedActions = new ActionQueue(); volatile ActionQueue delayedActions = new ActionQueue();
int waitingForAuthenticationCallback = 0; int waitingForAuthenticationCallback = 0;
ReplayRecorder recorder;
GameInformation gameInfo;
readonly List<GameInformation.Player> worldPlayers = new List<GameInformation.Player>();
public ServerState State public ServerState State
{ {
get { return internalState; } get { return internalState; }
@@ -123,6 +129,42 @@ namespace OpenRA.Server
{ {
foreach (var t in serverTraits.WithInterface<IEndGame>()) foreach (var t in serverTraits.WithInterface<IEndGame>())
t.GameEnded(this); t.GameEnded(this);
recorder?.Dispose();
recorder = null;
}
// Craft a fake handshake request/response because that's the
// only way to expose the Version and OrdersProtocol.
public void RecordFakeHandshake()
{
var request = new HandshakeRequest
{
Mod = ModData.Manifest.Id,
Version = ModData.Manifest.Metadata.Version,
};
recorder.ReceiveFrame(0, 0, new Order("HandshakeRequest", null, false)
{
Type = OrderType.Handshake,
IsImmediate = true,
TargetString = request.Serialize(),
}.Serialize());
var response = new HandshakeResponse()
{
Mod = ModData.Manifest.Id,
Version = ModData.Manifest.Metadata.Version,
OrdersProtocol = ProtocolVersion.Orders,
Client = new Session.Client(),
};
recorder.ReceiveFrame(0, 0, new Order("HandshakeResponse", null, false)
{
Type = OrderType.Handshake,
IsImmediate = true,
TargetString = response.Serialize(),
}.Serialize());
} }
public Server(List<IPEndPoint> endpoints, ServerSettings settings, ModData modData, ServerType type) public Server(List<IPEndPoint> endpoints, ServerSettings settings, ModData modData, ServerType type)
@@ -198,6 +240,15 @@ namespace OpenRA.Server
} }
}; };
if (Settings.RecordReplays && Type == ServerType.Dedicated)
{
recorder = new ReplayRecorder(() => { return Game.TimestampedFilename(extra: "-Server"); });
// We only need one handshake to initialize the replay.
// Add it now, then ignore the redundant handshakes from each client
RecordFakeHandshake();
}
new Thread(_ => new Thread(_ =>
{ {
foreach (var t in serverTraits.WithInterface<INotifyServerStart>()) foreach (var t in serverTraits.WithInterface<INotifyServerStart>())
@@ -633,6 +684,9 @@ namespace OpenRA.Server
var from = conn != null ? conn.PlayerIndex : 0; var from = conn != null ? conn.PlayerIndex : 0;
foreach (var c in Conns.Except(conn).ToList()) foreach (var c in Conns.Except(conn).ToList())
DispatchOrdersToClient(c, from, frame, data); DispatchOrdersToClient(c, from, frame, data);
if (recorder != null)
recorder.ReceiveFrame(from, frame, data);
} }
public void DispatchOrders(Connection conn, int frame, byte[] data) public void DispatchOrders(Connection conn, int frame, byte[] data)
@@ -1034,12 +1088,44 @@ namespace OpenRA.Server
// TODO: Enable for multiplayer (non-dedicated servers only) once the lobby UI has been created // TODO: Enable for multiplayer (non-dedicated servers only) once the lobby UI has been created
LobbyInfo.GlobalSettings.GameSavesEnabled = Type != ServerType.Dedicated && LobbyInfo.NonBotClients.Count() == 1; LobbyInfo.GlobalSettings.GameSavesEnabled = Type != ServerType.Dedicated && LobbyInfo.NonBotClients.Count() == 1;
// Player list for win/loss tracking
// HACK: NonCombatant and non-Playable players are set to null to simplify replay tracking
// The null padding is needed to keep the player indexes in sync with world.Players on the clients
// This will need to change if future code wants to use worldPlayers for other purposes
foreach (var cmpi in Map.Rules.Actors["world"].TraitInfos<ICreatePlayersInfo>())
cmpi.CreateServerPlayers(Map, LobbyInfo, worldPlayers);
if (recorder != null)
{
gameInfo = new GameInformation
{
Mod = Game.ModData.Manifest.Id,
Version = Game.ModData.Manifest.Metadata.Version,
MapUid = Map.Uid,
MapTitle = Map.Title,
StartTimeUtc = DateTime.UtcNow,
};
// Replay metadata should only include the playable players
foreach (var p in worldPlayers)
if (p != null)
gameInfo.Players.Add(p);
recorder.Metadata = new ReplayMetadata(gameInfo);
}
SyncLobbyInfo(); SyncLobbyInfo();
State = ServerState.GameStarted; State = ServerState.GameStarted;
var disconnectData = new[] { (byte)OrderType.Disconnect };
foreach (var c in Conns) foreach (var c in Conns)
{
foreach (var d in Conns) foreach (var d in Conns)
DispatchOrdersToClient(c, d.PlayerIndex, int.MaxValue, new[] { (byte)OrderType.Disconnect }); DispatchOrdersToClient(c, d.PlayerIndex, int.MaxValue, disconnectData);
if (recorder != null)
recorder.ReceiveFrame(c.PlayerIndex, int.MaxValue, disconnectData);
}
if (GameSave == null && LobbyInfo.GlobalSettings.GameSavesEnabled) if (GameSave == null && LobbyInfo.GlobalSettings.GameSavesEnabled)
GameSave = new GameSave(); GameSave = new GameSave();

View File

@@ -88,6 +88,9 @@ namespace OpenRA
[Desc("Allow clients to see the country of other clients.")] [Desc("Allow clients to see the country of other clients.")]
public bool EnableGeoIP = true; public bool EnableGeoIP = true;
[Desc("For dedicated servers only, save replays for all games played.")]
public bool RecordReplays = false;
public ServerSettings Clone() public ServerSettings Clone()
{ {
return (ServerSettings)MemberwiseClone(); return (ServerSettings)MemberwiseClone();

View File

@@ -7,6 +7,7 @@ set Mod=ra
set ListenPort=1234 set ListenPort=1234
set AdvertiseOnline=True set AdvertiseOnline=True
set Password="" set Password=""
set RecordReplays=False
set RequireAuthentication=False set RequireAuthentication=False
set ProfileIDBlacklist="" set ProfileIDBlacklist=""
@@ -21,6 +22,6 @@ set SupportDir=""
:loop :loop
OpenRA.Server.exe Game.Mod=%Mod% Server.Name=%Name% Server.ListenPort=%ListenPort% Server.AdvertiseOnline=%AdvertiseOnline% Server.EnableSingleplayer=%EnableSingleplayer% Server.Password=%Password% Server.RequireAuthentication=%RequireAuthentication% Server.ProfileIDBlacklist=%ProfileIDBlacklist% Server.ProfileIDWhitelist=%ProfileIDWhitelist% Server.EnableSyncReports=%EnableSyncReports% Server.EnableGeoIP=%EnableGeoIP% Server.ShareAnonymizedIPs=%ShareAnonymizedIPs% Engine.SupportDir=%SupportDir% OpenRA.Server.exe Game.Mod=%Mod% Server.Name=%Name% Server.ListenPort=%ListenPort% Server.AdvertiseOnline=%AdvertiseOnline% Server.EnableSingleplayer=%EnableSingleplayer% Server.Password=%Password% Server.RecordReplays=%RecordReplays% Server.RequireAuthentication=%RequireAuthentication% Server.ProfileIDBlacklist=%ProfileIDBlacklist% Server.ProfileIDWhitelist=%ProfileIDWhitelist% Server.EnableSyncReports=%EnableSyncReports% Server.EnableGeoIP=%EnableGeoIP% Server.ShareAnonymizedIPs=%ShareAnonymizedIPs% Engine.SupportDir=%SupportDir%
goto loop goto loop

View File

@@ -11,6 +11,7 @@ Mod="${Mod:-"ra"}"
ListenPort="${ListenPort:-"1234"}" ListenPort="${ListenPort:-"1234"}"
AdvertiseOnline="${AdvertiseOnline:-"True"}" AdvertiseOnline="${AdvertiseOnline:-"True"}"
Password="${Password:-""}" Password="${Password:-""}"
RecordReplays="${RecordReplays:-"False"}"
RequireAuthentication="${RequireAuthentication:-"False"}" RequireAuthentication="${RequireAuthentication:-"False"}"
ProfileIDBlacklist="${ProfileIDBlacklist:-""}" ProfileIDBlacklist="${ProfileIDBlacklist:-""}"
@@ -30,6 +31,7 @@ while true; do
Server.AdvertiseOnline="$AdvertiseOnline" \ Server.AdvertiseOnline="$AdvertiseOnline" \
Server.EnableSingleplayer="$EnableSingleplayer" \ Server.EnableSingleplayer="$EnableSingleplayer" \
Server.Password="$Password" \ Server.Password="$Password" \
Server.RecordReplays="$RecordReplays" \
Server.GeoIPDatabase="$GeoIPDatabase" \ Server.GeoIPDatabase="$GeoIPDatabase" \
Server.RequireAuthentication="$RequireAuthentication" \ Server.RequireAuthentication="$RequireAuthentication" \
Server.ProfileIDBlacklist="$ProfileIDBlacklist" \ Server.ProfileIDBlacklist="$ProfileIDBlacklist" \