Server: add basic replay recording
Signed-off-by: Paul Chote <pchote@users.noreply.github.com>
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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" \
|
||||||
|
|||||||
Reference in New Issue
Block a user