Improve replay metadata and the replay browser
List of changes: * Better and more filters with new layout, for both mods. * Rename/Delete/Detele all functionality. * Simplified ReplayMetadata class considerably by introducing a new GameInformation data object. The new GameInformation class contains more information than previously available so the new solution is not compatible with old replays, meaning it can't read old replays. * Better and cleaner game information gathering in order to be written at the end of the replay file. * Revert changes to ReplayConnection, no longer necessary. * Better exception message on missing sprites and fonts. * New "SpawnOccupant" class that holds all the information needed by the MapPreviewWidget to visualize a spawn point. It was using Session.Client before and it was necessary to separate it to be able to show information not available at lobby time. * Fix keyboard focus UI bug when closing a window would not remove focus.
This commit is contained in:
@@ -22,67 +22,33 @@ namespace OpenRA.FileFormats
|
||||
public const int MetaEndMarker = -2;
|
||||
public const int MetaVersion = 0x00000001;
|
||||
|
||||
public readonly GameInformation GameInfo;
|
||||
public string FilePath { get; private set; }
|
||||
public DateTime EndTimestampUtc { get; private set; }
|
||||
public TimeSpan Duration { get { return EndTimestampUtc - StartTimestampUtc; } }
|
||||
public WinState Outcome { get; private set; }
|
||||
|
||||
public readonly Lazy<Session> LobbyInfo;
|
||||
public readonly DateTime StartTimestampUtc;
|
||||
readonly string lobbyInfoData;
|
||||
|
||||
ReplayMetadata()
|
||||
public ReplayMetadata(GameInformation info)
|
||||
{
|
||||
Outcome = WinState.Undefined;
|
||||
if (info == null)
|
||||
throw new ArgumentNullException("info");
|
||||
|
||||
GameInfo = info;
|
||||
}
|
||||
|
||||
public ReplayMetadata(DateTime startGameTimestampUtc, Session lobbyInfo)
|
||||
: this()
|
||||
ReplayMetadata(BinaryReader reader, string path)
|
||||
{
|
||||
if (startGameTimestampUtc.Kind == DateTimeKind.Unspecified)
|
||||
throw new ArgumentException("The 'Kind' property of the timestamp must be specified", "startGameTimestamp");
|
||||
FilePath = path;
|
||||
|
||||
StartTimestampUtc = startGameTimestampUtc.ToUniversalTime();
|
||||
|
||||
lobbyInfoData = lobbyInfo.Serialize();
|
||||
LobbyInfo = Exts.Lazy(() => Session.Deserialize(this.lobbyInfoData));
|
||||
}
|
||||
|
||||
public void FinalizeReplayMetadata(DateTime endGameTimestampUtc, WinState outcome)
|
||||
{
|
||||
if (endGameTimestampUtc.Kind == DateTimeKind.Unspecified)
|
||||
throw new ArgumentException("The 'Kind' property of the timestamp must be specified", "endGameTimestampUtc");
|
||||
EndTimestampUtc = endGameTimestampUtc.ToUniversalTime();
|
||||
|
||||
Outcome = outcome;
|
||||
}
|
||||
|
||||
ReplayMetadata(BinaryReader reader)
|
||||
: this()
|
||||
{
|
||||
// Read start marker
|
||||
if (reader.ReadInt32() != MetaStartMarker)
|
||||
throw new InvalidOperationException("Expected MetaStartMarker but found an invalid value.");
|
||||
|
||||
// Read version
|
||||
var version = reader.ReadInt32();
|
||||
if (version > MetaVersion)
|
||||
if (version != MetaVersion)
|
||||
throw new NotSupportedException("Metadata version {0} is not supported".F(version));
|
||||
|
||||
// Read start game timestamp
|
||||
StartTimestampUtc = new DateTime(reader.ReadInt64(), DateTimeKind.Utc);
|
||||
|
||||
// Read end game timestamp
|
||||
EndTimestampUtc = new DateTime(reader.ReadInt64(), DateTimeKind.Utc);
|
||||
|
||||
// Read game outcome
|
||||
WinState outcome;
|
||||
if (Enum.TryParse(ReadUtf8String(reader), true, out outcome))
|
||||
Outcome = outcome;
|
||||
|
||||
// Read lobby info
|
||||
lobbyInfoData = ReadUtf8String(reader);
|
||||
LobbyInfo = Exts.Lazy(() => Session.Deserialize(this.lobbyInfoData));
|
||||
// Read game info
|
||||
string data = ReadUtf8String(reader);
|
||||
GameInfo = GameInformation.Deserialize(data);
|
||||
}
|
||||
|
||||
public void Write(BinaryWriter writer)
|
||||
@@ -94,19 +60,8 @@ namespace OpenRA.FileFormats
|
||||
// Write data
|
||||
int dataLength = 0;
|
||||
{
|
||||
// Write start game timestamp
|
||||
writer.Write(StartTimestampUtc.Ticks);
|
||||
dataLength += sizeof(long);
|
||||
|
||||
// Write end game timestamp
|
||||
writer.Write(EndTimestampUtc.Ticks);
|
||||
dataLength += sizeof(long);
|
||||
|
||||
// Write game outcome
|
||||
dataLength += WriteUtf8String(writer, Outcome.ToString());
|
||||
|
||||
// Write lobby info data
|
||||
dataLength += WriteUtf8String(writer, lobbyInfoData);
|
||||
dataLength += WriteUtf8String(writer, GameInfo.Serialize());
|
||||
}
|
||||
|
||||
// Write total length & end marker
|
||||
@@ -114,76 +69,43 @@ namespace OpenRA.FileFormats
|
||||
writer.Write(MetaEndMarker);
|
||||
}
|
||||
|
||||
public static ReplayMetadata Read(string path, bool enableFallbackMethod = true)
|
||||
public void RenameFile(string newFilenameWithoutExtension)
|
||||
{
|
||||
Func<DateTime> timestampProvider = () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return File.GetCreationTimeUtc(path);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return DateTime.MinValue;
|
||||
}
|
||||
};
|
||||
|
||||
using (var fs = new FileStream(path, FileMode.Open))
|
||||
{
|
||||
var o = Read(fs, enableFallbackMethod, timestampProvider);
|
||||
if (o != null)
|
||||
o.FilePath = path;
|
||||
return o;
|
||||
}
|
||||
var newPath = Path.Combine(Path.GetDirectoryName(FilePath), newFilenameWithoutExtension) + ".rep";
|
||||
File.Move(FilePath, newPath);
|
||||
FilePath = newPath;
|
||||
}
|
||||
|
||||
static ReplayMetadata Read(FileStream fs, bool enableFallbackMethod, Func<DateTime> fallbackTimestampProvider)
|
||||
public static ReplayMetadata Read(string path)
|
||||
{
|
||||
using (var fs = new FileStream(path, FileMode.Open))
|
||||
return Read(fs, path);
|
||||
}
|
||||
|
||||
static ReplayMetadata Read(FileStream fs, string path)
|
||||
{
|
||||
if (!fs.CanSeek)
|
||||
return null;
|
||||
|
||||
fs.Seek(-(4 + 4), SeekOrigin.End);
|
||||
using (var reader = new BinaryReader(fs))
|
||||
{
|
||||
// Disposing the BinaryReader will dispose the underlying stream
|
||||
// and we don't want that because ReplayConnection may use the
|
||||
// stream as well.
|
||||
//
|
||||
// Fixed in .NET 4.5.
|
||||
// See: http://msdn.microsoft.com/en-us/library/gg712804%28v=vs.110%29.aspx
|
||||
|
||||
if (fs.CanSeek)
|
||||
var dataLength = reader.ReadInt32();
|
||||
if (reader.ReadInt32() == MetaEndMarker)
|
||||
{
|
||||
fs.Seek(-(4 + 4), SeekOrigin.End);
|
||||
var dataLength = reader.ReadInt32();
|
||||
if (reader.ReadInt32() == MetaEndMarker)
|
||||
// go back end marker + length storage + data + version + start marker
|
||||
fs.Seek(-(4 + 4 + dataLength + 4 + 4), SeekOrigin.Current);
|
||||
try
|
||||
{
|
||||
// go back end marker + length storage + data + version + start marker
|
||||
fs.Seek(-(4 + 4 + dataLength + 4 + 4), SeekOrigin.Current);
|
||||
try
|
||||
{
|
||||
return new ReplayMetadata(reader);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
Log.Write("debug", ex.ToString());
|
||||
}
|
||||
catch (NotSupportedException ex)
|
||||
{
|
||||
Log.Write("debug", ex.ToString());
|
||||
}
|
||||
return new ReplayMetadata(reader, path);
|
||||
}
|
||||
|
||||
// Reset the stream position or the ReplayConnection will fail later
|
||||
fs.Seek(0, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
if (enableFallbackMethod)
|
||||
{
|
||||
using (var conn = new ReplayConnection(fs))
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
var replay = new ReplayMetadata(fallbackTimestampProvider(), conn.LobbyInfo);
|
||||
if (conn.TickCount == 0)
|
||||
return null;
|
||||
var seconds = (int)Math.Ceiling((conn.TickCount * Game.NetTickScale) / 25f);
|
||||
replay.EndTimestampUtc = replay.StartTimestampUtc.AddSeconds(seconds);
|
||||
return replay;
|
||||
Log.Write("debug", ex.ToString());
|
||||
}
|
||||
catch (NotSupportedException ex)
|
||||
{
|
||||
Log.Write("debug", ex.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -210,10 +132,5 @@ namespace OpenRA.FileFormats
|
||||
{
|
||||
return Encoding.UTF8.GetString(reader.ReadBytes(reader.ReadInt32()));
|
||||
}
|
||||
|
||||
public MapPreview MapPreview
|
||||
{
|
||||
get { return Game.modData.MapCache[LobbyInfo.Value.GlobalSettings.Map]; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,10 +263,6 @@ namespace OpenRA
|
||||
using (new PerfTimer("LoadComplete"))
|
||||
orderManager.world.LoadComplete(worldRenderer);
|
||||
|
||||
var rc = orderManager.Connection as ReplayRecorderConnection;
|
||||
if (rc != null)
|
||||
rc.Metadata = new OpenRA.FileFormats.ReplayMetadata(DateTime.UtcNow, orderManager.LobbyInfo);
|
||||
|
||||
if (orderManager.GameStarted)
|
||||
return;
|
||||
|
||||
|
||||
230
OpenRA.Game/GameInformation.cs
Normal file
230
OpenRA.Game/GameInformation.cs
Normal file
@@ -0,0 +1,230 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2014 The OpenRA Developers (see AUTHORS)
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation. For more information,
|
||||
* see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenRA.Graphics;
|
||||
using OpenRA.Network;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains information about a finished game
|
||||
/// </summary>
|
||||
public class GameInformation
|
||||
{
|
||||
/// <summary>The map identifier.</summary>
|
||||
public string MapUid;
|
||||
/// <summary>The map title.</summary>
|
||||
public string MapTitle;
|
||||
/// <summary>Game start timestamp.</summary>
|
||||
public DateTime StartTimeUtc;
|
||||
/// <summary>Game end timestamp (when the recoding stopped).</summary>
|
||||
public DateTime EndTimeUtc;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the game's duration, from the time the game started until the
|
||||
/// replay recording stopped.
|
||||
/// </summary>
|
||||
/// <value>The game's duration.</value>
|
||||
public TimeSpan Duration { get { return EndTimeUtc > StartTimeUtc ? EndTimeUtc - StartTimeUtc : TimeSpan.Zero; } }
|
||||
/// <summary>
|
||||
/// Gets the list of players.
|
||||
/// </summary>
|
||||
/// <value>The players.</value>
|
||||
public IList<Player> Players { get; private set; }
|
||||
/// <summary>
|
||||
/// Gets the map preview, using <see cref="Game.modData.MapCache"/> and the <see cref="MapUid"/>.
|
||||
/// </summary>
|
||||
/// <value>The map preview.</value>
|
||||
public MapPreview MapPreview { get { return Game.modData.MapCache[MapUid]; } }
|
||||
/// <summary>
|
||||
/// Gets the human players.
|
||||
/// </summary>
|
||||
/// <value>The human players.</value>
|
||||
public IEnumerable<Player> HumanPlayers { get { return Players.Where(p => p.IsHuman); } }
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance has just one human player.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance has just one human player; otherwise, <c>false</c>.</value>
|
||||
public bool IsSinglePlayer { get { return HumanPlayers.Count() == 1; } }
|
||||
|
||||
Dictionary<OpenRA.Player, Player> playersByRuntime;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the class.
|
||||
/// </summary>
|
||||
public GameInformation()
|
||||
{
|
||||
Players = new List<Player>();
|
||||
playersByRuntime = new Dictionary<OpenRA.Player, Player>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize the specified data into a new instance.
|
||||
/// </summary>
|
||||
/// <param name="data">Data.</param>
|
||||
public static GameInformation Deserialize(string data)
|
||||
{
|
||||
try
|
||||
{
|
||||
var info = new GameInformation();
|
||||
|
||||
var nodes = MiniYaml.FromString(data);
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
var keyParts = node.Key.Split('@');
|
||||
|
||||
switch (keyParts[0])
|
||||
{
|
||||
case "Root":
|
||||
FieldLoader.Load(info, node.Value);
|
||||
break;
|
||||
|
||||
case "Player":
|
||||
info.Players.Add(FieldLoader.Load<Player>(node.Value));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
Log.Write("exception", "GameInformation deserialized invalid MiniYaml:\n{0}".F(data));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize this instance.
|
||||
/// </summary>
|
||||
public string Serialize()
|
||||
{
|
||||
var nodes = new List<MiniYamlNode>();
|
||||
|
||||
nodes.Add(new MiniYamlNode("Root", FieldSaver.Save(this)));
|
||||
|
||||
for (var i=0; i<Players.Count; i++)
|
||||
nodes.Add(new MiniYamlNode("Player@{0}".F(i), FieldSaver.Save(Players[i])));
|
||||
|
||||
return nodes.WriteToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the start-up player information.
|
||||
/// </summary>
|
||||
/// <param name="runtimePlayer">Runtime player.</param>
|
||||
/// <param name="lobbyInfo">Lobby info.</param>
|
||||
public void AddPlayer(OpenRA.Player runtimePlayer, Session lobbyInfo)
|
||||
{
|
||||
if (runtimePlayer == null)
|
||||
throw new ArgumentNullException("runtimePlayer");
|
||||
|
||||
if (lobbyInfo == null)
|
||||
throw new ArgumentNullException("lobbyInfo");
|
||||
|
||||
// We don't care about spectators and map players
|
||||
if (runtimePlayer.NonCombatant || !runtimePlayer.Playable)
|
||||
return;
|
||||
|
||||
// Find the lobby client that created the runtime player
|
||||
var client = lobbyInfo.ClientWithIndex(runtimePlayer.ClientIndex);
|
||||
if (client == null)
|
||||
return;
|
||||
|
||||
var player = new Player
|
||||
{
|
||||
ClientIndex = runtimePlayer.ClientIndex,
|
||||
Name = runtimePlayer.PlayerName,
|
||||
IsHuman = !runtimePlayer.IsBot,
|
||||
IsBot = runtimePlayer.IsBot,
|
||||
FactionName = runtimePlayer.Country.Name,
|
||||
FactionId = runtimePlayer.Country.Race,
|
||||
Color = runtimePlayer.Color,
|
||||
Team = client.Team,
|
||||
SpawnPoint = runtimePlayer.SpawnPoint,
|
||||
IsRandomFaction = runtimePlayer.Country.Race != client.Country,
|
||||
IsRandomSpawnPoint = runtimePlayer.SpawnPoint != client.SpawnPoint
|
||||
};
|
||||
|
||||
playersByRuntime.Add(runtimePlayer, player);
|
||||
Players.Add(player);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the player information for the specified runtime player instance.
|
||||
/// </summary>
|
||||
/// <returns>The player, or <c>null</c>.</returns>
|
||||
/// <param name="runtimePlayer">Runtime player.</param>
|
||||
public Player GetPlayer(OpenRA.Player runtimePlayer)
|
||||
{
|
||||
Player player;
|
||||
|
||||
playersByRuntime.TryGetValue(runtimePlayer, out player);
|
||||
|
||||
return player;
|
||||
}
|
||||
|
||||
/// <summary>Specifies whether the player was defeated, victorious, or there was no outcome defined.</summary>
|
||||
public enum GameOutcome
|
||||
{
|
||||
/// <summary>Unknown outcome.</summary>
|
||||
Undefined,
|
||||
/// <summary>The player was defeated</summary>
|
||||
Defeat,
|
||||
/// <summary>The player was victorious</summary>
|
||||
Victory
|
||||
}
|
||||
|
||||
///<summary>
|
||||
/// Information about a player
|
||||
/// </summary>
|
||||
public class Player
|
||||
{
|
||||
//
|
||||
// Start-up information
|
||||
//
|
||||
|
||||
/// <summary>The client index.</summary>
|
||||
public int ClientIndex;
|
||||
/// <summary>The player name, not guaranteed to be unique.</summary>
|
||||
public string Name;
|
||||
/// <summary><c>true</c> if the player is a human player; otherwise, <c>false</c>.</summary>
|
||||
public bool IsHuman;
|
||||
/// <summary><c>true</c> if the player is a bot; otherwise, <c>false</c>.</summary>
|
||||
public bool IsBot;
|
||||
/// <summary>The faction name (aka Country).</summary>
|
||||
public string FactionName;
|
||||
/// <summary>The faction id (aka Country, aka Race).</summary>
|
||||
public string FactionId;
|
||||
/// <summary>The color used by the player in the game.</summary>
|
||||
public HSLColor Color;
|
||||
/// <summary>The team id on start-up, or 0 if the player is not part of the team.</summary>
|
||||
public int Team;
|
||||
/// <summary>The index of the spawn point on the map, or 0 if the player is not part of the team.</summary>
|
||||
public int SpawnPoint;
|
||||
/// <summary><c>true</c> if the faction was chosen at random; otherwise, <c>false</c>.</summary>
|
||||
public bool IsRandomFaction;
|
||||
/// <summary><c>true</c> if the spawn point was chosen at random; otherwise, <c>false</c>.</summary>
|
||||
public bool IsRandomSpawnPoint;
|
||||
|
||||
//
|
||||
// Information gathered at a later stage
|
||||
//
|
||||
|
||||
/// <summary>The game outcome for this player.</summary>
|
||||
public GameOutcome Outcome;
|
||||
/// <summary>The time when this player won or lost the game.</summary>
|
||||
public DateTime OutcomeTimestampUtc;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,14 +72,23 @@ namespace OpenRA.Graphics
|
||||
collections.Add(name, collection);
|
||||
}
|
||||
|
||||
public static Sprite GetImage(string collection, string image)
|
||||
public static Sprite GetImage(string collectionName, string imageName)
|
||||
{
|
||||
// Cached sprite
|
||||
if (cachedSprites.ContainsKey(collection) && cachedSprites[collection].ContainsKey(image))
|
||||
return cachedSprites[collection][image];
|
||||
Dictionary<string, Sprite> cachedCollection;
|
||||
Sprite sprite;
|
||||
if (cachedSprites.TryGetValue(collectionName, out cachedCollection) && cachedCollection.TryGetValue(imageName, out sprite))
|
||||
return sprite;
|
||||
|
||||
Collection collection;
|
||||
if (!collections.TryGetValue(collectionName, out collection))
|
||||
{
|
||||
Log.Write("debug", "Could not find collection '{0}'", collectionName);
|
||||
return null;
|
||||
}
|
||||
|
||||
MappedImage mi;
|
||||
if (!collections[collection].regions.TryGetValue(image, out mi))
|
||||
if (!collection.regions.TryGetValue(imageName, out mi))
|
||||
return null;
|
||||
|
||||
// Cached sheet
|
||||
@@ -93,11 +102,15 @@ namespace OpenRA.Graphics
|
||||
}
|
||||
|
||||
// Cache the sprite
|
||||
if (!cachedSprites.ContainsKey(collection))
|
||||
cachedSprites.Add(collection, new Dictionary<string, Sprite>());
|
||||
cachedSprites[collection].Add(image, mi.GetImage(sheet));
|
||||
if (cachedCollection == null)
|
||||
{
|
||||
cachedCollection = new Dictionary<string, Sprite>();
|
||||
cachedSprites.Add(collectionName, cachedCollection);
|
||||
}
|
||||
var image = mi.GetImage(sheet);
|
||||
cachedCollection.Add(imageName, image);
|
||||
|
||||
return cachedSprites[collection][image];
|
||||
return image;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using OpenRA.FileFormats;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA.Network
|
||||
@@ -34,60 +35,52 @@ namespace OpenRA.Network
|
||||
public readonly Session LobbyInfo;
|
||||
|
||||
public ReplayConnection(string replayFilename)
|
||||
{
|
||||
using (var rs = File.OpenRead(replayFilename))
|
||||
Read(rs, ref TickCount, ref IsValid, ref LobbyInfo);
|
||||
|
||||
ordersFrame = LobbyInfo.GlobalSettings.OrderLatency;
|
||||
}
|
||||
|
||||
public ReplayConnection(FileStream rs)
|
||||
{
|
||||
Read(rs, ref TickCount, ref IsValid, ref LobbyInfo);
|
||||
}
|
||||
|
||||
void Read(FileStream rs, ref int TickCount, ref bool IsValid, ref Session LobbyInfo)
|
||||
{
|
||||
// Parse replay data into a struct that can be fed to the game in chunks
|
||||
// to avoid issues with all immediate orders being resolved on the first tick.
|
||||
var chunk = new Chunk();
|
||||
|
||||
while (rs.Position < rs.Length)
|
||||
using (var rs = File.OpenRead(replayFilename))
|
||||
{
|
||||
var client = rs.ReadInt32();
|
||||
if (client == FileFormats.ReplayMetadata.MetaStartMarker)
|
||||
break;
|
||||
var packetLen = rs.ReadInt32();
|
||||
var packet = rs.ReadBytes(packetLen);
|
||||
var frame = BitConverter.ToInt32(packet, 0);
|
||||
chunk.Packets.Add(Pair.New(client, packet));
|
||||
var chunk = new Chunk();
|
||||
|
||||
if (packet.Length == 5 && packet[4] == 0xBF)
|
||||
continue; // disconnect
|
||||
else if (packet.Length >= 5 && packet[4] == 0x65)
|
||||
continue; // sync
|
||||
else if (frame == 0)
|
||||
while (rs.Position < rs.Length)
|
||||
{
|
||||
// Parse replay metadata from orders stream
|
||||
var orders = packet.ToOrderList(null);
|
||||
foreach (var o in orders)
|
||||
var client = rs.ReadInt32();
|
||||
if (client == ReplayMetadata.MetaStartMarker)
|
||||
break;
|
||||
var packetLen = rs.ReadInt32();
|
||||
var packet = rs.ReadBytes(packetLen);
|
||||
var frame = BitConverter.ToInt32(packet, 0);
|
||||
chunk.Packets.Add(Pair.New(client, packet));
|
||||
|
||||
if (packet.Length == 5 && packet[4] == 0xBF)
|
||||
continue; // disconnect
|
||||
else if (packet.Length >= 5 && packet[4] == 0x65)
|
||||
continue; // sync
|
||||
else if (frame == 0)
|
||||
{
|
||||
if (o.OrderString == "StartGame")
|
||||
IsValid = true;
|
||||
else if (o.OrderString == "SyncInfo" && !IsValid)
|
||||
LobbyInfo = Session.Deserialize(o.TargetString);
|
||||
// Parse replay metadata from orders stream
|
||||
var orders = packet.ToOrderList(null);
|
||||
foreach (var o in orders)
|
||||
{
|
||||
if (o.OrderString == "StartGame")
|
||||
IsValid = true;
|
||||
else if (o.OrderString == "SyncInfo" && !IsValid)
|
||||
LobbyInfo = Session.Deserialize(o.TargetString);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Regular order - finalize the chunk
|
||||
chunk.Frame = frame;
|
||||
chunks.Enqueue(chunk);
|
||||
chunk = new Chunk();
|
||||
|
||||
TickCount = Math.Max(TickCount, frame);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Regular order - finalize the chunk
|
||||
chunk.Frame = frame;
|
||||
chunks.Enqueue(chunk);
|
||||
chunk = new Chunk();
|
||||
|
||||
TickCount = Math.Max(TickCount, frame);
|
||||
}
|
||||
}
|
||||
|
||||
ordersFrame = LobbyInfo.GlobalSettings.OrderLatency;
|
||||
}
|
||||
|
||||
// Do nothing: ignore locally generated orders
|
||||
|
||||
@@ -20,7 +20,6 @@ namespace OpenRA.Network
|
||||
class ReplayRecorderConnection : IConnection
|
||||
{
|
||||
public ReplayMetadata Metadata;
|
||||
public WinState LocalGameState = WinState.Undefined;
|
||||
|
||||
IConnection inner;
|
||||
BinaryWriter writer;
|
||||
@@ -107,7 +106,8 @@ namespace OpenRA.Network
|
||||
|
||||
if (Metadata != null)
|
||||
{
|
||||
Metadata.FinalizeReplayMetadata(DateTime.UtcNow, LocalGameState);
|
||||
if (Metadata.GameInfo != null)
|
||||
Metadata.GameInfo.EndTimeUtc = DateTime.UtcNow;
|
||||
Metadata.Write(writer);
|
||||
}
|
||||
|
||||
|
||||
@@ -248,6 +248,7 @@
|
||||
<Compile Include="GameRules\Ruleset.cs" />
|
||||
<Compile Include="GameRules\RulesetCache.cs" />
|
||||
<Compile Include="Support\MersenneTwister.cs" />
|
||||
<Compile Include="GameInformation.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="FileSystem\D2kSoundResources.cs" />
|
||||
|
||||
@@ -41,6 +41,7 @@ namespace OpenRA
|
||||
public readonly int ClientIndex;
|
||||
public readonly PlayerReference PlayerReference;
|
||||
public bool IsBot;
|
||||
public int SpawnPoint;
|
||||
|
||||
public Shroud Shroud;
|
||||
public World World { get; private set; }
|
||||
|
||||
@@ -41,9 +41,12 @@ namespace OpenRA.Widgets
|
||||
{
|
||||
var name = GetImageName();
|
||||
var collection = GetImageCollection();
|
||||
WidgetUtils.DrawRGBA(
|
||||
ChromeProvider.GetImage(collection, name),
|
||||
RenderOrigin);
|
||||
|
||||
var sprite = ChromeProvider.GetImage(collection, name);
|
||||
if (sprite == null)
|
||||
throw new ArgumentException("Sprite {0}/{1} was not found.".F(collection, name));
|
||||
|
||||
WidgetUtils.DrawRGBA(sprite, RenderOrigin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,10 @@ namespace OpenRA.Widgets
|
||||
|
||||
public override void Draw()
|
||||
{
|
||||
SpriteFont font = Game.Renderer.Fonts[Font];
|
||||
SpriteFont font;
|
||||
if (!Game.Renderer.Fonts.TryGetValue(Font, out font))
|
||||
throw new ArgumentException("Request font '{0}' was not found.".F(Font));
|
||||
|
||||
var text = GetText();
|
||||
if (text == null)
|
||||
return;
|
||||
|
||||
@@ -19,10 +19,42 @@ using OpenRA.Network;
|
||||
|
||||
namespace OpenRA.Widgets
|
||||
{
|
||||
public class SpawnOccupant
|
||||
{
|
||||
public readonly HSLColor Color;
|
||||
public readonly int ClientIndex;
|
||||
public readonly string PlayerName;
|
||||
public readonly int Team;
|
||||
public readonly string Country;
|
||||
public readonly int SpawnPoint;
|
||||
|
||||
public SpawnOccupant()
|
||||
{
|
||||
}
|
||||
public SpawnOccupant(Session.Client client)
|
||||
{
|
||||
Color = client.Color;
|
||||
ClientIndex = client.Index;
|
||||
PlayerName = client.Name;
|
||||
Team = client.Team;
|
||||
Country = client.Country;
|
||||
SpawnPoint = client.SpawnPoint;
|
||||
}
|
||||
public SpawnOccupant(GameInformation.Player player)
|
||||
{
|
||||
Color = player.Color;
|
||||
ClientIndex = player.ClientIndex;
|
||||
PlayerName = player.Name;
|
||||
Team = player.Team;
|
||||
Country = player.FactionId;
|
||||
SpawnPoint = player.SpawnPoint;
|
||||
}
|
||||
}
|
||||
|
||||
public class MapPreviewWidget : Widget
|
||||
{
|
||||
public Func<MapPreview> Preview = () => null;
|
||||
public Func<Dictionary<CPos, Session.Client>> SpawnClients = () => new Dictionary<CPos, Session.Client>();
|
||||
public Func<Dictionary<CPos, SpawnOccupant>> SpawnOccupants = () => new Dictionary<CPos, SpawnOccupant>();
|
||||
public Action<MouseInput> OnMouseDown = _ => {};
|
||||
public bool IgnoreMouseInput = false;
|
||||
public bool ShowSpawnPoints = true;
|
||||
@@ -44,7 +76,7 @@ namespace OpenRA.Widgets
|
||||
: base(other)
|
||||
{
|
||||
Preview = other.Preview;
|
||||
SpawnClients = other.SpawnClients;
|
||||
SpawnOccupants = other.SpawnOccupants;
|
||||
ShowSpawnPoints = other.ShowSpawnPoints;
|
||||
TooltipTemplate = other.TooltipTemplate;
|
||||
TooltipContainer = other.TooltipContainer;
|
||||
@@ -109,7 +141,7 @@ namespace OpenRA.Widgets
|
||||
TooltipSpawnIndex = -1;
|
||||
if (ShowSpawnPoints)
|
||||
{
|
||||
var colors = SpawnClients().ToDictionary(c => c.Key, c => c.Value.Color.RGB);
|
||||
var colors = SpawnOccupants().ToDictionary(c => c.Key, c => c.Value.Color.RGB);
|
||||
|
||||
var spawnPoints = preview.SpawnPoints;
|
||||
foreach (var p in spawnPoints)
|
||||
|
||||
@@ -258,7 +258,7 @@ namespace OpenRA.Widgets
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove focus from this widget; return false if you don't want to give it up
|
||||
// Remove focus from this widget; return false to hint that you don't want to give it up
|
||||
public virtual bool YieldMouseFocus(MouseInput mi)
|
||||
{
|
||||
if (Ui.MouseFocusWidget == this)
|
||||
@@ -267,6 +267,12 @@ namespace OpenRA.Widgets
|
||||
return true;
|
||||
}
|
||||
|
||||
void ForceYieldMouseFocus()
|
||||
{
|
||||
if (Ui.MouseFocusWidget == this && !YieldMouseFocus(default(MouseInput)))
|
||||
Ui.MouseFocusWidget = null;
|
||||
}
|
||||
|
||||
public virtual bool TakeKeyboardFocus()
|
||||
{
|
||||
if (HasKeyboardFocus)
|
||||
@@ -287,6 +293,12 @@ namespace OpenRA.Widgets
|
||||
return true;
|
||||
}
|
||||
|
||||
void ForceYieldKeyboardFocus()
|
||||
{
|
||||
if (Ui.KeyboardFocusWidget == this && !YieldKeyboardFocus())
|
||||
Ui.KeyboardFocusWidget = null;
|
||||
}
|
||||
|
||||
public virtual string GetCursor(int2 pos) { return "default"; }
|
||||
public string GetCursorOuter(int2 pos)
|
||||
{
|
||||
@@ -410,6 +422,11 @@ namespace OpenRA.Widgets
|
||||
|
||||
public virtual void Removed()
|
||||
{
|
||||
// Using the forced versions because the widgets
|
||||
// have been removed
|
||||
ForceYieldKeyboardFocus();
|
||||
ForceYieldMouseFocus();
|
||||
|
||||
foreach (var c in Children.OfType<Widget>().Reverse())
|
||||
c.Removed();
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ namespace OpenRA
|
||||
public readonly TileSet TileSet;
|
||||
public readonly ActorMap ActorMap;
|
||||
public readonly ScreenMap ScreenMap;
|
||||
readonly GameInformation gameInfo;
|
||||
|
||||
public void IssueOrder(Order o) { orderManager.IssueOrder(o); } /* avoid exposing the OM to mod code */
|
||||
|
||||
@@ -142,6 +143,12 @@ namespace OpenRA
|
||||
p.Stances[q] = Stance.Neutral;
|
||||
|
||||
Sound.SoundVolumeModifier = 1.0f;
|
||||
|
||||
gameInfo = new GameInformation
|
||||
{
|
||||
MapUid = Map.Uid,
|
||||
MapTitle = Map.Title
|
||||
};
|
||||
}
|
||||
|
||||
public void LoadComplete(WorldRenderer wr)
|
||||
@@ -151,6 +158,14 @@ namespace OpenRA
|
||||
using (new Support.PerfTimer(wlh.GetType().Name + ".WorldLoaded"))
|
||||
wlh.WorldLoaded(this, wr);
|
||||
}
|
||||
|
||||
gameInfo.StartTimeUtc = DateTime.UtcNow;
|
||||
foreach (var player in Players)
|
||||
gameInfo.AddPlayer(player, orderManager.LobbyInfo);
|
||||
|
||||
var rc = orderManager.Connection as ReplayRecorderConnection;
|
||||
if (rc != null)
|
||||
rc.Metadata = new ReplayMetadata(gameInfo);
|
||||
}
|
||||
|
||||
public Actor CreateActor(string name, TypeDictionary initDict)
|
||||
@@ -288,11 +303,16 @@ namespace OpenRA
|
||||
return traitDict.ActorsWithTraitMultiple<T>(this);
|
||||
}
|
||||
|
||||
public void OnLocalPlayerWinStateChanged()
|
||||
public void OnPlayerWinStateChanged(Player player)
|
||||
{
|
||||
var rc = orderManager.Connection as ReplayRecorderConnection;
|
||||
if (rc != null)
|
||||
rc.LocalGameState = LocalPlayer.WinState;
|
||||
var pi = gameInfo.GetPlayer(player);
|
||||
if (pi != null)
|
||||
{
|
||||
pi.Outcome = player.WinState == WinState.Lost ? GameInformation.GameOutcome.Defeat
|
||||
: player.WinState == WinState.Won ? GameInformation.GameOutcome.Victory
|
||||
: GameInformation.GameOutcome.Undefined;
|
||||
pi.OutcomeTimestampUtc = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user