local MarketplaceService = game:GetService("MarketplaceService")

local Signal = require(script.Parent.Signal)
local AvatarAssetTypes = require(script.AvatarAssetTypes)
local AssetAccessoryTypes = require(script.AssetAccessoryTypes)
local Future = require(script.Parent.Future)
local TableUtil = require(script.Parent.TableUtil)

export type CatalogItem = {
	AssetId: number,
	Name: string,
	Type: string,
	AssetType: string,
}
export type AccessorySpec = {
	AssetId: number,
	AccessoryType: Enum.AccessoryType,
	Order: number?,
	Puffiness: number?,
	IsLayered: boolean?,
	Position: Vector3?,
	Rotation: Vector3?,
	Scale: Vector3?,
}
export type EquippedEmote = {
	Name: string,
	Slot: number,
}
export type Data = {
	Accessories: { AccessorySpec },
	Emotes: {
		[string]: { number },
	},
	EquippedEmotes: { EquippedEmote },
	Face: number,
	Scale: {
		BodyType: number,
		Depth: number,
		Head: number,
		Height: number,
		Proportion: number,
		Width: number,
	},
	Animations: {
		Walk: number,
		Run: number,
		Fall: number,
		Climb: number,
		Swim: number,
		Idle: number,
		Mood: number,
		Jump: number,
	},
	BodyParts: {
		Head: number,
		Torso: number,
		LeftArm: number,
		RightArm: number,
		LeftLeg: number,
		RightLeg: number,
	},
	BodyPartColors: {
		Head: Color3,
		Torso: Color3,
		LeftArm: Color3,
		RightArm: Color3,
		LeftLeg: Color3,
		RightLeg: Color3,
	},
	Clothing: {
		Shirt: number,
		TShirt: number,
		Pants: number,
	},
}

local function Extract(Source: { [any]: any }, Callback: (any, any) -> any)
	local Extracted = {}

	for Key, Value in pairs(Source) do
		table.insert(Extracted, Callback(Key, Value))
	end

	return Extracted
end
local function UnpackInto(Target: { [number]: any }, Source: { [number]: any })
	for _, Entry in ipairs(Source) do
		table.insert(Target, Entry)
	end
end

local function New(HumanoidDescription: HumanoidDescription)
	local HumanoidDescriber: HumanoidDescriber = {
		Data = {
			Accessories = {},
			Emotes = {},
			Face = 0,
			Scale = {
				BodyType = 0,
				Depth = 0,
				Head = 0,
				Height = 0,
				Proportion = 0,
				Width = 0,
			},
			Animations = {
				Walk = 0,
				Run = 0,
				Fall = 0,
				Climb = 0,
				Swim = 0,
				Idle = 0,
				Mood = 0,
				Jump = 0,
			},
			BodyParts = {
				Head = 0,
				Torso = 0,
				LeftArm = 0,
				RightArm = 0,
				LeftLeg = 0,
				RightLeg = 0,
			},
			BodyPartColors = {
				Head = Color3.fromRGB(0, 0, 0),
				Torso = Color3.fromRGB(0, 0, 0),
				LeftArm = Color3.fromRGB(0, 0, 0),
				RightArm = Color3.fromRGB(0, 0, 0),
				LeftLeg = Color3.fromRGB(0, 0, 0),
				RightLeg = Color3.fromRGB(0, 0, 0),
			},
			Clothing = {
				Shirt = 0,
				TShirt = 0,
				Pants = 0,
			},
		},
		HumanoidDescription = Instance.new("HumanoidDescription"),
		Updated = Signal.new(),
	}

	function HumanoidDescriber:EquipAsset(AssetId: number, AssetType: Enum.AvatarAssetType)
		if not self:IsAssetEquipped(AssetId, AssetType) then
			self:SetAssetEquipped(AssetId, AssetType, true)
		end
	end

	function HumanoidDescriber:UnequipAsset(AssetId: number, AssetType: Enum.AvatarAssetType)
		if self:IsAssetEquipped(AssetId, AssetType) then
			self:SetAssetEquipped(AssetId, AssetType, false)
		end
	end

	function HumanoidDescriber:SetAssetEquipped(AssetId: number, AssetType: Enum.AvatarAssetType, Equip: boolean)
		local Equipped = self:IsAssetEquipped(AssetId, AssetType)

		if table.find(AvatarAssetTypes.Types.Accessories, AssetType) then
			local AccessoryType = AssetAccessoryTypes[AssetType]
			if AccessoryType then
				if Equip then
					if not Equipped then
						self:AddAccessory({
							AssetId = AssetId,
							AccessoryType = AccessoryType,
							Order = 1,
						})
					end
				else
					self:RemoveAccessory(AssetId)
				end
			end
		elseif AssetType == Enum.AvatarAssetType.Face then
			self:UpdateData(function(Data)
				if Equip then
					if not Equipped then
						Data.Face = AssetId
					end
				else
					Data.Face = 0
				end

				return Data
			end)
		elseif table.find(AvatarAssetTypes.Types.Clothing, AssetType) then
			self:UpdateData(function(Data)
				if Equip then
					if not Equipped then
						Data.Clothing[AssetType.Name] = AssetId
					end
				else
					Data.Clothing[AssetType.Name] = 0
				end

				return Data
			end)
		elseif AssetType == Enum.AvatarAssetType.EmoteAnimation then
			local Name, Emote = self:GetEmoteByAssetId(AssetId)

			if Name == nil then
				Future.Try(function()
					local ProductInfo = MarketplaceService:GetProductInfo(AssetId, Enum.InfoType.Asset)
					if ProductInfo then
						Name = ProductInfo.Name
						Emote = { ProductInfo.AssetId }

						self:SetEmote(Name, Emote)
					end
				end):Await()
			end

			self:SetEmoteEquipped(Name, not Equipped)
		elseif table.find(AvatarAssetTypes.Types.Animations, AssetType) then
			self:UpdateData(function(Data)
				local AnimationName = string.gsub(AssetType.Name, "Animation", "", 1)
				local Animation = Data.Animations[AnimationName]
				if Animation then
					if Equip then
						if not Equipped then
							Data.Animations[AnimationName] = AssetId
						end
					else
						Data.Animations[AnimationName] = 0
					end
				end

				return Data
			end)
		elseif table.find(AvatarAssetTypes.Types.BodyParts, AssetType) then
			self:UpdateData(function(Data)
				local PartName = AssetType.Name
				if PartName == "DynamicHead" then
					PartName = "Head"
				end

				local BodyPart = Data.BodyParts[PartName]
				if BodyPart then
					if Equip then
						if not Equipped then
							Data.BodyParts[PartName] = AssetId
						end
					else
						Data.BodyParts[PartName] = 0
					end
				end

				return Data
			end)
		end
	end

	function HumanoidDescriber:IsAssetEquipped(AssetId: number, AssetType: Enum.AvatarAssetType)
		local Data = self:GetData()

		if table.find(AvatarAssetTypes.Types.Accessories, AssetType) then
			return self:GetAccessory(AssetId) ~= nil
		elseif AssetType == Enum.AvatarAssetType.Face then
			return Data.Face == AssetId
		elseif table.find(AvatarAssetTypes.Types.Clothing, AssetType) then
			return Data.Clothing[AssetType.Name] == AssetId
		elseif AssetType == Enum.AvatarAssetType.EmoteAnimation then
			local Name = self:GetEmoteByAssetId(AssetId)
			return self:GetEquippedEmote(Name) ~= nil
		elseif table.find(AvatarAssetTypes.Types.Animations, AssetType) then
			local AnimationName = string.gsub(AssetType.Name, "Animation", "", 1)
			local Animation = Data.Animations[AnimationName]

			return Animation == AssetId
		elseif table.find(AvatarAssetTypes.Types.BodyParts, AssetType) then
			local PartName = AssetType.Name
			if PartName == "DynamicHead" then
				PartName = "Head"
			end

			local BodyPart = Data.BodyParts[PartName]

			return BodyPart == AssetId
		else
			return false
		end
	end

	function HumanoidDescriber:SetEmoteEquipped(Name: string, Equip: boolean, Slot: number?)
		local Data = self:GetData()

		if Slot == nil then
			if Equip then
				local PotentialSlots = {}

				for Count = 1, 8 do
					table.insert(PotentialSlots, Count)
				end

				for _, Emote in ipairs(Data.EquippedEmotes) do
					local PotentialSlotIndex = table.find(PotentialSlots, Emote.Slot)
					if PotentialSlotIndex then
						table.remove(PotentialSlots, PotentialSlotIndex)
					end
				end

				Slot = PotentialSlots[1] or 8
			else
				local _, Emote = self:GetEquippedEmote(Name)
				if Emote then
					Slot = Emote.Slot
				else
					Slot = 8
				end
			end
		end

		self:UpdateData(function(ModifierData)
			local ExistingIndex = self:GetEquippedEmoteBySlot(Slot)

			if ExistingIndex then
				table.remove(ModifierData.EquippedEmotes, ExistingIndex)
			end

			if Equip then
				table.insert(ModifierData.EquippedEmotes, {
					Name = Name,
					Slot = Slot,
				})
			end

			return ModifierData
		end)
	end

	function HumanoidDescriber:GetEquippedEmoteBySlot(Slot: number)
		local Data = self:GetData()

		for Index, Emote in ipairs(Data.EquippedEmotes) do
			if Emote.Slot == Slot then
				return Index, Emote
			end
		end

		return nil
	end

	function HumanoidDescriber:GetEquippedEmote(Name: string): (number?, EquippedEmote?)
		local Data = self:GetData()

		for Index, Emote in ipairs(Data.EquippedEmotes) do
			if Emote.Name == Name then
				return Index, Emote
			end
		end

		return nil
	end

	function HumanoidDescriber:SetEmote(Name: string, Emote: { number } | nil)
		self:UpdateData(function(Data)
			Data.Emotes[Name] = Emote
			return Data
		end)
	end

	function HumanoidDescriber:GetEmoteByAssetId(AssetId: number)
		local Data = self:GetData()

		for Name, Emote in pairs(Data.Emotes) do
			if table.find(Emote, AssetId) ~= nil then
				return Name, Emote
			end
		end

		return nil
	end

	function HumanoidDescriber:GetEmote(Name: string)
		local Data = self:GetData()

		for EmoteName, Emote in pairs(Data.Emotes) do
			if EmoteName == Name then
				return Emote
			end
		end

		return nil
	end

	function HumanoidDescriber:ToggleAccessory(Accessory: AccessorySpec)
		if self:GetAccessory(Accessory.AssetId) then
			self:RemoveAccessory(Accessory.AssetId)
		else
			self:AddAccessory(Accessory)
		end
	end

	function HumanoidDescriber:AddAccessory(Accessory: AccessorySpec)
		local AccessoryIndex = self:GetAccessory(Accessory.AssetId)

		if AccessoryIndex == nil then
			self:UpdateData(function(Data)
				table.insert(Data.Accessories, Accessory)
				return Data
			end)
		end
	end

	function HumanoidDescriber:RemoveAccessory(AssetId: number)
		local AccessoryIndex = self:GetAccessory(AssetId)

		if AccessoryIndex ~= nil then
			self:UpdateData(function(Data)
				table.remove(Data.Accessories, AccessoryIndex)
				return Data
			end)
		end
	end

	function HumanoidDescriber:GetAccessory(AssetId: number): (number?, AccessorySpec?)
		local Data = self:GetData()

		for Index, Accessory: AccessorySpec in ipairs(Data.Accessories) do
			if Accessory.AssetId == AssetId then
				return Index, Accessory
			end
		end

		return nil
	end

	function HumanoidDescriber:GetAssets(IgnoreBundleAssets: boolean?): { number }
		local Data: Data = self:GetData()

		local AssetIds = {
			Data.Face,
		}

		UnpackInto(
			AssetIds,
			Extract(Data.Accessories, function(_, Value)
				return Value.AssetId
			end)
		)
		UnpackInto(
			AssetIds,
			Extract(Data.Emotes, function(_, Value)
				return table.unpack(Value)
			end)
		)
		UnpackInto(
			AssetIds,
			Extract(Data.Clothing, function(_, AssetId)
				return AssetId
			end)
		)

		if not IgnoreBundleAssets then
			UnpackInto(
				AssetIds,
				Extract(Data.Animations, function(_, Value)
					return Value
				end)
			)
			UnpackInto(
				AssetIds,
				Extract(Data.BodyParts, function(_, Value)
					return Value
				end)
			)
		end

		local InvalidAssetIds = {}
		for Key, AssetId in ipairs(AssetIds) do
			if (AssetId == nil) or (AssetId == 0) or (table.find(AssetIds, AssetId, Key + 1) ~= nil) then
				table.insert(InvalidAssetIds, AssetId)
			end
		end

		for _, AssetId in ipairs(InvalidAssetIds) do
			local Index = table.find(AssetIds, AssetId)
			if Index ~= nil then
				table.remove(AssetIds, Index)
			end
		end

		return AssetIds
	end

	function HumanoidDescriber:ReadFromDescription(NewHumanoidDescription: HumanoidDescription)
		self.HumanoidDescription = NewHumanoidDescription

		local NewData: Data = {
			Face = self.HumanoidDescription.Face,
			Accessories = self.HumanoidDescription:GetAccessories(true),
			Emotes = self.HumanoidDescription:GetEmotes(),
			EquippedEmotes = self.HumanoidDescription:GetEquippedEmotes(),
			Scale = {
				BodyType = self.HumanoidDescription.BodyTypeScale,
				Depth = self.HumanoidDescription.DepthScale,
				Head = self.HumanoidDescription.HeadScale,
				Height = self.HumanoidDescription.HeightScale,
				Proportion = self.HumanoidDescription.ProportionScale,
				Width = self.HumanoidDescription.WidthScale,
			},
			BodyParts = {
				Head = self.HumanoidDescription.Head,
				Torso = self.HumanoidDescription.Torso,
				LeftLeg = self.HumanoidDescription.LeftLeg,
				RightLeg = self.HumanoidDescription.RightLeg,
				LeftArm = self.HumanoidDescription.LeftArm,
				RightArm = self.HumanoidDescription.RightArm,
			},
			Animations = {
				Idle = self.HumanoidDescription.IdleAnimation,
				Walk = self.HumanoidDescription.WalkAnimation,
				Run = self.HumanoidDescription.RunAnimation,
				Fall = self.HumanoidDescription.FallAnimation,
				Jump = self.HumanoidDescription.JumpAnimation,
				Mood = self.HumanoidDescription.MoodAnimation,
				Swim = self.HumanoidDescription.SwimAnimation,
				Climb = self.HumanoidDescription.ClimbAnimation,
			},
			Clothing = {
				Shirt = self.HumanoidDescription.Shirt,
				TShirt = self.HumanoidDescription.GraphicTShirt,
				Pants = self.HumanoidDescription.Pants,
			},
			BodyPartColors = {
				Head = self.HumanoidDescription.HeadColor,
				Torso = self.HumanoidDescription.TorsoColor,
				LeftLeg = self.HumanoidDescription.LeftLegColor,
				RightLeg = self.HumanoidDescription.RightLegColor,
				LeftArm = self.HumanoidDescription.LeftArmColor,
				RightArm = self.HumanoidDescription.RightArmColor,
			},
		}

		self:UpdateData(function()
			return NewData
		end, false)
	end

	function HumanoidDescriber:_UpdateHumanoidDescription()
		local Data = self:GetData()
		local Properties = {
			Face = Data.Face,

			BodyTypeScale = Data.Scale.BodyType,
			DepthScale = Data.Scale.Depth,
			HeadScale = Data.Scale.Head,
			HeightScale = Data.Scale.Height,
			ProportionScale = Data.Scale.Proportion,
			WidthScale = Data.Scale.Width,

			WalkAnimation = Data.Animations.Walk,
			RunAnimation = Data.Animations.Run,
			FallAnimation = Data.Animations.Fall,
			ClimbAnimation = Data.Animations.Climb,
			SwimAnimation = Data.Animations.Swim,
			IdleAnimation = Data.Animations.Idle,
			MoodAnimation = Data.Animations.Mood,
			JumpAnimation = Data.Animations.Jump,

			Head = Data.BodyParts.Head,
			Torso = Data.BodyParts.Torso,
			LeftArm = Data.BodyParts.LeftArm,
			RightArm = Data.BodyParts.RightArm,
			LeftLeg = Data.BodyParts.LeftLeg,
			RightLeg = Data.BodyParts.RightLeg,

			HeadColor = Data.BodyPartColors.Head,
			TorsoColor = Data.BodyPartColors.Torso,
			LeftArmColor = Data.BodyPartColors.LeftArm,
			RightArmColor = Data.BodyPartColors.RightArm,
			LeftLegColor = Data.BodyPartColors.LeftLeg,
			RightLegColor = Data.BodyPartColors.RightLeg,

			Shirt = Data.Clothing.Shirt,
			GraphicTShirt = Data.Clothing.TShirt,
			Pants = Data.Clothing.Pants,
		}

		for Name, Value in pairs(Properties) do
			if self.HumanoidDescription[Name] ~= Value then
				self.HumanoidDescription[Name] = Value
			end
		end

		self.HumanoidDescription:SetAccessories(Data.Accessories, true)
		self.HumanoidDescription:SetEmotes(Data.Emotes)
		self.HumanoidDescription:SetEquippedEmotes(Data.EquippedEmotes)
	end

	function HumanoidDescriber:UpdateData(ModifierCallback: (Data) -> Data, UpdateDescription: boolean?)
		if UpdateDescription == nil then
			UpdateDescription = true
		end

		local OldData = self:GetData()
		local NewData = ModifierCallback(self:GetData())

		if NewData ~= nil then
			self.Data = NewData

			if UpdateDescription then
				self:_UpdateHumanoidDescription()
			end

			self.Updated:Fire(OldData, NewData)
		else
			assert(false, "Data was not returned.")
		end
	end

	function HumanoidDescriber:GetData(): Data
		return TableUtil.Copy(self.Data, true)
	end

	function HumanoidDescriber:Destroy()
		self.Updated:Destroy()
	end

	HumanoidDescriber:ReadFromDescription(HumanoidDescription)

	return HumanoidDescriber
end

export type HumanoidDescriber = typeof(New(Instance.new("HumanoidDescription")))

return New


local function Instantiate(ClassOrInstance: string | Instance)
	local Object: Instance

	if typeof(ClassOrInstance) == "string" then
		Object = Instance.new(ClassOrInstance)
	elseif typeof(ClassOrInstance) == "Instance" then
		Object = ClassOrInstance
	end

	return function(Properties: { [string]: any | (typeof(Object)) -> any, Children: { Instance } })
		for Name, Value in pairs(Properties) do
			if typeof(Value) == "function" then
				Value = Value(Object)
			end

			if Name == "Children" then
				for _, Child in ipairs(Value) do
					Child.Parent = Object
				end
			else
				Object[Name] = Value
			end
		end

		return Object
	end
end

return Instantiate


local Players = game:GetService("Players")

local Instantiate = require(script.Parent.Instantiate)

local TYPE_CLASSES = {
	int = "IntValue",
	number = "NumberValue",
	string = "StringValue",
	boolean = "BoolValue",
	Instance = "ObjectValue",
	BrickColor = "BrickColorValue",
	Color3 = "Color3Value",
	Vector3 = "Vector3Value",
}

local Leaderboard = {}

function Leaderboard.Set(Player: Player, Name: string, Value: any)
	local ClassName: string? = TYPE_CLASSES[typeof(Value)]
	local Folder = Leaderboard.GetFolder(Player)
	local ObjectValue = Leaderboard.GetValueObject(Player, Name)

	assert(ClassName ~= nil, "Invalid value type")
	assert(
		(ObjectValue and (ObjectValue.ClassName == ClassName)) or true,
		"Cannot set existing value with conflicting type"
	)

	if Folder then
		if ObjectValue == nil then
			ObjectValue = Instantiate(ClassName) {
				Name = Name,
				Value = Value,
				Parent = Folder,
			}
		end

		if Value ~= nil then
			ObjectValue.Value = Value
		else
			ObjectValue:Destroy()
		end
	end
end

function Leaderboard.Get(Player: Player, Name: string): any?
	local ObjectValue = Leaderboard.GetValueObject(Player, Name)

	if ObjectValue then
		return ObjectValue.Value
	else
		return nil
	end
end

function Leaderboard.GetValueObject(Player: Player, Name: string): Instance?
	local Folder = Leaderboard.GetFolder(Player)

	if Folder then
		local ObjectValue = Folder:FindFirstChild(Name)
		if ObjectValue then
			return ObjectValue
		end
	end

	return nil
end

function Leaderboard.GetFolder(Player: Player): Folder?
	if not Player:IsDescendantOf(Players) then
		return
	end

	local ExistingFolder = Player:FindFirstChild("leaderstats")

	if ExistingFolder then
		return ExistingFolder
	else
		return Instantiate "Folder" {
			Name = "leaderstats",
			Parent = Player,
		}
	end
end

return Leaderboard


local Players = game:GetService("Players")

local Signal = require(script.Parent.Signal)

export type SessionData = { any }
export type MetaData = {
	CreationTime: number,
}
export type SessionProfile = {
	Data: SessionData,
	MetaData: MetaData,
	UpdateData: (SessionProfile, SessionData) -> SessionData,
	DataUpdated: Signal.Signal<SessionData, SessionData>,
}

local SessionStore = {
	Profiles = {},
	ProfileCreated = Signal.new(),
	ProfileDeleted = Signal.new(),
}

function SessionStore:GetProfileSafe(Player: Player): SessionProfile
	return self:GetProfile(Player) or self:CreateProfile(Player)
end

function SessionStore:GetProfile(Player: Player): SessionProfile?
	return self.Profiles[Player.UserId]
end

function SessionStore:DeleteProfile(UserId: number)
	if self.Profiles[UserId] ~= nil then
		self.Profiles[UserId] = nil
		self.ProfileDeleted:Fire(UserId)
	end
end

function SessionStore:CreateProfile(Player: Player): SessionProfile?
	assert(self.DataTemplate, "Data template has not been set.")
	if self.Profiles[Player.UserId] ~= nil then
		return
	end

	local Profile = {
		Data = table.clone(self.DataTemplate),
		MetaData = {
			CreationTime = os.time(),
		},
		DataUpdated = Signal.new(),
	}

	function Profile:UpdateData(NewData: SessionData)
		local OldData = table.clone(self.Data)
		self.Data = NewData

		self.DataUpdated:Fire(OldData, NewData)
	end

	self.Profiles[Player.UserId] = Profile

	table.freeze(Profile)

	return Profile
end

function SessionStore:SetTemplate(DataTemplate: SessionData)
	self.DataTemplate = DataTemplate
end

Players.PlayerRemoving:Connect(function(Player)
	SessionStore:DeleteProfile(Player.UserId)
end)

return SessionStore


local function DeepEquals(TableA: { [any]: any }, TableB: { [any]: any })
	for Key, Value in pairs(TableA) do
		if typeof(Value) == "table" then
			if not DeepEquals(TableB[Key], Value) then
				return false
			end
		elseif Value ~= TableB[Key] then
			return false
		end
	end

	return true
end

return DeepEquals


local TableUtil = require(script.Parent.TableUtil)
local DeepEquals = require(script.DeepEquals)

local function CopyIfTable(Value)
	if typeof(Value) == "table" then
		return TableUtil.Copy(Value, true)
	else
		return Value
	end
end

local function Equals(A, B)
	if (typeof(A) == "table") and (typeof(B) == "table") then
		return DeepEquals(A, B)
	else
		return A == B
	end
end

local function ProcessObserver(Observer, OldValue, NewValue, Force: boolean?)
	if Observer.Specifier ~= nil then
		local SpecificValue, NewSpecificValue = Observer.Specifier(OldValue), Observer.Specifier(NewValue)

		if (not Force) and Equals(SpecificValue, NewSpecificValue) then
			return
		end

		Observer.Callback(SpecificValue, NewSpecificValue)
	else
		Observer.Callback(OldValue, NewValue)
	end
end

local function NewStator<Value>(Value: Value, Updaters: { [string]: (Value) -> Value })
	local Stator = {
		Value = Value,
		Updaters = Updaters,
		Observers = {},
	}

	function Stator:Observe(Callback: (Value, Value) -> (), Specifier: ((Value) -> any)?)
		local InitialValue = Stator:Get()
		local Observer = {
			Callback = Callback,
			Specifier = Specifier,
		}

		table.insert(Stator.Observers, Observer)

		ProcessObserver(Observer, InitialValue, InitialValue)

		return function()
			local Index = table.find(Stator.Observers, Observer)
			if Index ~= nil then
				table.remove(Stator.Observers, Index)
			end
		end
	end

	function Stator:Update(Updater)
		local OldValue = Stator:Get()
		local NewValue = Updater(CopyIfTable(OldValue))

		Stator.Value = NewValue

		for _, Observer in ipairs(Stator.Observers) do
			ProcessObserver(Observer, OldValue, NewValue)
		end
	end

	function Stator:Get(): Value
		return CopyIfTable(Stator.Value)
	end

	return Stator
end

return NewStator


local CollectionService = game:GetService("CollectionService")

local Signal = require(script.Parent.Signal)

local function NewWrapper<Value, Destroy>(Construct: (Instance) -> (Value, Destroy))
	assert(Construct ~= nil, "Construct argument not provided")

	export type Wrap = {
		Value: Value,
		Destroy: () -> (),
	}

	local InstanceWrapped: Signal.Signal<Instance, Value> = Signal.new()
	local InstanceUnwrapped: Signal.Signal<Instance> = Signal.new()

	local Wrapper = {
		Wraps = {},
		Tags = {},
		InstanceWrapped = InstanceWrapped,
		InstanceUnwrapped = InstanceUnwrapped,
	}

	function Wrapper:Untag(Tag: string)
		local TagEntry = Wrapper.Tags[Tag]

		if TagEntry ~= nil then
			for _, Listener in ipairs(TagEntry.Listeners) do
				Listener:Disconnect()
			end
		end

		Wrapper.Tags[Tag] = nil
	end

	function Wrapper:Tag(Tag: string)
		local TagEntry = {
			Listeners = {
				CollectionService:GetInstanceAddedSignal(Tag):Connect(function(Instance)
					Wrapper:Wrap(Instance)
				end),
				CollectionService:GetInstanceRemovedSignal(Tag):Connect(function(Instance)
					Wrapper:Unwrap(Instance)
				end),
			},
		}

		for _, Instance in ipairs(CollectionService:GetTagged(Tag)) do
			Wrapper:Wrap(Instance)
		end

		Wrapper.Tags[Tag] = TagEntry
	end

	function Wrapper:GetAll(): { Value }
		local WrapValues = {}

		for _, Wrap in pairs(Wrapper.Wraps) do
			table.insert(WrapValues, Wrap.Value)
		end

		return WrapValues
	end

	function Wrapper:Get(Instance: Instance): (Value?, Destroy?)
		local Wrap = Wrapper.Wraps[Instance]

		if Wrap ~= nil then
			return Wrap.Value, Wrap.Destroy
		else
			return nil
		end
	end

	function Wrapper:Unwrap(Instance: Instance)
		local _, Destroy = Wrapper:Get(Instance)

		if Destroy ~= nil then
			Destroy()

			Wrapper.InstanceUnwrapped:Fire(Instance)
		end
	end

	function Wrapper:Wrap(Instance: Instance): Value
		local ExistingWrap = Wrapper:Get(Instance)

		if ExistingWrap then
			return ExistingWrap
		else
			local Value, Destroy = Construct(Instance)

			local Wrap: Wrap = {
				Value = Value,
				Destroy = function()
					Destroy()

					Wrapper.Wraps[Instance] = nil
				end,
			}
			Wrapper.Wraps[Instance] = Wrap

			Wrapper.InstanceWrapped:Fire(Instance, Value)

			return Value
		end
	end

	function Wrapper:Destroy()
		for _, Wrap in pairs(Wrapper.Wraps) do
			Wrap.Destroy()
			Wrap.Value = nil
		end

		for Tag, _ in pairs(Wrapper.Tags) do
			Wrapper:Untag(Tag)
		end

		Wrapper.InstanceWrapped:Destroy()
		Wrapper.InstanceUnwrapped:Destroy()

		Wrapper.Wraps = {}
	end

	return Wrapper
end

return NewWrapper


local fs = require("@lune/fs")
local serde = require("@lune/serde")

local function convertDependencies(pesdeDependencies)
	local wallyDependencies = {}

	for alias, dependency in pairs(pesdeDependencies) do
		if next(dependency) ~= nil then
			assert(dependency.name == nil, `Package relies on Pesde package.`)

			if dependency.wally and dependency.version then
				wallyDependencies[alias] = `{dependency.wally}@{dependency.version}`
			end
		end
	end

	return wallyDependencies
end

local function convertName(pesdeName)
	return string.gsub(string.gsub(pesdeName, "avafe/", "imavafe/"), "_", "-")
end

local function convertManifest(pesdeToml)
	local wallyInfo = {
		package = {
			name = convertName(pesdeToml.name),
			version = pesdeToml.version,

			license = pesdeToml.license,
			authors = pesdeToml.authors,

			registry = pesdeToml["wally_indices"].default,
			realm = "shared",
		},
		dependencies = convertDependencies(pesdeToml.dependencies),
	}

	return wallyInfo
end

local function convertProject(path: string)
	if fs.isFile(`{path}/pesde.toml`) then
		local pesdeToml = serde.decode("toml", fs.readFile(`{path}/pesde.toml`))
		local wallyToml = serde.encode("toml", convertManifest(pesdeToml), true)

		fs.writeFile(`{path}/src/wally.toml`, wallyToml)
	else
		assert(false, `pesde.toml does not exist for {path}`)
	end
end

for _, moduleName in ipairs(fs.readDir("modules")) do
	convertProject(`modules/{moduleName}`)
end


local Tests = {
	BuiltIn = {},
	KillCallbacks = {},
}

function Tests:Run(ModuleScript: ModuleScript): () -> ()
	local NewModule = ModuleScript:Clone()
	NewModule.Parent = ModuleScript.Parent

	local Kill = require(NewModule)
	table.insert(Tests.KillCallbacks, Kill)

	return Kill
end

function Tests:RunAll(ModuleScripts: { ModuleScript })
	for _, Module in pairs(ModuleScripts) do
		Tests:Run(Module)
	end
end

function Tests:KillAll()
	for _, Kill in ipairs(Tests.KillCallbacks) do
		Kill()
	end

	Tests.KillCallbacks = {}
end

for _, Module in ipairs(script.Parent.Modules:GetChildren()) do
	local TestModule = Module:FindFirstChild("Test")
	if TestModule then
		Tests.BuiltIn[Module.Name] = TestModule
	end
end

return Tests


-- V.2.2

--[[Created by SteadyOn

	THIS MODULE MUST BE IN REPLICATEDSTORAGE - YOU NEED TO REQUIRE THIS MODULE SOMEWHERE ON THE CLIENT:
	require(game.ReplicatedStorage.ReplicatedTweening)

	Documentation:

	:Create(instance [Instance], TweenInfo [TweenInfo Object], PropertyTable [Table]) [Tween]
	Parameters are exactly the same as TweenService:Create(), it returns a fake Tween object with Play,
	Pause and Stop functions.

	Tween:Play(Yield [Boolean, optional], Player [Player Object, optional])
	Runs the tween. The player parameter will only play the tween for that specific player.
	The Yield parameter specifies whether the function should yield until the tween has completed or not.

	Tween:QueuePlay(Yield [Boolean, optional], Player [Player Object, optional]) - Add the tween to a tween queue which will start
	playing the queue automatically immediately after the previous tween on that instance completes. Behaves exactly the same way as
	Tween:Play() once started, except the initial firing of the tween is managed on the client. For this reason, best practice is to
	fire this event as close to when you would like it to be played on the client to maintain alignment between tweens. If fired
	multiple times in a short time frame, this may result in clients becoming out of sync over time.

	Tween:Stop(Player [Player Object, optional])
	Stops the tween. The player parameter will only stop the tween for that specific player.

	Tween:Pause(Player [Player Object, optional])
	Pauses the tween. The player parameter will only pause the tween for that specific player.

	Tutorial:

	To set up, you just need to put 'require(game.ReplicatedStorage.ReplicatedTweening)' somewhere in
	a LocalScript, if you don't have one yet, just create a new one. Also make sure to put this module
	into ReplicatedStorage.

	To use this module, first require it (e.g. require(game.ReplicatedStorage.ReplicatedTweening)).
	To get the tween object you must first call :GetTweenObject(), this returns a fake Tween object.
	Use this object to play, stop and pause the tween by using Play(), Stop(), Pause()
	as functions of the Tween object (e.g. Tween:Play()).

	You can also enter a player as the first parameter of each of these functions
	to stop, play or pause the tween only for that specific player.

	Example code:
	local CustomTweenService = require(game.ReplicatedStorage.ReplicatedTweening)
	local tweenInfo = TweenInfo.new(2, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)

	local tween = CustomTweenService:Create(game.Workspace.Part, tweenInfo, {CFrame = CFrame.new(Vector3.new(0,0,0))})
	tween:Play()
--]]

local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
local TweenEvent = script:FindFirstChild("TweenEvent")
local HttpService = game:GetService("HttpService")

local TweenModule = {}
local latestFinish = {} -- this table operates on both the client and the server, server side it only stores GLOBAL tweens, local side it stores every local tween.

--| if there is no tween event, it will be created and parented to the script.
if TweenEvent == nil then
	if RunService:IsServer() then
		TweenEvent = Instance.new("RemoteEvent")
		TweenEvent.Name = "TweenEvent"
		TweenEvent.Parent = script
	else
		TweenEvent = script:WaitForChild("TweenEvent")
	end
end

--| Converting TweenInfo to an array is needed to pass it through the remote event:
local function TweenInfo_To_Table(tInfo: TweenInfo)
	local info = {}
	info[1] = tInfo.Time or 1
	info[2] = tInfo.EasingStyle or Enum.EasingStyle.Quad
	info[3] = tInfo.EasingDirection or Enum.EasingDirection.Out
	info[4] = tInfo.RepeatCount or 0
	info[5] = tInfo.Reverses or false
	info[6] = tInfo.DelayTime or 0
	return info
end

--| When the TweenInfo array is received on the client, we will need to convert it back to TweenInfo:
local function Table_To_TweenInfo(tbl)
	return TweenInfo.new(unpack(tbl))
end

--| Will change server properties for the targetted instances after client tweens are done:
local function serverAssignProperties(instance: Instance, properties: {[string]: any})
	--print("Assigning server properties..")
	for property, value in pairs (properties) do
		--print(string.format("Assigned %s to %s", property, tostring(value)))
		instance[property] = value
	end
end

local function getInstanceCurrentSimulatedState(instance: Instance, properties : {[any]: any})
	local state = {}
	for property, _  in pairs(properties) do
		state[property] = instance[property]
	end
	return state
end

function TweenModule:Create(instance: Instance, tInfo: TweenInfo, propertyTable: {[string]: any}, SimulateServerTween: boolean?)
	local tweenMaster = {}
	tweenMaster.DontUpdate = {} -- table of specific players that it stopped for part way.
	tweenMaster._SafeTweenInfo = tInfo
	tweenMaster._TweenID = HttpService:GenerateGUID(false)
	tweenMaster.HasFinished = false
	tInfo = TweenInfo_To_Table(tInfo)

	if SimulateServerTween then
		tweenMaster._SimulateTween = true

		-- / Create a copy of the instance in Script.SimulatedTweensFolder
		local SimulatedTweensFolder = script:FindFirstChild("SimulatedTweensFolder")
		if not SimulatedTweensFolder then
			SimulatedTweensFolder = Instance.new("Folder")
			SimulatedTweensFolder.Name = "SimulatedTweensFolder"
			SimulatedTweensFolder.Parent = script
		end

		tweenMaster.SimulatedInstance = instance:Clone()
		tweenMaster.SimulatedInstance.Parent = SimulatedTweensFolder

		tweenMaster.SimulatedTween = TweenService:Create(tweenMaster.SimulatedInstance, tweenMaster._SafeTweenInfo, propertyTable)
	end

	local function Play(Yield, SpecificClient, Queue) -- this is on it's own as it needs to be called by both QueuePlay and Play
		local waitTime = tInfo[1] * (tInfo[4] ~= 0 and tInfo[4] or 1) * (tInfo[5] and 2 or 1)
		local finishTime = workspace:GetServerTimeNow() + waitTime

		latestFinish[instance] = latestFinish[instance] or workspace:GetServerTimeNow() -- cannot be nil.
		Queue = Queue or false
		tweenMaster.Paused = false

		if SpecificClient == nil and not Queue then
			latestFinish[instance] = finishTime -- adds an entry to array with finish time of this tween (used for queueing)
			TweenEvent:FireAllClients("RunTween", instance, tInfo, propertyTable, tweenMaster._TweenID, nil, workspace:GetServerTimeNow())
		elseif Queue and SpecificClient == nil then -- deal with queued tweens
			waitTime = waitTime + (latestFinish[instance] - workspace:GetServerTimeNow())
			latestFinish[instance] = finishTime + (latestFinish[instance] - workspace:GetServerTimeNow()) -- adds an entry to array with finish time of this tween (used for queueing)
			TweenEvent:FireAllClients("QueueTween", instance, tInfo, propertyTable, tweenMaster._TweenID, nil, workspace:GetServerTimeNow())
		elseif Queue and SpecificClient then
			TweenEvent:FireClient(SpecificClient, "QueueTween", instance, tInfo, propertyTable, tweenMaster._TweenID, nil, workspace:GetServerTimeNow()) -- queue tween for specific player
		elseif SpecificClient then
			TweenEvent:FireClient(SpecificClient, "RunTween", instance, tInfo, propertyTable, tweenMaster._TweenID, nil, workspace:GetServerTimeNow()) -- play tween for specific player
		end

		if tweenMaster._SimulateTween then
			-- / Run the tween on the simulated instance
			tweenMaster.SimulatedTween:Play()

			tweenMaster._PauseSimulatedTween = function()
				tweenMaster.SimulatedTween:Pause()
			end

			tweenMaster._StopSimulatedTween = function()
				tweenMaster.SimulatedTween:Cancel()
			end

			tweenMaster._ResumeSimulatedTween = function()
				tweenMaster.SimulatedTween:Play()
			end

			tweenMaster.SimulatedTween.Completed:Once(function() -- Clear up the simulated instance after the tween has completed
				tweenMaster.SimulatedInstance:Destroy()
			end)
		end

		local function applyTween()
			local i, existingFinish = 0, latestFinish[instance]
			repeat task.wait(0.05) i += 0.05 until i >= waitTime or tweenMaster.Stopped
			if latestFinish[instance] == existingFinish then
				latestFinish[instance] = nil
			end
			if not tweenMaster.Paused then
				if not tInfo[5] then
					serverAssignProperties(instance, propertyTable)
				end
				tweenMaster.HasFinished = true
			end
		end

		if SpecificClient == nil then
			if Yield then
				applyTween()
			else
				task.spawn(applyTween)
			end
		end
	end

	function tweenMaster:Play(Yield, SpecificClient)
		Play(Yield, SpecificClient)
	end

	function tweenMaster:QueuePlay(Yield, SpecificClient)
		Play(Yield, SpecificClient, true)
	end

	function tweenMaster:Pause(SpecificClient)
		if tweenMaster._SimulateTween then
			tweenMaster._CurrentExpectedState = getInstanceCurrentSimulatedState(tweenMaster.SimulatedInstance, propertyTable)
		end

		if tweenMaster._SimulateTween then
			tweenMaster._PauseSimulatedTween()
		end

		if SpecificClient == nil then
			tweenMaster.Paused = true
			TweenEvent:FireAllClients("PauseTween", instance, nil, nil, tweenMaster._TweenID, tweenMaster._CurrentExpectedState, workspace:GetServerTimeNow())
		else
			table.insert(tweenMaster.DontUpdate, SpecificClient)
			TweenEvent:FireClient(SpecificClient, "PauseTween", instance, nil, nil,  tweenMaster._TweenID, tweenMaster._CurrentExpectedState, workspace:GetServerTimeNow())
		end

	end

	function tweenMaster:Stop(SpecificClient)
		if tweenMaster._SimulateTween then
			tweenMaster._CurrentExpectedState = getInstanceCurrentSimulatedState(tweenMaster.SimulatedInstance, propertyTable)
		end

		if tweenMaster._SimulateTween then
			tweenMaster._StopSimulatedTween()
		end

		if SpecificClient == nil then
			tweenMaster.Stopped = true
			TweenEvent:FireAllClients("StopTween", instance, nil, nil,  tweenMaster._TweenID, tweenMaster._CurrentExpectedState, workspace:GetServerTimeNow())
		else
			TweenEvent:FireClient(SpecificClient, "StopTween", instance, nil, nil,  tweenMaster._TweenID, tweenMaster._CurrentExpectedState, workspace:GetServerTimeNow())
		end

	end
	return tweenMaster
end

if RunService:IsClient() then -- OnClientEvent only works clientside
	local runningTweens = {}

	local runningTweensById = {}
	TweenEvent.OnClientEvent:Connect(function(purpose, instance, tInfo, propertyTable, tweenID, expectedProperties, expectedStartTime)
		if not instance then return end
		local tInfoTable = tInfo
		if tInfo ~= nil then
			tInfo = Table_To_TweenInfo(tInfo)
		end

		if expectedProperties and not tInfo[5] then
			serverAssignProperties(instance, expectedProperties)
		end

		local shortenBy = 0
		if expectedStartTime and tInfo then
			shortenBy = workspace:GetServerTimeNow() - expectedStartTime

			-- Recreate TweenInfo with the shortened time using tInfoTable
			tInfo = TweenInfo.new(tInfoTable[1] - shortenBy, tInfoTable[2], tInfoTable[3], tInfoTable[4], tInfoTable[5], tInfoTable[6])
			-- print ("Shortened by", shortenBy)
		end

		local function runTween(queued)
			local finishTime = workspace:GetServerTimeNow() + tInfo.Time
			latestFinish[instance] = latestFinish[instance] or workspace:GetServerTimeNow() -- cannot be nil.

			local existingFinish = latestFinish[instance]
			if queued and latestFinish[instance] >= workspace:GetServerTimeNow() then
				local waitTime = (latestFinish[instance] - workspace:GetServerTimeNow())
				latestFinish[instance] = finishTime + waitTime
				existingFinish = latestFinish[instance]
				task.wait(waitTime)
			else
				latestFinish[instance] = finishTime
			end

			if runningTweens[instance] ~= nil and runningTweens[instance].PlaybackState ~= Enum.PlaybackState.Paused and runningTweensById[instance] ~= tweenID then -- im aware this will pick up paused tweens, however it doesn't matter (it did matter, i've fixed it now)
				runningTweens[instance]:Cancel() -- stop previously running tween to run this one
				warn(string.format('Cancelled a previously running tween for instance "%s" tor run a new requested one.', instance.Name))
			end

			local tween
			if runningTweensById[instance] == tweenID then
				tween = runningTweens[instance]
			else
				tween = TweenService:Create(instance, tInfo, propertyTable)
			end
			runningTweens[instance] = tween
			tween:Play()
			runningTweensById[instance] = tweenID

			--print("TweenStarted",workspace:GetServerTimeNow(),existingFinish)
			tween.Completed:Wait()
			--print("TweenComplete",workspace:GetServerTimeNow(),existingFinish)
			if latestFinish[instance] == existingFinish then
				latestFinish[instance] = nil -- clear memory if this instance hasn't already been retweened.
			end

			if runningTweens[instance] == tween then -- make sure it hasn't changed to a different tween
				runningTweens[instance] = nil -- remove to save memory
			end
		end
		local tween = runningTweens[instance]
		if purpose == "RunTween" then
			runTween()
		elseif purpose == "QueueTween" then
			runTween(true) -- run as a queued tween
		elseif purpose == "StopTween" then
			if tween ~= nil then -- check that the tween exists
				tween:Cancel() -- stop the tween
				runningTweens[instance] = nil -- delete from table
			else
				warn("Tween being stopped does not exist or the instance itself.")
			end
		elseif purpose == "PauseTween" then
			if tween ~= nil then -- check that the tween exists
				tween:Pause() -- pause the tween
			else
				warn("Tween being paused does not exist.")
			end
		end
	end)
end

return TweenModule


-- This codegen template *must* be 100% compatible with Lua 5.1x+, NOT just Luau
-- With that being said, this explains the weird cflow in some parts

return [[
-- ++++++++ WAX BUNDLED DATA BELOW ++++++++ --

-- Will be used later for getting flattened globals
local ImportGlobals

-- Holds direct closure data (defining this before the DOM tree for line debugging etc)
local ClosureBindings = ${ClosureBindings} -- [RefId] = Closure

-- Holds the actual DOM data
local ObjectTree = ${ObjectTree}

-- Line offsets for debugging (only included when minifyTables is false)
local LineOffsets = ${LineOffsets}

-- Misc AOT variable imports
local WaxVersion = "${Version}"
local EnvName = "${EnvName}"

-- ++++++++ RUNTIME IMPL BELOW ++++++++ --

-- Localizing certain libraries and built-ins for runtime efficiency
local string, task, setmetatable, error, next, table, unpack, coroutine, script, type, require, pcall, tostring, tonumber, _VERSION =
      string, task, setmetatable, error, next, table, unpack, coroutine, script, type, require, pcall, tostring, tonumber, _VERSION

local table_insert = table.insert
local table_remove = table.remove
local table_freeze = table.freeze or function(t) return t end -- lol

local coroutine_wrap = coroutine.wrap

local string_sub = string.sub
local string_match = string.match
local string_gmatch = string.gmatch

-- The Lune runtime has its own `task` impl, but it must be imported by its builtin
-- module path, "@lune/task"
if _VERSION and string_sub(_VERSION, 1, 4) == "Lune" then
    local RequireSuccess, LuneTaskLib = pcall(require, "@lune/task")
    if RequireSuccess and LuneTaskLib then
        task = LuneTaskLib
    end
end

local task_defer = task and task.defer

-- If we're not running on the Roblox engine, we won't have a `task` global
local Defer = task_defer or function(f, ...)
    coroutine_wrap(f)(...)
end

-- ClassName "IDs"
local ClassNameIdBindings = {
    [1] = "Folder",
    [2] = "ModuleScript",
    [3] = "Script",
    [4] = "LocalScript",
    [5] = "StringValue",
}

local RefBindings = {} -- [RefId] = RealObject

local ScriptClosures = {}
local ScriptClosureRefIds = {} -- [ScriptClosure] = RefId
local StoredModuleValues = {}
local ScriptsToRun = {}

-- wax.shared __index/__newindex
local SharedEnvironment = {}

-- We're creating 'fake' instance refs soley for traversal of the DOM for require() compatibility
-- It's meant to be as lazy as possible
local RefChildren = {} -- [Ref] = {ChildrenRef, ...}

-- Implemented instance methods
local InstanceMethods = {
    GetFullName = { {}, function(self)
        local Path = self.Name
        local ObjectPointer = self.Parent

        while ObjectPointer do
            Path = ObjectPointer.Name .. "." .. Path

            -- Move up the DOM (parent will be nil at the end, and this while loop will stop)
            ObjectPointer = ObjectPointer.Parent
        end

        return Path
    end},

    GetChildren = { {}, function(self)
        local ReturnArray = {}

        for Child in next, RefChildren[self] do
            table_insert(ReturnArray, Child)
        end

        return ReturnArray
    end},

    GetDescendants = { {}, function(self)
        local ReturnArray = {}

        for Child in next, RefChildren[self] do
            table_insert(ReturnArray, Child)

            for _, Descendant in next, Child:GetDescendants() do
                table_insert(ReturnArray, Descendant)
            end
        end

        return ReturnArray
    end},

    FindFirstChild = { {"string", "boolean?"}, function(self, name, recursive)
        local Children = RefChildren[self]

        for Child in next, Children do
            if Child.Name == name then
                return Child
            end
        end

        if recursive then
            for Child in next, Children do
                -- Yeah, Roblox follows this behavior- instead of searching the entire base of a
                -- ref first, the engine uses a direct recursive call
                return Child:FindFirstChild(name, true)
            end
        end
    end},

    FindFirstAncestor = { {"string"}, function(self, name)
        local RefPointer = self.Parent
        while RefPointer do
            if RefPointer.Name == name then
                return RefPointer
            end

            RefPointer = RefPointer.Parent
        end
    end},

    -- Just to implement for traversal usage
    WaitForChild = { {"string", "number?"}, function(self, name)
        return self:FindFirstChild(name)
    end},
}

-- "Proxies" to instance methods, with err checks etc
local InstanceMethodProxies = {}
for MethodName, MethodObject in next, InstanceMethods do
    local Types = MethodObject[1]
    local Method = MethodObject[2]

    local EvaluatedTypeInfo = {}
    for ArgIndex, TypeInfo in next, Types do
        local ExpectedType, IsOptional = string_match(TypeInfo, "^([^%?]+)(%??)")
        EvaluatedTypeInfo[ArgIndex] = {ExpectedType, IsOptional}
    end

    InstanceMethodProxies[MethodName] = function(self, ...)
        if not RefChildren[self] then
            error("Expected ':' not '.' calling member function " .. MethodName, 2)
        end

        local Args = {...}
        for ArgIndex, TypeInfo in next, EvaluatedTypeInfo do
            local RealArg = Args[ArgIndex]
            local RealArgType = type(RealArg)
            local ExpectedType, IsOptional = TypeInfo[1], TypeInfo[2]

            if RealArg == nil and not IsOptional then
                error("Argument " .. RealArg .. " missing or nil", 3)
            end

            if ExpectedType ~= "any" and RealArgType ~= ExpectedType and not (RealArgType == "nil" and IsOptional) then
                error("Argument " .. ArgIndex .. " expects type \"" .. ExpectedType .. "\", got \"" .. RealArgType .. "\"", 2)
            end
        end

        return Method(self, ...)
    end
end

local function CreateRef(className, name, parent)
    -- `name` and `parent` can also be set later by the init script if they're absent

    -- Extras
    local StringValue_Value

    -- Will be set to RefChildren later aswell
    local Children = setmetatable({}, {__mode = "k"})

    -- Err funcs
    local function InvalidMember(member)
        error(member .. " is not a valid (virtual) member of " .. className .. " \"" .. name .. "\"", 3)
    end
    local function ReadOnlyProperty(property)
        error("Unable to assign (virtual) property " .. property .. ". Property is read only", 3)
    end

    local Ref = {}
    local RefMetatable = {}

    RefMetatable.__metatable = false

    RefMetatable.__index = function(_, index)
        if index == "ClassName" then -- First check "properties"
            return className
        elseif index == "Name" then
            return name
        elseif index == "Parent" then
            return parent
        elseif className == "StringValue" and index == "Value" then
            -- Supporting StringValue.Value for Rojo .txt file conv
            return StringValue_Value
        else -- Lastly, check "methods"
            local InstanceMethod = InstanceMethodProxies[index]

            if InstanceMethod then
                return InstanceMethod
            end
        end

        -- Next we'll look thru child refs
        for Child in next, Children do
            if Child.Name == index then
                return Child
            end
        end

        -- At this point, no member was found; this is the same err format as Roblox
        InvalidMember(index)
    end

    RefMetatable.__newindex = function(_, index, value)
        -- __newindex is only for props fyi
        if index == "ClassName" then
            ReadOnlyProperty(index)
        elseif index == "Name" then
            name = value
        elseif index == "Parent" then
            -- We'll just ignore the process if it's trying to set itself
            if value == Ref then
                return
            end

            if parent ~= nil then
                -- Remove this ref from the CURRENT parent
                RefChildren[parent][Ref] = nil
            end

            parent = value

            if value ~= nil then
                -- And NOW we're setting the new parent
                RefChildren[value][Ref] = true
            end
        elseif className == "StringValue" and index == "Value" then
            -- Supporting StringValue.Value for Rojo .txt file conv
            StringValue_Value = value
        else
            -- Same err as __index when no member is found
            InvalidMember(index)
        end
    end

    RefMetatable.__tostring = function()
        return name
    end

    setmetatable(Ref, RefMetatable)

    RefChildren[Ref] = Children

    if parent ~= nil then
        RefChildren[parent][Ref] = true
    end

    return Ref
end

-- Create real ref DOM from object tree
local function CreateRefFromObject(object, parent)
    local RefId = object[1]
    local ClassNameId = object[2]
    local Properties = object[3] -- Optional
    local Children = object[4] -- Optional

    local ClassName = ClassNameIdBindings[ClassNameId]

    local Name = Properties and table_remove(Properties, 1) or ClassName

    local Ref = CreateRef(ClassName, Name, parent) -- 3rd arg may be nil if this is from root
    RefBindings[RefId] = Ref

    if Properties then
        for PropertyName, PropertyValue in next, Properties do
            Ref[PropertyName] = PropertyValue
        end
    end

    if Children then
        for _, ChildObject in next, Children do
            CreateRefFromObject(ChildObject, Ref)
        end
    end

    return Ref
end

local RealObjectRoot = CreateRef("Folder", "[" .. EnvName .. "]")
for _, Object in next, ObjectTree do
    CreateRefFromObject(Object, RealObjectRoot)
end

-- Now we'll set script closure refs and check if they should be ran as a BaseScript
for RefId, Closure in next, ClosureBindings do
    local Ref = RefBindings[RefId]

    ScriptClosures[Ref] = Closure
    ScriptClosureRefIds[Ref] = RefId

    local ClassName = Ref.ClassName
    if ClassName == "LocalScript" or ClassName == "Script" then
        table_insert(ScriptsToRun, Ref)
    end
end

local function LoadScript(scriptRef)
    local ScriptClassName = scriptRef.ClassName

    -- First we'll check for a cached module value (packed into a tbl)
    local StoredModuleValue = StoredModuleValues[scriptRef]
    if StoredModuleValue and ScriptClassName == "ModuleScript" then
        return unpack(StoredModuleValue)
    end

    local Closure = ScriptClosures[scriptRef]

    local function FormatError(originalErrorMessage)
        originalErrorMessage = tostring(originalErrorMessage)

        local VirtualFullName = scriptRef:GetFullName()

        -- Check for vanilla/Roblox format
        local OriginalErrorLine, BaseErrorMessage = string_match(originalErrorMessage, "[^:]+:(%d+): (.+)")

        if not OriginalErrorLine or not LineOffsets then
            return VirtualFullName .. ":*: " .. (BaseErrorMessage or originalErrorMessage)
        end

        OriginalErrorLine = tonumber(OriginalErrorLine)

        local RefId = ScriptClosureRefIds[scriptRef]
        local LineOffset = LineOffsets[RefId]

        local RealErrorLine = OriginalErrorLine - LineOffset + 1
        if RealErrorLine < 0 then
            RealErrorLine = "?"
        end

        return VirtualFullName .. ":" .. RealErrorLine .. ": " .. BaseErrorMessage
    end

    -- If it's a BaseScript, we'll just run it directly!
    if ScriptClassName == "LocalScript" or ScriptClassName == "Script" then
        local RunSuccess, ErrorMessage = xpcall(Closure, function(msg)
            return debug.traceback(msg, 2)
        end)
        if not RunSuccess then
            error(FormatError(ErrorMessage), 0)
        end
    else
        local PCallReturn = {xpcall(Closure, function(msg)
            return debug.traceback(msg, 2)
        end)}

        local RunSuccess = table_remove(PCallReturn, 1)
        if not RunSuccess then
            local ErrorMessage = table_remove(PCallReturn, 1)
            error(FormatError(ErrorMessage), 0)
        end

        StoredModuleValues[scriptRef] = PCallReturn
        return unpack(PCallReturn)
    end
end

-- We'll assign the actual func from the top of this output for flattening user globals at runtime
-- Returns (in a tuple order): wax, script, require
function ImportGlobals(refId)
    local ScriptRef = RefBindings[refId]

    local function RealCall(f, ...)
        local PCallReturn = {xpcall(f, function(msg)
            return debug.traceback(msg, 2)
        end, ...)}

        local CallSuccess = table_remove(PCallReturn, 1)
        if not CallSuccess then
            error(PCallReturn[1], 3)
        end

        return unpack(PCallReturn)
    end

    -- `wax.shared` index
    local WaxShared = table_freeze(setmetatable({}, {
        __index = SharedEnvironment,
        __newindex = function(_, index, value)
            SharedEnvironment[index] = value
        end,
        __len = function()
            return #SharedEnvironment
        end,
        __iter = function()
            return next, SharedEnvironment
        end,
    }))

    local Global_wax = table_freeze({
        -- From AOT variable imports
        version = WaxVersion,
        envname = EnvName,

        shared = WaxShared,

        -- "Real" globals instead of the env set ones
        script = script,
        require = require,
    })

    local Global_script = ScriptRef

    local function Global_require(module, ...)
        local ModuleArgType = type(module)

        local ErrorNonModuleScript = "Attempted to call require with a non-ModuleScript"
        local ErrorSelfRequire = "Attempted to call require with self"

        if ModuleArgType == "table" and RefChildren[module]  then
            if module.ClassName ~= "ModuleScript" then
                error(ErrorNonModuleScript, 2)
            elseif module == ScriptRef then
                error(ErrorSelfRequire, 2)
            end

            return LoadScript(module)
        elseif ModuleArgType == "string" and string_sub(module, 1, 1) ~= "@" then
            -- The control flow on this SUCKS

            if #module == 0 then
                error("Attempted to call require with empty string", 2)
            end

            local CurrentRefPointer = ScriptRef

            if string_sub(module, 1, 1) == "/" then
                CurrentRefPointer = RealObjectRoot
            elseif string_sub(module, 1, 2) == "./" then
                module = string_sub(module, 3)
            end

            local PreviousPathMatch
            for PathMatch in string_gmatch(module, "([^/]*)/?") do
                local RealIndex = PathMatch
                if PathMatch == ".." then
                    RealIndex = "Parent"
                end

                -- Don't advance dir if it's just another "/" either
                if RealIndex ~= "" then
                    local ResultRef = CurrentRefPointer:FindFirstChild(RealIndex)
                    if not ResultRef then
                        local CurrentRefParent = CurrentRefPointer.Parent
                        if CurrentRefParent then
                            ResultRef = CurrentRefParent:FindFirstChild(RealIndex)
                        end
                    end

                    if ResultRef then
                        CurrentRefPointer = ResultRef
                    elseif PathMatch ~= PreviousPathMatch and PathMatch ~= "init" and PathMatch ~= "init.server" and PathMatch ~= "init.client" then
                        error("Virtual script path \"" .. module .. "\" not found", 2)
                    end
                end

                -- For possible checks next cycle
                PreviousPathMatch = PathMatch
            end

            if CurrentRefPointer.ClassName ~= "ModuleScript" then
                error(ErrorNonModuleScript, 2)
            elseif CurrentRefPointer == ScriptRef then
                error(ErrorSelfRequire, 2)
            end

            return LoadScript(CurrentRefPointer)
        end

        return RealCall(require, module, ...)
    end

    -- Now, return flattened globals ready for direct runtime exec
    return Global_wax, Global_script, Global_require
end

for _, ScriptRef in next, ScriptsToRun do
    Defer(LoadScript, ScriptRef)
end
]]


local LuauSpec = require(script.Parent.Parent.Parent.Luau.Spec)

--[=[
	@class Boolean

	The Boolean class is used to lift a constant type with the LBC_CONSTANT_BOOLEAN id (1)
]=]
local Boolean = {}
Boolean.__constant_type = LuauSpec.LuauBytecodeConstants.LBC_CONSTANT_BOOLEAN
Boolean.__index = Boolean

--[=[
	@within Boolean

	The Boolean class is used to lift a constant type with the LBC_CONSTANT_BOOLEAN id (1)

	@return Boolean
]=]
function Boolean.new(State: boolean)
	local self = setmetatable({}, Boolean)
	self.ClassName = "Boolean"

    self.Value = State

	return self
end

--[=[
	@within Boolean

]=]
function Boolean:__tostring()
    return self.Value == true and "true" or "false"
end

export type Boolean = typeof(Boolean.new(nil :: never))

return Boolean


local LuauSpec = require(script.Parent.Parent.Parent.Luau.Spec)

--[=[
	@class Closure

	The Closure class is used to lift a constant type with the LBC_CONSTANT_CLOSURE id (6)
]=]
local Closure = {}
Closure.__constant_type = LuauSpec.LuauBytecodeConstants.LBC_CONSTANT_CLOSURE
Closure.__index = Closure

--[=[
	@within Closure

	The Closure class is used to lift a constant type with the LBC_CONSTANT_CLOSURE id (6)

	@return Closure
]=]
function Closure.new(Index: number)
	local self = setmetatable({}, Closure)
	self.ClassName = "Closure"

    self.Value = Index

	return self
end

--[=[
	@within Closure

]=]
function Closure:__tostring()
    return `closure({self.Value})`
end

export type Closure = typeof(Closure.new(nil :: never))

return Closure


local LuauSpec = require(script.Parent.Parent.Parent.Luau.Spec)
local String = require(script.Parent.String)

--[=[
	@class Import

	The Import class is used to lift a constant type with the LBC_CONSTANT_IMPORT id (4)
]=]
local Import = {}
Import.__constant_type = LuauSpec.LuauBytecodeConstants.LBC_CONSTANT_IMPORT
Import.__index = Import

--[=[
	@within Import

	The Import class is used to lift a constant type with the LBC_CONSTANT_IMPORT id (4)

	@return Import
]=]
function Import.new(Imports: { String.String })
	local self = setmetatable({}, Import)
	self.ClassName = "Import"

    self.Value = Imports

	return self
end

--[=[
	@within Import

]=]
function Import:__tostring()
    local result: { string } = {}

    for i, Constant in self.Value do
        table.insert(result, tostring(Constant.Value))
    end

    return table.concat(result, ".")

end

export type Import = typeof(Import.new(nil :: never))

return Import


local LuauSpec = require(script.Parent.Parent.Parent.Luau.Spec)

--[=[
	@class Nil

	The Nil class is used to lift a constant type with the LBC_CONSTANT_NIL id (0)
]=]
local Nil = {}
Nil.__constant_type = LuauSpec.LuauBytecodeConstants.LBC_CONSTANT_NIL
Nil.__index = Nil

--[=[
	@within Nil

	The Nil class is used to lift a constant type with the LBC_CONSTANT_NIL id (0)

	@return Nil
]=]
function Nil.new()
	local self = setmetatable({}, Nil)
	self.ClassName = "Nil"

    self.Value = nil

	return self
end

--[=[
	@within Nil

]=]
function Nil:__tostring()
    return "nil"
end

export type Nil = typeof(Nil.new())

return Nil


local LuauSpec = require(script.Parent.Parent.Parent.Luau.Spec)

--[=[
	@class Number

	The Number class is used to lift a constant type with the LBC_CONSTANT_NUMBER id (2)
]=]
local Number = {}
Number.__constant_type = LuauSpec.LuauBytecodeConstants.LBC_CONSTANT_NUMBER
Number.__index = Number

--[=[
	@within Number

	The Number class is used to lift a constant type with the LBC_CONSTANT_NUMBER id (2)

	@return Number
]=]
function Number.new(NumericalValue: number)
	local self = setmetatable({}, Number)
	self.ClassName = "Number"

    self.Value = NumericalValue

	return self
end

--[=[
	@within Number

]=]
function Number:__tostring()
    return tostring(self.Value)
end

export type Number = typeof(Number.new(nil :: never))

return Number


local LuauSpec = require(script.Parent.Parent.Parent.Luau.Spec)

--[=[
	@class String

	The String class is used to lift a constant type with the LBC_CONSTANT_STRING id (3)
]=]
local String = {}
String.__constant_type = LuauSpec.LuauBytecodeConstants.LBC_CONSTANT_STRING
String.__index = String

--[=[
	@within String

	The String class is used to lift a constant type with the LBC_CONSTANT_STRING id (3)

	@return String
]=]
function String.new(Value: string)
	local self = setmetatable({}, String)
	self.ClassName = "String"

    self.Value = Value

	return self
end

--[=[
	@within String

]=]
function String:__tostring()
    return `"{self.Value}"`
end

export type String = typeof(String.new(nil :: never))

return String


local LuauSpec = require(script.Parent.Parent.Parent.Luau.Spec)

--[=[
	@class Table

	The Table class is used to lift a constant type with the LBC_CONSTANT_TABLE id (5)
]=]
local Table = {}
Table.__constant_type = LuauSpec.LuauBytecodeConstants.LBC_CONSTANT_TABLE
Table.__index = Table

--[=[
	@within Table

	The Table class is used to lift a constant type with the LBC_CONSTANT_TABLE id (5)

	@return Table
]=]
function Table.new(Value: { any })
	local self = setmetatable({}, Table)
	self.ClassName = "Table"

    self.Value = Value

	return self
end

--[=[
	@within Table

]=]
function Table:__tostring()
    return `\{ {table.concat(self.Value, ", ")} \}`

end

export type Table = typeof(Table.new(nil :: never))

return Table


local LuauSpec = require(script.Parent.Parent.Parent.Luau.Spec)

--[=[
	@class Vector

	The Vector class is used to lift a constant type with the LBC_CONSTANT_VECTOR id (7)
]=]
local Vector = {}
Vector.__constant_type = LuauSpec.LuauBytecodeConstants.LBC_CONSTANT_VECTOR
Vector.__index = Vector

--[=[
	@within Vector

	The Vector class is used to lift a constant type with the LBC_CONSTANT_VECTOR id (7)

	@return Vector
]=]
function Vector.new(X: number, Y: number, Z: number, W: number)
	local self = setmetatable({}, Vector)
	self.ClassName = "Vector"

    self.X = X
    self.Y = Y
    self.Z = Z
    self.W = W

	return self
end

--[=[
	@within Vector

]=]
function Vector:__tostring()
    return `Vector({self.x}, {self.y}, {self.z}, {self.w})`

end

export type Vector = typeof(Vector.new(nil :: never))

return Vector


--[=[
	@class Hexadecimal

	The Hexadecimal class is used to represent an integer as a Hexadecimal
]=]
local Hexadecimal = {}
Hexadecimal.__index = Hexadecimal

--[=[
	@within Hexadecimal

	The Hexadecimal class is used to represent an integer as a Hexadecimal

	@return Hexadecimal
]=]
function Hexadecimal.new(Number: number): Hexadecimal
	local self = setmetatable({}, Hexadecimal)
	self.ClassName = "Hexadecimal"

    self.Value = Number

	return self
end

--[=[
	@within Hexadecimal

]=]
function Hexadecimal:__tostring()
    return ("0x%X"):format(self.Value)
end

export type Hexadecimal = typeof(Hexadecimal.new(nil :: never))

return Hexadecimal


local Buffer = require(script.Parent.Parent.Utilities.Buffer)

local LuauSpec = require(script.Parent.Parent.Luau.Spec)

local Hexadecimal = require(script.Parent.Hexadecimal)

export type Context = {
    LineDefined: number,
    Pc: number
}

--[=[
	@class Instruction

	The Instruction class is used to lift a luau instruction
]=]
local Instruction = {}
Instruction.__index = Instruction

--[=[
	@within Instruction

	The Instruction class is used to lift a luau instruction

	@param Bytecode Buffer.BufferReader
	@return Instruction
]=]
function Instruction.new(Bytecode: Buffer.BufferReader, Value: number?, EncodingKey: number?): Instruction
	local self = setmetatable({}, Instruction)
	self.ClassName = "Instruction"

    -- Opcode

    local real = Value or Bytecode:readu32()
    self.Value = Hexadecimal.new(if EncodingKey then bit32.band(real, 0xFF) * EncodingKey else real)
    self.Code = Hexadecimal.new(bit32.band(self.Value.Value, 0xFF))
    self.OpCode = {}

    assert(self.Code.Value >= 0 and self.Code.Value < LuauSpec.LuauOpcode.LOP__COUNT.Index, `Invalid opcode {self.Code}`)

    for i, OpCodeInfo in LuauSpec.LuauOpcode :: { LuauSpec.OpCodeInfo } do
        if OpCodeInfo.Index == self.Code.Value then
            self.OpCode.Information = OpCodeInfo
            break
        end
    end

    -- Aux

    if self.OpCode.Information.HasAux then
        self.AuxValue = Hexadecimal.new(Bytecode:readu32(false))
        self.Aux = Instruction.new(Bytecode, bit32.rshift(Bytecode:readu32(), 32))
    end

    -- Encodings

    self.Encodings = {
        A = bit32.band(bit32.rshift(real, 8), 0xFF),
        B = bit32.band(bit32.rshift(real, 16), 0xFF),
        C = bit32.band(bit32.rshift(real, 24), 0xFF),
        D = bit32.rshift(real, 16),
        E = bit32.rshift(real, 8)
    }

    -- Context

    self.Context = {
        LineDefined = 0,
        Pc = 0
    }

	return self
end

export type Instruction = typeof(Instruction.new(nil :: never))

return Instruction


--[=[
	@class Upvalue

	The Upvalue class is used to lift a luau Upvalue
]=]
local Upvalue = {}
Upvalue.__index = Upvalue

--[=[
	@within Upvalue

	The Upvalue class is used to lift a luau Upvalue

	@param Bytecode Buffer.BufferReader
	@return Upvalue
]=]
function Upvalue.new(Name: string): Upvalue
	local self = setmetatable({}, Upvalue)
	self.ClassName = "Upvalue"

    self.Name = Name

	return self
end

export type Upvalue = typeof(Upvalue.new(nil :: never))

return Upvalue


export type Upvalue = {
    ClassName: "Upvalue",
    Name: string
}

export type VectorConstant = {
    ClassName: "Vector",
    X: number,
    Y: number,
    Z: number,
    W: number
}

export type TableConstant = {
    ClassName: "Table",
    Value: { [number]: any }
}

export type StringConstant = {
    ClassName: "String",
    Value: string
}

export type NumberConstant = {
    ClassName: "Number",
    Value: number
}

export type NilConstant = {
    ClassName: "Nil",
    Value: nil
}

export type ImportConstant = {
    ClassName: "Import",
    Value: { [number]: StringConstant }
}

export type ClosureConstant = {
    ClassName: "Closure",
    Value: number
}

export type BooleanConstant = {
    ClassName: "Boolean",
    Value: boolean
}

export type Constant = BooleanConstant | ClosureConstant | ImportConstant | NilConstant | NumberConstant | StringConstant | TableConstant | VectorConstant

export type Hexadecimal = {
    ClassName: "Hexadecimal",
    Value: number
}

export type Instruction = {
    ClassName: "Instruction",
    Value: Hexadecimal,
    Code: Hexadecimal,
    OpCode: {
        Index: number,
        Name: string,
        HasAux: boolean
    },
    AuxValue: Hexadecimal?,
    Aux: Instruction?,
    Encodings: {
        A: number,
        B: number,
        C: number,
        D: number,
        E: number
    },
    Context: {
        LineDefined: number,
        Pc: number
    }
}

export type BufferReader = {
	Buffer: buffer,
	Offset: number,
	Length: number,
	LastUnreadBytes: number,

	tostring: (BufferReader) -> string,
	len: (BufferReader) -> number,

	readi8: (BufferReader, shift: boolean?) -> number,
	readu8: (BufferReader, shift: boolean?) -> number,
	readi16: (BufferReader, shift: boolean?) -> number,
	readu16: (BufferReader, shift: boolean?) -> number,
	readi32: (BufferReader, shift: boolean?) -> number,
	readu32: (BufferReader, shift: boolean?) -> number,
	readf32: (BufferReader, shift: boolean?) -> number,
	readf64: (BufferReader, shift: boolean?) -> number,

	writei8: (BufferReader, value: number, shift: boolean?) -> number,
	writeu8: (BufferReader, value: number, shift: boolean?) -> number,
	writei16: (BufferReader, value: number, shift: boolean?) -> number,
	writeu16: (BufferReader, value: number, shift: boolean?) -> number,
	writei32: (BufferReader, value: number, shift: boolean?) -> number,
	writeu32: (BufferReader, value: number, shift: boolean?) -> number,
	writef32: (BufferReader, value: number, shift: boolean?) -> number,
	writef64: (BufferReader, value: number, shift: boolean?) -> number,

	readstring: (BufferReader, size: number, shift: boolean?) -> string,
	writestring: (BufferReader, value: string, size: number?, shift: boolean?) -> (),

	fill: (BufferReader, value: number, count: number?) -> (),

	seek: (BufferReader, dst: number) -> (),
	resize: (BufferReader, newSize: number) -> (),

	ReadVarInt: (BufferReader, shift: boolean?) -> number,
	ReadString: (BufferReader, length: number?, shift: boolean?) -> string,
	ReadBoolean: (BufferReader, shift: boolean?) -> boolean,
	ReadBytes: (BufferReader, size: number, shift: boolean?) -> BufferReader
}

export type Proto = {
    ClassName: "Proto",
    Main: boolean,
    MaxStackSize: number,
    ParameterCount: number,
    UpvalueCount: number,
    IsVararg: number,
    Flags: number?,
    TypeSize: number?,
    SizeTypeInfo: number?,
    TypeInfo: BufferReader?,
    UserdataInfo: {
        TypeSize: number,
        UpvalueCount: number,
        LocalCount: number
    },
    InstructionCount: number,
    Instructions: { [number]: Instruction },
    ConstantCount: number,
    Constants: { [number]: Constant },
    ProtoCount: number,
    Protos: { [number]: Proto },
    LineDefined: number,
    LastLineDefined: number,
    Name: string,
    HasLineInfo: boolean,
    HasDebugInfo: boolean,
    LocalVariableCount: number?,
    LocalVariables: {
        [number]: {
            Name: string,
            StartPc: number,
            EndPc: number,
            Register: number
        }
    }?,
    DebugUpvalueCount: number?,
    Upvalues: { [number]: Upvalue },
}

export type DeserializerConfig = {
    RobloxBytecode: boolean?,
    _stop_key_overwrite_rblx: boolean?,
    EncodingKey: number?,
    PostFinish: (Deserializer: Deserializer) -> ()
}

export type Deserializer = {
    new: (Bytecode: BufferReader, Config: DeserializerConfig) -> Deserializer,
    ReadStringRef: (self: Deserializer) -> string?,
    GetTypeMapping: (self: Deserializer, Name: string, Length: number) -> number,

    ClassName: "Deserializer",
    Source: BufferReader,
    BytecodeVersion: number,
    IsTyped: boolean,
    TypesVersion: number?,
    StringCount: number,
    Strings: { [number]: string },
    UserdataTypeLimit: number,
    UserdataRemapping: { [number]: number },
    ProtoCount: number,
    Protos: { [number]: Proto },
    ProtoEntryPoint: number,
    FunctionCount: number,
    Functions: { [number]: Proto },
    FunctionEntryPoint: number,

    MagicA: number?,
    MagicB: number?,
    Hash: string?
}

export type Iridium = {
    Deserialize: (self: Iridium, Bytecode: string | buffer | BufferReader, Config: DeserializerConfig?) -> Deserializer
}

return nil


export type ChalkType = {
	bold: ChalkType;
	italic: ChalkType;
	underline: ChalkType;
	strikethrough: ChalkType;
	uppercase: ChalkType;
	smallcaps: ChalkType;

	size: (size: number) -> ChalkType;
	face: (face: string) -> ChalkType;
	family: (family: string) -> ChalkType;
	weight: (weight: string) -> ChalkType;
	transparency: (transparency: number) -> ChalkType;
	stroke: (options: { Color: any, Joins: string, Thickness: number, Transparency: number }) -> ChalkType;
	color: (color: any) -> ChalkType;

	white: ChalkType;
	black: ChalkType;
	red: ChalkType;
	brown: ChalkType;
	orange: ChalkType;
	yellow: ChalkType;
	lime: ChalkType;
	green: ChalkType;
	blue: ChalkType;
	purple: ChalkType;
	pink: ChalkType;
}

local Formats = {
	["FONT_COLOR_RGB"] = {
		Start = `<font color="rgb(%s,%s,%s)">`;
		End = `</font>`;
	};
	["FONT_COLOR_HEX"] = {
		Start = `<font color="#%s">`;
		End = `</font>`;
	};
	["FONT_SIZE"] = {
		Start = `<font size="%d">`;
		End = `</font>`;
	};
	["FONT_FACE"] = {
		Start = `<font face="%s">`;
		End = ""
	};
	["FONT_FAMILY"] = {
		Start = `<font family="%s">`;
		End = ""
	};
	["FONT_WEIGHT"] = {
		Start = `<font weight="%s">`;
		End = ""
	};
	["FONT_TRANSPARENCY"] = {
		Start = `<font transparency="%s">`;
		End = ""
	};
	["STROKE"] = {
		Start = `<stroke color="#%s" joins="%s" thickness="%s" transparency="%s">`;
		End = "</stroke>";
	};
	["BOLD"] = {
		Start = `<b>`;
		End = `</b>`;
	};
	["ITALIC"] = {
		Start = `<i>`;
		End = `</i>`;
	};
	["UNDERLINE"] = {
		Start = "";
		End = "";
	};
	["STRIKETHROUGH"] = {
		Start = "<s>";
		End = "</s>";
	};
	["UPPERCASE"] = {
		Start = "<uppercase>";
		End = "</uppercase>";
	};
	["SMALLCAPS"] = {
		Start = "<smallcaps>";
		End = "</smallcaps>";
	}
}

local ChalkData = {};
local ChalkObject = {}; ChalkObject.__index = ChalkObject;

local function GenerateCustomHandlerFunction(Type)
	return function(Argument)
		local FormatStart = Formats[Type].Start:format(Argument);

		return function(String)
			return `{FormatStart}{String}{Formats[Type].End}`
		end
	end
end

local function GenerateChalkDataFunction(Type)
	return function(String)
		local Type = Formats[Type]
		return `{Type.Start}{String}{Type.End}`
	end
end

local function ValidateHex(String)
	if typeof(String) ~= "string" then return end

	String = String:gsub("#", "")

	return string.match(String, `^%x%x%x%x%x%x$`)
end

local CustomHandler = {
	["size"] = GenerateCustomHandlerFunction("FONT_SIZE");
	["face"] = GenerateCustomHandlerFunction("FONT_FACE");
	["family"] = GenerateCustomHandlerFunction("FONT_FAMILY");
	["weight"] = GenerateCustomHandlerFunction("FONT_WEIGHT");
	["transparency"] = GenerateCustomHandlerFunction("FONT_TRANSPARENCY");
	["stroke"] = function(Data)
		local Color: Color3 = Data.Color
		Color = `#{Color:ToHex()}`
		local Joins = Data.Joins or "Round";
		local Thickness = Data.Thickness or 1;
		local transparency = Data.Transparency or 0;

		local FormatStart = Formats.STROKE.Start:format(Color, Joins, Thickness, transparency);

		return function(String)
			return `{FormatStart}{String}{Formats.STROKE.End}`
		end
	end;
	["color"] = function(...)
		local Args = {...}
		local IsHex = ValidateHex(Args[1]);
		local IsColor3 = typeof(Args[1]) == "Color3";

		local FirstArg = Args[1];

		local Color =
			(IsColor3 and FirstArg) or
			Color3.fromRGB(FirstArg, Args[2], Args[3]);

		local FormatStart = (IsHex and Formats.FONT_COLOR_HEX.Start:format(FirstArg:gsub("#", ""))) or
			Formats.FONT_COLOR_RGB.Start:format(math.floor(Color.R * 255), math.floor(Color.G * 255), math.floor(Color.B * 255));

		return function(String)
			return `{FormatStart}{String}{Formats.FONT_COLOR_RGB.End}`
		end
	end;
}

function ChalkObject.new(FirstObject)
	local self = {};
	local Meta = {};
	local Formatters = {}; self.Formatters = Formatters;

	local NextCall;
	local TrueSelf;

	function Meta:__call(...)
		local Arguments = {...};

		if NextCall then
			local Call = NextCall;

			NextCall = nil;

			return Call(unpack(Arguments));
		end

		local Results = {};

		for _, String in (Arguments) do
			for _, Value in (Formatters) do
				String = Value(String);
			end

			table.insert(Results, String);
		end

		return unpack(Results);
	end

	function Meta:__index(Index)
		local CustomHandler = CustomHandler[Index];

		if (CustomHandler) then
			NextCall = function(...)
				local Result = CustomHandler(...);
				table.insert(Formatters, Result);

				return TrueSelf;
			end

			return TrueSelf;
		end

		local Formatter = ChalkData[Index];

		table.insert(Formatters, Formatter);

		return self;
	end

	Meta:__index(FirstObject);

	TrueSelf = setmetatable(self, Meta)

	return TrueSelf
end

local Chalk: ChalkType = setmetatable({}, {
	__index = function(_, Index)
		return ChalkObject.new(Index);
	end;
	__newindex = function()
		return error "Chalk is READONLY";
	end
});

ChalkData.white = Chalk.color(Color3.fromRGB(255, 255, 255));
ChalkData.black = Chalk.color(Color3.fromRGB(0, 0, 0));
ChalkData.red = Chalk.color(Color3.fromRGB(255, 0, 0));
ChalkData.brown = Chalk.color(Color3.fromRGB(153, 51, 0));
ChalkData.orange = Chalk.color(Color3.fromRGB(255, 153, 0));
ChalkData.yellow = Chalk.color(Color3.fromRGB(255, 255, 0));
ChalkData.lime = Chalk.color(Color3.fromRGB(153, 255, 0));
ChalkData.green = Chalk.color(Color3.fromRGB(0, 255, 0));
ChalkData.blue = Chalk.color(Color3.fromRGB(0, 0, 255));
ChalkData.purple = Chalk.color(Color3.fromRGB(102, 0, 153));
ChalkData.pink = Chalk.color(Color3.fromRGB(255, 0, 255));

ChalkData.bold = GenerateChalkDataFunction("BOLD");
ChalkData.italic = GenerateChalkDataFunction("ITALIC");
ChalkData.underline = GenerateChalkDataFunction("UNDERLINE");
ChalkData.strikethrough = GenerateChalkDataFunction("STRIKETHROUGH");
ChalkData.uppercase = GenerateChalkDataFunction("UPPERCASE");
ChalkData.smallcaps = GenerateChalkDataFunction("SMALLCAPS");

return Chalk :: ChalkType;

-- API List

-- Chalk.[<BrickColorName>]
-- Chalk.bold
-- Chalk.italic
-- Chalk.underline
-- Chalk.strikethrough
-- Chalk.uppercase
-- Chalk.smallcaps

-- Chalk.size(<Size>) -- Number
-- Chalk.face(<Face>) -- string
-- Chalk.family(<rbxasset://>) -- string
-- Chalk.weight(<Weight>) -- string
-- Chalk.transparency(<Transparency>) -- number 0-1
-- Chalk.stroke({Color = <Color>, Joins = <Joins>, Thickness = <Thickness>, Transparency = <Transparency>}) -- Color3, string, number, number
-- Chalk.color(<Color>) -- Color3, string, number, number, number

-- Chalk.white - Set the color to white
-- Chalk.black - Set the color to black
-- Chalk.red - Set the color to red
-- Chalk.brown - Set the color to brown
-- Chalk.orange - Set the color to orange
-- Chalk.yellow - Set the color to yellow
-- Chalk.lime - Set the color to lime
-- Chalk.green - Set the color to green
-- Chalk.blue - Set the color to blue
-- Chalk.purple - Set the color to purple
-- Chalk.pink - Set the color to pink


--!nocheck
--// Variables

local RunService = game:GetService("RunService")

local Module = {}

type Hint = {[string]: any}
type Filter<T> = (T, Hint) -> (T?)

--[=[
	@within SDK
	@interface Options

	.DSN string? -- The DSN for the sentry project to send events to
	.debug boolean? -- See [SDK.debug] (must press the "Show Private")

	.DefaultIntegrations boolean? -- Whether to enable the built-in integrations. Defaults to true.
	.Integrations {Integration}?

	.Release string?
	.Environment string?

	.SampleRate number?

	.ServerName string? -- Defaults to `game.JobId` or `"local"`
]=]
export type Options = {
	DSN: string?,
	debug: boolean?,

	DefaultIntegrations: boolean?,
	Integrations: {ModuleScript}?,

	Release: string?,
	Environment: string?,

	SendClientEvents: boolean?,
	SendStudioEvents: boolean?,

	SampleRate: number?,
	MaxBreadcrumbs: number?,
	AttachStacktrace: boolean?,
	SendDefaultPII: boolean?,

	ServerName: string?,

	InAppInclude: {string}?,
	InAppExclude: {string}?,

	WithLocals: boolean?,

	BeforeSend: Filter<unknown>?,
--	BeforeSendTransaction: Filter<unknown>?,
	BeforeBreadcrumb: Filter<unknown>?,

	Transport: unknown,
	ShutdownTimeout: number?,
}

Module.Options = {
	Debug = false,

	DefaultIntegrations = true,
	Integrations = {},

	Release = string.format("%s#%d@%d", game.Name, game.PlaceId, game.PlaceVersion),
	Environment = (if RunService:IsStudio() then "studio" else "production"),

	SendClientEvents = true,
	SendStudioEvents = false,

	SampleRate = 1.0,
	MaxBreadcrumbs = 100,
	AttachStacktrace = false,
	SendDefaultPII = false,
	IncludeLocalVariables = true,

	ServerName = (if game.JobId ~= "" then game.JobId else "local"),

	InAppInclude = {},
	InAppExclude = {},

	WithLocals = true,

	Transport = require(script.Parent:WaitForChild("Transport")),
	ShutdownTimeout = 2,
}:: Options

Module.Levels = {"fatal", "error", "warning", "info", "debug"}

export type ValidJSONValues = string | number | boolean
export type Level = "fatal" | "error" | "warning" | "info" | "debug"
export type Event = {
	event_id: string?,
	timestamp: string | number | nil,
	platform: "other" | nil,

	level: Level?, --/ The record severity. Defaults to error.
	logger: string?, --/ The name of the logger which created the record.
	transaction: string?, --/ The name of the transaction which caused this exception.
	server_name: string?, --/ Identifies the host from which the event was recorded. Defaults to game.JobId
	release: string?, --/ The release version of the application. MUST BE UNIQUE ACROSS ORGANIZATION
	dist: string?, --/ The distribution of the application.

	tags: {[string]: string}?, --/ A map or list of tags for this event. Each tag must be less than 200 characters.
	environment: string?, --/ The environment name, such as production or staging.
	modules: {[string]: string}?, --/ A list of relevant modules and their versions.
	extra: {[string]: string}?, --/ An arbitrary mapping of additional metadata to store with the event.
	fingerprint: {string}?, --/ A list of strings used to dictate the deduplication of this event.

	contexts: {[string]: {[string]: any}}?,

	sdk: {
		name: string,
		version: string,
		integrations: {string}?,
		packages: {{
			name: string,
			version: string,
		}}?,
	}?,

	exception: {{
		type: string?,
		value: string?,
		module: string?,
		thread_id: string?,

		mechanism: {
			data: {[string]: any}?,
			description: string?,
			handled: boolean?,
			help_link: string?,
			synthetic: boolean?,
			type: string?,
		}?,

		stacktrace: {
			frames: {},
			registers: {[string]: string}?,
		}?,
	}}?,

	user: {
		id: number,
		username: string,

		geo: {
			city: string?,
			country_code: string, --/ Two-letter country code (ISO 3166-1 alpha-2).
			region: string?,
		}
	}?,

	message: {
		message: string,
		formatted: string?,
		params: {string}?,
	}?,

	errors: {{
		type: string,
		path: string?,
		details: string?,
	}}?,
}

--// Functions

function Module:IsValidLevel(Level: Level | unknown)
	return table.find(self.Levels, Level)
end

function Module:AggregateDictionaries(...)
	local Aggregate = {}

	for _, Dictionary in ipairs{...} do
		for Index, Value in Dictionary do
			if typeof(Value) == "table" and typeof(Aggregate[Index]) == "table" then
				Aggregate[Index] = self:AggregateDictionaries(Aggregate[Index], Value)
			else
				Aggregate[Index] = Value
			end
		end
	end

	return Aggregate
end

function Module:DeepCopy<T>(Table: T): T
	if type(Table) == "table" then
		Table = setmetatable(table.clone(Table), getmetatable(Table))

		for Index, Value in next, Table do
			Table[self:DeepCopy(Index)] = self:DeepCopy(Value)
		end
	end

	return Table
end

function Module:OverlapTables<B, F>(Background: B, Foreground: F?): F & B
	return setmetatable(Foreground or {}, {__index = Background})
end

return table.freeze(Module)


local args = { ... }
local version = args[2] or "unknown"
local hash = args[3] or "unknown"

local fs = require("@std/fs")
local system = require("@lute/system")

local log = require("../src/util/log")
log.setLevel("debug")

local runProcess = require("../src/util/runProcess")

if not fs.exists("build") then
	fs.createdirectory("build")
end

local output_path = "build/rocale-cli"
if string.lower(system.os) == "windows_nt" then
	output_path = output_path .. ".exe"
end

log.info("Version: " .. version)
log.info("Commit Hash: " .. hash)

local versionFile = fs.open("src/version.luau", "w+")
fs.write(versionFile, `return \{ version="{version}", hash="{hash}" \}`)
fs.close(versionFile)

log.info("Building rocale-cli...")
runProcess({ "lute", "compile", "src/cli.luau", "--output", output_path, "--show-require-graph", "--bundle-stats" })
log.info("Build complete: " .. output_path)


local process = require("@lute/process")
local fs = require("@std/fs")

local log = require("../src/util/log")
log.setLevel("debug")

local runProcess = require("../src/util/runProcess")

runProcess({
	"build/rocale-cli",
	"run",
	"--script",
	"tests/run-test-project.luau",
	"--placeId",
	process.env.TEST_PLACE_ID,
	"--universeId",
	process.env.TEST_UNIVERSE_ID,
	"--load.project",
	"tests/test.project.json",
	"--lua.globals",
	"HELLO_GLOBAL=hello,WORLD_GLOBAL=world!",
	"--binaryOutput",
	"build/testOutput.txt",
	"--verbose",
})

local testOutput = fs.readfiletostring("build/testOutput.txt")
fs.remove("build/testOutput.txt")
assert(testOutput == "hello world!", `Expected binary output to be "hello world!"`)

log.info("Rerunning tests using load.version and without optional variables")

runProcess({
	"build/rocale-cli",
	"run",
	"--script",
	"tests/run-test-project.luau",
	"--placeId",
	process.env.TEST_PLACE_ID,
	"--universeId",
	process.env.TEST_UNIVERSE_ID,
	"--load.version",
	"0",
	"--verbose",
})


local args = { ... }

local runCommand = require("./commands/run")
local versionInfo = require("./version")

local function helpCommand()
	print([[
rocale-cli
A command-line tool for building, uploading, and running Roblox projects in Open Cloud.

USAGE:
  rocale-cli <command> [options]

COMMANDS:
  run      Build, upload, and run a Roblox project in Open Cloud.
  help     Show this help message.
  version  Show version.

For more information about a command, run: rocale-cli <command> --help
]])
end

local function versionCommand()
	print(versionInfo.version)
	print(versionInfo.hash)
end

local availableCommands = {
	run = runCommand,
	help = helpCommand,
	version = versionCommand,
}

local command = availableCommands[args[2]] or availableCommands["help"]

command(...)


local function help()
	print([[
rocale-cli run
Build, upload, and run a Roblox project in Open Cloud

USAGE:
  rocale-cli run [options]

REQUIRED:
  -u, --universeId <id>      	Target universe ID to spawn an Open Cloud instance of
  -p, --placeId <id>         	Target place ID to spawn an Open Cloud instance of
  --apiKey <key>            	Roblox API key (or set ROBLOX_API_KEY env var)

LOAD OPTIONS:
  --load.project <file>      	Rojo project.json/rbxp to build and load (requires rojo or robloxdev-cli)
  --load.place <file>       	Load an rbxl
  --load.version <num>      	Load existing place version without building/uploading
                            	(set to 0 to rerun last uploaded version)

BUILD OPTIONS:
  --output <file>            	Output file path for the built place file
                             	(default: output.rbxl)
  --script <file>            	Luau script to load and set as entrypoint

EXECUTION OPTIONS:
  --lua.globals <key=value,...>	Comma-separated list of Lua globals to inject
  --timeout <seconds>        	Timeout for polling task completion (default: 300)
  --pollInterval <seconds>   	Interval between polling attempts (default: 2)
  --binaryOutput <file>      	Path to save binary output from the task

FLAGS:
  -v, --verbose              	Enable verbose logging
  --help                     	Show this help message
]])
end

return function(...)
	local fs = require("@std/fs")
	local process = require("@lute/process")

	local richterm = require("@batteries/richterm")

	local cli = require("@batteries/cli")
	local args = cli.parser()

	local log = require("../util/log")

	local ocaleSdk = require("../core/ocaleSdk")
	local runProcess = require("../util/runProcess")

	args:add("script", "option")
	args:add("load.project", "option")
	args:add("output", "option", { default = "output.rbxl" })
	args:add("universeId", "option", { aliases = { "u" } })
	args:add("placeId", "option", { aliases = { "p" } })
	args:add("load.version", "option")
	args:add("load.place", "option")
	args:add("apiKey", "option")
	args:add("lua.globals", "option", { default = nil })
	args:add("timeout", "option", { default = "300" })
	args:add("pollInterval", "option", { default = "2" })
	args:add("binaryOutput", "option", { default = nil })
	args:add("verbose", "flag", { aliases = { "v" } })
	args:add("help", "flag")

	args:parse({ ... })

	if args:has("help") then
		help()
		process.exit(0)
	end

	if args:has("verbose") then
		log.setLevel("debug")
	end

	local apiKey = process.env.ROBLOX_API_KEY
		or args:get("apiKey")
		or log.fatal("Missing API Key: Set ROBLOX_API_KEY environment variable or provide it as an argument")

	local luaGlobals = {}
	if args:get("lua.globals") then
		local globals = args:get("lua.globals")
		assert(globals, "Expected globals to be a comma-separated list of key=value pairs")

		for _, pair in ipairs(string.split(globals, ",")) do
			local key, value = pair:match("([^=]+)=([^=]+)")
			if key and value then
				luaGlobals[key] = value
			else
				log.fatal("Expected format for globals is key=value")
			end
		end
	end

	local placeInfo: ocaleSdk.PlaceInfo = {
		universeId = tonumber(args:get("universeId")) or log.fatal("Missing a universe ID"),
		placeId = tonumber(args:get("placeId")) or log.fatal("Missing a place ID"),
	}

	if
		(
			(args:get("load.place") and 1 or 0)
			+ (args:get("load.version") and 1 or 0)
			+ (args:get("load.project") and 1 or 0)
		) > 1
	then
		log.fatal("Cannot specify more than one --load option")
	end

	if args:get("load.version") then
		local placeVersion = tonumber(args:get("load.version"))
		assert(placeVersion, "Missing place version number")

		if placeVersion > 0 then
			placeInfo.placeVersion = placeVersion
		end
	elseif args:get("load.project") then
		local projectFile = args:get("load.project")
		local outputPath = args:get("output")

		if not projectFile or not fs.exists(projectFile) then
			log.fatal(`Project file '{projectFile}' does not exist`)
		end
		assert(projectFile, "Missing project file path")
		assert(outputPath, "Missing output file path")

		if projectFile:match("%.project%.json$") then
			runProcess({ "rojo", "build", projectFile, "--output", outputPath })
		elseif projectFile:match("%.rbxp$") then
			runProcess({
				"robloxdev-cli",
				"pack",
				"--input",
				projectFile,
				"--output",
				outputPath,
			})
		else
			log.fatal(`Unsupported project file: '{projectFile}'. Must be a 'project.json' or '.rbxp' file.`)
		end

		ocaleSdk.uploadPlaceFile(outputPath, placeInfo, apiKey)
	elseif args:get("load.place") then
		local placeFile = args:get("load.place")
		if not placeFile or not fs.exists(placeFile) then
			log.fatal(`Place file '{placeFile}' does not exist`)
		end
		assert(placeFile, "Missing place file path")

		ocaleSdk.uploadPlaceFile(placeFile, placeInfo, apiKey)
	end

	local entryScript = args:get("script")
	if not entryScript or not fs.exists(entryScript) then
		log.fatal(`'{entryScript}' is not a valid entry script`)
	end
	assert(entryScript)

	local binaryOutputPath = nil
	if args:get("binaryOutput") then
		binaryOutputPath = args:get("binaryOutput")
	end
	local enableBinaryOutput = binaryOutputPath ~= nil

	local task = ocaleSdk.createTask(
		apiKey,
		placeInfo,
		entryScript,
		tonumber(args:get("timeout")),
		nil,
		enableBinaryOutput,
		luaGlobals
	)
	local completedTask = ocaleSdk.pollTaskCompletion(
		apiKey,
		task.path,
		tonumber(args:get("timeout")),
		tonumber(args:get("pollInterval"))
	)

	local logs = ocaleSdk.getTaskLogs(apiKey, completedTask.path)
	print(table.concat(logs, "\n"))

	if enableBinaryOutput and completedTask.binaryOutputUri then
		local binaryOutput = ocaleSdk.getBinaryOutput(completedTask.binaryOutputUri)
		assert(binaryOutputPath, "Binary output path should be a valid file path")
		fs.writestringtofile(binaryOutputPath, binaryOutput)
		log.info(`{richterm.green("✓", log.noColor())} Binary output saved to '{binaryOutputPath}'`)
	end

	if completedTask.state == "FAILED" then
		local taskError = completedTask.error :: any
		log.fatal(`{richterm.red("✗", log.noColor())} Task failed with error: {taskError.code}\n{taskError.message}`)
	elseif completedTask.state == "CANCELLED" then
		log.fatal(`{richterm.red("✗", log.noColor())} Task was cancelled`)
	end
end


--[[
	Simple logger module.
]]

local richterm = require("@batteries/richterm")
local process = require("@lute/process")

local log = {}

type Level = "trace" | "debug" | "info" | "warn" | "error"

local currentLevel: Level = "info"

local colorsEnabled = true -- set to false to disable colors
if process.env.LOG_NO_COLOR then
	colorsEnabled = false
end

local showCaller = true -- set to false to disable caller information

local levels = {
	trace = 1,
	debug = 2,
	info = 3,
	warn = 4,
	error = 5,
} :: { [Level]: number }

function log.should(level: Level)
	return levels[level] >= levels[currentLevel]
end

-- Get caller information from debug traceback.
local function getCallerInfo()
	if not showCaller then
		return ""
	end
	local traceback = debug.traceback()
	local lines = string.split(traceback, "\n")

	-- Look for the first line that's not in this logger module.
	for _, line in ipairs(lines) do
		if line:find("util/log") then
			continue
		end

		return line
	end

	return "[unknown]"
end

function log.trace(...)
	if log.should("trace") then
		local l: string = ""
		if colorsEnabled then
			l = richterm.combine(richterm.blue, richterm.dim)("[TRACE] ")
			print(l .. richterm.dim(... or ""))
		else
			l = "[TRACE] "
			print(l .. (... or ""))
		end
	end
end

function log.debug(...)
	if log.should("debug") then
		local l: string = ""
		if colorsEnabled then
			l = richterm.cyan("[DEBUG] ")
			print(l .. richterm.dim(... or ""))
		else
			l = "[DEBUG] "
			print(l .. (... or ""))
		end
	end
end

function log.info(...)
	if log.should("info") then
		local l: string = ""
		if colorsEnabled then
			l = richterm.green("[INFO]  ")
		else
			l = "[INFO]  "
		end
		print(l .. (... or ""))
	end
end

function log.update(...)
	if log.should("info") then
		local l: string = ""
		if colorsEnabled then
			l = richterm.green("[INFO]  ")
			print("\x1b[1A\x1b[2K" .. l .. (... or ""))
		else
			l = "[INFO]  "
			print(l .. (... or ""))
		end
	end
end

function log.warn(...)
	if log.should("warn") then
		local l: string = ""
		if colorsEnabled then
			l = richterm.yellow("[WARN]  ")
		else
			l = "[WARN]  "
		end
		print(l .. (... or ""))
	end
end

function log.error(...)
	if log.should("error") then
		local l: string = ""
		if colorsEnabled then
			l = richterm.red("[ERROR] ")
		else
			l = "[ERROR] "
		end
		print(l .. (... or ""))
	end
end

function log.fatal(...): never
	local caller = getCallerInfo()
	local l: string = ""
	if colorsEnabled then
		l = richterm.red("[FATAL] ")
	else
		l = "[FATAL] "
	end
	print(caller .. " | " .. l .. (... or "") .. "\n")
	process.exit(1)
	return nil :: any
end

function log.fatalif(condition: boolean | number | string | nil, ...)
	if condition then
		log.fatal(...)
	end
end

-- Function to change log level
function log.setLevel(level: Level)
	if levels[level] then
		currentLevel = level
	else
		log.warn("Invalid log level:", level)
	end
end

-- Function to enable/disable colors
function log.enableColors()
	colorsEnabled = true
end

function log.disableColors()
	colorsEnabled = false
end

function log.noColor()
	return not colorsEnabled
end

function log.pretty(tbl: any, indent: number?): string
	local i = indent or 0
	local spacing = string.rep("  ", i)
	if type(tbl) ~= "table" then
		return tostring(tbl)
	end
	local result = "{\n"
	for key, value in pairs(tbl) do
		local keyStr = type(key) == "string" and key or "[" .. tostring(key) .. "]"
		result = result .. spacing .. "  " .. keyStr .. ": "
		if type(value) == "table" then
			result = result .. log.pretty(value, i + 1)
		elseif type(value) == "string" then
			result = result .. '"' .. value .. '"'
		else
			result = result .. tostring(value)
		end
		result = result .. "\n"
	end
	result = result .. spacing .. "}"
	return result
end

return log


local process = require("@lute/process")
local system = require("@lute/system")

local log = require("../util/log")

return function(cmd: { string })
	local isWin32 = string.lower(system.os) == "windows_nt"
	if isWin32 then
		cmd[1] = cmd[1] .. ".exe"
	end
	local cmdStr = table.concat(cmd, " ")
	log.debug(`Running command: {cmdStr}`)

	local ok, result = pcall(function()
		return process.run(
			cmd,
			{ stdio = "inherit", env = { PATH = `{process.cwd()}{if isWin32 then ";" else ":"}{process.env.PATH}` } }
		)
	end)

	if ok and result.ok then
		return result.stdout
	else
		return log.fatal(`Error while running command: {cmdStr}\n{result.stderr or result}`)
	end
end


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local TestProject = require(ReplicatedStorage:FindFirstChild("TestProject"))
assert(TestProject == true, "TestProject did not return true")

local hello = _G.HELLO_GLOBAL or ""
local world = _G.WORLD_GLOBAL or ""

local output = {
	BinaryOutput = buffer.fromstring(`{hello} {world}`),
	ReturnValues = { true }
}
return output


local MAX_TRIES = 20
local ZERO3 = Vector3.new(0, 0, 0)
-- Class
local GJK = {}
GJK.__index = GJK
-- Private Functions
local function tripleProduct(a, b, c)
	return b * c:Dot(a) - a * c:Dot(b)
end
local function containsOrigin(self, simplex, direction)
	local a = simplex[#simplex]
	local ao = -a
	if (#simplex == 4) then
		local b, c, d = simplex[3], simplex[2], simplex[1]
		local ab, ac, ad = b - a, c - a, d - a
		local abc, acd, adb = ab:Cross(ac), ac:Cross(ad), ad:Cross(ab)

		abc = abc:Dot(ad) > 0 and -abc or abc
		acd = acd:Dot(ab) > 0 and -acd or acd
		adb = adb:Dot(ac) > 0 and -adb or adb

		if (abc:Dot(ao) > 0) then
			table.remove(simplex, 1)
			direction = abc
		elseif (acd:Dot(ao) > 0) then
			table.remove(simplex, 2)
			direction = acd
		elseif (adb:Dot(ao) > 0) then
			table.remove(simplex, 3)
			direction = adb
		else
			return true
		end
	elseif (#simplex == 3) then
		local b, c = simplex[2], simplex[1]
		local ab, ac = b - a, c - a

		local abc = ab:Cross(ac)
		local abPerp = tripleProduct(ac, ab, ab).Unit
		local acPerp = tripleProduct(ab, ac, ac).Unit

		if (abPerp:Dot(ao) > 0) then
			table.remove(simplex, 1)
			direction = abPerp
		elseif (acPerp:Dot(ao) > 0) then
			table.remove(simplex, 2)
			direction = acPerp
		else
			local isV3 = ((a - a) == ZERO3)
			if (not isV3) then
				return true
			else
				direction = abc:Dot(ao) > 0 and abc or -abc
			end
		end
	else
		local b = simplex[1]
		local ab = b - a
		local bcPerp = tripleProduct(ab, ao, ab).Unit
		direction = bcPerp
	end

	return false, direction
end
-- Public Constructors
function GJK.new(SetA, SetB, CentroidA, CentroidB, SupportA, SupportB)
	local self = setmetatable({}, GJK)

	self.SetA = SetA
	self.SetB = SetB
	self.CentroidA = CentroidA
	self.CentroidB = CentroidB
	self.SupportA = SupportA
	self.SupportB = SupportB
	return self
end
-- Public Methods
function GJK:IsColliding()
	local direction = (self.CentroidA - self.CentroidB).Unit
	local simplex = {self.SupportA(self.SetA, direction) - self.SupportB(self.SetB, -direction)}

	direction = -direction

	for i = 1, MAX_TRIES do
		table.insert(simplex, self.SupportA(self.SetA, direction) - self.SupportB(self.SetB, -direction))

		if (simplex[#simplex]:Dot(direction) <= 0) then
			return false
		else
			local passed, newDirection = containsOrigin(self, simplex, direction)

			if (passed) then
				return true
			end

			direction = newDirection
		end
	end

	return false
end
--
return GJK


local ZERO = Vector3.new(0, 0, 0)
local RIGHT = Vector3.new(1, 0, 0)
--
local function rayPlane(p, v, s, n)
	local r = p - s;
	local t = -r:Dot(n) / v:Dot(n)
	return p + t * v, t
end;
--
local Supports = {}
function Supports.PointCloud(set, direction)
	local max, maxDot = set[1], set[1]:Dot(direction)
	for i = 2, #set do
		local dot = set[i]:Dot(direction)
		if (dot > maxDot) then
			max = set[i]
			maxDot = dot
		end
	end
	return max
end
function Supports.Cylinder(set, direction)
	local cf, size2 = unpack(set)
	direction = cf:VectorToObjectSpace(direction)
	local radius = math.min(size2.y, size2.z)
	local dotT, cPoint = direction:Dot(RIGHT), Vector3.new(size2.x, 0, 0)
	local h, t, final

	if (dotT == 0) then
		final = direction.Unit * radius
	else
		cPoint = dotT > 0 and cPoint or -cPoint
		h, t = rayPlane(ZERO, direction, cPoint, RIGHT)
		final = cPoint + (h - cPoint).Unit * radius
	end

	return cf:PointToWorldSpace(final)
end
function Supports.Ellipsoid(set, direction)
	local cf, size2 = unpack(set)
	return cf:PointToWorldSpace(size2 * (size2 * cf:VectorToObjectSpace(direction)).Unit)
end
return Supports