local A=game:GetService("\082\101\112\108\105\099\097\116\101\100\083\116\111\114\097\103\101")local f=game:GetService("\080\108\097\121\101\114\115")local m=game:GetService("\084\101\120\116\083\101\114\118\105\099\101")local x=A.AvatarEditor local n=require(x.Shared.Util.Promise)local N=require(x.Shared.Util.Signal)local v=require(x.Shared.Util.TableUtil)local t=require(x.Shared.QueryMap)local u=require(x.Shared.Colors)local M=require(x.Shared.Settings)local w=require(x.Shared.GetProductInfo)local Y=require(x.Shared.UserCanUse)local G=Instance.new("\082\101\109\111\116\101\069\118\101\110\116")G.Parent=x local U={}local j={}local a={}a.HumanoidDescriptionChanged=N.new()a.CostumeAdded=N.new()a.CostumeRemoved=N.new()a.CostumeUpdated=N.new()a.SettingChanged=N.new()a.PermissionFailed=N.new()local function P(A,f)return(n.new(function(x,n)local N,v=pcall(function()return m:FilterStringAsync(A,f.UserId)end)if N then x(v)else n(v)end end)):andThen(function(A)return n.new(function(m,x)local n,N=pcall(function()return A:GetNonChatStringForUserAsync(f.UserId)end)if n then m(N)else x(N)end end)end)end local function h(A)local f=A.Character local m=f and f:FindFirstChildWhichIsA("\072\117\109\097\110\111\105\100")if m and m.Health>0 then return m end end local function o(A)return n.new(function(m,x)local n,N=pcall(function()return f:GetHumanoidDescriptionFromUserId(A)end)if n then m(N)else x(N)end end)end local function V(A,f)return n.new(function(m,x)local n,N=pcall(function()A:ApplyDescription(f)end)if n then m(n)else x(N)end end)end local function Q(A,f)a.HumanoidDescriptionChanged:Fire(A,f)local m=h(A)if not m then return n.reject()end return V(m,f)end local function r(A,...)G:FireClient(A,...)end local function X(A,f)assert(typeof(f)=="\110\117\109\098\101\114","\097\099\099\101\115\115\111\114\121\044 \105\100\044 \110\117\109\098\101\114 \101\120\112\101\099\116\101\100")local m=h(A)if not m then return end local x=m:GetAppliedDescription()if not x then return end;((w(f)):andThen(function(f)local n=t.GetOptionName("\065\115\115\101\116\084\121\112\101",f.AssetTypeId)if not n then return end if n=="\072\097\116"then n="\072\097\116\065\099\099\101\115\115\111\114\121"elseif n=="\083\104\111\117\108\100\101\114\065\099\099\101\115\115\111\114\121"then n="\083\104\111\117\108\100\101\114\115\065\099\099\101\115\115\111\114\121"end if string.find(n,"\065\099\099\101\115\115\111\114\121")then local N=string.split(x[n],"\044")local v=table.find(N,tostring(f.AssetId))if v then table.remove(N,v)elseif#m:GetAccessories()<M.MAX_ACCESSORIES then table.insert(N,tostring(f.AssetId))else print(A,"\116\114\105\101\100 \116\111 \119\101\097\114 \116\111\111 \109\097\110\121 \097\099\099\101\115\115\111\114\105\101\115")end x[n]=table.concat(N,"\044")else if x[n]==0 or x[n]~=f.AssetId then x[n]=f.AssetId else x[n]=n=="\083\104\105\114\116"and 1 or n=="\080\097\110\116\115"and 2 or 0 end end;(Q(A,x)):andThenCall(r,A,"\097\099\099\101\115\115\111\114\121")end)):catch(warn)end local function K(A,f)assert(typeof(f)=="\110\117\109\098\101\114","\116\111\110\101\044 \105\110\100\101\120\044 \110\117\109\098\101\114 \101\120\112\101\099\116\101\100")local m=h(A)if not m then return end local x=m:GetAppliedDescription()if not x then return end f=math.clamp(f,1,75)local n=u[f]if n then x.HeadColor=n x.LeftArmColor=n x.LeftLegColor=n x.RightArmColor=n x.RightLegColor=n x.TorsoColor=n end;((Q(A,x)):doneCall(r,A,"\116\111\110\101")):catch(warn)end local function D(A)local f=h(A)if not f then return end;(((o(A.UserId)):andThen(function(f)return Q(A,f)end)):doneCall(r,A,"\114\101\115\101\116")):catch(warn)end local function c(A,f,m)assert(typeof(f)=="\115\116\114\105\110\103","\115\099\097\108\101\044 \115\099\097\108\101\078\097\109\101\044 \115\116\114\105\110\103 \101\120\112\101\099\116\101\100")assert(typeof(m)=="\110\117\109\098\101\114","\115\099\097\108\101\044 \118\097\108\117\101\044 \110\117\109\098\101\114 \101\120\112\101\099\116\101\100")local x=h(A)if not x then return end local N=x:GetAppliedDescription()if not N then return end if f=="\072\101\105\103\104\116\083\099\097\108\101"then m=math.clamp(m,.9,1.05)elseif f=="\087\105\100\116\104\083\099\097\108\101"then m=math.clamp(m,.7,1)elseif f=="\072\101\097\100\083\099\097\108\101"then m=math.clamp(m,.95,1)elseif f=="\080\114\111\112\111\114\116\105\111\110\083\099\097\108\101"or f=="\066\111\100\121\084\121\112\101\083\099\097\108\101"then m=math.clamp(m,0,1)else return end;(((n.new(function(A,x)local n,v=pcall(function()N[f]=m end)if n then A(n)else x(v)end end)):andThenCall(Q,A,N)):doneCall(r,A,"\115\099\097\108\101",f,m)):catch(warn)end local function O(A,f)assert(typeof(f)=="\115\116\114\105\110\103","\099\114\101\097\116\101\044 \110\097\109\101\044 \115\116\114\105\110\103 \101\120\112\101\099\116\101\100")if f==""then return end f=string.gsub(f,"\091\037\112\037\115\093\043","")local m=U[A]if not m then return end if#m.Costumes>=M.MAX_COSTUME then return end local x=h(A)if not x then return end local n=x:GetAppliedDescription()if not n then return end local N=a:ToAppearanceInfo(n);((P(f,A)):andThen(function(f)N.Name=f local x=a:ToHumanoidDescription(N)x.Parent=m.Folder table.insert(m.Costumes,N)local v=#m.Costumes m.Map[n]=v a.CostumeAdded:Fire(A,x,N,v)end)):catch(warn)end local function I(A,f)assert(typeof(f)=="\073\110\115\116\097\110\099\101"and f:IsA("\072\117\109\097\110\111\105\100\068\101\115\099\114\105\112\116\105\111\110"),"\100\101\108\101\116\101\044 \100\101\115\099\114\105\112\116\105\111\110\044 \072\117\109\097\110\111\105\100\068\101\115\099\114\105\112\116\105\111\110 \101\120\112\101\099\116\101\100")local m=U[A]if not m then return end local x=h(A)if not x then return end if f:IsDescendantOf(m.Folder)then local x=m.Map[f]m.Map[f]=nil f:Destroy()a.CostumeRemoved:Fire(A,f,m.Costumes[x],x)end end local function Z(A,f)assert(typeof(f)=="\073\110\115\116\097\110\099\101"and f:IsA("\072\117\109\097\110\111\105\100\068\101\115\099\114\105\112\116\105\111\110"),"\119\101\097\114\044 \100\101\115\099\114\105\112\116\105\111\110\044 \072\117\109\097\110\111\105\100\068\101\115\099\114\105\112\116\105\111\110 \101\120\112\101\099\116\101\100")local m=U[A]if not m then return end local x=h(A)if not x then return end if f:IsDescendantOf(m.Folder)then((Q(A,f)):doneCall(r,A,"\114\101\115\101\116")):catch(warn)end end local function B(A,f)assert(typeof(f)=="\115\116\114\105\110\103","\116\104\101\109\101\044 \116\104\101\109\101\078\097\109\101\044 \115\116\114\105\110\103 \101\120\112\101\099\116\101\100")local m=j[A]if not m then return end if not x.Themes:FindFirstChild(f)then G:FireClient(A,"\116\104\101\109\101")return end m.Folder:SetAttribute("\084\104\101\109\101",f)a.SettingChanged:Fire(A,"\084\104\101\109\101",f)end local d={accessory=X,reset=D,tone=K;scale=c,create=O,delete=I,wear=Z,theme=B}local function g(A,f,...)assert(typeof(f)=="\115\116\114\105\110\103","\111\110\083\101\114\118\101\114\069\118\101\110\116\044 \107\101\121\044 \115\116\114\105\110\103 \101\120\112\101\099\116\101\100")local m=Y:CanUse(A)if not m then a.PermissionFailed:Fire(A)return end local x=d[f]if x then x(A,...)end end local function z(A)local f=U[A]if f then for A,m in pairs(f.Map)do f.Map[A]=nil end f.Map=nil f.Folder:Destroy()f.Folder=nil f.Costumes=nil U[A]=nil end local m=j[A]if m then m.Folder:Destroy()m.Folder=nil m.Settings=nil j[A]=nil end end G.OnServerEvent:Connect(g)f.PlayerRemoving:Connect(z)function a.CacheSettings(m,A,f)local x=Instance.new("\067\111\110\102\105\103\117\114\097\116\105\111\110")x.Name="\065\069\095\083\101\116\116\105\110\103\115"for A,f in pairs(f)do x:SetAttribute(A,f)end j[A]={Folder=x,Settings=f}x.Parent=A end function a.CacheOutfits(m,A,f)local x=Instance.new("\070\111\108\100\101\114")x.Name="\065\069\095\067\111\115\116\117\109\101\115"local n={}for f,m in ipairs(f)do local N=a:ToHumanoidDescription(m);((P(m.Name,A)):andThen(function(A)m.Name=A end)):catch(warn)N.Parent=x n[N]=f end U[A]={Folder=x;Costumes=v.Copy(f),Map=n}x.Parent=A end function a.ApplyDescriptionFromInfo(m,A,f)return(n.new(function(f)local m=A:WaitForChild("\072\117\109\097\110\111\105\100")if not m:IsDescendantOf(workspace)then m.AncestryChanged:Wait()end f(m)end)):andThen(function(A)local m=f and a:ToHumanoidDescription(f)if m then(V(A,m)):catch(warn)end end)end local function e(A)local f,m,x=string.match(A,"\094\035\063\040\037\119\037\119\041\040\037\119\037\119\041\040\037\119\037\119\041\036")return Color3.fromRGB(tonumber(f,16),tonumber(m,16),tonumber(x,16))end local function L(A)return string.format("\035\037\048\050\088\037\048\050\088\037\048\050\088",A.R*255,A.G*255,A.B*255)end function a.ToHumanoidDescription(f,A)local m=Instance.new("\072\117\109\097\110\111\105\100\068\101\115\099\114\105\112\116\105\111\110")for A,f in pairs(A)do if string.find(A,"\067\111\108\111\114")then f=e(f)end m[A]=f end return m end function a.ToAppearanceInfo(f,A)assert(typeof(A)=="\073\110\115\116\097\110\099\101"and A:IsA("\072\117\109\097\110\111\105\100\068\101\115\099\114\105\112\116\105\111\110"),"\084\111\065\112\112\101\097\114\097\110\099\101\073\110\102\111 \105\115 \110\111\116 \097 \072\117\109\097\110\111\105\100\068\101\115\099\114\105\112\116\105\111\110")local m={}m.BackAccessory=A.BackAccessory m.FaceAccessory=A.FaceAccessory m.FrontAccessory=A.FrontAccessory m.HairAccessory=A.HairAccessory m.HatAccessory=A.HatAccessory m.NeckAccessory=A.NeckAccessory m.ShouldersAccessory=A.ShouldersAccessory m.WaistAccessory=A.WaistAccessory m.Face=A.Face m.Head=A.Head m.LeftArm=A.LeftArm m.LeftLeg=A.LeftLeg m.RightArm=A.RightArm m.RightLeg=A.RightLeg m.Torso=A.Torso m.BodyTypeScale=A.BodyTypeScale m.DepthScale=A.DepthScale m.HeadScale=A.HeadScale m.HeightScale=A.HeightScale m.ProportionScale=A.ProportionScale m.WidthScale=A.WidthScale m.Pants=A.Pants m.Shirt=A.Shirt m.HeadColor=L(A.HeadColor)m.LeftArmColor=L(A.LeftArmColor)m.LeftLegColor=L(A.LeftLegColor)m.RightArmColor=L(A.RightArmColor)m.RightLegColor=L(A.RightLegColor)m.TorsoColor=L(A.TorsoColor)m.ClimbAnimation=A.ClimbAnimation m.FallAnimation=A.FallAnimation m.IdleAnimation=A.IdleAnimation m.JumpAnimation=A.JumpAnimation m.RunAnimation=A.RunAnimation m.SwimAnimation=A.SwimAnimation m.WalkAnimation=A.WalkAnimation for A,f in pairs(m)do if f==""or f==0 and not string.find(A,"\083\099\097\108\101")then m[A]=nil end end return m end function a.GetSettings(f,A)return j[A]end function a.GetOutfits(f,A)return U[A]end return a
-- Class
-- 0_1195
-- January 10, 2021

--[[



]]

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local Text = game:GetService("TextService")

local AvatarEditor = ReplicatedStorage.AvatarEditor
local Promise = require(AvatarEditor.Shared.Util.Promise)
local Signal = require(AvatarEditor.Shared.Util.Signal)
local TableUtil = require(AvatarEditor.Shared.Util.TableUtil)
local QueryMap = require(AvatarEditor.Shared.QueryMap)
local Colors = require(AvatarEditor.Shared.Colors)
local Settings = require(AvatarEditor.Shared.Settings)
local GetProductInfo = require(AvatarEditor.Shared.GetProductInfo)
local UserCanUse = require(AvatarEditor.Shared.UserCanUse)

local remoteEvent = Instance.new("RemoteEvent")
remoteEvent.Parent = AvatarEditor

local outfitCache = {}
local settingsCache = {}

local module = {}
module.HumanoidDescriptionChanged = Signal.new()
module.CostumeAdded = Signal.new()
module.CostumeRemoved = Signal.new()
module.CostumeUpdated = Signal.new() -- TODO update outfit
module.SettingChanged = Signal.new()
module.PermissionFailed = Signal.new()


local function filterText(text, player)
	return Promise.new(function(resolve, reject)
		local success, result = pcall(function()
			return Text:FilterStringAsync(text, player.UserId)
		end)
		if success then
			resolve(result)
		else
			reject(result)
		end
	end)
	:andThen(function(textFilterResult)
		return Promise.new(function(resolve, reject)
			local success, result = pcall(function()
				-- hopefully this appeases the roblox gods
				return textFilterResult:GetNonChatStringForUserAsync(player.UserId)
			end)
			if success then
				resolve(result)
			else
				reject(result)
			end
		end)
	end)
end


local function getHumanoid(player)
	local character = player.Character
	local humanoid = character and character:FindFirstChildWhichIsA("Humanoid")
	if humanoid and humanoid.Health > 0 then
		return humanoid
	end
end


local function getHumanoidDescriptionFromUserId(userId)
	return Promise.new(function(resolve, reject)
		local success, result = pcall(function()
			return Players:GetHumanoidDescriptionFromUserId(userId)
		end)
		if success then
			resolve(result)
		else
			reject(result)
		end
	end)
end


local function applyDescriptionToHumanoid(humanoid, description)
	return Promise.new(function(resolve, reject)
		local success, result = pcall(function()
			humanoid:ApplyDescription(description)
		end)
		if success then
			resolve(success)
		else
			reject(result)
		end
	end)
end


local function applyDescriptionToPlayer(player, description)
	module.HumanoidDescriptionChanged:Fire(player, description)
	
	local humanoid = getHumanoid(player)
	if not humanoid then
		return Promise.reject()
	end
	
	return applyDescriptionToHumanoid(humanoid, description)
end


local function descriptionUpdated(player, ...)
	remoteEvent:FireClient(player, ...)
end


local function accessory(player, id)
	assert(typeof(id) == "number", "accessory, id, number expected")
	
	local humanoid = getHumanoid(player)
	if not humanoid then
		return
	end

	local description = humanoid:GetAppliedDescription()
	if not description then
		return
	end
	
	GetProductInfo(id)
	:andThen(function(productInfo)
		local assetType = QueryMap.GetOptionName("AssetType", productInfo.AssetTypeId)
		if not assetType then
			return
		end
		
		-- really Roblox...??
		if assetType == "Hat" then
			assetType = "HatAccessory"
		elseif assetType == "ShoulderAccessory" then
			assetType = "ShouldersAccessory"
		end
		
		if string.find(assetType, "Accessory") then
			local accessories = string.split(description[assetType], ",")
			local index = table.find(accessories, tostring(productInfo.AssetId))
			if index then
				table.remove(accessories, index)
			elseif #humanoid:GetAccessories() < Settings.MAX_ACCESSORIES then
				table.insert(accessories, tostring(productInfo.AssetId))
			else
				-- TODO should probably notify the client
				print(player, "tried to wear too many accessories")
			end
			description[assetType] = table.concat(accessories, ",")
		else
			if description[assetType] == 0 or (description[assetType] ~= productInfo.AssetId) then
				description[assetType] = productInfo.AssetId
			else
				-- https://devforum.roblox.com/t/default-shirt-and-pants-to-be-applied-if-the-requested-ones-are-unavailable-moderated/381818
				-- force load default clothes. hacky
				-- hopefully there will never be a shirt or pants with id = 1 or 2
				description[assetType] = (assetType == "Shirt") and 1 or (assetType == "Pants") and 2 or 0
			end
		end
		
		applyDescriptionToPlayer(player, description)
		:andThenCall(descriptionUpdated, player, "accessory")
	end)
	:catch(warn)
end

local function tone(player, index)
	assert(typeof(index) == "number", "tone, index, number expected")

	local humanoid = getHumanoid(player)
	if not humanoid then
		return
	end

	local description = humanoid:GetAppliedDescription()
	if not description then
		return
	end

	index = math.clamp(index, 1, 75)

	local color = Colors[index]
	if color then
		description.HeadColor = color
		description.LeftArmColor = color
		description.LeftLegColor = color
		description.RightArmColor = color
		description.RightLegColor = color
		description.TorsoColor = color
	end

	applyDescriptionToPlayer(player, description)
	:doneCall(descriptionUpdated, player, "tone")
	:catch(warn)
end

local function reset(player)
	local humanoid = getHumanoid(player)
	if not humanoid then
		return
	end
	
	getHumanoidDescriptionFromUserId(player.UserId)
	:andThen(function(description)
		return applyDescriptionToPlayer(player, description)
	end)
	:doneCall(descriptionUpdated, player, "reset")
	:catch(warn)
end

local function scale(player, scaleName, value)
	assert(typeof(scaleName) == "string", "scale, scaleName, string expected")
	assert(typeof(value) == "number", "scale, value, number expected")

	local humanoid = getHumanoid(player)
	if not humanoid then
		return
	end

	local description = humanoid:GetAppliedDescription()
	if not description then
		return
	end

	if scaleName == "HeightScale" then
		value = math.clamp(value, 0.9, 1.05)
	elseif scaleName == "WidthScale" then
		value = math.clamp(value, 0.7, 1)
	elseif scaleName == "HeadScale" then
		value = math.clamp(value, 0.95, 1)
	elseif scaleName == "ProportionScale" or scaleName == "BodyTypeScale" then
		value = math.clamp(value, 0, 1)
	else
		return
	end

	Promise.new(function(resolve, reject)
		local success, result = pcall(function()
			description[scaleName] = value
		end)
		if success then
			resolve(success)
		else
			reject(result)
		end
	end)
	:andThenCall(applyDescriptionToPlayer, player, description)
	:doneCall(descriptionUpdated, player, "scale", scaleName, value)
	:catch(warn)
end


local function create(player, name)
	assert(typeof(name) == "string", "create, name, string expected")
	
	if name == "" then
		return
	end
	name = string.gsub(name, "[%p%s]+", "")
	
	local cache = outfitCache[player]
	if not cache then
		return
	end
	
	if #cache.Costumes >= Settings.MAX_COSTUME then
		return
	end
	
	local humanoid = getHumanoid(player)
	if not humanoid then
		return
	end

	local description = humanoid:GetAppliedDescription()
	if not description then
		return
	end
	
	local info = module:ToAppearanceInfo(description)
	filterText(name, player)
	:andThen(function(result)
		info.Name = result
		
		-- converting it back to a HumanoidDescription for the client to use the filtered name
		local descriptionCopy = module:ToHumanoidDescription(info)
		descriptionCopy.Parent = cache.Folder
		table.insert(cache.Costumes, info)
		local index = #cache.Costumes
		cache.Map[description] = index
		
		module.CostumeAdded:Fire(player, descriptionCopy, info, index)
	end)
	:catch(warn)
end


local function delete(player, description)
	assert(typeof(description) == "Instance" and description:IsA("HumanoidDescription"), "delete, description, HumanoidDescription expected")
	
	local cache = outfitCache[player]
	if not cache then
		return
	end

	local humanoid = getHumanoid(player)
	if not humanoid then
		return
	end
	
	if description:IsDescendantOf(cache.Folder) then
		local index = cache.Map[description]
		cache.Map[description] = nil
		description:Destroy()
		module.CostumeRemoved:Fire(player, description, cache.Costumes[index], index)
	end
end


--local function update(player, description)
--	assert(typeof(description) == "Instance" and description:IsA("HumanoidDescription"))
	
--end


local function wear(player, description)
	assert(typeof(description) == "Instance" and description:IsA("HumanoidDescription"), "wear, description, HumanoidDescription expected")
	
	local cache = outfitCache[player]
	if not cache then
		return
	end
	
	local humanoid = getHumanoid(player)
	if not humanoid then
		return
	end
	
	if description:IsDescendantOf(cache.Folder) then
		applyDescriptionToPlayer(player, description)
		:doneCall(descriptionUpdated, player, "reset")
		:catch(warn)
	end
end


local function theme(player, themeName)
	assert(typeof(themeName) == "string", "theme, themeName, string expected")
	
	local _settings = settingsCache[player]
	if not _settings then
		return
	end
	
	if not AvatarEditor.Themes:FindFirstChild(themeName) then
		remoteEvent:FireClient(player, "theme")
		return
	end
	
	_settings.Folder:SetAttribute("Theme", themeName)
	module.SettingChanged:Fire(player, "Theme", themeName)
end


local actions = {
	["accessory"] = accessory,
	["reset"] = reset,
	["tone"] = tone,
	["scale"] = scale,
	["create"] = create,
	["delete"] = delete,
	["wear"] = wear,
	["theme"] = theme,
}
local function onServerEvent(player, key, ...)
	assert(typeof(key) == "string", "onServerEvent, key, string expected")
	
	local canUse = UserCanUse:CanUse(player)
	if not canUse then
		module.PermissionFailed:Fire(player)
		return
	end
	
	local callback = actions[key]
	if callback then
		callback(player, ...)
	end
end


local function playerRemoving(player)
	local cache = outfitCache[player]
	if cache then
		for description, i in pairs(cache.Map) do
			cache.Map[description] = nil
		end
		cache.Map = nil
		cache.Folder:Destroy()
		cache.Folder = nil
		cache.Costumes = nil
		outfitCache[player] = nil
	end
	
	local _settings = settingsCache[player]
	if _settings then
		_settings.Folder:Destroy()
		_settings.Folder = nil
		_settings.Settings = nil
		settingsCache[player] = nil
	end
end


remoteEvent.OnServerEvent:Connect(onServerEvent)
Players.PlayerRemoving:Connect(playerRemoving)


function module:CacheSettings(player, config)
	-- TODO maybe validate table??
	local folder = Instance.new("Configuration")
	folder.Name = "AE_Settings"
	
	for k, v in pairs(config) do
		folder:SetAttribute(k, v)
	end
	
	settingsCache[player] = {
		Folder = folder,
		Settings = config,
	}
	
	folder.Parent = player
end


function module:CacheOutfits(player, costumes)
	local folder = Instance.new("Folder")
	folder.Name = "AE_Costumes"
	
	local map = {}
	
	for i, v in ipairs(costumes) do
		-- https://developer.roblox.com/en-us/api-reference/function/TextService/FilterStringAsync
		-- apparently you need to filter again to keep it updated
		-- is caching illegal? dont ban me roblox. its not publically displayed for everyone, just the user
		
		local description = module:ToHumanoidDescription(v)
		filterText(v.Name, player)
		:andThen(function(result)
			-- update the saved name to comply with new filter rules, if any.
			v.Name = result
		end)
		:catch(warn)
		description.Parent = folder
		map[description] = i
	end
	
	outfitCache[player] = {
		Folder = folder,
		Costumes = TableUtil.Copy(costumes),
		Map = map,
	}
	
	folder.Parent = player
end


function module:ApplyDescriptionFromInfo(character, info)
	return Promise.new(function(resolve)
		local humanoid = character:WaitForChild("Humanoid")

		if not humanoid:IsDescendantOf(workspace) then
			humanoid.AncestryChanged:Wait()
		end
		
		resolve(humanoid)
	end)
	:andThen(function(humanoid)
		local description = info and module:ToHumanoidDescription(info)
		if description then
			applyDescriptionToHumanoid(humanoid, description)
			:catch(warn)
		end
	end)
end


local function fromHex(hex)
	local r, g, b = string.match(hex, "^#?(%w%w)(%w%w)(%w%w)$") -- owo
	return Color3.fromRGB(tonumber(r, 16), tonumber(g, 16), tonumber(b, 16))
end

local function toHex(color)
	return string.format("#%02X%02X%02X", color.R * 255, color.G * 255, color.B * 255)
end


function module:ToHumanoidDescription(info)
	-- TODO should probably check if _info_ is valid but whatever
	local description = Instance.new("HumanoidDescription")
	for k, v in pairs(info) do
		if string.find(k, "Color") then
			v = fromHex(v)
		end
		description[k] = v
	end
	
	-- TODO not sure if filter is needed here, need to get the player somehow
	--filterText(description.Name, player)
	--:andThen(function(result) --print(result)
	--	info.Name = result
	--end)
	--:catch(warn)
	
	return description
end


function module:ToAppearanceInfo(description)
	assert(typeof(description) == "Instance" and description:IsA("HumanoidDescription"), "ToAppearanceInfo is not a HumanoidDescription")
	
	local info = {}
	
	-- TODO here too
	--filterText(description.Name, player)
	--:andThen(function(result) --print(result)
	--	info.Name = result
	--end)
	--:catch(warn)
	
	info.BackAccessory = description.BackAccessory
	info.FaceAccessory = description.FaceAccessory
	info.FrontAccessory = description.FrontAccessory
	info.HairAccessory = description.HairAccessory
	info.HatAccessory = description.HatAccessory
	info.NeckAccessory = description.NeckAccessory
	info.ShouldersAccessory = description.ShouldersAccessory
	info.WaistAccessory = description.WaistAccessory
	
	info.Face = description.Face
	info.Head = description.Head
	info.LeftArm = description.LeftArm
	info.LeftLeg = description.LeftLeg
	info.RightArm = description.RightArm
	info.RightLeg = description.RightLeg
	info.Torso = description.Torso
	
	info.BodyTypeScale = description.BodyTypeScale
	info.DepthScale = description.DepthScale
	info.HeadScale = description.HeadScale
	info.HeightScale = description.HeightScale
	info.ProportionScale = description.ProportionScale
	info.WidthScale = description.WidthScale
	
	info.Pants = description.Pants
	info.Shirt = description.Shirt
	
	info.HeadColor = toHex(description.HeadColor)
	info.LeftArmColor = toHex(description.LeftArmColor)
	info.LeftLegColor = toHex(description.LeftLegColor)
	info.RightArmColor = toHex(description.RightArmColor)
	info.RightLegColor = toHex(description.RightLegColor)
	info.TorsoColor = toHex(description.TorsoColor)
	
	info.ClimbAnimation = description.ClimbAnimation
	info.FallAnimation = description.FallAnimation
	info.IdleAnimation = description.IdleAnimation
	info.JumpAnimation = description.JumpAnimation
	info.RunAnimation = description.RunAnimation
	info.SwimAnimation = description.SwimAnimation
	info.WalkAnimation = description.WalkAnimation
	
	for k, v in pairs(info) do
		if v == "" or ((v == 0) and not (string.find(k, "Scale"))) then
			info[k] = nil
		end
	end
	
	return info
end


function module:GetSettings(player)
	return settingsCache[player]
end


function module:GetOutfits(player)
	return outfitCache[player]
end


return module
