StatService | Easy leaderstats with autosaving

Introducing, StatSerivce!

Earlier today, I released a simple leaderstats wrapper called LeaderstatsService. This module recieved some awesome feedback, so I’ve decided to remake it trying to include some of the best suggestions there were.

So, here we have StatService! StatService automatically creates leaderstats and saves them.


I can do this myself easily! Why do I need a stupid module??
It’s for beginners, the module code is pretty easy to read too, so it’s a learning resource I suppose.


This module uses the Open-Sourced ProfileService, so this module is required for this module too.

Let’s get started with a simple documentation.

:Create(Player, Name, Class, Value)

Returns [Instance]
Example usage:

StatService:Create(plr, "Cash", "IntValue", 0)

Don’t worry about the value argument, the module will automatically overwrite it if the user has any saved data for that stat.

:Delete(Player, Name)

Returns nil
Example usage:

StatService:Delete(plr, "Cash")

This function will automatically destroy the leaderstat & clear the players data linked with this stat.

:SetValue(Player, Name, Value)

Returns nil
Example usage:

StatService:SetValue(plr, "Cash", 100)

This function will automatically edit the Leaderstats and the saved data accordingly.


Installation

Code

. . .
--// Configurations
local ProfileService = "Path to Profile Service" --// https://madstudioroblox.github.io/ProfileService/

local DatabaseName = "hhZBG4x9i8" --// Make this random to prevent this script from messing with other-
-- -datastores.

--// Data Saving Code
--// DON'T EDIT BELOW UNLESS YOU KNOW WHAT YOU'RE DOING!!!

-- Services
local PS = require(ProfileService)
local ProfileTemplate = {}
local Profiles = {}

local Database = PS.GetProfileStore(
	DatabaseName,
	ProfileTemplate
)

local Players = game:GetService("Players")

-- Local Functions
local function playerAdded(player)
	print("⌛ Loading "..player.Name.."'s data")
	local profile = Database:LoadProfileAsync("StatService-"..player.UserId)
	if profile ~= nil then
		profile:AddUserId(player.UserId)
		profile:ListenToRelease(function()
			Profiles[player] = nil
			player:Kick("❌ Data became unsecure")
		end)

		if player:IsDescendantOf(Players) then
			Profiles[player] = profile
			print("✅ Loaded "..player.Name.."'s data: ", profile.Data)
		else
			profile:Release()
		end
	else
		--// Failed to load data
		player:Kick("❌ Data failed to load.")
	end
end

local function createlsFolder(player)
	if not player:FindFirstChild("leaderstats") then
		local fld = Instance.new("Folder")
		fld.Name = "leaderstats"
		fld.Parent = player
		return fld
	else
		return player["leaderstats"]
	end
end

local function GetPlayerProfileAsync(player) --> [Profile] / nil
	-- Yields until a Profile linked to a player is loaded or the player leaves
	local profile = Profiles[player]
	while profile == nil and player:IsDescendantOf(Players) == true do
		task.wait()
		profile = Profiles[player]
	end
	return profile
end

-- Initialize
for _, player in ipairs(Players:GetPlayers()) do
	task.spawn(playerAdded, player)
end

Players.PlayerAdded:Connect(function(p)
	playerAdded(p)
end)


Players.PlayerRemoving:Connect(function(player)
	local profile = Profiles[player]
	if profile ~= nil then
		profile:Release()
	end
end)

-- Module
local StatService = {}

function StatService:Create(player: Player, name: string, class: string, value: ValueBase)
	if player:IsDescendantOf(Players) then
		local leaderstats = createlsFolder(player)

		local StatInstance = Instance.new(class)
		StatInstance.Name = name
		StatInstance.Value = value
		StatInstance.Parent = leaderstats	

		print("✅ Created "..name.." for "..player.Name)

		--// Save the new stat
		local Profile = GetPlayerProfileAsync(player)

		if Profile then
			local Data = Profile.Data

			if Data[name] then
				--// This stat is saved! Try and load it.
				local Stat = Data[name]
				StatInstance.Value = Stat["Value"]
			else
				--// Stat is new
				Data[name] = {
					["Class"] = class,
					["Name"] = name,
					["Value"] = value
				}
			end

			coroutine.wrap(function()
				while task.wait() do
					local d = GetPlayerProfileAsync(player)
					if d == nil then continue end
					if d.Data[name] == nil then continue end

					StatInstance.Value = d.Data[name]["Value"]
				end
			end)()
			
			return StatInstance
		end
	else
		warn("❌ No valid player was specified!")
	end
end


function StatService:Delete(player, name)
	if player:IsDescendantOf(Players) then
		if player:FindFirstChild("leaderstats") then
			if player.leaderstats:FindFirstChild(name) then
				player.leaderstats:FindFirstChild(name):Destroy()
				local Profile = GetPlayerProfileAsync(player)

				if Profile ~= nil then
					Profile.Data[name] = nil
					print("✅ Deleted \""..name.."\" from "..player.Name)
				end
			else
				warn("❌ No leaderstat to delete.")
			end
		else
			--no ls
			warn("❌ No leaderstats for this profile found")
		end
	else
		warn("❌ No valid player was specified!")
	end
end


function StatService:SetValue(player, name, value)
	if player then
		if player:IsDescendantOf(Players) then
			if player:FindFirstChild("leaderstats") then
				if player.leaderstats:FindFirstChild(name) then
					local Profile = GetPlayerProfileAsync(player)

					if Profile ~= nil then
						Profile.Data[name]["Value"] = value 
						print("✅ Set \""..name.."\" to "..value)
					end
				else
					warn("❌ No leaderstat to edit.")
				end
			else
				--no ls
				warn("❌ No leaderstats for this profile found")
			end
		else
			warn("❌ No valid player was specified!")
		end
	else
		warn("❌ No valid player was specified!")
	end
end

return StatService

Roblox Model

10 Likes

Code used for testing this module:

local StatService = require(game.ServerStorage.StatService)

game.Players.PlayerAdded:Connect(function(plr)
	while task.wait() do
		StatService:Create(plr, "Cash", "IntValue", 0)
		task.wait(2)
		StatService:SetValue(plr, "Cash", 100)
		task.wait(5)
		StatService:Delete(plr, "Cash")
		task.wait(5)
	end
end)

I very much love the effort you put into this, from turning something so basic but boring to something bit more advanced (from module side) but a lot more cooler well done!

1 Like

Wow! Incredible. This will be super easy for beginners.

Well done :slight_smile:

1 Like

Hey i just want to know on how can i remove the debug stuff

1 Like

Assuming you are already using ProfileService, here’s a simpler version.


local ProfileService = pathToProfileService

game.Players.PlayerAdded:Connect(function(player)
      local Profile = -- code that gets players Profile

      local niceNumber = Instance.new("NumberValue",player)

      niceNumber.Changed:Connect(function()
          Profile.Data.Value = value
      end)
end)

I know you are going to say “but with my module you don’t have to need to know how ProfileService works because it does it backend.” To that yeah I guess you are right but using ProfileService is pretty easy and the user should be using ProfileService anyway to store data.

1 Like

Hey! Thanks for being interested in my module.

The easiest way to remove all of the debug information would be to remove all of the print() functions from the StatService module.

You can also remove errors too by removing all warn() functions, but to track errors I’d keep those in.

I completely agree!

For an advanced developer that needs to get their players currently StatService data, they can just use this code in another script:

local ProfileService = require("PROFILE SERVICE")
local DatabaseName = "hhZBG4x9i8"--// DatabaseName from the StatService module
local ProfileTemplate = {}


local Database = ProfileService.GetProfileStore(
	DatabaseName,
	ProfileTemplate
)

game.Players.PlayerAdded:Connect(function(plr)
	local SavedData = Database:LoadProfileAsync("StatService-"..plr.UserId)
end)

However I wouldn’t suggest setting data here, since the module will get confused due to the two different values.

Does :SetValue add on to value or literally set the value?

Example:

If I had 100 coins and used :SetValue(plr, coins, 50) would I have 150 or exactly 50?

Oh thanks.

i tried to find the debugmode thing inside the module script lol

2 Likes

StatService:SetValue will only set the value.

2 Likes

ok so how do i increment a value
do i just use setvalue(player.leaderstats.Cash.Value + 50) ?

1 Like

Yes, that should work just fine.