----------------------- [ MenuV ] ----------------------- -- GitHub: https://github.com/ThymonA/menuv/ -- License: GNU General Public License v3.0 -- https://choosealicense.com/licenses/gpl-3.0/ -- Author: Thymon Arens -- Name: MenuV -- Version: 1.0.0 -- Description: FiveM menu library for creating menu's ----------------------- [ MenuV ] ----------------------- local assert = assert local pairs = assert(pairs) local rawget = assert(rawget) local rawset = assert(rawset) local insert = assert(table.insert) local remove = assert(table.remove) local format = assert(string.format) local upper = assert(string.upper) local lower = assert(string.lower) local setmetatable = assert(setmetatable) --- FiveM globals local GET_CURRENT_RESOURCE_NAME = assert(GetCurrentResourceName) local HAS_STREAMED_TEXTURE_DICT_LOADED = assert(HasStreamedTextureDictLoaded) local REQUEST_STREAMED_TEXTURE_DICT = assert(RequestStreamedTextureDict) local REGISTER_KEY_MAPPING = assert(RegisterKeyMapping) local REGISTER_COMMAND = assert(RegisterCommand) local GET_HASH_KEY = assert(GetHashKey) local CreateThread = assert(Citizen.CreateThread) local Wait = assert(Citizen.Wait) ---@load 'config.lua' ---@load 'app/lua_components/utilities.lua' ---@load 'app/lua_components/item.lua' ---@load 'app/lua_components/menu.lua' ---@load 'app/lua_components/translations.lua' --- MenuV table local menuv_table = { ---@type string __class = 'MenuV', ---@type string __type = 'MenuV', ---@type Menu|nil CurrentMenu = nil, ---@type string|nil CurrentUpdateUUID = nil, ---@type string CurrentResourceName = GET_CURRENT_RESOURCE_NAME(), ---@type boolean Loaded = false, ---@type Menu[] Menus = {}, ---@type Menu[] ParentMenus = {}, ---@type table NUICallbacks = {}, ---@type table Translations = translations, ---@class keys Keys = setmetatable({ data = {}, __class = 'MenuVKeys', __type = 'keys' }, { __index = function(t, k) return rawget(t.data, k) end, __newindex = function(t, actionHax, v) actionHax = Utilities:Ensure(actionHax, 'unknown') if (actionHax == 'unknown') then return end local rawKey = rawget(t.data, actionHax) local keyExists = rawKey ~= nil local prevState = Utilities:Ensure((rawKey or {}).status, false) local newState = Utilities:Ensure(v, false) if (keyExists) then rawset(t.data[actionHax], 'status', newState) if (prevState ~= newState and newState) then rawKey.func(rawKey.menu) end end end, __call = function(t, actionHax, m, actionFunc, inputType) actionHax = Utilities:Ensure(actionHax, 'unknown') m = Utilities:Typeof(m) == 'Menu' and m or nil actionFunc = Utilities:Ensure(actionFunc, function() end) inputType = Utilities:Ensure(inputType, 'KEYBOARD') inputType = upper(inputType) if (actionHax == 'unknown') then return end local rawKey = rawget(t.data, actionHax) local keyExists = rawKey ~= nil if (keyExists) then if not rawKey.inputTypes[inputType] then rawKey.inputTypes[inputType] = true end return end rawset(t.data, actionHax, { status = false, menu = m, func = actionFunc, inputTypes = { [inputType] = true } }) end }) } ---@class MenuV MenuV = setmetatable(menuv_table, {}) --- Send a NUI message to MenuV resource ---@param input any local SEND_NUI_MESSAGE = function(input) exports['menuv']:SendNUIMessage(input) end --- Register a NUI callback event ---@param name string Name of callback ---@param cb function Callback to execute local REGISTER_NUI_CALLBACK = function(name, cb) name = Utilities:Ensure(name, 'unknown') cb = Utilities:Ensure(cb, function(_, cb) cb('ok') end) MenuV.NUICallbacks[name] = cb end --- Load translation ---@param k string Translation key ---@return string Translation or 'MISSING TRANSLATION' function MenuV:T(k) k = Utilities:Ensure(k, 'unknown') return Utilities:Ensure(MenuV.Translations[k], 'MISSING TRANSLATION') end --- Create a `MenuV` menu ---@param title string Title of Menu ---@param subtitle string Subtitle of Menu ---@param position string Position of Menu ---@param r number 0-255 RED ---@param g number 0-255 GREEN ---@param b number 0-255 BLUE ---@param size string | "'size-100'" | "'size-110'" | "'size-125'" | "'size-150'" | "'size-175'" | "'size-200'" ---@param texture string Name of texture example: "default" ---@param dictionary string Name of dictionary example: "menuv" ---@param namespace string Namespace of Menu ---@param theme string Theme of Menu ---@return Menu function MenuV:CreateMenu(title, subtitle, position, r, g, b, size, texture, dictionary, namespace, theme) local menu = CreateMenu({ Theme = theme, Title = title, Subtitle = subtitle, Position = position, R = r, G = g, B = b, Size = size, Texture = texture, Dictionary = dictionary, Namespace = namespace }) local index = #(self.Menus or {}) + 1 insert(self.Menus, index, menu) return self.Menus[index] or menu end --- Create a menu that inherits properties from another menu ---@param parent Menu|string Menu or UUID of menu ---@param overrides table Properties to override in menu object (ignore parent) ---@param namespace string Namespace of menu function MenuV:InheritMenu(parent, overrides, namespace) overrides = Utilities:Ensure(overrides, {}) local uuid = Utilities:Typeof(parent) == 'Menu' and parent.UUID or Utilities:Typeof(parent) == 'string' and parent if (uuid == nil) then return end local parentMenu = self:GetMenu(uuid) if (parentMenu == nil) then return end local menu = CreateMenu({ Theme = Utilities:Ensure(overrides.theme or overrides.Theme, parentMenu.Theme), Title = Utilities:Ensure(overrides.title or overrides.Title, parentMenu.Title), Subtitle = Utilities:Ensure(overrides.subtitle or overrides.Subtitle, parentMenu.Subtitle), Position = Utilities:Ensure(overrides.position or overrides.Position, parentMenu.Position), R = Utilities:Ensure(overrides.r or overrides.R, parentMenu.Color.R), G = Utilities:Ensure(overrides.g or overrides.G, parentMenu.Color.G), B = Utilities:Ensure(overrides.b or overrides.B, parentMenu.Color.B), Size = Utilities:Ensure(overrides.size or overrides.Size, parentMenu.Size), Texture = Utilities:Ensure(overrides.texture or overrides.Texture, parentMenu.Texture), Dictionary = Utilities:Ensure(overrides.dictionary or overrides.Dictionary, parentMenu.Dictionary), Namespace = Utilities:Ensure(namespace, 'unknown') }) local index = #(self.Menus or {}) + 1 insert(self.Menus, index, menu) return self.Menus[index] or menu end --- Load a menu based on `uuid` ---@param uuid string UUID of menu ---@return Menu|nil Founded menu or `nil` function MenuV:GetMenu(uuid) uuid = Utilities:Ensure(uuid, '00000000-0000-0000-0000-000000000000') for _, v in pairs(self.Menus) do if (v.UUID == uuid) then return v end end return nil end --- Open a menu ---@param menu Menu|string Menu or UUID of Menu ---@param cb function Execute this callback when menu has opened function MenuV:OpenMenu(menu, cb, reopen) local uuid = Utilities:Typeof(menu) == 'Menu' and menu.UUID or Utilities:Typeof(menu) == 'string' and menu if (uuid == nil) then return end cb = Utilities:Ensure(cb, function() end) menu = self:GetMenu(uuid) if (menu == nil) then return end local dictionaryLoaded = HAS_STREAMED_TEXTURE_DICT_LOADED(menu.Dictionary) if (not self.Loaded or not dictionaryLoaded) then if (not dictionaryLoaded) then REQUEST_STREAMED_TEXTURE_DICT(menu.Dictionary) end CreateThread(function() repeat Wait(0) until MenuV.Loaded if (not dictionaryLoaded) then repeat Wait(10) until HAS_STREAMED_TEXTURE_DICT_LOADED(menu.Dictionary) end MenuV:OpenMenu(uuid, cb) end) return end if (self.CurrentMenu ~= nil) then insert(self.ParentMenus, self.CurrentMenu) self.CurrentMenu:RemoveOnEvent('update', self.CurrentUpdateUUID) self.CurrentMenu:DestroyThreads() end self.CurrentMenu = menu self.CurrentUpdateUUID = menu:On('update', function(m, k, v) k = Utilities:Ensure(k, 'unknown') if (k == 'Title' or k == 'title') then SEND_NUI_MESSAGE({ action = 'UPDATE_TITLE', title = Utilities:Ensure(v, 'MenuV'), __uuid = m.UUID }) elseif (k == 'Subtitle' or k == 'subtitle') then SEND_NUI_MESSAGE({ action = 'UPDATE_SUBTITLE', subtitle = Utilities:Ensure(v, ''), __uuid = m.UUID }) elseif (k == 'Items' or k == 'items') then SEND_NUI_MESSAGE({ action = 'UPDATE_ITEMS', items = (m.Items:ToTable() or {}), __uuid = m.UUID }) elseif (k == 'Item' or k == 'item' and Utilities:Typeof(v) == 'Item') then SEND_NUI_MESSAGE({ action = 'UPDATE_ITEM', item = m.Items:ItemToTable(v) or {}, __uuid = m.UUID }) elseif (k == 'AddItem' or k == 'additem' and Utilities:Typeof(v) == 'Item') then SEND_NUI_MESSAGE({ action = 'ADD_ITEM', item = m.Items:ItemToTable(v), __uuid = m.UUID }) elseif (k == 'RemoveItem' or k == 'removeitem' and Utilities:Typeof(v) == 'Item') then SEND_NUI_MESSAGE({ action = 'REMOVE_ITEM', uuid = v.UUID, __uuid = m.UUID }) elseif (k == 'UpdateItem' or k == 'updateitem' and Utilities:Typeof(v) == 'Item') then SEND_NUI_MESSAGE({ action = 'UPDATE_ITEM', item = m.Items:ItemToTable(v) or {}, __uuid = m.UUID }) end end) SEND_NUI_MESSAGE({ action = 'OPEN_MENU', menu = menu:ToTable(), reopen = Utilities:Ensure(reopen, false) }) cb() end function MenuV:Refresh() if (self.CurrentMenu == nil) then return end SEND_NUI_MESSAGE({ action = 'REFRESH_MENU', menu = self.CurrentMenu:ToTable() }) end --- Close a menu ---@param menu Menu|string Menu or UUID of Menu ---@param cb function Execute this callback when menu has is closed or parent menu has opened function MenuV:CloseMenu(menu, cb) local uuid = Utilities:Typeof(menu) == 'Menu' and menu.UUID or Utilities:Typeof(menu) == 'string' and menu if (uuid == nil) then cb() return end cb = Utilities:Ensure(cb, function() end) menu = self:GetMenu(uuid) if (menu == nil or self.CurrentMenu == nil or self.CurrentMenu.UUID ~= uuid) then cb() return end self.CurrentMenu:RemoveOnEvent('update', self.CurrentUpdateUUID) self.CurrentMenu:Trigger('close') self.CurrentMenu:DestroyThreads() self.CurrentMenu = nil SEND_NUI_MESSAGE({ action = 'CLOSE_MENU', uuid = uuid }) if (#self.ParentMenus <= 0) then cb() return end local prev_index = #self.ParentMenus local prev_menu = self.ParentMenus[prev_index] or nil if (prev_menu == nil) then cb() return end remove(self.ParentMenus, prev_index) self:OpenMenu(prev_menu, function() cb() end, true) end --- Close all menus ---@param cb function Execute this callback when all menus are closed function MenuV:CloseAll(cb) cb = Utilities:Ensure(cb, function() end) if (not self.Loaded) then CreateThread(function() repeat Wait(0) until MenuV.Loaded MenuV:CloseAll(cb) end) return end if (self.CurrentMenu == nil) then cb() return end local uuid = Utilities:Ensure(self.CurrentMenu.UUID, '00000000-0000-0000-0000-000000000000') self.CurrentMenu:RemoveOnEvent('update', self.CurrentUpdateUUID) self.CurrentMenu:Trigger('close') self.CurrentMenu:DestroyThreads() SEND_NUI_MESSAGE({ action = 'CLOSE_MENU', uuid = uuid }) self.CurrentMenu = nil self.ParentMenus = {} cb() end --- Register keybind for specific menu ---@param menu Menu|string MenuV menu ---@param action string Name of action ---@param func function This will be executed ---@param description string Key description ---@param defaultType string Default key type ---@param defaultKey string Default key function MenuV:AddControlKey(menu, action, func, description, defaultType, defaultKey) local uuid = Utilities:Typeof(menu) == 'Menu' and menu.UUID or Utilities:Typeof(menu) == 'string' and menu action = Utilities:Ensure(action, 'UNKNOWN') func = Utilities:Ensure(func, function() end) description = Utilities:Ensure(description, 'unknown') defaultType = Utilities:Ensure(defaultType, 'KEYBOARD') defaultType = upper(defaultType) defaultKey = Utilities:Ensure(defaultKey, 'F12') local m = self:GetMenu(uuid) if (m == nil) then return end if (Utilities:Typeof(m.Namespace) ~= 'string' or m.Namespace == 'unknown') then error('[MenuV] Namespace is required for assigning keys.') return end action = Utilities:Replace(action, ' ', '_') action = upper(action) local resourceName = Utilities:Ensure(self.CurrentResourceName, 'unknown') local namespace = Utilities:Ensure(m.Namespace, 'unknown') local actionHash = GET_HASH_KEY(('%s_%s_%s'):format(resourceName, namespace, action)) local actionHax = format('%x', actionHash) local typeGroup = Utilities:GetInputTypeGroup(defaultType) if (self.Keys[actionHax] and self.Keys[actionHax].inputTypes[typeGroup]) then return end self.Keys(actionHax, m, func, typeGroup) local k = actionHax if typeGroup > 0 then local inputGroupName = Utilities:GetInputGroupName(typeGroup) k = ('%s_%s'):format(lower(inputGroupName), k) end REGISTER_KEY_MAPPING(('+%s'):format(k), description, defaultType, defaultKey) REGISTER_COMMAND(('+%s'):format(k), function() MenuV.Keys[actionHax] = true end) REGISTER_COMMAND(('-%s'):format(k), function() MenuV.Keys[actionHax] = false end) end --- Checks if namespace is available ---@param namespace string Namespace ---@return boolean Returns `true` if given namespace is available function MenuV:IsNamespaceAvailable(namespace) namespace = lower(Utilities:Ensure(namespace, 'unknown')) if (namespace == 'unknown') then return true end ---@param v Menu for k, v in pairs(self.Menus or {}) do local v_namespace = Utilities:Ensure(v.Namespace, 'unknown') if (namespace == lower(v_namespace)) then return false end end return true end --- Mark MenuV as loaded when `main` resource is loaded exports['menuv']:IsLoaded(function() MenuV.Loaded = true end) --- Register callback handler for MenuV exports('NUICallback', function(name, info, cb) name = Utilities:Ensure(name, 'unknown') if (MenuV.NUICallbacks == nil or MenuV.NUICallbacks[name] == nil) then return end MenuV.NUICallbacks[name](info, cb) end) REGISTER_NUI_CALLBACK('open', function(info, cb) local uuid = Utilities:Ensure(info.uuid, '00000000-0000-0000-0000-000000000000') local new_uuid = Utilities:Ensure(info.new_uuid, '00000000-0000-0000-0000-000000000000') cb('ok') if (MenuV.CurrentMenu == nil or MenuV.CurrentMenu.UUID == uuid or MenuV.CurrentMenu.UUID == new_uuid) then return end for _, v in pairs(MenuV.ParentMenus) do if (v.UUID == uuid) then return end end MenuV.CurrentMenu:RemoveOnEvent('update', MenuV.CurrentUpdateUUID) MenuV.CurrentMenu:Trigger('close') MenuV.CurrentMenu:DestroyThreads() MenuV.CurrentMenu = nil MenuV.ParentMenus = {} end) REGISTER_NUI_CALLBACK('opened', function(info, cb) local uuid = Utilities:Ensure(info.uuid, '00000000-0000-0000-0000-000000000000') cb('ok') if (MenuV.CurrentMenu == nil or MenuV.CurrentMenu.UUID ~= uuid) then return end MenuV.CurrentMenu:Trigger('open') end) REGISTER_NUI_CALLBACK('submit', function(info, cb) local uuid = Utilities:Ensure(info.uuid, '00000000-0000-0000-0000-000000000000') cb('ok') if (MenuV.CurrentMenu == nil) then return end for k, v in pairs(MenuV.CurrentMenu.Items) do if (v.UUID == uuid) then if (v.__type == 'confirm' or v.__type == 'checkbox') then v.Value = Utilities:Ensure(info.value, false) elseif (v.__type == 'range') then v.Value = Utilities:Ensure(info.value, v.Min) elseif (v.__type == 'slider') then v.Value = Utilities:Ensure(info.value, 0) + 1 end MenuV.CurrentMenu:Trigger('select', v) if (v.__type == 'button' or v.__type == 'menu') then MenuV.CurrentMenu.Items[k]:Trigger('select') elseif (v.__type == 'range') then MenuV.CurrentMenu.Items[k]:Trigger('select', v.Value) elseif (v.__type == 'slider') then local option = MenuV.CurrentMenu.Items[k].Values[v.Value] or nil if (option == nil) then return end MenuV.CurrentMenu.Items[k]:Trigger('select', option.Value) end return end end end) REGISTER_NUI_CALLBACK('close', function(info, cb) local uuid = Utilities:Ensure(info.uuid, '00000000-0000-0000-0000-000000000000') if (MenuV.CurrentMenu == nil or MenuV.CurrentMenu.UUID ~= uuid) then cb('ok') return end MenuV.CurrentMenu:RemoveOnEvent('update', MenuV.CurrentUpdateUUID) MenuV.CurrentMenu:Trigger('close') MenuV.CurrentMenu:DestroyThreads() MenuV.CurrentMenu = nil if (#MenuV.ParentMenus <= 0) then cb('ok') return end local prev_index = #MenuV.ParentMenus local prev_menu = MenuV.ParentMenus[prev_index] or nil if (prev_menu == nil) then cb('ok') return end remove(MenuV.ParentMenus, prev_index) MenuV:OpenMenu(prev_menu, function() cb('ok') end, true) end) REGISTER_NUI_CALLBACK('close_all', function(info, cb) if (MenuV.CurrentMenu == nil) then for _, parentMenu in ipairs(MenuV.ParentMenus) do parentMenu:Trigger('close') end MenuV.ParentMenus = {} cb('ok') return end MenuV.CurrentMenu:RemoveOnEvent('update', MenuV.CurrentUpdateUUID) MenuV.CurrentMenu:Trigger('close') MenuV.CurrentMenu:DestroyThreads() MenuV.CurrentMenu = nil for _, parentMenu in ipairs(MenuV.ParentMenus) do parentMenu:Trigger('close') end MenuV.ParentMenus = {} cb('ok') end) REGISTER_NUI_CALLBACK('switch', function(info, cb) local prev_uuid = Utilities:Ensure(info.prev, '00000000-0000-0000-0000-000000000000') local next_uuid = Utilities:Ensure(info.next, '00000000-0000-0000-0000-000000000000') local prev_item, next_item = nil, nil cb('ok') if (MenuV.CurrentMenu == nil) then return end for k, v in pairs(MenuV.CurrentMenu.Items) do if (v.UUID == prev_uuid) then prev_item = v MenuV.CurrentMenu.Items[k]:Trigger('leave') end if (v.UUID == next_uuid) then next_item = v MenuV.CurrentMenu.Items[k]:Trigger('enter') end end if (prev_item ~= nil and next_item ~= nil) then MenuV.CurrentMenu:Trigger('switch', next_item, prev_item) end end) REGISTER_NUI_CALLBACK('update', function(info, cb) local uuid = Utilities:Ensure(info.uuid, '00000000-0000-0000-0000-000000000000') cb('ok') if (MenuV.CurrentMenu == nil) then return end for k, v in pairs(MenuV.CurrentMenu.Items) do if (v.UUID == uuid) then local newValue, oldValue = nil, nil if (v.__type == 'confirm' or v.__type == 'checkbox') then newValue = Utilities:Ensure(info.now, false) oldValue = Utilities:Ensure(info.prev, false) elseif (v.__type == 'range') then newValue = Utilities:Ensure(info.now, v.Min) oldValue = Utilities:Ensure(info.prev, v.Min) elseif (v.__type == 'slider') then newValue = Utilities:Ensure(info.now, 0) + 1 oldValue = Utilities:Ensure(info.prev, 0) + 1 end if (Utilities:Any(v.__type, { 'button', 'menu', 'label' }, 'value')) then return end MenuV.CurrentMenu:Trigger('update', v, newValue, oldValue) MenuV.CurrentMenu.Items[k]:Trigger('change', newValue, oldValue) if (v.SaveOnUpdate) then MenuV.CurrentMenu.Items[k].Value = newValue if (v.__type == 'range') then MenuV.CurrentMenu.Items[k]:Trigger('select', v.Value) elseif (v.__type == 'slider') then local option = MenuV.CurrentMenu.Items[k].Values[v.Value] or nil if (option == nil) then return end MenuV.CurrentMenu.Items[k]:Trigger('select', option.Value) end end return end end end)