if not lib.checkDependency('ox_lib', '3.30.0', true) then return end lib.locale() local utils = require 'client.utils' local state = require 'client.state' local options = require 'client.api'.getTargetOptions() require 'client.debug' require 'client.defaults' require 'client.compat.qtarget' local SendNuiMessage = SendNuiMessage local GetEntityCoords = GetEntityCoords local GetEntityType = GetEntityType local HasEntityClearLosToEntity = HasEntityClearLosToEntity local GetEntityBoneIndexByName = GetEntityBoneIndexByName local GetEntityBonePosition_2 = GetEntityBonePosition_2 local GetEntityModel = GetEntityModel local IsDisabledControlJustPressed = IsDisabledControlJustPressed local DisableControlAction = DisableControlAction local DisablePlayerFiring = DisablePlayerFiring local GetModelDimensions = GetModelDimensions local GetOffsetFromEntityInWorldCoords = GetOffsetFromEntityInWorldCoords local currentTarget = {} local currentMenu local menuChanged local menuHistory = {} local nearbyZones -- Toggle ox_target, instead of holding the hotkey local toggleHotkey = GetConvarInt('ox_target:toggleHotkey', 0) == 1 local mouseButton = GetConvarInt('ox_target:leftClick', 1) == 1 and 24 or 25 local debug = GetConvarInt('ox_target:debug', 0) == 1 local vec0 = vec3(0, 0, 0) ---@param option OxTargetOption ---@param distance number ---@param endCoords vector3 ---@param entityHit? number ---@param entityType? number ---@param entityModel? number | false local function shouldHide(option, distance, endCoords, entityHit, entityType, entityModel) if option.menuName ~= currentMenu then return true end if distance > (option.distance or 7) then return true end if option.groups and not utils.hasPlayerGotGroup(option.groups) then return true end if option.items and not utils.hasPlayerGotItems(option.items, option.anyItem) then return true end local bone = entityModel and option.bones or nil if bone then ---@cast entityHit number ---@cast entityType number ---@cast entityModel number local _type = type(bone) if _type == 'string' then local boneId = GetEntityBoneIndexByName(entityHit, bone) if boneId ~= -1 and #(endCoords - GetEntityBonePosition_2(entityHit, boneId)) <= 2 then bone = boneId else return true end elseif _type == 'table' then local closestBone, boneDistance for j = 1, #bone do local boneId = GetEntityBoneIndexByName(entityHit, bone[j]) if boneId ~= -1 then local dist = #(endCoords - GetEntityBonePosition_2(entityHit, boneId)) if dist <= (boneDistance or 1) then closestBone = boneId boneDistance = dist end end end if closestBone then bone = closestBone else return true end end end local offset = entityModel and option.offset or nil if offset then ---@cast entityHit number ---@cast entityType number ---@cast entityModel number if not option.absoluteOffset then local min, max = GetModelDimensions(entityModel) offset = (max - min) * offset + min end offset = GetOffsetFromEntityInWorldCoords(entityHit, offset.x, offset.y, offset.z) if #(endCoords - offset) > (option.offsetSize or 1) then return true end end if option.canInteract then local success, resp = pcall(option.canInteract, entityHit, distance, endCoords, option.name, bone) return not success or not resp end end local function startTargeting() if state.isDisabled() or state.isActive() or IsNuiFocused() or IsPauseMenuActive() then return end state.setActive(true) local flag = 511 local hit, entityHit, endCoords, distance, lastEntity, entityType, entityModel, hasTarget, zonesChanged local zones = {} CreateThread(function() local dict, texture = utils.getTexture() local lastCoords while state.isActive() do lastCoords = endCoords == vec0 and lastCoords or endCoords or vec0 if debug then DrawMarker(28, lastCoords.x, lastCoords.y, lastCoords.z, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.2, 0.2, 0.2, ---@diagnostic disable-next-line: param-type-mismatch 255, 42, 24, 100, false, false, 0, true, false, false, false) end utils.drawZoneSprites(dict, texture) DisablePlayerFiring(cache.playerId, true) DisableControlAction(0, 25, true) DisableControlAction(0, 140, true) DisableControlAction(0, 141, true) DisableControlAction(0, 142, true) if state.isNuiFocused() then DisableControlAction(0, 1, true) DisableControlAction(0, 2, true) if not hasTarget or options and IsDisabledControlJustPressed(0, 25) then state.setNuiFocus(false, false) end elseif hasTarget and IsDisabledControlJustPressed(0, mouseButton) then state.setNuiFocus(true, true) end Wait(0) end SetStreamedTextureDictAsNoLongerNeeded(dict) end) while state.isActive() do if not state.isNuiFocused() and lib.progressActive() then state.setActive(false) break end local playerCoords = GetEntityCoords(cache.ped) hit, entityHit, endCoords = lib.raycast.fromCamera(flag, 4, 20) distance = #(playerCoords - endCoords) if entityHit ~= 0 and entityHit ~= lastEntity then local success, result = pcall(GetEntityType, entityHit) entityType = success and result or 0 end if entityType == 0 then local _flag = flag == 511 and 26 or 511 local _hit, _entityHit, _endCoords = lib.raycast.fromCamera(_flag, 4, 20) local _distance = #(playerCoords - _endCoords) if _distance < distance then flag, hit, entityHit, endCoords, distance = _flag, _hit, _entityHit, _endCoords, _distance if entityHit ~= 0 then local success, result = pcall(GetEntityType, entityHit) entityType = success and result or 0 end end end nearbyZones, zonesChanged = utils.getNearbyZones(endCoords) local entityChanged = entityHit ~= lastEntity local newOptions = (zonesChanged or entityChanged or menuChanged) and true if entityHit > 0 and entityChanged then currentMenu = nil if flag ~= 511 then entityHit = HasEntityClearLosToEntity(entityHit, cache.ped, 7) and entityHit or 0 end if lastEntity ~= entityHit and debug then if lastEntity then SetEntityDrawOutline(lastEntity, false) end if entityType ~= 1 then SetEntityDrawOutline(entityHit, true) end end if entityHit > 0 then local success, result = pcall(GetEntityModel, entityHit) entityModel = success and result end end if hasTarget and (zonesChanged or entityChanged and hasTarget > 1) then SendNuiMessage('{"event": "leftTarget"}') if entityChanged then options:wipe() end if debug and lastEntity > 0 then SetEntityDrawOutline(lastEntity, false) end hasTarget = false end if newOptions and entityModel and entityHit > 0 then options:set(entityHit, entityType, entityModel) end lastEntity = entityHit currentTarget.entity = entityHit currentTarget.coords = endCoords currentTarget.distance = distance local hidden = 0 local totalOptions = 0 for k, v in pairs(options) do local optionCount = #v local dist = k == '__global' and 0 or distance totalOptions += optionCount for i = 1, optionCount do local option = v[i] local hide = shouldHide(option, dist, endCoords, entityHit, entityType, entityModel) if option.hide ~= hide then option.hide = hide newOptions = true end if hide then hidden += 1 end end end if zonesChanged then table.wipe(zones) end for i = 1, #nearbyZones do local zoneOptions = nearbyZones[i].options local optionCount = #zoneOptions totalOptions += optionCount zones[i] = zoneOptions for j = 1, optionCount do local option = zoneOptions[j] local hide = shouldHide(option, distance, endCoords, entityHit) if option.hide ~= hide then option.hide = hide newOptions = true end if hide then hidden += 1 end end end if newOptions then if hasTarget == 1 and (totalOptions - hidden) > 1 then hasTarget = true end if hasTarget and hidden == totalOptions then if hasTarget and hasTarget ~= 1 then hasTarget = false SendNuiMessage('{"event": "leftTarget"}') end elseif menuChanged or hasTarget ~= 1 and hidden ~= totalOptions then hasTarget = options.size if currentMenu and options.__global[1]?.name ~= 'builtin:goback' then table.insert(options.__global, 1, { icon = 'fa-solid fa-circle-chevron-left', label = locale('go_back'), name = 'builtin:goback', menuName = currentMenu, openMenu = 'home' }) end SendNuiMessage(json.encode({ event = 'setTarget', options = options, zones = zones, }, { sort_keys = true })) end menuChanged = false end if toggleHotkey and IsPauseMenuActive() then state.setActive(false) end if not hasTarget or hasTarget == 1 then flag = flag == 511 and 26 or 511 end Wait(hit and 50 or 100) end if lastEntity and debug then SetEntityDrawOutline(lastEntity, false) end state.setNuiFocus(false) SendNuiMessage('{"event": "visible", "state": false}') table.wipe(currentTarget) options:wipe() if nearbyZones then table.wipe(nearbyZones) end end do ---@type KeybindProps local keybind = { name = 'ox_target', defaultKey = GetConvar('ox_target:defaultHotkey', 'LMENU'), defaultMapper = 'keyboard', description = locale('toggle_targeting'), } if toggleHotkey then function keybind:onPressed() if state.isActive() then return state.setActive(false) end return startTargeting() end else keybind.onPressed = startTargeting function keybind:onReleased() state.setActive(false) end end lib.addKeybind(keybind) end ---@generic T ---@param option T ---@param server? boolean ---@return T local function getResponse(option, server) local response = table.clone(option) response.entity = currentTarget.entity response.zone = currentTarget.zone response.coords = currentTarget.coords response.distance = currentTarget.distance if server then response.entity = response.entity ~= 0 and NetworkGetEntityIsNetworked(response.entity) and NetworkGetNetworkIdFromEntity(response.entity) or 0 end response.icon = nil response.groups = nil response.items = nil response.canInteract = nil response.onSelect = nil response.export = nil response.event = nil response.serverEvent = nil response.command = nil return response end RegisterNUICallback('select', function(data, cb) cb(1) local zone = data[3] and nearbyZones[data[3]] ---@type OxTargetOption? local option = zone and zone.options[data[2]] or options[data[1]][data[2]] if option then if option.openMenu then local menuDepth = #menuHistory if option.name == 'builtin:goback' then option.menuName = option.openMenu option.openMenu = menuHistory[menuDepth] if menuDepth > 0 then menuHistory[menuDepth] = nil end else menuHistory[menuDepth + 1] = currentMenu end menuChanged = true currentMenu = option.openMenu ~= 'home' and option.openMenu or nil options:wipe() else state.setNuiFocus(false) end currentTarget.zone = zone?.id if option.onSelect then option.onSelect(option.qtarget and currentTarget.entity or getResponse(option)) elseif option.export then exports[option.resource or zone.resource][option.export](nil, getResponse(option)) elseif option.event then TriggerEvent(option.event, getResponse(option)) elseif option.serverEvent then TriggerServerEvent(option.serverEvent, getResponse(option, true)) elseif option.command then ExecuteCommand(option.command) end if option.menuName == 'home' then return end end if not option?.openMenu and IsNuiFocused() then state.setActive(false) end end)