├── client ├── framework │ ├── qbx.lua │ ├── ox.lua │ ├── nd.lua │ └── esx.lua ├── state.lua ├── debug.lua ├── defaults.lua ├── compat │ └── qtarget.lua ├── utils.lua ├── main.lua └── api.lua ├── web ├── js │ ├── fetchNui.js │ ├── createOptions.js │ └── main.js ├── index.html └── style.css ├── .github ├── actions │ └── bump-manifest-version.js └── workflows │ └── create-release.yml ├── locales ├── zh-cn.json ├── zh-tw.json ├── hr.json ├── cs.json ├── pl.json ├── nl.json ├── et.json ├── fr.json ├── id.json ├── fi.json ├── de.json ├── sl.json ├── da.json ├── pt.json ├── en.json ├── pt-br.json ├── es.json ├── ro.json ├── it.json ├── tr.json └── hu.json ├── fxmanifest.lua ├── LICENSE ├── README.md └── server └── main.lua /client/framework/qbx.lua: -------------------------------------------------------------------------------- 1 | if not lib.checkDependency('qbx_core', '1.18.0', true) then return end 2 | 3 | local QBX = exports.qbx_core 4 | local utils = require 'client.utils' 5 | 6 | ---@diagnostic disable-next-line: duplicate-set-field 7 | function utils.hasPlayerGotGroup(filter) 8 | return QBX:HasGroup(filter) 9 | end 10 | -------------------------------------------------------------------------------- /client/framework/ox.lua: -------------------------------------------------------------------------------- 1 | if not lib.checkDependency('ox_core', '0.21.3', true) then return end 2 | 3 | local Ox = require '@ox_core.lib.init' --[[@as OxClient]] 4 | local utils = require 'client.utils' 5 | local player = Ox.GetPlayer() 6 | 7 | ---@diagnostic disable-next-line: duplicate-set-field 8 | function utils.hasPlayerGotGroup(filter) 9 | return player.getGroup(filter) 10 | end 11 | -------------------------------------------------------------------------------- /web/js/fetchNui.js: -------------------------------------------------------------------------------- 1 | const resource = GetParentResourceName(); 2 | 3 | export async function fetchNui(eventName, data) { 4 | const resp = await fetch(`https://${resource}/${eventName}`, { 5 | method: 'post', 6 | headers: { 7 | 'Content-Type': 'application/json; charset=UTF-8', 8 | }, 9 | body: JSON.stringify(data), 10 | }); 11 | 12 | return await resp.json(); 13 | } 14 | -------------------------------------------------------------------------------- /.github/actions/bump-manifest-version.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const version = process.env.TGT_RELEASE_VERSION; 4 | const newVersion = version.replace('v', ''); 5 | 6 | const manifestFile = fs.readFileSync('fxmanifest.lua', { encoding: 'utf8' }); 7 | 8 | const newFileContent = manifestFile.replace(/\bversion\s+(.*)$/gm, `version '${newVersion}'`); 9 | 10 | fs.writeFileSync('fxmanifest.lua', newFileContent); 11 | -------------------------------------------------------------------------------- /locales/zh-cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "toggle_front_driver_door": "开关左前车门", 3 | "toggle_front_passenger_door": "开关左后车门", 4 | "toggle_rear_driver_door": "开关右前车门", 5 | "toggle_rear_passenger_door": "开关右后车门", 6 | "toggle_hood": "打开引擎盖", 7 | "toggle_trunk": "打开后备箱", 8 | "debug_box": "(Debug) 矩形区域", 9 | "debug_sphere": "(Debug) 球形区域", 10 | "debug_police_car": "警车", 11 | "debug_ped": "(Debug) 角色实体", 12 | "debug_vehicle": "(Debug) 车辆", 13 | "debug_object": "(Debug) 物体", 14 | "debug_global": "(Debug) 全局对象", 15 | "toggle_targeting": "交互菜单", 16 | "go_back": "返回" 17 | } 18 | -------------------------------------------------------------------------------- /locales/zh-tw.json: -------------------------------------------------------------------------------- 1 | { 2 | "toggle_front_driver_door": "開關左前車門", 3 | "toggle_front_passenger_door": "開關左後車門", 4 | "toggle_rear_driver_door": "開關右前車門", 5 | "toggle_rear_passenger_door": "開關右後車門", 6 | "toggle_hood": "打開引擎蓋", 7 | "toggle_trunk": "打開後備箱", 8 | "debug_box": "(Debug) 矩形區域", 9 | "debug_sphere": "(Debug) 球形區域", 10 | "debug_police_car": "警車", 11 | "debug_ped": "(Debug) 角色實體", 12 | "debug_vehicle": "(Debug) 車輛", 13 | "debug_object": "(Debug) 物體", 14 | "debug_global": "(Debug) 全局對象", 15 | "toggle_targeting": "交互菜單", 16 | "go_back": "返回" 17 | } 18 | -------------------------------------------------------------------------------- /locales/hr.json: -------------------------------------------------------------------------------- 1 | { 2 | "toggle_front_driver_door": "Prednja lijeva vrata", 3 | "toggle_front_passenger_door": "Prednja desna vrata", 4 | "toggle_rear_driver_door": "Zadnja lijeva vrata", 5 | "toggle_rear_passenger_door": "Zadnja desna vrata", 6 | "toggle_hood": "Hauba", 7 | "toggle_trunk": "Gepek", 8 | "debug_box": "(Debug) Kocka (Box)", 9 | "debug_sphere": "(Debug) Sfera (Sphere)", 10 | "debug_police_car": "Policijski auto", 11 | "debug_ped": "(Debug) Ped", 12 | "debug_vehicle": "(Debug) Auto", 13 | "debug_object": "(Debug) Objekt", 14 | "toggle_targeting": "Upali/Ugasi Target sistem" 15 | } 16 | -------------------------------------------------------------------------------- /locales/cs.json: -------------------------------------------------------------------------------- 1 | { 2 | "toggle_front_driver_door": "Přední dveře řidiče", 3 | "toggle_front_passenger_door": "Přední dveře spolujezdce", 4 | "toggle_rear_driver_door": "Zadní dveře řidiče", 5 | "toggle_rear_passenger_door": "Zadní dveře spolujezdce", 6 | "toggle_hood": "Kapota", 7 | "toggle_trunk": "Kufr", 8 | "debug_box": "(Debug) Box", 9 | "debug_sphere": "(Debug) Koule", 10 | "debug_police_car": "Policejní auto", 11 | "debug_ped": "(Debug) Ped", 12 | "debug_vehicle": "(Debug) Vozidlo", 13 | "debug_object": "(Debug) Objekt", 14 | "toggle_targeting": "Výběr zaměření", 15 | "go_back": "Zpět" 16 | } 17 | -------------------------------------------------------------------------------- /locales/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "toggle_front_driver_door": "Przednie drzwi kierowcy", 3 | "toggle_front_passenger_door": "Przednie drzwi pasażera", 4 | "toggle_rear_driver_door": "Tylne drzwi kierowcy", 5 | "toggle_rear_passenger_door": "Tylne drzwi pasażera", 6 | "toggle_hood": "Maska", 7 | "toggle_trunk": "Bagażnik", 8 | "debug_box": "(Debug) Blok", 9 | "debug_sphere": "(Debug) Kula", 10 | "debug_police_car": "Radiowóz", 11 | "debug_ped": "(Debug) Ped", 12 | "debug_vehicle": "(Debug) Pojazd", 13 | "debug_object": "(Debug) Obiekt", 14 | "toggle_targeting": "Przełącz celowanie", 15 | "go_back": "Wróć" 16 | } 17 | -------------------------------------------------------------------------------- /locales/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "toggle_front_driver_door": "Toggle bestuurdersdeur", 3 | "toggle_front_passenger_door": "Toggle bijrijdersdeur", 4 | "toggle_rear_driver_door": "Toggle achterdeur links", 5 | "toggle_rear_passenger_door": "Toggle achterdeur rechts", 6 | "toggle_hood": "Toggle motorkap", 7 | "toggle_trunk": "Toggle kofferbak", 8 | "debug_box": "(Debug) Doos", 9 | "debug_sphere": "(Debug) Bol", 10 | "debug_police_car": "Politie voertuig", 11 | "debug_ped": "(Debug) Ped", 12 | "debug_vehicle": "(Debug) Voertuig", 13 | "debug_object": "(Debug) Object", 14 | "toggle_targeting": "Toggle richten" 15 | } 16 | -------------------------------------------------------------------------------- /locales/et.json: -------------------------------------------------------------------------------- 1 | { 2 | "toggle_front_driver_door": "Ava/sulge eesuks", 3 | "toggle_front_passenger_door": "Ava/sulge eesuks", 4 | "toggle_rear_driver_door": "Ava/sulge tagauks", 5 | "toggle_rear_passenger_door": "Ava/sulge tagauks", 6 | "toggle_hood": "Ava/sulge kapott", 7 | "toggle_trunk": "Ava/sulge pagasiruum", 8 | "debug_box": "(Debug) Kast", 9 | "debug_sphere": "(Debug) Kera", 10 | "debug_police_car": "Politseiauto", 11 | "debug_ped": "(Debug) Ped", 12 | "debug_vehicle": "(Debug) Sõiduk", 13 | "debug_object": "(Debug) Objekt", 14 | "toggle_targeting": "Näita kolmandat silma", 15 | "go_back": "Mine tagasi" 16 | } -------------------------------------------------------------------------------- /locales/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "toggle_front_driver_door": "Porte conducteur avant", 3 | "toggle_front_passenger_door": "Porte passager avant", 4 | "toggle_rear_driver_door": "Porte conducteur arrière", 5 | "toggle_rear_passenger_door": "Porte passager arrière", 6 | "toggle_hood": "Capot", 7 | "toggle_trunk": "Coffre", 8 | "debug_box": "(Debug) Box", 9 | "debug_sphere": "(Debug) Sphère", 10 | "debug_police_car": "Véhicule de police", 11 | "debug_ped": "(Debug) Ped", 12 | "debug_vehicle": "(Debug) Véhicule", 13 | "debug_object": "(Debug) Objet", 14 | "toggle_targeting": "Afficher le système d'interaction", 15 | "go_back": "Retour" 16 | } 17 | -------------------------------------------------------------------------------- /locales/id.json: -------------------------------------------------------------------------------- 1 | { 2 | "toggle_front_driver_door": "Tombol pintu pengemudi depan", 3 | "toggle_front_passenger_door": "Tombol pintu penumpang depan", 4 | "toggle_rear_driver_door": "Tombol pintu pengemudi belakang", 5 | "toggle_rear_passenger_door": "Tombol pintu penumpang belakang", 6 | "toggle_hood": "Tombol kap", 7 | "toggle_trunk": "Tombol bagasi", 8 | "debug_box": "(Debug) Kotak", 9 | "debug_sphere": "(Debug) Bola", 10 | "debug_police_car": "Mobil polisi", 11 | "debug_ped": "(Debug) Ped", 12 | "debug_vehicle": "(Debug) Kendaraan", 13 | "debug_object": "(Debug) Objek", 14 | "toggle_targeting": "Tombol penargetan" 15 | } 16 | -------------------------------------------------------------------------------- /locales/fi.json: -------------------------------------------------------------------------------- 1 | { 2 | "toggle_front_driver_door": "Avaa/Sulje kuljettajan etuovi", 3 | "toggle_front_passenger_door": "Avaa/Sulje repsikan etuovi", 4 | "toggle_rear_driver_door": "Avaa/Sulje kuljettajan takaovi", 5 | "toggle_rear_passenger_door": "Avaa/Sulje repsikan takaovi", 6 | "toggle_hood": "Avaa/Sulje konepelti", 7 | "toggle_trunk": "Avaa/Sulje takakontti", 8 | "debug_box": "(Debug) Laatikko", 9 | "debug_sphere": "(Debug) Ympyrä", 10 | "debug_police_car": "Poliisiauto", 11 | "debug_ped": "(Debug) Ped", 12 | "debug_vehicle": "(Debug) Ajoneuvo", 13 | "debug_object": "(Debug) Objekti", 14 | "toggle_targeting": "Päällä/Pois tähtäys" 15 | } 16 | -------------------------------------------------------------------------------- /locales/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "toggle_front_driver_door": "Vordere Fahrertür umschalten", 3 | "toggle_front_passenger_door": "Vordere Beifahrertür umschalten", 4 | "toggle_rear_driver_door": "Hintere Fahrertür umschalten", 5 | "toggle_rear_passenger_door": "Hintere Beifahrertür umschalten", 6 | "toggle_hood": "Motorhaube umschalten", 7 | "toggle_trunk": "Kofferraum umschalten", 8 | "debug_box": "(Debug) Box", 9 | "debug_sphere": "(Debug) Sphäre", 10 | "debug_police_car": "Polizeiauto", 11 | "debug_ped": "(Debug) Ped", 12 | "debug_vehicle": "(Debug) Fahrzeug", 13 | "debug_object": "(Debug) Objekt", 14 | "toggle_targeting": "Zielen umschalten" 15 | } 16 | -------------------------------------------------------------------------------- /locales/sl.json: -------------------------------------------------------------------------------- 1 | { 2 | "toggle_front_driver_door": "Odpri/Zapri leva sprednja vrata", 3 | "toggle_front_passenger_door": "Odpri/Zapri desna sprednja vrata", 4 | "toggle_rear_driver_door": "Odpri/Zapri leva zadnja vrata", 5 | "toggle_rear_passenger_door": "Odpri/Zapri desna zadnja vrata", 6 | "toggle_hood": "Odpri/Zapri pokrov motorja", 7 | "toggle_trunk": "Odpri/Zapri prtljažnik", 8 | "debug_box": "(Debug) Kvadrat", 9 | "debug_sphere": "(Debug) Krog", 10 | "debug_police_car": "Policijsko vozilo", 11 | "debug_ped": "(Debug) Pešec", 12 | "debug_vehicle": "(Debug) Vozilo", 13 | "debug_object": "(Debug) Predmet", 14 | "toggle_targeting": "Vklopi/Izklopi tretje oko" 15 | } 16 | -------------------------------------------------------------------------------- /locales/da.json: -------------------------------------------------------------------------------- 1 | { 2 | "toggle_front_driver_door": "Skift forreste førerdør", 3 | "toggle_front_passenger_door": "Skift forreste passagerdør", 4 | "toggle_rear_driver_door": "Skift bageste førerdør", 5 | "toggle_rear_passenger_door": "Skift bageste passagerdør", 6 | "toggle_hood": "Skift motorhjelm", 7 | "toggle_trunk": "Skift bagagerum", 8 | "debug_box": "(Debug) Boks", 9 | "debug_sphere": "(Debug) Kugle", 10 | "debug_police_car": "Politibil", 11 | "debug_ped": "(Debug) Person", 12 | "debug_vehicle": "(Debug) Køretøj", 13 | "debug_object": "(Debug) Objekt", 14 | "debug_global": "(Debug) Global", 15 | "toggle_targeting": "Skift målretning", 16 | "go_back": "Gå tilbage" 17 | } 18 | -------------------------------------------------------------------------------- /locales/pt.json: -------------------------------------------------------------------------------- 1 | { 2 | "toggle_front_driver_door": "Abrir a porta do motorista dianteira", 3 | "toggle_front_passenger_door": "Abrir a porta dianteira do passageiro", 4 | "toggle_rear_driver_door": "Abrir a porta traseira do motorista", 5 | "toggle_rear_passenger_door": "Abrir a porta traseira do passageiro", 6 | "toggle_hood": "Abrir o capô", 7 | "toggle_trunk": "Abrir o porta-malas", 8 | "debug_box": "(Debug) Caixa", 9 | "debug_sphere": "(Debug) Esfera", 10 | "debug_police_car": "Carro de polícia", 11 | "debug_ped": "(Debug) Ped", 12 | "debug_vehicle": "(Debug) Veículo", 13 | "debug_object": "(Debug) Objeto", 14 | "toggle_targeting": "Alternar mira", 15 | "go_back": "Voltar" 16 | } 17 | -------------------------------------------------------------------------------- /locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "toggle_front_driver_door": "Toggle front driver door", 3 | "toggle_front_passenger_door": "Toggle front passenger door", 4 | "toggle_rear_driver_door": "Toggle rear driver door", 5 | "toggle_rear_passenger_door": "Toggle rear passenger door", 6 | "toggle_hood": "Toggle hood", 7 | "toggle_trunk": "Toggle trunk", 8 | "debug_box": "(Debug) Box", 9 | "debug_sphere": "(Debug) Sphere", 10 | "debug_police_car": "Police car", 11 | "debug_ped": "(Debug) Ped", 12 | "debug_vehicle": "(Debug) Vehicle", 13 | "debug_object": "(Debug) Object", 14 | "debug_global": "(Debug) Global", 15 | "toggle_targeting": "Toggle targeting", 16 | "go_back": "Go back" 17 | } 18 | -------------------------------------------------------------------------------- /locales/pt-br.json: -------------------------------------------------------------------------------- 1 | { 2 | "toggle_front_driver_door": "Abrir a porta do motorista dianteira", 3 | "toggle_front_passenger_door": "Abrir a porta dianteira do passageiro", 4 | "toggle_rear_driver_door": "Abrir a porta traseira do motorista", 5 | "toggle_rear_passenger_door": "Abrir a porta traseira do passageiro", 6 | "toggle_hood": "Abrir o capô", 7 | "toggle_trunk": "Abrir o porta-malas", 8 | "debug_box": "(Debug) Caixa", 9 | "debug_sphere": "(Debug) Esfera", 10 | "debug_police_car": "Carro de polícia", 11 | "debug_ped": "(Debug) Ped", 12 | "debug_vehicle": "(Debug) Veículo", 13 | "debug_object": "(Debug) Objeto", 14 | "toggle_targeting": "Alternar mira", 15 | "go_back": "Voltar" 16 | } 17 | -------------------------------------------------------------------------------- /locales/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "toggle_front_driver_door": "Abrir/Cerrar puerta delantera del conductor", 3 | "toggle_front_passenger_door": "Abrir/Cerrar puerta delantera del pasajero", 4 | "toggle_rear_driver_door": "Abrir/Cerrar puerta trasera del conductor", 5 | "toggle_rear_passenger_door": "Abrir/Cerrar puerta trasera del pasajero", 6 | "toggle_hood": "Abrir/Cerrar capó", 7 | "toggle_trunk": "Abrir/Cerrar maletero", 8 | "debug_box": "(Debug) Caja", 9 | "debug_sphere": "(Debug) Esfera", 10 | "debug_police_car": "Coche de policía", 11 | "debug_ped": "(Debug) Peatón", 12 | "debug_vehicle": "(Debug) Vehículo", 13 | "debug_object": "(Debug) Objeto", 14 | "toggle_targeting": "Activar/Desactivar apuntado" 15 | } 16 | -------------------------------------------------------------------------------- /locales/ro.json: -------------------------------------------------------------------------------- 1 | { 2 | "toggle_front_driver_door": "Comută ușa șoferului", 3 | "toggle_front_passenger_door": "Comută ușa pasagerului", 4 | "toggle_rear_driver_door": "Comută ușa din spatele șoferului", 5 | "toggle_rear_passenger_door": "Comută ușa pasagerului din spate", 6 | "toggle_hood": "Comută capota", 7 | "toggle_trunk": "Comută portbagajul", 8 | "debug_box": "(Debug) Cutie", 9 | "debug_sphere": "(Debug) Sferă", 10 | "debug_police_car": "Mașină de poliție", 11 | "debug_ped": "(Debug) Pedestrian", 12 | "debug_vehicle": "(Debug) Vehicul", 13 | "debug_object": "(Debug) Obiect", 14 | "debug_global": "(Debug) Global", 15 | "toggle_targeting": "Foloseste ochiul", 16 | "go_back": "Întoarce-te" 17 | } 18 | -------------------------------------------------------------------------------- /locales/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "toggle_front_driver_door": "Apri/Chiudi portiera anteriore sinistra", 3 | "toggle_front_passenger_door": "Apri/Chiudi portiera anteriore destra", 4 | "toggle_rear_driver_door": "Apri/Chiudi portiera posteriore sinistra", 5 | "toggle_rear_passenger_door": "Apri/Chiudi portiera posteriore destra", 6 | "toggle_hood": "Apri/Chiudi Cofano", 7 | "toggle_trunk": "Apri/Chiudi Bagagliaio", 8 | "debug_box": "(Debug) Box", 9 | "debug_sphere": "(Debug) Sphere", 10 | "debug_police_car": "Auto Polizia", 11 | "debug_ped": "(Debug) Ped", 12 | "debug_vehicle": "(Debug) Vehicle", 13 | "debug_object": "(Debug) Object", 14 | "toggle_targeting": "Apri/Chiudi targeting", 15 | "go_back": "Indietro" 16 | } 17 | -------------------------------------------------------------------------------- /locales/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "toggle_front_driver_door": "Ön sürücü tarafı kapısını aç/kapa", 3 | "toggle_front_passenger_door": "Ön yolcu tarafı kapısını aç/kapa", 4 | "toggle_rear_driver_door": "Arka sürücü tarafı kapısını aç/kapa", 5 | "toggle_rear_passenger_door": "Arka yolcu tarafı kapısını aç/kapa", 6 | "toggle_hood": "Kaputu aç/kapa", 7 | "toggle_trunk": "Bagajı aç/kapa", 8 | "debug_box": "(Debug) Kutu", 9 | "debug_sphere": "(Debug) Küre", 10 | "debug_police_car": "Polis arabası", 11 | "debug_ped": "(Debug) Ped", 12 | "debug_vehicle": "(Debug) Araç", 13 | "debug_object": "(Debug) Nesne", 14 | "debug_global": "(Debug) Genel", 15 | "toggle_targeting": "Hedeflemeyi aç/kapa", 16 | "go_back": "Geri dön" 17 | } 18 | -------------------------------------------------------------------------------- /locales/hu.json: -------------------------------------------------------------------------------- 1 | { 2 | "toggle_front_driver_door": "Sofőroldali ajtó nyitása/zárása", 3 | "toggle_front_passenger_door": "Anyósülés oldali ajtó nyitása/zárása", 4 | "toggle_rear_driver_door": "Sofőroldali hátsó ajtó nyitása/zárása", 5 | "toggle_rear_passenger_door": "Anyósülés oldali hátsó ajtó nyitása/zárása", 6 | "toggle_hood": "Motorháztető nyitása/zárása", 7 | "toggle_trunk": "Csomagtartó nyitása/zárása", 8 | "debug_box": "(Hibakeresés) Doboz", 9 | "debug_sphere": "(Hibakeresés) Gömb", 10 | "debug_police_car": "(Hibakeresés) Rendőrautó", 11 | "debug_ped": "(Hibakeresés) Entitás", 12 | "debug_vehicle": "(Hibakeresés) Jármű", 13 | "debug_object": "(Hibakeresés) Objekt", 14 | "toggle_targeting": "Célzó be- és kikapcsolása", 15 | "go_back": "Vissza" 16 | } 17 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 |
10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /fxmanifest.lua: -------------------------------------------------------------------------------- 1 | -- FX Information 2 | fx_version 'cerulean' 3 | use_experimental_fxv2_oal 'yes' 4 | lua54 'yes' 5 | game 'gta5' 6 | 7 | -- Resource Information 8 | name 'ox_target' 9 | author 'Overextended' 10 | version '1.17.2' 11 | repository 'https://github.com/overextended/ox_target' 12 | description '' 13 | 14 | -- Manifest 15 | ui_page 'web/index.html' 16 | 17 | shared_scripts { 18 | '@ox_lib/init.lua', 19 | } 20 | 21 | client_scripts { 22 | 'client/main.lua', 23 | } 24 | 25 | server_scripts { 26 | 'server/main.lua' 27 | } 28 | 29 | files { 30 | 'web/**', 31 | 'locales/*.json', 32 | 'client/api.lua', 33 | 'client/utils.lua', 34 | 'client/state.lua', 35 | 'client/debug.lua', 36 | 'client/defaults.lua', 37 | 'client/framework/nd.lua', 38 | 'client/framework/ox.lua', 39 | 'client/framework/esx.lua', 40 | 'client/framework/qbx.lua', 41 | 'client/compat/qtarget.lua', 42 | } 43 | 44 | provide 'qtarget' 45 | 46 | dependency 'ox_lib' 47 | -------------------------------------------------------------------------------- /client/state.lua: -------------------------------------------------------------------------------- 1 | local state = {} 2 | 3 | local isActive = false 4 | 5 | ---@return boolean 6 | function state.isActive() 7 | return isActive 8 | end 9 | 10 | ---@param value boolean 11 | function state.setActive(value) 12 | isActive = value 13 | 14 | if value then 15 | SendNuiMessage('{"event": "visible", "state": true}') 16 | end 17 | end 18 | 19 | local nuiFocus = false 20 | 21 | ---@return boolean 22 | function state.isNuiFocused() 23 | return nuiFocus 24 | end 25 | 26 | ---@param value boolean 27 | function state.setNuiFocus(value, cursor) 28 | if value then SetCursorLocation(0.5, 0.5) end 29 | 30 | nuiFocus = value 31 | SetNuiFocus(value, cursor or false) 32 | SetNuiFocusKeepInput(value) 33 | end 34 | 35 | local isDisabled = false 36 | 37 | ---@return boolean 38 | function state.isDisabled() 39 | return isDisabled 40 | end 41 | 42 | ---@param value boolean 43 | function state.setDisabled(value) 44 | isDisabled = value 45 | end 46 | 47 | return state 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Overextended 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /web/js/createOptions.js: -------------------------------------------------------------------------------- 1 | import { fetchNui } from "./fetchNui.js"; 2 | 3 | const optionsWrapper = document.getElementById("options-wrapper"); 4 | 5 | function onClick() { 6 | // when nuifocus is disabled after a click, the hover event is never released 7 | this.style.pointerEvents = "none"; 8 | 9 | fetchNui("select", [this.targetType, this.targetId, this.zoneId]); 10 | // is there a better way to handle this? probably 11 | setTimeout(() => (this.style.pointerEvents = "auto"), 100); 12 | } 13 | 14 | export function createOptions(type, data, id, zoneId) { 15 | if (data.hide) return; 16 | 17 | const option = document.createElement("div"); 18 | const iconElement = ``; 21 | 22 | option.innerHTML = `${iconElement}

${data.label}

`; 23 | option.className = "option-container"; 24 | option.targetType = type; 25 | option.targetId = id; 26 | option.zoneId = zoneId; 27 | 28 | option.addEventListener("click", onClick); 29 | optionsWrapper.appendChild(option); 30 | } 31 | -------------------------------------------------------------------------------- /web/js/main.js: -------------------------------------------------------------------------------- 1 | import { createOptions } from "./createOptions.js"; 2 | 3 | const optionsWrapper = document.getElementById("options-wrapper"); 4 | const body = document.body; 5 | const eye = document.getElementById("eyeSvg"); 6 | 7 | window.addEventListener("message", (event) => { 8 | optionsWrapper.innerHTML = ""; 9 | 10 | switch (event.data.event) { 11 | case "visible": { 12 | body.style.visibility = event.data.state ? "visible" : "hidden"; 13 | return eye.classList.remove("eye-hover"); 14 | } 15 | 16 | case "leftTarget": { 17 | return eye.classList.remove("eye-hover"); 18 | } 19 | 20 | case "setTarget": { 21 | eye.classList.add("eye-hover"); 22 | 23 | if (event.data.options) { 24 | for (const type in event.data.options) { 25 | event.data.options[type].forEach((data, id) => { 26 | createOptions(type, data, id + 1); 27 | }); 28 | } 29 | } 30 | 31 | if (event.data.zones) { 32 | for (let i = 0; i < event.data.zones.length; i++) { 33 | event.data.zones[i].forEach((data, id) => { 34 | createOptions("zones", data, id + 1, i + 1); 35 | }); 36 | } 37 | } 38 | } 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ox_target 2 | 3 | ![](https://img.shields.io/github/downloads/overextended/ox_target/total?logo=github) 4 | ![](https://img.shields.io/github/downloads/overextended/ox_target/latest/total?logo=github) 5 | ![](https://img.shields.io/github/contributors/overextended/ox_target?logo=github) 6 | ![](https://img.shields.io/github/v/release/overextended/ox_target?logo=github) 7 | 8 | 9 | A performant and flexible standalone "third-eye" targeting resource, with additional functionality for supported frameworks. 10 | 11 | ox_target is the successor to qtarget, which was a mostly-compatible fork of bt-target. 12 | 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. 13 | 14 | 15 | ## 📚 Documentation 16 | 17 | https://overextended.dev/ox_target 18 | 19 | ## 💾 Download 20 | 21 | https://github.com/overextended/ox_target/releases/latest/download/ox_target.zip 22 | 23 | ## ✨ Features 24 | 25 | - Improved entity and world collision than its predecessor. 26 | - Improved error handling when running external code. 27 | - Menus for nested target options. 28 | - Partial compatibility for qtarget (the thing qb-target is based on, I made the original idiots). 29 | - Registering options no longer overrides existing options. 30 | - Groups and items checking for supported frameworks. 31 | -------------------------------------------------------------------------------- /client/framework/nd.lua: -------------------------------------------------------------------------------- 1 | local NDCore = exports["ND_Core"] 2 | 3 | local playerGroups = NDCore:getPlayer()?.groups or {} 4 | 5 | RegisterNetEvent("ND:characterLoaded", function(data) 6 | playerGroups = data.groups 7 | end) 8 | 9 | RegisterNetEvent("ND:updateCharacter", function(data) 10 | if source == '' then return end 11 | playerGroups = data.groups or {} 12 | end) 13 | 14 | local utils = require 'client.utils' 15 | 16 | ---@diagnostic disable-next-line: duplicate-set-field 17 | function utils.hasPlayerGotGroup(filter) 18 | local _type = type(filter) 19 | 20 | if _type == 'string' then 21 | local group = playerGroups[filter] 22 | 23 | if group then 24 | return true 25 | end 26 | elseif _type == 'table' then 27 | local tabletype = table.type(filter) 28 | 29 | if tabletype == 'hash' then 30 | for name, grade in pairs(filter) do 31 | local playerGrade = playerGroups[name]?.rank 32 | 33 | if playerGrade and grade <= playerGrade then 34 | return true 35 | end 36 | end 37 | elseif tabletype == 'array' then 38 | for i = 1, #filter do 39 | local name = filter[i] 40 | local group = playerGroups[name] 41 | 42 | if group then 43 | return true 44 | end 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /server/main.lua: -------------------------------------------------------------------------------- 1 | lib.versionCheck('overextended/ox_target') 2 | 3 | if not lib.checkDependency('ox_lib', '3.30.0', true) then return end 4 | 5 | ---@type table 6 | local entityStates = {} 7 | 8 | ---@param netId number 9 | RegisterNetEvent('ox_target:setEntityHasOptions', function(netId) 10 | local entity = Entity(NetworkGetEntityFromNetworkId(netId)) 11 | entity.state.hasTargetOptions = true 12 | entityStates[netId] = entity 13 | end) 14 | 15 | ---@param netId number 16 | ---@param door number 17 | RegisterNetEvent('ox_target:toggleEntityDoor', function(netId, door) 18 | local entity = NetworkGetEntityFromNetworkId(netId) 19 | if not DoesEntityExist(entity) then return end 20 | 21 | local owner = NetworkGetEntityOwner(entity) 22 | TriggerClientEvent('ox_target:toggleEntityDoor', owner, netId, door) 23 | end) 24 | 25 | CreateThread(function() 26 | local arr = {} 27 | local num = 0 28 | 29 | while true do 30 | Wait(10000) 31 | 32 | for netId, entity in pairs(entityStates) do 33 | if not DoesEntityExist(entity.__data) or not entity.state.hasTargetOptions then 34 | entityStates[netId] = nil 35 | num += 1 36 | 37 | arr[num] = netId 38 | end 39 | end 40 | 41 | if num > 0 then 42 | TriggerClientEvent('ox_target:removeEntity', -1, arr) 43 | table.wipe(arr) 44 | 45 | num = 0 46 | end 47 | end 48 | end) 49 | -------------------------------------------------------------------------------- /.github/workflows/create-release.yml: -------------------------------------------------------------------------------- 1 | name: Create release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | jobs: 9 | create-release: 10 | name: Package and Create Tagged Release 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Install archive tools 15 | run: sudo apt install zip 16 | 17 | - name: Checkout source code 18 | uses: actions/checkout@v2 19 | with: 20 | fetch-depth: 0 21 | ref: ${{ github.event.repository.default_branch }} 22 | 23 | - name: Bump manifest version 24 | run: node .github/actions/bump-manifest-version.js 25 | env: 26 | TGT_RELEASE_VERSION: ${{ github.ref_name }} 27 | 28 | - name: Push manifest change 29 | uses: EndBug/add-and-commit@v8 30 | with: 31 | add: fxmanifest.lua 32 | push: true 33 | author_name: Manifest Bumper 34 | author_email: 41898282+github-actions[bot]@users.noreply.github.com 35 | message: 'chore: bump manifest version to ${{ github.ref_name }}' 36 | 37 | - name: Update tag ref 38 | uses: EndBug/latest-tag@latest 39 | with: 40 | tag-name: ${{ github.ref_name }} 41 | 42 | - name: Bundle files 43 | run: | 44 | mkdir -p ./temp/ox_target 45 | cp ./{LICENSE,README.md,fxmanifest.lua} ./temp/ox_target 46 | cp -r ./{client,server,web,locales} ./temp/ox_target 47 | cd ./temp && zip -r ../ox_target.zip ./ox_target 48 | 49 | - name: Create Release 50 | uses: 'marvinpinto/action-automatic-releases@v1.2.1' 51 | id: auto_release 52 | with: 53 | repo_token: '${{ secrets.GITHUB_TOKEN }}' 54 | title: '${{ env.RELEASE_VERSION }}' 55 | prerelease: false 56 | files: ox_target.zip 57 | 58 | env: 59 | CI: false 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | -------------------------------------------------------------------------------- /web/style.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap"); 2 | 3 | :root { 4 | --color-default: #cfd2da; 5 | --color-hover: white; 6 | } 7 | 8 | body { 9 | visibility: hidden; 10 | user-select: none; 11 | white-space: nowrap; 12 | margin: 0; 13 | user-select: none; 14 | overflow: hidden; 15 | } 16 | 17 | p { 18 | margin: 0; 19 | } 20 | 21 | .material-symbols-outlined { 22 | font-variation-settings: "FILL" 0, "wght" 300, "GRAD" 0, "opsz" 40; 23 | } 24 | 25 | #eye { 26 | position: absolute; 27 | top: 50%; 28 | left: 50%; 29 | transform: translate(-50%, -50%); 30 | font-size: 22pt; 31 | fill: black; 32 | } 33 | 34 | .eye-hover { 35 | fill: var(--color-default); 36 | } 37 | 38 | #options-wrapper { 39 | position: absolute; 40 | top: calc(48.4%); 41 | left: calc(50% + 18pt); 42 | } 43 | 44 | .option-container { 45 | color: var(--color-default); 46 | display: flex; 47 | flex-direction: row; 48 | justify-content: flex-start; 49 | align-items: center; 50 | font-family: "Nunito"; 51 | background: linear-gradient( 52 | 90deg, 53 | rgba(20, 20, 20, 0.7) 0%, 54 | rgba(20, 20, 20, 0.6) 66%, 55 | rgba(47, 48, 53, 0) 100% 56 | ); 57 | font-size: 11pt; 58 | line-height: 22pt; 59 | vertical-align: middle; 60 | margin: 2pt; 61 | transition: 300ms; 62 | transform-origin: left top; 63 | scale: 1; 64 | height: 22pt; 65 | width: 150pt; 66 | top: 0; 67 | } 68 | 69 | .option-container:hover { 70 | background: linear-gradient( 71 | 90deg, 72 | rgba(30, 30, 30, 0.7) 0%, 73 | rgba(30, 30, 30, 0.6) 66%, 74 | rgba(57, 58, 63, 0) 100% 75 | ); 76 | transform-origin: left top; 77 | color: var(--color-hover); 78 | margin-left: 4pt; 79 | } 80 | 81 | .option-icon { 82 | font-size: 12pt; 83 | line-height: 22pt; 84 | width: 14pt; 85 | margin: 5pt; 86 | color: var(--color-default); 87 | } 88 | 89 | .option-label { 90 | font-weight: 500; 91 | } 92 | -------------------------------------------------------------------------------- /client/debug.lua: -------------------------------------------------------------------------------- 1 | AddEventHandler('ox_target:debug', function(data) 2 | if data.entity and GetEntityType(data.entity) > 0 then 3 | data.archetype = GetEntityArchetypeName(data.entity) 4 | data.model = GetEntityModel(data.entity) 5 | end 6 | 7 | print(json.encode(data, {indent=true})) 8 | end) 9 | 10 | if GetConvarInt('ox_target:debug', 0) ~= 1 then return end 11 | 12 | local ox_target = exports.ox_target 13 | local drawZones = true 14 | 15 | ox_target:addBoxZone({ 16 | coords = vec3(442.5363, -1017.666, 28.85637), 17 | size = vec3(3, 3, 3), 18 | rotation = 45, 19 | debug = drawZones, 20 | drawSprite = true, 21 | options = { 22 | { 23 | name = 'debug_box', 24 | event = 'ox_target:debug', 25 | icon = 'fa-solid fa-cube', 26 | label = locale('debug_box'), 27 | } 28 | } 29 | }) 30 | 31 | ox_target:addSphereZone({ 32 | coords = vec3(440.5363, -1015.666, 28.85637), 33 | radius = 3, 34 | debug = drawZones, 35 | drawSprite = true, 36 | options = { 37 | { 38 | name = 'debug_sphere', 39 | event = 'ox_target:debug', 40 | icon = 'fa-solid fa-circle', 41 | label = locale('debug_sphere'), 42 | } 43 | } 44 | }) 45 | 46 | ox_target:addModel(`police`, { 47 | { 48 | name = 'debug_model', 49 | event = 'ox_target:debug', 50 | icon = 'fa-solid fa-handcuffs', 51 | label = locale('debug_police_car'), 52 | } 53 | }) 54 | 55 | ox_target:addGlobalPed({ 56 | { 57 | name = 'debug_ped', 58 | event = 'ox_target:debug', 59 | icon = 'fa-solid fa-male', 60 | label = locale('debug_ped'), 61 | } 62 | }) 63 | 64 | ox_target:addGlobalVehicle({ 65 | { 66 | name = 'debug_vehicle', 67 | event = 'ox_target:debug', 68 | icon = 'fa-solid fa-car', 69 | label = locale('debug_vehicle'), 70 | } 71 | }) 72 | 73 | ox_target:addGlobalObject({ 74 | { 75 | name = 'debug_object', 76 | event = 'ox_target:debug', 77 | icon = 'fa-solid fa-bong', 78 | label = locale('debug_object'), 79 | } 80 | }) 81 | 82 | ox_target:addGlobalOption({ 83 | { 84 | name = 'debug_global', 85 | icon = 'fa-solid fa-globe', 86 | label = locale('debug_global'), 87 | openMenu = 'debug_global' 88 | } 89 | }) 90 | 91 | ox_target:addGlobalOption({ 92 | { 93 | name = 'debug_global2', 94 | event = 'ox_target:debug', 95 | icon = 'fa-solid fa-globe', 96 | label = locale('debug_global') .. ' 2', 97 | menuName = 'debug_global' 98 | } 99 | }) -------------------------------------------------------------------------------- /client/framework/esx.lua: -------------------------------------------------------------------------------- 1 | local ESX = exports.es_extended:getSharedObject() 2 | local utils = require 'client.utils' 3 | local groups = { 'job', 'job2' } 4 | local playerGroups = {} 5 | local playerItems = utils.getItems() 6 | local usingOxInventory = utils.hasExport('ox_inventory.Items') 7 | 8 | local function setPlayerData(playerData) 9 | table.wipe(playerGroups) 10 | table.wipe(playerItems) 11 | 12 | for i = 1, #groups do 13 | local group = groups[i] 14 | local data = playerData[group] 15 | 16 | if data then 17 | playerGroups[group] = data 18 | end 19 | end 20 | 21 | if usingOxInventory or not playerData.inventory then return end 22 | 23 | for _, v in pairs(playerData.inventory) do 24 | if v.count > 0 then 25 | playerItems[v.name] = v.count 26 | end 27 | end 28 | end 29 | 30 | if ESX.PlayerLoaded then 31 | setPlayerData(ESX.PlayerData) 32 | end 33 | 34 | RegisterNetEvent('esx:playerLoaded', function(data) 35 | if source == '' then return end 36 | setPlayerData(data) 37 | end) 38 | 39 | RegisterNetEvent('esx:setJob', function(job) 40 | if source == '' then return end 41 | playerGroups.job = job 42 | end) 43 | 44 | RegisterNetEvent('esx:setJob2', function(job) 45 | if source == '' then return end 46 | playerGroups.job2 = job 47 | end) 48 | 49 | RegisterNetEvent('esx:addInventoryItem', function(name, count) 50 | playerItems[name] = count 51 | end) 52 | 53 | RegisterNetEvent('esx:removeInventoryItem', function(name, count) 54 | playerItems[name] = count 55 | end) 56 | 57 | ---@diagnostic disable-next-line: duplicate-set-field 58 | function utils.hasPlayerGotGroup(filter) 59 | local _type = type(filter) 60 | for i = 1, #groups do 61 | local group = groups[i] 62 | 63 | if _type == 'string' then 64 | local data = playerGroups[group] 65 | 66 | if filter == data?.name then 67 | return true 68 | end 69 | elseif _type == 'table' then 70 | local tabletype = table.type(filter) 71 | 72 | if tabletype == 'hash' then 73 | for name, grade in pairs(filter) do 74 | local data = playerGroups[group] 75 | 76 | if data?.name == name and grade <= data.grade then 77 | return true 78 | end 79 | end 80 | elseif tabletype == 'array' then 81 | for j = 1, #filter do 82 | local name = filter[j] 83 | local data = playerGroups[group] 84 | 85 | if data?.name == name then 86 | return true 87 | end 88 | end 89 | end 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /client/defaults.lua: -------------------------------------------------------------------------------- 1 | if GetConvarInt('ox_target:defaults', 1) ~= 1 then return end 2 | 3 | local api = require 'client.api' 4 | local GetEntityBoneIndexByName = GetEntityBoneIndexByName 5 | local GetEntityBonePosition_2 = GetEntityBonePosition_2 6 | local GetVehicleDoorLockStatus = GetVehicleDoorLockStatus 7 | 8 | local bones = { 9 | [0] = 'dside_f', 10 | [1] = 'pside_f', 11 | [2] = 'dside_r', 12 | [3] = 'pside_r' 13 | } 14 | 15 | ---@param vehicle number 16 | ---@param door number 17 | local function toggleDoor(vehicle, door) 18 | if GetVehicleDoorLockStatus(vehicle) ~= 2 then 19 | if GetVehicleDoorAngleRatio(vehicle, door) > 0.0 then 20 | SetVehicleDoorShut(vehicle, door, false) 21 | else 22 | SetVehicleDoorOpen(vehicle, door, false, false) 23 | end 24 | end 25 | end 26 | 27 | ---@param entity number 28 | ---@param coords vector3 29 | ---@param door number 30 | ---@param useOffset boolean? 31 | ---@return boolean? 32 | local function canInteractWithDoor(entity, coords, door, useOffset) 33 | if not GetIsDoorValid(entity, door) or GetVehicleDoorLockStatus(entity) > 1 or IsVehicleDoorDamaged(entity, door) or cache.vehicle then return end 34 | 35 | if useOffset then return true end 36 | 37 | local boneName = bones[door] 38 | 39 | if not boneName then return false end 40 | 41 | local boneId = GetEntityBoneIndexByName(entity, 'door_' .. boneName) 42 | 43 | if boneId ~= -1 then 44 | return #(coords - GetEntityBonePosition_2(entity, boneId)) < 0.5 or 45 | #(coords - GetEntityBonePosition_2(entity, GetEntityBoneIndexByName(entity, 'seat_' .. boneName))) < 0.72 46 | end 47 | end 48 | 49 | local function onSelectDoor(data, door) 50 | local entity = data.entity 51 | 52 | if NetworkGetEntityOwner(entity) == cache.playerId then 53 | return toggleDoor(entity, door) 54 | end 55 | 56 | TriggerServerEvent('ox_target:toggleEntityDoor', VehToNet(entity), door) 57 | end 58 | 59 | RegisterNetEvent('ox_target:toggleEntityDoor', function(netId, door) 60 | local entity = NetToVeh(netId) 61 | toggleDoor(entity, door) 62 | end) 63 | 64 | api.addGlobalVehicle({ 65 | { 66 | name = 'ox_target:driverF', 67 | icon = 'fa-solid fa-car-side', 68 | label = locale('toggle_front_driver_door'), 69 | bones = { 'door_dside_f', 'seat_dside_f' }, 70 | distance = 2, 71 | canInteract = function(entity, distance, coords, name) 72 | return canInteractWithDoor(entity, coords, 0) 73 | end, 74 | onSelect = function(data) 75 | onSelectDoor(data, 0) 76 | end 77 | }, 78 | { 79 | name = 'ox_target:passengerF', 80 | icon = 'fa-solid fa-car-side', 81 | label = locale('toggle_front_passenger_door'), 82 | bones = { 'door_pside_f', 'seat_pside_f' }, 83 | distance = 2, 84 | canInteract = function(entity, distance, coords, name) 85 | return canInteractWithDoor(entity, coords, 1) 86 | end, 87 | onSelect = function(data) 88 | onSelectDoor(data, 1) 89 | end 90 | }, 91 | { 92 | name = 'ox_target:driverR', 93 | icon = 'fa-solid fa-car-side', 94 | label = locale('toggle_rear_driver_door'), 95 | bones = { 'door_dside_r', 'seat_dside_r' }, 96 | distance = 2, 97 | canInteract = function(entity, distance, coords) 98 | return canInteractWithDoor(entity, coords, 2) 99 | end, 100 | onSelect = function(data) 101 | onSelectDoor(data, 2) 102 | end 103 | }, 104 | { 105 | name = 'ox_target:passengerR', 106 | icon = 'fa-solid fa-car-side', 107 | label = locale('toggle_rear_passenger_door'), 108 | bones = { 'door_pside_r', 'seat_pside_r' }, 109 | distance = 2, 110 | canInteract = function(entity, distance, coords) 111 | return canInteractWithDoor(entity, coords, 3) 112 | end, 113 | onSelect = function(data) 114 | onSelectDoor(data, 3) 115 | end 116 | }, 117 | { 118 | name = 'ox_target:bonnet', 119 | icon = 'fa-solid fa-car', 120 | label = locale('toggle_hood'), 121 | offset = vec3(0.5, 1, 0.5), 122 | distance = 2, 123 | canInteract = function(entity, distance, coords) 124 | return canInteractWithDoor(entity, coords, 4, true) 125 | end, 126 | onSelect = function(data) 127 | onSelectDoor(data, 4) 128 | end 129 | }, 130 | { 131 | name = 'ox_target:trunk', 132 | icon = 'fa-solid fa-car-rear', 133 | label = locale('toggle_trunk'), 134 | offset = vec3(0.5, 0, 0.5), 135 | distance = 2, 136 | canInteract = function(entity, distance, coords, name) 137 | return canInteractWithDoor(entity, coords, 5, true) 138 | end, 139 | onSelect = function(data) 140 | onSelectDoor(data, 5) 141 | end 142 | } 143 | }) 144 | -------------------------------------------------------------------------------- /client/compat/qtarget.lua: -------------------------------------------------------------------------------- 1 | local function exportHandler(exportName, func) 2 | AddEventHandler(('__cfx_export_qtarget_%s'):format(exportName), function(setCB) 3 | setCB(func) 4 | end) 5 | end 6 | 7 | ---@param options table 8 | ---@return table 9 | local function convert(options) 10 | local distance = options.distance 11 | options = options.options 12 | 13 | -- People may pass options as a hashmap (or mixed, even) 14 | for k, v in pairs(options) do 15 | if type(k) ~= 'number' then 16 | table.insert(options, v) 17 | end 18 | end 19 | 20 | for id, v in pairs(options) do 21 | if type(id) ~= 'number' then 22 | options[id] = nil 23 | goto continue 24 | end 25 | 26 | v.onSelect = v.action 27 | v.distance = v.distance or distance 28 | v.name = v.name or v.label 29 | v.groups = v.job 30 | v.items = v.item or v.required_item 31 | 32 | if v.event and v.type and v.type ~= 'client' then 33 | if v.type == 'server' then 34 | v.serverEvent = v.event 35 | elseif v.type == 'command' then 36 | v.command = v.event 37 | end 38 | 39 | v.event = nil 40 | v.type = nil 41 | end 42 | 43 | v.action = nil 44 | v.job = nil 45 | v.item = nil 46 | v.required_item = nil 47 | v.qtarget = true 48 | 49 | ::continue:: 50 | end 51 | 52 | return options 53 | end 54 | 55 | local api = require 'client.api' 56 | 57 | exportHandler('AddBoxZone', function(name, center, length, width, options, targetoptions) 58 | local z = center.z 59 | 60 | if not options.minZ then 61 | options.minZ = -100 62 | end 63 | 64 | if not options.maxZ then 65 | options.maxZ = 800 66 | end 67 | 68 | if not options.useZ then 69 | z = z + math.abs(options.maxZ - options.minZ) / 2 70 | center = vec3(center.x, center.y, z) 71 | end 72 | 73 | return api.addBoxZone({ 74 | name = name, 75 | coords = center, 76 | size = vec3(width, length, (options.useZ or not options.maxZ) and center.z or math.abs(options.maxZ - options.minZ)), 77 | debug = options.debugPoly, 78 | rotation = options.heading, 79 | options = convert(targetoptions), 80 | }) 81 | end) 82 | 83 | exportHandler('AddPolyZone', function(name, points, options, targetoptions) 84 | local newPoints = table.create(#points, 0) 85 | local thickness = math.abs(options.maxZ - options.minZ) 86 | 87 | for i = 1, #points do 88 | local point = points[i] 89 | newPoints[i] = vec3(point.x, point.y, options.maxZ - (thickness / 2)) 90 | end 91 | 92 | return api.addPolyZone({ 93 | name = name, 94 | points = newPoints, 95 | thickness = thickness, 96 | debug = options.debugPoly, 97 | options = convert(targetoptions), 98 | }) 99 | end) 100 | 101 | exportHandler('AddCircleZone', function(name, center, radius, options, targetoptions) 102 | return api.addSphereZone({ 103 | name = name, 104 | coords = center, 105 | radius = radius, 106 | debug = options.debugPoly, 107 | options = convert(targetoptions), 108 | }) 109 | end) 110 | 111 | exportHandler('RemoveZone', function(id) 112 | api.removeZone(id, true) 113 | end) 114 | 115 | exportHandler('AddTargetBone', function(bones, options) 116 | if type(bones) ~= 'table' then bones = { bones } end 117 | options = convert(options) 118 | 119 | for _, v in pairs(options) do 120 | v.bones = bones 121 | end 122 | 123 | exports.ox_target:addGlobalVehicle(options) 124 | end) 125 | 126 | exportHandler('AddTargetEntity', function(entities, options) 127 | if type(entities) ~= 'table' then entities = { entities } end 128 | options = convert(options) 129 | 130 | for i = 1, #entities do 131 | local entity = entities[i] 132 | 133 | if NetworkGetEntityIsNetworked(entity) then 134 | api.addEntity(NetworkGetNetworkIdFromEntity(entity), options) 135 | else 136 | api.addLocalEntity(entity, options) 137 | end 138 | end 139 | end) 140 | 141 | exportHandler('RemoveTargetEntity', function(entities, labels) 142 | if type(entities) ~= 'table' then entities = { entities } end 143 | 144 | for i = 1, #entities do 145 | local entity = entities[i] 146 | 147 | if NetworkGetEntityIsNetworked(entity) then 148 | api.removeEntity(NetworkGetNetworkIdFromEntity(entity), labels) 149 | else 150 | api.removeLocalEntity(entity, labels) 151 | end 152 | end 153 | end) 154 | 155 | exportHandler('AddTargetModel', function(models, options) 156 | api.addModel(models, convert(options)) 157 | end) 158 | 159 | exportHandler('RemoveTargetModel', function(models, labels) 160 | api.removeModel(models, labels) 161 | end) 162 | 163 | exportHandler('Ped', function(options) 164 | api.addGlobalPed(convert(options)) 165 | end) 166 | 167 | exportHandler('RemovePed', function(labels) 168 | api.removeGlobalPed(labels) 169 | end) 170 | 171 | exportHandler('Vehicle', function(options) 172 | api.addGlobalVehicle(convert(options)) 173 | end) 174 | 175 | exportHandler('RemoveVehicle', function(labels) 176 | api.removeGlobalVehicle(labels) 177 | end) 178 | 179 | exportHandler('Object', function(options) 180 | api.addGlobalObject(convert(options)) 181 | end) 182 | 183 | exportHandler('RemoveObject', function(labels) 184 | api.removeGlobalObject(labels) 185 | end) 186 | 187 | exportHandler('Player', function(options) 188 | api.addGlobalPlayer(convert(options)) 189 | end) 190 | 191 | exportHandler('RemovePlayer', function(labels) 192 | api.removeGlobalPlayer(labels) 193 | end) -------------------------------------------------------------------------------- /client/utils.lua: -------------------------------------------------------------------------------- 1 | local utils = {} 2 | 3 | local GetWorldCoordFromScreenCoord = GetWorldCoordFromScreenCoord 4 | local StartShapeTestLosProbe = StartShapeTestLosProbe 5 | local GetShapeTestResultIncludingMaterial = GetShapeTestResultIncludingMaterial 6 | 7 | ---@param flag number 8 | ---@return boolean hit 9 | ---@return number entityHit 10 | ---@return vector3 endCoords 11 | ---@return vector3 surfaceNormal 12 | ---@return number materialHash 13 | function utils.raycastFromCamera(flag) 14 | local coords, normal = GetWorldCoordFromScreenCoord(0.5, 0.5) 15 | local destination = coords + normal * 10 16 | local handle = StartShapeTestLosProbe(coords.x, coords.y, coords.z, destination.x, destination.y, destination.z, 17 | flag, cache.ped, 4) 18 | 19 | while true do 20 | Wait(0) 21 | local retval, hit, endCoords, surfaceNormal, materialHash, entityHit = GetShapeTestResultIncludingMaterial( 22 | handle) 23 | 24 | if retval ~= 1 then 25 | ---@diagnostic disable-next-line: return-type-mismatch 26 | return hit, entityHit, endCoords, surfaceNormal, materialHash 27 | end 28 | end 29 | end 30 | 31 | function utils.getTexture() 32 | return lib.requestStreamedTextureDict('shared'), 'emptydot_32' 33 | end 34 | 35 | -- SetDrawOrigin is limited to 32 calls per frame. Set as 0 to disable. 36 | local drawZoneSprites = GetConvarInt('ox_target:drawSprite', 24) 37 | local SetDrawOrigin = SetDrawOrigin 38 | local DrawSprite = DrawSprite 39 | local ClearDrawOrigin = ClearDrawOrigin 40 | local colour = vector(155, 155, 155, 175) 41 | local hover = vector(98, 135, 236, 255) 42 | local currentZones = {} 43 | local previousZones = {} 44 | local drawZones = {} 45 | local drawN = 0 46 | local width = 0.02 47 | local height = width * GetAspectRatio(false) 48 | 49 | if drawZoneSprites == 0 then drawZoneSprites = -1 end 50 | 51 | ---@param coords vector3 52 | ---@return CZone[], boolean 53 | function utils.getNearbyZones(coords) 54 | if not Zones then return currentZones, false end 55 | 56 | local n = 0 57 | local nearbyZones = lib.zones.getNearbyZones() 58 | drawN = 0 59 | previousZones, currentZones = currentZones, table.wipe(previousZones) 60 | 61 | for i = 1, #nearbyZones do 62 | local zone = nearbyZones[i] 63 | local contains = zone:contains(coords) 64 | 65 | if contains then 66 | n += 1 67 | currentZones[n] = zone 68 | end 69 | 70 | if drawN <= drawZoneSprites and zone.drawSprite ~= false and (contains or (zone.distance or 7) < 7) then 71 | drawN += 1 72 | drawZones[drawN] = zone 73 | zone.colour = contains and hover or nil 74 | end 75 | end 76 | 77 | local previousN = #previousZones 78 | 79 | if n ~= previousN then 80 | return currentZones, true 81 | end 82 | 83 | if n > 0 then 84 | for i = 1, n do 85 | local zoneA = currentZones[i] 86 | local found = false 87 | 88 | for j = 1, previousN do 89 | local zoneB = previousZones[j] 90 | 91 | if zoneA == zoneB then 92 | found = true 93 | break 94 | end 95 | end 96 | 97 | if not found then 98 | return currentZones, true 99 | end 100 | end 101 | end 102 | 103 | return currentZones, false 104 | end 105 | 106 | function utils.drawZoneSprites(dict, texture) 107 | if drawN == 0 then return end 108 | 109 | for i = 1, drawN do 110 | local zone = drawZones[i] 111 | local spriteColour = zone.colour or colour 112 | 113 | if zone.drawSprite ~= false then 114 | SetDrawOrigin(zone.coords.x, zone.coords.y, zone.coords.z) 115 | DrawSprite(dict, texture, 0, 0, width, height, 0, spriteColour.r, spriteColour.g, spriteColour.b, 116 | spriteColour.a) 117 | end 118 | end 119 | 120 | ClearDrawOrigin() 121 | end 122 | 123 | function utils.hasExport(export) 124 | local resource, exportName = string.strsplit('.', export) 125 | 126 | return pcall(function() 127 | return exports[resource][exportName] 128 | end) 129 | end 130 | 131 | local playerItems = {} 132 | 133 | function utils.getItems() 134 | return playerItems 135 | end 136 | 137 | ---@param filter string | string[] | table 138 | ---@param hasAny boolean? 139 | ---@return boolean 140 | function utils.hasPlayerGotItems(filter, hasAny) 141 | if not playerItems then return true end 142 | 143 | local _type = type(filter) 144 | 145 | if _type == 'string' then 146 | return (playerItems[filter] or 0) > 0 147 | elseif _type == 'table' then 148 | local tabletype = table.type(filter) 149 | 150 | if tabletype == 'hash' then 151 | for name, amount in pairs(filter) do 152 | local hasItem = (playerItems[name] or 0) >= amount 153 | 154 | if hasAny then 155 | if hasItem then return true end 156 | elseif not hasItem then 157 | return false 158 | end 159 | end 160 | elseif tabletype == 'array' then 161 | for i = 1, #filter do 162 | local hasItem = (playerItems[filter[i]] or 0) > 0 163 | 164 | if hasAny then 165 | if hasItem then return true end 166 | elseif not hasItem then 167 | return false 168 | end 169 | end 170 | end 171 | end 172 | 173 | return not hasAny 174 | end 175 | 176 | ---stub 177 | ---@param filter string | string[] | table 178 | ---@return boolean 179 | function utils.hasPlayerGotGroup(filter) 180 | return true 181 | end 182 | 183 | SetTimeout(0, function() 184 | if utils.hasExport('ox_inventory.Items') then 185 | setmetatable(playerItems, { 186 | __index = function(self, index) 187 | self[index] = exports.ox_inventory:Search('count', index) or 0 188 | return self[index] 189 | end 190 | }) 191 | 192 | AddEventHandler('ox_inventory:itemCount', function(name, count) 193 | playerItems[name] = count 194 | end) 195 | end 196 | 197 | if utils.hasExport('ox_core.GetPlayer') then 198 | require 'client.framework.ox' 199 | elseif utils.hasExport('es_extended.getSharedObject') then 200 | require 'client.framework.esx' 201 | elseif utils.hasExport('qbx_core.HasGroup') then 202 | require 'client.framework.qbx' 203 | elseif utils.hasExport('ND_Core.getPlayer') then 204 | require 'client.framework.nd' 205 | end 206 | end) 207 | 208 | function utils.warn(msg) 209 | local trace = Citizen.InvokeNative(`FORMAT_STACK_TRACE` & 0xFFFFFFFF, nil, 0, Citizen.ResultAsString()) 210 | local _, _, src = string.strsplit('\n', trace, 4) 211 | 212 | warn(('%s ^0%s\n'):format(msg, src:gsub(".-%(", '('))) 213 | end 214 | 215 | return utils 216 | -------------------------------------------------------------------------------- /client/main.lua: -------------------------------------------------------------------------------- 1 | if not lib.checkDependency('ox_lib', '3.30.0', true) then return end 2 | 3 | lib.locale() 4 | 5 | local utils = require 'client.utils' 6 | local state = require 'client.state' 7 | local options = require 'client.api'.getTargetOptions() 8 | 9 | require 'client.debug' 10 | require 'client.defaults' 11 | require 'client.compat.qtarget' 12 | 13 | local SendNuiMessage = SendNuiMessage 14 | local GetEntityCoords = GetEntityCoords 15 | local GetEntityType = GetEntityType 16 | local HasEntityClearLosToEntity = HasEntityClearLosToEntity 17 | local GetEntityBoneIndexByName = GetEntityBoneIndexByName 18 | local GetEntityBonePosition_2 = GetEntityBonePosition_2 19 | local GetEntityModel = GetEntityModel 20 | local IsDisabledControlJustPressed = IsDisabledControlJustPressed 21 | local DisableControlAction = DisableControlAction 22 | local DisablePlayerFiring = DisablePlayerFiring 23 | local GetModelDimensions = GetModelDimensions 24 | local GetOffsetFromEntityInWorldCoords = GetOffsetFromEntityInWorldCoords 25 | local currentTarget = {} 26 | local currentMenu 27 | local menuChanged 28 | local menuHistory = {} 29 | local nearbyZones 30 | 31 | -- Toggle ox_target, instead of holding the hotkey 32 | local toggleHotkey = GetConvarInt('ox_target:toggleHotkey', 0) == 1 33 | local mouseButton = GetConvarInt('ox_target:leftClick', 1) == 1 and 24 or 25 34 | local debug = GetConvarInt('ox_target:debug', 0) == 1 35 | local vec0 = vec3(0, 0, 0) 36 | 37 | ---@param option OxTargetOption 38 | ---@param distance number 39 | ---@param endCoords vector3 40 | ---@param entityHit? number 41 | ---@param entityType? number 42 | ---@param entityModel? number | false 43 | local function shouldHide(option, distance, endCoords, entityHit, entityType, entityModel) 44 | if option.menuName ~= currentMenu then 45 | return true 46 | end 47 | 48 | if distance > (option.distance or 7) then 49 | return true 50 | end 51 | 52 | if option.groups and not utils.hasPlayerGotGroup(option.groups) then 53 | return true 54 | end 55 | 56 | if option.items and not utils.hasPlayerGotItems(option.items, option.anyItem) then 57 | return true 58 | end 59 | 60 | local bone = entityModel and option.bones or nil 61 | 62 | if bone then 63 | ---@cast entityHit number 64 | ---@cast entityType number 65 | ---@cast entityModel number 66 | 67 | local _type = type(bone) 68 | 69 | if _type == 'string' then 70 | local boneId = GetEntityBoneIndexByName(entityHit, bone) 71 | 72 | if boneId ~= -1 and #(endCoords - GetEntityBonePosition_2(entityHit, boneId)) <= 2 then 73 | bone = boneId 74 | else 75 | return true 76 | end 77 | elseif _type == 'table' then 78 | local closestBone, boneDistance 79 | 80 | for j = 1, #bone do 81 | local boneId = GetEntityBoneIndexByName(entityHit, bone[j]) 82 | 83 | if boneId ~= -1 then 84 | local dist = #(endCoords - GetEntityBonePosition_2(entityHit, boneId)) 85 | 86 | if dist <= (boneDistance or 1) then 87 | closestBone = boneId 88 | boneDistance = dist 89 | end 90 | end 91 | end 92 | 93 | if closestBone then 94 | bone = closestBone 95 | else 96 | return true 97 | end 98 | end 99 | end 100 | 101 | local offset = entityModel and option.offset or nil 102 | 103 | if offset then 104 | ---@cast entityHit number 105 | ---@cast entityType number 106 | ---@cast entityModel number 107 | 108 | if not option.absoluteOffset then 109 | local min, max = GetModelDimensions(entityModel) 110 | offset = (max - min) * offset + min 111 | end 112 | 113 | offset = GetOffsetFromEntityInWorldCoords(entityHit, offset.x, offset.y, offset.z) 114 | 115 | if #(endCoords - offset) > (option.offsetSize or 1) then 116 | return true 117 | end 118 | end 119 | 120 | if option.canInteract then 121 | local success, resp = pcall(option.canInteract, entityHit, distance, endCoords, option.name, bone) 122 | return not success or not resp 123 | end 124 | end 125 | 126 | local function startTargeting() 127 | if state.isDisabled() or state.isActive() or IsNuiFocused() or IsPauseMenuActive() then return end 128 | 129 | state.setActive(true) 130 | 131 | local flag = 511 132 | local hit, entityHit, endCoords, distance, lastEntity, entityType, entityModel, hasTarget, zonesChanged 133 | local zones = {} 134 | 135 | CreateThread(function() 136 | local dict, texture = utils.getTexture() 137 | local lastCoords 138 | 139 | while state.isActive() do 140 | lastCoords = endCoords == vec0 and lastCoords or endCoords or vec0 141 | 142 | if debug then 143 | DrawMarker(28, lastCoords.x, lastCoords.y, lastCoords.z, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.2, 0.2, 144 | 0.2, 145 | ---@diagnostic disable-next-line: param-type-mismatch 146 | 255, 42, 24, 100, false, false, 0, true, false, false, false) 147 | end 148 | 149 | utils.drawZoneSprites(dict, texture) 150 | DisablePlayerFiring(cache.playerId, true) 151 | DisableControlAction(0, 25, true) 152 | DisableControlAction(0, 140, true) 153 | DisableControlAction(0, 141, true) 154 | DisableControlAction(0, 142, true) 155 | 156 | if state.isNuiFocused() then 157 | DisableControlAction(0, 1, true) 158 | DisableControlAction(0, 2, true) 159 | 160 | if not hasTarget or options and IsDisabledControlJustPressed(0, 25) then 161 | state.setNuiFocus(false, false) 162 | end 163 | elseif hasTarget and IsDisabledControlJustPressed(0, mouseButton) then 164 | state.setNuiFocus(true, true) 165 | end 166 | 167 | Wait(0) 168 | end 169 | 170 | SetStreamedTextureDictAsNoLongerNeeded(dict) 171 | end) 172 | 173 | while state.isActive() do 174 | if not state.isNuiFocused() and lib.progressActive() then 175 | state.setActive(false) 176 | break 177 | end 178 | 179 | local playerCoords = GetEntityCoords(cache.ped) 180 | hit, entityHit, endCoords = lib.raycast.fromCamera(flag, 4, 20) 181 | distance = #(playerCoords - endCoords) 182 | 183 | if entityHit ~= 0 and entityHit ~= lastEntity then 184 | local success, result = pcall(GetEntityType, entityHit) 185 | entityType = success and result or 0 186 | end 187 | 188 | if entityType == 0 then 189 | local _flag = flag == 511 and 26 or 511 190 | local _hit, _entityHit, _endCoords = lib.raycast.fromCamera(_flag, 4, 20) 191 | local _distance = #(playerCoords - _endCoords) 192 | 193 | if _distance < distance then 194 | flag, hit, entityHit, endCoords, distance = _flag, _hit, _entityHit, _endCoords, _distance 195 | 196 | if entityHit ~= 0 then 197 | local success, result = pcall(GetEntityType, entityHit) 198 | entityType = success and result or 0 199 | end 200 | end 201 | end 202 | 203 | nearbyZones, zonesChanged = utils.getNearbyZones(endCoords) 204 | 205 | local entityChanged = entityHit ~= lastEntity 206 | local newOptions = (zonesChanged or entityChanged or menuChanged) and true 207 | 208 | if entityHit > 0 and entityChanged then 209 | currentMenu = nil 210 | 211 | if flag ~= 511 then 212 | entityHit = HasEntityClearLosToEntity(entityHit, cache.ped, 7) and entityHit or 0 213 | end 214 | 215 | if lastEntity ~= entityHit and debug then 216 | if lastEntity then 217 | SetEntityDrawOutline(lastEntity, false) 218 | end 219 | 220 | if entityType ~= 1 then 221 | SetEntityDrawOutline(entityHit, true) 222 | end 223 | end 224 | 225 | if entityHit > 0 then 226 | local success, result = pcall(GetEntityModel, entityHit) 227 | entityModel = success and result 228 | end 229 | end 230 | 231 | if hasTarget and (zonesChanged or entityChanged and hasTarget > 1) then 232 | SendNuiMessage('{"event": "leftTarget"}') 233 | 234 | if entityChanged then options:wipe() end 235 | 236 | if debug and lastEntity > 0 then SetEntityDrawOutline(lastEntity, false) end 237 | 238 | hasTarget = false 239 | end 240 | 241 | if newOptions and entityModel and entityHit > 0 then 242 | options:set(entityHit, entityType, entityModel) 243 | end 244 | 245 | lastEntity = entityHit 246 | currentTarget.entity = entityHit 247 | currentTarget.coords = endCoords 248 | currentTarget.distance = distance 249 | local hidden = 0 250 | local totalOptions = 0 251 | 252 | for k, v in pairs(options) do 253 | local optionCount = #v 254 | local dist = k == '__global' and 0 or distance 255 | totalOptions += optionCount 256 | 257 | for i = 1, optionCount do 258 | local option = v[i] 259 | local hide = shouldHide(option, dist, endCoords, entityHit, entityType, entityModel) 260 | 261 | if option.hide ~= hide then 262 | option.hide = hide 263 | newOptions = true 264 | end 265 | 266 | if hide then hidden += 1 end 267 | end 268 | end 269 | 270 | if zonesChanged then table.wipe(zones) end 271 | 272 | for i = 1, #nearbyZones do 273 | local zoneOptions = nearbyZones[i].options 274 | local optionCount = #zoneOptions 275 | totalOptions += optionCount 276 | zones[i] = zoneOptions 277 | 278 | for j = 1, optionCount do 279 | local option = zoneOptions[j] 280 | local hide = shouldHide(option, distance, endCoords, entityHit) 281 | 282 | if option.hide ~= hide then 283 | option.hide = hide 284 | newOptions = true 285 | end 286 | 287 | if hide then hidden += 1 end 288 | end 289 | end 290 | 291 | if newOptions then 292 | if hasTarget == 1 and (totalOptions - hidden) > 1 then 293 | hasTarget = true 294 | end 295 | 296 | if hasTarget and hidden == totalOptions then 297 | if hasTarget and hasTarget ~= 1 then 298 | hasTarget = false 299 | SendNuiMessage('{"event": "leftTarget"}') 300 | end 301 | elseif menuChanged or hasTarget ~= 1 and hidden ~= totalOptions then 302 | hasTarget = options.size 303 | 304 | if currentMenu and options.__global[1]?.name ~= 'builtin:goback' then 305 | table.insert(options.__global, 1, 306 | { 307 | icon = 'fa-solid fa-circle-chevron-left', 308 | label = locale('go_back'), 309 | name = 'builtin:goback', 310 | menuName = currentMenu, 311 | openMenu = 'home' 312 | }) 313 | end 314 | 315 | SendNuiMessage(json.encode({ 316 | event = 'setTarget', 317 | options = options, 318 | zones = zones, 319 | }, { sort_keys = true })) 320 | end 321 | 322 | menuChanged = false 323 | end 324 | 325 | if toggleHotkey and IsPauseMenuActive() then 326 | state.setActive(false) 327 | end 328 | 329 | if not hasTarget or hasTarget == 1 then 330 | flag = flag == 511 and 26 or 511 331 | end 332 | 333 | Wait(hit and 50 or 100) 334 | end 335 | 336 | if lastEntity and debug then 337 | SetEntityDrawOutline(lastEntity, false) 338 | end 339 | 340 | state.setNuiFocus(false) 341 | SendNuiMessage('{"event": "visible", "state": false}') 342 | table.wipe(currentTarget) 343 | options:wipe() 344 | 345 | if nearbyZones then table.wipe(nearbyZones) end 346 | end 347 | 348 | do 349 | ---@type KeybindProps 350 | local keybind = { 351 | name = 'ox_target', 352 | defaultKey = GetConvar('ox_target:defaultHotkey', 'LMENU'), 353 | defaultMapper = 'keyboard', 354 | description = locale('toggle_targeting'), 355 | } 356 | 357 | if toggleHotkey then 358 | function keybind:onPressed() 359 | if state.isActive() then 360 | return state.setActive(false) 361 | end 362 | 363 | return startTargeting() 364 | end 365 | else 366 | keybind.onPressed = startTargeting 367 | 368 | function keybind:onReleased() 369 | state.setActive(false) 370 | end 371 | end 372 | 373 | lib.addKeybind(keybind) 374 | end 375 | 376 | ---@generic T 377 | ---@param option T 378 | ---@param server? boolean 379 | ---@return T 380 | local function getResponse(option, server) 381 | local response = table.clone(option) 382 | response.entity = currentTarget.entity 383 | response.zone = currentTarget.zone 384 | response.coords = currentTarget.coords 385 | response.distance = currentTarget.distance 386 | 387 | if server then 388 | response.entity = response.entity ~= 0 and NetworkGetEntityIsNetworked(response.entity) and 389 | NetworkGetNetworkIdFromEntity(response.entity) or 0 390 | end 391 | 392 | response.icon = nil 393 | response.groups = nil 394 | response.items = nil 395 | response.canInteract = nil 396 | response.onSelect = nil 397 | response.export = nil 398 | response.event = nil 399 | response.serverEvent = nil 400 | response.command = nil 401 | 402 | return response 403 | end 404 | 405 | RegisterNUICallback('select', function(data, cb) 406 | cb(1) 407 | 408 | local zone = data[3] and nearbyZones[data[3]] 409 | 410 | ---@type OxTargetOption? 411 | local option = zone and zone.options[data[2]] or options[data[1]][data[2]] 412 | 413 | if option then 414 | if option.openMenu then 415 | local menuDepth = #menuHistory 416 | 417 | if option.name == 'builtin:goback' then 418 | option.menuName = option.openMenu 419 | option.openMenu = menuHistory[menuDepth] 420 | 421 | if menuDepth > 0 then 422 | menuHistory[menuDepth] = nil 423 | end 424 | else 425 | menuHistory[menuDepth + 1] = currentMenu 426 | end 427 | 428 | menuChanged = true 429 | currentMenu = option.openMenu ~= 'home' and option.openMenu or nil 430 | 431 | options:wipe() 432 | else 433 | state.setNuiFocus(false) 434 | end 435 | 436 | currentTarget.zone = zone?.id 437 | 438 | if option.onSelect then 439 | option.onSelect(option.qtarget and currentTarget.entity or getResponse(option)) 440 | elseif option.export then 441 | exports[option.resource or zone.resource][option.export](nil, getResponse(option)) 442 | elseif option.event then 443 | TriggerEvent(option.event, getResponse(option)) 444 | elseif option.serverEvent then 445 | TriggerServerEvent(option.serverEvent, getResponse(option, true)) 446 | elseif option.command then 447 | ExecuteCommand(option.command) 448 | end 449 | 450 | if option.menuName == 'home' then return end 451 | end 452 | 453 | if not option?.openMenu and IsNuiFocused() then 454 | state.setActive(false) 455 | end 456 | end) 457 | -------------------------------------------------------------------------------- /client/api.lua: -------------------------------------------------------------------------------- 1 | ---@class OxTargetOption 2 | ---@field resource? string 3 | 4 | local utils = require 'client.utils' 5 | 6 | local api = setmetatable({}, { 7 | __newindex = function(self, index, value) 8 | rawset(self, index, value) 9 | exports(index, value) 10 | end 11 | }) 12 | 13 | ---Throws a formatted type error 14 | ---@param variable string 15 | ---@param expected string 16 | ---@param received string 17 | local function typeError(variable, expected, received) 18 | error(("expected %s to have type '%s' (received %s)"):format(variable, expected, received)) 19 | end 20 | 21 | ---Checks options and throws an error on type mismatch 22 | ---@param options OxTargetOption | OxTargetOption[] 23 | ---@return OxTargetOption[] 24 | local function checkOptions(options) 25 | local optionsType = type(options) 26 | 27 | if optionsType ~= 'table' then 28 | typeError('options', 'table', optionsType) 29 | end 30 | 31 | local tableType = table.type(options) 32 | 33 | if tableType == 'hash' and options.label then 34 | options = { options } 35 | elseif tableType ~= 'array' then 36 | typeError('options', 'array', ('%s table'):format(tableType)) 37 | end 38 | 39 | return options 40 | end 41 | 42 | ---@param data OxTargetPolyZone | table 43 | ---@return number 44 | function api.addPolyZone(data) 45 | if data.debug then utils.warn('Creating new PolyZone with debug enabled.') end 46 | 47 | data.resource = GetInvokingResource() 48 | data.options = checkOptions(data.options) 49 | return lib.zones.poly(data).id 50 | end 51 | 52 | ---@param data OxTargetBoxZone | table 53 | ---@return number 54 | function api.addBoxZone(data) 55 | if data.debug then utils.warn('Creating new BoxZone with debug enabled.') end 56 | 57 | data.resource = GetInvokingResource() 58 | data.options = checkOptions(data.options) 59 | return lib.zones.box(data).id 60 | end 61 | 62 | ---@param data OxTargetSphereZone | table 63 | ---@return number 64 | function api.addSphereZone(data) 65 | if data.debug then utils.warn('Creating new SphereZone with debug enabled.') end 66 | 67 | data.resource = GetInvokingResource() 68 | data.options = checkOptions(data.options) 69 | return lib.zones.sphere(data).id 70 | end 71 | 72 | ---@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. 73 | ---@return boolean returns true if the zone with the specified ID exists, otherwise false. 74 | function api.zoneExists(id) 75 | if not Zones or (type(id) ~= 'number' and type(id) ~= 'string') then return false end 76 | 77 | if type(id) == 'number' and Zones[id] then return true end 78 | 79 | for _, zone in pairs(lib.zones.getAllZones()) do 80 | if type(id) == 'string' and zone.name == id then return true end 81 | end 82 | 83 | return false 84 | end 85 | 86 | ---@param id number | string 87 | ---@param suppressWarning boolean? 88 | function api.removeZone(id, suppressWarning) 89 | if Zones then 90 | if type(id) == 'string' then 91 | local foundZone 92 | 93 | for _, v in pairs(lib.zones.getAllZones()) do 94 | if v.name == id then 95 | foundZone = true 96 | v:remove() 97 | end 98 | end 99 | 100 | if foundZone then return end 101 | elseif Zones[id] then 102 | return Zones[id]:remove() 103 | end 104 | end 105 | 106 | if suppressWarning then return end 107 | 108 | warn(('attempted to remove a zone that does not exist (id: %s)'):format(id)) 109 | end 110 | 111 | ---@param target table 112 | ---@param remove string | string[] 113 | ---@param resource string 114 | ---@param showWarning? boolean 115 | local function removeTarget(target, remove, resource, showWarning) 116 | if type(remove) ~= 'table' then remove = { remove } end 117 | 118 | for i = #target, 1, -1 do 119 | local option = target[i] 120 | 121 | if option.resource == resource then 122 | for j = #remove, 1, -1 do 123 | if option.name == remove[j] then 124 | table.remove(target, i) 125 | 126 | if showWarning then 127 | utils.warn(("Replacing existing target option '%s'."):format(option.name)) 128 | end 129 | end 130 | end 131 | end 132 | end 133 | end 134 | 135 | ---@param target table 136 | ---@param options OxTargetOption | OxTargetOption[] 137 | ---@param resource string 138 | local function addTarget(target, options, resource) 139 | options = checkOptions(options) 140 | 141 | local checkNames = {} 142 | 143 | resource = resource or 'ox_target' 144 | 145 | for i = 1, #options do 146 | local option = options[i] 147 | option.resource = resource 148 | 149 | if option.name then 150 | checkNames[#checkNames + 1] = option.name 151 | end 152 | end 153 | 154 | if checkNames[1] then 155 | removeTarget(target, checkNames, resource, true) 156 | end 157 | 158 | local num = #target 159 | 160 | for i = 1, #options do 161 | local option = options[i] 162 | 163 | if resource == 'ox_target' then 164 | if option.canInteract then 165 | option.canInteract = msgpack.unpack(msgpack.pack(option.canInteract)) 166 | end 167 | 168 | if option.onSelect then 169 | option.onSelect = msgpack.unpack(msgpack.pack(option.onSelect)) 170 | end 171 | end 172 | 173 | num += 1 174 | target[num] = options[i] 175 | end 176 | end 177 | 178 | ---@type table 179 | local peds = {} 180 | 181 | ---@param options OxTargetOption | OxTargetOption[] 182 | function api.addGlobalPed(options) 183 | addTarget(peds, options, GetInvokingResource()) 184 | end 185 | 186 | ---@param options string | string[] 187 | function api.removeGlobalPed(options) 188 | removeTarget(peds, options, GetInvokingResource()) 189 | end 190 | 191 | ---@type table 192 | local vehicles = {} 193 | 194 | ---@param options OxTargetOption | OxTargetOption[] 195 | function api.addGlobalVehicle(options) 196 | addTarget(vehicles, options, GetInvokingResource()) 197 | end 198 | 199 | ---@param options string | string[] 200 | function api.removeGlobalVehicle(options) 201 | removeTarget(vehicles, options, GetInvokingResource()) 202 | end 203 | 204 | ---@type table 205 | local objects = {} 206 | 207 | ---@param options OxTargetOption | OxTargetOption[] 208 | function api.addGlobalObject(options) 209 | addTarget(objects, options, GetInvokingResource()) 210 | end 211 | 212 | ---@param options string | string[] 213 | function api.removeGlobalObject(options) 214 | removeTarget(objects, options, GetInvokingResource()) 215 | end 216 | 217 | ---@type table 218 | local players = {} 219 | 220 | ---@param options OxTargetOption | OxTargetOption[] 221 | function api.addGlobalPlayer(options) 222 | addTarget(players, options, GetInvokingResource()) 223 | end 224 | 225 | ---@param options string | string[] 226 | function api.removeGlobalPlayer(options) 227 | removeTarget(players, options, GetInvokingResource()) 228 | end 229 | 230 | ---@type table 231 | local models = {} 232 | 233 | ---@param arr (number | string) | (number | string)[] 234 | ---@param options OxTargetOption | OxTargetOption[] 235 | function api.addModel(arr, options) 236 | if type(arr) ~= 'table' then arr = { arr } end 237 | local resource = GetInvokingResource() 238 | 239 | for i = 1, #arr do 240 | local model = arr[i] 241 | model = tonumber(model) or joaat(model) 242 | 243 | if not models[model] then 244 | models[model] = {} 245 | end 246 | 247 | addTarget(models[model], options, resource) 248 | end 249 | end 250 | 251 | ---@param arr (number | string) | (number | string)[] 252 | ---@param options? string | string[] 253 | function api.removeModel(arr, options) 254 | if type(arr) ~= 'table' then arr = { arr } end 255 | local resource = GetInvokingResource() 256 | 257 | for i = 1, #arr do 258 | local model = arr[i] 259 | model = tonumber(model) or joaat(model) 260 | 261 | if models[model] then 262 | if options then 263 | removeTarget(models[model], options, resource) 264 | end 265 | 266 | if not options or #models[model] == 0 then 267 | models[model] = nil 268 | end 269 | end 270 | end 271 | end 272 | 273 | ---@type table 274 | local entities = {} 275 | 276 | ---@param arr number | number[] 277 | ---@param options OxTargetOption | OxTargetOption[] 278 | function api.addEntity(arr, options) 279 | if type(arr) ~= 'table' then arr = { arr } end 280 | local resource = GetInvokingResource() 281 | 282 | for i = 1, #arr do 283 | local netId = arr[i] 284 | 285 | if NetworkDoesNetworkIdExist(netId) then 286 | if not entities[netId] then 287 | entities[netId] = {} 288 | 289 | if not Entity(NetworkGetEntityFromNetworkId(netId)).state.hasTargetOptions then 290 | TriggerServerEvent('ox_target:setEntityHasOptions', netId) 291 | end 292 | end 293 | 294 | addTarget(entities[netId], options, resource) 295 | end 296 | end 297 | end 298 | 299 | ---@param arr number | number[] 300 | ---@param options? string | string[] 301 | function api.removeEntity(arr, options) 302 | if type(arr) ~= 'table' then arr = { arr } end 303 | local resource = GetInvokingResource() 304 | 305 | for i = 1, #arr do 306 | local netId = arr[i] 307 | 308 | if entities[netId] then 309 | if options then 310 | removeTarget(entities[netId], options, resource) 311 | end 312 | 313 | if not options or #entities[netId] == 0 then 314 | entities[netId] = nil 315 | end 316 | end 317 | end 318 | end 319 | 320 | RegisterNetEvent('ox_target:removeEntity', api.removeEntity) 321 | 322 | ---@type table 323 | local localEntities = {} 324 | 325 | ---@param arr number | number[] 326 | ---@param options OxTargetOption | OxTargetOption[] 327 | function api.addLocalEntity(arr, options) 328 | if type(arr) ~= 'table' then arr = { arr } end 329 | local resource = GetInvokingResource() 330 | 331 | for i = 1, #arr do 332 | local entityId = arr[i] 333 | 334 | if DoesEntityExist(entityId) then 335 | if not localEntities[entityId] then 336 | localEntities[entityId] = {} 337 | end 338 | 339 | addTarget(localEntities[entityId], options, resource) 340 | else 341 | lib.print.warn(("No entity with id '%s' exists in %s."):format(entityId, resource)) 342 | end 343 | end 344 | end 345 | 346 | ---@param arr number | number[] 347 | ---@param options? table 348 | function api.removeLocalEntity(arr, options) 349 | if type(arr) ~= 'table' then arr = { arr } end 350 | local resource = GetInvokingResource() 351 | 352 | for i = 1, #arr do 353 | local entity = arr[i] 354 | 355 | if localEntities[entity] then 356 | if options then 357 | removeTarget(localEntities[entity], options, resource) 358 | end 359 | 360 | if not options or #localEntities[entity] == 0 then 361 | localEntities[entity] = nil 362 | end 363 | end 364 | end 365 | end 366 | 367 | CreateThread(function() 368 | while true do 369 | Wait(60000) 370 | 371 | for entityId in pairs(localEntities) do 372 | if not DoesEntityExist(entityId) then 373 | localEntities[entityId] = nil 374 | end 375 | end 376 | end 377 | end) 378 | 379 | ---@param resource string 380 | ---@param target table 381 | local function removeResourceGlobals(resource, target) 382 | for i = 1, #target do 383 | local options = target[i] 384 | 385 | for j = #options, 1, -1 do 386 | if options[j].resource == resource then 387 | table.remove(options, j) 388 | end 389 | end 390 | end 391 | end 392 | 393 | ---@param resource string 394 | ---@param target table 395 | local function removeResourceTargets(resource, target) 396 | for i = 1, #target do 397 | local tbl = target[i] 398 | 399 | for key, options in pairs(tbl) do 400 | for j = #options, 1, -1 do 401 | if options[j].resource == resource then 402 | table.remove(options, j) 403 | end 404 | end 405 | 406 | if #options == 0 then 407 | tbl[key] = nil 408 | end 409 | end 410 | end 411 | end 412 | 413 | ---@param resource string 414 | AddEventHandler('onClientResourceStop', function(resource) 415 | removeResourceGlobals(resource, { peds, vehicles, objects, players }) 416 | removeResourceTargets(resource, { models, entities, localEntities }) 417 | 418 | if Zones then 419 | for _, v in pairs(Zones) do 420 | if v.resource == resource then 421 | v:remove() 422 | end 423 | end 424 | end 425 | end) 426 | 427 | local NetworkGetEntityIsNetworked = NetworkGetEntityIsNetworked 428 | local NetworkGetNetworkIdFromEntity = NetworkGetNetworkIdFromEntity 429 | 430 | ---@class OxTargetOptions 431 | local options_mt = {} 432 | options_mt.__index = options_mt 433 | options_mt.size = 1 434 | 435 | function options_mt:wipe() 436 | options_mt.size = 1 437 | self.globalTarget = nil 438 | self.model = nil 439 | self.entity = nil 440 | self.localEntity = nil 441 | 442 | if self.__global[1]?.name == 'builtin:goback' then 443 | table.remove(self.__global, 1) 444 | end 445 | end 446 | 447 | ---@param entity? number 448 | ---@param _type? number 449 | ---@param model? number 450 | function options_mt:set(entity, _type, model) 451 | if not entity then return end 452 | 453 | if _type == 1 and IsPedAPlayer(entity) then 454 | self:wipe() 455 | self.globalTarget = players 456 | options_mt.size += 1 457 | 458 | return 459 | end 460 | 461 | local netId = NetworkGetEntityIsNetworked(entity) and NetworkGetNetworkIdFromEntity(entity) 462 | 463 | self.globalTarget = _type == 1 and peds or _type == 2 and vehicles or objects 464 | self.model = models[model] 465 | self.entity = netId and entities[netId] or nil 466 | self.localEntity = localEntities[entity] 467 | options_mt.size += 1 468 | 469 | if self.model then options_mt.size += 1 end 470 | if self.entity then options_mt.size += 1 end 471 | if self.localEntity then options_mt.size += 1 end 472 | end 473 | 474 | ---@type OxTargetOption[] 475 | local global = {} 476 | 477 | ---@param options OxTargetOption | OxTargetOption[] 478 | function api.addGlobalOption(options) 479 | addTarget(global, options, GetInvokingResource()) 480 | end 481 | 482 | ---@param options string | string[] 483 | function api.removeGlobalOption(options) 484 | removeTarget(global, options, GetInvokingResource()) 485 | end 486 | 487 | ---@class OxTargetOptions 488 | local options = setmetatable({ 489 | __global = global 490 | }, options_mt) 491 | 492 | ---@param entity? number 493 | ---@param _type? number 494 | ---@param model? number 495 | function api.getTargetOptions(entity, _type, model) 496 | if not entity then return options end 497 | 498 | if IsPedAPlayer(entity) then 499 | return { 500 | global = players, 501 | } 502 | end 503 | 504 | local netId = NetworkGetEntityIsNetworked(entity) and NetworkGetNetworkIdFromEntity(entity) 505 | 506 | return { 507 | global = _type == 1 and peds or _type == 2 and vehicles or objects, 508 | model = models[model], 509 | entity = netId and entities[netId] or nil, 510 | localEntity = localEntities[entity], 511 | } 512 | end 513 | 514 | local state = require 'client.state' 515 | 516 | function api.disableTargeting(value) 517 | if value then 518 | state.setActive(false) 519 | end 520 | 521 | state.setDisabled(value) 522 | end 523 | 524 | function api.isActive() 525 | return state.isActive() 526 | end 527 | 528 | return api 529 | --------------------------------------------------------------------------------