├── Client ├── Bones.lua ├── Client.lua ├── Framework.lua └── Utility.lua ├── Config.lua ├── LICENSE ├── README.md ├── Server └── Framework.lua ├── fxmanifest.lua └── ui ├── assets └── eye.png ├── index.html ├── main.js └── style.css /Client/Bones.lua: -------------------------------------------------------------------------------- 1 | Bones = {Options = {}, Vehicle = {'chassis', 'windscreen', 'seat_pside_r', 'seat_dside_r', 'bodyshell', 'suspension_lm', 'suspension_lr', 'platelight', 'attach_female', 'attach_male', 'bonnet', 'boot', 'chassis_dummy', 'chassis_Control', 'door_dside_f', 'door_dside_r', 'door_pside_f', 'door_pside_r', 'Gun_GripR', 'windscreen_f', 'platelight', 'VFX_Emitter', 'window_lf', 'window_lr', 'window_rf', 'window_rr', 'engine', 'gun_ammo', 'ROPE_ATTATCH', 'wheel_lf', 'wheel_lr', 'wheel_rf', 'wheel_rr', 'exhaust', 'overheat', 'seat_dside_f', 'seat_pside_f', 'Gun_Nuzzle', 'seat_r'}} 2 | 3 | if Config.EnableDefaultOptions then 4 | local BackEngineVehicles = { 5 | [`ninef`] = true, 6 | [`adder`] = true, 7 | [`vagner`] = true, 8 | [`t20`] = true, 9 | [`infernus`] = true, 10 | [`zentorno`] = true, 11 | [`reaper`] = true, 12 | [`comet2`] = true, 13 | [`comet3`] = true, 14 | [`jester`] = true, 15 | [`jester2`] = true, 16 | [`cheetah`] = true, 17 | [`cheetah2`] = true, 18 | [`prototipo`] = true, 19 | [`turismor`] = true, 20 | [`pfister811`] = true, 21 | [`ardent`] = true, 22 | [`nero`] = true, 23 | [`nero2`] = true, 24 | [`tempesta`] = true, 25 | [`vacca`] = true, 26 | [`bullet`] = true, 27 | [`osiris`] = true, 28 | [`entityxf`] = true, 29 | [`turismo2`] = true, 30 | [`fmj`] = true, 31 | [`re7b`] = true, 32 | [`tyrus`] = true, 33 | [`italigtb`] = true, 34 | [`penetrator`] = true, 35 | [`monroe`] = true, 36 | [`ninef2`] = true, 37 | [`stingergt`] = true, 38 | [`surfer`] = true, 39 | [`surfer2`] = true, 40 | [`gp1`] = true, 41 | [`autarch`] = true, 42 | [`tyrant`] = true 43 | } 44 | 45 | local function ToggleDoor(vehicle, door) 46 | if GetVehicleDoorLockStatus(vehicle) < 2 then 47 | if GetVehicleDoorAngleRatio(vehicle, door) > 0.0 then 48 | SetVehicleDoorShut(vehicle, door, false) 49 | else 50 | SetVehicleDoorOpen(vehicle, door, false) 51 | end 52 | end 53 | end 54 | 55 | local FrontDoorOptions = { 56 | ["Toggle Front Door"] = { 57 | icon = "fas fa-door-open", 58 | labels = { 59 | { 60 | label = "Open Front Door", 61 | condition = function(entity) 62 | return GetVehicleDoorAngleRatio(entity, 0) <= 0.0 63 | end 64 | }, 65 | { 66 | label = "Close Front Door" 67 | } 68 | }, 69 | canInteract = function(entity) 70 | return GetEntityBoneIndexByName(entity, 'door_dside_f') ~= -1 and GetVehicleDoorLockStatus(entity) < 2 71 | end, 72 | action = function(entity) 73 | ToggleDoor(entity, 0) 74 | end, 75 | distance = 1.8 76 | } 77 | } 78 | 79 | local BackDoorOptions = { 80 | ["Toggle Rear Door"] = { 81 | icon = "fas fa-door-open", 82 | labels = { 83 | { 84 | label = "Open Back Door", 85 | condition = function(entity) 86 | return GetVehicleDoorAngleRatio(entity, 2) <= 0.0 87 | end 88 | }, 89 | { 90 | label = "Close Back Door" 91 | } 92 | }, 93 | canInteract = function(entity) 94 | return GetEntityBoneIndexByName(entity, 'door_dside_r') ~= -1 and GetVehicleDoorLockStatus(entity) < 2 95 | end, 96 | action = function(entity) 97 | ToggleDoor(entity, 2) 98 | end, 99 | distance = 1.8 100 | } 101 | } 102 | 103 | local HoodOptions = { 104 | ["Toggle Hood"] = { 105 | icon = "fa-solid fa-wrench", 106 | labels = { 107 | { 108 | label = "Open Hood", 109 | condition = function(entity) 110 | return GetVehicleDoorAngleRatio(entity, BackEngineVehicles[GetEntityModel(entity)] and 5 or 4) <= 0.0 111 | end 112 | }, 113 | { 114 | label = "Close Hood", 115 | } 116 | }, 117 | action = function(entity) 118 | ToggleDoor(entity, BackEngineVehicles[GetEntityModel(entity)] and 5 or 4) 119 | end, 120 | distance = 1.8 121 | } 122 | } 123 | 124 | local TrunkOptions = { 125 | ["Toggle Trunk"] = { 126 | icon = "fas fa-truck-ramp-box", 127 | labels = { 128 | { 129 | label = "Open Trunk", 130 | condition = function(entity) 131 | return GetVehicleDoorAngleRatio(entity, BackEngineVehicles[GetEntityModel(entity)] and 4 or 5) <= 0.0 132 | end 133 | }, 134 | { 135 | label = "Close Trunk", 136 | } 137 | }, 138 | action = function(entity) 139 | ToggleDoor(entity, BackEngineVehicles[GetEntityModel(entity)] and 4 or 5) 140 | end, 141 | distance = 1.2 142 | }, 143 | } 144 | 145 | 146 | Bones.Options['seat_dside_f'] = FrontDoorOptions 147 | Bones.Options['seat_pside_f'] = FrontDoorOptions 148 | 149 | Bones.Options['seat_dside_r'] = BackDoorOptions 150 | Bones.Options['seat_pside_r'] = BackDoorOptions 151 | 152 | Bones.Options['bonnet'] = HoodOptions 153 | Bones.Options['boot'] = TrunkOptions 154 | end 155 | 156 | return Bones -------------------------------------------------------------------------------- /Client/Client.lua: -------------------------------------------------------------------------------- 1 | local display = false; 2 | local isMenuOpen = false; 3 | local flag = 30; 4 | local currentEntity = { 5 | target = -1, 6 | type = -1, 7 | hash = -1, 8 | closestBone = -1, 9 | endCoords = vector3(0, 0, 0), 10 | maxDistance = -1, 11 | distance = -1, 12 | options = {}, 13 | isPlayer = false, 14 | isZone = false, 15 | }; 16 | 17 | local successZone = false; 18 | local isTriedForSprites = false; 19 | 20 | local menuEntity = currentEntity; 21 | 22 | local Zones = {} 23 | 24 | local Globals = { 25 | PlayerOptions = Config.Globals.PlayerOptions, 26 | ObjectOptions = Config.Globals.ObjectOptions, 27 | VehicleOptions = Config.Globals.VehicleOptions, 28 | PedOptions = Config.Globals.PedOptions, 29 | } 30 | 31 | local SelfOptions = Config.SelfOptions; 32 | local VehicleOptions = {}; 33 | local Models = {}; 34 | local Entities = {}; 35 | local Players = {}; 36 | local Vehicles = {}; 37 | local Bones = Bones or {}; 38 | local controlIsEnabled = false; 39 | 40 | local Sprites = {} 41 | 42 | -- Draw sprites on screen 43 | function DrawSpirtes() 44 | CreateThread(function() 45 | while not HasStreamedTextureDictLoaded("shared") do Wait(10) RequestStreamedTextureDict("shared", true) end 46 | local sleep 47 | local colorCodes = { 48 | r, 49 | g, 50 | b, 51 | a 52 | } 53 | 54 | while display do 55 | sleep = 500 56 | for _, zone in pairs(Sprites) do 57 | sleep = 0 58 | for i, code in pairs(colorCodes) do 59 | code = zone.targetOptions.drawColor?[i] or Config.DrawColor[i] 60 | end 61 | 62 | if zone.success and #currentEntity.options > 0 then 63 | for i, code in pairs(colorCodes) do 64 | code = zone.targetOptions.successDrawColor?[i] or Config.SuccessDrawColor[i] 65 | end 66 | elseif zone.success and #currentEntity.options == 0 then 67 | for i, code in pairs(colorCodes) do 68 | code = zone.targetOptions.errorDrawColor?[i] or Config.ErrorDrawColor[i] 69 | end 70 | end 71 | 72 | SetDrawOrigin(zone.center.x, zone.center.y, zone.center.z, 0) 73 | DrawSprite("shared", "info_icon_32", 0, 0, 0.015, 0.025, 0, colorCodes[1], colorCodes[2], colorCodes[3], colorCodes[4]) 74 | ClearDrawOrigin() 75 | end 76 | Wait(sleep) 77 | end 78 | Sprites = {} 79 | end) 80 | end 81 | 82 | -- Keymapping for opening the context menu 83 | 84 | RegisterKeyMapping("+cmenu", Config.KeyMappingSettings.Label, "keyboard", Config.KeyMappingSettings.Key); 85 | 86 | RegisterCommand("+cmenu", function() 87 | SetDisplay(); 88 | end, false); 89 | 90 | RegisterCommand("-cmenu", function() 91 | if not isMenuOpen then 92 | SetDisplay(false); 93 | end 94 | end, false); 95 | 96 | -- NUI Callbacks 97 | 98 | RegisterNUICallback("SET_MENU_STATE", function(data, cb) 99 | isMenuOpen = data.state; 100 | if not isMenuOpen then 101 | SetDisplay(false); 102 | else 103 | menuEntity = currentEntity; 104 | end 105 | end) 106 | 107 | RegisterNUICallback("OPTION_SELECTED", function(data, cb) 108 | OptionSelected(data.index); 109 | cb("ok"); 110 | end) 111 | 112 | RegisterNUICallback("REFRESH_CURRENT_ENTITY_OPTIONS", function(data, cb) 113 | SendOptions(currentEntity.options); 114 | menuEntity = currentEntity; 115 | cb("ok"); 116 | end) 117 | 118 | -- NUI Messages 119 | 120 | function SendOptions(options) 121 | SendNUIMessage({ 122 | action = "SET_OPTIONS", 123 | options = RemoveAllFunctionsFromOptions(options), 124 | }); 125 | end 126 | 127 | function CloseNUIContextMenu() 128 | SendNUIMessage({ 129 | action = "CLOSE_CONTEXT_MENU", 130 | }); 131 | 132 | isMenuOpen = false; 133 | end 134 | 135 | function ChangeItemState(index, state) 136 | SendNUIMessage({ 137 | action = "CHANGE_ITEM_STATE", 138 | index = index, 139 | state = state, 140 | }); 141 | end 142 | 143 | -- Functions 144 | 145 | function OptionSelected(optionIndex) 146 | local option = GetOptionByIndex(optionIndex); 147 | 148 | if option then 149 | if option.action then 150 | option.action(option.entity) 151 | elseif option.event then 152 | if option.type == "client" then 153 | TriggerEvent(option.event, option) 154 | elseif option.type == "server" then 155 | TriggerServerEvent(option.event, option) 156 | elseif option.type == "command" then 157 | ExecuteCommand(option.event) 158 | elseif option.type == "qbcommand" then 159 | TriggerServerEvent('QBCore:CallCommand', option.event, option) 160 | else 161 | TriggerEvent(option.event, option) 162 | end 163 | end 164 | end 165 | end 166 | 167 | function RefreshCurrentEntityAndSend() 168 | DrawOutlineEntity(currentEntity.target, false) 169 | 170 | currentEntity = { 171 | target = -1, 172 | type = 0, 173 | hash = 0, 174 | closestBone = 0, 175 | endCoords = currentEntity.endCoords, 176 | distance = 0, 177 | options = {}, 178 | isPlayer = false, 179 | }; 180 | 181 | if not isMenuOpen then 182 | menuEntity = currentEntity; 183 | end 184 | 185 | SendOptions(currentEntity.options); 186 | end 187 | 188 | function SetDisplay(bool) 189 | if not Config.OpenConditons() then return end 190 | 191 | isTriedForSprites = false; 192 | 193 | if bool ~= nil then 194 | display = bool; 195 | else 196 | display = not display; 197 | end 198 | 199 | SetNuiFocus(display, display); 200 | SetNuiFocusKeepInput(display); 201 | SetCursorLocation(0.5, 0.5); 202 | 203 | if not display then 204 | DrawOutlineEntity(currentEntity.target, false) 205 | RefreshCurrentEntityAndSend(); 206 | CloseNUIContextMenu(); 207 | else 208 | if Config.DrawSprite then 209 | DrawSpirtes(); 210 | end 211 | end 212 | end 213 | 214 | function GetOptionByIndex(index, options) 215 | options = options or menuEntity.options; 216 | 217 | local optionIndexs = type(index) == "string" and Split(index, ".") or {index}; 218 | for _ = 1, #optionIndexs do 219 | optionIndexs[_] = tonumber(optionIndexs[_]); 220 | end 221 | if #optionIndexs > 1 then 222 | local deletedOption = table.remove(optionIndexs, 1); 223 | 224 | local optionIndexString = table.concat(optionIndexs, "."); 225 | return GetOptionByIndex(optionIndexString, options[deletedOption].subOptions); 226 | else 227 | return options[tonumber(optionIndexs[1])]; 228 | end 229 | return option; 230 | end 231 | 232 | function RemoveAllFunctionsFromOptions(options) 233 | local newOptions = {}; 234 | for _, option in pairs(options) do 235 | local newOption = {}; 236 | for key, value in pairs(option) do 237 | if type(value) ~= "function" and key ~= "labels" then 238 | newOption[key] = value; 239 | end 240 | 241 | if key == "subOptions" then 242 | newOption[key] = RemoveAllFunctionsFromOptions(value); 243 | end 244 | end 245 | table.insert(newOptions, newOption); 246 | end 247 | return newOptions; 248 | end 249 | 250 | 251 | function GetOptions(entity) 252 | local options = {}; 253 | 254 | local isAnyVisible = false; 255 | 256 | local coords = GetEntityCoords(PlayerPedId()); 257 | 258 | if entity.target == PlayerPedId() then 259 | for _, option in pairs(SelfOptions) do 260 | table.insert(options, option); 261 | end 262 | end 263 | 264 | if Models[entity.hash] then 265 | for _, option in pairs(Models[entity.hash]) do 266 | table.insert(options, option); 267 | end 268 | end 269 | 270 | if Entities[entity.target] then 271 | for _, option in pairs(Entities[entity.target]) do 272 | table.insert(options, option); 273 | end 274 | 275 | if GlobalObjectOptions then 276 | for _, option in pairs(GlobalObjectOptions) do 277 | table.insert(options, option); 278 | end 279 | end 280 | end 281 | 282 | if NetworkGetEntityIsNetworked(entity.target) then 283 | local netId = NetworkGetNetworkIdFromEntity(entity.target); 284 | if Entities[netId] then 285 | for _, option in pairs(Entities[netId]) do 286 | table.insert(options, option); 287 | end 288 | end 289 | 290 | 291 | if Globals.ObjectOptions then 292 | for _, option in pairs(Globals.ObjectOptions) do 293 | table.insert(options, option); 294 | end 295 | end 296 | end 297 | 298 | if entity.type == 1 then 299 | if IsPedAPlayer(entity.target) and entity.target ~= PlayerPedId() then 300 | if Globals.PlayerOptions then 301 | for _, option in pairs(Globals.PlayerOptions) do 302 | table.insert(options, option); 303 | end 304 | end 305 | else 306 | if Globals.PedOptions then 307 | for _, option in pairs(Globals.PedOptions) do 308 | table.insert(options, option); 309 | end 310 | end 311 | end 312 | elseif entity.type == 2 then 313 | local closestBone, _, closestBoneName = CheckBones(entity.endCoords, entity.target, Bones.Vehicle) 314 | local datatable = Bones.Options[closestBoneName]; 315 | 316 | if datatable and closestBone then 317 | for _, option in pairs(datatable) do 318 | table.insert(options, option); 319 | end 320 | end 321 | 322 | if Globals.VehicleOptions then 323 | for _, option in pairs(Globals.VehicleOptions) do 324 | table.insert(options, option); 325 | end 326 | end 327 | end 328 | 329 | for k, zone in pairs(Zones) do 330 | if Config.DrawSprite then 331 | if #(currentEntity.endCoords - zone.center) < (zone.targetOptions.drawDistance or Config.DrawDistance) then 332 | Sprites[k] = zone 333 | else 334 | Sprites[k] = nil 335 | end 336 | end 337 | 338 | if entity.distance < (Config.MaxDistance) and entity.distance <= zone.targetOptions.distance and zone:isPointInside(currentEntity.endCoords) then 339 | entity.maxDistance = zone.targetOptions.distance; 340 | local zOptions = zone.targetOptions.options; 341 | if #zOptions > 0 then 342 | for _, option in pairs(zOptions) do 343 | table.insert(options, option); 344 | end 345 | 346 | Sprites[k].success = true; 347 | entity.isZone = true; 348 | successZone = k; 349 | end 350 | end 351 | end 352 | 353 | 354 | function AddIndexAndCheckOptions(options, currentEntity, index) 355 | for _, option in pairs(options) do 356 | option.index = index and index .. "." .. _ or _; 357 | option.entity = currentEntity.target; 358 | if CheckOption(option, currentEntity) then 359 | option.display = true; 360 | 361 | if option.labels and next(option.labels) ~= nil then 362 | local isLabelFound = false; 363 | for labelKey, label in pairs(option.labels) do 364 | if label.condition and label.condition(currentEntity.target) then 365 | option.label = label.label; 366 | isLabelFound = true; 367 | break; 368 | end 369 | 370 | if not isLabelFound and #option.labels == labelKey then 371 | option.label = label.label 372 | end 373 | end 374 | end 375 | 376 | if option.action or option.event then 377 | option.isAction = true; 378 | end 379 | 380 | if option.subOptions then 381 | option.subOptions = AddIndexAndCheckOptions(option.subOptions, currentEntity, option.index); 382 | end 383 | isAnyVisible = true; 384 | else 385 | option.display = false; 386 | end 387 | end 388 | return options; 389 | end 390 | 391 | return AddIndexAndCheckOptions(options, entity), isAnyVisible; 392 | end 393 | 394 | Citizen.CreateThread(function() 395 | while true do 396 | local run = true; 397 | 398 | if not display then 399 | Citizen.Wait(200); 400 | end 401 | 402 | if not display and controlIsEnabled then 403 | run = false; 404 | end 405 | 406 | if run then 407 | DisableControlAction(0, 142, display); -- MeleeAttackAlternate 408 | DisableControlAction(0, 24, display); -- Attack 409 | DisableControlAction(0, 25, display); -- Aim 410 | DisableControlAction(0, 47, display); -- Weapon 411 | DisableControlAction(0, 199, display); -- PauseMenu 412 | DisableControlAction(0, 200, display); -- ESC 413 | 414 | -- Disable mouse 415 | if not Config.EnableMouseRotate then 416 | DisableControlAction(0, 1, display); -- LookLeftRight 417 | DisableControlAction(0, 2, display); -- LookUpDown 418 | DisableControlAction(0, 106, display); -- VehicleMouseControlOverride 419 | end 420 | 421 | if not display then 422 | controlIsEnabled = true; 423 | end 424 | end 425 | 426 | Citizen.Wait(0); 427 | end 428 | end) 429 | 430 | Citizen.CreateThread(function() 431 | while true do 432 | -- Check distance of opened menu's option 433 | local ped = PlayerPedId(); 434 | if display and menuEntity.target ~= -1 and menuEntity.target ~= ped then 435 | menuEntity.distance = #(menuEntity.endCoords - GetEntityCoords(ped)); 436 | 437 | if menuEntity.maxDistance and menuEntity.distance > menuEntity.maxDistance then 438 | RefreshCurrentEntityAndSend(); 439 | CloseNUIContextMenu(); 440 | end 441 | 442 | isAnyVisible = false; 443 | 444 | function CheckOptions(options) 445 | for _, option in pairs(options) do 446 | local checkOption = CheckOption(option, menuEntity) 447 | if checkOption and not option.display then 448 | option.display = true; 449 | ChangeItemState(option.index, true); 450 | elseif not checkOption and option.display then 451 | ChangeItemState(option.index, false); 452 | option.display = false; 453 | end 454 | if option.subOptions then 455 | CheckOptions(option.subOptions); 456 | end 457 | 458 | if option.display then 459 | isAnyVisible = true; 460 | end 461 | end 462 | end 463 | CheckOptions(menuEntity.options); 464 | 465 | if not isAnyVisible then 466 | RefreshCurrentEntityAndSend(); 467 | CloseNUIContextMenu(); 468 | end 469 | else 470 | Citizen.Wait(1000); 471 | end 472 | 473 | Citizen.Wait(300) 474 | end 475 | end) 476 | 477 | -- Main Thread 478 | 479 | Citizen.CreateThread(function() 480 | local flag = -1; 481 | local isSuccess 482 | local isRaycastSuccess = false; 483 | while true do 484 | Citizen.Wait(0); 485 | if display then 486 | local ped = PlayerPedId(); 487 | local pedCoords = GetEntityCoords(ped); 488 | 489 | flag = not isRaycastSuccess and flag == -1 and 30 or -1; 490 | 491 | local hit, entityHit, entityType, direction, distance, endCoords = RaycastCursor(flag); 492 | 493 | entityHit = entityHit or 0; 494 | entityType = entityType or 0; 495 | 496 | 497 | if hit then 498 | isRaycastSuccess = true; 499 | else 500 | isRaycastSuccess = false; 501 | end 502 | 503 | -- Check if the entity is the same as the last one 504 | if entityHit == currentEntity.target and ((distance - currentEntity.distance) < 1 or currentEntity.target == ped) and not Config.Debug then 505 | Citizen.Wait(100); 506 | else 507 | DrawOutlineEntity(currentEntity.target, false); 508 | 509 | flag = 30; 510 | local hash = entityType > 0 and GetEntityModel(entityHit) or 0; 511 | 512 | local entity = { 513 | target = entityHit, 514 | type = entityType, 515 | hash = hash, 516 | endCoords = endCoords, 517 | distance = distance, 518 | isPlayer = false, 519 | }; 520 | 521 | if entityType == 1 then 522 | local player = GetPlayerFromServerId(entityHit); 523 | if player ~= -1 then 524 | entity.isPlayer = true; 525 | end 526 | end 527 | 528 | currentEntity = entity; 529 | 530 | local options, isAnyVisible = GetOptions(entity); 531 | 532 | if #options > 0 and isAnyVisible then 533 | isSuccess = true; 534 | if not currentEntity.isZone then 535 | DrawOutlineEntity(currentEntity.target, true); 536 | end 537 | currentEntity.options = options; 538 | SendOptions(options); 539 | else 540 | RefreshCurrentEntityAndSend(); 541 | if not Config.Debug then 542 | Citizen.Wait(100); 543 | end 544 | 545 | if successZone then 546 | if Sprites[successZone] then 547 | Sprites[successZone].success = false; 548 | end 549 | 550 | successZone = false; 551 | end 552 | 553 | isSuccess = false; 554 | end 555 | end 556 | 557 | -- Debug 558 | if Config.Debug then 559 | if hit == 0 then 560 | entityType = 0; 561 | end 562 | 563 | local entityName = GetEntityName(entityType) or "Unknown"; 564 | 565 | print("Entity Hit: " .. entityHit); 566 | print("Entity Type: " .. entityType); 567 | print("Entity Name: " .. entityName); 568 | print("Entity Distance: " .. distance); 569 | if entity then 570 | print("Entity Hash: " .. entity.hash); 571 | print("Entity Options: " .. #entity.options); 572 | end 573 | 574 | DrawLine(pedCoords, direction, hit == 1 and 0 or 255, hit == 1 and 255 or 0, 0, 255); 575 | end 576 | else 577 | Citizen.Wait(150); 578 | end 579 | end 580 | end) 581 | 582 | function CheckBones(coords, entity, bonelist) 583 | local closestBone = -1 584 | local closestDistance = 20 585 | local closestPos, closestBoneName 586 | for _, v in pairs(bonelist) do 587 | if Bones.Options[v] then 588 | local boneId = GetEntityBoneIndexByName(entity, v) 589 | local bonePos = GetWorldPositionOfEntityBone(entity, boneId) 590 | local distance = #(coords - bonePos) 591 | if closestBone == -1 or distance < closestDistance then 592 | closestBone, closestDistance, closestPos, closestBoneName = boneId, distance, bonePos, v 593 | end 594 | end 595 | end 596 | if closestBone ~= -1 then return closestBone, closestPos, closestBoneName 597 | else return false end 598 | end 599 | 600 | function AddSelfOption(option) 601 | SelfOptions[option.label] = option 602 | end 603 | 604 | local function SetOptions(table, distance, options) 605 | for _, v in pairs(options) do 606 | if v.required_item then 607 | v.item = v.required_item 608 | v.required_item = nil 609 | end 610 | if not v.distance or v.distance > distance then v.distance = distance end 611 | table[v.label] = v 612 | end 613 | end 614 | 615 | local function AddGlobalObject(options) 616 | options.distance = options.distance or Config.MaxDistance 617 | SetOptions(Globals.ObjectOptions, options.distance, options.options) 618 | end 619 | 620 | local function AddGlobalPed(options) 621 | options.distance = options.distance or Config.MaxDistance 622 | SetOptions(Globals.PedOptions, options.distance, options.options) 623 | end 624 | 625 | local function AddGlobalVehicle(options) 626 | options.distance = options.distance or Config.MaxDistance 627 | SetOptions(Globals.VehicleOptions, options.distance, options.options) 628 | end 629 | 630 | local function AddGlobalPlayer(options) 631 | options.distance = options.distance or Config.MaxDistance 632 | SetOptions(Globals.PlayerOptions, options.distance, options.options) 633 | end 634 | 635 | local function AddCircleZone(name, center, radius, zoneOptions, options) 636 | local centerType = type(center) 637 | center = (centerType == 'table' or centerType == 'vector4') and vec3(center.x, center.y, center.z) or center 638 | Zones[name] = CircleZone:Create(center, radius, zoneOptions) 639 | options.distance = options.distance or Config.MaxDistance 640 | Zones[name].targetOptions = options 641 | return Zones[name] 642 | end 643 | 644 | local function AddBoxZone(name, center, length, width, zoneOptions, options) 645 | local centerType = type(center) 646 | center = (centerType == 'table' or centerType == 'vector4') and vec3(center.x, center.y, center.z) or center 647 | Zones[name] = BoxZone:Create(center, length, width, zoneOptions) 648 | options.distance = options.distance or Config.MaxDistance 649 | Zones[name].targetOptions = options 650 | return Zones[name] 651 | end 652 | 653 | local function AddPolyZone(name, points, zoneOptions, options) 654 | local _points = {} 655 | local pointsType = type(points[1]) 656 | if pointsType == 'table' or pointsType == 'vector3' or pointsType == 'vector4' then 657 | for i = 1, #points do 658 | _points[i] = vec2(points[i].x, points[i].y) 659 | end 660 | end 661 | Zones[name] = PolyZone:Create(#_points > 0 and _points or points, zoneOptions) 662 | options.distance = options.distance or Config.MaxDistance 663 | Zones[name].targetOptions = options 664 | return Zones[name] 665 | end 666 | 667 | local function AddComboZone(zones, zoneOptions, options) 668 | Zones[options.name] = ComboZone:Create(zones, zoneOptions) 669 | options.distance = options.distance or Config.MaxDistance 670 | Zones[options.name].targetOptions = options 671 | return Zones[options.name] 672 | end 673 | 674 | local function AddEntityZone(name, entity, zoneOptions, options) 675 | Zones[name] = EntityZone:Create(entity, zoneOptions) 676 | options.distance = options.distance or Config.MaxDistance 677 | Zones[name].targetOptions = options 678 | return Zones[name] 679 | end 680 | 681 | local function RemoveZone(name) 682 | if not Zones[name] then return end 683 | if Zones[name].destroy then Zones[name]:destroy() end 684 | Zones[name] = nil 685 | end 686 | 687 | local function AddTargetBone(bones, parameters) 688 | bones = type(bones) == 'table' and bones or {bones} 689 | local distance, options = parameters.distance or Config.MaxDistance, parameters.options 690 | 691 | for _, bone in pairs(bones) do 692 | if not Bones.Options[bone] then Bones.Options[bone] = {} end 693 | SetOptions(Bones.Options[bone], distance, options) 694 | end 695 | end 696 | 697 | local function RemoveTargetBone(bones, labels) 698 | bones = type(bones) == 'table' and bones or {bones} 699 | for _, bone in pairs(bones) do 700 | if labels then 701 | labels = type(labels) == 'table' and labels or {labels} 702 | for _, v in pairs(labels) do 703 | if Bones.Options[bone] then 704 | Bones.Options[bone][v] = nil 705 | end 706 | end 707 | else 708 | Bones.Options[bone] = nil 709 | end 710 | end 711 | end 712 | 713 | local function AddTargetEntity(entities, parameters) 714 | entities = type(entities) == 'table' and entities or {entities} 715 | local distance, options = parameters.distance or Config.MaxDistance, parameters.options 716 | 717 | for _, entity in pairs(entities) do 718 | if NetworkGetEntityIsNetworked(entity) then entity = NetworkGetNetworkIdFromEntity(entity) end -- Allow non-networked entities to be targeted 719 | if not Entities[entity] then Entities[entity] = {} end 720 | SetOptions(Entities[entity], distance, options) 721 | end 722 | end 723 | 724 | local function RemoveTargetEntity(entities, labels) 725 | entities = type(entities) == 'table' and entities or {entities} 726 | 727 | for _, entity in pairs(entities) do 728 | if NetworkGetEntityIsNetworked(entity) then entity = NetworkGetNetworkIdFromEntity(entity) end -- Allow non-networked entities to be targeted 729 | if labels then 730 | labels = type(labels) == 'table' and labels or {labels} 731 | for _, v in pairs(labels) do 732 | if Entities[entity] then 733 | Entities[entity][v] = nil 734 | end 735 | end 736 | else 737 | Entities[entity] = nil 738 | end 739 | end 740 | end 741 | 742 | local function AddTargetModel(models, parameters) 743 | models = type(models) == 'table' and models or {models} 744 | local distance, options = parameters.distance or Config.MaxDistance, parameters.options 745 | 746 | for _, model in pairs(models) do 747 | if type(model) == 'string' then model = joaat(model) end 748 | if not Models[model] then Models[model] = {} end 749 | SetOptions(Models[model], distance, options) 750 | end 751 | end 752 | 753 | local function RemoveTargetModel(models, labels) 754 | models = type(models) == 'table' and models or {models} 755 | 756 | for _, model in pairs(models) do 757 | if type(model) == 'string' then model = joaat(model) end 758 | if labels then 759 | labels = type(labels) == 'table' and labels or {labels} 760 | for _, v in pairs(labels) do 761 | if Models[model] then 762 | Models[model][v] = nil 763 | end 764 | end 765 | else 766 | Models[model] = nil 767 | end 768 | end 769 | end 770 | 771 | local function SpawnPed(data) 772 | local spawnedped 773 | local key, value = next(data) 774 | if type(value) ~= 'table' and type(key) == 'string' then 775 | data = {data} 776 | end 777 | 778 | for _, v in pairs(data) do 779 | if v.spawnNow then 780 | RequestModel(v.model) 781 | while not HasModelLoaded(v.model) do 782 | Wait(0) 783 | end 784 | 785 | if type(v.model) == 'string' then v.model = joaat(v.model) end 786 | 787 | if v.minusOne then 788 | spawnedped = CreatePed(0, v.model, v.coords.x, v.coords.y, v.coords.z - 1.0, v.coords.w or 0.0, v.networked or false, true) 789 | else 790 | spawnedped = CreatePed(0, v.model, v.coords.x, v.coords.y, v.coords.z, v.coords.w or 0.0, v.networked or false, true) 791 | end 792 | 793 | if v.freeze then 794 | FreezeEntityPosition(spawnedped, true) 795 | end 796 | 797 | if v.invincible then 798 | SetEntityInvincible(spawnedped, true) 799 | end 800 | 801 | if v.blockevents then 802 | SetBlockingOfNonTemporaryEvents(spawnedped, true) 803 | end 804 | 805 | if v.animDict and v.anim then 806 | RequestAnimDict(v.animDict) 807 | while not HasAnimDictLoaded(v.animDict) do 808 | Wait(0) 809 | end 810 | 811 | TaskPlayAnim(spawnedped, v.animDict, v.anim, 8.0, 0, -1, v.flag or 1, 0, false, false, false) 812 | end 813 | 814 | if v.scenario then 815 | SetPedCanPlayAmbientAnims(spawnedped, true) 816 | TaskStartScenarioInPlace(spawnedped, v.scenario, 0, true) 817 | end 818 | 819 | if v.pedrelations and type(v.pedrelations.groupname) == 'string' then 820 | if type(v.pedrelations.groupname) ~= 'string' then error(v.pedrelations.groupname .. ' is not a string') end 821 | 822 | local pedgrouphash = joaat(v.pedrelations.groupname) 823 | 824 | if not DoesRelationshipGroupExist(pedgrouphash) then 825 | AddRelationshipGroup(v.pedrelations.groupname) 826 | end 827 | 828 | SetPedRelationshipGroupHash(spawnedped, pedgrouphash) 829 | if v.pedrelations.toplayer then 830 | SetRelationshipBetweenGroups(v.pedrelations.toplayer, pedgrouphash, joaat('PLAYER')) 831 | end 832 | 833 | if v.pedrelations.toowngroup then 834 | SetRelationshipBetweenGroups(v.pedrelations.toowngroup, pedgrouphash, pedgrouphash) 835 | end 836 | end 837 | 838 | if v.weapon then 839 | if type(v.weapon.name) == 'string' then v.weapon.name = joaat(v.weapon.name) end 840 | 841 | if IsWeaponValid(v.weapon.name) then 842 | SetCanPedEquipWeapon(spawnedped, v.weapon.name, true) 843 | GiveWeaponToPed(spawnedped, v.weapon.name, v.weapon.ammo, v.weapon.hidden or false, true) 844 | SetPedCurrentWeaponVisible(spawnedped, not v.weapon.hidden or false, true) 845 | end 846 | end 847 | 848 | if v.target then 849 | if v.target.useModel then 850 | AddTargetModel(v.model, { 851 | options = v.target.options, 852 | distance = v.target.distance 853 | }) 854 | else 855 | AddTargetEntity(spawnedped, { 856 | options = v.target.options, 857 | distance = v.target.distance 858 | }) 859 | end 860 | end 861 | 862 | v.currentpednumber = spawnedped 863 | 864 | if v.action then 865 | v.action(v) 866 | end 867 | end 868 | 869 | local nextnumber = #Config.Peds + 1 870 | if nextnumber <= 0 then nextnumber = 1 end 871 | 872 | Config.Peds[nextnumber] = v 873 | end 874 | end 875 | 876 | Citizen.CreateThread(function() 877 | if table.type(Config.CircleZones) ~= 'empty' then 878 | for _, v in pairs(Config.CircleZones) do 879 | AddCircleZone(v.name, v.coords, v.radius, { 880 | name = v.name, 881 | debugPoly = v.debugPoly, 882 | useZ = v.useZ, 883 | }, { 884 | options = v.options, 885 | distance = v.distance 886 | }) 887 | end 888 | end 889 | 890 | if table.type(Config.BoxZones) ~= 'empty' then 891 | for _, v in pairs(Config.BoxZones) do 892 | AddBoxZone(v.name, v.coords, v.length, v.width, { 893 | name = v.name, 894 | heading = v.heading, 895 | debugPoly = v.debugPoly, 896 | minZ = v.minZ, 897 | maxZ = v.maxZ 898 | }, { 899 | options = v.options, 900 | distance = v.distance 901 | }) 902 | end 903 | end 904 | 905 | if table.type(Config.PolyZones) ~= 'empty' then 906 | for _, v in pairs(Config.PolyZones) do 907 | AddPolyZone(v.name, v.points, { 908 | name = v.name, 909 | debugPoly = v.debugPoly, 910 | minZ = v.minZ, 911 | maxZ = v.maxZ 912 | }, { 913 | options = v.options, 914 | distance = v.distance 915 | }) 916 | end 917 | end 918 | 919 | if table.type(Config.TargetBones) ~= 'empty' then 920 | for _, v in pairs(Config.TargetBones) do 921 | AddTargetBone(v.bones, { 922 | options = v.options, 923 | distance = v.distance 924 | }) 925 | end 926 | end 927 | 928 | if table.type(Config.TargetModels) ~= 'empty' then 929 | for _, v in pairs(Config.TargetModels) do 930 | AddTargetModel(v.models, { 931 | options = v.options, 932 | distance = v.distance 933 | }) 934 | end 935 | end 936 | end) 937 | 938 | function IsDisplay() 939 | return display 940 | end 941 | 942 | exports('CheckBones', CheckBones) 943 | 944 | exports("AddGlobalObject", AddGlobalObject) 945 | exports("AddGlobalPed", AddGlobalPed) 946 | exports("AddGlobalVehicle", AddGlobalVehicle) 947 | exports("AddGlobalPlayer", AddGlobalPlayer) 948 | 949 | exports("AddTargetEntity", AddTargetEntity) 950 | exports("AddTargetModel", AddTargetModel) 951 | exports("AddTargetBone", AddTargetBone) 952 | exports("AddCircleZone", AddCircleZone) 953 | exports("AddBoxZone", AddBoxZone) 954 | exports("AddPolyZone", AddPolyZone) 955 | exports("AddComboZone", AddComboZone) 956 | exports("AddEntityZone", AddEntityZone) 957 | 958 | exports("RemoveTargetBone", RemoveTargetBone) 959 | exports("RemoveTargetEntity", RemoveTargetEntity) 960 | exports("RemoveTargetModel", RemoveTargetModel) 961 | exports("RemoveZone", RemoveZone) 962 | 963 | exports("SpawnPed", SpawnPed) 964 | 965 | exports("IsDisplay", IsDisplay) 966 | 967 | local contextExports = { 968 | ["CheckBones"] = CheckBones, 969 | 970 | ["AddGlobalObject"] = AddGlobalObject, 971 | ["AddGlobalPed"] = AddGlobalPed, 972 | ["AddGlobalVehicle"] = AddGlobalVehicle, 973 | ["AddGlobalPlayer"] = AddGlobalPlayer, 974 | 975 | ["AddTargetEntity"] = AddTargetEntity, 976 | ["AddTargetModel"] = AddTargetModel, 977 | ["AddTargetBone"] = AddTargetBone, 978 | ["AddCircleZone"] = AddCircleZone, 979 | ["AddBoxZone"] = AddBoxZone, 980 | ["AddComboZone"] = AddComboZone, 981 | ["AddEntityZone"] = AddEntityZone, 982 | ["AddTargetModel"] = AddTargetModel, 983 | 984 | ["RemoveTargetBone"] = RemoveTargetBone, 985 | ["RemoveTargetEntity"] = RemoveTargetEntity, 986 | ["RemoveTargetModel"] = RemoveTargetModel, 987 | ["RemoveZone"] = RemoveZone, 988 | 989 | ["SpawnPed"] = SpawnPed, 990 | 991 | ["IsDisplay"] = IsDisplay 992 | } 993 | 994 | for exportName, func in pairs(contextExports) do 995 | AddEventHandler(('__cfx_export_qb-target_%s'):format(exportName), function(setCB) 996 | setCB(func) 997 | end) 998 | end 999 | -------------------------------------------------------------------------------- /Client/Framework.lua: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------- 2 | -- Functions 3 | ------------------------------------------------------------------------------- 4 | 5 | -- Heavily inspired by QB-Target 6 | 7 | local function JobCheck() return true end 8 | local function GangCheck() return true end 9 | local function JobTypeCheck() return true end 10 | local function ItemCheck() return true end 11 | local function CitizenCheck() return true end 12 | 13 | 14 | CreateThread(function() 15 | local state = GetResourceState('qb-core') 16 | if state ~= 'missing' then 17 | local timeout = 0 18 | while state ~= 'started' and timeout <= 100 do 19 | timeout = timeout + 1 20 | state = GetResourceState('qb-core') 21 | Wait(0) 22 | end 23 | Config.Standalone = false 24 | end 25 | if Config.Standalone then 26 | local firstSpawn = false 27 | local event = AddEventHandler('playerSpawned', function() 28 | SpawnPeds() 29 | firstSpawn = true 30 | end) 31 | -- Remove event after it has been triggered 32 | while true do 33 | if firstSpawn then 34 | RemoveEventHandler(event) 35 | break 36 | end 37 | Wait(1000) 38 | end 39 | else 40 | local QBCore = exports['qb-core']:GetCoreObject() 41 | local PlayerData = QBCore.Functions.GetPlayerData() 42 | 43 | AddFastActionToMetadata = function(type) 44 | local PlayerData = QBCore.Functions.GetPlayerData() 45 | local fastactions = PlayerData.metadata["fastactions"] or {animations = {}, commands = {}} 46 | local headerName = type == "animations" and "animation" or "command" 47 | local action = exports['qb-input']:ShowInput({ 48 | header = "Add new " .. headerName, 49 | submitText = "Add", 50 | inputs = { 51 | { 52 | type = 'text', 53 | name = 'label', 54 | text = "Title" 55 | }, 56 | { 57 | type = 'text', 58 | isRequired = true, 59 | name = 'action', 60 | text = type == "animations" and "Animation (sit)" or "Command (seat)" 61 | } 62 | } 63 | }) 64 | 65 | if fastactions[type] == nil then 66 | fastactions[type] = {} 67 | end 68 | 69 | if action then 70 | table.insert(fastactions[type], { 71 | label = #action.label > 0 and action.label or action.action, 72 | action = action.action, 73 | }) 74 | end 75 | 76 | TriggerServerEvent('luck-contextmenu:server:SetFastActions', fastactions) 77 | 78 | AddFastActions(fastactions["animations"], fastactions["commands"]) 79 | end 80 | 81 | AddFastActions = function(animations, commands) 82 | animations = animations or {} 83 | commands = commands or {} 84 | 85 | -- If animations more than 10, player can't add new animations 86 | local animationSubOptions = #animations < 10 and {{ 87 | label = "Add Animation", 88 | icon = "fa-solid fa-circle-plus", 89 | action = function() 90 | AddFastActionToMetadata("animations") 91 | end, 92 | }} or {} 93 | 94 | local commandSubOptions = #commands < 10 and {{ 95 | label = "Add Command", 96 | icon = "fa-solid fa-circle-plus", 97 | action = function() 98 | AddFastActionToMetadata("commands") 99 | end, 100 | }} or {} 101 | 102 | if animations then 103 | for _, v in pairs(animations) do 104 | table.insert(animationSubOptions, { 105 | label = v.label, 106 | event = "e " .. v.action, 107 | icon = "fa-solid fa-face-smile", 108 | type = "animation", 109 | subOptions = { 110 | { 111 | label = "Remove Animation", 112 | icon = "fa-solid fa-trash", 113 | action = function() 114 | for i = 1, #animations do 115 | if animations[i].action == v.action then 116 | table.remove(animations, i) 117 | break 118 | end 119 | end 120 | 121 | TriggerServerEvent('luck-contextmenu:SetFastActions', animations, "animations") 122 | 123 | AddFastActions(animations, commands) 124 | end 125 | } 126 | } 127 | }) 128 | end 129 | end 130 | 131 | if commands then 132 | for _, v in pairs(commands) do 133 | table.insert(commandSubOptions, { 134 | label = v.label, 135 | event = v.action, 136 | icon = "fa-solid fa-bolt", 137 | type = "command", 138 | subOptions = { 139 | { 140 | label = "Remove Command", 141 | icon = "fa-solid fa-trash", 142 | action = function() 143 | for i = 1, #commands do 144 | if commands[i].action == v.action then 145 | table.remove(commands, i) 146 | break 147 | end 148 | end 149 | 150 | TriggerServerEvent('luck-contextmenu:SetFastActions', commands, "commands") 151 | 152 | AddFastActions(animations, commands) 153 | end 154 | } 155 | } 156 | }) 157 | end 158 | end 159 | 160 | AddSelfOption({ 161 | label = "Animations", 162 | icon = "fa-solid fa-face-smile", 163 | subOptions = animationSubOptions, 164 | priority = 4, 165 | }) 166 | 167 | AddSelfOption({ 168 | label = "Commands", 169 | icon = "fa-solid fa-bolt", 170 | subOptions = commandSubOptions, 171 | priority = 5, 172 | }) 173 | end 174 | 175 | ItemCheck = QBCore.Functions.HasItem 176 | 177 | if PlayerData and PlayerData.metadata and PlayerData.metadata["fastactions"] and Config.EnableDefaultOptions then 178 | AddFastActions(PlayerData.metadata["fastactions"]["animations"], PlayerData.metadata["fastactions"]["commands"]) 179 | end 180 | 181 | JobCheck = function(job) 182 | if type(job) == 'table' then 183 | job = job[PlayerData.job.name] 184 | if job and PlayerData.job.grade.level >= job then 185 | return true 186 | end 187 | elseif job == 'all' or job == PlayerData.job.name then 188 | return true 189 | end 190 | return false 191 | end 192 | 193 | JobTypeCheck = function(jobType) 194 | if type(jobType) == 'table' then 195 | jobType = jobType[PlayerData.job.type] 196 | if jobType then 197 | return true 198 | end 199 | elseif jobType == 'all' or jobType == PlayerData.job.type then 200 | return true 201 | end 202 | return false 203 | end 204 | 205 | GangCheck = function(gang) 206 | if type(gang) == 'table' then 207 | gang = gang[PlayerData.gang.name] 208 | if gang and PlayerData.gang.grade.level >= gang then 209 | return true 210 | end 211 | elseif gang == 'all' or gang == PlayerData.gang.name then 212 | return true 213 | end 214 | return false 215 | end 216 | 217 | CitizenCheck = function(citizenid) 218 | return citizenid == PlayerData.citizenid or citizenid[PlayerData.citizenid] 219 | end 220 | 221 | RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function() 222 | PlayerData = QBCore.Functions.GetPlayerData() 223 | 224 | if PlayerData and PlayerData.metadata and PlayerData.metadata["fastactions"] and Config.EnableDefaultOptions then 225 | AddFastActions(PlayerData.metadata["fastactions"]["animations"], PlayerData.metadata["fastactions"]["commands"]) 226 | end 227 | end) 228 | 229 | RegisterNetEvent('QBCore:Client:OnPlayerUnload', function() 230 | PlayerData = {} 231 | DeletePeds() 232 | end) 233 | 234 | RegisterNetEvent('QBCore:Client:OnJobUpdate', function(JobInfo) 235 | PlayerData.job = JobInfo 236 | end) 237 | 238 | RegisterNetEvent('QBCore:Client:OnGangUpdate', function(GangInfo) 239 | PlayerData.gang = GangInfo 240 | end) 241 | 242 | RegisterNetEvent('QBCore:Player:SetPlayerData', function(val) 243 | PlayerData = val 244 | 245 | if PlayerData and PlayerData.metadata and PlayerData.metadata["fastactions"] and Config.EnableDefaultOptions then 246 | AddFastActions(PlayerData.metadata["fastactions"]["animations"], PlayerData.metadata["fastactions"]["commands"]) 247 | end 248 | end) 249 | end 250 | end) 251 | 252 | function CheckOption(data, entity) 253 | data.distance = data.distance or Config.DefaultDistance 254 | 255 | if entity.distance and data.distance and entity.distance > data.distance then return false end 256 | if data.job and not JobCheck(data.job) then return false end 257 | if data.excludejob and JobCheck(data.excludejob) then return false end 258 | if data.jobType and not JobTypeCheck(data.jobType) then return false end 259 | if data.excludejobType and JobTypeCheck(data.excludejobType) then return false end 260 | if data.gang and not GangCheck(data.gang) then return false end 261 | if data.excludegang and GangCheck(data.excludegang) then return false end 262 | if data.item and not ItemCheck(data.item) then return false end 263 | if data.citizenid and not CitizenCheck(data.citizenid) then return false end 264 | if data.canInteract and not data.canInteract(entity.target, distance, data) then return false end 265 | return true 266 | end 267 | -------------------------------------------------------------------------------- /Client/Utility.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Utility.lua 3 | If you don't know what you're doing, don't touch this file. 4 | ]]-- 5 | 6 | function dump(o) 7 | if type(o) == 'table' then 8 | local s = '{ ' 9 | for k,v in pairs(o) do 10 | if type(k) ~= 'number' then k = '"'..k..'"' end 11 | s = s .. '['..k..'] = ' .. dump(v) .. ',' 12 | end 13 | return s .. '} ' 14 | else 15 | return tostring(o) 16 | end 17 | end 18 | 19 | function DrawOutlineEntity(entity, bool) 20 | if not Config.DrawOutline then return end 21 | if not DoesEntityExist(entity) then return end 22 | if IsEntityAPed(entity) then return end 23 | if IsEntityAVehicle(entity) then 24 | SetEntityDrawOutline(entity, false) 25 | return 26 | end 27 | SetEntityDrawOutline(entity, bool) 28 | SetEntityDrawOutlineColor(Config.OutlineColor[1], Config.OutlineColor[2], Config.OutlineColor[3], Config.OutlineColor[4]) 29 | SetEntityDrawOutlineShader(1) 30 | end 31 | 32 | function Split(s, sep) 33 | local fields = {} 34 | 35 | local sep = sep or " " 36 | local pattern = string.format("([^%s]+)", sep) 37 | string.gsub(s, pattern, function(c) fields[#fields + 1] = c end) 38 | 39 | return fields 40 | end 41 | 42 | function TableConcat(t1,t2) 43 | for _,v in ipairs(t2) do 44 | table.insert(t1, v) 45 | end 46 | return t1 47 | end 48 | 49 | function GetEntityName(type) 50 | if type == 0 then 51 | return "None" 52 | elseif type == 1 then 53 | return "Ped" 54 | elseif type == 2 then 55 | return "Vehicle" 56 | elseif type == 3 then 57 | return "Object" 58 | end 59 | end 60 | 61 | 62 | local glm = require 'glm' 63 | 64 | local flag = 30 65 | 66 | local function ScreenPositionToCameraRay() 67 | local pos = GetFinalRenderedCamCoord() 68 | local rot = glm.rad(GetFinalRenderedCamRot(2)) 69 | local q = glm.quatEulerAngleZYX(rot.z, rot.y, rot.x) 70 | local cursor = vector2(GetControlNormal(0, 239), GetControlNormal(0, 240)) 71 | -- print(GetFinalRenderedCamCoord()) 72 | return pos, glm.rayPicking( 73 | q * glm.forward(), 74 | q * glm.up(), 75 | glm.rad(GetFinalRenderedCamFov()), 76 | GetAspectRatio(true), 77 | 0.10000, -- GetFinalRenderedCamNearClip(), 78 | 10000.0, -- GetFinalRenderedCamFarClip(), 79 | cursor.x * 2.0 - 1.0, cursor.y * 2.0 - 1.0 80 | ) 81 | end 82 | 83 | function RaycastCursor(flag) 84 | local playerPed = PlayerPedId() 85 | local pedCoords = GetEntityCoords(playerPed) 86 | local rayPos, rayDir = ScreenPositionToCameraRay() 87 | local direction = rayPos + 16 * rayDir 88 | local rayHandle = StartShapeTestLosProbe(rayPos.x, rayPos.y, rayPos.z, direction.x, direction.y, direction.z, flag or 30, 0, 4) 89 | 90 | local isSuccess = false 91 | while not isSuccess do 92 | local result, hit, endCoords, _, entityHit = GetShapeTestResult(rayHandle) 93 | 94 | if result ~= 1 then 95 | if entityHit >= 1 then 96 | entityType = GetEntityType(entityHit) 97 | end 98 | 99 | distance = #(pedCoords - endCoords) 100 | 101 | if entityType == 0 and pcall(GetEntityModel, entityHit) then 102 | entityType = 3 103 | isTypeZero = true 104 | end 105 | 106 | isSuccess = true 107 | 108 | return hit, entityHit, entityType, direction, distance, endCoords or cam3DPos 109 | end 110 | 111 | Citizen.Wait(0) 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /Config.lua: -------------------------------------------------------------------------------- 1 | Config = { 2 | -- Debug mode (will print object detials and create line for cursor and cam position) 3 | Debug = false, 4 | Standalone = false, 5 | DrawOutline = true, 6 | OutlineColor = {40, 237, 250, 255}, 7 | DrawColor = {255, 255, 255, 150}, 8 | SuccessDrawColor = {40, 237, 250, 255}, 9 | ErrorDrawColor = {255, 0, 0, 255}, 10 | DrawDistance = 15.0, 11 | DrawSprite = true, 12 | EnableDefaultOptions = true, 13 | EnableMouseRotate = false, 14 | MaxDistance = 7.0, 15 | DefaultDistance = 2, 16 | OpenConditons = function() 17 | return true 18 | end, 19 | KeyMappingSettings = { 20 | Label = "Open Context Menu", 21 | -- If you want to use a keybind, use the following format: https://docs.fivem.net/docs/game-references/input-mapper-parameter-ids/keyboard/ 22 | Key = "LMENU", 23 | }, 24 | SelfOptions = {}, 25 | Globals = { 26 | ObjectOptions = {}, 27 | VehicleOptions = {}, 28 | PlayerOptions = {}, 29 | PedOptions = {}, 30 | }, 31 | CircleZones = {}, 32 | BoxZones = { 33 | ["VehicleRental"] = { 34 | name = "vehiclerental", 35 | coords = vector3(109.9739, -1088.61, 28.302), 36 | length = 0.95, 37 | width = 0.9, 38 | heading = 345.64, 39 | debugPoly = false, 40 | minZ = 27.302, 41 | maxZ = 30.302, 42 | options = { 43 | { 44 | type = "client", 45 | event = "qb-rental:openMenu", 46 | icon = "fas fa-car", 47 | label = "Araç Kiralama", 48 | }, 49 | }, 50 | distance = 3.5 51 | }, 52 | }, 53 | PolyZones = {}, 54 | TargetBones = {}, 55 | TargetModels = { 56 | ["bike"] = { 57 | models = { 58 | `bmx`, 59 | `cruiser`, 60 | `scorcher`, 61 | `fixter`, 62 | `tribike`, 63 | `tribike2`, 64 | `tribike3`, 65 | }, 66 | options = { 67 | { 68 | type = "event", 69 | event = "pickup:bike", 70 | icon = "fas fa-bicycle", 71 | label = "Bisikleti Al", 72 | }, 73 | }, 74 | distance = 3.0 75 | }, 76 | }, 77 | Peds = {} 78 | } 79 | 80 | 81 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Codeverse 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # luck-contextmenu 2 | #### qb-target killer, but heavily inspired from qb-target 3 | #### This script created by goodluck, one of the Codeverse developers. 4 | 5 | ## About this script 6 | luck-contextmenu is for interaction with entities, players, PolyZones (PolyZone is required for this scripts), vehicles and etc. 7 | You can add options for this things and player can be right click to added things for access options. 8 | And you can add submenus for this things! 9 | 10 | #### Information 11 | You can start script without change any qb-target export! 12 | 13 | ## Features 14 | 15 | - Submenu options, you can add infinite submenus! 16 | - Options can have submenu and event on same time! 17 | - Condition label (You can change options label with condition without create any other options.) 18 | - Self clickable! 19 | - Fast actions. (This script comes with default options named "Fast Actions", player can create fast actions for animations and commands!) 20 | - Sprites for zones. 21 | - Realtime check options. (Example you right click to player and one option requires police job. If your job change this option appear/disappear with animation from menu.) 22 | - And other features from qb-target! 23 | - canInteract option for check any condtions you typed! 24 | - Global options for entites, vehicle bones, vehicles, players, peds and self interactions. 25 | 26 | ## Usage (Player) 27 | 28 | 1. Hold left alt key to show cursor. 29 | 2. Bring cursor to any thing with interactable options. (If thing have any options you can show eye image at corsor's top left.) 30 | 3. Right click to access thing's menu. 31 | 4. Now you can take your hand from left alt key. 32 | 5. Click to any option for interact. 33 | 5.1. If option has right arrow icon at right, on left click you will access submenu of option. 34 | 5.2. If option has menu icon at right, on left click you will trigger option's event. On right click you will access to option's submenu. 35 | 5.3 If option not has any icon at right on left click you will trigger option's event. 36 | 37 | ## Documanation for scripting 38 | 39 | #### New features. 40 | 41 | ##### Priority 42 | You can add priority for options, if you want at top of menu options you should give smallest number of menu. 43 | 44 | ### Both right, left clickable example. 45 | ``` 46 | { 47 | label = v.label, 48 | event = "e " .. v.action, 49 | icon = "fa-solid fa-face-smile", 50 | type = "animation", 51 | subOptions = { 52 | { 53 | label = "Remove Animation", 54 | icon = "fa-solid fa-trash", 55 | action = function() 56 | for i = 1, #animations do 57 | if animations[i].action == v.action then 58 | table.remove(animations, i) 59 | break 60 | end 61 | end 62 | 63 | TriggerServerEvent('luck-contextmenu:SetFastActions', animations, "animations") 64 | 65 | AddFastActions(animations, commands) 66 | end 67 | } 68 | } 69 | } 70 | ``` 71 | 72 | ##### Submenus example. 73 | ``` 74 | ['Kıyafetler']= { 75 | label = 'Kıyafetler', 76 | icon = 'fa-solid fa-shirt', 77 | priority = -1, 78 | subOptions = { 79 | { 80 | id = 'meer', 81 | label = 'Accessoires', 82 | icon = 'plus', 83 | subOptions = { 84 | { 85 | id = 'Hat', 86 | label = 'Hat', 87 | icon = 'fa-solid fa-hat-cowboy', 88 | type = 'client', 89 | event = 'qb-radialmenu:ToggleProps', 90 | } 91 | }, 92 | { 93 | id = 'Hair', 94 | label = 'Hair', 95 | icon = 'fa-solid fa-ribbon', 96 | type = 'client', 97 | event = 'qb-radialmenu:ToggleClothing', 98 | } 99 | } 100 | }, 101 | }, 102 | ``` 103 | ##### Condation labels example. 104 | 105 | ``` 106 | ["Toggle Trunk"] = { 107 | icon = "fas fa-truck-ramp-box", 108 | labels = { 109 | { 110 | label = "Open Trunk", 111 | condition = function(entity) 112 | return GetVehicleDoorAngleRatio(entity, BackEngineVehicles[GetEntityModel(entity)] and 4 or 5) <= 0.0 113 | end 114 | }, 115 | { 116 | label = "Close Trunk", 117 | } 118 | }, 119 | action = function(entity) 120 | ToggleDoor(entity, BackEngineVehicles[GetEntityModel(entity)] and 4 or 5) 121 | end, 122 | distance = 1.2 123 | }, 124 | ``` 125 | -------------------------------------------------------------------------------- /Server/Framework.lua: -------------------------------------------------------------------------------- 1 | if Config.EnableDefaultOptions and exports['qb-core'] then 2 | 3 | local QBCore = exports['qb-core']:GetCoreObject() 4 | 5 | RegisterNetEvent('luck-contextmenu:server:SetFastActions', function(fastactions, type) 6 | local src = source 7 | local Player = QBCore.Functions.GetPlayer(src) 8 | local newFastActions = fastactions 9 | 10 | if type then 11 | newFastActions = Player.PlayerData.metadata["fastactions"] 12 | newFastActions[type] = fastactions 13 | end 14 | 15 | Player.Functions.SetMetaData("fastactions", newFastActions) 16 | end) 17 | 18 | end -------------------------------------------------------------------------------- /fxmanifest.lua: -------------------------------------------------------------------------------- 1 | fx_version 'cerulean' 2 | game 'gta5' 3 | 4 | ui_page('ui/index.html') 5 | 6 | author 'Doğukan Duran (goodluck)' 7 | description 'Contextmenu for FiveM' 8 | version '1.0.0' 9 | 10 | client_scripts { 11 | '@PolyZone/client.lua', 12 | '@PolyZone/BoxZone.lua', 13 | '@PolyZone/EntityZone.lua', 14 | '@PolyZone/CircleZone.lua', 15 | '@PolyZone/ComboZone.lua', 16 | 'Config.lua', 17 | 'Client/*.lua', 18 | } 19 | 20 | server_scripts { 21 | 'Config.lua', 22 | 'Server/*.lua', 23 | } 24 | 25 | files { 26 | 'ui/index.html', 27 | 'ui/style.css', 28 | 'ui/assets/*.png', 29 | 'ui/main.js' 30 | } 31 | 32 | lua54 'yes' 33 | use_experimental_fxv2_oal 'yes' 34 | 35 | dependency 'PolyZone' 36 | -------------------------------------------------------------------------------- /ui/assets/eye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeverseFiveM/luck-contextmenu/7ed64419cf23166b6ad3ef8325b8bb6f1d99c058/ui/assets/eye.png -------------------------------------------------------------------------------- /ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |