A Simple Outfit Load/Save System..how to do it?

Hello! I’m currently working on a simple rp game and it is going absolutely wonderful in regards of me working on customization. My stuff is usually just very simple and I don’t intend to go all out in something like an Avatar Editor–since my experience in scripting is not the best.

I’m getting started with Datastores. And I don’t seem to understand them one bit. I’m currently working on a simple save/load outfit system that looks a little something like this.

I’ve searched every resource I could find about this sort of method–but it’s a little difficult when the majority of these resources are hard to try and dissect for my own purpose. Heck: there’s not even free models that could help me to try and at least learn something like this.

If anyone has any tips, resources I might’ve missed, or at least the time of day to help me script this, it would be very appreciated!

1 Like

If you have absolutely NO idea of how to set up a DataStore within your script, you should take a look at this community post about how you could set up your DataStore.

Else, how you’d go around with this would be using tables. You’d have to create different remotes for specific buttons. I’d make two RemoteEvents for the Save button and for the Load button. I’ll also have the LocalScript (ideally where you’re handling the ui button clicks) fire/invoke the server for the according button and the normal Script listen for those fire. And the way you’d go about sending the data, because you can’t actually store Instances within a DataStore, is by sending a sort of OOP (object-orientated programming) types of tables.

If you don't know what OOP tables are...

It’s basically objects that have keys with values in them, basically like the properties of an Instance, which can also be considered as a class. I know there’s a lot more than just that, but that’s how I interpreted it. (Would be appreciative if someone corrected me on this, though)

An example of an OOP type table would be something like this:

local PersonClass = {
	FirstName = "John",
	LastName = "Doe",
	Age = 32
}

All of this is mainly created for you to learn from. So, you’d have to adjust the script to how your current script is if you plan on using it.

So, how I’d go around doing this is within the LocalScript handling the SaveButton (or the script can handle the same button), within the Mouse1Click function, I’d fire the corresponding remote to the server. If you already have stored some sort of information about what the player is currently wearing, use that instead.

-- I also noticed you have a place where they can name their outfit
-- ... so i'll apply that as well.

-- This all assumes that you've stored different types of clothing in a folder in ReplicatedStorage.
-- And also assumes that SaveOutfitRemote has already been declared within the script.
-- So all the data is the name of that specific item.

SaveButton.Mouse1Click:Connect(function()
	SaveOutfitRemote:FireServer(outfitName)
-- You SHOULD make the OOP data on the server script,
-- since having the data here can lead to possible exploitation.
end)

And for the Load Button (where ever the script is handling that), I’d fire to the server to load the saved outfit within the Mouse1Click function: The thing with this is, this can also be exploitable if we don’t code this correctly. So, I’ll go to the extent of coding some sanity checks on the server script.

LoadButton.Mouse1Click:Connect(function()
	-- Since there are different types of outfits,
	-- the player may want a specific outfit that they selected.
	local Outfit = LoadOutfitRemote:FireServer("OutfitName")
end)


Scroll down if you just want the script without explanation, I wouldn’t blame you.

Now, I’d make a separate server-sided script to handle the Save/Load remotes. What I do first is create the DataStore that’ll store the outfit data and set up the script to handle the different types of remote events. Since this is loading and saving the outfits, we’d need to also have the outfit items stored in the script as well. The structure could be something like this:

local DataStoreService = game:GetService("DataStoreService")
local ReplicatedStorage = game:GetService("ReplicatedStorage") -- I believe your remotes would be somewhere in there.
local ServerStorage = game:GetService("ServerStorage") -- This is where your outfit items could potentially be stored at.
-- I'd store it here for safety purposes, but it may vary depending on how your code is structured.

local OutfitRemotes = ReplicatedStorage:WaitForChild("OutfitRemotes")
local OutfitAssets = ServerStorage:WaitForChild("OutfitAssets") -- This is where most of your outfit essentils will be located
-- Faces, Hairs, Clothing, etc.
local UserOutfits = DataStoreService:GetDataStore("UserOutfits") -- it just creates a new DataStore

OutfitRemotes.SaveOutfitRemote.OnServerEvent:Connect(function(player)

end)

OutfitRemotes.LoadOutfitRemote.OnServerEvent:Connect(function(player, outfitName)

end)

Now here’s how things get interesting. Within the save event listener (OnServerEvent) is where we create the OOP-like table. Afterward, save it to the player’s user outfits. Since there would be different types of outfits the player would have, we’d have to account for potential overwritting. And since we can’t just add things to DataStores (from my understanding, can be wrong), we’d have to get the current player’s outfits and insert the new outfit to THAT table. Something like this would do:

OutfitRemotes.SaveOutfitRemote.OnServerEvent:Connect(function(player, outfitName)
	local character = player.Character
	
	local shirt = character:FindFirstChild("Shirt")
	local pants = character:FindFirstChild("Pants")
	local head = character:FindFirstChild("Head")
	local face -- We'll get the face with a different method
	-- to account for decals that are for face decoration.
	local hairs = {} -- Expected to be different accessories. 
	-- We'll go the same sort of method as we did for finding the face

	for _, inst in head:GetChildren() do -- We're beginning to find the face
		if inst:IsA("Decal") then -- So we don't be checking for any Attachments/Motor6Ds
			local foundFace = false -- If true, we should break out of the loop since there's no reason to be continuing to find the face.

			for _, _face in OutfitAssets.Faces:GetChildren() do
				if _face.Name == inst.Name then -- Validating that the face is within the faces folder
					face = inst.Name
					foundFace = true
					break
				end
			end

			if foundFace then
				break
			end
		end
	end

	for _, inst in character:GetChildren() do -- Beginning to find different hairs
		if inst:IsA("Accessory") then -- Finding only accessories
			for _, _hair in OutfitAssets.Hair:GetChildren() do
				if _hair.Name == inst.Name then -- Validating that the accessory is within the hairs folder
					table.insert(hairs, inst.Name)
					break
				end
			end
		end
	end

	local Outfit = {
		Name = outfitName, -- or "Name"
		Shirt = shirt.Name, -- Or, you could use the ID to load it, 
		-- whatever method you choose should be coded according to that method
		Pants = pants.Name, -- Same idea as above in terms of storing the data.
		Hair = hairs, -- In case there'll be multiple hair being used, you'd store the name of the hair(s).
	-- This is just assuming if you've put those hairs within ReplicatedStorage.
		Face = face -- It can be the name or assetId of the face
	}

	local existingOutfits -- Expected to be a table for the outfits
	local success, results = pcall(function()
		existingOutfits = UserOutfits:GetAsync("Outfits." .. player.UserId)
	end)

	if not success and not results then
		warn(player.Name, "doesn't have any outfits! Creating new data...") -- This is actually unnecessary imo..
		existingOutfits = {}
	else
		error(results)
	end

	existingOutfits[Outfit.Name] = Outfit

	UserOutfits:SetAsync("Outfits." .. player.UserId)
end)

Now this should be able to save the player’s current outfit with the corresponding outfitName. Now, how about loading that data? This is where we start coding the LoadOutfitRemote. What we’d have to do is get the outfits that the player has and find the one with the corresponding outfitName. (As it was passed when firing to the server) And since it’s stored in a table with all of these Outfits, we would have to loop through that big table to find the one with the EXACT name. There is a way you could have it where it finds the right outfit even where there are outfits with duplicate names, but this is a simple Load/Save system.

I also decided to create a function to make it faster to get different outfit assets. The remote event may look like this:

OutfitRemotes.LoadOutfitRemote.OnServerEvent:Connect(function(player, outfitName)
    local function getAsset(assetType, name)
        local library = OutfitAssets:FindFirstChild(assetType) -- assetType is just the library that it should check to find "name".

        if library then -- If the library is an existing folder in OutfitAssets
            for _, inst in library:GetChildren() do
                if inst.Name == name then
                    return inst
                end
            end
        else -- Warn the coder that they may have made a typo
            warn(assetType, "doesn't exist within this folder/library!")
        end
    end

    local character = player.Character

    local Head = character:FindFirstChild("Head")
    local playerShirt = character:FindFirstChild("Shirt") :: Shirt -- Would create a new Shirt if there isn't one already
    local playerPants = character:FindFirstChild("Pants") :: Pants -- Same thinking from above applies here 
    
    for _, accessory in character:GetChildren() do
        if accessory:IsA("Accessory") then
            accessory:Destroy()
        end
    end

    for _, item in Head:GetChildren() do
        if getAsset("Face", item.Name) then
            item:Destroy()
        end
    end

	local playerOutfits
	local success, results = pcall(function()
		playerOutfits = UserOutfits:GetAsync("Outfits." .. player.UserId)
	end)

	if success then
		for _outfitName, _outfitData in pairs(playerOutfits) do
			-- You could actually do this two ways:
			-- You can check if outfitName matches _outfitName or _outfitData.Name
			-- That's ONLY if you've stored into the player's outfit table as a key (e.g. fruits["banana"], fruits["apple"], etc.)
			-- Instead of a number (e.g. table[1], table[2], etc.)
			if outfitName == _outfitName then
				local shirt = getAsset("Shirts", _outfitData.Shirt)
				local pants = getAsset("Pants", _outfitData.Pants)
				local face = getAsset("Faces", _outfitData.Face)
				local hair = {} -- Remember, _outfitData.Hair is a table.
				
                for _, _hair in _outfitData.Hair do
                    table.insert(hair, getAsset("Hair", _hair))
                end
				-- Now that we have this data, we can just overwrite the current players outfit
                playerShirt.ShirtTemplate = shirt.ShirtTemplate -- Apparently they decided to call these Templates instead of Ids
                playerPants.PantsTemplate = pants.PantsTemplate
                
                face:Clone().Parent = Head

                for _, _hair in hair do
                    local _newHair = _hair:Clone()
                    _newHair.Parent = character
                end

				break -- Because we don't need to find the right outfit anymore
			end
		end
	elseif results then
		error(results)
	end
end)

The final structure of the script should look something like this:

local DataStoreService = game:GetService("DataStoreService")
local ReplicatedStorage = game:GetService("ReplicatedStorage") -- I believe your remotes would be somewhere in there.
local ServerStorage = game:GetService("ServerStorage") -- This is where your outfit items could potentially be stored at.
-- I'd store it here for safety purposes, but it may vary depending on how your code is structured.

local OutfitRemotes = ReplicatedStorage:WaitForChild("OutfitRemotes")
local OutfitAssets = ServerStorage:WaitForChild("OutfitAssets") -- This is where most of your outfit essentils will be located
-- Faces, Hairs, Clothing, etc.
local UserOutfits = DataStoreService:GetDataStore("UserOutfits") -- it just creates a new DataStore

OutfitRemotes.SaveOutfitRemote.OnServerEvent:Connect(function(player, outfitName)
	local character = player.Character
	
	local shirt = character:FindFirstChild("Shirt")
	local pants = character:FindFirstChild("Pants")
	local head = character:FindFirstChild("Head")
	local face -- We'll get the face with a different method
	-- to account for decals that are for face decoration.
	local hairs = {} -- Expected to be different accessories. 
	-- We'll go the same sort of method as we did for finding the face

	for _, inst in head:GetChildren() do -- We're beginning to find the face
		if inst:IsA("Decal") then -- So we don't be checking for any Attachments/Motor6Ds
			local foundFace = false -- If true, we should break out of the loop since there's no reason to be continuing to find the face.

			for _, _face in OutfitAssets.Faces:GetChildren() do
				if _face.Name == inst.Name then -- Validating that the face is within the faces folder
					face = inst.Name
					foundFace = true
					break
				end
			end

			if foundFace then
				break
			end
		end
	end

	for _, inst in character:GetChildren() do -- Beginning to find different hairs
		if inst:IsA("Accessory") then -- Finding only accessories
			for _, _hair in OutfitAssets.Hair:GetChildren() do
				if _hair.Name == inst.Name then -- Validating that the accessory is within the hairs folder
					table.insert(hairs, inst.Name)
					break
				end
			end
		end
	end

	local Outfit = {
		Name = outfitName, -- or "Name"
		Shirt = shirt.Name, -- Or, you could use the ID to load it, 
		-- whatever method you choose should be coded according to that method
		Pants = pants.Name, -- Same idea as above in terms of storing the data.
		Hair = hairs, -- In case there'll be multiple hair being used, you'd store the name of the hair(s).
	-- This is just assuming if you've put those hairs within ReplicatedStorage.
		Face = face -- It can be the name or assetId of the face
	}

	local existingOutfits -- Expected to be a table for the outfits
	local success, results = pcall(function()
		existingOutfits = UserOutfits:GetAsync("Outfits." .. player.UserId)
	end)

	if not success and not results then
		warn(player.Name, "doesn't have any outfits! Creating new data...") -- This is actually unnecessary imo..
		existingOutfits = {}
	else
		error(results)
	end

	existingOutfits[Outfit.Name] = Outfit

	UserOutfits:SetAsync("Outfits." .. player.UserId)
end)

OutfitRemotes.LoadOutfitRemote.OnServerEvent:Connect(function(player, outfitName)
    local function getAsset(assetType, name)
        local library = OutfitAssets:FindFirstChild(assetType) -- assetType is just the library that it should check to find "name".

        if library then -- If the library is an existing folder in OutfitAssets
            for _, inst in library:GetChildren() do
                if inst.Name == name then
                    return inst
                end
            end
        else -- Warn the coder that they may have made a typo
            warn(assetType, "doesn't exist within this folder/library!")
        end
    end

    local character = player.Character

    local Head = character:FindFirstChild("Head")
    local playerShirt = character:FindFirstChild("Shirt") :: Shirt -- Would create a new Shirt if there isn't one already
    local playerPants = character:FindFirstChild("Pants") :: Pants -- Same thinking from above applies here 
    
    for _, accessory in character:GetChildren() do
        if accessory:IsA("Accessory") then
            accessory:Destroy()
        end
    end

    for _, item in Head:GetChildren() do
        if getAsset("Face", item.Name) then
            item:Destroy()
        end
    end

	local playerOutfits
	local success, results = pcall(function()
		playerOutfits = UserOutfits:GetAsync("Outfits." .. player.UserId)
	end)

	if success then
		for _outfitName, _outfitData in pairs(playerOutfits) do
			-- You could actually do this two ways:
			-- You can check if outfitName matches _outfitName or _outfitData.Name
			-- That's ONLY if you've stored into the player's outfit table as a key (e.g. fruits["banana"], fruits["apple"], etc.)
			-- Instead of a number (e.g. table[1], table[2], etc.)
			if outfitName == _outfitName then
				local shirt = getAsset("Shirts", _outfitData.Shirt)
				local pants = getAsset("Pants", _outfitData.Pants)
				local face = getAsset("Faces", _outfitData.Face)
				local hair = {} -- Remember, _outfitData.Hair is a table.
				
                for _, _hair in _outfitData.Hair do
                    table.insert(hair, getAsset("Hair", _hair))
                end
				-- Now that we have this data, we can just overwrite the current players outfit
                playerShirt.ShirtTemplate = shirt.ShirtTemplate -- Apparently they decided to call these Templates instead of Ids
                playerPants.PantsTemplate = pants.PantsTemplate
                
                face:Clone().Parent = Head

                for _, _hair in hair do
                    local _newHair = _hair:Clone()
                    _newHair.Parent = character
                end

				break -- Because we don't need to find the right outfit anymore
			end
		end
	elseif results then
		error(results)
	end
end)

Hopefully this, LONG post helps you out in some matter. If you still would like some assistance I would be happy to help you. Other than that, happy coding!

2 Likes

This is absolutely helpful!!! Thank you so much!

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.