Datastore Script - Overhaul

Hello!

I have overhauled my data script to include recursions instead of what was previously spaghetti code. Was looking for some feedback as well as how I could further improve it. I am somewhat proud of how the script looks and the work I have done.

So I am looking for feedback on:
Requesting data: if data is requested more than once within six seconds, the server will kick the player.
convertDataToInstance and convertInstanceToData functions.
toboolean function: Is there an easier way to do it?
Variable names

Quick overview of what each function and table does:

playersRequestingData table - Shows users who have requested data within the past 6 seconds
defaultData table - Default data, what each player’s data should be checked to include
instanceType table - Shows instance equivalent of the instance type.

toboolean function - Checks if inputted string can be converted into a boolean value.
convertDataToInstance function - Goes through the data and converts it into instances using the instanceType table.
verifyData function - Verifies that all values needed are inside of the data, otherwise puts it to the default value (from defaultData table)
requestData.OnServerInvoke function - Function to request data. Returns either true or false (whether data was successful or not). Client requests data 6 times and waits 10 seconds between each request before alerting the user that datastores may not be working if :GetAsync was not successful.
saveData function - Self explanatory. Converts the instances to a table and then sets the player’s data.

Script:

local dataStoreService = game:GetService('DataStoreService')
local players = game:GetService('Players')
local httpService = game:GetService('HttpService')
local marketplaceService = game:GetService('MarketplaceService')
local replicatedStorage = game:GetService('ReplicatedStorage')

local dataStore = dataStoreService:GetDataStore('PLAYERDATA')

local remotes = replicatedStorage:WaitForChild('Remotes')

local dataFinished = remotes:WaitForChild('DataFinished')
local requestData = remotes:WaitForChild('RequestData')

local playersRequestingData = {}

local defaultData = {
    ['Coins'] = 0;
    ['Wins'] = 0;
    ['Completed Obbies'] = 0;
    ['Inventory'] = {
        ['Accessories'] = {};
        ['Packages'] = {};
        ['Gear'] = {};
        ['Trails'] = {};
    };
    ['Passes'] = {
        ['Premium'] = false;
    };
    ['Rank'] = 'AlphaTester';
    ['Permission'] = 1;
    ['Settings'] = {
        ['UIPrimaryColour'] = {
            ['R'] = 86;
            ['G'] = 146;
            ['B'] = 127;
        };
        ['UISecondaryColour'] = {
            ['R'] = 255;
            ['G'] = 255;
            ['B'] = 255;
        };
        ['UIFont'] = 'SourceSans';
        ['GlobalShadows'] = true;
        ['PlayerMode'] = 'Visible';
        ['ResetKeybind'] = 'R';
    };        
}

local instanceTypes = {
    ['boolean'] = 'BoolValue';
    ['string'] = 'StringValue';
    ['number'] = 'NumberValue';
}

function toboolean(str)
    if string.lower(tostring(str)) == 'true' then
        return true
    elseif string.lower(tostring(str)) == 'false' then
        return false
    else
        return nil
    end
end

function convertDataToInstance(data, folder)
    
    function dataFunction(data, folder)
        for i,v in pairs(data) do
            local boolean = toboolean(v)
            if boolean ~= nil then
                data[i] = boolean
            end
            local number = tonumber(v)
            if number then
                data[i] = number
            end
            if typeof(v) == 'table' then
                local currentFolder = Instance.new('Folder', folder)
                currentFolder.Name = i
                dataFunction(v, currentFolder)
            else
                for i2,v2 in pairs(instanceTypes) do
                    if typeof(v) == i2 then
                        local currentValue = Instance.new(v2, folder)
                        currentValue.Name = i
                        currentValue.Value = v
                    end
                end
            end
        end
    end
    dataFunction(data, folder)
    
    dataFinished:FireClient(folder.Parent)
end


local function verifyData(data, player)

    function verifyCurrentTable(tableToVerify, currentIteration)
        local tableToReturn = {}
        for i,v in pairs(currentIteration) do
            if tableToVerify[i] then
                tableToReturn[i] = tableToVerify[i]
                if typeof(tableToVerify[i]) == 'table' then
                    tableToReturn[i] = tableToVerify[i]
                    verifyCurrentTable(tableToReturn[i], v)
                else
                    tableToReturn[i] = tableToVerify[i]
                end
            else
                tableToReturn[i] = v
            end
        end
        return tableToReturn
    end
    local dataFolder = Instance.new('Folder', player)
    dataFolder.Name = 'PlayerData'

    local newData = verifyCurrentTable(data, defaultData)

    if newData['Passes']['Premium'] == false then
        if marketplaceService:UserOwnsGamePassAsync(player.UserId, 13535231) then
            newData['Passes']['Premium'] = true
        end
    end

    if newData['Passes']['Premium'] then
        newData['Rank'] = 'Premium'
    end

    convertDataToInstance(newData, dataFolder)
end

requestData.OnServerInvoke = function(player)
    local key = 'PLAYERDATA_'..player.UserId

    local data
    
    if not table.find(playersRequestingData, player) then
        table.insert(playersRequestingData, #playersRequestingData + 1, player)
        coroutine.wrap(function()
            wait(6)
            table.remove(playersRequestingData, table.find(playersRequestingData, player))
        end)()
    else
        return player:Kick('Too many data requests!')
    end
    
    local successful, errorMessage = pcall(function()
        data = dataStore:GetAsync(key)
    end)
    print(data)
    if successful then
        if not data then
            data = defaultData
        end
        verifyData(data, player)
    else
        print(tostring(errorMessage))
    end
    return successful
end

function saveData(player)
    local key = 'PLAYERDATA_'..player.UserId
    local playerFolder = player:FindFirstChild('PlayerData')

    function convertInstanceToData(currentFolder)
        local returnedData = {}
        for i,v in pairs(currentFolder:GetChildren()) do
            if v:IsA('Folder') then
                returnedData[v.Name] = convertInstanceToData(v)
            else
                returnedData[v.Name] = v.Value
            end
        end
        return returnedData
    end

    local constructedTable = convertInstanceToData(playerFolder)

    print(constructedTable)

    local successful, errorMessage = pcall(function()
        dataStore:SetAsync(key, constructedTable)
    end)

    if not successful then
        warn(tostring(errorMessage))
    end
end

players.PlayerRemoving:Connect(saveData)

game:BindToClose(function()
    for i, player in pairs(players:GetPlayers()) do
        saveData(player)
    end
end)
2 Likes

Looks decent. A few things I would like to mention.

  1. Never globalize your functions unless the function is a key in a table. Globals are often never used since they are store in the heap and are slower to reference than the stack. It’s good practice to always localize them.

  2. In your ‘toBoolean’ function, returning nil is pointless. When a function returns nothing, it automatically returns nil. ‘nil’ means nothing after all.

  3. Yes, I understand this is premature optimization. But, you should never use the second argument of Instance.new()
    A thread on why Instance.new() is not recommended.

  4. If you are appending a new key into an array, array[#array + 1] would be a more optimal solution than table.insert. Yes, I’m unnecessarily optimizing by 0.01 again :((.

  5. When you iterate through the children of an instance, always use ipairs as your iterator factory. The :GetChildren method returns an array and therefore ipairs would be a better choice than pairs if we are talking about speed. Pairs would be better to use when iterating through dictionaries.

  6. Cool trick: In your ‘convertInstanceToData’ function, short circuit programming would be a good implementation. (This is not needed at all, I’m not discouraging if statements).

returnedData[v.Name] = v:IsA(‘Folder’) and convertInstanceToValue(v) or v.Value

As you can see these lines can get super lengthy, and lengthy lines aren’t good for readability. But when u have a short snippet of code and you are familiar with reading them, it’s perfectly readable.

  1. In your ‘verifyCurrentTable’ function, you seem to use the typeof function on a data type you commonly see in lua (not roblox lua / luau). typeof is meant to be used specifically on luau datatypes such as Vector3, etc… And it has been benchmarked, type is faster than typeof. So even if typeof does work, it’s still not the optimal solution.

  2. Your ‘toBoolean’ function is not useful at all. I wouldn’t recommend having string values called ‘true’ or ‘false in the first place. It’s just a waste of function calling. Even then, you can greatly shorten that function. You don’t need to use string.lower. And like I mentioned above, returning nil in a function is not necessary.

  3. In your ‘saveData’ function there’s is no need to concatenate the player’s UserID. The data store will work perfectly fine if u remove it. It’s a bad trend I see in many datastores.

3 Likes

Thanks for the feedback!

1- Will localize those. Should I keep them global if they’re referencing themselves? Such as in the convertDataToInstance function?

3- Got it. I will set their parent on a second line and not use the second argument in Instance.new.

4- Only reason I am using that is because I am afraid there will be an empty entry because the entries are later removed (assuming you are talking about the requestData.OnServerInvoke function)

5- Will change to ipairs.

6- I will look into short circuiting. Only problem is that I am not the best with operators and how they work haha. I think I have the or operator understood but I wouldn’t want to break something and be unsure of how to fix it.

7- Didn’t realize type was a function. Will definitely switch over to that.

8- You’re probably right. I will remove the function entirely (also the reason I didn’t address #2)

9- Im a little confused as to what you mean by concatenate. Should I remove the two dots or just have the user ID as the key?

No problem

Response to 1. Nope, always localize them unless the functions are set to a table’s key like ‘function Dictionary.convert()’.

Response to 4. Not sure what you mean, but in your .OnServerInvoke listener, table.insert(playersRequestingData, #playersRequestingData + 1, player) does the exact same thing as playersRequestingData[#playersRequestingData + 1] = player.

Response to 6. Short circuit programming is not really essential but I still use it. Feel free to HMU on my discord for help with that.

DarthFS#1915

Response to 9. In your .OnServerInvoke listener, you can do ‘local key = player.UserId’. There isn’t no need to concatenate / attach a string.

One more thing, your ‘dataFunction’ is not really needed to be a function considering two factors. The function is a subroutine meaning no values are returned, which isn’t the main reason. You are also calling that function once and never use it again. What I’m saying is, you can write that chunk code without storing it in a function.

When you store colors, in your defaultData table, you don’t need an individual key for each parameter of Color3.fromRGB. Instead you can just have one key like [‘RGB’] = Color3.fromRGB(1, 2, 3). It saves more memory this way.

I appreciate your civilized response lol.

2 Likes

1- Ah okay, but how would I go about making a function call itself if it’s local?

4- Got it. I will switch over to that.

6- I will look into it, but I think staying with if statements would be better as I find it easier to read and wouldn’t want to mess anything up and have no idea how to fix it haha.

9- Got it. Will switch the key to that.

It’s actually calling itself, each time data is requested, that function iterates through the nested table and calls itself if that makes sense, particularly the dataFunction(v, currentFolder) line.

Also it fires the client once data is finished loading, I wanted to wait for the data to finish loading before the client has access to the game so the client is able to configure the settings.

Didn’t know Color3’s could be saved to DataStores, will switch over.

No worries! I think feedback is an important part of developing.

1 Like

‘Didn’t know Color3’s could be saved to DataStores, will switch over.’. Oh my bad, if it doesn’t then don’t listen to what I said about that

" Ah okay, but how would I go about making a function call itself if it’s local?" Not sure what you mean by this. Im assuming you are talking about recursion?

local Tries = 0
local function Recursion()
      if Tries < 5 then
            Tries = Tries + 1
            return Recursion()
      end
      print('Recursion has stopped: ' .. Tries)
end

Recursion()
1 Like

Yeah, was talking about recursion. Could have sworn it errored, guess not haha. Thanks!

1 Like