Author: FlowGostavo
- Overview
- Installation & Setup
- Core Concepts
- API Reference
- Integration Guides
- Advanced Examples
- Performance Tips
- Troubleshooting
DataStoreHandler is a powerful, type-safe wrapper for managing player profile data in Roblox games. It provides:
- ✅ Dot-notation path access (
"Stats.Health","Inventory.1.Name") - ✅ Atomic batch operations (all-or-nothing updates)
- ✅ Deep copy/merge/diff operations
- ✅ Safe list manipulation (arrays & dictionaries)
- ✅ Copy-on-write optimization for large datasets
- ✅ ProfileService, ProfileStore, DataStore, and DataStore2 compatibility
- ✅ Zero external dependencies
| Feature | Benefit |
|---|---|
| Path-based access | No deep table nesting in code |
| Atomic operations | Data consistency guaranteed |
| Copy-on-write | Memory efficient batch updates |
| Snapshot/Restore | Easy undo/redo mechanics |
| Diff tracking | Know exactly what changed |
Link : https://create.roblox.com/store/asset/119568910221062/DataStoreHandler
Place DataStoreHandler.lua in ServerScriptService or your modules folder:
ServerScriptService/
└── DataStoreHandler.lua
local DataStoreHandler = require(game.ServerScriptService.DataStoreHandler)local handler, success = DataStoreHandler.New(profileData, player)
if not success then
warn("Failed to initialize handler")
return
end
-- Handler is now ready to useA path is a dot-separated string representing a location in your data tree:
-- Given this data structure:
{
Stats = {
Health = 100,
Level = 5
},
Inventory = {
{ Name = "Sword", Damage = 25 },
{ Name = "Shield", Defense = 15 }
}
}
-- These are valid paths:
"Stats" -- The entire Stats table
"Stats.Health" -- The Health number
"Stats.Level" -- The Level number
"Inventory" -- The entire Inventory array
"Inventory.1" -- First inventory slot (note: numeric)
"Inventory.1.Name" -- Name of first item
"Inventory.2.Damage" -- Damage of second item-- Reference mode (default): handler modifies the original table
local handler, ok = DataStoreHandler.New(profileData, player, false)
-- Changing via handler affects profileData directly
-- Copy mode: handler works on an isolated copy
local handler, ok = DataStoreHandler.New(profileData, player, true)
-- Changing via handler does NOT affect profileDataUse copy mode when:
- Testing with external data
- You want isolation for sandboxing
- The source data shouldn't be modified
DataStoreHandler automatically detects the structure:
-- Array (numeric keys)
local inventory = { "Sword", "Shield", "Potion" }
handler:AddToList("Inventory", "Dagger") -- Uses table.insert
-- Dictionary (string keys)
local settings = { Volume = 0.8, Brightness = 1.0 }
handler:AddToList("Settings", { Gamma = 1.2 }) -- Merges key-value pairsCreates a new handler instance.
Parameters:
profileData(table) - The data structure to manageplayer(Player) - The player objectcopy?(boolean) - If true, works on a deep copy (default: false)maxDepth?(number) - Max recursion depth for copy operations (default: 64)
Returns: (DataStoreHandler, boolean) - Handler and success flag
Example:
local data = {
Stats = { Health = 100, Mana = 50 },
Inventory = { "Sword", "Shield" }
}
local player = game.Players.Player1
local handler, success = DataStoreHandler.New(data, player)
if success then
print("Handler created successfully")
endRetrieves the value at the given path.
Parameters:
path(string) - Dot-separated path
Returns: any - Value at path, or nil if not found
Example:
local health = handler:Get("Stats.Health") -- 100
local itemName = handler:Get("Inventory.1.Name") -- "Sword"
local coins = handler:Get("Currency.Coins") -- nil if not setReturns the entire data table without copying.
Returns: The raw internal data table
Example:
local allData = handler:GetRaw()
print(allData.Stats.Health) -- Direct accessReturns the player associated with this handler.
Returns: Player object or nil
Example:
local player = handler:GetPlayer()
if player and player.Parent == game.Players then
print(player.Name .. " is still in game")
endChecks if a non-nil value exists at the path.
Parameters:
path(string) - Dot-separated path
Returns: boolean - true if value exists
Example:
if handler:Has("Quests.MainQuest.Completed") then
print("Player has completed main quest")
endSets a value at the given path, creating intermediate tables as needed.
Parameters:
path(string) - Dot-separated pathvalue(any) - Value to set
Returns: boolean - Success flag
Example:
handler:Set("Stats.Health", 50)
handler:Set("Inventory.1.Equipped", true)
handler:Set("Settings.Audio.Volume", 0.75)
-- Creates nested structure automatically
handler:Set("Achievements.Secret.Unlocked", true) -- OK even if Achievements or Secret don't existResets a value to its default (alias for Set with clearer intent).
Parameters:
path(string) - Dot-separated pathdefaultValue(any) - Value to reset to
Returns: boolean - Success flag
Example:
handler:Reset("Stats.Health", 100) -- Restore full health
handler:Reset("Inventory", {}) -- Clear inventory back to empty
handler:Reset("Buffs.Active", false) -- Deactivate buffsApplies multiple Set operations atomically (all succeed or all fail).
Parameters:
changes(table) - Map of paths to values
Returns: boolean - Success flag
Example:
-- Level up the player
local success = handler:BatchSet({
["Stats.Level"] = handler:Get("Stats.Level") + 1,
["Stats.Experience"] = 0,
["Stats.HealthMax"] = handler:Get("Stats.HealthMax") + 10,
["Achievements.LevelMilestones." .. tostring(newLevel)] = true,
})
if not success then
print("Level up failed - no changes applied")
endLike BatchSet, but with pre-parsed paths (faster, lower GC pressure).
Parameters:
changes(table) - Map of pre-parsed path tables to values
Returns: boolean - Success flag
Example:
-- Pre-compile paths once
local PATH_COINS = { "Currency", "Coins" }
local PATH_LEVEL = { "Stats", "Level" }
local PATH_XP = { "Stats", "Experience" }
-- Use many times with zero string parsing
local success = handler:BatchSetCompiled({
[PATH_COINS] = 5000,
[PATH_LEVEL] = 10,
[PATH_XP] = 2500,
})Deep-merges partial data into the table at path (non-destructive).
Parameters:
path(string) - Dot-separated path to target tablepartialData(table) - Partial updates to merge
Returns: boolean - Success flag
Example:
-- Without Patch (overwrites entire Settings)
handler:Set("Settings", { Volume = 0.5, Brightness = 1.0 })
-- With Patch (only updates Volume, keeps other keys)
handler:Patch("Settings", { Volume = 0.5 }) -- Brightness unchangedAdds to a number at the given path (defaults to 0 if not set).
Parameters:
path(string) - Dot-separated pathamount(number) - Amount to add
Returns: boolean - Success flag
Example:
handler:Increment("Currency.Coins", 100) -- +100 coins
handler:Increment("Stats.Experience", 50) -- +50 XP
handler:Increment("Counters.DeathCount", 1) -- Death counter++Performs mathematical operations: +, -, *, /, %
Parameters:
path(string) - Dot-separated pathoperation(string) - One of:"+","-","*","/","%"operand(number) - Right-hand operand
Returns: boolean - Success flag
Example:
handler:OperateOnValue("Stats.Health", "-", 25) -- Damage
handler:OperateOnValue("Stats.Speed", "*", 1.5) -- Buff
handler:OperateOnValue("Currency.Coins", "/", 2) -- Split coins
handler:OperateOnValue("Stats.Mana", "%", 100) -- Modulo
-- Division by zero returns false
if not handler:OperateOnValue("Value", "/", 0) then
print("Cannot divide by zero")
endFlips a boolean value.
Parameters:
path(string) - Dot-separated path to boolean
Returns: boolean - Success flag
Example:
handler:Toggle("Buffs.SpeedBoost") -- true → false
handler:Toggle("Settings.Audio.Enabled") -- false → true
handler:Toggle("Achievements.Hidden") -- Toggle hidden statusAdds an element to a list (array or dictionary).
Parameters:
path(string) - Dot-separated path to listvalue(any) - Value to add
Returns: boolean - Success flag
Example:
-- Array mode (numeric keys detected)
handler:AddToList("Inventory", { Name = "Sword", Damage = 25 })
handler:AddToList("Inventory", { Name = "Shield", Defense = 15 })
-- Inventory is now: [{ Name="Sword", Damage=25 }, { Name="Shield", Defense=15 }]
-- Dictionary mode (string keys detected)
handler:AddToList("PlayerStats", { Kills = 5 })
handler:AddToList("PlayerStats", { Deaths = 2 })
-- PlayerStats is now: { Kills = 5, Deaths = 2 }Replaces an existing element (must already exist).
Parameters:
path(string) - Dot-separated path to elementvalue(any) - New value
Returns: boolean - Success flag
Example:
-- Update first inventory slot
handler:UpdateList("Inventory.1", { Name = "Better Sword", Damage = 50 })
-- Update a stat
handler:UpdateList("PlayerStats.Kills", 10)Removes an element at the last path segment.
Parameters:
path(string) - Dot-separated path to element to remove
Returns: boolean - Success flag
Example:
-- Remove from numeric array (contiguous, uses table.remove)
handler:RemoveByKey("Inventory.1") -- Shifts remaining items
-- Remove from dictionary (sets to nil)
handler:RemoveByKey("PlayerStats.Kills")Removes multiple elements in one call (efficient).
Parameters:
path(string) - Dot-separated path to containerkeyList(table) - Array or map of keys to remove
Returns: boolean - Success flag
Example:
-- Remove array indices (automatically sorted descending to prevent shifts)
handler:RemoveByKeyList("Inventory", { 1, 3, 5 })
-- Remove dict keys
handler:RemoveByKeyList("PlayerStats", { "Deaths", "Losses" })
-- Remove dict entries by matching properties
handler:RemoveByKeyList("Quests", {
["MainQuest"] = { Status = "Failed" } -- Only remove if Status == "Failed"
})Empties a table without destroying it (references remain valid).
Parameters:
path(string) - Dot-separated path to table
Returns: boolean - Success flag
Example:
handler:ClearList("Inventory") -- Remove all items
handler:ClearList("ActiveBuffs") -- Deactivate all buffs
handler:ClearList("Quests.Completed") -- Clear completed questsRemoves and returns one element (from end if no index).
Parameters:
path(string) - Dot-separated path to arrayindex?(number) - Index to pop (default: end of array)
Returns: any - Popped element, or nil if empty
Example:
local lastItem = handler:PopFromList("Inventory") -- Pop last
local firstItem = handler:PopFromList("Inventory", 1) -- Pop firstReturns the number of elements (array or dictionary).
Parameters:
path(string) - Dot-separated path to table
Returns: number? - Element count, or nil if not a table
Example:
local inventorySize = handler:CountList("Inventory")
local statCount = handler:CountList("PlayerStats")
if not inventorySize or inventorySize == 0 then
print("Inventory is empty")
endSearches for the first element matching a predicate.
Parameters:
path(string) - Dot-separated path to arraypredicate(function) - Test function returning boolean
Returns: (number?, any) - Index and value, or (nil, nil) if not found
Example:
-- Find sword in inventory
local idx, item = handler:FindInList("Inventory", function(item)
return item.Name == "Sword"
end)
if idx then
print("Sword found at index " .. idx)
else
print("Sword not found")
end
-- Find first equipped item
local idx, item = handler:FindInList("Inventory", function(item)
return item.Equipped == true
end)
-- Find first weak enemy
local idx, enemy = handler:FindInList("Enemies", function(enemy)
return enemy.Health < 30
end)Applies a transformation function to every element.
Parameters:
path(string) - Dot-separated path to tabletransform(function) - Function taking value, returning new value
Returns: boolean - Success flag
Example:
-- Double all stat multipliers
handler:MapList("Stats.Multipliers", function(val)
return val * 2
end)
-- Add 1 to all item durations
handler:MapList("Buffs.Active", function(buff)
buff.Duration = buff.Duration + 1
return buff
end)
-- Increase all inventory item prices by 10%
handler:MapList("Inventory", function(item)
item.Price = item.Price * 1.1
return item
end)Moves an element from one index to another (array only).
Parameters:
path(string) - Dot-separated path to arrayfromIndex(number) - Current indextoIndex(number) - Target index
Returns: boolean - Success flag
Example:
-- Reorder inventory
handler:MoveInList("Inventory", 1, 3) -- Move first item to third position
-- Rearrange quest order
handler:MoveInList("Quests", 5, 1) -- Move fifth quest to firstAdds a new property to a list item.
Parameters:
path(string) - Dot-separated path to property (parent is the item)value(any) - Value to set
Returns: boolean - Success flag
Example:
-- Given path "Inventory.1.Enchanted", sets Inventory.1.Enchanted = value
handler:AddPropertyToListItem("Inventory.1.Enchanted", true)
-- Add equipped status to item
handler:AddPropertyToListItem("Inventory.2.Equipped", false)
-- Add durability tracking
handler:AddPropertyToListItem("Inventory.3.CurrentDurability", 100)Updates a property on a list item (creates intermediate tables).
Parameters:
path(string) - Dot-separated path to propertyvalue(any) - Value to set
Returns: boolean - Success flag
Example:
handler:UpdateListItemProperty("Inventory.1.Durability", 85)
handler:UpdateListItemProperty("Quests.1.Progress", 50)Removes a property from a list item.
Parameters:
path(string) - Dot-separated path to property
Returns: boolean - Success flag
Example:
handler:RemovePropertyFromListItem("Inventory.1.Enchanted")
handler:RemovePropertyFromListItem("Quests.2.Reward")Creates a deep copy of the entire current data state.
Returns: Isolated copy of all data
Example:
-- Take a backup
local backup = handler:Snapshot()
-- Make changes
handler:Set("Stats.Health", 50)
handler:Increment("Currency.Coins", -100)
-- Restore if needed
handler:Restore(backup)Creates a deep copy of just one branch.
Parameters:
path(string) - Dot-separated path to section
Returns: Isolated copy of that section, or nil if not found
Example:
-- Backup just inventory
local inventoryBackup = handler:SnapshotPath("Inventory")
-- Backup just stats
local statsBackup = handler:SnapshotPath("Stats")
-- Modify
handler:Set("Inventory.1.Name", "Broken Sword")
-- Restore just inventory
handler:Restore(inventoryBackup)Replaces all data with a snapshot.
Parameters:
snapshot(table) - A snapshot from Snapshot()
Returns: boolean - Success flag
Example:
if handler:Restore(backup) then
print("Data restored successfully")
endCompares current data against a snapshot and returns all differences.
Parameters:
snapshot(table) - Reference snapshot
Returns: Table of changes: { "Path.To.Value" = { old = oldVal, new = newVal } }
Example:
local before = handler:Snapshot()
-- Make changes
handler:Set("Stats.Health", 50)
handler:Increment("Currency.Coins", 100)
handler:Set("LastLogin", os.time())
-- See what changed
local changes = handler:Diff(before)
for path, change in changes do
print(path .. ": " .. tostring(change.old) .. " → " .. tostring(change.new))
end
-- Output:
-- Stats.Health: 100 → 50
-- Currency.Coins: 0 → 100
-- LastLogin: nil → 1714521600Swaps the values at two paths (with rollback on failure).
Parameters:
pathA(string) - First pathpathB(string) - Second path
Returns: boolean - Success flag
Example:
-- Swap health and mana (why? just for demo)
handler:Swap("Stats.Health", "Stats.Mana")
-- Swap first and second inventory items
handler:Swap("Inventory.1", "Inventory.2")
-- Swap currencies
handler:Swap("Currency.Gold", "Currency.Silver")Clears all references in the handler.
Example:
handler:Destroy()
handler = nil -- Allow garbage collectionProfileService is a popular profile management library. DataStoreHandler integrates seamlessly:
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 },
Settings = { Volume = 1.0, Brightness = 1.0 },
}
local ProfileStore = ProfileService.GetProfileStore(STORE_NAME, PROFILE_TEMPLATE)
local PlayerHandlers = {}
Players.PlayerAdded:Connect(function(player)
-- Load profile from ProfileService
local profile = ProfileStore:LoadProfileAsync("Player_" .. player.UserId)
if not profile then
player:Kick("Failed to load profile")
return
end
-- Bind release function
profile:Reconcile() -- Fill missing keys with template
profile:ListenToRelease(function()
PlayerHandlers[player] = nil
player:Kick("Profile released")
end)
-- Create handler (reference mode to work directly with profile.Data)
local handler, success = DataStoreHandler.New(profile.Data, player, false)
if not success then
profile:Release()
player:Kick("Failed to create handler")
return
end
PlayerHandlers[player] = handler
player.CharacterAdded:Connect(function()
-- Restore health on spawn
local health = handler:Get("Stats.Health")
player.Character.Humanoid.Health = health
end)
end)
Players.PlayerRemoving:Connect(function(player)
local handler = PlayerHandlers[player]
if handler then
handler:Destroy()
end
end)
-- Usage
game.Players.Player1.CharacterAdded:Connect(function(char)
local handler = PlayerHandlers[game.Players.Player1]
handler:Increment("Currency.Coins", 50)
handler:Increment("Stats.Experience", 100)
end)local Players = game:GetService("Players")
local DataStoreHandler = require(game.ServerScriptService.DataStoreHandler)
local ProfileStore = require(game.ServerScriptService.ProfileStore)
local store = ProfileStore.new("PlayerData", {
Version = 1,
Data = {
Stats = { Level = 1, Health = 100 },
Inventory = {},
}
})
local playerSessions = {}
Players.PlayerAdded:Connect(function(player)
-- Load with ProfileStore
local userId = tostring(player.UserId)
store:LoadAsync(userId, function(profile)
if profile then
-- Create handler
local handler, ok = DataStoreHandler.New(profile, player, false)
if ok then
playerSessions[player] = handler
print("Handler loaded for " .. player.Name)
end
end
end)
end)
Players.PlayerRemoving:Connect(function(player)
local handler = playerSessions[player]
if handler then
local dataToSave = handler:GetRaw()
local userId = tostring(player.UserId)
store:SaveAsync(userId, dataToSave)
handler:Destroy()
playerSessions[player] = nil
end
end)DataStore2 provides layered caching:
local Players = game:GetService("Players")
local DataStoreHandler = require(game.ServerScriptService.DataStoreHandler)
local DataStore2 = require(game.ServerScriptService.DataStore2)
DataStore2.Combine("PRIMARY", "Stats", "Inventory", "Currency")
local handlers = {}
Players.PlayerAdded:Connect(function(player)
local primaryStore = DataStore2("PRIMARY", player)
-- Get or create default
local data = primaryStore:Get({
Stats = { Level = 1, Health = 100 },
Inventory = {},
Currency = { Coins = 0 },
})
-- Create handler
local handler, ok = DataStoreHandler.New(data, player, false)
if ok then
handlers[player] = { handler = handler, store = primaryStore }
-- Listen to changes from DataStore2
primaryStore:OnUpdate(function(newData)
handler:Restore(newData)
end)
end
end)
Players.PlayerRemoving:Connect(function(player)
local session = handlers[player]
if session then
session.store:Set(session.handler:GetRaw())
session.handler:Destroy()
handlers[player] = nil
end
end)local Players = game:GetService("Players")
local DataStoreService = game:GetService("DataStoreService")
local DataStoreHandler = require(game.ServerScriptService.DataStoreHandler)
local playerStore = DataStoreService:GetDataStore("PlayerProfiles")
local handlers = {}
local DEFAULT_DATA = {
Stats = { Level = 1, Health = 100 },
Inventory = {},
Currency = { Coins = 0 },
}
Players.PlayerAdded:Connect(function(player)
local userId = player.UserId
local success, data = pcall(function()
return playerStore:GetAsync("Player_" .. userId)
end)
if success and data == nil then
data = deepCopy(DEFAULT_DATA)
elseif not success then
print("Failed to load data: " .. tostring(data))
player:Kick()
return
end
local handler, ok = DataStoreHandler.New(data, player, false)
if ok then
handlers[player] = handler
end
end)
Players.PlayerRemoving:Connect(function(player)
local handler = handlers[player]
if handler then
local data = handler:GetRaw()
local success, err = pcall(function()
playerStore:SetAsync("Player_" .. player.UserId, data)
end)
if not success then
warn("Failed to save: " .. err)
end
handler:Destroy()
handlers[player] = nil
end
end)
function deepCopy(t)
local new = {}
for k, v in t do
new[k] = type(v) == "table" and deepCopy(v) or v
end
return new
endlocal handler = handlers[player]
-- Level up player
local function levelUpPlayer()
local currentLevel = handler:Get("Stats.Level") or 1
local newLevel = currentLevel + 1
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.Milestones.Level" .. newLevel] = true,
}
if handler:BatchSet(changes) then
print("Level up to " .. newLevel)
end
end
-- Grant skill
local function learnSkill(skillName, skillData)
local skillPath = "Skills." .. skillName
return handler:Set(skillPath, skillData)
end
learnSkill("Fireball", { Cooldown = 5, Damage = 25, ManaCost = 30 })local handler = handlers[player]
-- Add item to inventory
local function addItem(itemName, itemStats, quantity)
quantity = quantity or 1
for i = 1, quantity do
local item = {
Name = itemName,
Stats = itemStats,
Acquired = os.time(),
Equipped = false,
Durability = 100,
}
handler:AddToList("Inventory", item)
end
end
-- Equip item
local function equipItem(index)
-- Unequip all first
handler:MapList("Inventory", function(item)
item.Equipped = false
return item
end)
-- Equip selected
local item = handler:Get("Inventory." .. index)
if item then
handler:AddPropertyToListItem("Inventory." .. index .. ".Equipped", true)
print("Equipped: " .. item.Name)
end
end
-- Find and remove item
local function removeItemByName(itemName)
local idx, item = handler:FindInList("Inventory", function(i)
return i.Name == itemName
end)
if idx then
handler:RemoveByKey("Inventory." .. idx)
return true
end
return false
end
-- Get inventory value
local function getTotalInventoryValue()
local total = 0
local count = handler:CountList("Inventory") or 0
for i = 1, count do
local item = handler:Get("Inventory." .. i)
if item and item.Stats and item.Stats.Value then
total += item.Stats.Value
end
end
return total
end
-- Usage
addItem("Sword", { Damage = 25, Value = 100 }, 1)
addItem("Health Potion", { Healing = 50, Value = 25 }, 5)
equipItem(1)
print("Inventory worth: " .. getTotalInventoryValue())local handler = handlers[player]
local saveSlots = {}
-- Create save point
local function createSavePoint(slotName)
local snapshot = handler:Snapshot()
saveSlots[slotName] = {
data = snapshot,
timestamp = os.time(),
}
print("Saved to slot: " .. slotName)
end
-- Load save point
local function loadSavePoint(slotName)
local slot = saveSlots[slotName]
if not slot then
return false
end
if handler:Restore(slot.data) then
print("Loaded from slot: " .. slotName)
return true
end
return false
end
-- Compare state
local function compareStates(slotName)
local slot = saveSlots[slotName]
if not slot then
return {}
end
return handler:Diff(slot.data)
end
-- Usage
createSavePoint("before_boss")
-- Battle and take damage
handler:Set("Stats.Health", 10)
local changes = compareStates("before_boss")
for path, change in changes do
print(path .. ": " .. change.old .. " → " .. change.new)
end
loadSavePoint("before_boss") -- Undo changeslocal handler1 = handlers[player1]
local handler2 = handlers[player2]
-- Propose trade
local function proposeTrade(sourceHandler, targetHandler, sourceItems, targetItems, amount)
-- Validate
local sourceInventorySize = sourceHandler:CountList("Inventory") or 0
if sourceInventorySize < #sourceItems then
return false, "Not enough inventory space"
end
-- Execute (atomic)
local success = sourceHandler:BatchSet({
["Inventory"] = {}, -- Clear inventory
})
if not success then
return false, "Trade failed"
end
-- Transfer items
for _, item in sourceItems do
targetHandler:AddToList("Inventory", item)
end
-- Transfer currency
targetHandler:Increment("Currency.Coins", amount)
sourceHandler:Increment("Currency.Coins", -amount)
return true
end
-- Usage
local sword = { Name = "Sword", Damage = 25 }
local shield = { Name = "Shield", Defense = 15 }
proposeTrade(handler1, handler2, { sword }, { shield }, 50)local handler = handlers[player]
-- Start quest
local function startQuest(questId, questData)
local questPath = "Quests.Active." .. questId
local questInfo = {
Id = questId,
Name = questData.Name,
Description = questData.Description,
Progress = 0,
MaxProgress = questData.MaxProgress,
Rewards = questData.Rewards,
StartTime = os.time(),
Status = "In Progress",
}
return handler:Set(questPath, questInfo)
end
-- Update quest progress
local function updateQuestProgress(questId, progress)
local questPath = "Quests.Active." .. questId
return handler:Set(questPath .. ".Progress", progress)
end
-- Complete quest
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
-- Get active quests
local function getActiveQuests()
local quests = {}
local activeQuestsList = handler:Get("Quests.Active") or {}
for questId, questData in activeQuestsList do
table.insert(quests, {
Id = questId,
Data = questData,
})
end
return quests
end
-- Usage
startQuest("kill_goblins", {
Name = "Kill Goblins",
Description = "Defeat 10 goblins",
MaxProgress = 10,
Rewards = { Coins = 100, Experience = 500 },
})
updateQuestProgress("kill_goblins", 5)
updateQuestProgress("kill_goblins", 10)
completeQuest("kill_goblins")-- Pre-compile frequently used paths
local PATH_COINS = { "Currency", "Coins" }
local PATH_LEVEL = { "Stats", "Level" }
local PATH_HEALTH = { "Stats", "Health" }
local PATH_MANA = { "Stats", "Mana" }
local PATH_XP = { "Stats", "Experience" }
-- Daily reward
local function grantDailyReward()
return handler:BatchSetCompiled({
[PATH_COINS] = (handler:Get("Currency.Coins") or 0) + 100,
[{ "Rewards", "LastDaily" }] = os.time(),
})
end
-- Combat system (called many times)
local function takeDamage(amount)
local currentHealth = handler:Get("Stats.Health") or 100
return handler:BatchSetCompiled({
[PATH_HEALTH] = math.max(0, currentHealth - amount),
})
end
local function gainExperience(amount)
local currentXp = handler:Get("Stats.Experience") or 0
local newXp = currentXp + amount
local currentLevel = handler:Get("Stats.Level") or 1
if newXp >= 1000 then
return handler:BatchSetCompiled({
[PATH_XP] = 0,
[PATH_LEVEL] = currentLevel + 1,
})
else
return handler:BatchSetCompiled({
[PATH_XP] = newXp,
})
end
end
-- Usage (no string parsing overhead)
for i = 1, 100 do
gainExperience(50)
end-- ❌ BAD: String parsed every call
for i = 1, 1000 do
handler:Increment("Currency.Coins", 1)
end
-- ✅ GOOD: Parse once, use many times
local PATH_COINS = { "Currency", "Coins" }
for i = 1, 1000 do
handler:BatchSetCompiled({ [PATH_COINS] = handler:Get("Currency.Coins") + 1 })
end-- ❌ BAD: 5 individual operations
handler:Set("Stats.Level", 5)
handler:Set("Stats.Health", 100)
handler:Set("Stats.Mana", 50)
handler:Set("Currency.Coins", 1000)
handler:Set("LastUpdate", os.time())
-- ✅ GOOD: 1 atomic operation
handler:BatchSet({
["Stats.Level"] = 5,
["Stats.Health"] = 100,
["Stats.Mana"] = 50,
["Currency.Coins"] = 1000,
["LastUpdate"] = os.time(),
})-- ❌ BAD: Copy entire data structure
local fullBackup = handler:Snapshot()
-- ✅ GOOD: Copy just the section you need
local inventoryBackup = handler:SnapshotPath("Inventory")-- Reference mode: Fast, but modifies original
local handler = DataStoreHandler.New(data, player, false)
-- Copy mode: Slightly slower, but safe
local handler = DataStoreHandler.New(data, player, true)-- ❌ AVOID: Very deep structures
{
Level1 = {
Level2 = {
Level3 = {
Level4 = {
Value = 100
}
}
}
}
}
-- ✅ PREFER: Flatter structures
{
Level1Value = 100,
Level2Value = 200,
}Cause: Data structure is too deeply nested.
Solution:
-- Increase max depth when creating handler
local handler, ok = DataStoreHandler.New(data, player, true, 128) -- default is 64Cause: Trying to write to a path where an intermediate node is a non-table type.
Solution:
-- Check before writing
if handler:Get("Path.To.Node") == nil or type(handler:Get("Path.To.Node")) == "table" then
handler:Set("Path.To.Node.SubKey", value)
endCause: Trying to divide or modulo by 0.
Solution:
if handler:OperateOnValue("Path", "/", 0) then
print("Success")
else
print("Division by zero prevented")
endCause: This shouldn't happen—BatchSet is atomic. If it fails, no changes apply.
Debug:
local success = handler:BatchSet(changes)
if not success then
-- Check console for warnings
-- No changes were applied
endCauses & Solutions:
- String parsing every call → Use compiled paths
- Individual operations → Use BatchSet
- Copying huge structures → Use SnapshotPath
- Deep structures → Flatten or increase maxDepth carefully
- ✅ Use dot-notation paths for clarity
- ✅ Batch related operations together
- ✅ Pre-compile frequently used paths
- ✅ Check return values (boolean success flags)
- ✅ Use Snapshot before risky operations
- ✅ Call Destroy() when done
- ✅ Use copy mode for external/test data
- ✅ Validate data before writing
- ✅ Handle ProfileService/DataStore2 profile releases
- ✅ Keep data structures relatively flat
- ✅ Use typed paths with compiled constants
- ✅ Monitor for circular references in deep data
local Players = game:GetService("Players")
local DataStoreHandler = require(game.ServerScriptService.DataStoreHandler)
-- Pre-compiled paths for performance
local PATHS = {
LEVEL = { "Stats", "Level" },
HEALTH = { "Stats", "Health" },
COINS = { "Currency", "Coins" },
INVENTORY = { "Inventory" },
}
local handlers = {}
-- Player joins
Players.PlayerAdded:Connect(function(player)
local data = {
Stats = { Level = 1, Health = 100, Experience = 0 },
Currency = { Coins = 0, Gems = 0 },
Inventory = {},
Settings = { Volume = 1.0 },
}
local handler, ok = DataStoreHandler.New(data, player, false)
if ok then
handlers[player] = handler
print("Handler created for " .. player.Name)
else
player:Kick("Failed to create handler")
end
end)
-- Player leaves
Players.PlayerRemoving:Connect(function(player)
local handler = handlers[player]
if handler then
local data = handler:GetRaw()
-- Save data here
handler:Destroy()
handlers[player] = nil
end
end)
-- Give reward
local function giveReward(player, coins, xp)
local handler = handlers[player]
if not handler then return end
handler:BatchSetCompiled({
[PATHS.COINS] = (handler:Get("Currency.Coins") or 0) + coins,
[PATHS.XP] = (handler:Get("Stats.Experience") or 0) + xp,
})
end
-- Test
giveReward(game.Players.Player1, 100, 50)