-- safe utilily commands for all players

return {
	{
		name = "version",
		description = "Displays the version of Kohl's Admin in this game.",
		optionalPrefix = true,
		runClient = function(context)
			context._K.Notify({ From = "_K", Text = `<sc>version</sc>:\t<b>{context._K.VERSION}</b>` })
		end,
	},
	{
		name = "prefix",
		aliases = { "hotkeys" },
		description = "Displays the prefix and hotkeys.",
		optionalPrefix = true,
		runClient = function(context)
			local commandBarKey = context._K.client.hotkeys.commandBar
			local commandBarKeyRaw = context._K.UI.UserInputService:GetStringForKeyCode(commandBarKey.key._value)
			local commandBarKeyText =
				`{commandBarKey.mods.Shift and "Shift+" or ""}{commandBarKey.mods.Alt and "Alt+" or ""}{commandBarKey.key._value.Name}`

			local dashboardKey = context._K.client.hotkeys.dashboard
			local dashboardKeyRaw = context._K.UI.UserInputService:GetStringForKeyCode(dashboardKey.key._value)
			local dashboardKeyText =
				`{dashboardKey.mods.Shift and "Shift+" or ""}{dashboardKey.mods.Alt and "Alt+" or ""}{dashboardKey.key._value.Name}`

			local prefix = context._K.UI.raw(context._K.client.settings.prefix)[1]
			local prefixKeyText = string.byte(prefix)
			for i, keyCode in Enum.KeyCode:GetEnumItems() do
				if keyCode.Value == prefixKeyText then
					prefixKeyText = keyCode.Name
					break
				end
			end

			context._K.Notify({
				From = "_K",
				Text = table.concat({
					`<font family="{context._K.UI.Theme.FontMono._value.Family}"><b>Prefix</b>     <font color="#0f0">{prefixKeyText} {prefix}</font>`,
					`<b>Commands</b>   <font color="#0f0">{commandBarKeyText} {commandBarKeyRaw}</font>`,
					`<b>Dashboard</b>  <font color="#0f0">{dashboardKeyText} {dashboardKeyRaw}</font></font>`,
				}, "\n"),
			})
		end,
	},
	{
		name = "cmdbar2",
		aliases = { "cmdbarfloating", "cmdbarwindow" },
		description = "Shows a floating command bar window.",
		optionalPrefix = true,
		envClient = function(_K)
			local UI = _K.UI
			local env = {}

			local input = UI.new "Input" {
				ClearTextButton = true,
				Fill = true,
				FontFace = UI.Theme.FontMono,
				FontSize = UI.Theme.FontSize,
				Placeholder = ";command",
				Value = "",
			}

			input._input:GetPropertyChangedSignal("Text"):Connect(function()
				_K.client.updateCommandInput(input)
			end)

			local function executeCommand()
				local trimmed = _K.Util.String.trimEnd(input._input.Text)
				task.spawn(_K.Process.runCommands, _K, _K.LocalPlayer.UserId, trimmed)
			end

			input._input.FocusLost:Connect(function(enterPressed)
				if enterPressed then
					executeCommand()
				end
			end)

			env.window = UI.new "Window" {
				Parent = UI.LayerTopInset,
				Icon = "rbxassetid://71961243872230",
				Name = "Cmdbar2",
				Title = "Command Bar",
				AutomaticSize = Enum.AutomaticSize.Y,
				Size = UDim2.fromOffset(420, 0),

				UI.new "UIListLayout" {
					SortOrder = Enum.SortOrder.LayoutOrder,
					Padding = UI.Theme.Padding,
				},

				input,
				UI.new "Button" {
					Label = "Run Command",
					Activated = executeCommand,
				},
			}

			return env
		end,

		runClient = function(context)
			context.env.window._instance.Visible = not context.env.window._instance.Visible
		end,
	},
	{
		name = "about",
		aliases = { "credit", "credits", "info" },
		description = "Shows the about tab in a separate window.",
		optionalPrefix = true,
		envClient = function(_K)
			local env = {}
			task.defer(function()
				while not _K.client do
					task.wait()
				end
				local window = _K.UI.new "Window" {
					Parent = _K.UI.LayerTopInset,
					Icon = "rbxassetid://71961243872230",
					Title = "About",
				}
				_K.UI.edit(window, {
					[_K.UI.Event] = {
						Visible = function()
							if not window._instance.Visible and #_K.client.dashboard then
								_K.UI.edit(_K.client.dashboard.Tabs, {
									_K.client.dashboard.About,
								})
							end
						end,
					},
				})
				env.window = window
			end)
			return env
		end,

		runClient = function(context)
			context._K.client.dashboard.About._instance.Parent = context.env.window._content
			context._K.client.dashboard.About._instance.Visible = true
			context.env.window._instance.Visible = true
		end,
	},
	{
		name = "commands",
		aliases = { "cmds" },
		description = "Shows the commands in a separate window.",
		optionalPrefix = true,
		envClient = function(_K)
			local env = {}
			task.defer(function()
				while not _K.client do
					task.wait()
				end
				local window = _K.UI.new "Window" {
					Parent = _K.UI.LayerTopInset,
					Icon = "rbxassetid://71961243872230",
					Title = "Commands",
				}
				_K.UI.edit(window, {
					[_K.UI.Event] = {
						Visible = function()
							if not window._instance.Visible and #_K.client.dashboard then
								_K.UI.edit(_K.client.dashboard.Tabs, {
									_K.client.dashboard.Commands,
								})
							end
						end,
					},
				})
				env.window = window
			end)
			return env
		end,

		runClient = function(context)
			context._K.UI.edit(context.env.window, {
				Visible = true,
				context._K.client.dashboard.Commands,
			})
			context._K.client.dashboard.Commands._instance.Visible = true
		end,
	},
	{
		name = "commandbar",
		aliases = { "cmdbar" },
		description = "Shows the command bar.",
		optionalPrefix = true,
		runClient = function(context)
			if context._K.Data.settings.commandBarRank == false then
				return "Command bar has been disabled."
			end
			if context._K.Data.settings.commandBarRank > context.fromRank then
				local role = context._K.Auth.getRoleFromRank(context._K.Data.settings.dashboardRank)
				return `Command bar has been restricted to {role.name}+.`
			end
			task.defer(context._K.client.CommandBar.show)
			return
		end,
	},
	{
		name = "dashboard",
		description = "Shows the admin dashboard.",
		optionalPrefix = true,
		runClient = function(context)
			if context._K.Data.settings.dashboardRank == false then
				return `Dashboard has been disabled.`
			end
			if context._K.Data.settings.dashboardRank > context.fromRank then
				local role = context._K.Auth.getRoleFromRank(context._K.Data.settings.dashboardRank)
				return `Dashboard has been restricted to {role.name}+.`
			end
			if not context._K.client.dashboard.Window.Visible then
				context._K.client.hotkeys.dashboard.callback()
			end
			return
		end,
	},
	{
		name = "donate",
		aliases = { "market", "support" },
		description = "Shows the market tab in a separate window.",
		optionalPrefix = true,
		envClient = function(_K)
			local env = {}
			task.defer(function()
				while not _K.client do
					task.wait()
				end
				local window = _K.UI.new "Window" {
					Parent = _K.UI.LayerTopInset,
					Icon = "rbxassetid://71961243872230",
					Title = "Market",
				}
				_K.UI.edit(window, {
					[_K.UI.Event] = {
						Visible = function()
							if not window._instance.Visible and #_K.client.dashboard then
								_K.UI.edit(_K.client.dashboard.Tabs, {
									_K.client.dashboard.Market,
								})
							end
						end,
					},
				})
				env.window = window
			end)
			return env
		end,

		runClient = function(context)
			context._K.client.dashboard.Market._instance.Parent = context.env.window._content
			context._K.client.dashboard.Market._instance.Visible = true
			context.env.window._instance.Visible = true
		end,
	},
	{
		name = "log",
		aliases = {
			"logs",
			"debuglogs",
			"errorlogs",
			"chatlogs",
			"commandlogs",
			"joinlogs",
			"damagelogs",
			"killlogs",
			"purchaselogs",
			"clogs",
			"cmdlogs",
			"dmglogs",
			"buylogs",
		},
		description = "Shows the logs in a separate window.",
		optionalPrefix = true,
		envClient = function(_K)
			local env = {
				aliasMap = {
					clogs = "chatlogs",
					cmdlogs = "commandlogs",
					dmglogs = "damagelogs",
					buylogs = "purchaselogs",
				},
				typeMap = {
					DEBUG = { "DEBUG", "INFO", "WARN", "ERROR" },
					ERROR = { "WARN", "ERROR" },
					CHAT = { "CHAT" },
					COMMAND = { "COMMAND" },
					JOIN = { "JOIN", "LEAVE" },
					KILL = { "KILL", "DEATH", "DAMAGE" },
					DAMAGE = { "KILL", "DEATH", "DAMAGE" },
					PURCHASE = { "PURCHASE" },
				},
				originalFilter = nil,
			}

			task.defer(function()
				while not _K.client do
					task.wait()
				end
				local window = _K.UI.new "Window" {
					Parent = _K.UI.LayerTopInset,
					Icon = "rbxassetid://71961243872230",
					Title = "Logs",
				}
				_K.UI.edit(window, {
					[_K.UI.Event] = {
						Visible = function()
							if not window._instance.Visible then
								_K.UI.edit(_K.client.dashboard.Tabs, {
									_K.client.dashboard.Logs,
								})
								if env.originalFilter then
									for logType, value in env.originalFilter do
										_K.client.dashboard.Logs.logTypeFilter[logType](value)
									end
									env.originalFilter = nil
								end
							end
						end,
					},
				})
				env.window = window
			end)
			return env
		end,

		runClient = function(context)
			context._K.UI.edit(context.env.window, {
				Visible = true,
				context._K.client.dashboard.Logs,
			})
			context._K.client.dashboard.Logs._instance.Visible = true

			if not context.env.originalFilter then
				context.env.originalFilter = {}
				for logType, state in context._K.client.dashboard.Logs.logTypeFilter do
					context.env.originalFilter[logType] = state._value
				end
			end

			if context.alias == "log" or context.alias == "logs" then
				for _, state in context._K.client.dashboard.Logs.logTypeFilter do
					state(true)
				end
			else
				for _, state in context._K.client.dashboard.Logs.logTypeFilter do
					state(false)
				end
				local alias = string.lower(context.alias :: string)
				local match = context.env.aliasMap[alias] or alias
				match = string.upper(string.match(match, "(%w+)logs?$") :: string)
				for _, logType in context.env.typeMap[match] do
					context._K.client.dashboard.Logs.logTypeFilter[logType](true)
				end
			end
		end,
	},
	{
		name = "settings",
		aliases = { "set" },
		description = "Shows the settings in a separate window.",
		optionalPrefix = true,
		envClient = function(_K)
			local env = {}
			task.defer(function()
				while not _K.client do
					task.wait()
				end
				local window = _K.UI.new "Window" {
					Parent = _K.UI.LayerTopInset,
					Icon = "rbxassetid://71961243872230",
					Title = "Settings",
				}
				_K.UI.edit(window, {
					[_K.UI.Event] = {
						Visible = function()
							if not window._instance.Visible and #_K.client.dashboard then
								_K.UI.edit(_K.client.dashboard.Tabs, {
									_K.client.dashboard.Settings,
								})
							end
						end,
					},
				})
				env.window = window
			end)
			return env
		end,

		runClient = function(context)
			context._K.UI.edit(context.env.window, {
				Visible = true,
				context._K.client.dashboard.Settings,
			})
			context._K.client.dashboard.Settings._instance.Visible = true
		end,
	},
	{
		name = "emote",
		aliases = { "animation", "anim", "stopemote", "stopanim" },
		description = "Plays an animation on one or more player(s).",
		args = {
			{
				type = "players",
				name = "Player(s)",
				description = "The player(s) to play the animation on.",
			},
			{
				type = "emote",
				name = "Animation",
				description = "The emote or animation id to play.",
				optional = true,
			},
			{
				type = "boolean",
				name = "Looping",
				description = "If the animation should loop.",
				optional = true,
			},
			{
				type = "number",
				name = "Speed",
				description = "The speed of the animation.",
				optional = true,
			},
		},
		envClient = function(_K)
			local UI = _K.UI
			local env = {
				playing = nil,
				looped = nil,
				speed = nil,
				window = nil,
				preview = nil,
			}

			local emoteId = UI.state()
			local ownsEmote = UI.state(false)

			env.emoteId = emoteId

			task.defer(function()
				while not _K.client do
					task.wait()
				end

				local cameraAngleX = 0
				local cameraAngleY = 180
				local cameraZ = 9
				local worldModel = Instance.new("WorldModel")
				local previewCamera = UI.new "Camera" {}

				local cameraStartCFrame = CFrame.new(0, 3, 0)
					* CFrame.fromOrientation(math.rad(cameraAngleX), math.rad(cameraAngleY), 0)
					* CFrame.new(0, 0, cameraZ)

				local resetCameraTween = game:GetService("TweenService")
					:Create(previewCamera, TweenInfo.new(1, Enum.EasingStyle.Cubic), {
						CFrame = cameraStartCFrame,
					})

				local resetCamera = _K.Util.Function.debounce(4, function()
					resetCameraTween:Play()
				end)

				local function updatePreviewCamera()
					previewCamera.CFrame = CFrame.new(0, 3, 0)
						* CFrame.fromOrientation(math.rad(cameraAngleX), math.rad(cameraAngleY), 0)
						* CFrame.new(0, 0, cameraZ)
					if not (previewCamera.CFrame):FuzzyEq(cameraStartCFrame) then
						resetCamera()
					end
				end

				updatePreviewCamera()

				local dragStart, startAngleX, startAngleY

				local previewId
				env.preview = UI.new "Frame" {
					Name = "Preview",
					Parent = UI.LayerTopInset,
					BackgroundColor3 = Color3.new(1, 1, 1),
					Size = UDim2.new(0, 300, 0, 360),
					Position = UDim2.new(0.5, -150, 0.5, -180),
					Visible = false,
					ZIndex = 9e8,

					UI.new "UICorner" {
						CornerRadius = UDim.new(0, 12),
					},
					UI.new "UIGradient" {
						Color = ColorSequence.new({
							ColorSequenceKeypoint.new(0, Color3.fromRGB(18, 18, 21)),
							ColorSequenceKeypoint.new(1 / 3, Color3.fromRGB(36, 36, 42)),
							ColorSequenceKeypoint.new(2 / 3, Color3.fromRGB(36, 36, 42)),
							ColorSequenceKeypoint.new(1, Color3.fromRGB(18, 18, 21)),
						}),
						Rotation = 90,
					},

					UI.new "ViewportFrame" {
						BackgroundTransparency = 1,
						Size = UDim2.new(1, 0, 1, 0),
						Ambient = Color3.fromRGB(127, 127, 127),
						LightColor = Color3.fromRGB(200, 200, 200),
						LightDirection = Vector3.new(0, -1, 1),
						CurrentCamera = previewCamera,

						previewCamera,
						worldModel,
						MouseWheelBackward = function()
							cameraZ = math.clamp(cameraZ + 1, 3, 15)
							updatePreviewCamera()
						end,
						MouseWheelForward = function()
							cameraZ = math.clamp(cameraZ - 1, 3, 15)
							updatePreviewCamera()
						end,
						TouchPinch = function(_touchPositions, scale, _velocity, state)
							if state == Enum.UserInputState.Change then
								cameraZ = math.clamp(scale * 15, 3, 15)
								updatePreviewCamera()
							end
						end,
						InputBegan = function(input)
							if
								input.UserInputType == Enum.UserInputType.MouseButton1
								or input.UserInputType == Enum.UserInputType.Touch
							then
								dragStart = input.Position
								startAngleX = cameraAngleX
								startAngleY = cameraAngleY
							end
						end,
						InputChanged = function(input, gameProcessed)
							if gameProcessed or not dragStart then
								return
							end
							if
								input.UserInputType == Enum.UserInputType.MouseMovement
								or input.UserInputType == Enum.UserInputType.Touch
							then
								local delta = dragStart - input.Position
								cameraAngleX = (startAngleX + delta.Y) % 360
								cameraAngleY = (startAngleY + delta.X) % 360
								updatePreviewCamera()
							end
						end,
						InputEnded = function(input)
							if
								input.UserInputType == Enum.UserInputType.MouseButton1
								or input.UserInputType == Enum.UserInputType.Touch
							then
								dragStart = nil
							end
						end,
					},

					UI.new "ImageButton" {
						Name = "Close",
						AnchorPoint = Vector2.new(1, 0),
						BackgroundTransparency = 1,
						Image = "rbxasset://textures/ui/ScreenshotHud/Close.png",
						Position = UDim2.new(1, -4, 0, 4),
						Size = UDim2.fromOffset(32, 32),
						Activated = function()
							env.preview.Visible = false
						end,
					},

					UI.new "TextLabel" {
						Name = "Title",
						BackgroundTransparency = 1,
						FontFace = Font.new("rbxassetid://12187365364", Enum.FontWeight.Bold, Enum.FontStyle.Normal),
						Position = UDim2.fromOffset(0, 16),
						Size = UDim2.new(1, 0, 0, 28),
						Text = "Loading",
						TextColor3 = Color3.new(1, 1, 1),
						TextSize = 28,

						UI.new "TextLabel" {
							Name = "Category",
							AutomaticSize = Enum.AutomaticSize.Y,
							BackgroundTransparency = 1,
							FontFace = Font.new("rbxassetid://12187365364"),
							Position = UDim2.fromScale(0, 1),
							Size = UDim2.fromScale(1, 0),
							Text = "Emote",
							TextColor3 = Color3.new(1, 1, 1),
							TextSize = 16,
						},
					},

					UI.new "TextButton" {
						Name = "Unlock",
						AnchorPoint = Vector2.new(0.5, 1),
						AutomaticSize = Enum.AutomaticSize.XY,
						BackgroundColor3 = Color3.fromRGB(51, 95, 255),
						FontFace = Font.new("rbxassetid://12187365364", Enum.FontWeight.Bold, Enum.FontStyle.Normal),
						Position = UDim2.new(0.5, 0, 1, -38),
						Text = "UNLOCK",
						TextColor3 = Color3.new(1, 1, 1),
						TextSize = 28,

						UI.new "UICorner" {
							Name = "UICorner",
							CornerRadius = UDim.new(0, 12),
						},

						UI.new "UIPadding" {
							Name = "UIPadding",
							PaddingBottom = UDim.new(0, 12),
							PaddingLeft = UDim.new(0, 34),
							PaddingRight = UDim.new(0, 34),
							PaddingTop = UDim.new(0, 12),
						},

						Activated = function()
							if previewId then
								_K.PromptBulkPurchase(previewId)
							end
						end,
					},

					UI.new "TextLabel" {
						Name = "Tip",
						AnchorPoint = Vector2.new(0, 1),
						AutomaticSize = Enum.AutomaticSize.Y,
						BackgroundTransparency = 1,
						FontFace = Font.new("rbxassetid://12187365364", Enum.FontWeight.Light, Enum.FontStyle.Normal),
						Position = UDim2.new(0, 0, 1, -16),
						RichText = true,
						Size = UDim2.fromScale(1, 0),
						Text = "Use in <b>every</b> Roblox game",
						TextColor3 = Color3.new(1, 1, 1),
						TextSize = 16,
					},
				}

				task.spawn(function()
					local ok, model = _K.Util.Retry(function()
						return _K.Service.Players:CreateHumanoidModelFromUserId(math.max(1, _K.LocalPlayer.UserId))
					end)
					if not ok then
						model = _K.Flux.new "Model" {
							_K.Flux.new "Humanoid" {
								_K.Flux.new "Animator" {},
							},
						}
					end
					model:PivotTo(CFrame.new(0, 3, 0))

					task.spawn(function()
						local animate = model:WaitForChild("Animate", 5)
						if typeof(animate) == "Instance" and animate:IsA("LocalScript") then
							animate:Destroy()
						end
						model.Parent = worldModel
					end)

					local humanoid = model:WaitForChild("Humanoid")
					local animator = humanoid:WaitForChild("Animator")

					local anim: Animation = Instance.new("Animation")
					local animTrack: AnimationTrack

					local function updatePreview(id)
						if animTrack then
							animTrack:Stop()
							animTrack:Destroy()
						end
						previewId = id
						if id then
							cameraAngleX = 0
							cameraAngleY = 180
							cameraZ = 9
							updatePreviewCamera()
							local item = _K.Data.emoteItemFromId[id]
							env.preview.Title.Text = item.name
							anim.AnimationId = "rbxassetid://" .. item.animationId
							animTrack = animator:LoadAnimation(anim)
							animTrack.Looped = true
							animTrack:Play()
						end
					end
					env.updatePreview = updatePreview
				end)

				local looped = UI.new "Switch" {
					[UI.Hook] = {
						Value = function(loop: boolean)
							if env.playing then
								env.playing.Looped = loop
								if not env.playing.IsPlaying then
									env.playing:Play(0.1, 99, env.speed.Value._value * 2)
								end
							end
						end,
					},
				}
				local speed = UI.new "Slider" {
					[UI.Hook] = {
						Value = function(alpha: number)
							if env.playing then
								env.playing:AdjustSpeed(alpha * 2)
							end
						end,
					},
				}
				env.looped = looped
				env.speed = speed

				local window = UI.new "Window" {
					Parent = UI.LayerTopInset,
					Title = "Emote",
					Resizable = false,
					Size = UDim2.new(0, 256, 0, 32),
					Position = UDim2.new(1, -256 - 6, 0.5, -16),
					UI.new "List" {
						UI.new "ListItem" { Text = "Looped", looped },
						UI.new "ListItem" { Text = "Speed", speed },
					},
				}

				window._content.Size = UDim2.fromScale(1, 0)
				window._content.AutomaticSize = Enum.AutomaticSize.Y
				window._instance.Frame.AutomaticSize = Enum.AutomaticSize.Y
				window._instance.AutomaticSize = Enum.AutomaticSize.Y

				local titleBarHeight = UI.compute(function()
					return UI.Theme.FontSize() + UI.Theme.Padding().Offset * 2
				end)
				local buttonSize = function()
					return UDim2.fromOffset(titleBarHeight(), titleBarHeight())
				end
				local buttonVisible = function()
					return emoteId() ~= nil
				end

				UI.new "Button" {
					LayoutOrder = 3,
					Parent = window._instance.Frame.TitleBar,
					Label = "🔓",
					Size = buttonSize,
					Visible = function()
						return buttonVisible() and not ownsEmote()
					end,
					Activated = function()
						if env.updatePreview and env.preview then
							env.updatePreview(emoteId._value)
							env.preview.Visible = true
						end
					end,
				}

				UI.new "Button" {
					LayoutOrder = 3,
					Parent = window._instance.Frame.TitleBar,
					Label = "⭐",
					Size = buttonSize,
					Visible = buttonVisible,
					Activated = function()
						pcall(function()
							_K.Service.AvatarEditor:PromptSetFavorite(emoteId._value, 1, true)
						end)
					end,
				}

				env.window = window
			end)

			local function playEmote(animation: number, loop: boolean?, speed: number?)
				if env.playing then
					env.playing:Stop()
					env.playing:Destroy()
					env.playing = nil
					emoteId(nil)
					ownsEmote(false)
				end

				if not animation then
					return
				end

				local player = _K.LocalPlayer
				local character = player.Character or player.CharacterAdded:Wait()
				local humanoid = character:WaitForChild("Humanoid")
				local animator = humanoid:WaitForChild("Animator")
				local anim: Animation = _K.Flux.new "Animation" { AnimationId = `rbxassetid://{animation}` }

				local playing = animator:LoadAnimation(anim)
				playing.Priority = Enum.AnimationPriority.Action
				anim.Parent = playing

				if loop ~= nil then
					playing.Looped = loop
					env.looped.Value(playing.Looped)
				else
					task.spawn(function()
						local start = tick()
						repeat
							task.wait()
						until playing.Looped == true or tick() - start > 1
						env.looped.Value(true)
					end)
				end

				env.speed.Value(math.clamp(speed or 1, 0, 2) / 2)

				if not playing.Looped then
					playing.Stopped:Once(function()
						playing:Destroy()
					end)
				end

				local id = _K.Data.animationToEmoteId[animation]
				local emoteItem = emoteId and _K.Data.emoteItemFromId[id]
				env.window.Title(if emoteItem then emoteItem.name else "Emote")
				emoteId(id)
				ownsEmote(false)
				task.spawn(function()
					local ok, result = _K.Util.Retry(function()
						return _K.Service.Marketplace:PlayerOwnsAssetAsync(_K.LocalPlayer, id)
					end)
					if ok then
						ownsEmote(result)
					end
				end)

				playing:Play(0.1, 99, speed)
				env.playing = playing
				env.window._instance.Visible = true

				return playing
			end

			env.playEmote = playEmote
			_K.Remote.Emote:Connect(playEmote)

			return env
		end,
		env = function(_K)
			return {
				remote = _K.Remote.Emote,
			}
		end,
		run = function(context, players, animation: number, loop: boolean?, speed: number?)
			if
				context._K.Auth.hasPermission(context.from, "targetOthers") ~= true
				and not context._K.Auth.hasPermission(context.from, "admin")
			then
				local ok, owned = context._K.Util.Retry(function()
					return context._K.Service.Marketplace:PlayerOwnsAssetAsync(context._K.LocalPlayer, animation)
				end)
				if ok and not owned then
					local id = tostring(animation)
					if not context._K.VIP.allowedBulkPurchaseIds[animation] then
						id = "115444443724463" -- chat commands
					end
					local items = { { Type = Enum.MarketplaceProductType.AvatarAsset, Id = id } }
					context._K.Service.Marketplace:PromptBulkPurchase(context._K.LocalPlayer, items, {})
					return
				end
			end
			for _, player in players do
				context.env.remote:Fire(player, animation, loop, speed)
			end
		end,
	},
	{
		name = "emotes",
		description = "Shows the emotes list.",
		optionalPrefix = true,
		envClient = function(_K)
			local UI = _K.UI

			local env = {}

			task.spawn(function()
				repeat
					task.wait()
				until _K.Registry.commands.emote and _K.Registry.commands.emote.env

				local EMOTE = _K.Registry.commands.emote.env

				local scroller = UI.new "ScrollerFast" {
					List = _K.Data.emotes,
					Enabled = true,
					Visible = true,
					CreateItem = function(self, initialLineData)
						local item = {
							lineData = initialLineData,
						}
						item._instance = UI.new "Frame" {
							BackgroundTransparency = 1,
							Size = self.ItemSize,

							UI.new "UIPadding" {
								PaddingTop = UI.Theme.PaddingHalf,
								PaddingBottom = UI.Theme.PaddingHalf,
							},

							UI.new "UIListLayout" {
								VerticalAlignment = Enum.VerticalAlignment.Center,
								FillDirection = Enum.FillDirection.Horizontal,
								SortOrder = Enum.SortOrder.LayoutOrder,
								Padding = UI.Theme.Padding,
							},

							UI.new "ImageButton" {
								BackgroundTransparency = 1,
								BackgroundColor3 = UI.Theme.PrimaryText,
								Size = UDim2.new(1, 0, 1, 0),
								SizeConstraint = Enum.SizeConstraint.RelativeYY,

								ImageTransparency = 0.25,
								ImageColor3 = function()
									return if EMOTE.emoteId() == item.lineData.emoteId
										then Color3.new(1, 0, 0)
										else Color3.new(1, 1, 1)
								end,
								Image = function()
									return if EMOTE.emoteId() == item.lineData.emoteId
										then UI.Theme.Image.Stop
										else UI.Theme.Image.Play
								end,

								UI.action(function(button)
									button.Activated:Connect(function()
										if
											not item.lineData.owned
											or _K.LocalPlayer:GetAttribute("_KDonationLevel") < 4
										then
											EMOTE.updatePreview(item.lineData.emoteId)
											EMOTE.preview.Visible = true
											return
										end
										if EMOTE.emoteId._value == item.lineData.emoteId then
											_K.UI.Sound.Hover01:Play()
											EMOTE.playEmote()
										else
											_K.UI.Sound.Hover03:Play()
											if _K.forceChat then
												local name = item.lineData.name
												if string.find(name, _K.Data.settings.splitKey, 1, true) then
													name = `"{name}"`
												end
												_K.forceChat(`{_K.getCommandPrefix()}emote me {name}`)
											end
										end
									end)
									button.MouseEnter:Connect(function()
										button.ImageTransparency = 0
									end)
									button.MouseLeave:Connect(function()
										button.ImageTransparency = 0.25
									end)
								end),
							},

							UI.new "TextLabel" {
								AutoLocalize = false,
								AutomaticSize = Enum.AutomaticSize.X,
								BackgroundTransparency = 1,
								Size = UDim2.new(0, 0, 1, 0),
								RichText = true,
								FontFace = UI.Theme.FontMono,
								TextSize = UI.Theme.FontSize,
								TextColor3 = UI.Theme.PrimaryText,
								TextStrokeColor3 = UI.Theme.Primary,
								TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
								TextTruncate = Enum.TextTruncate.SplitWord,
								TextXAlignment = Enum.TextXAlignment.Left,

								UI.new "UIFlexItem" { FlexMode = Enum.UIFlexMode.Fill },
							},

							UI.new "Spacer" {},

							UI.new "Button" {
								Name = "Unlock",
								Label = "🔓",
								Padding = UI.Theme.PaddingHalf,
								Size = UDim2.new(1, 0, 1, 0),
								SizeConstraint = Enum.SizeConstraint.RelativeYY,
								Activated = function()
									EMOTE.updatePreview(item.lineData.emoteId)
									EMOTE.preview.Visible = true
								end,
							},

							UI.new "Button" {
								Label = "⭐",
								Padding = UI.Theme.PaddingHalf,
								Size = UDim2.new(1, 0, 1, 0),
								SizeConstraint = Enum.SizeConstraint.RelativeYY,
								Activated = function()
									pcall(function()
										_K.Service.AvatarEditor:PromptSetFavorite(item.lineData.emoteId, 1, true)
									end)
								end,
							},
						}

						return item
					end,

					RenderItem = function(self, lineItem, lineData)
						lineItem.lineData = lineData
						local instance = lineItem._instance
						instance.TextLabel.Text = lineData.name
						if lineData.owned == nil then
							lineData.owned = false
							task.spawn(function()
								local ok, result = _K.Util.Retry(function()
									return _K.Service.Marketplace:PlayerOwnsAssetAsync(_K.LocalPlayer, lineData.emoteId)
								end)
								if ok then
									lineData.owned = result
									self:render(true)
								end
							end)
						end
						instance.Unlock.Visible = not lineData.owned
					end,
				}

				UI.edit(scroller._instance, {
					UI.new "UIPadding" {
						PaddingTop = UI.Theme.PaddingStroke,
						PaddingBottom = UI.Theme.PaddingStroke,
						PaddingLeft = UI.Theme.PaddingStroke,
						PaddingRight = UI.Theme.PaddingStroke,
					},
				})
				UI.edit(scroller._scroller.UIPadding, {
					PaddingTop = UDim.new(),
					PaddingBottom = UDim.new(),
				})

				env.scroller = scroller
				env.window = UI.new "Window" {
					Parent = UI.LayerTopInset,
					Name = "Emotes",
					Title = "Emotes",
					Exitable = true,
					Position = UDim2.new(0.5, -128, 0.5, -128),
					Size = UDim2.new(0, 256, 0, 256),

					scroller,
				}
			end)

			return env
		end,
		runClient = function(context, player)
			context.env.window._instance.Visible = true
		end,
	},

	{
		name = "age",
		aliases = { "accountage" },
		description = "Displays the account age of a player.",
		args = {
			{
				type = "player",
				name = "Player",
				description = "The player whose account age to display.",
				lowerRank = true,
			},
		},

		runClient = function(context, player: Player)
			context._K.Notify({
				From = player.UserId,
				Text = `<b>Account Age:</b> {context._K.Util.Time.readable(player.AccountAge * 86400)}`,
			})
		end,
	},
	{
		name = "serverage",
		aliases = { "uptime" },
		description = "Displays the age of the server.",
		runClient = function(context, volume)
			context._K.Notify({
				From = "_K",
				Text = `<b>Server Age:</b> {context._K.Util.Time.readable(
					workspace:GetServerTimeNow() - context._K.script:GetAttribute("ServerStartTime")
				)}`,
			})
		end,
	},
	{
		name = "gameid",
		aliases = {},
		description = "Displays the current GameId.",
		runClient = function(context, volume)
			context._K.Notify({ From = "GameId", Text = tostring(game.GameId), Selectable = true })
		end,
	},
	{
		name = "jobid",
		aliases = {},
		description = "Displays the current JobId.",
		runClient = function(context, volume)
			context._K.Notify({ From = "JobId", Text = tostring(game.JobId), Selectable = true })
		end,
	},
	{
		name = "placeid",
		aliases = {},
		description = "Displays the current PlaceId.",
		runClient = function(context, volume)
			context._K.Notify({ From = "PlaceId", Text = tostring(game.PlaceId), Selectable = true })
		end,
	},

	{
		name = "showfps",
		aliases = { "getfps", "checkfps", "playerfps" },
		description = "Displays the frames per second of a player.",
		args = {
			{
				type = "player",
				name = "Player",
				description = "The player whose frames per second to display.",
				lowerRank = true,
			},
		},
		env = function(_K)
			return { remote = _K.Remote.ShowFPS }
		end,
		envClient = function(_K)
			local env = {
				fps = 60,
			}
			function _K.Remote.ShowFPS.OnInvoke()
				return env.fps
			end
			local frames, last = 0, tick()
			_K.Service.Run.Heartbeat:Connect(function()
				frames += 1
				local dt = tick() - last
				if dt >= 1 then
					env.fps = frames / dt
					frames, last = 0, tick()
				end
			end)
			return env
		end,
		run = function(context, player: Player)
			context._K.Remote.Notify:Fire(context.fromPlayer, {
				Text = `<b>FPS:</b> {math.round(context.env.remote:Invoke(player) or 0)}`,
				From = player.UserId,
			})
		end,
	},
	{
		name = "ping",
		aliases = { "getping", "checkping", "latency" },
		description = "Displays the ping of a player.",
		args = {
			{
				type = "player",
				name = "Player",
				description = "The player to ping.",
				lowerRank = true,
			},
		},

		run = function(context, player: Player)
			context._K.Remote.Notify:Fire(
				context.fromPlayer,
				{ Text = `<b>Ping:</b> {math.round(player:GetNetworkPing() * 1000)} ms`, From = player.UserId }
			)
		end,
	},
	{
		name = "wait",
		aliases = { "delay" },
		description = "Delays command execution for a period of time.",
		args = {
			{
				type = "number",
				name = "Seconds",
				description = "How long to wait in seconds.",
			},
		},

		run = function(context, delay: number)
			task.wait(delay)
		end,
	},
	{
		name = "rejoin",
		aliases = { "rj" },
		description = "Rejoins the server.",

		run = function(context)
			local options = Instance.new("TeleportOptions")
			options.ServerInstanceId = game.JobId
			context._K.Util.SafeTeleport(game.PlaceId, { context.fromPlayer }, options)
		end,
	},
	{
		name = "join",
		description = "Join a player in the same game.",
		args = {
			{
				type = "userId",
				name = "UserId",
				description = "The UserId of the player to join.",
			},
		},

		run = function(context, userId)
			local ok, samePlace, errorMessage, placeId, jobId =
				pcall(context._K.Service.Teleport.GetPlayerPlaceInstanceAsync, context._K.Service.Teleport, userId)
			if not ok then
				return errorMessage or samePlace
			end
			if samePlace then
				return "You're already in the same server!"
			end

			if placeId and jobId then
				local options = Instance.new("TeleportOptions")
				options.ServerInstanceId = jobId
				context._K.Util.SafeTeleport(placeId, { context.fromPlayer }, options)
			end
			return
		end,
	},

	{
		name = "viewas",
		aliases = { "unviewas" },
		description = "Views the admin system as one or more role(s).",
		args = {
			{
				type = "roles",
				name = "Roles(s)",
				description = "The roles(s) to test.",
				optional = true,
			},
		},

		envClient = function(_K)
			return {
				originalCreatorId = nil,
				originalMember = nil,
			}
		end,
		runClient = function(context, roles)
			local key = tostring(context.from)
			if context.undo then
				if not context.env.originalMember then
					return
				end
				context._K.Data.members[key] = context.env.originalMember
				if context.env.originalCreatorId then
					context._K.creatorId = context.env.originalCreatorId
					context._K.Data.creatorId = context._K.creatorId
				end
				context.env.originalMember = nil
				context.env.originalCreatorId = nil
				context._K.Notify({
					From = "_K",
					Text = `View returned to original permissions.`,
				})
			else
				if not context.env.originalMember then
					context.env.originalMember = context._K.Data.members[key]
					if context.from == context._K.creatorId then
						context.env.originalCreatorId = context._K.creatorId
						context._K.creatorId = 0
						context._K.Data.creatorId = 0
					end
				end
				context._K.Data.members[key] = {
					name = context.fromPlayer.Name,
					roles = roles or {},
					persist = {},
				}

				local roleStrings = {}
				if roles then
					for _, role in roles do
						local roleData = context._K.Data.roles[role]
						if roleData then
							table.insert(roleStrings, `<font color="{roleData.color}">{roleData.name}</font>`)
						end
					end
				end
				local everyoneRole = context._K.Data.roles.everyone
				table.insert(roleStrings, `<font color="{everyoneRole.color}">{everyoneRole.name}</font>`)
				local roleString = table.concat(roleStrings, ", ")

				context._K.Notify({
					From = "_K",
					Text = `Viewing the system as <sc>{roleString}</sc>`
						.. `\n\nUse <b>{context._K.getCommandPrefix(context.from)}unviewas</b> to return to normal.`,
				})
			end
			context._K.client.members(context._K.Data.members)
			context._K.client.updateInterfaceAuth()
		end,
	},

	-- {
	-- 	name = "alias",
	-- 	aliases = {},
	-- 	description = "Creates a command alias out of a command and given arguments.",
	-- 	args = {
	-- 		{
	-- 			type = "string",
	-- 			name = "Alias name",
	-- 			description = "The name of the command alias.",
	-- 		},
	-- 		{
	-- 			type = "string",
	-- 			name = "Command string",
	-- 			description = "The command string to alias.",
	-- 		},
	-- 	},

	-- 	runClient = function(context, alias: string, command: string)
	-- 		context._K.Registry.registerCommandAlias(context._K, alias, command)
	-- 	end,
	-- },
}


local _K

local function parseBan(input)
	local query = string.lower(input)
	for userId, ban in _K.Data.bans do
		local name = ban[1]
		if
			(name and string.find(string.lower(name), query, 1, true) == 1)
			or string.find(string.lower(userId), query, 1, true) == 1
		then
			return userId
		end
	end

	if tonumber(input) then
		return tonumber(input)
	end

	return nil, "Invalid ban."
end

local typeBan = {
	validate = parseBan,
	parse = parseBan,
	prefixes = {
		["^%*$"] = "allBans",
		["^all$"] = "allBans",
	},
	suggestions = function(text)
		local names = { { { "*", "all" }, "*  [all] (Everyone)" } }
		for userId, ban in _K.Data.bans do
			local name = ban[1]
			if name then
				table.insert(names, { { name, userId }, `{name} [{userId}]` })
			else
				table.insert(names, { userId, `UNKNOWN [{userId}]` })
			end
		end
		return names
	end,
}

local typeBans = {
	listable = true,
}

local function parseAll(arg, self)
	local feedback = {}
	local bans = {}
	for userId, ban in _K.Data.bans do
		local message
		ban, message = self._K.Auth.targetUserArgument(self, tonumber(userId) :: number, userId)
		if ban then
			table.insert(bans, userId)
		elseif not table.find(feedback, message) then
			table.insert(feedback, userId)
		end
	end

	if #bans > 0 then
		return bans
	end

	return nil, #feedback > 0 and table.concat(feedback, "\n") or "No targetable user found"
end

local typeAll = {
	listable = true,
	transform = string.lower,
	validate = function(v, self)
		local ok, feedback = parseAll(v, self)
		return ok, feedback or "Invalid ban"
	end,
	suggestions = { { { "*", "all" }, "*  [all] (Everyone)" } },
	parse = parseAll,
}

return function(context)
	_K = context
	_K.Registry.registerType("ban", typeBan)
	_K.Registry.registerType("bans", typeBans, typeBan)
	_K.Registry.registerType("allBans", typeAll)
end


local _K

local COLORS = {
	["0,0,0"] = Color3.new(0, 0, 0),
	["255,255,255"] = Color3.new(1, 1, 1),
	["#000"] = Color3.new(0, 0, 0),
	["#fff"] = Color3.new(1, 1, 1),

	red = Color3.new(1, 0, 0),
	orange = Color3.new(1, 0.5, 0),
	yellow = Color3.new(1, 1, 0),
	yellowgreen = Color3.new(0.5, 1, 0),
	green = Color3.new(0, 1, 0),
	aqua = Color3.new(0, 1, 1),
	cyan = Color3.new(0, 1, 1),
	blue = Color3.new(0, 0, 1),
	purple = Color3.new(0.5, 0, 1),
	violet = Color3.new(0.5, 0, 1),
	pink = Color3.new(1, 0, 1),
	magenta = Color3.new(1, 0, 0.5),

	white = Color3.new(1, 1, 1),
	black = Color3.new(0, 0, 0),
	grey = Color3.new(0.5, 0.5, 0.5),
	gray = Color3.new(0.5, 0.5, 0.5),
}

local invalidColorId = BrickColor.new(-1).Number
for i = 1, 1032 do
	local color = BrickColor.new(i)
	if i ~= invalidColorId and color.Number == invalidColorId then
		continue
	end
	local name = string.lower(color.Name)
	if COLORS[name] then
		continue
	end
	COLORS[tostring(i)] = color.Color
	COLORS[name] = color.Color
end

local colorNames = {}
for name in COLORS do
	table.insert(colorNames, name)
end
table.sort(colorNames)

COLORS.random = Color3.new(math.random(), math.random(), math.random())
table.insert(colorNames, 1, "random")

local typeColor = {
	transform = function(input: string)
		if input == "" then
			return ""
		end
		input = _K.Util.String.trim(string.lower(input :: string))
		if COLORS[input] then
			if input == "random" then
				return Color3.new(math.random(), math.random(), math.random())
			end
			return COLORS[input]
		end
		if string.find(input, "^#") then
			if #input > 7 or string.find(input, "[^#a-f%d]") then
				return nil -- invalid hex input
			end
			if #input < 4 then
				input ..= string.rep("f", 4 - #input)
			elseif #input > 4 then
				input ..= string.rep("f", 7 - #input)
			end
			return Color3.fromHex(input)
		else
			local r, g, b = string.match(input, "^(%d+),?(%d*),?(%d*)$")
			if r then
				local validRGB = true
				for _, channel in { r, g, b } do
					local value = tonumber(channel)
					if value and (#channel > 3 or value < 0 or value > 255) then
						validRGB = false
						break
					end
				end
				if validRGB then
					return Color3.fromRGB(r or 255, g or 255, b or 255)
				end
			end
			for name, color in COLORS do -- partial match
				if string.find(name, input, 1, true) == 1 then
					return color
				end
			end
		end
		return nil, "Invalid Color"
	end,

	validate = function(color: Color3?): (boolean, string?)
		if typeof(color) == "Color3" then
			return true
		end
		return false, "Invalid Color3"
	end,

	parse = function(color: Color3): Color3
		return color
	end,

	suggestions = function(text: string)
		local names = table.clone(colorNames)

		if string.find(text, "^#") then
			if #text < 4 then
				table.insert(names, text .. string.rep("f", 4 - #text))
			end
			if #text < 7 then
				table.insert(names, text .. string.rep("f", 7 - #text))
			end
		elseif text ~= "" then
			if string.find("random", string.lower(text :: string), 1, true) == 1 then
				COLORS.random = Color3.new(math.random(), math.random(), math.random())
			end
			local r, g, b = string.match(text :: string, "^(%d+),?(%d*),?(%d*)$")
			table.insert(names, `{r or 255},{g or 255},{b or 255}`)
		end

		return names, COLORS
	end,
}

return function(context)
	_K = context
	_K.Registry.registerType("color", typeColor)
end


return function(_K)
	local commandType = {
		transform = function(text)
			local query = string.lower(text)
			local exactMatch = _K.Registry.commands[query]
			if exactMatch and exactMatch.LocalPlayerAuthorized then
				return exactMatch
			end
			for key, command in _K.Registry.commands do
				if not command.LocalPlayerAuthorized then
					continue
				end
				if string.find(string.lower(key), query, 1, true) then
					return command
				end
				if not command.aliases then
					continue
				end
				for _, alias in command.aliases do
					if string.find(string.lower(alias :: string), query, 1, true) then
						return command
					end
				end
			end
			return
		end,

		validate = function(command)
			return if command then true else false, "No command with that name could be found."
		end,

		suggestions = function(text)
			if _K.client.settings.onlyShowUsableCommands._value then
				local filtered = {}
				for _, name in _K.Registry.commandNames do
					local command = _K.Registry.commands[name]
					if command and command.LocalPlayerAuthorized then
						table.insert(filtered, name)
					end
				end
				return filtered, _K.Registry.commands
			end
			return _K.Registry.commandNames, _K.Registry.commands
		end,

		parse = function(command)
			return command
		end,
	}

	_K.Registry.registerType("command", commandType)
end


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

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

local Remote = Util.Remote.EmoteType

local _K

local emotes = {}
local emoteItemFromId = {}
local animationToEmoteId = {}
local emoteToAnimationId = {}

local function isEmote(id: number)
	if emoteItemFromId[id] then
		return emoteItemFromId[id]
	end

	local ok, result = pcall(MarketplaceService.GetProductInfoAsync, MarketplaceService, id)
	if ok and type(result) == "table" and result.AssetTypeId == Enum.AssetType.EmoteAnimation.Value then
		local item = {
			name = result.Name,
			nameLower = string.lower(result.Name),
			emoteId = id,
			canSell = result.IsForSale and not (result.isLimited or result.isLimitedUnique),
		}
		table.insert(emotes, item)
		emoteItemFromId[id] = item
		if item.canSell then
			_K.VIP.allowedBulkPurchaseIds[id] = true
		end
		return item
	end

	return false
end

local function getAnimationIdFromEmoteId(emoteId: number): number?
	if emoteToAnimationId[emoteId] then
		return emoteToAnimationId[emoteId]
	end

	if not isEmote(emoteId) then
		return
	end

	-- clients can't LoadAsset
	if RunService:IsClient() then
		return 0
	end

	local ok, result = pcall(function()
		local asset = InsertService:LoadAsset(emoteId)
		local animation = asset:FindFirstChildWhichIsA("Animation")
		local animationId = animation and animation.AnimationId
		asset:Destroy()
		return animationId
	end)

	if ok and result then
		result = string.match(result, "%d+$")
		if result then
			result = tonumber(result)
			animationToEmoteId[result] = emoteId
			emoteToAnimationId[emoteId] = result

			local item = emoteItemFromId[emoteId]
			if item then
				item.animationId = result
				Remote:FireAll({ item })
			end
			return result
		else
			warn("Invalid animationId")
		end
	else
		warn(result)
	end

	return
end

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

task.spawn(function()
	if RunService:IsServer() then
		local params = CatalogSearchParams.new()
		params.AssetTypes = { Enum.AvatarAssetType.EmoteAnimation }
		params.CreatorId = 3403354
		params.CreatorType = Enum.CreatorTypeFilter.Group

		local okPages, pages = Util.Retry(function()
			return AvatarEditorService:SearchCatalogAsync(params)
		end)

		if not okPages then
			warn(pages)
			return
		end

		local okItems, items = Util.Retry(function()
			return pagesToTable(pages)
		end)

		if not okItems then
			warn(items)
			return
		end

		local tasks = Util.Tasks.new()
		for _, item in items do
			local emote = {
				default = true,
				animationId = 0,
				emoteId = item.Id,
				name = item.Name,
				nameLower = string.lower(item.Name),
			}
			table.insert(emotes, emote)
			emoteItemFromId[item.Id] = emote
			_K.VIP.allowedBulkPurchaseIds[item.Id] = true
			tasks:add(getAnimationIdFromEmoteId, item.Id)
		end
		tasks:wait()

		Remote:Connect(function(player)
			Remote:Fire(player, emotes)
		end)
	else
		Remote:Connect(function(data)
			for index, item in data do
				emoteToAnimationId[item.emoteId] = item.animationId
				animationToEmoteId[item.animationId] = item.emoteId
				local existing = emoteItemFromId[item.emoteId]
				if existing then
					table.clear(existing)
					for k, v in item do
						existing[k] = v
					end
				else
					table.insert(emotes, item)
					emoteItemFromId[item.emoteId] = item
				end
			end

			if
				_K
				and _K.Registry.commands.emotes
				and _K.Registry.commands.emotes.env
				and _K.Registry.commands.emotes.env.scroller
			then
				_K.Registry.commands.emotes.env.scroller:refreshList()
			end
		end)
		Remote:Fire()
	end
end)

local emoteType = {
	filterLog = true,
	transform = function(v)
		return tonumber(v) or v
	end,
	validate = function(v)
		if type(v) == "number" and v ~= math.floor(v) then
			return false, "Only whole numbers are valid"
		end
		return v ~= nil, "Invalid emote"
	end,
	parse = function(v, self)
		if type(v) == "number" then
			local animationId = getAnimationIdFromEmoteId(v)
			if animationId then
				return animationId
			end
		elseif type(v) == "string" then
			local query = string.lower(v)
			local partial
			for _, emoteItem in emotes do
				if query == emoteItem.nameLower then
					return emoteItem.animationId
				end
				if not partial and string.find(emoteItem.nameLower, query, 1, true) == 1 then
					partial = emoteItem
				end
			end
			if partial then
				return partial.animationId
			end
		end
		if self.definition.optional and self.rawArg == "" then
			return ""
		end
		return nil, "Could not parse emote"
	end,
	suggestions = function(text)
		local suggestions = { if tonumber(text) then text else nil }
		for _, item in emotes do
			table.insert(suggestions, item.name)
		end
		return suggestions
	end,
}

return function(context)
	_K = context
	_K.Data.animationToEmoteId = animationToEmoteId
	_K.Data.emoteToAnimationId = emoteToAnimationId
	_K.Data.emoteItemFromId = emoteItemFromId
	_K.Data.emotes = emotes
	_K.Registry.registerType("emote", emoteType)
	_K.Registry.registerType("emotes", { listable = true }, emoteType)
end


local _K

local imageType = {
	filterLog = true,
	transform = function(v)
		return if v == "" then 0 else tonumber(v)
	end,
	validate = function(v)
		return v ~= nil and v == math.floor(v), "Only whole numbers are valid"
	end,
	parse = function(v, self)
		local texture = _K.Util.getTexture(v, _K.IsClient)
		if texture then
			return texture
		end
		if self.definition.optional and self.rawArg == "" then
			return ""
		end
		if _K.IsClient then
			return "rbxasset://textures/ui/GuiImagePlaceholder.png"
		end
		return nil, "Failed to load image"
	end,
	suggestions = function(text)
		return { if tonumber(text) then text else nil }
	end,
}

return function(context)
	_K = context
	_K.Registry.registerType("image", imageType)
	_K.Registry.registerType("images", { listable = true }, imageType)
end


local _K = nil

local function parseMember(input: string, self: any): string?
	local query = string.lower(input)
	local fromString = tostring(self.command.from)

	if input == "" or input == "me" and _K.Data.members[fromString] then
		return self._K.Auth.targetUserArgument(self, self.command.from, fromString)
	end

	local partial
	for id, member in _K.Data.members do
		local name = string.lower(member.name)
		if id == query or name == query then
			return self._K.Auth.targetUserArgument(self, tonumber(id) :: number, id)
		end
		if not partial and string.find(name, query, 1, true) == 1 then
			partial = id
		end
	end

	if partial then
		return self._K.Auth.targetUserArgument(self, tonumber(partial) :: number, partial)
	end

	return nil, "Invalid member"
end

local function parseAll(arg, self)
	local feedback = {}
	local members = {}
	for id, member in _K.Data.members do
		local message
		member, message = self._K.Auth.targetUserArgument(self, tonumber(id) :: number, id)
		if member then
			table.insert(members, member)
		elseif not table.find(feedback, message) then
			table.insert(feedback, message)
		end
	end

	if #members > 0 then
		return members
	end

	return nil, #feedback > 0 and table.concat(feedback, "\n") or "No targetable member found"
end

local typeAll = {
	listable = true,
	transform = string.lower,
	validate = function(v, self)
		local ok, feedback = parseAll(v, self)
		return ok, feedback or "Invalid member"
	end,
	suggestions = { { { "*", "all" }, "*  [all] (Everyone)" } },
	parse = parseAll,
}

local function nameSort(a, b)
	return string.lower(a[1][1]) < string.lower(b[1][1])
end

local typeMember = {
	validate = parseMember,
	parse = parseMember,
	prefixes = {
		["^%*$"] = "allMembers",
		["^all$"] = "allMembers",
	},
	suggestions = function(text: string, from: number, definition)
		local names = { { { "*", "all" }, "*  [all] (Everyone)" } }
		local fromString = tostring(from)
		local rank = _K.Auth.getRank(from)
		local lowerRank = definition.lowerRank
			or (definition.shouldRequest and _K.Data.settings.commandRequests ~= false)

		for id, member in _K.Data.members do
			local memberRank = _K.Auth.getRank(id)
			if id == fromString then
				continue
			end
			if from == _K.creatorId or not lowerRank or (fromString == id) or rank > memberRank then
				table.insert(names, { { member.name, id }, `{member.name} [{id}]` })
			end
		end

		table.sort(names, nameSort)

		local fromMember = _K.Data.members[fromString]
		if fromMember and not definition.ignoreSelf then
			table.insert(names, 1, { { "me", fromMember.name, from }, `me [{fromMember.name}] [{from}]` })
		end

		return names
	end,
	log = function(arg)
		return arg.rawArg
	end,
}

local typeMembers = {
	listable = true,
}

return function(context)
	_K = context
	_K.Registry.registerType("member", typeMember)
	_K.Registry.registerType("members", typeMembers, typeMember)
	_K.Registry.registerType("allMembers", typeAll)
end


local Players = game:GetService("Players")

local generateValidate = require(script.Parent.Parent:WaitForChild("generateValidate"))

local function allParse(arg, self)
	local feedback = {}
	local players = {}
	for _, player in Players:GetPlayers() do
		local message
		player, message = self._K.Auth.targetUserArgument(self, player.UserId, player)
		if player then
			table.insert(players, player)
		elseif not table.find(feedback, message) then
			table.insert(feedback, message)
		end
	end

	if #players > 0 then
		return players
	end

	return nil, #feedback > 0 and table.concat(feedback, "\n") or "No targetable player found"
end

local typeEveryone = {
	listable = true,
	transform = string.lower,
	validate = generateValidate(allParse),
	suggestions = function(text, self)
		return { { { "*", "all" }, "*  [all] (Everyone)" } }
	end,
	parse = allParse,
}

return function(_K)
	_K.Registry.registerType("allPlayers", typeEveryone)
end


local Players = game:GetService("Players")

-- TODO: finish inverse type

local _typeInverse = {
	postParse = function(arg, self)
		local players = {}
		if type(arg) == "table" then
			for _, player in Players:GetPlayers() do
				local matched
				for _, player2 in arg do
					if player == player2 then
						matched = true
						break
					end
				end
				if not matched then
					player = self._K.Auth.targetUserArgument(self, player.UserId, player)
					if player then
						table.insert(players, player)
					end
				end
			end
		elseif typeof(arg) == "Instance" then
			for _, player in Players:GetPlayers() do
				if player ~= arg then
					player = self._K.Auth.targetUserArgument(self, player.UserId, player)
					if player then
						table.insert(players, player)
					end
				end
			end
		end
		return players
	end,
	prefixes = {
		["%$"] = "magicPlayers",
		["@"] = "rolePlayers",
		["%%"] = "teamPlayers",
	},
}

return function(_K)
	-- _K.Registry.registerType("inversePlayers", typeInverse)
end


local Players = game:GetService("Players")

local generateValidate = require(script.Parent.Parent:WaitForChild("generateValidate"))

local function othersParse(arg, self)
	local feedback = {}
	local players = {}
	for _, player in Players:GetPlayers() do
		if player == self.command.fromPlayer then
			continue
		end
		local message
		player, message = self._K.Auth.targetUserArgument(self, player.UserId, player)
		if player then
			table.insert(players, player)
		elseif not table.find(feedback, message) then
			table.insert(feedback, message)
		end
	end

	if #players > 0 then
		return players
	elseif #Players:GetPlayers() == 1 then
		return nil, "No other players online"
	end

	return nil, table.concat(feedback, "\n") or "No targetable player found"
end

local typeOthers = {
	listable = true,
	transform = string.lower,
	validate = generateValidate(othersParse),
	parse = othersParse,
}

return function(_K)
	_K.Registry.registerType("otherPlayers", typeOthers)
end


local Players = game:GetService("Players")

local generateValidate = require(script.Parent.Parent:WaitForChild("generateValidate"))

local function parseRandom(arg, self)
	arg = if arg == "" then 1 else tonumber(arg)
	if not arg or math.floor(arg) ~= arg then
		return nil, "Invalid integer"
	end

	local feedback = {}
	local players = {}
	local list = {}
	for _, player in Players:GetPlayers() do
		local message
		player, message = self._K.Auth.targetUserArgument(self, player.UserId, player)
		if player then
			table.insert(list, player)
		elseif not table.find(feedback, message) then
			table.insert(feedback, message)
		end
	end

	if #list == 0 then
		return nil, #feedback > 0 and table.concat(feedback, "\n") or "No targetable player found"
	end

	for _ = 1, arg do
		local length = #list
		if length == 0 then
			break
		end
		table.insert(players, table.remove(list, math.random(length)))
	end
	return players
end

local typeRandom = {
	listable = true,
	validate = generateValidate(parseRandom),
	parse = parseRandom,
}

-- TODO: dynamic suggestion to show how many random people will be selected

return function(_K)
	_K.Registry.registerType("randomPlayers", typeRandom)
end


local Players = game:GetService("Players")

local generateValidate = require(script.Parent.Parent:WaitForChild("generateValidate"))

return function(_K)
	local function roleParse(arg, self)
		local feedback = {}
		local players, valid = {}, false
		for roleId in _K.Data.roles do
			if string.find(string.lower(roleId :: string), arg, 1, true) == 1 then
				valid = true
				for _, player in Players:GetPlayers() do
					local userPermissions = _K.Data.members[tostring(player.UserId)]
					if userPermissions and table.find(userPermissions.roles, roleId) then
						local message
						player, message = self._K.Auth.targetUserArgument(self, player.UserId, player)
						if player then
							table.insert(players, player)
						elseif not table.find(feedback, message) then
							table.insert(feedback, message)
						end
					end
				end
				break
			end
		end

		if not valid then
			return false, "Invalid role"
		end

		return #players > 0 and players, #feedback > 0 and table.concat(feedback, "\n") or "No targetable player found"
	end

	local roleIds = {}
	local typeRoles = {
		listable = true,
		transform = string.lower,
		validate = generateValidate(roleParse),
		parse = roleParse,
		suggestions = function(text, self)
			return roleIds
		end,
	}

	_K.Registry.registerType("rolePlayers", typeRoles)

	task.defer(function()
		repeat
			task.wait()
		until _K.Data.roles

		for roleId in _K.Data.roles do
			table.insert(roleIds, roleId)
		end
		table.sort(roleIds, _K.Auth.roleIdSortAscending)
		for index, roleId in next, roleIds do
			roleIds[index] = "@" .. roleId
		end
	end)
end


local Teams = game:GetService("Teams")

local generateValidate = require(script.Parent.Parent:WaitForChild("generateValidate"))

return function(_K)
	local function teamParse(arg, self)
		local feedback = {}
		local players, valid = {}, false
		for _, team in Teams:GetTeams() do
			if string.find(string.lower(team.Name), arg, 1, true) == 1 then
				valid = true
				for _, player in team:GetPlayers() do
					local message
					player, message = self._K.Auth.targetUserArgument(self, player.UserId, player)
					if player then
						table.insert(players, player)
					elseif not table.find(feedback, message) then
						table.insert(feedback, message)
					end
				end
				break
			end
		end
		if not valid then
			return false, "Invalid team"
		end
		return #players > 0 and players, #feedback > 0 and table.concat(feedback, "\n") or "No targetable player found"
	end

	local typeTeam = {
		listable = true,
		transform = string.lower,
		validate = generateValidate(teamParse),
		parse = teamParse,
		suggestions = function(text, self)
			local names = {}
			for _, team in next, Teams:GetTeams() do
				table.insert(names, "%" .. team.Name)
			end
			return names
		end,
	}

	_K.Registry.registerType("teamPlayers", typeTeam)
end


local function generateValidate(validate)
	return function(v, self)
		local ok, feedback = validate(v, self)
		return ok, feedback or "Invalid player"
	end
end

return generateValidate


local Players = game:GetService("Players")

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

local Types = script:WaitForChild("Types")

-- TODO: post random selector? "%blue team~5"
-- FIX: add a default description for autocompleting prefixes!

local _K = nil

--[[
	!	- inverse match
	*	- all
	@	- roles
		match displayname/username if no roles found?
	$	- special
	%	- teamPlayers match
		have non prefix fallback to userid if no username matched?
	$g=id=<>rank	- group/rank match
	$d=<>10			- distance match

	TODO: fallback matching for most prefixes? (in case of accidental typing)
]]

local prefixes = {
	["^%*$"] = "allPlayers",
	["^all$"] = "allPlayers",
	["@"] = "rolePlayers",
	["%%"] = "teamPlayers",
	["~"] = "randomPlayers",
	["%$"] = "specialPlayers",
	["^others$"] = "otherPlayers",
}

local prefixSuggestions = {
	{ { "*", "all" }, "*  [all] (Everyone)" },
	{ "@", "@\t\t(Roles)" },
	{ "%", "%\t\t(Teams)" },
	{ "~", "~\t\t(Random)" },
	{ "$", "$\t\t(Special)" },
	{ "others", "others   (Everyone else)" },
}

local userCache = {}
local function memoizeUser(player: Player)
	if userCache[player.UserId] then
		local freeThread = userCache[player.UserId].freeThread
		if typeof(freeThread) == "thread" and coroutine.status(freeThread) == "suspended" then
			task.cancel(freeThread)
		end
		return
	end
	userCache[player.UserId] = {
		Name = player.Name,
		DisplayName = player.DisplayName,
		UserId = player.UserId,
		freeThread = nil,
	}
end
local function freeUser(userId: number)
	if userCache[userId] then
		local freeThread = userCache[userId].freeThread
		if typeof(freeThread) == "thread" and coroutine.status(freeThread) == "suspended" then
			task.cancel(freeThread)
			userCache[userId].freeThread = nil
		end
	end
	userCache[userId] = nil
end

Players.PlayerAdded:Connect(function(player)
	freeUser(player.UserId)
end)
Players.PlayerRemoving:Connect(function(player)
	memoizeUser(player)
	userCache[player.UserId].freeThread = task.delay(120, freeUser, player.UserId)
end)

local function sortPlayers(a, b)
	if a == Players.LocalPlayer then
		return true
	elseif b == Players.LocalPlayer then
		return false
	end
	return a.Name < b.Name
end

local function playerSuggestion(player, suggestion)
	local displayName = if player.Name ~= player.DisplayName then player.DisplayName else nil
	local kName = if player.GetAttribute then player:GetAttribute("_KDisplayName") else nil

	if kName == player.Name or kName == displayName then
		kName = nil
	end

	local suggestions = { suggestion }
	table.insert(suggestions, player.Name)
	table.insert(suggestions, displayName)
	table.insert(suggestions, kName)

	displayName = if displayName then ` [{displayName}]` else ""
	kName = if kName then ` [{kName}]` else ""

	local suggestionDisplay = if suggestion
		then `{suggestion} [{player.Name}]{displayName}{kName}`
		else `{player.Name} {displayName}{kName}`

	return { suggestions, suggestionDisplay, player }
end

local function baseSuggestions(text, from, definition, userType: boolean?)
	local players = Players:GetPlayers()
	local names = {}

	if userType then
		for userId, user in userCache do
			table.insert(players, user)
		end
	end

	table.sort(players, sortPlayers)

	if not definition.ignoreSelf then
		table.insert(names, playerSuggestion(Players.LocalPlayer, "me"))
	end

	local rank = _K.Auth.getRank(from)
	for _, player in players do
		if
			player.UserId ~= Players.LocalPlayer.UserId
			and (from == _K.creatorId or not definition.lowerRank or rank > _K.Auth.getRank(player.UserId))
		then
			table.insert(names, playerSuggestion(player))
		end
	end

	for _, suggestion in prefixSuggestions do
		table.insert(names, suggestion)
	end

	return names
end

local function parseIndividual(arg, self, userType)
	if arg == "" or arg == "me" then
		return self._K.Auth.targetUserArgument(self, self.command.from, self.command.fromPlayer)
	end

	local players = Players:GetPlayers()

	if userType then
		for userId, user in userCache do
			table.insert(players, user)
		end
	end

	table.sort(players, sortPlayers)

	local partial
	for _, player in players do
		local name = string.lower(player.Name)
		if name == arg then
			return self._K.Auth.targetUserArgument(self, player.UserId, player)
		end
		if not partial and string.find(name, arg :: string, 1, true) == 1 then
			partial = player
		end
	end

	for _, player in players do
		local displayName = string.lower(player.DisplayName)
		local kDisplay = if userType then "" else string.lower(player:GetAttribute("_KDisplayName") or "")
		if displayName == arg or kDisplay == arg then
			return self._K.Auth.targetUserArgument(self, player.UserId, player)
		end
		if
			not partial
			and (
				string.find(displayName, arg :: string, 1, true) == 1
				or (string.find(kDisplay, arg :: string, 1, true) == 1)
			)
		then
			partial = player
		end
	end

	if partial then
		return self._K.Auth.targetUserArgument(self, partial.UserId, partial)
	end

	return nil, "Invalid player"
end

local typeIndividual = {
	transform = string.lower,
	validate = generateValidate(parseIndividual),
	parse = parseIndividual,
	suggestions = baseSuggestions,
	prefixes = prefixes,
}

local function parseUser(arg, self)
	local player, reason = parseIndividual(arg, self, true)
	local userId = tonumber(if player then player.UserId else arg)
	if userId and userId > 0 then
		_K.Data.targetLimits[self.command.from] -= 1
		return self._K.Auth.targetUserArgument(self, userId, userId)
	end
	return nil, if reason == "Invalid player" then "Invalid user" else reason
end

local typeUser = {
	transform = string.lower,
	validate = parseUser,
	parse = parseUser,
	postParse = function(parsedArgs)
		-- compile nested players from prefixes into one list, transform prefix results to userId
		local users = {}
		for _, arg in parsedArgs do
			if type(arg) == "table" then
				for _, user in arg do
					local userId = if type(user) == "table" or (typeof(user) == "Instance" and user:IsA("Player"))
						then user.UserId
						else user
					table.insert(users, userId)
				end
			else
				local userId = if type(arg) == "table" or (typeof(arg) == "Instance" and arg:IsA("Player"))
					then arg.UserId
					else arg
				table.insert(users, userId)
			end
		end
		return users
	end,
	suggestions = function(text, from, definition)
		local names = baseSuggestions(text, from, definition, true)
		if tonumber(text) then
			table.insert(names, 1, { text, nil, { UserId = tonumber(text) } })
		end
		return names
	end,
	prefixes = prefixes,
}

return function(context)
	_K = context

	_K.Registry.registerType("player", typeIndividual)
	_K.Registry.registerType("players", {
		listable = true,
		postParse = function(parsedArgs)
			-- compile nested players from prefixes into one list
			local players = {}
			for _, arg in parsedArgs do
				if type(arg) == "table" then
					for _, player in arg do
						table.insert(players, player)
					end
				else
					table.insert(players, arg)
				end
			end
			return players
		end,
	}, typeIndividual)

	_K.Registry.registerType("userId", typeUser)
	_K.Registry.registerType("userIds", { listable = true }, typeUser)

	for _, child in Types:GetChildren() do
		if child:IsA("ModuleScript") then
			require(child)(_K)
		end
	end
end


local _K

local function notNil(v)
	return v ~= nil, "Invalid value"
end

local function startMatch(list: { string }, v)
	for _, match in list do
		local vStart, vEnd = string.find(match, v, 1, true)
		if vStart == 1 and vEnd > 0 then
			return true
		end
	end
	return false
end

local yes, no = { "+", "true", "yes", "enabled" }, { "-", "false", "no", "disabled" }
local booleanSuggestions = {}
for i, v in yes do
	table.insert(booleanSuggestions, v)
	table.insert(booleanSuggestions, no[i])
end

local booleanType = {
	transform = string.lower,
	validate = function(v)
		if v == "" then
			return true
		end
		return startMatch(yes, v) or startMatch(no, v), "Invalid boolean"
	end,
	parse = function(v)
		if v == "" then
			return true
		end
		if startMatch(yes, v) then
			return true
		elseif startMatch(no, v) then
			return false
		end
		return nil, "Invalid boolean"
	end,
	suggestions = function(text: string, from: number)
		return booleanSuggestions
	end,
}

local integerType = {
	filterLog = true,
	transform = function(v)
		return if v == "" then 0 else tonumber(v), "Invalid number"
	end,
	validate = function(v)
		return v ~= nil and v == math.floor(v), "Only whole numbers are valid"
	end,
	parse = function(v)
		return v
	end,
}

local numberType = {
	filterLog = true,
	transform = integerType.transform,
	validate = notNil,
	parse = function(v)
		return v
	end,
}

local rawStringType = {
	filterLog = true,
	validate = notNil,
	parse = tostring,
}

local stringType = {
	validate = notNil,
}

return function(context)
	_K = context
	_K.Registry.registerType("boolean", booleanType)
	_K.Registry.registerType("booleans", {
		listable = true,
	}, booleanType)
	_K.Registry.registerType("integer", integerType)
	_K.Registry.registerType("integers", {
		listable = true,
	}, integerType)
	_K.Registry.registerType("number", numberType)
	_K.Registry.registerType("numbers", {
		listable = true,
	}, numberType)
	_K.Registry.registerType("stringGreedy", rawStringType)
	_K.Registry.registerType("rawString", rawStringType)
	_K.Registry.registerType("rawStrings", {
		listable = true,
	}, rawStringType)
	function stringType.parse(v, self)
		if _K.Service.RunService:IsClient() or self.command.global then
			return v
		end
		if
			not _K.Auth.hasRestrictedRole(self.command.from)
			and not _K.Service.TextChat:CanUserChatAsync(self.command.from)
		then
			return "User cannot chat"
		end
		return _K.Util.String.filterForBroadcast(v, self.command.from)
	end
	_K.Registry.registerType("string", stringType)
	_K.Registry.registerType("strings", {
		listable = true,
	}, stringType)
end


local _K

local function parse(input: string, self: any): (boolean, string?)
	local exact = _K.Data.reservedServers[input]
	if exact then
		return exact
	end
	local query, partial = string.lower(input)
	for reservedCode, data in _K.Data.reservedServers do
		local match = string.lower(reservedCode)
		if match == query then
			return data
		elseif string.find(match, query) then
			if not partial or #partial < #reservedCode then
				partial = reservedCode
			end
		end
	end
	return _K.Data.reservedServers[partial], "Invalid reserveCode"
end

local reservedType = {
	validate = parse,
	parse = parse,
	suggestions = function(text: string, from: number)
		local names = {}
		for reservedCode in _K.Data.reservedServers do
			table.insert(names, reservedCode)
		end
		table.sort(names)
		return names
	end,
}

return function(context)
	_K = context
	_K.Registry.registerType("reservedServer", reservedType)
	_K.Registry.registerType("reservedServers", { listable = true }, reservedType)
end


return function(context)
	local _K = context

	local function roleIdSort(a, b)
		return _K.Data.roles[a[1]]._rank > _K.Data.roles[b[1]]._rank
	end

	local function roleParse(input: string, self: any)
		local query = string.lower(input)
		local rank = _K.Auth.getRank(self.command.from)
		for roleId, role in _K.Data.roles do
			if role == _K.Data.roles.everyone then
				continue
			end
			if
				(self.command.from == _K.creatorId or rank > role._rank)
				and string.find(string.lower(roleId), query, 1, true) == 1
			then
				return roleId
			end
		end
		return nil, "Invalid role"
	end

	local typeRole = {
		validate = roleParse,
		parse = roleParse,
		suggestions = function(text: string, from: number)
			local roles = {}
			local rank = _K.Auth.getRank(from)
			for roleId, role in _K.Data.roles do
				if role == _K.Data.roles.everyone then
					continue
				end
				if from == _K.creatorId or rank > role._rank then
					table.insert(roles, { roleId, nil, role })
				end
			end
			table.sort(roles, roleIdSort)
			return roles, _K.Data.roles
		end,
	}

	_K.Registry.registerType("role", typeRole)
	_K.Registry.registerType("roles", { listable = true }, typeRole)
end


local Teams = game:GetService("Teams")

local _K

local typeTeam = {
	validate = function(input: string, from: number): (boolean, string?)
		local query = string.lower(input)
		local teams = Teams:GetTeams()
		if #teams == 0 then
			return false, "No teams available"
		end
		for _, team in teams do
			if string.find(string.lower(team.Name), query, 1, true) == 1 then
				return true
			end
		end
		return false, "Invalid team"
	end,
	parse = function(input: string, from: number): string
		local query = string.lower(input)
		for _, team in Teams:GetTeams() do
			if string.find(string.lower(team.Name), query, 1, true) == 1 then
				return team
			end
		end
		error("Invalid team value")
	end,
	suggestions = function(text: string, from: number)
		return _K.Util.Suggest.new(Teams:GetTeams())
	end,
}

local typeTeams = {
	listable = true,
}

return function(context)
	_K = context
	_K.Registry.registerType("team", typeTeam)
	_K.Registry.registerType("teams", typeTeams, typeTeam)
end


local TIME_PATTERN = "(%d+%.?%d*)%s*(%a+)"

local suggestedUnits = {
	"second",
	"minute",
	"hour",
	"day",
	"week",
	"month",
	"year",
	"millisecond",
	"s",
	"m",
	"h",
	"d",
	"w",
	"y",
	"hr",
	"wk",
	"yr",
	"ms",
}

local pluralUnits = {}

local defaultSuggestions = { { "0", "0 Session" }, { "-1", "-1 Forever" } }
for i, value in suggestedUnits do
	local capitalized = if #value > 2 then string.upper(string.sub(value, 1, 1)) .. string.sub(value, 2) else value
	defaultSuggestions[i + 2] = { "1" .. value, "1 " .. capitalized }
	if #value > 1 then
		pluralUnits[i + 2] = value .. "s"
	end
end

return function(_K)
	local function parseTime(input: string): (boolean, string | number)
		if input == "" then
			return true, 1
		elseif string.find("-1", input :: string, 1, true) then
			return true, -1
		end

		if tonumber(input) then
			-- no units specified, default to seconds?
			return true, tonumber(input) :: number
		end
		if string.find(input :: string, TIME_PATTERN) == nil then
			return false, "Invalid time format"
		end

		local seconds = 0
		for amount, unit in string.gmatch(string.lower(input :: string), TIME_PATTERN) do
			local duration: number? = _K.Util.Time.fromUnit(unit)
			if duration == nil then
				return false, "Invalid time format"
			else
				seconds += amount * duration
			end
		end

		return true, seconds
	end
	_K.Registry.registerType("timeSimple", {
		filterLog = true,
		validate = function(v)
			return parseTime(v)
		end,
		parse = function(v)
			local ok, result = parseTime(v)
			if not ok then
				error(result)
			end
			return result
		end,
		suggestions = function(text)
			local number = tonumber(string.match(text, "^%-?%d*"))
			if #text > 0 and string.find("-1", text, 1, true) == 1 then
				number = -1
			end
			if number then
				local names = { { "0", "0 Session" }, { "-1", "-1 Forever" } }
				if number > 0 then
					local original = if number > 1 then pluralUnits else suggestedUnits
					for i, value in original do
						local capitalized = if #value > 2
							then string.upper(string.sub(value :: string, 1, 1)) .. string.sub(value :: string, 2)
							else value
						names[i + 1] = {
							number .. value,
							number .. " " .. capitalized,
						}
					end
				end
				return names
			else
				return defaultSuggestions
			end
		end,
	})
end


local _K
local tools = {}

local typeTool = {
	validate = function(input: string, from: number): (boolean, string?)
		local query = string.lower(input)
		for _, tool in tools do
			if string.find(string.lower(tool.Name), query, 1, true) == 1 then
				return true
			end
		end
		return false, "Invalid tool"
	end,
	parse = function(input: string, from: number): Tool
		local query = string.lower(input)
		for _, tool in tools do
			if string.lower(tool.Name) == query then
				return tool
			end
		end
		for _, tool in tools do
			if string.find(string.lower(tool.Name), query, 1, true) == 1 then
				return tool
			end
		end
		error("Invalid tool value")
	end,
	suggestions = function(text: string, from: number)
		return _K.Util.Suggest.new(tools)
	end,
	prefixes = { ["^all$"] = "allTools" },
}

local function allParse()
	return #tools > 0 and tools, "No valid tool found"
end

local typeAllTools = {
	listable = true,
	transform = string.lower,
	validate = allParse,
	parse = allParse,
}

return function(context)
	_K = context

	if _K.IsServer then
		local function sortName(a, b)
			return a.Name < b.Name
		end

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

		local function shouldIgnore(object)
			if object == nil then
				return
			end
			if object:HasTag("_K_Ignore") then
				return true
			end
			return shouldIgnore(object.Parent)
		end

		local function addTool(tool)
			if not (tool:IsA("Tool") or tool:IsA("HopperBin")) or shouldIgnore(tool) then
				return
			end
			for _, existingTool in tools do
				if existingTool.Name == tool.Name then
					return
				end
			end
			table.insert(tools, tool)
			debounceSort(tools, sortName)
		end

		local function registerSource(source)
			for _, tool in source:GetDescendants() do
				addTool(tool)
			end
			source.DescendantAdded:Connect(addTool)
		end

		registerSource(_K.Service.Lighting)
		registerSource(_K.Service.ReplicatedStorage)
		registerSource(_K.Service.ServerStorage)
		registerSource(_K.Service.StarterPack)

		_K.Remote.Tools:Connect(function(player)
			if _K.Auth.hasCommand(player.UserId, "tools") then
				local toolData = table.create(#tools)
				for i, tool in tools do
					toolData[i] = { Name = tool.Name }
				end
				_K.Remote.Tools:Fire(player, toolData)
			end
		end)

		local toolData = table.create(#tools)
		for i, tool in tools do
			toolData[i] = { Name = tool.Name }
		end

		task.spawn(function()
			if not _K.ready then
				_K.Hook.init:Wait()
			end
			for _, player in _K.Service.Players:GetPlayers() do
				if _K.Auth.hasCommand(player.UserId, "tools") then
					_K.Remote.Tools:Fire(player, toolData)
				end
			end
		end)
	else -- Client
		_K.Remote.Tools:Connect(function(toolData)
			tools = toolData
			_K.tools = tools
			table.insert(tools, 1, { Name = "all" })
		end)
		_K.Remote.Tools:Fire()
	end

	_K.Registry.registerType("tool", typeTool)
	_K.Registry.registerType("tools", { listable = true }, typeTool)
	_K.Registry.registerType("allTools", typeAllTools)
end


local function validateVector(value, i)
	if value == nil then
		return false, `Invalid or missing number at position {i} in Vector type.`
	end
	return true
end

return function(context)
	local vector3Type = context.Registry.makeSequenceType({
		validate = validateVector,
		transform = tonumber,
		constructor = Vector3.new,
		length = 3,
	})

	local vector2Type = context.Registry.makeSequenceType({
		validate = validateVector,
		transform = tonumber,
		constructor = Vector2.new,
		length = 2,
	})

	context.Registry.registerType("vector3", vector3Type)
	context.Registry.registerType("vector2", vector2Type)
end


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

--- @class Hook
local Hook = {
	log = Util.Signal.new(),
	commandRegistered = Util.Signal.new(),
	preCommand = Util.Signal.new(),
	postCommand = Util.Signal.new(),
	runPreparedCommands = Util.Signal.new(),
	ban = Util.Signal.new(),
	unban = Util.Signal.new(),
	roleAdded = Util.Signal.new(),
	roleRemoved = Util.Signal.new(),
	authChanged = Util.Signal.new(),

	init = Util.Signal.new(),
	initDataStore = Util.Signal.new(),
	preSyncBans = Util.Signal.new(),
	postSyncBans = Util.Signal.new(),
	preSyncMain = Util.Signal.new(),
	postSyncMain = Util.Signal.new(),
	preSyncLogs = Util.Signal.new(),
	postSyncLogs = Util.Signal.new(),
}

return Hook


-- TODO: need to refactor this to make autocomplete easier hahahahaha ;(

local Argument = {}
Argument.__index = Argument

function Argument.parseTypeUnion(rawType: string, rawArg: string)
	local split = string.split(rawType, " ")

	-- Check prefixes in order from longest to shortest
	local types = {}
	for i = 1, #split, 2 do
		types[#types + 1] = {
			prefix = split[i - 1] or "",
			type = split[i],
		}
	end

	table.sort(types, function(a, b)
		return #a.prefix > #b.prefix
	end)

	for i, t in types do
		if string.sub(rawArg, 1, #t.prefix) == t.prefix then
			return t.type, rawArg:sub(#t.prefix + 1), t.prefix
		end
	end
	return
end

function Argument.new(command, definition, argPos, rawArg)
	local unionType, unionArg, prefix = Argument.parseTypeUnion(definition.type, rawArg)
	local rawType = command._K.Registry.types[unionType]
	assert(rawType, `Invalid argument type {unionType}, did it fail to register?`)

	local rawArgs = if rawType.listable then command._K.Util.String.splitOutsideQuotes(unionArg) else { unionArg }

	local object = {
		_K = command._K,
		command = command,
		commandArray = command.array,
		definition = definition,
		validated = false,

		argPos = argPos,
		prefix = prefix,
		rawArg = rawArg,
		rawType = rawType,
		rawArgs = rawArgs,
		transformedArgs = {},
		transformedTypes = {},
		parsedArgs = {},
	}

	return setmetatable(object, Argument)
end

function Argument:invalidate(rawArg, argPos, message)
	self.invalid = { arg = rawArg, pos = argPos, message = message }
	return false, self.invalid
end

function Argument:transform()
	local argPos = self.argPos
	for _, rawArg in self.rawArgs do
		local arg = self._K.Util.String.stripQuotes(self._K.Util.String.trim(rawArg))
		local argType = self.rawType
		if argType.prefixes then
			for prefix, prefixType in argType.prefixes do
				local startIndex, endIndex = string.find(arg, prefix :: string)
				if startIndex == 1 then
					argType = self._K.Registry.types[prefixType]
					arg = string.sub(arg, endIndex + 1)
					break
				end
			end
		end

		if argType.transform then
			local optional = not string.find(arg, "%S") and self.definition.optional
			local value, feedback = argType.transform(arg)
			arg = value
			if value == nil and not optional then
				local message = feedback
					or "Invalid transform: " .. if string.find(rawArg :: string, "^%s*$") then "nil" else rawArg
				return self:invalidate(rawArg, argPos, message)
			end
		end
		table.insert(self.transformedArgs, arg)
		table.insert(self.transformedTypes, argType)
		argPos += #rawArg + 1
	end
	return true
end

function Argument:validate()
	if self.validated then
		return true
	end

	if self.definition.optional and #self.rawArg == 0 then
		return true
	end

	if self.invalid then
		return false, self.invalid.message
	end

	local transformPos = self.argPos
	for i, arg in self.transformedArgs do
		local rawArg = self.rawArgs[i]
		local argType = self.transformedTypes[i]
		local ok, result = argType.validate(arg, self)
		if not ok then
			return self:invalidate(rawArg, transformPos, result or "Invalid argument: " .. rawArg)
		end
		transformPos += #rawArg + 1
	end

	self.validated = true
	return true
end

function Argument:parse()
	if not (self.validated or self.definition.optional) then
		return false, "Argument must be validated before parsing"
	end

	local argPos = self.argPos
	for i, transformedArg in self.transformedArgs do
		local rawArg = self.rawArgs[i]
		local argType = self.transformedTypes[i]
		local ok, arg, feedback = pcall(argType.parse, transformedArg, self)
		if not ok or arg == nil then
			return self:invalidate(rawArg, argPos, feedback or arg or "Invalid argument: " .. rawArg)
		end

		if argType.listable and not self.rawType.listable then
			table.insert(self.parsedArgs, arg[1])
		elseif argType.listable and self.rawType.listable and argType ~= self.rawType then
			for _, value in arg do
				table.insert(self.parsedArgs, value)
			end
		else
			table.insert(self.parsedArgs, arg)
		end

		argPos += #rawArg + 1
	end

	if self.rawType.postParse then
		_, self.parsedArgs = pcall(self.rawType.postParse, self.parsedArgs, self)
	end

	return true, if self.rawType.listable then self.parsedArgs else self.parsedArgs[1]
end

function Argument:prepare()
	self._K.Data.targetLimits[self.command.from] = 0

	if self.validated then
		return true, if self.rawType.listable then self.parsedArgs else self.parsedArgs[1]
	end

	local ok, result = self:transform()
	if not ok then
		return false, result
	end

	local success, feedback = self:validate()
	if not success then
		return false, feedback
	end

	return self:parse()
end

return Argument


local Players = game:GetService("Players")

local Shared = script.Parent.Parent

local Auth = require(Shared:WaitForChild("Auth"))
local Argument = require(script.Parent:WaitForChild("Argument"))
local Util = require(Shared:WaitForChild("Util"))
local Type = require(Shared:WaitForChild("Type"))

local Command = {
	Argument = Argument,
}
Command.__index = Command

function Command.new(_K, alias, definition: Type.CommandDefinition, array, from, text)
	local rank, role = Auth.getRank(from)
	return setmetatable({
		_K = _K,
		alias = alias,
		array = array,
		definition = definition,
		env = definition.env,
		from = from,
		fromPlayer = Players:GetPlayerByUserId(from),
		fromRank = rank,
		fromRole = role,
		global = false,
		rank = Auth.hasCommand(from, definition) or 0,
		text = text,
		undo = string.find(alias, "un", 1, true) == 1,

		args = definition.args and table.create(#definition.args) or {},
		preparedArgs = definition.args and table.create(#definition.args) or {},
		validated = false,
	}, Command)
end

function Command:shouldRequest(player: Player)
	if self._K.Data.settings.commandRequests == false then
		return false
	end
	if self._K._shouldRequestOverride then
		return self._K._shouldRequestOverride(self, player)
	end
	return self.from ~= self._K.creatorId and self.rank <= Auth.getRank(player.UserId)
end

function Command:requestCommand(targetPlayer: Player, args, run)
	if not self.validated then
		return false, self.Error or "Command must be validated before it is run!"
	end

	local userName = if targetPlayer.Name == targetPlayer.DisplayName then "" else " @" .. targetPlayer.Name

	self._K.Remote.Notify:Fire(self.fromPlayer, {
		Text = `Requesting <b>{self.alias}</b> command on <b>{targetPlayer.DisplayName}<font transparency="0.5">{userName}</font></b>.`,
	})

	local ok, accepted = pcall(
		self._K.Remote.RequestCommand.Invoke,
		self._K.Remote.RequestCommand,
		targetPlayer,
		self.fromPlayer,
		`{self._K.getCommandPrefix()}{self.alias} {targetPlayer.Name}`
	)

	if not ok then
		return false, `{userName} left during "{self.alias}" command request!`
	end

	local validColor = self._K.Data.settings.themeValid:ToHex()
	local invalidColor = self._K.Data.settings.themeInvalid:ToHex()
	local status = if accepted
		then `<font color='#{validColor}'>accepted</font>`
		else `<font color='#{invalidColor}'>denied</font>`
	self._K.Remote.Notify:Fire(
		self.fromPlayer,
		{ Text = `Command request for <b>{self.alias}</b> was <b>{status}</b>.`, From = targetPlayer.UserId }
	)

	if accepted then
		self._K.Hook.preCommand:Fire(self)
		local _ok, err = pcall(run, self, unpack(args))
		if err then
			self._K.Remote.Notify:Fire(self.fromPlayer, {
				Text = `<b>"{self.alias}" command failed:</b> {err}`,
				From = "_K",
				TextColor3 = Color3.new(1, 0, 0),
				Sound = "Call_Leave",
			})
		end
		-- TODO: log the command request!
		self._K.Hook.postCommand:Fire(self)
	end

	return
end

function Command:run()
	if not self.validated then
		return false, self.Error or "Command must be validated before it is run!"
	end

	local run = self.definition[if self._K.IsServer then "run" else "runClient"]
	if not run then
		return
	end

	if self._K.IsServer and self._K.Data.settings.commandRequests ~= false then
		for argIndex, arg in self.args do
			if arg.definition.shouldRequest and string.find(string.lower(arg.rawType.name), "player", 1, true) then
				local prepared = self.preparedArgs[argIndex]
				if prepared == nil then
					continue
				end

				if typeof(prepared) == "Instance" then
					if prepared == self.fromPlayer then
						continue -- don't request self
					end
					if self:shouldRequest(prepared) then
						task.spawn(self.requestCommand, self, prepared, self.preparedArgs, run)
						return true -- skip running initial command
					end
				else -- list of players
					for i, player in prepared do
						if player == self.fromPlayer then
							continue -- don't request self
						end
						if self:shouldRequest(player) then
							prepared[i] = nil -- strip player
							local soloPrepared = table.clone(self.preparedArgs)
							soloPrepared[argIndex] = { player }
							task.spawn(self.requestCommand, self, player, soloPrepared, run)
						end
					end
				end
			end
		end
	end

	self._K.Hook.preCommand:Fire(self)
	local ok, err = pcall(run, self, unpack(self.preparedArgs))
	self._K.Hook.postCommand:Fire(self)

	if not ok then
		warn(`Kohl's Admin "{self.definition.name}" Command Error:`, err)
	end

	return ok, err
end

function Command:validate()
	if self.validated then
		return true
	end

	-- always try first argument for player types
	if self.definition.args[1] and string.find(self.definition.args[1].type, "player", 1, true) and #self.array < 2 then
		local start, text = unpack(self.array[1])
		self.array[2] = { start + #text, "" }
	end

	local filledSelf
	local arrayIndex = 2
	for index, definition in self.definition.args do
		local data = self.array[arrayIndex]
		local hasArgument = self._K.Auth.hasArgument(self.from, definition)

		if not data then
			if definition.optional or not hasArgument then
				continue
			end
			local start: number = self.array[1][1]
			self.invalidPos = start
			self.invalidArg = string.sub(self.text, start, start + #self.text)
			self.argMissing = `Command "{self.definition.name}" failed, argument {index} missing: {definition.name}`
			return false, self.argMissing
		end

		if not hasArgument then
			continue
		end

		local argPos, rawArg = unpack(data)

		if index == #self.definition.args then
			local buffer = table.create(#self.array - arrayIndex)
			for i = arrayIndex, #self.array do
				table.insert(buffer, self.array[i][2])
			end
			rawArg = table.concat(buffer, " ")
		end

		local shouldFillWithSelf = not filledSelf
			and index ~= #self.definition.args
			and string.find(string.lower(definition.type), "player", 1, true)

		if shouldFillWithSelf and self._K.IsClient then
			local argType = self._K.Registry.types[definition.type]
			if argType and argType.suggestions then
				local suggestions = Util.Suggest.query(rawArg, argType.suggestions(rawArg, self.from, definition))
				if #suggestions > 0 then
					local input = string.lower(rawArg)
					local text, _, _, query = unpack(suggestions[1])
					if
						string.find(string.lower(text), input, 1, true)
						or (query and string.find(string.lower(query), `[{input}`, 1, true))
					then
						shouldFillWithSelf = false
					end
				end
			end
		end

		if shouldFillWithSelf and arrayIndex == #self.array then
			local arg = self.definition.args[index + 1]
			if arg and string.find(string.lower(arg.type), "player", 1, true) then
				rawArg = "me"
				arrayIndex -= 1
				filledSelf = true
			end
		end

		local arg = Argument.new(self, definition, argPos, rawArg)
		local ok, result = arg:prepare()

		if not (ok or filledSelf) and shouldFillWithSelf then
			arg = Argument.new(self, definition, argPos, "me")
			ok, result = arg:prepare()
			arrayIndex -= 1
			filledSelf = true
			arg.skipped = rawArg
		elseif filledSelf and shouldFillWithSelf then
			arg.skipped = rawArg
		end

		self.args[index] = arg

		if ok then
			arrayIndex += 1
			self.preparedArgs[index] = result
		else
			if definition.optional and index < #self.definition.args then
				arg.skipped = rawArg
				continue
			end
			if result then
				self.invalidArg = result.arg
				self.invalidPos = result.pos
				self.invalidMessage = result.message
			end
			return false, result.message or "Argument failed to prepare"
		end
	end

	self.validated = true
	return true
end

return Command