This commit is contained in:
Hawk 2024-12-29 20:48:41 +01:00
parent ebdae82166
commit 2678716618
No known key found for this signature in database
GPG Key ID: 2890D5366F8BAC14
855 changed files with 301591 additions and 0 deletions

View 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 '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'

View File

@ -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 }
--

View 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 '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'

View File

@ -0,0 +1,4 @@
-- FIB Server-rum
spawnpoint 'a_m_y_skater_01' { x = 153.14, y = -764.94, z = 258.15 }
--

View File

@ -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.'

View File

@ -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 }

View File

@ -0,0 +1,4 @@
AddEventHandler('onClientMapStart', function()
exports.spawnmanager:setAutoSpawn(true)
exports.spawnmanager:forceRespawn()
end)

View 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 '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'

View File

@ -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)

View File

@ -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'

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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'

View 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)

View File

@ -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'
}

View 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)

View File

@ -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'

View File

@ -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 &lt;%d&gt;'):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

View File

@ -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)

View File

@ -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()

View File

@ -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.

View File

@ -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 = {
["&"] = "&amp;",
["<"] = "&lt;",
[">"] = "&gt;",
['"'] = "&quot;",
["'"] = "&#39;",
["/"] = "&#47;"
}
local CODE_ENTITIES = {
["{"] = "&#123;",
["}"] = "&#125;",
["&"] = "&amp;",
["<"] = "&lt;",
[">"] = "&gt;",
['"'] = "&quot;",
["'"] = "&#39;",
["/"] = "&#47;"
}
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

View File

View File

@ -0,0 +1,3 @@
node_modules/
yarn-error.log
.yarn.installed

View 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.

View 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)
```

View File

@ -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'
}
};

View 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}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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'

View File

@ -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"
}
}

View File

@ -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'
};

View File

@ -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
}));
});

View File

@ -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": [
]
}

View File

@ -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}`);
});

View File

@ -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": [
]
}

View File

@ -0,0 +1 @@
export function setHttpCallback(requestHandler: any): void;

View File

@ -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/'
},
};

View File

@ -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>

View 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();

View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"outDir": "./",
"noImplicitAny": false,
"module": "es6",
"moduleResolution": "node",
"target": "es6",
"allowJs": true,
"lib": [
"es2016",
"dom"
]
},
"include": [
"./**/*"
],
"exclude": []
}

File diff suppressed because it is too large Load Diff

View 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.'

View File

@ -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)

View File

@ -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

View File

@ -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

View 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 '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.'

View 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)

View File

@ -0,0 +1,2 @@
.yarn.installed
node_modules/

View 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 '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'

View File

@ -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"
}
}

View File

@ -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);

View File

@ -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, {});
});
};

File diff suppressed because it is too large Load Diff

View File

@ -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'

View File

@ -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);

File diff suppressed because one or more lines are too long

View 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

View 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'

View 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)

View File

@ -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

View File

@ -0,0 +1,11 @@
Citizen.CreateThread(function()
while true do
Wait(0)
if NetworkIsSessionStarted() then
TriggerServerEvent('hardcap:playerActivated')
return
end
end
end)

View 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.'

View 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)

View 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.'

View 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)

View 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)

View File

@ -0,0 +1 @@
data.json

View 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'
}

View 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() ];
}
});

View 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)

View 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

View 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)

View 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)

View 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)

View 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">&nbsp;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>

View 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>

View File

@ -0,0 +1,2 @@
node_modules/
.yarn.installed

View File

@ -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'

View File

@ -0,0 +1,6 @@
{
"private": true,
"dependencies": {
"@citizenfx/protobufjs": "6.8.8"
}
}

View 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;
};

View File

@ -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);
}
});
});

View File

@ -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==

View File

@ -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.

View 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 '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'

View File

@ -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)

View 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'

View 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)

View 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)

View 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)

View 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

View 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"
}
}

View 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)
---------------------------------

View 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)

View 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)

View 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)

View 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