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