local Status = newproxy(false)

type TaskStatus = number
export type Interval = { [typeof(Status)]: TaskStatus }

local SCHEDULED = 1
local CANCELLED = 3

return function(delayImpl)
	local function setInterval(callback, intervalTime: number, ...): Interval
		local args = { ... }
		local task = {
			[Status] = SCHEDULED,
		}

		-- delayTime is an optional parameter
		if intervalTime == nil then
			intervalTime = 0
		end

		-- To mimic the JS interface, we're expecting delayTime to be in ms
		local intervalTimeMs = intervalTime / 1000
		local delay_
		delay_ = function()
			delayImpl(intervalTimeMs, function()
				if task[Status] == SCHEDULED then
					callback(unpack(args))
					delay_()
				end
			end)
		end

		delay_()

		return task
	end

	local function clearInterval(task: Interval)
		if task == nil then
			return
		end

		if task[Status] == SCHEDULED then
			task[Status] = CANCELLED
		end
	end

	return {
		setInterval = setInterval,
		clearInterval = clearInterval,
	}
end


local Status = newproxy(false)

type TaskStatus = number
export type Timeout = { [typeof(Status)]: TaskStatus }

local SCHEDULED = 1
local DONE = 2
local CANCELLED = 3

return function(delayImpl)
	local function setTimeout(callback, delayTime: number?, ...): Timeout
		local args = { ... }
		local task = {
			[Status] = SCHEDULED,
		}

		-- delayTime is an optional parameter
		if delayTime == nil then
			delayTime = 0
		end

		-- To mimic the JS interface, we're expecting delayTime to be in ms
		local delayTimeMs = delayTime :: number / 1000
		delayImpl(delayTimeMs, function()
			if task[Status] == SCHEDULED then
				callback(unpack(args))
				task[Status] = DONE
			end
		end)

		return task
	end

	local function clearTimeout(task: Timeout)
		if task == nil then
			return
		end
		if task[Status] == SCHEDULED then
			task[Status] = CANCELLED
		end
	end

	return {
		setTimeout = setTimeout,
		clearTimeout = clearTimeout,
	}
end


local config = {
	spring = {
		default = { tension = 170, friction = 26 },
		gentle = { tension = 120, friction = 14 },
		wobbly = { tension = 180, friction = 12 },
		stiff = { tension = 210, friction = 20 },
		slow = { tension = 280, friction = 60 },
		molasses = { tension = 280, friction = 120 },
	},

	linear = {
		default = { speed = 1 },
	},

	tween = {
		default = {
			time = 1,
			style = Enum.EasingStyle.Quad,
			direction = Enum.EasingDirection.Out,
			repeatCount = 0,
			reverses = false,
			delayTime = 0,
		},
	},
}

return config


local RunService = game:GetService("RunService")

local types = require(script.Parent.types)
local immediateSolver = require(script.Parent.solvers.immediate)
local linearSolver = require(script.Parent.solvers.linear)
local springSolver = require(script.Parent.solvers.spring)
local tweenSolver = require(script.Parent.solvers.tween)
local intermediate = require(script.Parent.utils.intermediate)
local assign = require(script.Parent.utils.assign)
local merge = require(script.Parent.utils.merge)

local defaults = {
	heartbeat = RunService.Heartbeat,
	start = false,
}

local function createMotion<T>(initialValue: T, options: types.MotionOptions?): types.Motion<T>
	local config = merge(defaults, options or {})
	local valueType = typeof(initialValue)

	local motionState: { [any]: types.MotionState } = {}
	local motionSolvers: { [any]: types.MotionSolver } = {}

	local onStepListeners: { (value: T, deltaTime: number) -> () } = {}
	local onCompleteListeners: { (value: T) -> () } = {}
	local nextListenerId = 1

	local connection: RBXScriptConnection?
	local wasComplete = false
	local lastCompleteValue

	for key, value in intermediate.to(initialValue) do
		motionState[key] = {
			value = value,
			complete = true,
		}
	end

	local function stop()
		if connection then
			connection:Disconnect()
			connection = nil
		end
	end

	local function start(self)
		if connection then
			return stop
		end

		connection = config.heartbeat:Connect(function(deltaTime)
			self:step(deltaTime)
		end)

		return stop
	end

	local function get()
		local intermediates = {}

		for key, state in motionState do
			intermediates[key] = state.value
		end

		return intermediate.from(intermediates, valueType)
	end

	local function getVelocity()
		local intermediates = {}

		for key, state in motionState do
			intermediates[key] = state.velocity or 0
		end

		return intermediate.from(intermediates, valueType)
	end

	local function set(_self, value)
		local intermediates = intermediate.to(value)

		for key, state in motionState do
			local newValue = intermediates[key]

			if newValue then
				state.value = newValue
				state.complete = false
			end
		end
	end

	local function patch(_self, source)
		for key, state in motionState do
			local stateSource = source[key]

			if stateSource then
				state.complete = false
				assign(state, stateSource)
			end
		end
	end

	local function impulse(_self, impulses)
		if type(impulses) == "number" then
			for _, state in motionState do
				if not state.velocity then
					continue
				end

				state.complete = false
				state.velocity += impulses
			end

			return
		end

		local intermediateImpulses = intermediate.to(impulses)

		for key, amount in intermediateImpulses do
			local state = motionState[key]

			if not state or not state.velocity then
				continue
			end

			state.complete = false
			state.velocity += amount
		end
	end

	local function setVelocity(_self, velocity)
		local intermediates = intermediate.to(velocity)

		for key, amount in intermediates do
			local state = motionState[key]

			if not state or not state.velocity then
				continue
			end

			state.complete = false
			state.velocity = amount
		end
	end

	local function to(_self, solvers)
		if type(solvers) == "function" then
			for key, state in motionState do
				if state.destructor then
					state.destructor()
					state.destructor = nil
				end

				state.complete = false

				if solvers(key, state, 0) ~= false then
					-- only sets the solver if this key is part of the new goal
					motionSolvers[key] = solvers
				end
			end

			return
		end

		for key, solver in solvers do
			local state = motionState[key]

			if not state then
				continue
			end

			if state.destructor then
				state.destructor()
				state.destructor = nil
			end

			state.complete = false

			if solver(key, state, 0) ~= false then
				-- only sets the solver if this key is part of the new goal
				motionSolvers[key] = solver
			end
		end
	end

	local function immediate(self, value)
		self:to(immediateSolver(value :: any))
	end

	local function linear(self, value, params)
		self:to(linearSolver(value :: any, params))
	end

	local function spring(self, value, params)
		self:to(springSolver(value :: any, params))
	end

	local function tween(self, value, params)
		self:to(tweenSolver(value :: any, params))
	end

	local function step(self, deltaTime)
		for key, handler in motionSolvers do
			local state = motionState[key]

			if state and not state.complete then
				handler(key, state, deltaTime)
			end
		end

		local value = self:get()
		local complete = self:isComplete()

		if not complete or not wasComplete or lastCompleteValue ~= value then
			for _, listener in onStepListeners do
				task.spawn(listener, value, deltaTime)
			end
		end

		if complete and (not wasComplete or lastCompleteValue ~= value) then
			for _, listener in onCompleteListeners do
				task.spawn(listener, value)
			end
		end

		wasComplete = complete
		lastCompleteValue = value

		return value
	end

	local function isComplete()
		for _, state in motionState do
			if not state.complete then
				return false
			end
		end

		return true
	end

	local function onComplete(_self, callback)
		local listenerId = nextListenerId
		nextListenerId += 1
		onCompleteListeners[listenerId] = callback

		return function()
			onCompleteListeners[listenerId] = nil
		end
	end

	local function onStep(_self, callback)
		local listenerId = nextListenerId
		nextListenerId += 1
		onStepListeners[listenerId] = callback

		return function()
			onStepListeners[listenerId] = nil
		end
	end

	local function destroy()
		wasComplete = false
		nextListenerId = 1

		stop()

		table.clear(onStepListeners)
		table.clear(onCompleteListeners)
		table.clear(motionSolvers)

		for _, state in motionState do
			if state.destructor then
				state.destructor()
				state.destructor = nil
			end
		end
	end

	local motion: types.Motion<T> = {
		state = motionState,
		start = start,
		stop = stop,
		get = get,
		set = set,
		getVelocity = getVelocity,
		setVelocity = setVelocity,
		impulse = impulse,
		patch = patch,
		to = to,
		immediate = immediate,
		linear = linear,
		spring = spring,
		tween = tween,
		step = step,
		isComplete = isComplete,
		onComplete = onComplete,
		onStep = onStep,
		destroy = destroy,
	}

	if config.start then
		motion:start()
	end

	return motion
end

return createMotion


local types = require(script.types)

export type Motion<T = number> = types.Motion<T>

export type MotionState = types.MotionState

export type MotionSolver = types.MotionSolver

export type MotionGoal = types.MotionGoal

export type SpringOptions = types.SpringOptions
export type LinearOptions = types.LinearOptions
export type TweenOptions = types.TweenOptions

return {
	createMotion = require(script.createMotion),
	config = require(script.config),
	immediate = require(script.solvers.immediate),
	linear = require(script.solvers.linear),
	spring = require(script.solvers.spring),
	tween = require(script.solvers.tween),
}


local types = require(script.Parent.Parent.types)
local intermediate = require(script.Parent.Parent.utils.intermediate)

local function immediate(motionGoal: types.MotionGoal): types.MotionSolver
	local goals = intermediate.to(motionGoal)

	return function(key, state)
		local goal = intermediate.index(goals, key)

		if not goal then
			return false
		end

		state.value = goal
		state.complete = true
	end
end

return immediate


local types = require(script.Parent.Parent.types)
local config = require(script.Parent.Parent.config)
local intermediate = require(script.Parent.Parent.utils.intermediate)

local function configure(options: types.LinearOptions)
	local speed = if type(options) == "table" then options.speed else options

	return {
		speed = speed or config.linear.default.speed,
	}
end

local function linear(motionGoal: types.MotionGoal, options: types.LinearOptions): types.MotionSolver
	local props = configure(options)
	local goals = intermediate.to(motionGoal)

	return function(key, state, deltaTime)
		local goal = intermediate.index(goals, key)

		if not goal then
			return false
		end

		local velocity = props.speed * deltaTime * math.sign(goal - state.value)

		if math.abs(velocity) >= math.abs(goal - state.value) then
			state.complete = true
			state.value = goal
			state.velocity = 0
		else
			state.value += velocity
			state.velocity = velocity
		end
	end
end

return linear


local types = require(script.Parent.Parent.types)
local config = require(script.Parent.Parent.config)
local intermediate = require(script.Parent.Parent.utils.intermediate)

local STEP = 1 -- milliseconds
local MAX_PASS = 100

local function configure(options: types.SpringOptions)
	local mass = options.mass or 1
	local tension = options.tension or config.spring.default.tension
	local friction = options.friction or config.spring.default.friction

	if options.frequency or options.damping then
		local frequency = options.frequency or 0.5
		local damping = options.damping or 1
		tension = (2 * math.pi / frequency) ^ 2 * mass
		friction = (4 * math.pi * damping * mass) / frequency
	end

	return {
		mass = mass,
		tension = tension,
		friction = friction,
		position = options.position,
		velocity = options.velocity,
		impulse = options.impulse,
		restingVelocity = options.restingVelocity or 0.001,
		restingPosition = options.restingPosition or 0.0001,
	}
end

local function spring(motionGoal: types.MotionGoal, options: types.SpringOptions?): types.MotionSolver
	local props = configure(options or {})
	local goals = intermediate.to(motionGoal)
	local mounting = true

	return function(key, state, deltaTime)
		local goal = intermediate.index(goals, key)

		if not goal then
			return false
		end

		if mounting then
			mounting = false
			state.value = (props.position or state.value or 0)
			state.velocity = (props.velocity or state.velocity or 0) + (props.impulse or 0)
		end

		local position = state.value
		local velocity = state.velocity or 0
		local passes = math.min(math.ceil((deltaTime * 1000) / STEP), MAX_PASS)

		for _ = 1, passes do
			local springForce = -props.tension * 0.000001 * (position - goal)
			local dampingForce = -props.friction * 0.001 * velocity
			local acceleration = (springForce + dampingForce) / props.mass

			velocity += acceleration * STEP
			position += velocity * STEP
		end

		if math.abs(velocity) < props.restingVelocity and math.abs(position - goal) < props.restingPosition then
			state.complete = true
			state.value = goal
			state.velocity = 0
		else
			state.value = position
			state.velocity = velocity
		end
	end
end

return spring


local TweenService = game:GetService("TweenService")

local types = require(script.Parent.Parent.types)
local config = require(script.Parent.Parent.config)
local intermediate = require(script.Parent.Parent.utils.intermediate)
local merge = require(script.Parent.Parent.utils.merge)

type TweenEntry = {
	value: NumberValue,
	tween: Tween,
	complete: boolean,
}

local function createTween(from: number, to: number, options: types.TweenOptions): TweenEntry
	local tweenInfo = TweenInfo.new(
		options.time,
		options.style,
		options.direction,
		options.repeatCount,
		options.reverses,
		options.delayTime
	)

	local value = Instance.new("NumberValue")
	local tween = TweenService:Create(value, tweenInfo, { Value = to })

	value.Value = from

	return {
		value = value,
		tween = tween,
		complete = false,
	}
end

local function tween(motionGoal: types.MotionGoal, options: types.TweenOptions?): types.MotionSolver
	local props = merge(config.tween.default, options or {})
	local goals = intermediate.to(motionGoal)
	local entries: { [unknown]: TweenEntry? } = {}

	return function(key, state)
		local goal = intermediate.index(goals, key)

		if not goal then
			return false
		end

		if not state.destructor then
			local entry = createTween(state.value, goal, props)

			entries[key] = entry

			entry.tween.Completed:Connect(function()
				entry.complete = true
				entry.value:Destroy()
				entry.tween:Destroy()
			end)

			entry.tween:Play()

			function state.destructor()
				entry.tween:Destroy()
				entry.value:Destroy()
				entries[key] = nil
			end
		end

		local entry = entries[key]

		if not entry then
			state.complete = true
		elseif entry.complete then
			state.complete = true
			state.value = goal
		else
			state.value = entry.value.Value
		end
	end
end

return tween


type Cleanup = () -> ()

export type Heartbeat = {
	Connect: (self: Heartbeat, callback: (deltaTime: number) -> ()) -> Disconnectable,
}

export type Disconnectable = {
	Disconnect: (self: Disconnectable) -> (),
}

export type Partial<T> = { [any]: any } & T

export type SpringOptions = {
	damping: number?,
	frequency: number?,
	mass: number?,
	tension: number?,
	friction: number?,
	position: number?,
	velocity: number?,
	impulse: number?,
	restingVelocity: number?,
	restingPosition: number?,
}

export type LinearOptions = {
	speed: number?,
} | number?

export type TweenOptions = {
	time: number?,
	style: Enum.EasingStyle?,
	direction: Enum.EasingDirection?,
	repeatCount: number?,
	reverses: boolean?,
	delayTime: number?,
}

export type Motion<T = number> = {
	state: { [any]: MotionState },
	start: (self: Motion<T>) -> Cleanup,
	stop: (self: Motion<T>) -> (),
	get: (self: Motion<T>) -> T,
	set: (self: Motion<T>, value: T | { [any]: number }) -> (),
	getVelocity: (self: Motion<T>) -> T,
	setVelocity: (self: Motion<T>, value: T | { [any]: number }) -> (),
	impulse: (self: Motion<T>, impulse: T | { [any]: number }) -> (),
	patch: (self: Motion<T>, patch: { [any]: Partial<MotionState> }) -> (),
	to: (self: Motion<T>, goal: MotionSolver | { [any]: MotionSolver }) -> (),
	immediate: (self: Motion<T>, goal: T) -> (),
	spring: (self: Motion<T>, goal: T, options: SpringOptions?) -> (),
	linear: (self: Motion<T>, goal: T, options: LinearOptions?) -> (),
	tween: (self: Motion<T>, goal: T, options: TweenOptions?) -> (),
	step: (self: Motion<T>, deltaTime: number) -> T,
	isComplete: (self: Motion<T>) -> boolean,
	onComplete: (self: Motion<T>, callback: (value: T) -> ()) -> Cleanup,
	onStep: (self: Motion<T>, callback: (value: T, deltaTime: number) -> ()) -> Cleanup,
	destroy: (self: Motion<T>) -> (),
}

export type MotionOptions = {
	heartbeat: Heartbeat?,
	start: boolean?,
}

export type MotionState = {
	value: number,
	complete: boolean,
	velocity: number?,
	destructor: (() -> ())?,
}

export type MotionSolver = (key: unknown, state: MotionState, deltaTime: number) -> ()

export type MotionGoal = { [any]: number } | number | UDim2 | UDim | Vector2 | Vector3 | Color3 | CFrame

return nil


local function assign<T>(object: T, ...: { [any]: any }): T
	assert(type(object) == "table", `Expected a table for first argument, got ${type(object)}`)

	for index = 1, select("#", ...) do
		local source = select(index, ...)

		for key, value in source do
			object[key] = value
		end
	end

	return object
end

return assign


type Intermediate = { [any]: number }

local intermediates = {
	number = {
		to = function(value: number): Intermediate
			return { value }
		end,
		from = function(value: Intermediate): number
			return value[1]
		end,
	},

	table = {
		to = function(value: Intermediate): Intermediate
			return value
		end,
		from = function(value: Intermediate): Intermediate
			return value
		end,
	},

	UDim2 = {
		to = function(value: UDim2): Intermediate
			return { value.X.Scale, value.X.Offset, value.Y.Scale, value.Y.Offset }
		end,
		from = function(value: Intermediate): UDim2
			return UDim2.new(value[1], math.round(value[2]), value[3], math.round(value[4]))
		end,
	},

	UDim = {
		to = function(value: UDim): Intermediate
			return { value.Scale, value.Offset }
		end,
		from = function(value: Intermediate)
			return UDim.new(value[1], math.round(value[2]))
		end,
	},

	Vector2 = {
		to = function(value: Vector2): Intermediate
			return { value.X, value.Y }
		end,
		from = function(value: Intermediate): Vector2
			return Vector2.new(table.unpack(value, 1, 2))
		end,
	},

	Vector3 = {
		to = function(value: Vector3): Intermediate
			return { value.X, value.Y, value.Z }
		end,
		from = function(value: Intermediate): Vector3
			return Vector3.new(table.unpack(value, 1, 3))
		end,
	},

	Color3 = {
		to = function(value: Color3): Intermediate
			return { value.R, value.G, value.B }
		end,
		from = function(value: Intermediate): Color3
			return Color3.new(math.clamp(value[1], 0, 1), math.clamp(value[2], 0, 1), math.clamp(value[3], 0, 1))
		end,
	},

	CFrame = {
		to = function(value: CFrame): Intermediate
			return { value:GetComponents() }
		end,
		from = function(value: Intermediate): CFrame
			return CFrame.new(table.unpack(value))
		end,
	},
}

local function to(value: unknown): Intermediate
	local type = typeof(value)

	if intermediates[type] then
		return intermediates[type].to(value)
	else
		error(`Ripple received an unsupported value '{value}' of type '{type}'`)
	end
end

local function from<T>(value: Intermediate, type: string): T
	if intermediates[type] then
		return intermediates[type].from(value)
	else
		error(`Ripple received an unsupported value '{value}' of type '{type}'`)
	end
end

local function index(value: Intermediate, key: unknown): number?
	return value[key] or value[1]
end

return {
	to = to,
	from = from,
	index = index,
}


local function merge<T, U>(a: T & { [any]: any }, b: U & { [any]: any }): T & U
	local copy = table.clone(a)

	for key, value in pairs(b) do
		copy[key] = value
	end

	return copy :: never
end

return merge


local HttpService = game:GetService("HttpService")

local types = require(script.Parent.Parent.types)
local createMotion = require(script.Parent.Parent.createMotion)

local STEP = 0.1
local MAX_LENGTH = 20

local function createSnapshot(solver: types.MotionSolver, length: number?)
	local data: { number } = {}
	local motion = createMotion(0)

	motion:to(solver)

	for _ = 1, length or MAX_LENGTH do
		local value = motion:step(STEP)

		value = if value < 1
			then math.floor(value * 100) / 100
			elseif value > 1 then math.ceil(value * 100) / 100
			else value

		table.insert(data, value)

		if value == 1 then
			break
		end
	end

	return HttpService:JSONEncode(data), motion
end

local function testSnapshot(solver: types.MotionSolver, expected: string)
	local length = select(2, string.gsub(expected, ",", ",")) + 1
	local snapshot, motion = createSnapshot(solver, length)

	if snapshot ~= expected then
		error(`Snapshot does not match expected value.\n\nExpected:\n{expected}\n\nActual:\n{snapshot}`, 2)
	end

	return motion
end

return {
	createSnapshot = createSnapshot,
	testSnapshot = testSnapshot,
}


type Spy = {
	calls: number,
	arguments: { { unknown } },
	handle: (...any) -> (),
}

local function spy(): Spy
	local self: Spy

	local function handle(...)
		self.calls += 1
		self.arguments[self.calls] = { ... }
	end

	self = {
		calls = 0,
		arguments = {},
		handle = handle,
	}

	return self
end

return spy


local Spawn = require(script.Parent.Spawn)

export type Future<T...> = {
	ValueList: { any }?,
	AfterList: { (T...) -> () },
	YieldList: { thread },

	IsComplete: (self: Future<T...>) -> boolean,
	IsPending: (self: Future<T...>) -> boolean,

	Expect: (self: Future<T...>, Message: string) -> T...,
	Unwrap: (self: Future<T...>) -> T...,
	UnwrapOr: (self: Future<T...>, T...) -> T...,
	UnwrapOrElse: (self: Future<T...>, Else: () -> T...) -> T...,

	After: (self: Future<T...>, Callback: (T...) -> ()) -> (),
	Await: (self: Future<T...>) -> T...,
}

local function IsComplete<T...>(self: Future<T...>): boolean
	return self.ValueList ~= nil
end

local function IsPending<T...>(self: Future<T...>): boolean
	return self.ValueList == nil
end

local function Expect<T...>(self: Future<T...>, Message: string): T...
	assert(self.ValueList, Message)

	return table.unpack(self.ValueList)
end

local function Unwrap<T...>(self: Future<T...>): T...
	return self:Expect("Attempt to unwrap pending future!")
end

local function UnwrapOr<T...>(self: Future<T...>, ...): T...
	if self.ValueList then
		return table.unpack(self.ValueList)
	else
		return ...
	end
end

local function UnwrapOrElse<T...>(self: Future<T...>, Else: () -> T...): T...
	if self.ValueList then
		return table.unpack(self.ValueList)
	else
		return Else()
	end
end

local function After<T...>(self: Future<T...>, Callback: (T...) -> ()): T...
	if self.ValueList then
		Spawn(Callback, table.unpack(self.ValueList))
	else
		table.insert(self.AfterList, Callback)
	end
end

local function Await<T...>(self: Future<T...>): T...
	if self.ValueList then
		return table.unpack(self.ValueList)
	else
		table.insert(self.YieldList, coroutine.running())

		return coroutine.yield()
	end
end

local function Future<T..., A...>(Callback: (A...) -> T..., ...: A...): Future<T...>
	local self: Future<T...> = {
		ValueList = nil,
		AfterList = {},
		YieldList = {},

		IsComplete = IsComplete,
		IsPending = IsPending,

		Expect = Expect,
		Unwrap = Unwrap,
		UnwrapOr = UnwrapOr,
		UnwrapOrElse = UnwrapOrElse,

		After = After,
		Await = Await,
	} :: any

	Spawn(function(self: Future<T...>, Callback: (A...) -> T..., ...: A...)
		local ValueList = { Callback(...) }
		self.ValueList = ValueList

		for _, Thread in self.YieldList do
			task.spawn(Thread, table.unpack(ValueList))
		end

		for _, Callback in self.AfterList do
			Spawn(Callback, table.unpack(ValueList))
		end
	end, self, Callback, ...)

	return self
end

local function Try<T..., A...>(Callback: (A...) -> T..., ...: A...): Future<(boolean, T...)>
	return Future(pcall, Callback, ...)
end

return {
	new = Future,
	Try = Try,
}


local REQUIRED_MODULE = require(script.Parent.Parent["red-blox_future@1.0.1"]["future"])
export type Future<T...> = REQUIRED_MODULE.Future<T...>
return REQUIRED_MODULE


local REQUIRED_MODULE = require(script.Parent.Parent["red-blox_guard@1.0.1"]["guard"])
export type Check<T> = REQUIRED_MODULE.Check<T>
return REQUIRED_MODULE


local REQUIRED_MODULE = require(script.Parent.Parent["red-blox_signal@2.0.2"]["signal"])
export type Signal<T...> = REQUIRED_MODULE.Signal<T...>
return REQUIRED_MODULE


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

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

export type Client<T...> = {
	Id: string,

	Unreliable: boolean,

	Fire: (self: Client<T...>, T...) -> (),
	On: (self: Client<T...>, Callback: (T...) -> ()) -> (),
}

local function Fire<T...>(self: Client<T...>, ...: T...)
	if self.Unreliable then
		Net.Client.SendUnreliableEvent(self.Id, table.pack(...))
	else
		Net.Client.SendReliableEvent(self.Id, table.pack(...))
	end
end

local function On<T...>(self: Client<T...>, Callback: (T...) -> ())
	Net.Client.SetListener(self.Id, function(Args)
		Spawn(Callback, table.unpack(Args))
	end)
end

local function Client<T...>(Id: string, Unreliable: boolean): Client<T...>
	return {
		Id = Id,

		Unreliable = Unreliable,

		Fire = Fire,
		On = On,
	} :: any
end

return Client


local Players = game:GetService("Players")

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

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

export type Server<T...> = {
	Id: string,
	Validate: (...unknown) -> T...,

	Unreliable: boolean,

	Fire: (self: Server<T...>, Player: Player, T...) -> (),
	FireAll: (self: Server<T...>, T...) -> (),
	FireAllExcept: (self: Server<T...>, Except: Player, T...) -> (),
	FireList: (self: Server<T...>, List: { Player }, T...) -> (),
	FireWithFilter: (self: Server<T...>, Filter: (Player) -> boolean, T...) -> (),

	On: (self: Server<T...>, Callback: (Player, T...) -> ()) -> (),
}

local function Fire<T...>(self: Server<T...>, Player: Player, ...: T...)
	if self.Unreliable then
		Net.Server.SendUnreliableEvent(Player, self.Id, table.pack(...))
	else
		Net.Server.SendReliableEvent(Player, self.Id, table.pack(...))
	end
end

local function FireAll<T...>(self: Server<T...>, ...: T...)
	local Args = table.pack(...)

	for _, Player in Players:GetPlayers() do
		if self.Unreliable then
			Net.Server.SendUnreliableEvent(Player, self.Id, Args)
		else
			Net.Server.SendReliableEvent(Player, self.Id, Args)
		end
	end
end

local function FireAllExcept<T...>(self: Server<T...>, Except: Player, ...: T...)
	local Args = table.pack(...)

	for _, Player in Players:GetPlayers() do
		if Player ~= Except then
			if self.Unreliable then
				Net.Server.SendUnreliableEvent(Player, self.Id, Args)
			else
				Net.Server.SendReliableEvent(Player, self.Id, Args)
			end
		end
	end
end

local function FireList<T...>(self: Server<T...>, List: { Player }, ...: T...)
	local Args = table.pack(...)

	for _, Player in List do
		if self.Unreliable then
			Net.Server.SendUnreliableEvent(Player, self.Id, Args)
		else
			Net.Server.SendReliableEvent(Player, self.Id, Args)
		end
	end
end

local function FireWithFilter<T...>(self: Server<T...>, Filter: (Player) -> boolean, ...: T...)
	local Args = table.pack(...)

	for _, Player in Players:GetPlayers() do
		if Filter(Player) then
			if self.Unreliable then
				Net.Server.SendUnreliableEvent(Player, self.Id, Args)
			else
				Net.Server.SendReliableEvent(Player, self.Id, Args)
			end
		end
	end
end

local function On<T...>(self: Server<T...>, Callback: (Player, T...) -> ())
	Net.Server.SetListener(self.Id, function(Player, Args)
		Spawn(function(self: typeof(self), Player: Player, ...: any)
			if pcall(self.Validate, ...) then
				Callback(Player, ...)
			end
		end, self, Player, table.unpack(Args))
	end)
end

local function Server<T...>(Id: string, Validate: (...unknown) -> T..., Unreliable: boolean): Server<T...>
	return {
		Id = Id,
		Validate = Validate,

		Unreliable = Unreliable,

		Fire = Fire,
		FireAll = FireAll,
		FireAllExcept = FireAllExcept,
		FireList = FireList,
		FireWithFilter = FireWithFilter,

		On = On,
	} :: any
end

return Server


local RunService = game:GetService("RunService")

local Identifier = require(script.Parent.Identifier)

local ServerEvent = require(script.Server)
local ClientEvent = require(script.Client)

export type Event<T...> = {
	Id: string,
	Validate: (...unknown) -> T...,

	Unreliable: boolean,

	ServerEvent: ServerEvent.Server<T...>?,
	ClientEvent: ClientEvent.Client<T...>?,

	Server: (self: Event<T...>) -> ServerEvent.Server<T...>,
	Client: (self: Event<T...>) -> ClientEvent.Client<T...>,
}

local function Server<T...>(self: Event<T...>): ServerEvent.Server<T...>
	assert(RunService:IsServer(), "Server events can only be accessed from the server")

	if not self.ServerEvent then
		self.ServerEvent = ServerEvent(self.Id, self.Validate, self.Unreliable)
	end

	return self.ServerEvent :: any
end

local function Client<T...>(self: Event<T...>): ClientEvent.Client<T...>
	assert(RunService:IsClient(), "Client events can only be accessed from the client")

	if not self.ClientEvent then
		self.ClientEvent = ClientEvent(self.Id, self.Unreliable) :: any
	end

	return self.ClientEvent :: any
end

type EventOptions = {
	Name: string,
	Unreliable: boolean?,
}

local function Event<T...>(Config: string | EventOptions, Validate: (...unknown) -> T...): Event<T...>
	local Name, Unreliable

	if type(Config) == "string" then
		Name = Config
		Unreliable = false
	else
		Name = Config.Name
		Unreliable = Config.Unreliable or false
	end

	assert(not Identifier.Exists(Name), "Cannot use same name twice")

	return {
		Id = Identifier.Shared(Name):Await(),
		Validate = Validate,

		Unreliable = Unreliable,

		ServerEvent = nil,
		ClientEvent = nil,

		Server = Server,
		Client = Client,
	} :: any
end

return Event


local RunService = game:GetService("RunService")

local Future = require(script.Parent.Parent.Future)
local Spawn = require(script.Parent.Parent.Spawn)

local Net = require(script.Parent.Net)
local Identifier = require(script.Parent.Identifier)

export type Function<A..., R...> = {
	Id: string,
	Validate: (...unknown) -> A...,

	SetCallback: (self: Function<A..., R...>, Callback: (Player, A...) -> R...) -> (),
	Call: (
		self: Function<A..., R...>,
		A...
	) -> typeof(Future.new(function(): (boolean, R...)
		return {} :: any
	end)),
}

local function SetCallback<A..., R...>(self: Function<A..., R...>, Callback: (Player, A...) -> R...)
	assert(RunService:IsServer(), "Cannot set callback to function on client")

	Net.Server.SetListener(self.Id, function(Player, Args)
		local CallId = table.remove(Args, 1)

		if type(CallId) ~= "string" then
			return
		end

		Spawn(function(Player: Player, CallId: string, ...: any)
			if pcall(self.Validate, ...) then
				Net.Server.SendCallReturn(Player, CallId, table.pack(pcall(Callback, Player, ...)))
			end
		end, Player, CallId, unpack(Args))
	end)
end

local function Call<A..., R...>(self: Function<A..., R...>, ...: A...)
	return Future.new(function(...: any)
		local CallId = Identifier.Unique()

		return unpack(Net.Client.CallAsync(self.Id, table.pack(CallId, ...)))
	end, ...)
end

local function Function<A..., R...>(
	Name: string,
	ValidateArg: (...unknown) -> A...,
	ValidateRet: (...unknown) -> R...
): Function<A..., R...>
	assert(not Identifier.Exists(Name), "Cannot use same name twice")

	return {
		Id = Identifier.Shared(Name):Await(),
		Validate = ValidateArg,

		SetCallback = SetCallback,
		Call = Call,
	} :: any
end

return Function


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

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

local Remote = ReplicatedStorage:WaitForChild("ReliableRedEvent")
local Identifier = {}

local NextShared = 0
local NextUnique = 0

local function UInt(Integer: number)
	return string.pack(`I{math.ceil(math.log(Integer + 1, 2) / 8)}`, Integer)
end

function Identifier.Shared(Name: string)
	return Future.new(function(Name: string)
		if RunService:IsServer() then
			if Remote:GetAttribute(Name) then
				return Remote:GetAttribute(Name)
			else
				NextShared += 1
				local Id = UInt(NextShared)

				Remote:SetAttribute(Name, Id)

				return Id
			end
		else
			local delayWarn = task.delay(5, function()
				warn(`Yielded while initializing identifier: {Name}. It may not exist on the server!`)
			end)

			while not Remote:GetAttribute(Name) do
				Remote.AttributeChanged:Wait()
			end

			task.cancel(delayWarn)

			return Remote:GetAttribute(Name)
		end
	end, Name)
end

function Identifier.Exists(Name: string)
	return RunService:IsServer() and Remote:GetAttribute(Name) ~= nil
end

function Identifier.Unique()
	NextUnique += 1

	if NextUnique == 0xFFFF then
		NextUnique = 0
	end

	return UInt(NextUnique)
end

return Identifier


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

local ReliableRemote = ReplicatedStorage:WaitForChild("ReliableRedEvent") :: RemoteEvent
local UnreliableRemote = ReplicatedStorage:WaitForChild("UnreliableRedEvent") :: RemoteEvent

local ListenerMap: { [string]: ({ any }) -> () } = {}
local CallMap: { [string]: thread } = {}
local Outgoing: {
	Reliable: { [string]: { { any } } },
	Call: { [string]: { { any } } },
} = {
	Reliable = {},
	Call = {},
}

local function SendReliableEvent(EventId: string, CallList: { any })
	if not Outgoing.Reliable[EventId] then
		Outgoing.Reliable[EventId] = {}
	end

	table.insert(Outgoing.Reliable[EventId], CallList)
end

local function SendUnreliableEvent(EventId: string, CallList: { any })
	UnreliableRemote:FireServer(EventId, CallList)
end

local function SetListener(EventId: string, Callback: ({ any }) -> ())
	ListenerMap[EventId] = Callback
end

local function CallAsync(EventId: string, CallList: { any }): { any }
	if not Outgoing.Call[EventId] then
		Outgoing.Call[EventId] = {}
	end

	table.insert(Outgoing.Call[EventId], CallList)
	CallMap[CallList[1]] = coroutine.running()

	return coroutine.yield()
end

local function Start()
	ReliableRemote.OnClientEvent:Connect(function(IncomingFireSection, IncomingCallSection)
		if IncomingFireSection then
			for EventId, CallList in IncomingFireSection do
				local Listener = ListenerMap[EventId]

				if Listener then
					for _, Call in CallList do
						Listener(Call)
					end
				end
			end
		end

		if IncomingCallSection then
			for CallId, CallList in IncomingCallSection do
				local CallThread = CallMap[CallId]

				if CallThread then
					CallMap[CallId] = nil
					coroutine.resume(CallThread, CallList)
				end
			end
		end
	end)

	UnreliableRemote.OnClientEvent:Connect(function(IncomingEventId, IncomingArgs)
		local Listener = ListenerMap[IncomingEventId]

		if Listener then
			Listener(IncomingArgs)
		end
	end)

	RunService.Heartbeat:Connect(function()
		if next(Outgoing.Reliable) or next(Outgoing.Call) then
			ReliableRemote:FireServer(Outgoing.Reliable, Outgoing.Call)

			Outgoing.Reliable = {}
			Outgoing.Call = {}
		end
	end)
end

return {
	SendReliableEvent = SendReliableEvent,
	SendUnreliableEvent = SendUnreliableEvent,
	SetListener = SetListener,
	CallAsync = CallAsync,
	Start = Start,
}


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

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

local ReliableRemote = ReplicatedStorage:WaitForChild("ReliableRedEvent") :: RemoteEvent
local UnreliableRemote = ReplicatedStorage:WaitForChild("UnreliableRedEvent") :: RemoteEvent

local FireSectionCheck = Guard.Check(Guard.Map(Guard.String, Guard.List(Guard.Any)))
local CallSectionCheck = Guard.Check(Guard.Map(Guard.String, Guard.List(Guard.Any)))

local ListenerMap: { [string]: (Player, { any }) -> () } = {}
local OutgoingMap: {
	[Player]: {
		Reliable: { [string]: { { any } } },
		CallReturn: { [string]: { any } },
	},
} = {}

local function SendReliableEvent(Player: Player, EventId: string, Args: { any })
	if not OutgoingMap[Player] then
		OutgoingMap[Player] = {
			Reliable = {},
			CallReturn = {},
		}
	end

	if not OutgoingMap[Player].Reliable[EventId] then
		OutgoingMap[Player].Reliable[EventId] = {}
	end

	table.insert(OutgoingMap[Player].Reliable[EventId], Args)
end

local function SendUnreliableEvent(Player: Player, EventId: string, Args: { any })
	UnreliableRemote:FireClient(Player, EventId, Args)
end

local function SetListener(EventId: string, Callback: (Player, { any }) -> ())
	ListenerMap[EventId] = Callback
end

local function SendCallReturn(Player: Player, CallId: string, Args: { any })
	if not OutgoingMap[Player] then
		OutgoingMap[Player] = {
			Reliable = {},
			CallReturn = {},
		}
	end

	OutgoingMap[Player].CallReturn[CallId] = Args
end

local function Start()
	ReliableRemote.OnServerEvent:Connect(function(Player, IncomingFireSection, IncomingCallSection)
		local FireSectionValid, FireSection = FireSectionCheck(IncomingFireSection)

		if FireSectionValid then
			for EventId, CallList in FireSection do
				local Callback = ListenerMap[EventId]

				if Callback then
					for _, Call in CallList do
						Callback(Player, Call)
					end
				end
			end
		end

		local CallSectionValid, CallSection = CallSectionCheck(IncomingCallSection)

		if CallSectionValid then
			for EventId, CallList in CallSection do
				local Callback = ListenerMap[EventId]

				if Callback then
					for _, Call in CallList do
						Callback(Player, Call)
					end
				else
					for _, Call in CallList do
						local CallId = Call[1]

						SendCallReturn(Player, CallId, { false, "Event has no listener." })
					end
				end
			end
		end
	end)

	UnreliableRemote.OnServerEvent:Connect(function(Player, IncomingEventId, IncomingArgs)
		if type(IncomingEventId) == "string" and type(IncomingArgs) == "table" then
			local Callback = ListenerMap[IncomingEventId]

			if Callback then
				Callback(Player, IncomingArgs)
			end
		end
	end)

	RunService.Heartbeat:Connect(function()
		for Player, Outgoing in OutgoingMap do
			if next(Outgoing.Reliable) or next(Outgoing.CallReturn) then
				ReliableRemote:FireClient(Player, Outgoing.Reliable, Outgoing.CallReturn)
			end

			OutgoingMap[Player] = nil
		end
	end)
end

return {
	SendReliableEvent = SendReliableEvent,
	SendUnreliableEvent = SendUnreliableEvent,
	SetListener = SetListener,
	SendCallReturn = SendCallReturn,
	Start = Start,
}


return {
	Server = require(script.Server),
	Client = require(script.Client),
}


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

if RunService:IsServer() then
	if not ReplicatedStorage:FindFirstChild("ReliableRedEvent") then
		local ReliableRemote = Instance.new("RemoteEvent")
		ReliableRemote.Name = "ReliableRedEvent"
		ReliableRemote.Parent = ReplicatedStorage
	end

	if not ReplicatedStorage:FindFirstChild("UnreliableRedEvent") then
		local UnreliableRemote = Instance.new("UnreliableRemoteEvent")
		UnreliableRemote.Name = "UnreliableRedEvent"
		UnreliableRemote.Parent = ReplicatedStorage
	end

	require(script.Net).Server.Start()
else
	ReplicatedStorage:WaitForChild("ReliableRedEvent")
	ReplicatedStorage:WaitForChild("UnreliableRedEvent")

	require(script.Net).Client.Start()
end

local SharedEvents = require(script.SharedEvent)

return {
	Event = require(script.Event),
	Function = require(script.Function),
	SharedEvent = SharedEvents.SharedCallEvent,
	SharedSignalEvent = SharedEvents.SharedSignalEvent,
}


local Spawn = require(script.Parent.Spawn)

type SignalNode<T...> = {
	Next: SignalNode<T...>?,
	Callback: (T...) -> (),
}

export type Signal<T...> = {
	Root: SignalNode<T...>?,

	Connect: (self: Signal<T...>, Callback: (T...) -> ()) -> () -> (),
	Wait: (self: Signal<T...>) -> T...,
	Once: (self: Signal<T...>, Callback: (T...) -> ()) -> () -> (),
	Fire: (self: Signal<T...>, T...) -> (),
	DisconnectAll: (self: Signal<T...>) -> (),
}

local Signal = {}
Signal.__index = Signal

-- Extracted this function from Connect as it results in the closure
-- made in Connect using less memory because this function can be static
local function Disconnect<T...>(self: Signal<T...>, Node: SignalNode<T...>)
	if self.Root == Node then
		self.Root = Node.Next
	else
		local Current = self.Root

		while Current do
			if Current.Next == Node then
				Current.Next = Node.Next
				break
			end

			Current = Current.Next
		end
	end
end

function Signal.Connect<T...>(self: Signal<T...>, Callback: (T...) -> ()): () -> ()
	local Node = {
		Next = self.Root,
		Callback = Callback,
	}

	self.Root = Node

	return function()
		Disconnect(self, Node)
	end
end

function Signal.Wait<T...>(self: Signal<T...>): T...
	local Thread = coroutine.running()
	local Disconnect

	Disconnect = self:Connect(function(...)
		Disconnect()
		coroutine.resume(Thread, ...)
	end)

	return coroutine.yield()
end

function Signal.Once<T...>(self: Signal<T...>, Callback: (T...) -> ()): () -> ()
	local Disconnect

	Disconnect = self:Connect(function(...)
		Disconnect()
		Callback(...)
	end)

	return Disconnect
end

function Signal.Fire<T...>(self: Signal<T...>, ...: T...)
	local Current = self.Root

	while Current do
		Spawn(Current.Callback, ...)
		Current = Current.Next
	end
end

function Signal.DisconnectAll<T...>(self: Signal<T...>)
	self.Root = nil
end

return function<T...>(): Signal<T...>
	return setmetatable({
		Root = nil,
	}, Signal) :: any
end


local FreeThreads: { thread } = {}

local function RunCallback(Callback, Thread, ...)
	Callback(...)
	table.insert(FreeThreads, Thread)
end

local function Yielder()
	while true do
		RunCallback(coroutine.yield())
	end
end

return function<T...>(Callback: (T...) -> (), ...: T...)
	local Thread
	if #FreeThreads > 0 then
		Thread = FreeThreads[#FreeThreads]
		FreeThreads[#FreeThreads] = nil
	else
		Thread = coroutine.create(Yielder)
		coroutine.resume(Thread)
	end

	task.spawn(Thread, Callback, Thread, ...)
end


-- Trove
-- Stephen Leitnick
-- October 16, 2021

local FN_MARKER = newproxy()
local THREAD_MARKER = newproxy()

local RunService = game:GetService("RunService")

local function GetObjectCleanupFunction(object, cleanupMethod)
	local t = typeof(object)
	if t == "function" then
		return FN_MARKER
	elseif t == "thread" then
		return THREAD_MARKER
	end
	if cleanupMethod then
		return cleanupMethod
	end
	if t == "Instance" then
		return "Destroy"
	elseif t == "RBXScriptConnection" then
		return "Disconnect"
	elseif t == "table" then
		if typeof(object.Destroy) == "function" then
			return "Destroy"
		elseif typeof(object.Disconnect) == "function" then
			return "Disconnect"
		end
	end
	error("Failed to get cleanup function for object " .. t .. ": " .. tostring(object), 3)
end

local function AssertPromiseLike(object)
	if
		type(object) ~= "table"
		or type(object.getStatus) ~= "function"
		or type(object.finally) ~= "function"
		or type(object.cancel) ~= "function"
	then
		error("Did not receive a Promise as an argument", 3)
	end
end

--[=[
	@class Trove
	A Trove is helpful for tracking any sort of object during
	runtime that needs to get cleaned up at some point.
]=]
local Trove = {}
Trove.__index = Trove

--[=[
	@return Trove
	Constructs a Trove object.
]=]
function Trove.new()
	local self = setmetatable({}, Trove)
	self._objects = {}
	self._cleaning = false
	return self
end

--[=[
	@return Trove
	Creates and adds another trove to itself. This is just shorthand
	for `trove:Construct(Trove)`. This is useful for contexts where
	the trove object is present, but the class itself isn't.

	:::note
	This does _not_ clone the trove. In other words, the objects in the
	trove are not given to the new constructed trove. This is simply to
	construct a new Trove and add it as an object to track.
	:::

	```lua
	local trove = Trove.new()
	local subTrove = trove:Extend()

	trove:Clean() -- Cleans up the subTrove too
	```
]=]
function Trove:Extend()
	if self._cleaning then
		error("Cannot call trove:Extend() while cleaning", 2)
	end
	return self:Construct(Trove)
end

--[=[
	Clones the given instance and adds it to the trove. Shorthand for
	`trove:Add(instance:Clone())`.
]=]
function Trove:Clone(instance: Instance): Instance
	if self._cleaning then
		error("Cannot call trove:Clone() while cleaning", 2)
	end
	return self:Add(instance:Clone())
end

--[=[
	@param class table | (...any) -> any
	@param ... any
	@return any
	Constructs a new object from either the
	table or function given.

	If a table is given, the table's `new`
	function will be called with the given
	arguments.

	If a function is given, the function will
	be called with the given arguments.

	The result from either of the two options
	will be added to the trove.

	This is shorthand for `trove:Add(SomeClass.new(...))`
	and `trove:Add(SomeFunction(...))`.

	```lua
	local Signal = require(somewhere.Signal)

	-- All of these are identical:
	local s = trove:Construct(Signal)
	local s = trove:Construct(Signal.new)
	local s = trove:Construct(function() return Signal.new() end)
	local s = trove:Add(Signal.new())

	-- Even Roblox instances can be created:
	local part = trove:Construct(Instance, "Part")
	```
]=]
function Trove:Construct(class, ...)
	if self._cleaning then
		error("Cannot call trove:Construct() while cleaning", 2)
	end
	local object = nil
	local t = type(class)
	if t == "table" then
		object = class.new(...)
	elseif t == "function" then
		object = class(...)
	end
	return self:Add(object)
end

--[=[
	@param signal RBXScriptSignal
	@param fn (...: any) -> ()
	@return RBXScriptConnection
	Connects the function to the signal, adds the connection
	to the trove, and then returns the connection.

	This is shorthand for `trove:Add(signal:Connect(fn))`.

	```lua
	trove:Connect(workspace.ChildAdded, function(instance)
		print(instance.Name .. " added to workspace")
	end)
	```
]=]
function Trove:Connect(signal, fn)
	if self._cleaning then
		error("Cannot call trove:Connect() while cleaning", 2)
	end
	return self:Add(signal:Connect(fn))
end

--[=[
	@param name string
	@param priority number
	@param fn (dt: number) -> ()
	Calls `RunService:BindToRenderStep` and registers a function in the
	trove that will call `RunService:UnbindFromRenderStep` on cleanup.

	```lua
	trove:BindToRenderStep("Test", Enum.RenderPriority.Last.Value, function(dt)
		-- Do something
	end)
	```
]=]
function Trove:BindToRenderStep(name: string, priority: number, fn: (dt: number) -> ())
	if self._cleaning then
		error("Cannot call trove:BindToRenderStep() while cleaning", 2)
	end
	RunService:BindToRenderStep(name, priority, fn)
	self:Add(function()
		RunService:UnbindFromRenderStep(name)
	end)
end

--[=[
	@param promise Promise
	@return Promise
	Gives the promise to the trove, which will cancel the promise if the trove is cleaned up or if the promise
	is removed. The exact promise is returned, thus allowing chaining.

	```lua
	trove:AddPromise(doSomethingThatReturnsAPromise())
		:andThen(function()
			print("Done")
		end)
	-- Will cancel the above promise (assuming it didn't resolve immediately)
	trove:Clean()

	local p = trove:AddPromise(doSomethingThatReturnsAPromise())
	-- Will also cancel the promise
	trove:Remove(p)
	```

	:::caution Promise v4 Only
	This is only compatible with the [roblox-lua-promise](https://eryn.io/roblox-lua-promise/) library, version 4.
	:::
]=]
function Trove:AddPromise(promise)
	if self._cleaning then
		error("Cannot call trove:AddPromise() while cleaning", 2)
	end
	AssertPromiseLike(promise)
	if promise:getStatus() == "Started" then
		promise:finally(function()
			if self._cleaning then
				return
			end
			self:_findAndRemoveFromObjects(promise, false)
		end)
		self:Add(promise, "cancel")
	end
	return promise
end

--[=[
	@param object any -- Object to track
	@param cleanupMethod string? -- Optional cleanup name override
	@return object: any
	Adds an object to the trove. Once the trove is cleaned or
	destroyed, the object will also be cleaned up.

	The following types are accepted (e.g. `typeof(object)`):

	| Type | Cleanup |
	| ---- | ------- |
	| `Instance` | `object:Destroy()` |
	| `RBXScriptConnection` | `object:Disconnect()` |
	| `function` | `object()` |
	| `thread` | `coroutine.close(object)` |
	| `table` | `object:Destroy()` _or_ `object:Disconnect()` |
	| `table` with `cleanupMethod` | `object:<cleanupMethod>()` |

	Returns the object added.

	```lua
	-- Add a part to the trove, then destroy the trove,
	-- which will also destroy the part:
	local part = Instance.new("Part")
	trove:Add(part)
	trove:Destroy()

	-- Add a function to the trove:
	trove:Add(function()
		print("Cleanup!")
	end)
	trove:Destroy()

	-- Standard cleanup from table:
	local tbl = {}
	function tbl:Destroy()
		print("Cleanup")
	end
	trove:Add(tbl)

	-- Custom cleanup from table:
	local tbl = {}
	function tbl:DoSomething()
		print("Do something on cleanup")
	end
	trove:Add(tbl, "DoSomething")
	```
]=]
function Trove:Add(object: any, cleanupMethod: string?): any
	if self._cleaning then
		error("Cannot call trove:Add() while cleaning", 2)
	end
	local cleanup = GetObjectCleanupFunction(object, cleanupMethod)
	table.insert(self._objects, { object, cleanup })
	return object
end

--[=[
	@param object any -- Object to remove
	Removes the object from the Trove and cleans it up.

	```lua
	local part = Instance.new("Part")
	trove:Add(part)
	trove:Remove(part)
	```
]=]
function Trove:Remove(object: any): boolean
	if self._cleaning then
		error("Cannot call trove:Remove() while cleaning", 2)
	end
	return self:_findAndRemoveFromObjects(object, true)
end

--[=[
	Cleans up all objects in the trove. This is
	similar to calling `Remove` on each object
	within the trove. The ordering of the objects
	removed is _not_ guaranteed.
]=]
function Trove:Clean()
	if self._cleaning then
		return
	end
	self._cleaning = true
	for _, obj in self._objects do
		self:_cleanupObject(obj[1], obj[2])
	end
	table.clear(self._objects)
	self._cleaning = false
end

function Trove:_findAndRemoveFromObjects(object: any, cleanup: boolean): boolean
	local objects = self._objects
	for i, obj in ipairs(objects) do
		if obj[1] == object then
			local n = #objects
			objects[i] = objects[n]
			objects[n] = nil
			if cleanup then
				self:_cleanupObject(obj[1], obj[2])
			end
			return true
		end
	end
	return false
end

function Trove:_cleanupObject(object, cleanupMethod)
	if cleanupMethod == FN_MARKER then
		object()
	elseif cleanupMethod == THREAD_MARKER then
		coroutine.close(object)
	else
		object[cleanupMethod](object)
	end
end

--[=[
	@param instance Instance
	@return RBXScriptConnection
	Attaches the trove to a Roblox instance. Once this
	instance is removed from the game (parent or ancestor's
	parent set to `nil`), the trove will automatically
	clean up.

	:::caution
	Will throw an error if `instance` is not a descendant
	of the game hierarchy.
	:::
]=]
function Trove:AttachToInstance(instance: Instance)
	if self._cleaning then
		error("Cannot call trove:AttachToInstance() while cleaning", 2)
	elseif not instance:IsDescendantOf(game) then
		error("Instance is not a descendant of the game hierarchy", 2)
	end
	return self:Connect(instance.Destroying, function()
		self:Destroy()
	end)
end

--[=[
	Alias for `trove:Clean()`.
]=]
function Trove:Destroy()
	self:Clean()
end

return Trove


--!strict

local ServerStorage = game:GetService("ServerStorage")
local Shared = game.ReplicatedStorage.Shared
local RunContext = require(Shared.RunContext)

type Service = {
    Init: (Service) -> ()?,
    Start: (Service) -> ()?,
}

local Services = {} :: {
    [string]: Service
}

for _, desc in ServerStorage:GetDescendants() do
    if desc:IsA("ModuleScript") then
        local name = desc.Name

        if not name:match("Service$") then
            continue
        end

        local success, service = xpcall(require, function(err)
            error(`!! FATAL ERROR REQUIRING SERVICE {desc:GetFullName()}\n{err}\n{debug.traceback()}`)
        end, desc)

        if success then
            local init = service.Init
            local didInit = true

            if type(init) == "function" then
                didInit = xpcall(init, function (err)
                    error(`!! FATAL ERROR INITIALIZING SERVICE {desc:GetFullName()}\n{err}\n{debug.traceback()}`)
                end, service)
            end

            if didInit then
                Services[name] = service
            end
        end
    end
end

for name, service in pairs(Services) do
    local start = service.Start

    if type(start) == "function" then
        task.spawn(start, service)
    end
end

if RunContext.IsStudio then
    print("Server started!")
end

for i, child in Shared:GetChildren() do
    if child:IsA("ModuleScript") then
        task.spawn(xpcall, require, function(err)
            error(`!! FATAL ERROR LOADING SHARED MODULE {child:GetFullName()}\n{err}\n{debug.traceback()}`)
        end, child)
    end
end


local AVLTree = {}
AVLTree.__index = AVLTree

export type Node<T = any> = {
    Value: T,
    Left: Node<T>?,
    Right: Node<T>?,
    Height: number,
}

export type Class<T = any> = typeof(setmetatable({} :: {
    Root: Node<T>?,
    Compare: (T, T) -> number,

    _lookup: {
        [T]: Node<T>,
    },

    _size: number,
}, AVLTree))

local function inOrder(node: Node)
    if node.Left then
        inOrder(node.Left)
    end

    coroutine.yield(node.Value)

    if node.Right then
        inOrder(node.Right)
    end
end

local function preOrder(node: Node)
    coroutine.yield(node.Value)

    if node.Left then
        preOrder(node.Left)
    end

    if node.Right then
        preOrder(node.Right)
    end
end

local function postOrder(node: Node)
    if node.Left then
        postOrder(node.Left)
    end

    if node.Right then
        postOrder(node.Right)
    end

    coroutine.yield(node.Value)
end

local function getHeight(node: Node?): number
    return node and node.Height or 0
end

local function getBalance(node: Node?): number
    if node then
        return getHeight(node.Left) - getHeight(node.Right)
    end

    return 0
end

local function updateHeight(node: Node)
    node.Height = 1 + math.max(getHeight(node.Left), getHeight(node.Right))
end

local function rotateRight(y: Node): Node
    local x = assert(y.Left)
    y.Left = x.Right
    x.Right = y

    updateHeight(y)
    updateHeight(x)

    return x
end

local function rotateLeft(x: Node): Node
    local y = assert(x.Right)
    x.Right = y.Left
    y.Left = x

    updateHeight(x)
    updateHeight(y)

    return y
end

local function insertNode<T>(self: Class<T>, node: Node<T>?, value: T): Node<T>
    if not node then
        local newNode: Node<T> = {
            Value = value,
            Left = nil,
            Right = nil,
            Height = 1,
        }

        self._lookup[value] = newNode
        return newNode
    end

    local comp = self.Compare(value, node.Value)

    if comp < 0 then
        node.Left = insertNode(self, node.Left, value)
    elseif comp > 0 then
        node.Right = insertNode(self, node.Right, value)
    else
        return node
    end

    updateHeight(node)

    local balance = getBalance(node)

    -- Left Left Case
    if balance > 1 and node.Left and self.Compare(value, node.Left.Value) < 0 then
        return rotateRight(node)
    end

    -- Right Right Case
    if balance < -1 and node.Right and self.Compare(value, node.Right.Value) > 0 then
        return rotateLeft(node)
    end

    -- Left Right Case
    if balance > 1 and node.Left and self.Compare(value, node.Left.Value) > 0 then
        node.Left = rotateLeft(node.Left)
        return rotateRight(node)
    end

    -- Right Left Case
    if balance < -1 and node.Right and self.Compare(value, node.Right.Value) < 0 then
        node.Right = rotateRight(node.Right)
        return rotateLeft(node)
    end

    return node
end

local function removeNode<T>(self: Class<T>, node: Node<T>?, value: T): Node<T>?
    if not node then
        return nil
    end

    local comp = self.Compare(value, node.Value)

    if comp < 0 then
        node.Left = removeNode(self, node.Left, value)
    elseif comp > 0 then
        node.Right = removeNode(self, node.Right, value)
    else
        if not node.Left or not node.Right then
            local temp = node.Left or node.Right

            if not temp then
                node = nil
            else
                node = temp
            end
        else
            local temp = node.Right

            while temp.Left do
                temp = temp.Left
            end

            local tempVal = temp.Value
            node.Value = tempVal

            self._lookup[tempVal] = node
            node.Right = removeNode(self, node.Right, tempVal)
        end
    end

    if node then
        updateHeight(node)
    else
        return nil
    end

    local balance = getBalance(node)

    -- Left Left Case
    if balance > 1 and getBalance(node.Left) >= 0 then
        return rotateRight(node)
    end

    -- Left Right Case
    if balance > 1 and node.Left and getBalance(node.Left) < 0 then
        node.Left = rotateLeft(node.Left)
        return rotateRight(node)
    end

    -- Right Right Case
    if balance < -1 and getBalance(node.Right) <= 0 then
        return rotateLeft(node)
    end

    -- Right Left Case
    if balance < -1 and node.Right and getBalance(node.Right) > 0 then
        node.Right = rotateRight(node.Right)
        return rotateLeft(node)
    end

    return node
end

function AVLTree.new<T>(compare: (T, T) -> number): Class<T>
    return setmetatable({
        Root = nil,
        Compare = compare,

        _lookup = {},
        _size = 0,
    }, AVLTree)
end

function AVLTree.Insert<T>(self: Class<T>, value: T)
    self.Root = insertNode(self, self.Root, value)
end

function AVLTree.Remove<T>(self: Class<T>, value: T)
    self.Root = removeNode(self, self.Root, value)
    self._lookup[value] = nil
end

function AVLTree.Find<T>(self: Class<T>, value: T): T?
    local node = self.Root

    while node do
        local comp = self.Compare(value, node.Value)

        if comp == 0 then
            return node.Value
        elseif comp < 0 then
            node = node.Left
        else
            node = node.Right
        end
    end

    return nil
end

function AVLTree.Contains(self: Class, value: any): boolean
    local node = self:Find(value)
    return node ~= nil
end

function AVLTree.GetHeight(self: Class): number
    return getHeight(self.Root)
end

function AVLTree.InOrder<T>(self: Class<T>): () -> T?
    return coroutine.wrap(function()
        if self.Root then
            inOrder(self.Root)
        end
    end)
end

function AVLTree.PreOrder<T>(self: Class<T>): () -> T?
    return coroutine.wrap(function()
        if self.Root then
            preOrder(self.Root)
        end
    end)
end

function AVLTree.PostOrder<T>(self: Class<T>): () -> T?
    return coroutine.wrap(function()
        if self.Root then
            postOrder(self.Root)
        end
    end)
end

function AVLTree.First<T>(self: Class<T>): T?
    local node = self.Root

    while node and node.Left do
        node = node.Left
    end

    return node and node.Value
end

function AVLTree.Last<T>(self: Class<T>): T?
    local node = self.Root

    while node and node.Right do
        node = node.Right
    end

    return node and node.Value
end

function AVLTree.IsEmpty(self: Class): boolean
    return self.Root == nil
end

function AVLTree.Clear(self: Class)
    self.Root = nil
    self._lookup = {}
end

function AVLTree.__iter<T>(self: Class)
    return AVLTree.InOrder(self)
end

return AVLTree


local function readf16(buf: buffer, offset: number): number
    local b0 = buffer.readu8(buf, offset)
    local b1 = buffer.readu8(buf, offset + 1)

    local sign = bit32.btest(b0, 128) and -1 or 1
    local exponent = bit32.rshift(bit32.band(b0, 127), 2)
    local mantissa = bit32.lshift(bit32.band(b0, 3), 8) + b1

    if exponent == 31 then --2^5-1
        if mantissa ~= 0 then
            return (0 / 0)
        else
            return math.huge * sign
        end
    elseif exponent == 0 then
        if mantissa == 0 then
            return 0
        else
            return math.ldexp(mantissa / 1024, -14) * sign
        end
    end

    return math.ldexp((mantissa / 1024) + 1, exponent - 15) * sign
end

local function writef16(buf: buffer, offset: number, value: number)
    local mantissa, exponent
    local sign = value < 0

    value = math.abs(value)
    mantissa, exponent = math.frexp(value)

    if value == math.huge then
        if sign then
            buffer.writeu8(buf, offset, 0b_11111100)
        else
            buffer.writeu8(buf, offset, 0b_01111100)
        end

        buffer.writeu8(buf, offset + 1, 0b_00000000)
    elseif value ~= value or value == 0 then
        buffer.writeu16(buf, offset, 0)
    elseif exponent + 15 <= 1 then -- Bias for halfs is 15
        mantissa = math.floor(mantissa * 1024 + 0.5)

        if sign then
            buffer.writeu8(buf, offset, (128 + bit32.rshift(mantissa, 8))) -- Sign bit, 5 empty bits, 2 from mantissa
        else
            buffer.writeu8(buf, offset, (bit32.rshift(mantissa, 8)))
        end

        buffer.writeu8(buf, offset + 1, bit32.band(mantissa, 255)) -- Get last 8 bits from mantissa
    else
        mantissa = ((mantissa - 0.5) * 2048 + 0.5) // 1

        -- The bias for halfs is 15, 15-1 is 14
        if sign then
            buffer.writeu8(buf, offset, (128 + bit32.lshift(exponent + 14, 2) + bit32.rshift(mantissa, 8)))
        else
            buffer.writeu8(buf, offset, (bit32.lshift(exponent + 14, 2) + bit32.rshift(mantissa, 8)))
        end

        buffer.writeu8(buf, offset + 1, bit32.band(mantissa, 255))
    end
end

local function readVarInt(buf: buffer, offset: number)
    local result = 0
    local shift = 0
    local byte

    repeat
        byte = buffer.readu8(buf, offset)
        result += bit32.lshift(bit32.band(byte, 127), shift)

        offset += 1
        shift += 7
    until bit32.btest(byte, 128)

    return result, offset
end

local function writeVarInt(buf: buffer, offset: number, value: number)
    repeat
        local byte = bit32.band(value, 127)
        value = bit32.rshift(value, 7)

        if value ~= 0 then
            byte = bit32.bor(byte, 128)
        end

        buffer.writeu8(buf, offset, byte)
        offset += 1
    until value == 0

    return offset
end

return table.freeze({
    ReadFloat16 = readf16,
    WriteFloat16 = writef16,

    ReadVarInt = readVarInt,
    WriteVarInt = writeVarInt,
})


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

local ConVar = require(Shared.ConVar)
export type IColor = Color3 | string
type ConVar = ConVar.Class

local Colors = {
    Black = Color3.fromHex("#000000"),
    White = Color3.fromHex("#ffffff"),

    Red = Color3.fromHex("#ff0000"),
    Green = Color3.fromHex("#00ff00"),
    Blue = Color3.fromHex("#0000ff"),

    Yellow = Color3.fromHex("#ffff00"),
    Cyan = Color3.fromHex("#00ffff"),
    Magenta = Color3.fromHex("#ff00ff"),

    Brick = table.freeze({
        Red = Color3.fromHex("#c4281c"),
        Orange = Color3.fromHex("#da8541"),
        Yellow = Color3.fromHex("#f5cd30"),
        Shrek = Color3.fromHex("#a4bd47"),
        Green = Color3.fromHex("#4b974b"),
        Blue = Color3.fromHex("#0d69ac"),
        Purple = Color3.fromHex("#6b327c"),
        White = Color3.fromHex("#f2f3f3"),
        Gray = Color3.fromHex("#a3a2a5"),
        Dark = Color3.fromHex("#635f62"),
        Black = Color3.fromHex("#1b2a35"),
        Pink = Color3.fromHex("#e8bac8"),
        Tan = Color3.fromHex("#d7c59a"),
    }),
}

Colors.ConVars = {
    BLOOD = ConVar.Declare("cl_blood_color", "#ff0000", ConVar.LOCAL, "Sets the color of blood effects.")
}

Colors.Gradient = table.freeze({
    Blue = ColorSequence.new(
        Color3.fromHex("#2CB9F7"),
        Color3.fromHex("#1441FF")
    ),
    Cyan = ColorSequence.new(
        Color3.fromHex("#3CEEFD"),
        Color3.fromHex("#29A4FF")
    ),
    Toothpaste = ColorSequence.new(
        Color3.fromHex("#3E3EFF"),
        Color3.fromHex("#0DFFAB")
    ),
    Green = ColorSequence.new(
        Color3.fromHex("#2CF77A"),
        Color3.fromHex("#10B274")
    ),
    LimeGreen = ColorSequence.new(
        Color3.fromHex("#39F791"),
        Color3.fromHex("#17FF79")
    ),
    Pink = ColorSequence.new(
        Color3.fromHex("#D341FF"),
        Color3.fromHex("#FF365D")
    ),
    Red = ColorSequence.new(
        Color3.fromHex("#F72C63"),
        Color3.fromHex("#D11440")
    ),
    Purple = ColorSequence.new(
        Color3.fromHex("#6514FF"),
        Color3.fromHex("#A12CF7")
    ),
    PurpleBurst = ColorSequence.new(
        Color3.fromHex("#8239F7"),
        Color3.fromHex("#2917FF")
    ),
    Yellow = ColorSequence.new(
        Color3.fromHex("#F7D02C"),
        Color3.fromHex("#FF7714")
    ),
    Orange = ColorSequence.new(
        Color3.fromHex("#F72C53"),
        Color3.fromHex("#FF7714")
    ),
    LightGrey = ColorSequence.new(
        Color3.fromHex("#e8e8e8"),
        Color3.fromHex("#cccccc")
    ),
    MediumGrey = ColorSequence.new(
        Color3.fromHex("#939393"),
        Color3.fromHex("#636363")
    ),
    Grey = ColorSequence.new(
        Color3.fromHex("#4d4d4d"),
        Color3.fromHex("#2d2d2d")
    ),
    DarkGrey = ColorSequence.new(
        Color3.fromHex("#3C3C3C"),
        Color3.fromHex("#1C1C1C")
    ),
    RedShade = ColorSequence.new(
        Color3.fromHex("#ff0000"),
        Color3.fromHex("#7f0000")
    ),
    GreenShade = ColorSequence.new(
        Color3.fromHex("#00ff00"),
        Color3.fromHex("#007f00")
    ),
    BlueShade = ColorSequence.new(
        Color3.fromHex("#0000ff"),
        Color3.fromHex("#00007f")
    ),
    PurpleShade = ColorSequence.new(
        Color3.fromHex("#ff00ff"),
        Color3.fromHex("#7f007f")
    ),
    MagentaShade = ColorSequence.new(
        Color3.fromHex("#ff00af"),
        Color3.fromHex("#7f005f")
    ),
    Admin = ColorSequence.new({
        ColorSequenceKeypoint.new(0 / 3, Color3.fromHex("#7F0000")),
        ColorSequenceKeypoint.new(2 / 3, Color3.fromHex("#FF00AF")),
        ColorSequenceKeypoint.new(3 / 3, Color3.fromHex("#7F0000")),
    }),
    Vintage = ColorSequence.new(
        Color3.fromHex("#5883ce"),
        Color3.fromHex("#2D3A4B")
    ),
    SelfMade = ColorSequence.new(
        Color3.fromHex("#70B04A"),
        Color3.fromHex("#4A7F2A")
    ),
})

Colors.TierColor = table.freeze({
    Default = Colors.Gradient.DarkGrey,
    Stock = Colors.Gradient.DarkGrey,
    Common = Colors.Gradient.DarkGrey,
    Uncommon = Colors.Gradient.Green,
    Rare = Colors.Gradient.Cyan,
    Epic = Colors.Gradient.Red,
    Legendary = Colors.Gradient.Orange,
    Mythic = Colors.Gradient.Purple,
    Exclusive = Colors.Gradient.Pink,
    Vintage = Colors.Gradient.Vintage,
    SelfMade = Colors.Gradient.SelfMade,
    Admin = Colors.Gradient.Admin,
})

Colors.Index8 = table.freeze({
    Colors.Brick.Red,
    Colors.Brick.Blue,
    Colors.Brick.Green,
    Colors.Brick.Purple,
    Colors.Brick.Orange,
    Colors.Brick.Yellow,
    Colors.Brick.Pink,
    Colors.Brick.Tan,
})

function Colors.fromIndex8(index: number)
    return Colors.Index8[1 + index % 8]
end

function Colors.fromName(rawName: string)
    local name = buffer.fromstring(rawName)
    local len = buffer.len(name)
    local value = 0

	for i = 1, len do
		local byte = buffer.readu8(name, i - 1)
		local revIndex = len - i + 1

		if len % 2 == 1 then
			revIndex -= 1
		end

		if revIndex % 4 >= 2 then
            value -= byte
        else
            value += byte
		end
	end

    return Colors.fromIndex8(value)
end

function Colors.fromPlayer(player: Player)
    if player.Neutral then
        return Colors.fromName(player.Name)
    else
        return player.TeamColor.Color
    end
end

function Colors.fromUserId(userId: number)
    local player = Players:GetPlayerByUserId(userId)

    if player then
        return Colors.fromName(player.Name)
    else
        return Colors.fromIndex8(userId)
    end
end

function Colors.GetColorDelta(color: Color3, delta: number)
	local H, S, V = color:ToHSV()
	return Color3.fromHSV(math.abs(H), math.abs(S + (delta / 255)), math.abs(V))
end

function Colors.ResolveColor(color: IColor?): Color3
    if type(color) == "string" then
        if color:lower():match("#?%x%x%x%x%x%x") then
            return Color3.fromHex(color)
        elseif color:sub(1, 1) == "@" then
            local id = color:sub(2)
            local cvar = Colors.ConVars[id]

            if cvar then
                return cvar:GetColor3()
            end
        else
            local indexed = Colors[color]

            if typeof(indexed) == "Color3" then
                return indexed
            end
        end
    elseif typeof(color) == "Color3" then
        return color
    end

    return Colors.Black
end

function Colors.AvgLumOfSequence(color: ColorSequence)
    local keypoints = color.Keypoints
    local count = #keypoints
    local avg = 0

    for i, keypoint in keypoints do
        local color = keypoint.Value
        local r, g, b = color.R, color.G, color.B

        local luminance = 0.299 * r + 0.587 * g + 0.114 * b
        avg += (luminance / count)
    end

    return avg
end

return table.freeze(Colors)


-- CrunchTable lets you define compression schemes for simple tables to be sent by roblox
-- If a field in a table is not defined in the layout, it will be ignored and stay in the table
-- If a field in a table is not present, but is defined in the layout, it'll default to 0 (or equiv)

--!native
--!strict

local module = {}

local Shared = script.Parent
local Guid = require(Shared.Guid)
local BitUtils = require(Shared.BitUtils)

export type LazyTable = {
    [string]: any
}

export type BinaryTable = {
    _b: buffer,
    [string]: any,
}

module.Enum = table.freeze({
    IGNORE = 0,

    BOOL = 1,
    BOOLEAN = 1,

    UINT8 = 2,
    BYTE = 2,

    INT8 = 3,
    SBYTE = 3,

    UINT16 = 4,
    USHORT = 4,

    INT16 = 5,
    SHORT = 5,

    UINT32 = 6,
    UINT = 6,

    INT32 = 7,
    INT = 7,

    FLOAT16 = 8,
    HALF = 8,

    FLOAT32 = 9,
    SINGLE = 9,
    FLOAT = 9,

    FLOAT64 = 10,
    DOUBLE = 10,

    INT53 = 11,
    UINT53 = 12,

    Vector2 = 13,
    Vector3 = 14,
    NumberRange = 15,

    STRING8 = 16,
    STRING16 = 17,
    STRING24 = 18,
    STRING32 = 19,
    STRING64 = 20,
    GUID = 21,
})

local building = {} :: {
    [Layout]: boolean
}

local meta = {} :: {
    [number]: {
        Size: number,
        Default: any,
        Reader: (buffer, number) -> any,
        Writer: (buffer, number, any) -> (),
    }
}

local function stringIO(size: number)
    return {
        Size = size,
        Default = "",

        Reader = function(buf: buffer, offset: number): string
            local str = buffer.readstring(buf, offset, size)
            return str:gsub("\0", "")
        end,

        Writer = function(buf: buffer, offset: number, value: string)
            -- Iterate the graphemes of the string, make sure we don't write invalid UTF-8.
            for i, j in utf8.graphemes(value) do
                if j > size then
                    break
                end

                local char = value:sub(i, j)
                buffer.writestring(buf, offset + (i - 1), char)
            end
        end,
    }
end

meta[module.Enum.IGNORE] = table.freeze({
    Size = 0,
    Default = nil,
    Reader = function() end,
    Writer = function() end,
})

meta[module.Enum.BOOL] = table.freeze({
    Size = 1,
    Default = false,

    Reader = function(buf: buffer, offset: number): boolean
        return buffer.readu8(buf, offset) > 0
    end,

    Writer = function(buf: buffer, offset: number, value: boolean)
        buffer.writeu8(buf, offset, value and 1 or 0)
    end,
})

meta[module.Enum.UINT8] = table.freeze({
    Size = 1,
    Default = 0,
    Reader = buffer.readu8,
    Writer = buffer.writeu8,
})

meta[module.Enum.INT8] = table.freeze({
    Size = 1,
    Default = 0,
    Reader = buffer.readi8,
    Writer = buffer.writei8,
})

meta[module.Enum.UINT16] = table.freeze({
    Size = 2,
    Default = 0,
    Reader = buffer.readu16,
    Writer = buffer.writeu16,
})

meta[module.Enum.INT16] = table.freeze({
    Size = 2,
    Default = 0,
    Reader = buffer.readi16,
    Writer = buffer.writei16,
})

meta[module.Enum.UINT32] = table.freeze({
    Size = 4,
    Default = 0,
    Reader = buffer.readu32,
    Writer = buffer.writeu32,
})

meta[module.Enum.INT32] = {
    Size = 4,
    Default = 0,
    Reader = buffer.readi32,
    Writer = buffer.writei32,
}

meta[module.Enum.FLOAT16] = table.freeze({
    Size = 2,
    Default = 0,

    Reader = BitUtils.ReadFloat16,
    Writer = BitUtils.WriteFloat16,
})

meta[module.Enum.FLOAT32] = table.freeze({
    Size = 4,
    Default = 0,
    Reader = buffer.readf32,
    Writer = buffer.writef32,
})

meta[module.Enum.FLOAT64] = table.freeze({
    Size = 8,
    Default = 0,
    Reader = buffer.readf64,
    Writer = buffer.writef64,
})

meta[module.Enum.INT53] = table.freeze({
    Size = 8,
    Default = 0,

    Reader = function (buf: buffer, offset: number): number
        local lo = buffer.readu32(buf, offset)
        local hi = buffer.readi32(buf, offset + 4)
        return (hi * 2147483647) + lo
    end,

    Writer = function (buf: buffer, offset: number, value: number)
        local lo = value % 2147483647
        local hi = value // 2147483647

        buffer.writeu32(buf, offset, lo)
        buffer.writei32(buf, offset + 4, hi)
    end,
})

meta[module.Enum.UINT53] = table.freeze({
    Size = 8,
    Default = 0,

    Reader = function (buf: buffer, offset: number): number
        local lo = buffer.readu32(buf, offset)
        local hi = buffer.readu32(buf, offset + 4)
        return (hi * 4294967296) + lo
    end,

    Writer = function (buf: buffer, offset: number, value: number)
        local lo = value % 4294967296
        local hi = value // 4294967296

        buffer.writeu32(buf, offset, lo)
        buffer.writeu32(buf, offset + 4, hi)
    end,
})

meta[module.Enum.Vector2] = {
    Size = 8,
    Default = Vector2.zero,

    Reader = function(buf: buffer, offset: number): Vector2
        local x = buffer.readf32(buf, offset)
        local y = buffer.readf32(buf, offset + 4)
        return Vector2.new(x, y)
    end,

    Writer = function(buf: buffer, offset: number, value: Vector2)
        buffer.writef32(buf, offset, value.X)
        buffer.writef32(buf, offset + 4, value.Y)
    end,
}

meta[module.Enum.Vector3] = {
    Size = 12,
    Default = Vector3.zero,

    Reader = function(buf: buffer, offset: number): Vector3
        local x = buffer.readf32(buf, offset)
        local y = buffer.readf32(buf, offset + 4)
        local z = buffer.readf32(buf, offset + 8)

        return Vector3.new(x, y, z)
    end,

    Writer = function(buf: buffer, offset: number, value: Vector3)
        buffer.writef32(buf, offset, value.X)
        buffer.writef32(buf, offset + 4, value.Y)
        buffer.writef32(buf, offset + 8, value.Z)
    end,
}

meta[module.Enum.NumberRange] = {
    Size = 8,
    Default = NumberRange.new(0),

    Reader = function(buf: buffer, offset: number): NumberRange
        local min = buffer.readf32(buf, offset)
        local max = buffer.readf32(buf, offset + 4)
        return NumberRange.new(min, max)
    end,

    Writer = function(buf: buffer, offset: number, value: NumberRange)
        buffer.writef32(buf, offset, value.Min)
        buffer.writef32(buf, offset + 4, value.Max)
    end,
}

meta[module.Enum.GUID] = {
    Size = 16,
    Default = "00000000-0000-0000-0000-000000000000",

    Reader = Guid.Decompress,
    Writer = Guid.Compress,
}

meta[module.Enum.STRING8] = stringIO(8)
meta[module.Enum.STRING16] = stringIO(16)
meta[module.Enum.STRING24] = stringIO(24)
meta[module.Enum.STRING32] = stringIO(32)
meta[module.Enum.STRING64] = stringIO(64)

local Layout = {}
Layout.__index = Layout

type FieldInfo = {
    Required: boolean,
    Default: any,
    Name: string,
    Enum: number,
    Size: number,

    Read: (buffer, number) -> any,
    Write: (buffer, number, any) -> (),
}

-- stylua: ignore
export type Layout = typeof(setmetatable({} :: {
    _building: boolean?,

    FieldsByName: {
        [string]: FieldInfo,
    },

    Fields: {FieldInfo},
    FieldBytes: number,
    TotalBytes: number,

    ContentBytes: number,
    ContentReader: (buffer, number) -> number,
    ContentWriter: (buffer, number, number) -> (),
}, Layout))

local function calcSize(self: Layout)
    if building[self] then
        return
    end

    local fields = self.Fields
    local numFields = #fields

    local contentBytes
    local contentReader
    local contentWriter

    if numFields <= 8 then
        contentBytes = 1
        contentReader = buffer.readu8
        contentWriter = buffer.writeu8
    elseif numFields <= 16 then
        contentBytes = 2
        contentReader = buffer.readu16
        contentWriter = buffer.writeu16
    elseif numFields <= 32 then
        contentBytes = 4
        contentReader = buffer.readu32
        contentWriter = buffer.writeu32
    else
        error("!! FATAL: Exceeded limit of 32 fields in CrunchTable layout!")
    end

    local fieldBytes = 0
    self.ContentBytes = contentBytes
    self.ContentReader = contentReader
    self.ContentWriter = contentWriter

    for index, rec in self.Fields do
        fieldBytes += rec.Size
    end

    self.FieldBytes = fieldBytes
    self.TotalBytes = contentBytes + fieldBytes
end

--[=[
    Call this to optimize redundant calculations when adding fields to the layout.

    **IMPORTANT:** Make sure to call End() after you're done adding fields

    @param fieldsAreRequiredByDefault If `true`, all fields added will be marked as **required** unless marked otherwise. If `false`, all fields added will be marked as **optional** unless marked otherwise. Default: `true`.
    @return The `Layout` object.
]=]
function Layout.Begin(self: Layout, fieldsAreRequiredByDefault: boolean?): Layout
    if fieldsAreRequiredByDefault == nil then
        building[self] = true
    else
        building[self] = fieldsAreRequiredByDefault
    end

    return self
end

--[=[
    Finishes the layout and calculates its size.
]=]
function Layout.End(self: Layout)
    if building[self] ~= nil then
        building[self] = nil
        calcSize(self)
    end
end

function Layout.Add(self: Layout, field: string, enum: number, required: boolean?): Layout
    local fieldMeta = meta[enum]

    local isRequired = if required == nil
        then building[self]
        else required

    local fieldInfo = table.freeze({
        Required = isRequired,
        Default = fieldMeta.Default,
        Write = fieldMeta.Writer,
        Read = fieldMeta.Reader,
        Size = fieldMeta.Size,
        Name = field,
        Enum = enum,
    })

    self.FieldsByName[field] = fieldInfo
    table.insert(self.Fields, fieldInfo)

    calcSize(self)
    return self
end

function Layout.GetFieldInfo(self: Layout, field: string): FieldInfo?
    return self.FieldsByName[field]
end

function Layout.WriteToBuffer(self: Layout, srcData: LazyTable, buf: buffer, start: number?, onWrite: (key: string) -> ()?): number
    local write = self.ContentWriter
    local offset = (start or 0) + self.ContentBytes

    local contents = 0
    local bit = 1

    for index, rec in self.Fields do
        local key = rec.Name
        local srcValue = srcData[key]

        if srcValue ~= nil then
            rec.Write(buf, offset, srcValue)
            offset += rec.Size
            contents += bit

            if onWrite then
                onWrite(key)
            end
        end

        bit += bit
    end

    write(buf, start or 0, contents)
    return offset
end

function Layout.Compress(self: Layout, srcData: LazyTable): BinaryTable
    local packet = table.clone(srcData)

    -- Create buffer with max possible size.
    local work = buffer.create(self.TotalBytes)

    local size = self:WriteToBuffer(srcData, work, 0, function(key: string)
        packet[key] = nil
    end)

    -- Copy the buffer to a new one with the exact size.
    local b = buffer.create(size)
    buffer.copy(b, 0, work, 0, size)
    packet._b = b

    return packet
end

function Layout.ReadFromBuffer(self: Layout, buf: buffer, start: number, callback: (key: string, value: any) -> ()): number
    local reader = self.ContentReader
    local offset = start or 0

    local contentBits
    local bit = 1

    contentBits = reader(buf, offset)
    offset += self.ContentBytes

    for index, rec in self.Fields do
        if bit32.btest(contentBits, bit) then
            local value = rec.Read(buf, offset)
            callback(rec.Name, value)
            offset += rec.Size
        elseif rec.Required then
            callback(rec.Name, rec.Default)
        end

        bit += bit
    end

    return offset
end

function Layout.Decompress(self: Layout, packet: BinaryTable): LazyTable
    local buf = packet._b

    local data: LazyTable = table.clone(packet)
    data._b = nil

    self:ReadFromBuffer(buf, 0, function(key: string, value: any)
        data[key] = value
    end)

    return data
end

function Layout.Clone(self: Layout): Layout
    local newLayout = setmetatable({
        FieldsByName = table.clone(self.FieldsByName),
        Fields = table.clone(self.Fields),

        TotalBytes = self.TotalBytes,
        FieldBytes = self.FieldBytes,
        ContentBytes = self.ContentBytes,

        ContentReader = self.ContentReader,
        ContentWriter = self.ContentWriter,
    }, Layout)

    return newLayout
end

function module.new(lazyFields: { [string]: number }?): Layout
    local layout = setmetatable({
        FieldsByName = {},
        Fields = {},

        TotalBytes = 0,
        FieldBytes = 0,
        ContentBytes = 0,

        ContentReader = buffer.readu8,
        ContentWriter = buffer.writeu8,
    }, Layout)

    if lazyFields then
        -- Order needs to be deterministic so its constant across all clients.
        -- If the order shouldn't be alphabetical, use the Begin/End API directly.
        local keys = {}

        for key in lazyFields do
            table.insert(keys, key)
        end

        layout:Begin(false)
        table.sort(keys)

        for i, key in keys do
            local enum = lazyFields[key]
            layout:Add(key, enum)
        end

        layout:End()
    end

    return layout
end

return module


--!native
--!strict

local DeltaTable = {}

export type LazyTable = {
    [any]: any,
}

export type Patch = {
    Path: { string },
    Value: any,
}

export type Trace = {
    Path: { string },
    OnApply: (path: { string }, value: any) -> (),
}

local function Deep(tbl: any): any
    local tCopy = table.create(#tbl)
    setmetatable(tCopy, getmetatable(tbl))

    for k, v in pairs(tbl) do
        if type(v) == "table" then
            tCopy[k] = Deep(v)
        else
            tCopy[k] = v
        end
    end

    return tCopy
end

local function GetDeltaKey(key: any): any
    if type(key) == "number" then
        return `_#{key}`
    end

    return key
end

local function ParseDeltaKey(key: any): any
    if type(key) == "string" and key:sub(1, 2) == "_#" then
        return tonumber(key:sub(3))
    end

    return key
end

local function Apply(maybeTarget: LazyTable?, deltaTable: LazyTable, trace: Trace?)
    local target: LazyTable = maybeTarget or {}
    local deletions = deltaTable.__deletions

    for deltaKey in deltaTable do
        if deltaKey == "__deletions" then
            continue
        end

        local key = ParseDeltaKey(deltaKey)

        if trace then
            table.insert(trace.Path, tostring(key))
        end

        if type(deltaTable[deltaKey]) == "table" then
            target[key] = Apply(target[key], deltaTable[deltaKey], trace)
        else
            target[key] = deltaTable[deltaKey]

            if trace then
                trace.OnApply(trace.Path, deltaTable[deltaKey])
            end
        end

        if trace then
            table.remove(trace.Path)
        end
    end

    if deletions then
        for i, key in pairs(deletions) do
            target[key] = nil

            if trace then
                table.insert(trace.Path, key)
                trace.OnApply(trace.Path, nil)
                table.remove(trace.Path)
            end
        end
    end

    return target
end

DeltaTable.DeepCopy = Deep

-- Compares two tables, and produces a new table containing the differences
function DeltaTable.Create(oldTable: any, newTable: any, fuzzyEpsilon: number?): (any, number)
    if oldTable == nil then
        return Deep(newTable), 0
    end

    local deltaTable = {}
    local changes = 0

    for key, data in pairs(newTable) do
        local deltaKey = GetDeltaKey(key)

        if oldTable[key] == nil then
            changes += 1
            deltaTable[deltaKey] = data
        else
            if type(newTable[key]) == "table" then
                -- its a table, recurse
                local childTable, num = DeltaTable.Create(oldTable[key], newTable[key])

                if num > 0 then
                    changes += num
                    deltaTable[deltaKey] = childTable
                end
            else
                local old = oldTable[key]
                local new = newTable[key]

                if fuzzyEpsilon then
                    if type(old) == "number" and type(new) == "number" then
                        if math.abs(old - new) < fuzzyEpsilon then
                            continue
                        end
                    elseif type(old) == "vector" and type(new) == "vector" then
                        if vector.magnitude((new - old) :: any) < fuzzyEpsilon then
                            continue
                        end
                    end
                end

                if old ~= new then
                    changes += 1
                    deltaTable[deltaKey] = new
                end
            end
        end
    end

    -- Check for deletions
    local deletions = {}

    for key in pairs(oldTable) do
        if newTable[key] == nil then
            table.insert(deletions, key)
            changes += 1
        end
    end

    if next(deletions) then
        deltaTable.__deletions = deletions
    end

    return deltaTable, changes
end

--- Produces a new table that is the combination of a target, and a deltaTable produced by DeltaTable.Create
function DeltaTable.Apply(target: LazyTable?, deltaTable: LazyTable, onApply: (path: {string}, value: any) -> ()?): LazyTable
    local trace: Trace?

    if onApply then
        trace = {
            Path = {},
            OnApply = onApply,
        }
    end

    return Apply(target, deltaTable, trace)
end

return DeltaTable


--!strict

local Flags = {}
Flags.__index = Flags

export type Class = typeof(setmetatable({} :: {
    _value: number?,
    _inst: Instance?,
    _attr: string?
}, Flags))

function Flags.new(value: number?): Class
    return setmetatable({
        _value = value or 0
    }, Flags)
end

function Flags.GetString(value: number, flags: { [string]: number }): string
    local result = {}

    for name, flag in pairs(flags) do
        if bit32.btest(value, flag) then
            table.insert(result, name)
        end
    end

    return "[" .. table.concat(result, " | ") .. "]"
end

function Flags.fromAttribute(inst: Instance, name: string): Class
    local value = tonumber(inst:GetAttribute(name)) or 0

    return setmetatable({
        _value = value // 1,
        _inst = inst,
        _attr = name,
    }, Flags)
end

function Flags.Get(self: Class): number
    local inst, attr = self._inst, self._attr
    local value

    if inst and attr then
        value = tonumber(inst:GetAttribute(attr)) or 0
    else
        value = self._value or 0
    end

    return math.abs(value // 1)
end

function Flags.Set(self: Class, value: number)
    local inst, attr = self._inst, self._attr
    value = math.abs(value // 1)

    if inst and attr then
        inst:SetAttribute(attr, value // 1)
    else
        self._value = value // 1
    end
end

function Flags.Has(self: Class, flag: number): boolean
    local value = self:Get()
    return bit32.btest(value, flag)
end

function Flags.Add(self: Class, flag: number)
    local value = self:Get()
    self:Set(bit32.bor(value, flag))
end

function Flags.Remove(self: Class, flag: number)
    local value = self:Get()
    self:Set(bit32.band(value, bit32.bnot(flag)))
end

function Flags.Toggle(self: Class, flag: number)
    local value = self:Get()
    self:Set(bit32.bxor(value, flag))
end

return Flags


local Packages = game.ReplicatedStorage.Packages

local module = require(Packages.Future)
export type Class<T...> = module.Future<T...>

return module


local HttpService = game:GetService("HttpService")

local function create()
    return HttpService:GenerateGUID(false)
end

local function compress(buf: buffer, offset: number, value: string)
    local stream = value:gsub("-", "")

    local a = tonumber(stream:sub(1, 8), 16)
    buffer.writeu32(buf, offset, a or 0)

    local b = tonumber(stream:sub(9, 12), 16)
    buffer.writeu16(buf, offset + 4, b or 0)

    local c = tonumber(stream:sub(13, 16), 16)
    buffer.writeu16(buf, offset + 6, c or 0)

    local d = tonumber(stream:sub(17, 20), 16)
    buffer.writeu16(buf, offset + 8, d or 0)

    local e = tonumber(stream:sub(21, 24), 16)
    buffer.writeu16(buf, offset + 10, e or 0)

    local f = tonumber(stream:sub(25, 32), 16)
    buffer.writeu32(buf, offset + 12, f or 0)

    return offset + 16
end

local function decompress(buf: buffer, offset: number): string
    local a = buffer.readu32(buf, offset)

    local b = buffer.readu16(buf, offset + 4)
    local c = buffer.readu16(buf, offset + 6)
    local d = buffer.readu16(buf, offset + 8)

    local e = buffer.readu16(buf, offset + 10)
    local f = buffer.readu32(buf, offset + 12)

    return string.format("%08x-%04x-%04x-%04x-%04x%08x", a, b, c, d, e, f), offset + 16
end

return table.freeze({
    Create = create,
    Compress = compress,
    Decompress = decompress,
})


-- JointTree
-- ContextLost
-- April 25th, 2020

--!strict

local JointTree = {}
JointTree.__index = JointTree

type Edge = {
    Part: BasePart,
    Joint: JointInstance,
}

type Edges = {
    [number]: Edge,
}

type Joints = {
    [BasePart]: Edges
}

type CFrames = {
    [BasePart]: CFrame,
}

--------------------------------------------------------------------------------------------------------------------------------------------------------
-- Tree Generation
--------------------------------------------------------------------------------------------------------------------------------------------------------

local function addJointEdge(joints: Joints, joint: JointInstance)
    local part0 = joint.Part0
    local part1 = joint.Part1

    if part0 and part1 then
        if not joints[part0] then
            joints[part0] = {}
        end

        if not joints[part1] then
            joints[part1] = {}
        end

        table.insert(joints[part0], {
            Joint = joint,
            Part = part1,
        })

        table.insert(joints[part1], {
            Joint = joint,
            Part = part0,
        })
    end
end

local function expandTree(joints: Joints, part0: BasePart, maybeEdges: Edges?): Edges
    local adjacent = joints[part0]
    local edges: Edges = maybeEdges or {}

    if adjacent then
        -- We only want to iterate over part's edges once, remove edges to mark as visited
        joints[part0] = nil

        for i, edge in adjacent do
            local part1 = edge.Part

            -- Checks if we've already included this part. This will at least be a list with edge back
            -- to parent unless we've already visited this part through another joint and removed it.
            -- Breaks cycles and prioritizes shortest path to root.

            if joints[part1] then
                -- Add the parent-child joint edge to the tree list
                table.insert(edges, edge)

                -- Recursively add child's edges, DFS order. BFS would
                -- have been fine too, but either works just as well.
                expandTree(joints, part1, edges)
            end
        end
    end

    return edges
end

--[[
     Returns a list of assembly edges in some tree-sorted order that can be used by `applyTree` to
     position other parts in `model` relative to `rootPart` if they would be in the same Assembly
     under a `WorldRoot`. This roughly imitates what the internal spanning tree that `WorldRoot` uses
     to build an internal transform hierarchy of parts in an assembly, with some limitations:

     - Only supports Motor6D, and Weld. Didn't bother with legacy Motor, Snap, ManualWeld.
     - Doesn't support Motor/Motor6D.CurrentAngle and co.
     - Doesn't support WeldConstraints. Can't. Transform isn't exposed to Lua.
     - Doesn't prioritize by joint type. Weld should take priority over Motor.
     - Doesn't prioritize by joint/part GUID. Can't. Not exposed to Lua.

     For a reasonable model, like an R15 character, that doesn't have duplicate or unsupported joints
     it should produce the same results as the Roblox spanning tree when applied.

     { { joint, childPart }, ... }
]]--

function JointTree.Build(model: Model, rootPart: BasePart?)
    local joints = {}

    -- Gather the part-joint graph.
    for i, desc in model:GetDescendants() do
        if desc:IsA("JointInstance") then
            local p0 = desc.Part0
            local p1 = desc.Part1

            if p0 and p1 then
                -- Add edge to both parts. Assembly joints are bidirectional.
                addJointEdge(joints, desc)
            end
        end
    end

    -- Build the tree, in order, by recursively following edges out from the root part
    -- Joint edge list map: { [part] = { { joint, otherPart }, ...}, ... }

    if not rootPart then
        rootPart = model.PrimaryPart
    end

    if not rootPart then
        local bestMass = 0
        local bestPart: BasePart?

        for part in joints do
            local mass = part.Mass

            if mass > bestMass then
                bestMass = mass
                bestPart = part
            end
        end

        rootPart = bestPart
    end

    -- Return the generated edges.
    assert(rootPart, "Model does not have any assembly root!")
    return expandTree(joints, rootPart)
end

--------------------------------------------------------------------------------------------------------------------------------------------------------
-- CFrame Index
--------------------------------------------------------------------------------------------------------------------------------------------------------

local CFrameMeta = {
    __index = function (_self, part: BasePart)
        return part.CFrame
    end,

    __newindex = function (_self, part: BasePart, cf: CFrame)
        part.CFrame = cf
    end,
}

local CFrameIndex = {} :: {
    [BasePart]: CFrame,
}

setmetatable(CFrameIndex :: any, CFrameMeta)

--------------------------------------------------------------------------------------------------------------------------------------------------------
-- Tree Solvers
--------------------------------------------------------------------------------------------------------------------------------------------------------

function JointTree.Solve(tree: { Edge }, index: CFrames?, ignoreTransform: boolean?)
    local cframes = index or CFrameIndex

    for i, edge in tree do
        local joint = edge.Joint
        local childPart = edge.Part

        local p0 = joint.Part0
        local p1 = joint.Part1

        if p0 and p1 then
            local c0 = joint.C0
            local c1 = joint.C1

            if joint:IsA("Motor6D") and not ignoreTransform then
                -- Motor6D, including Motor6D.Transform. Motor6D is now consistently P0->Transform->P1 after recent change.
                local transform = joint.Transform

                if p1 == childPart then
                    cframes[p1] = cframes[p0] * c0 * transform * c1:Inverse()
                else
                    cframes[p0] = cframes[p1] * c1 * transform:Inverse() * c0:Inverse()
                end
            else
                -- Weld
                if p1 == childPart then
                    cframes[p1] = cframes[p0] * c0 * c1:Inverse()
                else
                    cframes[p0] = cframes[p1] * c1 * c0:Inverse()
                end
            end
        end
    end
end

--------------------------------------------------------------------------------------------------------------------------------------------------------

return JointTree