943 lines
34 KiB
Lua
943 lines
34 KiB
Lua
-- cheat_detection: A minetest addon that will check/target/ban players for potential abnormal behavior caused commonly by hacked clients
|
|
-- Copyright (C) 2020 Genshin <emperor_genshin@hotmail.com>
|
|
--
|
|
-- This program is free software: you can redistribute it and/or modify
|
|
-- it under the terms of the GNU Affero General Public License as
|
|
-- published by the Free Software Foundation, either version 3 of the
|
|
-- License, or (at your option) any later version.
|
|
--
|
|
-- This program is distributed in the hope that it will be useful,
|
|
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
-- GNU Affero General Public License for more details.
|
|
--
|
|
-- You should have received a copy of the GNU Affero General Public License
|
|
-- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
--TODO: Test further to find additional false positives
|
|
|
|
local enable_automod = minetest.settings:get('enable_cheat_detection_automod') or false
|
|
local automod_type = minetest.settings:get('cheat_detection_automod_type') or "ban"
|
|
local automod_reason = minetest.settings:get('cheat_detection_automod_reason') or "Excessive Cheating Attempts"
|
|
local cheat_detection_step = tonumber(minetest.settings:get("cheat_detection_step")) or 0
|
|
local server_step = tonumber(minetest.settings:get("dedicated_server_step")) or 0.1
|
|
local patience_meter = 3
|
|
local cheat_patience_meter = 3
|
|
local node_under_height = 0.7
|
|
local server_host = minetest.settings:get("name") or ""
|
|
local detection_list = {}
|
|
local debug_hud_list = {}
|
|
|
|
if enable_automod and type(enable_automod) == "string" then
|
|
if enable_automod == "true" then
|
|
enable_automod = true
|
|
elseif enable_automod == "false" then
|
|
enable_automod = false
|
|
end
|
|
end
|
|
|
|
|
|
local function get_velocity_as_whole_interger(player, dir)
|
|
local result = nil
|
|
local velocity = player:get_player_velocity()
|
|
local vel_x, vel_z = nil
|
|
if dir == "horizontal" then
|
|
local speed = nil
|
|
vel_x = math.floor(velocity.x)
|
|
vel_z = math.floor(velocity.z)
|
|
if vel_x < 0 and vel_z >= 0 then
|
|
vel_x = math.abs(vel_x)
|
|
elseif vel_z < 0 and vel_x >= 0 then
|
|
vel_z = math.abs(vel_x)
|
|
end
|
|
if vel_x > vel_z then
|
|
speed = math.abs(vel_x)
|
|
elseif vel_z > vel_x then
|
|
speed = math.abs(vel_z)
|
|
elseif vel_x == vel_z then
|
|
speed = math.abs(vel_x or vel_z)
|
|
end
|
|
result = speed
|
|
elseif dir == "vertical" then
|
|
result = velocity.y
|
|
end
|
|
return result
|
|
end
|
|
|
|
|
|
--Patch for unknown block detection skipping, we really just need to get useful properties only anyway
|
|
local function verify_node(node)
|
|
local def = minetest.registered_nodes[node.name]
|
|
--Is it a undefined block? if so generate some properties for it
|
|
if def == nil then
|
|
def = {walkable = true, drawtype = "normal"}
|
|
end
|
|
return def
|
|
end
|
|
|
|
|
|
local function add_tracker(player)
|
|
local name = player:get_player_name()
|
|
if not detection_list[name] then
|
|
detection_list[name] = {
|
|
suspicion = "None",
|
|
prev_pos = {x = 0, y = 0, z = 0},
|
|
prev_velocity = {x = 0, y = 0, z = 0},
|
|
strikes = 0,
|
|
patience_cooldown = 2,
|
|
logged_in = true,
|
|
logged_in_cooldown = 4,
|
|
li_cd_full_time = 0,
|
|
automod_triggers = 0,
|
|
instant_punch_time = 0,
|
|
anticheat_callout_time = 0,
|
|
unhold_sneak_time = 0,
|
|
liquid_walk_time = 0,
|
|
flight_time = 0,
|
|
time_resetted = false,
|
|
falling_stops_time = 0,
|
|
node_clipping_time = 0,
|
|
flying = false,
|
|
alert_sent = false,
|
|
punched = false,
|
|
falling = false,
|
|
killaura = false,
|
|
fast_dig = false,
|
|
instant_break = false,
|
|
abnormal_range = false,
|
|
killaura_check = false,
|
|
}
|
|
end
|
|
end
|
|
|
|
|
|
local function update_tracker_info(player, list)
|
|
local name = player:get_player_name()
|
|
if detection_list[name] then
|
|
detection_list[name] = list
|
|
end
|
|
end
|
|
|
|
|
|
local function remove_tracker(player)
|
|
local name = player:get_player_name()
|
|
if detection_list[name] then
|
|
detection_list[name] = nil
|
|
end
|
|
end
|
|
|
|
|
|
local function get_tracker(player)
|
|
local name = player:get_player_name()
|
|
local result = nil
|
|
if detection_list[name] then
|
|
result = detection_list[name]
|
|
end
|
|
return result
|
|
end
|
|
|
|
|
|
local function cast_ray_under_player(player, range, objects, liquids)
|
|
local pos = player:get_pos()
|
|
objects = objects or false
|
|
liquids = liquids or false
|
|
|
|
--Raycast stuff.
|
|
local ray_start = vector.add({x = pos.x, y = pos.y - 1, z = pos.z}, {x=0, y=0, z=0})
|
|
local ray_modif = vector.multiply({x = 0, y = -0.9, z = 0}, range) --point ray down
|
|
local ray_end = vector.add(ray_start, ray_modif)
|
|
local ray = minetest.raycast(ray_start, ray_end, objects, liquids)
|
|
local object = ray:next()
|
|
|
|
--Skip player's collision
|
|
if object and object.type == "object" and object.ref == player then
|
|
object = ray:next()
|
|
end
|
|
|
|
return object
|
|
end
|
|
|
|
|
|
local function cast_ray_under_pos(pos, range, objects, liquids)
|
|
objects = objects or false
|
|
liquids = liquids or false
|
|
|
|
--Raycast stuff.
|
|
local ray_start = vector.add({x = pos.x, y = pos.y - 0.1, z = pos.z}, {x=0, y=0, z=0})
|
|
local ray_modif = vector.multiply({x = 0, y = -0.9, z = 0}, range) --point ray down
|
|
local ray_end = vector.add(ray_start, ray_modif)
|
|
local ray = minetest.raycast(ray_start, ray_end, objects, liquids)
|
|
local object = ray:next()
|
|
--
|
|
|
|
return object
|
|
end
|
|
|
|
|
|
|
|
--check if player has a obstacle underneath him (Returns a boolean)
|
|
local function check_obstacle_found_under(player, range)
|
|
local result = false
|
|
local object = cast_ray_under_player(player, range, true, true)
|
|
if object and object.type then
|
|
result = true
|
|
end
|
|
return result
|
|
end
|
|
|
|
|
|
--check if player has a obstacle underneath him and get their properties (Returns a table of properties, if failed returns nil)
|
|
local function get_obstacle_found_under(player, range)
|
|
local result = nil
|
|
local object = cast_ray_under_player(player, range, true, true)
|
|
if object and object.type then
|
|
if object.type == "node" then
|
|
local node = minetest.get_node(object.under)
|
|
--We need to make sure this raycast does not grab air as it's final target
|
|
if node then
|
|
local def = verify_node(node)
|
|
result = {name = node.name, type = "node", def = def}
|
|
end
|
|
elseif object.type == "object" and object.ref and not(object.ref:is_player() or object.ref == player) then
|
|
local entity = object.ref:get_luaentity()
|
|
result = {name = entity.name, type = "entity", def = entity}
|
|
end
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
|
|
--check if position has a node underneath it and get their properties (Returns a table of properties, if failed returns nil)
|
|
local function get_node_under_ray(pos, range)
|
|
local result = nil
|
|
local object = cast_ray_under_pos(pos, range, false, true)
|
|
if object and object.type and object.type == "node" then
|
|
local node = minetest.get_node(object.under)
|
|
--We need to make sure this raycast does not grab air as it's final target
|
|
if node then
|
|
local def = verify_node(node)
|
|
result = def
|
|
end
|
|
end
|
|
return result
|
|
end
|
|
|
|
|
|
--needed for flight check and jesus walk checks to prevent false positives by entity collision
|
|
local function check_if_entity_under(pos)
|
|
local entities = minetest.get_objects_inside_radius({x = pos.x, y = pos.y, z = pos.z}, 1)
|
|
local result = false
|
|
--look for physical objects only (TODO: convert method to raycast)
|
|
for _,entity in pairs(entities) do
|
|
if not entity:is_player() and entity:get_luaentity().physical == true then
|
|
result = true
|
|
break
|
|
end
|
|
end
|
|
return result
|
|
end
|
|
|
|
|
|
local function check_player_is_inside_nodes(player)
|
|
local pos = player:get_pos()
|
|
local node_top = minetest.get_node({x = pos.x, y = pos.y + 1, z = pos.z})
|
|
local node_bottom = minetest.get_node(pos)
|
|
local result = false
|
|
|
|
if node_top and node_bottom then
|
|
node_top = minetest.registered_nodes[node_top.name]
|
|
node_bottom = minetest.registered_nodes[node_bottom.name]
|
|
if node_top and node_top.walkable and node_bottom and node_bottom.walkable then
|
|
result = true
|
|
end
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
|
|
local function check_player_is_swimming(player)
|
|
local pos = player:get_pos()
|
|
local node_top = minetest.get_node({x = pos.x, y = pos.y + 1, z = pos.z})
|
|
local node_bottom = minetest.get_node(pos)
|
|
local result = false
|
|
|
|
if node_top and node_bottom then
|
|
node_top = minetest.registered_nodes[node_top.name]
|
|
node_bottom = minetest.registered_nodes[node_bottom.name]
|
|
if type(node_top) == "table"
|
|
and type(node_bottom) == "table"
|
|
and (node_top.drawtype == "liquid"
|
|
or node_top.drawtype == "flowingliquid"
|
|
or node_bottom.drawtype == "liquid"
|
|
or node_bottom.drawtype == "flowingliquid") then
|
|
result = true
|
|
end
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
|
|
local function is_solid_node_under(pos, max_height)
|
|
local result = false
|
|
local y_steps = 0
|
|
local found = false
|
|
while max_height > y_steps do
|
|
local node = minetest.get_node({x = pos.x, y = pos.y - y_steps, z = pos.z})
|
|
if node and minetest.registered_nodes[node.name] and minetest.registered_nodes[node.name].drawtype ~= "airlike" and minetest.registered_nodes[node.name].walkable == true then
|
|
found = true
|
|
result = true
|
|
elseif not minetest.registered_nodes[node.name] then --unknown block
|
|
found = true
|
|
result = true
|
|
end
|
|
if found then
|
|
print(node.name)
|
|
break
|
|
end
|
|
y_steps = y_steps + 1
|
|
end
|
|
return result
|
|
end
|
|
|
|
|
|
|
|
--Check surroundings for nodes in a 3x3 block order (Returns specified node property value if successful, if not then it returns nil)
|
|
local function check_surrounding_for_nodes(height, pos)
|
|
local result = false
|
|
local scan_tries = 8
|
|
|
|
--TODO: false positive - unable to grab slabs, fences and banners with raycast, make standard get_node() failsafe [Sneak Key] *facepalm...
|
|
|
|
--Only scan for nearby nodes by 3x3 blocks
|
|
while scan_tries > 0 do
|
|
local new_pos = nil
|
|
local node = nil
|
|
|
|
if scan_tries == 8 then
|
|
new_pos = {x = pos.x, y = pos.y + height, z = pos.z - 1}
|
|
node = is_solid_node_under(new_pos, 4)
|
|
elseif scan_tries == 7 then
|
|
new_pos = {x = pos.x - 1, y = pos.y + height, z = pos.z - 1}
|
|
node = is_solid_node_under(new_pos, 4)
|
|
elseif scan_tries == 6 then
|
|
new_pos = {x = pos.x + 1, y = pos.y + height, z = pos.z + 1}
|
|
node = is_solid_node_under(new_pos, 4)
|
|
elseif scan_tries == 5 then
|
|
new_pos = {x = pos.x - 1, y = pos.y + height, z = pos.z}
|
|
node = is_solid_node_under(new_pos, 4)
|
|
elseif scan_tries == 4 then
|
|
new_pos = {x = pos.x - 1, y = pos.y + height, z = pos.z + 1}
|
|
node = is_solid_node_under(new_pos, 4)
|
|
elseif scan_tries == 3 then
|
|
new_pos = {x = pos.x + 1, y = pos.y + height, z = pos.z - 1}
|
|
node = is_solid_node_under(new_pos, 4)
|
|
elseif scan_tries == 2 then
|
|
new_pos = {x = pos.x + 1, y = pos.y + height, z = pos.z}
|
|
node = is_solid_node_under(new_pos, 4)
|
|
elseif scan_tries == 1 then
|
|
new_pos = {x = pos.x, y = pos.y + height, z = pos.z + 1}
|
|
node = is_solid_node_under(new_pos, 4)
|
|
end
|
|
|
|
print(tostring(scan_tries)..") "..tostring(node))
|
|
|
|
if node == true then
|
|
result = node
|
|
break
|
|
end
|
|
|
|
scan_tries = scan_tries - 1
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
|
|
--Alert staff if goon is pulling hacks out of his own ass
|
|
local function send_alert_to_serverstaff(suspect, suspicion)
|
|
local players = minetest.get_connected_players()
|
|
for _,player in pairs(players) do
|
|
local name = player:get_player_name()
|
|
local info = get_tracker(player)
|
|
local is_staff = minetest.check_player_privs(name, {ban=true})
|
|
|
|
--Do not spam these alerts more than once per accusation since staff can get annoyed by accusation spam
|
|
if is_staff == true then
|
|
minetest.chat_send_player(name, minetest.colorize("#ffbd14" ,"*** "..os.date("%X")..":[CHEAT DETECTION]: Player ")..minetest.colorize("#FFFFFF", tostring(suspect))..minetest.colorize("#ffbd14" ," may be performing ")..minetest.colorize("#FF0004", tostring(suspicion))..minetest.colorize("#ffbd14" ," hacks!"))
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
local function check_if_forced_flying(player, info, pos, velocity, avg_rtt)
|
|
local result = false
|
|
|
|
--Skip flight check if punched or logged in
|
|
if info.logged_in == true then
|
|
return result
|
|
elseif info.punched == true then
|
|
return result
|
|
end
|
|
|
|
local name = player:get_player_name()
|
|
local can_fly = minetest.check_player_privs(name, {fly=true})
|
|
local min_speed = tonumber(minetest.settings:get("movement_speed_fast")) or 20
|
|
local min_jump = tonumber(minetest.settings:get("movement_speed_jump")) or 6.5
|
|
local speed_mod = tonumber(player:get_physics_override().speed)
|
|
local node_under = is_solid_node_under(pos, 1)
|
|
local sneak_hold = player:get_player_control().sneak or false
|
|
local object_under = check_obstacle_found_under(player, 1)
|
|
local suspicious = false
|
|
local delay = tonumber(patience_meter + avg_rtt)
|
|
local node_type = ""
|
|
|
|
min_speed = math.floor(speed_mod * min_speed)
|
|
|
|
--Reset sneak unhold time if player is sneak glitching
|
|
if sneak_hold == true then
|
|
info.unhold_sneak_time = 0
|
|
end
|
|
|
|
-- print(node_under, object_under, sneak_hold)
|
|
|
|
if info.flying == true then
|
|
suspicious = true
|
|
end
|
|
|
|
--check if they are standing still while hovering in the air
|
|
if node_under == false and object_under == false and can_fly == false and pos.y == info.prev_pos.y and velocity.y == 0 and sneak_hold == false and info.flying == false then
|
|
|
|
local was_falling = info.falling or false
|
|
|
|
minetest.log("action", "[CHEAT DETECTION]: Player "..name.." triggered the Hover Check.")
|
|
object_under = check_if_entity_under(pos)
|
|
|
|
--Prevent/skip false positive to trigger by unloaded block lag when falling too fast or when a object is underneath or if he/she just had loggen in to spare them from a aggressive detection
|
|
if was_falling == true or object_under == true then
|
|
info.flight_time = 0
|
|
info.falling_stops_time = 0
|
|
|
|
if was_falling == true then
|
|
info.falling = false
|
|
minetest.log("action", "[CHEAT DETECTION]: Player "..name.." was falling down but was halted by unloaded blocks, No suspicious activity found.")
|
|
elseif object_under == true then
|
|
minetest.log("action", "[CHEAT DETECTION]: Player "..name.." is standing on a entity obstacle, No suspicious activity found.")
|
|
end
|
|
return result
|
|
end
|
|
|
|
local nodes_around = check_surrounding_for_nodes(2, pos)
|
|
|
|
if nodes_around == false then
|
|
info.unhold_sneak_time = info.unhold_sneak_time + 1
|
|
end
|
|
|
|
--print("Sneak Unhold Time: "..tostring(info.unhold_sneak_time))
|
|
|
|
--Get triggered if the player has been caught constantly hovering up in the air
|
|
if info.unhold_sneak_time >= delay then
|
|
minetest.log("warning", "[CHEAT DETECTION]: Player "..name.." is hovering, Server has marked this as suspicious activity!")
|
|
suspicious = true
|
|
end
|
|
|
|
--Check if player is indeed flying
|
|
elseif node_under == false and object_under == false and can_fly == false and velocity.y > min_jump and info.flying == false then
|
|
|
|
if info.flying == true then
|
|
return true
|
|
end
|
|
|
|
minetest.log("action", "[CHEAT DETECTION]: Player "..name.." triggered the Flight Check.")
|
|
|
|
--Reset Falling Stops Time due to Flight check
|
|
info.falling_stops_time = 0
|
|
info.unhold_sneak_time = 0
|
|
info.falling = false
|
|
|
|
--Check for vertical velocity change to determine if the player is actually flying
|
|
local new_velocity = player:get_player_velocity()
|
|
|
|
--Get triggered if the player has been constantly climbing up for far too long at a very steady pace
|
|
if info.flight_time >= delay then
|
|
minetest.log("warning", "[CHEAT DETECTION]: Player "..name.." is continuously increasing vertical velocity too many times. Server has marked this as suspicious activity!")
|
|
suspicious = true
|
|
|
|
--If bastard is constantly acsending steadily at max speed, then he's flying like a little twat. add flight time
|
|
elseif new_velocity.y == min_speed then
|
|
info.flight_time = info.flight_time + 1
|
|
--If a player suddently begins dropping vertical velocity, then they might be falling
|
|
elseif new_velocity.y < info.prev_velocity.y then
|
|
minetest.log("action", "[CHEAT DETECTION]: Player "..name.." seem to be dropping vertical velocity due to falling. No suspicious activity found.")
|
|
|
|
--If a player is not flying, they can't be able to keep the same ammount of previous vertical velocity or constantly increasing their vertical velocity, if so they are really flying high
|
|
elseif new_velocity.y >= info.prev_velocity.y and new_velocity.y < min_speed then
|
|
info.prev_velocity = new_velocity
|
|
info.flight_time = info.flight_time + 1
|
|
end
|
|
|
|
--print("Flight Time: "..tostring(info.flight_time))
|
|
|
|
|
|
--Check if player is falling, or flying (false positives found)
|
|
elseif node_under == false and object_under == false and can_fly == false and velocity.y < -min_jump and info.flying == false then
|
|
|
|
if info.flying == true then
|
|
return true
|
|
end
|
|
|
|
minetest.log("action", "[CHEAT DETECTION]: Player "..name.." triggered the Fall Check.")
|
|
|
|
--Reset Flight Time due to Falling check
|
|
info.flight_time = 0
|
|
info.unhold_sneak_time = 0
|
|
|
|
--Check for vertical velocity change to determine if the player is actually flying
|
|
local new_velocity = player:get_player_velocity()
|
|
|
|
--print(new_velocity.y, info.prev_velocity.y)
|
|
|
|
--If still falling, then stop this...
|
|
if info.falling == true then
|
|
info.prev_velocity = new_velocity
|
|
info.falling_stops_time = 0
|
|
return result
|
|
|
|
--Get triggered if the player has been constantly stopping from falling for far too long at a very steady pace
|
|
elseif info.falling_stops_time >= delay then
|
|
info.prev_velocity = new_velocity
|
|
minetest.log("warning", "[CHEAT DETECTION]: Player "..name.." is continuously stopping from falling down too many times. Server has marked this as suspicious activity!")
|
|
suspicious = true
|
|
|
|
--If bastard is constantly decsending steadily at max speed, then he's flying like a little twat. add falling stop time
|
|
elseif new_velocity.y == -min_speed then
|
|
info.prev_velocity = new_velocity
|
|
info.falling_stops_time = info.falling_stops_time + 1
|
|
|
|
|
|
|
|
--if falling down, reset timer
|
|
elseif new_velocity.y < info.prev_velocity.y and new_velocity.y > -min_speed then
|
|
info.prev_velocity = new_velocity
|
|
info.falling_stops_time = 0
|
|
|
|
--If a player suddently begins dropping vertical velocity above max speed, then they are indeed falling
|
|
elseif new_velocity.y < -min_speed then
|
|
minetest.log("action", "[CHEAT DETECTION]: Player "..name.." is confirmed falling. No suspicious activity found.")
|
|
info.prev_velocity = new_velocity
|
|
info.falling = true
|
|
is_solid_node
|
|
--If a player is falling, they can't be able to suddenly increase vertical velocity or make their velocity stay the same
|
|
elseif new_velocity.y >= info.prev_velocity.y then
|
|
minetest.log("action", "[CHEAT DETECTION]: Player "..name.." suddenly stopped falling (Could be due to unloaded blocks or lag), verifying behavior...")
|
|
info.prev_velocity = new_velocity
|
|
info.falling_stops_time = info.falling_stops_time + 1
|
|
end
|
|
|
|
--Get triggered if the player has been constantly climbing up for far too long
|
|
if info.falling_stops_time >= delay then
|
|
suspicious = true
|
|
end
|
|
|
|
--print("Fall Time: "..tostring(info.falling_stops_time))
|
|
|
|
--Just a normal player doing normal things, reset timers
|
|
elseif node_under == true or object_under == true then
|
|
suspicious = false
|
|
info.flying = false
|
|
info.falling = false
|
|
info.unhold_sneak_time = 0
|
|
info.flight_time = 0
|
|
info.falling_stops_time = 0
|
|
end
|
|
|
|
if suspicious == true then
|
|
info.flying = true
|
|
result = true
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
|
|
local function check_if_forced_noclipping(player, info, velocity, avg_rtt)
|
|
local result = false
|
|
|
|
--Skip Noclip check if punched
|
|
if info.punched == true then
|
|
return result
|
|
end
|
|
|
|
local name = player:get_player_name()
|
|
local delay = tonumber(patience_meter + avg_rtt)
|
|
local can_noclip = minetest.check_player_privs(name, {noclip=true})
|
|
local inside_nodes = check_player_is_inside_nodes(player)
|
|
|
|
--if someone is inside a solid node then they shouldn't be moving at all, if they are then it's clearly due to noclipping
|
|
if inside_nodes == true and can_noclip == false and (velocity.x > 5 or velocity.x < -5 or velocity.z > 5 or velocity.z < -5) then
|
|
info.node_clipping_time = info.node_clipping_time + 1
|
|
|
|
|
|
|
|
if info.node_clipping_time > delay then
|
|
minetest.log("warning", "[CHEAT DETECTION]: Player "..name.." is clipping through solid nodes while moving without noclip privileges. Server has marked this as suspicious activity!")
|
|
info.strikes = 3
|
|
result = true
|
|
end
|
|
|
|
else
|
|
info.node_clipping_time = 0
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
|
|
local function check_if_forced_fast(player, info)
|
|
local result = false
|
|
|
|
--Skip Jesus Walk check if punched
|
|
if info.punched == true then
|
|
return result
|
|
end
|
|
|
|
local aux_pressed = player:get_player_control().aux1
|
|
|
|
--if player is not pressing sprint key, skip this check
|
|
if aux_pressed == false then
|
|
return result
|
|
end
|
|
|
|
local name = player:get_player_name()
|
|
local current_speed = get_velocity_as_whole_interger(player, "horizontal")
|
|
local min_speed = tonumber(minetest.settings:get("movement_speed_fast")) or 20
|
|
local detection_fast_speed = nil
|
|
local can_fast = minetest.check_player_privs(name, {fast=true})
|
|
local speed_mod = tonumber(player:get_physics_override().speed)
|
|
local f = math.floor
|
|
|
|
--This is needed to determine if user is speeding, subtract 1 for fast speed accuracy
|
|
min_speed = math.floor(speed_mod * min_speed)
|
|
detection_fast_speed = math.floor(min_speed - 1)
|
|
|
|
if can_fast == false and (current_speed == min_speed or current_speed == detection_fast_speed) then
|
|
minetest.log("warning", "[CHEAT DETECTION]: Player "..name.."\'s speed went past the server\'s max speed without fast privs too many times. Server has marked this as suspicious activity!")
|
|
result = true
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
|
|
local function check_if_jesus_walking(player, info, pos, velocity, avg_rtt)
|
|
local result = false
|
|
|
|
--Skip Jesus Walk check if punched
|
|
if info.punched == true then
|
|
return result
|
|
end
|
|
|
|
local name = player:get_player_name()
|
|
local obstacle_under = get_obstacle_found_under(player, 1)
|
|
local node_under = "not found"
|
|
local swimming = check_player_is_swimming(player)
|
|
local sneak_hold = player:get_player_control().sneak or false
|
|
local delay = tonumber(patience_meter + avg_rtt)
|
|
|
|
if obstacle_under and obstacle_under.type == "node" and obstacle_under.def.drawtype then
|
|
node_under = obstacle_under.def.drawtype
|
|
end
|
|
|
|
--If someone is able to stand still on a liquid type node, then they are clearly walking on water
|
|
if swimming == false and (node_under == "liquid" or node_under == "flowingliquid") and pos.y == info.prev_pos.y and velocity.y == 0 and sneak_hold == false then
|
|
local object_under = check_if_entity_under(pos)
|
|
if object_under == false then
|
|
info.node_clipping_time = info.node_clipping_time + 1
|
|
info.liquid_walk_time = info.liquid_walk_time + 1
|
|
print("Liquid Walk Time: "..tostring(info.liquid_walk_time))
|
|
end
|
|
else
|
|
info.liquid_walk_time = 0
|
|
end
|
|
|
|
--Get triggered if the player has been constantly walking on water for far too long
|
|
if info.liquid_walk_time >= delay then
|
|
minetest.log("warning", "[CHEAT DETECTION]: Player "..name.." is litteraly standing on water, Server has marked this as suspicious activity!")
|
|
info.strikes = 3
|
|
result = true
|
|
end
|
|
|
|
|
|
return result
|
|
end
|
|
|
|
|
|
local function verify_suspicious_behavior(info, suspicion, avg_rtt)
|
|
local timer_step = tonumber(cheat_detection_step + avg_rtt)
|
|
minetest.after(timer_step, function()
|
|
info.strikes = info.strikes + 1
|
|
end)
|
|
--Don't go past 3 strikes
|
|
if info.strikes >= 3 then
|
|
info.suspicion = suspicion
|
|
info.strikes = 3
|
|
info.patience_cooldown = 2
|
|
end
|
|
end
|
|
|
|
|
|
--Enable Server Anti-Cheat System if Player Manager is Present, keep an eye out for suspicious activity
|
|
local function handle_cheat_detection()
|
|
local players = minetest.get_connected_players()
|
|
|
|
for _,player in pairs(players) do
|
|
local pname = player:get_player_name()
|
|
local pos = player:get_pos()
|
|
local velocity = player:get_player_velocity()
|
|
local is_superuser = minetest.check_player_privs(pname, {server=true})
|
|
local pinfo = minetest.get_player_information(pname) --this is important
|
|
local info = get_tracker(player)
|
|
local smite = false
|
|
|
|
if pinfo and info and pname ~= server_host and is_superuser == false then
|
|
|
|
--If detection step is too fast, slow down the cooldown timer so some detection algorythms are less aggressive when a player reconnects to the server after jumping
|
|
if info.logged_in == true and info.logged_in_cooldown > 0 and cheat_detection_step < 0.3 and server_step <= 0.1 then
|
|
info.li_cd_full_time = info.li_cd_full_time + 1
|
|
|
|
if info.li_cd_full_time > 15 then
|
|
info.logged_in_cooldown = info.logged_in_cooldown - 1
|
|
info.li_cd_full_time = 0
|
|
end
|
|
|
|
if info.logged_in_cooldown < 1 then
|
|
info.logged_in = false
|
|
info.logged_in_cooldown = nil
|
|
end
|
|
|
|
--If detection step is at a balanced rate, then normally count down without any further delay
|
|
elseif info.logged_in == true and info.logged_in_cooldown > 0 and (cheat_detection_step >= 0.3 or server_step >= 0.1) then
|
|
info.logged_in_cooldown = info.logged_in_cooldown - 1
|
|
|
|
if info.logged_in_cooldown < 1 then
|
|
info.logged_in = false
|
|
info.logged_in_cooldown = nil
|
|
end
|
|
end
|
|
|
|
--Scan players every single average round trip time for accuracy
|
|
minetest.after(pinfo.avg_rtt, function()
|
|
|
|
info.time_resetted = false
|
|
|
|
--Only do debug for dummy test hacker client to see what's up whith him
|
|
if pname == "haxor" then
|
|
update_debug_hud(player, false)
|
|
end
|
|
|
|
local is_jesus_walking = check_if_jesus_walking(player, info, pos, velocity, pinfo.avg_rtt)
|
|
local is_force_noclipping = check_if_forced_noclipping(player, info, velocity, pinfo.avg_rtt)
|
|
local is_force_fast = check_if_forced_fast(player, info)
|
|
local is_force_flying = check_if_forced_flying(player, info, pos, velocity, pinfo.avg_rtt)
|
|
|
|
--Hmm, I sense suspicious activity in this sector... [Killaura]
|
|
if info.killaura == true then
|
|
verify_suspicious_behavior(info, "Killaura", pinfo.avg_rtt)
|
|
info.killaura_check = false
|
|
info.killaura = false
|
|
|
|
--Hmm, I sense suspicious activity in this sector... [Unlimited Range]
|
|
elseif info.abnormal_range == true then
|
|
verify_suspicious_behavior(info, "Unlimited Range", pinfo.avg_rtt)
|
|
info.abnormal_range = false
|
|
|
|
--Hmm, I sense suspicious activity in this sector... [Instant Break]
|
|
elseif info.instant_break == true then
|
|
verify_suspicious_behavior(info, "Instant Node Break", pinfo.avg_rtt)
|
|
info.instant_break = false
|
|
|
|
--Hmm, I sense suspicious activity in this sector... [Fast Dig]
|
|
elseif info.fast_dig == true then
|
|
verify_suspicious_behavior(info, "Fast Dig", pinfo.avg_rtt)
|
|
info.fast_dig = false
|
|
|
|
--Hmm, I sense suspicious activity in this sector... [Walk on Water Hacks]
|
|
elseif is_jesus_walking == true then
|
|
verify_suspicious_behavior(info, "Jesus Walk", pinfo.avg_rtt)
|
|
|
|
--Hmm, I sense suspicious activity in this sector... [Noclip Hacks]
|
|
elseif is_force_noclipping == true then
|
|
verify_suspicious_behavior(info, "Forced Noclip", pinfo.avg_rtt)
|
|
|
|
--Hmm, I sense suspicious activity in this sector... [Fast Hacks]
|
|
elseif is_force_fast == true then
|
|
verify_suspicious_behavior(info, "Forced Fast", pinfo.avg_rtt)
|
|
|
|
--Hmm, I sense suspicious activity in this sector... [Fly Hacks]
|
|
elseif is_force_flying == true then
|
|
verify_suspicious_behavior(info, "Forced Fly", pinfo.avg_rtt)
|
|
|
|
--So far so good, nothing to see here (Reset timers and strikes)
|
|
else
|
|
info.patience_cooldown = info.patience_cooldown - 1
|
|
if info.patience_cooldown < 1 then
|
|
info.time_resetted = true
|
|
info.automod_triggers = 0
|
|
info.patience_cooldown = 2
|
|
end
|
|
end
|
|
|
|
--Send Warning after 3 strikes, then reset. Following up with patience meter to drop
|
|
if info.strikes == 3 and info.suspicion ~= "None" then
|
|
send_alert_to_serverstaff(pname, info.suspicion)
|
|
|
|
if info.alert_sent == false then
|
|
minetest.log("warning", "[CHEAT DETECTION]: Player "..pname.." have been flagged by the Server for possibly using a Hacked Client!")
|
|
minetest.chat_send_player(pname, minetest.colorize("#ffbd14" ,"*** "..os.date("%X")..":[CHEAT DETECTION]: You have been flagged by the Server for possibly using a Hacked Client. Our server staff have been alerted!"))
|
|
info.alert_sent = true
|
|
end
|
|
|
|
if enable_automod == true then
|
|
local delay = tonumber(patience_meter + pinfo.avg_rtt)
|
|
info.automod_triggers = info.automod_triggers + 1
|
|
|
|
if info.automod_triggers >= delay then
|
|
smite = true
|
|
end
|
|
end
|
|
info.strikes = 0
|
|
elseif info.strikes == 3 and info.suspicion == "None" then
|
|
info.strikes = 0
|
|
end
|
|
|
|
--I ran out of patience, please for the love of god Let me BAN this sneaky little twat NOW!!!
|
|
if smite and enable_automod == true then
|
|
info.automod_triggers = 0
|
|
|
|
if automod_type == "kick" then
|
|
|
|
minetest.log("action", "[CHEAT DETECTION]: Server has kicked "..pname.." for performing continuous abnormal behaviors while in-game.")
|
|
minetest.kick_player(pname, "Cheat Detection: "..automod_reason)
|
|
|
|
elseif automod_type == "ban" then
|
|
|
|
minetest.log("action", "[CHEAT DETECTION]: Server has banned "..pname.." for performing continuous abnormal behaviors while in-game.")
|
|
minetest.ban_player(pname)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
info.punched = false
|
|
info.prev_velocity = velocity
|
|
info.prev_pos = pos
|
|
update_tracker_info(player, info)
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
minetest.after(cheat_detection_step, function()
|
|
handle_cheat_detection()
|
|
end)
|
|
end
|
|
|
|
|
|
|
|
minetest.register_on_mods_loaded(function()
|
|
handle_cheat_detection()
|
|
end)
|
|
|
|
minetest.register_on_leaveplayer(function(player)
|
|
remove_tracker(player)
|
|
update_debug_hud(player, true)
|
|
end)
|
|
|
|
|
|
minetest.register_on_joinplayer(function(player)
|
|
--Add the dang tracker onto the specified player
|
|
add_tracker(player)
|
|
end)
|
|
|
|
|
|
--Additional Anti-Hackerman stuff
|
|
minetest.register_on_cheat(function(player, cheat)
|
|
local info = get_tracker(player)
|
|
|
|
--Skip shenanigain check if player is punched, this is for knockback exceptions
|
|
if info.punched == true or info.killaura_check == true then
|
|
return
|
|
end
|
|
|
|
local name = player:get_player_name()
|
|
local pinfo = minetest.get_player_information(name)
|
|
local delay = tonumber(cheat_patience_meter + pinfo.avg_rtt)
|
|
local accusation = nil
|
|
|
|
if cheat.type == "interacted_too_far" then
|
|
info.anticheat_callout_time = info.anticheat_callout_time + 1
|
|
accusation = "unlimitedrange"
|
|
elseif cheat.type == "dug_unbreakable" then
|
|
info.anticheat_callout_time = info.anticheat_callout_time + 1
|
|
accusation = "instantbreak"
|
|
elseif cheat.type == "dug_too_fast" then
|
|
info.anticheat_callout_time = info.anticheat_callout_time + 1
|
|
accusation = "fastdig"
|
|
end
|
|
|
|
--Hackers normally do things very quickly
|
|
if accusation and info.anticheat_callout_time > delay then
|
|
if accusation == "instantbreak" then
|
|
info.instant_break = true
|
|
minetest.log("warning", "[CHEAT DETECTION]: Player "..name.." is trying to instantly break unbreakable nodes too many times. Server has marked this as suspicious activity!")
|
|
elseif accusation == "fastdig" then
|
|
info.fast_dig = true
|
|
minetest.log("warning", "[CHEAT DETECTION]: Player "..name.." is trying to instantly dig nodes too many times. Server has marked this as suspicious activity!")
|
|
elseif accusation == "unlimitedrange" then
|
|
info.abnormal_range = true
|
|
minetest.log("warning", "[CHEAT DETECTION]: Player "..name.." is trying to do long range interactions too many times. Server has marked this as suspicious activity!")
|
|
end
|
|
|
|
info.anticheat_callout_time = 0
|
|
end
|
|
|
|
end)
|
|
|
|
minetest.register_on_punchplayer(function(player, hitter, punchtime)
|
|
local info = get_tracker(player)
|
|
local name = nil
|
|
local info2 = nil
|
|
local pinfo = nil
|
|
local delay = nil
|
|
|
|
--Knockback Exceptions
|
|
if info then
|
|
info.punched = true
|
|
end
|
|
|
|
if hitter:is_player() then
|
|
name = hitter:get_player_name()
|
|
info2 = get_tracker(hitter)
|
|
pinfo = minetest.get_player_information(name)
|
|
delay = tonumber(patience_meter + pinfo.avg_rtt - 1)
|
|
end
|
|
|
|
--killaura detection, there is absolutely no flipping way any normal human being can land a 0 second punch repeatedly (No Mercy for these scrubs)
|
|
if info2 and punchtime <= 0 then
|
|
info2.killaura_check = true
|
|
info2.instant_punch_time = info2.instant_punch_time + 1
|
|
--Confirm killaura behavior if player instantly punched a player too many times
|
|
if info2.instant_punch_time > delay then
|
|
info2.killaura = true
|
|
minetest.log("warning", "[CHEAT DETECTION]: Player "..name.." is instantly punching players too many times. Server has marked this as suspicious activity!")
|
|
end
|
|
elseif info2 and punchtime > 0 then
|
|
info2.instant_punch_time = 0
|
|
end
|
|
|
|
end)
|