├── monitor ├── docs │ ├── banner.png │ ├── zaphosting.png │ ├── cache_wiping.md │ ├── custom_serverlog.md │ ├── permissions.md │ ├── troubleshooting.md │ ├── feature_graveyard.md │ ├── translation.md │ ├── logs.md │ ├── newPlayerlist.md │ ├── events.md │ ├── development.md │ ├── menu.md │ └── newDatabase.md ├── web │ ├── public │ │ ├── img │ │ │ ├── tx.png │ │ │ ├── tx2.gif │ │ │ ├── txadmin.png │ │ │ ├── txadminred.png │ │ │ ├── txadmin_beta.png │ │ │ ├── txadmingold.png │ │ │ ├── zap256_black.png │ │ │ ├── zap256_white.png │ │ │ ├── default_avatar.png │ │ │ ├── favicon_default.png │ │ │ ├── favicon_offline.png │ │ │ ├── favicon_online.png │ │ │ ├── favicon_partial.png │ │ │ └── txSnaily2anim_320.png │ │ ├── fonts │ │ │ ├── Simple-Line-Icons.ttf │ │ │ ├── Simple-Line-Icons.woff │ │ │ └── Simple-Line-Icons.woff2 │ │ ├── css │ │ │ ├── foldgutter.css │ │ │ └── codemirror_lucario.css │ │ └── js │ │ │ ├── codeEditor │ │ │ └── mode │ │ │ │ ├── fivem-cfg.js │ │ │ │ ├── yaml.js │ │ │ │ └── simple.js │ │ │ ├── txadmin │ │ │ └── chart.js │ │ │ └── bootstrap-notify.min.js │ ├── main │ │ ├── message.ejs │ │ ├── txAdminLog.ejs │ │ ├── cfgEditor.ejs │ │ ├── advanced.ejs │ │ └── diagnostics.ejs │ ├── standalone │ │ └── 404.ejs │ └── parts │ │ ├── footer.ejs │ │ ├── changePasswordModal.ejs │ │ └── adminModal.ejs ├── nui │ ├── images │ │ └── txadmin.png │ ├── sounds │ │ ├── warning_open.mp3 │ │ └── warning_pulse.mp3 │ ├── index.css │ └── index.html ├── resource │ ├── menu │ │ ├── vendor │ │ │ └── freecam │ │ │ │ ├── INFO.txt │ │ │ │ ├── LICENSE.txt │ │ │ │ ├── utils.lua │ │ │ │ ├── config.lua │ │ │ │ ├── camera.lua │ │ │ │ └── main.lua │ │ ├── server │ │ │ ├── sv_functions.lua │ │ │ ├── sv_freeze_player.lua │ │ │ ├── sv_player_mode.lua │ │ │ ├── sv_trollactions.lua │ │ │ ├── sv_player_modal.lua │ │ │ ├── sv_spectate.lua │ │ │ ├── sv_base.lua │ │ │ ├── sv_webpipe.lua │ │ │ └── sv_main_page.lua │ │ ├── shared.lua │ │ └── client │ │ │ ├── cl_misc.lua │ │ │ ├── cl_freeze.lua │ │ │ ├── cl_webpipe.lua │ │ │ ├── cl_functions.lua │ │ │ ├── cl_player_ids.lua │ │ │ ├── cl_trollactions.lua │ │ │ ├── cl_player_mode.lua │ │ │ └── cl_base.lua │ ├── sv_resources.lua │ ├── sv_admins.lua │ ├── cl_main.lua │ ├── sv_playerlist.lua │ ├── cl_playerlist.lua │ └── cl_logger.lua ├── entrypoint.js └── fxmanifest.lua └── README.md /monitor/docs/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marshxan/QBCORE-Monitor/HEAD/monitor/docs/banner.png -------------------------------------------------------------------------------- /monitor/docs/zaphosting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marshxan/QBCORE-Monitor/HEAD/monitor/docs/zaphosting.png -------------------------------------------------------------------------------- /monitor/web/public/img/tx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marshxan/QBCORE-Monitor/HEAD/monitor/web/public/img/tx.png -------------------------------------------------------------------------------- /monitor/nui/images/txadmin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marshxan/QBCORE-Monitor/HEAD/monitor/nui/images/txadmin.png -------------------------------------------------------------------------------- /monitor/web/public/img/tx2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marshxan/QBCORE-Monitor/HEAD/monitor/web/public/img/tx2.gif -------------------------------------------------------------------------------- /monitor/web/public/img/txadmin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marshxan/QBCORE-Monitor/HEAD/monitor/web/public/img/txadmin.png -------------------------------------------------------------------------------- /monitor/nui/sounds/warning_open.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marshxan/QBCORE-Monitor/HEAD/monitor/nui/sounds/warning_open.mp3 -------------------------------------------------------------------------------- /monitor/nui/sounds/warning_pulse.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marshxan/QBCORE-Monitor/HEAD/monitor/nui/sounds/warning_pulse.mp3 -------------------------------------------------------------------------------- /monitor/web/public/img/txadminred.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marshxan/QBCORE-Monitor/HEAD/monitor/web/public/img/txadminred.png -------------------------------------------------------------------------------- /monitor/web/public/img/txadmin_beta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marshxan/QBCORE-Monitor/HEAD/monitor/web/public/img/txadmin_beta.png -------------------------------------------------------------------------------- /monitor/web/public/img/txadmingold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marshxan/QBCORE-Monitor/HEAD/monitor/web/public/img/txadmingold.png -------------------------------------------------------------------------------- /monitor/web/public/img/zap256_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marshxan/QBCORE-Monitor/HEAD/monitor/web/public/img/zap256_black.png -------------------------------------------------------------------------------- /monitor/web/public/img/zap256_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marshxan/QBCORE-Monitor/HEAD/monitor/web/public/img/zap256_white.png -------------------------------------------------------------------------------- /monitor/web/public/img/default_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marshxan/QBCORE-Monitor/HEAD/monitor/web/public/img/default_avatar.png -------------------------------------------------------------------------------- /monitor/web/public/img/favicon_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marshxan/QBCORE-Monitor/HEAD/monitor/web/public/img/favicon_default.png -------------------------------------------------------------------------------- /monitor/web/public/img/favicon_offline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marshxan/QBCORE-Monitor/HEAD/monitor/web/public/img/favicon_offline.png -------------------------------------------------------------------------------- /monitor/web/public/img/favicon_online.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marshxan/QBCORE-Monitor/HEAD/monitor/web/public/img/favicon_online.png -------------------------------------------------------------------------------- /monitor/web/public/img/favicon_partial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marshxan/QBCORE-Monitor/HEAD/monitor/web/public/img/favicon_partial.png -------------------------------------------------------------------------------- /monitor/web/public/img/txSnaily2anim_320.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marshxan/QBCORE-Monitor/HEAD/monitor/web/public/img/txSnaily2anim_320.png -------------------------------------------------------------------------------- /monitor/web/public/fonts/Simple-Line-Icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marshxan/QBCORE-Monitor/HEAD/monitor/web/public/fonts/Simple-Line-Icons.ttf -------------------------------------------------------------------------------- /monitor/web/public/fonts/Simple-Line-Icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marshxan/QBCORE-Monitor/HEAD/monitor/web/public/fonts/Simple-Line-Icons.woff -------------------------------------------------------------------------------- /monitor/web/public/fonts/Simple-Line-Icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marshxan/QBCORE-Monitor/HEAD/monitor/web/public/fonts/Simple-Line-Icons.woff2 -------------------------------------------------------------------------------- /monitor/resource/menu/vendor/freecam/INFO.txt: -------------------------------------------------------------------------------- 1 | The original source code for freecam by Deltanic is available at this repository: 2 | https://github.com/Deltanic/fivem-freecam/ 3 | 4 | This code contains modifications and performance improvements, and is re-released under the MIT license. -------------------------------------------------------------------------------- /monitor/docs/cache_wiping.md: -------------------------------------------------------------------------------- 1 | ### Cache Wiping 2 | 3 | This feature was requested many times and in the end we decided against putting it into txAdmin for the following reasons: 4 | - Its just not necessary with the newest builds of FiveM anymore. 5 | - It can be dangerous considering that not configuring it correctly might end up recursively wiping out important files. 6 | 7 | More info: https://forum.fivem.net/t/why-people-delete-the-server-cache-folder/573851 8 | -------------------------------------------------------------------------------- /monitor/nui/index.css: -------------------------------------------------------------------------------- 1 | .App{height:100%;display:flex;flex-direction:column;transition:opacity .15s linear}::-webkit-scrollbar{width:7px}::-webkit-scrollbar-track{border-radius:20px}::-webkit-scrollbar-thumb{background:#24282b;border-radius:20px}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Inter,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;padding:20px 40px;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;height:100vh;overflow:hidden}#root{height:100%} 2 | -------------------------------------------------------------------------------- /monitor/web/main/message.ejs: -------------------------------------------------------------------------------- 1 | <%- await include('parts/header.ejs', locals) %> 2 | 3 |
4 |

Message

5 |
6 | 7 |
8 |
9 | 12 |
13 |
14 | 15 | 18 | 19 | 20 | <%- await include('parts/footer.ejs', locals) %> 21 | -------------------------------------------------------------------------------- /monitor/web/public/css/foldgutter.css: -------------------------------------------------------------------------------- 1 | .CodeMirror-foldmarker { 2 | color: blue; 3 | text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px, #b9f -1px 1px 2px; 4 | font-family: arial; 5 | line-height: .3; 6 | cursor: pointer; 7 | } 8 | .CodeMirror-foldgutter { 9 | width: .7em; 10 | } 11 | .CodeMirror-foldgutter-open, 12 | .CodeMirror-foldgutter-folded { 13 | cursor: pointer; 14 | } 15 | .CodeMirror-foldgutter-open:after { 16 | content: "\25BE"; 17 | } 18 | .CodeMirror-foldgutter-folded:after { 19 | content: "\25B8"; 20 | } 21 | -------------------------------------------------------------------------------- /monitor/nui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | txAdmin NUI 7 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /monitor/resource/menu/server/sv_functions.lua: -------------------------------------------------------------------------------- 1 | --Check Environment 2 | if GetConvar('txAdminServerMode', 'false') ~= 'true' then 3 | return 4 | end 5 | 6 | --- Determine if a source has a given permission 7 | ---@param source number 8 | ---@param reqPerm string 9 | ---@return boolean 10 | function PlayerHasTxPermission(source, reqPerm) 11 | local allow = false 12 | local admin = TX_ADMINS[tostring(source)] 13 | if admin and admin.perms then 14 | for _, perm in pairs(admin.perms) do 15 | if perm == 'all_permissions' or reqPerm == perm then 16 | allow = true 17 | break 18 | end 19 | end 20 | end 21 | debugPrint(string.format("permission check (src=^3%d^0, perm=^4%s^0, result=%s^0)", 22 | source, reqPerm, (allow and '^2true' or '^1false'))) 23 | return allow 24 | end 25 | -------------------------------------------------------------------------------- /monitor/resource/menu/server/sv_freeze_player.lua: -------------------------------------------------------------------------------- 1 | --Check Environment 2 | if GetConvar('txAdminServerMode', 'false') ~= 'true' then 3 | return 4 | end 5 | 6 | local frozenPlayers = {} 7 | 8 | local function isPlayerFrozen(targetId) 9 | return frozenPlayers[targetId] or false 10 | end 11 | 12 | local function setPlayerFrozenInMap(targetId, status) 13 | frozenPlayers[targetId] = status or nil 14 | end 15 | 16 | RegisterNetEvent("txAdmin:menu:freezePlayer", function(targetId) 17 | local src = source 18 | local allow = PlayerHasTxPermission(src, 'players.freeze') 19 | TriggerEvent("txaLogger:menuEvent", src, "freezePlayer", allow, targetId) 20 | if allow then 21 | local newFrozenStatus = not isPlayerFrozen(targetId) 22 | setPlayerFrozenInMap(targetId, newFrozenStatus) 23 | 24 | TriggerClientEvent("txAdmin:menu:freezeResp", src, newFrozenStatus) 25 | TriggerClientEvent("txAdmin:menu:freezePlayer", targetId, newFrozenStatus) 26 | end 27 | end) 28 | -------------------------------------------------------------------------------- /monitor/resource/menu/shared.lua: -------------------------------------------------------------------------------- 1 | debugModeEnabled = false 2 | 3 | function debugPrint(...) 4 | local args = {...} 5 | local appendedStr = '' 6 | if debugModeEnabled then 7 | for _, v in ipairs(args) do 8 | appendedStr = appendedStr .. ' ' .. (type(v)=="table" and json.encode(v) or tostring(v)) 9 | end 10 | local msgTemplate = '^3[txAdminMenu]^0%s^0' 11 | local msg = msgTemplate:format(appendedStr) 12 | print(msg) 13 | end 14 | end 15 | 16 | -- Used whenever we want to convey a message as from txAdminMenu without 17 | -- being in debug mode. 18 | function txPrint(...) 19 | local args = {...} 20 | local appendedStr = '' 21 | for _, v in ipairs(args) do 22 | appendedStr = appendedStr .. ' ' .. tostring(v) 23 | end 24 | local msgTemplate = '^3[txAdminMenu]^0%s^0' 25 | local msg = msgTemplate:format(appendedStr) 26 | print(msg) 27 | end 28 | 29 | CreateThread(function() 30 | debugModeEnabled = (GetConvar('txAdmin-menuDebug', 'false') == 'true') 31 | end) 32 | -------------------------------------------------------------------------------- /monitor/resource/menu/server/sv_player_mode.lua: -------------------------------------------------------------------------------- 1 | --Check Environment 2 | if GetConvar('txAdminServerMode', 'false') ~= 'true' then 3 | return 4 | end 5 | 6 | local IS_PTFX_DISABLED = (GetConvar('txAdmin-menuPtfxDisable', 'false') == 'true') 7 | 8 | RegisterNetEvent('txAdmin:menu:playerModeChanged', function(mode, nearbyPlayers) 9 | local src = source 10 | if mode ~= 'godmode' and mode ~= 'noclip' and mode ~= 'none' then 11 | debugPrint("Invalid player mode requested by " .. GetPlayerName(src) .. " (mode: " .. (mode or 'nil')) 12 | return 13 | end 14 | 15 | local allow = PlayerHasTxPermission(src, 'players.playermode') 16 | TriggerEvent("txaLogger:menuEvent", src, "playerModeChanged", allow, mode) 17 | if allow then 18 | TriggerClientEvent('txAdmin:menu:playerModeChanged', src, mode, not IS_PTFX_DISABLED) 19 | 20 | if not IS_PTFX_DISABLED then 21 | for _, v in ipairs(nearbyPlayers) do 22 | TriggerClientEvent('txcl:syncPtfxEffect', v, src) 23 | end 24 | end 25 | end 26 | end) 27 | -------------------------------------------------------------------------------- /monitor/entrypoint.js: -------------------------------------------------------------------------------- 1 | //NOTE: Due to fxs's node, declaring ANY variable in this file will pollute 2 | // the global scope, and it will NOT show in `Object.keys(global)`! 3 | // Hence why I'm doing some data juggling and duplicated function calls. 4 | 5 | //Check if running inside FXServer 6 | try { 7 | if (!IsDuplicityVersion()) throw new Error(); 8 | } catch (error) { 9 | console.log('txAdmin must be run inside FXServer in monitor mode!'); 10 | process.exit(); 11 | } 12 | 13 | //Checking monitor mode and starting 14 | try { 15 | if (GetConvar('monitorMode', 'false') == 'true') { 16 | require('./core/index.js'); 17 | } else if (GetConvar('txAdminServerMode', 'false') == 'true') { 18 | //Nothing, for now 19 | } 20 | } catch (error) { 21 | //Prevent any async console.log messing with the output 22 | process.stdout.write([ 23 | 'e'.repeat(80), 24 | `Resource load error: ${error.message}`, 25 | error.stack.toString(), 26 | 'e'.repeat(80), 27 | '' 28 | ].join('\n')); 29 | } 30 | -------------------------------------------------------------------------------- /monitor/docs/custom_serverlog.md: -------------------------------------------------------------------------------- 1 | # Logging Extra Data 2 | 3 | This feature allows you to add logging for custom commands like `/car` and `/tp`. 4 | To do that, you will need to edit the scripts of those commands to trigger a `txaLogger:CommandExecuted` event. 5 | > **Note: for now this only supports client commands!** 6 | 7 | ## How to Enable 8 | 9 | In the client script, add the following event call inside the command function: 10 | 11 | ```lua 12 | TriggerServerEvent('txaLogger:CommandExecuted', rawCommand) 13 | ``` 14 | 15 | Where `rawCommand` is a variable containing the full command with parameters. 16 | You don't NEED to pass `rawCommand`, you can edit this string or pass anything you want. 17 | 18 | ## Example 19 | 20 | In this example, we will log data from the `/car` command from the `CarCommand` script. 21 | 22 | ```lua 23 | RegisterCommand('car', function(source, args, rawCommand) 24 | TriggerServerEvent('txaLogger:CommandExecuted', rawCommand) -- txAdmin logging Callback 25 | 26 | local x,y,z = table.unpack(GetOffsetFromEntityInWorldCoords(PlayerPedId(), 0.0, 8.0, 0.5)) 27 | 28 | -- there is more code here, no need to edit 29 | end) 30 | ``` 31 | -------------------------------------------------------------------------------- /monitor/resource/menu/server/sv_trollactions.lua: -------------------------------------------------------------------------------- 1 | --Check Environment 2 | if GetConvar('txAdminServerMode', 'false') ~= 'true' then 3 | return 4 | end 5 | 6 | RegisterNetEvent('txAdmin:menu:drunkEffectPlayer', function(id) 7 | local src = source 8 | local allow = PlayerHasTxPermission(src, 'players.troll') 9 | if allow then 10 | TriggerClientEvent('txAdmin:menu:drunkEffect', id) 11 | end 12 | TriggerEvent('txaLogger:menuEvent', src, 'drunkEffect', allow, id) 13 | end) 14 | 15 | RegisterNetEvent('txAdmin:menu:setOnFire', function(id) 16 | local src = source 17 | local allow = PlayerHasTxPermission(src, 'players.troll') 18 | if allow then 19 | TriggerClientEvent('txAdmin:menu:setOnFire', id) 20 | end 21 | TriggerEvent('txaLogger:menuEvent', src, 'setOnFire', allow, id) 22 | end) 23 | 24 | RegisterNetEvent('txAdmin:menu:wildAttack', function(id) 25 | local src = source 26 | local allow = PlayerHasTxPermission(src, 'players.troll') 27 | if allow then 28 | TriggerClientEvent('txAdmin:menu:wildAttack', id) 29 | end 30 | TriggerEvent('txaLogger:menuEvent', src, 'wildAttack', allow, id) 31 | end) 32 | -------------------------------------------------------------------------------- /monitor/resource/menu/vendor/freecam/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Deltanic 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. -------------------------------------------------------------------------------- /monitor/resource/menu/client/cl_misc.lua: -------------------------------------------------------------------------------- 1 | -- ============================================= 2 | -- This file contains misc stuff, maybe deprecate? 3 | -- ============================================= 4 | if (GetConvar('txAdmin-menuEnabled', 'false') ~= 'true') then 5 | return 6 | end 7 | 8 | -- Consts 9 | SoundEnum = { 10 | move = 'NAV_UP_DOWN', 11 | enter = 'SELECT' 12 | } 13 | 14 | -- Audio play callback 15 | RegisterNUICallback('playSound', function(sound, cb) 16 | PlaySoundFrontend(-1, SoundEnum[sound], 'HUD_FRONTEND_DEFAULT_SOUNDSET', 1) 17 | cb({}) 18 | end) 19 | 20 | -- Heals local player 21 | RegisterNetEvent('txAdmin:menu:healed', function() 22 | debugPrint('Received heal event, healing to full') 23 | local ped = PlayerPedId() 24 | local pos = GetEntityCoords(ped) 25 | local heading = GetEntityHeading(ped) 26 | if IsEntityDead(ped) then 27 | NetworkResurrectLocalPlayer(pos[1], pos[2], pos[3], heading, false, false) 28 | end 29 | SetEntityHealth(ped, GetEntityMaxHealth(ped)) 30 | end) 31 | 32 | -- Tell the user he is an admin and that /tx is available 33 | AddEventHandler('playerSpawned', function() 34 | Wait(15000) 35 | if menuIsAccessible then 36 | sendMenuMessage('showMenuHelpInfo', {}) 37 | end 38 | end) 39 | -------------------------------------------------------------------------------- /monitor/resource/menu/client/cl_freeze.lua: -------------------------------------------------------------------------------- 1 | -- ============================================= 2 | -- This file contains all player freeze logic 3 | -- ============================================= 4 | if (GetConvar('txAdmin-menuEnabled', 'false') ~= 'true') then 5 | return 6 | end 7 | 8 | local function sendFreezeAlert(isFrozen) 9 | if isFrozen then 10 | sendPersistentAlert('freeze-status', 'warning', 'nui_menu.frozen.was_frozen', true) 11 | else 12 | clearPersistentAlert('freeze-status') 13 | end 14 | end 15 | 16 | RegisterNUICallback('togglePlayerFreeze', function(data, cb) 17 | local targetPlayerId = tonumber(data.id) 18 | if targetPlayerId == GetPlayerServerId(PlayerId()) then 19 | return sendSnackbarMessage('error', 'nui_menu.player_modal.actions.interaction.notifications.freeze_yourself', true) 20 | end 21 | 22 | TriggerServerEvent('txAdmin:menu:freezePlayer', targetPlayerId) 23 | cb({}) 24 | end) 25 | 26 | RegisterNetEvent('txAdmin:menu:freezeResp', function(isFrozen) 27 | local localeKey = isFrozen and 'nui_menu.frozen.froze_player' or 'nui_menu.frozen.unfroze_player' 28 | sendSnackbarMessage('info', localeKey, true) 29 | end) 30 | 31 | RegisterNetEvent('txAdmin:menu:freezePlayer', function(isFrozen) 32 | debugPrint('Frozen: ' .. tostring(isFrozen)) 33 | local playerPed = PlayerPedId() 34 | if IsPedInAnyVehicle(playerPed) then 35 | TaskLeaveAnyVehicle(playerPed, false, 16) 36 | end 37 | FreezeEntityPosition(playerPed, isFrozen) 38 | sendFreezeAlert(isFrozen) 39 | end) 40 | -------------------------------------------------------------------------------- /monitor/docs/permissions.md: -------------------------------------------------------------------------------- 1 | ## Permission System 2 | The permission system allows you to control which admins can perform which actions. 3 | For instance you can allow one admin to only view the console and kick players, but not restart the server and execute arbitrary commands. 4 | The permissions are saved in the `txData/admins.json` file and can be edited through the *Admin Manager* page by the Master admin, or users with `all_permissions` or `manage.admins` permissions. 5 | 6 | ### Available Permissions 7 | - `all_permissions`: Root permission that allows the user to perform any action. When set, this will remove all other permissions; 8 | - `manage.admins`: Permission to create, edit and remove other admin accounts; 9 | - `settings.view`: Settings: View (no tokens); 10 | - `settings.write`: Settings: Change; 11 | - `console.view`: Console: View; 12 | - `console.write`: Console: Write; 13 | - `control.server`: Start/Stop/Restart Server; 14 | - `commands.resources`: Start/Stop Resources; 15 | - `server.cfg.editor`: Read/Write server.cfg; 16 | - `txadmin.log.view`: View txAdmin Log; 17 | - `menu.vehicle`: Spawn / Fix Vehicles; 18 | - `players.message`: Announcement / DM; 19 | - `players.whitelist`: Whitelist player; 20 | - `players.warn`: Warn player; 21 | - `players.kick`: Kick player; 22 | - `players.ban`: Ban player; 23 | - `players.heal`: Heal self or everyone; 24 | - `players.playermode`: NoClip / God Mode; 25 | - `players.spectate`: Spectate player; 26 | - `players.teleport`: Teleport self or player; 27 | - `players.trollmenu`: Troll Menu (*not yet available*); 28 | - `players.freeze`: Freeze a players ped; -------------------------------------------------------------------------------- /monitor/docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | First and foremost check if you are using the most recent version of txAdmin and that you followed the installation instructions. 4 | 5 | ## Problems running txAdmin 6 | When executing txAdmin, it might show you some errors. Example of an [error](https://i.imgur.com/2huiyBf.png), example of a [successful startup](https://i.imgur.com/QLCBZBm.png). 7 | 8 | ### [txAdmin:AdminVault] Unable to load admins. 9 | If you get `cannot read file`, it means the admin file `txData/admins.json` doesn't exist or txAdmin doesn't have permission to read it. 10 | Any other error message means you somehow broke the admins file, delete it and restart txAdmin to generate a new one. 11 | 12 | ### [txAdmin:ConfigVault] Error: Unable to load configuration file `txData//config.json` 13 | The selected profile (or `default`) cannot be loaded due to permission issues (eg file owned by the root account), or due to broken JSON. 14 | 15 | ## Problems starting the server 16 | When you start txAdmin, your server will **not** start automatically (by default). Open the web panel and start txAdmin (actions > START Server). You can change this by enabling autostart in the settings page. 17 | If you are getting `FXServer is not responding!` it means the `txAdmin:Monitor` could not connect to the FXServer, causes: 18 | - FXServer is not responding to the TCP endpoint, this usually means some resource got stuck; 19 | - FXServer and txAdmin are on the same TCP port. In this case your server will not be able to listen to the configured port and the healthcheck will return a 404 error; 20 | - FXServer is not listening to the local interface. 21 | 22 |
23 | 24 | If this guide didn't help you, join our awesome [Discord server](https://discord.gg/AFAAXzq). 25 | -------------------------------------------------------------------------------- /monitor/web/main/txAdminLog.ejs: -------------------------------------------------------------------------------- 1 | <%- await include('parts/header.ejs', locals) %> 2 | 3 | 12 | 13 |
14 | 20 |
21 |
22 |
23 | 31 |
32 |
33 | 34 | 35 | <%- await include('parts/footer.ejs', locals) %> 36 | 37 | 38 | 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | ## Instructions 6 | 7 | - Just Drag And Drop Just Go To Your The Location That Your FXServer Is And Replace The Monitor With Our Monitor 8 | 9 | ```citizen\system_resources\monitor``` 10 | 11 | 12 |
13 | About It 14 |

Status

15 |

Tested

16 |

Not Updated

17 | Description: A QBCore Based Theme For TxAdmin Panel
18 | Credits: Marshy And IDKFORCE
19 | Download: Click Me!

20 | 21 | 22 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /monitor/web/public/js/codeEditor/mode/fivem-cfg.js: -------------------------------------------------------------------------------- 1 | // CodeMirror fivem-cfg syntax highlight 2 | // Written by Tabarra for https://github.com/tabarra/txAdmin 3 | 4 | CodeMirror.defineSimpleMode('fivem-cfg', { 5 | // The start state contains the rules that are intially used 6 | start: [ 7 | // The regex matches the token, the token property contains the type 8 | {regex: /(["'])(?:[^\\]|\\.)*?(?:\1|$)/, token: 'string'}, 9 | 10 | // Rules are matched in the order in which they appear, so there is 11 | // no ambiguity between this one and the one above 12 | {regex: /(?:start|stop|ensure|restart|refresh|exec|quit|set|seta|setr|sets)\b/i, token: 'def'}, 13 | {regex: /(?:endpoint_add_tcp|endpoint_add_udp|load_server_icon|sv_authMaxVariance|sv_authMinTrust|sv_endpointPrivacy|sv_hostname|sv_licenseKey|sv_master1|sv_maxClients|rcon_password|sv_scriptHookAllowed|gamename|onesync|sv_enforceGameBuild)\b/i, token: 'keyword'}, 14 | {regex: /(?:add_ace|add_principal|remove_ace|remove_principal|test_ace)\b/i, token: 'variable-2'}, 15 | {regex: /banner_connecting|banner_detail|locale|steam_webApiKey|tags|mysql_connection_string|sv_projectName|sv_projectDesc/i, token: 'atom'}, 16 | {regex: /0x[a-f\d]+|[-+]?(?:\.\d+|\d+\.?\d*)(?:e[-+]?\d+)?/i, token: 'number'}, 17 | {regex: /\/\/.*/, token: 'comment'}, 18 | {regex: /#.*/, token: 'comment'}, 19 | {regex: /\/(?:[^\\]|\\.)*?\//, token: 'variable-3'}, 20 | 21 | // A next property will cause the mode to move to a different state 22 | {regex: /\/\*/, token: 'comment', next: 'comment'}, 23 | 24 | {regex: /[a-z$][\w$]*/, token: 'variable'}, 25 | ], 26 | 27 | // The multi-line comment state. 28 | comment: [ 29 | {regex: /.*?\*\//, token: 'comment', next: 'start'}, 30 | {regex: /.*/, token: 'comment'}, 31 | ], 32 | }); 33 | -------------------------------------------------------------------------------- /monitor/docs/feature_graveyard.md: -------------------------------------------------------------------------------- 1 | # Feature Graveyard 2 | In txAdmin codebase, we try to keep things lean, this is one of the few reasons after one year of project, our code base is not *that* bad. 3 | And as part of the process, we "retired" many features and parts of our code base, here is a relation of the majority of them and the reason why: 4 | 5 | - **Setup script:** Now everything is automatic when you start with a profile set in the convars; 6 | - **Admin add script:** Now its done via the master account creation UI flow, and the Admin Manager page; 7 | - **Config tester:** With the gained knowledge of the edge cases, it became way easier to implement better checks and actionable error messages on the settings page; 8 | - **Resources injector:** With the integration with FiveM, our plans for it changed drastically. It may or may not come back, meanwhile it was removed to prevent issues; 9 | - **Automatic cache cleaner:** This feature were created due to the vast number of requests, but in the end this "common knowledge" was based on misinformation, therefore it was removed since we don't actually need it; 10 | - **SSL support:** With the rework of the entire web layer of txAdmin in preparation with the FiveM integration, we ended up removing this (tricky to implement) feature. But don't worry, one of the benefits from the integration is that now we have the FiveM cfx.re reverse proxy, which by default supports HTTPS; 11 | - **Experiments:** Well... not much to experience with right now; 12 | - **Discord static commands:** I don't think anyone ever used it since they can do it with basically any other bot; 13 | - **Set process priority:** Although it was quite requested in the beginning, people just don't seem to use it; 14 | - **Menu Weed troll effect:** It was just too similar to the drunk effect one, not worth keeping. 15 | 16 | Don't cry because they are gone. 17 | Smile because they existed :) 18 | -------------------------------------------------------------------------------- /monitor/web/public/css/codemirror_lucario.css: -------------------------------------------------------------------------------- 1 | .cm-s-lucario.CodeMirror, .cm-s-lucario .CodeMirror-gutters { 2 | background-color: #141414 !important; 3 | color: #f8f8f2 !important; 4 | border: none; 5 | } 6 | .cm-s-lucario .CodeMirror-gutters { color: #141414; } 7 | .cm-s-lucario .CodeMirror-cursor { border-left: solid thin #0db61b; } 8 | .cm-s-lucario .CodeMirror-linenumber { color: #ffffff; } 9 | .cm-s-lucario .CodeMirror-selected { background: #46494d; } 10 | .cm-s-lucario .CodeMirror-line::selection, .cm-s-lucario .CodeMirror-line > span::selection, .cm-s-lucario .CodeMirror-line > span > span::selection { background: #243443; } 11 | .cm-s-lucario .CodeMirror-line::-moz-selection, .cm-s-lucario .CodeMirror-line > span::-moz-selection, .cm-s-lucario .CodeMirror-line > span > span::-moz-selection { background: #243443; } 12 | .cm-s-lucario span.cm-comment { color: #e46c84; } 13 | .cm-s-lucario span.cm-string, .cm-s-lucario span.cm-string-2 { color: #ffffff; } 14 | .cm-s-lucario span.cm-number { color: #008d00; } 15 | .cm-s-lucario span.cm-variable { color: #f8f8f2; } 16 | .cm-s-lucario span.cm-variable-2 { color: #fdfdc6; } 17 | .cm-s-lucario span.cm-def { color: #dc143c; } 18 | .cm-s-lucario span.cm-operator { color: #66D9EF; } 19 | .cm-s-lucario span.cm-keyword { color: #a52a43; } 20 | .cm-s-lucario span.cm-atom { color: #e2506d; } 21 | .cm-s-lucario span.cm-meta { color: #f8f8f2; } 22 | .cm-s-lucario span.cm-tag { color: #f392e6; } 23 | .cm-s-lucario span.cm-attribute { color: #66D9EF; } 24 | .cm-s-lucario span.cm-qualifier { color: #72C05D; } 25 | .cm-s-lucario span.cm-property { color: #f8f8f2; } 26 | .cm-s-lucario span.cm-builtin { color: #72C05D; } 27 | .cm-s-lucario span.cm-variable-3, .cm-s-lucario span.cm-type { color: #ffb86c; } 28 | 29 | .cm-s-lucario .CodeMirror-activeline-background { background: #243443; } 30 | .cm-s-lucario .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; } 31 | -------------------------------------------------------------------------------- /monitor/resource/sv_resources.lua: -------------------------------------------------------------------------------- 1 | -- ============================================= 2 | -- Report all resource events to txAdmin 3 | -- ============================================= 4 | --Check Environment 5 | if GetConvar('txAdminServerMode', 'false') ~= 'true' then 6 | return 7 | end 8 | 9 | 10 | local function reportResourceEvent(event, resource) 11 | -- print(string.format("\27[107m\27[30m %s: %s \27[0m", event, resource)) 12 | PrintStructuredTrace(json.encode({ 13 | type = 'txAdminResourceStatus', 14 | event = event, 15 | resource = resource 16 | })) 17 | end 18 | 19 | 20 | -- An event that is triggered when a resource is trying to start. 21 | -- This can be canceled to prevent the resource from starting. 22 | AddEventHandler('onResourceStarting', function(resource) 23 | reportResourceEvent('onResourceStarting', resource) 24 | end) 25 | 26 | -- An event that is triggered immediately when a resource has started. 27 | AddEventHandler('onResourceStart', function(resource) 28 | reportResourceEvent('onResourceStart', resource) 29 | end) 30 | 31 | -- An event that is queued after a resource has started. 32 | AddEventHandler('onServerResourceStart', function(resource) 33 | reportResourceEvent('onServerResourceStart', resource) 34 | end) 35 | 36 | -- A server-side event triggered when the refresh command completes. 37 | AddEventHandler('onResourceListRefresh', function(resource) 38 | reportResourceEvent('onResourceListRefresh', resource) 39 | end) 40 | 41 | -- An event that is triggered immediately when a resource is stopping. 42 | AddEventHandler('onResourceStop', function(resource) 43 | reportResourceEvent('onResourceStop', resource) 44 | end) 45 | 46 | -- An event that is triggered after a resource has stopped. 47 | AddEventHandler('onServerResourceStop', function(resource) 48 | reportResourceEvent('onServerResourceStop', resource) 49 | end) 50 | 51 | -- TODO: As soon as the server start, send full list of resources to txAdmin 52 | -- CreateThread(function() 53 | -- blabla 54 | -- end) 55 | -------------------------------------------------------------------------------- /monitor/docs/translation.md: -------------------------------------------------------------------------------- 1 | # Translation Support 2 | txAdmin supports translation in over 25 languages for the in-game interface (menu/warn) and chat messages, as well as discord warnings. 3 | 4 | 5 | ## Custom locales: 6 | If your language is not available, or you want to customize the messages, create a `locale.json` file in inside the `txData` folder based on any language file found on [our repository](https://github.com/tabarra/txAdmin/tree/master/locale). Then go to the settings and select the "Custom" language option. 7 | 8 | The `$meta.humanizer_language` key must be compatible with the library [humanize-duration](https://www.npmjs.com/package/humanize-duration), check their page for a list of compatible languages. 9 | 10 | 11 | ## Contributing: 12 | We need the community help to translate, and keep the translations updated and high-quality. 13 | For that you will need to: 14 | - Make a custom locale file with the instructions above; 15 | - Name the file using the language code in [this page](https://www.science.co.il/language/Locale-codes.php); 16 | - The `$meta.label` must be the language name in English (eg `Spanish` instead of `Español`); 17 | - Do a [Pull Request](https://github.com/tabarra/txAdmin/pulls) posting a few screenshots of evidence that you tested what you changed in-game. 18 | 19 | > **Pro Tip:** To quickly test your changes, you can edit the `locale.json` file and then in the settings page click "Save Global Settings" again to see the changes in the game menu without needing to restart txAdmin or the server. 20 | 21 | > **Pro Tip2:** To make sure you didn't miss anything in the locale file, you can download the txAdmin source code, execute `npm i`, move the `locale.json` to inside the `txAdmin/locale` folder and run `npm run locale:diff`. This will tell you about missing or extra keys. 22 | 23 | > **Note:** The performance of custom locale for big servers may not be ideal due to the way we need to sync dynamic content to clients. So it is strongly encouraged that you contribute with translations in our GitHub so it gets packed with the rest of txAdmin. 24 | 25 | -------------------------------------------------------------------------------- /monitor/resource/menu/server/sv_player_modal.lua: -------------------------------------------------------------------------------- 1 | --Check Environment 2 | if GetConvar('txAdminServerMode', 'false') ~= 'true' then 3 | return 4 | end 5 | 6 | -- ============================================= 7 | -- This file is for general server side handlers 8 | -- related to actions defined within Menu's 9 | -- "Player Modal" 10 | -- ============================================= 11 | 12 | RegisterNetEvent('txAdmin:menu:tpToPlayer', function(tgtId) 13 | local src = source 14 | 15 | if type(tgtId) ~= 'number' then 16 | return 17 | end 18 | 19 | local allow = PlayerHasTxPermission(src, 'players.teleport') 20 | local data = { x = nil, y = nil, z = nil, target = id } 21 | 22 | data.playerName = "unknown" 23 | -- More OneSync dependent code 24 | if allow then 25 | -- Check for routing bucket diff 26 | 27 | local tgtBucket = GetPlayerRoutingBucket(tgtId) 28 | local srcBucket = GetPlayerRoutingBucket(src) 29 | 30 | -- This isn't stored anywhere for reversion, 31 | -- as TP to player is typically a one sided operation 32 | if tgtBucket ~= srcBucket then 33 | SetPlayerRoutingBucket(src, tgtBucket) 34 | end 35 | 36 | -- ensure the player ped exists 37 | local ped = GetPlayerPed(tgtId) 38 | if ped then 39 | data.playerName = GetPlayerName(tgtId) 40 | local coords = GetEntityCoords(ped) 41 | data.x = coords[1] 42 | data.y = coords[2] 43 | data.z = coords[3] 44 | TriggerClientEvent('txAdmin:menu:tpToCoords', src, data.x, data.y, data.z) 45 | end 46 | end 47 | 48 | TriggerEvent('txaLogger:menuEvent', src, 'teleportPlayer', allow, data) 49 | end) 50 | 51 | RegisterNetEvent('txAdmin:menu:summonPlayer', function(id) 52 | local src = source 53 | if type(id) ~= 'number' then 54 | return 55 | end 56 | local allow = PlayerHasTxPermission(src, 'players.teleport') 57 | if allow then 58 | -- ensure the target player ped exists 59 | local ped = GetPlayerPed(id) 60 | if ped then 61 | local coords = GetEntityCoords(GetPlayerPed(src)) 62 | TriggerClientEvent('txAdmin:menu:tpToCoords', id, coords[1], coords[2], coords[3]) 63 | end 64 | end 65 | TriggerEvent('txaLogger:menuEvent', src, 'summonPlayer', allow, id) 66 | end) 67 | -------------------------------------------------------------------------------- /monitor/web/standalone/404.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 404 - txAdmin 12 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 |
27 |
28 |

404

29 |

Oops! You're lost.

30 |

The page you are looking for was not found.

31 | Go Back 32 |
33 |
34 |
35 |
36 | 37 | 38 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /monitor/docs/logs.md: -------------------------------------------------------------------------------- 1 | # Logging 2 | In version v4.6.0, **txAdmin** added support for persistent logging with file rotate, meaning you will have an organized folder (`txData//logs/`) containing your log files up to a maximum size and number of days. 3 | 4 | > Note: player warn/ban/whitelist actions are not just stored in the Admin Logs, but also on the players database. 5 | 6 | ## Admin Logs: 7 | Contains log of administrative actions as well as some automated ones like server restarts, bans, warns, settings change, live console input and so on. It does not log the user IP unless if from an authentication endpoint. 8 | - Recent Buffer: None. Methods will read the entire file. 9 | - Interval: 7d 10 | - maxFiles: false 11 | - maxSize: false 12 | 13 | ## FXServer: 14 | Contains the log of everything that happens in the fxserver console (`stdin`, `stdout`, `stderr`). Any live console input is prefixed with `> `. 15 | - Recent Buffer: 64~128kb 16 | - Interval: 1d 17 | - maxFiles: 7 18 | - maxSize: 5G 19 | 20 | ## Server Logs: 21 | Contains all actions that happen inside the server, for example player join/leave/die, chat messages, explosions, menu events, commands. Player sources are kept in the format `[mutex#id] name` where the mutex is an identifier of that server execution. If you search the file for a `[mutex#id]`, the first result will be the player join with all his identifiers available. 22 | - Recent Buffer: 32k events 23 | - Interval: 1d 24 | - maxFiles: 7 25 | - maxSize: 10G 26 | 27 | ## Console log (not released): 28 | Contains everything that txAdmin prints on the console. 29 | - Recent Buffer: last 500 lines 30 | - Interval: 1d 31 | - maxFiles: 7 32 | - maxSize: 5G 33 | 34 | ## Configuring Log Rotate 35 | The log rotation can be configured, so you can choose to store more or less logs according to your needs. 36 | To configure it, edit your `txData//config.json` and add an object inside `logger` with the key being one of `[admin, fxserver, server, console]`. Then add option keys according with the library reference: https://github.com/iccicci/rotating-file-stream#options 37 | 38 | Example: 39 | ```jsonc 40 | { 41 | //... 42 | "logger": { 43 | "fxserver": { 44 | "interval": "1d", 45 | "maxSize": "2G", //max size of rotated files to keep 46 | "maxFiles": 14 //max number of rotated files to keep 47 | } 48 | } 49 | //... 50 | } 51 | ``` 52 | -------------------------------------------------------------------------------- /monitor/resource/menu/vendor/freecam/utils.lua: -------------------------------------------------------------------------------- 1 | local rad = math.rad 2 | local sin = math.sin 3 | local cos = math.cos 4 | local min = math.min 5 | local max = math.max 6 | local type = type 7 | 8 | function table.copy(x) 9 | local copy = {} 10 | for k, v in pairs(x) do 11 | if type(v) == 'table' then 12 | copy[k] = table.copy(v) 13 | else 14 | copy[k] = v 15 | end 16 | end 17 | return copy 18 | end 19 | 20 | function protect(t) 21 | local fn = function (_, k) 22 | error('Key `' .. tostring(k) .. '` is not supported.') 23 | end 24 | 25 | return setmetatable(t, { 26 | __index = fn, 27 | __newindex = fn 28 | }) 29 | end 30 | 31 | function CreateGamepadMetatable(keyboard, gamepad) 32 | return setmetatable({}, { 33 | __index = function (t, k) 34 | local src = IsGamepadControl() and gamepad or keyboard 35 | return src[k] 36 | end 37 | }) 38 | end 39 | 40 | function Clamp(x, _min, _max) 41 | return min(max(x, _min), _max) 42 | end 43 | 44 | function ClampCameraRotation(rotX, rotY, rotZ) 45 | local x = Clamp(rotX, -90.0, 90.0) 46 | local y = rotY % 360 47 | local z = rotZ % 360 48 | return x, y, z 49 | end 50 | 51 | function IsGamepadControl() 52 | return not IsInputDisabled(2) 53 | end 54 | 55 | function GetSmartControlNormal(control) 56 | if type(control) == 'table' then 57 | local normal1 = GetDisabledControlNormal(0, control[1]) 58 | local normal2 = GetDisabledControlNormal(0, control[2]) 59 | return normal1 - normal2 60 | end 61 | 62 | return GetDisabledControlNormal(0, control) 63 | end 64 | 65 | function EulerToMatrix(rotX, rotY, rotZ) 66 | local radX = rad(rotX) 67 | local radY = rad(rotY) 68 | local radZ = rad(rotZ) 69 | 70 | local sinX = sin(radX) 71 | local sinY = sin(radY) 72 | local sinZ = sin(radZ) 73 | local cosX = cos(radX) 74 | local cosY = cos(radY) 75 | local cosZ = cos(radZ) 76 | 77 | local vecX = {} 78 | local vecY = {} 79 | local vecZ = {} 80 | 81 | vecX.x = cosY * cosZ 82 | vecX.y = cosY * sinZ 83 | vecX.z = -sinY 84 | 85 | vecY.x = cosZ * sinX * sinY - cosX * sinZ 86 | vecY.y = cosX * cosZ - sinX * sinY * sinZ 87 | vecY.z = cosY * sinX 88 | 89 | vecZ.x = -cosX * cosZ * sinY + sinX * sinZ 90 | vecZ.y = -cosZ * sinX + cosX * sinY * sinZ 91 | vecZ.z = cosX * cosY 92 | 93 | vecX = vector3(vecX.x, vecX.y, vecX.z) 94 | vecY = vector3(vecY.x, vecY.y, vecY.z) 95 | vecZ = vector3(vecZ.x, vecZ.y, vecZ.z) 96 | 97 | return vecX, vecY, vecZ 98 | end 99 | -------------------------------------------------------------------------------- /monitor/web/parts/footer.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <% if (isWebInterface) { %> 8 |
9 |
v<%= txAdminVersion %> atop FXServer <%= fxServerVersion %>
10 |
11 | © 2019-<%= (new Date).getUTCFullYear() %> 12 | Credits To Tabarra & 13 | Marshy & 14 | IDKFORCE 15 |
16 |
17 | <% if (txaOutdated || fxsOutdated) { %> 18 | (outdated) 19 | <% } %> 20 | v<%= txAdminVersion %> Latest FXServer <%= fxServerVersion %> 21 |
22 |
23 | <% } %> 24 | 25 | 26 | 27 | 28 | <%- await include('parts/playerInfoModal.ejs', locals) %> 29 | <%- await include('parts/changePasswordModal.ejs', locals) %> 30 | 31 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /monitor/resource/menu/server/sv_spectate.lua: -------------------------------------------------------------------------------- 1 | --Check Environment 2 | if GetConvar('txAdminServerMode', 'false') ~= 'true' then 3 | return 4 | end 5 | 6 | -- Holds map containing source players original routing 7 | -- bucket so we can use it on end spectate. 8 | local ORIGINAL_SPEC_BUCKET = {} 9 | 10 | RegisterNetEvent('txAdmin:menu:spectatePlayer', function(id) 11 | local src = source 12 | -- Sanity as this is still converted tonumber on client side 13 | if type(id) ~= 'string' and type(id) ~= 'number' then 14 | return 15 | end 16 | 17 | id = tonumber(id) 18 | 19 | local allow = PlayerHasTxPermission(src, 'players.spectate') 20 | 21 | if allow then 22 | local target = GetPlayerPed(id) 23 | -- Lets exit if the target doesn't exist 24 | if not target then 25 | return 26 | end 27 | local tgtBucket = GetPlayerRoutingBucket(id) 28 | local srcBucket = GetPlayerRoutingBucket(src) 29 | -- If our source and target are not in the same routing bucket 30 | -- lets store it in our map 31 | 32 | -- If a player isn't stored within the map upon the spectateExit call, 33 | -- it can be assumed that the player had the same routing bucket as its target, 34 | -- and we don't need to store data in the map 35 | if tgtBucket ~= srcBucket then 36 | debugPrint(('Target and source buckets differ | src: %s, bkt: %i | tgt: %s, bkt: %i'):format(src, srcBucket, target, tgtBucket)) 37 | ORIGINAL_SPEC_BUCKET[src] = srcBucket 38 | SetPlayerRoutingBucket(src, tgtBucket) 39 | end 40 | 41 | local tgtCoords = GetEntityCoords(target) 42 | TriggerClientEvent('txAdmin:menu:specPlayerResp', src, id, tgtCoords) 43 | end 44 | TriggerEvent('txaLogger:menuEvent', src, 'spectatePlayer', allow, id) 45 | end) 46 | 47 | RegisterNetEvent('txAdmin:menu:endSpectate', function() 48 | local src = source 49 | local allow = PlayerHasTxPermission(src, 'players.spectate') 50 | if allow then 51 | -- If this is nil, assume that no routing bucket change is needed, 52 | -- as it wasn't stored 53 | local prevRoutBucket = ORIGINAL_SPEC_BUCKET[src] 54 | -- Since lua treats 0 as truthy, actually don't need to handle 55 | -- explicit nil check for int 0 56 | if prevRoutBucket then 57 | SetPlayerRoutingBucket(src, prevRoutBucket) 58 | -- Clean up our prev bucket map 59 | ORIGINAL_SPEC_BUCKET[src] = nil 60 | end 61 | end 62 | end) 63 | 64 | AddEventHandler('playerDropped', function() 65 | local src = source 66 | if ORIGINAL_SPEC_BUCKET[src] then 67 | ORIGINAL_SPEC_BUCKET[src] = nil 68 | end 69 | end) 70 | -------------------------------------------------------------------------------- /monitor/fxmanifest.lua: -------------------------------------------------------------------------------- 1 | -- Modifying or rewriting this resource for local use only is strongly discouraged. 2 | -- Feel free to open an issue or pull request in our GitHub. 3 | -- Official discord server: https://discord.gg/AFAAXzq 4 | 5 | author 'Tabarra' 6 | description 'Remotely Manage & Monitor your GTA5 FiveM Server' 7 | repository 'https://github.com/tabarra/txAdmin' 8 | version '4.18.1' 9 | ui_label 'txAdmin' 10 | 11 | rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.' 12 | fx_version 'cerulean' 13 | games { 'gta5', 'rdr3' } 14 | 15 | -- NOTE: All server_scripts will be executed both on monitor and server mode 16 | -- NOTE: Due to global package constraints, js scripts will be loaded from main.js 17 | -- NOTE: Due to people drag-n-dropping their artifacts, we can't do globbing 18 | shared_scripts { 19 | 'resource/menu/shared.lua' 20 | } 21 | 22 | server_scripts { 23 | 'entrypoint.js', 24 | 'resource/sv_main.lua', --must run first 25 | 'resource/sv_admins.lua', 26 | 'resource/sv_logger.lua', 27 | 'resource/sv_resources.lua', 28 | 'resource/sv_playerlist.lua', 29 | 'resource/menu/server/sv_webpipe.lua', 30 | 'resource/menu/server/sv_base.lua', 31 | 'resource/menu/server/sv_functions.lua', 32 | 'resource/menu/server/sv_main_page.lua', 33 | 'resource/menu/server/sv_freeze_player.lua', 34 | 'resource/menu/server/sv_trollactions.lua', 35 | 'resource/menu/server/sv_player_modal.lua', 36 | 'resource/menu/server/sv_spectate.lua', 37 | 'resource/menu/server/sv_player_mode.lua' 38 | } 39 | 40 | client_scripts { 41 | 'resource/cl_main.lua', 42 | 'resource/cl_logger.lua', 43 | 'resource/cl_playerlist.lua', 44 | 'resource/menu/client/cl_webpipe.lua', 45 | 'resource/menu/client/cl_base.lua', 46 | 'resource/menu/client/cl_functions.lua', 47 | 'resource/menu/client/cl_main_page.lua', 48 | 'resource/menu/client/cl_misc.lua', 49 | 'resource/menu/client/cl_player_ids.lua', 50 | 'resource/menu/client/cl_player_mode.lua', 51 | 'resource/menu/client/cl_spectate.lua', 52 | 'resource/menu/client/cl_trollactions.lua', 53 | 'resource/menu/client/cl_freeze.lua', 54 | 'resource/menu/vendor/freecam/utils.lua', 55 | 'resource/menu/vendor/freecam/config.lua', 56 | 'resource/menu/vendor/freecam/main.lua', 57 | 'resource/menu/vendor/freecam/camera.lua', 58 | } 59 | 60 | ui_page 'nui/index.html' 61 | 62 | files { 63 | 'nui/**/*', 64 | 65 | -- WebPipe optimization: 66 | 'web/public/css/coreui.min.css', 67 | 'web/public/css/jquery-confirm.min.css', 68 | 'web/public/css/txAdmin.css', 69 | 'web/public/css/dark.css', 70 | 'web/public/js/coreui.bundle.min.js', 71 | 'web/public/js/bootstrap-notify.min.js', 72 | 'web/public/js/jquery-confirm.min.js', 73 | 'web/public/js/txadmin/base.js', 74 | 'web/public/js/txadmin/main.js', 75 | 'web/public/js/txadmin/players.js', 76 | } 77 | -------------------------------------------------------------------------------- /monitor/resource/menu/vendor/freecam/config.lua: -------------------------------------------------------------------------------- 1 | local INPUT_LOOK_LR = 1 2 | local INPUT_LOOK_UD = 2 3 | local INPUT_CHARACTER_WHEEL = 19 4 | local INPUT_SPRINT = 21 5 | local INPUT_MOVE_UD = 31 6 | local INPUT_MOVE_LR = 30 7 | local INPUT_VEH_ACCELERATE = 71 8 | local INPUT_VEH_BRAKE = 72 9 | local INPUT_PARACHUTE_BRAKE_LEFT = 152 10 | local INPUT_PARACHUTE_BRAKE_RIGHT = 153 11 | 12 | -------------------------------------------------------------------------------- 13 | 14 | local BASE_CONTROL_MAPPING = protect({ 15 | -- Rotation 16 | LOOK_X = INPUT_LOOK_LR, 17 | LOOK_Y = INPUT_LOOK_UD, 18 | 19 | -- Position 20 | MOVE_X = INPUT_MOVE_LR, 21 | MOVE_Y = INPUT_MOVE_UD, 22 | MOVE_Z = { INPUT_PARACHUTE_BRAKE_LEFT, INPUT_PARACHUTE_BRAKE_RIGHT }, 23 | 24 | -- Multiplier 25 | MOVE_FAST = INPUT_SPRINT, 26 | MOVE_SLOW = INPUT_CHARACTER_WHEEL 27 | }) 28 | 29 | -------------------------------------------------------------------------------- 30 | 31 | local BASE_CONTROL_SETTINGS = protect({ 32 | -- Rotation 33 | LOOK_SENSITIVITY_X = 5, 34 | LOOK_SENSITIVITY_Y = 5, 35 | 36 | -- Position 37 | BASE_MOVE_MULTIPLIER = 0.85, 38 | FAST_MOVE_MULTIPLIER = 6, 39 | SLOW_MOVE_MULTIPLIER = 6, 40 | }) 41 | 42 | -------------------------------------------------------------------------------- 43 | 44 | local BASE_CAMERA_SETTINGS = protect({ 45 | --Camera 46 | FOV = 50.0, 47 | 48 | -- On enable/disable 49 | ENABLE_EASING = true, 50 | EASING_DURATION = 250, 51 | 52 | -- Keep position/rotation 53 | KEEP_POSITION = false, 54 | KEEP_ROTATION = false 55 | }) 56 | 57 | -------------------------------------------------------------------------------- 58 | 59 | _G.KEYBOARD_CONTROL_MAPPING = table.copy(BASE_CONTROL_MAPPING) 60 | _G.GAMEPAD_CONTROL_MAPPING = table.copy(BASE_CONTROL_MAPPING) 61 | 62 | -- Swap up/down movement (LB for down, RB for up) 63 | _G.GAMEPAD_CONTROL_MAPPING.MOVE_Z[1] = INPUT_PARACHUTE_BRAKE_LEFT 64 | _G.GAMEPAD_CONTROL_MAPPING.MOVE_Z[2] = INPUT_PARACHUTE_BRAKE_RIGHT 65 | 66 | -- Use LT and RT for speed 67 | _G.GAMEPAD_CONTROL_MAPPING.MOVE_FAST = INPUT_VEH_ACCELERATE 68 | _G.GAMEPAD_CONTROL_MAPPING.MOVE_SLOW = INPUT_VEH_BRAKE 69 | 70 | protect(_G.KEYBOARD_CONTROL_MAPPING) 71 | protect(_G.GAMEPAD_CONTROL_MAPPING) 72 | 73 | -------------------------------------------------------------------------------- 74 | 75 | _G.KEYBOARD_CONTROL_SETTINGS = table.copy(BASE_CONTROL_SETTINGS) 76 | _G.GAMEPAD_CONTROL_SETTINGS = table.copy(BASE_CONTROL_SETTINGS) 77 | 78 | -- Gamepad sensitivity can be reduced by BASE. 79 | _G.GAMEPAD_CONTROL_SETTINGS.LOOK_SENSITIVITY_X = 2 80 | _G.GAMEPAD_CONTROL_SETTINGS.LOOK_SENSITIVITY_Y = 2 81 | 82 | protect(_G.KEYBOARD_CONTROL_SETTINGS) 83 | protect(_G.GAMEPAD_CONTROL_SETTINGS) 84 | 85 | -------------------------------------------------------------------------------- 86 | 87 | _G.CAMERA_SETTINGS = table.copy(BASE_CAMERA_SETTINGS) 88 | protect(_G.CAMERA_SETTINGS) 89 | 90 | -------------------------------------------------------------------------------- 91 | 92 | -- Create some convenient variables. 93 | -- Allows us to access controls and config without a gamepad switch. 94 | _G.CONTROL_MAPPING = CreateGamepadMetatable(_G.KEYBOARD_CONTROL_MAPPING, _G.GAMEPAD_CONTROL_MAPPING) 95 | _G.CONTROL_SETTINGS = CreateGamepadMetatable(_G.KEYBOARD_CONTROL_SETTINGS, _G.GAMEPAD_CONTROL_SETTINGS) 96 | -------------------------------------------------------------------------------- /monitor/docs/newPlayerlist.md: -------------------------------------------------------------------------------- 1 | 2 | ### Menu playerlist fix 3 | ## TL;DR 4 | Server: 5 | - Will have it's own playerlist with {id = {name, health, vType}, ...} 6 | - Regularly run through the playerlist updating {health, vType} (yielding every 50 players) 7 | - On player join: 8 | - Send event "updatePlayer" with {id, name} to all admins 9 | - On player leave: 10 | - Send event "updatePlayer" with {id, false} to all admins 11 | - On admin join (auth): 12 | - Send event "setInitialPlayerlist" with {{id, name}, ...} 13 | - On getDetailedPlayerlist event: 14 | - check if admin 15 | - reply with event setDetailedPlayerlist and payload [ [id, health, vType] ] 16 | Client: 17 | - On setInitialPlayerlist: replace existing playerlist with the inbound one 18 | - On updatePlayer: add/remove specific id to playerlist 19 | - On setDetailedPlayerlist: 20 | - run through inbound playerlist updating existing data 21 | - try to get the dist from all players (susceptible to area culling, but that's fine) 22 | - TODO: decide what to do in case of missing or extra ids (missed updatePlayer?) 23 | - On player tab open: getDetailedPlayerlist() 24 | - Every 5 seconds while player tab is opened: getDetailedPlayerlist() 25 | 26 | - Everything is sent as array, no need to waste bytes on keys; 27 | - A list with id/name is always updated on the client (admins only); 28 | - Health and vehicle type is provided only when the "players" tab is open, and every 5s while tab is open; 29 | - The distance is calculated on the client side, and if the player is over the ~425m distance culling limit, it's probably not relevant to know exactly how far he is anyways; 30 | - The initial playerlist (sent after auth) will be 20.5kb if there are 1k players with nickname 16 chars long; 31 | - The detailed playerlist will be 6.8kb for 1k players; 32 | - The refresh interval for now is fixed at 2500ms, but I already coded a linear function to increase it to 5s when the server reaches 150 players online (tests pending) - https://www.desmos.com/calculator/ls0lfokshc. 33 | 34 | 35 | ## Taso Specs 36 | - React updates it's internal playerlist when receives the `setPlayerlist` event; 37 | - React calls `iNeedPlayerlistDetails` when the "tabs page" is opened, and then every 5s while it's open; 38 | - When the "players" tab open, it's okay to show the existing (outdated) playerlist, but need to show in yellow somewhere "updating playerlist..." and then remove it on the first `setPlayerlist` received after opening the page; 39 | - The playerlist will always have the 4 values; 40 | - `vType` can be one of: `unknown, walking, driving, biking, boating, flying`; 41 | - `dist` is the integer distance calculated locally (so culling applies). Will be `-1` if unknown; 42 | - `health` is always between 0 and 200, there is no unknown for this one. 43 | 44 | ```json 45 | { 46 | //Example self 47 | "1": { 48 | "name": "Tabarra", 49 | "vType": "unknown", 50 | "dist": 0, 51 | "health": 200 52 | }, 53 | 54 | //Example just joined 55 | "2": { 56 | "name": "poophead", 57 | "vType": "unknown", 58 | "dist": -1, 59 | "health": 0 60 | }, 61 | 62 | //Example someone close 63 | "3": { 64 | "name": "tittiesface", 65 | "vType": "walking", 66 | "dist": 152, 67 | "health": 200 68 | }, 69 | 70 | //Example someone far 71 | "4": { 72 | "name": "boaty mcboatface", 73 | "vType": "driving", 74 | "dist": -1, 75 | "health": 200 76 | } 77 | } 78 | ``` 79 | -------------------------------------------------------------------------------- /monitor/resource/menu/client/cl_webpipe.lua: -------------------------------------------------------------------------------- 1 | -- ============================================= 2 | -- This file contains all Client WebPipe logic. 3 | -- It is used to pass NUI HTTP reqs to txAdmin 4 | -- ============================================= 5 | if (GetConvar('txAdmin-menuEnabled', 'false') ~= 'true') then 6 | return 7 | end 8 | -- Vars 9 | local pipeReturnCallbacks = {} 10 | local pipeCallbackCounter = 1 11 | 12 | ---@class StaticCacheEntry 13 | ---@field body string 14 | ---@field headers string 15 | 16 | ---@class StaticCacheData : table 17 | local staticCacheData = {} 18 | 19 | -- catching all NUI requests for https://monitor/WebPipe/ 20 | RegisterRawNuiCallback('WebPipe', function(req, cb) 21 | local path = req.path 22 | local headers = req.headers 23 | local body = req.body 24 | local method = req.method 25 | 26 | debugPrint(("^3WebPipe[^1%d^3]^0 ^2%s ^4%s^0"):format(pipeCallbackCounter, method, path)) 27 | if staticCacheData[path] ~= nil then 28 | debugPrint(("^3WebPipe[^1%d^3]^0 ^2answered from cache!"):format(pipeCallbackCounter)) 29 | local cacheEntry = staticCacheData[path] 30 | cb({ 31 | status = 200, 32 | body = cacheEntry.body, 33 | headers = cacheEntry.headers, 34 | }) 35 | return 36 | end 37 | 38 | -- Cookie wiper to prevent sticky cookie sessions after reauth 39 | if path == '/nui/resetSession' then 40 | if type(headers['Cookie']) ~= 'string' then 41 | return cb({ 42 | status = 200, 43 | body = '{}', 44 | }) 45 | else 46 | local cookies = {} 47 | for cookie in headers['Cookie']:gmatch('(tx:[^=]+)') do 48 | cookies[#cookies +1] = cookie.."=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; httponly; SameSite=None; Secure" 49 | end 50 | return cb({ 51 | status = 200, 52 | body = '{}', 53 | headers = { 54 | ['Connection'] = "close", 55 | ['Content-Type'] = "text/plain", 56 | ['Set-Cookie'] = cookies 57 | } 58 | }) 59 | end 60 | end 61 | 62 | local id = pipeCallbackCounter 63 | pipeReturnCallbacks[id] = { cb = cb, path = path } 64 | pipeCallbackCounter = pipeCallbackCounter + 1 65 | if pipeCallbackCounter > 2048 then 66 | pipeCallbackCounter = 1 67 | end 68 | 69 | TriggerServerEvent('txAdmin:WebPipe', id, method, path, headers, body or '') 70 | end) 71 | 72 | 73 | -- receive the http responses from server 74 | RegisterNetEvent('txAdmin:WebPipe') 75 | AddEventHandler('txAdmin:WebPipe', function(callbackId, statusCode, body, headers) 76 | local ret = pipeReturnCallbacks[callbackId] 77 | if not ret then return end 78 | 79 | local sub = string.sub 80 | if 81 | sub(ret.path, 1, 5) == '/css/' or 82 | sub(ret.path, 1, 4) == '/js/' or 83 | sub(ret.path, 1, 5) == '/img/' or 84 | sub(ret.path, 1, 7) == '/fonts/' 85 | then 86 | staticCacheData[ret.path] = { 87 | body = body, 88 | headers = headers, 89 | } 90 | end 91 | 92 | ret.cb({ 93 | status = statusCode, 94 | body = body, 95 | headers = headers 96 | }) 97 | 98 | pipeReturnCallbacks[callbackId] = nil 99 | debugPrint("^3WebPipe[^1" .. callbackId .. "^3]^0 ^2finished^0 (" .. #pipeReturnCallbacks .. " open)") 100 | end) 101 | -------------------------------------------------------------------------------- /monitor/docs/events.md: -------------------------------------------------------------------------------- 1 | # Custom Events 2 | 3 | Starting in v3.2, **txAdmin** now has the ability to trigger server events. 4 | The event name will be `txAdmin:events:` and the first (and only) parameter will be a table that may contain relevant data. 5 | 6 | 7 | ## txAdmin:events:scheduledRestart (v3.2) 8 | Called automatically `[30, 15, 10, 5, 4, 3, 2, 1]` minutes before a scheduled restart, as well as the times configured in the settings page. 9 | Event Data: 10 | - `secondsRemaining`: The number of seconds before the scheduled restart. 11 | 12 | Example usage on ESX v1.2: 13 | ```lua 14 | ESX = nil 15 | TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end) 16 | 17 | AddEventHandler('txAdmin:events:scheduledRestart', function(eventData) 18 | if eventData.secondsRemaining == 60 then 19 | CreateThread(function() 20 | Wait(45000) 21 | print("15 seconds before restart... saving all players!") 22 | ESX.SavePlayers(function() 23 | -- do something 24 | end) 25 | end) 26 | end 27 | end) 28 | ``` 29 | 30 | 31 | ## txAdmin:events:playerKicked (v3.7) 32 | Called when a player is kicked using txAdmin. 33 | Event Data: 34 | - `target`: The id of the player that was kicked. 35 | - `author`: The name of the admin. 36 | - `reason`: The reason of the kick. 37 | 38 | 39 | ## txAdmin:events:playerWarned (v3.7) 40 | Called when a player is warned using txAdmin. 41 | Event Data: 42 | - `target`: The id of the player that was warned. 43 | - `author`: The name of the admin. 44 | - `reason`: The reason of the warn. 45 | - `actionId`: The ID of this action. 46 | 47 | 48 | ## txAdmin:events:playerBanned (v3.7) 49 | Called when a player is banned using txAdmin. 50 | Event Data: 51 | - `target`: The id of the player that was banned. 52 | - `author`: The name of the admin. 53 | - `reason`: The reason of the ban. 54 | - `actionId`: The ID of this action. 55 | - `expiration`: The timestamp for this ban expiration, for `false` if permanent. Added in txAdmin v4.9. 56 | 57 | 58 | ## txAdmin:events:playerWhitelisted (v3.7) 59 | Called when a player is whitelisted using txAdmin. 60 | Event Data: 61 | - `author`: The name of the admin. 62 | - `actionId`: The ID of this action. 63 | - `target`: The reference of this whitelist. Can be "license:" prefixed license or a whitelist request ID. 64 | 65 | ## txAdmin:event:configChanged (v4.0) 66 | Called when the txAdmin settings change in a way that could be relevant for the server. 67 | Event Data: this event has no data. 68 | At the moment, this is only used to signal the txAdmin in-game Menu if the configured language has changed, and can be used to easily test custom language files without requiring a server restart. 69 | 70 | ## txAdmin:events:healedPlayer (v4.8) 71 | Called when a heal event is triggered for a player/whole server. 72 | This is most useful for servers running "ambulance job" or other resources that keep a player unconscious even after the health being restored to 100%; 73 | Event Data: 74 | - `id`: The ID of the healed player, or `-1` if the entire server was healed. 75 | 76 | ## txAdmin:events:announcement (v4.8) 77 | Called when an announcement is made using txAdmin. 78 | Event Data: 79 | - `author`: The name of the admin or `txAdmin`. 80 | - `message`: The message of the broadcast. 81 | 82 | ## txAdmin:events:serverShuttingDown (v4.15) 83 | Called when the server is about to shut down. 84 | This can be triggered in a scheduled and unscheduled stop or restart, by an admin or by the system. 85 | Event Data: 86 | - `delay`: How many milliseconds txAdmin will wait before killing the server process. 87 | - `author`: The name of the admin or `txAdmin`. 88 | - `message`: The message of the broadcast. 89 | -------------------------------------------------------------------------------- /monitor/web/parts/changePasswordModal.ejs: -------------------------------------------------------------------------------- 1 | 2 | 66 | -------------------------------------------------------------------------------- /monitor/docs/development.md: -------------------------------------------------------------------------------- 1 | # txAdmin Development 2 | If you are interested in development of txAdmin, this short guide will help setup your environment. 3 | Before starting, please make sure you are familiar with the basics of NodeJS & ecosystem. 4 | > **Note:** This guide does not cover translations, [which are very easy to do!](./translation.md) 5 | 6 | 7 | ## Requirements 8 | - Windows, as the `main-builder.js` is doesn't yet work for other OSs; 9 | - NodeJS v16.x to match the one in FXServer; 10 | - FXServer; 11 | 12 | 13 | ## Project Structure 14 | - `core`: Node Backend & Components. This part is transpiled by `tsc` and then bundled with `esbuild`; 15 | - `resource`: The in-game resource that runs under the `monitor` name. These files will be synchronized with the deploy path when running the `dev:main` npm script; 16 | - `menu`: React source code for txAdmin's NUI Menu. It is transpiled & built using Vite; 17 | - `web`: SSR templates & static assets used for the txAdmin's web panel. Right now this uses EJS as templating engine, which should change soon to also be React with Vite; 18 | - `scripts`: The scripts used for development only. 19 | 20 | 21 | ## Preparing the environment 22 | 1. First, clone the txAdmin repository; 23 | ```sh 24 | git clone https://github.com/tabarra/txAdmin 25 | ``` 26 | 2. Install dependencies & prepare commit hook; 27 | ```sh 28 | npm install 29 | npm run prepare 30 | ``` 31 | 3. Edit `.deploy.config.js > fxserverPath` to the path of your `FXServer.exe` file. 32 | 33 | 34 | ## Development Workflows 35 | 36 | ### Core/Resource 37 | This workflow is controlled by `main-builder.js`, which is responsible for: 38 | - Watching and copying static files (resource, docs, license, entry file, etc) to the deploy path; 39 | - Watching and re-transpiling the core files, and then bundling and deploying it; 40 | - Run FXServer (in the same terminal), and restarting it when the core is modified (like `nodemon`, but fancy). 41 | 42 | Although the code is somewhat complex, to run it simply: 43 | ```sh 44 | npm run dev:main 45 | ``` 46 | 47 | ### NUI Menu 48 | To run Vite on browser dev mode: 49 | ```sh 50 | npm run dev:menu:browser 51 | ``` 52 | 53 | To run Vite on game dev mode: 54 | ```sh 55 | npm run dev:menu:browser 56 | ``` 57 | Keep in mind that for every change you will need to restart the `monitor` resource, and unless you started the server with `+setr txAdmin-menuDebug true` txAdmin will detect that as a crash and restart your server. 58 | 59 | 60 | ### Building/Publishing 61 | First make sure the files are linted properly and that the typecheck is successful, and then run the build command. The output will be on the `dist/` folder. 62 | ```sh 63 | npm run lint 64 | npm run typecheck 65 | npm run build 66 | ``` 67 | 68 | ## Note regarding the Web UI 69 | 70 | **⚠Warning: Soon we will rewrite the entire Web UI in react, so don't put any effort in the Web UI right now.** 71 | 72 | **DO NOT** Modify `css/coreui.css`. Either do a patch in the `custom.css` or modify the SCSS variables. 73 | This doc is a reference if you are trying to re-build the `css/coreui.css` from the SCSS source. 74 | The only thing I changed from CoreUI was the `aside-menu` size from 200px to 300px in `scss/_variables.scss : $aside-menu-width`. 75 | You can find the other variable names in `node_modules/@coreui/coreui/scss/coreui`. 76 | 77 | ```bash 78 | git clone https://github.com/coreui/coreui-free-bootstrap-admin-template.git coreui 79 | cd coreui 80 | npm i 81 | 82 | # If you want to make sure you used the same version of CoreUI 83 | git checkout 0cb1d81a8471ff4b6eb80c41b45c61a8e2ab3ef6 84 | 85 | # Edit your stuff and then to compile: 86 | npx node-sass --output-style expanded --source-map true --source-map-contents true --precision 6 src/scss/style.scss src/css/style.css 87 | ``` 88 | 89 | Then copy the `src/css/style.css` to txAdmin's folder. 90 | -------------------------------------------------------------------------------- /monitor/web/parts/adminModal.ejs: -------------------------------------------------------------------------------- 1 | 2 |
3 | 9 |
10 |
11 | > 13 |
14 |
15 |
16 | 17 | 18 |
19 | 23 |
24 |
25 | 27 |
28 | 29 | The admin's https://forum.cfx.re/ username. This is required if you want to login using the Cfx.re button. 30 | 31 |
32 |
33 | 34 | 35 |
36 | 40 |
41 |
42 | 43 |
44 | 45 | The admin's Discord User ID. Follow this guide if you don't know how to get the User ID. 46 | 47 |
48 |
49 | 50 | 51 |
52 |
53 |
Permissions
54 |
55 |
56 | <% if (editingSelf) { %> 57 |
58 |
you cannot edit your own permissions
59 |
60 | <% } else { %> 61 |
62 | <% for (const [key, perm] of permsMenu.entries()) { %> 63 |
64 | > 66 | 70 |
71 | <% } %> 72 |
73 |
74 | <% for (const [key, perm] of permsGeneral.entries()) { %> 75 |
76 | > 78 | 79 |
80 | <% } %> 81 |
82 | <% } %> 83 |
84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /monitor/resource/sv_admins.lua: -------------------------------------------------------------------------------- 1 | -- ============================================= 2 | -- Lua Admin Manager 3 | -- ============================================= 4 | -- Checking Environment (sv_main MUST run first) 5 | if GetConvar('txAdminServerMode', 'false') ~= 'true' then 6 | return 7 | end 8 | if TX_LUACOMHOST == "invalid" or TX_LUACOMTOKEN == "invalid" then 9 | log('^1API Host or Pipe Token ConVars not found. Do not start this resource if not using txAdmin.') 10 | return 11 | end 12 | if TX_LUACOMTOKEN == "removed" then 13 | log('^1Please do not restart the monitor resource.') 14 | return 15 | end 16 | 17 | 18 | -- Variables & Consts 19 | local failedAuths = {} 20 | local attemptCooldown = 15000 21 | 22 | 23 | -- Handle auth failures 24 | local function handleAuthFail(src, reason) 25 | local srcString = tostring(src) 26 | TX_ADMINS[srcString] = nil 27 | failedAuths[srcString] = GetGameTimer() 28 | reason = reason or "unknown" 29 | debugPrint("Auth rejected #"..srcString.." ("..reason..")") 30 | TriggerClientEvent('txcl:setAdmin', src, false, false, reason) 31 | end 32 | 33 | -- Handle menu auth requests 34 | RegisterNetEvent('txsv:checkAdminStatus', function() 35 | local src = source 36 | local srcString = tostring(source) 37 | debugPrint('Handling authentication request from player #'..srcString) 38 | 39 | -- Rate Limiter 40 | if type(failedAuths[srcString]) == 'number' and failedAuths[srcString] + attemptCooldown > GetGameTimer() then 41 | return handleAuthFail(source, "too many auth attempts") 42 | end 43 | 44 | -- Prepping http request 45 | local url = "http://"..TX_LUACOMHOST.."/nui/auth" 46 | local headers = { 47 | ['Content-Type'] = 'application/json', 48 | ['X-TxAdmin-Token'] = TX_LUACOMTOKEN, 49 | ['X-TxAdmin-Identifiers'] = table.concat(GetPlayerIdentifiers(src), ', ') 50 | } 51 | 52 | -- Making http request 53 | PerformHttpRequest(url, function(httpCode, data, resultHeaders) 54 | -- Validating response 55 | local resp = json.decode(data) 56 | if not resp or type(resp.isAdmin) ~= "boolean" then 57 | return handleAuthFail(src, "invalid response") 58 | end 59 | if not resp.isAdmin then 60 | return handleAuthFail(src, resp.reason) 61 | end 62 | if type(resp.permissions) ~= 'table' then 63 | resp.permissions = {} 64 | end 65 | 66 | -- Setting up admin 67 | local adminTag = "[#"..src.."] "..resp.username 68 | debugPrint(("^2Authenticated admin ^5%s^2 with permissions: %s"):format( 69 | src, 70 | adminTag, 71 | json.encode(resp.permissions) 72 | )) 73 | TX_ADMINS[srcString] = { 74 | tag = adminTag, 75 | perms = resp.permissions, 76 | bucket = 0 77 | } 78 | sendInitialPlayerlist(src) 79 | TriggerClientEvent('txcl:setAdmin', src, resp.username, resp.permissions) 80 | end, 'GET', '', headers) 81 | end) 82 | 83 | 84 | -- Remove admin from table when disconnected 85 | AddEventHandler('playerDropped', function() 86 | TX_ADMINS[tostring(source)] = nil 87 | end) 88 | 89 | 90 | -- Handle updated admin list 91 | AddEventHandler('txAdmin:events:adminsUpdated', function(onlineAdminIDs) 92 | debugPrint('^3Admins list updated. Online admins: ' .. json.encode(onlineAdminIDs)) 93 | 94 | -- Collect old and new admin IDs as key to prevent duplicate 95 | local refreshAdminIds = {} 96 | for id, _ in pairs(TX_ADMINS) do 97 | refreshAdminIds[id] = true 98 | end 99 | for _, id in pairs(onlineAdminIDs) do 100 | refreshAdminIds[tostring(id)] = true 101 | end 102 | debugPrint('^3Forcing clients to re-auth') 103 | 104 | -- Resetting all admin permissions and rate limiter 105 | TX_ADMINS = {} 106 | failedAuths = {} 107 | 108 | -- Informing clients that they need to reauth 109 | for id, _ in pairs(refreshAdminIds) do 110 | TriggerClientEvent('txAdmin:menu:reAuth', tonumber(id)) 111 | end 112 | end) 113 | -------------------------------------------------------------------------------- /monitor/web/main/cfgEditor.ejs: -------------------------------------------------------------------------------- 1 | <%- await include('parts/header.ejs', locals) %> 2 | 3 | 4 | 5 | 8 | 9 | 17 | 18 | 19 | <% if (!isWebInterface) { %> 20 | 21 | <% } %> 22 | 23 | 24 |
25 |

Server Config File Editor

26 |
27 | 28 | 29 |
30 |
31 | 33 |
34 |
35 |
36 |
37 | 41 | 44 |
45 |
46 | 47 | 48 | <%- await include('parts/footer.ejs', locals) %> 49 | 50 | 51 | 52 | 55 | 56 | 57 | 104 | -------------------------------------------------------------------------------- /monitor/resource/menu/vendor/freecam/camera.lua: -------------------------------------------------------------------------------- 1 | local floor = math.floor 2 | local vector3 = vector3 3 | local SetCamRot = SetCamRot 4 | local IsCamActive = IsCamActive 5 | local SetCamCoord = SetCamCoord 6 | local LoadInterior = LoadInterior 7 | local SetFocusArea = SetFocusArea 8 | local LockMinimapAngle = LockMinimapAngle 9 | local GetInteriorAtCoords = GetInteriorAtCoords 10 | local LockMinimapPosition = LockMinimapPosition 11 | 12 | local _internal_camera = nil 13 | local _internal_isFrozen = false 14 | 15 | local _internal_pos = nil 16 | local _internal_rot = nil 17 | local _internal_fov = nil 18 | local _internal_vecX = nil 19 | local _internal_vecY = nil 20 | local _internal_vecZ = nil 21 | 22 | -------------------------------------------------------------------------------- 23 | 24 | function GetInitialCameraPosition() 25 | if _G.CAMERA_SETTINGS.KEEP_POSITION and _internal_pos then 26 | return _internal_pos 27 | end 28 | 29 | return GetGameplayCamCoord() 30 | end 31 | 32 | function GetInitialCameraRotation() 33 | if _G.CAMERA_SETTINGS.KEEP_ROTATION and _internal_rot then 34 | return _internal_rot 35 | end 36 | 37 | local rot = GetGameplayCamRot() 38 | return vector3(rot.x, 0.0, rot.z) 39 | end 40 | 41 | -------------------------------------------------------------------------------- 42 | 43 | function IsFreecamFrozen() 44 | return _internal_isFrozen 45 | end 46 | 47 | function SetFreecamFrozen(frozen) 48 | local frozen = frozen == true 49 | _internal_isFrozen = frozen 50 | end 51 | 52 | -------------------------------------------------------------------------------- 53 | 54 | function GetFreecamPosition() 55 | return _internal_pos 56 | end 57 | 58 | function SetFreecamPosition(x, y, z) 59 | local pos = vector3(x, y, z) 60 | local int = GetInteriorAtCoords(pos) 61 | 62 | LoadInterior(int) 63 | SetFocusArea(pos) 64 | LockMinimapPosition(x, y) 65 | SetCamCoord(_internal_camera, pos) 66 | 67 | _internal_pos = pos 68 | end 69 | 70 | -------------------------------------------------------------------------------- 71 | 72 | function GetFreecamRotation() 73 | return _internal_rot 74 | end 75 | 76 | 77 | function SetFreecamRotation(x, y, z) 78 | local rotX, rotY, rotZ = ClampCameraRotation(x, y, z) 79 | local vecX, vecY, vecZ = EulerToMatrix(rotX, rotY, rotZ) 80 | local rot = vector3(rotX, rotY, rotZ) 81 | 82 | LockMinimapAngle(floor(rotZ)) 83 | SetCamRot(_internal_camera, rot) 84 | 85 | _internal_rot = rot 86 | _internal_vecX = vecX 87 | _internal_vecY = vecY 88 | _internal_vecZ = vecZ 89 | end 90 | 91 | -------------------------------------------------------------------------------- 92 | 93 | function GetFreecamFov() 94 | return _internal_fov 95 | end 96 | 97 | function SetFreecamFov(fov) 98 | local fov = Clamp(fov, 0.0, 90.0) 99 | SetCamFov(_internal_camera, fov) 100 | _internal_fov = fov 101 | end 102 | 103 | -------------------------------------------------------------------------------- 104 | 105 | function GetFreecamMatrix() 106 | return _internal_vecX, 107 | _internal_vecY, 108 | _internal_vecZ, 109 | _internal_pos 110 | end 111 | 112 | function GetFreecamTarget(distance) 113 | local target = _internal_pos + (_internal_vecY * distance) 114 | return target 115 | end 116 | 117 | -------------------------------------------------------------------------------- 118 | 119 | function IsFreecamActive() 120 | return IsCamActive(_internal_camera) == 1 121 | end 122 | 123 | function SetFreecamActive(active) 124 | if active == IsFreecamActive() then 125 | return 126 | end 127 | 128 | local enableEasing = _G.CAMERA_SETTINGS.ENABLE_EASING 129 | local easingDuration = _G.CAMERA_SETTINGS.EASING_DURATION 130 | 131 | if active then 132 | local pos = GetInitialCameraPosition() 133 | local rot = GetInitialCameraRotation() 134 | 135 | _internal_camera = CreateCam('DEFAULT_SCRIPTED_CAMERA', true) 136 | 137 | SetFreecamFov(_G.CAMERA_SETTINGS.FOV) 138 | SetFreecamPosition(pos.x, pos.y, pos.z) 139 | SetFreecamRotation(rot.x, rot.y, rot.z) 140 | TriggerEvent('freecam:onEnter') 141 | else 142 | DestroyCam(_internal_camera) 143 | ClearFocus() 144 | UnlockMinimapPosition() 145 | UnlockMinimapAngle() 146 | TriggerEvent('freecam:onExit') 147 | end 148 | 149 | RenderScriptCams(active, enableEasing, easingDuration, true, true) 150 | end 151 | -------------------------------------------------------------------------------- /monitor/resource/menu/client/cl_functions.lua: -------------------------------------------------------------------------------- 1 | -- ============================================= 2 | -- This file contains any strictly *pure* functions 3 | -- that are utilized by the rest of the menu. 4 | -- Many of them need to be available with menu disabled 5 | -- ============================================= 6 | 7 | --- Send a persistent alert to NUI 8 | ---@param key string An unique ID for this alert 9 | ---@param level string The level for the alert 10 | ---@param message string The message for this alert 11 | ---@param isTranslationKey boolean Whether the message is a translation key 12 | function sendPersistentAlert(key, level, message, isTranslationKey) 13 | debugPrint(('Sending persistent alert, key: %s, level: %s, message: %s'):format(key, level, message)) 14 | sendMenuMessage('setPersistentAlert', { key = key, level = level, message = message, isTranslationKey = isTranslationKey }) 15 | end 16 | 17 | --- Clear a persistent alert on screen 18 | ---@param key string The unique ID passed in sendPersistentAlert for the notification 19 | function clearPersistentAlert(key) 20 | debugPrint(('Clearing persistent alert, key: %s'):format(key)) 21 | sendMenuMessage('clearPersistentAlert', { key = key }) 22 | end 23 | 24 | --- Snackbar message 25 | ---@param level string The severity of the message can be 'info', 'error', or 'warning' 26 | ---@param message string Message to display with snackbar 27 | function sendSnackbarMessage(level, message, isTranslationKey) 28 | debugPrint(('Sending snackbar message, level: %s, message: %s, isTranslationKey: %s'):format(level, message, isTranslationKey)) 29 | sendMenuMessage('setSnackbarAlert', { level = level, message = message, isTranslationKey = isTranslationKey }) 30 | end 31 | 32 | --- Send data to the NUI frame 33 | ---@param action string Action 34 | ---@param data any Data corresponding to action 35 | function sendMenuMessage(action, data) 36 | SendNUIMessage({ 37 | action = action, 38 | data = data 39 | }) 40 | end 41 | 42 | --- Toggle visibility of the txAdmin NUI menu 43 | function toggleMenuVisibility(visible) 44 | if (visible == true and isMenuVisible) or (visible == false and not isMenuVisible) then 45 | return 46 | end 47 | if visible == nil then 48 | if not isMenuVisible and IsPauseMenuActive() then 49 | return 50 | end 51 | end 52 | 53 | sendReactPlayerlist() 54 | if visible ~= nil then 55 | isMenuVisible = visible 56 | sendMenuMessage('setVisible', visible) 57 | else 58 | isMenuVisible = not isMenuVisible 59 | sendMenuMessage('setVisible', isMenuVisible) 60 | end 61 | -- check if noclip and spectate still works with menu closed 62 | if not isMenuVisible then 63 | SetNuiFocus(false) 64 | SetNuiFocusKeepInput(false) 65 | end 66 | PlaySoundFrontend(-1, SoundEnum['enter'], 'HUD_FRONTEND_DEFAULT_SOUNDSET', 1) 67 | end 68 | 69 | --- Calculate a safe Z coordinate based off the (X, Y) 70 | ---@param x number 71 | ---@param y number 72 | ---@return number|nil 73 | function FindZForCoords(x, y) 74 | local found = true 75 | local START_Z = 1500 76 | local z = START_Z 77 | while found and z > 0 do 78 | local _found, _z = GetGroundZFor_3dCoord(x + 0.0, y + 0.0, z - 1.0) 79 | if _found then 80 | z = _z + 0.0 81 | end 82 | found = _found 83 | Wait(0) 84 | end 85 | if z == START_Z then return nil end 86 | return z + 0.0 87 | end 88 | 89 | --- Display simple help scaleform 90 | ---@param msg string - The message to display 91 | function DisplayHelpTxtThisFrame(msg) 92 | BeginTextCommandDisplayHelp('STRING') 93 | AddTextComponentString(msg) 94 | AddTextComponentSubstringTextLabel(msg) 95 | EndTextCommandDisplayHelp(0, 1, 0, -1); 96 | end 97 | 98 | --- Used for local feedback and permission checks. Checks are still 99 | --- performed on the server. 100 | ---@param perms table Array of all player permissions 101 | ---@param perm string The specific permission 102 | ---@return boolean 103 | function DoesPlayerHavePerm(perms, perm) 104 | if type(perms) ~= 'table' then 105 | return false 106 | end 107 | 108 | for _, v in pairs(perms) do 109 | if v == perm or v == 'all_permissions' then 110 | return true 111 | end 112 | end 113 | 114 | return false 115 | end 116 | -------------------------------------------------------------------------------- /monitor/web/public/js/codeEditor/mode/yaml.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: https://codemirror.net/LICENSE 3 | 4 | (function(mod) { 5 | if (typeof exports == "object" && typeof module == "object") // CommonJS 6 | mod(require("../../lib/codemirror")); 7 | else if (typeof define == "function" && define.amd) // AMD 8 | define(["../../lib/codemirror"], mod); 9 | else // Plain browser env 10 | mod(CodeMirror); 11 | })(function(CodeMirror) { 12 | "use strict"; 13 | 14 | CodeMirror.defineMode("yaml", function() { 15 | 16 | var cons = ['true', 'false', 'on', 'off', 'yes', 'no']; 17 | var keywordRegex = new RegExp("\\b(("+cons.join(")|(")+"))$", 'i'); 18 | 19 | return { 20 | token: function(stream, state) { 21 | var ch = stream.peek(); 22 | var esc = state.escaped; 23 | state.escaped = false; 24 | /* comments */ 25 | if (ch == "#" && (stream.pos == 0 || /\s/.test(stream.string.charAt(stream.pos - 1)))) { 26 | stream.skipToEnd(); 27 | return "comment"; 28 | } 29 | 30 | if (stream.match(/^('([^']|\\.)*'?|"([^"]|\\.)*"?)/)) 31 | return "string"; 32 | 33 | if (state.literal && stream.indentation() > state.keyCol) { 34 | stream.skipToEnd(); return "string"; 35 | } else if (state.literal) { state.literal = false; } 36 | if (stream.sol()) { 37 | state.keyCol = 0; 38 | state.pair = false; 39 | state.pairStart = false; 40 | /* document start */ 41 | if(stream.match(/---/)) { return "def"; } 42 | /* document end */ 43 | if (stream.match(/\.\.\./)) { return "def"; } 44 | /* array list item */ 45 | if (stream.match(/\s*-\s+/)) { return 'meta'; } 46 | } 47 | /* inline pairs/lists */ 48 | if (stream.match(/^(\{|\}|\[|\])/)) { 49 | if (ch == '{') 50 | state.inlinePairs++; 51 | else if (ch == '}') 52 | state.inlinePairs--; 53 | else if (ch == '[') 54 | state.inlineList++; 55 | else 56 | state.inlineList--; 57 | return 'meta'; 58 | } 59 | 60 | /* list seperator */ 61 | if (state.inlineList > 0 && !esc && ch == ',') { 62 | stream.next(); 63 | return 'meta'; 64 | } 65 | /* pairs seperator */ 66 | if (state.inlinePairs > 0 && !esc && ch == ',') { 67 | state.keyCol = 0; 68 | state.pair = false; 69 | state.pairStart = false; 70 | stream.next(); 71 | return 'meta'; 72 | } 73 | 74 | /* start of value of a pair */ 75 | if (state.pairStart) { 76 | /* block literals */ 77 | if (stream.match(/^\s*(\||\>)\s*/)) { state.literal = true; return 'meta'; }; 78 | /* references */ 79 | if (stream.match(/^\s*(\&|\*)[a-z0-9\._-]+\b/i)) { return 'variable-2'; } 80 | /* numbers */ 81 | if (state.inlinePairs == 0 && stream.match(/^\s*-?[0-9\.\,]+\s?$/)) { return 'number'; } 82 | if (state.inlinePairs > 0 && stream.match(/^\s*-?[0-9\.\,]+\s?(?=(,|}))/)) { return 'number'; } 83 | /* keywords */ 84 | if (stream.match(keywordRegex)) { return 'keyword'; } 85 | } 86 | 87 | /* pairs (associative arrays) -> key */ 88 | if (!state.pair && stream.match(/^\s*(?:[,\[\]{}&*!|>'"%@`][^\s'":]|[^,\[\]{}#&*!|>'"%@`])[^#]*?(?=\s*:($|\s))/)) { 89 | state.pair = true; 90 | state.keyCol = stream.indentation(); 91 | return "atom"; 92 | } 93 | if (state.pair && stream.match(/^:\s*/)) { state.pairStart = true; return 'meta'; } 94 | 95 | /* nothing found, continue */ 96 | state.pairStart = false; 97 | state.escaped = (ch == '\\'); 98 | stream.next(); 99 | return null; 100 | }, 101 | startState: function() { 102 | return { 103 | pair: false, 104 | pairStart: false, 105 | keyCol: 0, 106 | inlinePairs: 0, 107 | inlineList: 0, 108 | literal: false, 109 | escaped: false 110 | }; 111 | }, 112 | lineComment: "#", 113 | fold: "indent" 114 | }; 115 | }); 116 | 117 | CodeMirror.defineMIME("text/x-yaml", "yaml"); 118 | CodeMirror.defineMIME("text/yaml", "yaml"); 119 | 120 | }); 121 | -------------------------------------------------------------------------------- /monitor/resource/menu/client/cl_player_ids.lua: -------------------------------------------------------------------------------- 1 | -- ============================================= 2 | -- This file contains all overhead player ID logic 3 | -- ============================================= 4 | if (GetConvar('txAdmin-menuEnabled', 'false') ~= 'true') then 5 | return 6 | end 7 | 8 | local isPlayerIDActive = false 9 | local playerGamerTags = {} 10 | 11 | -- Convar used to determine the distance in which player ID's are visible 12 | local distanceToCheck = GetConvarInt('txAdmin-menuPlayerIdDistance', 150) 13 | 14 | local gamerTagCompsEnum = { 15 | GamerName = 0, 16 | CrewTag = 1, 17 | HealthArmour = 2, 18 | BigText = 3, 19 | AudioIcon = 4, 20 | UsingMenu = 5, 21 | PassiveMode = 6, 22 | WantedStars = 7, 23 | Driver = 8, 24 | CoDriver = 9, 25 | Tagged = 12, 26 | GamerNameNearby = 13, 27 | Arrow = 14, 28 | Packages = 15, 29 | InvIfPedIsFollowing = 16, 30 | RankText = 17, 31 | Typing = 18 32 | } 33 | 34 | local function cleanUpGamerTags() 35 | debugPrint('Cleaning up gamer tags table') 36 | for _, v in pairs(playerGamerTags) do 37 | if IsMpGamerTagActive(v.gamerTag) then 38 | RemoveMpGamerTag(v.gamerTag) 39 | end 40 | end 41 | playerGamerTags = {} 42 | end 43 | 44 | local function showGamerTags() 45 | local curCoords = GetEntityCoords(PlayerPedId()) 46 | -- Per infinity this will only return players within 300m 47 | local allActivePlayers = GetActivePlayers() 48 | 49 | for _, i in ipairs(allActivePlayers) do 50 | local targetPed = GetPlayerPed(i) 51 | local playerStr = '[' .. GetPlayerServerId(i) .. ']' .. ' ' .. GetPlayerName(i) 52 | 53 | -- If we have not yet indexed this player or their tag has somehow dissapeared (pause, etc) 54 | if not playerGamerTags[i] or not IsMpGamerTagActive(playerGamerTags[i].gamerTag) then 55 | playerGamerTags[i] = { 56 | gamerTag = CreateFakeMpGamerTag(targetPed, playerStr, false, false, 0), 57 | ped = targetPed 58 | } 59 | end 60 | 61 | local targetTag = playerGamerTags[i].gamerTag 62 | 63 | local targetPedCoords = GetEntityCoords(targetPed) 64 | 65 | -- Distance Check 66 | if #(targetPedCoords - curCoords) <= distanceToCheck then 67 | -- Setup name 68 | SetMpGamerTagVisibility(targetTag, gamerTagCompsEnum.GamerName, 1) 69 | 70 | -- Setup AudioIcon 71 | SetMpGamerTagAlpha(targetTag, gamerTagCompsEnum.AudioIcon, 255) 72 | -- Set audio to red when player is talking 73 | SetMpGamerTagVisibility(targetTag, gamerTagCompsEnum.AudioIcon, NetworkIsPlayerTalking(i)) 74 | -- Setup Health 75 | SetMpGamerTagHealthBarColor(targetTag, 129) 76 | SetMpGamerTagAlpha(targetTag, gamerTagCompsEnum.HealthArmour, 255) 77 | SetMpGamerTagVisibility(targetTag, gamerTagCompsEnum.HealthArmour, 1) 78 | else 79 | -- Cleanup name 80 | SetMpGamerTagVisibility(targetTag, gamerTagCompsEnum.GamerName, 0) 81 | -- Cleanup Health 82 | SetMpGamerTagVisibility(targetTag, gamerTagCompsEnum.HealthArmour, 0) 83 | -- Cleanup AudioIcon 84 | SetMpGamerTagVisibility(targetTag, gamerTagCompsEnum.AudioIcon, 0) 85 | end 86 | end 87 | end 88 | 89 | local function showPlayerIDs(enabled) 90 | if not menuIsAccessible then return end 91 | 92 | isPlayerIDActive = enabled 93 | if not isPlayerIDActive then 94 | sendSnackbarMessage('info', 'nui_menu.page_main.player_ids.alert_hide', true) 95 | -- Remove all gamer tags and clear out active table 96 | cleanUpGamerTags() 97 | else 98 | sendSnackbarMessage('info', 'nui_menu.page_main.player_ids.alert_show', true) 99 | end 100 | 101 | debugPrint('Show Player IDs Status: ' .. tostring(isPlayerIDActive)) 102 | end 103 | 104 | RegisterNetEvent('txAdmin:menu:showPlayerIDs', function(enabled) 105 | debugPrint('Received showPlayerIDs event') 106 | showPlayerIDs(enabled) 107 | end) 108 | 109 | local function togglePlayerIDsHandler() 110 | TriggerServerEvent('txAdmin:menu:showPlayerIDs', not isPlayerIDActive) 111 | end 112 | 113 | RegisterNUICallback('togglePlayerIDs', function(_, cb) 114 | togglePlayerIDsHandler() 115 | cb({}) 116 | end) 117 | 118 | RegisterCommand('txAdmin:menu:togglePlayerIDs', togglePlayerIDsHandler) 119 | 120 | CreateThread(function() 121 | local sleep = 150 122 | while true do 123 | if isPlayerIDActive then 124 | showGamerTags() 125 | sleep = 50 126 | else 127 | sleep = 500 128 | end 129 | Wait(sleep) 130 | end 131 | end) 132 | -------------------------------------------------------------------------------- /monitor/resource/cl_main.lua: -------------------------------------------------------------------------------- 1 | -- ============================================= 2 | -- ServerCtx Synchronization 3 | -- ============================================= 4 | ServerCtx = false 5 | 6 | -- NOTE: for now the ServerCtx is only being set when the menu tries to load (enabled or not) 7 | --- Will update ServerCtx based on GlobalState and will send it to NUI 8 | function updateServerCtx() 9 | _ServerCtx = GlobalState.txAdminServerCtx 10 | if _ServerCtx == nil then 11 | print('^3ServerCtx fallback support activated.') 12 | TriggerServerEvent('txAdmin:events:getServerCtx') 13 | else 14 | ServerCtx = _ServerCtx 15 | print('^2ServerCtx updated from global state') 16 | end 17 | end 18 | 19 | RegisterNetEvent('txAdmin:events:setServerCtx', function(ctx) 20 | if type(ctx) ~= 'table' then return end 21 | ServerCtx = ctx 22 | print('^2ServerCtx updated from server event.') 23 | sendMenuMessage('setServerCtx', ServerCtx) 24 | end) 25 | 26 | 27 | 28 | -- ============================================= 29 | -- Warn & Announcement handling 30 | -- ============================================= 31 | -- Dispatch Announcements 32 | RegisterNetEvent('txAdmin:receiveAnnounce', function(message, author) 33 | sendMenuMessage( 34 | 'addAnnounceMessage', 35 | { 36 | message = message, 37 | author = author 38 | } 39 | ) 40 | end) 41 | 42 | -- TODO: remove [SPACE] holding requirement? 43 | local isRDR = not TerraingridActivate and true or false 44 | local dismissKey = isRDR and 0xD9D0E1C0 or 22 45 | local dismissKeyGroup = isRDR and 1 or 0 46 | RegisterNetEvent('txAdminClient:warn', function(author, reason) 47 | toggleMenuVisibility(false) 48 | sendMenuMessage('setWarnOpen', { 49 | reason = reason, 50 | warnedBy = author 51 | }) 52 | CreateThread(function() 53 | local countLimit = 100 --10 seconds 54 | local count = 0 55 | while true do 56 | Wait(100) 57 | if IsControlPressed(dismissKeyGroup, dismissKey) then 58 | count = count +1 59 | if count >= countLimit then 60 | sendMenuMessage('closeWarning') 61 | return 62 | elseif math.fmod(count, 10) == 0 then 63 | sendMenuMessage('pulseWarning') 64 | end 65 | else 66 | count = 0 67 | end 68 | end 69 | end) 70 | end) 71 | 72 | 73 | -- ============================================= 74 | -- Other stuff 75 | -- ============================================= 76 | -- Removing unwanted chat suggestions 77 | -- We only want suggestion for: /tx, /txAdmin-debug, /txAdmin-reauth 78 | -- The suggestion is added after 500ms, so we need to wait more 79 | CreateThread(function() 80 | Wait(1000) 81 | --Commands 82 | TriggerEvent('chat:removeSuggestion', '/txadmin') --too spammy 83 | TriggerEvent('chat:removeSuggestion', '/txaPing') 84 | TriggerEvent('chat:removeSuggestion', '/txaKickAll') 85 | TriggerEvent('chat:removeSuggestion', '/txaKickID') 86 | TriggerEvent('chat:removeSuggestion', '/txaDropIdentifiers') 87 | TriggerEvent('chat:removeSuggestion', '/txaEvent') 88 | TriggerEvent('chat:removeSuggestion', '/txaSendDM') 89 | TriggerEvent('chat:removeSuggestion', '/txaReportResources') 90 | 91 | --Keybinds 92 | TriggerEvent('chat:removeSuggestion', '/txAdmin:menu:noClipToggle') 93 | TriggerEvent('chat:removeSuggestion', '/txAdmin:menu:endSpectate') 94 | TriggerEvent('chat:removeSuggestion', '/txAdmin:menu:openPlayersPage') 95 | TriggerEvent('chat:removeSuggestion', '/txAdmin:menu:togglePlayerIDs') 96 | 97 | --Convars 98 | TriggerEvent('chat:removeSuggestion', '/txAdmin-version') 99 | TriggerEvent('chat:removeSuggestion', '/txAdmin-locale') 100 | TriggerEvent('chat:removeSuggestion', '/txAdmin-localeFile') 101 | TriggerEvent('chat:removeSuggestion', '/txAdmin-verbose') 102 | TriggerEvent('chat:removeSuggestion', '/txAdmin-luaComHost') 103 | TriggerEvent('chat:removeSuggestion', '/txAdmin-luaComToken') 104 | TriggerEvent('chat:removeSuggestion', '/txAdmin-checkPlayerJoin') 105 | TriggerEvent('chat:removeSuggestion', '/txAdmin-pipeToken') 106 | TriggerEvent('chat:removeSuggestion', '/txAdminServerMode') 107 | 108 | --Menu convars 109 | TriggerEvent('chat:removeSuggestion', '/txAdmin-menuEnabled') 110 | TriggerEvent('chat:removeSuggestion', '/txAdmin-menuAlignRight') 111 | TriggerEvent('chat:removeSuggestion', '/txAdmin-menuPageKey') 112 | TriggerEvent('chat:removeSuggestion', '/txAdmin-menuDebug') 113 | TriggerEvent('chat:removeSuggestion', '/txAdmin-playerIdDistance') 114 | TriggerEvent('chat:removeSuggestion', '/txAdmin-menuDrunkDuration') 115 | end) 116 | 117 | -------------------------------------------------------------------------------- /monitor/resource/menu/client/cl_trollactions.lua: -------------------------------------------------------------------------------- 1 | -- ============================================= 2 | -- Troll action logic from the player modal is located here (callbacks, events) 3 | -- ============================================= 4 | if (GetConvar('txAdmin-menuEnabled', 'false') ~= 'true') then 5 | return 6 | end 7 | 8 | local EFFECT_TIME_MS = GetConvarInt('txAdmin-menuDrunkDuration', 30)*1000 9 | local DRUNK_ANIM_SET = "move_m@drunk@verydrunk" 10 | 11 | local DRUNK_DRIVING_EFFECTS = { 12 | 1, -- brake 13 | 7, --turn left + accelerate 14 | 8, -- turn right + accelerate 15 | 23, -- accelerate 16 | 4, -- turn left 90 + braking 17 | 5, -- turn right 90 + braking 18 | } 19 | 20 | local function getRandomDrunkCarTask() 21 | math.randomseed(GetGameTimer()) 22 | 23 | return DRUNK_DRIVING_EFFECTS[math.random(#DRUNK_DRIVING_EFFECTS)] 24 | end 25 | 26 | -- NOTE: We might want to check if a player already has an effect 27 | local function drunkThread() 28 | local playerPed = PlayerPedId() 29 | local isDrunk = true 30 | 31 | debugPrint('Starting drunk effect') 32 | RequestAnimSet(DRUNK_ANIM_SET) 33 | while not HasAnimSetLoaded(DRUNK_ANIM_SET) do 34 | Wait(5) 35 | end 36 | 37 | SetPedMovementClipset(playerPed, DRUNK_ANIM_SET) 38 | ShakeGameplayCam("DRUNK_SHAKE", 3.0) 39 | SetPedIsDrunk(playerPed, true) 40 | SetTransitionTimecycleModifier("spectator5", 10.00) 41 | 42 | CreateThread(function() 43 | while isDrunk do 44 | local vehPedIsIn = GetVehiclePedIsIn(playerPed) 45 | local isPedInVehicleAndDriving = (vehPedIsIn ~= 0) and (GetPedInVehicleSeat(vehPedIsIn, -1) == playerPed) 46 | 47 | if isPedInVehicleAndDriving then 48 | local randomTask = getRandomDrunkCarTask() 49 | debugPrint('Dispatching random car tasks: ' .. randomTask) 50 | TaskVehicleTempAction(playerPed, vehPedIsIn, randomTask, 500) 51 | end 52 | 53 | Wait(5000) 54 | end 55 | end) 56 | 57 | Wait(EFFECT_TIME_MS) 58 | debugPrint('Cleaning up drunk effect') 59 | isDrunk = false 60 | SetTransitionTimecycleModifier("default", 10.00) 61 | StopGameplayCamShaking(true) 62 | ResetPedMovementClipset(playerPed) 63 | RemoveAnimSet(DRUNK_ANIM_SET) 64 | end 65 | 66 | 67 | --[[ Wild Attack command ]] 68 | local attackAnimalHashes = { 69 | GetHashKey("a_c_chimp"), 70 | GetHashKey("a_c_rottweiler"), 71 | GetHashKey("a_c_coyote") 72 | } 73 | local animalGroupHash = GetHashKey("Animal") 74 | local playerGroupHash = GetHashKey("PLAYER") 75 | 76 | local function startWildAttack() 77 | -- Consts 78 | local playerPed = PlayerPedId() 79 | local animalHash = attackAnimalHashes[math.random(#attackAnimalHashes)] 80 | local coordsBehindPlayer = GetOffsetFromEntityInWorldCoords(playerPed, 100, -15.0, 0) 81 | local playerHeading = GetEntityHeading(playerPed) 82 | local belowGround, groundZ, vec3OnFloor = GetGroundZAndNormalFor_3dCoord(coordsBehindPlayer.x, coordsBehindPlayer.y, coordsBehindPlayer.z) 83 | 84 | -- Requesting model 85 | RequestModel(animalHash) 86 | while not HasModelLoaded(animalHash) do 87 | Wait(5) 88 | end 89 | SetModelAsNoLongerNeeded(animalHash) 90 | 91 | -- Creating Animal & setting player as enemy 92 | local animalPed = CreatePed(1, animalHash, coordsBehindPlayer.x, coordsBehindPlayer.y, groundZ, playerHeading, true, false) 93 | SetPedFleeAttributes(animalPed, 0, 0) 94 | SetPedRelationshipGroupHash(animalPed, animalGroupHash) 95 | TaskSetBlockingOfNonTemporaryEvents(animalPed, true) 96 | TaskCombatHatedTargetsAroundPed(animalPed, 30.0, 0) 97 | ClearPedTasks(animalPed) 98 | TaskPutPedDirectlyIntoMelee(animalPed, playerPed, 0.0, -1.0, 0.0, 0) 99 | SetRelationshipBetweenGroups(5, animalGroupHash, playerGroupHash) 100 | SetRelationshipBetweenGroups(5, playerGroupHash, animalGroupHash) 101 | end 102 | -- RegisterCommand('atk', startWildAttack) 103 | 104 | 105 | --[[ Net Events ]] 106 | RegisterNetEvent('txAdmin:menu:drunkEffect', drunkThread) 107 | 108 | RegisterNetEvent('txAdmin:menu:setOnFire', function() 109 | debugPrint('Setting player on fire') 110 | local playerPed = PlayerPedId() 111 | StartEntityFire(playerPed) 112 | end) 113 | 114 | RegisterNetEvent('txAdmin:menu:wildAttack', function() 115 | startWildAttack() 116 | end) 117 | 118 | 119 | --[[ NUI Callbacks ]] 120 | RegisterNUICallback('drunkEffectPlayer', function(data, cb) 121 | TriggerServerEvent('txAdmin:menu:drunkEffectPlayer', tonumber(data.id)) 122 | cb({}) 123 | end) 124 | 125 | RegisterNUICallback('setOnFire', function(data, cb) 126 | TriggerServerEvent('txAdmin:menu:setOnFire', tonumber(data.id)) 127 | cb({}) 128 | end) 129 | 130 | RegisterNUICallback('wildAttack', function(data, cb) 131 | TriggerServerEvent('txAdmin:menu:wildAttack', tonumber(data.id)) 132 | cb({}) 133 | end) 134 | -------------------------------------------------------------------------------- /monitor/docs/menu.md: -------------------------------------------------------------------------------- 1 | # In-Game Menu 2 | 3 | txAdmin v4.0.0 introduced an in-game menu equipped with common admin functionality, 4 | an online player browser, and a slightly trimmed down version of the web panel. 5 | 6 | You can find a short preview video [here](https://www.youtube.com/watch?v=jWKg0VQK0sc) 7 | 8 | ## Accessing the Menu 9 | 10 | You can access the menu in-game by using the command `/tx` or `/txadmin`, alternatively 11 | you can also use a keybind by going to `Game Settings > Key Bindings > FiveM` and 12 | setting the `(txAdmin) Menu: Open Main Page` option. 13 | 14 | ### Permissions 15 | Anybody who you would like to give permissions to open the menu in-game, must have a txAdmin 16 | account with either their Discord or Cfx.re identifiers tied to it. 17 | 18 | ***If you do not have any of these identifiers attached, you will not be able to access the menu*** 19 | 20 | You can further control the menu options accessible to admins by changing their permissions 21 | in the admin manager as shown below. 22 | 23 | ![img](https://i.imgur.com/LP7Ij8M.png) 24 | 25 | ## Convars 26 | The txAdmin menu has a variety of different convars that can alter the default behavior of the menu. 27 | Convars configured in the settings page should not be set manually. 28 | 29 | **txAdmin-menuEnabled** (settings page only) 30 | - Description: Whether the menu is enabled or not. Changing it requires server restart. 31 | - Default: `true` 32 | 33 | **txAdmin-menuAlignRight** (settings page only) 34 | - Description: Whether to align the menu to the right of the screen instead of the left. 35 | - Default: `false` 36 | 37 | **txAdmin-menuPageKey** (settings page only) 38 | - Description: Will change the key used for changing pages in the menu. This value must be the exact browser key code for your preferred key. You can use [this](https://keycode.info/) website and the `event.code` section to find it. 39 | - Default: `Tab` 40 | 41 | **txAdmin-menuDebug** 42 | - Description: Will toggle debug printing on the server and client. 43 | - Default: `false` 44 | - Usage: `+setr txAdmin-menuDebug true` 45 | 46 | **txAdmin-menuPlayerIdDistance** 47 | - Description: The distance in which Player IDs become visible, if toggled on. 48 | - Default: 150 49 | - Usage: `+setr txAdmin-menuPlayerIdDistance 100` 50 | 51 | **txAdmin-menuDrunkDuration** 52 | - Description: How many seconds the drunk effect (troll action) should last. 53 | - Default: 30 54 | - Usage: `+setr txAdmin-menuDrunkDuration 120` 55 | 56 | **txAdmin-menuPtfxDisable** 57 | - Description: Determine whether to not play particles effects whenever an admin's player mode is changed. 58 | - Default: `false` 59 | - Usage: `+set txAdmin-menuPtfxDisable true` 60 | 61 | **txAdmin-menuAnnounceNotiPos** 62 | - Description: Determines the location of the txAdmin announcement notification. This **must** use one of the following valid 63 | positions, `top-center`, `top-left`, `top-right`, `bottom-center`, `bottom-left`, `bottom-right`. 64 | - Default: `top-center` 65 | - Usage: `+set txAdmin-menuAnnounceNotiPos top-right` 66 | 67 | ## Commands 68 | **tx | txadmin** 69 | - Description: Will toggle the in-game menu. This command has an optional argument of a player id that will quickly open up the target player's info modal. 70 | - Usage: `/tx (playerID)`, `/txadmin (playerID)` 71 | - Required Perm: `Must be an admin registered in the Admin Manager` 72 | 73 | **txAdmin-debug** 74 | - Description: Will toggle on debug mode without requiring a restart. (Can be used from console) 75 | - Usage: `/txAdmin-debug [0 | 1]` 76 | - Required Perm: `control.server` 77 | 78 | **txAdmin-reauth** 79 | - Description: Will retrigger the reauthentication process. 80 | - Usage: `/txAdmin-reauth` 81 | - Required Perm: `none` 82 | 83 | ## Troubleshooting menu access 84 | 85 | - If you type `/tx` and nothing happens, your menu is probably disabled. 86 | - If you see a red message like [this](https://i.imgur.com/G83uTNC.png) and you are registered on txAdmin, you can type `/txAdmin-reauth` in the chat to retry the authentication. 87 | - If you can't authenticate and the reason id `Invalid Request: source`, this means the source IP of the HTTP request being made by fxserver to txAdmin is not a "localhost" one, which might occur if your host has multiple IPs. To disable this protection, edit your `config.json` file and add `webServer.disableNuiSourceCheck` with value `true` then restart txAdmin. 88 | 89 | ## Development 90 | You can find development instructions regarding the menu [here.](https://github.com/tabarra/txAdmin/blob/master/docs/development.md#menu-development) 91 | 92 | ## FAQ 93 | - **Q**: Why don't the 'Heal' options revive a player when using ESX/QBCore/etc? 94 | - **A**: Many frameworks independently handle a "dead" state for a player, meaning 95 | the menu is unable to reset this state in an resource agnostic form directly. To establish compatibility 96 | with any framework, txAdmin will emit an [txAdmin:events:healedPlayer](https://github.com/tabarra/txAdmin/blob/master/docs/events.md#txadmineventshealedplayer-v48) 97 | for developers to handle. 98 | -------------------------------------------------------------------------------- /monitor/resource/menu/server/sv_base.lua: -------------------------------------------------------------------------------- 1 | --Check Environment 2 | if GetConvar('txAdminServerMode', 'false') ~= 'true' then 3 | return 4 | end 5 | 6 | 7 | ServerCtxObj = { 8 | oneSync = { 9 | type = nil, 10 | status = false 11 | }, 12 | 13 | projectName = nil, 14 | maxClients = 30, 15 | locale = nil, 16 | localeData = nil, 17 | switchPageKey = '', 18 | txAdminVersion = '', 19 | alignRight = false, 20 | announceNotiPos = '', -- top-center, top-right, top-left, bottom-center, bottom-right, bottom-left 21 | } 22 | 23 | 24 | RegisterCommand('txAdmin-debug', function(src, args) 25 | if src > 0 then 26 | if not PlayerHasTxPermission(src, 'control.server') then 27 | return 28 | end 29 | end 30 | 31 | local playerName = (src > 0) and GetPlayerName(src) or 'Console' 32 | 33 | if not args[1] then 34 | return 35 | end 36 | 37 | if args[1] == '1' then 38 | debugModeEnabled = true 39 | debugPrint("^1!! Debug mode enabled by ^2" .. playerName .. "^1 !!^0") 40 | TriggerClientEvent('txAdmin:events:setDebugMode', -1, true) 41 | elseif args[1] == '0' then 42 | debugPrint("^1!! Debug mode disabled by ^2" .. playerName .. "^1 !!^0") 43 | debugModeEnabled = false 44 | TriggerClientEvent('txAdmin:events:setDebugMode', -1, false) 45 | end 46 | end) 47 | 48 | 49 | local function getCustomLocaleData() 50 | --Get convar 51 | local filePath = GetConvar('txAdmin-localeFile', 'false') 52 | if filePath == 'false' then 53 | return false 54 | end 55 | 56 | -- Get file data 57 | local fileHandle = io.open(filePath, "rb") 58 | if not fileHandle then 59 | print('^1WARNING: failed to load custom locale from path: '..filePath) 60 | return false 61 | end 62 | local fileData = fileHandle:read "*a" 63 | fileHandle:close() 64 | 65 | -- Parse and validate data 66 | local locale = json.decode(fileData) 67 | if 68 | not locale 69 | or type(locale['$meta']) ~= "table" 70 | or type(locale['nui_warning']) ~= "table" 71 | or type(locale['nui_menu']) ~= "table" 72 | then 73 | print('^1WARNING: load or validate custom locale JSON data from path: '..filePath) 74 | return false 75 | end 76 | 77 | -- Build response 78 | debugPrint('^2Loaded custom locale file.') 79 | return { 80 | ['$meta'] = locale['$meta'], 81 | ['nui_warning'] = locale['nui_warning'], 82 | ['nui_menu'] = locale['nui_menu'], 83 | } 84 | end 85 | 86 | local function syncServerCtx() 87 | local oneSyncConvar = GetConvar('onesync', 'off') 88 | if oneSyncConvar == 'on' or oneSyncConvar == 'legacy' then 89 | ServerCtxObj.oneSync.type = oneSyncConvar 90 | ServerCtxObj.oneSync.status = true 91 | elseif oneSyncConvar == 'off' then 92 | ServerCtxObj.oneSync.type = nil 93 | ServerCtxObj.oneSync.status = false 94 | end 95 | 96 | -- Convar must match the event.code *EXACTLY* as shown on this site 97 | -- https://keycode.info/ 98 | local switchPageKey = GetConvar('txAdmin-menuPageKey', 'Tab') 99 | ServerCtxObj.switchPageKey = switchPageKey 100 | 101 | local alignRight = (GetConvar('txAdmin-menuAlignRight', 'false') == 'true') 102 | ServerCtxObj.alignRight = alignRight 103 | 104 | local txAdminVersion = GetConvar('txAdmin-version', '0.0.0') 105 | ServerCtxObj.txAdminVersion = txAdminVersion 106 | 107 | -- Default '' in fxServer 108 | local svProjectName = GetConvar('sv_projectname', '') 109 | if svProjectName ~= '' then 110 | ServerCtxObj.projectName = svProjectName 111 | end 112 | 113 | -- Default 30 in fxServer 114 | local svMaxClients = GetConvarInt('sv_maxclients', 30) 115 | ServerCtxObj.maxClients = svMaxClients 116 | 117 | -- Custom locale 118 | local txAdminLocale = GetConvar('txAdmin-locale', 'en') 119 | ServerCtxObj.locale = txAdminLocale 120 | if txAdminLocale == 'custom' then 121 | ServerCtxObj.localeData = getCustomLocaleData() 122 | else 123 | ServerCtxObj.localeData = false 124 | end 125 | 126 | local announceNotiPos = GetConvar('txAdmin-menuAnnounceNotiPos', 'top-center') 127 | -- verify we have a valid position type 128 | if announceNotiPos == 'top-center' or announceNotiPos == 'top-right' or announceNotiPos == 'top-left' or announceNotiPos == 'bottom-center' or announceNotiPos == 'bottom-right' or announceNotiPos == 'bottom-left' then 129 | ServerCtxObj.announceNotiPos = announceNotiPos 130 | else 131 | local errorMsg = ('^1Invalid notification position: %s, this must match one of the following "top-center, top-left, top-right, bottom-left, bottom-right, bottom-center" defaulting to "top-center"'):format(announceNotiPos) 132 | txPrint(errorMsg) 133 | ServerCtxObj.announceNotiPos = 'top-center' 134 | end 135 | 136 | debugPrint('Updated ServerCtx.') 137 | GlobalState.txAdminServerCtx = ServerCtxObj 138 | 139 | -- Telling admins that the server context changed 140 | for adminID, _ in pairs(TX_ADMINS) do 141 | TriggerClientEvent('txAdmin:events:setServerCtx', adminID, ServerCtxObj) 142 | end 143 | end 144 | 145 | RegisterNetEvent('txAdmin:events:getServerCtx', function() 146 | local src = source 147 | TriggerClientEvent('txAdmin:events:setServerCtx', src, ServerCtxObj) 148 | end) 149 | 150 | -- Everytime the txAdmin convars are changed this event will fire 151 | -- Therefore, lets update global state with that. 152 | AddEventHandler('txAdmin:events:configChanged', function() 153 | debugPrint('configChanged event triggered, syncing GlobalState') 154 | syncServerCtx() 155 | end) 156 | 157 | 158 | CreateThread(function() 159 | -- If we don't wait for a tick there is some race condition that 160 | -- sometimes prevents debugPrint lmao 161 | Wait(0) 162 | syncServerCtx() 163 | end) 164 | -------------------------------------------------------------------------------- /monitor/resource/menu/server/sv_webpipe.lua: -------------------------------------------------------------------------------- 1 | -- ============================================= 2 | -- This file is responsible for all the webpipe 3 | -- handling and caching. 4 | -- ============================================= 5 | -- Checking Environment (sv_main MUST run first) 6 | if GetConvar('txAdminServerMode', 'false') ~= 'true' then 7 | return 8 | end 9 | if TX_LUACOMHOST == "invalid" or TX_LUACOMTOKEN == "invalid" then 10 | log('^1API Host or Pipe Token ConVars not found. Do not start this resource if not using txAdmin.') 11 | return 12 | end 13 | if TX_LUACOMTOKEN == "removed" then 14 | log('^1Please do not restart the monitor resource.') 15 | return 16 | end 17 | 18 | -- 19 | -- [[ WebPipe Proxy ]] 20 | -- 21 | local _pipeLastReject 22 | local _pipeFastCache = {} 23 | 24 | ---@param src string 25 | ---@param callbackId number 26 | ---@param statusCode number 27 | ---@param path string 28 | ---@param body string 29 | ---@param headers table 30 | ---@param cached boolean|nil 31 | local function sendResponse(src, callbackId, statusCode, path, body, headers, cached) 32 | local errorCode = tonumber(statusCode) >= 400 33 | local resultColor = errorCode and '^1' or '^2' 34 | local cachedStr = cached and " ^1(cached)^0" or "" 35 | debugPrint(("^3WebPipe[^5%d^0:^1%d^3]^0 %s<< %s ^4%s%s^0"):format( 36 | src, callbackId, resultColor, statusCode, path, cachedStr)) 37 | if errorCode then 38 | debugPrint(("^3WebPipe[^5%d^0:^1%d^3]^0 %s<< Headers: %s^0"):format( 39 | src, callbackId, resultColor, json.encode(headers))) 40 | end 41 | TriggerLatentClientEvent('txAdmin:WebPipe', src, 125000, callbackId, statusCode, body, headers) 42 | end 43 | 44 | RegisterNetEvent('txAdmin:WebPipe', function(callbackId, method, path, headers, body) 45 | local s = source 46 | local src = tostring(s) 47 | if type(callbackId) ~= 'number' or type(headers) ~= 'table' then 48 | return 49 | end 50 | if type(method) ~= 'string' or type(path) ~= 'string' or type(body) ~= 'string' then 51 | return 52 | end 53 | 54 | -- Reject large paths as we use regex 55 | if #path > 500 then 56 | return sendResponse(s, callbackId, 400, path:sub(1, 300), "{}", {}) 57 | end 58 | 59 | -- Treat path slashes 60 | local url = "http://" .. (TX_LUACOMHOST .. '/' .. path):gsub("//+", "/") 61 | 62 | -- Reject requests from un-authed players 63 | if not TX_ADMINS[src] then 64 | if _pipeLastReject ~= nil then 65 | if (GetGameTimer() - _pipeLastReject) < 250 then 66 | _pipeLastReject = GetGameTimer() 67 | return 68 | end 69 | end 70 | debugPrint(string.format( 71 | "^3WebPipe[^5%d^0:^1%d^3]^0 ^1rejected request from ^3%s^1 for ^5%s^0", s, callbackId, s, path)) 72 | TriggerClientEvent('txAdmin:WebPipe', s, callbackId, 403, "{}", {}) 73 | return 74 | end 75 | 76 | -- Return fast cache 77 | if _pipeFastCache[path] ~= nil then 78 | local cachedData = _pipeFastCache[path] 79 | sendResponse(s, callbackId, 200, path, cachedData.data, cachedData.headers, true) 80 | return 81 | end 82 | 83 | -- Adding auth information for NUI routes 84 | if path:sub(1, 5) == '/nui/' then 85 | headers['X-TxAdmin-Token'] = TX_LUACOMTOKEN 86 | headers['X-TxAdmin-Identifiers'] = table.concat(GetPlayerIdentifiers(s), ', ') 87 | else 88 | headers['X-TxAdmin-Token'] = 'not_required' -- so it's easy to detect webpipes 89 | end 90 | 91 | 92 | debugPrint(("^3WebPipe[^5%d^0:^1%d^3]^0 ^4>>^0 ^6%s^0"):format(s, callbackId, url)) 93 | debugPrint(("^3WebPipe[^5%d^0:^1%d^3]^0 ^4>>^0 ^6Headers: %s^0"):format(s, callbackId, json.encode(headers))) 94 | 95 | PerformHttpRequest(url, function(httpCode, data, resultHeaders) 96 | -- fixing body for error pages (eg 404) 97 | -- this is likely because of how json.encode() interprets null and an empty table 98 | data = data or '' 99 | resultHeaders['x-badcast-fix'] = 'https://youtu.be/LDU_Txk06tM' -- fixed in artifact v3996 100 | 101 | -- fixing redirects 102 | if resultHeaders.Location then 103 | if resultHeaders.Location:sub(1, 1) == '/' then 104 | resultHeaders.Location = '/WebPipe' .. resultHeaders.Location 105 | end 106 | end 107 | 108 | -- fixing cookies 109 | if resultHeaders['Set-Cookie'] then 110 | local cookieHeader = resultHeaders['Set-Cookie'] 111 | local cookies = type(cookieHeader) == 'table' and cookieHeader or { cookieHeader } 112 | 113 | for k in pairs(cookies) do 114 | cookies[k] = cookies[k] .. '; SameSite=None; Secure' 115 | end 116 | 117 | resultHeaders['Set-Cookie'] = cookies 118 | end 119 | 120 | -- cache response if it is a static file 121 | local sub = string.sub 122 | if 123 | httpCode == 200 and 124 | ( 125 | sub(path, 1, 5) == '/css/' or 126 | sub(path, 1, 4) == '/js/' or 127 | sub(path, 1, 5) == '/img/' or 128 | sub(path, 1, 7) == '/fonts/' 129 | ) 130 | then 131 | -- remove query params from path, so people can't consume memory by spamming cache-busters 132 | for safePath in path:gmatch("([^?]+)") do 133 | local slimHeaders = {} 134 | for k, v in pairs(resultHeaders) do 135 | if k ~= 'Set-Cookie' then 136 | slimHeaders[k] = v 137 | end 138 | end 139 | _pipeFastCache[safePath] = { data = data, headers = slimHeaders } 140 | debugPrint(("^3WebPipe[^5%d^0:^1%d^3]^0 ^5cached ^4%s^0"):format(s, callbackId, safePath)) 141 | break 142 | end 143 | end 144 | 145 | sendResponse(s, callbackId, httpCode, path, data, resultHeaders) 146 | end, method, body, headers, {followLocation = false}) 147 | end) 148 | -------------------------------------------------------------------------------- /monitor/resource/sv_playerlist.lua: -------------------------------------------------------------------------------- 1 | -- ============================================= 2 | -- Server PlayerList handler 3 | -- ============================================= 4 | --Check Environment 5 | if GetConvar('txAdminServerMode', 'false') ~= 'true' then 6 | return 7 | end 8 | local oneSyncConvar = GetConvar('onesync', 'off') 9 | local onesyncEnabled = oneSyncConvar == 'on' or oneSyncConvar == 'legacy' 10 | 11 | 12 | -- Optimizations 13 | local floor = math.floor 14 | local max = math.max 15 | local min = math.min 16 | local sub = string.sub 17 | local tonumber = tonumber 18 | local tostring = tostring 19 | local pairs = pairs 20 | 21 | 22 | -- Variables & Consts 23 | local refreshMinDelay = 1500 24 | local refreshMaxDelay = 5000 25 | local maxPlayersDelayCeil = 300 --at this number, the delay won't increase more 26 | local intervalYieldLimit = 50 27 | local vTypeMap = { 28 | ["nil"] = -1, 29 | ["walking"] = 0, 30 | ["automobile"] = 1, 31 | ["bike"] = 2, 32 | ["boat"] = 3, 33 | ["heli"] = 4, 34 | ["plane"] = 5, 35 | ["submarine"] = 6, 36 | ["trailer"] = 7, 37 | ["train"] = 8, 38 | } 39 | 40 | 41 | --[[ Refresh player list data ]] 42 | CreateThread(function() 43 | while true do 44 | -- For each player 45 | local players = GetPlayers() 46 | for yieldCounter, serverID in pairs(players) do 47 | -- Updating player vehicle/health 48 | -- NOTE: after testing this seem not to need any error handling 49 | local health = 0 50 | local vType = -1 51 | if onesyncEnabled == true then 52 | local ped = GetPlayerPed(serverID) 53 | local veh = GetVehiclePedIsIn(ped) 54 | if veh ~= 0 then 55 | vType = vTypeMap[tostring(GetVehicleType(veh))] 56 | else 57 | vType = vTypeMap["walking"] 58 | end 59 | -- Its extremely hard to normalize this value to actually reflect 60 | -- it as a percentage of the current users max health depending on the server 61 | -- Therefore, lets just handle for base case of maxHealth 175 and health range from 100-175 62 | health = floor((GetEntityHealth(ped) - 100) / (GetEntityMaxHealth(ped) - 100) * 100) 63 | end 64 | 65 | -- Updating TX_PLAYERLIST 66 | if type(TX_PLAYERLIST[serverID]) ~= 'table' then 67 | TX_PLAYERLIST[serverID] = { 68 | name = sub(GetPlayerName(serverID) or "unknown", 1, 75), 69 | health = health, 70 | vType = vType, 71 | } 72 | else 73 | TX_PLAYERLIST[serverID].health = health 74 | TX_PLAYERLIST[serverID].vType = vType 75 | end 76 | 77 | -- Mark as refreshed 78 | TX_PLAYERLIST[serverID].foundLastCheck = true 79 | 80 | -- Yield to prevent hitches 81 | if yieldCounter % intervalYieldLimit == 0 then 82 | Wait(0) 83 | end 84 | end --end for players 85 | 86 | 87 | --Check if player disconnected 88 | for playerID, playerData in pairs(TX_PLAYERLIST) do 89 | if playerData.foundLastCheck == true then 90 | playerData.foundLastCheck = false 91 | else 92 | TX_PLAYERLIST[playerID] = nil 93 | end 94 | end 95 | 96 | -- DEBUG 97 | -- debugPrint("====================================") 98 | -- print(json.encode(TX_PLAYERLIST, {indent = true})) 99 | -- debugPrint("====================================") 100 | 101 | -- Refresh interval with linear function 102 | local hDiff = refreshMaxDelay - refreshMinDelay 103 | local calcDelay = (hDiff/maxPlayersDelayCeil) * (#players) + refreshMinDelay 104 | local delay = floor(min(calcDelay, refreshMaxDelay)) 105 | Wait(delay) 106 | end --end while true 107 | end) 108 | 109 | 110 | --[[ Handle player Join or Leave ]] 111 | AddEventHandler('playerJoining', function() 112 | local playerName = sub(GetPlayerName(source) or "unknown", 1, 75) 113 | for adminID, _ in pairs(TX_ADMINS) do 114 | TriggerClientEvent('txcl:updatePlayer', adminID, source, playerName) 115 | end 116 | end) 117 | AddEventHandler('playerDropped', function() 118 | for adminID, _ in pairs(TX_ADMINS) do 119 | TriggerClientEvent('txcl:updatePlayer', adminID, source, false) 120 | end 121 | end) 122 | 123 | 124 | -- Handle getDetailedPlayerlist 125 | -- This event is only called when the menu "players" tab is opened, and every 5s while the tab is open 126 | RegisterNetEvent('txsv:getDetailedPlayerlist', function() 127 | if TX_ADMINS[tostring(source)] == nil then 128 | debugPrint('Ignoring unauthenticated getDetailedPlayerlist() by ' .. source) 129 | return 130 | end 131 | 132 | local players = {} 133 | for playerID, playerData in pairs(TX_PLAYERLIST) do 134 | players[#players + 1] = {tonumber(playerID), playerData.health, playerData.vType} 135 | end 136 | local admins = {} 137 | for adminID, _ in pairs(TX_ADMINS) do 138 | admins[#admins + 1] = tonumber(adminID) 139 | end 140 | TriggerClientEvent('txcl:setDetailedPlayerlist', source, players, admins) 141 | end) 142 | 143 | 144 | -- Sends the initial playlist to a specific admin 145 | -- Triggered by the server after admin auth 146 | function sendInitialPlayerlist(adminID) 147 | local payload = {} 148 | for playerID, playerData in pairs(TX_PLAYERLIST) do 149 | payload[#payload + 1] = {tonumber(playerID), playerData.name} 150 | end 151 | debugPrint('Sending initial playerlist to ' .. adminID) 152 | TriggerClientEvent('txcl:setInitialPlayerlist', adminID, payload) 153 | end 154 | -------------------------------------------------------------------------------- /monitor/resource/menu/client/cl_player_mode.lua: -------------------------------------------------------------------------------- 1 | -- =============== 2 | -- This file contains functionality purely related 3 | -- to player modes (noclip, godmode) 4 | -- =============== 5 | if (GetConvar('txAdmin-menuEnabled', 'false') ~= 'true') then 6 | return 7 | end 8 | 9 | local noClipEnabled = false 10 | 11 | local function toggleGodMode(enabled) 12 | if enabled then 13 | sendPersistentAlert('godModeEnabled', 'info', 'nui_menu.page_main.player_mode.godmode.success', true) 14 | else 15 | clearPersistentAlert('godModeEnabled') 16 | end 17 | SetEntityInvincible(PlayerPedId(), enabled) 18 | end 19 | 20 | local freecamVeh = 0 21 | local function toggleFreecam(enabled) 22 | noClipEnabled = enabled 23 | local ped = PlayerPedId() 24 | SetEntityVisible(ped, not enabled) 25 | SetEntityInvincible(ped, enabled) 26 | FreezeEntityPosition(ped, enabled) 27 | 28 | if enabled then 29 | freecamVeh = GetVehiclePedIsIn(ped, false) 30 | if freecamVeh > 0 then 31 | NetworkSetEntityInvisibleToNetwork(freecamVeh, true) 32 | SetEntityCollision(freecamVeh, false, false) 33 | end 34 | end 35 | 36 | local function enableNoClip() 37 | lastTpCoords = GetEntityCoords(ped) 38 | 39 | SetFreecamActive(true) 40 | StartFreecamThread() 41 | 42 | Citizen.CreateThread(function() 43 | while IsFreecamActive() do 44 | SetEntityLocallyInvisible(ped) 45 | if freecamVeh > 0 then 46 | if DoesEntityExist(freecamVeh) then 47 | SetEntityLocallyInvisible(freecamVeh) 48 | else 49 | freecamVeh = 0 50 | end 51 | end 52 | Wait(0) 53 | end 54 | 55 | if not DoesEntityExist(freecamVeh) then 56 | freecamVeh = 0 57 | end 58 | if freecamVeh > 0 then 59 | local coords = GetEntityCoords(ped) 60 | NetworkSetEntityInvisibleToNetwork(freecamVeh, false) 61 | SetEntityCollision(freecamVeh, true, true) 62 | SetEntityCoords(freecamVeh, coords[1], coords[2], coords[3]) 63 | SetPedIntoVehicle(ped, freecamVeh, -1) 64 | freecamVeh = 0 65 | end 66 | end) 67 | end 68 | 69 | local function disableNoClip() 70 | SetFreecamActive(false) 71 | SetGameplayCamRelativeHeading(0) 72 | end 73 | 74 | if not IsFreecamActive() and enabled then 75 | sendPersistentAlert('noClipEnabled', 'info', 'nui_menu.page_main.player_mode.noclip.success', true) 76 | enableNoClip() 77 | end 78 | 79 | if IsFreecamActive() and not enabled then 80 | clearPersistentAlert('noClipEnabled') 81 | disableNoClip() 82 | end 83 | end 84 | 85 | 86 | local PTFX_ASSET = 'ent_dst_elec_fire_sp' 87 | local PTFX_DICT = 'core' 88 | local LOOP_AMOUNT = 25 89 | local PTFX_DURATION = 1000 90 | 91 | -- Applies the particle effect to a ped 92 | local function createPlayerModePtfxLoop(tgtPedId) 93 | CreateThread(function() 94 | if tgtPedId <= 0 or tgtPedId == nil then return end 95 | RequestNamedPtfxAsset(PTFX_DICT) 96 | 97 | -- Wait until it's done loading. 98 | while not HasNamedPtfxAssetLoaded(PTFX_DICT) do 99 | Wait(0) 100 | end 101 | 102 | local particleTbl = {} 103 | 104 | for i=0, LOOP_AMOUNT do 105 | UseParticleFxAssetNextCall(PTFX_DICT) 106 | local partiResult = StartParticleFxLoopedOnEntity(PTFX_ASSET, tgtPedId, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.5, false, false, false) 107 | particleTbl[#particleTbl + 1] = partiResult 108 | Wait(0) 109 | end 110 | 111 | Wait(PTFX_DURATION) 112 | for _, parti in ipairs(particleTbl) do 113 | StopParticleFxLooped(parti, true) 114 | end 115 | end) 116 | end 117 | 118 | RegisterNetEvent('txcl:syncPtfxEffect', function(tgtSrc) 119 | debugPrint('Syncing particle effect for target netId') 120 | local tgtPlayer = GetPlayerFromServerId(tgtSrc) 121 | if tgtPlayer == -1 then return end 122 | createPlayerModePtfxLoop(GetPlayerPed(tgtPlayer)) 123 | end) 124 | 125 | -- Ask server for playermode change and sends nearby playerlist 126 | local function askChangePlayerMode(mode) 127 | debugPrint("Requesting player mode change to " .. mode) 128 | 129 | -- get nearby players to receive the ptfx sync event 130 | local players = GetActivePlayers() 131 | local nearbyPlayers = {} 132 | for _, player in ipairs(players) do 133 | nearbyPlayers[#nearbyPlayers + 1] = GetPlayerServerId(player) 134 | end 135 | 136 | TriggerServerEvent('txAdmin:menu:playerModeChanged', mode, nearbyPlayers) 137 | end 138 | 139 | -- NoClip toggle keybind 140 | RegisterCommand('txAdmin:menu:noClipToggle', function() 141 | if not menuIsAccessible then return end 142 | if not DoesPlayerHavePerm(menuPermissions, 'players.playermode') then 143 | return sendSnackbarMessage('error', 'nui_menu.misc.no_perms', true) 144 | end 145 | askChangePlayerMode(noClipEnabled and 'none' or 'noclip') 146 | end) 147 | 148 | -- Menu callback to change the player mode 149 | RegisterNUICallback('playerModeChanged', function(mode, cb) 150 | askChangePlayerMode(mode) 151 | cb({}) 152 | end) 153 | 154 | -- [[ Player mode changed cb event ]] 155 | RegisterNetEvent('txAdmin:menu:playerModeChanged', function(mode, ptfx) 156 | if ptfx then 157 | createPlayerModePtfxLoop(PlayerPedId()) 158 | end 159 | 160 | if mode == 'godmode' then 161 | toggleFreecam(false) 162 | toggleGodMode(true) 163 | elseif mode == 'noclip' then 164 | toggleGodMode(false) 165 | toggleFreecam(true) 166 | elseif mode == 'none' then 167 | toggleFreecam(false) 168 | toggleGodMode(false) 169 | end 170 | end) 171 | 172 | -------------------------------------------------------------------------------- /monitor/resource/menu/client/cl_base.lua: -------------------------------------------------------------------------------- 1 | -- ============================================= 2 | -- This file is for base menu functionality (admin status, 3 | -- visibility, keybinds, focus callbacks's, threads, etc) 4 | -- ============================================= 5 | 6 | -- Global Variables 7 | -- TODO: they should be upper case 8 | menuIsAccessible = false 9 | isMenuDebug = false 10 | isMenuVisible = false 11 | menuPermissions = {} 12 | lastTpCoords = false; 13 | local isMenuEnabled = (GetConvar('txAdmin-menuEnabled', 'false') == 'true') 14 | 15 | 16 | -- Check if menu is in debug mode 17 | CreateThread(function() 18 | isMenuDebug = (GetConvar('txAdmin-menuDebug', 'false') == 'true') 19 | end) 20 | 21 | local function checkMenuAccessible() 22 | if not isMenuEnabled then 23 | sendSnackbarMessage('error', 'nui_menu.misc.not_enabled', true) 24 | return false 25 | end 26 | if not menuIsAccessible then 27 | sendSnackbarMessage('error', 'nui_menu.misc.menu_not_allowed', true) 28 | return false 29 | end 30 | 31 | return true 32 | end 33 | 34 | 35 | -- Register txAdmin command 36 | local function txadmin(_, args) 37 | if not checkMenuAccessible() then return end 38 | 39 | -- Make visible 40 | toggleMenuVisibility() 41 | 42 | -- Shortcut to open a specific players profile 43 | if isMenuVisible and #args >= 1 then 44 | local targetPlayer = table.concat(args, ' ') 45 | sendMenuMessage('openPlayerModal', targetPlayer) 46 | end 47 | end 48 | RegisterCommand('txadmin', txadmin) 49 | RegisterCommand('tx', txadmin) 50 | 51 | RegisterCommand('txAdmin:menu:openPlayersPage', function() 52 | if not checkMenuAccessible() then return end 53 | sendMenuMessage('setMenuPage', 1) 54 | toggleMenuVisibility(true) 55 | SetNuiFocus(true, true) 56 | end) 57 | 58 | 59 | 60 | -- ============================================= 61 | -- The rest of the file will only run if menu is enabled 62 | -- ============================================= 63 | if not isMenuEnabled then 64 | return 65 | end 66 | 67 | -- Checking with server if we are an admin 68 | TriggerServerEvent('txsv:checkAdminStatus') 69 | 70 | -- Triggered as callback of txsv:checkAdminStatus 71 | RegisterNetEvent('txcl:setAdmin', function(username, perms, rejectReason) 72 | if type(perms) == 'table' then 73 | print("^2[AUTH] logged in as '"..username.."' with perms: " .. json.encode(perms or "nil")) 74 | menuIsAccessible = true 75 | menuPermissions = perms 76 | RegisterKeyMapping('txadmin', 'Menu: Open Main Page', 'keyboard', '') 77 | RegisterKeyMapping('txAdmin:menu:openPlayersPage', 'Menu: Open Players page', 'KEYBOARD', '') 78 | RegisterKeyMapping('txAdmin:menu:noClipToggle', 'Menu: Toggle NoClip', 'keyboard', '') 79 | RegisterKeyMapping('txAdmin:menu:togglePlayerIDs', 'Menu: Toggle Player IDs', 'KEYBOARD', '') 80 | RegisterKeyMapping('txAdmin:menu:endSpectate', 'Menu: Exit spectate mode', 'keyboard', 'BACK') 81 | else 82 | print("^3[AUTH] rejected (" .. tostring(rejectReason) ..")") 83 | menuIsAccessible = false 84 | menuPermissions = {} 85 | end 86 | sendMenuMessage('setPermissions', menuPermissions) 87 | end) 88 | 89 | 90 | --[[ Debug Events / Commands ]] 91 | -- Command/event to trigger a authentication attempt 92 | local function retryAuthentication() 93 | print("^5[AUTH] Retrying menu authentication.") 94 | menuIsAccessible = false 95 | menuPermissions = {} 96 | sendMenuMessage('resetSession') 97 | sendMenuMessage('setPermissions', menuPermissions) 98 | TriggerServerEvent('txsv:checkAdminStatus') 99 | end 100 | RegisterCommand('txAdmin-reauth', retryAuthentication) 101 | RegisterNetEvent('txAdmin:menu:reAuth', retryAuthentication) 102 | 103 | 104 | -- Register chat suggestions 105 | -- txAdmin starts before the chat resource, so we need to wait a bit 106 | CreateThread(function() 107 | Wait(1000) 108 | TriggerEvent( 109 | 'chat:addSuggestion', 110 | '/tx', 111 | 'Opens the main txAdmin Menu or specific for a player.', 112 | {{ name="player ID/name", help="(Optional) Open player modal for specific ID or name." }} 113 | ) 114 | TriggerEvent( 115 | 'chat:addSuggestion', 116 | '/txAdmin-reauth', 117 | 'Retries to authenticate the menu NUI.' 118 | ) 119 | TriggerEvent( 120 | 'chat:addSuggestion', 121 | '/txAdmin-debug', -- on /scripts/menu/server/sv_base.lua 122 | 'Enables or disables the debug mode. Requires \'control.server\' permission.', 123 | {{ name="1|0", help="1 to enable, 0 to disable" }} 124 | ) 125 | end) 126 | 127 | 128 | -- Will toggle debug logging 129 | RegisterNetEvent('txAdmin:events:setDebugMode', function(enabled) 130 | isMenuDebug = enabled 131 | debugModeEnabled = enabled 132 | sendMenuMessage('setDebugMode', isMenuDebug) 133 | end) 134 | 135 | 136 | --[[ NUI Callbacks ]] 137 | -- Triggered whenever we require full focus, cursor and keyboard 138 | RegisterNUICallback('focusInputs', function(shouldFocus, cb) 139 | debugPrint('NUI Focus + Keep Input ' .. tostring(shouldFocus)) 140 | -- Will prevent mouse focus on initial menu mount as the useEffect emits there 141 | if not isMenuVisible then 142 | return 143 | end 144 | SetNuiFocus(true, shouldFocus) 145 | SetNuiFocusKeepInput(not shouldFocus) 146 | cb({}) 147 | end) 148 | 149 | 150 | RegisterNUICallback('reactLoaded', function(aaa, cb) 151 | print("React loaded, sending variables.") 152 | sendMenuMessage('setDebugMode', isMenuDebug) 153 | sendMenuMessage('setPermissions', menuPermissions) 154 | 155 | CreateThread(function() 156 | updateServerCtx() 157 | while ServerCtx == false do Wait(0) end 158 | sendMenuMessage('setServerCtx', ServerCtx) 159 | end) 160 | 161 | cb({}) 162 | end) 163 | 164 | -- When the escape key is pressed in menu 165 | RegisterNUICallback('closeMenu', function(_, cb) 166 | isMenuVisible = false 167 | debugPrint('Releasing all NUI Focus') 168 | SetNuiFocus(false) 169 | SetNuiFocusKeepInput(false) 170 | cb({}) 171 | end) 172 | 173 | 174 | --[[ Threads ]] 175 | CreateThread(function() 176 | while true do 177 | if isMenuVisible and IsPauseMenuActive() then 178 | toggleMenuVisibility() 179 | end 180 | Wait(250) 181 | end 182 | end) 183 | -------------------------------------------------------------------------------- /monitor/resource/cl_playerlist.lua: -------------------------------------------------------------------------------- 1 | -- ============================================= 2 | -- Client PlayerList handler 3 | -- ============================================= 4 | if (GetConvar('txAdmin-menuEnabled', 'false') ~= 'true') then 5 | return 6 | end 7 | 8 | -- Optimizations 9 | local tonumber = tonumber 10 | local tostring = tostring 11 | local floor = math.floor 12 | 13 | -- Variables & Consts 14 | LOCAL_PLAYERLIST = {} -- available globally in tx 15 | local vTypeMap = { 16 | ["0"] = "walking", 17 | ["1"] = "driving", --automobile 18 | ["2"] = "biking", 19 | ["3"] = "boating", 20 | ["4"] = "flying", --heli 21 | ["5"] = "flying", --plane 22 | ["6"] = "boating", --submarine 23 | ["7"] = "driving", --trailer 24 | ["8"] = "driving", --train 25 | } 26 | 27 | 28 | -- Transforms the playerlist and sends to react 29 | -- The playerlist is converted to an object array to save refactor time 30 | function sendReactPlayerlist() 31 | local upload = {} 32 | for pids, playerData in pairs(LOCAL_PLAYERLIST) do 33 | upload[#upload + 1] = { 34 | id = tonumber(pids), 35 | name = playerData.name, 36 | health = playerData.health, 37 | dist = playerData.dist, 38 | vType = playerData.vType, 39 | admin = playerData.admin 40 | } 41 | end 42 | -- print("========== function sendReactPlayerlist()") 43 | -- print(json.encode(upload)) 44 | -- print("------------------------------------") 45 | sendMenuMessage('setPlayerList', upload) 46 | end 47 | 48 | 49 | -- Triggered when the admin authenticates 50 | -- Replaces current playerlist 51 | RegisterNetEvent('txcl:setInitialPlayerlist', function(payload) 52 | -- print("========== EVENT setInitialPlayerlist") 53 | -- print(json.encode(payload)) -- [[id, name]] 54 | -- print("------------------------------------") 55 | LOCAL_PLAYERLIST = {} 56 | for _, playerData in pairs(payload) do 57 | local pids = tostring(playerData[1]) 58 | LOCAL_PLAYERLIST[pids] = { 59 | name = playerData[2], 60 | health = 0, 61 | dist = -1, 62 | vType = "unknown", 63 | admin = false 64 | } 65 | end 66 | -- print("------------------------------------") 67 | -- print(json.encode(LOCAL_PLAYERLIST, {indent = true})) 68 | -- print("------------------------------------") 69 | sendReactPlayerlist() 70 | end) 71 | 72 | 73 | -- Triggered on the return of "getDetailedPlayerlist" 74 | -- > run through inbound playerlist updating existing data 75 | -- > try to get the dist from all players (susceptible to area culling, but that's fine) 76 | -- > TODO: decide what to do in case of missing or extra ids (missed updatePlayer?) 77 | RegisterNetEvent('txcl:setDetailedPlayerlist', function(players, admins) 78 | -- print("========== EVENT setDetailedPlayerlist") 79 | -- print(json.encode(players)) -- [[id, health, vType]] 80 | -- print("------------------------------------") 81 | local myID = GetPlayerServerId(PlayerId()) 82 | local myCoords = GetEntityCoords(PlayerPedId()) 83 | 84 | for _, playerData in pairs(players) do 85 | local pid = playerData[1] 86 | local pids = tostring(playerData[1]) 87 | local admin = LOCAL_PLAYERLIST[pids] 88 | -- Set inbound data 89 | if admin == nil then 90 | debugPrint("Playerlist: received detailed info for player "..pids.." not present in local playerlist") 91 | LOCAL_PLAYERLIST[pids] = { 92 | name = "unknown", 93 | health = playerData[2], 94 | vType = vTypeMap[tostring(playerData[3])] or "unknown", 95 | admin = false 96 | } 97 | else 98 | LOCAL_PLAYERLIST[pids].health = playerData[2] 99 | LOCAL_PLAYERLIST[pids].vType = vTypeMap[tostring(playerData[3])] or "unknown" 100 | end 101 | 102 | --Getting distance 103 | if pid == myID then 104 | LOCAL_PLAYERLIST[pids].dist = 0 105 | else 106 | local remotePlayer = GetPlayerFromServerId(pid) 107 | if remotePlayer == -1 then 108 | LOCAL_PLAYERLIST[pids].dist = -1 109 | else 110 | local remotePed = GetPlayerPed(remotePlayer) 111 | local remoteCoords = GetEntityCoords(remotePed) 112 | LOCAL_PLAYERLIST[pids].dist = floor(#(myCoords - remoteCoords)) 113 | end 114 | end 115 | end 116 | 117 | -- Mark admins 118 | for _, adminID in pairs(admins) do 119 | local id = tostring(adminID) 120 | if LOCAL_PLAYERLIST[id] ~= nil then 121 | LOCAL_PLAYERLIST[id].admin = true 122 | end 123 | end 124 | 125 | -- print("------------------------------------") 126 | -- print(json.encode(LOCAL_PLAYERLIST, {indent = true})) 127 | -- print("------------------------------------") 128 | sendReactPlayerlist() 129 | end) 130 | 131 | 132 | -- Triggered on player join/leave 133 | -- add/remove specific id to playerlist 134 | RegisterNetEvent('txcl:updatePlayer', function(id, data) 135 | local pids = tostring(id) 136 | if data == false then 137 | debugPrint("^2txcl:updatePlayer: ^3"..id.."^2 disconnected") 138 | LOCAL_PLAYERLIST[pids] = nil 139 | else 140 | debugPrint("^2txcl:updatePlayer: ^3"..id.."^2 connected") 141 | LOCAL_PLAYERLIST[pids] = { 142 | name = data, 143 | health = 0, 144 | dist = -1, 145 | vType = "unknown", 146 | admin = false 147 | } 148 | end 149 | sendReactPlayerlist() 150 | end) 151 | 152 | 153 | -- Triggered when the "player" tab opens in the menu, and every 5s after that 154 | RegisterNUICallback('signalPlayersPageOpen', function(_, cb) 155 | TriggerServerEvent("txsv:getDetailedPlayerlist") --request latest from server 156 | cb({}) 157 | end) 158 | 159 | 160 | -- DEBUG only 161 | -- RegisterCommand('tnew', function() 162 | -- TriggerServerEvent("txsv:getDetailedPlayerlist") 163 | -- end) 164 | -- RegisterCommand('tprint', function() 165 | -- print("------------------------------------") 166 | -- print(json.encode(LOCAL_PLAYERLIST, {indent = true})) 167 | -- print("------------------------------------") 168 | -- end) 169 | -------------------------------------------------------------------------------- /monitor/web/main/advanced.ejs: -------------------------------------------------------------------------------- 1 | <%- await include('parts/header.ejs', locals) %> 2 | 3 |
4 |
5 | 13 |
14 |
15 | 16 | 17 |
18 | 19 |
20 |
21 |
Random buttons, knobs and data:
22 |
23 | 24 | 25 | With verbosity enabled, you will see more detailed information on the terminal.
26 | Good to help getting information on errors.
27 |
28 | <% if (verbosityEnabled) { %> 29 | 32 | <% } else { %> 33 | 36 | <% } %> 37 |
38 | 39 | 40 | 41 | This will execute the profiler in the Monitor for 5 seconds.
42 | Required the Server to be started for showing the profiler URL.
43 |
44 | 45 |
46 |
47 |
48 | 49 |
50 |
51 |
52 | 53 |
54 | 55 |
56 |
What will happen when its pressed?!
57 |
58 |
59 | 60 | 61 |
62 | 63 | Not A Meme 64 | 65 |
66 | 67 | <%- await include('parts/footer.ejs', locals) %> 68 | 69 | 155 | -------------------------------------------------------------------------------- /monitor/web/main/diagnostics.ejs: -------------------------------------------------------------------------------- 1 | <%- await include('parts/header.ejs', locals) %> 2 | 3 | 4 |
5 |
6 | 7 | 8 |
9 |
txAdmin Info:
10 |
11 |
Stats:
12 | Versions: 13 | v<%= txAdminVersion %> / <%= fxServerVersion %>
14 | Uptime: <%= txadmin.uptime %>
15 | Cfx.re URL: 16 | <%= txadmin.cfxUrl %>
17 | Ban/Whitelist Checking: 18 | <%= txadmin.banlistEnabled %>, 19 | <%= txadmin.whitelistEnabled %>
20 | HTTP Counter: 21 | <%= txadmin.httpCounterLog %> < <%= txadmin.httpCounterMax %>
22 | Monitor Restarts: 23 | CL <%= txadmin.monitorRestarts.close %> / 24 | HB <%= txadmin.monitorRestarts.heartBeat %> / 25 | HC <%= txadmin.monitorRestarts.healthCheck %>
26 | HB Fails: 27 | HTTP <%= txadmin.hbHTTPFails %> / FD3 <%= txadmin.hbFD3Fails %>
28 | Boot Seconds: <%= txadmin.hbBootSeconds %>
29 | Freeze Seconds: <%= txadmin.freezeSeconds %>
30 | Koa Sessions: <%= txadmin.koaSessions %>
31 | Logs Storage Size: <%= txadmin.logStorageSize %>
32 | Logger Status:
33 | ├─ Admin: <%= txadmin.loggerStatusAdmin %>
34 | ├─ FXServer: <%= txadmin.loggerStatusFXServer %>
35 | └─ Server: <%= txadmin.loggerStatusServer %>
36 |
37 |
Settings:
38 | Schedule: <%= txadmin.schedule %>
39 | Cooldown: <%= txadmin.cooldown %>
40 | FXServer Path: <%= txadmin.fxServerPath %>
41 | Server Data Path: <%= txadmin.serverDataPath %>
42 | CFG Path: <%= txadmin.cfgPath %>
43 | Connect Endpoint: <%= txadmin.fxServerHost %>
44 | Additional Arguments: <%= txadmin.commandLine %> 45 |
46 |
47 | 48 |
49 | 50 | 51 | 52 |
53 | 54 | 55 |
56 |
Environment:
57 |
58 | <% if (host.error !== false) { %> 59 | <%- host.error %> 60 | <% } else { %> 61 | Node: <%= host.nodeVersion %>
62 | OS: <%= host.osDistro %>
63 | Username: <%= host.username %>
64 | Host CPUs: <%= host.cpus %> <%- host.clockWarning %>
65 | Host Memory: <%= host.memory %> 66 | <% } %> 67 |
68 |
69 | 70 | 71 |
72 |
FXServer /info.json:
73 |
74 | <% if (fxserver.versionMismatch) { %> 75 | 80 | <% } %> 81 | <% if (fxserver.error !== false) { %> 82 | <%- fxserver.error %> 83 | <% } else { %> 84 | Status: <%= fxserver.status %>
85 | Version: <%= fxserver.version %>
86 | Resources: <%= fxserver.resources %>
87 | OneSync: <%= fxserver.onesync %>
88 | Max Clients: <%= fxserver.maxClients %>
89 | txAdmin Version: <%= fxserver.txAdminVersion %>
90 | <% } %> 91 |
92 |
93 | 94 | 95 |
96 |
Processes:
97 |
98 | <% if (!proccesses.length) { %> 99 | Failed to retrieve processed data.
100 | Check the terminal for more information (if verbosity is enabled) 101 | <% } else { %> 102 | <% for (const process of proccesses) { %> 103 | Process: (<%= process.pid %>) <%= process.name %>
104 | Parent: <%= process.ppid %>
105 | Memory: <%= process.memory %>
106 | CPU: <%= process.cpu %>
107 |
108 | <% } %> 109 | <% } %> 110 |
111 |
112 | 113 | 114 |
115 | <%- message %> 116 |
117 | 118 |
119 | 120 |
121 | 122 | <%- await include('parts/footer.ejs', locals) %> 123 | -------------------------------------------------------------------------------- /monitor/resource/menu/vendor/freecam/main.lua: -------------------------------------------------------------------------------- 1 | local Wait = Citizen.Wait 2 | local vector3 = vector3 3 | local IsPauseMenuActive = IsPauseMenuActive 4 | local GetSmartControlNormal = GetSmartControlNormal 5 | 6 | local SETTINGS = _G.CONTROL_SETTINGS 7 | local CONTROLS = _G.CONTROL_MAPPING 8 | 9 | ------------------------------------------------------------------------------- 10 | local function GetSpeedMultiplier() 11 | local fastNormal = GetSmartControlNormal(CONTROLS.MOVE_FAST) 12 | local slowNormal = GetSmartControlNormal(CONTROLS.MOVE_SLOW) 13 | 14 | local baseSpeed = SETTINGS.BASE_MOVE_MULTIPLIER 15 | local fastSpeed = 1 + ((SETTINGS.FAST_MOVE_MULTIPLIER - 1) * fastNormal) 16 | local slowSpeed = 1 + ((SETTINGS.SLOW_MOVE_MULTIPLIER - 1) * slowNormal) 17 | 18 | local frameMultiplier = GetFrameTime() * 60 19 | local speedMultiplier = baseSpeed * fastSpeed / slowSpeed 20 | 21 | return speedMultiplier * frameMultiplier 22 | end 23 | 24 | local function UpdateCamera() 25 | if not IsFreecamActive() or IsPauseMenuActive() then 26 | return 27 | end 28 | 29 | if not IsFreecamFrozen() then 30 | local vecX, vecY = GetFreecamMatrix() 31 | local vecZ = vector3(0, 0, 1) 32 | 33 | local pos = GetFreecamPosition() 34 | local rot = GetFreecamRotation() 35 | 36 | -- Get speed multiplier for movement 37 | local speedMultiplier = GetSpeedMultiplier() 38 | 39 | -- Get rotation input 40 | local lookX = GetSmartControlNormal(CONTROLS.LOOK_X) 41 | local lookY = GetSmartControlNormal(CONTROLS.LOOK_Y) 42 | 43 | -- Get position input 44 | local moveX = GetSmartControlNormal(CONTROLS.MOVE_X) 45 | local moveY = GetSmartControlNormal(CONTROLS.MOVE_Y) 46 | local moveZ = GetSmartControlNormal(CONTROLS.MOVE_Z) 47 | 48 | -- Calculate new rotation. 49 | local rotX = rot.x + (-lookY * SETTINGS.LOOK_SENSITIVITY_X) 50 | local rotZ = rot.z + (-lookX * SETTINGS.LOOK_SENSITIVITY_Y) 51 | local rotY = rot.y 52 | 53 | -- Adjust position relative to camera rotation. 54 | pos = pos + (vecX * moveX * speedMultiplier) 55 | pos = pos + (vecY * -moveY * speedMultiplier) 56 | pos = pos + (vecZ * moveZ * speedMultiplier) 57 | 58 | -- Adjust new rotation 59 | rot = vector3(rotX, rotY, rotZ) 60 | 61 | -- Update camera 62 | SetFreecamPosition(pos.x, pos.y, pos.z) 63 | SetFreecamRotation(rot.x, rot.y, rot.z) 64 | 65 | return pos, rotZ 66 | end 67 | 68 | -- Trigger a tick event. Resources depending on the freecam position can 69 | -- make use of this event. 70 | -- TriggerEvent('freecam:onTick') 71 | end 72 | 73 | ------------------------------------------------------------------------------- 74 | function StartFreecamThread() 75 | -- Camera/Pos updating thread 76 | Citizen.CreateThread(function () 77 | local ped = PlayerPedId() 78 | local initialPos = GetEntityCoords(ped) 79 | SetFreecamPosition(initialPos[1], initialPos[2], initialPos[3]) 80 | 81 | local function updatePos(pos, rotZ) 82 | if pos ~= nil and rotZ ~= nil then 83 | -- Update ped 84 | SetEntityCoords(ped, pos.x, pos.y, pos.z) 85 | SetEntityHeading(ped, rotZ) 86 | -- Update veh 87 | local veh = GetVehiclePedIsIn(ped, false) 88 | if veh and veh > 0 then 89 | SetEntityCoords(veh, pos.x, pos.y, pos.z) 90 | end 91 | end 92 | end 93 | 94 | local frameCounter = 0 95 | local loopPos, loopRotZ 96 | while IsFreecamActive() do 97 | loopPos, loopRotZ = UpdateCamera() 98 | frameCounter = frameCounter + 1 99 | if frameCounter > 100 then 100 | frameCounter = 0 101 | updatePos(loopPos, loopRotZ) 102 | end 103 | Wait(0) 104 | end 105 | 106 | -- One last time due to the optimization 107 | updatePos(loopPos, loopRotZ) 108 | end) 109 | 110 | local function InstructionalButton(controlButton, text) 111 | ScaleformMovieMethodAddParamPlayerNameString(controlButton) 112 | BeginTextCommandScaleformString("STRING") 113 | AddTextComponentScaleform(text) 114 | EndTextCommandScaleformString() 115 | end 116 | 117 | --Scaleform drawing thread 118 | Citizen.CreateThread(function() 119 | local scaleform = RequestScaleformMovie("instructional_buttons") 120 | while not HasScaleformMovieLoaded(scaleform) do 121 | Wait(1) 122 | end 123 | PushScaleformMovieFunction(scaleform, "CLEAR_ALL") 124 | PopScaleformMovieFunctionVoid() 125 | 126 | PushScaleformMovieFunction(scaleform, "SET_CLEAR_SPACE") 127 | PushScaleformMovieFunctionParameterInt(200) 128 | PopScaleformMovieFunctionVoid() 129 | 130 | PushScaleformMovieFunction(scaleform, "SET_DATA_SLOT") 131 | PushScaleformMovieFunctionParameterInt(0) 132 | InstructionalButton(GetControlInstructionalButton(0, CONTROLS.MOVE_FAST, 1), "Faster") 133 | PopScaleformMovieFunctionVoid() 134 | 135 | PushScaleformMovieFunction(scaleform, "SET_DATA_SLOT") 136 | PushScaleformMovieFunctionParameterInt(1) 137 | InstructionalButton(GetControlInstructionalButton(0, CONTROLS.MOVE_SLOW, 1), "Slower") 138 | PopScaleformMovieFunctionVoid() 139 | 140 | PushScaleformMovieFunction(scaleform, "SET_DATA_SLOT") 141 | PushScaleformMovieFunctionParameterInt(2) 142 | InstructionalButton(GetControlInstructionalButton(0, CONTROLS.MOVE_Y, 1), "Fwd/Back") 143 | PopScaleformMovieFunctionVoid() 144 | 145 | PushScaleformMovieFunction(scaleform, "SET_DATA_SLOT") 146 | PushScaleformMovieFunctionParameterInt(3) 147 | InstructionalButton(GetControlInstructionalButton(0, CONTROLS.MOVE_X, 1), "Left/Right") 148 | PopScaleformMovieFunctionVoid() 149 | 150 | PushScaleformMovieFunction(scaleform, "SET_DATA_SLOT") 151 | PushScaleformMovieFunctionParameterInt(4) 152 | InstructionalButton(GetControlInstructionalButton(0, CONTROLS.MOVE_Z[2], 1), "Down") 153 | PopScaleformMovieFunctionVoid() 154 | 155 | PushScaleformMovieFunction(scaleform, "SET_DATA_SLOT") 156 | PushScaleformMovieFunctionParameterInt(5) 157 | InstructionalButton(GetControlInstructionalButton(0, CONTROLS.MOVE_Z[1], 1), "Up") 158 | PopScaleformMovieFunctionVoid() 159 | 160 | PushScaleformMovieFunction(scaleform, "DRAW_INSTRUCTIONAL_BUTTONS") 161 | PopScaleformMovieFunctionVoid() 162 | 163 | PushScaleformMovieFunction(scaleform, "SET_BACKGROUND_COLOUR") 164 | PushScaleformMovieFunctionParameterInt(0) 165 | PushScaleformMovieFunctionParameterInt(0) 166 | PushScaleformMovieFunctionParameterInt(0) 167 | PushScaleformMovieFunctionParameterInt(80) 168 | PopScaleformMovieFunctionVoid() 169 | 170 | while IsFreecamActive() do 171 | DrawScaleformMovieFullscreen(scaleform, 255, 255, 255, 255, 0) 172 | Wait(0) 173 | end 174 | SetScaleformMovieAsNoLongerNeeded() 175 | end) 176 | end 177 | 178 | -------------------------------------------------------------------------------- 179 | 180 | -- When the resource is stopped, make sure to return the camera to the player. 181 | AddEventHandler('onResourceStop', function (resourceName) 182 | if resourceName == GetCurrentResourceName() then 183 | SetFreecamActive(false) 184 | end 185 | end) 186 | -------------------------------------------------------------------------------- /monitor/web/public/js/txadmin/chart.js: -------------------------------------------------------------------------------- 1 | const translate = (x, y) => { 2 | return `translate(${x}, ${y})`; 3 | }; 4 | 5 | const clientsToMargin = (maxClients) => { 6 | //each char is about 5px 7 | return 12 + (maxClients.toString().length * 5); 8 | }; 9 | 10 | const yLabels = ['5 ms', '10 ms', '25 ms', '50 ms', '75 ms', '100 ms', '250 ms', '500 ms', '750 ms', '1.0 s', '2.5 s', '5.0 s', '7.5 s', '10 s', '+Inf']; 11 | 12 | 13 | const drawHeatmap = (d3Container, perfData, options = {}) => { 14 | //Dynamic label interval size 15 | // got the points manually, plotted to https://www.geogebra.org/graphing 16 | // then made a function with a slider to help me match the best fitting one 17 | const tickIntervalMod = Math.max( 18 | 15, 19 | Math.ceil(55 - (d3Container.offsetWidth * 0.051)) 20 | ); 21 | 22 | //Flatten data 23 | const snapTimes = []; 24 | const snapAvgTickTimes = []; 25 | const snapClients = []; 26 | const snapBuckets = []; 27 | const snapSkips = []; 28 | for (let snapIndex = 0; snapIndex < perfData.length; snapIndex++) { 29 | const snap = perfData[snapIndex]; 30 | snapAvgTickTimes.push(snap.avgTime); 31 | snapClients.push({ 32 | x: snapIndex, 33 | c: snap.clients, 34 | }); 35 | 36 | //Process skips 37 | if (snap.skipped) { 38 | snapSkips.push(snapIndex); 39 | } 40 | 41 | //Process times 42 | const time = new Date(snap.ts); 43 | if (snapIndex % tickIntervalMod == 0) { 44 | const hours = String(time.getHours()).padStart(2, "0"); 45 | const minutes = String(time.getMinutes()).padStart(2, "0"); 46 | snapTimes.push({ 47 | x: snapIndex, 48 | t: `${hours}:${minutes}` 49 | }) 50 | } 51 | 52 | //Process buckets 53 | for (let bucketIndex = 0; bucketIndex < 15; bucketIndex++) { 54 | const freq = (typeof snap.buckets[bucketIndex] == 'number') ? snap.buckets[bucketIndex] : 0 55 | snapBuckets.push({ 56 | x: snapIndex, 57 | y: bucketIndex, 58 | freq: freq 59 | }) 60 | } 61 | } 62 | const maxClients = d3.max(snapClients.map(t => t.c)); 63 | 64 | //Options 65 | if (typeof options.margin == 'undefined') options.margin = {} 66 | const margin = { 67 | top: options.margin.top || 5, 68 | right: options.margin.right || 45, 69 | bottom: options.margin.bottom || 20, 70 | left: options.margin.left || clientsToMargin(maxClients) 71 | }; 72 | const height = options.height || 340; 73 | const colorScheme = options.colorScheme || d3.interpolateViridis; 74 | 75 | 76 | //Macro drawing stuff 77 | const width = d3Container.offsetWidth; 78 | const color = d3.scaleSequential(colorScheme) 79 | .domain([0, 1]) 80 | const svg = d3.create("svg") 81 | .attr("viewBox", [0, 0, width, height]); 82 | const bgColor = options.bgColor || d3.color(color(0)).darker(0.35); 83 | 84 | 85 | // X Axis - Time 86 | const timeScale = d3.scaleBand() 87 | .domain(d3.range(perfData.length)) 88 | .range([margin.left, width - margin.right]) 89 | const timeAxis = d3.axisBottom(timeScale) 90 | .tickValues(snapTimes.map(t => t.x)) 91 | .tickFormat((d, i) => snapTimes[i].t) 92 | svg.append('g') 93 | .attr("id", "timeAxis") 94 | .attr("transform", translate(0, height - margin.bottom)) 95 | .attr("id", "x-axis") 96 | .attr("class", "axis") 97 | .call(timeAxis); 98 | 99 | 100 | // Y Axis - Tick Times 101 | const tickBucketsScale = d3.scaleBand() 102 | .domain(d3.range(yLabels.length)) 103 | .range([height - margin.bottom, margin.top]) 104 | const tickBucketsAxis = d3.axisRight(tickBucketsScale) 105 | .tickFormat((d, i) => `${yLabels[i]}`) 106 | svg.append("g") 107 | .attr("id", "tickBucketsAxis") 108 | .attr("class", "axis") 109 | .attr("transform", translate(width - margin.right - 3, 0)) 110 | .call(tickBucketsAxis); 111 | 112 | 113 | //Background 114 | svg.append('rect') 115 | .attr('x', margin.left) 116 | .attr('y', margin.top) 117 | .attr('width', width - margin.right - margin.left) 118 | .attr('height', height - margin.top - margin.bottom) 119 | .attr('fill', bgColor) 120 | .attr('stroke', bgColor) 121 | 122 | 123 | // Drawing the Heatmap 124 | const heatmap = svg.append("g") 125 | .attr("id", "heatmap") 126 | .selectAll('rect') 127 | .data(snapBuckets) 128 | .enter() 129 | .append('rect') 130 | .filter(d => (typeof d.freq == 'number' && d.freq > 0)) 131 | .attr('x', (d, i) => timeScale(d.x)) 132 | .attr('y', (d, i) => tickBucketsScale(d.y)) 133 | .attr('width', timeScale.bandwidth()) 134 | .attr('height', tickBucketsScale.bandwidth()) 135 | .attr('fill', d => color(d.freq)) 136 | .attr('stroke', d => color(d.freq)) 137 | 138 | 139 | // Y2 Axis - Player count 140 | const y2Padding = Math.round(tickBucketsScale.bandwidth() / 2); 141 | const clientsScale = d3.scaleLinear() 142 | .domain([0, maxClients]) 143 | .range([height - margin.bottom - y2Padding, margin.top + y2Padding]); 144 | 145 | const clientsAxis = d3.axisLeft(clientsScale) 146 | .tickFormat(t => t) 147 | .tickValues((maxClients > 7)? null : d3.range(maxClients+1)) 148 | svg.append("g") 149 | .attr("id", "clientAxis") 150 | .attr("class", "axis") 151 | .attr("transform", translate(margin.left - 1.5, 0)) 152 | .call(clientsAxis); 153 | 154 | const clientsLine = d3.line() 155 | .defined(d => !isNaN(d.c)) 156 | .x(d => timeScale(d.x) + 2) // very small offset 157 | .y(d => clientsScale(d.c)) 158 | const playerLineGroup = svg.append("g") 159 | .attr("id", "clientsLine"); 160 | 161 | playerLineGroup.append("path") 162 | .datum(snapClients) 163 | .attr("fill", "none") 164 | .attr("stroke", "black") 165 | .attr("stroke-width", 6) 166 | .attr("stroke-linejoin", "round") 167 | .attr("stroke-linecap", "round") 168 | .attr("d", clientsLine); 169 | playerLineGroup.append("path") 170 | .datum(snapClients) 171 | .attr("fill", "none") 172 | .attr("stroke", " rgb(204, 203, 203)") 173 | .attr("stroke-width", 2) 174 | .attr("stroke-linejoin", "round") 175 | .attr("stroke-linecap", "round") 176 | .attr("d", clientsLine); 177 | 178 | // Horizontal line on the max client count 179 | // playerLineGroup.append('line') 180 | // .style("stroke", "dodgerblue") // dodgerblue maybe 181 | // .attr("stroke-dasharray", 6) 182 | // .attr("y1", clientsScale(maxClients)) 183 | // .attr("y2", clientsScale(maxClients)) 184 | // .attr("x1", margin.left) 185 | // .attr("x2", width - margin.right) 186 | 187 | 188 | // Skip lines 189 | svg.append("g") 190 | .attr("id", "skipLines") 191 | .selectAll(".link") 192 | .data(snapSkips) 193 | .enter() 194 | .append('line') 195 | .style("stroke", "gold") // dodgerblue maybe 196 | .attr("stroke-dasharray", 6) 197 | .attr("x1", (d) => timeScale(d)) 198 | .attr("x2", (d) => timeScale(d)) 199 | .attr("y1", height - margin.bottom) 200 | .attr("y2", margin.top); 201 | 202 | d3Container.innerHTML = ''; 203 | d3Container.append(svg.node()); 204 | 205 | return heatmap._groups[0].length + 1; 206 | } 207 | 208 | 209 | export { drawHeatmap }; 210 | -------------------------------------------------------------------------------- /monitor/resource/menu/server/sv_main_page.lua: -------------------------------------------------------------------------------- 1 | --Check Environment 2 | if GetConvar('txAdminServerMode', 'false') ~= 'true' then 3 | return 4 | end 5 | 6 | -- ============================================= 7 | -- This file is for server side handlers related to 8 | -- actions defined on Menu's "Main Page" 9 | -- ============================================= 10 | 11 | RegisterNetEvent('txAdmin:menu:tpToWaypoint', function() 12 | local src = source 13 | local allow = PlayerHasTxPermission(src, 'players.teleport') 14 | if allow then 15 | TriggerClientEvent('txAdmin:menu:tpToWaypoint', src) 16 | Wait(250) 17 | local coords = GetEntityCoords(GetPlayerPed(src)) 18 | TriggerEvent("txaLogger:menuEvent", src, "teleportWaypoint", true, 19 | { x = coords[1], y = coords[2], z = coords[3] }) 20 | else 21 | TriggerEvent("txaLogger:menuEvent", src, "teleportWaypoint", false) 22 | end 23 | end) 24 | 25 | RegisterNetEvent('txAdmin:menu:sendAnnouncement', function(message) 26 | local src = source 27 | if type(message) ~= 'string' then 28 | return 29 | end 30 | local allow = PlayerHasTxPermission(src, 'players.message') 31 | TriggerEvent("txaLogger:menuEvent", src, "announcement", allow, message) 32 | if allow then 33 | local author = TX_ADMINS[tostring(src)].tag 34 | TriggerClientEvent("txAdmin:receiveAnnounce", -1, message, author) 35 | end 36 | end) 37 | 38 | RegisterNetEvent('txAdmin:menu:fixVehicle', function() 39 | local src = source 40 | local allow = PlayerHasTxPermission(src, 'menu.vehicle') 41 | TriggerEvent("txaLogger:menuEvent", src, "vehicleRepair", allow) 42 | if allow then 43 | TriggerClientEvent('txAdmin:menu:fixVehicle', src) 44 | end 45 | end) 46 | 47 | RegisterNetEvent('txAdmin:menu:clearArea', function(radius) 48 | local src = source 49 | local allow = PlayerHasTxPermission(src, 'menu.clear_area') 50 | TriggerEvent("txaLogger:menuEvent", src, "clearArea", allow, radius) 51 | if allow then 52 | TriggerClientEvent('txAdmin:menu:clearArea', src, radius) 53 | end 54 | end) 55 | 56 | local CREATE_AUTOMOBILE = GetHashKey('CREATE_AUTOMOBILE') 57 | 58 | --- Spawn a vehicle on the server at the request of a client 59 | ---@param model string 60 | ---@param isAutomobile boolean 61 | RegisterNetEvent('txAdmin:menu:spawnVehicle', function(model, isAutomobile) 62 | local src = source 63 | if type(model) ~= 'string' then 64 | return 65 | end 66 | if type(isAutomobile) ~= 'boolean' then 67 | return 68 | end 69 | 70 | local allow = PlayerHasTxPermission(src, 'menu.vehicle') 71 | TriggerEvent("txaLogger:menuEvent", src, "spawnVehicle", allow, model) 72 | if allow then 73 | local ped = GetPlayerPed(src) 74 | local coords = GetEntityCoords(ped) 75 | local heading = GetEntityHeading(ped) 76 | 77 | local seatsToPlace = {} 78 | local oldVeh = GetVehiclePedIsIn(ped, false) 79 | if oldVeh and oldVeh > 0 then 80 | for i = 6, -1, -1 do 81 | local pedInSeat = GetPedInVehicleSeat(oldVeh, i) 82 | if pedInSeat > 0 then 83 | seatsToPlace[i] = pedInSeat 84 | end 85 | end 86 | else 87 | seatsToPlace[-1] = ped 88 | end 89 | 90 | local veh 91 | if isAutomobile then 92 | coords = vec4(coords[1], coords[2], coords[3], heading) 93 | veh = Citizen.InvokeNative(CREATE_AUTOMOBILE, GetHashKey(model), coords); 94 | else 95 | veh = CreateVehicle(model, coords[1], coords[2], coords[3], heading, true, true) 96 | end 97 | local tries = 0 98 | while not DoesEntityExist(veh) do 99 | Wait(0) 100 | tries = tries + 1 101 | if tries > 250 then 102 | break 103 | end 104 | end 105 | local netID = NetworkGetNetworkIdFromEntity(veh) 106 | debugPrint(string.format("spawn vehicle (src=^3%d^0, model=^4%s^0, isAuto=%s^0, netID=^3%s^0)", src, model, 107 | (isAutomobile and '^2yes' or '^3no'), netID)) 108 | 109 | -- map all player ids to peds 110 | local players = GetPlayers() 111 | local pedMap = {} 112 | for _, id in pairs(players) do 113 | local pedId = GetPlayerPed(id) 114 | pedMap[pedId] = id 115 | end 116 | 117 | for seatIndex, seatPed in pairs(seatsToPlace) do 118 | debugPrint(("setting %d into seat index %d"):format(seatPed, seatIndex)) 119 | local targetSrc = pedMap[seatPed] 120 | if type(targetSrc) == 'string' then 121 | TriggerClientEvent('txAdmin:events:queueSeatInVehicle', targetSrc, netID, seatIndex) 122 | end 123 | end 124 | end 125 | end) 126 | 127 | --- Deletes the vehicle the player is currently in 128 | --- @param netId int 129 | RegisterNetEvent("txAdmin:menu:deleteVehicle", function(netId) 130 | local src = source 131 | local allow = PlayerHasTxPermission(src, 'menu.vehicle') 132 | TriggerEvent("txaLogger:menuEvent", src, "deleteVehicle", allow) 133 | if allow then 134 | local vehicle = NetworkGetEntityFromNetworkId(netId) 135 | DeleteEntity(vehicle) 136 | end 137 | end) 138 | 139 | RegisterNetEvent('txAdmin:menu:healAllPlayers', function() 140 | local src = source 141 | local allow = PlayerHasTxPermission(src, 'players.heal') 142 | TriggerEvent("txaLogger:menuEvent", src, "healAll", true) 143 | if allow then 144 | -- For use with third party resources that handle players 145 | -- 'revive state' standalone from health (esx-ambulancejob, qb-ambulancejob, etc) 146 | TriggerEvent("txAdmin:events:healedPlayer", {id = -1}) 147 | TriggerClientEvent('txAdmin:menu:healed', -1) 148 | end 149 | end) 150 | 151 | RegisterNetEvent('txAdmin:menu:healMyself', function() 152 | local src = source 153 | local allow = PlayerHasTxPermission(src, 'players.heal') 154 | TriggerEvent("txaLogger:menuEvent", src, "healSelf", allow) 155 | if allow then 156 | -- For use with third party resources that handle players 157 | -- 'revive state' standalone from health (esx-ambulancejob, qb-ambulancejob, etc) 158 | TriggerEvent("txAdmin:events:healedPlayer", {id = src}) 159 | TriggerClientEvent('txAdmin:menu:healed', src) 160 | end 161 | end) 162 | 163 | RegisterNetEvent('txAdmin:menu:healPlayer', function(id) 164 | local src = source 165 | if type(id) ~= 'string' and type(id) ~= 'number' then 166 | return 167 | end 168 | id = tonumber(id) 169 | local allow = PlayerHasTxPermission(src, 'players.heal') 170 | if allow then 171 | local ped = GetPlayerPed(id) 172 | if ped then 173 | -- For use with third party resources that handle players 174 | -- 'revive state' standalone from health (esx-ambulancejob, qb-ambulancejob, etc) 175 | -- TriggerEvent('txAdmin:healedPlayer', id) 176 | TriggerEvent("txAdmin:events:healedPlayer", {id = id}) 177 | TriggerClientEvent('txAdmin:menu:healed', id) 178 | end 179 | end 180 | TriggerEvent('txaLogger:menuEvent', src, "healPlayer", allow, id) 181 | end) 182 | 183 | RegisterNetEvent('txAdmin:menu:showPlayerIDs', function(enabled) 184 | local src = source 185 | local allow = PlayerHasTxPermission(src, 'menu.viewids') 186 | TriggerEvent("txaLogger:menuEvent", src, "showPlayerIDs", allow, enabled) 187 | if allow then 188 | TriggerClientEvent('txAdmin:menu:showPlayerIDs', src, enabled) 189 | end 190 | end) 191 | 192 | ---@param x number|nil 193 | ---@param y number|nil 194 | ---@param z number|nil 195 | RegisterNetEvent('txAdmin:menu:tpToCoords', function(x, y, z) 196 | local src = source 197 | if type(x) ~= 'number' or type(y) ~= 'number' or type(z) ~= 'number' then 198 | return 199 | end 200 | 201 | local allow = PlayerHasTxPermission(src, 'players.teleport') 202 | TriggerEvent("txaLogger:menuEvent", src, "teleportCoords", true, { x = x, y = y, z = z }) 203 | if allow then 204 | TriggerClientEvent('txAdmin:menu:tpToCoords', src, x, y, z) 205 | end 206 | end) 207 | -------------------------------------------------------------------------------- /monitor/docs/newDatabase.md: -------------------------------------------------------------------------------- 1 | NOTE: mesmo arquivo da pasta ~/Desktop/PROGRAMMING/txAdmin-newDatabase 2 | ## New database schema: 3 | ```js 4 | //Every player that have been on the server over specific threshold (15m default) gets added to the database 5 | players: [{ 6 | license: 'xxxxxxxxx', 7 | name: 'yyyyy', 8 | totalPlayTime: 205, 9 | joined: TS, 10 | whitelisted?: TS, //ts instead of bool so we can have a feature to automatically remove WL after xxx days 11 | notes?: { 12 | text: '', 13 | lastAdmin: 'null', 14 | tsLastEdit: TS 15 | }, 16 | activity: [ 17 | //The most recent days with session time 18 | //Array max length configurable on settings page 19 | //Format: date, minutes played 20 | ['2022-03-24', 138], 21 | ['2022-03-25', 42], 22 | ], 23 | ids: [ 24 | //Every identifier to ever be used with the license above 25 | ], 26 | hwids: [ 27 | //Every HWID to ever be used with the license above 28 | ], 29 | log: [{ 30 | //...bans, warns, commends 31 | id: "Bxxx-xxxx/Axxx-xxxx/Cxxx-xxxx", 32 | type: "ban/warn/commend", 33 | author: "tabarra", 34 | reason: "sdf sdf dsf ds", 35 | ts: TS, 36 | exp?: TS, 37 | revokedBy?: "tabarra" 38 | }] 39 | }] 40 | 41 | // The legacy bans collection is for bans we cannot strictly correlate to a player while migrating 42 | // Once a day check for expired legacy bans and remove from the collection 43 | // Reserved for: 44 | // - bans that cannot be linked to a license 45 | // - bans that which identifiers types are not unique (eg more than one "discord:") 46 | // - bans without player name (meaning it was a manual id ban) 47 | // - active bans only, do not import expired or revoked bans 48 | // - bans with reason starting with `[IMPORTED]` 49 | // - any future bans that get imported 50 | legacyBans: [{ 51 | id: "Lxxx-xxxx", 52 | reason: "", 53 | ts: TS, 54 | exp?: TS, 55 | ids: [...], 56 | }] 57 | ``` 58 | 59 | - License is unique 60 | - FIXME: what is the correct handling of license2? 61 | - Any other identifier can be present in more than one player 62 | - Identifier and HWIDs array will contain EVERY id to ever join a server with that specific license 63 | - For hwid matching, add a setting with numbers `3, 5, 7 (default), 11, 13` (13 should be 65% of the median number of hwids) 64 | - If you join an account with new license, but matching identifier with another one, it will be registered as a new account 65 | - If your identifier matches any account that is banned, the ban apllies to you 66 | - make sure the ban message makes clear which identifier matched and that it is from another account (maybe even say the name) 67 | - would be cool if we could give the player an adaptive card, as it is a lot of information to show 68 | - whitelist will be cease to be an action and become a prop in the player object containing only a timestamp of when it was issued 69 | - JSON db migration of bans/warns: 70 | - FIXME: can i migrate Axxx-xxxx to Wxxx-xxxx? 71 | - if license is present, matches with a user, no multiple ids of the same type (ex more than one discord) 72 | - then it is added to the player history 73 | - else 74 | - added to a "legacy bans" table 75 | - Warns now can be revoked, as they are quite often used as "negative points" on player log instead of actual Warns 76 | - When we query (and find) a player on playerConnecting, cache the player object in a lru-cache so we don't have to requery on playerJoining 77 | - FIXME: should I change how the whitelist currently works? 78 | 79 | 80 | ## Interface changes: 81 | - Player modal MUST find any matching accounts with that identifier and show: 82 | - matching accounts ex "1 account with matching identifiers found" 83 | - if it is banned in another account 84 | - if any other related account has low credibility 85 | - also search for legacy bans 86 | - In the player modal we would need a good space for all the warnings like the matching account, matching ban, or no-license so no-save 87 | - FIXME: in the modal, how to handle opening for a id that already disconnected 88 | 89 | - Break down the Players page into: 90 | - Overview 91 | - General stats, maybe a chart 92 | - Actions by admin 93 | - Players page, with a more explicit and "dumbed down" way of searching for players 94 | - Paginated table 95 | - Default view can be "latest players to join the database" (crossed the 15m default threshold) 96 | - Clicking on a player will bring the modal up 97 | - Bans/Warns/Commends page 98 | - Paginated table 99 | - Should we have a button to ban a specific identifier or identifier array? We prefer banning players instead of adding legacy bans 100 | - Whitelist (behavior/components to be defined) 101 | 102 | 103 | ## Database use cases: 104 | 1. Player connecting / Search by player identifier 105 | Search for any account or legacy ban with any identifier (or license) matching the connecting users. 106 | In other words: 107 | - the input string `license` should match any `players[?].license` 108 | - the input array `ids` should match ids in `players[?].ids` 109 | - the input array `hwids` should match hwids in `players[?].hwids` 110 | For player connection, this array will be processed to check for active bans and whitelist. 111 | 112 | 2. Search for players by name 113 | Using normalized (no unicode), lowercase, and within some levenshtein distance of a specified string. 114 | For instance, searching for `tabara` should find `𝓣𝓪𝓫𝓪𝓻𝓻𝓪` with distance 1. 115 | 116 | 3. Find player by action ID (eg ban) 117 | 4. Get player by license 118 | 5. Edit player by license 119 | 6. Edit a player action by it's ID (the previous one kinda covers it) 120 | 7. Edit player action by ID 121 | 8. Get legacy ban by ID 122 | 9. Delete legacy ban by ID 123 | 124 | 10. List of top players by activity time (all time) 125 | 11. List of top players by activity time (last X days) 126 | 12. Paginated list of newest players 127 | 13. Paginated list of latest log entries (bans, warns, commends) 128 | 129 | 14. Count of all players, bans, warns, whitelists, players active in the last week 130 | 15. Get all actions grouped by admin name 131 | This feature requested on #476 would allow for easier admin action auditing, as well as statistics page which people always love. 132 | 133 | 16. Database cleanup functions: 134 | - Remove players inactive over X days 135 | - Remove players not whitelisted 136 | - Remove revoked, revoked or expired or all bans 137 | - Remove revoked, older than X days, or all warns 138 | - Remove whitelists over X days, or all whitelists 139 | 140 | 17. Insert new player 141 | 18. Insert new player action 142 | 19. Insert new legacy ban (bulk only) 143 | 144 | 145 | ## Random notes: 146 | GH Issues: 147 | - #578 Edit bans 148 | - maybe block editing legacy, expired and revoked bans 149 | - maybe an editLog array on the ban with author, date and reason (prefill reason with "edited expiration and reason because ___"?) 150 | - #449 Add revoke ban/whitelist to the player modal 151 | - #446 HWID Token bans 152 | - #385 Save identifier history 153 | - #522 offline warns 154 | - #476 feature to see how many bans/kicks/warns an admin issued 155 | - expensive to calculate in the current db schema? 156 | 157 | Ways to "click" on a user (reference them) 158 | - web playerlist: currently license, change to id 159 | - nui playerlist: id 160 | - server log: mutex_id 161 | - the ban/warn/commend page: license 162 | 163 | 164 | talvez fazer os usuários serem uma classe com props .warn, .ban e etc seja uma boa 165 | ele teria prop pra setar como offline, e uma prop pra marca-lo como temp que ele mesmo remove depois do tempo 166 | -------------------------------------------------------------------------------- /monitor/web/public/js/bootstrap-notify.min.js: -------------------------------------------------------------------------------- 1 | /* Project: Bootstrap Growl = v3.1.3 | Description: Turns standard Bootstrap alerts into "Growl-like" notifications. | Author: Mouse0270 aka Robert McIntosh | License: MIT License | Website: https://github.com/mouse0270/bootstrap-growl */ 2 | !function(t){"function"==typeof define&&define.amd?define(["jquery"],t):t("object"==typeof exports?require("jquery"):jQuery)}(function(t){function e(e,i,n){var i={content:{message:"object"==typeof i?i.message:i,title:i.title?i.title:"",icon:i.icon?i.icon:"",url:i.url?i.url:"#",target:i.target?i.target:"-"}};n=t.extend(!0,{},i,n),this.settings=t.extend(!0,{},s,n),this._defaults=s,"-"==this.settings.content.target&&(this.settings.content.target=this.settings.url_target),this.animations={start:"webkitAnimationStart oanimationstart MSAnimationStart animationstart",end:"webkitAnimationEnd oanimationend MSAnimationEnd animationend"},"number"==typeof this.settings.offset&&(this.settings.offset={x:this.settings.offset,y:this.settings.offset}),this.init()}var s={element:"body",position:null,type:"info",allow_dismiss:!0,newest_on_top:!1,showProgressbar:!1,placement:{from:"top",align:"right"},offset:20,spacing:10,z_index:1031,delay:5e3,timer:1e3,url_target:"_blank",mouse_over:null,animate:{enter:"animated fadeInDown",exit:"animated fadeOutUp"},onShow:null,onShown:null,onClose:null,onClosed:null,icon_type:"class",template:''};String.format=function(){for(var t=arguments[0],e=1;e .progress-bar').removeClass("progress-bar-"+t.settings.type),t.settings.type=i[e],this.$ele.addClass("alert-"+i[e]).find('[data-notify="progressbar"] > .progress-bar').addClass("progress-bar-"+i[e]);break;case"icon":var n=this.$ele.find('[data-notify="icon"]');"class"==t.settings.icon_type.toLowerCase()?n.removeClass(t.settings.content.icon).addClass(i[e]):(n.is("img")||n.find("img"),n.attr("src",i[e]));break;case"progress":var a=t.settings.delay-t.settings.delay*(i[e]/100);this.$ele.data("notify-delay",a),this.$ele.find('[data-notify="progressbar"] > div').attr("aria-valuenow",i[e]).css("width",i[e]+"%");break;case"url":this.$ele.find('[data-notify="url"]').attr("href",i[e]);break;case"target":this.$ele.find('[data-notify="url"]').attr("target",i[e]);break;default:this.$ele.find('[data-notify="'+e+'"]').html(i[e])}var o=this.$ele.outerHeight()+parseInt(t.settings.spacing)+parseInt(t.settings.offset.y);t.reposition(o)},close:function(){t.close()}}},buildNotify:function(){var e=this.settings.content;this.$ele=t(String.format(this.settings.template,this.settings.type,e.title,e.message,e.url,e.target)),this.$ele.attr("data-notify-position",this.settings.placement.from+"-"+this.settings.placement.align),this.settings.allow_dismiss||this.$ele.find('[data-notify="dismiss"]').css("display","none"),(this.settings.delay<=0&&!this.settings.showProgressbar||!this.settings.showProgressbar)&&this.$ele.find('[data-notify="progressbar"]').remove()},setIcon:function(){"class"==this.settings.icon_type.toLowerCase()?this.$ele.find('[data-notify="icon"]').addClass(this.settings.content.icon):this.$ele.find('[data-notify="icon"]').is("img")?this.$ele.find('[data-notify="icon"]').attr("src",this.settings.content.icon):this.$ele.find('[data-notify="icon"]').append('Notify Icon')},styleURL:function(){this.$ele.find('[data-notify="url"]').css({backgroundImage:"url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)",height:"100%",left:"0px",position:"absolute",top:"0px",width:"100%",zIndex:this.settings.z_index+1}),this.$ele.find('[data-notify="dismiss"]').css({position:"absolute",right:"10px",top:"5px",zIndex:this.settings.z_index+2})},placement:function(){var e=this,s=this.settings.offset.y,i={display:"inline-block",margin:"0px auto",position:this.settings.position?this.settings.position:"body"===this.settings.element?"fixed":"absolute",transition:"all .5s ease-in-out",zIndex:this.settings.z_index},n=!1,a=this.settings;switch(t('[data-notify-position="'+this.settings.placement.from+"-"+this.settings.placement.align+'"]:not([data-closing="true"])').each(function(){return s=Math.max(s,parseInt(t(this).css(a.placement.from))+parseInt(t(this).outerHeight())+parseInt(a.spacing))}),1==this.settings.newest_on_top&&(s=this.settings.offset.y),i[this.settings.placement.from]=s+"px",this.settings.placement.align){case"left":case"right":i[this.settings.placement.align]=this.settings.offset.x+"px";break;case"center":i.left=0,i.right=0}this.$ele.css(i).addClass(this.settings.animate.enter),t.each(Array("webkit","moz","o","ms",""),function(t,s){e.$ele[0].style[s+"AnimationIterationCount"]=1}),t(this.settings.element).append(this.$ele),1==this.settings.newest_on_top&&(s=parseInt(s)+parseInt(this.settings.spacing)+this.$ele.outerHeight(),this.reposition(s)),t.isFunction(e.settings.onShow)&&e.settings.onShow.call(this.$ele),this.$ele.one(this.animations.start,function(){n=!0}).one(this.animations.end,function(){t.isFunction(e.settings.onShown)&&e.settings.onShown.call(this)}),setTimeout(function(){n||t.isFunction(e.settings.onShown)&&e.settings.onShown.call(this)},600)},bind:function(){var e=this;if(this.$ele.find('[data-notify="dismiss"]').on("click",function(){e.close()}),this.$ele.mouseover(function(){t(this).data("data-hover","true")}).mouseout(function(){t(this).data("data-hover","false")}),this.$ele.data("data-hover","false"),this.settings.delay>0){e.$ele.data("notify-delay",e.settings.delay);var s=setInterval(function(){var t=parseInt(e.$ele.data("notify-delay"))-e.settings.timer;if("false"===e.$ele.data("data-hover")&&"pause"==e.settings.mouse_over||"pause"!=e.settings.mouse_over){var i=(e.settings.delay-t)/e.settings.delay*100;e.$ele.data("notify-delay",t),e.$ele.find('[data-notify="progressbar"] > div').attr("aria-valuenow",i).css("width",i+"%")}t<=-e.settings.timer&&(clearInterval(s),e.close())},e.settings.timer)}},close:function(){var e=this,s=parseInt(this.$ele.css(this.settings.placement.from)),i=!1;this.$ele.data("closing","true").addClass(this.settings.animate.exit),e.reposition(s),t.isFunction(e.settings.onClose)&&e.settings.onClose.call(this.$ele),this.$ele.one(this.animations.start,function(){i=!0}).one(this.animations.end,function(){t(this).remove(),t.isFunction(e.settings.onClosed)&&e.settings.onClosed.call(this)}),setTimeout(function(){i||(e.$ele.remove(),e.settings.onClosed&&e.settings.onClosed(e.$ele))},600)},reposition:function(e){var s=this,i='[data-notify-position="'+this.settings.placement.from+"-"+this.settings.placement.align+'"]:not([data-closing="true"])',n=this.$ele.nextAll(i);1==this.settings.newest_on_top&&(n=this.$ele.prevAll(i)),n.each(function(){t(this).css(s.settings.placement.from,e),e=parseInt(e)+parseInt(s.settings.spacing)+t(this).outerHeight()})}}),t.notify=function(t,s){var i=new e(this,t,s);return i.notify},t.notifyDefaults=function(e){return s=t.extend(!0,{},s,e)},t.notifyClose=function(e){"undefined"==typeof e||"all"==e?t("[data-notify]").find('[data-notify="dismiss"]').trigger("click"):t('[data-notify-position="'+e+'"]').find('[data-notify="dismiss"]').trigger("click")}}); -------------------------------------------------------------------------------- /monitor/resource/cl_logger.lua: -------------------------------------------------------------------------------- 1 | -- Death reasons 2 | local deathHashTable = { 3 | [GetHashKey('WEAPON_ANIMAL')] = 'Animal', 4 | [GetHashKey('WEAPON_COUGAR')] = 'Cougar', 5 | [GetHashKey('WEAPON_ADVANCEDRIFLE')] = 'Advanced Rifle', 6 | [GetHashKey('WEAPON_APPISTOL')] = 'AP Pistol', 7 | [GetHashKey('WEAPON_ASSAULTRIFLE')] = 'Assault Rifle', 8 | [GetHashKey('WEAPON_ASSAULTRIFLE_MK2')] = 'Assault Rifke Mk2', 9 | [GetHashKey('WEAPON_ASSAULTSHOTGUN')] = 'Assault Shotgun', 10 | [GetHashKey('WEAPON_ASSAULTSMG')] = 'Assault SMG', 11 | [GetHashKey('WEAPON_AUTOSHOTGUN')] = 'Automatic Shotgun', 12 | [GetHashKey('WEAPON_BULLPUPRIFLE')] = 'Bullpup Rifle', 13 | [GetHashKey('WEAPON_BULLPUPRIFLE_MK2')] = 'Bullpup Rifle Mk2', 14 | [GetHashKey('WEAPON_BULLPUPSHOTGUN')] = 'Bullpup Shotgun', 15 | [GetHashKey('WEAPON_CARBINERIFLE')] = 'Carbine Rifle', 16 | [GetHashKey('WEAPON_CARBINERIFLE_MK2')] = 'Carbine Rifle Mk2', 17 | [GetHashKey('WEAPON_COMBATMG')] = 'Combat MG', 18 | [GetHashKey('WEAPON_COMBATMG_MK2')] = 'Combat MG Mk2', 19 | [GetHashKey('WEAPON_COMBATPDW')] = 'Combat PDW', 20 | [GetHashKey('WEAPON_COMBATPISTOL')] = 'Combat Pistol', 21 | [GetHashKey('WEAPON_COMPACTRIFLE')] = 'Compact Rifle', 22 | [GetHashKey('WEAPON_DBSHOTGUN')] = 'Double Barrel Shotgun', 23 | [GetHashKey('WEAPON_DOUBLEACTION')] = 'Double Action Revolver', 24 | [GetHashKey('WEAPON_FLAREGUN')] = 'Flare gun', 25 | [GetHashKey('WEAPON_GUSENBERG')] = 'Gusenberg', 26 | [GetHashKey('WEAPON_HEAVYPISTOL')] = 'Heavy Pistol', 27 | [GetHashKey('WEAPON_HEAVYSHOTGUN')] = 'Heavy Shotgun', 28 | [GetHashKey('WEAPON_HEAVYSNIPER')] = 'Heavy Sniper', 29 | [GetHashKey('WEAPON_HEAVYSNIPER_MK2')] = 'Heavy Sniper', 30 | [GetHashKey('WEAPON_MACHINEPISTOL')] = 'Machine Pistol', 31 | [GetHashKey('WEAPON_MARKSMANPISTOL')] = 'Marksman Pistol', 32 | [GetHashKey('WEAPON_MARKSMANRIFLE')] = 'Marksman Rifle', 33 | [GetHashKey('WEAPON_MARKSMANRIFLE_MK2')] = 'Marksman Rifle Mk2', 34 | [GetHashKey('WEAPON_MG')] = 'MG', 35 | [GetHashKey('WEAPON_MICROSMG')] = 'Micro SMG', 36 | [GetHashKey('WEAPON_MINIGUN')] = 'Minigun', 37 | [GetHashKey('WEAPON_MINISMG')] = 'Mini SMG', 38 | [GetHashKey('WEAPON_MUSKET')] = 'Musket', 39 | [GetHashKey('WEAPON_PISTOL')] = 'Pistol', 40 | [GetHashKey('WEAPON_PISTOL_MK2')] = 'Pistol Mk2', 41 | [GetHashKey('WEAPON_PISTOL50')] = 'Pistol .50', 42 | [GetHashKey('WEAPON_PUMPSHOTGUN')] = 'Pump Shotgun', 43 | [GetHashKey('WEAPON_PUMPSHOTGUN_MK2')] = 'Pump Shotgun Mk2', 44 | [GetHashKey('WEAPON_RAILGUN')] = 'Railgun', 45 | [GetHashKey('WEAPON_REVOLVER')] = 'Revolver', 46 | [GetHashKey('WEAPON_REVOLVER_MK2')] = 'Revolver Mk2', 47 | [GetHashKey('WEAPON_SAWNOFFSHOTGUN')] = 'Sawnoff Shotgun', 48 | [GetHashKey('WEAPON_SMG')] = 'SMG', 49 | [GetHashKey('WEAPON_SMG_MK2')] = 'SMG Mk2', 50 | [GetHashKey('WEAPON_SNIPERRIFLE')] = 'Sniper Rifle', 51 | [GetHashKey('WEAPON_SNSPISTOL')] = 'SNS Pistol', 52 | [GetHashKey('WEAPON_SNSPISTOL_MK2')] = 'SNS Pistol Mk2', 53 | [GetHashKey('WEAPON_SPECIALCARBINE')] = 'Special Carbine', 54 | [GetHashKey('WEAPON_SPECIALCARBINE_MK2')] = 'Special Carbine Mk2', 55 | [GetHashKey('WEAPON_STINGER')] = 'Stinger', 56 | [GetHashKey('WEAPON_STUNGUN')] = 'Stungun', 57 | [GetHashKey('WEAPON_VINTAGEPISTOL')] = 'Vintage Pistol', 58 | [GetHashKey('VEHICLE_WEAPON_PLAYER_LASER')] = 'Vehicle Lasers', 59 | [GetHashKey('WEAPON_FIRE')] = 'Fire', 60 | [GetHashKey('WEAPON_FLARE')] = 'Flare', 61 | [GetHashKey('WEAPON_FLAREGUN')] = 'Flaregun', 62 | [GetHashKey('WEAPON_MOLOTOV')] = 'Molotov', 63 | [GetHashKey('WEAPON_PETROLCAN')] = 'Petrol Can', 64 | [GetHashKey('WEAPON_HELI_CRASH')] = 'Helicopter Crash', 65 | [GetHashKey('WEAPON_RAMMED_BY_CAR')] = 'Rammed by Vehicle', 66 | [GetHashKey('WEAPON_RUN_OVER_BY_CAR')] = 'Ranover by Vehicle', 67 | [GetHashKey('VEHICLE_WEAPON_SPACE_ROCKET')] = 'Vehicle Space Rocket', 68 | [GetHashKey('VEHICLE_WEAPON_TANK')] = 'Tank', 69 | [GetHashKey('WEAPON_AIRSTRIKE_ROCKET')] = 'Airstrike Rocket', 70 | [GetHashKey('WEAPON_AIR_DEFENCE_GUN')] = 'Air Defence Gun', 71 | [GetHashKey('WEAPON_COMPACTLAUNCHER')] = 'Compact Launcher', 72 | [GetHashKey('WEAPON_EXPLOSION')] = 'Explosion', 73 | [GetHashKey('WEAPON_FIREWORK')] = 'Firework', 74 | [GetHashKey('WEAPON_GRENADE')] = 'Grenade', 75 | [GetHashKey('WEAPON_GRENADELAUNCHER')] = 'Grenade Launcher', 76 | [GetHashKey('WEAPON_HOMINGLAUNCHER')] = 'Homing Launcher', 77 | [GetHashKey('WEAPON_PASSENGER_ROCKET')] = 'Passenger Rocket', 78 | [GetHashKey('WEAPON_PIPEBOMB')] = 'Pipe bomb', 79 | [GetHashKey('WEAPON_PROXMINE')] = 'Proximity Mine', 80 | [GetHashKey('WEAPON_RPG')] = 'RPG', 81 | [GetHashKey('WEAPON_STICKYBOMB')] = 'Sticky Bomb', 82 | [GetHashKey('WEAPON_VEHICLE_ROCKET')] = 'Vehicle Rocket', 83 | [GetHashKey('WEAPON_BZGAS')] = 'BZ Gas', 84 | [GetHashKey('WEAPON_FIREEXTINGUISHER')] = 'Fire Extinguisher', 85 | [GetHashKey('WEAPON_SMOKEGRENADE')] = 'Smoke Grenade', 86 | [GetHashKey('WEAPON_BATTLEAXE')] = 'Battleaxe', 87 | [GetHashKey('WEAPON_BOTTLE')] = 'Bottle', 88 | [GetHashKey('WEAPON_KNIFE')] = 'Knife', 89 | [GetHashKey('WEAPON_MACHETE')] = 'Machete', 90 | [GetHashKey('WEAPON_SWITCHBLADE')] = 'Switch Blade', 91 | [GetHashKey('OBJECT')] = 'Object', 92 | [GetHashKey('VEHICLE_WEAPON_ROTORS')] = 'Vehicle Rotors', 93 | [GetHashKey('WEAPON_BALL')] = 'Ball', 94 | [GetHashKey('WEAPON_BAT')] = 'Bat', 95 | [GetHashKey('WEAPON_CROWBAR')] = 'Crowbar', 96 | [GetHashKey('WEAPON_FLASHLIGHT')] = 'Flashlight', 97 | [GetHashKey('WEAPON_GOLFCLUB')] = 'Golfclub', 98 | [GetHashKey('WEAPON_HAMMER')] = 'Hammer', 99 | [GetHashKey('WEAPON_HATCHET')] = 'Hatchet', 100 | [GetHashKey('WEAPON_HIT_BY_WATER_CANNON')] = 'Water Cannon', 101 | [GetHashKey('WEAPON_KNUCKLE')] = 'Knuckle', 102 | [GetHashKey('WEAPON_NIGHTSTICK')] = 'Night Stick', 103 | [GetHashKey('WEAPON_POOLCUE')] = 'Pool Cue', 104 | [GetHashKey('WEAPON_SNOWBALL')] = 'Snowball', 105 | [GetHashKey('WEAPON_UNARMED')] = 'Fist', 106 | [GetHashKey('WEAPON_WRENCH')] = 'Wrench', 107 | [GetHashKey('WEAPON_DROWNING')] = 'Drowned', 108 | [GetHashKey('WEAPON_DROWNING_IN_VEHICLE')] = 'Drowned in Vehicle', 109 | [GetHashKey('WEAPON_BARBED_WIRE')] = 'Barbed Wire', 110 | [GetHashKey('WEAPON_BLEEDING')] = 'Bleed', 111 | [GetHashKey('WEAPON_ELECTRIC_FENCE')] = 'Electric Fence', 112 | [GetHashKey('WEAPON_EXHAUSTION')] = 'Exhaustion', 113 | [GetHashKey('WEAPON_FALL')] = 'Falling', 114 | } 115 | 116 | local function processDeath(ped) 117 | local killerPed = GetPedSourceOfDeath(ped) 118 | local causeHash = GetPedCauseOfDeath(ped) 119 | local killer = false 120 | 121 | if killerPed == ped then 122 | killer = false 123 | else 124 | if IsEntityAPed(killerPed) and IsPedAPlayer(killerPed) then 125 | killer = NetworkGetPlayerIndexFromPed(killerPed) 126 | elseif IsEntityAVehicle(killerPed) then 127 | local drivingPed = GetPedInVehicleSeat(killerPed, -1) 128 | if IsEntityAPed(drivingPed) == 1 and IsPedAPlayer(drivingPed) then 129 | killer = NetworkGetPlayerIndexFromPed(drivingPed) 130 | end 131 | end 132 | end 133 | 134 | local deathReason = deathHashTable[causeHash] or 'unknown' 135 | 136 | if not killer then 137 | if deathReason ~= "unknown" then 138 | deathReason = "suicide (" .. deathReason .. ")" 139 | else 140 | deathReason = "suicide" 141 | end 142 | else 143 | killer = GetPlayerServerId(killer) 144 | end 145 | 146 | TriggerServerEvent("txaLogger:DeathNotice", killer, deathReason) 147 | end 148 | 149 | -- Trigger Event From External Script 150 | -- NOTE: couldn't people just call the txaLogger:DeathNotice event??? 151 | RegisterNetEvent('txAdmin:beta:deathLog') 152 | AddEventHandler('txAdmin:beta:deathLog', function(ped) 153 | processDeath(ped) -- Remember to add a wait function before reviving into an animation. 154 | end) 155 | 156 | --[[ Thread ]]-- 157 | local deathFlag = false 158 | local IsEntityDead = IsEntityDead 159 | CreateThread(function() 160 | while true do 161 | Wait(500) 162 | local ped = PlayerPedId() 163 | local isDead = IsEntityDead(ped) 164 | if isDead and not deathFlag then 165 | deathFlag = true 166 | processDeath(ped) 167 | elseif not isDead then 168 | deathFlag = false 169 | end 170 | end 171 | end) 172 | -------------------------------------------------------------------------------- /monitor/web/public/js/codeEditor/mode/simple.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: https://codemirror.net/LICENSE 3 | 4 | (function(mod) { 5 | if (typeof exports == "object" && typeof module == "object") // CommonJS 6 | mod(require("../../lib/codemirror")); 7 | else if (typeof define == "function" && define.amd) // AMD 8 | define(["../../lib/codemirror"], mod); 9 | else // Plain browser env 10 | mod(CodeMirror); 11 | })(function(CodeMirror) { 12 | "use strict"; 13 | 14 | CodeMirror.defineSimpleMode = function(name, states) { 15 | CodeMirror.defineMode(name, function(config) { 16 | return CodeMirror.simpleMode(config, states); 17 | }); 18 | }; 19 | 20 | CodeMirror.simpleMode = function(config, states) { 21 | ensureState(states, "start"); 22 | var states_ = {}, meta = states.meta || {}, hasIndentation = false; 23 | for (var state in states) if (state != meta && states.hasOwnProperty(state)) { 24 | var list = states_[state] = [], orig = states[state]; 25 | for (var i = 0; i < orig.length; i++) { 26 | var data = orig[i]; 27 | list.push(new Rule(data, states)); 28 | if (data.indent || data.dedent) hasIndentation = true; 29 | } 30 | } 31 | var mode = { 32 | startState: function() { 33 | return {state: "start", pending: null, 34 | local: null, localState: null, 35 | indent: hasIndentation ? [] : null}; 36 | }, 37 | copyState: function(state) { 38 | var s = {state: state.state, pending: state.pending, 39 | local: state.local, localState: null, 40 | indent: state.indent && state.indent.slice(0)}; 41 | if (state.localState) 42 | s.localState = CodeMirror.copyState(state.local.mode, state.localState); 43 | if (state.stack) 44 | s.stack = state.stack.slice(0); 45 | for (var pers = state.persistentStates; pers; pers = pers.next) 46 | s.persistentStates = {mode: pers.mode, 47 | spec: pers.spec, 48 | state: pers.state == state.localState ? s.localState : CodeMirror.copyState(pers.mode, pers.state), 49 | next: s.persistentStates}; 50 | return s; 51 | }, 52 | token: tokenFunction(states_, config), 53 | innerMode: function(state) { return state.local && {mode: state.local.mode, state: state.localState}; }, 54 | indent: indentFunction(states_, meta) 55 | }; 56 | if (meta) for (var prop in meta) if (meta.hasOwnProperty(prop)) 57 | mode[prop] = meta[prop]; 58 | return mode; 59 | }; 60 | 61 | function ensureState(states, name) { 62 | if (!states.hasOwnProperty(name)) 63 | throw new Error("Undefined state " + name + " in simple mode"); 64 | } 65 | 66 | function toRegex(val, caret) { 67 | if (!val) return /(?:)/; 68 | var flags = ""; 69 | if (val instanceof RegExp) { 70 | if (val.ignoreCase) flags = "i"; 71 | val = val.source; 72 | } else { 73 | val = String(val); 74 | } 75 | return new RegExp((caret === false ? "" : "^") + "(?:" + val + ")", flags); 76 | } 77 | 78 | function asToken(val) { 79 | if (!val) return null; 80 | if (val.apply) return val 81 | if (typeof val == "string") return val.replace(/\./g, " "); 82 | var result = []; 83 | for (var i = 0; i < val.length; i++) 84 | result.push(val[i] && val[i].replace(/\./g, " ")); 85 | return result; 86 | } 87 | 88 | function Rule(data, states) { 89 | if (data.next || data.push) ensureState(states, data.next || data.push); 90 | this.regex = toRegex(data.regex); 91 | this.token = asToken(data.token); 92 | this.data = data; 93 | } 94 | 95 | function tokenFunction(states, config) { 96 | return function(stream, state) { 97 | if (state.pending) { 98 | var pend = state.pending.shift(); 99 | if (state.pending.length == 0) state.pending = null; 100 | stream.pos += pend.text.length; 101 | return pend.token; 102 | } 103 | 104 | if (state.local) { 105 | if (state.local.end && stream.match(state.local.end)) { 106 | var tok = state.local.endToken || null; 107 | state.local = state.localState = null; 108 | return tok; 109 | } else { 110 | var tok = state.local.mode.token(stream, state.localState), m; 111 | if (state.local.endScan && (m = state.local.endScan.exec(stream.current()))) 112 | stream.pos = stream.start + m.index; 113 | return tok; 114 | } 115 | } 116 | 117 | var curState = states[state.state]; 118 | for (var i = 0; i < curState.length; i++) { 119 | var rule = curState[i]; 120 | var matches = (!rule.data.sol || stream.sol()) && stream.match(rule.regex); 121 | if (matches) { 122 | if (rule.data.next) { 123 | state.state = rule.data.next; 124 | } else if (rule.data.push) { 125 | (state.stack || (state.stack = [])).push(state.state); 126 | state.state = rule.data.push; 127 | } else if (rule.data.pop && state.stack && state.stack.length) { 128 | state.state = state.stack.pop(); 129 | } 130 | 131 | if (rule.data.mode) 132 | enterLocalMode(config, state, rule.data.mode, rule.token); 133 | if (rule.data.indent) 134 | state.indent.push(stream.indentation() + config.indentUnit); 135 | if (rule.data.dedent) 136 | state.indent.pop(); 137 | var token = rule.token 138 | if (token && token.apply) token = token(matches) 139 | if (matches.length > 2 && rule.token && typeof rule.token != "string") { 140 | state.pending = []; 141 | for (var j = 2; j < matches.length; j++) 142 | if (matches[j]) 143 | state.pending.push({text: matches[j], token: rule.token[j - 1]}); 144 | stream.backUp(matches[0].length - (matches[1] ? matches[1].length : 0)); 145 | return token[0]; 146 | } else if (token && token.join) { 147 | return token[0]; 148 | } else { 149 | return token; 150 | } 151 | } 152 | } 153 | stream.next(); 154 | return null; 155 | }; 156 | } 157 | 158 | function cmp(a, b) { 159 | if (a === b) return true; 160 | if (!a || typeof a != "object" || !b || typeof b != "object") return false; 161 | var props = 0; 162 | for (var prop in a) if (a.hasOwnProperty(prop)) { 163 | if (!b.hasOwnProperty(prop) || !cmp(a[prop], b[prop])) return false; 164 | props++; 165 | } 166 | for (var prop in b) if (b.hasOwnProperty(prop)) props--; 167 | return props == 0; 168 | } 169 | 170 | function enterLocalMode(config, state, spec, token) { 171 | var pers; 172 | if (spec.persistent) for (var p = state.persistentStates; p && !pers; p = p.next) 173 | if (spec.spec ? cmp(spec.spec, p.spec) : spec.mode == p.mode) pers = p; 174 | var mode = pers ? pers.mode : spec.mode || CodeMirror.getMode(config, spec.spec); 175 | var lState = pers ? pers.state : CodeMirror.startState(mode); 176 | if (spec.persistent && !pers) 177 | state.persistentStates = {mode: mode, spec: spec.spec, state: lState, next: state.persistentStates}; 178 | 179 | state.localState = lState; 180 | state.local = {mode: mode, 181 | end: spec.end && toRegex(spec.end), 182 | endScan: spec.end && spec.forceEnd !== false && toRegex(spec.end, false), 183 | endToken: token && token.join ? token[token.length - 1] : token}; 184 | } 185 | 186 | function indexOf(val, arr) { 187 | for (var i = 0; i < arr.length; i++) if (arr[i] === val) return true; 188 | } 189 | 190 | function indentFunction(states, meta) { 191 | return function(state, textAfter, line) { 192 | if (state.local && state.local.mode.indent) 193 | return state.local.mode.indent(state.localState, textAfter, line); 194 | if (state.indent == null || state.local || meta.dontIndentStates && indexOf(state.state, meta.dontIndentStates) > -1) 195 | return CodeMirror.Pass; 196 | 197 | var pos = state.indent.length - 1, rules = states[state.state]; 198 | scan: for (;;) { 199 | for (var i = 0; i < rules.length; i++) { 200 | var rule = rules[i]; 201 | if (rule.data.dedent && rule.data.dedentIfLineStart !== false) { 202 | var m = rule.regex.exec(textAfter); 203 | if (m && m[0]) { 204 | pos--; 205 | if (rule.next || rule.push) rules = states[rule.next || rule.push]; 206 | textAfter = textAfter.slice(m[0].length); 207 | continue scan; 208 | } 209 | } 210 | } 211 | break; 212 | } 213 | return pos < 0 ? 0 : state.indent[pos]; 214 | }; 215 | } 216 | }); 217 | --------------------------------------------------------------------------------
23 | 24 | 25 |