--// Properties //--

local ALERT_ABOUT_FLIPBOOK_DIMENSIONS = true
local REDUCE_RATE_BY_QUALITY = true
local DELTA_TIME = nil -- leave nil if you want to use user framerate

--// Types //--

export type ParticleEmitter2D = ParticleEmitter & {
	EmitPosition: UDim2,
	EmitParent: GuiObject?,
	FlipbookDimensions: number?,

	EmitAtPosition: (self: ParticleEmitter2D, particleCount: number?, position: UDim2?) -> (),
	EmitInParent: (self: ParticleEmitter2D, particleCount: number?, parent: GuiObject) -> (),
}

type ParticleReference = {
	label: ImageLabel,
	particleEmitter: ParticleEmitter,
	particleEmitterProperties: {[string]: any},
	aspectRatio: UIAspectRatioConstraint,

	time: number,
	lifetime: number,

	seed: number,

	drag: number,
	speed: number,
	rotSpeed: number,
	size: UDim2,
	direction: UDim2,
	position: UDim2?,

	currentFrame: number?,
	frameRate: number,
}

--// Constaints //--

local EMITTER_PROPERTIES = {
	"Acceleration",
	"Color",
	"Drag",
	"EmissionDirection",
	"Enabled",
	"FlipbookFramerate",
	"FlipbookLayout",
	"FlipbookMode",
	"FlipbookStartRandom",
	"LockedToPart",
	"Lifetime",
	"Orientation",
	"Rate",
	"Rotation",
	"RotSpeed",
	"Size",
	"Speed",
	"SpreadAngle",
	"Squash",
	"Texture",
	"Transparency",
	"ZOffset",
	"TimeScale"
}

local DIMENSIONS_ARRAY = {8, 16, 32, 64, 128, 256, 512, 1024}

local EMISSION_DIRECTION: {[Enum.NormalId]: number} = {
	[Enum.NormalId.Left] = 0,
	[Enum.NormalId.Top] = 90,
	[Enum.NormalId.Right] = 180,
	[Enum.NormalId.Bottom] = 270
}

local FLIPBOOK_LAYOUT_TO_NUMBER = {
	[Enum.ParticleFlipbookLayout.Grid2x2] = 2,
	[Enum.ParticleFlipbookLayout.Grid4x4] = 4,
	[Enum.ParticleFlipbookLayout.Grid8x8] = 8,
}

--// Main Setup //--
local Ember = {}

Ember.FlipbookDimensions = {
	By8x8 = 8,
	By16x16 = 16,
	By32x32 = 32,
	By64x64 = 64,
	By128x128 = 128,
	By256x256 = 256,
	By512x512 = 512,
	By1024x1024 = 1024
}

--// Services //--

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

--// Variables //--

local LocalPlayer = Players.LocalPlayer
local Camera = workspace.CurrentCamera

local ParticlesGUI = Instance.new("ScreenGui")
ParticlesGUI.DisplayOrder = 5
ParticlesGUI.Name = "Particles"
ParticlesGUI.Parent = LocalPlayer.PlayerGui
ParticlesGUI.ScreenInsets = Enum.ScreenInsets.None

--// Tables //--

local CachedEmitters2D: {[ParticleEmitter]: ParticleEmitter2D} = {}
local RecycledParticles: {[ParticleEmitter]: {ParticleReference}} = {}
local Particles: {[ParticleEmitter]: {ParticleReference}} = {}
local EmitterProperties: {[ParticleEmitter]: {[string]: any}} = {}
local AllParticles: {ParticleReference} = {}

--// Connections //--

local ParticleRenderer: RBXScriptConnection

--// Helper functions //--

local _, set_value = xpcall(function() game.test = true end, function() return debug.info(2, "f") end)
local _, get_value = xpcall(function()  _ = game[nil] end, function() return debug.info(2, "f") end)
local get_attribute = game.GetAttribute

local function decimalRandom(min: number, max: number): number
	return math.lerp(min, max, math.random())
end

local function evalNumberSequence(sequence: NumberSequence, t: number, seed: number) : number
	local sequenceKeypoints = sequence.Keypoints
	local sequenceLength = #sequenceKeypoints

	if t <= 0 then
		local keyPoint = sequenceKeypoints[1]
		return keyPoint.Value + seed * keyPoint.Envelope
	elseif t >= 1 then
		local keyPoint = sequenceKeypoints[sequenceLength]
		return keyPoint.Value + seed * keyPoint.Envelope
	end

	for i = 1, sequenceLength - 1 do
		local thisKeypoint = sequenceKeypoints[i]
		local nextKeypoint = sequenceKeypoints[i + 1]

		local thisKeypointTime = thisKeypoint.Time
		local nextKeypointTime = nextKeypoint.Time

		if t >= thisKeypointTime and t < nextKeypointTime then
			local alpha = (t - thisKeypointTime) / (nextKeypointTime - thisKeypointTime)

			return math.lerp(thisKeypoint.Value, nextKeypoint.Value, alpha) + seed * math.lerp(thisKeypoint.Envelope, nextKeypoint.Envelope, alpha)
		end
	end
end

local function evalColorSequence(sequence: ColorSequence, t: number) : Color3
	local sequenceKeypoints = sequence.Keypoints
	local sequenceLength = #sequenceKeypoints

	if t <= 0 then
		return sequenceKeypoints[1].Value
	elseif t >= 1 then
		return sequenceKeypoints[sequenceLength].Value
	end

	for i = 1, sequenceLength - 1 do
		local thisKeypoint = sequenceKeypoints[i]
		local nextKeypoint = sequenceKeypoints[i + 1]

		local thisKeypointTime = thisKeypoint.Time
		local nextKeypointTime = nextKeypoint.Time

		if t >= thisKeypointTime and t < nextKeypointTime then
			local alpha = (t - thisKeypointTime) / (nextKeypointTime - thisKeypointTime)

			local thisKeypointValue = thisKeypoint.Value
			local nextKeypointValue = nextKeypoint.Value

			return Color3.new(
				math.lerp(thisKeypointValue.R, nextKeypointValue.R, alpha),
				math.lerp(thisKeypointValue.G, nextKeypointValue.G, alpha),
				math.lerp(thisKeypointValue.B, nextKeypointValue.B, alpha)
			)
		end
	end
end

local function getRandomPositionInFrame(frame: Frame) : Vector2
	local size = frame.AbsoluteSize
	local position = frame.AbsolutePosition
	local rotation = math.rad(frame.AbsoluteRotation)

	local center = position + size / 2

	local localX = math.random() * size.X - size.X / 2
	local localY =  math.random() * size.Y - size.Y / 2

	local rotatedX = localX * math.cos(rotation) - localY * math.sin(rotation)
	local rotatedY = localX * math.sin(rotation) + localY * math.cos(rotation)

	local normalized = (center + Vector2.new(rotatedX, rotatedY)) / Camera.ViewportSize

	return UDim2.fromScale(normalized.X, normalized.Y)
end

local function getRandomDirectionVector(baseAngleDeg: number, spreadAngleDeg: number) : UDim2
	local halfSpread = spreadAngleDeg / 2
	local minAngle = baseAngleDeg - halfSpread
	local maxAngle = baseAngleDeg + halfSpread
	local randomAngleDeg = math.random() * (maxAngle - minAngle) + minAngle
	local randomAngleRad = math.rad(randomAngleDeg)

	local x = math.cos(randomAngleRad)
	local y = math.sin(randomAngleRad)

	local normalized = Vector2.new(x, y) / Camera.ViewportSize

	return UDim2.fromScale(-normalized.X, -normalized.Y)
end

local function getFrameFromFlipbook(dimensions: number, layout: number, frame: number) : (Vector2, Vector2)
	frame = math.max(1, frame % (layout * layout)) - 1

	local frameSize = dimensions / layout

	local columIndex = frame % layout
	local rowIndex = frame // layout

	local offsetX = columIndex * frameSize
	local offsetY = rowIndex * frameSize

	return Vector2.new(offsetX, offsetY), Vector2.new(frameSize, frameSize)
end

local function createParticleEmitterFromTable(emitterData: ParticleEmitter)
	local emitter = Instance.new("ParticleEmitter")

	for property, value in emitterData do
		if not table.find(EMITTER_PROPERTIES, property) then continue end

		emitter[property] = value
	end

	return emitter
end

--// Main functions //--

local function DestroyParticleReference(particleReference: ParticleReference)
	local index = table.find(AllParticles, particleReference)

	if index then
		particleReference.label.Visible = false
		particleReference.label.Position = UDim2.fromScale(-.5, -.5)

		table.remove(AllParticles, index)
		table.insert(RecycledParticles[particleReference.particleEmitter], particleReference.label)

		table.clear(particleReference)
	end
end

local function RenderParticles(dt: number)
	dt = DELTA_TIME or dt
	local cameraSize = get_value(Camera, "ViewportSize")
	local cameraX, cameraY = cameraSize.X, cameraSize.Y

	set_value(ParticlesGUI, "Enabled", false)

	for index, particle in AllParticles do
		local label = particle.label
		local emitterProperties = particle.particleEmitterProperties
		local aspectRatioConstraint = particle.aspectRatio

		local t = particle.time

		particle.time += dt * emitterProperties.TimeScale

		local lifetime = particle.lifetime
		local alpha = t / lifetime

		if alpha >= 1 then
			set_value(label, "Visible", false)

			local emitter = particle.particleEmitter
			table.insert(RecycledParticles[emitter], label)
			table.remove(AllParticles, index)
			table.clear(particle)

			continue
		end

		local seed = particle.seed
		local drag = particle.drag
		local speed = particle.speed
		local direction = particle.direction
		local rotSpeed = particle.rotSpeed

		local acceleration = emitterProperties.Acceleration
		local accX = acceleration.X / cameraX * t * t * 20
		local accY = acceleration.Y / cameraY * t * t * 20

		local actualSpeed = drag and speed * (0.5 ^ (t / drag)) or speed
		local newPosition = particle.position + UDim2.fromScale(
			direction.X.Scale * actualSpeed * t - accX,
			direction.Y.Scale * actualSpeed * t - accY
		)

		local positionXScale, positionYScale = newPosition.X.Scale, newPosition.Y.Scale
		if positionXScale < -.1 or positionXScale > 1.1 or positionYScale > 1.1 or positionYScale < -.1 then continue end

		local transparency = evalNumberSequence(emitterProperties.Transparency, alpha, seed)
		if transparency == 1 then continue end

		local sizeAlpha = evalNumberSequence(emitterProperties.Size, alpha, seed) / 10
		local squash = evalNumberSequence(emitterProperties.Squash, alpha, seed)
		local color = evalColorSequence(emitterProperties.Color, alpha)
		local rotation = get_value(label, "Rotation")

		set_value(label, "Position", newPosition)
		set_value(label, "ImageTransparency", transparency)
		set_value(label, "ImageColor3", color)
		set_value(label, "Rotation", rotation + rotSpeed * dt)
		set_value(label, "Size", UDim2.fromScale(sizeAlpha, sizeAlpha))

		set_value(aspectRatioConstraint, "AspectRatio", squash > 0 and squash + 1 or math.max(1e-4, 1 + (squash / 3)))

		local layout = emitterProperties.FlipbookLayout
		if layout ~= Enum.ParticleFlipbookLayout.None then
			local frame = particle.currentFrame
			local flipbookDim = emitterProperties.FlipbookDimensions or Ember.FlipbookDimensions.By1024x1024
			local layoutSize = FLIPBOOK_LAYOUT_TO_NUMBER[layout]
			local mode = emitterProperties.FlipbookMode

			local offset, size = getFrameFromFlipbook(flipbookDim, layoutSize, frame // 1)
			set_value(label, "ImageRectOffset", offset)
			set_value(label, "ImageRectSize", size)

			if mode == Enum.ParticleFlipbookMode.Loop then
				frame = t * particle.frameRate
			elseif mode == Enum.ParticleFlipbookMode.OneShot then
				frame = alpha * layoutSize * layoutSize
			elseif mode == Enum.ParticleFlipbookMode.Random then
				frame = math.random(layoutSize * layoutSize)
			else
				frame = (alpha < 0.5)
					and (alpha * layoutSize * layoutSize * 2)
					or ((1 - alpha) * layoutSize * layoutSize * 2)
			end

			particle.currentFrame = frame
		end
	end

	set_value(ParticlesGUI, "Enabled", true)
end

local function GenerateParticleReference(particleEmitter: ParticleEmitter, particleLabelTemplate: ImageLabel) : ParticleReference
	local particleLabel: ImageLabel = table.remove(RecycledParticles[particleEmitter])

	if not particleLabel then
		particleLabel = particleLabelTemplate:Clone()
		particleLabel.ZIndex = particleEmitter.ZOffset * 1e4
		particleLabel.Parent = particleLabelTemplate.Parent or ParticlesGUI
		particleLabel.Rotation = decimalRandom(particleEmitter.Rotation.Min, particleEmitter.Rotation.Max)
	else
		particleLabel.Position = UDim2.fromScale(-.5, -.5)
	end

	particleLabel.Visible = true

	local lifeTime = decimalRandom(particleEmitter.Lifetime.Min, particleEmitter.Lifetime.Max)
	local speed = decimalRandom(particleEmitter.Speed.Min, particleEmitter.Speed.Max) * 20
	local rotSpeed = decimalRandom(particleEmitter.RotSpeed.Min, particleEmitter.RotSpeed.Max)
	local frameRate = decimalRandom(particleEmitter.FlipbookFramerate.Min, particleEmitter.FlipbookFramerate.Max)

	local particleReference = {
		label = particleLabel,
		particleEmitter = particleEmitter,
		particleEmitterProperties = EmitterProperties[particleEmitter],
		aspectRatio = particleLabel:FindFirstChildWhichIsA("UIAspectRatioConstraint"),

		time = 0,
		lifetime = lifeTime,

		seed = math.random() - 1,

		speed = speed,
		drag = particleEmitter.Drag ~= 0 and particleEmitter.Drag,
		rotSpeed = rotSpeed,
		size = particleLabelTemplate.Size,

		frameRate = frameRate
	}

	if particleEmitter.FlipbookLayout ~= Enum.ParticleFlipbookLayout.None then
		local layout = FLIPBOOK_LAYOUT_TO_NUMBER[particleEmitter.FlipbookLayout]

		particleReference.currentFrame = particleEmitter.FlipbookStartRandom and math.random(layout * layout) or 1
	end

	table.insert(Particles[particleEmitter], particleReference)

	return particleReference
end

local function EmitAtPosition(particleEmitter: ParticleEmitter, particleLabelTemplate: ImageLabel, position: UDim2)
	local particleReference = GenerateParticleReference(particleEmitter, particleLabelTemplate)
	local particleDirection = getRandomDirectionVector(EMISSION_DIRECTION[particleEmitter.EmissionDirection] or 90, decimalRandom(particleEmitter.SpreadAngle.X, particleEmitter.SpreadAngle.Y))

	particleReference.position = position
	particleReference.direction = particleDirection

	table.insert(AllParticles, 1, particleReference)
end

local function EmitInParent(particleEmitter: ParticleEmitter, particleLabelTemplate: ImageLabel, parent: GuiObject)
	local particleReference = GenerateParticleReference(particleEmitter, particleLabelTemplate)
	local particleDirection = getRandomDirectionVector((EMISSION_DIRECTION[particleEmitter.EmissionDirection] or 90) + parent.AbsoluteRotation, decimalRandom(particleEmitter.SpreadAngle.X, particleEmitter.SpreadAngle.Y))

	particleReference.position = getRandomPositionInFrame(parent)
	particleReference.direction = particleDirection

	table.insert(AllParticles, particleReference)
end

--// Module functions //--

function Ember.createFromTable(struct : ParticleEmitter & {EmitPosition: Vector2?}) : ParticleEmitter2D
	return Ember.new(createParticleEmitterFromTable(struct), struct.EmitPosition)
end

function Ember.new(particleEmitter: ParticleEmitter, emitPosition: UDim2?) : ParticleEmitter2D
	if CachedEmitters2D[particleEmitter] then return CachedEmitters2D[particleEmitter] end
	if ALERT_ABOUT_FLIPBOOK_DIMENSIONS and particleEmitter.FlipbookLayout ~= Enum.ParticleFlipbookLayout.None then
		warn("Flipbook particle detected, make sure to set the FlipbookDimensions if its different from 1024x1024")
	end

	local folder = Instance.new("Folder")
	folder.Name = particleEmitter.Name
	folder.Parent = ParticlesGUI

	local particleLabel = Instance.new("ImageLabel")
	particleLabel.AnchorPoint = Vector2.new(.5, .5)
	particleLabel.BackgroundTransparency = 1
	particleLabel.Size = UDim2.fromScale(.05, .05)
	particleLabel.Position = UDim2.fromScale(-.5, -.5)
	particleLabel.Image = particleEmitter.Texture
	particleLabel.Visible = false
	particleLabel.Parent = folder

	local uiAspectRatioConstraint = Instance.new("UIAspectRatioConstraint")
	uiAspectRatioConstraint.Parent = particleLabel

	local emitParticlesThread: thread
	local enabledChanged: RBXScriptConnection
	local destroyedParticle: RBXScriptConnection
	local propertyChanged: RBXScriptConnection

	local uiParticleEmitter: ParticleEmitter2D

	RecycledParticles[particleEmitter] = RecycledParticles[particleEmitter] or {}
	Particles[particleEmitter] = Particles[particleEmitter] or {}
	EmitterProperties[particleEmitter] = EmitterProperties[particleEmitter] or {}

	for _, propertyName in EMITTER_PROPERTIES do
		EmitterProperties[particleEmitter][propertyName] = particleEmitter[propertyName]
	end
	EmitterProperties[particleEmitter].FlipbookDimensions = Ember.FlipbookDimensions.By1024x1024

	local variableGetFunctions = {
		FlipbookDimensions = function()
			return EmitterProperties[particleEmitter].FlipbookDimensions or Ember.FlipbookDimensions.By1024x1024
		end,
	}

	local variableSetFunctions = {
		Enabled = function(enabled: boolean?)
			enabled = enabled or particleEmitter.Enabled

			if enabled and not emitParticlesThread then
				emitParticlesThread = task.spawn(function()
					while true do
						local rate = particleEmitter.Rate

						if REDUCE_RATE_BY_QUALITY then
							local qualityLevel = UserGameSettings.SavedQualityLevel.Value
							local rateReduction = qualityLevel == 0 and .5 or qualityLevel/10

							rate *= rateReduction
						end

						for _ = 1, math.max(rate, 1) do
							if particleEmitter.Parent:IsA("GuiObject") then
								EmitInParent(particleEmitter, particleLabel, particleEmitter.Parent)
							else
								EmitAtPosition(particleEmitter, particleLabel, uiParticleEmitter.EmitPosition or UDim2.fromScale(.5, .5))
							end

							task.wait(1/rate)
						end
					end
				end)
			elseif emitParticlesThread and not enabled then
				task.cancel(emitParticlesThread)
				emitParticlesThread = nil
			end
		end,

		FlipbookDimensions = function(dimensions: Vector2)
			if not table.find(DIMENSIONS_ARRAY, dimensions) then
				return warn("The flipbook texture must be of pixel dimensions 8×8, 16×16, 32×32, 64×64, 128×128, 256×256, 512×512, or 1024×1024")
			end

			EmitterProperties[particleEmitter].FlipbookDimensions = dimensions
		end,
	}

	uiParticleEmitter = setmetatable({
		EmitPosition = emitPosition or UDim2.fromScale(.5, .5),

		Clear = function(self)
			local particles = Particles[particleEmitter]

			for i = #particles, 1, -1 do
				local particleReference = table.remove(particles, i)
				DestroyParticleReference(particleReference)
			end
		end,

		Emit = function(self, particleCount: number?)
			particleCount = particleCount or 16

			task.spawn(function()
				for _ = 1, particleCount do
					if particleEmitter.Parent:IsA("GuiObject") then
						EmitInParent(particleEmitter, particleLabel, particleEmitter.Parent)
					else
						EmitAtPosition(particleEmitter, particleLabel, self.EmitPosition)
					end

					task.wait()
				end
			end)
		end,

		EmitAtPosition = function(self, particleCount: number?, position: UDim2?)
			particleCount = particleCount or 16
			position = position or self.EmitPosition

			task.spawn(function()
				for _ = 1, particleCount do
					EmitAtPosition(particleEmitter, particleLabel, position)
					task.wait()
				end
			end)
		end,

		EmitInParent = function(self, particleCount: number?, parent: GuiObject?)
			particleCount = particleCount or 16
			parent = parent or particleEmitter.Parent

			if not parent:IsA("GuiObject") then
				return warn("The parent is not a gui object")
			end

			task.spawn(function()
				for _ = 1, particleCount do
					EmitInParent(particleEmitter, particleLabel, parent)
					task.wait()
				end
			end)
		end,

		Destroy = function()
			if emitParticlesThread then
				task.cancel(emitParticlesThread)
			end

			destroyedParticle:Disconnect()
			propertyChanged:Disconnect()

			particleLabel:Destroy()
			folder:Destroy()

			RecycledParticles[particleEmitter] = nil
			Particles[particleEmitter] = nil
			EmitterProperties[particleEmitter] = nil
			CachedEmitters2D[particleEmitter] = nil

			table.clear(variableSetFunctions)
			table.clear(variableGetFunctions)
			table.clear(uiParticleEmitter)
		end,

	} :: ParticleEmitter, {
		__index = function(self, key)
			if variableGetFunctions[key] then
				return variableGetFunctions[key]()
			else
				return particleEmitter[key]
			end
		end,

		__newindex = function(self, key, value)
			if variableSetFunctions[key] then
				variableSetFunctions[key](value)
			else
				EmitterProperties[particleEmitter][key] = value
				particleEmitter[key] = value
			end
		end
		,})

	propertyChanged = particleEmitter.Changed:Connect(function(key: string)
		local value = particleEmitter[key]

		if variableSetFunctions[key] then
			variableSetFunctions[key](value)
		elseif table.find(EMITTER_PROPERTIES, key) then
			EmitterProperties[particleEmitter][key] = value
		end
	end)

	destroyedParticle = particleEmitter.Destroying:Connect(uiParticleEmitter.Destroy)

	variableSetFunctions.Enabled(particleEmitter.Enabled)

	CachedEmitters2D[particleEmitter] = uiParticleEmitter

	return uiParticleEmitter
end

function Ember.RenderEnabled(enabled: boolean?)
	enabled = enabled or not ParticleRenderer.Connected

	if enabled and ParticlesGUI.Enabled then
		ParticleRenderer = RunService.PreRender:Connect(RenderParticles)
	elseif ParticleRenderer.Connected then
		ParticleRenderer:Disconnect()

		for _, particleReference in AllParticles do
			DestroyParticleReference(particleReference)
		end
	end
end

--// Connection Setup //--

ParticleRenderer = RunService.PreRender:Connect(RenderParticles)

return Ember


local DataTypes = {
	Axes = Axes.new(),
	BrickColor = BrickColor.Random(),
	CellId = CellId.new(),
	CFrame = CFrame.identity,
	CatalogSearchParams = CatalogSearchParams.new(),
	Color3 = Color3.new(),
	ColorSequence = ColorSequence.new(Color3.new()),
	ColorSequenceKeypoint = ColorSequenceKeypoint.new(0, Color3.new()),
	Content = Content.none,
	DateTime = DateTime.now(),
	DockWidgetPluginGuiInfo = DockWidgetPluginGuiInfo.new(),
	Enum = Enum:GetEnums()[1],
	EnumItem = Enum:GetEnums()[1]:GetEnumItems()[1],
	Enums = Enum,
	Faces = Faces.new(),
	FloatCurveKey = FloatCurveKey.new(0, 1, Enum.KeyInterpolationMode:GetEnumItems()[1]),
	Font = Font.fromEnum(Enum.Font:GetEnumItems()[1]),
	Instance = game:GetService("RunService"),
	NumberSequence = NumberSequence.new(0),
	NumberSequenceKeypoint = NumberSequenceKeypoint.new(0, 1),
	OverlapParams = OverlapParams.new(),
	PathWaypoint = PathWaypoint.new(Vector3.zero, Enum.PathWaypointAction:GetEnumItems()[1]),
	PluginDrag = PluginDrag.new(),
	RBXScriptConnection = (function()
		local Connection = game.ChildAdded:Once(function() end)
		Connection:Disconnect()

		return Connection
	end)(),
	RBXScriptSignal = game.ChildAdded,
	Random = Random.new(),
	Ray = Ray.new(Vector3.zero, Vector3.zero),
	RaycastParams = RaycastParams.new(),
	Rect = Rect.new(),
	Region3 = Region3.new(),
	Region3int16 = Region3int16.new(),
	SecurityCapabilities = SecurityCapabilities.fromCurrent(),
	SharedTable = SharedTable.new(),
	TweenInfo = TweenInfo.new(),
	UDim = UDim.new(),
	UDim2 = UDim2.new(),
	Vector2 = Vector2.new(),
	Vector2int16 = Vector2int16.new(),
	Vector3 = vector.zero,
	Vector3int16 = Vector3int16.new(),
	boolean = {
		["true"] = true,
		["false"] = false,
	},
	buffer = buffer.create(1),
	["function"] = {
		Lua = function() end,
		C = print
	},
	table = {
		Empty = {},
		Frozen = table.freeze({}),
		Array = {1,2,3},
		Dictionary = {
			["1"] = true,
			["2"] = true,
			["3"] = true
		},
		Mix = {
			1,
			2,
			3,
			["1"] = true,
			["2"] = true,
			["3"] = true
		},
		Cyclic = (function()
			local tbl = {}
			tbl[tbl] = tbl

			return tbl
		end)(),
		metatable = setmetatable({1,2,3}, {__index = {4, 5, 6}})
	},
	number = {
		int = 1,
		nan = tonumber("nan"),
		infinite = math.huge,
		negative_infinite = -math.huge
	},
	string = {
		normal = "normal string",
		special_characters = "\a\b\t\t\v\f\r",
		special_case = "\8"..4,
		UTF8 = utf8.char(256)
	},
	thread = coroutine.create(function() end),
	userdata = {
		metatable = newproxy(true),
		empty = newproxy(false)
	}
}


-- Synchronization Interface --
cloneref = cloneref or function(f) return f; end;

local SyncManager = {};
local CoreGui = cloneref(game:GetService('CoreGui'));

SyncManager.Players = {};
SyncManager.RenderValue = false;
SyncManager.Thread = nil;
SyncManager.Icon = "rbxassetid://120358385035996";

function SyncManager:Init(NL_Lib)
	SyncManager.Icon = NL_Lib.GlobalLogo;
	SyncManager.NL = NL_Lib;
end;

function SyncManager:SetPlayers(list)
	table.clear(SyncManager.Players);
	SyncManager.Players = list;
end;

function SyncManager:_LoadPlayerList()
	local PlayerList: ScreenGui = CoreGui:FindFirstChild("PlayerList");

	for i,v: Player in next , SyncManager.Players do task.wait();
		local uid = tostring(v.UserId);

		local frame: Frame = PlayerList:FindFirstChild('p_'..uid,true) or PlayerList:FindFirstChild("PlayerEntry_"..uid,true);

		if frame then
			local PlayerIcon: ImageLabel = frame:FindFirstChild("PlayerIcon",true);

			if PlayerIcon then
				if not PlayerIcon:GetAttribute('OldImage') then
					PlayerIcon:SetAttribute('OldImage',PlayerIcon.Image);
				end;

				PlayerIcon.Image = SyncManager.Icon;
			end;
		end;
	end;
end;

SyncManager.Clear = (function()
    local PlayerList: ScreenGui = CoreGui:FindFirstChild("PlayerList");

    for i,v in next , PlayerList:GetDescendants() do
        if v:GetAttribute('OldImage') then
            v.Image = v:GetAttribute('OldImage')
        end;
    end;
end);

function SyncManager:Render(value)
	if not value then
		if SyncManager.Thread then
			task.cancel(SyncManager.Thread);
			SyncManager.Thread = nil;

            SyncManager.Clear();
		end;
	else
		if not SyncManager.Thread then
			SyncManager.Thread = task.spawn(function()
				while true do task.wait(0.1)

					SyncManager:_LoadPlayerList();

					if not SyncManager.RenderValue then
						break;
					end;
				end;
			end);
		end;
	end;

	SyncManager.RenderValue = value;
end;

return SyncManager;


local jestGlobals = require('@pkg/@jsdotlua/jest-globals')

local character = require('../character')

local expect = jestGlobals.expect
local it = jestGlobals.it
local describe = jestGlobals.describe

local TO_UPPER_CASES = {
    { 'a', 'A' },
    { 'i', 'I' },
    { 'á', 'Á' },
    { 'ã', 'Ã' },
    { 'ç', 'Ç' },
    { 'é', 'É' },
    { 'ö', 'Ö' },
    { 'ȝ', 'Ȝ' },
    { 'δ', 'Δ' },
    { 'ς', 'Σ' },
    { 'ї', 'Ї' },
    { 'ա', 'Ա' },
    { 'պ', 'Պ' },
    { 'ḃ', 'Ḃ' },
    { 'ṵ', 'Ṵ' },
    { 'ⰷ', 'Ⰷ' },
    { 'ⳗ', 'Ⳗ' },
    { 'ꭽ', 'Ꭽ' },
    { 'ﬄ', 'FFL' },
}

local TO_LOWER_CASES = {
    { 'A', 'a' },
    { 'FFL', 'ffl' },
    { 'I', 'i' },
    { 'Á', 'á' },
    { 'Ã', 'ã' },
    { 'Ç', 'ç' },
    { 'É', 'é' },
    { 'Ö', 'ö' },
    { 'Ȝ', 'ȝ' },
    { 'Δ', 'δ' },
    { 'Σ', 'σ' },
    { 'Ї', 'ї' },
    { 'Ա', 'ա' },
    { 'Պ', 'պ' },
    { 'Ꭽ', 'ꭽ' },
    { 'Ḃ', 'ḃ' },
    { 'Ṵ', 'ṵ' },
    { 'Ⰷ', 'ⰷ' },
    { 'Ⳗ', 'ⳗ' },
}

local UNCHANGED_CASES = {
    '-',
    '0',
    '1',
    '{',
    '\\',
    '\n',
    'ᴂ',
    '𝛇',
    '𝔰',
    '𝕒',
    'ℳ',
}

describe('toLowercase', function()
    for _, case in TO_LOWER_CASES do
        local upper = case[1]
        local lower = case[2]

        it(`converts '{upper}' to '{lower}'`, function()
            if character.toLowercase(upper) ~= lower then
                error(
                    'toLowercase '
                        .. upper
                        .. ' -> '
                        .. character.toLowercase(upper)
                        .. ' expected '
                        .. lower
                )
            end
            expect(character.toLowercase(upper) == lower).toBe(true)
        end)

        it(`keeps '{lower}' unchanged`, function()
            expect(character.toLowercase(lower) == lower).toBe(true)
        end)
    end

    for _, case in UNCHANGED_CASES do
        it(`keeps '{case}' unchanged`, function()
            expect(character.toLowercase(case)).toEqual(case)
        end)
    end
end)

describe('toUppercase', function()
    for _, case in TO_UPPER_CASES do
        local lower = case[1]
        local upper = case[2]

        it(`converts '{lower}' to '{upper}'`, function()
            if character.toUppercase(lower) ~= upper then
                error(
                    'toUppercase '
                        .. lower
                        .. ' -> '
                        .. character.toUppercase(lower)
                        .. ' expected '
                        .. upper
                )
            end
            expect(character.toUppercase(lower) == upper).toBe(true)
        end)

        it(`keeps '{upper}' unchanged`, function()
            expect(character.toUppercase(upper) == upper).toBe(true)
        end)
    end

    for _, case in UNCHANGED_CASES do
        it(`keeps '{case}' unchanged`, function()
            expect(character.toUppercase(case)).toEqual(case)
        end)
    end
end)


local function binarySeachBy<T>(array: { T }, fn: (value: T) -> number): { ok: boolean, index: number }
    local size = #array
    if size == 0 then
        return { ok = false, index = 1 }
    end

    local base = 1

    while size > 1 do
        local half = math.floor(size / 2)
        local mid = base + half

        local cmp = fn(array[mid])

        if cmp <= 0 then
            base = mid
        end

        size = size - half
    end

    local cmp = fn(array[base])

    if cmp == 0 then
        return { ok = true, index = base }
    else
        local result = base + (if cmp < 0 then 1 else 0)
        return { ok = false, index = result }
    end
end

local function binarySeachByKey<T>(
    array: { T },
    key: number,
    keyFn: (value: T) -> number
): { ok: boolean, index: number }
    return binarySeachBy(array, function(value)
        return keyFn(value) - key
    end)
end

return {
    binarySeachBy = binarySeachBy,
    binarySeachByKey = binarySeachByKey,
}


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

export type Module = {
    Get: () -> boolean,
    Set: (value: boolean) -> (),
    Flip: () -> (),
    SetFalse: () -> (),
    SetTrue: () -> ()
}

--[[
Type for the Cereberal Bool Default Module
]] -- Skip out on Name, Value, New, GetSafe, Set
export type BoolModule = __Types.Attribute<Module, boolean>

local Bool = {}
Bool.__index = Bool
Bool.Name = nil
Bool.__subscribed = false

function Bool.New(value: boolean)
    if value == nil then
        error(`[{script.Name}]: Must have value`)
    elseif type(value) ~= "boolean" then
        error(`[{script.Name}]: value be of type boolean`)
    end

    local self

    self = {
        Value = value
    }
    setmetatable(self, Bool)

    return self
end

function Bool:Get(): boolean
    return self.Value
end

function Bool:GetSafe(): boolean
    return self:Get()
end

function Bool:Set(value: boolean)
    if value == nil then
        error(`[{script.Name}]: Must have name`)
    elseif type(value) ~= "boolean" then
        error(`[{script.Name}]: value be of type boolean`)
    end

    self.value = value
end

function Bool:Flip()
    self.value = not self.value
end

function Bool:SetFalse()
    self.Value = false
end

function Bool:SetTrue()
    self.Value = true
end

return Bool


--!strict

-- Constants --
-- Do we need integer overflow protection?

--Dependencies --
local __Types = require(script.Parent.Parent.__Types)

-- Private --
type Module = {
	Get: () -> number,
	Increment: (number) -> number,
	Decrement: (number) -> number,
	Zero: () -> (),
	Multiply: (number) -> (),
	Divide: (number) -> (),
}

-- Public --
--[[
Type for the Cereberal Bool Default Module
]]
-- Skip out on Name, Value, New, GetSafe, Set
export type NumberModule = __Types.Attribute<Module, number>

local Number = {}
Number.__index = Number
Number.Name = nil
Number.__subscribed = false

function Number.New(value: number)
	if value == nil then
		error(`[{script.Name}]: Must have value`)
	elseif type(value) ~= "number" then
		error(`[{script.Name}]: value be of type number`)
	end

	local self

	self = {
		Value = value,
	}
	setmetatable(self, Number)

	return self
end

function Number:Get(): number
	return self.Value
end

function Number:HasEnough(amt: number): boolean
	return self:Get() >= amt
end

function Number:GetSafe(): number
	return self:Get()
end

function Number:Set(value: number)
	if value == nil then
		error(`[{script.Name}]: Must have name`)
	elseif type(value) ~= "number" then
		error(`[{script.Name}]: value be of type number`)
	end

	self.Value = value
end

function Number:Increment(value: number)
	if value == nil then
		error(`[{script.Name}]: Must have value`)
	elseif type(value) ~= "number" then
		error(`[{script.Name}]: value be of type number`)
	end

	local _v: number = self.Value
	self.Value = _v + value
end

function Number:Decrement(value: number)
	if value == nil then
		error(`[{script.Name}]: Must have value`)
	elseif type(value) ~= "number" then
		error(`[{script.Name}]: value be of type number`)
	end

	local _v: number = self.Value
	self.Value = _v - value
end

function Number:Zero(value: number)
	if value == nil then
		error(`[{script.Name}]: Must have value`)
	elseif type(value) ~= "number" then
		error(`[{script.Name}]: value be of type number`)
	end

	self.Value = 0
end

function Number:Multiply(mult: number)
	if mult == nil then
		error(`[{script.Name}]: Must have value`)
	elseif type(mult) ~= "number" then
		error(`[{script.Name}]: value be of type number`)
	end

	local _v: number = self.Value
	self.Value = _v * mult
end

function Number:Divide(div: number)
	if div == nil then
		error(`[{script.Name}]: Must have value`)
	elseif type(div) ~= "number" then
		error(`[{script.Name}]: value be of type number`)
	end

	if div == 0 then
		warn(`[{script.Name}]: div cannot be 0`)
		return
	end

	local _v: number = self.Value
	self.Value = _v / div
end

return Number


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

type Module = {
	Get: () -> string,
	Set: (value: string) -> string,
	GetLen: () -> number,
	Lower: () -> string,
	Upper: () -> string,
	Concat: (value: string) -> string
}

export type StringModule = __Types.Attribute<Module, string>

local String = {}
String.__index = String
String.Name = nil
String.__subscribed = false

function String.New(value: string)
	if value == nil then
		error(`[{script.Name}]: Must have value`)
	elseif type(value) ~= "string" then
		error(`[{script.Name}]: value be of type string`)
	end

	local self

	self = {
		Value = value,
	}
	setmetatable(self, string)

	return self
end

function String:Get(): string
	return self.value
end

function String:Set(value: string): string
	self.value = value
	return value
end

function String:GetLen(): number
	return string.len(self.value)
end

function String:Upper(): string
	self.value = string.upper(self.value)
	return self.value
end

function String:Lower(): string
	self.value = string.lower(self.value)
	return self.value
end

function String:Concat(value: string): string
	self.value = self.value .. value
	return self.value
end

return String


-- !optimize 2
-- !strict

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

export type Module = {
    Get: () -> table,
    Set: () -> table,
    Update:(callback: (oldvalue: {}) -> {}) -> table
}

export type TableModule = __Types.Attribute<Module, table>

local Table = {}
Table.__index = Table
Table.Name = nil
Table.__subscribed = false

function Table.New(value: table): TableModule
    if value == nil then
        error(`[{script.name}]: Must have value`)
    elseif type(value) ~= "table" then
        error(`[{script.name}]: value must be of type table`)
    end

    warn("[DefaultModules.Table]: UNTESTED")

    local self

    self = {
        Value = value
    }
    setmetatable(self, Table)

    return self
end

function Table:Get(): table
    return self.Value
end

-- TODO add GetSafe method

function Table:Set(value: table): table
    self.Value = value
    return self.Value
end

function Table:Update(callback: (oldvalue: {}) -> table): {}
    local success: boolean
    local newValue: {}

    success, newValue = pcall(function()
        return callback(self.Value)
    end)

    if success == false then
        error(`[{script.Name}]: callback function failed`)
    end

    return newValue
end

return Table


local Bool = require(script.Bool)
local Number = require(script.Number)
local String = require(script.String)
local Table = require(script.Table)

export type BoolModule = Bool.BoolModule
export type NumberModule = Number.NumberModule
export type StringModule = String.StringModule
export type TableModule = Table.TableModule

return {
	Bool = Bool,
	Number = Number,
	String = String,
	Table = Table
}


--!strict

-- Dependencies --
local Types = require(script.Parent.__Types)
local DataStore = require(script.Parent.__DataStore)

-- Constants --

-- Types --
export type Agent = Types.Agent & {
	player: Player,
	ds_data: {[string]: Types.JSONAcceptable},
	AddAttribute: (self: Agent, class: any, initialValue: any, name: string) -> Types.Attribute<any, any>,
	GetAttribute: (self: Agent, className: string) -> Types.Attribute<any, any>,
	UpdateTable: (self: Agent, attribute: string, callback: (any) -> any) -> any,
	AddPlayerFolder: (self: Agent, folderName: string) -> Instance
}

-- What the Player data looks like in cerebral
export type PlayingTemplate = {
	playerId: number, -- player.UserId
	attributes: { Types.Attribute<any, any> }, -- All the attributes, is converted to data at save
	DataId: number, -- Data id; increments after each safe
	metaData: {
		sessions: number, -- total logins
		lastUpdate: number, -- last update
		lastLogin: number, -- last login
		profileCreationTime: number, -- when the profile was created
	}
}

-- What the Player data looks like in DataStore
export type StoringTemplate = {
	playerId: number, -- player.UserId,
	data: { [string]: Types.JSONAcceptable }, -- All the data from each attributes [.Name] = .GetSafe()
	DataId: number, -- Data id; increments after each save.
	metaData: {
		sessions: number, -- total logins; wont be the same of DataId
		lastUpdate: number, -- last update
		lastLogin: number, -- last login
		profileCreationTime: number, -- when the profile was created
	}
}

-- Private --

-- For new players with no data
local function newPlayer(player: Player)
	if typeof(player) ~= "Instance" then
		error(`[{script.Name}]: Must have player`)
	end

	return {
		ds_data = {
			data = {}
		}
	}
end

local function getAgentData(player: Player)
	local data: {}

	data = DataStore:GetPlayerData(player)
end

-- Public --
---- Agent Class ----

local Agent = {}
Agent.__index = Agent

-- Creates new Agent and adds to Cerebral control
-- TODO:
-- A new Agent must get the data from the datastore and return it in
-- self.data
function Agent.New(player: Player): Agent
	if typeof(player) ~= "Instance" then
		error(`[{script.Name}]: Must have player`)
	end

	local self
	local data

	data = getAgentData(player) or newPlayer(player)

	self = {
		player = player,
		attributes = {},
		ds_data = data
	}
	setmetatable(self, Agent)

	return (self :: any) :: Agent
end

-- Adds an attribute to a Agent
function Agent:__AddAttribute<A, B>(class: Types.Attribute<A, B>, initValue: B, name: string): Types.Subscribed<A, B>
	if name == nil then
		error(`[{script.Name}]: Must have a name for class`)
	elseif type(name) ~= "string" then
		error(`[{script.Name}]: name must be of type string`)
	elseif initValue == nil then
		error(`[{script.Name}]: Must have initValue`)
	end

	self.attributes[name] = class.New(initValue)
	return self.attributes[name] :: Types.Subscribed<typeof(class), typeof(initValue)>
end

--[[
Returns an attribute of an Agent

WARNING: Can fail if attribute does not exist
]]
function Agent:GetAttribute<A>(attribute: string)
	if attribute == nil then
		error(`[{script.Name}]: Must have attribute`)
	elseif type(attribute) ~= "string" then
		error(`[{script.Name}]: attribute must be of type string`)
	end

	local class: A

	class = self.attributes[attribute]

	if class == nil then
		error(`[{script.Name}]: attribute [{attribute}] does not exist in {self.player.Name}`)
	end

	return class
end

--[[
DEVELOPER NOTE: What do I want this to look like in the DataStore?
{
	player_id: number,
	server_id: number,
	data_id: number,
	data: {
		...
	},
	meta_data: {
		session_count: number,
	}
}

And then serverside...
Seems silly tbh, could we not just convert everything to attributes?
{
	player: Player,
	attributes: ...,
	ds_data: {
		data: { [string]: JSON }
	}
}
]]
function Agent:SaveToDataStore()
	local safe_data

	-- Get safe data from current attributes
	safe_data = self:GetSafeData()

	for name, data in safe_data do
		if self.ds_data.data[name] then
			self.ds_data.data[name] = data
		end
	end

	-- returns data with updated data_id upon success
	self.ds_data.data = DataStore:UpdatePlayerData(self.player, safe_data)
end

--[[
UNTESTED: If the agent has an attribute that is a table (say for inventory),
this allows the user to update that table like they would a SharedTable.

DEVELOPER NOTE: Currently, tables are not an accepted value type. This is a priority.
]] -- Update function similar to how SharedTables updated
function Agent:UpdateTable<A>(attribute: string, callback: (oldValue: A & {}) -> A & {}): A & {}
	local class
	local value: A & {}
	local success: boolean
	local newValue: A & {}

	class = self:GetAttribute(attribute)
	value = class:Get()

	-- Protect the user
	success, newValue = pcall(function()
		return callback(value)
	end)

	if success == false then
		error(`[{script.Name}]: callback function failed`)
	end

	return newValue
end

-- Consider making private?
-- Adds folder to Agent's player
-- should we consider using attributes instead?
function Agent:AddPlayerDataFolder(folderName: string): Instance
	local folder = Instance.new("Folder")
	folder.Name = folderName
	folder.Parent = self.player
	return folder
end

function Agent:GetSafeData(): {}
	local safeData: {}

	safeData = {}

	for _, attribute in self.attributes do
		local data: Types.JSONAcceptable
		local name: string

		name = attribute.Name
		if name == nil then
			error(`[{script.Name}]: attribute does not have name!`)
		end

		if typeof(attribute["GetSafe"]) ~= "function" then
			error(`[{script.Name}]: attribute [{name}] does not have GetSafe function!`)
		end
		data = attribute:GetSafe()

		-- What happens if data is nil?
		safeData[name] = data
	end

	return safeData
end

return Agent


--!strict
--!optimize 2

-- Dependencies --
local DataStoreService = game:GetService("DataStoreService")

local Types = require(script.Parent.__Types)

-- Constants --
local MAX_ATTEMPTS = 10
local RETRY_TIME = 0.2
local DATA_STORE_NAME = "Cerebral_DB"

-- Private --
local function waitOnDataStore(ds_service: DataStoreService, enum: typeof(Enum))
    local attempts: number
    local success: boolean

    attempts = 0
    success = false

    repeat
        while ds_service:GetRequestBudgetForRequestType(enum) < 1 do
            warn("Data store budget depleted. Waiting")
            task.wait(attempts * RETRY_TIME)
        end

        success = true
    until success or attempts == MAX_ATTEMPTS

    if not success then
        error(`[{script.Name}]: DataStore budget completed depleted`)
    end

    return
end

-- Public --

---- DataStore Class ----

local DataStore = {}
DataStore.__index = DataStore

-- DataStore Constructor
function DataStore.New<A>(): Types.DataStore<A>
    local self

    self = {
        DataStoreName = DATA_STORE_NAME
    }
    setmetatable(self, DataStore)

    return self
end

function DataStore:GetPlayerData<A>(player: Player, _kickPlayerOnFail: boolean): A
    local ds: DataStoreService
    local success: boolean
    local data: {}
    local kickPlayerOnFail: boolean

    ds = DataStoreService:GetDataStore(self.DataStoreName)
    kickPlayerOnFail = if _kickPlayerOnFail == nil then true else _kickPlayerOnFail

    success, data = pcall(function()
        waitOnDataStore(ds, Enum.DataStoreRequestType.GetAsync) -- yeilds
        return ds:GetAsync(player.UserId)
    end)

    if success then
        return data
    end

    -- If GetPlayerData reaches this point, kick the player
    if kickPlayerOnFail then
        player:Kick("Failed to get user data. Please try again later.")
    end

    return {}
end

function DataStore:UpdatePlayerData<A>(
    player: Player,
    data: A
): A
   local ds: DataStoreService
   local success: boolean
   local err: string

    ds = DataStoreService:GetDataStore(self.DataStoreName)
   success, err = pcall(function()
        waitOnDataStore(ds, Enum.DataStoreRequestType.UpdateAsync) -- yields
        if data then
            ds:UpdateAsync(player.UserId, function(oldValue)
                if data.DataId == oldValue.DataId then
                    data.DataId += 1
                    return data
                else
                    return nil
                end
            end)
        end
   end)

    if success then
        return data
    end

    warn(`[{script.Name}]: Could not update player [{player.Name}] data.`)
    warn(data)

    if err then
        warn(`[{script.Name}] ERROR: {err}`)
    end

    return {}
end

local out = DataStore.New()
return out


--[[
__Packages is not really a package directory. It is no like node_modules
to Node.js. Instead, this directory harbors variours code snippets I come
across in the Luau community (proper license of course).
]]

local Signal = require(script.Signal)

local Packages = {
    Signal = Signal -- Handles Luau event signals
}

return Packages


local DataStoreService = game:GetService("DataStoreService")
export type JSONAcceptable = { JSONAcceptable } | { [string]: JSONAcceptable } | number | string | boolean | buffer

export type Attribute<A, B> = typeof(setmetatable({}, {})) & {
	Name: string,
	Value: B,
	__Subscribed: boolean,
	New: (...any) -> A,
	GetSafe: (self: A) -> JSONAcceptable,
	Set: (self: A, ...any) -> (),
} & A

export type Subscribed<A, B> = Attribute<A, B> & {
	Fire: (...any) -> (),
	Connect: (...any) -> (),
	__signal: {
		Connect: (self: any, listener: (JSONAcceptable) -> ()) -> any,
		Fire: (self: any, ...any) -> (),
	},
} & A

export type Agent = {
	player: Player,
	attributes: { [string]: Attribute<{}, JSONAcceptable> },
	__AddAttribute: (self: Agent, class: Attribute<any, any>, initValue: any, name: string) -> Subscribed<any, any>,
	GetAttribute: (self: Agent, attribute: string) -> Subscribed<any, any>,
	UpdateTable: (self: Agent, attribute: string, callback: (oldValue: {}) -> {}) -> ()
}

export type Cerebral = {
	Agents: { [string]: Agent },
	__New: () -> Cerebral,
	AddAgent: (self: Cerebral, agent: Agent) -> (),
	RemoveAgent: (self: Cerebral, agent: Player) -> (),
	AutoSave: (typeof(coroutine)) -> ()
}

export type DataStore<A> = {
	DataStoreName: string,

	New: () -> DataStore<A>,
	GetPlayerData: (self: DataStore<A>, player: Player, _kickPlayerOnFail: boolean) -> A,
	UpdatePlayerData: (self: DataStore<A>, player: Player, DataStoreService: A) -> A,
}

export type AvailableDefaultModules = "boolean" | "string" | "number"

return {}


--!optimize 2
--!strict

-- Constants --
local AGENT_FOLDER = "__Data"
local AUTO_SAVE_PERIOD = 60

--- Constant Type ----
type Constants = "AGENT_FOLDER" | "AUTO_SAVE_PERIOD"

-- Dependencies --
local DefaultModules = require(script.DefaultModules)
local Signal = require(script.__Packages.Signal)
local Types = require(script.__Types)
local Agent = require(script.__Agent)

-- Types --
export type JSONAcceptable = Types.JSONAcceptable
export type Attribute<A, B> = Types.Attribute<A, B>
export type Subscribed<A, B> = Types.Subscribed<A, B>
export type Agent = Agent.Agent
export type AvailableDefaultModules = Types.AvailableDefaultModules
export type Cerebral = Types.Cerebral

type ClassName = string

-- Private --

-- Public --

---- Cerebral Class ----
local Cerebral = {}
Cerebral.__index = Cerebral

--[[
Private.

Creates the Cerebral object.
]]
function Cerebral.__New(): Types.Cerebral
	local self

	self = {
		Agents = {},
		__attributes = {},
		__sessionAttributes = {},
	}

	setmetatable(self, Cerebral)

	-- Auto save feature.
	self.AutoSave = task.spawn(function()
		while true do
			task.wait(AUTO_SAVE_PERIOD)
			self:__SaveAll()
		end
	end)

	return self
end

--[[
Sets global values used throughout Cerebral.
]]
function Cerebral:SetGlobal(constantString: Constants, constantValue: JSONAcceptable)
	if constantString == nil then
		warn(`[{script.Name}]: Must have constantString`)
		return
	end

	if type(constantString) ~= "string" then
		warn(`[{script.Name}]: constantString must be of type string`)
		return
	end

	if constantValue == nil then
		warn(`[{script.Name}]: Must have constantValue`)
		return
	end

	if constantString == "AUTO_SAVE_PERIOD" then
		AUTO_SAVE_PERIOD = constantValue
	elseif constantString ==  "AGENT_FOLDER" then
		AGENT_FOLDER = constantValue
	end
end

--[[
Defines the attributes of every joining player.

PRIORITY IMPLEMENTATION: The ClassName must match that found in the DataStore.

Example:
```
local Cerebral = Cerebral.New()
Cerebral:DefineAttributes({
	money = Cerebral:Create("number"),
	xp = Cerebral:Create("number"),
	is_admin = Cerebral:Create("boolean"),
})
```
]]
function Cerebral:DefineAgentAttributes(attributes: { [ClassName]: Attribute<any, JSONAcceptable> })
	for name, class in attributes do
		self.__attributes[name] = class
	end
end

--[[
Defines the session data for every joining player. Session data does not persist between sessions.

Example:
```luau
local Cerebral = require(path to cerebral)
Cerebral:DefineAgentSessionData({
	points = Cerebral:Create("number")
})
```
]]
function Cerebral:DefineAgentSessionData(attributes: { [ClassName]: Attribute<any, JSONAcceptable> })
	for name, class in attributes do
		self.__sessionAttributes[name] = class
	end
end

--[[
Private.

Example of incoming playerData:
```
data = {
	"money": 42,
	"xp": 9999,
	"is_admin": false
}
```
]]
function Cerebral:__SetAttribute<A, B>(
	agent: Agent,
	data: JSONAcceptable & B,
	attribute: Attribute<A, B>,
	name: string
): Subscribed<A, B>
	-- Add subscription methods
	local inter = self:__Subscribe(attribute)

	-- Add the updated attribute to the Agent
	local att = agent:__AddAttribute(inter, data, name)

	-- Return updated attribute for further use
	return att
end

-- Adds a new agent to Cerebral
function Cerebral:NewAgent(player: Player): Agent
	local agent: Agent
	local agentFolder: Instance
	local _att: Subscribed<any, any>
	local dataType: AvailableDefaultModules
	local storedData: any -- BoolValue | NumberValue | StringValue

	agent = Agent.New(player)
	agentFolder = agent:AddPlayerFolder(AGENT_FOLDER)

	self.Agents[player.UserId] = agent

	-- UNTESTED
	for name, data in agent.data do
		if self.__attributes[name] then
			_att = Cerebral:__SetAttribute(agent, data, self.__attributes[name], name)
			dataType = typeof(_att:GetSafe()) :: AvailableDefaultModules

			if dataType == "boolean" then
				storedData = Instance.new("BoolValue")
			elseif dataType == "number" then
				storedData = Instance.new("NumberValue")
			elseif dataType == "string" then
				storedData = Instance.new("StringValue")
			else
				-- boolean, number, and string are the only available data types
				-- supported in subscriptions
				continue
			end

			storedData.Value = data
			storedData.Parent = agentFolder

			-- Automatically updates values stored under Players[player] whenver
			-- the value of _att changes
			_att:Connect(function(value: typeof(data))
				storedData.Value = value
			end)
		else
			warn(`[{script.Name}]: MISSING DATA. Please add attribute for {name}`)
		end
	end

	return agent
end

-- Removes an agent from Cerebral
-- TODO: RemoveAgent must also go through the process of saving the users data
function Cerebral:RemoveAgent(player: Player)
	warn(`[{script.Name}]: RemoveAgent UNTESTED. User data may be lost.`)
	self.Agents[player.UserId]:SaveToDataStore();
	self.Agents[player.UserId] = nil
end

-- Gets an agent from Cerebral
function Cerebral:GetAgent(player: Player)
	return self.Agents[player.UserId]
end

-- Subscribes to an attribute
function Cerebral:__Subscribe<A, B>(class: A): Subscribed<A, B>
	--[[
    Maybe every single class has a `state` value. This value can be anything except a table.
    Every time state changes, we trigger a fire event for that player.
    ]]

	-- selene: allow(shadowing)
	local _class = (class :: any) :: Subscribed<A, B>

	local _s = Signal.New()
	_class.__signal = _s

	-- selene: allow(shadowing)
	function _class:Fire(...)
		_class.__signal:Fire(...)
	end

	-- selene: allow(shadowing)
	function _class:Connect(listener: (...any) -> ())
		local connection = _class.__signal:Connect(listener)
		return connection
	end

	for key, method in pairs(_class) do
		if typeof(method) == "function" and key ~= "Get" and key ~= "New" and key ~= "GetSafe" then
			(_class :: any)[key] = function(_self, ...)
				local oldValue = _self and _self.Value
				local _r = method(_self, ...)
				local newValue = _self and _self.Value

				if oldValue ~= newValue then
					_class:Fire(newValue)
				end

				return _r
			end
		end
	end

	return _class
end

-- DEVELOPER NOTE: Don't love the `any` return value
function Cerebral:Create(type: AvailableDefaultModules): any
	if type == nil then
		error(`[{script.Name}]: Must have type`)
	elseif type ~= "boolean" and type ~= "string" and type ~= "number" then
		error(`[{script.Name}]: No availbe default module for {type}`)
	end

	local _r: any

	if type == "boolean" then
		_r = DefaultModules.Bool
	elseif type == "number" then
		_r = DefaultModules.Number
	elseif type == "string" then
		_r = DefaultModules.String
	elseif type == "table" then
		_r = DefaultModules.Table
	end

	return _r
end

--[[
Saves every agents data to data store.
]]
function Cerebral:__SaveAll()
	for agent in self.Agents do
		agent:SaveToDataStore()
	end
end

--[[
Allows only one Cerebral to be passed around the application.
Importing Cerebral anywhere in the game, will always point towards
the same Cerebral Object.
]]
local out = Cerebral.__New()
return out


-------------------------------------------------------------------------------------------------------------------
---- understading of errors of infinte yield, nill, timeout -------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------

--[[
print("Script Started")
local Players  = game:GetService("Players")

local player = Players.LocalPlayer

print("Got player")
local character = player.Character or player.CharacterAdded:Wait()

if not character then
    warn("Character not found")
    return
end

print("Got character")

local humanoid = character:WaitForChild("Humanoid", 10)

if not humanoid then
    warn("Humanoid not found ")
    return
end

print("Got humanoid")

print(humanoid.Health)

-- understood "inifinte yied possible" error means something luau didnt find what we wanted to find on that location

-- Players.gam_devX.PlayerScripts.Client:7: attempt to index nil with 'WaitForChild'
-- understood this eroor bcz its not loading the character at start so we need to add
-- characterAdded:Wait()

-- understood timeout without timeout if humanoid doesnt load it wil search for ever
-- and script will block forever or roblox can show the infinite error
-- thats why we need to add timeout local humanoid = character:WaitForChild("Humanoid", 10)
-- it will serach humanoid for 10 seconds
-- if humanoid found within 10 seconds return child
-- else warn

]]

-------------------------------------------------------------------------------------------------------------------
---- roblox Humanoid touched event findfirstancestorofclass jumppower jumpheight ----------------------------------
-------------------------------------------------------------------------------------------------------------------

-- method 1: longer
--[[
local part = workspace:WaitForChild("bridgeTrigger")
part.Anchored = true

part.Touched:Connect(function(hitPart)

    local character = hitPart.Parent
    local player = game.Players:GetPlayerFromCharacter(character)

    if not player then return end
    local humanoid = character:WaitForChild("Humanoid")

    humanoid.JumpPower = 0
    humanoid.JumpHeight = 0

end
)
]]

--[[
    Workspace
 └── Character (Model)   ← THIS is what you want
      ├── Humanoid
      ├── HumanoidRootPart
      ├── Head
      ├── LeftHand / RightHand
      ├── Accessories
      │    └── Pal Hair (Accessory)
      │         └── Handle (BasePart)  ← this often touches things
      ├── Shirt / Pants
      └── Other body parts
]]

-- sometimes on touched function handle can be touched too with that part
-- so we can do hiPart.Parent ~= character but equals to Pal Hair
-- so we need to use ( not findfirstancestor bcz this is random for every players user id)
-- we need to use findfirstancestorofclass which is a model and then model:WaitForChild("Humanoid")
-- thus we get humanoid

--[[
local part = workspace:WaitForChild("bridgeTrigger")
part.Anchored = true

part.Touched:Connect(function(hitPart)
    local character = hitPart:FindFirstAncestorOfClass("Model")
    if not character then return end
    local humanoid = character:WaitForChild("Humanoid")
    humanoid.JumpPower = 0
    humanoid.JumpHeight = 0

end
)
]]


--[[
local table1 = {1, 2, 3}
local table2 = {4, 5, 6}

local metatable = {

    __add = function(table1, table2)
        local elements = {}

        for i in ipairs(table1) do
            elements[i] = table1[i] + table2[i]
        end

        return elements
    end
}

setmetatable(table1, metatable)
setmetatable(table2, metatable)

local sum = table1 + table2
print(sum)
]]

--[[
local metaTable = {
    __index = function(table, key)

        return "Key is missing: " .. key
    end
}

local myTable = setmetatable({}, metaTable)

print(myTable.missing)
]]

-- __index mathod use

-- level 1 basic fallback
--[[
defaults = {health = 100, mana = 50}
player = {}

setmetatable(player, {__index = defaults})

print(player.health)
print(player.mana)
]]

-- level 2 -- override parent value

-- defaults = {health = 100, mana = 50}
-- player = {health = 80}

-- this time lua will print health 80 bcz it never ask for metatable bcz the key already exists

-- local defaults = {health = 100}
-- local player = {health = nil}

-- this time key health exists but setting it to nil in luau it means i am just setting health key dne
-- health = nil means player = {} so it gonna print parent value

-- local defaults = {health = 100}
-- local player = {}
-- player.health = 50

-- this time it gonna print 50 bcz ket exists so it never gonna ask metatable

--[[
    so if key dne only then lua will ask if metatble exists
                                            if metatable exists search for __index if it exists
                                                if __index exists use it
]]

-- __newindex method
--[[
local myTable = {}
local metaTable = {
    __newindex = function(table, key, value)
        if type(value) ~= "number" then
            print("Can't add Key " .. key .. " with value: " .. value)
            return
        end

        rawset(table, key, value)
        print("Value added: " .. tostring(value)) -- converting number to str
    end
}

setmetatable(myTable, metaTable)

myTable.newKey = "newValue"
print(myTable.newKey)

myTable.newKey = 60
print(myTable.newKey)
]]

-- __newindex just for checking ok i am trying to add something but it will not actualy add that
-- to actually add that we need rawset

-- creating npc class
--[[
local NPC = {}
NPC.__index = NPC

function NPC:new(name, age, health, position, dialogue, quests)

    local self = setmetatable({}, NPC)

    self.name = name
    self.age = age
    self.health = health
    self.position = position
    self.dialogue = dialogue
    self.quests = quests
    self.state = "Idle"
    return self

end

-- Class methods / functions
function NPC:Speak(dialogueIndex)
    -- code block
    return self.dialogue[dialogueIndex]
end

function NPC:AssignQuest(questIndex)
    --code block
    return self.quests[questIndex]
end

function NPC:TakeDamage(damage)
    self.health -= damage
end

function NPC:Move(targetPosition)
    -- code block
    self.Position = targetPosition
end

function NPC:UpdateState(newState)
    self.state = newState
end

local guardNpc = NPC:new("Guard", 29, 100,
    {x = 0, y = 0},
    {"Hello traveler", "Stay safe"},
    {"Find the sword", "Defeat the goblin"}
)

print(guardNpc:speak(1))
]]


--[[local function calaculateDamege(baseDamage, multiplier)
    return baseDamage * multiplier
end

print(calaculateDamege(10, 2)) -- Output: 20
--[[for i = 1, 20, 1 do
    if i % 3 == 0 and i % 5 == 0 then
        print("FizzBuzz")
    elseif i % 5 ==0 then
        print("Buzz")
    elseif i % 3 == 0 then
        print("Fizz")
    else
        print(i)
    end
end]]

--[[
local player = {
    name = "Arindam",
    level = 100,
    gold = 250
}

print("Player " .. player.name .. " has " .. player.gold .. " gold") ]]
--[[
local inventory = {"Sword", "Shield", "Potion", "Helmet"}

for i, item in inventory do
    print("Item " .. i .. ": ".. item)
end]]

--[[
local enemies = {"Zombie", "Goblin", "Skeleton"}

for _, enemy in pairs(enemies) do

    if enemy ~= "Goblin" then
        print(enemy)
    else
        print("Dragon")
    end
end
]]


--[[
-- was understadning modulescripts

local module = require(game.ReplicatedStorage.Shared.moduleScript)

val = module.add(5, 3)

print(val)

]]


--[[
local module = {}

-- do something

function module.add(a, b)
	return a + b
end

return module

]]


--!optimize 2
--!native

local sRGB = {}

local function fromLinear(channel: number): number
	if channel < 0.0031308 then
		return channel * 12.92
	else
		return channel ^ (1 / 2.4) * 1.055 - 0.055
	end
end

local function toLinear(channel: number): number
	if channel < 0.04045 then
		return channel / 12.92
	else
		return ((channel + 0.055) / 1.055) ^ 2.4
	end
end

--- Converts linear sRGB space to sRGB space
function sRGB.fromLinear(c: Color3): Color3
	return Color3.new(fromLinear(c.R), fromLinear(c.G), fromLinear(c.B))
end

--- Converts sRGB space to linear sRGB space
function sRGB.toLinear(c: Color3): Color3
	return Color3.new(toLinear(c.R), toLinear(c.G), toLinear(c.B))
end

local Oklab = {}

--- Converts a Color3 in linear sRGB space to a Vector3 in Oklab space
function Oklab.fromLinear(c: Color3): Vector3
	local l = c.R * 0.4122214708 + c.G * 0.5363325363 + c.B * 0.0514459929
	local m = c.R * 0.2119034982 + c.G * 0.6806995451 + c.B * 0.1073969566
	local s = c.R * 0.0883024619 + c.G * 0.2817188376 + c.B * 0.6299787005

	local l_ = l ^ (1 / 3)
	local m_ = m ^ (1 / 3)
	local s_ = s ^ (1 / 3)

	return Vector3.new(
		l_ * 0.2104542553 + m_ * 0.7936177850 - s_ * 0.0040720468,
		l_ * 1.9779984951 - m_ * 2.4285922050 + s_ * 0.4505937099,
		l_ * 0.0259040371 + m_ * 0.7827717662 - s_ * 0.8086757660
	)
end

--- Converts a Vector3 in Oklab space to a Color3 in linear sRGB space, clamps channels by default
function Oklab.toLinear(c: Vector3, clamped: boolean?): Color3
	local l_ = c.X + 0.3963377774 * c.Y + 0.2158037573 * c.Z
	local m_ = c.X - 0.1055613458 * c.Y - 0.0638541728 * c.Z
	local s_ = c.X - 0.0894841775 * c.Y - 1.2914855480 * c.Z

	local l = l_ ^ 3
	local m = m_ ^ 3
	local s = s_ ^ 3

	local r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s
	local g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s
	local b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s

	if clamped ~= false then
		r = math.clamp(r, 0, 1)
		g = math.clamp(g, 0, 1)
		b = math.clamp(b, 0, 1)
	end

	return Color3.new(r, g, b)
end

--- Converts a Vector3 in HCL space to a Vector3 in Oklab space
function Oklab.fromHCL(hcl: Vector3): Vector3
	local h = hcl.X * 2 * math.pi
	local c = hcl.Y

	local l = hcl.Z
	local a = c * math.cos(h)
	local b = c * math.sin(h)

	return Vector3.new(l, a, b)
end

--- Converts a Vector3 in Oklab space to a Vector3 in HCL space
function Oklab.toHCL(lab: Vector3): Vector3
	local h = math.atan2(lab.Z, lab.Y) / (2 * math.pi)
	local c = math.sqrt(lab.Y ^ 2 + lab.Z ^ 2)
	local l = lab.X

	return Vector3.new(h, c, l)
end

--- Converts a Color3 in sRGB space to a Vector3 in Oklab space
function Oklab.fromRGB(rgb: Color3): Vector3
	return Oklab.fromLinear(sRGB.toLinear(rgb))
end

--- Converts a Vector3 in Oklab space to a Color3 in sRGB space
function Oklab.toRGB(lab: Vector3, clamped: boolean?): Color3
	return sRGB.fromLinear(Oklab.toLinear(lab, clamped))
end

local Color = {
	Oklab = Oklab,
	sRGB = sRGB,
}

return Color


--!optimize 2
--!native
--!strict
--!nolint LocalShadow

local Package = script.Parent
local Type = require(Package.Type)
local Color = require(Package.Color)
local Oklab = Color.Oklab

local DataType = {}

--- Packs an array of numbers into a given animatable data type
function DataType.pack(numbers: { number }, typeString: string): Type.Animatable?
	if typeString == "number" then
		return numbers[1]
	elseif typeString == "CFrame" then
		return CFrame.new(numbers[1], numbers[2], numbers[3])
			* CFrame.fromAxisAngle(Vector3.new(numbers[4], numbers[5], numbers[6]).Unit, numbers[7])
	elseif typeString == "Color3" then
		return Oklab.toRGB(Vector3.new(numbers[1], numbers[2], numbers[3]), false)
	elseif typeString == "ColorSequenceKeypoint" then
		return ColorSequenceKeypoint.new(
			numbers[4],
			Oklab.toRGB(Vector3.new(numbers[1], numbers[2], numbers[3]), false)
		)
	elseif typeString == "DateTime" then
		return DateTime.fromUnixTimestampMillis(numbers[1])
	elseif typeString == "NumberRange" then
		return NumberRange.new(numbers[1], numbers[2])
	elseif typeString == "NumberSequenceKeypoint" then
		return NumberSequenceKeypoint.new(numbers[2], numbers[1], numbers[3])
	elseif typeString == "PhysicalProperties" then
		return PhysicalProperties.new(numbers[1], numbers[2], numbers[3], numbers[4], numbers[5])
	elseif typeString == "Ray" then
		return Ray.new(Vector3.new(numbers[1], numbers[2], numbers[3]), Vector3.new(numbers[4], numbers[5], numbers[6]))
	elseif typeString == "Rect" then
		return Rect.new(numbers[1], numbers[2], numbers[3], numbers[4])
	elseif typeString == "Region3" then
		-- FUTURE: support rotated Region3s if/when they become constructable
		local position = Vector3.new(numbers[1], numbers[2], numbers[3])
		local halfSize = Vector3.new(numbers[4] / 2, numbers[5] / 2, numbers[6] / 2)
		return Region3.new(position - halfSize, position + halfSize)
	elseif typeString == "Region3int16" then
		return Region3int16.new(
			Vector3int16.new(math.round(numbers[1]), math.round(numbers[2]), math.round(numbers[3])),
			Vector3int16.new(math.round(numbers[4]), math.round(numbers[5]), math.round(numbers[6]))
		)
	elseif typeString == "UDim" then
		return UDim.new(numbers[1], math.round(numbers[2]))
	elseif typeString == "UDim2" then
		return UDim2.new(numbers[1], math.round(numbers[2]), numbers[3], math.round(numbers[4]))
	elseif typeString == "Vector2" then
		return Vector2.new(numbers[1], numbers[2])
	elseif typeString == "Vector2int16" then
		return Vector2int16.new(math.round(numbers[1]), math.round(numbers[2]))
	elseif typeString == "Vector3" then
		return Vector3.new(numbers[1], numbers[2], numbers[3])
	elseif typeString == "Vector3int16" then
		return Vector3int16.new(math.round(numbers[1]), math.round(numbers[2]), math.round(numbers[3]))
	else
		return nil
	end
end

--- Unpacks an animatable type into an array of numbers
function DataType.unpack(value: unknown, typeString: string): { number }
	if typeString == "number" then
		local value = value :: number
		return { value }
	elseif typeString == "CFrame" then
		local value = value :: CFrame
		-- FUTURE: is there a better way of doing this? doing distance
		-- calculations on `angle` may be incorrect
		local axis, angle = value:ToAxisAngle()
		return { value.X, value.Y, value.Z, axis.X, axis.Y, axis.Z, angle }
	elseif typeString == "Color3" then
		local value = value :: Color3
		local lab = Oklab.fromRGB(value)
		return { lab.X, lab.Y, lab.Z }
	elseif typeString == "ColorSequenceKeypoint" then
		local value = value :: ColorSequenceKeypoint
		local lab = Oklab.fromRGB(value.Value)
		return { lab.X, lab.Y, lab.Z, value.Time }
	elseif typeString == "DateTime" then
		local value = value :: DateTime
		return { value.UnixTimestampMillis }
	elseif typeString == "NumberRange" then
		local value = value :: NumberRange
		return { value.Min, value.Max }
	elseif typeString == "NumberSequenceKeypoint" then
		local value = value :: NumberSequenceKeypoint
		return { value.Value, value.Time, value.Envelope }
	elseif typeString == "PhysicalProperties" then
		local value = value :: PhysicalProperties
		return { value.Density, value.Friction, value.Elasticity, value.FrictionWeight, value.ElasticityWeight }
	elseif typeString == "Ray" then
		local value = value :: Ray
		return {
			value.Origin.X,
			value.Origin.Y,
			value.Origin.Z,
			value.Direction.X,
			value.Direction.Y,
			value.Direction.Z,
		}
	elseif typeString == "Rect" then
		local value = value :: Rect
		return { value.Min.X, value.Min.Y, value.Max.X, value.Max.Y }
	elseif typeString == "Region3" then
		local value = value :: Region3
		-- FUTURE: support rotated Region3s if/when they become constructable
		return {
			value.CFrame.X,
			value.CFrame.Y,
			value.CFrame.Z,
			value.Size.X,
			value.Size.Y,
			value.Size.Z,
		}
	elseif typeString == "Region3int16" then
		local value = value :: Region3int16
		return { value.Min.X, value.Min.Y, value.Min.Z, value.Max.X, value.Max.Y, value.Max.Z }
	elseif typeString == "UDim" then
		local value = value :: UDim
		return { value.Scale, value.Offset }
	elseif typeString == "UDim2" then
		local value = value :: UDim2
		return { value.X.Scale, value.X.Offset, value.Y.Scale, value.Y.Offset }
	elseif typeString == "Vector2" then
		local value = value :: Vector2
		return { value.X, value.Y }
	elseif typeString == "Vector2int16" then
		local value = value :: Vector2int16
		return { value.X, value.Y }
	elseif typeString == "Vector3" then
		local value = value :: Vector3
		return { value.X, value.Y, value.Z }
	elseif typeString == "Vector3int16" then
		local value = value :: Vector3int16
		return { value.X, value.Y, value.Z }
	else
		return {}
	end
end

return DataType


--!optimize 2
--!native

local Color = require(script.Parent.Color)

local Oklab = Color.Oklab

local function Interpolate(a, b, t: number): any
	if typeof(a) == "number" then
		return math.lerp(a :: number, b :: number, t)
	elseif typeof(a) == "CFrame" then
		return a:Lerp(b, t)
	elseif typeof(a) == "Color3" then
		return Oklab.toRGB(Oklab.fromRGB(a):Lerp(Oklab.fromRGB(b), t))
	elseif typeof(a) == "ColorSequenceKeypoint" then
		return ColorSequenceKeypoint.new(
			math.lerp(a.Time, b.Time, t),
			Oklab.toRGB(Oklab.fromRGB(a.Value):Lerp(Oklab.fromRGB(b.Value), t))
		)
	elseif typeof(a) == "DateTime" then
		return DateTime.fromUnixTimestampMillis(math.lerp(a.UnixTimestampMillis, b.UnixTimestampMillis, t))
	elseif typeof(a) == "NumberRange" then
		return NumberRange.new(math.lerp(a.Min, b.Min, t), math.lerp(a.Max, b.Max, t))
	elseif typeof(a) == "NumberSequenceKeypoint" then
		return NumberSequenceKeypoint.new(math.lerp(a.Time, b.Time, t), math.lerp(a.Value, b.Value, t))
	elseif typeof(a) == "PhysicalProperties" then
		return PhysicalProperties.new(
			math.lerp(a.Density, b.Density, t),
			math.lerp(a.Friction, b.Friction, t),
			math.lerp(a.Elasticity, b.Elasticity, t),
			math.lerp(a.FrictionWeight, b.FrictionWeight, t),
			math.lerp(a.ElasticityWeight, b.ElasticityWeight, t)
		)
	elseif typeof(a) == "Ray" then
		return Ray.new(a.Origin:Lerp(b.Origin, t), a.Direction:Lerp(b.Direction, t))
	elseif typeof(a) == "Rect" then
		return Rect.new(a.Min:Lerp(b.Min, t), a.Max:Lerp(b.Max, t))
	elseif typeof(a) == "Region3" then
		return Region3.new(a.CFrame:Lerp(b.CFrame, t).Position, a.Size:Lerp(b.Size, t))
	elseif typeof(a) == "Region3int16" then
		return Region3int16.new(
			Vector3int16.new(
				math.lerp(a.Min.X, b.Min.X, t),
				math.lerp(a.Min.Y, b.Min.Y, t),
				math.lerp(a.Min.Z, b.Min.Z, t)
			),
			Vector3int16.new(
				math.lerp(a.Max.X, b.Max.X, t),
				math.lerp(a.Max.Y, b.Max.Y, t),
				math.lerp(a.Max.Z, b.Max.Z, t)
			)
		)
	elseif typeof(a) == "UDim" then
		return UDim.new(math.lerp(a.Scale, b.Scale, t), math.lerp(a.Offset, b.Offset, t))
	elseif typeof(a) == "UDim2" then
		return UDim2.new(
			math.lerp(a.X.Scale, b.X.Scale, t),
			math.lerp(a.X.Offset, b.X.Offset, t),
			math.lerp(a.Y.Scale, b.Y.Scale, t),
			math.lerp(a.Y.Offset, b.Y.Offset, t)
		)
	elseif typeof(a) == "Vector2" then
		return a:Lerp(b, t)
	elseif typeof(a) == "Vector2int16" then
		return Vector2int16.new(math.lerp(a.X, b.X, t), math.lerp(a.Y, b.Y, t))
	elseif typeof(a) == "Vector3" then
		return a:Lerp(b, t)
	elseif typeof(a) == "Vector3int16" then
		return Vector3int16.new(math.lerp(a.X, b.X, t), math.lerp(a.Y, b.Y, t), math.lerp(a.Z, b.Z, t))
	else -- fallback
		return if t < 0.5 then a else b
	end
end

return Interpolate


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

local Interpolate = require(script.Parent.Interpolate)
local State = require(script.Parent.State)
local Type = require(script.Parent.Type)

local Tween = {}
Tween.__index = Tween

local tweens = setmetatable({}, { __mode = "_k" })
Tween._tweens = tweens

function Tween.new<T>(state: T | Type.Function | State.State<T>, tweenInfo: State.Stateful<TweenInfo>): State.State<T>
	if type(state) == "function" then
		state = State.compute(state)
	elseif State.isState(state) and (state :: any)._computation == nil then
		-- TODO: find a better way to update this state without a new closure
		state = State.compute(function()
			return (state :: any)()
		end)
	else
		state = State.new(state)
	end

	local self = state :: State.State<T>
	self._tweenInfo = tweenInfo
	self._tweenStart = self._value
	Tween._tweens[self] = true

	return self
end

RunService.Heartbeat:Connect(function()
	for tween in tweens do
		if tween._tweenInit then
			local now = os.clock()
			local delta = now - tween._tweenInit
			if delta < 0 then
				continue -- skip until DelayTime
			end

			local info = State.raw(tween._tweenInfo)
			local alpha = delta / info.Time

			if alpha < (if info.Reverses then 2 else 1) then
				if info.Reverses then
					alpha = 1 - (alpha - 1)
				end
				local t = TweenService:GetValue(alpha, info.EasingStyle, info.EasingDirection)
				tween._value = Interpolate(tween._tweenStart, tween._tweenGoal, t)
			else
				if (tween._tweenRepeat or 0) > 0 then
					tween._tweenRepeat -= 1
					tween._tweenInit = now + info.DelayTime
				else -- finish
					tween._tweenInit = nil
				end
				tween._value = if info.Reverses then tween._tweenStart else tween._tweenGoal
			end
			task.spawn(tween._update, tween, true, true, true)
		end
	end
end)

return Tween


export type Function = (...any) -> ...any
export type Dict<T> = { [T]: any }
export type Set<T> = { [T]: boolean }
export type Symbol = { type: "Symbol" }

export type New<Name, Class> = (Name) -> (Class & Dict<any>) -> Class
export type BaseConstructors =
	New<"Folder", Folder>
	& New<"BillboardGui", BillboardGui>
	& New<"CanvasGroup", CanvasGroup>
	& New<"Frame", Frame>
	& New<"ImageButton", ImageButton>
	& New<"ImageLabel", ImageLabel>
	& New<"ScreenGui", ScreenGui>
	& New<"ScrollingFrame", ScrollingFrame>
	& New<"SurfaceGui", SurfaceGui>
	& New<"TextBox", TextBox>
	& New<"TextButton", TextButton>
	& New<"TextLabel", TextLabel>
	& New<"UIAspectRatioConstraint", UIAspectRatioConstraint>
	& New<"UICorner", UICorner>
	& New<"UIDragDetector", UIDragDetector>
	& New<"UIFlexItem", UIFlexItem>
	& New<"UIGradient", UIGradient>
	& New<"UIGridLayout", UIGridLayout>
	& New<"UIListLayout", UIListLayout>
	& New<"UIPadding", UIPadding>
	& New<"UIPageLayout", UIPageLayout>
	& New<"UIScale", UIScale>
	& New<"UISizeConstraint", UISizeConstraint>
	& New<"UIStroke", UIStroke>
	& New<"UITableLayout", UITableLayout>
	& New<"UITextSizeConstraint", UITextSizeConstraint>
	& New<"VideoFrame", VideoFrame>
	& New<"ViewportFrame", ViewportFrame>
	& New<string, Instance>
	& { [Symbol]: Dict<any> }

export type Constructor = (<T>(T & Instance) -> (T & Dict<any>) -> T) & BaseConstructors

export type Animatable =
	number
	| CFrame
	| Color3
	| ColorSequenceKeypoint
	| DateTime
	| NumberRange
	| NumberSequenceKeypoint
	| PhysicalProperties
	| Ray
	| Rect
	| Region3
	| Region3int16
	| UDim
	| UDim2
	| Vector2
	| Vector2int16
	| Vector3
	| Vector3int16

return nil


local Color = require(script.Color)
local Interpolate = require(script.Interpolate)
local State = require(script.State)
local Spring = require(script.Spring)
local Tween = require(script.Tween)
local Type = require(script.Type)

--- A reactive state management library that makes it easy to create dynamic user interfaces and game logic
local Flux = {
	--- A symbol for defining cleanup in [Flux.edit] and [Flux.new]
	Clean = table.freeze({ type = "Symbol" }) :: Type.Symbol,
	--- A symbol for defining attributes in [Flux.edit] and [Flux.new]
	Attribute = table.freeze({ type = "Symbol" }) :: Type.Symbol,
	--- A symbol for defining events in [Flux.edit] and [Flux.new]
	Event = table.freeze({ type = "Symbol" }) :: Type.Symbol,

	Color = Color,
	Interpolate = Interpolate,
	State = State,
	Type = Type,

	isState = State.isState,
	raw = State.raw,
	rawVariadic = State.rawVariadic,

	--- Creates a computed state, automatically tracking dependencies
	compute = State.compute,
	--- Creates a new Flux state with an initial value
	state = State.new,
	--- Creates a new Flux spring state
	spring = Spring.new,
	--- Creates a new Flux tween state
	tween = Tween.new,

	--- Sets the update defer behaviour, defaults to false to improve performance
	deferUpdates = function(value: boolean?)
		State.deferUpdates = value
	end,
}
Flux.__index = Flux

function Flux.cleanup(obj: any)
	local objType = typeof(obj)
	if objType == "Instance" then
		pcall(game.Destroy, obj)
	elseif objType == "RBXScriptConnection" then
		if obj.Connected then
			obj:Disconnect()
		end
	elseif objType == "function" then
		obj()
	elseif objType == "thread" then
		pcall(task.cancel, obj)
	elseif objType == "table" then
		if typeof(obj.destroy) == "function" then
			obj:destroy()
		elseif typeof(obj.Destroy) == "function" then
			obj:Destroy()
		elseif obj[1] ~= nil then
			for _, subtask in obj do
				Flux.cleanup(subtask)
			end
		elseif typeof(obj[Flux.Clean]) == "table" then
			for _, subtask in obj[Flux.Clean] do
				Flux.cleanup(subtask)
			end
		end
	end
end

type Action = {
	callback: (Instance) -> (),
}

local Action = table.freeze {}

local function isAction(v: any)
	return getmetatable(v) == Action
end

--- Creates an action to run logic on an instance
function Flux.action(callback: (Instance) -> ()): Action
	local action = setmetatable({ callback = callback }, Action)
	return table.freeze(action)
end

local reserved = { Flux.Attribute, Flux.Clean, Flux.Event, "Parent" }

local function processProperties(instance: Instance, properties: Type.Dict<any>)
	local cache = {
		Parent = nil,
		actions = {},
		signals = {},
	}
	for _, key in reserved do
		cache[key] = properties[key]
		properties[key] = nil
	end

	for property, value in properties do
		-- numeric index child definition
		if type(property) == "number" and property == math.floor(property) then
			if typeof(value) == "Instance" then
				if value:IsA("GuiObject") and value.LayoutOrder == 0 then
					value.LayoutOrder = property
				end
				value.Parent = instance
			elseif type(value) == "table" and isAction(value) then
				table.insert(cache.actions, value)
			else
				if type(value) == "function" then
					value = Flux.compute(value)
				end
				if Flux.isState(value) then
					if not value._boundChildren then
						value._boundChildren = {}
					end
					value._boundChildrenLayoutOrder = property
					value._boundParent = instance
					value:_updateBoundChildren()
				end
			end
			continue
		end

		if type(value) == "function" then
			if typeof(instance[property]) == "RBXScriptSignal" then
				cache.signals[instance[property]] = value
				continue
			end
			value = Flux.compute(value)
		end

		State._bindProperty(value, instance, property)
		instance[property] = Flux.raw(value)
	end

	if cache[Flux.Attribute] then
		for attribute, value in cache[Flux.Attribute] do
			State._bindProperty(value, instance, "Attribute." .. attribute)
			instance:SetAttribute(attribute, Flux.raw(value))
		end
	end

	if cache[Flux.Event] then
		local events = cache[Flux.Event]
		local attributeEvents = events[Flux.Attribute]
		events[Flux.Attribute] = nil

		for event, callback in events do
			if typeof(instance[event]) == "RBXScriptSignal" then
				cache.signals[instance[event]] = callback
			else
				instance:GetPropertyChangedSignal(event):Connect(function()
					callback(instance[event])
				end)
			end
		end

		if attributeEvents then
			for attribute, callback in attributeEvents do
				instance:GetAttributeChangedSignal(attribute):Connect(function()
					callback(instance:GetAttribute(attribute))
				end)
			end
		end
	end

	for signal, callback in cache.signals do
		signal:Connect(callback)
	end

	local clean = cache[Flux.Clean] or {}

	for _, action in cache.actions do
		table.insert(clean, task.spawn(action.callback, instance))
	end

	if clean and next(clean) then
		if not clean[Flux.Clean] then
			instance.Destroying:Once(function()
				Flux.cleanup(clean)
			end)
		end
	end

	local parent = cache.Parent
	if parent then
		if type(parent) == "function" then
			parent = Flux.compute(parent)
		end
		State._bindProperty(parent, instance, "Parent")
		instance.Parent = Flux.raw(parent)
	end
end

--- Edits an existing Instance with Flux state bindings and other properties
function Flux.edit<T>(instance: T & Instance, properties: Type.Dict<any>): T
	processProperties(instance, properties)
	return instance
end

local constructors: { [string]: () -> Instance } = {}
setmetatable(constructors, {
	__index = function(self, class: string)
		local function new(properties: Type.Dict<any>): Instance
			return Flux.edit(Instance.new(class), properties)
		end
		self[class] = new
		return new
	end,
})

--- Creates a new Instance with Flux state bindings and other properties
Flux.new = (
	function(classOrInstance: string | Instance): (Type.Dict<any>) -> Instance
		if typeof(classOrInstance) == "Instance" then
			return function(properties: Type.Dict<any>): Instance
				local clone = classOrInstance:Clone()
				if not clone then
					error "Attempt to clone non-archivable instance"
				end
				return Flux.edit(clone, properties)
			end
		end
		return constructors[classOrInstance]
	end :: any
) :: Type.Constructor

return Flux


--[[

Hey reader! If you've seen this branch then you'll see that this is a new
file and is empty!

I'm currently working on documentation first and then going to
this, however I am debating whether we should take an object-oriented
approach to this or functional

The api would look something similar to this for OOP:

HitboxHandler.new()
Hitbox:itemEnteredHitbox(function()
    -- avoid
end)
Hitbox:Destroy() -- cleans everything up

This would make every hitbox independent and would probably be easier

Or

HitboxHandler.CreateHitbox("HitboxName")
HitboxHandler.HitboxEntered("HitboxName", function(Part)

end)
HitboxHandler.DestroyHitbox("HitboxName")

Please discuss in the discord channel which you think would be best and why

]]

local Hitboxes = {}

return Hitboxes


local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local Promise = require(script.Parent.Packages.Promise)
local Logging = require(script.Parent.Logging)
local FunctionLib = require(script.Parent.Util.FunctionLib)

local Lifecycles = {}
local CustomCycles = {
	Server = {},
	Client = {},
}

local LifecyclesEvents = {
	PlayerAdded = Players.PlayerAdded,
	PlayerRemoving = Players.PlayerRemoving,
	Heartbeat = RunService.Heartbeat,
	PostSimulation = RunService.PostSimulation,
	PreSimulation = RunService.PreSimulation,
}

local function RunCycles(Modules, Callback, ...)
	for _, Module in Modules do
		local Args = ...

		task.spawn(function()
			if require(Module)[Callback] then
				require(Module)[Callback](Args)
			end
		end)
	end
end

function Lifecycles.Run(Modules: { any }, PossibleLifecycles: { typeof(LifecyclesEvents) }?)
	if not PossibleLifecycles then
		return
	end

	local TotalLifecycles = LifecyclesEvents

	local CycleNamesServer = {}
	local CycleNamesClient = {}

	for Name, _ in CustomCycles.Client do
		table.insert(CycleNamesServer, Name)
	end

	if RunService:IsServer() then
		TotalLifecycles = FunctionLib:MergeDictionary(TotalLifecycles, CustomCycles.Server)

		for Name, _ in CustomCycles.Server do
			table.insert(CycleNamesServer, Name)
		end

		PossibleLifecycles = FunctionLib:MergeDictionary(PossibleLifecycles, CycleNamesServer)
	end

	if RunService:IsClient() then
		local ClientCycles = {
			CharacterAdded = Players.LocalPlayer.CharacterAdded,
			CharacterRemoving = Players.LocalPlayer.CharacterRemoving,
			RenderStepped = RunService.RenderStepped,
		}

		TotalLifecycles = FunctionLib:MergeDictionary(TotalLifecycles, ClientCycles)
		TotalLifecycles = FunctionLib:MergeDictionary(TotalLifecycles, CustomCycles.Client)

		for Name, _ in CustomCycles.Client do
			table.insert(CycleNamesClient, Name)
		end

		PossibleLifecycles = FunctionLib:MergeDictionary(PossibleLifecycles, CycleNamesClient)
	end

	for _, Lifecycle in PossibleLifecycles do
		if TotalLifecycles[Lifecycle] then
			TotalLifecycles[Lifecycle]:Connect(function(...)
				RunCycles(Modules, Lifecycle, ...)
			end)
		end
	end
end

function Lifecycles.New(Name: string, Connection: RBXScriptSignal)
	return Promise.new(function(Resolve, Reject)
		local CustomLifecycleTable = CustomCycles[RunService:IsServer() and "Server" or "Client"]

		if CustomLifecycleTable[Name] then
			Logging.Message({
				Message = "You already have a lifecycle with this name! Overriding",
				Level = "WARN",
				LogCat = "HYDROGEN LIFECYCLES: ",
			})
		end

		CustomLifecycleTable[Name] = Connection

		if not CustomLifecycleTable[Name] then
			Reject(`HYDROGEN: Failed to add lifecycle: {Name}`)
		end

		Resolve(true)
	end):catch(warn)
end

function Lifecycles.Chain(NewLifecycles: { [string]: RBXScriptSignal })
	return Promise.new(function(Resolve)
		for Name, Connection in NewLifecycles do
			Lifecycles.New(Name, Connection):expect():catch(warn)
		end

		Resolve(true)
	end)
end

return Lifecycles


--[=[
	@class Log

	Created by railworks2, used with written permission.
]=]
local RunService = game:GetService("RunService")
local Promise = require(script.Parent.Packages.Promise)
local Log = {}

--[=[
	@type LogLevel "INFO" | "DEBUG" | "WARN" | "ERROR" | "FATAL"
	@within Log
]=]
type LogLevel = "INFO" | "DEBUG" | "WARN" | "ERROR" | "FATAL"

--[=[
	@interface Log.Config
	@within Log
	.LogLevel { Enabled: boolean, Command: function, Traceback?: boolean }

	| Log Level | Enabled | Traceback | Command |
	|---|---|---|---|
	| **DEBUG** | [RunMode](https://create.roblox.com/docs/reference/engine/classes/RunService#IsRunMode) only | Yes | print |
	| **INFO** | Yes | No | print |
	| **WARN** | Yes | Yes | warn |
	| **ERROR** | Yes | Yes | warn |
	| **FATAL** | Yes | Yes | **error** |

	_Be aware that __FATAL__ will terminate code execution and should only be used for errors that are unrecoverable._
]=]
Log.Config = {
	["DEBUG"] = {
		Enabled = RunService:IsRunMode(),
		Command = print,
		Traceback = true,
	},
	["INFO"] = {
		Enabled = true,
		Command = print,
	},
	["WARN"] = {
		Enabled = true,
		Command = warn,
		Traceback = true,
	},
	["ERROR"] = {
		Enabled = true,
		Command = warn,
		Traceback = true,
	},
	["FATAL"] = {
		Enabled = true,
		Command = error,
		Traceback = false,
	},
}

--[=[
	@within Log
	@param scriptLine string
	@return string -- scriptLine
	@private

	Removes ReplicatedStorage and ServerStorage from the provided scriptLine - Part output of debug.info(2, "sl")
]=]
local function cleanupScriptLine(scriptLine)
	for _, removeService in { "ReplicatedStorage.", "ServerStorage." } do
		scriptLine = string.gsub(scriptLine, removeService, "")
	end
	return scriptLine
end

--[=[
	@within Log
	@return nil | string
	@param debugTraceback string -- Multi line string outputn of debug.traceback()
	@private

	Takes the results from the provided debug.traceback() and more neatly formats it into a single multiline string.

	Removing results from areas that are from core scripts (This module, ModuleLoader, Root loading scripts)

	**Note:** Return can be nil if there is no traceback
]=]
local function cleanupTraceback(debugTraceback: string)
	local tracebackArray = {}
	local doNotShowLogsFromHere = { ".Log:", ".ClientCore:", ".ServerCore:", ".promise:" }

	for _, trackbackDetails in string.split(debugTraceback, "\n") do
		local acceptableItem = true

		if string.len(trackbackDetails) == 0 then
			acceptableItem = false
		else
			for _, item in doNotShowLogsFromHere do
				if string.find(trackbackDetails, item) then
					acceptableItem = false
					break
				end
			end
		end

		if acceptableItem then
			table.insert(tracebackArray, cleanupScriptLine(trackbackDetails) .. "\t")
		end
	end

	if tracebackArray == nil or #tracebackArray >= 0 then
		return
	end

	tracebackArray[#tracebackArray] = string.gsub(tracebackArray[#tracebackArray], "\n", "")
	return table.unpack(tracebackArray)
end

--[=[
	@within Log

	Creates a message to be logged. Everything uses this function.

	Example 1:
	> [INFO][LOG] This is an example logging message - Shared.Log:111 - Client

	Example 2:

	> [ERROR][LOAD][ID: NotActive] Failed to start module TestModule due to the Active flag not being enabled - Shared.Log:111 - Server
	>  Data: {"testData"}
	>  Traceback: Shared.UILoad:62
	>  Server.Load:152

]=]
function Log.Message(
	config: {
		Level: LogLevel,
		LogCat: string,
		Message: string,
		LogRef: string?,
		ForceTraceback: boolean?,
	},
	...: any?
)
	local debugTraceback = debug.traceback()
	local scriptName, scriptLine = debug.info(2, "sl")
	local data = if ... then { ... } else nil

	Promise.new(function()
		if Log.Config[config.Level] and Log.Config[config.Level].Enabled == true then
			local startOfMessage = `[{config.Level}][{string.upper(config.LogCat) or "NOCAT"}]`
			if config.LogRef ~= nil then
				startOfMessage ..= `[ID: {config.LogRef}]`
			end

			local endOfMessage = `- {cleanupScriptLine(scriptName)}:{scriptLine} - {if RunService:IsServer()
				then "Server"
				else "Client"}`
			if data ~= nil then
				print(data)
				endOfMessage ..= "\n\t\tData: " .. data
			end

			if
				(Log.Config[config.Level].Traceback == true or config.ForceTraceback == true)
				and cleanupTraceback(debugTraceback) ~= nil
			then
				endOfMessage ..= "\n\t\tTraceback: " .. cleanupTraceback(debugTraceback)
			end

			Log.Config[config.Level].Command(`{startOfMessage} {config.Message} {endOfMessage}`)
		end
	end)
end

--[=[
	@within Log
	@return value | false
	Throws an error if the provided value is false or nil. If the assertion passes, it returns all values passed to it. See [#Message](#Message) for guidance.
]=]
function Log.assert(
	value: any,
	config: {
		Level: LogLevel,
		LogCat: string,
		LogRef: string?,
		Message: string,
		ForceTraceback: boolean?,
	}
)
	if not value then
		Log.Message({
			Level = "ERROR",
			LogCat = config.LogCat,
			Message = config.Message,
			LogRef = config.LogRef,
			ForceTraceback = config.ForceTraceback,
		})
		return false
	end
	return value
end

return Log


-- fewkz/typed-luau-promise 2023
-- Generated by generate-promise.ts
type PromiseStatus = "Started" | "Resolved" | "Rejected" | "Cancelled"
type PromiseLike<T...> = { expect: (self: PromiseLike<T...>) -> T..., [any]: any }
-- stylua: ignore start
type PromiseExhausted = {
	andThen: <T2...>(
		self: PromiseLike<...any>,
		successHandler: ((...any) -> T2...)?,
		failureHandler: ((...any) -> T2...)?
	) -> PromiseExhausted,
	andThenCall: <A..., T2...>(
		self: PromiseLike<...any>,
		callback: (A...) -> T2...,
		A...
	) -> PromiseExhausted,
	andThenReturn: <T2...>(self: PromiseLike<...any>, T2...) -> PromiseExhausted,
	cancel: (self: PromiseLike<...any>) -> (),
	catch: <T2...>(
		self: PromiseLike<...any>,
		failureHandler: (...any) -> T2...
	) -> PromiseExhausted,
	expect: (self: PromiseLike<...any>) -> ...any,
	finally: <T2...>(self: PromiseLike<...any>, (status: "Resolved" | "Rejected" | "Cancelled") -> T2...) -> PromiseExhausted,
	getStatus: (self: PromiseLike<...any>) -> PromiseStatus,
	now: (self: PromiseLike<...any>, rejectionValue: any?) -> PromiseExhausted
}
type Promise8<T...> = {
	andThen: <T2...>(
		self: PromiseLike<T...>,
		successHandler: ((T...) -> T2...)?,
		failureHandler: ((...any) -> T2...)?
	) -> PromiseExhausted,
	andThenCall: <A..., T2...>(
		self: PromiseLike<T...>,
		callback: (A...) -> T2...,
		A...
	) -> PromiseExhausted,
	andThenReturn: <T2...>(self: PromiseLike<T...>, T2...) -> PromiseExhausted,
	cancel: (self: PromiseLike<T...>) -> (),
	catch: <T2...>(
		self: PromiseLike<T...>,
		failureHandler: (...any) -> T2...
	) -> PromiseExhausted,
	expect: (self: PromiseLike<T...>) -> T...,
	finally: <T2...>(self: PromiseLike<T...>, (status: "Resolved" | "Rejected" | "Cancelled") -> T2...) -> PromiseExhausted,
	getStatus: (self: PromiseLike<T...>) -> PromiseStatus,
	now: (self: PromiseLike<T...>, rejectionValue: any?) -> Promise8<T...>
}
type Promise7<T...> = {
	andThen: <T2...>(
		self: PromiseLike<T...>,
		successHandler: ((T...) -> T2...)?,
		failureHandler: ((...any) -> T2...)?
	) -> Promise8<T2...>,
	andThenCall: <A..., T2...>(
		self: PromiseLike<T...>,
		callback: (A...) -> T2...,
		A...
	) -> Promise8<T2...>,
	andThenReturn: <T2...>(self: PromiseLike<T...>, T2...) -> Promise8<T2...>,
	cancel: (self: PromiseLike<T...>) -> (),
	catch: <T2...>(
		self: PromiseLike<T...>,
		failureHandler: (...any) -> T2...
	) -> Promise8<T2...>,
	expect: (self: PromiseLike<T...>) -> T...,
	finally: <T2...>(self: PromiseLike<T...>, (status: "Resolved" | "Rejected" | "Cancelled") -> T2...) -> Promise8<T2...>,
	getStatus: (self: PromiseLike<T...>) -> PromiseStatus,
	now: (self: PromiseLike<T...>, rejectionValue: any?) -> Promise7<T...>
}
type Promise6<T...> = {
	andThen: <T2...>(
		self: PromiseLike<T...>,
		successHandler: ((T...) -> T2...)?,
		failureHandler: ((...any) -> T2...)?
	) -> Promise7<T2...>,
	andThenCall: <A..., T2...>(
		self: PromiseLike<T...>,
		callback: (A...) -> T2...,
		A...
	) -> Promise7<T2...>,
	andThenReturn: <T2...>(self: PromiseLike<T...>, T2...) -> Promise7<T2...>,
	cancel: (self: PromiseLike<T...>) -> (),
	catch: <T2...>(
		self: PromiseLike<T...>,
		failureHandler: (...any) -> T2...
	) -> Promise7<T2...>,
	expect: (self: PromiseLike<T...>) -> T...,
	finally: <T2...>(self: PromiseLike<T...>, (status: "Resolved" | "Rejected" | "Cancelled") -> T2...) -> Promise7<T2...>,
	getStatus: (self: PromiseLike<T...>) -> PromiseStatus,
	now: (self: PromiseLike<T...>, rejectionValue: any?) -> Promise6<T...>
}
type Promise5<T...> = {
	andThen: <T2...>(
		self: PromiseLike<T...>,
		successHandler: ((T...) -> T2...)?,
		failureHandler: ((...any) -> T2...)?
	) -> Promise6<T2...>,
	andThenCall: <A..., T2...>(
		self: PromiseLike<T...>,
		callback: (A...) -> T2...,
		A...
	) -> Promise6<T2...>,
	andThenReturn: <T2...>(self: PromiseLike<T...>, T2...) -> Promise6<T2...>,
	cancel: (self: PromiseLike<T...>) -> (),
	catch: <T2...>(
		self: PromiseLike<T...>,
		failureHandler: (...any) -> T2...
	) -> Promise6<T2...>,
	expect: (self: PromiseLike<T...>) -> T...,
	finally: <T2...>(self: PromiseLike<T...>, (status: "Resolved" | "Rejected" | "Cancelled") -> T2...) -> Promise6<T2...>,
	getStatus: (self: PromiseLike<T...>) -> PromiseStatus,
	now: (self: PromiseLike<T...>, rejectionValue: any?) -> Promise5<T...>
}
type Promise4<T...> = {
	andThen: <T2...>(
		self: PromiseLike<T...>,
		successHandler: ((T...) -> T2...)?,
		failureHandler: ((...any) -> T2...)?
	) -> Promise5<T2...>,
	andThenCall: <A..., T2...>(
		self: PromiseLike<T...>,
		callback: (A...) -> T2...,
		A...
	) -> Promise5<T2...>,
	andThenReturn: <T2...>(self: PromiseLike<T...>, T2...) -> Promise5<T2...>,
	cancel: (self: PromiseLike<T...>) -> (),
	catch: <T2...>(
		self: PromiseLike<T...>,
		failureHandler: (...any) -> T2...
	) -> Promise5<T2...>,
	expect: (self: PromiseLike<T...>) -> T...,
	finally: <T2...>(self: PromiseLike<T...>, (status: "Resolved" | "Rejected" | "Cancelled") -> T2...) -> Promise5<T2...>,
	getStatus: (self: PromiseLike<T...>) -> PromiseStatus,
	now: (self: PromiseLike<T...>, rejectionValue: any?) -> Promise4<T...>
}
type Promise3<T...> = {
	andThen: <T2...>(
		self: PromiseLike<T...>,
		successHandler: ((T...) -> T2...)?,
		failureHandler: ((...any) -> T2...)?
	) -> Promise4<T2...>,
	andThenCall: <A..., T2...>(
		self: PromiseLike<T...>,
		callback: (A...) -> T2...,
		A...
	) -> Promise4<T2...>,
	andThenReturn: <T2...>(self: PromiseLike<T...>, T2...) -> Promise4<T2...>,
	cancel: (self: PromiseLike<T...>) -> (),
	catch: <T2...>(
		self: PromiseLike<T...>,
		failureHandler: (...any) -> T2...
	) -> Promise4<T2...>,
	expect: (self: PromiseLike<T...>) -> T...,
	finally: <T2...>(self: PromiseLike<T...>, (status: "Resolved" | "Rejected" | "Cancelled") -> T2...) -> Promise4<T2...>,
	getStatus: (self: PromiseLike<T...>) -> PromiseStatus,
	now: (self: PromiseLike<T...>, rejectionValue: any?) -> Promise3<T...>
}
type Promise2<T...> = {
	andThen: <T2...>(
		self: PromiseLike<T...>,
		successHandler: ((T...) -> T2...)?,
		failureHandler: ((...any) -> T2...)?
	) -> Promise3<T2...>,
	andThenCall: <A..., T2...>(
		self: PromiseLike<T...>,
		callback: (A...) -> T2...,
		A...
	) -> Promise3<T2...>,
	andThenReturn: <T2...>(self: PromiseLike<T...>, T2...) -> Promise3<T2...>,
	cancel: (self: PromiseLike<T...>) -> (),
	catch: <T2...>(
		self: PromiseLike<T...>,
		failureHandler: (...any) -> T2...
	) -> Promise3<T2...>,
	expect: (self: PromiseLike<T...>) -> T...,
	finally: <T2...>(self: PromiseLike<T...>, (status: "Resolved" | "Rejected" | "Cancelled") -> T2...) -> Promise3<T2...>,
	getStatus: (self: PromiseLike<T...>) -> PromiseStatus,
	now: (self: PromiseLike<T...>, rejectionValue: any?) -> Promise2<T...>
}
type Promise1<T...> = {
	andThen: <T2...>(
		self: PromiseLike<T...>,
		successHandler: ((T...) -> T2...)?,
		failureHandler: ((...any) -> T2...)?
	) -> Promise2<T2...>,
	andThenCall: <A..., T2...>(
		self: PromiseLike<T...>,
		callback: (A...) -> T2...,
		A...
	) -> Promise2<T2...>,
	andThenReturn: <T2...>(self: PromiseLike<T...>, T2...) -> Promise2<T2...>,
	cancel: (self: PromiseLike<T...>) -> (),
	catch: <T2...>(
		self: PromiseLike<T...>,
		failureHandler: (...any) -> T2...
	) -> Promise2<T2...>,
	expect: (self: PromiseLike<T...>) -> T...,
	finally: <T2...>(self: PromiseLike<T...>, (status: "Resolved" | "Rejected" | "Cancelled") -> T2...) -> Promise2<T2...>,
	getStatus: (self: PromiseLike<T...>) -> PromiseStatus,
	now: (self: PromiseLike<T...>, rejectionValue: any?) -> Promise1<T...>
}
type _AnyPromise = {
	andThen: <T2...>(
		self: PromiseLike<...any>,
		successHandler: ((...any) -> T2...)?,
		failureHandler: ((...any) -> T2...)?
	) -> any,
	andThenCall: <A..., T2...>(
		self: PromiseLike<...any>,
		callback: (A...) -> T2...,
		A...
	) -> any,
	andThenReturn: <T2...>(self: PromiseLike<...any>, T2...) -> any,
	cancel: (self: PromiseLike<...any>) -> (),
	catch: <T2...>(
		self: PromiseLike<...any>,
		failureHandler: (...any) -> T2...
	) -> any,
	expect: (self: PromiseLike<...any>) -> ...any,
	finally: <T2...>(self: PromiseLike<...any>, (status: "Resolved" | "Rejected" | "Cancelled") -> T2...) -> any,
	getStatus: (self: PromiseLike<...any>) -> PromiseStatus,
	now: (self: PromiseLike<...any>, rejectionValue: any?) -> _AnyPromise
}
export type AnyPromise = _AnyPromise
type PromiseLib = {
	Status: {
		Started: "Started",
		Resolved: "Resolved",
		Rejected: "Rejected",
		Cancelled: "Cancelled",
	},

	all: <T>(promises: { PromiseLike<T> }) -> Promise1<{ T }>,
	delay: (seconds: number) -> Promise1<number>,
	fromEvent: <T...>(event: RBXScriptSignal<T...>, predicate: ((T...) -> boolean)?) -> Promise1<T...>,
	new: <T...>((
		resolve: (T...) -> (),
		reject: (...any) -> (),
		cancel: ((callback: (() -> ())?) -> boolean) -> ()
	) -> ()) -> Promise1<T...>,
	resolve: <T...>(T...) -> Promise1<T...>,
	try: <T..., A...>(callback: (A...) -> T..., A...) -> Promise1<T...>,
}
-- stylua: ignore end

return require(script.untyped) :: PromiseLib


--[[

WHY THIS?

- Packages from pesde itself aren't great. So we put stuff here.
Also the datastore manager we use is ProfileStore (changed from lapis) which isnt on a package manager.

]]

return {}