451 lines
		
	
	
		
			No EOL
		
	
	
		
			14 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			451 lines
		
	
	
		
			No EOL
		
	
	
		
			14 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
| Utils = {}
 | |
| Utils.Debug = {}
 | |
| Utils.Table = {}
 | |
| Utils.Math = {}
 | |
| Utils.String = {}
 | |
| Utils.CustomScripts = {}
 | |
| Utils.Config = Config
 | |
| Utils.Lang = {}
 | |
| Utils.Version = LoadResourceFile("lc_utils", "version") and string.gsub(LoadResourceFile("lc_utils", "version"), '^%s*(.-)%s*$', '%1') or nil
 | |
| 
 | |
| exports('GetUtils', function()
 | |
| 	return Utils
 | |
| end)
 | |
| 
 | |
| -----------------------------------------------------------------------------------------------------------------------------------------
 | |
| -- Debug
 | |
| -----------------------------------------------------------------------------------------------------------------------------------------
 | |
| 
 | |
| function Utils.Debug.printTable(...)
 | |
|     local args = {...}
 | |
|     for _, arg in ipairs(args) do
 | |
|         printNode(arg)
 | |
|     end
 | |
| end
 | |
| 
 | |
| function printNode(node)
 | |
| 	if type(node) == "table" then
 | |
| 		-- to make output beautiful
 | |
| 		local function tab(amt)
 | |
| 			local str = ""
 | |
| 			for i=1,amt do
 | |
| 				str = str .. "\t"
 | |
| 			end
 | |
| 			return str
 | |
| 		end
 | |
| 	
 | |
| 		local cache, stack, output = {},{},{}
 | |
| 		local depth = 1
 | |
| 		local output_str = "{\n"
 | |
| 	
 | |
| 		while true do
 | |
| 			local size = 0
 | |
| 			for k,v in pairs(node) do
 | |
| 				size = size + 1
 | |
| 			end
 | |
| 	
 | |
| 			local cur_index = 1
 | |
| 			for k,v in pairs(node) do
 | |
| 				if (cache[node] == nil) or (cur_index >= cache[node]) then
 | |
| 				
 | |
| 					if (string.find(output_str,"}",output_str:len())) then
 | |
| 						output_str = output_str .. ",\n"
 | |
| 					elseif not (string.find(output_str,"\n",output_str:len())) then
 | |
| 						output_str = output_str .. "\n"
 | |
| 					end
 | |
| 	
 | |
| 					-- This is necessary for working with HUGE tables otherwise we run out of memory using concat on huge strings
 | |
| 					table.insert(output,output_str)
 | |
| 					output_str = ""
 | |
| 				
 | |
| 					local key
 | |
| 					if (type(k) == "number" or type(k) == "boolean") then
 | |
| 						key = "["..tostring(k).."]"
 | |
| 					else
 | |
| 						key = "['"..tostring(k).."']"
 | |
| 					end
 | |
| 	
 | |
| 					if (type(v) == "number" or type(v) == "boolean") then
 | |
| 						output_str = output_str .. tab(depth) .. key .. " = "..tostring(v)
 | |
| 					elseif (type(v) == "table") then
 | |
| 						output_str = output_str .. tab(depth) .. key .. " = {\n"
 | |
| 						table.insert(stack,node)
 | |
| 						table.insert(stack,v)
 | |
| 						cache[node] = cur_index+1
 | |
| 						break
 | |
| 					else
 | |
| 						output_str = output_str .. tab(depth) .. key .. " = '"..tostring(v).."'"
 | |
| 					end
 | |
| 	
 | |
| 					if (cur_index == size) then
 | |
| 						output_str = output_str .. "\n" .. tab(depth-1) .. "}"
 | |
| 					else
 | |
| 						output_str = output_str .. ","
 | |
| 					end
 | |
| 				else
 | |
| 					-- close the table
 | |
| 					if (cur_index == size) then
 | |
| 						output_str = output_str .. "\n" .. tab(depth-1) .. "}"
 | |
| 					end
 | |
| 				end
 | |
| 	
 | |
| 				cur_index = cur_index + 1
 | |
| 			end
 | |
| 	
 | |
| 			if (#stack > 0) then
 | |
| 				node = stack[#stack]
 | |
| 				stack[#stack] = nil
 | |
| 				depth = cache[node] == nil and depth + 1 or depth - 1
 | |
| 			else
 | |
| 				break
 | |
| 			end
 | |
| 		end
 | |
| 	
 | |
| 		-- This is necessary for working with HUGE tables otherwise we run out of memory using concat on huge strings
 | |
| 		table.insert(output,output_str)
 | |
| 		output_str = table.concat(output)
 | |
| 	
 | |
| 		print(output_str)
 | |
| 	else
 | |
| 		print(node)
 | |
| 	end
 | |
| end
 | |
| 
 | |
| -----------------------------------------------------------------------------------------------------------------------------------------
 | |
| -- Table
 | |
| -----------------------------------------------------------------------------------------------------------------------------------------
 | |
| 
 | |
| function Utils.Table.tableLength(T)
 | |
| 	local count = 0
 | |
| 	for _ in pairs(T) do count = count + 1 end
 | |
| 	return count
 | |
| end
 | |
| 
 | |
| function Utils.Table.contains(table, element)
 | |
| 	for _, value in pairs(table) do
 | |
| 		if value == element then
 | |
| 			return true
 | |
| 		end
 | |
| 	end
 | |
| 	return false
 | |
| end
 | |
| 
 | |
| function Utils.Table.deepCopy(orig)
 | |
| 	local orig_type = type(orig)
 | |
| 	local copy
 | |
| 	if orig_type == 'table' then
 | |
| 		copy = {}
 | |
| 		for orig_key, orig_value in next, orig, nil do
 | |
| 			copy[Utils.Table.deepCopy(orig_key)] = Utils.Table.deepCopy(orig_value)
 | |
| 		end
 | |
| 		setmetatable(copy, Utils.Table.deepCopy(getmetatable(orig)))
 | |
| 	else -- number, string, boolean, etc
 | |
| 		copy = orig
 | |
| 	end
 | |
| 	return copy
 | |
| end
 | |
| 
 | |
| function Utils.Table.deepMerge(target, source)
 | |
| 	for key, value in pairs(source) do
 | |
| 		if type(value) == "function" then
 | |
| 			target[key] = value
 | |
| 		elseif type(value) == "table" and value ~= nil then
 | |
| 			-- If the target does not have the key, initialize it as a table
 | |
| 			if type(target[key]) ~= "table" then
 | |
| 				target[key] = {}
 | |
| 			end
 | |
| 			-- Recursively merge tables
 | |
| 			Utils.Table.deepMerge(target[key], value)
 | |
| 		else
 | |
| 			target[key] = value
 | |
| 		end
 | |
| 	end
 | |
| end
 | |
| 
 | |
| 
 | |
| -----------------------------------------------------------------------------------------------------------------------------------------
 | |
| -- String
 | |
| -----------------------------------------------------------------------------------------------------------------------------------------
 | |
| 
 | |
| function Utils.String.capitalizeFirst(str)
 | |
| 	if str == nil or str == '' then
 | |
| 		return str
 | |
| 	end
 | |
| 	return (str:sub(1, 1):upper() .. str:sub(2))
 | |
| end
 | |
| 
 | |
| function Utils.String.split(str, sep)
 | |
| 	sep = sep or "%s"
 | |
| 	local fields = {}
 | |
| 	local pattern = string.format("([^%s]+)", sep)
 | |
| 	str:gsub(pattern, function(c) fields[#fields + 1] = c end)
 | |
| 	return fields
 | |
| end
 | |
| 
 | |
| -----------------------------------------------------------------------------------------------------------------------------------------
 | |
| -- Math
 | |
| -----------------------------------------------------------------------------------------------------------------------------------------
 | |
| 
 | |
| function Utils.numberFormat(number, decimalPlaces)
 | |
|     local formatString = "%f"
 | |
|     if decimalPlaces ~= nil then
 | |
|         formatString = string.format("%%.%df", decimalPlaces)
 | |
|     end
 | |
| 
 | |
|     local formattedNumber = string.format(formatString, number)
 | |
| 
 | |
|     -- Remove trailing zeros if needed
 | |
|     if decimalPlaces == nil then
 | |
|         formattedNumber = formattedNumber:gsub("%.?0*$", "")
 | |
|     end
 | |
| 
 | |
|     return formattedNumber
 | |
| end
 | |
| 
 | |
| function Utils.Math.trim(value)
 | |
| 	if not value then return nil end
 | |
| 	return (string.gsub(value, '^%s*(.-)%s*$', '%1'))
 | |
| end
 | |
| 
 | |
| function Utils.Math.round(value, numDecimalPlaces)
 | |
| 	if not numDecimalPlaces then return math.floor(value + 0.5) end
 | |
| 	local power = 10 ^ numDecimalPlaces
 | |
| 	return math.floor((value * power) + 0.5) / (power)
 | |
| end
 | |
| 
 | |
| function Utils.Math.weightedRandom(weights, shift)
 | |
| 	local sum = 0
 | |
| 	for _, weight in pairs(weights) do
 | |
| 		sum = sum + weight
 | |
| 	end
 | |
| 
 | |
| 	local threshold = math.random(0, sum) + (shift or 0)
 | |
| 	if threshold > sum then threshold = sum end
 | |
| 	local cumulative = 0
 | |
| 	for number, weight in pairs(weights) do
 | |
| 		cumulative = cumulative + weight
 | |
| 		if threshold <= cumulative then
 | |
| 			return number
 | |
| 		end
 | |
| 	end
 | |
| end
 | |
| 
 | |
| function Utils.Math.getRandomKeyFromTable(tbl)
 | |
| 	local keys = {}
 | |
| 	for key in pairs(tbl) do
 | |
| 		table.insert(keys, key)
 | |
| 	end
 | |
| 	local index = keys[math.random(1, #keys)]
 | |
| 	return index
 | |
| end
 | |
| 
 | |
| function Utils.Math.checkIfCurrentVersionisOutdated(latestVersion, curVersion)
 | |
| 	local curVersionParts = {}
 | |
| 	for part in string.gmatch(curVersion, "[^.]+") do
 | |
| 		table.insert(curVersionParts, part)
 | |
| 	end
 | |
| 
 | |
| 	local latestVersionParts = {}
 | |
| 	for part in string.gmatch(latestVersion, "[^.]+") do
 | |
| 		table.insert(latestVersionParts, part)
 | |
| 	end
 | |
| 
 | |
| 	local function isPositiveInteger(str)
 | |
| 		return tonumber(str) ~= nil and math.floor(tonumber(str) or 0) == tonumber(str) and tonumber(str) >= 0
 | |
| 	end
 | |
| 
 | |
| 	local function validateParts(parts)
 | |
| 		for i = 1, #parts do
 | |
| 			if not isPositiveInteger(parts[i]) then
 | |
| 				return false
 | |
| 			end
 | |
| 		end
 | |
| 		return true
 | |
| 	end
 | |
| 
 | |
| 	if not validateParts(curVersionParts) or not validateParts(latestVersionParts) then
 | |
| 		return 0 / 0 -- NaN in Lua
 | |
| 	end
 | |
| 
 | |
| 	for i = 1, #latestVersionParts do
 | |
| 		if tonumber(curVersionParts[i]) == tonumber(latestVersionParts[i]) then
 | |
| 			-- Do nothing, continue to the next part
 | |
| 		elseif tonumber(curVersionParts[i]) > tonumber(latestVersionParts[i]) then
 | |
| 			return false
 | |
| 		else
 | |
| 			return true
 | |
| 		end
 | |
| 	end
 | |
| 
 | |
| 	if #curVersionParts ~= #latestVersionParts then
 | |
| 		return true
 | |
| 	end
 | |
| 
 | |
| 	return false
 | |
| end
 | |
| 
 | |
| -----------------------------------------------------------------------------------------------------------------------------------------
 | |
| -- Language
 | |
| -----------------------------------------------------------------------------------------------------------------------------------------
 | |
| 
 | |
| local cached_langs = {}
 | |
| function Utils.loadLanguageFile(lang_file)
 | |
| 	local resource = getResourceName()
 | |
| 	assert(resource,"^3Unknown resource loading the language files.^7")
 | |
| 
 | |
| 	Utils.Table.deepMerge(lang_file, Utils.Lang)
 | |
| 	cached_langs[resource] = lang_file
 | |
| end
 | |
| 
 | |
| function Utils.translate(key)
 | |
| 	local resource = getResourceName()
 | |
| 	if not resource then
 | |
| 		return 'missing_resource'
 | |
| 	end
 | |
| 
 | |
| 	if not cached_langs[resource] then
 | |
| 		return 'missing_lang'
 | |
| 	end
 | |
| 
 | |
| 	local locale = Config.locale
 | |
| 	local langObj = cached_langs[resource][locale]
 | |
| 	if not langObj then
 | |
| 		print(string.format("Language '%s' is not available. Using default 'en'.", locale))
 | |
| 		Config.locale = 'en'
 | |
| 		langObj = cached_langs[resource][Config.locale]
 | |
| 	end
 | |
| 
 | |
| 	local keys = Utils.String.split(key,".")
 | |
| 	for _, k in ipairs(keys) do
 | |
| 		if not langObj[k] then
 | |
| 			print(string.format("Translation key '%s' not found for language '%s'.", key, locale))
 | |
| 			return 'missing_translation'
 | |
| 		end
 | |
| 		langObj = langObj[k]
 | |
| 	end
 | |
| 
 | |
| 	return langObj
 | |
| end
 | |
| 
 | |
| Citizen.CreateThread(function()
 | |
| 	if GetCurrentResourceName() ~= "lc_utils" then return end
 | |
| 	Wait(1000)
 | |
| 
 | |
| 	Utils.loadLanguageFile(Utils.Lang)
 | |
| 
 | |
| 	if Utils.Version then
 | |
| 		print("^2[lc_utils] Loaded! Support discord: https://discord.gg/U5YDgbh ^3[v"..Utils.Version.."]^7")
 | |
| 	else
 | |
| 		error("^1[lc_utils] Warning: Could not load the version file.^7")
 | |
| 	end
 | |
| 
 | |
| 	assert(Config, "^3You have errors in your config file, consider fixing it or redownload the original config.^7")
 | |
| 	assert(Config.framework == "QBCore" or Config.framework == "ESX", string.format("^3The Config.framework must be set to ^1ESX^3 or ^1QBCore^3, its actually set to ^1%s^3.^7", Config.framework))
 | |
| 
 | |
| 	local configs_to_validate = {
 | |
| 		{ config_path = {"custom_scripts_compatibility"}, default_value = {	['fuel'] = "default", ['inventory'] = "default", ['keys'] = "default", ['mdt'] = "default", ['target'] = "disabled", ['notification'] = "default"} },
 | |
| 		{ config_path = {"custom_scripts_compatibility", "notification"}, default_value = "default" },
 | |
| 		{ config_path = {"owned_vehicles", "default"}, default_value = { ['garage'] = 'motelgarage', ['garage_display_name'] = 'Motel Parking' } },
 | |
| 		{ config_path = {"notification"}, default_value = { ['has_title'] = false, ['position'] = "top-right", ['duration'] = 8000 } },
 | |
| 		{ config_path = {"spawned_vehicles"}, default_value = {
 | |
| 			['lc_truck_logistics'] = {
 | |
| 				['is_static'] = false,
 | |
| 				['plate_prefix'] = "TR"
 | |
| 			},
 | |
| 			['lc_stores'] = {
 | |
| 				['is_static'] = false,
 | |
| 				['plate_prefix'] = "ST"
 | |
| 			},
 | |
| 			['lc_gas_stations'] = {
 | |
| 				['is_static'] = false,
 | |
| 				['plate_prefix'] = "GS"
 | |
| 			},
 | |
| 			['lc_dealership'] = {
 | |
| 				['is_static'] = false,
 | |
| 				['plate_prefix'] = "DE"
 | |
| 			},
 | |
| 			['lc_factories'] = {
 | |
| 				['is_static'] = false,
 | |
| 				['plate_prefix'] = "FA"
 | |
| 			}
 | |
| 		} },
 | |
| 	}
 | |
| 	Config = Utils.validateConfig(Config, configs_to_validate)
 | |
| end)
 | |
| 
 | |
| -----------------------------------------------------------------------------------------------------------------------------------------
 | |
| -- File functions validator
 | |
| -----------------------------------------------------------------------------------------------------------------------------------------
 | |
| 
 | |
| function Utils.validateFunctions(functions, file_name)
 | |
| 	local resourceName = GetCurrentResourceName()
 | |
| 
 | |
| 	for i = 1, #functions do
 | |
| 		local fnName = functions[i]
 | |
| 		local fn = _G[fnName]
 | |
| 		if not Utils.functionExists(fn) then
 | |
| 			print("^8[" .. resourceName .. "] ^3You have a missing function (^1" .. fnName .. "^3) in your '^1" .. file_name .. "^3' file. Please update this file to ensure the script functions correctly.^7")
 | |
| 			_G[fnName] = function()
 | |
| 				return true
 | |
| 			end
 | |
| 		end
 | |
| 	end
 | |
| end
 | |
| 
 | |
| local cached_functions = {}
 | |
| function Utils.functionExists(fn)
 | |
| 	if fn == nil then
 | |
| 		return false
 | |
| 	end
 | |
| 
 | |
| 	if cached_functions[fn] ~= nil then
 | |
| 		return cached_functions[fn]
 | |
| 	end
 | |
| 
 | |
| 	-- Cache the result of type check
 | |
| 	local exists = type(fn) == "function"
 | |
| 	cached_functions[fn] = exists
 | |
| 	return exists
 | |
| end
 | |
| 
 | |
| -----------------------------------------------------------------------------------------------------------------------------------------
 | |
| -- Config validator
 | |
| -----------------------------------------------------------------------------------------------------------------------------------------
 | |
| 
 | |
| function Utils.validateConfig(_Config, configs_to_validate)
 | |
| 	for _, v in pairs(configs_to_validate) do
 | |
| 		local config_entry = getConfigEntry(_Config, v.config_path)
 | |
| 
 | |
| 		if config_entry == nil then
 | |
| 			printMissingConfigMessage("Config."..table.concat(v.config_path, "."))
 | |
| 			setConfigValue(_Config, v.config_path, v.default_value)
 | |
| 		end
 | |
| 	end
 | |
| 	return _Config
 | |
| end
 | |
| 
 | |
| function getConfigEntry(_Config, path)
 | |
| 	for _, key in ipairs(path) do
 | |
| 		_Config = _Config and _Config[key]
 | |
| 	end
 | |
| 	return _Config
 | |
| end
 | |
| 
 | |
| function setConfigValue(_Config, path, value)
 | |
| 	for i = 1, #path - 1 do
 | |
| 		if _Config[path[i]] == nil then
 | |
| 			_Config[path[i]] = {}
 | |
| 		end
 | |
| 		_Config = _Config[path[i]]
 | |
| 	end
 | |
| 	_Config[path[#path]] = value
 | |
| end
 | |
| 
 | |
| function printMissingConfigMessage(config_entry)
 | |
| 	local resource = getResourceName()
 | |
| 	print("^3WARNING: Missing config '^1" .. config_entry .. "^3' in resource '^1"..resource.."^3'. The value will be set to its default. Consider redownloading the original config to obtain the correct config.^7")
 | |
| end
 | |
| 
 | |
| function getResourceName()
 | |
| 	return GetCurrentResourceName()
 | |
| end | 
