User settings, how?

Thanks for the color theme.

You kinda got it wrong. You dont need to fire it twice. I’m guessing since your ModuleScript is in the same folder as your LocalScript, the module is executed on the client, so you dont need to use a RemoteEvent between two clientside scripts, use a BindableEvent for that. You generally use RemoteEvents from client to server or vice versa.

1 Like

Yeah, that’s right, except if you want to you can use self instead of playerSettings in the module functions. The local script detects changes because in the :UpdateSetting function, you run playerSettings.SettingChanged, which is assigned to by the LocalScript, which basically means whenever you change a setting that function is run which is where you apply changes.

Don’t do that, you should probably only verify the information on the server when it receives it, don’t bother getting the client to fix it because exploiters can just change it again and stop that remote being fired.

Apply the changes in the LocalScript to get rid of the need for a bindable.

(also, please format your code using backticks! It makes it much easier to read! You can use the ‘</>’ button on your post editor if you don’t know how to do the backticks.)

dear god.

uhm,

self? instead of playerSettings?

…like, you want me to do;

function playerSettings:GetSetting(name: string)
   return self[name] -- ?
end

instead of what i currently have?

sorry i’ve never really used self before :face_with_spiral_eyes:

yeah that works, it’s kind of optional.

Basically, self just refers to the player settings module. It’s because you used a colon to define it, which means self was automatically defined.

Here’s a post about it:
What is self and how can I use it? - Help and Feedback / Scripting Support - Developer Forum | Roblox

hm, okay

but what about


how exactly can i apply the changes if the local script has to detect them (which as of writing this currently doesn’t exist, unless it does and i’m just stupid)

isn’t that what a bindable event should be used for in this case?

send data and update it in the local script

Your LocalScript updates it and it receives the information through the callback.

  1. We set up the settings with no callback associated.
  2. LocalScript assigns the callback (playerSettings.SettingChanged = function)
  3. When setting is changed through :UpdateSetting, playerSettings.SettingChanged is run which is that function in your LocalScript
  4. Parameters like the setting and it’s new value help apply the change by identifying the setting
  5. Change is carries out within that function
  6. Yay, the setting has been updated!

The callback here is the function you assigned to playerSettings.SettingChanged.

so it really is that simple

okay i’m gonna write some code and go through bajillion errors
but i will keep you updated

1 Like

SORRY BUT I NEED TO ASK THIS @12345koip

for settings that are just simple booleans, do i set some attributes and then read those in other scripts when needed, or should i read from the settings (probably not optimal) directly?

You don’t need to use attributes, you can just require the settings module and use :GetSetting (sometimes outdated if module table itself doesnt read it which is why :GetSetting is needed)

1 Like

okay just to be sure i am not writing things pointlessly and stupidly now

is there a way you could check if i did anything wrong? :point_right: :point_left:


local script

local rStorage = game:GetService("ReplicatedStorage")

-- Player variables;
local plr = game:GetService("Players").LocalPlayer
local plrGui = plr.PlayerGui

-- Path to event;
local eventsFolder = rStorage:WaitForChild("Events")
local fromClient = eventsFolder:WaitForChild("FromClient")
local functionalityEvents = fromClient:WaitForChild("Functionality")

-- Event;
local sendPlayerSettings = functionalityEvents.SendPlayerSettings

-- Path to module;
local plrScripts = plr.PlayerScripts
local settingsScriptsFolder = plrScripts:WaitForChild("Settings")

-- Module;
local playerSettings = require(settingsScriptsFolder.UserSettingsModule)


-- Functions;
playerSettings.SettingChanged = function(setting: string, value: any)
	if not playerSettings[setting] then
		warn("Setting doesn't exist lol " .. setting)
		return
	end
	
	if setting == "Field of View" then
		-- This should hopefully work without issues
		if value > 110 then value = 110 end
		print("Field of view changed to: " .. value)
		
	elseif setting == "Classic Aim Down Sights" then
		-- Need to apply some sort of value here
		print("The value of " .. setting .. " is now " .. value)
		
	elseif setting == "Vitals GUI Color" then
		-- Now we loop through the screen guis and find 'PlayerHUD'
		-- Indentations and nested for loops warning :(
		for _,element in plrGui:GetChildren() do
			if element.Name ~= "PlayerHUD" then continue end
			
			-- Then we loop through the descendants of PlayerHUD and
			-- Recolor the image labels to the color given
			for _, toRecolor in element:GetDescendants() do
				if not toRecolor:IsA("ImageLabel") then continue end
				
				toRecolor.ImageColor3 = value
			end
		end
		
	elseif setting == "Weapon GUI Color" then
		-- Same as in vitals GUI recoloring
		for _, element in plrGui:GetChildren() do
			if element.Name ~= "Weapons" then
				
				for _, toRecolor in element:GetDescendants() do
					if not toRecolor:IsA("ImageLabel") then continue end
					
					toRecolor.ImageColor3 = value
				end
			end
		end
		
	elseif setting == "Visible Viewmodel" then
		print("The value of " .. setting .. " is now " .. value)
		
	elseif setting == "Show Crosshair" then
		-- Same as in viewmodel and ads, some value
		print("The value of " .. setting .. " is now " .. value)
	end
	
	-- After the clusterbomb of elseif statements, it is time to
	-- send the setting and the value to the server
	sendPlayerSettings:FireServer(setting, value)
end

server script (very barebones)

local rStorage = game:GetService("ReplicatedStorage")
local players = game:GetService("Players")

-- Path to event;
local eventsFolder = rStorage:WaitForChild("Events")
local fromClient = eventsFolder:WaitForChild("FromClient")
local functionalityEvents = fromClient:WaitForChild("Functionality")

-- Event;
local sendPlayerSettings = functionalityEvents.SendPlayerSettings

-- Table to store player data in (CURRENT SERVER ONLY, NOT DATASTORE)
local playerSettingsTable = {}

-- Valid settings (for safety purposes)
local validSettings = {
	"Field of View",
	"Classic Aim Down Sights",
	"Vitals GUI Color",
	"Weapon GUI Color",
	"Visible Viewmodel",
	"Show Crosshair",
}

-- Functions;
function OnEventReceived(plr: Player, setting: string, value: any)
	if not table.find(validSettings, setting) then plr:Kick("Don't do that") return end
	
	if not playerSettingsTable[plr.UserId] then
		playerSettingsTable[plr.UserId] = {}
	end
	
	-- Update that value!
	playerSettingsTable[plr.UserId][setting] = value
	
	-- Some sort of warning that's here specifically for debugging :^)
	warn(plr.Name .. " changed " .. setting .. " to " .. tostring(value))
end

Looks good in terms of syntax, I didn’t see any logic errors going through it but you might wanna double check that.

1 Like

hi!!!

9am notsad wrote some code

i think this should work flawlessly

now i just need to figure out need your help on actually applying the settings whenever the player joins (i can save the totalKills and stuff since they’re values, not tables)

:point_right: :point_left:


server-sided code as that is the only thing that changed

-- Nothing here gets applied, it only gets saved
-- Except for settings of course!

local rStorage = game:GetService("ReplicatedStorage")
local dStoreService = game:GetService("DataStoreService")
local players = game:GetService("Players")

-- Path to events;
local eventsFolder = rStorage:WaitForChild("Events")
local fromClient = eventsFolder:WaitForChild("FromClient")
local functionalityEvents = fromClient:WaitForChild("Functionality")

-- Event;
local sendPlayerSettings = functionalityEvents.SendPlayerSettings

-- Table to store player data in (CURRENT SERVER ONLY, NOT DATASTORE)
local playerSettingsTable = {}

-- Valid settings (for safety purposes)
local validSettings = {
	"Field of View",
	"Classic Aim Down Sights",
	"Vitals GUI Color",
	"Weapon GUI Color",
	"Visible Viewmodel",
	"Show Crosshair",
}

-- Datastores;
local playerDataStorage = dStoreService:GetDataStore("Player_Data")

-- Functions;
function GetOrCreateAttribute(plr: Player, attributeName: string, defaultValue: any)
	if plr:GetAttribute(attributeName) == nil then
		plr:SetAttribute(attributeName, defaultValue)
	end
	
	return plr:GetAttribute(attributeName)
end

function InitializePlayerData(plr: Player)
	local totalKills = GetOrCreateAttribute(plr, "TotalKills", 0)
	local totalDeaths = GetOrCreateAttribute(plr, "TotalDeaths", 0)
	local totalCurrencyEarned = GetOrCreateAttribute(plr, "TotalCurrencyEarned", 0)
	local totalXpEarned = GetOrCreateAttribute(plr, "TotalXpEarned", 0)
	local currentLevel = GetOrCreateAttribute(plr, "CurrentLevel", 1)
	local currentWhisper = GetOrCreateAttribute(plr, "CurrentWhisper", 0) 
	-- 'Whisper' is basically a rebirth, but with an edgy name
	-- so think of 'Whisper' as 'Rebirth' if you find it easier
	
	
	if not playerSettingsTable[plr.UserId] then
		-- Set data;
		playerSettingsTable[plr.UserId] = {
			TotalKills = totalKills,
			TotalDeaths = totalDeaths,
			TotalCurrencyEarned = totalCurrencyEarned,
			TotalXpEarned = totalXpEarned,
			CurrentLevel = currentLevel,
			CurrentWhisper = currentWhisper,
			Settings = {} -- Settings is an empty table
			-- That gets filled with the settings
			-- inside the OnSettingsReceived function
		}
	end
end

function OnPlayerJoined(plr: Player)
	InitializePlayerData()
end

function OnSettingsReceived(plr: Player, setting: string, value: any)
	if not table.find(validSettings, setting) then plr:Kick("Don't do that") return end
	if value == value then plr:Kick("Ummm you didn't change the value?") return end
	
	if not playerSettingsTable[plr.UserId] then
		-- If player data isn't found, then initialize it;
		InitializePlayerData(plr)
	end
	
	-- Update that value!
	playerSettingsTable[plr.UserId].Settings[setting] = value
	
	-- Some sort of warning that's here specifically for debugging :^)
	warn(plr.Name .. " changed " .. setting .. " to " .. tostring(value))
	warn("Current settings of " .. plr.Name .. " are: ")
	print(playerSettingsTable[plr.UserId].Settings)
end


function AutosaveInIntervals()
	coroutine.wrap(function()
		while true do
			local interval = math.random(300, 600) -- Random interval between 5 to 10 minutes
			-- Probably the best choice for security :^)
			task.wait(interval)
			
			for userId, data in pairs(playerSettingsTable) do
				local success, result = pcall(function()
					local key = tostring(userId)
					playerDataStorage:SetAsync(key, data)
				end)
				
				if not success then
					warn("Failed to save " .. userId .. "'s data!")
					warn("Error information: ")
					warn(result)
					continue -- Skips to the next iteration so that print 
					-- below doesn't run 
				end
				
				print("Successfully saved data for " .. userId)
			end
		end
	end)()
end

-- Runtime;
sendPlayerSettings.OnServerEvent:Connect(OnSettingsReceived)
players.PlayerAdded:Connect(OnPlayerJoined)
AutosaveInIntervals()

looking at the code around 4 hours later
this won’t work properly, sigh

EDIT 1 HOUR LATER

i think it’s going to work now!!

-- Nothing here gets applied, it only gets saved
-- Except for settings of course!

local rStorage = game:GetService("ReplicatedStorage")
local dStoreService = game:GetService("DataStoreService")
local players = game:GetService("Players")

-- Path to events;
local eventsFolder = rStorage:WaitForChild("Events")
local fromClient = eventsFolder:WaitForChild("FromClient")
local functionalityEvents = fromClient:WaitForChild("Functionality")

-- Event;
local sendPlayerSettings = functionalityEvents.SendPlayerSettings

-- Table to store player data in (CURRENT SERVER ONLY, NOT DATASTORE)
local playerSettingsTable = {}

-- Valid settings (for safety purposes)
local validSettings = {
	"Field of View",
	"Classic Aim Down Sights",
	"Vitals GUI Color",
	"Weapon GUI Color",
	"Visible Viewmodel",
	"Show Crosshair",
}

-- Datastores;
local playerDataStorage = dStoreService:GetDataStore("Player_Data")

-- Misc values;
local previousValue = nil

-- Functions;
function GetOrCreateAttribute(plr: Player, attributeName: string, defaultValue: any)
	if plr:GetAttribute(attributeName) == nil then
		plr:SetAttribute(attributeName, defaultValue)
	end

	return plr:GetAttribute(attributeName)
end

function InitializePlayerData(plr: Player)
	local totalKills = GetOrCreateAttribute(plr, "TotalKills", 0)
	local totalDeaths = GetOrCreateAttribute(plr, "TotalDeaths", 0)
	local totalCurrencyEarned = GetOrCreateAttribute(plr, "TotalCurrencyEarned", 0)
	local totalXpEarned = GetOrCreateAttribute(plr, "TotalXpEarned", 0)
	local currentLevel = GetOrCreateAttribute(plr, "CurrentLevel", 1)
	local currentWhisper = GetOrCreateAttribute(plr, "CurrentWhisper", 0) 
	-- 'Whisper' is basically a rebirth, but with an edgy name
	-- so think of 'Whisper' as 'Rebirth' if you find it easier

	local success, result = pcall(function()
		return playerDataStorage:GetAsync(plr.UserId)
	end)

	if not success then
		warn("Failed to fetch " .. plr.UserId .. "'s data!")
		warn("Error log: " .. result)

		result = nil
	end


	if not playerSettingsTable[plr.UserId] then
		-- Set data;
		playerSettingsTable[plr.UserId] = {
			TotalKills = totalKills,
			TotalDeaths = totalDeaths,
			TotalCurrencyEarned = totalCurrencyEarned,
			TotalXpEarned = totalXpEarned,
			CurrentLevel = currentLevel,
			CurrentWhisper = currentWhisper,
			Settings = {
				-- Default values
				["Field of View"] = 90, 
				["Classic Aim Down Sights"] = false,
				["Vitals GUI Color"] = Color3.fromRGB(255,255,255),
				["Weapon GUI Color"] = Color3.fromRGB(255,255,255),
				["Visible Viewmodel"] = true,
				["Show Crosshair"] = true,
			} 
		}
	end

	if result then
		playerSettingsTable[plr.UserId].TotalKills = result.TotalKills or totalKills
		playerSettingsTable[plr.UserId].TotalDeaths = result.TotalDeaths or totalDeaths
		playerSettingsTable[plr.UserId].TotalCurrencyEarned = result.TotalCurrencyEarned or totalCurrencyEarned
		playerSettingsTable[plr.UserId].TotalXpEarned = result.TotalXpEarned or totalXpEarned
		playerSettingsTable[plr.UserId].CurrentLevel = result.CurrentLevel or currentLevel
		playerSettingsTable[plr.UserId].CurrentWhisper = result.CurrentWhisper or currentWhisper

		if not result.Settings then return end

		for setting, value in pairs(result.Settings) do
			playerSettingsTable[plr.UserId].Settings[setting] = value
		end
	end
end

function OnPlayerJoined(plr: Player)
	InitializePlayerData()
end

function OnSettingsReceived(plr: Player, setting: string, value: any)
	if not table.find(validSettings, setting) then plr:Kick("Don't do that") return end
	if value == previousValue then plr:Kick("Ummm you didn't change the value?") return end

	if not playerSettingsTable[plr.UserId] then
		-- If player data isn't found, then initialize it;
		InitializePlayerData(plr)
	end

	-- Update that value!
	playerSettingsTable[plr.UserId].Settings[setting] = value

	-- Some sort of warning that's here specifically for debugging :^)
	warn(plr.Name .. " changed " .. setting .. " to " .. tostring(value))
	warn("Current settings of " .. plr.Name .. " are: ")
	print(playerSettingsTable[plr.UserId].Settings)

	previousValue = value
end


function AutosaveInIntervals()
	coroutine.wrap(function()
		while true do
			local interval = math.random(300, 600) -- Random interval between 5 to 10 minutes
			-- Probably the best choice for security :^)
			task.wait(interval)

			for userId, data in pairs(playerSettingsTable) do
				local success, result = pcall(function()
					local key = tostring(userId)

					playerDataStorage:UpdateAsync(key, function(oldData)
						oldData = oldData or {} -- No current data will instead initialize an empty table :^)

						local newData = {
							TotalKills = data.TotalKills or 0,
							TotalDeaths = data.TotalDeaths or 0,
							TotalCurrencyEarned = data.TotalCurrencyEarned or 0,
							TotalXpEarned = data.TotalXpEarned or 0,
							CurrentLevel = data.CurrentLevel or 1,
							CurrentWhisper = data.CurrentWhisper or 0,
							Settings = data.Settings or {
								["Field of View"] = 90,
								["Classic Aim Down Sights"] = false,
								["Vitals GUI Color"] = Color3.fromRGB(255,255,255),
								["Weapon GUI Color"] = Color3.fromRGB(255,255,255),
								["Visible Viewmodel"] = true,
								["Show Crosshair"] = true
							}
						}
						
						return newData
					end)
				end)

				if not success then
					warn("Failed to save " .. userId .. "'s data!")
					warn("Error information: ")
					warn(result)
					continue -- Skips to the next iteration so that print 
					-- below doesn't run 
				end

				print("Successfully saved data for " .. userId)
			end
		end
	end)()
end

-- Runtime;
sendPlayerSettings.OnServerEvent:Connect(OnSettingsReceived)
players.PlayerAdded:Connect(OnPlayerJoined)
AutosaveInIntervals()

That looks good! Remember to get the client to unpack it and apply it.
You might want to save player data when they leave so it doesn’t only save on autosave.

You might also want to add in Dos/DDoS protection on your remote.

local cooldowns = {}

local function OnSettingReceived(plr: Player, setting: string, value: any)
    if cooldowns[player.Name] then plr:Kick("Don't try to crash the server. That's not very nice.") return nil end
    cooldowns[player.Name = true]

    --the rest of the code for your function. Just remember that if you early return (return before the end of the function) reset their cooldown.

    task.wait(3)
    cooldowns[player.Name] = nil
end
Other kind of not related to that bits
bit 1
  1. I assume you used a function in the middle because other code happens there? If not, just connect InitializePlayerData directly to the Players.PlayerAdded event.
  2. You forgot to pass the player as parameter…
bit 2

I noticed when you define functions, you just do like:

function foo()
end

instead of like:

local function foo()
end

I just wanted to let you know that if you use global (no local keyword) function defining in your LocalScripts you should change it to use the local keyword because otherwise exploiters can access those functions. They can change them, modify them, do whatever they want to them. They have this function called getsenv() (get script environment) which returns all the globals available to the script.

If you try this code, you can see what I mean:

function foo()
    print("function called.")
end

foo() --> "function called"
getfenv().foo() --> "function called"
getfenv().foo = function()
    print("Exploiters can modify your functions.")
end

getfenv().foo() --> "Exploiters can modify your functions"
foo() --> "Exploiters can modify your functions"

Don’t worry, this won’t apply to settings.SettingChanged = function because you used the local keyword when requiring the module.

sorry i keep talking about unrelated things lol

1 Like

doesn’t this mean i also need to add a client-sided cooldown of 3 seconds? isn’t that kind of annoying?

It doesn’t have to be 3 seconds, it can be any value. But yes. Just make sure you don’t only cooldown the client because that can be bypassed.

(any value less than ~0.5 probably isnt great)

1 Like

well, i suppose that’s everything for this post?

i appreciate the help, you’re a lifesaver :^)
i will message you directly if i need help with this though
so you’re not exactly free from me yet, sorry

:heart:

1 Like

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