----------------------- [ 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.4.1 -- 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) ----------------------- [ 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.4.1 -- Description: FiveM menu library for creating menu's ----------------------- [ MenuV ] ----------------------- Config = { Language = 'da', HideInterval = 250, Sounds = { UP = { type = 'native', name = 'NAV_UP_DOWN', library = 'HUD_FREEMODE_SOUNDSET' }, DOWN = { type = 'native', name = 'NAV_UP_DOWN', library = 'HUD_FREEMODE_SOUNDSET' }, LEFT = { type = 'native', name = 'NAV_LEFT_RIGHT', library = 'HUD_FRONTEND_DEFAULT_SOUNDSET' }, RIGHT = { type = 'native', name = 'NAV_LEFT_RIGHT', library = 'HUD_FRONTEND_DEFAULT_SOUNDSET' }, ENTER = { type = 'native', name = 'SELECT', library = 'HUD_FRONTEND_DEFAULT_SOUNDSET' }, CLOSE = { type = 'native', name = 'BACK', library = 'HUD_FRONTEND_DEFAULT_SOUNDSET' } } } _G.Config = Config _ENV.Config = Config ----------------------- [ 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.4.1 -- Description: FiveM menu library for creating menu's ----------------------- [ MenuV ] ----------------------- local assert = assert local type = assert(type) local tonumber = assert(tonumber) local tostring = assert(tostring) local lower = assert(string.lower) local upper = assert(string.upper) local sub = assert(string.sub) local encode = assert(json.encode) local decode = assert(json.decode) local floor = assert(math.floor) local random = assert(math.random) local randomseed = assert(math.randomseed) local rawget = assert(rawget) local setmetatable = assert(setmetatable) --- FiveM globals local GET_GAME_TIMER = assert(GetGameTimer) local GET_CURRENT_RESOURCE_NAME = assert(GetCurrentResourceName) --- Utilities for MenuV ---@class Utilities local Utilities = setmetatable({ __class = 'Utilities' }, {}) --- Returns `true` if `input` starts with `start`, otherwise `false` ---@param input string Checks if this string starts with `start` ---@param start string Checks if `input` starts with this ---@return boolean `true` if `input` starts with `start`, otherwise `false` function Utilities:StartsWith(input, start) if (self:Typeof(input) ~= 'string') then return false end if (self:Typeof(start) == 'number') then start = tostring(start) end if (self:Typeof(start) ~= 'string') then return false end return sub(input, 1, #start) == start end --- Returns `true` if `input` ends with `ends`, otherwise `false` ---@param input string Checks if this string ends with `ends` ---@param ends string Checks if `input` ends with this ---@return boolean `true` if `input` ends with `ends`, otherwise `false` function Utilities:EndsWith(input, ends) if (self:Typeof(input) ~= 'string') then return false end if (self:Typeof(ends) == 'number') then ends = tostring(ends) end if (self:Typeof(ends) ~= 'string') then return false end return sub(input, -#ends) == ends end --- Returns the type of given `input` ---@param input any Any input ---@return string Type of given input function Utilities:Typeof(input) if (input == nil) then return 'nil' end local rawType = type(input) or 'nil' if (rawType ~= 'table') then return rawType end local isFXFunction = rawget(input, '__cfx_functionReference') ~= nil or rawget(input, '__cfx_async_retval') ~= nil if (isFXFunction) then return 'function' end if (rawget(input, '__cfx_functionSource') ~= nil) then return 'number' end local rawClass = rawget(input, '__class') if (rawClass ~= nil) then return type(rawClass) == 'string' and rawClass or 'class' end local rawTableType = rawget(input, '__type') if (rawTableType ~= nil) then return type(rawTableType) == 'string' and rawTableType or 'table' end return rawType end local INPUT_GROUPS = { [0] = "KEYBOARD", [2] = "CONTROLLER" } local INPUT_TYPE_GROUPS = { ["KEYBOARD"] = 0, ["MOUSE_ABSOLUTEAXIS"] = 0, ["MOUSE_CENTEREDAXIS"] = 0, ["MOUSE_RELATIVEAXIS"] = 0, ["MOUSE_SCALEDAXIS"] = 0, ["MOUSE_NORMALIZED"] = 0, ["MOUSE_WHEEL"] = 0, ["MOUSE_BUTTON"] = 0, ["MOUSE_BUTTONANY"] = 0, ["MKB_AXIS"] = 0, ["PAD_AXIS"] = 2, ["PAD_DIGITALBUTTON"] = 2, ["PAD_DIGITALBUTTONANY"] = 2, ["PAD_ANALOGBUTTON"] = 2, ["JOYSTICK_POV"] = 2, ["JOYSTICK_POV_AXIS"] = 2, ["JOYSTICK_BUTTON"] = 2, ["JOYSTICK_AXIS"] = 2, ["JOYSTICK_IAXIS"] = 2, ["JOYSTICK_AXIS_NEGATIVE"] = 2, ["JOYSTICK_AXIS_POSITIVE"] = 2, ["PAD_DEBUGBUTTON"] = 2, ["GAME_CONTROLLED"] = 2, ["DIGITALBUTTON_AXIS"] = 2, } function Utilities:GetInputTypeGroup(inputType) return INPUT_TYPE_GROUPS[inputType] or 0 end function Utilities:GetInputGroupName(inputTypeGroup) return INPUT_GROUPS[inputTypeGroup] or "KEYBOARD" end --- Transform any `input` to the same type as `defaultValue` ---@type function ---@param input any Transform this `input` to `defaultValue`'s type ---@param defaultValue any Returns this if `input` can't transformed to this type ---@param ignoreDefault boolean Don't return default value if this is true ---@return any Returns `input` matches the `defaultValue` type or `defaultValue` function Utilities:Ensure(input, defaultValue, ignoreDefault) ignoreDefault = type(ignoreDefault) == 'boolean' and ignoreDefault or false if (defaultValue == nil) then return nil end local requiredType = self:Typeof(defaultValue) if (requiredType == 'nil') then return nil end local inputType = self:Typeof(input) if (inputType == requiredType) then return input end if (inputType == 'nil') then return defaultValue end if (requiredType == 'number') then if (inputType == 'boolean') then return input and 1 or 0 end return tonumber(input) or (not ignoreDefault and defaultValue or nil) end if (requiredType == 'string') then if (inputType == 'boolean') then return input and 'yes' or 'no' end if (inputType == 'vector3') then return encode({ x = input.x, y = input.y, z = input.z }) or (not ignoreDefault and defaultValue or nil) end if (inputType == 'vector2') then return encode({ x = input.x, y = input.y }) or (not ignoreDefault and defaultValue or nil) end if (inputType == 'table') then return encode(input) or (not ignoreDefault and defaultValue or nil) end local result = tostring(input) if (result == 'nil') then return not ignoreDefault and defaultValue or 'nil' end return result end if (requiredType == 'boolean') then if (inputType == 'string') then input = lower(input) if (input == 'true' or input == '1' or input == 'yes' or input == 'y') then return true end if (input == 'false' or input == '0' or input == 'no' or input == 'n') then return false end return (not ignoreDefault and defaultValue or nil) end if (inputType == 'number') then if (input == 1) then return true end if (input == 0) then return false end return (not ignoreDefault and defaultValue or nil) end return (not ignoreDefault and defaultValue or nil) end if (requiredType == 'table') then if (inputType == 'string') then if (self:StartsWith(input, '{') and self:EndsWith(input, '}')) then return decode(input) or (not ignoreDefault and defaultValue or nil) end if (self:StartsWith(input, '[') and self:EndsWith(input, ']')) then return decode(input) or (not ignoreDefault and defaultValue or nil) end return (not ignoreDefault and defaultValue or nil) end if (inputType == 'vector3') then return { x = input.x or 0, y = input.y or 0, z = input.z or 0 } end if (inputType == 'vector2') then return { x = input.x or 0, y = input.y or 0 } end return (not ignoreDefault and defaultValue or nil) end if (requiredType == 'vector3') then if (inputType == 'table') then local _x = self:Ensure(input.x, defaultValue.x) local _y = self:Ensure(input.y, defaultValue.y) local _z = self:Ensure(input.z, defaultValue.z) return vector3(_x, _y, _z) end if (inputType == 'vector2') then local _x = self:Ensure(input.x, defaultValue.x) local _y = self:Ensure(input.y, defaultValue.y) return vector3(_x, _y, 0) end if (inputType == 'number') then return vector3(input, input, input) end return (not ignoreDefault and defaultValue or nil) end if (requiredType == 'vector2') then if (inputType == 'table') then local _x = self:Ensure(input.x, defaultValue.x) local _y = self:Ensure(input.y, defaultValue.y) return vector2(_x, _y) end if (inputType == 'vector3') then local _x = self:Ensure(input.x, defaultValue.x) local _y = self:Ensure(input.y, defaultValue.y) return vector2(_x, _y) end if (inputType == 'number') then return vector2(input, input) end return (not ignoreDefault and defaultValue or nil) end return (not ignoreDefault and defaultValue or nil) end --- Checks if input exists in inputs --- '0' and 0 are both the same '0' == 0 equals `true` --- 'yes' and true are both the same 'yes' == true equals `true` ---@param input any Any input ---@param inputs any[] Any table ---@param checkType string | "'value'" | "'key'" | "'both'" ---@return boolean Returns `true` if input has been found as `key` and/or `value` function Utilities:Any(input, inputs, checkType) if (input == nil) then return false end if (inputs == nil) then return false end inputs = self:Ensure(inputs, {}) checkType = lower(self:Ensure(checkType, 'value')) local checkMethod = 1 if (checkType == 'value' or checkType == 'v') then checkMethod = 1 elseif (checkType == 'key' or checkType == 'k') then checkMethod = -1 elseif (checkType == 'both' or checkType == 'b') then checkMethod = 0 end for k, v in pairs(inputs) do if (checkMethod == 0 or checkMethod == -1) then local checkK = self:Ensure(input, k, true) if (checkK ~= nil and checkK == k) then return true end end if (checkMethod == 0 or checkMethod == 1) then local checkV = self:Ensure(input, v, true) if (checkV ~= nil and checkV == v) then return true end end end return false end --- Round any `value` ---@param value number Round this value ---@param decimal number Number of decimals ---@return number Rounded number function Utilities:Round(value, decimal) value = self:Ensure(value, 0) decimal = self:Ensure(decimal, 0) if (decimal > 0) then return floor((value * 10 ^ decimal) + 0.5) / (10 ^ decimal) end return floor(value + 0.5) end --- Checks if `item1` equals `item2` ---@param item1 any Item1 ---@param item2 any Item2 ---@return boolean `true` if both are equal, otherwise `false` function Utilities:Equal(item1, item2) if (item1 == nil and item2 == nil) then return true end if (item1 == nil or item2 == nil) then return false end if (type(item1) == 'table') then local item1EQ = rawget(item1, '__eq') if (item1EQ ~= nil and self:Typeof(item1EQ) == 'function') then return item1EQ(item1, item2) end return item1 == item2 end if (type(item2) == 'table') then local item2EQ = rawget(item2, '__eq') if (item2EQ ~= nil and self:Typeof(item2EQ) == 'function') then return item2EQ(item2, item1) end return item2 == item1 end return item1 == item2 end local function tohex(x) x = Utilities:Ensure(x, 32) local s, base, d = '', 16 while x > 0 do d = x % base + 1 x = floor(x / base) s = sub('0123456789abcdef', d, d) .. s end while #s < 2 do s = ('0%s'):format(s) end return s end local function bitwise(x, y, matrix) x = Utilities:Ensure(x, 32) y = Utilities:Ensure(y, 16) matrix = Utilities:Ensure(matrix, {{0,0}, {0, 1}}) local z, pow = 0, 1 while x > 0 or y > 0 do z = z + (matrix[x %2 + 1][y %2 + 1] * pow) pow = pow * 2 x = floor(x / 2) y = floor(y / 2) end return z end --- Generates a random UUID like: 00000000-0000-0000-0000-000000000000 ---@return string Random generated UUID function Utilities:UUID() randomseed(GET_GAME_TIMER() + random(30720, 92160)) ---@type number[] local bytes = { random(0, 255), random(0, 255), random(0, 255), random(0, 255), random(0, 255), random(0, 255), random(0, 255), random(0, 255), random(0, 255), random(0, 255), random(0, 255), random(0, 255), random(0, 255), random(0, 255), random(0, 255), random(0, 255) } bytes[7] = bitwise(bytes[7], 0x0f, {{0,0},{0,1}}) bytes[7] = bitwise(bytes[7], 0x40, {{0,1},{1,1}}) bytes[9] = bitwise(bytes[7], 0x3f, {{0,0},{0,1}}) bytes[9] = bitwise(bytes[7], 0x80, {{0,1},{1,1}}) return upper(('%s%s%s%s-%s%s-%s%s-%s%s-%s%s%s%s%s%s'):format( tohex(bytes[1]), tohex(bytes[2]), tohex(bytes[3]), tohex(bytes[4]), tohex(bytes[5]), tohex(bytes[6]), tohex(bytes[7]), tohex(bytes[8]), tohex(bytes[9]), tohex(bytes[10]), tohex(bytes[11]), tohex(bytes[12]), tohex(bytes[13]), tohex(bytes[14]), tohex(bytes[15]), tohex(bytes[16]) )) end --- Replace a string that contains `this` to `that` ---@param str string String where to replace in ---@param this string Word that's need to be replaced ---@param that string Replace `this` whit given string ---@return string String where `this` has been replaced with `that` function Utilities:Replace(str, this, that) local b, e = str:find(this, 1, true) if b == nil then return str else return str:sub(1, b - 1) .. that .. self:Replace(str:sub(e + 1), this, that) end end _G.Utilities = Utilities _ENV.Utilities = Utilities ----------------------- [ 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.4.1 -- Description: FiveM menu library for creating menu's ----------------------- [ MenuV ] ----------------------- local assert = assert ---@type Utilities local U = assert(Utilities) local type = assert(type) local pairs = assert(pairs) local lower = assert(string.lower) local upper = assert(string.upper) local sub = assert(string.sub) local pack = assert(table.pack) local unpack = assert(table.unpack) local insert = assert(table.insert) local rawset = assert(rawset) local rawget = assert(rawget) local setmetatable = assert(setmetatable) --- FiveM globals local CreateThread = assert(Citizen.CreateThread) --- Create a new menu item ---@param info table Menu information ---@return Item New item function CreateMenuItem(info) info = U:Ensure(info, {}) local item = { ---@type Menu|nil __menu = U:Ensure(info.__Menu or info.__menu, { __class = 'Menu', __type = 'Menu' }, true) or nil, ---@type string __event = U:Ensure(info.PrimaryEvent or info.primaryEvent, 'unknown'), ---@type string UUID = U:UUID(), ---@type string Icon = U:Ensure(info.Icon or info.icon, 'none'), ---@type string Label = U:Ensure(info.Label or info.label, ''), ---@type string Description = U:Ensure(info.Description or info.description, ''), ---@type any Value = info.Value or info.value, ---@type table[] Values = {}, ---@type number Min = U:Ensure(info.Min or info.min, 0), ---@type number Max = U:Ensure(info.Max or info.max, 0), ---@type boolean Disabled = U:Ensure(info.Disabled or info.disabled, false), ---@type table Events = U:Ensure(info.Events or info.events, {}), ---@type boolean SaveOnUpdate = U:Ensure(info.SaveOnUpdate or info.saveOnUpdate, false), ---@param t Item ---@param event string Name of Event Trigger = function(t, event, ...) event = lower(U:Ensure(event, 'unknown')) if (event == 'unknown') then return end if (U:StartsWith(event, 'on')) then event = 'On' .. sub(event, 3):gsub('^%l', upper) else event = 'On' .. event:gsub('^%l', upper) end if (not U:Any(event, (t.Events or {}), 'key')) then return end local args = pack(...) for _, v in pairs(t.Events[event]) do CreateThread(function() v(t, unpack(args)) end) end end, ---@param t Item ---@param event string Name of event ---@param func function|Menu Function or Menu to trigger On = function(t, event, func) event = lower(U:Ensure(event, 'unknown')) if (event == 'unknown') then return end if (U:StartsWith(event, 'on')) then event = 'On' .. sub(event, 3):gsub('^%l', upper) else event = 'On' .. event:gsub('^%l', upper) end if (not U:Any(event, (t.Events or {}), 'key')) then return end local _type = U:Typeof(func) if (_type == 'Menu') then local menu_t = { __class = 'function', __type = 'function', func = function(t) MenuV:OpenMenu(t.uuid) end, uuid = func.UUID or func.uuid or U:UUID() } local menu_mt = { __index = menu_t, __call = function(t) t:func() end } local menu_item = setmetatable(menu_t, menu_mt) insert(t.Events[event], menu_item) return end func = U:Ensure(func, function() end) insert(t.Events[event], func) end, ---@param t Item ---@param k string ---@param v string Validate = U:Ensure(info.Validate or info.validate, function(t, k, v) return true end), ---@param t Item ---@param k string ---@param v string Parser = U:Ensure(info.Parser or info.parser, function(t, k, v) return v end), ---@param t Item ---@param k string ---@param v string NewIndex = U:Ensure(info.NewIndex or info.newIndex, function(t, k, v) end), ---@param t Item ---@return any GetValue = function(t) local itemType = U:Ensure(t.__type, 'unknown') if (itemType == 'button' or itemType == 'menu' or itemType == 'unknown') then return t.Value end if (itemType == 'checkbox' or itemType == 'confirm') then return U:Ensure(t.Value, false) end if (itemType == 'slider') then for _, item in pairs(t.Values) do if (item.Value == t.Value) then return item.Value end end return nil end if (itemType == 'range') then local rawValue = U:Ensure(t.Value, 0) if (t.Min > rawValue) then return t.Min end if (t.Max < rawValue) then return t.Max end return rawValue end end, ---@return Menu|nil GetParentMenu = function(t) return t.__menu or nil end } item.Events.OnEnter = {} item.Events.OnLeave = {} item.Events.OnUpdate = {} item.Events.OnDestroy = {} local mt = { __index = function(t, k) return rawget(t.data, k) end, __tostring = function(t) return t.UUID end, __call = function(t, ...) if (t.Trigger ~= nil and type(t.Trigger) == 'function') then t:Trigger(t.__event, ...) end end, __newindex = function(t, k, v) local key = U:Ensure(k, 'unknown') local oldValue = rawget(t.data, k) local checkInput = t.Validate ~= nil and type(t.Validate) == 'function' local inputParser = t.Parser ~= nil and type(t.Parser) == 'function' local updateIndexTrigger = t.NewIndex ~= nil and type(t.NewIndex) == 'function' if (checkInput) then local result = t:Validate(key, v) result = U:Ensure(result, true) if (not result) then return end end if (inputParser) then local parsedValue = t:Parser(key, v) v = parsedValue or v end rawset(t.data, k, v) if (updateIndexTrigger) then t:NewIndex(key, v) end if (t.__menu ~= nil and U:Typeof(t.__menu) == 'Menu' and t.__menu.Trigger ~= nil and U:Typeof( t.__menu.Trigger) == 'function') then t.__menu:Trigger('update', 'UpdateItem', t) end if (key == 'Value' and t.Trigger ~= nil and type(t.Trigger) == 'function') then t:Trigger('update', key, v, oldValue) end end, __metatable = 'MenuV' } ---@class Item ---@filed private __event string Name of primary event ---@field public UUID string UUID of Item ---@field public Icon string Icon/Emoji for Item ---@field public Label string Label of Item ---@field public Description string Description of Item ---@field public Value any Value of Item ---@field public Values table[] List of values ---@field public Min number Min range value ---@field public Max number Max range value ---@field public Disabled boolean Disabled state of Item ---@field public SaveOnUpdate boolean Save on `update` ---@field private Events table List of registered `on` events ---@field public Trigger fun(t: Item, event: string) ---@field public On fun(t: Item, event: string, func: function|Menu) ---@field public Validate fun(t: Item, k: string, v:any) ---@field public NewIndex fun(t: Item, k: string, v: any) ---@field public Parser fun(t: Item, k: string, v: any) ---@field public GetValue fun(t: Item):any ---@field public GetParentMenu func(t: Item):Menu|nil local i = setmetatable({ data = item, __class = 'Item', __type = U:Ensure(info.Type or info.type, 'unknown') }, mt) for k, v in pairs(info or {}) do local key = U:Ensure(k, 'unknown') if (key == 'unknown') then return end i:On(key, v) end return i end _ENV.CreateMenuItem = CreateMenuItem _G.CreateMenuItem = CreateMenuItem ----------------------- [ 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.4.1 -- Description: FiveM menu library for creating menu's ----------------------- [ MenuV ] ----------------------- local assert = assert ---@type Utilities local U = assert(Utilities) local type = assert(type) local next = assert(next) local pairs = assert(pairs) local ipairs = assert(ipairs) local lower = assert(string.lower) local upper = assert(string.upper) local sub = assert(string.sub) local insert = assert(table.insert) local remove = assert(table.remove) local pack = assert(table.pack) local unpack = assert(table.unpack) local encode = assert(json.encode) local rawset = assert(rawset) local rawget = assert(rawget) local setmetatable = assert(setmetatable) --- FiveM globals local GET_CURRENT_RESOURCE_NAME = assert(GetCurrentResourceName) local GET_INVOKING_RESOURCE = assert(GetInvokingResource) local HAS_STREAMED_TEXTURE_DICT_LOADED = assert(HasStreamedTextureDictLoaded) local REQUEST_STREAMED_TEXTURE_DICT = assert(RequestStreamedTextureDict) --- MenuV local variable local current_resource = GET_CURRENT_RESOURCE_NAME() --- Returns default empty table for items ---@returns items function CreateEmptyItemsTable(data) data = U:Ensure(data, {}) data.ToTable = function(t) local tempTable = {} local index = 0 for _, option in pairs(t) do index = index + 1 tempTable[index] = { index = index, type = option.__type, uuid = U:Ensure(option.UUID, 'unknown'), icon = U:Ensure(option.Icon, 'none'), label = U:Ensure(option.Label, 'Unknown'), description = U:Ensure(option.Description, ''), value = 'none', values = {}, min = U:Ensure(option.Min, 0), max = U:Ensure(option.Max, 0), disabled = U:Ensure(option.Disabled, false) } if (option.__type == 'button' or option.__type == 'menu') then tempTable[index].value = 'none' elseif (option.__type == 'checkbox' or option.__type == 'confirm') then tempTable[index].value = U:Ensure(option.Value, false) elseif (option.__type == 'range') then tempTable[index].value = U:Ensure(option.Value, 0) if (tempTable[index].value <= tempTable[index].min) then tempTable[index].value = tempTable[index].min elseif (tempTable[index].value >= tempTable[index].max) then tempTable[index].value = tempTable[index].max end elseif (option.__type == 'slider') then tempTable[index].value = 0 end local _values = U:Ensure(option.Values, {}) local vIndex = 0 for valueIndex, value in pairs(_values) do vIndex = vIndex + 1 tempTable[index].values[vIndex] = { label = U:Ensure(value.Label, 'Option'), description = U:Ensure(value.Description, ''), value = vIndex } if (option.__type == 'slider') then if (U:Ensure(option.Value, 0) == valueIndex) then tempTable[index].value = (valueIndex - 1) end end end end return tempTable end data.ItemToTable = function(t, i) local tempTable = {} local index = 0 local uuid = U:Typeof(i) == 'Item' and i.UUID or U:Ensure(i, '00000000-0000-0000-0000-000000000000') for _, option in pairs(t) do index = index + 1 if (option.UUID == uuid) then tempTable = { index = index, type = option.__type, uuid = U:Ensure(option.UUID, 'unknown'), icon = U:Ensure(option.Icon, 'none'), label = U:Ensure(option.Label, 'Unknown'), description = U:Ensure(option.Description, ''), value = 'none', values = {}, min = U:Ensure(option.Min, 0), max = U:Ensure(option.Max, 0), disabled = U:Ensure(option.Disabled, false) } if (option.__type == 'button' or option.__type == 'menu') then tempTable.value = 'none' elseif (option.__type == 'checkbox' or option.__type == 'confirm') then tempTable.value = U:Ensure(option.Value, false) elseif (option.__type == 'range') then tempTable.value = U:Ensure(option.Value, 0) if (tempTable.value <= tempTable.min) then tempTable.value = tempTable.min elseif (tempTable.value >= tempTable.max) then tempTable.value = tempTable.max end elseif (option.__type == 'slider') then tempTable.value = 0 end local _values = U:Ensure(option.Values, {}) local vIndex = 0 for valueIndex, value in pairs(_values) do vIndex = vIndex + 1 tempTable.values[vIndex] = { label = U:Ensure(value.Label, 'Option'), description = U:Ensure(value.Description, ''), value = vIndex } if (option.__type == 'slider') then if (U:Ensure(option.Value, 0) == valueIndex) then tempTable.value = (valueIndex - 1) end end end return tempTable end end return tempTable end data.AddItem = function(t, item) if (U:Typeof(item) == 'Item') then local newIndex = #(U:Ensure(rawget(t, 'data'), {})) + 1 rawset(t.data, newIndex, item) if (t.Trigger ~= nil and type(t.Trigger) == 'function') then t:Trigger('update', 'AddItem', item) end end return U:Ensure(rawget(t, 'data'), {}) end local item_pairs = function(t, k) local _k, _v = next((rawget(t, 'data') or {}), k) if (_v ~= nil and type(_v) ~= 'table') then return item_pairs(t, _k) end return _k, _v end local item_ipairs = function(t, k) local _k, _v = next((rawget(t, 'data') or {}), k) if (_v ~= nil and (type(_v) ~= 'table' or type(_k) ~= 'number')) then return item_ipairs(t, _k) end return _k, _v end _G.item_pairs = item_pairs _ENV.item_pairs = item_pairs ---@class items return setmetatable({ data = data, Trigger = nil }, { __index = function(t, k) return rawget(t.data, k) end, __newindex = function(t, k, v) local oldValue = rawget(t.data, k) rawset(t.data, k, v) if (t.Trigger ~= nil and type(t.Trigger) == 'function') then if (oldValue == nil) then t:Trigger('update', 'AddItem', v) elseif (oldValue ~= nil and v == nil) then t:Trigger('update', 'RemoveItem', oldValue) elseif (oldValue ~= v) then t:Trigger('update', 'UpdateItem', v, oldValue) end end end, __call = function(t, func) rawset(t, 'Trigger', U:Ensure(func, function() end)) end, __pairs = function(t) local k = nil return function() local v k, v = item_pairs(t, k) return k, v end, t, nil end, __ipairs = function(t) local k = nil return function() local v k, v = item_ipairs(t, k) return k, v end, t, 0 end, __len = function(t) local items = U:Ensure(rawget(t, 'data'), {}) local itemCount = 0 for _, v in pairs(items) do if (U:Typeof(v) == 'Item') then itemCount = itemCount + 1 end end return itemCount end }) end --- Load a texture dictionary if not already loaded ---@param textureDictionary string Name of texture dictionary local function LoadTextureDictionary(textureDictionary) textureDictionary = U:Ensure(textureDictionary, 'menuv') if (HAS_STREAMED_TEXTURE_DICT_LOADED(textureDictionary)) then return end REQUEST_STREAMED_TEXTURE_DICT(textureDictionary, true) end --- Create a new menu item ---@param info table Menu information ---@return Menu New item function CreateMenu(info) info = U:Ensure(info, {}) local namespace = U:Ensure(info.Namespace or info.namespace, 'unknown') local namespace_available = MenuV:IsNamespaceAvailable(namespace) if (not namespace_available) then error(("[MenuV] Namespace '%s' is already taken, make sure it is unique."):format(namespace)) end local theme = lower(U:Ensure(info.Theme or info.theme, 'default')) if (theme ~= 'default' and theme ~= 'native') then theme = 'default' end if (theme == 'native') then info.R, info.G, info.B = 255, 255, 255 info.r, info.g, info.b = 255, 255, 255 end local item = { ---@type string Namespace = namespace, ---@type boolean IsOpen = false, ---@type string UUID = U:UUID(), ---@type string Title = not (info.Title or info.title) and '‏‏‎ ‎' or U:Ensure(info.Title or info.title, 'MenuV'), ---@type string Subtitle = U:Ensure(info.Subtitle or info.subtitle, ''), ---@type string | "'topleft'" | "'topcenter'" | "'topright'" | "'centerleft'" | "'center'" | "'centerright'" | "'bottomleft'" | "'bottomcenter'" | "'bottomright'" Position = U:Ensure(info.Position or info.position, 'topleft'), ---@type table Color = { R = U:Ensure(info.R or info.r, 0), G = U:Ensure(info.G or info.g, 0), B = U:Ensure(info.B or info.b, 255) }, ---@type string | "'size-100'" | "'size-110'" | "'size-125'" | "'size-150'" | "'size-175'" | "'size-200'" Size = U:Ensure(info.Size or info.size, 'size-110'), ---@type string Dictionary = U:Ensure(info.Dictionary or info.dictionary, 'menuv'), ---@type string Texture = U:Ensure(info.Texture or info.texture, 'default'), ---@type table Events = U:Ensure(info.Events or info.events, {}), ---@type string Theme = theme, ---@type Item[] Items = CreateEmptyItemsTable({}), ---@param t Menu ---@param event string Name of Event Trigger = function(t, event, ...) event = lower(U:Ensure(event, 'unknown')) if (event == 'unknown') then return end if (U:StartsWith(event, 'on')) then event = 'On' .. sub(event, 3):gsub('^%l', upper) else event = 'On' .. event:gsub('^%l', upper) end if (not U:Any(event, (t.Events or {}), 'key')) then return end if (event == 'OnOpen') then rawset(t, 'IsOpen', true) elseif (event == 'OnClose') then rawset(t, 'IsOpen', false) end local args = pack(...) for _, v in pairs(t.Events[event]) do if (type(v) == 'table' and U:Typeof(v.func) == 'function') then CreateThread(function() if (event == 'OnClose') then v.func(t, unpack(args)) else local threadId = coroutine.running() if (threadId ~= nil) then insert(t.data.Threads, threadId) v.func(t, unpack(args)) for i = 0, #(t.data.Threads or {}), 1 do if (t.data.Threads[i] == threadId) then remove(t.data.Threads, i) return end end end end end) end end end, ---@type thread[] Threads = {}, ---@param t Menu DestroyThreads = function(t) for _, threadId in pairs(t.data.Threads or {}) do local threadStatus = coroutine.status(threadId) if (threadStatus ~= nil and threadStatus ~= 'dead') then coroutine.close(threadId) end end t.data.Threads = {} end, ---@param t Menu ---@param event string Name of event ---@param func function|Menu Function or Menu to trigger ---@return string UUID of event On = function(t, event, func) local ir = GET_INVOKING_RESOURCE() local resource = U:Ensure(ir, current_resource) event = lower(U:Ensure(event, 'unknown')) if (event == 'unknown') then return end if (U:StartsWith(event, 'on')) then event = 'On' .. sub(event, 3):gsub('^%l', upper) else event = 'On' .. event:gsub('^%l', upper) end if (not U:Any(event, (t.Events or {}), 'key')) then return end func = U:Ensure(func, function() end) local uuid = U:UUID() insert(t.Events[event], { __uuid = uuid, __resource = resource, func = func }) return uuid end, ---@param t Menu ---@param event string Name of event ---@param uuid string UUID of event RemoveOnEvent = function(t, event, uuid) local ir = GET_INVOKING_RESOURCE() local resource = U:Ensure(ir, current_resource) event = lower(U:Ensure(event, 'unknown')) if (event == 'unknown') then return end if (U:StartsWith(event, 'on')) then event = 'On' .. sub(event, 3):gsub('^%l', upper) else event = 'On' .. event:gsub('^%l', upper) end if (not U:Any(event, (t.Events or {}), 'key')) then return end uuid = U:Ensure(uuid, '00000000-0000-0000-0000-000000000000') for i = 1, #t.Events[event], 1 do if (t.Events[event][i] ~= nil and t.Events[event][i].__uuid == uuid and t.Events[event][i].__resource == resource) then remove(t.Events[event], i) end end end, ---@param t Item ---@param k string ---@param v string Validate = U:Ensure(info.Validate or info.validate, function(t, k, v) return true end), ---@param t Item ---@param k string ---@param v string Parser = function(t, k, v) if (k == 'Position' or k == 'position') then local position = lower(U:Ensure(v, 'topleft')) if (U:Any(position, {'topleft', 'topcenter', 'topright', 'centerleft', 'center', 'centerright', 'bottomleft', 'bottomcenter', 'bottomright'}, 'value')) then return position else return 'topleft' end end return v end, ---@param t Item ---@param k string ---@param v string NewIndex = U:Ensure(info.NewIndex or info.newIndex, function(t, k, v) end), ---@type function ---@param t Menu MenuV menu ---@param info table Information about button ---@return Item New item AddButton = function(t, info) info = U:Ensure(info, {}) info.Type = 'button' info.Events = { OnSelect = {} } info.PrimaryEvent = 'OnSelect' info.TriggerUpdate = not U:Ensure(info.IgnoreUpdate or info.ignoreUpdate, false) info.__menu = t if (U:Typeof(info.Value or info.value) == 'Menu') then info.Type = 'menu' end local item = CreateMenuItem(info) if (info.Type == 'menu') then item:On('select', function() item.Value() end) end if (info.TriggerUpdate) then t.Items:AddItem(item) else local items = rawget(t.data, 'Items') if (items) then local newIndex = #items + 1 rawset(items.data, newIndex, item) return items.data[newIndex] or item end end return t.Items[#t.Items] or item end, ---@type function ---@param t Menu MenuV menu ---@param info table Information about checkbox ---@return Item New item AddCheckbox = function(t, info) info = U:Ensure(info, {}) info.Type = 'checkbox' info.Value = U:Ensure(info.Value or info.value, false) info.Events = { OnChange = {}, OnCheck = {}, OnUncheck = {} } info.PrimaryEvent = 'OnCheck' info.TriggerUpdate = not U:Ensure(info.IgnoreUpdate or info.ignoreUpdate, false) info.__menu = t info.NewIndex = function(t, k, v) if (k == 'Value') then local value = U:Ensure(v, false) if (value) then t:Trigger('check', t) else t:Trigger('uncheck', t) end end end local item = CreateMenuItem(info) if (info.TriggerUpdate) then t.Items:AddItem(item) else local items = rawget(t.data, 'Items') if (items) then local newIndex = #items + 1 rawset(items.data, newIndex, item) return items.data[newIndex] or item end end return t.Items[#t.Items] or item end, ---@type function ---@param t Menu MenuV menu ---@param info table Information about slider ---@return SliderItem New slider item AddSlider = function(t, info) info = U:Ensure(info, {}) info.Type = 'slider' info.Events = { OnChange = {}, OnSelect = {} } info.PrimaryEvent = 'OnSelect' info.TriggerUpdate = not U:Ensure(info.IgnoreUpdate or info.ignoreUpdate, false) info.__menu = t ---@class SliderItem : Item ---@filed private __event string Name of primary event ---@field public UUID string UUID of Item ---@field public Icon string Icon/Emoji for Item ---@field public Label string Label of Item ---@field public Description string Description of Item ---@field public Value any Value of Item ---@field public Values table[] List of values ---@field public Min number Min range value ---@field public Max number Max range value ---@field public Disabled boolean Disabled state of Item ---@field private Events table List of registered `on` events ---@field public Trigger fun(t: Item, event: string) ---@field public On fun(t: Item, event: string, func: function) ---@field public Validate fun(t: Item, k: string, v:any) ---@field public NewIndex fun(t: Item, k: string, v: any) ---@field public GetValue fun(t: Item):any ---@field public AddValue fun(t: Item, info: table) ---@field public AddValues fun(t: Item) local item = CreateMenuItem(info) --- Add a value to slider ---@param info table Information about slider function item:AddValue(info) info = U:Ensure(info, {}) local value = { Label = U:Ensure(info.Label or info.label, 'Value'), Description = U:Ensure(info.Description or info.description, ''), Value = info.Value or info.value } insert(self.Values, value) end --- Add values to slider ---@vararg table[] List of values function item:AddValues(...) local arguments = pack(...) for _, argument in pairs(arguments) do if (U:Typeof(argument) == 'table') then local hasIndex = argument[1] or nil if (hasIndex and U:Typeof(hasIndex) == 'table') then self:AddValues(unpack(argument)) else self:AddValue(argument) end end end end local values = U:Ensure(info.Values or info.values, {}) if (#values > 0) then item:AddValues(values) end if (info.TriggerUpdate) then t.Items:AddItem(item) else local items = rawget(t.data, 'Items') if (items) then local newIndex = #items + 1 rawset(items.data, newIndex, item) return items.data[newIndex] or item end end return t.Items[#t.Items] or item end, ---@type function ---@param t Menu MenuV menu ---@param info table Information about range ---@return RangeItem New Range item AddRange = function(t, info) info = U:Ensure(info, {}) info.Type = 'range' info.Events = { OnChange = {}, OnSelect = {}, OnMin = {}, OnMax = {} } info.PrimaryEvent = 'OnSelect' info.TriggerUpdate = not U:Ensure(info.IgnoreUpdate or info.ignoreUpdate, false) info.__menu = t info.Value = U:Ensure(info.Value or info.value, 0) info.Min = U:Ensure(info.Min or info.min, 0) info.Max = U:Ensure(info.Max or info.max, 0) info.Validate = function(t, k, v) if (k == 'Value' or k == 'value') then v = U:Ensure(v, 0) if (t.Min > v) then return false end if (t.Max < v) then return false end end return true end if (info.Min > info.Max) then local min = info.Min local max = info.Max info.Min = min info.Max = max end if (info.Value < info.Min) then info.Value = info.Min end if (info.Value > info.Max) then info.Value = info.Max end ---@class RangeItem : Item ---@filed private __event string Name of primary event ---@field public UUID string UUID of Item ---@field public Icon string Icon/Emoji for Item ---@field public Label string Label of Item ---@field public Description string Description of Item ---@field public Value any Value of Item ---@field public Values table[] List of values ---@field public Min number Min range value ---@field public Max number Max range value ---@field public Disabled boolean Disabled state of Item ---@field private Events table List of registered `on` events ---@field public Trigger fun(t: Item, event: string) ---@field public On fun(t: Item, event: string, func: function) ---@field public Validate fun(t: Item, k: string, v:any) ---@field public NewIndex fun(t: Item, k: string, v: any) ---@field public GetValue fun(t: Item):any ---@field public SetMinValue fun(t: any) ---@field public SetMaxValue fun(t: any) local item = CreateMenuItem(info) --- Update min value of range ---@param input number Minimum value of Range function item:SetMinValue(input) input = U:Ensure(input, 0) self.Min = input if (self.Value < self.Min) then self.Value = self.Min end if (self.Min > self.Max) then self.Max = self.Min end end --- Update max value of range ---@param input number Minimum value of Range function item:SetMaxValue(input) input = U:Ensure(input, 0) self.Min = input if (self.Value > self.Max) then self.Value = self.Max end if (self.Min < self.Max) then self.Min = self.Max end end if (info.TriggerUpdate) then t.Items:AddItem(item) else local items = rawget(t.data, 'Items') if (items) then local newIndex = #items + 1 rawset(items.data, newIndex, item) return items.data[newIndex] or item end end return t.Items[#t.Items] or item end, ---@type function ---@param t Menu MenuV menu ---@param info table Information about confirm ---@return ConfirmItem New Confirm item AddConfirm = function(t, info) info = U:Ensure(info, {}) info.Type = 'confirm' info.Value = U:Ensure(info.Value or info.value, false) info.Events = { OnConfirm = {}, OnDeny = {}, OnChange = {} } info.PrimaryEvent = 'OnConfirm' info.TriggerUpdate = not U:Ensure(info.IgnoreUpdate or info.ignoreUpdate, false) info.__menu = t info.NewIndex = function(t, k, v) if (k == 'Value') then local value = U:Ensure(v, false) if (value) then t:Trigger('confirm', t) else t:Trigger('deny', t) end end end ---@class ConfirmItem : Item ---@filed private __event string Name of primary event ---@field public UUID string UUID of Item ---@field public Icon string Icon/Emoji for Item ---@field public Label string Label of Item ---@field public Description string Description of Item ---@field public Value any Value of Item ---@field public Values table[] List of values ---@field public Min number Min range value ---@field public Max number Max range value ---@field public Disabled boolean Disabled state of Item ---@field private Events table List of registered `on` events ---@field public Trigger fun(t: Item, event: string) ---@field public On fun(t: Item, event: string, func: function) ---@field public Validate fun(t: Item, k: string, v:any) ---@field public NewIndex fun(t: Item, k: string, v: any) ---@field public GetValue fun(t: Item):any ---@field public Confirm fun(t: Item) ---@field public Deny fun(t: Item) local item = CreateMenuItem(info) --- Confirm this item function item:Confirm() item.Value = true end --- Deny this item function item:Deny() item.Value = false end if (info.TriggerUpdate) then t.Items:AddItem(item) else local items = rawget(t.data, 'Items') if (items) then local newIndex = #items + 1 rawset(items.data, newIndex, item) return items.data[newIndex] or item end end return t.Items[#t.Items] or item end, --- Create child menu from properties of this object ---@param t Menu|string MenuV menu ---@param overrides table Properties to override in menu object (ignore parent) ---@param namespace string Namespace of menu InheritMenu = function(t, overrides, namespace) return MenuV:InheritMenu(t, overrides, namespace) end, --- Add control key for specific menu ---@param t 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 AddControlKey = function(t, action, func, description, defaultType, defaultKey) if (U:Typeof(t.Namespace) ~= 'string' or t.Namespace == 'unknown') then error('[MenuV] Namespace is required for assigning keys.') return end MenuV:AddControlKey(t, action, func, description, defaultType, defaultKey) end, --- Assign key for opening this menu ---@param t Menu|string MenuV menu ---@param defaultType string Default key type ---@param defaultKey string Default key OpenWith = function(t, defaultType, defaultKey) t:AddControlKey('open', function(m) MenuV:CloseAll(function() MenuV:OpenMenu(m) end) end, MenuV:T('open_menu'):format(t.Namespace), defaultType, defaultKey) end, --- Change title of menu ---@param t Menu ---@param title string Title of menu SetTitle = function(t, title) t.Title = U:Ensure(title, 'MenuV') end, --- Change subtitle of menu ---@param t Menu ---@param subtitle string Subtitle of menu SetSubtitle = function(t, subtitle) t.Subtitle = U:Ensure(subtitle, '') end, --- Change subtitle of menu ---@param t Menu ---@param position string | "'topleft'" | "'topcenter'" | "'topright'" | "'centerleft'" | "'center'" | "'centerright'" | "'bottomleft'" | "'bottomcenter'" | "'bottomright'" SetPosition = function(t, position) t.Position = U:Ensure(position, 'topleft') end, --- Clear all Menu items ---@param t Menu ClearItems = function(t, update) update = U:Ensure(update, true) local items = CreateEmptyItemsTable({}) items(function(_, trigger, key, index, value, oldValue) t:Trigger(trigger, key, index, value, oldValue) end) rawset(t.data, 'Items', items) if (update and t.Trigger ~= nil and type(t.Trigger) == 'function') then t:Trigger('update', 'Items', items) end end, Open = function(t) MenuV:OpenMenu(t) end, Close = function(t) MenuV:CloseMenu(t) end, --- @see Menu to @see table ---@param t Menu ---@return table ToTable = function(t) local tempTable = { theme = U:Ensure(t.Theme, 'default'), uuid = U:Ensure(t.UUID, '00000000-0000-0000-0000-000000000000'), title = U:Ensure(t.Title, 'MenuV'), subtitle = U:Ensure(t.Subtitle, ''), position = U:Ensure(t.Position, 'topleft'), size = U:Ensure(t.Size, 'size-110'), dictionary = U:Ensure(t.Dictionary, 'menuv'), texture = U:Ensure(t.Texture, 'default'), color = { r = U:Ensure(t.Color.R, 0), g = U:Ensure(t.Color.G, 0), b = U:Ensure(t.Color.B, 255) }, items = {} } local items = rawget(t.data, 'Items') if (items ~= nil and items.ToTable ~= nil) then tempTable.items = items:ToTable() end if (tempTable.color.r <= 0) then tempTable.color.r = 0 end if (tempTable.color.r >= 255) then tempTable.color.r = 255 end if (tempTable.color.g <= 0) then tempTable.color.g = 0 end if (tempTable.color.g >= 255) then tempTable.color.g = 255 end if (tempTable.color.b <= 0) then tempTable.color.b = 0 end if (tempTable.color.b >= 255) then tempTable.color.b = 255 end return tempTable end } if (lower(item.Texture) == 'default' and lower(item.Dictionary) == 'menuv' and theme == 'native') then item.Texture = 'default_native' end item.Events.OnOpen = {} item.Events.OnClose = {} item.Events.OnSelect = {} item.Events.OnUpdate = {} item.Events.OnSwitch = {} item.Events.OnChange = {} item.Events.OnIChange = {} if (not U:Any(item.Size, { 'size-100', 'size-110', 'size-125', 'size-150', 'size-175', 'size-200' }, 'value')) then item.Size = 'size-110' end local mt = { __index = function(t, k) return rawget(t.data, k) end, ---@param t Menu __tostring = function(t) return encode(t:ToTable()) end, __call = function(t) MenuV:OpenMenu(t) end, __newindex = function(t, k, v) local whitelisted = { 'Title', 'Subtitle', 'Position', 'Color', 'R', 'G', 'B', 'Size', 'Dictionary', 'Texture', 'Theme' } local key = U:Ensure(k, 'unknown') local oldValue = rawget(t.data, k) if (not U:Any(key, whitelisted, 'value') and oldValue ~= nil) then return end local checkInput = t.Validate ~= nil and type(t.Validate) == 'function' local inputParser = t.Parser ~= nil and type(t.Parser) == 'function' local updateIndexTrigger = t.NewIndex ~= nil and type(t.NewIndex) == 'function' if (checkInput) then local result = t:Validate(key, v) result = U:Ensure(result, true) if (not result) then return end end if (inputParser) then local parsedValue = t:Parser(key, v) v = parsedValue or v end rawset(t.data, k, v) if (key == 'Theme' or key == 'theme') then local theme_value = string.lower(U:Ensure(v, 'default')) if (theme_value == 'native') then rawset(t.data, 'color', { R = 255, G = 255, B = 255 }) local texture = U:Ensure(rawget(t.data, 'Texture'), 'default') if (texture == 'default') then rawset(t.data, 'Texture', 'default_native') end elseif (theme_value == 'default') then local texture = U:Ensure(rawget(t.data, 'Texture'), 'default') if (texture == 'default_native') then rawset(t.data, 'Texture', 'default') end end end if (updateIndexTrigger) then t:NewIndex(key, v) end if (t.Trigger ~= nil and type(t.Trigger) == 'function') then t:Trigger('update', key, v, oldValue) end end, __len = function(t) return #t.Items end, __pairs = function(t) return pairs(rawget(t.data, 'Items') or {}) end, __ipairs = function(t) return ipairs(rawget(t.data, 'Items') or {}) end, __metatable = 'MenuV', } ---@class Menu ---@field public IsOpen boolean `true` if menu is open, otherwise `false` ---@field public UUID string UUID of Menu ---@field public Title string Title of Menu ---@field public Subtitle string Subtitle of Menu ---@field public Position string | "'topleft'" | "'topcenter'" | "'topright'" | "'centerleft'" | "'center'" | "'centerright'" | "'bottomleft'" | "'bottomcenter'" | "'bottomright'" ---@field public Texture string Name of texture example: "default" ---@field public Dictionary string Name of dictionary example: "menuv" ---@field public Color table Color of Menu ---@field private Events table List of registered `on` events ---@field public Items Item[] List of items ---@field public Trigger fun(t: Item, event: string) ---@field public On fun(t: Menu, event: string, func: function|Menu): string ---@field public RemoveOnEvent fun(t: Menu, event: string, uuid: string) ---@field public Validate fun(t: Menu, k: string, v:any) ---@field public NewIndex fun(t: Menu, k: string, v: any) ---@field public Parser fun(t: Menu, k: string, v: any) ---@field public AddButton fun(t: Menu, info: table):Item ---@field public AddCheckbox fun(t: Menu, info: table):Item ---@field public AddSlider fun(t: Menu, info: table):SliderItem ---@field public AddRange fun(t: Menu, info: table):RangeItem ---@field public AddConfirm fun(t: Menu, info: table):ConfirmItem ---@field public AddControlKey fun(t: Menu, action: string, func: function, description: string, defaultType: string, defaultKey: string) ---@field public OpenWith fun(t: Menu, defaultType: string, defaultKey: string) ---@field public SetTitle fun(t: Menu, title: string) ---@field public SetSubtitle fun(t: Menu, subtitle: string) ---@field public SetPosition fun(t: Menu, position: string) ---@field public ClearItems fun(t: Menu) ---@field public Open fun(t: Menu) ---@field public Close fun(t: Menu) ---@field public ToTable fun(t: Menu):table local menu = setmetatable({ data = item, __class = 'Menu', __type = 'Menu' }, mt) menu.Items(function(items, trigger, key, index, value, oldValue) menu:Trigger(trigger, key, index, value, oldValue) end) for k, v in pairs(info or {}) do local key = U:Ensure(k, 'unknown') if (key == 'unknown') then return end menu:On(key, v) end LoadTextureDictionary(menu.Dictionary) return menu end _ENV.CreateMenu = CreateMenu _G.CreateMenu = CreateMenu ----------------------- [ 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.4.1 -- Description: FiveM menu library for creating menu's ----------------------- [ MenuV ] ----------------------- local assert = assert local decode = assert(json.decode) --- FiveM globals local LoadResourceFile = assert(LoadResourceFile) --- MenuV globals ---@type Utilities local Utilities = assert(Utilities) --- Empty translations table local translations = {} --- Load all translations local lang = Utilities:Ensure((Config or {}).Language, 'en') local translations_path = ('languages/%s.json'):format(lang) local translations_raw = LoadResourceFile('menuv', translations_path) if (translations_raw) then local transFile = decode(translations_raw) if (transFile) then translations = Utilities:Ensure(transFile.translations, {}) end end _ENV.translations = translations _G.translations = translations --- 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 cb('ok') return end MenuV.CurrentMenu:RemoveOnEvent('update', MenuV.CurrentUpdateUUID) MenuV.CurrentMenu:Trigger('close') MenuV.CurrentMenu:DestroyThreads() MenuV.CurrentMenu = nil 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)