Odd behavior involving GUI visibility

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! :​)

Hi! This is a very interesting, and I have a couple suggestions on potential fixes, however I will make naive assumptions as I am unsure of truly the root cause (probably since it is pretty late at night for me as well!). My suggestions are as follows:

  1. My first assumption is that perhaps what if we make the state instead of true, as not [.Visible] and see if that changes the behavior. I don’t have strong rationale to why this might cause it to work, but perhaps it is worth a try.

  2. I am a little more confident in this suggestion, I haven’t thoroughly looked at your whole script but I will explain my approach: suppose you clone the guis and frames in the beginning. This means we only need to mess with its Visibility states. However, you can also make it so that you destroy the frames when they are invisible, then when the player uses the command, clone the frame again. In this case, you have a cycle and this I am fairly confident is a viable and working option. If you have cloned the objects in the beginning, I suspect you only need to use frame.Visible = not frame.Visible, and this perhaps may resolve the issue.

I apologize for not being able to provide further insight, I hope this helps!

1 Like

The first suggestion is a little ambiguous, so I will focus on the second here.

While this sounds like an interesting idea, it is approximately what I am already doing for the error message display (really just a TextButton). It already has all of its properties set (including visibility), so it only needs to be cloned into a ScreenGui and have its text set to the error message. When it is clicked, a local script that is disabled by default but enabled after cloning fires that calls the :Destroy() method on the cloned button.

The admin list does the other part of your second suggestion. It is cloned once into the GUI, then simply made visible or invisible whenever we do or don’t want the player to see it respectively.

In other words, both of those approaches have already been used, and neither of them seem to work. However, I do appreciate you taking the time to give these suggestions. We’ll see where this goes from here.

1 Like

UPDATE: As it turns out, both of these GUIs are separate problems. For the error message button, it was actually a problem in the script that destroyed it on click; it was deleting the entire ScreenGui instead of just the button. (Whoops!) For the admin list, I do not know what is going on. While the button now works as intended, the list still exhibits the weird behavior described and demonstrated in the topic post.

The problem has been solved. It ended up being a client/server issue, but instead of client changes not replicating to the server, changes on the server did not replicate to the client. This is unusual behavior, but not one without a solution; to get around the issue, I added a remote event to change the visibility on the client.

You could also clone the GUI each time you want to use it (akin to @RealCalculus’s suggestion), but that tends to use up a lot of resources. It’s ultimately easier on the server to just send a remote event each time.

Thanks for your help! I’m glad I could find a solution.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.