mirror of
https://github.com/jimathy/jim_bridge.git
synced 2026-08-17 05:56:02 +01:00
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:
@@ -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
|
||||
Reference in New Issue
Block a user