This commit is contained in:
Nordi98 2025-07-14 18:34:49 +02:00
parent 19fb68f805
commit 01d047b3cc
53 changed files with 3222 additions and 5 deletions

View file

@ -0,0 +1,349 @@
if not LoadResourceFile(cache.resource, 'web/build/index.html') then
error('Unable to load UI. Build ox_doorlock or download the latest release.\n ^3https://github.com/overextended/ox_doorlock/releases/latest/download/ox_doorlock.zip^0')
end

if not lib.checkDependency('ox_lib', '3.14.0', true) then return end

local function createDoor(door)
local double = door.doors
door.zone = GetLabelText(GetNameOfZone(door.coords.x, door.coords.y, door.coords.z))

if double then
for i = 1, 2 do
AddDoorToSystem(double[i].hash, double[i].model, double[i].coords.x, double[i].coords.y, double[i].coords.z, false, false, false)
DoorSystemSetDoorState(double[i].hash, 4, false, false)
DoorSystemSetDoorState(double[i].hash, door.state, false, false)

if door.doorRate or not door.auto then
DoorSystemSetAutomaticRate(double[i].hash, door.doorRate or 10.0, false, false)
end
end
else
AddDoorToSystem(door.hash, door.model, door.coords.x, door.coords.y, door.coords.z, false, false, false)
DoorSystemSetDoorState(door.hash, 4, false, false)
DoorSystemSetDoorState(door.hash, door.state, false, false)

if door.doorRate or not door.auto then
DoorSystemSetAutomaticRate(door.hash, door.doorRate or 10.0, false, false)
end
end
end

local nearbyDoors = {}
local Entity = Entity

lib.callback('ox_doorlock:getDoors', false, function(data)
doors = data

for _, door in pairs(data) do
createDoor(door)
end

while true do
table.wipe(nearbyDoors)
local coords = GetEntityCoords(cache.ped)

for _, door in pairs(doors) do
local double = door.doors
door.distance = #(coords - door.coords)

if double then
if door.distance < 80 then
for i = 1, 2 do
if not double[i].entity and IsModelValid(double[i].model) then
local entity = GetClosestObjectOfType(double[i].coords.x, double[i].coords.y, double[i].coords.z, 1.0, double[i].model, false, false, false)

if entity ~= 0 then
double[i].entity = entity
Entity(entity).state.doorId = door.id
end
end
end

if door.distance < 20 then
nearbyDoors[#nearbyDoors + 1] = door
end
else
for i = 1, 2 do
double[i].entity = nil
end
end
elseif door.distance < 80 then
if not door.entity and IsModelValid(door.model) then
local entity = GetClosestObjectOfType(door.coords.x, door.coords.y, door.coords.z, 1.0, door.model, false, false, false)

if entity ~= 0 then
local min, max = GetModelDimensions(door.model)
local points = {
GetOffsetFromEntityInWorldCoords(entity, min.x, min.y, min.z).xy,
GetOffsetFromEntityInWorldCoords(entity, min.x, min.y, max.z).xy,
GetOffsetFromEntityInWorldCoords(entity, min.x, max.y, max.z).xy,
GetOffsetFromEntityInWorldCoords(entity, min.x, max.y, min.z).xy,
GetOffsetFromEntityInWorldCoords(entity, max.x, min.y, min.z).xy,
GetOffsetFromEntityInWorldCoords(entity, max.x, min.y, max.z).xy,
GetOffsetFromEntityInWorldCoords(entity, max.x, max.y, max.z).xy,
GetOffsetFromEntityInWorldCoords(entity, max.x, max.y, min.z).xy
}

local centroid = vec2(0, 0)

for i = 1, 8 do
centroid += points[i]
end

centroid = centroid / 8
door.coords = vec3(centroid.x, centroid.y, door.coords.z)
door.entity = entity
Entity(entity).state.doorId = door.id
end
end

if door.distance < 20 then
nearbyDoors[#nearbyDoors + 1] = door
end
elseif door.entity then
door.entity = nil
end
end

Wait(500)
end
end)

RegisterNetEvent('ox_doorlock:setState', function(id, state, source, data)
if not doors then return end

if data then
doors[id] = data
createDoor(data)

if NuiHasLoaded then
SendNuiMessage(json.encode({
action = 'updateDoorData',
data = data
}))
end
end

if Config.Notify and source == cache.serverId then
if state == 0 then
lib.notify({
type = 'success',
icon = 'unlock',
description = locale('unlocked_door')
})
else
lib.notify({
type = 'success',
icon = 'lock',
description = locale('locked_door')
})
end
end

local door = data or doors[id]
local double = door.doors
door.state = state

if double then
DoorSystemSetDoorState(double[1].hash, door.state, false, false)
DoorSystemSetDoorState(double[2].hash, door.state, false, false)

if door.holdOpen then
DoorSystemSetHoldOpen(double[1].hash, door.state == 0)
DoorSystemSetHoldOpen(double[2].hash, door.state == 0)
end

while door.state == 1 and (not IsDoorClosed(double[1].hash) or not IsDoorClosed(double[2].hash)) do Wait(0) end
else
DoorSystemSetDoorState(door.hash, door.state, false, false)

if door.holdOpen then DoorSystemSetHoldOpen(door.hash, door.state == 0) end
while door.state == 1 and not IsDoorClosed(door.hash) do Wait(0) end
end

if door.state == state and door.distance and door.distance < 20 then
if Config.NativeAudio then
RequestScriptAudioBank('dlc_oxdoorlock/oxdoorlock', false)
local sound = state == 0 and door.unlockSound or door.lockSound or 'door_bolt'
local soundId = GetSoundId()

PlaySoundFromCoord(soundId, sound, door.coords.x, door.coords.y, door.coords.z, 'DLC_OXDOORLOCK_SET', false, 0, false)
ReleaseSoundId(soundId)
ReleaseNamedScriptAudioBank('dlc_oxdoorlock/oxdoorlock')
else
local volume = (0.01 * GetProfileSetting(300)) / (door.distance / 2)
if volume > 1 then volume = 1 end
local sound = state == 0 and door.unlockSound or door.lockSound or 'door-bolt-4'

SendNUIMessage({
action = 'playSound',
data = {
sound = sound,
volume = volume
}
})
end
end
end)

RegisterNetEvent('ox_doorlock:editDoorlock', function(id, data)
if source == '' then return end

local door = doors[id]
local double = door.doors
local doorState = data and data.state or 0

if data then
data.zone = door.zone or GetLabelText(GetNameOfZone(door.coords.x, door.coords.y, door.coords.z))

-- hacky method to resolve a bug with "closest door" by forcing a distance recalculation
if door.distance < 20 then door.distance = 80 end
elseif ClosestDoor?.id == id then
ClosestDoor = nil
end

if double then
for i = 1, 2 do
local doorHash = double[i].hash

if data then
if data.doorRate or door.doorRate or not data.auto then
DoorSystemSetAutomaticRate(doorHash, data.doorRate or door.doorRate and 0.0 or 10.0, false, false)
end

DoorSystemSetDoorState(doorHash, doorState, false, false)

if data.holdOpen then DoorSystemSetHoldOpen(doorHash, doorState == 0) end
else
DoorSystemSetDoorState(doorHash, 4, false, false)
DoorSystemSetDoorState(doorHash, 0, false, false)

if double[i].entity then
Entity(double[i].entity).state.doorId = nil
end
end
end
else
if data then
if data.doorRate or door.doorRate or not data.auto then
DoorSystemSetAutomaticRate(door.hash, data.doorRate or door.doorRate and 0.0 or 10.0, false, false)
end

DoorSystemSetDoorState(door.hash, doorState, false, false)

if data.holdOpen then DoorSystemSetHoldOpen(door.hash, doorState == 0) end
else
DoorSystemSetDoorState(door.hash, 4, false, false)
DoorSystemSetDoorState(door.hash, 0, false, false)

if door.entity then
Entity(door.entity).state.doorId = nil
end
end
end

doors[id] = data

if NuiHasLoaded then
SendNuiMessage(json.encode({
action = 'updateDoorData',
data = data or id
}))
end
end)

ClosestDoor = nil

lib.callback.register('ox_doorlock:inputPassCode', function()
return ClosestDoor?.passcode and lib.inputDialog(locale('door_lock'), {
{
type = 'input',
label = locale('passcode'),
password = true,
icon = 'lock'
},
})?[1]
end)

local lastTriggered = 0

local function useClosestDoor()
if not ClosestDoor then return false end

local gameTimer = GetGameTimer()

if gameTimer - lastTriggered > 500 then
lastTriggered = gameTimer
TriggerServerEvent('ox_doorlock:setState', ClosestDoor.id, ClosestDoor.state == 1 and 0 or 1)
end
end

exports('useClosestDoor', useClosestDoor)

CreateThread(function()
local lockDoor = locale('lock_door')
local unlockDoor = locale('unlock_door')
local showUI
local drawSprite = Config.DrawSprite

if drawSprite then
local sprite1 = drawSprite[0]?[1]
local sprite2 = drawSprite[1]?[1]

if sprite1 then
RequestStreamedTextureDict(sprite1, true)
end

if sprite2 then
RequestStreamedTextureDict(sprite2, true)
end
end

local SetDrawOrigin = SetDrawOrigin
local ClearDrawOrigin = ClearDrawOrigin
local DrawSprite = drawSprite and DrawSprite

while true do
local num = #nearbyDoors

if num > 0 then
local ratio = drawSprite and GetAspectRatio(true)
for i = 1, num do
local door = nearbyDoors[i]

if door.distance < door.maxDistance then
if door.distance < (ClosestDoor?.distance or 10) then
ClosestDoor = door
end

if drawSprite and not door.hideUi then
local sprite = drawSprite[door.state]

if sprite then
SetDrawOrigin(door.coords.x, door.coords.y, door.coords.z)
DrawSprite(sprite[1], sprite[2], sprite[3], sprite[4], sprite[5], sprite[6] * ratio, sprite[7], sprite[8], sprite[9], sprite[10], sprite[11])
ClearDrawOrigin()
end
end
end
end
else ClosestDoor = nil end

if ClosestDoor and ClosestDoor.distance < ClosestDoor.maxDistance then
if Config.DrawTextUI and not ClosestDoor.hideUi and ClosestDoor.state ~= showUI then
lib.showTextUI(ClosestDoor.state == 0 and lockDoor or unlockDoor)
showUI = ClosestDoor.state
end

if not PickingLock and IsDisabledControlJustReleased(0, 38) then
useClosestDoor()
end
elseif showUI then
lib.hideTextUI()
showUI = nil
end

Wait(num > 0 and 0 or 500)
end
end)

View file

@ -0,0 +1,410 @@
local Entity = Entity

local function getDoorFromEntity(data)
local entity = type(data) == 'table' and data.entity or data

if not entity then return end

local state = Entity(entity)?.state
local doorId = state?.doorId

if not doorId then return end

local door = doors[doorId]

if not door then
state.doorId = nil
end

return door
end

exports('getClosestDoorId', function() return ClosestDoor?.id end)
exports('getDoorIdFromEntity', function(entityId) return getDoorFromEntity(entityId)?.id end) -- same as Entity(entityId).state.doorId

local function entityIsNotDoor(data)
local entity = type(data) == 'number' and data or data.entity
return not getDoorFromEntity(entity)
end

PickingLock = false

local function canPickLock(entity)
if PickingLock then return false end

local door = getDoorFromEntity(entity)

return door and door.lockpick and (Config.CanPickUnlockedDoors or door.state == 1)
end

---@param entity number
local function pickLock(entity)
local door = getDoorFromEntity(entity)

if not door or PickingLock or not door.lockpick or (not Config.CanPickUnlockedDoors and door.state == 0) then return end

PickingLock = true

TaskTurnPedToFaceCoord(cache.ped, door.coords.x, door.coords.y, door.coords.z, 4000)
Wait(500)

local animDict = lib.requestAnimDict('mp_common_heist')

TaskPlayAnim(cache.ped, animDict, 'pick_door', 3.0, 1.0, -1, 49, 0, true, true, true)

local success = lib.skillCheck(door.lockpickDifficulty or Config.LockDifficulty)
local rand = math.random(1, success and 100 or 5)

if success then
TriggerServerEvent('ox_doorlock:setState', door.id, door.state == 1 and 0 or 1, true)
end

if rand == 1 then
TriggerServerEvent('ox_doorlock:breakLockpick')
lib.notify({ type = 'error', description = locale('lockpick_broke') })
end

StopEntityAnim(cache.ped, 'pick_door', animDict, 0)
RemoveAnimDict(animDict)

PickingLock = false
end

exports('pickClosestDoor', function()
if not ClosestDoor then return end

pickLock(ClosestDoor.entity)
end)

local tempData = {}

local function addDoorlock(data)
local entity = type(data) == 'number' and data or data.entity
local model = GetEntityModel(entity)
local coords = GetEntityCoords(entity)

AddDoorToSystem(`temp`, model, coords.x, coords.y, coords.z, false, false, false)
DoorSystemSetDoorState(`temp`, 4, false, false)

coords = GetEntityCoords(entity)
tempData[#tempData + 1] = {
entity = entity,
model = model,
coords = coords,
heading = math.floor(GetEntityHeading(entity) + 0.5)
}

RemoveDoorFromSystem(`temp`)
end

local isAddingDoorlock = false

RegisterNUICallback('notify', function(data, cb)
cb(1)
lib.notify({ title = data })
end)

RegisterNUICallback('createDoor', function(data, cb)
cb(1)
SetNuiFocus(false, false)

data.state = data.state and 1 or 0

if data.items and not next(data.items) then
data.items = nil
end

if data.characters and not next(data.characters) then
data.characters = nil
end

if data.lockpickDifficulty and not next(data.lockpickDifficulty) then
data.lockpickDifficulty = nil
end

if data.groups and not next(data.groups) then
data.groups = nil
end

if not data.id then
isAddingDoorlock = true
local doorCount = data.doors and 2 or 1
local lastEntity = 0

lib.showTextUI(locale('add_door_textui'))

repeat
DisablePlayerFiring(cache.playerId, true)
DisableControlAction(0, 25, true)

local hit, entity, coords = lib.raycast.cam(1|16)
local changedEntity = lastEntity ~= entity
local doorA = tempData[1]?.entity

if changedEntity and lastEntity ~= doorA then
SetEntityDrawOutline(lastEntity, false)
end

lastEntity = entity

if hit then
---@diagnostic disable-next-line: param-type-mismatch
DrawMarker(28, coords.x, coords.y, coords.z, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.2, 0.2, 0.2, 255, 42, 24,
100, false, false, 0, true, false, false, false)
end

if hit and entity > 0 and GetEntityType(entity) == 3 and (doorCount == 1 or doorA ~= entity) and entityIsNotDoor(entity) then
if changedEntity then
SetEntityDrawOutline(entity, true)
end

if IsDisabledControlJustPressed(0, 24) then
addDoorlock(entity)
end
end

if IsDisabledControlJustPressed(0, 25) then
SetEntityDrawOutline(entity, false)

if not doorA then
isAddingDoorlock = false
return lib.hideTextUI()
end

SetEntityDrawOutline(doorA, false)
table.wipe(tempData)
end
until tempData[doorCount]

lib.hideTextUI()
SetEntityDrawOutline(tempData[1].entity, false)

if data.doors then
SetEntityDrawOutline(tempData[2].entity, false)
tempData[1].entity = nil
tempData[2].entity = nil
data.doors = tempData
else
data.model = tempData[1].model
data.coords = tempData[1].coords
data.heading = tempData[1].heading
end
else
if data.doors then
for i = 1, 2 do
local coords = data.doors[i].coords
data.doors[i].coords = vector3(coords.x, coords.y, coords.z)
data.doors[i].entity = nil
end
else
data.entity = nil
end

data.coords = vector3(data.coords.x, data.coords.y, data.coords.z)
data.distance = nil
data.zone = nil
end

isAddingDoorlock = false

------------------------------------
------- Brutal Housing Editing -----
------------------------------------
if PropertyID ~= nil then
if data.name:match("^(.-)_") == nil or data.name:match("^(.-)_") ~= PropertyID then
data.name = PropertyID.."_"..data.name
end

local coords = data.coords or data.doors[1].coords
if coords ~= nil then
if #(coords - vector3(PropertyCoords.x, PropertyCoords.y, PropertyCoords.z)) > MaxDoorlockDistance then
TriggerEvent('brutal_housing:client:SendNotifyNumber', 75)
return
end
else
return
end

PropertyID = nil
PropertyCoords = nil
end
------------------------------------
------- Brutal Housing Editing -----
------------------------------------

TriggerServerEvent('ox_doorlock:editDoorlock', data.id or false, data)
table.wipe(tempData)
end)

RegisterNUICallback('deleteDoor', function(id, cb)
cb(1)
TriggerServerEvent('ox_doorlock:editDoorlock', id)
end)

RegisterNUICallback('teleportToDoor', function(id, cb)
cb(1)
SetNuiFocus(false, false)
local doorCoords = doors[id].coords
if not doorCoords then return end
SetEntityCoords(cache.ped, doorCoords.x, doorCoords.y, doorCoords.z, false, false, false, false)
end)

RegisterNUICallback('exit', function(_, cb)
cb(1)
SetNuiFocus(false, false)
end)

------------------------------------
------- Brutal Housing Editing -----
------------------------------------

PropertyID = nil
PropertyCoords = nil
MaxDoorlockDistance = 50.0

RegisterNetEvent("ox_doorlock:dataFromHousing")
AddEventHandler("ox_doorlock:dataFromHousing", function(propertyID, coords, maxdoorlockdistance)
PropertyID = propertyID
PropertyCoords = coords
MaxDoorlockDistance = maxdoorlockdistance

if isAddingDoorlock then return end

NuiHasLoaded = true

local allowedDoors = {}
for k,v in pairs(doors) do
print(v.name:match("^(.-)_"), PropertyID)
if v.name:match("^(.-)_") == PropertyID then
table.insert(allowedDoors, v)
end
end

SendNuiMessage(json.encode({
action = 'updateDoorData',
data = allowedDoors
}, { with_hole = false }))
Wait(100)

SendNUIMessage({
action = 'setSoundFiles',
data = lib.callback.await('ox_doorlock:getSounds', false)
})

SetNuiFocus(true, true)
SendNuiMessage(json.encode({
action = 'setVisible',
data = id
}))
end)

------------------------------------
------- Brutal Housing Editing -----
------------------------------------

local function openUi(id)
if source == '' or isAddingDoorlock then return end

------------------------------------
------- Brutal Housing Editing -----
------------------------------------
PropertyID = nil
PropertyCoords = nil
------------------------------------
------- Brutal Housing Editing -----
------------------------------------

NuiHasLoaded = true

SendNuiMessage(json.encode({
action = 'updateDoorData',
data = doors
}, { with_hole = false }))
Wait(100)

SendNUIMessage({
action = 'setSoundFiles',
data = lib.callback.await('ox_doorlock:getSounds', false)
})

SetNuiFocus(true, true)
SendNuiMessage(json.encode({
action = 'setVisible',
data = id
}))
end

RegisterNetEvent('ox_doorlock:triggeredCommand', function(closest)
openUi(closest and ClosestDoor?.id or nil)
end)

CreateThread(function()
local target

if GetResourceState('ox_target'):find('start') then
target = {
ox = true,
exp = exports.ox_target
}
elseif GetResourceState('qb-target'):find('start') then
target = {
qb = true,
exp = exports['qb-target']
}
elseif GetResourceState('qtarget'):find('start') then
target = {
qt = true,
exp = exports.qtarget
}
end

if not target then return end

if target.ox then
target.exp:addGlobalObject({
{
name = 'pickDoorlock',
label = locale('pick_lock'),
icon = 'fas fa-user-lock',
onSelect = pickLock,
canInteract = canPickLock,
items = Config.LockpickItems,
anyItem = true,
distance = 1
}
})
else
local options = {
{
label = locale('pick_lock'),
icon = 'fas fa-user-lock',
action = pickLock,
canInteract = canPickLock,
item = Config.LockpickItems[1],
distance = 1
}
}

---@cast target table

if target.qt then
target.exp:Object({ options = options })
elseif target.qb then
target.exp:AddGlobalObject({ options = options })
end

options = { locale('pick_lock') }

AddEventHandler('onResourceStop', function(resource)
if resource == cache.resource then
if target.qt then
return target.exp:RemoveObject(options)
end

if target.qb then
return target.exp:RemoveGlobalObject(options)
end
end
end)
end
end)