Looking for Feedback on my Player Data Management System

Hi everyone,

I’ve been working on a system to manage player data using ProfileStore by loleris, and I’d love some feedback on it. The system handles things like saving/loading inventory data and managing player sessions. It works, but I feel like there’s room for improvement, and I want to make sure I’m doing things the right way.

What I Need Help With:

  • Better Practices: Am I using ProfileStore the way it’s meant to be used?
  • General Feedback: Anything else you see that could be improved or cleaned up?
--<Services>--
local runService = game:GetService("RunService")
local players = game:GetService("Players")

--<Modules>--
local profileStoreModule = require(script.Parent:FindFirstChild("ProfileStore"))
local defaultData = require(script.Parent.Default_Data)

--<Variables>--
local playersInventoryFolder = script.Parent.Parent.Parent.Players_Inventories
local inventoryTemplate = playersInventoryFolder:FindFirstChild("Inventory_Temp")


local key = "MainKey"
if runService:IsStudio() then
	key = "StudioKey"
end

local profileStore = profileStoreModule.New(key, defaultData)
local Profiles: {[player]: typeof(playerStore:StartSessionAsync())} = {}



local Player = {}
Player.__index = Player

function Player.new(newPlayer: Player)
	local self = setmetatable({}, Player)
	
	self.Player = newPlayer
	self.Character = newPlayer.Character
	self.Player.CharacterAdded:Connect(function(character: Model)
		self.Character = character
	end)
	
    --Extra variables
	self.CurrentlySaving = false
	self.CurrentlyLoading = false
	self.UnLoading = false
	
	self.UserId = self.Player.UserId
	
	self:CreateProfile(self.Player)
	
	return self
end

function Player:CreateProfile(player: Player)
	print("Data key name: "..profileStore.Name)
	
	local profile = profileStore:StartSessionAsync(`{player.UserId}`, {
		Cancel = function()
			return player.Parent ~= players
		end,
	})
	
	if profile ~= nil then

		profile:AddUserId(player.UserId) 
		profile:Reconcile() 

		profile.OnSessionEnd:Connect(function()
			Profiles[player] = nil
			player:Kick("Profile session end - Please rejoin")
		end)

		if player.Parent == players then
			Profiles[player] = profile
			print(`Profile loaded for {player.DisplayName}!`)
			
			self:CloneDefaultInventory()
			task.wait()
			self:LoadData(player)
			
		else
			profile:EndSession()
		end

	else
		player:Kick("Profile load fail - Please rejoin")
	end
end

function Player:LoadData(player: Player)
	local playerInventoryModule = require(playersInventoryFolder:FindFirstChild(player.UserId))

	if not playerInventoryModule then
		return false
	end

	local profile = Profiles[player]
	
	if profile and profile.Data then
		local success, errorMsg = pcall(function()
			playerInventoryModule.SendInventoryData(profile.Data.Inventory)
		end)

		if success then
			print(profile.Data)
			--task.wait(3)
			--for _, player in ipairs(players:GetPlayers()) do
			--	playerInventoryModule.AddItem("Wooden Axe")
			--	print(playerInventoryModule["Inventory"])
			--end
		else
			warn("Failed to save data for " .. player.DisplayName .. ": " .. errorMsg)
		end
	else
		warn("No profile found for "..player.DisplayName)
	end
end

function Player:SaveData(player: Player)
	local profile = Profiles[player]

	if profile ~= nil then
		local success, errorMsg = pcall(function()
			profile.Data.Inventory = self:GetPlayerInventory(player)
		end)

		if success then
			profile:EndSession()
			print(player.DisplayName .. "'s data successfully saved!")
		end
	end
end
	
function Player:CloneDefaultInventory()
	if not inventoryTemplate then 
		return
	end
	
	local defaultInventory = inventoryTemplate:Clone()
	defaultInventory.Name = self.UserId
	defaultInventory.Parent = playersInventoryFolder
end

function Player:GetPlayerInventory(player: Player)
	local playerInventoryModule = require(playersInventoryFolder:FindFirstChild(player.UserId))
	
	if not playerInventoryModule then
		return
	end
	
	return playerInventoryModule.GetInventoryData()
end

return Player

I appreciate any tips or suggestions you can provide—I’m continually learning and eager to improve, so your insights would be incredibly valuable.

Looks clean to me! I would suggest adding more comments.

2 Likes

you could have your player data class object just be standalone and create a module script that contain your loadprofile or saveprofile or anyhing like that, also i would try separating really other logic from the module you are working on.

Depending on how big your game is gonna be if you have stuff like inventory and add items, clearitems etc in a module named playerdata it can get quite confusing. i would try making different seperate modules for like inventory and equipment or any other things and their own logic, so instead of having the GetPlayerInventory function in playerdata you can have that in a own modulescript for the inventory.

Hello,

Thank you for your recommendation and feedback. I had already implemented an improved version that follows this approach, separating the player class and profile class. If you’re interested, I’ve shared the code below.

Player Class: Manages all player/character-related mechanics. (Work in progress)

local serverScriptService = game:GetService("ServerScriptService")
local playersInventoryFolder = serverScriptService.Players_Inventory

local Player_Class = {}
Player_Class.__index = Player_Class

local MAX_WEIGHT = 200
local SLOW_SPEED = 8
local WALK_SPEED = 16
local RUN_SPEED = 32

local runMechanic
local profileClass

------------------------
--< Local Functions >--
------------------------

local function ShiftRun(player, eventName, speed)
	if eventName ~= "RunMechanic" or typeof(speed) ~= "number" or speed > RUN_SPEED or speed < 0 then
		return
	end
	player:SetAttribute("RunSpeed", speed)
end

----------------------
--< Main Fumctions >--
----------------------

function Player_Class.new(newPlayer)
	local self = setmetatable({}, Player_Class)
	
	self.Player = newPlayer
	self.Character = newPlayer.Character or newPlayer.CharacterAdded:wait()
	self.UserId = newPlayer.UserId
	
	self.Player.CharacterAdded:Connect(function(character)
		self.Character = character
	end)
	
	self:SetUpPlayer()
	self:MonitorChanges()
	
	return self
end

--< Set Players Attributes >--
function Player_Class:SetUpPlayer()
	local playerInventory = playersInventoryFolder.Inventory_Template:Clone()
	playerInventory.Name = tostring(self.UserId)
	playerInventory.Parent = playersInventoryFolder
	
	profileClass.new(self.Player)
end

--< Recieve Damage >--
function Player_Class:RecieveDamage(damage: number)
	local humanoid = self.Character:FindFirstChildOfClass("Humanoid")
	if humanoid then
		humanoid:TakeDamage(damage)
	else
		warn("No humanoid found in player")
	end
end

--< Live Connections >--
function Player_Class:MonitorChanges()
	self.Player:GetAttributeChangedSignal("Weight"):Connect(function()
		self:UpdateSpeed()
	end)

	self.Player:GetAttributeChangedSignal("RunSpeed"):Connect(function()
		self:UpdateSpeed()
	end)
	
	self.Player:GetAttributeChangedSignal("Health"):Connect(function()
		self:UpdateHealth()
	end)
	
	self.Player:GetAttributeChangedSignal("Banned"):Connect(function()
		self:ApplyBan()
	end)
end

--< Apply Ban >--
function Player_Class:ApplyBan()
	local banAttribute = self.Player:GetAttribute("Banned")
	
	if banAttribute then
		self.Player:Kick("You are banned from this game")
	end
end

(Other functions)...

function Player_Class.Init(modules, remotes)
	
	profileClass = modules.Profile_Class
	runMechanic = remotes.Run_Mechanic
	
	runMechanic.OnServerEvent:Connect(ShiftRun)
	
	
	game:GetService("Players").PlayerAdded:Connect(function(player)
		local playerObject = Player_Class.new(player)
	end)
end

return Player_Class

Profile Class: Handles data loading and saving.

local players = game:GetService("Players")
local RunService = game:GetService("RunService")
local ServerScriptService = game:GetService("ServerScriptService")
local playersInventoryFolder = ServerScriptService.Players_Inventory

local key = "MainKey"
if RunService:IsStudio() then
	key = "StudioKey1"
end

local Profile_Class = {}
Profile_Class.__index = Profile_Class

local profileStoreModule
local defaultData 

local profileStore
local Profiles: {[player]: typeof(playerStore:StartSessionAsync())} = {}

-----------------------
--< Local Functions >--
-----------------------
local function CheckPlayerBan(player)
	local banAttribute = player:GetAttribute("Banned")
	
	if not banAttribute then
		print(player.Name, " is not banned")
	else
		print(player.Name, " is banned")
		player:Kick("You are banned from this game")
	end
end

local function GetPlayerInventoryModule(playerUserId)
	local moduleName = tostring(playerUserId)
	local moduleScript = playersInventoryFolder:FindFirstChild(moduleName)

	if not moduleScript then
		warn("Didn't found the module: " .. moduleName)
		return nil
	end

	local success, result = pcall(require, moduleScript)
	if not success then
		warn("Error to required the module: " .. result)
		return nil
	end

	return result
end

----------------------
--< Main Fumctions >--
----------------------
function Profile_Class.new(player)
	local self = setmetatable({}, Profile_Class)
	
	self.Player = player
	self.UserId = player.UserId
	
	profileStore = profileStoreModule.New(key, defaultData)
	self:CreatePlayerProfile()
	
	return self
end

--< Create Player Profile >--
function Profile_Class:CreatePlayerProfile()
	print("Profile Store Key: ", key)
	
	local profile = profileStore:StartSessionAsync(`{self.UserId}`, {
		Cancel = function()
			return self.Player.Parent ~= players
		end,
	})
	
	if profile ~= nil then

		profile:AddUserId(self.UserId) 
		profile:Reconcile() 

		profile.OnSessionEnd:Connect(function()
			Profiles[self.Player] = nil
			self.Player:Kick("Profile session end - Please rejoin")
		end)
		
		if self.Player.Parent == players then
			Profiles[self.Player] = profile
			
			self.Player:SetAttribute("ProfileLoaded", true)
			print(`Profile loaded for {self.Player.DisplayName}!`)
			
			self:LoadData()
			
		else
			profile:EndSession()
		end
	else
		self.Player:Kick("Profile load fail - Please rejoin")
	end
end


--< Loading Data >--
function Profile_Class:LoadData()
	local profile = Profiles[self.Player]
	
	if not profile then
		return
	end
	
	if profile.Data then
		local success, errorMsg = pcall(function()
			
			-- Load Player Attributes
			for name, values in pairs(profile.Data.Attributes) do
				self.Player:SetAttribute(name, values)
			end
			
			-- Load Player Inventory
			local playerInventoryModule = GetPlayerInventoryModule(self.UserId)
			if playerInventoryModule then
				playerInventoryModule:SendInventoryTable(profile.Data.Inventory)
			end
			
			CheckPlayerBan(self.Player)
			
			print("Loaded Data: ", profile.Data)
		end)

		if not success then
			warn("Failed to load data for " .. self.Player.DisplayName .. ": " .. errorMsg)
		end
	else
		warn("No profile data found for "..self.Player.DisplayName)
	end
end


--< Saving Data >--
function Profile_Class:SaveData(player)
	local profile = Profiles[player]
	
	if not profile or not player:GetAttribute("ProfileLoaded") then
		return
	end
	
	local success, errorMsg = pcall(function()
		
		-- Saves Player Attributes
		local atributes = player:GetAttributes()
		for name, values in pairs(atributes) do
			if name ~= "Health" then
				profile.Data.Attributes[name] = values
			end
		end
		
		-- Saves Player Inventory
		local playerInventoryModule = GetPlayerInventoryModule(player.UserId)
		if playerInventoryModule then
			profile.Data.Inventory = playerInventoryModule:GetInventoryTable()
		end
	end)
	
	if success then
		print("New Saved Data: ", profile.Data)
		profile:EndSession()
		print(player.DisplayName .. "'s data successfully saved!")
	else
		warn("Failed to save data for " .. player.DisplayName .. ": " .. errorMsg)
	end
end

function Profile_Class.Init(modules, remotes)
	profileStoreModule = modules.ProfileStore
	defaultData = modules.Default_Data
	
	
	players.PlayerRemoving:Connect(function(player)
		if Profiles[player] then
			Profile_Class:SaveData(player)
		end
	end)
end

return Profile_Class

Let me know if you see any areas for improvement!