├── .gitattributes ├── src ├── client │ └── index.js ├── queue.config.json └── server │ └── index.js ├── fxmanifest.lua ├── LICENSE └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /src/client/index.js: -------------------------------------------------------------------------------- 1 | let clientID = setInterval(function () { 2 | if (NetworkIsSessionStarted()) { // When the user has loaded in to the server, shift the queue 3 | emitNet('pQueue:shiftQueue'); 4 | clearInterval(clientID); 5 | } 6 | }, 500) 7 | -------------------------------------------------------------------------------- /fxmanifest.lua: -------------------------------------------------------------------------------- 1 | -- Define the FX Server version and game type 2 | fx_version "adamant" 3 | game "gta5" 4 | 5 | -- Define the resource metadata 6 | name "pQueue" 7 | description "FiveM queue with priority based on discord roles (Requires sPerms)" 8 | author "Petrikov" 9 | version "0.0.0" 10 | 11 | server_script 'src/server/index.js' 12 | client_script 'src/client/index.js' -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 devPetrikov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/queue.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "debug": false, 4 | "alwaysUse": false, 5 | "noDiscordRejectMsg": "We could not find your Discord ID, please make sure Discord is running and that you're logged in." , 6 | "graceListTime": 2 7 | }, 8 | "adaptiveCard": { 9 | "card_title_isVisible": true, 10 | "card_title": "Title", 11 | "card_header": "https://i.ibb.co/6DWQg68/dxIwJT3.png", 12 | "card_description": "card description", 13 | "button1_title": "Button1", 14 | "button1_url": "https://someurl.com", 15 | "button2_title": "Button2", 16 | "button2_url": "https://someurl.com" 17 | }, 18 | "priority_setup": [ 19 | { 20 | "category": "category", 21 | "role": "headAdmin", 22 | "prio": 1 23 | }, 24 | { 25 | "category": "category", 26 | "role": "staff", 27 | "prio": 2 28 | }, 29 | { 30 | "category": "category", 31 | "role": "donator", 32 | "prio": 3 33 | } 34 | ], 35 | "defaultPrio": 4 36 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pQueue 2 | 3 | This is a FiveM server queue with discord based priority built on [SpaceTheDev](https://github.com/SpaceTheDev/)'s Discord API. 4 | You **NEED** [sPerms](https://forum.cfx.re/t/release-sperms-real-time-discord-perms/1686063) and [sDiscord](https://forum.cfx.re/t/release-sdiscord/1680021) for this resource to work. 5 | 6 | # Installation 7 | Copy or download the resource in your resource folder, and add ``ensure pQueue`` to your ``server.cfg`` 8 | 9 | # Config 10 | The config (`queue.config.json`) file can be found in the ``src`` folder. 11 | In the config file you will find three different sections the first being the settings section: 12 | ```js 13 | "settings": { 14 | "debug": false, 15 | "alwaysUse": false, 16 | "noDiscordRejectMsg": "Change Me", 17 | "graceListTime": 5 18 | } 19 | ``` 20 | ``debug`` Will enable debug messages in your console, such as: users being added to the queue and their priority, users being removed from the queue and the queue itself. 21 | 22 | ``alwaysUse`` If set to true the queue will ALWAYS be used, regardless of the number of people in server, this will only allow one user to connect at a time. If set to false the queue will only take effect if there's < 5 open slots. 23 | 24 | ``noDiscordRejectMsg`` The message presented to a user when rejected for not having a Discord ID. 25 | 26 | ``graceListTime`` How long a user that has just disconnect/crashed has too reconnect before they will be put at the end of the queue again. (In minutes) 27 | 28 | 29 | The next section is for customizing the Adaptive Card UI presented to users while in the queue. Should be obvious what the different settings do, see the screenshot or the comments down below if you are still unsure. 30 | **Note: If you copy this over to your config file the comments MUST be removed** 31 | ```js 32 | "adaptiveCard": { 33 | "card_title_isVisible": false, // decides whether the title is visible, defaults to false as you will most likely have your community name in the header. 34 | "card_title": "Title", // the cards title, recommended use is for your community's name. 35 | "card_header": "https://i.ibb.co/6DWQg68/dxIwJT3.png", // a link to the header picture 36 | "card_description": "card description", // a short description can be used for messages such as "While you're waiting check out our Discord" 37 | "button1_title": "Button 1", // The title for the first button 38 | "button1_url": "https://discord.gg/ssrp", // The URL the first button should open. 39 | "button2_title": "Button 2", // The title for the second button 40 | "button2_url": "https://instagram.com/ssrp.leo/" // The URL the second button should open. 41 | }, 42 | ``` 43 | ![Alt text](https://i.ibb.co/7CT9rQK/Screenshot-29.png "Adaptive Card Layout") 44 | 45 | 46 | 47 | 48 | The third and the final section is the most complicated to set up as it requires an understanding of how [sPerms](https://forum.cfx.re/t/release-sperms-real-time-discord-perms/1686063) and [sDiscord](https://forum.cfx.re/t/release-sdiscord/1680021) works, as well as some experience with working with objects in JS. But if you follow all the following steps you should be able to set everything up without problems: 49 | 50 | 1. Download and set up sDiscord and sPerms 51 | 52 | 2. Add the roles you want to set up priority for in the sPerms config file (src/config.json). 53 | In the config file the individual roles are divided into categories, example configuration: 54 | ```js 55 | { 56 | "discordRoles": { 57 | "administration": { 58 | "owner": "Discord Role ID", 59 | "coOwner": "Discord Role ID", 60 | "headDev": "Disord Role ID" 61 | }, 62 | "staff": { 63 | "admin": "Discord Role ID", 64 | "mod": "Discord Role ID" 65 | } 66 | }, 67 | "needDiscord": false 68 | } 69 | ``` 70 | When sPerms builds the ``perms`` object it checks each individual role, but also the different categories (if you have one role in a category, the category will return as true). We can see the built ``perms`` object by going into the client script in sPerms (src/client/index.js) and adding ``console.log(perms)`` to the ``sPerms:setPerms`` event, this will log the object in the player's console when they first spawn in: 71 | 72 | ![Alt text](https://i.ibb.co/kgmv3v1/image-2021-01-16-210720.png "Structure of the built perms") 73 | 74 | 3. Add the roles you want to set up for priority to the ``pQueue`` config file (``queue.config.json``). 75 | ```js 76 | { 77 | "category": "category", 78 | "role": "roleName", 79 | "prio": 1 80 | } 81 | ``` 82 | ``category`` If you want to check for an individual role this should be the category the role is under, ex. staff. If you want to check for a whole category this should just be "category" 83 | 84 | ``role`` If you are checking for a role, this should be the name of the role, ex. owner. If you are checking a whole category it should be the name of the category ex. staff. 85 | 86 | ``prio`` This is the priority, the lower the number the higher the priority. (Use whole numbers) 87 | 88 | Here is an example for how you could set up the priority: 89 | ```js 90 | { 91 | "category": "administration", 92 | "role": "owner", 93 | "prio": 1 94 | }, 95 | { 96 | "category": "category", 97 | "role": "administration", 98 | "prio": 2 99 | }, 100 | { 101 | "category": "category", 102 | "role": "staff", 103 | "prio": 3 104 | } 105 | ``` 106 | **NOTE: Make sure you sort the priority from highest to lowest, if not the script might not use the highest priority if a user has multiple roles** 107 | 108 | Lastly, set ``defaultPrio`` to a higher number than all the priority roles/categories, in the last example it would be ``4`` or higher. 109 | 110 | 111 | If everything has been done correctly, the script should now work as intended. If you have any issues, feel free to reach out to me on Discord (MightyViking#9126) 112 | -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | const config = require("./src/queue.config.json"); 2 | 3 | // Originally written by devpetrikov for use in Sunshine State RP (discord.gg/ssrp) 4 | 5 | StopResource("hardcap"); // Stopping the hardcap resource as it will reject connections when the server is full and thus the queue won't work 6 | var msg; 7 | 8 | var graceList = []; 9 | 10 | on('playerConnecting', (name, setKickReason, deferrals) => { 11 | deferrals.defer(); // stops the user from being connected to the server 12 | deferrals.update(`Hello ${name}. Your discord roles are currently being checked...`); // updates the message on the users screen 13 | const src = global.source; 14 | let idFound = false; 15 | for (let i = 0; i < GetNumPlayerIdentifiers(src); i++) { // finds the users discord ID 16 | const identifier = GetPlayerIdentifier(src, i); 17 | 18 | if (identifier.includes('discord:')) { 19 | discordIdentifier = identifier.slice(8); 20 | idFound = true; 21 | }; 22 | }; 23 | if(!idFound) { 24 | deferrals.done(config.settings.noDiscordRejectMsg); //rejects the connecting user if they don't have a dicsord ID 25 | } 26 | addToQueue(discordIdentifier , src); // add the player to the queue 27 | var intervalId = setInterval(function () { 28 | for (let i = 0; i < GetNumPlayerIdentifiers(src); i++) { 29 | const identifier = GetPlayerIdentifier(src, i); 30 | if (identifier.includes('discord:')) { 31 | discordIdentifier = identifier.slice(8); 32 | } 33 | } 34 | if (!isUserInQueue(discordIdentifier)) { // stops the interval if the user is no longer in the queue 35 | clearInterval(intervalId); 36 | } 37 | checkQueue((cb) => { //checks if there is open server slots 38 | if(cb == true) { 39 | if (GetConvar("sv_maxclients") - GetNumPlayerIndices() > 4) { //Checks if there's more than 5 open slots 40 | if(config.settings.alwaysUse) { // checks if the alwaysUse setting is enabled 41 | if(priorityQueue.front().element == discordIdentifier) { // checks if the user is number 1 in the queue 42 | deferrals.done(); // allows the user to connect to the server 43 | console.log(`Connecting: ${name}`) // since hardcap is stopped we have to log connecting users 44 | clearInterval(intervalId); // stops the interval 45 | } 46 | else { 47 | msg = `You are in queue [${findInQueue(discordIdentifier) + 1}/${priorityQueue.items.length}]`; 48 | updateCard(callback => { // call the function to update the adaptive card content 49 | deferrals.presentCard(callback); // update the card on client side 50 | }) 51 | } 52 | } 53 | else { // if there's more than 5 open slots and the alwaysUse setting is not disabled allow the user to connect without going through the queue 54 | deferrals.done(); 55 | } 56 | } 57 | else { 58 | if(priorityQueue.front().element == discordIdentifier) { // checks if the user is number 1 in the queue 59 | deferrals.done(); // allows the user to connect to the server 60 | console.log(`Connecting: ${name}`) // since hardcap is stopped we have to log connecting users 61 | clearInterval(intervalId); // stops the interval 62 | } 63 | else { 64 | msg = `You are in queue [${findInQueue(discordIdentifier) + 1}/${priorityQueue.items.length}]`; 65 | updateCard(callback => { // call the function to update the adaptive card content 66 | deferrals.presentCard(callback); // update the card on client side 67 | }) 68 | } 69 | } 70 | } 71 | else { 72 | msg = `You are in queue [${findInQueue(discordIdentifier) + 1}/${priorityQueue.items.length}]`; 73 | updateCard(callback => { // call the function to update the adaptive card content 74 | deferrals.presentCard(callback); // update the card on client side 75 | }) 76 | } 77 | }) 78 | }, 500); 79 | }) 80 | 81 | on('playerDropped', (reason) => { 82 | const src = global.source; 83 | for (let i = 0; i < GetNumPlayerIdentifiers(src); i++) { // finds the users discord ID 84 | const identifier = GetPlayerIdentifier(src, i); 85 | if (identifier.includes('discord:')) { 86 | discordIdentifier = identifier.slice(8); 87 | }; 88 | }; 89 | graceListInsert(discordIdentifier); 90 | setTimeout(function () { 91 | graceListRemove(discordIdentifier); 92 | }, config.settings.graceListTime * 60 * 1000) 93 | }) 94 | 95 | onNet('pQueue:shiftQueue', () => { //Removes the user in posistion 1 once they have connected to the server 96 | if(config.settings.debug) { 97 | console.log(`[DEBUG] ${priorityQueue.front().element} has been removed from the queue.`) 98 | } 99 | priorityQueue.remove(); 100 | }) 101 | 102 | setInterval(function removeGhostUsers() { //checks for and removes ghost users every 15 seconds 103 | for (var i = 0; i < priorityQueue.items.length; i++) { 104 | if(GetPlayerName(priorityQueue.items[i].source) == null){ 105 | if(config.settings.debug) { 106 | console.log(`[DEBUG] Removed ghost user: ${priorityQueue.items[i].element}`) 107 | } 108 | removeFromQueue(priorityQueue.items[i].element) 109 | } 110 | } 111 | }, 15000) 112 | 113 | if (config.settings.debug) { 114 | setInterval(function () { // debug function that prints the queue every 15 seconds 115 | console.log("[DEBUG] Queue: " + priorityQueue.printQueue()) 116 | }, 15000); 117 | }; 118 | 119 | function isUserInQueue (identifier) { // Checks if the user is still in the queue 120 | let b = false; 121 | for(let i = 0; i < priorityQueue.items.length; i++) { 122 | if (priorityQueue.items[i].element == identifier) { 123 | b = true; 124 | return b; 125 | } 126 | } 127 | } 128 | 129 | function addToQueue (identifier, src) { // adds a user to the queue 130 | emit('sPerms:getPerms', src, (perms) => { 131 | userPerms = perms; 132 | let prio = config.defaultPrio; 133 | for (let i = 0; i < config.priority_setup.length; i++) { 134 | let setup = config.priority_setup[i]; 135 | if(userPerms[setup.category][setup.role]) { 136 | prio = setup.prio; 137 | break; 138 | } 139 | } 140 | for (let i = 0; i < graceList.length; i++) { 141 | if (graceList[i] == identifier) { 142 | prio = 1; 143 | break; 144 | } 145 | } 146 | priorityQueue.insert(identifier, prio, src); 147 | if (config.settings.debug) { 148 | console.log(`[DEBUG] ${identifier} has been added to the queue with priority ${prio}`) 149 | } 150 | }) 151 | }; 152 | 153 | function removeFromQueue(identifier) { // removes a user from the queue 154 | for (var i = 0; i < priorityQueue.items.length; i++) { 155 | if (priorityQueue.items[i].element == identifier) { 156 | priorityQueue.items.splice(i, 1); 157 | if (config.settings.debug) { 158 | console.log(`[DEBUG] ${identifier} has been removed from the queue.`) 159 | } 160 | break; 161 | }; 162 | } 163 | } 164 | 165 | function findInQueue(identifier) { // find the user's placement in the queue 166 | for (var i = 0; i < priorityQueue.items.length; i++) { 167 | if (priorityQueue.items[i].element == identifier) { 168 | return i; 169 | } 170 | } 171 | } 172 | 173 | function checkQueue(cb) { // check if the server is full 174 | if (GetNumPlayerIndices() < GetConvar("sv_maxclients")) { 175 | cb(true); 176 | } 177 | else { 178 | cb(false); 179 | } 180 | } 181 | 182 | function graceListInsert(id) { 183 | graceList.push(id); 184 | if(config.settings.debug) { 185 | console.log(`[DEBUG] ${id} has been added to the grace list.`) 186 | } 187 | } 188 | function graceListRemove(id) { 189 | for (var i = 0; i < graceList.length; i++) { 190 | if (graceList[i] == id) { 191 | graceList.splice(i, 1); 192 | if (config.settings.debug) { 193 | console.log(`[DEBUG] ${discordIdentifier} has been removed from the grace list.`) 194 | } 195 | } 196 | } 197 | } 198 | 199 | // User defined class 200 | // to store elements and its priority 201 | class QElement { 202 | constructor(element, priority, source) 203 | { 204 | this.element = element; 205 | this.priority = priority; 206 | this.source = source; 207 | } 208 | } 209 | 210 | // PriorityQueue class 211 | class PriorityQueue { 212 | 213 | // An array is used to implement priority 214 | constructor() 215 | { 216 | this.items = []; 217 | } 218 | 219 | // insert function to add element to the queue as per priority 220 | insert(element, priority, source) 221 | { 222 | // creating object from queue element 223 | var qElement = new QElement(element, priority, source); 224 | var contain = false; 225 | 226 | // iterating through the entire item array to add element at the correct location of the Queue 227 | for (var i = 0; i < this.items.length; i++) { 228 | if (this.items[i].priority > qElement.priority) { 229 | // Once the correct location is found it is inserted 230 | this.items.splice(i, 0, qElement); 231 | contain = true; 232 | break; 233 | }; 234 | } 235 | 236 | // if the element have the highest priority it is added at the end of the queue 237 | if (!contain) { 238 | this.items.push(qElement); 239 | } 240 | } 241 | 242 | // remove method to remove element from the queue 243 | remove() 244 | { 245 | //return the remove element and remove it. If the queue is empty returs UnderFlow 246 | if (this.isEmpty()) 247 | return "UnderFlow"; 248 | return this.items.shift(); 249 | } 250 | 251 | // front function 252 | front() { 253 | //returns the highest priority element in the priority queue wightout removing it 254 | if (this.isEmpty()) 255 | return "No elements in Queue"; 256 | return this.items[0]; 257 | } 258 | 259 | // rear function 260 | rear() { 261 | // returns the lowest priority element of the queue 262 | if (this.isEmpty()) 263 | return "No elements in Queue"; 264 | return this.items[this.items.length -1]; 265 | } 266 | // isEmpty function 267 | isEmpty() { 268 | //return true if the queue is empty. 269 | return this.items.length == 0; 270 | } 271 | // printQueue function prints all the elements of the queue 272 | printQueue() 273 | { 274 | var str = ""; 275 | for (var i = 0; i < this.items.length; i++) 276 | str += this.items[i].element + ", "; 277 | return str; 278 | } 279 | } 280 | 281 | var priorityQueue = new PriorityQueue(); 282 | 283 | function updateCard(callback) { // Updates the adaptive card content and sends a callback with said content so that it can be sent to the user 284 | var card = { 285 | "type":"AdaptiveCard", 286 | "body":[ 287 | { 288 | "type":"Image", 289 | "url": config.adaptiveCard.card_header, 290 | "horizontalAlignment":"Center" 291 | }, 292 | { 293 | "type":"Container", 294 | "items": 295 | [ 296 | { 297 | "type":"TextBlock", 298 | "text": config.adaptiveCard.card_title, 299 | "wrap":true, 300 | "fontType":"Default", 301 | "size":"ExtraLarge", 302 | "weight":"Bolder", 303 | "color":"light", 304 | "horizontalAlignment":"Center", 305 | "isVisible": config.adaptiveCard.card_title_isVisible 306 | }, 307 | { 308 | "type":"TextBlock", 309 | "text": msg, 310 | "wrap":true, 311 | "size":"Large", 312 | "weight":"Bolder", 313 | "color":"Light", 314 | "horizontalAlignment":"Center" 315 | }, 316 | { 317 | "type":"TextBlock", 318 | "text": config.adaptiveCard.card_description, 319 | "wrap":true, 320 | "color":"Light","size":"Medium", 321 | "horizontalAlignment":"Center" 322 | }, 323 | { 324 | "type":"ColumnSet","height":"stretch", 325 | "minHeight":"35px","bleed":true, 326 | "horizontalAlignment":"Center", 327 | "columns": 328 | [ 329 | { 330 | "type":"Column", 331 | "width":"stretch", 332 | "items": 333 | [ 334 | { 335 | "type":"ActionSet", 336 | "actions": 337 | [ 338 | { 339 | "type":"Action.OpenUrl", 340 | "title": config.adaptiveCard.button1_title, 341 | "style":"positive" 342 | } 343 | ] 344 | } 345 | ], 346 | "height":"stretch" 347 | }, 348 | { 349 | "type":"Column","width":"stretch", 350 | "items": 351 | [ 352 | { 353 | "type":"ActionSet", 354 | "actions": 355 | [ 356 | { 357 | "type":"Action.OpenUrl", 358 | "title": config.adaptiveCard.button2_title, 359 | "style":"positive", 360 | "url": config.adaptiveCard.button2_url 361 | } 362 | ] 363 | } 364 | ] 365 | } 366 | ] 367 | } 368 | ], 369 | "style":"default", 370 | "bleed":true, 371 | "height":"automatic", 372 | "isVisible":true 373 | } 374 | ], 375 | "$schema":"http://adaptivecards.io/schemas/adaptive-card.json", 376 | "version":"1.3" 377 | } 378 | callback(card); 379 | } 380 | --------------------------------------------------------------------------------