-- CREDITS
-- https://forum.cfx.re/t/allow-drawgizmo-to-be-used-outside-of-fxdk/5091845/8?u=demi-automatic
-- https://github.com/overextended/ox_lib/tree/master
-- https://github.com/DemiAutomatic/object_gizmo
-- Credit: https://github.com/citizenfx/lua/blob/luaglm-dev/cfx/libs/scripts/examples/dataview.lua
-- to minimize unnecessary dependencies, script has been adapted and modified for devhub_lib
local objectGizmoDataview = setmetatable({
EndBig = ">",
EndLittle = "<",
Types = {
Int8 = { code = "i1" },
Uint8 = { code = "I1" },
Int16 = { code = "i2" },
Uint16 = { code = "I2" },
Int32 = { code = "i4" },
Uint32 = { code = "I4" },
Int64 = { code = "i8" },
Uint64 = { code = "I8" },
Float32 = { code = "f", size = 4 }, -- a float (native size)
Float64 = { code = "d", size = 8 }, -- a double (native size)
LuaInt = { code = "j" }, -- a lua_Integer
UluaInt = { code = "J" }, -- a lua_Unsigned
LuaNum = { code = "n" }, -- a lua_Number
String = { code = "z", size = -1, }, -- zero terminated string
},
FixedTypes = {
String = { code = "c" }, -- a fixed-sized string with n bytes
Int = { code = "i" }, -- a signed int with n bytes
Uint = { code = "I" }, -- an unsigned int with n bytes
},
}, {
__call = function(_, length)
return objectGizmoDataview.ArrayBuffer(length)
end
})
objectGizmoDataview.__index = objectGizmoDataview
--[[ Create an ArrayBuffer with a size in bytes --]]
function objectGizmoDataview.ArrayBuffer(length)
return setmetatable({
blob = string.blob(length),
length = length,
offset = 1,
cangrow = true,
}, objectGizmoDataview)
end
--[[ Wrap a non-internalized string --]]
function objectGizmoDataview.Wrap(blob)
return setmetatable({
blob = blob,
length = blob:len(),
offset = 1,
cangrow = true,
}, objectGizmoDataview)
end
--[[ Return the underlying bytebuffer --]]
function objectGizmoDataview:Buffer() return self.blob end
function objectGizmoDataview:ByteLength() return self.length end
function objectGizmoDataview:ByteOffset() return self.offset end
function objectGizmoDataview:SubView(offset, length)
return setmetatable({
blob = self.blob,
length = length or self.length,
offset = 1 + offset,
cangrow = false,
}, objectGizmoDataview)
end
--[[ Return the Endianness format character --]]
local function ef(big) return (big and objectGizmoDataview.EndBig) or objectGizmoDataview.EndLittle end
--[[ Helper function for setting fixed datatypes within a buffer --]]
local function packblob(self, offset, value, code)
-- If cangrow is false the objectGizmoDataview represents a subview, i.e., a subset
-- of some other string view. Ensure the references are the same before
-- updating the subview
local packed = self.blob:blob_pack(offset, code, value)
if self.cangrow or packed == self.blob then
self.blob = packed
self.length = packed:len()
return true
else
return false
end
end
--[[
Create the API by using objectGizmoDataview.Types
--]]
for label,datatype in pairs(objectGizmoDataview.Types) do
if not datatype.size then -- cache fixed encoding size
datatype.size = string.packsize(datatype.code)
elseif datatype.size >= 0 and string.packsize(datatype.code) ~= datatype.size then
local msg = "Pack size of %s (%d) does not match cached length: (%d)"
error(msg:format(label, string.packsize(datatype.code), datatype.size))
return nil
end
objectGizmoDataview["Get" .. label] = function(self, offset, endian)
offset = offset or 0
if offset >= 0 then
local o = self.offset + offset
local v,_ = self.blob:blob_unpack(o, ef(endian) .. datatype.code)
return v
end
return nil
end
objectGizmoDataview["Set" .. label] = function(self, offset, value, endian)
if offset >= 0 and value then
local o = self.offset + offset
local v_size = (datatype.size < 0 and value:len()) or datatype.size
if self.cangrow or ((o + (v_size - 1)) <= self.length) then
if not packblob(self, o, value, ef(endian) .. datatype.code) then
error("cannot grow subview")
end
else
error("cannot grow objectGizmoDataview")
end
end
return self
end
end
for label,datatype in pairs(objectGizmoDataview.FixedTypes) do
datatype.size = -1 -- Ensure cached encoding size is invalidated
objectGizmoDataview["GetFixed" .. label] = function(self, offset, typelen, endian)
if offset >= 0 then
local o = self.offset + offset
if (o + (typelen - 1)) <= self.length then
local code = ef(endian) .. "c" .. tostring(typelen)
local v,_ = self.blob:blob_unpack(o, code)
return v
end
end
return nil -- Out of bounds
end
objectGizmoDataview["SetFixed" .. label] = function(self, offset, typelen, value, endian)
if offset >= 0 and value then
local o = self.offset + offset
if self.cangrow or ((o + (typelen - 1)) <= self.length) then
local code = ef(endian) .. "c" .. tostring(typelen)
if not packblob(self, o, value, code) then
error("cannot grow subview")
end
else
error("cannot grow objectGizmoDataview")
end
end
return self
end
end
local enableScale = false -- allow scaling mode. doesnt scale collisions and resets when physics are applied it seems
local isCursorActive = false
local gizmoEnabled = false
local isRelative = false
local currentEntity
local wasCancelled = false
local isOffsetMode = false
local initialPosition = nil
local referenceEntity = nil
-- FUNCTIONS
local function normalize(x, y, z)
local length = math.sqrt(x * x + y * y + z * z)
if length == 0 then
return 0, 0, 0
end
return x / length, y / length, z / length
end
local function makeEntityMatrix(entity)
local f, r, u, a = GetEntityMatrix(entity)
local view = objectGizmoDataview.ArrayBuffer(60)
view:SetFloat32(0, r[1])
:SetFloat32(4, r[2])
:SetFloat32(8, r[3])
:SetFloat32(12, 0)
:SetFloat32(16, f[1])
:SetFloat32(20, f[2])
:SetFloat32(24, f[3])
:SetFloat32(28, 0)
:SetFloat32(32, u[1])
:SetFloat32(36, u[2])
:SetFloat32(40, u[3])
:SetFloat32(44, 0)
:SetFloat32(48, a[1])
:SetFloat32(52, a[2])
:SetFloat32(56, a[3])
:SetFloat32(60, 1)
return view
end
local function applyEntityMatrix(entity, view)
local x1, y1, z1 = view:GetFloat32(16), view:GetFloat32(20), view:GetFloat32(24)
local x2, y2, z2 = view:GetFloat32(0), view:GetFloat32(4), view:GetFloat32(8)
local x3, y3, z3 = view:GetFloat32(32), view:GetFloat32(36), view:GetFloat32(40)
local tx, ty, tz = view:GetFloat32(48), view:GetFloat32(52), view:GetFloat32(56)
if not enableScale then
x1, y1, z1 = normalize(x1, y1, z1)
x2, y2, z2 = normalize(x2, y2, z2)
x3, y3, z3 = normalize(x3, y3, z3)
end
SetEntityMatrix(entity,
x1, y1, z1,
x2, y2, z2,
x3, y3, z3,
tx, ty, tz
)
end
-- LOOPS
local function gizmoLoop(entity)
if not gizmoEnabled then
return LeaveCursorMode()
end
EnterCursorMode()
isCursorActive = true
if IsEntityAPed(entity) then
SetEntityAlpha(entity, 200)
else
SetEntityDrawOutline(entity, true)
end
while gizmoEnabled and DoesEntityExist(entity) do
Wait(0)
if IsControlJustPressed(0, 47) then -- G
if isCursorActive then
LeaveCursorMode()
isCursorActive = false
else
EnterCursorMode()
isCursorActive = true
end
end
DisableControlAction(0, 24, true) -- lmb
DisableControlAction(0, 25, true) -- rmb
DisableControlAction(0, 140, true) -- r
DisablePlayerFiring(PlayerPedId(), true)
local matrixBuffer = makeEntityMatrix(entity)
local changed = Citizen.InvokeNative(0xEB2EDCA2, matrixBuffer:Buffer(), 'Editor1',
Citizen.ReturnResultAnyway())
if changed then
applyEntityMatrix(entity, matrixBuffer)
end
end
if isCursorActive then
LeaveCursorMode()
end
isCursorActive = false
if DoesEntityExist(entity) then
if IsEntityAPed(entity) then SetEntityAlpha(entity, 255) end
SetEntityDrawOutline(entity, false)
end
gizmoEnabled = false
currentEntity = nil
end
local function GetVectorText(vectorType)
if not currentEntity then return 'ERR_NO_ENTITY_' .. (vectorType or "UNK") end
local label = (vectorType == "coords" and "Position" or "Rotation")
local vec = (vectorType == "coords" and GetEntityCoords(currentEntity) or GetEntityRotation(currentEntity))
return ('%s: %.2f, %.2f, %.2f'):format(label, vec.x, vec.y, vec.z)
end
local function textUILoop()
CreateThread(function()
Core.ShowControlButtons(
'G Cursor Mode
' ..
'R Rotate Mode
' ..
'LALT Snap To Ground
' ..
'ENTER Done Editing
' ..
'ESC Cancel
')
while gizmoEnabled do
Wait(250)
end
Core.ShowControlButtons(false)
end)
end
-- EXPORTS
local currentRotationStatus = 'translate'
local activeCamera = nil
local function createGizmoCamera(refEntity, cameraData)
if not refEntity or not DoesEntityExist(refEntity) then return nil end
local refCoords = GetEntityCoords(refEntity)
local distance = cameraData.distance or 5.0
local height = cameraData.height or 2.0
local angle = cameraData.angle or 0.0
local fov = cameraData.fov or 50.0
local lookAtOffset = cameraData.lookAtOffset or vector3(0.0, 0.0, 0.0)
-- Calculate camera position based on angle around the reference entity
local rad = math.rad(angle)
local camX = refCoords.x + (distance * math.sin(rad))
local camY = refCoords.y + (distance * math.cos(rad))
local camZ = refCoords.z + height
local cam = CreateCam("DEFAULT_SCRIPTED_CAMERA", true)
SetCamCoord(cam, camX, camY, camZ)
-- Point camera at entity with offset
local lookAtCoords = GetOffsetFromEntityInWorldCoords(refEntity, lookAtOffset.x, lookAtOffset.y, lookAtOffset.z)
PointCamAtCoord(cam, lookAtCoords.x, lookAtCoords.y, lookAtCoords.z)
SetCamFov(cam, fov)
SetCamActive(cam, true)
RenderScriptCams(true, true, cameraData.transitionTime or 500, true, true)
return cam
end
local function destroyGizmoCamera(transitionTime)
if activeCamera then
RenderScriptCams(false, true, transitionTime or 500, true, true)
DestroyCam(activeCamera, false)
activeCamera = nil
end
end
local function useGizmo(entity, offsetProp, cameraData)
gizmoEnabled = true
currentEntity = entity
wasCancelled = false
if currentRotationStatus == 'rotate' then
currentRotationStatus = 'translate'
ExecuteCommand('-gizmoRotation')
ExecuteCommand('+gizmoTranslation')
end
-- offsetProp can be an entity handle to calculate offset relative to
if offsetProp and type(offsetProp) ~= "boolean" and DoesEntityExist(offsetProp) then
isOffsetMode = true
referenceEntity = offsetProp
initialPosition = GetEntityCoords(offsetProp)
-- Create camera if cameraData is provided and offsetProp is an entity
if cameraData then
activeCamera = createGizmoCamera(offsetProp, cameraData)
end
elseif offsetProp == true then
isOffsetMode = true
referenceEntity = nil
initialPosition = GetEntityCoords(entity)
-- Create camera looking at the entity itself if cameraData is provided
if cameraData then
activeCamera = createGizmoCamera(entity, cameraData)
end
else
isOffsetMode = false
referenceEntity = nil
initialPosition = nil
end
textUILoop()
gizmoLoop(entity)
-- Cleanup camera when gizmo is done
destroyGizmoCamera(cameraData and cameraData.transitionTime or 500)
return {
handle = entity,
position = GetEntityCoords(entity),
rotation = GetEntityRotation(entity),
cancelled = wasCancelled
}
end
exports("useGizmo", useGizmo) -- No need to upload this functionality to all scripts
local function gizmoAddKeybind(data)
data.name = 'devhub_lib'..data.name
RegisterCommand('+' .. data.name, function()
if data.disabled or IsPauseMenuActive() then return end
data.isPressed = true
if data.onPressed then data:onPressed() end
end)
RegisterCommand('-' .. data.name, function()
if data.disabled or IsPauseMenuActive() then return end
data.isPressed = false
if data.onReleased then data:onReleased() end
end)
RegisterKeyMapping('+' .. data.name, data.description, data.defaultMapper or "keyboard", data.defaultKey)
end
-- CONTROLS these execute the existing gizmo commands but allow me to add additional logic to update the mode display.
gizmoAddKeybind({
name = '_gizmoSelect',
description = 'Selects the currently highlighted gizmo',
defaultMapper = 'MOUSE_BUTTON',
defaultKey = 'MOUSE_LEFT',
onPressed = function(self)
if not gizmoEnabled then return end
ExecuteCommand('+gizmoSelect')
end,
onReleased = function (self)
ExecuteCommand('-gizmoSelect')
end
})
gizmoAddKeybind({
name = '_gizmoRotation',
description = 'Sets mode for the gizmo',
defaultKey = 'R',
onPressed = function(self)
if not gizmoEnabled then return end
if currentRotationStatus == 'translate' then
currentRotationStatus = 'rotate'
ExecuteCommand('-gizmoTranslation')
ExecuteCommand('+gizmoRotation')
else
currentRotationStatus = 'translate'
ExecuteCommand('-gizmoRotation')
ExecuteCommand('+gizmoTranslation')
end
end,
onReleased = function (self)
-- no action on release
end
})
gizmoAddKeybind({
name = '_gizmoclose',
description = 'Close gizmo',
defaultKey = 'RETURN',
onReleased = function(self)
if not gizmoEnabled then return end
local coords = GetEntityCoords(currentEntity)
local rotation = GetEntityRotation(currentEntity)
local clipboardText
if isOffsetMode and initialPosition then
-- Calculate the local offset from the reference entity's coordinate space
local refEntity = referenceEntity or currentEntity
local offset
if referenceEntity and DoesEntityExist(referenceEntity) then
-- Use GetOffsetFromEntityGivenWorldCoords for accurate coordinate conversion
offset = GetOffsetFromEntityGivenWorldCoords(referenceEntity, coords.x, coords.y, coords.z)
else
-- Fallback to simple world difference if no reference entity
local refPos = initialPosition
offset = coords - refPos
end
clipboardText = ('offset = vector3(%.4f, %.4f, %.4f), rotation = vector3(%.4f, %.4f, %.4f)'):format(
offset.x, offset.y, offset.z,
rotation.x, rotation.y, rotation.z
)
else
clipboardText = ('coords = vector3(%.4f, %.4f, %.4f), rotation = vector3(%.4f, %.4f, %.4f)'):format(
coords.x, coords.y, coords.z,
rotation.x, rotation.y, rotation.z
)
end
print(clipboardText)
Core.CopyClipboard(clipboardText)
Core.Notify("Copied to clipboard", 3000, "success")
gizmoEnabled = false
end,
})
gizmoAddKeybind({
name = '_gizmoSnapToGround',
description = 'Snap current gizmo object to floor/surface',
defaultKey = 'LMENU',
onPressed = function(self)
if not gizmoEnabled then return end
PlaceObjectOnGroundProperly_2(currentEntity)
end,
})
gizmoAddKeybind({
name = '_gizmoCancel',
description = 'Cancel gizmo and delete entity',
defaultKey = 'ESCAPE',
onReleased = function(self)
if not gizmoEnabled then return end
wasCancelled = true
gizmoEnabled = false
end,
})
AddEventHandler('onResourceStop', function(resourceName)
if GetCurrentResourceName() ~= resourceName then return end
if gizmoEnabled then
wasCancelled = true
gizmoEnabled = false
end
destroyGizmoCamera(0)
end)
RegisterCommand('testdialog', function()
local coords = vec4(1727.8600, 3322.8416, 40.2235, 193.7808)
-- spawn ped
local pedModel = `a_m_m_farmer_01`
RequestModel(pedModel)
while not HasModelLoaded(pedModel) do
Wait(0)
end
local ped = CreatePed(4, pedModel, coords.x, coords.y, coords.z, coords.w, false, true)
SetEntityAsMissionEntity(ped, true, true)
SetBlockingOfNonTemporaryEvents(ped, true)
FreezeEntityPosition(ped, true)
SetEntityInvincible(ped, true)
local npc = {
name = "Bob Grass",
role = "Citizen",
icon = 'fas fa-user',
camera = { -- @table (optional) camera settings for npc dialog
distance = 0.85, -- @number (optional) camera distance from npc, default 0.85
height = 0.65, -- @number (optional) camera height offset, default 0.65
},
animation = { -- @table (optional) animation to play on npc when dialog opens
dict = "gestures@m@standing@casual", -- @string (required) animation dictionary
name = "gesture_shrug_hard", -- @string (required) animation name
blendIn = 8.0, -- @number (optional) blend in speed, default 8.0
blendOut = -8.0, -- @number (optional) blend out speed, default -8.0
duration = -1, -- @number (optional) animation duration in ms, -1 for full length, default -1
flag = 2, -- @number (optional) animation flags (0 = normal, 1 = repeat, 2 = stop on last frame, etc.), default 1
playbackRate = 0, -- @number (optional) playback rate, default 0
},
}
local result = Core.NpcDialog(ped, {
npc = npc,
soundFile = 'https://upload.devhub.gg/dh_upload/soundSample.mp3',
text = "We are happy to present the new devhub script license manager. Take your server to new heights with our unique features, making what was previously impossible a reality. Features include a license generator, fake license creation, license stealing detection, and a license scanner, among many others.",
grid = {
{
uid = "talk_bob", -- @string (required) unique identifier for the option
icon = 'fas fa-comment', -- @string (required) fontawesome icon class
title = "Talk to Bob", -- @string (required) option title, can be html
badge = { -- @table (optional) badge config
text = "Aggressive action", -- @string (optional) can be html
color = "#ff0000", -- @string (optional) badge background color
},
span = 2, -- @number (optional) makes the option span 2 columns
},
{
uid = "buy_items",
icon = 'fas fa-shopping-cart',
title = "Buy items",
},
{
uid = "give_money",
icon = 'fas fa-dollar-sign',
title = "Give him money",
},
{
uid = "goodbye",
icon = 'fas fa-times',
title = "Goodbye",
}
},
})
print("Dialog result - Status: " .. tostring(result.status), json.encode(result.data))
if result.status then
if result.data == 'goodbye' then
Core.CloseNpcDialog()
elseif result.data == 'give_money' then
local result = Core.NpcDialog(entity, {
npc = npc,
text = "Dialog text here", -- @string (required) dialog text
input = {
type = "number", -- @string (required) type of input: "number" or "text"
default = 3, -- @number or @string (required) default value
text = "I can give you %s dollars.", -- @string (required) text with %s placeholder for input value
buttonText = "Give", -- @string (required) button text
inputWidth = "3vw", -- @string (optional) width of the input field
min = 1, -- @number (optional) [type number] min number value
max = 1000, -- @number (optional) [type number] max number value
maxLength = 7, -- @number (optional) [type text] only for type text
hint = { -- @table (optional) hints for input values
number = { -- [type number]
[5] = "A small amount", -- from 0 to first option
[50] = "A moderate amount", -- from first option to second
[500] = "A large amount",
[1000] = "An extremely large amount",
},
text = { -- [type text]
['orange'] = "Orange is not what im looking for",
['banana'] = "Banana is what i need",
},
}
},
})
print("Dialog result - Status: " .. tostring(result.status), json.encode(result.data))
elseif result.data == 'buy_items' then
-- Payment method selection example
local paymentResult = Core.NpcDialog(ped, {
npc = npc,
text = "How would you like to pay for your items?",
paymentMethod = {
default = "cash", -- cash selected by default
buttonText = "Proceed",
cashLabel = "Cash",
cardLabel = "Card",
},
})
print("Payment method result - Status: " .. tostring(paymentResult.status), json.encode(paymentResult.data))
-- paymentResult.data.method will be "cash" or "card"
end
end
-- Clean up ped after dialog
DeleteEntity(ped)
SetModelAsNoLongerNeeded(pedModel)
end)