Outfit Rig Preview

You can write your topic however you want, but you need to answer these questions:

  1. What do you want to achieve? Keep it simple and clear!
    I want the WorldModel to display my saved outfit’s HumanoidDescription.
    Also, if possible, I’d like to improve my script so it can save body parts and skin color as well.

  2. What is the issue? Include screenshots / videos if possible!
    image
    The rig should display my outfit’s appearance.

  3. What solutions have you tried so far? Did you look for solutions on the Developer Hub?
    I’ve tried to make a RemoteEvent that sends HumanoidDescription but I don’t know how to make the thing I want.

After that, you should include more details if you have any. Try to make your topic as descriptive as possible, so that it’s easier for people to help you!

LocalScript

local localPlayer = game:GetService("Players").LocalPlayer
local character = localPlayer.Character
local replicatedStorage = game:GetService("ReplicatedStorage")

local addOutfitButton = script.Parent
local outfitSavingFrame = script.Parent.Parent.Parent.Parent.Parent.Confirmation

local scrollingFrameFits = script.Parent.Parent.Parent.Parent.Parent.Container.ScrollingFrame

local outfitTemplate = script.Parent.Parent.Parent.Fit_FitName
local outfitName = outfitTemplate.FitName
local outfitRig = outfitTemplate.ViewportFrame.WorldModel.Outfit
local rigAppearance = game.ReplicatedStorage.HumanoidDescLoader

local outfitSavingFrameNoButton = script.Parent.Parent.Parent.Parent.Parent.Confirmation.No
local outfitSavingFrameYesButton = script.Parent.Parent.Parent.Parent.Parent.Confirmation.Yes

repeat game:GetService("RunService").Heartbeat:Wait() until localPlayer.CharacterAppearanceLoaded -- Local script will yield until the character has FULLY LOADED
local deleteDebounce = false

function clientDataMessenger(saveOrLoad, key)
	print("Client data messenger invoked for", saveOrLoad, "with key:", key)
	if saveOrLoad == "Save" then
		replicatedStorage.Remotes.SaveOutfit:InvokeServer(outfitSavingFrame.TextBox.Text)
	elseif saveOrLoad == "Load" then
		replicatedStorage.Remotes.LoadOutfit:InvokeServer(key)
	end
end

replicatedStorage.Remotes.AddAccessory.OnClientInvoke = function(returnedValue)
	print("AddAccessory invoked on client with returned value:", returnedValue)
	if typeof(returnedValue) == "string" then
		print("Returned value is a string:", returnedValue)
	end
end

addOutfitButton.MouseButton1Click:Connect(function()
	outfitSavingFrame.Visible = true
	print("Clicked Add Outfit button")
end)

outfitSavingFrameNoButton.MouseButton1Click:Connect(function()
	outfitSavingFrame.Visible = false
	print("Clicked No on outfit saving frame")
end)

outfitSavingFrameYesButton.MouseButton1Click:Connect(function()
	outfitSavingFrame.Visible = false
	clientDataMessenger("Save")
	outfitSavingFrame.TextBox.Text = ""
	print("Saved outfit")
end)

replicatedStorage.Remotes.ListSaves.OnClientEvent:Connect(function(passedData)
	print("ListSaves received data:", passedData)
	local clonedSaveTemplate = outfitTemplate:Clone()
	clonedSaveTemplate.Name = passedData
	clonedSaveTemplate.FitName.Text = passedData
	clonedSaveTemplate.Parent = scrollingFrameFits

	clonedSaveTemplate.Click.MouseButton1Click:Connect(function()
		clientDataMessenger("Load", passedData)
		print("Clicked to load outfit:", passedData)
	end)

	clonedSaveTemplate.DeleteIcon.MouseButton1Click:Connect(function()
		replicatedStorage.Remotes.RemoveOutfit:InvokeServer(passedData)
		clonedSaveTemplate:Destroy()
		print("Clicked to delete outfit:", passedData)
	end)
end)

replicatedStorage.Remotes.RemoveOutfit.OnClientInvoke = function(success, key)
	print("RemoveOutfit invoked on client with success:", success, "and key:", key)
	if success then
		local outfitToRemove = scrollingFrameFits:FindFirstChild(key)
		if outfitToRemove then
			outfitToRemove:Destroy()
		end
	else
		task.delay(3, function()
			print("Failed to remove outfit after delay")
		end)
	end
end

ServerScript:

local replicatedStorage = game:GetService("ReplicatedStorage")
local playerTable = {} -- this is some really basic serverside remote security. could be better
-- you can also do this with just a static boolean for each player instead of using tick() but waga baga bobo
local cooldownTime = 0.1

local dataStoreService = game:GetService("DataStoreService")
local outfitStore = dataStoreService:GetDataStore("OutfitStore")

local appearanceChangedRemote = game.ReplicatedStorage.AppearanceChanged
local appearanceResetRemote = game.ReplicatedStorage.AppearanceReset
local rigAppearance = game.ReplicatedStorage.HumanoidDescLoader

function getTextObject(message, fromPlayerId) -- another thing from the offical roblox docs
	local textObject
	local success, errorMessage = pcall(function()
		textObject = game:GetService("TextService"):FilterStringAsync(message, fromPlayerId)
	end)
	if success then
		return textObject
	elseif errorMessage then
		print("Error generating TextFilterResult:", errorMessage)
	end
	return false
end

function filterOutfitName(outfitName, player)
	local textObject = getTextObject(outfitName, player.UserId)
	if textObject then
		local filteredName = getFilteredMessage(textObject)
		return filteredName
	end
	return false
end


function getFilteredMessage(textObject)
	local filteredMessage
	local success, errorMessage = pcall(function()
		filteredMessage = textObject:GetNonChatStringForBroadcastAsync()
	end)
	if success then
		return filteredMessage
	elseif errorMessage then
		print("Error filtering message:", errorMessage)
	end
	return false
end

function insertAccessory(player, id)
	local accessoryCount = 0
	for i, v in pairs(player.Character:GetChildren()) do
		if v:IsA("Accessory") then
			accessoryCount += 1
		end
	end

	if id then
		local success, value = pcall(game:GetService("InsertService").LoadAsset, game:GetService("InsertService"), id)
		if success and value then
			if accessoryCount < 20 then
				if value:GetChildren()[1]:IsA("Accessory") then
					local accessory = value:GetChildren()[1]
					accessory:SetAttribute("AssetId", id)
					player.Character:FindFirstChildWhichIsA("Humanoid"):AddAccessory(accessory)
					value:Destroy()
					return accessory
				else
					value:Destroy()
					warn("First child of LoadAsset is not an accessory")
				end
			else
				warn("Maximum accessories reached")
			end
		else
			warn("Failed to load asset")
		end
	else
		warn("Attempted to load asset with nil ID")
	end
end



function serializeCharacter(character)
	local characterInfo = {}
	characterInfo["Accessories"] = {}

	for i, v in pairs(character:GetChildren()) do
		if v:IsA("Accessory") then
			local specialMesh = v.Handle:FindFirstChildWhichIsA("SpecialMesh")
			if specialMesh then
				local accessoryInfoTable = {}
				accessoryInfoTable["AssetId"] = v:GetAttribute("AssetId")
				accessoryInfoTable["TextureId"] = specialMesh.TextureId
				accessoryInfoTable["Offset"] = {
					X = specialMesh.Offset.X,
					Y = specialMesh.Offset.Y,
					Z = specialMesh.Offset.Z
				}
				accessoryInfoTable["Scale"] = {
					X = specialMesh.Scale.X,
					Y = specialMesh.Scale.Y,
					Z = specialMesh.Scale.Z
				}
				accessoryInfoTable["VertexColor"] = {
					X = specialMesh.VertexColor.X,
					Y = specialMesh.VertexColor.Y,
					Z = specialMesh.VertexColor.Z
				}
				table.insert(characterInfo["Accessories"], accessoryInfoTable)
			else
				warn("Accessory " .. v.Name .. " does not have a SpecialMesh")
			end
		elseif v:IsA("Shirt") then
			characterInfo["Shirt"] = v.ShirtTemplate
		elseif v:IsA("Pants") then
			characterInfo["Pants"] = v.PantsTemplate
		elseif v:IsA("ShirtGraphic") then
			characterInfo["TShirt"] = v.Graphic
		elseif v:IsA("Part") and v.Name == "Head" then
			local faceDecal = v:FindFirstChildWhichIsA("Decal")
			if faceDecal then
				characterInfo["Face"] = faceDecal.Texture
			else
				warn("Head part does not have a Decal")
			end
		end
	end

	print(characterInfo)
	return characterInfo
end

function getPlayerData(player)
	local success, value = pcall(outfitStore.GetAsync, outfitStore, player.UserId)
	if success then
		if not value then
			value = {}
		end
	else
		print("Error getting player data:", value)  -- Print the error message
	end
	return value
end

game.Players.PlayerAdded:Connect(function(player)
	print("Player added:", player.Name)

	if not playerTable[player.UserId] then
		playerTable[player.UserId] = {}
		playerTable[player.UserId]["cooldown"] = tick()
		playerTable[player.UserId]["modifying_data"] = false
		playerTable[player.UserId]["local_data"] = {}
		playerTable[player.UserId]["switch"] = false
	end

	player.CharacterAppearanceLoaded:Connect(function(character)
		print("CharacterAppearanceLoaded event triggered for", player.Name)

		character.Humanoid:RemoveAccessories()

		local accessoriesTable = game:GetService("Players"):GetHumanoidDescriptionFromUserId(player.UserId):GetAccessories(true)
		print("Accessories table:", accessoriesTable)
		for i, v in accessoriesTable do
			insertAccessory(player, v["AssetId"])
		end

		-- Check and add default clothing if not present
		if not player.Character:FindFirstChildWhichIsA("Shirt") then
			local shirt = Instance.new("Shirt")
			shirt.ShirtTemplate = "rbxassetid://1"
			shirt.Parent = character
		end
		if not player.Character:FindFirstChildWhichIsA("Pants") then
			local pants = Instance.new("Pants")
			pants.PantsTemplate = "rbxassetid://1"
			pants.Parent = character
		end
		if not player.Character:FindFirstChildWhichIsA("ShirtGraphic") then
			local tShirt = Instance.new("ShirtGraphic")
			tShirt.Graphic = "rbxassetid://1"
			tShirt.Parent = character
		end

		if not playerTable[player.UserId]["switch"] then
			playerTable[player.UserId]["local_data"] = getPlayerData(player)
			playerTable[player.UserId]["switch"] = true
		end
		local data = playerTable[player.UserId]["local_data"]
		for i, v in pairs(data) do
			replicatedStorage.Remotes.ListSaves:FireClient(player, i)
		end
		--replicatedStorage.Remotes.SaveOutfit:InvokeClient(player, "Success")
	end)
end)

game.Players.PlayerRemoving:Connect(function(player)
	if playerTable[player.UserId] then
		local success, value = pcall(function()
			outfitStore:SetAsync(player.UserId, playerTable[player.UserId]["local_data"])
		end)
		if success then
			print("Player data saved for", player.Name)
		else
			print("Error saving player data:", value)  -- Print the error message
		end

		playerTable[player.UserId] = nil
	end
	print("Player removed:", player.Name)
end)

replicatedStorage.Remotes.SaveOutfit.OnServerInvoke = function(player, outfitName)
	print("SaveOutfit invoked by", player.Name, "for outfit:", outfitName)

	-- Ensure no concurrent modifications
	if playerTable[player.UserId]["modifying_data"] then
		print("Modifying data conflict for", player.Name)
		return
	end

	-- Set modifying_data flag to true to prevent concurrent modifications
	playerTable[player.UserId]["modifying_data"] = true

	local filteredOutfitName = filterOutfitName(outfitName, player)
	if not filteredOutfitName then
		--replicatedStorage.Remotes.SaveOutfit:InvokeClient(player, "Failed to filter outfit name")
		playerTable[player.UserId]["modifying_data"] = false
		print("Failed to filter outfit name for", player.Name)
		return
	end

	local playerData = playerTable[player.UserId]["local_data"]

	for i, v in pairs(playerData) do
		if i == filteredOutfitName then
			--replicatedStorage.Remotes.SaveOutfit:InvokeClient(player, "Outfit name already taken")
			playerTable[player.UserId]["modifying_data"] = false
			print("Outfit name already taken for", player.Name)
			return
		elseif #playerData >= 50 then
			--replicatedStorage.Remotes.SaveOutfit:InvokeClient(player, "Maximum of 50 saves")
			playerTable[player.UserId]["modifying_data"] = false
			print("Maximum outfit saves reached for", player.Name)
			return
		end
	end

	local success, value = pcall(function()
		playerData[filteredOutfitName] = serializeCharacter(player.Character)
	end)
	if success then
		replicatedStorage.Remotes.ListSaves:FireClient(player, filteredOutfitName)
		--replicatedStorage.Remotes.SaveOutfit:InvokeClient(player, "Success")
		print("Outfit saved successfully for", player.Name)

		-- Reset modifying_data flag after successful save
		playerTable[player.UserId]["modifying_data"] = false
	else
		print("Error while saving outfit:", value)  -- Print the error message
		--replicatedStorage.Remotes.SaveOutfit:InvokeClient(player, "Failed to save outfit")
		print("Failed to save outfit for", player.Name)

		-- Reset modifying_data flag on error
		playerTable[player.UserId]["modifying_data"] = false
	end
end


replicatedStorage.Remotes.LoadOutfit.OnServerInvoke = function(player, outfitName)
	print("LoadOutfit invoked by", player.Name, "for outfit:", outfitName)

	if tick() - playerTable[player.UserId]["cooldown"] > cooldownTime and not playerTable[player.UserId]["modifying_data"] then
		playerTable[player.UserId]["cooldown"] = tick()
		playerTable[player.UserId]["modifying_data"] = true

		local retreivedData = playerTable[player.UserId]["local_data"][outfitName]

		print("Retrieved data:", retreivedData)

		player.Character:FindFirstChildWhichIsA("Humanoid"):RemoveAccessories()

		-- Check and set accessories
		if retreivedData["Accessories"] then
			for i, v in ipairs(retreivedData["Accessories"]) do
				local accessory = insertAccessory(player, v["AssetId"])
				if accessory then
					local mesh = accessory.Handle:FindFirstChildWhichIsA("SpecialMesh")
					if mesh then
						mesh.Offset = Vector3.new(v["Offset"]["X"], v["Offset"]["Y"], v["Offset"]["Z"])
						mesh.Scale = Vector3.new(v["Scale"]["X"], v["Scale"]["Y"], v["Scale"]["Z"])
						mesh.VertexColor = Vector3.new(v["VertexColor"]["X"], v["VertexColor"]["Y"], v["VertexColor"]["Z"])
						mesh.TextureId = v["TextureId"]
					end
				end
			end
		end

		-- Check and set clothing
		if player.Character:FindFirstChild("Shirt") then
			player.Character:FindFirstChild("Shirt").ShirtTemplate = retreivedData["Shirt"]
		end
		if player.Character:FindFirstChild("Pants") then
			player.Character:FindFirstChild("Pants").PantsTemplate = retreivedData["Pants"]
		end
		if player.Character:FindFirstChild("ShirtGraphic") then
			player.Character:FindFirstChild("ShirtGraphic").Graphic = retreivedData["TShirt"]
		end
		if player.Character.Head:FindFirstChild("Decal") then
			player.Character.Head:FindFirstChild("Decal").Texture = retreivedData["Face"]
		end

		--replicatedStorage.Remotes.LoadOutfit:InvokeClient(player, "Success")

		playerTable[player.UserId]["modifying_data"] = false
	else
		print("Cooldown or modifying data conflict for", player.Name)
	end
end

replicatedStorage.Remotes.RemoveOutfit.OnServerInvoke = function(player, outfitName)
	print("RemoveOutfit invoked by", player.Name, "for outfit:", outfitName)

	if tick() - playerTable[player.UserId]["cooldown"] > cooldownTime and not playerTable[player.UserId]["modifying_data"] then
		playerTable[player.UserId]["cooldown"] = tick()
		playerTable[player.UserId]["modifying_data"] = true

		local playerData = playerTable[player.UserId]["local_data"]
		playerData[outfitName] = nil

		--replicatedStorage.Remotes.RemoveOutfit:InvokeClient(player, true, outfitName)

		playerTable[player.UserId]["modifying_data"] = false
	else
		print("Cooldown or modifying data conflict for", player.Name)
	end
end

local function refreshCharacter(player)
	print("Player refreshed avatar:", player.Name)

	if not playerTable[player.UserId] then
		playerTable[player.UserId] = {}
		playerTable[player.UserId]["cooldown"] = tick()
		playerTable[player.UserId]["modifying_data"] = false
		playerTable[player.UserId]["local_data"] = {}
		playerTable[player.UserId]["switch"] = false
	end
	
	local character = player.Character
	local humanoid = character:WaitForChild("Humanoid")
	
		print("CharacterAppearanceLoaded event triggered for", player.Name)
		
	character.Humanoid:RemoveAccessories()

		local humanoidDescription = humanoid:GetAppliedDescription()
		if humanoidDescription then
			local accessoriesTable = humanoidDescription:GetAccessories(true)
			print("Accessories table:", accessoriesTable)
			for i, accessory in ipairs(accessoriesTable) do
				insertAccessory(player, accessory.AssetId)
			end
		end

		-- Check and add default clothing if not present
		if not player.Character:FindFirstChildWhichIsA("Shirt") then
			local shirt = Instance.new("Shirt")
			shirt.ShirtTemplate = "rbxassetid://1"
			shirt.Parent = character
		end
		if not player.Character:FindFirstChildWhichIsA("Pants") then
			local pants = Instance.new("Pants")
			pants.PantsTemplate = "rbxassetid://1"
			pants.Parent = character
		end
		if not player.Character:FindFirstChildWhichIsA("ShirtGraphic") then
			local tShirt = Instance.new("ShirtGraphic")
			tShirt.Graphic = "rbxassetid://1"
			tShirt.Parent = character
		end

		if not playerTable[player.UserId]["switch"] then
			playerTable[player.UserId]["local_data"] = getPlayerData(player)
			playerTable[player.UserId]["switch"] = true
		end
end

local function resetCharacter(player)
	print("Player resetted avatar:", player.Name)

	if not playerTable[player.UserId] then
		playerTable[player.UserId] = {}
		playerTable[player.UserId]["cooldown"] = tick()
		playerTable[player.UserId]["modifying_data"] = false
		playerTable[player.UserId]["local_data"] = {}
		playerTable[player.UserId]["switch"] = false
	end


	local character = player.Character
	local humanoid = character:WaitForChild("Humanoid")
	character.Humanoid:RemoveAccessories()

	local humanoidDescription = humanoid:GetAppliedDescription()
	if humanoidDescription then
		local accessoriesTable = humanoidDescription:GetAccessories(true)
		print("Accessories table:", accessoriesTable)
	end

	-- Check and add default clothing if not present
	if not player.Character:FindFirstChildWhichIsA("Shirt") then
		local shirt = Instance.new("Shirt")
		shirt.ShirtTemplate = "rbxassetid://1"
		shirt.Parent = character
	end
	if not player.Character:FindFirstChildWhichIsA("Pants") then
		local pants = Instance.new("Pants")
		pants.PantsTemplate = "rbxassetid://1"
		pants.Parent = character
	end
	if not player.Character:FindFirstChildWhichIsA("ShirtGraphic") then
		local tShirt = Instance.new("ShirtGraphic")
		tShirt.Graphic = "rbxassetid://1"
		tShirt.Parent = character
	end

	if not playerTable[player.UserId]["switch"] then
		playerTable[player.UserId]["local_data"] = getPlayerData(player)
		playerTable[player.UserId]["switch"] = true
	end
end

appearanceChangedRemote.OnServerEvent:Connect(function(player)
	refreshCharacter(player)
end)

appearanceResetRemote.Event:Connect(function(player)
	resetCharacter(player)
end)

You might have better luck by doing something like, when a new shirt (or hat or whatever) is applied doing:

  • HumanoidDescription.Shirt = newShirt

This will let you use Humanoid:ApplyDescription() over having to do all that Character.Head.Decal.Texture = …

local propertiesToExtract = {
	"Archivable",
	"BackAccessory",
	"BodyTypeScale",
	"ClimbAnimation",
	"DepthScale",
	"Face",
	"FaceAccessory",
	"FallAnimation",
	"FrontAccessory",
	"GraphicTShirt",
	"HairAccessory",
	"HatAccessory",
	"Head",
	"HeadColor",
	"HeadScale",
	"HeightScale",
	"IdleAnimation",
	"JumpAnimation",
	"LeftArm",
	"LeftArmColor",
	"LeftLeg",
	"LeftLegColor",
	"NeckAccessory",
	"Pants",
	"ProportionScale",
	"RightArm",
	"RightArmColor",
	"RightLeg",
	"RightLegColor",
	"RunAnimation",
	"Shirt",
	"ShouldersAccessory",
	"SwimAnimation",
	"Torso",
	"TorsoColor",
	"WaistAccessory",
	"WalkAnimation",
	"WidthScale"
}

local extractHumanoidDescription = function(description)
	local extracted = {}
	for _,property in pairs(propertiesToExtract) do
		extracted[property] = description[property]
	end
	return extracted
end

local reconstructFromExtracted = function(extracted)
	local description = Instance.new("HumanoidDescription")
	for property,value in pairs(extracted) do
		description[property] = value
	end
	return description
end

Here’s a simple 2 functions that’ll let you serialize the description, example:

local currentDescription = humanoid:GetAppliedDescription()
local toSave = extractHumanoidDescription(currentDescription)

-- later:

local reconstructed = reconstructFromExtracted(saved)
humanoid:ApplyDescription(reconstructed)

Then you could probably create a viewport using something like:

1 Like

It’s kinda hard for me to understand advanced coding, I’ve tried to figure out how to do it with humanoid descriptions, but I can’t… I know the code is like spaghetti, but I managed to apply shirt and pants on the model

Accessories wont work because insert service doesnt work on client and im not sure how to fix this, i cant put the method in server script because i don’t know how to find the rig in the local player’s gui etc.

All I need is for accessories to work, and some additional stuff so body parts and colors save as well

Local

local localPlayer = game:GetService("Players").LocalPlayer
local character = localPlayer.Character
local replicatedStorage = game:GetService("ReplicatedStorage")

local addOutfitButton = script.Parent
local outfitSavingFrame = script.Parent.Parent.Parent.Parent.Parent.Confirmation

local scrollingFrameFits = script.Parent.Parent.Parent.Parent.Parent.Container.ScrollingFrame

local outfitTemplate = replicatedStorage.Fit_FitName
local outfitName = outfitTemplate.FitName
local outfitRig = outfitTemplate.ViewportFrame.WorldModel.Outfit
local rigAppearance = game.ReplicatedStorage.HumanoidDescLoader

local outfitSavingFrameNoButton = script.Parent.Parent.Parent.Parent.Parent.Confirmation.No
local outfitSavingFrameYesButton = script.Parent.Parent.Parent.Parent.Parent.Confirmation.Yes

repeat game:GetService("RunService").Heartbeat:Wait() until localPlayer.CharacterAppearanceLoaded -- Local script will yield until the character has FULLY LOADED
local deleteDebounce = false

function applyOutfitToModel(outfitData, model)
	if not model or not outfitData then return end

	-- Clear existing accessories
	for _, child in pairs(model:GetChildren()) do
		if child:IsA("Accessory") then
			child:Destroy()
		end
	end

	-- Add accessories from saved data
	if outfitData["Accessories"] then
		for _, accessoryData in ipairs(outfitData["Accessories"]) do
			local success, accessory = pcall(function()
				return game:GetService("InsertService"):LoadAsset(accessoryData["AssetId"]):GetChildren()[1]
			end)
			if success and accessory then
				if accessory:IsA("Accessory") then
					accessory.Parent = model
					local mesh = accessory:FindFirstChildWhichIsA("SpecialMesh")
					if mesh then
						mesh.Offset = Vector3.new(accessoryData["Offset"]["X"], accessoryData["Offset"]["Y"], accessoryData["Offset"]["Z"])
						mesh.Scale = Vector3.new(accessoryData["Scale"]["X"], accessoryData["Scale"]["Y"], accessoryData["Scale"]["Z"])
						mesh.VertexColor = Vector3.new(accessoryData["VertexColor"]["X"], accessoryData["VertexColor"]["Y"], accessoryData["VertexColor"]["Z"])
						mesh.TextureId = accessoryData["TextureId"]
					end
				else
					accessory:Destroy()
				end
			end
		end
	end

	-- Add clothing from saved data
	if outfitData["Shirt"] then
		local shirt = model:FindFirstChildWhichIsA("Shirt") or Instance.new("Shirt", model)
		shirt.ShirtTemplate = outfitData["Shirt"]
	end

	if outfitData["Pants"] then
		local pants = model:FindFirstChildWhichIsA("Pants") or Instance.new("Pants", model)
		pants.PantsTemplate = outfitData["Pants"]
	end

	if outfitData["TShirt"] then
		local tshirt = model:FindFirstChildWhichIsA("ShirtGraphic") or Instance.new("ShirtGraphic", model)
		tshirt.Graphic = outfitData["TShirt"]
	end

	if outfitData["Face"] then
		local head = model:FindFirstChild("Head")
		if head then
			local face = head:FindFirstChildWhichIsA("Decal") or Instance.new("Decal", head)
			face.Texture = outfitData["Face"]
		end
	end
end

function clientDataMessenger(saveOrLoad, key)
	print("Client data messenger invoked for", saveOrLoad, "with key:", key)
	if saveOrLoad == "Save" then
		local success, outfitData = pcall(function()
			return replicatedStorage.Remotes.SaveOutfit:InvokeServer(outfitSavingFrame.TextBox.Text)
		end)
		if success then
			applyOutfitToModel(outfitData, outfitRig)
		end
	elseif saveOrLoad == "Load" then
		local success, outfitData = pcall(function()
			return replicatedStorage.Remotes.LoadOutfit:InvokeServer(key)
		end)
		if success then
			applyOutfitToModel(outfitData, character)
		end
	end
end

replicatedStorage.Remotes.AddAccessory.OnClientInvoke = function(returnedValue)
	print("AddAccessory invoked on client with returned value:", returnedValue)
	if typeof(returnedValue) == "string" then
		print("Returned value is a string:", returnedValue)
	end
end

addOutfitButton.MouseButton1Click:Connect(function()
	outfitSavingFrame.Visible = true
	print("Clicked Add Outfit button")
end)

outfitSavingFrameNoButton.MouseButton1Click:Connect(function()
	outfitSavingFrame.Visible = false
	print("Clicked No on outfit saving frame")
end)

outfitSavingFrameYesButton.MouseButton1Click:Connect(function()
	outfitSavingFrame.Visible = false
	clientDataMessenger("Save")
	outfitSavingFrame.TextBox.Text = ""
	print("Saved outfit")
end)

replicatedStorage.Remotes.ListSaves.OnClientEvent:Connect(function(passedData)
	print("ListSaves received data:", passedData)
	local clonedSaveTemplate = outfitTemplate:Clone()
	clonedSaveTemplate.Name = passedData
	clonedSaveTemplate.FitName.Text = passedData
	clonedSaveTemplate.Parent = scrollingFrameFits

	local success, outfitData = pcall(function()
		return replicatedStorage.Remotes.GetOutfitData:InvokeServer(passedData)
	end)
	if success then
		applyOutfitToModel(outfitData, clonedSaveTemplate.ViewportFrame.WorldModel.Outfit)
	end

	clonedSaveTemplate.Click.MouseButton1Click:Connect(function()
		clientDataMessenger("Load", passedData)
		print("Clicked to load outfit:", passedData)
	end)

	clonedSaveTemplate.DeleteIcon.MouseButton1Click:Connect(function()
		replicatedStorage.Remotes.RemoveOutfit:InvokeServer(passedData)
		clonedSaveTemplate:Destroy()
		print("Clicked to delete outfit:", passedData)
	end)
end)

replicatedStorage.Remotes.RemoveOutfit.OnClientInvoke = function(success, key)
	print("RemoveOutfit invoked on client with success:", success, "and key:", key)
	if success then
		local outfitToRemove = scrollingFrameFits:FindFirstChild(key)
		if outfitToRemove then
			outfitToRemove:Destroy()
		end
	else
		task.delay(3, function()
			print("Failed to remove outfit after delay")
		end)
	end
end

Server:

local replicatedStorage = game:GetService("ReplicatedStorage")
local playerTable = {} -- this is some really basic serverside remote security. could be better
-- you can also do this with just a static boolean for each player instead of using tick() but waga baga bobo
local cooldownTime = 0.1

local dataStoreService = game:GetService("DataStoreService")
local outfitStore = dataStoreService:GetDataStore("OutfitStore")

local appearanceChangedRemote = game.ReplicatedStorage.AppearanceChanged
local appearanceResetRemote = game.ReplicatedStorage.AppearanceReset
local rigAppearance = game.ReplicatedStorage.HumanoidDescLoader

function getTextObject(message, fromPlayerId) -- another thing from the offical roblox docs
	local textObject
	local success, errorMessage = pcall(function()
		textObject = game:GetService("TextService"):FilterStringAsync(message, fromPlayerId)
	end)
	if success then
		return textObject
	elseif errorMessage then
		print("Error generating TextFilterResult:", errorMessage)
	end
	return false
end

function filterOutfitName(outfitName, player)
	local textObject = getTextObject(outfitName, player.UserId)
	if textObject then
		local filteredName = getFilteredMessage(textObject)
		return filteredName
	end
	return false
end


function getFilteredMessage(textObject)
	local filteredMessage
	local success, errorMessage = pcall(function()
		filteredMessage = textObject:GetNonChatStringForBroadcastAsync()
	end)
	if success then
		return filteredMessage
	elseif errorMessage then
		print("Error filtering message:", errorMessage)
	end
	return false
end

function insertAccessory(player, id)
	local accessoryCount = 0
	for i, v in pairs(player.Character:GetChildren()) do
		if v:IsA("Accessory") then
			accessoryCount += 1
		end
	end

	if id then
		local success, value = pcall(game:GetService("InsertService").LoadAsset, game:GetService("InsertService"), id)
		if success and value then
			if accessoryCount < 20 then
				if value:GetChildren()[1]:IsA("Accessory") then
					local accessory = value:GetChildren()[1]
					accessory:SetAttribute("AssetId", id)
					player.Character:FindFirstChildWhichIsA("Humanoid"):AddAccessory(accessory)
					value:Destroy()
					return accessory
				else
					value:Destroy()
					warn("First child of LoadAsset is not an accessory")
				end
			else
				warn("Maximum accessories reached")
			end
		else
			warn("Failed to load asset")
		end
	else
		warn("Attempted to load asset with nil ID")
	end
end



function serializeCharacter(character)
	local characterInfo = {}
	characterInfo["Accessories"] = {}

	for i, v in pairs(character:GetChildren()) do
		if v:IsA("Accessory") then
			local specialMesh = v.Handle:FindFirstChildWhichIsA("SpecialMesh")
			if specialMesh then
				local accessoryInfoTable = {}
				accessoryInfoTable["AssetId"] = v:GetAttribute("AssetId")
				accessoryInfoTable["TextureId"] = specialMesh.TextureId
				accessoryInfoTable["Offset"] = {
					X = specialMesh.Offset.X,
					Y = specialMesh.Offset.Y,
					Z = specialMesh.Offset.Z
				}
				accessoryInfoTable["Scale"] = {
					X = specialMesh.Scale.X,
					Y = specialMesh.Scale.Y,
					Z = specialMesh.Scale.Z
				}
				accessoryInfoTable["VertexColor"] = {
					X = specialMesh.VertexColor.X,
					Y = specialMesh.VertexColor.Y,
					Z = specialMesh.VertexColor.Z
				}
				table.insert(characterInfo["Accessories"], accessoryInfoTable)
			else
				warn("Accessory " .. v.Name .. " does not have a SpecialMesh")
			end
		elseif v:IsA("Shirt") then
			characterInfo["Shirt"] = v.ShirtTemplate
		elseif v:IsA("Pants") then
			characterInfo["Pants"] = v.PantsTemplate
		elseif v:IsA("ShirtGraphic") then
			characterInfo["TShirt"] = v.Graphic
		elseif v:IsA("Part") and v.Name == "Head" then
			local faceDecal = v:FindFirstChildWhichIsA("Decal")
			if faceDecal then
				characterInfo["Face"] = faceDecal.Texture
			else
				warn("Head part does not have a Decal")
			end
		end
	end

	print(characterInfo)
	return characterInfo
end

function getPlayerData(player)
	local success, value = pcall(outfitStore.GetAsync, outfitStore, player.UserId)
	if success then
		if not value then
			value = {}
		end
	else
		print("Error getting player data:", value)  -- Print the error message
	end
	return value
end

game.Players.PlayerAdded:Connect(function(player)
	print("Player added:", player.Name)

	if not playerTable[player.UserId] then
		playerTable[player.UserId] = {}
		playerTable[player.UserId]["cooldown"] = tick()
		playerTable[player.UserId]["modifying_data"] = false
		playerTable[player.UserId]["local_data"] = {}
		playerTable[player.UserId]["switch"] = false
	end

	player.CharacterAppearanceLoaded:Connect(function(character)
		print("CharacterAppearanceLoaded event triggered for", player.Name)

		character.Humanoid:RemoveAccessories()

		local accessoriesTable = game:GetService("Players"):GetHumanoidDescriptionFromUserId(player.UserId):GetAccessories(true)
		print("Accessories table:", accessoriesTable)
		for i, v in accessoriesTable do
			insertAccessory(player, v["AssetId"])
		end

		-- Check and add default clothing if not present
		if not player.Character:FindFirstChildWhichIsA("Shirt") then
			local shirt = Instance.new("Shirt")
			shirt.ShirtTemplate = "rbxassetid://1"
			shirt.Parent = character
		end
		if not player.Character:FindFirstChildWhichIsA("Pants") then
			local pants = Instance.new("Pants")
			pants.PantsTemplate = "rbxassetid://1"
			pants.Parent = character
		end
		if not player.Character:FindFirstChildWhichIsA("ShirtGraphic") then
			local tShirt = Instance.new("ShirtGraphic")
			tShirt.Graphic = "rbxassetid://1"
			tShirt.Parent = character
		end

		if not playerTable[player.UserId]["switch"] then
			playerTable[player.UserId]["local_data"] = getPlayerData(player)
			playerTable[player.UserId]["switch"] = true
		end
		local data = playerTable[player.UserId]["local_data"]
		for i, v in pairs(data) do
			replicatedStorage.Remotes.ListSaves:FireClient(player, i)
		end
		--replicatedStorage.Remotes.SaveOutfit:InvokeClient(player, "Success")
	end)
end)

game.Players.PlayerRemoving:Connect(function(player)
	if playerTable[player.UserId] then
		local success, value = pcall(function()
			outfitStore:SetAsync(player.UserId, playerTable[player.UserId]["local_data"])
		end)
		if success then
			print("Player data saved for", player.Name)
		else
			print("Error saving player data:", value)  -- Print the error message
		end

		playerTable[player.UserId] = nil
	end
	print("Player removed:", player.Name)
end)

replicatedStorage.Remotes.GetOutfitData.OnServerInvoke = function(player, outfitName)
	print("GetOutfitData invoked by", player.Name, "for outfit:", outfitName)
	return playerTable[player.UserId]["local_data"][outfitName]
end


replicatedStorage.Remotes.SaveOutfit.OnServerInvoke = function(player, outfitName)
	print("SaveOutfit invoked by", player.Name, "for outfit:", outfitName)

	-- Ensure no concurrent modifications
	if playerTable[player.UserId]["modifying_data"] then
		print("Modifying data conflict for", player.Name)
		return
	end

	-- Set modifying_data flag to true to prevent concurrent modifications
	playerTable[player.UserId]["modifying_data"] = true

	local filteredOutfitName = filterOutfitName(outfitName, player)
	if not filteredOutfitName then
		--replicatedStorage.Remotes.SaveOutfit:InvokeClient(player, "Failed to filter outfit name")
		playerTable[player.UserId]["modifying_data"] = false
		print("Failed to filter outfit name for", player.Name)
		return
	end

	local playerData = playerTable[player.UserId]["local_data"]

	for i, v in pairs(playerData) do
		if i == filteredOutfitName then
			--replicatedStorage.Remotes.SaveOutfit:InvokeClient(player, "Outfit name already taken")
			playerTable[player.UserId]["modifying_data"] = false
			print("Outfit name already taken for", player.Name)
			return
		elseif #playerData >= 50 then
			--replicatedStorage.Remotes.SaveOutfit:InvokeClient(player, "Maximum of 50 saves")
			playerTable[player.UserId]["modifying_data"] = false
			print("Maximum outfit saves reached for", player.Name)
			return
		end
	end

	local success, value = pcall(function()
		playerData[filteredOutfitName] = serializeCharacter(player.Character)
	end)
	if success then
		replicatedStorage.Remotes.ListSaves:FireClient(player, filteredOutfitName)
		--replicatedStorage.Remotes.SaveOutfit:InvokeClient(player, "Success")
		print("Outfit saved successfully for", player.Name)

		-- Reset modifying_data flag after successful save
		playerTable[player.UserId]["modifying_data"] = false
	else
		print("Error while saving outfit:", value)  -- Print the error message
		--replicatedStorage.Remotes.SaveOutfit:InvokeClient(player, "Failed to save outfit")
		print("Failed to save outfit for", player.Name)

		-- Reset modifying_data flag on error
		playerTable[player.UserId]["modifying_data"] = false
	end
end


replicatedStorage.Remotes.LoadOutfit.OnServerInvoke = function(player, outfitName)
	print("LoadOutfit invoked by", player.Name, "for outfit:", outfitName)

	if tick() - playerTable[player.UserId]["cooldown"] > cooldownTime and not playerTable[player.UserId]["modifying_data"] then
		playerTable[player.UserId]["cooldown"] = tick()
		playerTable[player.UserId]["modifying_data"] = true

		local retreivedData = playerTable[player.UserId]["local_data"][outfitName]

		print("Retrieved data:", retreivedData)

		player.Character:FindFirstChildWhichIsA("Humanoid"):RemoveAccessories()

		-- Check and set accessories
		if retreivedData["Accessories"] then
			for i, v in ipairs(retreivedData["Accessories"]) do
				local accessory = insertAccessory(player, v["AssetId"])
				if accessory then
					local mesh = accessory.Handle:FindFirstChildWhichIsA("SpecialMesh")
					if mesh then
						mesh.Offset = Vector3.new(v["Offset"]["X"], v["Offset"]["Y"], v["Offset"]["Z"])
						mesh.Scale = Vector3.new(v["Scale"]["X"], v["Scale"]["Y"], v["Scale"]["Z"])
						mesh.VertexColor = Vector3.new(v["VertexColor"]["X"], v["VertexColor"]["Y"], v["VertexColor"]["Z"])
						mesh.TextureId = v["TextureId"]
					end
				end
			end
		end

		-- Check and set clothing
		if player.Character:FindFirstChild("Shirt") then
			player.Character:FindFirstChild("Shirt").ShirtTemplate = retreivedData["Shirt"]
		end
		if player.Character:FindFirstChild("Pants") then
			player.Character:FindFirstChild("Pants").PantsTemplate = retreivedData["Pants"]
		end
		if player.Character:FindFirstChild("ShirtGraphic") then
			player.Character:FindFirstChild("ShirtGraphic").Graphic = retreivedData["TShirt"]
		end
		if player.Character.Head:FindFirstChild("Decal") then
			player.Character.Head:FindFirstChild("Decal").Texture = retreivedData["Face"]
		end

		--replicatedStorage.Remotes.LoadOutfit:InvokeClient(player, "Success")

		playerTable[player.UserId]["modifying_data"] = false
	else
		print("Cooldown or modifying data conflict for", player.Name)
	end
end

replicatedStorage.Remotes.RemoveOutfit.OnServerInvoke = function(player, outfitName)
	print("RemoveOutfit invoked by", player.Name, "for outfit:", outfitName)

	if tick() - playerTable[player.UserId]["cooldown"] > cooldownTime and not playerTable[player.UserId]["modifying_data"] then
		playerTable[player.UserId]["cooldown"] = tick()
		playerTable[player.UserId]["modifying_data"] = true

		local playerData = playerTable[player.UserId]["local_data"]
		playerData[outfitName] = nil

		--replicatedStorage.Remotes.RemoveOutfit:InvokeClient(player, true, outfitName)

		playerTable[player.UserId]["modifying_data"] = false
	else
		print("Cooldown or modifying data conflict for", player.Name)
	end
end

local function refreshCharacter(player)
	print("Player refreshed avatar:", player.Name)

	if not playerTable[player.UserId] then
		playerTable[player.UserId] = {}
		playerTable[player.UserId]["cooldown"] = tick()
		playerTable[player.UserId]["modifying_data"] = false
		playerTable[player.UserId]["local_data"] = {}
		playerTable[player.UserId]["switch"] = false
	end

	local character = player.Character
	local humanoid = character:WaitForChild("Humanoid")

	print("CharacterAppearanceLoaded event triggered for", player.Name)

	character.Humanoid:RemoveAccessories()

	local humanoidDescription = humanoid:GetAppliedDescription()
	if humanoidDescription then
		local accessoriesTable = humanoidDescription:GetAccessories(true)
		print("Accessories table:", accessoriesTable)
		for i, accessory in ipairs(accessoriesTable) do
			insertAccessory(player, accessory.AssetId)
		end
	end

	-- Check and add default clothing if not present
	if not player.Character:FindFirstChildWhichIsA("Shirt") then
		local shirt = Instance.new("Shirt")
		shirt.ShirtTemplate = "rbxassetid://1"
		shirt.Parent = character
	end
	if not player.Character:FindFirstChildWhichIsA("Pants") then
		local pants = Instance.new("Pants")
		pants.PantsTemplate = "rbxassetid://1"
		pants.Parent = character
	end
	if not player.Character:FindFirstChildWhichIsA("ShirtGraphic") then
		local tShirt = Instance.new("ShirtGraphic")
		tShirt.Graphic = "rbxassetid://1"
		tShirt.Parent = character
	end

	if not playerTable[player.UserId]["switch"] then
		playerTable[player.UserId]["local_data"] = getPlayerData(player)
		playerTable[player.UserId]["switch"] = true
	end
end

local function resetCharacter(player)
	print("Player resetted avatar:", player.Name)

	if not playerTable[player.UserId] then
		playerTable[player.UserId] = {}
		playerTable[player.UserId]["cooldown"] = tick()
		playerTable[player.UserId]["modifying_data"] = false
		playerTable[player.UserId]["local_data"] = {}
		playerTable[player.UserId]["switch"] = false
	end


	local character = player.Character
	local humanoid = character:WaitForChild("Humanoid")
	character.Humanoid:RemoveAccessories()

	local humanoidDescription = humanoid:GetAppliedDescription()
	if humanoidDescription then
		local accessoriesTable = humanoidDescription:GetAccessories(true)
		print("Accessories table:", accessoriesTable)
	end

	-- Check and add default clothing if not present
	if not player.Character:FindFirstChildWhichIsA("Shirt") then
		local shirt = Instance.new("Shirt")
		shirt.ShirtTemplate = "rbxassetid://1"
		shirt.Parent = character
	end
	if not player.Character:FindFirstChildWhichIsA("Pants") then
		local pants = Instance.new("Pants")
		pants.PantsTemplate = "rbxassetid://1"
		pants.Parent = character
	end
	if not player.Character:FindFirstChildWhichIsA("ShirtGraphic") then
		local tShirt = Instance.new("ShirtGraphic")
		tShirt.Graphic = "rbxassetid://1"
		tShirt.Parent = character
	end

	if not playerTable[player.UserId]["switch"] then
		playerTable[player.UserId]["local_data"] = getPlayerData(player)
		playerTable[player.UserId]["switch"] = true
	end
end

appearanceChangedRemote.OnServerEvent:Connect(function(player)
	refreshCharacter(player)
end)

appearanceResetRemote.Event:Connect(function(player)
	resetCharacter(player)
end)