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" />