883 lines
25 KiB
C#
883 lines
25 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright (c) The OpenRA Developers and Contributors
|
|
* 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, either version 3 of
|
|
* the License, or (at your option) any later version. For more
|
|
* information, see COPYING.
|
|
*/
|
|
#endregion
|
|
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using OpenRA.FileFormats;
|
|
using OpenRA.Network;
|
|
using OpenRA.Primitives;
|
|
using OpenRA.Traits;
|
|
using OpenRA.Widgets;
|
|
|
|
namespace OpenRA.Mods.Common.Widgets.Logic
|
|
{
|
|
public class ReplayBrowserLogic : ChromeLogic
|
|
{
|
|
[FluentReference("time")]
|
|
const string Duration = "label-duration";
|
|
|
|
[FluentReference]
|
|
const string Singleplayer = "options-replay-type.singleplayer";
|
|
|
|
[FluentReference]
|
|
const string Multiplayer = "options-replay-type.multiplayer";
|
|
|
|
[FluentReference]
|
|
const string Today = "options-replay-date.today";
|
|
|
|
[FluentReference]
|
|
const string LastWeek = "options-replay-date.last-week";
|
|
|
|
[FluentReference]
|
|
const string LastFortnight = "options-replay-date.last-fortnight";
|
|
|
|
[FluentReference]
|
|
const string LastMonth = "options-replay-date.last-month";
|
|
|
|
[FluentReference]
|
|
const string ReplayDurationVeryShort = "options-replay-duration.very-short";
|
|
|
|
[FluentReference]
|
|
const string ReplayDurationShort = "options-replay-duration.short";
|
|
|
|
[FluentReference]
|
|
const string ReplayDurationMedium = "options-replay-duration.medium";
|
|
|
|
[FluentReference]
|
|
const string ReplayDurationLong = "options-replay-duration.long";
|
|
|
|
[FluentReference]
|
|
const string RenameReplayTitle = "dialog-rename-replay.title";
|
|
|
|
[FluentReference]
|
|
const string RenameReplayPrompt = "dialog-rename-replay.prompt";
|
|
|
|
[FluentReference]
|
|
const string RenameReplayAccept = "dialog-rename-replay.confirm";
|
|
|
|
[FluentReference]
|
|
const string DeleteReplayTitle = "dialog-delete-replay.title";
|
|
|
|
[FluentReference("replay")]
|
|
const string DeleteReplayPrompt = "dialog-delete-replay.prompt";
|
|
|
|
[FluentReference]
|
|
const string DeleteReplayAccept = "dialog-delete-replay.confirm";
|
|
|
|
[FluentReference]
|
|
const string DeleteAllReplaysTitle = "dialog-delete-all-replays.title";
|
|
|
|
[FluentReference("count")]
|
|
const string DeleteAllReplaysPrompt = "dialog-delete-all-replays.prompt";
|
|
|
|
[FluentReference]
|
|
const string DeleteAllReplaysAccept = "dialog-delete-all-replays.confirm";
|
|
|
|
[FluentReference("file")]
|
|
const string ReplayDeletionFailed = "notification-replay-deletion-failed";
|
|
|
|
[FluentReference]
|
|
const string Players = "label-players";
|
|
|
|
[FluentReference("team")]
|
|
const string TeamNumber = "label-team-name";
|
|
|
|
[FluentReference]
|
|
const string NoTeam = "label-no-team";
|
|
|
|
[FluentReference]
|
|
const string Victory = "options-winstate.victory";
|
|
|
|
[FluentReference]
|
|
const string Defeat = "options-winstate.defeat";
|
|
|
|
static Filter filter = new();
|
|
|
|
readonly Widget panel;
|
|
readonly ScrollPanelWidget replayList, playerList;
|
|
readonly ScrollItemWidget playerTemplate, playerHeader;
|
|
readonly List<ReplayMetadata> replays = new();
|
|
readonly Dictionary<ReplayMetadata, ReplayState> replayState = new();
|
|
readonly Action onStart;
|
|
readonly ModData modData;
|
|
readonly WebServices services;
|
|
|
|
MapPreview map;
|
|
ReplayMetadata selectedReplay;
|
|
|
|
volatile bool cancelLoadingReplays;
|
|
|
|
[ObjectCreator.UseCtor]
|
|
public ReplayBrowserLogic(Widget widget, ModData modData, Action onExit, Action onStart)
|
|
{
|
|
map = MapCache.UnknownMap;
|
|
panel = widget;
|
|
|
|
services = modData.Manifest.Get<WebServices>();
|
|
this.modData = modData;
|
|
this.onStart = onStart;
|
|
Game.BeforeGameStart += OnGameStart;
|
|
|
|
playerList = panel.Get<ScrollPanelWidget>("PLAYER_LIST");
|
|
playerHeader = playerList.Get<ScrollItemWidget>("HEADER");
|
|
playerTemplate = playerList.Get<ScrollItemWidget>("TEMPLATE");
|
|
playerList.RemoveChildren();
|
|
|
|
panel.Get<ButtonWidget>("CANCEL_BUTTON").OnClick = () => { cancelLoadingReplays = true; Ui.CloseWindow(); onExit(); };
|
|
|
|
replayList = panel.Get<ScrollPanelWidget>("REPLAY_LIST");
|
|
var template = panel.Get<ScrollItemWidget>("REPLAY_TEMPLATE");
|
|
|
|
var mod = modData.Manifest;
|
|
var dir = Path.Combine(Platform.SupportDir, "Replays", mod.Id, mod.Metadata.Version);
|
|
|
|
if (Directory.Exists(dir))
|
|
ThreadPool.QueueUserWorkItem(_ => LoadReplays(dir, template));
|
|
|
|
var watch = panel.Get<ButtonWidget>("WATCH_BUTTON");
|
|
watch.IsDisabled = () => selectedReplay == null || map.Status != MapStatus.Available;
|
|
watch.OnClick = WatchReplay;
|
|
|
|
var mapPreviewRoot = panel.Get("MAP_PREVIEW_ROOT");
|
|
mapPreviewRoot.IsVisible = () => selectedReplay != null;
|
|
panel.Get("REPLAY_INFO").IsVisible = () => selectedReplay != null;
|
|
|
|
var spawnOccupants = new CachedTransform<ReplayMetadata, Dictionary<int, SpawnOccupant>>(r =>
|
|
{
|
|
// Avoid using .ToDictionary to improve robustness against replays defining duplicate spawn assignments
|
|
var occupants = new Dictionary<int, SpawnOccupant>();
|
|
foreach (var p in r.GameInfo.Players)
|
|
if (p.SpawnPoint != 0)
|
|
occupants[p.SpawnPoint] = new SpawnOccupant(p);
|
|
|
|
return occupants;
|
|
});
|
|
|
|
var noSpawns = new HashSet<int>();
|
|
var disabledSpawnPoints = new CachedTransform<ReplayMetadata, HashSet<int>>(r => r.GameInfo.DisabledSpawnPoints ?? noSpawns);
|
|
|
|
Ui.LoadWidget("MAP_PREVIEW", mapPreviewRoot, new WidgetArgs
|
|
{
|
|
{ "orderManager", null },
|
|
{ "getMap", (Func<(MapPreview, Session.MapStatus)>)(() => (map, Session.MapStatus.Playable)) },
|
|
{ "onMouseDown", null },
|
|
{ "getSpawnOccupants", (Func<Dictionary<int, SpawnOccupant>>)(() => spawnOccupants.Update(selectedReplay)) },
|
|
{ "getDisabledSpawnPoints", (Func<HashSet<int>>)(() => disabledSpawnPoints.Update(selectedReplay)) },
|
|
{ "showUnoccupiedSpawnpoints", false },
|
|
{ "mapUpdatesEnabled", false },
|
|
{ "onMapUpdate", (Action<string>)(_ => { }) },
|
|
});
|
|
|
|
var replayDuration = new CachedTransform<ReplayMetadata, string>(r =>
|
|
FluentProvider.GetMessage(Duration, "time", WidgetUtils.FormatTimeSeconds((int)selectedReplay.GameInfo.Duration.TotalSeconds)));
|
|
panel.Get<LabelWidget>("DURATION").GetText = () => replayDuration.Update(selectedReplay);
|
|
|
|
SetupFilters();
|
|
SetupManagement();
|
|
}
|
|
|
|
void LoadReplays(string dir, ScrollItemWidget template)
|
|
{
|
|
using (new Support.PerfTimer("Load replays"))
|
|
{
|
|
var loadedReplays = new ConcurrentBag<ReplayMetadata>();
|
|
Parallel.ForEach(Directory.GetFiles(dir, "*.orarep", SearchOption.AllDirectories), (fileName, pls) =>
|
|
{
|
|
if (cancelLoadingReplays)
|
|
{
|
|
pls.Stop();
|
|
return;
|
|
}
|
|
|
|
var replay = ReplayMetadata.Read(fileName);
|
|
if (replay != null)
|
|
loadedReplays.Add(replay);
|
|
});
|
|
|
|
if (cancelLoadingReplays)
|
|
return;
|
|
|
|
var sortedReplays = loadedReplays.OrderByDescending(replay => replay.GameInfo.StartTimeUtc).ToList();
|
|
Game.RunAfterTick(() =>
|
|
{
|
|
replayList.RemoveChildren();
|
|
foreach (var replay in sortedReplays)
|
|
AddReplay(replay, template);
|
|
|
|
SetupReplayDependentFilters();
|
|
ApplyFilter();
|
|
});
|
|
}
|
|
}
|
|
|
|
void SetupFilters()
|
|
{
|
|
// Game type
|
|
{
|
|
var ddb = panel.GetOrNull<DropDownButtonWidget>("FLT_GAMETYPE_DROPDOWNBUTTON");
|
|
if (ddb != null)
|
|
{
|
|
// Using list to maintain the order
|
|
var options = new List<(GameType GameType, string Text)>
|
|
{
|
|
(GameType.Any, ddb.GetText()),
|
|
(GameType.Singleplayer, FluentProvider.GetMessage(Singleplayer)),
|
|
(GameType.Multiplayer, FluentProvider.GetMessage(Multiplayer))
|
|
};
|
|
|
|
var lookup = options.ToDictionary(kvp => kvp.GameType, kvp => kvp.Text);
|
|
|
|
ddb.GetText = () => lookup[filter.Type];
|
|
ddb.OnMouseDown = _ =>
|
|
{
|
|
ScrollItemWidget SetupItem((GameType GameType, string Text) option, ScrollItemWidget tpl)
|
|
{
|
|
var item = ScrollItemWidget.Setup(
|
|
tpl,
|
|
() => filter.Type == option.GameType,
|
|
() => { filter.Type = option.GameType; ApplyFilter(); });
|
|
item.Get<LabelWidget>("LABEL").GetText = () => option.Text;
|
|
return item;
|
|
}
|
|
|
|
ddb.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 330, options, SetupItem);
|
|
};
|
|
}
|
|
}
|
|
|
|
// Date type
|
|
{
|
|
var ddb = panel.GetOrNull<DropDownButtonWidget>("FLT_DATE_DROPDOWNBUTTON");
|
|
if (ddb != null)
|
|
{
|
|
// Using list to maintain the order
|
|
var options = new List<(DateType DateType, string Text)>
|
|
{
|
|
(DateType.Any, ddb.GetText()),
|
|
(DateType.Today, FluentProvider.GetMessage(Today)),
|
|
(DateType.LastWeek, FluentProvider.GetMessage(LastWeek)),
|
|
(DateType.LastFortnight, FluentProvider.GetMessage(LastFortnight)),
|
|
(DateType.LastMonth, FluentProvider.GetMessage(LastMonth))
|
|
};
|
|
|
|
var lookup = options.ToDictionary(kvp => kvp.DateType, kvp => kvp.Text);
|
|
|
|
ddb.GetText = () => lookup[filter.Date];
|
|
ddb.OnMouseDown = _ =>
|
|
{
|
|
ScrollItemWidget SetupItem((DateType DateType, string Text) option, ScrollItemWidget tpl)
|
|
{
|
|
var item = ScrollItemWidget.Setup(
|
|
tpl,
|
|
() => filter.Date == option.DateType,
|
|
() => { filter.Date = option.DateType; ApplyFilter(); });
|
|
|
|
item.Get<LabelWidget>("LABEL").GetText = () => option.Text;
|
|
return item;
|
|
}
|
|
|
|
ddb.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 330, options, SetupItem);
|
|
};
|
|
}
|
|
}
|
|
|
|
// Duration
|
|
{
|
|
var ddb = panel.GetOrNull<DropDownButtonWidget>("FLT_DURATION_DROPDOWNBUTTON");
|
|
if (ddb != null)
|
|
{
|
|
// Using list to maintain the order
|
|
var options = new List<(DurationType DurationType, string Text)>
|
|
{
|
|
(DurationType.Any, ddb.GetText()),
|
|
(DurationType.VeryShort, FluentProvider.GetMessage(ReplayDurationVeryShort)),
|
|
(DurationType.Short, FluentProvider.GetMessage(ReplayDurationShort)),
|
|
(DurationType.Medium, FluentProvider.GetMessage(ReplayDurationMedium)),
|
|
(DurationType.Long, FluentProvider.GetMessage(ReplayDurationLong))
|
|
};
|
|
|
|
var lookup = options.ToDictionary(kvp => kvp.DurationType, kvp => kvp.Text);
|
|
|
|
ddb.GetText = () => lookup[filter.Duration];
|
|
ddb.OnMouseDown = _ =>
|
|
{
|
|
ScrollItemWidget SetupItem((DurationType DurationType, string Text) option, ScrollItemWidget tpl)
|
|
{
|
|
var item = ScrollItemWidget.Setup(
|
|
tpl,
|
|
() => filter.Duration == option.DurationType,
|
|
() => { filter.Duration = option.DurationType; ApplyFilter(); });
|
|
item.Get<LabelWidget>("LABEL").GetText = () => option.Text;
|
|
return item;
|
|
}
|
|
|
|
ddb.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 330, options, SetupItem);
|
|
};
|
|
}
|
|
}
|
|
|
|
// Outcome (depends on Player)
|
|
{
|
|
var ddb = panel.GetOrNull<DropDownButtonWidget>("FLT_OUTCOME_DROPDOWNBUTTON");
|
|
if (ddb != null)
|
|
{
|
|
ddb.IsDisabled = () => string.IsNullOrEmpty(filter.PlayerName);
|
|
|
|
// Using list to maintain the order
|
|
var options = new List<(WinState WinState, string Text)>
|
|
{
|
|
(WinState.Undefined, ddb.GetText()),
|
|
(WinState.Lost, FluentProvider.GetMessage(Defeat)),
|
|
(WinState.Won, FluentProvider.GetMessage(Victory))
|
|
};
|
|
|
|
var lookup = options.ToDictionary(kvp => kvp.WinState, kvp => kvp.Text);
|
|
|
|
ddb.GetText = () => lookup[filter.Outcome];
|
|
ddb.OnMouseDown = _ =>
|
|
{
|
|
ScrollItemWidget SetupItem((WinState WinState, string Text) option, ScrollItemWidget tpl)
|
|
{
|
|
var item = ScrollItemWidget.Setup(
|
|
tpl,
|
|
() => filter.Outcome == option.WinState,
|
|
() => { filter.Outcome = option.WinState; ApplyFilter(); });
|
|
item.Get<LabelWidget>("LABEL").GetText = () => option.Text;
|
|
return item;
|
|
}
|
|
|
|
ddb.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 330, options, SetupItem);
|
|
};
|
|
}
|
|
}
|
|
|
|
// Reset button
|
|
{
|
|
var button = panel.Get<ButtonWidget>("FLT_RESET_BUTTON");
|
|
button.IsDisabled = () => filter.IsEmpty;
|
|
button.OnClick = () => { filter = new Filter(); ApplyFilter(); };
|
|
}
|
|
}
|
|
|
|
void SetupReplayDependentFilters()
|
|
{
|
|
// Map
|
|
{
|
|
var ddb = panel.GetOrNull<DropDownButtonWidget>("FLT_MAPNAME_DROPDOWNBUTTON");
|
|
if (ddb != null)
|
|
{
|
|
var options = replays.Select(r => r.GameInfo.MapTitle).Distinct(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 = _ =>
|
|
{
|
|
ScrollItemWidget SetupItem(string option, ScrollItemWidget tpl)
|
|
{
|
|
var item = ScrollItemWidget.Setup(
|
|
tpl,
|
|
() => string.Equals(filter.MapName, option, StringComparison.CurrentCultureIgnoreCase),
|
|
() => { filter.MapName = option; ApplyFilter(); });
|
|
item.Get<LabelWidget>("LABEL").GetText = () => option ?? anyText;
|
|
return item;
|
|
}
|
|
|
|
ddb.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 330, options, SetupItem);
|
|
};
|
|
}
|
|
}
|
|
|
|
// Players
|
|
{
|
|
var ddb = panel.GetOrNull<DropDownButtonWidget>("FLT_PLAYER_DROPDOWNBUTTON");
|
|
if (ddb != null)
|
|
{
|
|
var options = replays.SelectMany(r => r.GameInfo.Players.Select(p => r.GameInfo.ResolvedPlayerName(p)))
|
|
.Distinct(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 = _ =>
|
|
{
|
|
ScrollItemWidget SetupItem(string option, ScrollItemWidget tpl)
|
|
{
|
|
var item = ScrollItemWidget.Setup(
|
|
tpl,
|
|
() => string.Equals(filter.PlayerName, option, StringComparison.CurrentCultureIgnoreCase),
|
|
() => { filter.PlayerName = option; ApplyFilter(); });
|
|
item.Get<LabelWidget>("LABEL").GetText = () => option ?? anyText;
|
|
return item;
|
|
}
|
|
|
|
ddb.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 330, options, SetupItem);
|
|
};
|
|
}
|
|
}
|
|
|
|
// Faction (depends on Player)
|
|
{
|
|
var ddb = panel.GetOrNull<DropDownButtonWidget>("FLT_FACTION_DROPDOWNBUTTON");
|
|
if (ddb != null)
|
|
{
|
|
ddb.IsDisabled = () => string.IsNullOrEmpty(filter.PlayerName);
|
|
|
|
var options = replays
|
|
.SelectMany(r => r.GameInfo.Players.Select(p => p.FactionName).Where(n => !string.IsNullOrEmpty(n)))
|
|
.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
|
options.Sort(StringComparer.OrdinalIgnoreCase);
|
|
options.Insert(0, null); // no filter
|
|
|
|
var anyText = ddb.GetText();
|
|
ddb.GetText = () => string.IsNullOrEmpty(filter.Faction) ? anyText : FluentProvider.GetMessage(filter.Faction);
|
|
ddb.OnMouseDown = _ =>
|
|
{
|
|
ScrollItemWidget SetupItem(string option, ScrollItemWidget tpl)
|
|
{
|
|
var item = ScrollItemWidget.Setup(
|
|
tpl,
|
|
() => string.Equals(filter.Faction, option, StringComparison.CurrentCultureIgnoreCase),
|
|
() => { filter.Faction = option; ApplyFilter(); });
|
|
item.Get<LabelWidget>("LABEL").GetText = () => option != null ? FluentProvider.GetMessage(option) : anyText;
|
|
return item;
|
|
}
|
|
|
|
ddb.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 330, options, SetupItem);
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
void SetupManagement()
|
|
{
|
|
var renameButton = panel.Get<ButtonWidget>("MNG_RENSEL_BUTTON");
|
|
renameButton.IsDisabled = () => selectedReplay == null;
|
|
renameButton.OnClick = () =>
|
|
{
|
|
var r = selectedReplay;
|
|
var initialName = Path.GetFileNameWithoutExtension(r.FilePath);
|
|
var directoryName = Path.GetDirectoryName(r.FilePath);
|
|
var invalidChars = Path.GetInvalidFileNameChars();
|
|
|
|
ConfirmationDialogs.TextInputPrompt(modData,
|
|
RenameReplayTitle,
|
|
RenameReplayPrompt,
|
|
initialName,
|
|
onAccept: newName => RenameReplay(r, newName),
|
|
onCancel: null,
|
|
acceptText: RenameReplayAccept,
|
|
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;
|
|
});
|
|
};
|
|
|
|
void OnDeleteReplay(ReplayMetadata r, Action after)
|
|
{
|
|
ConfirmationDialogs.ButtonPrompt(modData,
|
|
title: DeleteReplayTitle,
|
|
text: DeleteReplayPrompt,
|
|
textArguments: new object[] { "replay", Path.GetFileNameWithoutExtension(r.FilePath) },
|
|
onConfirm: () =>
|
|
{
|
|
DeleteReplay(r);
|
|
after?.Invoke();
|
|
},
|
|
confirmText: DeleteReplayAccept,
|
|
onCancel: () => { });
|
|
}
|
|
|
|
var deleteButton = panel.Get<ButtonWidget>("MNG_DELSEL_BUTTON");
|
|
deleteButton.IsDisabled = () => selectedReplay == null;
|
|
deleteButton.OnClick = () =>
|
|
{
|
|
OnDeleteReplay(selectedReplay, () =>
|
|
{
|
|
if (selectedReplay == null)
|
|
SelectFirstVisibleReplay();
|
|
});
|
|
};
|
|
|
|
var deleteAllButton = panel.Get<ButtonWidget>("MNG_DELALL_BUTTON");
|
|
deleteAllButton.IsDisabled = () => !replayState.Any(kvp => kvp.Value.Visible);
|
|
deleteAllButton.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.ButtonPrompt(modData,
|
|
title: DeleteAllReplaysTitle,
|
|
text: DeleteAllReplaysPrompt,
|
|
textArguments: new object[] { "count", list.Count },
|
|
onConfirm: () =>
|
|
{
|
|
foreach (var replayMetadata in list)
|
|
DeleteReplay(replayMetadata);
|
|
|
|
if (selectedReplay == null)
|
|
SelectFirstVisibleReplay();
|
|
},
|
|
confirmText: DeleteAllReplaysAccept,
|
|
onCancel: () => { });
|
|
};
|
|
}
|
|
|
|
void RenameReplay(ReplayMetadata replay, string newFilenameWithoutExtension)
|
|
{
|
|
try
|
|
{
|
|
var item = replayState[replay].Item;
|
|
replay.RenameFile(newFilenameWithoutExtension);
|
|
item.GetText = () => newFilenameWithoutExtension;
|
|
|
|
var label = item.Get<LabelWithTooltipWidget>("TITLE");
|
|
WidgetUtils.TruncateLabelToTooltip(label, item.GetText());
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Write("debug", ex.ToString());
|
|
}
|
|
}
|
|
|
|
void DeleteReplay(ReplayMetadata replay)
|
|
{
|
|
try
|
|
{
|
|
File.Delete(replay.FilePath);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
TextNotificationsManager.Debug(FluentProvider.GetMessage(ReplayDeletionFailed, "file", replay.FilePath));
|
|
Log.Write("debug", ex.ToString());
|
|
return;
|
|
}
|
|
|
|
if (replay == selectedReplay)
|
|
SelectReplay(null);
|
|
|
|
replayList.RemoveChild(replayState[replay].Item);
|
|
replays.Remove(replay);
|
|
replayState.Remove(replay);
|
|
}
|
|
|
|
static 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.Equals(filter.MapName, replay.GameInfo.MapTitle, StringComparison.CurrentCultureIgnoreCase))
|
|
return false;
|
|
|
|
// Player
|
|
if (!string.IsNullOrEmpty(filter.PlayerName))
|
|
{
|
|
var player = replay.GameInfo.Players.FirstOrDefault(
|
|
p => string.Equals(filter.PlayerName, replay.GameInfo.ResolvedPlayerName(p), StringComparison.CurrentCultureIgnoreCase));
|
|
if (player == null)
|
|
return false;
|
|
|
|
// Outcome
|
|
if (filter.Outcome != WinState.Undefined && filter.Outcome != player.Outcome)
|
|
return false;
|
|
|
|
// Faction
|
|
if (!string.IsNullOrEmpty(filter.Faction) &&
|
|
!string.Equals(filter.Faction, player.FactionName, StringComparison.CurrentCultureIgnoreCase))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void ApplyFilter()
|
|
{
|
|
foreach (var replay in replays)
|
|
replayState[replay].Visible = EvaluateReplayVisibility(replay);
|
|
|
|
if (selectedReplay == null || !replayState[selectedReplay].Visible)
|
|
SelectFirstVisibleReplay();
|
|
|
|
replayList.Layout.AdjustChildren();
|
|
replayList.ScrollToSelectedItem();
|
|
}
|
|
|
|
void SelectFirstVisibleReplay()
|
|
{
|
|
SelectReplay(replays.FirstOrDefault(r => replayState[r].Visible));
|
|
}
|
|
|
|
void SelectReplay(ReplayMetadata replay)
|
|
{
|
|
selectedReplay = replay;
|
|
map = selectedReplay != null ? selectedReplay.GameInfo.MapPreview : MapCache.UnknownMap;
|
|
|
|
if (replay == null)
|
|
return;
|
|
|
|
try
|
|
{
|
|
if (map.Status == MapStatus.Unavailable && Game.Settings.Game.AllowDownloading)
|
|
modData.MapCache.QueryRemoteMapDetails(services.MapRepository, new[] { map.Uid });
|
|
|
|
var players = replay.GameInfo.Players
|
|
.GroupBy(p => p.Team)
|
|
.OrderBy(g => g.Key)
|
|
.ToList();
|
|
|
|
var teams = new Dictionary<string, IEnumerable<GameInformation.Player>>();
|
|
var noTeams = players.Count == 1;
|
|
foreach (var p in players)
|
|
{
|
|
var label = noTeams ? FluentProvider.GetMessage(Players) : p.Key > 0
|
|
? FluentProvider.GetMessage(TeamNumber, "team", p.Key)
|
|
: FluentProvider.GetMessage(NoTeam);
|
|
|
|
teams.Add(label, p);
|
|
}
|
|
|
|
playerList.RemoveChildren();
|
|
|
|
foreach (var kv in teams)
|
|
{
|
|
var group = kv.Key;
|
|
if (group.Length > 0)
|
|
{
|
|
var header = ScrollItemWidget.Setup(playerHeader, () => false, () => { });
|
|
header.Get<LabelWidget>("LABEL").GetText = () => group;
|
|
playerList.AddChild(header);
|
|
}
|
|
|
|
foreach (var option in kv.Value)
|
|
{
|
|
var o = option;
|
|
|
|
var color = o.Color;
|
|
|
|
var item = ScrollItemWidget.Setup(playerTemplate, () => false, () => { });
|
|
|
|
var label = item.Get<LabelWidget>("LABEL");
|
|
var font = Game.Renderer.Fonts[label.Font];
|
|
var name = WidgetUtils.TruncateText(replay.GameInfo.ResolvedPlayerName(o), label.Bounds.Width, font);
|
|
label.GetText = () => name;
|
|
label.GetColor = () => color;
|
|
|
|
var flag = item.Get<ImageWidget>("FLAG");
|
|
flag.GetImageCollection = () => "flags";
|
|
var factionInfo = modData.DefaultRules.Actors[SystemActors.World].TraitInfos<FactionInfo>();
|
|
flag.GetImageName = () => (factionInfo != null && factionInfo.Any(f => f.InternalName == o.FactionId)) ? o.FactionId : "Random";
|
|
|
|
playerList.AddChild(item);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.Write("debug", $"Exception while parsing replay: {replay}");
|
|
Log.Write("debug", e);
|
|
SelectReplay(null);
|
|
}
|
|
}
|
|
|
|
void WatchReplay()
|
|
{
|
|
if (selectedReplay != null && ReplayUtils.PromptConfirmReplayCompatibility(selectedReplay, modData))
|
|
{
|
|
cancelLoadingReplays = true;
|
|
|
|
DiscordService.UpdateStatus(DiscordState.WatchingReplay);
|
|
|
|
Game.JoinReplay(selectedReplay.FilePath);
|
|
}
|
|
}
|
|
|
|
void AddReplay(ReplayMetadata replay, ScrollItemWidget template)
|
|
{
|
|
replays.Add(replay);
|
|
|
|
var item = ScrollItemWidget.Setup(template,
|
|
() => selectedReplay == replay,
|
|
() => SelectReplay(replay),
|
|
() => WatchReplay());
|
|
|
|
replayState[replay] = new ReplayState
|
|
{
|
|
Item = item,
|
|
Visible = true
|
|
};
|
|
|
|
var itemText = Path.GetFileNameWithoutExtension(replay.FilePath);
|
|
item.GetText = () => itemText;
|
|
var label = item.Get<LabelWithTooltipWidget>("TITLE");
|
|
WidgetUtils.TruncateLabelToTooltip(label, itemText);
|
|
|
|
item.IsVisible = () => replayState[replay].Visible;
|
|
replayList.AddChild(item);
|
|
}
|
|
|
|
void OnGameStart()
|
|
{
|
|
Ui.CloseWindow();
|
|
onStart();
|
|
}
|
|
|
|
bool disposed;
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
if (disposing && !disposed)
|
|
{
|
|
disposed = true;
|
|
Game.BeforeGameStart -= OnGameStart;
|
|
}
|
|
|
|
base.Dispose(disposing);
|
|
}
|
|
|
|
sealed class ReplayState
|
|
{
|
|
public bool Visible;
|
|
public ScrollItemWidget Item;
|
|
}
|
|
|
|
sealed 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 =>
|
|
Type == default
|
|
&& Date == default
|
|
&& Duration == default
|
|
&& Outcome == default
|
|
&& 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
|
|
}
|
|
}
|
|
}
|