How to save a TextBox's Text

I have a Notepad GUI in my game and I want to save the notepad’s text when the player leaves and load the notepad’s saved text when the player join’s back but for some reason my code won’t work and there’s no error’s that show inside of the console. Even using online tutorial’s and stuff also won’t work so here I am lol.

Server Script:

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local DataStoreService = game:GetService("DataStoreService")
local NotepadData = DataStoreService:GetDataStore("NotepadData")

Players.PlayerAdded:Connect(function(Player)
	local Data
	local Success, Error = pcall(function()
		Data = NotepadData:GetAsync("User_"..Player.UserId)
	end)
	
	if Success and Data then
		ReplicatedStorage.Events.RemoteEvents.NotepadChangedText:FireClient(Player, Data)
	end
end)

ReplicatedStorage.Events.RemoteEvents.NotepadSaveText.OnServerEvent:Connect(function(Player, Text)
	pcall(function()
		NotepadData:SetAsync("User_"..Player.UserId, Text)
	end)
end)

LocalScript that controls the Notepad (some lines have been cut out in this post only):

ReplicatedStorage.Events.RemoteEvents.NotepadChangedText.OnClientEvent:Connect(function(Text)
	TextBox.Text = Text
end)

game.Players.PlayerRemoving:Connect(function(Player)
	if Player == game.Players.LocalPlayer then
		ReplicatedStorage.Events.RemoteEvents.NotepadSaveText:FireServer(TextBox.Text)
	end
end)

spawn(function()
	while wait(60) do --my attempt at an auto save
		ReplicatedStorage.Events.RemoteEvents.NotepadSaveText:FireServer(TextBox.Text)
	end
end)
1 Like

Likely, your client script is not loading fast enough to hear the server call upon NotepadSaveText::FireClient. This is a common problem, and there really should be a wiki article we can link to about issues like it.

When I use a remote to both save and retrieve data, I always use a RemoteFunction. My setup is usually something like this:

Server

Remote.OnServerInvoke = function(Player, Save, Data)
    if Save == true then
        -- If the player called the remote to SAVE their data

        --[[ Make sure you sanitize the data before this point so exploiters
             can't fill up your data stores with junk ]]

        SaveData[Player] = Data
        pcall(function()
            DataStore:SetAsync(Player.UserId, Data)
        end)
    elseif Save == false then
        -- If the player called the remote to RETRIEVE the server's data
        return SaveData[Player]
    end
end

where SaveData is a table in the server’s script which has the player’s data. This would presumably be loaded from a PlayerAdded function.

Client

local SaveData = Remote:InvokeServer(false)

--[[
Between the above line and the below loop, you would have code which modifies 
the contents of SaveData as you play the game and make changes you want to save
]]

while true do
    task.wait(60)
    Remote:InvokeServer(true, SaveData)
end

As a sidenote, you should be using the task library for your spawn's, wait's, etc. since apparently it’s a big improvement.

4 Likes

To expand upon SaveData, it would be loaded like this:

Server

local SaveData = {}

function PlayerAdded(Player)
	local Success, Data = pcall(function()
        return DataStore:GetAsync(Player.UserId) 
    end)
	if Success then
		SaveData[Player] = Data
	end
end

However, there is a problem.

Although it’s unlikely, the server’s Remote.OnServerInvoke data retrieval function could be called by a new player’s quickly-loaded LocalScript before the server’s PlayerAdded function could finish loading that player’s data from the DataStore::GetAsync call.

This is called a race condition, and is essentially the same problem that my first post was designed to alleviate, but in reverse: this time, the server isn’t loading data fast enough.

An in-house fix for this is a solution I’ve used often, but it looks really ugly:

Server

local SaveData = {}

local LoadedData = {}
local LoadedDataBindable = Instance.new("BindableEvent")

function PlayerAdded(Player)
	local Success, Data = pcall(function() 
        return DataStore:GetAsync(Player.UserId) 
    end)
	if Success then
		SaveData[Player] = Data
	end
	LoadedData[Player] = true
	LoadedDataBindable:Fire()
end

Remote.OnServerInvoke = function(Player, Save, Data)
	while not LoadedData[Player] do
		LoadedDataBindable.Event:Wait()
	end
	-- The rest of the remote call remains the same as in the original code
end

If you are saving sensitive data like inventory, money etc. you should be using UpdateAsync because the server’s data store call can fail, return nil data to the client in the retrieval remote call, client then treats it like a new save, then auto-saving overwrites the old save with the new stuff. In this case, though, it makes sense to use SetAsync since it’s just notepad text.

I don’t think this solution has any room for failure. It’s a pain to set up, though. If someone else knows a cleaner alternative, I would appreciate it.

3 Likes

^ So this is replacing my chunk of code here, right? v

Here, I don’t know how I would be able to actually fetch the data when the player joins and set the Textbox’s Text to the saved data.

1 Like

Check if success is not true and print out the error. If it isn’t working, most likely theres an error message being stored in there. Since this is a pcall then it will just hide the error message inside of error.

if not Success then print(Error) end

1 Like

Here’s my current Server Script and I’m still trying to figure out how I would be able to load the data on the client.

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local DataStoreService = game:GetService("DataStoreService")
local NotepadData = DataStoreService:GetDataStore("NotepadData")
local RemoteFunction = ReplicatedStorage.Events.RemoteFunctions.NotepadData
local LoadedDataBindable = Instance.new("BindableEvent")
local SaveData = {}
local LoadedData = {}

function PlayerAdded(Player)
	local Success, Data = pcall(function() 
		return NotepadData:GetAsync("User_"..Player.UserId)
		if not Success then print(Error) end -- you want me to put that line here, right?
	end)
	if Success then
		SaveData[Player] = Data
	end
	LoadedData[Player] = true
	LoadedDataBindable:Fire()
end

RemoteFunction.OnServerInvoke = function(Player, Save, Data)
	while not LoadedData[Player] do
		LoadedDataBindable.Event:Wait()
	end
end
1 Like
function PlayerAdded(Player)
	local Success, Data = pcall(function() 
		return NotepadData:GetAsync("User_"..Player.UserId)
	end)
	if Success then
		SaveData[Player] = Data
	end
if not Success then print(Data) end 
	LoadedData[Player] = true
	LoadedDataBindable:Fire()
end
2 Likes

You would just set the TextLabel.Text to the returned SaveData:

local NotepadText = Remote:InvokeServer(false)
TextLabel.Text = NotepadText
1 Like

For some reason this stops my code from working meaning the notepad won’t open.

I tried debugging this and “c” doesn’t ever print.

Server Script:

RemoteFunction.OnServerInvoke = function(Player, Save, Data)
	print("a")
	while not LoadedData[Player] do
		print("b")
		LoadedDataBindable.Event:Wait()
		print("c")
	end
end

Full Server Script:

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local DataStoreService = game:GetService("DataStoreService")
local NotepadData = DataStoreService:GetDataStore("NotepadData")
local RemoteFunction = ReplicatedStorage.Events.RemoteFunctions.NotepadData
local LoadedDataBindable = Instance.new("BindableEvent")
local SaveData = {}
local LoadedData = {}

function PlayerAdded(Player)
	local Success, Data = pcall(function() 
		return NotepadData:GetAsync("User_"..Player.UserId)
	end)
	if Success then
		SaveData[Player] = Data
	end
	if not Success then print(Data) end 
	LoadedData[Player] = true
	LoadedDataBindable:Fire()
end

RemoteFunction.OnServerInvoke = function(Player, Save, Data)
	print("a")
	while not LoadedData[Player] do
		print("b")
		LoadedDataBindable.Event:Wait()
		print("c")
	end
end
1 Like

Is PlayerAdded connected to PlayersService.PlayerAdded?

1 Like

Yes it is now LOL but now I’m getting this erorr.

image

^ This is line 86.

1 Like

You need to have a “default condition” if the player has no saved data. Something like this:

if NotepadText then
    TextLabel.Text = NotepadText
else
    TextLabel.Text = "Write something here"
end
1 Like

I have a placeholder text that says start taking your first note so I decited to do this instead…

if NotepadText then
    TextLabel.Text = NotepadText
end

So that way the TextBox’s Text is empty.

Anyways, the text I put into the TextBox won’t save, with no errors, maybe you can see what’s up?

LocalScript (a bit of code has been sniped out):

local SaveData = RemoteFunction:InvokeServer(false)
if SaveData then
	TextBox.Text = SaveData
end

ServerScript:

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local DataStoreService = game:GetService("DataStoreService")
local NotepadData = DataStoreService:GetDataStore("NotepadData")
local RemoteFunction = ReplicatedStorage.Events.RemoteFunctions.NotepadData
local LoadedDataBindable = Instance.new("BindableEvent")
local SaveData = {}
local LoadedData = {}

function PlayerAdded(Player)
	local Success, Data = pcall(function() 
		return NotepadData:GetAsync("User_"..Player.UserId)
	end)
	if Success then
		SaveData[Player] = Data
	end
	if not Success then print(Data) end 
	LoadedData[Player] = true
	LoadedDataBindable:Fire()
end

Players.PlayerAdded:Connect(PlayerAdded)

RemoteFunction.OnServerInvoke = function(Player, Save, Data)
	while not LoadedData[Player] do
		LoadedDataBindable.Event:Wait()
	end
end
1 Like

Don’t mean to sound rude, but I was under the impression you were more familiar with Roblox Lua or programming in general. That’s okay, I’m fine holding your hand through this problem — others will be reading this in the future as well, and may have the same kinds of questions.

You left the remainder of this function empty:

RemoteFunction.OnServerInvoke = function(Player, Save, Data)
	while not LoadedData[Player] do
		LoadedDataBindable.Event:Wait()
	end
end

In my first follow-up post, I put this here just so I wasn’t copy-pasting a bunch of code from my initial post:

In actuality, it would look like this:

RemoteFunction.OnServerInvoke = function(Player, Save, Data)
	while not LoadedData[Player] do
		LoadedDataBindable.Event:Wait()
	end
	if Save == true then
        -- If the player called the remote to SAVE their data

        --[[ Make sure you sanitize the data before this point so exploiters
             can't fill up your data stores with junk ]]

        SaveData[Player] = Data
        pcall(function()
            DataStore:SetAsync(Player.UserId, Data)
        end)
    elseif Save == false then
        -- If the player called the remote to RETRIEVE the server's data
        return SaveData[Player]
    end
end

If you used the above code for your Remote.OnServerInvoke function, it would work.

This server code is not finished, however. Note this comment:

It would not be “secure” — a remote exploiter could choose to spam that remote with giant 4 megabyte strings and throttle the server’s data store usage.

Prior to these lines,

make sure you have some cancel conditions if they’re firing the remote too fast or they’re sending through notepad text which is obviously way too long. You’ll need to set some parameters here. Here is how you “cancel” a function:

if 2 + 2 == 5 then return end
print("Test")

Test is not printed because the code was “canceled” with a blank return before that print line could be reached. The code was “canceled” because 2+2 isn’t 5.

If this is all still confusing to you, I can try to elaborate further. I would like to see what methods you try though.

2 Likes

Sorry, I specialize in the UI side of coding on Roblox, not so much game functionality hence why I’m making a notepad.

I did notice though, my LocalScript does not save the data when the player leaves. So I added this.

game.Players.PlayerRemoving:Connect(function(Player)
	RemoteFunction:InvokeServer(true)
end)

Hence this part

if Save == true then
		SaveData[Player] = Data
		pcall(function()
			NotepadData:SetAsync("User_"..Player.UserId, Data)
		end)

is for when the player calls the remote to save their data, though it still won’t work. I’m losing my mind lol

1 Like

I’ve never tested this before. I wonder if it would run if you left the game, since it’s a question of which code gets run first — your PlayerRemoving function or Roblox’s internal script cleanup?

I would not rely on this method. I would just Remote:InvokeServer(true, notepadText) every time I changed the notepad text, and then have the server only run SetAsync when its own PlayerRemoving function is called, using its SaveData table.

In other words, the client cares enough to tell the server every time it makes changes to the notepad text, but the server only cares enough to tell Roblox’s data stores when you leave the game.

Running SetAsync on every remote call is not an efficient use of data stores, even though that’s what I put in the code before. I would recommend to you and any other readers that the server only call Roblox’s data store functions when players join or leave the game, and also perhaps on the server’s own auto saving schedule.

Oh, also you aren’t checking if this Player is the LocalPlayer:

There’s also DataStore2, which I’ve never used or examined in any way, but it’s worth a shot if it helps make this less of a headache.

1 Like

I didn’t know I had to include the TextBox’s text, all I had was Remote:InvokeServer(true).

Oh alright, this should do the trick then.

game.Players.PlayerRemoving:Connect(function(Player)
	local LocalPlayer = game.Players.LocalPlayer
	if Player.Name == LocalPlayer.Name then
		RemoteFunction:InvokeServer(true, TextBox.Text)
	end
end)

Gotcha, I’ll look into it.

1 Like

You have to send the text because the server has to save it. How else would the server know what to save?

1 Like

Lol, you’re right, sorry. I’m getting a new error were the TextBox’s Text gets set to the Data in the localscript

image

1 Like

Print it to debug whats happening