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 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 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 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 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 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 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 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 ---@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 ---@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 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 ================================================
================================================ 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 = ``; option.innerHTML = `${iconElement}

${data.label}

`; 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; }