From 98a05b61b34939180f0a207e31f70d8c94672c77 Mon Sep 17 00:00:00 2001 From: Pavlos Touboulidis Date: Mon, 28 Apr 2014 00:44:04 +0300 Subject: [PATCH 01/11] Add metadata block to replays The replay files are just streams all network communication so to get any info out of them it is necessary to play back the stream until the wanted information is reached. This introduces a new metadata block placed at the end of the replay files and logic to read the new block, or fall back to playing back the stream for older files. The replay browser is also updated to use the metadata information instead of reading the replay stream directly. --- OpenRA.Game/FileFormats/ReplayMetadata.cs | 214 ++++++++++++++++++ OpenRA.Game/Game.cs | 4 + OpenRA.Game/Network/ReplayConnection.cs | 88 +++---- .../Network/ReplayRecorderConnection.cs | 10 + OpenRA.Game/OpenRA.Game.csproj | 1 + OpenRA.Game/World.cs | 7 + OpenRA.Mods.RA/ConquestVictoryConditions.cs | 8 + .../Widgets/Logic/ReplayBrowserLogic.cs | 107 +++++---- 8 files changed, 353 insertions(+), 86 deletions(-) create mode 100644 OpenRA.Game/FileFormats/ReplayMetadata.cs diff --git a/OpenRA.Game/FileFormats/ReplayMetadata.cs b/OpenRA.Game/FileFormats/ReplayMetadata.cs new file mode 100644 index 0000000000..d7d16d8011 --- /dev/null +++ b/OpenRA.Game/FileFormats/ReplayMetadata.cs @@ -0,0 +1,214 @@ +#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. + */ +using System.Text; + + +#endregion + +using System; +using System.IO; +using OpenRA.Network; + +namespace OpenRA.FileFormats +{ + public class ReplayMetadata + { + public const int MetaStartMarker = -1; // Must be an invalid replay 'client' value + public const int MetaEndMarker = -2; + public const int MetaVersion = 0x00000001; + + public string FilePath { get; private set; } + public DateTime EndTimestampUtc { get; private set; } + public TimeSpan Duration { get { return EndTimestampUtc.Subtract(StartTimestampUtc); } } + public WinState Outcome { get; private set; } + + public readonly Lazy Session; + public readonly DateTime StartTimestampUtc; + readonly string sessionData; + + ReplayMetadata() + { + Outcome = WinState.Undefined; + } + + public ReplayMetadata(DateTime startGameTimestampUtc, Session session) + : this() + { + if (startGameTimestampUtc.Kind == DateTimeKind.Unspecified) + throw new ArgumentException("The 'Kind' property of the timestamp must be specified", "startGameTimestamp"); + StartTimestampUtc = startGameTimestampUtc.ToUniversalTime(); + + sessionData = session.Serialize(); + Session = new Lazy(() => OpenRA.Network.Session.Deserialize(this.sessionData)); + } + + 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(); + } + + 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) + 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 session + sessionData = ReadUtf8String(reader); + Session = new Lazy(() => OpenRA.Network.Session.Deserialize(this.sessionData)); + } + + public void Write(BinaryWriter writer) + { + // Write start marker & version + writer.Write(MetaStartMarker); + writer.Write(MetaVersion); + + // 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 session data + dataLength += WriteUtf8String(writer, sessionData); + } + + // Write total length & end marker + writer.Write(dataLength); + writer.Write(MetaEndMarker); + } + + public static ReplayMetadata Read(string path, bool enableFallbackMethod = true) + { + Func 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; + } + } + + static ReplayMetadata Read(FileStream fs, bool enableFallbackMethod, Func fallbackTimestampProvider) + { + 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) + { + 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 + { + return new ReplayMetadata(reader); + } + catch (InvalidOperationException) + { + } + catch (NotSupportedException) + { + } + } + + // Reset the stream position or the ReplayConnection will fail later + fs.Seek(0, SeekOrigin.Begin); + } + + if (enableFallbackMethod) + { + using (var conn = new ReplayConnection(fs)) + { + 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; + } + } + } + + return null; + } + + static int WriteUtf8String(BinaryWriter writer, string text) + { + byte[] bytes; + + if (!string.IsNullOrEmpty(text)) + bytes = Encoding.UTF8.GetBytes(text); + else + bytes = new byte[0]; + + writer.Write(bytes.Length); + writer.Write(bytes); + + return 4 + bytes.Length; + } + + static string ReadUtf8String(BinaryReader reader) + { + return Encoding.UTF8.GetString(reader.ReadBytes(reader.ReadInt32())); + } + + public MapPreview MapPreview + { + get { return Game.modData.MapCache[Session.Value.GlobalSettings.Map]; } + } + } +} diff --git a/OpenRA.Game/Game.cs b/OpenRA.Game/Game.cs index 4f56b9cc42..c71bfb5865 100644 --- a/OpenRA.Game/Game.cs +++ b/OpenRA.Game/Game.cs @@ -263,6 +263,10 @@ 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; diff --git a/OpenRA.Game/Network/ReplayConnection.cs b/OpenRA.Game/Network/ReplayConnection.cs index b78db96c47..cc5fcb8da6 100755 --- a/OpenRA.Game/Network/ReplayConnection.cs +++ b/OpenRA.Game/Network/ReplayConnection.cs @@ -35,51 +35,63 @@ namespace OpenRA.Network public ReplayConnection(string replayFilename) { - // 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. using (var rs = File.OpenRead(replayFilename)) { - var chunk = new Chunk(); - - while (rs.Position < rs.Length) - { - var client = rs.ReadInt32(); - 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) - { - // 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); - } - } + 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) + { + 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)); + + if (packet.Length == 5 && packet[4] == 0xBF) + continue; // disconnect + else if (packet.Length >= 5 && packet[4] == 0x65) + continue; // sync + else if (frame == 0) + { + // 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); + } + } + } + // Do nothing: ignore locally generated orders public void Send(int frame, List orders) { } public void SendImmediate(List orders) { } diff --git a/OpenRA.Game/Network/ReplayRecorderConnection.cs b/OpenRA.Game/Network/ReplayRecorderConnection.cs index 3e6f6a8c31..5066b551de 100644 --- a/OpenRA.Game/Network/ReplayRecorderConnection.cs +++ b/OpenRA.Game/Network/ReplayRecorderConnection.cs @@ -12,12 +12,16 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using OpenRA.FileFormats; using OpenRA.Widgets; namespace OpenRA.Network { class ReplayRecorderConnection : IConnection { + public ReplayMetadata Metadata; + public WinState LocalGameState = WinState.Undefined; + IConnection inner; BinaryWriter writer; Func chooseFilename; @@ -101,6 +105,12 @@ namespace OpenRA.Network if (disposed) return; + if (Metadata != null) + { + Metadata.FinalizeReplayMetadata(DateTime.UtcNow, LocalGameState); + Metadata.Write(writer); + } + writer.Close(); inner.Dispose(); disposed = true; diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index bb8f427bd9..28bec9885b 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -330,6 +330,7 @@ + diff --git a/OpenRA.Game/World.cs b/OpenRA.Game/World.cs index cd0f8f4d90..78416db7c1 100644 --- a/OpenRA.Game/World.cs +++ b/OpenRA.Game/World.cs @@ -287,6 +287,13 @@ namespace OpenRA { return traitDict.ActorsWithTraitMultiple(this); } + + public void OnLocalPlayerWinStateChanged() + { + var rc = orderManager.Connection as ReplayRecorderConnection; + if (rc != null) + rc.LocalGameState = LocalPlayer.WinState; + } } public struct TraitPair diff --git a/OpenRA.Mods.RA/ConquestVictoryConditions.cs b/OpenRA.Mods.RA/ConquestVictoryConditions.cs index dae465959f..52dd79eb97 100644 --- a/OpenRA.Mods.RA/ConquestVictoryConditions.cs +++ b/OpenRA.Mods.RA/ConquestVictoryConditions.cs @@ -67,11 +67,15 @@ namespace OpenRA.Mods.RA a.Kill(a); if (self.Owner == self.World.LocalPlayer) + { + self.World.OnLocalPlayerWinStateChanged(); + Game.RunAfterDelay(Info.NotificationDelay, () => { if (Game.IsCurrentWorld(self.World)) Sound.PlayNotification(self.World.Map.Rules, self.Owner, "Speech", "Lose", self.Owner.Country.Race); }); + } } public void Win(Actor self) @@ -81,7 +85,11 @@ namespace OpenRA.Mods.RA Game.Debug("{0} is victorious.".F(self.Owner.PlayerName)); if (self.Owner == self.World.LocalPlayer) + { + self.World.OnLocalPlayerWinStateChanged(); + Game.RunAfterDelay(Info.NotificationDelay, () => Sound.PlayNotification(self.World.Map.Rules, self.Owner, "Speech", "Win", self.Owner.Country.Race)); + } } } diff --git a/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs index 2f280ec317..9311f814c9 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs @@ -13,6 +13,7 @@ using System.Collections.Generic; using System.Drawing; using System.IO; using System.Linq; +using OpenRA.FileFormats; using OpenRA.Network; using OpenRA.Widgets; @@ -51,11 +52,22 @@ namespace OpenRA.Mods.RA.Widgets.Logic rl.RemoveChildren(); if (Directory.Exists(dir)) { - var files = Directory.GetFiles(dir, "*.rep").Reverse(); - foreach (var replayFile in files) - AddReplay(rl, replayFile, template); + List replays; - SelectReplay(files.FirstOrDefault()); + using (new Support.PerfTimer("Load replays")) + { + replays = Directory + .GetFiles(dir, "*.rep") + .Select((filename) => ReplayMetadata.Read(filename)) + .Where((r) => r != null) + .OrderByDescending((r) => Path.GetFileName(r.FilePath)) + .ToList(); + } + + foreach (var replay in replays) + AddReplay(rl, replay, template); + + SelectReplay(replays.FirstOrDefault()); } var watch = panel.Get("WATCH_BUTTON"); @@ -79,63 +91,62 @@ namespace OpenRA.Mods.RA.Widgets.Logic panel.Get("DURATION").GetText = () => selectedDuration; } - void SelectReplay(string filename) + void SelectReplay(ReplayMetadata replay) { - if (filename == null) + if (replay == null) return; try { - using (var conn = new ReplayConnection(filename)) + var lobby = replay.Session.Value; + + selectedFilename = replay.FilePath; + selectedMap = Game.modData.MapCache[lobby.GlobalSettings.Map]; + selectedSpawns = LobbyUtils.GetSpawnClients(lobby, selectedMap); + selectedDuration = WidgetUtils.FormatTimeSeconds((int)replay.Duration.TotalSeconds); + selectedValid = true; + + var clients = lobby.Clients.Where(c => c.Slot != null) + .GroupBy(c => c.Team) + .OrderBy(g => g.Key); + + var teams = new Dictionary>(); + var noTeams = clients.Count() == 1; + foreach (var c in clients) { - selectedFilename = filename; - selectedMap = Game.modData.MapCache[conn.LobbyInfo.GlobalSettings.Map]; - selectedSpawns = LobbyUtils.GetSpawnClients(conn.LobbyInfo, selectedMap); - selectedDuration = WidgetUtils.FormatTime(conn.TickCount * Game.NetTickScale); - selectedValid = conn.TickCount > 0; + var label = noTeams ? "Players" : c.Key == 0 ? "No Team" : "Team {0}".F(c.Key); + teams.Add(label, c); + } - var clients = conn.LobbyInfo.Clients.Where(c => c.Slot != null) - .GroupBy(c => c.Team) - .OrderBy(g => g.Key); + playerList.RemoveChildren(); - var teams = new Dictionary>(); - var noTeams = clients.Count() == 1; - foreach (var c in clients) + foreach (var kv in teams) + { + var group = kv.Key; + if (group.Length > 0) { - var label = noTeams ? "Players" : c.Key == 0 ? "No Team" : "Team {0}".F(c.Key); - teams.Add(label, c); + var header = ScrollItemWidget.Setup(playerHeader, () => true, () => {}); + header.Get("LABEL").GetText = () => group; + playerList.AddChild(header); } - playerList.RemoveChildren(); - - foreach (var kv in teams) + foreach (var option in kv.Value) { - var group = kv.Key; - if (group.Length > 0) - { - var header = ScrollItemWidget.Setup(playerHeader, () => true, () => {}); - header.Get("LABEL").GetText = () => group; - playerList.AddChild(header); - } + var o = option; - foreach (var option in kv.Value) - { - var o = option; + var color = o.Color.RGB; - var color = o.Color.RGB; + var item = ScrollItemWidget.Setup(playerTemplate, () => false, () => { }); - var item = ScrollItemWidget.Setup(playerTemplate, () => false, () => { }); + var label = item.Get("LABEL"); + label.GetText = () => o.Name; + label.GetColor = () => color; - var label = item.Get("LABEL"); - label.GetText = () => o.Name; - label.GetColor = () => color; + var flag = item.Get("FLAG"); + flag.GetImageCollection = () => "flags"; + flag.GetImageName = () => o.Country; - var flag = item.Get("FLAG"); - flag.GetImageCollection = () => "flags"; - flag.GetImageName = () => o.Country; - - playerList.AddChild(item); - } + playerList.AddChild(item); } } } @@ -157,13 +168,13 @@ namespace OpenRA.Mods.RA.Widgets.Logic } } - void AddReplay(ScrollPanelWidget list, string filename, ScrollItemWidget template) + void AddReplay(ScrollPanelWidget list, ReplayMetadata replay, ScrollItemWidget template) { var item = ScrollItemWidget.Setup(template, - () => selectedFilename == filename, - () => SelectReplay(filename), + () => selectedFilename == replay.FilePath, + () => SelectReplay(replay), () => WatchReplay()); - var f = Path.GetFileName(filename); + var f = Path.GetFileName(replay.FilePath); item.Get("TITLE").GetText = () => f; list.AddChild(item); } From a80c4f086a4119aaecc29db436d26078678aa06c Mon Sep 17 00:00:00 2001 From: Pavlos Touboulidis Date: Mon, 28 Apr 2014 18:12:39 +0300 Subject: [PATCH 02/11] Add filters to the replay browser dialog This closes issue #2152. The filters added are: * Game type (singleplayer / multiplayer) * Date * Duration * Outcome * Player name Other changes: * Added a 'CollapseHiddenChildren' option to the ScrollPanelWidget to make hidden children take up no space. * Removed the extension (.rep) from the replay filenames in the replay browser. --- OpenRA.Game/FileFormats/ReplayMetadata.cs | 2 + OpenRA.Game/Widgets/ListLayout.cs | 6 +- OpenRA.Game/Widgets/ScrollPanelWidget.cs | 1 + .../Widgets/Logic/ReplayBrowserLogic.cs | 347 ++++++++++++++++-- mods/ra/chrome/replaybrowser.yaml | 155 ++++++-- 5 files changed, 454 insertions(+), 57 deletions(-) diff --git a/OpenRA.Game/FileFormats/ReplayMetadata.cs b/OpenRA.Game/FileFormats/ReplayMetadata.cs index d7d16d8011..0e86442aad 100644 --- a/OpenRA.Game/FileFormats/ReplayMetadata.cs +++ b/OpenRA.Game/FileFormats/ReplayMetadata.cs @@ -53,6 +53,8 @@ namespace OpenRA.FileFormats 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) diff --git a/OpenRA.Game/Widgets/ListLayout.cs b/OpenRA.Game/Widgets/ListLayout.cs index 83779fc4ce..e80bf8c08b 100644 --- a/OpenRA.Game/Widgets/ListLayout.cs +++ b/OpenRA.Game/Widgets/ListLayout.cs @@ -22,7 +22,8 @@ namespace OpenRA.Widgets widget.ContentHeight = widget.ItemSpacing; w.Bounds.Y = widget.ContentHeight; - widget.ContentHeight += w.Bounds.Height + widget.ItemSpacing; + if (!widget.CollapseHiddenChildren || w.IsVisible()) + widget.ContentHeight += w.Bounds.Height + widget.ItemSpacing; } public void AdjustChildren() @@ -31,7 +32,8 @@ namespace OpenRA.Widgets foreach (var w in widget.Children) { w.Bounds.Y = widget.ContentHeight; - widget.ContentHeight += w.Bounds.Height + widget.ItemSpacing; + if (!widget.CollapseHiddenChildren || w.IsVisible()) + widget.ContentHeight += w.Bounds.Height + widget.ItemSpacing; } } } diff --git a/OpenRA.Game/Widgets/ScrollPanelWidget.cs b/OpenRA.Game/Widgets/ScrollPanelWidget.cs index c721cbab88..150b5048ea 100644 --- a/OpenRA.Game/Widgets/ScrollPanelWidget.cs +++ b/OpenRA.Game/Widgets/ScrollPanelWidget.cs @@ -30,6 +30,7 @@ namespace OpenRA.Widgets public ILayout Layout; public int MinimumThumbSize = 10; public ScrollPanelAlign Align = ScrollPanelAlign.Top; + public bool CollapseHiddenChildren = false; protected float ListOffset = 0; protected bool UpPressed = false; protected bool DownPressed = false; diff --git a/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs index 9311f814c9..52e1dbac19 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs @@ -15,21 +15,23 @@ using System.IO; using System.Linq; using OpenRA.FileFormats; using OpenRA.Network; +using OpenRA.Primitives; using OpenRA.Widgets; namespace OpenRA.Mods.RA.Widgets.Logic { public class ReplayBrowserLogic { + static Filter filter = new Filter(); + Widget panel; ScrollPanelWidget playerList; ScrollItemWidget playerTemplate, playerHeader; + List replays; + Dictionary replayVis = new Dictionary(); - MapPreview selectedMap = MapCache.UnknownMap; Dictionary selectedSpawns; - string selectedFilename; - string selectedDuration; - bool selectedValid; + ReplayMetadata selectedReplay; [ObjectCreator.UseCtor] public ReplayBrowserLogic(Widget widget, Action onExit, Action onStart) @@ -52,47 +54,315 @@ namespace OpenRA.Mods.RA.Widgets.Logic rl.RemoveChildren(); if (Directory.Exists(dir)) { - List replays; - using (new Support.PerfTimer("Load replays")) { replays = Directory .GetFiles(dir, "*.rep") .Select((filename) => ReplayMetadata.Read(filename)) .Where((r) => r != null) - .OrderByDescending((r) => Path.GetFileName(r.FilePath)) + .OrderByDescending(r => r.StartTimestampUtc) .ToList(); } foreach (var replay in replays) AddReplay(rl, replay, template); - SelectReplay(replays.FirstOrDefault()); + ApplyFilter(); } var watch = panel.Get("WATCH_BUTTON"); - watch.IsDisabled = () => !selectedValid || selectedMap.Status != MapStatus.Available; + watch.IsDisabled = () => selectedReplay == null || selectedReplay.MapPreview.Status != MapStatus.Available; watch.OnClick = () => { WatchReplay(); onStart(); }; - panel.Get("REPLAY_INFO").IsVisible = () => selectedFilename != null; + panel.Get("REPLAY_INFO").IsVisible = () => selectedReplay != null; var preview = panel.Get("MAP_PREVIEW"); preview.SpawnClients = () => selectedSpawns; - preview.Preview = () => selectedMap; + preview.Preview = () => selectedReplay != null ? selectedReplay.MapPreview : null; var title = panel.GetOrNull("MAP_TITLE"); if (title != null) - title.GetText = () => selectedMap.Title; + title.GetText = () => selectedReplay != null ? selectedReplay.MapPreview.Title : null; var type = panel.GetOrNull("MAP_TYPE"); if (type != null) - type.GetText = () => selectedMap.Type; + type.GetText = () => selectedReplay.MapPreview.Type; - panel.Get("DURATION").GetText = () => selectedDuration; + panel.Get("DURATION").GetText = () => WidgetUtils.FormatTimeSeconds((int)selectedReplay.Duration.TotalSeconds); + + SetupFilters(); + } + + void SetupFilters() + { + // + // Game type + // + { + var ddb = panel.GetOrNull("FLT_GAMETYPE_DROPDOWNBUTTON"); + if (ddb != null) + { + // Using list to maintain the order + var options = new List> + { + new KeyValuePair(GameType.Any, ddb.GetText()), + new KeyValuePair(GameType.Singleplayer, "Singleplayer"), + new KeyValuePair(GameType.Multiplayer, "Multiplayer") + }; + var lookup = options.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + + ddb.GetText = () => lookup[filter.Type]; + ddb.OnMouseDown = _ => + { + Func, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) => + { + var item = ScrollItemWidget.Setup( + tpl, + () => filter.Type == option.Key, + () => { filter.Type = option.Key; ApplyFilter(); } + ); + item.Get("LABEL").GetText = () => option.Value; + return item; + }; + + ddb.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", options.Count * 30, options, setupItem); + }; + } + } + + // + // Date type + // + { + var ddb = panel.GetOrNull("FLT_DATE_DROPDOWNBUTTON"); + if (ddb != null) + { + // Using list to maintain the order + var options = new List> + { + new KeyValuePair(DateType.Any, ddb.GetText()), + new KeyValuePair(DateType.Today, "Today"), + new KeyValuePair(DateType.LastWeek, "Last Week"), + new KeyValuePair(DateType.LastMonth, "Last Month") + }; + var lookup = options.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + + ddb.GetText = () => lookup[filter.Date]; + ddb.OnMouseDown = _ => + { + Func, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) => + { + var item = ScrollItemWidget.Setup( + tpl, + () => filter.Date == option.Key, + () => { filter.Date = option.Key; ApplyFilter(); } + ); + item.Get("LABEL").GetText = () => option.Value; + return item; + }; + + ddb.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", options.Count * 30, options, setupItem); + }; + } + } + + // + // Duration + // + { + var ddb = panel.GetOrNull("FLT_DURATION_DROPDOWNBUTTON"); + if (ddb != null) + { + // Using list to maintain the order + var options = new List> + { + new KeyValuePair(DurationType.Any, ddb.GetText()), + new KeyValuePair(DurationType.VeryShort, "Under 5 min"), + new KeyValuePair(DurationType.Short, "Short (10 min)"), + new KeyValuePair(DurationType.Medium, "Medium (30 min)"), + new KeyValuePair(DurationType.Long, "Long (60+ min)") + }; + var lookup = options.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + + ddb.GetText = () => lookup[filter.Duration]; + ddb.OnMouseDown = _ => + { + Func, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) => + { + var item = ScrollItemWidget.Setup( + tpl, + () => filter.Duration == option.Key, + () => { filter.Duration = option.Key; ApplyFilter(); } + ); + item.Get("LABEL").GetText = () => option.Value; + return item; + }; + + ddb.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", options.Count * 30, options, setupItem); + }; + } + } + + // + // Outcome + // + { + var ddb = panel.GetOrNull("FLT_OUTCOME_DROPDOWNBUTTON"); + if (ddb != null) + { + // Using list to maintain the order + var options = new List> + { + new KeyValuePair(WinState.Undefined, ddb.GetText()), + new KeyValuePair(WinState.Won, "Won"), + new KeyValuePair(WinState.Lost, "Lost") + }; + var lookup = options.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + + ddb.GetText = () => lookup[filter.Outcome]; + ddb.OnMouseDown = _ => + { + Func, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) => + { + var item = ScrollItemWidget.Setup( + tpl, + () => filter.Outcome == option.Key, + () => { filter.Outcome = option.Key; ApplyFilter(); } + ); + item.Get("LABEL").GetText = () => option.Value; + return item; + }; + + ddb.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", options.Count * 30, options, setupItem); + }; + } + } + + // + // Players + // + { + var ddb = panel.GetOrNull("FLT_PLAYER_DROPDOWNBUTTON"); + if (ddb != null) + { + var options = new HashSet(replays.SelectMany(r => r.Session.Value.Clients.Select(c => c.Name)), StringComparer.OrdinalIgnoreCase).ToList(); + options.Sort(StringComparer.OrdinalIgnoreCase); + options.Insert(0, null); // no filter + + var nobodyText = ddb.GetText(); + ddb.GetText = () => string.IsNullOrEmpty(filter.PlayerName) ? nobodyText : filter.PlayerName; + ddb.OnMouseDown = _ => + { + Func setupItem = (option, tpl) => + { + var item = ScrollItemWidget.Setup( + tpl, + () => string.Compare(filter.PlayerName, option, true) == 0, + () => { filter.PlayerName = option; ApplyFilter(); } + ); + item.Get("LABEL").GetText = () => option ?? nobodyText; + return item; + }; + + ddb.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", options.Count * 30, options, setupItem); + }; + } + } + } + + bool EvaluateReplayVisibility(ReplayMetadata replay) + { + // Game type + if ((filter.Type == GameType.Multiplayer && replay.Session.Value.IsSinglePlayer) || (filter.Type == GameType.Singleplayer && !replay.Session.Value.IsSinglePlayer)) + return false; + + // Date type + if (filter.Date != DateType.Any) + { + TimeSpan t; + switch (filter.Date) + { + case DateType.Today: + t = TimeSpan.FromDays(1d); + break; + + case DateType.LastWeek: + t = TimeSpan.FromDays(7d); + break; + + case DateType.LastMonth: + default: + t = TimeSpan.FromDays(30d); + break; + } + if (replay.StartTimestampUtc < DateTime.UtcNow.Subtract(t)) + return false; + } + + // Duration + if (filter.Duration != DurationType.Any) + { + double minutes = replay.Duration.TotalMinutes; + switch (filter.Duration) + { + case DurationType.VeryShort: + if (minutes >= 5) + return false; + break; + + case DurationType.Short: + if (minutes < 5 || minutes >= 20) + return false; + break; + + case DurationType.Medium: + if (minutes < 20 || minutes >= 60) + return false; + break; + + case DurationType.Long: + if (minutes < 60) + return false; + break; + } + } + + // Outcome + if (filter.Outcome != WinState.Undefined && filter.Outcome != replay.Outcome) + return false; + + // Player + if (!string.IsNullOrEmpty(filter.PlayerName)) + { + var player = replay.Session.Value.Clients.Find(c => string.Compare(filter.PlayerName, c.Name, true) == 0); + if (player == null) + return false; + } + + return true; + } + + void ApplyFilter() + { + foreach (var replay in replays) + replayVis[replay] = EvaluateReplayVisibility(replay); + + if (selectedReplay == null || replayVis[selectedReplay] == false) + SelectFirstVisibleReplay(); + + panel.Get("REPLAY_LIST").Layout.AdjustChildren(); + } + + void SelectFirstVisibleReplay() + { + SelectReplay(replays.FirstOrDefault(r => replayVis[r])); } void SelectReplay(ReplayMetadata replay) { + selectedReplay = replay; + selectedSpawns = (selectedReplay != null) ? LobbyUtils.GetSpawnClients(selectedReplay.Session.Value, selectedReplay.MapPreview) : null; + if (replay == null) return; @@ -100,12 +370,6 @@ namespace OpenRA.Mods.RA.Widgets.Logic { var lobby = replay.Session.Value; - selectedFilename = replay.FilePath; - selectedMap = Game.modData.MapCache[lobby.GlobalSettings.Map]; - selectedSpawns = LobbyUtils.GetSpawnClients(lobby, selectedMap); - selectedDuration = WidgetUtils.FormatTimeSeconds((int)replay.Duration.TotalSeconds); - selectedValid = true; - var clients = lobby.Clients.Where(c => c.Slot != null) .GroupBy(c => c.Team) .OrderBy(g => g.Key); @@ -153,17 +417,15 @@ namespace OpenRA.Mods.RA.Widgets.Logic catch (Exception e) { Log.Write("debug", "Exception while parsing replay: {0}", e); - selectedFilename = null; - selectedValid = false; - selectedMap = MapCache.UnknownMap; + SelectReplay(null); } } void WatchReplay() { - if (selectedFilename != null) + if (selectedReplay != null) { - Game.JoinReplay(selectedFilename); + Game.JoinReplay(selectedReplay.FilePath); Ui.CloseWindow(); } } @@ -171,12 +433,43 @@ namespace OpenRA.Mods.RA.Widgets.Logic void AddReplay(ScrollPanelWidget list, ReplayMetadata replay, ScrollItemWidget template) { var item = ScrollItemWidget.Setup(template, - () => selectedFilename == replay.FilePath, + () => selectedReplay == replay, () => SelectReplay(replay), () => WatchReplay()); - var f = Path.GetFileName(replay.FilePath); + var f = Path.GetFileNameWithoutExtension(replay.FilePath); item.Get("TITLE").GetText = () => f; + item.IsVisible = () => { bool visible; return replayVis.TryGetValue(replay, out visible) && visible; }; list.AddChild(item); } + + class Filter + { + public GameType Type; + public DateType Date; + public DurationType Duration; + public WinState Outcome = WinState.Undefined; + public string PlayerName; + } + enum GameType + { + Any, + Singleplayer, + Multiplayer + } + enum DateType + { + Any, + Today, + LastWeek, + LastMonth + } + enum DurationType + { + Any, + VeryShort, + Short, + Medium, + Long + } } } diff --git a/mods/ra/chrome/replaybrowser.yaml b/mods/ra/chrome/replaybrowser.yaml index 7ef5acbea5..43e42e1723 100644 --- a/mods/ra/chrome/replaybrowser.yaml +++ b/mods/ra/chrome/replaybrowser.yaml @@ -2,45 +2,144 @@ Background@REPLAYBROWSER_PANEL: Logic: ReplayBrowserLogic X: (WINDOW_RIGHT - WIDTH)/2 Y: (WINDOW_BOTTOM - HEIGHT)/2 - Width: 530 + Width: 490 Height: 535 Children: - Label@REPLAYBROWSER_LABEL_TITLE: - Y: 20 - Width: PARENT_RIGHT - Height: 25 - Text: Choose Replay - Align: Center - Font: Bold - ScrollPanel@REPLAY_LIST: + Container@FILTERS: X: 20 - Y: 50 - Width: 282 - Height: 430 + Y: 20 + Width: 280 + Height: 180 Children: - ScrollItem@REPLAY_TEMPLATE: - Width: PARENT_RIGHT-27 + Label@FILTERS_TITLE: + Width: PARENT_RIGHT Height: 25 - X: 2 - Visible: false + Font: Bold + Align: Center + Text: Filter + Label@FLT_GAMETYPE_DESC: + X: 0 + Y: 30 + Width: 80 + Height: 25 + Text: Type: + Align: Right + DropDownButton@FLT_GAMETYPE_DROPDOWNBUTTON: + X: 85 + Y: 30 + Width: 160 + Height: 25 + Font: Regular + Text: Any + Label@FLT_DATE_DESC: + X: 0 + Y: 60 + Width: 80 + Height: 25 + Text: Date: + Align: Right + DropDownButton@FLT_DATE_DROPDOWNBUTTON: + X: 85 + Y: 60 + Width: 160 + Height: 25 + Font: Regular + Text: Any + Label@FLT_PLAYER_DESC: + X: 0 + Y: 90 + Width: 80 + Height: 25 + Text: Player: + Align: Right + DropDownButton@FLT_PLAYER_DROPDOWNBUTTON: + X: 85 + Y: 90 + Width: 160 + Height: 25 + Font: Regular + Text: Anyone + Label@FLT_OUTCOME_DESC: + X: 0 + Y: 120 + Width: 80 + Height: 25 + Text: Outcome: + Align: Right + DropDownButton@FLT_OUTCOME_DROPDOWNBUTTON: + X: 85 + Y: 120 + Width: 160 + Height: 25 + Font: Regular + Text: Any + Label@FLT_DURATION_DESC: + X: 0 + Y: 150 + Width: 80 + Height: 25 + Text: Duration: + Align: Right + DropDownButton@FLT_DURATION_DROPDOWNBUTTON: + X: 85 + Y: 150 + Width: 160 + Height: 25 + Font: Regular + Text: Any + Container@REPLAY_LIST_CONTAINER: + X: 20 + Y: 210 + Width: 245 + Height: PARENT_BOTTOM - 270 + Children: + Label@REPLAYBROWSER_LABEL_TITLE: + Width: PARENT_RIGHT + Height: 25 + Text: Choose Replay + Align: Center + Font: Bold + ScrollPanel@REPLAY_LIST: + X: 0 + Y: 30 + Width: PARENT_RIGHT + Height: PARENT_BOTTOM - 25 + CollapseHiddenChildren: True Children: - Label@TITLE: - X: 10 - Width: PARENT_RIGHT-20 + ScrollItem@REPLAY_TEMPLATE: + Width: PARENT_RIGHT-27 Height: 25 - Background@MAP_BG: + X: 2 + Visible: false + Children: + Label@TITLE: + X: 10 + Width: PARENT_RIGHT-20 + Height: 25 + Container@MAP_BG_CONTAINER: X: PARENT_RIGHT-WIDTH-20 - Y: 50 + Y: 20 Width: 194 Height: 194 - Background: dialog3 Children: - MapPreview@MAP_PREVIEW: - X: 1 - Y: 1 - Width: 192 - Height: 192 - TooltipContainer: TOOLTIP_CONTAINER + Label@MAP_BG_TITLE: + Width: PARENT_RIGHT + Height: 25 + Text: Preview + Align: Center + Font: Bold + Background@MAP_BG: + Y: 30 + Width: 194 + Height: 194 + Background: dialog3 + Children: + MapPreview@MAP_PREVIEW: + X: 1 + Y: 1 + Width: 192 + Height: 192 + TooltipContainer: TOOLTIP_CONTAINER Container@REPLAY_INFO: X: PARENT_RIGHT-WIDTH - 20 Y: 50 From ce8c42b552ebb8170a76cfca73eacbccd73b3b9a Mon Sep 17 00:00:00 2001 From: Pavlos Touboulidis Date: Mon, 28 Apr 2014 21:49:41 +0300 Subject: [PATCH 03/11] Style & nit fixes --- OpenRA.Game/FileFormats/ReplayMetadata.cs | 41 ++++++++++--------- OpenRA.Game/Network/ReplayConnection.cs | 2 - .../Widgets/Logic/ReplayBrowserLogic.cs | 10 ++--- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/OpenRA.Game/FileFormats/ReplayMetadata.cs b/OpenRA.Game/FileFormats/ReplayMetadata.cs index 0e86442aad..301327228c 100644 --- a/OpenRA.Game/FileFormats/ReplayMetadata.cs +++ b/OpenRA.Game/FileFormats/ReplayMetadata.cs @@ -6,46 +6,46 @@ * as published by the Free Software Foundation. For more information, * see COPYING. */ -using System.Text; - - #endregion using System; using System.IO; +using System.Text; using OpenRA.Network; namespace OpenRA.FileFormats { public class ReplayMetadata { - public const int MetaStartMarker = -1; // Must be an invalid replay 'client' value + // Must be an invalid replay 'client' value + public const int MetaStartMarker = -1; public const int MetaEndMarker = -2; public const int MetaVersion = 0x00000001; public string FilePath { get; private set; } public DateTime EndTimestampUtc { get; private set; } - public TimeSpan Duration { get { return EndTimestampUtc.Subtract(StartTimestampUtc); } } + public TimeSpan Duration { get { return EndTimestampUtc - StartTimestampUtc; } } public WinState Outcome { get; private set; } - public readonly Lazy Session; + public readonly Lazy LobbyInfo; public readonly DateTime StartTimestampUtc; - readonly string sessionData; + readonly string lobbyInfoData; ReplayMetadata() { Outcome = WinState.Undefined; } - public ReplayMetadata(DateTime startGameTimestampUtc, Session session) + public ReplayMetadata(DateTime startGameTimestampUtc, Session lobbyInfo) : this() { if (startGameTimestampUtc.Kind == DateTimeKind.Unspecified) throw new ArgumentException("The 'Kind' property of the timestamp must be specified", "startGameTimestamp"); + StartTimestampUtc = startGameTimestampUtc.ToUniversalTime(); - sessionData = session.Serialize(); - Session = new Lazy(() => OpenRA.Network.Session.Deserialize(this.sessionData)); + lobbyInfoData = lobbyInfo.Serialize(); + LobbyInfo = Exts.Lazy(() => Session.Deserialize(this.lobbyInfoData)); } public void FinalizeReplayMetadata(DateTime endGameTimestampUtc, WinState outcome) @@ -80,9 +80,9 @@ namespace OpenRA.FileFormats if (Enum.TryParse(ReadUtf8String(reader), true, out outcome)) Outcome = outcome; - // Read session - sessionData = ReadUtf8String(reader); - Session = new Lazy(() => OpenRA.Network.Session.Deserialize(this.sessionData)); + // Read lobby info + lobbyInfoData = ReadUtf8String(reader); + LobbyInfo = Exts.Lazy(() => Session.Deserialize(this.lobbyInfoData)); } public void Write(BinaryWriter writer) @@ -105,8 +105,8 @@ namespace OpenRA.FileFormats // Write game outcome dataLength += WriteUtf8String(writer, Outcome.ToString()); - // Write session data - dataLength += WriteUtf8String(writer, sessionData); + // Write lobby info data + dataLength += WriteUtf8String(writer, lobbyInfoData); } // Write total length & end marker @@ -116,7 +116,8 @@ namespace OpenRA.FileFormats public static ReplayMetadata Read(string path, bool enableFallbackMethod = true) { - Func timestampProvider = () => { + Func timestampProvider = () => + { try { return File.GetCreationTimeUtc(path); @@ -159,11 +160,13 @@ namespace OpenRA.FileFormats { return new ReplayMetadata(reader); } - catch (InvalidOperationException) + catch (InvalidOperationException ex) { + Log.Write("debug", ex.ToString()); } - catch (NotSupportedException) + catch (NotSupportedException ex) { + Log.Write("debug", ex.ToString()); } } @@ -210,7 +213,7 @@ namespace OpenRA.FileFormats public MapPreview MapPreview { - get { return Game.modData.MapCache[Session.Value.GlobalSettings.Map]; } + get { return Game.modData.MapCache[LobbyInfo.Value.GlobalSettings.Map]; } } } } diff --git a/OpenRA.Game/Network/ReplayConnection.cs b/OpenRA.Game/Network/ReplayConnection.cs index cc5fcb8da6..b1eefce4c8 100755 --- a/OpenRA.Game/Network/ReplayConnection.cs +++ b/OpenRA.Game/Network/ReplayConnection.cs @@ -36,9 +36,7 @@ namespace OpenRA.Network public ReplayConnection(string replayFilename) { using (var rs = File.OpenRead(replayFilename)) - { Read(rs, ref TickCount, ref IsValid, ref LobbyInfo); - } ordersFrame = LobbyInfo.GlobalSettings.OrderLatency; } diff --git a/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs index 52e1dbac19..1d68e7e8db 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs @@ -245,7 +245,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic var ddb = panel.GetOrNull("FLT_PLAYER_DROPDOWNBUTTON"); if (ddb != null) { - var options = new HashSet(replays.SelectMany(r => r.Session.Value.Clients.Select(c => c.Name)), StringComparer.OrdinalIgnoreCase).ToList(); + var options = new HashSet(replays.SelectMany(r => r.LobbyInfo.Value.Clients.Select(c => c.Name)), StringComparer.OrdinalIgnoreCase).ToList(); options.Sort(StringComparer.OrdinalIgnoreCase); options.Insert(0, null); // no filter @@ -273,7 +273,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic bool EvaluateReplayVisibility(ReplayMetadata replay) { // Game type - if ((filter.Type == GameType.Multiplayer && replay.Session.Value.IsSinglePlayer) || (filter.Type == GameType.Singleplayer && !replay.Session.Value.IsSinglePlayer)) + if ((filter.Type == GameType.Multiplayer && replay.LobbyInfo.Value.IsSinglePlayer) || (filter.Type == GameType.Singleplayer && !replay.LobbyInfo.Value.IsSinglePlayer)) return false; // Date type @@ -334,7 +334,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic // Player if (!string.IsNullOrEmpty(filter.PlayerName)) { - var player = replay.Session.Value.Clients.Find(c => string.Compare(filter.PlayerName, c.Name, true) == 0); + var player = replay.LobbyInfo.Value.Clients.Find(c => string.Compare(filter.PlayerName, c.Name, true) == 0); if (player == null) return false; } @@ -361,14 +361,14 @@ namespace OpenRA.Mods.RA.Widgets.Logic void SelectReplay(ReplayMetadata replay) { selectedReplay = replay; - selectedSpawns = (selectedReplay != null) ? LobbyUtils.GetSpawnClients(selectedReplay.Session.Value, selectedReplay.MapPreview) : null; + selectedSpawns = (selectedReplay != null) ? LobbyUtils.GetSpawnClients(selectedReplay.LobbyInfo.Value, selectedReplay.MapPreview) : null; if (replay == null) return; try { - var lobby = replay.Session.Value; + var lobby = replay.LobbyInfo.Value; var clients = lobby.Clients.Where(c => c.Slot != null) .GroupBy(c => c.Team) From 713141cf4d2eb2f9e7980d12fd39642f43e5b103 Mon Sep 17 00:00:00 2001 From: Pavlos Touboulidis Date: Thu, 1 May 2014 19:35:11 +0300 Subject: [PATCH 04/11] Support for input validation on TextFieldWidgets See OnTextEdited(), IsValid(), TextColorInvalid (with the default in metrics.yaml). --- OpenRA.Game/Widgets/TextFieldWidget.cs | 13 ++++++++++++- mods/cnc/metrics.yaml | 1 + mods/d2k/metrics.yaml | 1 + mods/modchooser/metrics.yaml | 1 + mods/ra/metrics.yaml | 1 + mods/ts/metrics.yaml | 1 + 6 files changed, 17 insertions(+), 1 deletion(-) diff --git a/OpenRA.Game/Widgets/TextFieldWidget.cs b/OpenRA.Game/Widgets/TextFieldWidget.cs index e7b6474d3a..c5cd7b0bde 100644 --- a/OpenRA.Game/Widgets/TextFieldWidget.cs +++ b/OpenRA.Game/Widgets/TextFieldWidget.cs @@ -31,12 +31,15 @@ namespace OpenRA.Widgets public Func OnTabKey = () => false; public Func OnEscKey = () => false; public Action OnLoseFocus = () => { }; + public Action OnTextEdited = () => { }; public int CursorPosition { get; set; } public Func IsDisabled = () => false; + public Func IsValid = () => true; public string Font = ChromeMetrics.Get("TextfieldFont"); public Color TextColor = ChromeMetrics.Get("TextfieldColor"); public Color TextColorDisabled = ChromeMetrics.Get("TextfieldColorDisabled"); + public Color TextColorInvalid = ChromeMetrics.Get("TextfieldColorInvalid"); public TextFieldWidget() {} protected TextFieldWidget(TextFieldWidget widget) @@ -47,6 +50,7 @@ namespace OpenRA.Widgets Font = widget.Font; TextColor = widget.TextColor; TextColorDisabled = widget.TextColorDisabled; + TextColorInvalid = widget.TextColorInvalid; VisualHeight = widget.VisualHeight; } @@ -148,7 +152,10 @@ namespace OpenRA.Widgets if (e.Key == Keycode.DELETE) { if (CursorPosition < Text.Length) + { Text = Text.Remove(CursorPosition, 1); + OnTextEdited(); + } return true; } @@ -156,6 +163,7 @@ namespace OpenRA.Widgets { CursorPosition--; Text = Text.Remove(CursorPosition, 1); + OnTextEdited(); } return true; @@ -171,6 +179,7 @@ namespace OpenRA.Widgets Text = Text.Insert(CursorPosition, text); CursorPosition++; + OnTextEdited(); return true; } @@ -228,7 +237,9 @@ namespace OpenRA.Widgets Bounds.Width - LeftMargin - RightMargin, Bounds.Bottom)); } - var color = disabled ? TextColorDisabled : TextColor; + var color = disabled ? TextColorDisabled + : IsValid() ? TextColor + : TextColorInvalid; font.DrawText(apparentText, textPos, color); if (showCursor && HasKeyboardFocus) diff --git a/mods/cnc/metrics.yaml b/mods/cnc/metrics.yaml index 4b71e6239c..e32eeabe8d 100644 --- a/mods/cnc/metrics.yaml +++ b/mods/cnc/metrics.yaml @@ -14,6 +14,7 @@ Metrics: TextfieldFont: Regular TextfieldColor: 255,255,255 TextfieldColorDisabled: 128,128,128 + TextfieldColorInvalid: 255,192,192 TextFont: Regular TextColor: 255,255,255 TextContrast: false diff --git a/mods/d2k/metrics.yaml b/mods/d2k/metrics.yaml index 137dc82c50..24241acadb 100644 --- a/mods/d2k/metrics.yaml +++ b/mods/d2k/metrics.yaml @@ -14,6 +14,7 @@ Metrics: TextfieldFont: Regular TextfieldColor: 255,255,255 TextfieldColorDisabled: 128,128,128 + TextfieldColorInvalid: 255,192,192 TextFont: Regular TextColor: 255,255,255 TextContrast: false diff --git a/mods/modchooser/metrics.yaml b/mods/modchooser/metrics.yaml index dcbaf3bfa1..a4f6800caa 100644 --- a/mods/modchooser/metrics.yaml +++ b/mods/modchooser/metrics.yaml @@ -14,6 +14,7 @@ Metrics: TextfieldFont: Regular TextfieldColor: 255,255,255 TextfieldColorDisabled: 128,128,128 + TextfieldColorInvalid: 255,192,192 TextFont: Regular TextColor: 255,255,255 TextContrast: false diff --git a/mods/ra/metrics.yaml b/mods/ra/metrics.yaml index c4b902869c..8a2e4c7c39 100644 --- a/mods/ra/metrics.yaml +++ b/mods/ra/metrics.yaml @@ -14,6 +14,7 @@ Metrics: TextfieldFont: Regular TextfieldColor: 255,255,255 TextfieldColorDisabled: 128,128,128 + TextfieldColorInvalid: 255,192,192 TextFont: Regular TextColor: 255,255,255 TextContrast: false diff --git a/mods/ts/metrics.yaml b/mods/ts/metrics.yaml index 5402e155fb..33b2c9673d 100644 --- a/mods/ts/metrics.yaml +++ b/mods/ts/metrics.yaml @@ -14,6 +14,7 @@ Metrics: TextfieldFont: Regular TextfieldColor: 255,255,255 TextfieldColorDisabled: 128,128,128 + TextfieldColorInvalid: 255,192,192 TextFont: Regular TextColor: 255,255,255 TextContrast: false From 042910bd5ef4a8e7048cb10926f5db230c6af56b Mon Sep 17 00:00:00 2001 From: Pavlos Touboulidis Date: Thu, 1 May 2014 19:37:58 +0300 Subject: [PATCH 05/11] New common dialog: TextInputPrompt to get a string from the user For both ra and cnc --- OpenRA.Mods.RA/Widgets/ConfirmationDialogs.cs | 97 +++++++++++++++++++ mods/cnc/chrome/dialogs.yaml | 46 +++++++++ mods/ra/chrome/confirmation-dialogs.yaml | 40 ++++++++ 3 files changed, 183 insertions(+) diff --git a/OpenRA.Mods.RA/Widgets/ConfirmationDialogs.cs b/OpenRA.Mods.RA/Widgets/ConfirmationDialogs.cs index a81df9295d..4545697936 100644 --- a/OpenRA.Mods.RA/Widgets/ConfirmationDialogs.cs +++ b/OpenRA.Mods.RA/Widgets/ConfirmationDialogs.cs @@ -9,6 +9,7 @@ #endregion using System; +using System.Drawing; using OpenRA.Widgets; namespace OpenRA.Mods.RA.Widgets @@ -38,5 +39,101 @@ namespace OpenRA.Mods.RA.Widgets onCancel(); }; } + + public static void TextInputPrompt( + string title, string prompt, string initialText, + Action onAccept, Action onCancel = null, + string acceptText = null, string cancelText = null, + Func inputValidator = null) + { + var panel = Ui.OpenWindow("TEXT_INPUT_PROMPT"); + Func doValidate = null; + ButtonWidget acceptButton = null, cancelButton = null; + + // + // Title + // + panel.Get("PROMPT_TITLE").GetText = () => title; + + // + // Prompt + // + panel.Get("PROMPT_TEXT").GetText = () => prompt; + + // + // Text input + // + var input = panel.Get("INPUT_TEXT"); + var isValid = false; + input.Text = initialText; + input.IsValid = () => isValid; + input.OnEnterKey = () => + { + if (acceptButton.IsDisabled()) + return false; + + acceptButton.OnClick(); + return true; + }; + input.OnEscKey = () => + { + if (cancelButton.IsDisabled()) + return false; + + cancelButton.OnClick(); + return true; + }; + input.TakeKeyboardFocus(); + input.CursorPosition = input.Text.Length; + input.OnTextEdited = () => doValidate(); + + // + // Buttons + // + acceptButton = panel.Get("ACCEPT_BUTTON"); + if (!string.IsNullOrEmpty(acceptText)) + acceptButton.GetText = () => acceptText; + + acceptButton.OnClick = () => + { + if (!doValidate()) + return; + + Ui.CloseWindow(); + onAccept(input.Text); + }; + + cancelButton = panel.Get("CANCEL_BUTTON"); + if (!string.IsNullOrEmpty(cancelText)) + cancelButton.GetText = () => cancelText; + + cancelButton.OnClick = () => + { + Ui.CloseWindow(); + if (onCancel != null) + onCancel(); + }; + + // + // Validation + // + doValidate = () => + { + if (inputValidator == null) + return true; + + isValid = inputValidator(input.Text); + if (isValid) + { + acceptButton.Disabled = false; + return true; + } + + acceptButton.Disabled = true; + return false; + }; + + doValidate(); + } } } diff --git a/mods/cnc/chrome/dialogs.yaml b/mods/cnc/chrome/dialogs.yaml index a95831d6b4..186e657262 100644 --- a/mods/cnc/chrome/dialogs.yaml +++ b/mods/cnc/chrome/dialogs.yaml @@ -154,3 +154,49 @@ Container@CONFIRM_PROMPT: Height: 35 Text: Confirm + +Container@TEXT_INPUT_PROMPT: + X: (WINDOW_RIGHT - WIDTH)/2 + Y: (WINDOW_BOTTOM - HEIGHT)/2 + Width: 370 + Height: 80 + Children: + Label@PROMPT_TITLE: + Width: PARENT_RIGHT + Y: 0-25 + Font: BigBold + Contrast: true + Align: Center + Background@bg: + Width: PARENT_RIGHT + Height: 80 + Background: panel-black + Children: + Label@PROMPT_TEXT: + X: 20 + Y: 10 + Width: PARENT_RIGHT - 40 + Height: 25 + Font: Bold + Align: Center + TextField@INPUT_TEXT: + X: 20 + Y: 40 + Width: PARENT_RIGHT - 40 + Height: 25 + Button@ACCEPT_BUTTON: + X: PARENT_RIGHT - 160 + Y: PARENT_BOTTOM - 1 + Width: 160 + Height: 30 + Text: OK + Font: Bold + Key: return + Button@CANCEL_BUTTON: + X: 0 + Y: PARENT_BOTTOM - 1 + Width: 160 + Height: 30 + Text: Cancel + Font: Bold + Key: escape diff --git a/mods/ra/chrome/confirmation-dialogs.yaml b/mods/ra/chrome/confirmation-dialogs.yaml index 972067af83..35522917d2 100644 --- a/mods/ra/chrome/confirmation-dialogs.yaml +++ b/mods/ra/chrome/confirmation-dialogs.yaml @@ -32,3 +32,43 @@ Background@CONFIRM_PROMPT: Text: Cancel Font: Bold Key: escape + +Background@TEXT_INPUT_PROMPT: + X: (WINDOW_RIGHT - WIDTH)/2 + Y: (WINDOW_BOTTOM - HEIGHT)/2 + Width: 370 + Height: 175 + Children: + Label@PROMPT_TITLE: + Width: PARENT_RIGHT + Y: 20 + Height: 25 + Font: Bold + Align: Center + Label@PROMPT_TEXT: + X: 20 + Y: 50 + Width: PARENT_RIGHT - 40 + Height: 25 + Align: Center + TextField@INPUT_TEXT: + X: 20 + Y: 80 + Width: PARENT_RIGHT - 40 + Height: 25 + Button@ACCEPT_BUTTON: + X: 20 + Y: PARENT_BOTTOM - 45 + Width: 160 + Height: 25 + Text: OK + Font: Bold + Key: return + Button@CANCEL_BUTTON: + X: PARENT_RIGHT - 180 + Y: PARENT_BOTTOM - 45 + Width: 160 + Height: 25 + Text: Cancel + Font: Bold + Key: escape From de0a5ebd430a7837fba4b5abf1bb6954a0b1708a Mon Sep 17 00:00:00 2001 From: Pavlos Touboulidis Date: Thu, 1 May 2014 19:39:47 +0300 Subject: [PATCH 06/11] 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. --- OpenRA.Game/FileFormats/ReplayMetadata.cs | 163 ++------ OpenRA.Game/Game.cs | 4 - OpenRA.Game/GameInformation.cs | 230 ++++++++++++ OpenRA.Game/Graphics/ChromeProvider.cs | 29 +- OpenRA.Game/Network/ReplayConnection.cs | 81 ++-- .../Network/ReplayRecorderConnection.cs | 4 +- OpenRA.Game/OpenRA.Game.csproj | 1 + OpenRA.Game/Player.cs | 1 + OpenRA.Game/Widgets/ImageWidget.cs | 9 +- OpenRA.Game/Widgets/LabelWidget.cs | 5 +- OpenRA.Game/Widgets/MapPreviewWidget.cs | 38 +- OpenRA.Game/Widgets/Widget.cs | 19 +- OpenRA.Game/World.cs | 28 +- OpenRA.Mods.RA/ConquestVictoryConditions.cs | 9 +- OpenRA.Mods.RA/MPStartLocations.cs | 8 +- .../Widgets/Logic/LobbyMapPreviewLogic.cs | 6 +- OpenRA.Mods.RA/Widgets/Logic/LobbyUtils.cs | 11 +- .../Widgets/Logic/ReplayBrowserLogic.cs | 354 +++++++++++++++--- .../Logic/SpawnSelectorTooltipLogic.cs | 10 +- mods/cnc/chrome/replaybrowser.yaml | 270 ++++++++++--- mods/ra/chrome/replaybrowser.yaml | 257 ++++++++----- 21 files changed, 1125 insertions(+), 412 deletions(-) create mode 100644 OpenRA.Game/GameInformation.cs diff --git a/OpenRA.Game/FileFormats/ReplayMetadata.cs b/OpenRA.Game/FileFormats/ReplayMetadata.cs index 301327228c..291111d5b9 100644 --- a/OpenRA.Game/FileFormats/ReplayMetadata.cs +++ b/OpenRA.Game/FileFormats/ReplayMetadata.cs @@ -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 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 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 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]; } - } } } diff --git a/OpenRA.Game/Game.cs b/OpenRA.Game/Game.cs index c71bfb5865..4f56b9cc42 100644 --- a/OpenRA.Game/Game.cs +++ b/OpenRA.Game/Game.cs @@ -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; diff --git a/OpenRA.Game/GameInformation.cs b/OpenRA.Game/GameInformation.cs new file mode 100644 index 0000000000..202b23100b --- /dev/null +++ b/OpenRA.Game/GameInformation.cs @@ -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 +{ + /// + /// Contains information about a finished game + /// + public class GameInformation + { + /// The map identifier. + public string MapUid; + /// The map title. + public string MapTitle; + /// Game start timestamp. + public DateTime StartTimeUtc; + /// Game end timestamp (when the recoding stopped). + public DateTime EndTimeUtc; + + /// + /// Gets the game's duration, from the time the game started until the + /// replay recording stopped. + /// + /// The game's duration. + public TimeSpan Duration { get { return EndTimeUtc > StartTimeUtc ? EndTimeUtc - StartTimeUtc : TimeSpan.Zero; } } + /// + /// Gets the list of players. + /// + /// The players. + public IList Players { get; private set; } + /// + /// Gets the map preview, using and the . + /// + /// The map preview. + public MapPreview MapPreview { get { return Game.modData.MapCache[MapUid]; } } + /// + /// Gets the human players. + /// + /// The human players. + public IEnumerable HumanPlayers { get { return Players.Where(p => p.IsHuman); } } + /// + /// Gets a value indicating whether this instance has just one human player. + /// + /// true if this instance has just one human player; otherwise, false. + public bool IsSinglePlayer { get { return HumanPlayers.Count() == 1; } } + + Dictionary playersByRuntime; + + /// + /// Initializes a new instance of the class. + /// + public GameInformation() + { + Players = new List(); + playersByRuntime = new Dictionary(); + } + + /// + /// Deserialize the specified data into a new instance. + /// + /// Data. + 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(node.Value)); + break; + } + } + + return info; + } + catch (InvalidOperationException) + { + Log.Write("exception", "GameInformation deserialized invalid MiniYaml:\n{0}".F(data)); + throw; + } + } + + /// + /// Serialize this instance. + /// + public string Serialize() + { + var nodes = new List(); + + nodes.Add(new MiniYamlNode("Root", FieldSaver.Save(this))); + + for (var i=0; i + /// Adds the start-up player information. + /// + /// Runtime player. + /// Lobby info. + 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); + } + + /// + /// Gets the player information for the specified runtime player instance. + /// + /// The player, or null. + /// Runtime player. + public Player GetPlayer(OpenRA.Player runtimePlayer) + { + Player player; + + playersByRuntime.TryGetValue(runtimePlayer, out player); + + return player; + } + + /// Specifies whether the player was defeated, victorious, or there was no outcome defined. + public enum GameOutcome + { + /// Unknown outcome. + Undefined, + /// The player was defeated + Defeat, + /// The player was victorious + Victory + } + + /// + /// Information about a player + /// + public class Player + { + // + // Start-up information + // + + /// The client index. + public int ClientIndex; + /// The player name, not guaranteed to be unique. + public string Name; + /// true if the player is a human player; otherwise, false. + public bool IsHuman; + /// true if the player is a bot; otherwise, false. + public bool IsBot; + /// The faction name (aka Country). + public string FactionName; + /// The faction id (aka Country, aka Race). + public string FactionId; + /// The color used by the player in the game. + public HSLColor Color; + /// The team id on start-up, or 0 if the player is not part of the team. + public int Team; + /// The index of the spawn point on the map, or 0 if the player is not part of the team. + public int SpawnPoint; + /// true if the faction was chosen at random; otherwise, false. + public bool IsRandomFaction; + /// true if the spawn point was chosen at random; otherwise, false. + public bool IsRandomSpawnPoint; + + // + // Information gathered at a later stage + // + + /// The game outcome for this player. + public GameOutcome Outcome; + /// The time when this player won or lost the game. + public DateTime OutcomeTimestampUtc; + } + } +} diff --git a/OpenRA.Game/Graphics/ChromeProvider.cs b/OpenRA.Game/Graphics/ChromeProvider.cs index 2e9d17ea28..79630d8e1c 100644 --- a/OpenRA.Game/Graphics/ChromeProvider.cs +++ b/OpenRA.Game/Graphics/ChromeProvider.cs @@ -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 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()); - cachedSprites[collection].Add(image, mi.GetImage(sheet)); + if (cachedCollection == null) + { + cachedCollection = new Dictionary(); + cachedSprites.Add(collectionName, cachedCollection); + } + var image = mi.GetImage(sheet); + cachedCollection.Add(imageName, image); - return cachedSprites[collection][image]; + return image; } } } diff --git a/OpenRA.Game/Network/ReplayConnection.cs b/OpenRA.Game/Network/ReplayConnection.cs index b1eefce4c8..275d3003d7 100755 --- a/OpenRA.Game/Network/ReplayConnection.cs +++ b/OpenRA.Game/Network/ReplayConnection.cs @@ -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 diff --git a/OpenRA.Game/Network/ReplayRecorderConnection.cs b/OpenRA.Game/Network/ReplayRecorderConnection.cs index 5066b551de..d5eaf19c70 100644 --- a/OpenRA.Game/Network/ReplayRecorderConnection.cs +++ b/OpenRA.Game/Network/ReplayRecorderConnection.cs @@ -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); } diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index 28bec9885b..566ccfa344 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -248,6 +248,7 @@ + diff --git a/OpenRA.Game/Player.cs b/OpenRA.Game/Player.cs index e269f80de2..ff5d5a8a2f 100644 --- a/OpenRA.Game/Player.cs +++ b/OpenRA.Game/Player.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; } diff --git a/OpenRA.Game/Widgets/ImageWidget.cs b/OpenRA.Game/Widgets/ImageWidget.cs index 5c5eeadce0..90192f8419 100644 --- a/OpenRA.Game/Widgets/ImageWidget.cs +++ b/OpenRA.Game/Widgets/ImageWidget.cs @@ -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); } } } diff --git a/OpenRA.Game/Widgets/LabelWidget.cs b/OpenRA.Game/Widgets/LabelWidget.cs index 5f454f14fe..e5110b1176 100644 --- a/OpenRA.Game/Widgets/LabelWidget.cs +++ b/OpenRA.Game/Widgets/LabelWidget.cs @@ -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; diff --git a/OpenRA.Game/Widgets/MapPreviewWidget.cs b/OpenRA.Game/Widgets/MapPreviewWidget.cs index 928066c406..f047b77c16 100644 --- a/OpenRA.Game/Widgets/MapPreviewWidget.cs +++ b/OpenRA.Game/Widgets/MapPreviewWidget.cs @@ -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 Preview = () => null; - public Func> SpawnClients = () => new Dictionary(); + public Func> SpawnOccupants = () => new Dictionary(); public Action 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) diff --git a/OpenRA.Game/Widgets/Widget.cs b/OpenRA.Game/Widgets/Widget.cs index 58fbe38dd0..2f5ee2c2d1 100644 --- a/OpenRA.Game/Widgets/Widget.cs +++ b/OpenRA.Game/Widgets/Widget.cs @@ -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().Reverse()) c.Removed(); } diff --git a/OpenRA.Game/World.cs b/OpenRA.Game/World.cs index 78416db7c1..df94effcd2 100644 --- a/OpenRA.Game/World.cs +++ b/OpenRA.Game/World.cs @@ -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(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; + } } } diff --git a/OpenRA.Mods.RA/ConquestVictoryConditions.cs b/OpenRA.Mods.RA/ConquestVictoryConditions.cs index 52dd79eb97..170e1feeaf 100644 --- a/OpenRA.Mods.RA/ConquestVictoryConditions.cs +++ b/OpenRA.Mods.RA/ConquestVictoryConditions.cs @@ -60,6 +60,7 @@ namespace OpenRA.Mods.RA { if (self.Owner.WinState == WinState.Lost) return; self.Owner.WinState = WinState.Lost; + self.World.OnPlayerWinStateChanged(self.Owner); Game.Debug("{0} is defeated.".F(self.Owner.PlayerName)); @@ -68,8 +69,6 @@ namespace OpenRA.Mods.RA if (self.Owner == self.World.LocalPlayer) { - self.World.OnLocalPlayerWinStateChanged(); - Game.RunAfterDelay(Info.NotificationDelay, () => { if (Game.IsCurrentWorld(self.World)) @@ -82,14 +81,12 @@ namespace OpenRA.Mods.RA { if (self.Owner.WinState == WinState.Won) return; self.Owner.WinState = WinState.Won; + self.World.OnPlayerWinStateChanged(self.Owner); Game.Debug("{0} is victorious.".F(self.Owner.PlayerName)); - if (self.Owner == self.World.LocalPlayer) - { - self.World.OnLocalPlayerWinStateChanged(); + if (self.Owner == self.World.LocalPlayer) Game.RunAfterDelay(Info.NotificationDelay, () => Sound.PlayNotification(self.World.Map.Rules, self.Owner, "Speech", "Win", self.Owner.Country.Race)); - } } } diff --git a/OpenRA.Mods.RA/MPStartLocations.cs b/OpenRA.Mods.RA/MPStartLocations.cs index 37ff45d9c9..0ae43f4d16 100755 --- a/OpenRA.Mods.RA/MPStartLocations.cs +++ b/OpenRA.Mods.RA/MPStartLocations.cs @@ -28,7 +28,7 @@ namespace OpenRA.Mods.RA public void WorldLoaded(World world, WorldRenderer wr) { - var spawns = world.Map.GetSpawnPoints(); + var spawns = world.Map.GetSpawnPoints().ToList(); var taken = world.LobbyInfo.Clients.Where(c => c.SpawnPoint != 0 && c.Slot != null) .Select(c => spawns[c.SpawnPoint-1]).ToList(); var available = spawns.Except(taken).ToList(); @@ -42,9 +42,13 @@ namespace OpenRA.Mods.RA var client = world.LobbyInfo.ClientInSlot(kv.Key); var spid = (client == null || client.SpawnPoint == 0) ? ChooseSpawnPoint(world, available, taken) - : world.Map.GetSpawnPoints()[client.SpawnPoint-1]; + : spawns[client.SpawnPoint-1]; Start.Add(player, spid); + + player.SpawnPoint = (client == null || client.SpawnPoint == 0) + ? spawns.IndexOf(spid) + 1 + : client.SpawnPoint; } // Explore allied shroud diff --git a/OpenRA.Mods.RA/Widgets/Logic/LobbyMapPreviewLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/LobbyMapPreviewLogic.cs index 42181fceeb..67a551b9a6 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/LobbyMapPreviewLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/LobbyMapPreviewLogic.cs @@ -31,7 +31,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic var preview = available.Get("MAP_PREVIEW"); preview.Preview = () => lobby.Map; preview.OnMouseDown = mi => LobbyUtils.SelectSpawnPoint(orderManager, preview, lobby.Map, mi); - preview.SpawnClients = () => LobbyUtils.GetSpawnClients(orderManager.LobbyInfo, lobby.Map); + preview.SpawnOccupants = () => LobbyUtils.GetSpawnClients(orderManager.LobbyInfo, lobby.Map); var title = available.GetOrNull("MAP_TITLE"); if (title != null) @@ -73,7 +73,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic var preview = download.Get("MAP_PREVIEW"); preview.Preview = () => lobby.Map; preview.OnMouseDown = mi => LobbyUtils.SelectSpawnPoint(orderManager, preview, lobby.Map, mi); - preview.SpawnClients = () => LobbyUtils.GetSpawnClients(orderManager.LobbyInfo, lobby.Map); + preview.SpawnOccupants = () => LobbyUtils.GetSpawnClients(orderManager.LobbyInfo, lobby.Map); var title = download.GetOrNull("MAP_TITLE"); if (title != null) @@ -100,7 +100,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic var preview = progress.Get("MAP_PREVIEW"); preview.Preview = () => lobby.Map; preview.OnMouseDown = mi => LobbyUtils.SelectSpawnPoint(orderManager, preview, lobby.Map, mi); - preview.SpawnClients = () => LobbyUtils.GetSpawnClients(orderManager.LobbyInfo, lobby.Map); + preview.SpawnOccupants = () => LobbyUtils.GetSpawnClients(orderManager.LobbyInfo, lobby.Map); var title = progress.GetOrNull("MAP_TITLE"); if (title != null) diff --git a/OpenRA.Mods.RA/Widgets/Logic/LobbyUtils.cs b/OpenRA.Mods.RA/Widgets/Logic/LobbyUtils.cs index 6a36afcdca..ad7608ccfc 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/LobbyUtils.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/LobbyUtils.cs @@ -149,12 +149,19 @@ namespace OpenRA.Mods.RA.Widgets.Logic color.AttachPanel(colorChooser, onExit); } - public static Dictionary GetSpawnClients(Session lobbyInfo, MapPreview preview) + public static Dictionary GetSpawnClients(Session lobbyInfo, MapPreview preview) { var spawns = preview.SpawnPoints; return lobbyInfo.Clients .Where(c => c.SpawnPoint != 0) - .ToDictionary(c => spawns[c.SpawnPoint - 1], c => c); + .ToDictionary(c => spawns[c.SpawnPoint - 1], c => new SpawnOccupant(c)); + } + public static Dictionary GetSpawnClients(IEnumerable players, MapPreview preview) + { + var spawns = preview.SpawnPoints; + return players + .Where(c => c.SpawnPoint != 0) + .ToDictionary(c => spawns[c.SpawnPoint - 1], c => new SpawnOccupant(c)); } public static void SelectSpawnPoint(OrderManager orderManager, MapPreviewWidget mapPreview, MapPreview preview, MouseInput mi) diff --git a/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs index 1d68e7e8db..9277e3b938 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs @@ -1,6 +1,6 @@ #region Copyright & License Information /* - * Copyright 2007-2013 The OpenRA Developers (see AUTHORS) + * 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, @@ -25,12 +25,12 @@ namespace OpenRA.Mods.RA.Widgets.Logic static Filter filter = new Filter(); Widget panel; - ScrollPanelWidget playerList; + ScrollPanelWidget replayList, playerList; ScrollItemWidget playerTemplate, playerHeader; List replays; - Dictionary replayVis = new Dictionary(); + Dictionary replayState = new Dictionary(); - Dictionary selectedSpawns; + Dictionary selectedSpawns; ReplayMetadata selectedReplay; [ObjectCreator.UseCtor] @@ -45,13 +45,13 @@ namespace OpenRA.Mods.RA.Widgets.Logic panel.Get("CANCEL_BUTTON").OnClick = () => { Ui.CloseWindow(); onExit(); }; - var rl = panel.Get("REPLAY_LIST"); + replayList = panel.Get("REPLAY_LIST"); var template = panel.Get("REPLAY_TEMPLATE"); var mod = Game.modData.Manifest.Mod; var dir = new[] { Platform.SupportDir, "Replays", mod.Id, mod.Version }.Aggregate(Path.Combine); - rl.RemoveChildren(); + replayList.RemoveChildren(); if (Directory.Exists(dir)) { using (new Support.PerfTimer("Load replays")) @@ -60,37 +60,38 @@ namespace OpenRA.Mods.RA.Widgets.Logic .GetFiles(dir, "*.rep") .Select((filename) => ReplayMetadata.Read(filename)) .Where((r) => r != null) - .OrderByDescending(r => r.StartTimestampUtc) + .OrderByDescending(r => r.GameInfo.StartTimeUtc) .ToList(); } foreach (var replay in replays) - AddReplay(rl, replay, template); + AddReplay(replay, template); ApplyFilter(); } var watch = panel.Get("WATCH_BUTTON"); - watch.IsDisabled = () => selectedReplay == null || selectedReplay.MapPreview.Status != MapStatus.Available; + watch.IsDisabled = () => selectedReplay == null || selectedReplay.GameInfo.MapPreview.Status != MapStatus.Available; watch.OnClick = () => { WatchReplay(); onStart(); }; panel.Get("REPLAY_INFO").IsVisible = () => selectedReplay != null; var preview = panel.Get("MAP_PREVIEW"); - preview.SpawnClients = () => selectedSpawns; - preview.Preview = () => selectedReplay != null ? selectedReplay.MapPreview : null; + preview.SpawnOccupants = () => selectedSpawns; + preview.Preview = () => selectedReplay != null ? selectedReplay.GameInfo.MapPreview : null; var title = panel.GetOrNull("MAP_TITLE"); if (title != null) - title.GetText = () => selectedReplay != null ? selectedReplay.MapPreview.Title : null; + title.GetText = () => selectedReplay != null ? selectedReplay.GameInfo.MapPreview.Title : null; var type = panel.GetOrNull("MAP_TYPE"); if (type != null) - type.GetText = () => selectedReplay.MapPreview.Type; + type.GetText = () => selectedReplay.GameInfo.MapPreview.Type; - panel.Get("DURATION").GetText = () => WidgetUtils.FormatTimeSeconds((int)selectedReplay.Duration.TotalSeconds); + panel.Get("DURATION").GetText = () => WidgetUtils.FormatTimeSeconds((int)selectedReplay.GameInfo.Duration.TotalSeconds); SetupFilters(); + SetupManagement(); } void SetupFilters() @@ -204,25 +205,89 @@ namespace OpenRA.Mods.RA.Widgets.Logic } // - // Outcome + // Map + // + { + var ddb = panel.GetOrNull("FLT_MAPNAME_DROPDOWNBUTTON"); + if (ddb != null) + { + var options = new HashSet(replays.Select(r => r.GameInfo.MapTitle), StringComparer.OrdinalIgnoreCase).ToList(); + options.Sort(StringComparer.OrdinalIgnoreCase); + options.Insert(0, null); // no filter + + var anyText = ddb.GetText(); + ddb.GetText = () => string.IsNullOrEmpty(filter.MapName) ? anyText : filter.MapName; + ddb.OnMouseDown = _ => + { + Func setupItem = (option, tpl) => + { + var item = ScrollItemWidget.Setup( + tpl, + () => string.Compare(filter.MapName, option, true) == 0, + () => { filter.MapName = option; ApplyFilter(); } + ); + item.Get("LABEL").GetText = () => option ?? anyText; + return item; + }; + + ddb.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", options.Count * 30, options, setupItem); + }; + } + } + + // + // Players + // + { + var ddb = panel.GetOrNull("FLT_PLAYER_DROPDOWNBUTTON"); + if (ddb != null) + { + var options = new HashSet(replays.SelectMany(r => r.GameInfo.Players.Select(p => p.Name)), StringComparer.OrdinalIgnoreCase).ToList(); + options.Sort(StringComparer.OrdinalIgnoreCase); + options.Insert(0, null); // no filter + + var anyText = ddb.GetText(); + ddb.GetText = () => string.IsNullOrEmpty(filter.PlayerName) ? anyText : filter.PlayerName; + ddb.OnMouseDown = _ => + { + Func setupItem = (option, tpl) => + { + var item = ScrollItemWidget.Setup( + tpl, + () => string.Compare(filter.PlayerName, option, true) == 0, + () => { filter.PlayerName = option; ApplyFilter(); } + ); + item.Get("LABEL").GetText = () => option ?? anyText; + return item; + }; + + ddb.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", options.Count * 30, options, setupItem); + }; + } + } + + // + // Outcome (depends on Player) // { var ddb = panel.GetOrNull("FLT_OUTCOME_DROPDOWNBUTTON"); if (ddb != null) { + ddb.IsDisabled = () => string.IsNullOrEmpty(filter.PlayerName); + // Using list to maintain the order - var options = new List> + var options = new List> { - new KeyValuePair(WinState.Undefined, ddb.GetText()), - new KeyValuePair(WinState.Won, "Won"), - new KeyValuePair(WinState.Lost, "Lost") + new KeyValuePair(GameInformation.GameOutcome.Undefined, ddb.GetText()), + new KeyValuePair(GameInformation.GameOutcome.Defeat, "Defeat"), + new KeyValuePair(GameInformation.GameOutcome.Victory, "Victory") }; var lookup = options.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); ddb.GetText = () => lookup[filter.Outcome]; ddb.OnMouseDown = _ => { - Func, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) => + Func, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) => { var item = ScrollItemWidget.Setup( tpl, @@ -239,28 +304,30 @@ namespace OpenRA.Mods.RA.Widgets.Logic } // - // Players + // Faction (depends on Player) // { - var ddb = panel.GetOrNull("FLT_PLAYER_DROPDOWNBUTTON"); + var ddb = panel.GetOrNull("FLT_FACTION_DROPDOWNBUTTON"); if (ddb != null) { - var options = new HashSet(replays.SelectMany(r => r.LobbyInfo.Value.Clients.Select(c => c.Name)), StringComparer.OrdinalIgnoreCase).ToList(); + ddb.IsDisabled = () => string.IsNullOrEmpty(filter.PlayerName); + + var options = new HashSet(replays.SelectMany(r => r.GameInfo.Players.Select(p => p.FactionName).Where(n => !string.IsNullOrEmpty(n))), StringComparer.OrdinalIgnoreCase).ToList(); options.Sort(StringComparer.OrdinalIgnoreCase); options.Insert(0, null); // no filter - var nobodyText = ddb.GetText(); - ddb.GetText = () => string.IsNullOrEmpty(filter.PlayerName) ? nobodyText : filter.PlayerName; + var anyText = ddb.GetText(); + ddb.GetText = () => string.IsNullOrEmpty(filter.Faction) ? anyText : filter.Faction; ddb.OnMouseDown = _ => { Func setupItem = (option, tpl) => { var item = ScrollItemWidget.Setup( tpl, - () => string.Compare(filter.PlayerName, option, true) == 0, - () => { filter.PlayerName = option; ApplyFilter(); } + () => string.Compare(filter.Faction, option, true) == 0, + () => { filter.Faction = option; ApplyFilter(); } ); - item.Get("LABEL").GetText = () => option ?? nobodyText; + item.Get("LABEL").GetText = () => option ?? anyText; return item; }; @@ -268,12 +335,152 @@ namespace OpenRA.Mods.RA.Widgets.Logic }; } } + + // + // Reset button + // + { + var button = panel.Get("FLT_RESET_BUTTON"); + button.IsDisabled = () => filter.IsEmpty; + button.OnClick = () => { filter = new Filter(); ApplyFilter(); }; + } + } + + void SetupManagement() + { + { + var button = panel.Get("MNG_RENSEL_BUTTON"); + button.IsDisabled = () => selectedReplay == null; + button.OnClick = () => + { + var r = selectedReplay; + var initialName = Path.GetFileNameWithoutExtension(r.FilePath); + var directoryName = Path.GetDirectoryName(r.FilePath); + var invalidChars = Path.GetInvalidFileNameChars(); + + ConfirmationDialogs.TextInputPrompt( + "Rename Replay", + "Enter a new file name:", + initialName, + onAccept: (newName) => + { + RenameReplay(r, newName); + }, + onCancel: null, + acceptText: "Rename", + cancelText: null, + inputValidator: (newName) => + { + if (newName == initialName) + return false; + + if (string.IsNullOrWhiteSpace(newName)) + return false; + + if (newName.IndexOfAny(invalidChars) >= 0) + return false; + + if (File.Exists(Path.Combine(directoryName, newName))) + return false; + + return true; + }); + }; + } + + Action onDeleteReplay = (r, after) => + { + ConfirmationDialogs.PromptConfirmAction( + "Delete selected replay?", + "Delete replay '{0}'?".F(Path.GetFileNameWithoutExtension(r.FilePath)), + () => + { + DeleteReplay(r); + if (after != null) + after.Invoke(); + }, + null, + "Delete"); + }; + + { + var button = panel.Get("MNG_DELSEL_BUTTON"); + button.IsDisabled = () => selectedReplay == null; + button.OnClick = () => + { + onDeleteReplay(selectedReplay, () => { if (selectedReplay == null) SelectFirstVisibleReplay(); }); + }; + } + + { + var button = panel.Get("MNG_DELALL_BUTTON"); + button.IsDisabled = () => replayState.Count(kvp => kvp.Value.Visible) == 0; + button.OnClick = () => + { + var list = replayState.Where(kvp => kvp.Value.Visible).Select(kvp => kvp.Key).ToList(); + if (list.Count == 0) + return; + + if (list.Count == 1) + { + onDeleteReplay(list[0], () => { if (selectedReplay == null) SelectFirstVisibleReplay(); }); + return; + } + + ConfirmationDialogs.PromptConfirmAction( + "Delete all selected replays?", + "Delete {0} replays?".F(list.Count), + () => + { + list.ForEach((r) => DeleteReplay(r)); + if (selectedReplay == null) + SelectFirstVisibleReplay(); + }, + null, + "Delete All"); + }; + } + } + + void RenameReplay(ReplayMetadata replay, string newFilenameWithoutExtension) + { + try + { + replay.RenameFile(newFilenameWithoutExtension); + replayState[replay].Item.Text = newFilenameWithoutExtension; + } + catch (Exception ex) + { + Log.Write("debug", ex.ToString()); + return; + } + } + + void DeleteReplay(ReplayMetadata replay) + { + try + { + File.Delete(replay.FilePath); + } + catch (Exception ex) + { + Game.Debug("Failed to delete replay file '{0}'. See the logs for details.", replay.FilePath); + Log.Write("debug", ex.ToString()); + return; + } + + if (replay == selectedReplay) + SelectReplay(null); + + replayList.RemoveChild(replayState[replay].Item); + replays.Remove(replay); + replayState.Remove(replay); } bool EvaluateReplayVisibility(ReplayMetadata replay) { // Game type - if ((filter.Type == GameType.Multiplayer && replay.LobbyInfo.Value.IsSinglePlayer) || (filter.Type == GameType.Singleplayer && !replay.LobbyInfo.Value.IsSinglePlayer)) + if ((filter.Type == GameType.Multiplayer && replay.GameInfo.IsSinglePlayer) || (filter.Type == GameType.Singleplayer && !replay.GameInfo.IsSinglePlayer)) return false; // Date type @@ -295,14 +502,14 @@ namespace OpenRA.Mods.RA.Widgets.Logic t = TimeSpan.FromDays(30d); break; } - if (replay.StartTimestampUtc < DateTime.UtcNow.Subtract(t)) + if (replay.GameInfo.StartTimeUtc < DateTime.UtcNow - t) return false; } // Duration if (filter.Duration != DurationType.Any) { - double minutes = replay.Duration.TotalMinutes; + double minutes = replay.GameInfo.Duration.TotalMinutes; switch (filter.Duration) { case DurationType.VeryShort: @@ -327,16 +534,24 @@ namespace OpenRA.Mods.RA.Widgets.Logic } } - // Outcome - if (filter.Outcome != WinState.Undefined && filter.Outcome != replay.Outcome) + // Map + if (!string.IsNullOrEmpty(filter.MapName) && string.Compare(filter.MapName, replay.GameInfo.MapTitle, true) != 0) return false; // Player if (!string.IsNullOrEmpty(filter.PlayerName)) { - var player = replay.LobbyInfo.Value.Clients.Find(c => string.Compare(filter.PlayerName, c.Name, true) == 0); + var player = replay.GameInfo.Players.FirstOrDefault(p => string.Compare(filter.PlayerName, p.Name, true) == 0); if (player == null) return false; + + // Outcome + if (filter.Outcome != GameInformation.GameOutcome.Undefined && filter.Outcome != player.Outcome) + return false; + + // Faction + if (!string.IsNullOrEmpty(filter.Faction) && string.Compare(filter.Faction, player.FactionName, true) != 0) + return false; } return true; @@ -345,41 +560,41 @@ namespace OpenRA.Mods.RA.Widgets.Logic void ApplyFilter() { foreach (var replay in replays) - replayVis[replay] = EvaluateReplayVisibility(replay); + replayState[replay].Visible = EvaluateReplayVisibility(replay); - if (selectedReplay == null || replayVis[selectedReplay] == false) + if (selectedReplay == null || replayState[selectedReplay].Visible == false) SelectFirstVisibleReplay(); - panel.Get("REPLAY_LIST").Layout.AdjustChildren(); + replayList.Layout.AdjustChildren(); } void SelectFirstVisibleReplay() { - SelectReplay(replays.FirstOrDefault(r => replayVis[r])); + SelectReplay(replays.FirstOrDefault(r => replayState[r].Visible)); } void SelectReplay(ReplayMetadata replay) { selectedReplay = replay; - selectedSpawns = (selectedReplay != null) ? LobbyUtils.GetSpawnClients(selectedReplay.LobbyInfo.Value, selectedReplay.MapPreview) : null; + selectedSpawns = (selectedReplay != null) + ? LobbyUtils.GetSpawnClients(selectedReplay.GameInfo.Players, selectedReplay.GameInfo.MapPreview) + : new Dictionary(); if (replay == null) return; try { - var lobby = replay.LobbyInfo.Value; - - var clients = lobby.Clients.Where(c => c.Slot != null) - .GroupBy(c => c.Team) + var players = replay.GameInfo.Players + .GroupBy(p => p.Team) .OrderBy(g => g.Key); - var teams = new Dictionary>(); - var noTeams = clients.Count() == 1; - foreach (var c in clients) + var teams = new Dictionary>(); + var noTeams = players.Count() == 1; + foreach (var p in players) { - var label = noTeams ? "Players" : c.Key == 0 ? "No Team" : "Team {0}".F(c.Key); - teams.Add(label, c); + var label = noTeams ? "Players" : p.Key == 0 ? "No Team" : "Team {0}".F(p.Key); + teams.Add(label, p); } playerList.RemoveChildren(); @@ -408,7 +623,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic var flag = item.Get("FLAG"); flag.GetImageCollection = () => "flags"; - flag.GetImageName = () => o.Country; + flag.GetImageName = () => o.FactionId; playerList.AddChild(item); } @@ -430,16 +645,29 @@ namespace OpenRA.Mods.RA.Widgets.Logic } } - void AddReplay(ScrollPanelWidget list, ReplayMetadata replay, ScrollItemWidget template) + void AddReplay(ReplayMetadata replay, ScrollItemWidget template) { var item = ScrollItemWidget.Setup(template, () => selectedReplay == replay, () => SelectReplay(replay), () => WatchReplay()); - var f = Path.GetFileNameWithoutExtension(replay.FilePath); - item.Get("TITLE").GetText = () => f; - item.IsVisible = () => { bool visible; return replayVis.TryGetValue(replay, out visible) && visible; }; - list.AddChild(item); + + replayState[replay] = new ReplayState + { + Item = item, + Visible = true + }; + + item.Text = Path.GetFileNameWithoutExtension(replay.FilePath); + item.Get("TITLE").GetText = () => item.Text; + item.IsVisible = () => replayState[replay].Visible; + replayList.AddChild(item); + } + + class ReplayState + { + public bool Visible; + public ScrollItemWidget Item; } class Filter @@ -447,8 +675,24 @@ namespace OpenRA.Mods.RA.Widgets.Logic public GameType Type; public DateType Date; public DurationType Duration; - public WinState Outcome = WinState.Undefined; + public GameInformation.GameOutcome Outcome; public string PlayerName; + public string MapName; + public string Faction; + + public bool IsEmpty + { + get + { + return Type == default(GameType) + && Date == default(DateType) + && Duration == default(DurationType) + && Outcome == default(GameInformation.GameOutcome) + && string.IsNullOrEmpty(PlayerName) + && string.IsNullOrEmpty(MapName) + && string.IsNullOrEmpty(Faction); + } + } } enum GameType { diff --git a/OpenRA.Mods.RA/Widgets/Logic/SpawnSelectorTooltipLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/SpawnSelectorTooltipLogic.cs index 77cede929f..af64618a50 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/SpawnSelectorTooltipLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/SpawnSelectorTooltipLogic.cs @@ -38,10 +38,10 @@ namespace OpenRA.Mods.RA.Widgets.Logic tooltipContainer.BeforeRender = () => { - var client = preview.SpawnClients().Values.FirstOrDefault(c => c.SpawnPoint == preview.TooltipSpawnIndex); + var occupant = preview.SpawnOccupants().Values.FirstOrDefault(c => c.SpawnPoint == preview.TooltipSpawnIndex); var teamWidth = 0; - if (client == null) + if (occupant == null) { labelText = "Available spawn"; playerCountry = null; @@ -50,9 +50,9 @@ namespace OpenRA.Mods.RA.Widgets.Logic } else { - labelText = client.Name; - playerCountry = client.Country; - playerTeam = client.Team; + labelText = occupant.PlayerName; + playerCountry = occupant.Country; + playerTeam = occupant.Team; widget.Bounds.Height = playerTeam > 0 ? doubleHeight : singleHeight; teamWidth = teamFont.Measure(team.GetText()).X; } diff --git a/mods/cnc/chrome/replaybrowser.yaml b/mods/cnc/chrome/replaybrowser.yaml index f74b8822de..714ae37607 100644 --- a/mods/cnc/chrome/replaybrowser.yaml +++ b/mods/cnc/chrome/replaybrowser.yaml @@ -1,80 +1,250 @@ Container@REPLAYBROWSER_PANEL: Logic: ReplayBrowserLogic X: (WINDOW_RIGHT - WIDTH)/2 - Y: (WINDOW_BOTTOM - 500)/2 - Width: 520 - Height: 535 + Y: (WINDOW_BOTTOM - HEIGHT)/2 + Width: 780 + Height: 500 Children: Label@TITLE: - Width: 520 + Width: PARENT_RIGHT Y: 0-25 Font: BigBold Contrast: true Align: Center Text: Replay Viewer Background@bg: - Width: 520 - Height: 500 + Width: PARENT_RIGHT + Height: PARENT_BOTTOM Background: panel-black Children: - ScrollPanel@REPLAY_LIST: - X: 15 - Y: 15 - Width: 282 - Height: PARENT_BOTTOM-30 + Container@FILTER_AND_MANAGE_CONTAINER: + X: 20 + Y: 20 + Width: 280 + Height: PARENT_BOTTOM - 40 Children: - ScrollItem@REPLAY_TEMPLATE: - Width: PARENT_RIGHT-27 - Height: 25 - X: 2 - Y: 0 - Visible: false + Container@FILTERS: + Width: 280 + Height: 320 Children: - Label@TITLE: - X: 10 - Width: PARENT_RIGHT-20 + Label@FILTERS_TITLE: + X: 85 + Width: PARENT_RIGHT - 85 Height: 25 - Background@MAP_BG: - X: PARENT_RIGHT-WIDTH-15 - Y: 15 - Width: 194 - Height: 194 - Background: panel-gray + Font: Bold + Align: Center + Text: Filter + Label@FLT_GAMETYPE_DESC: + X: 0 + Y: 30 + Width: 80 + Height: 25 + Text: Type: + Align: Right + DropDownButton@FLT_GAMETYPE_DROPDOWNBUTTON: + X: 85 + Y: 30 + Width: PARENT_RIGHT - 85 + Height: 25 + Text: Any + Label@FLT_DATE_DESC: + X: 0 + Y: 60 + Width: 80 + Height: 25 + Text: Date: + Align: Right + DropDownButton@FLT_DATE_DROPDOWNBUTTON: + X: 85 + Y: 60 + Width: PARENT_RIGHT - 85 + Height: 25 + Text: Any + Label@FLT_DURATION_DESC: + X: 0 + Y: 90 + Width: 80 + Height: 25 + Text: Duration: + Align: Right + DropDownButton@FLT_DURATION_DROPDOWNBUTTON: + X: 85 + Y: 90 + Width: PARENT_RIGHT - 85 + Height: 25 + Text: Any + Label@FLT_MAPNAME_DESC: + X: 0 + Y: 120 + Width: 80 + Height: 25 + Text: Map: + Align: Right + DropDownButton@FLT_MAPNAME_DROPDOWNBUTTON: + X: 85 + Y: 120 + Width: PARENT_RIGHT - 85 + Height: 25 + Text: Any + Label@FLT_PLAYER_DESC: + X: 0 + Y: 150 + Width: 80 + Height: 25 + Text: Player: + Align: Right + DropDownButton@FLT_PLAYER_DROPDOWNBUTTON: + X: 85 + Y: 150 + Width: PARENT_RIGHT - 85 + Height: 25 + Text: Anyone + Label@FLT_OUTCOME_DESC: + X: 0 + Y: 180 + Width: 80 + Height: 25 + Text: Outcome: + Align: Right + DropDownButton@FLT_OUTCOME_DROPDOWNBUTTON: + X: 85 + Y: 180 + Width: PARENT_RIGHT - 85 + Height: 25 + Text: Any + Label@FLT_FACTION_DESC: + X: 0 + Y: 210 + Width: 80 + Height: 25 + Text: Faction: + Align: Right + DropDownButton@FLT_FACTION_DROPDOWNBUTTON: + X: 85 + Y: 210 + Width: PARENT_RIGHT - 85 + Height: 25 + Text: Any + Button@FLT_RESET_BUTTON: + X: 85 + Y: 250 + Width: PARENT_RIGHT - 85 + Height: 25 + Text: Reset Filters + Font: Bold + Container@MANAGEMENT: + X: 85 + Y: PARENT_BOTTOM - 115 + Width: PARENT_RIGHT - 85 + Height: 115 + Children: + Label@MANAGE_TITLE: + Width: PARENT_RIGHT + Height: 25 + Font: Bold + Align: Center + Text: Manage + Button@MNG_RENSEL_BUTTON: + Y: 30 + Width: PARENT_RIGHT + Height: 25 + Text: Rename + Font: Bold + Key: F2 + Button@MNG_DELSEL_BUTTON: + Y: 60 + Width: PARENT_RIGHT + Height: 25 + Text: Delete + Font: Bold + Key: Delete + Button@MNG_DELALL_BUTTON: + Y: 90 + Width: PARENT_RIGHT + Height: 25 + Text: Delete All + Font: Bold + Container@REPLAY_LIST_CONTAINER: + X: 310 + Y: 20 + Width: 245 + Height: PARENT_BOTTOM - 20 - 20 Children: - MapPreview@MAP_PREVIEW: - X: 1 - Y: 1 - Width: 192 - Height: 192 - TooltipContainer: TOOLTIP_CONTAINER - Container@REPLAY_INFO: - X: PARENT_RIGHT-WIDTH-15 - Y: 15 - Width: 194 - Height: PARENT_BOTTOM - 15 - Children: - Label@MAP_TITLE: - Y: 197 + Label@REPLAYBROWSER_LABEL_TITLE: Width: PARENT_RIGHT Height: 25 + Text: Choose Replay + Align: Center + Font: Bold + ScrollPanel@REPLAY_LIST: + X: 0 + Y: 30 + Width: PARENT_RIGHT + Height: PARENT_BOTTOM - 30 + CollapseHiddenChildren: True + Children: + ScrollItem@REPLAY_TEMPLATE: + Width: PARENT_RIGHT-27 + Height: 25 + X: 2 + Visible: false + Children: + Label@TITLE: + X: 10 + Width: PARENT_RIGHT-20 + Height: 25 + Container@MAP_BG_CONTAINER: + X: PARENT_RIGHT - WIDTH - 20 + Y: 20 + Width: 194 + Height: 30 + 194 + Children: + Label@MAP_BG_TITLE: + Width: PARENT_RIGHT + Height: 25 + Text: Preview + Align: Center + Font: Bold + Background@MAP_BG: + Y: 30 + Width: 194 + Height: 194 + Background: panel-gray + Children: + MapPreview@MAP_PREVIEW: + X: 1 + Y: 1 + Width: 192 + Height: 192 + TooltipContainer: TOOLTIP_CONTAINER + Container@REPLAY_INFO: + X: PARENT_RIGHT - WIDTH - 20 + Y: 20 + 30+194 + 10 + Width: 194 + Height: PARENT_BOTTOM - 20 - 30-194 - 10 - 20 + Children: + Label@MAP_TITLE: + Y: 0 + Width: PARENT_RIGHT + Height: 15 Font: Bold Align: Center Label@MAP_TYPE: - Y: 212 + Y: 15 Width: PARENT_RIGHT - Height: 25 + Height: 15 Font: TinyBold Align: Center Label@DURATION: - Y: 225 + Y: 30 Width: PARENT_RIGHT - Height: 25 + Height: 15 Font: Tiny Align: Center ScrollPanel@PLAYER_LIST: - Y: 250 + Y: 50 Width: PARENT_RIGHT - Height: PARENT_BOTTOM - 250 - 15 + Height: PARENT_BOTTOM - 50 IgnoreChildMouseOver: true Children: ScrollItem@HEADER: @@ -98,7 +268,7 @@ Container@REPLAYBROWSER_PANEL: Children: Image@FLAG: X: 4 - Y: 4 + Y: 6 Width: 32 Height: 16 Label@LABEL: @@ -112,14 +282,14 @@ Container@REPLAYBROWSER_PANEL: Button@CANCEL_BUTTON: Key: escape X: 0 - Y: 499 + Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 Text: Back Button@WATCH_BUTTON: Key: return - X: 380 - Y: 499 + X: PARENT_RIGHT - 140 + Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 Text: Watch diff --git a/mods/ra/chrome/replaybrowser.yaml b/mods/ra/chrome/replaybrowser.yaml index 43e42e1723..f664e5313e 100644 --- a/mods/ra/chrome/replaybrowser.yaml +++ b/mods/ra/chrome/replaybrowser.yaml @@ -2,96 +2,161 @@ Background@REPLAYBROWSER_PANEL: Logic: ReplayBrowserLogic X: (WINDOW_RIGHT - WIDTH)/2 Y: (WINDOW_BOTTOM - HEIGHT)/2 - Width: 490 + Width: 780 Height: 535 Children: - Container@FILTERS: + Container@FILTER_AND_MANAGE_CONTAINER: X: 20 Y: 20 Width: 280 - Height: 180 + Height: PARENT_BOTTOM - 75 Children: - Label@FILTERS_TITLE: - Width: PARENT_RIGHT - Height: 25 - Font: Bold - Align: Center - Text: Filter - Label@FLT_GAMETYPE_DESC: - X: 0 - Y: 30 - Width: 80 - Height: 25 - Text: Type: - Align: Right - DropDownButton@FLT_GAMETYPE_DROPDOWNBUTTON: + Container@FILTERS: + Width: 280 + Height: 320 + Children: + Label@FILTERS_TITLE: + X: 85 + Width: PARENT_RIGHT - 85 + Height: 25 + Font: Bold + Align: Center + Text: Filter + Label@FLT_GAMETYPE_DESC: + X: 0 + Y: 30 + Width: 80 + Height: 25 + Text: Type: + Align: Right + DropDownButton@FLT_GAMETYPE_DROPDOWNBUTTON: + X: 85 + Y: 30 + Width: PARENT_RIGHT - 85 + Height: 25 + Text: Any + Label@FLT_DATE_DESC: + X: 0 + Y: 60 + Width: 80 + Height: 25 + Text: Date: + Align: Right + DropDownButton@FLT_DATE_DROPDOWNBUTTON: + X: 85 + Y: 60 + Width: PARENT_RIGHT - 85 + Height: 25 + Text: Any + Label@FLT_DURATION_DESC: + X: 0 + Y: 90 + Width: 80 + Height: 25 + Text: Duration: + Align: Right + DropDownButton@FLT_DURATION_DROPDOWNBUTTON: + X: 85 + Y: 90 + Width: PARENT_RIGHT - 85 + Height: 25 + Text: Any + Label@FLT_MAPNAME_DESC: + X: 0 + Y: 120 + Width: 80 + Height: 25 + Text: Map: + Align: Right + DropDownButton@FLT_MAPNAME_DROPDOWNBUTTON: + X: 85 + Y: 120 + Width: PARENT_RIGHT - 85 + Height: 25 + Text: Any + Label@FLT_PLAYER_DESC: + X: 0 + Y: 150 + Width: 80 + Height: 25 + Text: Player: + Align: Right + DropDownButton@FLT_PLAYER_DROPDOWNBUTTON: + X: 85 + Y: 150 + Width: PARENT_RIGHT - 85 + Height: 25 + Text: Anyone + Label@FLT_OUTCOME_DESC: + X: 0 + Y: 180 + Width: 80 + Height: 25 + Text: Outcome: + Align: Right + DropDownButton@FLT_OUTCOME_DROPDOWNBUTTON: + X: 85 + Y: 180 + Width: PARENT_RIGHT - 85 + Height: 25 + Text: Any + Label@FLT_FACTION_DESC: + X: 0 + Y: 210 + Width: 80 + Height: 25 + Text: Faction: + Align: Right + DropDownButton@FLT_FACTION_DROPDOWNBUTTON: + X: 85 + Y: 210 + Width: PARENT_RIGHT - 85 + Height: 25 + Text: Any + Button@FLT_RESET_BUTTON: + X: 85 + Y: 250 + Width: PARENT_RIGHT - 85 + Height: 25 + Text: Reset Filters + Font: Bold + Container@MANAGEMENT: X: 85 - Y: 30 - Width: 160 - Height: 25 - Font: Regular - Text: Any - Label@FLT_DATE_DESC: - X: 0 - Y: 60 - Width: 80 - Height: 25 - Text: Date: - Align: Right - DropDownButton@FLT_DATE_DROPDOWNBUTTON: - X: 85 - Y: 60 - Width: 160 - Height: 25 - Font: Regular - Text: Any - Label@FLT_PLAYER_DESC: - X: 0 - Y: 90 - Width: 80 - Height: 25 - Text: Player: - Align: Right - DropDownButton@FLT_PLAYER_DROPDOWNBUTTON: - X: 85 - Y: 90 - Width: 160 - Height: 25 - Font: Regular - Text: Anyone - Label@FLT_OUTCOME_DESC: - X: 0 - Y: 120 - Width: 80 - Height: 25 - Text: Outcome: - Align: Right - DropDownButton@FLT_OUTCOME_DROPDOWNBUTTON: - X: 85 - Y: 120 - Width: 160 - Height: 25 - Font: Regular - Text: Any - Label@FLT_DURATION_DESC: - X: 0 - Y: 150 - Width: 80 - Height: 25 - Text: Duration: - Align: Right - DropDownButton@FLT_DURATION_DROPDOWNBUTTON: - X: 85 - Y: 150 - Width: 160 - Height: 25 - Font: Regular - Text: Any + Y: PARENT_BOTTOM - 115 + Width: PARENT_RIGHT - 85 + Height: 115 + Children: + Label@MANAGE_TITLE: + Width: PARENT_RIGHT + Height: 25 + Font: Bold + Align: Center + Text: Manage + Button@MNG_RENSEL_BUTTON: + Y: 30 + Width: PARENT_RIGHT + Height: 25 + Text: Rename + Font: Bold + Key: F2 + Button@MNG_DELSEL_BUTTON: + Y: 60 + Width: PARENT_RIGHT + Height: 25 + Text: Delete + Font: Bold + Key: Delete + Button@MNG_DELALL_BUTTON: + Y: 90 + Width: PARENT_RIGHT + Height: 25 + Text: Delete All + Font: Bold Container@REPLAY_LIST_CONTAINER: - X: 20 - Y: 210 + X: 310 + Y: 20 Width: 245 - Height: PARENT_BOTTOM - 270 + Height: PARENT_BOTTOM - 20 - 55 Children: Label@REPLAYBROWSER_LABEL_TITLE: Width: PARENT_RIGHT @@ -103,7 +168,7 @@ Background@REPLAYBROWSER_PANEL: X: 0 Y: 30 Width: PARENT_RIGHT - Height: PARENT_BOTTOM - 25 + Height: PARENT_BOTTOM - 30 CollapseHiddenChildren: True Children: ScrollItem@REPLAY_TEMPLATE: @@ -117,10 +182,10 @@ Background@REPLAYBROWSER_PANEL: Width: PARENT_RIGHT-20 Height: 25 Container@MAP_BG_CONTAINER: - X: PARENT_RIGHT-WIDTH-20 + X: PARENT_RIGHT - WIDTH - 20 Y: 20 Width: 194 - Height: 194 + Height: 30 + 194 Children: Label@MAP_BG_TITLE: Width: PARENT_RIGHT @@ -141,33 +206,33 @@ Background@REPLAYBROWSER_PANEL: Height: 192 TooltipContainer: TOOLTIP_CONTAINER Container@REPLAY_INFO: - X: PARENT_RIGHT-WIDTH - 20 - Y: 50 + X: PARENT_RIGHT - WIDTH - 20 + Y: 20 + 30+194 + 10 Width: 194 - Height: PARENT_BOTTOM - 15 + Height: PARENT_BOTTOM - 20 - 30-194 - 10 - 55 Children: Label@MAP_TITLE: - Y: 197 + Y: 0 Width: PARENT_RIGHT - Height: 25 + Height: 15 Font: Bold Align: Center Label@MAP_TYPE: - Y: 212 + Y: 15 Width: PARENT_RIGHT - Height: 25 + Height: 15 Font: TinyBold Align: Center Label@DURATION: - Y: 225 + Y: 30 Width: PARENT_RIGHT - Height: 25 + Height: 15 Font: Tiny Align: Center ScrollPanel@PLAYER_LIST: - Y: 250 + Y: 50 Width: PARENT_RIGHT - Height: PARENT_BOTTOM - 340 + Height: PARENT_BOTTOM - 50 IgnoreChildMouseOver: true Children: ScrollItem@HEADER: @@ -192,7 +257,7 @@ Background@REPLAYBROWSER_PANEL: Children: Image@FLAG: X: 4 - Y: 4 + Y: 6 Width: 32 Height: 16 Label@LABEL: From f4c52eefaed4bf37822eeae4520368fce2104050 Mon Sep 17 00:00:00 2001 From: Pavlos Touboulidis Date: Wed, 7 May 2014 20:56:51 +0300 Subject: [PATCH 07/11] Change date labels Change "Last Week" to "Last 7 days" and "Last Month" to "Last 30 days". Also added a "Last 14 days" option. --- OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs index 9277e3b938..cb8fa0cbd7 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs @@ -143,8 +143,9 @@ namespace OpenRA.Mods.RA.Widgets.Logic { new KeyValuePair(DateType.Any, ddb.GetText()), new KeyValuePair(DateType.Today, "Today"), - new KeyValuePair(DateType.LastWeek, "Last Week"), - new KeyValuePair(DateType.LastMonth, "Last Month") + new KeyValuePair(DateType.LastWeek, "Last 7 days"), + new KeyValuePair(DateType.LastFortnight, "Last 14 days"), + new KeyValuePair(DateType.LastMonth, "Last 30 days") }; var lookup = options.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); @@ -497,6 +498,10 @@ namespace OpenRA.Mods.RA.Widgets.Logic t = TimeSpan.FromDays(7d); break; + case DateType.LastFortnight: + t = TimeSpan.FromDays(14d); + break; + case DateType.LastMonth: default: t = TimeSpan.FromDays(30d); @@ -705,6 +710,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic Any, Today, LastWeek, + LastFortnight, LastMonth } enum DurationType From 38a5e326f653bf2b95ecd2dd6534116fd494b3f3 Mon Sep 17 00:00:00 2001 From: Pavlos Touboulidis Date: Tue, 13 May 2014 22:29:56 +0300 Subject: [PATCH 08/11] Fix crash when trying to read empty replay files It was crashing because it tried to seek 8 bytes before the end of a file that was empty (zero length). I also added a few more checks and another try/catch to prevent any more crashes related to damaged files. --- OpenRA.Game/FileFormats/ReplayMetadata.cs | 54 ++++++++++++++--------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/OpenRA.Game/FileFormats/ReplayMetadata.cs b/OpenRA.Game/FileFormats/ReplayMetadata.cs index 291111d5b9..5c8fd89bf1 100644 --- a/OpenRA.Game/FileFormats/ReplayMetadata.cs +++ b/OpenRA.Game/FileFormats/ReplayMetadata.cs @@ -46,8 +46,8 @@ namespace OpenRA.FileFormats if (version != MetaVersion) throw new NotSupportedException("Metadata version {0} is not supported".F(version)); - // Read game info - string data = ReadUtf8String(reader); + // Read game info (max 100K limit as a safeguard against corrupted files) + string data = ReadUtf8String(reader, 1024 * 100); GameInfo = GameInformation.Deserialize(data); } @@ -87,28 +87,38 @@ namespace OpenRA.FileFormats if (!fs.CanSeek) return null; - fs.Seek(-(4 + 4), SeekOrigin.End); - using (var reader = new BinaryReader(fs)) + if (fs.Length < 20) + return null; + + try { - var dataLength = reader.ReadInt32(); - if (reader.ReadInt32() == MetaEndMarker) + fs.Seek(-(4 + 4), SeekOrigin.End); + using (var reader = new BinaryReader(fs)) { - // go back end marker + length storage + data + version + start marker - fs.Seek(-(4 + 4 + dataLength + 4 + 4), SeekOrigin.Current); - try + var dataLength = reader.ReadInt32(); + if (reader.ReadInt32() == MetaEndMarker) { - return new ReplayMetadata(reader, path); - } - catch (InvalidOperationException ex) - { - Log.Write("debug", ex.ToString()); - } - catch (NotSupportedException ex) - { - Log.Write("debug", ex.ToString()); + // go back by (end marker + length storage + data + version + start marker) bytes + fs.Seek(-(4 + 4 + dataLength + 4 + 4), SeekOrigin.Current); + try + { + return new ReplayMetadata(reader, path); + } + catch (InvalidOperationException ex) + { + Log.Write("debug", ex.ToString()); + } + catch (NotSupportedException ex) + { + Log.Write("debug", ex.ToString()); + } } } } + catch (IOException ex) + { + Log.Write("debug", ex.ToString()); + } return null; } @@ -128,9 +138,13 @@ namespace OpenRA.FileFormats return 4 + bytes.Length; } - static string ReadUtf8String(BinaryReader reader) + static string ReadUtf8String(BinaryReader reader, int maxLength) { - return Encoding.UTF8.GetString(reader.ReadBytes(reader.ReadInt32())); + var length = reader.ReadInt32(); + if (length > maxLength) + throw new InvalidOperationException("The length of the string ({0}) is longer than the maximum allowed ({1}).".F(length, maxLength)); + + return Encoding.UTF8.GetString(reader.ReadBytes(length)); } } } From 39cc4cc014c7514a9a72824b35e08742eab4c564 Mon Sep 17 00:00:00 2001 From: Pavlos Touboulidis Date: Thu, 15 May 2014 17:50:56 +0300 Subject: [PATCH 09/11] Use "debug" channel to report loading exceptions --- OpenRA.Game/GameInformation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenRA.Game/GameInformation.cs b/OpenRA.Game/GameInformation.cs index 202b23100b..a4d191d599 100644 --- a/OpenRA.Game/GameInformation.cs +++ b/OpenRA.Game/GameInformation.cs @@ -99,7 +99,7 @@ namespace OpenRA } catch (InvalidOperationException) { - Log.Write("exception", "GameInformation deserialized invalid MiniYaml:\n{0}".F(data)); + Log.Write("debug", "GameInformation deserialized invalid MiniYaml:\n{0}".F(data)); throw; } } From fe1eb1f3e034d8c59b193dff344e63839c355b91 Mon Sep 17 00:00:00 2001 From: Pavlos Touboulidis Date: Tue, 20 May 2014 17:40:17 +0300 Subject: [PATCH 10/11] Use the stream extensions instead of the BinaryReader/Writer --- OpenRA.Game/FileFormats/ReplayMetadata.cs | 66 +++++++---------------- OpenRA.Game/StreamExts.cs | 32 +++++++++++ 2 files changed, 52 insertions(+), 46 deletions(-) diff --git a/OpenRA.Game/FileFormats/ReplayMetadata.cs b/OpenRA.Game/FileFormats/ReplayMetadata.cs index 5c8fd89bf1..1d97ef5dfe 100644 --- a/OpenRA.Game/FileFormats/ReplayMetadata.cs +++ b/OpenRA.Game/FileFormats/ReplayMetadata.cs @@ -33,21 +33,21 @@ namespace OpenRA.FileFormats GameInfo = info; } - ReplayMetadata(BinaryReader reader, string path) + ReplayMetadata(FileStream fs, string path) { FilePath = path; // Read start marker - if (reader.ReadInt32() != MetaStartMarker) + if (fs.ReadInt32() != MetaStartMarker) throw new InvalidOperationException("Expected MetaStartMarker but found an invalid value."); // Read version - var version = reader.ReadInt32(); + var version = fs.ReadInt32(); if (version != MetaVersion) throw new NotSupportedException("Metadata version {0} is not supported".F(version)); // Read game info (max 100K limit as a safeguard against corrupted files) - string data = ReadUtf8String(reader, 1024 * 100); + string data = fs.ReadString(Encoding.UTF8, 1024 * 100); GameInfo = GameInformation.Deserialize(data); } @@ -61,7 +61,8 @@ namespace OpenRA.FileFormats int dataLength = 0; { // Write lobby info data - dataLength += WriteUtf8String(writer, GameInfo.Serialize()); + writer.Flush(); + dataLength += writer.BaseStream.WriteString(Encoding.UTF8, GameInfo.Serialize()); } // Write total length & end marker @@ -93,25 +94,22 @@ namespace OpenRA.FileFormats try { fs.Seek(-(4 + 4), SeekOrigin.End); - using (var reader = new BinaryReader(fs)) + var dataLength = fs.ReadInt32(); + if (fs.ReadInt32() == MetaEndMarker) { - var dataLength = reader.ReadInt32(); - if (reader.ReadInt32() == MetaEndMarker) + // go back by (end marker + length storage + data + version + start marker) bytes + fs.Seek(-(4 + 4 + dataLength + 4 + 4), SeekOrigin.Current); + try { - // go back by (end marker + length storage + data + version + start marker) bytes - fs.Seek(-(4 + 4 + dataLength + 4 + 4), SeekOrigin.Current); - try - { - return new ReplayMetadata(reader, path); - } - catch (InvalidOperationException ex) - { - Log.Write("debug", ex.ToString()); - } - catch (NotSupportedException ex) - { - Log.Write("debug", ex.ToString()); - } + return new ReplayMetadata(fs, path); + } + catch (InvalidOperationException ex) + { + Log.Write("debug", ex.ToString()); + } + catch (NotSupportedException ex) + { + Log.Write("debug", ex.ToString()); } } } @@ -122,29 +120,5 @@ namespace OpenRA.FileFormats return null; } - - static int WriteUtf8String(BinaryWriter writer, string text) - { - byte[] bytes; - - if (!string.IsNullOrEmpty(text)) - bytes = Encoding.UTF8.GetBytes(text); - else - bytes = new byte[0]; - - writer.Write(bytes.Length); - writer.Write(bytes); - - return 4 + bytes.Length; - } - - static string ReadUtf8String(BinaryReader reader, int maxLength) - { - var length = reader.ReadInt32(); - if (length > maxLength) - throw new InvalidOperationException("The length of the string ({0}) is longer than the maximum allowed ({1}).".F(length, maxLength)); - - return Encoding.UTF8.GetString(reader.ReadBytes(length)); - } } } diff --git a/OpenRA.Game/StreamExts.cs b/OpenRA.Game/StreamExts.cs index e2bef625f0..2317476a3a 100755 --- a/OpenRA.Game/StreamExts.cs +++ b/OpenRA.Game/StreamExts.cs @@ -64,6 +64,11 @@ namespace OpenRA return BitConverter.ToInt32(s.ReadBytes(4), 0); } + public static void Write(this Stream s, int value) + { + s.Write(BitConverter.GetBytes(value)); + } + public static float ReadFloat(this Stream s) { return BitConverter.ToSingle(s.ReadBytes(4), 0); @@ -134,5 +139,32 @@ namespace OpenRA } } } + + // The string is assumed to be length-prefixed, as written by WriteString() + public static string ReadString(this Stream s, Encoding encoding, int maxLength) + { + var length = s.ReadInt32(); + if (length > maxLength) + throw new InvalidOperationException("The length of the string ({0}) is longer than the maximum allowed ({1}).".F(length, maxLength)); + + return encoding.GetString(s.ReadBytes(length)); + } + + // Writes a length-prefixed string using the specified encoding and returns + // the number of bytes written. + public static int WriteString(this Stream s, Encoding encoding, string text) + { + byte[] bytes; + + if (!string.IsNullOrEmpty(text)) + bytes = encoding.GetBytes(text); + else + bytes = new byte[0]; + + s.Write(bytes.Length); + s.Write(bytes); + + return 4 + bytes.Length; + } } } From b8bbd55598d3f9c7d8cfafd916584776fcc441fc Mon Sep 17 00:00:00 2001 From: Pavlos Touboulidis Date: Tue, 20 May 2014 17:45:33 +0300 Subject: [PATCH 11/11] Misc changes * Use Pair instead of KeyValuePair * double -> var * Butcher XML comments * Change WinState default to Undefined and use it instead of the new GameOutcome * Other changes --- OpenRA.Game/GameInformation.cs | 90 +++-------------- OpenRA.Game/Player.cs | 2 +- OpenRA.Game/Widgets/LabelWidget.cs | 2 +- OpenRA.Game/World.cs | 4 +- OpenRA.Mods.RA/MPStartLocations.cs | 2 +- .../Widgets/Logic/LobbyMapPreviewLogic.cs | 8 +- OpenRA.Mods.RA/Widgets/Logic/LobbyUtils.cs | 4 +- .../Widgets/Logic/ReplayBrowserLogic.cs | 96 ++++++++++--------- 8 files changed, 74 insertions(+), 134 deletions(-) diff --git a/OpenRA.Game/GameInformation.cs b/OpenRA.Game/GameInformation.cs index a4d191d599..9776fd688b 100644 --- a/OpenRA.Game/GameInformation.cs +++ b/OpenRA.Game/GameInformation.cs @@ -16,62 +16,30 @@ using OpenRA.Network; namespace OpenRA { - /// - /// Contains information about a finished game - /// public class GameInformation { - /// The map identifier. public string MapUid; - /// The map title. public string MapTitle; - /// Game start timestamp. public DateTime StartTimeUtc; - /// Game end timestamp (when the recoding stopped). + // Game end timestamp (when the recoding stopped). public DateTime EndTimeUtc; - /// - /// Gets the game's duration, from the time the game started until the - /// replay recording stopped. - /// - /// The game's duration. + // Gets the game's duration, from the time the game started until the + // replay recording stopped. public TimeSpan Duration { get { return EndTimeUtc > StartTimeUtc ? EndTimeUtc - StartTimeUtc : TimeSpan.Zero; } } - /// - /// Gets the list of players. - /// - /// The players. public IList Players { get; private set; } - /// - /// Gets the map preview, using and the . - /// - /// The map preview. public MapPreview MapPreview { get { return Game.modData.MapCache[MapUid]; } } - /// - /// Gets the human players. - /// - /// The human players. public IEnumerable HumanPlayers { get { return Players.Where(p => p.IsHuman); } } - /// - /// Gets a value indicating whether this instance has just one human player. - /// - /// true if this instance has just one human player; otherwise, false. public bool IsSinglePlayer { get { return HumanPlayers.Count() == 1; } } Dictionary playersByRuntime; - /// - /// Initializes a new instance of the class. - /// public GameInformation() { Players = new List(); playersByRuntime = new Dictionary(); } - /// - /// Deserialize the specified data into a new instance. - /// - /// Data. public static GameInformation Deserialize(string data) { try @@ -104,9 +72,6 @@ namespace OpenRA } } - /// - /// Serialize this instance. - /// public string Serialize() { var nodes = new List(); @@ -119,11 +84,7 @@ namespace OpenRA return nodes.WriteToString(); } - /// - /// Adds the start-up player information. - /// - /// Runtime player. - /// Lobby info. + // Adds the player information at start-up. public void AddPlayer(OpenRA.Player runtimePlayer, Session lobbyInfo) { if (runtimePlayer == null) @@ -160,11 +121,7 @@ namespace OpenRA Players.Add(player); } - /// - /// Gets the player information for the specified runtime player instance. - /// - /// The player, or null. - /// Runtime player. + // Gets the player information for the specified runtime player instance. public Player GetPlayer(OpenRA.Player runtimePlayer) { Player player; @@ -174,56 +131,37 @@ namespace OpenRA return player; } - /// Specifies whether the player was defeated, victorious, or there was no outcome defined. - public enum GameOutcome - { - /// Unknown outcome. - Undefined, - /// The player was defeated - Defeat, - /// The player was victorious - Victory - } - - /// - /// Information about a player - /// public class Player { // // Start-up information // - /// The client index. public int ClientIndex; - /// The player name, not guaranteed to be unique. + // The player name, not guaranteed to be unique. public string Name; - /// true if the player is a human player; otherwise, false. public bool IsHuman; - /// true if the player is a bot; otherwise, false. public bool IsBot; - /// The faction name (aka Country). + // The faction name (aka Country) public string FactionName; - /// The faction id (aka Country, aka Race). + // The faction id (aka Country, aka Race) public string FactionId; - /// The color used by the player in the game. public HSLColor Color; - /// The team id on start-up, or 0 if the player is not part of the team. + // The team id on start-up, or 0 if the player is not part of the team. public int Team; - /// The index of the spawn point on the map, or 0 if the player is not part of the team. public int SpawnPoint; - /// true if the faction was chosen at random; otherwise, false. + // True if the faction was chosen at random; otherwise, false public bool IsRandomFaction; - /// true if the spawn point was chosen at random; otherwise, false. + // True if the spawn point was chosen at random; otherwise, false. public bool IsRandomSpawnPoint; // // Information gathered at a later stage // - /// The game outcome for this player. - public GameOutcome Outcome; - /// The time when this player won or lost the game. + // The game outcome for this player + public WinState Outcome; + // The time when this player won or lost the game public DateTime OutcomeTimestampUtc; } } diff --git a/OpenRA.Game/Player.cs b/OpenRA.Game/Player.cs index ff5d5a8a2f..fcea4dc616 100644 --- a/OpenRA.Game/Player.cs +++ b/OpenRA.Game/Player.cs @@ -23,7 +23,7 @@ using OpenRA.Traits; namespace OpenRA { public enum PowerState { Normal, Low, Critical }; - public enum WinState { Won, Lost, Undefined }; + public enum WinState { Undefined, Won, Lost }; public class Player : IScriptBindable, IScriptNotifyBind, ILuaTableBinding, ILuaEqualityBinding, ILuaToStringBinding { diff --git a/OpenRA.Game/Widgets/LabelWidget.cs b/OpenRA.Game/Widgets/LabelWidget.cs index e5110b1176..42b913c166 100644 --- a/OpenRA.Game/Widgets/LabelWidget.cs +++ b/OpenRA.Game/Widgets/LabelWidget.cs @@ -58,7 +58,7 @@ namespace OpenRA.Widgets { SpriteFont font; if (!Game.Renderer.Fonts.TryGetValue(Font, out font)) - throw new ArgumentException("Request font '{0}' was not found.".F(Font)); + throw new ArgumentException("Requested font '{0}' was not found.".F(Font)); var text = GetText(); if (text == null) diff --git a/OpenRA.Game/World.cs b/OpenRA.Game/World.cs index df94effcd2..c7cf404a55 100644 --- a/OpenRA.Game/World.cs +++ b/OpenRA.Game/World.cs @@ -308,9 +308,7 @@ namespace OpenRA 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.Outcome = player.WinState; pi.OutcomeTimestampUtc = DateTime.UtcNow; } } diff --git a/OpenRA.Mods.RA/MPStartLocations.cs b/OpenRA.Mods.RA/MPStartLocations.cs index 0ae43f4d16..6569df50d9 100755 --- a/OpenRA.Mods.RA/MPStartLocations.cs +++ b/OpenRA.Mods.RA/MPStartLocations.cs @@ -42,7 +42,7 @@ namespace OpenRA.Mods.RA var client = world.LobbyInfo.ClientInSlot(kv.Key); var spid = (client == null || client.SpawnPoint == 0) ? ChooseSpawnPoint(world, available, taken) - : spawns[client.SpawnPoint-1]; + : spawns[client.SpawnPoint-1]; Start.Add(player, spid); diff --git a/OpenRA.Mods.RA/Widgets/Logic/LobbyMapPreviewLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/LobbyMapPreviewLogic.cs index 67a551b9a6..46e27c493c 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/LobbyMapPreviewLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/LobbyMapPreviewLogic.cs @@ -31,7 +31,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic var preview = available.Get("MAP_PREVIEW"); preview.Preview = () => lobby.Map; preview.OnMouseDown = mi => LobbyUtils.SelectSpawnPoint(orderManager, preview, lobby.Map, mi); - preview.SpawnOccupants = () => LobbyUtils.GetSpawnClients(orderManager.LobbyInfo, lobby.Map); + preview.SpawnOccupants = () => LobbyUtils.GetSpawnOccupants(orderManager.LobbyInfo, lobby.Map); var title = available.GetOrNull("MAP_TITLE"); if (title != null) @@ -54,7 +54,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic var preview = invalid.Get("MAP_PREVIEW"); preview.Preview = () => lobby.Map; preview.OnMouseDown = mi => LobbyUtils.SelectSpawnPoint(orderManager, preview, lobby.Map, mi); - preview.SpawnClients = () => LobbyUtils.GetSpawnClients(orderManager.LobbyInfo, lobby.Map); + preview.SpawnOccupants = () => LobbyUtils.GetSpawnOccupants(orderManager.LobbyInfo, lobby.Map); var title = invalid.GetOrNull("MAP_TITLE"); if (title != null) @@ -73,7 +73,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic var preview = download.Get("MAP_PREVIEW"); preview.Preview = () => lobby.Map; preview.OnMouseDown = mi => LobbyUtils.SelectSpawnPoint(orderManager, preview, lobby.Map, mi); - preview.SpawnOccupants = () => LobbyUtils.GetSpawnClients(orderManager.LobbyInfo, lobby.Map); + preview.SpawnOccupants = () => LobbyUtils.GetSpawnOccupants(orderManager.LobbyInfo, lobby.Map); var title = download.GetOrNull("MAP_TITLE"); if (title != null) @@ -100,7 +100,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic var preview = progress.Get("MAP_PREVIEW"); preview.Preview = () => lobby.Map; preview.OnMouseDown = mi => LobbyUtils.SelectSpawnPoint(orderManager, preview, lobby.Map, mi); - preview.SpawnOccupants = () => LobbyUtils.GetSpawnClients(orderManager.LobbyInfo, lobby.Map); + preview.SpawnOccupants = () => LobbyUtils.GetSpawnOccupants(orderManager.LobbyInfo, lobby.Map); var title = progress.GetOrNull("MAP_TITLE"); if (title != null) diff --git a/OpenRA.Mods.RA/Widgets/Logic/LobbyUtils.cs b/OpenRA.Mods.RA/Widgets/Logic/LobbyUtils.cs index ad7608ccfc..90663983e6 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/LobbyUtils.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/LobbyUtils.cs @@ -149,14 +149,14 @@ namespace OpenRA.Mods.RA.Widgets.Logic color.AttachPanel(colorChooser, onExit); } - public static Dictionary GetSpawnClients(Session lobbyInfo, MapPreview preview) + public static Dictionary GetSpawnOccupants(Session lobbyInfo, MapPreview preview) { var spawns = preview.SpawnPoints; return lobbyInfo.Clients .Where(c => c.SpawnPoint != 0) .ToDictionary(c => spawns[c.SpawnPoint - 1], c => new SpawnOccupant(c)); } - public static Dictionary GetSpawnClients(IEnumerable players, MapPreview preview) + public static Dictionary GetSpawnOccupants(IEnumerable players, MapPreview preview) { var spawns = preview.SpawnPoints; return players diff --git a/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs index cb8fa0cbd7..3eb6068382 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs @@ -104,25 +104,25 @@ namespace OpenRA.Mods.RA.Widgets.Logic if (ddb != null) { // Using list to maintain the order - var options = new List> + var options = new List> { - new KeyValuePair(GameType.Any, ddb.GetText()), - new KeyValuePair(GameType.Singleplayer, "Singleplayer"), - new KeyValuePair(GameType.Multiplayer, "Multiplayer") + Pair.New(GameType.Any, ddb.GetText()), + Pair.New(GameType.Singleplayer, "Singleplayer"), + Pair.New(GameType.Multiplayer, "Multiplayer") }; - var lookup = options.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + var lookup = options.ToDictionary(kvp => kvp.First, kvp => kvp.Second); ddb.GetText = () => lookup[filter.Type]; ddb.OnMouseDown = _ => { - Func, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) => + Func, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) => { var item = ScrollItemWidget.Setup( tpl, - () => filter.Type == option.Key, - () => { filter.Type = option.Key; ApplyFilter(); } + () => filter.Type == option.First, + () => { filter.Type = option.First; ApplyFilter(); } ); - item.Get("LABEL").GetText = () => option.Value; + item.Get("LABEL").GetText = () => option.Second; return item; }; @@ -139,27 +139,27 @@ namespace OpenRA.Mods.RA.Widgets.Logic if (ddb != null) { // Using list to maintain the order - var options = new List> + var options = new List> { - new KeyValuePair(DateType.Any, ddb.GetText()), - new KeyValuePair(DateType.Today, "Today"), - new KeyValuePair(DateType.LastWeek, "Last 7 days"), - new KeyValuePair(DateType.LastFortnight, "Last 14 days"), - new KeyValuePair(DateType.LastMonth, "Last 30 days") + Pair.New(DateType.Any, ddb.GetText()), + Pair.New(DateType.Today, "Today"), + Pair.New(DateType.LastWeek, "Last 7 days"), + Pair.New(DateType.LastFortnight, "Last 14 days"), + Pair.New(DateType.LastMonth, "Last 30 days") }; - var lookup = options.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + var lookup = options.ToDictionary(kvp => kvp.First, kvp => kvp.Second); ddb.GetText = () => lookup[filter.Date]; ddb.OnMouseDown = _ => { - Func, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) => + Func, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) => { var item = ScrollItemWidget.Setup( tpl, - () => filter.Date == option.Key, - () => { filter.Date = option.Key; ApplyFilter(); } + () => filter.Date == option.First, + () => { filter.Date = option.First; ApplyFilter(); } ); - item.Get("LABEL").GetText = () => option.Value; + item.Get("LABEL").GetText = () => option.Second; return item; }; @@ -176,27 +176,27 @@ namespace OpenRA.Mods.RA.Widgets.Logic if (ddb != null) { // Using list to maintain the order - var options = new List> + var options = new List> { - new KeyValuePair(DurationType.Any, ddb.GetText()), - new KeyValuePair(DurationType.VeryShort, "Under 5 min"), - new KeyValuePair(DurationType.Short, "Short (10 min)"), - new KeyValuePair(DurationType.Medium, "Medium (30 min)"), - new KeyValuePair(DurationType.Long, "Long (60+ min)") + Pair.New(DurationType.Any, ddb.GetText()), + Pair.New(DurationType.VeryShort, "Under 5 min"), + Pair.New(DurationType.Short, "Short (10 min)"), + Pair.New(DurationType.Medium, "Medium (30 min)"), + Pair.New(DurationType.Long, "Long (60+ min)") }; - var lookup = options.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + var lookup = options.ToDictionary(kvp => kvp.First, kvp => kvp.Second); ddb.GetText = () => lookup[filter.Duration]; ddb.OnMouseDown = _ => { - Func, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) => + Func, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) => { var item = ScrollItemWidget.Setup( tpl, - () => filter.Duration == option.Key, - () => { filter.Duration = option.Key; ApplyFilter(); } + () => filter.Duration == option.First, + () => { filter.Duration = option.First; ApplyFilter(); } ); - item.Get("LABEL").GetText = () => option.Value; + item.Get("LABEL").GetText = () => option.Second; return item; }; @@ -277,25 +277,25 @@ namespace OpenRA.Mods.RA.Widgets.Logic ddb.IsDisabled = () => string.IsNullOrEmpty(filter.PlayerName); // Using list to maintain the order - var options = new List> + var options = new List> { - new KeyValuePair(GameInformation.GameOutcome.Undefined, ddb.GetText()), - new KeyValuePair(GameInformation.GameOutcome.Defeat, "Defeat"), - new KeyValuePair(GameInformation.GameOutcome.Victory, "Victory") + Pair.New(WinState.Undefined, ddb.GetText()), + Pair.New(WinState.Lost, "Defeat"), + Pair.New(WinState.Won, "Victory") }; - var lookup = options.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + var lookup = options.ToDictionary(kvp => kvp.First, kvp => kvp.Second); ddb.GetText = () => lookup[filter.Outcome]; ddb.OnMouseDown = _ => { - Func, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) => + Func, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) => { var item = ScrollItemWidget.Setup( tpl, - () => filter.Outcome == option.Key, - () => { filter.Outcome = option.Key; ApplyFilter(); } + () => filter.Outcome == option.First, + () => { filter.Outcome = option.First; ApplyFilter(); } ); - item.Get("LABEL").GetText = () => option.Value; + item.Get("LABEL").GetText = () => option.Second; return item; }; @@ -409,7 +409,11 @@ namespace OpenRA.Mods.RA.Widgets.Logic button.IsDisabled = () => selectedReplay == null; button.OnClick = () => { - onDeleteReplay(selectedReplay, () => { if (selectedReplay == null) SelectFirstVisibleReplay(); }); + onDeleteReplay(selectedReplay, () => + { + if (selectedReplay == null) + SelectFirstVisibleReplay(); + }); }; } @@ -514,7 +518,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic // Duration if (filter.Duration != DurationType.Any) { - double minutes = replay.GameInfo.Duration.TotalMinutes; + var minutes = replay.GameInfo.Duration.TotalMinutes; switch (filter.Duration) { case DurationType.VeryShort: @@ -551,7 +555,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic return false; // Outcome - if (filter.Outcome != GameInformation.GameOutcome.Undefined && filter.Outcome != player.Outcome) + if (filter.Outcome != WinState.Undefined && filter.Outcome != player.Outcome) return false; // Faction @@ -582,7 +586,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic { selectedReplay = replay; selectedSpawns = (selectedReplay != null) - ? LobbyUtils.GetSpawnClients(selectedReplay.GameInfo.Players, selectedReplay.GameInfo.MapPreview) + ? LobbyUtils.GetSpawnOccupants(selectedReplay.GameInfo.Players, selectedReplay.GameInfo.MapPreview) : new Dictionary(); if (replay == null) @@ -680,7 +684,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic public GameType Type; public DateType Date; public DurationType Duration; - public GameInformation.GameOutcome Outcome; + public WinState Outcome; public string PlayerName; public string MapName; public string Faction; @@ -692,7 +696,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic return Type == default(GameType) && Date == default(DateType) && Duration == default(DurationType) - && Outcome == default(GameInformation.GameOutcome) + && Outcome == default(WinState) && string.IsNullOrEmpty(PlayerName) && string.IsNullOrEmpty(MapName) && string.IsNullOrEmpty(Faction);