Favorite Music System (Saving Issue)

Hello,
I want to make a music gui where you can add favorite songs to a ScrollingFrame. It would be pretty useless if the favorites aren’t saved, but i don’t know how to use data stores properly to save it.

My questions are:

1. How can i save multiple values (Using data stores) without knowing how much values i need to save?

2. How can i create a TextButton for each saved value after i joined?

3. Is it even possible to save gui objects without serialization?

4. How can i get seperated values from the data store? For example: I have 2 values saved. One value who contains the name of the song, and another one who contains the song id. How can i get this values seperated back?

So far i tried to understand how data stores work by reading the Data Stores article.

And i was looking for similar topics like this or this.

I also watched a few youtube videos like this:

After that my data store Script looked like this:

local DataStoreService = game:GetService("DataStoreService")

game.Players.PlayerAdded:connect(function(player)

	local Datastore = DataStoreService:GetDataStore(player.UserId.." FavsTest")

	local success, Datastore = pcall(function()
		local Data = Datastore:GetAsync("favs")
		for i = 1, #Data do			
			local nval = Instance.new("StringValue")
			nval.Name = "Name"
			nval.Value = Data.Value
			local idval = Instance.new("StringValue")
			idval.Name = "ID"
			idval.Parent = nval
			idval.Value = Data.Value
		end
	end)
	if not success then
		print(Datastore)
	end
end)

game.Players.PlayerRemoving:connect(function(player)
	
	local Datastore = DataStoreService:GetDataStore(player.UserId.." FavsTest")
	local Saving = player:FindFirstChild("favs"):GetChildren()

	local success, errorMessage = pcall(function()
		for i =  1, #Saving do
			Datastore:SetAsync("favs", Saving[i].Name, Saving[i].Value)
		end
	end)
	if not success then
		print(errorMessage)
	end
end)

But it obviously doesn’t work…
And it prints this error:

Unable to cast to Array  -  Server

This is the LocalScript that creates the TextButtons in the ScrollingFrame:

local frame = script.Parent.Parent.Parent.ScrollingFrame
local temp = frame.Template
local favs = frame.favs
local count

script.Parent.MouseButton1Click:Connect(function()
	count = #favs:GetChildren()
	local favb = Instance.new("TextButton")
	favb.Name = "fav"..count
	favb.Parent = favs
	favb.Position = temp.Position
	favb.Size = temp.Size
	favb.BackgroundTransparency = 0.5
	favb.TextColor3 = Color3.new(1, 1, 1)
	favb.TextScaled = true
	favb.Font = Enum.Font.Fondamento
	favb.Text = script.Parent.Parent.Parent.mname.Text
	local uic = Instance.new("UICorner")
	uic.Parent = favb
	local val = Instance.new("StringValue")
	val.Name = "id"
	val.Parent = favb
	val.Value = workspace:WaitForChild("MusicPlayerSound").SoundId
	local lsc = temp.LocalScript:Clone()
	lsc.Parent = favb
	local name = script.Parent.Parent.Parent.mname.Text
	local id = workspace:WaitForChild("MusicPlayerSound").SoundId
	game.ReplicatedStorage.MusicPlayerEvents.AddFav:FireServer(id, name)
end)

And this is the Script that creates the values:

game.ReplicatedStorage.MusicPlayerEvents.AddFav.OnServerEvent:Connect(function(plr, id, name)
	local nval = Instance.new("StringValue")
	nval.Name = "Name"
	nval.Parent = plr.favs
	nval.Value = name
	local idval = Instance.new("StringValue")
	idval.Name = "ID"
	idval.Parent = nval
	idval.Value = id
end)

Here a short video so you can see how the values are stored:

I’m sorry if something is misleading or misspelled.
It’s my first post here and i hope you guys can help me.

Thank You

2 Likes

Is there more to this? Like the line of the script it is casting the error? It would be great to see where the actual error is coming from :slight_smile:

1 Like

You don’t have to save the UI objects, just save the name of the songs that are favorited, and then create a UI object for each favorited song when the player joins the game. Check out this datastore video.

3 Likes

It’s the printed errorMessage from the PlayerRemoving function in the data store Script. And the PlayerAdded function prints this error:

 ServerScriptService.Script:9: attempt to get length of a nil value  -  Server - Script:20

But i guess it’s the empty GetChildren() table who causes the error in the PlayerAdded function because it didn’t save anything.

I am not quite sure what is happening here, but I’ll try to help.

This is improper usage of a datastore if I am correct.

I would assume that you are trying to save all the songs. Try the code below:

local DataStoreService = game:GetService("DataStoreService")

game.Players.PlayerRemoving:connect(function(player)

	local Datastore = DataStoreService:GetDataStore(player.UserId.." FavsTest")
	local Saving = player:FindFirstChild("favs")
	
	local SavingTable = {}
	
	local success, errorMessage = pcall(function()
		for _, Song in pairs(Saving:GetChildren()) do
			if Song.Value and Song["ID"].Value then
				SavingTable[Song.Value] = Song["ID"].Value
			end
		end
		Datastore:SetAsync("favs", SavingTable)
	end)
	if not success then
		print(errorMessage)
	end
end)

If you can’t save a dictionary on a DataStore, then you can convert it into JSON instead and then save it.

EDIT: I don’t think you can save dictionaries, so use this instead: Datastore:SetAsync("favs", HTTPService:JSONEncode(SavingTable))

4 Likes

I have a few suggestion and corrections to make for your system. First,

This would cause errors. Apart from being incorrect usage, Saving[i].Name would give you the literal string “Name” and Saving[i].Value would give you the value of the StringObject, which is the song’s name. Nowhere here are you saving the ID itself.

You are creating two instances but never actually parenting the Name instance anywhere. They will get garbage collected later on and will be of no use. Additionally, you are setting the Value of both instances to the same thing. This would also give you an error since Data.Value does not exist as far as I am aware. You’d have to use the Data itself. Also, Data isn’t a table you can loop through, it is the return value.

I personally would save the ID as a number instead of a string. This can save up Datastore space and would work just fine. Additionally, I would not create a separate value for Name and ID, I would instead create a single integer value for the ID, set the value of that integer value to the ID, and set the name of the integer value to the name of the song. This way, you don’t need to create unnecessary instances.

Lastly, I would combine your two server scripts into one and utilize variables for certain stuff. I will not be touching the client script at all, I am sure you can manage it on your own, but here is how I would write your server script that handles all of this:

-- Serivces --

local DataStoreService = game:GetService("DataStoreService")
local HTTPService = game:GetService("HttpService")

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Players = game:GetService("Players")

-- Variables --

local AddFavoriteEvent = ReplicatedStorage:WaitForChild("AddFavorite")

-- Functions --

local function GenerateSongInstance(Player, Name, ID)
	local FavoriteSongList = Player:FindFirstChild("Favorites")
	if not FavoriteSongList then return end
	
	if type(ID) ~= "number" then
		ID = tonumber(string.match(ID, "[%d]+"))
	end
	
	local Song = Instance.new("IntValue")
	Song.Name = Name
	Song.Value = ID
	Song.Parent = FavoriteSongList
end

-- Scripting --

Players.PlayerAdded:Connect(function(Player)
	local Datastore = DataStoreService:GetDataStore(tostring(Player.UserId).."_FavoriteSongs")
	
	local FavoriteSongList = Instance.new("Folder")
	FavoriteSongList.Name = "Favorites"
	FavoriteSongList.Parent = Player
	
	local Success, FavoriteSongs = pcall(function()
		return Datastore:GetAsync("FavoriteSongs")
	end)
	if Success and FavoriteSongs then
		local Songs = HTTPService:JSONDecode(FavoriteSongs)
		
		for SongName, SongID in pairs(Songs) do
			GenerateSongInstance(Player, SongName, SongID)
		end
	else
		warn(FavoriteSongs)
	end
end)

Players.PlayerRemoving:Connect(function(Player)
	local Datastore = DataStoreService:GetDataStore(tostring(Player.UserId).."_FavoriteSongs")
	
	local FavoriteSongList = Player:FindFirstChild("Favorites")
	if not FavoriteSongList then return end
	
	local FavoriteSongs = {}
	
	for _, Song in pairs(FavoriteSongs:GetChildren()) do
		FavoriteSongs[Song.Name] = Song.Value
	end
	
	local Success, ErrorMessage = pcall(function()
		Datastore:SetAsync("FavoriteSongs", HTTPService:JSONEncode(FavoriteSongs))
	end)
	if not Success then
		warn(ErrorMessage)
	end
end)

AddFavoriteEvent.OnServerEvent:Connect(function(Player, ID, Name)
	GenerateSongInstance(Player, ID, Name)
end)

Also, I renamed your AddFav event to AddFavorite and renamed the fav folder to Favorites because I found it more fitting that way. It really has no impact though, you can have it however you like. Hopefully this helped you.

DISCLAIMER: I did not test the above code, but I believe that it should work. If you do use it and you get an error, let me know.

1 Like

Thank you for this exact explanation and for showing me the errors i made.

I implemented your Script and only changed one thing:

local function GenerateSongInstance(Player, Name, ID)
	
	local FavoriteSongList = Player:FindFirstChild("Favorites")
	if not FavoriteSongList then return end

	if type(Name) ~= "number" then --Replaced ID with Name
		Name = tonumber(string.match(Name, "[%d]+"))
	end

	local Song = Instance.new("IntValue")
	Song.Name = ID --Also changed it here
	Song.Value = Name
	Song.Parent = FavoriteSongList
end

Because for some reason the Name and the ID values was switched, but that’s no problem.

So…
When i join it prints this warning:

 nil  -  Server - Script:50

It’s the warning from the PlayerAdded function

And when i leave it throws this error out:

ServerScriptService.Script:62: attempt to call a nil value  -  Server - Script:62

I hope i explained and showed everything correctly.

Thank You

I found my error(s).

Don’t change that. The problem wasn’t with that function, but the function invoking. These lines:

AddFavoriteEvent.OnServerEvent:Connect(function(Player, ID, Name)
	GenerateSongInstance(Player, ID, Name)
end)

Should be switched to:

AddFavoriteEvent.OnServerEvent:Connect(function(Player, ID, Name)
	GenerateSongInstance(Player, Name, ID)
end)

This is not an actual error, just indicates that there is no data. It will be removed in the fix below.

Just realized. I was looping through FavoriteSongs instead of FavoriteSongList. Of course, we want to loop through FavoriteSongList.

Here is the fixed version of the code:

-- Serivces --

local DataStoreService = game:GetService("DataStoreService")
local HTTPService = game:GetService("HttpService")

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Players = game:GetService("Players")

-- Variables --

local AddFavoriteEvent = ReplicatedStorage:WaitForChild("AddFavorite")

-- Functions --

local function GenerateSongInstance(Player, Name, ID)
	local FavoriteSongList = Player:FindFirstChild("Favorites")
	if not FavoriteSongList then return end

	if type(ID) ~= "number" then
		ID = tonumber(string.match(ID, "[%d]+"))
	end

	local Song = Instance.new("IntValue")
	Song.Name = Name
	Song.Value = ID
	Song.Parent = FavoriteSongList
end

-- Scripting --

Players.PlayerAdded:Connect(function(Player)
	local Datastore = DataStoreService:GetDataStore(tostring(Player.UserId).."_FavoriteSongs")

	local FavoriteSongList = Instance.new("Folder")
	FavoriteSongList.Name = "Favorites"
	FavoriteSongList.Parent = Player

	local Success, FavoriteSongs = pcall(function()
		return Datastore:GetAsync("FavoriteSongs")
	end)
	if Success then
		if not FavoriteSongs then return end
		local Songs = HTTPService:JSONDecode(FavoriteSongs)

		for SongName, SongID in pairs(Songs) do
			GenerateSongInstance(Player, SongName, SongID)
		end
	else
		warn(FavoriteSongs)
	end
end)

Players.PlayerRemoving:Connect(function(Player)
	local Datastore = DataStoreService:GetDataStore(tostring(Player.UserId).."_FavoriteSongs")

	local FavoriteSongList = Player:FindFirstChild("Favorites")
	if not FavoriteSongList then return end

	local FavoriteSongs = {}

	for _, Song in pairs(FavoriteSongList:GetChildren()) do
		FavoriteSongs[Song.Name] = Song.Value
	end

	local Success, ErrorMessage = pcall(function()
		Datastore:SetAsync("FavoriteSongs", HTTPService:JSONEncode(FavoriteSongs))
	end)
	if not Success then
		warn(ErrorMessage)
	end
end)

AddFavoriteEvent.OnServerEvent:Connect(function(Player, ID, Name)
	GenerateSongInstance(Player, Name, ID)
end)

I tested the above code in a demo and it works like a charm. Hopefully this helped you and solved your problem.

1 Like

Works perfect. Thank you a lot.
There is only this one thing you forgot, but it’s no problem and easy to fix.

Instead of:

You need to do:

AddFavoriteEvent.OnServerEvent:Connect(function(Player, Name, ID)
	GenerateSongInstance(Player, Name, ID)
end)

That’s all. Everything else is really perfect.

Thank You

1 Like