This commit is contained in:
2026-07-07 21:08:52 +02:00
commit 4c20cfc716
2613 changed files with 318021 additions and 0 deletions

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

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 'adamant'
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 'adamant'
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 'adamant'
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 'adamant'
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 'bodacious'
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 = 'http://' .. 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(`http://${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 'adamant'
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,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)