319 lines
		
	
	
	
		
			9.6 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			319 lines
		
	
	
	
		
			9.6 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
local PLAYER_INTERACTIONS = {}
 | 
						|
local INTERACTION_THREAD_RUNNING = false
 | 
						|
local LAST_OUTLINE_ENTITY = nil
 | 
						|
 | 
						|
-- HELPER FUNCTIONS
 | 
						|
local function GetClosestPlayerInteraction(maxDistance)
 | 
						|
    return UseCache('GetClosestPlayerInteraction', function()
 | 
						|
        local playerPed = PlayerPedId()
 | 
						|
        local playerCoords = GetEntityCoords(playerPed)
 | 
						|
        
 | 
						|
        local nearest = nil
 | 
						|
        local nearestDistance = maxDistance
 | 
						|
        
 | 
						|
        for k, playerInteraction in pairs(PLAYER_INTERACTIONS) do
 | 
						|
            if playerInteraction.canInteract() then
 | 
						|
                local coords = playerInteraction.GetCoords()
 | 
						|
                
 | 
						|
                local distance = #(playerCoords - coords)
 | 
						|
                if distance < nearestDistance then
 | 
						|
                    nearest = playerInteraction
 | 
						|
                    nearestDistance = distance
 | 
						|
                end
 | 
						|
            end
 | 
						|
        end
 | 
						|
        
 | 
						|
        return nearest, nearestDistance
 | 
						|
    end, 1000)
 | 
						|
end
 | 
						|
 | 
						|
local function CleanUpDeadInteractions()
 | 
						|
    for k, interaction in pairs(PLAYER_INTERACTIONS) do
 | 
						|
        if interaction.entity and not DoesEntityExist(interaction.entity) then
 | 
						|
            interaction.Delete()
 | 
						|
        elseif GetResourceState(interaction.GetInvoker()) ~= 'started' then
 | 
						|
            interaction.Delete()
 | 
						|
        end
 | 
						|
    end
 | 
						|
end
 | 
						|
 | 
						|
local function DeadInteractionRemovalThread()
 | 
						|
    Citizen.CreateThread(function()
 | 
						|
        while true do
 | 
						|
            CleanUpDeadInteractions()
 | 
						|
            Citizen.Wait(5000)
 | 
						|
        end
 | 
						|
    end)
 | 
						|
end
 | 
						|
 | 
						|
--- MAIN
 | 
						|
local function TriggerInteractionThread()
 | 
						|
    Citizen.CreateThread(function()
 | 
						|
        Citizen.Wait(500
 | 
						|
        )
 | 
						|
        Debug('triggering interaction thread')
 | 
						|
        -- Only needs to run once for non-target systems
 | 
						|
        if INTERACTION_THREAD_RUNNING or Link.input.target.enabled then
 | 
						|
            Debug('aborted', Count(PLAYER_INTERACTIONS))
 | 
						|
            return
 | 
						|
        end
 | 
						|
        
 | 
						|
        INTERACTION_THREAD_RUNNING = true
 | 
						|
        
 | 
						|
        while Count(PLAYER_INTERACTIONS) > 0 do
 | 
						|
            local sleep = 2500
 | 
						|
            
 | 
						|
            local interaction, distance = GetClosestPlayerInteraction(5)
 | 
						|
            if interaction then
 | 
						|
                sleep = 500
 | 
						|
            end
 | 
						|
            
 | 
						|
            if interaction ~= nil and distance < interaction.interactDist then
 | 
						|
                sleep = 1
 | 
						|
                
 | 
						|
                if interaction.Handle() and interaction.entity then
 | 
						|
                    local entity = interaction.entity
 | 
						|
                    if Link.input.other.outline.enabled and LAST_OUTLINE_ENTITY ~= interaction.entity then
 | 
						|
                        if LAST_OUTLINE_ENTITY ~= nil then
 | 
						|
                            SetEntityDrawOutline(LAST_OUTLINE_ENTITY, false)
 | 
						|
                        end
 | 
						|
                        
 | 
						|
                        LAST_OUTLINE_ENTITY = entity
 | 
						|
                        
 | 
						|
                        SetEntityDrawOutline(entity, true)
 | 
						|
                        SetEntityDrawOutlineShader(1)
 | 
						|
                        local outlineColor = Link.input.other.outline.color
 | 
						|
                        SetEntityDrawOutlineColor(outlineColor.r, outlineColor.g, outlineColor.b, outlineColor.a)
 | 
						|
                    end
 | 
						|
                end
 | 
						|
            else
 | 
						|
                if LAST_OUTLINE_ENTITY ~= nil then
 | 
						|
                    SetEntityDrawOutline(LAST_OUTLINE_ENTITY, false)
 | 
						|
                    LAST_OUTLINE_ENTITY = nil
 | 
						|
                end
 | 
						|
            end
 | 
						|
            
 | 
						|
            Citizen.Wait(sleep)
 | 
						|
        end
 | 
						|
        INTERACTION_THREAD_RUNNING = false
 | 
						|
        
 | 
						|
        if Link.input.other.outline.enabled then
 | 
						|
            SetEntityDrawOutline(LAST_OUTLINE_ENTITY, false)
 | 
						|
            LAST_OUTLINE_ENTITY = nil
 | 
						|
        end
 | 
						|
    end)
 | 
						|
end
 | 
						|
 | 
						|
local function RegisterInteraction(data)
 | 
						|
    local self = {
 | 
						|
        invoker = GetInvokingResource(),
 | 
						|
        
 | 
						|
        key = GetGameTimer() .. '-' .. math.random(0, 9999999),
 | 
						|
        
 | 
						|
        entity = data.entity,
 | 
						|
        entityOffset = data.offset,
 | 
						|
        
 | 
						|
        coords = data.coords,
 | 
						|
        rotation = data.rotation or vector3(0, 0, 0),
 | 
						|
        scale = data.scale,
 | 
						|
        
 | 
						|
        message = data.message,
 | 
						|
        targetMessage = data.targetMessage,
 | 
						|
        input = data.input,
 | 
						|
        interactDist = data.interactDist or 2,
 | 
						|
        
 | 
						|
        callback = data.callback,
 | 
						|
        canInteract = data.canInteract,
 | 
						|
        
 | 
						|
        meta = data.meta,
 | 
						|
        
 | 
						|
        -- Filled later
 | 
						|
        targetEntity = nil,
 | 
						|
        targetZone = nil,
 | 
						|
        
 | 
						|
        eventHandler = nil,
 | 
						|
    }
 | 
						|
    
 | 
						|
    -- Booting / Setup
 | 
						|
    self.Boot = function()
 | 
						|
        if Link.input.target.enabled then
 | 
						|
            self.SetupTargetSystem()
 | 
						|
            
 | 
						|
            return
 | 
						|
        end
 | 
						|
        
 | 
						|
        TriggerInteractionThread()
 | 
						|
    end
 | 
						|
    
 | 
						|
    self.SetupTargetSystem = function()
 | 
						|
        local eventKey = GetCurrentResourceName() .. ':target:' .. self.key
 | 
						|
        
 | 
						|
        RegisterNetEvent(eventKey)
 | 
						|
        self.eventHandler = AddEventHandler(eventKey, function()
 | 
						|
            self.PerformSafeCallback()
 | 
						|
        end)
 | 
						|
        
 | 
						|
        if self.entity then
 | 
						|
            self.targetEntity = InputUtils.AddEntityToTargeting(self.entity, self.targetMessage, eventKey, function()
 | 
						|
                if not self then return end
 | 
						|
                return self.canInteract(self.clientReturnData)
 | 
						|
            end, self.meta, self.interactDist, self.icon)
 | 
						|
        else
 | 
						|
            self.targetZone = InputUtils.AddZoneToTargeting(self.coords, self.rotation, self.scale, self.targetMessage, eventKey, function()
 | 
						|
                if not self then return end
 | 
						|
                return self.canInteract(self.clientReturnData)
 | 
						|
            end, self.meta, self.interactDist, self.icon)
 | 
						|
        end
 | 
						|
    end
 | 
						|
    
 | 
						|
    -- Displaying and handling of the input options
 | 
						|
    self.Handle = function()
 | 
						|
        if not self then
 | 
						|
            return
 | 
						|
        end
 | 
						|
        
 | 
						|
        local cachedCanInteract = UseCache('canInteract' .. self.key, function()
 | 
						|
            return self.canInteract(self.clientReturnData)
 | 
						|
        end, 500)
 | 
						|
        
 | 
						|
        if not cachedCanInteract then
 | 
						|
            return
 | 
						|
        end
 | 
						|
        
 | 
						|
        local coords = self.GetCoords()
 | 
						|
        
 | 
						|
        local inputType = Link.input.other.inputType
 | 
						|
        if inputType == 'top-left' then
 | 
						|
            InputUtils.KeybindTip(self.message)
 | 
						|
        elseif inputType == 'help-text' then
 | 
						|
            InputUtils.DrawFloatingText(coords, self.message)
 | 
						|
        elseif inputType == '3d-text' then
 | 
						|
            InputUtils.Draw3DText(coords, self.message, 0.042)
 | 
						|
        end
 | 
						|
        
 | 
						|
        if IsControlJustReleased(0, self.input) then
 | 
						|
            if self.entity then
 | 
						|
                LAST_OUTLINE_ENTITY = nil
 | 
						|
                SetEntityDrawOutline(self.entity, false)
 | 
						|
            end
 | 
						|
            
 | 
						|
            self.PerformSafeCallback()
 | 
						|
            Citizen.Wait(500) -- Interaction debounce
 | 
						|
        end
 | 
						|
        
 | 
						|
        return true
 | 
						|
    end
 | 
						|
    
 | 
						|
    -- Perform a safe callback with error logging
 | 
						|
    self.PerformSafeCallback = function()
 | 
						|
        local success, err = pcall(self.callback, self.clientReturnData)
 | 
						|
        if not success then
 | 
						|
            Debug(
 | 
						|
                ('^1Interactable callback from {resource} has failed.'):gsub('{resource}', self.invoker),
 | 
						|
                err
 | 
						|
            )
 | 
						|
        end
 | 
						|
    end
 | 
						|
    
 | 
						|
    -- Getters
 | 
						|
    self.GetMeta = function()
 | 
						|
        return self.meta
 | 
						|
    end
 | 
						|
    
 | 
						|
    self.GetInvoker = function()
 | 
						|
        return self.invoker
 | 
						|
    end
 | 
						|
    
 | 
						|
    self.GetCoords = function()
 | 
						|
        return self.coords or GetOffsetFromEntityInWorldCoords(self.entity, self.entityOffset)
 | 
						|
    end
 | 
						|
    
 | 
						|
    self.GetEntity = function()
 | 
						|
        return self.entity
 | 
						|
    end
 | 
						|
    
 | 
						|
    -- Deleting
 | 
						|
    
 | 
						|
    self.Delete = function()
 | 
						|
        if self.eventHandler then
 | 
						|
            RemoveEventHandler(self.eventHandler)
 | 
						|
        end
 | 
						|
        
 | 
						|
        -- Targeting cleanup
 | 
						|
        if self.targetEntity then
 | 
						|
            InputUtils.RemoveTargetEntity(self.targetEntity)
 | 
						|
        elseif self.targetZone then
 | 
						|
            InputUtils.RemoveTargetZone(self.targetZone)
 | 
						|
        end
 | 
						|
        
 | 
						|
        Debug('Delete interaction', self.key)
 | 
						|
        
 | 
						|
        PLAYER_INTERACTIONS[self.key] = nil
 | 
						|
        self = nil
 | 
						|
    end
 | 
						|
    
 | 
						|
    --
 | 
						|
    
 | 
						|
    self.Boot()
 | 
						|
    
 | 
						|
    -- Add the interaction to the list
 | 
						|
    PLAYER_INTERACTIONS[self.key] = self
 | 
						|
    
 | 
						|
    Debug('Registered new interactable', json.encode(self.meta), self.GetCoords(), DoesEntityExist(self.entity))
 | 
						|
    
 | 
						|
    self.clientReturnData = {
 | 
						|
        GetMeta = self.GetMeta,
 | 
						|
        GetCoords = self.GetCoords,
 | 
						|
        GetEntity = self.GetEntity,
 | 
						|
        GetInvoker = self.GetInvoker,
 | 
						|
        Delete = self.Delete,
 | 
						|
    }
 | 
						|
    
 | 
						|
    return self.clientReturnData
 | 
						|
end
 | 
						|
 | 
						|
function AddInteractionEntity(entity, offset, message, targetMessage, input, callback, canInteract, meta, interactDist, icon)
 | 
						|
    return RegisterInteraction({
 | 
						|
        entity = entity,
 | 
						|
        offset = offset,
 | 
						|
        
 | 
						|
        message = message,
 | 
						|
        targetMessage = targetMessage,
 | 
						|
        input = input,
 | 
						|
        callback = callback,
 | 
						|
        canInteract = canInteract,
 | 
						|
        interactDist = interactDist,
 | 
						|
        icon = icon,
 | 
						|
        
 | 
						|
        meta = meta,
 | 
						|
    })
 | 
						|
end
 | 
						|
 | 
						|
function AddInteractionZone(coords, rotation, scale, message, targetMessage, input, callback, canInteract, meta, interactDist, icon)
 | 
						|
    return RegisterInteraction({
 | 
						|
        coords = coords,
 | 
						|
        rotation = rotation,
 | 
						|
        scale = scale,
 | 
						|
        
 | 
						|
        message = message,
 | 
						|
        targetMessage = targetMessage,
 | 
						|
        input = input,
 | 
						|
        callback = callback,
 | 
						|
        canInteract = canInteract,
 | 
						|
        interactDist = interactDist,
 | 
						|
        icon = icon,
 | 
						|
        
 | 
						|
        meta = meta,
 | 
						|
    })
 | 
						|
end
 | 
						|
 | 
						|
-- CLEANUP
 | 
						|
DeadInteractionRemovalThread()
 | 
						|
 | 
						|
AddEventHandler('onResourceStop', function(stoppedResource)
 | 
						|
    for k, interaction in pairs(PLAYER_INTERACTIONS) do
 | 
						|
        if interaction.GetInvoker() == stoppedResource then
 | 
						|
            interaction.Delete()
 | 
						|
        end
 | 
						|
    end
 | 
						|
end)
 |