diff --git a/OpenRA.Game/FileFormats/ReplayMetadata.cs b/OpenRA.Game/FileFormats/ReplayMetadata.cs new file mode 100644 index 0000000000..1d97ef5dfe --- /dev/null +++ b/OpenRA.Game/FileFormats/ReplayMetadata.cs @@ -0,0 +1,124 @@ +#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.IO; +using System.Text; +using OpenRA.Network; + +namespace OpenRA.FileFormats +{ + public class ReplayMetadata + { + // Must be an invalid replay 'client' value + public const int MetaStartMarker = -1; + public const int MetaEndMarker = -2; + public const int MetaVersion = 0x00000001; + + public readonly GameInformation GameInfo; + public string FilePath { get; private set; } + + public ReplayMetadata(GameInformation info) + { + if (info == null) + throw new ArgumentNullException("info"); + + GameInfo = info; + } + + ReplayMetadata(FileStream fs, string path) + { + FilePath = path; + + // Read start marker + if (fs.ReadInt32() != MetaStartMarker) + throw new InvalidOperationException("Expected MetaStartMarker but found an invalid value."); + + // Read version + 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 = fs.ReadString(Encoding.UTF8, 1024 * 100); + GameInfo = GameInformation.Deserialize(data); + } + + public void Write(BinaryWriter writer) + { + // Write start marker & version + writer.Write(MetaStartMarker); + writer.Write(MetaVersion); + + // Write data + int dataLength = 0; + { + // Write lobby info data + writer.Flush(); + dataLength += writer.BaseStream.WriteString(Encoding.UTF8, GameInfo.Serialize()); + } + + // Write total length & end marker + writer.Write(dataLength); + writer.Write(MetaEndMarker); + } + + public void RenameFile(string newFilenameWithoutExtension) + { + var newPath = Path.Combine(Path.GetDirectoryName(FilePath), newFilenameWithoutExtension) + ".rep"; + File.Move(FilePath, newPath); + FilePath = newPath; + } + + 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; + + if (fs.Length < 20) + return null; + + try + { + fs.Seek(-(4 + 4), SeekOrigin.End); + var dataLength = fs.ReadInt32(); + if (fs.ReadInt32() == MetaEndMarker) + { + // 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(fs, 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; + } + } +} diff --git a/OpenRA.Game/GameInformation.cs b/OpenRA.Game/GameInformation.cs new file mode 100644 index 0000000000..9776fd688b --- /dev/null +++ b/OpenRA.Game/GameInformation.cs @@ -0,0 +1,168 @@ +#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 +{ + public class GameInformation + { + public string MapUid; + public string MapTitle; + 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. + public TimeSpan Duration { get { return EndTimeUtc > StartTimeUtc ? EndTimeUtc - StartTimeUtc : TimeSpan.Zero; } } + public IList Players { get; private set; } + public MapPreview MapPreview { get { return Game.modData.MapCache[MapUid]; } } + public IEnumerable HumanPlayers { get { return Players.Where(p => p.IsHuman); } } + public bool IsSinglePlayer { get { return HumanPlayers.Count() == 1; } } + + Dictionary playersByRuntime; + + public GameInformation() + { + Players = new List(); + playersByRuntime = new Dictionary(); + } + + 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("debug", "GameInformation deserialized invalid MiniYaml:\n{0}".F(data)); + throw; + } + } + + public string Serialize() + { + var nodes = new List(); + + nodes.Add(new MiniYamlNode("Root", FieldSaver.Save(this))); + + for (var i=0; i + public bool IsRandomSpawnPoint; + + // + // Information gathered at a later stage + // + + // 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/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 b78db96c47..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 @@ -44,6 +45,8 @@ namespace OpenRA.Network while (rs.Position < rs.Length) { var client = rs.ReadInt32(); + if (client == ReplayMetadata.MetaStartMarker) + break; var packetLen = rs.ReadInt32(); var packet = rs.ReadBytes(packetLen); var frame = BitConverter.ToInt32(packet, 0); diff --git a/OpenRA.Game/Network/ReplayRecorderConnection.cs b/OpenRA.Game/Network/ReplayRecorderConnection.cs index 3e6f6a8c31..d5eaf19c70 100644 --- a/OpenRA.Game/Network/ReplayRecorderConnection.cs +++ b/OpenRA.Game/Network/ReplayRecorderConnection.cs @@ -12,12 +12,15 @@ 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; + IConnection inner; BinaryWriter writer; Func chooseFilename; @@ -101,6 +104,13 @@ namespace OpenRA.Network if (disposed) return; + if (Metadata != null) + { + if (Metadata.GameInfo != null) + Metadata.GameInfo.EndTimeUtc = DateTime.UtcNow; + 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..566ccfa344 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -248,6 +248,7 @@ + @@ -330,6 +331,7 @@ + diff --git a/OpenRA.Game/Player.cs b/OpenRA.Game/Player.cs index e269f80de2..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 { @@ -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/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; + } } } 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..42b913c166 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("Requested font '{0}' was not found.".F(Font)); + var text = GetText(); if (text == null) return; 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/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/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.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/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 cd0f8f4d90..c7cf404a55 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) @@ -287,6 +302,16 @@ namespace OpenRA { return traitDict.ActorsWithTraitMultiple(this); } + + public void OnPlayerWinStateChanged(Player player) + { + var pi = gameInfo.GetPlayer(player); + if (pi != null) + { + pi.Outcome = player.WinState; + pi.OutcomeTimestampUtc = DateTime.UtcNow; + } + } } public struct TraitPair diff --git a/OpenRA.Mods.RA/ConquestVictoryConditions.cs b/OpenRA.Mods.RA/ConquestVictoryConditions.cs index dae465959f..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)); @@ -67,19 +68,23 @@ namespace OpenRA.Mods.RA a.Kill(a); if (self.Owner == self.World.LocalPlayer) + { 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) { 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) 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..6569df50d9 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/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/OpenRA.Mods.RA/Widgets/Logic/LobbyMapPreviewLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/LobbyMapPreviewLogic.cs index 42181fceeb..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.SpawnClients = () => 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.SpawnClients = () => 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.SpawnClients = () => 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 6a36afcdca..90663983e6 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 GetSpawnOccupants(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 GetSpawnOccupants(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 2f280ec317..3eb6068382 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, @@ -13,22 +13,25 @@ using System.Collections.Generic; using System.Drawing; 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 { - Widget panel; - ScrollPanelWidget playerList; - ScrollItemWidget playerTemplate, playerHeader; + static Filter filter = new Filter(); - MapPreview selectedMap = MapCache.UnknownMap; - Dictionary selectedSpawns; - string selectedFilename; - string selectedDuration; - bool selectedValid; + Widget panel; + ScrollPanelWidget replayList, playerList; + ScrollItemWidget playerTemplate, playerHeader; + List replays; + Dictionary replayState = new Dictionary(); + + Dictionary selectedSpawns; + ReplayMetadata selectedReplay; [ObjectCreator.UseCtor] public ReplayBrowserLogic(Widget widget, Action onExit, Action onStart) @@ -42,130 +45,685 @@ 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)) { - var files = Directory.GetFiles(dir, "*.rep").Reverse(); - foreach (var replayFile in files) - AddReplay(rl, replayFile, template); + using (new Support.PerfTimer("Load replays")) + { + replays = Directory + .GetFiles(dir, "*.rep") + .Select((filename) => ReplayMetadata.Read(filename)) + .Where((r) => r != null) + .OrderByDescending(r => r.GameInfo.StartTimeUtc) + .ToList(); + } - SelectReplay(files.FirstOrDefault()); + foreach (var replay in replays) + AddReplay(replay, template); + + ApplyFilter(); } var watch = panel.Get("WATCH_BUTTON"); - watch.IsDisabled = () => !selectedValid || selectedMap.Status != MapStatus.Available; + watch.IsDisabled = () => selectedReplay == null || selectedReplay.GameInfo.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.SpawnOccupants = () => selectedSpawns; + preview.Preview = () => selectedReplay != null ? selectedReplay.GameInfo.MapPreview : null; var title = panel.GetOrNull("MAP_TITLE"); if (title != null) - title.GetText = () => selectedMap.Title; + title.GetText = () => selectedReplay != null ? selectedReplay.GameInfo.MapPreview.Title : null; var type = panel.GetOrNull("MAP_TYPE"); if (type != null) - type.GetText = () => selectedMap.Type; + type.GetText = () => selectedReplay.GameInfo.MapPreview.Type; - panel.Get("DURATION").GetText = () => selectedDuration; + panel.Get("DURATION").GetText = () => WidgetUtils.FormatTimeSeconds((int)selectedReplay.GameInfo.Duration.TotalSeconds); + + SetupFilters(); + SetupManagement(); } - void SelectReplay(string filename) + void SetupFilters() { - if (filename == null) + // + // Game type + // + { + var ddb = panel.GetOrNull("FLT_GAMETYPE_DROPDOWNBUTTON"); + if (ddb != null) + { + // Using list to maintain the order + var options = new List> + { + Pair.New(GameType.Any, ddb.GetText()), + Pair.New(GameType.Singleplayer, "Singleplayer"), + Pair.New(GameType.Multiplayer, "Multiplayer") + }; + var lookup = options.ToDictionary(kvp => kvp.First, kvp => kvp.Second); + + ddb.GetText = () => lookup[filter.Type]; + ddb.OnMouseDown = _ => + { + Func, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) => + { + var item = ScrollItemWidget.Setup( + tpl, + () => filter.Type == option.First, + () => { filter.Type = option.First; ApplyFilter(); } + ); + item.Get("LABEL").GetText = () => option.Second; + 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> + { + 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.First, kvp => kvp.Second); + + ddb.GetText = () => lookup[filter.Date]; + ddb.OnMouseDown = _ => + { + Func, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) => + { + var item = ScrollItemWidget.Setup( + tpl, + () => filter.Date == option.First, + () => { filter.Date = option.First; ApplyFilter(); } + ); + item.Get("LABEL").GetText = () => option.Second; + 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> + { + 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.First, kvp => kvp.Second); + + ddb.GetText = () => lookup[filter.Duration]; + ddb.OnMouseDown = _ => + { + Func, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) => + { + var item = ScrollItemWidget.Setup( + tpl, + () => filter.Duration == option.First, + () => { filter.Duration = option.First; ApplyFilter(); } + ); + item.Get("LABEL").GetText = () => option.Second; + return item; + }; + + ddb.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", options.Count * 30, options, setupItem); + }; + } + } + + // + // 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> + { + Pair.New(WinState.Undefined, ddb.GetText()), + Pair.New(WinState.Lost, "Defeat"), + Pair.New(WinState.Won, "Victory") + }; + var lookup = options.ToDictionary(kvp => kvp.First, kvp => kvp.Second); + + ddb.GetText = () => lookup[filter.Outcome]; + ddb.OnMouseDown = _ => + { + Func, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) => + { + var item = ScrollItemWidget.Setup( + tpl, + () => filter.Outcome == option.First, + () => { filter.Outcome = option.First; ApplyFilter(); } + ); + item.Get("LABEL").GetText = () => option.Second; + return item; + }; + + ddb.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", options.Count * 30, options, setupItem); + }; + } + } + + // + // Faction (depends on Player) + // + { + var ddb = panel.GetOrNull("FLT_FACTION_DROPDOWNBUTTON"); + if (ddb != null) + { + 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 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.Faction, option, true) == 0, + () => { filter.Faction = option; ApplyFilter(); } + ); + item.Get("LABEL").GetText = () => option ?? anyText; + return item; + }; + + ddb.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", options.Count * 30, options, setupItem); + }; + } + } + + // + // 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.GameInfo.IsSinglePlayer) || (filter.Type == GameType.Singleplayer && !replay.GameInfo.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.LastFortnight: + t = TimeSpan.FromDays(14d); + break; + + case DateType.LastMonth: + default: + t = TimeSpan.FromDays(30d); + break; + } + if (replay.GameInfo.StartTimeUtc < DateTime.UtcNow - t) + return false; + } + + // Duration + if (filter.Duration != DurationType.Any) + { + var minutes = replay.GameInfo.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; + } + } + + // 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.GameInfo.Players.FirstOrDefault(p => string.Compare(filter.PlayerName, p.Name, true) == 0); + if (player == null) + return false; + + // Outcome + if (filter.Outcome != WinState.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; + } + + void ApplyFilter() + { + foreach (var replay in replays) + replayState[replay].Visible = EvaluateReplayVisibility(replay); + + if (selectedReplay == null || replayState[selectedReplay].Visible == false) + SelectFirstVisibleReplay(); + + replayList.Layout.AdjustChildren(); + } + + void SelectFirstVisibleReplay() + { + SelectReplay(replays.FirstOrDefault(r => replayState[r].Visible)); + } + + void SelectReplay(ReplayMetadata replay) + { + selectedReplay = replay; + selectedSpawns = (selectedReplay != null) + ? LobbyUtils.GetSpawnOccupants(selectedReplay.GameInfo.Players, selectedReplay.GameInfo.MapPreview) + : new Dictionary(); + + if (replay == null) return; try { - using (var conn = new ReplayConnection(filename)) + var players = replay.GameInfo.Players + .GroupBy(p => p.Team) + .OrderBy(g => g.Key); + + var teams = new Dictionary>(); + var noTeams = players.Count() == 1; + foreach (var p in players) { - 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" : p.Key == 0 ? "No Team" : "Team {0}".F(p.Key); + teams.Add(label, p); + } - 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.FactionId; - var flag = item.Get("FLAG"); - flag.GetImageCollection = () => "flags"; - flag.GetImageName = () => o.Country; - - playerList.AddChild(item); - } + playerList.AddChild(item); } } } 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(); } } - void AddReplay(ScrollPanelWidget list, string filename, ScrollItemWidget template) + void AddReplay(ReplayMetadata replay, ScrollItemWidget template) { var item = ScrollItemWidget.Setup(template, - () => selectedFilename == filename, - () => SelectReplay(filename), + () => selectedReplay == replay, + () => SelectReplay(replay), () => WatchReplay()); - var f = Path.GetFileName(filename); - item.Get("TITLE").GetText = () => f; - 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 + { + public GameType Type; + public DateType Date; + public DurationType Duration; + public WinState 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(WinState) + && string.IsNullOrEmpty(PlayerName) + && string.IsNullOrEmpty(MapName) + && string.IsNullOrEmpty(Faction); + } + } + } + enum GameType + { + Any, + Singleplayer, + Multiplayer + } + enum DateType + { + Any, + Today, + LastWeek, + LastFortnight, + LastMonth + } + enum DurationType + { + Any, + VeryShort, + Short, + Medium, + Long } } } 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/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/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/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/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 diff --git a/mods/ra/chrome/replaybrowser.yaml b/mods/ra/chrome/replaybrowser.yaml index 7ef5acbea5..f664e5313e 100644 --- a/mods/ra/chrome/replaybrowser.yaml +++ b/mods/ra/chrome/replaybrowser.yaml @@ -2,73 +2,237 @@ Background@REPLAYBROWSER_PANEL: Logic: ReplayBrowserLogic X: (WINDOW_RIGHT - WIDTH)/2 Y: (WINDOW_BOTTOM - HEIGHT)/2 - Width: 530 + Width: 780 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@FILTER_AND_MANAGE_CONTAINER: X: 20 - Y: 50 - Width: 282 - Height: 430 + Y: 20 + Width: 280 + Height: PARENT_BOTTOM - 75 Children: - ScrollItem@REPLAY_TEMPLATE: - Width: PARENT_RIGHT-27 - Height: 25 - X: 2 - 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-20 - Y: 50 - Width: 194 - Height: 194 - Background: dialog3 + 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 - 55 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 - 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: 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: 20 + 30+194 + 10 + Width: 194 + Height: PARENT_BOTTOM - 20 - 30-194 - 10 - 55 + 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 - 340 + Height: PARENT_BOTTOM - 50 IgnoreChildMouseOver: true Children: ScrollItem@HEADER: @@ -93,7 +257,7 @@ Background@REPLAYBROWSER_PANEL: Children: Image@FLAG: X: 4 - Y: 4 + Y: 6 Width: 32 Height: 16 Label@LABEL: 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