diff --git a/vMenu/Exports.cs b/vMenu/Exports.cs new file mode 100644 index 000000000..9c2b4b46f --- /dev/null +++ b/vMenu/Exports.cs @@ -0,0 +1,1097 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using CitizenFX.Core; +using MenuAPI; +using Newtonsoft.Json; + +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(); + + /// + /// 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 + + /// + /// 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) + { + 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; + } + + /// + /// 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; + } + + /// + /// 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 + /// Error context + /// Menu instance if found, null otherwise + private Menu RetrieveMenu(string menuId, string context = null) + { + if (DynamicMenus.TryGetValue(menuId, out Menu menu)) + { + return menu; + } + + menu = GetMenu(menuId); + + if (menu == null && !string.IsNullOrEmpty(context)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] {context}: Menu with ID '{menuId}' not found."); + } + + return menu; + } + + /// + /// 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; + } + } + } + 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) + { + // 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); + } + } + }; + + // Listen for item selection (pressing enter/select) + menu.OnListItemSelect += (sender, menuItem, listIndex, itemIndex) => + { + if (menuItem == item) + { + // Bounds check to prevent ArgumentOutOfRangeException + if (listIndex >= 0 && listIndex < options.Count) + { + 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 + /// + 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(); + }); + } + + /// + /// 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)); + + // Menu Information + Exports.Add("GetAllMenuIds", new Func(GetAllMenuIds)); + Exports.Add("GetMenu", new Func(GetMenuExport)); + + // Ready State Management + Exports.Add("OnReady", new Action(OnReadyExport)); + Exports.Add("IsReady", new Func(IsReadyExport)); + } + + #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) + { + if (!ValidateParameter(menuId, "menuId", "CreateMenu")) return; + if (HasIdConflict(menuId, "CreateMenu")) return; + + // Apply defaults for optional parameters + if (string.IsNullOrEmpty(menuTitle)) + menuTitle = "Menu"; + if (string.IsNullOrEmpty(menuDescription)) + menuDescription = ""; + + // Cache player name at creation time to prevent dynamic changes + var playerName = Game.Player.Name; + var newMenu = new Menu(playerName, menuTitle) + { + MenuSubtitle = menuDescription + }; + MenuController.AddMenu(newMenu); + DynamicMenus[menuId] = newMenu; + + AttachMenuOpenCallback(newMenu, callbackObj, menuId); + } + + /// + /// Displays notification to the player + /// + /// Notification message + /// Notification type (error, info, success, or default) + private void notifExp(string message, string type) + { + 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; + } + } + + /// + /// Opens a menu by ID + /// + /// Menu identifier + private void OpenMenu(string menuId) + { + if (!ValidateParameter(menuId, "menuId", "OpenMenu")) return; + + Menu menu = RetrieveMenu(menuId, "OpenMenu"); + if (menu == null) return; + + // Close all currently open menus before opening the new one + MenuController.CloseAllMenus(); + + // Open the requested menu + menu.OpenMenu(); + } + + /// + /// Adds a button item to a menu + /// + /// Target menu ID + /// Unique button identifier + /// Button label + /// Button description + /// 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; + } + + if (!ValidateParameter(buttonId, "buttonId", "AddButton")) return; + if (HasIdConflict(buttonId, "AddButton")) return; + + Menu menu = RetrieveMenu(menuId, "AddButton"); + if (menu == null) return; + + 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, + Label = rightLabel ?? "" + }; + + AttachItemSelectCallback(menu, menuItem, callbackObj, buttonId); + 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 parameters + if (!ValidateParameter(listId, "listId", "AddList")) return; + if (HasIdConflict(listId, "AddList")) return; + + Menu menu = RetrieveMenu(menuId, "AddList"); + if (menu == null) return; + + if (HasItemConflict(menu, listId, "AddList")) return; + + // Apply defaults for optional parameters + listLabel = string.IsNullOrEmpty(listLabel) ? "List" : listLabel; + description = string.IsNullOrEmpty(description) ? "" : description; + + // Parse options based on input type + List optionsList = options switch + { + 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] No valid options provided for list {listId}. List item will not be added."); + return; + } + + // 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 + }; + + AttachListCallbacks(menu, menuListItem, optionsList, callbackObj, listId); + menu.AddMenuItem(menuListItem); + 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 + /// + /// 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) + { + if (!ValidateParameter(menuId, "menuId", "AddCheckbox")) return; + if (!ValidateParameter(checkboxId, "checkboxId", "AddCheckbox")) return; + + Menu menu = RetrieveMenu(menuId, "AddCheckbox"); + if (menu == null) return; + + if (HasIdConflict(checkboxId, "AddCheckbox")) return; + 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 + }; + + AttachCheckboxCallback(menu, menuCheckboxItem, callbackObj, checkboxId); + 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) + { + if (!ValidateParameter(menuId, "menuId", "AddSlider")) return; + if (!ValidateParameter(sliderId, "sliderId", "AddSlider")) return; + + Menu menu = RetrieveMenu(menuId, "AddSlider"); + if (menu == null) return; + + if (HasIdConflict(sliderId, "AddSlider")) return; + 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 + }; + + AttachSliderCallback(menu, menuSliderItem, callbackObj, sliderId); + 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) + { + if (!ValidateParameter(menuId, "menuId", "AddSpacer")) return; + if (!ValidateParameter(spacerId, "spacerId", "AddSpacer")) return; + + Menu menu = RetrieveMenu(menuId, "AddSpacer"); + if (menu == null) return; + + if (HasIdConflict(spacerId, "AddSpacer")) return; + 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) + { + if (!ValidateParameter(menuId, "menuId", "RefreshMenu")) return; + + Menu menu = RetrieveMenu(menuId, "RefreshMenu"); + if (menu == null) 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) + { + if (!ValidateParameter(menuId, "menuId", "CloseMenu")) return; + + Menu menu = RetrieveMenu(menuId, "CloseMenu"); + if (menu == null) 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) + { + if (!ValidateParameter(parentMenuId, "parentMenuId", "AddSubmenuButton")) return; + if (!ValidateParameter(buttonId, "buttonId", "AddSubmenuButton")) return; + if (!ValidateParameter(submenuId, "submenuId", "AddSubmenuButton")) return; + + 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 = RetrieveMenu(parentMenuId, "AddSubmenuButton (parent)"); + if (parentMenu == null) return; + + Menu submenu = RetrieveMenu(submenuId, "AddSubmenuButton (submenu)"); + if (submenu == null) return; + + 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(); + + AttachItemSelectCallback(parentMenu, submenuButton, callbackObj, buttonId); + parentMenu.RefreshIndex(); + } + + /// + /// Removes an item from a menu by ID (string) or index (int) + /// + /// Target menu ID + /// Item identifier (string) or index (int) to remove + private void RemoveItem(string menuId, object itemIdOrIndex) + { + if (!ValidateParameter(menuId, "menuId", "RemoveItem")) return; + + if (itemIdOrIndex == null) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] RemoveItem: itemIdOrIndex cannot be null."); + return; + } + + Menu menu = RetrieveMenu(menuId, "RemoveItem"); + if (menu == null) return; + + var items = menu.GetMenuItems(); + + // 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; + } + 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) + { + 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] RemoveItem: Item with ID {itemId} not found in menu {menuId}."); + } + } + else + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] RemoveItem: itemIdOrIndex must be a string or int."); + } + } + + /// + /// Clears all items from a menu + /// + /// Target menu ID + private void ClearMenu(string menuId) + { + if (!ValidateParameter(menuId, "menuId", "ClearMenu")) return; + + Menu menu = RetrieveMenu(menuId, "ClearMenu"); + if (menu == null) return; + + menu.ClearMenuItems(); + } + + /// + /// Checks if a menu exists + /// + /// Menu ID to check + /// True if menu exists, false otherwise + private bool CheckMenu(string menuId) + { + if (!ValidateParameter(menuId, "menuId", "CheckMenu")) return false; + + Menu menu = RetrieveMenu(menuId); + return menu != null; + } + + /// + /// Deletes a dynamically created menu + /// + /// Menu ID to delete + private void DeleteMenu(string menuId) + { + if (!ValidateParameter(menuId, "menuId", "DeleteMenu")) return; + + if (!DynamicMenus.TryGetValue(menuId, out Menu menu)) + { + CitizenFX.Core.Debug.WriteLine($"[vMenu] DeleteMenu: 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 (without permission filtering) + /// + /// Menu identifier string + /// Menu instance if found, null otherwise + private Menu GetMenu(string menuId) + { + // 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(" ", "-"); + + // Exact match only + if (normalizedTitle == normalizedId) + { + return menu; + } + } + + return null; + } + + /// + /// Export wrapper for GetMenu that returns an object (for export compatibility) + /// + /// Menu identifier + /// Menu instance as object if found, null otherwise + private object GetMenuExport(string 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; + } + } + + /// + /// 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(); + + // Add dynamic menu IDs + foreach (var kvp in DynamicMenus) + { + menuIds.Add(kvp.Key); + } + + // Add built-in menu IDs based on their subtitles (avoid duplicates) + try + { + 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); + } + } + 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 + return menuIds.Distinct().OrderBy(id => id).ToArray(); + } + + /// + /// 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(); + } + + #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