--!optimize 2
--!native

local Shared = script.Parent

local Auth = require(Shared:WaitForChild("Auth"))
local Data = require(Shared.Data:WaitForChild("Defaults"))
local Hook = require(Shared:WaitForChild("Hook"))
local Registry = require(Shared:WaitForChild("Registry"))
local Util = require(Shared:WaitForChild("Util"))
local Command = require(script:WaitForChild("Command"))
local Type = require(Shared:WaitForChild("Type"))

type ArgumentType = Type.ArgumentType
type ArgumentDefinition = Type.ArgumentDefinition

--- @class Process
local Process = {
	ignoreFormat = `"\`%%$%%%%^%%*%%.%%+%%-%%(%%)%%[%%]\{\}%%p`,
}

--- Parses a string into a raw command format.
function Process.rawParse(message: string, prefixes: { string }, delimiter: string): { any }?
	local escapedSplit = Util.String.escapePattern(delimiter)
	local commandFormat = `%s[^%s"\`,{escapedSplit}]`
	local commandStart, prefix
	for _, value in prefixes do
		local escapedPrefix = Util.String.escapePattern(value)
		commandStart = string.find(message, string.format(commandFormat, escapedPrefix, escapedPrefix))
		if commandStart then
			prefix = value
			break
		end
	end
	if not prefix then
		return
	end

	local batchIgnore = string.format(`[%s{Process.ignoreFormat}{escapedSplit}]`, Util.String.escapePattern(prefix))
	message = string.sub(message, commandStart + #prefix)

	local len, invalidPos = utf8.len(message)
	if not len then
		message = string.sub(message, invalidPos - 1)
	end

	local argument = { 1 }
	local command = { argument }
	local commands = { command }
	local lastChar, quote = "", nil

	for first, last in utf8.graphemes(message) do
		local char: string = string.sub(message, first, last)
		if quote then
			table.insert(argument, char)
			if char == quote then
				quote = nil
			end
		elseif lastChar == prefix and #argument == 1 and string.find(char, batchIgnore) then
			table.remove(commands)
			command = commands[#commands]
			argument = { first - 1, prefix, char }
			table.insert(command, argument)
		elseif char == prefix and (string.find(lastChar, "%s") or string.find(lastChar, delimiter)) then
			if
				#argument == 1
				or string.find(argument[2] :: string, "%s")
				or string.find(argument[2] :: string, delimiter)
			then
				table.remove(command)
			elseif string.find(lastChar, "%s") then
				table.remove(argument)
			end
			argument = { first + 1 }
			command = { argument }
			table.insert(commands, command)
		elseif char == "`" or char == '"' then
			quote = char
			if string.find(lastChar, delimiter) then
				if #argument == 1 then
					argument[1], argument[2] = first, char
				else
					argument = { first, char }
					table.insert(command, argument)
				end
			else
				table.insert(argument, char)
			end
		elseif string.find(char :: string, delimiter) and not string.find(lastChar, delimiter) then
			argument = { first + 1 }
			table.insert(command, argument)
		else
			table.insert(argument, char)
		end
		lastChar = char
	end

	-- compiles arg char list into a string
	for _, cmd in commands do
		for argIndex, arg in cmd do
			local position = arg[1]
			local text = table.concat(arg, nil, 2)
			table.clear(arg)
			arg[1] = position + #prefix
			arg[2] = text
		end
	end

	return commands
end

function Process.prepareCommands(_K: any, from: number, rawText: string)
	if not utf8.len(rawText) then
		return false, "Invalid utf8 string"
	end

	local text = Util.String.trimStart(rawText)
	local rawCommands = Process.rawParse(
		text,
		{ _K.getCommandPrefix(from), unpack(_K.Data.settings.prefix) },
		_K.Data.settings.splitKey or " "
	)
	if not rawCommands then
		return
	end

	local commands = {}

	for i, commandArray in rawCommands do
		local commandText = commandArray[1][2]
		local commandAlias = string.lower(commandText)
		local commandDefinition = Registry.commands[commandAlias]

		if not commandDefinition then
			return false, `Invalid command: {commandText}, did you mean "{Registry.suggestCommandAlias(commandAlias)}"?`
		end

		if not Auth.hasCommand(from, commandDefinition) then
			if _K.IsClient then
				for role, roleData in Data.roles do
					if
						((role == "vip" and _K.Data.settings.vip) or roleData.gamepasses)
						and Auth.roleCanUseCommand(role, commandDefinition)
					then
						if role == "vip" and _K.Data.settings.vip then
							_K.PromptPurchaseVIP()
						elseif roleData.gamepasses[1] then
							_K.Service.Marketplace:PromptGamePassPurchase(_K.LocalPlayer, roleData.gamepasses[1])
						end
					end
				end
				return false,
					`"<b>{commandText}</b>" command restricted to <b>{commandDefinition.RestrictedToRole.name}</b>`
			end
			return false, `"<b>{commandText}</b>" command restricted`
		end

		local command = Command.new(_K, commandAlias, commandDefinition, commandArray, from, text)
		local ok, result = command:validate()

		table.insert(commands, command)

		if not ok then
			command.Error = result
			return false, result
		end
	end

	return true, commands
end

local commandLimits = {}
local function resetLimit(from: number)
	commandLimits[from] = nil
end

function Process.runCommands(_K: any, from: number, rawText: string, noLog: boolean?, global: boolean?): boolean
	from = from or _K.creatorId

	local cooldown, limit
	if _K.IsServer then
		local _rank, role = _K.Auth.getRank(from)
		if role.limits then
			cooldown, limit = role.limits.cooldown, role.limits.commands
		end
		if (commandLimits[from] or 0) > (limit or math.huge) then
			_K.alert(from, `<b>Command usage on cooldown!</b>`, true)
			return false
		end
		if #rawText > 1024 and not _K.Auth.hasRestrictedRole(from) then
			rawText = string.sub(rawText, 1, 1024)
		end
	end

	local ok, result = Process.prepareCommands(_K, from, rawText)

	local commandsRan = {}
	local commandErrors = {}

	if ok then
		Hook.runPreparedCommands:Fire(from, result, rawText)

		for index, command in result do
			if global then
				command.global = true
			end
			local _ok, err = command:run()
			if err then
				local escapedError = _K.Util.String.escapeRichText(err)
				table.insert(commandErrors, `<b>"{command.alias}" command failed:</b> {escapedError}`)
			else
				table.insert(commandsRan, command)
			end
			if _K.IsServer and limit then
				if not commandLimits[from] then
					commandLimits[from] = 0
					task.delay(cooldown, resetLimit, from)
				end
				commandLimits[from] += 1
				if commandLimits[from] > limit then
					local notRan = {}
					for _ = index, #result do
						table.insert(notRan, command.alias)
					end
					local appendText = if #notRan == 0 then "" else `\nCommands not ran: {table.concat(notRan, ", ")}`
					_K.alert(from, `<b>Command limit reached!</b>{appendText}`, true)
					break
				end
			end
		end
	else
		table.insert(commandErrors, result)
	end

	if #commandErrors > 0 then
		local commandError = `{table.concat(commandErrors, "\n")}`
		if _K.IsServer then
			local player = _K.Service.Players:GetPlayerByUserId(from)
			if player then
				_K.Remote.Notify:Fire(
					player,
					{ Text = commandError, From = "_K", TextColor3 = Color3.new(1, 0, 0), Sound = "Call_Leave" }
				)
				for _, err in commandErrors do
					_K.Remote.Log:Fire(player, {
						text = err,
						level = _K.Logger:encode("ERROR"),
						time = workspace:GetServerTimeNow(),
						from = from,
						name = player.Name,
					})
				end
			end
		else
			task.spawn(_K.Notify, {
				From = "_K",
				Text = commandError,
				TextColor3 = Color3.new(1, 0, 0),
				Sound = "Call_Leave",
			})
			for _, err in commandErrors do
				_K.log(err, "ERROR", from)
			end
		end
	end

	if _K.IsClient and #commandsRan > 0 then
		local prefix = _K.getCommandPrefix(from)
		local commandStrings, runOnServer = {}, false
		for _, command in commandsRan do
			if not command.definition.run then
				continue
			end
			runOnServer = true
			local last = command.array[#command.array]
			table.insert(commandStrings, string.sub(rawText, command.array[1][1] :: number, last[1] + #last[2]))
		end
		if runOnServer then
			_K.Remote.Command:Fire(`{prefix}{table.concat(commandStrings, " " .. prefix)}`)
		end
	end

	if #commandsRan == 0 and #commandErrors == 0 then
		-- skip logging if nothing happened
		return true
	end

	task.spawn(function()
		if noLog then
			return
		end

		local logText

		if #commandsRan > 0 then
			local index, log, tasks = 1, {}, Util.Tasks.new()
			local prefix = _K.getCommandPrefix()
			for _, command in commandsRan do
				if command.definition.noLog or (_K.IsClient and not command.definition.runClient) then
					continue
				end
				log[index] = prefix .. command.alias
				index += 1
				for _, arg in command.args do
					local position = index
					tasks:add(function()
						local argString
						if arg.rawType.log then
							argString = arg.rawType.log(arg)
						else
							argString = Util.Table.concat(arg.parsedArgs, ",")
							if arg.rawType.filterLog and _K.IsServer then
								argString = Util.String.filterForBroadcast(argString, from)
							end
						end
						log[position] = argString
					end)
					index += 1
				end
			end

			tasks:wait()

			if #log == 0 then
				return
			end

			logText = table.concat(log, " ")
		else -- filter entire rawText as broadcast
			local trimmed = Util.String.trim(rawText)
			if #trimmed == 0 then
				return
			end
			logText = if _K.IsServer then Util.String.filterForBroadcast(trimmed, from) else trimmed
		end

		_K.log(logText or "", "COMMAND", from)
	end)

	return #commandErrors == 0
end

return Process


local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local Hook = require(script.Parent:WaitForChild("Hook"))
local Util = require(script.Parent:WaitForChild("Util"))
local Type = require(script.Parent:WaitForChild("Type"))

--- @within Registry
--- @prop types { [string]: ArgumentType }

--- @within Registry
--- @prop commands { [string]: CommandDefinition }

--- @within Registry
--- @prop commandsList { CommandDefinition }

--- @class Registry
local Registry = {
	types = {} :: { [string]: Type.ArgumentType },
	commands = {},
	commandsList = {},
	commandNames = {},
}

--- Makes an Enum argument type definition
function Registry.makeEnumType(typeName: string, enumItems: { EnumItem | string })
	local suggestions = Util.Suggest.new(enumItems)
	return {
		displayName = typeName,
		validate = function(text)
			return Util.Suggest.query(text, suggestions)[1] ~= nil, `Invalid {typeName}`
		end,
		parse = function(text)
			return Util.Suggest.query(text, suggestions, enumItems)[1][3]
		end,
		suggestions = function(text)
			return suggestions, enumItems
		end,
	}
end

--- @within Registry
--- @type SequenceDefinition { constructor: (...any) -> any, parse: (...any) -> any, transform: (string) -> any, validate: (any) -> boolean, prefixes: { [string]: string } }
export type SequenceDefinition = {
	constructor: (...any) -> any,
	parse: (...any) -> any,
	transform: (string) -> any,
	validate: (any) -> boolean,
	prefixes: { [string]: string },
}

local function commandSort(a, b)
	if #a == #b then
		return a < b
	end
	return #a < #b
end

local debounceSort = Util.Function.debounce(0.2, table.sort)

local function cacheCommandNames(command)
	local name = string.lower(command.name)
	local oldCommand = table.find(Registry.commandNames, name)
	if oldCommand then
		oldCommand = Registry.commands[name]
		if oldCommand.aliases then
			for _, alias in oldCommand.aliases do
				table.remove(Registry.commandNames, table.find(Registry.commandNames, alias))
			end
		end
	else
		table.insert(Registry.commandNames, name)
	end

	if command.aliases then
		for _, alias in command.aliases do
			alias = string.lower(alias :: string)
			table.insert(Registry.commandNames, alias)
		end
	end
	debounceSort(Registry.commandNames, commandSort)
end

--- Finds the command alias with the lowest Levenshtein distance to the provided alias.
function Registry.suggestCommandAlias(alias: string): string
	local suggestedAlias = ""
	local lowestDistance = math.huge
	for _, commandAlias in Registry.commandNames do
		local distance = Util.String.levenshteinDistance(alias, commandAlias)
		if distance < lowestDistance then
			lowestDistance = distance
			suggestedAlias = commandAlias
		end
		if distance == 0 then
			break
		end
	end

	return suggestedAlias
end

--- Makes an sequence argument type definition, useful for Color3, Vector3, etc
function Registry.makeSequenceType(definition: SequenceDefinition?)
	local options = definition or {}
	assert(options.parse ~= nil or options.constructor ~= nil, "Must provide one of: constructor, parse")

	options.transform = options.transform or function(...)
		return ...
	end
	options.validate = options.validate or function()
		return true
	end

	return {
		prefixes = options.prefixes,
		transform = function(text)
			return Util.Table.map(string.split(text, ","), function(text)
				return options.transform(text)
			end)
		end,
		validate = function(components)
			if options.length and #components > options.length then
				return false, `Maximum of {options.length} values allowed in sequence`
			end
			for i = 1, options.Length or #components do
				local valid, reason = options.validate(components[i], i)
				if not valid then
					return false, reason
				end
			end
			return true
		end,
		parse = options.parse or function(components)
			return options.constructor(unpack(components))
		end,
	}
end

--- Registers an argument type
function Registry.registerType(name: string, typeObject: Type.ArgumentType, override: Type.Dict?)
	assert(name and typeof(name) == "string", "Invalid type name: " .. typeof(name))
	assert(Registry.types[name] == nil, 'Type "' .. name .. '" already exists!')

	if override then
		for key, value in override do
			typeObject[key] = value
		end
	end

	typeObject.name = name
	typeObject.displayName = typeObject.displayName or name
	Registry.types[name] = typeObject
end

--- Registers a command
function Registry.registerCommand(_K: any, commandObject: Type.CommandDefinition)
	local lowerName = string.lower(commandObject.name)
	if not commandObject.name then
		return -- invalid commandObject definition
	end
	local oldCommand = Registry.commands[lowerName]
	if oldCommand and string.lower(oldCommand.name) == lowerName then
		print("Overriding Kohl's Admin command: " .. lowerName)
		local index = table.find(Registry.commandsList, oldCommand)
		if index then
			if not commandObject.group then
				commandObject.group = oldCommand.group
			end
			Registry.commandsList[index] = commandObject
		end
		if oldCommand.aliases then
			for _, alias in oldCommand.aliases do
				Registry.commands[string.lower(alias :: string)] = nil
			end
		end
	elseif not oldCommand then
		table.insert(Registry.commandsList, commandObject)
	end

	commandObject.args = commandObject.args or {}
	for _, arg in commandObject.args :: any do
		if Registry.types[arg.type] then
			continue
		end
		if string.find(arg.type, "Enum.", 1, true) == 1 then
			local argType = arg.type
			local ok, enum = pcall(function()
				return (Enum :: any)[string.sub(argType, 6)]
			end)

			if not ok and string.find(argType, "s$") then
				argType = string.sub(argType, -2)
				ok, enum = pcall(function()
					return (Enum :: any)[string.sub(argType, 6)]
				end)
			end

			assert(ok, `Invalid Enum type: {arg.type}`)

			local enumType = Registry.makeEnumType(argType, enum:GetEnumItems())
			Registry.registerType(argType, enumType)
			Registry.registerType(argType .. "s", { listable = true }, enumType)
		elseif string.find(arg.type, "%S,%S") then
			local enumType = Registry.makeEnumType(arg.type, string.split(arg.type))
			Registry.registerType(arg.type, enumType)
			Registry.registerType(arg.type .. "s", { listable = true }, enumType)
		end
	end

	if RunService:IsServer() and type(commandObject.env) == "function" then
		commandObject.env = commandObject.env(_K)
	elseif not RunService:IsServer() and type(commandObject.envClient) == "function" then
		commandObject.env = commandObject.envClient(_K)
	end

	Registry.commands[lowerName] = commandObject
	Hook.commandRegistered:Fire(commandObject)
	task.defer(cacheCommandNames, commandObject)

	if commandObject.aliases then
		for _, alias in commandObject.aliases do
			alias = string.lower(alias)
			-- error if alias conflicts
			local conflict = Registry.commands[alias]
			assert(
				not conflict,
				`"{commandObject.name}" command alias "{alias}" conflicts with "{conflict and conflict.name}" command`
			)
			Registry.commands[alias] = commandObject
		end
	end
end

local aliases = {}
--- Registers a local command alias
function Registry.registerCommandAlias(_K: any, alias: string, command: string)
	print("command alias", alias, command)
	local oldAlias = aliases[alias]
	if not oldAlias and Registry.commands[string.lower(alias)] then
		warn("Command alias already exists")
		return
	end

	print("adding command alias", alias, command)

	Registry.registerCommand(_K, {
		name = alias,
		aliases = {},
		description = `Alias of "{command}"`,
		group = "Utility",
		args = {},
		runClient = function(context)
			context.Process.runCommands(context, Players.LocalPlayer, command)
		end,
	})
end

--- Registers a list of commands with the module name as the command group
function Registry.registerCommandModule(_K: any, commandModule: ModuleScript)
	local moduleResult = require(commandModule)
	local moduleList = #moduleResult > 0 and commandModule.Name
	local commandList = if moduleList then moduleResult else { moduleResult }

	for _, commandObject in commandList do
		if not commandObject.group then
			commandObject.group = commandModule.Name
		end
		Registry.registerCommand(_K, commandObject)
	end
end

return Registry


export type Dict = { [any]: any }

export type Environment = {} | (_K: any) -> {}

--- @within Registry
--- @type ArgumentFunction (value: any, from: Player) -> ...any
export type ArgumentFunction = (value: any, from: Player) -> ...any

--- @within Registry
--- @type ArgumentType {name: string, displayName: string?, listable: boolean?, filterLog: boolean?, transform: ArgumentFunction?, validate: ArgumentFunction, parse: ArgumentFunction, preParse: ArgumentFunction?, postParse: ArgumentFunction?, suggestions: ArgumentFunction?, prefixes: { [string]: string }? }
export type ArgumentType = {
	name: string?,
	displayName: string?,
	listable: boolean?,
	filterLog: boolean?,
	transform: ArgumentFunction?,
	validate: ArgumentFunction?,
	parse: ArgumentFunction?,
	postParse: ArgumentFunction?,
	suggestions: ArgumentFunction?,
	prefixes: { [string]: string }?,
}

--- @within Registry
--- @type ArgumentDefinition {type: string, name: string, description: string, optional: boolean?, permissions: { [string]: boolean }?, lowerRank: boolean?, ignoreSelf: boolean?, shouldRequest: boolean? }
export type ArgumentDefinition = {
	type: string,
	name: string,
	description: string,
	optional: boolean?,
	permissions: { [string]: boolean }?,
	-- player type options
	lowerRank: boolean?,
	ignoreSelf: boolean?,
	shouldRequest: boolean?,
}

--- @within Registry
--- @type CommandDefinition {name: string, aliases: { string }?, description: string, group: string, noLog: boolean?, args: { ArgumentDefinition }, permissions: { [string]: boolean }?, envClient: {} | () -> {}?, env: {} | () -> {}?, runClient: (...any) -> ()?, run: (...any) -> ()? }
export type CommandDefinition = {
	name: string,
	aliases: { string }?,
	description: string,
	group: string,
	noLog: boolean?,
	args: { ArgumentDefinition }?,
	permissions: { [string]: boolean }?,
	envClient: Environment?,
	env: Environment?,
	runClient: (...any) -> ()?,
	run: (...any) -> ()?,
	_listIndex: number?,
}

return {}


local defaultBudget, expireTime = 1 / 240, 0

local Defer = {}

-- Call at start of process to prevent unnecessarily waiting.
function Defer.reset(budget: number?)
	expireTime = tick() + (defaultBudget or budget)
end

function Defer.wait(budget: number?)
	if tick() >= expireTime then
		task.wait()
		Defer.reset(budget)
	end
end

return Defer


local Function = {}

function Function.debounce(delay: number, func: (...any) -> ...any)
	local thread
	return function(...)
		if typeof(thread) == "thread" and coroutine.status(thread) == "suspended" then
			task.cancel(thread)
		end
		thread = task.delay(delay, func, ...)
	end
end

function Function.throttle(delay: number, func: (...any) -> ...any)
	local thread, throttled
	local function trailing(...)
		throttled = tick() + delay
		func(...)
	end

	return function(...)
		local now = tick()

		if typeof(thread) == "thread" and coroutine.status(thread) == "suspended" then
			task.cancel(thread)
			thread = nil
		end

		if throttled and now < throttled then
			thread = task.delay(throttled - now, trailing, ...)
			return
		end

		throttled = now + delay
		func(...)
	end
end

return Function


local Humanoid = {}

local properties = {
	"Head",
	"Face",
	"Shirt",
	"Pants",
	"GraphicTShirt",
	"HeadColor",
	"LeftArmColor",
	"LeftLegColor",
	"RightArmColor",
	"RightLegColor",
	"TorsoColor",
}
local particles = { "_KFire", "_KSmoke", "_KSparkles", "_KLight", "_KParticleEffect" }

function Humanoid.applyDescription(humanoid, description)
	humanoid:ApplyDescriptionReset(description)
	humanoid:ApplyDescription(description)
end

function Humanoid.updateDescription(player, humanoid, description, property, value)
	local attribute = "_K" .. property
	local original = attribute .. "Original"
	if not player:GetAttribute(original) then
		player:SetAttribute(original, description[property])
	end
	description[property] = value or player:GetAttribute(original)
end

function Humanoid.reapplyDescription(player)
	local humanoid = player.Character and player.Character:FindFirstChildOfClass("Humanoid")
	local description = humanoid and humanoid:FindFirstChildOfClass("HumanoidDescription")
	if not description then
		return
	end

	local change
	for _, property in properties do
		local value = player:GetAttribute("_K" .. property)
		if value then
			change = true
			Humanoid.updateDescription(player, humanoid, description, property, value)
		end
	end

	if change then
		Humanoid.applyDescription(humanoid, description)
	end
end

function Humanoid.attributeDescription(player, property, value)
	local humanoid = player.Character and player.Character:FindFirstChildOfClass("Humanoid")
	local description = humanoid and humanoid:FindFirstChildOfClass("HumanoidDescription")
	if not description then
		return
	end

	Humanoid.updateDescription(player, humanoid, description, property, value)
	player:SetAttribute("_K" .. property, value)

	Humanoid.applyDescription(humanoid, description)
end

function Humanoid.resetDescription(player)
	-- reset particles
	local root = player.Character and player.Character:FindFirstChild("HumanoidRootPart")
		or player.Character.PrimaryPart
	if root then
		for _, particle in particles do
			local existing = root:FindFirstChild(particle)
			if existing then
				existing:Destroy()
			end
		end
	end

	local humanoid = player.Character and player.Character:FindFirstChildOfClass("Humanoid")
	local description = humanoid and humanoid:FindFirstChildOfClass("HumanoidDescription")

	local originalDescription = player:FindFirstChild("_KHumanoidDescription")
	if originalDescription then
		description:Destroy()
		originalDescription.Name = "HumanoidDescription"
		originalDescription.Parent = humanoid
		description = originalDescription
	end

	-- reset properties
	for _, property in properties do
		local attribute = "_K" .. property
		local original = attribute .. "Original"
		local originalValue = player:GetAttribute(original)
		if originalValue == nil then
			continue
		end
		player:SetAttribute(original, nil)
		if description then
			description[property] = originalValue
		end
	end

	-- TODO: reset scale, accessories

	if humanoid and description then
		Humanoid.applyDescription(humanoid, description)
	end
end

return Humanoid


--- @within Logger
--- @type LogType "INFO" | "WARN" | "ERROR" | "DEBUG" | "CHAT" | "COMMAND" | "PURCHASE" | "DAMAGE" | "DEATH" | "KILL" | "JOIN" | "LEAVE" | string
export type LogType =
	"INFO"
	| "WARN"
	| "ERROR"
	| "DEBUG"
	| "CHAT"
	| "COMMAND"
	| "PURCHASE"
	| "DAMAGE"
	| "DEATH"
	| "KILL"
	| "JOIN"
	| "LEAVE"
	| string

--- @within Logger
--- @type Log { string | LogType | number? | boolean? }
export type Log = { string | LogType | number? | boolean? }

--- @class Logger
local Logger = { limit = 100_000 }
Logger.__index = Logger

--- @within Logger
--- @type Logger typeof(setmetatable({} :: { logs: { Log }, debug: boolean? }, Logger))
export type Logger = typeof(setmetatable({} :: { logs: { Log }, debug: boolean? }, Logger))

-- WARN: DO NOT CHANGE THE ARRAY ORDER WITHOUT A DATASTORE MIGRATION!
local LEVELS = {
	"INFO",
	"WARN",
	"ERROR",
	"DEBUG",

	"CHAT",
	"COMMAND",
	"PURCHASE",

	"DAMAGE",
	"DEATH",
	"KILL",

	"JOIN",
	"LEAVE",
}

function Logger.new(debugEnabled: boolean?): Logger
	return setmetatable({ logs = {}, debug = debugEnabled }, Logger)
end

function Logger.sortTime(a, b)
	return a[3] < b[3]
end

function Logger:encode(level: LogType): string
	return string.char((table.find(LEVELS, level) or 1))
end

function Logger:decode(level: string): LogType
	return LEVELS[string.byte(level)] or "UNKNOWN"
end

function Logger:log(text: string, level: LogType, userId: number?, noAppend: boolean?): Log?
	if level == "DEBUG" and not self.debug then
		return
	end

	local log = {
		text,
		Logger:encode(level),
		workspace:GetServerTimeNow(),
		if userId then math.abs(userId) else nil,
		-- [5] = name: string?
		-- [6] = client: boolean? -- true = client/false = global/nil = server
	}

	if not noAppend then
		table.insert(self.logs, log)
	end

	return log
end

return Logger


local module = {}

function module.getItemsFromHashMap(hashMap: MemoryStoreHashMap): { [string]: any }
	local items = {}
	local pages = hashMap:ListItemsAsync(200)

	while true do
		for _, entry in pages:GetCurrentPage() do
			items[entry.key] = entry.value
		end
		if pages.IsFinished then
			break
		end
		pages:AdvanceToNextPageAsync()
	end

	return items
end

return module


local Property = {}

-- originalValue, overrideValue, propertyChangedConnection, internalChange
local overrideCache = {}
Property.cache = overrideCache

function Property.override(instance: Instance, property: string, value: any)
	local overrides = overrideCache[instance]
	if not overrides then
		overrides = {}
		overrideCache[instance] = overrides
		instance.Destroying:Once(function()
			for property in overrides do
				Property.reset(instance, property)
			end
			table.clear(overrides)
			overrideCache[instance] = nil
		end)
	end

	local cache = overrides[property]
	if not cache then
		cache = { instance[property] }
		overrides[property] = cache
		cache[3] = instance:GetPropertyChangedSignal(property):Connect(function()
			if not cache or cache[4] then
				return -- override change
			end
			-- update cache with external change
			cache[1] = instance[property]
			-- re-apply override value
			cache[4] = true
			instance[property] = cache[2]
			cache[4] = false
		end)
	end

	-- cache overrideValue
	cache[2] = value

	-- apply override value
	cache[4] = true
	instance[property] = value
	cache[4] = false
end

function Property.reset(instance: Instance, property: string)
	local overrides = overrideCache[instance]
	local cache = if overrides then overrides[property] else nil

	if cache then
		if typeof(cache[3]) == "RBXScriptConnection" then
			cache[3]:Disconnect()
		end

		instance[property] = cache[1]
		overrides[property] = nil

		table.clear(cache)
	end
end

return Property


--!strict
local Queue = {}
Queue.__index = Queue

export type Queue<T> = typeof(setmetatable(
	{} :: {
		_first: number,
		_last: number,
		_queue: { T },
	},
	Queue
))

function Queue.new<T>(): Queue<T>
	local self = setmetatable({
		_first = 0,
		_last = -1,
		_queue = {},
	}, Queue)

	return self
end

-- Check if the queue is empty
function Queue.IsEmpty<T>(self: Queue<T>)
	return self._first > self._last
end

-- Add a value to the queue
function Queue.Enqueue<T>(self: Queue<T>, value: T)
	local last = self._last + 1
	self._last = last
	self._queue[last] = value
end

-- Remove a value from the queue
function Queue.Dequeue<T>(self: Queue<T>): T
	if self:IsEmpty() then
		error("Cannot dequeue from empty queue")
	end

	local first = self._first
	local value = self._queue[first]
	self._queue[first] = nil
	self._first = first + 1

	return value
end

return Queue


--[[
Retries a function with exponential backoff until it succeeds or exceeds the maximum number of attempts.
@param callback: () -> any | nil -- The function to retry
@param attempts: number -- Number of retry attempts (Default: 5)
@param base: number? -- Backoff delay exponent base, adds a delay of `base ^ attempts` between attempts (Default: 2)
@param ...: any? -- Additional arguments to pass to the callback function
@return (boolean, any) -- Returns the result of the pcall callback execution
]]
local function Retry(callback: (...any) -> any | nil, attempts: number?, base: number?, ...: any?): (boolean, any)
	attempts = attempts or 5
	base = base or 2

	for attempt = 1, attempts :: number do
		local results = { pcall(callback, ...) }
		if results[1] or attempt == attempts then
			return unpack(results)
		end
		task.wait(base :: number ^ attempt)
	end

	return false
end

return Retry


local datePattern = "(%d%d%d%d)%-(%d%d)%-(%d%d)"

local FFlags = nil

local Rollout = {}
Rollout.__index = Rollout

function Rollout.new(start, finish, key, seed, curve)
	local startYear, startMonth, startDay = string.match(start, datePattern)
	local finishYear, finishMonth, finishDay = string.match(finish, datePattern)
	assert(startYear and startMonth and startDay, "Invalid start date format, use YYYY-MM-DD")
	assert(finishYear and finishMonth and finishDay, "Invalid finish date format, use YYYY-MM-DD")

	local startTime = os.time({ year = startYear, month = startMonth, day = startDay })
	local finishTime = os.time({ year = finishYear, month = finishMonth, day = finishDay })

	assert(finishTime > startTime, "finish date must be greater than start date")

	return setmetatable({
		key = key,
		seed = seed or 0,
		curve = curve or 1,
		start = startTime,
		duration = finishTime - startTime,
	}, Rollout)
end

function Rollout:test(id)
	if FFlags and FFlags[self.key] ~= nil then
		return FFlags[self.key]
	end
	local alpha = math.clamp(0, 1, math.max(1, (os.time() - self.start) / self.duration))
	return Random.new(id + self.seed):NextNumber() <= alpha ^ self.curve
end

function Rollout:setFFlagDictionary(dict)
	FFlags = dict
end

return Rollout


--!strict

local UserInputService = game:GetService("UserInputService")
local GuiService = game:GetService("GuiService")
local RunService = game:GetService("RunService")
local VRService = game:GetService("VRService")

local isEdit = false
local isServer = RunService:IsServer()
local isClient = RunService:IsClient()
local isStudio = RunService:IsStudio()

local isOnline = game.JobId ~= ""
local isRCC = isOnline and isServer

pcall(function()
	isEdit = if plugin then RunService:IsEdit() else false
end)

return table.freeze({
	IsRCC = isRCC,
	IsEdit = isEdit,

	IsTestRun = isStudio and not isEdit,

	IsServer = isServer,
	IsClient = isClient,
	IsStudio = isStudio,
	IsOnline = isOnline,

	IsTenFoot = GuiService:IsTenFootInterface(),
	IsTouch = UserInputService.TouchEnabled,
	IsVR = VRService.VREnabled,
})


--!strict

local Players = game:GetService("Players")

type PlayerSet = { [Player]: boolean }

local cache: { [PlayerSet]: RBXScriptConnection } = {}

Players.PlayerRemoving:Connect(function(player)
	for processed, connection in cache do
		if connection and connection.Connected then
			processed[player] = nil
		else
			cache[processed] = nil
			table.clear(processed)
		end
	end
end)

return function(callback): RBXScriptConnection
	local processed: PlayerSet = {}

	local function playerAdded(player)
		if processed[player] then
			return
		end
		processed[player] = true
		callback(player)
	end

	local connection = Players.PlayerAdded:Connect(playerAdded)
	cache[processed] = connection

	for _, player in Players:GetPlayers() do
		task.spawn(playerAdded, player)
	end

	return connection
end


local TeleportService = game:GetService("TeleportService")

local ATTEMPT_LIMIT = 5
local RETRY_DELAY = 1
local FLOOD_DELAY = 15

local function SafeTeleport(placeId, players, options)
	local attemptIndex = 0
	local success, result -- define pcall results outside of loop so results can be reported later on

	repeat
		success, result = pcall(function()
			return TeleportService:TeleportAsync(placeId, players, options) -- teleport the user in a protected call to prevent erroring
		end)
		attemptIndex += 1
		if not success then
			task.wait(RETRY_DELAY)
		end
	until success or attemptIndex == ATTEMPT_LIMIT -- stop trying to teleport if call was successful, or if retry limit has been reached

	if not success then
		warn(result) -- print the failure reason to output
	end

	return success, result
end

local function handleFailedTeleport(player, teleportResult, errorMessage, targetPlaceId, teleportOptions)
	if teleportResult == Enum.TeleportResult.Flooded then
		task.wait(FLOOD_DELAY)
	elseif teleportResult == Enum.TeleportResult.Failure then
		task.wait(RETRY_DELAY)
	end

	if
		teleportResult == Enum.TeleportResult.GameNotFound
		or teleportResult == Enum.TeleportResult.Unauthorized
		or string.find(errorMessage, "Roblox Studio")
	then
		return
	end

	SafeTeleport(targetPlaceId, { player }, teleportOptions)
end

TeleportService.TeleportInitFailed:Connect(handleFailedTeleport)

return SafeTeleport


local Service = setmetatable({
	Asset = game:GetService("AssetService"),
	AvatarEditor = game:GetService("AvatarEditorService"),
	Badge = game:GetService("BadgeService"),
	Collection = game:GetService("CollectionService"),
	ContextAction = game:GetService("ContextActionService"),
	Group = game:GetService("GroupService"),
	Lighting = game:GetService("Lighting"),
	Log = game:GetService("LogService"),
	Http = game:GetService("HttpService"),
	Insert = game:GetService("InsertService"),
	Marketplace = game:GetService("MarketplaceService"),
	MemoryStore = game:GetService("MemoryStoreService"),
	Messaging = game:GetService("MessagingService"),
	Players = game:GetService("Players"),
	Policy = game:GetService("PolicyService"),
	ReplicatedFirst = game:GetService("ReplicatedFirst"),
	ReplicatedStorage = game:GetService("ReplicatedStorage"),
	Run = game:GetService("RunService"),
	ServerScript = game:GetService("ServerScriptService"),
	ServerStorage = game:GetService("ServerStorage"),
	Social = game:GetService("SocialService"),
	Sound = game:GetService("SoundService"),
	StarterGui = game:GetService("StarterGui"),
	StarterPack = game:GetService("StarterPack"),
	StarterPlayer = game:GetService("StarterPlayer"),
	Teams = game:GetService("Teams"),
	Teleport = game:GetService("TeleportService"),
	Text = game:GetService("TextService"),
	TextChat = game:GetService("TextChatService"),
	Tween = game:GetService("TweenService"),
	UserInput = game:GetService("UserInputService"),
}, {
	__index = function(self, key: string)
		local service = game:GetService(key)
		self[key] = service
		return service
	end,
})

return Service


--!strict
--!optimize 2

--- @class Signal
--- A performant, doubly linked-list implementation of the Observer pattern.

type Function = (...any) -> ()

export type Connection = {
	Connected: boolean?,
	Disconnect: (self: Connection) -> (),
	_signal: Signal?,
	_next: Connection?,
	_prev: Connection?,
	_callback: Function?,
	_thread: thread?,
}

export type Signal = {
	Connect: (self: Signal, callback: Function) -> Connection,
	Once: (self: Signal, callback: Function) -> Connection,
	Fire: (self: Signal, ...any) -> (),
	Wait: (self: Signal) -> ...any,
	Destroy: (self: Signal) -> (),
	_head: Connection?,
}

local Connection = {}
Connection.__index = Connection

function Connection.new(signal: Signal, callback: Function): Connection
	return setmetatable({
		Connected = true,
		_signal = signal,
		_callback = callback,
		_next = nil,
		_prev = nil,
	}, Connection) :: any
end

function Connection:Disconnect()
	if not self.Connected then
		return
	end
	self.Connected = false
	local nextConn = self._next
	local prevConn = self._prev
	if nextConn then
		nextConn._prev = prevConn
	end
	if prevConn then
		prevConn._next = nextConn
	elseif self._signal then
		self._signal._head = nextConn
	end
	self._callback = nil
	self._signal = nil :: any
	self._prev = nil :: any
	-- _next is preserved so the firing loop can continue if disconnected during dispatch
end

local Signal = {}
Signal.__index = Signal

function Signal.new(): Signal
	return setmetatable({}, Signal) :: any
end

function Signal:Connect(callback: Function): Connection
	local connection = Connection.new(self, callback)
	if self._head then
		connection._next = self._head
		self._head._prev = connection
	end
	self._head = connection
	return connection
end

function Signal:Once(callback: Function): Connection
	local connection: Connection
	connection = self:Connect(function(...)
		if connection.Connected then
			connection:Disconnect()
		end
		callback(...)
	end)
	return connection
end

function Signal:Fire(...)
	local item = self._head
	while item do
		local nextItem = item._next
		if item.Connected then
			task.spawn(item._callback, ...)
		else -- Disconnected
			item.Connected = nil
			item._next = nil :: any
		end
		item = nextItem
	end
end

function Signal:Wait(): ...any
	local thread = coroutine.running()
	local connection: Connection
	connection = self:Connect(function(...)
		connection:Disconnect()
		connection._thread = nil
		if coroutine.status(thread) == "suspended" then
			task.spawn(thread, ...)
		end
	end)
	connection._thread = thread
	return coroutine.yield()
end

function Signal:Destroy()
	local item = self._head
	while item do
		local nextItem = item._next
		if item.Connected then
			if typeof(item._thread) == "thread" and coroutine.status(item._thread) == "suspended" then
				task.cancel(item._thread)
			end
			table.clear(item :: any)
		end
		item = nextItem
	end
	table.clear(self)
end

return Signal


--!optimize 2
--!native

local String = require("./String")
local Table = require("./Table")

local Suggest = {}

function Suggest.getNames(objects: any): { string }
	local names = {}
	local existing = {}
	for key, object in objects do
		local name = object.Name or object.name or tostring(object)
		if existing[name] then
			continue
		end
		existing[name] = true
		names[key] = name
	end
	return names, objects
end

function Suggest.query(text: string, names: { string | { string } }, instances: { any }?)
	local results, starts, contains, levenshtein = {}, {}, {}, {}
	local query = string.lower(text)

	for index, name in names do
		local suggestions, display, key = name, nil
		if type(name) == "table" then
			suggestions, display, key = unpack(name)
		end
		if type(suggestions) ~= "table" then
			suggestions = { suggestions }
		end
		local value = instances and (instances[key] or instances[index] or instances[name]) or key

		local matched = false
		for _, target in suggestions do
			if string.lower(target :: string) == query then
				table.insert(results, 1, { target, display, value or target })
				matched = true
				break
			end
		end
		if not matched then
			local length, suggestion, partial = 0, nil, nil
			for _, target in suggestions do
				local startIndex, endIndex = string.find(string.lower(target :: string), query, 1, true)
				if startIndex then
					if startIndex == 1 and endIndex > length then -- starts
						length, suggestion = endIndex, target
					elseif length == 0 and not partial then -- contains
						partial = target
					end
				end
			end
			local target = suggestion or partial
			if target then
				table.insert(if suggestion then starts else contains, { target, display, value or target })
				matched = true
			end
		end
		if not matched then
			for _, target in suggestions do
				table.insert(levenshtein, { target, display, value or target })
			end
		end
	end

	if #levenshtein > 0 then
		local lowestDistance, suggestionData = math.huge, nil
		for _, data in levenshtein do
			local distance = String.levenshteinDistance(string.lower(data[1] :: string), query)
			if distance < lowestDistance then
				lowestDistance = distance
				suggestionData = data
			end
		end
		if suggestionData then
			table.insert(contains, suggestionData)
		end
	end

	if #starts > 0 then
		Table.mergeList(results, starts)
	end
	if #contains > 0 then
		Table.mergeList(results, contains)
	end

	return results
end

function Suggest.new(setOrContainer: any): ({ string }, { any })
	local names
	local instances = {}
	local setType = typeof(setOrContainer)

	if setType == "Enum" then
		setOrContainer = setOrContainer:GetEnumItems()
		setType = typeof(setOrContainer)
	end

	if setType == "Instance" then
		names, instances = Suggest.getNames(setOrContainer:GetChildren())
	elseif setType == "table" then
		local setItem = setOrContainer[1] or next(setOrContainer)
		local setItemType = typeof(setItem)
		if
			setItemType == "Instance"
			or setItemType == "EnumItem"
			or (
				setItemType == "table"
				and setItem
				and (type(setItem.Name) == "string" or type(setItem.name) == "string")
			)
		then
			names, instances = Suggest.getNames(setOrContainer)
		elseif setItemType == "string" then
			names = setOrContainer
		elseif setItem ~= nil then
			error(`only accepts tables of instances or strings, got: {setItemType}`)
		else
			names = {}
		end
	else
		error(`only accepts a table, Enum, or Instance, got: {setType}`)
	end

	return names, instances
end

return Suggest


export type Dict = { [any]: any }
export type List = { any }

local Table = {}

function Table.concat(array: List, delimiter: string): string
	local strings = {}
	for _, item in array do
		table.insert(strings, tostring(item))
	end
	return table.concat(strings, delimiter)
end

function Table.countKeys(t: Dict): number
	local count = 0
	for _ in t do
		count += 1
	end
	return count
end

function Table.toHashMap(array: List): Dict
	local map = {}
	for _, key in array do
		map[key] = true
	end
	return map
end

function Table.map(array: List, callback: (any, any) -> any)
	for i, v in array do
		array[i] = callback(v, i)
	end
	return array
end

function Table.isArray(object: Dict)
	local index = 1
	for _ in object do
		if object[index] == nil then
			return false
		end
		index += 1
	end
	return true
end

function Table.merge(to: Dict, from: Dict): Dict
	for key, value in from do
		to[key] = value
	end
	return to
end

function Table.mergeList(to: List, from: List)
	table.move(from, 1, #from, #to + 1, to)
end

function Table.deepCompare(a: any, b: any)
	if type(a) ~= "table" or type(b) ~= "table" then
		return a == b
	end
	for k, v in a do
		if not Table.deepCompare(v, b[k]) then
			return false
		end
	end
	return true
end

function Table.deepMerge(to: Dict, from: Dict, appendArrays: boolean?): Dict
	if appendArrays and Table.isArray(from) then
		if Table.isArray(to) then
			Table.mergeList(to, from)
		else
			for _, v in from do
				table.insert(to, v)
			end
		end
	else
		for key, value in from do
			if to[key] and type(value) == "table" then
				Table.deepMerge(to[key], value, appendArrays)
			else
				to[key] = value
			end
		end
	end
	return to
end

function Table.deepCopy(original: Dict): Dict
	local copy = table.clone(original)
	for key, value in copy do
		if type(value) == "table" then
			copy[key] = Table.deepCopy(value)
		end
	end
	return copy
end

function Table.settle(t: { any })
	local shift = 1
	for i, v in t do
		t[shift], t[i] = v, nil
		shift += 1
	end
	return t
end

function Table.fastRemove(t: { any }, index: number, indexEnd: number?)
	for i = index, indexEnd or index do
		t[i] = nil
	end
	return Table.settle(t)
end

function Table.shuffle(list: { any })
	for i = #list, 2, -1 do
		local j = math.random(i)
		list[i], list[j] = list[j], list[i]
	end
	return list
end

return Table


--- A utility for managing concurrent tasks and awaiting their completion.
--- @class Tasks
local Tasks = {}
Tasks.__index = Tasks

type self = {
	_numTasks: number,
	_results: { any },
	_returnFirstResult: boolean,
	_running: thread,
	_waiting: boolean,
}

export type Tasks = typeof(setmetatable({} :: self, Tasks))

--- Creates a new Tasks instance.
function Tasks.new(): Tasks
	return setmetatable({
		_numTasks = 0,
		_results = {},
		_returnFirstResult = false,
		_running = coroutine.running(),
		_waiting = false,
	}, Tasks)
end

local function _run(self: Tasks, callback: (...any) -> ...any, ...: any)
	local result = { pcall(callback, ...) }
	self._numTasks -= 1

	if self._waiting then
		if not result[1] then
			warn("Tasks: Task failed with error:", result[2])
		end
		table.insert(self._results, result)
		if self._returnFirstResult or self._numTasks == 0 then
			self._waiting = false
			if coroutine.status(self._running) == "suspended" then
				task.spawn(self._running, if self._returnFirstResult then result else self._results)
			end
		end
	end
end

--- Adds a new task to be executed concurrently.
function Tasks.add(self: Tasks, callback: (...any) -> ...any, ...: any)
	assert(typeof(callback) == "function", "Callback must be a function")
	self._numTasks += 1
	task.defer(_run, self, callback, ...)
end

--- Waits for all tasks to complete, returning the results in a list.
function Tasks.wait(self: Tasks): { { any } }?
	if self._numTasks > 0 then
		self._waiting = true
		return coroutine.yield()
	end
	return
end

--- Waits for the first task to complete, returning the function's success and its results.
function Tasks.waitForFirst(self: Tasks): (boolean, ...any)
	self._returnFirstResult = true
	return unpack(self:wait())
end

return Tasks


local Time = {
	UNITS = table.freeze({
		{ 1 / 1000, "millisecond" },
		{ 1, "second" },
		{ 60, "minute" },
		{ 60 * 60, "hour", "hr" },
		{ 60 * 60 * 24, "day" },
		{ 60 * 60 * 24 * 7, "week", "wk" },
		{ 60 * 60 * 24 * 30, "month" },
		{ 60 * 60 * 24 * 365.25, "year", "yr" },
	}),
}
Time.__index = Time

function Time.fromUnit(unit: string): number?
	unit = string.lower(unit)
	if unit == "m" then
		return 60
	elseif unit == "mss" then
		return 1 / 1000
	end
	if #unit > 1 and string.find(unit :: string, "s$") then
		unit = string.sub(unit :: string, 1, -2)
	end
	for _, list in Time.UNITS do
		for i = 2, #list do
			if string.find(list[i], unit) == 1 then
				-- match found, return duration in seconds
				return list[1]
			end
		end
	end
	return
end

function Time.clock(seconds: number): string
	local hh = math.floor(seconds / 3600)
	local mm = math.floor((seconds % 3600) / 60)
	local ss = math.floor(seconds % 60)

	if hh > 0 then
		return string.format("%02d:%02d:%02d", hh, mm, ss)
	elseif mm > 0 then
		return string.format("%02d:%02d", mm, ss)
	else
		return string.format("%02d", ss)
	end
end

function Time.readable(seconds: number): string
	local buffer = {}
	for index = #Time.UNITS, 2, -1 do
		local definition = Time.UNITS[index]
		local unitDuration, unitName = unpack(definition)
		local unitAmount = math.floor(seconds / unitDuration)
		if unitAmount > 0 then
			seconds = seconds % unitDuration
			local plural = if unitAmount > 1 then "s" else ""
			table.insert(buffer, string.format("%s %s%s", unitAmount, unitName, plural))
		end
	end
	return table.concat(buffer, ", ")
end

return Time


local InsertService = game:GetService("InsertService")
local MarketplaceService = game:GetService("MarketplaceService")
local RunService = game:GetService("RunService")

local loadAssetTextureTypes = {
	Enum.AssetType.Face.Value,
	Enum.AssetType.Shirt.Value,
	Enum.AssetType.Pants.Value,
	Enum.AssetType.TShirt.Value,
}

local textureProperty = {
	Decal = "Texture",
	Shirt = "ShirtTemplate",
	Pants = "PantsTemplate",
	ShirtGraphic = "Graphic",
}

local textureCache = {}

local function getTexture(assetId: number, small: boolean?): string?
	local cached = textureCache[assetId]
	if cached then
		return cached
	end

	local ok, info = pcall(MarketplaceService.GetProductInfoAsync, MarketplaceService, assetId)
	if not ok then
		return
	end

	local texture = false

	if info.AssetTypeId == Enum.AssetType.Image.Value then
		texture = "rbxassetid://" .. assetId
	elseif RunService:IsServer() and table.find(loadAssetTextureTypes, info.AssetTypeId) then
		local success, model = pcall(function()
			return InsertService:LoadAsset(assetId)
		end)
		if success then
			local object = model:GetChildren()[1]
			texture = object[textureProperty[object.ClassName]]
			model:Destroy()
		end
	end

	if not texture then
		local size = if small then 150 else 420
		texture = `rbxthumb://type=Asset&id={assetId}&w={size}&h={size}`
	end

	textureCache[assetId] = texture

	return texture
end

return getTexture


local UserService = game:GetService("UserService")
local RunService = game:GetService("RunService")

local Function = require(script.Parent:WaitForChild("Function"))
local Signal = require(script.Parent:WaitForChild("Signal"))
local SafePlayerAdded = require(script.Parent:WaitForChild("SafePlayerAdded"))

type UserInfo = {
	Id: number,
	Username: string,
	DisplayName: string,
	HasVerifiedBadge: boolean,
}

local userInfoCache =
	{} :: { [number]: { Id: number, Username: string, DisplayName: string, HasVerifiedBadge: boolean } }

local REMOTE
if RunService:IsServer() then
	REMOTE = Instance.new("RemoteFunction", script)
elseif RunService:IsClient() then
	REMOTE = script:WaitForChild("RemoteFunction")
end

local loading = {}
local attempting = {}
local debounceGetUserInfos
debounceGetUserInfos = Function.debounce(0.2, function()
	for userId in loading do
		if #attempting == 200 then
			break
		end
		table.insert(attempting, userId)
	end

	local attempt = 0
	local ok, result = pcall(UserService.GetUserInfosByUserIdsAsync, UserService, attempting)
	while not ok and attempt < 5 do
		attempt += 1
		task.wait(2 ^ attempt)
		ok, result = pcall(UserService.GetUserInfosByUserIdsAsync, UserService, attempting)
	end

	if ok and result then
		for _, info in result do
			userInfoCache[info.Id] = info
			local hook = loading[info.Id]
			if hook then
				hook:Fire(info)
				hook:Destroy()
			end
			loading[info.Id] = nil
		end
		for _, userId in attempting do
			if not userInfoCache[userId] then
				local info = { Id = userId, Username = "Deleted", DisplayName = "Deleted", Deleted = true }
				userInfoCache[userId] = info
				local hook = loading[userId]
				if hook then
					hook:Fire(info)
					hook:Destroy()
				end
			end
			loading[userId] = nil
		end

		table.clear(attempting)
		if next(loading) then
			task.defer(debounceGetUserInfos)
		end
		return
	end

	warn(`getUserInfo failed: {result}`)

	task.delay(5, debounceGetUserInfos)
end)

local function getUserInfo(userId: number, checkCache: boolean?): UserInfo
	userId = tonumber(userId) :: number
	if not userId then
		return { Id = userId, Username = "Unknown", DisplayName = "Unknown", HasVerifiedBadge = false }
	end

	if checkCache or userInfoCache[userId] then
		return userInfoCache[userId]
	end

	if RunService:IsClient() then
		local info = REMOTE:InvokeServer(userId)
		if not info then
			return { Id = userId, Username = "Unknown", DisplayName = "Unknown", HasVerifiedBadge = false }
		end
		userInfoCache[userId] = info
		return info
	end

	local hook = loading[userId]
	if hook then
		return hook:Wait()
	end

	hook = Signal.new()
	loading[userId] = hook

	task.defer(debounceGetUserInfos)

	return hook:Wait()
end

if RunService:IsServer() then
	REMOTE.OnServerInvoke = function(player, userId)
		return getUserInfo(userId)
	end
end

SafePlayerAdded(function(player)
	userInfoCache[player.UserId] = {
		Id = player.UserId,
		Username = player.Name,
		DisplayName = player.DisplayName,
		HasVerifiedBadge = player.HasVerifiedBadge,
	}
end)

return getUserInfo


local Logger = require(script:WaitForChild("Logger"))

export type Log = Logger.Log
export type LogType = Logger.LogType
export type Logger = Logger.Logger

--- @class Util
local Util = {
	--- @within Util
	--- @prop Logger Logger
	Logger = Logger,

	Defer = require(script:WaitForChild("Defer")),
	Function = require(script:WaitForChild("Function")),
	getTexture = require(script:WaitForChild("getTexture")),
	getUserInfo = require(script:WaitForChild("getUserInfo")),
	Humanoid = require(script:WaitForChild("Humanoid")),
	iterPageItems = require(script:WaitForChild("iterPageItems")),
	MemoryStore = require(script:WaitForChild("MemoryStore")),
	Property = require(script:WaitForChild("Property")),
	Queue = require(script:WaitForChild("Queue")),
	Remote = require(script:WaitForChild("Remote")),
	Retry = require(script:WaitForChild("Retry")),
	Rollout = require(script:WaitForChild("Rollout")),
	RunContext = require(script:WaitForChild("RunContext")),
	SafePlayerAdded = require(script:WaitForChild("SafePlayerAdded")),
	SafeTeleport = require(script:WaitForChild("SafeTeleport")),
	Service = require(script:WaitForChild("Service")),
	Signal = require(script:WaitForChild("Signal")),
	--- @within Util
	--- @prop String String
	String = require(script:WaitForChild("String")),
	Suggest = require(script:WaitForChild("Suggest")),
	Table = require(script:WaitForChild("Table")),
	Tasks = require(script:WaitForChild("Tasks")),
	Time = require(script:WaitForChild("Time")),
}

return Util


local function pagesToTable(pages)
	local items = {}
	while true do
		table.insert(items, pages:GetCurrentPage())
		if pages.IsFinished then
			break
		end
		pages:AdvanceToNextPageAsync()
	end
	return items
end

local function iterPageItems(pages)
	local contents = pagesToTable(pages)
	local pageNum = 1
	local lastPageNum = #contents

	return coroutine.wrap(function()
		while pageNum <= lastPageNum do
			for _, item in ipairs(contents[pageNum]) do
				coroutine.yield(item, pageNum)
			end
			pageNum += 1
		end
	end)
end

return iterPageItems


local MarketplaceService = game:GetService("MarketplaceService")
local RunService = game:GetService("RunService")

local Util = require(script.Parent.Parent.Util)

local LocalPlayer = game.Players.LocalPlayer

if RunService:IsServer() then
	return false
end

local vip = Instance.new("Frame")
vip.Name = "VIP"
vip.AnchorPoint = Vector2.new(0.5, 0.5)
vip.BackgroundTransparency = 1
vip.Position = UDim2.fromScale(0.5, 0.5)
vip.Size = UDim2.fromOffset(420, 420)
vip.Visible = false
vip.ZIndex = 9e8

local imageLabel = Instance.new("ImageLabel")
imageLabel.Name = "ImageLabel"
imageLabel.BackgroundColor3 = Color3.fromRGB(18, 18, 21)
imageLabel.BackgroundTransparency = 0
imageLabel.Image = "rbxassetid://104331312960285"
imageLabel.Size = UDim2.fromScale(1, 1)

local uICorner = Instance.new("UICorner")
uICorner.Name = "UICorner"
uICorner.CornerRadius = UDim.new(0, 12)
uICorner.Parent = imageLabel

imageLabel.Parent = vip

local frame = Instance.new("Frame")
frame.Name = "Frame"
frame.BackgroundTransparency = 1
frame.Size = UDim2.fromScale(1, 1)

local uIListLayout1 = Instance.new("UIListLayout")
uIListLayout1.Name = "UIListLayout"
uIListLayout1.HorizontalAlignment = Enum.HorizontalAlignment.Center
uIListLayout1.Padding = UDim.new(0, 24)
uIListLayout1.SortOrder = Enum.SortOrder.LayoutOrder
uIListLayout1.Parent = frame

local unlock = Instance.new("Frame")
unlock.Name = "Unlock"
unlock.AutomaticSize = Enum.AutomaticSize.XY
unlock.BackgroundTransparency = 1
unlock.LayoutOrder = 1

local label = Instance.new("TextLabel")
label.Name = "Label"
label.AnchorPoint = Vector2.new(0.5, 0)
label.AutomaticSize = Enum.AutomaticSize.XY
label.BackgroundTransparency = 1
label.FontFace = Font.new("rbxassetid://12187365364", Enum.FontWeight.SemiBold, Enum.FontStyle.Normal)
label.LayoutOrder = 1
label.Position = UDim2.new(0.5, 8, 0, 64)
label.RichText = true
label.Size = UDim2.new(0.5, 0, 0, 48)
label.Text = "Unlock exclusive commands forever"
label.TextColor3 = Color3.new(1, 1, 1)
label.TextSize = 24
label.TextWrapped = true
label.TextXAlignment = Enum.TextXAlignment.Left
label.Parent = unlock

local emoji = Instance.new("TextLabel")
emoji.Name = "Emoji"
emoji.AnchorPoint = Vector2.new(1, 0)
emoji.AutomaticSize = Enum.AutomaticSize.XY
emoji.BackgroundTransparency = 1
emoji.FontFace = Font.new("rbxassetid://12187365364", Enum.FontWeight.Bold, Enum.FontStyle.Normal)
emoji.Position = UDim2.new(0.25, 0, 0, 64)
emoji.Text = "✨"
emoji.TextColor3 = Color3.new(1, 1, 1)
emoji.TextSize = 48
emoji.TextWrapped = true
emoji.Parent = unlock

local uIListLayout = Instance.new("UIListLayout")
uIListLayout.Name = "UIListLayout"
uIListLayout.FillDirection = Enum.FillDirection.Horizontal
uIListLayout.Padding = UDim.new(0, 8)
uIListLayout.SortOrder = Enum.SortOrder.LayoutOrder
uIListLayout.Parent = unlock

unlock.Parent = frame

local games = Instance.new("Frame")
games.Name = "Games"
games.AutomaticSize = Enum.AutomaticSize.XY
games.BackgroundTransparency = 1
games.LayoutOrder = 2

local label1 = Instance.new("TextLabel")
label1.Name = "Label"
label1.AnchorPoint = Vector2.new(0.5, 0)
label1.AutomaticSize = Enum.AutomaticSize.XY
label1.BackgroundTransparency = 1
label1.FontFace = Font.new("rbxassetid://12187365364", Enum.FontWeight.SemiBold, Enum.FontStyle.Normal)
label1.LayoutOrder = 1
label1.Position = UDim2.new(0.5, 8, 0, 64)
label1.RichText = true
label1.Size = UDim2.new(0.5, 0, 0, 48)
label1.Text = "Use in over 10,000\nRoblox experiences"
label1.TextColor3 = Color3.new(1, 1, 1)
label1.TextSize = 24
label1.TextWrapped = true
label1.TextXAlignment = Enum.TextXAlignment.Left
label1.Parent = games

local uIListLayout2 = Instance.new("UIListLayout")
uIListLayout2.Name = "UIListLayout"
uIListLayout2.FillDirection = Enum.FillDirection.Horizontal
uIListLayout2.Padding = UDim.new(0, 8)
uIListLayout2.SortOrder = Enum.SortOrder.LayoutOrder
uIListLayout2.Parent = games

local imageLabel1 = Instance.new("ImageLabel")
imageLabel1.Name = "ImageLabel"
imageLabel1.BackgroundTransparency = 1
imageLabel1.Image = "rbxasset://textures/loading/robloxTilt.png"
imageLabel1.Size = UDim2.fromOffset(48, 48)
imageLabel1.Parent = games

games.Parent = frame

local panel = Instance.new("Frame")
panel.Name = "Panel"
panel.AutomaticSize = Enum.AutomaticSize.XY
panel.BackgroundTransparency = 1
panel.LayoutOrder = 5

local label2 = Instance.new("TextLabel")
label2.Name = "Label"
label2.AnchorPoint = Vector2.new(0.5, 0)
label2.AutomaticSize = Enum.AutomaticSize.XY
label2.BackgroundTransparency = 1
label2.FontFace = Font.new("rbxassetid://12187365364", Enum.FontWeight.Bold, Enum.FontStyle.Normal)
label2.LayoutOrder = 1
label2.Position = UDim2.new(0.5, 8, 0, 64)
label2.RichText = true
label2.Size = UDim2.new(0.5, 0, 0, 32)
label2.Text = "30% goes to supporting free and open-source projects"
label2.TextColor3 = Color3.fromRGB(184, 184, 184)
label2.TextSize = 16
label2.TextWrapped = true
label2.TextXAlignment = Enum.TextXAlignment.Left
label2.Parent = panel

local emoji1 = Instance.new("TextLabel")
emoji1.Name = "Emoji"
emoji1.AnchorPoint = Vector2.new(1, 0)
emoji1.AutomaticSize = Enum.AutomaticSize.XY
emoji1.BackgroundTransparency = 1
emoji1.FontFace = Font.new("rbxassetid://12187365364", Enum.FontWeight.Bold, Enum.FontStyle.Normal)
emoji1.Position = UDim2.new(0.25, 0, 0, 64)
emoji1.Text = "🌎"
emoji1.TextColor3 = Color3.new(1, 1, 1)
emoji1.TextSize = 24
emoji1.TextWrapped = true
emoji1.Parent = panel

local uIListLayout3 = Instance.new("UIListLayout")
uIListLayout3.Name = "UIListLayout"
uIListLayout3.FillDirection = Enum.FillDirection.Horizontal
uIListLayout3.Padding = UDim.new(0, 8)
uIListLayout3.SortOrder = Enum.SortOrder.LayoutOrder
uIListLayout3.VerticalAlignment = Enum.VerticalAlignment.Center
uIListLayout3.Parent = panel

panel.Parent = frame

local creator = Instance.new("Frame")
creator.Name = "Creator"
creator.AutomaticSize = Enum.AutomaticSize.XY
creator.BackgroundTransparency = 1
creator.LayoutOrder = 4

local label3 = Instance.new("TextLabel")
label3.Name = "Label"
label3.AnchorPoint = Vector2.new(0.5, 0)
label3.AutomaticSize = Enum.AutomaticSize.XY
label3.BackgroundTransparency = 1
label3.FontFace = Font.new("rbxassetid://12187365364", Enum.FontWeight.Bold, Enum.FontStyle.Normal)
label3.LayoutOrder = 1
label3.Position = UDim2.new(0.5, 8, 0, 64)
label3.RichText = true
label3.Size = UDim2.new(0.5, 0, 0, 32)
label3.Text = "40% of your purchase goes to the creator of this experience"
label3.TextColor3 = Color3.fromRGB(184, 184, 184)
label3.TextSize = 16
label3.TextWrapped = true
label3.TextXAlignment = Enum.TextXAlignment.Left
label3.Parent = creator

local emoji2 = Instance.new("TextLabel")
emoji2.Name = "Emoji"
emoji2.AnchorPoint = Vector2.new(1, 0)
emoji2.AutomaticSize = Enum.AutomaticSize.XY
emoji2.BackgroundTransparency = 1
emoji2.FontFace = Font.new("rbxassetid://12187365364", Enum.FontWeight.Bold, Enum.FontStyle.Normal)
emoji2.Position = UDim2.new(0.25, 0, 0, 64)
emoji2.Text = "❤"
emoji2.TextColor3 = Color3.new(1, 1, 1)
emoji2.TextSize = 24
emoji2.TextWrapped = true
emoji2.Parent = creator

local uIListLayout4 = Instance.new("UIListLayout")
uIListLayout4.Name = "UIListLayout"
uIListLayout4.FillDirection = Enum.FillDirection.Horizontal
uIListLayout4.Padding = UDim.new(0, 8)
uIListLayout4.SortOrder = Enum.SortOrder.LayoutOrder
uIListLayout4.VerticalAlignment = Enum.VerticalAlignment.Center
uIListLayout4.Parent = creator

creator.Parent = frame

local titleFrame = Instance.new("Frame")
titleFrame.Name = "Title"
titleFrame.BackgroundTransparency = 1
titleFrame.Size = UDim2.new(1, 0, 0, 40)

local title = Instance.new("TextLabel")
title.Name = "Header"
title.BackgroundTransparency = 1
title.FontFace = Font.new("rbxasset://fonts/families/RobotoMono.json", Enum.FontWeight.Bold, Enum.FontStyle.Normal)
title.Size = UDim2.fromScale(1, 1)
title.Text = ""
title.TextColor3 = Color3.new(1, 1, 1)
title.TextSize = 24
title.TextTransparency = 0.25
title.TextWrapped = true
title.RichText = true
title.Visible = true
title.Parent = titleFrame

local titleBG = Instance.new("Frame")
titleBG.Name = "BG"
titleBG.BackgroundColor3 = Color3.fromRGB(255, 0, 0)
titleBG.BackgroundTransparency = 0.5
titleBG.Size = UDim2.fromScale(1, 2)
titleBG.ZIndex = 0

local uICorner1 = Instance.new("UICorner")
uICorner1.Name = "UICorner"
uICorner1.CornerRadius = UDim.new(0, 12)
uICorner1.Parent = titleBG

local uIGradient = Instance.new("UIGradient")
uIGradient.Name = "UIGradient"
uIGradient.Rotation = 90
uIGradient.Transparency = NumberSequence.new({
	NumberSequenceKeypoint.new(0, 0),
	NumberSequenceKeypoint.new(1, 1),
})
uIGradient.Parent = titleBG

titleBG.Parent = titleFrame

titleFrame.Parent = frame

local buyFrame = Instance.new("Frame")
buyFrame.Name = "BuyFrame"
buyFrame.BackgroundTransparency = 1
buyFrame.LayoutOrder = 3
buyFrame.Size = UDim2.new(1, 0, 0, 64)

local favorite = Instance.new("TextButton", buyFrame)
favorite.Name = "Favorite"
favorite.BackgroundTransparency = 1
favorite.FontFace = Font.new("rbxasset://fonts/families/SourceSansPro.json")
favorite.Size = UDim2.fromOffset(84, 48)
favorite.Text = "⭐"
favorite.TextColor3 = Color3.new()
favorite.TextSize = 48
favorite.TextXAlignment = Enum.TextXAlignment.Right

local buy = Instance.new("TextButton")
buy.Name = "Buy"
buy.BackgroundColor3 = Color3.fromRGB(51, 95, 255)
buy.FontFace = Font.new("rbxasset://fonts/families/SourceSansPro.json")
buy.LayoutOrder = 3
buy.Size = UDim2.fromOffset(114, 64)
buy.Text = "399"
buy.TextColor3 = Color3.new(1, 1, 1)
buy.TextSize = 12
buy.TextTransparency = 1

local uICorner2 = Instance.new("UICorner")
uICorner2.Name = "UICorner"
uICorner2.CornerRadius = UDim.new(0, 12)
uICorner2.Parent = buy

local robuxIcon = Instance.new("ImageLabel")
robuxIcon.Name = "RobuxIcon"
robuxIcon.BackgroundTransparency = 1
robuxIcon.Image = "rbxasset://textures/ui/common/robux@2x.png"
robuxIcon.Size = UDim2.fromScale(1, 1)
robuxIcon.SizeConstraint = Enum.SizeConstraint.RelativeYY
robuxIcon.ZIndex = 2
robuxIcon.Parent = buy

local uIPadding = Instance.new("UIPadding")
uIPadding.Name = "UIPadding"
uIPadding.PaddingBottom = UDim.new(0, 16)
uIPadding.PaddingLeft = UDim.new(0, 16)
uIPadding.PaddingRight = UDim.new(0, 16)
uIPadding.PaddingTop = UDim.new(0, 16)
uIPadding.Parent = buy

local price = Instance.new("TextLabel")
price.Name = "Price"
price.BackgroundTransparency = 1
price.FontFace = Font.new("rbxassetid://12187365364", Enum.FontWeight.Bold, Enum.FontStyle.Normal)
price.Position = UDim2.fromOffset(40, 0)
price.Size = UDim2.fromOffset(42, 32)
price.Text = "399"
price.TextColor3 = Color3.new(1, 1, 1)
price.TextSize = 24
price.Parent = buy

buy.Parent = buyFrame

local originalPrice = Instance.new("Frame")
originalPrice.Name = "OriginalPrice"
originalPrice.BackgroundTransparency = 1
originalPrice.Size = UDim2.fromOffset(84, 32)
originalPrice.Visible = false

local originalPriceLabel = Instance.new("TextLabel")
originalPriceLabel.Name = "Label"
originalPriceLabel.BackgroundTransparency = 1
originalPriceLabel.FontFace = Font.new("rbxassetid://12187365364", Enum.FontWeight.Medium, Enum.FontStyle.Normal)
originalPriceLabel.Position = UDim2.fromOffset(40, 0)
originalPriceLabel.Size = UDim2.fromOffset(42, 32)
originalPriceLabel.Text = "499"
originalPriceLabel.TextColor3 = Color3.fromRGB(184, 184, 184)
originalPriceLabel.TextSize = 24

local line = Instance.new("Frame")
line.Name = "Line"
line.AnchorPoint = Vector2.new(0.5, 0.5)
line.BackgroundColor3 = Color3.fromRGB(184, 184, 184)
line.BorderColor3 = Color3.new()
line.BorderSizePixel = 0
line.Position = UDim2.fromScale(0.5, 0.5)
line.Size = UDim2.fromOffset(48, 2)
line.ZIndex = 2
line.Parent = originalPriceLabel

originalPriceLabel.Parent = originalPrice

local robuxIcon1 = Instance.new("ImageLabel")
robuxIcon1.Name = "RobuxIcon"
robuxIcon1.BackgroundTransparency = 1
robuxIcon1.Image = "rbxasset://textures/ui/common/robux@2x.png"
robuxIcon1.ImageColor3 = Color3.fromRGB(184, 184, 184)
robuxIcon1.Size = UDim2.fromScale(1, 1)
robuxIcon1.SizeConstraint = Enum.SizeConstraint.RelativeYY
robuxIcon1.ZIndex = 2
robuxIcon1.Parent = originalPrice

originalPrice.Parent = buyFrame

local uIListLayout5 = Instance.new("UIListLayout")
uIListLayout5.Name = "UIListLayout"
uIListLayout5.FillDirection = Enum.FillDirection.Horizontal
uIListLayout5.HorizontalAlignment = Enum.HorizontalAlignment.Center
uIListLayout5.Padding = UDim.new(0, 16)
uIListLayout5.SortOrder = Enum.SortOrder.LayoutOrder
uIListLayout5.VerticalAlignment = Enum.VerticalAlignment.Center
uIListLayout5.Parent = buyFrame

local view = Instance.new("TextButton")
view.Name = "View"
view.AnchorPoint = Vector2.new(0, 0.5)
view.AutomaticSize = Enum.AutomaticSize.X
view.BackgroundColor3 = Color3.fromRGB(64, 64, 64)
view.FontFace = Font.new("rbxassetid://12187365364", Enum.FontWeight.Bold, Enum.FontStyle.Normal)
view.LayoutOrder = 3
view.Position = UDim2.new(1, 32, 0.5, 0)
view.Size = UDim2.fromOffset(0, 48)
view.Text = "View"
view.TextColor3 = Color3.new(1, 1, 1)
view.TextSize = 24

local uiCorner3 = Instance.new("UICorner")
uiCorner3.Name = "UICorner"
uiCorner3.CornerRadius = UDim.new(0, 12)
uiCorner3.Parent = view

local uIPadding1 = Instance.new("UIPadding")
uIPadding1.Name = "UIPadding"
uIPadding1.PaddingBottom = UDim.new(0, 16)
uIPadding1.PaddingLeft = UDim.new(0, 16)
uIPadding1.PaddingRight = UDim.new(0, 16)
uIPadding1.PaddingTop = UDim.new(0, 16)
uIPadding1.Parent = view

view.Parent = buyFrame

buyFrame.Parent = frame

frame.Parent = vip

local close = Instance.new("ImageButton")
close.Name = "Close"
close.AnchorPoint = Vector2.new(1, 0)
close.BackgroundTransparency = 1
close.Image = "rbxasset://textures/ui/ScreenshotHud/Close.png"
close.Position = UDim2.new(1, -4, 0, 4)
close.Size = UDim2.fromOffset(32, 32)
close.Parent = vip

local originalProductId = "110019084552349"
local saleProductId = "115444443724463"

local productId = originalProductId

local function promptBulkPurchase(id: number | string)
	Util.Remote.PromptBulkPurchase:Fire(tonumber(id) :: any)
end

local function promptPurchaseVIP()
	promptBulkPurchase(productId)
end

task.spawn(function()
	while not shared._K_INTERFACE do
		task.wait()
	end
	shared._K_INTERFACE.PromptBulkPurchase = promptBulkPurchase
	shared._K_INTERFACE.PromptPurchaseVIP = promptPurchaseVIP

	shared._K_INTERFACE.UI.new "Tooltip" {
		Parent = favorite,
		Text = "Add to Favorites",
	}
end)

local function updateLabelPrice(label: TextLabel, id: number)
	local ok, result = pcall(MarketplaceService.GetProductInfoAsync, MarketplaceService, id, Enum.InfoType.Asset)
	if ok then
		label.Text = result.PriceInRobux
	end
end

local function setSalePrice(onSale: boolean?)
	productId = if onSale then saleProductId else originalProductId
	favorite.Visible = not onSale
	originalPrice.Visible = onSale
	title.Visible = true
	buy.Price.Text = if onSale then "479" else "799"
	task.spawn(updateLabelPrice, buy.Price, productId)
end

task.spawn(updateLabelPrice, originalPriceLabel, productId)
task.spawn(function()
	local initialJoin = LocalPlayer:GetAttribute("_KInitialJoin")
	while not initialJoin do
		task.wait()
		initialJoin = LocalPlayer:GetAttribute("_KInitialJoin")
	end

	local elapsed = workspace:GetServerTimeNow() - initialJoin

	local PERIOD = 10 * 60

	if elapsed < PERIOD then
		setSalePrice(false)
		while elapsed < PERIOD do
			task.wait(0.5)
			elapsed = (workspace:GetServerTimeNow() - initialJoin)
			local duration = PERIOD - elapsed
			title.Text = `40% OFF in {string.format(
				"%02d:%02d",
				duration // 60,
				duration % 60
			)}`
		end
		title.Text = `<font color="#0f0" transparency="0">40% OFF</font>`
	end

	setSalePrice(true)
end)

buy.Activated:Connect(promptPurchaseVIP)
favorite.Activated:Connect(function()
	pcall(function()
		shared._K_INTERFACE.Service.AvatarEditor:PromptSetFavorite(productId, 1, true)
	end)
end)
view.Activated:Connect(function()
	shared._K_INTERFACE.client.commandsWindowVisible(true)
	shared._K_INTERFACE.client.dashboard.Commands._input._input.Text = "role=vip"
	vip.ZIndex = shared._K_INTERFACE.Registry.commands.cmds.env.window._instance.ZIndex - 1
end)

close.Activated:Connect(function()
	vip.Visible = false
end)

MarketplaceService.PromptBulkPurchaseFinished:Connect(function(player, status, results)
	if player ~= LocalPlayer or (results.Items[1].id ~= productId and results.Items[1].id ~= saleProductId) then
		return
	end
	shared._K_INTERFACE.Remote.VIPCheck:Fire(tonumber(results.Items[1].id))
	if status == Enum.MarketplaceBulkPurchasePromptStatus.Completed then
		shared._K_INTERFACE.client.VIPNotification = function() end
		vip.Visible = false
		task.wait(4)
		pcall(function()
			game:GetService("AvatarEditorService"):PromptSetFavorite(17873329124, 1, true)
		end)
	end
end)

-- automatic scaling for small screens
local scale = Instance.new("UIScale", vip)
vip:GetPropertyChangedSignal("Parent"):Once(function()
	local parent = vip.Parent
	local function updateScale()
		scale.Scale = math.min(1, parent.AbsoluteSize.Y / 420)
	end
	updateScale()
	parent:GetPropertyChangedSignal("AbsoluteSize"):Connect(updateScale)
end)

return vip


return {
	chatCommands = {
		115444443724463, -- 40% off chat commands
		110019084552349, -- chat commands
		130880915606069, -- elite
		84349662672781, -- epic
		129439770798699, -- hero
		132161234381156, -- legend
		89859349616824, -- mythic
		102980653148583, -- eternal
	},
	chatCommandsLower = {
		88978146769822, -- tribute
		115403290087767, -- patron
	},

	donations = {
		{ name = "Tip", color = "#bf0", id = 5391355, assetId = 129760409821083 },
		{ name = "Gift", color = "#8f0", id = 5391356, assetId = 77680874300094 },
		{ name = "Donor", color = "#0f0", id = 933980650, assetId = 112699242861456 },
		{ name = "Tribute", color = "#0f8", id = 933582848, assetId = 88978146769822 },
		{ name = "Patron", color = "#0ff", id = 938480383, assetId = 115403290087767 },
		{ name = "Elite", color = "#80f", id = 939618404, assetId = 130880915606069 },
		{ name = "Epic", color = "#f0f", id = 936146448, assetId = 84349662672781 },
		{ name = "Hero", color = "#f08", id = 941076536, assetId = 129439770798699 },
		{ name = "Legend", color = "#f00", id = 941162509, assetId = 132161234381156 },
		{ name = "Mythic", color = "#000", id = 941006661, assetId = 89859349616824 },
		{ name = "Eternal", color = "#000", id = 983235767, assetId = 102980653148583 },
	},

	scriptedUGC = {
		{
			tip = "🕊 Light Wings\nProvides the following special effects in games using Kohl's Admin:"
				.. "\n - Flying Animations 💫\n - Neon Material ✨\n\nAnimator: @LuhHorsie\nModeler: @HyperbolicHydra\nScripter: @Scripth",
			id = 133292294488871,
			equip = "rbxassetid://89119211625300",
			name = "Light Wings",
			level = 3,
		},
		{
			tip = "👑 Light Crown\nProvides the following special effects in games using Kohl's Admin:"
				.. "\n - Fire Effect 🔥\n - Neon Material ✨\n - Spinning 💫",
			id = 18966788838,
			equip = "rbxassetid://18966762965",
			name = "Light Crown",
			level = 2,
		},
		{
			tip = "🕊 Dark Wings\nProvides the following special effects in games using Kohl's Admin:"
				.. "\n - Flying Animations 💫\n - Neon Material ✨\n\nAnimator: @LuhHorsie\nModeler: @HyperbolicHydra\nScripter: @Scripth",
			id = 110848154960799,
			equip = "rbxassetid://89119211625300",
			name = "Dark Wings",
			level = 3,
		},
		{
			tip = "👑 Dark Crown\nProvides the following special effects in games using Kohl's Admin:"
				.. "\n - Fire Effect 🔥\n - Neon Material ✨\n - Spinning 💫",
			id = 106645613603989,
			equip = "rbxassetid://18966762965",
			name = "Dark Crown",
			level = 2,
		},
		{
			tip = "🕊 Gold Wings\nProvides the following special effects in games using Kohl's Admin:"
				.. "\n - Flying Animations 💫\n - Neon Material ✨\n\nAnimator: @LuhHorsie\nModeler: @HyperbolicHydra\nScripter: @Scripth",
			id = 92807314389236,
			equip = "rbxassetid://89119211625300",
			name = "Gold Wings",
			level = 3,
		},
		{
			tip = "👑 Gold Crown\nProvides the following special effects in games using Kohl's Admin:"
				.. "\n - Fire Effect 🔥\n - Neon Material ✨\n - Spinning 💫",
			id = 99317257118206,
			equip = "rbxassetid://18966762965",
			name = "Gold Crown",
			level = 2,
		},
	},
}


local CollectionService = game:GetService("CollectionService")
local RunService = game:GetService("RunService")

local Package = script.Parent.Parent.Parent

local Flux = require(Package:WaitForChild("Flux"))

local Oklab = Flux.Color.Oklab

local function sortKeypoints(a, b)
	return a.Time < b.Time
end

local Title = {
	definition = {
		vip = {
			color = Color3.fromHex("#fc0"),
			assetId = { 110019084552349, 115444443724463 },
			gamepass = 5411126,
		},
		patron = {
			color = Color3.fromHex("#0ff"),
			assetId = 115403290087767,
			gamepass = 938480383,
		},
		elite = {
			color = Color3.fromHex("#0bf"),
			assetId = 130880915606069,
			gamepass = 939618404,
		},
		epic = {
			assetId = 84349662672781,
			gamepass = 936146448,
			rotation = 0,
			getValue = function(elapsed: number)
				elapsed /= 2
				local keypoints = {}
				for i = 0, 2 do
					local step = i / 2
					local oscillate = math.abs(((elapsed + step) % 2) - 1) * 1 / 7
					local color = Oklab.toRGB(Oklab.fromHCL(Vector3.new(3 / 7 + oscillate, 0.4, 1)))
					table.insert(keypoints, ColorSequenceKeypoint.new(step, color))
				end
				return ColorSequence.new(keypoints)
			end,
			update = function(object, value)
				object.Title.UIGradient.Color = value
			end,
		},
		hero = {
			assetId = 129439770798699,
			gamepass = 941076536,
			getValue = function(elapsed: number)
				elapsed /= 2
				local keypoints = {}
				for i = 0, 2 do
					local step = i / 2
					local oscillate = math.abs(((elapsed + step) % 2) - 1) * 1 / 7
					local color = Oklab.toRGB(Oklab.fromHCL(Vector3.new(5 / 7 + oscillate, 0.4, 1)))
					table.insert(keypoints, ColorSequenceKeypoint.new(step, color))
				end
				return ColorSequence.new(keypoints)
			end,
			update = function(object, value)
				object.Title.UIGradient.Color = value
			end,
		},
		legend = {
			assetId = 132161234381156,
			gamepass = 941162509,
			getValue = function(elapsed: number)
				elapsed /= 2
				local keypoints = {}
				for i = 0, 2 do
					local step = i / 2
					local oscillate = math.abs(((elapsed + step) % 2) - 1) * 1 / 7
					local color = Oklab.toRGB(Oklab.fromHCL(Vector3.new(oscillate, 0.4, 1)))
					table.insert(keypoints, ColorSequenceKeypoint.new(step, color))
				end
				return ColorSequence.new(keypoints)
			end,
			update = function(object, value)
				object.Title.UIGradient.Color = value
			end,
		},
		mythic = {
			assetId = 89859349616824,
			gamepass = 941006661,
			getValue = function(elapsed: number)
				return ColorSequence.new(Oklab.toRGB(Oklab.fromHCL(Vector3.new((time() / 10) % 1, 0.4, 1))))
			end,
			update = function(object, value)
				object.Title.UIGradient.Color = value
			end,
		},
		eternal = {
			assetId = 102980653148583,
			gamepass = 983235767,
			rotation = 0,
			getValue = function(elapsed: number)
				elapsed /= 4
				local steps = 6
				local keypoints = {}
				local offset = elapsed % 1

				local colorAtStart = Oklab.toRGB(Oklab.fromHCL(Vector3.new(-offset, 0.4, 1)))
				table.insert(keypoints, ColorSequenceKeypoint.new(0, colorAtStart))

				local colorAtEnd = Oklab.toRGB(Oklab.fromHCL(Vector3.new((1 - offset), 0.4, 1)))
				table.insert(keypoints, ColorSequenceKeypoint.new(1, colorAtEnd))

				for i = 0, steps do
					local step = i / steps
					local color = Oklab.toRGB(Oklab.fromHCL(Vector3.new(step, 0.4, 1)))
					local timeOffset = 1 - ((step + elapsed) % 1)
					table.insert(keypoints, ColorSequenceKeypoint.new(timeOffset, color))
				end
				table.sort(keypoints, sortKeypoints)
				return ColorSequence.new(keypoints)
			end,
			update = function(object, value)
				object.Title.UIGradient.Color = value
			end,
		},
	},
}

for _, definition in Title.definition do
	definition.ownerCache = {}
end

shared._KVIPTitle = Title

local gui = Instance.new("BillboardGui")
gui.Name = "_K_Title"
gui.MaxDistance = 256
gui.Size = UDim2.new(4, 0, 1, 0)
gui.StudsOffset = Vector3.new(0, 3, 0)
gui.ResetOnSpawn = false
gui.LightInfluence = 0

local title = Instance.new("TextLabel")
title.AutoLocalize = false
title.Name = "Title"
title.BackgroundTransparency = 1
title.Size = UDim2.fromScale(1, 1)
title.Font = Enum.Font.FredokaOne
title.Text = "LEGEND"
title.TextScaled = true
title.TextColor3 = Color3.new(1, 1, 1)
title.Parent = gui

local stroke = Instance.new("UIStroke")
stroke.Thickness = 1
stroke.Parent = title

local gradient = Instance.new("UIGradient")
gradient.Parent = title

function Title.Method(character: Model, title: string?, color: Color3?, topColor: Color3?, font: Enum.Font?)
	local titleGui = character:FindFirstChild("_K_Title")
	if titleGui then
		titleGui:Destroy()
	end
	if title == nil then
		return
	end

	local titleRef = string.lower(title)
	local titleDefinition = Title.definition[titleRef]
	if titleDefinition then
		color = color or titleDefinition.color
		topColor = topColor or titleDefinition.topColor
	end

	titleGui = gui:Clone()
	titleGui.Parent = character
	titleGui.Adornee = character:FindFirstChild("Head")
	titleGui:AddTag("_K" .. titleRef)

	titleGui.Title.Text = title
	titleGui.Title.UIGradient.Color = ColorSequence.new(topColor or Color3.new(1, 1, 1), color or Color3.new(1, 1, 1))
	titleGui.Title.UIGradient.Rotation = titleDefinition and titleDefinition.rotation or 90
	if font then
		titleGui.Title.Font = font
	end
end

if RunService:IsClient() then
	local titleCache = {}
	for titleRef, definition in Title.definition do
		if not definition.update then
			continue
		end
		local tag = "_K" .. titleRef
		local cache = { getValue = definition.getValue, update = definition.update, objects = {} }
		titleCache[tag] = cache
		CollectionService:GetInstanceAddedSignal(tag):Connect(function(obj)
			cache.objects[obj] = true
		end)
		CollectionService:GetInstanceRemovedSignal(tag):Connect(function(obj)
			cache.objects[obj] = nil
		end)
	end

	local start = os.clock()
	RunService.Heartbeat:Connect(function()
		local elapsed = os.clock() - start
		for tag, cache in titleCache do
			if not next(cache.objects) then
				continue
			end
			local value = cache.getValue(elapsed)
			for object in cache.objects do
				if object.AbsoluteSize.Y <= 2 then
					continue
				end
				cache.update(object, value)
			end
		end
	end)
end

return Title


local TweenService = game:GetService("TweenService")

local Animate = {
	timeScale = 1,
}

function Animate.getBoneFromPose(rig: Instance, pose: Pose): Motor6D?
	if not pose.Parent then
		return
	end
	local poseParentName = pose.Parent.Name
	for i, v in rig:GetDescendants() do
		if v:IsA("Bone") and v.Name == pose.Name and v.Parent.Name == poseParentName then
			return v
		end
	end
	return
end

local tracks = {}

function Animate.play(rig: MeshPart, sequence: KeyframeSequence, loop: boolean?)
	if typeof(tracks[rig]) == "thread" and coroutine.status(tracks[rig]) == "suspended" then
		task.cancel(tracks[rig])
	end
	tracks[rig] = coroutine.running()

	local frames = sequence:GetKeyframes()
	table.sort(frames, function(a, b)
		return a.Time < b.Time
	end)

	local bones = {}
	local blend = true
	repeat
		for i, keyframe in frames do
			local duration = 0

			if frames[i - 1] then
				duration = (keyframe.Time - frames[i - 1].Time) / Animate.timeScale
			elseif blend then
				blend = false
				duration = 0.5
			end

			for _, pose: Pose in keyframe:GetDescendants() do
				local bone = Animate.getBoneFromPose(rig, pose)
				if not bone then
					continue
				end
				bones[bone] = true

				local originalCFrame = bone:GetAttribute("OriginalCFrame")
				if not originalCFrame then
					originalCFrame = bone.CFrame
					bone:SetAttribute("OriginalCFrame", originalCFrame)
				end

				local originalSize = rig:GetAttribute("OriginalSize")
				if not originalSize then
					originalSize = rig.Size
					rig:SetAttribute("OriginalCFrame", originalSize)
				end

				local scale = rig.Size / originalSize

				if bone and pose.CFrame ~= CFrame.identity then
					local EasingStyle, EasingDirection =
						Enum.EasingStyle[pose.EasingStyle.Name], Enum.EasingDirection[pose.EasingDirection.Name]

					TweenService:Create(bone, TweenInfo.new(duration, EasingStyle, EasingDirection), {
						CFrame = CFrame.new(originalCFrame.Position * scale.X)
							* originalCFrame.Rotation
							* pose.CFrame.Rotation,
					}):Play()
				end
			end
			while rig.Anchored do
				task.wait()
			end
			task.wait(duration)
		end
	until not loop

	for bone in bones do
		TweenService:Create(bone, TweenInfo.new(0.5), { CFrame = bone:GetAttribute("OriginalCFrame") }):Play()
	end
end

return Animate


local part = script.Parent

local Animate = require(script:WaitForChild("Animate"))

local Animations = script:WaitForChild("Animations")
local fall = Animations:WaitForChild("fall")
local idle = Animations:WaitForChild("idle")
local idle2 = Animations:WaitForChild("idle2")

local humanoid = part.Parent.Parent:FindFirstChildOfClass("Humanoid")
while not humanoid do
	humanoid = part.Parent.Parent:FindFirstChildOfClass("Humanoid")
	task.wait()
end

local animFalling, lastY
local randomIdle = true
task.delay(math.random(15, 30), function()
	randomIdle = false
end)

game:GetService("RunService").PreSimulation:Connect(function(step)
	local falling = humanoid.FloorMaterial == Enum.Material.Air
		and humanoid.RootPart
		and humanoid.RootPart.Anchored == false
	local currentY = part.Position.Y
	local speed = (if lastY then currentY - lastY else part.Velocity.Y) / step
	lastY = currentY

	if falling then
		if speed < 0 then -- falling
			Animate.timeScale = 0.5 + (200 + math.max(-200, speed)) / 400
		else -- flying
			Animate.timeScale = 1 + math.min(200, speed) / 100
		end
		if animFalling then
			return
		end
		animFalling = true
		Animate.play(part, fall, true)
		animFalling = false
	else
		if animFalling then
			animFalling = false
			Animate.timeScale = 0.5
			Animate.play(part, idle, true)
		elseif not randomIdle then
			randomIdle = true
			task.delay(math.random(15, 30), function()
				randomIdle = false
			end)
			Animate.timeScale = 0.5
			Animate.play(part, idle2)
			Animate.play(part, idle, true)
		end
	end
end)

Animate.timeScale = 0.5
Animate.play(part, idle, true)


local part = Instance.new("Part")
part.Anchored = true
part.CanCollide = false
part.CanQuery = false
part.CanTouch = false
part.Locked = true
part.Transparency = 1

local boneL1 = Instance.new("Bone", part)
boneL1.Name = "Bone_l"
boneL1.CFrame = CFrame.new(-0.576, 0, 0) * CFrame.Angles(0, 0, math.rad(76.663))

local boneL2 = Instance.new("Bone", boneL1)
boneL2.Name = "Bone.001_l"
boneL2.CFrame = CFrame.new(0, 2.153, 0) * CFrame.Angles(0, 0, math.rad(-34.32))

local boneL3 = Instance.new("Bone", boneL2)
boneL3.Name = "Bone.002_l"
boneL3.CFrame = CFrame.new(0, 1.67, 0) * CFrame.Angles(0, 0, math.rad(24.665))

local boneR1 = Instance.new("Bone", part)
boneR1.Name = "Bone_r"
boneR1.CFrame = CFrame.new(0.576, 0, 0) * CFrame.Angles(0, 0, math.rad(-76.663))

local boneR2 = Instance.new("Bone", boneR1)
boneR2.Name = "Bone.001_r"
boneR2.CFrame = CFrame.new(0, 2.153, 0) * CFrame.Angles(0, 0, math.rad(34.32))

local boneR3 = Instance.new("Bone", boneR2)
boneR3.Name = "Bone.002_r"
boneR3.CFrame = CFrame.new(0, 1.67, 0) * CFrame.Angles(0, 0, math.rad(-24.665))

return part


local CollectionService = game:GetService("CollectionService")
local InsertService = game:GetService("InsertService")
local RunService = game:GetService("RunService")

local Package = script.Parent.Parent.Parent.Parent

local Flux = require(Package:WaitForChild("Flux"))
local Util = require(Package.Shared:WaitForChild("Util"))

local Oklab = Flux.Color.Oklab

local hat = Instance.new("Accessory")
hat.AccessoryType = Enum.AccessoryType.Back
hat.Name = "_KAlphaWings"
hat:AddTag("_KAlphaWings")

local handle = Instance.new("Part", hat)
handle.Name = "Handle"
handle.Transparency = 1
handle.CanCollide = false
handle.CanQuery = false
handle.CanTouch = false
handle.Massless = true
handle.Locked = true

local attachment = Instance.new("Attachment", handle)
attachment.CFrame = CFrame.new(0, -0.5, 0)
attachment.Name = "BodyBackAttachment"

task.spawn(function()
	local ok, result = Util.Retry(function()
		return InsertService:CreateMeshPartAsync(
			"rbxassetid://135389107063517",
			Enum.CollisionFidelity.Box,
			Enum.RenderFidelity.Precise
		)
	end)
	if not ok then
		warn(result)
		return
	end
	local wingMesh = result
	wingMesh:AddTag("_KAlphaWings")
	wingMesh.Parent = hat
	wingMesh.Name = "Wings"
	wingMesh.Color = Color3.fromRGB(170, 170, 170)
	wingMesh.CastShadow = false
	wingMesh.CanCollide = false
	wingMesh.CanQuery = false
	wingMesh.CanTouch = false
	wingMesh.Massless = true
	wingMesh.Locked = true
	wingMesh.Material = Enum.Material.Neon
	wingMesh:SetAttribute("OriginalSize", wingMesh.Size)

	local weld = Instance.new("Weld", wingMesh)
	weld.Part0 = handle
	weld.Part1 = wingMesh

	local Bones = require(script:WaitForChild("Bones"))
	for _, bone in Bones:GetChildren() do
		bone:SetAttribute("OriginalCFrame", bone.CFrame)
		bone.Parent = wingMesh
	end

	script:WaitForChild("Animate"):Clone().Parent = wingMesh
end)

if RunService:IsClient() then
	local rainbow = {}
	RunService.Heartbeat:Connect(function()
		if next(rainbow) then
			local color = Oklab.toRGB(Oklab.fromHCL(Vector3.new((time() / 10) % 1, 0.4, 1)))
			for wings in rainbow do
				if wings and wings.Parent and wings:FindFirstChild("Handle") then
					wings.Wings.Color = color
				else
					rainbow[wings] = nil
				end
			end
		end
	end)

	CollectionService:GetInstanceAddedSignal("_KAlphaWingsRainbow"):Connect(function(obj)
		rainbow[obj] = true
	end)
	CollectionService:GetInstanceRemovedSignal("_KAlphaWingsRainbow"):Connect(function(obj)
		rainbow[obj] = nil
	end)
	for _, obj in CollectionService:GetTagged("_KAlphaWingsRainbow") do
		rainbow[obj] = true
	end
end

local COLORS = {
	dark = Color3.new(),
	gold = Color3.fromRGB(170, 125, 0),
}

return {
	Name = hat.Name,
	MatchId = "rbxassetid://89119211625300",
	Method = function(character: Model, accessory: Accessory)
		local color
		if accessory then
			if typeof(accessory) == "Instance" then
				local part = accessory:FindFirstChild("Handle")
				if part:IsA("MeshPart") then
					if string.find(part.TextureID, "74391200400192") then
						color = COLORS.dark
					elseif string.find(part.TextureID, "115319077247149") then
						color = COLORS.gold
					end
				end
				accessory:Destroy()
			elseif typeof(accessory) == "string" then
				for key, value in COLORS do
					if string.find(string.lower(accessory :: string), key :: string, 1, true) then
						color = value
						break
					end
				end
			end
		end

		local existing = character:FindFirstChild(hat.Name)
		if existing then
			existing:Destroy()
		end

		local clone = hat:Clone()

		if clone:FindFirstChild("Wings") then
			if color then
				clone:SetAttribute("_KWingsColor", color)
				clone.Wings.Color = color
			end

			clone.Wings.Animate.Enabled = true
		end

		clone.Parent = character

		return clone
	end,
}


local CollectionService = game:GetService("CollectionService")
local InsertService = game:GetService("InsertService")
local RunService = game:GetService("RunService")

local Package = script.Parent.Parent.Parent.Parent

local Flux = require(Package:WaitForChild("Flux"))
local Util = require(Package.Shared:WaitForChild("Util"))

local Oklab = Flux.Color.Oklab

local hat = Instance.new("Accessory")
hat.AccessoryType = Enum.AccessoryType.Hat
hat.Name = "SuperCrown"
hat.AttachmentPoint = CFrame.new(0, -1, 0)
hat:AddTag("_KSuperCrown")

task.spawn(function()
	local ok, result = Util.Retry(function()
		return InsertService:CreateMeshPartAsync(
			"rbxassetid://18966762965",
			Enum.CollisionFidelity.Box,
			Enum.RenderFidelity.Automatic
		)
	end)
	if not ok then
		warn(result)
		return
	end
	local handle = result
	handle.Parent = hat
	handle.Name = "Handle"
	handle.Color = Color3.new(1, 1, 1)
	handle.CastShadow = false
	handle.CanCollide = false
	handle.CanQuery = false
	handle.CanTouch = false
	handle.Massless = true
	handle.Locked = true
	handle.Material = Enum.Material.Neon
	-- handle.Size = Vector3.new(1.2, 0.2, 1.2)

	local attachment = Instance.new("Attachment", handle)
	attachment.Name = "HatAttachment"
	attachment.CFrame = CFrame.new(0, -1.5, 0)

	local fire = Instance.new("Fire", handle)
	fire.Color = Color3.new(0, 0, 0)
	fire.SecondaryColor = Color3.new(0, 0, 0)
	fire.Heat = 4
	fire.Size = 2
end)

if RunService:IsClient() then
	-- handle crown animations
	local crowns, rainbowCrowns = {}, {}
	RunService.Heartbeat:Connect(function()
		if next(crowns) then
			local t = time()
			local tpi = t % (math.pi * 2)
			local cframe = CFrame.new(0, math.sin(tpi) / 8, 0) * CFrame.Angles(0, tpi, 0)
			for crown in crowns do
				if crown and crown.Parent and crown:FindFirstChild("Handle") then
					if crown.Handle:FindFirstChild("AccessoryWeld") then
						crown.Handle.AccessoryWeld.C1 = cframe
					end
				else
					crowns[crown] = nil
				end
			end
			-- rainbow crowns
			local color = Oklab.toRGB(Oklab.fromHCL(Vector3.new((t / 10) % 1, 0.4, 1)))
			for crown in rainbowCrowns do
				if crown and crown.Parent and crown:FindFirstChild("Handle") then
					crown.Handle.Color = color
					local fire = crown.Handle:FindFirstChild("Fire")
					if fire then
						fire.SecondaryColor = color
					end
				else
					crowns[crown] = nil
				end
			end
		end
	end)
	for tag, cache in { _KSuperCrown = crowns, _KSuperCrownRainbow = rainbowCrowns } do
		CollectionService:GetInstanceAddedSignal(tag):Connect(function(obj)
			cache[obj] = true
		end)
		CollectionService:GetInstanceRemovedSignal(tag):Connect(function(obj)
			cache[obj] = nil
		end)
		for _, obj in CollectionService:GetTagged(tag) do
			cache[obj] = true
		end
	end
end

local COLORS = {
	dark = { Color3.new(), Color3.new(1, 1, 1) },
	gold = { Color3.fromRGB(170, 125, 0), Color3.new() },
}

return {
	Name = hat.Name,
	MatchId = "rbxassetid://18966762965",
	Method = function(character: Model, accessory: Accessory)
		local colors
		if accessory then
			if typeof(accessory) == "Instance" then
				local part = accessory:FindFirstChild("Handle")
				if part:IsA("MeshPart") then
					if string.find(part.TextureID, "74391200400192") then
						colors = COLORS.dark
					elseif string.find(part.TextureID, "115319077247149") then
						colors = COLORS.gold
					end
				end
				accessory:Destroy()
			elseif typeof(accessory) == "string" then
				for key, value in COLORS do
					if string.find(string.lower(accessory :: string), key :: string, 1, true) then
						colors = value
						break
					end
				end
			end
		end

		local existing = character:FindFirstChild(hat.Name)
		if existing then
			existing:Destroy()
		end

		local clone = hat:Clone()

		if clone:FindFirstChild("Handle") then
			if colors then
				local primary, secondary = unpack(colors)
				clone.Handle.Color = primary
				clone.Handle.Fire.Color = secondary
				clone.Handle.Fire.SecondaryColor = secondary
			end

			clone:SetAttribute("_KCrownColor", clone.Handle.Color)
			clone:SetAttribute("_KCrownFireColor", clone.Handle.Fire.Color)
		end

		clone.Parent = character

		return clone
	end,
}