Main/resources/[standalone]/sit/client.lua
2025-06-07 08:51:21 +02:00

1938 lines
70 KiB
Lua

-- Variables --
local CURRENT_RESOURCE = GetCurrentResourceName()
local MODELS <const> = GetModels()
local POLY_ZONES <const> = GetPolyZones()
-- Ignore this list, it's only used to detect NPC's sitting around and about, and has nothing to do with what scenarios players can use.
local SITTING_SCENARIOS <const> = {
"WORLD_HUMAN_SEAT_LEDGE", "WORLD_HUMAN_SEAT_LEDGE_EATING", "WORLD_HUMAN_SEAT_STEPS", "WORLD_HUMAN_SEAT_WALL", "WORLD_HUMAN_SEAT_WALL_EATING", "WORLD_HUMAN_SEAT_WALL_TABLET",
"PROP_HUMAN_SEAT_ARMCHAIR", "PROP_HUMAN_SEAT_BAR", "PROP_HUMAN_SEAT_BENCH", "PROP_HUMAN_SEAT_BENCH_FACILITY", "PROP_HUMAN_SEAT_BENCH_DRINK", "PROP_HUMAN_SEAT_BENCH_DRINK_FACILITY",
"PROP_HUMAN_SEAT_BENCH_DRINK_BEER", "PROP_HUMAN_SEAT_BENCH_FOOD", "PROP_HUMAN_SEAT_BENCH_FOOD_FACILITY", "PROP_HUMAN_SEAT_BUS_STOP_WAIT", "PROP_HUMAN_SEAT_CHAIR",
"PROP_HUMAN_SEAT_CHAIR_DRINK", "PROP_HUMAN_SEAT_CHAIR_DRINK_BEER", "PROP_HUMAN_SEAT_CHAIR_FOOD", "PROP_HUMAN_SEAT_CHAIR_UPRIGHT", "PROP_HUMAN_SEAT_CHAIR_MP_PLAYER",
"PROP_HUMAN_SEAT_COMPUTER", "PROP_HUMAN_SEAT_COMPUTER_LOW", "PROP_HUMAN_SEAT_DECKCHAIR", "PROP_HUMAN_SEAT_DECKCHAIR_DRINK", "PROP_HUMAN_SEAT_MUSCLE_BENCH_PRESS",
"PROP_HUMAN_SEAT_MUSCLE_BENCH_PRESS_PRISON", "PROP_HUMAN_SEAT_SEWING", "PROP_HUMAN_SEAT_STRIP_WATCH", "PROP_HUMAN_SEAT_SUNLOUNGER"
}
-- TODO: Add missing keys in the list + rename some of the mouse related ones
local SPECIAL_KEY_CODES <const> = {
['b_100'] = 'LMB', ['b_101'] = 'RMB', ['b_102'] = 'MMB', ['b_103'] = 'Mouse.ExtraBtn1', ['b_104'] = 'Mouse.ExtraBtn2', ['b_105'] = 'Mouse.ExtraBtn3', ['b_106'] = 'Mouse.ExtraBtn4', ['b_107'] = 'Mouse.ExtraBtn5', ['b_108'] = 'Mouse.ExtraBtn6', ['b_109'] = 'Mouse.ExtraBtn7', ['b_110'] = 'Mouse.ExtraBtn8', ['b_115'] = 'MouseWheel.Up', ['b_116'] = 'MouseWheel.Down', ['b_130'] = 'NumSubstract', ['b_131'] = 'NumAdd', ['b_134'] = 'Num Multiplication', ['b_135'] = 'Num Enter', ['b_137'] = 'Num1', ['b_138'] = 'Num2', ['b_139'] = 'Num3', ['b_140'] = 'Num4', ['b_141'] = 'Num5', ['b_142'] = 'Num6', ['b_143'] = 'Num7', ['b_144'] = 'Num8', ['b_145'] = 'Num9', ['b_170'] = 'F1', ['b_171'] = 'F2', ['b_172'] = 'F3', ['b_173'] = 'F4', ['b_174'] = 'F5', ['b_175'] = 'F6', ['b_176'] = 'F7', ['b_177'] = 'F8', ['b_178'] = 'F9', ['b_179'] = 'F10', ['b_180'] = 'F11', ['b_181'] = 'F12', ['b_182'] = 'F13', ['b_183'] = 'F14', ['b_184'] = 'F15', ['b_185'] = 'F16', ['b_186'] = 'F17', ['b_187'] = 'F18', ['b_188'] = 'F19', ['b_189'] = 'F20', ['b_190'] = 'F21', ['b_191'] = 'F22', ['b_192'] = 'F23', ['b_193'] = 'F24', ['b_194'] = 'Arrow Up', ['b_195'] = 'Arrow Down', ['b_196'] = 'Arrow Left', ['b_197'] = 'Arrow Right', ['b_198'] = 'Delete', ['b_199'] = 'Escape', ['b_200'] = 'Insert', ['b_201'] = 'End', ['b_210'] = 'Delete', ['b_211'] = 'Insert', ['b_212'] = 'End', ['b_1000'] = 'Shift', ['b_1002'] = 'Tab', ['b_1003'] = 'Enter', ['b_1004'] = 'Backspace', ['b_1009'] = 'PageUp', ['b_1008'] = 'Home', ['b_1010'] = 'PageDown', ['b_1012'] = 'CapsLock', ['b_1013'] = 'Control', ['b_1014'] = 'Right Control', ['b_1015'] = 'Alt', ['b_1055'] = 'Home', ['b_1056'] = 'PageUp', ['b_2000'] = 'Space'
}
local isSittingDisabled = false
local isLayingDisabled = false
local disableReasons = {
sit = {},
lay = {}
}
local metadata = {
isSitting = false,
isLaying = false,
entity = 0,
poly = false,
type = nil,
lastPos = nil,
targetPos = nil,
teleportOut = false,
frozen = false,
plyFrozen = false,
animation = {},
scenario = false,
showingPrompt = false,
attAction = false,
}
-- Ignore this if you don't use the TMC framework
local TMC = nil
if Config.Framework == "TMC" or Config.Notifications == "TMC" then
TMC = exports.core:getCoreObject()
end
-- Functions --
local function DisplayNotification(msg)
if Config.Notifications == "OX" then
lib.notify({
title = 'Sit',
description = msg,
type = 'error'
})
elseif Config.Notifications == "QBCore" then
TriggerEvent('QBCore:Notify', msg, "error", 5000)
elseif Config.Notifications == "TMC" then
TMC.Functions.SimpleNotify(msg, "error", 5000)
else
-- Native Notifications
BeginTextCommandThefeedPost("STRING")
AddTextComponentSubstringPlayerName(msg)
EndTextCommandThefeedPostTicker(false, false)
-- Mythic Notify (Variant)
--exports.mythic_notify:SendAlert('error', msg)
end
end
-- Debug printing
local function DebugPrint(text)
if Config.Debugmode then
print(text)
end
end
---Get's the label of a key mapping command
---@param commandHash any
---@return string
local function GetKeyLabel(commandHash)
local key = GetControlInstructionalButton(0, commandHash | 0x80000000, true)
if string.find(key, "t_") then
local label, _count = string.gsub(key, "t_", "")
return label
else
return SPECIAL_KEY_CODES[key] or "unknown"
end
end
-- Add your type of persistent notifications here
local function ShowPersistentNotification(type)
CreateThread(function()
if Config.Notifications == "OX" then
lib.showTextUI(string.format(Config.Lang.KeyMapping.GetUp, GetKeyLabel(`getup`)))
while metadata[type] do
Wait(100)
end
lib.hideTextUI()
elseif Config.Notifications == "QBCore" then
exports["qb-core"]:DrawText(string.format(Config.Lang.KeyMapping.GetUp, GetKeyLabel(`getup`)), "left")
while metadata[type] do
Wait(100)
end
exports["qb-core"]:HideText()
elseif Config.Notifications == "TMC" then
TMC.Functions.Notify({
message = string.format(Config.Lang.KeyMapping.GetUp, GetKeyLabel(`getup`)),
id = 'sit_notif',
persist = true,
notifType = 'info'
})
while metadata[type] do
Wait(100)
end
TMC.Functions.StopNotify("sit_notif")
else
while metadata[type] do
if IsUsingKeyboard(1) then
DisplayHelpTextThisFrame("sit_getup_keyboard", false)
else
DisplayHelpTextThisFrame("sit_getup_controller", false)
end
Wait(0)
end
end
end)
end
local function LoadAnimDict(dict)
RequestAnimDict(dict)
while not HasAnimDictLoaded(dict) do
Wait(10)
end
end
local function GetAmountOfSeats(model)
return #MODELS[model].sit.seats
end
local function HandleLooseEntity(entity)
if not IsEntityPositionFrozen(entity) then
NetworkRequestControlOfEntity(entity)
FreezeEntityPosition(entity, true)
metadata.frozen = true
end
end
local function UnhandleLooseEntity(entity)
if metadata.frozen then
FreezeEntityPosition(entity, false)
metadata.frozen = false
end
end
local function HeadingToRotation(heading)
local rotation = heading
if rotation > 180.0 then
rotation = 180.0 - math.abs(rotation - 180.0)
rotation = rotation*-1
end
return rotation
end
local function GetOffsetFromCoordsInWorldCoords(position, rotation, offset)
local rotX, rotY, rotZ = math.rad(rotation.x), math.rad(rotation.y), math.rad(rotation.z)
local cosX, cosY, cosZ = math.cos(rotX), math.cos(rotY), math.cos(rotZ)
local sinX, sinY, sinZ = math.sin(rotX), math.sin(rotY), math.sin(rotZ)
local matrix = {}
matrix[1] = {}
matrix[1][1] = cosZ * cosY - sinZ * sinX * sinY
matrix[1][2] = cosY * sinZ + cosZ * sinX * sinY
matrix[1][3] = -cosX * sinY
matrix[1][4] = 1
matrix[2] = {}
matrix[2][1] = -cosX * sinZ
matrix[2][2] = cosZ * cosX
matrix[2][3] = sinX
matrix[2][4] = 1
matrix[3] = {}
matrix[3][1] = cosZ * sinY + cosY * sinZ * sinX
matrix[3][2] = sinZ * sinY - cosZ * cosY * sinX
matrix[3][3] = cosX * cosY
matrix[3][4] = 1
matrix[4] = {}
matrix[4][1], matrix[4][2], matrix[4][3] = position.x, position.y, position.z
matrix[4][4] = 1
local x = offset.x * matrix[1][1] + offset.y * matrix[2][1] + offset.z * matrix[3][1] + matrix[4][1]
local y = offset.x * matrix[1][2] + offset.y * matrix[2][2] + offset.z * matrix[3][2] + matrix[4][2]
local z = offset.x * matrix[1][3] + offset.y * matrix[2][3] + offset.z * matrix[3][3] + matrix[4][3]
return vector3(x, y, z)
end
local function IsPedPlayingAnyLayAnim(ped)
local checked = {}
for _type, settings in pairs(Config.LayTypes) do
local anim = settings.animation
if not checked[anim.dict] then
if IsEntityPlayingAnim(ped, anim.dict, anim.name, 3) then
return true
else
checked[anim.dict] = true
end
end
end
return false
end
local function IsPedSitting(ped)
for _index, scenario in pairs(SITTING_SCENARIOS) do
if IsPedUsingScenario(ped, scenario) then
return true
end
end
return false
end
local function IsSeatAvailable(coords, action)
local playerPed = PlayerPedId()
for _index, ped in pairs(GetGamePool('CPed')) do
if ped ~= playerPed then
local dist = #(GetEntityCoords(ped)-coords)
if dist < 1.35 then
if action == 'sit' then
if IsPedPlayingAnyLayAnim(ped) or dist < 0.55 then
return false
end
elseif action == 'lay' then
if IsPedPlayingAnyLayAnim(ped) or IsPedSitting(ped) then
return false
end
end
end
end
end
return true
end
local function SeatSort(a, b)
return a.dist < b.dist
end
local function Raycast(startCoords, destination, ignoreEntity)
local rayHandle = StartShapeTestLosProbe(startCoords.x, startCoords.y, startCoords.z, destination.x, destination.y, destination.z, -1, ignoreEntity, 4)
while true do
local result, hit, endCoords, surfaceNormal, entityHit = GetShapeTestResult(rayHandle)
if result ~= 1 then
return hit, endCoords, surfaceNormal, entityHit
end
Wait(0)
end
end
local function RaycastCamera()
local worldVector, normalVector = GetWorldCoordFromScreenCoord(0.5, 0.5)
local destination = worldVector + normalVector * 10
local hit, endCoords = Raycast(worldVector, destination, PlayerPedId())
if hit then
return endCoords
else
return false
end
end
local function SortSeatsByDistance(seatCoords, seats, raycast)
local sortedSeats = {}
local coords = GetEntityCoords(PlayerPedId())
if raycast and Config.Target and Config.UseTargetingCoords then
local endCoords = RaycastCamera()
if endCoords then
coords = endCoords
end
end
for k, v in pairs(seats) do
sortedSeats[k] = {}
if seatCoords then
local heading = seatCoords.w
local rotation = vector3(0.0, 0.0, HeadingToRotation(seatCoords.w))
sortedSeats[k].coords = GetOffsetFromCoordsInWorldCoords(seatCoords.xyz, rotation, v)
heading = heading + v.w
if heading > 360.0 then
heading = heading - 360.0
end
sortedSeats[k].heading = heading
else
sortedSeats[k].coords = v.xyz
sortedSeats[k].heading = v.w
end
sortedSeats[k].dist = #(coords-sortedSeats[k].coords)
end
table.sort(sortedSeats, SeatSort)
return sortedSeats
end
local function GetAvailableSeat(seatCoords, seats, raycast)
local coords = nil
local heading = nil
local sortedSeats = SortSeatsByDistance(seatCoords, seats, raycast)
for _index, data in pairs(sortedSeats) do
if IsSeatAvailable(data.coords, 'sit') then
coords = data.coords
heading = data.heading
break
end
end
return coords, heading
end
local function LeaveSeat(clearTask, clearTaskImmediately, waitIfAttached)
metadata.isSitting = false
metadata.isLaying = false
metadata.scenario = false
local playerPed = PlayerPedId()
if metadata.plyFrozen then
SetEntityCollision(playerPed, true, false)
FreezeEntityPosition(playerPed, false)
metadata.plyFrozen = false
end
if metadata.entity ~= 0 then
UnhandleLooseEntity(metadata.entity)
metadata.entity = 0
end
if clearTask or clearTaskImmediately then
if waitIfAttached then
-- Wait until the person is no longer attached to another ped (aka. getting escorted or carried).
CreateThread(function()
while true do
if not IsEntityAttachedToAnyPed(PlayerPedId()) then
break
end
Wait(200)
end
playerPed = PlayerPedId()
if clearTask then
ClearPedTasks(playerPed)
else
ClearPedTasksImmediately(playerPed)
end
end)
elseif clearTask then
ClearPedTasks(playerPed)
else
ClearPedTasksImmediately(playerPed)
end
end
end
local function StopSitting()
if metadata.lastPos and (Config.AlwaysTeleportOutOfSeat or Config.TeleportToLastPosWhenNoRoute or Config.SitTypes[metadata.type].teleportOut or metadata.teleportOut) then
ClearPedTasks(PlayerPedId())
Wait(1500)
SetEntityCoords(PlayerPedId(), metadata.lastPos.x, metadata.lastPos.y, metadata.lastPos.z - 0.95, false, false, false, false)
end
LeaveSeat(true, false, false)
end
local function GetScenario(type)
local scenarios = Config.SitTypes[type].scenarios
if not scenarios then return false, vector4(0.0, 0.0, 0.0, 0.0) end
local index = 1
if #scenarios > 1 then
index = math.floor(math.random(100, #scenarios*100)/100 + 0.5)
end
return scenarios[index].name, scenarios[index].offset or Config.SitTypes['default'].scenarios[1].offset
end
local function IsPlayerDoingAnyAction()
if IsPedUsingScenario(PlayerPedId(), metadata.scenario) or metadata.isSitting or metadata.isLaying then
return true
else
return false
end
end
local function CanPlayerReachSeat(destination, entity)
local playerPed = PlayerPedId()
local coords = GetEntityCoords(playerPed)
local start = vector3(coords.x, coords.y, coords.z+0.25)
local _hit, endCoords, _surfaceNormal, entityHit = Raycast(start, destination, playerPed)
while true do
-- If a ped stands in the way just ignore it and start a new raycast from behind them
if GetEntityType(entityHit) ~= 1 then
local dist = #(endCoords - destination)
if (dist < 0.5 or endCoords.x == 0.0) or entityHit == entity then
return true
else
return false
end
else
_hit, endCoords, _surfaceNormal, entityHit = Raycast(GetEntityCoords(entityHit), destination, entityHit)
end
Wait(0)
end
end
-- Checks if the seat is "sitable"
local function CanPlayerSitInSeat(coords, heading, entity)
local rotation = HeadingToRotation(heading)
local start = GetOffsetFromCoordsInWorldCoords(coords, vector3(0.0, 0.0, rotation), vector3(0.0, 0.25, 0.0))
local destination = vector3(start.x, start.y, start.z+0.30)
local hit, _endCoords, _surfaceNormal, entityHit = Raycast(start, destination, entity)
if hit == 0 or entityHit == entity or entityHit == PlayerPedId() then
return true
else
return false
end
end
---Find the closest sit or lay position of spesified type
---@param type string 'sit' | 'lay' | 'both'
---@param maxDistance number|nil the maximum distance to check
---@return boolean found if any position was found or not
---@return table closest table containing the data about the closest position
local function GetClosestPositionOfType(type, maxDistance)
local playerCoords = GetEntityCoords(PlayerPedId())
local closest = {
type = "unknown",
distance = maxDistance or 10000.0
}
local entites = GetGamePool('CObject')
for _index, entity in pairs(entites) do
local model = GetEntityModel(entity)
if MODELS[model] and (MODELS[model][type] or type == "both") then
local coords = GetEntityCoords(entity)
local dist = #(coords - playerCoords)
if dist < closest.distance then
closest.type = (MODELS[model].sit and MODELS[model].lay and "both") or (MODELS[model].sit and "sit") or (MODELS[model].lay and "lay") or "unknown"
closest.entity = entity
closest.coords = coords
closest.distance = dist
end
end
end
for group, data in pairs(POLY_ZONES) do
if data.enabled then
if not data.radius or (#(data.center.xy - playerCoords.xy) < data.radius) then
for name, information in pairs(data.polys) do
for typeset, info in pairs(information) do
if typeset == type or (type == "both" and (typeset == "sit" or typeset == "lay")) then
for _index, coords in pairs(info.seats) do
local dist = #(playerCoords - coords.xyz)
if dist < closest.distance then
closest.type = (information.sit and information.lay and "both") or (information.sit and "sit") or (information.lay and "lay") or "unknown"
closest.entity = 0
closest.coords = coords.xyz
closest.distance = dist
closest.name = name
closest.group = group
end
end
end
end
end
end
end
end
local found = closest.type ~= "unknown"
return found, found and closest or {}
end
local function SitOnSeat(data)
metadata.attAction = true
metadata.entity = data.entity
metadata.poly = data.poly
metadata.type = data.sit.type
local seat = data.sit
local settings = Config.SitTypes[seat.type]
local seatLocation = nil
if not settings then
if seat.type == nil then
if data.entity ~= 0 then
warn(string.format("No sit type was set for model with hash %s, the default settings were used instead!", GetEntityModel(data.entity)))
else
warn(string.format("No sit type was set for poly '%s', the default settings were used instead!", data.poly))
end
else
warn(string.format("No settings was set for sit type '%s' in Config.SitTypes, the default settings were used instead!", seat.type))
end
seat.type = 'default'
settings = Config.SitTypes['default']
end
if data.entity ~= nil and data.entity ~= 0 then
local rot = GetEntityRotation(data.entity)
local xRot = rot.x
local yRot = rot.y
if xRot < 0.0 then xRot = xRot*-1 end
if yRot < 0.0 then yRot = yRot*-1 end
local tilt = xRot + yRot
if tilt > Config.MaxTilt then
DisplayNotification(Config.Lang.Notification.TooTilted)
metadata.attAction = false
return
end
local seatCoords = GetEntityCoords(data.entity)
seatLocation = vector4(seatCoords.x, seatCoords.y, seatCoords.z, GetEntityHeading(data.entity))
end
local coords, heading = GetAvailableSeat(seatLocation, seat.seats, data.raycast)
if coords == nil then
local model = GetEntityModel(data.entity)
if model ~= 0 and GetAmountOfSeats(model) ~= 1 then
DisplayNotification(Config.Lang.Notification.NoAvailable)
else
DisplayNotification(Config.Lang.Notification.OccupiedSit)
end
metadata.attAction = false
return
end
if heading == nil then
DisplayNotification(Config.Lang.Notification.NoAvailable)
metadata.attAction = false
error("Heading of seat was nil!")
return
end
local skipReachCheck = seat.skipSeeCheck or false
if not skipReachCheck and not CanPlayerReachSeat(coords, data.entity) then
DisplayNotification(Config.Lang.Notification.CannotReachSeat)
metadata.attAction = false
return
end
if data.entity ~= 0 and not CanPlayerSitInSeat(coords, heading, data.entity) then
DisplayNotification(Config.Lang.Notification.CannotSitInSeat)
metadata.attAction = false
return
end
local playerPed = PlayerPedId()
if IsPedInAnyVehicle(playerPed, true) then
DisplayNotification(Config.Lang.Notification.InVehicleSit)
metadata.attAction = false
return
end
-- Scenario offsets
local scenario, offset = GetScenario(seat.type)
heading = heading + offset.w
if heading > 360.0 then
heading = heading - 360.0
end
local rotation = HeadingToRotation(heading)
coords = GetOffsetFromCoordsInWorldCoords(coords, vector3(0.0, 0.0, rotation), offset.xyz)
local lastPos = GetEntityCoords(playerPed)
metadata.teleportOut = false
metadata.lastPos = nil
if Config.AlwaysTeleportOutOfSeat or settings.teleportOut or seat.teleportOut then
metadata.teleportOut = true
metadata.lastPos = lastPos
end
-- If we are already sitting then leave the current seat first, however if we are attempting to sit on the current seat then stop sitting.
if metadata.isSitting or metadata.isLaying then
if #(coords-lastPos) < 0.2 then
StopSitting()
metadata.attAction = false
return
else
if metadata.teleportOut then
LeaveSeat(false, true, false)
else
LeaveSeat(true, false, false)
Wait(2000)
end
metadata.entity = data.entity
end
end
metadata.scenario = scenario
metadata.isLaying = false
metadata.animation = {}
ClearPedTasks(playerPed)
if data.entity ~= 0 then
HandleLooseEntity(data.entity)
end
local timeout = settings.timeout or Config.SitTypes['default'].timeout
local skipGoStraightTask = settings.skipGoStraightTask
local prevDist = #(coords.xy - GetEntityCoords(playerPed).xy)
local dist = prevDist
local teleport = Config.AlwaysTeleportToSeat or seat.teleportIn or settings.teleportIn
local breakCounter = 0
local tick = 0
if not teleport and not skipGoStraightTask then
local gotoCoords = GetOffsetFromCoordsInWorldCoords(coords, vector3(0.0, 0.0, rotation), vector3(0.0, 0.695, 0.0))
TaskGoStraightToCoord(playerPed, gotoCoords.x, gotoCoords.y, gotoCoords.z, 1, timeout*500, heading, 0.15)
while true do
Wait(500)
if not metadata.attAction then
return
end
local playerCoords = GetEntityCoords(playerPed)
dist = #(gotoCoords.xy - playerCoords.xy)
tick = tick + 1
if dist < prevDist then
lastPos = playerCoords
prevDist = dist
end
local diff = math.abs(dist - prevDist)
local taskStatus = GetScriptTaskStatus(playerPed, "SCRIPT_TASK_GO_STRAIGHT_TO_COORD")
if taskStatus == 0 or taskStatus == 7 then
break
elseif tick > timeout then
break
elseif dist > prevDist+0.1 and dist > 0.85 then
breakCounter = breakCounter + 1
elseif diff <= 0.085 and dist < Config.MaxInteractionDist and dist > 0.05 and tick > 1 then
breakCounter = breakCounter + 1
else
breakCounter = breakCounter - 1
if breakCounter < 0 then
breakCounter = 0
end
end
if breakCounter > 2 and seat.type ~= "sunlounger" then
break
end
end
teleport = dist > 0.5 or false
tick = 0
end
if metadata.scenario then
metadata.targetPos = coords
TaskStartScenarioAtPosition(playerPed, metadata.scenario, coords.x, coords.y, coords.z, heading, -1, false, teleport)
while true do
Wait(500)
local playerCoords = GetEntityCoords(playerPed)
dist = #(coords.xy - playerCoords.xy)
tick = tick + 1
local taskStatus = GetScriptTaskStatus(playerPed, "SCRIPT_TASK_START_SCENARIO_AT_POSITION")
if taskStatus == 0 or taskStatus == 7 then
break
elseif IsPedUsingScenario(playerPed, metadata.scenario) and dist < 0.40 then
metadata.isSitting = true
break
elseif tick > timeout then
break
elseif not IsPedUsingScenario(playerPed, metadata.scenario) then
break
end
end
else
local animation = settings.animation
if animation.offset then
coords = GetOffsetFromCoordsInWorldCoords(coords, vector3(0.0, 0.0, rotation), animation.offset.xyz)
heading = heading+animation.offset.w
end
metadata.targetPos = coords
SetEntityCollision(playerPed, false, false)
FreezeEntityPosition(playerPed, true)
SetEntityCoords(playerPed, coords.x, coords.y, coords.z, false, false, false, false)
SetEntityHeading(playerPed, heading)
LoadAnimDict(animation.dict)
TaskPlayAnim(playerPed, animation.dict, animation.name, 2.0, 2.0, -1, animation.flag or 1, 0, false, false, false)
RemoveAnimDict(animation.dict)
metadata.plyFrozen = true
metadata.isSitting = true
metadata.animation = animation
end
if metadata.isSitting then
Wait(350)
if Config.ShowHelpText then
TriggerEvent('sit:helpTextThread', 'isSitting')
end
TriggerEvent('sit:checkThread', 'isSitting')
TriggerEvent('sit:onSit')
elseif dist <= 2.0 then
TaskStartScenarioAtPosition(playerPed, metadata.scenario, coords.x, coords.y, coords.z, heading, -1, false, true)
metadata.lastPos = lastPos
metadata.isSitting = true
Wait(350)
if Config.ShowHelpText then
TriggerEvent('sit:helpTextThread', 'isSitting')
end
TriggerEvent('sit:checkThread', 'isSitting')
TriggerEvent('sit:onSit')
else
LeaveSeat(true, false, false)
end
metadata.attAction = false
end
local function SitOnClosestSeat()
if metadata.attAction then
DisplayNotification(Config.Lang.Notification.AlreadyAttemptingToSit)
return
end
local found, closest = GetClosestPositionOfType("sit", Config.MaxInteractionDist)
if not found then
DisplayNotification(Config.Lang.Notification.NoFound)
elseif closest.name ~= nil then
local seat = POLY_ZONES[closest.group].polys[closest.name]
SitOnSeat({entity = 0, poly = closest.name, sit = seat.sit, raycast = false})
elseif closest.entity ~= 0 then
SitOnSeat({entity = closest.entity, poly = false, sit = MODELS[GetEntityModel(closest.entity)].sit, raycast = false})
else
error('SitOnClosestSeat: Found a chair, but it was missing critical data')
end
end
local function LayDownOnBed(data)
metadata.attAction = true
metadata.isSitting = false
metadata.plyFrozen = true
metadata.scenario = false
metadata.teleportOut = false
metadata.entity = data.entity
metadata.poly = data.poly
metadata.type = data.bed.type
local bed = data.bed
local bedLocation = nil
if not Config.LayTypes[bed.type] then
if bed.type == nil then
if data.entity ~= 0 then
warn(string.format("No lay type was set for model with hash %s, the default settings were used instead!", GetEntityModel(data.entity)))
else
warn(string.format("No lay type was set for poly '%s', the default settings were used instead!", data.poly))
end
else
warn(string.format("No lay animation settings was set for type '%s' in Config.LayTypes, the default settings were used instead!", bed.type))
end
bed.type = 'default'
end
if data.entity then
local rot = GetEntityRotation(data.entity)
local xRot = rot.x
local yRot = rot.y
if xRot < 0.0 then xRot = xRot*-1 end
if yRot < 0.0 then yRot = yRot*-1 end
local tilt = xRot + yRot
if tilt > Config.MaxTilt then
DisplayNotification(Config.Lang.Notification.TooTilted)
metadata.attAction = false
return
end
local bedCoords = GetEntityCoords(data.entity)
bedLocation = vector4(bedCoords.x, bedCoords.y, bedCoords.z, GetEntityHeading(data.entity))
end
local coords, heading = GetAvailableSeat(bedLocation, bed.seats, data.raycast)
if coords == nil then
local model = GetEntityModel(data.entity)
if Config.SitTypes[bed.type] and GetAmountOfSeats(model) ~= 1 then
DisplayNotification(Config.Lang.Notification.NoAvailable)
else
DisplayNotification(Config.Lang.Notification.OccupiedSit)
end
metadata.attAction = false
return
end
if heading == nil then
DisplayNotification(Config.Lang.Notification.NoAvailable)
metadata.attAction = false
error('Heading of bed was nil!')
return
end
if not IsSeatAvailable(coords, 'lay') then
DisplayNotification(Config.Lang.Notification.OccupiedLay)
metadata.attAction = false
return
end
local skipReachCheck = bed.skipSeeCheck or false
if not skipReachCheck and not CanPlayerReachSeat(coords, data.entity) then
DisplayNotification(Config.Lang.Notification.CannotReachBed)
metadata.attAction = false
return
end
local playerPed = PlayerPedId()
if IsPedInAnyVehicle(playerPed, true) then
DisplayNotification(Config.Lang.Notification.InVehicleLay)
metadata.attAction = false
return
end
local animation = Config.LayTypes[bed.type].animation
metadata.animation = animation
if Config.AlwaysTeleportOutOfSeat or Config.LayTypes[bed.type].teleportOut or bed.teleportOut then
metadata.teleportOut = true
metadata.lastPos = GetEntityCoords(playerPed)
end
if animation.offset then
coords = GetOffsetFromCoordsInWorldCoords(coords, vector3(0.0, 0.0, HeadingToRotation(heading)), animation.offset.xyz)
heading = heading+animation.offset.w
if heading > 360 then
heading = heading - 360
end
end
LoadAnimDict(animation.dict)
ClearPedTasksImmediately(playerPed)
SetEntityCollision(playerPed, false, false) -- TODO: figure out why this is causing issues for some people but not others
FreezeEntityPosition(playerPed, true)
SetEntityCoords(playerPed, coords.x, coords.y, coords.z, false, false, false, false)
SetEntityHeading(playerPed, heading)
TaskPlayAnim(playerPed, animation.dict, animation.name, 2.0, 2.0, -1, animation.flag or 1, 0, false, false, false)
RemoveAnimDict(animation.dict)
Wait(350)
metadata.isLaying = true
metadata.attAction = false
metadata.targetPos = coords
if Config.ShowHelpText then
TriggerEvent('sit:helpTextThread', 'isLaying')
end
TriggerEvent('sit:checkThread', 'isLaying')
TriggerEvent('sit:onLay')
end
local function LayOnClosestBed()
if metadata.attAction then
DisplayNotification(Config.Lang.Notification.AlreadyAttemptingToLay)
return
end
local found, closest = GetClosestPositionOfType("lay", Config.MaxInteractionDist)
if not found then
DisplayNotification(Config.Lang.Notification.NoBedFound)
elseif closest.name ~= nil then
local seat = POLY_ZONES[closest.group].polys[closest.name]
LayDownOnBed({entity = 0, poly = closest.name, bed = seat.lay, raycast = false})
elseif closest.entity ~= 0 then
LayDownOnBed({entity = closest.entity, poly = false, bed = MODELS[GetEntityModel(closest.entity)].lay, raycast = false})
else
error('LayOnClosestBed: Found a bed, but it lacked critical data.')
end
end
local function GetUpFromBed()
local clearTask = true
local exitAnim = Config.LayTypes[metadata.type]?.exitAnim or Config.LayTypes['default'].exitAnim
metadata.isLaying = false
if metadata.teleportOut then
local playterPed = PlayerPedId()
ClearPedTasksImmediately(playterPed)
SetEntityCoords(playterPed, metadata.lastPos.x, metadata.lastPos.y, metadata.lastPos.z-0.95, false, false, false, false)
clearTask = false
elseif exitAnim then
local exitAnimType = Config.LayTypes[metadata.type]?.exitAnimType or Config.LayTypes['default'].exitAnimType
local direction = nil
if exitAnimType == 0 then
if GetGameplayCamRelativeHeading() < 0 then
direction = "m_getout_l"
else
direction = "m_getout_r"
end
elseif exitAnimType == 1 then
direction = "m_getout_l"
elseif exitAnimType == 2 then
direction = "m_getout_r"
else
warn(string.format("exitAnimType: '%s' was not an expcted type, please correct this, setting type to 1 for this instance ('m_getout_r').", exitAnimType))
direction = "m_getout_r"
end
LoadAnimDict("savem_default@")
TaskPlayAnim(PlayerPedId(), "savem_default@", direction, 1.0, 1.0, 3000, 0, 0, false, false, false)
RemoveAnimDict("savem_default@")
Wait(1400)
clearTask = false
end
metadata.animation = {}
LeaveSeat(clearTask, false, false)
end
local function StopCurrentAction()
if IsPedUsingScenario(PlayerPedId(), metadata.scenario) or metadata.isSitting then
StopSitting()
elseif metadata.isLaying then
GetUpFromBed()
elseif metadata.attAction then
metadata.attAction = false
LeaveSeat(true, false, false)
end
end
local function AddTargetModel(models, options)
if Config.Target == 'ox_target' then
exports.ox_target:addModel(models, options)
else
exports[Config.Target]:AddTargetModel(models, { options = options, distance = Config.MaxInteractionDist })
end
end
local function AddCircleZone(name, center, radius, heading, minZ, maxZ, useZ, options, debug)
if Config.Target == 'ox_target' then
exports.ox_target:addSphereZone({
coords = center,
radius = radius,
debug = Config.DebugPoly or debug,
options = options
})
else
exports[Config.Target]:AddCircleZone(name, center, radius, {
name = name,
heading = heading,
debugPoly = Config.DebugPoly or debug,
minZ = minZ,
maxZ = maxZ,
useZ = useZ
}, {
options = options,
distance = Config.MaxInteractionDist
})
end
end
local function AddBoxZone(name, center, heading, length, width, height, minZ, maxZ, useZ, options, debug)
if Config.Target == 'ox_target' then
exports.ox_target:addBoxZone({
coords = center,
size = vector3(width, length, height),
rotation = heading,
debug = Config.DebugPoly or debug,
drawSprite = Config.TargetDrawSprite,
options = options
})
else
exports[Config.Target]:AddBoxZone(name, center, length, width, {
name = name,
heading = heading,
debugPoly = Config.DebugPoly or debug,
minZ = minZ,
maxZ = maxZ
}, {
options = options,
distance = Config.MaxInteractionDist
})
end
end
local function AttemptToLay(entity, poly, info)
if not metadata.attAction then
if metadata.isLaying then
GetUpFromBed()
else
LayDownOnBed({entity = entity, poly = poly, bed = info.lay, raycast = true})
end
else
DisplayNotification(Config.Lang.Notification.AlreadyAttemptingToLay)
end
end
local function AttemptToSit(entity, poly, info)
if not metadata.attAction then
if metadata.isSitting or metadata.isLaying then
if poly == metadata.poly then
SitOnSeat({entity = entity, poly = poly, sit = info.sit, raycast = true})
else
StopSitting()
end
else
SitOnSeat({entity = entity, poly = poly, sit = info.sit, raycast = true})
end
else
DisplayNotification(Config.Lang.Notification.AlreadyAttemptingToSit)
end
end
local function SetupBeds()
local models = {}
for model, data in pairs(MODELS) do
if data.lay then
models[#models+1] = model
end
end
local options = {
{
icon = Config.Targeting.LayIcon,
label = Config.Targeting.LayLabel
}
}
if Config.Target == 'ox_target' then
options[1].distance = Config.MaxInteractionDist
options[1].onSelect = function(data)
local info = MODELS[GetEntityModel(data.entity)]
AttemptToLay(data.entity, false, info)
end
else
options[1].action = function(entity)
local info = MODELS[GetEntityModel(entity)]
AttemptToLay(entity, false, info)
end
end
AddTargetModel(models, options)
end
local function SetupSeats()
local models = {}
for model, data in pairs(MODELS) do
if data.sit then
models[#models+1] = model
end
end
local options = {
{
icon = Config.Targeting.SitIcon,
label = Config.Targeting.SitLabel
}
}
if Config.Target == 'ox_target' then
options[1].distance = Config.MaxInteractionDist
options[1].onSelect = function(data)
local info = MODELS[GetEntityModel(data.entity)]
AttemptToSit(data.entity, false, info)
end
else
options[1].action = function(entity)
local info = MODELS[GetEntityModel(entity)]
AttemptToSit(entity, false, info)
end
end
AddTargetModel(models, options)
end
local function SetupPolyZones()
for group, data in pairs(POLY_ZONES) do
if data.enabled then
for name, info in pairs(data.polys) do
-- Remove the zone if it already exists. (targeting script does the checking, ox_target does on resource restart so no need)
if Config.Target ~= 'ox_target' then
exports[Config.Target]:RemoveZone(name)
end
if info.poly == nil then
error("PolyZone '"..name.."' could not be generated! (lacks poly specifications)")
elseif info.lay == nil and info.sit == nil then
error("PolyZone '"..name.."' could not be generated! (no action assinged)")
else
local polyType = 'sit'
local options = {}
if info.lay then
local index = #options+1
polyType = 'lay'
options[index] = {
icon = Config.Targeting.LayIcon,
label = Config.Targeting.LayLabel
}
if Config.Target == 'ox_target' then
options[index].distance = Config.MaxInteractionDist
options[index].onSelect = function()
AttemptToLay(0, name, info)
end
-- This is to prevent duplicate interaction options in ox_target
options[index].canInteract = function(entity, _distance, _coords, _name, _bone)
if not entity or #GetEntityCoords(entity).xyz <= 0.0 then
return true
end
local model = GetEntityModel(entity)
if MODELS[model]?.lay then
return false
end
return true
end
else
options[index].action = function()
AttemptToLay(0, name, info)
end
end
end
if info.sit then
local index = #options+1
polyType = 'sit'
options[index] = {
icon = Config.Targeting.SitIcon,
label = Config.Targeting.SitLabel
}
if Config.Target == 'ox_target' then
options[index].distance = Config.MaxInteractionDist
options[index].onSelect = function()
AttemptToSit(0, name, info)
end
-- This is to prevent duplicate interaction options in ox_target
options[index].canInteract = function(entity, _distance, _coords, _name, _bone)
if not entity or #GetEntityCoords(entity).xyz <= 0.0 then
return true
end
local model = GetEntityModel(entity)
if MODELS[model]?.sit then
return false
end
return true
end
else
options[index].action = function()
AttemptToSit(0, name, info)
end
end
end
local minZ = info.poly.minZ or (info.poly.center and info.poly.center.z-(info.poly.height/2)) or info[polyType].seats[1].z-(info.poly.height/2)
local maxZ = info.poly.maxZ or (info.poly.center and info.poly.center.z+(info.poly.height/2)) or info[polyType].seats[1].z+(info.poly.height/2)
local heading = info.poly.heading or info[polyType].seats[1].w
local center = info.poly.center or info[polyType].seats[1].xyz
if info.poly.type == "circle" then
local radius = info.poly.radius
if radius == nil then
warn(string.format("PolyZone: '%s' did not have a specified radius! Radius was automatically set to 1.5!", name))
radius = 1.5
end
AddCircleZone(name, center, radius, heading, minZ, maxZ, true, options, data.debug)
else
AddBoxZone(name, center, heading, info.poly.length, info.poly.width, info.poly.height, minZ, maxZ, true, options, data.debug)
end
end
end
DebugPrint("^2Info: PolyZone group '"..group.."' was generated.")
else
DebugPrint("^3Info: PolyZone group '"..group.."' is disabled.")
end
end
end
local function SetupLocalization()
AddTextEntry("sit_getup_keyboard", string.format(Config.Lang.KeyMapping.GetUp, "~INPUT_BA1F4C6D~"))
AddTextEntry("sit_getup_controller", string.format(Config.Lang.KeyMapping.GetUp, "~INPUT_6ED7AA10~"))
if Config.UsePrompts then
AddTextEntry("sit_on_keyboard", string.format(Config.Lang.KeyMapping.SitDown, "~INPUT_93A2CC75~"))
AddTextEntry("sit_down_controller", string.format(Config.Lang.KeyMapping.SitDown, "~INPUT_53FA0B5E~"))
AddTextEntry("lay_on_keyboard", string.format(Config.Lang.KeyMapping.LayDown, "~INPUT_C15C4337~"))
AddTextEntry("lay_down_controller", string.format(Config.Lang.KeyMapping.LayDown, "~INPUT_49E4480F~"))
AddTextEntry("both_on_keyboard", string.format(Config.Lang.KeyMapping.SitDown, "~INPUT_93A2CC75~").."\n"..string.format(Config.Lang.KeyMapping.LayDown, "~INPUT_C15C4337~"))
AddTextEntry("both_down_controller", string.format(Config.Lang.KeyMapping.SitDown, "~INPUT_53FA0B5E~").."\n"..string.format(Config.Lang.KeyMapping.LayDown, "~INPUT_49E4480F~"))
end
end
-- Prompt System
local function StartPromptSystem()
-- Commands
RegisterCommand("siton", function()
if metadata.showingPrompt then
ExecuteCommand("sit")
end
end, false)
RegisterCommand("sitdown", function()
if metadata.showingPrompt then
ExecuteCommand("sit")
end
end, false)
RegisterCommand("layon", function()
if metadata.showingPrompt then
ExecuteCommand("lay")
end
end, false)
RegisterCommand("laydown", function()
if metadata.showingPrompt then
ExecuteCommand("lay")
end
end, false)
-- Keymapping
RegisterKeyMapping('siton', Config.Lang.KeyBindingDesc.Keyboard.SitDown, 'keyboard', Config.DefaultKeybinds.SitDown.SitKeyboard)
RegisterKeyMapping('sitdown', Config.Lang.KeyBindingDesc.PadAnalog.SitDown, 'PAD_ANALOGBUTTON', Config.DefaultKeybinds.SitDown.SitPadAnalog)
RegisterKeyMapping('layon', Config.Lang.KeyBindingDesc.Keyboard.LayDown, 'keyboard', Config.DefaultKeybinds.SitDown.LayKeyboard)
RegisterKeyMapping('laydown', Config.Lang.KeyBindingDesc.PadAnalog.LayDown, 'PAD_ANALOGBUTTON', Config.DefaultKeybinds.SitDown.LayPadAnalog)
-- A nested function!
local function ShowPromptText(type)
metadata.showingPrompt = true
local textHash = "sit_on_keyboard"
if IsUsingKeyboard(1) then
textHash = type.."_on_keyboard"
else
textHash = type.."_down_controller"
end
for _tick = 1, 25 do
DisplayHelpTextThisFrame(textHash, false)
Wait(0)
end
end
-- Prompt Thread
CreateThread(function()
while true do
local wait = 500
if not metadata.isSitting and not metadata.isLaying and not metadata.attAction then
local found, closest = GetClosestPositionOfType("both", Config.MaxPromptDist)
if found and CanPlayerReachSeat(closest.coords, closest.entity) then
-- Make sure that no other help message is being displayed when we start showing the prompt text, this will stop the annoying pling sounds when two help texts fights over priority
if metadata.showingPrompt or not (IsHelpMessageBeingDisplayed() and Config.PreventHelpTextOverwrites) then
ShowPromptText(closest.type)
wait = 0
end
else
metadata.showingPrompt = false
end
else
wait = 1000
end
Wait(wait)
end
end)
end
-- Commands --
RegisterCommand("sit", function()
if IsPauseMenuActive() then
return
end
if IsPlayerDoingAnyAction() then
if not Config.UsePrompts then
StopCurrentAction()
end
elseif not isSittingDisabled then
SitOnClosestSeat()
end
end, false)
RegisterCommand("lay", function()
if IsPauseMenuActive() then
return
end
if IsPlayerDoingAnyAction() then
if not Config.UsePrompts then
StopCurrentAction()
end
elseif not isLayingDisabled then
LayOnClosestBed()
end
end, false)
-- KeyMapping --
RegisterKeyMapping('getup', Config.Lang.KeyBindingDesc.Keyboard.GetUp, 'keyboard', Config.DefaultKeybinds.GetUp.Keyboard)
RegisterCommand('getup', function()
if not IsPauseMenuActive() then
StopCurrentAction()
end
end, false)
RegisterKeyMapping('standup', Config.Lang.KeyBindingDesc.PadAnalog.GetUp, 'PAD_ANALOGBUTTON', Config.DefaultKeybinds.GetUp.PadAnalog)
RegisterCommand('standup', function()
if not IsPauseMenuActive() then
StopCurrentAction()
end
end, false)
-- Events --
AddEventHandler('sit:helpTextThread', function(type)
ShowPersistentNotification(type)
end)
AddEventHandler('sit:checkThread', function(type)
CreateThread(function()
while true do
Wait(500)
if not metadata[type] then
break
end
-- Distance and animation check
local playerPed = PlayerPedId()
local playerPos = GetEntityCoords(playerPed)
local distance = #(playerPos.xy - metadata.targetPos.xy)
local distZ = playerPos.z - metadata.targetPos.z - 1.25
if distZ > 0.0 then
distance = distance + distZ
end
if distance > 0.5 or (metadata.scenario and not IsPedUsingScenario(playerPed, metadata.scenario) or (metadata.animation and metadata.animation.dict and not IsEntityPlayingAnim(playerPed, metadata.animation.dict, metadata.animation.name, 3))) or IsEntityDead(playerPed) or (metadata.entity ~= 0 and not DoesEntityExist(metadata.entity)) or IsPedRagdoll(playerPed) then
local clearTask = true
if IsEntityDead(playerPed) then
clearTask = false
end
LeaveSeat(clearTask, false, true)
break
end
end
if type == 'isSitting' then
TriggerEvent('sit:onGetUp', 'sit')
else
TriggerEvent('sit:onGetUp', 'lay')
end
end)
end)
-- Reduce stress
-- NOTE: This is set up to use qb-core functions, you may need to edit it!
if Config.ReduceStress then
local stressLevel = 0.0
if Config.Framework == "QBCore" then
RegisterNetEvent('QBCore:Player:SetPlayerData', function(data)
stressLevel = data?.metadata?.stress or 0.0
end)
end
AddEventHandler('sit:checkThread', function(type)
CreateThread(function()
Wait(5000)
while metadata[type] do
if stressLevel > 0.0 then
TriggerServerEvent('hud:server:RelieveStress', math.random(2, 4))
end
Wait(10000)
end
end)
end)
end
RegisterNetEvent('sit:sitDown', function()
SitOnClosestSeat()
end)
RegisterNetEvent('sit:layDown', function()
LayOnClosestBed()
end)
RegisterNetEvent('sit:getUp', function()
StopCurrentAction()
end)
if Config.InvalidateIdleCam then
AddEventHandler('sit:onSit', function()
local sitting = true
-- Register get up event
local getupEvent = AddEventHandler('sit:onGetUp', function(type)
if type == "sit" then
sitting = false
end
end)
-- We don't use DisableIdleCamera as we can't check if the idle cam already was disabled, and we don't want to overwrite any other scripts.
CreateThread(function()
while sitting do
InvalidateIdleCam()
Wait(1000)
end
RemoveEventHandler(getupEvent)
end)
end)
AddEventHandler('sit:onLay', function()
local laying = true
-- Register get up event
local getupEvent = AddEventHandler('sit:onGetUp', function(type)
if type == "lay" then
laying = false
end
end)
-- We don't use DisableIdleCamera as we can't check if the idle cam already was disabled, and we don't want to overwrite any other scripts.
CreateThread(function()
while laying do
InvalidateIdleCam()
Wait(1000)
end
RemoveEventHandler(getupEvent)
end)
end)
end
-- Initialization --
CreateThread(function()
-- Set up localization
SetupLocalization()
-- Chat command suggestions
if Config.AddChatSuggestions then
TriggerEvent('chat:addSuggestion', '/sit', Config.Lang.ChatSuggestions.Sit)
TriggerEvent('chat:addSuggestion', '/lay', Config.Lang.ChatSuggestions.Lay)
end
-- Prompts
if Config.UsePrompts then
StartPromptSystem()
end
-- Yes this is needed, people are just... not very good a reading.
if Config.Target == 'false' then
warn("Config.Target was set to 'false' (string), but it needs to be set to false (boolean).^7")
Config.Target = false
end
-- Targeting
if Config.Target then
SetupBeds()
SetupSeats()
SetupPolyZones()
end
end)
-- Debugging --
-- This is some of the code I used when I was debugging/adding new models and polys.
if Config.Debugmode then
local debugging = true
local function DrawText3D(coords, text, rgb)
SetTextColour(rgb.r, rgb.g, rgb.b, 255)
SetTextScale(0.0, 0.35)
SetTextFont(4)
SetTextOutline()
SetTextCentre(true)
-- Diplay the text
BeginTextCommandDisplayText("STRING")
AddTextComponentSubstringPlayerName(text)
SetDrawOrigin(coords.x, coords.y, coords.z, 0)
EndTextCommandDisplayText(0.0, 0.0)
ClearDrawOrigin()
DrawRect(coords.x, coords.y, 1.0, 1.0, 230, 230, 230, 255)
end
local function GetDebugEntities(playerPed)
local playerCoords = GetEntityCoords(playerPed)
local objectPool = GetGamePool('CObject')
local objects = {}
for i = 1, #objectPool do
local pos = GetEntityCoords(objectPool[i])
local distance = #(playerCoords - pos)
if distance < 8.0 then
objects[i] = {pos = pos, entity = objectPool[i]}
end
end
return objects
end
local function DebugIsSeatAvailable(coords, action)
for _index, ped in pairs(GetGamePool('CPed')) do
local dist = #(GetEntityCoords(ped)-coords)
if dist < 1.35 then
if action == 'sit' then
if IsPedPlayingAnyLayAnim(ped) or dist < 0.55 then
return false
end
elseif action == 'lay' then
if IsPedPlayingAnyLayAnim(ped) or IsPedSitting(ped) then
return false
end
end
end
end
return true
end
-- A debug thread, the messiest shit I've ever made.
local function StartDebuging()
local toDisplay = {}
local colors = {
['occupied'] = {r=200, g=0, b=0},
['sit'] = {r=255, g=255, b=255},
['lay'] = {r=150, g=150, b=150},
['sit_line'] = {r=255, g=0, b=0, a=200},
['lay_line'] = {r=0, g=102, b=204, a=255}
}
CreateThread(function()
while debugging do
local globalIndex = 0
local playerPed = PlayerPedId()
local playerCoords = GetEntityCoords(playerPed)
toDisplay = {}
local entites = GetDebugEntities(playerPed)
for _key, info in pairs(entites) do
local model = GetEntityModel(info.entity)
local information = MODELS[model]
if information then
local modelName = GetEntityArchetypeName(info.entity) or model
for typeset, data in pairs(information) do
for seatIndex, seat in pairs(data.seats) do
local subName = typeset..": "..modelName
if #data.seats > 1 then
subName = subName.." ("..seatIndex..")"
end
local heading = GetEntityHeading(info.entity)
local coords = nil
if typeset == "lay" then
coords = GetOffsetFromCoordsInWorldCoords(info.pos, vector3(0.0, 0.0, HeadingToRotation(heading)), vector3(seat.x, seat.y, seat.z+0.25))
else
coords = GetOffsetFromCoordsInWorldCoords(info.pos, vector3(0.0, 0.0, HeadingToRotation(heading)), seat.xyz)
end
local newHeading = heading + seat.w
if newHeading > 360 then
newHeading = newHeading - 360
end
local textColor = colors[typeset]
if not DebugIsSeatAvailable(coords.xyz, typeset) then
textColor = colors['occupied']
end
globalIndex = globalIndex + 1
toDisplay[globalIndex] = { vector4(coords.x, coords.y, coords.z, newHeading), subName, textColor, colors[typeset.."_line"]}
end
end
end
end
for _group, data in pairs(POLY_ZONES) do
if data.enabled then
if not data.radius or (#(data.center.xy - playerCoords.xy) < data.radius) then
for name, information in pairs(data.polys) do
for typeset, info in pairs(information) do
if typeset == "sit" or typeset == "lay" then
for index, coords in pairs(info.seats) do
if #(playerCoords-coords.xyz) < 8.0 then
local subName = typeset..": "..name
if #info.seats > 1 then
subName = subName.." ("..index..")"
end
local location = coords.xyz
if typeset == "lay" then
location = GetOffsetFromCoordsInWorldCoords(coords, vector3(0.0, 0.0, 0.0), vector3(0.0, 0.0, 0.25))
end
local textColor = colors[typeset]
if not DebugIsSeatAvailable(location, typeset) then
textColor = colors['occupied']
end
globalIndex = globalIndex + 1
toDisplay[globalIndex] = { vector4(location.x, location.y, location.z, coords.w), subName, textColor, colors[typeset.."_line"]}
end
end
end
end
end
end
end
end
Wait(1000)
end
end)
CreateThread(function()
while debugging do
for _index, data in pairs(toDisplay) do
DrawText3D(data[1].xyz, data[2], data[3])
end
Wait(0)
end
end)
CreateThread(function()
while debugging do
for _index, data in pairs(toDisplay) do
if data[1].w ~= nil then
local offset = GetOffsetFromCoordsInWorldCoords(data[1].xyz, vector3(0.0, 0.0, HeadingToRotation(data[1].w)), vector3(0.0, 0.50, 0.0))
local offset2 = GetOffsetFromCoordsInWorldCoords(data[1].xyz, vector3(0.0, 0.0, HeadingToRotation(data[1].w)), vector3(0.0, 0.00, 0.20))
DrawLine(data[1].x, data[1].y, data[1].z, offset.x, offset.y, offset.z, data[4].r, data[4].g, data[4].b, data[4].a)
DrawLine(data[1].x, data[1].y, data[1].z, offset2.x, offset2.y, offset2.z, data[4].r, data[4].g, data[4].b, data[4].a)
end
end
Wait(0)
end
end)
end
RegisterKeyMapping('sit:debug', 'Sit Debuging', 'keyboard', 'G')
RegisterCommand('sit:debug', function()
debugging = not debugging
if debugging then
StartDebuging()
end
end, false)
local function GetAverage(table)
local sum = 0
for key, value in pairs(table) do
sum = sum + value
end
return sum / #table
end
-- Not a true "center", as it calculates the average of all the points, but it's good enough for our purpose.
RegisterCommand('sit:getcenter', function(_source, args, _rawCommand)
local group = args[1]
if POLY_ZONES[group] then
local xPoints = {}
local yPoints = {}
local zPoints = {}
local index = 0
for k, v in pairs(POLY_ZONES[group].polys) do
index = index + 1
xPoints[index] = (v.poly.center and v.poly.center.x) or (v.sit and v.sit.seats[1].x) or (v.lay and v.lay.seats[1].x)
yPoints[index] = (v.poly.center and v.poly.center.y) or (v.sit and v.sit.seats[1].y) or (v.lay and v.lay.seats[1].y)
zPoints[index] = (v.poly.center and v.poly.center.z) or (v.sit and v.sit.seats[1].z) or (v.lay and v.lay.seats[1].z)
end
local average = vector3(GetAverage(xPoints), GetAverage(yPoints), GetAverage(zPoints))
print('average "center":', average)
else
print(group, 'is not a valid poly group!')
end
end, false)
RegisterCommand('sit:getfarthestdist', function(_source, args, _rawCommand)
local group = args[1]
if POLY_ZONES[group] and POLY_ZONES[group].center then
local center = POLY_ZONES[group].center
local farthest = {
dist = 0,
name = 'error'
}
for name, data in pairs(POLY_ZONES[group].polys) do
local pos = data.poly.center or (data.sit and data.sit.seats[1].xyz) or (data.lay and data.lay.seats[1].xyz)
local distance = #(center-pos)
if distance > farthest.dist then
farthest.dist = distance
farthest.name = name
end
end
print(farthest.name, farthest.dist)
else
print(group, 'is not a valid poly group!')
end
end, false)
-- Load poly groups (only on your client)
RegisterCommand('sit:loadGroup', function(_source, args, _rawCommand)
local group = args[1]
if POLY_ZONES[group] and POLY_ZONES[group].center then
POLY_ZONES[group].enabled = true
SetupPolyZones()
else
print(group, 'is not a valid poly group!')
end
end, false)
-- Unload poly groups (only on your client)
RegisterCommand('sit:unloadGroup', function(_source, args, _rawCommand)
if Config.Target == 'ox_target' then
print("ox_target does not support this action!")
return
end
local group = args[1]
if POLY_ZONES[group] and POLY_ZONES[group].center then
POLY_ZONES[group].enabled = false
for name, _info in pairs(POLY_ZONES[group].polys) do
exports[Config.Target]:RemoveZone(name)
end
else
print(group, 'is not a valid poly group!')
end
end, false)
-- Events with some prints
AddEventHandler('sit:onSit', function()
print('sit:onSit')
end)
AddEventHandler('sit:onLay', function()
print('sit:onLay')
end)
AddEventHandler('sit:onGetUp', function(type)
print('sit:onGetUp', type)
end)
StartDebuging()
end
AddEventHandler('onResourceStop', function(resourceName)
if resourceName ~= CURRENT_RESOURCE then
return
end
if not metadata.isSitting and not metadata.isLaying then
return
end
local playerPed = PlayerPedId()
ClearPedTasks(playerPed)
if metadata.plyFrozen then
SetEntityCollision(playerPed, true, false)
FreezeEntityPosition(playerPed, false)
metadata.plyFrozen = false
end
if metadata.entity ~= 0 then
UnhandleLooseEntity(metadata.entity)
metadata.entity = 0
end
end)
-- Exports --
local function IsSitting()
return metadata.isSitting
end
local function IsLaying()
return metadata.isLaying
end
local function IsSittingOrLaying()
return metadata.isSitting or metadata.isLaying
end
local function IsNearSeat()
local found, _closest = GetClosestPositionOfType("sit", Config.MaxInteractionDist)
return found
end
local function IsNearBed()
local found, _closest = GetClosestPositionOfType("lay", Config.MaxInteractionDist)
return found
end
local function GetClosestSeat(maxDistance)
return GetClosestPositionOfType("sit", maxDistance)
end
local function GetClosestBed(maxDistance)
return GetClosestPositionOfType("lay", maxDistance)
end
local function RefreshIsSitDisabled()
for _index, status in pairs(disableReasons.sit) do
if status then
return
end
end
isSittingDisabled = false
end
local function RefreshIsLayDisabled()
for _index, status in pairs(disableReasons.lay) do
if status then
return
end
end
isLayingDisabled = false
end
local function DisableSitting(state, reason)
if not reason then reason = 'default' end
if state then
disableReasons.sit[reason] = true
if not isSittingDisabled then
isSittingDisabled = true
end
else
disableReasons.sit[reason] = nil
RefreshIsSitDisabled()
end
end
local function DisableLaying(state, reason)
if not reason then reason = 'default' end
if state then
disableReasons.lay[reason] = true
if not isLayingDisabled then
isLayingDisabled = true
end
else
disableReasons.lay[reason] = nil
RefreshIsLayDisabled()
end
end
local function DisableSittingAndLaying(state, reason)
if not reason then reason = 'default' end
if state then
disableReasons.sit[reason] = true
disableReasons.lay[reason] = true
if not isSittingDisabled then
isSittingDisabled = true
end
if not isLayingDisabled then
isLayingDisabled = true
end
else
disableReasons.sit[reason] = nil
disableReasons.lay[reason] = nil
RefreshIsSitDisabled()
RefreshIsLayDisabled()
end
end
local function IsSittingDisabled()
return isSittingDisabled, disableReasons.sit
end
local function IsLayingDisabled()
return isLayingDisabled, disableReasons.lay
end
local function GetSitDisableReasons()
return disableReasons.sit
end
local function GetLayDisableReasons()
return disableReasons.lay
end
exports('IsSitting', IsSitting)
exports('IsLaying', IsLaying)
exports('IsSittingOrLaying', IsSittingOrLaying)
exports('IsNearSeat', IsNearSeat)
exports('IsNearBed', IsNearBed)
exports('GetClosestSeat', GetClosestSeat)
exports('GetClosestBed', GetClosestBed)
exports('SitOnClosestSeat', SitOnClosestSeat)
exports('LayOnClosestBed', LayOnClosestBed)
exports('StopCurrentAction', StopCurrentAction)
exports('DisableSitting', DisableSitting)
exports('DisableLaying', DisableLaying)
exports('DisableSittingAndLaying', DisableSittingAndLaying)
exports('IsSittingDisabled', IsSittingDisabled)
exports('IsLayingDisabled', IsLayingDisabled)
exports('GetSitDisableReasons', GetSitDisableReasons)
exports('GetLayDisableReasons', GetLayDisableReasons)