local validate = require(script.Parent.validate)

local __DEV__ = _G.__DEV__

--[=[
	A special symbol that denotes the absence of a value. Used to represent
	deleted values in patches.
]=]
local None = { __none = "__none" }

local function isNone(value: any): boolean
	return type(value) == "table" and value.__none == "__none"
end

--[=[
	JSON serialization can drop the last values of a sparse array. For example,
	`{ [1] = 1, [3] = 3 }` becomes `[1]` in some cases. Ambiguity in the
	serialization of sparse arrays can be resolved by converting them to
	dictionaries with string keys.

	The `apply` function will convert these dictionaries back into arrays as
	long as the target array is not sparse. This function ensures that the diff
	function produces a patch that can be correctly applied.
]=]
local function stringifySparseArray(object: { any }): { [string | number]: any }
	local maxn = table.maxn(object)

	if maxn == 0 or maxn == #object then
		-- The object is either not an array or a dense array
		return object
	end

	local dictionary = {}

	for index, value in next, object do
		dictionary[tostring(index)] = value
	end

	return dictionary
end

--[=[
	Compares two states and returns a patch that can be applied to the previous
	state to produce the next state.

	@param prevState The previous state.
	@param nextState The next state.
	@param serialize Whether to convert sparse arrays into dictionaries. Defaults to `true`.
	@returns A patch that can be applied to the previous state.
]=]
local function diff(prevState: { [any]: any }, nextState: { [any]: any }, serialize: boolean?)
	serialize = serialize ~= false

	local patches = table.clone(nextState)

	for key, previous in next, prevState do
		local current = nextState[key]

		if previous == current then
			patches[key] = nil
		elseif current == nil then
			patches[key] = None
		elseif type(previous) == "table" and type(current) == "table" then
			patches[key] = diff(previous, current, serialize)
		end
	end

	-- Coerce sparse array patches into dictionaries
	if serialize and (prevState[1] ~= nil or nextState[1] ~= nil) then
		patches = stringifySparseArray(patches)
	end

	if serialize and __DEV__ then
		for key, value in next, patches do
			validate(value, key)
		end
	end

	return patches
end

--[=[
	Applies a patch to a state and returns the next state.

	@param state The current state.
	@param patches The patches to apply.
	@returns The next state.
]=]
local function apply(state: any, patches: any): any
	if type(patches) == "table" and patches.__none == "__none" then
		return nil
	elseif type(state) ~= "table" or type(patches) ~= "table" then
		return patches
	end

	local nextState = table.clone(state)
	local stateIsArray = state[1] ~= nil

	for key, patch in next, patches do
		-- Diff-checking an array produces a sparse array, which will not be
		-- preserved when converted to JSON. To prevent this, we turn string
		-- keys back into numeric keys.
		if stateIsArray and type(key) == "string" then
			key = tonumber(key) or key
		end

		nextState[key] = apply(nextState[key], patch)
	end

	return nextState
end

return {
	isNone = isNone,
	diff = diff,
	apply = apply,
}


local Players = game:GetService("Players")

local Charm = require(script.Parent.Parent.Charm)
local interval = require(script.Parent.interval)
local patch = require(script.Parent.patch)
local types = require(script.Parent.types)
type AtomMap = types.AtomMap
type SyncPayload = types.SyncPayload

type ServerOptions = {
	--[=[
		The atoms to synchronize with the client.
	]=]
	atoms: AtomMap,
	--[=[
		The interval at which to send patches to the client, in seconds.
		Defaults to `0` (patches are sent up to once per frame). Set to a
		negative value to disable automatic syncing.
	]=]
	interval: number?,
	--[=[
		Whether the history of state changes since the client's last update
		should be preserved. This is useful for values that change multiple times
		per frame, where each individual change is important. Defaults to `false`.
	]=]
	preserveHistory: boolean?,
	--[=[
		When `true`, Charm will apply validation and serialize unsafe arrays
		to address remote event argument limitations. Defaults to `true`.

		This option should be disabled if your network library uses a custom
		serialization method (i.e. Zap, ByteNet) to prevent interference.
	]=]
	autoSerialize: boolean?,
}

type ServerSyncer = {
	--[=[
		Sets up a subscription to each atom that schedules a patch to be sent to
		the client whenever the state changes. When a change occurs, the `callback`
		is called with the player and the payload to send.

		Note that the `payload` object should not be mutated. If you need to
		modify the payload, apply the changes to a copy of the object.

		@param callback The function to call when the state changes.
		@return A cleanup function that unsubscribes all listeners.
	]=]
	connect: (self: ServerSyncer, callback: (player: Player, ...SyncPayload) -> ()) -> () -> (),
	--[=[
		Hydrates the client's state with the server's state. This should be
		called when a player joins the game and requires the server's state.

		@param player The player to hydrate.
	]=]
	hydrate: (self: ServerSyncer, player: Player) -> (),
	--[=[
		@deprecated For internal use only.
	]=]
	_sendPatch: (self: ServerSyncer, player: Player) -> (),
}

--[=[
	Creates a `ServerSyncer` object that sends patches to the client and
	hydrates the client's state.

	@param options The atoms to synchronize with the client.
	@return A `ServerSyncer` object.
]=]
local function server(options: ServerOptions): ServerSyncer
	local atoms = options.atoms
	local autoSerialize = options.autoSerialize ~= false
	local preserveHistory = options.preserveHistory
	local syncRate = options.interval or 0

	local self = {} :: ServerSyncer
	local sync: (player: Player, payload: SyncPayload) -> ()

	local function createSnapshot()
		local snapshot = {}

		for key, atom in next, atoms do
			snapshot[key] = atom()
		end

		return snapshot
	end

	-- Send the initial state to a player when they join the server.
	function self:hydrate(player)
		assert(sync, "connect() must be called before hydrate()")

		sync(player, {
			type = "init",
			data = createSnapshot(),
		})
	end

	-- If the history of state changes should be preserved, keep a list of
	-- previous snapshots and diff-check each one from the last.
	if preserveHistory then
		local snapshots = { createSnapshot() }
		local changed = false

		function self:connect(callback)
			local subscriptions = {}

			sync = callback

			local function pushSnapshot(key: string | number, current: any, previous: any)
				local lastSnapshot = snapshots[#snapshots]
				local previousSnapshot = snapshots[#snapshots - 1]

				-- Optimize snapshots by updating the most recent snapshot if the
				-- previous and current values are the same, since this allows us
				-- to group multiple changes into a single snapshot.
				if previousSnapshot and previousSnapshot[key] == previous and lastSnapshot[key] == previous then
					lastSnapshot[key] = current
				else
					local nextSnapshot = table.clone(lastSnapshot)
					nextSnapshot[key] = current
					table.insert(snapshots, nextSnapshot)
				end
			end

			-- Populate the snapshot with the initial state of each atom.
			-- Subscribe to each atom and update the state when it changes.
			for key, atom in next, atoms do
				subscriptions[key] = Charm.subscribe(atom, function(current, previous)
					pushSnapshot(key, current, previous)
					changed = true
				end)
			end

			local disconnect = interval(function()
				if not changed then
					return
				end

				local payloads: { SyncPayload } = {}
				local lastSnapshot

				for index, snapshot in next, snapshots do
					lastSnapshot = snapshot

					if index == 1 then
						continue
					end

					table.insert(payloads, {
						type = "patch",
						data = patch.diff(snapshots[index - 1], snapshot, autoSerialize),
					})
				end

				snapshots = { lastSnapshot }
				changed = false

				for _, player in next, Players:GetPlayers() do
					callback(player, unpack(payloads))
				end
			end, syncRate)

			return function()
				disconnect()

				for _, unsubscribe in next, subscriptions do
					unsubscribe()
				end
			end
		end

		function self:_sendPatch(player: Player)
			assert(sync, "connect() must be called before _sendPatch()")

			if not changed then
				return
			end

			local payloads: { SyncPayload } = {}

			for index, snapshot in next, snapshots do
				if index == 1 then
					continue
				end

				table.insert(payloads, {
					type = "patch",
					data = patch.diff(snapshots[index - 1], snapshot, autoSerialize),
				})
			end

			snapshots = { snapshots[#snapshots] }
			changed = false

			sync(player, unpack(payloads))
		end

		return self
	end

	local previousSnapshot = createSnapshot()
	local currentSnapshot = table.clone(previousSnapshot)
	local changed = false

	function self:connect(callback)
		local subscriptions = {}

		sync = callback

		-- Subscribe to each atom and update the state when one changes.
		for key, atom in next, atoms do
			subscriptions[key] = Charm.subscribe(atom, function(state)
				currentSnapshot[key] = state
				changed = true
			end)
		end

		local disconnect = interval(function()
			if not changed then
				return
			end

			local payload: SyncPayload = {
				type = "patch",
				data = patch.diff(previousSnapshot, currentSnapshot, autoSerialize),
			}

			previousSnapshot = table.clone(currentSnapshot)
			changed = false

			for _, player in next, Players:GetPlayers() do
				callback(player, payload)
			end
		end, syncRate)

		return function()
			disconnect()

			for _, unsubscribe in next, subscriptions do
				unsubscribe()
			end
		end
	end

	function self:_sendPatch(player)
		assert(sync, "connect() must be called before _sendPatch()")

		if not changed then
			return
		end

		local payload: SyncPayload = {
			type = "patch",
			data = patch.diff(previousSnapshot, currentSnapshot, autoSerialize),
		}

		previousSnapshot = table.clone(currentSnapshot)
		changed = false

		sync(player, payload)
	end

	return self
end

return server


local Charm = require(script.Parent.Parent.Charm)
type Atom<T> = Charm.Atom<T>

--[=[
	A payload that can be sent from the server to the client to synchronize
	state between the two.
]=]
export type SyncPayload = {
	type: "init",
	data: { [string | number]: any },
} | {
	type: "patch",
	data: { [string | number]: any },
}

export type AtomMap = {
	[string | number]: Atom<any>,
}

return nil


local SAFE_KEYS = { string = true, number = true }
local UNSAFE_VALUES = { ["function"] = true, thread = true }

local function isTableUnsafe(object: { [any]: any })
	local keyType = nil
	local objectSize = 0

	-- All keys must have the same type
	for key in next, object do
		local currentType = type(key)

		if not keyType and SAFE_KEYS[currentType] then
			keyType = currentType
		elseif keyType ~= currentType then
			return true
		end

		objectSize += 1
	end

	-- If there are more keys than the length of the array, it's an array with
	-- non-sequential keys.
	if objectSize > #object and keyType == "number" then
		return true
	end

	return false
end

--[=[
	Validates a value to ensure it can be synced over a remote event.

	@param value The value to validate.
	@param key The key of the value in the table.
	@error Throws an error if the value cannot be synced.
]=]
local function validate(value: any, key: any)
	local typeOfKey = type(key)
	local typeOfValue = type(value)

	if not SAFE_KEYS[typeOfKey] then
		error(`Invalid key type '{typeOfKey}' at key '{key}'`)
	elseif UNSAFE_VALUES[typeOfValue] then
		error(`Invalid value type '{typeOfValue}' at key '{key}'`)
	elseif typeOfValue == "table" then
		if getmetatable(value) ~= nil then
			error(`Cannot sync tables with metatables! Got {value} at key '{key}'`)
		elseif isTableUnsafe(value) then
			error(
				`Cannot sync tables unsupported by remote events! The value has the key '{key}'.\n\n`
					.. "This can be for the following reasons:\n"
					.. "1. The object is an array with non-sequential keys\n"
					.. "2. The object is a dictionary with mixed key types (e.g. string and number)\n\n"
					.. "Read more: https://create.roblox.com/docs/scripting/events/remote#argument-limitations"
			)
		end
	end

	if typeOfKey == "number" then
		if key == math.huge or key == -math.huge then
			error("Cannot sync infinity as key")
		elseif key ~= math.floor(key) then
			error("Cannot sync non-integer number as key")
		end
	end
end

return validate


local store = require(script.Parent.store)
local types = require(script.Parent.types)
type Atom<T> = types.Atom<T>
type AtomOptions<T> = types.AtomOptions<T>

--[=[
	Creates a new atom with the given state.

	@param state The initial state.
	@param options Optional configuration.
	@return A new atom.
]=]
local function atom<T>(state: T, options: AtomOptions<T>?): Atom<T>
	local equals = options and options.equals

	local function atom(...)
		if select("#", ...) == 0 then
			local index = store.capturing.index

			if index > 0 then
				store.capturing.stack[index][atom] = true
			end

			return state
		end

		local nextState = store.peek(..., state)

		if state ~= nextState and not (equals and equals(state, nextState)) then
			state = nextState
			store.notify(atom)
		end

		return state
	end

	store.listeners[atom] = setmetatable({}, { __mode = "v" })

	return atom
end

return atom


local atom = require(script.Parent.atom)
local store = require(script.Parent.store)
local types = require(script.Parent.types)
type AtomOptions<T> = types.AtomOptions<T>
type Selector<T> = types.Selector<T>

--[=[
	Creates a read-only atom that derives its state from one or more atoms.
	Used to avoid unnecessary recomputations if multiple listeners depend on
	the same atoms.

	@param callback The function that produces the state.
	@param options Optional configuration.
	@return A new read-only atom.
]=]
local function computed<T>(callback: Selector<T>, options: AtomOptions<T>?): Selector<T>
	local dependencies, state = store.capture(callback)
	local computedAtom = atom(state, options)
	local computedRef = setmetatable({ current = computedAtom }, { __mode = "v" })

	local function listener()
		local computedAtom = computedRef.current

		if computedAtom then
			store.disconnect(dependencies, listener)
			dependencies, state = store.capture(callback)
			store.connect(dependencies, listener, computedAtom)
			computedAtom(state)
		end
	end

	store.connect(dependencies, listener, computedAtom)

	return computedAtom
end

return computed


local store = require(script.Parent.store)

type Cleanup = () -> ()

--[=[
	Runs the given callback immediately and whenever any atom it depends on
	changes. Returns a cleanup function that unsubscribes the callback.

	@param callback The function to run.
	@return A function that unsubscribes the callback.
]=]
local function effect(callback: (cleanup: Cleanup) -> Cleanup?): Cleanup
	local dependencies = {}
	local cleanup: Cleanup?
	local disconnected = false
	local disconnect

	local function listener()
		if cleanup then
			cleanup()
		end

		store.disconnect(dependencies, listener)
		dependencies, cleanup = store.capture(callback, disconnect)

		if not disconnected then
			store.connect(dependencies, listener)
		end
	end

	function disconnect()
		if disconnected then
			return
		end

		disconnected = true
		store.disconnect(dependencies, listener)

		if cleanup then
			cleanup()
		end
	end

	dependencies, cleanup = store.capture(callback, disconnect)

	if not disconnected then
		store.connect(dependencies, listener)
	end

	return disconnect
end

return effect :: (callback: ((cleanup: Cleanup) -> ()) | ((cleanup: Cleanup) -> Cleanup?)) -> Cleanup


local atom = require(script.atom)
local computed = require(script.computed)
local effect = require(script.effect)
local mapped = require(script.mapped)
local observe = require(script.observe)
local store = require(script.store)
local subscribe = require(script.subscribe)
local types = require(script.types)

export type Atom<State> = types.Atom<State>
export type Selector<State> = types.Selector<State>
export type Molecule<State> = types.Selector<State>
export type SyncPayload = types.SyncPayload

return {
	atom = atom,
	computed = computed,
	effect = effect,
	mapped = mapped,
	observe = observe,
	subscribe = subscribe,
	batch = store.batch,
	capture = store.capture,
	isAtom = store.isAtom,
	notify = store.notify,
	peek = store.peek,
}


local atom = require(script.Parent.atom)
local store = require(script.Parent.store)
local subscribe = require(script.Parent.subscribe)
local types = require(script.Parent.types)
type Atom<T> = types.Atom<T>
type Selector<T> = types.Selector<T>

type Map =
	(<K0, V0, K1, V1>(fn: Selector<{ [K0]: V0 }>, mapper: (V0, K0) -> (V1?, K1)) -> Selector<{ [K1]: V1 }>)
	& (<K0, V0, V1>(fn: Selector<{ [K0]: V0 }>, mapper: (V0, K0) -> V1?) -> Selector<{ [K0]: V1 }>)
	& (<K0, V0, K1, V1>(fn: Selector<{ [K0]: V0 }>, mapper: (V0, K0) -> (V1?, K1?)) -> Selector<{ [K1]: V1 }>)

--[=[
	Maps each entry in the atom's state to a new key-value pair. If the `mapper`
	function returns `undefined`, the entry is omitted from the resulting map.
	When the atom changes, the `mapper` is called for each entry in the state
	to compute the new state.

	@param callback The atom or selector to map.
	@param mapper The function that maps each entry.
	@return A new atom with the mapped state.
]=]
local function mapped<K0, V0, K1, V1>(callback: Selector<{ [K0]: V0 }>, mapper: (V0, K0) -> (V1?, K1?)): Selector<{ [K1]: V1 }>
	local mappedAtom = atom({})
	local mappedAtomRef = setmetatable({ current = mappedAtom }, { __mode = "v" })
	local prevMappedItems: { [K1]: V1 } = {}
	local unsubscribe

	local function listener(items: { [K0]: V0 })
		local mappedAtom = mappedAtomRef.current

		if not mappedAtom then
			return unsubscribe()
		end

		local mappedItems = table.clone(mappedAtom())
		local mappedKeys = {}

		-- TODO: Only call mapper if the item has changed.
		for key, item in next, items do
			local newItem, newKey = mapper(item, key)
			if newKey == nil then
				newKey = key :: any
			end
			if mappedItems[newKey :: K1] ~= newItem then
				mappedItems[newKey :: K1] = newItem :: V1
			else
				mappedKeys[newKey] = key
			end
		end

		for key in next, prevMappedItems do
			if mappedKeys[key] == nil and mappedItems[key] == prevMappedItems[key] then
				mappedItems[key] = nil
			end
		end

		prevMappedItems = mappedItems
		mappedAtom(mappedItems)
	end

	unsubscribe = subscribe(callback, listener)

	store.peek(function(): ()
		listener(callback())
	end)

	return mappedAtom
end

return mapped :: Map


local store = require(script.Parent.store)
local subscribe = require(script.Parent.subscribe)
local types = require(script.Parent.types)
type Selector<T> = types.Selector<T>

local function noop() end

--[=[
	Creates an instance of `factory` for each item in the atom's state, and
	cleans up the instance when the item is removed. Returns a cleanup function
	that unsubscribes all instances.

	@param callback The atom or selector to observe.
	@param factory The function that tracks the lifecycle of each item.
	@return A function that unsubscribes all instances.
]=]
local function observe<K, V>(callback: Selector<{ [K]: V }>, factory: (value: V, key: K) -> (() -> ())?): () -> ()
	local connections: { [K]: () -> () } = {}

	local function listener(state: { [K]: V })
		for key, disconnect in next, connections do
			if state[key] == nil then
				connections[key] = nil
				disconnect()
			end
		end

		for key, value in next, state do
			if not connections[key] then
				connections[key] = factory(value, key) or noop
			end
		end
	end

	local unsubscribe = subscribe(callback, listener)

	store.peek(function(): ()
		listener(callback())
	end)

	return function()
		unsubscribe()
		for _, disconnect in next, connections do
			disconnect()
		end
		table.clear(connections)
	end
end

return observe


local types = require(script.Parent.types)
type Atom<T> = types.Atom<T>
type Set<T> = types.Set<T>
type WeakMap<K, V> = types.WeakMap<K, V>

local __DEV__ = _G.__DEV__

local listeners: WeakMap<Atom<any>, WeakMap<() -> (), unknown>> = setmetatable({}, { __mode = "k" })

local batched: Set<() -> ()> = {}
local batching = false

local capturing = {
	stack = {} :: { Set<Atom<any>> },
	index = 0,
}

--[=[
	Calls the given function and returns the result. If the function yields or
	throws an error, the thread is closed and an error is thrown. Regardless of
	the outcome, the `finally` function is called to clean up any resources.

	@param callback The function to run.
	@param finally Cleanup logic to run before error handling.
	@param ... Arguments to pass to the callback.
	@return The result of the callback.
]=]
local function try<T, U...>(callback: (U...) -> T | any, finally: (() -> ())?, ...: U...): T
	if __DEV__ then
		local thread = coroutine.create(callback)
		local success, result = coroutine.resume(thread, ...)

		if finally then
			finally()
		end

		if coroutine.status(thread) == "suspended" then
			local source, line, name = debug.info(callback, "sln")

			coroutine.close(thread)

			error(
				"Yielding is not allowed in atom functions. Consider wrapping this code in a Promise or task.defer instead."
					.. `\nFunction defined at: {source}:{line}`
					.. if name == "" then "" else ` function {name}`
			)
		elseif not success then
			local source, line, name = debug.info(callback, "sln")

			error(
				"An error occurred while running an atom function"
					.. `\nFunction defined at: {source}:{line}`
					.. (if name == "" then "" else ` function {name}`)
					.. `\nError: {result}`
			)
		end

		return result
	end

	if not finally then
		return callback(...)
	end

	local success, result = pcall(callback, ...)

	finally()
	assert(success, result)

	return result
end

--[=[
	Returns whether the given value is an atom.

	@param value The value to check.
	@return `true` if the value is an atom, otherwise `false`.
]=]
local function isAtom(value: any): boolean
	return not not (value and listeners[value])
end

--[=[
	Notifies all subscribers of the given atom that the state has changed.

	@param atom The atom to notify.
]=]
local function notify(atom: Atom<any>)
	if batching then
		for listener in next, listeners[atom] do
			batched[listener] = true
		end
		return
	end

	for listener in next, table.clone(listeners[atom]) do
		try(listener)
	end
end

--[=[
	Returns the result of the function without subscribing to changes. If a
	non-function value is provided, it is returned as is.

	@param callback The atom or selector to get the state of.
	@param args Arguments to pass to the function.
	@return The current state.
]=]
local function peek<T, U...>(callback: ((U...) -> T) | T, ...: U...): T
	if type(callback) ~= "function" then
		return callback
	end

	if capturing.index == 0 then
		return callback(...)
	end

	capturing.index += 1
	capturing.stack[capturing.index] = {}

	local result = try(callback, function()
		capturing.stack[capturing.index] = nil
		capturing.index -= 1
	end, ...)

	return result
end

--[=[
	Captures all atoms that are read during the function call and returns them
	along with the result of the function. Useful for tracking dependencies.

	@param callback The function to run.
	@return A tuple containing the captured atoms and the result of the function.
]=]
local function capture<T, U...>(callback: (U...) -> T, ...: U...): (Set<Atom<any>>, T)
	-- If the callback is an atom, return it immediately
	if listeners[callback :: any] then
		return { [callback :: any] = true }, peek(callback)
	end

	local dependencies: Set<Atom<any>> = {}

	capturing.index += 1
	capturing.stack[capturing.index] = dependencies

	local result = try(callback, function()
		capturing.stack[capturing.index] = nil
		capturing.index -= 1
	end, ...)

	return dependencies, result
end

--[=[
	Runs the given function and schedules listeners to be notified only once
	after the function has completed. Useful for batching multiple changes.

	@param callback The function to run.
]=]
local function batch(callback: () -> ())
	if batching then
		return callback()
	end

	batching = true

	try(callback, function()
		batching = false
	end)

	for listener in next, batched do
		try(listener)
	end

	table.clear(batched)
end

--[=[
	Subscribes the listener to the changes of the given atoms.

	@param atoms The atoms to listen to.
	@param listener The function to call when the atoms change.
	@param ref Optionally bind the lifetime of the listener to a value.
]=]
local function connect(atoms: Set<Atom<any>>, listener: () -> (), ref: unknown?)
	for atom in next, atoms do
		listeners[atom][listener] = ref or true
	end
end

--[=[
	Unsubscribes the listener from every atom it was connected to.

	@param atoms The atoms to stop listening to.
	@param listener The function to stop calling when the atoms change.
]=]
local function disconnect(atoms: Set<Atom<any>>, listener: () -> ())
	for atom in next, atoms do
		listeners[atom][listener] = nil
	end
end

return {
	listeners = listeners,
	capturing = capturing,
	isAtom = isAtom,
	notify = notify,
	capture = capture,
	batch = batch,
	peek = peek,
	connect = connect,
	disconnect = disconnect,
}


local store = require(script.Parent.store)
local types = require(script.Parent.types)
type Selector<T> = types.Selector<T>

--[=[
	Subscribes to changes in the given atom or selector. The callback is
	called with the current state and the previous state immediately after a
	change occurs.

	@param callback The atom or selector to subscribe to.
	@param listener The function to call when the state changes.
	@return A function that unsubscribes the callback.
]=]
local function subscribe<T>(callback: Selector<T>, listener: (state: T, prev: T) -> ()): () -> ()
	local dependencies, state = store.capture(callback)
	local disconnected = false

	local function handler()
		local prevState = state

		store.disconnect(dependencies, handler)
		dependencies, state = store.capture(callback)

		if not disconnected then
			store.connect(dependencies, handler)
		end

		if state ~= prevState then
			listener(state, prevState)
		end
	end

	store.connect(dependencies, handler)

	return function()
		if not disconnected then
			disconnected = true
			store.disconnect(dependencies, handler)
		end
	end
end

return subscribe


export type Set<T> = { [T]: true }

export type WeakMap<K, V> = typeof(setmetatable({} :: { [K]: V }, { __mode = "k" }))

--[=[
	A primitive state container that can be read from and written to. When the
	state changes, all subscribers are notified.

	@param state The next state or a function that produces the next state.
	@return The current state, if no arguments are provided.
]=]
export type Atom<T> = (state: (T | (T) -> T)?) -> T

--[=[
	A function that depends on one or more atoms and produces a state. Can be
	used to derive state from atoms.

	@template State The type of the state.
	@return The current state.
]=]
export type Selector<T> = () -> T

--[=[
	Optional configuration for creating an atom.
]=]
export type AtomOptions<T> = {
	--[=[
		A function that determines whether the state has changed. By default,
		a strict equality check (`===`) is used.
	]=]
	equals: (prev: T, next: T) -> boolean,
}

--[=[
	A payload that can be sent from the server to the client to synchronize
	state between the two.
]=]
export type SyncPayload = {
	type: "patch" | "init",
	data: { [any]: any },
}

return nil


local Charm = require(script.Parent.Charm)
local React = if script.Parent:FindFirstChild("react")
	then require(script.Parent.react) :: never
	else require(script.Parent.React)

--[=[
	A hook that subscribes to changes in the given atom or selector. The
	component is re-rendered whenever the state changes.

	If the `dependencies` array is provided, the subscription to the atom or
	selector is re-created whenever the dependencies change. Otherwise, the
	subscription is created once when the component is mounted.

	@param callback The atom or selector to subscribe to.
	@param dependencies An array of values that the subscription depends on.
	@return The current state.
]=]
local function useAtom<State>(callback: Charm.Selector<State>, dependencies: { any }?): State
	local state, setState = React.useState(callback)

	React.useEffect(function()
		setState(callback())
		return Charm.subscribe(callback, setState)
	end, dependencies or {})

	return state
end

return {
	useAtom = useAtom,
}


local Charm = require(script.Parent.Charm)
local Vide = if script.Parent:FindFirstChild("vide")
	then require(script.Parent.vide.src) :: never
	else require(script.Parent.Vide)

--[=[
	Subscribes to the state of an atom and returns a Vide source.

	@param callback The atom or selector to subscribe to.
	@return The reactive source.
]=]
local function useAtom<State>(callback: () -> State): () -> State
	local state = Vide.source(callback())
	local unsubscribe = Charm.subscribe(callback, function(value)
		task.spawn(state :: (State) -> State, value)
	end)

	Vide.cleanup(unsubscribe)

	return state
end

return {
	useAtom = useAtom,
}


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Charm = require(ReplicatedStorage.DevPackages.Charm)
local computed = Charm.computed
local atom = Charm.atom

return {
	ParameterGenerator = function() end,
	Functions = {
		atom = function()
			local state = atom(0)

			for _ = 1, 500 do
				state(math.random(1, 2))
			end
		end,

		computed = function()
			local state = atom(0)

			local _double = computed(function()
				return 2 * state()
			end)

			for _ = 1, 500 do
				state(math.random(1, 2))
			end
		end,
	},
}


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local patch = require(ReplicatedStorage.DevPackages.CharmSync.patch)

local USERS = 1000

local function generate()
	local users = {}

	for user = 1, USERS do
		users[user] = {
			coins = math.random(1, 2),
			gems = math.random(1, 2),
		}
	end

	return { users = users }
end

local function update(state)
	state = table.clone(state)
	state.users = table.clone(state.users)

	for user = 1, USERS do
		if math.random() > 0.5 then
			continue
		end

		state.users[user] = {
			coins = math.random(1, 2),
			gems = math.random(1, 2),
		}
	end

	return state
end

return {
	ParameterGenerator = function()
		local state = generate()
		return state, update(state)
	end,
	Functions = {
		diff = function(_, a, b)
			patch.diff(a, b)
		end,
	},
}


--!nocheck

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local TestEZ = require(ReplicatedStorage.DevPackages.TestEZ)

-- Start Charm in debug mode
_G.__DEV__ = true
require(ReplicatedStorage.DevPackages.Charm)
require(ReplicatedStorage.DevPackages.CharmSync)
_G.__DEV__ = false

TestEZ.TestBootstrap:run({ script.Parent }, TestEZ.Reporters.TextReporter)


--[=[
	Trigger a garbage collection cycle by allocating a large amount of junk memory
	and then waiting for the next frame.
]=]
local function collect()
	for _ = 1, 1e4 do
		local _ = table.create(1e3)
	end
	task.wait()
end

return collect


--[=[
	Counts the number of elements in a table.

	@param object The table to count.
	@return The number of elements in the table.
]=]
local function count(object: any): number
	local count = 0
	for _ in next, object do
		count += 1
	end
	return count
end

return count


local ReplicatedStorage = game:GetService("ReplicatedStorage")

_G.__ROACT_17_MOCK_SCHEDULER__ = true

local React = require(ReplicatedStorage.DevPackages.React)
local ReactRoblox = require(ReplicatedStorage.DevPackages.ReactRoblox)

type RenderedHook<Props, Result> = {
	result: Result,
	rerender: (Props?) -> (),
	unmount: () -> (),
}

local function renderHook<Props, Result>(callback: (Props) -> Result, props: Props?): RenderedHook<Props, Result>
	local root = ReactRoblox.createLegacyRoot(Instance.new("Folder"))
	local hook = {}

	local function Component(props)
		hook.result = callback(props)
	end

	function hook.rerender(newProps)
		props = newProps or props
		ReactRoblox.act(function()
			root:render(React.createElement(Component, props))
		end)
	end

	function hook.unmount()
		ReactRoblox.act(function()
			root:unmount()
		end)
	end

	ReactRoblox.act(function()
		hook.rerender(props)
	end)

	return hook
end

return renderHook


return {
	Name = "announce",
	Aliases = { "m" },
	Description = "Makes a server-wide announcement.",
	Group = "DefaultAdmin",
	Args = {
		{
			Type = "string",
			Name = "text",
			Description = "The announcement text.",
		},
	},
}


return {
	Name = "goto-place",
	Aliases = {},
	Description = "Teleport to a Roblox place",
	Group = "DefaultAdmin",
	AutoExec = {
		'alias "follow-player|Join a player in another server" goto-place $1{players|Players} ${{get-player-place-instance $2{playerId|Target}}}',
		'alias "rejoin|Rejoin this place. You might end up in a different server." goto-place $1{players|Players} ${get-player-place-instance ${me} PlaceId}',
	},
	Args = {
		{
			Type = "players",
			Name = "Players",
			Description = "The players you want to teleport",
		},
		{
			Type = "positiveInteger",
			Name = "Place ID",
			Description = "The Place ID you want to teleport to",
		},
		{
			Type = "string",
			Name = "JobId",
			Description = "The specific JobId you want to teleport to",
			Optional = true,
		},
	},
}


return {
	Name = "kick",
	Aliases = { "boot" },
	Description = "Kicks a player or set of players.",
	Group = "DefaultAdmin",
	Args = {
		{
			Type = "players",
			Name = "players",
			Description = "The players to kick.",
		},
	},
}


return function(_, players)
	for _, player in pairs(players) do
		player:Kick("Kicked by admin.")
	end

	return ("Kicked %d players."):format(#players)
end


return {
	Name = "kill",
	Aliases = { "slay" },
	Description = "Kills a player or set of players.",
	Group = "DefaultAdmin",
	Args = {
		{
			Type = "players",
			Name = "victims",
			Description = "The players to kill.",
		},
	},
}


return function(_, players)
	for _, player in pairs(players) do
		if player.Character then
			player.Character:BreakJoints()
		end
	end

	return ("Killed %d players."):format(#players)
end


return {
	Name = "teleport",
	Aliases = { "tp" },
	Description = "Teleports a player or set of players to one target.",
	Group = "DefaultAdmin",
	AutoExec = {
		'alias "bring|Brings a player or set of players to you." teleport $1{players|players|The players to bring} ${me}',
		'alias "to|Teleports you to another player or location." teleport ${me} $1{player @ vector3|Destination|The player or location to teleport to}',
	},
	Args = {
		{
			Type = "players",
			Name = "From",
			Description = "The players to teleport",
		},
		{
			Type = "player @ vector3",
			Name = "Destination",
			Description = "The player to teleport to",
		},
	},
}


return function(_, fromPlayers, destination)
	local cframe

	if typeof(destination) == "Instance" then
		if destination.Character and destination.Character:FindFirstChild("HumanoidRootPart") then
			cframe = destination.Character.HumanoidRootPart.CFrame
		else
			return "Target player has no character."
		end
	elseif typeof(destination) == "Vector3" then
		cframe = CFrame.new(destination)
	end

	for _, player in ipairs(fromPlayers) do
		if player.Character and player.Character:FindFirstChild("HumanoidRootPart") then
			if player.Character.Humanoid.Sit then
				player.Character.Humanoid.Jump = true
			end
			player.Character.HumanoidRootPart.CFrame = cframe
		end
	end

	return ("Teleported %d players."):format(#fromPlayers)
end


return {
	Name = "fetch",
	Aliases = {},
	Description = "Fetch a value from the Internet",
	Group = "DefaultDebug",
	Args = {
		{
			Type = "url",
			Name = "URL",
			Description = "The URL to fetch.",
		},
	},
}


local HttpService = game:GetService("HttpService")

return function(_, url)
	return HttpService:GetAsync(url)
end


return {
	Name = "get-player-place-instance",
	Aliases = {},
	Description = "Returns the target player's Place ID and the JobId separated by a space. Returns 0 if the player is offline or something else goes wrong.",
	Group = "DefaultDebug",
	Args = {
		{
			Type = "playerId",
			Name = "Player",
			Description = "Get the place instance of this player",
		},
		function(context)
			return {
				Type = context.Cmdr.Util.MakeEnumType("PlaceInstance Format", { "PlaceIdJobId", "PlaceId", "JobId" }),
				Name = "Format",
				Description = "What data to return. PlaceIdJobId returns both separated by a space.",
				Default = "PlaceIdJobId",
			}
		end,
	},
}


local TeleportService = game:GetService("TeleportService")

return function(_, playerId, format)
	format = format or "PlaceIdJobId"

	local ok, _, errorText, placeId, jobId = pcall(function()
		return TeleportService:GetPlayerPlaceInstanceAsync(playerId)
	end)

	if not ok or (errorText and #errorText > 0) then
		if format == "PlaceIdJobId" then
			return "0" .. " -"
		elseif format == "PlaceId" then
			return "0"
		elseif format == "JobId" then
			return "-"
		end
	end

	if format == "PlaceIdJobId" then
		return placeId .. " " .. jobId
	elseif format == "PlaceId" then
		return tostring(placeId)
	elseif format == "JobId" then
		return tostring(jobId)
	end
end


local Players = game:GetService("Players")

return {
	Name = "position",
	Aliases = { "pos" },
	Description = "Returns Vector3 position of you or other players. Empty string is the player has no character.",
	Group = "DefaultDebug",
	Args = {
		{
			Type = "player",
			Name = "Player",
			Description = "The player to report the position of. Omit for your own position.",
			Default = Players.LocalPlayer,
		},
	},

	ClientRun = function(_, player)
		local character = player.Character

		if not character or not character:FindFirstChild("HumanoidRootPart") then
			return ""
		end

		return tostring(character.HumanoidRootPart.Position):gsub("%s", "")
	end,
}


return {
	Name = "uptime",
	Aliases = {},
	Description = "Returns the amount of time the server has been running.",
	Group = "DefaultDebug",
	Args = {},
}


local startTime = os.time()

return function()
	local uptime = os.time() - startTime
	return ("%dd %dh %dm %ds"):format(
		math.floor(uptime / (60 * 60 * 24)),
		math.floor(uptime / (60 * 60)) % 24,
		math.floor(uptime / 60) % 60,
		math.floor(uptime) % 60
	)
end


local version = "v1.12.0"

return {
	Name = "version",
	Args = {},
	Description = "Shows the current version of Cmdr",
	Group = "DefaultDebug",

	ClientRun = function()
		return ("Cmdr Version %s"):format(version)
	end,
}


return {
	Name = "alias",
	Aliases = {},
	Description = "Creates a new, single command out of a command and given arguments.",
	Group = "DefaultUtil",
	Args = {
		{
			Type = "string",
			Name = "Alias name",
			Description = "The key or input type you'd like to bind the command to.",
		},
		{
			Type = "string",
			Name = "Command string",
			Description = 'The command text you want to run. Separate multiple commands with "&&". Accept arguments with $1, $2, $3, etc.',
		},
	},

	ClientRun = function(context, name, commandString)
		context.Cmdr.Registry:RegisterCommandObject(context.Cmdr.Util.MakeAliasCommand(name, commandString), true)

		return ("Created alias %q"):format(name)
	end,
}


local UserInputService = game:GetService("UserInputService")
local TextChatService = game:GetService("TextChatService")

return {
	Name = "bind",
	Aliases = {},
	Description = "Binds a command string to a key or mouse input.",
	Group = "DefaultUtil",
	Args = {
		{
			Type = "userInput ! bindableResource @ player",
			Name = "Input",
			Description = "The key or input type you'd like to bind the command to.",
		},
		{
			Type = "command",
			Name = "Command",
			Description = "The command you want to run on this input",
		},
		{
			Type = "string",
			Name = "Arguments",
			Description = "The arguments for the command",
			Default = "",
		},
	},

	ClientRun = function(context, bind, command, arguments)
		local binds = context:GetStore("CMDR_Binds")

		command = command .. " " .. arguments

		if binds[bind] then
			binds[bind]:Disconnect()
		end

		local bindType = context:GetArgument(1).Type.Name

		if bindType == "userInput" then
			binds[bind] = UserInputService.InputBegan:Connect(function(input, gameProcessed)
				if gameProcessed then
					return
				end

				if input.UserInputType == bind or input.KeyCode == bind then
					context:Reply(
						context.Dispatcher:EvaluateAndRun(
							context.Cmdr.Util.RunEmbeddedCommands(context.Dispatcher, command)
						)
					)
				end
			end)
		elseif bindType == "bindableResource" then
			return "Unimplemented..."
		elseif bindType == "player" then
			local function RunCommand(message)
				local args = { message }
				local chatCommand = context.Cmdr.Util.RunEmbeddedCommands(
					context.Dispatcher,
					context.Cmdr.Util.SubstituteArgs(command, args)
				)
				context:Reply(
					("%s $ %s : %s"):format(bind.Name, chatCommand, context.Dispatcher:EvaluateAndRun(chatCommand)),
					Color3.fromRGB(244, 92, 66)
				)
			end

			if TextChatService.ChatVersion == Enum.ChatVersion.LegacyChatService then
				binds[bind] = bind.Chatted:Connect(RunCommand)
			else
				binds[bind] = TextChatService.SendingMessage:Connect(function(message)
					RunCommand(message.Text)
				end)
			end
		end

		return "Bound command to input."
	end,
}


local Players = game:GetService("Players")

return {
	Name = "clear",
	Aliases = {},
	Description = "Clear all lines above the entry line of the Cmdr window.",
	Group = "DefaultUtil",
	Args = {},
	ClientRun = function()
		local player = Players.LocalPlayer
		local gui = player:WaitForChild("PlayerGui"):WaitForChild("Cmdr")
		local frame = gui:WaitForChild("Frame")

		if gui and frame then
			for _, child in pairs(frame:GetChildren()) do
				if child.Name == "Line" and child:IsA("TextBox") then
					child:Destroy()
				end
			end
		end
		return ""
	end,
}


return {
	Name = "convertTimestamp",
	Aliases = { "date" },
	Description = "Convert a timestamp to a human-readable format.",
	Group = "DefaultUtil",
	Args = {
		{
			Type = "number",
			Name = "timestamp",
			Description = "A numerical representation of a specific moment in time.",
			Optional = true,
		},
	},
	ClientRun = function(_, timestamp)
		timestamp = timestamp or os.time()
		return `{os.date("%x", timestamp)} {os.date("%X", timestamp)}`
	end,
}


return {
	Name = "echo",
	Aliases = { "=" },
	Description = "Echoes your text back to you.",
	Group = "DefaultUtil",
	Args = {
		{
			Type = "string",
			Name = "Text",
			Description = "The text.",
		},
	},

	ClientRun = function(_, text)
		return text
	end,
}


local Players = game:GetService("Players")

local TEXT_BOX_PROPERTIES = {
	AnchorPoint = Vector2.new(0.5, 0.5),
	BackgroundColor3 = Color3.fromRGB(17, 17, 17),
	BackgroundTransparency = 0.05,
	BorderColor3 = Color3.fromRGB(17, 17, 17),
	BorderSizePixel = 20,
	ClearTextOnFocus = false,
	MultiLine = true,
	Position = UDim2.new(0.5, 0, 0.5, 0),
	Size = UDim2.new(0.5, 0, 0.4, 0),
	Font = Enum.Font.Code,
	TextColor3 = Color3.fromRGB(241, 241, 241),
	TextWrapped = true,
	TextSize = 18,
	TextXAlignment = "Left",
	TextYAlignment = "Top",
	AutoLocalize = false,
	PlaceholderText = "Right click to exit",
}

local lock

return {
	Name = "edit",
	Aliases = {},
	Description = "Edit text in a TextBox",
	Group = "DefaultUtil",
	Args = {
		{
			Type = "string",
			Name = "Input text",
			Description = "The text you wish to edit",
			Default = "",
		},
		{
			Type = "string",
			Name = "Delimiter",
			Description = "The character that separates each line",
			Default = ",",
		},
	},

	ClientRun = function(context, text, delimeter)
		lock = lock or context.Cmdr.Util.Mutex()

		local unlock = lock()

		context:Reply("Right-click on the text area to exit.", Color3.fromRGB(158, 158, 158))

		local screenGui = Instance.new("ScreenGui")
		screenGui.Name = "CmdrEditBox"
		screenGui.ResetOnSpawn = false

		local textBox = Instance.new("TextBox")

		for key, value in pairs(TEXT_BOX_PROPERTIES) do
			textBox[key] = value
		end

		textBox.Text = text:gsub(delimeter, "\n")
		textBox.Parent = screenGui

		screenGui.Parent = Players.LocalPlayer:WaitForChild("PlayerGui")

		local thread = coroutine.running()

		textBox.InputBegan:Connect(function(input)
			if input.UserInputType == Enum.UserInputType.MouseButton2 then
				coroutine.resume(thread, textBox.Text:gsub("\n", delimeter))
				screenGui:Destroy()
				unlock()
			end
		end)

		return coroutine.yield()
	end,
}


return {
	Name = "history",
	Aliases = {},
	AutoExec = {
		'alias "!|Displays previous command from history." run ${history $1{number|Line Number}}',
		'alias "^|Runs the previous command, replacing all occurrences of A with B." run ${run replace ${history -1} $1{string|A} $2{string|B}}',
		'alias "!!|Reruns the last command." ! -1',
	},
	Description = "Displays previous commands from history.",
	Group = "DefaultUtil",
	Args = {
		{
			Type = "integer",
			Name = "Line Number",
			Description = "Command line number (can be negative to go from end)",
		},
	},

	ClientRun = function(context, line)
		local history = context.Dispatcher:GetHistory()

		if line <= 0 then
			line = #history + line
		end

		return history[line] or ""
	end,
}


local Players = game:GetService("Players")

return {
	Name = "hover",
	Description = "Returns the name of the player you are hovering over.",
	Group = "DefaultUtil",
	Args = {},

	ClientRun = function()
		local mouse = Players.LocalPlayer:GetMouse()
		local target = mouse.Target

		if not target then
			return ""
		end

		local p = Players:GetPlayerFromCharacter(target:FindFirstAncestorOfClass("Model"))

		return p and p.Name or ""
	end,
}


return {
	Name = "json-array-decode",
	Aliases = {},
	Description = "Decodes a JSON Array into a comma-separated list",
	Group = "DefaultUtil",
	Args = {
		{
			Type = "json",
			Name = "JSON",
			Description = "The JSON array.",
		},
	},

	ClientRun = function(_, value)
		if type(value) ~= "table" then
			value = { value }
		end

		return table.concat(value, ",")
	end,
}


local HttpService = game:GetService("HttpService")

return {
	Name = "json-array-encode",
	Aliases = {},
	Description = "Encodes a comma-separated list into a JSON array",
	Group = "DefaultUtil",
	Args = {
		{
			Type = "string",
			Name = "CSV",
			Description = "The comma-separated list",
		},
	},

	ClientRun = function(_, text)
		return HttpService:JSONEncode(text:split(","))
	end,
}


return {
	Name = "len",
	Aliases = {},
	Description = "Returns the length of a comma-separated list",
	Group = "DefaultUtil",
	Args = {
		{
			Type = "string",
			Name = "CSV",
			Description = "The comma-separated list",
		},
	},

	ClientRun = function(_, list)
		return #(list:split(","))
	end,
}


return {
	Name = "math",
	Aliases = {},
	Description = "Perform a math operation on 2 values.",
	Group = "DefaultUtil",
	AutoExec = {
		'alias "+|Perform an addition." math + $1{number|Number} $2{number|Number}',
		'alias "-|Perform a subtraction." math - $1{number|Number} $2{number|Number}',
		'alias "*|Perform a multiplication." math * $1{number|Number} $2{number|Number}',
		'alias "/|Perform a division." math / $1{number|Number} $2{number|Number}',
		'alias "**|Perform an exponentiation." math ** $1{number|Number} $2{number|Number}',
		'alias "%|Perform a modulus." math % $1{number|Number} $2{number|Number}',
	},
	Args = {
		{
			Type = "mathOperator",
			Name = "Operation",
			Description = "A math operation.",
		},
		{
			Type = "number",
			Name = "Value",
			Description = "A number value.",
		},
		{
			Type = "number",
			Name = "Value",
			Description = "A number value.",
		},
	},

	ClientRun = function(_, operation, a, b)
		return operation.Perform(a, b)
	end,
}


return {
	Name = "pick",
	Aliases = {},
	Description = "Picks a value out of a comma-separated list.",
	Group = "DefaultUtil",
	Args = {
		{
			Type = "positiveInteger",
			Name = "Index to pick",
			Description = "The index of the item you want to pick",
		},
		{
			Type = "string",
			Name = "CSV",
			Description = "The comma-separated list",
		},
	},

	ClientRun = function(_, index, list)
		return list:split(",")[index] or ""
	end,
}


local generator = Random.new()

return {
	Name = "rand",
	Aliases = {},
	Description = "Returns a random number between min and max",
	Group = "DefaultUtil",
	Args = {
		{
			Type = "integer",
			Name = "First number",
			Description = "If second number is nil, random number is between 1 and this value. If second number is provided, number is between this number and the second number.",
		},
		{
			Type = "integer",
			Name = "Second number",
			Description = "The upper bound.",
			Optional = true,
		},
	},

	ClientRun = function(_, firstNumber, secondNumber)
		return tostring(
			if secondNumber
				then generator:NextInteger(firstNumber, secondNumber)
				else generator:NextInteger(1, firstNumber)
		)
	end,
}


return {
	Name = "replace",
	Aliases = { "gsub", "//" },
	Description = "Replaces text A with text B",
	Group = "DefaultUtil",
	AutoExec = {
		'alias "map|Maps a CSV into another CSV" replace $1{string|CSV} ([^,]+) "$2{string|mapped value|Use %1 to insert the element}"',
		'alias "join|Joins a CSV with a specified delimiter" replace $1{string|CSV} , $2{string|Delimiter}',
	},
	Args = {
		{
			Type = "string",
			Name = "Haystack",
			Description = "The source string upon which to perform replacement.",
		},
		{
			Type = "string",
			Name = "Needle",
			Description = "The string pattern search for.",
		},
		{
			Type = "string",
			Name = "Replacement",
			Description = "The string to replace matches (%1 to insert matches).",
			Default = "",
		},
	},

	ClientRun = function(_, haystack, needle, replacement)
		return haystack:gsub(needle, replacement)
	end,
}


return {
	Name = "resolve",
	Aliases = {},
	Description = "Resolves Argument Value Operators into lists. E.g., resolve players * gives you a list of all players.",
	Group = "DefaultUtil",
	AutoExec = {
		'alias "me|Displays your username" resolve players .',
	},
	Args = {
		{
			Type = "type",
			Name = "Type",
			Description = "The type for which to resolve",
		},
		function(context)
			if context:GetArgument(1):Validate() == false then
				return
			end

			return {
				Type = context:GetArgument(1):GetValue(),
				Name = "Argument Value Operator",
				Description = "The value operator to resolve. One of: * ** . ? ?N",
				Optional = true,
			}
		end,
	},

	ClientRun = function(context)
		return table.concat(context:GetArgument(2).RawSegments, ",")
	end,
}


return {
	Name = "run",
	Aliases = { ">" },
	AutoExec = {
		'alias "discard|Run a command and discard the output." replace ${run $1} .* \\"\\"',
	},
	Description = "Runs a given command string (replacing embedded commands).",
	Group = "DefaultUtil",
	Args = {
		{
			Type = "string",
			Name = "Command",
			Description = "The command string to run",
		},
	},

	ClientRun = function(context, commandString)
		return context.Cmdr.Util.RunCommandString(context.Dispatcher, commandString)
	end,
}


return {
	Name = "run-lines",
	Aliases = {},
	Description = "Splits input by newlines and runs each line as its own command. This is used by the init-run command.",
	Group = "DefaultUtil",
	Args = {
		{
			Type = "string",
			Name = "Script",
			Description = "The script to parse.",
			Default = "",
		},
	},

	ClientRun = function(context, text)
		if #text == 0 then
			return ""
		end

		local shouldPrintOutput = context.Dispatcher:Run("var", "INIT_PRINT_OUTPUT") ~= ""

		local commands = text:gsub("\n+", "\n"):split("\n")

		for _, command in ipairs(commands) do
			if command:sub(1, 1) == "#" then
				continue
			end

			local output = context.Dispatcher:EvaluateAndRun(command)

			if shouldPrintOutput then
				context:Reply(output)
			end
		end

		return ""
	end,
}


local conditions = {
	startsWith = function(text, arg)
		if text:sub(1, #arg) == arg then
			return text:sub(#arg + 1)
		end
	end,
}

return {
	Name = "runif",
	Aliases = {},
	Description = "Runs a given command string if a certain condition is met.",
	Group = "DefaultUtil",
	Args = {
		{
			Type = "conditionFunction",
			Name = "Condition",
			Description = "The condition function",
		},
		{
			Type = "string",
			Name = "Argument",
			Description = "The argument to the condition function",
		},
		{
			Type = "string",
			Name = "Test against",
			Description = "The text to test against.",
		},
		{
			Type = "string",
			Name = "Command",
			Description = "The command string to run if requirements are met. If omitted, return value from condition function is used.",
			Optional = true,
		},
	},

	ClientRun = function(context, condition, arg, testAgainst, command)
		local conditionFunc = conditions[condition]

		if not conditionFunc then
			return ("Condition %q is not valid."):format(condition)
		end

		local text = conditionFunc(testAgainst, arg)

		if text then
			return context.Dispatcher:EvaluateAndRun(
				context.Cmdr.Util.RunEmbeddedCommands(context.Dispatcher, command or text)
			)
		end

		return ""
	end,
}


return {
	Name = "unbind",
	Aliases = {},
	Description = "Unbinds an input previously bound with Bind",
	Group = "DefaultUtil",
	Args = {
		{
			Type = "userInput ! bindableResource @ player",
			Name = "Input/Key",
			Description = "The key or input type you'd like to unbind.",
		},
	},

	ClientRun = function(context, inputEnum)
		local binds = context:GetStore("CMDR_Binds")

		if binds[inputEnum] then
			binds[inputEnum]:Disconnect()
			binds[inputEnum] = nil
			return "Unbound command from input."
		else
			return "That input wasn't bound."
		end
	end,
}


return {
	Name = "var",
	Aliases = {},
	Description = "Gets a stored variable.",
	Group = "DefaultUtil",
	AutoExec = {
		'alias "init-edit|Edit your initialization script" edit ${var init} \\\\\n && var= init ||',
		'alias "init-run|Re-runs the initialization script manually." run-lines ${var init}',
		"init-run",
	},
	Args = {
		{
			Type = "storedKey",
			Name = "Key",
			Description = "The key to get, retrieved from your user data store. Keys prefixed with . are not saved. Keys prefixed with $ are game-wide. Keys prefixed with $. are game-wide and non-saved.",
		},
	},

	ClientRun = function(context, key)
		context:GetStore("vars_used")[key] = true
	end,
}


return {
	Name = "var=",
	Aliases = {},
	Description = "Sets a stored value.",
	Group = "DefaultUtil",
	Args = {
		{
			Type = "storedKey",
			Name = "Key",
			Description = "The key to set, saved in your user data store. Keys prefixed with . are not saved. Keys prefixed with $ are game-wide. Keys prefixed with $. are game-wide and non-saved.",
		},
		{
			Type = "string",
			Name = "Value",
			Description = "Value or values to set.",
			Default = "",
		},
	},

	ClientRun = function(context, key)
		context:GetStore("vars_used")[key] = true
	end,
}


local ARGUMENT_SHORTHANDS = [[
Argument Shorthands
-------------------
.   Me/Self
*   All/Everyone
**  Others
?   Random
?N  List of N random values
]]

local TIPS = [[
Tips
----
• Utilize the Tab key to automatically complete commands
• Easily select and copy command output
]]

return {
	Name = "help",
	Aliases = { "cmds", "commands" },
	Description = "Displays a list of all commands, or inspects one command.",
	Group = "Help",
	Args = {
		{
			Type = "command",
			Name = "Command",
			Description = "The command to view information on",
			Optional = true,
		},
	},

	ClientRun = function(context, commandName)
		if commandName then
			local command = context.Cmdr.Registry:GetCommand(commandName)
			context:Reply(`Command: {command.Name}`, Color3.fromRGB(230, 126, 34))
			if command.Aliases and #command.Aliases > 0 then
				context:Reply(`Aliases: {table.concat(command.Aliases, ", ")}`, Color3.fromRGB(230, 230, 230))
			end
			if command.Description then
				context:Reply(`Description: {command.Description}`, Color3.fromRGB(230, 230, 230))
			end
			if command.Group then
				context:Reply(`Group: {command.Group}`, Color3.fromRGB(230, 230, 230))
			end
			if command.Args then
				for i, arg in ipairs(command.Args) do
					context:Reply(
						`#{i} {if type(arg) == "table"
							then `{arg.Name}{if arg.Optional == true then "?" else ""}: {arg.Type} - {arg.Description}`
							else "Unknown (inline argument)"}`
					)
				end
			end
		else
			context:Reply(ARGUMENT_SHORTHANDS)
			context:Reply(TIPS)

			local commands = context.Cmdr.Registry:GetCommands()
			table.sort(commands, function(a, b)
				return if a.Group and b.Group then a.Group < b.Group else a.Group
			end)
			local lastGroup
			for _, command in ipairs(commands) do
				local group = command.Group or "No Group"
				if lastGroup ~= group then
					context:Reply(`\n{group}\n{string.rep("-", #group)}`)
					lastGroup = group
				end
				context:Reply(if command.Description then `{command.Name} - {command.Description}` else command.Name)
			end
		end
		return ""
	end,
}


return function(registry)
	registry:RegisterType("bindableResource", registry.Cmdr.Util.MakeEnumType("BindableResource", { "Chat" }))
end


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

local brickColorNames = {
	"White",
	"Grey",
	"Light yellow",
	"Brick yellow",
	"Light green (Mint)",
	"Light reddish violet",
	"Pastel Blue",
	"Light orange brown",
	"Nougat",
	"Bright red",
	"Med. reddish violet",
	"Bright blue",
	"Bright yellow",
	"Earth orange",
	"Black",
	"Dark grey",
	"Dark green",
	"Medium green",
	"Lig. Yellowich orange",
	"Bright green",
	"Dark orange",
	"Light bluish violet",
	"Transparent",
	"Tr. Red",
	"Tr. Lg blue",
	"Tr. Blue",
	"Tr. Yellow",
	"Light blue",
	"Tr. Flu. Reddish orange",
	"Tr. Green",
	"Tr. Flu. Green",
	"Phosph. White",
	"Light red",
	"Medium red",
	"Medium blue",
	"Light grey",
	"Bright violet",
	"Br. yellowish orange",
	"Bright orange",
	"Bright bluish green",
	"Earth yellow",
	"Bright bluish violet",
	"Tr. Brown",
	"Medium bluish violet",
	"Tr. Medi. reddish violet",
	"Med. yellowish green",
	"Med. bluish green",
	"Light bluish green",
	"Br. yellowish green",
	"Lig. yellowish green",
	"Med. yellowish orange",
	"Br. reddish orange",
	"Bright reddish violet",
	"Light orange",
	"Tr. Bright bluish violet",
	"Gold",
	"Dark nougat",
	"Silver",
	"Neon orange",
	"Neon green",
	"Sand blue",
	"Sand violet",
	"Medium orange",
	"Sand yellow",
	"Earth blue",
	"Earth green",
	"Tr. Flu. Blue",
	"Sand blue metallic",
	"Sand violet metallic",
	"Sand yellow metallic",
	"Dark grey metallic",
	"Black metallic",
	"Light grey metallic",
	"Sand green",
	"Sand red",
	"Dark red",
	"Tr. Flu. Yellow",
	"Tr. Flu. Red",
	"Gun metallic",
	"Red flip/flop",
	"Yellow flip/flop",
	"Silver flip/flop",
	"Curry",
	"Fire Yellow",
	"Flame yellowish orange",
	"Reddish brown",
	"Flame reddish orange",
	"Medium stone grey",
	"Royal blue",
	"Dark Royal blue",
	"Bright reddish lilac",
	"Dark stone grey",
	"Lemon metalic",
	"Light stone grey",
	"Dark Curry",
	"Faded green",
	"Turquoise",
	"Light Royal blue",
	"Medium Royal blue",
	"Rust",
	"Brown",
	"Reddish lilac",
	"Lilac",
	"Light lilac",
	"Bright purple",
	"Light purple",
	"Light pink",
	"Light brick yellow",
	"Warm yellowish orange",
	"Cool yellow",
	"Dove blue",
	"Medium lilac",
	"Slime green",
	"Smoky grey",
	"Dark blue",
	"Parsley green",
	"Steel blue",
	"Storm blue",
	"Lapis",
	"Dark indigo",
	"Sea green",
	"Shamrock",
	"Fossil",
	"Mulberry",
	"Forest green",
	"Cadet blue",
	"Electric blue",
	"Eggplant",
	"Moss",
	"Artichoke",
	"Sage green",
	"Ghost grey",
	"Lilac",
	"Plum",
	"Olivine",
	"Laurel green",
	"Quill grey",
	"Crimson",
	"Mint",
	"Baby blue",
	"Carnation pink",
	"Persimmon",
	"Maroon",
	"Gold",
	"Daisy orange",
	"Pearl",
	"Fog",
	"Salmon",
	"Terra Cotta",
	"Cocoa",
	"Wheat",
	"Buttermilk",
	"Mauve",
	"Sunrise",
	"Tawny",
	"Rust",
	"Cashmere",
	"Khaki",
	"Lily white",
	"Seashell",
	"Burgundy",
	"Cork",
	"Burlap",
	"Beige",
	"Oyster",
	"Pine Cone",
	"Fawn brown",
	"Hurricane grey",
	"Cloudy grey",
	"Linen",
	"Copper",
	"Dirt brown",
	"Bronze",
	"Flint",
	"Dark taupe",
	"Burnt Sienna",
	"Institutional white",
	"Mid gray",
	"Really black",
	"Really red",
	"Deep orange",
	"Alder",
	"Dusty Rose",
	"Olive",
	"New Yeller",
	"Really blue",
	"Navy blue",
	"Deep blue",
	"Cyan",
	"CGA brown",
	"Magenta",
	"Pink",
	"Deep orange",
	"Teal",
	"Toothpaste",
	"Lime green",
	"Camo",
	"Grime",
	"Lavender",
	"Pastel light blue",
	"Pastel orange",
	"Pastel violet",
	"Pastel blue-green",
	"Pastel green",
	"Pastel yellow",
	"Pastel brown",
	"Royal purple",
	"Hot pink",
}

local brickColorFinder = Util.MakeFuzzyFinder(brickColorNames)

local brickColorType = {
	Prefixes = "% teamColor",

	Transform = function(text)
		local brickColors = {}
		for i, name in pairs(brickColorFinder(text)) do
			brickColors[i] = BrickColor.new(name)
		end
		return brickColors
	end,

	Validate = function(brickColors)
		return #brickColors > 0, "No valid brick colors with that name could be found."
	end,

	Autocomplete = function(brickColors)
		return Util.GetNames(brickColors)
	end,

	Parse = function(brickColors)
		return brickColors[1]
	end,
}

local brickColor3Type = {
	Transform = brickColorType.Transform,
	Validate = brickColorType.Validate,
	Autocomplete = brickColorType.Autocomplete,

	Parse = function(brickColors)
		return brickColors[1].Color
	end,
}

return function(registry)
	registry:RegisterType("brickColor", brickColorType)
	registry:RegisterType(
		"brickColors",
		Util.MakeListableType(brickColorType, {
			Prefixes = "% teamColors",
		})
	)

	registry:RegisterType("brickColor3", brickColor3Type)
	registry:RegisterType("brickColor3s", Util.MakeListableType(brickColor3Type))
end


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

local color3Type = Util.MakeSequenceType({
	Prefixes = "# hexColor3 ! brickColor3",
	ValidateEach = function(value, i)
		if value == nil then
			return false, ("Invalid or missing number at position %d in Color3 type."):format(i)
		elseif value < 0 or value > 255 then
			return false, ("Number out of acceptable range 0-255 at position %d in Color3 type."):format(i)
		elseif value % 1 ~= 0 then
			return false, ("Number is not an integer at position %d in Color3 type."):format(i)
		end

		return true
	end,
	TransformEach = tonumber,
	Constructor = Color3.fromRGB,
	Length = 3,
})

local function parseHexDigit(x)
	if #x == 1 then
		x = x .. x
	end

	return tonumber(x, 16)
end

local hexColor3Type = {
	Transform = function(text)
		local r, g, b = text:match("^#?(%x%x?)(%x%x?)(%x%x?)$")
		return Util.Each(parseHexDigit, r, g, b)
	end,

	Validate = function(r, g, b)
		return r ~= nil and g ~= nil and b ~= nil, "Invalid hex color"
	end,

	Parse = function(...)
		return Color3.fromRGB(...)
	end,
}

return function(cmdr)
	cmdr:RegisterType("color3", color3Type)
	cmdr:RegisterType(
		"color3s",
		Util.MakeListableType(color3Type, {
			Prefixes = "# hexColor3s ! brickColor3s",
		})
	)

	cmdr:RegisterType("hexColor3", hexColor3Type)
	cmdr:RegisterType("hexColor3s", Util.MakeListableType(hexColor3Type))
end


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

return function(cmdr)
	local commandType = {
		Transform = function(text)
			local findCommand = Util.MakeFuzzyFinder(cmdr:GetCommandNames())

			return findCommand(text)
		end,

		Validate = function(commands)
			return #commands > 0, "No command with that name could be found."
		end,

		Autocomplete = function(commands)
			return commands
		end,

		Parse = function(commands)
			return commands[1]
		end,
	}

	cmdr:RegisterType("command", commandType)
	cmdr:RegisterType("commands", Util.MakeListableType(commandType))
end


return function(registry)
	registry:RegisterType("conditionFunction", registry.Cmdr.Util.MakeEnumType("ConditionFunction", { "startsWith" }))
end


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

local unitTable = {
	Years = 31556926,
	Months = 2629744,
	Weeks = 604800,
	Days = 86400,
	Hours = 3600,
	Minutes = 60,
	Seconds = 1,
}

local searchKeyTable = {}
for key, _ in pairs(unitTable) do
	table.insert(searchKeyTable, key)
end
local unitFinder = Util.MakeFuzzyFinder(searchKeyTable)

local function stringToSecondDuration(stringDuration)
	-- The duration cannot be null or an empty string.
	if stringDuration == nil or stringDuration == "" then
		return nil
	end
	-- Allow 0 by itself (without a unit) to indicate 0 seconds
	local durationNum = tonumber(stringDuration)
	if durationNum and durationNum == 0 then
		return 0, 0, true
	end
	-- The duration must end with a unit,
	-- if it doesn't then return true as the fourth value to indicate the need to offer autocomplete for units.
	local endOnlyString = stringDuration:gsub("-?%d+%a+", "")
	local endNumber = endOnlyString:match("-?%d+")
	if endNumber then
		return nil, tonumber(endNumber), true
	end
	local seconds = nil
	local rawNum, rawUnit
	for rawComponent in stringDuration:gmatch("-?%d+%a+") do
		rawNum, rawUnit = rawComponent:match("(-?%d+)(%a+)")
		local unitNames = unitFinder(rawUnit, false, true)
		-- There were no matching units, it's invalid. Return the parsed number to be used for autocomplete
		if #unitNames == 0 then
			return nil, tonumber(rawNum)
		end
		if seconds == nil then
			seconds = 0
		end
		-- While it was already defaulting to use minutes when using just "m", this does it without worrying
		-- about any consistency between list ordering.
		seconds = seconds + (rawUnit:lower() == "m" and 60 or unitTable[unitNames[1]]) * tonumber(rawNum)
	end
	-- If no durations were provided, return nil.
	if seconds == nil then
		return nil
	else
		return seconds, tonumber(rawNum)
	end
end

local function mapUnits(units, rawText, lastNumber, subStart)
	subStart = subStart or 1
	local returnTable = {}
	for i, unit in pairs(units) do
		if lastNumber == 1 then
			returnTable[i] = rawText .. unit:sub(subStart, #unit - 1)
		else
			returnTable[i] = rawText .. unit:sub(subStart)
		end
	end
	return returnTable
end

local durationType = {
	Transform = function(text)
		return text, stringToSecondDuration(text)
	end,

	Validate = function(_, duration)
		return duration ~= nil
	end,

	Autocomplete = function(rawText, duration, lastNumber, isUnitMissing, matchedUnits)
		local returnTable = {}
		if isUnitMissing or matchedUnits then
			local unitsTable = isUnitMissing == true and unitFinder("", false, true) or matchedUnits
			if isUnitMissing == true then
				-- Concat the entire unit name to existing text.
				returnTable = mapUnits(unitsTable, rawText, lastNumber)
			else
				-- Concat the rest of the unit based on what already exists of the unit name.
				local existingUnitLength = rawText:match("^.*(%a+)$"):len()
				returnTable = mapUnits(unitsTable, rawText, existingUnitLength + 1)
			end
		elseif duration ~= nil then
			local endingUnit = rawText:match("^.*-?%d+(%a+)%s?$")
			-- Assume there is a singular match at this point
			local fuzzyUnits = unitFinder(endingUnit, false, true)
			-- List all possible fuzzy matches. This is for the Minutes/Months ambiguity case.
			returnTable = mapUnits(fuzzyUnits, rawText, lastNumber, #endingUnit + 1)
			-- Sort alphabetically in the Minutes/Months case, so Minutes are displayed on top.
			table.sort(returnTable)
		end
		return returnTable
	end,

	Parse = function(_, duration)
		return duration
	end,
}

return function(registry)
	registry:RegisterType("duration", durationType)
	registry:RegisterType("durations", Util.MakeListableType(durationType))
end


local HttpService = game:GetService("HttpService")

return function(registry)
	registry:RegisterType("json", {
		Validate = function(text)
			return pcall(HttpService.JSONDecode, HttpService, text)
		end,

		Parse = function(text)
			return HttpService:JSONDecode(text)
		end,
	})
end


return function(registry)
	registry:RegisterType(
		"mathOperator",
		registry.Cmdr.Util.MakeEnumType("Math Operator", {
			{
				Name = "+",
				Perform = function(a, b)
					return a + b
				end,
			},
			{
				Name = "-",
				Perform = function(a, b)
					return a - b
				end,
			},
			{
				Name = "*",
				Perform = function(a, b)
					return a * b
				end,
			},
			{
				Name = "/",
				Perform = function(a, b)
					return a / b
				end,
			},
			{
				Name = "**",
				Perform = function(a, b)
					return a ^ b
				end,
			},
			{
				Name = "%",
				Perform = function(a, b)
					return a % b
				end,
			},
		})
	)
end


local Util = require(script.Parent.Parent.Shared.Util)
local Players = game:GetService("Players")

local playerType = {
	Transform = function(text)
		local findPlayer = Util.MakeFuzzyFinder(Players:GetPlayers())

		return findPlayer(text)
	end,

	Validate = function(players)
		return #players > 0, "No player with that name could be found."
	end,

	Autocomplete = function(players)
		return Util.GetNames(players)
	end,

	Parse = function(players)
		return players[1]
	end,

	Default = function(player)
		return player.Name
	end,

	ArgumentOperatorAliases = {
		me = ".",
		all = "*",
		others = "**",
		random = "?",
	},
}

return function(cmdr)
	cmdr:RegisterType("player", playerType)
	cmdr:RegisterType(
		"players",
		Util.MakeListableType(playerType, {
			Prefixes = "% teamPlayers",
		})
	)
end


local Util = require(script.Parent.Parent.Shared.Util)
local Players = game:GetService("Players")

local nameCache = {}
local function getUserId(name)
	if nameCache[name] then
		return nameCache[name]
	elseif Players:FindFirstChild(name) then
		nameCache[name] = Players[name].UserId
		return Players[name].UserId
	else
		local ok, userid = pcall(Players.GetUserIdFromNameAsync, Players, name)

		if not ok then
			return nil
		end

		nameCache[name] = userid
		return userid
	end
end

local playerIdType = {
	DisplayName = "Full Player Name",
	Prefixes = "# positiveInteger",

	Transform = function(text)
		local findPlayer = Util.MakeFuzzyFinder(Players:GetPlayers())

		return text, findPlayer(text)
	end,

	ValidateOnce = function(text)
		return getUserId(text) ~= nil, "No player with that name could be found."
	end,

	Autocomplete = function(_, players)
		return Util.GetNames(players)
	end,

	Parse = function(text)
		return getUserId(text)
	end,

	Default = function(player)
		return player.Name
	end,

	ArgumentOperatorAliases = {
		me = ".",
		all = "*",
		others = "**",
		random = "?",
	},
}

return function(cmdr)
	cmdr:RegisterType("playerId", playerIdType)
	cmdr:RegisterType(
		"playerIds",
		Util.MakeListableType(playerIdType, {
			Prefixes = "# positiveIntegers",
		})
	)
end


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

local stringType = {
	Validate = function(value)
		return value ~= nil
	end,

	Parse = function(value)
		return tostring(value)
	end,
}

local numberType = {
	Transform = function(text)
		return tonumber(text)
	end,

	Validate = function(value)
		return value ~= nil
	end,

	Parse = function(value)
		return value
	end,
}

local intType = {
	Transform = function(text)
		return tonumber(text)
	end,

	Validate = function(value)
		return value ~= nil and value == math.floor(value), "Only whole numbers are valid."
	end,

	Parse = function(value)
		return value
	end,
}

local positiveIntType = {
	Transform = function(text)
		return tonumber(text)
	end,

	Validate = function(value)
		return value ~= nil and value == math.floor(value) and value > 0, "Only positive whole numbers are valid."
	end,

	Parse = function(value)
		return value
	end,
}

local nonNegativeIntType = {
	Transform = function(text)
		return tonumber(text)
	end,

	Validate = function(value)
		return value ~= nil and value == math.floor(value) and value >= 0, "Only non-negative whole numbers are valid."
	end,

	Parse = function(value)
		return value
	end,
}

local byteType = {
	Transform = function(text)
		return tonumber(text)
	end,

	Validate = function(value)
		return value ~= nil and value == math.floor(value) and value >= 0 and value <= 255, "Only bytes are valid."
	end,

	Parse = function(value)
		return value
	end,
}

local digitType = {
	Transform = function(text)
		return tonumber(text)
	end,

	Validate = function(value)
		return value ~= nil and value == math.floor(value) and value >= 0 and value <= 9, "Only digits are valid."
	end,

	Parse = function(value)
		return value
	end,
}

local boolType
do
	local truthy = Util.MakeDictionary({ "true", "t", "yes", "y", "on", "enable", "enabled", "1", "+" })
	local falsy = Util.MakeDictionary({ "false", "f", "no", "n", "off", "disable", "disabled", "0", "-" })

	boolType = {
		Transform = function(text)
			return text:lower()
		end,

		Validate = function(value)
			return truthy[value] ~= nil or falsy[value] ~= nil, "Please use true/yes/on or false/no/off."
		end,

		Parse = function(value)
			if truthy[value] then
				return true
			elseif falsy[value] then
				return false
			else
				return nil
			end
		end,
	}
end

return function(cmdr)
	cmdr:RegisterType("string", stringType)
	cmdr:RegisterType("number", numberType)
	cmdr:RegisterType("integer", intType)
	cmdr:RegisterType("positiveInteger", positiveIntType)
	cmdr:RegisterType("nonNegativeInteger", nonNegativeIntType)
	cmdr:RegisterType("byte", byteType)
	cmdr:RegisterType("digit", digitType)
	cmdr:RegisterType("boolean", boolType)

	cmdr:RegisterType("strings", Util.MakeListableType(stringType))
	cmdr:RegisterType("numbers", Util.MakeListableType(numberType))
	cmdr:RegisterType("integers", Util.MakeListableType(intType))
	cmdr:RegisterType("positiveIntegers", Util.MakeListableType(positiveIntType))
	cmdr:RegisterType("nonNegativeIntegers", Util.MakeListableType(nonNegativeIntType))
	cmdr:RegisterType("bytes", Util.MakeListableType(byteType))
	cmdr:RegisterType("digits", Util.MakeListableType(digitType))
	cmdr:RegisterType("booleans", Util.MakeListableType(boolType))
end


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

local VALID_STORED_KEY_NAME_PATTERNS = {
	"^%a[%w_]*$",
	"^%$%a[%w_]*$",
	"^%.%a[%w_]*$",
	"^%$%.%a[%w_]*$",
}

return function(registry)
	local storedKeyType = {
		Autocomplete = function(text)
			local find = registry.Cmdr.Util.MakeFuzzyFinder(
				registry.Cmdr.Util.DictionaryKeys(registry:GetStore("vars_used") or {})
			)

			return find(text)
		end,

		Validate = function(text)
			for _, pattern in ipairs(VALID_STORED_KEY_NAME_PATTERNS) do
				if text:match(pattern) then
					return true
				end
			end

			return false, "Key names must start with an optional modifier: . $ or $. and must begin with a letter."
		end,

		Parse = function(text)
			return text
		end,
	}
	registry:RegisterType("storedKey", storedKeyType)
	registry:RegisterType("storedKeys", Util.MakeListableType(storedKeyType))
end


local Teams = game:GetService("Teams")
local Util = require(script.Parent.Parent.Shared.Util)

local teamType = {
	Transform = function(text)
		local findTeam = Util.MakeFuzzyFinder(Teams:GetTeams())

		return findTeam(text)
	end,

	Validate = function(teams)
		return #teams > 0, "No team with that name could be found."
	end,

	Autocomplete = function(teams)
		return Util.GetNames(teams)
	end,

	Parse = function(teams)
		return teams[1]
	end,
}

local teamPlayersType = {
	Listable = true,
	Transform = teamType.Transform,
	Validate = teamType.Validate,
	Autocomplete = teamType.Autocomplete,

	Parse = function(teams)
		return teams[1]:GetPlayers()
	end,
}

local teamColorType = {
	Transform = teamType.Transform,
	Validate = teamType.Validate,
	Autocomplete = teamType.Autocomplete,

	Parse = function(teams)
		return teams[1].TeamColor
	end,
}

return function(cmdr)
	cmdr:RegisterType("team", teamType)
	cmdr:RegisterType("teams", Util.MakeListableType(teamType))

	cmdr:RegisterType("teamPlayers", teamPlayersType)

	cmdr:RegisterType("teamColor", teamColorType)
	cmdr:RegisterType("teamColors", Util.MakeListableType(teamColorType))
end


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

return function(cmdr)
	local typeType = {
		Transform = function(text)
			local findCommand = Util.MakeFuzzyFinder(cmdr:GetTypeNames())

			return findCommand(text)
		end,

		Validate = function(commands)
			return #commands > 0, "No type with that name could be found."
		end,

		Autocomplete = function(commands)
			return commands
		end,

		Parse = function(commands)
			return commands[1]
		end,
	}

	cmdr:RegisterType("type", typeType)
	cmdr:RegisterType("types", Util.MakeListableType(typeType))
end


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

local storedKeyType = {
	Validate = function(text)
		if text:match("^https?://.+$") then
			return true
		end

		return false, "URLs must begin with http:// or https://"
	end,

	Parse = function(text)
		return text
	end,
}

return function(cmdr)
	cmdr:RegisterType("url", storedKeyType)
	cmdr:RegisterType("urls", Util.MakeListableType(storedKeyType))
end


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

local combinedInputEnums = Enum.UserInputType:GetEnumItems()

for _, e in pairs(Enum.KeyCode:GetEnumItems()) do
	combinedInputEnums[#combinedInputEnums + 1] = e
end

local userInputType = {
	Transform = function(text)
		local findEnum = Util.MakeFuzzyFinder(combinedInputEnums)

		return findEnum(text)
	end,

	Validate = function(enums)
		return #enums > 0
	end,

	Autocomplete = function(enums)
		return Util.GetNames(enums)
	end,

	Parse = function(enums)
		return enums[1]
	end,
}

return function(cmdr)
	cmdr:RegisterType("userInput", userInputType)
	cmdr:RegisterType("userInputs", Util.MakeListableType(userInputType))
end


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

local function validateVector(value, i)
	if value == nil then
		return false, ("Invalid or missing number at position %d in Vector type."):format(i)
	end

	return true
end

local vector3Type = Util.MakeSequenceType({
	ValidateEach = validateVector,
	TransformEach = tonumber,
	Constructor = Vector3.new,
	Length = 3,
})

local vector2Type = Util.MakeSequenceType({
	ValidateEach = validateVector,
	TransformEach = tonumber,
	Constructor = Vector2.new,
	Length = 2,
})

return function(cmdr)
	cmdr:RegisterType("vector3", vector3Type)
	cmdr:RegisterType("vector3s", Util.MakeListableType(vector3Type))

	cmdr:RegisterType("vector2", vector2Type)
	cmdr:RegisterType("vector2s", Util.MakeListableType(vector2Type))
end


local Players = game:GetService("Players")
local Player = Players.LocalPlayer

return function(Cmdr)
	local AutoComplete = {
		Items = {},
		ItemOptions = {},
		SelectedItem = 0,
	}

	local Util = Cmdr.Util

	local Gui = Player:WaitForChild("PlayerGui"):WaitForChild("Cmdr"):WaitForChild("Autocomplete")
	local AutoItem = Gui:WaitForChild("TextButton")
	local Title = Gui:WaitForChild("Title")
	local Description = Gui:WaitForChild("Description")
	local Entry = Gui.Parent:WaitForChild("Frame"):WaitForChild("Entry")
	AutoItem.Parent = nil

	local defaultBarThickness = Gui.ScrollBarThickness

	-- Helper function that sets text and resizes labels
	local function SetText(obj, textObj, text, sizeFromContents)
		obj.Visible = text ~= nil
		textObj.Text = text or ""

		if sizeFromContents then
			textObj.Size = UDim2.new(
				0,
				Util.GetTextSize(text or "", textObj, Vector2.new(1000, 1000), 1, 0).X,
				obj.Size.Y.Scale,
				obj.Size.Y.Offset
			)
		end
	end

	local function UpdateContainerSize()
		Gui.Size = UDim2.new(
			0,
			math.max(Title.Field.TextBounds.X + Title.Field.Type.TextBounds.X, Gui.Size.X.Offset),
			0,
			math.min(Gui.UIListLayout.AbsoluteContentSize.Y, Gui.Parent.AbsoluteSize.Y - Gui.AbsolutePosition.Y - 10)
		)
	end

	-- Update the info display (Name, type, and description) based on given options.
	local function UpdateInfoDisplay(options)
		-- Update the objects' text and sizes
		SetText(Title, Title.Field, options.name, true)
		SetText(
			Title.Field.Type,
			Title.Field.Type,
			options.type and ": " .. options.type:sub(1, 1):upper() .. options.type:sub(2)
		)
		SetText(Description, Description.Label, options.description)

		Description.Label.TextColor3 = options.invalid and Color3.fromRGB(255, 73, 73) or Color3.fromRGB(255, 255, 255)
		Description.Size = UDim2.new(1, 0, 0, 40)

		-- Flow description text
		while not Description.Label.TextFits do
			Description.Size = Description.Size + UDim2.new(0, 0, 0, 2)

			if Description.Size.Y.Offset > 500 then
				break
			end
		end

		-- Update container
		task.wait()
		Gui.UIListLayout:ApplyLayout()
		UpdateContainerSize()
		Gui.ScrollBarThickness = defaultBarThickness
	end

	-- Shows the auto complete menu with the given list and possible options
	-- item = {typedText, suggestedText, options?=options}
	-- The options table is optional. `at` should only be passed into AutoComplete::Show
	-- name, type, and description may be passed in an options dictionary inside the items as well
	-- options.at?: the character index at which to show the menu
	-- options.name?: The name to display in the info box
	-- options.type?: The type to display in the info box
	-- options.prefix?: The current type prefix (%Team)
	-- options.description?: The description for the currently active info box
	-- options.invalid?: If true, description is shown in red.
	-- options.isLast?: If true, auto complete won't keep going after this argument.
	function AutoComplete:Show(items, options)
		options = options or {}

		-- Remove old options.
		for _, item in pairs(self.Items) do
			if item.gui then
				item.gui:Destroy()
			end
		end

		-- Reset state
		self.SelectedItem = 1
		self.Items = items
		self.Prefix = options.prefix or ""
		self.LastItem = options.isLast or false
		self.Command = options.command
		self.Arg = options.arg
		self.NumArgs = options.numArgs
		self.IsPartial = options.isPartial

		-- Generate the new option labels
		local autocompleteWidth = 200

		Gui.ScrollBarThickness = 0

		for i, item in pairs(self.Items) do
			local leftText = item[1]
			local rightText = item[2]

			local btn = AutoItem:Clone()
			btn.Name = leftText .. rightText
			btn.BackgroundTransparency = i == self.SelectedItem and 0.5 or 1

			local start, stop = string.find(rightText:lower(), leftText:lower(), 1, true)
			if start == nil and stop == nil then
				--start and stop are nil when the type returns an autocomplete result that is completely different (such as a custom alias hanlded within the type).
				--One should never be nil without the other.
				start, stop = 1, string.len(rightText)
			end
			btn.Typed.Text = string.rep(" ", start - 1) .. leftText
			btn.Suggest.Text = string.sub(rightText, 0, start - 1)
				.. string.rep(" ", #leftText)
				.. string.sub(rightText, stop + 1)

			btn.Parent = Gui
			btn.LayoutOrder = i

			local maxBounds = math.max(btn.Typed.TextBounds.X, btn.Suggest.TextBounds.X) + 20
			if maxBounds > autocompleteWidth then
				autocompleteWidth = maxBounds
			end

			item.gui = btn
		end

		Gui.UIListLayout:ApplyLayout()

		-- Todo: Use TextService to find accurate position for auto complete box
		local text = Entry.TextBox.Text
		local words = Util.SplitString(text)
		if text:sub(#text, #text) == " " and not options.at then
			words[#words + 1] = "e"
		end
		table.remove(words, #words)
		local extra = (options.at and options.at or (#table.concat(words, " ") + 1)) * 7

		-- Update the auto complete container
		Gui.Position =
			UDim2.new(0, Entry.TextBox.AbsolutePosition.X - 10 + extra, 0, Entry.TextBox.AbsolutePosition.Y + 30)
		Gui.Size = UDim2.new(0, autocompleteWidth, 0, Gui.UIListLayout.AbsoluteContentSize.Y)
		Gui.Visible = true

		-- Finally, update thge info display
		UpdateInfoDisplay(self.Items[1] and self.Items[1].options or options)
	end

	-- Returns the selected item in the auto complete
	function AutoComplete:GetSelectedItem()
		if Gui.Visible == false then
			return nil
		end

		return AutoComplete.Items[AutoComplete.SelectedItem]
	end

	-- Hides the auto complete
	function AutoComplete:Hide()
		Gui.Visible = false
	end

	-- Returns if the menu is visible
	function AutoComplete:IsVisible()
		return Gui.Visible
	end

	-- Changes the user's item selection by the given delta
	function AutoComplete:Select(delta)
		if not Gui.Visible then
			return
		end

		self.SelectedItem = self.SelectedItem + delta

		if self.SelectedItem > #self.Items then
			self.SelectedItem = 1
		elseif self.SelectedItem < 1 then
			self.SelectedItem = #self.Items
		end

		for i, item in pairs(self.Items) do
			item.gui.BackgroundTransparency = i == self.SelectedItem and 0.5 or 1
		end

		Gui.CanvasPosition = Vector2.new(
			0,
			math.max(
				0,
				Title.Size.Y.Offset
					+ Description.Size.Y.Offset
					+ self.SelectedItem * AutoItem.Size.Y.Offset
					- Gui.Size.Y.Offset
			)
		)

		if self.Items[self.SelectedItem] and self.Items[self.SelectedItem].options then
			UpdateInfoDisplay(self.Items[self.SelectedItem].options or {})
		end
	end

	Gui.Parent:GetPropertyChangedSignal("AbsoluteSize"):Connect(UpdateContainerSize)

	return AutoComplete
end


-- Here be dragons
local GuiService = game:GetService("GuiService")
local UserInputService = game:GetService("UserInputService")
local TextChatService = game:GetService("TextChatService")
local Players = game:GetService("Players")
local Player = Players.LocalPlayer

local WINDOW_MAX_HEIGHT = 300
local MOUSE_TOUCH_ENUM = { Enum.UserInputType.MouseButton1, Enum.UserInputType.MouseButton2, Enum.UserInputType.Touch }

-- Window handles the command bar GUI
local Window = {
	Valid = true,
	AutoComplete = nil,
	ProcessEntry = nil,
	OnTextChanged = nil,
	Cmdr = nil,
	HistoryState = nil,
}

local Gui = Player:WaitForChild("PlayerGui"):WaitForChild("Cmdr"):WaitForChild("Frame")
local Line = Gui:WaitForChild("Line")
local Entry = Gui:WaitForChild("Entry")

Line.Parent = nil

-- Update the text entry label
function Window:UpdateLabel()
	Entry.TextLabel.Text =
		`{Player.Name}{if self.Cmdr.PlaceName and self.Cmdr.PlaceName ~= "" then `@{self.Cmdr.PlaceName}` else ""}$`
end

-- Get the text entry label
function Window:GetLabel()
	return Entry.TextLabel.Text
end

-- Recalculate the window height
function Window:UpdateWindowHeight()
	local windowHeight = Gui.UIListLayout.AbsoluteContentSize.Y
		+ Gui.UIPadding.PaddingTop.Offset
		+ Gui.UIPadding.PaddingBottom.Offset
	Gui.Size = UDim2.new(Gui.Size.X.Scale, Gui.Size.X.Offset, 0, math.clamp(windowHeight, 0, WINDOW_MAX_HEIGHT))
	Gui.CanvasPosition = Vector2.new(0, windowHeight)
end

-- Add a line to the command bar
function Window:AddLine(text, options)
	options = options or {}
	text = tostring(text)

	if typeof(options) == "Color3" then
		options = { Color = options }
	end

	if #text == 0 then
		Window:UpdateWindowHeight()
		return
	end

	local str = self.Cmdr.Util.EmulateTabstops(text or "nil", 8)

	local line = Line:Clone()
	line.Text = str
	line.TextColor3 = options.Color or line.TextColor3
	line.RichText = options.RichText or false
	line.Parent = Gui
end

-- Returns if the command bar is visible
function Window:IsVisible()
	return Gui.Visible
end

-- Sets the command bar visible or not
function Window:SetVisible(visible)
	Gui.Visible = visible

	if visible then
		self.PreviousChatWindowConfigurationEnabled = TextChatService.ChatWindowConfiguration.Enabled
		self.PreviousChatInputBarConfigurationEnabled = TextChatService.ChatInputBarConfiguration.Enabled
		self.PreviousChannelTabsConfigurationEnabled = TextChatService.ChannelTabsConfiguration.Enabled
		TextChatService.ChatWindowConfiguration.Enabled = false
		TextChatService.ChatInputBarConfiguration.Enabled = false
		TextChatService.ChannelTabsConfiguration.Enabled = false

		Entry.TextBox:CaptureFocus()
		self:SetEntryText("")

		if self.Cmdr.ActivationUnlocksMouse then
			self.PreviousMouseBehavior = UserInputService.MouseBehavior
			UserInputService.MouseBehavior = Enum.MouseBehavior.Default
		end
	else
		TextChatService.ChatWindowConfiguration.Enabled = if self.PreviousChatWindowConfigurationEnabled ~= nil
			then self.PreviousChatWindowConfigurationEnabled
			else true
		TextChatService.ChatInputBarConfiguration.Enabled = if self.PreviousChatInputBarConfigurationEnabled
				~= nil
			then self.PreviousChatInputBarConfigurationEnabled
			else true
		TextChatService.ChannelTabsConfiguration.Enabled = if self.PreviousChannelTabsConfigurationEnabled ~= nil
			then self.PreviousChannelTabsConfigurationEnabled
			else true

		Entry.TextBox:ReleaseFocus()
		self.AutoComplete:Hide()

		if self.PreviousMouseBehavior then
			UserInputService.MouseBehavior = self.PreviousMouseBehavior
			self.PreviousMouseBehavior = nil
		end
	end
end

-- Hides the command bar
function Window:Hide()
	return self:SetVisible(false)
end

-- Shows the command bar
function Window:Show()
	return self:SetVisible(true)
end

-- Sets the text in the command bar text box, and captures focus
function Window:SetEntryText(text)
	Entry.TextBox.Text = text

	if self:IsVisible() then
		Entry.TextBox:CaptureFocus()
		Entry.TextBox.CursorPosition = #text + 1
		Window:UpdateWindowHeight()
	end
end

-- Gets the text in the command bar text box
function Window:GetEntryText()
	return Entry.TextBox.Text:gsub("\t", "")
end

-- Sets whether the command is in a valid state or not.
-- Cannot submit if in invalid state.
function Window:SetIsValidInput(isValid, errorText)
	Entry.TextBox.TextColor3 = isValid and Color3.fromRGB(255, 255, 255) or Color3.fromRGB(255, 73, 73)
	self.Valid = isValid
	self._errorText = errorText
end

function Window:HideInvalidState()
	Entry.TextBox.TextColor3 = Color3.fromRGB(255, 255, 255)
end

-- Event handler for text box focus lost
function Window:LoseFocus(submit)
	local text = Entry.TextBox.Text

	self:ClearHistoryState()

	if Gui.Visible and not GuiService.MenuIsOpen and not UserInputService.TouchEnabled then
		-- self:SetEntryText("")
		Entry.TextBox:CaptureFocus()
	elseif GuiService.MenuIsOpen and Gui.Visible then
		self:Hide()
	end

	if submit and self.Valid then
		wait()
		self:SetEntryText("")
		self.ProcessEntry(text)
	elseif submit then
		self:AddLine(self._errorText, Color3.fromRGB(255, 153, 153))
	end
end

function Window:TraverseHistory(delta)
	local history = self.Cmdr.Dispatcher:GetHistory()

	if self.HistoryState == nil then
		self.HistoryState = {
			Position = #history + 1,
			InitialText = self:GetEntryText(),
		}
	end

	self.HistoryState.Position = math.clamp(self.HistoryState.Position + delta, 1, #history + 1)

	self:SetEntryText(
		self.HistoryState.Position == #history + 1 and self.HistoryState.InitialText
			or history[self.HistoryState.Position]
	)
end

function Window:ClearHistoryState()
	self.HistoryState = nil
end

function Window:SelectVertical(delta)
	if self.AutoComplete:IsVisible() and not self.HistoryState then
		self.AutoComplete:Select(delta)
	else
		self:TraverseHistory(delta)
	end
end

local lastPressTime = 0
local pressCount = 0
-- Handles user input when the box is focused
function Window:BeginInput(input, gameProcessed)
	if GuiService.MenuIsOpen then
		self:Hide()
	end

	if gameProcessed and self:IsVisible() == false then
		return
	end

	if self.Cmdr.ActivationKeys[input.KeyCode] then -- Activate the command bar
		if self.Cmdr.MashToEnable and not self.Cmdr.Enabled then
			if tick() - lastPressTime < 1 then
				if pressCount >= 5 then
					return self.Cmdr:SetEnabled(true)
				else
					pressCount = pressCount + 1
				end
			else
				pressCount = 1
			end
			lastPressTime = tick()
		elseif self.Cmdr.Enabled then
			self:SetVisible(not self:IsVisible())
			wait()
			self:SetEntryText("")

			if GuiService.MenuIsOpen then -- Special case for menu getting stuck open (roblox bug)
				self:Hide()
			end
		end

		return
	end

	if self.Cmdr.Enabled == false or not self:IsVisible() then
		if self:IsVisible() then
			self:Hide()
		end

		return
	end

	if self.Cmdr.HideOnLostFocus and table.find(MOUSE_TOUCH_ENUM, input.UserInputType) then
		local ps = input.Position
		local ap = Gui.AbsolutePosition
		local as = Gui.AbsoluteSize
		if ps.X < ap.X or ps.X > ap.X + as.X or ps.Y < ap.Y or ps.Y > ap.Y + as.Y then
			self:Hide()
		end
	elseif input.KeyCode == Enum.KeyCode.Down then -- Auto Complete Down
		self:SelectVertical(1)
	elseif input.KeyCode == Enum.KeyCode.Up then -- Auto Complete Up
		self:SelectVertical(-1)
	elseif input.KeyCode == Enum.KeyCode.Return then -- Eat new lines
		wait()
		self:SetEntryText(self:GetEntryText():gsub("\n", ""):gsub("\r", ""))
	elseif input.KeyCode == Enum.KeyCode.Tab then -- Auto complete
		local item = self.AutoComplete:GetSelectedItem()
		local text = self:GetEntryText()
		if item and not (text:sub(#text, #text):match("%s") and self.AutoComplete.LastItem) then
			local replace = item[2]
			local newText
			local insertSpace = true
			local command = self.AutoComplete.Command

			if command then
				local lastArg = self.AutoComplete.Arg

				newText = command.Alias
				insertSpace = self.AutoComplete.NumArgs ~= #command.ArgumentDefinitions
					and self.AutoComplete.IsPartial == false

				local args = command.Arguments
				for i = 1, #args do
					local arg = args[i]
					local segments = arg.RawSegments
					if arg == lastArg then
						segments[#segments] = replace
					end

					local argText = arg.Prefix .. table.concat(segments, ",")

					-- Put auto completion options in quotation marks if they have a space
					if argText:find(" ") or argText == "" then
						argText = ("%q"):format(argText)
					end

					newText = ("%s %s"):format(newText, argText)

					if arg == lastArg then
						break
					end
				end
			else
				newText = replace
			end
			-- need to wait a frame so we can eat the \t
			wait()
			-- Update the text box
			self:SetEntryText(newText .. (insertSpace and " " or ""))
		else
			-- Still need to eat the \t even if there is no auto-complete to show
			wait()
			self:SetEntryText(self:GetEntryText())
		end
	else
		self:ClearHistoryState()
	end
end

-- Hook events
Entry.TextBox.FocusLost:Connect(function(submit)
	return Window:LoseFocus(submit)
end)

UserInputService.InputBegan:Connect(function(input, gameProcessed)
	return Window:BeginInput(input, gameProcessed)
end)

Entry.TextBox:GetPropertyChangedSignal("Text"):Connect(function()
	Gui.CanvasPosition = Vector2.new(0, Gui.AbsoluteCanvasSize.Y)

	if Entry.TextBox.Text:match("\t") then -- Eat \t
		Entry.TextBox.Text = Entry.TextBox.Text:gsub("\t", "")
		return
	end
	if Window.OnTextChanged then
		return Window.OnTextChanged(Entry.TextBox.Text)
	end
end)

Gui.ChildAdded:Connect(function()
	task.defer(Window.UpdateWindowHeight)
end)

return Window


-- Here be dragons

local Players = game:GetService("Players")
local Player = Players.LocalPlayer

return function(Cmdr)
	local Util = Cmdr.Util

	local Window = require(script:WaitForChild("Window"))
	Window.Cmdr = Cmdr

	local AutoComplete = require(script:WaitForChild("AutoComplete"))(Cmdr)
	Window.AutoComplete = AutoComplete

	-- Sets the Window.ProcessEntry callback so that we can dispatch our commands out
	function Window.ProcessEntry(text)
		text = Util.TrimString(text)

		if #text == 0 then
			return
		end

		Window:AddLine(Window:GetLabel() .. " " .. text, Color3.fromRGB(255, 223, 93))

		Window:AddLine(Cmdr.Dispatcher:EvaluateAndRun(text, Player, {
			IsHuman = true,
		}))
	end

	-- Sets the Window.OnTextChanged callback so we can update the auto complete
	function Window.OnTextChanged(text)
		local command = Cmdr.Dispatcher:Evaluate(text, Player, true)
		local arguments = Util.SplitString(text)
		local commandText = table.remove(arguments, 1)
		local atEnd = false
		if command then
			arguments = Util.MashExcessArguments(arguments, #command.Object.Args)

			atEnd = #arguments == #command.Object.Args
		end

		local entryComplete = commandText and #arguments > 0

		if text:sub(#text, #text):match("%s") and not atEnd then
			entryComplete = true
			arguments[#arguments + 1] = ""
		end

		if command and entryComplete then
			local commandValid, errorText = command:Validate()

			Window:SetIsValidInput(commandValid, ("Validation errors: %s"):format(errorText or ""))

			local acItems = {}

			local lastArgument = command:GetArgument(#arguments)
			if lastArgument then
				local typedText = lastArgument.TextSegmentInProgress

				local isPartial = false
				if lastArgument.RawSegmentsAreAutocomplete then
					for i, segment in ipairs(lastArgument.RawSegments) do
						acItems[i] = { segment, segment }
					end
				else
					local items, options = lastArgument:GetAutocomplete()
					options = options or {}
					isPartial = options.IsPartial or false

					for i, item in pairs(items) do
						acItems[i] = { typedText, item }
					end
				end

				local valid = true

				if #typedText > 0 then
					valid, errorText = lastArgument:Validate()
				end

				if not atEnd and valid then
					Window:HideInvalidState()
				end

				return AutoComplete:Show(acItems, {
					at = atEnd and #text - #typedText + (text:sub(#text, #text):match("%s") and -1 or 0),
					prefix = #lastArgument.RawSegments == 1 and lastArgument.Prefix or "",
					isLast = #command.Arguments == #command.ArgumentDefinitions and #typedText > 0,
					numArgs = #arguments,
					command = command,
					arg = lastArgument,
					name = lastArgument.Name .. (lastArgument.Required and "" or "?"),
					type = lastArgument.Type.DisplayName,
					description = (valid == false and errorText) or lastArgument.Object.Description,
					invalid = not valid,
					isPartial = isPartial,
				})
			end
		elseif commandText and #arguments == 0 then
			Window:SetIsValidInput(true)
			local exactCommand = Cmdr.Registry:GetCommand(commandText)
			local exactMatch
			if exactCommand then
				exactMatch = {
					exactCommand.Name,
					exactCommand.Name,
					options = {
						name = exactCommand.Name,
						description = exactCommand.Description,
					},
				}

				local arg = exactCommand.Args and exactCommand.Args[1]

				if type(arg) == "function" then
					arg = arg(command)
				end

				if arg and (not arg.Optional and arg.Default == nil) then
					Window:SetIsValidInput(false, "This command has required arguments.")
					Window:HideInvalidState()
				end
			else
				Window:SetIsValidInput(
					false,
					("%q is not a valid command name. Use the help command to see all available commands."):format(
						commandText
					)
				)
			end

			local acItems = { exactMatch }
			for _, cmd in pairs(Cmdr.Registry:GetCommandNames()) do
				if
					commandText:lower() == cmd:lower():sub(1, #commandText)
					and (exactMatch == nil or exactMatch[1] ~= commandText)
				then
					local commandObject = Cmdr.Registry:GetCommand(cmd)
					acItems[#acItems + 1] = {
						commandText,
						cmd,
						options = {
							name = commandObject.Name,
							description = commandObject.Description,
						},
					}
				end
			end

			return AutoComplete:Show(acItems)
		end

		Window:SetIsValidInput(false, "Use the help command to see all available commands.")
		AutoComplete:Hide()
	end

	Window:UpdateLabel()
	Window:UpdateWindowHeight()

	return {
		Window = Window,
		AutoComplete = AutoComplete,
	}
end


local StarterGui = game:GetService("StarterGui")
local TextChatService = game:GetService("TextChatService")
local Window = require(script.Parent.CmdrInterface.Window)

return function(Cmdr)
	Cmdr:HandleEvent("Message", function(text)
		if TextChatService.ChatVersion == Enum.ChatVersion.LegacyChatService then
			StarterGui:SetCore("ChatMakeSystemMessage", {
				Text = ("[Announcement] %s"):format(text),
				Color = Color3.fromRGB(249, 217, 56),
			})
		else
			TextChatService.TextChannels.RBXSystem:DisplaySystemMessage(
				`<font color="rgb(249, 217, 56)">[Announcement] {text}</font>`
			)
		end
	end)

	Cmdr:HandleEvent("AddLine", function(...)
		Window:AddLine(...)
	end)
end