From c0fb287a5f5579b6cf56adcca081d08d6d05cb5e Mon Sep 17 00:00:00 2001 From: Tommy Johnston Date: Thu, 11 Sep 2025 18:19:29 -0400 Subject: [PATCH 1/9] feat: exports --- vMenu/Exports.cs | 1222 +++++++++++++++++++++++++++++++++++++++++++++ vMenu/MainMenu.cs | 5 +- 2 files changed, 1226 insertions(+), 1 deletion(-) create mode 100644 vMenu/Exports.cs diff --git a/vMenu/Exports.cs b/vMenu/Exports.cs new file mode 100644 index 000000000..538b91278 --- /dev/null +++ b/vMenu/Exports.cs @@ -0,0 +1,1222 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using CitizenFX.Core; +using MenuAPI; +using Newtonsoft.Json; +using static vMenuClient.CommonFunctions; +using static vMenuShared.PermissionsManager; +using static vMenuShared.ConfigManager; + +namespace vMenuClient +{ + /// + /// Handles all dynamic menu exports for plugin usage + /// + public partial class MainMenu + { + /// + /// Callback delegate for export functions + /// + /// Optional arguments passed to the callback + private delegate void CallbackDelegate(params object[] args); + + /// + /// Dictionary to store dynamically created menus + /// + private static Dictionary DynamicMenus = new Dictionary(); + + /// + /// Checks if an ID conflicts with any existing menu or item + /// + /// ID to check + /// Context for error messages + /// True if there's a conflict + private bool HasIdConflict(string id, string context) + { + // Check against dynamic menus + if (DynamicMenus.ContainsKey(id)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] {context}: ID '{id}' conflicts with existing dynamic menu."); + return true; + } + + // Check against core built-in menus only (strict matching) + if (IsBuiltInMenuId(id)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] {context}: ID '{id}' conflicts with built-in vMenu menu."); + return true; + } + + return false; + } + + /// + /// Checks if an ID matches a core built-in menu (strict matching only) + /// + /// ID to check + /// True if it matches a built-in menu ID + private bool IsBuiltInMenuId(string id) + { + return id.ToLower() switch + { + "main" or "player" or "vehicle" or "world" or + "playeroptions" or "onlineplayers" or "bannedplayers" or + "personalvehicle" or "vehicleoptions" or "vehiclespawner" or + "savedvehicles" or "playerappearance" or "mppedcustomization" or + "timeoptions" or "weatheroptions" or "weaponoptions" or + "weaponloadouts" or "recording" or "miscsettings" or + "voicechat" or "about" => true, + _ => false + }; + } + + /// + /// Checks if an item ID conflicts within a specific menu + /// + /// Menu to check + /// Item ID to check + /// Context for error messages + /// True if there's a conflict + private bool HasItemConflict(Menu menu, string itemId, string context) + { + if (menu.GetMenuItems().Any(item => item.ItemData as string == itemId)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] {context}: Item with ID '{itemId}' already exists in this menu."); + return true; + } + return false; + } + + #region Export Registration and Initialization + /// + /// Initialize dynamic menu exports for plugin usage + /// + public void InitializeDynamicMenuExports() + { + RegisterDynamicMenuExports(); + } + + /// + /// Registers all dynamic menu exports + /// + private void RegisterDynamicMenuExports() + { + // Core Menu Management + Exports.Add("CreateMenu", new Action(CreateMenu)); + Exports.Add("OpenMenu", new Action(OpenMenu)); + Exports.Add("CloseMenu", new Action(CloseMenu)); + Exports.Add("CloseAllMenus", new Action(CloseAllMenus)); + Exports.Add("CheckMenu", new Func(CheckMenu)); + Exports.Add("ClearMenu", new Action(ClearMenu)); + Exports.Add("RefreshMenu", new Action(RefreshMenu)); + Exports.Add("DeleteMenu", new Action(DeleteMenu)); + + // Add Menu Items + Exports.Add("AddButton", new Action(AddButton)); + Exports.Add("AddList", new Action(AddList)); + Exports.Add("AddCheckbox", new Action(AddCheckbox)); + Exports.Add("AddSlider", new Action(AddSlider)); + Exports.Add("AddSpacer", new Action(AddSpacer)); + Exports.Add("AddSubmenuButton", new Action(AddSubmenuButton)); + + // Modify Menu Items + Exports.Add("RemoveItem", new Action(RemoveItem)); + + // Notifications + Exports.Add("Notify", new Action(notifExp)); + } + #endregion + + #region Export Implementation Methods + + /// + /// Creates a new dynamic menu + /// + /// Unique identifier for the menu + /// Display title for the menu + /// Menu description/subtitle + /// Optional callback for menu open events + private void CreateMenu(string menuId, string menuTitle, string menuDescription, object callbackObj) + { + // Validate required parameters + if (string.IsNullOrEmpty(menuId)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] CreateMenu: menuId cannot be null or empty."); + return; + } + + // Check for any ID conflicts + if (HasIdConflict(menuId, "CreateMenu")) + { + return; + } + + // Apply defaults for optional parameters + if (string.IsNullOrEmpty(menuTitle)) + menuTitle = "Menu"; + if (string.IsNullOrEmpty(menuDescription)) + menuDescription = ""; + + var newMenu = new Menu(Game.Player.Name, menuTitle) + { + MenuSubtitle = menuDescription + }; + MenuController.AddMenu(newMenu); + DynamicMenus[menuId] = newMenu; + + if (callbackObj != null) + { + // Handle both C# delegates and Lua functions + if (callbackObj is CallbackDelegate callback) + { + newMenu.OnMenuOpen += (sender) => + { + callback.Invoke(); + }; + } + else + { + // Handle Lua functions and dynamic callbacks + newMenu.OnMenuOpen += (sender) => + { + try + { + dynamic dynamicCallback = callbackObj; + dynamicCallback(); + } + catch (Exception ex) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking menu open callback for {menuId}: {ex.Message}"); + } + }; + } + } + } + + /// + /// Displays notification to the player + /// + /// Notification message + /// Notification type (error, info, success, or default) + private void notifExp(string message, string type) + { + if (type == "error") + { + Notify.Error(message); + } + else if (type == "info") + { + Notify.Info(message); + } + else if (type == "success") + { + Notify.Success(message); + } + else + { + Notify.Alert(message); + } + } + + /// + /// Opens a menu by ID + /// + /// Menu identifier + private void OpenMenu(string menuId) + { + // Validate required parameters + if (string.IsNullOrEmpty(menuId)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] OpenMenu: menuId cannot be null or empty."); + return; + } + + Menu menu = null; + if (!DynamicMenus.TryGetValue(menuId, out menu)) + { + menu = GetMenu(menuId); + } + + if (menu == null) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Menu with ID {menuId} not found."); + return; + } + + menu.OpenMenu(); + } + + /// + /// Adds a button to a menu + /// + /// Target menu ID + /// Unique button identifier + /// Button display text + /// Button description + /// Optional callback for button selection + private void AddButton(string menuId, string buttonId, string buttonLabel, string buttonDescription, object callbackObj = null) + { + // Validate required parameters + if (string.IsNullOrEmpty(buttonId)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] AddButton: buttonId cannot be null or empty."); + return; + } + + // Check for ID conflicts (menu names, other items) + if (HasIdConflict(buttonId, "AddButton")) + { + return; + } + + Menu menu = null; + if (!DynamicMenus.TryGetValue(menuId, out menu)) + { + menu = GetMenu(menuId); + } + + if (menu == null) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Menu with ID {menuId} not found."); + return; + } + + // Check for item conflicts within this menu + if (HasItemConflict(menu, buttonId, "AddButton")) + { + return; + } + + // Apply defaults for optional parameters + if (string.IsNullOrEmpty(buttonLabel)) + buttonLabel = "Button"; + if (string.IsNullOrEmpty(buttonDescription)) + buttonDescription = ""; + + var menuItem = new MenuItem(buttonLabel, buttonDescription) + { + ItemData = buttonId + }; + + if (callbackObj != null) + { + // Handle both C# delegates and Lua functions + if (callbackObj is CallbackDelegate callback) + { + menu.OnItemSelect += (sender, item, index) => + { + if (item == menuItem) + { + callback.Invoke(); + } + }; + } + else if (callbackObj is Func luaCallback) + { + menu.OnItemSelect += (sender, item, index) => + { + if (item == menuItem) + { + try + { + luaCallback.Invoke(); + } + catch (Exception ex) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking callback for button {buttonId}: {ex.Message}"); + } + } + }; + } + else + { + // Try to invoke as dynamic for other callback types + menu.OnItemSelect += (sender, item, index) => + { + if (item == menuItem) + { + try + { + dynamic dynamicCallback = callbackObj; + dynamicCallback(); + } + catch (Exception ex) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking dynamic callback for button {buttonId}: {ex.Message}"); + } + } + }; + } + } + + menu.AddMenuItem(menuItem); + menu.RefreshIndex(); + } + + /// + /// Adds a list item to a menu + /// + /// Target menu ID + /// Unique list identifier + /// List display text + /// List options as object array + /// Default selected index + /// List description + /// Optional callback for list changes + private void AddList(string menuId, string listId, string listLabel, object options, int defaultIndex, string description, object callbackObj = null) + { + // Validate required parameters + if (string.IsNullOrEmpty(listId)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] AddList: listId cannot be null or empty."); + return; + } + + // Check for ID conflicts (menu names, other items) + if (HasIdConflict(listId, "AddList")) + { + return; + } + + Menu menu = null; + if (!DynamicMenus.TryGetValue(menuId, out menu)) + { + menu = GetMenu(menuId); + } + + if (menu == null) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Menu with ID {menuId} not found."); + return; + } + + // Check for item conflicts within this menu + if (HasItemConflict(menu, listId, "AddList")) + { + return; + } + + // Apply defaults for optional parameters + if (string.IsNullOrEmpty(listLabel)) + listLabel = "List"; + if (string.IsNullOrEmpty(description)) + description = ""; + + List optionsList; + + // Handle different option input types + if (options is string jsonString) + { + // Legacy support for JSON string + optionsList = JsonConvert.DeserializeObject>(jsonString); + } + else if (options is object[] objectArray) + { + // Convert object array to string list + optionsList = objectArray.Select(o => o?.ToString() ?? "").ToList(); + } + else if (options is List objectList) + { + // Convert object list to string list + optionsList = objectList.Select(o => o?.ToString() ?? "").ToList(); + } + else if (options == null) + { + // Provide default empty list if options is null + optionsList = new List { "Option 1", "Option 2" }; + CitizenFX.Core.Debug.WriteLine($"[vMenu] No options provided for list {listId}, using default options."); + } + else + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Invalid options type for list {listId}. Expected array or JSON string."); + return; + } + + // Validate defaultIndex + if (defaultIndex < 0 || defaultIndex >= optionsList.Count) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Invalid defaultIndex {defaultIndex} for list {listId}. Using 0 instead."); + defaultIndex = 0; + } + + var menuListItem = new MenuListItem(listLabel, optionsList, defaultIndex, description) + { + ItemData = listId + }; + + if (callbackObj != null) + { + // Handle both C# delegates and Lua functions + if (callbackObj is CallbackDelegate callback) + { + // Listen for index changes (browsing through list) + menu.OnListIndexChange += (sender, item, oldIndex, newIndex, itemIndex) => + { + if (item == menuListItem) + { + var currentValue = optionsList[newIndex]; + var oldValue = optionsList[oldIndex]; + callback.Invoke(false, currentValue, newIndex, oldIndex); + } + }; + + // Listen for item selection (pressing enter/select) + menu.OnListItemSelect += (sender, item, listIndex, itemIndex) => + { + if (item == menuListItem) + { + var selectedValue = optionsList[listIndex]; + callback.Invoke(true, selectedValue, listIndex, listIndex); + } + }; + } + else + { + // Handle Lua functions and dynamic callbacks + menu.OnListIndexChange += (sender, item, oldIndex, newIndex, itemIndex) => + { + if (item == menuListItem) + { + try + { + var currentValue = optionsList[newIndex]; + var oldValue = optionsList[oldIndex]; + dynamic dynamicCallback = callbackObj; + dynamicCallback(false, currentValue, newIndex, oldIndex); + } + catch (Exception ex) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking callback for list {listId}: {ex.Message}"); + } + } + }; + + menu.OnListItemSelect += (sender, item, listIndex, itemIndex) => + { + if (item == menuListItem) + { + try + { + var selectedValue = optionsList[listIndex]; + dynamic dynamicCallback = callbackObj; + dynamicCallback(true, selectedValue, listIndex, listIndex); + } + catch (Exception ex) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking callback for list {listId}: {ex.Message}"); + } + } + }; + } + } + + menu.AddMenuItem(menuListItem); + menu.RefreshIndex(); + } + + /// + /// Adds a checkbox to a menu + /// + /// Target menu ID + /// Unique checkbox identifier + /// Checkbox display text + /// Checkbox description + /// Default checked state + /// Optional callback for checkbox changes + private void AddCheckbox(string menuId, string checkboxId, string checkboxLabel, string description, bool defaultValue, object callbackObj = null) + { + // Validate required parameters + if (string.IsNullOrEmpty(menuId)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] AddCheckbox: menuId cannot be null or empty."); + return; + } + if (string.IsNullOrEmpty(checkboxId)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] AddCheckbox: checkboxId cannot be null or empty."); + return; + } + + Menu menu = null; + if (!DynamicMenus.TryGetValue(menuId, out menu)) + { + menu = GetMenu(menuId); + } + + if (menu == null) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Menu with ID {menuId} not found."); + return; + } + + // Check for ID conflicts (menu names, other items) + if (HasIdConflict(checkboxId, "AddCheckbox")) + { + return; + } + + // Check for item conflicts within this menu + if (HasItemConflict(menu, checkboxId, "AddCheckbox")) + { + return; + } + + // Apply defaults for optional parameters + if (string.IsNullOrEmpty(checkboxLabel)) + checkboxLabel = "Checkbox"; + if (string.IsNullOrEmpty(description)) + description = ""; + + var menuCheckboxItem = new MenuCheckboxItem(checkboxLabel, description, defaultValue) + { + ItemData = checkboxId + }; + + if (callbackObj != null) + { + // Handle both C# delegates and Lua functions + if (callbackObj is CallbackDelegate callback) + { + menu.OnCheckboxChange += (sender, item, index, _checked) => + { + if (item == menuCheckboxItem) + { + callback.Invoke(_checked); + } + }; + } + else if (callbackObj is Func luaCallback) + { + menu.OnCheckboxChange += (sender, item, index, _checked) => + { + if (item == menuCheckboxItem) + { + try + { + dynamic dynamicCallback = luaCallback; + dynamicCallback(_checked); + } + catch (Exception ex) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking callback for checkbox {checkboxId}: {ex.Message}"); + } + } + }; + } + else + { + // Try to invoke as dynamic for other callback types + menu.OnCheckboxChange += (sender, item, index, _checked) => + { + if (item == menuCheckboxItem) + { + try + { + dynamic dynamicCallback = callbackObj; + dynamicCallback(_checked); + } + catch (Exception ex) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking dynamic callback for checkbox {checkboxId}: {ex.Message}"); + } + } + }; + } + } + + menu.AddMenuItem(menuCheckboxItem); + menu.RefreshIndex(); + } + + /// + /// Adds a slider to a menu (0-100% range) + /// + /// Target menu ID + /// Unique slider identifier + /// Slider display text + /// Slider description + /// Default slider value (0-100) + /// Optional callback for slider changes + private void AddSlider(string menuId, string sliderId, string sliderLabel, string description, int defaultValue, object callbackObj = null) + { + // Validate required parameters + if (string.IsNullOrEmpty(menuId)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] AddSlider: menuId cannot be null or empty."); + return; + } + if (string.IsNullOrEmpty(sliderId)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] AddSlider: sliderId cannot be null or empty."); + return; + } + + Menu menu = null; + if (!DynamicMenus.TryGetValue(menuId, out menu)) + { + menu = GetMenu(menuId); + } + + if (menu == null) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Menu with ID {menuId} not found."); + return; + } + + // Check for ID conflicts (menu names, other items) + if (HasIdConflict(sliderId, "AddSlider")) + { + return; + } + + // Check for item conflicts within this menu + if (HasItemConflict(menu, sliderId, "AddSlider")) + { + return; + } + + // Apply defaults for optional parameters + if (string.IsNullOrEmpty(sliderLabel)) + sliderLabel = "Slider"; + if (string.IsNullOrEmpty(description)) + description = ""; + + // Validate defaultValue range (assuming 0-10 range) + if (defaultValue < 0 || defaultValue > 10) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Invalid defaultValue {defaultValue} for slider {sliderId}. Using 0 instead."); + defaultValue = 0; + } + + var menuSliderItem = new MenuSliderItem(sliderLabel, description, 0, 10, defaultValue, false) + { + ItemData = sliderId + }; + + if (callbackObj != null) + { + // Handle both C# delegates and Lua functions + if (callbackObj is CallbackDelegate callback) + { + // Listen for position changes + menu.OnSliderPositionChange += (sender, item, oldPosition, newPosition, itemIndex) => + { + if (item == menuSliderItem) + { + callback.Invoke(oldPosition, newPosition); + } + }; + } + else + { + // Handle Lua functions and dynamic callbacks + menu.OnSliderPositionChange += (sender, item, oldPosition, newPosition, itemIndex) => + { + if (item == menuSliderItem) + { + try + { + dynamic dynamicCallback = callbackObj; + dynamicCallback(oldPosition, newPosition); + } + catch (Exception ex) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking callback for slider {sliderId}: {ex.Message}"); + } + } + }; + } + } + + menu.AddMenuItem(menuSliderItem); + menu.RefreshIndex(); + } + + /// + /// Adds a spacer/separator to a menu + /// + /// Target menu ID + /// Unique spacer identifier + /// Spacer display text + /// Optional spacer description + private void AddSpacer(string menuId, string spacerId, string spacerText, string description) + { + // Validate required parameters + if (string.IsNullOrEmpty(menuId)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] AddSpacer: menuId cannot be null or empty."); + return; + } + if (string.IsNullOrEmpty(spacerId)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] AddSpacer: spacerId cannot be null or empty."); + return; + } + + Menu menu = null; + if (!DynamicMenus.TryGetValue(menuId, out menu)) + { + menu = GetMenu(menuId); + } + + if (menu == null) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Menu with ID {menuId} not found."); + return; + } + + // Check for ID conflicts (menu names, other items) + if (HasIdConflict(spacerId, "AddSpacer")) + { + return; + } + + // Check for item conflicts within this menu + if (HasItemConflict(menu, spacerId, "AddSpacer")) + { + return; + } + + // Apply defaults for optional parameters + if (string.IsNullOrEmpty(spacerText)) + spacerText = "---"; + if (string.IsNullOrEmpty(description)) + description = ""; + + var spacerItem = CommonFunctions.GetSpacerMenuItem(spacerText, description); + spacerItem.ItemData = spacerId; + menu.AddMenuItem(spacerItem); + } + + /// + /// Refreshes a menu's display + /// + /// Target menu ID + private void RefreshMenu(string menuId) + { + // Validate required parameters + if (string.IsNullOrEmpty(menuId)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] RefreshMenu: menuId cannot be null or empty."); + return; + } + + Menu menu = null; + if (!DynamicMenus.TryGetValue(menuId, out menu)) + { + menu = GetMenu(menuId); + } + + if (menu == null) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Menu with ID {menuId} not found."); + return; + } + + menu.RefreshIndex(); + } + + /// + /// Closes all open menus + /// + private void CloseAllMenus() + { + MenuController.CloseAllMenus(); + } + + /// + /// Closes a specific menu + /// + /// Menu ID to close + private void CloseMenu(string menuId) + { + // Validate required parameters + if (string.IsNullOrEmpty(menuId)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] CloseMenu: menuId cannot be null or empty."); + return; + } + + Menu menu = null; + if (!DynamicMenus.TryGetValue(menuId, out menu)) + { + menu = GetMenu(menuId); + } + + if (menu == null) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Menu with ID {menuId} not found."); + return; + } + + if (menu.Visible) + { + menu.CloseMenu(); + } + } + + /// + /// Adds a submenu button to link a parent menu to a submenu + /// + /// Parent menu ID + /// Unique button identifier + /// Submenu ID to link to + /// Button display text + /// Button description + /// Optional callback for button selection + private void AddSubmenuButton(string parentMenuId, string buttonId, string submenuId, string buttonLabel, string buttonDescription, object callbackObj = null) + { + // Validate required parameters + if (string.IsNullOrEmpty(parentMenuId)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] AddSubmenuButton: parentMenuId cannot be null or empty."); + return; + } + + + + if (string.IsNullOrEmpty(buttonId)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] AddSubmenuButton: buttonId cannot be null or empty."); + return; + } + if (string.IsNullOrEmpty(submenuId)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] AddSubmenuButton: submenuId cannot be null or empty."); + return; + } + + // Check for ID conflicts for buttonId + if (HasIdConflict(buttonId, "AddSubmenuButton")) + { + return; + } + + // Verify submenu exists (either dynamic or built-in) + if (!DynamicMenus.ContainsKey(submenuId) && GetMenu(submenuId) == null) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] AddSubmenuButton: Submenu '{submenuId}' does not exist. Create it first."); + return; + } + + + Menu parentMenu = null; + if (!DynamicMenus.TryGetValue(parentMenuId, out parentMenu)) + { + parentMenu = GetMenu(parentMenuId); + } + + Menu submenu = null; + if (!DynamicMenus.TryGetValue(submenuId, out submenu)) + { + submenu = GetMenu(submenuId); + } + + if (parentMenu == null) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Parent menu {parentMenuId} not found."); + return; + } + + if (submenu == null) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Submenu {submenuId} not found."); + return; + } + + // Check for item conflicts within parent menu + if (HasItemConflict(parentMenu, buttonId, "AddSubmenuButton")) + { + return; + } + + // Apply defaults for optional parameters + if (string.IsNullOrEmpty(buttonLabel)) + buttonLabel = "Submenu"; + if (string.IsNullOrEmpty(buttonDescription)) + buttonDescription = ""; + + var submenuButton = new MenuItem(buttonLabel, buttonDescription) + { + Label = "→→→", + ItemData = buttonId + }; + + parentMenu.AddMenuItem(submenuButton); + MenuController.AddSubmenu(parentMenu, submenu); + MenuController.BindMenuItem(parentMenu, submenu, submenuButton); + submenu.RefreshIndex(); + + if (callbackObj != null) + { + // Handle both C# delegates and Lua functions + if (callbackObj is CallbackDelegate callback) + { + parentMenu.OnItemSelect += (sender, item, index) => + { + if (item == submenuButton) + { + callback.Invoke(); + } + }; + } + else if (callbackObj is Func luaCallback) + { + parentMenu.OnItemSelect += (sender, item, index) => + { + if (item == submenuButton) + { + try + { + luaCallback.Invoke(); + } + catch (Exception ex) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking callback for submenu button {buttonLabel}: {ex.Message}"); + } + } + }; + } + else + { + // Try to invoke as dynamic for other callback types + parentMenu.OnItemSelect += (sender, item, index) => + { + if (item == submenuButton) + { + try + { + dynamic dynamicCallback = callbackObj; + dynamicCallback(); + } + catch (Exception ex) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking dynamic callback for submenu button {buttonLabel}: {ex.Message}"); + } + } + }; + } + } + + parentMenu.RefreshIndex(); + } + + /// + /// Removes an item from a menu + /// + /// Target menu ID + /// Item identifier to remove + private void RemoveItem(string menuId, string itemId) + { + // Validate required parameters + if (string.IsNullOrEmpty(menuId)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] RemoveItem: menuId cannot be null or empty."); + return; + } + if (string.IsNullOrEmpty(itemId)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] RemoveItem: itemId cannot be null or empty."); + return; + } + + Menu menu = null; + if (!DynamicMenus.TryGetValue(menuId, out menu)) + { + menu = GetMenu(menuId); + } + + if (menu == null) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Menu with ID {menuId} not found."); + return; + } + + // Find the item to remove, regardless of its type + var itemToRemove = menu.GetMenuItems().Find(item => item.ItemData as string == itemId); + + if (itemToRemove != null) + { + menu.RemoveMenuItem(itemToRemove); + } + else + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Item with ID {itemId} not found in menu {menuId}."); + } + } + + /// + /// Clears all items from a menu + /// + /// Target menu ID + private void ClearMenu(string menuId) + { + // Validate required parameters + if (string.IsNullOrEmpty(menuId)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] ClearMenu: menuId cannot be null or empty."); + return; + } + + Menu menu = null; + if (!DynamicMenus.TryGetValue(menuId, out menu)) + { + menu = GetMenu(menuId); + } + + if (menu == null) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Menu with ID {menuId} not found."); + return; + } + + menu.ClearMenuItems(); + } + + /// + /// Checks if a menu exists + /// + /// Menu ID to check + /// True if menu exists, false otherwise + private bool CheckMenu(string menuId) + { + // Validate required parameters + if (string.IsNullOrEmpty(menuId)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] CheckMenu: menuId cannot be null or empty."); + return false; + } + + Menu menu = null; + if (!DynamicMenus.TryGetValue(menuId, out menu)) + { + menu = GetMenu(menuId); + } + + if (menu == null) + { + return false; + } + return true; + } + + /// + /// Deletes a dynamically created menu + /// + /// Menu ID to delete + private void DeleteMenu(string menuId) + { + // Validate required parameters + if (string.IsNullOrEmpty(menuId)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] DeleteMenu: menuId cannot be null or empty."); + return; + } + + Menu menu = null; + if (!DynamicMenus.TryGetValue(menuId, out menu)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Menu with ID {menuId} not found."); + return; + } + + // Close the menu if it's currently open + if (menu.Visible) + { + menu.CloseMenu(); + } + + // Clear all menu items (handles all item cleanup including submenu buttons) + menu.ClearMenuItems(); + + // Remove from MenuController's menu collection + if (MenuController.Menus.Contains(menu)) + { + MenuController.Menus.Remove(menu); + } + + // Remove from dynamic menus dictionary + DynamicMenus.Remove(menuId); + + // Set menu reference to null for garbage collection + menu = null; + } + + + /// + /// Dynamically finds built-in vMenu menus by ID with permission checking + /// + /// Menu identifier string + /// Menu instance if found and permitted, null otherwise + private Menu GetMenu(string menuId) + { + // Handle core menu shortcuts first + switch (menuId.ToLower()) + { + case "main": return Menu; + case "player": return PlayerSubmenu; + case "vehicle": return VehicleSubmenu; + case "world": + return ((IsAllowed(Permission.TOMenu) && GetSettingsBool(Setting.vmenu_enable_time_sync)) || + (IsAllowed(Permission.WOMenu) && GetSettingsBool(Setting.vmenu_enable_weather_sync))) ? WorldSubmenu : null; + } + + // Dynamically search all registered menus + foreach (var menu in MenuController.Menus) + { + // Skip if this is a dynamic menu (user-created) + if (DynamicMenus.ContainsValue(menu)) continue; + + // Try to match by menu title (converted to lowercase, spaces removed) + var normalizedTitle = menu.MenuTitle.ToLower().Replace(" ", "").Replace("-", ""); + var normalizedId = menuId.ToLower().Replace(" ", "").Replace("-", ""); + + if (normalizedTitle.Contains(normalizedId) || normalizedId.Contains(normalizedTitle)) + { + // Basic permission check for known menu patterns + if (IsMenuPermitted(menu, menuId.ToLower())) + { + return menu; + } + } + } + + return null; + } + + /// + /// Checks if a menu is permitted based on vMenu permissions + /// + /// Menu to check + /// Menu identifier + /// True if permitted, false otherwise + private bool IsMenuPermitted(Menu menu, string menuId) + { + // Common permission patterns based on menu titles/IDs + return menuId switch + { + var id when id.Contains("player") && id.Contains("option") => IsAllowed(Permission.POMenu), + var id when id.Contains("online") && id.Contains("player") => IsAllowed(Permission.OPMenu), + var id when id.Contains("banned") => IsAllowed(Permission.OPUnban) || IsAllowed(Permission.OPViewBannedPlayers), + var id when id.Contains("personal") && id.Contains("vehicle") => IsAllowed(Permission.PVMenu), + var id when id.Contains("vehicle") && id.Contains("option") => IsAllowed(Permission.VOMenu), + var id when id.Contains("vehicle") && id.Contains("spawn") => IsAllowed(Permission.VSMenu), + var id when id.Contains("saved") && id.Contains("vehicle") => IsAllowed(Permission.SVMenu), + var id when id.Contains("player") && id.Contains("appearance") => IsAllowed(Permission.PAMenu), + var id when id.Contains("ped") && id.Contains("customization") => IsAllowed(Permission.PAMenu), + var id when id.Contains("time") => IsAllowed(Permission.TOMenu) && GetSettingsBool(Setting.vmenu_enable_time_sync), + var id when id.Contains("weather") => IsAllowed(Permission.WOMenu) && GetSettingsBool(Setting.vmenu_enable_weather_sync), + var id when id.Contains("weapon") && id.Contains("option") => IsAllowed(Permission.WPMenu), + var id when id.Contains("weapon") && id.Contains("loadout") => IsAllowed(Permission.WLMenu), + var id when id.Contains("voice") => IsAllowed(Permission.VCMenu), + // Allow access to menus without specific restrictions + var id when id.Contains("recording") || id.Contains("misc") || id.Contains("about") => true, + // Default to allowing access for unrecognized menus + _ => true + }; + } + + #endregion + } +} diff --git a/vMenu/MainMenu.cs b/vMenu/MainMenu.cs index 45f0d7086..a5ae526af 100644 --- a/vMenu/MainMenu.cs +++ b/vMenu/MainMenu.cs @@ -17,7 +17,7 @@ namespace vMenuClient { - public class MainMenu : BaseScript + public partial class MainMenu : BaseScript { #region Variables @@ -345,6 +345,9 @@ public MainMenu() // Request server state from the server. TriggerServerEvent("vMenu:RequestServerState"); + + // Initialize dynamic menu exports + InitializeDynamicMenuExports(); } #region Infinity bits From eec4e1565dc356479782658da160952aa5d93ac8 Mon Sep 17 00:00:00 2001 From: Tommy Johnston Date: Sat, 18 Oct 2025 14:33:32 -0400 Subject: [PATCH 2/9] Update Exports.cs --- vMenu/Exports.cs | 89 +++++++++++++++++++++++++++++++----------------- 1 file changed, 58 insertions(+), 31 deletions(-) diff --git a/vMenu/Exports.cs b/vMenu/Exports.cs index 538b91278..b1eff9922 100644 --- a/vMenu/Exports.cs +++ b/vMenu/Exports.cs @@ -125,6 +125,9 @@ private void RegisterDynamicMenuExports() // Notifications Exports.Add("Notify", new Action(notifExp)); + + // Menu Information + Exports.Add("GetAllMenuIds", new Func(GetAllMenuIds)); } #endregion @@ -1151,26 +1154,14 @@ private void DeleteMenu(string menuId) /// Menu instance if found and permitted, null otherwise private Menu GetMenu(string menuId) { - // Handle core menu shortcuts first - switch (menuId.ToLower()) - { - case "main": return Menu; - case "player": return PlayerSubmenu; - case "vehicle": return VehicleSubmenu; - case "world": - return ((IsAllowed(Permission.TOMenu) && GetSettingsBool(Setting.vmenu_enable_time_sync)) || - (IsAllowed(Permission.WOMenu) && GetSettingsBool(Setting.vmenu_enable_weather_sync))) ? WorldSubmenu : null; - } - - // Dynamically search all registered menus - foreach (var menu in MenuController.Menus) + // Search all registered menus (built-in and dynamic) - find first matching unique menu + var uniqueMenus = MenuController.Menus.GroupBy(m => m.MenuSubtitle ?? m.MenuTitle).Select(g => g.First()).ToList(); + foreach (var menu in uniqueMenus) { - // Skip if this is a dynamic menu (user-created) - if (DynamicMenus.ContainsValue(menu)) continue; - - // Try to match by menu title (converted to lowercase, spaces removed) - var normalizedTitle = menu.MenuTitle.ToLower().Replace(" ", "").Replace("-", ""); - var normalizedId = menuId.ToLower().Replace(" ", "").Replace("-", ""); + // Try to match by menu subtitle (or title if subtitle is null) + var menuIdentifier = menu.MenuSubtitle ?? menu.MenuTitle ?? ""; + var normalizedTitle = menuIdentifier.ToLower().Replace(" ", "-"); + var normalizedId = menuId.ToLower().Replace(" ", "-"); if (normalizedTitle.Contains(normalizedId) || normalizedId.Contains(normalizedTitle)) { @@ -1185,6 +1176,40 @@ private Menu GetMenu(string menuId) return null; } + /// + /// Export function to get all available menu IDs + /// + /// Array of menu IDs that can be accessed through GetMenu + private string[] GetAllMenuIds() + { + var menuIds = new List(); + + // Add dynamic menu IDs + foreach (var kvp in DynamicMenus) + { + menuIds.Add(kvp.Key); + } + + // Add built-in menu IDs based on their subtitles (avoid duplicates) + var uniqueMenus = MenuController.Menus.GroupBy(m => m.MenuSubtitle ?? m.MenuTitle).Select(g => g.First()).ToList(); + foreach (var menu in uniqueMenus) + { + // Skip dynamic menus (already added above) + if (DynamicMenus.ContainsValue(menu)) continue; + + // Check if menu is permitted + var menuIdentifier = menu.MenuSubtitle ?? menu.MenuTitle ?? ""; + var normalizedTitle = menuIdentifier.ToLower().Replace(" ", "-"); + if (IsMenuPermitted(menu, normalizedTitle)) + { + menuIds.Add(normalizedTitle); + } + } + + // Remove duplicates and sort + return menuIds.Distinct().OrderBy(id => id).ToArray(); + } + /// /// Checks if a menu is permitted based on vMenu permissions /// @@ -1196,22 +1221,24 @@ private bool IsMenuPermitted(Menu menu, string menuId) // Common permission patterns based on menu titles/IDs return menuId switch { - var id when id.Contains("player") && id.Contains("option") => IsAllowed(Permission.POMenu), - var id when id.Contains("online") && id.Contains("player") => IsAllowed(Permission.OPMenu), - var id when id.Contains("banned") => IsAllowed(Permission.OPUnban) || IsAllowed(Permission.OPViewBannedPlayers), - var id when id.Contains("personal") && id.Contains("vehicle") => IsAllowed(Permission.PVMenu), - var id when id.Contains("vehicle") && id.Contains("option") => IsAllowed(Permission.VOMenu), - var id when id.Contains("vehicle") && id.Contains("spawn") => IsAllowed(Permission.VSMenu), + "player-options" => IsAllowed(Permission.POMenu), + "online-players" => IsAllowed(Permission.OPMenu), + var id when id.Contains("banned-players") => IsAllowed(Permission.OPUnban) || IsAllowed(Permission.OPViewBannedPlayers), + "personal-vehicle-options" => IsAllowed(Permission.PVMenu), + "vehicle-options" => IsAllowed(Permission.VOMenu), + "vehicle-spawner" => IsAllowed(Permission.VSMenu), var id when id.Contains("saved") && id.Contains("vehicle") => IsAllowed(Permission.SVMenu), - var id when id.Contains("player") && id.Contains("appearance") => IsAllowed(Permission.PAMenu), - var id when id.Contains("ped") && id.Contains("customization") => IsAllowed(Permission.PAMenu), + "player-appearance" or "character-appearance-options" => IsAllowed(Permission.PAMenu), + "mp-ped-customization" => IsAllowed(Permission.PAMenu), var id when id.Contains("time") => IsAllowed(Permission.TOMenu) && GetSettingsBool(Setting.vmenu_enable_time_sync), var id when id.Contains("weather") => IsAllowed(Permission.WOMenu) && GetSettingsBool(Setting.vmenu_enable_weather_sync), - var id when id.Contains("weapon") && id.Contains("option") => IsAllowed(Permission.WPMenu), - var id when id.Contains("weapon") && id.Contains("loadout") => IsAllowed(Permission.WLMenu), - var id when id.Contains("voice") => IsAllowed(Permission.VCMenu), + var id when id == "world" || id.Contains("world") => (IsAllowed(Permission.TOMenu) && GetSettingsBool(Setting.vmenu_enable_time_sync)) || + (IsAllowed(Permission.WOMenu) && GetSettingsBool(Setting.vmenu_enable_weather_sync)), + "weapon-options" => IsAllowed(Permission.WPMenu), + var id when id.Contains("weapon-loadouts") => IsAllowed(Permission.WLMenu), + "voice-chat-settings" => IsAllowed(Permission.VCMenu), // Allow access to menus without specific restrictions - var id when id.Contains("recording") || id.Contains("misc") || id.Contains("about") => true, + var id when id.Contains("recording-options") || id.Contains("misc-settings") || id.Contains("about-vmenu") => true, // Default to allowing access for unrecognized menus _ => true }; From 72108713226060be992f56898969b9ef029ee526 Mon Sep 17 00:00:00 2001 From: Tommy Johnston Date: Sat, 18 Oct 2025 19:40:04 -0400 Subject: [PATCH 3/9] fix: fuzzy id finding --- vMenu/Exports.cs | 240 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 196 insertions(+), 44 deletions(-) diff --git a/vMenu/Exports.cs b/vMenu/Exports.cs index b1eff9922..7612846aa 100644 --- a/vMenu/Exports.cs +++ b/vMenu/Exports.cs @@ -58,17 +58,31 @@ private bool HasIdConflict(string id, string context) /// True if it matches a built-in menu ID private bool IsBuiltInMenuId(string id) { - return id.ToLower() switch - { - "main" or "player" or "vehicle" or "world" or - "playeroptions" or "onlineplayers" or "bannedplayers" or - "personalvehicle" or "vehicleoptions" or "vehiclespawner" or - "savedvehicles" or "playerappearance" or "mppedcustomization" or - "timeoptions" or "weatheroptions" or "weaponoptions" or - "weaponloadouts" or "recording" or "miscsettings" or - "voicechat" or "about" => true, - _ => false - }; + if (string.IsNullOrEmpty(id)) + { + return false; + } + + var normalizedId = id.ToLower().Replace(" ", "-"); + + // Get all built-in menu IDs based on their subtitles (same logic as GetAllMenuIds) + var uniqueMenus = MenuController.Menus.GroupBy(m => m.MenuSubtitle ?? m.MenuTitle).Select(g => g.First()).ToList(); + foreach (var menu in uniqueMenus) + { + // Skip dynamic menus + if (DynamicMenus.ContainsValue(menu)) continue; + + // Check if menu identifier matches + var menuIdentifier = menu.MenuSubtitle ?? menu.MenuTitle ?? ""; + var normalizedTitle = menuIdentifier.ToLower().Replace(" ", "-"); + + if (normalizedTitle == normalizedId) + { + return true; + } + } + + return false; } /// @@ -113,7 +127,7 @@ private void RegisterDynamicMenuExports() Exports.Add("DeleteMenu", new Action(DeleteMenu)); // Add Menu Items - Exports.Add("AddButton", new Action(AddButton)); + Exports.Add("AddButton", new Action(AddButton)); Exports.Add("AddList", new Action(AddList)); Exports.Add("AddCheckbox", new Action(AddCheckbox)); Exports.Add("AddSlider", new Action(AddSlider)); @@ -121,7 +135,7 @@ private void RegisterDynamicMenuExports() Exports.Add("AddSubmenuButton", new Action(AddSubmenuButton)); // Modify Menu Items - Exports.Add("RemoveItem", new Action(RemoveItem)); + Exports.Add("RemoveItem", new Action(RemoveItem)); // Notifications Exports.Add("Notify", new Action(notifExp)); @@ -161,7 +175,9 @@ private void CreateMenu(string menuId, string menuTitle, string menuDescription, if (string.IsNullOrEmpty(menuDescription)) menuDescription = ""; - var newMenu = new Menu(Game.Player.Name, menuTitle) + // Cache player name at creation time to prevent dynamic changes + var playerName = Game.Player.Name; + var newMenu = new Menu(playerName, menuTitle) { MenuSubtitle = menuDescription }; @@ -175,7 +191,22 @@ private void CreateMenu(string menuId, string menuTitle, string menuDescription, { newMenu.OnMenuOpen += (sender) => { - callback.Invoke(); + try + { + BaseScript.TriggerEvent("vMenu:DelayedCallback", new Action(() => callback.Invoke())); + } + catch (System.IO.EndOfStreamException) + { + // Silently ignore - callback was deleted/garbage collected + } + catch (Exception ex) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking menu open callback for {menuId}: {ex.Message}"); + if (ex.InnerException != null) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Inner exception: {ex.InnerException.Message}"); + } + } }; } else @@ -185,12 +216,27 @@ private void CreateMenu(string menuId, string menuTitle, string menuDescription, { try { + // Validate callback before invoking + if (callbackObj == null) + { + return; + } + dynamic dynamicCallback = callbackObj; dynamicCallback(); } + catch (System.IO.EndOfStreamException) + { + // Silently ignore - callback was deleted/garbage collected + } catch (Exception ex) { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking menu open callback for {menuId}: {ex.Message}"); + CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking menu open callback for {menuId}: {ex.GetType().Name} - {ex.Message}"); + if (ex.InnerException != null) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Inner exception: {ex.InnerException.GetType().Name} - {ex.InnerException.Message}"); + } + CitizenFX.Core.Debug.WriteLine($"[vMenu] Stack trace: {ex.StackTrace}"); } }; } @@ -243,23 +289,43 @@ private void OpenMenu(string menuId) if (menu == null) { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Menu with ID {menuId} not found."); + CitizenFX.Core.Debug.WriteLine($"[vMenu] OpenMenu: Menu with ID '{menuId}' not found. Available menus: {string.Join(", ", GetAllMenuIds())}"); return; } + // Close all currently open menus before opening the new one + MenuController.CloseAllMenus(); + + // Open the requested menu menu.OpenMenu(); } /// - /// Adds a button to a menu + /// Adds a button item to a menu /// /// Target menu ID /// Unique button identifier - /// Button display text + /// Button label /// Button description - /// Optional callback for button selection - private void AddButton(string menuId, string buttonId, string buttonLabel, string buttonDescription, object callbackObj = null) + /// Either rightLabel (string) or callback (object) + /// Optional callback if param5 is a string + private void AddButton(string menuId, string buttonId, string buttonLabel, string buttonDescription, object param5 = null, object param6 = null) { + // Handle backwards compatibility: param5 can be either rightLabel (string) or callback (object) + string rightLabel = null; + object callbackObj = null; + + if (param5 is string label) + { + // New signature: AddButton(menuId, buttonId, label, desc, rightLabel, callback) + rightLabel = label; + callbackObj = param6; + } + else + { + // Old signature: AddButton(menuId, buttonId, label, desc, callback) + callbackObj = param5; + } // Validate required parameters if (string.IsNullOrEmpty(buttonId)) { @@ -299,7 +365,8 @@ private void AddButton(string menuId, string buttonId, string buttonLabel, strin var menuItem = new MenuItem(buttonLabel, buttonDescription) { - ItemData = buttonId + ItemData = buttonId, + Label = rightLabel ?? "" }; if (callbackObj != null) @@ -307,24 +374,30 @@ private void AddButton(string menuId, string buttonId, string buttonLabel, strin // Handle both C# delegates and Lua functions if (callbackObj is CallbackDelegate callback) { - menu.OnItemSelect += (sender, item, index) => + menu.OnItemSelect += async (sender, item, index) => { if (item == menuItem) { + await BaseScript.Delay(0); callback.Invoke(); } }; } else if (callbackObj is Func luaCallback) { - menu.OnItemSelect += (sender, item, index) => + menu.OnItemSelect += async (sender, item, index) => { if (item == menuItem) { + await BaseScript.Delay(0); try { luaCallback.Invoke(); } + catch (System.IO.EndOfStreamException) + { + // Silently ignore - callback was deleted/garbage collected + } catch (Exception ex) { CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking callback for button {buttonId}: {ex.Message}"); @@ -335,15 +408,20 @@ private void AddButton(string menuId, string buttonId, string buttonLabel, strin else { // Try to invoke as dynamic for other callback types - menu.OnItemSelect += (sender, item, index) => + menu.OnItemSelect += async (sender, item, index) => { if (item == menuItem) { + await BaseScript.Delay(0); try { dynamic dynamicCallback = callbackObj; dynamicCallback(); } + catch (System.IO.EndOfStreamException) + { + // Silently ignore - callback was deleted/garbage collected + } catch (Exception ex) { CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking dynamic callback for button {buttonId}: {ex.Message}"); @@ -488,6 +566,10 @@ private void AddList(string menuId, string listId, string listLabel, object opti dynamic dynamicCallback = callbackObj; dynamicCallback(false, currentValue, newIndex, oldIndex); } + catch (System.IO.EndOfStreamException) + { + // Silently ignore - callback was deleted/garbage collected + } catch (Exception ex) { CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking callback for list {listId}: {ex.Message}"); @@ -505,6 +587,10 @@ private void AddList(string menuId, string listId, string listLabel, object opti dynamic dynamicCallback = callbackObj; dynamicCallback(true, selectedValue, listIndex, listIndex); } + catch (System.IO.EndOfStreamException) + { + // Silently ignore - callback was deleted/garbage collected + } catch (Exception ex) { CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking callback for list {listId}: {ex.Message}"); @@ -600,6 +686,10 @@ private void AddCheckbox(string menuId, string checkboxId, string checkboxLabel, dynamic dynamicCallback = luaCallback; dynamicCallback(_checked); } + catch (System.IO.EndOfStreamException) + { + // Silently ignore - callback was deleted/garbage collected + } catch (Exception ex) { CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking callback for checkbox {checkboxId}: {ex.Message}"); @@ -619,6 +709,10 @@ private void AddCheckbox(string menuId, string checkboxId, string checkboxLabel, dynamic dynamicCallback = callbackObj; dynamicCallback(_checked); } + catch (System.IO.EndOfStreamException) + { + // Silently ignore - callback was deleted/garbage collected + } catch (Exception ex) { CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking dynamic callback for checkbox {checkboxId}: {ex.Message}"); @@ -723,6 +817,10 @@ private void AddSlider(string menuId, string sliderId, string sliderLabel, strin dynamic dynamicCallback = callbackObj; dynamicCallback(oldPosition, newPosition); } + catch (System.IO.EndOfStreamException) + { + // Silently ignore - callback was deleted/garbage collected + } catch (Exception ex) { CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking callback for slider {sliderId}: {ex.Message}"); @@ -956,24 +1054,30 @@ private void AddSubmenuButton(string parentMenuId, string buttonId, string subme // Handle both C# delegates and Lua functions if (callbackObj is CallbackDelegate callback) { - parentMenu.OnItemSelect += (sender, item, index) => + parentMenu.OnItemSelect += async (sender, item, index) => { if (item == submenuButton) { + await BaseScript.Delay(0); callback.Invoke(); } }; } else if (callbackObj is Func luaCallback) { - parentMenu.OnItemSelect += (sender, item, index) => + parentMenu.OnItemSelect += async (sender, item, index) => { if (item == submenuButton) { + await BaseScript.Delay(0); try { luaCallback.Invoke(); } + catch (System.IO.EndOfStreamException) + { + // Silently ignore - callback was deleted/garbage collected + } catch (Exception ex) { CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking callback for submenu button {buttonLabel}: {ex.Message}"); @@ -984,15 +1088,20 @@ private void AddSubmenuButton(string parentMenuId, string buttonId, string subme else { // Try to invoke as dynamic for other callback types - parentMenu.OnItemSelect += (sender, item, index) => + parentMenu.OnItemSelect += async (sender, item, index) => { if (item == submenuButton) { + await BaseScript.Delay(0); try { dynamic dynamicCallback = callbackObj; dynamicCallback(); } + catch (System.IO.EndOfStreamException) + { + // Silently ignore - callback was deleted/garbage collected + } catch (Exception ex) { CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking dynamic callback for submenu button {buttonLabel}: {ex.Message}"); @@ -1006,11 +1115,11 @@ private void AddSubmenuButton(string parentMenuId, string buttonId, string subme } /// - /// Removes an item from a menu + /// Removes an item from a menu by ID (string) or index (int) /// /// Target menu ID - /// Item identifier to remove - private void RemoveItem(string menuId, string itemId) + /// Item identifier (string) or index (int) to remove + private void RemoveItem(string menuId, object itemIdOrIndex) { // Validate required parameters if (string.IsNullOrEmpty(menuId)) @@ -1018,9 +1127,9 @@ private void RemoveItem(string menuId, string itemId) CitizenFX.Core.Debug.WriteLine($"[vMenu] RemoveItem: menuId cannot be null or empty."); return; } - if (string.IsNullOrEmpty(itemId)) + if (itemIdOrIndex == null) { - CitizenFX.Core.Debug.WriteLine($"[vMenu] RemoveItem: itemId cannot be null or empty."); + CitizenFX.Core.Debug.WriteLine($"[vMenu] RemoveItem: itemIdOrIndex cannot be null."); return; } @@ -1036,16 +1145,42 @@ private void RemoveItem(string menuId, string itemId) return; } - // Find the item to remove, regardless of its type - var itemToRemove = menu.GetMenuItems().Find(item => item.ItemData as string == itemId); + var items = menu.GetMenuItems(); - if (itemToRemove != null) + // Check if it's an int (index-based removal) + if (itemIdOrIndex is int index) + { + if (index < 0 || index >= items.Count) + { + // Silent fail for out of range index (used for clearing menus in loops) + return; + } + menu.RemoveMenuItem(items[index]); + } + // Otherwise treat as string (ID-based removal) + else if (itemIdOrIndex is string itemId) { - menu.RemoveMenuItem(itemToRemove); + if (string.IsNullOrEmpty(itemId)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] RemoveItem: itemId cannot be empty."); + return; + } + + // Find the item to remove by ItemData + var itemToRemove = items.Find(item => item.ItemData as string == itemId); + + if (itemToRemove != null) + { + menu.RemoveMenuItem(itemToRemove); + } + else + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Item with ID {itemId} not found in menu {menuId}."); + } } else { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Item with ID {itemId} not found in menu {menuId}."); + CitizenFX.Core.Debug.WriteLine($"[vMenu] RemoveItem: itemIdOrIndex must be a string or int."); } } @@ -1063,14 +1198,20 @@ private void ClearMenu(string menuId) } Menu menu = null; - if (!DynamicMenus.TryGetValue(menuId, out menu)) + + // First check dynamic menus + if (DynamicMenus.TryGetValue(menuId, out menu)) { - menu = GetMenu(menuId); + menu.ClearMenuItems(); + return; } + // Then check built-in menus using GetMenu + menu = GetMenu(menuId); + if (menu == null) { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Menu with ID {menuId} not found."); + CitizenFX.Core.Debug.WriteLine($"[vMenu] ClearMenu: Menu with ID '{menuId}' not found. Available menus: {string.Join(", ", GetAllMenuIds())}"); return; } @@ -1154,16 +1295,27 @@ private void DeleteMenu(string menuId) /// Menu instance if found and permitted, null otherwise private Menu GetMenu(string menuId) { - // Search all registered menus (built-in and dynamic) - find first matching unique menu + // First check dynamic menus by exact ID + if (DynamicMenus.TryGetValue(menuId, out Menu dynamicMenu)) + { + return dynamicMenu; + } + + // Search built-in menus by normalized subtitle var uniqueMenus = MenuController.Menus.GroupBy(m => m.MenuSubtitle ?? m.MenuTitle).Select(g => g.First()).ToList(); + var normalizedId = menuId.ToLower().Replace(" ", "-"); + foreach (var menu in uniqueMenus) { + // Skip dynamic menus (already checked above) + if (DynamicMenus.ContainsValue(menu)) continue; + // Try to match by menu subtitle (or title if subtitle is null) var menuIdentifier = menu.MenuSubtitle ?? menu.MenuTitle ?? ""; var normalizedTitle = menuIdentifier.ToLower().Replace(" ", "-"); - var normalizedId = menuId.ToLower().Replace(" ", "-"); - if (normalizedTitle.Contains(normalizedId) || normalizedId.Contains(normalizedTitle)) + // Exact match only + if (normalizedTitle == normalizedId) { // Basic permission check for known menu patterns if (IsMenuPermitted(menu, menuId.ToLower())) From 5cae19e9a690aa68d3b17c8c603228cde1e8d835 Mon Sep 17 00:00:00 2001 From: Tommy Johnston Date: Mon, 20 Oct 2025 13:45:09 -0400 Subject: [PATCH 4/9] refactor: clean up callback handling & permission checks --- vMenu/Exports.cs | 1013 ++++++++++++++++------------------------------ 1 file changed, 345 insertions(+), 668 deletions(-) diff --git a/vMenu/Exports.cs b/vMenu/Exports.cs index 7612846aa..18e7b3cff 100644 --- a/vMenu/Exports.cs +++ b/vMenu/Exports.cs @@ -26,6 +26,8 @@ public partial class MainMenu /// private static Dictionary DynamicMenus = new Dictionary(); + #region Helper Methods + /// /// Checks if an ID conflicts with any existing menu or item /// @@ -102,7 +104,227 @@ private bool HasItemConflict(Menu menu, string itemId, string context) return false; } + /// + /// Validates a string parameter is not null or empty + /// + /// Value to check + /// Parameter name for error message + /// Context for error messages + /// True if valid, false otherwise + private bool ValidateParameter(string value, string paramName, string context) + { + if (string.IsNullOrEmpty(value)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] {context}: {paramName} cannot be null or empty."); + return false; + } + return true; + } + + /// + /// Retrieves a menu by ID from dynamic or built-in menus + /// + /// Menu identifier + /// Menu instance if found, null otherwise + private Menu RetrieveMenu(string menuId) + { + if (DynamicMenus.TryGetValue(menuId, out Menu menu)) + { + return menu; + } + return GetMenu(menuId); + } + + /// + /// Safely invokes a callback with exception handling + /// + /// Callback object to invoke + /// Item identifier for error logging + /// Arguments to pass to callback + private void SafeInvokeCallback(object callbackObj, string itemId, params object[] args) + { + try + { + if (callbackObj == null) return; + + if (callbackObj is CallbackDelegate callback) + { + callback.Invoke(args); + } + else if (callbackObj is Func luaCallback) + { + luaCallback.Invoke(); + } + else + { + dynamic dynamicCallback = callbackObj; + // Dynamic invocation handles variable arguments automatically + switch (args?.Length ?? 0) + { + case 0: + dynamicCallback(); + break; + case 1: + dynamicCallback(args[0]); + break; + case 2: + dynamicCallback(args[0], args[1]); + break; + case 3: + dynamicCallback(args[0], args[1], args[2]); + break; + case 4: + dynamicCallback(args[0], args[1], args[2], args[3]); + break; + default: + CitizenFX.Core.Debug.WriteLine($"[vMenu] Callback for {itemId} has unsupported number of arguments: {args.Length}"); + break; + } + } + } + catch (System.IO.EndOfStreamException) + { + // Silently ignore - callback was deleted/garbage collected + } + catch (Exception ex) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking callback for {itemId}: {ex.GetType().Name} - {ex.Message}"); + } + } + + /// + /// Attaches a callback to a menu item selection event + /// + private void AttachItemSelectCallback(Menu menu, MenuItem item, object callbackObj, string itemId) + { + if (callbackObj == null) return; + + if (callbackObj is CallbackDelegate callback) + { + menu.OnItemSelect += async (sender, menuItem, index) => + { + if (menuItem == item) + { + await BaseScript.Delay(0); + SafeInvokeCallback(callback, itemId); + } + }; + } + else + { + menu.OnItemSelect += async (sender, menuItem, index) => + { + if (menuItem == item) + { + await BaseScript.Delay(0); + SafeInvokeCallback(callbackObj, itemId); + } + }; + } + } + + /// + /// Attaches a callback to a checkbox change event + /// + private void AttachCheckboxCallback(Menu menu, MenuCheckboxItem item, object callbackObj, string itemId) + { + if (callbackObj == null) return; + + menu.OnCheckboxChange += (sender, menuItem, index, _checked) => + { + if (menuItem == item) + { + SafeInvokeCallback(callbackObj, itemId, _checked); + } + }; + } + + /// + /// Attaches callbacks to a list item events + /// + private void AttachListCallbacks(Menu menu, MenuListItem item, List options, object callbackObj, string itemId) + { + if (callbackObj == null) return; + + // Listen for index changes (browsing through list) + menu.OnListIndexChange += (sender, menuItem, oldIndex, newIndex, itemIndex) => + { + if (menuItem == item) + { + var currentValue = options[newIndex]; + SafeInvokeCallback(callbackObj, itemId, false, currentValue, newIndex, oldIndex); + } + }; + + // Listen for item selection (pressing enter/select) + menu.OnListItemSelect += (sender, menuItem, listIndex, itemIndex) => + { + if (menuItem == item) + { + var selectedValue = options[listIndex]; + SafeInvokeCallback(callbackObj, itemId, true, selectedValue, listIndex, listIndex); + } + }; + } + + /// + /// Attaches a callback to a slider position change event + /// + private void AttachSliderCallback(Menu menu, MenuSliderItem item, object callbackObj, string itemId) + { + if (callbackObj == null) return; + + menu.OnSliderPositionChange += (sender, menuItem, oldPosition, newPosition, itemIndex) => + { + if (menuItem == item) + { + SafeInvokeCallback(callbackObj, itemId, oldPosition, newPosition); + } + }; + } + + /// + /// Attaches a callback to a menu open event + /// + private void AttachMenuOpenCallback(Menu menu, object callbackObj, string menuId) + { + if (callbackObj == null) return; + + if (callbackObj is CallbackDelegate callback) + { + menu.OnMenuOpen += (sender) => + { + try + { + BaseScript.TriggerEvent("vMenu:DelayedCallback", new Action(() => callback.Invoke())); + } + catch (System.IO.EndOfStreamException) + { + // Silently ignore - callback was deleted/garbage collected + } + catch (Exception ex) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking menu open callback for {menuId}: {ex.Message}"); + if (ex.InnerException != null) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Inner exception: {ex.InnerException.Message}"); + } + } + }; + } + else + { + menu.OnMenuOpen += (sender) => + { + SafeInvokeCallback(callbackObj, null, menuId); + }; + } + } + + #endregion + #region Export Registration and Initialization + /// /// Initialize dynamic menu exports for plugin usage /// @@ -142,7 +364,10 @@ private void RegisterDynamicMenuExports() // Menu Information Exports.Add("GetAllMenuIds", new Func(GetAllMenuIds)); + Exports.Add("GetMenu", new Func(GetMenuExport)); + Exports.Add("IsMenuPermitted", new Func(IsMenuPermittedExport)); } + #endregion #region Export Implementation Methods @@ -156,18 +381,8 @@ private void RegisterDynamicMenuExports() /// Optional callback for menu open events private void CreateMenu(string menuId, string menuTitle, string menuDescription, object callbackObj) { - // Validate required parameters - if (string.IsNullOrEmpty(menuId)) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] CreateMenu: menuId cannot be null or empty."); - return; - } - - // Check for any ID conflicts - if (HasIdConflict(menuId, "CreateMenu")) - { - return; - } + if (!ValidateParameter(menuId, "menuId", "CreateMenu")) return; + if (HasIdConflict(menuId, "CreateMenu")) return; // Apply defaults for optional parameters if (string.IsNullOrEmpty(menuTitle)) @@ -184,63 +399,7 @@ private void CreateMenu(string menuId, string menuTitle, string menuDescription, MenuController.AddMenu(newMenu); DynamicMenus[menuId] = newMenu; - if (callbackObj != null) - { - // Handle both C# delegates and Lua functions - if (callbackObj is CallbackDelegate callback) - { - newMenu.OnMenuOpen += (sender) => - { - try - { - BaseScript.TriggerEvent("vMenu:DelayedCallback", new Action(() => callback.Invoke())); - } - catch (System.IO.EndOfStreamException) - { - // Silently ignore - callback was deleted/garbage collected - } - catch (Exception ex) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking menu open callback for {menuId}: {ex.Message}"); - if (ex.InnerException != null) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Inner exception: {ex.InnerException.Message}"); - } - } - }; - } - else - { - // Handle Lua functions and dynamic callbacks - newMenu.OnMenuOpen += (sender) => - { - try - { - // Validate callback before invoking - if (callbackObj == null) - { - return; - } - - dynamic dynamicCallback = callbackObj; - dynamicCallback(); - } - catch (System.IO.EndOfStreamException) - { - // Silently ignore - callback was deleted/garbage collected - } - catch (Exception ex) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking menu open callback for {menuId}: {ex.GetType().Name} - {ex.Message}"); - if (ex.InnerException != null) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Inner exception: {ex.InnerException.GetType().Name} - {ex.InnerException.Message}"); - } - CitizenFX.Core.Debug.WriteLine($"[vMenu] Stack trace: {ex.StackTrace}"); - } - }; - } - } + AttachMenuOpenCallback(newMenu, callbackObj, menuId); } /// @@ -250,21 +409,20 @@ private void CreateMenu(string menuId, string menuTitle, string menuDescription, /// Notification type (error, info, success, or default) private void notifExp(string message, string type) { - if (type == "error") - { - Notify.Error(message); - } - else if (type == "info") - { - Notify.Info(message); - } - else if (type == "success") - { - Notify.Success(message); - } - else - { - Notify.Alert(message); + switch (type) + { + case "error": + Notify.Error(message); + break; + case "info": + Notify.Info(message); + break; + case "success": + Notify.Success(message); + break; + default: + Notify.Alert(message); + break; } } @@ -274,18 +432,9 @@ private void notifExp(string message, string type) /// Menu identifier private void OpenMenu(string menuId) { - // Validate required parameters - if (string.IsNullOrEmpty(menuId)) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] OpenMenu: menuId cannot be null or empty."); - return; - } + if (!ValidateParameter(menuId, "menuId", "OpenMenu")) return; - Menu menu = null; - if (!DynamicMenus.TryGetValue(menuId, out menu)) - { - menu = GetMenu(menuId); - } + Menu menu = RetrieveMenu(menuId); if (menu == null) { @@ -326,36 +475,18 @@ private void AddButton(string menuId, string buttonId, string buttonLabel, strin // Old signature: AddButton(menuId, buttonId, label, desc, callback) callbackObj = param5; } - // Validate required parameters - if (string.IsNullOrEmpty(buttonId)) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] AddButton: buttonId cannot be null or empty."); - return; - } - // Check for ID conflicts (menu names, other items) - if (HasIdConflict(buttonId, "AddButton")) - { - return; - } - - Menu menu = null; - if (!DynamicMenus.TryGetValue(menuId, out menu)) - { - menu = GetMenu(menuId); - } + if (!ValidateParameter(buttonId, "buttonId", "AddButton")) return; + if (HasIdConflict(buttonId, "AddButton")) return; + Menu menu = RetrieveMenu(menuId); if (menu == null) { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Menu with ID {menuId} not found."); + CitizenFX.Core.Debug.WriteLine($"[vMenu] AddButton: Menu with ID {menuId} not found."); return; } - // Check for item conflicts within this menu - if (HasItemConflict(menu, buttonId, "AddButton")) - { - return; - } + if (HasItemConflict(menu, buttonId, "AddButton")) return; // Apply defaults for optional parameters if (string.IsNullOrEmpty(buttonLabel)) @@ -369,68 +500,7 @@ private void AddButton(string menuId, string buttonId, string buttonLabel, strin Label = rightLabel ?? "" }; - if (callbackObj != null) - { - // Handle both C# delegates and Lua functions - if (callbackObj is CallbackDelegate callback) - { - menu.OnItemSelect += async (sender, item, index) => - { - if (item == menuItem) - { - await BaseScript.Delay(0); - callback.Invoke(); - } - }; - } - else if (callbackObj is Func luaCallback) - { - menu.OnItemSelect += async (sender, item, index) => - { - if (item == menuItem) - { - await BaseScript.Delay(0); - try - { - luaCallback.Invoke(); - } - catch (System.IO.EndOfStreamException) - { - // Silently ignore - callback was deleted/garbage collected - } - catch (Exception ex) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking callback for button {buttonId}: {ex.Message}"); - } - } - }; - } - else - { - // Try to invoke as dynamic for other callback types - menu.OnItemSelect += async (sender, item, index) => - { - if (item == menuItem) - { - await BaseScript.Delay(0); - try - { - dynamic dynamicCallback = callbackObj; - dynamicCallback(); - } - catch (System.IO.EndOfStreamException) - { - // Silently ignore - callback was deleted/garbage collected - } - catch (Exception ex) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking dynamic callback for button {buttonId}: {ex.Message}"); - } - } - }; - } - } - + AttachItemSelectCallback(menu, menuItem, callbackObj, buttonId); menu.AddMenuItem(menuItem); menu.RefreshIndex(); } @@ -447,36 +517,17 @@ private void AddButton(string menuId, string buttonId, string buttonLabel, strin /// Optional callback for list changes private void AddList(string menuId, string listId, string listLabel, object options, int defaultIndex, string description, object callbackObj = null) { - // Validate required parameters - if (string.IsNullOrEmpty(listId)) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] AddList: listId cannot be null or empty."); - return; - } - - // Check for ID conflicts (menu names, other items) - if (HasIdConflict(listId, "AddList")) - { - return; - } - - Menu menu = null; - if (!DynamicMenus.TryGetValue(menuId, out menu)) - { - menu = GetMenu(menuId); - } + if (!ValidateParameter(listId, "listId", "AddList")) return; + if (HasIdConflict(listId, "AddList")) return; + Menu menu = RetrieveMenu(menuId); if (menu == null) { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Menu with ID {menuId} not found."); + CitizenFX.Core.Debug.WriteLine($"[vMenu] AddList: Menu with ID {menuId} not found."); return; } - // Check for item conflicts within this menu - if (HasItemConflict(menu, listId, "AddList")) - { - return; - } + if (HasItemConflict(menu, listId, "AddList")) return; // Apply defaults for optional parameters if (string.IsNullOrEmpty(listLabel)) @@ -526,80 +577,7 @@ private void AddList(string menuId, string listId, string listLabel, object opti ItemData = listId }; - if (callbackObj != null) - { - // Handle both C# delegates and Lua functions - if (callbackObj is CallbackDelegate callback) - { - // Listen for index changes (browsing through list) - menu.OnListIndexChange += (sender, item, oldIndex, newIndex, itemIndex) => - { - if (item == menuListItem) - { - var currentValue = optionsList[newIndex]; - var oldValue = optionsList[oldIndex]; - callback.Invoke(false, currentValue, newIndex, oldIndex); - } - }; - - // Listen for item selection (pressing enter/select) - menu.OnListItemSelect += (sender, item, listIndex, itemIndex) => - { - if (item == menuListItem) - { - var selectedValue = optionsList[listIndex]; - callback.Invoke(true, selectedValue, listIndex, listIndex); - } - }; - } - else - { - // Handle Lua functions and dynamic callbacks - menu.OnListIndexChange += (sender, item, oldIndex, newIndex, itemIndex) => - { - if (item == menuListItem) - { - try - { - var currentValue = optionsList[newIndex]; - var oldValue = optionsList[oldIndex]; - dynamic dynamicCallback = callbackObj; - dynamicCallback(false, currentValue, newIndex, oldIndex); - } - catch (System.IO.EndOfStreamException) - { - // Silently ignore - callback was deleted/garbage collected - } - catch (Exception ex) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking callback for list {listId}: {ex.Message}"); - } - } - }; - - menu.OnListItemSelect += (sender, item, listIndex, itemIndex) => - { - if (item == menuListItem) - { - try - { - var selectedValue = optionsList[listIndex]; - dynamic dynamicCallback = callbackObj; - dynamicCallback(true, selectedValue, listIndex, listIndex); - } - catch (System.IO.EndOfStreamException) - { - // Silently ignore - callback was deleted/garbage collected - } - catch (Exception ex) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking callback for list {listId}: {ex.Message}"); - } - } - }; - } - } - + AttachListCallbacks(menu, menuListItem, optionsList, callbackObj, listId); menu.AddMenuItem(menuListItem); menu.RefreshIndex(); } @@ -615,41 +593,18 @@ private void AddList(string menuId, string listId, string listLabel, object opti /// Optional callback for checkbox changes private void AddCheckbox(string menuId, string checkboxId, string checkboxLabel, string description, bool defaultValue, object callbackObj = null) { - // Validate required parameters - if (string.IsNullOrEmpty(menuId)) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] AddCheckbox: menuId cannot be null or empty."); - return; - } - if (string.IsNullOrEmpty(checkboxId)) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] AddCheckbox: checkboxId cannot be null or empty."); - return; - } - - Menu menu = null; - if (!DynamicMenus.TryGetValue(menuId, out menu)) - { - menu = GetMenu(menuId); - } + if (!ValidateParameter(menuId, "menuId", "AddCheckbox")) return; + if (!ValidateParameter(checkboxId, "checkboxId", "AddCheckbox")) return; + Menu menu = RetrieveMenu(menuId); if (menu == null) { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Menu with ID {menuId} not found."); + CitizenFX.Core.Debug.WriteLine($"[vMenu] AddCheckbox: Menu with ID {menuId} not found."); return; } - // Check for ID conflicts (menu names, other items) - if (HasIdConflict(checkboxId, "AddCheckbox")) - { - return; - } - - // Check for item conflicts within this menu - if (HasItemConflict(menu, checkboxId, "AddCheckbox")) - { - return; - } + if (HasIdConflict(checkboxId, "AddCheckbox")) return; + if (HasItemConflict(menu, checkboxId, "AddCheckbox")) return; // Apply defaults for optional parameters if (string.IsNullOrEmpty(checkboxLabel)) @@ -662,66 +617,7 @@ private void AddCheckbox(string menuId, string checkboxId, string checkboxLabel, ItemData = checkboxId }; - if (callbackObj != null) - { - // Handle both C# delegates and Lua functions - if (callbackObj is CallbackDelegate callback) - { - menu.OnCheckboxChange += (sender, item, index, _checked) => - { - if (item == menuCheckboxItem) - { - callback.Invoke(_checked); - } - }; - } - else if (callbackObj is Func luaCallback) - { - menu.OnCheckboxChange += (sender, item, index, _checked) => - { - if (item == menuCheckboxItem) - { - try - { - dynamic dynamicCallback = luaCallback; - dynamicCallback(_checked); - } - catch (System.IO.EndOfStreamException) - { - // Silently ignore - callback was deleted/garbage collected - } - catch (Exception ex) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking callback for checkbox {checkboxId}: {ex.Message}"); - } - } - }; - } - else - { - // Try to invoke as dynamic for other callback types - menu.OnCheckboxChange += (sender, item, index, _checked) => - { - if (item == menuCheckboxItem) - { - try - { - dynamic dynamicCallback = callbackObj; - dynamicCallback(_checked); - } - catch (System.IO.EndOfStreamException) - { - // Silently ignore - callback was deleted/garbage collected - } - catch (Exception ex) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking dynamic callback for checkbox {checkboxId}: {ex.Message}"); - } - } - }; - } - } - + AttachCheckboxCallback(menu, menuCheckboxItem, callbackObj, checkboxId); menu.AddMenuItem(menuCheckboxItem); menu.RefreshIndex(); } @@ -737,41 +633,18 @@ private void AddCheckbox(string menuId, string checkboxId, string checkboxLabel, /// Optional callback for slider changes private void AddSlider(string menuId, string sliderId, string sliderLabel, string description, int defaultValue, object callbackObj = null) { - // Validate required parameters - if (string.IsNullOrEmpty(menuId)) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] AddSlider: menuId cannot be null or empty."); - return; - } - if (string.IsNullOrEmpty(sliderId)) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] AddSlider: sliderId cannot be null or empty."); - return; - } - - Menu menu = null; - if (!DynamicMenus.TryGetValue(menuId, out menu)) - { - menu = GetMenu(menuId); - } + if (!ValidateParameter(menuId, "menuId", "AddSlider")) return; + if (!ValidateParameter(sliderId, "sliderId", "AddSlider")) return; + Menu menu = RetrieveMenu(menuId); if (menu == null) { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Menu with ID {menuId} not found."); - return; - } - - // Check for ID conflicts (menu names, other items) - if (HasIdConflict(sliderId, "AddSlider")) - { + CitizenFX.Core.Debug.WriteLine($"[vMenu] AddSlider: Menu with ID {menuId} not found."); return; } - // Check for item conflicts within this menu - if (HasItemConflict(menu, sliderId, "AddSlider")) - { - return; - } + if (HasIdConflict(sliderId, "AddSlider")) return; + if (HasItemConflict(menu, sliderId, "AddSlider")) return; // Apply defaults for optional parameters if (string.IsNullOrEmpty(sliderLabel)) @@ -791,45 +664,7 @@ private void AddSlider(string menuId, string sliderId, string sliderLabel, strin ItemData = sliderId }; - if (callbackObj != null) - { - // Handle both C# delegates and Lua functions - if (callbackObj is CallbackDelegate callback) - { - // Listen for position changes - menu.OnSliderPositionChange += (sender, item, oldPosition, newPosition, itemIndex) => - { - if (item == menuSliderItem) - { - callback.Invoke(oldPosition, newPosition); - } - }; - } - else - { - // Handle Lua functions and dynamic callbacks - menu.OnSliderPositionChange += (sender, item, oldPosition, newPosition, itemIndex) => - { - if (item == menuSliderItem) - { - try - { - dynamic dynamicCallback = callbackObj; - dynamicCallback(oldPosition, newPosition); - } - catch (System.IO.EndOfStreamException) - { - // Silently ignore - callback was deleted/garbage collected - } - catch (Exception ex) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking callback for slider {sliderId}: {ex.Message}"); - } - } - }; - } - } - + AttachSliderCallback(menu, menuSliderItem, callbackObj, sliderId); menu.AddMenuItem(menuSliderItem); menu.RefreshIndex(); } @@ -843,41 +678,18 @@ private void AddSlider(string menuId, string sliderId, string sliderLabel, strin /// Optional spacer description private void AddSpacer(string menuId, string spacerId, string spacerText, string description) { - // Validate required parameters - if (string.IsNullOrEmpty(menuId)) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] AddSpacer: menuId cannot be null or empty."); - return; - } - if (string.IsNullOrEmpty(spacerId)) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] AddSpacer: spacerId cannot be null or empty."); - return; - } - - Menu menu = null; - if (!DynamicMenus.TryGetValue(menuId, out menu)) - { - menu = GetMenu(menuId); - } + if (!ValidateParameter(menuId, "menuId", "AddSpacer")) return; + if (!ValidateParameter(spacerId, "spacerId", "AddSpacer")) return; + Menu menu = RetrieveMenu(menuId); if (menu == null) { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Menu with ID {menuId} not found."); + CitizenFX.Core.Debug.WriteLine($"[vMenu] AddSpacer: Menu with ID {menuId} not found."); return; } - // Check for ID conflicts (menu names, other items) - if (HasIdConflict(spacerId, "AddSpacer")) - { - return; - } - - // Check for item conflicts within this menu - if (HasItemConflict(menu, spacerId, "AddSpacer")) - { - return; - } + if (HasIdConflict(spacerId, "AddSpacer")) return; + if (HasItemConflict(menu, spacerId, "AddSpacer")) return; // Apply defaults for optional parameters if (string.IsNullOrEmpty(spacerText)) @@ -896,22 +708,12 @@ private void AddSpacer(string menuId, string spacerId, string spacerText, string /// Target menu ID private void RefreshMenu(string menuId) { - // Validate required parameters - if (string.IsNullOrEmpty(menuId)) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] RefreshMenu: menuId cannot be null or empty."); - return; - } - - Menu menu = null; - if (!DynamicMenus.TryGetValue(menuId, out menu)) - { - menu = GetMenu(menuId); - } + if (!ValidateParameter(menuId, "menuId", "RefreshMenu")) return; + Menu menu = RetrieveMenu(menuId); if (menu == null) { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Menu with ID {menuId} not found."); + CitizenFX.Core.Debug.WriteLine($"[vMenu] RefreshMenu: Menu with ID {menuId} not found."); return; } @@ -932,22 +734,12 @@ private void CloseAllMenus() /// Menu ID to close private void CloseMenu(string menuId) { - // Validate required parameters - if (string.IsNullOrEmpty(menuId)) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] CloseMenu: menuId cannot be null or empty."); - return; - } - - Menu menu = null; - if (!DynamicMenus.TryGetValue(menuId, out menu)) - { - menu = GetMenu(menuId); - } + if (!ValidateParameter(menuId, "menuId", "CloseMenu")) return; + Menu menu = RetrieveMenu(menuId); if (menu == null) { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Menu with ID {menuId} not found."); + CitizenFX.Core.Debug.WriteLine($"[vMenu] CloseMenu: Menu with ID {menuId} not found."); return; } @@ -968,31 +760,11 @@ private void CloseMenu(string menuId) /// Optional callback for button selection private void AddSubmenuButton(string parentMenuId, string buttonId, string submenuId, string buttonLabel, string buttonDescription, object callbackObj = null) { - // Validate required parameters - if (string.IsNullOrEmpty(parentMenuId)) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] AddSubmenuButton: parentMenuId cannot be null or empty."); - return; - } - - - - if (string.IsNullOrEmpty(buttonId)) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] AddSubmenuButton: buttonId cannot be null or empty."); - return; - } - if (string.IsNullOrEmpty(submenuId)) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] AddSubmenuButton: submenuId cannot be null or empty."); - return; - } + if (!ValidateParameter(parentMenuId, "parentMenuId", "AddSubmenuButton")) return; + if (!ValidateParameter(buttonId, "buttonId", "AddSubmenuButton")) return; + if (!ValidateParameter(submenuId, "submenuId", "AddSubmenuButton")) return; - // Check for ID conflicts for buttonId - if (HasIdConflict(buttonId, "AddSubmenuButton")) - { - return; - } + if (HasIdConflict(buttonId, "AddSubmenuButton")) return; // Verify submenu exists (either dynamic or built-in) if (!DynamicMenus.ContainsKey(submenuId) && GetMenu(submenuId) == null) @@ -1001,36 +773,22 @@ private void AddSubmenuButton(string parentMenuId, string buttonId, string subme return; } - - Menu parentMenu = null; - if (!DynamicMenus.TryGetValue(parentMenuId, out parentMenu)) - { - parentMenu = GetMenu(parentMenuId); - } - - Menu submenu = null; - if (!DynamicMenus.TryGetValue(submenuId, out submenu)) - { - submenu = GetMenu(submenuId); - } + Menu parentMenu = RetrieveMenu(parentMenuId); + Menu submenu = RetrieveMenu(submenuId); if (parentMenu == null) { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Parent menu {parentMenuId} not found."); + CitizenFX.Core.Debug.WriteLine($"[vMenu] AddSubmenuButton: Parent menu {parentMenuId} not found."); return; } if (submenu == null) { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Submenu {submenuId} not found."); + CitizenFX.Core.Debug.WriteLine($"[vMenu] AddSubmenuButton: Submenu {submenuId} not found."); return; } - // Check for item conflicts within parent menu - if (HasItemConflict(parentMenu, buttonId, "AddSubmenuButton")) - { - return; - } + if (HasItemConflict(parentMenu, buttonId, "AddSubmenuButton")) return; // Apply defaults for optional parameters if (string.IsNullOrEmpty(buttonLabel)) @@ -1049,68 +807,7 @@ private void AddSubmenuButton(string parentMenuId, string buttonId, string subme MenuController.BindMenuItem(parentMenu, submenu, submenuButton); submenu.RefreshIndex(); - if (callbackObj != null) - { - // Handle both C# delegates and Lua functions - if (callbackObj is CallbackDelegate callback) - { - parentMenu.OnItemSelect += async (sender, item, index) => - { - if (item == submenuButton) - { - await BaseScript.Delay(0); - callback.Invoke(); - } - }; - } - else if (callbackObj is Func luaCallback) - { - parentMenu.OnItemSelect += async (sender, item, index) => - { - if (item == submenuButton) - { - await BaseScript.Delay(0); - try - { - luaCallback.Invoke(); - } - catch (System.IO.EndOfStreamException) - { - // Silently ignore - callback was deleted/garbage collected - } - catch (Exception ex) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking callback for submenu button {buttonLabel}: {ex.Message}"); - } - } - }; - } - else - { - // Try to invoke as dynamic for other callback types - parentMenu.OnItemSelect += async (sender, item, index) => - { - if (item == submenuButton) - { - await BaseScript.Delay(0); - try - { - dynamic dynamicCallback = callbackObj; - dynamicCallback(); - } - catch (System.IO.EndOfStreamException) - { - // Silently ignore - callback was deleted/garbage collected - } - catch (Exception ex) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Error invoking dynamic callback for submenu button {buttonLabel}: {ex.Message}"); - } - } - }; - } - } - + AttachItemSelectCallback(parentMenu, submenuButton, callbackObj, buttonId); parentMenu.RefreshIndex(); } @@ -1121,27 +818,18 @@ private void AddSubmenuButton(string parentMenuId, string buttonId, string subme /// Item identifier (string) or index (int) to remove private void RemoveItem(string menuId, object itemIdOrIndex) { - // Validate required parameters - if (string.IsNullOrEmpty(menuId)) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] RemoveItem: menuId cannot be null or empty."); - return; - } + if (!ValidateParameter(menuId, "menuId", "RemoveItem")) return; + if (itemIdOrIndex == null) { CitizenFX.Core.Debug.WriteLine($"[vMenu] RemoveItem: itemIdOrIndex cannot be null."); return; } - Menu menu = null; - if (!DynamicMenus.TryGetValue(menuId, out menu)) - { - menu = GetMenu(menuId); - } - + Menu menu = RetrieveMenu(menuId); if (menu == null) { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Menu with ID {menuId} not found."); + CitizenFX.Core.Debug.WriteLine($"[vMenu] RemoveItem: Menu with ID {menuId} not found."); return; } @@ -1175,7 +863,7 @@ private void RemoveItem(string menuId, object itemIdOrIndex) } else { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Item with ID {itemId} not found in menu {menuId}."); + CitizenFX.Core.Debug.WriteLine($"[vMenu] RemoveItem: Item with ID {itemId} not found in menu {menuId}."); } } else @@ -1190,25 +878,9 @@ private void RemoveItem(string menuId, object itemIdOrIndex) /// Target menu ID private void ClearMenu(string menuId) { - // Validate required parameters - if (string.IsNullOrEmpty(menuId)) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] ClearMenu: menuId cannot be null or empty."); - return; - } - - Menu menu = null; - - // First check dynamic menus - if (DynamicMenus.TryGetValue(menuId, out menu)) - { - menu.ClearMenuItems(); - return; - } - - // Then check built-in menus using GetMenu - menu = GetMenu(menuId); + if (!ValidateParameter(menuId, "menuId", "ClearMenu")) return; + Menu menu = RetrieveMenu(menuId); if (menu == null) { CitizenFX.Core.Debug.WriteLine($"[vMenu] ClearMenu: Menu with ID '{menuId}' not found. Available menus: {string.Join(", ", GetAllMenuIds())}"); @@ -1225,24 +897,10 @@ private void ClearMenu(string menuId) /// True if menu exists, false otherwise private bool CheckMenu(string menuId) { - // Validate required parameters - if (string.IsNullOrEmpty(menuId)) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] CheckMenu: menuId cannot be null or empty."); - return false; - } - - Menu menu = null; - if (!DynamicMenus.TryGetValue(menuId, out menu)) - { - menu = GetMenu(menuId); - } + if (!ValidateParameter(menuId, "menuId", "CheckMenu")) return false; - if (menu == null) - { - return false; - } - return true; + Menu menu = RetrieveMenu(menuId); + return menu != null; } /// @@ -1251,17 +909,11 @@ private bool CheckMenu(string menuId) /// Menu ID to delete private void DeleteMenu(string menuId) { - // Validate required parameters - if (string.IsNullOrEmpty(menuId)) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] DeleteMenu: menuId cannot be null or empty."); - return; - } + if (!ValidateParameter(menuId, "menuId", "DeleteMenu")) return; - Menu menu = null; - if (!DynamicMenus.TryGetValue(menuId, out menu)) + if (!DynamicMenus.TryGetValue(menuId, out Menu menu)) { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Menu with ID {menuId} not found."); + CitizenFX.Core.Debug.WriteLine($"[vMenu] DeleteMenu: Menu with ID {menuId} not found."); return; } @@ -1287,12 +939,11 @@ private void DeleteMenu(string menuId) menu = null; } - /// - /// Dynamically finds built-in vMenu menus by ID with permission checking + /// Dynamically finds built-in vMenu menus by ID (without permission filtering) /// /// Menu identifier string - /// Menu instance if found and permitted, null otherwise + /// Menu instance if found, null otherwise private Menu GetMenu(string menuId) { // First check dynamic menus by exact ID @@ -1317,11 +968,7 @@ private Menu GetMenu(string menuId) // Exact match only if (normalizedTitle == normalizedId) { - // Basic permission check for known menu patterns - if (IsMenuPermitted(menu, menuId.ToLower())) - { - return menu; - } + return menu; } } @@ -1329,9 +976,19 @@ private Menu GetMenu(string menuId) } /// - /// Export function to get all available menu IDs + /// Export wrapper for GetMenu that returns an object (for export compatibility) /// - /// Array of menu IDs that can be accessed through GetMenu + /// Menu identifier + /// Menu instance as object if found, null otherwise + private object GetMenuExport(string menuId) + { + return GetMenu(menuId); + } + + /// + /// Export function to get all available menu IDs (without permission filtering) + /// + /// Array of all menu IDs that exist private string[] GetAllMenuIds() { var menuIds = new List(); @@ -1349,19 +1006,39 @@ private string[] GetAllMenuIds() // Skip dynamic menus (already added above) if (DynamicMenus.ContainsValue(menu)) continue; - // Check if menu is permitted var menuIdentifier = menu.MenuSubtitle ?? menu.MenuTitle ?? ""; var normalizedTitle = menuIdentifier.ToLower().Replace(" ", "-"); - if (IsMenuPermitted(menu, normalizedTitle)) - { - menuIds.Add(normalizedTitle); - } + menuIds.Add(normalizedTitle); } // Remove duplicates and sort return menuIds.Distinct().OrderBy(id => id).ToArray(); } + /// + /// Export function to check if a menu is permitted based on vMenu permissions + /// + /// Menu identifier + /// True if permitted, false otherwise + private bool IsMenuPermittedExport(string menuId) + { + if (string.IsNullOrEmpty(menuId)) + { + return false; + } + + var normalizedId = menuId.ToLower().Replace(" ", "-"); + + // Get the menu to check if it exists + var menu = GetMenu(normalizedId); + if (menu == null) + { + return false; + } + + return IsMenuPermitted(menu, normalizedId); + } + /// /// Checks if a menu is permitted based on vMenu permissions /// From 3e58e9b1198b20e95698f13a71c5c02309d5499f Mon Sep 17 00:00:00 2001 From: Tommy Johnston Date: Mon, 20 Oct 2025 14:43:03 -0400 Subject: [PATCH 5/9] refactor: add ready callback & clean logs --- vMenu/Exports.cs | 264 ++++++++++++++++++++++++++++++----------------- 1 file changed, 172 insertions(+), 92 deletions(-) diff --git a/vMenu/Exports.cs b/vMenu/Exports.cs index 18e7b3cff..3138d7a0a 100644 --- a/vMenu/Exports.cs +++ b/vMenu/Exports.cs @@ -26,6 +26,16 @@ public partial class MainMenu /// private static Dictionary DynamicMenus = new Dictionary(); + /// + /// List to store callbacks waiting for vMenu to be ready + /// + private static List ReadyCallbacks = new List(); + + /// + /// Flag indicating if vMenu is ready for external interactions + /// + private static bool IsVMenuReady = false; + #region Helper Methods /// @@ -125,14 +135,23 @@ private bool ValidateParameter(string value, string paramName, string context) /// Retrieves a menu by ID from dynamic or built-in menus /// /// Menu identifier + /// Error context /// Menu instance if found, null otherwise - private Menu RetrieveMenu(string menuId) + private Menu RetrieveMenu(string menuId, string context = null) { if (DynamicMenus.TryGetValue(menuId, out Menu menu)) { return menu; } - return GetMenu(menuId); + + menu = GetMenu(menuId); + + if (menu == null && !string.IsNullOrEmpty(context)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] {context}: Menu with ID '{menuId}' not found."); + } + + return menu; } /// @@ -176,9 +195,6 @@ private void SafeInvokeCallback(object callbackObj, string itemId, params object case 4: dynamicCallback(args[0], args[1], args[2], args[3]); break; - default: - CitizenFX.Core.Debug.WriteLine($"[vMenu] Callback for {itemId} has unsupported number of arguments: {args.Length}"); - break; } } } @@ -251,8 +267,12 @@ private void AttachListCallbacks(Menu menu, MenuListItem item, List opti { if (menuItem == item) { - var currentValue = options[newIndex]; - SafeInvokeCallback(callbackObj, itemId, false, currentValue, newIndex, oldIndex); + // Bounds check to prevent ArgumentOutOfRangeException + if (newIndex >= 0 && newIndex < options.Count && oldIndex >= 0 && oldIndex < options.Count) + { + var currentValue = options[newIndex]; + SafeInvokeCallback(callbackObj, itemId, false, currentValue, newIndex, oldIndex); + } } }; @@ -261,8 +281,12 @@ private void AttachListCallbacks(Menu menu, MenuListItem item, List opti { if (menuItem == item) { - var selectedValue = options[listIndex]; - SafeInvokeCallback(callbackObj, itemId, true, selectedValue, listIndex, listIndex); + // Bounds check to prevent ArgumentOutOfRangeException + if (listIndex >= 0 && listIndex < options.Count) + { + var selectedValue = options[listIndex]; + SafeInvokeCallback(callbackObj, itemId, true, selectedValue, listIndex, listIndex); + } } }; } @@ -331,6 +355,19 @@ private void AttachMenuOpenCallback(Menu menu, object callbackObj, string menuId public void InitializeDynamicMenuExports() { RegisterDynamicMenuExports(); + RegisterReadyEventHandler(); + } + + /// + /// Registers event handler to listen for vMenu ready state + /// + private void RegisterReadyEventHandler() + { + // Listen for vMenu initialization complete + EventHandlers["vMenu:SetupTickFunctions"] += new Action(() => + { + TriggerReady(); + }); } /// @@ -366,6 +403,10 @@ private void RegisterDynamicMenuExports() Exports.Add("GetAllMenuIds", new Func(GetAllMenuIds)); Exports.Add("GetMenu", new Func(GetMenuExport)); Exports.Add("IsMenuPermitted", new Func(IsMenuPermittedExport)); + + // Ready State Management + Exports.Add("OnReady", new Action(OnReadyExport)); + Exports.Add("IsReady", new Func(IsReadyExport)); } #endregion @@ -434,13 +475,8 @@ private void OpenMenu(string menuId) { if (!ValidateParameter(menuId, "menuId", "OpenMenu")) return; - Menu menu = RetrieveMenu(menuId); - - if (menu == null) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] OpenMenu: Menu with ID '{menuId}' not found. Available menus: {string.Join(", ", GetAllMenuIds())}"); - return; - } + Menu menu = RetrieveMenu(menuId, "OpenMenu"); + if (menu == null) return; // Close all currently open menus before opening the new one MenuController.CloseAllMenus(); @@ -479,12 +515,8 @@ private void AddButton(string menuId, string buttonId, string buttonLabel, strin if (!ValidateParameter(buttonId, "buttonId", "AddButton")) return; if (HasIdConflict(buttonId, "AddButton")) return; - Menu menu = RetrieveMenu(menuId); - if (menu == null) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] AddButton: Menu with ID {menuId} not found."); - return; - } + Menu menu = RetrieveMenu(menuId, "AddButton"); + if (menu == null) return; if (HasItemConflict(menu, buttonId, "AddButton")) return; @@ -520,12 +552,8 @@ private void AddList(string menuId, string listId, string listLabel, object opti if (!ValidateParameter(listId, "listId", "AddList")) return; if (HasIdConflict(listId, "AddList")) return; - Menu menu = RetrieveMenu(menuId); - if (menu == null) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] AddList: Menu with ID {menuId} not found."); - return; - } + Menu menu = RetrieveMenu(menuId, "AddList"); + if (menu == null) return; if (HasItemConflict(menu, listId, "AddList")) return; @@ -596,12 +624,8 @@ private void AddCheckbox(string menuId, string checkboxId, string checkboxLabel, if (!ValidateParameter(menuId, "menuId", "AddCheckbox")) return; if (!ValidateParameter(checkboxId, "checkboxId", "AddCheckbox")) return; - Menu menu = RetrieveMenu(menuId); - if (menu == null) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] AddCheckbox: Menu with ID {menuId} not found."); - return; - } + Menu menu = RetrieveMenu(menuId, "AddCheckbox"); + if (menu == null) return; if (HasIdConflict(checkboxId, "AddCheckbox")) return; if (HasItemConflict(menu, checkboxId, "AddCheckbox")) return; @@ -636,12 +660,8 @@ private void AddSlider(string menuId, string sliderId, string sliderLabel, strin if (!ValidateParameter(menuId, "menuId", "AddSlider")) return; if (!ValidateParameter(sliderId, "sliderId", "AddSlider")) return; - Menu menu = RetrieveMenu(menuId); - if (menu == null) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] AddSlider: Menu with ID {menuId} not found."); - return; - } + Menu menu = RetrieveMenu(menuId, "AddSlider"); + if (menu == null) return; if (HasIdConflict(sliderId, "AddSlider")) return; if (HasItemConflict(menu, sliderId, "AddSlider")) return; @@ -681,12 +701,8 @@ private void AddSpacer(string menuId, string spacerId, string spacerText, string if (!ValidateParameter(menuId, "menuId", "AddSpacer")) return; if (!ValidateParameter(spacerId, "spacerId", "AddSpacer")) return; - Menu menu = RetrieveMenu(menuId); - if (menu == null) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] AddSpacer: Menu with ID {menuId} not found."); - return; - } + Menu menu = RetrieveMenu(menuId, "AddSpacer"); + if (menu == null) return; if (HasIdConflict(spacerId, "AddSpacer")) return; if (HasItemConflict(menu, spacerId, "AddSpacer")) return; @@ -710,12 +726,8 @@ private void RefreshMenu(string menuId) { if (!ValidateParameter(menuId, "menuId", "RefreshMenu")) return; - Menu menu = RetrieveMenu(menuId); - if (menu == null) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] RefreshMenu: Menu with ID {menuId} not found."); - return; - } + Menu menu = RetrieveMenu(menuId, "RefreshMenu"); + if (menu == null) return; menu.RefreshIndex(); } @@ -736,12 +748,8 @@ private void CloseMenu(string menuId) { if (!ValidateParameter(menuId, "menuId", "CloseMenu")) return; - Menu menu = RetrieveMenu(menuId); - if (menu == null) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] CloseMenu: Menu with ID {menuId} not found."); - return; - } + Menu menu = RetrieveMenu(menuId, "CloseMenu"); + if (menu == null) return; if (menu.Visible) { @@ -773,20 +781,11 @@ private void AddSubmenuButton(string parentMenuId, string buttonId, string subme return; } - Menu parentMenu = RetrieveMenu(parentMenuId); - Menu submenu = RetrieveMenu(submenuId); + Menu parentMenu = RetrieveMenu(parentMenuId, "AddSubmenuButton (parent)"); + if (parentMenu == null) return; - if (parentMenu == null) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] AddSubmenuButton: Parent menu {parentMenuId} not found."); - return; - } - - if (submenu == null) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] AddSubmenuButton: Submenu {submenuId} not found."); - return; - } + Menu submenu = RetrieveMenu(submenuId, "AddSubmenuButton (submenu)"); + if (submenu == null) return; if (HasItemConflict(parentMenu, buttonId, "AddSubmenuButton")) return; @@ -826,12 +825,8 @@ private void RemoveItem(string menuId, object itemIdOrIndex) return; } - Menu menu = RetrieveMenu(menuId); - if (menu == null) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] RemoveItem: Menu with ID {menuId} not found."); - return; - } + Menu menu = RetrieveMenu(menuId, "RemoveItem"); + if (menu == null) return; var items = menu.GetMenuItems(); @@ -843,7 +838,17 @@ private void RemoveItem(string menuId, object itemIdOrIndex) // Silent fail for out of range index (used for clearing menus in loops) return; } - menu.RemoveMenuItem(items[index]); + try + { + menu.RemoveMenuItem(items[index]); + } + catch (ArgumentOutOfRangeException ex) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] ArgumentOutOfRangeException in RemoveItem for menu {menuId}, index {index}:"); + CitizenFX.Core.Debug.WriteLine($"[vMenu] Items count: {items.Count}"); + CitizenFX.Core.Debug.WriteLine($"[vMenu] Message: {ex.Message}"); + CitizenFX.Core.Debug.WriteLine($"[vMenu] Stack trace: {ex.StackTrace}"); + } } // Otherwise treat as string (ID-based removal) else if (itemIdOrIndex is string itemId) @@ -880,12 +885,8 @@ private void ClearMenu(string menuId) { if (!ValidateParameter(menuId, "menuId", "ClearMenu")) return; - Menu menu = RetrieveMenu(menuId); - if (menu == null) - { - CitizenFX.Core.Debug.WriteLine($"[vMenu] ClearMenu: Menu with ID '{menuId}' not found. Available menus: {string.Join(", ", GetAllMenuIds())}"); - return; - } + Menu menu = RetrieveMenu(menuId, "ClearMenu"); + if (menu == null) return; menu.ClearMenuItems(); } @@ -982,7 +983,17 @@ private Menu GetMenu(string menuId) /// Menu instance as object if found, null otherwise private object GetMenuExport(string menuId) { - return GetMenu(menuId); + try + { + return GetMenu(menuId); + } + catch (ArgumentOutOfRangeException ex) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] ArgumentOutOfRangeException in GetMenuExport for {menuId}:"); + CitizenFX.Core.Debug.WriteLine($"[vMenu] Message: {ex.Message}"); + CitizenFX.Core.Debug.WriteLine($"[vMenu] Stack trace: {ex.StackTrace}"); + return null; + } } /// @@ -1000,15 +1011,24 @@ private string[] GetAllMenuIds() } // Add built-in menu IDs based on their subtitles (avoid duplicates) - var uniqueMenus = MenuController.Menus.GroupBy(m => m.MenuSubtitle ?? m.MenuTitle).Select(g => g.First()).ToList(); - foreach (var menu in uniqueMenus) + try { - // Skip dynamic menus (already added above) - if (DynamicMenus.ContainsValue(menu)) continue; + var uniqueMenus = MenuController.Menus.GroupBy(m => m.MenuSubtitle ?? m.MenuTitle).Select(g => g.First()).ToList(); + foreach (var menu in uniqueMenus) + { + // Skip dynamic menus (already added above) + if (DynamicMenus.ContainsValue(menu)) continue; - var menuIdentifier = menu.MenuSubtitle ?? menu.MenuTitle ?? ""; - var normalizedTitle = menuIdentifier.ToLower().Replace(" ", "-"); - menuIds.Add(normalizedTitle); + var menuIdentifier = menu.MenuSubtitle ?? menu.MenuTitle ?? ""; + var normalizedTitle = menuIdentifier.ToLower().Replace(" ", "-"); + menuIds.Add(normalizedTitle); + } + } + catch (ArgumentOutOfRangeException ex) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] ArgumentOutOfRangeException in GetAllMenuIds:"); + CitizenFX.Core.Debug.WriteLine($"[vMenu] Message: {ex.Message}"); + CitizenFX.Core.Debug.WriteLine($"[vMenu] Stack trace: {ex.StackTrace}"); } // Remove duplicates and sort @@ -1039,6 +1059,66 @@ private bool IsMenuPermittedExport(string menuId) return IsMenuPermitted(menu, normalizedId); } + /// + /// Registers a callback to be invoked when vMenu is ready + /// If vMenu is already ready, the callback is invoked immediately + /// Note: Callbacks that perform asynchronous work (menu modifications, waits, etc.) + /// should wrap their logic in Citizen.CreateThread() to prevent serialization errors + /// Example: exports.vMenu:OnReady(function() Citizen.CreateThread(setupMenu) end) + /// + /// Callback function to invoke + private void OnReadyExport(object callbackObj) + { + if (callbackObj == null) + { + CitizenFX.Core.Debug.WriteLine("[vMenu] OnReady: callback cannot be null."); + return; + } + + if (IsVMenuReady) + { + // vMenu is already ready, invoke callback immediately + SafeInvokeCallback(callbackObj, "OnReady"); + } + else + { + // vMenu not ready yet, queue the callback + ReadyCallbacks.Add(callbackObj); + } + } + + /// + /// Checks if vMenu is ready for external interactions + /// + /// True if ready, false otherwise + private bool IsReadyExport() + { + return IsVMenuReady; + } + + /// + /// Marks vMenu as ready and invokes all queued callbacks + /// This should be called when vMenu initialization is complete + /// + public void TriggerReady() + { + if (IsVMenuReady) + { + return; // Already triggered + } + + IsVMenuReady = true; + + // Invoke all queued callbacks + foreach (var callbackObj in ReadyCallbacks) + { + SafeInvokeCallback(callbackObj, "OnReady"); + } + + // Clear the callbacks list + ReadyCallbacks.Clear(); + } + /// /// Checks if a menu is permitted based on vMenu permissions /// From 5e24ccda937c9777d097587e44c3561200ab5fe2 Mon Sep 17 00:00:00 2001 From: Tommy Johnston Date: Mon, 20 Oct 2025 14:44:19 -0400 Subject: [PATCH 6/9] refactor: remove unused import --- vMenu/Exports.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/vMenu/Exports.cs b/vMenu/Exports.cs index 3138d7a0a..fb9834ab8 100644 --- a/vMenu/Exports.cs +++ b/vMenu/Exports.cs @@ -4,7 +4,6 @@ using CitizenFX.Core; using MenuAPI; using Newtonsoft.Json; -using static vMenuClient.CommonFunctions; using static vMenuShared.PermissionsManager; using static vMenuShared.ConfigManager; From 453c4b09111f4ab7ba756847c3ff80067aa9283a Mon Sep 17 00:00:00 2001 From: Tommy Johnston Date: Mon, 20 Oct 2025 15:04:04 -0400 Subject: [PATCH 7/9] refactor: simplify IsMenuPermitted and AddList methods --- vMenu/Exports.cs | 91 +++++++++++++++++++++++++++--------------------- 1 file changed, 52 insertions(+), 39 deletions(-) diff --git a/vMenu/Exports.cs b/vMenu/Exports.cs index fb9834ab8..9bb0231fb 100644 --- a/vMenu/Exports.cs +++ b/vMenu/Exports.cs @@ -548,6 +548,7 @@ private void AddButton(string menuId, string buttonId, string buttonLabel, strin /// Optional callback for list changes private void AddList(string menuId, string listId, string listLabel, object options, int defaultIndex, string description, object callbackObj = null) { + // Validate parameters if (!ValidateParameter(listId, "listId", "AddList")) return; if (HasIdConflict(listId, "AddList")) return; @@ -557,48 +558,32 @@ private void AddList(string menuId, string listId, string listLabel, object opti if (HasItemConflict(menu, listId, "AddList")) return; // Apply defaults for optional parameters - if (string.IsNullOrEmpty(listLabel)) - listLabel = "List"; - if (string.IsNullOrEmpty(description)) - description = ""; - - List optionsList; + listLabel = string.IsNullOrEmpty(listLabel) ? "List" : listLabel; + description = string.IsNullOrEmpty(description) ? "" : description; - // Handle different option input types - if (options is string jsonString) - { - // Legacy support for JSON string - optionsList = JsonConvert.DeserializeObject>(jsonString); - } - else if (options is object[] objectArray) + // Parse options based on input type + List optionsList = options switch { - // Convert object array to string list - optionsList = objectArray.Select(o => o?.ToString() ?? "").ToList(); - } - else if (options is List objectList) - { - // Convert object list to string list - optionsList = objectList.Select(o => o?.ToString() ?? "").ToList(); - } - else if (options == null) - { - // Provide default empty list if options is null - optionsList = new List { "Option 1", "Option 2" }; - CitizenFX.Core.Debug.WriteLine($"[vMenu] No options provided for list {listId}, using default options."); - } - else + string jsonString => ParseJsonOptions(jsonString, listId), + object[] objectArray => objectArray.Select(o => o?.ToString() ?? "").ToList(), + List objectList => objectList.Select(o => o?.ToString() ?? "").ToList(), + _ => null + }; + + if (optionsList == null) { - CitizenFX.Core.Debug.WriteLine($"[vMenu] Invalid options type for list {listId}. Expected array or JSON string."); + CitizenFX.Core.Debug.WriteLine($"[vMenu] No valid options provided for list {listId}. List item will not be added."); return; } - // Validate defaultIndex + // Validate and normalize defaultIndex if (defaultIndex < 0 || defaultIndex >= optionsList.Count) { CitizenFX.Core.Debug.WriteLine($"[vMenu] Invalid defaultIndex {defaultIndex} for list {listId}. Using 0 instead."); defaultIndex = 0; } + // Create and add the list item var menuListItem = new MenuListItem(listLabel, optionsList, defaultIndex, description) { ItemData = listId @@ -609,6 +594,22 @@ private void AddList(string menuId, string listId, string listLabel, object opti menu.RefreshIndex(); } + /// + /// Parses JSON string options into a list + /// + private List ParseJsonOptions(string jsonString, string listId) + { + try + { + return JsonConvert.DeserializeObject>(jsonString); + } + catch (JsonException ex) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] Failed to parse JSON options for list {listId}: {ex.Message}"); + return null; + } + } + /// /// Adds a checkbox to a menu /// @@ -1129,29 +1130,41 @@ private bool IsMenuPermitted(Menu menu, string menuId) // Common permission patterns based on menu titles/IDs return menuId switch { + // Direct menu mappings "player-options" => IsAllowed(Permission.POMenu), "online-players" => IsAllowed(Permission.OPMenu), - var id when id.Contains("banned-players") => IsAllowed(Permission.OPUnban) || IsAllowed(Permission.OPViewBannedPlayers), "personal-vehicle-options" => IsAllowed(Permission.PVMenu), "vehicle-options" => IsAllowed(Permission.VOMenu), "vehicle-spawner" => IsAllowed(Permission.VSMenu), + "weapon-options" => IsAllowed(Permission.WPMenu), + "voice-chat-settings" => IsAllowed(Permission.VCMenu), + "player-appearance" or "character-appearance-options" or "mp-ped-customization" => IsAllowed(Permission.PAMenu), + + // Pattern-based menu checks + var id when id.Contains("banned-players") => IsAllowed(Permission.OPUnban) || IsAllowed(Permission.OPViewBannedPlayers), var id when id.Contains("saved") && id.Contains("vehicle") => IsAllowed(Permission.SVMenu), - "player-appearance" or "character-appearance-options" => IsAllowed(Permission.PAMenu), - "mp-ped-customization" => IsAllowed(Permission.PAMenu), + var id when id.Contains("weapon-loadouts") => IsAllowed(Permission.WLMenu), var id when id.Contains("time") => IsAllowed(Permission.TOMenu) && GetSettingsBool(Setting.vmenu_enable_time_sync), var id when id.Contains("weather") => IsAllowed(Permission.WOMenu) && GetSettingsBool(Setting.vmenu_enable_weather_sync), - var id when id == "world" || id.Contains("world") => (IsAllowed(Permission.TOMenu) && GetSettingsBool(Setting.vmenu_enable_time_sync)) || - (IsAllowed(Permission.WOMenu) && GetSettingsBool(Setting.vmenu_enable_weather_sync)), - "weapon-options" => IsAllowed(Permission.WPMenu), - var id when id.Contains("weapon-loadouts") => IsAllowed(Permission.WLMenu), - "voice-chat-settings" => IsAllowed(Permission.VCMenu), - // Allow access to menus without specific restrictions + var id when id == "world" || id.Contains("world") => IsWorldMenuPermitted(), + + // Unrestricted menus var id when id.Contains("recording-options") || id.Contains("misc-settings") || id.Contains("about-vmenu") => true, + // Default to allowing access for unrecognized menus _ => true }; } + /// + /// Checks if the world menu is permitted (requires time or weather permissions) + /// + private bool IsWorldMenuPermitted() + { + return (IsAllowed(Permission.TOMenu) && GetSettingsBool(Setting.vmenu_enable_time_sync)) || + (IsAllowed(Permission.WOMenu) && GetSettingsBool(Setting.vmenu_enable_weather_sync)); + } + #endregion } } From 35f840677e194e408bb3c3812a1cf07976b17b11 Mon Sep 17 00:00:00 2001 From: Tommy Johnston Date: Mon, 20 Oct 2025 15:13:58 -0400 Subject: [PATCH 8/9] refactor: extract IsMenuPermitted logic into focused helper methods --- vMenu/Exports.cs | 167 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 140 insertions(+), 27 deletions(-) diff --git a/vMenu/Exports.cs b/vMenu/Exports.cs index 9bb0231fb..124e557e3 100644 --- a/vMenu/Exports.cs +++ b/vMenu/Exports.cs @@ -1127,33 +1127,146 @@ public void TriggerReady() /// True if permitted, false otherwise private bool IsMenuPermitted(Menu menu, string menuId) { - // Common permission patterns based on menu titles/IDs - return menuId switch - { - // Direct menu mappings - "player-options" => IsAllowed(Permission.POMenu), - "online-players" => IsAllowed(Permission.OPMenu), - "personal-vehicle-options" => IsAllowed(Permission.PVMenu), - "vehicle-options" => IsAllowed(Permission.VOMenu), - "vehicle-spawner" => IsAllowed(Permission.VSMenu), - "weapon-options" => IsAllowed(Permission.WPMenu), - "voice-chat-settings" => IsAllowed(Permission.VCMenu), - "player-appearance" or "character-appearance-options" or "mp-ped-customization" => IsAllowed(Permission.PAMenu), - - // Pattern-based menu checks - var id when id.Contains("banned-players") => IsAllowed(Permission.OPUnban) || IsAllowed(Permission.OPViewBannedPlayers), - var id when id.Contains("saved") && id.Contains("vehicle") => IsAllowed(Permission.SVMenu), - var id when id.Contains("weapon-loadouts") => IsAllowed(Permission.WLMenu), - var id when id.Contains("time") => IsAllowed(Permission.TOMenu) && GetSettingsBool(Setting.vmenu_enable_time_sync), - var id when id.Contains("weather") => IsAllowed(Permission.WOMenu) && GetSettingsBool(Setting.vmenu_enable_weather_sync), - var id when id == "world" || id.Contains("world") => IsWorldMenuPermitted(), - - // Unrestricted menus - var id when id.Contains("recording-options") || id.Contains("misc-settings") || id.Contains("about-vmenu") => true, - - // Default to allowing access for unrecognized menus - _ => true - }; + // Check direct menu mappings first + if (IsDirectMenuMapping(menuId, out bool permitted)) + { + return permitted; + } + + // Check pattern-based menus + if (IsPatternBasedMenu(menuId, out permitted)) + { + return permitted; + } + + // Check unrestricted menus + if (IsUnrestrictedMenu(menuId)) + { + return true; + } + + // Default to allowing access for unrecognized menus + return true; + } + + /// + /// Checks direct menu ID mappings to permissions + /// + private bool IsDirectMenuMapping(string menuId, out bool permitted) + { + permitted = false; + + switch (menuId) + { + case "player-options": + permitted = IsAllowed(Permission.POMenu); + return true; + case "online-players": + permitted = IsAllowed(Permission.OPMenu); + return true; + case "personal-vehicle-options": + permitted = IsAllowed(Permission.PVMenu); + return true; + case "vehicle-options": + permitted = IsAllowed(Permission.VOMenu); + return true; + case "vehicle-spawner": + permitted = IsAllowed(Permission.VSMenu); + return true; + case "weapon-options": + permitted = IsAllowed(Permission.WPMenu); + return true; + case "voice-chat-settings": + permitted = IsAllowed(Permission.VCMenu); + return true; + case "player-appearance": + case "character-appearance-options": + case "mp-ped-customization": + permitted = IsAllowed(Permission.PAMenu); + return true; + default: + return false; + } + } + + /// + /// Checks pattern-based menu permissions (menus with dynamic IDs) + /// + private bool IsPatternBasedMenu(string menuId, out bool permitted) + { + permitted = false; + + if (menuId.Contains("banned-players")) + { + permitted = IsBannedPlayersMenuPermitted(); + return true; + } + + if (menuId.Contains("saved") && menuId.Contains("vehicle")) + { + permitted = IsAllowed(Permission.SVMenu); + return true; + } + + if (menuId.Contains("weapon-loadouts")) + { + permitted = IsAllowed(Permission.WLMenu); + return true; + } + + if (menuId.Contains("time")) + { + permitted = IsTimeMenuPermitted(); + return true; + } + + if (menuId.Contains("weather")) + { + permitted = IsWeatherMenuPermitted(); + return true; + } + + if (menuId == "world" || menuId.Contains("world")) + { + permitted = IsWorldMenuPermitted(); + return true; + } + + return false; + } + + /// + /// Checks if a menu is unrestricted (always accessible) + /// + private bool IsUnrestrictedMenu(string menuId) + { + return menuId.Contains("recording-options") || + menuId.Contains("misc-settings") || + menuId.Contains("about-vmenu"); + } + + /// + /// Checks if the banned players menu is permitted + /// + private bool IsBannedPlayersMenuPermitted() + { + return IsAllowed(Permission.OPUnban) || IsAllowed(Permission.OPViewBannedPlayers); + } + + /// + /// Checks if the time menu is permitted + /// + private bool IsTimeMenuPermitted() + { + return IsAllowed(Permission.TOMenu) && GetSettingsBool(Setting.vmenu_enable_time_sync); + } + + /// + /// Checks if the weather menu is permitted + /// + private bool IsWeatherMenuPermitted() + { + return IsAllowed(Permission.WOMenu) && GetSettingsBool(Setting.vmenu_enable_weather_sync); } /// From c58ae7f21c14db483e49d355257461572a48550c Mon Sep 17 00:00:00 2001 From: Tommy Johnston Date: Mon, 20 Oct 2025 15:40:35 -0400 Subject: [PATCH 9/9] refactor: remove IsMenuPermitted --- vMenu/Exports.cs | 186 ----------------------------------------------- 1 file changed, 186 deletions(-) diff --git a/vMenu/Exports.cs b/vMenu/Exports.cs index 124e557e3..9c2b4b46f 100644 --- a/vMenu/Exports.cs +++ b/vMenu/Exports.cs @@ -4,8 +4,6 @@ using CitizenFX.Core; using MenuAPI; using Newtonsoft.Json; -using static vMenuShared.PermissionsManager; -using static vMenuShared.ConfigManager; namespace vMenuClient { @@ -401,7 +399,6 @@ private void RegisterDynamicMenuExports() // Menu Information Exports.Add("GetAllMenuIds", new Func(GetAllMenuIds)); Exports.Add("GetMenu", new Func(GetMenuExport)); - Exports.Add("IsMenuPermitted", new Func(IsMenuPermittedExport)); // Ready State Management Exports.Add("OnReady", new Action(OnReadyExport)); @@ -1035,30 +1032,6 @@ private string[] GetAllMenuIds() return menuIds.Distinct().OrderBy(id => id).ToArray(); } - /// - /// Export function to check if a menu is permitted based on vMenu permissions - /// - /// Menu identifier - /// True if permitted, false otherwise - private bool IsMenuPermittedExport(string menuId) - { - if (string.IsNullOrEmpty(menuId)) - { - return false; - } - - var normalizedId = menuId.ToLower().Replace(" ", "-"); - - // Get the menu to check if it exists - var menu = GetMenu(normalizedId); - if (menu == null) - { - return false; - } - - return IsMenuPermitted(menu, normalizedId); - } - /// /// Registers a callback to be invoked when vMenu is ready /// If vMenu is already ready, the callback is invoked immediately @@ -1119,165 +1092,6 @@ public void TriggerReady() ReadyCallbacks.Clear(); } - /// - /// Checks if a menu is permitted based on vMenu permissions - /// - /// Menu to check - /// Menu identifier - /// True if permitted, false otherwise - private bool IsMenuPermitted(Menu menu, string menuId) - { - // Check direct menu mappings first - if (IsDirectMenuMapping(menuId, out bool permitted)) - { - return permitted; - } - - // Check pattern-based menus - if (IsPatternBasedMenu(menuId, out permitted)) - { - return permitted; - } - - // Check unrestricted menus - if (IsUnrestrictedMenu(menuId)) - { - return true; - } - - // Default to allowing access for unrecognized menus - return true; - } - - /// - /// Checks direct menu ID mappings to permissions - /// - private bool IsDirectMenuMapping(string menuId, out bool permitted) - { - permitted = false; - - switch (menuId) - { - case "player-options": - permitted = IsAllowed(Permission.POMenu); - return true; - case "online-players": - permitted = IsAllowed(Permission.OPMenu); - return true; - case "personal-vehicle-options": - permitted = IsAllowed(Permission.PVMenu); - return true; - case "vehicle-options": - permitted = IsAllowed(Permission.VOMenu); - return true; - case "vehicle-spawner": - permitted = IsAllowed(Permission.VSMenu); - return true; - case "weapon-options": - permitted = IsAllowed(Permission.WPMenu); - return true; - case "voice-chat-settings": - permitted = IsAllowed(Permission.VCMenu); - return true; - case "player-appearance": - case "character-appearance-options": - case "mp-ped-customization": - permitted = IsAllowed(Permission.PAMenu); - return true; - default: - return false; - } - } - - /// - /// Checks pattern-based menu permissions (menus with dynamic IDs) - /// - private bool IsPatternBasedMenu(string menuId, out bool permitted) - { - permitted = false; - - if (menuId.Contains("banned-players")) - { - permitted = IsBannedPlayersMenuPermitted(); - return true; - } - - if (menuId.Contains("saved") && menuId.Contains("vehicle")) - { - permitted = IsAllowed(Permission.SVMenu); - return true; - } - - if (menuId.Contains("weapon-loadouts")) - { - permitted = IsAllowed(Permission.WLMenu); - return true; - } - - if (menuId.Contains("time")) - { - permitted = IsTimeMenuPermitted(); - return true; - } - - if (menuId.Contains("weather")) - { - permitted = IsWeatherMenuPermitted(); - return true; - } - - if (menuId == "world" || menuId.Contains("world")) - { - permitted = IsWorldMenuPermitted(); - return true; - } - - return false; - } - - /// - /// Checks if a menu is unrestricted (always accessible) - /// - private bool IsUnrestrictedMenu(string menuId) - { - return menuId.Contains("recording-options") || - menuId.Contains("misc-settings") || - menuId.Contains("about-vmenu"); - } - - /// - /// Checks if the banned players menu is permitted - /// - private bool IsBannedPlayersMenuPermitted() - { - return IsAllowed(Permission.OPUnban) || IsAllowed(Permission.OPViewBannedPlayers); - } - - /// - /// Checks if the time menu is permitted - /// - private bool IsTimeMenuPermitted() - { - return IsAllowed(Permission.TOMenu) && GetSettingsBool(Setting.vmenu_enable_time_sync); - } - - /// - /// Checks if the weather menu is permitted - /// - private bool IsWeatherMenuPermitted() - { - return IsAllowed(Permission.WOMenu) && GetSettingsBool(Setting.vmenu_enable_weather_sync); - } - - /// - /// Checks if the world menu is permitted (requires time or weather permissions) - /// - private bool IsWorldMenuPermitted() - { - return (IsAllowed(Permission.TOMenu) && GetSettingsBool(Setting.vmenu_enable_time_sync)) || - (IsAllowed(Permission.WOMenu) && GetSettingsBool(Setting.vmenu_enable_weather_sync)); - } - #endregion } }