--!strict

local Types = require(script.Parent.Parent.Types)
type SocketLimits = Types.SocketLimits

local NECK_LIMITS: SocketLimits = {MaxFrictionTorque = 55, UpperAngle = 15, TwistLowerAngle = -25, TwistUpperAngle = 25}
local WAIST_LIMITS: SocketLimits = {MaxFrictionTorque = 25, UpperAngle = 40, TwistLowerAngle = -45, TwistUpperAngle = 40}
local SHOULDER_LIMITS: SocketLimits = {MaxFrictionTorque = 25, UpperAngle = 60, TwistLowerAngle = -180, TwistUpperAngle = 180}
local ELBOW_LIMITS: SocketLimits = {MaxFrictionTorque = 25, UpperAngle = 5, TwistLowerAngle = -20, TwistUpperAngle = 160}
local HIP_LIMITS: SocketLimits = {MaxFrictionTorque = 25, UpperAngle = 50, TwistLowerAngle = -30, TwistUpperAngle = 30}
local KNEE_LIMITS: SocketLimits = {MaxFrictionTorque = 25, UpperAngle = 5, TwistLowerAngle = -50, TwistUpperAngle = 10}
local WRIST_LIMITS: SocketLimits = {MaxFrictionTorque = 25, UpperAngle = 20, TwistLowerAngle = -70, TwistUpperAngle = 40}
local ANKLE_LIMITS: SocketLimits = {MaxFrictionTorque = 25, UpperAngle = 10, TwistLowerAngle = -45, TwistUpperAngle = 25}

local Config: Types.RigConfig = {
	Animator = {"Humanoid", "Animator"},
	Humanoid = {"Humanoid"},
	RootPart = {"HumanoidRootPart"},
	CameraSubject = {"Head"},
	Limbs = {
		["HumanoidRootPart"] = {"HumanoidRootPart"},
		["Head"] = {"Head"},
		["LeftHand"] = {"LeftHand"},
		["RightHand"] = {"RightHand"},
		["LeftLowerArm"] = {"LeftLowerArm"},
		["RightLowerArm"] = {"RightLowerArm"},
		["LeftUpperArm"] = {"LeftUpperArm"},
		["RightUpperArm"] = {"RightUpperArm"},
		["LeftFoot"] = {"LeftFoot"},
		["LeftLowerLeg"] = {"LeftLowerLeg"},
		["UpperTorso"] = {"UpperTorso"},
		["LeftUpperLeg"] = {"LeftUpperLeg"},
		["RightFoot"] = {"RightFoot"},
		["RightLowerLeg"] = {"RightLowerLeg"},
		["LowerTorso"] = {"LowerTorso"},
		["RightUpperLeg"] = {"RightUpperLeg"},
	},
	Joints = {
		Neck = {"Head","Neck"},
		Waist = {"UpperTorso","Waist"},

		LeftWrist = {"LeftHand","LeftWrist"},
		LeftElbow = {"LeftLowerArm","LeftElbow"},
		LeftShoulder = {"LeftUpperArm","LeftShoulder"},

		RightWrist = {"RightHand","RightWrist"},
		RightElbow = {"RightLowerArm","RightElbow"},
		RightShoulder = {"RightUpperArm","RightShoulder"},

		RightHip = {"RightUpperLeg","RightHip"},
		RightKnee = {"RightLowerLeg","RightKnee"},
		RightAnkle = {"RightFoot","RightAnkle"},

		LeftHip = {"LeftUpperLeg","LeftHip"},
		LeftKnee = {"LeftLowerLeg","LeftKnee"},
		LeftAnkle = {"LeftFoot","LeftAnkle"},
	},
	Sockets = {
		Neck = NECK_LIMITS,
		Waist = WAIST_LIMITS,

		LeftShoulder = SHOULDER_LIMITS,
		LeftElbow = ELBOW_LIMITS,
		LeftWrist = WRIST_LIMITS,

		RightShoulder = SHOULDER_LIMITS,
		RightElbow = ELBOW_LIMITS,
		RightWrist = WRIST_LIMITS,

		RightHip = HIP_LIMITS,
		RightKnee = KNEE_LIMITS,
		RightAnkle = ANKLE_LIMITS,

		LeftHip = HIP_LIMITS,
		LeftKnee = KNEE_LIMITS,
		LeftAnkle = ANKLE_LIMITS,
	},
	NoCollisionConstraints = {
		{"HumanoidRootPart","UpperTorso"},
		{"HumanoidRootPart","LowerTorso"},
		{"HumanoidRootPart","Head"},
		{"HumanoidRootPart","RightUpperArm"},
		{"HumanoidRootPart","LeftUpperArm"},
		{"HumanoidRootPart","RightLowerArm"},
		{"HumanoidRootPart","LeftLowerArm"},
		{"HumanoidRootPart","RightHand"},
		{"HumanoidRootPart","LeftHand"},
		{"HumanoidRootPart","RightUpperLeg"},
		{"HumanoidRootPart","LeftUpperLeg"},
		{"HumanoidRootPart","RightLowerLeg"},
		{"HumanoidRootPart","LeftLowerLeg"},
		{"HumanoidRootPart","RightFoot"},
		{"HumanoidRootPart","LeftFoot"},

		{"UpperTorso","Head"},
		{"RightUpperArm","Head"},
		{"LeftUpperArm","Head"},
		{"RightLowerArm","Head"},
		{"LeftLowerArm","Head"},
		{"LeftLowerArm","LeftHand"},
		{"LeftHand","LeftUpperArm"},
		{"RightLowerArm","RightHand"},
		{"RightHand","RightUpperArm"},
		{"LeftUpperArm","LeftLowerArm"},
		{"RightUpperArm","RightLowerArm"},
		{"UpperTorso","LeftUpperArm"},
		{"UpperTorso","RightUpperArm"},
		{"LeftLowerLeg","LeftFoot"},
		{"LeftUpperLeg","LeftLowerLeg"},
		{"LeftFoot","LeftLowerLeg"},
		{"LowerTorso","UpperTorso"},
		{"RightLowerLeg","UpperTorso"},
		{"LeftUpperLeg","UpperTorso"},
		{"LeftHand","UpperTorso"},
		{"RightHand","UpperTorso"},
		{"LowerTorso","LeftUpperLeg"},
		{"LeftUpperLeg","RightUpperLeg"},
		{"RightLowerLeg","RightFoot"},
		{"RightUpperLeg","RightLowerLeg"},
		{"RightFoot","RightLowerLeg"},
		{"HumanoidRootPart","LowerTorso"},
		{"RightUpperLeg","LowerTorso"},
		{"LeftLowerLeg","LowerTorso"},
		{"UpperTorso","LowerTorso"},
		{"LeftHand","LowerTorso"},
		{"RightHand","LowerTorso"},
		{"LowerTorso","RightUpperLeg"},
	}
}

return Config


--!strict

local Types = require(script.Parent.Parent.Types)
type SocketLimits = Types.SocketLimits

local NECK_LIMITS: SocketLimits = {MaxFrictionTorque = 10, UpperAngle = 30, TwistLowerAngle = -60, TwistUpperAngle = 60}
local SHOULDER_LIMITS: SocketLimits = {MaxFrictionTorque = 10, UpperAngle = 90, TwistLowerAngle = -45, TwistUpperAngle = 175}
local HIP_LIMITS: SocketLimits = {MaxFrictionTorque = 10, UpperAngle = 60, TwistLowerAngle = -25, TwistUpperAngle = 120}

local Config: Types.RigConfig = {
	Animator = {"Humanoid", "Animator"},
	Humanoid = {"Humanoid"},
	RootPart = {"HumanoidRootPart"},
	CameraSubject = {"Head"},
	Limbs = {
		["HumanoidRootPart"] = {"HumanoidRootPart"},
		["Head"] = {"Head"},
		["Torso"] = {"Torso"},
		["Right Leg"] = {"Right Leg"},
		["Right Arm"] = {"Right Arm"},
		["Left Leg"] = {"Left Leg"},
		["Left Arm"] = {"Left Arm"},
	},
	Joints = {
		Neck = {"Torso", "Neck"},
		RightShoulder = {"Torso", "Right Shoulder"},
		LeftShoulder = {"Torso", "Left Shoulder"},
		RightHip = {"Torso", "Right Hip"},
		LeftHip = {"Torso", "Left Hip"},
	},
	Sockets = {
		Neck = NECK_LIMITS,
		RightShoulder = SHOULDER_LIMITS,
		LeftShoulder = SHOULDER_LIMITS,
		RightHip = HIP_LIMITS,
		LeftHip = HIP_LIMITS,
	},
	NoCollisionConstraints = {
		{"HumanoidRootPart","Torso"},
		{"HumanoidRootPart","Head"},
		{"HumanoidRootPart","Left Arm"},
		{"HumanoidRootPart","Right Arm"},
		{"HumanoidRootPart","Right Leg"},
		{"HumanoidRootPart","Left Leg"},

		{"Head", "Torso"},
		{"Left Arm", "Torso"},
		{"Right Arm", "Torso"},
		{"Left Leg", "Torso"},
		{"Right Leg", "Torso"},
	}
}

return Config


--!strict

local Types = require(script.Parent.Types)
local RigConfigs = {} :: {[string]: Types.RigConfig}

for _, v in script:GetChildren() do
	RigConfigs[v.Name] = require(v) :: any
end

return RigConfigs


--!strict

export type SavedTracks = {{Track: AnimationTrack, Speed: number, TimePosition: number}}
export type CharacterRagdollInfo = {
	Ragdolled: boolean,
	Character: Model,
	RigType: string,
	Animator: Animator?,
	Humanoid: Humanoid?,
	RootPart: BasePart?,
	Limbs: {[string]: BasePart},
	Joints: {[string]: Motor6D},
	Sockets: {[string]: BallSocketConstraint},
	Attachments: {Attachment},
	NoCollisionConstraints: {NoCollisionConstraint},
	Connections: {RBXScriptConnection},
}
export type LocalPlayerRagdollInfo = {
	Character: Model,
	CameraSubject: Instance?,
	PreviousCameraSubject: Instance?,
	Humanoid: Humanoid?,
	Animator: Animator?,
	RootPart: BasePart?,
	Connections: {RBXScriptConnection},
	SavedTracks: SavedTracks,
}

-- Path array is a relative path to a certain instance from Character
-- Example: Animator path -> {"Humanoid", "Animator"}
export type QueryPath = {string}
export type AttachmentConfig = {
	Limb: string,
	Position: Vector3?,
	Orientation: Vector3?,
}
export type SocketLimits = {
	MaxFrictionTorque: number?,
	UpperAngle: number,
	TwistLowerAngle: number,
	TwistUpperAngle: number,
}
export type NoCollisionConfig = {string}
export type RigConfig = {
	Animator: QueryPath?,
	Humanoid: QueryPath?,
	RootPart: QueryPath,
	CameraSubject: QueryPath?,
	Joints: {[string]: QueryPath},
	Limbs: {[string]: QueryPath},
	Sockets: {[string]: SocketLimits},
	NoCollisionConstraints: {NoCollisionConfig},
}

return nil


--!strict

local Util = {}

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

local DEFAULT_MAX_FRICTION_TORQUE = 20
local IsStudio = game:GetService("RunService"):IsStudio()

type SavedTracks = Types.SavedTracks
type CharacterRagdollInfo = Types.CharacterRagdollInfo

local function IsModel(value: any)
	return typeof(value) == "Instance" and value:IsA("Model")
end

local function StudioWarn(msg: string)
	if not IsStudio then return end
	debug.traceback(`[RagdollService]: {msg}`)
end

local function NewAttachment(cframe: CFrame, parent: BasePart): Attachment
	local attachment = Instance.new("Attachment")
	attachment.CFrame = cframe
	attachment.Parent = parent
	return attachment
end

local function NewNoCollision(part0: BasePart, part1: BasePart): NoCollisionConstraint
	local no_collision = Instance.new("NoCollisionConstraint")
	no_collision.Enabled = false
	no_collision.Part0 = part0
	no_collision.Part1 = part1
	no_collision.Parent = part1
	return no_collision
end

local function NewSocket(attachment0: Attachment, attachment1: Attachment, socket_limits: Types.SocketLimits?): BallSocketConstraint
	local socket = Instance.new("BallSocketConstraint")
	socket.Attachment0 = attachment0
	socket.Attachment1 = attachment1
	socket.LimitsEnabled = true
	socket.TwistLimitsEnabled = true
	socket.MaxFrictionTorque = DEFAULT_MAX_FRICTION_TORQUE

	if socket_limits then
		if socket_limits.MaxFrictionTorque then
			socket.MaxFrictionTorque = socket_limits.MaxFrictionTorque
		end
		socket.UpperAngle = socket_limits.UpperAngle
		socket.TwistLowerAngle = socket_limits.TwistLowerAngle
		socket.TwistUpperAngle = socket_limits.TwistUpperAngle
	end

	socket.Parent = attachment0.Parent
	return socket
end

local function QueryInstance(parent: Instance, path: Types.QueryPath?): any?
	if not path then return nil end
	return InstanceQuery:Get(parent, path)
end

local function RegisterDestroyedConnection(info: CharacterRagdollInfo, callback: () -> ())
	table.insert(info.Connections, info.Character.Destroying:Once(callback))
	table.insert(info.Connections, info.Character.AncestryChanged:Once(function(child, parent)
		if parent == nil then
			callback()
		end
	end))
end

local function RegisterRootNoCollision(limb_cache: {[BasePart]: boolean}, limb: BasePart, info: CharacterRagdollInfo)
	local root_part = info.RootPart
	if not root_part or limb == root_part or limb_cache[limb] then return end
	limb_cache[limb] = true
	table.insert(info.NoCollisionConstraints, NewNoCollision(limb, root_part))
end

local function SafeChangeState(humanoid: Humanoid, state: Enum.HumanoidStateType)
	if humanoid:GetState() ~= Enum.HumanoidStateType.Dead then
		humanoid:ChangeState(state)
	end
end

local function ToggleHumanoidRagdoll(ragdolled: boolean, humanoid: Humanoid)
	local state = if ragdolled then Enum.HumanoidStateType.Physics else Enum.HumanoidStateType.GettingUp
	SafeChangeState(humanoid, state)
	humanoid.AutoRotate = not ragdolled
end

local function ToggleRagdollComponents(ragdolled: boolean, info: CharacterRagdollInfo)
	for _, motor6d in info.Joints do
		motor6d.Enabled = not ragdolled
	end

	for _, no_collision in info.NoCollisionConstraints do
		no_collision.Enabled = ragdolled
	end
end

local function SaveTrack(saved_tracks: SavedTracks, track: AnimationTrack)
	table.insert(saved_tracks, {
		Track = track,
		TimePosition = os.clock() - track.TimePosition,
		Speed = track.Speed,
	})
	track:AdjustSpeed(0)
end

Util.IsModel = IsModel
Util.StudioWarn = StudioWarn

Util.NewAttachment = NewAttachment
Util.NewSocket = NewSocket
Util.NewNoCollision = NewNoCollision

Util.RegisterRootNoCollision = RegisterRootNoCollision
Util.RegisterDestroyedConnection = RegisterDestroyedConnection

Util.QueryInstance = QueryInstance
Util.SafeChangeState = SafeChangeState
Util.ToggleHumanoidRagdoll = ToggleHumanoidRagdoll
Util.ToggleRagdollComponents = ToggleRagdollComponents
Util.SaveTrack = SaveTrack

return table.freeze(Util)


--!strict
--!optimize 2

local RagdollService = {}

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

local Util = require(script.Util)
local Types = require(script.Types)
local RigConfigs = require(script.RigConfigs)
local RagdollBuilder = require(script.RagdollBuilder)

local IsServer = RunService:IsServer()
local IsClient = RunService:IsClient()

local RAGDOLL_REMOTE_NAME = "LocalPlayerRagdollRemote"

type CharacterRagdollInfo = Types.CharacterRagdollInfo
type LocalPlayerRagdollInfo = Types.LocalPlayerRagdollInfo

local Camera = workspace.CurrentCamera
local CharacterRagdollInfos = {} :: {[Model]: CharacterRagdollInfo}
local LocalPlayerRagdollInfo: LocalPlayerRagdollInfo? = nil
local LocalPlayerRagdollRemote: RemoteEvent

local function InvalidateLocalPlayerRagdollInfo()
	if not LocalPlayerRagdollInfo then return end
	if Players.LocalPlayer.Character ~= LocalPlayerRagdollInfo.Character then
		LocalPlayerRagdollInfo = nil
	end
end

local function CreateLocalPlayerRagdollInfo(character: Model, ragdoll_type: string): LocalPlayerRagdollInfo
	local info: LocalPlayerRagdollInfo = {
		Character = character,
		SavedTracks = {},
		Connections = {},
	}
	if ragdoll_type == "Generic" then
		info.Humanoid = character:FindFirstChildWhichIsA("Humanoid", true)
		info.Animator = character:FindFirstChildWhichIsA("Animator", true)
		info.CameraSubject = character:FindFirstChild("Head", true)
		info.RootPart = character.PrimaryPart
		if info.Humanoid then
			info.RootPart = info.Humanoid.RootPart
		end
	else
		local config = RigConfigs[ragdoll_type]
		info.Humanoid = Util.QueryInstance(character, config.Humanoid)
		info.Animator = Util.QueryInstance(character, config.Animator)
		info.CameraSubject = Util.QueryInstance(character, config.CameraSubject)
		info.RootPart = Util.QueryInstance(character, config.RootPart)
	end
	return info
end

local function ActivateLocalPlayerRagdoll(ragdoll_type: string)
	InvalidateLocalPlayerRagdollInfo()
	if LocalPlayerRagdollInfo then return end

	local player = Players.LocalPlayer :: Player
	local character = player.Character
	if not character or not character:IsDescendantOf(workspace) then return end

	local info = CreateLocalPlayerRagdollInfo(character, ragdoll_type)
	LocalPlayerRagdollInfo = info

	if info.Humanoid then
		Util.ToggleHumanoidRagdoll(true, info.Humanoid)
	end

	if info.CameraSubject then
		info.PreviousCameraSubject = Camera.CameraSubject
		Camera.CameraSubject = info.CameraSubject
	end

	-- Break the ragdoll balance
	if info.RootPart then
		info.RootPart:ApplyAngularImpulse(info.RootPart.CFrame.RightVector * 50)
	end

	local animator = info.Animator
	if animator then
		-- Let humanoid step once for Animate script compatibility
		table.insert(info.Connections, RunService.PreSimulation:Once(function()
			for _, track: AnimationTrack in animator:GetPlayingAnimationTracks() do
				Util.SaveTrack(info.SavedTracks, track)
			end
			table.insert(info.Connections, animator.AnimationPlayed:Connect(function(track: AnimationTrack)
				Util.SaveTrack(info.SavedTracks, track)
			end))
		end))
	end
end

local function DeactivateLocalPlayerRagdoll(ragdoll_type: string)
	InvalidateLocalPlayerRagdollInfo()
	if not LocalPlayerRagdollInfo then return end
	local info = LocalPlayerRagdollInfo
	LocalPlayerRagdollInfo = nil

	for _, connection in info.Connections do
		connection:Disconnect()
	end

	if info.Humanoid then
		Util.ToggleHumanoidRagdoll(false, info.Humanoid)
	end

	for _, track_info in info.SavedTracks do
		local track = track_info.Track
		local timeposition = os.clock() - track.TimePosition
		if track.Looped or timeposition < track.Length then
			track:AdjustSpeed(track_info.Speed)
			track.TimePosition = timeposition
		else
			track:Stop()
		end
	end

	if info.PreviousCameraSubject then
		Camera.CameraSubject = info.PreviousCameraSubject
	end
end

local function ConnectLocalPlayerRagdollRemote(remote: RemoteEvent)
	remote.OnClientEvent:Connect(function(enabled: boolean, ragdoll_type: string)
		if enabled then
			ActivateLocalPlayerRagdoll(ragdoll_type)
		else
			DeactivateLocalPlayerRagdoll(ragdoll_type)
		end
	end)
end

local function IsRagdolled(character: Model): boolean
	local info = CharacterRagdollInfos[character]
	if not info then return false end
	return info.Ragdolled
end

local function IsPlayerRagdolled(player: Player): boolean
	if not player.Character then return false end
	return IsRagdolled(player.Character)
end

local function GetRigType(character: Model): string
	local rig_type = character:GetAttribute("RigType") :: string?
	if rig_type then return rig_type end

	local humanoid = character:FindFirstChildOfClass("Humanoid")
	if humanoid then
		if humanoid.RigType == Enum.HumanoidRigType.R6 then
			return "R6"
		elseif humanoid.RigType == Enum.HumanoidRigType.R15 then
			return "R15"
		end
	end

	return "Generic"
end

local function DestroyRagdoll(character: Model)
	local info = CharacterRagdollInfos[character]
	if not info then return end

	for _, no_collision in info.NoCollisionConstraints do
		no_collision:Destroy()
	end
	for _, socket in info.Sockets do
		socket:Destroy()
	end
	for _, attachment in info.Attachments do
		attachment:Destroy()
	end
	for _, c in info.Connections do
		c:Disconnect()
	end
	CharacterRagdollInfos[character] = nil
end

local function SetupRagdoll(character: Model, rig_type: string?): boolean
	assert(Util.IsModel(character), "Character is not model.")
	if not character:IsDescendantOf(workspace) then return false end

	local rig_type = rig_type or GetRigType(character)
	if not RagdollBuilder.Validate(character, rig_type) then
		return false
	end

	local info = RagdollBuilder.Build(character, rig_type)
	Util.RegisterDestroyedConnection(info, function()
		DestroyRagdoll(character)
	end)
	CharacterRagdollInfos[character] = info
	return true
end

local function ActivateRagdoll(character: Model): boolean
	assert(Util.IsModel(character), "Character is not model.")

	local info = CharacterRagdollInfos[character]
	if not info or info.Ragdolled then return false end
	info.Ragdolled = true

	if IsServer then
		local player = Players:GetPlayerFromCharacter(character)
		if player then
			LocalPlayerRagdollRemote:FireClient(player, true, info.RigType)
		end
	end

	if info.Humanoid then
		Util.ToggleHumanoidRagdoll(true, info.Humanoid)
	end

	Util.ToggleRagdollComponents(true, info)

	if info.Animator then
		local animator = info.Animator
		for _, track in animator:GetPlayingAnimationTracks() do
			track:Stop()
		end
	end

	-- Break the ragdoll balance on server if owned by server on client if local rig
	if info.RootPart and (IsClient or info.RootPart:GetNetworkOwner() == nil) then
		info.RootPart:ApplyAngularImpulse(info.RootPart.CFrame.RightVector * 50)
	end

	return true
end

local function DeactivateRagdoll(character: Model): boolean
	assert(Util.IsModel(character), "Character is not model.")

	local info = CharacterRagdollInfos[character]
	if not info or not info.Ragdolled then return false end
	info.Ragdolled = false

	if info.Humanoid then
		Util.ToggleHumanoidRagdoll(false, info.Humanoid)
	end
	Util.ToggleRagdollComponents(false, info)

	if IsServer then
		local player = Players:GetPlayerFromCharacter(character)
		if player then
			LocalPlayerRagdollRemote:FireClient(player, false, info.RigType)
		end
	end

	return true
end

local function Ragdoll(character: Model, rig_type: string?): boolean
	assert(Util.IsModel(character), "Character is not model.")

	local info = CharacterRagdollInfos[character]
	if not info then
		local success = SetupRagdoll(character, rig_type)
		if not success then return false end
	end

	ActivateRagdoll(character)

	return true
end

local function Unragdoll(character: Model): boolean
	assert(Util.IsModel(character), "Character is not model.")

	local info = CharacterRagdollInfos[character]
	if not info then return false end

	DeactivateRagdoll(character)
	DestroyRagdoll(character)

	return true
end

if IsServer then
	LocalPlayerRagdollRemote = Instance.new("RemoteEvent")
	LocalPlayerRagdollRemote.Name = RAGDOLL_REMOTE_NAME
	LocalPlayerRagdollRemote.Parent = script
else
	LocalPlayerRagdollRemote = script:FindFirstChild(RAGDOLL_REMOTE_NAME) :: RemoteEvent
	if LocalPlayerRagdollRemote then
		ConnectLocalPlayerRagdollRemote(LocalPlayerRagdollRemote)
	end
	local connection: RBXScriptConnection
	connection = script.ChildAdded:Connect(function(child: Instance)
		if child:IsA("RemoteEvent") and child.Name == RAGDOLL_REMOTE_NAME then
			connection:Disconnect()
			ConnectLocalPlayerRagdollRemote(child)
		end
	end)
end

RagdollService.IsPlayerRagdolled = IsPlayerRagdolled
RagdollService.IsRagdolled = IsRagdolled
RagdollService.SetupRagdoll = SetupRagdoll
RagdollService.DestroyRagdoll = DestroyRagdoll
RagdollService.ActivateRagdoll = ActivateRagdoll
RagdollService.DeactivateRagdoll = DeactivateRagdoll
RagdollService.Ragdoll = Ragdoll
RagdollService.Unragdoll = Unragdoll

return table.freeze(RagdollService)


--!strict
local TestEZ = require(script.Parent.TestEZ) :: any
TestEZ.TestBootstrap:run {script}

-- Test type annotations
local Val = require(script.Val)

local x = Val.new(2)
local z = Val.new("Str")

local y = Val.calc(function(get)
	return get(x) .. get(z)
end)

x:on(function(value, old, wasCalledImmediately)
	local percent = value / 100
end)

Val.batch(function(set)
	set(x, 3)
	set(z, "E")
end)


export type Expectation = {
	to: Expectation,
	be: Expectation,
	been: Expectation,
	have: Expectation,
	was: Expectation,
	at: Expectation,
	never: Expectation,
	a: (typeName: string?) -> Expectation,
	an: (typeName: string?) -> Expectation,
	ok: () -> Expectation,
	throw: (message: string?) -> Expectation,
	near: (goal: number, limit: number?) -> Expectation,
	equal: (goal: any) -> Expectation,
}

declare function describe(name: string, out: () -> ()): ()

declare function it(name: string, out: () -> ()): ()

declare function expect(value: any): Expectation

declare function afterAll(func: () -> ()): ()

declare function afterEach(func: () -> ()): ()

declare function beforeAll(func: () -> ()): ()

declare function beforeEach(func: () -> ()): ()

declare function FIXME(message: string?): ()

declare function FOCUS(): ()

declare function SKIP(): ()

declare function describeFOCUS(phrase: string): ()

declare function describeSKIP(phrase: string): ()

declare function itFOCUS(phrase: string, callback: ((context: {[any]: any}) -> ())): ()

declare function itSKIP(phrase: string, callback: ((context: {[any]: any}) -> ())): ()

declare function itFIXME(phrase: string, callback: ((context: {[any]: any}) -> ())): ()


--[[
TheNexusAvenger

Loader for Nexus VR Backpack.
--]]
--!strict

local ReplicatedStorage = game:GetService("ReplicatedStorage")

return function()
    --Copy NexusVRBackpack to ReplicatedStorage if it doesn't exist already.
    --Client setup is up to Nexus VR Character Model or the game creator.
    if ReplicatedStorage:FindFirstChild("NexusVRBackpack") then return end
    script:WaitForChild("NexusVRBackpack").Parent = ReplicatedStorage
end


--[[
TheNexusAvenger

Loads Nexus VR Backpack on the server for testing.
--]]

local TOTAL_TEST_TOOLS = 9

local ServerScriptService = game:GetService("ServerScriptService")
local StarterPack = game:GetService("StarterPack")
local HttpService = game:GetService("HttpService")

--Create the test tools.
for i = 1, TOTAL_TEST_TOOLS do
    local TestTool = Instance.new("Tool")
    TestTool.Name = HttpService:GenerateGUID()
    if math.random() > 0.5 then
        TestTool.TextureId = "http://www.roblox.com/asset/?id=94746192"
    end
    TestTool.Parent = StarterPack

    local TestHandle = Instance.new("Part")
    TestHandle.BrickColor = BrickColor.random()
    TestHandle.Size = Vector3.new(1, 1, 1)
    TestHandle.Name = "Handle"
    TestHandle.CanCollide = false
    TestHandle.Parent = TestTool
end

--Load Nexus VR Backpack.
require(ServerScriptService:WaitForChild("MainModule"))()


--[[
TheNexusAvenger

Loads Nexus VR Backpack on the client for testing.
--]]

local Workspace = game:GetService("Workspace")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local GuiService = game:GetService("GuiService")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")

local NexusVRBackpack = require(ReplicatedStorage:WaitForChild("NexusVRBackpack"))

--Prepare Nexus VR Backpack for non-VR testing.
if not UserInputService.VREnabled then
    --Modify the CharacterBackpack for non-VR testing.
    local CharacterBackpack = require(ReplicatedStorage:WaitForChild("NexusVRBackpack"):WaitForChild("CharacterBackpack"))
    CharacterBackpack.GetBackpackCFrame = function(self)
        return Workspace.CurrentCamera.CFrame * CFrame.new(0, 0, -2)
    end

    CharacterBackpack.GetHandPosition = function(self)
        --The following is long because the API to test with is in world space.
        --Since the backpack is a SurfaceGui in 3D space, this is required to translate the mouse into 3D space relative to the backpack.
        local BackpackSize = self.Backpack.Part.Size
        local BackpackCFrame = Workspace.CurrentCamera.CFrame * CFrame.new(0, 0, -2)
        local MouseLocation = UserInputService:GetMouseLocation() - GuiService:GetGuiInset()
        local UpperLeftCorner = Workspace.CurrentCamera:WorldToScreenPoint((BackpackCFrame * CFrame.new(-BackpackSize.X / 2, BackpackSize.Y / 2, 0)).Position)
        local BottomRightCorner = Workspace.CurrentCamera:WorldToScreenPoint((BackpackCFrame * CFrame.new(BackpackSize.X / 2, -BackpackSize.Y / 2, 0)).Position)
        local RelativeX = (MouseLocation.X - UpperLeftCorner.X) / (BottomRightCorner.X - UpperLeftCorner.X)
        local RelativeY = (MouseLocation.Y - UpperLeftCorner.Y) / (BottomRightCorner.Y - UpperLeftCorner.Y)
        return (BackpackCFrame * CFrame.new(BackpackSize.X * (RelativeX - 0.5), BackpackSize.Y * (0.5 - RelativeY), 0)).Position
    end

    --Set the non-VR keybind.
    NexusVRBackpack:SetKeyCode(Enum.KeyCode.Q)
end

--Load Nexus VR Backpack.
NexusVRBackpack:Load()


--Backpack associated with a character.
--!strict

local PROCESSED_KEYCODES_WHITELIST = {
    [Enum.KeyCode.ButtonL3] = true, --Left thumbstick.
    [Enum.KeyCode.ButtonR3] = true, --Right thumbstick.
}

local VRService = game:GetService("VRService")
local Workspace = game:GetService("Workspace")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")

local Backpack3D = require(script.Parent:WaitForChild("UI"):WaitForChild("Backpack3D"))

local CharacterBackpack = {}
CharacterBackpack.__index = CharacterBackpack

export type CharacterBackpack = {
    Enabled: boolean,
    Player: Player,
    KeyCode: Enum.KeyCode,
    UserCFrame: Enum.UserCFrame,
    Events: {RBXScriptConnection},
    Humanoid: Humanoid,
    Backpack: Backpack3D.Backpack3D,
    UpdateFocusEvent: RBXScriptConnection?,
} & typeof(setmetatable({}, CharacterBackpack))

--[[
Creates the character backpack.
--]]
function CharacterBackpack.new(Character: Model): CharacterBackpack
    --Create the object.
    local self = setmetatable({
        Enabled = true,
        Player = Players:GetPlayerFromCharacter(Character),
        KeyCode = Enum.KeyCode.ButtonR3,
        UserCFrame = Enum.UserCFrame.RightHand,
        Events = {},
    }, CharacterBackpack) :: CharacterBackpack

    --Wait for the Humanoid.
    local Humanoid = Character:FindFirstChildOfClass("Humanoid")
    while not Humanoid do
        Character.ChildAdded:Wait()
        Humanoid = Character:FindFirstChildOfClass("Humanoid")
    end
    self.Humanoid = Humanoid :: Humanoid

    --Create the backpack.
    self.Backpack = Backpack3D.new(self.Player:WaitForChild("PlayerGui"), {Character, self.Player:WaitForChild("Backpack")})

    --Connect the interact events.
    table.insert(self.Events, UserInputService.InputBegan:Connect(function(Input, Processed)
        if Input.KeyCode ~= self.KeyCode then return end
        if Processed and not PROCESSED_KEYCODES_WHITELIST[Input.KeyCode] then return end
        if #self.Backpack.Inventory.Tools == 0 then return end
        self:Open()
    end))
    table.insert(self.Events, UserInputService.InputEnded:Connect(function(Input)
        if Input.KeyCode ~= self.KeyCode then return end
        self:Close()
    end))

    --Connect the destroy events.
    table.insert(self.Events, (self :: any).Humanoid.Died:Connect(function()
        self:Destroy()
    end))
    table.insert(self.Events, self.Player.CharacterAdded:Connect(function()
        self:Destroy()
    end))
    table.insert(self.Events, self.Player.CharacterRemoving:Connect(function()
        self:Destroy()
    end))

    --Set the default KeyCode.
    --This will open up the backpack if it is already pressed.
    self:SetKeyCode(Enum.KeyCode.ButtonR3)

    --Return the object.
    return self
end

--[[
Returns the CFrame to open the backpack at.
--]]
function CharacterBackpack.GetBackpackCFrame(self: CharacterBackpack): CFrame
    return (Workspace.CurrentCamera:GetRenderCFrame() * UserInputService:GetUserCFrame(Enum.UserCFrame.Head):Inverse() * UserInputService:GetUserCFrame(self.UserCFrame))
end

--[[
Returns the position the hand is at.
--]]
function CharacterBackpack.GetHandPosition(self: CharacterBackpack): Vector3
    return self:GetBackpackCFrame().Position
end

--[[
Sets the key to use for opening and closing the backpack.
--]]
function CharacterBackpack.SetKeyCode(self: CharacterBackpack, KeyCode: Enum.KeyCode): ()
    self.KeyCode = KeyCode
    if UserInputService:IsKeyDown(KeyCode) then
        self:Open()
    end
end

--[[
Sets the input to open the menu at.
--]]
function CharacterBackpack.SetUserCFrame(self: CharacterBackpack, UserCFrame: Enum.UserCFrame): ()
    self.UserCFrame = UserCFrame
end

--[[
Opens the backpack.
--]]
function CharacterBackpack.Open(self: CharacterBackpack): ()
    --Open the backpack.
    if not self.Enabled then return end
    if self.Backpack.Opened then return end
    self.Backpack:Open()
    local NexusVRCharacterModelControllerApi = (self :: any).NexusVRCharacterModelControllerApi
    if NexusVRCharacterModelControllerApi then
        NexusVRCharacterModelControllerApi:DisableControllerInput(self.UserCFrame)
    end

    --Update the position and focus until it is closed.
    local OriginOffset = (Workspace.CurrentCamera:GetRenderCFrame() * VRService:GetUserCFrame(Enum.UserCFrame.Head):Inverse()):Inverse() * self:GetBackpackCFrame()
    self.UpdateFocusEvent = RunService.RenderStepped:Connect(function()
        self.Backpack:MoveTo(Workspace.CurrentCamera:GetRenderCFrame() * VRService:GetUserCFrame(Enum.UserCFrame.Head):Inverse() * OriginOffset)
        self.Backpack:UpdateFocusedToolWorldSpace(self:GetHandPosition())
    end)
end

--[[
Closes the backpack.
--]]
function CharacterBackpack.Close(self: CharacterBackpack): ()
    if not self.Backpack.Opened then return end

    --Update the tool.
    if self.UpdateFocusEvent then
        self.UpdateFocusEvent:Disconnect()
        self.UpdateFocusEvent = nil
    end
    local SelectedTool = self.Backpack:GetFocusedTool()
    if SelectedTool then
        (self.Humanoid :: Humanoid):EquipTool(SelectedTool :: Tool)
    else
        (self.Humanoid :: Humanoid):UnequipTools()
    end

    --Close the backpack.
    self.Backpack:Close()
    local NexusVRCharacterModelControllerApi = (self :: any).NexusVRCharacterModelControllerApi
    if NexusVRCharacterModelControllerApi then
        NexusVRCharacterModelControllerApi:EnableControllerInput(self.UserCFrame)
    end
end

--[[
Destroys the backpack.
--]]
function CharacterBackpack.Destroy(self: CharacterBackpack): ()
    --Disconnect the events.
    if self.UpdateFocusEvent then
        self.UpdateFocusEvent:Disconnect()
        self.UpdateFocusEvent = nil
    end
    for _, Event in self.Events do
        Event:Disconnect()
    end
    self.Events = {}

    --Close and clear the backpack.
    if self.Backpack.Opened then
        self.Backpack:Close()
        task.delay(0.1, function()
            self.Backpack:Destroy()
        end)
    else
        self.Backpack:Destroy()
    end
end

return CharacterBackpack


--Simple state manager for the tools the player has access to.
--!strict

local Inventory = {}
Inventory.__index = Inventory

export type Inventory = {
    Containers: {Instance},
    Tools: {BackpackItem},
    Events: {RBXScriptConnection},
    ToolsChangedEvent: BindableEvent,
    ToolsChanged: RBXScriptSignal,
} & typeof(setmetatable({}, Inventory))

--[[
Creates the inventory.
--]]
function Inventory.new(Containers: {Instance}?): Inventory
    --Create the object.
    local ToolsChangedEvent = Instance.new("BindableEvent")
    local self = setmetatable({
        Containers = {},
        Tools = {},
        Events = {},
        ToolsChangedEvent = ToolsChangedEvent,
        ToolsChanged = ToolsChangedEvent.Event :: RBXScriptSignal,
    }, Inventory) :: Inventory

    --Add the containers.
    if Containers then
        for _, Container in Containers do
            self:AddContainer(Container)
        end
    end

    --Return the object.
    return self
end

--[[
Adds a tool.
--]]
function Inventory.AddTool(self: Inventory, Tool: BackpackItem): ()
    if not Tool:IsA("BackpackItem") then return end

    --Return if the tool is already stored.
    for i, OtherTool in self.Tools do
        if Tool == OtherTool then
            return
        end
    end

    --Add the tool.
    table.insert(self.Tools, Tool)
    self.ToolsChangedEvent:Fire()
end

--[[
Adds a tool.
--]]
function Inventory.RemoveTool(self: Inventory, Tool: BackpackItem): ()
    if not Tool:IsA("BackpackItem") then return end

    --Return if the tool is in one of the containers.
    for _, Container in self.Containers do
        if Tool.Parent == Container then
            return
        end
    end

    --Determine the index and remove the tool.
    local ToolIndex = nil
    for i, OtherTool in self.Tools do
        if Tool == OtherTool then
            ToolIndex = i
            break
        end
    end
    if not ToolIndex then return end
    table.remove(self.Tools, ToolIndex)
    self.ToolsChangedEvent:Fire()
end

--[[
Adds a container to monitor.
--]]
function Inventory.AddContainer(self: Inventory, Container: Instance): ()
    table.insert(self.Containers, Container)
    for _, Tool in Container:GetChildren() do
        self:AddTool(Tool :: BackpackItem)
    end
    table.insert(self.Events, Container.ChildAdded:Connect(function(Tool)
        self:AddTool(Tool :: BackpackItem)
    end))
    table.insert(self.Events, Container.ChildRemoved:Connect(function(Tool)
        self:RemoveTool(Tool :: BackpackItem)
    end))
end

--[[
Destroys the inventory.
--]]
function Inventory.Destroy(self: Inventory): ()
    for _, Event in self.Events do
        Event:Disconnect()
    end
    self.Events = {}
    self.Tools = {}
    self.Containers = {}
    self.ToolsChangedEvent:Destroy()
end

return Inventory


--Class for the backpack in 3D space.
--!strict

local UI_PHYSICAL_SCALE = 0.5

local TweenService = game:GetService("TweenService")

local ToolGrid = require(script.Parent:WaitForChild("ToolGrid"))
local Inventory = require(script.Parent.Parent:WaitForChild("State"):WaitForChild("Inventory"))

local Backpack3D = {}
Backpack3D.__index = Backpack3D

export type Backpack3D = {
    Opened: boolean,
    Inventory: Inventory.Inventory,
    SurfaceGui: SurfaceGui,
    Part: Part,
    CenterFrame: Frame,
    Cursor: Frame,
    ToolGrid: ToolGrid.ToolGrid,
} & typeof(setmetatable({}, Backpack3D))

--[[
Creates the 3D backpack.
--]]
function Backpack3D.new(Parent: Instance, Containers: {Instance}): Backpack3D
    --Create the object.
    local self = setmetatable({
        Opened = false,
    }, Backpack3D) :: Backpack3D

    --Create the adornment.
    local SurfaceGui = Instance.new("SurfaceGui")
    SurfaceGui.Name = "NexusVRBackpack"
    SurfaceGui.AlwaysOnTop = true
    SurfaceGui.Enabled = false
    SurfaceGui.LightInfluence = 0
    SurfaceGui.Face = Enum.NormalId.Back
    SurfaceGui.SizingMode = Enum.SurfaceGuiSizingMode.PixelsPerStud
    SurfaceGui.PixelsPerStud = 250
    SurfaceGui.Parent = Parent
    self.SurfaceGui = SurfaceGui

    local Part = Instance.new("Part")
    Part.Transparency = 1
    Part.Size = Vector3.zero
    Part.Anchored = true
    Part.CanCollide = false
    Part.CanQuery = false
    Part.Parent = SurfaceGui
    SurfaceGui.Adornee = Part
    self.Part = Part

    local CenterFrame = Instance.new("Frame")
    CenterFrame.BackgroundTransparency = 1
    CenterFrame.AnchorPoint = Vector2.new(0.5, 0.5)
    CenterFrame.Position = UDim2.new(0.5, 0, 0.5, 0)
    CenterFrame.Parent = SurfaceGui
    self.CenterFrame = CenterFrame

    local Cursor = Instance.new("Frame")
    Cursor.BackgroundColor3 = Color3.new(1, 1, 1)
    Cursor.Size = UDim2.new(0.1, 0, 0.1, 0)
    Cursor.AnchorPoint = Vector2.new(0.5, 0.5)
    Cursor.ZIndex = 10
    Cursor.Parent = CenterFrame
    self.Cursor = Cursor

    local UICorner = Instance.new("UICorner")
    UICorner.CornerRadius = UDim.new(1, 0)
    UICorner.Parent = Cursor

    --Create the tool grid.
    local BackpackToolGrid = ToolGrid.new()
    BackpackToolGrid.AdornFrame.Size = UDim2.new(0, 0, 0, 0)
    BackpackToolGrid.AdornFrame.Parent = CenterFrame
    self.ToolGrid = BackpackToolGrid

    --Create the inventory.
    local BackpackInventory = Inventory.new(Containers)
    self.Inventory = BackpackInventory
    BackpackInventory.ToolsChanged:Connect(function()
        self:UpdateInventory()
    end)
    self:UpdateInventory()

    --Return the object.
    return self
end

--[[
Returns the focused tool, if any.
--]]
function Backpack3D.GetFocusedTool(self: Backpack3D): BackpackItem?
    return self.ToolGrid.FocusedIcon and self.ToolGrid.FocusedIcon.Tool
end

--[[
Updates the backpack when the tools change.
--]]
function Backpack3D.UpdateInventory(self: Backpack3D): ()
    --Update the tools.
    self.ToolGrid:SetTools(self.Inventory.Tools)

    --Update the size.
    local Diameter = (#self.ToolGrid.IconGroups * 2) + 1
    local DiameterScaled = Diameter * (math.sqrt(3) / 2)
    self.Part.Size = Vector3.new(DiameterScaled * UI_PHYSICAL_SCALE, DiameterScaled * UI_PHYSICAL_SCALE, 0)
    self.CenterFrame.Size = UDim2.new(1 / DiameterScaled, 0, 1 / DiameterScaled, 0)
end

--[[
Updates the focused tool in local space.
--]]
function Backpack3D.UpdateFocusedToolLocalSpace(self: Backpack3D, RelativeX: number, RelativeY: number): ()
    if not self.Opened then return end
    local SurfaceGuiPositionX, SurfaceGuiPositionY = (self.SurfaceGui :: SurfaceGui).AbsoluteSize.X * RelativeX, (self.SurfaceGui :: SurfaceGui).AbsoluteSize.Y * RelativeY
    local GridX = (SurfaceGuiPositionX - self.CenterFrame.AbsolutePosition.X) / self.CenterFrame.AbsoluteSize.X
    local GridY = (SurfaceGuiPositionY - self.CenterFrame.AbsolutePosition.Y) / self.CenterFrame.AbsoluteSize.Y
    self.Cursor.Position = UDim2.new(GridX, 0, GridY, 0)
    self.ToolGrid:UpdateFocusedIcon(GridX, GridY)
end

--[[
Updates the focused tool in world space.
--]]
function Backpack3D.UpdateFocusedToolWorldSpace(self: Backpack3D, Position: Vector3): ()
    local RelativeCFrame = (self.Part :: Part).CFrame:Inverse() * CFrame.new(Position)
    local Size = self.Part.Size
    self:UpdateFocusedToolLocalSpace((RelativeCFrame.X / Size.X) + 0.5, 0.5 - (RelativeCFrame.Y / Size.Y))
end

--[[
Moves the backpack to the specified CFrame.
--]]
function Backpack3D.MoveTo(self: Backpack3D,Location: CFrame): ()
    self.Part.CFrame = Location
end

--[[
Opens the backpack.
--]]
function Backpack3D.Open(self: Backpack3D): ()
    if self.Opened then return end
    self.Opened = true
    self.SurfaceGui.Enabled = true
    TweenService:Create(self.ToolGrid.AdornFrame, TweenInfo.new(0.1), {
        Size = UDim2.new(1, 0, 1, 0)
    }):Play()
end

--[[
Closes the backpack.
--]]
function Backpack3D.Close(self: Backpack3D): ()
    if not self.Opened then return end
    self:UpdateFocusedToolLocalSpace(math.huge, math.huge)
    self.Opened = false
    TweenService:Create(self.ToolGrid.AdornFrame, TweenInfo.new(0.1), {
        Size = UDim2.new(0, 0, 0, 0)
    }):Play()
    task.delay(0.1, function()
        if self.Opened then return end
        self.SurfaceGui.Enabled = false
    end)
end

--[[
Destroys the backpack.
--]]
function Backpack3D.Destroy(self: Backpack3D): ()
    self.SurfaceGui:Destroy()
    self.ToolGrid:Destroy()
    self.Inventory:Destroy()
end

return Backpack3D


--Hexagon grid for showing tools.
--!strict

local ToolIcon = require(script.Parent:WaitForChild("ToolIcon"))

local ToolGrid = {}
ToolGrid.__index = ToolGrid

export type ToolGrid = {
    IconGroups: {{ToolIcon.ToolIcon}},
    AdornFrame: Frame,
    FocusedIcon: ToolIcon.ToolIcon?,
} & typeof(setmetatable({}, ToolGrid))

--[[
Creates the tool grid.
--]]
function ToolGrid.new(): ToolGrid
    --Create the object.
    local self = setmetatable({
        IconGroups = {},
    }, ToolGrid) :: ToolGrid

    --Create the adorn frame.
    local AdornFrame = Instance.new("Frame")
    AdornFrame.BackgroundTransparency = 1
    AdornFrame.AnchorPoint = Vector2.new(0.5, 0.5)
    AdornFrame.Position = UDim2.new(0.5, 0, 0.5, 0)
    self.AdornFrame = AdornFrame

    --Return the object.
    return self
end

--[[
Creates a ToolIcon.
--]]
function ToolGrid.CreateToolIcon(self: ToolGrid, RelativePositionX: number, RelativePositionY: number)
    return ToolIcon.new(self.AdornFrame, RelativePositionX, RelativePositionY)
end

--[[
Sets the radius of the grid.
--]]
function ToolGrid.SetRadius(self: ToolGrid, Radius: number): ()
    Radius = math.max(1, Radius)
    if Radius == #self.IconGroups then return end

    --Create the grids that don't exist.
    for i = #self.IconGroups + 1, Radius do
        --Create the top row.
        local NewGroup = {}
        for j = 1, i + 1 do
            table.insert(NewGroup, self:CreateToolIcon((-(i + 1) / 2) + j - 0.5, -i))
        end

        --Create the sides.
        local LeftSideIcons = {}
        local CurrentRowSize = i + 1
        for j = 2, (2 * i) do
            if j <= i + 1 then
                CurrentRowSize += 1
            else
                CurrentRowSize += -1
            end
            table.insert(LeftSideIcons, self:CreateToolIcon((-(CurrentRowSize) / 2) + 1 - 0.5, -i + j - 1))
            table.insert(NewGroup, self:CreateToolIcon((-(CurrentRowSize) / 2) + CurrentRowSize - 0.5, -i + j - 1))
        end

        --Create the bottom row and store the left side icons.
        for j = i + 1, 1, -1 do
            table.insert(NewGroup, self:CreateToolIcon((-(i + 1) / 2) + j - 0.5, i))
        end
        for j = #LeftSideIcons, 1, -1 do
            table.insert(NewGroup, LeftSideIcons[j])
        end

        --Store the group.
        table.insert(self.IconGroups, NewGroup)
    end

    --Remove the extra rows.
    for i = #self.IconGroups, Radius + 1, -1 do
        local Group = self.IconGroups[i]
        table.remove(self.IconGroups, i)
        for _, Icon in Group do
            Icon:Destroy()
        end
    end
end

--[[
Sets the tools for the grid to show.
--]]
function ToolGrid.SetTools(self: ToolGrid, Tools: {BackpackItem}): ()
    --Update the radius.
    local MinimumRadius = 1
    local OpenSlots = 6
    while #Tools > OpenSlots do
        MinimumRadius += 1
        OpenSlots += (6 * MinimumRadius)
    end
    self:SetRadius(MinimumRadius)

    --Assign the tools.
    local CurrentToolId = 1
    for _, IconGroup in self.IconGroups do
        for _, Icon in IconGroup do
            Icon:SetTool(Tools[CurrentToolId])
            CurrentToolId += 1
        end
    end
end

--[[
Updates the icon that is focused.
--]]
function ToolGrid.UpdateFocusedIcon(self: ToolGrid, RelativePositionX: number, RelativePositionY: number): ()
    --Determine the new focused icon.
    local NewFocusedIcon = nil
    local DistanceToFocusedIcon = 0.5
    for _, IconGroup in self.IconGroups do
        for _, Icon in IconGroup do
            local DistanceTo = (((RelativePositionX - Icon.RelativePositionX) ^ 2) + ((RelativePositionY - Icon.RelativePositionY) ^ 2)) ^ 0.5
            if DistanceTo < DistanceToFocusedIcon then
                NewFocusedIcon = Icon
                DistanceToFocusedIcon = DistanceTo
            end
        end
    end

    --Update the focused icon.
    if NewFocusedIcon ~= self.FocusedIcon then
        if self.FocusedIcon then
            self.FocusedIcon:SetFocused(false)
        end
        if NewFocusedIcon then
            NewFocusedIcon:SetFocused(true)
        end
        self.FocusedIcon = NewFocusedIcon
    end
end

--[[
Destroys the tool grid.
--]]
function ToolGrid.Destroy(self: ToolGrid)
    self.AdornFrame:Destroy()
    for _, IconGroup in self.IconGroups do
        for _, Icon in IconGroup do
            Icon:Destroy()
        end
    end
    self.IconGroups = {}
end

return ToolGrid


--Class for a tool icon.
--!strict

local ToolIcon = {}
ToolIcon.__index = ToolIcon

export type ToolIcon = {
    Focused: boolean,
    Players: Players,
    TweenService: TweenService,
    RelativePositionX: number,
    RelativePositionY: number,
    Background: ImageLabel,
    ToolText: TextLabel,
    ToolImage: ImageLabel,
    Tool: BackpackItem?,
    ToolEvents: {RBXScriptConnection},
} & typeof(setmetatable({}, ToolIcon))

--[[
Creates the tool icon.
--]]
function ToolIcon.new(Parent: GuiObject, PositionX: number, PositionY: number): ToolIcon
    --Create the object.
    local self = setmetatable({
        Focused = false,
        Players = game:GetService("Players"), --Stored to be replaced in unit tests.
        TweenService = game:GetService("TweenService"), --Stored to be replaced in unit tests.
        RelativePositionX = (PositionX * (math.sqrt(3) / 2)) + 0.5,
        RelativePositionY = (PositionY * 0.75) + 0.5,
        ToolEvents = {},
    }, ToolIcon) :: ToolIcon

    --Create the frames.
    local Background = Instance.new("ImageLabel")
    Background.BackgroundTransparency = 1
    Background.AnchorPoint = Vector2.new(0.5, 0.5)
    Background.Size = UDim2.new(0.9, 0, 0.9, 0)
    Background.Position = UDim2.new(self.RelativePositionX, 0, self.RelativePositionY, 0)
    Background.Image = "http://www.roblox.com/asset/?id=10708006436"
    Background.ImageColor3 = Color3.new(0.1, 0.1, 0.1)
    Background.ImageTransparency = 0.8
    Background.Parent = Parent
    self.Background = Background

    local ToolText = Instance.new("TextLabel")
    ToolText.BackgroundTransparency = 1
    ToolText.AnchorPoint = Vector2.new(0.5, 0.5)
    ToolText.Position = UDim2.new(0.5, 0, 0.5, 0)
    ToolText.Size = UDim2.new(0.625, 0, 0.625, 0)
    ToolText.Font = Enum.Font.SourceSans
    ToolText.TextColor3 = Color3.new(1, 1, 1)
    ToolText.Text = ""
    ToolText.TextSize = (Background.AbsoluteSize.Y * 0.625) / 4
    ToolText.TextWrapped = true
    ToolText.Visible = false
    ToolText.Parent = Background
    self.ToolText = ToolText
    Background:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
        ToolText.TextSize = (Background.AbsoluteSize.Y * 0.625) / 4
    end)

    local ToolImage = Instance.new("ImageLabel")
    ToolImage.BackgroundTransparency = 1
    ToolImage.AnchorPoint = Vector2.new(0.5, 0.5)
    ToolImage.Position = UDim2.new(0.5, 0, 0.5, 0)
    ToolImage.Size = UDim2.new(0.625, 0, 0.625, 0)
    ToolImage.Visible = false
    ToolImage.Parent = Background
    self.ToolImage = ToolImage

    --Return the object.
    return self
end

--[[
Updates the color of the icon.
--]]
function ToolIcon.UpdateColor(self: ToolIcon): ()
    --Update the text and icon.
    if self.Tool then
        if self.Tool.TextureId == "" then
            self.ToolText.Visible = true
            self.ToolImage.Visible = false
            self.ToolText.Text = self.Tool.Name
        else
            self.ToolText.Visible = false
            self.ToolImage.Visible = true
            self.ToolImage.Image = self.Tool.TextureId
        end
    else
        self.ToolText.Visible = false
        self.ToolImage.Visible = false
    end

    --Determine the color, transparency, and size.
    local Color = Color3.new(0.1, 0.1, 0.1)
    local Transparency = (self.Tool and 0.5 or 0.8)
    local Size = ((self.Tool and self.Focused and UDim2.new(1.05, 0, 1.05, 0)) or UDim2.new(0.9, 0, 0.9, 0))
    if self.Tool then
        local LocalPlayer = self.Players.LocalPlayer :: Player
        if LocalPlayer.Character and LocalPlayer.Character == self.Tool.Parent then
            if self.Focused then
                Color = Color3.new(0.2, 1, 0.2)
            else
                Color = Color3.new(0, 0.7, 0)
            end
        else
            if self.Focused then
                Color = Color3.new(0.2, 0.2, 0.2)
            end
        end
    end

    --Set the properties.
    self.TweenService:Create(self.Background, TweenInfo.new(0.1), {
        Size = Size,
        ImageColor3 = Color,
        ImageTransparency = Transparency,
    }):Play()
end

--[[
Sets the tool for the icon.
--]]
function ToolIcon.SetTool(self: ToolIcon, Tool: BackpackItem): ()
    if Tool == self.Tool then return end

    --Disconnect the tool events.
    self.Tool = Tool
    for _, Event in self.ToolEvents do
        Event:Disconnect()
    end
    self.ToolEvents = {}
    self:UpdateColor()
    if not Tool then return end

    --Connect the tool events.
    table.insert(self.ToolEvents, Tool.Changed:Connect(function(PropertyName)
        if PropertyName ~= "Name" and PropertyName ~= "TextureId" and PropertyName ~= "Parent" then return end
        self:UpdateColor()
    end))
end

--[[
Sets the icon as focused.
--]]
function ToolIcon.SetFocused(self: ToolIcon, Focused: boolean): ()
    self.Focused = Focused
    self:UpdateColor()
end

--[[
Destroys the tool icon.
--]]
function ToolIcon.Destroy(self: ToolIcon): ()
    self.Background:Destroy()
    for _, Event in self.ToolEvents do
        Event:Disconnect()
    end
    self.ToolEvents = {}
end

return ToolIcon


--Main module and API for Nexus VR Backpack.
--!strict

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

local CharacterBackpack = require(script:WaitForChild("CharacterBackpack"))

local NexusVRBackpack = {}
NexusVRBackpack.Enabled = true

export type NexusVRBackpack = {
    CurrentBackpack: CharacterBackpack.CharacterBackpack?,
    OverrideKeyCode: Enum.KeyCode?,
    OverrideUserCFrame: Enum.UserCFrame?,
} & typeof(NexusVRBackpack)

--[[
Creates a backpack for the current character.
--]]
function NexusVRBackpack.CreateBackpack(self: NexusVRBackpack): ()
    --Clear the existing backpack.
    if self.CurrentBackpack then
        self.CurrentBackpack:Destroy()
        self.CurrentBackpack = nil
    end

    --Create the new backpack.
    if not Players.LocalPlayer.Character then return end
    local NewBackpack = CharacterBackpack.new(Players.LocalPlayer.Character)
    NewBackpack.Enabled = self.Enabled
    if self.OverrideKeyCode then
        NewBackpack:SetKeyCode(self.OverrideKeyCode)
    end
    if self.OverrideUserCFrame then
        NewBackpack:SetUserCFrame(self.OverrideUserCFrame)
    end
    self.CurrentBackpack = NewBackpack
end

--[[
Loads Nexus VR Backpack.
--]]
function NexusVRBackpack.Load(self: NexusVRBackpack): ()
    --Set up the backpacks.
    Players.LocalPlayer.CharacterAdded:Connect(function()
        self:CreateBackpack()
    end)
    self:CreateBackpack()

    --Load the Nexus VR Character Model API.
    task.spawn(function()
        --Get the Nexus VR Character Model API.
        local NexusVRCharacterModel = require(ReplicatedStorage:WaitForChild("NexusVRCharacterModel", 10 ^ 99))
        if not NexusVRCharacterModel.Api then
            warn("Nexus VR Character Model is loaded by no API is found. This was added in V.2.4.0. Inputs on the right controller won't be disabled when interacting with the backpack.")
            return
        end
        (CharacterBackpack :: any).NexusVRCharacterModelControllerApi = NexusVRCharacterModel.Api:WaitFor("Controller")

        --Add the Nexus VR Backpack API.
        NexusVRCharacterModel.Api:Register("Backpack", {
            GetBackpackEnabled = function(_): boolean
                return self:GetBackpackEnabled()
            end,
            SetBackpackEnabled = function(_, Enabled: boolean): ()
                self:SetBackpackEnabled(Enabled)
            end,
            SetKeyCode = function(_, KeyCode: Enum.KeyCode): ()
                self:SetKeyCode(KeyCode)
            end,
            SetUserCFrame = function(_, UserCFrame: Enum.UserCFrame): ()
                self:SetUserCFrame(UserCFrame)
            end,
        })
    end)
end

--[[
Returns if the backpack is enabled or disabled.
--]]
function NexusVRBackpack.GetBackpackEnabled(self: NexusVRBackpack): boolean
    return self.Enabled
end

--[[
Sets the backpack as enabled or disabled.
--]]
function NexusVRBackpack.SetBackpackEnabled(self: NexusVRBackpack, Enabled: boolean): ()
    self.Enabled = (Enabled ~= false)
    if self.CurrentBackpack then
        self.CurrentBackpack.Enabled = self.Enabled
        if not self.Enabled then
            self.CurrentBackpack:Close()
        end
    end
end

--[[
Sets the key to use for opening and closing the backpack.
--]]
function NexusVRBackpack.SetKeyCode(self: NexusVRBackpack, KeyCode: Enum.KeyCode): ()
    self.OverrideKeyCode = KeyCode
    if self.CurrentBackpack then
        self.CurrentBackpack:SetKeyCode(KeyCode or Enum.KeyCode.ButtonR3)
    end
end

--[[
Sets the input to open the menu at.
--]]
function NexusVRBackpack.SetUserCFrame(self: NexusVRBackpack, UserCFrame: Enum.UserCFrame): ()
    self.OverrideUserCFrame = UserCFrame
    if self.CurrentBackpack then
        self.CurrentBackpack:SetUserCFrame(UserCFrame or Enum.UserCFrame.RightHand)
    end
end

return NexusVRBackpack


--!strict
local RunService = game:GetService("RunService")

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local UserInterface = require(pluginRoot.Main.UserInterface)

local Plugin = {}

-- Public

function Plugin.build(plugin: Plugin)
	if RunService:IsStudio() and RunService:IsEdit() then
		local unmount = UserInterface.mountAsync()
		plugin.Unloading:Once(function()
			unmount()
		end)
	end
end

return Plugin


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local React = require(pluginRoot.Packages.React)

type AxisProps = {
	Text: string,
	Color: Color3,
	Vector: Vector3,
	Thickness: UDim,
	ZIndex: number,
}

local function Axis(props: AxisProps)
	local x, y = props.Vector.X, props.Vector.Y
	local rotationDeg = math.deg(-math.atan2(y, x))

	return React.createElement("Frame", {
		BackgroundTransparency = 1,
		AnchorPoint = Vector2.new(0.5, 0.5),
		Position = UDim2.fromScale(0.5, 0.5),
		Size = UDim2.fromScale(1, 1),
		Rotation = rotationDeg,
		ZIndex = props.ZIndex,
	}, {
		UIAspectRatioConstraint = React.createElement("UIAspectRatioConstraint", {
			AspectRatio = 1,
			AspectType = Enum.AspectType.FitWithinMaxSize,
		}),
		Line = React.createElement("Frame", {
			BorderSizePixel = 0,
			BackgroundColor3 = props.Color,
			AnchorPoint = Vector2.new(0, 0.5),
			Position = UDim2.fromScale(0.5, 0.5),
			Size = UDim2.new(math.sqrt(x * x + y * y) * 0.5, 0, props.Thickness.Scale, props.Thickness.Offset),
		}, {
			Label = React.createElement("TextLabel", {
				BackgroundTransparency = 1,
				AnchorPoint = Vector2.new(1, 0),
				Position = UDim2.fromScale(1, 1),
				Size = UDim2.fromOffset(0, 0),
				Rotation = -rotationDeg,
				FontFace = Font.fromEnum(Enum.Font.Legacy),
				TextXAlignment = Enum.TextXAlignment.Left,
				TextYAlignment = Enum.TextYAlignment.Top,
				TextColor3 = props.Color,
				Text = props.Text,
				TextSize = 8,
			}),
		}),
	})
end

export type Props = {
	AnchorPoint: Vector2?,
	Position: UDim2?,
	Size: UDim2?,
	ZIndex: number?,

	Rotation: CFrame,
	ColorMap: { [Enum.Axis]: Color3 },
}

local function LegacyViewSelector(props: Props)
	local axes = React.useMemo(function()
		local sorted = {}
		for _, enum in Enum.Axis:GetEnumItems() do
			local vector = props.Rotation:VectorToWorldSpace(Vector3.FromAxis(enum))
			table.insert(sorted, {
				enum = enum,
				vector = vector,
			})
		end

		table.sort(sorted, function(a, b)
			return a.vector.Z < b.vector.Z
		end)

		return sorted
	end, { props.Rotation })

	local children: { [string]: React.Node } = {}
	for zIndex, axis in axes do
		children[`{axis.enum.Name}Axis`] = React.createElement(Axis, {
			Text = axis.enum.Name,
			Color = props.ColorMap[axis.enum],
			Vector = axis.vector,
			Thickness = UDim.new(0, 1),
			ZIndex = zIndex,
		})
	end

	return React.createElement("Frame", {
		BackgroundTransparency = 1,
		AnchorPoint = props.AnchorPoint,
		Position = props.Position,
		Size = props.Size,
		ZIndex = props.ZIndex,
	}, children)
end

return React.memo(LegacyViewSelector)


--!strict

return {
	useConstant = require(script.useConstant),
	useRefProperty = require(script.useRefProperty),
}


local pluginRoot = script:FindFirstAncestor("PluginRoot")
local React = require(pluginRoot.Packages.React)

local function useConstant<T...>(create: () -> T...): T...
	local ref = React.useRef(nil :: any)

	if not ref.current then
		ref.current = { packed = table.pack(create()) }
	end

	return table.unpack(ref.current.packed)
end

return useConstant


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local React = require(pluginRoot.Packages.React)

local function useRefProperty<T>(ref: { current: Instance? }, property: string, default: T)
	local value, setValue = React.useState(default)

	React.useEffect(function()
		if not ref.current then
			setValue(default)
			return
		end

		print(ref.current, property)
		local connection = ref.current:GetPropertyChangedSignal(property):Connect(function()
			print("???")
			setValue((ref.current :: any)[property])
		end)

		return function()
			connection:Disconnect()
		end
	end, { ref.current, property } :: { any })

	return value
end

return useRefProperty


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local ReactRoblox = require(pluginRoot.Packages.ReactRoblox)
local React = require(pluginRoot.Packages.React)

local function createStory(component: React.FC<any>)
	return function(target: Frame)
		local element = React.createElement(component, {})
		local root = ReactRoblox.createRoot(Instance.new("Folder"))
		local portal = ReactRoblox.createPortal(element, target)
		root:render(portal)
		return function()
			root:unmount()
		end
	end
end

return createStory


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local ReactRoblox = require(pluginRoot.Packages.ReactRoblox)
local React = require(pluginRoot.Packages.React)

return {
	name = "BetterViewSelector",
	storyRoots = { script.Parent.Parent },
	react = React,
	reactRoblox = ReactRoblox,
}


--!strict

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

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local React = require(pluginRoot.Packages.React)

local stories = script:FindFirstAncestor("Stories")
local CreateStory = require(stories.Helpers.CreateStory)

local uiRoot = script:FindFirstAncestor("UserInterface")
local LegacyViewSelector = require(uiRoot.Components.Viewers.LegacyViewSelector)

local function story()
	local rotation, setRotation = React.useState(CFrame.identity)

	React.useEffect(function()
		local cf = rotation
		local connection = RunService.PreRender:Connect(function(dt)
			-- stylua: ignore
			cf = cf * CFrame.fromEulerAnglesXYZ(
				math.rad(15 * dt),
				math.rad(30 * dt),
				math.rad(45 * dt)
			)

			setRotation(cf)
		end)

		return function()
			connection:Disconnect()
		end
	end, {})

	return React.createElement(LegacyViewSelector, {
		AnchorPoint = Vector2.new(0.5, 0.5),
		Position = UDim2.fromScale(0.5, 0.5),
		Size = UDim2.fromOffset(150, 150),

		Rotation = rotation,
		ColorMap = {
			[Enum.Axis.X] = Color3.new(1, 0, 0),
			[Enum.Axis.Y] = Color3.new(0, 1, 0),
			[Enum.Axis.Z] = Color3.new(0, 0, 1),
		},
	})
end

return CreateStory(story)


--!strict
local RunService = game:GetService("RunService")

local StudioService = game:GetService("StudioService") :: StudioService
local SelectionService = game:GetService("Selection") :: Selection

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local ReactRoblox = require(pluginRoot.Packages.ReactRoblox)
local React = require(pluginRoot.Packages.React)

local LegacyViewSelector = require(script.Components.Viewers.LegacyViewSelector)
local Hooks = require(script.Hooks)

local UserInterface = {}

local WORLD_SPACE_COLOR_MAP = {
	[Enum.Axis.X] = Color3.new(1, 0, 0),
	[Enum.Axis.Y] = Color3.new(0, 1, 0),
	[Enum.Axis.Z] = Color3.new(0, 0, 1),
}

local OBJECT_SPACE_COLOR_MAP = {
	[Enum.Axis.X] = Color3.new(1, 0, 1),
	[Enum.Axis.Y] = Color3.new(1, 1, 0),
	[Enum.Axis.Z] = Color3.new(0, 1, 1),
}

local function tryGetCFrame(instance: Instance): CFrame?
	local success, value = pcall(function()
		if instance:IsA("BasePart") then
			return instance:GetPivot()
		elseif instance:IsA("Model") then
			if instance.PrimaryPart then
				return instance.PrimaryPart:GetPivot()
			else
				return instance:GetPivot()
			end
		elseif instance:IsA("Attachment") then
			return instance.WorldCFrame
		else
			error("Can't get CFrame from " .. instance.ClassName)
		end
	end)

	return if success then value else nil
end

local function getRenderCFrame()
	local isWorldSpace = true
	local worldCF = CFrame.identity
	if StudioService.UseLocalSpace then
		local selection = SelectionService:Get()
		local selectedCF = if #selection == 1 then tryGetCFrame(selection[1]) else nil
		if selectedCF then
			worldCF = selectedCF
			isWorldSpace = false
		end
	end

	local cameraCF = (workspace.CurrentCamera :: Camera).CFrame
	return cameraCF:ToObjectSpace(worldCF), isWorldSpace
end

local function App()
	local initialRenderCF, initialIsWorldSpace = Hooks.useConstant(function()
		return getRenderCFrame()
	end)

	local renderCF, setRenderCF = React.useState(initialRenderCF)
	local isWorldSpace, setIsWorldSpace = React.useState(initialIsWorldSpace)

	React.useEffect(function()
		local connection = RunService.PreRender:Connect(function()
			local renderCF, isWorldSpace = getRenderCFrame()
			setRenderCF(renderCF)
			setIsWorldSpace(isWorldSpace)
		end)

		return function()
			connection:Disconnect()
		end
	end, {})

	return React.createElement(LegacyViewSelector, {
		AnchorPoint = Vector2.new(0.5, 0.5),
		Position = UDim2.fromScale(0.5, 0.5),
		Size = UDim2.fromScale(0.6, 0.6),
		ZIndex = -1,

		Rotation = renderCF,
		ColorMap = if isWorldSpace then WORLD_SPACE_COLOR_MAP else OBJECT_SPACE_COLOR_MAP,
	})
end

function UserInterface.mountAsync()
	local viewSelector = game:GetService("CoreGui"):WaitForChild("ViewSelectorScreenGui") :: ScreenGui
	local defaultZIndexBehavior = viewSelector.ZIndexBehavior

	local panel = viewSelector:WaitForChild("Panel")
	local viewport = panel:WaitForChild("Viewport") :: ViewportFrame
	local arrowButtons = panel:WaitForChild("ArrowButtons") :: Frame
	local xLabel = panel:WaitForChild("X") :: TextLabel
	local yLabel = panel:WaitForChild("Y") :: TextLabel
	local zLabel = panel:WaitForChild("Z") :: TextLabel

	local function setBuiltInViewSelectorVisible(visible: boolean)
		local transparency = if visible then 0 else 1
		viewSelector.ZIndexBehavior = if visible then defaultZIndexBehavior else Enum.ZIndexBehavior.Sibling
		viewport.ImageTransparency = transparency
		xLabel.TextTransparency = transparency
		yLabel.TextTransparency = transparency
		zLabel.TextTransparency = transparency
		for _, arrow in arrowButtons:QueryDescendants(">ImageButton") do
			arrow.Visible = visible
		end
	end

	local folder = Instance.new("Folder")
	folder.Name = "BetterViewSelector"
	folder.Parent = panel

	setBuiltInViewSelectorVisible(false)
	local root = ReactRoblox.createRoot(folder)
	root:render(React.createElement(App))

	return function()
		setBuiltInViewSelectorVisible(true)
		root:unmount()
		folder:Destroy()
	end
end

return UserInterface


--[[
	cleanup method names
	a map of names sorted by (what i think is) the most common to least common
]]

return table.freeze({
	clear = 1, -- this is here because the cleanup method for catboygirl maid is 'clear'
	Destroy = 2,
	Disconnect = 3,
	destroy = 4,
	disconnect = 5,
	cleanup = 6,
	delete = 7,
	Clear = 8,
	Cleanup = 9,
	Delete = 10,
})


--!native

--[[
	catboygirl maid
	fast and lightweight maid impl ( thats also better than janitor )
]]

local cleanup_method_names = require("./cleanup_method_names")

type TableCleanupMethod = <V>(self: (V & {})) -> ()

type Cleaner<V> = (((value: V) -> ()) | () -> ())?

--[[
	this exists because otherwise if the dev doesnt have roblox types in their lsp
	cleanable will become a type error
]]
type function IfAndUnion(a: type, b: type, c: type)
	return if b ~= types.any and c ~= types.any then
			types.unionof(a, b, c)
		else
			a
end

type RobloxScriptConnection = RBXScriptConnection

type RobloxInstance = Instance

export type Cleanable = IfAndUnion<
	| { [keyof<typeof(cleanup_method_names)>]: TableCleanupMethod }
	| () -> ()
	| thread,
	RobloxScriptConnection, RobloxInstance
>

type MaidImpl<D> = {
	--[[
		Inserts a value to the `maid`, with the option of providing a `cleaner` to use to clean the value.
		This function is eqvilent to `table.insert`, except for the fact it allows for a `cleaner` function.
		It is reccomended to use `table.insert` instead if possible, as it will be much more faster as it is [fast-called](https://luau.org/performance#optimized-libraries).
	]]
	insert: <V>(maid: D, value: V & Cleanable, cleaner: Cleaner<V>) -> V,
	--[[
		Cleans up and removes a `value` from the `maid` provided.
	]]
	remove: (maid: D, value: Cleanable) -> (),
	--[[
		Cleans up and removes every value in the `maid` provided.
	]]
	clear: (maid: D) -> (),
}

type MaidPrototype =  MaidImpl<Maid> & {
	__index: MaidPrototype,
}

export type Maid = typeof(setmetatable({} :: {
	values: { Cleanable }
}, {} :: MaidPrototype))

type Export = MaidImpl<{ Cleanable }> & {
	--[[
		Creates a new `maid` object.
	]]
	create: () -> Maid,
}

local DISCONNECT: (any) -> () = function() end
local CLOSE: (thread) -> () = coroutine.close
local DESTROY: (any) -> () = function() end
local INSERT = table.insert
local FIND = table.find
local TYPEOF = typeof

local function SWAP_REMOVE(t: { unknown }, index: number)
	if #t ~= 1 then
		local last = t[#t]
		t[index] = last
		t[#t] = nil
	else
		t[1] = nil
	end
end

local function insert<V>(maid: { Cleanable }, value: V & Cleanable, cleaner: Cleaner<V>): V
	if cleaner then
		INSERT(maid, function()
			(cleaner :: any)(value)
		end)
	else
		INSERT(maid, value :: any)
	end

	return value
end

local function remove(maid: { Cleanable }, value: Cleanable)
	local index = FIND(maid, value)

	if index then
		CLEANUP(SWAP_REMOVE)
	end
end

local function clear(maid: { Cleanable })
	for index, value in maid do
		CLEANUP(nil, "continue")
	end

	--[[
		using table.clear because im unsure if a gc pause can occur during iteration
		that will resize the cleanup array making it a hash mid iteration
	]]
	table.clear(maid)
end

local maid = {} :: MaidPrototype

function maid.insert(maid, value, cleaner)
	insert(maid.values, value, cleaner)
end

function maid.remove(maid, value)
	remove(maid.values, value)
end

function maid.clear(maid)
	clear(maid.values)
end

local export = {
	insert = insert,
	remove = remove,
	clear = clear,
} :: Export

function export.create()
	return table.freeze(setmetatable({
		values = {}
	}, maid))
end

do

	-- checking if catboygirl maid is running in roblox
	if game and script and elapsedTime and Vector3int16 then
		local connection = script.Changed:Connect(function() end)
		DISCONNECT = connection.Disconnect :: any
		DESTROY = game.Destroy :: any
		CLOSE = task.cancel :: any
	else
		local task_aliases = {
			"@std", "@lute", "@lune"
		}

		for _, path in task_aliases do
			local require_success, task = pcall(require, path .. "/task")

			if require_success then
				local index_success, cancel = pcall(function(task)
					return task.cancel
				end, task)

				if index_success then
					CLOSE = cancel :: any
					break
				end
			end
		end
	end

	maid.__index = maid
	table.freeze(maid)
end

return table.freeze(export) :: Export


local type = TYPEOF(value)

if type == "Instance" then
	DESTROY((value :: any))
elseif type == "RBXScriptConnection" then
	DISCONNECT((value :: any))
elseif type == "function" then
	(value :: any)()
elseif type == "table" then
	METHOD_CLEAN()
elseif type == "thread" then
	CLOSE((value :: any))
end
REMOVE()


--[[
	codegen
	replaces placeholders in catboygirl maid for testing & release
]]

local cleanup_method_names = require("../../cleanup_method_names")
local process = require("@lune/process")
local license = require("../license")
local fs = require("@lune/fs")

local function REMOVE_FROM_END(s: string, remove: string): string
	return if string.sub(s, #s - #remove, #s) == remove then
			string.sub(s, 1, #s - #remove)
		else
			s
end

local NEW_LINE_CHAR = if process.os == "windows" then "\r\n" else "\n"
local METHOD_CLEAN = REMOVE_FROM_END(fs.readFile("scripts/codegen/method_clean.luau"), NEW_LINE_CHAR)
local CLEANUP_SRC = REMOVE_FROM_END(fs.readFile("scripts/codegen/cleanup.luau"), NEW_LINE_CHAR)
local HOT_COMMENT_PATTERN = `%-%-![^%[%]]+{NEW_LINE_CHAR}`
local LICENSE = string.sub(license, 5, #license - 2)
local METHOD_NAMES_ARRAY = {} :: { string }
local SRC = fs.readFile("maid.luau")

do -- populating METHOD_NAMES_ARRAY
	for name, order in cleanup_method_names do
		local current = METHOD_NAMES_ARRAY[order]

		if current then
			error(`cleanup method name '{name}' has the same order as '{current}'`)
		else
			METHOD_NAMES_ARRAY[order] = name
		end
	end

	table.freeze(METHOD_NAMES_ARRAY)
end

do -- formatting CLEANUP_SRC
	CLEANUP_SRC = string.gsub(CLEANUP_SRC, "\tMETHOD_CLEAN%(%)", function()
		local method_cleaners = table.create(#METHOD_NAMES_ARRAY)

		for index, method_name in METHOD_NAMES_ARRAY do
			local cleaner = string.gsub(METHOD_CLEAN, "KEY", method_name)
			local variable_name = string.lower(method_name)

			if variable_name ~= method_name then
				variable_name ..= "2"
			end

			cleaner = string.gsub(cleaner, "VARIABLE", variable_name)
			cleaner = string.gsub(cleaner, NEW_LINE_CHAR, "\n\t")
			method_cleaners[index] = cleaner
		end

		return `\t{table.concat(method_cleaners, "\n\t")}`
	end)

	CLEANUP_SRC = string.gsub(CLEANUP_SRC, "\n", "\n\t\t")
end

SRC = string.gsub(SRC, `local cleanup_method_names = require%("%./cleanup_method_names"%){NEW_LINE_CHAR}{NEW_LINE_CHAR}`, "")

SRC = string.gsub(SRC, "| { %[keyof<typeof%(cleanup_method_names%)>%]: TableCleanupMethod }", function()
	local type_union = table.create(#METHOD_NAMES_ARRAY) :: { string }

	for index, name in METHOD_NAMES_ARRAY do
		type_union[index] = `| \{ {name}: TableCleanupMethod }`
	end

	table.sort(type_union, function(a, b)
		return #a > #b
	end)

	return table.concat(type_union, "\n\t")
end)

SRC = string.gsub(SRC, "CLEANUP%((.-)%)", function(args)
	local setter, keyword = string.match(args, "([%w_]-), \"(.-)\"")
	local inner = CLEANUP_SRC

	if keyword then
		--keyword = string.sub(keyword, 2, #keyword - 1)
		inner = string.gsub(inner, "return", keyword)
	end

	if not setter then
		setter = args
	end

	return if setter ~= "nil" then
			string.gsub(inner, "REMOVE%(%)", `{setter}(maid :: any, index)`)
		else
			string.gsub(inner, "%s-REMOVE%(%)", "")
end)

do -- appending license to top of file
	local hot_comments = {}

	SRC = string.gsub(SRC, HOT_COMMENT_PATTERN, function(hot_comment)
		table.insert(hot_comments, hot_comment)
		return ""
	end)

	local header_start = string.find(SRC, "--[[", 1, true)
	local header_end = string.find(SRC, "]]", 1, true)

	if header_start and header_end then
		SRC = ((#hot_comments ~= 0 and table.concat(hot_comments)) or NEW_LINE_CHAR)
			.. string.sub(SRC, header_start, header_end - 1)
			.. LICENSE
			.. string.sub(SRC, header_end, #SRC)
	else
		error("maid.luau doesn't have a header comment")
	end
end

if string.sub(process.args[1], 1, 1) == "w" then
	fs.writeFile("maid.luau", SRC)
end

print(SRC)
return SRC


local VARIABLE = (value :: any).KEY

if VARIABLE then
	VARIABLE(value)
	REMOVE()
	return
end


--[[
	release
	makes a release for catboygirl maid
]]

local process = require("@lune/process")
local codegen = require("./codegen")
local summon = require("./summon")
local fs = require("@lune/fs")

local before_codegen = fs.readFile("maid.luau")
local version = process.args[1]

if type(version) ~= "string" then
	error("first argument cannot be nil, must provide a version string for release")
end

if not string.match(version, "v?%d+%.%d+%.%d+[%a%-]*") then
	error("version string is not valid semver")
end

if string.sub(version, 1, 1) == "v" then
	version = string.sub(version, 2, #version)
end

fs.writeFile("maid.luau", codegen)
summon(`gh release create {version} maid.luau --title v{version} --generate-notes --draft`)
fs.writeFile("maid.luau", before_codegen)
print(string.match(summon(`gh release view {version} --json url`), ":\"(.*)\""))


--[[
	luau
	generic luau catboygirl maid test
]]

local cleanup_method_names = require("../cleanup_method_names")
local maid = require("@catboygirl-maid")

local function TEST(cleanup: { maid.Cleanable }, call: (key: string, ...unknown) -> ())
	local thread = coroutine.create(function() end)
	local cleaner_given_cleaner_table = false
	local function_has_ran = false
	local cleaner_has_ran = false
	local cleaner_table = {}

	local function cleanup_function()
		function_has_ran = true
	end

	local function cleaner(t)
		if t == cleaner_table then
			cleaner_given_cleaner_table = true
		end
		cleaner_has_ran = true
	end

	call("insert", thread)

	assert(cleanup[1] == thread, "cleanup array index 1 is not the thread")

	call("insert", cleanup_function)

	assert(cleanup[2] == cleanup_function, "cleanup array index 2 is not 'cleanup_function'")

	call("remove", thread)

	assert(cleanup[1] == cleanup_function, "cleanup array index 1 is not 'cleanup_function'")
	assert(coroutine.status(thread) == "dead", "thread didn't die")

	call("insert", cleaner_table, cleaner)

	assert(type(cleanup[2]) == "function", "cleanup array index 2 is not a function wrapping 'cleaner'")

	call("clear")

	assert(cleaner_given_cleaner_table, "'cleaner' wasn't given 'cleaner_table'")
	assert(function_has_ran, "'cleanup_function' didn't run")
	assert(#cleanup == 0, "cleanup array wasn't cleared")
	assert(cleaner_has_ran, "'cleaner' didn't run")

	local function class_cleanup_test(method_name: string, maid_method: string)
		local has_cleanup_been_called = false
		local maid_method = "clear"
		local class

		class = {
			[method_name] = function(provided_class)
				if not provided_class == class then
					error(`cleanup method '{method_name}' wasn't given the correct class table by {maid_method}`)
				end
				has_cleanup_been_called = true
			end
		}

		call("insert", class)
		call(maid_method, class)

		if not has_cleanup_been_called then
			error( `cleanup method '{method_name}' was not called by {maid_method}`)
		end
	end

	for method_name in cleanup_method_names do
		class_cleanup_test(method_name, "remove")
		class_cleanup_test(method_name, "clear")
	end
end

local maid_class = maid.create()

assert(getmetatable(maid_class) ~= nil, "maid.create() doesn't set metatable")
assert(type(maid_class.values) == "table", "type of maid.values isn't table")

TEST(maid_class.values, function(method, ...)
	return maid_class[method](maid_class, ...)
end)

local cleanup = {}

TEST(cleanup, function(method, ...)
	return maid[method](cleanup, ...)
end)


--[[
	roblox
	roblox specific catboygirl maid test
]]

local maid = require("../maid")

local function TEST(cleanup: { maid.Cleanable }, call: (method: string, ...unknown) -> ())
	local connection = game.Changed:Connect(function() end)
	local part = Instance.new("Part")
	local has_destroyed = false

	part.Destroying:Connect(function()
		has_destroyed = true
	end)

	call("insert", connection)

	assert(cleanup[1] == connection, "cleanup array index 1 is not the connection")

	call("insert", part)

	assert(cleanup[2] == part, "cleanup array index 2 is not the part")

	call("remove", connection)

	assert(cleanup[1] == part, "connection wasn't removed from cleanup array")
	assert(connection.Connected == false, "connection wasn't disconnected")

	call("clear")

	assert(cleanup[1] == nil, "part wasn't removed from cleanup array")
	task.wait() -- waiting because defferred signaling
	assert(has_destroyed, "part wasn't destroyed")
end

local maid_class = maid.create()
local cleanup = {}

TEST(maid_class, function(method, ...)
	return maid_class[method](maid_class, ...)
end)

TEST(cleanup, function(method, ...)
	return maid[method](...)
end)


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local red = require(ReplicatedStorage.Packages.red)

red.subscribe(function(action: red.Action<any>)
	if action.type == "PLAYER_KILL" then
		print(`{action.payload} was killed!`)
	end
end)

-- "get" will wait for a result
local data = red.get({ type = "GET_DATA" })

if data.success then
	print(data.payload.data) -- Hello, world!
end


local PlayerController = {}

function PlayerController.kill(player: Player): boolean?
	local character = player.Character :: Model?
	local humanoid = character
		and character:FindFirstChildOfClass("Humanoid") :: Humanoid?

	if humanoid then
		humanoid.Health = 0

		print(player.Name .. " was killed!")

		return true
	end

	return false
end

function PlayerController.damage(player: Player, amount: number)
	local character = player.Character :: Model?
	local humanoid = character
		and character:FindFirstChildOfClass("Humanoid") :: Humanoid?

	if humanoid then
		humanoid.Health -= amount
		print(`{player.Name} took {amount} damage!`)
	end
end

return PlayerController


local red = require(game.ReplicatedStorage.Packages.red)

return function(bind: red.Bind)
	bind("GET_DATA", function(_player: Player)
		print("Getting data...")
		task.wait(1)

		return {
			type = "DATA",
			payload = { data = "Hello, world!" },
		}
	end)
end


local red = require(game.ReplicatedStorage.Packages.red)

return function(bind: red.Bind)
	bind("PING", function(player: Player)
		red.dispatch(player, { type = "PONG" })
	end)
end


local red = require(game.ReplicatedStorage.Packages.red)

local PlayerController = require(script.Parent.Parent.Controllers.PlayerController)

return function(bind: red.Bind)
	bind("PLAYER_KILL", function(player: Player)
		local success = PlayerController.kill(player)

		if success then
			red.dispatch(true, { type = "PLAYER_KILLED", payload = player.Name })
		end
	end)

	bind(
		"PLAYER_DAMAGE",
		function(player: Player, payload: red.ActionPayload<{ amount: number }>)
			PlayerController.damage(player, payload.amount)
		end
	)
end


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local red = require(ReplicatedStorage.Packages.red)

local Loader = require(script.Parent.Handlers.Loader)
local PingPong = require(script.Parent.Handlers.PingPong)

red.useHandlers({
	Loader,
	PingPong,
})

-- Alternatively you can use red.useHandlers(script.Parent.Handlers)
-- This will automatically bind all handlers in the Handlers folder


--!nocheck

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

local Util = require(ReplicatedStorage.Packages.Util)
local red = require(ReplicatedStorage.Packages.red)

red.bind("PLAYER_TELEPORT", function(player, payload)
	if Util.isAlive(player) then
		task.wait()

		player.Character.PrimaryPart.CFrame = payload.location
	end
end)

local function playerAdded(player)
	player.CharacterAdded:Connect(function(character)
		if not character.Parent then
			character.AncestryChanged:Wait()

			if not character.Parent then
				return
			end
		end

		character:WaitForChild("Humanoid")

		red.dispatch(player, {
			type = "PLAYER_TELEPORT",
			payload = {
				location = CFrame.new(Util.random(-100, 100), 40, Util.random(-100, 100)),
			},
		})
	end)
end

-- Add players that joined before the connection was created
for _, player in pairs(Players:GetPlayers()) do
	playerAdded(player)
end

Players.PlayerAdded:Connect(playerAdded)


return {
	REMOTE_FOLDER_NAME = "redRemotes",
	STORE_TIMEOUT = 6, -- seconds to wait before rejecting a get() promise
}


--!strict

export type BindCallback = (Player, ActionPayload<any>) -> ()
export type Bind = (string, BindCallback) -> ()

export type ActionPayload<T> = T

export type Action<T> = {
	uid: string?,
	method: string?,
	player: Player?,
	timeout: number?,
	type: string,
	payload: ActionPayload<T>,
	success: boolean?,
	err: string?,
	unreliable: boolean?,
	serverTime: number?,
}

export type Handler = (Bind) -> ()

return {}


--!strict

local Store = require(script.Store)
local Types = require(script.Types)

export type ActionPayload<T> = Types.ActionPayload<T>
export type Action<T> = Types.Action<T>

export type BindCallback = Types.BindCallback
export type Bind = Types.Bind

export type Handler = Types.Handler

return Store


--!strict

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

local Constants = require(script.Parent.Constants)

local Util = {}

function Util.getRemotes(): { Client: RemoteEvent, UnreliableClient: UnreliableRemoteEvent }
	local remotes = RunService:IsClient()
			and ReplicatedStorage:WaitForChild(Constants.REMOTE_FOLDER_NAME)
		or ReplicatedStorage:FindFirstChild(Constants.REMOTE_FOLDER_NAME)

	if remotes then
		return {
			Client = remotes:WaitForChild("Client", 60) :: RemoteEvent,
			UnreliableClient = remotes:WaitForChild("UnreliableClient", 60) :: UnreliableRemoteEvent,
		}
	end

	-- Container for remotes
	local newRemotes = Instance.new("Folder")
	newRemotes.Name = Constants.REMOTE_FOLDER_NAME

	-- store:dispatch() calls from client to server and server to client
	local client = Instance.new("RemoteEvent")
	client.Name = "Client"
	client.Parent = newRemotes

	local unreliableClient = Instance.new("UnreliableRemoteEvent")
	unreliableClient.Name = "UnreliableClient"
	unreliableClient.Parent = newRemotes

	newRemotes.Parent = ReplicatedStorage

	return {
		Client = client,
		UnreliableClient = unreliableClient,
	}
end

return Util


--!strict

--[[
	Classy: A Classy is useful because writing code for instances becomes much better!
	Author: Jaeymo
	Version: 1.6.0
	License: MIT
	Created: 01/18/2026

	For issues or feedback message `jaeymo` on Discord!
]]

local CollectionService = game:GetService("CollectionService")

local Packages = script.Parent

local Janitor = require(Packages.Janitor)
local Signal = require(Packages.Signal)

--[=[
	@class Applied
	The Applied class is what Classy manages and attaches instances to
]=]
local Applied = {}
Applied.__index = Applied

--[=[
	@class Classy
	A Classy is useful for instantiating and managing instances of
	based on tags
]=]
local Classy = {}
Classy.__index = Classy

export type Janitor = Janitor.Janitor
export type Connection = Signal.Connection
export type Signal<T...> = Signal.Signal<T...>

export type Constructable<T> = ((Instance, Janitor) -> T) | {
    new: (Instance, Janitor) -> T,
}

export type ClassyConfig<U> = {
    Predicate: ((Instance) -> boolean)?,
    ClassNames: { string }?,
    NamingConvention: {
        Init: string?,
        Destroy: string?,
    }?,
    RemoveTagOnCleanup: boolean?,
    Logging: boolean?,
    Ancestors: { U }?,
}

export type Applied<T, U> = typeof(setmetatable({} :: {
    Classy: Classy<T, U>,
    Instance: Instance,
    Janitor: Janitor,
    Data: T,
    Triggers: { [string]: string }?,
}, Applied))

export type Classy<T, U> = typeof(setmetatable({} :: {
    Tag: string,
    Janitor: Janitor,
    Logging: boolean,
    Ancestors: { U },
    ClassNames: { string },
    NamingConvention: {
        Init: string,
        Destroy: string,
    },
    RemoveTagOnCleanup: boolean,
    Constructable: Constructable<T>,
    Predicate: ((Instance) -> boolean)?,
    Applied: { [Instance]: Applied<T, U> },
    InstanceAdded: Signal<Instance, Applied<T, U>>,
    InstanceRevoked: Signal<Instance>,
}, Classy))

local GlobalRegistry = {} :: { [string]: any }

local InstanceRegistry = (setmetatable({}, {
    __mode = "k"
}) :: any ) :: { [Instance]: { [string]: Applied<any, any> } }

--[=[
	@private
]=]
local function validate(Instance: Instance, List: { string | Instance }, Fn: string): boolean
    if not next(List) then
        return true
    end

    for _, v in List do
        if (Instance::any)[Fn](Instance, v) then
            return true
        end
    end

    return false
end

--[=[
	@private
]=]
local function safeCall<T>(Data: T, Fn: string, ...: any): ...any
    if typeof(Data) == "table" and typeof((Data::any)[Fn]) == "function" then
        return (Data::any)[Fn](...)
    end

    return
end

--[=[
	@private
]=]
local function safeDestroy<T>(Data: T)
    if typeof(Data) == "table" then
        table.clear(Data::any)
        setmetatable(Data::any, nil)
    end
end

--[=[
	@private
]=]
local function construct<T>(Constructable: Constructable<T>, Instance: Instance, JanitorObject: Janitor): T
    if typeof(Constructable) == "function" then
        return Constructable(Instance, JanitorObject)
    end

    if typeof(Constructable) == "table" and typeof(Constructable.new) == "function" then
        return Constructable.new(Instance, JanitorObject)
    end

    error(`[{script.Name}] Invalid Constructable`)
end

--[=[
	@private
]=]
local function emitInternal(Instance: Instance, TriggerName: string, Context: any?)
	local Registry = InstanceRegistry[Instance]
	if not Registry then
		return
	end

	for _, AppliedValue in Registry do
		local Triggers = AppliedValue.Triggers
		if not Triggers then
			continue
		end

		local MethodName = Triggers[TriggerName]
		if not MethodName then
			continue
		end

		local Data = (AppliedValue :: any):GetData()
		local Method = Data and (Data :: any)[MethodName]

		if typeof(Method) == "function" then
			(Method :: any)(Data, Context)
		end
	end
end

--[=[
	Instantiates a new Applied object.
    @param Instance Instance
    @param ClassyObject Classy<T, U>
	@return Applied<T, U>
]=]
function Applied.new<T, U>(Instance: Instance, ClassyObject: Classy<T, U>): Applied<T, U>
    local AppliedJanitor = Janitor.new()
    ClassyObject.Janitor:Add(AppliedJanitor)

	local Data: T = construct(ClassyObject.Constructable, Instance, AppliedJanitor)

	local TriggerAttributeName = `__classy_triggers_{ClassyObject.Tag}`
	local RawTriggers = Instance:GetAttribute(TriggerAttributeName)

	local Triggers: { [string]: string }? = nil

	if typeof(RawTriggers) == "string" and RawTriggers ~= "" then
		Triggers = {}

		for Pair in string.gmatch(RawTriggers, "[^,]+") do
			local TriggerName, MethodName = string.match(Pair, "([^:]+):([^:]+)")

			if TriggerName and MethodName then
				(Triggers :: {[ string]: string})[TriggerName] = MethodName
			else
				-- backwards compatibility
				(Triggers :: {[ string]: string})[Pair] = "onApply"
			end
		end
	end

    safeCall(Data, ClassyObject.NamingConvention.Init, Data)

    return setmetatable({
        Janitor = AppliedJanitor,
        Classy = ClassyObject,
        Instance = Instance,
        Data = Data,
        Triggers = Triggers,
    }, Applied)
end

--[=[
	@return T
	Returns the T of an Applied object, either referring to a custom
    return from a function constructable or a object from a class
]=]
function Applied.GetData<T, U>(self: Applied<T, U>): T
    return self.Data
end

--[=[
	Destroys the Janitor and the attached data of an Applied object

	:::warning Metatable Removal
	Running this will make any further attempts to call a method of Applied
	error
	:::
]=]
function Applied.Destroy<T, U>(self: Applied<T, U>)
    self.Janitor:Destroy()

    safeCall(self.Data, self.Classy.NamingConvention.Destroy, self.Data)
    safeDestroy(self.Data)

    table.clear(self::any)
    setmetatable(self::any, nil)
end

--[=[
	When an instance gets an object applied into Classy
	@prop InstanceAdded Signal<Instance, Applied<T, U>>
	@within Classy
]=]
--[=[
	When an instance gets an object revoked from Classy
	@prop InstanceRevoked Signal<Instance>
	@within Classy
]=]

--[=[
	Instantiates a new Classy object
    @param Tag string
    @param Constructable Constructable<T>
    @param ClassyConfig ClassyConfig<U>
	@return Classy<T, U>
]=]
function Classy.new<T, U>(Tag: string, Constructable: Constructable<T>, ClassyConfig: ClassyConfig<U>?): Classy<T, U>
    local Config: ClassyConfig<U> = ClassyConfig or {} :: any

    return setmetatable({
        Tag = Tag,
        Constructable = Constructable,

        ClassNames = table.freeze(Config.ClassNames or {}),
        Ancestors = table.freeze(Config.Ancestors or {}),
        Logging = Config.Logging or false,
        Predicate = Config.Predicate,
        RemoveTagOnCleanup = if Config.RemoveTagOnCleanup ~= nil then Config.RemoveTagOnCleanup else true,

        NamingConvention = {
            Init = Config.NamingConvention and Config.NamingConvention.Init or "Init",
            Destroy = Config.NamingConvention and Config.NamingConvention.Destroy or "Destroy",
        },

        Applied = {},
        Janitor = Janitor.new(),

        InstanceAdded = Signal.new(),
        InstanceRevoked = Signal.new(),
    }, Classy)
end

--[=[
	Initializes a Classy object
]=]
function Classy.Init<T, U>(self: Classy<T, U>)
    for _, Instance in CollectionService:GetTagged(self.Tag) do
        if self:CanBeApplied(Instance) then
            self:Apply(Instance)
        end
    end

    self.Janitor:Add(CollectionService:GetInstanceAddedSignal(self.Tag):Connect(function(Instance: Instance)
        if self:CanBeApplied(Instance) then
            self:Apply(Instance)
        end
    end))

    self.Janitor:Add(CollectionService:GetInstanceRemovedSignal(self.Tag):Connect(function(Instance: Instance)
        self:Revoke(Instance)
    end))
end

--[=[
	Returns if an Instance passes all of the checks to be applied
    @param Instance Instance
    @return boolean
]=]
function Classy.CanBeApplied<T, U>(self: Classy<T, U>, Instance: Instance): boolean
    if not validate(Instance, self.ClassNames, "IsA") then
        return false
    end

    if not validate(Instance, (self.Ancestors :: any) :: { Instance }, "IsDescendantOf") then
        return false
    end

    if self.Predicate and not self.Predicate(Instance) then
        return false
    end

    return true
end

--[=[
	Returns if an Instance passes all of the checks to be applied
    @param Instance Instance
    @return boolean
]=]
function Classy.GetApplied<T, U>(self: Classy<T, U>, Instance: Instance): Applied<T, U>?
    return self.Applied[Instance]
end

--[=[
	Returns all applied instances
    @return { [Instance]: Applied<T, U> }
]=]
function Classy.GetAll<T, U>(self: Classy<T, U>): { [Instance]: Applied<T, U> }
    return self.Applied
end

--[=[
	Bypasses the `CanBeApplied` check and applies an instance

    ```lua
    local AppliedObjet = ClassyObject:Apply(SomeInstance)
    AppliedObject:GetData():DoSomething()
    ```

    @param Instance Instance
    @return Applied<T, U>
]=]
function Classy.Apply<T, U>(self: Classy<T, U>, Instance: Instance): Applied<T, U>
    local Existing = self:GetApplied(Instance)
    if Existing then return Existing end

    if self.Logging then
        print(`[Classy] Applied instance: {Instance:GetFullName()}`)
    end

    local AppliedObject = Applied.new(Instance, self)
    self.Applied[Instance] = AppliedObject

    InstanceRegistry[Instance] = InstanceRegistry[Instance] or {}
    InstanceRegistry[Instance][self.Tag] = AppliedObject

    local Connection: RBXScriptConnection
    Connection = Instance.AncestryChanged:Connect(function()
        if not self:CanBeApplied(Instance) then
            self:Revoke(Instance)
            Connection:Disconnect()
        end
    end)

    self.InstanceAdded:Fire(Instance, AppliedObject)
    return AppliedObject
end

--[=[
	Revokes an Instance from Classy

    :::info Note
	This will nullify the object's metatable, so you will not be able to use it's
	object after revoking it from Classy
	:::

    ```lua
    if ClassyObject:GetApplied(SomeInstance) then
        ClassyObject:Revoke(SomeInstance)
    end
    ```

    @param Instance Instance
]=]
function Classy.Revoke<T, U>(self: Classy<T, U>, Instance: Instance)
    local AppliedObject = self:GetApplied(Instance)
    if not AppliedObject then return end

    local Registry = InstanceRegistry[Instance]
    if Registry then
        Registry[self.Tag] = nil
        if not next(Registry) then
            InstanceRegistry[Instance] = nil
        end
    end

    if self.Logging then
        print(`[Classy] Revoked instance: {Instance:GetFullName()}`)
    end

    AppliedObject:Destroy()
    self.Applied[Instance] = nil

    if self.RemoveTagOnCleanup then
        Instance:RemoveTag(self.Tag)
    end

    self.InstanceRevoked:Fire(Instance)
end

--[=[
	Runs a callback for all applied objects and hooks into the `InstanceAdded` signal

    ```lua
    local Connection = ClassyObject:ObserveApplied(function(Instance, Applied)
        Applied:GetData():DoSomething()
    end)

    Connection:Disconnect()
    ```

    @param Callback (Instance: Instance, Applied: Applied<T, U>)
    @return Connection Connection
]=]
function Classy.ObserveApplied<T, U>(self: Classy<T, U>, Callback: (Instance: Instance, Applied: Applied<T, U>) -> ()): Connection
    for Inst, AppliedObject in self:GetAll() do
        task.spawn(function()
            Callback(Inst, AppliedObject :: Applied<T, U>)
        end)
    end

    return self.InstanceAdded:Connect(Callback)
end

--[=[
	Destroys all tracked objects, cleans the Janitor and destroys signals, rendering
    this object unusable.

	:::warning Metatable Removal
	Running this will make any further attempts to call a method of Classy
	error.
	:::

	```lua
	ClassyObject:Destroy()
	```
]=]
function Classy.Destroy<T, U>(self: Classy<T, U>)
    for Instance in self.Applied do
        self:Revoke(Instance)
    end

    if self.Logging then
        print(`[Classy] Cleaned classy for: {self.Tag}`)
    end

    self.Janitor:Destroy()

    table.clear(self::any)
    setmetatable(self::any, nil)
end

return {
    version = "1.6.0",

    new = function<T, U>(Tag: string, Constructable: Constructable<T>, Config: ClassyConfig<U>?): Classy<T, U>
        return Classy.new(Tag, Constructable, Config)
    end,

    create = function<T, U>(Tag: string, Constructable: Constructable<T>, Config: ClassyConfig<U>?): Classy<T, U>
        local ClassyObject = Classy.new(Tag, Constructable, Config)
        ClassyObject:Init()

        return ClassyObject
    end,

	registerGlobal = function(Tag: string, ClassyInstance: any)
		GlobalRegistry[Tag] = ClassyInstance
	end,

	getComponent = function(Inst: Instance, Tag: string): Applied<any, any>?
		return InstanceRegistry[Inst]
			and InstanceRegistry[Inst][Tag]
    end,

	getComponents = function(Inst: Instance): { [string]: Applied<any, any> }?
		return InstanceRegistry[Inst]
    end,

	callOnAllComponents = function(Inst: Instance, MethodName: string, ...: any): ()
		local Registry = InstanceRegistry[Inst]
		if not Registry then
			return
		end

		for _, AppliedValue in Registry do
			local Data = (AppliedValue :: any):GetData()
			local Method = Data and Data[MethodName]

			if typeof(Method) == "function" then
				Method(Data, ...)
			end
		end
	end,

	applyComponents = function(Inst: Instance, Components: { [string]: { [string]: any } })
		for Tag, Data in Components do
			if Data then
				local Triggers = Data.triggers
				local TriggerString = ""

				for Key, Value in Data do
					if Key ~= "triggers" then
						Inst:SetAttribute(Key, Value)
					end
				end

				if typeof(Triggers) == "table" then
					local Buffer = {} :: { string }

					for TriggerName, MethodName in Triggers do
						table.insert(Buffer, `{TriggerName}:{MethodName}`)
					end

					TriggerString = table.concat(Buffer, ",")
				end

				if TriggerString ~= "" then
					Inst:SetAttribute(`__classy_triggers_{Tag}`, TriggerString)
				end
			end

			Inst:AddTag(Tag)

			local ClassyInstance = GlobalRegistry[Tag]
			if ClassyInstance then
				ClassyInstance:Apply(Inst)
			end
		end
	end,

    emit = emitInternal,
}