237 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			237 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
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)
 |