DataStoreHandler - Player Data Management

DataStoreHandler is a type-safe Luau module for managing player profile data. It provides dot-notation path access, atomic batch operations, deep copy/merge/diff utilities, and seamless integration with ProfileService, ProfileStore, DataStore2, and standard DataStore APIs.

Key Features:

  • Dot-notation paths ("Stats.Health", "Inventory.1.Name")
  • Atomic batch operations (all-or-nothing updates)
  • Copy-on-write optimization
  • Snapshot/Restore mechanics
  • ProfileService, ProfileStore, DataStore, DataStore2 compatible
  • Zero dependencies

Installation

  1. Download DataStoreHandler to your game
  2. Place it in ServerScriptService or your modules folder
  3. Require it in your scripts:
local DataStoreHandler = require(game.ServerScriptService.DataStoreHandler)

Quick Start Examples

Complete Example: Quest System
local Players = game:GetService("Players")
local DataStoreHandler = require(game.ServerScriptService.DataStoreHandler)

local PlayerHandlers = {}

Players.PlayerAdded:Connect(function(player)

	local data = {
		Quests = { Active = {}, Completed = {} },
		Stats = { Level = 1 },
	}

	local handler = DataStoreHandler.New(data, player, false)
	PlayerHandlers[player] = handler

	-- Start a quest
	local function questStart(questId, questName, objective)
		handler:Set("Quests.Active." .. questId, {
			Name = questName,
			Objective = objective,
			Progress = 0,
			StartTime = os.time(),
		})
	end

	-- Update quest progress
	local function questProgress(questId, progress)
		handler:Set("Quests.Active." .. questId .. ".Progress", progress)
		print("Quests.Active." .. questId .. ".Progress", progress.." :", handler:GetRaw())
	end

	-- Complete quest
	local function questComplete(questId)
		local quest = handler:Get("Quests.Active." .. questId)
		print("Quest found:", quest)
		print("Active table:", handler:Get("Quests.Active"))
		if quest then
			local setOk = handler:Set("Quests.Completed." .. questId, quest)
			local removeOk = handler:RemoveByKey("Quests.Active." .. questId)
			print("Set ok:", setOk, "Remove ok:", removeOk)
			print("GetRaw:", handler:GetRaw())
			print("Player:", handler:GetPlayer())
			return true
		end
		warn("Quest not found:", questId)
		
		return false
	end

	-- Character spawn
	player.CharacterAdded:Connect(function(character: Model)
		-- Example usage
		questStart("kill_10_goblins", "Goblin Slayer", "Kill 10 goblins")
		questProgress("kill_10_goblins", 5)
		questProgress("kill_10_goblins", 10)
		questComplete("kill_10_goblins")
	end)
end)

-- Cleanup (IMPORTANT to avoid memory leaks)
Players.PlayerRemoving:Connect(function(player)
  PlayerHandlers[player]:Destroy()
  PlayerHandlers[player] = nil
end)

Example 1: Basic Setup with ProfileService
local Players = game:GetService("Players")
local DataStoreHandler = require(game.ServerScriptService.DataStoreHandler)
local ProfileService = require(game.ServerScriptService.ProfileService)

local STORE_NAME = "PlayerProfiles"
local PROFILE_TEMPLATE = {
    Stats = { Health = 100, Level = 1, Experience = 0 },
    Inventory = {},
    Currency = { Coins = 0, Gems = 0 },
}

local ProfileStore = ProfileService.GetProfileStore(STORE_NAME, PROFILE_TEMPLATE)
local PlayerHandlers = {}

Players.PlayerAdded:Connect(function(player)
    local profile = ProfileStore:LoadProfileAsync("Player_" .. player.UserId)
    
    if not profile then
        player:Kick("Failed to load profile")
        return
    end
    
    profile:Reconcile()
    profile:ListenToRelease(function()
        PlayerHandlers[player] = nil
        player:Kick("Profile released")
    end)
    
    -- Create handler (reference mode to sync with ProfileService)
    local handler, success = DataStoreHandler.New(profile.Data, player, false)
    if not success then
        profile:Release()
        player:Kick("Handler creation failed")
        return
    end
    
    PlayerHandlers[player] = handler
    print("Handler loaded for " .. player.Name)
end)

Players.PlayerRemoving:Connect(function(player)
    local handler = PlayerHandlers[player]
    if handler then
        handler:Destroy()
    end
end)

Example 2: Simple Data Access
local handler = PlayerHandlers[player]

-- Read data
local health = handler:Get("Stats.Health")
local coins = handler:Get("Currency.Coins")
local inventory = handler:Get("Inventory")

-- Write data
handler:Set("Stats.Health", 50)
handler:Set("Stats.Level", 5)

-- Check if exists
if handler:Has("Achievements.Secret") then
    print("Secret achievement unlocked!")
end

Example 3: Numeric Operations
local handler = PlayerHandlers[player]

-- Increment values
handler:Increment("Currency.Coins", 100)        -- Add 100 coins
handler:Increment("Stats.Experience", 50)       -- Add 50 XP

-- Math operations
handler:OperateOnValue("Stats.Health", "-", 25) -- Take 25 damage
handler:OperateOnValue("Stats.Speed", "*", 1.5) -- Apply 50% speed buff

-- Toggle boolean
handler:Toggle("Settings.AudioEnabled")         -- true ↔ false

Example 4: Inventory Management
local handler = PlayerHandlers[player]

-- Add item to inventory
local item = { Name = "Sword", Damage = 25, Rarity = "Rare" }
handler:AddToList("Inventory", item)

-- Find item by name
local index, foundItem = handler:FindInList("Inventory", function(i)
    return i.Name == "Sword"
end)

if index then
    print("Found " .. foundItem.Name .. " at index " .. index)
end

-- Update item
handler:UpdateList("Inventory." .. index, { Name = "Enchanted Sword", Damage = 50 })

-- Remove item
handler:RemoveByKey("Inventory." .. index)

-- Count inventory
local itemCount = handler:CountList("Inventory")
print("Items in inventory: " .. (itemCount or 0))

-- Clear inventory
handler:ClearList("Inventory")

Example 5: Batch Operations (Atomic Updates)
local handler = PlayerHandlers[player]

-- Level up player (all-or-nothing)
local success = handler:BatchSet({
    ["Stats.Level"] = handler:Get("Stats.Level") + 1,
    ["Stats.Experience"] = 0,
    ["Stats.HealthMax"] = handler:Get("Stats.HealthMax") + 10,
    ["Achievements.LevelReached." .. tostring(newLevel)] = true,
})

if success then
    print("Level up successful!")
else
    print("Level up failed - no changes applied")
end

Example 6: Snapshot & Restore (Undo System)
local handler = PlayerHandlers[player]

-- Take a backup before risky operation
local backup = handler:Snapshot()

-- Make changes
handler:Set("Stats.Health", 10)
handler:Increment("Currency.Coins", -1000)

-- Compare what changed
local changes = handler:Diff(backup)
for path, change in changes do
    print(path .. ": " .. tostring(change.old) .. " → " .. tostring(change.new))
end

-- Undo all changes
if handler:Restore(backup) then
    print("Changes reverted!")
end

Example 7: Partial Updates (Patch)
local handler = PlayerHandlers[player]

-- Only update Audio settings, keep other Settings intact
handler:Patch("Settings", {
    Audio = {
        Volume = 0.5,
        Enabled = true,
    }
})

-- Settings.Brightness and other keys remain unchanged

Example 8: Advanced - Quest System
local handler = PlayerHandlers[player]

-- Start a quest
local function startQuest(questId, questData)
    local questPath = "Quests.Active." .. questId
    
    local questInfo = {
        Id = questId,
        Name = questData.Name,
        Progress = 0,
        MaxProgress = questData.MaxProgress,
        Rewards = questData.Rewards,
        StartTime = os.time(),
    }
    
    return handler:Set(questPath, questInfo)
end

-- Update quest progress
local function updateQuestProgress(questId, progress)
    return handler:Set("Quests.Active." .. questId .. ".Progress", progress)
end

-- Complete quest and grant rewards
local function completeQuest(questId)
    local questPath = "Quests.Active." .. questId
    local quest = handler:Get(questPath)
    
    if not quest then return false end
    
    -- Move to completed
    handler:Set("Quests.Completed." .. questId, quest)
    handler:RemoveByKey(questPath)
    
    -- Grant rewards
    if quest.Rewards then
        for rewardType, amount in quest.Rewards do
            handler:Increment("Currency." .. rewardType, amount)
        end
    end
    
    return true
end

-- Usage
startQuest("kill_goblins", {
    Name = "Kill 10 Goblins",
    MaxProgress = 10,
    Rewards = { Coins = 100, Experience = 500 },
})

updateQuestProgress("kill_goblins", 5)
updateQuestProgress("kill_goblins", 10)
completeQuest("kill_goblins")

Example 9: Performance - Compiled Paths
local handler = PlayerHandlers[player]

-- Pre-compile paths used frequently (zero string parsing overhead)
local PATH_COINS = { "Currency", "Coins" }
local PATH_LEVEL = { "Stats", "Level" }
local PATH_HEALTH = { "Stats", "Health" }

-- Daily reward (called many times per second)
local function grantDailyReward()
    return handler:BatchSetCompiled({
        [PATH_COINS] = (handler:Get("Currency.Coins") or 0) + 100,
        [{ "Rewards", "LastDaily" }] = os.time(),
    })
end

-- Combat system - no string parsing overhead
local function takeDamage(amount)
    local currentHealth = handler:Get("Stats.Health") or 100
    return handler:BatchSetCompiled({
        [PATH_HEALTH] = math.max(0, currentHealth - amount),
    })
end

Example 10: RPG Leveling System
local handler = PlayerHandlers[player]

local function levelUpPlayer()
    local currentLevel = handler:Get("Stats.Level") or 1
    local newLevel = currentLevel + 1
    
    -- Atomic level up operation
    local changes = {
        ["Stats.Level"] = newLevel,
        ["Stats.Experience"] = 0,
        ["Stats.HealthMax"] = 100 + (newLevel * 10),
        ["Stats.Health"] = 100 + (newLevel * 10),
        ["Stats.ManaMax"] = 50 + (newLevel * 5),
        ["Stats.Mana"] = 50 + (newLevel * 5),
        ["Achievements.LevelMilestones.Level" .. newLevel] = true,
    }
    
    if handler:BatchSet(changes) then
        print("Level up to " .. newLevel)
        return true
    else
        print("Level up failed")
        return false
    end
end

levelUpPlayer()

API Reference

API Reference

Reading Data

  • Get(path) - Get value at path
  • GetRaw() - Get entire data table
  • GetPlayer() - Get associated player
  • Has(path) - Check if path exists

Writing Data

  • Set(path, value) - Set value at path
  • Reset(path, defaultValue) - Reset to default
  • Patch(path, partialData) - Merge partial updates

Numeric Operations

  • Increment(path, amount) - Add to number
  • OperateOnValue(path, operation, operand) - Math operations (+, -, *, /, %)
  • Toggle(path) - Flip boolean

Batch Operations

  • BatchSet(changes) - Atomic multi-set
  • BatchSetCompiled(changes) - Atomic multi-set with compiled paths

List/Array Operations

  • AddToList(path, value) - Add to array/dict
  • UpdateList(path, value) - Update element
  • RemoveByKey(path) - Remove element
  • RemoveByKeyList(path, keyList) - Remove multiple
  • ClearList(path) - Empty table
  • PopFromList(path, index?) - Remove and return
  • CountList(path) - Get element count
  • FindInList(path, predicate) - Search array
  • MapList(path, transform) - Transform all elements
  • MoveInList(path, fromIndex, toIndex) - Reorder

Snapshots & Restore

  • Snapshot() - Full backup
  • SnapshotPath(path) - Backup one branch
  • Restore(snapshot) - Restore from backup
  • Diff(snapshot) - See what changed

Other

  • Swap(pathA, pathB) - Swap two values
  • Destroy() - Cleanup handler

Integration Guides


DataStore
local DataStoreService = game:GetService("DataStoreService")
local playerStore = DataStoreService:GetDataStore("PlayerProfiles")

Players.PlayerAdded:Connect(function(player)
    local success, data = pcall(function()
        return playerStore:GetAsync("Player_" .. player.UserId)
    end)
    
    if not success or data == nil then
        data = { Stats = { Level = 1, Health = 100 }, Inventory = {} }
    end
    
    local handler = DataStoreHandler.New(data, player, false)
    PlayerHandlers[player] = handler
end)

Players.PlayerRemoving:Connect(function(player)
    local handler = PlayerHandlers[player]
    if handler then
        playerStore:SetAsync("Player_" .. player.UserId, handler:GetRaw())
        handler:Destroy()
    end
end)

DataStore2
local DataStore2 = require(game.ServerScriptService.DataStore2)
DataStore2.Combine("PRIMARY", "Stats", "Inventory")

Players.PlayerAdded:Connect(function(player)
    local primaryStore = DataStore2("PRIMARY", player)
    local data = primaryStore:Get({
        Stats = { Level = 1 },
        Inventory = {},
    })
    
    local handler = DataStoreHandler.New(data, player, false)
    PlayerHandlers[player] = handler
end)

ProfileService & ProfileStore

Integration Guide


Performance Tips

Tips
  1. Pre-compile paths for operations called frequently:

    local PATH_COINS = { "Currency", "Coins" }
    handler:BatchSetCompiled({ [PATH_COINS] = 1000 })
    
  2. Batch operations instead of individual calls:

    -- ❌ Bad
    handler:Set("A", 1)
    handler:Set("B", 2)
    
    -- ✅ Good
    handler:BatchSet({ ["A"] = 1, ["B"] = 2 })
    
  3. Snapshot only what you need:

    -- ✅ Efficient
    local backup = handler:SnapshotPath("Inventory")
    
  4. Use reference mode for ProfileService (no copy):

    -- ✅ Direct sync
    local handler = DataStoreHandler.New(profile.Data, player, false)
    
Benchmark
local DataStoreHandler = require(game.ServerScriptService.DataStoreHandler)


local function makeBigData(size)
 local data = {
 	Stats = {
 		Level = 1,
 		XP = 0,
 	},
 	Inventory = {},
 }

 for i = 1, size do
 	data.Inventory[i] = {
 		Id = i,
 		Name = "Item" .. i,
 		Value = i * 10,
 	}
 end

 return data
end

local function benchmark(label, fn)
 local memBefore = gcinfo()

 local t0 = os.clock()
 fn()
 local dt = os.clock() - t0

 local memAfter = gcinfo()

 print(string.format(
 	"[%s] Time: %.4f ms | Memory: %.2f KB (approx)",
 	label,
 	dt * 1000,
 	memAfter - memBefore
 	))
end

local SIZE = 5000
local rawData = makeBigData(SIZE)

local handler = DataStoreHandler.New(rawData, nil, true)

-- TESTS

benchmark("Get x10000", function()
 for i = 1, 10000 do
 	handler:Get("Stats.Level")
 end
end)

benchmark("Set x5000", function()
 for i = 1, 5000 do
 	handler:Set("Stats.Level", i)
 end
end)

benchmark("BatchSet", function()
 handler:BatchSet({
 	["Stats.Level"] = 50,
 	["Stats.XP"] = 999,
 	["Inventory.1.Value"] = 9999,
 })
end)

local PATH_LEVEL = { "Stats", "Level" }
local PATH_XP = { "Stats", "XP" }
local PATH_ITEM = { "Inventory", 1, "Value" }

benchmark("BatchSetCompiled", function()
 handler:BatchSetCompiled({
 	[PATH_LEVEL] = 50,
 	[PATH_XP] = 999,
 	[PATH_ITEM] = 9999,
 })
end)

benchmark("Snapshot (deepCopy)", function()
 handler:Snapshot()
end)

benchmark("Diff", function()
 local snap = handler:Snapshot()
 handler:Set("Stats.Level", 100)
 handler:Diff(snap)
 print(handler:GetRaw())
end)


Error Handling Example

Summary
local handler, success = DataStoreHandler.New(data, player)

if not success then
    warn("Failed to create handler for " .. player.Name)
    player:Kick()
    return
end

-- Operations return boolean success flags
if not handler:Set("Stats.Level", 5) then
    warn("Failed to set level")
end

if not handler:Increment("Currency.Coins", 100) then
    warn("Failed to increment coins")
end

Documentation
GitHub Repository


Tags

roblox datastore profileservice module

1 Like