From 39691a2254965b416a6cdd2eb90a88f87607a085 Mon Sep 17 00:00:00 2001 From: Jim Shield Date: Sat, 30 Aug 2025 13:19:01 +0100 Subject: [PATCH] enhance built in ui target code Now properly tracks scripts that added a new target, if that is restarted it created ones properly If an overriding target is sent, use that and remove old one --- ui_modules/target.lua | 311 +++++++++++++++++++++++++----------------- 1 file changed, 183 insertions(+), 128 deletions(-) diff --git a/ui_modules/target.lua b/ui_modules/target.lua index c54bae1..76e7b88 100644 --- a/ui_modules/target.lua +++ b/ui_modules/target.lua @@ -1,5 +1,4 @@ -- Global Key Table, defined once. ---- local KEY_TABLE = { 38, 29, 47, 23, 45, 159, 162, 163 } -- Mapping of key codes to human-readable key names. @@ -17,122 +16,196 @@ local Keys = { [20] = "Z", [73] = "X", [26] = "C", [0] = "V", [29] = "B", [249] = "N", [244] = "M", [82] = ",", [81] = "." } --- Tables for storing created targets. -local TextTargets = {} -- For fallback DrawText3D targets. -local targetEntities = {} -- For entity targets. -function createEntityTarget(entity, opts, dist) - startTargetLoop() - targetEntities[#targetEntities + 1] = entity - local entityCoords = GetEntityCoords(entity) - local existingTarget = nil - for _, target in pairs(TextTargets) do - if #(target.coords - entityCoords) < 0.01 then - existingTarget = target - break - end +-- ===== Ownership + indexes ===== +-- TextTargets: key -> target data (coords/entity/models/options/etc.) +local TextTargets = {} +-- targetEntities kept for parity (not strictly required) +local targetEntities = {} + +-- registry: key -> owner, owner -> set(keys) +local TargetRegistry = { byKey = {}, byResource = {} } + +local function getOwnerResource() + return GetInvokingResource() or GetCurrentResourceName() or "unknown" +end + +local function registerTarget(owner, key) + -- move key across owners if replacing + local prev = TargetRegistry.byKey[key] + if prev and TargetRegistry.byResource[prev] then + TargetRegistry.byResource[prev][key] = nil end + TargetRegistry.byKey[key] = owner + TargetRegistry.byResource[owner] = TargetRegistry.byResource[owner] or {} + TargetRegistry.byResource[owner][key] = true +end - if existingTarget then - for i = 1, #opts do - local key = KEY_TABLE[#existingTarget.options + i] - opts[i].key = key - existingTarget.buttontext[#existingTarget.buttontext + 1] = " ~b~[~w~" .. Keys[key] .. "~b~] ~w~" .. opts[i].label - existingTarget.options[#existingTarget.options + 1] = opts[i] +local function removeTargetKey(key, reason) + local owner = TargetRegistry.byKey[key] + if TextTargets[key] then + TextTargets[key] = nil + -- print("^6Bridge^7:^5 Target^7: ^2Removed target '%s'%s", key, reason and (" ("..reason..")") or "") + end + if owner then + if TargetRegistry.byResource[owner] then + TargetRegistry.byResource[owner][key] = nil end - updateCachedText(existingTarget) - else - local tempText = {} - for i = 1, #opts do - opts[i].key = KEY_TABLE[i] - tempText[#tempText + 1] = " ~b~[~w~" .. Keys[opts[i].key] .. "~b~] ~w~" .. opts[i].label - end - TextTargets[entity] = { - coords = vec3(entityCoords.x, entityCoords.y, entityCoords.z), - buttontext = tempText, - options = opts, - dist = dist, - text = table.concat(tempText, "\n") - } + TargetRegistry.byKey[key] = nil end end -function createZoneTarget(data, opts, dist) - startTargetLoop() - local existingTarget = nil - for _, target in pairs(TextTargets) do - if #(target.coords - data[2]) < 0.01 then - existingTarget = target - break - end +AddEventHandler("onResourceStop", function(res) + local owned = TargetRegistry.byResource[res] + if not owned then return end + local cnt = 0 + for key in pairs(owned) do + removeTargetKey(key, "resource stopped: "..res) + cnt = cnt + 1 end + TargetRegistry.byResource[res] = nil + -- print("^6Bridge^7:^5 Target^7: ^2Cleared "..cnt.." target(s) from '"..res.."'") +end) - if existingTarget then - for i = 1, #opts do - local key = KEY_TABLE[#existingTarget.options + i] - opts[i].key = key - existingTarget.buttontext[#existingTarget.buttontext + 1] = " ~b~[~w~" .. Keys[key] .. "~b~] ~w~" .. opts[i].label - existingTarget.options[#existingTarget.options + 1] = opts[i] - end - updateCachedText(existingTarget) - else - local tempText = {} - for i = 1, #opts do - opts[i].key = KEY_TABLE[i] - tempText[#tempText + 1] = " ~b~[~w~" .. Keys[opts[i].key] .. "~b~] ~w~" .. opts[i].label - end - TextTargets[data[1]] = { - coords = data[2], - buttontext = tempText, - options = opts, - dist = dist, - text = table.concat(tempText, "\n") - } - end - return data[1] +-- ===== Helpers ===== +local function vecKey(v) + -- stable rounded coord string for entity dedupe when name isn't provided + return ("%.3f,%.3f,%.3f"):format(v.x, v.y, v.z) end -function createModelTarget(models, opts, dist) - startTargetLoop() - if type(models) ~= "table" then - models = { models } - end - +local function bakeButtons(opts) local tempText = {} for i = 1, #opts do opts[i].key = KEY_TABLE[i] - tempText[#tempText + 1] = " ~b~[~w~" .. Keys[opts[i].key] .. "~b~] ~w~" .. opts[i].label + tempText[#tempText + 1] = + (" ~b~[~w~%s~b~] ~w~%s"):format(Keys[opts[i].key] or ("K"..opts[i].key), opts[i].label or ("Option "..i)) end + return tempText +end - local keyStr = "" - for i, m in ipairs(models) do - keyStr = keyStr .. tostring(m) .. (i < #models and "_" or "") - end - local targetKey = "model_" .. keyStr +-- Update cached text blob +local function updateCachedText(target) + target.text = table.concat(target.buttontext, "\n") +end - TextTargets[targetKey] = { - models = models, - buttontext = tempText, - options = opts, - dist = dist, - coords = vec3(0, 0, 0), - text = table.concat(tempText, "\n") +-- ===== Public API: Create targets ===== +-- ENTITY: createEntityTarget(entity, opts, dist, nameOpt?) +function createEntityTarget(entity, opts, dist, name) + startTargetLoop() + + if not entity or entity == 0 then return end + targetEntities[#targetEntities + 1] = entity + + local owner = getOwnerResource() + local coords = GetEntityCoords(entity) + local key = tostring(name) or ("entity@" .. vecKey(coords)) + + -- Always overwrite on same key + local buttontext = bakeButtons(opts) + TextTargets[key] = { + _key = key, + _type = "entity", + _owner = owner, + entity = entity, + coords = vec3(coords.x, coords.y, coords.z), + buttontext = buttontext, + options = opts, + dist = dist, } - - return targetKey + updateCachedText(TextTargets[key]) + registerTarget(owner, key) + -- print("^6Bridge^7:^5 Target^7: ^2Added/Updated ENTITY target '"..key.."' by '"..owner.."' @ "..formatCoord(coords)) + return key end -function removeEntityTarget(entity) - TextTargets[entity] = nil +-- ZONE: createZoneTarget(data, opts, dist) +-- Expect data[1] = id/name, data[2] = vec3 coords (as in your original) +function createZoneTarget(data, opts, dist) + startTargetLoop() + + local owner = getOwnerResource() + local zname = tostring(data[1] or ("zone@"..vecKey(data[2] or vec3(0,0,0)))) + local coords = data[2] + + local buttontext = bakeButtons(opts) + TextTargets[zname] = { + _key = zname, + _type = "zone", + _owner = owner, + coords = coords, + buttontext = buttontext, + options = opts, + dist = dist, + } + updateCachedText(TextTargets[zname]) + registerTarget(owner, zname) + -- print("^6Bridge^7:^5 Target^7: ^2Added/Updated ZONE target '"..zname.."' by '"..owner.."' @ "..formatCoord(coords)) + return zname end -function removeZoneTarget(target) - TextTargets[target] = nil +-- MODEL: createModelTarget(models, opts, dist, nameOpt?) +function createModelTarget(models, opts, dist, name) + startTargetLoop() + + local owner = getOwnerResource() + if type(models) ~= "table" then models = { models } end + + local key + if name then + key = tostring(name) + else + local parts = {} + for i, m in ipairs(models) do parts[i] = tostring(m) end + key = "model_" .. table.concat(parts, "_") + end + + local buttontext = bakeButtons(opts) + TextTargets[key] = { + _key = key, + _type = "model", + _owner = owner, + models = models, + buttontext = buttontext, + options = opts, + dist = dist, + coords = vec3(0, 0, 0), -- will be updated by the refresher + } + updateCachedText(TextTargets[key]) + registerTarget(owner, key) + -- print("^6Bridge^7:^5 Target^7: ^2Added/Updated MODEL target '"..key.."' by '"..owner.."' (models: "..table.concat(models, ",")..")") + return key end -function removeModelTarget(model) - TextTargets[model] = nil +-- ===== Public API: Remove targets ===== +-- Entity removal accepts entity handle or key string. +function removeEntityTarget(entityOrKey) + local key = nil + if type(entityOrKey) == "string" then + key = entityOrKey + elseif type(entityOrKey) == "number" then + for k, t in pairs(TextTargets) do + if t._type == "entity" and t.entity == entityOrKey then key = k break end + end + if not key then + -- Fallback: try coord-key match + local c = GetEntityCoords(entityOrKey) + local guess = "entity@"..vecKey(c) + if TextTargets[guess] then key = guess end + end + end + if key then removeTargetKey(key, "removeEntityTarget") end +end + +function removeZoneTarget(key) + if not key then return end + removeTargetKey(key, "removeZoneTarget") +end + +-- For models, pass the returned key from createModelTarget (recommended). +function removeModelTarget(key) + if not key then return end + removeTargetKey(key, "removeModelTarget") end exports("createEntityTarget", createEntityTarget) @@ -143,22 +216,20 @@ exports("removeEntityTarget", removeEntityTarget) exports("removeZoneTarget", removeZoneTarget) exports("removeModelTarget", removeModelTarget) - ------------------------------------------------------------- -- Fallback: DrawText3D Targets (Experimental) ------------------------------------------------------------- local started = false function startTargetLoop() if started then return end - Config = { - System = { - - } - } started = true + + -- lazy include (unchanged from your original) + Config = { System = {} } local fileLoader = assert(load(LoadResourceFile("jim_bridge", ('starter.lua')), ('@@jim_bridge/starter.lua'))) fileLoader() - -- Model Entity Refresher + + -- Model Entity Refresher (kept) CreateThread(function() while true do local pedCoords = GetEntityCoords(PlayerPedId()) @@ -178,7 +249,7 @@ function startTargetLoop() end end) - -- Main Target Loop + -- Main Target Loop (unchanged logic, just uses new TextTargets entries) CreateThread(function() while true do local ped = PlayerPedId() @@ -215,10 +286,10 @@ function startTargetLoop() for i, opt in ipairs(target.options) do if IsControlJustPressed(0, opt.key) and isClosest then if (not target.canInteract or target.canInteract()) and - (not opt.item or hasItem(opt.item)) and - (not opt.job or hasJob(opt.job, nil)) then + (not opt.item or hasItem(opt.item)) and + (not opt.job or hasJob(opt.job, nil)) then if opt.onSelect then opt.onSelect(targetEntity) end - if opt.action then opt.action(targetEntity) end + if opt.action then opt.action(targetEntity) end end end end @@ -228,8 +299,8 @@ function startTargetLoop() for i, opt in ipairs(target.options) do if (not target.canInteract or target.canInteract()) and - (not opt.item or hasItem(opt.item)) and - (not opt.job or hasJob(opt.job, nil)) then + (not opt.item or hasItem(opt.item)) and + (not opt.job or hasJob(opt.job, nil)) then DrawText3D(vec3(target.coords.x, target.coords.y, baseZ + lineHeight * lineOffset), target.buttontext[i], isClosest) lineOffset = lineOffset + 1 end @@ -238,12 +309,11 @@ function startTargetLoop() ::continue:: end - Wait(1) -- Throttled + Wait(1) end end) end - function DrawText3D(coord, text, highlight) SetTextScale(0.30, 0.30) SetTextFont(0) @@ -253,37 +323,28 @@ function DrawText3D(coord, text, highlight) SetTextCentre(true) local totalLength = string.len(text) - local textMaxLength = 99 -- max 99 - local text = totalLength > textMaxLength and text:sub(1, totalLength - (totalLength - textMaxLength)) or text - AddTextComponentString(highlight and text:gsub("%~w~", "~y~") or text) + local textMaxLength = 99 + local txt = totalLength > textMaxLength and text:sub(1, textMaxLength) or text + AddTextComponentString(highlight and txt:gsub("%~w~", "~y~") or txt) SetDrawOrigin(coord.x, coord.y, coord.z, 0) DrawText(0.0, 0.0) - local count, length = GetLineCountAndMaxLength(text) + local count, length = GetLineCountAndMaxLength(txt) local padding = 0.005 local heightFactor = (count / 43) + padding local weightFactor = (length / 150) + padding local height = (heightFactor / 2) - padding / 1 - local width = (weightFactor / 2) - padding / 1 + local width = (weightFactor / 2) - padding / 1 DrawRect(0.0, height, width, heightFactor, 0, 0, 0, 150) ClearDrawOrigin() end ---- Calculates the number of lines and the maximum line length from the given text. ---- ---- @param text string The text to analyze. ---- @return number, number The line count and maximum line length. ---- ---- @usage ---- ```lua ---- local count, maxLen = GetLineCountAndMaxLength("Hello World") ---- ``` function GetLineCountAndMaxLength(text) local lineCount, maxLength = 0, 0 for line in text:gmatch("[^\n]+") do - lineCount += 1 + lineCount = lineCount + 1 local lineLength = string.len(line) if lineLength > maxLength then maxLength = lineLength @@ -293,7 +354,6 @@ function GetLineCountAndMaxLength(text) return lineCount, maxLength end - function RotationToDirection(rot) local adjust = math.pi / 180 return vec3( @@ -311,8 +371,3 @@ function normalizeVector(vec) return vec3(0, 0, 0) end end - --- Helper to update cached text. -function updateCachedText(target) - target.text = table.concat(target.buttontext, "\n") -end \ No newline at end of file