0.0.1
This commit is contained in:
2
resources/[cfx-default]/[system]/[builders]/webpack/.gitignore
vendored
Normal file
2
resources/[cfx-default]/[system]/[builders]/webpack/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.yarn.installed
|
||||
node_modules/
|
||||
@@ -0,0 +1,13 @@
|
||||
-- This resource is part of the default Cfx.re asset pack (cfx-server-data)
|
||||
-- Altering or recreating for local use only is strongly discouraged.
|
||||
|
||||
version '1.0.0'
|
||||
author 'Cfx.re <root@cfx.re>'
|
||||
description 'Builds resources with webpack. To learn more: https://webpack.js.org'
|
||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||
|
||||
dependency 'yarn'
|
||||
server_script 'webpack_builder.js'
|
||||
|
||||
fx_version 'adamant'
|
||||
game 'common'
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "webpack-builder",
|
||||
"version": "1.0.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"async": "^3.1.0",
|
||||
"webpack": "^4.41.2",
|
||||
"worker-farm": "^1.7.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const workerFarm = require('worker-farm');
|
||||
const async = require('async');
|
||||
let buildingInProgress = false;
|
||||
let currentBuildingModule = '';
|
||||
|
||||
// some modules will not like the custom stack trace logic
|
||||
const ops = Error.prepareStackTrace;
|
||||
Error.prepareStackTrace = undefined;
|
||||
|
||||
const webpackBuildTask = {
|
||||
shouldBuild(resourceName) {
|
||||
const numMetaData = GetNumResourceMetadata(resourceName, 'webpack_config');
|
||||
|
||||
if (numMetaData > 0) {
|
||||
for (let i = 0; i < numMetaData; i++) {
|
||||
const configName = GetResourceMetadata(resourceName, 'webpack_config');
|
||||
|
||||
if (shouldBuild(configName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
function loadCache(config) {
|
||||
const cachePath = `cache/${resourceName}/${config.replace(/\//g, '_')}.json`;
|
||||
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(cachePath, {encoding: 'utf8'}));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldBuild(config) {
|
||||
const cache = loadCache(config);
|
||||
|
||||
if (!cache) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const file of cache) {
|
||||
const stats = getStat(file.name);
|
||||
|
||||
if (!stats ||
|
||||
stats.mtime !== file.stats.mtime ||
|
||||
stats.size !== file.stats.size ||
|
||||
stats.inode !== file.stats.inode) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getStat(path) {
|
||||
try {
|
||||
const stat = fs.statSync(path);
|
||||
|
||||
return stat ? {
|
||||
mtime: stat.mtimeMs,
|
||||
size: stat.size,
|
||||
inode: stat.ino,
|
||||
} : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
build(resourceName, cb) {
|
||||
let buildWebpack = async () => {
|
||||
let error = null;
|
||||
const configs = [];
|
||||
const promises = [];
|
||||
const numMetaData = GetNumResourceMetadata(resourceName, 'webpack_config');
|
||||
|
||||
for (let i = 0; i < numMetaData; i++) {
|
||||
configs.push(GetResourceMetadata(resourceName, 'webpack_config', i));
|
||||
}
|
||||
|
||||
for (const configName of configs) {
|
||||
const configPath = GetResourcePath(resourceName) + '/' + configName;
|
||||
|
||||
const cachePath = `cache/${resourceName}/${configName.replace(/\//g, '_')}.json`;
|
||||
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(cachePath));
|
||||
} catch {
|
||||
}
|
||||
|
||||
const config = require(configPath);
|
||||
|
||||
const workers = workerFarm(require.resolve('./webpack_runner'));
|
||||
|
||||
if (config) {
|
||||
const resourcePath = path.resolve(GetResourcePath(resourceName));
|
||||
|
||||
while (buildingInProgress) {
|
||||
console.log(`webpack is busy: we are waiting to compile ${resourceName} (${configName})`);
|
||||
await sleep(3000);
|
||||
}
|
||||
|
||||
console.log(`${resourceName}: started building ${configName}`);
|
||||
|
||||
buildingInProgress = true;
|
||||
currentBuildingModule = resourceName;
|
||||
|
||||
promises.push(new Promise((resolve, reject) => {
|
||||
workers({
|
||||
configPath,
|
||||
resourcePath,
|
||||
cachePath
|
||||
}, (err, outp) => {
|
||||
workerFarm.end(workers);
|
||||
|
||||
if (err) {
|
||||
console.error(err.stack || err);
|
||||
if (err.details) {
|
||||
console.error(err.details);
|
||||
}
|
||||
|
||||
buildingInProgress = false;
|
||||
currentBuildingModule = '';
|
||||
currentBuildingScript = '';
|
||||
reject("worker farm webpack errored out");
|
||||
return;
|
||||
}
|
||||
|
||||
if (outp.errors) {
|
||||
for (const error of outp.errors) {
|
||||
console.log(error);
|
||||
}
|
||||
buildingInProgress = false;
|
||||
currentBuildingModule = '';
|
||||
currentBuildingScript = '';
|
||||
reject("webpack got an error");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`${resourceName}: built ${configName}`);
|
||||
buildingInProgress = false;
|
||||
resolve();
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
} catch (e) {
|
||||
error = e.toString();
|
||||
}
|
||||
|
||||
buildingInProgress = false;
|
||||
currentBuildingModule = '';
|
||||
|
||||
if (error) {
|
||||
cb(false, error);
|
||||
} else cb(true);
|
||||
};
|
||||
buildWebpack().then();
|
||||
}
|
||||
};
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
RegisterResourceBuildTaskFactory('z_webpack', () => webpackBuildTask);
|
||||
@@ -0,0 +1,75 @@
|
||||
const webpack = require('webpack');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
function getStat(path) {
|
||||
try {
|
||||
const stat = fs.statSync(path);
|
||||
|
||||
return stat ? {
|
||||
mtime: stat.mtimeMs,
|
||||
size: stat.size,
|
||||
inode: stat.ino,
|
||||
} : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class SaveStatePlugin {
|
||||
constructor(inp) {
|
||||
this.cache = [];
|
||||
this.cachePath = inp.cachePath;
|
||||
}
|
||||
|
||||
apply(compiler) {
|
||||
compiler.hooks.afterCompile.tap('SaveStatePlugin', (compilation) => {
|
||||
for (const file of compilation.fileDependencies) {
|
||||
this.cache.push({
|
||||
name: file,
|
||||
stats: getStat(file)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
compiler.hooks.done.tap('SaveStatePlugin', (stats) => {
|
||||
if (stats.hasErrors()) {
|
||||
return;
|
||||
}
|
||||
|
||||
fs.writeFile(this.cachePath, JSON.stringify(this.cache), () => {
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = (inp, callback) => {
|
||||
const config = require(inp.configPath);
|
||||
|
||||
config.context = inp.resourcePath;
|
||||
|
||||
if (config.output && config.output.path) {
|
||||
config.output.path = path.resolve(inp.resourcePath, config.output.path);
|
||||
}
|
||||
|
||||
if (!config.plugins) {
|
||||
config.plugins = [];
|
||||
}
|
||||
|
||||
config.plugins.push(new SaveStatePlugin(inp));
|
||||
|
||||
webpack(config, (err, stats) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (stats.hasErrors()) {
|
||||
callback(null, stats.toJson());
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, {});
|
||||
});
|
||||
};
|
||||
@@ -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'
|
||||
@@ -0,0 +1,81 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const child_process = require('child_process');
|
||||
let buildingInProgress = false;
|
||||
let currentBuildingModule = '';
|
||||
|
||||
const initCwd = process.cwd();
|
||||
const trimOutput = (data) => {
|
||||
return `[yarn]\t` + data.toString().replace(/\s+$/, '');
|
||||
}
|
||||
|
||||
const yarnBuildTask = {
|
||||
shouldBuild(resourceName) {
|
||||
try {
|
||||
const resourcePath = GetResourcePath(resourceName);
|
||||
|
||||
const packageJson = path.resolve(resourcePath, 'package.json');
|
||||
const yarnLock = path.resolve(resourcePath, '.yarn.installed');
|
||||
|
||||
const packageStat = fs.statSync(packageJson);
|
||||
|
||||
try {
|
||||
const yarnStat = fs.statSync(yarnLock);
|
||||
|
||||
if (packageStat.mtimeMs > yarnStat.mtimeMs) {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
// no yarn.installed, but package.json - install time!
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
build(resourceName, cb) {
|
||||
(async () => {
|
||||
while (buildingInProgress && currentBuildingModule !== resourceName) {
|
||||
console.log(`yarn is currently busy: we are waiting to compile ${resourceName}`);
|
||||
await sleep(3000);
|
||||
}
|
||||
buildingInProgress = true;
|
||||
currentBuildingModule = resourceName;
|
||||
const proc = child_process.fork(
|
||||
require.resolve('./yarn_cli.js'),
|
||||
['install', '--ignore-scripts', '--cache-folder', path.join(initCwd, 'cache', 'yarn-cache'), '--mutex', 'file:' + path.join(initCwd, 'cache', 'yarn-mutex')],
|
||||
{
|
||||
cwd: path.resolve(GetResourcePath(resourceName)),
|
||||
stdio: 'pipe',
|
||||
});
|
||||
proc.stdout.on('data', (data) => console.log(trimOutput(data)));
|
||||
proc.stderr.on('data', (data) => console.error(trimOutput(data)));
|
||||
proc.on('exit', (code, signal) => {
|
||||
setImmediate(() => {
|
||||
if (code != 0 || signal) {
|
||||
buildingInProgress = false;
|
||||
currentBuildingModule = '';
|
||||
cb(false, 'yarn failed!');
|
||||
return;
|
||||
}
|
||||
|
||||
const resourcePath = GetResourcePath(resourceName);
|
||||
const yarnLock = path.resolve(resourcePath, '.yarn.installed');
|
||||
fs.writeFileSync(yarnLock, '');
|
||||
|
||||
buildingInProgress = false;
|
||||
currentBuildingModule = '';
|
||||
cb(true);
|
||||
});
|
||||
});
|
||||
})();
|
||||
}
|
||||
};
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
RegisterResourceBuildTaskFactory('yarn', () => yarnBuildTask);
|
||||
147392
resources/[cfx-default]/[system]/[builders]/yarn/yarn_cli.js
Normal file
147392
resources/[cfx-default]/[system]/[builders]/yarn/yarn_cli.js
Normal file
File diff suppressed because one or more lines are too long
73
resources/[cfx-default]/[system]/baseevents/deathevents.lua
Normal file
73
resources/[cfx-default]/[system]/baseevents/deathevents.lua
Normal file
@@ -0,0 +1,73 @@
|
||||
Citizen.CreateThread(function()
|
||||
local isDead = false
|
||||
local hasBeenDead = false
|
||||
local diedAt
|
||||
|
||||
while true do
|
||||
Wait(0)
|
||||
|
||||
local player = PlayerId()
|
||||
|
||||
if NetworkIsPlayerActive(player) then
|
||||
local ped = PlayerPedId()
|
||||
|
||||
if IsPedFatallyInjured(ped) and not isDead then
|
||||
isDead = true
|
||||
if not diedAt then
|
||||
diedAt = GetGameTimer()
|
||||
end
|
||||
|
||||
local killer, killerweapon = NetworkGetEntityKillerOfPlayer(player)
|
||||
local killerentitytype = GetEntityType(killer)
|
||||
local killertype = -1
|
||||
local killerinvehicle = false
|
||||
local killervehiclename = ''
|
||||
local killervehicleseat = 0
|
||||
if killerentitytype == 1 then
|
||||
killertype = GetPedType(killer)
|
||||
if IsPedInAnyVehicle(killer, false) == 1 then
|
||||
killerinvehicle = true
|
||||
killervehiclename = GetDisplayNameFromVehicleModel(GetEntityModel(GetVehiclePedIsUsing(killer)))
|
||||
killervehicleseat = GetPedVehicleSeat(killer)
|
||||
else killerinvehicle = false
|
||||
end
|
||||
end
|
||||
|
||||
local killerid = GetPlayerByEntityID(killer)
|
||||
if killer ~= ped and killerid ~= nil and NetworkIsPlayerActive(killerid) then killerid = GetPlayerServerId(killerid)
|
||||
else killerid = -1
|
||||
end
|
||||
|
||||
if killer == ped or killer == -1 then
|
||||
TriggerEvent('baseevents:onPlayerDied', killertype, { table.unpack(GetEntityCoords(ped)) })
|
||||
TriggerServerEvent('baseevents:onPlayerDied', killertype, { table.unpack(GetEntityCoords(ped)) })
|
||||
hasBeenDead = true
|
||||
else
|
||||
TriggerEvent('baseevents:onPlayerKilled', killerid, {killertype=killertype, weaponhash = killerweapon, killerinveh=killerinvehicle, killervehseat=killervehicleseat, killervehname=killervehiclename, killerpos={table.unpack(GetEntityCoords(ped))}})
|
||||
TriggerServerEvent('baseevents:onPlayerKilled', killerid, {killertype=killertype, weaponhash = killerweapon, killerinveh=killerinvehicle, killervehseat=killervehicleseat, killervehname=killervehiclename, killerpos={table.unpack(GetEntityCoords(ped))}})
|
||||
hasBeenDead = true
|
||||
end
|
||||
elseif not IsPedFatallyInjured(ped) then
|
||||
isDead = false
|
||||
diedAt = nil
|
||||
end
|
||||
|
||||
-- check if the player has to respawn in order to trigger an event
|
||||
if not hasBeenDead and diedAt ~= nil and diedAt > 0 then
|
||||
TriggerEvent('baseevents:onPlayerWasted', { table.unpack(GetEntityCoords(ped)) })
|
||||
TriggerServerEvent('baseevents:onPlayerWasted', { table.unpack(GetEntityCoords(ped)) })
|
||||
|
||||
hasBeenDead = true
|
||||
elseif hasBeenDead and diedAt ~= nil and diedAt <= 0 then
|
||||
hasBeenDead = false
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
function GetPlayerByEntityID(id)
|
||||
for i=0,32 do
|
||||
if(NetworkIsPlayerActive(i) and GetPlayerPed(i) == id) then return i end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
14
resources/[cfx-default]/[system]/baseevents/fxmanifest.lua
Normal file
14
resources/[cfx-default]/[system]/baseevents/fxmanifest.lua
Normal file
@@ -0,0 +1,14 @@
|
||||
-- This resource is part of the default Cfx.re asset pack (cfx-server-data)
|
||||
-- Altering or recreating for local use only is strongly discouraged.
|
||||
|
||||
version '1.0.0'
|
||||
author 'Cfx.re <root@cfx.re>'
|
||||
description 'Adds basic events for developers to use in their scripts. Some third party resources may depend on this resource.'
|
||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||
|
||||
client_script 'deathevents.lua'
|
||||
client_script 'vehiclechecker.lua'
|
||||
server_script 'server.lua'
|
||||
|
||||
fx_version 'adamant'
|
||||
game 'gta5'
|
||||
19
resources/[cfx-default]/[system]/baseevents/server.lua
Normal file
19
resources/[cfx-default]/[system]/baseevents/server.lua
Normal file
@@ -0,0 +1,19 @@
|
||||
RegisterServerEvent('baseevents:onPlayerDied')
|
||||
RegisterServerEvent('baseevents:onPlayerKilled')
|
||||
RegisterServerEvent('baseevents:onPlayerWasted')
|
||||
RegisterServerEvent('baseevents:enteringVehicle')
|
||||
RegisterServerEvent('baseevents:enteringAborted')
|
||||
RegisterServerEvent('baseevents:enteredVehicle')
|
||||
RegisterServerEvent('baseevents:leftVehicle')
|
||||
|
||||
AddEventHandler('baseevents:onPlayerKilled', function(killedBy, data)
|
||||
local victim = source
|
||||
|
||||
RconLog({msgType = 'playerKilled', victim = victim, attacker = killedBy, data = data})
|
||||
end)
|
||||
|
||||
AddEventHandler('baseevents:onPlayerDied', function(killedBy, pos)
|
||||
local victim = source
|
||||
|
||||
RconLog({msgType = 'playerDied', victim = victim, attackerType = killedBy, pos = pos})
|
||||
end)
|
||||
@@ -0,0 +1,57 @@
|
||||
local isInVehicle = false
|
||||
local isEnteringVehicle = false
|
||||
local currentVehicle = 0
|
||||
local currentSeat = 0
|
||||
|
||||
Citizen.CreateThread(function()
|
||||
while true do
|
||||
Citizen.Wait(0)
|
||||
|
||||
local ped = PlayerPedId()
|
||||
|
||||
if not isInVehicle and not IsPlayerDead(PlayerId()) then
|
||||
if DoesEntityExist(GetVehiclePedIsTryingToEnter(ped)) and not isEnteringVehicle then
|
||||
-- trying to enter a vehicle!
|
||||
local vehicle = GetVehiclePedIsTryingToEnter(ped)
|
||||
local seat = GetSeatPedIsTryingToEnter(ped)
|
||||
local netId = VehToNet(vehicle)
|
||||
isEnteringVehicle = true
|
||||
TriggerServerEvent('baseevents:enteringVehicle', vehicle, seat, GetDisplayNameFromVehicleModel(GetEntityModel(vehicle)), netId)
|
||||
elseif not DoesEntityExist(GetVehiclePedIsTryingToEnter(ped)) and not IsPedInAnyVehicle(ped, true) and isEnteringVehicle then
|
||||
-- vehicle entering aborted
|
||||
TriggerServerEvent('baseevents:enteringAborted')
|
||||
isEnteringVehicle = false
|
||||
elseif IsPedInAnyVehicle(ped, false) then
|
||||
-- suddenly appeared in a vehicle, possible teleport
|
||||
isEnteringVehicle = false
|
||||
isInVehicle = true
|
||||
currentVehicle = GetVehiclePedIsUsing(ped)
|
||||
currentSeat = GetPedVehicleSeat(ped)
|
||||
local model = GetEntityModel(currentVehicle)
|
||||
local name = GetDisplayNameFromVehicleModel()
|
||||
local netId = VehToNet(currentVehicle)
|
||||
TriggerServerEvent('baseevents:enteredVehicle', currentVehicle, currentSeat, GetDisplayNameFromVehicleModel(GetEntityModel(currentVehicle)), netId)
|
||||
end
|
||||
elseif isInVehicle then
|
||||
if not IsPedInAnyVehicle(ped, false) or IsPlayerDead(PlayerId()) then
|
||||
-- bye, vehicle
|
||||
local model = GetEntityModel(currentVehicle)
|
||||
local name = GetDisplayNameFromVehicleModel()
|
||||
local netId = VehToNet(currentVehicle)
|
||||
TriggerServerEvent('baseevents:leftVehicle', currentVehicle, currentSeat, GetDisplayNameFromVehicleModel(GetEntityModel(currentVehicle)), netId)
|
||||
isInVehicle = false
|
||||
currentVehicle = 0
|
||||
currentSeat = 0
|
||||
end
|
||||
end
|
||||
Citizen.Wait(50)
|
||||
end
|
||||
end)
|
||||
|
||||
function GetPedVehicleSeat(ped)
|
||||
local vehicle = GetVehiclePedIsIn(ped, false)
|
||||
for i=-2,GetVehicleMaxNumberOfPassengers(vehicle) do
|
||||
if(GetPedInVehicleSeat(vehicle, i) == ped) then return i end
|
||||
end
|
||||
return -2
|
||||
end
|
||||
11
resources/[cfx-default]/[system]/hardcap/client.lua
Normal file
11
resources/[cfx-default]/[system]/hardcap/client.lua
Normal file
@@ -0,0 +1,11 @@
|
||||
Citizen.CreateThread(function()
|
||||
while true do
|
||||
Wait(0)
|
||||
|
||||
if NetworkIsSessionStarted() then
|
||||
TriggerServerEvent('hardcap:playerActivated')
|
||||
|
||||
return
|
||||
end
|
||||
end
|
||||
end)
|
||||
14
resources/[cfx-default]/[system]/hardcap/fxmanifest.lua
Normal file
14
resources/[cfx-default]/[system]/hardcap/fxmanifest.lua
Normal file
@@ -0,0 +1,14 @@
|
||||
-- This resource is part of the default Cfx.re asset pack (cfx-server-data)
|
||||
-- Altering or recreating for local use only is strongly discouraged.
|
||||
|
||||
version '1.0.0'
|
||||
author 'Cfx.re <root@cfx.re>'
|
||||
description 'Limits the number of players to the amount set by sv_maxclients in your server.cfg.'
|
||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||
|
||||
client_script 'client.lua'
|
||||
server_script 'server.lua'
|
||||
|
||||
fx_version '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.'
|
||||
31
resources/[cfx-default]/[system]/hardcap/server.lua
Normal file
31
resources/[cfx-default]/[system]/hardcap/server.lua
Normal file
@@ -0,0 +1,31 @@
|
||||
local playerCount = 0
|
||||
local list = {}
|
||||
|
||||
RegisterServerEvent('hardcap:playerActivated')
|
||||
|
||||
AddEventHandler('hardcap:playerActivated', function()
|
||||
if not list[source] then
|
||||
playerCount = playerCount + 1
|
||||
list[source] = true
|
||||
end
|
||||
end)
|
||||
|
||||
AddEventHandler('playerDropped', function()
|
||||
if list[source] then
|
||||
playerCount = playerCount - 1
|
||||
list[source] = nil
|
||||
end
|
||||
end)
|
||||
|
||||
AddEventHandler('playerConnecting', function(name, setReason)
|
||||
local cv = GetConvarInt('sv_maxclients', 32)
|
||||
|
||||
print('Connecting: ' .. name .. '^7')
|
||||
|
||||
if playerCount >= cv then
|
||||
print('Full. :(')
|
||||
|
||||
setReason('This server is full (past ' .. tostring(cv) .. ' players).')
|
||||
CancelEvent()
|
||||
end
|
||||
end)
|
||||
15
resources/[cfx-default]/[system]/rconlog/fxmanifest.lua
Normal file
15
resources/[cfx-default]/[system]/rconlog/fxmanifest.lua
Normal file
@@ -0,0 +1,15 @@
|
||||
-- This resource is part of the default Cfx.re asset pack (cfx-server-data)
|
||||
-- Altering or recreating for local use only is strongly discouraged.
|
||||
|
||||
version '1.0.0'
|
||||
author 'Cfx.re <root@cfx.re>'
|
||||
description 'Handles old-style server player management commands.'
|
||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||
|
||||
client_script 'rconlog_client.lua'
|
||||
server_script 'rconlog_server.lua'
|
||||
|
||||
fx_version '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.'
|
||||
25
resources/[cfx-default]/[system]/rconlog/rconlog_client.lua
Normal file
25
resources/[cfx-default]/[system]/rconlog/rconlog_client.lua
Normal file
@@ -0,0 +1,25 @@
|
||||
RegisterNetEvent('rlUpdateNames')
|
||||
|
||||
AddEventHandler('rlUpdateNames', function()
|
||||
local names = {}
|
||||
|
||||
for i = 0, 31 do
|
||||
if NetworkIsPlayerActive(i) then
|
||||
names[GetPlayerServerId(i)] = { id = i, name = GetPlayerName(i) }
|
||||
end
|
||||
end
|
||||
|
||||
TriggerServerEvent('rlUpdateNamesResult', names)
|
||||
end)
|
||||
|
||||
Citizen.CreateThread(function()
|
||||
while true do
|
||||
Wait(0)
|
||||
|
||||
if NetworkIsSessionStarted() then
|
||||
TriggerServerEvent('rlPlayerActivated')
|
||||
|
||||
return
|
||||
end
|
||||
end
|
||||
end)
|
||||
84
resources/[cfx-default]/[system]/rconlog/rconlog_server.lua
Normal file
84
resources/[cfx-default]/[system]/rconlog/rconlog_server.lua
Normal file
@@ -0,0 +1,84 @@
|
||||
RconLog({ msgType = 'serverStart', hostname = 'lovely', maxplayers = 32 })
|
||||
|
||||
RegisterServerEvent('rlPlayerActivated')
|
||||
|
||||
local names = {}
|
||||
|
||||
AddEventHandler('rlPlayerActivated', function()
|
||||
RconLog({ msgType = 'playerActivated', netID = source, name = GetPlayerName(source), guid = GetPlayerIdentifiers(source)[1], ip = GetPlayerEP(source) })
|
||||
|
||||
names[source] = { name = GetPlayerName(source), id = source }
|
||||
|
||||
if GetHostId() then
|
||||
TriggerClientEvent('rlUpdateNames', GetHostId())
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterServerEvent('rlUpdateNamesResult')
|
||||
|
||||
AddEventHandler('rlUpdateNamesResult', function(res)
|
||||
if source ~= tonumber(GetHostId()) then
|
||||
print('bad guy')
|
||||
return
|
||||
end
|
||||
|
||||
for id, data in pairs(res) do
|
||||
if data then
|
||||
if data.name then
|
||||
if not names[id] then
|
||||
names[id] = data
|
||||
end
|
||||
|
||||
if names[id].name ~= data.name or names[id].id ~= data.id then
|
||||
names[id] = data
|
||||
|
||||
RconLog({ msgType = 'playerRenamed', netID = id, name = data.name })
|
||||
end
|
||||
end
|
||||
else
|
||||
names[id] = nil
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
AddEventHandler('playerDropped', function()
|
||||
RconLog({ msgType = 'playerDropped', netID = source, name = GetPlayerName(source) })
|
||||
|
||||
names[source] = nil
|
||||
end)
|
||||
|
||||
AddEventHandler('chatMessage', function(netID, name, message)
|
||||
RconLog({ msgType = 'chatMessage', netID = netID, name = name, message = message, guid = GetPlayerIdentifiers(netID)[1] })
|
||||
end)
|
||||
|
||||
-- NOTE: DO NOT USE THIS METHOD FOR HANDLING COMMANDS
|
||||
-- This resource has not been updated to use newer methods such as RegisterCommand.
|
||||
AddEventHandler('rconCommand', function(commandName, args)
|
||||
if commandName == 'status' then
|
||||
for netid, data in pairs(names) do
|
||||
local guid = GetPlayerIdentifiers(netid)
|
||||
|
||||
if guid and guid[1] and data then
|
||||
local ping = GetPlayerPing(netid)
|
||||
|
||||
RconPrint(netid .. ' ' .. guid[1] .. ' ' .. data.name .. ' ' .. GetPlayerEP(netid) .. ' ' .. ping .. "\n")
|
||||
end
|
||||
end
|
||||
|
||||
CancelEvent()
|
||||
elseif commandName:lower() == 'clientkick' then
|
||||
local playerId = table.remove(args, 1)
|
||||
local msg = table.concat(args, ' ')
|
||||
|
||||
DropPlayer(playerId, msg)
|
||||
|
||||
CancelEvent()
|
||||
elseif commandName:lower() == 'tempbanclient' then
|
||||
local playerId = table.remove(args, 1)
|
||||
local msg = table.concat(args, ' ')
|
||||
|
||||
TempBanPlayer(playerId, msg)
|
||||
|
||||
CancelEvent()
|
||||
end
|
||||
end)
|
||||
1
resources/[cfx-default]/[system]/runcode/.gitignore
vendored
Normal file
1
resources/[cfx-default]/[system]/runcode/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
data.json
|
||||
24
resources/[cfx-default]/[system]/runcode/fxmanifest.lua
Normal file
24
resources/[cfx-default]/[system]/runcode/fxmanifest.lua
Normal file
@@ -0,0 +1,24 @@
|
||||
-- This resource is part of the default Cfx.re asset pack (cfx-server-data)
|
||||
-- Altering or recreating for local use only is strongly discouraged.
|
||||
|
||||
version '1.0.0'
|
||||
author 'Cfx.re <root@cfx.re>'
|
||||
description 'Allows server owners to execute arbitrary server-side or client-side JavaScript/Lua code. *Consider only using this on development servers.'
|
||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||
|
||||
game 'common'
|
||||
fx_version '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'
|
||||
}
|
||||
11
resources/[cfx-default]/[system]/runcode/runcode.js
Normal file
11
resources/[cfx-default]/[system]/runcode/runcode.js
Normal file
@@ -0,0 +1,11 @@
|
||||
exports('runJS', (snippet) => {
|
||||
if (IsDuplicityVersion() && GetInvokingResource() !== GetCurrentResourceName()) {
|
||||
return [ 'Invalid caller.', false ];
|
||||
}
|
||||
|
||||
try {
|
||||
return [ new Function(snippet)(), false ];
|
||||
} catch (e) {
|
||||
return [ false, e.toString() ];
|
||||
}
|
||||
});
|
||||
15
resources/[cfx-default]/[system]/runcode/runcode_cl.lua
Normal file
15
resources/[cfx-default]/[system]/runcode/runcode_cl.lua
Normal file
@@ -0,0 +1,15 @@
|
||||
RegisterNetEvent('runcode:gotSnippet')
|
||||
|
||||
AddEventHandler('runcode:gotSnippet', function(id, lang, code)
|
||||
local res, err = RunCode(lang, code)
|
||||
|
||||
if not err then
|
||||
if type(res) == 'vector3' then
|
||||
res = json.encode({ table.unpack(res) })
|
||||
elseif type(res) == 'table' then
|
||||
res = json.encode(res)
|
||||
end
|
||||
end
|
||||
|
||||
TriggerServerEvent('runcode:gotResult', id, res, err)
|
||||
end)
|
||||
32
resources/[cfx-default]/[system]/runcode/runcode_shared.lua
Normal file
32
resources/[cfx-default]/[system]/runcode/runcode_shared.lua
Normal file
@@ -0,0 +1,32 @@
|
||||
local runners = {}
|
||||
|
||||
function runners.lua(arg)
|
||||
local code, err = load('return ' .. arg, '@runcode')
|
||||
|
||||
-- if failed, try without return
|
||||
if err then
|
||||
code, err = load(arg, '@runcode')
|
||||
end
|
||||
|
||||
if err then
|
||||
print(err)
|
||||
return nil, err
|
||||
end
|
||||
|
||||
local status, result = pcall(code)
|
||||
print(result)
|
||||
|
||||
if status then
|
||||
return result
|
||||
end
|
||||
|
||||
return nil, result
|
||||
end
|
||||
|
||||
function runners.js(arg)
|
||||
return table.unpack(exports[GetCurrentResourceName()]:runJS(arg))
|
||||
end
|
||||
|
||||
function RunCode(lang, str)
|
||||
return runners[lang](str)
|
||||
end
|
||||
42
resources/[cfx-default]/[system]/runcode/runcode_sv.lua
Normal file
42
resources/[cfx-default]/[system]/runcode/runcode_sv.lua
Normal file
@@ -0,0 +1,42 @@
|
||||
function GetPrivs(source)
|
||||
return {
|
||||
canServer = IsPlayerAceAllowed(source, 'command.run'),
|
||||
canClient = IsPlayerAceAllowed(source, 'command.crun'),
|
||||
canSelf = IsPlayerAceAllowed(source, 'runcode.self'),
|
||||
}
|
||||
end
|
||||
|
||||
RegisterCommand('run', function(source, args, rawCommand)
|
||||
local res, err = RunCode('lua', rawCommand:sub(4))
|
||||
end, true)
|
||||
|
||||
RegisterCommand('crun', function(source, args, rawCommand)
|
||||
if not source then
|
||||
return
|
||||
end
|
||||
|
||||
TriggerClientEvent('runcode:gotSnippet', source, -1, 'lua', rawCommand:sub(5))
|
||||
end, true)
|
||||
|
||||
RegisterCommand('runcode', function(source, args, rawCommand)
|
||||
if not source then
|
||||
return
|
||||
end
|
||||
|
||||
local df = LoadResourceFile(GetCurrentResourceName(), 'data.json')
|
||||
local saveData = {}
|
||||
|
||||
if df then
|
||||
saveData = json.decode(df)
|
||||
end
|
||||
|
||||
local p = GetPrivs(source)
|
||||
|
||||
if not p.canServer and not p.canClient and not p.canSelf then
|
||||
return
|
||||
end
|
||||
|
||||
p.saveData = saveData
|
||||
|
||||
TriggerClientEvent('runcode:openUi', source, p)
|
||||
end, true)
|
||||
66
resources/[cfx-default]/[system]/runcode/runcode_ui.lua
Normal file
66
resources/[cfx-default]/[system]/runcode/runcode_ui.lua
Normal file
@@ -0,0 +1,66 @@
|
||||
local openData
|
||||
|
||||
RegisterNetEvent('runcode:openUi')
|
||||
|
||||
AddEventHandler('runcode:openUi', function(options)
|
||||
openData = {
|
||||
type = 'open',
|
||||
options = options,
|
||||
url = '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)
|
||||
192
resources/[cfx-default]/[system]/runcode/runcode_web.lua
Normal file
192
resources/[cfx-default]/[system]/runcode/runcode_web.lua
Normal file
@@ -0,0 +1,192 @@
|
||||
local cachedFiles = {}
|
||||
|
||||
local function sendFile(res, fileName)
|
||||
if cachedFiles[fileName] then
|
||||
res.send(cachedFiles[fileName])
|
||||
return
|
||||
end
|
||||
|
||||
local fileData = LoadResourceFile(GetCurrentResourceName(), 'web/' .. fileName)
|
||||
|
||||
if not fileData then
|
||||
res.writeHead(404)
|
||||
res.send('Not found.')
|
||||
return
|
||||
end
|
||||
|
||||
cachedFiles[fileName] = fileData
|
||||
res.send(fileData)
|
||||
end
|
||||
|
||||
local codeId = 1
|
||||
local codes = {}
|
||||
|
||||
local attempts = 0
|
||||
local lastAttempt
|
||||
|
||||
local function handleRunCode(data, res)
|
||||
if not data.lang then
|
||||
data.lang = 'lua'
|
||||
end
|
||||
|
||||
if not data.client or data.client == '' then
|
||||
CreateThread(function()
|
||||
local result, err = RunCode(data.lang, data.code)
|
||||
|
||||
res.send(json.encode({
|
||||
result = result,
|
||||
error = err
|
||||
}))
|
||||
end)
|
||||
else
|
||||
codes[codeId] = {
|
||||
timeout = GetGameTimer() + 1000,
|
||||
res = res
|
||||
}
|
||||
|
||||
TriggerClientEvent('runcode:gotSnippet', tonumber(data.client), codeId, data.lang, data.code)
|
||||
|
||||
codeId = codeId + 1
|
||||
end
|
||||
end
|
||||
|
||||
RegisterNetEvent('runcode:runInBand')
|
||||
|
||||
AddEventHandler('runcode:runInBand', function(id, data)
|
||||
local s = source
|
||||
local privs = GetPrivs(s)
|
||||
|
||||
local res = {
|
||||
send = function(str)
|
||||
TriggerClientEvent('runcode:inBandResult', s, id, str)
|
||||
end
|
||||
}
|
||||
|
||||
if (not data.client or data.client == '') and not privs.canServer then
|
||||
res.send(json.encode({ error = 'Insufficient permissions.'}))
|
||||
return
|
||||
end
|
||||
|
||||
if (data.client and data.client ~= '') and not privs.canClient then
|
||||
if privs.canSelf then
|
||||
data.client = s
|
||||
else
|
||||
res.send(json.encode({ error = 'Insufficient permissions.'}))
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
SaveResourceFile(GetCurrentResourceName(), 'data.json', json.encode({
|
||||
lastSnippet = data.code,
|
||||
lastLang = data.lang or 'lua'
|
||||
}), -1)
|
||||
|
||||
handleRunCode(data, res)
|
||||
end)
|
||||
|
||||
local function handlePost(req, res)
|
||||
req.setDataHandler(function(body)
|
||||
local data = json.decode(body)
|
||||
|
||||
if not data or not data.password or not data.code then
|
||||
res.send(json.encode({ error = 'Bad request.'}))
|
||||
return
|
||||
end
|
||||
|
||||
if GetConvar('rcon_password', '') == '' then
|
||||
res.send(json.encode({ error = 'The server has an empty rcon_password.'}))
|
||||
return
|
||||
end
|
||||
|
||||
if attempts > 5 or data.password ~= GetConvar('rcon_password', '') then
|
||||
attempts = attempts + 1
|
||||
lastAttempt = GetGameTimer()
|
||||
|
||||
res.send(json.encode({ error = 'Bad password.'}))
|
||||
return
|
||||
end
|
||||
|
||||
handleRunCode(data, res)
|
||||
end)
|
||||
end
|
||||
|
||||
CreateThread(function()
|
||||
while true do
|
||||
Wait(1000)
|
||||
|
||||
if attempts > 0 and (GetGameTimer() - lastAttempt) > 5000 then
|
||||
attempts = 0
|
||||
lastAttempt = 0
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
local function returnCode(id, res, err)
|
||||
if not codes[id] then
|
||||
return
|
||||
end
|
||||
|
||||
local code = codes[id]
|
||||
codes[id] = nil
|
||||
|
||||
local gotFrom
|
||||
|
||||
if source then
|
||||
gotFrom = GetPlayerName(source) .. ' [' .. tostring(source) .. ']'
|
||||
end
|
||||
|
||||
code.res.send(json.encode({
|
||||
result = res,
|
||||
error = err,
|
||||
from = gotFrom
|
||||
}))
|
||||
end
|
||||
|
||||
CreateThread(function()
|
||||
while true do
|
||||
Wait(100)
|
||||
|
||||
for k, v in ipairs(codes) do
|
||||
if GetGameTimer() > v.timeout then
|
||||
source = nil
|
||||
returnCode(k, '', 'Timed out waiting on the target client.')
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterNetEvent('runcode:gotResult')
|
||||
AddEventHandler('runcode:gotResult', returnCode)
|
||||
|
||||
SetHttpHandler(function(req, res)
|
||||
local path = req.path
|
||||
|
||||
if req.method == 'POST' then
|
||||
return handlePost(req, res)
|
||||
end
|
||||
|
||||
-- client shortcuts
|
||||
if req.path == '/clients' then
|
||||
local clientList = {}
|
||||
|
||||
for _, id in ipairs(GetPlayers()) do
|
||||
table.insert(clientList, { GetPlayerName(id), id })
|
||||
end
|
||||
|
||||
res.send(json.encode({
|
||||
clients = clientList
|
||||
}))
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
-- should this be the index?
|
||||
if req.path == '/' then
|
||||
path = 'index.html'
|
||||
end
|
||||
|
||||
-- remove any '..' from the path
|
||||
path = path:gsub("%.%.", "")
|
||||
|
||||
return sendFile(res, path)
|
||||
end)
|
||||
486
resources/[cfx-default]/[system]/runcode/web/index.html
Normal file
486
resources/[cfx-default]/[system]/runcode/web/index.html
Normal file
@@ -0,0 +1,486 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>fivem runcode</title>
|
||||
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulmaswatch/0.7.2/cyborg/bulmaswatch.min.css">
|
||||
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
|
||||
|
||||
<style type="text/css">
|
||||
body {
|
||||
font-family: "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
z-index: inherit;
|
||||
}
|
||||
|
||||
html.in-nui {
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
|
||||
margin-top: 5vh;
|
||||
margin-left: 7.5vw;
|
||||
margin-right: 7.5vw;
|
||||
margin-bottom: 5vh;
|
||||
|
||||
height: calc(100% - 10vh);
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
html.in-nui body > div.bg {
|
||||
background-color: rgba(0, 0, 0, 1);
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
z-index: -999;
|
||||
|
||||
box-shadow: 0 22px 70px 4px rgba(0, 0, 0, 0.56);
|
||||
}
|
||||
|
||||
span.nui-edition {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html.in-nui span.nui-edition {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
#close {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html.in-nui #close {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#result {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg">
|
||||
|
||||
</div>
|
||||
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="/runcode">
|
||||
<strong>runcode</strong> <span class="nui-edition"> in-game</span>
|
||||
</a>
|
||||
|
||||
<a role="button" class="navbar-burger burger" aria-label="menu" aria-expanded="false" data-target="navbarMain">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="navbarMain" class="navbar-menu">
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item">
|
||||
<div class="field" id="cl-field">
|
||||
<div class="control has-icons-left">
|
||||
<div class="select">
|
||||
<select id="cl-select">
|
||||
</select>
|
||||
</div>
|
||||
<div class="icon is-small is-left">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="navbar-item">
|
||||
<div class="field has-addons" id="lang-toggle">
|
||||
<p class="control">
|
||||
<button class="button" id="lua-button">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-moon"></i>
|
||||
</span>
|
||||
<span>Lua</span>
|
||||
</button>
|
||||
</p>
|
||||
<p class="control">
|
||||
<button class="button" id="js-button">
|
||||
<span class="icon is-small">
|
||||
<i class="fab fa-js"></i>
|
||||
</span>
|
||||
<span>JS</span>
|
||||
</button>
|
||||
</p>
|
||||
<!-- TODO pending add-on resource that'll contain webpack'd compiler
|
||||
<p class="control">
|
||||
<button class="button" id="ts-button">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-code"></i>
|
||||
</span>
|
||||
<span>TS</span>
|
||||
</button>
|
||||
</p>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="navbar-item">
|
||||
<div class="field has-addons" id="cl-sv-toggle">
|
||||
<p class="control">
|
||||
<button class="button" id="cl-button">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-user-friends"></i>
|
||||
</span>
|
||||
<span>Client</span>
|
||||
</button>
|
||||
</p>
|
||||
<p class="control">
|
||||
<button class="button" id="sv-button">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-server"></i>
|
||||
</span>
|
||||
<span>Server</span>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="navbar-item" id="close">
|
||||
<button class="button is-danger">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div id="code-container" style="width:100%;height:60vh;border:1px solid grey"></div><br>
|
||||
<div class="field" id="passwordField">
|
||||
<p class="control has-icons-left">
|
||||
<input class="input" type="password" id="password" placeholder="RCon Password">
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fas fa-lock"></i>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<button class="button is-primary" id="run">Run</button>
|
||||
<div id="result">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!--
|
||||
to use a local deployment, uncomment; do note currently the server isn't optimized to serve >1MB files
|
||||
<script src="monaco-editor/vs/loader.js"></script>
|
||||
-->
|
||||
|
||||
<script src="https://unpkg.com/monaco-editor@0.18.1/min/vs/loader.js"></script>
|
||||
|
||||
<script>
|
||||
function fetchClients() {
|
||||
fetch('/runcode/clients').then(res => res.json()).then(res => {
|
||||
const el = document.querySelector('#cl-select');
|
||||
|
||||
const clients = res.clients;
|
||||
const realClients = [['All', '-1'], ...clients];
|
||||
|
||||
const createdClients = new Set([...el.querySelectorAll('option').entries()].map(([i, el]) => el.value));
|
||||
const existentClients = new Set(realClients.map(([ name, id ]) => id));
|
||||
|
||||
const toRemove = [...createdClients].filter(a => !existentClients.has(a));
|
||||
|
||||
for (const [name, id] of realClients) {
|
||||
const ex = el.querySelector(`option[value="${id}"]`);
|
||||
|
||||
if (!ex) {
|
||||
const l = document.createElement('option');
|
||||
l.setAttribute('value', id);
|
||||
l.appendChild(document.createTextNode(name));
|
||||
|
||||
el.appendChild(l);
|
||||
}
|
||||
}
|
||||
|
||||
for (const id of toRemove) {
|
||||
const l = el.querySelector(`option[value="${id}"]`);
|
||||
|
||||
if (l) {
|
||||
el.removeChild(l);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let useClient = false;
|
||||
let editServerCb = null;
|
||||
|
||||
[['#cl-button', true], ['#sv-button', false]].forEach(([ selector, isClient ]) => {
|
||||
const eh = () => {
|
||||
if (isClient) {
|
||||
document.querySelector('#cl-select').disabled = false;
|
||||
useClient = true;
|
||||
} else {
|
||||
document.querySelector('#cl-select').disabled = true;
|
||||
useClient = false;
|
||||
}
|
||||
|
||||
document.querySelectorAll('#cl-sv-toggle button').forEach(el => {
|
||||
el.classList.remove('is-selected', 'is-info');
|
||||
});
|
||||
|
||||
const tgt = document.querySelector(selector);
|
||||
|
||||
tgt.classList.add('is-selected', 'is-info');
|
||||
|
||||
if (editServerCb) {
|
||||
editServerCb();
|
||||
}
|
||||
};
|
||||
|
||||
// default to not-client
|
||||
if (!isClient) {
|
||||
eh();
|
||||
}
|
||||
|
||||
document.querySelector(selector).addEventListener('click', ev => {
|
||||
eh();
|
||||
|
||||
ev.preventDefault();
|
||||
});
|
||||
});
|
||||
|
||||
let lang = 'lua';
|
||||
let editLangCb = null;
|
||||
let initCb = null;
|
||||
|
||||
function getLangCode(lang) {
|
||||
switch (lang) {
|
||||
case 'js':
|
||||
return 'javascript';
|
||||
case 'ts':
|
||||
return 'typescript';
|
||||
}
|
||||
|
||||
return lang;
|
||||
}
|
||||
|
||||
[['#lua-button', 'lua'], ['#js-button', 'js']/*, ['#ts-button', 'ts']*/].forEach(([ selector, langOpt ]) => {
|
||||
const eh = () => {
|
||||
lang = langOpt;
|
||||
|
||||
document.querySelectorAll('#lang-toggle button').forEach(el => {
|
||||
el.classList.remove('is-selected', 'is-info');
|
||||
});
|
||||
|
||||
const tgt = document.querySelector(selector);
|
||||
|
||||
tgt.classList.add('is-selected', 'is-info');
|
||||
|
||||
if (editLangCb) {
|
||||
editLangCb();
|
||||
}
|
||||
};
|
||||
|
||||
// default to not-client
|
||||
if (langOpt === 'lua') {
|
||||
eh();
|
||||
}
|
||||
|
||||
document.querySelector(selector).addEventListener('click', ev => {
|
||||
eh();
|
||||
|
||||
ev.preventDefault();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
setInterval(() => fetchClients(), 1000);
|
||||
|
||||
const inNui = (!!window.invokeNative);
|
||||
let openData = {};
|
||||
|
||||
if (inNui) {
|
||||
document.querySelector('#passwordField').style.display = 'none';
|
||||
document.querySelector('html').classList.add('in-nui');
|
||||
|
||||
fetch(`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>
|
||||
60
resources/[cfx-default]/[system]/runcode/web/nui.html
Normal file
60
resources/[cfx-default]/[system]/runcode/web/nui.html
Normal file
@@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<title>runcode nui</title>
|
||||
|
||||
<style type="text/css">
|
||||
html {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: transparent;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="holder">
|
||||
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
let openData = null;
|
||||
|
||||
window.addEventListener('message', ev => {
|
||||
switch (ev.data.type) {
|
||||
case 'open':
|
||||
const frame = document.createElement('iframe');
|
||||
|
||||
frame.name = 'rc';
|
||||
frame.allow = 'microphone *;';
|
||||
frame.src = ev.data.url;
|
||||
frame.style.visibility = 'hidden';
|
||||
|
||||
openData = ev.data;
|
||||
openData.frame = frame;
|
||||
|
||||
document.querySelector('#holder').appendChild(frame);
|
||||
break;
|
||||
case 'ok':
|
||||
openData.frame.style.visibility = 'visible';
|
||||
break;
|
||||
case 'close':
|
||||
document.querySelector('#holder').removeChild(openData.frame);
|
||||
|
||||
openData = null;
|
||||
break;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
2
resources/[cfx-default]/[system]/sessionmanager-rdr3/.gitignore
vendored
Normal file
2
resources/[cfx-default]/[system]/sessionmanager-rdr3/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
.yarn.installed
|
||||
@@ -0,0 +1,17 @@
|
||||
-- This resource is part of the default Cfx.re asset pack (cfx-server-data)
|
||||
-- Altering or recreating for local use only is strongly discouraged.
|
||||
|
||||
version '1.0.0'
|
||||
author 'Cfx.re <root@cfx.re>'
|
||||
description 'Handles Social Club conductor session API for RedM. Do not disable.'
|
||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||
|
||||
fx_version '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'
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@citizenfx/protobufjs": "6.8.8"
|
||||
}
|
||||
}
|
||||
192
resources/[cfx-default]/[system]/sessionmanager-rdr3/rline.proto
Normal file
192
resources/[cfx-default]/[system]/sessionmanager-rdr3/rline.proto
Normal file
@@ -0,0 +1,192 @@
|
||||
syntax = "proto3";
|
||||
package rline;
|
||||
|
||||
message RpcErrorData {
|
||||
string ErrorCodeString = 1;
|
||||
int32 ErrorCode = 2;
|
||||
string DomainString = 3;
|
||||
int32 DomainCode = 4;
|
||||
bytes DataEx = 5;
|
||||
};
|
||||
|
||||
message RpcError {
|
||||
int32 ErrorCode = 1;
|
||||
string ErrorMessage = 2;
|
||||
RpcErrorData Data = 3;
|
||||
};
|
||||
|
||||
message RpcHeader {
|
||||
string RequestId = 1;
|
||||
string MethodName = 2;
|
||||
RpcError Error = 3;
|
||||
string srcTid = 4;
|
||||
};
|
||||
|
||||
message RpcMessage {
|
||||
RpcHeader Header = 1;
|
||||
bytes Content = 2;
|
||||
};
|
||||
|
||||
message RpcResponseContainer {
|
||||
bytes Content = 1;
|
||||
};
|
||||
|
||||
message RpcResponseMessage {
|
||||
RpcHeader Header = 1;
|
||||
RpcResponseContainer Container = 2;
|
||||
};
|
||||
|
||||
message TokenStuff {
|
||||
string tkn = 1;
|
||||
};
|
||||
|
||||
message InitSessionResponse {
|
||||
bytes sesid = 1;
|
||||
TokenStuff token = 2;
|
||||
};
|
||||
|
||||
message MpGamerHandleDto {
|
||||
string gh = 1;
|
||||
};
|
||||
|
||||
message MpPeerAddressDto {
|
||||
string addr = 1;
|
||||
};
|
||||
|
||||
message InitPlayer2_Parameters {
|
||||
MpGamerHandleDto gh = 1;
|
||||
MpPeerAddressDto peerAddress = 2;
|
||||
int32 discriminator = 3;
|
||||
int32 seamlessType = 4;
|
||||
uint32 connectionReason = 5;
|
||||
};
|
||||
|
||||
message InitPlayerResult {
|
||||
uint32 code = 1;
|
||||
};
|
||||
|
||||
message Restriction {
|
||||
int32 u1 = 1;
|
||||
int32 u2 = 2;
|
||||
int32 u3 = 3;
|
||||
}
|
||||
|
||||
message GetRestrictionsData {
|
||||
repeated Restriction restriction = 1;
|
||||
repeated string unk2 = 2;
|
||||
};
|
||||
|
||||
message GetRestrictionsResult {
|
||||
GetRestrictionsData data = 1;
|
||||
};
|
||||
|
||||
message PlayerIdSto {
|
||||
int32 acctId = 1;
|
||||
int32 platId = 2;
|
||||
};
|
||||
|
||||
message MpSessionRequestIdDto {
|
||||
PlayerIdSto requestor = 1;
|
||||
int32 index = 2;
|
||||
int32 hash = 3;
|
||||
};
|
||||
|
||||
message QueueForSession_Seamless_Parameters {
|
||||
MpSessionRequestIdDto requestId = 1;
|
||||
uint32 optionFlags = 2;
|
||||
int32 x = 3;
|
||||
int32 y = 4;
|
||||
};
|
||||
|
||||
message QueueForSessionResult {
|
||||
uint32 code = 1;
|
||||
};
|
||||
|
||||
message QueueEntered_Parameters {
|
||||
uint32 queueGroup = 1;
|
||||
MpSessionRequestIdDto requestId = 2;
|
||||
uint32 optionFlags = 3;
|
||||
};
|
||||
|
||||
message GuidDto {
|
||||
fixed64 a = 1;
|
||||
fixed64 b = 2;
|
||||
};
|
||||
|
||||
message MpTransitionIdDto {
|
||||
GuidDto value = 1;
|
||||
};
|
||||
|
||||
message MpSessionIdDto {
|
||||
GuidDto value = 1;
|
||||
};
|
||||
|
||||
message SessionSubcommandEnterSession {
|
||||
int32 index = 1;
|
||||
int32 hindex = 2;
|
||||
uint32 sessionFlags = 3;
|
||||
uint32 mode = 4;
|
||||
int32 size = 5;
|
||||
int32 teamIndex = 6;
|
||||
MpTransitionIdDto transitionId = 7;
|
||||
uint32 sessionManagerType = 8;
|
||||
int32 slotCount = 9;
|
||||
};
|
||||
|
||||
message SessionSubcommandLeaveSession {
|
||||
uint32 reason = 1;
|
||||
};
|
||||
|
||||
message SessionSubcommandAddPlayer {
|
||||
PlayerIdSto id = 1;
|
||||
MpGamerHandleDto gh = 2;
|
||||
MpPeerAddressDto addr = 3;
|
||||
int32 index = 4;
|
||||
};
|
||||
|
||||
message SessionSubcommandRemovePlayer {
|
||||
PlayerIdSto id = 1;
|
||||
};
|
||||
|
||||
message SessionSubcommandHostChanged {
|
||||
int32 index = 1;
|
||||
};
|
||||
|
||||
message SessionCommand {
|
||||
uint32 cmd = 1;
|
||||
string cmdname = 2;
|
||||
SessionSubcommandEnterSession EnterSession = 3;
|
||||
SessionSubcommandLeaveSession LeaveSession = 4;
|
||||
SessionSubcommandAddPlayer AddPlayer = 5;
|
||||
SessionSubcommandRemovePlayer RemovePlayer = 6;
|
||||
SessionSubcommandHostChanged HostChanged = 7;
|
||||
};
|
||||
|
||||
message scmds_Parameters {
|
||||
MpSessionIdDto sid = 1;
|
||||
int32 ncmds = 2;
|
||||
repeated SessionCommand cmds = 3;
|
||||
};
|
||||
|
||||
message UriType {
|
||||
string url = 1;
|
||||
};
|
||||
|
||||
message TransitionReady_PlayerQueue_Parameters {
|
||||
UriType serverUri = 1;
|
||||
uint32 serverSandbox = 2;
|
||||
MpTransitionIdDto id = 3;
|
||||
uint32 sessionType = 4;
|
||||
MpSessionRequestIdDto requestId = 5;
|
||||
MpSessionIdDto transferId = 6;
|
||||
};
|
||||
|
||||
message TransitionToSession_Parameters {
|
||||
MpTransitionIdDto id = 1;
|
||||
float x = 2;
|
||||
float y = 3;
|
||||
};
|
||||
|
||||
message TransitionToSessionResult {
|
||||
uint32 code = 1;
|
||||
};
|
||||
@@ -0,0 +1,328 @@
|
||||
const protobuf = require("@citizenfx/protobufjs");
|
||||
|
||||
const playerDatas = {};
|
||||
let slotsUsed = 0;
|
||||
|
||||
function assignSlotId() {
|
||||
for (let i = 0; i < 32; i++) {
|
||||
if (!(slotsUsed & (1 << i))) {
|
||||
slotsUsed |= (1 << i);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
let hostIndex = -1;
|
||||
const isOneSync = GetConvar("onesync", "off") !== "off";
|
||||
|
||||
protobuf.load(GetResourcePath(GetCurrentResourceName()) + "/rline.proto", function(err, root) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
return;
|
||||
}
|
||||
|
||||
const RpcMessage = root.lookupType("rline.RpcMessage");
|
||||
const RpcResponseMessage = root.lookupType("rline.RpcResponseMessage");
|
||||
const InitSessionResponse = root.lookupType("rline.InitSessionResponse");
|
||||
const InitPlayer2_Parameters = root.lookupType("rline.InitPlayer2_Parameters");
|
||||
const InitPlayerResult = root.lookupType("rline.InitPlayerResult");
|
||||
const GetRestrictionsResult = root.lookupType("rline.GetRestrictionsResult");
|
||||
const QueueForSession_Seamless_Parameters = root.lookupType("rline.QueueForSession_Seamless_Parameters");
|
||||
const QueueForSessionResult = root.lookupType("rline.QueueForSessionResult");
|
||||
const QueueEntered_Parameters = root.lookupType("rline.QueueEntered_Parameters");
|
||||
const TransitionReady_PlayerQueue_Parameters = root.lookupType("rline.TransitionReady_PlayerQueue_Parameters");
|
||||
const TransitionToSession_Parameters = root.lookupType("rline.TransitionToSession_Parameters");
|
||||
const TransitionToSessionResult = root.lookupType("rline.TransitionToSessionResult");
|
||||
const scmds_Parameters = root.lookupType("rline.scmds_Parameters");
|
||||
|
||||
function toArrayBuffer(buf) {
|
||||
var ab = new ArrayBuffer(buf.length);
|
||||
var view = new Uint8Array(ab);
|
||||
for (var i = 0; i < buf.length; ++i) {
|
||||
view[i] = buf[i];
|
||||
}
|
||||
return ab;
|
||||
}
|
||||
|
||||
function emitMsg(target, data) {
|
||||
emitNet('__cfx_internal:pbRlScSession', target, toArrayBuffer(data));
|
||||
}
|
||||
|
||||
function emitSessionCmds(target, cmd, cmdname, msg) {
|
||||
const stuff = {};
|
||||
stuff[cmdname] = msg;
|
||||
|
||||
emitMsg(target, RpcMessage.encode({
|
||||
Header: {
|
||||
MethodName: 'scmds'
|
||||
},
|
||||
Content: scmds_Parameters.encode({
|
||||
sid: {
|
||||
value: {
|
||||
a: 2,
|
||||
b: 2
|
||||
}
|
||||
},
|
||||
ncmds: 1,
|
||||
cmds: [
|
||||
{
|
||||
cmd,
|
||||
cmdname,
|
||||
...stuff
|
||||
}
|
||||
]
|
||||
}).finish()
|
||||
}).finish());
|
||||
}
|
||||
|
||||
function emitAddPlayer(target, msg) {
|
||||
emitSessionCmds(target, 2, 'AddPlayer', msg);
|
||||
}
|
||||
|
||||
function emitRemovePlayer(target, msg) {
|
||||
emitSessionCmds(target, 3, 'RemovePlayer', msg);
|
||||
}
|
||||
|
||||
function emitHostChanged(target, msg) {
|
||||
emitSessionCmds(target, 5, 'HostChanged', msg);
|
||||
}
|
||||
|
||||
onNet('playerDropped', () => {
|
||||
if (isOneSync) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const oData = playerDatas[source];
|
||||
delete playerDatas[source];
|
||||
|
||||
if (oData && hostIndex === oData.slot) {
|
||||
const pda = Object.entries(playerDatas);
|
||||
|
||||
if (pda.length > 0) {
|
||||
hostIndex = pda[0][1].slot | 0; // TODO: actually use <=31 slot index *and* check for id
|
||||
|
||||
for (const [ id, data ] of Object.entries(playerDatas)) {
|
||||
emitHostChanged(id, {
|
||||
index: hostIndex
|
||||
});
|
||||
}
|
||||
} else {
|
||||
hostIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!oData) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (oData.slot > -1) {
|
||||
slotsUsed &= ~(1 << oData.slot);
|
||||
}
|
||||
|
||||
for (const [ id, data ] of Object.entries(playerDatas)) {
|
||||
emitRemovePlayer(id, {
|
||||
id: oData.id
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.log(e.stack);
|
||||
}
|
||||
});
|
||||
|
||||
function makeResponse(type, data) {
|
||||
return {
|
||||
Header: {
|
||||
},
|
||||
Container: {
|
||||
Content: type.encode(data).finish()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handlers = {
|
||||
async InitSession(source, data) {
|
||||
return makeResponse(InitSessionResponse, {
|
||||
sesid: Buffer.alloc(16),
|
||||
/*token: {
|
||||
tkn: 'ACSTOKEN token="meow",signature="meow"'
|
||||
}*/
|
||||
});
|
||||
},
|
||||
|
||||
async InitPlayer2(source, data) {
|
||||
const req = InitPlayer2_Parameters.decode(data);
|
||||
|
||||
if (!isOneSync) {
|
||||
playerDatas[source] = {
|
||||
gh: req.gh,
|
||||
peerAddress: req.peerAddress,
|
||||
discriminator: req.discriminator,
|
||||
slot: -1
|
||||
};
|
||||
}
|
||||
|
||||
return makeResponse(InitPlayerResult, {
|
||||
code: 0
|
||||
});
|
||||
},
|
||||
|
||||
async GetRestrictions(source, data) {
|
||||
return makeResponse(GetRestrictionsResult, {
|
||||
data: {
|
||||
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async ConfirmSessionEntered(source, data) {
|
||||
return {};
|
||||
},
|
||||
|
||||
async TransitionToSession(source, data) {
|
||||
const req = TransitionToSession_Parameters.decode(data);
|
||||
|
||||
return makeResponse(TransitionToSessionResult, {
|
||||
code: 1 // in this message, 1 is success
|
||||
});
|
||||
},
|
||||
|
||||
async QueueForSession_Seamless(source, data) {
|
||||
const req = QueueForSession_Seamless_Parameters.decode(data);
|
||||
|
||||
if (!isOneSync) {
|
||||
playerDatas[source].req = req.requestId;
|
||||
playerDatas[source].id = req.requestId.requestor;
|
||||
playerDatas[source].slot = assignSlotId();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
emitMsg(source, RpcMessage.encode({
|
||||
Header: {
|
||||
MethodName: 'QueueEntered'
|
||||
},
|
||||
Content: QueueEntered_Parameters.encode({
|
||||
queueGroup: 69,
|
||||
requestId: req.requestId,
|
||||
optionFlags: req.optionFlags
|
||||
}).finish()
|
||||
}).finish());
|
||||
|
||||
if (isOneSync) {
|
||||
hostIndex = 16
|
||||
} else if (hostIndex === -1) {
|
||||
hostIndex = playerDatas[source].slot | 0;
|
||||
}
|
||||
|
||||
emitMsg(source, RpcMessage.encode({
|
||||
Header: {
|
||||
MethodName: 'TransitionReady_PlayerQueue'
|
||||
},
|
||||
Content: TransitionReady_PlayerQueue_Parameters.encode({
|
||||
serverUri: {
|
||||
url: ''
|
||||
},
|
||||
requestId: req.requestId,
|
||||
id: {
|
||||
value: {
|
||||
a: 2,
|
||||
b: 0
|
||||
}
|
||||
},
|
||||
serverSandbox: 0xD656C677,
|
||||
sessionType: 3,
|
||||
transferId: {
|
||||
value: {
|
||||
a: 2,
|
||||
b: 2
|
||||
}
|
||||
},
|
||||
}).finish()
|
||||
}).finish());
|
||||
|
||||
setTimeout(() => {
|
||||
emitSessionCmds(source, 0, 'EnterSession', {
|
||||
index: (isOneSync) ? 16 : playerDatas[source].slot | 0,
|
||||
hindex: hostIndex,
|
||||
sessionFlags: 0,
|
||||
mode: 0,
|
||||
size: (isOneSync) ? 0 : Object.entries(playerDatas).filter(a => a[1].id).length,
|
||||
//size: 2,
|
||||
//size: Object.entries(playerDatas).length,
|
||||
teamIndex: 0,
|
||||
transitionId: {
|
||||
value: {
|
||||
a: 2,
|
||||
b: 0
|
||||
}
|
||||
},
|
||||
sessionManagerType: 0,
|
||||
slotCount: 32
|
||||
});
|
||||
}, 50);
|
||||
|
||||
if (!isOneSync) {
|
||||
setTimeout(() => {
|
||||
// tell player about everyone, and everyone about player
|
||||
const meData = playerDatas[source];
|
||||
|
||||
const aboutMe = {
|
||||
id: meData.id,
|
||||
gh: meData.gh,
|
||||
addr: meData.peerAddress,
|
||||
index: playerDatas[source].slot | 0
|
||||
};
|
||||
|
||||
for (const [ id, data ] of Object.entries(playerDatas)) {
|
||||
if (id == source || !data.id) continue;
|
||||
|
||||
emitAddPlayer(source, {
|
||||
id: data.id,
|
||||
gh: data.gh,
|
||||
addr: data.peerAddress,
|
||||
index: data.slot | 0
|
||||
});
|
||||
|
||||
emitAddPlayer(id, aboutMe);
|
||||
}
|
||||
}, 150);
|
||||
}
|
||||
}, 250);
|
||||
|
||||
return makeResponse(QueueForSessionResult, {
|
||||
code: 1
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
async function handleMessage(source, method, data) {
|
||||
if (handlers[method]) {
|
||||
return await handlers[method](source, data);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
onNet('__cfx_internal:pbRlScSession', async (data) => {
|
||||
const s = source;
|
||||
|
||||
try {
|
||||
const message = RpcMessage.decode(new Uint8Array(data));
|
||||
const response = await handleMessage(s, message.Header.MethodName, message.Content);
|
||||
|
||||
if (!response || !response.Header) {
|
||||
return;
|
||||
}
|
||||
|
||||
response.Header.RequestId = message.Header.RequestId;
|
||||
|
||||
emitMsg(s, RpcResponseMessage.encode(response).finish());
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.log(e.stack);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
--This empty file causes the scheduler.lua to load clientside
|
||||
--scheduler.lua when loaded inside the sessionmanager resource currently manages remote callbacks.
|
||||
--Without this, callbacks will only work server->client and not client->server.
|
||||
@@ -0,0 +1,13 @@
|
||||
-- This resource is part of the default Cfx.re asset pack (cfx-server-data)
|
||||
-- Altering or recreating for local use only is strongly discouraged.
|
||||
|
||||
version '1.0.0'
|
||||
author 'Cfx.re <root@cfx.re>'
|
||||
description 'Handles the "host lock" for non-OneSync servers. Do not disable.'
|
||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||
|
||||
fx_version 'cerulean'
|
||||
games { 'gta4', 'gta5' }
|
||||
|
||||
server_script 'server/host_lock.lua'
|
||||
client_script 'client/empty.lua'
|
||||
@@ -0,0 +1,69 @@
|
||||
-- whitelist c2s events
|
||||
RegisterServerEvent('hostingSession')
|
||||
RegisterServerEvent('hostedSession')
|
||||
|
||||
-- event handler for pre-session 'acquire'
|
||||
local currentHosting
|
||||
local hostReleaseCallbacks = {}
|
||||
|
||||
-- TODO: add a timeout for the hosting lock to be held
|
||||
-- TODO: add checks for 'fraudulent' conflict cases of hosting attempts (typically whenever the host can not be reached)
|
||||
AddEventHandler('hostingSession', function()
|
||||
-- if the lock is currently held, tell the client to await further instruction
|
||||
if currentHosting then
|
||||
TriggerClientEvent('sessionHostResult', source, 'wait')
|
||||
|
||||
-- register a callback for when the lock is freed
|
||||
table.insert(hostReleaseCallbacks, function()
|
||||
TriggerClientEvent('sessionHostResult', source, 'free')
|
||||
end)
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
-- if the current host was last contacted less than a second ago
|
||||
if GetHostId() then
|
||||
if GetPlayerLastMsg(GetHostId()) < 1000 then
|
||||
TriggerClientEvent('sessionHostResult', source, 'conflict')
|
||||
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
hostReleaseCallbacks = {}
|
||||
|
||||
currentHosting = source
|
||||
|
||||
TriggerClientEvent('sessionHostResult', source, 'go')
|
||||
|
||||
-- set a timeout of 5 seconds
|
||||
SetTimeout(5000, function()
|
||||
if not currentHosting then
|
||||
return
|
||||
end
|
||||
|
||||
currentHosting = nil
|
||||
|
||||
for _, cb in ipairs(hostReleaseCallbacks) do
|
||||
cb()
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
AddEventHandler('hostedSession', function()
|
||||
-- check if the client is the original locker
|
||||
if currentHosting ~= source then
|
||||
-- TODO: drop client as they're clearly lying
|
||||
print(currentHosting, '~=', source)
|
||||
return
|
||||
end
|
||||
|
||||
-- free the host lock (call callbacks and remove the lock value)
|
||||
for _, cb in ipairs(hostReleaseCallbacks) do
|
||||
cb()
|
||||
end
|
||||
|
||||
currentHosting = nil
|
||||
end)
|
||||
|
||||
EnableEnhancedHostSupport(true)
|
||||
Reference in New Issue
Block a user