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
This commit is contained in:
Jim Shield
2025-08-30 13:19:01 +01:00
parent 2924c611f7
commit 39691a2254

View File

@@ -1,5 +1,4 @@
-- Global Key Table, defined once. -- Global Key Table, defined once.
---
local KEY_TABLE = { 38, 29, 47, 23, 45, 159, 162, 163 } local KEY_TABLE = { 38, 29, 47, 23, 45, 159, 162, 163 }
-- Mapping of key codes to human-readable key names. -- 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", [20] = "Z", [73] = "X", [26] = "C", [0] = "V", [29] = "B", [249] = "N",
[244] = "M", [82] = ",", [81] = "." [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 -- ===== Ownership + indexes =====
for _, target in pairs(TextTargets) do -- TextTargets: key -> target data (coords/entity/models/options/etc.)
if #(target.coords - entityCoords) < 0.01 then local TextTargets = {}
existingTarget = target -- targetEntities kept for parity (not strictly required)
break local targetEntities = {}
end
-- 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 end
TargetRegistry.byKey[key] = owner
TargetRegistry.byResource[owner] = TargetRegistry.byResource[owner] or {}
TargetRegistry.byResource[owner][key] = true
end
if existingTarget then local function removeTargetKey(key, reason)
for i = 1, #opts do local owner = TargetRegistry.byKey[key]
local key = KEY_TABLE[#existingTarget.options + i] if TextTargets[key] then
opts[i].key = key TextTargets[key] = nil
existingTarget.buttontext[#existingTarget.buttontext + 1] = " ~b~[~w~" .. Keys[key] .. "~b~] ~w~" .. opts[i].label -- print("^6Bridge^7:^5 Target^7: ^2Removed target '%s'%s", key, reason and (" ("..reason..")") or "")
existingTarget.options[#existingTarget.options + 1] = opts[i] end
if owner then
if TargetRegistry.byResource[owner] then
TargetRegistry.byResource[owner][key] = nil
end end
updateCachedText(existingTarget) TargetRegistry.byKey[key] = nil
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")
}
end end
end end
function createZoneTarget(data, opts, dist) AddEventHandler("onResourceStop", function(res)
startTargetLoop() local owned = TargetRegistry.byResource[res]
local existingTarget = nil if not owned then return end
for _, target in pairs(TextTargets) do local cnt = 0
if #(target.coords - data[2]) < 0.01 then for key in pairs(owned) do
existingTarget = target removeTargetKey(key, "resource stopped: "..res)
break cnt = cnt + 1
end
end end
TargetRegistry.byResource[res] = nil
-- print("^6Bridge^7:^5 Target^7: ^2Cleared "..cnt.." target(s) from '"..res.."'")
end)
if existingTarget then -- ===== Helpers =====
for i = 1, #opts do local function vecKey(v)
local key = KEY_TABLE[#existingTarget.options + i] -- stable rounded coord string for entity dedupe when name isn't provided
opts[i].key = key return ("%.3f,%.3f,%.3f"):format(v.x, v.y, v.z)
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]
end end
function createModelTarget(models, opts, dist) local function bakeButtons(opts)
startTargetLoop()
if type(models) ~= "table" then
models = { models }
end
local tempText = {} local tempText = {}
for i = 1, #opts do for i = 1, #opts do
opts[i].key = KEY_TABLE[i] 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 end
return tempText
end
local keyStr = "" -- Update cached text blob
for i, m in ipairs(models) do local function updateCachedText(target)
keyStr = keyStr .. tostring(m) .. (i < #models and "_" or "") target.text = table.concat(target.buttontext, "\n")
end end
local targetKey = "model_" .. keyStr
TextTargets[targetKey] = { -- ===== Public API: Create targets =====
models = models, -- ENTITY: createEntityTarget(entity, opts, dist, nameOpt?)
buttontext = tempText, function createEntityTarget(entity, opts, dist, name)
options = opts, startTargetLoop()
dist = dist,
coords = vec3(0, 0, 0), if not entity or entity == 0 then return end
text = table.concat(tempText, "\n") 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,
} }
updateCachedText(TextTargets[key])
return targetKey registerTarget(owner, key)
-- print("^6Bridge^7:^5 Target^7: ^2Added/Updated ENTITY target '"..key.."' by '"..owner.."' @ "..formatCoord(coords))
return key
end end
function removeEntityTarget(entity) -- ZONE: createZoneTarget(data, opts, dist)
TextTargets[entity] = nil -- 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 end
function removeZoneTarget(target) -- MODEL: createModelTarget(models, opts, dist, nameOpt?)
TextTargets[target] = nil 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 end
function removeModelTarget(model) -- ===== Public API: Remove targets =====
TextTargets[model] = nil -- 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 end
exports("createEntityTarget", createEntityTarget) exports("createEntityTarget", createEntityTarget)
@@ -143,22 +216,20 @@ exports("removeEntityTarget", removeEntityTarget)
exports("removeZoneTarget", removeZoneTarget) exports("removeZoneTarget", removeZoneTarget)
exports("removeModelTarget", removeModelTarget) exports("removeModelTarget", removeModelTarget)
------------------------------------------------------------- -------------------------------------------------------------
-- Fallback: DrawText3D Targets (Experimental) -- Fallback: DrawText3D Targets (Experimental)
------------------------------------------------------------- -------------------------------------------------------------
local started = false local started = false
function startTargetLoop() function startTargetLoop()
if started then return end if started then return end
Config = {
System = {
}
}
started = true started = true
-- lazy include (unchanged from your original)
Config = { System = {} }
local fileLoader = assert(load(LoadResourceFile("jim_bridge", ('starter.lua')), ('@@jim_bridge/starter.lua'))) local fileLoader = assert(load(LoadResourceFile("jim_bridge", ('starter.lua')), ('@@jim_bridge/starter.lua')))
fileLoader() fileLoader()
-- Model Entity Refresher
-- Model Entity Refresher (kept)
CreateThread(function() CreateThread(function()
while true do while true do
local pedCoords = GetEntityCoords(PlayerPedId()) local pedCoords = GetEntityCoords(PlayerPedId())
@@ -178,7 +249,7 @@ function startTargetLoop()
end end
end) end)
-- Main Target Loop -- Main Target Loop (unchanged logic, just uses new TextTargets entries)
CreateThread(function() CreateThread(function()
while true do while true do
local ped = PlayerPedId() local ped = PlayerPedId()
@@ -215,10 +286,10 @@ function startTargetLoop()
for i, opt in ipairs(target.options) do for i, opt in ipairs(target.options) do
if IsControlJustPressed(0, opt.key) and isClosest then if IsControlJustPressed(0, opt.key) and isClosest then
if (not target.canInteract or target.canInteract()) and if (not target.canInteract or target.canInteract()) and
(not opt.item or hasItem(opt.item)) and (not opt.item or hasItem(opt.item)) and
(not opt.job or hasJob(opt.job, nil)) then (not opt.job or hasJob(opt.job, nil)) then
if opt.onSelect then opt.onSelect(targetEntity) end 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 end
end end
@@ -228,8 +299,8 @@ function startTargetLoop()
for i, opt in ipairs(target.options) do for i, opt in ipairs(target.options) do
if (not target.canInteract or target.canInteract()) and if (not target.canInteract or target.canInteract()) and
(not opt.item or hasItem(opt.item)) and (not opt.item or hasItem(opt.item)) and
(not opt.job or hasJob(opt.job, nil)) then (not opt.job or hasJob(opt.job, nil)) then
DrawText3D(vec3(target.coords.x, target.coords.y, baseZ + lineHeight * lineOffset), target.buttontext[i], isClosest) DrawText3D(vec3(target.coords.x, target.coords.y, baseZ + lineHeight * lineOffset), target.buttontext[i], isClosest)
lineOffset = lineOffset + 1 lineOffset = lineOffset + 1
end end
@@ -238,12 +309,11 @@ function startTargetLoop()
::continue:: ::continue::
end end
Wait(1) -- Throttled Wait(1)
end end
end) end)
end end
function DrawText3D(coord, text, highlight) function DrawText3D(coord, text, highlight)
SetTextScale(0.30, 0.30) SetTextScale(0.30, 0.30)
SetTextFont(0) SetTextFont(0)
@@ -253,37 +323,28 @@ function DrawText3D(coord, text, highlight)
SetTextCentre(true) SetTextCentre(true)
local totalLength = string.len(text) local totalLength = string.len(text)
local textMaxLength = 99 -- max 99 local textMaxLength = 99
local text = totalLength > textMaxLength and text:sub(1, totalLength - (totalLength - textMaxLength)) or text local txt = totalLength > textMaxLength and text:sub(1, textMaxLength) or text
AddTextComponentString(highlight and text:gsub("%~w~", "~y~") or text) AddTextComponentString(highlight and txt:gsub("%~w~", "~y~") or txt)
SetDrawOrigin(coord.x, coord.y, coord.z, 0) SetDrawOrigin(coord.x, coord.y, coord.z, 0)
DrawText(0.0, 0.0) DrawText(0.0, 0.0)
local count, length = GetLineCountAndMaxLength(text) local count, length = GetLineCountAndMaxLength(txt)
local padding = 0.005 local padding = 0.005
local heightFactor = (count / 43) + padding local heightFactor = (count / 43) + padding
local weightFactor = (length / 150) + padding local weightFactor = (length / 150) + padding
local height = (heightFactor / 2) - padding / 1 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) DrawRect(0.0, height, width, heightFactor, 0, 0, 0, 150)
ClearDrawOrigin() ClearDrawOrigin()
end 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) function GetLineCountAndMaxLength(text)
local lineCount, maxLength = 0, 0 local lineCount, maxLength = 0, 0
for line in text:gmatch("[^\n]+") do for line in text:gmatch("[^\n]+") do
lineCount += 1 lineCount = lineCount + 1
local lineLength = string.len(line) local lineLength = string.len(line)
if lineLength > maxLength then if lineLength > maxLength then
maxLength = lineLength maxLength = lineLength
@@ -293,7 +354,6 @@ function GetLineCountAndMaxLength(text)
return lineCount, maxLength return lineCount, maxLength
end end
function RotationToDirection(rot) function RotationToDirection(rot)
local adjust = math.pi / 180 local adjust = math.pi / 180
return vec3( return vec3(
@@ -311,8 +371,3 @@ function normalizeVector(vec)
return vec3(0, 0, 0) return vec3(0, 0, 0)
end end
end end
-- Helper to update cached text.
function updateCachedText(target)
target.text = table.concat(target.buttontext, "\n")
end