Part 1
This commit is contained in:
parent
ebdae82166
commit
2678716618
@ -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 'Example spawn points for FiveM with a "hipster" model.'
|
||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||
|
||||
resource_type 'map' { gameTypes = { ['basic-gamemode'] = true } }
|
||||
|
||||
map 'map.lua'
|
||||
|
||||
fx_version 'cerulean'
|
||||
game 'gta5'
|
@ -0,0 +1,6 @@
|
||||
vehicle_generator "airtug" { -54.26639938354492, -1679.548828125, 28.4414, heading = 228.2736053466797 }
|
||||
|
||||
-- FIB Server-rum
|
||||
spawnpoint 'a_m_y_hipster_01' { x = 153.14, y = -764.94, z = 258.15 }
|
||||
|
||||
--
|
@ -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 'Example spawn points for FiveM with a "skater" model.'
|
||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||
|
||||
resource_type 'map' { gameTypes = { ['basic-gamemode'] = true } }
|
||||
|
||||
map 'map.lua'
|
||||
|
||||
fx_version 'cerulean'
|
||||
game 'gta5'
|
@ -0,0 +1,4 @@
|
||||
-- FIB Server-rum
|
||||
spawnpoint 'a_m_y_skater_01' { x = 153.14, y = -764.94, z = 258.15 }
|
||||
|
||||
--
|
@ -0,0 +1,16 @@
|
||||
-- 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 'Example spawn points for RedM.'
|
||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||
|
||||
resource_type 'map' { gameTypes = { ['basic-gamemode'] = true } }
|
||||
|
||||
map 'map.lua'
|
||||
|
||||
fx_version 'cerulean'
|
||||
game 'rdr3'
|
||||
|
||||
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,2 @@
|
||||
spawnpoint 'player_three' { x = -262.849, y = 793.404, z = 118.087 }
|
||||
spawnpoint 'player_zero' { x = -262.849, y = 793.404, z = 118.087 }
|
@ -0,0 +1,4 @@
|
||||
AddEventHandler('onClientMapStart', function()
|
||||
exports.spawnmanager:setAutoSpawn(true)
|
||||
exports.spawnmanager:forceRespawn()
|
||||
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 'A basic freeroam gametype that uses the default spawn logic from spawnmanager.'
|
||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||
|
||||
resource_type 'gametype' { name = 'Freeroam' }
|
||||
|
||||
client_script 'basic_client.lua'
|
||||
|
||||
game 'common'
|
||||
fx_version 'cerulean'
|
@ -0,0 +1,101 @@
|
||||
-- add text entries for all the help types we have
|
||||
AddTextEntry('FOUNTAIN_HELP', 'This fountain currently contains $~1~.~n~Press ~INPUT_PICKUP~ to obtain $~1~.~n~Press ~INPUT_DETONATE~ to place $~1~.')
|
||||
AddTextEntry('FOUNTAIN_HELP_DRAINED', 'This fountain currently contains $~1~.~n~Press ~INPUT_DETONATE~ to place $~1~.')
|
||||
AddTextEntry('FOUNTAIN_HELP_BROKE', 'This fountain currently contains $~1~.~n~Press ~INPUT_PICKUP~ to obtain $~1~.')
|
||||
AddTextEntry('FOUNTAIN_HELP_BROKE_N_DRAINED', 'This fountain currently contains $~1~.')
|
||||
AddTextEntry('FOUNTAIN_HELP_INUSE', 'This fountain currently contains $~1~.~n~You can use it again in ~a~.')
|
||||
|
||||
-- upvalue aliases so that we will be fast if far away
|
||||
local Wait = Wait
|
||||
local GetEntityCoords = GetEntityCoords
|
||||
local PlayerPedId = PlayerPedId
|
||||
|
||||
-- timer, don't tick as frequently if we're far from any money fountain
|
||||
local relevanceTimer = 500
|
||||
|
||||
CreateThread(function()
|
||||
local pressing = false
|
||||
|
||||
while true do
|
||||
Wait(relevanceTimer)
|
||||
|
||||
local coords = GetEntityCoords(PlayerPedId())
|
||||
|
||||
for _, data in pairs(moneyFountains) do
|
||||
-- if we're near this fountain
|
||||
local dist = #(coords - data.coords)
|
||||
|
||||
-- near enough to draw
|
||||
if dist < 40 then
|
||||
-- ensure per-frame tick
|
||||
relevanceTimer = 0
|
||||
|
||||
DrawMarker(29, data.coords.x, data.coords.y, data.coords.z, 0, 0, 0, 0.0, 0, 0, 1.0, 1.0, 1.0, 0, 150, 0, 120, false, true, 2, false, nil, nil, false)
|
||||
else
|
||||
-- put the relevance timer back to the way it was
|
||||
relevanceTimer = 500
|
||||
end
|
||||
|
||||
-- near enough to use
|
||||
if dist < 1 then
|
||||
-- are we able to use it? if not, display appropriate help
|
||||
local player = LocalPlayer
|
||||
local nextUse = player.state['fountain_nextUse']
|
||||
|
||||
-- GetNetworkTime is synced for everyone
|
||||
if nextUse and nextUse >= GetNetworkTime() then
|
||||
BeginTextCommandDisplayHelp('FOUNTAIN_HELP_INUSE')
|
||||
AddTextComponentInteger(GlobalState['fountain_' .. data.id])
|
||||
AddTextComponentSubstringTime(math.tointeger(nextUse - GetNetworkTime()), 2 | 4) -- seconds (2), minutes (4)
|
||||
EndTextCommandDisplayHelp(0, false, false, 1000)
|
||||
else
|
||||
-- handle inputs for pickup/place
|
||||
if not pressing then
|
||||
if IsControlPressed(0, 38 --[[ INPUT_PICKUP ]]) then
|
||||
TriggerServerEvent('money_fountain:tryPickup', data.id)
|
||||
pressing = true
|
||||
elseif IsControlPressed(0, 47 --[[ INPUT_DETONATE ]]) then
|
||||
TriggerServerEvent('money_fountain:tryPlace', data.id)
|
||||
pressing = true
|
||||
end
|
||||
else
|
||||
if not IsControlPressed(0, 38 --[[ INPUT_PICKUP ]]) and
|
||||
not IsControlPressed(0, 47 --[[ INPUT_DETONATE ]]) then
|
||||
pressing = false
|
||||
end
|
||||
end
|
||||
|
||||
-- decide the appropriate help message
|
||||
local youCanSpend = (player.state['money_cash'] or 0) >= data.amount
|
||||
local fountainCanSpend = GlobalState['fountain_' .. data.id] >= data.amount
|
||||
|
||||
local helpName
|
||||
|
||||
if youCanSpend and fountainCanSpend then
|
||||
helpName = 'FOUNTAIN_HELP'
|
||||
elseif youCanSpend and not fountainCanSpend then
|
||||
helpName = 'FOUNTAIN_HELP_DRAINED'
|
||||
elseif not youCanSpend and fountainCanSpend then
|
||||
helpName = 'FOUNTAIN_HELP_BROKE'
|
||||
else
|
||||
helpName = 'FOUNTAIN_HELP_BROKE_N_DRAINED'
|
||||
end
|
||||
|
||||
-- and print it
|
||||
BeginTextCommandDisplayHelp(helpName)
|
||||
AddTextComponentInteger(GlobalState['fountain_' .. data.id])
|
||||
|
||||
if fountainCanSpend then
|
||||
AddTextComponentInteger(data.amount)
|
||||
end
|
||||
|
||||
if youCanSpend then
|
||||
AddTextComponentInteger(data.amount)
|
||||
end
|
||||
|
||||
EndTextCommandDisplayHelp(0, false, false, 1000)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
@ -0,0 +1,22 @@
|
||||
-- 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'
|
||||
description 'An example money system client containing a money fountain.'
|
||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||
author 'Cfx.re <root@cfx.re>'
|
||||
|
||||
fx_version 'cerulean'
|
||||
game 'gta5'
|
||||
|
||||
client_script 'client.lua'
|
||||
server_script 'server.lua'
|
||||
|
||||
shared_script 'mapdata.lua'
|
||||
|
||||
dependencies {
|
||||
'mapmanager',
|
||||
'money'
|
||||
}
|
||||
|
||||
lua54 'yes'
|
@ -0,0 +1,28 @@
|
||||
-- define the money fountain list (SHARED SCRIPT)
|
||||
moneyFountains = {}
|
||||
|
||||
-- index to know what to remove
|
||||
local fountainIdx = 1
|
||||
|
||||
AddEventHandler('getMapDirectives', function(add)
|
||||
-- add a 'money_fountain' map directive
|
||||
add('money_fountain', function(state, name)
|
||||
return function(data)
|
||||
local coords = data[1]
|
||||
local amount = data.amount or 100
|
||||
|
||||
local idx = fountainIdx
|
||||
fountainIdx += 1
|
||||
|
||||
moneyFountains[idx] = {
|
||||
id = name,
|
||||
coords = coords,
|
||||
amount = amount
|
||||
}
|
||||
|
||||
state.add('idx', idx)
|
||||
end
|
||||
end, function(state)
|
||||
moneyFountains[state.idx] = nil
|
||||
end)
|
||||
end)
|
@ -0,0 +1,107 @@
|
||||
-- track down what we've added to global state
|
||||
local sentState = {}
|
||||
|
||||
-- money system
|
||||
local ms = exports['money']
|
||||
|
||||
-- get the fountain content from storage
|
||||
local function getMoneyForId(fountainId)
|
||||
return GetResourceKvpInt(('money:%s'):format(fountainId)) / 100.0
|
||||
end
|
||||
|
||||
-- set the fountain content in storage + state
|
||||
local function setMoneyForId(fountainId, money)
|
||||
GlobalState['fountain_' .. fountainId] = math.tointeger(money)
|
||||
|
||||
return SetResourceKvpInt(('money:%s'):format(fountainId), math.tointeger(money * 100.0))
|
||||
end
|
||||
|
||||
-- get the nearest fountain to the player + ID
|
||||
local function getMoneyFountain(id, source)
|
||||
local coords = GetEntityCoords(GetPlayerPed(source))
|
||||
|
||||
for _, v in pairs(moneyFountains) do
|
||||
if v.id == id then
|
||||
if #(v.coords - coords) < 2.5 then
|
||||
return v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- generic function for events
|
||||
local function handleFountainStuff(source, id, pickup)
|
||||
-- if near the fountain we specify
|
||||
local fountain = getMoneyFountain(id, source)
|
||||
|
||||
if fountain then
|
||||
-- and we can actually use the fountain already
|
||||
local player = Player(source)
|
||||
|
||||
local nextUse = player.state['fountain_nextUse']
|
||||
if not nextUse then
|
||||
nextUse = 0
|
||||
end
|
||||
|
||||
-- GetGameTimer ~ GetNetworkTime on client
|
||||
if nextUse <= GetGameTimer() then
|
||||
-- not rate limited
|
||||
local success = false
|
||||
local money = getMoneyForId(fountain.id)
|
||||
|
||||
-- decide the op
|
||||
if pickup then
|
||||
-- if the fountain is rich enough to get the per-use amount
|
||||
if money >= fountain.amount then
|
||||
-- give the player money
|
||||
if ms:addMoney(source, 'cash', fountain.amount) then
|
||||
money -= fountain.amount
|
||||
success = true
|
||||
end
|
||||
end
|
||||
else
|
||||
-- if the player is rich enough
|
||||
if ms:removeMoney(source, 'cash', fountain.amount) then
|
||||
-- add to the fountain
|
||||
money += fountain.amount
|
||||
success = true
|
||||
end
|
||||
end
|
||||
|
||||
-- save it and set the player's cooldown
|
||||
if success then
|
||||
setMoneyForId(fountain.id, money)
|
||||
player.state['fountain_nextUse'] = GetGameTimer() + GetConvarInt('moneyFountain_cooldown', 5000)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- event for picking up fountain->player
|
||||
RegisterNetEvent('money_fountain:tryPickup')
|
||||
AddEventHandler('money_fountain:tryPickup', function(id)
|
||||
handleFountainStuff(source, id, true)
|
||||
end)
|
||||
|
||||
-- event for donating player->fountain
|
||||
RegisterNetEvent('money_fountain:tryPlace')
|
||||
AddEventHandler('money_fountain:tryPlace', function(id)
|
||||
handleFountainStuff(source, id, false)
|
||||
end)
|
||||
|
||||
-- listener: if a new fountain is added, set its current money in state
|
||||
CreateThread(function()
|
||||
while true do
|
||||
Wait(500)
|
||||
|
||||
for _, fountain in pairs(moneyFountains) do
|
||||
if not sentState[fountain.id] then
|
||||
GlobalState['fountain_' .. fountain.id] = math.tointeger(getMoneyForId(fountain.id))
|
||||
|
||||
sentState[fountain.id] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
@ -0,0 +1,30 @@
|
||||
local moneyTypes = {
|
||||
cash = `MP0_WALLET_BALANCE`,
|
||||
bank = `BANK_BALANCE`,
|
||||
}
|
||||
|
||||
RegisterNetEvent('money:displayUpdate')
|
||||
|
||||
AddEventHandler('money:displayUpdate', function(type, money)
|
||||
local stat = moneyTypes[type]
|
||||
if not stat then return end
|
||||
StatSetInt(stat, math.floor(money))
|
||||
end)
|
||||
|
||||
TriggerServerEvent('money:requestDisplay')
|
||||
|
||||
CreateThread(function()
|
||||
while true do
|
||||
Wait(0)
|
||||
|
||||
if IsControlJustPressed(0, 20) then
|
||||
SetMultiplayerBankCash()
|
||||
SetMultiplayerWalletCash()
|
||||
|
||||
Wait(4350)
|
||||
|
||||
RemoveMultiplayerBankCash()
|
||||
RemoveMultiplayerWalletCash()
|
||||
end
|
||||
end
|
||||
end)
|
@ -0,0 +1,16 @@
|
||||
-- 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'
|
||||
description 'An example money system using KVS.'
|
||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||
author 'Cfx.re <root@cfx.re>'
|
||||
|
||||
fx_version 'cerulean'
|
||||
game 'gta5'
|
||||
|
||||
client_script 'client.lua'
|
||||
server_script 'server.lua'
|
||||
|
||||
--dependency 'cfx.re/playerData.v1alpha1'
|
||||
lua54 'yes'
|
119
resources/[cfx-default]/[gameplay]/[examples]/money/server.lua
Normal file
119
resources/[cfx-default]/[gameplay]/[examples]/money/server.lua
Normal file
@ -0,0 +1,119 @@
|
||||
local playerData = exports['cfx.re/playerData.v1alpha1']
|
||||
|
||||
local validMoneyTypes = {
|
||||
bank = true,
|
||||
cash = true,
|
||||
}
|
||||
|
||||
local function getMoneyForId(playerId, moneyType)
|
||||
return GetResourceKvpInt(('money:%s:%s'):format(playerId, moneyType)) / 100.0
|
||||
end
|
||||
|
||||
local function setMoneyForId(playerId, moneyType, money)
|
||||
local s = playerData:getPlayerById(playerId)
|
||||
|
||||
TriggerEvent('money:updated', {
|
||||
dbId = playerId,
|
||||
source = s,
|
||||
moneyType = moneyType,
|
||||
money = money
|
||||
})
|
||||
|
||||
return SetResourceKvpInt(('money:%s:%s'):format(playerId, moneyType), math.tointeger(money * 100.0))
|
||||
end
|
||||
|
||||
local function addMoneyForId(playerId, moneyType, amount)
|
||||
local curMoney = getMoneyForId(playerId, moneyType)
|
||||
curMoney += amount
|
||||
|
||||
if curMoney >= 0 then
|
||||
setMoneyForId(playerId, moneyType, curMoney)
|
||||
return true, curMoney
|
||||
end
|
||||
|
||||
return false, 0
|
||||
end
|
||||
|
||||
exports('addMoney', function(playerIdx, moneyType, amount)
|
||||
amount = tonumber(amount)
|
||||
|
||||
if amount <= 0 or amount > (1 << 30) then
|
||||
return false
|
||||
end
|
||||
|
||||
if not validMoneyTypes[moneyType] then
|
||||
return false
|
||||
end
|
||||
|
||||
local playerId = playerData:getPlayerId(playerIdx)
|
||||
local success, money = addMoneyForId(playerId, moneyType, amount)
|
||||
|
||||
if success then
|
||||
Player(playerIdx).state['money_' .. moneyType] = money
|
||||
end
|
||||
|
||||
return true
|
||||
end)
|
||||
|
||||
exports('removeMoney', function(playerIdx, moneyType, amount)
|
||||
amount = tonumber(amount)
|
||||
|
||||
if amount <= 0 or amount > (1 << 30) then
|
||||
return false
|
||||
end
|
||||
|
||||
if not validMoneyTypes[moneyType] then
|
||||
return false
|
||||
end
|
||||
|
||||
local playerId = playerData:getPlayerId(playerIdx)
|
||||
local success, money = addMoneyForId(playerId, moneyType, -amount)
|
||||
|
||||
if success then
|
||||
Player(playerIdx).state['money_' .. moneyType] = money
|
||||
end
|
||||
|
||||
return success
|
||||
end)
|
||||
|
||||
exports('getMoney', function(playerIdx, moneyType)
|
||||
local playerId = playerData:getPlayerId(playerIdx)
|
||||
return getMoneyForId(playerId, moneyType)
|
||||
end)
|
||||
|
||||
-- player display bits
|
||||
AddEventHandler('money:updated', function(data)
|
||||
if data.source then
|
||||
TriggerClientEvent('money:displayUpdate', data.source, data.moneyType, data.money)
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterNetEvent('money:requestDisplay')
|
||||
|
||||
AddEventHandler('money:requestDisplay', function()
|
||||
local source = source
|
||||
local playerId = playerData:getPlayerId(source)
|
||||
|
||||
for type, _ in pairs(validMoneyTypes) do
|
||||
local amount = getMoneyForId(playerId, type)
|
||||
TriggerClientEvent('money:displayUpdate', source, type, amount)
|
||||
|
||||
Player(source).state['money_' .. type] = amount
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterCommand('earn', function(source, args)
|
||||
local type = args[1]
|
||||
local amount = tonumber(args[2])
|
||||
|
||||
exports['money']:addMoney(source, type, amount)
|
||||
end, true)
|
||||
|
||||
RegisterCommand('spend', function(source, args)
|
||||
local type = args[1]
|
||||
local amount = tonumber(args[2])
|
||||
|
||||
if not exports['money']:removeMoney(source, type, amount) then
|
||||
print('you are broke??')
|
||||
end
|
||||
end, true)
|
@ -0,0 +1,16 @@
|
||||
-- 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'
|
||||
description 'A basic resource for storing player identifiers.'
|
||||
author 'Cfx.re <root@cfx.re>'
|
||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||
|
||||
fx_version 'cerulean'
|
||||
game 'common'
|
||||
|
||||
server_script 'server.lua'
|
||||
|
||||
provides {
|
||||
'cfx.re/playerData.v1alpha1'
|
||||
}
|
222
resources/[cfx-default]/[gameplay]/player-data/server.lua
Normal file
222
resources/[cfx-default]/[gameplay]/player-data/server.lua
Normal file
@ -0,0 +1,222 @@
|
||||
--- player-data is a basic resource to showcase player identifier storage
|
||||
--
|
||||
-- it works in a fairly simple way: a set of identifiers is assigned to an account ID, and said
|
||||
-- account ID is then returned/added as state bag
|
||||
--
|
||||
-- it also implements the `cfx.re/playerData.v1alpha1` spec, which is exposed through the following:
|
||||
-- - getPlayerId(source: string)
|
||||
-- - getPlayerById(dbId: string)
|
||||
-- - getPlayerIdFromIdentifier(identifier: string)
|
||||
-- - setting `cfx.re/playerData@id` state bag field on the player
|
||||
|
||||
-- identifiers that we'll ignore (e.g. IP) as they're low-trust/high-variance
|
||||
local identifierBlocklist = {
|
||||
ip = true
|
||||
}
|
||||
|
||||
-- function to check if the identifier is blocked
|
||||
local function isIdentifierBlocked(identifier)
|
||||
-- Lua pattern to correctly split
|
||||
local idType = identifier:match('([^:]+):')
|
||||
|
||||
-- ensure it's a boolean
|
||||
return identifierBlocklist[idType] or false
|
||||
end
|
||||
|
||||
-- our database schema, in hierarchical KVS syntax:
|
||||
-- player:
|
||||
-- <id>:
|
||||
-- identifier:
|
||||
-- <identifier>: 'true'
|
||||
-- identifier:
|
||||
-- <identifier>: <playerId>
|
||||
|
||||
-- list of player indices to data
|
||||
local players = {}
|
||||
|
||||
-- list of player DBIDs to player indices
|
||||
local playersById = {}
|
||||
|
||||
-- a sequence field using KVS
|
||||
local function incrementId()
|
||||
local nextId = GetResourceKvpInt('nextId')
|
||||
nextId = nextId + 1
|
||||
SetResourceKvpInt('nextId', nextId)
|
||||
|
||||
return nextId
|
||||
end
|
||||
|
||||
-- gets the ID tied to an identifier in the schema, or nil
|
||||
local function getPlayerIdFromIdentifier(identifier)
|
||||
local str = GetResourceKvpString(('identifier:%s'):format(identifier))
|
||||
|
||||
if not str then
|
||||
return nil
|
||||
end
|
||||
|
||||
return msgpack.unpack(str).id
|
||||
end
|
||||
|
||||
-- stores the identifier + adds to a logging list
|
||||
local function setPlayerIdFromIdentifier(identifier, id)
|
||||
local str = ('identifier:%s'):format(identifier)
|
||||
SetResourceKvp(str, msgpack.pack({ id = id }))
|
||||
SetResourceKvp(('player:%s:identifier:%s'):format(id, identifier), 'true')
|
||||
end
|
||||
|
||||
-- stores any new identifiers for this player ID
|
||||
local function storeIdentifiers(playerIdx, newId)
|
||||
for _, identifier in ipairs(GetPlayerIdentifiers(playerIdx)) do
|
||||
if not isIdentifierBlocked(identifier) then
|
||||
-- TODO: check if the player already has an identifier of this type
|
||||
setPlayerIdFromIdentifier(identifier, newId)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- registers a new player (increments sequence, stores data, returns ID)
|
||||
local function registerPlayer(playerIdx)
|
||||
local newId = incrementId()
|
||||
storeIdentifiers(playerIdx, newId)
|
||||
|
||||
return newId
|
||||
end
|
||||
|
||||
-- initializes a player's data set
|
||||
local function setupPlayer(playerIdx)
|
||||
-- try getting the oldest-known identity from all the player's identifiers
|
||||
local defaultId = 0xFFFFFFFFFF
|
||||
local lowestId = defaultId
|
||||
|
||||
for _, identifier in ipairs(GetPlayerIdentifiers(playerIdx)) do
|
||||
if not isIdentifierBlocked(identifier) then
|
||||
local dbId = getPlayerIdFromIdentifier(identifier)
|
||||
|
||||
if dbId then
|
||||
if dbId < lowestId then
|
||||
lowestId = dbId
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- if this is the default ID, register. if not, update
|
||||
local playerId
|
||||
|
||||
if lowestId == defaultId then
|
||||
playerId = registerPlayer(playerIdx)
|
||||
else
|
||||
storeIdentifiers(playerIdx, lowestId)
|
||||
playerId = lowestId
|
||||
end
|
||||
|
||||
-- add state bag field
|
||||
if Player then
|
||||
Player(playerIdx).state['cfx.re/playerData@id'] = playerId
|
||||
end
|
||||
|
||||
-- and add to our caching tables
|
||||
players[playerIdx] = {
|
||||
dbId = playerId
|
||||
}
|
||||
|
||||
playersById[tostring(playerId)] = playerIdx
|
||||
end
|
||||
|
||||
-- we want to add a player pretty early
|
||||
AddEventHandler('playerConnecting', function()
|
||||
local playerIdx = tostring(source)
|
||||
setupPlayer(playerIdx)
|
||||
end)
|
||||
|
||||
-- and migrate them to a 'joining' ID where possible
|
||||
RegisterNetEvent('playerJoining')
|
||||
|
||||
AddEventHandler('playerJoining', function(oldIdx)
|
||||
-- resource restart race condition
|
||||
local oldPlayer = players[tostring(oldIdx)]
|
||||
|
||||
if oldPlayer then
|
||||
players[tostring(source)] = oldPlayer
|
||||
players[tostring(oldIdx)] = nil
|
||||
else
|
||||
setupPlayer(tostring(source))
|
||||
end
|
||||
end)
|
||||
|
||||
-- remove them if they're dropped
|
||||
AddEventHandler('playerDropped', function()
|
||||
local player = players[tostring(source)]
|
||||
|
||||
if player then
|
||||
playersById[tostring(player.dbId)] = nil
|
||||
end
|
||||
|
||||
players[tostring(source)] = nil
|
||||
end)
|
||||
|
||||
-- and when the resource is restarted, set up all players that are on right now
|
||||
for _, player in ipairs(GetPlayers()) do
|
||||
setupPlayer(player)
|
||||
end
|
||||
|
||||
-- also a quick command to get the current state
|
||||
RegisterCommand('playerData', function(source, args)
|
||||
if not args[1] then
|
||||
print('Usage:')
|
||||
print('\tplayerData getId <dbId>: gets identifiers for ID')
|
||||
print('\tplayerData getIdentifier <identifier>: gets ID for identifier')
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
if args[1] == 'getId' then
|
||||
local prefix = ('player:%s:identifier:'):format(args[2])
|
||||
local handle = StartFindKvp(prefix)
|
||||
local key
|
||||
|
||||
repeat
|
||||
key = FindKvp(handle)
|
||||
|
||||
if key then
|
||||
print('result:', key:sub(#prefix + 1))
|
||||
end
|
||||
until not key
|
||||
|
||||
EndFindKvp(handle)
|
||||
elseif args[1] == 'getIdentifier' then
|
||||
print('result:', getPlayerIdFromIdentifier(args[2]))
|
||||
end
|
||||
end, true)
|
||||
|
||||
-- COMPATIBILITY for server versions that don't export provide
|
||||
local function getExportEventName(resource, name)
|
||||
return string.format('__cfx_export_%s_%s', resource, name)
|
||||
end
|
||||
|
||||
function AddExport(name, fn)
|
||||
if not Citizen.Traits or not Citizen.Traits.ProvidesExports then
|
||||
AddEventHandler(getExportEventName('cfx.re/playerData.v1alpha1', name), function(setCB)
|
||||
setCB(fn)
|
||||
end)
|
||||
end
|
||||
|
||||
exports(name, fn)
|
||||
end
|
||||
|
||||
-- exports
|
||||
AddExport('getPlayerIdFromIdentifier', getPlayerIdFromIdentifier)
|
||||
|
||||
AddExport('getPlayerId', function(playerIdx)
|
||||
local player = players[tostring(playerIdx)]
|
||||
|
||||
if not player then
|
||||
return nil
|
||||
end
|
||||
|
||||
return player.dbId
|
||||
end)
|
||||
|
||||
AddExport('getPlayerById', function(playerId)
|
||||
return playersById[tostring(playerId)]
|
||||
end)
|
@ -0,0 +1,36 @@
|
||||
-- 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 basic resource for displaying player names.'
|
||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||
|
||||
-- add scripts
|
||||
client_script 'playernames_api.lua'
|
||||
server_script 'playernames_api.lua'
|
||||
|
||||
client_script 'playernames_cl.lua'
|
||||
server_script 'playernames_sv.lua'
|
||||
|
||||
-- make exports
|
||||
local exportList = {
|
||||
'setComponentColor',
|
||||
'setComponentAlpha',
|
||||
'setComponentVisibility',
|
||||
'setWantedLevel',
|
||||
'setHealthBarColor',
|
||||
'setNameTemplate'
|
||||
}
|
||||
|
||||
exports(exportList)
|
||||
server_exports(exportList)
|
||||
|
||||
-- add files
|
||||
files {
|
||||
'template/template.lua'
|
||||
}
|
||||
|
||||
-- support the latest resource manifest
|
||||
fx_version 'cerulean'
|
||||
game 'gta5'
|
@ -0,0 +1,80 @@
|
||||
local ids = {}
|
||||
|
||||
local function getTriggerFunction(key)
|
||||
return function(id, ...)
|
||||
-- if on the client, it's easy
|
||||
if not IsDuplicityVersion() then
|
||||
TriggerEvent('playernames:configure', GetPlayerServerId(id), key, ...)
|
||||
else
|
||||
-- if on the server, save configuration
|
||||
if not ids[id] then
|
||||
ids[id] = {}
|
||||
end
|
||||
|
||||
-- save the setting
|
||||
ids[id][key] = table.pack(...)
|
||||
|
||||
-- broadcast to clients
|
||||
TriggerClientEvent('playernames:configure', -1, id, key, ...)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if IsDuplicityVersion() then
|
||||
function reconfigure(source)
|
||||
for id, data in pairs(ids) do
|
||||
for key, args in pairs(data) do
|
||||
TriggerClientEvent('playernames:configure', source, id, key, table.unpack(args))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
AddEventHandler('playerDropped', function()
|
||||
ids[source] = nil
|
||||
end)
|
||||
end
|
||||
|
||||
setComponentColor = getTriggerFunction('setc')
|
||||
setComponentAlpha = getTriggerFunction('seta')
|
||||
setComponentVisibility = getTriggerFunction('tglc')
|
||||
setWantedLevel = getTriggerFunction('setw')
|
||||
setHealthBarColor = getTriggerFunction('sehc')
|
||||
setNameTemplate = getTriggerFunction('tpl')
|
||||
setName = getTriggerFunction('name')
|
||||
|
||||
if not io then
|
||||
io = { write = nil, open = nil }
|
||||
end
|
||||
|
||||
local template = load(LoadResourceFile(GetCurrentResourceName(), 'template/template.lua'))()
|
||||
|
||||
function formatPlayerNameTag(i, templateStr)
|
||||
--return ('%s <%d>'):format(GetPlayerName(i), GetPlayerServerId(i))
|
||||
local str = ''
|
||||
|
||||
template.print = function(txt)
|
||||
str = str .. txt
|
||||
end
|
||||
|
||||
local context = {
|
||||
name = GetPlayerName(i),
|
||||
i = i,
|
||||
global = _G
|
||||
}
|
||||
|
||||
if IsDuplicityVersion() then
|
||||
context.id = i
|
||||
else
|
||||
context.id = GetPlayerServerId(i)
|
||||
end
|
||||
|
||||
TriggerEvent('playernames:extendContext', i, function(k, v)
|
||||
context[k] = v
|
||||
end)
|
||||
|
||||
template.render(templateStr, context, nil, true)
|
||||
|
||||
template.print = print
|
||||
|
||||
return str
|
||||
end
|
@ -0,0 +1,191 @@
|
||||
local mpGamerTags = {}
|
||||
local mpGamerTagSettings = {}
|
||||
|
||||
local gtComponent = {
|
||||
GAMER_NAME = 0,
|
||||
CREW_TAG = 1,
|
||||
healthArmour = 2,
|
||||
BIG_TEXT = 3,
|
||||
AUDIO_ICON = 4,
|
||||
MP_USING_MENU = 5,
|
||||
MP_PASSIVE_MODE = 6,
|
||||
WANTED_STARS = 7,
|
||||
MP_DRIVER = 8,
|
||||
MP_CO_DRIVER = 9,
|
||||
MP_TAGGED = 10,
|
||||
GAMER_NAME_NEARBY = 11,
|
||||
ARROW = 12,
|
||||
MP_PACKAGES = 13,
|
||||
INV_IF_PED_FOLLOWING = 14,
|
||||
RANK_TEXT = 15,
|
||||
MP_TYPING = 16
|
||||
}
|
||||
|
||||
local function makeSettings()
|
||||
return {
|
||||
alphas = {},
|
||||
colors = {},
|
||||
healthColor = false,
|
||||
toggles = {},
|
||||
wantedLevel = false
|
||||
}
|
||||
end
|
||||
|
||||
local templateStr
|
||||
|
||||
function updatePlayerNames()
|
||||
-- re-run this function the next frame
|
||||
SetTimeout(0, updatePlayerNames)
|
||||
|
||||
-- return if no template string is set
|
||||
if not templateStr then
|
||||
return
|
||||
end
|
||||
|
||||
-- get local coordinates to compare to
|
||||
local localCoords = GetEntityCoords(PlayerPedId())
|
||||
|
||||
-- for each valid player index
|
||||
for _, i in ipairs(GetActivePlayers()) do
|
||||
-- if the player exists
|
||||
if i ~= PlayerId() then
|
||||
-- get their ped
|
||||
local ped = GetPlayerPed(i)
|
||||
local pedCoords = GetEntityCoords(ped)
|
||||
|
||||
-- make a new settings list if needed
|
||||
if not mpGamerTagSettings[i] then
|
||||
mpGamerTagSettings[i] = makeSettings()
|
||||
end
|
||||
|
||||
-- check the ped, because changing player models may recreate the ped
|
||||
-- also check gamer tag activity in case the game deleted the gamer tag
|
||||
if not mpGamerTags[i] or mpGamerTags[i].ped ~= ped or not IsMpGamerTagActive(mpGamerTags[i].tag) then
|
||||
local nameTag = formatPlayerNameTag(i, templateStr)
|
||||
|
||||
-- remove any existing tag
|
||||
if mpGamerTags[i] then
|
||||
RemoveMpGamerTag(mpGamerTags[i].tag)
|
||||
end
|
||||
|
||||
-- store the new tag
|
||||
mpGamerTags[i] = {
|
||||
tag = CreateMpGamerTag(GetPlayerPed(i), nameTag, false, false, '', 0),
|
||||
ped = ped
|
||||
}
|
||||
end
|
||||
|
||||
-- store the tag in a local
|
||||
local tag = mpGamerTags[i].tag
|
||||
|
||||
-- should the player be renamed? this is set by events
|
||||
if mpGamerTagSettings[i].rename then
|
||||
SetMpGamerTagName(tag, formatPlayerNameTag(i, templateStr))
|
||||
mpGamerTagSettings[i].rename = nil
|
||||
end
|
||||
|
||||
-- check distance
|
||||
local distance = #(pedCoords - localCoords)
|
||||
|
||||
-- show/hide based on nearbyness/line-of-sight
|
||||
-- nearby checks are primarily to prevent a lot of LOS checks
|
||||
if distance < 250 and HasEntityClearLosToEntity(PlayerPedId(), ped, 17) then
|
||||
SetMpGamerTagVisibility(tag, gtComponent.GAMER_NAME, true)
|
||||
SetMpGamerTagVisibility(tag, gtComponent.healthArmour, IsPlayerTargettingEntity(PlayerId(), ped))
|
||||
SetMpGamerTagVisibility(tag, gtComponent.AUDIO_ICON, NetworkIsPlayerTalking(i))
|
||||
|
||||
SetMpGamerTagAlpha(tag, gtComponent.AUDIO_ICON, 255)
|
||||
SetMpGamerTagAlpha(tag, gtComponent.healthArmour, 255)
|
||||
|
||||
-- override settings
|
||||
local settings = mpGamerTagSettings[i]
|
||||
|
||||
for k, v in pairs(settings.toggles) do
|
||||
SetMpGamerTagVisibility(tag, gtComponent[k], v)
|
||||
end
|
||||
|
||||
for k, v in pairs(settings.alphas) do
|
||||
SetMpGamerTagAlpha(tag, gtComponent[k], v)
|
||||
end
|
||||
|
||||
for k, v in pairs(settings.colors) do
|
||||
SetMpGamerTagColour(tag, gtComponent[k], v)
|
||||
end
|
||||
|
||||
if settings.wantedLevel then
|
||||
SetMpGamerTagWantedLevel(tag, settings.wantedLevel)
|
||||
end
|
||||
|
||||
if settings.healthColor then
|
||||
SetMpGamerTagHealthBarColour(tag, settings.healthColor)
|
||||
end
|
||||
else
|
||||
SetMpGamerTagVisibility(tag, gtComponent.GAMER_NAME, false)
|
||||
SetMpGamerTagVisibility(tag, gtComponent.healthArmour, false)
|
||||
SetMpGamerTagVisibility(tag, gtComponent.AUDIO_ICON, false)
|
||||
end
|
||||
elseif mpGamerTags[i] then
|
||||
RemoveMpGamerTag(mpGamerTags[i].tag)
|
||||
|
||||
mpGamerTags[i] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function getSettings(id)
|
||||
local i = GetPlayerFromServerId(tonumber(id))
|
||||
|
||||
if not mpGamerTagSettings[i] then
|
||||
mpGamerTagSettings[i] = makeSettings()
|
||||
end
|
||||
|
||||
return mpGamerTagSettings[i]
|
||||
end
|
||||
|
||||
RegisterNetEvent('playernames:configure')
|
||||
|
||||
AddEventHandler('playernames:configure', function(id, key, ...)
|
||||
local args = table.pack(...)
|
||||
|
||||
if key == 'tglc' then
|
||||
getSettings(id).toggles[args[1]] = args[2]
|
||||
elseif key == 'seta' then
|
||||
getSettings(id).alphas[args[1]] = args[2]
|
||||
elseif key == 'setc' then
|
||||
getSettings(id).colors[args[1]] = args[2]
|
||||
elseif key == 'setw' then
|
||||
getSettings(id).wantedLevel = args[1]
|
||||
elseif key == 'sehc' then
|
||||
getSettings(id).healthColor = args[1]
|
||||
elseif key == 'rnme' then
|
||||
getSettings(id).rename = true
|
||||
elseif key == 'name' then
|
||||
getSettings(id).serverName = args[1]
|
||||
getSettings(id).rename = true
|
||||
elseif key == 'tpl' then
|
||||
for _, v in pairs(mpGamerTagSettings) do
|
||||
v.rename = true
|
||||
end
|
||||
|
||||
templateStr = args[1]
|
||||
end
|
||||
end)
|
||||
|
||||
AddEventHandler('playernames:extendContext', function(i, cb)
|
||||
cb('serverName', getSettings(GetPlayerServerId(i)).serverName)
|
||||
end)
|
||||
|
||||
AddEventHandler('onResourceStop', function(name)
|
||||
if name == GetCurrentResourceName() then
|
||||
for _, v in pairs(mpGamerTags) do
|
||||
RemoveMpGamerTag(v.tag)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
SetTimeout(0, function()
|
||||
TriggerServerEvent('playernames:init')
|
||||
end)
|
||||
|
||||
-- run this function every frame
|
||||
SetTimeout(0, updatePlayerNames)
|
@ -0,0 +1,46 @@
|
||||
local curTemplate
|
||||
local curTags = {}
|
||||
|
||||
local activePlayers = {}
|
||||
|
||||
local function detectUpdates()
|
||||
SetTimeout(500, detectUpdates)
|
||||
|
||||
local template = GetConvar('playerNames_template', '[{{id}}] {{name}}')
|
||||
|
||||
if curTemplate ~= template then
|
||||
setNameTemplate(-1, template)
|
||||
|
||||
curTemplate = template
|
||||
end
|
||||
|
||||
template = GetConvar('playerNames_svTemplate', '[{{id}}] {{name}}')
|
||||
|
||||
for v, _ in pairs(activePlayers) do
|
||||
local newTag = formatPlayerNameTag(v, template)
|
||||
if newTag ~= curTags[v] then
|
||||
setName(v, newTag)
|
||||
|
||||
curTags[v] = newTag
|
||||
end
|
||||
end
|
||||
|
||||
for i, tag in pairs(curTags) do
|
||||
if not activePlayers[i] then
|
||||
curTags[i] = nil -- in case curTags doesnt get cleared when the player left, clear it now.
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
AddEventHandler('playerDropped', function()
|
||||
curTags[source] = nil
|
||||
activePlayers[source] = nil
|
||||
end)
|
||||
|
||||
RegisterNetEvent('playernames:init')
|
||||
AddEventHandler('playernames:init', function()
|
||||
reconfigure(source)
|
||||
activePlayers[source] = true
|
||||
end)
|
||||
|
||||
detectUpdates()
|
@ -0,0 +1,27 @@
|
||||
Copyright (c) 2014 - 2017 Aapo Talvensaari
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this
|
||||
list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the {organization} nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
@ -0,0 +1,478 @@
|
||||
local setmetatable = setmetatable
|
||||
local loadstring = loadstring
|
||||
local loadchunk
|
||||
local tostring = tostring
|
||||
local setfenv = setfenv
|
||||
local require = require
|
||||
local capture
|
||||
local concat = table.concat
|
||||
local assert = assert
|
||||
local prefix
|
||||
local write = io.write
|
||||
local pcall = pcall
|
||||
local phase
|
||||
local open = io.open
|
||||
local load = load
|
||||
local type = type
|
||||
local dump = string.dump
|
||||
local find = string.find
|
||||
local gsub = string.gsub
|
||||
local byte = string.byte
|
||||
local null
|
||||
local sub = string.sub
|
||||
local ngx = ngx
|
||||
local jit = jit
|
||||
local var
|
||||
|
||||
local _VERSION = _VERSION
|
||||
local _ENV = _ENV
|
||||
local _G = _G
|
||||
|
||||
local HTML_ENTITIES = {
|
||||
["&"] = "&",
|
||||
["<"] = "<",
|
||||
[">"] = ">",
|
||||
['"'] = """,
|
||||
["'"] = "'",
|
||||
["/"] = "/"
|
||||
}
|
||||
|
||||
local CODE_ENTITIES = {
|
||||
["{"] = "{",
|
||||
["}"] = "}",
|
||||
["&"] = "&",
|
||||
["<"] = "<",
|
||||
[">"] = ">",
|
||||
['"'] = """,
|
||||
["'"] = "'",
|
||||
["/"] = "/"
|
||||
}
|
||||
|
||||
local VAR_PHASES
|
||||
|
||||
local ok, newtab = pcall(require, "table.new")
|
||||
if not ok then newtab = function() return {} end end
|
||||
|
||||
local caching = true
|
||||
local template = newtab(0, 12)
|
||||
|
||||
template._VERSION = "1.9"
|
||||
template.cache = {}
|
||||
|
||||
local function enabled(val)
|
||||
if val == nil then return true end
|
||||
return val == true or (val == "1" or val == "true" or val == "on")
|
||||
end
|
||||
|
||||
local function trim(s)
|
||||
return gsub(gsub(s, "^%s+", ""), "%s+$", "")
|
||||
end
|
||||
|
||||
local function rpos(view, s)
|
||||
while s > 0 do
|
||||
local c = sub(view, s, s)
|
||||
if c == " " or c == "\t" or c == "\0" or c == "\x0B" then
|
||||
s = s - 1
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
return s
|
||||
end
|
||||
|
||||
local function escaped(view, s)
|
||||
if s > 1 and sub(view, s - 1, s - 1) == "\\" then
|
||||
if s > 2 and sub(view, s - 2, s - 2) == "\\" then
|
||||
return false, 1
|
||||
else
|
||||
return true, 1
|
||||
end
|
||||
end
|
||||
return false, 0
|
||||
end
|
||||
|
||||
local function readfile(path)
|
||||
local file = open(path, "rb")
|
||||
if not file then return nil end
|
||||
local content = file:read "*a"
|
||||
file:close()
|
||||
return content
|
||||
end
|
||||
|
||||
local function loadlua(path)
|
||||
return readfile(path) or path
|
||||
end
|
||||
|
||||
local function loadngx(path)
|
||||
local vars = VAR_PHASES[phase()]
|
||||
local file, location = path, vars and var.template_location
|
||||
if sub(file, 1) == "/" then file = sub(file, 2) end
|
||||
if location and location ~= "" then
|
||||
if sub(location, -1) == "/" then location = sub(location, 1, -2) end
|
||||
local res = capture(concat{ location, '/', file})
|
||||
if res.status == 200 then return res.body end
|
||||
end
|
||||
local root = vars and (var.template_root or var.document_root) or prefix
|
||||
if sub(root, -1) == "/" then root = sub(root, 1, -2) end
|
||||
return readfile(concat{ root, "/", file }) or path
|
||||
end
|
||||
|
||||
do
|
||||
if ngx then
|
||||
VAR_PHASES = {
|
||||
set = true,
|
||||
rewrite = true,
|
||||
access = true,
|
||||
content = true,
|
||||
header_filter = true,
|
||||
body_filter = true,
|
||||
log = true
|
||||
}
|
||||
template.print = ngx.print or write
|
||||
template.load = loadngx
|
||||
prefix, var, capture, null, phase = ngx.config.prefix(), ngx.var, ngx.location.capture, ngx.null, ngx.get_phase
|
||||
if VAR_PHASES[phase()] then
|
||||
caching = enabled(var.template_cache)
|
||||
end
|
||||
else
|
||||
template.print = write
|
||||
template.load = loadlua
|
||||
end
|
||||
if _VERSION == "Lua 5.1" then
|
||||
local context = { __index = function(t, k)
|
||||
return t.context[k] or t.template[k] or _G[k]
|
||||
end }
|
||||
if jit then
|
||||
loadchunk = function(view)
|
||||
return assert(load(view, nil, nil, setmetatable({ template = template }, context)))
|
||||
end
|
||||
else
|
||||
loadchunk = function(view)
|
||||
local func = assert(loadstring(view))
|
||||
setfenv(func, setmetatable({ template = template }, context))
|
||||
return func
|
||||
end
|
||||
end
|
||||
else
|
||||
local context = { __index = function(t, k)
|
||||
return t.context[k] or t.template[k] or _ENV[k]
|
||||
end }
|
||||
loadchunk = function(view)
|
||||
return assert(load(view, nil, nil, setmetatable({ template = template }, context)))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function template.caching(enable)
|
||||
if enable ~= nil then caching = enable == true end
|
||||
return caching
|
||||
end
|
||||
|
||||
function template.output(s)
|
||||
if s == nil or s == null then return "" end
|
||||
if type(s) == "function" then return template.output(s()) end
|
||||
return tostring(s)
|
||||
end
|
||||
|
||||
function template.escape(s, c)
|
||||
if type(s) == "string" then
|
||||
if c then return gsub(s, "[}{\">/<'&]", CODE_ENTITIES) end
|
||||
return gsub(s, "[\">/<'&]", HTML_ENTITIES)
|
||||
end
|
||||
return template.output(s)
|
||||
end
|
||||
|
||||
function template.new(view, layout)
|
||||
assert(view, "view was not provided for template.new(view, layout).")
|
||||
local render, compile = template.render, template.compile
|
||||
if layout then
|
||||
if type(layout) == "table" then
|
||||
return setmetatable({ render = function(self, context)
|
||||
local context = context or self
|
||||
context.blocks = context.blocks or {}
|
||||
context.view = compile(view)(context)
|
||||
layout.blocks = context.blocks or {}
|
||||
layout.view = context.view or ""
|
||||
return layout:render()
|
||||
end }, { __tostring = function(self)
|
||||
local context = self
|
||||
context.blocks = context.blocks or {}
|
||||
context.view = compile(view)(context)
|
||||
layout.blocks = context.blocks or {}
|
||||
layout.view = context.view
|
||||
return tostring(layout)
|
||||
end })
|
||||
else
|
||||
return setmetatable({ render = function(self, context)
|
||||
local context = context or self
|
||||
context.blocks = context.blocks or {}
|
||||
context.view = compile(view)(context)
|
||||
return render(layout, context)
|
||||
end }, { __tostring = function(self)
|
||||
local context = self
|
||||
context.blocks = context.blocks or {}
|
||||
context.view = compile(view)(context)
|
||||
return compile(layout)(context)
|
||||
end })
|
||||
end
|
||||
end
|
||||
return setmetatable({ render = function(self, context)
|
||||
return render(view, context or self)
|
||||
end }, { __tostring = function(self)
|
||||
return compile(view)(self)
|
||||
end })
|
||||
end
|
||||
|
||||
function template.precompile(view, path, strip)
|
||||
local chunk = dump(template.compile(view), strip ~= false)
|
||||
if path then
|
||||
local file = open(path, "wb")
|
||||
file:write(chunk)
|
||||
file:close()
|
||||
end
|
||||
return chunk
|
||||
end
|
||||
|
||||
function template.compile(view, key, plain)
|
||||
assert(view, "view was not provided for template.compile(view, key, plain).")
|
||||
if key == "no-cache" then
|
||||
return loadchunk(template.parse(view, plain)), false
|
||||
end
|
||||
key = key or view
|
||||
local cache = template.cache
|
||||
if cache[key] then return cache[key], true end
|
||||
local func = loadchunk(template.parse(view, plain))
|
||||
if caching then cache[key] = func end
|
||||
return func, false
|
||||
end
|
||||
|
||||
function template.parse(view, plain)
|
||||
assert(view, "view was not provided for template.parse(view, plain).")
|
||||
if not plain then
|
||||
view = template.load(view)
|
||||
if byte(view, 1, 1) == 27 then return view end
|
||||
end
|
||||
local j = 2
|
||||
local c = {[[
|
||||
context=... or {}
|
||||
local function include(v, c) return template.compile(v)(c or context) end
|
||||
local ___,blocks,layout={},blocks or {}
|
||||
]] }
|
||||
local i, s = 1, find(view, "{", 1, true)
|
||||
while s do
|
||||
local t, p = sub(view, s + 1, s + 1), s + 2
|
||||
if t == "{" then
|
||||
local e = find(view, "}}", p, true)
|
||||
if e then
|
||||
local z, w = escaped(view, s)
|
||||
if i < s - w then
|
||||
c[j] = "___[#___+1]=[=[\n"
|
||||
c[j+1] = sub(view, i, s - 1 - w)
|
||||
c[j+2] = "]=]\n"
|
||||
j=j+3
|
||||
end
|
||||
if z then
|
||||
i = s
|
||||
else
|
||||
c[j] = "___[#___+1]=template.escape("
|
||||
c[j+1] = trim(sub(view, p, e - 1))
|
||||
c[j+2] = ")\n"
|
||||
j=j+3
|
||||
s, i = e + 1, e + 2
|
||||
end
|
||||
end
|
||||
elseif t == "*" then
|
||||
local e = find(view, "*}", p, true)
|
||||
if e then
|
||||
local z, w = escaped(view, s)
|
||||
if i < s - w then
|
||||
c[j] = "___[#___+1]=[=[\n"
|
||||
c[j+1] = sub(view, i, s - 1 - w)
|
||||
c[j+2] = "]=]\n"
|
||||
j=j+3
|
||||
end
|
||||
if z then
|
||||
i = s
|
||||
else
|
||||
c[j] = "___[#___+1]=template.output("
|
||||
c[j+1] = trim(sub(view, p, e - 1))
|
||||
c[j+2] = ")\n"
|
||||
j=j+3
|
||||
s, i = e + 1, e + 2
|
||||
end
|
||||
end
|
||||
elseif t == "%" then
|
||||
local e = find(view, "%}", p, true)
|
||||
if e then
|
||||
local z, w = escaped(view, s)
|
||||
if z then
|
||||
if i < s - w then
|
||||
c[j] = "___[#___+1]=[=[\n"
|
||||
c[j+1] = sub(view, i, s - 1 - w)
|
||||
c[j+2] = "]=]\n"
|
||||
j=j+3
|
||||
end
|
||||
i = s
|
||||
else
|
||||
local n = e + 2
|
||||
if sub(view, n, n) == "\n" then
|
||||
n = n + 1
|
||||
end
|
||||
local r = rpos(view, s - 1)
|
||||
if i <= r then
|
||||
c[j] = "___[#___+1]=[=[\n"
|
||||
c[j+1] = sub(view, i, r)
|
||||
c[j+2] = "]=]\n"
|
||||
j=j+3
|
||||
end
|
||||
c[j] = trim(sub(view, p, e - 1))
|
||||
c[j+1] = "\n"
|
||||
j=j+2
|
||||
s, i = n - 1, n
|
||||
end
|
||||
end
|
||||
elseif t == "(" then
|
||||
local e = find(view, ")}", p, true)
|
||||
if e then
|
||||
local z, w = escaped(view, s)
|
||||
if i < s - w then
|
||||
c[j] = "___[#___+1]=[=[\n"
|
||||
c[j+1] = sub(view, i, s - 1 - w)
|
||||
c[j+2] = "]=]\n"
|
||||
j=j+3
|
||||
end
|
||||
if z then
|
||||
i = s
|
||||
else
|
||||
local f = sub(view, p, e - 1)
|
||||
local x = find(f, ",", 2, true)
|
||||
if x then
|
||||
c[j] = "___[#___+1]=include([=["
|
||||
c[j+1] = trim(sub(f, 1, x - 1))
|
||||
c[j+2] = "]=],"
|
||||
c[j+3] = trim(sub(f, x + 1))
|
||||
c[j+4] = ")\n"
|
||||
j=j+5
|
||||
else
|
||||
c[j] = "___[#___+1]=include([=["
|
||||
c[j+1] = trim(f)
|
||||
c[j+2] = "]=])\n"
|
||||
j=j+3
|
||||
end
|
||||
s, i = e + 1, e + 2
|
||||
end
|
||||
end
|
||||
elseif t == "[" then
|
||||
local e = find(view, "]}", p, true)
|
||||
if e then
|
||||
local z, w = escaped(view, s)
|
||||
if i < s - w then
|
||||
c[j] = "___[#___+1]=[=[\n"
|
||||
c[j+1] = sub(view, i, s - 1 - w)
|
||||
c[j+2] = "]=]\n"
|
||||
j=j+3
|
||||
end
|
||||
if z then
|
||||
i = s
|
||||
else
|
||||
c[j] = "___[#___+1]=include("
|
||||
c[j+1] = trim(sub(view, p, e - 1))
|
||||
c[j+2] = ")\n"
|
||||
j=j+3
|
||||
s, i = e + 1, e + 2
|
||||
end
|
||||
end
|
||||
elseif t == "-" then
|
||||
local e = find(view, "-}", p, true)
|
||||
if e then
|
||||
local x, y = find(view, sub(view, s, e + 1), e + 2, true)
|
||||
if x then
|
||||
local z, w = escaped(view, s)
|
||||
if z then
|
||||
if i < s - w then
|
||||
c[j] = "___[#___+1]=[=[\n"
|
||||
c[j+1] = sub(view, i, s - 1 - w)
|
||||
c[j+2] = "]=]\n"
|
||||
j=j+3
|
||||
end
|
||||
i = s
|
||||
else
|
||||
y = y + 1
|
||||
x = x - 1
|
||||
if sub(view, y, y) == "\n" then
|
||||
y = y + 1
|
||||
end
|
||||
local b = trim(sub(view, p, e - 1))
|
||||
if b == "verbatim" or b == "raw" then
|
||||
if i < s - w then
|
||||
c[j] = "___[#___+1]=[=[\n"
|
||||
c[j+1] = sub(view, i, s - 1 - w)
|
||||
c[j+2] = "]=]\n"
|
||||
j=j+3
|
||||
end
|
||||
c[j] = "___[#___+1]=[=["
|
||||
c[j+1] = sub(view, e + 2, x)
|
||||
c[j+2] = "]=]\n"
|
||||
j=j+3
|
||||
else
|
||||
if sub(view, x, x) == "\n" then
|
||||
x = x - 1
|
||||
end
|
||||
local r = rpos(view, s - 1)
|
||||
if i <= r then
|
||||
c[j] = "___[#___+1]=[=[\n"
|
||||
c[j+1] = sub(view, i, r)
|
||||
c[j+2] = "]=]\n"
|
||||
j=j+3
|
||||
end
|
||||
c[j] = 'blocks["'
|
||||
c[j+1] = b
|
||||
c[j+2] = '"]=include[=['
|
||||
c[j+3] = sub(view, e + 2, x)
|
||||
c[j+4] = "]=]\n"
|
||||
j=j+5
|
||||
end
|
||||
s, i = y - 1, y
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif t == "#" then
|
||||
local e = find(view, "#}", p, true)
|
||||
if e then
|
||||
local z, w = escaped(view, s)
|
||||
if i < s - w then
|
||||
c[j] = "___[#___+1]=[=[\n"
|
||||
c[j+1] = sub(view, i, s - 1 - w)
|
||||
c[j+2] = "]=]\n"
|
||||
j=j+3
|
||||
end
|
||||
if z then
|
||||
i = s
|
||||
else
|
||||
e = e + 2
|
||||
if sub(view, e, e) == "\n" then
|
||||
e = e + 1
|
||||
end
|
||||
s, i = e - 1, e
|
||||
end
|
||||
end
|
||||
end
|
||||
s = find(view, "{", s + 1, true)
|
||||
end
|
||||
s = sub(view, i)
|
||||
if s and s ~= "" then
|
||||
c[j] = "___[#___+1]=[=[\n"
|
||||
c[j+1] = s
|
||||
c[j+2] = "]=]\n"
|
||||
j=j+3
|
||||
end
|
||||
c[j] = "return layout and include(layout,setmetatable({view=table.concat(___),blocks=blocks},{__index=context})) or table.concat(___)"
|
||||
return concat(c)
|
||||
end
|
||||
|
||||
function template.render(view, context, key, plain)
|
||||
assert(view, "view was not provided for template.render(view, context, key, plain).")
|
||||
return template.print(template.compile(view, key, plain)(context))
|
||||
end
|
||||
|
||||
return template
|
0
resources/[cfx-default]/[local]/.gitkeep
Normal file
0
resources/[cfx-default]/[local]/.gitkeep
Normal file
3
resources/[cfx-default]/[local]/screenshot-basic/.gitignore
vendored
Normal file
3
resources/[cfx-default]/[local]/screenshot-basic/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
yarn-error.log
|
||||
.yarn.installed
|
7
resources/[cfx-default]/[local]/screenshot-basic/LICENSE
Normal file
7
resources/[cfx-default]/[local]/screenshot-basic/LICENSE
Normal file
@ -0,0 +1,7 @@
|
||||
Copyright 2019 The CitizenFX Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
91
resources/[cfx-default]/[local]/screenshot-basic/README.md
Normal file
91
resources/[cfx-default]/[local]/screenshot-basic/README.md
Normal file
@ -0,0 +1,91 @@
|
||||
# screenshot-basic for FiveM
|
||||
|
||||
## Description
|
||||
|
||||
screenshot-basic is a basic resource for making screenshots of clients' game render targets using FiveM. It uses the same backing
|
||||
WebGL/OpenGL ES calls as used by the `application/x-cfx-game-view` plugin (see the code in [citizenfx/fivem](https://github.com/citizenfx/fivem/blob/b0a7cda1007dc53d2ba0f638c035c0a5d1402796/data/client/bin/d3d_rendering.cc#L248)),
|
||||
and wraps these calls using Three.js to 'simplify' WebGL initialization and copying to a buffer from asynchronous NUI.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Make sure your [cfx-server-data](https://github.com/citizenfx/cfx-server-data) is updated as of 2019-01-15 or later. You can easily
|
||||
update it by running `git pull` in your local clone directory.
|
||||
2. Install `screenshot-basic`:
|
||||
```
|
||||
mkdir -p 'resources/[local]/'
|
||||
cd 'resources/[local]'
|
||||
git clone https://github.com/citizenfx/screenshot-basic.git screenshot-basic
|
||||
```
|
||||
3. Make/use a resource that uses it. Currently, there are no directly-usable commands, it is only usable through exports.
|
||||
|
||||
## API
|
||||
|
||||
### Client
|
||||
|
||||
#### requestScreenshot(options?: any, cb: (result: string) => void)
|
||||
Takes a screenshot and passes the data URI to a callback. Please don't send this through _any_ server events.
|
||||
|
||||
Arguments:
|
||||
* **options**: An optional object containing options.
|
||||
* **encoding**: 'png' | 'jpg' | 'webp' - The target image encoding. Defaults to 'jpg'.
|
||||
* **quality**: number - The quality for a lossy image encoder, in a range for 0.0-1.0. Defaults to 0.92.
|
||||
* **cb**: A callback upon result.
|
||||
* **result**: A `base64` data URI for the image.
|
||||
|
||||
Example:
|
||||
|
||||
```lua
|
||||
exports['screenshot-basic']:requestScreenshot(function(data)
|
||||
TriggerEvent('chat:addMessage', { template = '<img src="{0}" style="max-width: 300px;" />', args = { data } })
|
||||
end)
|
||||
```
|
||||
|
||||
#### requestScreenshotUpload(url: string, field: string, options?: any, cb: (result: string) => void)
|
||||
Takes a screenshot and uploads it as a file (`multipart/form-data`) to a remote HTTP URL.
|
||||
|
||||
Arguments:
|
||||
* **url**: The URL to a file upload handler.
|
||||
* **field**: The name for the form field to add the file to.
|
||||
* **options**: An optional object containing options.
|
||||
* **encoding**: 'png' | 'jpg' | 'webp' - The target image encoding. Defaults to 'jpg'.
|
||||
* **quality**: number - The quality for a lossy image encoder, in a range for 0.0-1.0. Defaults to 0.92.
|
||||
* **cb**: A callback upon result.
|
||||
* **result**: The response data for the remote URL.
|
||||
|
||||
Example:
|
||||
|
||||
```lua
|
||||
exports['screenshot-basic']:requestScreenshotUpload('https://wew.wtf/upload.php', 'files[]', function(data)
|
||||
local resp = json.decode(data)
|
||||
TriggerEvent('chat:addMessage', { template = '<img src="{0}" style="max-width: 300px;" />', args = { resp.files[1].url } })
|
||||
end)
|
||||
```
|
||||
|
||||
### Server
|
||||
The server can also request a client to take a screenshot and upload it to a built-in HTTP handler on the server.
|
||||
|
||||
Using this API on the server requires at least FiveM client version 1129160, and server pipeline 1011 or higher.
|
||||
|
||||
#### requestClientScreenshot(player: string | number, options: any, cb: (err: string | boolean, data: string) => void)
|
||||
Requests the specified client to take a screenshot.
|
||||
|
||||
Arguments:
|
||||
* **player**: The target player's player index.
|
||||
* **options**: An object containing options.
|
||||
* **fileName**: string? - The file name on the server to save the image to. If not passed, the callback will get a data URI for the image data.
|
||||
* **encoding**: 'png' | 'jpg' | 'webp' - The target image encoding. Defaults to 'jpg'.
|
||||
* **quality**: number - The quality for a lossy image encoder, in a range for 0.0-1.0. Defaults to 0.92.
|
||||
* **cb**: A callback upon result.
|
||||
* **err**: `false`, or an error string.
|
||||
* **data**: The local file name the upload was saved to, or the data URI for the image.
|
||||
|
||||
|
||||
Example:
|
||||
```lua
|
||||
exports['screenshot-basic']:requestClientScreenshot(GetPlayers()[1], {
|
||||
fileName = 'cache/screenshot.jpg'
|
||||
}, function(err, data)
|
||||
print('err', err)
|
||||
print('data', data)
|
||||
end)
|
||||
```
|
@ -0,0 +1,22 @@
|
||||
module.exports = {
|
||||
entry: './src/client/client.ts',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: [ '.tsx', '.ts', '.js' ]
|
||||
},
|
||||
output: {
|
||||
filename: 'client.js',
|
||||
path: __dirname + '/dist/'
|
||||
},
|
||||
node: {
|
||||
fs: 'empty'
|
||||
}
|
||||
};
|
1
resources/[cfx-default]/[local]/screenshot-basic/dist/client.js
vendored
Normal file
1
resources/[cfx-default]/[local]/screenshot-basic/dist/client.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
!function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t,r){(function(e){const t=e.exports;RegisterNuiCallbackType("screenshot_created");const r={};let n=0;function o(e){const t=n.toString();return r[t]={cb:e},n++,t}on("__cfx_nui:screenshot_created",(e,t)=>{t(!0),void 0!==e.id&&r[e.id]&&(r[e.id].cb(e.data),delete r[e.id])}),t("requestScreenshot",(e,t)=>{const r=void 0!==t?e:{encoding:"jpg"},n=void 0!==t?t:e;r.resultURL=null,r.targetField=null,r.targetURL=`https://${GetCurrentResourceName()}/screenshot_created`,r.correlation=o(n),SendNuiMessage(JSON.stringify({request:r}))}),t("requestScreenshotUpload",(e,t,r,n)=>{const i=void 0!==n?r:{headers:{},encoding:"jpg"},u=void 0!==n?n:r;i.targetURL=e,i.targetField=t,i.resultURL=`https://${GetCurrentResourceName()}/screenshot_created`,i.correlation=o(u),SendNuiMessage(JSON.stringify({request:i}))}),onNet("screenshot_basic:requestScreenshot",(e,t)=>{e.encoding=e.encoding||"jpg",e.targetURL=`http://${GetCurrentServerEndpoint()}${t}`,e.targetField="file",e.resultURL=null,e.correlation=o(()=>{}),SendNuiMessage(JSON.stringify({request:e}))})}).call(this,r(1))},function(e,t){var r;r=function(){return this}();try{r=r||new Function("return this")()}catch(e){"object"==typeof window&&(r=window)}e.exports=r}]);
|
27796
resources/[cfx-default]/[local]/screenshot-basic/dist/server.js
vendored
Normal file
27796
resources/[cfx-default]/[local]/screenshot-basic/dist/server.js
vendored
Normal file
File diff suppressed because one or more lines are too long
21856
resources/[cfx-default]/[local]/screenshot-basic/dist/ui.html
vendored
Normal file
21856
resources/[cfx-default]/[local]/screenshot-basic/dist/ui.html
vendored
Normal file
File diff suppressed because one or more lines are too long
5
resources/[cfx-default]/[local]/screenshot-basic/dist/ui.js
vendored
Normal file
5
resources/[cfx-default]/[local]/screenshot-basic/dist/ui.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -0,0 +1,18 @@
|
||||
fx_version 'bodacious'
|
||||
game 'common'
|
||||
|
||||
client_script 'dist/client.js'
|
||||
server_script 'dist/server.js'
|
||||
|
||||
--dependency 'yarn'
|
||||
--dependency 'webpack'
|
||||
|
||||
--webpack_config 'client.config.js'
|
||||
--webpack_config 'server.config.js'
|
||||
--webpack_config 'ui.config.js'
|
||||
|
||||
files {
|
||||
'dist/ui.html'
|
||||
}
|
||||
|
||||
ui_page 'dist/ui.html'
|
@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "screenshot-basic",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@citizenfx/client": "^1.0.3404-1",
|
||||
"@citizenfx/http-wrapper": "^0.2.2",
|
||||
"@citizenfx/server": "^1.0.3404-1",
|
||||
"@citizenfx/three": "^0.100.0",
|
||||
"@types/koa": "^2.11.6",
|
||||
"@types/koa-router": "^7.4.1",
|
||||
"@types/mv": "^2.1.0",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"html-webpack-inline-source-plugin": "^0.0.10",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"koa": "^2.6.2",
|
||||
"koa-body": "^4.0.6",
|
||||
"koa-router": "^7.4.0",
|
||||
"mv": "^2.1.1",
|
||||
"ts-loader": "^5.3.3",
|
||||
"typescript": "3.2.2",
|
||||
"uuid": "^3.3.2",
|
||||
"webpack": "4.28.4"
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
const webpack = require('webpack');
|
||||
|
||||
module.exports = {
|
||||
entry: './src/server/server.ts',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/
|
||||
}
|
||||
]
|
||||
},
|
||||
// https://github.com/felixge/node-formidable/issues/337#issuecomment-153408479
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({ "global.GENTLY": false })
|
||||
],
|
||||
optimization: {
|
||||
minimize: false
|
||||
},
|
||||
resolve: {
|
||||
extensions: [ '.tsx', '.ts', '.js' ]
|
||||
},
|
||||
output: {
|
||||
filename: 'server.js',
|
||||
path: __dirname + '/dist/'
|
||||
},
|
||||
target: 'node'
|
||||
};
|
@ -0,0 +1,80 @@
|
||||
const exp = (<any>global).exports;
|
||||
|
||||
RegisterNuiCallbackType('screenshot_created');
|
||||
|
||||
class ResultData {
|
||||
cb: (data: string) => void;
|
||||
}
|
||||
|
||||
const results: {[id: string]: ResultData} = {};
|
||||
let correlationId = 0;
|
||||
|
||||
function registerCorrelation(cb: (result: string) => void) {
|
||||
const id = correlationId.toString();
|
||||
|
||||
results[id] = { cb };
|
||||
|
||||
correlationId++;
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
on('__cfx_nui:screenshot_created', (body: any, cb: (arg: any) => void) => {
|
||||
cb(true);
|
||||
|
||||
if (body.id !== undefined && results[body.id]) {
|
||||
results[body.id].cb(body.data);
|
||||
delete results[body.id];
|
||||
}
|
||||
});
|
||||
|
||||
exp('requestScreenshot', (options: any, cb: (result: string) => void) => {
|
||||
const realOptions = (cb !== undefined) ? options : {
|
||||
encoding: 'jpg'
|
||||
};
|
||||
|
||||
const realCb = (cb !== undefined) ? cb : options;
|
||||
|
||||
realOptions.resultURL = null;
|
||||
realOptions.targetField = null;
|
||||
realOptions.targetURL = `http://${GetCurrentResourceName()}/screenshot_created`;
|
||||
|
||||
realOptions.correlation = registerCorrelation(realCb);
|
||||
|
||||
SendNuiMessage(JSON.stringify({
|
||||
request: realOptions
|
||||
}));
|
||||
});
|
||||
|
||||
exp('requestScreenshotUpload', (url: string, field: string, options: any, cb: (result: string) => void) => {
|
||||
const realOptions = (cb !== undefined) ? options : {
|
||||
headers: {},
|
||||
encoding: 'jpg'
|
||||
};
|
||||
|
||||
const realCb = (cb !== undefined) ? cb : options;
|
||||
|
||||
realOptions.targetURL = url;
|
||||
realOptions.targetField = field;
|
||||
realOptions.resultURL = `http://${GetCurrentResourceName()}/screenshot_created`;
|
||||
|
||||
realOptions.correlation = registerCorrelation(realCb);
|
||||
|
||||
SendNuiMessage(JSON.stringify({
|
||||
request: realOptions
|
||||
}));
|
||||
});
|
||||
|
||||
onNet('screenshot_basic:requestScreenshot', (options: any, url: string) => {
|
||||
options.encoding = options.encoding || 'jpg';
|
||||
|
||||
options.targetURL = `http://${GetCurrentServerEndpoint()}${url}`;
|
||||
options.targetField = 'file';
|
||||
options.resultURL = null;
|
||||
|
||||
options.correlation = registerCorrelation(() => {});
|
||||
|
||||
SendNuiMessage(JSON.stringify({
|
||||
request: options
|
||||
}));
|
||||
});
|
@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./",
|
||||
"noImplicitAny": true,
|
||||
"module": "es6",
|
||||
"target": "es6",
|
||||
"allowJs": true,
|
||||
"lib": ["es2017"],
|
||||
"types": ["@citizenfx/server", "@citizenfx/client", "node"],
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": [
|
||||
"./**/*"
|
||||
],
|
||||
"exclude": [
|
||||
|
||||
]
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
import { setHttpCallback } from '@citizenfx/http-wrapper';
|
||||
|
||||
import { v4 } from 'uuid';
|
||||
import * as fs from 'fs';
|
||||
import * as Koa from 'koa';
|
||||
import * as Router from 'koa-router';
|
||||
import * as koaBody from 'koa-body';
|
||||
import * as mv from 'mv';
|
||||
import { File } from 'formidable';
|
||||
|
||||
const app = new Koa();
|
||||
const router = new Router();
|
||||
|
||||
class UploadData {
|
||||
fileName: string;
|
||||
|
||||
cb: (err: string | boolean, data: string) => void;
|
||||
}
|
||||
|
||||
const uploads: { [token: string]: UploadData } = {};
|
||||
|
||||
router.post('/upload/:token', async (ctx) => {
|
||||
const tkn: string = ctx.params['token'];
|
||||
|
||||
ctx.response.append('Access-Control-Allow-Origin', '*');
|
||||
ctx.response.append('Access-Control-Allow-Methods', 'GET, POST');
|
||||
|
||||
if (uploads[tkn] !== undefined) {
|
||||
const upload = uploads[tkn];
|
||||
delete uploads[tkn];
|
||||
|
||||
const finish = (err: string, data: string) => {
|
||||
setImmediate(() => {
|
||||
upload.cb(err || false, data);
|
||||
});
|
||||
}
|
||||
|
||||
const f = ctx.request.files['file'] as File;
|
||||
|
||||
if (f) {
|
||||
if (upload.fileName) {
|
||||
mv(f.path, upload.fileName, (err) => {
|
||||
if (err) {
|
||||
finish(err.message, null);
|
||||
return;
|
||||
}
|
||||
|
||||
finish(null, upload.fileName);
|
||||
});
|
||||
} else {
|
||||
fs.readFile(f.path, (err, data) => {
|
||||
if (err) {
|
||||
finish(err.message, null);
|
||||
return;
|
||||
}
|
||||
|
||||
fs.unlink(f.path, (err) => {
|
||||
finish(null, `data:${f.type};base64,${data.toString('base64')}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ctx.body = { success: true };
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.body = { success: false };
|
||||
});
|
||||
|
||||
app.use(koaBody({
|
||||
patchKoa: true,
|
||||
multipart: true,
|
||||
}))
|
||||
.use(router.routes())
|
||||
.use(router.allowedMethods());
|
||||
|
||||
setHttpCallback(app.callback());
|
||||
|
||||
// Cfx stuff
|
||||
const exp = (<any>global).exports;
|
||||
|
||||
exp('requestClientScreenshot', (player: string | number, options: any, cb: (err: string | boolean, data: string) => void) => {
|
||||
const tkn = v4();
|
||||
|
||||
const fileName = options.fileName;
|
||||
delete options['fileName']; // so the client won't get to know this
|
||||
|
||||
uploads[tkn] = {
|
||||
fileName,
|
||||
cb
|
||||
};
|
||||
|
||||
emitNet('screenshot_basic:requestScreenshot', player, options, `/${GetCurrentResourceName()}/upload/${tkn}`);
|
||||
});
|
@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": { "*": ["types/*"] },
|
||||
"outDir": "./",
|
||||
"noImplicitAny": false,
|
||||
"module": "es6",
|
||||
"target": "es6",
|
||||
"allowJs": true,
|
||||
"lib": ["es2017"],
|
||||
"types": ["@citizenfx/server", "@citizenfx/client", "node"],
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": [
|
||||
"./**/*"
|
||||
],
|
||||
"exclude": [
|
||||
|
||||
]
|
||||
}
|
@ -0,0 +1 @@
|
||||
export function setHttpCallback(requestHandler: any): void;
|
@ -0,0 +1,30 @@
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin');
|
||||
|
||||
module.exports = {
|
||||
entry: './ui/src/main.ts',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
inlineSource: '.(js|css)$',
|
||||
template: './ui/index.html',
|
||||
filename: 'ui.html'
|
||||
}),
|
||||
new HtmlWebpackInlineSourcePlugin()
|
||||
],
|
||||
resolve: {
|
||||
extensions: [ '.ts', '.js' ]
|
||||
},
|
||||
output: {
|
||||
filename: 'ui.js',
|
||||
path: __dirname + '/dist/'
|
||||
},
|
||||
};
|
@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Screenshot Helper</title>
|
||||
|
||||
<style type="text/css">
|
||||
* {
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
225
resources/[cfx-default]/[local]/screenshot-basic/ui/src/main.ts
Normal file
225
resources/[cfx-default]/[local]/screenshot-basic/ui/src/main.ts
Normal file
@ -0,0 +1,225 @@
|
||||
import {
|
||||
OrthographicCamera,
|
||||
Scene,
|
||||
WebGLRenderTarget,
|
||||
LinearFilter,
|
||||
NearestFilter,
|
||||
RGBAFormat,
|
||||
UnsignedByteType,
|
||||
CfxTexture,
|
||||
ShaderMaterial,
|
||||
PlaneBufferGeometry,
|
||||
Mesh,
|
||||
WebGLRenderer
|
||||
} from '@citizenfx/three';
|
||||
|
||||
class ScreenshotRequest {
|
||||
encoding: 'jpg' | 'png' | 'webp';
|
||||
quality: number;
|
||||
headers: any;
|
||||
|
||||
correlation: string;
|
||||
|
||||
resultURL: string;
|
||||
|
||||
targetURL: string;
|
||||
targetField: string;
|
||||
}
|
||||
|
||||
// from https://stackoverflow.com/a/12300351
|
||||
function dataURItoBlob(dataURI: string) {
|
||||
const byteString = atob(dataURI.split(',')[1]);
|
||||
const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]
|
||||
|
||||
const ab = new ArrayBuffer(byteString.length);
|
||||
const ia = new Uint8Array(ab);
|
||||
|
||||
for (let i = 0; i < byteString.length; i++) {
|
||||
ia[i] = byteString.charCodeAt(i);
|
||||
}
|
||||
|
||||
const blob = new Blob([ab], {type: mimeString});
|
||||
return blob;
|
||||
}
|
||||
|
||||
class ScreenshotUI {
|
||||
renderer: any;
|
||||
rtTexture: any;
|
||||
sceneRTT: any;
|
||||
cameraRTT: any;
|
||||
material: any;
|
||||
request: ScreenshotRequest;
|
||||
|
||||
initialize() {
|
||||
window.addEventListener('message', event => {
|
||||
this.request = event.data.request;
|
||||
});
|
||||
|
||||
window.addEventListener('resize', event => {
|
||||
this.resize();
|
||||
});
|
||||
|
||||
const cameraRTT: any = new OrthographicCamera( window.innerWidth / -2, window.innerWidth / 2, window.innerHeight / 2, window.innerHeight / -2, -10000, 10000 );
|
||||
cameraRTT.position.z = 100;
|
||||
|
||||
const sceneRTT: any = new Scene();
|
||||
|
||||
const rtTexture = new WebGLRenderTarget( window.innerWidth, window.innerHeight, { minFilter: LinearFilter, magFilter: NearestFilter, format: RGBAFormat, type: UnsignedByteType } );
|
||||
const gameTexture: any = new CfxTexture( );
|
||||
gameTexture.needsUpdate = true;
|
||||
|
||||
const material = new ShaderMaterial( {
|
||||
|
||||
uniforms: { "tDiffuse": { value: gameTexture } },
|
||||
vertexShader: `
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vUv = vec2(uv.x, 1.0-uv.y); // fuck gl uv coords
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
varying vec2 vUv;
|
||||
uniform sampler2D tDiffuse;
|
||||
|
||||
void main() {
|
||||
gl_FragColor = texture2D( tDiffuse, vUv );
|
||||
}
|
||||
`
|
||||
|
||||
} );
|
||||
|
||||
this.material = material;
|
||||
|
||||
const plane = new PlaneBufferGeometry( window.innerWidth, window.innerHeight );
|
||||
const quad: any = new Mesh( plane, material );
|
||||
quad.position.z = -100;
|
||||
sceneRTT.add( quad );
|
||||
|
||||
const renderer = new WebGLRenderer();
|
||||
renderer.setPixelRatio( window.devicePixelRatio );
|
||||
renderer.setSize( window.innerWidth, window.innerHeight );
|
||||
renderer.autoClear = false;
|
||||
|
||||
document.getElementById('app').appendChild(renderer.domElement);
|
||||
document.getElementById('app').style.display = 'none';
|
||||
|
||||
this.renderer = renderer;
|
||||
this.rtTexture = rtTexture;
|
||||
this.sceneRTT = sceneRTT;
|
||||
this.cameraRTT = cameraRTT;
|
||||
|
||||
this.animate = this.animate.bind(this);
|
||||
|
||||
requestAnimationFrame(this.animate);
|
||||
}
|
||||
|
||||
resize() {
|
||||
const cameraRTT: any = new OrthographicCamera( window.innerWidth / -2, window.innerWidth / 2, window.innerHeight / 2, window.innerHeight / -2, -10000, 10000 );
|
||||
cameraRTT.position.z = 100;
|
||||
|
||||
this.cameraRTT = cameraRTT;
|
||||
|
||||
const sceneRTT: any = new Scene();
|
||||
|
||||
const plane = new PlaneBufferGeometry( window.innerWidth, window.innerHeight );
|
||||
const quad: any = new Mesh( plane, this.material );
|
||||
quad.position.z = -100;
|
||||
sceneRTT.add( quad );
|
||||
|
||||
this.sceneRTT = sceneRTT;
|
||||
|
||||
this.rtTexture = new WebGLRenderTarget( window.innerWidth, window.innerHeight, { minFilter: LinearFilter, magFilter: NearestFilter, format: RGBAFormat, type: UnsignedByteType } );
|
||||
|
||||
this.renderer.setSize( window.innerWidth, window.innerHeight );
|
||||
}
|
||||
|
||||
animate() {
|
||||
requestAnimationFrame(this.animate);
|
||||
|
||||
this.renderer.clear();
|
||||
this.renderer.render(this.sceneRTT, this.cameraRTT, this.rtTexture, true);
|
||||
|
||||
if (this.request) {
|
||||
const request = this.request;
|
||||
this.request = null;
|
||||
|
||||
this.handleRequest(request);
|
||||
}
|
||||
}
|
||||
|
||||
handleRequest(request: ScreenshotRequest) {
|
||||
// read the screenshot
|
||||
const read = new Uint8Array(window.innerWidth * window.innerHeight * 4);
|
||||
this.renderer.readRenderTargetPixels(this.rtTexture, 0, 0, window.innerWidth, window.innerHeight, read);
|
||||
|
||||
// create a temporary canvas to compress the image
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.style.display = 'inline';
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
|
||||
// draw the image on the canvas
|
||||
const d = new Uint8ClampedArray(read.buffer);
|
||||
|
||||
const cxt = canvas.getContext('2d');
|
||||
cxt.putImageData(new ImageData(d, window.innerWidth, window.innerHeight), 0, 0);
|
||||
|
||||
// encode the image
|
||||
let type = 'image/png';
|
||||
|
||||
switch (request.encoding) {
|
||||
case 'jpg':
|
||||
type = 'image/jpeg';
|
||||
break;
|
||||
case 'png':
|
||||
type = 'image/png';
|
||||
break;
|
||||
case 'webp':
|
||||
type = 'image/webp';
|
||||
break;
|
||||
}
|
||||
|
||||
if (!request.quality) {
|
||||
request.quality = 0.92;
|
||||
}
|
||||
|
||||
// actual encoding
|
||||
const imageURL = canvas.toDataURL(type, request.quality);
|
||||
|
||||
const getFormData = () => {
|
||||
const formData = new FormData();
|
||||
formData.append(request.targetField, dataURItoBlob(imageURL), `screenshot.${request.encoding}`);
|
||||
|
||||
return formData;
|
||||
};
|
||||
|
||||
// upload the image somewhere
|
||||
fetch(request.targetURL, {
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
headers: request.headers,
|
||||
body: (request.targetField) ? getFormData() : JSON.stringify({
|
||||
data: imageURL,
|
||||
id: request.correlation
|
||||
})
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(text => {
|
||||
if (request.resultURL) {
|
||||
fetch(request.resultURL, {
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
body: JSON.stringify({
|
||||
data: text,
|
||||
id: request.correlation
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const ui = new ScreenshotUI();
|
||||
ui.initialize();
|
@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./",
|
||||
"noImplicitAny": false,
|
||||
"module": "es6",
|
||||
"moduleResolution": "node",
|
||||
"target": "es6",
|
||||
"allowJs": true,
|
||||
"lib": [
|
||||
"es2016",
|
||||
"dom"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./**/*"
|
||||
],
|
||||
"exclude": []
|
||||
}
|
3391
resources/[cfx-default]/[local]/screenshot-basic/yarn.lock
Normal file
3391
resources/[cfx-default]/[local]/screenshot-basic/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
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 'cerulean'
|
||||
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 'cerulean'
|
||||
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)
|
2
resources/[cfx-default]/[system]/[builders]/webpack/.gitignore
vendored
Normal file
2
resources/[cfx-default]/[system]/[builders]/webpack/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
.yarn.installed
|
||||
node_modules/
|
@ -0,0 +1,13 @@
|
||||
-- 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 'Builds resources with webpack. To learn more: https://webpack.js.org'
|
||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||
|
||||
dependency 'yarn'
|
||||
server_script 'webpack_builder.js'
|
||||
|
||||
fx_version 'cerulean'
|
||||
game 'common'
|
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "webpack-builder",
|
||||
"version": "1.0.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"async": "^3.1.0",
|
||||
"webpack": "^4.41.2",
|
||||
"worker-farm": "^1.7.0"
|
||||
}
|
||||
}
|
@ -0,0 +1,173 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const workerFarm = require('worker-farm');
|
||||
const async = require('async');
|
||||
let buildingInProgress = false;
|
||||
let currentBuildingModule = '';
|
||||
|
||||
// some modules will not like the custom stack trace logic
|
||||
const ops = Error.prepareStackTrace;
|
||||
Error.prepareStackTrace = undefined;
|
||||
|
||||
const webpackBuildTask = {
|
||||
shouldBuild(resourceName) {
|
||||
const numMetaData = GetNumResourceMetadata(resourceName, 'webpack_config');
|
||||
|
||||
if (numMetaData > 0) {
|
||||
for (let i = 0; i < numMetaData; i++) {
|
||||
const configName = GetResourceMetadata(resourceName, 'webpack_config');
|
||||
|
||||
if (shouldBuild(configName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
function loadCache(config) {
|
||||
const cachePath = `cache/${resourceName}/${config.replace(/\//g, '_')}.json`;
|
||||
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(cachePath, {encoding: 'utf8'}));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldBuild(config) {
|
||||
const cache = loadCache(config);
|
||||
|
||||
if (!cache) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const file of cache) {
|
||||
const stats = getStat(file.name);
|
||||
|
||||
if (!stats ||
|
||||
stats.mtime !== file.stats.mtime ||
|
||||
stats.size !== file.stats.size ||
|
||||
stats.inode !== file.stats.inode) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getStat(path) {
|
||||
try {
|
||||
const stat = fs.statSync(path);
|
||||
|
||||
return stat ? {
|
||||
mtime: stat.mtimeMs,
|
||||
size: stat.size,
|
||||
inode: stat.ino,
|
||||
} : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
build(resourceName, cb) {
|
||||
let buildWebpack = async () => {
|
||||
let error = null;
|
||||
const configs = [];
|
||||
const promises = [];
|
||||
const numMetaData = GetNumResourceMetadata(resourceName, 'webpack_config');
|
||||
|
||||
for (let i = 0; i < numMetaData; i++) {
|
||||
configs.push(GetResourceMetadata(resourceName, 'webpack_config', i));
|
||||
}
|
||||
|
||||
for (const configName of configs) {
|
||||
const configPath = GetResourcePath(resourceName) + '/' + configName;
|
||||
|
||||
const cachePath = `cache/${resourceName}/${configName.replace(/\//g, '_')}.json`;
|
||||
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(cachePath));
|
||||
} catch {
|
||||
}
|
||||
|
||||
const config = require(configPath);
|
||||
|
||||
const workers = workerFarm(require.resolve('./webpack_runner'));
|
||||
|
||||
if (config) {
|
||||
const resourcePath = path.resolve(GetResourcePath(resourceName));
|
||||
|
||||
while (buildingInProgress) {
|
||||
console.log(`webpack is busy: we are waiting to compile ${resourceName} (${configName})`);
|
||||
await sleep(3000);
|
||||
}
|
||||
|
||||
console.log(`${resourceName}: started building ${configName}`);
|
||||
|
||||
buildingInProgress = true;
|
||||
currentBuildingModule = resourceName;
|
||||
|
||||
promises.push(new Promise((resolve, reject) => {
|
||||
workers({
|
||||
configPath,
|
||||
resourcePath,
|
||||
cachePath
|
||||
}, (err, outp) => {
|
||||
workerFarm.end(workers);
|
||||
|
||||
if (err) {
|
||||
console.error(err.stack || err);
|
||||
if (err.details) {
|
||||
console.error(err.details);
|
||||
}
|
||||
|
||||
buildingInProgress = false;
|
||||
currentBuildingModule = '';
|
||||
currentBuildingScript = '';
|
||||
reject("worker farm webpack errored out");
|
||||
return;
|
||||
}
|
||||
|
||||
if (outp.errors) {
|
||||
for (const error of outp.errors) {
|
||||
console.log(error);
|
||||
}
|
||||
buildingInProgress = false;
|
||||
currentBuildingModule = '';
|
||||
currentBuildingScript = '';
|
||||
reject("webpack got an error");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`${resourceName}: built ${configName}`);
|
||||
buildingInProgress = false;
|
||||
resolve();
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
} catch (e) {
|
||||
error = e.toString();
|
||||
}
|
||||
|
||||
buildingInProgress = false;
|
||||
currentBuildingModule = '';
|
||||
|
||||
if (error) {
|
||||
cb(false, error);
|
||||
} else cb(true);
|
||||
};
|
||||
buildWebpack().then();
|
||||
}
|
||||
};
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
RegisterResourceBuildTaskFactory('z_webpack', () => webpackBuildTask);
|
@ -0,0 +1,75 @@
|
||||
const webpack = require('webpack');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
function getStat(path) {
|
||||
try {
|
||||
const stat = fs.statSync(path);
|
||||
|
||||
return stat ? {
|
||||
mtime: stat.mtimeMs,
|
||||
size: stat.size,
|
||||
inode: stat.ino,
|
||||
} : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class SaveStatePlugin {
|
||||
constructor(inp) {
|
||||
this.cache = [];
|
||||
this.cachePath = inp.cachePath;
|
||||
}
|
||||
|
||||
apply(compiler) {
|
||||
compiler.hooks.afterCompile.tap('SaveStatePlugin', (compilation) => {
|
||||
for (const file of compilation.fileDependencies) {
|
||||
this.cache.push({
|
||||
name: file,
|
||||
stats: getStat(file)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
compiler.hooks.done.tap('SaveStatePlugin', (stats) => {
|
||||
if (stats.hasErrors()) {
|
||||
return;
|
||||
}
|
||||
|
||||
fs.writeFile(this.cachePath, JSON.stringify(this.cache), () => {
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = (inp, callback) => {
|
||||
const config = require(inp.configPath);
|
||||
|
||||
config.context = inp.resourcePath;
|
||||
|
||||
if (config.output && config.output.path) {
|
||||
config.output.path = path.resolve(inp.resourcePath, config.output.path);
|
||||
}
|
||||
|
||||
if (!config.plugins) {
|
||||
config.plugins = [];
|
||||
}
|
||||
|
||||
config.plugins.push(new SaveStatePlugin(inp));
|
||||
|
||||
webpack(config, (err, stats) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (stats.hasErrors()) {
|
||||
callback(null, stats.toJson());
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, {});
|
||||
});
|
||||
};
|
2325
resources/[cfx-default]/[system]/[builders]/webpack/yarn.lock
Normal file
2325
resources/[cfx-default]/[system]/[builders]/webpack/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,12 @@
|
||||
-- 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 'Builds resources with yarn. To learn more: https://classic.yarnpkg.com'
|
||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||
|
||||
fx_version 'cerulean'
|
||||
game 'common'
|
||||
|
||||
server_script 'yarn_builder.js'
|
@ -0,0 +1,81 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const child_process = require('child_process');
|
||||
let buildingInProgress = false;
|
||||
let currentBuildingModule = '';
|
||||
|
||||
const initCwd = process.cwd();
|
||||
const trimOutput = (data) => {
|
||||
return `[yarn]\t` + data.toString().replace(/\s+$/, '');
|
||||
}
|
||||
|
||||
const yarnBuildTask = {
|
||||
shouldBuild(resourceName) {
|
||||
try {
|
||||
const resourcePath = GetResourcePath(resourceName);
|
||||
|
||||
const packageJson = path.resolve(resourcePath, 'package.json');
|
||||
const yarnLock = path.resolve(resourcePath, '.yarn.installed');
|
||||
|
||||
const packageStat = fs.statSync(packageJson);
|
||||
|
||||
try {
|
||||
const yarnStat = fs.statSync(yarnLock);
|
||||
|
||||
if (packageStat.mtimeMs > yarnStat.mtimeMs) {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
// no yarn.installed, but package.json - install time!
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
build(resourceName, cb) {
|
||||
(async () => {
|
||||
while (buildingInProgress && currentBuildingModule !== resourceName) {
|
||||
console.log(`yarn is currently busy: we are waiting to compile ${resourceName}`);
|
||||
await sleep(3000);
|
||||
}
|
||||
buildingInProgress = true;
|
||||
currentBuildingModule = resourceName;
|
||||
const proc = child_process.fork(
|
||||
require.resolve('./yarn_cli.js'),
|
||||
['install', '--ignore-scripts', '--cache-folder', path.join(initCwd, 'cache', 'yarn-cache'), '--mutex', 'file:' + path.join(initCwd, 'cache', 'yarn-mutex')],
|
||||
{
|
||||
cwd: path.resolve(GetResourcePath(resourceName)),
|
||||
stdio: 'pipe',
|
||||
});
|
||||
proc.stdout.on('data', (data) => console.log(trimOutput(data)));
|
||||
proc.stderr.on('data', (data) => console.error(trimOutput(data)));
|
||||
proc.on('exit', (code, signal) => {
|
||||
setImmediate(() => {
|
||||
if (code != 0 || signal) {
|
||||
buildingInProgress = false;
|
||||
currentBuildingModule = '';
|
||||
cb(false, 'yarn failed!');
|
||||
return;
|
||||
}
|
||||
|
||||
const resourcePath = GetResourcePath(resourceName);
|
||||
const yarnLock = path.resolve(resourcePath, '.yarn.installed');
|
||||
fs.writeFileSync(yarnLock, '');
|
||||
|
||||
buildingInProgress = false;
|
||||
currentBuildingModule = '';
|
||||
cb(true);
|
||||
});
|
||||
});
|
||||
})();
|
||||
}
|
||||
};
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
RegisterResourceBuildTaskFactory('yarn', () => yarnBuildTask);
|
147392
resources/[cfx-default]/[system]/[builders]/yarn/yarn_cli.js
Normal file
147392
resources/[cfx-default]/[system]/[builders]/yarn/yarn_cli.js
Normal file
File diff suppressed because one or more lines are too long
73
resources/[cfx-default]/[system]/baseevents/deathevents.lua
Normal file
73
resources/[cfx-default]/[system]/baseevents/deathevents.lua
Normal file
@ -0,0 +1,73 @@
|
||||
Citizen.CreateThread(function()
|
||||
local isDead = false
|
||||
local hasBeenDead = false
|
||||
local diedAt
|
||||
|
||||
while true do
|
||||
Wait(0)
|
||||
|
||||
local player = PlayerId()
|
||||
|
||||
if NetworkIsPlayerActive(player) then
|
||||
local ped = PlayerPedId()
|
||||
|
||||
if IsPedFatallyInjured(ped) and not isDead then
|
||||
isDead = true
|
||||
if not diedAt then
|
||||
diedAt = GetGameTimer()
|
||||
end
|
||||
|
||||
local killer, killerweapon = NetworkGetEntityKillerOfPlayer(player)
|
||||
local killerentitytype = GetEntityType(killer)
|
||||
local killertype = -1
|
||||
local killerinvehicle = false
|
||||
local killervehiclename = ''
|
||||
local killervehicleseat = 0
|
||||
if killerentitytype == 1 then
|
||||
killertype = GetPedType(killer)
|
||||
if IsPedInAnyVehicle(killer, false) == 1 then
|
||||
killerinvehicle = true
|
||||
killervehiclename = GetDisplayNameFromVehicleModel(GetEntityModel(GetVehiclePedIsUsing(killer)))
|
||||
killervehicleseat = GetPedVehicleSeat(killer)
|
||||
else killerinvehicle = false
|
||||
end
|
||||
end
|
||||
|
||||
local killerid = GetPlayerByEntityID(killer)
|
||||
if killer ~= ped and killerid ~= nil and NetworkIsPlayerActive(killerid) then killerid = GetPlayerServerId(killerid)
|
||||
else killerid = -1
|
||||
end
|
||||
|
||||
if killer == ped or killer == -1 then
|
||||
TriggerEvent('baseevents:onPlayerDied', killertype, { table.unpack(GetEntityCoords(ped)) })
|
||||
TriggerServerEvent('baseevents:onPlayerDied', killertype, { table.unpack(GetEntityCoords(ped)) })
|
||||
hasBeenDead = true
|
||||
else
|
||||
TriggerEvent('baseevents:onPlayerKilled', killerid, {killertype=killertype, weaponhash = killerweapon, killerinveh=killerinvehicle, killervehseat=killervehicleseat, killervehname=killervehiclename, killerpos={table.unpack(GetEntityCoords(ped))}})
|
||||
TriggerServerEvent('baseevents:onPlayerKilled', killerid, {killertype=killertype, weaponhash = killerweapon, killerinveh=killerinvehicle, killervehseat=killervehicleseat, killervehname=killervehiclename, killerpos={table.unpack(GetEntityCoords(ped))}})
|
||||
hasBeenDead = true
|
||||
end
|
||||
elseif not IsPedFatallyInjured(ped) then
|
||||
isDead = false
|
||||
diedAt = nil
|
||||
end
|
||||
|
||||
-- check if the player has to respawn in order to trigger an event
|
||||
if not hasBeenDead and diedAt ~= nil and diedAt > 0 then
|
||||
TriggerEvent('baseevents:onPlayerWasted', { table.unpack(GetEntityCoords(ped)) })
|
||||
TriggerServerEvent('baseevents:onPlayerWasted', { table.unpack(GetEntityCoords(ped)) })
|
||||
|
||||
hasBeenDead = true
|
||||
elseif hasBeenDead and diedAt ~= nil and diedAt <= 0 then
|
||||
hasBeenDead = false
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
function GetPlayerByEntityID(id)
|
||||
for i=0,32 do
|
||||
if(NetworkIsPlayerActive(i) and GetPlayerPed(i) == id) then return i end
|
||||
end
|
||||
return nil
|
||||
end
|
14
resources/[cfx-default]/[system]/baseevents/fxmanifest.lua
Normal file
14
resources/[cfx-default]/[system]/baseevents/fxmanifest.lua
Normal file
@ -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 'Adds basic events for developers to use in their scripts. Some third party resources may depend on this resource.'
|
||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||
|
||||
client_script 'deathevents.lua'
|
||||
client_script 'vehiclechecker.lua'
|
||||
server_script 'server.lua'
|
||||
|
||||
fx_version 'cerulean'
|
||||
game 'gta5'
|
19
resources/[cfx-default]/[system]/baseevents/server.lua
Normal file
19
resources/[cfx-default]/[system]/baseevents/server.lua
Normal file
@ -0,0 +1,19 @@
|
||||
RegisterServerEvent('baseevents:onPlayerDied')
|
||||
RegisterServerEvent('baseevents:onPlayerKilled')
|
||||
RegisterServerEvent('baseevents:onPlayerWasted')
|
||||
RegisterServerEvent('baseevents:enteringVehicle')
|
||||
RegisterServerEvent('baseevents:enteringAborted')
|
||||
RegisterServerEvent('baseevents:enteredVehicle')
|
||||
RegisterServerEvent('baseevents:leftVehicle')
|
||||
|
||||
AddEventHandler('baseevents:onPlayerKilled', function(killedBy, data)
|
||||
local victim = source
|
||||
|
||||
RconLog({msgType = 'playerKilled', victim = victim, attacker = killedBy, data = data})
|
||||
end)
|
||||
|
||||
AddEventHandler('baseevents:onPlayerDied', function(killedBy, pos)
|
||||
local victim = source
|
||||
|
||||
RconLog({msgType = 'playerDied', victim = victim, attackerType = killedBy, pos = pos})
|
||||
end)
|
@ -0,0 +1,57 @@
|
||||
local isInVehicle = false
|
||||
local isEnteringVehicle = false
|
||||
local currentVehicle = 0
|
||||
local currentSeat = 0
|
||||
|
||||
Citizen.CreateThread(function()
|
||||
while true do
|
||||
Citizen.Wait(0)
|
||||
|
||||
local ped = PlayerPedId()
|
||||
|
||||
if not isInVehicle and not IsPlayerDead(PlayerId()) then
|
||||
if DoesEntityExist(GetVehiclePedIsTryingToEnter(ped)) and not isEnteringVehicle then
|
||||
-- trying to enter a vehicle!
|
||||
local vehicle = GetVehiclePedIsTryingToEnter(ped)
|
||||
local seat = GetSeatPedIsTryingToEnter(ped)
|
||||
local netId = VehToNet(vehicle)
|
||||
isEnteringVehicle = true
|
||||
TriggerServerEvent('baseevents:enteringVehicle', vehicle, seat, GetDisplayNameFromVehicleModel(GetEntityModel(vehicle)), netId)
|
||||
elseif not DoesEntityExist(GetVehiclePedIsTryingToEnter(ped)) and not IsPedInAnyVehicle(ped, true) and isEnteringVehicle then
|
||||
-- vehicle entering aborted
|
||||
TriggerServerEvent('baseevents:enteringAborted')
|
||||
isEnteringVehicle = false
|
||||
elseif IsPedInAnyVehicle(ped, false) then
|
||||
-- suddenly appeared in a vehicle, possible teleport
|
||||
isEnteringVehicle = false
|
||||
isInVehicle = true
|
||||
currentVehicle = GetVehiclePedIsUsing(ped)
|
||||
currentSeat = GetPedVehicleSeat(ped)
|
||||
local model = GetEntityModel(currentVehicle)
|
||||
local name = GetDisplayNameFromVehicleModel()
|
||||
local netId = VehToNet(currentVehicle)
|
||||
TriggerServerEvent('baseevents:enteredVehicle', currentVehicle, currentSeat, GetDisplayNameFromVehicleModel(GetEntityModel(currentVehicle)), netId)
|
||||
end
|
||||
elseif isInVehicle then
|
||||
if not IsPedInAnyVehicle(ped, false) or IsPlayerDead(PlayerId()) then
|
||||
-- bye, vehicle
|
||||
local model = GetEntityModel(currentVehicle)
|
||||
local name = GetDisplayNameFromVehicleModel()
|
||||
local netId = VehToNet(currentVehicle)
|
||||
TriggerServerEvent('baseevents:leftVehicle', currentVehicle, currentSeat, GetDisplayNameFromVehicleModel(GetEntityModel(currentVehicle)), netId)
|
||||
isInVehicle = false
|
||||
currentVehicle = 0
|
||||
currentSeat = 0
|
||||
end
|
||||
end
|
||||
Citizen.Wait(50)
|
||||
end
|
||||
end)
|
||||
|
||||
function GetPedVehicleSeat(ped)
|
||||
local vehicle = GetVehiclePedIsIn(ped, false)
|
||||
for i=-2,GetVehicleMaxNumberOfPassengers(vehicle) do
|
||||
if(GetPedInVehicleSeat(vehicle, i) == ped) then return i end
|
||||
end
|
||||
return -2
|
||||
end
|
11
resources/[cfx-default]/[system]/hardcap/client.lua
Normal file
11
resources/[cfx-default]/[system]/hardcap/client.lua
Normal file
@ -0,0 +1,11 @@
|
||||
Citizen.CreateThread(function()
|
||||
while true do
|
||||
Wait(0)
|
||||
|
||||
if NetworkIsSessionStarted() then
|
||||
TriggerServerEvent('hardcap:playerActivated')
|
||||
|
||||
return
|
||||
end
|
||||
end
|
||||
end)
|
14
resources/[cfx-default]/[system]/hardcap/fxmanifest.lua
Normal file
14
resources/[cfx-default]/[system]/hardcap/fxmanifest.lua
Normal file
@ -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 'Limits the number of players to the amount set by sv_maxclients in your server.cfg.'
|
||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||
|
||||
client_script 'client.lua'
|
||||
server_script 'server.lua'
|
||||
|
||||
fx_version 'cerulean'
|
||||
games { 'gta5', 'rdr3' }
|
||||
rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.'
|
31
resources/[cfx-default]/[system]/hardcap/server.lua
Normal file
31
resources/[cfx-default]/[system]/hardcap/server.lua
Normal file
@ -0,0 +1,31 @@
|
||||
local playerCount = 0
|
||||
local list = {}
|
||||
|
||||
RegisterServerEvent('hardcap:playerActivated')
|
||||
|
||||
AddEventHandler('hardcap:playerActivated', function()
|
||||
if not list[source] then
|
||||
playerCount = playerCount + 1
|
||||
list[source] = true
|
||||
end
|
||||
end)
|
||||
|
||||
AddEventHandler('playerDropped', function()
|
||||
if list[source] then
|
||||
playerCount = playerCount - 1
|
||||
list[source] = nil
|
||||
end
|
||||
end)
|
||||
|
||||
AddEventHandler('playerConnecting', function(name, setReason)
|
||||
local cv = GetConvarInt('sv_maxclients', 32)
|
||||
|
||||
print('Connecting: ' .. name .. '^7')
|
||||
|
||||
if playerCount >= cv then
|
||||
print('Full. :(')
|
||||
|
||||
setReason('This server is full (past ' .. tostring(cv) .. ' players).')
|
||||
CancelEvent()
|
||||
end
|
||||
end)
|
15
resources/[cfx-default]/[system]/rconlog/fxmanifest.lua
Normal file
15
resources/[cfx-default]/[system]/rconlog/fxmanifest.lua
Normal file
@ -0,0 +1,15 @@
|
||||
-- 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 old-style server player management commands.'
|
||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||
|
||||
client_script 'rconlog_client.lua'
|
||||
server_script 'rconlog_server.lua'
|
||||
|
||||
fx_version 'cerulean'
|
||||
games { 'gta5', 'rdr3' }
|
||||
|
||||
rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.'
|
25
resources/[cfx-default]/[system]/rconlog/rconlog_client.lua
Normal file
25
resources/[cfx-default]/[system]/rconlog/rconlog_client.lua
Normal file
@ -0,0 +1,25 @@
|
||||
RegisterNetEvent('rlUpdateNames')
|
||||
|
||||
AddEventHandler('rlUpdateNames', function()
|
||||
local names = {}
|
||||
|
||||
for i = 0, 31 do
|
||||
if NetworkIsPlayerActive(i) then
|
||||
names[GetPlayerServerId(i)] = { id = i, name = GetPlayerName(i) }
|
||||
end
|
||||
end
|
||||
|
||||
TriggerServerEvent('rlUpdateNamesResult', names)
|
||||
end)
|
||||
|
||||
Citizen.CreateThread(function()
|
||||
while true do
|
||||
Wait(0)
|
||||
|
||||
if NetworkIsSessionStarted() then
|
||||
TriggerServerEvent('rlPlayerActivated')
|
||||
|
||||
return
|
||||
end
|
||||
end
|
||||
end)
|
84
resources/[cfx-default]/[system]/rconlog/rconlog_server.lua
Normal file
84
resources/[cfx-default]/[system]/rconlog/rconlog_server.lua
Normal file
@ -0,0 +1,84 @@
|
||||
RconLog({ msgType = 'serverStart', hostname = 'lovely', maxplayers = 32 })
|
||||
|
||||
RegisterServerEvent('rlPlayerActivated')
|
||||
|
||||
local names = {}
|
||||
|
||||
AddEventHandler('rlPlayerActivated', function()
|
||||
RconLog({ msgType = 'playerActivated', netID = source, name = GetPlayerName(source), guid = GetPlayerIdentifiers(source)[1], ip = GetPlayerEP(source) })
|
||||
|
||||
names[source] = { name = GetPlayerName(source), id = source }
|
||||
|
||||
if GetHostId() then
|
||||
TriggerClientEvent('rlUpdateNames', GetHostId())
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterServerEvent('rlUpdateNamesResult')
|
||||
|
||||
AddEventHandler('rlUpdateNamesResult', function(res)
|
||||
if source ~= tonumber(GetHostId()) then
|
||||
print('bad guy')
|
||||
return
|
||||
end
|
||||
|
||||
for id, data in pairs(res) do
|
||||
if data then
|
||||
if data.name then
|
||||
if not names[id] then
|
||||
names[id] = data
|
||||
end
|
||||
|
||||
if names[id].name ~= data.name or names[id].id ~= data.id then
|
||||
names[id] = data
|
||||
|
||||
RconLog({ msgType = 'playerRenamed', netID = id, name = data.name })
|
||||
end
|
||||
end
|
||||
else
|
||||
names[id] = nil
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
AddEventHandler('playerDropped', function()
|
||||
RconLog({ msgType = 'playerDropped', netID = source, name = GetPlayerName(source) })
|
||||
|
||||
names[source] = nil
|
||||
end)
|
||||
|
||||
AddEventHandler('chatMessage', function(netID, name, message)
|
||||
RconLog({ msgType = 'chatMessage', netID = netID, name = name, message = message, guid = GetPlayerIdentifiers(netID)[1] })
|
||||
end)
|
||||
|
||||
-- NOTE: DO NOT USE THIS METHOD FOR HANDLING COMMANDS
|
||||
-- This resource has not been updated to use newer methods such as RegisterCommand.
|
||||
AddEventHandler('rconCommand', function(commandName, args)
|
||||
if commandName == 'status' then
|
||||
for netid, data in pairs(names) do
|
||||
local guid = GetPlayerIdentifiers(netid)
|
||||
|
||||
if guid and guid[1] and data then
|
||||
local ping = GetPlayerPing(netid)
|
||||
|
||||
RconPrint(netid .. ' ' .. guid[1] .. ' ' .. data.name .. ' ' .. GetPlayerEP(netid) .. ' ' .. ping .. "\n")
|
||||
end
|
||||
end
|
||||
|
||||
CancelEvent()
|
||||
elseif commandName:lower() == 'clientkick' then
|
||||
local playerId = table.remove(args, 1)
|
||||
local msg = table.concat(args, ' ')
|
||||
|
||||
-- DropPlayer(playerId, msg)
|
||||
|
||||
CancelEvent()
|
||||
elseif commandName:lower() == 'tempbanclient' then
|
||||
local playerId = table.remove(args, 1)
|
||||
local msg = table.concat(args, ' ')
|
||||
|
||||
TempBanPlayer(playerId, msg)
|
||||
|
||||
CancelEvent()
|
||||
end
|
||||
end)
|
1
resources/[cfx-default]/[system]/runcode/.gitignore
vendored
Normal file
1
resources/[cfx-default]/[system]/runcode/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
data.json
|
24
resources/[cfx-default]/[system]/runcode/fxmanifest.lua
Normal file
24
resources/[cfx-default]/[system]/runcode/fxmanifest.lua
Normal file
@ -0,0 +1,24 @@
|
||||
-- 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 'Allows server owners to execute arbitrary server-side or client-side JavaScript/Lua code. *Consider only using this on development servers.'
|
||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||
|
||||
game 'common'
|
||||
fx_version 'cerulean'
|
||||
|
||||
client_script 'runcode_cl.lua'
|
||||
server_script 'runcode_sv.lua'
|
||||
server_script 'runcode_web.lua'
|
||||
|
||||
shared_script 'runcode_shared.lua'
|
||||
shared_script 'runcode.js'
|
||||
|
||||
client_script 'runcode_ui.lua'
|
||||
|
||||
ui_page 'web/nui.html'
|
||||
files {
|
||||
'web/nui.html'
|
||||
}
|
11
resources/[cfx-default]/[system]/runcode/runcode.js
Normal file
11
resources/[cfx-default]/[system]/runcode/runcode.js
Normal file
@ -0,0 +1,11 @@
|
||||
exports('runJS', (snippet) => {
|
||||
if (IsDuplicityVersion() && GetInvokingResource() !== GetCurrentResourceName()) {
|
||||
return [ 'Invalid caller.', false ];
|
||||
}
|
||||
|
||||
try {
|
||||
return [ new Function(snippet)(), false ];
|
||||
} catch (e) {
|
||||
return [ false, e.toString() ];
|
||||
}
|
||||
});
|
15
resources/[cfx-default]/[system]/runcode/runcode_cl.lua
Normal file
15
resources/[cfx-default]/[system]/runcode/runcode_cl.lua
Normal file
@ -0,0 +1,15 @@
|
||||
RegisterNetEvent('runcode:gotSnippet')
|
||||
|
||||
AddEventHandler('runcode:gotSnippet', function(id, lang, code)
|
||||
local res, err = RunCode(lang, code)
|
||||
|
||||
if not err then
|
||||
if type(res) == 'vector3' then
|
||||
res = json.encode({ table.unpack(res) })
|
||||
elseif type(res) == 'table' then
|
||||
res = json.encode(res)
|
||||
end
|
||||
end
|
||||
|
||||
TriggerServerEvent('runcode:gotResult', id, res, err)
|
||||
end)
|
32
resources/[cfx-default]/[system]/runcode/runcode_shared.lua
Normal file
32
resources/[cfx-default]/[system]/runcode/runcode_shared.lua
Normal file
@ -0,0 +1,32 @@
|
||||
local runners = {}
|
||||
|
||||
function runners.lua(arg)
|
||||
local code, err = load('return ' .. arg, '@runcode')
|
||||
|
||||
-- if failed, try without return
|
||||
if err then
|
||||
code, err = load(arg, '@runcode')
|
||||
end
|
||||
|
||||
if err then
|
||||
print(err)
|
||||
return nil, err
|
||||
end
|
||||
|
||||
local status, result = pcall(code)
|
||||
print(result)
|
||||
|
||||
if status then
|
||||
return result
|
||||
end
|
||||
|
||||
return nil, result
|
||||
end
|
||||
|
||||
function runners.js(arg)
|
||||
return table.unpack(exports[GetCurrentResourceName()]:runJS(arg))
|
||||
end
|
||||
|
||||
function RunCode(lang, str)
|
||||
return runners[lang](str)
|
||||
end
|
42
resources/[cfx-default]/[system]/runcode/runcode_sv.lua
Normal file
42
resources/[cfx-default]/[system]/runcode/runcode_sv.lua
Normal file
@ -0,0 +1,42 @@
|
||||
function GetPrivs(source)
|
||||
return {
|
||||
canServer = IsPlayerAceAllowed(source, 'command.run'),
|
||||
canClient = IsPlayerAceAllowed(source, 'command.crun'),
|
||||
canSelf = IsPlayerAceAllowed(source, 'runcode.self'),
|
||||
}
|
||||
end
|
||||
|
||||
RegisterCommand('run', function(source, args, rawCommand)
|
||||
local res, err = RunCode('lua', rawCommand:sub(4))
|
||||
end, true)
|
||||
|
||||
RegisterCommand('crun', function(source, args, rawCommand)
|
||||
if not source then
|
||||
return
|
||||
end
|
||||
|
||||
TriggerClientEvent('runcode:gotSnippet', source, -1, 'lua', rawCommand:sub(5))
|
||||
end, true)
|
||||
|
||||
RegisterCommand('runcode', function(source, args, rawCommand)
|
||||
if not source then
|
||||
return
|
||||
end
|
||||
|
||||
local df = LoadResourceFile(GetCurrentResourceName(), 'data.json')
|
||||
local saveData = {}
|
||||
|
||||
if df then
|
||||
saveData = json.decode(df)
|
||||
end
|
||||
|
||||
local p = GetPrivs(source)
|
||||
|
||||
if not p.canServer and not p.canClient and not p.canSelf then
|
||||
return
|
||||
end
|
||||
|
||||
p.saveData = saveData
|
||||
|
||||
TriggerClientEvent('runcode:openUi', source, p)
|
||||
end, true)
|
66
resources/[cfx-default]/[system]/runcode/runcode_ui.lua
Normal file
66
resources/[cfx-default]/[system]/runcode/runcode_ui.lua
Normal file
@ -0,0 +1,66 @@
|
||||
local openData
|
||||
|
||||
RegisterNetEvent('runcode:openUi')
|
||||
|
||||
AddEventHandler('runcode:openUi', function(options)
|
||||
openData = {
|
||||
type = 'open',
|
||||
options = options,
|
||||
url = 'https://' .. GetCurrentServerEndpoint() .. '/' .. GetCurrentResourceName() .. '/',
|
||||
res = GetCurrentResourceName()
|
||||
}
|
||||
|
||||
SendNuiMessage(json.encode(openData))
|
||||
end)
|
||||
|
||||
RegisterNUICallback('getOpenData', function(args, cb)
|
||||
cb(openData)
|
||||
end)
|
||||
|
||||
RegisterNUICallback('doOk', function(args, cb)
|
||||
SendNuiMessage(json.encode({
|
||||
type = 'ok'
|
||||
}))
|
||||
|
||||
SetNuiFocus(true, true)
|
||||
|
||||
cb('ok')
|
||||
end)
|
||||
|
||||
RegisterNUICallback('doClose', function(args, cb)
|
||||
SendNuiMessage(json.encode({
|
||||
type = 'close'
|
||||
}))
|
||||
|
||||
SetNuiFocus(false, false)
|
||||
|
||||
cb('ok')
|
||||
end)
|
||||
|
||||
local rcCbs = {}
|
||||
local id = 1
|
||||
|
||||
RegisterNUICallback('runCodeInBand', function(args, cb)
|
||||
id = id + 1
|
||||
|
||||
rcCbs[id] = cb
|
||||
|
||||
TriggerServerEvent('runcode:runInBand', id, args)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('runcode:inBandResult')
|
||||
|
||||
AddEventHandler('runcode:inBandResult', function(id, result)
|
||||
if rcCbs[id] then
|
||||
local cb = rcCbs[id]
|
||||
rcCbs[id] = nil
|
||||
|
||||
cb(result)
|
||||
end
|
||||
end)
|
||||
|
||||
AddEventHandler('onResourceStop', function(resourceName)
|
||||
if resourceName == GetCurrentResourceName() then
|
||||
SetNuiFocus(false, false)
|
||||
end
|
||||
end)
|
192
resources/[cfx-default]/[system]/runcode/runcode_web.lua
Normal file
192
resources/[cfx-default]/[system]/runcode/runcode_web.lua
Normal file
@ -0,0 +1,192 @@
|
||||
local cachedFiles = {}
|
||||
|
||||
local function sendFile(res, fileName)
|
||||
if cachedFiles[fileName] then
|
||||
res.send(cachedFiles[fileName])
|
||||
return
|
||||
end
|
||||
|
||||
local fileData = LoadResourceFile(GetCurrentResourceName(), 'web/' .. fileName)
|
||||
|
||||
if not fileData then
|
||||
res.writeHead(404)
|
||||
res.send('Not found.')
|
||||
return
|
||||
end
|
||||
|
||||
cachedFiles[fileName] = fileData
|
||||
res.send(fileData)
|
||||
end
|
||||
|
||||
local codeId = 1
|
||||
local codes = {}
|
||||
|
||||
local attempts = 0
|
||||
local lastAttempt
|
||||
|
||||
local function handleRunCode(data, res)
|
||||
if not data.lang then
|
||||
data.lang = 'lua'
|
||||
end
|
||||
|
||||
if not data.client or data.client == '' then
|
||||
CreateThread(function()
|
||||
local result, err = RunCode(data.lang, data.code)
|
||||
|
||||
res.send(json.encode({
|
||||
result = result,
|
||||
error = err
|
||||
}))
|
||||
end)
|
||||
else
|
||||
codes[codeId] = {
|
||||
timeout = GetGameTimer() + 1000,
|
||||
res = res
|
||||
}
|
||||
|
||||
TriggerClientEvent('runcode:gotSnippet', tonumber(data.client), codeId, data.lang, data.code)
|
||||
|
||||
codeId = codeId + 1
|
||||
end
|
||||
end
|
||||
|
||||
RegisterNetEvent('runcode:runInBand')
|
||||
|
||||
AddEventHandler('runcode:runInBand', function(id, data)
|
||||
local s = source
|
||||
local privs = GetPrivs(s)
|
||||
|
||||
local res = {
|
||||
send = function(str)
|
||||
TriggerClientEvent('runcode:inBandResult', s, id, str)
|
||||
end
|
||||
}
|
||||
|
||||
if (not data.client or data.client == '') and not privs.canServer then
|
||||
res.send(json.encode({ error = 'Insufficient permissions.'}))
|
||||
return
|
||||
end
|
||||
|
||||
if (data.client and data.client ~= '') and not privs.canClient then
|
||||
if privs.canSelf then
|
||||
data.client = s
|
||||
else
|
||||
res.send(json.encode({ error = 'Insufficient permissions.'}))
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
SaveResourceFile(GetCurrentResourceName(), 'data.json', json.encode({
|
||||
lastSnippet = data.code,
|
||||
lastLang = data.lang or 'lua'
|
||||
}), -1)
|
||||
|
||||
handleRunCode(data, res)
|
||||
end)
|
||||
|
||||
local function handlePost(req, res)
|
||||
req.setDataHandler(function(body)
|
||||
local data = json.decode(body)
|
||||
|
||||
if not data or not data.password or not data.code then
|
||||
res.send(json.encode({ error = 'Bad request.'}))
|
||||
return
|
||||
end
|
||||
|
||||
if GetConvar('rcon_password', '') == '' then
|
||||
res.send(json.encode({ error = 'The server has an empty rcon_password.'}))
|
||||
return
|
||||
end
|
||||
|
||||
if attempts > 5 or data.password ~= GetConvar('rcon_password', '') then
|
||||
attempts = attempts + 1
|
||||
lastAttempt = GetGameTimer()
|
||||
|
||||
res.send(json.encode({ error = 'Bad password.'}))
|
||||
return
|
||||
end
|
||||
|
||||
handleRunCode(data, res)
|
||||
end)
|
||||
end
|
||||
|
||||
CreateThread(function()
|
||||
while true do
|
||||
Wait(1000)
|
||||
|
||||
if attempts > 0 and (GetGameTimer() - lastAttempt) > 5000 then
|
||||
attempts = 0
|
||||
lastAttempt = 0
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
local function returnCode(id, res, err)
|
||||
if not codes[id] then
|
||||
return
|
||||
end
|
||||
|
||||
local code = codes[id]
|
||||
codes[id] = nil
|
||||
|
||||
local gotFrom
|
||||
|
||||
if source then
|
||||
gotFrom = GetPlayerName(source) .. ' [' .. tostring(source) .. ']'
|
||||
end
|
||||
|
||||
code.res.send(json.encode({
|
||||
result = res,
|
||||
error = err,
|
||||
from = gotFrom
|
||||
}))
|
||||
end
|
||||
|
||||
CreateThread(function()
|
||||
while true do
|
||||
Wait(100)
|
||||
|
||||
for k, v in ipairs(codes) do
|
||||
if GetGameTimer() > v.timeout then
|
||||
source = nil
|
||||
returnCode(k, '', 'Timed out waiting on the target client.')
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterNetEvent('runcode:gotResult')
|
||||
AddEventHandler('runcode:gotResult', returnCode)
|
||||
|
||||
SetHttpHandler(function(req, res)
|
||||
local path = req.path
|
||||
|
||||
if req.method == 'POST' then
|
||||
return handlePost(req, res)
|
||||
end
|
||||
|
||||
-- client shortcuts
|
||||
if req.path == '/clients' then
|
||||
local clientList = {}
|
||||
|
||||
for _, id in ipairs(GetPlayers()) do
|
||||
table.insert(clientList, { GetPlayerName(id), id })
|
||||
end
|
||||
|
||||
res.send(json.encode({
|
||||
clients = clientList
|
||||
}))
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
-- should this be the index?
|
||||
if req.path == '/' then
|
||||
path = 'index.html'
|
||||
end
|
||||
|
||||
-- remove any '..' from the path
|
||||
path = path:gsub("%.%.", "")
|
||||
|
||||
return sendFile(res, path)
|
||||
end)
|
486
resources/[cfx-default]/[system]/runcode/web/index.html
Normal file
486
resources/[cfx-default]/[system]/runcode/web/index.html
Normal file
@ -0,0 +1,486 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>fivem runcode</title>
|
||||
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulmaswatch/0.7.2/cyborg/bulmaswatch.min.css">
|
||||
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
|
||||
|
||||
<style type="text/css">
|
||||
body {
|
||||
font-family: "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
z-index: inherit;
|
||||
}
|
||||
|
||||
html.in-nui {
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
|
||||
margin-top: 5vh;
|
||||
margin-left: 7.5vw;
|
||||
margin-right: 7.5vw;
|
||||
margin-bottom: 5vh;
|
||||
|
||||
height: calc(100% - 10vh);
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
html.in-nui body > div.bg {
|
||||
background-color: rgba(0, 0, 0, 1);
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
z-index: -999;
|
||||
|
||||
box-shadow: 0 22px 70px 4px rgba(0, 0, 0, 0.56);
|
||||
}
|
||||
|
||||
span.nui-edition {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html.in-nui span.nui-edition {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
#close {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html.in-nui #close {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#result {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg">
|
||||
|
||||
</div>
|
||||
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="/runcode">
|
||||
<strong>runcode</strong> <span class="nui-edition"> in-game</span>
|
||||
</a>
|
||||
|
||||
<a role="button" class="navbar-burger burger" aria-label="menu" aria-expanded="false" data-target="navbarMain">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="navbarMain" class="navbar-menu">
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item">
|
||||
<div class="field" id="cl-field">
|
||||
<div class="control has-icons-left">
|
||||
<div class="select">
|
||||
<select id="cl-select">
|
||||
</select>
|
||||
</div>
|
||||
<div class="icon is-small is-left">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="navbar-item">
|
||||
<div class="field has-addons" id="lang-toggle">
|
||||
<p class="control">
|
||||
<button class="button" id="lua-button">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-moon"></i>
|
||||
</span>
|
||||
<span>Lua</span>
|
||||
</button>
|
||||
</p>
|
||||
<p class="control">
|
||||
<button class="button" id="js-button">
|
||||
<span class="icon is-small">
|
||||
<i class="fab fa-js"></i>
|
||||
</span>
|
||||
<span>JS</span>
|
||||
</button>
|
||||
</p>
|
||||
<!-- TODO pending add-on resource that'll contain webpack'd compiler
|
||||
<p class="control">
|
||||
<button class="button" id="ts-button">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-code"></i>
|
||||
</span>
|
||||
<span>TS</span>
|
||||
</button>
|
||||
</p>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="navbar-item">
|
||||
<div class="field has-addons" id="cl-sv-toggle">
|
||||
<p class="control">
|
||||
<button class="button" id="cl-button">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-user-friends"></i>
|
||||
</span>
|
||||
<span>Client</span>
|
||||
</button>
|
||||
</p>
|
||||
<p class="control">
|
||||
<button class="button" id="sv-button">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-server"></i>
|
||||
</span>
|
||||
<span>Server</span>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="navbar-item" id="close">
|
||||
<button class="button is-danger">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div id="code-container" style="width:100%;height:60vh;border:1px solid grey"></div><br>
|
||||
<div class="field" id="passwordField">
|
||||
<p class="control has-icons-left">
|
||||
<input class="input" type="password" id="password" placeholder="RCon Password">
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fas fa-lock"></i>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<button class="button is-primary" id="run">Run</button>
|
||||
<div id="result">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!--
|
||||
to use a local deployment, uncomment; do note currently the server isn't optimized to serve >1MB files
|
||||
<script src="monaco-editor/vs/loader.js"></script>
|
||||
-->
|
||||
|
||||
<script src="https://unpkg.com/monaco-editor@0.18.1/min/vs/loader.js"></script>
|
||||
|
||||
<script>
|
||||
function fetchClients() {
|
||||
fetch('/runcode/clients').then(res => res.json()).then(res => {
|
||||
const el = document.querySelector('#cl-select');
|
||||
|
||||
const clients = res.clients;
|
||||
const realClients = [['All', '-1'], ...clients];
|
||||
|
||||
const createdClients = new Set([...el.querySelectorAll('option').entries()].map(([i, el]) => el.value));
|
||||
const existentClients = new Set(realClients.map(([ name, id ]) => id));
|
||||
|
||||
const toRemove = [...createdClients].filter(a => !existentClients.has(a));
|
||||
|
||||
for (const [name, id] of realClients) {
|
||||
const ex = el.querySelector(`option[value="${id}"]`);
|
||||
|
||||
if (!ex) {
|
||||
const l = document.createElement('option');
|
||||
l.setAttribute('value', id);
|
||||
l.appendChild(document.createTextNode(name));
|
||||
|
||||
el.appendChild(l);
|
||||
}
|
||||
}
|
||||
|
||||
for (const id of toRemove) {
|
||||
const l = el.querySelector(`option[value="${id}"]`);
|
||||
|
||||
if (l) {
|
||||
el.removeChild(l);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let useClient = false;
|
||||
let editServerCb = null;
|
||||
|
||||
[['#cl-button', true], ['#sv-button', false]].forEach(([ selector, isClient ]) => {
|
||||
const eh = () => {
|
||||
if (isClient) {
|
||||
document.querySelector('#cl-select').disabled = false;
|
||||
useClient = true;
|
||||
} else {
|
||||
document.querySelector('#cl-select').disabled = true;
|
||||
useClient = false;
|
||||
}
|
||||
|
||||
document.querySelectorAll('#cl-sv-toggle button').forEach(el => {
|
||||
el.classList.remove('is-selected', 'is-info');
|
||||
});
|
||||
|
||||
const tgt = document.querySelector(selector);
|
||||
|
||||
tgt.classList.add('is-selected', 'is-info');
|
||||
|
||||
if (editServerCb) {
|
||||
editServerCb();
|
||||
}
|
||||
};
|
||||
|
||||
// default to not-client
|
||||
if (!isClient) {
|
||||
eh();
|
||||
}
|
||||
|
||||
document.querySelector(selector).addEventListener('click', ev => {
|
||||
eh();
|
||||
|
||||
ev.preventDefault();
|
||||
});
|
||||
});
|
||||
|
||||
let lang = 'lua';
|
||||
let editLangCb = null;
|
||||
let initCb = null;
|
||||
|
||||
function getLangCode(lang) {
|
||||
switch (lang) {
|
||||
case 'js':
|
||||
return 'javascript';
|
||||
case 'ts':
|
||||
return 'typescript';
|
||||
}
|
||||
|
||||
return lang;
|
||||
}
|
||||
|
||||
[['#lua-button', 'lua'], ['#js-button', 'js']/*, ['#ts-button', 'ts']*/].forEach(([ selector, langOpt ]) => {
|
||||
const eh = () => {
|
||||
lang = langOpt;
|
||||
|
||||
document.querySelectorAll('#lang-toggle button').forEach(el => {
|
||||
el.classList.remove('is-selected', 'is-info');
|
||||
});
|
||||
|
||||
const tgt = document.querySelector(selector);
|
||||
|
||||
tgt.classList.add('is-selected', 'is-info');
|
||||
|
||||
if (editLangCb) {
|
||||
editLangCb();
|
||||
}
|
||||
};
|
||||
|
||||
// default to not-client
|
||||
if (langOpt === 'lua') {
|
||||
eh();
|
||||
}
|
||||
|
||||
document.querySelector(selector).addEventListener('click', ev => {
|
||||
eh();
|
||||
|
||||
ev.preventDefault();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
setInterval(() => fetchClients(), 1000);
|
||||
|
||||
const inNui = (!!window.invokeNative);
|
||||
let openData = {};
|
||||
|
||||
if (inNui) {
|
||||
document.querySelector('#passwordField').style.display = 'none';
|
||||
document.querySelector('html').classList.add('in-nui');
|
||||
|
||||
fetch(`https://${window.parent.GetParentResourceName()}/getOpenData`, {
|
||||
method: 'POST',
|
||||
body: '{}'
|
||||
}).then(a => a.json())
|
||||
.then(a => {
|
||||
openData = a;
|
||||
|
||||
if (!openData.options.canServer) {
|
||||
document.querySelector('#cl-sv-toggle').style.display = 'none';
|
||||
|
||||
const trigger = document.createEvent('HTMLEvents');
|
||||
trigger.initEvent('click', true, true);
|
||||
|
||||
document.querySelector('#cl-button').dispatchEvent(trigger);
|
||||
} else if (!openData.options.canClient && !openData.options.canSelf) {
|
||||
document.querySelector('#cl-sv-toggle').style.display = 'none';
|
||||
document.querySelector('#cl-field').style.display = 'none';
|
||||
|
||||
const trigger = document.createEvent('HTMLEvents');
|
||||
trigger.initEvent('click', true, true);
|
||||
|
||||
document.querySelector('#sv-button').dispatchEvent(trigger);
|
||||
}
|
||||
|
||||
if (!openData.options.canClient && openData.options.canSelf) {
|
||||
document.querySelector('#cl-field').style.display = 'none';
|
||||
}
|
||||
|
||||
if (openData.options.saveData) {
|
||||
const cb = () => {
|
||||
if (initCb) {
|
||||
initCb({
|
||||
lastLang: openData.options.saveData.lastLang,
|
||||
lastSnippet: openData.options.saveData.lastSnippet
|
||||
});
|
||||
} else {
|
||||
setTimeout(cb, 50);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(cb, 50);
|
||||
}
|
||||
|
||||
fetch(`https://${window.parent.GetParentResourceName()}/doOk`, {
|
||||
method: 'POST',
|
||||
body: '{}'
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelector('#close button').addEventListener('click', ev => {
|
||||
fetch(`https://${window.parent.GetParentResourceName()}/doClose`, {
|
||||
method: 'POST',
|
||||
body: '{}'
|
||||
});
|
||||
|
||||
ev.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
const defFiles = ['index.d.ts'];
|
||||
const defFilesServer = [...defFiles, 'natives_server.d.ts'];
|
||||
const defFilesClient = [...defFiles, 'natives_universal.d.ts'];
|
||||
|
||||
const prefix = 'https://unpkg.com/@citizenfx/{}/';
|
||||
const prefixClient = prefix.replace('{}', 'client');
|
||||
const prefixServer = prefix.replace('{}', 'server');
|
||||
|
||||
require.config({ paths: { 'vs': 'https://unpkg.com/monaco-editor@0.18.1/min/vs' }});
|
||||
require(['vs/editor/editor.main'], function() {
|
||||
const editor = monaco.editor.create(document.getElementById('code-container'), {
|
||||
value: 'return 42',
|
||||
language: 'lua'
|
||||
});
|
||||
|
||||
monaco.editor.setTheme('vs-dark');
|
||||
|
||||
let finalizers = [];
|
||||
|
||||
const updateScript = (client, lang) => {
|
||||
finalizers.forEach(a => a());
|
||||
finalizers = [];
|
||||
|
||||
if (lang === 'js' || lang === 'ts') {
|
||||
const defaults = (lang === 'js') ? monaco.languages.typescript.javascriptDefaults :
|
||||
monaco.languages.typescript.typescriptDefaults;
|
||||
|
||||
defaults.setCompilerOptions({
|
||||
noLib: true,
|
||||
allowNonTsExtensions: true
|
||||
});
|
||||
|
||||
for (const file of (client ? defFilesClient : defFilesServer)) {
|
||||
const prefix = (client ? prefixClient : prefixServer);
|
||||
|
||||
fetch(`${prefix}${file}`)
|
||||
.then(a => a.text())
|
||||
.then(a => {
|
||||
const l = defaults.addExtraLib(a, file);
|
||||
|
||||
finalizers.push(() => l.dispose());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editLangCb = () => {
|
||||
monaco.editor.setModelLanguage(editor.getModel(), getLangCode(lang));
|
||||
|
||||
updateScript(useClient, lang);
|
||||
};
|
||||
|
||||
editServerCb = () => {
|
||||
updateScript(useClient, lang);
|
||||
};
|
||||
|
||||
initCb = (data) => {
|
||||
if (data.lastLang) {
|
||||
const trigger = document.createEvent('HTMLEvents');
|
||||
trigger.initEvent('click', true, true);
|
||||
document.querySelector(`#${data.lastLang}-button`).dispatchEvent(trigger);
|
||||
}
|
||||
|
||||
if (data.lastSnippet) {
|
||||
editor.getModel().setValue(data.lastSnippet);
|
||||
}
|
||||
};
|
||||
|
||||
document.querySelector('#run').addEventListener('click', e => {
|
||||
const text = editor.getValue();
|
||||
|
||||
fetch((!inNui) ? '/runcode/' : `https://${openData.res}/runCodeInBand`, {
|
||||
method: 'post',
|
||||
body: JSON.stringify({
|
||||
password: document.querySelector('#password').value,
|
||||
client: (useClient) ? document.querySelector('#cl-select').value : '',
|
||||
code: text,
|
||||
lang: lang
|
||||
})
|
||||
}).then(res => res.json()).then(res => {
|
||||
if (inNui) {
|
||||
res = JSON.parse(res); // double packing for sad msgpack-to-json
|
||||
}
|
||||
|
||||
const resultElement = document.querySelector('#result');
|
||||
|
||||
if (res.error) {
|
||||
resultElement.classList.remove('notification', 'is-success');
|
||||
resultElement.classList.add('notification', 'is-danger');
|
||||
} else {
|
||||
resultElement.classList.remove('notification', 'is-danger');
|
||||
resultElement.classList.add('notification', 'is-success');
|
||||
}
|
||||
|
||||
resultElement.innerHTML = res.error || res.result;
|
||||
|
||||
if (res.from) {
|
||||
resultElement.innerHTML += ' (from ' + res.from + ')';
|
||||
}
|
||||
});
|
||||
|
||||
e.preventDefault();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
60
resources/[cfx-default]/[system]/runcode/web/nui.html
Normal file
60
resources/[cfx-default]/[system]/runcode/web/nui.html
Normal file
@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<title>runcode nui</title>
|
||||
|
||||
<style type="text/css">
|
||||
html {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: transparent;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="holder">
|
||||
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
let openData = null;
|
||||
|
||||
window.addEventListener('message', ev => {
|
||||
switch (ev.data.type) {
|
||||
case 'open':
|
||||
const frame = document.createElement('iframe');
|
||||
|
||||
frame.name = 'rc';
|
||||
frame.allow = 'microphone *;';
|
||||
frame.src = ev.data.url;
|
||||
frame.style.visibility = 'hidden';
|
||||
|
||||
openData = ev.data;
|
||||
openData.frame = frame;
|
||||
|
||||
document.querySelector('#holder').appendChild(frame);
|
||||
break;
|
||||
case 'ok':
|
||||
openData.frame.style.visibility = 'visible';
|
||||
break;
|
||||
case 'close':
|
||||
document.querySelector('#holder').removeChild(openData.frame);
|
||||
|
||||
openData = null;
|
||||
break;
|
||||
}
|
||||
});
|
||||
</script>
|
2
resources/[cfx-default]/[system]/sessionmanager-rdr3/.gitignore
vendored
Normal file
2
resources/[cfx-default]/[system]/sessionmanager-rdr3/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
.yarn.installed
|
@ -0,0 +1,17 @@
|
||||
-- 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 Social Club conductor session API for RedM. Do not disable.'
|
||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||
|
||||
fx_version 'cerulean'
|
||||
game 'rdr3'
|
||||
rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.'
|
||||
|
||||
dependencies {
|
||||
'yarn'
|
||||
}
|
||||
|
||||
server_script 'sm_server.js'
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@citizenfx/protobufjs": "6.8.8"
|
||||
}
|
||||
}
|
192
resources/[cfx-default]/[system]/sessionmanager-rdr3/rline.proto
Normal file
192
resources/[cfx-default]/[system]/sessionmanager-rdr3/rline.proto
Normal file
@ -0,0 +1,192 @@
|
||||
syntax = "proto3";
|
||||
package rline;
|
||||
|
||||
message RpcErrorData {
|
||||
string ErrorCodeString = 1;
|
||||
int32 ErrorCode = 2;
|
||||
string DomainString = 3;
|
||||
int32 DomainCode = 4;
|
||||
bytes DataEx = 5;
|
||||
};
|
||||
|
||||
message RpcError {
|
||||
int32 ErrorCode = 1;
|
||||
string ErrorMessage = 2;
|
||||
RpcErrorData Data = 3;
|
||||
};
|
||||
|
||||
message RpcHeader {
|
||||
string RequestId = 1;
|
||||
string MethodName = 2;
|
||||
RpcError Error = 3;
|
||||
string srcTid = 4;
|
||||
};
|
||||
|
||||
message RpcMessage {
|
||||
RpcHeader Header = 1;
|
||||
bytes Content = 2;
|
||||
};
|
||||
|
||||
message RpcResponseContainer {
|
||||
bytes Content = 1;
|
||||
};
|
||||
|
||||
message RpcResponseMessage {
|
||||
RpcHeader Header = 1;
|
||||
RpcResponseContainer Container = 2;
|
||||
};
|
||||
|
||||
message TokenStuff {
|
||||
string tkn = 1;
|
||||
};
|
||||
|
||||
message InitSessionResponse {
|
||||
bytes sesid = 1;
|
||||
TokenStuff token = 2;
|
||||
};
|
||||
|
||||
message MpGamerHandleDto {
|
||||
string gh = 1;
|
||||
};
|
||||
|
||||
message MpPeerAddressDto {
|
||||
string addr = 1;
|
||||
};
|
||||
|
||||
message InitPlayer2_Parameters {
|
||||
MpGamerHandleDto gh = 1;
|
||||
MpPeerAddressDto peerAddress = 2;
|
||||
int32 discriminator = 3;
|
||||
int32 seamlessType = 4;
|
||||
uint32 connectionReason = 5;
|
||||
};
|
||||
|
||||
message InitPlayerResult {
|
||||
uint32 code = 1;
|
||||
};
|
||||
|
||||
message Restriction {
|
||||
int32 u1 = 1;
|
||||
int32 u2 = 2;
|
||||
int32 u3 = 3;
|
||||
}
|
||||
|
||||
message GetRestrictionsData {
|
||||
repeated Restriction restriction = 1;
|
||||
repeated string unk2 = 2;
|
||||
};
|
||||
|
||||
message GetRestrictionsResult {
|
||||
GetRestrictionsData data = 1;
|
||||
};
|
||||
|
||||
message PlayerIdSto {
|
||||
int32 acctId = 1;
|
||||
int32 platId = 2;
|
||||
};
|
||||
|
||||
message MpSessionRequestIdDto {
|
||||
PlayerIdSto requestor = 1;
|
||||
int32 index = 2;
|
||||
int32 hash = 3;
|
||||
};
|
||||
|
||||
message QueueForSession_Seamless_Parameters {
|
||||
MpSessionRequestIdDto requestId = 1;
|
||||
uint32 optionFlags = 2;
|
||||
int32 x = 3;
|
||||
int32 y = 4;
|
||||
};
|
||||
|
||||
message QueueForSessionResult {
|
||||
uint32 code = 1;
|
||||
};
|
||||
|
||||
message QueueEntered_Parameters {
|
||||
uint32 queueGroup = 1;
|
||||
MpSessionRequestIdDto requestId = 2;
|
||||
uint32 optionFlags = 3;
|
||||
};
|
||||
|
||||
message GuidDto {
|
||||
fixed64 a = 1;
|
||||
fixed64 b = 2;
|
||||
};
|
||||
|
||||
message MpTransitionIdDto {
|
||||
GuidDto value = 1;
|
||||
};
|
||||
|
||||
message MpSessionIdDto {
|
||||
GuidDto value = 1;
|
||||
};
|
||||
|
||||
message SessionSubcommandEnterSession {
|
||||
int32 index = 1;
|
||||
int32 hindex = 2;
|
||||
uint32 sessionFlags = 3;
|
||||
uint32 mode = 4;
|
||||
int32 size = 5;
|
||||
int32 teamIndex = 6;
|
||||
MpTransitionIdDto transitionId = 7;
|
||||
uint32 sessionManagerType = 8;
|
||||
int32 slotCount = 9;
|
||||
};
|
||||
|
||||
message SessionSubcommandLeaveSession {
|
||||
uint32 reason = 1;
|
||||
};
|
||||
|
||||
message SessionSubcommandAddPlayer {
|
||||
PlayerIdSto id = 1;
|
||||
MpGamerHandleDto gh = 2;
|
||||
MpPeerAddressDto addr = 3;
|
||||
int32 index = 4;
|
||||
};
|
||||
|
||||
message SessionSubcommandRemovePlayer {
|
||||
PlayerIdSto id = 1;
|
||||
};
|
||||
|
||||
message SessionSubcommandHostChanged {
|
||||
int32 index = 1;
|
||||
};
|
||||
|
||||
message SessionCommand {
|
||||
uint32 cmd = 1;
|
||||
string cmdname = 2;
|
||||
SessionSubcommandEnterSession EnterSession = 3;
|
||||
SessionSubcommandLeaveSession LeaveSession = 4;
|
||||
SessionSubcommandAddPlayer AddPlayer = 5;
|
||||
SessionSubcommandRemovePlayer RemovePlayer = 6;
|
||||
SessionSubcommandHostChanged HostChanged = 7;
|
||||
};
|
||||
|
||||
message scmds_Parameters {
|
||||
MpSessionIdDto sid = 1;
|
||||
int32 ncmds = 2;
|
||||
repeated SessionCommand cmds = 3;
|
||||
};
|
||||
|
||||
message UriType {
|
||||
string url = 1;
|
||||
};
|
||||
|
||||
message TransitionReady_PlayerQueue_Parameters {
|
||||
UriType serverUri = 1;
|
||||
uint32 serverSandbox = 2;
|
||||
MpTransitionIdDto id = 3;
|
||||
uint32 sessionType = 4;
|
||||
MpSessionRequestIdDto requestId = 5;
|
||||
MpSessionIdDto transferId = 6;
|
||||
};
|
||||
|
||||
message TransitionToSession_Parameters {
|
||||
MpTransitionIdDto id = 1;
|
||||
float x = 2;
|
||||
float y = 3;
|
||||
};
|
||||
|
||||
message TransitionToSessionResult {
|
||||
uint32 code = 1;
|
||||
};
|
@ -0,0 +1,328 @@
|
||||
const protobuf = require("@citizenfx/protobufjs");
|
||||
|
||||
const playerDatas = {};
|
||||
let slotsUsed = 0;
|
||||
|
||||
function assignSlotId() {
|
||||
for (let i = 0; i < 32; i++) {
|
||||
if (!(slotsUsed & (1 << i))) {
|
||||
slotsUsed |= (1 << i);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
let hostIndex = -1;
|
||||
const isOneSync = GetConvar("onesync", "off") !== "off";
|
||||
|
||||
protobuf.load(GetResourcePath(GetCurrentResourceName()) + "/rline.proto", function(err, root) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
return;
|
||||
}
|
||||
|
||||
const RpcMessage = root.lookupType("rline.RpcMessage");
|
||||
const RpcResponseMessage = root.lookupType("rline.RpcResponseMessage");
|
||||
const InitSessionResponse = root.lookupType("rline.InitSessionResponse");
|
||||
const InitPlayer2_Parameters = root.lookupType("rline.InitPlayer2_Parameters");
|
||||
const InitPlayerResult = root.lookupType("rline.InitPlayerResult");
|
||||
const GetRestrictionsResult = root.lookupType("rline.GetRestrictionsResult");
|
||||
const QueueForSession_Seamless_Parameters = root.lookupType("rline.QueueForSession_Seamless_Parameters");
|
||||
const QueueForSessionResult = root.lookupType("rline.QueueForSessionResult");
|
||||
const QueueEntered_Parameters = root.lookupType("rline.QueueEntered_Parameters");
|
||||
const TransitionReady_PlayerQueue_Parameters = root.lookupType("rline.TransitionReady_PlayerQueue_Parameters");
|
||||
const TransitionToSession_Parameters = root.lookupType("rline.TransitionToSession_Parameters");
|
||||
const TransitionToSessionResult = root.lookupType("rline.TransitionToSessionResult");
|
||||
const scmds_Parameters = root.lookupType("rline.scmds_Parameters");
|
||||
|
||||
function toArrayBuffer(buf) {
|
||||
var ab = new ArrayBuffer(buf.length);
|
||||
var view = new Uint8Array(ab);
|
||||
for (var i = 0; i < buf.length; ++i) {
|
||||
view[i] = buf[i];
|
||||
}
|
||||
return ab;
|
||||
}
|
||||
|
||||
function emitMsg(target, data) {
|
||||
emitNet('__cfx_internal:pbRlScSession', target, toArrayBuffer(data));
|
||||
}
|
||||
|
||||
function emitSessionCmds(target, cmd, cmdname, msg) {
|
||||
const stuff = {};
|
||||
stuff[cmdname] = msg;
|
||||
|
||||
emitMsg(target, RpcMessage.encode({
|
||||
Header: {
|
||||
MethodName: 'scmds'
|
||||
},
|
||||
Content: scmds_Parameters.encode({
|
||||
sid: {
|
||||
value: {
|
||||
a: 2,
|
||||
b: 2
|
||||
}
|
||||
},
|
||||
ncmds: 1,
|
||||
cmds: [
|
||||
{
|
||||
cmd,
|
||||
cmdname,
|
||||
...stuff
|
||||
}
|
||||
]
|
||||
}).finish()
|
||||
}).finish());
|
||||
}
|
||||
|
||||
function emitAddPlayer(target, msg) {
|
||||
emitSessionCmds(target, 2, 'AddPlayer', msg);
|
||||
}
|
||||
|
||||
function emitRemovePlayer(target, msg) {
|
||||
emitSessionCmds(target, 3, 'RemovePlayer', msg);
|
||||
}
|
||||
|
||||
function emitHostChanged(target, msg) {
|
||||
emitSessionCmds(target, 5, 'HostChanged', msg);
|
||||
}
|
||||
|
||||
onNet('playerDropped', () => {
|
||||
if (isOneSync) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const oData = playerDatas[source];
|
||||
delete playerDatas[source];
|
||||
|
||||
if (oData && hostIndex === oData.slot) {
|
||||
const pda = Object.entries(playerDatas);
|
||||
|
||||
if (pda.length > 0) {
|
||||
hostIndex = pda[0][1].slot | 0; // TODO: actually use <=31 slot index *and* check for id
|
||||
|
||||
for (const [ id, data ] of Object.entries(playerDatas)) {
|
||||
emitHostChanged(id, {
|
||||
index: hostIndex
|
||||
});
|
||||
}
|
||||
} else {
|
||||
hostIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!oData) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (oData.slot > -1) {
|
||||
slotsUsed &= ~(1 << oData.slot);
|
||||
}
|
||||
|
||||
for (const [ id, data ] of Object.entries(playerDatas)) {
|
||||
emitRemovePlayer(id, {
|
||||
id: oData.id
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.log(e.stack);
|
||||
}
|
||||
});
|
||||
|
||||
function makeResponse(type, data) {
|
||||
return {
|
||||
Header: {
|
||||
},
|
||||
Container: {
|
||||
Content: type.encode(data).finish()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handlers = {
|
||||
async InitSession(source, data) {
|
||||
return makeResponse(InitSessionResponse, {
|
||||
sesid: Buffer.alloc(16),
|
||||
/*token: {
|
||||
tkn: 'ACSTOKEN token="meow",signature="meow"'
|
||||
}*/
|
||||
});
|
||||
},
|
||||
|
||||
async InitPlayer2(source, data) {
|
||||
const req = InitPlayer2_Parameters.decode(data);
|
||||
|
||||
if (!isOneSync) {
|
||||
playerDatas[source] = {
|
||||
gh: req.gh,
|
||||
peerAddress: req.peerAddress,
|
||||
discriminator: req.discriminator,
|
||||
slot: -1
|
||||
};
|
||||
}
|
||||
|
||||
return makeResponse(InitPlayerResult, {
|
||||
code: 0
|
||||
});
|
||||
},
|
||||
|
||||
async GetRestrictions(source, data) {
|
||||
return makeResponse(GetRestrictionsResult, {
|
||||
data: {
|
||||
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async ConfirmSessionEntered(source, data) {
|
||||
return {};
|
||||
},
|
||||
|
||||
async TransitionToSession(source, data) {
|
||||
const req = TransitionToSession_Parameters.decode(data);
|
||||
|
||||
return makeResponse(TransitionToSessionResult, {
|
||||
code: 1 // in this message, 1 is success
|
||||
});
|
||||
},
|
||||
|
||||
async QueueForSession_Seamless(source, data) {
|
||||
const req = QueueForSession_Seamless_Parameters.decode(data);
|
||||
|
||||
if (!isOneSync) {
|
||||
playerDatas[source].req = req.requestId;
|
||||
playerDatas[source].id = req.requestId.requestor;
|
||||
playerDatas[source].slot = assignSlotId();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
emitMsg(source, RpcMessage.encode({
|
||||
Header: {
|
||||
MethodName: 'QueueEntered'
|
||||
},
|
||||
Content: QueueEntered_Parameters.encode({
|
||||
queueGroup: 69,
|
||||
requestId: req.requestId,
|
||||
optionFlags: req.optionFlags
|
||||
}).finish()
|
||||
}).finish());
|
||||
|
||||
if (isOneSync) {
|
||||
hostIndex = 16
|
||||
} else if (hostIndex === -1) {
|
||||
hostIndex = playerDatas[source].slot | 0;
|
||||
}
|
||||
|
||||
emitMsg(source, RpcMessage.encode({
|
||||
Header: {
|
||||
MethodName: 'TransitionReady_PlayerQueue'
|
||||
},
|
||||
Content: TransitionReady_PlayerQueue_Parameters.encode({
|
||||
serverUri: {
|
||||
url: ''
|
||||
},
|
||||
requestId: req.requestId,
|
||||
id: {
|
||||
value: {
|
||||
a: 2,
|
||||
b: 0
|
||||
}
|
||||
},
|
||||
serverSandbox: 0xD656C677,
|
||||
sessionType: 3,
|
||||
transferId: {
|
||||
value: {
|
||||
a: 2,
|
||||
b: 2
|
||||
}
|
||||
},
|
||||
}).finish()
|
||||
}).finish());
|
||||
|
||||
setTimeout(() => {
|
||||
emitSessionCmds(source, 0, 'EnterSession', {
|
||||
index: (isOneSync) ? 16 : playerDatas[source].slot | 0,
|
||||
hindex: hostIndex,
|
||||
sessionFlags: 0,
|
||||
mode: 0,
|
||||
size: (isOneSync) ? 0 : Object.entries(playerDatas).filter(a => a[1].id).length,
|
||||
//size: 2,
|
||||
//size: Object.entries(playerDatas).length,
|
||||
teamIndex: 0,
|
||||
transitionId: {
|
||||
value: {
|
||||
a: 2,
|
||||
b: 0
|
||||
}
|
||||
},
|
||||
sessionManagerType: 0,
|
||||
slotCount: 32
|
||||
});
|
||||
}, 50);
|
||||
|
||||
if (!isOneSync) {
|
||||
setTimeout(() => {
|
||||
// tell player about everyone, and everyone about player
|
||||
const meData = playerDatas[source];
|
||||
|
||||
const aboutMe = {
|
||||
id: meData.id,
|
||||
gh: meData.gh,
|
||||
addr: meData.peerAddress,
|
||||
index: playerDatas[source].slot | 0
|
||||
};
|
||||
|
||||
for (const [ id, data ] of Object.entries(playerDatas)) {
|
||||
if (id == source || !data.id) continue;
|
||||
|
||||
emitAddPlayer(source, {
|
||||
id: data.id,
|
||||
gh: data.gh,
|
||||
addr: data.peerAddress,
|
||||
index: data.slot | 0
|
||||
});
|
||||
|
||||
emitAddPlayer(id, aboutMe);
|
||||
}
|
||||
}, 150);
|
||||
}
|
||||
}, 250);
|
||||
|
||||
return makeResponse(QueueForSessionResult, {
|
||||
code: 1
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
async function handleMessage(source, method, data) {
|
||||
if (handlers[method]) {
|
||||
return await handlers[method](source, data);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
onNet('__cfx_internal:pbRlScSession', async (data) => {
|
||||
const s = source;
|
||||
|
||||
try {
|
||||
const message = RpcMessage.decode(new Uint8Array(data));
|
||||
const response = await handleMessage(s, message.Header.MethodName, message.Content);
|
||||
|
||||
if (!response || !response.Header) {
|
||||
return;
|
||||
}
|
||||
|
||||
response.Header.RequestId = message.Header.RequestId;
|
||||
|
||||
emitMsg(s, RpcResponseMessage.encode(response).finish());
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.log(e.stack);
|
||||
}
|
||||
});
|
||||
});
|
@ -0,0 +1,90 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@citizenfx/protobufjs@6.8.8":
|
||||
version "6.8.8"
|
||||
resolved "https://registry.yarnpkg.com/@citizenfx/protobufjs/-/protobufjs-6.8.8.tgz#d12edcada06182f3785dea1d35eebf0ebe4fd2c3"
|
||||
integrity sha512-RBJvHPWNwguEPxV+ALbCZBXAEQf2byP5KtYrYl36Mbb0+Ch7MxpOm+764S+YqYWj/A/iNSW3jXglfJ9hlxjBLA==
|
||||
dependencies:
|
||||
"@protobufjs/aspromise" "^1.1.2"
|
||||
"@protobufjs/base64" "^1.1.2"
|
||||
"@protobufjs/codegen" "^2.0.4"
|
||||
"@protobufjs/eventemitter" "^1.1.0"
|
||||
"@protobufjs/fetch" "^1.1.0"
|
||||
"@protobufjs/float" "^1.0.2"
|
||||
"@protobufjs/inquire" "^1.1.0"
|
||||
"@protobufjs/path" "^1.1.2"
|
||||
"@protobufjs/pool" "^1.1.0"
|
||||
"@protobufjs/utf8" "^1.1.0"
|
||||
"@types/long" "^4.0.0"
|
||||
"@types/node" "^10.1.0"
|
||||
long "^4.0.0"
|
||||
|
||||
"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
|
||||
integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78=
|
||||
|
||||
"@protobufjs/base64@^1.1.2":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735"
|
||||
integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==
|
||||
|
||||
"@protobufjs/codegen@^2.0.4":
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb"
|
||||
integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==
|
||||
|
||||
"@protobufjs/eventemitter@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70"
|
||||
integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A=
|
||||
|
||||
"@protobufjs/fetch@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45"
|
||||
integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=
|
||||
dependencies:
|
||||
"@protobufjs/aspromise" "^1.1.1"
|
||||
"@protobufjs/inquire" "^1.1.0"
|
||||
|
||||
"@protobufjs/float@^1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1"
|
||||
integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=
|
||||
|
||||
"@protobufjs/inquire@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089"
|
||||
integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=
|
||||
|
||||
"@protobufjs/path@^1.1.2":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d"
|
||||
integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=
|
||||
|
||||
"@protobufjs/pool@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54"
|
||||
integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=
|
||||
|
||||
"@protobufjs/utf8@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
|
||||
integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=
|
||||
|
||||
"@types/long@^4.0.0":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
|
||||
integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==
|
||||
|
||||
"@types/node@^10.1.0":
|
||||
version "10.17.58"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.58.tgz#10682f6016fd866725c36d22ce6bbbd029bf4545"
|
||||
integrity sha512-Dn5RBxLohjdHFj17dVVw3rtrZAeXeWg+LQfvxDIW/fdPkSiuQk7h3frKMYtsQhtIW42wkErDcy9UMVxhGW4O7w==
|
||||
|
||||
long@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
|
||||
integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==
|
@ -0,0 +1,3 @@
|
||||
--This empty file causes the scheduler.lua to load clientside
|
||||
--scheduler.lua when loaded inside the sessionmanager resource currently manages remote callbacks.
|
||||
--Without this, callbacks will only work server->client and not client->server.
|
@ -0,0 +1,13 @@
|
||||
-- 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 the "host lock" for non-OneSync servers. Do not disable.'
|
||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||
|
||||
fx_version 'cerulean'
|
||||
games { 'gta4', 'gta5' }
|
||||
|
||||
server_script 'server/host_lock.lua'
|
||||
client_script 'client/empty.lua'
|
@ -0,0 +1,69 @@
|
||||
-- whitelist c2s events
|
||||
RegisterServerEvent('hostingSession')
|
||||
RegisterServerEvent('hostedSession')
|
||||
|
||||
-- event handler for pre-session 'acquire'
|
||||
local currentHosting
|
||||
local hostReleaseCallbacks = {}
|
||||
|
||||
-- TODO: add a timeout for the hosting lock to be held
|
||||
-- TODO: add checks for 'fraudulent' conflict cases of hosting attempts (typically whenever the host can not be reached)
|
||||
AddEventHandler('hostingSession', function()
|
||||
-- if the lock is currently held, tell the client to await further instruction
|
||||
if currentHosting then
|
||||
TriggerClientEvent('sessionHostResult', source, 'wait')
|
||||
|
||||
-- register a callback for when the lock is freed
|
||||
table.insert(hostReleaseCallbacks, function()
|
||||
TriggerClientEvent('sessionHostResult', source, 'free')
|
||||
end)
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
-- if the current host was last contacted less than a second ago
|
||||
if GetHostId() then
|
||||
if GetPlayerLastMsg(GetHostId()) < 1000 then
|
||||
TriggerClientEvent('sessionHostResult', source, 'conflict')
|
||||
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
hostReleaseCallbacks = {}
|
||||
|
||||
currentHosting = source
|
||||
|
||||
TriggerClientEvent('sessionHostResult', source, 'go')
|
||||
|
||||
-- set a timeout of 5 seconds
|
||||
SetTimeout(5000, function()
|
||||
if not currentHosting then
|
||||
return
|
||||
end
|
||||
|
||||
currentHosting = nil
|
||||
|
||||
for _, cb in ipairs(hostReleaseCallbacks) do
|
||||
cb()
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
AddEventHandler('hostedSession', function()
|
||||
-- check if the client is the original locker
|
||||
if currentHosting ~= source then
|
||||
-- TODO: drop client as they're clearly lying
|
||||
print(currentHosting, '~=', source)
|
||||
return
|
||||
end
|
||||
|
||||
-- free the host lock (call callbacks and remove the lock value)
|
||||
for _, cb in ipairs(hostReleaseCallbacks) do
|
||||
cb()
|
||||
end
|
||||
|
||||
currentHosting = nil
|
||||
end)
|
||||
|
||||
EnableEnhancedHostSupport(true)
|
13
resources/[cfx-default]/[test]/fivem/fxmanifest.lua
Normal file
13
resources/[cfx-default]/[test]/fivem/fxmanifest.lua
Normal file
@ -0,0 +1,13 @@
|
||||
-- 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 compatibility resource to load basic-gamemode.'
|
||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||
|
||||
-- compatibility wrapper
|
||||
fx_version 'cerulean'
|
||||
game 'common'
|
||||
|
||||
dependency 'basic-gamemode'
|
14
resources/[custom_script]/2na_core/Client/events.lua
Normal file
14
resources/[custom_script]/2na_core/Client/events.lua
Normal file
@ -0,0 +1,14 @@
|
||||
RegisterNetEvent("2na_core:Client:HandleCallback")
|
||||
AddEventHandler("2na_core:Client:HandleCallback", function(name, data)
|
||||
if TwoNa.Callbacks[name] then
|
||||
TwoNa.Callbacks[name](data)
|
||||
TwoNa.Callbacks[name] = nil
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterNetEvent("2na_core:getSharedObject")
|
||||
AddEventHandler("2na_core:getSharedObject", function(cb)
|
||||
if cb and type(cb) == 'function' then
|
||||
cb(TwoNa)
|
||||
end
|
||||
end)
|
56
resources/[custom_script]/2na_core/Client/main.lua
Normal file
56
resources/[custom_script]/2na_core/Client/main.lua
Normal file
@ -0,0 +1,56 @@
|
||||
TwoNa = {}
|
||||
TwoNa.Callbacks = {}
|
||||
TwoNa.Framework = nil
|
||||
TwoNa.Game = {}
|
||||
TwoNa.Functions = TwoNaShared.Functions
|
||||
TwoNa.Types = TwoNaShared.Types
|
||||
|
||||
TwoNa.TriggerServerCallback = function(name, payload, func)
|
||||
if not func then
|
||||
func = function() end
|
||||
end
|
||||
|
||||
TwoNa.Callbacks[name] = func
|
||||
|
||||
TriggerServerEvent("2na_core:Server:HandleCallback", name, payload)
|
||||
end
|
||||
|
||||
TwoNa.Game.GetVehicleProperties = function(vehicle)
|
||||
if Config.Framework == "ESX" then
|
||||
return TwoNa.Framework.Game.GetVehicleProperties(vehicle)
|
||||
elseif Config.Framework == "QBCore" then
|
||||
return TwoNa.Framework.Functions.GetVehicleProperties(vehicle)
|
||||
end
|
||||
end
|
||||
|
||||
TwoNa.Game.SetVehicleProperties = function(vehicle, props)
|
||||
if Config.Framework == "ESX" then
|
||||
return TwoNa.Framework.Game.SetVehicleProperties(vehicle, props)
|
||||
elseif Config.Framework == "QBCore" then
|
||||
return TwoNa.Framework.Functions.SetVehicleProperties(vehicle, props)
|
||||
end
|
||||
end
|
||||
|
||||
TwoNa.Draw3DText = function(x, y, z, scale, text, hideBox)
|
||||
local onScreen,_x,_y=World3dToScreen2d(x,y,z)
|
||||
local px,py,pz=table.unpack(GetGameplayCamCoords())
|
||||
|
||||
SetTextScale(0.40, 0.40)
|
||||
SetTextFont(4)
|
||||
SetTextProportional(1)
|
||||
SetTextColour(255, 255, 255, 215)
|
||||
SetTextEntry("STRING")
|
||||
SetTextCentre(1)
|
||||
AddTextComponentString(text)
|
||||
DrawText(_x,_y)
|
||||
|
||||
if not hideBox then
|
||||
local factor = (string.len(text)) / 350
|
||||
|
||||
DrawRect(_x,_y+0.0140, 0.025+ factor, 0.03, 0, 0, 0, 105)
|
||||
end
|
||||
end
|
||||
|
||||
exports("getSharedObject", function()
|
||||
return TwoNa
|
||||
end)
|
9
resources/[custom_script]/2na_core/Client/threads.lua
Normal file
9
resources/[custom_script]/2na_core/Client/threads.lua
Normal file
@ -0,0 +1,9 @@
|
||||
Citizen.CreateThread(function()
|
||||
while TwoNa.Framework == nil do
|
||||
if Config.Framework then
|
||||
TwoNa.Framework = Config.Framework.GetFramework()
|
||||
end
|
||||
|
||||
Citizen.Wait(1)
|
||||
end
|
||||
end)
|
58
resources/[custom_script]/2na_core/Common/main.lua
Normal file
58
resources/[custom_script]/2na_core/Common/main.lua
Normal file
@ -0,0 +1,58 @@
|
||||
TwoNaShared = {}
|
||||
TwoNaShared.Functions = {}
|
||||
|
||||
TwoNaShared.Functions.Trim = function(str)
|
||||
return (str:gsub("^%s*(.-)%s*$", "%1"))
|
||||
end
|
||||
|
||||
TwoNaShared.Functions.Capitalize = function(str)
|
||||
return string.upper(str:sub(1,1)) .. str:sub(2)
|
||||
end
|
||||
|
||||
TwoNaShared.Functions.Includes = function(arr, target)
|
||||
local includes = false
|
||||
|
||||
for _, v in ipairs(arr) do
|
||||
if v == target then
|
||||
includes = true
|
||||
end
|
||||
end
|
||||
|
||||
return includes
|
||||
end
|
||||
|
||||
TwoNaShared.Functions.GetFramework = function()
|
||||
local availableFramework = nil
|
||||
|
||||
for k,v in ipairs(TwoNaShared.Types.Frameworks) do
|
||||
if GetResourceState(v.ResourceName) == "starting" or GetResourceState(v.ResourceName) == "started" then
|
||||
availableFramework = v
|
||||
end
|
||||
end
|
||||
|
||||
if not availableFramework then
|
||||
TwoNaShared.Functions.Log("^1Intet understøttet framework fundet! Kontroller at framework-navnet ikke er ændret.^7")
|
||||
end
|
||||
|
||||
return availableFramework
|
||||
end
|
||||
|
||||
TwoNaShared.Functions.GetDatabase = function()
|
||||
local availableDatabase = nil
|
||||
|
||||
for k,v in ipairs(TwoNaShared.Types.Databases) do
|
||||
if GetResourceState(v.ResourceName) == "starting" or GetResourceState(v.ResourceName) == "started" then
|
||||
availableDatabase = v
|
||||
end
|
||||
end
|
||||
|
||||
if not availableDatabase then
|
||||
TwoNaShared.Functions.Log("^1Kunne ikke finde en understøttet database! Kontroller at database-scriptnavnet ikke er ændret.^7")
|
||||
end
|
||||
|
||||
return availableDatabase
|
||||
end
|
||||
|
||||
TwoNaShared.Functions.Log = function(str)
|
||||
print("^4[2na_core]^7: " .. TwoNaShared.Functions.Trim(str))
|
||||
end
|
26
resources/[custom_script]/2na_core/Common/types.lua
Normal file
26
resources/[custom_script]/2na_core/Common/types.lua
Normal file
@ -0,0 +1,26 @@
|
||||
TwoNaShared.Types = {}
|
||||
|
||||
TwoNaShared.Types.Frameworks = {
|
||||
{
|
||||
Name = "ESX",
|
||||
ResourceName = "es_extended",
|
||||
GetFramework = function() return exports["es_extended"]:getSharedObject() end
|
||||
},
|
||||
{
|
||||
Name = "QBCore",
|
||||
ResourceName = "qb-core",
|
||||
GetFramework = function() return exports["qb-core"]:GetCoreObject() end
|
||||
}
|
||||
}
|
||||
|
||||
TwoNaShared.Types.Databases = {
|
||||
{
|
||||
Name = "MYSQL-ASYNC",
|
||||
ResourceName = "mysql_async"
|
||||
},
|
||||
{
|
||||
Name = "OXMYSQL",
|
||||
ResourceName = "oxmysql"
|
||||
}
|
||||
}
|
||||
|
12
resources/[custom_script]/2na_core/Config.lua
Normal file
12
resources/[custom_script]/2na_core/Config.lua
Normal file
@ -0,0 +1,12 @@
|
||||
Config = {}
|
||||
|
||||
local Framework = "QBCore" -- Supports both ESX and QBCore
|
||||
local Database = "OXMYSQL" -- Supports both mysql-async and oxmysql
|
||||
|
||||
|
||||
--------- DO NOT MODIFY ---------
|
||||
|
||||
Config.Framework = TwoNaShared.Functions.GetFramework(Framework)
|
||||
Config.Database = TwoNaShared.Functions.GetDatabase(Database)
|
||||
|
||||
---------------------------------
|
16
resources/[custom_script]/2na_core/Server/events.lua
Normal file
16
resources/[custom_script]/2na_core/Server/events.lua
Normal file
@ -0,0 +1,16 @@
|
||||
RegisterNetEvent("2na_core:Server:HandleCallback")
|
||||
AddEventHandler("2na_core:Server:HandleCallback", function(name, payload)
|
||||
local source = source
|
||||
|
||||
if TwoNa.Callbacks[name] then
|
||||
TwoNa.Callbacks[name](source, payload, function(cb)
|
||||
TriggerClientEvent("2na_core:Client:HandleCallback", source, name, cb)
|
||||
end)
|
||||
end
|
||||
end)
|
||||
|
||||
AddEventHandler("onResourceStart", function(resourceName)
|
||||
if GetCurrentResourceName() == resourceName then
|
||||
TwoNa.CheckUpdate()
|
||||
end
|
||||
end)
|
456
resources/[custom_script]/2na_core/Server/main.lua
Normal file
456
resources/[custom_script]/2na_core/Server/main.lua
Normal file
@ -0,0 +1,456 @@
|
||||
TwoNa = {}
|
||||
TwoNa.Callbacks = {}
|
||||
TwoNa.Players = {}
|
||||
TwoNa.Framework = nil
|
||||
TwoNa.Functions = TwoNaShared.Functions
|
||||
TwoNa.Types = TwoNaShared.Types
|
||||
TwoNa.Vehicles = nil
|
||||
TwoNa.MySQL = {
|
||||
Async = {},
|
||||
Sync = {}
|
||||
}
|
||||
|
||||
TwoNa.RegisterServerCallback = function(name, func)
|
||||
TwoNa.Callbacks[name] = func
|
||||
end
|
||||
|
||||
TwoNa.TriggerCallback = function(name, source, payload, cb)
|
||||
if not cb then
|
||||
cb = function() end
|
||||
end
|
||||
|
||||
if TwoNa.Callbacks[name] then
|
||||
TwoNa.Callbacks[name](source, payload, cb)
|
||||
end
|
||||
end
|
||||
|
||||
TwoNa.MySQL.Async.Fetch = function(query, variables, cb)
|
||||
if not cb or type(cb) ~= 'function' then
|
||||
cb = function() end
|
||||
end
|
||||
|
||||
if Config.Database.Name == "MYSQL-ASYNC" then
|
||||
return exports["mysql-async"]:mysql_fetch_all(query, variables, cb)
|
||||
elseif Config.Database.Name == "OXMYSQL" then
|
||||
return exports["oxmysql"]:prepare(query, variables, cb)
|
||||
end
|
||||
end
|
||||
|
||||
TwoNa.MySQL.Sync.Fetch = function(query, variables)
|
||||
local result = {}
|
||||
local finishedQuery = false
|
||||
local cb = function(r)
|
||||
result = r
|
||||
finishedQuery = true
|
||||
end
|
||||
|
||||
if Config.Database.Name == "MYSQL-ASYNC" then
|
||||
exports["mysql-async"]:mysql_fetch_all(query, variables, cb)
|
||||
elseif Config.Database.Name == "OXMYSQL" then
|
||||
exports["oxmysql"]:execute(query, variables, cb)
|
||||
end
|
||||
|
||||
while not finishedQuery do
|
||||
Citizen.Wait(0)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
TwoNa.MySQL.Async.Execute = function(query, variables, cb)
|
||||
if Config.Database.Name == "MYSQL-ASYNC" then
|
||||
return exports["mysql-async"]:mysql_execute(query, variables, cb)
|
||||
elseif Config.Database.Name == "OXMYSQL" then
|
||||
return exports["oxmysql"]:update(query, variables, cb)
|
||||
end
|
||||
end
|
||||
|
||||
TwoNa.MySQL.Sync.Execute = function(query, variables)
|
||||
local result = {}
|
||||
local finishedQuery = false
|
||||
local cb = function(r)
|
||||
result = r
|
||||
finishedQuery = true
|
||||
end
|
||||
|
||||
if Config.Database.Name == "MYSQL-ASYNC" then
|
||||
exports["mysql-async"]:mysql_execute(query, variables, cb)
|
||||
elseif Config.Database.Name == "OXMYSQL" then
|
||||
exports["oxmysql"]:execute(query, variables, cb)
|
||||
end
|
||||
|
||||
while not finishedQuery do
|
||||
Citizen.Wait(0)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
TwoNa.IsPlayerAvailable = function(source)
|
||||
local available = false
|
||||
|
||||
if type(source) == 'number' then
|
||||
if Config.Framework.Name == "ESX" then
|
||||
available = TwoNa.Framework.GetPlayerFromId(source) ~= nil
|
||||
elseif Config.Framework.Name == "QBCore" then
|
||||
available = TwoNa.Framework.Functions.GetPlayer(source) ~= nil
|
||||
end
|
||||
elseif type(source) == 'string' then
|
||||
if Config.Framework.Name == "ESX" then
|
||||
available = TwoNa.Framework.GetPlayerFromIdentifier(identifier) ~= nil
|
||||
elseif Config.Framework.Name == "QBCore" then
|
||||
available = TwoNa.Framework.Functions.GetSource(identifier) ~= nil
|
||||
end
|
||||
end
|
||||
|
||||
return available
|
||||
end
|
||||
|
||||
TwoNa.GetPlayerIdentifier = function(source)
|
||||
if TwoNa.IsPlayerAvailable(source) then
|
||||
if Config.Framework.Name == "ESX" then
|
||||
local xPlayer = TwoNa.Framework.GetPlayerFromId(source)
|
||||
return xPlayer.getIdentifier()
|
||||
elseif Config.Framework.Name == "QBCore" then
|
||||
return TwoNa.Framework.Functions.GetIdentifier(source, 'license')
|
||||
end
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
TwoNa.GetCharacterIdentifier = function(source)
|
||||
if TwoNa.IsPlayerAvailable(source) then
|
||||
if Config.Framework.Name == "ESX" then
|
||||
local xPlayer = TwoNa.Framework.GetPlayerFromId(source)
|
||||
return xPlayer.identifier
|
||||
elseif Config.Framework.Name == "QBCore" then
|
||||
return TwoNa.Framework.Functions.GetPlayer(source).PlayerData.citizenid
|
||||
end
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
TwoNa.CreatePlayer = function(xPlayer)
|
||||
local player = {}
|
||||
|
||||
if not xPlayer then
|
||||
return nil
|
||||
end
|
||||
|
||||
if Config.Framework.Name == "ESX" then
|
||||
player.name = xPlayer.getName()
|
||||
player.accounts = {}
|
||||
for _,v in ipairs(xPlayer.getAccounts()) do
|
||||
if v.name == 'bank' then
|
||||
player.accounts["bank"] = v.money
|
||||
elseif v.name == 'money' then
|
||||
player.accounts["cash"] = v.money
|
||||
end
|
||||
end
|
||||
if xPlayer.variables.sex == 'm' then
|
||||
player.gender = 'male'
|
||||
else
|
||||
player.gender = 'female'
|
||||
end
|
||||
player.job = {
|
||||
name = xPlayer.getJob().name,
|
||||
label = xPlayer.getJob().label
|
||||
}
|
||||
player.birth = xPlayer.variables.dateofbirth
|
||||
|
||||
player.getBank = function()
|
||||
return xPlayer.getAccount("bank").money
|
||||
end
|
||||
player.getMoney = xPlayer.getMoney
|
||||
player.addBank = function(amount)
|
||||
xPlayer.addAccountMoney('bank', amount)
|
||||
end
|
||||
player.addMoney = xPlayer.addMoney
|
||||
player.removeBank = function(amount)
|
||||
xPlayer.removeAccountMoney('bank', amount)
|
||||
end
|
||||
player.removeMoney = xPlayer.removeMoney
|
||||
elseif Config.Framework.Name == "QBCore" then
|
||||
player.name = xPlayer.PlayerData.charinfo.firstname .. " " .. xPlayer.PlayerData.charinfo.lastname
|
||||
player.accounts = {
|
||||
bank = xPlayer.PlayerData.money.bank,
|
||||
cash = xPlayer.PlayerData.money.cash
|
||||
}
|
||||
if xPlayer.PlayerData.charinfo.gender == 0 then
|
||||
player.gender = 'male'
|
||||
else
|
||||
player.gender = 'female'
|
||||
end
|
||||
player.job = {
|
||||
name = xPlayer.PlayerData.job.name,
|
||||
label = xPlayer.PlayerData.job.label
|
||||
}
|
||||
player.birth = xPlayer.PlayerData.charinfo.birthdate
|
||||
|
||||
player.getBank = function()
|
||||
return xPlayer.Functions.GetMoney("bank")
|
||||
end
|
||||
player.getMoney = function()
|
||||
return xPlayer.Functions.GetMoney("cash")
|
||||
end
|
||||
player.addBank = function(amount)
|
||||
return xPlayer.Functions.AddMoney("bank", amount, "")
|
||||
end
|
||||
player.addMoney = function(amount)
|
||||
return xPlayer.Functions.AddMoney("cash", amount, "")
|
||||
end
|
||||
player.removeBank = function(amount)
|
||||
return xPlayer.Functions.RemoveMoney("bank", amount, "")
|
||||
end
|
||||
player.removeMoney = function(amount)
|
||||
return xPlayer.Functions.RemoveMoney("cash", amount, "")
|
||||
end
|
||||
end
|
||||
|
||||
return player
|
||||
end
|
||||
|
||||
TwoNa.GetPlayer = function(source)
|
||||
if TwoNa.IsPlayerAvailable(source) then
|
||||
local xPlayer = nil
|
||||
|
||||
if Config.Framework.Name == "ESX" then
|
||||
xPlayer = TwoNa.Framework.GetPlayerFromId(source)
|
||||
elseif Config.Framework.Name == "QBCore" then
|
||||
xPlayer = TwoNa.Framework.Functions.GetPlayer(source)
|
||||
end
|
||||
|
||||
return TwoNa.CreatePlayer(xPlayer)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
TwoNa.GetPlayerFromIdentifier = function(identifier)
|
||||
if TwoNa.IsPlayerAvailable(identifier) then
|
||||
local xPlayer = nil
|
||||
|
||||
if Config.Framework.Name == "ESX" then
|
||||
xPlayer = TwoNa.Framework.GetPlayerFromIdentifier(identifier)
|
||||
elseif Config.Framework.Name == "QBCore" then
|
||||
xPlayer = TwoNa.Framework.Functions.GetPlayer(TwoNa.Framework.Functions.GetSource(identifier))
|
||||
end
|
||||
|
||||
return TwoNa.CreatePlayer(xPlayer)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
TwoNa.GetAllVehicles = function(force)
|
||||
if TwoNa.Vehicles and not force then
|
||||
return TwoNa.Vehicles
|
||||
end
|
||||
|
||||
local vehicles = {}
|
||||
|
||||
if Config.Framework.Name == "ESX" then
|
||||
local data = TwoNa.MySQL.Sync.Fetch("SELECT * FROM vehicles", {})
|
||||
|
||||
for k, v in ipairs(data) do
|
||||
vehicles[v.model] = {
|
||||
model = v.model,
|
||||
name = v.name,
|
||||
category = v.category,
|
||||
price = v.price
|
||||
}
|
||||
end
|
||||
|
||||
elseif Config.Framework.Name == "QBCore" then
|
||||
for k,v in pairs(TwoNa.Framework.Shared.Vehicles) do
|
||||
vehicles[k] = {
|
||||
model = k,
|
||||
name = v.name,
|
||||
category = v.category,
|
||||
price = v.price
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
TwoNa.Vehicles = vehicles
|
||||
|
||||
return vehicles
|
||||
end
|
||||
|
||||
TwoNa.GetVehicleByName = function(name)
|
||||
local vehicles = TwoNa.GetAllVehicles(false)
|
||||
local targetVehicle = nil
|
||||
|
||||
for k,v in pairs(vehicles) do
|
||||
if v.name == name then
|
||||
targetVehicle = v
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return targetVehicle
|
||||
end
|
||||
|
||||
TwoNa.GetVehicleByHash = function(hash)
|
||||
local vehicles = TwoNa.GetAllVehicles(false)
|
||||
local targetVehicle = nil
|
||||
|
||||
for k,v in pairs(vehicles) do
|
||||
if GetHashKey(v.model) == hash then
|
||||
targetVehicle = v
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return targetVehicle
|
||||
end
|
||||
|
||||
TwoNa.GetPlayerVehicles = function(source)
|
||||
local identifier = TwoNa.GetPlayerIdentifier(source)
|
||||
|
||||
if identifier then
|
||||
local vehicles = TwoNa.GetAllVehicles(false)
|
||||
local playerVehicles = {}
|
||||
|
||||
if Config.Framework.Name == "ESX" then
|
||||
local data = TwoNa.MySQL.Sync.Fetch("SELECT * FROM owned_vehicles WHERE owner = @identifier", { ["@identifier"] = identifier })
|
||||
|
||||
for k,v in ipairs(data) do
|
||||
local vehicleDetails = TwoNa.GetVehicleByHash(json.decode(v.vehicle).model)
|
||||
|
||||
if not vehicleDetails then
|
||||
vehicleDetails = {
|
||||
name = nil,
|
||||
model = json.decode(v.vehicle).model,
|
||||
category = nil,
|
||||
price = nil
|
||||
}
|
||||
end
|
||||
|
||||
table.insert(playerVehicles, {
|
||||
name = vehicleDetails.name,
|
||||
model = vehicleDetails.model,
|
||||
category = vehicleDetails.category,
|
||||
plate = v.plate,
|
||||
fuel = v.fuel or 100,
|
||||
price = vehicleDetails.price,
|
||||
properties = json.decode(v.vehicle),
|
||||
stored = v.stored,
|
||||
garage = v.garage or nil
|
||||
})
|
||||
end
|
||||
elseif Config.Framework.Name == "QBCore" then
|
||||
local data = TwoNa.MySQL.Sync.Fetch("SELECT * FROM player_vehicles WHERE license = @identifier", { ["@identifier"] = identifier })
|
||||
|
||||
for k,v in ipairs(data) do
|
||||
if v.stored == 1 then
|
||||
v.stored = true
|
||||
else
|
||||
v.stored = false
|
||||
end
|
||||
|
||||
table.insert(playerVehicles, {
|
||||
name = vehicles[v.vehicle].name,
|
||||
model = vehicles[v.vehicle].model,
|
||||
category = vehicles[v.vehicle].category,
|
||||
plate = v.plate,
|
||||
fuel = v.fuel,
|
||||
price = vehicles[v.vehicle].price or -1,
|
||||
properties = json.decode(v.mods),
|
||||
stored = v.stored,
|
||||
garage = v.garage
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
return playerVehicles
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
TwoNa.UpdatePlayerVehicle = function(source, plate, vehicleData)
|
||||
local identifier = TwoNa.GetPlayerIdentifier(source)
|
||||
|
||||
if identifier then
|
||||
local playerVehicles = TwoNa.GetPlayerVehicles(source)
|
||||
local targetVehicle = nil
|
||||
|
||||
for k,v in ipairs(playerVehicles) do
|
||||
if v.plate == plate then
|
||||
targetVehicle = v
|
||||
end
|
||||
end
|
||||
|
||||
if not targetVehicle then
|
||||
return false
|
||||
end
|
||||
|
||||
local query = nil
|
||||
if Config.Framework.Name == "ESX" then
|
||||
query = "UPDATE owned_vehicles SET vehicle = @props, stored = @stored, garage = @garage WHERE owner = @identifier AND plate = @plate"
|
||||
elseif Config.Framework.Name == "QBCore" then
|
||||
query = "UPDATE player_vehicles SET mods = @props, stored = @stored, garage = @garage WHERE license = @identifier AND plate = @plate"
|
||||
end
|
||||
|
||||
if query then
|
||||
TwoNa.MySQL.Sync.Execute(query, {
|
||||
["@props"] = json.encode(vehicleData.properties or targetVehicle.properties),
|
||||
["@stored"] = vehicleData.stored,
|
||||
["@garage"] = vehicleData.garage,
|
||||
["@identifier"] = identifier,
|
||||
["@plate"] = plate
|
||||
})
|
||||
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
TwoNa.UpdateVehicleOwner = function(plate, target)
|
||||
local identifier = TwoNa.GetPlayerIdentifier(target)
|
||||
|
||||
if not identifier then
|
||||
return false
|
||||
end
|
||||
|
||||
local query = nil
|
||||
if Config.Framework.Name == "ESX" then
|
||||
query = "UPDATE owned_vehicles SET owner = @newOwner WHERE plate = @plate"
|
||||
elseif Config.Framework.Name == "QBCore" then
|
||||
query = "UPDATE player_vehicles SET license = @newOwner WHERE plate = @plate"
|
||||
end
|
||||
|
||||
if query then
|
||||
TwoNa.MySQL.Sync.Execute(query, { ["@newOwner"] = identifier, ["@plate"] = plate })
|
||||
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
TwoNa.CheckUpdate = function()
|
||||
PerformHttpRequest("https://api.github.com/repos/tunasayin/2na_core/releases/latest", function(errorCode, rawData, headers)
|
||||
if rawData ~= nil then
|
||||
local data = json.decode(tostring(rawData))
|
||||
local version = string.gsub(data.tag_name, "v", ""):gsub("%.", "")
|
||||
local installedVersion = GetResourceMetadata(GetCurrentResourceName(), "version", 0):gsub("%.", "")
|
||||
|
||||
if tonumber(installedVersion) < tonumber(version) then
|
||||
TwoNa.Functions.Log("^3An update is available! You can download the update from this link: " .. data.html_url .. "^7")
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
exports("getSharedObject", function()
|
||||
return TwoNa
|
||||
end)
|
9
resources/[custom_script]/2na_core/Server/threads.lua
Normal file
9
resources/[custom_script]/2na_core/Server/threads.lua
Normal file
@ -0,0 +1,9 @@
|
||||
Citizen.CreateThread(function()
|
||||
while TwoNa.Framework == nil do
|
||||
if Config.Framework then
|
||||
TwoNa.Framework = Config.Framework.GetFramework()
|
||||
end
|
||||
|
||||
Citizen.Wait(1)
|
||||
end
|
||||
end)
|
21
resources/[custom_script]/2na_core/fxmanifest.lua
Normal file
21
resources/[custom_script]/2na_core/fxmanifest.lua
Normal file
@ -0,0 +1,21 @@
|
||||
fx_version 'cerulean'
|
||||
|
||||
game 'gta5'
|
||||
|
||||
author 'tunasayin'
|
||||
description 'Core script needed for almost all 2na scripts.'
|
||||
|
||||
version '0.2.8'
|
||||
|
||||
shared_scripts {
|
||||
'Common/*.lua',
|
||||
'Config.lua'
|
||||
}
|
||||
|
||||
client_scripts {
|
||||
'Client/*.lua'
|
||||
}
|
||||
|
||||
server_scripts {
|
||||
'Server/*.lua'
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user