mirror of
https://github.com/jimathy/jim_bridge.git
synced 2026-08-16 21:46:03 +01:00
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
374 lines
13 KiB
Lua
374 lines
13 KiB
Lua
-- 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.
|
|
local Keys = {
|
|
[322] = "ESC", [288] = "F1", [289] = "F2", [170] = "F3", [166] = "F5",
|
|
[167] = "F6", [168] = "F7", [169] = "F8", [56] = "F9", [57] = "F10",
|
|
[243] = "~", [157] = "1", [158] = "2", [160] = "3", [164] = "4", [165] = "5",
|
|
[159] = "6", [161] = "7", [162] = "8", [163] = "9", [84] = "-", [83] = "=",
|
|
[177] = "BACKSPACE", [37] = "TAB",
|
|
[44] = "Q", [32] = "W", [38] = "E", [45] = "R", [245] = "T", [246] = "Y",
|
|
[303] = "U", [199] = "P",
|
|
[39] = "[", [40] = "]", [18] = "ENTER", [137] = "CAPS",
|
|
[34] = "A", [8] = "S", [9] = "D", [23] = "F", [47] = "G",
|
|
[74] = "H", [311] = "K", [182] = "L", [21] = "LEFTSHIFT",
|
|
[20] = "Z", [73] = "X", [26] = "C", [0] = "V", [29] = "B", [249] = "N",
|
|
[244] = "M", [82] = ",", [81] = "."
|
|
}
|
|
|
|
|
|
-- ===== 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
|
|
|
|
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
|
|
TargetRegistry.byKey[key] = nil
|
|
end
|
|
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)
|
|
|
|
-- ===== 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
|
|
|
|
local function bakeButtons(opts)
|
|
local tempText = {}
|
|
for i = 1, #opts do
|
|
opts[i].key = KEY_TABLE[i]
|
|
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
|
|
|
|
-- Update cached text blob
|
|
local function updateCachedText(target)
|
|
target.text = table.concat(target.buttontext, "\n")
|
|
end
|
|
|
|
-- ===== 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,
|
|
}
|
|
updateCachedText(TextTargets[key])
|
|
registerTarget(owner, key)
|
|
-- print("^6Bridge^7:^5 Target^7: ^2Added/Updated ENTITY target '"..key.."' by '"..owner.."' @ "..formatCoord(coords))
|
|
return key
|
|
end
|
|
|
|
-- 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
|
|
|
|
-- 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
|
|
|
|
-- ===== 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)
|
|
exports("createZoneTarget", createZoneTarget)
|
|
exports("createModelTarget", createModelTarget)
|
|
|
|
exports("removeEntityTarget", removeEntityTarget)
|
|
exports("removeZoneTarget", removeZoneTarget)
|
|
exports("removeModelTarget", removeModelTarget)
|
|
|
|
-------------------------------------------------------------
|
|
-- Fallback: DrawText3D Targets (Experimental)
|
|
-------------------------------------------------------------
|
|
local started = false
|
|
function startTargetLoop()
|
|
if started then return end
|
|
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 (kept)
|
|
CreateThread(function()
|
|
while true do
|
|
local pedCoords = GetEntityCoords(PlayerPedId())
|
|
for _, target in pairs(TextTargets) do
|
|
if target.models then
|
|
for _, model in ipairs(target.models) do
|
|
local entity = GetClosestObjectOfType(pedCoords.x, pedCoords.y, pedCoords.z, target.dist, model, false, false, false)
|
|
if entity and entity ~= 0 then
|
|
target.entity = entity
|
|
target.coords = GetEntityCoords(entity)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
Wait(3000) -- Refresh every 3s
|
|
end
|
|
end)
|
|
|
|
-- Main Target Loop (unchanged logic, just uses new TextTargets entries)
|
|
CreateThread(function()
|
|
while true do
|
|
local ped = PlayerPedId()
|
|
local pedCoords = GetEntityCoords(ped)
|
|
local camCoords = GetGameplayCamCoord()
|
|
local camRot = GetGameplayCamRot(2)
|
|
local camForward = RotationToDirection(camRot)
|
|
|
|
local closestTarget, closestDist, targetEntity = nil, math.huge, nil
|
|
|
|
for _, target in pairs(TextTargets) do
|
|
local coords = target.coords
|
|
if coords then
|
|
local dist = #(pedCoords - coords)
|
|
if dist <= target.dist then
|
|
local normVec = normalizeVector(coords - camCoords)
|
|
local dot = camForward.x * normVec.x + camForward.y * normVec.y + camForward.z * normVec.z
|
|
|
|
if dot > 0.5 and dist < closestDist then
|
|
closestTarget = target
|
|
closestDist = dist
|
|
targetEntity = target.entity
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
for _, target in pairs(TextTargets) do
|
|
if not target.coords then goto continue end
|
|
local dist = #(pedCoords - target.coords)
|
|
if dist > target.dist then goto continue end
|
|
local isClosest = (target == closestTarget)
|
|
|
|
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
|
|
if opt.onSelect then opt.onSelect(targetEntity) end
|
|
if opt.action then opt.action(targetEntity) end
|
|
end
|
|
end
|
|
end
|
|
|
|
local baseZ, lineHeight = target.coords.z + 1.0, -0.16
|
|
local lineOffset = 0
|
|
|
|
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
|
|
DrawText3D(vec3(target.coords.x, target.coords.y, baseZ + lineHeight * lineOffset), target.buttontext[i], isClosest)
|
|
lineOffset = lineOffset + 1
|
|
end
|
|
end
|
|
|
|
::continue::
|
|
end
|
|
|
|
Wait(1)
|
|
end
|
|
end)
|
|
end
|
|
|
|
function DrawText3D(coord, text, highlight)
|
|
SetTextScale(0.30, 0.30)
|
|
SetTextFont(0)
|
|
SetTextProportional(1)
|
|
SetTextColour(255, 255, 255, 215)
|
|
SetTextEntry("STRING")
|
|
SetTextCentre(true)
|
|
|
|
local totalLength = string.len(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(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
|
|
|
|
DrawRect(0.0, height, width, heightFactor, 0, 0, 0, 150)
|
|
ClearDrawOrigin()
|
|
end
|
|
|
|
function GetLineCountAndMaxLength(text)
|
|
local lineCount, maxLength = 0, 0
|
|
for line in text:gmatch("[^\n]+") do
|
|
lineCount = lineCount + 1
|
|
local lineLength = string.len(line)
|
|
if lineLength > maxLength then
|
|
maxLength = lineLength
|
|
end
|
|
end
|
|
if lineCount == 0 then lineCount = 1 end
|
|
return lineCount, maxLength
|
|
end
|
|
|
|
function RotationToDirection(rot)
|
|
local adjust = math.pi / 180
|
|
return vec3(
|
|
-math.sin(adjust * rot.z) * math.abs(math.cos(adjust * rot.x)),
|
|
math.cos(adjust * rot.z) * math.abs(math.cos(adjust * rot.x)),
|
|
math.sin(adjust * rot.x)
|
|
)
|
|
end
|
|
|
|
function normalizeVector(vec)
|
|
local len = math.sqrt(vec.x^2 + vec.y^2 + vec.z^2)
|
|
if len ~= 0 then
|
|
return vec3(vec.x / len, vec.y / len, vec.z / len)
|
|
else
|
|
return vec3(0, 0, 0)
|
|
end
|
|
end
|