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)
 | 
