forked from Simnation/Main
394 lines
12 KiB
Lua
394 lines
12 KiB
Lua
-- Cutscene Manager for FiveM
|
|
-- Handles cutscene loading, playback, and character management
|
|
|
|
---@class Cutscene
|
|
Cutscenes = Cutscenes or {}
|
|
Cutscene = Cutscene or {}
|
|
Cutscene.done = true
|
|
|
|
-- Constants
|
|
local LOAD_TIMEOUT <const> = 10000
|
|
local FADE_DURATION <const> = 1000
|
|
local CUTSCENE_WAIT <const> = 1000
|
|
|
|
-- Character model definitions
|
|
local characterTags <const> = {
|
|
{ male = 'MP_1' },
|
|
{ male = 'MP_2' },
|
|
{ male = 'MP_3' },
|
|
{ male = 'MP_4' },
|
|
{ male = 'MP_Male_Character', female = 'MP_Female_Character' },
|
|
{ male = 'MP_Male_Character_1', female = 'MP_Female_Character_1' },
|
|
{ male = 'MP_Male_Character_2', female = 'MP_Female_Character_2' },
|
|
{ male = 'MP_Male_Character_3', female = 'MP_Female_Character_3' },
|
|
{ male = 'MP_Male_Character_4', female = 'MP_Female_Character_4' },
|
|
{ male = 'MP_Plane_Passenger_1' },
|
|
{ male = 'MP_Plane_Passenger_2' },
|
|
{ male = 'MP_Plane_Passenger_3' },
|
|
{ male = 'MP_Plane_Passenger_4' },
|
|
{ male = 'MP_Plane_Passenger_5' },
|
|
{ male = 'MP_Plane_Passenger_6' },
|
|
{ male = 'MP_Plane_Passenger_7' },
|
|
{ male = 'MP_Plane_Passenger_8' },
|
|
{ male = 'MP_Plane_Passenger_9' },
|
|
}
|
|
|
|
-- Component definitions for character customization
|
|
local componentsToSave <const> = {
|
|
-- Components
|
|
{ name = "head", id = 0, type = "drawable" },
|
|
{ name = "beard", id = 1, type = "drawable" },
|
|
{ name = "hair", id = 2, type = "drawable" },
|
|
{ name = "arms", id = 3, type = "drawable" },
|
|
{ name = "pants", id = 4, type = "drawable" },
|
|
{ name = "parachute", id = 5, type = "drawable" },
|
|
{ name = "feet", id = 6, type = "drawable" },
|
|
{ name = "accessories", id = 7, type = "drawable" },
|
|
{ name = "undershirt", id = 8, type = "drawable" },
|
|
{ name = "vest", id = 9, type = "drawable" },
|
|
{ name = "decals", id = 10, type = "drawable" },
|
|
{ name = "jacket", id = 11, type = "drawable" },
|
|
-- Props
|
|
{ name = "hat", id = 0, type = "prop" },
|
|
{ name = "glasses", id = 1, type = "prop" },
|
|
{ name = "ears", id = 2, type = "prop" },
|
|
{ name = "watch", id = 3, type = "prop" },
|
|
{ name = "bracelet", id = 4, type = "prop" },
|
|
{ name = "misc", id = 5, type = "prop" },
|
|
{ name = "left_wrist", id = 6, type = "prop" },
|
|
{ name = "right_wrist", id = 7, type = "prop" },
|
|
{ name = "prop8", id = 8, type = "prop" },
|
|
{ name = "prop9", id = 9, type = "prop" },
|
|
}
|
|
|
|
-- Utility Functions
|
|
|
|
function table.shallow_copy(t)
|
|
local t2 = {}
|
|
for k, v in pairs(t) do
|
|
t2[k] = v
|
|
end
|
|
return t2
|
|
end
|
|
|
|
local function WaitForModelLoad(modelHash)
|
|
local timeout = GetGameTimer() + LOAD_TIMEOUT
|
|
while not HasModelLoaded(modelHash) and GetGameTimer() < timeout do
|
|
Wait(0)
|
|
end
|
|
return HasModelLoaded(modelHash)
|
|
end
|
|
|
|
local function CreatePedFromModel(modelName, coords)
|
|
local model = GetHashKey(modelName)
|
|
RequestModel(model)
|
|
|
|
if not WaitForModelLoad(model) then
|
|
print("Failed to load model: " .. modelName)
|
|
return nil
|
|
end
|
|
|
|
local ped = CreatePed(0, model, coords.x, coords.y, coords.z, 0.0, true, false)
|
|
if not DoesEntityExist(ped) then
|
|
print("Failed to create ped from model: " .. modelName)
|
|
return nil
|
|
end
|
|
|
|
return ped
|
|
end
|
|
|
|
-- Cutscene Core Functions
|
|
local function LoadCutscene(cutscene)
|
|
assert(cutscene, "Cutscene.Load called without a cutscene name.")
|
|
local playbackList = IsPedMale(PlayerPedId()) and 31 or 103
|
|
RequestCutsceneWithPlaybackList(cutscene, playbackList, 8)
|
|
|
|
local timeout = GetGameTimer() + LOAD_TIMEOUT
|
|
while not HasCutsceneLoaded() and GetGameTimer() < timeout do
|
|
Wait(0)
|
|
end
|
|
|
|
if not HasCutsceneLoaded() then
|
|
print("Cutscene failed to load: ", cutscene)
|
|
return false
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
local function GetCutsceneTags(cutscene)
|
|
if not LoadCutscene(cutscene) then return end
|
|
|
|
StartCutscene(0)
|
|
Wait(CUTSCENE_WAIT)
|
|
|
|
local tags = {}
|
|
for _, tag in pairs(characterTags) do
|
|
if DoesCutsceneEntityExist(tag.male, 0) or DoesCutsceneEntityExist(tag.female, 0) then
|
|
table.insert(tags, tag)
|
|
end
|
|
end
|
|
|
|
StopCutsceneImmediately()
|
|
Wait(CUTSCENE_WAIT * 2)
|
|
return tags
|
|
end
|
|
|
|
-- Character Outfit Management
|
|
local function SavePedOutfit(ped)
|
|
local outfitData = {}
|
|
|
|
for _, component in ipairs(componentsToSave) do
|
|
if component.type == "drawable" then
|
|
outfitData[component.name] = {
|
|
id = component.id,
|
|
type = component.type,
|
|
drawable = GetPedDrawableVariation(ped, component.id),
|
|
texture = GetPedTextureVariation(ped, component.id),
|
|
palette = GetPedPaletteVariation(ped, component.id),
|
|
}
|
|
elseif component.type == "prop" then
|
|
outfitData[component.name] = {
|
|
id = component.id,
|
|
type = component.type,
|
|
propIndex = GetPedPropIndex(ped, component.id),
|
|
propTexture = GetPedPropTextureIndex(ped, component.id),
|
|
}
|
|
end
|
|
end
|
|
|
|
return outfitData
|
|
end
|
|
|
|
local function ApplyPedOutfit(ped, outfitData)
|
|
if not outfitData or type(outfitData) ~= "table" then
|
|
print("ApplyPedOutfit: Invalid outfitData provided.")
|
|
return
|
|
end
|
|
|
|
for componentName, data in pairs(outfitData) do
|
|
if data.type == "drawable" then
|
|
SetPedComponentVariation(ped, data.id, data.drawable or 0, data.texture or 0, data.palette or 0)
|
|
elseif data.type == "prop" then
|
|
if data.propIndex == -1 or data.propIndex == nil then
|
|
ClearPedProp(ped, data.id)
|
|
else
|
|
SetPedPropIndex(ped, data.id, data.propIndex, data.propTexture or 0, true)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Public Interface
|
|
function Cutscene.GetTags(cutscene)
|
|
return GetCutsceneTags(cutscene)
|
|
end
|
|
|
|
function Cutscene.Load(cutscene)
|
|
return LoadCutscene(cutscene)
|
|
end
|
|
|
|
function Cutscene.SavePedOutfit(ped)
|
|
return SavePedOutfit(ped)
|
|
end
|
|
|
|
function Cutscene.ApplyPedOutfit(ped, outfitData)
|
|
return ApplyPedOutfit(ped, outfitData)
|
|
end
|
|
|
|
function Cutscene.Create(cutscene, coords, srcs)
|
|
local lastCoords = coords or GetEntityCoords(PlayerPedId())
|
|
DoScreenFadeOut(0)
|
|
|
|
local tagsFromCutscene = GetCutsceneTags(cutscene)
|
|
if not LoadCutscene(cutscene) then
|
|
print("Cutscene.Create: Failed to load cutscene", cutscene)
|
|
DoScreenFadeIn(0)
|
|
return false
|
|
end
|
|
|
|
srcs = srcs or {}
|
|
local clothes = {}
|
|
local localped = PlayerPedId()
|
|
|
|
-- Process players and create necessary peds
|
|
local playersToProcess = { { ped = localped, identifier = "localplayer", coords = lastCoords } }
|
|
|
|
for _, src_raw in ipairs(srcs) do
|
|
if type(src_raw) == 'number' then
|
|
if src_raw and not DoesEntityExist(src_raw) then
|
|
local playerPed = GetPlayerPed(GetPlayerFromServerId(src_raw))
|
|
if DoesEntityExist(playerPed) then
|
|
local ped = ClonePed(playerPed, false, false, true)
|
|
table.insert(playersToProcess, { ped = ped, identifier = "player" })
|
|
end
|
|
else
|
|
table.insert(playersToProcess, { ped = src_raw, identifier = "user" })
|
|
end
|
|
elseif type(src_raw) == 'string' then
|
|
local ped = CreatePedFromModel(src_raw, GetEntityCoords(localped))
|
|
if ped then
|
|
table.insert(playersToProcess, { ped = ped, identifier = 'script' })
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Process available tags and assign to players
|
|
local availableTags = table.shallow_copy(tagsFromCutscene or {})
|
|
local usedTags = {}
|
|
local cleanupTags = {}
|
|
|
|
for _, playerData in ipairs(playersToProcess) do
|
|
local currentPed = playerData.ped
|
|
if not currentPed or not DoesEntityExist(currentPed) then goto continue end
|
|
|
|
local tagTable = table.remove(availableTags, 1)
|
|
if not tagTable then
|
|
print("Cutscene.Create: No available tags for player", playerData.identifier)
|
|
break
|
|
end
|
|
|
|
local isPedMale = IsPedMale(currentPed)
|
|
local tag = isPedMale and tagTable.male or tagTable.female
|
|
local unusedTag = not isPedMale and tagTable.male or tagTable.female -- needs to be this way as default has to be null for missing female
|
|
|
|
SetCutsceneEntityStreamingFlags(tag, 0, 1)
|
|
RegisterEntityForCutscene(currentPed, tag, 0, GetEntityModel(currentPed), 64)
|
|
SetCutscenePedComponentVariationFromPed(tag, currentPed, 0)
|
|
|
|
clothes[tag] = {
|
|
clothing = SavePedOutfit(currentPed),
|
|
ped = currentPed
|
|
}
|
|
|
|
table.insert(usedTags, tag)
|
|
if unusedTag then table.insert(cleanupTags, unusedTag) end
|
|
|
|
::continue::
|
|
end
|
|
|
|
-- Clean up unused tags
|
|
for _, tag in ipairs(cleanupTags) do
|
|
local ped = RegisterEntityForCutscene(0, tag, 3, 0, 64)
|
|
if ped then
|
|
SetEntityVisible(ped, false, false)
|
|
end
|
|
end
|
|
|
|
-- Handle remaining unused tags
|
|
for _, tag in ipairs(availableTags) do
|
|
for _, tagType in pairs({ tag.male, tag.female }) do
|
|
if tagType then
|
|
local ped = RegisterEntityForCutscene(0, tagType, 3, 0, 64)
|
|
if ped then
|
|
SetEntityVisible(ped, false, false)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return {
|
|
cutscene = cutscene,
|
|
coords = coords,
|
|
tags = usedTags,
|
|
srcs = srcs,
|
|
peds = playersToProcess,
|
|
clothes = clothes,
|
|
}
|
|
end
|
|
|
|
function Cutscene.Start(cutsceneData)
|
|
if not cutsceneData then
|
|
print("Cutscene.Start: Cutscene data is nil.")
|
|
return
|
|
end
|
|
|
|
DoScreenFadeIn(FADE_DURATION)
|
|
Cutscene.done = false
|
|
|
|
local clothes = cutsceneData.clothes
|
|
local coords = cutsceneData.coords
|
|
|
|
-- Start cutscene
|
|
if coords then
|
|
if type(coords) == 'boolean' then
|
|
coords = GetEntityCoords(PlayerPedId())
|
|
end
|
|
StartCutsceneAtCoords(coords.x, coords.y, coords.z, 0)
|
|
else
|
|
StartCutscene(0)
|
|
end
|
|
|
|
Wait(100)
|
|
|
|
-- Apply clothing to cutscene characters
|
|
for k, datam in pairs(clothes) do
|
|
local ped = datam.ped
|
|
if DoesEntityExist(ped) then
|
|
SetCutscenePedComponentVariationFromPed(k, ped, 0)
|
|
ApplyPedOutfit(ped, datam.clothing)
|
|
end
|
|
end
|
|
|
|
-- Scene loading thread
|
|
CreateThread(function()
|
|
local lastCoords
|
|
while not Cutscene.done do
|
|
local coords = GetWorldCoordFromScreenCoord(0.5, 0.5)
|
|
if not lastCoords or #(lastCoords - coords) > 100 then
|
|
NewLoadSceneStartSphere(coords.x, coords.y, coords.z, 2000, 0)
|
|
lastCoords = coords
|
|
end
|
|
Wait(500)
|
|
end
|
|
end)
|
|
|
|
-- Control blocking thread
|
|
CreateThread(function()
|
|
while not Cutscene.done do
|
|
DisableAllControlActions(0)
|
|
DisableFrontendThisFrame()
|
|
Wait(3)
|
|
end
|
|
end)
|
|
|
|
-- Main cutscene loop
|
|
while not HasCutsceneFinished() and not Cutscene.done do
|
|
Wait(0)
|
|
if IsDisabledControlJustPressed(0, 200) then
|
|
DoScreenFadeOut(FADE_DURATION)
|
|
Wait(FADE_DURATION)
|
|
StopCutsceneImmediately()
|
|
Wait(500)
|
|
Cutscene.done = true
|
|
break
|
|
end
|
|
end
|
|
|
|
-- Cleanup
|
|
DoScreenFadeIn(FADE_DURATION)
|
|
for _, playerData in ipairs(cutsceneData.peds) do
|
|
local ped = playerData.ped
|
|
if not ped or not DoesEntityExist(ped) then goto continue end
|
|
|
|
if playerData.identifier == 'script' then
|
|
DeleteEntity(ped)
|
|
elseif playerData.identifier == 'localplayer' then
|
|
SetEntityCoords(ped, playerData.coords.x, playerData.coords.y, playerData.coords.z, false, false, false, false)
|
|
end
|
|
::continue::
|
|
end
|
|
Cutscene.done = true
|
|
end
|
|
|
|
|
|
-- RegisterCommand('cutscene', function(source, args, rawCommand)
|
|
-- local cutscene = args[1]
|
|
-- if cutscene then
|
|
-- local cutsceneData = Cutscene.Create(cutscene, false, { 1,1,1 })
|
|
-- Cutscene.Start(cutsceneData)
|
|
|
|
-- end
|
|
-- end, false)
|
|
|
|
return Cutscene
|