--!strict

--[[
	Unpacks an animatable type into an array of numbers.
	If the type is not animatable, an empty array will be returned.

	FIXME: This function uses a lot of redefinitions to suppress false positives
	from the Luau typechecker - ideally these wouldn't be required

	FUTURE: When Luau supports singleton types, those could be used in
	conjunction with intersection types to make this function fully statically
	type checkable.
]]

local Package = script.Parent.Parent
local PubTypes = require(Package.PubTypes)
local Oklab = require(Package.Colour.Oklab)

local function unpackType(value: any, typeString: string): {number}
	if typeString == "number" then
		local value = value :: number
		return {value}

	elseif typeString == "CFrame" then
		-- FUTURE: is there a better way of doing this? doing distance
		-- calculations on `angle` may be incorrect
		local axis, angle = value:ToAxisAngle()
		return {value.X, value.Y, value.Z, axis.X, axis.Y, axis.Z, angle}

	elseif typeString == "Color3" then
		local lab = Oklab.to(value)
		return {lab.X, lab.Y, lab.Z}

	elseif typeString == "ColorSequenceKeypoint" then
		local lab = Oklab.to(value.Value)
		return {lab.X, lab.Y, lab.Z, value.Time}

	elseif typeString == "DateTime" then
		return {value.UnixTimestampMillis}

	elseif typeString == "NumberRange" then
		return {value.Min, value.Max}

	elseif typeString == "NumberSequenceKeypoint" then
		return {value.Value, value.Time, value.Envelope}

	elseif typeString == "PhysicalProperties" then
		return {value.Density, value.Friction, value.Elasticity, value.FrictionWeight, value.ElasticityWeight}

	elseif typeString == "Ray" then
		return {value.Origin.X, value.Origin.Y, value.Origin.Z, value.Direction.X, value.Direction.Y, value.Direction.Z}

	elseif typeString == "Rect" then
		return {value.Min.X, value.Min.Y, value.Max.X, value.Max.Y}

	elseif typeString == "Region3" then
		-- FUTURE: support rotated Region3s if/when they become constructable
		return {
			value.CFrame.X, value.CFrame.Y, value.CFrame.Z,
			value.Size.X, value.Size.Y, value.Size.Z
		}

	elseif typeString == "Region3int16" then
		return {value.Min.X, value.Min.Y, value.Min.Z, value.Max.X, value.Max.Y, value.Max.Z}

	elseif typeString == "UDim" then
		return {value.Scale, value.Offset}

	elseif typeString == "UDim2" then
		return {value.X.Scale, value.X.Offset, value.Y.Scale, value.Y.Offset}

	elseif typeString == "Vector2" then
		return {value.X, value.Y}

	elseif typeString == "Vector2int16" then
		return {value.X, value.Y}

	elseif typeString == "Vector3" then
		return {value.X, value.Y, value.Z}

	elseif typeString == "Vector3int16" then
		return {value.X, value.Y, value.Z}
	else
		return {}
	end
end

return unpackType


--!strict

--[[
	Calls the given callback, and stores any used external dependencies.
	Arguments can be passed in after the callback.
	If the callback completed successfully, returns true and the returned value,
	otherwise returns false and the error thrown.
	The callback shouldn't yield or run asynchronously.

	NOTE: any calls to useDependency() inside the callback (even if inside any
	nested captureDependencies() call) will not be included in the set, to avoid
	self-dependencies.
]]

local Package = script.Parent.Parent
local PubTypes = require(Package.PubTypes)
local parseError = require(Package.Logging.parseError)
local sharedState = require(Package.Dependencies.sharedState)

type Set<T> = {[T]: any}

local initialisedStack = sharedState.initialisedStack
local initialisedStackCapacity = 0

local function captureDependencies(
	saveToSet: Set<PubTypes.Dependency>,
	callback: (...any) -> any,
	...
): (boolean, any)

	local prevDependencySet = sharedState.dependencySet
	sharedState.dependencySet = saveToSet

	sharedState.initialisedStackSize += 1
	local initialisedStackSize = sharedState.initialisedStackSize

	local initialisedSet
	if initialisedStackSize > initialisedStackCapacity then
		initialisedSet = {}
		initialisedStack[initialisedStackSize] = initialisedSet
		initialisedStackCapacity = initialisedStackSize
	else
		initialisedSet = initialisedStack[initialisedStackSize]
		table.clear(initialisedSet)
	end

	local data = table.pack(xpcall(callback, parseError, ...))

	sharedState.dependencySet = prevDependencySet
	sharedState.initialisedStackSize -= 1

	return table.unpack(data, 1, data.n)
end

return captureDependencies


--!strict

--[[
	Registers the creation of an object which can be used as a dependency.

	This is used to make sure objects don't capture dependencies originating
	from inside of themselves.
]]

local Package = script.Parent.Parent
local PubTypes = require(Package.PubTypes)
local sharedState = require(Package.Dependencies.sharedState)

local initialisedStack = sharedState.initialisedStack

local function initDependency(dependency: PubTypes.Dependency)
	local initialisedStackSize = sharedState.initialisedStackSize

	for index, initialisedSet in ipairs(initialisedStack) do
		if index > initialisedStackSize then
			return
		end

		initialisedSet[dependency] = true
	end
end

return initDependency


--!strict

--[[
	Stores shared state for dependency management functions.
]]

local Package = script.Parent.Parent
local PubTypes = require(Package.PubTypes)

type Set<T> = {[T]: any}

-- The set where used dependencies should be saved to.
local dependencySet: Set<PubTypes.Dependency>? = nil

-- A stack of sets where newly created dependencies should be stored.
local initialisedStack: {Set<PubTypes.Dependency>} = {}
local initialisedStackSize = 0

return {
	dependencySet = dependencySet,
	initialisedStack = initialisedStack,
	initialisedStackSize = initialisedStackSize
}


--!strict

--[[
	Given a reactive object, updates all dependent reactive objects.
	Objects are only ever updated after all of their dependencies are updated,
	are only ever updated once, and won't be updated if their dependencies are
	unchanged.
]]

local Package = script.Parent.Parent
local PubTypes = require(Package.PubTypes)

type Set<T> = {[T]: any}
type Descendant = (PubTypes.Dependent & PubTypes.Dependency) | PubTypes.Dependent

-- Credit: https://blog.elttob.uk/2022/11/07/sets-efficient-topological-search.html
local function updateAll(root: PubTypes.Dependency)
	local counters: {[Descendant]: number} = {}
	local flags: {[Descendant]: boolean} = {}
	local queue: {Descendant} = {}
	local queueSize = 0
	local queuePos = 1

	for object in root.dependentSet do
		queueSize += 1
		queue[queueSize] = object
		flags[object] = true
	end

	-- Pass 1: counting up
	while queuePos <= queueSize do
		local next = queue[queuePos]
		local counter = counters[next]
		counters[next] = if counter == nil then 1 else counter + 1
		if (next :: any).dependentSet ~= nil then
			for object in (next :: any).dependentSet do
				queueSize += 1
				queue[queueSize] = object
			end
		end
		queuePos += 1
	end

	-- Pass 2: counting down + processing
	queuePos = 1
	while queuePos <= queueSize do
		local next = queue[queuePos]
		local counter = counters[next] - 1
		counters[next] = counter
		if counter == 0 and flags[next] and next:update() and (next :: any).dependentSet ~= nil then
			for object in (next :: any).dependentSet do
				flags[object] = true
			end
		end
		queuePos += 1
	end
end

return updateAll


--!strict

--[[
	If a target set was specified by captureDependencies(), this will add the
	given dependency to the target set.
]]

local Package = script.Parent.Parent
local PubTypes = require(Package.PubTypes)
local sharedState = require(Package.Dependencies.sharedState)

local initialisedStack = sharedState.initialisedStack

local function useDependency(dependency: PubTypes.Dependency)
	local dependencySet = sharedState.dependencySet

	if dependencySet ~= nil then
		local initialisedStackSize = sharedState.initialisedStackSize
		if initialisedStackSize > 0 then
			local initialisedSet = initialisedStack[initialisedStackSize]
			if initialisedSet[dependency] ~= nil then
				return
			end
		end
		dependencySet[dependency] = true
	end
end

return useDependency


--!strict

--[[
	A special key for property tables, which parents any given descendants into
	an instance.
]]

local Package = script.Parent.Parent
local PubTypes = require(Package.PubTypes)
local logWarn = require(Package.Logging.logWarn)
local Observer = require(Package.State.Observer)
local xtypeof = require(Package.Utility.xtypeof)

type Set<T> = {[T]: boolean}

-- Experimental flag: name children based on the key used in the [Children] table
local EXPERIMENTAL_AUTO_NAMING = false

local Children = {}
Children.type = "SpecialKey"
Children.kind = "Children"
Children.stage = "descendants"

function Children:apply(propValue: any, applyTo: Instance, cleanupTasks: {PubTypes.Task})
	local newParented: Set<Instance> = {}
	local oldParented: Set<Instance> = {}

	-- save disconnection functions for state object observers
	local newDisconnects: {[PubTypes.StateObject<any>]: () -> ()} = {}
	local oldDisconnects: {[PubTypes.StateObject<any>]: () -> ()} = {}

	local updateQueued = false
	local queueUpdate: () -> ()

	-- Rescans this key's value to find new instances to parent and state objects
	-- to observe for changes; then unparents instances no longer found and
	-- disconnects observers for state objects no longer present.
	local function updateChildren()
		if not updateQueued then
			return -- this update may have been canceled by destruction, etc.
		end
		updateQueued = false

		oldParented, newParented = newParented, oldParented
		oldDisconnects, newDisconnects = newDisconnects, oldDisconnects
		table.clear(newParented)
		table.clear(newDisconnects)

		local function processChild(child: any, autoName: string?)
			local kind = xtypeof(child)

			if kind == "Instance" then
				-- case 1; single instance

				newParented[child] = true
				if oldParented[child] == nil then
					-- wasn't previously present

					-- TODO: check for ancestry conflicts here
					child.Parent = applyTo
				else
					-- previously here; we want to reuse, so remove from old
					-- set so we don't encounter it during unparenting
					oldParented[child] = nil
				end

				if EXPERIMENTAL_AUTO_NAMING and autoName ~= nil then
					child.Name = autoName
				end

			elseif kind == "State" then
				-- case 2; state object

				local value = child:get(false)
				-- allow nil to represent the absence of a child
				if value ~= nil then
					processChild(value, autoName)
				end

				local disconnect = oldDisconnects[child]
				if disconnect == nil then
					-- wasn't previously present
					disconnect = Observer(child):onChange(queueUpdate)
				else
					-- previously here; we want to reuse, so remove from old
					-- set so we don't encounter it during unparenting
					oldDisconnects[child] = nil
				end

				newDisconnects[child] = disconnect

			elseif kind == "table" then
				-- case 3; table of objects

				for key, subChild in pairs(child) do
					local keyType = typeof(key)
					local subAutoName: string? = nil

					if keyType == "string" then
						subAutoName = key
					elseif keyType == "number" and autoName ~= nil then
						subAutoName = autoName .. "_" .. key
					end

					processChild(subChild, subAutoName)
				end

			else
				logWarn("unrecognisedChildType", kind)
			end
		end

		if propValue ~= nil then
			-- `propValue` is set to nil on cleanup, so we don't process children
			-- in that case
			processChild(propValue)
		end

		-- unparent any children that are no longer present
		for oldInstance in pairs(oldParented) do
			oldInstance.Parent = nil
		end

		-- disconnect observers which weren't reused
		for oldState, disconnect in pairs(oldDisconnects) do
			disconnect()
		end
	end

	queueUpdate = function()
		if not updateQueued then
			updateQueued = true
			task.defer(updateChildren)
		end
	end

	table.insert(cleanupTasks, function()
		propValue = nil
		updateQueued = true
		updateChildren()
	end)

	-- perform initial child parenting
	updateQueued = true
	updateChildren()
end

return Children :: PubTypes.SpecialKey


--!strict

--[[
	A special key for property tables, which adds user-specified tasks to be run
	when the instance is destroyed.
]]

local Package = script.Parent.Parent
local PubTypes = require(Package.PubTypes)

local Cleanup = {}
Cleanup.type = "SpecialKey"
Cleanup.kind = "Cleanup"
Cleanup.stage = "observer"

function Cleanup:apply(userTask: any, applyTo: Instance, cleanupTasks: {PubTypes.Task})
	table.insert(cleanupTasks, userTask)
end

return Cleanup


--!strict

--[[
	Processes and returns an existing instance, with options for setting
	properties, event handlers and other attributes on the instance.
]]

local Package = script.Parent.Parent
local PubTypes = require(Package.PubTypes)
local applyInstanceProps = require(Package.Instances.applyInstanceProps)

local function Hydrate(target: Instance)
	return function(props: PubTypes.PropertyTable): Instance
		applyInstanceProps(props, target)
		return target
	end
end

return Hydrate


--!strict

--[[
	Constructs and returns a new instance, with options for setting properties,
	event handlers and other attributes on the instance right away.
]]

local Package = script.Parent.Parent
local PubTypes = require(Package.PubTypes)
local defaultProps = require(Package.Instances.defaultProps)
local applyInstanceProps = require(Package.Instances.applyInstanceProps)
local logError= require(Package.Logging.logError)

local function New(className: string)
	return function(props: PubTypes.PropertyTable): Instance
		local ok, instance = pcall(Instance.new, className)

		if not ok then
			logError("cannotCreateClass", nil, className)
		end

		local classDefaults = defaultProps[className]
		if classDefaults ~= nil then
			for defaultProp, defaultValue in pairs(classDefaults) do
				instance[defaultProp] = defaultValue
			end
		end

		applyInstanceProps(props, instance)

		return instance
	end
end

return New


--!strict

--[[
	Constructs special keys for property tables which connect property change
	listeners to an instance.
]]

local Package = script.Parent.Parent
local PubTypes = require(Package.PubTypes)
local logError = require(Package.Logging.logError)

local function OnChange(propertyName: string): PubTypes.SpecialKey
	local changeKey = {}
	changeKey.type = "SpecialKey"
	changeKey.kind = "OnChange"
	changeKey.stage = "observer"

	function changeKey:apply(callback: any, applyTo: Instance, cleanupTasks: {PubTypes.Task})
		local ok, event = pcall(applyTo.GetPropertyChangedSignal, applyTo, propertyName)
		if not ok then
			logError("cannotConnectChange", nil, applyTo.ClassName, propertyName)
		elseif typeof(callback) ~= "function" then
			logError("invalidChangeHandler", nil, propertyName)
		else
			table.insert(cleanupTasks, event:Connect(function()
				callback((applyTo :: any)[propertyName])
			end))
		end
	end

	return changeKey
end

return OnChange


--!strict

--[[
	Constructs special keys for property tables which connect event listeners to
	an instance.
]]

local Package = script.Parent.Parent
local PubTypes = require(Package.PubTypes)
local logError = require(Package.Logging.logError)

local function getProperty_unsafe(instance: Instance, property: string)
	return (instance :: any)[property]
end

local function OnEvent(eventName: string): PubTypes.SpecialKey
	local eventKey = {}
	eventKey.type = "SpecialKey"
	eventKey.kind = "OnEvent"
	eventKey.stage = "observer"

	function eventKey:apply(callback: any, applyTo: Instance, cleanupTasks: {PubTypes.Task})
		local ok, event = pcall(getProperty_unsafe, applyTo, eventName)
		if not ok or typeof(event) ~= "RBXScriptSignal" then
			logError("cannotConnectEvent", nil, applyTo.ClassName, eventName)
		elseif typeof(callback) ~= "function" then
			logError("invalidEventHandler", nil, eventName)
		else
			table.insert(cleanupTasks, event:Connect(callback))
		end
	end

	return eventKey
end

return OnEvent


--!strict

--[[
	A special key for property tables, which allows users to extract values from
	an instance into an automatically-updated Value object.
]]

local Package = script.Parent.Parent
local PubTypes = require(Package.PubTypes)
local logError = require(Package.Logging.logError)
local xtypeof = require(Package.Utility.xtypeof)

local function Out(propertyName: string): PubTypes.SpecialKey
	local outKey = {}
	outKey.type = "SpecialKey"
	outKey.kind = "Out"
	outKey.stage = "observer"

	function outKey:apply(outState: any, applyTo: Instance, cleanupTasks: { PubTypes.Task })
		local ok, event = pcall(applyTo.GetPropertyChangedSignal, applyTo, propertyName)
		if not ok then
			logError("invalidOutProperty", nil, applyTo.ClassName, propertyName)
		elseif xtypeof(outState) ~= "State" or outState.kind ~= "Value" then
			logError("invalidOutType")
		else
			outState:set((applyTo :: any)[propertyName])
			table.insert(
				cleanupTasks,
				event:Connect(function()
					outState:set((applyTo :: any)[propertyName])
				end)
			)
			table.insert(cleanupTasks, function()
				outState:set(nil)
			end)
		end
	end

	return outKey
end

return Out


--!strict

--[[
	A special key for property tables, which stores a reference to the instance
	in a user-provided Value object.
]]

local Package = script.Parent.Parent
local PubTypes = require(Package.PubTypes)
local logError = require(Package.Logging.logError)
local xtypeof = require(Package.Utility.xtypeof)

local Ref = {}
Ref.type = "SpecialKey"
Ref.kind = "Ref"
Ref.stage = "observer"

function Ref:apply(refState: any, applyTo: Instance, cleanupTasks: {PubTypes.Task})
	if xtypeof(refState) ~= "State" or refState.kind ~= "Value" then
		logError("invalidRefType")
	else
		refState:set(applyTo)
		table.insert(cleanupTasks, function()
			refState:set(nil)
		end)
	end
end

return Ref


--!strict

--[[
	Applies a table of properties to an instance, including binding to any
	given state objects and applying any special keys.

	No strong reference is kept by default - special keys should take care not
	to accidentally hold strong references to instances forever.

	If a key is used twice, an error will be thrown. This is done to avoid
	double assignments or double bindings. However, some special keys may want
	to enable such assignments - in which case unique keys should be used for
	each occurence.
]]

local Package = script.Parent.Parent
local PubTypes = require(Package.PubTypes)
local cleanup = require(Package.Utility.cleanup)
local xtypeof = require(Package.Utility.xtypeof)
local logError = require(Package.Logging.logError)
local Observer = require(Package.State.Observer)

local function setProperty_unsafe(instance: Instance, property: string, value: any)
	(instance :: any)[property] = value
end

local function testPropertyAssignable(instance: Instance, property: string)
	(instance :: any)[property] = (instance :: any)[property]
end

local function setProperty(instance: Instance, property: string, value: any)
	if not pcall(setProperty_unsafe, instance, property, value) then
		if not pcall(testPropertyAssignable, instance, property) then
			if instance == nil then
				-- reference has been lost
				logError("setPropertyNilRef", nil, property, tostring(value))
			else
				-- property is not assignable
				logError("cannotAssignProperty", nil, instance.ClassName, property)
			end
		else
			-- property is assignable, but this specific assignment failed
			-- this typically implies the wrong type was received
			local givenType = typeof(value)
			local expectedType = typeof((instance :: any)[property])
			logError("invalidPropertyType", nil, instance.ClassName, property, expectedType, givenType)
		end
	end
end

local function bindProperty(instance: Instance, property: string, value: PubTypes.CanBeState<any>, cleanupTasks: {PubTypes.Task})
	if xtypeof(value) == "State" then
		-- value is a state object - assign and observe for changes
		local willUpdate = false
		local function updateLater()
			if not willUpdate then
				willUpdate = true
				task.defer(function()
					willUpdate = false
					setProperty(instance, property, value:get(false))
				end)
			end
		end

		setProperty(instance, property, value:get(false))
		table.insert(cleanupTasks, Observer(value :: any):onChange(updateLater))
	else
		-- value is a constant - assign once only
		setProperty(instance, property, value)
	end
end

local function applyInstanceProps(props: PubTypes.PropertyTable, applyTo: Instance)
	local specialKeys = {
		self = {} :: {[PubTypes.SpecialKey]: any},
		descendants = {} :: {[PubTypes.SpecialKey]: any},
		ancestor = {} :: {[PubTypes.SpecialKey]: any},
		observer = {} :: {[PubTypes.SpecialKey]: any}
	}
	local cleanupTasks = {}

	for key, value in pairs(props) do
		local keyType = xtypeof(key)

		if keyType == "string" then
			if key ~= "Parent" then
				bindProperty(applyTo, key :: string, value, cleanupTasks)
			end
		elseif keyType == "SpecialKey" then
			local stage = (key :: PubTypes.SpecialKey).stage
			local keys = specialKeys[stage]
			if keys == nil then
				logError("unrecognisedPropertyStage", nil, stage)
			else
				keys[key] = value
			end
		else
			-- we don't recognise what this key is supposed to be
			logError("unrecognisedPropertyKey", nil, xtypeof(key))
		end
	end

	for key, value in pairs(specialKeys.self) do
		key:apply(value, applyTo, cleanupTasks)
	end
	for key, value in pairs(specialKeys.descendants) do
		key:apply(value, applyTo, cleanupTasks)
	end

	if props.Parent ~= nil then
		bindProperty(applyTo, "Parent", props.Parent, cleanupTasks)
	end

	for key, value in pairs(specialKeys.ancestor) do
		key:apply(value, applyTo, cleanupTasks)
	end
	for key, value in pairs(specialKeys.observer) do
		key:apply(value, applyTo, cleanupTasks)
	end

	applyTo.Destroying:Connect(function()
		cleanup(cleanupTasks)
	end)
end

return applyInstanceProps


--!strict

--[[
	Stores 'sensible default' properties to be applied to instances created by
	the New function.
]]

return {
	ScreenGui = {
		ResetOnSpawn = false,
		ZIndexBehavior = Enum.ZIndexBehavior.Sibling
	},

	BillboardGui = {
		ResetOnSpawn = false,
		ZIndexBehavior = Enum.ZIndexBehavior.Sibling
	},

	SurfaceGui = {
		ResetOnSpawn = false,
		ZIndexBehavior = Enum.ZIndexBehavior.Sibling,

		SizingMode = Enum.SurfaceGuiSizingMode.PixelsPerStud,
		PixelsPerStud = 50
	},

	Frame = {
		BackgroundColor3 = Color3.new(1, 1, 1),
		BorderColor3 = Color3.new(0, 0, 0),
		BorderSizePixel = 0
	},

	ScrollingFrame = {
		BackgroundColor3 = Color3.new(1, 1, 1),
		BorderColor3 = Color3.new(0, 0, 0),
		BorderSizePixel = 0,

		ScrollBarImageColor3 = Color3.new(0, 0, 0)
	},

	TextLabel = {
		BackgroundColor3 = Color3.new(1, 1, 1),
		BorderColor3 = Color3.new(0, 0, 0),
		BorderSizePixel = 0,

		Font = Enum.Font.SourceSans,
		Text = "",
		TextColor3 = Color3.new(0, 0, 0),
		TextSize = 14
	},

	TextButton = {
		BackgroundColor3 = Color3.new(1, 1, 1),
		BorderColor3 = Color3.new(0, 0, 0),
		BorderSizePixel = 0,

		AutoButtonColor = false,

		Font = Enum.Font.SourceSans,
		Text = "",
		TextColor3 = Color3.new(0, 0, 0),
		TextSize = 14
	},

	TextBox = {
		BackgroundColor3 = Color3.new(1, 1, 1),
		BorderColor3 = Color3.new(0, 0, 0),
		BorderSizePixel = 0,

		ClearTextOnFocus = false,

		Font = Enum.Font.SourceSans,
		Text = "",
		TextColor3 = Color3.new(0, 0, 0),
		TextSize = 14
	},

	ImageLabel = {
		BackgroundColor3 = Color3.new(1, 1, 1),
		BorderColor3 = Color3.new(0, 0, 0),
		BorderSizePixel = 0
	},

	ImageButton = {
		BackgroundColor3 = Color3.new(1, 1, 1),
		BorderColor3 = Color3.new(0, 0, 0),
		BorderSizePixel = 0,

		AutoButtonColor = false
	},

	ViewportFrame = {
		BackgroundColor3 = Color3.new(1, 1, 1),
		BorderColor3 = Color3.new(0, 0, 0),
		BorderSizePixel = 0
	},

	VideoFrame = {
		BackgroundColor3 = Color3.new(1, 1, 1),
		BorderColor3 = Color3.new(0, 0, 0),
		BorderSizePixel = 0
	},

	CanvasGroup = {
		BackgroundColor3 = Color3.new(1, 1, 1),
		BorderColor3 = Color3.new(0, 0, 0),
		BorderSizePixel = 0
	}
}


--!strict

--[[
	Utility function to log a Fusion-specific error.
]]

local Package = script.Parent.Parent
local Types = require(Package.Types)
local messages = require(Package.Logging.messages)

local function logError(messageID: string, errObj: Types.Error?, ...)
	local formatString: string

	if messages[messageID] ~= nil then
		formatString = messages[messageID]
	else
		messageID = "unknownMessage"
		formatString = messages[messageID]
	end

	local errorString
	if errObj == nil then
		errorString = string.format("[Fusion] " .. formatString .. "\n(ID: " .. messageID .. ")", ...)
	else
		formatString = formatString:gsub("ERROR_MESSAGE", errObj.message)
		errorString = string.format("[Fusion] " .. formatString .. "\n(ID: " .. messageID .. ")\n---- Stack trace ----\n" .. errObj.trace, ...)
	end

	error(errorString:gsub("\n", "\n    "), 0)
end

return logError


--!strict

--[[
	Utility function to log a Fusion-specific error, without halting execution.
]]

local Package = script.Parent.Parent
local Types = require(Package.Types)
local messages = require(Package.Logging.messages)

local function logErrorNonFatal(messageID: string, errObj: Types.Error?, ...)
	local formatString: string

	if messages[messageID] ~= nil then
		formatString = messages[messageID]
	else
		messageID = "unknownMessage"
		formatString = messages[messageID]
	end

	local errorString
	if errObj == nil then
		errorString = string.format("[Fusion] " .. formatString .. "\n(ID: " .. messageID .. ")", ...)
	else
		formatString = formatString:gsub("ERROR_MESSAGE", errObj.message)
		errorString = string.format("[Fusion] " .. formatString .. "\n(ID: " .. messageID .. ")\n---- Stack trace ----\n" .. errObj.trace, ...)
	end

	task.spawn(function(...)
		error(errorString:gsub("\n", "\n    "), 0)
	end, ...)
end

return logErrorNonFatal


--!strict

--[[
	Utility function to log a Fusion-specific warning.
]]

local Package = script.Parent.Parent
local messages = require(Package.Logging.messages)

local function logWarn(messageID, ...)
	local formatString: string

	if messages[messageID] ~= nil then
		formatString = messages[messageID]
	else
		messageID = "unknownMessage"
		formatString = messages[messageID]
	end

	warn(string.format("[Fusion] " .. formatString .. "\n(ID: " .. messageID .. ")", ...))
end

return logWarn


--!strict

--[[
	An xpcall() error handler to collect and parse useful information about
	errors, such as clean messages and stack traces.

	TODO: this should have a 'type' field for runtime type checking!
]]

local Package = script.Parent.Parent
local Types = require(Package.Types)

local function parseError(err: string): Types.Error
	return {
		type = "Error",
		raw = err,
		message = err:gsub("^.+:%d+:%s*", ""),
		trace = debug.traceback(nil, 2)
	}
end

return parseError


--!strict

--[[
	Stores common public-facing type information for Fusion APIs.
]]

type Set<T> = {[T]: any}

--[[
	General use types
]]

-- A unique symbolic value.
export type Symbol = {
	type: string, -- replace with "Symbol" when Luau supports singleton types
	name: string
}

-- Types that can be expressed as vectors of numbers, and so can be animated.
export type Animatable =
	number |
	CFrame |
	Color3 |
	ColorSequenceKeypoint |
	DateTime |
	NumberRange |
	NumberSequenceKeypoint |
	PhysicalProperties |
	Ray |
	Rect |
	Region3 |
	Region3int16 |
	UDim |
	UDim2 |
	Vector2 |
	Vector2int16 |
	Vector3 |
	Vector3int16

-- A task which can be accepted for cleanup.
export type Task =
	Instance |
	RBXScriptConnection |
	() -> () |
	{destroy: (any) -> ()} |
	{Destroy: (any) -> ()} |
	{Task}

-- Script-readable version information.
export type Version = {
	major: number,
	minor: number,
	isRelease: boolean
}
--[[
	Generic reactive graph types
]]

-- A graph object which can have dependents.
export type Dependency = {
	dependentSet: Set<Dependent>
}

-- A graph object which can have dependencies.
export type Dependent = {
	update: (Dependent) -> boolean,
	dependencySet: Set<Dependency>
}

-- An object which stores a piece of reactive state.
export type StateObject<T> = Dependency & {
	type: string, -- replace with "State" when Luau supports singleton types
	kind: string,
	get: (StateObject<T>, asDependency: boolean?) -> T
}

-- Either a constant value of type T, or a state object containing type T.
export type CanBeState<T> = StateObject<T> | T

--[[
	Specific reactive graph types
]]

-- A state object whose value can be set at any time by the user.
export type Value<T> = StateObject<T> & {
	-- kind: "State" (add this when Luau supports singleton types)
 	set: (Value<T>, newValue: any, force: boolean?) -> ()
}

-- A state object whose value is derived from other objects using a callback.
export type Computed<T> = StateObject<T> & Dependent & {
	-- kind: "Computed" (add this when Luau supports singleton types)
}

-- A state object whose value is derived from other objects using a callback.
export type ForPairs<KO, VO> = StateObject<{ [KO]: VO }> & Dependent & {
	-- kind: "ForPairs" (add this when Luau supports singleton types)
}
-- A state object whose value is derived from other objects using a callback.
export type ForKeys<KO, V> = StateObject<{ [KO]: V }> & Dependent & {
	-- kind: "ForKeys" (add this when Luau supports singleton types)
}
-- A state object whose value is derived from other objects using a callback.
export type ForValues<K, VO> = StateObject<{ [K]: VO }> & Dependent & {
	-- kind: "ForKeys" (add this when Luau supports singleton types)
}

-- A state object which follows another state object using tweens.
export type Tween<T> = StateObject<T> & Dependent & {
	-- kind: "Tween" (add this when Luau supports singleton types)
}

-- A state object which follows another state object using spring simulation.
export type Spring<T> = StateObject<T> & Dependent & {
	-- kind: "Spring" (add this when Luau supports singleton types)
	-- Uncomment when ENABLE_PARAM_SETTERS is enabled
	-- setPosition: (Spring<T>, newValue: Animatable) -> (),
	-- setVelocity: (Spring<T>, newValue: Animatable) -> (),
	-- addVelocity: (Spring<T>, deltaValue: Animatable) -> ()
}

-- An object which can listen for updates on another state object.
export type Observer = Dependent & {
	-- kind: "Observer" (add this when Luau supports singleton types)
  	onChange: (Observer, callback: () -> ()) -> (() -> ())
}

--[[
	Instance related types
]]

-- Denotes children instances in an instance or component's property table.
export type SpecialKey = {
	type: string, -- replace with "SpecialKey" when Luau supports singleton types
	kind: string,
	stage: string, -- replace with "self" | "descendants" | "ancestor" | "observer" when Luau supports singleton types
	apply: (SpecialKey, value: any, applyTo: Instance, cleanupTasks: {Task}) -> ()
}

-- A collection of instances that may be parented to another instance.
export type Children = Instance | StateObject<Children> | {[any]: Children}

-- A table that defines an instance's properties, handlers and children.
export type PropertyTable = {[string | SpecialKey]: any}

return nil


--!nonstrict

--[[
	Constructs and returns objects which can be used to model derived reactive
	state.
]]

local Package = script.Parent.Parent
local Types = require(Package.Types)
local captureDependencies = require(Package.Dependencies.captureDependencies)
local initDependency = require(Package.Dependencies.initDependency)
local useDependency = require(Package.Dependencies.useDependency)
local logErrorNonFatal = require(Package.Logging.logErrorNonFatal)
local logWarn = require(Package.Logging.logWarn)
local isSimilar = require(Package.Utility.isSimilar)
local needsDestruction = require(Package.Utility.needsDestruction)

local class = {}

local CLASS_METATABLE = {__index = class}
local WEAK_KEYS_METATABLE = {__mode = "k"}

--[[
	Returns the last cached value calculated by this Computed object.
	The computed object will be registered as a dependency unless `asDependency`
	is false.
]]
function class:get(asDependency: boolean?): any
	if asDependency ~= false then
		useDependency(self)
	end
	return self._value
end

--[[
	Recalculates this Computed's cached value and dependencies.
	Returns true if it changed, or false if it's identical.
]]
function class:update(): boolean
	-- remove this object from its dependencies' dependent sets
	for dependency in pairs(self.dependencySet) do
		dependency.dependentSet[self] = nil
	end

	-- we need to create a new, empty dependency set to capture dependencies
	-- into, but in case there's an error, we want to restore our old set of
	-- dependencies. by using this table-swapping solution, we can avoid the
	-- overhead of allocating new tables each update.
	self._oldDependencySet, self.dependencySet = self.dependencySet, self._oldDependencySet
	table.clear(self.dependencySet)

	local ok, newValue, newMetaValue = captureDependencies(self.dependencySet, self._processor)

	if ok then
		if self._destructor == nil and needsDestruction(newValue) then
			logWarn("destructorNeededComputed")
		end

		if newMetaValue ~= nil then
			logWarn("multiReturnComputed")
		end

		local oldValue = self._value
		local similar = isSimilar(oldValue, newValue)
		if self._destructor ~= nil then
			self._destructor(oldValue)
		end
		self._value = newValue

		-- add this object to the dependencies' dependent sets
		for dependency in pairs(self.dependencySet) do
			dependency.dependentSet[self] = true
		end

		return not similar
	else
		-- this needs to be non-fatal, because otherwise it'd disrupt the
		-- update process
		logErrorNonFatal("computedCallbackError", newValue)

		-- restore old dependencies, because the new dependencies may be corrupt
		self._oldDependencySet, self.dependencySet = self.dependencySet, self._oldDependencySet

		-- restore this object in the dependencies' dependent sets
		for dependency in pairs(self.dependencySet) do
			dependency.dependentSet[self] = true
		end

		return false
	end
end

local function Computed<T>(processor: () -> T, destructor: ((T) -> ())?): Types.Computed<T>
	local self = setmetatable({
		type = "State",
		kind = "Computed",
		dependencySet = {},
		-- if we held strong references to the dependents, then they wouldn't be
		-- able to get garbage collected when they fall out of scope
		dependentSet = setmetatable({}, WEAK_KEYS_METATABLE),
		_oldDependencySet = {},
		_processor = processor,
		_destructor = destructor,
		_value = nil,
	}, CLASS_METATABLE)

	initDependency(self)
	self:update()

	return self
end

return Computed


--!nonstrict

--[[
	Constructs a new ForKeys state object which maps keys of an array using
	a `processor` function.

	Optionally, a `destructor` function can be specified for cleaning up
	calculated keys. If omitted, the default cleanup function will be used instead.

	Optionally, a `meta` value can be returned in the processor function as the
	second value to pass data from the processor to the destructor.
]]

local Package = script.Parent.Parent
local PubTypes = require(Package.PubTypes)
local Types = require(Package.Types)
local captureDependencies = require(Package.Dependencies.captureDependencies)
local initDependency = require(Package.Dependencies.initDependency)
local useDependency = require(Package.Dependencies.useDependency)
local parseError = require(Package.Logging.parseError)
local logErrorNonFatal = require(Package.Logging.logErrorNonFatal)
local logError = require(Package.Logging.logError)
local logWarn = require(Package.Logging.logWarn)
local cleanup = require(Package.Utility.cleanup)
local needsDestruction = require(Package.Utility.needsDestruction)

local class = {}

local CLASS_METATABLE = { __index = class }
local WEAK_KEYS_METATABLE = { __mode = "k" }

--[[
	Returns the current value of this ForKeys object.
	The object will be registered as a dependency unless `asDependency` is false.
]]
function class:get(asDependency: boolean?): any
	if asDependency ~= false then
		useDependency(self)
	end
	return self._outputTable
end

--[[
	Called when the original table is changed.

	This will firstly find any keys meeting any of the following criteria:

	- they were not previously present
	- a dependency used during generation of this value has changed

	It will recalculate those key pairs, storing information about any
	dependencies used in the processor callback during output key generation,
	and save the new key to the output array with the same value. If it is
	overwriting an older value, that older value will be passed to the
	destructor for cleanup.

	Finally, this function will find keys that are no longer present, and remove
	their output keys from the output table and pass them to the destructor.
]]

function class:update(): boolean
	local inputIsState = self._inputIsState
	local newInputTable = if inputIsState then self._inputTable:get(false) else self._inputTable
	local oldInputTable = self._oldInputTable
	local outputTable = self._outputTable

	local keyOIMap = self._keyOIMap
	local keyIOMap = self._keyIOMap
	local meta = self._meta

	local didChange = false

	-- clean out main dependency set
	for dependency in pairs(self.dependencySet) do
		dependency.dependentSet[self] = nil
	end

	self._oldDependencySet, self.dependencySet = self.dependencySet, self._oldDependencySet
	table.clear(self.dependencySet)

	-- if the input table is a state object, add it as a dependency
	if inputIsState then
		self._inputTable.dependentSet[self] = true
		self.dependencySet[self._inputTable] = true
	end

	-- STEP 1: find keys that changed or were not previously present
	for newInKey, value in pairs(newInputTable) do
		-- get or create key data
		local keyData = self._keyData[newInKey]

		if keyData == nil then
			keyData = {
				dependencySet = setmetatable({}, WEAK_KEYS_METATABLE),
				oldDependencySet = setmetatable({}, WEAK_KEYS_METATABLE),
				dependencyValues = setmetatable({}, WEAK_KEYS_METATABLE),
			}
			self._keyData[newInKey] = keyData
		end

		-- check if the key is new
		local shouldRecalculate = oldInputTable[newInKey] == nil

		-- check if the key's dependencies have changed
		if shouldRecalculate == false then
			for dependency, oldValue in pairs(keyData.dependencyValues) do
				if oldValue ~= dependency:get(false) then
					shouldRecalculate = true
					break
				end
			end
		end

		-- recalculate the output key if necessary
		if shouldRecalculate then
			keyData.oldDependencySet, keyData.dependencySet = keyData.dependencySet, keyData.oldDependencySet
			table.clear(keyData.dependencySet)

			local processOK, newOutKey, newMetaValue = captureDependencies(
				keyData.dependencySet,
				self._processor,
				newInKey
			)

			if processOK then
				if self._destructor == nil and (needsDestruction(newOutKey) or needsDestruction(newMetaValue)) then
					logWarn("destructorNeededForKeys")
				end

				local oldInKey = keyOIMap[newOutKey]
				local oldOutKey = keyIOMap[newInKey]

				-- check for key collision
				if oldInKey ~= newInKey and newInputTable[oldInKey] ~= nil then
					logError("forKeysKeyCollision", nil, tostring(newOutKey), tostring(oldInKey), tostring(newOutKey))
				end

				-- check for a changed output key
				if oldOutKey ~= newOutKey and keyOIMap[oldOutKey] == newInKey then
					-- clean up the old calculated value
					local oldMetaValue = meta[oldOutKey]

					local destructOK, err = xpcall(self._destructor or cleanup, parseError, oldOutKey, oldMetaValue)
					if not destructOK then
						logErrorNonFatal("forKeysDestructorError", err)
					end

					keyOIMap[oldOutKey] = nil
					outputTable[oldOutKey] = nil
					meta[oldOutKey] = nil
				end

				-- update the stored data for this key
				oldInputTable[newInKey] = value
				meta[newOutKey] = newMetaValue
				keyOIMap[newOutKey] = newInKey
				keyIOMap[newInKey] = newOutKey
				outputTable[newOutKey] = value

				-- if we had to recalculate the output, then we did change
				didChange = true
			else
				-- restore old dependencies, because the new dependencies may be corrupt
				keyData.oldDependencySet, keyData.dependencySet = keyData.dependencySet, keyData.oldDependencySet

				logErrorNonFatal("forKeysProcessorError", newOutKey)
			end
		end

		-- save dependency values and add to main dependency set
		for dependency in pairs(keyData.dependencySet) do
			keyData.dependencyValues[dependency] = dependency:get(false)

			self.dependencySet[dependency] = true
			dependency.dependentSet[self] = true
		end
	end

	-- STEP 2: find keys that were removed
	for outputKey, inputKey in pairs(keyOIMap) do
		if newInputTable[inputKey] == nil then
			-- clean up the old calculated value
			local oldMetaValue = meta[outputKey]

			local destructOK, err = xpcall(self._destructor or cleanup, parseError, outputKey, oldMetaValue)
			if not destructOK then
				logErrorNonFatal("forKeysDestructorError", err)
			end

			-- remove data
			oldInputTable[inputKey] = nil
			meta[outputKey] = nil
			keyOIMap[outputKey] = nil
			keyIOMap[inputKey] = nil
			outputTable[outputKey] = nil
			self._keyData[inputKey] = nil

			-- if we removed a key, then the table/state changed
			didChange = true
		end
	end

	return didChange
end

local function ForKeys<KI, KO, M>(
	inputTable: PubTypes.CanBeState<{ [KI]: any }>,
	processor: (KI) -> (KO, M?),
	destructor: (KO, M?) -> ()?
): Types.ForKeys<KI, KO, M>

	local inputIsState = inputTable.type == "State" and typeof(inputTable.get) == "function"

	local self = setmetatable({
		type = "State",
		kind = "ForKeys",
		dependencySet = {},
		-- if we held strong references to the dependents, then they wouldn't be
		-- able to get garbage collected when they fall out of scope
		dependentSet = setmetatable({}, WEAK_KEYS_METATABLE),
		_oldDependencySet = {},

		_processor = processor,
		_destructor = destructor,
		_inputIsState = inputIsState,

		_inputTable = inputTable,
		_oldInputTable = {},
		_outputTable = {},
		_keyOIMap = {},
		_keyIOMap = {},
		_keyData = {},
		_meta = {},
	}, CLASS_METATABLE)

	initDependency(self)
	self:update()

	return self
end

return ForKeys


--!nonstrict

--[[
	Constructs a new ForPairs object which maps pairs of a table using
	a `processor` function.

	Optionally, a `destructor` function can be specified for cleaning up values.
	If omitted, the default cleanup function will be used instead.

	Additionally, a `meta` table/value can optionally be returned to pass data created
	when running the processor to the destructor when the created object is cleaned up.
]]

local Package = script.Parent.Parent
local PubTypes = require(Package.PubTypes)
local Types = require(Package.Types)
local captureDependencies = require(Package.Dependencies.captureDependencies)
local initDependency = require(Package.Dependencies.initDependency)
local useDependency = require(Package.Dependencies.useDependency)
local parseError = require(Package.Logging.parseError)
local logErrorNonFatal = require(Package.Logging.logErrorNonFatal)
local logError = require(Package.Logging.logError)
local logWarn = require(Package.Logging.logWarn)
local cleanup = require(Package.Utility.cleanup)
local needsDestruction = require(Package.Utility.needsDestruction)

local class = {}

local CLASS_METATABLE = { __index = class }
local WEAK_KEYS_METATABLE = { __mode = "k" }

--[[
	Returns the current value of this ForPairs object.
	The object will be registered as a dependency unless `asDependency` is false.
]]
function class:get(asDependency: boolean?): any
	if asDependency ~= false then
		useDependency(self)
	end
	return self._outputTable
end

--[[
	Called when the original table is changed.

	This will firstly find any keys meeting any of the following criteria:

	- they were not previously present
	- their associated value has changed
	- a dependency used during generation of this value has changed

	It will recalculate those key/value pairs, storing information about any
	dependencies used in the processor callback during value generation, and
	save the new key/value pair to the output array. If it is overwriting an
	older key/value pair, that older pair will be passed to the destructor
	for cleanup.

	Finally, this function will find keys that are no longer present, and remove
	their key/value pairs from the output table and pass them to the destructor.
]]
function class:update(): boolean
	local inputIsState = self._inputIsState
	local newInputTable = if inputIsState then self._inputTable:get(false) else self._inputTable
	local oldInputTable = self._oldInputTable

	local keyIOMap = self._keyIOMap
	local meta = self._meta

	local didChange = false

	-- clean out main dependency set
	for dependency in pairs(self.dependencySet) do
		dependency.dependentSet[self] = nil
	end

	self._oldDependencySet, self.dependencySet = self.dependencySet, self._oldDependencySet
	table.clear(self.dependencySet)

	-- if the input table is a state object, add it as a dependency
	if inputIsState then
		self._inputTable.dependentSet[self] = true
		self.dependencySet[self._inputTable] = true
	end

	-- clean out output table
	self._oldOutputTable, self._outputTable = self._outputTable, self._oldOutputTable

	local oldOutputTable = self._oldOutputTable
	local newOutputTable = self._outputTable
	table.clear(newOutputTable)

	-- Step 1: find key/value pairs that changed or were not previously present

	for newInKey, newInValue in pairs(newInputTable) do
		-- get or create key data
		local keyData = self._keyData[newInKey]

		if keyData == nil then
			keyData = {
				dependencySet = setmetatable({}, WEAK_KEYS_METATABLE),
				oldDependencySet = setmetatable({}, WEAK_KEYS_METATABLE),
				dependencyValues = setmetatable({}, WEAK_KEYS_METATABLE),
			}
			self._keyData[newInKey] = keyData
		end

		-- check if the pair is new or changed
		local shouldRecalculate = oldInputTable[newInKey] ~= newInValue

		-- check if the pair's dependencies have changed
		if shouldRecalculate == false then
			for dependency, oldValue in pairs(keyData.dependencyValues) do
				if oldValue ~= dependency:get(false) then
					shouldRecalculate = true
					break
				end
			end
		end

		-- recalculate the output pair if necessary
		if shouldRecalculate then
			keyData.oldDependencySet, keyData.dependencySet = keyData.dependencySet, keyData.oldDependencySet
			table.clear(keyData.dependencySet)

			local processOK, newOutKey, newOutValue, newMetaValue = captureDependencies(
				keyData.dependencySet,
				self._processor,
				newInKey,
				newInValue
			)

			if processOK then
				if self._destructor == nil and (needsDestruction(newOutKey) or needsDestruction(newOutValue) or needsDestruction(newMetaValue)) then
					logWarn("destructorNeededForPairs")
				end

				-- if this key was already written to on this run-through, throw a fatal error.
				if newOutputTable[newOutKey] ~= nil then
					-- figure out which key/value pair previously wrote to this key
					local previousNewKey, previousNewValue
					for inKey, outKey in pairs(keyIOMap) do
						if outKey == newOutKey then
							previousNewValue = newInputTable[inKey]
							if previousNewValue ~= nil then
								previousNewKey = inKey
								break
							end
						end
					end

					if previousNewKey ~= nil then
						logError(
							"forPairsKeyCollision",
							nil,
							tostring(newOutKey),
							tostring(previousNewKey),
							tostring(previousNewValue),
							tostring(newInKey),
							tostring(newInValue)
						)
					end
				end

				local oldOutValue = oldOutputTable[newOutKey]

				if oldOutValue ~= newOutValue then
					local oldMetaValue = meta[newOutKey]
					if oldOutValue ~= nil then
						local destructOK, err = xpcall(self._destructor or cleanup, parseError, newOutKey, oldOutValue, oldMetaValue)
						if not destructOK then
							logErrorNonFatal("forPairsDestructorError", err)
						end
					end

					oldOutputTable[newOutKey] = nil
				end

				-- update the stored data for this key/value pair
				oldInputTable[newInKey] = newInValue
				keyIOMap[newInKey] = newOutKey
				meta[newOutKey] = newMetaValue
				newOutputTable[newOutKey] = newOutValue

				-- if we had to recalculate the output, then we did change
				didChange = true
			else
				-- restore old dependencies, because the new dependencies may be corrupt
				keyData.oldDependencySet, keyData.dependencySet = keyData.dependencySet, keyData.oldDependencySet

				logErrorNonFatal("forPairsProcessorError", newOutKey)
			end
		else
			local storedOutKey = keyIOMap[newInKey]

			-- check for key collision
			if newOutputTable[storedOutKey] ~= nil then
				-- figure out which key/value pair previously wrote to this key
				local previousNewKey, previousNewValue
				for inKey, outKey in pairs(keyIOMap) do
					if storedOutKey == outKey then
						previousNewValue = newInputTable[inKey]

						if previousNewValue ~= nil then
							previousNewKey = inKey
							break
						end
					end
				end

				if previousNewKey ~= nil then
					logError(
						"forPairsKeyCollision",
						nil,
						tostring(storedOutKey),
						tostring(previousNewKey),
						tostring(previousNewValue),
						tostring(newInKey),
						tostring(newInValue)
					)
				end
			end

			-- copy the stored key/value pair into the new output table
			newOutputTable[storedOutKey] = oldOutputTable[storedOutKey]
		end

		-- save dependency values and add to main dependency set
		for dependency in pairs(keyData.dependencySet) do
			keyData.dependencyValues[dependency] = dependency:get(false)

			self.dependencySet[dependency] = true
			dependency.dependentSet[self] = true
		end
	end

	-- STEP 2: find keys that were removed
	for oldOutKey, oldOutValue in pairs(oldOutputTable) do
		-- check if this key/value pair is in the new output table
		if newOutputTable[oldOutKey] ~= oldOutValue then
			-- clean up the old output pair
			local oldMetaValue = meta[oldOutKey]
			if oldOutValue ~= nil then
				local destructOK, err = xpcall(self._destructor or cleanup, parseError, oldOutKey, oldOutValue, oldMetaValue)
				if not destructOK then
					logErrorNonFatal("forPairsDestructorError", err)
				end
			end

			-- check if the key was completely removed from the output table
			if newOutputTable[oldOutKey] == nil then
				meta[oldOutKey] = nil
				self._keyData[oldOutKey] = nil
			end

			didChange = true
		end
	end

	for key in pairs(oldInputTable) do
		if newInputTable[key] == nil then
			oldInputTable[key] = nil
			keyIOMap[key] = nil
		end
	end

	return didChange
end

local function ForPairs<KI, VI, KO, VO, M>(
	inputTable: PubTypes.CanBeState<{ [KI]: VI }>,
	processor: (KI, VI) -> (KO, VO, M?),
	destructor: (KO, VO, M?) -> ()?
): Types.ForPairs<KI, VI, KO, VO, M>

	local inputIsState = inputTable.type == "State" and typeof(inputTable.get) == "function"

	local self = setmetatable({
		type = "State",
		kind = "ForPairs",
		dependencySet = {},
		-- if we held strong references to the dependents, then they wouldn't be
		-- able to get garbage collected when they fall out of scope
		dependentSet = setmetatable({}, WEAK_KEYS_METATABLE),
		_oldDependencySet = {},

		_processor = processor,
		_destructor = destructor,
		_inputIsState = inputIsState,

		_inputTable = inputTable,
		_oldInputTable = {},
		_outputTable = {},
		_oldOutputTable = {},
		_keyIOMap = {},
		_keyData = {},
		_meta = {},
	}, CLASS_METATABLE)

	initDependency(self)
	self:update()

	return self
end

return ForPairs


--!nonstrict

--[[
	Constructs a new state object which can listen for updates on another state
	object.

	FIXME: enabling strict types here causes free types to leak
]]

local Package = script.Parent.Parent
local PubTypes = require(Package.PubTypes)
local Types = require(Package.Types)
local initDependency = require(Package.Dependencies.initDependency)

type Set<T> = {[T]: any}

local class = {}
local CLASS_METATABLE = {__index = class}

-- Table used to hold Observer objects in memory.
local strongRefs: Set<Types.Observer> = {}

--[[
	Called when the watched state changes value.
]]
function class:update(): boolean
	for _, callback in pairs(self._changeListeners) do
		task.spawn(callback)
	end
	return false
end

--[[
	Adds a change listener. When the watched state changes value, the listener
	will be fired.

	Returns a function which, when called, will disconnect the change listener.
	As long as there is at least one active change listener, this Observer
	will be held in memory, preventing GC, so disconnecting is important.
]]
function class:onChange(callback: () -> ()): () -> ()
	local uniqueIdentifier = {}

	self._numChangeListeners += 1
	self._changeListeners[uniqueIdentifier] = callback

	-- disallow gc (this is important to make sure changes are received)
	strongRefs[self] = true

	local disconnected = false
	return function()
		if disconnected then
			return
		end
		disconnected = true
		self._changeListeners[uniqueIdentifier] = nil
		self._numChangeListeners -= 1

		if self._numChangeListeners == 0 then
			-- allow gc if all listeners are disconnected
			strongRefs[self] = nil
		end
	end
end

local function Observer(watchedState: PubTypes.Value<any>): Types.Observer
	local self = setmetatable({
		type = "State",
		kind = "Observer",
		dependencySet = {[watchedState] = true},
		dependentSet = {},
		_changeListeners = {},
		_numChangeListeners = 0,
	}, CLASS_METATABLE)

	initDependency(self)
	-- add this object to the watched state's dependent set
	watchedState.dependentSet[self] = true

	return self
end

return Observer


--!nonstrict

--[[
	Constructs and returns objects which can be used to model independent
	reactive state.
]]

local Package = script.Parent.Parent
local Types = require(Package.Types)
local useDependency = require(Package.Dependencies.useDependency)
local initDependency = require(Package.Dependencies.initDependency)
local updateAll = require(Package.Dependencies.updateAll)
local isSimilar = require(Package.Utility.isSimilar)

local class = {}

local CLASS_METATABLE = {__index = class}
local WEAK_KEYS_METATABLE = {__mode = "k"}

--[[
	Returns the value currently stored in this State object.
	The state object will be registered as a dependency unless `asDependency` is
	false.
]]
function class:get(asDependency: boolean?): any
	if asDependency ~= false then
		useDependency(self)
	end
	return self._value
end

--[[
	Updates the value stored in this State object.

	If `force` is enabled, this will skip equality checks and always update the
	state object and any dependents - use this with care as this can lead to
	unnecessary updates.
]]
function class:set(newValue: any, force: boolean?)
	local oldValue = self._value
	if force or not isSimilar(oldValue, newValue) then
		self._value = newValue
		updateAll(self)
	end
end

local function Value<T>(initialValue: T): Types.State<T>
	local self = setmetatable({
		type = "State",
		kind = "Value",
		-- if we held strong references to the dependents, then they wouldn't be
		-- able to get garbage collected when they fall out of scope
		dependentSet = setmetatable({}, WEAK_KEYS_METATABLE),
		_value = initialValue
	}, CLASS_METATABLE)

	initDependency(self)

	return self
end

return Value


--!strict

--[[
	A common interface for accessing the values of state objects or constants.
]]

local Package = script.Parent.Parent
local PubTypes = require(Package.PubTypes)
local xtypeof = require(Package.Utility.xtypeof)

local function unwrap<T>(item: PubTypes.CanBeState<T>, useDependency: boolean?): T
	return if xtypeof(item) == "State" then (item :: PubTypes.StateObject<T>):get(useDependency) else (item :: T)
end

return unwrap


--!strict

--[[
	Stores common type information used internally.

	These types may be used internally so Fusion code can type-check, but
	should never be exposed to public users, as these definitions are fair game
	for breaking changes.
]]

local Package = script.Parent
local PubTypes = require(Package.PubTypes)

type Set<T> = {[T]: any}

--[[
	General use types
]]

-- A symbol that represents the absence of a value.
export type None = PubTypes.Symbol & {
	-- name: "None" (add this when Luau supports singleton types)
}

-- Stores useful information about Luau errors.
export type Error = {
	type: string, -- replace with "Error" when Luau supports singleton types
	raw: string,
	message: string,
	trace: string
}

--[[
	Specific reactive graph types
]]

-- A state object whose value can be set at any time by the user.
export type State<T> = PubTypes.Value<T> & {
	_value: T
}

-- A state object whose value is derived from other objects using a callback.
export type Computed<T> = PubTypes.Computed<T> & {
	_oldDependencySet: Set<PubTypes.Dependency>,
	_callback: () -> T,
	_value: T
}

-- A state object whose value is derived from other objects using a callback.
export type ForPairs<KI, VI, KO, VO, M> = PubTypes.ForPairs<KO, VO> & {
	_oldDependencySet: Set<PubTypes.Dependency>,
	_processor: (KI, VI) -> (KO, VO),
	_destructor: (VO, M?) -> (),
	_inputIsState: boolean,
	_inputTable: PubTypes.CanBeState<{ [KI]: VI }>,
	_oldInputTable: { [KI]: VI },
	_outputTable: { [KO]: VO },
	_oldOutputTable: { [KO]: VO },
	_keyIOMap: { [KI]: KO },
	_meta: { [KO]: M? },
	_keyData: {
		[KI]: {
			dependencySet: Set<PubTypes.Dependency>,
			oldDependencySet: Set<PubTypes.Dependency>,
			dependencyValues: { [PubTypes.Dependency]: any },
		},
	},
}

-- A state object whose value is derived from other objects using a callback.
export type ForKeys<KI, KO, M> = PubTypes.ForKeys<KO, any> & {
	_oldDependencySet: Set<PubTypes.Dependency>,
	_processor: (KI) -> (KO),
	_destructor: (KO, M?) -> (),
	_inputIsState: boolean,
	_inputTable: PubTypes.CanBeState<{ [KI]: KO }>,
	_oldInputTable: { [KI]: KO },
	_outputTable: { [KO]: any },
	_keyOIMap: { [KO]: KI },
	_meta: { [KO]: M? },
	_keyData: {
		[KI]: {
			dependencySet: Set<PubTypes.Dependency>,
			oldDependencySet: Set<PubTypes.Dependency>,
			dependencyValues: { [PubTypes.Dependency]: any },
		},
	},
}

-- A state object whose value is derived from other objects using a callback.
export type ForValues<VI, VO, M> = PubTypes.ForValues<any, VO> & {
	_oldDependencySet: Set<PubTypes.Dependency>,
	_processor: (VI) -> (VO),
	_destructor: (VO, M?) -> (),
	_inputIsState: boolean,
	_inputTable: PubTypes.CanBeState<{ [VI]: VO }>,
	_outputTable: { [any]: VI },
	_valueCache: { [VO]: any },
	_oldValueCache: { [VO]: any },
	_meta: { [VO]: M? },
	_valueData: {
		[VI]: {
			dependencySet: Set<PubTypes.Dependency>,
			oldDependencySet: Set<PubTypes.Dependency>,
			dependencyValues: { [PubTypes.Dependency]: any },
		},
	},
}

-- A state object which follows another state object using tweens.
export type Tween<T> = PubTypes.Tween<T> & {
	_goalState: State<T>,
	_tweenInfo: TweenInfo,
	_prevValue: T,
	_nextValue: T,
	_currentValue: T,
	_currentTweenInfo: TweenInfo,
	_currentTweenDuration: number,
	_currentTweenStartTime: number,
	_currentlyAnimating: boolean
}

-- A state object which follows another state object using spring simulation.
export type Spring<T> = PubTypes.Spring<T> & {
	_speed: PubTypes.CanBeState<number>,
	_speedIsState: boolean,
	_lastSpeed: number,
	_damping: PubTypes.CanBeState<number>,
	_dampingIsState: boolean,
	_lastDamping: number,
	_goalState: State<T>,
	_goalValue: T,
	_currentType: string,
	_currentValue: T,
	_springPositions: {number},
	_springGoals: {number},
	_springVelocities: {number}
}

-- An object which can listen for updates on another state object.
export type Observer = PubTypes.Observer & {
	_changeListeners: Set<() -> ()>,
	_numChangeListeners: number
}

return nil


--!strict

--[[
	A symbol for representing nil values in contexts where nil is not usable.
]]

local Package = script.Parent.Parent
local Types = require(Package.Types)

return {
	type = "Symbol",
	name = "None"
} :: Types.None


--!strict

--[[
	Cleans up the tasks passed in as the arguments.
	A task can be any of the following:

	- an Instance - will be destroyed
	- an RBXScriptConnection - will be disconnected
	- a function - will be run
	- a table with a `Destroy` or `destroy` function - will be called
	- an array - `cleanup` will be called on each item
]]

local function cleanupOne(task: any)
	local taskType = typeof(task)

	-- case 1: Instance
	if taskType == "Instance" then
		task:Destroy()

	-- case 2: RBXScriptConnection
	elseif taskType == "RBXScriptConnection" then
		task:Disconnect()

	-- case 3: callback
	elseif taskType == "function" then
		task()

	elseif taskType == "table" then
		-- case 4: destroy() function
		if typeof(task.destroy) == "function" then
			task:destroy()

		-- case 5: Destroy() function
		elseif typeof(task.Destroy) == "function" then
			task:Destroy()

		-- case 6: array of tasks
		elseif task[1] ~= nil then
			for _, subtask in ipairs(task) do
				cleanupOne(subtask)
			end
		end
	end
end

local function cleanup(...: any)
	for index = 1, select("#", ...) do
		cleanupOne(select(index, ...))
	end
end

return cleanup


--!strict

--[[
	An empty function. Often used as a destructor to indicate no destruction.
]]

local function doNothing(...: any)
end

return doNothing


--!strict
--[[
    Returns true if A and B are 'similar' - i.e. any user of A would not need
    to recompute if it changed to B.
]]

local function isSimilar(a: any, b: any): boolean
    -- HACK: because tables are mutable data structures, don't make assumptions
    -- about similarity from equality for now (see issue #44)
    if typeof(a) == "table" then
        return false
    else
        return a == b
    end
end

return isSimilar


--!strict

--[[
    Returns true if the given value is not automatically memory managed, and
    requires manual cleanup.
]]

local function needsDestruction(x: any): boolean
    return typeof(x) == "Instance"
end

return needsDestruction


--!strict

--[[
	Restricts the reading of missing members for a table.
]]

local Package = script.Parent.Parent
local logError = require(Package.Logging.logError)

type table = {[any]: any}

local function restrictRead(tableName: string, strictTable: table): table
	-- FIXME: Typed Luau doesn't recognise this correctly yet
	local metatable = getmetatable(strictTable :: any)

	if metatable == nil then
		metatable = {}
		setmetatable(strictTable, metatable)
	end

	function metatable:__index(memberName)
		logError("strictReadError", nil, tostring(memberName), tableName)
	end

	return strictTable
end

return restrictRead


--!strict

--[[
	Extended typeof, designed for identifying custom objects.
	If given a table with a `type` string, returns that.
	Otherwise, returns `typeof()` the argument.
]]

local function xtypeof(x: any)
	local typeString = typeof(x)

	if typeString == "table" and typeof(x.type) == "string" then
		return x.type
	else
		return typeString
	end
end

return xtypeof


--!strict

--[[
	The entry point for the Fusion library.
]]

local PubTypes = require(script.PubTypes)
local restrictRead = require(script.Utility.restrictRead)

export type StateObject<T> = PubTypes.StateObject<T>
export type CanBeState<T> = PubTypes.CanBeState<T>
export type Symbol = PubTypes.Symbol
export type Value<T> = PubTypes.Value<T>
export type Computed<T> = PubTypes.Computed<T>
export type ForPairs<KO, VO> = PubTypes.ForPairs<KO, VO>
export type ForKeys<KI, KO> = PubTypes.ForKeys<KI, KO>
export type ForValues<VI, VO> = PubTypes.ForKeys<VI, VO>
export type Observer = PubTypes.Observer
export type Tween<T> = PubTypes.Tween<T>
export type Spring<T> = PubTypes.Spring<T>

type Fusion = {
	version: PubTypes.Version,

	New: (className: string) -> ((propertyTable: PubTypes.PropertyTable) -> Instance),
	Hydrate: (target: Instance) -> ((propertyTable: PubTypes.PropertyTable) -> Instance),
	Ref: PubTypes.SpecialKey,
	Cleanup: PubTypes.SpecialKey,
	Children: PubTypes.SpecialKey,
	Out: PubTypes.SpecialKey,
	OnEvent: (eventName: string) -> PubTypes.SpecialKey,
	OnChange: (propertyName: string) -> PubTypes.SpecialKey,

	Value: <T>(initialValue: T) -> Value<T>,
	Computed: <T>(callback: () -> T, destructor: (T) -> ()?) -> Computed<T>,
	ForPairs: <KI, VI, KO, VO, M>(inputTable: CanBeState<{[KI]: VI}>, processor: (KI, VI) -> (KO, VO, M?), destructor: (KO, VO, M?) -> ()?) -> ForPairs<KO, VO>,
	ForKeys: <KI, KO, M>(inputTable: CanBeState<{[KI]: any}>, processor: (KI) -> (KO, M?), destructor: (KO, M?) -> ()?) -> ForKeys<KO, any>,
	ForValues: <VI, VO, M>(inputTable: CanBeState<{[any]: VI}>, processor: (VI) -> (VO, M?), destructor: (VO, M?) -> ()?) -> ForValues<any, VO>,
	Observer: (watchedState: StateObject<any>) -> Observer,

	Tween: <T>(goalState: StateObject<T>, tweenInfo: TweenInfo?) -> Tween<T>,
	Spring: <T>(goalState: StateObject<T>, speed: number?, damping: number?) -> Spring<T>,

	cleanup: (...any) -> (),
	doNothing: (...any) -> ()
}

return restrictRead("Fusion", {
	version = {major = 0, minor = 2, isRelease = true},

	New = require(script.Instances.New),
	Hydrate = require(script.Instances.Hydrate),
	Ref = require(script.Instances.Ref),
	Out = require(script.Instances.Out),
	Cleanup = require(script.Instances.Cleanup),
	Children = require(script.Instances.Children),
	OnEvent = require(script.Instances.OnEvent),
	OnChange = require(script.Instances.OnChange),

	Value = require(script.State.Value),
	Computed = require(script.State.Computed),
	ForPairs = require(script.State.ForPairs),
	ForKeys = require(script.State.ForKeys),
	ForValues = require(script.State.ForValues),
	Observer = require(script.State.Observer),

	Tween = require(script.Animation.Tween),
	Spring = require(script.Animation.Spring),

	cleanup = require(script.Utility.cleanup),
	doNothing = require(script.Utility.doNothing)
}) :: Fusion


local TextService = game:GetService("TextService")
local util = {}

-- Below function is basically copy and pasted from Roblox core scripts for the chat
function util.getStringTextBounds(text, font, textSize, sizeBounds)
	sizeBounds = sizeBounds or Vector2.new(10000, 10000)
	return TextService:GetTextSize(text, textSize, font, sizeBounds)
end

function util.getMessageHeight(BaseMessage, BaseFrame, xSize)
	xSize = xSize or BaseFrame.AbsoluteSize.X
	local textBoundsSize = util.getStringTextBounds(BaseMessage.Text, BaseMessage.Font, BaseMessage.TextSize, Vector2.new(xSize, 1000))
	if textBoundsSize.Y ~= math.floor(textBoundsSize.Y) then
		-- some hack ig
		return textBoundsSize.Y + 1
	end
	return textBoundsSize.Y
end

return util


--!strict
--!native

--[[
	CuteSignal
	- Sync listeners fire immediately.
	- Async listeners run through a reusable coroutine runner.
	- Waiters suspend and resume on the next fire.
]]

-- Type definitions
type Callback = (...any) -> ()
export type Disconnect = () -> ()

type SignalCore = {
	Fire: (self: Signal, ...any) -> (),
	Connect: (self: Signal, callback: Callback) -> Disconnect,
	ConnectAsync: (self: Signal, callback: Callback) -> Disconnect,
	Wait: (self: Signal) -> ...any,
	DisconnectAll: (self: Signal) -> (),
	Destroy: (self: Signal) -> (),
	Count: (self: Signal) -> number,
}

export type Signal = SignalCore & {
	Once: (self: Signal, callback: Callback) -> Disconnect,
	HasConnections: (self: Signal) -> boolean,
	IsDestroyed: (self: Signal) -> boolean,
}

type Slot = {
	index: number,
	generation: number,
}

type FireFn = (self: Signal, ...any) -> ()

-- Localized globals
local table_create = table.create
local table_clear = table.clear
local table_freeze = table.freeze
local coroutine_create = coroutine.create
local coroutine_resume = coroutine.resume
local coroutine_running = coroutine.running
local coroutine_yield = coroutine.yield

-- Async callback runner cache.
-- Reuses one coroutine when async handlers do not yield, reducing allocations.
local freeAsyncRunnerThread: thread? = nil

local function RunAsyncCallback(callback: Callback, ...): ()
	local runner = freeAsyncRunnerThread
	freeAsyncRunnerThread = nil
	callback(...)
	freeAsyncRunnerThread = runner
end

local function AsyncRunnerLoop(): ()
	while true do
		RunAsyncCallback(coroutine_yield())
	end
end

local function DispatchAsyncRunner(callback: Callback, ...): ()
	local runner = freeAsyncRunnerThread
	if runner == nil then
		runner = coroutine_create(AsyncRunnerLoop)
		coroutine_resume(runner :: thread)
		freeAsyncRunnerThread = runner
	end
	coroutine_resume(runner :: thread, callback, ...)
end

-- No-op and destroyed-state handlers.
-- These keep hot paths branch-light by swapping function references.
local function NoopFire(_self: Signal, ...): ()
	return
end

local function NoopDisconnectAll(_self: Signal): ()
	return
end

local function NoopDestroy(_self: Signal): ()
	return
end

local function ZeroCount(_self: Signal): number
	return 0
end

local function DestroyedConnect(_self: Signal, _callback: Callback): never
	error("Signal is destroyed")
end

local function DestroyedWait(_self: Signal): never
	error("Signal is destroyed")
end

-- Shared methods exposed through the metatable.
local function SignalOnce(self: Signal, callback: Callback): Disconnect
	if type(callback) ~= "function" then
		error("Once expects a function callback")
	end

	local disconnect = nil :: any
	disconnect = self:Connect(function(...: any)
		disconnect()
		callback(...)
	end)

	return disconnect
end

local function SignalHasConnections(self: Signal): boolean
	return self:Count() ~= 0
end

local function SignalIsDestroyed(self: Signal): boolean
	return self.Connect == DestroyedConnect
end

local SIGNAL_METHODS = table_freeze({
	Once = SignalOnce,
	HasConnections = SignalHasConnections,
	IsDestroyed = SignalIsDestroyed,
})

local SIGNAL_METATABLE = table_freeze({
	__index = SIGNAL_METHODS,
})

-- Signal constructor.
local function NewSignal(): Signal
	-- Sync listener state
	local syncCount = 0
	local syncGeneration = 1
	local syncCallbacks = {} :: { [number]: Callback? }
	local syncSlots = {} :: { [number]: Slot? }

	-- Async listener state
	local asyncCount = 0
	local asyncGeneration = 1
	local asyncCallbacks = {} :: { [number]: Callback? }
	local asyncSlots = {} :: { [number]: Slot? }

	-- Waiting coroutine state
	local waitCount = 0
	local waitThreads = {} :: { [number]: thread? }

	local signal: Signal
	local syncFire: FireFn = NoopFire
	local asyncFire: FireFn = NoopFire
	local baseFire: FireFn = NoopFire
	local fireWithWait: FireFn

	-- Recomputes the currently active `Fire` implementation.
	-- Waiters wrap the combined listener fire path when present.
	local function RebuildFire(): ()
		local fireNoWait: FireFn

		if asyncCount == 0 then
			fireNoWait = syncFire
		elseif syncCount == 0 then
			fireNoWait = asyncFire
		else
			fireNoWait = function(self: Signal, ...: any): ()
				asyncFire(self, ...)
				syncFire(self, ...)
			end
		end

		baseFire = fireNoWait
		if waitCount == 0 then
			signal.Fire = fireNoWait
		else
			signal.Fire = fireWithWait
		end
	end

	-- Rebuild sync dispatch function using a stable snapshot.
	local function RebuildSyncFire(): ()
		local fire: FireFn

		if syncCount == 0 then
			fire = NoopFire
		elseif syncCount == 1 then
			fire = function(_self: Signal, ...: any): ()
				(syncCallbacks[1] :: Callback)(...)
			end
		else
			local snapshot = table_create(syncCount) :: { [number]: Callback }
			local source = syncCount
			for i = 1, syncCount do
				-- Newest-first execution order.
				snapshot[i] = syncCallbacks[source] :: Callback
				source -= 1
			end

			local snap = snapshot
			local n = syncCount
			fire = function(_self: Signal, ...: any): ()
				for i = 1, n do
					(snap[i] :: Callback)(...)
				end
			end
		end

		syncFire = fire
		RebuildFire()
	end

	-- Rebuild async dispatch function using a stable snapshot.
	local function RebuildAsyncFire(): ()
		local fire: FireFn

		if asyncCount == 0 then
			fire = NoopFire
		else
			local snapshot = table_create(asyncCount) :: { [number]: Callback }
			local source = asyncCount
			for i = 1, asyncCount do
				-- Newest-first execution order.
				snapshot[i] = asyncCallbacks[source] :: Callback
				source -= 1
			end

			local snap = snapshot
			local n = asyncCount
			fire = function(_self: Signal, ...: any): ()
				for i = 1, n do
					DispatchAsyncRunner(snap[i] :: Callback, ...)
				end
			end
		end

		asyncFire = fire
		RebuildFire()
	end

	-- Suspend the current coroutine until the next fire.
	local function Wait(self: Signal): ...any
		if self.Connect == DestroyedConnect then
			error("Signal is destroyed")
		end

		local thread = coroutine_running()
		if thread == nil then
			error("Wait must be called from a running coroutine")
		end

		local index = waitCount + 1
		waitCount = index
		waitThreads[index] = thread
		if index == 1 then
			signal.Fire = fireWithWait
		end

		return coroutine_yield()
	end

	-- Fire path used while at least one thread is waiting.
	fireWithWait = function(self: Signal, ...: any): ()
		local n = waitCount
		if n == 0 then
			signal.Fire = baseFire
			baseFire(self, ...)
			return
		end

		local snapshot = table_create(n) :: { [number]: thread }
		for i = 1, n do
			snapshot[i] = waitThreads[i] :: thread
			waitThreads[i] = nil
		end
		waitCount = 0

		-- Return to no-wait fast path before listener execution.
		signal.Fire = baseFire
		baseFire(self, ...)

		for i = 1, n do
			coroutine_resume(snapshot[i] :: thread, ...)
		end
	end

	-- Register a synchronous callback.
	local function Connect(_self: Signal, callback: Callback): Disconnect
		if type(callback) ~= "function" then
			error("Connect expects a function callback")
		end

		local index = syncCount + 1
		syncCount = index
		syncCallbacks[index] = callback

		local slot = {
			index = index,
			generation = syncGeneration,
		}
		syncSlots[index] = slot

		RebuildSyncFire()

		return function(): ()
			if slot.generation ~= syncGeneration then
				return
			end

			local removeIndex = slot.index
			if removeIndex == 0 then
				return
			end

			local last = syncCount
			if removeIndex ~= last then
				-- O(1) remove by moving the last callback into the removed slot.
				syncCallbacks[removeIndex] = syncCallbacks[last]
				syncCallbacks[last] = nil

				local movedSlot = syncSlots[last]
				syncSlots[removeIndex] = movedSlot
				syncSlots[last] = nil
				if movedSlot ~= nil then
					movedSlot.index = removeIndex
				end
			else
				syncCallbacks[last] = nil
				syncSlots[last] = nil
			end

			syncCount = last - 1
			slot.index = 0
			RebuildSyncFire()
		end
	end

	-- Register an asynchronous callback.
	local function ConnectAsync(_self: Signal, callback: Callback): Disconnect
		if type(callback) ~= "function" then
			error("ConnectAsync expects a function callback")
		end

		local index = asyncCount + 1
		asyncCount = index
		asyncCallbacks[index] = callback

		local slot = {
			index = index,
			generation = asyncGeneration,
		}
		asyncSlots[index] = slot

		RebuildAsyncFire()

		return function(): ()
			if slot.generation ~= asyncGeneration then
				return
			end

			local removeIndex = slot.index
			if removeIndex == 0 then
				return
			end

			local last = asyncCount
			if removeIndex ~= last then
				-- O(1) remove by moving the last callback into the removed slot.
				asyncCallbacks[removeIndex] = asyncCallbacks[last]
				asyncCallbacks[last] = nil

				local movedSlot = asyncSlots[last]
				asyncSlots[removeIndex] = movedSlot
				asyncSlots[last] = nil
				if movedSlot ~= nil then
					movedSlot.index = removeIndex
				end
			else
				asyncCallbacks[last] = nil
				asyncSlots[last] = nil
			end

			asyncCount = last - 1
			slot.index = 0
			RebuildAsyncFire()
		end
	end

	-- Clear all listeners and waiters, and reset dispatch fast paths.
	local function DisconnectAll(_self: Signal): ()
		if syncCount == 0 and asyncCount == 0 and waitCount == 0 then
			return
		end

		syncGeneration += 1
		asyncGeneration += 1
		syncCount = 0
		asyncCount = 0
		waitCount = 0
		table_clear(syncCallbacks)
		table_clear(syncSlots)
		table_clear(asyncCallbacks)
		table_clear(asyncSlots)
		table_clear(waitThreads)

		syncFire = NoopFire
		asyncFire = NoopFire
		baseFire = NoopFire
		signal.Fire = NoopFire
	end

	-- Permanently disable the signal instance.
	local function Destroy(self: Signal): ()
		DisconnectAll(self)
		signal.Connect = DestroyedConnect
		signal.ConnectAsync = DestroyedConnect
		signal.Wait = DestroyedWait
		signal.DisconnectAll = NoopDisconnectAll
		signal.Destroy = NoopDestroy
		signal.Count = ZeroCount
	end

	local function Count(_self: Signal): number
		return syncCount + asyncCount
	end

	signal = {
		Fire = NoopFire,
		Connect = Connect,
		ConnectAsync = ConnectAsync,
		Wait = Wait,
		DisconnectAll = DisconnectAll,
		Destroy = Destroy,
		Count = Count,
	} :: Signal

	-- Cast through `any` to satisfy strict typing around metatable extension.
	return (setmetatable(signal :: Signal, SIGNAL_METATABLE) :: any) :: Signal
end

return table_freeze({
	new = NewSignal,
})


local fs = require('@lune/fs')
local process = require('@lune/process')
local serde = require('@lune/serde')

local function dotenv()
	local success, content = pcall(fs.readFile, `{process.cwd}/.env`)

	if success and type(content) == 'string' then
		success, content = pcall(serde.decode, 'toml', content)

		if success and content then
			for key, value in content do
				process.env[key] = value
			end
		else
			warn('unable to parse .env file: ', content)
		end
	end

	return process.env
end

return dotenv


local fs = require('@lune/fs')

pcall(fs.removeDir, 'dist')
fs.copy('src', 'dist/lib')
fs.writeDir('dist/roblox_packages')
fs.writeFile(
	'dist/roblox_packages/vide.luau',
	[[
local TS = _G[script.Parent.Parent.lib]
return TS.import(script.Parent.Parent.lib, TS.getModule(script.Parent.Parent.lib, "@rbxts", "vide").src)]]
)


local LocalizationService = game:GetService('LocalizationService')
local Players = game:GetService('Players')

local vide = require(script.Parent.roblox_packages.vide)

local local_player = Players.LocalPlayer

local player_locale_translator = vide.source(nil :: Translator?)
if local_player then
	local function update_localized_translator()
		local success, result =
			pcall(LocalizationService.GetTranslatorForPlayerAsync, LocalizationService, local_player)
		if success then
			player_locale_translator(result)
		else
			player_locale_translator(nil)
			warn('lexi: getting player translator failed. falling back to the source language.', result)
		end
	end

	task.spawn(update_localized_translator)
	local_player:GetPropertyChangedSignal('LocaleId'):Connect(update_localized_translator)
end

local function has_placeholders(template: string): boolean
	return string.gmatch(template, '{(.-)}')() ~= nil
end

type function get_placeholders(template: string)
	local placeholders = {}

	local placeholder_types = {
		int = types.number,
		fixed = types.number,
		num = types.number,
		HEX = types.number,
		hex = types.number,
		datetime = types.number,
		iso8601 = types.number,
		shorttime = types.number,
		shortdatetime = types.number,
		shortdate = types.number,
		translate = types.string,
	}

	for key in string.gmatch(template, '{(.-)}') do
		local name, kind = unpack(string.split(key, ':'))
		local ty: type = if kind then placeholder_types[kind] else types.string
		placeholders[types.singleton(name)] = assert(ty, `Unsupported placeholder type {kind}`)
	end

	return types.newtable(placeholders)
end

type function Lexicon(entries: type)
	assert(entries:is('table'), 'entries must be a table')

	local function recurse(entry: type)
		if entry:is('table') then
			local result = {}
			for name, entry in entry:properties() do
				result[name] = recurse(assert(entry.read))
			end
			return types.newtable(result)
		end

		local template = entry:value()
		assert(type(template) == 'string')
		local placeholders: type = get_placeholders(template)
		return types.newfunction(
			{ head = if next(placeholders:properties()) then { placeholders } else {} },
			{ head = { types.string } }
		)
	end

	local lexicon = {}
	for name, entry in entries:properties() do
		lexicon[name] = recurse(assert(entry.read))
	end

	return types.newtable(lexicon)
end

local lexi_context_mt = {}

local function context<I>(source: I & string)
	return function(context: string): I
		return setmetatable({ source = source, context = context }, lexi_context_mt) :: any
	end
end

local function try_format(translator: Translator, key: string, params: { [string]: string | number }?): string?
	local success, result = pcall(translator.FormatByKey, translator, key, params)
	if success then
		return result
	end

	return nil
end

local function lexicon<E>(source_locale: string, entries: E): Lexicon<E>
	local localization_table = Instance.new('LocalizationTable')
	localization_table.SourceLocaleId = source_locale
	localization_table.Name = 'lexi_lexicon'
	localization_table.Archivable = false
	localization_table.Parent = LocalizationService

	local lexicon_translator = localization_table:GetTranslator(source_locale)

	local function recurse(entry: string | { [string]: unknown }, key: string): unknown
		local source = ''
		local context = ''

		if getmetatable(entry) == lexi_context_mt then
			source = (entry :: { source: string }).source
			context = (entry :: { context: string }).context
		elseif type(entry) == 'table' then
			local result = {}
			for name, entry in entry do
				result[name] = recurse(entry :: any, `{key}.{name}`)
			end
			return result
		else
			source = entry
		end

		local function handle(params: { [string]: string | number }?)
			local result: string?
			local localized = player_locale_translator()

			if
				localized
				-- the ones given to us will be more up-to-date
				and localized.LocaleId ~= source_locale
			then
				result = try_format(localized, key, params)
			end

			if not result then
				result = try_format(lexicon_translator, key, params)
			end

			return result
				-- shouldn't happen
				or entry
		end

		localization_table:SetEntryValue(key, source, context, source_locale, source)

		return if has_placeholders(source) then handle else vide.derive(handle :: never)
	end

	local lexicon = {}
	vide.root(function()
		for name, entry in entries :: { [string]: unknown } do
			lexicon[name] = recurse(entry :: any, name)
		end
	end)

	return lexicon :: any
end

return {
	context = context,
	lexicon = lexicon,
}


--[[
This is a server script.
An example script that uses the module to convert all players faces into dynamic ones (if possible).
]]

local Players = game:GetService("Players")

-- Don't forget to require module from wherever you put it!
local ClassicHeadConversion = require("@path/to/ClassicHeadConversion")

local function onPlayerCharacterAppearenceLoaded(character: Model)
	ClassicHeadConversion.ChangeFace(character) --try to change face to classic
end

local function onPlayerAdded(player: Player)
	player.CharacterAppearanceLoaded:Connect(onPlayerCharacterAppearenceLoaded)
	if player:HasAppearanceLoaded() then --checks if all the accessories has been loaded already
		onPlayerCharacterAppearenceLoaded(player.Character)
	end
end

-- Initalize
Players.PlayerAdded:Connect(onPlayerAdded)

for _, player in Players:GetPlayers() do
	onPlayerAdded(player)
end


--[[

	Author: @Conikku
	Start Date: August 15, 2023
	Last Updated: November 19, 2023

	Note:
	Credit Roblox user @Conikku if used.

]]

local ConversionHandler = {}

local FacesTable = require(script.FacesTable) -- Table for heads is in a seperate module to make it easier to edit and to make this script have less lines

local function CreateMesh(Head_Mesh, MatchHead, ParentTo) -- This is to make the mesh for the Head
	Head_Mesh = Instance.new("SpecialMesh")
	Head_Mesh.MeshId = "rbxassetid://"..MatchHead
	Head_Mesh.Parent = ParentTo
end

function ConversionHandler.ChangeFace(Character)

	if Character:IsA("Model") and Character:FindFirstChild("Humanoid") then

		local URL, ID, Face, MatchHead, IDofMesh -- Variables that will be used later
		local Head, Humanoid = Character:WaitForChild("Head", 10), Character:WaitForChild("Humanoid", 10)

		local Head_Mesh = Head:FindFirstChild("Mesh") -- Check if there's a specialmesh

		if Head_Mesh == nil then -- If nil then change Head_Mesh to the player's head and set URL to the TextureID of the head
			warn("Head_Mesh is nil")
			Head_Mesh = Head -- Set to head

			task.wait()

			URL = Head.MeshId -- Get URL of Mesh
			ID = tonumber(string.split(tostring(URL), "https://assetdelivery.roblox.com/v1/asset/?id=")[2]) -- Get ID from URL

			if not FacesTable[ID] then return end -- If can't find the MeshID of the head in the FacesTable, then end the script, if not then continue
			MatchHead = FacesTable[ID]

			local InsertHats = {} -- This is the table where we will be inserting the players hats
			local ClonedHead = Head_Mesh:Clone() -- This is to make it easier to get hat attachmenets and all that jazz

			for i, hat in pairs(Character:GetChildren()) do -- Get hats from player, clone them, and insert the cloned into the table

				if hat:IsA("Accessory") then
					table.insert(InsertHats, hat:Clone())
					task.wait()
					hat:Destroy()
				end

			end

			Humanoid.RequiresNeck = false -- Temporary set this to false so the character doesn't die when getting rid of the current head

			local NewHead = Instance.new("Part") -- Start making the new head. We wouldn't have to do if Roblox let you change MeshID's on MeshParts
			NewHead.Name = "Head"
			NewHead.Size = Vector3.new(2, 1, 1)
			NewHead.CFrame = Head.CFrame
			NewHead.BrickColor = Head.BrickColor

			CreateMesh(NewHead, MatchHead.Head, NewHead) -- Create mesh for the head

			local Motor -- This will be used to detect where the motor is located depending on if the player is in R6 or R15

			if Humanoid.RigType == Enum.HumanoidRigType.R15 then -- If in R15 change the parent of the current head's Neck Motor6D to the soon to be new head
				Head_Mesh.Neck.Parent = NewHead
			end

			NewHead.Parent = Character -- Parent new head to Character
			Head_Mesh:Destroy() -- very obvious what this should be, duh!

			Head_Mesh = Character.Head

			if Humanoid.RigType == Enum.HumanoidRigType.R15 then -- Now we set the Motor6D
				Motor = Head_Mesh.Neck
				Motor.Parent = Head_Mesh
			elseif Humanoid.RigType == Enum.HumanoidRigType.R6 then
				Motor = Character.Torso.Neck
			end

			Motor.Part1 = Head_Mesh -- Parent it to the head

			Face = Instance.new("Decal") -- Create new Face Texture
			Face.Name = "Face"
			Face.Face = Enum.NormalId.Front
			Face.ZIndex = 1
			Face.Parent = Head_Mesh

			for i, head in pairs(ClonedHead:GetChildren()) do -- Now we get the attachments from the ClonedHead and we set them to the current head of player

				if head:IsA("Attachment") or head:IsA("StringValue") then
					head.Parent = Head_Mesh
				end

			end

			task.wait()

			for i, hats in InsertHats do -- Now we set the hats back to the player
				hats.Parent = Character
			end

			Humanoid.RequiresNeck = true -- Now the character can die if the head or neck Motor6D is deleted, lol.
			ClonedHead:Destroy() -- Destroy this just in case
		elseif Head_Mesh.Name == "Mesh" then -- If not nil then set URL to specialmesh ID
			if not FacesTable[ID] then return end -- If can't find the MeshID of the head in the FacesTable, then end the script, if not then continue
			MatchHead = FacesTable[ID]

			URL = Head_Mesh.MeshId -- Get URL of Head_Mesh
			ID = tonumber(string.split(tostring(URL), "https://assetdelivery.roblox.com/v1/asset/?id=")[2]) -- Get ID from URL

			Head_Mesh:Destroy() -- Destroy Head_Mesh
			task.wait()
			CreateMesh(Head, MatchHead.Head, Head) -- Create mesh for the head
		end

		Face.Texture = "rbxassetid://"..MatchHead.Face -- Set face Texture

	end

end

return ConversionHandler


--[[

	Author: @Conikku
	Date: August 16, 2023

	Place in ServerScriptStorage or Workspace

	Note:
	Credit Roblox user @Conikku if used.
	Will be updated when more dynamic faces are made, module will auto update live in servers so be aware it could break stuff depending on how you coded it.
	If you are modifying it yourself grab the module here: https://www.roblox.com/library/14449877379/
	Just make sure to check back every now and then to see if I updated the module so new dynamic heads will work

]]

-- Services
local Players = game:GetService("Players")
local InsertService = game:GetService("InsertService")

-- Modules
local Module

-- Constants
local MODULE_UPDATE_TIME = 60
local MODULE_ID = 14449877379

-- Variables
local moduleLastId
local moduleLoaded = false

function getLatestModule()
	local latest = InsertService:GetLatestAssetVersionAsync(MODULE_ID)
	if moduleLastId ~= latest then
		moduleLoaded = false
		moduleLastId = latest
		local module = InsertService:LoadAssetVersion(moduleLastId):GetChildren()[1]

		if script:FindFirstChild("MainModule") then
			script.MainModule:Destroy()
		end

		Module = require(module)
		moduleLoaded = true
		print("Module has been updated")
	else
		print("Module is already up to date")
	end
end

do
	spawn(function()
		while true do
			getLatestModule()
			task.wait(MODULE_UPDATE_TIME)
		end
	end)
end

Players.PlayerAdded:Connect(function(plr)
	plr.CharacterAppearanceLoaded:Connect(function(chr)
		repeat task.wait() until Module ~= nil
		Module.ChangeFace(chr)
	end)
end)


--!strict
-- [Dependencies]
local Promise = require(script.Parent["typed-promise"])

local Modula = {}

-- [Types]
export type ModulaType = typeof(Modula)
type PredicateFn = (Module: ModuleScript) -> boolean
type DirectoryType = {[number]: ModuleScript | Instance}
type DepthType = "Children" | "Descendants"
type ConfigType = {
	Depth: DepthType?,
	MatchName: string?,
	InitName: string?,
	StartName: string?,
}

-- [Helper Functions]
local function MatchesName(MatchName: string): (Module: ModuleScript) -> boolean
	return function(ModuleScript: ModuleScript): boolean
		return ModuleScript.Name:match(MatchName) ~= nil
	end
end

local function RequireModules(Directory: DirectoryType, Predicate: PredicateFn?)
	local Modules: { [number]: any } = {}
	local RequirePromises: {Promise.Promise} = {}
	for _, ModuleScript in Directory do
		if ModuleScript:IsA("ModuleScript") then
			if Predicate and not Predicate(ModuleScript) then
				continue
			end
			local PromiseRequire = Promise.try(function()
				local Result = require(ModuleScript) :: any
				if typeof(Result) == "table" then
					table.insert(Modules, Result)
				end
			end):catch(function(err)
				warn(("Modula failed to require %s -> %s"):format(ModuleScript.Name, tostring(err)))
			end)
			table.insert(RequirePromises, PromiseRequire)
		end
	end
	return Promise.all(RequirePromises):andThen(function()
		return Modules
	end)
end

local function RunMethod(RequiredModules, MethodName)
	local MethodPromises: {Promise.Promise} = {}
	for _, Module in ipairs(RequiredModules) do
		local Method = Module[MethodName]
		if typeof(Method) == "function" then
			table.insert(MethodPromises, Promise.try(function()
				local Result = Method(Module)
				if Promise.is and Promise.is(Result) then
					return Result
				end
				return Promise.resolve(Result)
			end))
		end
	end
	return Promise.all(MethodPromises)
end

local function SortByPriority(Modules) -- // Sort modules by Priority in descending order. (e.g higher number = higher Priority)
	table.sort(Modules, function(A, B)
		local PA = math.abs(tonumber(rawget(A, "Priority")) or 1)
		local PB = math.abs(tonumber(rawget(B, "Priority")) or 1)
		return PA > PB
	end)
end

function Modula.Load(self: ModulaType, Directory: DirectoryType, Config: ConfigType?)
	assert(Directory, "Missing Directory instance")

	if typeof(Directory) == "Instance" then
		assert(Config and Config.Depth, "Missing Depth for Instance Directory")
		if Config.Depth == "Children" then
			Directory = Directory:GetChildren()
		elseif Config.Depth == "Descendants" then
			Directory = Directory:GetDescendants()
		else
			error("Invalid Depth value: must be 'Children' or 'Descendants'")
		end
	elseif typeof(Directory) ~= "table" then
		error("Invalid Directory type. Expected Instance or table")
	end

	local MatchName = Config and Config.MatchName or nil
	local InitName = (Config and Config.InitName) or "Init"
	local StartName = (Config and Config.StartName) or "OnStart"
	local Predicate = MatchName and MatchesName(MatchName)

	return RequireModules(Directory, Predicate)
		-- // Sort and Run Init Methods
		:andThen(function(RequiredModules)
			SortByPriority(RequiredModules)
			return RunMethod(RequiredModules, InitName)
			:andThenReturn(RequiredModules)
		end)
		-- // Run Start Methods after Init Methods Resolve.
		:andThen(function(RequiredModules)
			return RunMethod(RequiredModules, StartName)
			:andThenReturn(RequiredModules)
		end)
end

return Modula :: ModulaType


--!strict
--!optimize 2

local InstanceQuery = {}

type QueryPath = {string}

function InstanceQuery:GetAsync(parent: Instance, query_path: QueryPath): any
	local query_result = parent
	for _, path in query_path do
		query_result = query_result:WaitForChild(path)
	end
	return query_result
end

function InstanceQuery:Get(parent: Instance, query_path: QueryPath): any?
	local query_result = parent
	for _, path in query_path do
		query_result = (query_result :: any):FindFirstChild(path)
		if not query_result then return nil end
	end
	return query_result
end

return InstanceQuery


--!strict

local RagdollBuilder = {}

local Util = require(script.Parent.Util)
local Types = require(script.Parent.Types)
local RigConfigs = require(script.Parent.RigConfigs)
local InstanceQuery = require(script.Parent.InstanceQuery)

type RigConfig = Types.RigConfig
type CharacterRagdollInfo = Types.CharacterRagdollInfo

local function CreateInfo(character: Model, rig_type: string): CharacterRagdollInfo
	return {
		Ragdolled = false,
		Character = character,
		RigType = rig_type,
		Limbs = {},
		Joints = {},
		Attachments = {},
		Sockets = {},
		NoCollisionConstraints = {},
		Connections = {},
	}
end

function RagdollBuilder.Validate(character: Model, rig_type: string): boolean
	if rig_type == "Generic" then return true end
	local config = RigConfigs[rig_type]
	if not config then
		warn(`[RagdollService]: Unknown rig type: {rig_type}`)
		return false
	end
	for name, path in config.Limbs do
		local limb = InstanceQuery:Get(character, path) :: BasePart?
		if not limb then
			Util.StudioWarn(`Missing the limb with path: {table.concat(path, ".")}`)
			return false
		end
		if not limb:IsA("BasePart") then
			Util.StudioWarn(`Limb must be a BasePart: {table.concat(path, ".")}`)
			return false
		end
	end
	for name, path in config.Joints do
		local motor6d = InstanceQuery:Get(character, path) :: Motor6D?
		if not motor6d then
			Util.StudioWarn(`Missing the joint with path: {table.concat(path, ".")}`)
			return false
		end
		if not motor6d:IsA("Motor6D") then
			Util.StudioWarn(`Joint must be a Motor6D: {table.concat(path, ".")}`)
			return false
		end
		if not (motor6d.Part0 and motor6d.Part1) then
			Util.StudioWarn(`Joint is missing Part0 or Part1: {table.concat(path, ".")}`)
			return false
		end
		local socket_limits = config.Sockets[name]
		if not socket_limits then
			Util.StudioWarn(`No socket for following joint: {table.concat(path, ".")}`)
			return false
		end
	end

	for _, no_collision_config in config.NoCollisionConstraints do
		local limb0 = config.Limbs[no_collision_config[1]]
		local limb1 = config.Limbs[no_collision_config[2]]
		if not (limb0 and limb1) then
			Util.StudioWarn(`Unable to find limb0 or limb1 for NoCollisionConstraint.`)
			return false
		end
	end

	return true
end

local function BuildGeneric(info: CharacterRagdollInfo)
	local character = info.Character
	local root_part = character.PrimaryPart

	local humanoid = character:FindFirstChildWhichIsA("Humanoid", true)
	if humanoid then
		root_part = humanoid.RootPart
		humanoid.BreakJointsOnDeath = false
		humanoid.RequiresNeck = false
		info.Humanoid = humanoid
	end

	info.Animator = character:FindFirstChildWhichIsA("Animator", true)
	info.RootPart = root_part

	local limb_cache = {} :: {[BasePart]: boolean}
	local motor6ds: {Motor6D} = character:QueryDescendants("Motor6D") :: any
	for _, motor6d in motor6ds do
		if motor6d.Name:lower():find("root", nil, true) then continue end
		local part0 = motor6d.Part0
		local part1 = motor6d.Part1
		if not part0 or not part1 then continue end

		Util.RegisterRootNoCollision(limb_cache, part0, info)
		Util.RegisterRootNoCollision(limb_cache, part1, info)

		local attachment0 = Util.NewAttachment(motor6d.C0, part0)
		local attachment1 = Util.NewAttachment(motor6d.C1, part1)

		table.insert(info.NoCollisionConstraints, Util.NewNoCollision(part0, part1))
		table.insert(info.Attachments, attachment0)
		table.insert(info.Attachments, attachment1)

		info.Joints[motor6d.Name] = motor6d
		info.Sockets[motor6d.Name] = Util.NewSocket(attachment0, attachment1)
	end
end

local function BuildFromConfig(info: CharacterRagdollInfo)
	local character = info.Character
	local config = RigConfigs[info.RigType] :: RigConfig

	info.RootPart = Util.QueryInstance(character, config.RootPart) :: BasePart?
	info.Animator = Util.QueryInstance(character, config.Animator) :: Animator?

	local humanoid = Util.QueryInstance(character, config.Humanoid) :: Humanoid?
	if humanoid then
		humanoid.BreakJointsOnDeath = false
		humanoid.RequiresNeck = false
		info.Humanoid = humanoid
	end

	for name, path in config.Limbs do
		info.Limbs[name] = InstanceQuery:Get(character, path) :: BasePart
	end

	for name, path in config.Joints do
		local motor6d = InstanceQuery:Get(character, path) :: Motor6D

		local attachment0 = Util.NewAttachment(motor6d.C0, motor6d.Part0 :: BasePart)
		local attachment1 = Util.NewAttachment(motor6d.C1, motor6d.Part1 :: BasePart)

		table.insert(info.Attachments, attachment0)
		table.insert(info.Attachments, attachment1)

		info.Sockets[name] = Util.NewSocket(attachment0, attachment1, config.Sockets[name])
		info.Joints[name] = motor6d
	end

	for _, no_collision_config in config.NoCollisionConstraints do
		local limb0 = info.Limbs[no_collision_config[1]]
		local limb1 = info.Limbs[no_collision_config[2]]
		table.insert(info.NoCollisionConstraints, Util.NewNoCollision(limb0, limb1))
	end
end

function RagdollBuilder.Build(character: Model, rig_type: string): CharacterRagdollInfo
	local info = CreateInfo(character, rig_type)

	if rig_type == "Generic" then
		BuildGeneric(info)
	else
		BuildFromConfig(info)
	end

	return info
end

return RagdollBuilder