Full Code of overextended/ox_target for AI

main abe153aa3370 cached
45 files
74.4 KB
20.7k tokens
3 symbols
1 requests
Download .txt
Repository: overextended/ox_target
Branch: main
Commit: abe153aa3370
Files: 45
Total size: 74.4 KB

Directory structure:
gitextract_ba2qc7hr/

├── .github/
│   ├── actions/
│   │   └── bump-manifest-version.js
│   └── workflows/
│       └── release.yml
├── LICENSE
├── README.md
├── client/
│   ├── api.lua
│   ├── compat/
│   │   └── qtarget.lua
│   ├── debug.lua
│   ├── defaults.lua
│   ├── framework/
│   │   ├── esx.lua
│   │   ├── nd.lua
│   │   ├── ox.lua
│   │   └── qbx.lua
│   ├── main.lua
│   ├── state.lua
│   └── utils.lua
├── fxmanifest.lua
├── locales/
│   ├── cs.json
│   ├── da.json
│   ├── de.json
│   ├── en.json
│   ├── es.json
│   ├── et.json
│   ├── fi.json
│   ├── fr.json
│   ├── hr.json
│   ├── hu.json
│   ├── id.json
│   ├── it.json
│   ├── nl.json
│   ├── no.json
│   ├── pl.json
│   ├── pt-br.json
│   ├── pt.json
│   ├── ro.json
│   ├── sl.json
│   ├── sv.json
│   ├── tr.json
│   ├── zh-cn.json
│   └── zh-tw.json
├── server/
│   └── main.lua
└── web/
    ├── index.html
    ├── js/
    │   ├── createOptions.js
    │   ├── fetchNui.js
    │   └── main.js
    └── style.css

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/actions/bump-manifest-version.js
================================================
const fxManifest = await Bun.file('./fxmanifest.lua').text();

let newVersion = process.env.TGT_RELEASE_VERSION;
newVersion = newVersion.replace('v', '')

const newFileContent = fxManifest.replace(/\bversion\s+(.*)$/gm, `version '${newVersion}'`);

await Bun.write('./fxmanifest.lua', newFileContent);


================================================
FILE: .github/workflows/release.yml
================================================
name: Create release

on:
  push:
    tags:
      - "v*.*.*"

jobs:
  create-release:
    if: github.actor_id != 278903378
    runs-on: ubuntu-latest
    steps:
      - name: Install zip
        run: sudo apt install zip

      - name: Install Bun
        uses: oven-sh/setup-bun@v2
        with:
          bun-version: latest

      - name: Generate GitHub App token
        id: app_token
        uses: tibdex/github-app-token@v2
        with:
          app_id: ${{ secrets.APP_ID }}
          private_key: ${{ secrets.APP_PRIVATE_KEY }}

      - name: Get latest code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          ref: ${{ github.event.repository.default_branch }}
          token: ${{ steps.app_token.outputs.token }}

      - name: Bump manifest version
        run: bun run .github/actions/bump-manifest-version.js
        env:
          TGT_RELEASE_VERSION: ${{ github.ref_name }}

      - name: Push version bump change
        uses: EndBug/add-and-commit@v9
        with:
          add: fxmanifest.lua
          push: true
          default_author: github_actions
          message: "chore: bump version to ${{ github.ref_name }}"

      - name: Bundle files
        run: |
          mkdir -p ./temp/ox_target
          cp ./{LICENSE,README.md,fxmanifest.lua} ./temp/ox_target
          cp -r ./{client,server,web,locales} ./temp/ox_target
          cd ./temp && zip -r ../ox_target.zip ./ox_target

      - name: Create release
        uses: "marvinpinto/action-automatic-releases@v1.2.1"
        with:
          repo_token: ${{ secrets.GITHUB_TOKEN }}
          prerelease: false
          files: ox_target.zip

      - name: Update tag
        uses: EndBug/latest-tag@v1
        with:
          ref: ${{ github.ref_name }}


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2022 Overextended

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# ox_target

![](https://img.shields.io/github/downloads/overextended/ox_target/total?logo=github)
![](https://img.shields.io/github/downloads/overextended/ox_target/latest/total?logo=github)
![](https://img.shields.io/github/contributors/overextended/ox_target?logo=github)
![](https://img.shields.io/github/v/release/overextended/ox_target?logo=github) 


A performant and flexible standalone "third-eye" targeting resource, with additional functionality for supported frameworks.

ox_target is the successor to qtarget, which was a mostly-compatible fork of bt-target.
To improve many design flaws, ox_target has been written from scratch and drops support for bt-target/qtarget standards, though partial compatibility is being implemented where possible.


## 📚 Documentation

https://overextended.dev/ox_target

## 💾 Download

https://github.com/overextended/ox_target/releases/latest/download/ox_target.zip

## ✨ Features

- Improved entity and world collision than its predecessor.
- Improved error handling when running external code.
- Menus for nested target options.
- Partial compatibility for qtarget (the thing qb-target is based on, I made the original idiots).
- Registering options no longer overrides existing options.
- Groups and items checking for supported frameworks.


================================================
FILE: client/api.lua
================================================
---@class OxTargetOption
---@field resource? string

local utils = require 'client.utils'

local api = setmetatable({}, {
    __newindex = function(self, index, value)
        rawset(self, index, value)
        exports(index, value)
    end
})

---Throws a formatted type error
---@param variable string
---@param expected string
---@param received string
local function typeError(variable, expected, received)
    error(("expected %s to have type '%s' (received %s)"):format(variable, expected, received))
end

---Checks options and throws an error on type mismatch
---@param options OxTargetOption | OxTargetOption[]
---@return OxTargetOption[]
local function checkOptions(options)
    local optionsType = type(options)

    if optionsType ~= 'table' then
        typeError('options', 'table', optionsType)
    end

    local tableType = table.type(options)

    if tableType == 'hash' and options.label then
        options = { options }
    elseif tableType ~= 'array' then
        typeError('options', 'array', ('%s table'):format(tableType))
    end

    return options
end

---@param data OxTargetPolyZone | table
---@return number
function api.addPolyZone(data)
    if data.debug then utils.warn('Creating new PolyZone with debug enabled.') end

    data.resource = GetInvokingResource()
    data.options = checkOptions(data.options)
    return lib.zones.poly(data).id
end

---@param data OxTargetBoxZone | table
---@return number
function api.addBoxZone(data)
    if data.debug then utils.warn('Creating new BoxZone with debug enabled.') end

    data.resource = GetInvokingResource()
    data.options = checkOptions(data.options)
    return lib.zones.box(data).id
end

---@param data OxTargetSphereZone | table
---@return number
function api.addSphereZone(data)
    if data.debug then utils.warn('Creating new SphereZone with debug enabled.') end

    data.resource = GetInvokingResource()
    data.options = checkOptions(data.options)
    return lib.zones.sphere(data).id
end

---@param id number | string The ID of the zone to check. It can be either a number or a string representing the zone's index or name, respectively.
---@return boolean returns true if the zone with the specified ID exists, otherwise false.
function api.zoneExists(id)
    if not Zones or (type(id) ~= 'number' and type(id) ~= 'string') then return false end

    if type(id) == 'number' and Zones[id] then return true end

    for _, zone in pairs(lib.zones.getAllZones()) do
        if type(id) == 'string' and zone.name == id then return true end
    end

    return false
end

---@param id number | string
---@param suppressWarning boolean?
function api.removeZone(id, suppressWarning)
    if Zones then
        if type(id) == 'string' then
            local foundZone

            for _, v in pairs(lib.zones.getAllZones()) do
                if v.name == id then
                    foundZone = true
                    v:remove()
                end
            end

            if foundZone then return end
        elseif Zones[id] then
            return Zones[id]:remove()
        end
    end

    if suppressWarning then return end

    warn(('attempted to remove a zone that does not exist (id: %s)'):format(id))
end

---@param target table
---@param remove string | string[]
---@param resource string
---@param showWarning? boolean
local function removeTarget(target, remove, resource, showWarning)
    if type(remove) ~= 'table' then remove = { remove } end

    for i = #target, 1, -1 do
        local option = target[i]

        if option.resource == resource then
            for j = #remove, 1, -1 do
                if option.name == remove[j] then
                    table.remove(target, i)

                    if showWarning then
                        utils.warn(("Replacing existing target option '%s'."):format(option.name))
                    end
                end
            end
        end
    end
end

---@param target table
---@param options OxTargetOption | OxTargetOption[]
---@param resource string
local function addTarget(target, options, resource)
    options = checkOptions(options)

    local checkNames = {}

    resource = resource or 'ox_target'

    for i = 1, #options do
        local option = options[i]
        option.resource = resource

        if option.name then
            checkNames[#checkNames + 1] = option.name
        end
    end

    if checkNames[1] then
        removeTarget(target, checkNames, resource, true)
    end

    local num = #target

    for i = 1, #options do
        local option = options[i]

        if resource == 'ox_target' then
            if option.canInteract then
                option.canInteract = msgpack.unpack(msgpack.pack(option.canInteract))
            end

            if option.onSelect then
                option.onSelect = msgpack.unpack(msgpack.pack(option.onSelect))
            end
        end

        num += 1
        target[num] = options[i]
    end
end

---@type table<number, OxTargetOption[]>
local peds = {}

---@param options OxTargetOption | OxTargetOption[]
function api.addGlobalPed(options)
    addTarget(peds, options, GetInvokingResource())
end

---@param options string | string[]
function api.removeGlobalPed(options)
    removeTarget(peds, options, GetInvokingResource())
end

---@type table<number, OxTargetOption[]>
local vehicles = {}

---@param options OxTargetOption | OxTargetOption[]
function api.addGlobalVehicle(options)
    addTarget(vehicles, options, GetInvokingResource())
end

---@param options string | string[]
function api.removeGlobalVehicle(options)
    removeTarget(vehicles, options, GetInvokingResource())
end

---@type table<number, OxTargetOption[]>
local objects = {}

---@param options OxTargetOption | OxTargetOption[]
function api.addGlobalObject(options)
    addTarget(objects, options, GetInvokingResource())
end

---@param options string | string[]
function api.removeGlobalObject(options)
    removeTarget(objects, options, GetInvokingResource())
end

---@type table<number, OxTargetOption[]>
local players = {}

---@param options OxTargetOption | OxTargetOption[]
function api.addGlobalPlayer(options)
    addTarget(players, options, GetInvokingResource())
end

---@param options string | string[]
function api.removeGlobalPlayer(options)
    removeTarget(players, options, GetInvokingResource())
end

---@type table<number, OxTargetOption[]>
local models = {}

---@param arr (number | string) | (number | string)[]
---@param options OxTargetOption | OxTargetOption[]
function api.addModel(arr, options)
    if type(arr) ~= 'table' then arr = { arr } end
    local resource = GetInvokingResource()

    for i = 1, #arr do
        local model = arr[i]
        model = tonumber(model) or joaat(model)

        if not models[model] then
            models[model] = {}
        end

        addTarget(models[model], options, resource)
    end
end

---@param arr (number | string) | (number | string)[]
---@param options? string | string[]
function api.removeModel(arr, options)
    if type(arr) ~= 'table' then arr = { arr } end
    local resource = GetInvokingResource()

    for i = 1, #arr do
        local model = arr[i]
        model = tonumber(model) or joaat(model)

        if models[model] then
            if options then
                removeTarget(models[model], options, resource)
            end

            if not options or #models[model] == 0 then
                models[model] = nil
            end
        end
    end
end

---@type table<number, OxTargetOption[]>
local entities = {}

---@param arr number | number[]
---@param options OxTargetOption | OxTargetOption[]
function api.addEntity(arr, options)
    if type(arr) ~= 'table' then arr = { arr } end
    local resource = GetInvokingResource()

    for i = 1, #arr do
        local netId = arr[i]

        if NetworkDoesNetworkIdExist(netId) then
            if not entities[netId] then
                entities[netId] = {}

                if not Entity(NetworkGetEntityFromNetworkId(netId)).state.hasTargetOptions then
                    TriggerServerEvent('ox_target:setEntityHasOptions', netId)
                end
            end

            addTarget(entities[netId], options, resource)
        end
    end
end

---@param arr number | number[]
---@param options? string | string[]
function api.removeEntity(arr, options)
    if type(arr) ~= 'table' then arr = { arr } end
    local resource = GetInvokingResource()

    for i = 1, #arr do
        local netId = arr[i]

        if entities[netId] then
            if options then
                removeTarget(entities[netId], options, resource)
            end

            if not options or #entities[netId] == 0 then
                entities[netId] = nil
            end
        end
    end
end

RegisterNetEvent('ox_target:removeEntity', api.removeEntity)

---@type table<number, OxTargetOption[]>
local localEntities = {}

---@param arr number | number[]
---@param options OxTargetOption | OxTargetOption[]
function api.addLocalEntity(arr, options)
    if type(arr) ~= 'table' then arr = { arr } end
    local resource = GetInvokingResource()

    for i = 1, #arr do
        local entityId = arr[i]

        if DoesEntityExist(entityId) then
            if not localEntities[entityId] then
                localEntities[entityId] = {}
            end

            addTarget(localEntities[entityId], options, resource)
        else
             lib.print.warn(("No entity with id '%s' exists in %s."):format(entityId, resource))
        end
    end
end

---@param arr number | number[]
---@param options? table
function api.removeLocalEntity(arr, options)
    if type(arr) ~= 'table' then arr = { arr } end
    local resource = GetInvokingResource()

    for i = 1, #arr do
        local entity = arr[i]

        if localEntities[entity] then
            if options then
                removeTarget(localEntities[entity], options, resource)
            end

            if not options or #localEntities[entity] == 0 then
                localEntities[entity] = nil
            end
        end
    end
end

CreateThread(function()
    while true do
        Wait(60000)

        for entityId in pairs(localEntities) do
            if not DoesEntityExist(entityId) then
                localEntities[entityId] = nil
            end
        end
    end
end)

---@param resource string
---@param target table
local function removeResourceGlobals(resource, target)
    for i = 1, #target do
        local options = target[i]

        for j = #options, 1, -1 do
            if options[j].resource == resource then
                table.remove(options, j)
            end
        end
    end
end

---@param resource string
---@param target table
local function removeResourceTargets(resource, target)
    for i = 1, #target do
        local tbl = target[i]

        for key, options in pairs(tbl) do
            for j = #options, 1, -1 do
                if options[j].resource == resource then
                    table.remove(options, j)
                end
            end

            if #options == 0 then
                tbl[key] = nil
            end
        end
    end
end

---@param resource string
AddEventHandler('onClientResourceStop', function(resource)
    removeResourceGlobals(resource, { peds, vehicles, objects, players })
    removeResourceTargets(resource, { models, entities, localEntities })

    if Zones then
        for _, v in pairs(Zones) do
            if v.resource == resource then
                v:remove()
            end
        end
    end
end)

local NetworkGetEntityIsNetworked = NetworkGetEntityIsNetworked
local NetworkGetNetworkIdFromEntity = NetworkGetNetworkIdFromEntity

---@class OxTargetOptions
local options_mt = {}
options_mt.__index = options_mt
options_mt.size = 1

function options_mt:wipe()
    options_mt.size = 1
    self.globalTarget = nil
    self.model = nil
    self.entity = nil
    self.localEntity = nil

    if self.__global[1]?.name == 'builtin:goback' then
        table.remove(self.__global, 1)
    end
end

---@param entity? number
---@param _type? number
---@param model? number
function options_mt:set(entity, _type, model)
    if not entity then return end

    if _type == 1 and IsPedAPlayer(entity) then
        self:wipe()
        self.globalTarget = players
        options_mt.size += 1

        return
    end

    local netId = NetworkGetEntityIsNetworked(entity) and NetworkGetNetworkIdFromEntity(entity)

    self.globalTarget = _type == 1 and peds or _type == 2 and vehicles or objects
    self.model = models[model]
    self.entity = netId and entities[netId] or nil
    self.localEntity = localEntities[entity]
    options_mt.size += 1

    if self.model then options_mt.size += 1 end
    if self.entity then options_mt.size += 1 end
    if self.localEntity then options_mt.size += 1 end
end

---@type OxTargetOption[]
local global = {}

---@param options OxTargetOption | OxTargetOption[]
function api.addGlobalOption(options)
    addTarget(global, options, GetInvokingResource())
end

---@param options string | string[]
function api.removeGlobalOption(options)
    removeTarget(global, options, GetInvokingResource())
end

---@class OxTargetOptions
local options = setmetatable({
    __global = global
}, options_mt)

---@param entity? number
---@param _type? number
---@param model? number
function api.getTargetOptions(entity, _type, model)
    if not entity then return options end

    if IsPedAPlayer(entity) then
        return {
            global = players,
        }
    end

    local netId = NetworkGetEntityIsNetworked(entity) and NetworkGetNetworkIdFromEntity(entity)

    return {
        global = _type == 1 and peds or _type == 2 and vehicles or objects,
        model = models[model],
        entity = netId and entities[netId] or nil,
        localEntity = localEntities[entity],
    }
end

local state = require 'client.state'

function api.disableTargeting(value)
    if value then
        state.setActive(false)
    end

    state.setDisabled(value)
end

function api.isActive()
    return state.isActive()
end

return api


================================================
FILE: client/compat/qtarget.lua
================================================
local function exportHandler(exportName, func)
    AddEventHandler(('__cfx_export_qtarget_%s'):format(exportName), function(setCB)
        setCB(func)
    end)
end

---@param options table
---@return table
local function convert(options)
    local distance = options.distance
    options = options.options

    -- People may pass options as a hashmap (or mixed, even)
    for k, v in pairs(options) do
        if type(k) ~= 'number' then
            table.insert(options, v)
        end
    end

    for id, v in pairs(options) do
        if type(id) ~= 'number' then
            options[id] = nil
            goto continue
        end

        v.onSelect = v.action
        v.distance = v.distance or distance
        v.name = v.name or v.label
        v.groups = v.job
        v.items = v.item or v.required_item

        if v.event and v.type and v.type ~= 'client' then
            if v.type == 'server' then
                v.serverEvent = v.event
            elseif v.type == 'command' then
                v.command = v.event
            end

            v.event = nil
            v.type = nil
        end

        v.action = nil
        v.job = nil
        v.item = nil
        v.required_item = nil
        v.qtarget = true

        ::continue::
    end

    return options
end

local api = require 'client.api'

exportHandler('AddBoxZone', function(name, center, length, width, options, targetoptions)
    local z = center.z

    if not options.minZ then
        options.minZ = -100
    end

    if not options.maxZ then
        options.maxZ = 800
    end

    if not options.useZ then
        z = z + math.abs(options.maxZ - options.minZ) / 2
        center = vec3(center.x, center.y, z)
    end

    return api.addBoxZone({
        name = name,
        coords = center,
        size = vec3(width, length, (options.useZ or not options.maxZ) and center.z or math.abs(options.maxZ - options.minZ)),
        debug = options.debugPoly,
        rotation = options.heading,
        options = convert(targetoptions),
    })
end)

exportHandler('AddPolyZone', function(name, points, options, targetoptions)
    local newPoints = table.create(#points, 0)
    local thickness = math.abs(options.maxZ - options.minZ)

    for i = 1, #points do
        local point = points[i]
        newPoints[i] = vec3(point.x, point.y, options.maxZ - (thickness / 2))
    end

    return api.addPolyZone({
        name = name,
        points = newPoints,
        thickness = thickness,
        debug = options.debugPoly,
        options = convert(targetoptions),
    })
end)

exportHandler('AddCircleZone', function(name, center, radius, options, targetoptions)
    return api.addSphereZone({
        name = name,
        coords = center,
        radius = radius,
        debug = options.debugPoly,
        options = convert(targetoptions),
    })
end)

exportHandler('RemoveZone', function(id)
    api.removeZone(id, true)
end)

exportHandler('AddTargetBone', function(bones, options)
    if type(bones) ~= 'table' then bones = { bones } end
    options = convert(options)

    for _, v in pairs(options) do
        v.bones = bones
    end

    exports.ox_target:addGlobalVehicle(options)
end)

exportHandler('AddTargetEntity', function(entities, options)
    if type(entities) ~= 'table' then entities = { entities } end
    options = convert(options)

    for i = 1, #entities do
        local entity = entities[i]

        if NetworkGetEntityIsNetworked(entity) then
            api.addEntity(NetworkGetNetworkIdFromEntity(entity), options)
        else
            api.addLocalEntity(entity, options)
        end
    end
end)

exportHandler('RemoveTargetEntity', function(entities, labels)
    if type(entities) ~= 'table' then entities = { entities } end

    for i = 1, #entities do
        local entity = entities[i]

        if NetworkGetEntityIsNetworked(entity) then
            api.removeEntity(NetworkGetNetworkIdFromEntity(entity), labels)
        else
            api.removeLocalEntity(entity, labels)
        end
    end
end)

exportHandler('AddTargetModel', function(models, options)
    api.addModel(models, convert(options))
end)

exportHandler('RemoveTargetModel', function(models, labels)
    api.removeModel(models, labels)
end)

exportHandler('Ped', function(options)
    api.addGlobalPed(convert(options))
end)

exportHandler('RemovePed', function(labels)
    api.removeGlobalPed(labels)
end)

exportHandler('Vehicle', function(options)
    api.addGlobalVehicle(convert(options))
end)

exportHandler('RemoveVehicle', function(labels)
    api.removeGlobalVehicle(labels)
end)

exportHandler('Object', function(options)
    api.addGlobalObject(convert(options))
end)

exportHandler('RemoveObject', function(labels)
    api.removeGlobalObject(labels)
end)

exportHandler('Player', function(options)
    api.addGlobalPlayer(convert(options))
end)

exportHandler('RemovePlayer', function(labels)
    api.removeGlobalPlayer(labels)
end)

================================================
FILE: client/debug.lua
================================================
AddEventHandler('ox_target:debug', function(data)
    if data.entity and GetEntityType(data.entity) > 0 then
        data.archetype = GetEntityArchetypeName(data.entity)
        data.model = GetEntityModel(data.entity)
    end

	print(json.encode(data, {indent=true}))
end)

if GetConvarInt('ox_target:debug', 0) ~= 1 then return end

local ox_target = exports.ox_target
local drawZones = true

ox_target:addBoxZone({
    coords = vec3(442.5363, -1017.666, 28.85637),
    size = vec3(3, 3, 3),
    rotation = 45,
    debug = drawZones,
    drawSprite = true,
    options = {
        {
            name = 'debug_box',
            event = 'ox_target:debug',
            icon = 'fa-solid fa-cube',
            label = locale('debug_box'),
        }
    }
})

ox_target:addSphereZone({
    coords = vec3(440.5363, -1015.666, 28.85637),
    radius = 3,
    debug = drawZones,
    drawSprite = true,
    options = {
        {
            name = 'debug_sphere',
            event = 'ox_target:debug',
            icon = 'fa-solid fa-circle',
            label = locale('debug_sphere'),
        }
    }
})

ox_target:addModel(`police`, {
    {
        name = 'debug_model',
        event = 'ox_target:debug',
        icon = 'fa-solid fa-handcuffs',
        label = locale('debug_police_car'),
    }
})

ox_target:addGlobalPed({
    {
        name = 'debug_ped',
        event = 'ox_target:debug',
        icon = 'fa-solid fa-male',
        label = locale('debug_ped'),
    }
})

ox_target:addGlobalVehicle({
    {
        name = 'debug_vehicle',
        event = 'ox_target:debug',
        icon = 'fa-solid fa-car',
        label = locale('debug_vehicle'),
    }
})

ox_target:addGlobalObject({
    {
        name = 'debug_object',
        event = 'ox_target:debug',
        icon = 'fa-solid fa-bong',
        label = locale('debug_object'),
    }
})

ox_target:addGlobalOption({
    {
        name = 'debug_global',
        icon = 'fa-solid fa-globe',
        label = locale('debug_global'),
        openMenu = 'debug_global'
    }
})

ox_target:addGlobalOption({
    {
        name = 'debug_global2',
        event = 'ox_target:debug',
        icon = 'fa-solid fa-globe',
        label = locale('debug_global') .. ' 2',
        menuName = 'debug_global'
    }
})

================================================
FILE: client/defaults.lua
================================================
if GetConvarInt('ox_target:defaults', 1) ~= 1 then return end

local api = require 'client.api'
local GetEntityBoneIndexByName = GetEntityBoneIndexByName
local GetEntityBonePosition_2 = GetEntityBonePosition_2
local GetVehicleDoorLockStatus = GetVehicleDoorLockStatus

local bones = {
    [0] = 'dside_f',
    [1] = 'pside_f',
    [2] = 'dside_r',
    [3] = 'pside_r'
}

---@param vehicle number
---@param door number
local function toggleDoor(vehicle, door)
    if GetVehicleDoorLockStatus(vehicle) ~= 2 then
        if GetVehicleDoorAngleRatio(vehicle, door) > 0.0 then
            SetVehicleDoorShut(vehicle, door, false)
        else
            SetVehicleDoorOpen(vehicle, door, false, false)
        end
    end
end

---@param entity number
---@param coords vector3
---@param door number
---@param useOffset boolean?
---@return boolean?
local function canInteractWithDoor(entity, coords, door, useOffset)
    if not GetIsDoorValid(entity, door) or GetVehicleDoorLockStatus(entity) > 1 or IsVehicleDoorDamaged(entity, door) or cache.vehicle then return end

    if useOffset then return true end

    local boneName = bones[door]

    if not boneName then return false end

    local boneId = GetEntityBoneIndexByName(entity, 'door_' .. boneName)

    if boneId ~= -1 then
        return #(coords - GetEntityBonePosition_2(entity, boneId)) < 0.5 or
            #(coords - GetEntityBonePosition_2(entity, GetEntityBoneIndexByName(entity, 'seat_' .. boneName))) < 0.72
    end
end

local function onSelectDoor(data, door)
    local entity = data.entity

    if NetworkGetEntityOwner(entity) == cache.playerId then
        return toggleDoor(entity, door)
    end

    TriggerServerEvent('ox_target:toggleEntityDoor', VehToNet(entity), door)
end

RegisterNetEvent('ox_target:toggleEntityDoor', function(netId, door)
    local entity = NetToVeh(netId)
    toggleDoor(entity, door)
end)

api.addGlobalVehicle({
    {
        name = 'ox_target:driverF',
        icon = 'fa-solid fa-car-side',
        label = locale('toggle_front_driver_door'),
        bones = { 'door_dside_f', 'seat_dside_f' },
        distance = 2,
        canInteract = function(entity, distance, coords, name)
            return canInteractWithDoor(entity, coords, 0)
        end,
        onSelect = function(data)
            onSelectDoor(data, 0)
        end
    },
    {
        name = 'ox_target:passengerF',
        icon = 'fa-solid fa-car-side',
        label = locale('toggle_front_passenger_door'),
        bones = { 'door_pside_f', 'seat_pside_f' },
        distance = 2,
        canInteract = function(entity, distance, coords, name)
            return canInteractWithDoor(entity, coords, 1)
        end,
        onSelect = function(data)
            onSelectDoor(data, 1)
        end
    },
    {
        name = 'ox_target:driverR',
        icon = 'fa-solid fa-car-side',
        label = locale('toggle_rear_driver_door'),
        bones = { 'door_dside_r', 'seat_dside_r' },
        distance = 2,
        canInteract = function(entity, distance, coords)
            return canInteractWithDoor(entity, coords, 2)
        end,
        onSelect = function(data)
            onSelectDoor(data, 2)
        end
    },
    {
        name = 'ox_target:passengerR',
        icon = 'fa-solid fa-car-side',
        label = locale('toggle_rear_passenger_door'),
        bones = { 'door_pside_r', 'seat_pside_r' },
        distance = 2,
        canInteract = function(entity, distance, coords)
            return canInteractWithDoor(entity, coords, 3)
        end,
        onSelect = function(data)
            onSelectDoor(data, 3)
        end
    },
    {
        name = 'ox_target:bonnet',
        icon = 'fa-solid fa-car',
        label = locale('toggle_hood'),
        offset = vec3(0.5, 1, 0.5),
        distance = 2,
        canInteract = function(entity, distance, coords)
            return canInteractWithDoor(entity, coords, 4, true)
        end,
        onSelect = function(data)
            onSelectDoor(data, 4)
        end
    },
    {
        name = 'ox_target:trunk',
        icon = 'fa-solid fa-car-rear',
        label = locale('toggle_trunk'),
        offset = vec3(0.5, 0, 0.5),
        distance = 2,
        canInteract = function(entity, distance, coords, name)
            return canInteractWithDoor(entity, coords, 5, true)
        end,
        onSelect = function(data)
            onSelectDoor(data, 5)
        end
    }
})


================================================
FILE: client/framework/esx.lua
================================================
local ESX = exports.es_extended:getSharedObject()
local utils = require 'client.utils'
local groups = { 'job', 'job2' }
local playerGroups = {}
local playerItems = utils.getItems()
local usingOxInventory = utils.hasExport('ox_inventory.Items')

local function setPlayerData(playerData)
    table.wipe(playerGroups)
    table.wipe(playerItems)

    for i = 1, #groups do
        local group = groups[i]
        local data = playerData[group]

        if data then
            playerGroups[group] = data
        end
    end

    if usingOxInventory or not playerData.inventory then return end

    for _, v in pairs(playerData.inventory) do
        if v.count > 0 then
            playerItems[v.name] = v.count
        end
    end
end

if ESX.PlayerLoaded then
    setPlayerData(ESX.PlayerData)
end

RegisterNetEvent('esx:playerLoaded', function(data)
    if source == '' then return end
    setPlayerData(data)
end)

RegisterNetEvent('esx:setJob', function(job)
    if source == '' then return end
    playerGroups.job = job
end)

RegisterNetEvent('esx:setJob2', function(job)
    if source == '' then return end
    playerGroups.job2 = job
end)

RegisterNetEvent('esx:addInventoryItem', function(name, count)
    playerItems[name] = count
end)

RegisterNetEvent('esx:removeInventoryItem', function(name, count)
    playerItems[name] = count
end)

---@diagnostic disable-next-line: duplicate-set-field
function utils.hasPlayerGotGroup(filter)
    local _type = type(filter)
    for i = 1, #groups do
        local group = groups[i]

        if _type == 'string' then
            local data = playerGroups[group]

            if filter == data?.name then
                return true
            end
        elseif _type == 'table' then
            local tabletype = table.type(filter)

            if tabletype == 'hash' then
                for name, grade in pairs(filter) do
                    local data = playerGroups[group]

                    if data?.name == name and grade <= data.grade then
                        return true
                    end
                end
            elseif tabletype == 'array' then
                for j = 1, #filter do
                    local name = filter[j]
                    local data = playerGroups[group]

                    if data?.name == name then
                        return true
                    end
                end
            end
        end
    end
end


================================================
FILE: client/framework/nd.lua
================================================
local NDCore = exports["ND_Core"]

local playerGroups = NDCore:getPlayer()?.groups or {}

RegisterNetEvent("ND:characterLoaded", function(data)
    playerGroups = data.groups
end)

RegisterNetEvent("ND:updateCharacter", function(data)
    if source == '' then return end
    playerGroups = data.groups or {}
end)

local utils = require 'client.utils'

---@diagnostic disable-next-line: duplicate-set-field
function utils.hasPlayerGotGroup(filter)
    local _type = type(filter)

    if _type == 'string' then
        local group = playerGroups[filter]

        if group then
            return true
        end
    elseif _type == 'table' then
        local tabletype = table.type(filter)

        if tabletype == 'hash' then
            for name, grade in pairs(filter) do
                local playerGrade = playerGroups[name]?.rank

                if playerGrade and grade <= playerGrade then
                    return true
                end
            end
        elseif tabletype == 'array' then
            for i = 1, #filter do
                local name = filter[i]
                local group = playerGroups[name]

                if group then
                    return true
                end
            end
        end
    end
end


================================================
FILE: client/framework/ox.lua
================================================
if not lib.checkDependency('ox_core', '0.21.3', true) then return end

local Ox = require '@ox_core.lib.init' --[[@as OxClient]]
local utils = require 'client.utils'
local player = Ox.GetPlayer()

---@diagnostic disable-next-line: duplicate-set-field
function utils.hasPlayerGotGroup(filter)
    return player.getGroup(filter)
end


================================================
FILE: client/framework/qbx.lua
================================================
if not lib.checkDependency('qbx_core', '1.18.0', true) then return end

local QBX = exports.qbx_core
local utils = require 'client.utils'

---@diagnostic disable-next-line: duplicate-set-field
function utils.hasPlayerGotGroup(filter)
    return QBX:HasGroup(filter)
end


================================================
FILE: client/main.lua
================================================
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)


================================================
FILE: client/state.lua
================================================
local state = {}

local isActive = false

---@return boolean
function state.isActive()
    return isActive
end

---@param value boolean
function state.setActive(value)
    isActive = value

    if value then
        SendNuiMessage('{"event": "visible", "state": true}')
    end
end

local nuiFocus = false

---@return boolean
function state.isNuiFocused()
    return nuiFocus
end

---@param value boolean
function state.setNuiFocus(value, cursor)
    if value then SetCursorLocation(0.5, 0.5) end

    nuiFocus = value
    SetNuiFocus(value, cursor or false)
    SetNuiFocusKeepInput(value)
end

local isDisabled = false

---@return boolean
function state.isDisabled()
    return isDisabled
end

---@param value boolean
function state.setDisabled(value)
    isDisabled = value
end

return state


================================================
FILE: client/utils.lua
================================================
local utils = {}

local GetWorldCoordFromScreenCoord = GetWorldCoordFromScreenCoord
local StartShapeTestLosProbe = StartShapeTestLosProbe
local GetShapeTestResultIncludingMaterial = GetShapeTestResultIncludingMaterial

---@param flag number
---@return boolean hit
---@return number entityHit
---@return vector3 endCoords
---@return vector3 surfaceNormal
---@return number materialHash
function utils.raycastFromCamera(flag)
    local coords, normal = GetWorldCoordFromScreenCoord(0.5, 0.5)
    local destination = coords + normal * 10
    local handle = StartShapeTestLosProbe(coords.x, coords.y, coords.z, destination.x, destination.y, destination.z,
        flag, cache.ped, 4)

    while true do
        Wait(0)
        local retval, hit, endCoords, surfaceNormal, materialHash, entityHit = GetShapeTestResultIncludingMaterial(
        handle)

        if retval ~= 1 then
            ---@diagnostic disable-next-line: return-type-mismatch
            return hit, entityHit, endCoords, surfaceNormal, materialHash
        end
    end
end

function utils.getTexture()
    return lib.requestStreamedTextureDict('shared'), 'emptydot_32'
end

-- SetDrawOrigin is limited to 32 calls per frame. Set as 0 to disable.
local drawZoneSprites = GetConvarInt('ox_target:drawSprite', 24)
local SetDrawOrigin = SetDrawOrigin
local DrawSprite = DrawSprite
local ClearDrawOrigin = ClearDrawOrigin
local colour = vector(155, 155, 155, 175)
local hover = vector(98, 135, 236, 255)
local currentZones = {}
local previousZones = {}
local drawZones = {}
local drawN = 0
local width = 0.02
local height = width * GetAspectRatio(false)

if drawZoneSprites == 0 then drawZoneSprites = -1 end

---@param coords vector3
---@return CZone[], boolean
function utils.getNearbyZones(coords)
    if not Zones then return currentZones, false end

    local n = 0
    local nearbyZones = lib.zones.getNearbyZones()
    drawN = 0
    previousZones, currentZones = currentZones, table.wipe(previousZones)

    for i = 1, #nearbyZones do
        local zone = nearbyZones[i]
        local contains = zone:contains(coords)

        if contains then
            n += 1
            currentZones[n] = zone
        end

        if drawN <= drawZoneSprites and zone.drawSprite ~= false and (contains or (zone.distance or 7) < 7) then
            drawN += 1
            drawZones[drawN] = zone
            zone.colour = contains and hover or nil
        end
    end

    local previousN = #previousZones

    if n ~= previousN then
        return currentZones, true
    end

    if n > 0 then
        for i = 1, n do
            local zoneA = currentZones[i]
            local found = false

            for j = 1, previousN do
                local zoneB = previousZones[j]

                if zoneA == zoneB then
                    found = true
                    break
                end
            end

            if not found then
                return currentZones, true
            end
        end
    end

    return currentZones, false
end

function utils.drawZoneSprites(dict, texture)
    if drawN == 0 then return end

    for i = 1, drawN do
        local zone = drawZones[i]
        local spriteColour = zone.colour or colour

        if zone.drawSprite ~= false then
            SetDrawOrigin(zone.coords.x, zone.coords.y, zone.coords.z)
            DrawSprite(dict, texture, 0, 0, width, height, 0, spriteColour.r, spriteColour.g, spriteColour.b,
                spriteColour.a)
        end
    end

    ClearDrawOrigin()
end

function utils.hasExport(export)
    local resource, exportName = string.strsplit('.', export)

    return pcall(function()
        return exports[resource][exportName]
    end)
end

local playerItems = {}

function utils.getItems()
    return playerItems
end

---@param filter string | string[] | table<string, number>
---@param hasAny boolean?
---@return boolean
function utils.hasPlayerGotItems(filter, hasAny)
    if not playerItems then return true end

    local _type = type(filter)

    if _type == 'string' then
        return (playerItems[filter] or 0) > 0
    elseif _type == 'table' then
        local tabletype = table.type(filter)

        if tabletype == 'hash' then
            for name, amount in pairs(filter) do
                local hasItem = (playerItems[name] or 0) >= amount

                if hasAny then
                    if hasItem then return true end
                elseif not hasItem then
                    return false
                end
            end
        elseif tabletype == 'array' then
            for i = 1, #filter do
                local hasItem = (playerItems[filter[i]] or 0) > 0

                if hasAny then
                    if hasItem then return true end
                elseif not hasItem then
                    return false
                end
            end
        end
    end

    return not hasAny
end

---stub
---@param filter string | string[] | table<string, number>
---@return boolean
function utils.hasPlayerGotGroup(filter)
    return true
end

SetTimeout(0, function()
    if utils.hasExport('ox_inventory.Items') then
        setmetatable(playerItems, {
            __index = function(self, index)
                self[index] = exports.ox_inventory:Search('count', index) or 0
                return self[index]
            end
        })

        AddEventHandler('ox_inventory:itemCount', function(name, count)
            playerItems[name] = count
        end)
    end

    if utils.hasExport('ox_core.GetPlayer') then
        require 'client.framework.ox'
    elseif utils.hasExport('es_extended.getSharedObject') then
        require 'client.framework.esx'
    elseif utils.hasExport('qbx_core.HasGroup') then
        require 'client.framework.qbx'
    elseif utils.hasExport('ND_Core.getPlayer') then
        require 'client.framework.nd'
    end
end)

function utils.warn(msg)
    local trace = Citizen.InvokeNative(`FORMAT_STACK_TRACE` & 0xFFFFFFFF, nil, 0, Citizen.ResultAsString())
    local _, _, src = string.strsplit('\n', trace, 4)

    warn(('%s ^0%s\n'):format(msg, src:gsub(".-%(", '(')))
end

return utils


================================================
FILE: fxmanifest.lua
================================================
-- FX Information
fx_version 'cerulean'
use_experimental_fxv2_oal 'yes'
nui_callback_strict_mode 'true'
lua54 'yes'
game 'gta5'

-- Resource Information
name 'ox_target'
author 'Overextended'
version '1.18.1'
repository 'https://github.com/overextended/ox_target'
description ''

-- Manifest
ui_page 'web/index.html'

shared_scripts {
    '@ox_lib/init.lua',
}

client_scripts {
    'client/main.lua',
}

server_scripts {
    'server/main.lua'
}

files {
    'web/**',
    'locales/*.json',
    'client/api.lua',
    'client/utils.lua',
    'client/state.lua',
    'client/debug.lua',
    'client/defaults.lua',
    'client/framework/nd.lua',
    'client/framework/ox.lua',
    'client/framework/esx.lua',
    'client/framework/qbx.lua',
    'client/compat/qtarget.lua',
}

provide 'qtarget'

dependency 'ox_lib'


================================================
FILE: locales/cs.json
================================================
{
  "toggle_front_driver_door": "Přední dveře řidiče",
  "toggle_front_passenger_door": "Přední dveře spolujezdce",
  "toggle_rear_driver_door": "Zadní dveře řidiče",
  "toggle_rear_passenger_door": "Zadní dveře spolujezdce",
  "toggle_hood": "Kapota",
  "toggle_trunk": "Kufr",
  "debug_box": "(Debug) Box",
  "debug_sphere": "(Debug) Koule",
  "debug_police_car": "Policejní auto",
  "debug_ped": "(Debug) Ped",
  "debug_vehicle": "(Debug) Vozidlo",
  "debug_object": "(Debug) Objekt",
  "toggle_targeting": "Výběr zaměření",
  "go_back": "Zpět"
}


================================================
FILE: locales/da.json
================================================
{
  "toggle_front_driver_door": "Skift forreste førerdør",
  "toggle_front_passenger_door": "Skift forreste passagerdør",
  "toggle_rear_driver_door": "Skift bageste førerdør",
  "toggle_rear_passenger_door": "Skift bageste passagerdør",
  "toggle_hood": "Skift motorhjelm",
  "toggle_trunk": "Skift bagagerum",
  "debug_box": "(Debug) Boks",
  "debug_sphere": "(Debug) Kugle",
  "debug_police_car": "Politibil",
  "debug_ped": "(Debug) Person",
  "debug_vehicle": "(Debug) Køretøj",
  "debug_object": "(Debug) Objekt",
  "debug_global": "(Debug) Global",
  "toggle_targeting": "Skift målretning",
  "go_back": "Gå tilbage"
}


================================================
FILE: locales/de.json
================================================
{
  "toggle_front_driver_door": "Vordere Fahrertür umschalten",
  "toggle_front_passenger_door": "Vordere Beifahrertür umschalten",
  "toggle_rear_driver_door": "Hintere Fahrertür umschalten",
  "toggle_rear_passenger_door": "Hintere Beifahrertür umschalten",
  "toggle_hood": "Motorhaube umschalten",
  "toggle_trunk": "Kofferraum umschalten",
  "debug_box": "(Debug) Box",
  "debug_sphere": "(Debug) Sphäre",
  "debug_police_car": "Polizeiauto",
  "debug_ped": "(Debug) Ped",
  "debug_vehicle": "(Debug) Fahrzeug",
  "debug_object": "(Debug) Objekt",
  "toggle_targeting": "Zielen umschalten"
}


================================================
FILE: locales/en.json
================================================
{
  "toggle_front_driver_door": "Toggle front driver door",
  "toggle_front_passenger_door": "Toggle front passenger door",
  "toggle_rear_driver_door": "Toggle rear driver door",
  "toggle_rear_passenger_door": "Toggle rear passenger door",
  "toggle_hood": "Toggle hood",
  "toggle_trunk": "Toggle trunk",
  "debug_box": "(Debug) Box",
  "debug_sphere": "(Debug) Sphere",
  "debug_police_car": "Police car",
  "debug_ped": "(Debug) Ped",
  "debug_vehicle": "(Debug) Vehicle",
  "debug_object": "(Debug) Object",
  "debug_global": "(Debug) Global",
  "toggle_targeting": "Toggle targeting",
  "go_back": "Go back"
}


================================================
FILE: locales/es.json
================================================
{
  "toggle_front_driver_door": "Abrir/Cerrar puerta delantera del conductor",
  "toggle_front_passenger_door": "Abrir/Cerrar puerta delantera del pasajero",
  "toggle_rear_driver_door": "Abrir/Cerrar puerta trasera del conductor",
  "toggle_rear_passenger_door": "Abrir/Cerrar puerta trasera del pasajero",
  "toggle_hood": "Abrir/Cerrar capó",
  "toggle_trunk": "Abrir/Cerrar maletero",
  "debug_box": "(Debug) Caja",
  "debug_sphere": "(Debug) Esfera",
  "debug_police_car": "Coche de policía",
  "debug_ped": "(Debug) Peatón",
  "debug_vehicle": "(Debug) Vehículo",
  "debug_object": "(Debug) Objeto",
  "toggle_targeting": "Activar/Desactivar apuntado"
}


================================================
FILE: locales/et.json
================================================
{
  "toggle_front_driver_door": "Ava/sulge eesuks",
  "toggle_front_passenger_door": "Ava/sulge eesuks",
  "toggle_rear_driver_door": "Ava/sulge tagauks",
  "toggle_rear_passenger_door": "Ava/sulge tagauks",
  "toggle_hood": "Ava/sulge kapott",
  "toggle_trunk": "Ava/sulge pagasiruum",
  "debug_box": "(Debug) Kast",
  "debug_sphere": "(Debug) Kera",
  "debug_police_car": "Politseiauto",
  "debug_ped": "(Debug) Ped",
  "debug_vehicle": "(Debug) Sõiduk",
  "debug_object": "(Debug) Objekt",
  "toggle_targeting": "Näita kolmandat silma",
  "go_back": "Mine tagasi"
}

================================================
FILE: locales/fi.json
================================================
{
  "toggle_front_driver_door": "Avaa/Sulje kuljettajan etuovi",
  "toggle_front_passenger_door": "Avaa/Sulje repsikan etuovi",
  "toggle_rear_driver_door": "Avaa/Sulje kuljettajan takaovi",
  "toggle_rear_passenger_door": "Avaa/Sulje repsikan takaovi",
  "toggle_hood": "Avaa/Sulje konepelti",
  "toggle_trunk": "Avaa/Sulje takakontti",
  "debug_box": "(Debug) Laatikko",
  "debug_sphere": "(Debug) Ympyrä",
  "debug_police_car": "Poliisiauto",
  "debug_ped": "(Debug) Ped",
  "debug_vehicle": "(Debug) Ajoneuvo",
  "debug_object": "(Debug) Objekti",
  "toggle_targeting": "Päällä/Pois tähtäys"
}


================================================
FILE: locales/fr.json
================================================
{
  "toggle_front_driver_door": "Porte conducteur avant",
  "toggle_front_passenger_door": "Porte passager avant",
  "toggle_rear_driver_door": "Porte conducteur arrière",
  "toggle_rear_passenger_door": "Porte passager arrière",
  "toggle_hood": "Capot",
  "toggle_trunk": "Coffre",
  "debug_box": "(Debug) Box",
  "debug_sphere": "(Debug) Sphère",
  "debug_police_car": "Véhicule de police",
  "debug_ped": "(Debug) Ped",
  "debug_vehicle": "(Debug) Véhicule",
  "debug_object": "(Debug) Objet",
  "toggle_targeting": "Afficher le système d'interaction",
  "go_back": "Retour"
}


================================================
FILE: locales/hr.json
================================================
{
  "toggle_front_driver_door": "Prednja lijeva vrata",
  "toggle_front_passenger_door": "Prednja desna vrata",
  "toggle_rear_driver_door": "Zadnja lijeva vrata",
  "toggle_rear_passenger_door": "Zadnja desna vrata",
  "toggle_hood": "Hauba",
  "toggle_trunk": "Gepek",
  "debug_box": "(Debug) Kocka (Box)",
  "debug_sphere": "(Debug) Sfera (Sphere)",
  "debug_police_car": "Policijski auto",
  "debug_ped": "(Debug) Ped",
  "debug_vehicle": "(Debug) Auto",
  "debug_object": "(Debug) Objekt",
  "toggle_targeting": "Upali/Ugasi Target sistem"
}


================================================
FILE: locales/hu.json
================================================
{
"toggle_front_driver_door": "Sofőroldali ajtó nyitása/zárása",
"toggle_front_passenger_door": "Anyósülés oldali ajtó nyitása/zárása",
"toggle_rear_driver_door": "Sofőroldali hátsó ajtó nyitása/zárása",
"toggle_rear_passenger_door": "Anyósülés oldali hátsó ajtó nyitása/zárása",
"toggle_hood": "Motorháztető nyitása/zárása",
"toggle_trunk": "Csomagtartó nyitása/zárása",
"debug_box": "(Hibakeresés) Doboz",
"debug_sphere": "(Hibakeresés) Gömb",
"debug_police_car": "(Hibakeresés) Rendőrautó",
"debug_ped": "(Hibakeresés) Entitás",
"debug_vehicle": "(Hibakeresés) Jármű",
"debug_object": "(Hibakeresés) Objekt",
"toggle_targeting": "Célzó be- és kikapcsolása",
"go_back": "Vissza"
}


================================================
FILE: locales/id.json
================================================
{
  "toggle_front_driver_door": "Tombol pintu pengemudi depan",
  "toggle_front_passenger_door": "Tombol pintu penumpang depan",
  "toggle_rear_driver_door": "Tombol pintu pengemudi belakang",
  "toggle_rear_passenger_door": "Tombol pintu penumpang belakang",
  "toggle_hood": "Tombol kap",
  "toggle_trunk": "Tombol bagasi",
  "debug_box": "(Debug) Kotak",
  "debug_sphere": "(Debug) Bola",
  "debug_police_car": "Mobil polisi",
  "debug_ped": "(Debug) Ped",
  "debug_vehicle": "(Debug) Kendaraan",
  "debug_object": "(Debug) Objek",
  "toggle_targeting": "Tombol penargetan"
}


================================================
FILE: locales/it.json
================================================
{
  "toggle_front_driver_door": "Apri/Chiudi portiera anteriore sinistra",
  "toggle_front_passenger_door": "Apri/Chiudi portiera anteriore destra",
  "toggle_rear_driver_door": "Apri/Chiudi portiera posteriore sinistra",
  "toggle_rear_passenger_door": "Apri/Chiudi portiera posteriore destra",
  "toggle_hood": "Apri/Chiudi Cofano",
  "toggle_trunk": "Apri/Chiudi Bagagliaio",
  "debug_box": "(Debug) Box",
  "debug_sphere": "(Debug) Sphere",
  "debug_police_car": "Auto Polizia",
  "debug_ped": "(Debug) Ped",
  "debug_vehicle": "(Debug) Vehicle",
  "debug_object": "(Debug) Object",
  "toggle_targeting": "Apri/Chiudi targeting",
  "go_back": "Indietro"
}


================================================
FILE: locales/nl.json
================================================
{
  "toggle_front_driver_door": "Toggle bestuurdersdeur",
  "toggle_front_passenger_door": "Toggle bijrijdersdeur",
  "toggle_rear_driver_door": "Toggle achterdeur links",
  "toggle_rear_passenger_door": "Toggle achterdeur rechts",
  "toggle_hood": "Toggle motorkap",
  "toggle_trunk": "Toggle kofferbak",
  "debug_box": "(Debug) Doos",
  "debug_sphere": "(Debug) Bol",
  "debug_police_car": "Politie voertuig",
  "debug_ped": "(Debug) Ped",
  "debug_vehicle": "(Debug) Voertuig",
  "debug_object": "(Debug) Object",
  "toggle_targeting": "Toggle richten"
}


================================================
FILE: locales/no.json
================================================
{
  "toggle_front_driver_door": "Åpne/lukke førerdør",
  "toggle_front_passenger_door": "Åpne/lukke passasjerdør",
  "toggle_rear_driver_door": "Åpne/lukke venstre bakdør",
  "toggle_rear_passenger_door": "Åpne/lukke høyre bakdør",
  "toggle_hood": "Åpne/lukke panser",
  "toggle_trunk": "Åpne/lukke bagasjerom",
  "toggle_targeting": "Sikt",
  "go_back": "Tilbake"
}


================================================
FILE: locales/pl.json
================================================
{
  "toggle_front_driver_door": "Przednie drzwi kierowcy",
  "toggle_front_passenger_door": "Przednie drzwi pasażera",
  "toggle_rear_driver_door": "Tylne drzwi kierowcy",
  "toggle_rear_passenger_door": "Tylne drzwi pasażera",
  "toggle_hood": "Maska",
  "toggle_trunk": "Bagażnik",
  "debug_box": "(Debug) Blok",
  "debug_sphere": "(Debug) Kula",
  "debug_police_car": "Radiowóz",
  "debug_ped": "(Debug) Ped",
  "debug_vehicle": "(Debug) Pojazd",
  "debug_object": "(Debug) Obiekt",
  "toggle_targeting": "Przełącz celowanie",
  "go_back": "Wróć"
}


================================================
FILE: locales/pt-br.json
================================================
{
  "toggle_front_driver_door": "Abrir a porta do motorista dianteira",
  "toggle_front_passenger_door": "Abrir a porta dianteira do passageiro",
  "toggle_rear_driver_door": "Abrir a porta traseira do motorista",
  "toggle_rear_passenger_door": "Abrir a porta traseira do passageiro",
  "toggle_hood": "Abrir o capô",
  "toggle_trunk": "Abrir o porta-malas",
  "debug_box": "(Debug) Caixa",
  "debug_sphere": "(Debug) Esfera",
  "debug_police_car": "Carro de polícia",
  "debug_ped": "(Debug) Ped",
  "debug_vehicle": "(Debug) Veículo",
  "debug_object": "(Debug) Objeto",
  "toggle_targeting": "Alternar mira",
  "go_back": "Voltar"
}


================================================
FILE: locales/pt.json
================================================
{
  "toggle_front_driver_door": "Abrir a porta do motorista dianteira",
  "toggle_front_passenger_door": "Abrir a porta dianteira do passageiro",
  "toggle_rear_driver_door": "Abrir a porta traseira do motorista",
  "toggle_rear_passenger_door": "Abrir a porta traseira do passageiro",
  "toggle_hood": "Abrir o capô",
  "toggle_trunk": "Abrir o porta-malas",
  "debug_box": "(Debug) Caixa",
  "debug_sphere": "(Debug) Esfera",
  "debug_police_car": "Carro de polícia",
  "debug_ped": "(Debug) Ped",
  "debug_vehicle": "(Debug) Veículo",
  "debug_object": "(Debug) Objeto",
  "toggle_targeting": "Alternar mira",
  "go_back": "Voltar"
}


================================================
FILE: locales/ro.json
================================================
{
  "toggle_front_driver_door": "Comută ușa șoferului",
  "toggle_front_passenger_door": "Comută ușa pasagerului",
  "toggle_rear_driver_door": "Comută ușa din spatele șoferului",
  "toggle_rear_passenger_door": "Comută ușa pasagerului din spate",
  "toggle_hood": "Comută capota",
  "toggle_trunk": "Comută portbagajul",
  "debug_box": "(Debug) Cutie",
  "debug_sphere": "(Debug) Sferă",
  "debug_police_car": "Mașină de poliție",
  "debug_ped": "(Debug) Pedestrian",
  "debug_vehicle": "(Debug) Vehicul",
  "debug_object": "(Debug) Obiect",
  "debug_global": "(Debug) Global",
  "toggle_targeting": "Foloseste ochiul",
  "go_back": "Întoarce-te"
}


================================================
FILE: locales/sl.json
================================================
{
  "toggle_front_driver_door": "Odpri/Zapri leva sprednja vrata",
  "toggle_front_passenger_door": "Odpri/Zapri desna sprednja vrata",
  "toggle_rear_driver_door": "Odpri/Zapri leva zadnja vrata",
  "toggle_rear_passenger_door": "Odpri/Zapri desna zadnja vrata",
  "toggle_hood": "Odpri/Zapri pokrov motorja",
  "toggle_trunk": "Odpri/Zapri prtljažnik",
  "debug_box": "(Debug) Kvadrat",
  "debug_sphere": "(Debug) Krog",
  "debug_police_car": "Policijsko vozilo",
  "debug_ped": "(Debug) Pešec",
  "debug_vehicle": "(Debug) Vozilo",
  "debug_object": "(Debug) Predmet",
  "toggle_targeting": "Vklopi/Izklopi tretje oko"
  }


================================================
FILE: locales/sv.json
================================================
{
  "toggle_front_driver_door": "Öppna/stäng främre förardörr",
  "toggle_front_passenger_door": "Öppna/stäng främre passagerardörr",
  "toggle_rear_driver_door": "Öppna/stäng bakre förardörr",
  "toggle_rear_passenger_door": "Öppna/stäng bakre passagerardörr",
  "toggle_hood": "Öppna/stäng motorhuv",
  "toggle_trunk": "Öppna/stäng bagagelucka",
  "debug_box": "(Debug) Låda",
  "debug_sphere": "(Debug) Sfär",
  "debug_police_car": "Polisbil",
  "debug_ped": "(Debug) Ped",
  "debug_vehicle": "(Debug) Fordon",
  "debug_object": "(Debug) Objekt",
  "debug_global": "(Debug) Global",
  "toggle_targeting": "Växla Target",
  "go_back": "Gå tillbaka"
}


================================================
FILE: locales/tr.json
================================================
{
  "toggle_front_driver_door": "Ön sürücü tarafı kapısını aç/kapa",
  "toggle_front_passenger_door": "Ön yolcu tarafı kapısını aç/kapa",
  "toggle_rear_driver_door": "Arka sürücü tarafı kapısını aç/kapa",
  "toggle_rear_passenger_door": "Arka yolcu tarafı kapısını aç/kapa",
  "toggle_hood": "Kaputu aç/kapa",
  "toggle_trunk": "Bagajı aç/kapa",
  "debug_box": "(Debug) Kutu",
  "debug_sphere": "(Debug) Küre",
  "debug_police_car": "Polis arabası",
  "debug_ped": "(Debug) Ped",
  "debug_vehicle": "(Debug) Araç",
  "debug_object": "(Debug) Nesne",
  "debug_global": "(Debug) Genel",
  "toggle_targeting": "Hedeflemeyi aç/kapa",
  "go_back": "Geri dön"
}


================================================
FILE: locales/zh-cn.json
================================================
{
  "toggle_front_driver_door": "开关左前车门",
  "toggle_front_passenger_door": "开关左后车门",
  "toggle_rear_driver_door": "开关右前车门",
  "toggle_rear_passenger_door": "开关右后车门",
  "toggle_hood": "打开引擎盖",
  "toggle_trunk": "打开后备箱",
  "debug_box": "(Debug) 矩形区域",
  "debug_sphere": "(Debug) 球形区域",
  "debug_police_car": "警车",
  "debug_ped": "(Debug) 角色实体",
  "debug_vehicle": "(Debug) 车辆",
  "debug_object": "(Debug) 物体",
  "debug_global": "(Debug) 全局对象",
  "toggle_targeting": "交互菜单",
  "go_back": "返回"
}


================================================
FILE: locales/zh-tw.json
================================================
{
  "toggle_front_driver_door": "開關左前車門",
  "toggle_front_passenger_door": "開關左後車門",
  "toggle_rear_driver_door": "開關右前車門",
  "toggle_rear_passenger_door": "開關右後車門",
  "toggle_hood": "打開引擎蓋",
  "toggle_trunk": "打開後備箱",
  "debug_box": "(Debug) 矩形區域",
  "debug_sphere": "(Debug) 球形區域",
  "debug_police_car": "警車",
  "debug_ped": "(Debug) 角色實體",
  "debug_vehicle": "(Debug) 車輛",
  "debug_object": "(Debug) 物體",
  "debug_global": "(Debug) 全局對象",
  "toggle_targeting": "交互菜單",
  "go_back": "返回"
}


================================================
FILE: server/main.lua
================================================
lib.versionCheck('overextended/ox_target')

if not lib.checkDependency('ox_lib', '3.30.0', true) then return end

---@type table<number, EntityInterface>
local entityStates = {}

---@param netId number
RegisterNetEvent('ox_target:setEntityHasOptions', function(netId)
    local entity = Entity(NetworkGetEntityFromNetworkId(netId))
    entity.state.hasTargetOptions = true
    entityStates[netId] = entity
end)

---@param netId number
---@param door number
RegisterNetEvent('ox_target:toggleEntityDoor', function(netId, door)
    local entity = NetworkGetEntityFromNetworkId(netId)
    if not DoesEntityExist(entity) then return end

    local owner = NetworkGetEntityOwner(entity)
    TriggerClientEvent('ox_target:toggleEntityDoor', owner, netId, door)
end)

CreateThread(function()
    local arr = {}
    local num = 0

    while true do
        Wait(10000)

        for netId, entity in pairs(entityStates) do
            if not DoesEntityExist(entity.__data) or not entity.state.hasTargetOptions then
                entityStates[netId] = nil
                num += 1

                arr[num] = netId
            end
        end

        if num > 0 then
            TriggerClientEvent('ox_target:removeEntity', -1, arr)
            table.wipe(arr)

            num = 0
        end
    end
end)


================================================
FILE: web/index.html
================================================
<html>
  <head>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free/css/all.min.css" />
    <link href="style.css" rel="stylesheet" type="text/css" />
  </head>
  <body>
    <div id="eye">
      <svg id="eyeSvg" xmlns="http://www.w3.org/2000/svg" height="36px" viewBox="0 0 24 24" width="36px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 4C7 4 2.73 7.11 1 11.5 2.73 15.89 7 19 12 19s9.27-3.11 11-7.5C21.27 7.11 17 4 12 4zm0 12.5c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>
    </div>
    <div id="options-wrapper"></div>
    <script defer type="module" src="js/main.js"></script>
  </body>
</html>


================================================
FILE: web/js/createOptions.js
================================================
import { fetchNui } from "./fetchNui.js";

const optionsWrapper = document.getElementById("options-wrapper");

function onClick() {
  // when nuifocus is disabled after a click, the hover event is never released
  this.style.pointerEvents = "none";

  fetchNui("select", [this.targetType, this.targetId, this.zoneId]);
  // is there a better way to handle this? probably
  setTimeout(() => (this.style.pointerEvents = "auto"), 100);
}

export function createOptions(type, data, id, zoneId) {
  if (data.hide) return;

  const option = document.createElement("div");
  const iconElement = `<i class="fa-fw ${data.icon} option-icon" ${
    data.iconColor ? `style = color:${data.iconColor} !important` : null
  }"></i>`;

  option.innerHTML = `${iconElement}<p class="option-label">${data.label}</p>`;
  option.className = "option-container";
  option.targetType = type;
  option.targetId = id;
  option.zoneId = zoneId;

  option.addEventListener("click", onClick);
  optionsWrapper.appendChild(option);
}


================================================
FILE: web/js/fetchNui.js
================================================
const resource = GetParentResourceName();

export async function fetchNui(eventName, data) {
  const resp = await fetch(`https://${resource}/${eventName}`, {
    method: 'post',
    headers: {
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: JSON.stringify(data),
  });

  return await resp.json();
}


================================================
FILE: web/js/main.js
================================================
import { createOptions } from "./createOptions.js";

const optionsWrapper = document.getElementById("options-wrapper");
const body = document.body;
const eye = document.getElementById("eyeSvg");

window.addEventListener("message", (event) => {
  switch (event.data.event) {
    case "visible": {
      optionsWrapper.innerHTML = "";
      body.style.visibility = event.data.state ? "visible" : "hidden";
      return eye.classList.remove("eye-hover");
    }

    case "leftTarget": {
      optionsWrapper.innerHTML = "";
      return eye.classList.remove("eye-hover");
    }

    case "setTarget": {
      optionsWrapper.innerHTML = "";
      eye.classList.add("eye-hover");

      if (event.data.options) {
        for (const type in event.data.options) {
          event.data.options[type].forEach((data, id) => {
            createOptions(type, data, id + 1);
          });
        }
      }

      if (event.data.zones) {
        for (let i = 0; i < event.data.zones.length; i++) {
          event.data.zones[i].forEach((data, id) => {
            createOptions("zones", data, id + 1, i + 1);
          });
        }
      }
    }
  }
});


================================================
FILE: web/style.css
================================================
@import url("https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap");

:root {
  --color-default: #cfd2da;
  --color-hover: white;
}

body {
  visibility: hidden;
  user-select: none;
  white-space: nowrap;
  margin: 0;
  user-select: none;
  overflow: hidden;
}

p {
  margin: 0;
}

.material-symbols-outlined {
  font-variation-settings: "FILL" 0, "wght" 300, "GRAD" 0, "opsz" 40;
}

#eye {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: 22pt;
  fill: black;
}

.eye-hover {
  fill: var(--color-default);
}

#options-wrapper {
  position: absolute;
  top: calc(48.4%);
  left: calc(50% + 18pt);
}

.option-container {
  color: var(--color-default);
  display: flex;
  flex-direction: row;
  justify-content: flex-start;
  align-items: center;
  font-family: "Nunito";
  background: linear-gradient(
    90deg,
    rgba(20, 20, 20, 0.7) 0%,
    rgba(20, 20, 20, 0.6) 66%,
    rgba(47, 48, 53, 0) 100%
  );
  font-size: 11pt;
  line-height: 22pt;
  vertical-align: middle;
  margin: 2pt;
  transition: 300ms;
  transform-origin: left top;
  scale: 1;
  height: 22pt;
  width: 150pt;
  top: 0;
}

.option-container:hover {
  background: linear-gradient(
    90deg,
    rgba(30, 30, 30, 0.7) 0%,
    rgba(30, 30, 30, 0.6) 66%,
    rgba(57, 58, 63, 0) 100%
  );
  transform-origin: left top;
  color: var(--color-hover);
  margin-left: 4pt;
}

.option-icon {
  font-size: 12pt;
  line-height: 22pt;
  width: 14pt;
  margin: 5pt;
  color: var(--color-default);
}

.option-label {
  font-weight: 500;
}
Download .txt
gitextract_ba2qc7hr/

├── .github/
│   ├── actions/
│   │   └── bump-manifest-version.js
│   └── workflows/
│       └── release.yml
├── LICENSE
├── README.md
├── client/
│   ├── api.lua
│   ├── compat/
│   │   └── qtarget.lua
│   ├── debug.lua
│   ├── defaults.lua
│   ├── framework/
│   │   ├── esx.lua
│   │   ├── nd.lua
│   │   ├── ox.lua
│   │   └── qbx.lua
│   ├── main.lua
│   ├── state.lua
│   └── utils.lua
├── fxmanifest.lua
├── locales/
│   ├── cs.json
│   ├── da.json
│   ├── de.json
│   ├── en.json
│   ├── es.json
│   ├── et.json
│   ├── fi.json
│   ├── fr.json
│   ├── hr.json
│   ├── hu.json
│   ├── id.json
│   ├── it.json
│   ├── nl.json
│   ├── no.json
│   ├── pl.json
│   ├── pt-br.json
│   ├── pt.json
│   ├── ro.json
│   ├── sl.json
│   ├── sv.json
│   ├── tr.json
│   ├── zh-cn.json
│   └── zh-tw.json
├── server/
│   └── main.lua
└── web/
    ├── index.html
    ├── js/
    │   ├── createOptions.js
    │   ├── fetchNui.js
    │   └── main.js
    └── style.css
Download .txt
SYMBOL INDEX (3 symbols across 2 files)

FILE: web/js/createOptions.js
  function onClick (line 5) | function onClick() {
  function createOptions (line 14) | function createOptions(type, data, id, zoneId) {

FILE: web/js/fetchNui.js
  function fetchNui (line 3) | async function fetchNui(eventName, data) {
Condensed preview — 45 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (83K chars).
[
  {
    "path": ".github/actions/bump-manifest-version.js",
    "chars": 302,
    "preview": "const fxManifest = await Bun.file('./fxmanifest.lua').text();\n\nlet newVersion = process.env.TGT_RELEASE_VERSION;\nnewVers"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 1771,
    "preview": "name: Create release\n\non:\n  push:\n    tags:\n      - \"v*.*.*\"\n\njobs:\n  create-release:\n    if: github.actor_id != 2789033"
  },
  {
    "path": "LICENSE",
    "chars": 1069,
    "preview": "MIT License\n\nCopyright (c) 2022 Overextended\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
  },
  {
    "path": "README.md",
    "chars": 1291,
    "preview": "# ox_target\n\n![](https://img.shields.io/github/downloads/overextended/ox_target/total?logo=github)\n![](https://img.shiel"
  },
  {
    "path": "client/api.lua",
    "chars": 14094,
    "preview": "---@class OxTargetOption\n---@field resource? string\n\nlocal utils = require 'client.utils'\n\nlocal api = setmetatable({}, "
  },
  {
    "path": "client/compat/qtarget.lua",
    "chars": 4940,
    "preview": "local function exportHandler(exportName, func)\n    AddEventHandler(('__cfx_export_qtarget_%s'):format(exportName), funct"
  },
  {
    "path": "client/debug.lua",
    "chars": 2255,
    "preview": "AddEventHandler('ox_target:debug', function(data)\n    if data.entity and GetEntityType(data.entity) > 0 then\n        dat"
  },
  {
    "path": "client/defaults.lua",
    "chars": 4419,
    "preview": "if GetConvarInt('ox_target:defaults', 1) ~= 1 then return end\n\nlocal api = require 'client.api'\nlocal GetEntityBoneIndex"
  },
  {
    "path": "client/framework/esx.lua",
    "chars": 2428,
    "preview": "local ESX = exports.es_extended:getSharedObject()\nlocal utils = require 'client.utils'\nlocal groups = { 'job', 'job2' }\n"
  },
  {
    "path": "client/framework/nd.lua",
    "chars": 1251,
    "preview": "local NDCore = exports[\"ND_Core\"]\n\nlocal playerGroups = NDCore:getPlayer()?.groups or {}\n\nRegisterNetEvent(\"ND:character"
  },
  {
    "path": "client/framework/ox.lua",
    "chars": 331,
    "preview": "if not lib.checkDependency('ox_core', '0.21.3', true) then return end\n\nlocal Ox = require '@ox_core.lib.init' --[[@as Ox"
  },
  {
    "path": "client/framework/qbx.lua",
    "chars": 270,
    "preview": "if not lib.checkDependency('qbx_core', '1.18.0', true) then return end\n\nlocal QBX = exports.qbx_core\nlocal utils = requi"
  },
  {
    "path": "client/main.lua",
    "chars": 14302,
    "preview": "if not lib.checkDependency('ox_lib', '3.30.0', true) then return end\n\nlib.locale()\n\nlocal utils = require 'client.utils'"
  },
  {
    "path": "client/state.lua",
    "chars": 795,
    "preview": "local state = {}\n\nlocal isActive = false\n\n---@return boolean\nfunction state.isActive()\n    return isActive\nend\n\n---@para"
  },
  {
    "path": "client/utils.lua",
    "chars": 6109,
    "preview": "local utils = {}\n\nlocal GetWorldCoordFromScreenCoord = GetWorldCoordFromScreenCoord\nlocal StartShapeTestLosProbe = Start"
  },
  {
    "path": "fxmanifest.lua",
    "chars": 813,
    "preview": "-- FX Information\nfx_version 'cerulean'\nuse_experimental_fxv2_oal 'yes'\nnui_callback_strict_mode 'true'\nlua54 'yes'\ngame"
  },
  {
    "path": "locales/cs.json",
    "chars": 550,
    "preview": "{\n  \"toggle_front_driver_door\": \"Přední dveře řidiče\",\n  \"toggle_front_passenger_door\": \"Přední dveře spolujezdce\",\n  \"t"
  },
  {
    "path": "locales/da.json",
    "chars": 626,
    "preview": "{\n  \"toggle_front_driver_door\": \"Skift forreste førerdør\",\n  \"toggle_front_passenger_door\": \"Skift forreste passagerdør\""
  },
  {
    "path": "locales/de.json",
    "chars": 612,
    "preview": "{\r\n  \"toggle_front_driver_door\": \"Vordere Fahrertür umschalten\",\r\n  \"toggle_front_passenger_door\": \"Vordere Beifahrertür"
  },
  {
    "path": "locales/en.json",
    "chars": 634,
    "preview": "{\r\n  \"toggle_front_driver_door\": \"Toggle front driver door\",\r\n  \"toggle_front_passenger_door\": \"Toggle front passenger d"
  },
  {
    "path": "locales/es.json",
    "chars": 660,
    "preview": "{\n  \"toggle_front_driver_door\": \"Abrir/Cerrar puerta delantera del conductor\",\n  \"toggle_front_passenger_door\": \"Abrir/C"
  },
  {
    "path": "locales/et.json",
    "chars": 568,
    "preview": "{\n  \"toggle_front_driver_door\": \"Ava/sulge eesuks\",\n  \"toggle_front_passenger_door\": \"Ava/sulge eesuks\",\n  \"toggle_rear_"
  },
  {
    "path": "locales/fi.json",
    "chars": 598,
    "preview": "{\n  \"toggle_front_driver_door\": \"Avaa/Sulje kuljettajan etuovi\",\n  \"toggle_front_passenger_door\": \"Avaa/Sulje repsikan e"
  },
  {
    "path": "locales/fr.json",
    "chars": 581,
    "preview": "{\n  \"toggle_front_driver_door\": \"Porte conducteur avant\",\n  \"toggle_front_passenger_door\": \"Porte passager avant\",\n  \"to"
  },
  {
    "path": "locales/hr.json",
    "chars": 547,
    "preview": "{\n  \"toggle_front_driver_door\": \"Prednja lijeva vrata\",\n  \"toggle_front_passenger_door\": \"Prednja desna vrata\",\n  \"toggl"
  },
  {
    "path": "locales/hu.json",
    "chars": 699,
    "preview": "{\r\n\"toggle_front_driver_door\": \"Sofőroldali ajtó nyitása/zárása\",\r\n\"toggle_front_passenger_door\": \"Anyósülés oldali ajtó"
  },
  {
    "path": "locales/id.json",
    "chars": 594,
    "preview": "{\r\n  \"toggle_front_driver_door\": \"Tombol pintu pengemudi depan\",\r\n  \"toggle_front_passenger_door\": \"Tombol pintu penumpa"
  },
  {
    "path": "locales/it.json",
    "chars": 660,
    "preview": "{\n  \"toggle_front_driver_door\": \"Apri/Chiudi portiera anteriore sinistra\",\n  \"toggle_front_passenger_door\": \"Apri/Chiudi"
  },
  {
    "path": "locales/nl.json",
    "chars": 558,
    "preview": "{\n  \"toggle_front_driver_door\": \"Toggle bestuurdersdeur\",\n  \"toggle_front_passenger_door\": \"Toggle bijrijdersdeur\",\n  \"t"
  },
  {
    "path": "locales/no.json",
    "chars": 368,
    "preview": "{\n  \"toggle_front_driver_door\": \"Åpne/lukke førerdør\",\n  \"toggle_front_passenger_door\": \"Åpne/lukke passasjerdør\",\n  \"to"
  },
  {
    "path": "locales/pl.json",
    "chars": 552,
    "preview": "{\n  \"toggle_front_driver_door\": \"Przednie drzwi kierowcy\",\n  \"toggle_front_passenger_door\": \"Przednie drzwi pasażera\",\n "
  },
  {
    "path": "locales/pt-br.json",
    "chars": 637,
    "preview": "{\n  \"toggle_front_driver_door\": \"Abrir a porta do motorista dianteira\",\n  \"toggle_front_passenger_door\": \"Abrir a porta "
  },
  {
    "path": "locales/pt.json",
    "chars": 637,
    "preview": "{\n  \"toggle_front_driver_door\": \"Abrir a porta do motorista dianteira\",\n  \"toggle_front_passenger_door\": \"Abrir a porta "
  },
  {
    "path": "locales/ro.json",
    "chars": 650,
    "preview": "{\n  \"toggle_front_driver_door\": \"Comută ușa șoferului\",\n  \"toggle_front_passenger_door\": \"Comută ușa pasagerului\",\n  \"to"
  },
  {
    "path": "locales/sl.json",
    "chars": 626,
    "preview": "{\n  \"toggle_front_driver_door\": \"Odpri/Zapri leva sprednja vrata\",\n  \"toggle_front_passenger_door\": \"Odpri/Zapri desna s"
  },
  {
    "path": "locales/sv.json",
    "chars": 653,
    "preview": "{\n  \"toggle_front_driver_door\": \"Öppna/stäng främre förardörr\",\n  \"toggle_front_passenger_door\": \"Öppna/stäng främre pas"
  },
  {
    "path": "locales/tr.json",
    "chars": 657,
    "preview": "{\n  \"toggle_front_driver_door\": \"Ön sürücü tarafı kapısını aç/kapa\",\n  \"toggle_front_passenger_door\": \"Ön yolcu tarafı k"
  },
  {
    "path": "locales/zh-cn.json",
    "chars": 492,
    "preview": "{\n  \"toggle_front_driver_door\": \"开关左前车门\",\n  \"toggle_front_passenger_door\": \"开关左后车门\",\n  \"toggle_rear_driver_door\": \"开关右前车"
  },
  {
    "path": "locales/zh-tw.json",
    "chars": 509,
    "preview": "{\r\n  \"toggle_front_driver_door\": \"開關左前車門\",\r\n  \"toggle_front_passenger_door\": \"開關左後車門\",\r\n  \"toggle_rear_driver_door\": \"開關"
  },
  {
    "path": "server/main.lua",
    "chars": 1300,
    "preview": "lib.versionCheck('overextended/ox_target')\n\nif not lib.checkDependency('ox_lib', '3.30.0', true) then return end\n\n---@ty"
  },
  {
    "path": "web/index.html",
    "chars": 736,
    "preview": "<html>\n  <head>\n    <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free/css/all.min."
  },
  {
    "path": "web/js/createOptions.js",
    "chars": 1005,
    "preview": "import { fetchNui } from \"./fetchNui.js\";\n\nconst optionsWrapper = document.getElementById(\"options-wrapper\");\n\nfunction "
  },
  {
    "path": "web/js/fetchNui.js",
    "chars": 326,
    "preview": "const resource = GetParentResourceName();\n\nexport async function fetchNui(eventName, data) {\n  const resp = await fetch("
  },
  {
    "path": "web/js/main.js",
    "chars": 1143,
    "preview": "import { createOptions } from \"./createOptions.js\";\n\nconst optionsWrapper = document.getElementById(\"options-wrapper\");\n"
  },
  {
    "path": "web/style.css",
    "chars": 1579,
    "preview": "@import url(\"https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap\");\n\n:root {\n  --color"
  }
]

About this extraction

This page contains the full source code of the overextended/ox_target GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 45 files (74.4 KB), approximately 20.7k tokens, and a symbol index with 3 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!