Tibia

In this tutorial you will learn how to setup the EMAC Anti-Cheat on your tibia server.

Database

Create the tables using the following queries:

CREATE TABLE `emac_anticheat` (
    `account_id` int(11) NOT NULL,
    `last_heartbeat_time` timestamp(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL,
    `kicked` int(11) DEFAULT 0 NOT NULL,
    `banned` int(11) DEFAULT 0 NOT NULL,
    `name` varchar(32) NOT NULL,
    `email` varchar(255),
    `version` varchar(16),
    `cheating` int(11) DEFAULT 0 NOT NULL,
    PRIMARY KEY (`account_id`)
);

CREATE TABLE `emac_anticheat_config` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `name` varchar(50) NOT NULL,
    `value` varchar(255) NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `emac_anticheat_config_UN` (`name`)
);

CREATE TABLE `emac_anticheat_kicks` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `account_id` int(11) NOT NULL,
    `kick_reason` varchar(100) NOT NULL,
    `time` timestamp(6) NOT NULL,
    `version` varchar(16),
    PRIMARY KEY(`id`)
);

CREATE TABLE `emac_anticheat_whitelist` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `account_id` int(11) NOT NULL,
    `reason` varchar(255),
    `date` datetime NOT NULL,
    PRIMARY KEY (`id`)
);

Insert the following values into emac_anticheat_config table:

INSERT INTO emac_anticheat_config (name, value) VALUES ('last_heartbeat_time', '0');
INSERT INTO emac_anticheat_config (name, value) VALUES ('min_version', '1.0.0');
INSERT INTO emac_anticheat_config (name, value) VALUES ('enable_kick', '0');
INSERT INTO emac_anticheat_config (name, value) VALUES ('kick_message_noac', 'You must use EMAC Anti-Cheat to play on this server');
INSERT INTO emac_anticheat_config (name, value) VALUES ('kick_message_bannedcheat', 'You have been banned by EMAC Anti-Cheat');
INSERT INTO emac_anticheat_config (name, value) VALUES ('kick_message_cheat', 'Kicked by EMAC Anti-Cheat (cheat detected)');
INSERT INTO emac_anticheat_config (name, value) VALUES ('kick_message_oldversion', 'You need to reopen your client to update EMAC Anti-Cheat to the lastest version to play on this server');
INSERT INTO emac_anticheat_config (name, value) VALUES ('max_clients', '1');

Create a user named emaclab with alter, delete, insert, select and update permissions to emac_anticheat table

CREATE USER `emaclab`@`%` IDENTIFIED BY '<PASSWORD>';
GRANT Alter, Delete, Insert, Select, Update ON TABLE `<DATABASE_NAME>`.`emac_anticheat` TO `emaclab`@`%`;
GRANT Alter, Delete, Insert, Select, Update ON TABLE `<DATABASE_NAME>`.`emac_anticheat_whitelist` TO `emaclab`@`%`;
GRANT Alter, Delete, Insert, Select, Update ON TABLE `<DATABASE_NAME>`.`emac_anticheat_kicks` TO `emaclab`@`%`;
GRANT Alter, Delete, Insert, Select, Update ON TABLE `<DATABASE_NAME>`.`emac_anticheat_config` TO `emaclab`@`%`;

PS: Change <PASSWORD> and <DATABASE_NAME>

OT Plugins

Create the following lua plugins inside /data/emac_anti_cheat folder on your OT Server

login.lua

function getPlayerAccountInfo(accountId, shouldUseName)
    local accountInfo = {}

    if shouldUseName ~= false then
        local resultQuery = db.storeQuery("SELECT name, email FROM accounts WHERE id = " .. accountId)

        if resultQuery ~= false then
            accountInfo["login"] = result.getString(resultQuery, "name")
            accountInfo["email"] = result.getString(resultQuery, "email")
            result.free(resultQuery)
        end
    else
        local resultQuery = db.storeQuery("SELECT id, email FROM accounts WHERE id = " .. accountId)

        if resultQuery ~= false then
            accountInfo["login"] = result.getString(resultQuery, "id")
            accountInfo["email"] = result.getString(resultQuery, "email")
            result.free(resultQuery)
        end
    end

    return accountInfo
end

function onLogin(player)
    local accountId = player:getAccountId()

    -- You should set the last parameter to true if your server uses login instead account number to authenticate the user.
    local playerAccountInfo = getPlayerAccountInfo(accountId, false)

    if playerAccountInfo ~= nil then
        local playerLogin = playerAccountInfo["login"]
        local playerEmail = playerAccountInfo["email"]
        
        if playerLogin ~= nil then
            local resultKickQuery = db.storeQuery("SELECT time from emac_anticheat_kicks where account_id = " .. accountId .. " AND (TIMESTAMPADD(SECOND, -10, CURRENT_TIMESTAMP) <= time) LIMIT 1")

            -- Prevent user from reconnecting too quickly after being kicked.
            if resultKickQuery ~= false then
                result.free(resultKickQuery)
                return false
            end

            -- Insert the user field in database, that field will be used by EMAC Anti-Cheat to update some informations (last heartbeat received, etc) 
            if playerEmail ~= nil then
                db.query("INSERT INTO emac_anticheat (account_id, name, email, version) VALUES (" .. accountId .. ", " .. db.escapeString(playerLogin) .. ", " .. db.escapeString(playerEmail) .. ", '9.9.9') ON DUPLICATE KEY UPDATE last_heartbeat_time = CURRENT_TIMESTAMP")
            else
                db.query("INSERT INTO emac_anticheat (account_id, name, version) VALUES (" .. accountId .. ", " .. db.escapeString(playerLogin) .. ", '9.9.9') ON DUPLICATE KEY UPDATE last_heartbeat_time = CURRENT_TIMESTAMP")
            end
            
            -- Check if user is banned from EMAC Anti-Cheat
            local resultQueryBan = db.storeQuery("SELECT account_id FROM emac_anticheat WHERE kicked = 0 AND banned = 1 AND account_id = "  .. accountId)
            if resultQueryBan ~= false then
                for _, charId in ipairs(getPlayersByAccountNumber(accountId)) do
                    local player = Player(charId)
                    if player ~= nil then
                        db.query("UPDATE emac_anticheat SET kicked = 1 WHERE account_id = " .. db.escapeString(accountId))
                    end
                end
                result.free(resultQueryBan)
            end
        end
    end

    return true
end

logout.lua

function onLogout(player)
    db.query("DELETE FROM emac_anticheat WHERE account_id = " .. player:getAccountId())
    return true
end

startup.lua

-- Some versions of theforgottenserver do not have this function defined, so let's define it ourselves
local function getPlayersByAccountNumber(accountNumber)
    local result = {}

    for _, player in ipairs(Game.getPlayers()) do
        if player:getAccountId() == accountNumber then
            result[#result + 1] = player:getId()
        end
    end

    return result
end

local function disconnectPlayer(charId)
    local player = Player(charId)

    if player ~= nil then
        -- WARNING: Some versions of theforgottenserver do not have the hasCondition function exported
        if not player:hasCondition(CONDITION_INFIGHT) then
            player:remove()
        else
            -- WARNING: That function is a custom exported function, you should define it yourself
            player:disconnect()
        end
    end

    return true
end

local function getCurrentTimestamp()
    local currentTimestamp = os.time(os.date("!*t"))

    -- Query current timestamp from database to avoid timing discrepancies between OS time and database time
    local resultQuery = db.storeQuery("SELECT UNIX_TIMESTAMP() as timestamp")

    if resultQuery ~= false then
        currentTimestamp = result.getNumber(resultQuery, "timestamp")
        result.free(resultQuery)
    end

    return currentTimestamp
end

local function getWhitelistedPlayers()
    local whitelistedPlayers = {}
    local resultQuery = db.storeQuery("SELECT account_id from emac_anticheat_whitelist")

    if resultQuery ~= false then
        repeat
            whitelistedPlayers[#whitelistedPlayers + 1] = result.getNumber(resultQuery, "account_id")
        until not result.next(resultQuery)
        result.free(resultQuery)
    end

    return whitelistedPlayers
end

local function getAnticheatConfig()
    local config = {}
    local resultQueryConfig = db.storeQuery("SELECT name, value FROM emac_anticheat_config")

    if resultQueryConfig ~= false then
        repeat
            config[result.getString(resultQueryConfig, "name")] = result.getString(resultQueryConfig, "value")
        until not result.next(resultQueryConfig)

        result.free(resultQueryConfig)
    
        return config
    end

    return nil
end

local function isPlayerWhitelisted(accountId, whitelistedPlayers)
    for _, whitelistedAccountId in ipairs(whitelistedPlayers) do
        if whitelistedAccountId == accountId then
            return true
        end
    end

    return false
end

local function checkPlayers(anticheatConfig, minVersion)
    local whitelistedPlayers = getWhitelistedPlayers()
    local kickStr = (anticheatConfig["kick_message_noac"] ~= nil and anticheatConfig["kick_message_noac"] or "You must use EMAC Anti-Cheat to play on this server")
    local kickCheatStr = (anticheatConfig["kick_message_cheat"] ~= nil and anticheatConfig["kick_message_cheat"] or "Kicked by EMAC Anti-Cheat (cheat detected)")
    local kickBanStr = (anticheatConfig["kick_message_bannedcheat"] ~= nil and anticheatConfig["kick_message_bannedcheat"] or "You have been banned by EMAC Anti-Cheat")
    local kickVersionStr = (anticheatConfig["kick_message_oldversion"] ~= nil and anticheatConfig["kick_message_oldversion"] or "You need to reopen your client to update EMAC Anti-Cheat to the lastest version to play on this server")
    
    -- List all players that don't have the field last_heartbeat_time updated in the last 5 minutes (300 seconds) or have the fields cheating/banned equal to 1
    local resultQueryHeartbeat = db.storeQuery("SELECT account_id, banned, cheating, kicked, version FROM emac_anticheat WHERE (kicked = 0 AND ((TIMESTAMPADD(SECOND, -300, CURRENT_TIMESTAMP) > last_heartbeat_time) OR (version < " .. db.escapeString(minVersion) .. ") OR (cheating = 1) OR (banned = 1)))")

    if resultQueryHeartbeat ~= false then
        repeat
            local accountId = result.getNumber(resultQueryHeartbeat, "account_id")
            local banned = result.getNumber(resultQueryHeartbeat, "banned")
            local cheating = result.getNumber(resultQueryHeartbeat, "cheating")
            local kicked = result.getNumber(resultQueryHeartbeat, "kicked")
            local version = result.getString(resultQueryHeartbeat, "version")

            for _, charId in ipairs(getPlayersByAccountNumber(accountId)) do
                if not isPlayerWhitelisted(accountId, whitelistedPlayers) then
                    local player = Player(charId)
                    if player ~= nil then
                        local kickMessageStr = kickStr
                        if cheating == 1 then
                            kickMessageStr = kickCheatStr
                        elseif banned == 1 then
                            kickMessageStr = kickBanStr
                        elseif version ~= nil and version < minVersion then
                            kickMessageStr = kickVersionStr
                        end

                        -- Show a message to the user to know what is happening
                        player:popupFYI(kickStr)
                        player:sendTextMessage(MESSAGE_STATUS_WARNING, kickStr)
                            
                        -- Insert into table emac_anticheat_kicks
                        db.query("INSERT INTO emac_anticheat_kicks (account_id, kick_reason, version, time) VALUES (" .. accountId .. ", " .. db.escapeString(kickMessageStr) .. ", " .. db.escapeString(version) .. ", CURRENT_TIMESTAMP)")
                    
                        -- Update kicked field to 1
                        db.query("UPDATE emac_anticheat SET kicked = 1 WHERE account_id = " .. accountId)

                        addEvent(disconnectPlayer, 2500, charId)
                    end
                end
            end
        until not result.next(resultQueryHeartbeat)
        
        result.free(resultQueryHeartbeat)
    end
end

local function heartbeat()
    local anticheatConfig = getAnticheatConfig()

    if anticheatConfig ~= nil then
        local lastHeartbeatTime = (anticheatConfig["last_heartbeat_time"] ~= nil and anticheatConfig["last_heartbeat_time"] or "0")
        local enableKick = (anticheatConfig["enable_kick"] ~= nil and anticheatConfig["enable_kick"] or "0")
        local minVersion = (anticheatConfig["min_version"] ~= nil and anticheatConfig["min_version"] or "1.0.0")

        -- Only start players scan if the enable_kick field is set to 1
        if enableKick == "1" then
            local currentTimestamp = getCurrentTimestamp()
            local isBackendExecuting = ((currentTimestamp - tonumber(lastHeartbeatTime)) < 120)

            -- Only start players scan if we are sure that the anticheat backend is running
            if isBackendExecuting ~= false then
                checkPlayers(anticheatConfig, minVersion)
            end
        end
    end

    -- Execute again the heartbeat scan after 25 seconds
    addEvent(heartbeat, 25000)
    
    return true
end

function onStartup()
    print("> EMAC Anti-cheat Loaded!")

    -- Truncate anti cheat tables
    db.query("TRUNCATE TABLE emac_anticheat")
    db.query("TRUNCATE TABLE emac_anticheat_kicks")

    -- Execute heartbeat function after 5 seconds
    addEvent(heartbeat, 5000)
end

Edit the following files to load the scripts we have created.

/data/creaturescripts/creaturescripts.xml

<!-- EMAC Anti-Cheat  -->
<event type="login" name="emac_login" script="../../emac_anti_cheat/login.lua" />
<event type="logout" name="emac_logout" script="../../emac_anti_cheat/logout.lua" />

/data/globalevents/globalevents.xml

<!-- EMAC Anti-Cheat -->
<globalevent type="startup" name="emac_startup" script="../../emac_anti_cheat/startup.lua" />

Proteja seu jogo já!

Para fazer seu orçamento personalizado e exclusivo ou tirar dúvidas com nossa equipe, entre em contato conosco!

Contato

© EMAC LAB SOFTWARE LTDA (CNPJ: 25.526.555/0001-00) - 2024