Files
2026-03-29 21:41:17 +03:00

743 lines
24 KiB
Lua

--[[
Planned Commands
/tp -> player and coords
/tpm -> marker
/back -> go to last position after /tp
/kill id
/kick id reason
/ban id yMdhm reason
/spawn vehicle
/giveitem id item amount
/givemoney id account amount
/setmoney id account amount
/removemoney id account amount
]]
---@type table<string, table>
local registeredCommands = {}
---@type table<string, string>
local allCommands = {}
do
for _, command in ipairs(GetRegisteredCommands()) do
allCommands[command.name] = command.resource
end
end
---@param commandName string | string[]
---@param properties table | false
---@param cb function
---@param ... any
function Luxu.addCommand(commandName, properties, cb, ...)
if not cb then return end
local shouldSendCommands = type(properties) == "table"
local restricted = properties and properties.restricted or false
local params = properties and properties.params
-- Handle group permissions
if type(restricted) == "table" then
for _, group in ipairs(restricted) do
local ace = ("command.%s"):format(commandName)
if not IsPrincipalAceAllowed(group, ace) then
ExecuteCommand(("add_ace %s %s allow"):format(group, ace))
end
end
else
if not restricted then
local ace = ("command.%s"):format(commandName)
ExecuteCommand(("remove_ace builtin.everyone %s deny"):format(ace))
ExecuteCommand(("add_ace builtin.everyone %s allow"):format(ace))
end
end
-- Process parameter help text
if params then
for _, param in ipairs(params) do
if param.type and param.help then
param.help = ("%s (type: %s)"):format(param.help, param.type)
elseif param.type then
param.help = ("(type: %s)"):format(param.type)
end
end
end
local commands = type(commandName) == "table" and commandName or { commandName }
local commandHandler = function(source, args, raw)
local success, err = pcall(cb, source, args, raw)
if not success then
Citizen.Trace(("^1Command '%s' failed: %s^0"):format(raw:match("^(%S+)") or "unknown", err))
end
end
for _, cmdName in ipairs(commands) do
-- Check for command conflicts
if allCommands[cmdName] and allCommands[cmdName] ~= GetCurrentResourceName() then
--- Pre registered commands might be restricted with ace and this is incompatible with the admin menu
Luxu.print.warning(("Command '%s' is already registered by resource '%s'"):format(cmdName,
allCommands[cmdName]))
end
RegisterCommand(cmdName, commandHandler, restricted)
if shouldSendCommands then
local cmdProps = table.clone(properties)
cmdProps.name = ("/%s"):format(cmdName)
cmdProps.restricted = nil
registeredCommands[cmdName] = cmdProps
TriggerClientEvent('chat:addSuggestions', -1, cmdProps)
end
end
end
AddEventHandler("playerJoining", function()
local player = source
local suggestions = {}
for _, cmdProps in pairs(registeredCommands) do
suggestions[#suggestions + 1] = cmdProps
end
if #suggestions > 0 then
TriggerClientEvent("chat:addSuggestions", player, suggestions)
end
end)
local commands = Config.commands
local function setLastCoords(src, coords, bucket)
Player(src).state:set("luxu_admin_last_coords", { x = coords.x, y = coords.y, z = coords.z }, true)
if bucket then
Player(src).state:set("luxu_admin_last_bucket", bucket, true)
end
end
---@param duration string Duration string (e.g., "1y2M3w4d5h6m" or "0"/"-1" for permanent)
---@return number|nil durationMs Duration in milliseconds, or nil if invalid
---@return table|nil result Parsed duration components {y, M, w, d, h, m}
local function parseDurationString(duration)
if not duration then return nil, nil end
local result = { y = 0, M = 0, w = 0, d = 0, h = 0, m = 0 }
-- Check if duration is permanent
if duration == "0" or duration == "-1" then
result.y = 100 -- Permanent (100 years)
else
-- Pattern: (%d+) captures digits, (%a) captures the letter
for amount, unit in duration:gmatch("(%d+)(%a)") do
local n = tonumber(amount)
if unit == "y" then
result.y = n
elseif unit == "M" then
result.M = n
elseif unit == "w" then
result.w = n
elseif unit == "d" then
result.d = n
elseif unit == "h" then
result.h = n
elseif unit == "m" then
result.m = n
end
end
-- Validate that at least one duration component is provided
if result.y == 0 and result.M == 0 and result.w == 0 and result.d == 0 and result.h == 0 and result.m == 0 then
return nil, nil
end
end
-- Convert to milliseconds
local durationMs = result.y * 31536000000 + result.M * 2629746000 + result.w * 604800000 + result.d * 86400000 +
result.h * 3600000 + result.m * 60000
return durationMs, result
end
if Config.commands.teleport_go_back.enabled then
Luxu.addCommand(Config.commands.teleport_go_back.name, {
help = Config.commands.teleport_go_back.description,
restricted = false,
params = Config.commands.teleport_go_back.params,
}, function(src, args)
if not HasPermission(src, "self.teleport", true, true) then return end
local target = src
if args[1] and GetPlayerName(args[1]) then
target = args[1]
end
local lastCoords = Player(target).state.luxu_admin_last_coords
local lastBucket = Player(target).state.luxu_admin_last_bucket
if not lastCoords then
return
end
local currentBucket = GetPlayerRoutingBucket(tostring(target))
if lastBucket then
SetPlayerRoutingBucket(tostring(target), lastBucket)
end
local ped = GetPlayerPed(target)
local currentCoords = GetEntityCoords(ped)
RequestAnticheatBypass(target, 3)
SetEntityCoords(ped, lastCoords.x, lastCoords.y, lastCoords.z, true, false, false, false)
setLastCoords(target, currentCoords, currentBucket)
end)
end
if Config.commands.teleport_player.enabled then
Luxu.addCommand(Config.commands.teleport_player.name, {
help = Config.commands.teleport_player.description,
restricted = false,
}, function(src, args)
if not HasPermission(src, "self.teleport", true, true) then return end
local ped = GetPlayerPed(src)
local vehicle = GetVehiclePedIsIn(ped, false)
local entity = vehicle
if vehicle == 0 then
entity = ped
end
local currentCoords = GetEntityCoords(ped)
local currentBucket = GetPlayerRoutingBucket(tostring(src))
RequestAnticheatBypass(src, 5)
--- Check if first argument is a player
local target = args[1]
if GetPlayerName(target) and not args[2] then
local targetPed = GetPlayerPed(target)
local coords = GetEntityCoords(targetPed)
local targetBucket = GetPlayerRoutingBucket(tostring(target))
setLastCoords(src, currentCoords, targetBucket)
SetPlayerRoutingBucket(tostring(src), targetBucket)
SetEntityCoords(entity, coords.x, coords.y, coords.z, true, false, false, false)
return
end
if args[2] and args[3] then
local x = args[1]:gsub(",", "")
local y = args[2]:gsub(",", "")
local z = args[3]:gsub(",", "")
local coords = {
x = tonumber(x),
y = tonumber(y),
z = tonumber(z),
}
if not coords.x or not coords.y or not coords.z then
return
end
setLastCoords(src, currentCoords)
SetEntityCoords(entity, coords.x, coords.y, coords.z, true, false, false, false)
return
end
local coords = ParseCoordsString(table.concat(args, " "))
-- If we successfully extracted coordinates from either method
if coords then
setLastCoords(src, currentCoords)
SetEntityCoords(entity, coords.x, coords.y, coords.z, true, false, false, false)
if coords.w then
SetEntityHeading(entity, coords.w)
end
else
-- Handle error: Invalid coordinates format
-- TODO: Send a notification to the player about the invalid format
print(("Player %s provided invalid coordinates for tp: %s"):format(src, table.concat(args, " ")))
end
end)
end
if commands.spectate.enabled then
Luxu.addCommand(commands.spectate.name, {
help = commands.spectate.description,
restricted = false,
}, function(src, args)
if not HasPermission(src, "player.spectate", true, true) then
return
end
local target = args[1]
if not target or target == "stop" then
if Player(src).state["luxu_admin_is_spectating"] then
Luxu.triggerClientEvent("spectate:stop", src)
end
return
end
if tonumber(target) == 0 or not GetPlayerName(target) then
Luxu.notifyPlayer(src, {
description = Locales.player_not_found,
type = "error",
title = Locales.player_not_found,
duration = 4000
})
return
end
if Player(target).state["luxu_admin_is_spectating"] then
Luxu.notifyPlayer(src, {
description = Locales.this_staff_member_is_currently_spectating_another_player,
type = "error",
title = Locales.spectating_unavailable,
duration = 4000
})
return
end
local ped = target and GetPlayerPed(target)
if not ped then
return
end
local coords = GetEntityCoords(ped)
Luxu.triggerClientEvent("actions:spectate", src,
{ target = tonumber(target), coords = { x = coords.x, y = coords.y, z = coords.z } })
end)
end
if commands.open_panel.enabled then
Luxu.addCommand(commands.open_panel.name, {
help = commands.open_panel.description,
restricted = false,
}, function(src, args)
if not IsStaff(src) then return end
Luxu.triggerClientEvent("commands:open_panel", src)
end)
end
if commands.open_menu.enabled then
Luxu.addCommand(commands.open_menu.name, {
help = commands.open_menu.description,
restricted = false,
}, function(src, args)
if not HasPermission(src, "self.quickmenu", true, true) then return end
Luxu.triggerClientEvent("commands:open_menu", src)
end)
end
if commands.teleport_marker.enabled then
Luxu.addCommand(commands.teleport_marker.name, {
help = commands.teleport_marker.description,
restricted = false,
}, function(src, args)
if not HasPermission(src, "self.teleport", true, true) then return end
local ped = GetPlayerPed(src)
local currentCoords = GetEntityCoords(ped)
setLastCoords(src, currentCoords)
Luxu.triggerClientEvent("commands:tpm", src)
end)
end
if Config.reports.enabled and commands.player_report.enabled then
Luxu.addCommand(commands.player_report.name, {
help = commands.player_report.description,
restricted = false,
}, function(src, args)
Luxu.triggerClientEvent("commands:report", src)
end)
end
if commands.noclip.enabled then
Luxu.addCommand(commands.noclip.name, {
help = commands.noclip.description,
restricted = false,
}, function(src, args)
if not HasPermission(src, "self.noclip", true, true) then return end
Luxu.triggerClientEvent('actions:noclip', src);
end)
end
Luxu.addCommand("myidentifiers", {}, function(src)
local identifiers = GetPlayerIdentifiers(src)
for _, identifier in ipairs(identifiers) do
if not identifier:find("ip:") then
print(identifier)
end
end
end)
if commands.staff.enabled then
Luxu.addCommand(commands.staff.name, {
help = commands.staff.description,
restricted = false,
}, function(src)
local staffOnline = exports.luxu_admin:getOnlineStaff(true)
Luxu.triggerClientEvent("commands:staff", src, staffOnline)
end)
end
if commands.teleport_bring_player.enabled then
Luxu.addCommand(commands.teleport_bring_player.name, {
help = commands.teleport_bring_player.description,
restricted = false,
params = commands.teleport_bring_player.params,
}, function(src, args)
if not HasPermission(src, "player.teleport", true, true) then return end
local target = args[1]
-- Validate target argument
if not target then
Luxu.notifyPlayer(src, {
description = Locales.player_id,
type = "error",
title = "Invalid Arguments",
duration = 4000
})
return
end
-- Check if target player exists
if not GetPlayerName(target) then
Luxu.notifyPlayer(src, {
description = Locales.player_not_found,
type = "error",
title = Locales.player_not_found,
duration = 4000
})
return
end
-- Call the teleport_bring_player action
local success = exports[GetCurrentResourceName()]:action("teleport_bring_player", src, {
target = target
})
if success then
Luxu.notifyPlayer(src, {
description = "Player brought successfully",
type = "success",
title = Locales.success,
duration = 5000
})
else
Luxu.notifyPlayer(src, {
description = "Failed to bring player",
type = "error",
title = Locales.error,
duration = 4000
})
end
end)
end
if commands.ban.enabled then
Luxu.addCommand(commands.ban.name, {
help = commands.ban.description,
restricted = false,
params = commands.ban.params,
}, function(src, args)
if not HasPermission(src, "player.ban", true, true) then return end
local playerId = args[1]
local duration = args[2]
local reason = table.concat(args, " ", 3)
-- Validate required arguments
if not playerId then
Luxu.notifyPlayer(src, {
description = Locales.player_id,
type = "error",
title = "Invalid Arguments",
duration = 4000
})
return
end
if not reason or reason:len() < 3 then
Luxu.notifyPlayer(src, {
description = Locales.reason_must_be_at_least_3_characters,
type = "error",
title = "Invalid Reason",
duration = 4000
})
return
end
if not duration then
Luxu.notifyPlayer(src, {
description = Locales.duration_format .. ": 1y2d3h4m",
type = "error",
title = "Invalid Duration",
duration = 4000
})
return
end
local banDurationMs = parseDurationString(duration)
if not banDurationMs then
Luxu.notifyPlayer(src, {
description = Locales.invalid_duration,
type = "error",
title = "Invalid Duration Format",
duration = 4000
})
return
end
-- Check if player exists
local targetPlayer = GetPlayerName(playerId)
if not targetPlayer then
Luxu.notifyPlayer(src, {
description = Locales.player_not_found,
type = "error",
title = Locales.player_not_found,
duration = 4000
})
return
end
-- Call the ban action
local success = exports[GetCurrentResourceName()]:action("ban", src, {
target = playerId,
reason = reason,
duration = banDurationMs
})
if success then
Luxu.notifyPlayer(src, {
description = Locales.player_banned_successfully,
type = "success",
title = Locales.success,
duration = 5000
})
else
Luxu.notifyPlayer(src, {
description = Locales.failed_to_ban_player,
type = "error",
title = Locales.error,
duration = 4000
})
end
end)
end
if commands.sentence.enabled then
Luxu.addCommand(commands.sentence.name, {
help = commands.sentence.description,
restricted = false,
}, function(src)
local target = src
local targetName = GetPlayerName(target)
-- Check if target is a valid player
if not targetName then
Luxu.notifyPlayer(src, {
description = Locales.player_not_found,
type = "error",
title = Locales.player_not_found,
duration = 4000
})
return
end
local playerObj = Luxu.player.getPlayerObject(target)
if not playerObj then return end
local charId = Luxu.player.getCharId(playerObj)
-- Get jail information from database
local jailInfo = MySQL.prepare.await(
"SELECT *, UNIX_TIMESTAMP(expires_at) as expires_at, UNIX_TIMESTAMP(created_at) as created_at FROM luxu_admin_jail WHERE char_id = ? AND expires_at > NOW()",
{ charId })
if not jailInfo then
Luxu.notifyPlayer(src, {
description = Locales.you_are_not_currently_jailed,
type = "error",
title = Locales.error,
duration = 4000
})
return
end
jailInfo.expires_at = jailInfo.expires_at * 1000
jailInfo.created_at = jailInfo.created_at * 1000
-- Calculate remaining time
local currentTime = os.time() * 1000 -- Convert to milliseconds
local remainingMs = jailInfo.expires_at - currentTime
local isExpired = remainingMs <= 0
-- Send sentence information to client for display
Luxu.triggerClientEvent("commands:showSentence", src, {
target = target,
targetName = targetName,
jailInfo = jailInfo,
remainingMs = remainingMs,
isExpired = isExpired,
})
end)
end
if commands.jail and commands.jail.enabled then
Luxu.addCommand(commands.jail.name, {
help = commands.jail.description,
restricted = false,
params = commands.jail.params,
}, function(src, args)
if not HasPermission(src, "player.jail", true, true) then return end
local playerId = args[1]
local duration = args[2]
local reason = table.concat(args, " ", 3)
-- Validate required arguments
if not playerId then
Luxu.notifyPlayer(src, {
description = Locales.player_id,
type = "error",
title = "Invalid Arguments",
duration = 4000
})
return
end
if not duration then
Luxu.notifyPlayer(src, {
description = Locales.duration_format .. ": 1y2d3h4m",
type = "error",
title = "Invalid Duration",
duration = 4000
})
return
end
if not reason or reason:len() < 3 then
Luxu.notifyPlayer(src, {
description = Locales.reason_must_be_at_least_3_characters,
type = "error",
title = "Invalid Reason",
duration = 4000
})
return
end
local jailDurationMs = parseDurationString(duration)
if not jailDurationMs then
Luxu.notifyPlayer(src, {
description = Locales.invalid_duration,
type = "error",
title = "Invalid Duration Format",
duration = 4000
})
return
end
-- Check if player exists
local targetPlayer = GetPlayerName(playerId)
if not targetPlayer then
Luxu.notifyPlayer(src, {
description = Locales.player_not_found,
type = "error",
title = Locales.player_not_found,
duration = 4000
})
return
end
-- Get the first jail cell from config
local jailCell = Config.jail.jail_cells[1]
if not jailCell then
Luxu.notifyPlayer(src, {
description = "No jail cells configured",
type = "error",
title = Locales.error,
duration = 4000
})
return
end
-- Call the jail action
local success = exports[GetCurrentResourceName()]:action("jail", src, {
target = tonumber(playerId),
reason = reason,
duration = jailDurationMs,
cell = jailCell.name
})
if success then
Luxu.notifyPlayer(src, {
description = Locales.player_jailed_successfully or "Player jailed successfully",
type = "success",
title = Locales.success,
duration = 5000
})
else
Luxu.notifyPlayer(src, {
description = Locales.failed_to_jail_player or "Failed to jail player",
type = "error",
title = Locales.error,
duration = 4000
})
end
end)
end
if commands.admincar and commands.admincar.enabled then
Luxu.addCommand(commands.admincar.name, {
help = commands.jail.description,
restricted = false,
}, function(src)
if not HasPermission(src, "self.give_vehicle") then return end
local vehicle = GetVehiclePedIsIn(GetPlayerPed(src), false)
if vehicle == 0 then return end
local plate = GetVehicleNumberPlateText(vehicle)
local response = lib.callback.await("luxu_admin:client:getVehicleMods", src, false)
if not response then return end
local playerObj = Luxu.player.getPlayerObject(src)
local charId = playerObj and Luxu.player.getCharId(playerObj)
if not charId then return end
local result = false
if Framework.name == "esx" then
result = MySQL.insert.await("INSERT INTO owned_vehicles (owner,plate,vehicle) VALUES (?,?,?)",
{ charId, plate, json.encode(response.mods) })
else
local license = Framework.name == "qb" and GetPlayerIdentifierByType(src, "license") or
GetPlayerIdentifierByType(src, "license2")
result = MySQL.insert.await(
"INSERT INTO player_vehicles (citizenid, license, plate, mods, vehicle, hash) VALUES (?,?,?,?,?,?)", {
charId, license, plate, json.encode(response.mods), response.model, response.hash
})
end
if result then
Luxu.notifyPlayer(src, {
description = "Vehicle given successfully",
type = "success",
title = Locales.success,
duration = 5000
})
else
Luxu.notifyPlayer(src, {
description = "Failed to give vehicle",
type = "error",
title = Locales.error,
duration = 4000
})
end
end)
end