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.Generic; 14 using System.ComponentModel; 15 using System.Diagnostics.CodeAnalysis; 16 using System.Globalization; 17 using System.IO; 18 using System.Linq; 19 using System.Net; 20 using OpenRA.Primitives; 21 using OpenRA.Widgets; 22 23 namespace OpenRA.Mods.Common.Widgets.Logic 24 { 25 public class MainMenuLogic : ChromeLogic 26 { 27 protected enum MenuType { Main, Singleplayer, Extras, MapEditor, StartupPrompts, None } 28 29 protected enum MenuPanel { None, Missions, Skirmish, Multiplayer, MapEditor, Replays, GameSaves } 30 31 protected MenuType menuType = MenuType.Main; 32 readonly Widget rootMenu; 33 readonly ScrollPanelWidget newsPanel; 34 readonly Widget newsTemplate; 35 readonly LabelWidget newsStatus; 36 37 // Update news once per game launch 38 static bool fetchedNews; 39 40 protected static MenuPanel lastGameState = MenuPanel.None; 41 42 bool newsOpen; 43 SwitchMenu(MenuType type)44 void SwitchMenu(MenuType type) 45 { 46 menuType = type; 47 48 // Update button mouseover 49 Game.RunAfterTick(Ui.ResetTooltips); 50 } 51 52 [ObjectCreator.UseCtor] MainMenuLogic(Widget widget, World world, ModData modData)53 public MainMenuLogic(Widget widget, World world, ModData modData) 54 { 55 rootMenu = widget; 56 rootMenu.Get<LabelWidget>("VERSION_LABEL").Text = modData.Manifest.Metadata.Version; 57 58 // Menu buttons 59 var mainMenu = widget.Get("MAIN_MENU"); 60 mainMenu.IsVisible = () => menuType == MenuType.Main; 61 62 mainMenu.Get<ButtonWidget>("SINGLEPLAYER_BUTTON").OnClick = () => SwitchMenu(MenuType.Singleplayer); 63 64 mainMenu.Get<ButtonWidget>("MULTIPLAYER_BUTTON").OnClick = OpenMultiplayerPanel; 65 66 mainMenu.Get<ButtonWidget>("CONTENT_BUTTON").OnClick = () => 67 { 68 // Switching mods changes the world state (by disposing it), 69 // so we can't do this inside the input handler. 70 Game.RunAfterTick(() => 71 { 72 var content = modData.Manifest.Get<ModContent>(); 73 Game.InitializeMod(content.ContentInstallerMod, new Arguments(new[] { "Content.Mod=" + modData.Manifest.Id })); 74 }); 75 }; 76 77 mainMenu.Get<ButtonWidget>("SETTINGS_BUTTON").OnClick = () => 78 { 79 SwitchMenu(MenuType.None); 80 Game.OpenWindow("SETTINGS_PANEL", new WidgetArgs 81 { 82 { "onExit", () => SwitchMenu(MenuType.Main) } 83 }); 84 }; 85 86 mainMenu.Get<ButtonWidget>("EXTRAS_BUTTON").OnClick = () => SwitchMenu(MenuType.Extras); 87 88 mainMenu.Get<ButtonWidget>("QUIT_BUTTON").OnClick = Game.Exit; 89 90 // Singleplayer menu 91 var singleplayerMenu = widget.Get("SINGLEPLAYER_MENU"); 92 singleplayerMenu.IsVisible = () => menuType == MenuType.Singleplayer; 93 94 var missionsButton = singleplayerMenu.Get<ButtonWidget>("MISSIONS_BUTTON"); 95 missionsButton.OnClick = OpenMissionBrowserPanel; 96 97 var hasCampaign = modData.Manifest.Missions.Any(); 98 var hasMissions = modData.MapCache 99 .Any(p => p.Status == MapStatus.Available && p.Visibility.HasFlag(MapVisibility.MissionSelector)); 100 101 missionsButton.Disabled = !hasCampaign && !hasMissions; 102 103 var hasMaps = modData.MapCache.Any(p => p.Visibility.HasFlag(MapVisibility.Lobby)); 104 var skirmishButton = singleplayerMenu.Get<ButtonWidget>("SKIRMISH_BUTTON"); 105 skirmishButton.OnClick = StartSkirmishGame; 106 skirmishButton.Disabled = !hasMaps; 107 108 var loadButton = singleplayerMenu.Get<ButtonWidget>("LOAD_BUTTON"); 109 loadButton.IsDisabled = () => !GameSaveBrowserLogic.IsLoadPanelEnabled(modData.Manifest); 110 loadButton.OnClick = OpenGameSaveBrowserPanel; 111 112 singleplayerMenu.Get<ButtonWidget>("BACK_BUTTON").OnClick = () => SwitchMenu(MenuType.Main); 113 114 // Extras menu 115 var extrasMenu = widget.Get("EXTRAS_MENU"); 116 extrasMenu.IsVisible = () => menuType == MenuType.Extras; 117 118 extrasMenu.Get<ButtonWidget>("REPLAYS_BUTTON").OnClick = OpenReplayBrowserPanel; 119 120 extrasMenu.Get<ButtonWidget>("MUSIC_BUTTON").OnClick = () => 121 { 122 SwitchMenu(MenuType.None); 123 Ui.OpenWindow("MUSIC_PANEL", new WidgetArgs 124 { 125 { "onExit", () => SwitchMenu(MenuType.Extras) }, 126 { "world", world } 127 }); 128 }; 129 130 extrasMenu.Get<ButtonWidget>("MAP_EDITOR_BUTTON").OnClick = () => SwitchMenu(MenuType.MapEditor); 131 132 var assetBrowserButton = extrasMenu.GetOrNull<ButtonWidget>("ASSETBROWSER_BUTTON"); 133 if (assetBrowserButton != null) 134 assetBrowserButton.OnClick = () => 135 { 136 SwitchMenu(MenuType.None); 137 Game.OpenWindow("ASSETBROWSER_PANEL", new WidgetArgs 138 { 139 { "onExit", () => SwitchMenu(MenuType.Extras) }, 140 }); 141 }; 142 143 extrasMenu.Get<ButtonWidget>("CREDITS_BUTTON").OnClick = () => 144 { 145 SwitchMenu(MenuType.None); 146 Ui.OpenWindow("CREDITS_PANEL", new WidgetArgs 147 { 148 { "onExit", () => SwitchMenu(MenuType.Extras) }, 149 }); 150 }; 151 152 extrasMenu.Get<ButtonWidget>("BACK_BUTTON").OnClick = () => SwitchMenu(MenuType.Main); 153 154 // Map editor menu 155 var mapEditorMenu = widget.Get("MAP_EDITOR_MENU"); 156 mapEditorMenu.IsVisible = () => menuType == MenuType.MapEditor; 157 158 // Loading into the map editor 159 Game.BeforeGameStart += RemoveShellmapUI; 160 161 var onSelect = new Action<string>(uid => LoadMapIntoEditor(modData.MapCache[uid].Uid)); 162 163 var newMapButton = widget.Get<ButtonWidget>("NEW_MAP_BUTTON"); 164 newMapButton.OnClick = () => 165 { 166 SwitchMenu(MenuType.None); 167 Game.OpenWindow("NEW_MAP_BG", new WidgetArgs() 168 { 169 { "onSelect", onSelect }, 170 { "onExit", () => SwitchMenu(MenuType.MapEditor) } 171 }); 172 }; 173 174 var loadMapButton = widget.Get<ButtonWidget>("LOAD_MAP_BUTTON"); 175 loadMapButton.OnClick = () => 176 { 177 SwitchMenu(MenuType.None); 178 Game.OpenWindow("MAPCHOOSER_PANEL", new WidgetArgs() 179 { 180 { "initialMap", null }, 181 { "initialTab", MapClassification.User }, 182 { "onExit", () => SwitchMenu(MenuType.MapEditor) }, 183 { "onSelect", onSelect }, 184 { "filter", MapVisibility.Lobby | MapVisibility.Shellmap | MapVisibility.MissionSelector }, 185 }); 186 }; 187 188 loadMapButton.Disabled = !hasMaps; 189 190 mapEditorMenu.Get<ButtonWidget>("BACK_BUTTON").OnClick = () => SwitchMenu(MenuType.Extras); 191 192 var newsBG = widget.GetOrNull("NEWS_BG"); 193 if (newsBG != null) 194 { 195 newsBG.IsVisible = () => Game.Settings.Game.FetchNews && menuType != MenuType.None && menuType != MenuType.StartupPrompts; 196 197 newsPanel = Ui.LoadWidget<ScrollPanelWidget>("NEWS_PANEL", null, new WidgetArgs()); 198 newsTemplate = newsPanel.Get("NEWS_ITEM_TEMPLATE"); 199 newsPanel.RemoveChild(newsTemplate); 200 201 newsStatus = newsPanel.Get<LabelWidget>("NEWS_STATUS"); 202 SetNewsStatus("Loading news"); 203 } 204 205 Game.OnRemoteDirectConnect += OnRemoteDirectConnect; 206 207 // Check for updates in the background 208 var webServices = modData.Manifest.Get<WebServices>(); 209 if (Game.Settings.Debug.CheckVersion) 210 webServices.CheckModVersion(); 211 212 var updateLabel = rootMenu.GetOrNull("UPDATE_NOTICE"); 213 if (updateLabel != null) 214 updateLabel.IsVisible = () => !newsOpen && menuType != MenuType.None && 215 menuType != MenuType.StartupPrompts && 216 webServices.ModVersionStatus == ModVersionStatus.Outdated; 217 218 var playerProfile = widget.GetOrNull("PLAYER_PROFILE_CONTAINER"); 219 if (playerProfile != null) 220 { 221 Func<bool> minimalProfile = () => Ui.CurrentWindow() != null; 222 Game.LoadWidget(world, "LOCAL_PROFILE_PANEL", playerProfile, new WidgetArgs() 223 { 224 { "minimalProfile", minimalProfile } 225 }); 226 } 227 228 menuType = MenuType.StartupPrompts; 229 230 Action onIntroductionComplete = () => 231 { 232 Action onSysInfoComplete = () => 233 { 234 LoadAndDisplayNews(webServices.GameNews, newsBG); 235 SwitchMenu(MenuType.Main); 236 }; 237 238 if (SystemInfoPromptLogic.ShouldShowPrompt()) 239 { 240 Ui.OpenWindow("MAINMENU_SYSTEM_INFO_PROMPT", new WidgetArgs 241 { 242 { "onComplete", onSysInfoComplete } 243 }); 244 } 245 else 246 onSysInfoComplete(); 247 }; 248 249 if (IntroductionPromptLogic.ShouldShowPrompt()) 250 { 251 Game.OpenWindow("MAINMENU_INTRODUCTION_PROMPT", new WidgetArgs 252 { 253 { "onComplete", onIntroductionComplete } 254 }); 255 } 256 else 257 onIntroductionComplete(); 258 259 Game.OnShellmapLoaded += OpenMenuBasedOnLastGame; 260 } 261 LoadAndDisplayNews(string newsURL, Widget newsBG)262 void LoadAndDisplayNews(string newsURL, Widget newsBG) 263 { 264 if (newsBG != null && Game.Settings.Game.FetchNews) 265 { 266 var cacheFile = Platform.ResolvePath(Platform.SupportDirPrefix, "news.yaml"); 267 var currentNews = ParseNews(cacheFile); 268 if (currentNews != null) 269 DisplayNews(currentNews); 270 271 var newsButton = newsBG.GetOrNull<DropDownButtonWidget>("NEWS_BUTTON"); 272 if (newsButton != null) 273 { 274 if (!fetchedNews) 275 { 276 // Send the mod and engine version to support version-filtered news (update prompts) 277 newsURL += "?version={0}&mod={1}&modversion={2}".F( 278 Uri.EscapeUriString(Game.EngineVersion), 279 Uri.EscapeUriString(Game.ModData.Manifest.Id), 280 Uri.EscapeUriString(Game.ModData.Manifest.Metadata.Version)); 281 282 // Parameter string is blank if the player has opted out 283 newsURL += SystemInfoPromptLogic.CreateParameterString(); 284 285 new Download(newsURL, cacheFile, e => { }, 286 e => NewsDownloadComplete(e, cacheFile, currentNews, 287 () => OpenNewsPanel(newsButton))); 288 } 289 290 newsButton.OnClick = () => OpenNewsPanel(newsButton); 291 } 292 } 293 } 294 OpenNewsPanel(DropDownButtonWidget button)295 void OpenNewsPanel(DropDownButtonWidget button) 296 { 297 newsOpen = true; 298 button.AttachPanel(newsPanel, () => newsOpen = false); 299 } 300 OnRemoteDirectConnect(string host, int port)301 void OnRemoteDirectConnect(string host, int port) 302 { 303 SwitchMenu(MenuType.None); 304 Ui.OpenWindow("MULTIPLAYER_PANEL", new WidgetArgs 305 { 306 { "onStart", RemoveShellmapUI }, 307 { "onExit", () => SwitchMenu(MenuType.Main) }, 308 { "directConnectHost", host }, 309 { "directConnectPort", port }, 310 }); 311 } 312 LoadMapIntoEditor(string uid)313 void LoadMapIntoEditor(string uid) 314 { 315 ConnectionLogic.Connect(IPAddress.Loopback.ToString(), 316 Game.CreateLocalServer(uid), 317 "", 318 () => { Game.LoadEditor(uid); }, 319 () => { Game.CloseServer(); SwitchMenu(MenuType.MapEditor); }); 320 321 lastGameState = MenuPanel.MapEditor; 322 } 323 SetNewsStatus(string message)324 void SetNewsStatus(string message) 325 { 326 message = WidgetUtils.WrapText(message, newsStatus.Bounds.Width, Game.Renderer.Fonts[newsStatus.Font]); 327 newsStatus.GetText = () => message; 328 } 329 330 class NewsItem 331 { 332 public string Title; 333 public string Author; 334 public DateTime DateTime; 335 public string Content; 336 } 337 ParseNews(string path)338 NewsItem[] ParseNews(string path) 339 { 340 if (!File.Exists(path)) 341 return null; 342 343 try 344 { 345 return MiniYaml.FromFile(path).Select(node => 346 { 347 var nodesDict = node.Value.ToDictionary(); 348 return new NewsItem 349 { 350 Title = nodesDict["Title"].Value, 351 Author = nodesDict["Author"].Value, 352 DateTime = FieldLoader.GetValue<DateTime>("DateTime", node.Key), 353 Content = nodesDict["Content"].Value 354 }; 355 }).ToArray(); 356 } 357 catch (Exception ex) 358 { 359 SetNewsStatus("Failed to parse news: {0}".F(ex.Message)); 360 } 361 362 return null; 363 } 364 NewsDownloadComplete(AsyncCompletedEventArgs e, string cacheFile, NewsItem[] oldNews, Action onNewsDownloaded)365 void NewsDownloadComplete(AsyncCompletedEventArgs e, string cacheFile, NewsItem[] oldNews, Action onNewsDownloaded) 366 { 367 Game.RunAfterTick(() => // run on the main thread 368 { 369 if (e.Error != null) 370 { 371 SetNewsStatus("Failed to retrieve news: {0}".F(Download.FormatErrorMessage(e.Error))); 372 return; 373 } 374 375 fetchedNews = true; 376 var newNews = ParseNews(cacheFile); 377 if (newNews == null) 378 return; 379 380 DisplayNews(newNews); 381 382 if (oldNews == null || newNews.Any(n => !oldNews.Select(c => c.DateTime).Contains(n.DateTime))) 383 onNewsDownloaded(); 384 }); 385 } 386 DisplayNews(IEnumerable<NewsItem> newsItems)387 void DisplayNews(IEnumerable<NewsItem> newsItems) 388 { 389 newsPanel.RemoveChildren(); 390 SetNewsStatus(""); 391 392 foreach (var i in newsItems) 393 { 394 var item = i; 395 396 var newsItem = newsTemplate.Clone(); 397 398 var titleLabel = newsItem.Get<LabelWidget>("TITLE"); 399 titleLabel.GetText = () => item.Title; 400 401 var authorDateTimeLabel = newsItem.Get<LabelWidget>("AUTHOR_DATETIME"); 402 var authorDateTime = authorDateTimeLabel.Text.F(item.Author, item.DateTime.ToLocalTime()); 403 authorDateTimeLabel.GetText = () => authorDateTime; 404 405 var contentLabel = newsItem.Get<LabelWidget>("CONTENT"); 406 var content = item.Content.Replace("\\n", "\n"); 407 content = WidgetUtils.WrapText(content, contentLabel.Bounds.Width, Game.Renderer.Fonts[contentLabel.Font]); 408 contentLabel.GetText = () => content; 409 contentLabel.Bounds.Height = Game.Renderer.Fonts[contentLabel.Font].Measure(content).Y; 410 newsItem.Bounds.Height += contentLabel.Bounds.Height; 411 412 newsPanel.AddChild(newsItem); 413 newsPanel.Layout.AdjustChildren(); 414 } 415 } 416 RemoveShellmapUI()417 void RemoveShellmapUI() 418 { 419 rootMenu.Parent.RemoveChild(rootMenu); 420 } 421 StartSkirmishGame()422 void StartSkirmishGame() 423 { 424 var map = Game.ModData.MapCache.ChooseInitialMap(Game.Settings.Server.Map, Game.CosmeticRandom); 425 Game.Settings.Server.Map = map; 426 Game.Settings.Save(); 427 428 ConnectionLogic.Connect(IPAddress.Loopback.ToString(), 429 Game.CreateLocalServer(map), 430 "", 431 OpenSkirmishLobbyPanel, 432 () => { Game.CloseServer(); SwitchMenu(MenuType.Main); }); 433 } 434 OpenMissionBrowserPanel()435 void OpenMissionBrowserPanel() 436 { 437 SwitchMenu(MenuType.None); 438 Game.OpenWindow("MISSIONBROWSER_PANEL", new WidgetArgs 439 { 440 { "onExit", () => SwitchMenu(MenuType.Singleplayer) }, 441 { "onStart", () => { RemoveShellmapUI(); lastGameState = MenuPanel.Missions; } } 442 }); 443 } 444 OpenSkirmishLobbyPanel()445 void OpenSkirmishLobbyPanel() 446 { 447 SwitchMenu(MenuType.None); 448 Game.OpenWindow("SERVER_LOBBY", new WidgetArgs 449 { 450 { "onExit", () => { Game.Disconnect(); SwitchMenu(MenuType.Singleplayer); } }, 451 { "onStart", () => { RemoveShellmapUI(); lastGameState = MenuPanel.Skirmish; } }, 452 { "skirmishMode", true } 453 }); 454 } 455 OpenMultiplayerPanel()456 void OpenMultiplayerPanel() 457 { 458 SwitchMenu(MenuType.None); 459 Ui.OpenWindow("MULTIPLAYER_PANEL", new WidgetArgs 460 { 461 { "onStart", () => { RemoveShellmapUI(); lastGameState = MenuPanel.Multiplayer; } }, 462 { "onExit", () => SwitchMenu(MenuType.Main) }, 463 { "directConnectHost", null }, 464 { "directConnectPort", 0 }, 465 }); 466 } 467 OpenReplayBrowserPanel()468 void OpenReplayBrowserPanel() 469 { 470 SwitchMenu(MenuType.None); 471 Ui.OpenWindow("REPLAYBROWSER_PANEL", new WidgetArgs 472 { 473 { "onExit", () => SwitchMenu(MenuType.Extras) }, 474 { "onStart", () => { RemoveShellmapUI(); lastGameState = MenuPanel.Replays; } } 475 }); 476 } 477 OpenGameSaveBrowserPanel()478 void OpenGameSaveBrowserPanel() 479 { 480 SwitchMenu(MenuType.None); 481 Ui.OpenWindow("GAMESAVE_BROWSER_PANEL", new WidgetArgs 482 { 483 { "onExit", () => SwitchMenu(MenuType.Singleplayer) }, 484 { "onStart", () => { RemoveShellmapUI(); lastGameState = MenuPanel.GameSaves; } }, 485 { "isSavePanel", false }, 486 { "world", null } 487 }); 488 } 489 Dispose(bool disposing)490 protected override void Dispose(bool disposing) 491 { 492 if (disposing) 493 { 494 Game.OnRemoteDirectConnect -= OnRemoteDirectConnect; 495 Game.BeforeGameStart -= RemoveShellmapUI; 496 } 497 498 Game.OnShellmapLoaded -= OpenMenuBasedOnLastGame; 499 base.Dispose(disposing); 500 } 501 OpenMenuBasedOnLastGame()502 void OpenMenuBasedOnLastGame() 503 { 504 switch (lastGameState) 505 { 506 case MenuPanel.Missions: 507 OpenMissionBrowserPanel(); 508 break; 509 510 case MenuPanel.Replays: 511 OpenReplayBrowserPanel(); 512 break; 513 514 case MenuPanel.Skirmish: 515 StartSkirmishGame(); 516 break; 517 518 case MenuPanel.Multiplayer: 519 OpenMultiplayerPanel(); 520 break; 521 522 case MenuPanel.MapEditor: 523 SwitchMenu(MenuType.MapEditor); 524 break; 525 526 case MenuPanel.GameSaves: 527 SwitchMenu(MenuType.Singleplayer); 528 break; 529 } 530 531 lastGameState = MenuPanel.None; 532 } 533 } 534 } 535