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: