--[[ File Info
	Author(s): TactBacon, ChiefWildin (feat. eliphant)
--]]

-- Types

export type SplineImpl = {
	-- Metatable members

	__index: (self: Spline, key: string) -> any,
	__newindex: (self: Spline, key: string, value: any) -> any,

	-- Public members

	ControlPoints: { CFrame },
	Curve: Model,
	Parent: Instance?,
	Visible: boolean,

	new: (controlPoints: { CFrame }) -> Spline,
	Destroy: (self: Spline) -> (),
	GetCFrameFromAlpha: (self: Spline, alpha: number, alignment: ("Track" | "Nodes")?) -> CFrame,
	GetLinearCFrameFromAlpha: (self: Spline, alpha: number, alignment: ("Track" | "Nodes")?) -> CFrame,

	-- Private members

	_drawCurve: (self: Spline) -> (),
	_update: (self: Spline) -> (),
}
export type Spline = typeof(setmetatable(
	{} :: {
		-- Constructed public members
		ControlPoints: { CFrame },
		Curve: Model,
		Destroying: RBXScriptSignal,
		Parent: Instance?,
		Segments: number,
		Visible: boolean,

		-- Constructed private members
		_controlPoints: { CFrame },
		_curveDrawn: boolean,
		_curveLength: number,
		_destroying: BindableEvent,
		_distancePoints: { number },
		_listeners: { RBXScriptConnection },
		_normalizedDistancePoints: { number },
		_parent: Instance?,
		_segments: number,
		_visible: boolean,
	},
	{} :: SplineImpl
))

-- Constants

local CURVE_DISPLAY_COLOR = BrickColor.new("Toothpaste").Color

-- Dependencies

local Quaternion = require(script.Parent:WaitForChild("Quaternion")) ---@module Quaternion

-- Main Module

--[=[
	@class Spline

	A spline curve class for Roblox.
]=]
--[=[
	@prop ControlPoints { CFrame }
	@within Spline
	A table of CFrames that are used as control points for the spline curve
]=]
--[=[
	@prop Curve Model
	@within Spline
	A model containing the parts that make up the spline curve. This is
	updated whenever the control points are moved or the Segments property
	is updated
]=]
--[=[
	@prop Destroying RBXScriptSignal
	@within Spline
	Fires when the spline curve is destroyed.
]=]
--[=[
	@prop Parent Instance?
	@within Spline
	The parent of the spline curve model. Default workspace.
]=]
--[=[
	@prop Segments number
	@within Spline
	The number of segments per curve. The higher this number, the more
	accurate the curve will be, but the more expensive it will be to
	calculate. Default 10.
]=]
--[=[
	@prop Visible boolean
	@within Spline
	Whether or not the spline curve is visible in the workspace. Default false.
]=]

local Spline: SplineImpl = {} :: SplineImpl

Spline.__index = function(self, key)
	if key == "Visible" then
		return rawget(self, "_visible")
	elseif key == "Parent" then
		return rawget(self, "_parent")
	elseif key == "Segments" then
		return rawget(self, "_segments")
	elseif key == "ControlPoints" then
		return rawget(self, "_controlPoints")
	else
		return Spline[key]
	end
end

Spline.__newindex = function(self, key, value)
	if key == "Visible" then
		rawset(self, "_visible", value)
		if value and not self._curveDrawn then
			self:_drawCurve()
		end
		self.Curve.Parent = value and self._parent or nil
	elseif key == "Parent" then
		rawset(self, "_parent", value)
		self.Curve.Parent = self.Visible and self._parent or nil
	elseif key == "Segments" then
		rawset(self, "_segments", value)
		self:_update()
	elseif key == "ControlPoints" then
		rawset(self, "_controlPoints", value)
		self:_update()
	else
		rawset(self, key, value)
	end
end

-- Internal functions

-- Returns values for the Catmull-Rom spline at the point p1. Return value order
-- is the interpolated point, the tangent, the second derivative, and the third
-- derivative
local function CatmullRom(p0, p1, p2, p3)
	return
		-- The interpolated point at p1
		p1,
		-- The tangent at p1
		0.5 * (p2 - p0),
		-- The second derivative at p1
		p0
			- 2.5 * p1
			+ 2 * p2
			- 0.5 * p3,
		-- The third derivative at p1
		1.5 * (p1 - p2) + 0.5 * (p3 - p0)
end

-- Performs spherical linear interpolation between two CFrames and returns the
-- CFrame at the given alpha value
local function Slerp(origin: CFrame, target: CFrame, alpha: number): CFrame
	local position = origin.Position:Lerp(target.Position, alpha)
	local originQuaternion = Quaternion.fromCFrame(origin)
	local targetQuaternion = Quaternion.fromCFrame(target)
	local slerpedQuaternion = originQuaternion:Slerp(targetQuaternion, alpha)

	return CFrame.new(
		position.X,
		position.Y,
		position.Z,
		slerpedQuaternion.Vector.X,
		slerpedQuaternion.Vector.Y,
		slerpedQuaternion.Vector.Z,
		slerpedQuaternion.W
	)
end

-- Private functions

function Spline:_drawCurve()
	self.Curve:ClearAllChildren()

	local controlPointCount = #self._controlPoints
	self._curveDrawn = true

	local lastPoint = nil
	for i = 0, (self._segments * controlPointCount) do
		local alpha = i / (self._segments * controlPointCount)
		local cframe = self:GetCFrameFromAlpha(alpha)
		local segmentIndex = 0

		for j = 1, controlPointCount - 1 do
			if self._normalizedDistancePoints[j] >= alpha then
				segmentIndex = j
				break
			end
		end

		local point = Instance.new("Part")
		point.Name = "Point" .. segmentIndex
		point.Anchored = true
		point.CanCollide = false
		point.CanTouch = false
		point.CanQuery = false
		point.Color = CURVE_DISPLAY_COLOR
		point.Material = Enum.Material.Neon
		point.Size = Vector3.new(0.1, 0.1, lastPoint and (lastPoint - cframe.Position).Magnitude or 0.1)
		point.CFrame = CFrame.new(cframe.Position, lastPoint or cframe.Position) * CFrame.new(0, 0, -point.Size.Z * 0.5)
		point:SetAttribute("Alpha", alpha)
		point.Parent = self.Curve

		lastPoint = cframe.Position
	end
end

function Spline:_update()
	local controlPointCount = #self._controlPoints
	if self._controlPoints and controlPointCount >= 3 then
		self._curveLength = 0
		self._distancePoints = {}
		self._normalizedDistancePoints = {}
		self._curveDrawn = false

		-- Calculate lengths per segment
		for i = 1, controlPointCount - 1 do
			local length = 0
			local step = 0.01
			local lastPosition = self._controlPoints[i].Position

			for t = 0, 1 - step, step do
				local p0 = self._controlPoints[math.max(i - 1, 1)].Position
				local p1 = self._controlPoints[i].Position
				local p2 = self._controlPoints[math.min(i + 1, controlPointCount)].Position
				local p3 = self._controlPoints[math.min(i + 2, controlPointCount)].Position

				local interpolatedPoint, tangent, secondDerivative, thirdDerivative = CatmullRom(p0, p1, p2, p3)

				local position = interpolatedPoint
					+ (tangent * t)
					+ (secondDerivative * t ^ 2)
					+ (thirdDerivative * t ^ 3)

				length += (position - lastPosition).Magnitude

				lastPosition = position
			end

			self._curveLength += length
			self._distancePoints[i] = self._curveLength
		end

		for i = 1, #self._controlPoints - 1 do
			self._normalizedDistancePoints[i] = self._distancePoints[i] / self._curveLength
		end

		-- Update visualization, if applicable
		if rawget(self, "_visible") then
			self:_drawCurve()
		end
	end
end

-- Public Functions

--[=[
	Gets a CFrame along the spline at a given alpha value using the natural
	Catmull-Rom spline algorithm.

	@param alpha number -- The alpha value to get the point at. This should be a value between 0 and 1.
	@param alignment ("Track" | "Nodes")? -- This will orient the rotation of the result to either follow the rotation of the path (`"Track"`) or the rotation of the nodes (`"Nodes"`)
	@return CFrame -- The CFrame along the curve at the given alpha value using the segments for distances.
]=]
function Spline:GetCFrameFromAlpha(alpha: number, alignment: ("Track" | "Nodes")?): CFrame
	alignment = alignment or "Track"

	if alpha == 1 then
		return self._controlPoints[#self._controlPoints]
	end

	local segmentCount = #self._controlPoints - 1
	local previousControlPoint, remainder = math.modf(segmentCount * alpha + 1)
	local targetControlPoint = previousControlPoint + 1

	local c0 = self._controlPoints[math.max(previousControlPoint - 1, 1)]
	local c1 = self._controlPoints[previousControlPoint]
	local c2 = self._controlPoints[targetControlPoint]
	local c3 = self._controlPoints[math.min(targetControlPoint + 1, segmentCount + 1)]

	local interpolatedPoint, tangent, secondDerivative, thirdDerivative =
		CatmullRom(c0.Position, c1.Position, c2.Position, c3.Position)

	local position = interpolatedPoint
		+ (tangent * remainder)
		+ (secondDerivative * remainder ^ 2)
		+ (thirdDerivative * remainder ^ 3)

	if alignment == "Track" then
		local offsetTangent = tangent + (2 * secondDerivative * remainder) + (3 * thirdDerivative * remainder ^ 2)

		return CFrame.lookAt(position, position + offsetTangent)
	else
		local cubicInOutAlpha = remainder * remainder * (3 - 2 * remainder)
		return CFrame.new(position) * c1:Lerp(c2, cubicInOutAlpha).Rotation
		-- if c1.Rotation == c2.Rotation then
		-- 	return CFrame.new(position) * c1.Rotation
		-- else
		-- 	return CFrame.new(position) * Slerp(c1, c2, cubicInOutAlpha).Rotation
		-- end
	end
end

--[=[
	Gets a CFrame along the spline using a close approximation to where the
	point would be linearly to a given alpha value using the Catmull-Rom spline
	algorithm.

	@param alpha number -- The alpha value to get the point at. This should be a value between 0 and 1.
	@param alignment ("Track" | "Nodes")? -- This will orient the rotation of the result to either follow the rotation of the path (`"Track"`) or the rotation of the nodes (`"Nodes"`)
	@return CFrame -- The CFrame along the curve at the given alpha value using the segments for distances.
]=]
function Spline:GetLinearCFrameFromAlpha(alpha: number, alignment: ("Track" | "Nodes")?): CFrame
	alignment = alignment or "Track"

	local controlPointCount = #self._controlPoints
	local targetControlPoint = 1

	for i = 1, controlPointCount do
		if self._normalizedDistancePoints[i] >= alpha then
			targetControlPoint = i
			break
		end
	end

	local previousControlPoint = targetControlPoint - 1
	local normalizedPrevious = self._normalizedDistancePoints[previousControlPoint] or 0
	local normalizedTarget = self._normalizedDistancePoints[targetControlPoint]
	local t = (alpha - normalizedPrevious) / (normalizedTarget - normalizedPrevious)

	previousControlPoint = math.max(previousControlPoint, 1)

	local c0 = self._controlPoints[previousControlPoint]
	local c1 = self._controlPoints[targetControlPoint]
	local c2 = self._controlPoints[math.min(targetControlPoint + 1, controlPointCount)]
	local c3 = self._controlPoints[math.min(targetControlPoint + 2, controlPointCount)]

	local interpolatedPoint, tangent, secondDerivative, thirdDerivative =
		CatmullRom(c0.Position, c1.Position, c2.Position, c3.Position)

	local position = interpolatedPoint + (tangent * t) + (secondDerivative * t ^ 2) + (thirdDerivative * t ^ 3)

	if alignment == "Track" then
		local offsetTangent = tangent + (2 * secondDerivative * t) + (3 * thirdDerivative * t ^ 2)

		return CFrame.lookAt(position, position + offsetTangent)
	else
		local cubicInOutAlpha = t * t * (3 - 2 * t)
		return CFrame.new(position) * c1:Lerp(c2, cubicInOutAlpha).Rotation
		-- Using Lerp temporarily because Slerp is still resulting in NaN
		-- rotations and interpolating in strange ways. Might be how the
		-- quaternions are being constructed, but I don't have time to deep dive
		-- into that right now.

		-- if c1.Rotation == c2.Rotation then
		-- 	return CFrame.new(position) * c1.Rotation
		-- else
		-- 	return CFrame.new(position) * Slerp(c1, c2, cubicInOutAlpha).Rotation
		-- end
	end
end

--[=[
	Destroys the spline curve and cleans up any listeners.
]=]
function Spline:Destroy()
	self._destroying:Fire()
	self._destroying:Destroy()
	self.Curve:Destroy()

	for _, listener in self._listeners do
		listener:Disconnect()
	end

	self._listeners = {}
end

-- Constructor

--[=[
	Constructor for the Spline class. Takes in a curve model and returns a new
	Spline object.

	@param controlPoints { CFrame } -- The control points for the spline.
	@return Spline -- The new Spline object.
]=]
function Spline.new(controlPoints: { CFrame })
	local self = setmetatable({}, Spline)

	self._listeners = {} :: { RBXScriptConnection }
	self._visible = false
	self._parent = workspace
	self._segments = 10

	self._controlPoints = controlPoints

	self.Curve = Instance.new("Model")
	self.Curve.Name = "Curve"

	self._destroying = Instance.new("BindableEvent")
	self.Destroying = self._destroying.Event

	self:_update()

	return self
end

return Spline :: Spline


-- Retrieves GuiTemplates
-- @module GuiTemplates
-- @author Quenty

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local TemplateProvider = shared("TemplateProvider") ---@module TemplateProvider

local provider = TemplateProvider.new(ReplicatedStorage.GuiTemplates)

provider:Provide()

return provider


--[[ File Info

	Author(s): ChiefWildin
	Module: AtomicButton.luau
	Version: 1.3.1

]]

-- Dependencies

local AtomicPane = shared("AtomicPane") ---@module AtomicPane
-- local Sounds = shared("Sounds") ---@module Sounds

-- Types

export type AtomicButtonImpl = ({
	-- Metatable members

	__index: AtomicButtonImpl,

	-- Public members

	new: (template: string | Instance, instanceKeys: { [string]: string }) -> AtomicButton,
	Activate: (self: AtomicButton) -> (),
	IsEnabled: (self: AtomicButton) -> boolean,
	IsHovered: (self: AtomicButton) -> boolean,
	IsPressed: (self: AtomicButton) -> boolean,
	IsSelected: (self: AtomicButton) -> boolean,
	SetEnabled: (self: AtomicButton, enabled: boolean) -> (),
	SetHovered: (self: AtomicButton, hovered: boolean) -> (),
	SetPressed: (self: AtomicButton, pressed: boolean) -> (),
	SetSelected: (self: AtomicButton, selected: boolean) -> (),

	-- Private members

	_addSensor: (self: AtomicButton) -> (),
	_isEnabled: boolean,
	_isHovered: boolean,
	_isPressed: boolean,
	_isSelected: boolean,
} & AtomicPane.AtomicPaneImpl)?

export type AtomicButton = typeof(setmetatable(
	{} :: ({
		-- Constructed public members

		Activated: RBXScriptSignal,
		Hovered: RBXScriptSignal,
		Enabled: RBXScriptSignal,
		Pressed: RBXScriptSignal,
		Selected: RBXScriptSignal,

		-- Constructed private members

		_activated: BindableEvent,
		_hovered: BindableEvent,
		_enabled: BindableEvent,
		_pressed: BindableEvent,
		_selected: BindableEvent,

		_sensor: TextButton,

		[string]: any,
	} & AtomicPane.AtomicPane) | {},
	{} :: AtomicButtonImpl
))

-- Module Declaration

--[=[
	@class AtomicButton
	@tag UI

	The foundational model for all UI button components in the game. On top of
	the AtomicPane base, it adds interaction states such as hover, press,
	selected, and enabled. It also provides events for these interactions.
	Anything can be a button with this class, it will automatically create a
	TextButton if one does not already exist (must be named "Sensor" and a
	direct child of the Gui instance).

	**Inherits from AtomicPane**

	A VS Code snippet for creating AtomicButtons quickly is included with the
	Git repository.

	:::note
	The source GUI objects should be located in
	`ReplicatedStorage.GuiTemplates`, or `Assets.GuiTemplates` if using an
	Enclave package. They can be of any GuiObject type.
	:::

	### Basic usage
	```lua
	local AtomicButton = shared("AtomicButton") ---@module AtomicButton

	local ExampleButton = setmetatable({}, AtomicButton)
	ExampleButton.__index = ExampleButton
	ExampleButton.ClassName = "ExampleButton"

	function ExampleButton:_draw()
		local alpha = self._springs.Alpha.Position

		self.Gui.Visible = alpha < 0.999
	end

	function ExampleButton.new(parent: Instance)
		local self = setmetatable(AtomicButton.new("ExampleButtonTemplate", {}), ExampleButton)

		self:SetVisible(false, true)

		self.Gui.Parent = parent

		return self
	end

	return ExampleButton
	```
]=]
local AtomicButton: AtomicButtonImpl = setmetatable({}, AtomicPane) :: AtomicButtonImpl
AtomicButton.__index = AtomicButton
AtomicButton.ClassName = "AtomicButton"

-- Private Functions

--[=[
	@private
	Adds a sensor to the button or connects to the existing sensor
]=]
function AtomicButton:_addSensor()
	if self._sensor then
		return
	end

	self._sensor = self.Gui:FindFirstChild("Sensor")
	if not self._sensor then
		self._sensor = Instance.new("TextButton")
		self._sensor.Name = "Sensor"
		self._sensor.BackgroundTransparency = 1
		self._sensor.Text = ""
		self._sensor.TextTransparency = 1
		self._sensor.AutoButtonColor = false
		self._sensor.Size = UDim2.fromScale(1, 1)
		self._sensor.ZIndex = 10
		self._sensor.Parent = self.Gui
	end

	self._sensor:GetPropertyChangedSignal("GuiState"):Connect(function()
		self:SetPressed(self._sensor.GuiState == Enum.GuiState.Press)
		self:SetHovered(self._sensor.GuiState == Enum.GuiState.Hover or self._sensor.GuiState == Enum.GuiState.Press)
	end)

	self._sensor.Activated:Connect(function()
		self:Activate()
	end)
end

-- Public Functions

--[=[
	Manually activates the button without requiring the user to click it.
]=]
function AtomicButton:Activate()
	self._activated:Fire()
	-- Sounds:PlaySound("Click")
end

--[=[
	Manually sets the pressed state of the button without requiring the user to
	click it.

	@param pressed boolean -- The new pressed state
]=]
function AtomicButton:SetPressed(pressed: boolean)
	-- Standardize input into boolean type
	pressed = if not pressed then false else true

	if pressed == self._isPressed then
		return
	end

	self._isPressed = pressed
	self._pressed:Fire(pressed)
	self:SetSpring("Pressed", pressed and 1 or 0)
end

--[=[
	Sets the hovered state of the button

	@param hovered boolean
]=]
function AtomicButton:SetHovered(hovered: boolean)
	-- Standardize input into boolean type
	hovered = if not hovered then false else true

	if hovered == self._isHovered then
		return
	end

	self._isHovered = hovered
	self._hovered:Fire(hovered)
	self:SetSpring("Hovered", hovered and 1 or 0)

	-- if hovered then
	-- 	Sounds:PlaySound("Hover")
	-- end
end

--[=[
	Sets the selected state of the button

	@param selected boolean
]=]
function AtomicButton:SetSelected(selected: boolean)
	-- Standardize input into boolean type
	selected = if not selected then false else true

	if selected == self._isSelected then
		return
	end

	self._selected:Fire(selected)
	self._isSelected = selected
	self:SetSpring("Selected", selected and 1 or 0)
end

--[=[
	Sets the enabled state of the button

	@param enabled boolean
]=]
function AtomicButton:SetEnabled(enabled: boolean)
	-- Standardize input into boolean type
	enabled = if not enabled then false else true

	if enabled == self._isEnabled then
		return
	end

	self._sensor.Visible = enabled
	self._isEnabled = enabled
	self._enabled:Fire(enabled)
	self:SetSpring("Enabled", enabled and 1 or 0)
end

--[=[
	@return boolean -- Whether the button is enabled
]=]
function AtomicButton:IsEnabled(): boolean
	return self._isEnabled
end

--[=[
	@return boolean -- Whether the button is hovered
]=]
function AtomicButton:IsHovered(): boolean
	return self._isHovered
end

--[=[
	@return boolean -- Whether the button is pressed
]=]
function AtomicButton:IsPressed(): boolean
	return self._isPressed
end

--[=[
	@return boolean -- Whether the button is selected
]=]
function AtomicButton:IsSelected(): boolean
	return self._isSelected
end

--[=[
	@tag Constructor
	Creates a new AtomicButton based on the template key or existing frame.
	Instances are mapped based on a simple descendants search, so unique names
	are required for consistency.

	@param template string | Instance -- The name of a GuiObject in ReplicatedStorage.GuiTemplates or an existing instance to use as the button's Gui.
	@param instanceKeys table -- A mapping of instance names to keys for quick access
]=]
--[=[
	@prop Activated RBXScriptSignal<boolean>
	@within AtomicButton
	Fires when the button is activated or deactivated.
]=]
--[=[
	@prop Hovered RBXScriptSignal<boolean>
	@within AtomicButton
	Fires when the button is hovered or unhovered.
]=]
--[=[
	@prop Enabled RBXScriptSignal<boolean>
	@within AtomicButton
	Fires when the button is enabled or disabled.
]=]
--[=[
	@prop Pressed RBXScriptSignal<boolean>
	@within AtomicButton
	Fires when the button is pressed or unpressed.
]=]
--[=[
	@prop Selected RBXScriptSignal<boolean>
	@within AtomicButton
	Fires when the button is selected or unselected.
]=]
--[=[
	@prop _springs {[string]: Spring}
	@within AtomicButton
	@private
	A table of all of the pane's springs. Includes the Alpha, Hovered, Pressed,
	Selected and Enabled springs by default.
]=]
function AtomicButton.new(template: string | Instance, instanceKeys: { [string]: string })
	local self = setmetatable(AtomicPane.new(template, instanceKeys), AtomicButton)

	-- Private members

	self._activated = Instance.new("BindableEvent")
	self._hovered = Instance.new("BindableEvent")
	self._enabled = Instance.new("BindableEvent")
	self._pressed = Instance.new("BindableEvent")
	self._selected = Instance.new("BindableEvent")
	self._isEnabled = true
	self._isHovered = false
	self._isPressed = false
	self._isSelected = false

	-- Public members

	self.Activated = self._activated.Event
	self.Hovered = self._hovered.Event
	self.Enabled = self._enabled.Event
	self.Pressed = self._pressed.Event
	self.Selected = self._selected.Event

	-- Setup

	self:AddSprings({ "Hovered", "Pressed", "Selected" }, { s = 40, d = 1, i = 0 })
	self:AddSpring("Enabled", { s = 40, d = 1, i = 1 })

	self:_addSensor()

	return self
end

return AtomicButton


local storybook = {
	name = "Core UI",
	storyRoots = {
		script.Parent,
	},
}

return storybook


--[[ File Info

	Author(s): ChiefWildin
	Module: ScreenGui.luau
	Version: 1.0.0

]]

--[=[
	@class ScreenGui
	@client

	Utility for creating `ScreenGui` instances with sensible defaults.
	Automatically parents to `PlayerGui` at runtime, or to `CoreGui` when
	running inside a story environment.
]=]

-- Services

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

-- Module Declaration

local ScreenGui = {}

-- Objects

local LocalPlayer = Players.LocalPlayer

-- Public Functions

--[=[
	Creates a new `ScreenGui` with `ResetOnSpawn` disabled and
	`ZIndexBehavior` set to `Sibling`, then parents it to `PlayerGui`.

	@param name string -- The name to assign to the ScreenGui
	@return ScreenGui
]=]
function ScreenGui.new(name: string)
	local gui = Instance.new("ScreenGui")
	gui.Name = name
	gui.ResetOnSpawn = false
	gui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling

	if not RunService:IsRunning() then
		warn(`Story created ScreenGui "{name}"`)
		gui.Archivable = false
		gui.Parent = game.CoreGui
	else
		task.spawn(function()
			gui.Parent = LocalPlayer:WaitForChild("PlayerGui")
		end)
	end

	return gui
end

return ScreenGui


--[[
	AnalyticsClient.lua
	ChiefWildin
	Version: 1.2.0
]]

-- Services

-- Main job table

local AnalyticsClient = { Priority = 5000 }

-- Dependencies

local GamebeastSDK = shared("GamebeastSDK") ---@module GamebeastSDK

-- Constants

-- Global variables

-- Objects

-- Private functions

-- Public functions

-- Framework callbacks

function AnalyticsClient:Init()
	GamebeastSDK:Setup()
end

return AnalyticsClient


--[[ File Info

    Author(s): ChiefWildin
    Module: CmdrStarter.lua
    Version: 1.0.0

]]

-- Services

local ReplicatedStorage = game:GetService("ReplicatedStorage")

-- Module Declaration

local CmdrStarter = {}

-- Task Initialization

function CmdrStarter:Init()
	local CmdrClient = require(ReplicatedStorage:WaitForChild("CmdrClient")) ---@module CmdrClient
	CmdrClient:SetActivationKeys({ Enum.KeyCode.F2 })
end

return CmdrStarter


--[[ File Info

	Module: DataController.luau
	Author(s): ChiefWildin
	Version: 2.4.0

]]

--[=[
	@class DataController
	@client

	Handles data replication coming from the server.
]=]

-- Services

local RunService = game:GetService("RunService")

-- Task Declaration

local DataController = { Priority = 10 }

-- Dependencies

local ProfileTemplate = shared("ProfileTemplate") ---@module ProfileTemplate
local ReplicaController = if RunService:IsRunning() ---@module ReplicaController
	then shared("ReplicaController")
	else nil

-- Types

type Replica = ReplicaController.Replica

-- Constants

-- Global variables

local PlayerDataReplica: Replica

-- Objects

-- Private functions

local function deepCopy(target: {}): {}
	local copy = {}

	for key, value in pairs(target) do
		if typeof(value) == "table" then
			copy[key] = deepCopy(value)
		else
			copy[key] = value
		end
	end

	return copy
end

-- Public functions

--[=[
	Returns the `Replica` object that contains player data. Yields until the
	replica is available.

	@return Replica
	@yields
]=]
function DataController:GetDataReplica(): Replica
	while not PlayerDataReplica do
		if not RunService:IsRunning() then
			PlayerDataReplica = {
				Data = deepCopy(shared("ProfileTemplate")),
				ListenToChange = function() end,
			} :: Replica
		else
			task.wait()
		end
	end

	return PlayerDataReplica
end

--[=[
	Returns the top-level player data table containing both session and profile
	data.

	:::warning
	This table is read-only. Do not attempt to write to it directly.
	:::

	@return { [string]: any }
]=]
function DataController:GetData(): { [string]: any }
	return if RunService:IsRunning() then self:GetDataReplica().Data else ProfileTemplate
end

--[=[
	Returns the value at the specified path in the player data table. Paths can
	be dot-separated strings or string arrays.

	```lua
	DataController:GetValue("SomeKey.SomeOtherKey")
	-- OR
	DataController:GetValue({"SomeKey", "SomeOtherKey"})
	```

	@param keyPath string | {string} -- The dot-separated path or array of keys
	@return any?
]=]
function DataController:GetValue(keyPath: string | { string }): any?
	local indices
	if typeof(keyPath) == "string" then
		indices = string.split(keyPath, ".")
	elseif typeof(keyPath) == "table" then
		indices = keyPath
	else
		error("Invalid keyPath type: " .. typeof(keyPath))
	end

	local currentLocation = self:GetData()
	for count, index in indices do
		if count == #indices then
			return currentLocation[index]
		end
		currentLocation = currentLocation[index]
	end
end

--[=[
	Immediately invokes `callback` with the current value at `path`, then
	listens for changes and invokes it again on every subsequent update.

	```lua
	DataController:Observe("SomeKey.SomeOtherKey", function(newValue, oldValue)
		print("Value changed from", oldValue, "to", newValue)
	end)
	```

	@param path string -- The dot-separated path to observe
	@param callback function -- Called with (newValue, oldValue) immediately and on every change
	@return RBXScriptConnection
]=]
function DataController:Observe(path: string, callback: (newValue: any, oldValue: any) -> ()): RBXScriptConnection
	local value = DataController:GetValue(path)
	task.spawn(callback, value, value)
	return self:GetDataReplica():ListenToChange(path, callback)
end

--[=[
	Sets a callback to be run when the value at `path` changes. If
	`runImmediately` is true, behaves identically to `Observe` and invokes the
	callback with the current value right away.

	@param path string | {string} -- The path to listen to
	@param callback function -- Called with (newValue, oldValue) on change
	@param runImmediately boolean? -- Whether to invoke the callback immediately with the current value
	@return RBXScriptConnection
]=]
function DataController:SetValueCallback(
	path: string | { string },
	callback: (newValue: any, oldValue: any) -> (),
	runImmediately: boolean?
)
	if runImmediately then
		return self:Observe(path, callback)
	else
		return self:GetDataReplica():ListenToChange(path, callback)
	end
end

-- Framework callbacks

function DataController:Prep()
	if RunService:IsRunning() then
		ReplicaController.ReplicaOfClassCreated("PlayerData", function(replica: Replica)
			PlayerDataReplica = replica
		end)

		ReplicaController.RequestData()
	end
end

return DataController


--[[ File Info

	Author(s): ChiefWildin
	Module: BranchButton.story.luau
	Version: 1.0.0

]]

local shared = require(game.ReplicatedStorage.Order)
local BranchButton = shared("BranchButton") ---@module BranchButton

local Story = {
	controls = {
		Visible = true,
	},
	render = function(props)
		local pane = BranchButton.new("Example Text")
		pane.Gui.Parent = props.target

		-- Define controls

		local controlCallbacks = {
			Visible = function(value: boolean)
				pane:SetVisible(value)
			end,
		}

		-- Subscribe to control changes (don't edit)

		local unsubscribe = props.subscribe(function(_, infos)
			for control, value in pairs(infos) do
				if controlCallbacks[control] and value.__new ~= value.__old then
					controlCallbacks[control](value.__new)
				end
			end
		end)

		-- Initialize control values (don't edit)

		for control, value in pairs(props.controls) do
			if controlCallbacks[control] then
				controlCallbacks[control](value)
			end
		end

		-- Cleanup

		return function() -- Cleanup your story here
			pane:Destroy()
			unsubscribe()
		end
	end,
}

return Story


local storybook = {
	name = script.Parent.Name,
	storyRoots = {
		script.Parent,
	},
}

return storybook


--[[ File Info

	Author(s): ChiefWildin
	Module: BranchButton.luau
	Version: 1.0.0

]]

-- Services

-- Dependencies

local AtomicButton = shared("AtomicButton") ---@module AtomicButton
local BranchButtonTemplate = shared("BranchButtonTemplate") ---@module BranchButtonTemplate

-- Types

-- Module Declaration

local BranchButton = setmetatable({}, AtomicButton)
BranchButton.__index = BranchButton
BranchButton.ClassName = "BranchButton"

-- Constants

local DESIGN_COLOR = Color3.fromRGB(50, 50, 50)
local DESIGN_DISABLED_COLOR = Color3.fromRGB(34, 34, 34)
local DESIGN_HIGHLIGHT_COLOR = Color3.fromRGB(133, 133, 133)
local DESIGN_PRESS_COLOR = Color3.fromRGB(212, 212, 212)

local LABEL_COLOR = Color3.fromRGB(245, 245, 245)
local LABEL_DISABLED_COLOR = Color3.fromRGB(127, 127, 127)

-- Global Variables

-- Objects

-- Private Functions

function BranchButton:_draw()
	local alpha = self._springs.Alpha.Position
	local hover = self._springs.Hovered.Position
	local press = self._springs.Pressed.Position
	local enabled = self._springs.Enabled.Position

	self._scale.Scale = 0.96 + 0.04 * hover - 0.1 * press

	self._design.BackgroundColor3 = DESIGN_COLOR:Lerp(DESIGN_HIGHLIGHT_COLOR, hover)
		:Lerp(DESIGN_PRESS_COLOR, press)
		:Lerp(DESIGN_DISABLED_COLOR, 1 - enabled)
	self._label.TextColor3 = LABEL_COLOR:Lerp(LABEL_DISABLED_COLOR, 1 - enabled)

	self.Gui.Visible = alpha < 0.999
end

-- Public Functions

function BranchButton.new(text: string?, image: string?)
	local self = setmetatable(
		AtomicButton.new(BranchButtonTemplate(), {
			_designStroke = "DesignStroke",
			_design = "Design",
			_label = "Label",
			_scale = "UIScale",
			_thumbnail = "Thumbnail",
		}),
		BranchButton
	)

	-- Private members

	-- Public members

	-- Setup

	self._label.Text = text or "[Unknown]"
	self._thumbnail.Image = image or ""

	self.Maid:GiveTask(self.Hovered:Connect(function(hovered)
		self.Gui.ZIndex = if hovered then 2 else 1
	end))

	self:SetVisible(false, true)

	return self
end

return BranchButton


local function BranchButtonTemplate(): GuiBase2d
	local branchButton = Instance.new("Frame")
	branchButton.Name = "BranchButton"
	branchButton.AnchorPoint = Vector2.new(0.5, 0.5)
	branchButton.BackgroundTransparency = 1
	branchButton.Position = UDim2.fromScale(0.5, 0.5)
	branchButton.Size = UDim2.new(1, 0, 0, 80)

	local uIScale = Instance.new("UIScale")
	uIScale.Scale = 1
	uIScale.Parent = branchButton

	local design = Instance.new("Frame")
	design.Name = "Design"
	design.AnchorPoint = Vector2.new(0.5, 0.5)
	design.BackgroundColor3 = Color3.fromRGB(50, 50, 50)
	design.Position = UDim2.fromScale(0.5, 0.5)
	design.Size = UDim2.new(1, -8, 1, -8)

	local uICorner = Instance.new("UICorner")
	uICorner.Name = "UICorner"
	uICorner.CornerRadius = UDim.new(0.15, 0)
	uICorner.Parent = design

	local designStroke = Instance.new("UIStroke")
	designStroke.Name = "DesignStroke"
	designStroke.Color = Color3.fromRGB(127, 127, 127)
	designStroke.StrokeSizingMode = Enum.StrokeSizingMode.ScaledSize
	designStroke.Thickness = 0.02

	local uIGradient = Instance.new("UIGradient")
	uIGradient.Name = "UIGradient"
	uIGradient.Color = ColorSequence.new({
		ColorSequenceKeypoint.new(0, Color3.fromRGB(255, 85, 0)),
		ColorSequenceKeypoint.new(1, Color3.fromRGB(255, 148, 94)),
	})
	uIGradient.Rotation = 90
	uIGradient.Parent = designStroke

	designStroke.Parent = design

	local uIGradient1 = Instance.new("UIGradient")
	uIGradient1.Name = "UIGradient"
	uIGradient1.Color = ColorSequence.new({
		ColorSequenceKeypoint.new(0, Color3.fromRGB(255, 85, 0)),
		ColorSequenceKeypoint.new(1, Color3.fromRGB(255, 148, 94)),
	})
	uIGradient1.Rotation = 90
	uIGradient1.Parent = design

	local thumbnail = Instance.new("ImageLabel")
	thumbnail.Name = "Thumbnail"
	thumbnail.AnchorPoint = Vector2.new(0.5, 0.5)
	thumbnail.BackgroundTransparency = 1
	thumbnail.Image = "rbxthumb://type=GameThumbnail&id=15694891095&w=480&h=270"
	thumbnail.ImageTransparency = 0.5
	thumbnail.Position = UDim2.fromScale(0.5, 0.5)
	thumbnail.ScaleType = Enum.ScaleType.Crop
	thumbnail.Size = UDim2.fromScale(1, 1)

	local uICorner1 = Instance.new("UICorner")
	uICorner1.Name = "UICorner"
	uICorner1.CornerRadius = UDim.new(0.15, 0)
	uICorner1.Parent = thumbnail

	thumbnail.Parent = design

	design.Parent = branchButton

	local label = Instance.new("TextLabel")
	label.Name = "Label"
	label.AnchorPoint = Vector2.new(0.5, 1)
	label.AutoLocalize = false
	label.BackgroundTransparency = 1
	label.FontFace = Font.new("rbxassetid://12187375422", Enum.FontWeight.Bold, Enum.FontStyle.Normal)
	label.Position = UDim2.fromScale(0.5, 0.9)
	label.Size = UDim2.fromScale(0.925, 0.5)
	label.Text = "Main Place"
	label.TextColor3 = Color3.fromRGB(245, 245, 245)
	label.TextScaled = true
	label.TextXAlignment = Enum.TextXAlignment.Left
	label.Parent = branchButton

	return branchButton
end

-- BranchButtonTemplate().Parent = game.StarterGui

return BranchButtonTemplate


--[[ File Info

	Author(s): ChiefWildin
	Module: BranchCloseButton.luau
	Version: 1.0.0

]]

-- Services

-- Dependencies

local AtomicButton = shared("AtomicButton") ---@module AtomicButton
local BranchCloseButtonTemplate = shared("BranchCloseButtonTemplate") ---@module BranchCloseButtonTemplate

-- Types

-- Module Declaration

local BranchButton = setmetatable({}, AtomicButton)
BranchButton.__index = BranchButton
BranchButton.ClassName = "BranchButton"

-- Constants

local DESIGN_COLOR = Color3.fromRGB(50, 50, 50)
local DESIGN_DISABLED_COLOR = Color3.fromRGB(34, 34, 34)
local DESIGN_HIGHLIGHT_COLOR = Color3.fromRGB(133, 133, 133)
local DESIGN_PRESS_COLOR = Color3.fromRGB(212, 212, 212)

-- Global Variables

-- Objects

-- Private Functions

function BranchButton:_draw()
	local alpha = self._springs.Alpha.Position
	local hover = self._springs.Hovered.Position
	local press = self._springs.Pressed.Position
	local enabled = self._springs.Enabled.Position

	self._scale.Scale = 0.96 + 0.04 * hover - 0.1 * press

	self._design.BackgroundColor3 = DESIGN_COLOR:Lerp(DESIGN_HIGHLIGHT_COLOR, hover)
		:Lerp(DESIGN_PRESS_COLOR, press)
		:Lerp(DESIGN_DISABLED_COLOR, 1 - enabled)

	self.Gui.Visible = alpha < 0.999
end

-- Public Functions

function BranchButton.new()
	local self = setmetatable(
		AtomicButton.new(BranchCloseButtonTemplate(), {
			_design = "Design",
			_scale = "UIScale",
		}),
		BranchButton
	)

	-- Private members

	-- Public members

	-- Setup

	self.Maid:GiveTask(self.Hovered:Connect(function(hovered)
		self.Gui.ZIndex = if hovered then 2 else 1
	end))

	self:SetVisible(false, true)

	return self
end

return BranchButton


local function BranchCloseButtonTemplate(): GuiBase2d
	local branchCloseButton = Instance.new("Frame")
	branchCloseButton.Name = "BranchCloseButton"
	branchCloseButton.AnchorPoint = Vector2.new(1, 0)
	branchCloseButton.BackgroundTransparency = 1
	branchCloseButton.Position = UDim2.new(1, 0, 0, 0)
	branchCloseButton.Size = UDim2.new(0.08, 16, 0.08, 16)
	branchCloseButton.SizeConstraint = Enum.SizeConstraint.RelativeYY

	local uIScale = Instance.new("UIScale")
	uIScale.Scale = 1
	uIScale.Parent = branchCloseButton

	local design = Instance.new("Frame")
	design.Name = "Design"
	design.AnchorPoint = Vector2.new(0.5, 0.5)
	design.BackgroundColor3 = Color3.fromRGB(50, 50, 50)
	design.Position = UDim2.fromScale(0.5, 0.5)
	design.Size = UDim2.new(1, -8, 1, -8)

	local uICorner = Instance.new("UICorner")
	uICorner.Name = "UICorner"
	uICorner.CornerRadius = UDim.new(0.15, 0)
	uICorner.Parent = design

	local designStroke = Instance.new("UIStroke")
	designStroke.Name = "DesignStroke"
	designStroke.Color = Color3.fromRGB(127, 127, 127)
	designStroke.StrokeSizingMode = Enum.StrokeSizingMode.ScaledSize
	designStroke.Thickness = 0.02

	local uIGradient = Instance.new("UIGradient")
	uIGradient.Name = "UIGradient"
	uIGradient.Color = ColorSequence.new({
		ColorSequenceKeypoint.new(0, Color3.fromRGB(255, 85, 0)),
		ColorSequenceKeypoint.new(1, Color3.fromRGB(255, 148, 94)),
	})
	uIGradient.Rotation = 90
	uIGradient.Parent = designStroke

	designStroke.Parent = design

	local uIGradient1 = Instance.new("UIGradient")
	uIGradient1.Name = "UIGradient"
	uIGradient1.Color = ColorSequence.new({
		ColorSequenceKeypoint.new(0, Color3.fromRGB(255, 85, 0)),
		ColorSequenceKeypoint.new(1, Color3.fromRGB(255, 148, 94)),
	})
	uIGradient1.Rotation = 90
	uIGradient1.Parent = design

	design.Parent = branchCloseButton

	local label = Instance.new("TextLabel")
	label.Name = "Label"
	label.AnchorPoint = Vector2.new(0.5, 0.5)
	label.AutoLocalize = false
	label.BackgroundTransparency = 1
	label.FontFace = Font.new("rbxassetid://11702779517", Enum.FontWeight.Bold, Enum.FontStyle.Normal)
	label.Position = UDim2.fromScale(0.5, 0.5)
	label.Size = UDim2.fromScale(0.925, 0.5)
	label.Text = "X"
	label.TextColor3 = Color3.fromRGB(245, 245, 245)
	label.TextScaled = true
	label.TextXAlignment = Enum.TextXAlignment.Center
	label.Parent = branchCloseButton

	return branchCloseButton
end

-- BranchButtonTemplate().Parent = game.StarterGui

return BranchCloseButtonTemplate


local CycleMetatable

local function warning(self, message: string)
	warn(
		"Detected bare code referencing a cyclic dependency ("
			.. tostring(CycleMetatable.CurrentModuleLoading)
			.. " -> "
			.. tostring(self.Name)
			.. ").",
		message,
		debug.traceback(1)
	)
end

CycleMetatable = {
	__index = function(self, key)
		warning(self, "Attempt to read key '" .. key .. "'. Please revise.")
	end,
	__newindex = function(self, key, value)
		warning(self, "Attempt to set key '" .. key .. "' to " .. tostring(value) .. ". Please revise.")
	end,
	__call = function(self, ...)
		warning(self, "Attempt to call module. Please revise.")
	end,
	__concat = function(self, other)
		warning(self, "Attempt to concatenate module with " .. tostring(other) .. ". Please revise.")
	end,
	__unm = function(self)
		warning(self, "Attempt to use unary operator on module. Please revise.")
	end,
	__add = function(self, other)
		warning(self, "Attempt to add module with " .. tostring(other) .. ". Please revise.")
	end,
	__sub = function(self, other)
		warning(self, "Attempt to subtract module with " .. tostring(other) .. ". Please revise.")
	end,
	__mul = function(self, other)
		warning(self, "Attempt to multiply module with " .. tostring(other) .. ". Please revise.")
	end,
	__div = function(self, other)
		warning(self, "Attempt to divide module with " .. tostring(other) .. ". Please revise.")
	end,
	__mod = function(self, other)
		warning(self, "Attempt to modulo module with " .. tostring(other) .. ". Please revise.")
	end,
	__pow = function(self, other)
		warning(self, "Attempt to raise module to power of " .. tostring(other) .. ". Please revise.")
	end,
	__tostring = function(self)
		warning(self, "Attempt to convert module to string. Please revise.")
	end,
	__eq = function(self, other)
		warning(self, "Attempt to compare module with " .. tostring(other) .. ". Please revise.")
	end,
	__lt = function(self, other)
		warning(self, "Attempt to compare module with " .. tostring(other) .. ". Please revise.")
	end,
	__le = function(self, other)
		warning(self, "Attempt to compare module with " .. tostring(other) .. ". Please revise.")
	end,
	__len = function(self)
		warning(self, "Attempt to get length of module. Please revise.")
	end,

	CurrentModuleLoading = nil,
}

return CycleMetatable


export type Initializer = {
	Name: string,
	Async: boolean,
	Protected: boolean,
	WarnDelay: number,
}

export type OrderSettings = {
	Debug: {
		CyclicAnalysis: boolean,
		VerboseLoading: boolean,
	},
	PlaceTypes: { [number]: string },
	CodeGroups: { [string]: { [string]: true } },
	InitOrder: "Individual" | "Project",
	InitFunctionConfig: { Initializer },
	SilentMode: boolean,
}

return {
	Debug = {
		-- When true, performs analysis to detect and report on cyclic
		-- dependency chains.
		CyclicAnalysis = false,
		-- When true, outputs detailed information about the module loading
		-- process.
		VerboseLoading = false,
	},

	-- A mapping of PlaceIds to their place type aliases. This determines the
	-- code groups that are loaded for the current place.
	PlaceTypes = {
		-- [123456789] = "MyPlaceType",
	},

	-- A mapping of place type aliases to the active code groups for that type.
	CodeGroups = {
		Generic = {
			core = true,
		},
	},

	-- "Individual" means that each task will run its initializers in order
	-- before moving on to the next task. "Project" means that all tasks will
	-- run their first initializers, then all tasks will run their second
	-- initializers, and so on.
	InitOrder = "Project",

	-- Initializers will be executed in the order in which they appear in this
	-- array. The options are as follows:
	--
	-- Name: The name of the initialization function.
	-- Async: When true, if this function yields during execution, the thread
	--     will continue to the next initializer while waiting.
	-- Protected: When true, the function will not halt the initialization
	--     process if it errors. Async initializers are always protected.
	-- WarnDelay: The number of seconds to wait before warning about a slow
	--     execution time. Has no effect on async initializers.
	--
	-- Modules can override this configuration by defining a table named
	-- InitConfigOverride that contains the same structure as this table.
	InitFunctionConfig = {
		[1] = {
			Name = "Prep",
			Async = false,
			Protected = true,
			WarnDelay = 1,
		},
		[2] = {
			Name = "Init",
			Async = true,
			Protected = true,
			WarnDelay = 5,
		},
	},

	-- Disables regular output (does not disable warnings)
	SilentMode = not game:GetService("RunService"):IsStudio(),
} :: OrderSettings


--[[
{Madwork}

-[ReplicaServiceListeners]
	Injects listener methods to the Replica class server-side - these methods are otherwise
		only accessible client-side due to personal decisions based on code sanity.

	More information about the decision to opt out server-side listeners can be found here:
	https://devforum.roblox.com/t/replicate-your-states-with-replicaservice-networking-system/894736/22

	After this module is required it directly alters the Replica class in ReplicaService and
	wraps the setter methods to drive connected listeners.

	Injected methods [Replica]:

		Replica:ListenToChange(path, listener) --> [ScriptConnection] (new_value, old_value)
		Replica:ListenToNewKey(path, listener) --> [ScriptConnection] (new_value, new_key)
		Replica:ListenToArrayInsert(path, listener) --> [ScriptConnection] (new_index, new_value)
		Replica:ListenToArraySet(path, listener) --> [ScriptConnection] (index, new_value)
		Replica:ListenToArrayRemove(path, listener) --> [ScriptConnection] (old_index, old_value)
		Replica:ListenToWrite(function_name, listener) --> [ScriptConnection] (params...)
		Replica:ListenToRaw(listener) --> [ScriptConnection] (action_name, path_array, params...)

	-- NOTICE: This module returns a reference to require(ReplicaService) - if your game code execution is
		not linear then it's advised to fetch ReplicaService through this module.
--]]

-- Loaded Modules --

local ReplicaService = shared("ReplicaService") ---@module ReplicaService

export type Replica = ReplicaService.Replica

-- Private functions --

local function StringPathToArray(path)
	local path_array = {}
	if path ~= "" then
		for s in string.gmatch(path, "[^%.]+") do
			table.insert(path_array, s)
		end
	end
	return path_array
end

local function CreateTableListenerPathIndex(replica, path_array, listener_type)
	-- Getting listener table:
	local listeners = replica._table_listeners
	-- Getting and or creating the structure nescessary to index the path for the listened key:
	for i = 1, #path_array do
		local key_listeners = listeners[1][path_array[i]]
		if key_listeners == nil then
			key_listeners = { [1] = {} }
			listeners[1][path_array[i]] = key_listeners
		end
		listeners = key_listeners
	end

	local listener_type_table = listeners[listener_type]
	if listener_type_table == nil then
		listener_type_table = {}
		listeners[listener_type] = listener_type_table
	end
	return listener_type_table
end

local function CleanTableListenerTable(disconnect_param)
	local table_listeners = disconnect_param[1]
	local path_array = disconnect_param[2]
	local pointer = table_listeners
	local pointer_stack = { pointer }
	for i = 1, #path_array do
		pointer = pointer[1][path_array[i]]
		table.insert(pointer_stack, pointer)
	end
	for i = #pointer_stack, 2, -1 do
		local listeners = pointer_stack[i]
		if next(listeners[1]) ~= nil then
			return -- Lower branches exist for this branch - this branch will not need further cleanup
		end
		for k = 2, 6 do
			if listeners[k] ~= nil then
				if #listeners[k] > 0 then
					return -- A listener exists - this branch will not need further cleanup
				end
			end
		end
		pointer_stack[i - 1][1][path_array[i - 1]] = nil -- Clearing listeners table for this branch
	end
end

-- ArrayScriptConnection object:
local ArrayScriptConnection = {
	--[[
		_listener = function -- [function]
		_listener_table = {} -- [table] -- Table from which the function entry will be removed
		_disconnect_listener -- [function / nil]
		_disconnect_param    -- [value / nil]
	--]]
}

function ArrayScriptConnection:Disconnect()
	local listener = self._listener
	if listener ~= nil then
		local listener_table = self._listener_table
		local index = table.find(listener_table, listener)
		if index ~= nil then
			table.remove(listener_table, index)
		end
		self._listener = nil
	end
	if self._disconnect_listener ~= nil then
		self._disconnect_listener(self._disconnect_param)
		self._disconnect_listener = nil
	end
end

local function NewArrayScriptConnection(listener_table, listener, disconnect_listener, disconnect_param) --> [ScriptConnection]
	return {
		_listener = listener,
		_listener_table = listener_table,
		_disconnect_listener = disconnect_listener,
		_disconnect_param = disconnect_param,
		Disconnect = ArrayScriptConnection.Disconnect,
	}
end

-- Initialize --

do
	local Replica = ReplicaService._replica_class

	-- Wrapping Replica creation:
	local new_replica_raw = ReplicaService.NewReplica

	function ReplicaService.NewReplica(replica_params)
		local replica = new_replica_raw(replica_params)

		-- Creating listener tables:
		replica._table_listeners = { [1] = {} }
		replica._function_listeners = {}
		replica._raw_listeners = {}

		return replica
	end

	-- Wrapping Replica listeners:
	local set_value_raw = Replica.SetValue -- (replica, path, value)
	local set_values_raw = Replica.SetValues -- (replica, path, values)
	local array_insert_raw = Replica.ArrayInsert -- (replica, path, value) --> new_index
	local array_set_raw = Replica.ArraySet -- (replica, path, index, value)
	local array_remove_raw = Replica.ArrayRemove -- (replica, path, index) --> removed_value
	local write_raw = Replica.Write -- (replica, function_name, ...) --> return_params...

	function Replica:SetValue(path, value)
		local path_array = (type(path) == "string") and StringPathToArray(path) or path
		local replica = self
		-- Getting path pointer and listener table:
		local pointer = replica.Data
		local listeners = replica._table_listeners
		for i = 1, #path_array - 1 do
			pointer = pointer[path_array[i]]
			if listeners ~= nil then
				listeners = listeners[1][path_array[i]]
			end
		end
		-- Setting value:
		local key = path_array[#path_array]
		local old_value = pointer[key]
		set_value_raw(replica, path_array, value)
		-- Signaling listeners:
		if old_value ~= value and listeners ~= nil then
			if old_value == nil then
				if listeners[3] ~= nil then -- "NewKey" listeners
					for _, listener in ipairs(listeners[3]) do
						listener(value, key)
					end
				end
			end
			listeners = listeners[1][path_array[#path_array]]
			if listeners ~= nil then
				if listeners[2] ~= nil then -- "Change" listeners
					for _, listener in ipairs(listeners[2]) do
						listener(value, old_value)
					end
				end
			end
		end
		-- Raw listeners:
		for _, listener in ipairs(replica._raw_listeners) do
			listener("SetValue", path_array, value)
		end
	end

	function Replica:SetValues(path, values)
		local path_array = (type(path) == "string") and StringPathToArray(path) or path
		local replica = self
		-- Getting path pointer and listener table:
		local pointer = replica.Data
		local listeners = replica._table_listeners
		for i = 1, #path_array do
			pointer = pointer[path_array[i]]
			if listeners ~= nil then
				listeners = listeners[1][path_array[i]]
			end
		end
		-- Setting values:
		for key, value in pairs(values) do
			-- Set value:
			local old_value = pointer[key]
			pointer[key] = value
			-- Signaling listeners:
			if old_value ~= value and listeners ~= nil then
				if old_value == nil then
					if listeners[3] ~= nil then -- "NewKey" listeners
						for _, listener in ipairs(listeners[3]) do
							listener(value, key)
						end
					end
				end
				listeners = listeners[1][key]
				if listeners ~= nil then
					if listeners[2] ~= nil then -- "Change" listeners
						for _, listener in ipairs(listeners[2]) do
							listener(value, old_value)
						end
					end
				end
			end
		end
		-- We end up setting values twice here, but it shouldn't bother anyone too much:
		set_values_raw(replica, path_array, values)
		-- Raw listeners:
		for _, listener in ipairs(replica._raw_listeners) do
			listener("SetValues", path_array, values)
		end
	end

	function Replica:ArrayInsert(path, value) --> new_index
		local path_array = (type(path) == "string") and StringPathToArray(path) or path
		local replica = self
		-- Getting path pointer and listener table:
		local pointer = replica.Data
		local listeners = replica._table_listeners
		for i = 1, #path_array do
			pointer = pointer[path_array[i]]
			if listeners ~= nil then
				listeners = listeners[1][path_array[i]]
			end
		end
		-- Setting value:
		array_insert_raw(replica, path_array, value)
		-- Signaling listeners:
		local new_index = #pointer
		if listeners ~= nil then
			if listeners[4] ~= nil then -- "ArrayInsert" listeners
				for _, listener in ipairs(listeners[4]) do
					listener(new_index, value)
				end
			end
		end
		-- Raw listeners:
		for _, listener in ipairs(replica._raw_listeners) do
			listener("ArrayInsert", path_array, value)
		end
		return new_index
	end

	function Replica:ArraySet(path, index, value)
		local path_array = (type(path) == "string") and StringPathToArray(path) or path
		local replica = self
		-- Getting path pointer and listener table:
		local listeners = replica._table_listeners
		for i = 1, #path_array do
			if listeners ~= nil then
				listeners = listeners[1][path_array[i]]
			end
		end
		-- Setting value:
		array_set_raw(replica, path_array, index, value)
		-- Signaling listeners:
		if listeners ~= nil then
			if listeners[5] ~= nil then -- "ArraySet" listeners
				for _, listener in ipairs(listeners[5]) do
					listener(index, value)
				end
			end
		end
		-- Raw listeners:
		for _, listener in ipairs(replica._raw_listeners) do
			listener("ArraySet", path_array, index, value)
		end
	end

	function Replica:ArrayRemove(path, index) --> removed_value
		local path_array = (type(path) == "string") and StringPathToArray(path) or path
		local replica = self
		-- Getting path pointer and listener table:
		local listeners = replica._table_listeners
		for i = 1, #path_array do
			if listeners ~= nil then
				listeners = listeners[1][path_array[i]]
			end
		end
		-- Setting value:
		local old_value = array_remove_raw(replica, path_array, index)
		-- Signaling listeners:
		if listeners ~= nil then
			if listeners[6] ~= nil then -- "ArrayRemove" listeners
				for _, listener in ipairs(listeners[6]) do
					listener(index, old_value)
				end
			end
		end
		-- Raw listeners:
		for _, listener in ipairs(replica._raw_listeners) do
			listener("ArrayRemove", path_array, index, old_value)
		end
		return old_value
	end

	function Replica:Write(function_name, ...) --> return_params...
		local return_params = table.pack(write_raw(self, function_name, ...))
		-- Signaling listeners:
		local func_id = self._write_lib[function_name]
		func_id = func_id and func_id[1]

		local listeners = self._function_listeners[func_id]
		if listeners ~= nil then
			for _, listener in ipairs(listeners) do
				listener(...)
			end
		end
		return table.unpack(return_params)
	end

	-- Listener methods:
	function Replica:ListenToChange(path, listener) --> [ScriptConnection] listener(new_value)
		if type(listener) ~= "function" then
			error("[ReplicaService]: Only a function can be set as listener in Replica:ListenToChange()")
		end

		local path_array = (type(path) == "string") and StringPathToArray(path) or path
		if #path_array < 1 then
			error("[ReplicaService]: Passed empty path - a value key must be specified")
		end
		-- Getting listener table for given path:
		local listeners = CreateTableListenerPathIndex(self, path_array, 2)
		table.insert(listeners, listener)
		-- ScriptConnection which allows the disconnection of the listener:
		return NewArrayScriptConnection(
			listeners,
			listener,
			CleanTableListenerTable,
			{ self._table_listeners, path_array }
		)
	end

	function Replica:ListenToNewKey(path, listener) --> [ScriptConnection] listener(new_value, new_key)
		if type(listener) ~= "function" then
			error("[ReplicaService]: Only a function can be set as listener in Replica:ListenToNewKey()")
		end

		local path_array = (type(path) == "string") and StringPathToArray(path) or path
		-- Getting listener table for given path:
		local listeners = CreateTableListenerPathIndex(self, path_array, 3)
		table.insert(listeners, listener)
		-- ScriptConnection which allows the disconnection of the listener:
		if #path_array == 0 then
			return NewArrayScriptConnection(listeners, listener)
		else
			return NewArrayScriptConnection(
				listeners,
				listener,
				CleanTableListenerTable,
				{ self._table_listeners, path_array }
			)
		end
	end

	function Replica:ListenToArrayInsert(path, listener) --> [ScriptConnection] listener(new_value, new_index)
		if type(listener) ~= "function" then
			error("[ReplicaService]: Only a function can be set as listener in Replica:ListenToArrayInsert()")
		end

		local path_array = (type(path) == "string") and StringPathToArray(path) or path
		-- Getting listener table for given path:
		local listeners = CreateTableListenerPathIndex(self, path_array, 4)
		table.insert(listeners, listener)
		-- ScriptConnection which allows the disconnection of the listener:
		if #path_array == 0 then
			return NewArrayScriptConnection(listeners, listener)
		else
			return NewArrayScriptConnection(
				listeners,
				listener,
				CleanTableListenerTable,
				{ self._table_listeners, path_array }
			)
		end
	end

	function Replica:ListenToArraySet(path, listener) --> [ScriptConnection] listener(new_value, index)
		if type(listener) ~= "function" then
			error("[ReplicaService]: Only a function can be set as listener in Replica:ListenToArraySet()")
		end

		local path_array = (type(path) == "string") and StringPathToArray(path) or path
		-- Getting listener table for given path:
		local listeners = CreateTableListenerPathIndex(self, path_array, 5)
		table.insert(listeners, listener)
		-- ScriptConnection which allows the disconnection of the listener:
		if #path_array == 0 then
			return NewArrayScriptConnection(listeners, listener)
		else
			return NewArrayScriptConnection(
				listeners,
				listener,
				CleanTableListenerTable,
				{ self._table_listeners, path_array }
			)
		end
	end

	function Replica:ListenToArrayRemove(path, listener) --> [ScriptConnection] listener(old_value, old_index)
		if type(listener) ~= "function" then
			error("[ReplicaService]: Only a function can be set as listener in Replica:ListenToArrayRemove()")
		end

		local path_array = (type(path) == "string") and StringPathToArray(path) or path
		-- Getting listener table for given path:
		local listeners = CreateTableListenerPathIndex(self, path_array, 6)
		table.insert(listeners, listener)
		-- ScriptConnection which allows the disconnection of the listener:
		if #path_array == 0 then
			return NewArrayScriptConnection(listeners, listener)
		else
			return NewArrayScriptConnection(
				listeners,
				listener,
				CleanTableListenerTable,
				{ self._table_listeners, path_array }
			)
		end
	end

	function Replica:ListenToWrite(function_name, listener) --> [ScriptConnection] listener(params...)
		if type(listener) ~= "function" then
			error("[ReplicaService]: Only a function can be set as listener in Replica:ListenToWrite()")
		end
		if self._write_lib == nil then
			error("[ReplicaService]: _write_lib was not declared for this replica")
		end

		local func_id = self._write_lib[function_name]
		func_id = func_id and func_id[1]
		if func_id == nil then
			error(
				'[ReplicaService]: Write function "'
					.. function_name
					.. '" not declared inside _write_lib of this replica'
			)
		end

		-- Getting listener table for given path:
		local listeners = self._function_listeners[func_id]
		if listeners == nil then
			listeners = {}
			self._function_listeners[func_id] = listeners
		end
		table.insert(listeners, listener)
		-- ScriptConnection which allows the disconnection of the listener:
		return NewArrayScriptConnection(listeners, listener)
	end

	function Replica:ListenToRaw(listener) --> [ScriptConnection] (action_name, params...)
		local listeners = self._raw_listeners
		table.insert(listeners, listener)
		return NewArrayScriptConnection(listeners, listener)
	end
end

return ReplicaService


--[[ File Info

	Author(s): ChiefWildin
	Module: BadgeGiver.luau
	Version: 1.0.1

]]

-- Services

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

-- Dependencies

local PlayerData = shared("PlayerData") ---@module PlayerData
local rcall = shared("rcall") ---@module rcall

-- Module Declaration

--[=[
	@class BadgeGiver

	Handles awarding badges to players.
]=]
local BadgeGiver = {}

-- Constants

local STUDIO_PRINTS_ENABLED = false

-- Public Functions

--[=[
	Awards the specified badge to the player if they do not already have it.
	Otherwise, records that we've already given the badge to this player in
	their data profile.

	@yields
	@param player Player -- The player to award the badge to.
	@param badgeId number -- The ID of the badge to award.
]=]
function BadgeGiver:GiveBadge(player: Player, badgeId: number)
	-- Don't even try if we already know it's been awarded
	if not RunService:IsRunning() or PlayerData:GetValue(player, { "AwardedBadges", badgeId }) then
		return
	end

	if RunService:IsStudio() then
		if STUDIO_PRINTS_ENABLED then
			print("Badge award attempt -", player, badgeId)
		end

		return
	end

	if rcall({ retryLimit = 3 }, BadgeService.UserHasBadgeAsync, BadgeService, player.UserId, badgeId) then
		-- Need to double check because player might have left during ownership
		-- check
		if player.Parent then
			PlayerData:SetValue(player, { "AwardedBadges", badgeId }, true)
		end

		return
	end

	-- Need to double check because player might have left during ownership
	-- check
	rcall({ retryLimit = 3 }, BadgeService.AwardBadge, BadgeService, player.UserId, badgeId)

	if player.Parent then
		PlayerData:SetValue(player, { "AwardedBadges", badgeId }, true)
	end
end

return BadgeGiver


--[[
	AnalyticsServer.luau
	ChiefWildin
    Version: 1.2.0
]]

-- Services

local AnalyticsService = game:GetService("AnalyticsService")
local HttpService = game:GetService("HttpService")
local Players = game:GetService("Players")

-- Dependencies

local GamebeastSDK = shared("GamebeastSDK") ---@module GamebeastSDK

local GamebeastMarkers = GamebeastSDK:GetService("Markers")

-- Main job table

local AnalyticsServer = {}

-- Constants

-- Global Variables

local FunnelSessionIds: { [Player]: string } = {}

-- Local functions

-- Public functions

-- Framework callbacks

function AnalyticsServer:Prep()
	local fetchSuccess, gamebeastSecret = pcall(HttpService.GetSecret, HttpService, "Gamebeast")
	if fetchSuccess then
		GamebeastSDK:Setup({
			key = gamebeastSecret,
			sdkSettings = {
				sdkDebugEnabled = false,
				sdkWarningsEnabled = false,
				includeWarningStackTrace = false,
			},
		})
	else
		print("[AnalyticsServer]: Gamebeast API secret not found. Gamebeast analytics will be disabled.")
	end

	local function setupPlayer(player: Player)
		if player:IsA("Configuration") then
			return
		end

		FunnelSessionIds[player] = HttpService:GenerateGUID(false)
	end
	for _, player in Players:GetPlayers() do
		task.spawn(setupPlayer, player)
	end
	Players.PlayerAdded:Connect(setupPlayer)
	Players.PlayerRemoving:Connect(function(player: Player)
		if player:IsA("Configuration") then
			return
		end

		FunnelSessionIds[player] = nil
	end)
end

function AnalyticsServer:Init() end

return AnalyticsServer


return {
	Name = "branch",
	Aliases = { "build" },
	Group = "DefaultAdmin",
	Description = "Teleport to a specific development branch.",
	Args = {
		{
			Type = "branch",
			Name = "branch",
			Description = "The branch to teleport to."
		},
		{
			Type = "players",
			Name = "players",
			Description = "Players who will be teleported to the branch."
		}
	}
}


local TeleportService = game:GetService("TeleportService")

local GetBranches = shared("GetBranches") ---@module GetBranches

return function(_, branch: string, players: { Player })
	if not branch then
		return
	end

	local branches = GetBranches()

	local branchId = nil
	for _, branchInfo in branches do
		if string.lower(branchInfo.Name) == branch:lower() then
			branchId = branchInfo.PlaceId
			break
		end
	end

	if not branchId then
		return "Invalid branch"
	end

	TeleportService:TeleportAsync(branchId, players)

	return `Teleporting {#players} players to branch {branch}`
end


return {
	Name = "ban",
	Aliases = {},
	Group = "DefaultAdmin",
	Description = "Bans a player using Roblox's BanService",
	Args = {
		{
			Type = "players",
			Name = "Players",
			Description = "The player or group of players to ban."
		},
		{
			Type = "number",
			Name = "Duration",
			Description = "How long to ban the user for in days. Set to 0 for a permanent ban."
		},
		{
			Type = "string",
			Name = "PublicReason",
			Description = "The reason for the ban that will be shown to the user. (make sure to type this in quotes)"
		},
		{
			Type = "string",
			Name = "PrivateReason",
			Description = "The reason for the ban for our internal notes. (make sure to type this in quotes)"
		},
		{
			Type = "boolean",
			Name = "AlsoBanAlts",
			Description = "Whether to ban the user's known alt accounts as well."
		}
	}
}


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

return function(_, players: {Player}, duration: number, publicReason: string, privateReason: string, alsoBanAlts: boolean)
	if #publicReason > 400 then
		return "Public reason is too long. Please keep it under 400 characters."
	end
	if #privateReason > 1000 then
		return "Private reason is too long. Please keep it under 1000 characters."
	end
	if duration < 0 then
		return "Duration must be a positive number."
	end

	local ids = {}
	for _, player in ipairs(players) do
		table.insert(ids, player.UserId)
	end

	Players:BanAsync({
		UserIds = ids,
		ApplyToUniverse = true,
		Duration = if duration == 0 then -1 else duration * 86400,
		DisplayReason = publicReason,
		PrivateReason = privateReason,
		ExcludeAltAccounts = not alsoBanAlts,
	})

	return `Banned {#players} players {if duration == 0 then "permanently." else "for " .. duration .. " days."}`
end


return {
	Name = "banid",
	Aliases = {},
	Group = "DefaultAdmin",
	Description = "Bans a player by ID using Roblox's BanService",
	Args = {
		{
			Type = "number",
			Name = "UserId",
			Description = "The ID of the player to ban."
		},
		{
			Type = "number",
			Name = "Duration",
			Description = "How long to ban the user for in days. Set to 0 for a permanent ban."
		},
		{
			Type = "string",
			Name = "PublicReason",
			Description = "The reason for the ban that will be shown to the user. (make sure to type this in quotes)"
		},
		{
			Type = "string",
			Name = "PrivateReason",
			Description = "The reason for the ban for our internal notes. (make sure to type this in quotes)"
		},
		{
			Type = "boolean",
			Name = "AlsoBanAlts",
			Description = "Whether to ban the user's known alt accounts as well."
		}
	}
}


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

return function(_, userId: number, duration: number, publicReason: string, privateReason: string, alsoBanAlts: boolean)
	if #publicReason > 400 then
		return "Public reason is too long. Please keep it under 400 characters."
	end
	if #privateReason > 1000 then
		return "Private reason is too long. Please keep it under 1000 characters."
	end
	if duration < 0 then
		return "Duration must be a positive number."
	end

	Players:BanAsync({
		UserIds = { userId },
		ApplyToUniverse = true,
		Duration = if duration == 0 then -1 else duration * 86400,
		DisplayReason = publicReason,
		PrivateReason = privateReason,
		ExcludeAltAccounts = not alsoBanAlts,
	})

	local username = Players:GetNameFromUserIdAsync(userId)

	return `Banned {username} ({userId}) {if duration == 0 then "permanently." else "for " .. duration .. " days."}`
end


return {
	Name = "banuser",
	Aliases = {},
	Group = "DefaultAdmin",
	Description = "Bans a player by username using Roblox's BanService",
	Args = {
		{
			Type = "string",
			Name = "Username",
			Description = "The username of the player to ban."
		},
		{
			Type = "number",
			Name = "Duration",
			Description = "How long to ban the user for in days. Set to 0 for a permanent ban."
		},
		{
			Type = "string",
			Name = "PublicReason",
			Description = "The reason for the ban that will be shown to the user. (make sure to type this in quotes)"
		},
		{
			Type = "string",
			Name = "PrivateReason",
			Description = "The reason for the ban for our internal notes. (make sure to type this in quotes)"
		},
		{
			Type = "boolean",
			Name = "AlsoBanAlts",
			Description = "Whether to ban the user's known alt accounts as well."
		}
	}
}


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

return function(_, username: string, duration: number, publicReason: string, privateReason: string, alsoBanAlts: boolean)
	if #publicReason > 400 then
		return "Public reason is too long. Please keep it under 400 characters."
	end
	if #privateReason > 1000 then
		return "Private reason is too long. Please keep it under 1000 characters."
	end
	if duration < 0 then
		return "Duration must be a positive number."
	end

	local userId = Players:GetUserIdFromNameAsync(username)

	Players:BanAsync({
		UserIds = { userId },
		ApplyToUniverse = true,
		Duration = if duration == 0 then -1 else duration * 86400,
		DisplayReason = publicReason,
		PrivateReason = privateReason,
		ExcludeAltAccounts = not alsoBanAlts,
	})

	return `Banned {username} ({userId}) {if duration == 0 then "permanently." else "for " .. duration .. " days."}`
end


return {
	Name = "unbanid",
	Aliases = { "unban" },
	Group = "DefaultAdmin",
	Description = "Unbans a player by ID using Roblox's BanService",
	Args = {
		{
			Type = "number",
			Name = "UserId",
			Description = "The ID of the player to unban."
		},
	}
}


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

return function(_, userId: number)
	Players:UnbanAsync({
		UserIds = { userId },
		ApplyToUniverse = true,
	})

	local username = Players:GetNameFromUserIdAsync(userId)

	return `Unbanned {username} ({userId})`
end


return {
	Name = "unbanuser",
	Aliases = {},
	Group = "DefaultAdmin",
	Description = "Unbans a player by username using Roblox's BanService",
	Args = {
		{
			Type = "string",
			Name = "Username",
			Description = "The username of the player to unban."
		},
	}
}


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

return function(_, username: string)
	local userId = Players:GetUserIdFromNameAsync(username)

	Players:UnbanAsync({
		UserIds = { userId },
		ApplyToUniverse = true,
	})

	return `Unbanned {username} ({userId})`
end


local GetBranches = shared("GetBranches") ---@module GetBranches

return function (registry)
	local currentBranches = GetBranches()
	local arrayOfBranches = {}

	for _, branch in currentBranches do
		table.insert(arrayOfBranches, branch.Name)
	end

	return registry:RegisterType("branch", registry.Cmdr.Util.MakeEnumType("branch", arrayOfBranches))
end


--[[
    Author: ChiefWildin
    Module: CmdrHooks.lua
    Version: 1.1.0
]]

-- Services

-- Dependencies

local Cmdr = shared("Cmdr") ---@module Cmdr

-- Types

-- Module Declaration

local CmdrHooks = {}

-- Constants

-- Global Variables

-- Objects

-- Private Functions

-- Public Functions

-- Job Initialization

function CmdrHooks:Init()
	Cmdr:RegisterDefaultCommands()
    Cmdr:RegisterCommandsIn(script.CustomCommands)
    Cmdr:RegisterTypesIn(script.CustomTypes)
	Cmdr:RegisterHooksIn(script.CustomHooks)
end

return CmdrHooks


-- A BaseObject basically just adds the :Destroy() interface, and a _maid, along with an optional object it references.
-- @classmod BaseObject

local Maid = shared("Maid")

local BaseObject = {}
BaseObject.ClassName = "BaseObject"
BaseObject.__index = BaseObject

function BaseObject.new(obj)
	local self = setmetatable({}, BaseObject)

	self._maid = Maid.new()
	self._obj = obj

	return self
end

function BaseObject:Destroy()
	-- local startTime = tick()
	self._maid:DoCleaning()

	-- This could emit events, which could cause bad startTime
	-- but we'll take this risk over getting the ClassName
	-- if tick() - startTime >= 0.01 then
	-- 	warn(("[BaseObject.Destroy] - Took %f ms to clean up %s")
	-- 		:format((tick() - startTime)*1000, tostring(self.ClassName)))
	-- end

	setmetatable(self, nil)
end

return BaseObject


--[[
{Madwork}

-[RateLimiter]
	Prevents RemoteEvent spamming; Player references are automatically removed as they leave

	Members:

		RateLimiter.Default   [RateLimiter]

	Functions:

		RateLimiter.NewRateLimiter(rate) --> [RateLimiter]
			rate   [number] -- Events per second allowed; Excessive events are dropped

	Methods [RateLimiter]:

		RateLimiter:CheckRate(source) --> is_to_be_processed [bool] -- Whether event should be processed
			source   [any]

		RateLimiter:CleanSource(source) -- Forgets about the source - must be called for any object that
			has been passed to RateLimiter:CheckRate() after that object is no longer going to be used;
			Does not have to be called for Player instances!

		RateLimiter:Cleanup() -- Forgets all sources

		RateLimiter:Destroy() -- Make the RateLimiter module forget about this RateLimiter object

--]]

local SETTINGS = {
	DefaultRateLimiterRate = 120,
}

-- Service Table

local RateLimiter = {
	Default = nil,
}

-- Private Variables --

local Players = game:GetService("Players")

local PlayerReference = {} -- {player = true}
local RateLimiters = {} -- {rate_limiter = true, ...}

-- Public functions --

-- RateLimiter object:
local RateLimiterObject = {
	--[[
		_sources = {},
		_rate_period = 0,
	--]]
}
RateLimiterObject.__index = RateLimiterObject

function RateLimiterObject:CheckRate(source) --> is_to_be_processed [bool] -- Whether event should be processed
	local sources = self._sources
	local os_clock = os.clock()

	local rate_time = sources[source]
	if rate_time ~= nil then
		rate_time = math.max(os_clock, rate_time + self._rate_period)
		if rate_time - os_clock < 1 then
			sources[source] = rate_time
			return true
		else
			return false
		end
	else
		-- Preventing from remembering players that already left:
		if typeof(source) == "Instance" and source:IsA("Player") and PlayerReference[source] == nil then
			return false
		end
		sources[source] = os_clock + self._rate_period
		return true
	end
end

function RateLimiterObject:CleanSource(source) -- Forgets about the source - must be called for any object that
	self._sources[source] = nil
end

function RateLimiterObject:Cleanup() -- Forgets all sources
	self._sources = {}
end

function RateLimiterObject:Destroy() -- Make the RateLimiter module forget about this RateLimiter object
	RateLimiters[self] = nil
end

-- Module functions:
function RateLimiter.NewRateLimiter(rate) --> [RateLimiter]
	if rate <= 0 then
		error("[RateLimiter]: Invalid rate")
	end

	local rate_limiter = {
		_sources = {},
		_rate_period = 1 / rate,
	}
	setmetatable(rate_limiter, RateLimiterObject)

	RateLimiters[rate_limiter] = true

	return rate_limiter
end

-- Initialize --

for _, player in ipairs(Players:GetPlayers()) do
	PlayerReference[player] = true
end

RateLimiter.Default = RateLimiter.NewRateLimiter(SETTINGS.DefaultRateLimiterRate)

-- Connections --

Players.PlayerAdded:Connect(function(player)
	PlayerReference[player] = true
end)

Players.PlayerRemoving:Connect(function(player)
	PlayerReference[player] = nil
	-- Automatic player reference cleanup:
	for rate_limiter in pairs(RateLimiters) do
		rate_limiter._sources[player] = nil
	end
end)

return RateLimiter