You can use a RemoteEvent
to tell the client from the server, or a BindableEvent
to tell that between LocalScripts that something has changed.
also, whats that color theme? i like it
You can use a RemoteEvent
to tell the client from the server, or a BindableEvent
to tell that between LocalScripts that something has changed.
also, whats that color theme? i like it
…so
fire a remote event with the changed setting in the module script, then fire another remote event on the server that tells the client something has changed and passes that value
kind of a good idea, verify the information on the server first before applying it
Courier, 10ot, Normal ← Font
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 RemoteEvent
s from client to server or vice versa.
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
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.
LocalScript
assigns the callback (playerSettings.SettingChanged = function
):UpdateSetting
, playerSettings.SettingChanged
is run which is that function in your LocalScript
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
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)
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?
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
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.
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)
-- 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
InitializePlayerData
directly to the Players.PlayerAdded
event.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 LocalScript
s 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
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)
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
This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.