Main/resources/[carscripts]/slashtires/server/server.lua

238 lines
11 KiB
Lua
Raw Normal View History

2025-06-07 08:51:21 +02:00
local TIRE_LABLES <const> = {
[0] = "front left",
[1] = "front right",
[2] = "left middle",
[3] = "right middle",
[4] = "left rear",
[5] = "right rear",
[45] = "left middle #2",
[46] = "left middle #3",
[47] = "right middle #2",
[48] = "right middle #3",
}
---Displays a notification to the player
---@param message string
function DisplayNotification(serverId, message)
TriggerClientEvent('slashtires:displayNotification', serverId, message)
end
---Gets the label string for the spesifed tire index
---@param tireIndex integer
---@return string
local function getTireLabel(tireIndex)
return TIRE_LABLES[tireIndex] or string.format("unknown (%s)", tireIndex)
end
---Returns if the current player weapon can slash a tire
---@param playerPed integer
---@return boolean canSlash
---@return integer weaponHash
local function canCurrentWeaponSlashTires(playerPed)
local weaponHash = GetSelectedPedWeapon(playerPed)
return Config.AllowedWeapons[weaponHash] ~= nil, weaponHash
end
---Returns if the vehicle's model is blacklisted from getting slashed
---@param vehicleModel integer
---@return boolean isBlacklisted
---@return string blacklistReason
local function isVehicleModelBlacklisted(vehicleModel)
local blacklistReason = Config.VehicleBlacklist[vehicleModel]
-- NOTE: It is not possible to get the vehicle class server side, so emergency vehicles are sadly not blocked by the server :/ (but the amount of cheaters trying abusing this event is probably so low that i doesn't matter)
return blacklistReason ~= nil, VEHICLE_BLACKLIST_REASONS[blacklistReason] or 'vehicle_is_blacklisted'
end
---Checks if the ped should be given the flee task from the spesifed coords
---@param ped integer
---@param coords vector3
---@return boolean canFleePed
local function canGivePedFleeTask(ped, coords)
local dist = #(coords - GetEntityCoords(ped))
if dist > Config.AIReactionDistance then
return false
end
if IsPedAPlayer(ped) then
return false
end
-- Frozen peds can't flee anyway, and they are most likley a script handled ped (for stores and robberies for example)
if IsEntityPositionFrozen(ped) then
return false
end
-- Ignore dead peds
if GetEntityHealth(ped) == 0 then
return false
end
if not IsEntityVisible(ped) then
return false
end
return true
end
---Makes the nearby peds flee from the ped
---@param coords vector3
---@param fleePed integer The ped to flee from
local function reactAndFleeNearbyPeds(peds, coords, fleePed)
for index = 1, #peds do
local ped = NetworkGetEntityFromNetworkId(peds[index])
if canGivePedFleeTask(ped, coords) then
TaskReactAndFleePed(ped, fleePed)
end
end
end
---Checks if the weapon should break
---@param weaponHash integer
---@return boolean shouldBreak
local function shouldWeaponBreak(weaponHash)
if not Config.CanWeaponsBreak then
return false
end
local chanceToBreak = Config.AllowedWeapons[weaponHash].breakChance
if not chanceToBreak then
warn(string.format("Weapon with hash %s does not have a specified chance at breaking. Please add it in under Config.AllowedWeapons or set Config.CanWeaponsBreak to false", weaponHash))
return false
end
if chanceToBreak == 0 then
return false
end
local chance = math.random(1, 1000) / 10
if chance > chanceToBreak then
return false
end
return true
end
---Checks and validations function for when a player wants to slash a tire
---@param netId integer netId of the vehicle
---@param tireIndex integer The index of the tire that was slashed
---@param reactPeds table<integer, integer> The peds that the client belives should react to tire slashing
---@param hasBurstedTire boolean If the client already has bursted the tire on the client side
local function slashTire(netId, tireIndex, reactPeds, hasBurstedTire)
local src = source
local vehicle = NetworkGetEntityFromNetworkId(netId)
if vehicle == 0 then
Log(src, 'slashtires:cheaterOrError', string.format("attempted to slash a tire of a vehicle with an invalid netId: %s", netId), { source = src, netId = netId, tireIndex = tireIndex, hasBurstedTire = hasBurstedTire })
DisplayNotification(src, GetLocalization('no_vehicle_nearby'))
return
end
local entityType = GetEntityType(vehicle)
if entityType ~= 2 then
Log(src, 'slashtires:cheaterOrError', string.format("attempted to slash a tire of a non-vehicle entity with netId: %s", netId), { source = src, netId = netId, tireIndex = tireIndex, hasBurstedTire = hasBurstedTire, entityType = entityType })
DisplayNotification(src, GetLocalization('no_vehicle_nearby'))
return
end
local tireIndexType = type(tireIndex)
if tireIndexType ~= 'number' or not TIRE_LABLES[tireIndex] then
Log(src, 'slashtires:cheaterOrError', string.format("attempted to slash an unknown tire with index: %s (lua type: %s)", tireIndex, tireIndexType), { source = src, netId = netId, tireIndex = tireIndex, hasBurstedTire = hasBurstedTire, tireIndexType = tireIndexType })
DisplayNotification(src, GetLocalization('no_tire_nearby'))
return
end
local playerPed = GetPlayerPed(src)
local playerCoords = GetEntityCoords(playerPed)
local vehicleCoords = GetEntityCoords(vehicle)
local distance = #(vehicleCoords - playerCoords)
if distance > 15.0 then
Log(src, 'slashtires:cheater', string.format("attempted to slash the %s tire of a vehicle that was %.2f meters away", getTireLabel(tireIndex), distance), { source = src, netId = netId, tireIndex = tireIndex, hasBurstedTire = hasBurstedTire, vehicleCoords = vehicleCoords, playerCoords = playerCoords, distance = distance })
DisplayNotification(src, GetLocalization('too_far_away'))
return
end
local canPlayerSlash, reason = CanPlayerSlashTires(src, playerPed)
if not canPlayerSlash and reason then
Log(src, 'slashtires:cheaterOrError', string.format("attempted to slash the %s tire of a vehicle but was was blocked by a server bridge script (blocked reason: %s)", reason), { source = src, netId = netId, tireIndex = tireIndex, hasBurstedTire = hasBurstedTire, vehicleCoords = vehicleCoords, playerCoords = playerCoords, distance = distance, blockedReason = reason })
DisplayNotification(src, GetLocalization(reason))
return
end
if IsPedRagdoll(playerPed) then
Log(src, 'slashtires:cheaterOrError', "attempted to slash the %s tire of a vehicle but was blocked due to being in ragdoll", { source = src, netId = netId, tireIndex = tireIndex, hasBurstedTire = hasBurstedTire, vehicleCoords = vehicleCoords, playerCoords = playerCoords, distance = distance })
DisplayNotification(src, GetLocalization('in_ragdoll'))
return
end
local canWeaponSlash, weaponHash = canCurrentWeaponSlashTires(playerPed)
if not canWeaponSlash then
Log(src, 'slashtires:cheater', string.format("attempted to slash the %s tire of a vehicle but did not have a allowed weapon equipped (current player weapon hash: %s)", getTireLabel(tireIndex), weaponHash), { source = src, netId = netId, tireIndex = tireIndex, hasBurstedTire = hasBurstedTire, vehicleCoords = vehicleCoords, playerCoords = playerCoords, distance = distance, weaponHash = weaponHash })
DisplayNotification(src, GetLocalization('invalid_weapon'))
return
end
local hasItem = HasWeapon(src, weaponHash)
local weaponName = Config.AllowedWeapons[weaponHash]?.name or string.format("unknown (hash: %s)", weaponHash)
if not hasItem then
Log(src, 'slashtires:cheater', string.format("attempted to slash the %s tire of a vehicle but did not have the weapon item required (item needed: %s)", getTireLabel(tireIndex), weaponName), { source = src, netId = netId, tireIndex = tireIndex, hasBurstedTire = hasBurstedTire, vehicleCoords = vehicleCoords, playerCoords = playerCoords, distance = distance, weaponHash = weaponHash, weaponName = weaponName })
DisplayNotification(src, GetLocalization('invalid_weapon'))
return
end
local vehicleModel = GetEntityModel(vehicle)
local isBlacklisted, blacklistReason = isVehicleModelBlacklisted(vehicleModel)
if isBlacklisted then
Log(src, 'slashtires:cheater', string.format("attempted to slash the %s tire of a vehicle that was blacklisted (model: %s)", getTireLabel(tireIndex), vehicleModel), { source = src, netId = netId, tireIndex = tireIndex, hasBurstedTire = hasBurstedTire, vehicleCoords = vehicleCoords, playerCoords = playerCoords, distance = distance, weaponHash = weaponHash, weaponName = weaponName, vehicleModel = vehicleModel })
DisplayNotification(src, GetLocalization(blacklistReason))
return
end
local sendtAlert = false
if Config.Dispatch and #reactPeds > 0 then
local chance = math.random(0, 1000) / 10
if chance < Config.DispatchAlertChance then
sendtAlert = true
SendDispatchAlert(source, playerCoords, vehicle)
end
end
if Config.AIReactAndFlee then
reactAndFleeNearbyPeds(reactPeds, playerCoords, playerPed)
end
local wasWeaponRemoved = false
if shouldWeaponBreak(weaponHash) then
local success = RemoveWeapon(src, weaponHash, playerPed)
if success then
wasWeaponRemoved = true
end
elseif Config.ReducedItemDurability and Config.AllowedWeapons[weaponHash]?.durability then
local weaponBroke = ReduceDurabilityForWeapon(src, weaponHash, playerPed)
if weaponBroke then
wasWeaponRemoved = true
end
end
if wasWeaponRemoved then
DisplayNotification(source, GetLocalization('weapon_broke'))
end
local numberPlate = GetVehicleNumberPlateText(vehicle)
local data = { source = src, netId = netId, tireIndex = tireIndex, hasBurstedTire = hasBurstedTire, vehicleCoords = vehicleCoords, playerCoords = playerCoords, distance = distance, weaponHash = weaponHash, weaponName = weaponName, sendtAlert = sendtAlert, weaponRemoved = wasWeaponRemoved, vehicleModel = vehicleModel, numberPlate = numberPlate }
Log(src, 'slashtires:slashedTire', string.format("slashed the %s tire of a vehicle with plate \"%s\"", getTireLabel(tireIndex), numberPlate), data)
-- Event for other scripts to listen to
TriggerEvent('slashtires:slashedTire', data)
-- If the tire has already been bursted by the client then there isn't any need to send any events.
if hasBurstedTire then
return
end
local netOwner = NetworkGetEntityOwner(vehicle)
TriggerClientEvent('slashtires:burstTire', netOwner, netId, tireIndex)
end
RegisterServerEvent('slashtires:slashTire', slashTire)