1 #region Copyright & License Information
2 /*
3  * Copyright 2007-2020 The OpenRA Developers (see AUTHORS)
4  * This file is part of OpenRA, which is free software. It is made
5  * available to you under the terms of the GNU General Public License
6  * as published by the Free Software Foundation, either version 3 of
7  * the License, or (at your option) any later version. For more
8  * information, see COPYING.
9  */
10 #endregion
11 
12 using System;
13 using System.Collections.Concurrent;
14 using System.Collections.Generic;
15 using System.IO;
16 using System.Linq;
17 using System.Threading;
18 using System.Threading.Tasks;
19 using OpenRA.FileFormats;
20 using OpenRA.Primitives;
21 using OpenRA.Traits;
22 using OpenRA.Widgets;
23 
24 namespace OpenRA.Mods.Common.Widgets.Logic
25 {
26 	public class ReplayBrowserLogic : ChromeLogic
27 	{
28 		static Filter filter = new Filter();
29 
30 		readonly Widget panel;
31 		readonly ScrollPanelWidget replayList, playerList;
32 		readonly ScrollItemWidget playerTemplate, playerHeader;
33 		readonly List<ReplayMetadata> replays = new List<ReplayMetadata>();
34 		readonly Dictionary<ReplayMetadata, ReplayState> replayState = new Dictionary<ReplayMetadata, ReplayState>();
35 		readonly Action onStart;
36 		readonly ModData modData;
37 		readonly WebServices services;
38 
39 		MapPreview map;
40 		ReplayMetadata selectedReplay;
41 
42 		volatile bool cancelLoadingReplays;
43 
44 		[ObjectCreator.UseCtor]
ReplayBrowserLogic(Widget widget, ModData modData, Action onExit, Action onStart)45 		public ReplayBrowserLogic(Widget widget, ModData modData, Action onExit, Action onStart)
46 		{
47 			map = MapCache.UnknownMap;
48 			panel = widget;
49 
50 			services = modData.Manifest.Get<WebServices>();
51 			this.modData = modData;
52 			this.onStart = onStart;
53 			Game.BeforeGameStart += OnGameStart;
54 
55 			playerList = panel.Get<ScrollPanelWidget>("PLAYER_LIST");
56 			playerHeader = playerList.Get<ScrollItemWidget>("HEADER");
57 			playerTemplate = playerList.Get<ScrollItemWidget>("TEMPLATE");
58 			playerList.RemoveChildren();
59 
60 			panel.Get<ButtonWidget>("CANCEL_BUTTON").OnClick = () => { cancelLoadingReplays = true; Ui.CloseWindow(); onExit(); };
61 
62 			replayList = panel.Get<ScrollPanelWidget>("REPLAY_LIST");
63 			var template = panel.Get<ScrollItemWidget>("REPLAY_TEMPLATE");
64 
65 			var mod = modData.Manifest;
66 			var dir = Platform.ResolvePath(Platform.SupportDirPrefix, "Replays", mod.Id, mod.Metadata.Version);
67 
68 			if (Directory.Exists(dir))
69 				ThreadPool.QueueUserWorkItem(_ => LoadReplays(dir, template));
70 
71 			var watch = panel.Get<ButtonWidget>("WATCH_BUTTON");
72 			watch.IsDisabled = () => selectedReplay == null || map.Status != MapStatus.Available;
73 			watch.OnClick = () => { WatchReplay(); };
74 
75 			var mapPreviewRoot = panel.Get("MAP_PREVIEW_ROOT");
76 			mapPreviewRoot.IsVisible = () => selectedReplay != null;
77 			panel.Get("REPLAY_INFO").IsVisible = () => selectedReplay != null;
78 
79 			Ui.LoadWidget("MAP_PREVIEW", mapPreviewRoot, new WidgetArgs
80 			{
81 				{ "orderManager", null },
82 				{ "getMap", (Func<MapPreview>)(() => map) },
83 				{ "onMouseDown",  (Action<MapPreviewWidget, MapPreview, MouseInput>)((preview, mapPreview, mi) => { }) },
84 				{
85 					"getSpawnOccupants", (Func<MapPreview, Dictionary<CPos, SpawnOccupant>>)(mapPreview =>
86 						LobbyUtils.GetSpawnOccupants(selectedReplay.GameInfo.Players, mapPreview))
87 				},
88 				{ "showUnoccupiedSpawnpoints", false },
89 			});
90 
91 			var replayDuration = new CachedTransform<ReplayMetadata, string>(r =>
92 				"Duration: {0}".F(WidgetUtils.FormatTimeSeconds((int)selectedReplay.GameInfo.Duration.TotalSeconds)));
93 			panel.Get<LabelWidget>("DURATION").GetText = () => replayDuration.Update(selectedReplay);
94 
95 			SetupFilters();
96 			SetupManagement();
97 		}
98 
LoadReplays(string dir, ScrollItemWidget template)99 		void LoadReplays(string dir, ScrollItemWidget template)
100 		{
101 			using (new Support.PerfTimer("Load replays"))
102 			{
103 				var loadedReplays = new ConcurrentBag<ReplayMetadata>();
104 				Parallel.ForEach(Directory.GetFiles(dir, "*.orarep", SearchOption.AllDirectories), (fileName, pls) =>
105 				{
106 					if (cancelLoadingReplays)
107 					{
108 						pls.Stop();
109 						return;
110 					}
111 
112 					var replay = ReplayMetadata.Read(fileName);
113 					if (replay != null)
114 						loadedReplays.Add(replay);
115 				});
116 
117 				if (cancelLoadingReplays)
118 					return;
119 
120 				var sortedReplays = loadedReplays.OrderByDescending(replay => replay.GameInfo.StartTimeUtc).ToList();
121 				Game.RunAfterTick(() =>
122 				{
123 					replayList.RemoveChildren();
124 					foreach (var replay in sortedReplays)
125 						AddReplay(replay, template);
126 
127 					SetupReplayDependentFilters();
128 					ApplyFilter();
129 				});
130 			}
131 		}
132 
SetupFilters()133 		void SetupFilters()
134 		{
135 			// Game type
136 			{
137 				var ddb = panel.GetOrNull<DropDownButtonWidget>("FLT_GAMETYPE_DROPDOWNBUTTON");
138 				if (ddb != null)
139 				{
140 					// Using list to maintain the order
141 					var options = new List<Pair<GameType, string>>
142 					{
143 						Pair.New(GameType.Any, ddb.GetText()),
144 						Pair.New(GameType.Singleplayer, "Singleplayer"),
145 						Pair.New(GameType.Multiplayer, "Multiplayer")
146 					};
147 
148 					var lookup = options.ToDictionary(kvp => kvp.First, kvp => kvp.Second);
149 
150 					ddb.GetText = () => lookup[filter.Type];
151 					ddb.OnMouseDown = _ =>
152 					{
153 						Func<Pair<GameType, string>, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) =>
154 						{
155 							var item = ScrollItemWidget.Setup(
156 								tpl,
157 								() => filter.Type == option.First,
158 								() => { filter.Type = option.First; ApplyFilter(); });
159 							item.Get<LabelWidget>("LABEL").GetText = () => option.Second;
160 							return item;
161 						};
162 
163 						ddb.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 330, options, setupItem);
164 					};
165 				}
166 			}
167 
168 			// Date type
169 			{
170 				var ddb = panel.GetOrNull<DropDownButtonWidget>("FLT_DATE_DROPDOWNBUTTON");
171 				if (ddb != null)
172 				{
173 					// Using list to maintain the order
174 					var options = new List<Pair<DateType, string>>
175 					{
176 						Pair.New(DateType.Any, ddb.GetText()),
177 						Pair.New(DateType.Today, "Today"),
178 						Pair.New(DateType.LastWeek, "Last 7 days"),
179 						Pair.New(DateType.LastFortnight, "Last 14 days"),
180 						Pair.New(DateType.LastMonth, "Last 30 days")
181 					};
182 
183 					var lookup = options.ToDictionary(kvp => kvp.First, kvp => kvp.Second);
184 
185 					ddb.GetText = () => lookup[filter.Date];
186 					ddb.OnMouseDown = _ =>
187 					{
188 						Func<Pair<DateType, string>, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) =>
189 						{
190 							var item = ScrollItemWidget.Setup(
191 								tpl,
192 								() => filter.Date == option.First,
193 								() => { filter.Date = option.First; ApplyFilter(); });
194 
195 							item.Get<LabelWidget>("LABEL").GetText = () => option.Second;
196 							return item;
197 						};
198 
199 						ddb.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 330, options, setupItem);
200 					};
201 				}
202 			}
203 
204 			// Duration
205 			{
206 				var ddb = panel.GetOrNull<DropDownButtonWidget>("FLT_DURATION_DROPDOWNBUTTON");
207 				if (ddb != null)
208 				{
209 					// Using list to maintain the order
210 					var options = new List<Pair<DurationType, string>>
211 					{
212 						Pair.New(DurationType.Any, ddb.GetText()),
213 						Pair.New(DurationType.VeryShort, "Under 5 min"),
214 						Pair.New(DurationType.Short, "Short (10 min)"),
215 						Pair.New(DurationType.Medium, "Medium (30 min)"),
216 						Pair.New(DurationType.Long, "Long (60+ min)")
217 					};
218 
219 					var lookup = options.ToDictionary(kvp => kvp.First, kvp => kvp.Second);
220 
221 					ddb.GetText = () => lookup[filter.Duration];
222 					ddb.OnMouseDown = _ =>
223 					{
224 						Func<Pair<DurationType, string>, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) =>
225 						{
226 							var item = ScrollItemWidget.Setup(
227 								tpl,
228 								() => filter.Duration == option.First,
229 								() => { filter.Duration = option.First; ApplyFilter(); });
230 							item.Get<LabelWidget>("LABEL").GetText = () => option.Second;
231 							return item;
232 						};
233 
234 						ddb.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 330, options, setupItem);
235 					};
236 				}
237 			}
238 
239 			// Outcome (depends on Player)
240 			{
241 				var ddb = panel.GetOrNull<DropDownButtonWidget>("FLT_OUTCOME_DROPDOWNBUTTON");
242 				if (ddb != null)
243 				{
244 					ddb.IsDisabled = () => string.IsNullOrEmpty(filter.PlayerName);
245 
246 					// Using list to maintain the order
247 					var options = new List<Pair<WinState, string>>
248 					{
249 						Pair.New(WinState.Undefined, ddb.GetText()),
250 						Pair.New(WinState.Lost, "Defeat"),
251 						Pair.New(WinState.Won, "Victory")
252 					};
253 
254 					var lookup = options.ToDictionary(kvp => kvp.First, kvp => kvp.Second);
255 
256 					ddb.GetText = () => lookup[filter.Outcome];
257 					ddb.OnMouseDown = _ =>
258 					{
259 						Func<Pair<WinState, string>, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) =>
260 						{
261 							var item = ScrollItemWidget.Setup(
262 								tpl,
263 								() => filter.Outcome == option.First,
264 								() => { filter.Outcome = option.First; ApplyFilter(); });
265 							item.Get<LabelWidget>("LABEL").GetText = () => option.Second;
266 							return item;
267 						};
268 
269 						ddb.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 330, options, setupItem);
270 					};
271 				}
272 			}
273 
274 			// Reset button
275 			{
276 				var button = panel.Get<ButtonWidget>("FLT_RESET_BUTTON");
277 				button.IsDisabled = () => filter.IsEmpty;
278 				button.OnClick = () => { filter = new Filter(); ApplyFilter(); };
279 			}
280 		}
281 
SetupReplayDependentFilters()282 		void SetupReplayDependentFilters()
283 		{
284 			// Map
285 			{
286 				var ddb = panel.GetOrNull<DropDownButtonWidget>("FLT_MAPNAME_DROPDOWNBUTTON");
287 				if (ddb != null)
288 				{
289 					var options = replays.Select(r => r.GameInfo.MapTitle).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
290 					options.Sort(StringComparer.OrdinalIgnoreCase);
291 					options.Insert(0, null);	// no filter
292 
293 					var anyText = ddb.GetText();
294 					ddb.GetText = () => string.IsNullOrEmpty(filter.MapName) ? anyText : filter.MapName;
295 					ddb.OnMouseDown = _ =>
296 					{
297 						Func<string, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) =>
298 						{
299 							var item = ScrollItemWidget.Setup(
300 								tpl,
301 								() => string.Compare(filter.MapName, option, true) == 0,
302 								() => { filter.MapName = option; ApplyFilter(); });
303 							item.Get<LabelWidget>("LABEL").GetText = () => option ?? anyText;
304 							return item;
305 						};
306 
307 						ddb.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 330, options, setupItem);
308 					};
309 				}
310 			}
311 
312 			// Players
313 			{
314 				var ddb = panel.GetOrNull<DropDownButtonWidget>("FLT_PLAYER_DROPDOWNBUTTON");
315 				if (ddb != null)
316 				{
317 					var options = replays.SelectMany(r => r.GameInfo.Players.Select(p => p.Name)).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
318 					options.Sort(StringComparer.OrdinalIgnoreCase);
319 					options.Insert(0, null);	// no filter
320 
321 					var anyText = ddb.GetText();
322 					ddb.GetText = () => string.IsNullOrEmpty(filter.PlayerName) ? anyText : filter.PlayerName;
323 					ddb.OnMouseDown = _ =>
324 					{
325 						Func<string, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) =>
326 						{
327 							var item = ScrollItemWidget.Setup(
328 								tpl,
329 								() => string.Compare(filter.PlayerName, option, true) == 0,
330 								() => { filter.PlayerName = option; ApplyFilter(); });
331 							item.Get<LabelWidget>("LABEL").GetText = () => option ?? anyText;
332 							return item;
333 						};
334 
335 						ddb.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 330, options, setupItem);
336 					};
337 				}
338 			}
339 
340 			// Faction (depends on Player)
341 			{
342 				var ddb = panel.GetOrNull<DropDownButtonWidget>("FLT_FACTION_DROPDOWNBUTTON");
343 				if (ddb != null)
344 				{
345 					ddb.IsDisabled = () => string.IsNullOrEmpty(filter.PlayerName);
346 
347 					var options = replays
348 						.SelectMany(r => r.GameInfo.Players.Select(p => p.FactionName).Where(n => !string.IsNullOrEmpty(n)))
349 						.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
350 					options.Sort(StringComparer.OrdinalIgnoreCase);
351 					options.Insert(0, null);	// no filter
352 
353 					var anyText = ddb.GetText();
354 					ddb.GetText = () => string.IsNullOrEmpty(filter.Faction) ? anyText : filter.Faction;
355 					ddb.OnMouseDown = _ =>
356 					{
357 						Func<string, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) =>
358 						{
359 							var item = ScrollItemWidget.Setup(
360 								tpl,
361 								() => string.Compare(filter.Faction, option, true) == 0,
362 								() => { filter.Faction = option; ApplyFilter(); });
363 							item.Get<LabelWidget>("LABEL").GetText = () => option ?? anyText;
364 							return item;
365 						};
366 
367 						ddb.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 330, options, setupItem);
368 					};
369 				}
370 			}
371 		}
372 
SetupManagement()373 		void SetupManagement()
374 		{
375 			var renameButton = panel.Get<ButtonWidget>("MNG_RENSEL_BUTTON");
376 			renameButton.IsDisabled = () => selectedReplay == null;
377 			renameButton.OnClick = () =>
378 			{
379 				var r = selectedReplay;
380 				var initialName = Path.GetFileNameWithoutExtension(r.FilePath);
381 				var directoryName = Path.GetDirectoryName(r.FilePath);
382 				var invalidChars = Path.GetInvalidFileNameChars();
383 
384 				ConfirmationDialogs.TextInputPrompt(
385 					"Rename Replay",
386 					"Enter a new file name:",
387 					initialName,
388 					onAccept: newName => RenameReplay(r, newName),
389 					onCancel: null,
390 					acceptText: "Rename",
391 					cancelText: null,
392 					inputValidator: newName =>
393 					{
394 						if (newName == initialName)
395 							return false;
396 
397 						if (string.IsNullOrWhiteSpace(newName))
398 							return false;
399 
400 						if (newName.IndexOfAny(invalidChars) >= 0)
401 							return false;
402 
403 						if (File.Exists(Path.Combine(directoryName, newName)))
404 							return false;
405 
406 						return true;
407 					});
408 			};
409 
410 			Action<ReplayMetadata, Action> onDeleteReplay = (r, after) =>
411 			{
412 				ConfirmationDialogs.ButtonPrompt(
413 					title: "Delete selected replay?",
414 					text: "Delete replay '{0}'?".F(Path.GetFileNameWithoutExtension(r.FilePath)),
415 					onConfirm: () =>
416 					{
417 						DeleteReplay(r);
418 						if (after != null)
419 							after.Invoke();
420 					},
421 					confirmText: "Delete",
422 					onCancel: () => { });
423 			};
424 
425 			var deleteButton = panel.Get<ButtonWidget>("MNG_DELSEL_BUTTON");
426 			deleteButton.IsDisabled = () => selectedReplay == null;
427 			deleteButton.OnClick = () =>
428 			{
429 				onDeleteReplay(selectedReplay, () =>
430 				{
431 					if (selectedReplay == null)
432 						SelectFirstVisibleReplay();
433 				});
434 			};
435 
436 			var deleteAllButton = panel.Get<ButtonWidget>("MNG_DELALL_BUTTON");
437 			deleteAllButton.IsDisabled = () => replayState.Count(kvp => kvp.Value.Visible) == 0;
438 			deleteAllButton.OnClick = () =>
439 			{
440 				var list = replayState.Where(kvp => kvp.Value.Visible).Select(kvp => kvp.Key).ToList();
441 				if (list.Count == 0)
442 					return;
443 
444 				if (list.Count == 1)
445 				{
446 					onDeleteReplay(list[0], () => { if (selectedReplay == null) SelectFirstVisibleReplay(); });
447 					return;
448 				}
449 
450 				ConfirmationDialogs.ButtonPrompt(
451 					title: "Delete all selected replays?",
452 					text: "Delete {0} replays?".F(list.Count),
453 					onConfirm: () =>
454 					{
455 						list.ForEach(DeleteReplay);
456 						if (selectedReplay == null)
457 							SelectFirstVisibleReplay();
458 					},
459 					confirmText: "Delete All",
460 					onCancel: () => { });
461 			};
462 		}
463 
RenameReplay(ReplayMetadata replay, string newFilenameWithoutExtension)464 		void RenameReplay(ReplayMetadata replay, string newFilenameWithoutExtension)
465 		{
466 			try
467 			{
468 				var item = replayState[replay].Item;
469 				replay.RenameFile(newFilenameWithoutExtension);
470 				item.Text = newFilenameWithoutExtension;
471 
472 				var label = item.Get<LabelWithTooltipWidget>("TITLE");
473 				WidgetUtils.TruncateLabelToTooltip(label, item.Text);
474 			}
475 			catch (Exception ex)
476 			{
477 				Log.Write("debug", ex.ToString());
478 				return;
479 			}
480 		}
481 
DeleteReplay(ReplayMetadata replay)482 		void DeleteReplay(ReplayMetadata replay)
483 		{
484 			try
485 			{
486 				File.Delete(replay.FilePath);
487 			}
488 			catch (Exception ex)
489 			{
490 				Game.Debug("Failed to delete replay file '{0}'. See the logs for details.", replay.FilePath);
491 				Log.Write("debug", ex.ToString());
492 				return;
493 			}
494 
495 			if (replay == selectedReplay)
496 				SelectReplay(null);
497 
498 			replayList.RemoveChild(replayState[replay].Item);
499 			replays.Remove(replay);
500 			replayState.Remove(replay);
501 		}
502 
EvaluateReplayVisibility(ReplayMetadata replay)503 		bool EvaluateReplayVisibility(ReplayMetadata replay)
504 		{
505 			// Game type
506 			if ((filter.Type == GameType.Multiplayer && replay.GameInfo.IsSinglePlayer) || (filter.Type == GameType.Singleplayer && !replay.GameInfo.IsSinglePlayer))
507 				return false;
508 
509 			// Date type
510 			if (filter.Date != DateType.Any)
511 			{
512 				TimeSpan t;
513 				switch (filter.Date)
514 				{
515 					case DateType.Today:
516 						t = TimeSpan.FromDays(1d);
517 						break;
518 
519 					case DateType.LastWeek:
520 						t = TimeSpan.FromDays(7d);
521 						break;
522 
523 					case DateType.LastFortnight:
524 						t = TimeSpan.FromDays(14d);
525 						break;
526 
527 					case DateType.LastMonth:
528 					default:
529 						t = TimeSpan.FromDays(30d);
530 						break;
531 				}
532 
533 				if (replay.GameInfo.StartTimeUtc < DateTime.UtcNow - t)
534 					return false;
535 			}
536 
537 			// Duration
538 			if (filter.Duration != DurationType.Any)
539 			{
540 				var minutes = replay.GameInfo.Duration.TotalMinutes;
541 				switch (filter.Duration)
542 				{
543 					case DurationType.VeryShort:
544 						if (minutes >= 5)
545 							return false;
546 						break;
547 
548 					case DurationType.Short:
549 						if (minutes < 5 || minutes >= 20)
550 							return false;
551 						break;
552 
553 					case DurationType.Medium:
554 						if (minutes < 20 || minutes >= 60)
555 							return false;
556 						break;
557 
558 					case DurationType.Long:
559 						if (minutes < 60)
560 							return false;
561 						break;
562 				}
563 			}
564 
565 			// Map
566 			if (!string.IsNullOrEmpty(filter.MapName) && string.Compare(filter.MapName, replay.GameInfo.MapTitle, true) != 0)
567 				return false;
568 
569 			// Player
570 			if (!string.IsNullOrEmpty(filter.PlayerName))
571 			{
572 				var player = replay.GameInfo.Players.FirstOrDefault(p => string.Compare(filter.PlayerName, p.Name, true) == 0);
573 				if (player == null)
574 					return false;
575 
576 				// Outcome
577 				if (filter.Outcome != WinState.Undefined && filter.Outcome != player.Outcome)
578 					return false;
579 
580 				// Faction
581 				if (!string.IsNullOrEmpty(filter.Faction) && string.Compare(filter.Faction, player.FactionName, true) != 0)
582 					return false;
583 			}
584 
585 			return true;
586 		}
587 
ApplyFilter()588 		void ApplyFilter()
589 		{
590 			foreach (var replay in replays)
591 				replayState[replay].Visible = EvaluateReplayVisibility(replay);
592 
593 			if (selectedReplay == null || replayState[selectedReplay].Visible == false)
594 				SelectFirstVisibleReplay();
595 
596 			replayList.Layout.AdjustChildren();
597 			replayList.ScrollToSelectedItem();
598 		}
599 
SelectFirstVisibleReplay()600 		void SelectFirstVisibleReplay()
601 		{
602 			SelectReplay(replays.FirstOrDefault(r => replayState[r].Visible));
603 		}
604 
SelectReplay(ReplayMetadata replay)605 		void SelectReplay(ReplayMetadata replay)
606 		{
607 			selectedReplay = replay;
608 			map = selectedReplay != null ? selectedReplay.GameInfo.MapPreview : MapCache.UnknownMap;
609 
610 			if (replay == null)
611 				return;
612 
613 			try
614 			{
615 				if (map.Status != MapStatus.Available)
616 				{
617 					if (map.Status == MapStatus.DownloadAvailable)
618 						LoadMapPreviewRules(map);
619 					else if (Game.Settings.Game.AllowDownloading)
620 						modData.MapCache.QueryRemoteMapDetails(services.MapRepository, new[] { map.Uid }, LoadMapPreviewRules);
621 				}
622 
623 				var players = replay.GameInfo.Players
624 					.GroupBy(p => p.Team)
625 					.OrderBy(g => g.Key);
626 
627 				var teams = new Dictionary<string, IEnumerable<GameInformation.Player>>();
628 				var noTeams = players.Count() == 1;
629 				foreach (var p in players)
630 				{
631 					var label = noTeams ? "Players" : p.Key == 0 ? "No Team" : "Team {0}".F(p.Key);
632 					teams.Add(label, p);
633 				}
634 
635 				playerList.RemoveChildren();
636 
637 				foreach (var kv in teams)
638 				{
639 					var group = kv.Key;
640 					if (group.Length > 0)
641 					{
642 						var header = ScrollItemWidget.Setup(playerHeader, () => true, () => { });
643 						header.Get<LabelWidget>("LABEL").GetText = () => group;
644 						playerList.AddChild(header);
645 					}
646 
647 					foreach (var option in kv.Value)
648 					{
649 						var o = option;
650 
651 						var color = o.Color;
652 
653 						var item = ScrollItemWidget.Setup(playerTemplate, () => false, () => { });
654 
655 						var label = item.Get<LabelWidget>("LABEL");
656 						var font = Game.Renderer.Fonts[label.Font];
657 						var name = WidgetUtils.TruncateText(o.Name, label.Bounds.Width, font);
658 						label.GetText = () => name;
659 						label.GetColor = () => color;
660 
661 						var flag = item.Get<ImageWidget>("FLAG");
662 						flag.GetImageCollection = () => "flags";
663 						var factionInfo = modData.DefaultRules.Actors["world"].TraitInfos<FactionInfo>();
664 						flag.GetImageName = () => (factionInfo != null && factionInfo.Any(f => f.InternalName == o.FactionId)) ? o.FactionId : "Random";
665 
666 						playerList.AddChild(item);
667 					}
668 				}
669 			}
670 			catch (Exception e)
671 			{
672 				Log.Write("debug", "Exception while parsing replay: {0}", e);
673 				SelectReplay(null);
674 			}
675 		}
676 
LoadMapPreviewRules(MapPreview map)677 		void LoadMapPreviewRules(MapPreview map)
678 		{
679 			new Task(() =>
680 			{
681 				// Force map rules to be loaded on this background thread
682 				map.PreloadRules();
683 			}).Start();
684 		}
685 
WatchReplay()686 		void WatchReplay()
687 		{
688 			if (selectedReplay != null && ReplayUtils.PromptConfirmReplayCompatibility(selectedReplay))
689 			{
690 				cancelLoadingReplays = true;
691 				Game.JoinReplay(selectedReplay.FilePath);
692 			}
693 		}
694 
AddReplay(ReplayMetadata replay, ScrollItemWidget template)695 		void AddReplay(ReplayMetadata replay, ScrollItemWidget template)
696 		{
697 			replays.Add(replay);
698 
699 			var item = ScrollItemWidget.Setup(template,
700 				() => selectedReplay == replay,
701 				() => SelectReplay(replay),
702 				() => WatchReplay());
703 
704 			replayState[replay] = new ReplayState
705 			{
706 				Item = item,
707 				Visible = true
708 			};
709 
710 			item.Text = Path.GetFileNameWithoutExtension(replay.FilePath);
711 			var label = item.Get<LabelWithTooltipWidget>("TITLE");
712 			WidgetUtils.TruncateLabelToTooltip(label, item.Text);
713 
714 			item.IsVisible = () => replayState[replay].Visible;
715 			replayList.AddChild(item);
716 		}
717 
OnGameStart()718 		void OnGameStart()
719 		{
720 			Ui.CloseWindow();
721 			onStart();
722 		}
723 
724 		bool disposed;
Dispose(bool disposing)725 		protected override void Dispose(bool disposing)
726 		{
727 			if (disposing && !disposed)
728 			{
729 				disposed = true;
730 				Game.BeforeGameStart -= OnGameStart;
731 			}
732 
733 			base.Dispose(disposing);
734 		}
735 
736 		class ReplayState
737 		{
738 			public bool Visible;
739 			public ScrollItemWidget Item;
740 		}
741 
742 		class Filter
743 		{
744 			public GameType Type;
745 			public DateType Date;
746 			public DurationType Duration;
747 			public WinState Outcome;
748 			public string PlayerName;
749 			public string MapName;
750 			public string Faction;
751 
752 			public bool IsEmpty
753 			{
754 				get
755 				{
756 					return Type == default(GameType)
757 						&& Date == default(DateType)
758 						&& Duration == default(DurationType)
759 						&& Outcome == default(WinState)
760 						&& string.IsNullOrEmpty(PlayerName)
761 						&& string.IsNullOrEmpty(MapName)
762 						&& string.IsNullOrEmpty(Faction);
763 				}
764 			}
765 		}
766 
767 		enum GameType
768 		{
769 			Any,
770 			Singleplayer,
771 			Multiplayer
772 		}
773 
774 		enum DateType
775 		{
776 			Any,
777 			Today,
778 			LastWeek,
779 			LastFortnight,
780 			LastMonth
781 		}
782 
783 		enum DurationType
784 		{
785 			Any,
786 			VeryShort,
787 			Short,
788 			Medium,
789 			Long
790 		}
791 	}
792 }
793