0.0.1
This commit is contained in:
30
resources/[cfx-default]/[managers]/mapmanager/fxmanifest.lua
Normal file
30
resources/[cfx-default]/[managers]/mapmanager/fxmanifest.lua
Normal file
@@ -0,0 +1,30 @@
|
||||
-- This resource is part of the default Cfx.re asset pack (cfx-server-data)
|
||||
-- Altering or recreating for local use only is strongly discouraged.
|
||||
|
||||
version '1.0.0'
|
||||
author 'Cfx.re <root@cfx.re>'
|
||||
description 'A flexible handler for game type/map association.'
|
||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||
|
||||
client_scripts {
|
||||
"mapmanager_shared.lua",
|
||||
"mapmanager_client.lua"
|
||||
}
|
||||
|
||||
server_scripts {
|
||||
"mapmanager_shared.lua",
|
||||
"mapmanager_server.lua"
|
||||
}
|
||||
|
||||
fx_version 'adamant'
|
||||
games { 'gta5', 'rdr3' }
|
||||
|
||||
server_export "getCurrentGameType"
|
||||
server_export "getCurrentMap"
|
||||
server_export "changeGameType"
|
||||
server_export "changeMap"
|
||||
server_export "doesMapSupportGameType"
|
||||
server_export "getMaps"
|
||||
server_export "roundEnded"
|
||||
|
||||
rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.'
|
||||
@@ -0,0 +1,108 @@
|
||||
local maps = {}
|
||||
local gametypes = {}
|
||||
|
||||
AddEventHandler('onClientResourceStart', function(res)
|
||||
-- parse metadata for this resource
|
||||
|
||||
-- map files
|
||||
local num = GetNumResourceMetadata(res, 'map')
|
||||
|
||||
if num > 0 then
|
||||
for i = 0, num-1 do
|
||||
local file = GetResourceMetadata(res, 'map', i)
|
||||
|
||||
if file then
|
||||
addMap(file, res)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- resource type data
|
||||
local type = GetResourceMetadata(res, 'resource_type', 0)
|
||||
|
||||
if type then
|
||||
local extraData = GetResourceMetadata(res, 'resource_type_extra', 0)
|
||||
|
||||
if extraData then
|
||||
extraData = json.decode(extraData)
|
||||
else
|
||||
extraData = {}
|
||||
end
|
||||
|
||||
if type == 'map' then
|
||||
maps[res] = extraData
|
||||
elseif type == 'gametype' then
|
||||
gametypes[res] = extraData
|
||||
end
|
||||
end
|
||||
|
||||
-- handle starting
|
||||
loadMap(res)
|
||||
|
||||
-- defer this to the next game tick to work around a lack of dependencies
|
||||
Citizen.CreateThread(function()
|
||||
Citizen.Wait(15)
|
||||
|
||||
if maps[res] then
|
||||
TriggerEvent('onClientMapStart', res)
|
||||
elseif gametypes[res] then
|
||||
TriggerEvent('onClientGameTypeStart', res)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
AddEventHandler('onResourceStop', function(res)
|
||||
if maps[res] then
|
||||
TriggerEvent('onClientMapStop', res)
|
||||
elseif gametypes[res] then
|
||||
TriggerEvent('onClientGameTypeStop', res)
|
||||
end
|
||||
|
||||
unloadMap(res)
|
||||
end)
|
||||
|
||||
AddEventHandler('getMapDirectives', function(add)
|
||||
if not CreateScriptVehicleGenerator then
|
||||
return
|
||||
end
|
||||
|
||||
add('vehicle_generator', function(state, name)
|
||||
return function(opts)
|
||||
local x, y, z, heading
|
||||
local color1, color2
|
||||
|
||||
if opts.x then
|
||||
x = opts.x
|
||||
y = opts.y
|
||||
z = opts.z
|
||||
else
|
||||
x = opts[1]
|
||||
y = opts[2]
|
||||
z = opts[3]
|
||||
end
|
||||
|
||||
heading = opts.heading or 1.0
|
||||
color1 = opts.color1 or -1
|
||||
color2 = opts.color2 or -1
|
||||
|
||||
CreateThread(function()
|
||||
local hash = GetHashKey(name)
|
||||
RequestModel(hash)
|
||||
|
||||
while not HasModelLoaded(hash) do
|
||||
Wait(0)
|
||||
end
|
||||
|
||||
local carGen = CreateScriptVehicleGenerator(x, y, z, heading, 5.0, 3.0, hash, color1, color2, -1, -1, true, false, false, true, true, -1)
|
||||
SetScriptVehicleGenerator(carGen, true)
|
||||
SetAllVehicleGeneratorsActive(true)
|
||||
|
||||
state.add('cargen', carGen)
|
||||
end)
|
||||
end
|
||||
end, function(state, arg)
|
||||
Citizen.Trace("deleting car gen " .. tostring(state.cargen) .. "\n")
|
||||
|
||||
DeleteScriptVehicleGenerator(state.cargen)
|
||||
end)
|
||||
end)
|
||||
@@ -0,0 +1,331 @@
|
||||
-- loosely based on MTA's https://code.google.com/p/mtasa-resources/source/browse/trunk/%5Bmanagers%5D/mapmanager/mapmanager_main.lua
|
||||
|
||||
local maps = {}
|
||||
local gametypes = {}
|
||||
|
||||
local function refreshResources()
|
||||
local numResources = GetNumResources()
|
||||
|
||||
for i = 0, numResources - 1 do
|
||||
local resource = GetResourceByFindIndex(i)
|
||||
|
||||
if GetNumResourceMetadata(resource, 'resource_type') > 0 then
|
||||
local type = GetResourceMetadata(resource, 'resource_type', 0)
|
||||
local params = json.decode(GetResourceMetadata(resource, 'resource_type_extra', 0))
|
||||
|
||||
local valid = false
|
||||
|
||||
local games = GetNumResourceMetadata(resource, 'game')
|
||||
if games > 0 then
|
||||
for j = 0, games - 1 do
|
||||
local game = GetResourceMetadata(resource, 'game', j)
|
||||
|
||||
if game == GetConvar('gamename', 'gta5') or game == 'common' then
|
||||
valid = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if valid then
|
||||
if type == 'map' then
|
||||
maps[resource] = params
|
||||
elseif type == 'gametype' then
|
||||
gametypes[resource] = params
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
AddEventHandler('onResourceListRefresh', function()
|
||||
refreshResources()
|
||||
end)
|
||||
|
||||
refreshResources()
|
||||
|
||||
AddEventHandler('onResourceStarting', function(resource)
|
||||
local num = GetNumResourceMetadata(resource, 'map')
|
||||
|
||||
if num then
|
||||
for i = 0, num-1 do
|
||||
local file = GetResourceMetadata(resource, 'map', i)
|
||||
|
||||
if file then
|
||||
addMap(file, resource)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if maps[resource] then
|
||||
if getCurrentMap() and getCurrentMap() ~= resource then
|
||||
if doesMapSupportGameType(getCurrentGameType(), resource) then
|
||||
print("Changing map from " .. getCurrentMap() .. " to " .. resource)
|
||||
|
||||
changeMap(resource)
|
||||
else
|
||||
-- check if there's only one possible game type for the map
|
||||
local map = maps[resource]
|
||||
local count = 0
|
||||
local gt
|
||||
|
||||
for type, flag in pairs(map.gameTypes) do
|
||||
if flag then
|
||||
count = count + 1
|
||||
gt = type
|
||||
end
|
||||
end
|
||||
|
||||
if count == 1 then
|
||||
print("Changing map from " .. getCurrentMap() .. " to " .. resource .. " (gt " .. gt .. ")")
|
||||
|
||||
changeGameType(gt)
|
||||
changeMap(resource)
|
||||
end
|
||||
end
|
||||
|
||||
CancelEvent()
|
||||
end
|
||||
elseif gametypes[resource] then
|
||||
if getCurrentGameType() and getCurrentGameType() ~= resource then
|
||||
print("Changing gametype from " .. getCurrentGameType() .. " to " .. resource)
|
||||
|
||||
changeGameType(resource)
|
||||
|
||||
CancelEvent()
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
math.randomseed(GetInstanceId())
|
||||
|
||||
local currentGameType = nil
|
||||
local currentMap = nil
|
||||
|
||||
AddEventHandler('onResourceStart', function(resource)
|
||||
if maps[resource] then
|
||||
if not getCurrentGameType() then
|
||||
for gt, _ in pairs(maps[resource].gameTypes) do
|
||||
changeGameType(gt)
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if getCurrentGameType() and not getCurrentMap() then
|
||||
if doesMapSupportGameType(currentGameType, resource) then
|
||||
if TriggerEvent('onMapStart', resource, maps[resource]) then
|
||||
if maps[resource].name then
|
||||
print('Started map ' .. maps[resource].name)
|
||||
SetMapName(maps[resource].name)
|
||||
else
|
||||
print('Started map ' .. resource)
|
||||
SetMapName(resource)
|
||||
end
|
||||
|
||||
currentMap = resource
|
||||
else
|
||||
currentMap = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif gametypes[resource] then
|
||||
if not getCurrentGameType() then
|
||||
if TriggerEvent('onGameTypeStart', resource, gametypes[resource]) then
|
||||
currentGameType = resource
|
||||
|
||||
local gtName = gametypes[resource].name or resource
|
||||
|
||||
SetGameType(gtName)
|
||||
|
||||
print('Started gametype ' .. gtName)
|
||||
|
||||
SetTimeout(50, function()
|
||||
if not currentMap then
|
||||
local possibleMaps = {}
|
||||
|
||||
for map, data in pairs(maps) do
|
||||
if data.gameTypes[currentGameType] then
|
||||
table.insert(possibleMaps, map)
|
||||
end
|
||||
end
|
||||
|
||||
if #possibleMaps > 0 then
|
||||
local rnd = math.random(#possibleMaps)
|
||||
changeMap(possibleMaps[rnd])
|
||||
end
|
||||
end
|
||||
end)
|
||||
else
|
||||
currentGameType = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle starting
|
||||
loadMap(resource)
|
||||
end)
|
||||
|
||||
local function handleRoundEnd()
|
||||
local possibleMaps = {}
|
||||
|
||||
for map, data in pairs(maps) do
|
||||
if data.gameTypes[currentGameType] then
|
||||
table.insert(possibleMaps, map)
|
||||
end
|
||||
end
|
||||
|
||||
if #possibleMaps > 1 then
|
||||
local mapname = currentMap
|
||||
|
||||
while mapname == currentMap do
|
||||
local rnd = math.random(#possibleMaps)
|
||||
mapname = possibleMaps[rnd]
|
||||
end
|
||||
|
||||
changeMap(mapname)
|
||||
elseif #possibleMaps > 0 then
|
||||
local rnd = math.random(#possibleMaps)
|
||||
changeMap(possibleMaps[rnd])
|
||||
end
|
||||
end
|
||||
|
||||
AddEventHandler('mapmanager:roundEnded', function()
|
||||
-- set a timeout as we don't want to return to a dead environment
|
||||
SetTimeout(50, handleRoundEnd) -- not a closure as to work around some issue in neolua?
|
||||
end)
|
||||
|
||||
function roundEnded()
|
||||
SetTimeout(50, handleRoundEnd)
|
||||
end
|
||||
|
||||
AddEventHandler('onResourceStop', function(resource)
|
||||
if resource == currentGameType then
|
||||
TriggerEvent('onGameTypeStop', resource)
|
||||
|
||||
currentGameType = nil
|
||||
|
||||
if currentMap then
|
||||
StopResource(currentMap)
|
||||
end
|
||||
elseif resource == currentMap then
|
||||
TriggerEvent('onMapStop', resource)
|
||||
|
||||
currentMap = nil
|
||||
end
|
||||
|
||||
-- unload the map
|
||||
unloadMap(resource)
|
||||
end)
|
||||
|
||||
AddEventHandler('rconCommand', function(commandName, args)
|
||||
if commandName == 'map' then
|
||||
if #args ~= 1 then
|
||||
RconPrint("usage: map [mapname]\n")
|
||||
end
|
||||
|
||||
if not maps[args[1]] then
|
||||
RconPrint('no such map ' .. args[1] .. "\n")
|
||||
CancelEvent()
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
if currentGameType == nil or not doesMapSupportGameType(currentGameType, args[1]) then
|
||||
local map = maps[args[1]]
|
||||
local count = 0
|
||||
local gt
|
||||
|
||||
for type, flag in pairs(map.gameTypes) do
|
||||
if flag then
|
||||
count = count + 1
|
||||
gt = type
|
||||
end
|
||||
end
|
||||
|
||||
if count == 1 then
|
||||
print("Changing map from " .. getCurrentMap() .. " to " .. args[1] .. " (gt " .. gt .. ")")
|
||||
|
||||
changeGameType(gt)
|
||||
changeMap(args[1])
|
||||
|
||||
RconPrint('map ' .. args[1] .. "\n")
|
||||
else
|
||||
RconPrint('map ' .. args[1] .. ' does not support ' .. currentGameType .. "\n")
|
||||
end
|
||||
|
||||
CancelEvent()
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
changeMap(args[1])
|
||||
|
||||
RconPrint('map ' .. args[1] .. "\n")
|
||||
|
||||
CancelEvent()
|
||||
elseif commandName == 'gametype' then
|
||||
if #args ~= 1 then
|
||||
RconPrint("usage: gametype [name]\n")
|
||||
end
|
||||
|
||||
if not gametypes[args[1]] then
|
||||
RconPrint('no such gametype ' .. args[1] .. "\n")
|
||||
CancelEvent()
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
changeGameType(args[1])
|
||||
|
||||
RconPrint('gametype ' .. args[1] .. "\n")
|
||||
|
||||
CancelEvent()
|
||||
end
|
||||
end)
|
||||
|
||||
function getCurrentGameType()
|
||||
return currentGameType
|
||||
end
|
||||
|
||||
function getCurrentMap()
|
||||
return currentMap
|
||||
end
|
||||
|
||||
function getMaps()
|
||||
return maps
|
||||
end
|
||||
|
||||
function changeGameType(gameType)
|
||||
if currentMap and not doesMapSupportGameType(gameType, currentMap) then
|
||||
StopResource(currentMap)
|
||||
end
|
||||
|
||||
if currentGameType then
|
||||
StopResource(currentGameType)
|
||||
end
|
||||
|
||||
StartResource(gameType)
|
||||
end
|
||||
|
||||
function changeMap(map)
|
||||
if currentMap then
|
||||
StopResource(currentMap)
|
||||
end
|
||||
|
||||
StartResource(map)
|
||||
end
|
||||
|
||||
function doesMapSupportGameType(gameType, map)
|
||||
if not gametypes[gameType] then
|
||||
return false
|
||||
end
|
||||
|
||||
if not maps[map] then
|
||||
return false
|
||||
end
|
||||
|
||||
if not maps[map].gameTypes then
|
||||
return true
|
||||
end
|
||||
|
||||
return maps[map].gameTypes[gameType]
|
||||
end
|
||||
@@ -0,0 +1,87 @@
|
||||
-- shared logic file for map manager - don't call any subsystem-specific functions here
|
||||
mapFiles = {}
|
||||
|
||||
function addMap(file, owningResource)
|
||||
if not mapFiles[owningResource] then
|
||||
mapFiles[owningResource] = {}
|
||||
end
|
||||
|
||||
table.insert(mapFiles[owningResource], file)
|
||||
end
|
||||
|
||||
undoCallbacks = {}
|
||||
|
||||
function loadMap(res)
|
||||
if mapFiles[res] then
|
||||
for _, file in ipairs(mapFiles[res]) do
|
||||
parseMap(file, res)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function unloadMap(res)
|
||||
if undoCallbacks[res] then
|
||||
for _, cb in ipairs(undoCallbacks[res]) do
|
||||
cb()
|
||||
end
|
||||
|
||||
undoCallbacks[res] = nil
|
||||
mapFiles[res] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function parseMap(file, owningResource)
|
||||
if not undoCallbacks[owningResource] then
|
||||
undoCallbacks[owningResource] = {}
|
||||
end
|
||||
|
||||
local env = {
|
||||
math = math, pairs = pairs, ipairs = ipairs, next = next, tonumber = tonumber, tostring = tostring,
|
||||
type = type, table = table, string = string, _G = env,
|
||||
vector3 = vector3, quat = quat, vec = vec, vector2 = vector2
|
||||
}
|
||||
|
||||
TriggerEvent('getMapDirectives', function(key, cb, undocb)
|
||||
env[key] = function(...)
|
||||
local state = {}
|
||||
|
||||
state.add = function(k, v)
|
||||
state[k] = v
|
||||
end
|
||||
|
||||
local result = cb(state, ...)
|
||||
local args = table.pack(...)
|
||||
|
||||
table.insert(undoCallbacks[owningResource], function()
|
||||
undocb(state)
|
||||
end)
|
||||
|
||||
return result
|
||||
end
|
||||
end)
|
||||
|
||||
local mt = {
|
||||
__index = function(t, k)
|
||||
if rawget(t, k) ~= nil then return rawget(t, k) end
|
||||
|
||||
-- as we're not going to return nothing here (to allow unknown directives to be ignored)
|
||||
local f = function()
|
||||
return f
|
||||
end
|
||||
|
||||
return function() return f end
|
||||
end
|
||||
}
|
||||
|
||||
setmetatable(env, mt)
|
||||
|
||||
local fileData = LoadResourceFile(owningResource, file)
|
||||
local mapFunction, err = load(fileData, file, 't', env)
|
||||
|
||||
if not mapFunction then
|
||||
Citizen.Trace("Couldn't load map " .. file .. ": " .. err .. " (type of fileData: " .. type(fileData) .. ")\n")
|
||||
return
|
||||
end
|
||||
|
||||
mapFunction()
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
-- This resource is part of the default Cfx.re asset pack (cfx-server-data)
|
||||
-- Altering or recreating for local use only is strongly discouraged.
|
||||
|
||||
version '1.0.0'
|
||||
author 'Cfx.re <root@cfx.re>'
|
||||
description 'Handles spawning a player in a unified fashion to prevent resources from having to implement custom spawn logic.'
|
||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||
|
||||
client_script 'spawnmanager.lua'
|
||||
|
||||
fx_version 'adamant'
|
||||
games { 'rdr3', 'gta5' }
|
||||
|
||||
rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.'
|
||||
386
resources/[cfx-default]/[managers]/spawnmanager/spawnmanager.lua
Normal file
386
resources/[cfx-default]/[managers]/spawnmanager/spawnmanager.lua
Normal file
@@ -0,0 +1,386 @@
|
||||
-- in-memory spawnpoint array for this script execution instance
|
||||
local spawnPoints = {}
|
||||
|
||||
-- auto-spawn enabled flag
|
||||
local autoSpawnEnabled = false
|
||||
local autoSpawnCallback
|
||||
|
||||
-- support for mapmanager maps
|
||||
AddEventHandler('getMapDirectives', function(add)
|
||||
-- call the remote callback
|
||||
add('spawnpoint', function(state, model)
|
||||
-- return another callback to pass coordinates and so on (as such syntax would be [spawnpoint 'model' { options/coords }])
|
||||
return function(opts)
|
||||
local x, y, z, heading
|
||||
|
||||
local s, e = pcall(function()
|
||||
-- is this a map or an array?
|
||||
if opts.x then
|
||||
x = opts.x
|
||||
y = opts.y
|
||||
z = opts.z
|
||||
else
|
||||
x = opts[1]
|
||||
y = opts[2]
|
||||
z = opts[3]
|
||||
end
|
||||
|
||||
x = x + 0.0001
|
||||
y = y + 0.0001
|
||||
z = z + 0.0001
|
||||
|
||||
-- get a heading and force it to a float, or just default to null
|
||||
heading = opts.heading and (opts.heading + 0.01) or 0
|
||||
|
||||
-- add the spawnpoint
|
||||
addSpawnPoint({
|
||||
x = x, y = y, z = z,
|
||||
heading = heading,
|
||||
model = model
|
||||
})
|
||||
|
||||
-- recalculate the model for storage
|
||||
if not tonumber(model) then
|
||||
model = GetHashKey(model, _r)
|
||||
end
|
||||
|
||||
-- store the spawn data in the state so we can erase it later on
|
||||
state.add('xyz', { x, y, z })
|
||||
state.add('model', model)
|
||||
end)
|
||||
|
||||
if not s then
|
||||
Citizen.Trace(e .. "\n")
|
||||
end
|
||||
end
|
||||
-- delete callback follows on the next line
|
||||
end, function(state, arg)
|
||||
-- loop through all spawn points to find one with our state
|
||||
for i, sp in ipairs(spawnPoints) do
|
||||
-- if it matches...
|
||||
if sp.x == state.xyz[1] and sp.y == state.xyz[2] and sp.z == state.xyz[3] and sp.model == state.model then
|
||||
-- remove it.
|
||||
table.remove(spawnPoints, i)
|
||||
return
|
||||
end
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
|
||||
-- loads a set of spawn points from a JSON string
|
||||
function loadSpawns(spawnString)
|
||||
-- decode the JSON string
|
||||
local data = json.decode(spawnString)
|
||||
|
||||
-- do we have a 'spawns' field?
|
||||
if not data.spawns then
|
||||
error("no 'spawns' in JSON data")
|
||||
end
|
||||
|
||||
-- loop through the spawns
|
||||
for i, spawn in ipairs(data.spawns) do
|
||||
-- and add it to the list (validating as we go)
|
||||
addSpawnPoint(spawn)
|
||||
end
|
||||
end
|
||||
|
||||
local spawnNum = 1
|
||||
|
||||
function addSpawnPoint(spawn)
|
||||
-- validate the spawn (position)
|
||||
if not tonumber(spawn.x) or not tonumber(spawn.y) or not tonumber(spawn.z) then
|
||||
error("invalid spawn position")
|
||||
end
|
||||
|
||||
-- heading
|
||||
if not tonumber(spawn.heading) then
|
||||
error("invalid spawn heading")
|
||||
end
|
||||
|
||||
-- model (try integer first, if not, hash it)
|
||||
local model = spawn.model
|
||||
|
||||
if not tonumber(spawn.model) then
|
||||
model = GetHashKey(spawn.model)
|
||||
end
|
||||
|
||||
-- is the model actually a model?
|
||||
if not IsModelInCdimage(model) then
|
||||
error("invalid spawn model")
|
||||
end
|
||||
|
||||
-- is is even a ped?
|
||||
-- not in V?
|
||||
--[[if not IsThisModelAPed(model) then
|
||||
error("this model ain't a ped!")
|
||||
end]]
|
||||
|
||||
-- overwrite the model in case we hashed it
|
||||
spawn.model = model
|
||||
|
||||
-- add an index
|
||||
spawn.idx = spawnNum
|
||||
spawnNum = spawnNum + 1
|
||||
|
||||
-- all OK, add the spawn entry to the list
|
||||
table.insert(spawnPoints, spawn)
|
||||
|
||||
return spawn.idx
|
||||
end
|
||||
|
||||
-- removes a spawn point
|
||||
function removeSpawnPoint(spawn)
|
||||
for i = 1, #spawnPoints do
|
||||
if spawnPoints[i].idx == spawn then
|
||||
table.remove(spawnPoints, i)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- changes the auto-spawn flag
|
||||
function setAutoSpawn(enabled)
|
||||
autoSpawnEnabled = enabled
|
||||
end
|
||||
|
||||
-- sets a callback to execute instead of 'native' spawning when trying to auto-spawn
|
||||
function setAutoSpawnCallback(cb)
|
||||
autoSpawnCallback = cb
|
||||
autoSpawnEnabled = true
|
||||
end
|
||||
|
||||
-- function as existing in original R* scripts
|
||||
local function freezePlayer(id, freeze)
|
||||
local player = id
|
||||
SetPlayerControl(player, not freeze, false)
|
||||
|
||||
local ped = GetPlayerPed(player)
|
||||
|
||||
if not freeze then
|
||||
if not IsEntityVisible(ped) then
|
||||
SetEntityVisible(ped, true)
|
||||
end
|
||||
|
||||
if not IsPedInAnyVehicle(ped) then
|
||||
SetEntityCollision(ped, true)
|
||||
end
|
||||
|
||||
FreezeEntityPosition(ped, false)
|
||||
--SetCharNeverTargetted(ped, false)
|
||||
SetPlayerInvincible(player, false)
|
||||
else
|
||||
if IsEntityVisible(ped) then
|
||||
SetEntityVisible(ped, false)
|
||||
end
|
||||
|
||||
SetEntityCollision(ped, false)
|
||||
FreezeEntityPosition(ped, true)
|
||||
--SetCharNeverTargetted(ped, true)
|
||||
SetPlayerInvincible(player, true)
|
||||
--RemovePtfxFromPed(ped)
|
||||
|
||||
if not IsPedFatallyInjured(ped) then
|
||||
ClearPedTasksImmediately(ped)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function loadScene(x, y, z)
|
||||
if not NewLoadSceneStart then
|
||||
return
|
||||
end
|
||||
|
||||
NewLoadSceneStart(x, y, z, 0.0, 0.0, 0.0, 20.0, 0)
|
||||
|
||||
while IsNewLoadSceneActive() do
|
||||
networkTimer = GetNetworkTimer()
|
||||
|
||||
NetworkUpdateLoadScene()
|
||||
end
|
||||
end
|
||||
|
||||
-- to prevent trying to spawn multiple times
|
||||
local spawnLock = false
|
||||
|
||||
-- spawns the current player at a certain spawn point index (or a random one, for that matter)
|
||||
function spawnPlayer(spawnIdx, cb)
|
||||
if spawnLock then
|
||||
return
|
||||
end
|
||||
|
||||
spawnLock = true
|
||||
|
||||
Citizen.CreateThread(function()
|
||||
-- if the spawn isn't set, select a random one
|
||||
if not spawnIdx then
|
||||
spawnIdx = GetRandomIntInRange(1, #spawnPoints + 1)
|
||||
end
|
||||
|
||||
-- get the spawn from the array
|
||||
local spawn
|
||||
|
||||
if type(spawnIdx) == 'table' then
|
||||
spawn = spawnIdx
|
||||
|
||||
-- prevent errors when passing spawn table
|
||||
spawn.x = spawn.x + 0.00
|
||||
spawn.y = spawn.y + 0.00
|
||||
spawn.z = spawn.z + 0.00
|
||||
|
||||
spawn.heading = spawn.heading and (spawn.heading + 0.00) or 0
|
||||
else
|
||||
spawn = spawnPoints[spawnIdx]
|
||||
end
|
||||
|
||||
if not spawn.skipFade then
|
||||
DoScreenFadeOut(500)
|
||||
|
||||
while not IsScreenFadedOut() do
|
||||
Citizen.Wait(0)
|
||||
end
|
||||
end
|
||||
|
||||
-- validate the index
|
||||
if not spawn then
|
||||
Citizen.Trace("tried to spawn at an invalid spawn index\n")
|
||||
|
||||
spawnLock = false
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
-- freeze the local player
|
||||
freezePlayer(PlayerId(), true)
|
||||
|
||||
-- if the spawn has a model set
|
||||
if spawn.model then
|
||||
RequestModel(spawn.model)
|
||||
|
||||
-- load the model for this spawn
|
||||
while not HasModelLoaded(spawn.model) do
|
||||
RequestModel(spawn.model)
|
||||
|
||||
Wait(0)
|
||||
end
|
||||
|
||||
-- change the player model
|
||||
SetPlayerModel(PlayerId(), spawn.model)
|
||||
|
||||
-- release the player model
|
||||
SetModelAsNoLongerNeeded(spawn.model)
|
||||
|
||||
-- RDR3 player model bits
|
||||
if N_0x283978a15512b2fe then
|
||||
N_0x283978a15512b2fe(PlayerPedId(), true)
|
||||
end
|
||||
end
|
||||
|
||||
-- preload collisions for the spawnpoint
|
||||
RequestCollisionAtCoord(spawn.x, spawn.y, spawn.z)
|
||||
|
||||
-- spawn the player
|
||||
local ped = PlayerPedId()
|
||||
|
||||
-- V requires setting coords as well
|
||||
SetEntityCoordsNoOffset(ped, spawn.x, spawn.y, spawn.z, false, false, false, true)
|
||||
|
||||
NetworkResurrectLocalPlayer(spawn.x, spawn.y, spawn.z, spawn.heading, true, true, false)
|
||||
|
||||
-- gamelogic-style cleanup stuff
|
||||
ClearPedTasksImmediately(ped)
|
||||
--SetEntityHealth(ped, 300) -- TODO: allow configuration of this?
|
||||
RemoveAllPedWeapons(ped) -- TODO: make configurable (V behavior?)
|
||||
ClearPlayerWantedLevel(PlayerId())
|
||||
|
||||
-- why is this even a flag?
|
||||
--SetCharWillFlyThroughWindscreen(ped, false)
|
||||
|
||||
-- set primary camera heading
|
||||
--SetGameCamHeading(spawn.heading)
|
||||
--CamRestoreJumpcut(GetGameCam())
|
||||
|
||||
-- load the scene; streaming expects us to do it
|
||||
--ForceLoadingScreen(true)
|
||||
--loadScene(spawn.x, spawn.y, spawn.z)
|
||||
--ForceLoadingScreen(false)
|
||||
|
||||
local time = GetGameTimer()
|
||||
|
||||
while (not HasCollisionLoadedAroundEntity(ped) and (GetGameTimer() - time) < 5000) do
|
||||
Citizen.Wait(0)
|
||||
end
|
||||
|
||||
ShutdownLoadingScreen()
|
||||
|
||||
if IsScreenFadedOut() then
|
||||
DoScreenFadeIn(500)
|
||||
|
||||
while not IsScreenFadedIn() do
|
||||
Citizen.Wait(0)
|
||||
end
|
||||
end
|
||||
|
||||
-- and unfreeze the player
|
||||
freezePlayer(PlayerId(), false)
|
||||
|
||||
TriggerEvent('playerSpawned', spawn)
|
||||
|
||||
if cb then
|
||||
cb(spawn)
|
||||
end
|
||||
|
||||
spawnLock = false
|
||||
end)
|
||||
end
|
||||
|
||||
-- automatic spawning monitor thread, too
|
||||
local respawnForced
|
||||
local diedAt
|
||||
|
||||
Citizen.CreateThread(function()
|
||||
-- main loop thing
|
||||
while true do
|
||||
Citizen.Wait(50)
|
||||
|
||||
local playerPed = PlayerPedId()
|
||||
|
||||
if playerPed and playerPed ~= -1 then
|
||||
-- check if we want to autospawn
|
||||
if autoSpawnEnabled then
|
||||
if NetworkIsPlayerActive(PlayerId()) then
|
||||
if (diedAt and (math.abs(GetTimeDifference(GetGameTimer(), diedAt)) > 2000)) or respawnForced then
|
||||
if autoSpawnCallback then
|
||||
autoSpawnCallback()
|
||||
else
|
||||
spawnPlayer()
|
||||
end
|
||||
|
||||
respawnForced = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if IsEntityDead(playerPed) then
|
||||
if not diedAt then
|
||||
diedAt = GetGameTimer()
|
||||
end
|
||||
else
|
||||
diedAt = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
function forceRespawn()
|
||||
spawnLock = false
|
||||
respawnForced = true
|
||||
end
|
||||
|
||||
exports('spawnPlayer', spawnPlayer)
|
||||
exports('addSpawnPoint', addSpawnPoint)
|
||||
exports('removeSpawnPoint', removeSpawnPoint)
|
||||
exports('loadSpawns', loadSpawns)
|
||||
exports('setAutoSpawn', setAutoSpawn)
|
||||
exports('setAutoSpawnCallback', setAutoSpawnCallback)
|
||||
exports('forceRespawn', forceRespawn)
|
||||
Reference in New Issue
Block a user