diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..831b254
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "search.useIgnoreFiles": false
+}
diff --git a/OpenRA.Mods.D2/OpenRA.Mods.D2.csproj b/OpenRA.Mods.D2/OpenRA.Mods.D2.csproj
index 236eb6f..5fc66b2 100644
--- a/OpenRA.Mods.D2/OpenRA.Mods.D2.csproj
+++ b/OpenRA.Mods.D2/OpenRA.Mods.D2.csproj
@@ -3,7 +3,7 @@
net472
true
true
- 5
+ 7
true
true
../mods/d2
@@ -40,4 +40,4 @@
-
\ No newline at end of file
+
diff --git a/OpenRA.Mods.D2/Widgets/Logic/Ingame/D2IngameMenuLogic.cs b/OpenRA.Mods.D2/Widgets/Logic/Ingame/D2IngameMenuLogic.cs
new file mode 100644
index 0000000..212e4e6
--- /dev/null
+++ b/OpenRA.Mods.D2/Widgets/Logic/Ingame/D2IngameMenuLogic.cs
@@ -0,0 +1,392 @@
+#region Copyright & License Information
+/*
+ * Copyright 2007-2020 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, either version 3 of
+ * the License, or (at your option) any later version. For more
+ * information, see COPYING.
+ */
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using OpenRA.Graphics;
+using OpenRA.Mods.Common.Scripting;
+using OpenRA.Mods.Common.Traits;
+using OpenRA.Mods.Common.Widgets;
+using OpenRA.Mods.Common.Widgets.Logic;
+using OpenRA.Widgets;
+
+namespace OpenRA.Mods.D2.Widgets.Logic
+{
+ public class D2IngameMenuLogic : ChromeLogic
+ {
+ readonly Widget menu;
+ readonly Widget buttonContainer;
+ readonly ButtonWidget buttonTemplate;
+ readonly int2 buttonStride;
+ readonly List buttons = new List();
+
+ readonly ModData modData;
+ readonly Action onExit;
+ readonly World world;
+ readonly WorldRenderer worldRenderer;
+ readonly MenuPaletteEffect mpe;
+ readonly bool isSinglePlayer;
+ bool hasError;
+ bool leaving;
+ bool hideMenu;
+
+ [ObjectCreator.UseCtor]
+ public D2IngameMenuLogic(Widget widget, ModData modData, World world, Action onExit, WorldRenderer worldRenderer,
+ IngameInfoPanel activePanel, Dictionary logicArgs)
+ {
+ this.modData = modData;
+ this.world = world;
+ this.worldRenderer = worldRenderer;
+ this.onExit = onExit;
+
+ var buttonHandlers = new Dictionary
+ {
+ { "ABORT_MISSION", CreateAbortMissionButton },
+ { "RESTART", CreateRestartButton },
+ { "SURRENDER", CreateSurrenderButton },
+ { "LOAD_GAME", CreateLoadGameButton },
+ { "SAVE_GAME", CreateSaveGameButton },
+ { "MUSIC", CreateMusicButton },
+ { "SETTINGS", CreateSettingsButton },
+ { "RESUME", CreateResumeButton },
+ { "SAVE_MAP", CreateSaveMapButton },
+ { "EXIT_EDITOR", CreateExitEditorButton }
+ };
+
+ isSinglePlayer = !world.LobbyInfo.GlobalSettings.Dedicated && world.LobbyInfo.NonBotClients.Count() == 1;
+
+ menu = widget.Get("INGAME_MENU");
+ mpe = world.WorldActor.TraitOrDefault();
+ mpe?.Fade(mpe.Info.MenuEffect);
+
+ menu.Get("VERSION_LABEL").Text = modData.Manifest.Metadata.Version;
+
+ buttonContainer = menu.Get("MENU_BUTTONS");
+ buttonTemplate = buttonContainer.Get("BUTTON_TEMPLATE");
+ buttonContainer.RemoveChild(buttonTemplate);
+ buttonContainer.IsVisible = () => !hideMenu;
+
+ if (logicArgs.TryGetValue("ButtonStride", out var buttonStrideNode))
+ buttonStride = FieldLoader.GetValue("ButtonStride", buttonStrideNode.Value);
+
+ var scriptContext = world.WorldActor.TraitOrDefault();
+ hasError = scriptContext != null && scriptContext.FatalErrorOccurred;
+
+ if (logicArgs.TryGetValue("Buttons", out var buttonsNode))
+ {
+ var buttonIds = FieldLoader.GetValue("Buttons", buttonsNode.Value);
+ foreach (var button in buttonIds)
+ if (buttonHandlers.TryGetValue(button, out var createHandler))
+ createHandler();
+ }
+
+ // Recenter the button container
+ if (buttons.Count > 0)
+ {
+ var expand = (buttons.Count - 1) * buttonStride;
+ buttonContainer.Bounds.X -= expand.X / 2;
+ buttonContainer.Bounds.Y -= expand.Y / 2;
+ buttonContainer.Bounds.Width += expand.X;
+ buttonContainer.Bounds.Height += expand.Y;
+ }
+
+ var panelRoot = widget.GetOrNull("PANEL_ROOT");
+ if (panelRoot != null && world.Type != WorldType.Editor)
+ {
+ Action requestHideMenu = h => hideMenu = h;
+ var gameInfoPanel = Game.LoadWidget(world, "GAME_INFO_PANEL", panelRoot, new WidgetArgs()
+ {
+ { "activePanel", activePanel },
+ { "hideMenu", requestHideMenu }
+ });
+
+ gameInfoPanel.IsVisible = () => !hideMenu;
+ }
+ }
+
+ void OnQuit()
+ {
+ // TODO: Create a mechanism to do things like this cleaner. Also needed for scripted missions
+ if (world.Type == WorldType.Regular)
+ {
+ var moi = world.Map.Rules.Actors["player"].TraitInfoOrDefault();
+ if (moi != null)
+ {
+ var faction = world.LocalPlayer == null ? null : world.LocalPlayer.Faction.InternalName;
+ Game.Sound.PlayNotification(world.Map.Rules, null, "Speech", moi.LeaveNotification, faction);
+ }
+ }
+
+ leaving = true;
+
+ var iop = world.WorldActor.TraitsImplementing().FirstOrDefault();
+ var exitDelay = iop != null ? iop.ExitDelay : 0;
+ if (mpe != null)
+ {
+ Game.RunAfterDelay(exitDelay, () =>
+ {
+ if (Game.IsCurrentWorld(world))
+ mpe.Fade(MenuPaletteEffect.EffectType.Black);
+ });
+ exitDelay += 40 * mpe.Info.FadeLength;
+ }
+
+ Game.RunAfterDelay(exitDelay, () =>
+ {
+ if (!Game.IsCurrentWorld(world))
+ return;
+
+ Game.Disconnect();
+ Ui.ResetAll();
+ Game.LoadShellMap();
+ });
+ }
+
+ void ShowMenu()
+ {
+ hideMenu = false;
+ }
+
+ void CloseMenu()
+ {
+ Ui.CloseWindow();
+ mpe?.Fade(MenuPaletteEffect.EffectType.None);
+ onExit();
+ Ui.ResetTooltips();
+ }
+
+ ButtonWidget AddButton(string id, string text)
+ {
+ var button = buttonTemplate.Clone() as ButtonWidget;
+ var lastButton = buttons.LastOrDefault();
+ if (lastButton != null)
+ {
+ button.Bounds.X = lastButton.Bounds.X + buttonStride.X;
+ button.Bounds.Y = lastButton.Bounds.Y + buttonStride.Y;
+ }
+
+ button.Id = id;
+ button.IsDisabled = () => leaving;
+ button.GetText = () => text;
+ buttonContainer.AddChild(button);
+ buttons.Add(button);
+
+ return button;
+ }
+
+ void CreateAbortMissionButton()
+ {
+ if (world.Type != WorldType.Regular)
+ return;
+
+ var button = AddButton("ABORT_MISSION", world.IsGameOver ? "Leave" : "Abort Mission");
+
+ button.OnClick = () =>
+ {
+ hideMenu = true;
+
+ ConfirmationDialogs.ButtonPrompt(
+ title: "Leave Mission",
+ text: "Leave this game and return to the menu?",
+ onConfirm: OnQuit,
+ onCancel: ShowMenu,
+ confirmText: "Leave",
+ cancelText: "Stay");
+ };
+ }
+
+ void CreateRestartButton()
+ {
+ if (world.Type != WorldType.Regular || !isSinglePlayer)
+ return;
+
+ var iop = world.WorldActor.TraitsImplementing().FirstOrDefault();
+ var exitDelay = iop != null ? iop.ExitDelay : 0;
+
+ Action onRestart = () =>
+ {
+ Ui.CloseWindow();
+ if (mpe != null)
+ {
+ if (Game.IsCurrentWorld(world))
+ mpe.Fade(MenuPaletteEffect.EffectType.Black);
+ exitDelay += 40 * mpe.Info.FadeLength;
+ }
+
+ Game.RunAfterDelay(exitDelay, Game.RestartGame);
+ };
+
+ var button = AddButton("RESTART", "Restart");
+ button.IsDisabled = () => hasError || leaving;
+ button.OnClick = () =>
+ {
+ hideMenu = true;
+ ConfirmationDialogs.ButtonPrompt(
+ title: "Restart",
+ text: "Are you sure you want to restart?",
+ onConfirm: onRestart,
+ onCancel: ShowMenu,
+ confirmText: "Restart",
+ cancelText: "Stay");
+ };
+ }
+
+ void CreateSurrenderButton()
+ {
+ if (world.Type != WorldType.Regular || isSinglePlayer || world.LocalPlayer == null)
+ return;
+
+ Action onSurrender = () =>
+ {
+ world.IssueOrder(new Order("Surrender", world.LocalPlayer.PlayerActor, false));
+ CloseMenu();
+ };
+
+ var button = AddButton("SURRENDER", "Surrender");
+ button.IsDisabled = () => world.LocalPlayer.WinState != WinState.Undefined || hasError || leaving;
+ button.OnClick = () =>
+ {
+ hideMenu = true;
+ ConfirmationDialogs.ButtonPrompt(
+ title: "Surrender",
+ text: "Are you sure you want to surrender?",
+ onConfirm: onSurrender,
+ onCancel: ShowMenu,
+ confirmText: "Surrender",
+ cancelText: "Stay");
+ };
+ }
+
+ void CreateLoadGameButton()
+ {
+ if (world.Type != WorldType.Regular || !world.LobbyInfo.GlobalSettings.GameSavesEnabled || world.IsReplay)
+ return;
+
+ var button = AddButton("LOAD_GAME", "Load Game");
+ button.IsDisabled = () => leaving || !GameSaveBrowserLogic.IsLoadPanelEnabled(modData.Manifest);
+ button.OnClick = () =>
+ {
+ hideMenu = true;
+ Ui.OpenWindow("GAMESAVE_BROWSER_PANEL", new WidgetArgs
+ {
+ { "onExit", () => hideMenu = false },
+ { "onStart", CloseMenu },
+ { "isSavePanel", false },
+ { "world", null }
+ });
+ };
+ }
+
+ void CreateSaveGameButton()
+ {
+ if (world.Type != WorldType.Regular || !world.LobbyInfo.GlobalSettings.GameSavesEnabled || world.IsReplay)
+ return;
+
+ var button = AddButton("SAVE_GAME", "Save Game");
+ button.IsDisabled = () => hasError || leaving || !world.Players.Any(p => p.Playable && p.WinState == WinState.Undefined);
+ button.OnClick = () =>
+ {
+ hideMenu = true;
+ Ui.OpenWindow("GAMESAVE_BROWSER_PANEL", new WidgetArgs
+ {
+ { "onExit", () => hideMenu = false },
+ { "onStart", () => { } },
+ { "isSavePanel", true },
+ { "world", world }
+ });
+ };
+ }
+
+ void CreateMusicButton()
+ {
+ var button = AddButton("MUSIC", "Music");
+ button.OnClick = () =>
+ {
+ hideMenu = true;
+ Ui.OpenWindow("MUSIC_PANEL", new WidgetArgs()
+ {
+ { "onExit", () => hideMenu = false },
+ { "world", world }
+ });
+ };
+ }
+
+ void CreateSettingsButton()
+ {
+ var button = AddButton("SETTINGS", "Settings");
+ button.OnClick = () =>
+ {
+ hideMenu = true;
+ Ui.OpenWindow("SETTINGS_PANEL", new WidgetArgs()
+ {
+ { "world", world },
+ { "worldRenderer", worldRenderer },
+ { "onExit", () => hideMenu = false },
+ });
+ };
+ }
+
+ void CreateResumeButton()
+ {
+ var button = AddButton("RESUME", world.IsGameOver ? "Return to map" : "Resume");
+ button.Key = modData.Hotkeys["escape"];
+ button.OnClick = CloseMenu;
+ }
+
+ void CreateSaveMapButton()
+ {
+ if (world.Type != WorldType.Editor)
+ return;
+
+ var button = AddButton("SAVE_MAP", "Save Map");
+ button.OnClick = () =>
+ {
+ hideMenu = true;
+ var editorActorLayer = world.WorldActor.Trait();
+ var actionManager = world.WorldActor.Trait();
+ Ui.OpenWindow("SAVE_MAP_PANEL", new WidgetArgs()
+ {
+ { "onSave", (Action)(_ => { hideMenu = false; actionManager.Modified = false; }) },
+ { "onExit", () => hideMenu = false },
+ { "map", world.Map },
+ { "playerDefinitions", editorActorLayer.Players.ToMiniYaml() },
+ { "actorDefinitions", editorActorLayer.Save() }
+ });
+ };
+ }
+
+ void CreateExitEditorButton()
+ {
+ if (world.Type != WorldType.Editor)
+ return;
+
+ var actionManager = world.WorldActor.Trait();
+ var button = AddButton("EXIT_EDITOR", "Exit Map Editor");
+
+ // Show dialog only if updated since last save
+ button.OnClick = () =>
+ {
+ if (actionManager.HasUnsavedItems())
+ {
+ hideMenu = true;
+ ConfirmationDialogs.ButtonPrompt(
+ title: "Exit Map Editor",
+ text: "Exit and lose all unsaved changes?",
+ onConfirm: OnQuit,
+ onCancel: ShowMenu);
+ }
+ else
+ OnQuit();
+ };
+ }
+ }
+}
diff --git a/OpenRA.Mods.D2/Widgets/Logic/Ingame/D2MenuButtonsChromeLogic.cs b/OpenRA.Mods.D2/Widgets/Logic/Ingame/D2MenuButtonsChromeLogic.cs
new file mode 100644
index 0000000..b715ccb
--- /dev/null
+++ b/OpenRA.Mods.D2/Widgets/Logic/Ingame/D2MenuButtonsChromeLogic.cs
@@ -0,0 +1,132 @@
+#region Copyright & License Information
+/*
+ * Copyright 2007-2020 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, either version 3 of
+ * the License, or (at your option) any later version. For more
+ * information, see COPYING.
+ */
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using OpenRA.Mods.Common.Traits;
+using OpenRA.Mods.Common.Widgets;
+using OpenRA.Mods.Common.Widgets.Logic;
+using OpenRA.Widgets;
+
+namespace OpenRA.Mods.D2.Widgets.Logic
+{
+ public class D2MenuButtonsChromeLogic : ChromeLogic
+ {
+ readonly World world;
+ readonly Widget worldRoot;
+ readonly Widget menuRoot;
+ readonly string clickSound = ChromeMetrics.Get("ClickSound");
+
+ bool disableSystemButtons;
+ Widget currentWidget;
+
+ [ObjectCreator.UseCtor]
+ public D2MenuButtonsChromeLogic(Widget widget, ModData modData, World world, Dictionary logicArgs)
+ {
+ this.world = world;
+
+ worldRoot = Ui.Root.Get("WORLD_ROOT");
+ menuRoot = Ui.Root.Get("MENU_ROOT");
+
+ // System buttons
+ var options = widget.GetOrNull("OPTIONS_BUTTON");
+ if (options != null)
+ {
+ var blinking = false;
+ var lp = world.LocalPlayer;
+ options.IsDisabled = () => disableSystemButtons;
+ options.OnClick = () =>
+ {
+ blinking = false;
+ OpenMenuPanel(options, new WidgetArgs()
+ {
+ { "activePanel", IngameInfoPanel.AutoSelect }
+ });
+ };
+ options.IsHighlighted = () => blinking && Game.LocalTick % 50 < 25;
+
+ if (lp != null)
+ {
+ Action startBlinking = (player, inhibitAnnouncement) =>
+ {
+ if (!inhibitAnnouncement && player == world.LocalPlayer)
+ blinking = true;
+ };
+
+ var mo = lp.PlayerActor.TraitOrDefault();
+
+ if (mo != null)
+ mo.ObjectiveAdded += startBlinking;
+ }
+ }
+
+ var debug = widget.GetOrNull("DEBUG_BUTTON");
+ if (debug != null)
+ {
+ // Can't use DeveloperMode.Enabled because there is a hardcoded hack to *always*
+ // enable developer mode for singleplayer games, but we only want to show the button
+ // if it has been explicitly enabled
+ var def = world.Map.Rules.Actors["player"].TraitInfo().CheckboxEnabled;
+ var enabled = world.LobbyInfo.GlobalSettings.OptionOrDefault("cheats", def);
+ debug.IsVisible = () => enabled;
+ debug.IsDisabled = () => disableSystemButtons;
+ debug.OnClick = () => OpenMenuPanel(debug, new WidgetArgs()
+ {
+ { "activePanel", IngameInfoPanel.Debug }
+ });
+ }
+
+ if (logicArgs.TryGetValue("ClickSound", out var yaml))
+ clickSound = yaml.Value;
+ }
+
+ void OpenMenuPanel(MenuButtonWidget button, WidgetArgs widgetArgs = null)
+ {
+ disableSystemButtons = true;
+ var cachedPause = world.PredictedPaused;
+
+ if (button.HideIngameUI)
+ {
+ // Cancel custom input modes (guard, building placement, etc)
+ world.CancelInputMode();
+
+ worldRoot.IsVisible = () => false;
+ }
+
+ if (button.Pause && world.LobbyInfo.NonBotClients.Count() == 1)
+ world.SetPauseState(true);
+
+ var cachedDisableWorldSounds = Game.Sound.DisableWorldSounds;
+ if (button.DisableWorldSounds)
+ Game.Sound.DisableWorldSounds = true;
+
+ widgetArgs = widgetArgs ?? new WidgetArgs();
+ widgetArgs.Add("onExit", () =>
+ {
+ if (button.HideIngameUI)
+ worldRoot.IsVisible = () => true;
+
+ if (button.DisableWorldSounds)
+ Game.Sound.DisableWorldSounds = cachedDisableWorldSounds;
+
+ if (button.Pause && world.LobbyInfo.NonBotClients.Count() == 1)
+ world.SetPauseState(cachedPause);
+
+ menuRoot.RemoveChild(currentWidget);
+ disableSystemButtons = false;
+ });
+
+ currentWidget = Game.LoadWidget(world, button.MenuContainer, menuRoot, widgetArgs);
+ Game.RunAfterTick(Ui.ResetTooltips);
+ }
+ }
+}
diff --git a/mods/d2/chrome.yaml b/mods/d2/chrome.yaml
index e6d0c29..d85554c 100644
--- a/mods/d2/chrome.yaml
+++ b/mods/d2/chrome.yaml
@@ -1,6 +1,8 @@
screen:
SpriteImage: SCREEN.CPS
Regions:
+ mentat-button: 15, 1, 78, 13
+ options-button: 103, 1, 78, 13
topbar-left: 0, 0, 185, 40
topbar: 185, 0, 15, 40
topbar-right: 190, 0, 130, 40
diff --git a/mods/d2/chrome/ingame-player.yaml b/mods/d2/chrome/ingame-player.yaml
index 2cdadf6..f9ee9c1 100644
--- a/mods/d2/chrome/ingame-player.yaml
+++ b/mods/d2/chrome/ingame-player.yaml
@@ -33,10 +33,20 @@ Container@PLAYER_WIDGETS:
ImageName: topbar-right
ClickThrough: false
Container@TOPBAR_BUTTONS:
- Logic: MenuButtonsChromeLogic
+ Logic: D2MenuButtonsChromeLogic
X: 0
Y: 0
Children:
+ MenuButton@MENTAT_BUTTON:
+ X: 15
+ Y: 1
+ Width: 78
+ Height: 15
+ Background:
+ TooltipText: Mentat
+ TooltipContainer: TOOLTIP_CONTAINER
+ DisableWorldSounds: false
+ VisualHeight: 0
MenuButton@OPTIONS_BUTTON:
Key: escape
X: 103