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)
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.
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
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
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.
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;
}
}
}
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