From 9f8b51d2e68ab02b988a98d9bfed6ecaec1844f0 Mon Sep 17 00:00:00 2001 From: Fernando Arzola <17498701+Arufonsu@users.noreply.github.com> Date: Tue, 26 Nov 2024 19:31:41 -0300 Subject: [PATCH] feature: simplified escape menu (#2395) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feature: simplified escape menu + In game interface setting to toggle this feature. + Fix: "InvalidOperationException: Collection was modified (during iteration);" client crash was happening upon selecting 'yes' on Combat Warning prompt when logging out/exiting. * fix: ♻️ Apply review changes (I) * fix: ♻️ Apply review changes (II) --- .../Database/GameDatabase.cs | 4 + .../Gwen/Control/Base.cs | 9 +- Intersect.Client/Core/Input.cs | 11 +- .../Interface/Game/GameInterface.cs | 3 + Intersect.Client/Interface/Game/Menu.cs | 13 +- .../Interface/Game/SimplifiedEscapeMenu.cs | 167 ++++++++++++++++++ .../Interface/Shared/SettingsWindow.cs | 9 + Intersect.Client/Localization/Strings.cs | 3 + 8 files changed, 215 insertions(+), 4 deletions(-) create mode 100644 Intersect.Client/Interface/Game/SimplifiedEscapeMenu.cs diff --git a/Intersect.Client.Framework/Database/GameDatabase.cs b/Intersect.Client.Framework/Database/GameDatabase.cs index e772fcb64f..ef1de0c3ff 100644 --- a/Intersect.Client.Framework/Database/GameDatabase.cs +++ b/Intersect.Client.Framework/Database/GameDatabase.cs @@ -58,6 +58,8 @@ public abstract partial class GameDatabase public bool ShowHealthAsPercentage { get; set; } public bool ShowManaAsPercentage { get; set; } + + public bool SimplifiedEscapeMenu { get; set; } public TypewriterBehavior TypewriterBehavior { get; set; } @@ -130,6 +132,7 @@ public virtual void LoadPreferences() ShowExperienceAsPercentage = LoadPreference(nameof(ShowExperienceAsPercentage), true); ShowHealthAsPercentage = LoadPreference(nameof(ShowHealthAsPercentage), false); ShowManaAsPercentage = LoadPreference(nameof(ShowManaAsPercentage), false); + SimplifiedEscapeMenu = LoadPreference(nameof(SimplifiedEscapeMenu), false); TypewriterBehavior = LoadPreference(nameof(TypewriterBehavior), TypewriterBehavior.Word); UIScale = LoadPreference(nameof(UIScale), 1.0f); WorldZoom = LoadPreference(nameof(WorldZoom), 1.0f); @@ -166,6 +169,7 @@ public virtual void SavePreferences() SavePreference(nameof(ShowExperienceAsPercentage), ShowExperienceAsPercentage); SavePreference(nameof(ShowHealthAsPercentage), ShowHealthAsPercentage); SavePreference(nameof(ShowManaAsPercentage), ShowManaAsPercentage); + SavePreference(nameof(SimplifiedEscapeMenu), SimplifiedEscapeMenu); SavePreference(nameof(TypewriterBehavior), TypewriterBehavior); SavePreference(nameof(UIScale), UIScale); SavePreference(nameof(WorldZoom), WorldZoom); diff --git a/Intersect.Client.Framework/Gwen/Control/Base.cs b/Intersect.Client.Framework/Gwen/Control/Base.cs index 628a82e50a..c730eb2072 100644 --- a/Intersect.Client.Framework/Gwen/Control/Base.cs +++ b/Intersect.Client.Framework/Gwen/Control/Base.cs @@ -765,7 +765,14 @@ public virtual void Dispose() Gwen.ToolTip.ControlDeleted(this); Animation.Cancel(this); - mChildren?.ForEach(child => child?.Dispose()); + // [Fix]: "InvalidOperationException: Collection was modified (during iteration); enumeration operation may not execute". + // (Creates an array copy of the children to avoid modifying the collection during iteration). + var children = mChildren.ToArray(); + foreach (var child in children) + { + child.Dispose(); + } + mChildren?.Clear(); mInnerPanel?.Dispose(); diff --git a/Intersect.Client/Core/Input.cs b/Intersect.Client/Core/Input.cs index 4d6948b669..2367f4f8db 100644 --- a/Intersect.Client/Core/Input.cs +++ b/Intersect.Client/Core/Input.cs @@ -133,7 +133,16 @@ public static void OnKeyPressed(Keys modifier, Keys key) } else { - Interface.Interface.GameUi?.EscapeMenu?.ToggleHidden(); + var simplifiedEscapeMenuSetting = Globals.Database.SimplifiedEscapeMenu; + + if (simplifiedEscapeMenuSetting) + { + Interface.Interface.GameUi?.SimplifiedEscapeMenu?.ToggleHidden(); + } + else + { + Interface.Interface.GameUi?.EscapeMenu?.ToggleHidden(); + } } } diff --git a/Intersect.Client/Interface/Game/GameInterface.cs b/Intersect.Client/Interface/Game/GameInterface.cs index e55e36ec35..a59008ce9a 100644 --- a/Intersect.Client/Interface/Game/GameInterface.cs +++ b/Intersect.Client/Interface/Game/GameInterface.cs @@ -93,6 +93,7 @@ public GameInterface(Canvas canvas) : base(canvas) { GameCanvas = canvas; EscapeMenu = new EscapeMenu(GameCanvas) {IsHidden = true}; + SimplifiedEscapeMenu = new SimplifiedEscapeMenu(GameCanvas) {IsHidden = true}; AnnouncementWindow = new AnnouncementWindow(GameCanvas) { IsHidden = true }; InitGameGui(); @@ -101,6 +102,8 @@ public GameInterface(Canvas canvas) : base(canvas) public Canvas GameCanvas { get; } public EscapeMenu EscapeMenu { get; } + + public SimplifiedEscapeMenu SimplifiedEscapeMenu { get; } public AnnouncementWindow AnnouncementWindow { get; } diff --git a/Intersect.Client/Interface/Game/Menu.cs b/Intersect.Client/Interface/Game/Menu.cs index 6e1698e89a..7b41abcc35 100644 --- a/Intersect.Client/Interface/Game/Menu.cs +++ b/Intersect.Client/Interface/Game/Menu.cs @@ -355,9 +355,18 @@ public bool HasWindowsOpen() } //Input Handlers - private static void MenuButtonClicked(Base sender, ClickedEventArgs arguments) + private void MenuButtonClicked(Base sender, ClickedEventArgs arguments) { - Interface.GameUi?.EscapeMenu?.ToggleHidden(); + var simplifiedEscapeMenuSetting = Globals.Database.SimplifiedEscapeMenu; + + if (simplifiedEscapeMenuSetting) + { + Interface.GameUi?.SimplifiedEscapeMenu?.ToggleHidden(mMenuButton); + } + else + { + Interface.GameUi?.EscapeMenu?.ToggleHidden(); + } } private void PartyBtn_Clicked(Base sender, ClickedEventArgs arguments) diff --git a/Intersect.Client/Interface/Game/SimplifiedEscapeMenu.cs b/Intersect.Client/Interface/Game/SimplifiedEscapeMenu.cs new file mode 100644 index 0000000000..3e315befc0 --- /dev/null +++ b/Intersect.Client/Interface/Game/SimplifiedEscapeMenu.cs @@ -0,0 +1,167 @@ +using Intersect.Client.Core; +using Intersect.Client.Framework.File_Management; +using Intersect.Client.Framework.Gwen; +using Intersect.Client.Framework.Gwen.Control; +using Intersect.Client.Framework.Gwen.Control.EventArguments; +using Intersect.Client.General; +using Intersect.Client.Interface.Shared; +using Intersect.Client.Localization; +using Intersect.Utilities; + +namespace Intersect.Client.Interface.Game; + +public sealed partial class SimplifiedEscapeMenu : Framework.Gwen.Control.Menu +{ + private readonly SettingsWindow _settingsWindow; + private readonly MenuItem _settings; + private readonly MenuItem _character; + private readonly MenuItem _logout; + private readonly MenuItem _exit; + + public SimplifiedEscapeMenu(Canvas gameCanvas) : base(gameCanvas, nameof(SimplifiedEscapeMenu)) + { + IsHidden = true; + IconMarginDisabled = true; + _settingsWindow = new SettingsWindow(gameCanvas, null, null); + + Children.Clear(); + + _settings = AddItem(Strings.EscapeMenu.Settings); + _character = AddItem(Strings.EscapeMenu.CharacterSelect); + _logout = AddItem(Strings.EscapeMenu.Logout); + _exit = AddItem(Strings.EscapeMenu.ExitToDesktop); + + _settings.Clicked += OpenSettingsWindow; + _character.Clicked += LogoutToCharacterSelectSelectClicked; + _logout.Clicked += LogoutToMainToMainMenuClicked; + _exit.Clicked += ExitToDesktopToDesktopClicked; + + LoadJsonUi(GameContentManager.UI.InGame, Graphics.Renderer?.GetResolutionString()); + } + + public void ToggleHidden(Button? target) + { + if (!_settingsWindow.IsHidden || target == null) + { + return; + } + + if (this.IsHidden) + { + // Position the context menu within the game canvas if near borders. + var menuPosX = target.LocalPosToCanvas(new Point(0, 0)).X; + var menuPosY = target.LocalPosToCanvas(new Point(0, 0)).Y; + var newX = menuPosX; + var newY = menuPosY + target.Height + 6; + + if (newX + Width >= Canvas?.Width) + { + newX = menuPosX - Width + target.Width; + } + + if (newY + Height >= Canvas?.Height) + { + newY = menuPosY - Height - 6; + } + + SizeToChildren(); + Open(Pos.None); + SetPosition(newX, newY); + } + else + { + Close(); + } + } + + private void LogoutToCharacterSelectSelectClicked(Base sender, ClickedEventArgs arguments) + { + if (Globals.Me?.CombatTimer > Timing.Global.Milliseconds) + { + _ = new InputBox( + title: Strings.Combat.WarningTitle, + prompt: Strings.Combat.WarningCharacterSelect, + inputType: InputBox.InputType.YesNo, + onSuccess: LogoutToCharacterSelect + ); + } + else + { + LogoutToCharacterSelect(null, null); + } + } + + private void LogoutToMainToMainMenuClicked(Base sender, ClickedEventArgs arguments) + { + if (Globals.Me?.CombatTimer > Timing.Global.Milliseconds) + { + _ = new InputBox( + title: Strings.Combat.WarningTitle, + prompt: Strings.Combat.WarningLogout, + inputType: InputBox.InputType.YesNo, + onSuccess: LogoutToMainMenu + ); + } + else + { + LogoutToMainMenu(null, null); + } + } + + private void ExitToDesktopToDesktopClicked(Base sender, ClickedEventArgs arguments) + { + if (Globals.Me?.CombatTimer > Timing.Global.Milliseconds) + { + _ = new InputBox( + title: Strings.Combat.WarningTitle, + prompt: Strings.Combat.WarningExitDesktop, + inputType: InputBox.InputType.YesNo, + onSuccess: ExitToDesktop + ); + } + else + { + ExitToDesktop(null, null); + } + } + + private void OpenSettingsWindow(object? sender, EventArgs? e) + { + if (!_settingsWindow.IsHidden) + { + return; + } + + _settingsWindow.Show(); + } + + private static void LogoutToCharacterSelect(object? sender, EventArgs? e) + { + if (Globals.Me != null) + { + Globals.Me.CombatTimer = 0; + } + + Main.Logout(true); + } + + private static void LogoutToMainMenu(object? sender, EventArgs? e) + { + if (Globals.Me != null) + { + Globals.Me.CombatTimer = 0; + } + + Main.Logout(false); + } + + private static void ExitToDesktop(object? sender, EventArgs? e) + { + if (Globals.Me != null) + { + Globals.Me.CombatTimer = 0; + } + + Globals.IsRunning = false; + } +} \ No newline at end of file diff --git a/Intersect.Client/Interface/Shared/SettingsWindow.cs b/Intersect.Client/Interface/Shared/SettingsWindow.cs index b7cf28fb8c..ff9baa3cf7 100644 --- a/Intersect.Client/Interface/Shared/SettingsWindow.cs +++ b/Intersect.Client/Interface/Shared/SettingsWindow.cs @@ -45,6 +45,7 @@ public partial class SettingsWindow : ImagePanel private readonly LabeledCheckBox _showExperienceAsPercentageCheckbox; private readonly LabeledCheckBox _showHealthAsPercentageCheckbox; private readonly LabeledCheckBox _showManaAsPercentageCheckbox; + private readonly LabeledCheckBox _simplifiedEscapeMenu; // Game Settings - Information. private readonly ScrollControl _informationSettings; @@ -180,6 +181,12 @@ public SettingsWindow(Base parent, MainMenu? mainMenu, EscapeMenu? escapeMenu) : { Text = Strings.Settings.ShowManaAsPercentage }; + + // Game Settings - Interface: simplified escape menu. + _simplifiedEscapeMenu = new LabeledCheckBox(_interfaceSettings, "SimplifiedEscapeMenu") + { + Text = Strings.Settings.SimplifiedEscapeMenu + }; // Game Settings - Information. _informationSettings = new ScrollControl(_gameSettingsContainer, "InformationSettings"); @@ -730,6 +737,7 @@ public void Show(bool returnToMenu = false) _showHealthAsPercentageCheckbox.IsChecked = Globals.Database.ShowHealthAsPercentage; _showManaAsPercentageCheckbox.IsChecked = Globals.Database.ShowManaAsPercentage; _showExperienceAsPercentageCheckbox.IsChecked = Globals.Database.ShowExperienceAsPercentage; + _simplifiedEscapeMenu.IsChecked = Globals.Database.SimplifiedEscapeMenu; _friendOverheadInfoCheckbox.IsChecked = Globals.Database.FriendOverheadInfo; _guildMemberOverheadInfoCheckbox.IsChecked = Globals.Database.GuildMemberOverheadInfo; _myOverheadInfoCheckbox.IsChecked = Globals.Database.MyOverheadInfo; @@ -910,6 +918,7 @@ private void SettingsApplyBtn_Clicked(Base sender, ClickedEventArgs arguments) Globals.Database.ShowExperienceAsPercentage = _showExperienceAsPercentageCheckbox.IsChecked; Globals.Database.ShowHealthAsPercentage = _showHealthAsPercentageCheckbox.IsChecked; Globals.Database.ShowManaAsPercentage = _showManaAsPercentageCheckbox.IsChecked; + Globals.Database.SimplifiedEscapeMenu = _simplifiedEscapeMenu.IsChecked; Globals.Database.FriendOverheadInfo = _friendOverheadInfoCheckbox.IsChecked; Globals.Database.GuildMemberOverheadInfo = _guildMemberOverheadInfoCheckbox.IsChecked; Globals.Database.MyOverheadInfo = _myOverheadInfoCheckbox.IsChecked; diff --git a/Intersect.Client/Localization/Strings.cs b/Intersect.Client/Localization/Strings.cs index 3b44e1adaf..99c1465581 100644 --- a/Intersect.Client/Localization/Strings.cs +++ b/Intersect.Client/Localization/Strings.cs @@ -1932,6 +1932,9 @@ public partial struct Settings [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public static LocalizedString ShowPlayerOverheadInformation = @"Show players overhead information"; + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public static LocalizedString SimplifiedEscapeMenu = @"Simplified escape menu"; [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public static LocalizedString StickyTarget = @"Sticky Target";