Anti Meta-Gaming

Showcase
Snti Meta-Gaming

Download
GitHub

Dependencies
None

Introducing a unique FiveM script that enforces server access based on Discord voice channel participation. This script actively monitors each player every minute, ensuring they remain in a designated voice channel. If a player leaves the channel, they will be promptly removed from the server.

Code is accessible Yes
Subscription-based No
Lines (approximately) 70
Requirements Requirements
Support Yes
12 Likes

Can you add a web camera functionality so I can track my player’s face during RP.

image

20 Likes

Give me your opinion! :speech_balloon:

Why Discord? Change this to TeamSpeak so is it useless!

3 Likes

i’m sorry but who uses teamspeak for a fivem server in 2024?

6 Likes

Only the real ones.

3 Likes

I think this is good and all, but what if the person playing wants to be in a voice call with a freind playing a seperate game, and freind two wants to still be playing on a server?

1 Like

its possible to add this only for X “job” or "X"users ?

1 Like

This is a meta gaming

Greate idea, next update

1 Like

Meta gaming is using external information & both users being in the server & in a vc (Both parties) what if they want to be in a call with someone playing a different game and doesnt play the server…?

This resource doesn’t stop any metagaming, you could just text someone meta knowledge, which is probably faster/more straightforward to do.

1 Like

bro’s acting like this is samp

A Teamspeak intigration would have no added value for META GAMING, because the players could then continue to meta via Discord

I think you guys have mistaken my reply as serious. :rofl:

1 Like

Can we add category with 6 voice channels. With this script?

I cannot seem to get this working… I’ve inputted the ServerID, Bot Token and VC Channel id however it’s acting as though I’m still not in it when I am?

yeah bro

1 Like

Hey, can you update it and add “ignore_staff” ??

Hey, you can find the code here for “ignore staff”

-- if not lib.checkDependency('ox_lib', '3.30.0', true) then return end

---@class Config
local Config = {
    GuildID = "956998102479405077",
    DiscordBotToken = "YOUR DISCORD TOKEN",
    VoiceChannels = lib.table.freeze({
        "1376905746603708528",
        "1296207068088373330"
    }),
    BypassRoles = lib.table.freeze({
        "1295864051674513503"--,
        --""
    }),
    Messages = {
        checking = "Vérification des autorisations...",
        voiceRequired = "^1Vous devez être dans le salon [ Présence IG ] pour rejoindre le serveur",
        noDiscord = "^1Impossible de vérifier votre compte Discord",
        invalidPlayer = "^1Erreur de vérification du joueur"
    },
    CheckInterval = 3 * 60000 -- 3 minutes
}

---@type table<number, boolean>
local playerChecks = {}

---@type table<number, boolean>
local bypassCache = {}

---@param source number
---@return boolean
local function IsPlayerValid(source)
    if not source then 
        lib.print.info('Source invalide')
        return false
    end
    if not DoesPlayerExist(source) then
        lib.print.warn('Joueur inexistant:', source)
        return false
    end
    return true
end

---@param source number
---@return string|nil
local function GetDiscordId(source)
    if not IsPlayerValid(source) then return nil end
    local discordId = GetPlayerIdentifierByType(source, "discord")
    if not discordId then
        lib.print.warn('Discord non trouvé pour:', source)
        return nil
    end
    local cleanId = discordId:gsub("discord:", "")
    lib.print.info('Discord ID:', source, '->', cleanId)
    return cleanId
end

---@param source number
---@return boolean
local function CheckVoiceChannel(source)
    if not IsPlayerValid(source) then return false end
    local discordId = GetDiscordId(source)
    if not discordId then return false end
    local endpoint = string.format("https://discord.com/api/v9/guilds/%s/voice-states/%s", Config.GuildID, discordId)
    local headers = {["Authorization"] = "Bot " .. Config.DiscordBotToken, ["Content-Type"] = "application/json"}
    local statusCode, response = PerformHttpRequestAwait(endpoint, 'GET', "", headers)
    if not statusCode == 200 or not response then
        lib.print.error('Erreur API Discord:', source, 'Status:', statusCode)
        return false
    end
    local success, data = pcall(json.decode, response)
    if not success then
        lib.print.error('Erreur decode JSON:', source)
        return false
    end
    local isInChannel = lib.table.contains(Config.VoiceChannels, data.channel_id)
    lib.print.info('Vérif vocal:', source, '->', isInChannel)
    return isInChannel
end

---@param source number
---@return boolean
local function HasBypassRole(source)
    if not IsPlayerValid(source) then return false end
    if bypassCache[source] ~= nil then
        lib.print.info('Cache bypass:', source, '->', bypassCache[source])
        return bypassCache[source]
    end
    local discordId = GetDiscordId(source)
    if not discordId then return false end
    local endpoint = string.format("https://discord.com/api/v9/guilds/%s/members/%s", Config.GuildID, discordId)
    local headers = {["Authorization"] = "Bot " .. Config.DiscordBotToken, ["Content-Type"] = "application/json"}
    local statusCode, response = PerformHttpRequestAwait(endpoint, 'GET', "", headers)
    if not statusCode == 200 or not response then
        lib.print.error('Erreur API Discord:', source, 'Status:', statusCode)
        return false
    end
    local success, data = pcall(json.decode, response)
    if not success then
        lib.print.error('Erreur decode JSON:', source)
        return false
    end
    local hasRole = false
    for _, roleId in ipairs(Config.BypassRoles) do
        if lib.table.contains(data.roles or {}, roleId) then
            hasRole = true
            break
        end
    end
    bypassCache[source] = hasRole
    lib.print.info('Vérif bypass:', source, '->', hasRole)
    return hasRole
end

AddEventHandler('playerConnecting', function(name, _, deferrals)
    local source = source
    deferrals.defer()
    Wait(0)
    if not IsPlayerValid(source) then
        lib.print.error('Connexion invalide:', source)
        return deferrals.done(Config.Messages.invalidPlayer)
    end
    lib.print.info('Connexion:', name, '(ID:', source, ')')
    deferrals.update(Config.Messages.checking)
    if not GetDiscordId(source) then
        lib.print.warn('Pas de Discord:', source)
        return deferrals.done(Config.Messages.noDiscord)
    end
    if HasBypassRole(source) then
        lib.print.info('Bypass autorisé:', source)
        return deferrals.done()
    end
    playerChecks[source] = true
    if CheckVoiceChannel(source) then
        lib.print.info('Vocal vérifié:', source)
        deferrals.done()
    else
        lib.print.warn('Vocal requis:', source)
        deferrals.done(Config.Messages.voiceRequired)
    end
    playerChecks[source] = nil
end)

AddEventHandler('playerDropped', function()
    local source = source
    if not IsPlayerValid(source) then return end
    playerChecks[source] = nil
    bypassCache[source] = nil
    lib.print.info('Cache nettoyé:', source)
end)

CreateThread(function()
    local arr = {}
    local num = 0
    while true do
        Wait(Config.CheckInterval)
        lib.print.info('Début vérification périodique')
        for _, playerId in ipairs(GetPlayers()) do
            if IsPlayerValid(playerId) and not playerChecks[playerId] then
                if not HasBypassRole(playerId) and not CheckVoiceChannel(playerId) then
                    num += 1
                    arr[num] = playerId
                    lib.print.warn('Joueur Ă  expulser:', playerId)
                end
            end
        end
        if num > 0 then
            lib.print.info('Expulsion de', num, 'joueurs')
            for i = 1, num do
                if IsPlayerValid(arr[i]) then
                    DropPlayer(arr[i], Config.Messages.voiceRequired)
                end
            end
            table.wipe(arr)
            num = 0
        end
    end
end)