Please Help Datastore Stuff

As shown back in this topic: Help Melee Foundation I’m trying to make a melee combat system with ModuleScripts and stuff and etc. Basically, I have a ServerScript that handles a player database with savedata, the savedata has a parameter for an array of “OwnedWeapons”. I have another script that checks to see if anything is added to the OwnedWeapons data (none by default) and adds a button for the specific weapon inside of a menu if so (the button doesn’t do anything just yet). I then have a proximity prompt in workspace that fires a RemoteEvent to a LocalPlayerScript that then calls a function inside of my main WeaponModuleScript that should add the weapon’s name into the playerdata OwnedWeapons.

The big problem that’s been harassing me and driving me crazy the past few days is that everything works fine until that AddWeaponToPlayer function inside the main module runs. The function runs fine but when the module adds the weapon to the playerdata.OwnedWeapons table, it works fine but it seems like the other scripts can’t detect anything inside the table even after checking the same table. When the player leaves the game the weapon data doesn’t seem to save to the OwnedWeapons either.

I’m so lost because, in theory, it should work, but the scripts have proven me wrong. If anyone would like to help (please do I’m practically begging), I could just barf out the code of my scripts to you so you can take a look. (I’m not sure if I can but I could also maybe give one access to my game in case one wants a better look idk tho)

1 Like

Hard to really say anything from your descriptions. Can you send the code that isn’t working? The client side that should be sending the data to the server and the server side that should get it and process it?

The issue occurs when you’re firing the remote event to the client and then that local script calls the method to add the weapon.

Changes to server owned scripts, attributes, modules, values are only allowed to be manipulated by the server NOT the client, so when you’re saving to the OwnedWeapons table on the client, it is not doing the same thing on the server version of that, so when your game tries to save it when you leave its saving the blank server data, let me know if you need me to elaborate.

Your flow should be reorganized so that any data changes are sent to the server before proceeding with game mechanics. In other words, update the owned weapons table on the server in order for changes to reflect globally.

1 Like

Okay, sorry it took me a while to respond, I’ve been busy the past few days. (Also sorry for the essay long scripts)

The system first starts with the PlayerDatabase Script in ServerScriptService

local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")

local database = DataStoreService:GetDataStore("PlayerDatabase")
local data = {}

local function LoadData(player)
	local success = nil
	local playerData = nil
	local attempt = 1

	repeat
		success, playerData = pcall(function()
			return database:GetAsync(player.UserId)
		end)

		attempt += 1
		if not success then
			warn(playerData)
			task.wait()
		end

	until success or attempt == 3

	if success then
		print("Connection success")
		if not playerData then
			print("New player ["..player.Name.."], giving default data")
			playerData = {
				["Yen"] = 0,
				["OwnedTools"] = {},
				["HotbarTools"] = {},
				["OffHandTool"] = {},
				["HotbarSize"] = 2,
				["Inventory"] = {},
				["Affiliation"] = "Tokyo-3 Citizen",
			}
		else
			print(playerData.OwnedTools[1])
		end
		data[player.UserId] = playerData
	else
		warn("Unable to get data for player"..player.UserId)
		player:Kick("There was a problem getting your data")
	end
end
Players.PlayerAdded:Connect(LoadData)

local function SaveData(player)
	if data[player.UserId] then
		local success = nil
		local playerData = nil
		local attempt = 1

		repeat
			success, playerData = pcall(function()
				return database:UpdateAsync(player.UserId, function()
					return data[player.UserId]
				end)
			end)

			attempt += 1
			if not success then
				warn(playerData)
				task.wait()
			end

		until success or attempt == 3

		if success then
			print("["..player.Name.."] Data saved successfully")
		else
			warn("Unable to save data for"..player.Name.." "..player.UserId)
		end
	else
		warn("No session data for"..player.Name.." "..player.UserId)
	end
end

ReplicatedStorage.Remotes.DeleteData.OnServerEvent:Connect(function(player)
	data[player.UserId] = nil
	database:RemoveAsync(player.UserId)
	player:Kick("Data Deleted")
end)

Players.PlayerRemoving:Connect(function(player)
	SaveData(player)
	data[player.UserId] = nil
end)

ReplicatedStorage.Remotes.SaveData.OnServerEvent:Connect(function(player)
	SaveData(player)
	data[player.UserId] = nil
end)

game:BindToClose(function()
	if not RunService:IsStudio() then
		for index, player in pairs(Players:GetPlayers()) do
			task.spawn(function()
				SaveData(player)
			end)
		end
	else
		print("Shutting down inside studio")
	end
end)

ReplicatedStorage.Remotes.GetData.OnServerInvoke = function(player)
	return data[player.UserId]
end

This script should just initialize the individual data for each player and create functions related to that data.

Next is the ToolModule which handles data related to the weapons in game.

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local equipment = ReplicatedStorage:WaitForChild("Equipment")

local ToolModule = {}
local ToolData = {
	["Karambit Progressive Knife"] = {
		["Name"] = "Karambit Progressive Knife",
		["ToolId"] = 1,
		["Model"] = equipment:WaitForChild("Karambit Progressive Knife")
	}
}

function ToolModule.addToolToPlayer(toolId)
	local playerData = ReplicatedStorage.Remotes.GetData:InvokeServer()
	
	for i, tool in ipairs(ToolData) do
		if tool.ToolId == toolId then
			if not table.find(playerData.OwnedTools, tool) then
				table.insert(playerData.OwnedTools, tool)
				ReplicatedStorage.Remotes.SaveData:FireServer()
			end
		end
	end
end

return ToolModule

This should just create the data for each future weapon and tool in game (only K Prog Knife exists right now). It should also be able to add the name for the weapon in the player’s OwnedTools table for later access.

This script is just in a part in workspace that allows the player to “collect” the weapon.

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local proximityPrompt = script.Parent:WaitForChild("ProximityPrompt")

proximityPrompt.Triggered:Connect(function(player)
	ReplicatedStorage.Remotes.AddTool:FireClient(player, 1)
end)

This next one is in StarterPlayerScripts

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ToolModule = require(ReplicatedStorage:WaitForChild("ToolModule"))

ReplicatedStorage.Remotes.AddTool.OnClientEvent:Connect(function(toolId)
	ToolModule.addToolToPlayer(toolId)
end)

Then this script is inside of the PlayerGui for the equip menu

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ToolModule = require(ReplicatedStorage:WaitForChild("ToolModule"))
local player = game.Players.LocalPlayer

local function updateMenu()
	print("WAdadwad")
	local playerData = ReplicatedStorage.Remotes.GetData:InvokeServer()
	
	local ownedTools = playerData.OwnedTools
	print(ownedTools[1])
	
	for i, tool in pairs(ownedTools) do
		print(tool.Name)
		print("Awdad")
		if not script.Parent.EquipContainer:FindFirstChild(tool.Name) then
			local newButton = script.Template:Clone()
			local model = tool.Model:Clone()
			newButton.Parent = script.Parent.EquipContainer
			newButton.Name = tool.Name
			newButton.Visible = true
			model.Parent = newButton.Viewport
			model.CFrame = newButton.Viewport.HandleAnchor.CFrame
		end
	end
end

ReplicatedStorage.Remotes.UpdateMenu.Event:Connect(function()
	updateMenu()
end)

What happens is that after giving the player default data and then interacting with the part that fires the AddTool event, when the updateMenu function is called in the equip menu, the first print is seen just fine but then the second print on line 10 is printed as “nil”.
Opening the game again loads the player data just fine however the print on line 41 in the PlayerDatabase script also prints out as “nil”.

Again, sorry for the big heaps of code. I am not an expert programmer so this could be a simple solution that I oversighted, but I think it would be better to get a fresh perspective on my work.

From what I see, I think you’re simply seeing nil on the ownedTools data table because you’re doing
print(ownedTools[1]) and print(playerData.OwnedTools[1]).

If we refer to what the tools table should look like:

	["Karambit Progressive Knife"] = {
		["Name"] = "Karambit Progressive Knife",
		["ToolId"] = 1,
		["Model"] = equipment:WaitForChild("Karambit Progressive Knife")
	}
}

This is a dictionary, so for each tool in this table, its a Key = Value as opposed to an array which is [numerical index i.e. “1”] = Value. In your case the key is a string representative of the name. When you tell lua ownedTools[1] you’re telling it to literally find the numerical index 1 of the corresponding table, so it tries to find it but since your table is a dictionary, it can’t find it.

I would typically offer a solution but what you do right after the print(ownedTools[1])
in the updateMenu function basically suffices the solution because you end up printing each tool in the dictionary. For best practice use key or k instead of i because using it can end up confusing you into thinking you’re working with an array instead of a dictionary.

print(key) would print each tools name as it appears on the ToolModule database.
Hope this helps!

https://create.roblox.com/docs/tutorials/fundamentals/coding-5/landing

Module
--ModuleScript in ReplicatedStorage
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local equipment = ReplicatedStorage:WaitForChild("Equipment")

local ToolModule = {}
local ToolData = {
	["Karambit Progressive Knife"] = {
		Name = "Karambit Progressive Knife",
		ToolId = 1,
		Model = equipment:WaitForChild("Karambit Progressive Knife")
	}
}

function ToolModule.addToolToPlayer(toolId)
	local playerData = ReplicatedStorage.Remotes.GetData:InvokeServer()
	for _, tool in pairs(ToolData) do
		if tool.ToolId == toolId and not table.find(playerData.OwnedTools, tool.Name) then
			table.insert(playerData.OwnedTools, tool.Name)
			ReplicatedStorage.Remotes.SaveData:FireServer()
		end
	end
end

return ToolModule
LocalScript
--LocalScript in PlayerGui
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ToolModule = require(ReplicatedStorage:WaitForChild("ToolModule"))
local player = game.Players.LocalPlayer

local function updateMenu()
	local playerData = ReplicatedStorage.Remotes.GetData:InvokeServer()
	for _, toolName in ipairs(playerData.OwnedTools) do
		local tool = ToolModule[toolName] or ToolModule.ToolData[toolName]
		if tool and not script.Parent.EquipContainer:FindFirstChild(tool.Name) then
			local newButton = script.Template:Clone()
			local model = tool.Model:Clone()
			newButton.Parent = script.Parent.EquipContainer
			newButton.Name = tool.Name
			newButton.Visible = true
			model.Parent = newButton.Viewport
			model.CFrame = newButton.Viewport.HandleAnchor.CFrame
		end
	end
end

ReplicatedStorage.Remotes.UpdateMenu.Event:Connect(updateMenu)

Are you saying that my code is fine, and that the problem is in how I search for the tool in the player’s data?

I think I understand now. As I’ve already said, I am not an expert on these things so I appreciate any form of help. Can you show me what a corrected version of my script will look like? I think I understand where I went wrong, but I would prefer to see what it should look like so I can get a better idea.

(Thanks for everything so far and also sorry for the long wait for a reply, I’ve been busy this week.)

Well based on the prompt you sent you said the issue you were experiencing was that the info was printing as nil. So yeah, I’m going by that to say that the issue is how your calling the information from the table.

heres what a fixed version would look for your playerdatabase script:

local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")

local database = DataStoreService:GetDataStore("PlayerDatabase")
local data = {}

local function LoadData(player)
	local success = nil
	local playerData = nil
	local attempt = 1

	repeat
		success, playerData = pcall(function()
			return database:GetAsync(player.UserId)
		end)

		attempt += 1
		if not success then
			warn(playerData)
			task.wait()
		end

	until success or attempt == 3

	if success then
		print("Connection success")
		if not playerData then
			print("New player ["..player.Name.."], giving default data")
			playerData = {
				["Yen"] = 0,
				["OwnedTools"] = {},
				["HotbarTools"] = {},
				["OffHandTool"] = {},
				["HotbarSize"] = 2,
				["Inventory"] = {},
				["Affiliation"] = "Tokyo-3 Citizen",
			}
		else
                        for key, tool in pairs(playerData.OwnedTools) do 
                                print(`Tool: {key}, Tool information: {tool}`) 
                        end 

			--print(playerData.OwnedTools[1]) --This is where you were getting the nil 
		end
		data[player.UserId] = playerData
	else
		warn("Unable to get data for player"..player.UserId)
		player:Kick("There was a problem getting your data")
	end
end
Players.PlayerAdded:Connect(LoadData)

local function SaveData(player)
	if data[player.UserId] then
		local success = nil
		local playerData = nil
		local attempt = 1

		repeat
			success, playerData = pcall(function()
				return database:UpdateAsync(player.UserId, function()
					return data[player.UserId]
				end)
			end)

			attempt += 1
			if not success then
				warn(playerData)
				task.wait()
			end

		until success or attempt == 3

		if success then
			print("["..player.Name.."] Data saved successfully")
		else
			warn("Unable to save data for"..player.Name.." "..player.UserId)
		end
	else
		warn("No session data for"..player.Name.." "..player.UserId)
	end
end

ReplicatedStorage.Remotes.DeleteData.OnServerEvent:Connect(function(player)
	data[player.UserId] = nil
	database:RemoveAsync(player.UserId)
	player:Kick("Data Deleted")
end)

Players.PlayerRemoving:Connect(function(player)
	SaveData(player)
	data[player.UserId] = nil
end)

ReplicatedStorage.Remotes.SaveData.OnServerEvent:Connect(function(player)
	SaveData(player)
	data[player.UserId] = nil
end)

game:BindToClose(function()
	if not RunService:IsStudio() then
		for index, player in pairs(Players:GetPlayers()) do
			task.spawn(function()
				SaveData(player)
			end)
		end
	else
		print("Shutting down inside studio")
	end
end)

ReplicatedStorage.Remotes.GetData.OnServerInvoke = function(player)
	return data[player.UserId]
end

for your equip menu it would be the same thing, although you dont need it because you end up fixing it in the loop

local function updateMenu()
	print("WAdadwad")
	local playerData = ReplicatedStorage.Remotes.GetData:InvokeServer()
	
	local ownedTools = playerData.OwnedTools
	print(ownedTools[1]) --Wrong
	
	for key, tool in pairs(ownedTools) do --Right
		print(tool.Name)
		print("Awdad")
		if not script.Parent.EquipContainer:FindFirstChild(tool.Name) then
			local newButton = script.Template:Clone()
			local model = tool.Model:Clone()
			newButton.Parent = script.Parent.EquipContainer
			newButton.Name = tool.Name
			newButton.Visible = true
			model.Parent = newButton.Viewport
			model.CFrame = newButton.Viewport.HandleAnchor.CFrame
		end
	end
end

If you want to print the first index of a dictionary you can do something like

local firstKey, firstTool = next(playerData.OwnedTools)
print(firstKey, firstTool)

Okay, so it seems there is also a problem with the GetData function in the PlayerDatabase script. The new print that you added in when the player joins the game doesn’t print, and when the updateMenu function is called only the very first keyboard smash print is actually printed, most likely meaning that there is nothing in OwnedTools for the for loop to run through.

I’m not sure if it’s an issue with the saving and/or loading data but at this rate I’m getting tired of looking at these same scripts :sob:

I’m going to assume by GetData you’re referring to the LoadData function in your playerdatabase script.

The reason that the print is not showing is because its not reaching the else statement. In your context this means that when the player joins its not finding any playerdata for them and instead is creating a new data template for them.

Try and take a screenshot of the console when you leave the game and it saves the data and also take a screenshot of the console of when you join and it should in theory load the data.

No, I was actually referring to the GetData remote event at the bottom of the PlayerDatabase script.

ReplicatedStorage.Remotes.GetData.OnServerInvoke = function(player)
	return data[player.UserId]
end

I think my problem resides here, but I’ll show a video showing what happens

The following video is of me joining the game, pressing the button to delete my previous data before rejoining. When I joined you can see that the new player data print has done it’s job and then I go over to the weapon on the ground to pick it up. When I pick it up, I go to the equip menu where a button showing that weapon should appear, however it didn’t. You can see the first print in the updateMenu function prints just fine but nothing else happens.

In the next video, when I rejoin the game, you can see that no print for the data loading can be seen. Also when I access the equip menu, still no weapon button appears.

What I think this means is that there probably isn’t any data to be found in the OwnedTools for the player here.

for key, tool in pairs(playerData.OwnedTools) do

Since the print for the loadData function didn’t print either, I also think this means that the game isn’t saving the player’s data properly, and/or isn’t loading the data properly.

Your issue stems from what I mentioned in my original response:

“The issue occurs when you’re firing the remote event to the client and then that local script calls the method to add the weapon. Changes to server owned scripts, attributes, modules, values are only allowed to be manipulated by the server NOT the client, so when you’re saving to the OwnedWeapons table on the client, it is not doing the same thing on the server version of that, so when your game tries to save it when you leave its saving the blank server data, let me know if you need me to elaborate. Your flow should be reorganized so that any data changes are sent to the server before proceeding with game mechanics. In other words, update the owned weapons table on the server in order for changes to reflect globally.”

Look through your scripts and check for data saving via local scripts, if you’re doing this where you’re directly saving data to the players data table via the client, your data will never save to the server.

You can send signals to the server from the client indicating a request to save data, but you should never manage the saving via the client.

OHHHH, okayyyy, I did not catch that the first time I read that. I didn’t know that changing data on the client would not affect the actual table on the server though in hindsight it makes sense.

After tweaking my code so that all the data changing is happening on the server, everything seems to work great now! It just took a bit of a complex system but I think it’s sound enough.

ughhh, thank you so so so much for everything these past few days, it means so much to me that you took time out of your day to help me. I really am grateful for it (even if it was just a simple server-to-client communication error that was the issue).

1 Like

No problem! I’m glad that resolved your issue !

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