Starting March 18th, I have been working on a small, lightweight admin system called LiteAdmin. It contains all of the necessary amenities of an admin system (or, more appropriately, will) needed to smoothly run a Roblox game without large amounts of dead weight and unused APIs. So far, I have made a command interpreter with multiple administrative levels and a few basic commands to boot (namely admin
, admins
, kick
and ban
).
Now, while constructing the GUI for the admin list, I noticed some odd things going on with their Visible
property. You see, when the command was run once, it would work fine - the frame or button would simply be made visible - but when the GuiObject
’s Visible
property was disabled (typically via another script) and the command was run again, the object would simply refuse to be visible.
After testing, I can confirm that both GUI objects currently utilized experience this issue. However, what seems off to me is that one object is cloned with visibility already enabled, but the other is first cloned, then made visible manually. I’ve recorded a demonstration to show this strange behavior in action:
It is a bit long (about six minutes), but it is worth watching for a full demonstration of the issue.
The code for the main script is as follows:
--[[ Services ]]--
local DataStoreService = game:GetService('DataStoreService')
local Players = game:GetService('Players')
--[[ Constants ]]--
local options = require(script.Parent)
--[[ Functions ]]--
-- Utility functions
local function switch(k: any, t: {({string}) -> ()}, args: {string}, level: number): ()
if t[k] then
if t[k][1] <= level then t[k][2](args) else error('Permission denied') end
elseif t._ then t._(args) end
end
local function join(...: {any}): {any}
local table_tree: {{any}} = {...}
local combined_table: {any} = {}
for _, u in pairs(table_tree) do
for k, v in pairs(u) do
combined_table[k] = v
end
end
return combined_table
end
local function njoin(u: {any}, v: {any}): {any}
table.move(v, 1, #v, #u + 1, u)
return u
end
local function trim(t: {any}): {any}
local processed_t: {any} = {}
for _, v in ipairs(t) do
if not table.find(processed_t, v) then
table.insert(processed_t, v)
end
end
return processed_t
end
-- Specific data structure return types
local function cascadeCheck(conditions: {boolean}, last: number?): number
local result: number = #conditions + (last or 0)
for _, v in conditions do
if v then return result end
result -= 1
end
return result
end
-- Abstract getters and setters
local function getAllStoredNumberValues(dataStoreName: string): {number}
local requestedDataStore = DataStoreService:GetDataStore(options.DataStoreKey .. '_' .. dataStoreName)
local values: {number} = {}
local i: number = 1
local success: boolean = true
while success do
local valid, _ = pcall(function()
local value: number = requestedDataStore:GetAsync(tostring(i))
assert(value > 0)
if not table.find(values, value) then
table.insert(values, value)
end
end)
success = valid
i += 1
task.wait()
end
return values
end
local function setAllStoredNumberValues(dataStoreName: string, stores: {number})
local requestedDataStore = DataStoreService:GetDataStore(options.DataStoreKey .. '_' .. dataStoreName)
for i, v in ipairs(stores) do
if v > 0 then pcall(function() requestedDataStore:SetAsync(tostring(i), v) end) end
end
end
-- Unfitting, but we need to do this here to allow processCommand to access it
local admin: {number} = {}
local bans: {number} = {}
-- Process a given command from a player
local function processCommand(player: Player, message: string)
-- Should we even try to process this?
if message:sub(1, options.Prefix:len()) ~= options.Prefix then return end
-- Pattern splitting
local splitCommand: {{string}} = {}
for i in message:gmatch(`[^{options.Prefix}]+`) do -- Using backquotes to make sure everything works correctly
if i:sub(-1, -1) == ' ' then
table.insert(splitCommand, i:sub(1, -2):split(' '))
else
table.insert(splitCommand, i:split(' '))
end
end
-- Interpreting
local hinted_commands: {() -> ()} = {
admin = {2, function(args: {string})
local referenced_player: number? = Players:GetUserIdFromNameAsync(args[1])
if not referenced_player then error('Player not found') end
if table.find(admin, referenced_player) then return end
table.insert(admin, referenced_player)
setAllStoredNumberValues('Admin', admin)
end},
adminr = {2, function(args: {string})
local referenced_player: number? = Players:GetUserIdFromNameAsync(args[1])
if not referenced_player then error('Player not found') end
if not table.find(admin, referenced_player) then return end
table.remove(admin, table.find(admin, referenced_player))
setAllStoredNumberValues('Admin', admin)
end},
admins = {1, function(args: {string})
local SmallListFrame: Folder = player.PlayerGui.LiteAdmin.SmallListFrame.Content.Items
for i, v in ipairs(trim(njoin(admin, options.Admins))) do
local newListItem: TextLabel = script.SmallListItem:Clone()
newListItem.Text = string.format('%d: %s (%d)', i, Players:GetNameFromUserIdAsync(v), v)
newListItem.Parent = SmallListFrame
end
SmallListFrame.Parent.Parent.Visible = true
end},
push = {1, function(args: {string})
print(args[1] or 'Congratulations - this field is blank')
end},
kick = {1, function(args: {string})
local referenced_player: Player? = Players[args[1]]
if not referenced_player then error('Player not in game') end
if referenced_player:GetAttribute('LiteAdminLevel') <= player:GetAttribute('LiteAdminLevel') then error('Player is on same or lower level as you') end
referenced_player:Kick(args[2] or 'Kicked by an admin')
end},
ban = {1, function(args: {string})
local referenced_player: Player? = Players[args[1]]
if not referenced_player then error('Player not in game') end
if referenced_player:GetAttribute('LiteAdminLevel') <= player:GetAttribute('LiteAdminLevel') then error('Player is on same or lower level as you') end
referenced_player:Kick(args[2] or 'Banned by an admin')
setAllStoredNumberValues('Bans', bans)
end},
}
local unhinted_commands: {() -> ()} = {
_ = function(args: {string}) error('Command does not exist') end
}
for _, v in ipairs(splitCommand) do
switch(v[1], join(hinted_commands, unhinted_commands), {table.unpack(v, 2, #v)}, player:GetAttribute('LiteAdminLevel'))
end
end
--[[ Main ]]--
-- Check if player chatted
Players.PlayerAdded:Connect(function(player: Player)
player:SetAttribute('LiteAdminLevel', cascadeCheck({
player.UserId == game.CreatorId or table.find(options.Owners, player.UserId),
table.find(admin, player.UserId) or table.find(options.Admins, player.UserId)
}))
task.wait()
print(player:GetAttribute('LiteAdminLevel'))
local LiteAdminGui: ScreenGui = Instance.new('ScreenGui')
LiteAdminGui.Name = 'LiteAdmin'
LiteAdminGui.IgnoreGuiInset = true
LiteAdminGui.Enabled = true
LiteAdminGui.Parent = player.PlayerGui
script.SmallListFrame:Clone().Parent = LiteAdminGui
player.Chatted:Connect(function(message: string)
local success, err = pcall(function()
processCommand(player, message)
end)
if not success then
local ErrorMessage: TextButton = script.ErrorMessage:Clone()
ErrorMessage.Text = err
ErrorMessage.Parent = LiteAdminGui
ErrorMessage.Dismiss.Enabled = true
end
end)
end)
admin = getAllStoredNumberValues('Admin')
bans = getAllStoredNumberValues('Bans')
game:BindToClose(function()
setAllStoredNumberValues('Admin', admin)
setAllStoredNumberValues('Bans', bans)
end)
And for the config script:
return {
Prefix = ';', -- Command prefix
Owners = {}, -- List of players that will be considered "owners" (have all the rights of the creator; creators are automatically assigned these rights)
Admins = {
2375686828
}, -- List of players that will be permanently granted admin
DataStoreKey = 'LiteAdmin' -- Key used for storing LiteAdmin data (default 'LiteAdmin')
}
I’d particularly focus on the parts outlined in the video, but the issue could easily be caused by a lot of things.
I haven’t been able to find any possible solution to this issue, and I haven’t found anyone on the developer forums with the same issue. That said, it is rather late at night, so perhaps I’m missing something obvious.
Thank you! :)