mirror of
https://github.com/lifestorm/wnsrc.git
synced 2025-12-16 21:33:46 +03:00
637 lines
16 KiB
Lua
637 lines
16 KiB
Lua
--[[
|
|
| This file was obtained through the combined efforts
|
|
| of Madbluntz & Plymouth Antiquarian Society.
|
|
|
|
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
|
|
| Maloy, DrPepper10 @ RIP, Atle!
|
|
|
|
|
| Visit for more: https://plymouth.thetwilightzone.ru/
|
|
--]]
|
|
|
|
if SAM_LOADED then return end
|
|
if SERVER then return end
|
|
|
|
local sam = sam
|
|
local SUI = sam.SUI
|
|
|
|
local GetColor = SUI.GetColor
|
|
local Line = sui.TDLib.LibClasses.Line
|
|
local AnimatedSetVisible = sui.TDLib.LibClasses.AnimatedSetVisible
|
|
|
|
local RANK_NAME = SUI.CreateFont("RankName", "Roboto Bold", 18)
|
|
local RANK_INFO = SUI.CreateFont("RankInfo", "Roboto Medium", 12)
|
|
|
|
local CREATE_RANK = SUI.CreateFont("CreateRank", "Roboto Bold", 16, 200)
|
|
local RANK_TITLE = SUI.CreateFont("RankTitle", "Roboto Bold", 20)
|
|
|
|
local rank_menu = function(rank, data)
|
|
local valid = sui.valid_options()
|
|
|
|
local imm, banlim
|
|
if rank then
|
|
imm, banlim = data.immunity, data.ban_limit
|
|
end
|
|
|
|
local edit_rank = vgui.Create("SAM.QueryBox")
|
|
edit_rank:SetWide(470)
|
|
edit_rank:SetTitle(rank and string.format("Edit Rank '%s'", rank) or "Create Rank")
|
|
|
|
local new_name = rank
|
|
if not sam.ranks.is_default_rank(rank) then
|
|
local name = edit_rank:Add("SAM.LabelPanel")
|
|
name:SetLabel("Rank Name")
|
|
|
|
local entry = name:Add("SAM.TextEntry")
|
|
entry:SetSize(210, 28)
|
|
entry:SetNoBar(false)
|
|
entry:SetPlaceholder("")
|
|
entry:SetValue(rank or "")
|
|
entry:SetCheck(function(_name)
|
|
new_name = _name
|
|
|
|
if _name == rank then return end
|
|
if _name == "" or sam.ranks.is_rank(_name) then
|
|
return false
|
|
end
|
|
end)
|
|
|
|
valid.Add(entry)
|
|
end
|
|
|
|
local new_immunity = imm
|
|
do
|
|
local immunity = edit_rank:Add("SAM.LabelPanel")
|
|
immunity:SetLabel("Immunity (2~99)")
|
|
immunity:DockMargin(0, 5, 0, 0)
|
|
|
|
local entry = immunity:Add("SAM.TextEntry")
|
|
entry:SetSize(210, 28)
|
|
entry:SetNumeric(true)
|
|
entry:DisallowFloats(true)
|
|
entry:DisallowNegative(true)
|
|
entry:SetPlaceholder("")
|
|
entry:SetValue(imm or "2")
|
|
entry:SetCheck(function(_immunity)
|
|
new_immunity = _immunity
|
|
|
|
if _immunity == "" then
|
|
return false
|
|
end
|
|
|
|
_immunity = tonumber(_immunity)
|
|
new_immunity = _immunity
|
|
if _immunity < 2 or _immunity > 99 then
|
|
return false
|
|
end
|
|
end)
|
|
|
|
valid.Add(entry)
|
|
end
|
|
|
|
local new_banlimit = banlim
|
|
do
|
|
local banlimit = edit_rank:Add("SAM.LabelPanel")
|
|
banlimit:SetLabel("Ban Limit (1y 1mo 1w 1d 1h 1m)")
|
|
banlimit:DockMargin(0, 5, 0, 0)
|
|
|
|
local entry = banlimit:Add("SAM.TextEntry")
|
|
entry:SetSize(210, 28)
|
|
entry:SetNoBar(false)
|
|
entry:SetPlaceholder("")
|
|
entry:SetValue(banlim and sam.reverse_parse_length(banlim) or "2w")
|
|
entry:SetCheck(function(_banlimit)
|
|
new_banlimit = sam.parse_length(_banlimit)
|
|
if not new_banlimit and _banlimit ~= banlim then
|
|
return false
|
|
end
|
|
end)
|
|
|
|
valid.Add(entry)
|
|
end
|
|
|
|
local inherit = rank and sam.ranks.get_rank(rank).inherit or "user"
|
|
local new_inherit = inherit
|
|
do
|
|
local inherits_from = edit_rank:Add("SAM.LabelPanel")
|
|
inherits_from:SetLabel("Inherits From")
|
|
inherits_from:DockMargin(0, 5, 0, 0)
|
|
|
|
local entry = inherits_from:Add("SAM.ComboBox")
|
|
entry:SetSize(210, 28)
|
|
entry:SetValue(inherit)
|
|
|
|
for name in SortedPairsByMemberValue(sam.ranks.get_ranks(), "immunity", true) do
|
|
if name ~= rank and not sam.ranks.inherits_from(name, rank) then
|
|
entry:AddChoice(name)
|
|
end
|
|
end
|
|
|
|
function entry:OnSelect(_, value)
|
|
new_inherit = value
|
|
end
|
|
end
|
|
|
|
|
|
edit_rank:Done()
|
|
edit_rank.save:SetEnabled(true)
|
|
edit_rank.save:SetText("SAVE")
|
|
|
|
if rank then
|
|
edit_rank:SetCallback(function()
|
|
local to_run = {}
|
|
|
|
if new_immunity ~= imm then
|
|
table.insert(to_run, {"changerankimmunity", rank, new_immunity})
|
|
end
|
|
|
|
if new_banlimit ~= banlim then
|
|
table.insert(to_run, {"changerankbanlimit", rank, new_banlimit})
|
|
end
|
|
|
|
if new_inherit ~= inherit then
|
|
table.insert(to_run, {"changeinherit", rank, new_inherit})
|
|
end
|
|
|
|
if new_name ~= rank then
|
|
table.insert(to_run, {"renamerank", rank, new_name})
|
|
end
|
|
sam.command.run_commands(to_run)
|
|
end)
|
|
else
|
|
edit_rank:SetCallback(function()
|
|
RunConsoleCommand("sam", "addrank", new_name, new_inherit, new_immunity, new_banlimit)
|
|
end)
|
|
end
|
|
|
|
function edit_rank.save:Think()
|
|
self:SetEnabled(valid.IsValid())
|
|
end
|
|
end
|
|
|
|
sam.menu.add_tab("https://raw.githubusercontent.com/Srlion/Addons-Data/main/icons/sam/military_rank.png", function(column_sheet)
|
|
local current_rank
|
|
|
|
local parent = column_sheet:Add("Panel")
|
|
parent:Dock(FILL)
|
|
parent:DockMargin(0, 1, 0, 0)
|
|
|
|
local title = parent:Add("SAM.Label")
|
|
title:Dock(TOP)
|
|
title:DockMargin(10, 10, 0, 0)
|
|
title:SetFont(SAM_TAB_TITLE_FONT)
|
|
title:SetText("Ranks")
|
|
title:SetTextColor(GetColor("menu_tabs_title"))
|
|
title:SizeToContents()
|
|
|
|
local total = parent:Add("SAM.Label")
|
|
total:Dock(TOP)
|
|
total:DockMargin(10, 6, 0, 0)
|
|
total:SetFont(SAM_TAB_DESC_FONT)
|
|
total:SetText(table.Count(sam.ranks.get_ranks()) .. " total ranks")
|
|
total:SetTextColor(GetColor("menu_tabs_title"))
|
|
total:SizeToContents()
|
|
|
|
local search_entry
|
|
do
|
|
local container = parent:Add("SAM.Panel")
|
|
container:Dock(TOP)
|
|
container:DockMargin(10, 6, 10, SUI.Scale(15))
|
|
container:SetTall(30)
|
|
|
|
search_entry = container:Add("SAM.TextEntry")
|
|
search_entry:Dock(LEFT)
|
|
search_entry:SetNoBar(true)
|
|
search_entry:SetPlaceholder("Search...")
|
|
search_entry:SetRadius(4)
|
|
search_entry:SetWide(220)
|
|
end
|
|
|
|
local create_rank = parent:Add("SAM.Button")
|
|
create_rank:SetFont(CREATE_RANK)
|
|
create_rank:SetText("Create Rank")
|
|
create_rank:Dock(BOTTOM)
|
|
create_rank:DockMargin(10, 0, 10, 10)
|
|
|
|
create_rank:On("DoClick", function()
|
|
rank_menu()
|
|
end)
|
|
|
|
local right_body = parent:Add("Panel")
|
|
right_body:Dock(RIGHT)
|
|
right_body:DockMargin(0, 5, 10, 10)
|
|
right_body:SetWide(0)
|
|
right_body:SetZPos(-1)
|
|
|
|
local rank_title = right_body:Add("SAM.Label")
|
|
rank_title:Dock(TOP)
|
|
rank_title:DockMargin(0, 0, 0, 5)
|
|
rank_title:SetFont(RANK_TITLE)
|
|
rank_title:SetTextColor(GetColor("menu_tabs_title"))
|
|
|
|
local permissions_body = right_body:Add("SAM.CollapseCategory")
|
|
permissions_body:Dock(FILL)
|
|
permissions_body:GetCanvas():DockPadding(0, 0, 5, 0)
|
|
|
|
local function refresh_access()
|
|
if not IsValid(current_rank) then return end
|
|
|
|
for k, v in ipairs(permissions_body.items) do
|
|
AnimatedSetVisible(v.img, sam.ranks.has_permission(current_rank.name, v.name))
|
|
end
|
|
end
|
|
|
|
for k, v in ipairs({"SAM.ChangedInheritRank", "SAM.RankPermissionGiven", "SAM.RankPermissionTaken"}) do
|
|
hook.Add(v, "SAM.Menu.RefreshPermissions ", refresh_access)
|
|
end
|
|
|
|
local function sortFunc(a, b)
|
|
if (a.category == b.category) then return a.name < b.name end
|
|
|
|
return a.category < b.category
|
|
end
|
|
|
|
local function refresh_permissions()
|
|
permissions_body:GetCanvas():Clear()
|
|
table.Empty(permissions_body.items)
|
|
table.Empty(permissions_body.categories)
|
|
|
|
local item_click = function(s)
|
|
local rank = current_rank.name
|
|
if not sam.ranks.has_permission(rank, s.name) then
|
|
RunConsoleCommand("sam", "givepermission", rank, s.name)
|
|
else
|
|
RunConsoleCommand("sam", "takepermission", rank, s.name)
|
|
end
|
|
end
|
|
|
|
local permissions = sam.permissions.get()
|
|
table.sort(permissions, sortFunc)
|
|
for k, v in ipairs(permissions) do
|
|
local item = permissions_body:add_item(v.name, v.category)
|
|
item:SetContentAlignment(4)
|
|
item:SetTextInset(6, 0)
|
|
item:SizeToContentsY(SUI.Scale(10))
|
|
item:SetZPos(k)
|
|
item.name = v.name
|
|
item.DoClick = item_click
|
|
|
|
local img = item:Add("SAM.Image")
|
|
img:Dock(RIGHT)
|
|
img:DockMargin(4, 4, 4, 4)
|
|
img:InvalidateParent(true)
|
|
img:SetWide(img:GetTall())
|
|
img:SetImageColor(Color(52, 161, 224))
|
|
img:SetImage("https://raw.githubusercontent.com/Srlion/Addons-Data/main/icons/sam/check_mark.png")
|
|
|
|
item.img = img
|
|
end
|
|
end
|
|
|
|
local limits_body
|
|
|
|
do
|
|
local permissions_search = right_body:Add("SAM.TextEntry")
|
|
permissions_search:Dock(TOP)
|
|
permissions_search:DockMargin(0, 0, 5, 10)
|
|
permissions_search:SetNoBar(true)
|
|
permissions_search:SetPlaceholder("Search...")
|
|
permissions_search:SetRadius(4)
|
|
permissions_search:SetTall(30)
|
|
|
|
function permissions_search:OnValueChange(text)
|
|
if limits_body and limits_body:IsVisible() then
|
|
local children = limits_body:GetCanvas():GetChildren()
|
|
for k, v in ipairs(children) do
|
|
v:AnimatedSetVisible(v.title:find(text, nil, true) ~= nil)
|
|
end
|
|
limits_body:InvalidateLayout(true)
|
|
else
|
|
permissions_body:Search(text)
|
|
end
|
|
end
|
|
|
|
Line(right_body):SetZPos(2)
|
|
end
|
|
|
|
local function load_limits()
|
|
if sam.limit_types then
|
|
if limits_body then return end
|
|
else
|
|
if limits_body then
|
|
limits_body:SetVisible(false)
|
|
permissions_body:AnimatedSetVisible(true)
|
|
limits_body:Remove()
|
|
limits_body = nil
|
|
end
|
|
return
|
|
end
|
|
|
|
limits_body = right_body:Add("SAM.ScrollPanel")
|
|
limits_body:Dock(FILL)
|
|
limits_body:GetCanvas():DockPadding(0, 0, 5, 0)
|
|
limits_body:SetVisible(false)
|
|
|
|
local item_enter = function(s)
|
|
if not IsValid(current_rank) then return end
|
|
|
|
local rank = current_rank.name
|
|
|
|
local limit = math.Clamp(s:GetValue(), -1, 1000)
|
|
if limit ~= sam.ranks.get_limit(rank, s.limit_type) then
|
|
RunConsoleCommand("sam", "changeranklimit", rank, s.limit_type, limit)
|
|
else
|
|
s:SetText(tostring(sam.ranks.get_limit(rank, s.limit_type)))
|
|
end
|
|
end
|
|
|
|
local not_empty = function(s)
|
|
return s and s ~= ""
|
|
end
|
|
|
|
local limit_values = {}
|
|
for k, v in ipairs(sam.limit_types) do
|
|
local immunity = limits_body:Add("SAM.LabelPanel")
|
|
immunity:SetLabel(v)
|
|
immunity:DockMargin(5, 0, 0, 5)
|
|
|
|
local entry = immunity:Add("SAM.TextEntry")
|
|
entry:SetSize(60, 26)
|
|
entry:SetNumeric(true)
|
|
entry:DisallowFloats(true)
|
|
entry:SetPlaceholder("")
|
|
entry:SetCheck(not_empty)
|
|
entry.limit_type = v
|
|
entry.OnEnter = item_enter
|
|
|
|
table.insert(limit_values, entry)
|
|
end
|
|
|
|
function limits_body:Refresh()
|
|
if not IsValid(current_rank) then return end
|
|
|
|
local rank = current_rank.name
|
|
for k, v in ipairs(limit_values) do
|
|
v:SetValue(tostring(sam.ranks.get_limit(rank, v.limit_type)))
|
|
end
|
|
end
|
|
|
|
local right_current_rank = right_body:Add("SAM.Button")
|
|
right_current_rank:Dock(BOTTOM)
|
|
right_current_rank:DockMargin(0, 5, 0, 0)
|
|
right_current_rank:SetFont(CREATE_RANK)
|
|
right_current_rank:SetText("Switch to Limits")
|
|
right_current_rank:On("DoClick", function()
|
|
limits_body:AnimatedToggleVisible()
|
|
permissions_body:AnimatedToggleVisible()
|
|
|
|
if permissions_body:AnimatedIsVisible() then
|
|
right_current_rank:SetText("Switch to Limits")
|
|
else
|
|
right_current_rank:SetText("Switch to Permissions")
|
|
end
|
|
end)
|
|
|
|
limits_body:On("OnRemove", function()
|
|
right_current_rank:Remove()
|
|
end)
|
|
limits_body:Refresh()
|
|
end
|
|
|
|
local function refresh_all()
|
|
timer.Create("SAM.Menu.Ranks.Refresh", 1, 1, function()
|
|
load_limits()
|
|
refresh_permissions()
|
|
refresh_access()
|
|
end)
|
|
end
|
|
|
|
sam.config.hook({"Restrictions.Limits"}, refresh_all)
|
|
|
|
for k, v in ipairs({"SAM.AddedPermission", "SAM.PermissionModified", "SAM.RemovedPermission"}) do
|
|
hook.Add(v, "SAM.Menu.RefreshPermissions", refresh_all)
|
|
end
|
|
|
|
local body = parent:Add("SAM.ScrollPanel")
|
|
body:Dock(FILL)
|
|
body:DockMargin(10, 0, 5, 10)
|
|
body:SetVBarPadding(6)
|
|
|
|
body:Line():SetZPos(-101)
|
|
|
|
local select_rank = function(s)
|
|
if not IsValid(s) then
|
|
current_rank = nil
|
|
right_body:SizeTo(0, -1, 0.3)
|
|
return
|
|
end
|
|
|
|
if IsValid(current_rank) then
|
|
current_rank.Selected = false
|
|
|
|
if current_rank == s then
|
|
current_rank = nil
|
|
right_body:SizeTo(0, -1, 0.3)
|
|
return
|
|
end
|
|
end
|
|
|
|
s.Selected = true
|
|
current_rank = s
|
|
refresh_access()
|
|
if limits_body then
|
|
limits_body:Refresh()
|
|
end
|
|
right_body:SizeTo(SUI.Scale(300), -1, 0.3)
|
|
|
|
rank_title:SetText(s.name)
|
|
rank_title:SizeToContents()
|
|
end
|
|
|
|
local ranks = {}
|
|
|
|
function search_entry:OnValueChange()
|
|
local value = self:GetValue()
|
|
for k, v in pairs(ranks) do
|
|
local show = k:find(value, nil, true)
|
|
show = show ~= nil
|
|
v.line:AnimatedSetVisible(show)
|
|
v:GetParent():AnimatedSetVisible(show)
|
|
end
|
|
end
|
|
|
|
local add_rank = function(rank_name, data)
|
|
if rank_name == "superadmin" then return end
|
|
if not IsValid(body) then return end
|
|
|
|
local line = body:Add("SAM.Panel")
|
|
line:Dock(TOP)
|
|
line:DockMargin(0, 0, 0, 10)
|
|
line:SetTall(34)
|
|
line:SetZPos(-data.immunity)
|
|
line:InvalidateLayout(true)
|
|
|
|
local container = line:Add("SAM.Button")
|
|
container:Dock(FILL)
|
|
container:DockMargin(0, 0, 5, 0)
|
|
container:DockPadding(5, 5, 0, 5)
|
|
container:SetText("")
|
|
container:SetContained(false)
|
|
container.name = rank_name
|
|
|
|
ranks[rank_name] = container
|
|
|
|
container:On("DoClick", select_rank)
|
|
|
|
function container:DoRightClick()
|
|
rank_name = container.name
|
|
|
|
if rank_name == "user" then return end
|
|
|
|
local dmenu = vgui.Create("SAM.Menu")
|
|
dmenu:SetSize(w, h)
|
|
dmenu:SetInternal(container)
|
|
|
|
dmenu:AddOption("Edit Rank", function()
|
|
rank_menu(rank_name, sam.ranks.get_rank(rank_name))
|
|
end)
|
|
|
|
if not sam.ranks.is_default_rank(rank_name) then
|
|
dmenu:AddSpacer()
|
|
|
|
dmenu:AddOption("Remove Rank", function()
|
|
local remove_rank = vgui.Create("SAM.QueryBox")
|
|
remove_rank:SetWide(350)
|
|
|
|
local check = remove_rank:Add("SAM.Label")
|
|
check:SetText("Are you sure that you want to remove '" .. rank_name .. "'?")
|
|
check:SetFont("SAMLine")
|
|
check:SetWrap(true)
|
|
check:SetAutoStretchVertical(true)
|
|
|
|
remove_rank:Done()
|
|
remove_rank.save:SetEnabled(true)
|
|
remove_rank.save:SetText("REMOVE")
|
|
remove_rank.save:SetContained(false)
|
|
remove_rank.save:SetColors(GetColor("query_box_cancel"), GetColor("query_box_cancel_text"))
|
|
|
|
remove_rank.cancel:SetContained(true)
|
|
remove_rank.cancel:SetColors()
|
|
|
|
remove_rank:SetCallback(function()
|
|
RunConsoleCommand("sam", "removerank", rank_name)
|
|
end)
|
|
end)
|
|
end
|
|
|
|
dmenu:Open()
|
|
dmenu:SetPos(input.GetCursorPos())
|
|
end
|
|
|
|
do
|
|
local name = container:Add("SAM.Label")
|
|
name:Dock(TOP)
|
|
name:DockMargin(0, 0, 0, 2)
|
|
name:SetTextColor(GetColor("player_list_names"))
|
|
name:SetFont(RANK_NAME)
|
|
name:SetText(rank_name)
|
|
name:SizeToContents()
|
|
|
|
local immunity = container:Add("SAM.Label")
|
|
immunity:Dock(TOP)
|
|
immunity:SetTextColor(GetColor("player_list_steamid"))
|
|
immunity:SetFont(RANK_INFO)
|
|
immunity:SetText("Immunity: " .. data.immunity)
|
|
immunity:SizeToContents()
|
|
|
|
local banlimit = container:Add("SAM.Label")
|
|
banlimit:Dock(TOP)
|
|
banlimit:SetTextColor(GetColor("player_list_steamid"))
|
|
banlimit:SetFont(RANK_INFO)
|
|
banlimit:SetText("Ban limit: " .. sam.reverse_parse_length(sam.parse_length(data.ban_limit)))
|
|
banlimit:SizeToContents()
|
|
|
|
local inherit = container:Add("SAM.Label")
|
|
inherit:Dock(TOP)
|
|
inherit:SetTextColor(GetColor("player_list_steamid"))
|
|
inherit:SetFont(RANK_INFO)
|
|
inherit:SetText("Inherits from: " .. (sam.isstring(data.inherit) and data.inherit or "none"))
|
|
inherit:SizeToContents()
|
|
end
|
|
|
|
container:InvalidateLayout(true)
|
|
container:SizeToChildren(false, true)
|
|
line:SizeToChildren(false, true)
|
|
|
|
local _line = body:Line()
|
|
_line:SetZPos(-data.immunity)
|
|
|
|
container.line = _line
|
|
container.data = data
|
|
end
|
|
|
|
for rank_name, v in pairs(sam.ranks.get_ranks()) do
|
|
add_rank(rank_name, v)
|
|
end
|
|
|
|
hook.Add("SAM.AddedRank", "SAM.RefreshRanksList", function(name, rank)
|
|
add_rank(name, rank)
|
|
end)
|
|
|
|
hook.Add("SAM.RemovedRank", "SAM.RefreshRanksList", function(name)
|
|
local line = ranks[name]
|
|
if not IsValid(line) then return end
|
|
|
|
line.line:Remove()
|
|
line:GetParent():Remove()
|
|
ranks[name] = nil
|
|
|
|
if line == current_rank then
|
|
select_rank()
|
|
end
|
|
end)
|
|
|
|
-- This is just better than caching panels for stuff that ain't gonna be called a lot
|
|
hook.Add("SAM.RankNameChanged", "SAM.RefreshRanksList", function(name, new_name)
|
|
local line = ranks[name]
|
|
if not IsValid(line) then return end
|
|
|
|
-- if current_rank == name then
|
|
-- rank_name:SetText(new_name)
|
|
-- end
|
|
|
|
line:GetChildren()[1]:SetText(new_name)
|
|
|
|
ranks[new_name], ranks[name] = line, nil
|
|
line.name = new_name
|
|
end)
|
|
|
|
hook.Add("SAM.RankImmunityChanged", "SAM.RefreshRanksList", function(name, immunity)
|
|
local line = ranks[name]
|
|
if not IsValid(line) then return end
|
|
|
|
line:GetChildren()[2]:SetText("Immunity: " .. immunity)
|
|
line:GetParent():SetZPos(-immunity)
|
|
|
|
-- SetZPos is kinda weird to deal with
|
|
line.line:SetZPos(-immunity + 1)
|
|
line.line:SetZPos(-immunity)
|
|
end)
|
|
|
|
hook.Add("SAM.RankBanLimitChanged", "SAM.RefreshRanksList", function(name, new_limit)
|
|
local line = ranks[name]
|
|
if IsValid(line) then
|
|
line:GetChildren()[3]:SetText("Ban limit: " .. sam.reverse_parse_length(new_limit))
|
|
end
|
|
end)
|
|
|
|
hook.Add("SAM.ChangedInheritRank", "SAM.RefreshRanksList", function(name, new_inherit)
|
|
local line = ranks[name]
|
|
if IsValid(line) then
|
|
line:GetChildren()[4]:SetText("Inherits from: " .. new_inherit)
|
|
end
|
|
end)
|
|
|
|
return parent
|
|
end, function()
|
|
return LocalPlayer():HasPermission("manage_ranks")
|
|
end, 3) |