Improve replay metadata and the replay browser

List of changes:

* Better and more filters with new layout, for both mods.

* Rename/Delete/Detele all functionality.

* Simplified ReplayMetadata class considerably by introducing a new
GameInformation data object. The new GameInformation class contains
more information than previously available so the new solution is not
compatible with old replays, meaning it can't read old replays.

* Better and cleaner game information gathering in order to be written
at the end of the replay file.

* Revert changes to ReplayConnection, no longer necessary.

* Better exception message on missing sprites and fonts.

* New "SpawnOccupant" class that holds all the information needed by the
MapPreviewWidget to visualize a spawn point. It was using Session.Client
before and it was necessary to separate it to be able to show information
not available at lobby time.

* Fix keyboard focus UI bug when closing a window would not remove focus.
This commit is contained in:
Pavlos Touboulidis
2014-05-01 19:39:47 +03:00
parent 042910bd5e
commit de0a5ebd43
21 changed files with 1125 additions and 412 deletions

View File

@@ -22,67 +22,33 @@ namespace OpenRA.FileFormats
public const int MetaEndMarker = -2; public const int MetaEndMarker = -2;
public const int MetaVersion = 0x00000001; public const int MetaVersion = 0x00000001;
public readonly GameInformation GameInfo;
public string FilePath { get; private set; } public string FilePath { get; private set; }
public DateTime EndTimestampUtc { get; private set; }
public TimeSpan Duration { get { return EndTimestampUtc - StartTimestampUtc; } }
public WinState Outcome { get; private set; }
public readonly Lazy<Session> LobbyInfo; public ReplayMetadata(GameInformation info)
public readonly DateTime StartTimestampUtc;
readonly string lobbyInfoData;
ReplayMetadata()
{ {
Outcome = WinState.Undefined; if (info == null)
throw new ArgumentNullException("info");
GameInfo = info;
} }
public ReplayMetadata(DateTime startGameTimestampUtc, Session lobbyInfo) ReplayMetadata(BinaryReader reader, string path)
: this()
{ {
if (startGameTimestampUtc.Kind == DateTimeKind.Unspecified) FilePath = path;
throw new ArgumentException("The 'Kind' property of the timestamp must be specified", "startGameTimestamp");
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 // Read start marker
if (reader.ReadInt32() != MetaStartMarker) if (reader.ReadInt32() != MetaStartMarker)
throw new InvalidOperationException("Expected MetaStartMarker but found an invalid value."); throw new InvalidOperationException("Expected MetaStartMarker but found an invalid value.");
// Read version // Read version
var version = reader.ReadInt32(); var version = reader.ReadInt32();
if (version > MetaVersion) if (version != MetaVersion)
throw new NotSupportedException("Metadata version {0} is not supported".F(version)); throw new NotSupportedException("Metadata version {0} is not supported".F(version));
// Read start game timestamp // Read game info
StartTimestampUtc = new DateTime(reader.ReadInt64(), DateTimeKind.Utc); string data = ReadUtf8String(reader);
GameInfo = GameInformation.Deserialize(data);
// 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));
} }
public void Write(BinaryWriter writer) public void Write(BinaryWriter writer)
@@ -94,19 +60,8 @@ namespace OpenRA.FileFormats
// Write data // Write data
int dataLength = 0; 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 // Write lobby info data
dataLength += WriteUtf8String(writer, lobbyInfoData); dataLength += WriteUtf8String(writer, GameInfo.Serialize());
} }
// Write total length & end marker // Write total length & end marker
@@ -114,43 +69,27 @@ namespace OpenRA.FileFormats
writer.Write(MetaEndMarker); writer.Write(MetaEndMarker);
} }
public static ReplayMetadata Read(string path, bool enableFallbackMethod = true) public void RenameFile(string newFilenameWithoutExtension)
{ {
Func<DateTime> timestampProvider = () => var newPath = Path.Combine(Path.GetDirectoryName(FilePath), newFilenameWithoutExtension) + ".rep";
{ File.Move(FilePath, newPath);
try FilePath = newPath;
{
return File.GetCreationTimeUtc(path);
} }
catch
{
return DateTime.MinValue;
}
};
public static ReplayMetadata Read(string path)
{
using (var fs = new FileStream(path, FileMode.Open)) using (var fs = new FileStream(path, FileMode.Open))
{ return Read(fs, path);
var o = Read(fs, enableFallbackMethod, timestampProvider);
if (o != null)
o.FilePath = path;
return o;
}
} }
static ReplayMetadata Read(FileStream fs, bool enableFallbackMethod, Func<DateTime> fallbackTimestampProvider) static ReplayMetadata Read(FileStream fs, string path)
{ {
if (!fs.CanSeek)
return null;
fs.Seek(-(4 + 4), SeekOrigin.End);
using (var reader = new BinaryReader(fs)) using (var reader = new BinaryReader(fs))
{ {
// Disposing the BinaryReader will dispose the underlying stream
// and we don't want that because ReplayConnection may use the
// stream as well.
//
// Fixed in .NET 4.5.
// See: http://msdn.microsoft.com/en-us/library/gg712804%28v=vs.110%29.aspx
if (fs.CanSeek)
{
fs.Seek(-(4 + 4), SeekOrigin.End);
var dataLength = reader.ReadInt32(); var dataLength = reader.ReadInt32();
if (reader.ReadInt32() == MetaEndMarker) if (reader.ReadInt32() == MetaEndMarker)
{ {
@@ -158,7 +97,7 @@ namespace OpenRA.FileFormats
fs.Seek(-(4 + 4 + dataLength + 4 + 4), SeekOrigin.Current); fs.Seek(-(4 + 4 + dataLength + 4 + 4), SeekOrigin.Current);
try try
{ {
return new ReplayMetadata(reader); return new ReplayMetadata(reader, path);
} }
catch (InvalidOperationException ex) catch (InvalidOperationException ex)
{ {
@@ -169,23 +108,6 @@ namespace OpenRA.FileFormats
Log.Write("debug", ex.ToString()); Log.Write("debug", ex.ToString());
} }
} }
// Reset the stream position or the ReplayConnection will fail later
fs.Seek(0, SeekOrigin.Begin);
}
if (enableFallbackMethod)
{
using (var conn = new ReplayConnection(fs))
{
var replay = new ReplayMetadata(fallbackTimestampProvider(), conn.LobbyInfo);
if (conn.TickCount == 0)
return null;
var seconds = (int)Math.Ceiling((conn.TickCount * Game.NetTickScale) / 25f);
replay.EndTimestampUtc = replay.StartTimestampUtc.AddSeconds(seconds);
return replay;
}
}
} }
return null; return null;
@@ -210,10 +132,5 @@ namespace OpenRA.FileFormats
{ {
return Encoding.UTF8.GetString(reader.ReadBytes(reader.ReadInt32())); return Encoding.UTF8.GetString(reader.ReadBytes(reader.ReadInt32()));
} }
public MapPreview MapPreview
{
get { return Game.modData.MapCache[LobbyInfo.Value.GlobalSettings.Map]; }
}
} }
} }

View File

@@ -263,10 +263,6 @@ namespace OpenRA
using (new PerfTimer("LoadComplete")) using (new PerfTimer("LoadComplete"))
orderManager.world.LoadComplete(worldRenderer); 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) if (orderManager.GameStarted)
return; return;

View File

@@ -0,0 +1,230 @@
#region Copyright & License Information
/*
* Copyright 2007-2014 The OpenRA Developers (see AUTHORS)
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation. For more information,
* see COPYING.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Network;
namespace OpenRA
{
/// <summary>
/// Contains information about a finished game
/// </summary>
public class GameInformation
{
/// <summary>The map identifier.</summary>
public string MapUid;
/// <summary>The map title.</summary>
public string MapTitle;
/// <summary>Game start timestamp.</summary>
public DateTime StartTimeUtc;
/// <summary>Game end timestamp (when the recoding stopped).</summary>
public DateTime EndTimeUtc;
/// <summary>
/// Gets the game's duration, from the time the game started until the
/// replay recording stopped.
/// </summary>
/// <value>The game's duration.</value>
public TimeSpan Duration { get { return EndTimeUtc > StartTimeUtc ? EndTimeUtc - StartTimeUtc : TimeSpan.Zero; } }
/// <summary>
/// Gets the list of players.
/// </summary>
/// <value>The players.</value>
public IList<Player> Players { get; private set; }
/// <summary>
/// Gets the map preview, using <see cref="Game.modData.MapCache"/> and the <see cref="MapUid"/>.
/// </summary>
/// <value>The map preview.</value>
public MapPreview MapPreview { get { return Game.modData.MapCache[MapUid]; } }
/// <summary>
/// Gets the human players.
/// </summary>
/// <value>The human players.</value>
public IEnumerable<Player> HumanPlayers { get { return Players.Where(p => p.IsHuman); } }
/// <summary>
/// Gets a value indicating whether this instance has just one human player.
/// </summary>
/// <value><c>true</c> if this instance has just one human player; otherwise, <c>false</c>.</value>
public bool IsSinglePlayer { get { return HumanPlayers.Count() == 1; } }
Dictionary<OpenRA.Player, Player> playersByRuntime;
/// <summary>
/// Initializes a new instance of the class.
/// </summary>
public GameInformation()
{
Players = new List<Player>();
playersByRuntime = new Dictionary<OpenRA.Player, Player>();
}
/// <summary>
/// Deserialize the specified data into a new instance.
/// </summary>
/// <param name="data">Data.</param>
public static GameInformation Deserialize(string data)
{
try
{
var info = new GameInformation();
var nodes = MiniYaml.FromString(data);
foreach (var node in nodes)
{
var keyParts = node.Key.Split('@');
switch (keyParts[0])
{
case "Root":
FieldLoader.Load(info, node.Value);
break;
case "Player":
info.Players.Add(FieldLoader.Load<Player>(node.Value));
break;
}
}
return info;
}
catch (InvalidOperationException)
{
Log.Write("exception", "GameInformation deserialized invalid MiniYaml:\n{0}".F(data));
throw;
}
}
/// <summary>
/// Serialize this instance.
/// </summary>
public string Serialize()
{
var nodes = new List<MiniYamlNode>();
nodes.Add(new MiniYamlNode("Root", FieldSaver.Save(this)));
for (var i=0; i<Players.Count; i++)
nodes.Add(new MiniYamlNode("Player@{0}".F(i), FieldSaver.Save(Players[i])));
return nodes.WriteToString();
}
/// <summary>
/// Adds the start-up player information.
/// </summary>
/// <param name="runtimePlayer">Runtime player.</param>
/// <param name="lobbyInfo">Lobby info.</param>
public void AddPlayer(OpenRA.Player runtimePlayer, Session lobbyInfo)
{
if (runtimePlayer == null)
throw new ArgumentNullException("runtimePlayer");
if (lobbyInfo == null)
throw new ArgumentNullException("lobbyInfo");
// We don't care about spectators and map players
if (runtimePlayer.NonCombatant || !runtimePlayer.Playable)
return;
// Find the lobby client that created the runtime player
var client = lobbyInfo.ClientWithIndex(runtimePlayer.ClientIndex);
if (client == null)
return;
var player = new Player
{
ClientIndex = runtimePlayer.ClientIndex,
Name = runtimePlayer.PlayerName,
IsHuman = !runtimePlayer.IsBot,
IsBot = runtimePlayer.IsBot,
FactionName = runtimePlayer.Country.Name,
FactionId = runtimePlayer.Country.Race,
Color = runtimePlayer.Color,
Team = client.Team,
SpawnPoint = runtimePlayer.SpawnPoint,
IsRandomFaction = runtimePlayer.Country.Race != client.Country,
IsRandomSpawnPoint = runtimePlayer.SpawnPoint != client.SpawnPoint
};
playersByRuntime.Add(runtimePlayer, player);
Players.Add(player);
}
/// <summary>
/// Gets the player information for the specified runtime player instance.
/// </summary>
/// <returns>The player, or <c>null</c>.</returns>
/// <param name="runtimePlayer">Runtime player.</param>
public Player GetPlayer(OpenRA.Player runtimePlayer)
{
Player player;
playersByRuntime.TryGetValue(runtimePlayer, out player);
return player;
}
/// <summary>Specifies whether the player was defeated, victorious, or there was no outcome defined.</summary>
public enum GameOutcome
{
/// <summary>Unknown outcome.</summary>
Undefined,
/// <summary>The player was defeated</summary>
Defeat,
/// <summary>The player was victorious</summary>
Victory
}
///<summary>
/// Information about a player
/// </summary>
public class Player
{
//
// Start-up information
//
/// <summary>The client index.</summary>
public int ClientIndex;
/// <summary>The player name, not guaranteed to be unique.</summary>
public string Name;
/// <summary><c>true</c> if the player is a human player; otherwise, <c>false</c>.</summary>
public bool IsHuman;
/// <summary><c>true</c> if the player is a bot; otherwise, <c>false</c>.</summary>
public bool IsBot;
/// <summary>The faction name (aka Country).</summary>
public string FactionName;
/// <summary>The faction id (aka Country, aka Race).</summary>
public string FactionId;
/// <summary>The color used by the player in the game.</summary>
public HSLColor Color;
/// <summary>The team id on start-up, or 0 if the player is not part of the team.</summary>
public int Team;
/// <summary>The index of the spawn point on the map, or 0 if the player is not part of the team.</summary>
public int SpawnPoint;
/// <summary><c>true</c> if the faction was chosen at random; otherwise, <c>false</c>.</summary>
public bool IsRandomFaction;
/// <summary><c>true</c> if the spawn point was chosen at random; otherwise, <c>false</c>.</summary>
public bool IsRandomSpawnPoint;
//
// Information gathered at a later stage
//
/// <summary>The game outcome for this player.</summary>
public GameOutcome Outcome;
/// <summary>The time when this player won or lost the game.</summary>
public DateTime OutcomeTimestampUtc;
}
}
}

View File

@@ -72,14 +72,23 @@ namespace OpenRA.Graphics
collections.Add(name, collection); collections.Add(name, collection);
} }
public static Sprite GetImage(string collection, string image) public static Sprite GetImage(string collectionName, string imageName)
{ {
// Cached sprite // Cached sprite
if (cachedSprites.ContainsKey(collection) && cachedSprites[collection].ContainsKey(image)) Dictionary<string, Sprite> cachedCollection;
return cachedSprites[collection][image]; 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; MappedImage mi;
if (!collections[collection].regions.TryGetValue(image, out mi)) if (!collection.regions.TryGetValue(imageName, out mi))
return null; return null;
// Cached sheet // Cached sheet
@@ -93,11 +102,15 @@ namespace OpenRA.Graphics
} }
// Cache the sprite // Cache the sprite
if (!cachedSprites.ContainsKey(collection)) if (cachedCollection == null)
cachedSprites.Add(collection, new Dictionary<string, Sprite>()); {
cachedSprites[collection].Add(image, mi.GetImage(sheet)); cachedCollection = new Dictionary<string, Sprite>();
cachedSprites.Add(collectionName, cachedCollection);
}
var image = mi.GetImage(sheet);
cachedCollection.Add(imageName, image);
return cachedSprites[collection][image]; return image;
} }
} }
} }

View File

@@ -11,6 +11,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using OpenRA.FileFormats;
using OpenRA.Primitives; using OpenRA.Primitives;
namespace OpenRA.Network namespace OpenRA.Network
@@ -34,28 +35,17 @@ namespace OpenRA.Network
public readonly Session LobbyInfo; public readonly Session LobbyInfo;
public ReplayConnection(string replayFilename) 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 // 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. // to avoid issues with all immediate orders being resolved on the first tick.
using (var rs = File.OpenRead(replayFilename))
{
var chunk = new Chunk(); var chunk = new Chunk();
while (rs.Position < rs.Length) while (rs.Position < rs.Length)
{ {
var client = rs.ReadInt32(); var client = rs.ReadInt32();
if (client == FileFormats.ReplayMetadata.MetaStartMarker) if (client == ReplayMetadata.MetaStartMarker)
break; break;
var packetLen = rs.ReadInt32(); var packetLen = rs.ReadInt32();
var packet = rs.ReadBytes(packetLen); var packet = rs.ReadBytes(packetLen);
@@ -90,6 +80,9 @@ namespace OpenRA.Network
} }
} }
ordersFrame = LobbyInfo.GlobalSettings.OrderLatency;
}
// Do nothing: ignore locally generated orders // Do nothing: ignore locally generated orders
public void Send(int frame, List<byte[]> orders) { } public void Send(int frame, List<byte[]> orders) { }
public void SendImmediate(List<byte[]> orders) { } public void SendImmediate(List<byte[]> orders) { }

View File

@@ -20,7 +20,6 @@ namespace OpenRA.Network
class ReplayRecorderConnection : IConnection class ReplayRecorderConnection : IConnection
{ {
public ReplayMetadata Metadata; public ReplayMetadata Metadata;
public WinState LocalGameState = WinState.Undefined;
IConnection inner; IConnection inner;
BinaryWriter writer; BinaryWriter writer;
@@ -107,7 +106,8 @@ namespace OpenRA.Network
if (Metadata != null) if (Metadata != null)
{ {
Metadata.FinalizeReplayMetadata(DateTime.UtcNow, LocalGameState); if (Metadata.GameInfo != null)
Metadata.GameInfo.EndTimeUtc = DateTime.UtcNow;
Metadata.Write(writer); Metadata.Write(writer);
} }

View File

@@ -248,6 +248,7 @@
<Compile Include="GameRules\Ruleset.cs" /> <Compile Include="GameRules\Ruleset.cs" />
<Compile Include="GameRules\RulesetCache.cs" /> <Compile Include="GameRules\RulesetCache.cs" />
<Compile Include="Support\MersenneTwister.cs" /> <Compile Include="Support\MersenneTwister.cs" />
<Compile Include="GameInformation.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="FileSystem\D2kSoundResources.cs" /> <Compile Include="FileSystem\D2kSoundResources.cs" />

View File

@@ -41,6 +41,7 @@ namespace OpenRA
public readonly int ClientIndex; public readonly int ClientIndex;
public readonly PlayerReference PlayerReference; public readonly PlayerReference PlayerReference;
public bool IsBot; public bool IsBot;
public int SpawnPoint;
public Shroud Shroud; public Shroud Shroud;
public World World { get; private set; } public World World { get; private set; }

View File

@@ -41,9 +41,12 @@ namespace OpenRA.Widgets
{ {
var name = GetImageName(); var name = GetImageName();
var collection = GetImageCollection(); var collection = GetImageCollection();
WidgetUtils.DrawRGBA(
ChromeProvider.GetImage(collection, name), var sprite = ChromeProvider.GetImage(collection, name);
RenderOrigin); if (sprite == null)
throw new ArgumentException("Sprite {0}/{1} was not found.".F(collection, name));
WidgetUtils.DrawRGBA(sprite, RenderOrigin);
} }
} }
} }

View File

@@ -56,7 +56,10 @@ namespace OpenRA.Widgets
public override void Draw() 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(); var text = GetText();
if (text == null) if (text == null)
return; return;

View File

@@ -19,10 +19,42 @@ using OpenRA.Network;
namespace OpenRA.Widgets 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 class MapPreviewWidget : Widget
{ {
public Func<MapPreview> Preview = () => null; public Func<MapPreview> Preview = () => null;
public Func<Dictionary<CPos, Session.Client>> SpawnClients = () => new Dictionary<CPos, Session.Client>(); public Func<Dictionary<CPos, SpawnOccupant>> SpawnOccupants = () => new Dictionary<CPos, SpawnOccupant>();
public Action<MouseInput> OnMouseDown = _ => {}; public Action<MouseInput> OnMouseDown = _ => {};
public bool IgnoreMouseInput = false; public bool IgnoreMouseInput = false;
public bool ShowSpawnPoints = true; public bool ShowSpawnPoints = true;
@@ -44,7 +76,7 @@ namespace OpenRA.Widgets
: base(other) : base(other)
{ {
Preview = other.Preview; Preview = other.Preview;
SpawnClients = other.SpawnClients; SpawnOccupants = other.SpawnOccupants;
ShowSpawnPoints = other.ShowSpawnPoints; ShowSpawnPoints = other.ShowSpawnPoints;
TooltipTemplate = other.TooltipTemplate; TooltipTemplate = other.TooltipTemplate;
TooltipContainer = other.TooltipContainer; TooltipContainer = other.TooltipContainer;
@@ -109,7 +141,7 @@ namespace OpenRA.Widgets
TooltipSpawnIndex = -1; TooltipSpawnIndex = -1;
if (ShowSpawnPoints) 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; var spawnPoints = preview.SpawnPoints;
foreach (var p in spawnPoints) foreach (var p in spawnPoints)

View File

@@ -258,7 +258,7 @@ namespace OpenRA.Widgets
return true; 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) public virtual bool YieldMouseFocus(MouseInput mi)
{ {
if (Ui.MouseFocusWidget == this) if (Ui.MouseFocusWidget == this)
@@ -267,6 +267,12 @@ namespace OpenRA.Widgets
return true; return true;
} }
void ForceYieldMouseFocus()
{
if (Ui.MouseFocusWidget == this && !YieldMouseFocus(default(MouseInput)))
Ui.MouseFocusWidget = null;
}
public virtual bool TakeKeyboardFocus() public virtual bool TakeKeyboardFocus()
{ {
if (HasKeyboardFocus) if (HasKeyboardFocus)
@@ -287,6 +293,12 @@ namespace OpenRA.Widgets
return true; return true;
} }
void ForceYieldKeyboardFocus()
{
if (Ui.KeyboardFocusWidget == this && !YieldKeyboardFocus())
Ui.KeyboardFocusWidget = null;
}
public virtual string GetCursor(int2 pos) { return "default"; } public virtual string GetCursor(int2 pos) { return "default"; }
public string GetCursorOuter(int2 pos) public string GetCursorOuter(int2 pos)
{ {
@@ -410,6 +422,11 @@ namespace OpenRA.Widgets
public virtual void Removed() public virtual void Removed()
{ {
// Using the forced versions because the widgets
// have been removed
ForceYieldKeyboardFocus();
ForceYieldMouseFocus();
foreach (var c in Children.OfType<Widget>().Reverse()) foreach (var c in Children.OfType<Widget>().Reverse())
c.Removed(); c.Removed();
} }

View File

@@ -85,6 +85,7 @@ namespace OpenRA
public readonly TileSet TileSet; public readonly TileSet TileSet;
public readonly ActorMap ActorMap; public readonly ActorMap ActorMap;
public readonly ScreenMap ScreenMap; public readonly ScreenMap ScreenMap;
readonly GameInformation gameInfo;
public void IssueOrder(Order o) { orderManager.IssueOrder(o); } /* avoid exposing the OM to mod code */ 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; p.Stances[q] = Stance.Neutral;
Sound.SoundVolumeModifier = 1.0f; Sound.SoundVolumeModifier = 1.0f;
gameInfo = new GameInformation
{
MapUid = Map.Uid,
MapTitle = Map.Title
};
} }
public void LoadComplete(WorldRenderer wr) public void LoadComplete(WorldRenderer wr)
@@ -151,6 +158,14 @@ namespace OpenRA
using (new Support.PerfTimer(wlh.GetType().Name + ".WorldLoaded")) using (new Support.PerfTimer(wlh.GetType().Name + ".WorldLoaded"))
wlh.WorldLoaded(this, wr); 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) public Actor CreateActor(string name, TypeDictionary initDict)
@@ -288,11 +303,16 @@ namespace OpenRA
return traitDict.ActorsWithTraitMultiple<T>(this); return traitDict.ActorsWithTraitMultiple<T>(this);
} }
public void OnLocalPlayerWinStateChanged() public void OnPlayerWinStateChanged(Player player)
{ {
var rc = orderManager.Connection as ReplayRecorderConnection; var pi = gameInfo.GetPlayer(player);
if (rc != null) if (pi != null)
rc.LocalGameState = LocalPlayer.WinState; {
pi.Outcome = player.WinState == WinState.Lost ? GameInformation.GameOutcome.Defeat
: player.WinState == WinState.Won ? GameInformation.GameOutcome.Victory
: GameInformation.GameOutcome.Undefined;
pi.OutcomeTimestampUtc = DateTime.UtcNow;
}
} }
} }

View File

@@ -60,6 +60,7 @@ namespace OpenRA.Mods.RA
{ {
if (self.Owner.WinState == WinState.Lost) return; if (self.Owner.WinState == WinState.Lost) return;
self.Owner.WinState = WinState.Lost; self.Owner.WinState = WinState.Lost;
self.World.OnPlayerWinStateChanged(self.Owner);
Game.Debug("{0} is defeated.".F(self.Owner.PlayerName)); Game.Debug("{0} is defeated.".F(self.Owner.PlayerName));
@@ -68,8 +69,6 @@ namespace OpenRA.Mods.RA
if (self.Owner == self.World.LocalPlayer) if (self.Owner == self.World.LocalPlayer)
{ {
self.World.OnLocalPlayerWinStateChanged();
Game.RunAfterDelay(Info.NotificationDelay, () => Game.RunAfterDelay(Info.NotificationDelay, () =>
{ {
if (Game.IsCurrentWorld(self.World)) if (Game.IsCurrentWorld(self.World))
@@ -82,16 +81,14 @@ namespace OpenRA.Mods.RA
{ {
if (self.Owner.WinState == WinState.Won) return; if (self.Owner.WinState == WinState.Won) return;
self.Owner.WinState = WinState.Won; self.Owner.WinState = WinState.Won;
self.World.OnPlayerWinStateChanged(self.Owner);
Game.Debug("{0} is victorious.".F(self.Owner.PlayerName)); 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)); Game.RunAfterDelay(Info.NotificationDelay, () => Sound.PlayNotification(self.World.Map.Rules, self.Owner, "Speech", "Win", self.Owner.Country.Race));
} }
} }
}
[Desc("Tag trait for things that must be destroyed for a short game to end.")] [Desc("Tag trait for things that must be destroyed for a short game to end.")]
public class MustBeDestroyedInfo : TraitInfo<MustBeDestroyed> { } public class MustBeDestroyedInfo : TraitInfo<MustBeDestroyed> { }

View File

@@ -28,7 +28,7 @@ namespace OpenRA.Mods.RA
public void WorldLoaded(World world, WorldRenderer wr) 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) var taken = world.LobbyInfo.Clients.Where(c => c.SpawnPoint != 0 && c.Slot != null)
.Select(c => spawns[c.SpawnPoint-1]).ToList(); .Select(c => spawns[c.SpawnPoint-1]).ToList();
var available = spawns.Except(taken).ToList(); var available = spawns.Except(taken).ToList();
@@ -42,9 +42,13 @@ namespace OpenRA.Mods.RA
var client = world.LobbyInfo.ClientInSlot(kv.Key); var client = world.LobbyInfo.ClientInSlot(kv.Key);
var spid = (client == null || client.SpawnPoint == 0) var spid = (client == null || client.SpawnPoint == 0)
? ChooseSpawnPoint(world, available, taken) ? ChooseSpawnPoint(world, available, taken)
: world.Map.GetSpawnPoints()[client.SpawnPoint-1]; : spawns[client.SpawnPoint-1];
Start.Add(player, spid); Start.Add(player, spid);
player.SpawnPoint = (client == null || client.SpawnPoint == 0)
? spawns.IndexOf(spid) + 1
: client.SpawnPoint;
} }
// Explore allied shroud // Explore allied shroud

View File

@@ -31,7 +31,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic
var preview = available.Get<MapPreviewWidget>("MAP_PREVIEW"); var preview = available.Get<MapPreviewWidget>("MAP_PREVIEW");
preview.Preview = () => lobby.Map; preview.Preview = () => lobby.Map;
preview.OnMouseDown = mi => LobbyUtils.SelectSpawnPoint(orderManager, preview, lobby.Map, mi); 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<LabelWidget>("MAP_TITLE"); var title = available.GetOrNull<LabelWidget>("MAP_TITLE");
if (title != null) if (title != null)
@@ -73,7 +73,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic
var preview = download.Get<MapPreviewWidget>("MAP_PREVIEW"); var preview = download.Get<MapPreviewWidget>("MAP_PREVIEW");
preview.Preview = () => lobby.Map; preview.Preview = () => lobby.Map;
preview.OnMouseDown = mi => LobbyUtils.SelectSpawnPoint(orderManager, preview, lobby.Map, mi); 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<LabelWidget>("MAP_TITLE"); var title = download.GetOrNull<LabelWidget>("MAP_TITLE");
if (title != null) if (title != null)
@@ -100,7 +100,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic
var preview = progress.Get<MapPreviewWidget>("MAP_PREVIEW"); var preview = progress.Get<MapPreviewWidget>("MAP_PREVIEW");
preview.Preview = () => lobby.Map; preview.Preview = () => lobby.Map;
preview.OnMouseDown = mi => LobbyUtils.SelectSpawnPoint(orderManager, preview, lobby.Map, mi); 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<LabelWidget>("MAP_TITLE"); var title = progress.GetOrNull<LabelWidget>("MAP_TITLE");
if (title != null) if (title != null)

View File

@@ -149,12 +149,19 @@ namespace OpenRA.Mods.RA.Widgets.Logic
color.AttachPanel(colorChooser, onExit); color.AttachPanel(colorChooser, onExit);
} }
public static Dictionary<CPos, Session.Client> GetSpawnClients(Session lobbyInfo, MapPreview preview) public static Dictionary<CPos, SpawnOccupant> GetSpawnClients(Session lobbyInfo, MapPreview preview)
{ {
var spawns = preview.SpawnPoints; var spawns = preview.SpawnPoints;
return lobbyInfo.Clients return lobbyInfo.Clients
.Where(c => c.SpawnPoint != 0) .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<CPos, SpawnOccupant> GetSpawnClients(IEnumerable<GameInformation.Player> 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) public static void SelectSpawnPoint(OrderManager orderManager, MapPreviewWidget mapPreview, MapPreview preview, MouseInput mi)

View File

@@ -1,6 +1,6 @@
#region Copyright & License Information #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 * 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 * available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation. For more information, * 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(); static Filter filter = new Filter();
Widget panel; Widget panel;
ScrollPanelWidget playerList; ScrollPanelWidget replayList, playerList;
ScrollItemWidget playerTemplate, playerHeader; ScrollItemWidget playerTemplate, playerHeader;
List<ReplayMetadata> replays; List<ReplayMetadata> replays;
Dictionary<ReplayMetadata, bool> replayVis = new Dictionary<ReplayMetadata, bool>(); Dictionary<ReplayMetadata, ReplayState> replayState = new Dictionary<ReplayMetadata, ReplayState>();
Dictionary<CPos, Session.Client> selectedSpawns; Dictionary<CPos, SpawnOccupant> selectedSpawns;
ReplayMetadata selectedReplay; ReplayMetadata selectedReplay;
[ObjectCreator.UseCtor] [ObjectCreator.UseCtor]
@@ -45,13 +45,13 @@ namespace OpenRA.Mods.RA.Widgets.Logic
panel.Get<ButtonWidget>("CANCEL_BUTTON").OnClick = () => { Ui.CloseWindow(); onExit(); }; panel.Get<ButtonWidget>("CANCEL_BUTTON").OnClick = () => { Ui.CloseWindow(); onExit(); };
var rl = panel.Get<ScrollPanelWidget>("REPLAY_LIST"); replayList = panel.Get<ScrollPanelWidget>("REPLAY_LIST");
var template = panel.Get<ScrollItemWidget>("REPLAY_TEMPLATE"); var template = panel.Get<ScrollItemWidget>("REPLAY_TEMPLATE");
var mod = Game.modData.Manifest.Mod; var mod = Game.modData.Manifest.Mod;
var dir = new[] { Platform.SupportDir, "Replays", mod.Id, mod.Version }.Aggregate(Path.Combine); var dir = new[] { Platform.SupportDir, "Replays", mod.Id, mod.Version }.Aggregate(Path.Combine);
rl.RemoveChildren(); replayList.RemoveChildren();
if (Directory.Exists(dir)) if (Directory.Exists(dir))
{ {
using (new Support.PerfTimer("Load replays")) using (new Support.PerfTimer("Load replays"))
@@ -60,37 +60,38 @@ namespace OpenRA.Mods.RA.Widgets.Logic
.GetFiles(dir, "*.rep") .GetFiles(dir, "*.rep")
.Select((filename) => ReplayMetadata.Read(filename)) .Select((filename) => ReplayMetadata.Read(filename))
.Where((r) => r != null) .Where((r) => r != null)
.OrderByDescending(r => r.StartTimestampUtc) .OrderByDescending(r => r.GameInfo.StartTimeUtc)
.ToList(); .ToList();
} }
foreach (var replay in replays) foreach (var replay in replays)
AddReplay(rl, replay, template); AddReplay(replay, template);
ApplyFilter(); ApplyFilter();
} }
var watch = panel.Get<ButtonWidget>("WATCH_BUTTON"); var watch = panel.Get<ButtonWidget>("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(); }; watch.OnClick = () => { WatchReplay(); onStart(); };
panel.Get("REPLAY_INFO").IsVisible = () => selectedReplay != null; panel.Get("REPLAY_INFO").IsVisible = () => selectedReplay != null;
var preview = panel.Get<MapPreviewWidget>("MAP_PREVIEW"); var preview = panel.Get<MapPreviewWidget>("MAP_PREVIEW");
preview.SpawnClients = () => selectedSpawns; preview.SpawnOccupants = () => selectedSpawns;
preview.Preview = () => selectedReplay != null ? selectedReplay.MapPreview : null; preview.Preview = () => selectedReplay != null ? selectedReplay.GameInfo.MapPreview : null;
var title = panel.GetOrNull<LabelWidget>("MAP_TITLE"); var title = panel.GetOrNull<LabelWidget>("MAP_TITLE");
if (title != null) if (title != null)
title.GetText = () => selectedReplay != null ? selectedReplay.MapPreview.Title : null; title.GetText = () => selectedReplay != null ? selectedReplay.GameInfo.MapPreview.Title : null;
var type = panel.GetOrNull<LabelWidget>("MAP_TYPE"); var type = panel.GetOrNull<LabelWidget>("MAP_TYPE");
if (type != null) if (type != null)
type.GetText = () => selectedReplay.MapPreview.Type; type.GetText = () => selectedReplay.GameInfo.MapPreview.Type;
panel.Get<LabelWidget>("DURATION").GetText = () => WidgetUtils.FormatTimeSeconds((int)selectedReplay.Duration.TotalSeconds); panel.Get<LabelWidget>("DURATION").GetText = () => WidgetUtils.FormatTimeSeconds((int)selectedReplay.GameInfo.Duration.TotalSeconds);
SetupFilters(); SetupFilters();
SetupManagement();
} }
void SetupFilters() void SetupFilters()
@@ -204,25 +205,89 @@ namespace OpenRA.Mods.RA.Widgets.Logic
} }
// //
// Outcome // Map
//
{
var ddb = panel.GetOrNull<DropDownButtonWidget>("FLT_MAPNAME_DROPDOWNBUTTON");
if (ddb != null)
{
var options = new HashSet<string>(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<string, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) =>
{
var item = ScrollItemWidget.Setup(
tpl,
() => string.Compare(filter.MapName, option, true) == 0,
() => { filter.MapName = option; ApplyFilter(); }
);
item.Get<LabelWidget>("LABEL").GetText = () => option ?? anyText;
return item;
};
ddb.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", options.Count * 30, options, setupItem);
};
}
}
//
// Players
//
{
var ddb = panel.GetOrNull<DropDownButtonWidget>("FLT_PLAYER_DROPDOWNBUTTON");
if (ddb != null)
{
var options = new HashSet<string>(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<string, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) =>
{
var item = ScrollItemWidget.Setup(
tpl,
() => string.Compare(filter.PlayerName, option, true) == 0,
() => { filter.PlayerName = option; ApplyFilter(); }
);
item.Get<LabelWidget>("LABEL").GetText = () => option ?? anyText;
return item;
};
ddb.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", options.Count * 30, options, setupItem);
};
}
}
//
// Outcome (depends on Player)
// //
{ {
var ddb = panel.GetOrNull<DropDownButtonWidget>("FLT_OUTCOME_DROPDOWNBUTTON"); var ddb = panel.GetOrNull<DropDownButtonWidget>("FLT_OUTCOME_DROPDOWNBUTTON");
if (ddb != null) if (ddb != null)
{ {
ddb.IsDisabled = () => string.IsNullOrEmpty(filter.PlayerName);
// Using list to maintain the order // Using list to maintain the order
var options = new List<KeyValuePair<WinState, string>> var options = new List<KeyValuePair<GameInformation.GameOutcome, string>>
{ {
new KeyValuePair<WinState, string>(WinState.Undefined, ddb.GetText()), new KeyValuePair<GameInformation.GameOutcome, string>(GameInformation.GameOutcome.Undefined, ddb.GetText()),
new KeyValuePair<WinState, string>(WinState.Won, "Won"), new KeyValuePair<GameInformation.GameOutcome, string>(GameInformation.GameOutcome.Defeat, "Defeat"),
new KeyValuePair<WinState, string>(WinState.Lost, "Lost") new KeyValuePair<GameInformation.GameOutcome, string>(GameInformation.GameOutcome.Victory, "Victory")
}; };
var lookup = options.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); var lookup = options.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
ddb.GetText = () => lookup[filter.Outcome]; ddb.GetText = () => lookup[filter.Outcome];
ddb.OnMouseDown = _ => ddb.OnMouseDown = _ =>
{ {
Func<KeyValuePair<WinState, string>, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) => Func<KeyValuePair<GameInformation.GameOutcome, string>, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) =>
{ {
var item = ScrollItemWidget.Setup( var item = ScrollItemWidget.Setup(
tpl, tpl,
@@ -239,28 +304,30 @@ namespace OpenRA.Mods.RA.Widgets.Logic
} }
// //
// Players // Faction (depends on Player)
// //
{ {
var ddb = panel.GetOrNull<DropDownButtonWidget>("FLT_PLAYER_DROPDOWNBUTTON"); var ddb = panel.GetOrNull<DropDownButtonWidget>("FLT_FACTION_DROPDOWNBUTTON");
if (ddb != null) if (ddb != null)
{ {
var options = new HashSet<string>(replays.SelectMany(r => r.LobbyInfo.Value.Clients.Select(c => c.Name)), StringComparer.OrdinalIgnoreCase).ToList(); ddb.IsDisabled = () => string.IsNullOrEmpty(filter.PlayerName);
var options = new HashSet<string>(replays.SelectMany(r => r.GameInfo.Players.Select(p => p.FactionName).Where(n => !string.IsNullOrEmpty(n))), StringComparer.OrdinalIgnoreCase).ToList();
options.Sort(StringComparer.OrdinalIgnoreCase); options.Sort(StringComparer.OrdinalIgnoreCase);
options.Insert(0, null); // no filter options.Insert(0, null); // no filter
var nobodyText = ddb.GetText(); var anyText = ddb.GetText();
ddb.GetText = () => string.IsNullOrEmpty(filter.PlayerName) ? nobodyText : filter.PlayerName; ddb.GetText = () => string.IsNullOrEmpty(filter.Faction) ? anyText : filter.Faction;
ddb.OnMouseDown = _ => ddb.OnMouseDown = _ =>
{ {
Func<string, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) => Func<string, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) =>
{ {
var item = ScrollItemWidget.Setup( var item = ScrollItemWidget.Setup(
tpl, tpl,
() => string.Compare(filter.PlayerName, option, true) == 0, () => string.Compare(filter.Faction, option, true) == 0,
() => { filter.PlayerName = option; ApplyFilter(); } () => { filter.Faction = option; ApplyFilter(); }
); );
item.Get<LabelWidget>("LABEL").GetText = () => option ?? nobodyText; item.Get<LabelWidget>("LABEL").GetText = () => option ?? anyText;
return item; return item;
}; };
@@ -268,12 +335,152 @@ namespace OpenRA.Mods.RA.Widgets.Logic
}; };
} }
} }
//
// Reset button
//
{
var button = panel.Get<ButtonWidget>("FLT_RESET_BUTTON");
button.IsDisabled = () => filter.IsEmpty;
button.OnClick = () => { filter = new Filter(); ApplyFilter(); };
}
}
void SetupManagement()
{
{
var button = panel.Get<ButtonWidget>("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<ReplayMetadata, 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<ButtonWidget>("MNG_DELSEL_BUTTON");
button.IsDisabled = () => selectedReplay == null;
button.OnClick = () =>
{
onDeleteReplay(selectedReplay, () => { if (selectedReplay == null) SelectFirstVisibleReplay(); });
};
}
{
var button = panel.Get<ButtonWidget>("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) bool EvaluateReplayVisibility(ReplayMetadata replay)
{ {
// Game type // 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; return false;
// Date type // Date type
@@ -295,14 +502,14 @@ namespace OpenRA.Mods.RA.Widgets.Logic
t = TimeSpan.FromDays(30d); t = TimeSpan.FromDays(30d);
break; break;
} }
if (replay.StartTimestampUtc < DateTime.UtcNow.Subtract(t)) if (replay.GameInfo.StartTimeUtc < DateTime.UtcNow - t)
return false; return false;
} }
// Duration // Duration
if (filter.Duration != DurationType.Any) if (filter.Duration != DurationType.Any)
{ {
double minutes = replay.Duration.TotalMinutes; double minutes = replay.GameInfo.Duration.TotalMinutes;
switch (filter.Duration) switch (filter.Duration)
{ {
case DurationType.VeryShort: case DurationType.VeryShort:
@@ -327,16 +534,24 @@ namespace OpenRA.Mods.RA.Widgets.Logic
} }
} }
// Outcome // Map
if (filter.Outcome != WinState.Undefined && filter.Outcome != replay.Outcome) if (!string.IsNullOrEmpty(filter.MapName) && string.Compare(filter.MapName, replay.GameInfo.MapTitle, true) != 0)
return false; return false;
// Player // Player
if (!string.IsNullOrEmpty(filter.PlayerName)) 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) if (player == null)
return false; 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; return true;
@@ -345,41 +560,41 @@ namespace OpenRA.Mods.RA.Widgets.Logic
void ApplyFilter() void ApplyFilter()
{ {
foreach (var replay in replays) 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(); SelectFirstVisibleReplay();
panel.Get<ScrollPanelWidget>("REPLAY_LIST").Layout.AdjustChildren(); replayList.Layout.AdjustChildren();
} }
void SelectFirstVisibleReplay() void SelectFirstVisibleReplay()
{ {
SelectReplay(replays.FirstOrDefault(r => replayVis[r])); SelectReplay(replays.FirstOrDefault(r => replayState[r].Visible));
} }
void SelectReplay(ReplayMetadata replay) void SelectReplay(ReplayMetadata replay)
{ {
selectedReplay = 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<CPos, SpawnOccupant>();
if (replay == null) if (replay == null)
return; return;
try try
{ {
var lobby = replay.LobbyInfo.Value; var players = replay.GameInfo.Players
.GroupBy(p => p.Team)
var clients = lobby.Clients.Where(c => c.Slot != null)
.GroupBy(c => c.Team)
.OrderBy(g => g.Key); .OrderBy(g => g.Key);
var teams = new Dictionary<string, IEnumerable<Session.Client>>(); var teams = new Dictionary<string, IEnumerable<GameInformation.Player>>();
var noTeams = clients.Count() == 1; var noTeams = players.Count() == 1;
foreach (var c in clients) foreach (var p in players)
{ {
var label = noTeams ? "Players" : c.Key == 0 ? "No Team" : "Team {0}".F(c.Key); var label = noTeams ? "Players" : p.Key == 0 ? "No Team" : "Team {0}".F(p.Key);
teams.Add(label, c); teams.Add(label, p);
} }
playerList.RemoveChildren(); playerList.RemoveChildren();
@@ -408,7 +623,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic
var flag = item.Get<ImageWidget>("FLAG"); var flag = item.Get<ImageWidget>("FLAG");
flag.GetImageCollection = () => "flags"; flag.GetImageCollection = () => "flags";
flag.GetImageName = () => o.Country; flag.GetImageName = () => o.FactionId;
playerList.AddChild(item); 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, var item = ScrollItemWidget.Setup(template,
() => selectedReplay == replay, () => selectedReplay == replay,
() => SelectReplay(replay), () => SelectReplay(replay),
() => WatchReplay()); () => WatchReplay());
var f = Path.GetFileNameWithoutExtension(replay.FilePath);
item.Get<LabelWidget>("TITLE").GetText = () => f; replayState[replay] = new ReplayState
item.IsVisible = () => { bool visible; return replayVis.TryGetValue(replay, out visible) && visible; }; {
list.AddChild(item); Item = item,
Visible = true
};
item.Text = Path.GetFileNameWithoutExtension(replay.FilePath);
item.Get<LabelWidget>("TITLE").GetText = () => item.Text;
item.IsVisible = () => replayState[replay].Visible;
replayList.AddChild(item);
}
class ReplayState
{
public bool Visible;
public ScrollItemWidget Item;
} }
class Filter class Filter
@@ -447,8 +675,24 @@ namespace OpenRA.Mods.RA.Widgets.Logic
public GameType Type; public GameType Type;
public DateType Date; public DateType Date;
public DurationType Duration; public DurationType Duration;
public WinState Outcome = WinState.Undefined; public GameInformation.GameOutcome Outcome;
public string PlayerName; 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 enum GameType
{ {

View File

@@ -38,10 +38,10 @@ namespace OpenRA.Mods.RA.Widgets.Logic
tooltipContainer.BeforeRender = () => 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; var teamWidth = 0;
if (client == null) if (occupant == null)
{ {
labelText = "Available spawn"; labelText = "Available spawn";
playerCountry = null; playerCountry = null;
@@ -50,9 +50,9 @@ namespace OpenRA.Mods.RA.Widgets.Logic
} }
else else
{ {
labelText = client.Name; labelText = occupant.PlayerName;
playerCountry = client.Country; playerCountry = occupant.Country;
playerTeam = client.Team; playerTeam = occupant.Team;
widget.Bounds.Height = playerTeam > 0 ? doubleHeight : singleHeight; widget.Bounds.Height = playerTeam > 0 ? doubleHeight : singleHeight;
teamWidth = teamFont.Measure(team.GetText()).X; teamWidth = teamFont.Measure(team.GetText()).X;
} }

View File

@@ -1,42 +1,212 @@
Container@REPLAYBROWSER_PANEL: Container@REPLAYBROWSER_PANEL:
Logic: ReplayBrowserLogic Logic: ReplayBrowserLogic
X: (WINDOW_RIGHT - WIDTH)/2 X: (WINDOW_RIGHT - WIDTH)/2
Y: (WINDOW_BOTTOM - 500)/2 Y: (WINDOW_BOTTOM - HEIGHT)/2
Width: 520 Width: 780
Height: 535 Height: 500
Children: Children:
Label@TITLE: Label@TITLE:
Width: 520 Width: PARENT_RIGHT
Y: 0-25 Y: 0-25
Font: BigBold Font: BigBold
Contrast: true Contrast: true
Align: Center Align: Center
Text: Replay Viewer Text: Replay Viewer
Background@bg: Background@bg:
Width: 520 Width: PARENT_RIGHT
Height: 500 Height: PARENT_BOTTOM
Background: panel-black Background: panel-black
Children: Children:
Container@FILTER_AND_MANAGE_CONTAINER:
X: 20
Y: 20
Width: 280
Height: PARENT_BOTTOM - 40
Children:
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: 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:
Label@REPLAYBROWSER_LABEL_TITLE:
Width: PARENT_RIGHT
Height: 25
Text: Choose Replay
Align: Center
Font: Bold
ScrollPanel@REPLAY_LIST: ScrollPanel@REPLAY_LIST:
X: 15 X: 0
Y: 15 Y: 30
Width: 282 Width: PARENT_RIGHT
Height: PARENT_BOTTOM-30 Height: PARENT_BOTTOM - 30
CollapseHiddenChildren: True
Children: Children:
ScrollItem@REPLAY_TEMPLATE: ScrollItem@REPLAY_TEMPLATE:
Width: PARENT_RIGHT-27 Width: PARENT_RIGHT-27
Height: 25 Height: 25
X: 2 X: 2
Y: 0
Visible: false Visible: false
Children: Children:
Label@TITLE: Label@TITLE:
X: 10 X: 10
Width: PARENT_RIGHT-20 Width: PARENT_RIGHT-20
Height: 25 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: Background@MAP_BG:
X: PARENT_RIGHT-WIDTH-15 Y: 30
Y: 15
Width: 194 Width: 194
Height: 194 Height: 194
Background: panel-gray Background: panel-gray
@@ -48,33 +218,33 @@ Container@REPLAYBROWSER_PANEL:
Height: 192 Height: 192
TooltipContainer: TOOLTIP_CONTAINER TooltipContainer: TOOLTIP_CONTAINER
Container@REPLAY_INFO: Container@REPLAY_INFO:
X: PARENT_RIGHT-WIDTH-15 X: PARENT_RIGHT - WIDTH - 20
Y: 15 Y: 20 + 30+194 + 10
Width: 194 Width: 194
Height: PARENT_BOTTOM - 15 Height: PARENT_BOTTOM - 20 - 30-194 - 10 - 20
Children: Children:
Label@MAP_TITLE: Label@MAP_TITLE:
Y: 197 Y: 0
Width: PARENT_RIGHT Width: PARENT_RIGHT
Height: 25 Height: 15
Font: Bold Font: Bold
Align: Center Align: Center
Label@MAP_TYPE: Label@MAP_TYPE:
Y: 212 Y: 15
Width: PARENT_RIGHT Width: PARENT_RIGHT
Height: 25 Height: 15
Font: TinyBold Font: TinyBold
Align: Center Align: Center
Label@DURATION: Label@DURATION:
Y: 225 Y: 30
Width: PARENT_RIGHT Width: PARENT_RIGHT
Height: 25 Height: 15
Font: Tiny Font: Tiny
Align: Center Align: Center
ScrollPanel@PLAYER_LIST: ScrollPanel@PLAYER_LIST:
Y: 250 Y: 50
Width: PARENT_RIGHT Width: PARENT_RIGHT
Height: PARENT_BOTTOM - 250 - 15 Height: PARENT_BOTTOM - 50
IgnoreChildMouseOver: true IgnoreChildMouseOver: true
Children: Children:
ScrollItem@HEADER: ScrollItem@HEADER:
@@ -98,7 +268,7 @@ Container@REPLAYBROWSER_PANEL:
Children: Children:
Image@FLAG: Image@FLAG:
X: 4 X: 4
Y: 4 Y: 6
Width: 32 Width: 32
Height: 16 Height: 16
Label@LABEL: Label@LABEL:
@@ -112,14 +282,14 @@ Container@REPLAYBROWSER_PANEL:
Button@CANCEL_BUTTON: Button@CANCEL_BUTTON:
Key: escape Key: escape
X: 0 X: 0
Y: 499 Y: PARENT_BOTTOM - 1
Width: 140 Width: 140
Height: 35 Height: 35
Text: Back Text: Back
Button@WATCH_BUTTON: Button@WATCH_BUTTON:
Key: return Key: return
X: 380 X: PARENT_RIGHT - 140
Y: 499 Y: PARENT_BOTTOM - 1
Width: 140 Width: 140
Height: 35 Height: 35
Text: Watch Text: Watch

View File

@@ -2,17 +2,22 @@ Background@REPLAYBROWSER_PANEL:
Logic: ReplayBrowserLogic Logic: ReplayBrowserLogic
X: (WINDOW_RIGHT - WIDTH)/2 X: (WINDOW_RIGHT - WIDTH)/2
Y: (WINDOW_BOTTOM - HEIGHT)/2 Y: (WINDOW_BOTTOM - HEIGHT)/2
Width: 490 Width: 780
Height: 535 Height: 535
Children: Children:
Container@FILTERS: Container@FILTER_AND_MANAGE_CONTAINER:
X: 20 X: 20
Y: 20 Y: 20
Width: 280 Width: 280
Height: 180 Height: PARENT_BOTTOM - 75
Children:
Container@FILTERS:
Width: 280
Height: 320
Children: Children:
Label@FILTERS_TITLE: Label@FILTERS_TITLE:
Width: PARENT_RIGHT X: 85
Width: PARENT_RIGHT - 85
Height: 25 Height: 25
Font: Bold Font: Bold
Align: Center Align: Center
@@ -27,9 +32,8 @@ Background@REPLAYBROWSER_PANEL:
DropDownButton@FLT_GAMETYPE_DROPDOWNBUTTON: DropDownButton@FLT_GAMETYPE_DROPDOWNBUTTON:
X: 85 X: 85
Y: 30 Y: 30
Width: 160 Width: PARENT_RIGHT - 85
Height: 25 Height: 25
Font: Regular
Text: Any Text: Any
Label@FLT_DATE_DESC: Label@FLT_DATE_DESC:
X: 0 X: 0
@@ -41,57 +45,118 @@ Background@REPLAYBROWSER_PANEL:
DropDownButton@FLT_DATE_DROPDOWNBUTTON: DropDownButton@FLT_DATE_DROPDOWNBUTTON:
X: 85 X: 85
Y: 60 Y: 60
Width: 160 Width: PARENT_RIGHT - 85
Height: 25 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 Text: Any
Label@FLT_DURATION_DESC: Label@FLT_DURATION_DESC:
X: 0 X: 0
Y: 150 Y: 90
Width: 80 Width: 80
Height: 25 Height: 25
Text: Duration: Text: Duration:
Align: Right Align: Right
DropDownButton@FLT_DURATION_DROPDOWNBUTTON: DropDownButton@FLT_DURATION_DROPDOWNBUTTON:
X: 85 X: 85
Y: 150 Y: 90
Width: 160 Width: PARENT_RIGHT - 85
Height: 25 Height: 25
Font: Regular
Text: Any Text: Any
Container@REPLAY_LIST_CONTAINER: Label@FLT_MAPNAME_DESC:
X: 20 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 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 Width: 245
Height: PARENT_BOTTOM - 270 Height: PARENT_BOTTOM - 20 - 55
Children: Children:
Label@REPLAYBROWSER_LABEL_TITLE: Label@REPLAYBROWSER_LABEL_TITLE:
Width: PARENT_RIGHT Width: PARENT_RIGHT
@@ -103,7 +168,7 @@ Background@REPLAYBROWSER_PANEL:
X: 0 X: 0
Y: 30 Y: 30
Width: PARENT_RIGHT Width: PARENT_RIGHT
Height: PARENT_BOTTOM - 25 Height: PARENT_BOTTOM - 30
CollapseHiddenChildren: True CollapseHiddenChildren: True
Children: Children:
ScrollItem@REPLAY_TEMPLATE: ScrollItem@REPLAY_TEMPLATE:
@@ -117,10 +182,10 @@ Background@REPLAYBROWSER_PANEL:
Width: PARENT_RIGHT-20 Width: PARENT_RIGHT-20
Height: 25 Height: 25
Container@MAP_BG_CONTAINER: Container@MAP_BG_CONTAINER:
X: PARENT_RIGHT-WIDTH-20 X: PARENT_RIGHT - WIDTH - 20
Y: 20 Y: 20
Width: 194 Width: 194
Height: 194 Height: 30 + 194
Children: Children:
Label@MAP_BG_TITLE: Label@MAP_BG_TITLE:
Width: PARENT_RIGHT Width: PARENT_RIGHT
@@ -141,33 +206,33 @@ Background@REPLAYBROWSER_PANEL:
Height: 192 Height: 192
TooltipContainer: TOOLTIP_CONTAINER TooltipContainer: TOOLTIP_CONTAINER
Container@REPLAY_INFO: Container@REPLAY_INFO:
X: PARENT_RIGHT-WIDTH - 20 X: PARENT_RIGHT - WIDTH - 20
Y: 50 Y: 20 + 30+194 + 10
Width: 194 Width: 194
Height: PARENT_BOTTOM - 15 Height: PARENT_BOTTOM - 20 - 30-194 - 10 - 55
Children: Children:
Label@MAP_TITLE: Label@MAP_TITLE:
Y: 197 Y: 0
Width: PARENT_RIGHT Width: PARENT_RIGHT
Height: 25 Height: 15
Font: Bold Font: Bold
Align: Center Align: Center
Label@MAP_TYPE: Label@MAP_TYPE:
Y: 212 Y: 15
Width: PARENT_RIGHT Width: PARENT_RIGHT
Height: 25 Height: 15
Font: TinyBold Font: TinyBold
Align: Center Align: Center
Label@DURATION: Label@DURATION:
Y: 225 Y: 30
Width: PARENT_RIGHT Width: PARENT_RIGHT
Height: 25 Height: 15
Font: Tiny Font: Tiny
Align: Center Align: Center
ScrollPanel@PLAYER_LIST: ScrollPanel@PLAYER_LIST:
Y: 250 Y: 50
Width: PARENT_RIGHT Width: PARENT_RIGHT
Height: PARENT_BOTTOM - 340 Height: PARENT_BOTTOM - 50
IgnoreChildMouseOver: true IgnoreChildMouseOver: true
Children: Children:
ScrollItem@HEADER: ScrollItem@HEADER:
@@ -192,7 +257,7 @@ Background@REPLAYBROWSER_PANEL:
Children: Children:
Image@FLAG: Image@FLAG:
X: 4 X: 4
Y: 4 Y: 6
Width: 32 Width: 32
Height: 16 Height: 16
Label@LABEL: Label@LABEL: