From 387f9260f30324464b1a3509f2d607edd5e36a14 Mon Sep 17 00:00:00 2001 From: Musiker15 Date: Sat, 1 Jun 2024 14:07:27 +0200 Subject: [PATCH] Update v2.1.0 * Added new VersionChecker * Added Config.customNotification * Integrated MSK Bansystem * Changes in Callbacks --- VERSION | 2 +- client/cl_callback.lua | 78 ++++++------- client/main.lua | 10 +- config.lua | 30 ++++- fxmanifest.lua | 4 +- server/main.lua | 80 +++++++++---- server/sv_bansystem.lua | 241 ++++++++++++++++++++++++++++++++++++++++ server/sv_callback.lua | 51 ++++----- 8 files changed, 390 insertions(+), 106 deletions(-) create mode 100644 server/sv_bansystem.lua diff --git a/VERSION b/VERSION index 415b19f..50aea0e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0 \ No newline at end of file +2.1.0 \ No newline at end of file diff --git a/client/cl_callback.lua b/client/cl_callback.lua index d215fec..707ef6d 100644 --- a/client/cl_callback.lua +++ b/client/cl_callback.lua @@ -11,8 +11,18 @@ local GenerateCallbackHandlerKey = function() end end +RegisterNetEvent("msk_core:client:callbackResponse", function(requestId, ...) + if not CallbackHandler[requestId] then return end + CallbackHandler[requestId] = {...} +end) + +RegisterNetEvent("msk_core:client:callbackNotFound", function(requestId) + if not CallbackHandler[requestId] then return end + CallbackHandler[requestId] = nil +end) + ---------------------------------------------------------------- --- NEW Method for Client Callbacks +-- Client Callbacks ---------------------------------------------------------------- MSK.Register = function(eventName, cb) Callbacks[eventName] = cb @@ -33,7 +43,7 @@ RegisterNetEvent('msk_core:client:triggerClientCallback', function(playerId, eve end) ---------------------------------------------------------------- --- NEW Method for Server Callbacks +-- Server Callbacks with Method [return] ---------------------------------------------------------------- MSK.Trigger = function(eventName, ...) local requestId = GenerateCallbackHandlerKey() @@ -45,7 +55,7 @@ MSK.Trigger = function(eventName, ...) p:reject(('Request Timed Out: [%s] [%s]'):format(eventName, requestId)) end) - TriggerServerEvent('msk_core:server:triggerServerCallback', eventName, requestId, ...) + TriggerServerEvent('msk_core:server:triggerCallback', eventName, requestId, false, ...) while CallbackHandler[requestId] == 'request' do Wait(0) end if not CallbackHandler[requestId] then return end @@ -58,52 +68,30 @@ MSK.Trigger = function(eventName, ...) end exports('Trigger', MSK.Trigger) -RegisterNetEvent("msk_core:client:callbackResponse", function(requestId, ...) - if not CallbackHandler[requestId] then return end - CallbackHandler[requestId] = {...} -end) - -RegisterNetEvent("msk_core:client:callbackNotFound", function(requestId) - if not CallbackHandler[requestId] then return end - CallbackHandler[requestId] = nil -end) - ---------------------------------------------------------------- --- OLD Method for Server Callbacks [OUTDATED - Do not use this!] +-- Server Callbacks with Method [cb] ---------------------------------------------------------------- -local callbackRequest = {} - -local GenerateRequestKey = function() - local requestId = math.random(1, 999999999) - - if not callbackRequest[requestId] then - return tostring(requestId) - else - GenerateRequestKey() - end -end - -MSK.TriggerServerCallback = function(name, ...) - local requestId = GenerateRequestKey() - local response +MSK.TriggerCallback = function(eventName, ...) + local requestId = GenerateCallbackHandlerKey() + local p = promise.new() + CallbackHandler[requestId] = 'request' - callbackRequest[requestId] = function(...) - response = {...} - end + SetTimeout(5000, function() + CallbackHandler[requestId] = nil + p:reject(('Request Timed Out: [%s] [%s]'):format(eventName, requestId)) + end) + + TriggerServerEvent('msk_core:server:triggerCallback', eventName, requestId, true, ...) - TriggerServerEvent('msk_core:triggerCallback', name, requestId, ...) + while CallbackHandler[requestId] == 'request' do Wait(0) end + if not CallbackHandler[requestId] then return end - while not response do Wait(0) end + p:resolve(CallbackHandler[requestId]) + CallbackHandler[requestId] = nil - return table.unpack(response) + local result = Citizen.Await(p) + return table.unpack(result) end -MSK.TriggerCallback = MSK.TriggerServerCallback -exports('TriggerServerCallback', MSK.TriggerServerCallback) - -RegisterNetEvent("msk_core:responseCallback") -AddEventHandler("msk_core:responseCallback", function(requestId, ...) - if callbackRequest[requestId] then - callbackRequest[requestId](...) - callbackRequest[requestId] = nil - end -end) \ No newline at end of file +MSK.TriggerServerCallback = MSK.TriggerCallback +exports('TriggerCallback', MSK.TriggerCallback) +exports('TriggerServerCallback', MSK.TriggerCallback) \ No newline at end of file diff --git a/client/main.lua b/client/main.lua index 31595d3..daad959 100644 --- a/client/main.lua +++ b/client/main.lua @@ -29,6 +29,10 @@ elseif Config.Framework:match('qbcore') then end) end +exports('getCoreObject', function() + return MSK +end) + MSK.Notification = function(title, message, info, time) if Config.Notification == 'native' then SetNotificationTextEntry('STRING') @@ -36,6 +40,8 @@ MSK.Notification = function(title, message, info, time) DrawNotification(false, true) elseif Config.Notification == 'okok' then exports['okokNotify']:Alert(title, message, time or 5000, info or 'info') + elseif Config.Notification == 'custom' then + Config.customNotification() else SendNUIMessage({ action = 'notify', @@ -148,8 +154,4 @@ end) RegisterNetEvent('msk_core:advancedNotification') AddEventHandler('msk_core:advancedNotification', function(text, title, subtitle, icon, flash, icontype) MSK.AdvancedNotification(text, title, subtitle, icon, flash, icontype) -end) - -exports('getCoreObject', function() - return MSK end) \ No newline at end of file diff --git a/config.lua b/config.lua index 36ef2c6..01330ac 100644 --- a/config.lua +++ b/config.lua @@ -3,9 +3,10 @@ Config = {} Config.Debug = false Config.VersionChecker = true ---------------------------------------------------------------- --- Only Required for MSK.RegisterCommand // View Wiki for more Information about that! +-- Only Required for MSK.RegisterCommand and MSK.HasItem // View Docu for more Information about that! Config.Framework = 'esx' -- Set to 'standalone', 'esx' or 'qbcore' ---------------------------------------------------------------- +-- /coords Config.showCoords = { enable = true, command = 'coords', @@ -15,8 +16,14 @@ Config.showCoords = { -- Set to 'native' for FiveM Native Notification -- Set to 'msk' for NUI Notification -- Set to 'okok' for OKOK Notification +-- Set to 'custom' for Config.customNotification() Config.Notification = 'msk' +Config.customNotification = function() + -- Set Config.Notification = 'custom' + -- Add your own clientside Notification here +end + Config.progressColor = "#5eb131" -- Default Color for ProgressBar ---------------------------------------------------------------- Config.LoggingTypes = { @@ -28,16 +35,33 @@ Config.LoggingTypes = { Config.AntiCombatlog = { enable = false, -- Set to true if you want to use this Feature console = { - enable = true, + enable = false, text = "Der Spieler ^3%s^0 mit der ^3ID %s^0 hat den Server verlassen.\n^4Uhrzeit:^0 %s\n^4Grund:^0 %s\n^4Identifier:^0\n %s\n %s\n %s\n^4Koordinaten:^0 %s" }, discord = { -- Webhook in sv_anticombatlog.lua - enable = true, + enable = false, color = "6205745", -- https://www.mathsisfun.com/hexadecimal-decimal-colors.html botName = "MSK Scripts", botAvatar = "https://i.imgur.com/PizJGsh.png", title = "Player Disconnected", text = "Der Spieler **%s** mit der **ID %s** hat den Server verlassen." } +} +---------------------------------------------------------------- +-- For more Information go to: https://github.com/MSK-Scripts/msk_bansystem/blob/main/README.md +Config.BanSystem = { + enable = false, -- Set to true if you want to use this Feature + + discordLog = false, -- Set true to enable DiscordLogs // Webhook on sv_bansystem.lua + botColor = "6205745", -- https://www.mathsisfun.com/hexadecimal-decimal-colors.html + botName = "MSK Scripts", + botAvatar = "https://i.imgur.com/PizJGsh.png", + + commands = { + enable = true, + groups = {'superadmin', 'admin', 'god'}, + ban = 'banPlayer', + unbank = 'unbanPlayer' + } } \ No newline at end of file diff --git a/fxmanifest.lua b/fxmanifest.lua index d25571d..47721d6 100644 --- a/fxmanifest.lua +++ b/fxmanifest.lua @@ -1,10 +1,10 @@ -fx_version 'adamant' +fx_version 'cerulean' games { 'gta5' } author 'Musiker15 - MSK Scripts' name 'msk_core' description 'Core functions for MSK Scripts' -version '2.0' +version '2.1.0' lua54 'yes' diff --git a/server/main.lua b/server/main.lua index d4f1f54..75a34c1 100644 --- a/server/main.lua +++ b/server/main.lua @@ -1,22 +1,16 @@ MSK = {} -local RegisteredCommands = {} - -AddEventHandler('onResourceStart', function(resource) - if GetCurrentResourceName() ~= 'msk_core' then - print('^1Please rename the Script to^3 msk_core^0!') - print('^1Server will be shutdown^0!') - Wait(250) - os.exit() - end -end) - if Config.Framework:match('esx') then ESX = exports["es_extended"]:getSharedObject() elseif Config.Framework:match('qbcore') then QBCore = exports['qb-core']:GetCoreObject() end +exports('getCoreObject', function() + return MSK +end) + +local RegisteredCommands = {} MSK.RegisterCommand = function(name, group, cb, console, framework, suggestion) if type(name) == 'table' then for k, v in ipairs(name) do @@ -209,30 +203,70 @@ addChatSuggestions = function(name, suggestion) end GithubUpdater = function() - GetCurrentVersion = function() - return GetResourceMetadata( GetCurrentResourceName(), "version" ) + local GetCurrentVersion = function() + return GetResourceMetadata(GetCurrentResourceName(), "version") end + + local isVersionIncluded = function(Versions, cVersion) + for k, v in pairs(Versions) do + if v.version == cVersion then + return true + end + end + + return false + end local CurrentVersion = GetCurrentVersion() local resourceName = "^0[^2"..GetCurrentResourceName().."^0]" if Config.VersionChecker then - PerformHttpRequest('https://raw.githubusercontent.com/MSK-Scripts/msk_core/main/VERSION', function(Error, NewestVersion, Header) - print("###############################") - if CurrentVersion == NewestVersion then + PerformHttpRequest('https://raw.githubusercontent.com/Musiker15/VERSIONS/main/Lib.json', function(errorCode, jsonString, headers) + if not jsonString then + print(resourceName .. '^1 Update Check failed ^3Please Update to the latest Version:^9 https://github.com/MSK-Scripts/msk_core/ ^0') + print(resourceName .. '^2 ✓ Resource loaded^0 - ^5Current Version: ^0' .. CurrentVersion) + return + end + + local decoded = json.decode(jsonString) + local version = decoded[1].version + + if CurrentVersion == version then print(resourceName .. '^2 ✓ Resource is Up to Date^0 - ^5Current Version: ^2' .. CurrentVersion .. '^0') - elseif CurrentVersion ~= NewestVersion then + elseif CurrentVersion ~= version then print(resourceName .. '^1 ✗ Resource Outdated. Please Update!^0 - ^5Current Version: ^1' .. CurrentVersion .. '^0') - print('^5Newest Version: ^2' .. NewestVersion .. '^0 - ^6Download here:^9 https://github.com/MSK-Scripts/msk_core/releases/tag/v'.. NewestVersion .. '^0') + print('^5Latest Version: ^2' .. version .. '^0 - ^6Download here:^9 https://github.com/MSK-Scripts/msk_core/releases/tag/v'.. version .. '^0') + print('') + for i=1, #decoded do + if decoded[i]['version'] == CurrentVersion then + break + elseif not isVersionIncluded(decoded, CurrentVersion) then + print('^1You are using an^3 UNSUPPORTED VERSION^1 of ^0' .. resourceName) + break + end + + if decoded[i]['changelogs'] then + print('^3Changelogs v' .. decoded[i]['version'] .. '^0') + + for _, c in ipairs(decoded[i]['changelogs']) do + print(c) + end + end + end end - print("###############################") end) else - print(resourceName .. '^2 ✓ Resource loaded^0') + print(resourceName .. '^2 ✓ Resource loaded^0 - ^5Current Version: ^2' .. CurrentVersion .. '^0') end end GithubUpdater() -exports('getCoreObject', function() - return MSK -end) \ No newline at end of file +checkResourceName = function() + if GetCurrentResourceName() ~= 'msk_core' then + while true do + print('^1Please rename the Script to^3 msk_core^0!') + Wait(5000) + end + end +end +checkResourceName() \ No newline at end of file diff --git a/server/sv_bansystem.lua b/server/sv_bansystem.lua new file mode 100644 index 0000000..1dadf75 --- /dev/null +++ b/server/sv_bansystem.lua @@ -0,0 +1,241 @@ +-- Insert you Discord Webhook here +local webhookLink = "https://discord.com/api/webhooks/" + +banLog = function(source, bannedby, targetId, time, reason, playerIds, banId) + if not Config.BanSystem.discordLog then return end + + local botColor = Config.BanSystem.botColor + local botName = Config.BanSystem.botName + local botAvatar = Config.BanSystem.botAvatar + local title = "MSK Bansystem" + local description = ('Player %s (ID: %s) banned the Player %s (ID: %s) for %s until %s'):format(bannedby, source or 0, GetPlayerName(targetId), targetId, reason, time) + local fields = { + {name = "Some IDs", value = playerIds}, + } + local footer = { + text = "© MSK Scripts", + link = "https://i.imgur.com/PizJGsh.png" + } + local time = "%d/%m/%Y %H:%M:%S" -- format: "day/month/year hour:minute:second" + + MSK.AddWebhook(webhookLink, botColor, botName, botAvatar, title, description, fields, footer, time) +end + +unbanLog = function(source, unbannedby, banId) + if not Config.BanSystem.discordLog then return end + + local botColor = Config.BanSystem.botColor + local botName = Config.BanSystem.botName + local botAvatar = Config.BanSystem.botAvatar + local title = "MSK Bansystem" + local description = ('Player %s (ID: %s) unbanned BanID %s'):format(unbannedby, source or 0, banId) + local fields = false + local footer = { + text = "© MSK Scripts", + link = "https://i.imgur.com/PizJGsh.png" + } + local time = "%d/%m/%Y %H:%M:%S" -- format: "day/month/year hour:minute:second" + + MSK.AddWebhook(webhookLink, botColor, botName, botAvatar, title, description, fields, footer, time) +end + +local bannedPlayers = {} + +AddEventHandler('onResourceStart', function(resourceName) + if GetCurrentResourceName() ~= resourceName then return end + if not Config.BanSystem.enable then return end + + local createTable = MySQL.query.await("CREATE TABLE IF NOT EXISTS msk_bansystem (`id` int(10) NOT NULL AUTO_INCREMENT, `ids` longtext DEFAULT NULL, `time` text NULL, `reason` text NOT NULL, `bannedby` varchar(80) NOT NULL, PRIMARY KEY (`id`));") + if createTable.warningStatus == 0 then + logging('debug', '^2 Successfully ^3 created ^2 table ^3 msk_bansystem ^0') + end + + local data = MySQL.query.await("SELECT * FROM msk_bansystem") + if not data then return end + + for k, v in pairs(data) do + table.insert(bannedPlayers, {id = v.id, ids = json.decode(v.ids), reason = v.reason, time = v.time, from = v.bannedby}) + end +end) + +local split = function(s, delimiter) + local result = {} + + for match in (s..delimiter):gmatch("(.-)"..delimiter) do + table.insert(result, match) + end + + return result +end + +local isPlayerBanned = function(source, playerName) + local identifiers = GetPlayerIdentifiers(source) + local player = {} + + player['name'] = playerName or GetPlayerName(source) + for _, v in pairs(identifiers) do + player[split(v, ':')[1]] = v + end + + for k, v in pairs(bannedPlayers) do + for name, id in pairs(v.ids) do + if player[name] and id == player[name] then + local pattern = "(%d+)-(%d+)-(%d+) (%d+):(%d+)" + local timeToConvert = v.time + local day, month, year, hour, minute = timeToConvert:match(pattern) + local time = os.time({day = day, month = month, year = year, hour = hour, min = minute}) + logging('debug', os.date('%d-%m-%Y %H:%M', os.time()), os.date('%d-%m-%Y %H:%M', time)) + + if os.time() < time then + return v + else + return v, true + end + break + end + end + end + + return false +end +exports('isPlayerBanned', isPlayerBanned) + +local formatTime = function(time) + local banTime = 0 + + if time:find('P') then + banTime = os.time() + (60 * 60 * 24 * 7 * 52 * 100) + elseif time:find('M') then + banTime = os.time() + (60 * split(time, 'M')[1]) + elseif time:find('H') then + banTime = os.time() + (60 * 60 * split(time, 'H')[1]) + elseif time:find('D') then + banTime = os.time() + (60 * 60 * 24 * split(time, 'D')[1]) + elseif time:find('W') then + banTime = os.time() + (60 * 60 * 24 * 7 * split(time, 'W')[1]) + end + + return banTime, os.date('%d-%m-%Y %H:%M', banTime) +end + +banPlayer = function(source, playerId, time, reason) + local playerName = GetPlayerName(playerId) + + if not playerName then + if source then Config.Notification(source, ('Player with ID %s not found!'):format(playerId)) end + return logging('debug', ('Player with ^2ID %s^0 not found!'):format(playerId)) + end + + local identifiers = GetPlayerIdentifiers(playerId) + local timestamp, banTime = formatTime(time) + local player = {} + + player['name'] = playerName + for _, v in pairs(identifiers) do + player[split(v, ':')[1]] = v + end + + local bannedby = 'System' + if source then bannedby = GetPlayerName(source) end + + MySQL.query('INSERT INTO msk_bansystem (ids, time, reason, bannedby) VALUES (@ids, @time, @reason, @bannedby)', { + ['@ids'] = json.encode(player), + ['@time'] = banTime, + ['@reason'] = reason, + ['@bannedby'] = bannedby + }, function(response) + if response then + logging('debug', 'Player with ID ^2' .. playerId .. '^0 was banned until ^2' .. banTime .. '^0 for Reason: ^2' .. reason .. '^0. BanID: ^2' .. response.insertId .. '^0') + if source then Config.Notification(source, 'Player with ID ' .. playerId .. ' was banned until ' .. banTime .. ' for Reason: ' .. reason .. '. BanID: ' .. response.insertId) end + table.insert(bannedPlayers, {id = response.insertId, ids = player, reason = reason, time = banTime, from = bannedby}) + banLog(source, bannedby, playerId, banTime, reason, json.encode(player), response.insertId) + DropPlayer(playerId, ('Banned by %s for %s until %s. BanID: %s'):format(bannedby, reason, banTime, response.insertId)) + end + end) +end +exports('banPlayer', banPlayer) + +unbanPlayer = function(source, banId) + MySQL.query('DELETE FROM msk_bansystem WHERE id = @id', { + ['@id'] = banId + }, function(response) + if response.affectedRows > 0 then + logging('debug', 'Player with BanID ^2' .. banId .. '^0 was unbanned.') + if source then Config.Notification(source, 'Player with BanID ' .. banId .. ' was unbanned.') end + + local index + for k, v in pairs(bannedPlayers) do + if v.id == banID then + index = k + end + end + table.remove(bannedPlayers, index) + + local unbannedby = 'System' + if source then unbannedby = GetPlayerName(source) end + unbanLog(source, unbannedby, banId) + else + logging('debug', 'BanID ^2' .. banId .. '^0 was not found.') + if source then Config.Notification(source, 'BanID ' .. banId .. ' was not found.') end + end + end) +end +exports('unbanPlayer', unbanPlayer) + +AddEventHandler('playerConnecting', function(playerName, setKickReason, deferrals) + local src = source + if not Config.BanSystem.enable then return end + local isBanned, expired = isPlayerBanned(src, playerName) + + if isBanned and not expired then + CancelEvent() -- FiveM Native Function for cancelling the currently executing event + setKickReason(('Banned by %s until %s for %s. BanID: %s'):format(isBanned.from, isBanned.time, isBanned.reason, isBanned.id)) + elseif isBanned and expired then + unbanPlayer(nil, isBanned.id) + end +end) + +if Config.BanSystem.enable and Config.BanSystem.commands.enable then + for k, v in pairs(Config.BanSystem.commands.groups) do + ExecuteCommand(('add_ace group.%s command.%s allow'):format(v, Config.BanSystem.commands.ban)) + ExecuteCommand(('add_ace group.%s command.%s allow'):format(v, Config.BanSystem.commands.unban)) + end + + local isAllowed = function(source) + for k, group in pairs(Config.BanSystem.commands.groups) do + if IsPlayerAceAllowed(source, group) then + return true + end + end + return false + end + + RegisterCommand(Config.BanSystem.commands.ban, function(source, args, raw) + local src = source + local playerId, time, reason = args[1], args[2], args[3] + + if not playerId or not time then return end + if not reason then reason = 'Unknown' end + if src == 0 then return banPlayer(nil, playerId, time, reason) end + + if isAllowed(src) then + banPlayer(src, playerId, time, reason) + else + Config.Notification(src, 'You don\'t have permission to do that!') + end + end) + + RegisterCommand(Config.BanSystem.commands.unban, function(source, args, raw) + local src = source + local banId = args[1] + + if not banId then return end + if src == 0 then return unbanPlayer(nil, banId) end + + if isAllowed(src) then + unbanPlayer(src, banId) + else + Config.Notification(src, 'You don\'t have permission to do that!') + end + end) +end \ No newline at end of file diff --git a/server/sv_callback.lua b/server/sv_callback.lua index 720b8a9..77b732c 100644 --- a/server/sv_callback.lua +++ b/server/sv_callback.lua @@ -1,18 +1,8 @@ local Callbacks = {} local CallbackHandler = {} -local GenerateCallbackHandlerKey = function() - local requestId = math.random(1, 999999999) - - if not CallbackHandler[requestId] then - return tostring(requestId) - else - GenerateCallbackHandlerKey() - end -end - ---------------------------------------------------------------- --- NEW Method for Server Callbacks +-- Server Callbacks ---------------------------------------------------------------- MSK.Register = function(eventName, cb) Callbacks[eventName] = cb @@ -23,7 +13,7 @@ exports('Register', MSK.Register) exports('RegisterCallback', MSK.Register) exports('RegisterServerCallback', MSK.Register) -RegisterNetEvent('msk_core:server:triggerServerCallback', function(eventName, requestId, ...) +RegisterNetEvent('msk_core:server:triggerCallback', function(eventName, requestId, cb, ...) local playerId = source if not Callbacks[eventName] then @@ -31,12 +21,30 @@ RegisterNetEvent('msk_core:server:triggerServerCallback', function(eventName, re return end - TriggerClientEvent("msk_core:client:callbackResponse", playerId, requestId, Callbacks[eventName](playerId, ...)) + if not cb then + -- Method [return] + TriggerClientEvent("msk_core:client:callbackResponse", playerId, requestId, Callbacks[eventName](playerId, ...)) + else + -- Method [cb] + Callbacks[eventName](playerId, function(...) + TriggerClientEvent("msk_core:client:callbackResponse", playerId, requestId, ...) + end, ...) + end end) ---------------------------------------------------------------- --- NEW Method for Client Callbacks +-- Client Callbacks ---------------------------------------------------------------- +local GenerateCallbackHandlerKey = function() + local requestId = math.random(1, 999999999) + + if not CallbackHandler[requestId] then + return tostring(requestId) + else + GenerateCallbackHandlerKey() + end +end + MSK.Trigger = function(eventName, playerId, ...) local requestId = GenerateCallbackHandlerKey() local p = promise.new() @@ -75,20 +83,7 @@ RegisterNetEvent("msk_core:server:callbackNotFound", function(requestId) end) ---------------------------------------------------------------- --- OLD Method for Server Callbacks [OUTDATED - Do not use this!] ----------------------------------------------------------------- -RegisterNetEvent('msk_core:triggerCallback') -AddEventHandler('msk_core:triggerCallback', function(name, requestId, ...) - local src = source - if not Callbacks[name] then return end - - Callbacks[name](src, function(...) - TriggerClientEvent("msk_core:responseCallback", src, requestId, ...) - end, ...) -end) - ----------------------------------------------------------------- --- Server Callbacks with New Method +-- Server Callbacks with Method [return] ---------------------------------------------------------------- MSK.Register('msk_core:hasItem', function(source, item) local src = source