(C#) MenuAPI - MAPI v3.0.3 [RedM & FiveM]

I think the correct usage is:
Having a resource named MenuAPI which contains the MenuAPI.net.dll as client script, so

resource_manifest_version '44febabe-d386-4d18-afbe-5e627f4af937'

client_script 'MenuAPI.net.dll'

then all the exports once available.

Then each script uses it. Eg. using the TestMenu would mean to have a resource called TestMenu in this case with such resource.lua

resource_manifest_version '44febabe-d386-4d18-afbe-5e627f4af937'

dependency 'MenuAPI'

client_scripts {
	'@MenuAPI/MenuAPI.net.dll',
	'TestMenu.net.dll'
}

The only question I have is if '@MenuAPI/MenuAPI.net.dll' will create another instance of MenuAPI itself
(We only want one instance of the script running and then all the other scripts to use exports in order to add menus to the shared controller and manage them)

1 Like

I guess so, because loading it from the files section also made an instance of it for vMenu.

But yeah the wrapper would probably be something like that. However, for just a simple resource, you could just add it to your own resource files because you might want to use a specific version of this API. Having one resource where all resources can ‘hook’ to get the file would mean all resources would require the same version of the MenuAPI.

Something the wrapper could possibly prevent.

Using exports will mean that such scripts like TestMenu project won’t have any dependency from MenuAPI assembly, so in that case '@MenuAPI/MenuAPI.net.dll' won’t be required, the only required thing will be dependency 'MenuAPI' as we want to be sure it’s running

Still the API version could be a problem. Unless the wrapper manages to correctly handle this.

Well yes, but will make everything easier avoiding to have multiple instances for all the scripts each binded on their own control, all you have to do is to open the interaction menu and choose the menu for your script, then a submenu will open or an empty one as placeholder if some conditions aren’t true, for example if such submenu is for the handling editor but you aren’t in a vehicle. Everything would be managed by a single instance with the main menu being a stack of all the menus made by each script and each one will manage itself. it’s a lot of work ofc and a lot of exports are required

That’s odd. Do you directly rely on BaseScript instantiation? If so, that should not work ever at all from a file.

just one though, maybe another to remove something

easy fix: keep the API compatible at all times

I don’t think so. Would have to check the source code again but I don’t think I do.

It seems you do for MenuController’s ticks. If this actually gets instantiated from a dependent DLL that’s likely a bug, but probably a bug people ended up relying on. :confused:

I’m not planning on removing or changing features perse, just adding new ones might be an issue when people don’t update the main api.

Only one way to find out :stuck_out_tongue: canary build?

that wouldn’t matter much though :open_mouth: could just expose a exports.MenuAPI:GetAPILevel() or so that resources could check just to be sure.

1 Like

So using files {} to load it is actually a bug. Good to know. At least I know now :smile:

Made a pull request to the project to fix it on resolutions used by ~35% of FiveM players:

Also, an example of a export/reference-based wrapper, which could look like this:

local menu = exports.MenuAPI:CreateMenu('Mine!')

local item

item = menu.AddMenuItem('Say hello', function()
  TriggerEvent('chat:addMessage', {
    args = { 'Welcome to the party!~' }
  })

  item.SetLabel('Don\'t do that again :(')
end)

-- ...

menu.SetVisible(true)
menu.AddMenu()
WIP source code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using CitizenFX.Core;
using static CitizenFX.Core.Native.API;
 
namespace MenuAPI
{
    public class MenuWrapper : BaseScript
    {
        public MenuWrapper()
        {
            // don't add MenuWrapper exports if this is a resource that merely includes us as a C# dependency
            if (GetCurrentResourceName() != "MenuAPI")
            {
                return;
            }
 
            Exports.Add("CreateMenu", new Func<string, string, object>(CreateMenu));
        }
 
        private object CreateMenu(string title, string subtitle)
        {
            var menu = new Menu(title, subtitle);
 
            return WrapMenu(menu);
        }
 
        private object WrapMenu(Menu menu)
        {
            return WrapClass(new MenuProxy(menu));
        }
 
        private static object WrapClass(object instance)
        {
            var type = instance.GetType();
            var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance);
 
            var retval = new Dictionary<string, object>();
 
            foreach (var method in methods)
            {
                var delegType = Expression.GetDelegateType(
                    method.GetParameters()
                          .Select(param => param.ParameterType)
                          .Concat(new[] { method.ReturnType })
                          .ToArray()
                );
 
                retval[method.Name] = method.CreateDelegate(delegType, instance);
            }
 
            return retval;
        }
 
        private class MenuProxy
        {
            private readonly Menu menu;
 
            private readonly IDictionary<MenuItem, CallbackDelegate> callbacks = new Dictionary<MenuItem, CallbackDelegate>();
 
            public MenuProxy(Menu menu)
            {
                this.menu = menu;
 
                this.menu.OnItemSelect += Menu_OnItemSelect;
            }
 
            private void Menu_OnItemSelect(Menu menu, MenuItem menuItem, int itemIndex)
            {
                if (callbacks.TryGetValue(menuItem, out var deleg))
                {
                    deleg();
                }
            }
 
            public bool GetVisible() => menu.Visible;
 
            public void SetVisible(bool visible) => menu.Visible = visible;
 
            public int GetCurrentIndex() => menu.CurrentIndex;
 
            public bool GetEnableInstructionalButtons() => menu.EnableInstructionalButtons;
 
            public void SetEnableInstructionalButtons(bool enable) => menu.EnableInstructionalButtons = enable;
 
            public IEnumerable<object> GetMenuItems() => menu.GetMenuItems().Select(item => WrapClass(new MenuItemProxy(item, this)));
 
            public void ClearMenuItems(bool dontResetIndex) => menu.ClearMenuItems(dontResetIndex);
 
            public void AddMenu()
            {
                MenuController.AddMenu(menu);
            }
 
            public object AddMenuItem(string text, CallbackDelegate callback, IDictionary<string, object> args)
            {
                var description = GetDictValue<string>(args, "description");
 
                var menuItem = new MenuItem(text, description);
 
                // initialize common properties
                InitializeMenuItem(menuItem, args);
 
                // add the callback
                callbacks[menuItem] = callback;
 
                // add the menu item
                menu.AddMenuItem(menuItem);
 
                // return a proxy
                return WrapClass(new MenuItemProxy(menuItem, this));
            }
 
            internal void RemoveItem(MenuItem item)
            {
                menu.RemoveMenuItem(item);
 
                callbacks.Remove(item);
            }
 
            private static void InitializeMenuItem(MenuItem menuItem, IDictionary<string, object> args)
            {
                menuItem.LeftIcon = GetEnumValue(GetDictValue<string>(args, "leftIcon"), MenuItem.Icon.NONE);
                menuItem.RightIcon = GetEnumValue(GetDictValue<string>(args, "rightIcon"), MenuItem.Icon.NONE);
            }
 
            private static T GetEnumValue<T>(string str, T defaultValue = default(T)) where T : struct
            {
                if (str != null)
                {
                    if (Enum.TryParse(str, out T value))
                    {
                        return value;
                    }
                }
 
                return defaultValue;
            }
 
            private static T GetDictValue<T>(IDictionary<string, object> argDict, string key, T defaultValue = default(T))
            {
                if (argDict != null)
                {
                    if (argDict.TryGetValue(key, out object value))
                    {
                        if (value is T typedValue)
                        {
                            return typedValue;
                        }
                    }
                }
 
                return defaultValue;
            }
        }
 
        private class MenuItemProxy
        {
            private readonly MenuItem menuItem;
 
            private readonly MenuProxy parent;
 
            public MenuItemProxy(MenuItem menuItem, MenuProxy parent)
            {
                this.menuItem = menuItem;
 
                this.parent = parent;
            }
 
            public void Remove()
            {
                parent.RemoveItem(menuItem);
            }
 
            public bool IsSelected() => menuItem.Selected;
 
            public string GetLabel() => menuItem.Label;
 
            public void SetLabel(string label) => menuItem.Label = label;
        }
    }
}
2 Likes

if i find some spare time, maybe i can do that, seems fun :joy:

1 Like

v1.2

  • Merged PR https://github.com/TomGrobbe/MenuAPI/pull/1
  • Fix hardcoded ‘width’ values of 500, they now use the Width constant
    Needed for future changes when PR is merged. Also fixed the scaleform relative height remaining intact, although this might fuckup on different aspect ratios.
  • Rename natives now that the native reference is updated

hello need help please ca me but that’s when I put in the .cfg etc … who could give me a hand please thank youSans%20titre

Show us you MenuAPI resource folder and the content of the resource.lua
I’ve posted an example above of how it should be

do not have resource.lua in my menuAPI folder