Datastore script including permission and data verification

Hello!

I’ve been working on this script for awhile and was just wondering how efficient it was. It includes packing from folders to tables and vice versa. It also verifies all data is there and it’s very easy to add more arguments, problem is that it looks like spaghetti code so I was wondering if it was as efficient as I thought or if it could be improved.

local DataStoreService = game:GetService('DataStoreService')
local DataStore = DataStoreService:GetDataStore('adsaddsdasdads')
local HttpService = game:GetService('HttpService')
local RS = game:GetService('ReplicatedStorage')
local Remotes = RS:WaitForChild('Remotes')

local defaultMusic = {}

local defaultData = {
	['General'] = {
		['MusicIDs'] = defaultMusic;
		['Coins'] = 0;
		['Experience'] = 0;
		['RequiredExperience'] = 100;
		['Stage'] = 0;
		['Gamepasses'] = {};
		['Inventory'] = {
			['Pets'] = {};
			['Accessories'] = {};
			['Crates'] = {};
		};
		['BanInfo'] = {
			['Banned'] = false;
			['ExpiryDate'] = 'nil';
			['BanReason'] = 'nil';
		};
	};
	['LoginInfo'] = {
		['TimeStamp'] = os.time();
		['Streak'] = 0;
	};
}

local function unpackTable(tbl, parent)
	for i,v in pairs(tbl) do
		if tostring(typeof(v)) == 'table' then
			local folder = Instance.new('Folder', parent)
			folder.Name = i
			unpackTable(v, folder)
		elseif tostring(typeof(v)) == 'boolean' then
			local bool = Instance.new('BoolValue', parent)
			bool.Name = i
			bool.Value = v
		elseif tostring(typeof(v)) == 'string' then
			local value = Instance.new('StringValue', parent)
			value.Name = i
			value.Value = v
			print(i,v)
		elseif tostring(typeof(v)) == 'number' then
			local value = Instance.new('IntValue', parent)
			value.Name = i
			value.Value = v
			print(i,v)
		end
	end
end

local function loadData(data, plr)
	local dataFolder = Instance.new('Folder', plr)
	dataFolder.Name = 'PlayerData'
	local generalInfo = Instance.new('Folder', dataFolder)
	generalInfo.Name = 'General'
	local loginInfo = Instance.new('Folder', dataFolder)
	loginInfo.Name = 'LoginInfo'
	for i,v in pairs(data['General']) do
		if tostring(typeof(v)) == 'table' then
			local folder = Instance.new('Folder', generalInfo)
			folder.Name = i
			unpackTable(v, folder)
		elseif tostring(typeof(v)) == 'boolean' then
			local bool = Instance.new('BoolValue', generalInfo)
			bool.Name = i
			bool.Value = v
		elseif tostring(typeof(v)) == 'string' then
			local value = Instance.new('StringValue', generalInfo)
			value.Name = i
			value.Value = v
			print(i,v)
		elseif tostring(typeof(v)) == 'number' then
			local value = Instance.new('NumberValue', generalInfo)
			value.Name = i
			value.Value = v
			print(i,v)
		end
	end
	for i,v in pairs(data['LoginInfo']) do
		if tostring(typeof(v)) == 'table' then
			local folder = Instance.new('Folder', loginInfo)
			folder.Name = i
			unpackTable(v, folder)
		elseif tostring(typeof(v)) == 'boolean' then
			local bool = Instance.new('BoolValue', loginInfo)
			bool.Name = i
			bool.Value = v
		elseif tostring(typeof(v)) == 'string' then
			local value = Instance.new('StringValue', loginInfo)
			value.Name = i
			value.Value = v
		elseif tostring(typeof(v)) == 'number' then
			local value = Instance.new('NumberValue', loginInfo)
			value.Name = i
			value.Value = v
		end
    end
    local loc = Instance.new('StringValue', plr)
    loc.Name = 'Location'
    loc.Value = 'MainIsland'
    Remotes.DataFinished:FireClient(plr)
end

local function verifyPermission(data, plr)
	if data['General']['BanInfo']['Banned'] == true then
		Remotes.SetBan:FireClient(plr, data['BanInfo'])
	else
		loadData(data, plr)
	end
end

local function verifyData(data, plr)
	local mod = false
	if not data['General'] then
		data = defaultData
	end
	if not data['General']['MusicIDs'] then
		data['General']['MusicIDs'] = defaultMusic
		mod = true
	end
	if not data['General']['Coins'] or tostring(typeof(data['Coins'])) ~= 'number' then
		data['General']['Coins'] = 0
		mod = true
	end
	if not data['General']['Experience'] or tostring(typeof(data['Experience'])) ~= 'number' then
		data['General']['Experience'] = 0
		mod = true
	end
	if not data['General']['RequiredExperience'] or tostring(typeof(data['RequiredExperience'])) ~= 'number' then
		data['General']['RequiredExperience'] = 100
		mod = true
	end
	if not data['General']['Stage'] or tostring(typeof(data['Stage'])) ~= 'number' then
		data['General']['Stage'] = 0
		mod = true
	end
	if not data['General']['Gamepasses'] or tostring(typeof(data['Gamepasses'])) ~= 'table' then
		data['General']['Gamepasses'] = {}
		mod = true
	end
	if not data['General']['Inventory'] or tostring(typeof(data['Inventory'])) ~= 'table' then
		data['General']['Inventory'] = {
			['Pets'] = {};
			['Accessories'] = {};
			['Crates'] = {};
		}
		mod = true
	elseif data['General']['Inventory'] then
		if not data['General']['Inventory']['Pets'] or tostring(typeof(data['Inventory']['Pets'])) ~= 'table' then
			data['General']['Inventory']['Pets'] = {}
			mod = true
		end
		if not data['General']['Inventory']['Accessories'] or tostring(typeof(data['Inventory']['Accessories'])) ~= 'table' then
			data['General']['Inventory']['Accessories'] = {}
			mod = true
		end
		if not data['General']['Inventory']['Crates'] or tostring(typeof(data['Inventory']['Crates'])) ~= 'table' then
			data['General']['Inventory']['Crates'] = {}
			mod = true
		end
	end
	if not data['General']['BanInfo'] or tostring(typeof(data['BanInfo'])) ~= 'table' then
		data['General']['BanInfo'] = {
			['Banned'] = false;
			['ExpiryDate'] = nil;
			['BanReason'] = nil;
		}
		mod = true
	elseif data['General']['BanInfo'] then
		if data['General']['BanInfo']['Banned'] == nil then
			data['General']['BanInfo']['Banned'] = false
			mod = true
		end
		if not data['General']['BanInfo']['ExpiryDate'] or tostring(typeof(data['BanInfo']['ExpiryDate'])) ~= 'string' then
			data['General']['BanInfo']['ExpiryDate'] = 'nil'
			mod = true
		end
		if not data['General']['BanInfo']['BanReason'] or tostring(typeof(data['BanInfo']['BanReason'])) ~= 'string' then
			data['General']['BanInfo']['BanReason'] = 'nil'
			mod = true
		end
    end
    if not data['General']['Settings'] or tostring(typeof(data['General']['Settings'])) ~= 'table' then
        data['General']['Settings'] = {}
        data['General']['Settings']['UIPrimaryColour'] = {['R'] = 86; ['G'] = 146; ['B'] = 127}
        data['General']['Settings']['UISecondaryColour'] = {['R'] = 255; ['G'] = 255; ['B'] = 255}
        data['General']['Settings']['UIFont'] = 'SourceSans'
        data['General']['Settings']['GlobalShadows'] = true
        data['General']['Settings']['PlayerMode'] = 'Visible'
        data['General']['Settings']['ResetKeybind'] = 'R'
        mod = true
    elseif data['General']['Settings'] then
        if not data['General']['Settings']['UIPrimaryColour'] then
            data['General']['Settings']['UIPrimaryColour'] = {['R'] = 86; ['G'] = 146; ['B'] = 127}
            mod = true
        end
        if not data['General']['Settings']['UISecondaryColour'] then
            data['General']['Settings']['UISecondaryColour'] = {['R'] = 255; ['G'] = 255; ['B'] = 255}
            mod = true
        end
        if not data['General']['Settings']['UIFont'] then
            data['Settings']['UIFont'] = 'SourceSans'
            mod = true
        end
        if data['General']['Settings']['GlobalShadows'] == nil then
            data['General']['Settings']['GlobalShadows'] = true
            mod = true
        end
        if not data['General']['Settings']['PlayerMode'] then
            data['General']['Settings']['PlayerMode'] = 'Visible'
            mod = true
        end
        if not data['General']['Settings']['ResetKeybind'] then
            data['General']['Settings']['ResetKeybind'] = 'R'
            mod = true
        end
    end
    
	if not data['LoginInfo'] then
		data['LoginInfo'] = {
			['TimeStamp'] = os.time();
			['Streak'] = 0;
		};
		mod = true
	elseif data['LoginInfo'] then
		if not data['TimeStamp'] then
			data['TimeStamp'] = os.time();
			mod = true
		end
		if not data['Streak'] then
            data['Streak'] = 0
            mod = true
		end
	end
	
	local key = 'DATA_'..plr.UserId
	if mod == true then
		local s,e = pcall(function()
			DataStore:SetAsync(key, HttpService:JSONEncode(data))
		end)
	end
	return data
end

game.Players.PlayerAdded:Connect(function(plr)
	local key = 'DATA_'..plr.UserId
	local dataSaves = Instance.new('BoolValue', plr)
	dataSaves.Name = 'DataSaves'
    local data
    local s,e = pcall(function()
    	data = DataStore:GetAsync(key)
    end)
	if s then
		if not data then
			data = defaultData
			data = HttpService:JSONEncode(data)
			local s,e = pcall(function()
				DataStore:SetAsync(key, data)
			end)
			if not s then
				return plr:Kick('Unable to set blank data: '..tostring(e)..'.')
			end
			data = HttpService:JSONDecode(data)
			dataSaves.Value = true
			local data = verifyData(data, plr)
		else
			data = HttpService:JSONDecode(data)
			print(data)
			dataSaves.Value = true
			local data = verifyData(data, plr)
		end
		verifyPermission(data, plr)
	else
		plr:Kick('Unable to load data: '..tostring(e)..'.')
	end
end)

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

local function saveData(plr)
	local key = 'DATA_'..plr.UserId
	local compiledData = {}
	if plr:FindFirstChild('DataSaves') then
		if plr:FindFirstChild('DataSaves').Value == true then
			compiledData['General'] = {}
			for i,v in pairs(plr:FindFirstChild('PlayerData'):FindFirstChild('General'):GetChildren()) do
				if v:IsA('ValueBase') then
					compiledData['General'][v.Name] = v.Value
                elseif v:IsA('Folder') then
                    print(i,v)
					compiledData['General'][v.Name] = unloadData(v)
				end
			end
			compiledData['LoginInfo'] = {}
			local loginInfo = plr:FindFirstChild('PlayerData'):FindFirstChild('LoginInfo')
			if os.time() - loginInfo:FindFirstChild('TimeStamp').Value > 86400 then
				compiledData['LoginInfo']['TimeStamp'] = os.time()
			else
				compiledData['LoginInfo']['TimeStamp'] = loginInfo:FindFirstChild('TimeStamp').Value
			end

			compiledData['LoginInfo']['Streak'] = loginInfo:FindFirstChild('Streak').Value
			print(compiledData)
		end
	end
	local data = HttpService:JSONEncode(compiledData)
	local s,e = pcall(function()
		DataStore:SetAsync(key, data)
	end)
	if not s then
		warn('Unable to set data for '..tostring(plr)..'. '..tostring(e))
	end
end

game.Players.PlayerRemoving:Connect(function(plr)
	saveData(plr)
end)

game:BindToClose(function()
	for i,plr in pairs(game:GetService('Players'):GetPlayers()) do
		saveData(plr)
	end
end)
3 Likes

Here’s what I picked up:

  1. Using tostring() on a typeof() return value. I’m almost certain that typeof() will always return a string, so you don’t need this security case.
  2. Function names. Things like “unpackTable” are fine, but I think they could be named something that more properly represents what it does. Yes, it “unpacks” a table, but remember that there’s a Lua global table.unpack which has completely different functionality. Maybe integrating the word “convert” would be a better choice.
  3. verifyData is the worst offender here because of it’s excessive use of conditionals. While it’s sufficient to use conditionals in this case, you’ll see that a lot of these if-blocks’ content have a trend when you juxtapose them; you set the parsed async (the data[path] reference) to its default found in defaultData if it doesn’t exist. This could be condensed into a recursive function that fetches a provided key from your defaultData dictionary and sees if that same key in your data dictionary is either missing or doesn’t accurately match the type of default value defaultData has. If that key is missing or doesn’t match the default value type, replace it and set mod to true. Use a for-pairs loop would be great for this use case, and shorten your ~70+ lines of if statements down to about 20-30 including the function content. (If I’m not explaining this well enough, I’d be happy to elaborate!)
  4. One-letter variable names are a bad habit for readability. While an experienced coder can still break down your code to figure out what it does with these obfuscations, you shouldn’t make it harder for yourself or anyone else to read your code. pcall seems to be the main offender here.
  5. You shouldn’t need to use JSONEncode for DataStores because (iirc) they do this internally already. Just simply send the table through here and remove any JSONDecode calls you have when loading the data.
  6. Services. Not a horrible thing, but I tend to always reference services using :GetService() even if they’re found in the DataModel (game). game.Players would be game:GetService("Players")

Just some food for thought: I think this is slight overengineering. While by all means this is rather intuitive and the means of converting your data into physical values and whatnot is intriguing, I’m a firm believer of strictly using dictionaries for data structures as tampering with physical objects can get messier and a bit more unpredictable.

Neither convention of saving data is horrible, however, and using physical data structures works just fine as using dictionaries. Don’t sweat it, but if you’re interested I can give you a breakdown of how I’d do it.

Good stuff! Looking forward to seeing your work in the future.

1 Like

Thank you for your feedback! I’ll take all of this into account, I also agree that the large amount of if statements is overkill, probably should do a for pairs loop as you suggested. Also would make it way easier to add more values in the future.

Wasn’t really sure what else to put for unpackTable, probably could do something like convertTable/convertInstance, but I think pack and unpack work as well. Would like some suggestions if you’re open to giving that. Your convert idea looks like it could fit.

I really only do one-letter variables because I’m lazy, bad habit, should really go over that again haha.

Will remove the encoding/decoding.

Good idea for the GetService, just having a variable like local Players = game:GetService('Players') works as well, also makes it easier to type. Could just make a library type thing getting all of the required services as the top.

I think I will stick with instances and whatnot because it makes it easier to modify in my opinion, but otherwise I will take your advice and revise my script. Thanks!

1 Like

I’ve seen a lot of credible programmers on here state that they prefer variable names that are extremely descriptive, and I do too. I’d name your function convertTableToFolder, but again: all up to you.

Best of luck!

1 Like

I’m a having a bit of trouble, how do you think I could go about creating a recursive loop that would verify a nested folder? Do you think I should create the new instances at the same time as the verification?

I would probably just verify the dictionary data before you send it to the function that creates the folder rather than verifying the folder itself; it’ll make things much, much easier on you.

Just to clarify: are you asking for example code on how to do this or just asking for some insight into the thought process?

1 Like

Either one works, not really sure how I would go about creating a recursive loop for nested tables/dictionaries.

Here’s a robust implementation that I wrote quickly:

local defaultData = {
    money = 500,
    level = 1,

    inventory = {
        primary = "sword",
        secondary = "torch"
    },
}

local function parseData(data, comparisonTable)
    if typeof(comparisonTable) ~= "table" then
        -- remove the need for the second parameter for most generic calls:
        comparisonTable = defaultData
    end
    for key, value in pairs(comparisonTable) do
        if data[key] == nil then
            -- if this data is missing, fill in the key with the defaultData async:
            data[key] = value
            print("Filled in key", key)
        else
            local DATA_TYPEOF = typeof(data[key])
            if DATA_TYPEOF == "table" then
                -- if this key is a table, verify it:
                print("Beginning parse on table value", key)
                parseData(data[key], comparisonTable[key])
            elseif DATA_TYPEOF ~= typeof(value) then
                -- if this key is not a table and has the wrong data type in accordance with the defaultData, impose
                -- the defaultData's value onto it:
                data[key] = value
                print("Malformed data in key", key, "replacing with defaultData value of", value)
            end
        end
    end
    for key, _ in pairs(data) do
        if comparisonTable[key] == nil then
            -- if the table being verified (data) has an extra key that reference table (comparisonTable), remove that
            -- key:
            data[key] = nil
            print("Shaved off excess key", key)
        end
    end
end

local inventory = {
    money = "broke",
    level = 5,

    inventory = {
        primary = "bow",
        secondary = 2,
        tertiary = "combat knife",
    }
}
parseData(inventory)
print(inventory)

With the Rich Output (which I believe is live for all users now?), you can see the contents of the verified table through the final print statement.

Happy coding.

1 Like