2025-04-13 05:52:23 +00:00

1703 lines
52 KiB
Lua

-- Variables --
local Models = GetModels()
local PolyZones = GetPolyZones()
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,
}
local sittingScenarios = {
"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"
}
-- Ignore this if you don't use the TMC framework
local TMC = nil
if Config.UseTMCFramework then
TMC = exports.core:getCoreObject()
end
-- Functions --
local function DisplayNotification(msg)
-- Ignore this if you don't use the TMC framework
if Config.UseTMCFramework then
TMC.Functions.SimpleNotify(msg, "error", 5000)
return
end
-- Native Notification (just comment out this and un-comment/add what notifcation you would like to use)
BeginTextCommandThefeedPost("STRING")
AddTextComponentSubstringPlayerName(msg)
EndTextCommandThefeedPostTicker(false, false)
-- QBCore
--TriggerEvent('QBCore:Notify', msg, "success", 5000)
-- Mythic Notify (Variant)
--exports.mythic_notify:SendAlert('error', msg)
end
-- TODO: Add missing keys in the list + rename some of the mouse related ones
local specialKeyCodes = {
['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'
}
---Get's the label of a key mapping command
---@param commandHash any
---@return string
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 specialKeyCodes[key] or "unknown"
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 matrix = {}
matrix[1] = {}
matrix[1][1] = math.cos(rotZ) * math.cos(rotY) - math.sin(rotZ) * math.sin(rotX) * math.sin(rotY)
matrix[1][2] = math.cos(rotY) * math.sin(rotZ) + math.cos(rotZ) * math.sin(rotX) * math.sin(rotY)
matrix[1][3] = -math.cos(rotX) * math.sin(rotY)
matrix[1][4] = 1
matrix[2] = {}
matrix[2][1] = -math.cos(rotX) * math.sin(rotZ)
matrix[2][2] = math.cos(rotZ) * math.cos(rotX)
matrix[2][3] = math.sin(rotX)
matrix[2][4] = 1
matrix[3] = {}
matrix[3][1] = math.cos(rotZ) * math.sin(rotY) + math.cos(rotY) * math.sin(rotZ) * math.sin(rotX)
matrix[3][2] = math.sin(rotZ) * math.sin(rotY) - math.cos(rotZ) * math.cos(rotY) * math.sin(rotX)
matrix[3][3] = math.cos(rotX) * math.cos(rotY)
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 IsEntityPlayingAnyLayAnim(entity)
local checked = {}
for _type, settings in pairs(Config.LayTypes) do
local anim = settings.animation
if not checked[anim.dict] then
if IsEntityPlayingAnim(entity, 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(sittingScenarios) 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 IsEntityPlayingAnyLayAnim(ped) or dist < 0.55 then
return false
end
elseif action == 'lay' then
if IsEntityPlayingAnyLayAnim(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
ClearPedTasksImmediately(PlayerPedId())
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 or "both"
---@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)
local playerCoords = GetEntityCoords(PlayerPedId())
local closest = {
type = "unknown",
distance = 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(PolyZones) 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
return closest.type ~= "unknown", closest
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
print("^3Warning: No settings were set for type^2", seat.type, "^3 in Config.SitTypes, the default settings were used instead!")
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
-- 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 playerPed = PlayerPedId()
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 = coords+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")
if not found or closest.distance > Config.MaxInteractionDist then
DisplayNotification(Config.Lang.Notification.NoFound)
elseif closest.name ~= nil then
local seat = PolyZones[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 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 Config.AlwaysTeleportOutOfSeat or Config.LayTypes[bed.type].teleportOut or bed.teleportOut then
metadata.teleportOut = true
metadata.lastPos = GetEntityCoords(playerPed)
end
local animation = nil
if Config.LayTypes[bed.type] then
animation = Config.LayTypes[bed.type].animation
else
print("^3Warning: No animation settings were set for type^2", bed.type, "^3 in Config.LayTypes, the default animation settings were used instead!")
animation = Config.LayTypes['default'].animation
end
metadata.animation = animation
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")
if not found or closest.distance > Config.MaxInteractionDist then
DisplayNotification(Config.Lang.Notification.NoBedFound)
elseif closest.name ~= nil then
local seat = PolyZones[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
print('^3Warning: exitAnimType:', exitAnimType, 'was not an expcted type, please correct this, setting type to 1 for this instance ("m_getout_r").')
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,
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(PolyZones) 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 type = 'sit'
local options = {}
if info.lay then
local index = #options+1
type = '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
options[index].canInteract = function(entity, _distance, _coords, _name, _bone)
if not entity or GetEntityType(entity) == 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
type = '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
options[index].canInteract = function(entity, _distance, _coords, _name, _bone)
if not entity or GetEntityType(entity) == 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[type].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[type].seats[1].z+(info.poly.height/2)
local heading = info.poly.heading or info[type].seats[1].w
local center = info.poly.center or info[type].seats[1].xyz
if info.poly.type == "circle" then
local radius = info.poly.radius
if radius == nil then
print("^3Warning: PolyZone '"..name.."' did not have a specified radius! Radius was automatically set to 1.5!")
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
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")
if found and closest.distance < Config.MaxPromptDist 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 not IsPauseMenuActive() then
if IsPlayerDoingAnyAction() then
if not Config.UsePrompts then
StopCurrentAction()
end
else
SitOnClosestSeat()
end
end
end, false)
RegisterCommand("lay", function()
if not IsPauseMenuActive() then
if IsPlayerDoingAnyAction() then
if not Config.UsePrompts then
StopCurrentAction()
end
else
LayOnClosestBed()
end
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 --
-- Add your type of persistent notifcations here
AddEventHandler('sit:helpTextThread', function(type)
CreateThread(function()
-- Ignore this if you don't use the TMC framework
if Config.UseTMCFramework 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(50)
end
TMC.Functions.StopNotify("sit_notif")
return
end
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)
AddEventHandler('sit:checkThread', function(type)
CreateThread(function()
while true do
Wait(500)
if not metadata[type] then
break
end
-- Reduce stress
-- if Config.ReduceStress then
-- TriggerEvent("status:reduceStress", 0.15) -- keep in mind this is per 500 ms (500 ms = half a second).
-- 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)) then
local clearTask = true
if IsEntityDead(playerPed) or IsPedRagdoll(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)
AddEventHandler('sit:sitDown', function()
SitOnClosestSeat()
end)
AddEventHandler('sit:layDown', function()
LayOnClosestBed()
end)
AddEventHandler('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
print("^3Warning: 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 IsEntityPlayingAnyLayAnim(ped) or dist < 0.55 then
return false
end
elseif action == 'lay' then
if IsEntityPlayingAnyLayAnim(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
for typeset, data in pairs(information) do
for seatIndex, seat in pairs(data.seats) do
local subName = typeset..": "..model
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(PolyZones) 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 PolyZones[group] then
local xPoints = {}
local yPoints = {}
local zPoints = {}
local index = 0
for k, v in pairs(PolyZones[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 PolyZones[group] and PolyZones[group].center then
local center = PolyZones[group].center
local farthest = {
dist = 0,
name = 'error'
}
for name, data in pairs(PolyZones[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 PolyZones[group] and PolyZones[group].center then
PolyZones[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 PolyZones[group] and PolyZones[group].center then
PolyZones[group].enabled = false
for name, _info in pairs(PolyZones[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
-- Exports --
local function IsSitting()
return metadata.isSitting
end
local function IsLaying()
return metadata.isLaying
end
local function IsNearSeat()
local found, closest = GetClosestPositionOfType("sit")
return found and closest.distance < Config.MaxInteractionDist or false
end
local function IsNearBed()
local found, closest = GetClosestPositionOfType("lay")
return found and closest.distance < Config.MaxInteractionDist or false
end
local function GetClosestSeat()
return GetClosestPositionOfType("sit")
end
local function GetClosestBed()
return GetClosestPositionOfType("lay")
end
exports('IsSitting', IsSitting)
exports('IsLaying', IsLaying)
exports('IsNearSeat', IsNearSeat)
exports('IsNearBed', IsNearBed)
exports('GetClosestSeat', GetClosestSeat)
exports('GetClosestBed', GetClosestBed)
exports('SitOnClosestSeat', SitOnClosestSeat)
exports('LayOnClosestBed', LayOnClosestBed)
exports('StopCurrentAction', StopCurrentAction)