-- local Madwork = _G.Madwork
--[[
{Madwork}

-[ReplicaController]
	(STANDALONE VERSION)
	Lua table replication achieved through write function wrapping

	LISTENER WARNING: Making listeners disconnect their own script connections may result in other listeners
		being skipped. Fix pending.

	WARNING: Replica update listeners are not cleaned up automatically (e.g. when their value's parent table is set to nil)
		unless the replica is destroyed. Either split one god replica into several replicas or carefully manage listeners
		with :Disconnect() to prevent memory leaks. Does not apply to destroyed replicas.

	Notice: Replicas are destroyed client-side when the server stops replicating the replica to the client or the
		server destroys the replica completely. This means that the exact replica that was previously destroyed
		client-side could be created again client-side (as a new object, though).

	Replica replication guarantees:
		- When the client receives first data, replica references received will have all nested replicas
			already loaded and accessible through Replica.Children;
		- When the client receives first data or receives selective replication of a top level replica,
			.NewReplicaSignal and .ReplicaOfClassCreated() will be fired for all replicas in the order
			they were created server-side from earliest to latest;

	Members:

		ReplicaController.NewReplicaSignal            [ScriptSignal](Replica) -- Fired every time an replica is created client-side
		ReplicaController.InitialDataReceivedSignal   [ScriptSignal]() -- Fired once after the client finishes receiving initial replica data from server
		ReplicaController.InitialDataReceived         [bool] -- Set to true after the client finishes receiving initial replica data from server

	Functions:

		ReplicaController.RequestData() -- Requests the server to start sending replica data

		ReplicaController.ReplicaOfClassCreated(replica_class, listener) --> [ScriptConnection] listener(replica)

		ReplicaController.GetReplicaById(replica_id) --> replica / nil

	Members [Replica]:

		Replica.Data       [table] (Read only) Table which is replicated

		Replica.Id         [number] Unique identifier
		Replica.Class      [string] Primary Replica identifier
		Replica.Tags       [table] Secondary Replica identifiers

		Replica.Parent     [Replica] or nil
		Replica.Children   [table]: {replica, ...}

	Methods [Replica]:

	-- Dictionary update listening: (listener functions can't yield)
		Replica:ListenToChange(path, listener) --> [ScriptConnection] (new_value, old_value)
		Replica:ListenToNewKey(path, listener) --> [ScriptConnection] (new_value, new_key)

		* Notice: When Replica:SetValues(path, values) is used server-side, Replica:ListenToChange() and Replica:ListenToNewKey()
			will only be invoked with changes to the top level keys in the "values" argument passed.

	-- (Numeric) Array update listening:
		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)

	-- Write function listening:
		Replica:ListenToWrite(function_name, listener) --> [ScriptConnection] (params...)

		* Parameter description for "path":

			[string] = "TableMember.TableMember" -- Roblox-style path
			[table] = {"Players", 2312310, "Health"} -- Key array path

		Replica:ListenToRaw(listener) --> [ScriptConnection] (action_name, path_array, params...)
			-- ("SetValue", path_array, value)
			-- ("SetValues", path_array, values)
			-- ("ArrayInsert", path_array, value, new_index)
			-- ("ArraySet", path_array, index, value)
			-- ("ArrayRemove", path_array, index, old_value)

	-- Signals:
		Replica:ConnectOnClientEvent(listener) --> [ScriptConnection] (params...) -- listener functions can't yield
		Replica:FireServer(params...) -- Fire a signal to server-side listeners for this specific Replica

	-- Children:
		Replica:ListenToChildAdded(listener) --> [ScriptConnection] listener(replica)

		Replica:FindFirstChildOfClass(replica_class) --> [Replica] or nil
			replica_class   [string]

	-- Debug:
		Replica:Identify() --> [string]

	-- Cleanup:

		Replica:IsActive() --> is_active [bool] -- Returns false if the replica was destroyed

		Replica:AddCleanupTask(task) -- Add cleanup task to be performed
		Replica:RemoveCleanupTask(task) -- Remove cleanup task

		* Parameter description for "Replica:AddCleanupTask()":

			[function] -- Function to be invoked when the Replica is destroyed (function can't yield)
			[RBXScriptConnection] -- Roblox script connection to be :Disconnect()'ed when the Replica is destroyed
			[Object] -- Object with a :Destroy() method to be destroyed when the Replica is destroyed (destruction method can't yield)

	-- Write function setters: (Calling outside a write function will throw an error)

		Replica:SetValue(path, value)
		Replica:SetValues(path, values)
		Replica:ArrayInsert(path, value) --> new_index
		Replica:ArraySet(path, index, value)
		Replica:ArrayRemove(path, index) --> removed_value
		Replica:Write(function_name, params...) --> return_params...

--]]

export type Replica = {
	Data: {},
	Id: number,
	Class: string,
	Tags: {},
	Parent: Replica,
	Children: {},
	IsActive: (Replica) -> boolean,
	Identify: (Replica) -> string,
	AddCleanupTask: (Replica) -> () | Instance,
	RemoveCleanupTask: (Replica) -> () | Instance,
	ListenToWrite: (Replica, string, (...any) -> ()) -> RBXScriptConnection,
	ListenToChange: (Replica, path: string | { string }, listener: (...any) -> ()) -> RBXScriptConnection,
	ListenToNewKey: (Replica, path: string | { string }, listener: (...any) -> ()) -> RBXScriptConnection,
	ListenToArrayInsert: (Replica, path: string | { string }, listener: (...any) -> ()) -> RBXScriptConnection,
	ListenToArraySet: (Replica, path: string | { string }, listener: (...any) -> ()) -> RBXScriptConnection,
	ListenToArrayRemove: (Replica, path: string | { string }, listener: (...any) -> ()) -> RBXScriptConnection,
	ListenToRaw: (Replica, callback: (...any) -> ()) -> RBXScriptConnection,
	ListenToChildAdded: (Replica, callback: (...any) -> ()) -> RBXScriptConnection,
	FindFirstChildOfClass: (Replica, ClassName: string) -> Replica?,
	ConnectOnClientEvent: (Replica, callback: (...any) -> ()) -> RBXScriptConnection,
	FireServer: (Replica, ...any) -> (),
}

local SETTINGS = {
	RequestDataRepeat = 10,
	SetterError = "[ReplicaController]: Replica setters can only be called inside write functions",
}

local Madwork -- Standalone Madwork reference for portable version of ReplicaService/ReplicaController
do
	local RunService = game:GetService("RunService")

	local function WaitForDescendant(ancestor, instance_name, warn_name)
		local instance = ancestor:FindFirstChild(instance_name, true) -- Recursive
		if instance == nil then
			local start_time = os.clock()
			local connection
			connection = ancestor.DescendantAdded:Connect(function(descendant)
				if descendant.Name == instance_name then
					instance = descendant
				end
			end)
			while instance == nil do
				if
					start_time ~= nil
					and os.clock() - start_time > 1
					and (RunService:IsServer() == true or game:IsLoaded() == true)
				then
					start_time = nil
					warn(
						"["
							.. script.Name
							.. "]: Missing "
							.. warn_name
							.. ' "'
							.. instance_name
							.. '" in '
							.. ancestor:GetFullName()
							.. "; Please check setup documentation"
					)
				end
				task.wait()
			end
			connection:Disconnect()
			return instance
		else
			return instance
		end
	end

	local RemoteEventContainer
	if RunService:IsServer() == true then
		RemoteEventContainer = Instance.new("Folder")
		RemoteEventContainer.Name = "ReplicaRemoteEvents"
		RemoteEventContainer.Parent = game:GetService("ReplicatedStorage")
	else
		RemoteEventContainer = WaitForDescendant(game:GetService("ReplicatedStorage"), "ReplicaRemoteEvents", "folder")
	end

	Madwork = {
		GetShared = function(package_name, item_name)
			-- Ignoring package_name as we're working without Madwork framework
			return WaitForDescendant(game:GetService("ReplicatedStorage"), item_name, "module")
		end,
		GetModule = function(package_name, module_name)
			return WaitForDescendant(game:GetService("ServerScriptService"), module_name, "module")
		end,
		SetupRemoteEvent = function(remote_name)
			if RunService:IsServer() == true then
				local remote_event = Instance.new("RemoteEvent")
				remote_event.Name = remote_name
				remote_event.Parent = RemoteEventContainer
				return remote_event
			else
				return WaitForDescendant(RemoteEventContainer, remote_name, "remote event")
			end
		end,
		Shared = {}, -- A Madwork package reference - ReplicaService will try to check this table
	}

	local MadworkScriptSignal = require(Madwork.GetShared("Madwork", "MadworkScriptSignal"))
	Madwork.NewScriptSignal = MadworkScriptSignal.NewScriptSignal
	Madwork.NewArrayScriptConnection = MadworkScriptSignal.NewArrayScriptConnection
end

-- Controller Table

local ReplicaController = {

	NewReplicaSignal = Madwork.NewScriptSignal(), -- (Replica)
	InitialDataReceivedSignal = Madwork.NewScriptSignal(), -- ()
	InitialDataReceived = false,

	_replicas = {
		--[[
			[replica_id] = {
				Data = {}, -- [table] Replicated Replica data table
				Id = 1, -- [integer] Replica id
				Class = "", -- [string] Primary Replica identifier
				Tags = {UserId = 2312310}, -- [table] Secondary Replica identifiers

				Parent = Replica, -- [Replica / nil] -- Child replicas inherit replication settings
				Children = {}, -- [table] {replica, ...}

				_write_lib = {[func_id] = function, ...} / nil, -- [table] List of wrapped write functions
				_write_lib_dictionary = {["function_name"] = func_id, ...} / nil, -- [table] Dictionary of function names and their id's

				_table_listeners = {[1] = {}} -- [table] Listeners of specific value changes in replica data
				_function_listeners = {[func_id] = {listener, ...}, ...} -- [table] Listeners of write function calls
				_raw_listeners = {listener, ...},

				_signal_listeners = {},
				_maid = maid,
			}
		--]]
	},

	_class_listeners = {}, -- {["replica_class"] = script_signal, ...}
	_child_listeners = {}, -- {[replica_id] = {listener, ...}, ...}
}

--[[
	_table_listeners structure:

		_table_listeners = {
			[1] = {
				["key_of_table"] = {
					[1] = {["key_of_table"] = {...}, ...},
					[2] = {listener, ...} / nil, -- Change
					[3] = {listener, ...} / nil, -- NewKey
					[4] = {listener, ...} / nil, -- ArrayInsert
					[5] = {listener, ...} / nil, -- ArraySet
					[6] = {listener, ...} / nil, -- ArrayRemove
				},
			},
			[2] = {listener, ...} / nil, -- Change
			[3] = {listener, ...} / nil, -- NewKey
			[4] = {listener, ...} / nil, -- ArrayInsert
			[5] = {listener, ...} / nil, -- ArraySet
			[6] = {listener, ...} / nil, -- ArrayRemove
		}

	_function_listeners structure:

		_function_listeners = {
			[func_id] = {
				listening_function, ...
			},
			...
		}
--]]

-- Loaded Modules

local MadworkMaid = require(Madwork.GetShared("Madwork", "MadworkMaid"))

-- Private Variables

local Replica

local Replicas = ReplicaController._replicas
local NewReplicaSignal = ReplicaController.NewReplicaSignal

local ClassListeners = ReplicaController._class_listeners -- {["replica_class"] = script_signal, ...}
local ChildListeners = ReplicaController._child_listeners -- {[replica_id] = {listener, ...}, ...}

local rev_ReplicaRequestData = Madwork.SetupRemoteEvent("Replica_ReplicaRequestData") -- Fired client-side when the client loads for the first time

local rev_ReplicaSetValue = Madwork.SetupRemoteEvent("Replica_ReplicaSetValue") -- (replica_id, {path}, value)
local rev_ReplicaSetValues = Madwork.SetupRemoteEvent("Replica_ReplicaSetValues") -- (replica_id, {path}, {values})
local rev_ReplicaArrayInsert = Madwork.SetupRemoteEvent("Replica_ReplicaArrayInsert") -- (replica_id, {path}, value)
local rev_ReplicaArraySet = Madwork.SetupRemoteEvent("Replica_ReplicaArraySet") -- (replica_id, {path}, index, value)
local rev_ReplicaArrayRemove = Madwork.SetupRemoteEvent("Replica_ReplicaArrayRemove") -- (replica_id, {path}, index)
local rev_ReplicaWrite = Madwork.SetupRemoteEvent("Replica_ReplicaWrite") -- (replica_id, func_id, params...)
local rev_ReplicaSignal = Madwork.SetupRemoteEvent("Replica_ReplicaSignal") -- (replica_id, params...)
local rev_ReplicaSetParent = Madwork.SetupRemoteEvent("Replica_ReplicaSetParent") -- (replica_id, parent_replica_id)
local rev_ReplicaCreate = Madwork.SetupRemoteEvent("Replica_ReplicaCreate") -- (replica_id, {replica_data}) OR (top_replica_id, {creation_data}) or ({replica_package})
local rev_ReplicaDestroy = Madwork.SetupRemoteEvent("Replica_ReplicaDestroy") -- (replica_id)

local DataRequestStarted = false

local LoadedWriteLibPacks = {}
--[[
	LoadedWriteLibPacks = {
		[ModuleScript] = {
			{[func_id] = function, ...}, -- Write functions
			{["function_name"] = func_id, ...}, -- Function names and their id's
		},
		...
	}
--]]

local WriteFunctionFlag = false -- Set to true when running inside a write function stack

-- Private functions

local function GetWriteLibFunctionsRecursive(list_table, pointer, name_stack)
	for key, value in pairs(pointer) do
		if type(value) == "table" then
			GetWriteLibFunctionsRecursive(list_table, value, name_stack .. key .. ".")
		elseif type(value) == "function" then
			table.insert(list_table, { name_stack .. key, value })
		else
			error(
				'[ReplicaController]: Invalid write function value "'
					.. tostring(value)
					.. '" ('
					.. typeof(value)
					.. '); name_stack = "'
					.. name_stack
					.. '"'
			)
		end
	end
end

local function LoadWriteLib(write_lib_module)
	local get_write_lib_pack = LoadedWriteLibPacks[write_lib_module]
	if get_write_lib_pack ~= nil then
		return get_write_lib_pack
	end

	local write_lib_raw = require(write_lib_module)

	local function_list = {} -- func_id = {func_name, func}

	GetWriteLibFunctionsRecursive(function_list, write_lib_raw, "")
	table.sort(function_list, function(item1, item2)
		return item1[1] < item2[1] -- Sort functions by their names - this creates a consistent indexing on server and client-side
	end)

	local write_lib = {} -- {[func_id] = function, ...}
	local write_lib_dictionary = {} -- {["function_name"] = func_id, ...}

	for func_id, func_params in ipairs(function_list) do
		write_lib[func_id] = func_params[2]
		write_lib_dictionary[func_params[1]] = func_id
	end

	local write_lib_pack = { write_lib, write_lib_dictionary }

	LoadedWriteLibPacks[write_lib_module] = write_lib_pack

	return write_lib_pack
end

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 DestroyReplicaAndDescendantsRecursive(replica, not_first_in_stack)
	-- Scan children replicas:
	for _, child in ipairs(replica.Children) do
		DestroyReplicaAndDescendantsRecursive(child, true)
	end

	local id = replica.Id
	-- Clear replica entry:
	Replicas[id] = nil
	-- Cleanup:
	replica._maid:Cleanup()
	-- Clear from children table of top parent replica:
	if not_first_in_stack ~= true then
		if replica.Parent ~= nil then
			local children = replica.Parent.Children
			table.remove(children, table.find(children, replica))
		end
	end
	-- Clear child listeners:
	ChildListeners[id] = nil
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

local function CreateReplicaBranch(replica_entries, created_replicas) --> created_replicas: {replica, ...}
	-- Sorting replica entries:
	-- replica_entries = {[replica_id_string] = {replica_class, replica_tags, data_table, parent_id / 0, write_lib_module / nil}, ...}
	local sorted_replica_entries = {} -- {replica_class, replica_tags, data_table, parent_id / 0, write_lib_module / nil, replica_id}, ...}
	for replica_id_string, replica_entry in pairs(replica_entries) do
		replica_entry[6] = tonumber(replica_id_string)
		table.insert(sorted_replica_entries, replica_entry)
	end
	table.sort(sorted_replica_entries, function(a, b)
		return a[6] < b[6]
	end)
	local waiting_for_parent = {} -- [parent_replica_id] = {replica, ...}
	created_replicas = created_replicas or {}
	for _, replica_entry in ipairs(sorted_replica_entries) do
		local replica_id = replica_entry[6]
		-- Fetching replica parent:
		local parent_id = replica_entry[4]
		local parent = nil
		local wait_for_parent = false
		if parent_id ~= 0 then
			parent = Replicas[parent_id]
			if parent == nil then
				wait_for_parent = true
			end
		end
		-- Fetching replica write_lib:
		local write_lib, write_lib_dictionary
		if replica_entry[5] ~= nil then
			local write_lib_pack = LoadWriteLib(replica_entry[5])
			write_lib = write_lib_pack[1]
			write_lib_dictionary = write_lib_pack[2]
		end
		-- New Replica object table:
		local replica = {
			Data = replica_entry[3],
			Id = replica_id,
			Class = replica_entry[1],
			Tags = replica_entry[2],

			Parent = parent,
			Children = {},

			_write_lib = write_lib,
			_write_lib_dictionary = write_lib_dictionary,

			_table_listeners = { [1] = {} },
			_function_listeners = {},
			_raw_listeners = {},

			_signal_listeners = {},
			_maid = MadworkMaid.NewMaid(),
		}
		setmetatable(replica, Replica)
		-- Setting as child to parent:
		if parent ~= nil then
			table.insert(parent.Children, replica)
		elseif wait_for_parent == true then
			local wait_table = waiting_for_parent[parent_id]
			if wait_table == nil then
				wait_table = {}
				waiting_for_parent[parent_id] = wait_table
			end
			table.insert(wait_table, replica)
		end
		-- Adding replica to replica list:
		Replicas[replica_id] = replica
		table.insert(created_replicas, replica)
		-- Checking replicas waiting for parent:
		local children_waiting = waiting_for_parent[replica_id]
		if children_waiting ~= nil then
			waiting_for_parent[replica_id] = nil
			for _, child_replica in ipairs(children_waiting) do
				child_replica.Parent = replica
				table.insert(replica.Children, child_replica)
			end
		end
	end
	if next(waiting_for_parent) ~= nil then
		-- An error occured while replicating an replica branch.
		local error_string = "[ReplicaService]: BRANCH REPLICATION ERROR - Missing parents: "
		for parent_replica_id, child_replicas in pairs(waiting_for_parent) do
			error_string = error_string .. "[" .. tostring(parent_replica_id) .. "]: {"
			for k, replica in ipairs(child_replicas) do
				error_string = error_string .. (k == 1 and "" or ", ") .. replica:Identify()
			end
			error_string = error_string .. "}; "
		end
		error(error_string)
	end
	return created_replicas
end

-- Write handlers:

local function ReplicaSetValue(replica_id, path_array, value)
	local replica = Replicas[replica_id]
	-- 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]
	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][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

local function ReplicaSetValues(replica_id, path_array, values)
	local replica = Replicas[replica_id]
	-- 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
			local key_listeners = listeners[1][key]
			if key_listeners ~= nil then
				if key_listeners[2] ~= nil then -- "Change" listeners
					for _, listener in ipairs(key_listeners[2]) do
						listener(value, old_value)
					end
				end
			end
		end
	end
	-- Raw listeners:
	for _, listener in ipairs(replica._raw_listeners) do
		listener("SetValues", path_array, values)
	end
end

local function ReplicaArrayInsert(replica_id, path_array, value) --> new_index
	local replica = Replicas[replica_id]
	-- 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:
	table.insert(pointer, 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, new_index)
	end
	return new_index
end

local function ReplicaArraySet(replica_id, path_array, index, value)
	local replica = Replicas[replica_id]
	-- 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:
	pointer[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

local function ReplicaArrayRemove(replica_id, path_array, index) --> removed_value
	local replica = Replicas[replica_id]
	-- 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:
	local old_value = table.remove(pointer, 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

-- Public functions

-- Replica object:

Replica = {}
Replica.__index = Replica

-- Listening:
function Replica:ListenToChange(path, listener) --> [ScriptConnection] listener(new_value)
	if type(listener) ~= "function" then
		error("[ReplicaController]: 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("[ReplicaController]: 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 Madwork.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("[ReplicaController]: 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 Madwork.NewArrayScriptConnection(listeners, listener)
	else
		return Madwork.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("[ReplicaController]: 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 Madwork.NewArrayScriptConnection(listeners, listener)
	else
		return Madwork.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("[ReplicaController]: 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 Madwork.NewArrayScriptConnection(listeners, listener)
	else
		return Madwork.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("[ReplicaController]: 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 Madwork.NewArrayScriptConnection(listeners, listener)
	else
		return Madwork.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("[ReplicaController]: Only a function can be set as listener in Replica:ListenToWrite()")
	end
	if self._write_lib == nil then
		error("[ReplicaController]: _write_lib was not declared for this replica")
	end

	local func_id = self._write_lib_dictionary[function_name]
	if func_id == nil then
		error(
			'[ReplicaController]: 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 Madwork.NewArrayScriptConnection(listeners, listener)
end

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

-- Signals:
function Replica:ConnectOnClientEvent(listener) --> [ScriptConnection] listener(...)
	if type(listener) ~= "function" then
		error("[ReplicaController]: Only functions can be passed to Replica:ConnectOnClientEvent()")
	end
	table.insert(self._signal_listeners, listener)
	return Madwork.NewArrayScriptConnection(self._signal_listeners, listener)
end

function Replica:FireServer(...)
	rev_ReplicaSignal:FireServer(self.Id, ...)
end

-- Children:
function Replica:ListenToChildAdded(listener) --> [ScriptConnection] listener(replica)
	if type(listener) ~= "function" then
		error("[ReplicaController]: Only a function can be set as listener")
	end
	if Replicas[self.Id] == nil then
		return -- Replica is destroyed - listener will not be connected
	end
	-- Getting listener table for replica class:
	local listeners = ChildListeners[self.Id]
	if listeners == nil then
		listeners = {}
		ChildListeners[self.Id] = listeners
	end
	table.insert(listeners, listener)
	-- ScriptConnection which allows the disconnection of the listener:
	return Madwork.NewArrayScriptConnection(listeners, listener)
end

function Replica:FindFirstChildOfClass(replica_class) --> [Replica] or nil
	for _, child in ipairs(self.Children) do
		if child.Class == replica_class then
			return child
		end
	end
	return nil
end

-- Debug:
function Replica:Identify() --> [string]
	local tag_string = ""
	local first_tag = true
	for tag_key, tag_val in pairs(self.Tags) do
		tag_string = tag_string .. (first_tag and "" or ";") .. tostring(tag_key) .. "=" .. tostring(tag_val)
	end
	return "[Id:" .. tostring(self.Id) .. ";Class:" .. self.Class .. ";Tags:{" .. tag_string .. "}]"
end

-- Cleanup:

function Replica:IsActive() --> is_active [bool]
	return Replicas[self.Id] ~= nil
end

function Replica:AddCleanupTask(task)
	return self._maid:AddCleanupTask(task)
end

function Replica:RemoveCleanupTask(task)
	self._maid:RemoveCleanupTask(task)
end

-- Write function setters: (Calling outside a write function will throw an error)

function Replica:SetValue(path, value)
	if WriteFunctionFlag == false then
		error(SETTINGS.SetterError)
	end
	local path_array = (type(path) == "string") and StringPathToArray(path) or path
	ReplicaSetValue(self.Id, path_array, value)
end

function Replica:SetValues(path, values)
	if WriteFunctionFlag == false then
		error(SETTINGS.SetterError)
	end
	local path_array = (type(path) == "string") and StringPathToArray(path) or path
	ReplicaSetValues(self.Id, path_array, values)
end

function Replica:ArrayInsert(path, value) --> new_index
	if WriteFunctionFlag == false then
		error(SETTINGS.SetterError)
	end
	local path_array = (type(path) == "string") and StringPathToArray(path) or path
	return ReplicaArrayInsert(self.Id, path_array, value)
end

function Replica:ArraySet(path, index, value)
	if WriteFunctionFlag == false then
		error(SETTINGS.SetterError)
	end
	local path_array = (type(path) == "string") and StringPathToArray(path) or path
	ReplicaArraySet(self.Id, path_array, index, value)
end

function Replica:ArrayRemove(path, index) --> removed_value
	if WriteFunctionFlag == false then
		error(SETTINGS.SetterError)
	end
	local path_array = (type(path) == "string") and StringPathToArray(path) or path
	return ReplicaArrayRemove(self.Id, path_array, index)
end

function Replica:Write(function_name, ...) --> return_params...
	if WriteFunctionFlag == false then
		error(SETTINGS.SetterError)
	end
	local func_id = self._write_lib_dictionary[function_name]
	local return_params = table.pack(self._write_lib[func_id](self, ...))
	-- Signaling listeners:
	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

-- ReplicaController functions:

function ReplicaController.RequestData() -- Call after all client controllers are loaded and before CoreReadySignal is fired
	if DataRequestStarted == true then
		return
	end
	DataRequestStarted = true
	task.spawn(function() -- In case the initial rev_ReplicaRequestData signal was lost (Highly unlikely)
		while game:IsLoaded() == false do
			task.wait()
		end
		rev_ReplicaRequestData:FireServer()
		while task.wait(SETTINGS.RequestDataRepeat) do
			if ReplicaController.InitialDataReceived == true then
				break
			end
			rev_ReplicaRequestData:FireServer()
		end
	end)
end

function ReplicaController.ReplicaOfClassCreated(replica_class, listener) --> [ScriptConnection] listener(replica)
	if type(replica_class) ~= "string" then
		error("[ReplicaController]: replica_class must be a string")
	end
	if type(listener) ~= "function" then
		error(
			"[ReplicaController]: Only a function can be set as listener in ReplicaController.ReplicaOfClassCreated()"
		)
	end
	-- Getting listener table for replica class:
	local signal = ClassListeners[replica_class]
	if signal == nil then
		signal = Madwork.NewScriptSignal()
		ClassListeners[replica_class] = signal
	end
	return signal:Connect(listener, function()
		-- Cleanup script signals that are no longer used:
		if signal:GetListenerCount() == 0 and ClassListeners[replica_class] == signal then
			ClassListeners[replica_class] = nil
		end
	end)
end

function ReplicaController.GetReplicaById(replica_id)
	return Replicas[replica_id]
end

-- Connections

-- Fired from server after initial data is sent:
rev_ReplicaRequestData.OnClientEvent:Connect(function()
	ReplicaController.InitialDataReceived = true
	-- print("[ReplicaController]: Initial data received")
	ReplicaController.InitialDataReceivedSignal:Fire()
end)

-- Replica data changes:
rev_ReplicaSetValue.OnClientEvent:Connect(ReplicaSetValue) -- (replica_id, {path}, value)

rev_ReplicaSetValues.OnClientEvent:Connect(ReplicaSetValues) -- (replica_id, {path}, {values})

rev_ReplicaArrayInsert.OnClientEvent:Connect(ReplicaArrayInsert) -- (replica_id, {path}, value)

rev_ReplicaArraySet.OnClientEvent:Connect(ReplicaArraySet) -- (replica_id, {path}, index, value)

rev_ReplicaArrayRemove.OnClientEvent:Connect(ReplicaArrayRemove) -- (replica_id, {path}, index)

rev_ReplicaWrite.OnClientEvent:Connect(function(replica_id, func_id, ...) -- (replica_id, func_id, {params})
	local replica = Replicas[replica_id]
	-- Running function:
	WriteFunctionFlag = true
	replica._write_lib[func_id](replica, ...)
	WriteFunctionFlag = false
	-- Signaling listeners:
	local listeners = replica._function_listeners[func_id]
	if listeners ~= nil then
		for _, listener in ipairs(listeners) do
			listener(...)
		end
	end
end)

-- Replica signals:
rev_ReplicaSignal.OnClientEvent:Connect(function(replica_id, ...) -- (replica_id, params...)
	local replica = Replicas[replica_id]
	-- Signaling listeners:
	local listeners = replica._signal_listeners
	for _, listener in ipairs(listeners) do
		listener(...)
	end
end)

-- Inheritance:
rev_ReplicaSetParent.OnClientEvent:Connect(function(replica_id, parent_replica_id) -- (replica_id, parent_replica_id)
	local replica = Replicas[replica_id]
	local old_parent_children = replica.Parent.Children
	local new_parent = Replicas[parent_replica_id]
	table.remove(old_parent_children, table.find(old_parent_children, replica))
	table.insert(new_parent.Children, replica)
	replica.Parent = new_parent
	-- Trigger child added:
	local child_listener_table = ChildListeners[parent_replica_id]
	if child_listener_table ~= nil then
		for i = 1, #child_listener_table do
			child_listener_table[i](replica)
		end
	end
end)

-- Replica creation:
rev_ReplicaCreate.OnClientEvent:Connect(
	function(param1, param2) -- (top_replica_id, {replica_data}) OR (top_replica_id, {creation_data}) or ({replica_package})
		--[[
	param1 description:
		top_replica_id = replica_id
		OR
		replica_package = {{replica_id, creation_data}, ...}
	param2 description:
		replica_data = {replica_class, replica_tags, data_table, parent_id / 0, write_lib_module / nil}
		OR
		creation_data = {["replica_id"] = replica_data, ...}
	--]]
		local created_replicas = {}
		-- Unpacking replica data:
		if type(param1) == "table" then -- Replica package
			table.sort(param1, function(a, b) -- Sorting top level replicas by their id
				return a[1] < b[1]
			end)
			for _, replica_branch_entry in ipairs(param1) do
				CreateReplicaBranch(replica_branch_entry[2], created_replicas)
			end
		elseif param2[1] ~= nil then -- One replica data
			CreateReplicaBranch({ [tostring(param1)] = param2 }, created_replicas)
		else -- Creation data table
			CreateReplicaBranch(param2, created_replicas)
		end
		-- Broadcasting replica creation:
		table.sort(created_replicas, function(a, b)
			return a.Id < b.Id
		end)
		-- 1) Child added:
		for _, replica in ipairs(created_replicas) do
			local parent_replica = replica.Parent
			if parent_replica ~= nil then
				local child_listener_table = ChildListeners[parent_replica.Id]
				if child_listener_table ~= nil then
					for i = 1, #child_listener_table do
						child_listener_table[i](replica)
					end
				end
			end
		end
		-- 2) New Replica and Replica of class created:
		for _, replica in ipairs(created_replicas) do
			NewReplicaSignal:Fire(replica)
			local class_listener_signal = ClassListeners[replica.Class]
			if class_listener_signal ~= nil then
				class_listener_signal:Fire(replica)
			end
		end
	end
)

-- Replica destruction:
rev_ReplicaDestroy.OnClientEvent:Connect(function(replica_id) -- (replica_id)
	local replica = Replicas[replica_id]
	DestroyReplicaAndDescendantsRecursive(replica)
end)

return ReplicaController


local RunService = game:GetService("RunService")

local BaseObject = shared("BaseObject")
local Maid = shared("Maid")
local String = shared("String")

local TemplateProvider = setmetatable({ Priority = 100 }, BaseObject)
TemplateProvider.ClassName = "TemplateProvider"
TemplateProvider.__index = TemplateProvider

-- @param[opt=nil] container
function TemplateProvider.new(container)
	local self = setmetatable(BaseObject.new(), TemplateProvider)

	self.Priority = 1
	self._containersToInitializeSet = {}

	if container then
		self:AddContainer(container)
	end

	self._registry = {} -- [name] = rawTemplate
	self._containersSet = {} -- [parent] = true

	return self
end

-- Initializes the container provider
function TemplateProvider:Provide()
	assert(self._containersToInitializeSet, "Already initialized")

	local containers = self._containersToInitializeSet
	self._containersToInitializeSet = nil

	for container, _ in pairs(containers) do
		self:AddContainer(container)
	end
end

-- Clones the template. If it has a prefix of "Template" then it will remove it
function TemplateProvider:Clone(templateName)
	assert(type(templateName) == "string", "templateName must be a string")

	self:_verifyInit()

	local template = self._registry[templateName]
	if not template then
		error(("[TemplateProvider.Clone] - Cannot provide %q"):format(tostring(templateName)))
		return nil
	end

	local newItem = template:Clone()
	newItem.Name = String.removePostfix(templateName, "Template")
	return newItem
end

-- Returns the raw template
function TemplateProvider:Get(templateName)
	assert(type(templateName) == "string", "templateName must be a string")
	self:_verifyInit()

	return self._registry[templateName]
end

-- Adds a new container to the provider for provision of assets
function TemplateProvider:AddContainer(container)
	assert(typeof(container) == "Instance")

	if self._containersToInitializeSet then
		self._containersToInitializeSet[container] = true
	else
		assert(not self._containersSet[container], "Already added")

		self._containersSet[container] = true
		self._maid[container] = self:_loadFolder(container)
	end
end

function TemplateProvider:RemoveContainer(container)
	if self._containersToInitializeSet then
		self._containersToInitializeSet[container] = nil
	else
		self._containersSet[container] = nil
		self._maid[container] = nil
	end
end

-- Returns whether or not a template is registered at the time
function TemplateProvider:IsAvailable(templateName)
	assert(type(templateName) == "string", "templateName must be a string")
	self:_verifyInit()

	return self._registry[templateName] ~= nil
end

-- Returns all current registered items
function TemplateProvider:GetAll()
	self:_verifyInit()

	local list = {}
	for _, item in pairs(self._registry) do
		table.insert(list, item)
	end

	return list
end

-- Gets the container
function TemplateProvider:GetContainers()
	self:_verifyInit()

	local list = {}
	for parent, _ in pairs(self._containersSet) do
		table.insert(list, parent)
	end
	return list
end

function TemplateProvider:_verifyInit()
	if not self._containersToInitializeSet then
		return
	end

	if not RunService:IsRunning() then
		-- Initialize for hoarcecat!
		self:Init()
	end

	assert(not self._containersToInitializeSet, "TemplateProvider is not initialized")
end

function TemplateProvider:_transformParent(getParent)
	if typeof(getParent) == "Instance" then
		return getParent
	elseif type(getParent) == "function" then
		local container = getParent()
		assert(typeof(container) == "Instance")
		return container
	else
		error("Bad getParent type")
	end
end

function TemplateProvider:_loadFolder(parent)
	local maid = Maid.new()

	-- Only connect events if we're running
	if RunService:IsRunning() then
		maid:GiveTask(parent.ChildAdded:Connect(function(child)
			self:_handleChildAdded(maid, child)
		end))
		maid:GiveTask(parent.ChildRemoved:Connect(function(child)
			self:_handleChildRemoved(maid, child)
		end))
	end

	for _, child in pairs(parent:GetChildren()) do
		self:_handleChildAdded(maid, child)
	end

	maid:GiveTask(function()
		maid:DoCleaning()

		-- Deregister children
		for _, child in pairs(parent:GetChildren()) do
			self:_handleChildRemoved(maid, child)
		end
	end)

	return maid
end

function TemplateProvider:_handleChildRemoved(maid, child)
	maid[child] = nil
	self:_removeFromRegistry(child)
end

function TemplateProvider:_handleChildAdded(maid, child)
	if child:IsA("Folder") then
		maid[child] = self:_loadFolder(child)
	elseif child:IsA("PackageLink") then
		return
	else
		self:_addToRegistery(child)
	end
end

function TemplateProvider:_addToRegistery(child)
	local childName = child.Name
	if self._registry[childName] then
		warn(("[TemplateProvider._addToRegistery] - Duplicate %q in registery. Overridding"):format(childName))
	end

	self._registry[childName] = child
end

function TemplateProvider:_removeFromRegistry(child)
	local childName = child.Name

	if self._registry[childName] == child then
		self._registry[childName] = nil
	end
end

return TemplateProvider


--[[ File Info

	Author: ChiefWildin
	Module: CacheService.lua
	Version: 1.3.0

]]

--[=[
	@class CacheService

	Simple module for caching any data.
]=]

-- Root

local CacheService = {}

-- Types

--[=[
	@interface Cache
	@within CacheService

	.EntryLimit number -- Maximum number of entries the cache will hold
	.EntryAgeLimit number -- Number of seconds entries are allowed to exist
	.Entries {[string]: {Data: any, EntryTime: number}} -- The cached entries
]=]
export type Cache = {
	EntryLimit: number,
	EntryAgeLimit: number,
	Entries: { [string]: {
		Data: any,
		EntryTime: number,
	} },
	Get: (self: Cache, index: string) -> any?,
	GetOrAdd: (self: Cache, index: string, addCallback: () -> any?) -> any?,
	Set: (self: Cache, index: string, data: any) -> (),
}

-- Variables

local Caches: { [string]: Cache } = {}

-- Constants

-- The total number of entries allowed in a cache
local DEFAULT_CACHE_ENTRY_LIMIT = 1000
-- The number of seconds that entries are allowed to exist for
local DEFAULT_ENTRY_AGE_LIMIT = 60 * 60 -- One hour
-- The number of seconds between cache limit checks
local LIMIT_CHECK_INTERVAL = 30
local VERBOSE_OUTPUT = false
local CLOCK = tick

-- Private Functions

local function vprint(...)
	if VERBOSE_OUTPUT then
		print(...)
	end
end

-- API Functions

--[=[
	@class Cache

	A named cache object returned by [CacheService:CreateCache()](/api/CacheService#CreateCache). Stores
	key-value pairs with an age limit and entry limit.
]=]

local Cache = {} :: Cache
Cache.__index = Cache

function Cache.new(name: string, entryLimit: number?, entryAgeLimit: number?): Cache
	local self = Caches[name]

	if not self then
		self = setmetatable({}, Cache)

		self.EntryLimit = entryLimit or DEFAULT_CACHE_ENTRY_LIMIT
		self.EntryAgeLimit = entryAgeLimit or DEFAULT_ENTRY_AGE_LIMIT
		self.Entries = {}
	else
		self.EntryLimit = entryLimit or self.EntryLimit
		self.EntryAgeLimit = entryAgeLimit or self.EntryAgeLimit
	end

	return self
end

--[=[
	Returns the cached value at the given index, or `nil` if the entry is
	missing or expired.

	@param index string -- The cache key to look up
	@return any? -- The cached value, or nil if missing or expired
]=]
function Cache:Get(index: string): any?
	if not index then
		return nil
	end

	if not self.Entries[index] or self.Entries[index].EntryTime + self.EntryAgeLimit < CLOCK() then
		vprint(`[CacheService] Requested data entry ({index}) is expired. Removing.`)
		self.Entries[index] = nil
		return nil
	end

	return self.Entries[index].Data
end

--[=[
	Returns the cached value at the given index. If it is missing or expired,
	calls `addCallback` to produce a value, caches it, and returns it.

	@param index string -- The cache key to look up
	@param addCallback function -- Called to produce the value if not cached
	@return any? -- The cached or newly produced value
]=]
function Cache:GetOrAdd(index: string, addCallback: () -> any?): any?
	local data = self:Get(index)

	if not data then
		data = addCallback()
		self:Set(index, data)
	end

	return data
end

--[=[
	Stores the given value at the specified index.

	@param index string -- The cache key to store the value under
	@param data any -- The value to cache
]=]
function Cache:Set(index: string, data: any)
	if not index then
		return
	end

	self.Entries[index] = {
		Data = data,
		EntryTime = CLOCK(),
	}
end

--[=[
	Returns the named `Cache`, creating it if it does not already exist.

	@param cacheName string -- The name of the cache
	@param entryLimit number? -- Maximum number of entries (default 1000)
	@param entryAgeLimit number? -- Entry lifetime in seconds (default 3600)
	@return Cache
]=]
function CacheService:CreateCache(cacheName: string, entryLimit: number?, entryAgeLimit: number?): Cache
	local cache = Caches[cacheName]
	if not cache then
		cache = Cache.new(cacheName, entryLimit, entryAgeLimit)
		Caches[cacheName] = cache
	end

	return cache
end

--[=[
	Stores a value at the given index in the named cache.

	@param cacheName string -- The name of the cache
	@param index string -- The cache key
	@param data any -- The value to cache
]=]
function CacheService:Set(cacheName: string, index: string, data: any)
	Cache.new(cacheName):Set(index, data)
end

--[=[
	Returns the cached value at the given index, or `nil` if missing or expired.

	@param cacheName string -- The name of the cache
	@param index string -- The cache key to look up
	@return any?
]=]
function CacheService:Get(cacheName: string, index: string)
	return Cache.new(cacheName):Get(index)
end

--[=[
	Returns the cached value at the given index. If missing or expired, calls
	`addCallback` to produce a value, caches it, and returns it.

	@param cacheName string -- The name of the cache
	@param index string -- The cache key to look up
	@param addCallback function -- Called to produce the value if not cached
	@return any?
]=]
function CacheService:GetOrAdd(cacheName: string, index: string, addCallback: () -> any?): any?
	return Cache.new(cacheName):GetOrAdd(index, addCallback)
end

-- Cache clearing to prevent memory leakage
task.spawn(function()
	while task.wait(LIMIT_CHECK_INTERVAL) do
		vprint("[CacheService] Validating caches")
		for cacheName: string, cache: Cache in pairs(Caches) do
			local cacheSize = 0

			for entryName, data in pairs(cache.Entries) do
				if CLOCK() - data.EntryTime > cache.EntryAgeLimit then
					vprint(`[CacheService] Cached data entry ({entryName}) in cache {cacheName} is expired. Removing.`)
					cache.Entries[entryName] = nil
				else
					cacheSize += 1
				end
			end

			if cacheSize > cache.EntryLimit then
				vprint(
					`[CacheService] Number of cached entries exceeded limit ({cache.EntryLimit}) in cache {cacheName}, clearing.`
				)
				Caches[cacheName].Entries = {}
			end
		end
	end
end)

return CacheService


--[[ File Info

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

]]

-- Services

local Debris = game:GetService("Debris")

-- Types

export type DebugPartProperties = {
	Parent: Instance?,
	Size: Vector3?,
	CFrame: CFrame?,
	Name: string?,
	Position: Vector3?,
	Orientation: Vector3?,
	Anchored: boolean?,
	CanCollide: boolean?,
	Transparency: number?,
	Color: Color3?,
	Lifetime: number?,
}

-- Module Declaration

local DebugPart = {}
DebugPart.__index = DebugPart

-- Global Variables

local Count = 0

-- Public Functions

function DebugPart.new(properties: DebugPartProperties)
	Count += 1

	local self = setmetatable({}, DebugPart)

	self._part = Instance.new("Part")
	self._part.Size = properties.Size or Vector3.new(1, 1, 1)
	self._part.Position = properties.Position or Vector3.new(0, 0, 0)
	self._part.Orientation = properties.Orientation or Vector3.new(0, 0, 0)
	self._part.Anchored = properties.Anchored or true
	self._part.CanCollide = properties.CanCollide or false
	self._part.Transparency = properties.Transparency or 0.5
	self._part.Color = properties.Color or Color3.fromRGB(255, 0, 0)
	self._part.Name = properties.Name or "DebugPart" .. Count

	if properties.CFrame then
		self._part.CFrame = properties.CFrame
	end

	if properties.Lifetime then
		Debris:AddItem(self._part, properties.Lifetime)
	end

	self._part.Parent = properties.Parent or workspace

	return self
end

return DebugPart


--[[ File Info

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

]]

-- Services

local Debris = game:GetService("Debris")

-- Module Declaration

local DebugRaycast = {}

-- Constants

local DEFAULT_HIT_COLOR = Color3.fromRGB(0, 255, 0)
local DEFAULT_MISS_COLOR = Color3.fromRGB(255, 0, 0)

-- Global Variables

local Container: Folder?

-- Private Functions

local function GetContainer()
	if not Container then
		Container = Instance.new("Folder")
		Container.Name = "DebugRaycast"
		Container.Parent = workspace
	end

	return Container
end

local function CreateDebugPart(
	color: Color3,
	cframe: CFrame,
	size: Vector3,
	shape: Enum.PartType,
	transparency: number?,
	duration: number?
)
	local debugPart = Instance.new("Part")
	debugPart.Anchored = true
	debugPart.CanCollide = false
	debugPart.CanQuery = false
	debugPart.CanTouch = false
	debugPart.CastShadow = false
	debugPart.Color = color
	debugPart.Material = Enum.Material.Neon
	debugPart.CFrame = cframe
	debugPart.Shape = shape
	debugPart.Size = size
	debugPart.Transparency = transparency or 0.5
	debugPart.Parent = GetContainer()

	if duration then
		Debris:AddItem(debugPart, duration)
	end

	return debugPart
end

-- local function CreateDebugMeshPart(color: Color3, cframe: CFrame, transparency: number?, duration: number?)
-- 	local debugPart = Instance.new("MeshPart")
-- 	debugPart.Anchored = true
-- 	debugPart.CanCollide = false
-- 	debugPart.CanQuery = false
-- 	debugPart.CanTouch = false
-- 	debugPart.CastShadow = false
-- 	debugPart.Color = color
-- 	debugPart.Material = Enum.Material.Neon
-- 	debugPart.CFrame = cframe
-- 	debugPart.Size = Vector3.new(1, 1, 1)
-- 	debugPart.Transparency = transparency or 0.5
-- 	debugPart.Parent = GetContainer()

-- 	if duration then
-- 		Debris:AddItem(debugPart, duration)
-- 	end

-- 	return debugPart
-- end

-- Public Functions

function DebugRaycast:Spherecast(
	position: Vector3,
	radius: number,
	direction: Vector3,
	params: RaycastParams?,
	hitColor: Color3?,
	missColor: Color3?,
	transparency: number?,
	duration: number?
)
	local raycastResult = workspace:Spherecast(position, radius, direction, params)
	hitColor = hitColor or DEFAULT_HIT_COLOR
	missColor = missColor or DEFAULT_MISS_COLOR
	local color = raycastResult and hitColor or missColor
	local endPos = raycastResult and raycastResult.Position or position + direction

	-- Start
	CreateDebugPart(
		color,
		CFrame.new(position),
		Vector3.new(radius * 2, radius * 2, radius * 2),
		Enum.PartType.Ball,
		transparency,
		duration
	)
	-- Middle
	CreateDebugPart(
		color,
		CFrame.new(position:Lerp(endPos, 0.5))
			* CFrame.new(position, endPos).Rotation
			* CFrame.Angles(0, math.pi / 2, 0),
		Vector3.new((position - endPos).Magnitude, radius * 2, radius * 2),
		Enum.PartType.Cylinder,
		transparency,
		duration
	)
	-- End
	CreateDebugPart(
		color,
		CFrame.new(endPos),
		Vector3.new(radius * 2, radius * 2, radius * 2),
		Enum.PartType.Ball,
		transparency,
		duration
	)

	return raycastResult
end

function DebugRaycast:Blockcast(
	cframe: CFrame,
	size: Vector3,
	direction: Vector3,
	params: RaycastParams?,
	hitColor: Color3?,
	missColor: Color3?,
	transparency: number?,
	duration: number?
)
	local raycastResult = workspace:Blockcast(cframe, size, direction, params)
	hitColor = hitColor or DEFAULT_HIT_COLOR
	missColor = missColor or DEFAULT_MISS_COLOR
	local color = raycastResult and hitColor or missColor
	local endPos = raycastResult and raycastResult.Position or cframe.Position + direction

	CreateDebugPart(
		color,
		CFrame.new(cframe.Position:Lerp(endPos, 0.5)) * CFrame.new(cframe.Position, endPos).Rotation,
		Vector3.new(size.X, size.Y, (cframe.Position - endPos).Magnitude),
		Enum.PartType.Block,
		transparency,
		duration
	)
	-- local meshPart = CreateDebugMeshPart(
	-- 	color,
	-- 	CFrame.new(cframe.Position:Lerp(endPos, 0.5)) * CFrame.new(cframe.Position, endPos).Rotation,
	-- 	transparency,
	-- 	duration
	-- )
	-- local x = size.X / 2
	-- local y = size.Y / 2
	-- local z = (cframe.Position - endPos).Magnitude / 2
	-- local editableMesh = Instance.new("EditableMesh")

	-- -- Vertex setup
	-- local first = editableMesh:AddVertex(Vector3.new(-x, y, -z))
	-- local second = editableMesh:AddVertex(Vector3.new(x, y, -z))
	-- local third = editableMesh:AddVertex(Vector3.new(-x, y, z))
	-- local fourth = editableMesh:AddVertex(Vector3.new(x, y, z))
	-- local fifth = editableMesh:AddVertex(Vector3.new(-x, -y, -z))
	-- local sixth = editableMesh:AddVertex(Vector3.new(x, -y, -z))
	-- local seventh = editableMesh:AddVertex(Vector3.new(-x, -y, z))
	-- local eighth = editableMesh:AddVertex(Vector3.new(x, -y, z))

	-- -- Correct vertex normals
	-- for _, vertex in pairs(editableMesh:GetVertices()) do
	-- 	editableMesh:SetVertexNormal(vertex, editableMesh:GetPosition(vertex).Unit)
	-- end

	-- -- Draw faces
	-- editableMesh:AddTriangle(first, second, third)
	-- editableMesh:AddTriangle(second, fourth, third)

	-- editableMesh:AddTriangle(first, fifth, second)
	-- editableMesh:AddTriangle(second, fifth, sixth)

	-- editableMesh:AddTriangle(second, fourth, sixth)
	-- editableMesh:AddTriangle(sixth, fourth, eighth)

	-- editableMesh:AddTriangle(first, third, fifth)
	-- editableMesh:AddTriangle(third, seventh, fifth)

	-- editableMesh:AddTriangle(third, fourth, seventh)
	-- editableMesh:AddTriangle(fourth, eighth, seventh)

	-- editableMesh:AddTriangle(fifth, seventh, sixth)
	-- editableMesh:AddTriangle(seventh, eighth, sixth)

	-- editableMesh.Parent = meshPart

	return raycastResult
end

function DebugRaycast:Raycast(
	origin: Vector3,
	direction: Vector3,
	raycastParams: RaycastParams?,
	hitColor: Color3?,
	missColor: Color3?,
	thickness: number?,
	transparency: number?,
	duration: number?
): RaycastResult?
	local raycastResult = workspace:Raycast(origin, direction, raycastParams)
	hitColor = hitColor or DEFAULT_HIT_COLOR
	missColor = missColor or DEFAULT_MISS_COLOR
	local color = raycastResult and hitColor or missColor
	local endPos = raycastResult and raycastResult.Position or origin + direction

	CreateDebugPart(
		color,
		CFrame.new(origin:Lerp(endPos, 0.5)) * CFrame.new(origin, endPos).Rotation,
		Vector3.new(thickness, thickness, (origin - endPos).Magnitude),
		Enum.PartType.Block,
		transparency,
		duration
	)

	return raycastResult
end

return DebugRaycast


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

return DeepCopy


local FormatNumber = {}

local SUFFIXES = {
	"K",
	"M",
	"B",
	"T",
	"Q",
	"QI",
	"SX",
	"SP",
	"OC",
	"NO",
	"DC",
	"UD",
	"DD",
	"TD",
	"QAD",
	"QID",
	"SXD",
	"SPD",
	"OCD",
	"NOD",
	"VG",
	"UVG",
	"DVG",
	"TVG",
	"QAVG",
	"QIVG",
	"SXVG",
	"SPVG",
	"OCVG",
}

function FormatNumber.AddCommas(input: number): string
    if not input then
        return ""
    end

	return (
		tostring(math.modf(input)):reverse():gsub("(%d%d%d)", "%1,"):reverse()
		.. (tostring(input):match("%.%d+") or "")
	):gsub("^,", "")
end

function FormatNumber.Shorten(input: number): string
    if not input then
        return ""
    end

	if input == 0 then
		return "0"
	end

	local suffixIndex = math.min(math.floor(math.log(input, 1000)), #SUFFIXES)
	local v = math.pow(10, suffixIndex * 3)

	return ("%.1f"):format(input / v):gsub("%.?0+$", "") .. (SUFFIXES[suffixIndex] or "")
end

return FormatNumber


--[[
	Author: ChiefWildin

	A utility module that provides functions that make it easier to get multiple
	objects in one call.
]]

--[=[
	Takes an `Instance` value followed by any number of strings that represent
	child `Instance` names and returns the final child. If the last argument is
	a table, it will return a tuple of the children specified by that table.

	```lua
	--- Single instance
	local keystone = Get(workspace, "Arch", "Keystone")
	--- Multiple children
	local keystone, door = Get(workspace, "Arch", { "Keystone", "Door" })
	```

	@class Get
]=]

--[=[
	@function Get
	@within Get
	@param initialObject Instance -- The object to start navigating from.
	@param ... string | {string} -- The names of the children to navigate through.
	@return ...Instance -- A tuple of all the requested children at the final location.
]=]
return function(initialObject: Instance, ...): ...Instance
	local object = initialObject
	local args: { string | { string } } = { ... }

	if typeof(args[#args]) == "table" then
		local namesToFind: { string } = table.remove(args, #args)
		local foundObjects: { Instance } = table.create(#namesToFind)

		-- Traverse to the target object
		for _, childName in ipairs(args) do
			object = object:WaitForChild(childName)
		end

		-- Wait for specified children
		for _, childName in ipairs(namesToFind) do
			table.insert(foundObjects, object:WaitForChild(childName))
		end

		return table.unpack(foundObjects)
	else
		-- Traverse to the target object
		for _, childName in ipairs(args) do
			object = object:WaitForChild(childName)
		end

		return object
	end
end


local AssetService = game:GetService("AssetService")

export type Branch = {
	DisplayName: string,
	Name: string,
	PlaceId: number,
}

local Branches: { Branch }

--[=[
	@class GetBranches

	Return an array of places in this game using GetGamePlacesAsync.

	```lua
	--	Print all branches in this game.
	local branches = GetBranches()
	for index, branch in branches do
		print(`Branch "{branch.Name}" PlaceId: {branch.PlaceId}`)
	end
	```
]=]
local function GetBranches(): { Branch }
	if Branches then
		return Branches -- This function ran already.
	end

	local allPlaces: { Branch } = {}
	local placePages: StandardPages = AssetService:GetGamePlacesAsync()

	while true do
		for _, placeInfo in placePages:GetCurrentPage() do
			table.insert(allPlaces, placeInfo)
		end
		if placePages.IsFinished then
			break
		end
		placePages:AdvanceToNextPageAsync()
	end

	for _, placeInfo in allPlaces do
		--	Removes non-alphnumeric characters
		placeInfo.DisplayName = placeInfo.Name
		placeInfo.Name = string.gsub(placeInfo.Name, "[^%w]", "")
	end

	Branches = allPlaces

	return Branches
end

return GetBranches


--[[ File Info

	Author: ChiefWildin
	Module: GetRemote.lua
	Version: 1.5.0

]]

--[=[
	@class GetRemote

	Provides a simple interface for creating and using remotes. The [Remote](/api/Remote)
	object is a wrapper that will dynamically create `RemoteEvents`,
	`UnreliableRemoteEvents`, and `RemoteFunctions` based on how it is used,
	combining the interface of all three into one for ease of use.

	```lua
	local GetRemote = shared("GetRemote") ---@module GetRemote

	local TestRemote = GetRemote("TestRemote")

	TestRemote:OnEvent(function(player, ...)
		print("TestRemote fired by " .. player.Name .. ", got:", ...)
	end)

	TestRemote:OnInvoke(function()
		return "Working"
	end)

	TestRemote:Fire("Fired to all clients")

	local player = Players:GetPlayers()[1]
	print("Client invoke test returned:", TestRemote:Invoke(player))
	TestRemote:FireClient(player, "Fired to " .. player.Name)
	```
]=]

-- Services

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

-- Constants

local FOLDER_NAME = "Remotes"
local IS_SERVER = RunService:IsServer()
local IS_CLIENT = RunService:IsClient()
local PRINT_CLIENT_EVENTS = false
local PRINT_CLIENT_INVOKES = false
local PRINT_SERVER_EVENTS = false
local PRINT_SERVER_INVOKES = false

-- Global Variables

local AllRemotes = {}
local RemoteFolder: Folder

-- Objects

local RequestFunction: RemoteFunction

-- Types

export type Remote = {
	OnEvent: (self: Remote, callback: (...any) -> (), rateLimitInterval: number?) -> (),
	OnUnreliableEvent: (self: Remote, callback: (...any) -> (), rateLimitInterval: number?) -> (),
	OnClientEvent: (self: Remote, callback: (...any) -> (), rateLimitInterval: number?) -> (),
	OnClientUnreliableEvent: (self: Remote, callback: (...any) -> (), rateLimitInterval: number?) -> (),
	OnServerEvent: (self: Remote, callback: (...any) -> (), rateLimitInterval: number?) -> (),
	OnServerUnreliableEvent: (self: Remote, callback: (...any) -> (), rateLimitInterval: number?) -> (),
	OnInvoke: (self: Remote, callback: (player: Player, ...any) -> any) -> (),
	FireClient: (self: Remote, player: Player, ...any) -> (),
	FireClientUnreliable: (self: Remote, player: Player, ...any) -> (),
	FireClientList: (self: Remote, players: { Player }, ...any) -> (),
	FireClientListUnreliable: (self: Remote, players: { Player }, ...any) -> (),
	FireAllClients: (self: Remote, ...any) -> (),
	FireAllClientsUnreliable: (self: Remote, ...any) -> (),
	FireAllExcept: (self: Remote, excluded: Player | { Player }, ...any) -> (),
	FireAllExceptUnreliable: (self: Remote, excluded: Player | { Player }, ...any) -> (),
	FireServer: (self: Remote, ...any) -> (),
	FireServerUnreliable: (self: Remote, ...any) -> (),
	Fire: (self: Remote, ...any) -> (),
	FireUnreliable: (self: Remote, ...any) -> (),
	InvokeClient: (self: Remote, player: Player, ...any) -> any,
	InvokeServer: (self: Remote, ...any) -> any,
	Invoke: (self: Remote, ...any) -> (),
}

export type RemoteForceLoad = { Event: boolean, UnreliableEvent: boolean, Function: boolean }

--[=[
	@class Remote

	A wrapper around `RemoteEvent`, `UnreliableRemoteEvent`, and `RemoteFunction`
	that combines their interfaces into a single object. Instances are created
	and retrieved via [GetRemote](/api/GetRemote).
]=]

-- Classes

local Remote = {}
Remote.__index = Remote

function Remote.new(name: string, forceLoad: RemoteForceLoad?): Remote
	if AllRemotes[name] then
		return AllRemotes[name]
	end

	local self = setmetatable({}, Remote)

	self._name = name
	self._callbackProcessTimes = {}
	self._callbackArgCache = {}
	self._event = nil :: RemoteEvent?
	self._unreliableEvent = nil :: UnreliableRemoteEvent?
	self._function = nil :: RemoteFunction?

	AllRemotes[name] = self

	if IS_SERVER then
		local function registerPlayerArgs(player: Player)
			self._callbackArgCache[player] = {}
			self._callbackProcessTimes[player] = {}
		end
		Players.PlayerAdded:Connect(registerPlayerArgs)
		for _, player in pairs(Players:GetPlayers()) do
			registerPlayerArgs(player)
		end
		Players.PlayerRemoving:Connect(function(player: Player)
			self._callbackArgCache[player] = nil
			self._callbackProcessTimes[player] = nil
		end)
	end

	if forceLoad and RunService:IsRunning() then
		if forceLoad.Event then
			if IS_SERVER then
				self:_getEventServer()
			else
				self:_getEventClient()
			end
		end

		if forceLoad.UnreliableEvent then
			if IS_SERVER then
				self:_getUnreliableEventServer()
			else
				self:_getUnreliableEventClient()
			end
		end

		if forceLoad.Function then
			if IS_SERVER then
				self:_getFunctionServer()
			else
				self:_getFunctionClient()
			end
		end
	end

	return self
end

function Remote:_getEventServer(): RemoteEvent
	if not self._event then
		self._event = Instance.new("RemoteEvent")
		self._event.Name = self._name .. "Event"
		self._event.Parent = RemoteFolder

		if PRINT_SERVER_EVENTS then
			self._event.OnServerEvent:Connect(function(player, ...)
				print(`[{self._name}] [{player.Name}]`, ...)
			end)
		end
	end

	return self._event
end

function Remote:_getUnreliableEventServer(): UnreliableRemoteEvent
	if not self._unreliableEvent then
		self._unreliableEvent = Instance.new("UnreliableRemoteEvent")
		self._unreliableEvent.Name = self._name .. "UnreliableEvent"
		self._unreliableEvent.Parent = RemoteFolder
	end

	return self._unreliableEvent
end

function Remote:_getEventClient(): RemoteEvent
	local shouldConnectDebug = false
	if PRINT_CLIENT_EVENTS and not self._connectedClientEventsDebug then
		self._connectedClientEventsDebug = true
		shouldConnectDebug = true
	end

	if not self._event and RunService:IsRunning() then
		-- Make sure assets have been retrieved from server
		if not game:IsLoaded() then
			game.Loaded:Wait()
		end

		-- Get event if it exists
		self._event = RemoteFolder:FindFirstChild(self._name .. "Event")

		-- Fall back on server if not found
		if not self._event then
			self._event = RequestFunction:InvokeServer(self._name, "Event")
		end
	end

	if not self._event then
		error(`Client failed to get remote '{self._name}', check that it is being created on the server.`)
	end

	if shouldConnectDebug then
		self._event.OnClientEvent:Connect(function(...)
			print(`[{self._name}]`, ...)
		end)
	end

	return self._event
end

function Remote:_getUnreliableEventClient(): UnreliableRemoteEvent
	if not self._unreliableEvent then
		-- Make sure assets have been retrieved from server
		if not game:IsLoaded() then
			game.Loaded:Wait()
		end

		-- Get event if it exists
		self._unreliableEvent = RemoteFolder:FindFirstChild(self._name .. "UnreliableEvent")

		-- Fall back on server if not found
		if not self._unreliableEvent then
			self._unreliableEvent = RequestFunction:InvokeServer(self._name, "UnreliableEvent")
		end
	end

	return self._unreliableEvent
end

function Remote:_getFunctionServer(): RemoteFunction
	if not self._function then
		self._function = Instance.new("RemoteFunction")
		self._function.Name = self._name .. "Function"
		self._function.Parent = RemoteFolder
	end

	return self._function
end

function Remote:_getFunctionClient(): RemoteFunction
	if not self._function then
		-- Make sure assets have been retrieved from server
		if not game:IsLoaded() then
			game.Loaded:Wait()
		end

		-- Get function if it exists
		self._function = RemoteFolder:FindFirstChild(self._name .. "Function")

		-- Fall back on server if not found
		if not self._function then
			self._function = RequestFunction:InvokeServer(self._name, "Function")
		end
	end

	return self._function
end

function Remote:_connectRateLimitedCallback(
	event: RBXScriptSignal,
	callback: (...any) -> (),
	rateLimitInterval: number
): RBXScriptConnection
	if typeof(rateLimitInterval) ~= "number" then
		warn(
			"Invalid rate limit interval provided to OnUnreliableEvent for remote "
				.. self._name
				.. " - expected number, got "
				.. typeof(rateLimitInterval)
				.. "\n"
				.. debug.traceback()
		)
		return event:Connect(callback)
	end

	return event:Connect(function(...)
		local args = { ... }
		local player = args[1]

		local processTimes = IS_SERVER and self._callbackProcessTimes[player] or self._callbackProcessTimes
		local argCache = IS_SERVER and self._callbackArgCache[player] or self._callbackArgCache

		local lastProcessedTime = processTimes[callback] or 0
		local timeSinceLastProcessed = os.clock() - lastProcessedTime

		if timeSinceLastProcessed >= rateLimitInterval then
			processTimes[callback] = os.clock()
			callback(...)
		else
			local needsCall = argCache[callback] == nil
			argCache[callback] = args
			if needsCall then
				task.delay(rateLimitInterval - timeSinceLastProcessed, function()
					local cachedArgs = argCache[callback]
					if cachedArgs then
						processTimes[callback] = os.clock()
						argCache[callback] = nil
						callback(table.unpack(cachedArgs))
					end
				end)
			end
		end
	end)
end

--[=[
	Connects the provided callback to the remote's event. Context is determined
	automatically. If a `rateLimitInterval` is provided, events fired within
	that duration will be queued and only the latest arguments processed. This
	can prevent remote spam, but may cause events to be dropped if set too high.

	`OnServerEvent` and `OnClientEvent` are aliases for this method.

	@param callback function -- The callback to invoke when the event fires
	@param rateLimitInterval number? -- If provided, rate-limits the callback to this interval in seconds
	@return RBXScriptConnection
]=]
function Remote:OnEvent(callback: (...any) -> (), rateLimitInterval: number?)
	task.spawn(function()
		if not RunService:IsRunning() then
			return
		end

		local event = if IS_SERVER then self:_getEventServer().OnServerEvent else self:_getEventClient().OnClientEvent

		return if rateLimitInterval
			then self:_connectRateLimitedCallback(event, callback, rateLimitInterval)
			else event:Connect(callback)
	end)
end

Remote.OnServerEvent = Remote.OnEvent
Remote.OnClientEvent = Remote.OnEvent

--[=[
	Connects the provided callback to the remote's unreliable event. Context is
	determined automatically. If a `rateLimitInterval` is provided, events fired
	within that duration will be queued and only the latest arguments processed.
	This can prevent remote spam, but may cause events to be dropped if set too
	high.

	`OnServerUnreliableEvent` and `OnClientUnreliableEvent` are aliases for this method.

	@param callback function -- The callback to invoke when the event fires
	@param rateLimitInterval number? -- If provided, rate-limits the callback to this interval in seconds
	@return RBXScriptConnection
]=]
function Remote:OnUnreliableEvent(callback: (...any) -> (), rateLimitInterval: number?)
	task.spawn(function()
		local event = if IS_SERVER
			then self:_getUnreliableEventServer().OnServerEvent
			else self:_getUnreliableEventClient().OnClientEvent

		return if rateLimitInterval
			then self:_connectRateLimitedCallback(event, callback, rateLimitInterval)
			else event:Connect(callback)
	end)
end

Remote.OnServerUnreliableEvent = Remote.OnUnreliableEvent
Remote.OnClientUnreliableEvent = Remote.OnUnreliableEvent

--[=[
	Fires the remote's event to the given player. Identical to `RemoteEvent:FireClient()`.

	@server
	@param player Player -- The player to fire the event to
	@param ... any -- The arguments to pass to the client
]=]
function Remote:FireClient(player: Player, ...)
	assert(IS_SERVER, "FireClient can only be called on the server")

	if not (typeof(player) == "Instance" and player:IsA("Player")) then
		return
	end

	self:_getEventServer():FireClient(player, ...)
end

--[=[
	Fires the remote's event to the given player using an unreliable connection.
	Identical to `UnreliableRemoteEvent:FireClient()`.

	Also accessible as `FireClientFast`.

	@server
	@param player Player -- The player to fire the event to
	@param ... any -- The arguments to pass to the client
]=]
function Remote:FireClientUnreliable(player: Player, ...)
	assert(IS_SERVER, "FireClientUnreliable can only be called on the server")

	if not (typeof(player) == "Instance" and player:IsA("Player")) then
		return
	end

	self:_getUnreliableEventServer():FireClient(player, ...)
end

Remote.FireClientFast = Remote.FireClientUnreliable

--[=[
	Fires the remote's event to each player in the provided list.

	@server
	@param playerList {Player} -- The list of players to fire the event to
	@param ... any -- The arguments to pass to the clients
]=]
function Remote:FireClientList(playerList: { Player }, ...)
	if typeof(playerList) ~= "table" then
		warn(`Attempt to fire Remote to non-table list ({tostring(playerList)})\n{debug.traceback()}`)
		return
	end

	for _, player in pairs(playerList) do
		if player:IsA("Player") then
			self:FireClient(player, ...)
		else
			warn(`Attempt to fire Remote to non-Player in list ({tostring(player)})\n{debug.traceback()}`)
		end
	end
end

--[=[
	Fires the remote's event to each player in the provided list using an
	unreliable connection.

	Also accessible as `FireClientListFast`.

	@server
	@param playerList {Player} -- The list of players to fire the event to
	@param ... any -- The arguments to pass to the clients
]=]
function Remote:FireClientListUnreliable(playerList: { Player }, ...)
	if typeof(playerList) ~= "table" then
		warn(`Attempt to fire Remote to non-table list ({tostring(playerList)})\n{debug.traceback()}`)
		return
	end

	for _, player in pairs(playerList) do
		if player:IsA("Player") then
			self:FireClientUnreliable(player, ...)
		else
			warn(`Attempt to fire Remote to non-Player in list ({tostring(player)})\n{debug.traceback()}`)
		end
	end
end

Remote.FireClientListFast = Remote.FireClientListUnreliable

--[=[
	Fires the remote's event to all players. Identical to `RemoteEvent:FireAllClients()`.

	@server
	@param ... any -- The arguments to pass to the clients
]=]
function Remote:FireAllClients(...)
	assert(IS_SERVER, "FireAllClients can only be called on the server")
	self:_getEventServer():FireAllClients(...)
end

--[=[
	Fires the remote's event to all players using an unreliable connection.
	Identical to `UnreliableRemoteEvent:FireAllClients()`.

	Also accessible as `FireAllClientsFast`.

	@server
	@param ... any -- The arguments to pass to the clients
]=]
function Remote:FireAllClientsUnreliable(...)
	assert(IS_SERVER, "FireAllClientsUnreliable can only be called on the server")
	self:_getUnreliableEventServer():FireAllClients(...)
end

Remote.FireAllClientsFast = Remote.FireAllClientsUnreliable

--[=[
	Fires the remote's event to all players except the given player or list of
	players.

	Also accessible as `FireAllClientsExcept`.

	@server
	@param excluded Player | {Player} -- The player(s) to exclude
	@param ... any -- The arguments to pass to the clients
]=]
function Remote:FireAllExcept(excluded: Player | { Player }, ...)
	if typeof(excluded) == "Instance" and excluded:IsA("Player") then
		excluded = { excluded }
	else
		warn(`Attempt to exclude non-Player from FireAllExcept ({tostring(excluded)})\n{debug.traceback()}`)
		return
	end

	for _, player in pairs(Players:GetPlayers()) do
		if not table.find(excluded, player) then
			self:FireClient(player, ...)
		end
	end
end

Remote.FireAllClientsExcept = Remote.FireAllExcept

--[=[
	Fires the remote's event to all players except the given player or list of
	players using an unreliable connection.

	Also accessible as `FireAllExceptFast`, `FireAllClientsExceptUnreliable`, and `FireAllClientsExceptFast`.

	@server
	@param excluded Player | {Player} -- The player(s) to exclude
	@param ... any -- The arguments to pass to the clients
]=]
function Remote:FireAllExceptUnreliable(excluded: Player | { Player }, ...)
	if typeof(excluded) == "Instance" and excluded:IsA("Player") then
		excluded = { excluded }
	else
		warn(`Attempt to exclude non-Player from FireAllExcept ({tostring(excluded)})\n{debug.traceback()}`)
		return
	end

	for _, player in pairs(Players:GetPlayers()) do
		if not table.find(excluded, player) then
			self:FireClientUnreliable(player, ...)
		end
	end
end

Remote.FireAllExceptFast = Remote.FireAllExceptUnreliable
Remote.FireAllClientsExceptUnreliable = Remote.FireAllExceptUnreliable
Remote.FireAllClientsExceptFast = Remote.FireAllExceptUnreliable

--[=[
	Fires the remote's event to the server. Identical to `RemoteEvent:FireServer()`.

	@client
	@param ... any -- The arguments to pass to the server
]=]
function Remote:FireServer(...)
	assert(IS_CLIENT, "FireServer can only be called on the client")
	if not RunService:IsRunning() then
		return
	end

	self:_getEventClient():FireServer(...)
end

--[=[
	Fires the remote's event to the server using an unreliable connection.
	Identical to `UnreliableRemoteEvent:FireServer()`.

	Also accessible as `FireServerFast`.

	@client
	@param ... any -- The arguments to pass to the server
]=]
function Remote:FireServerUnreliable(...)
	assert(IS_CLIENT, "FireServerUnreliable can only be called on the client")
	self:_getUnreliableEventClient():FireServer(...)
end

Remote.FireServerFast = Remote.FireServerUnreliable

--[=[
	Fires the remote's event, automatically routing by context: uses
	`:FireServer()` on the client, or `:FireAllClients()` on the server.

	@param ... any -- The arguments to pass
]=]
function Remote:Fire(...)
	if not RunService:IsRunning() then
		return
	elseif IS_SERVER then
		self:_getEventServer():FireAllClients(...)
	else
		self:_getEventClient():FireServer(...)
	end
end

--[=[
	Fires the remote's event using an unreliable connection, automatically
	routing by context: uses `:FireServerUnreliable()` on the client, or
	`:FireAllClientsUnreliable()` on the server.

	Also accessible as `FireFast`.

	@param ... any -- The arguments to pass
]=]
function Remote:FireUnreliable(...)
	if not RunService:IsRunning() then
		return
	elseif IS_SERVER then
		self:_getUnreliableEventServer():FireAllClients(...)
	else
		self:_getUnreliableEventClient():FireServer(...)
	end
end

Remote.FireFast = Remote.FireUnreliable

--[=[
	Invokes the remote's function on the given player. Identical to
	`RemoteFunction:InvokeClient()`.

	@server
	@param player Player -- The player to invoke the function on
	@param ... any -- The arguments to pass
	@return any
	@yields
]=]
function Remote:InvokeClient(player: Player, ...): any
	assert(IS_SERVER, "InvokeClient can only be called on the server")
	return self:_getFunctionServer():InvokeClient(player, ...)
end

--[=[
	Invokes the remote's function on the server. Identical to
	`RemoteFunction:InvokeServer()`.

	@client
	@param ... any -- The arguments to pass
	@return any
	@yields
]=]
function Remote:InvokeServer(...): any
	assert(IS_CLIENT, "InvokeServer can only be called on the client")
	return self:_getFunctionClient():InvokeServer(...)
end

--[=[
	Invokes the remote's function, automatically routing by context: uses
	`:InvokeServer()` on the client, or `:InvokeClient()` on the server. When
	calling from the server, the first argument should be the target `Player`.

	@param ... any -- The arguments to pass (first arg is the target Player when called from server)
	@return any
	@yields
]=]
function Remote:Invoke(...): any
	if IS_SERVER then
		local args = { ... }
		local player = table.remove(args, 1)
		return self:_getFunctionServer():InvokeClient(player, table.unpack(args))
	else
		return self:_getFunctionClient():InvokeServer(...)
	end
end

--[=[
	Sets the callback for the remote's function. Equivalent to setting
	`RemoteFunction.OnServerInvoke` or `RemoteFunction.OnClientInvoke`.
	Context is determined automatically.

	`OnServerInvoke` and `OnClientInvoke` are aliases for this method.

	@param callback function -- The callback to invoke when the function is called
]=]
function Remote:OnInvoke(callback: (any) -> ()?)
	local function callbackWithPrint(...)
		if not callback then
			return
		end

		local results = { callback(...) }

		if IS_SERVER then
			local args = { ... }
			local player = table.remove(args, 1)
			print(`[{self._name}] [{player.Name}]`, table.unpack(args), "->", table.unpack(results))
		elseif IS_CLIENT then
			print(`[{self._name}]`, ..., "->", table.unpack(results))
		end

		return table.unpack(results)
	end

	task.spawn(function()
		if IS_SERVER then
			self:_getFunctionServer().OnServerInvoke = if PRINT_SERVER_INVOKES then callbackWithPrint else callback
		else
			self:_getFunctionClient().OnClientInvoke = if PRINT_CLIENT_INVOKES then callbackWithPrint else callback
		end
	end)
end

Remote.OnServerInvoke = Remote.OnInvoke
Remote.OnClientInvoke = Remote.OnInvoke

--[=[
	@function GetRemote
	@within GetRemote

	Returns a [Remote](/api/Remote) object for the given name, creating it if it does not
	already exist. The same object is returned for repeated calls with the same
	name.

	@param name string -- The name of the remote
	@param forceLoad RemoteForceLoad? -- Optionally pre-create specific underlying remote instances
	@return Remote
]=]

if not RunService:IsRunning() then
	return function(name: string, forceLoad: RemoteForceLoad?): Remote
		return Remote.new("Mock" .. name, forceLoad)
	end
elseif IS_SERVER then
	return function(name: string, forceLoad: RemoteForceLoad?): Remote
		assert(type(name) == "string", "Invalid name '" .. tostring(name) .. "' - remote name must be a string")

		if not RemoteFolder then
			RemoteFolder = Instance.new("Folder")
			RemoteFolder.Name = FOLDER_NAME
			RemoteFolder.Archivable = false
			RemoteFolder.Parent = ReplicatedStorage

			RequestFunction = Instance.new("RemoteFunction")
			RequestFunction.Name = "Request"
			RequestFunction.Parent = RemoteFolder

			RequestFunction.OnServerInvoke = function(_, remoteName, remoteType)
				local remote = AllRemotes[remoteName]
				if remote then
					if remoteType == "Event" then
						return remote:_getEventServer()
					elseif remoteType == "UnreliableEvent" then
						return remote:_getUnreliableEventServer()
					elseif remoteType == "Function" then
						return remote:_getFunctionServer()
					end
				end
			end
		end

		return Remote.new(name, forceLoad)
	end
else -- IS_CLIENT
	return function(name: string, forceLoad: RemoteForceLoad?): Remote
		assert(type(name) == "string", "Invalid name '" .. tostring(name) .. "' - remote name must be a string")

		if not RemoteFolder then
			RemoteFolder = ReplicatedStorage:WaitForChild(FOLDER_NAME)
		end

		if not RequestFunction then
			RequestFunction = RemoteFolder:WaitForChild("Request")
		end

		return Remote.new(name, forceLoad)
	end
end


--[[ File Info

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

]]

--[=[
	@class Observe

	Provides utility functions for observing instance properties, attributes,
	and children, immediately invoking callbacks with the current value and
	then again on every subsequent change.
]=]

local Observe = {}

-- Public Functions

--[=[
	Observes the given property on an instance, immediately calling `callback`
	with the current value and again whenever the property changes.

	@param instance Instance -- The instance to observe
	@param property string -- The name of the property to observe
	@param callback function -- Called with the current value immediately and on every change
	@return RBXScriptConnection
]=]
function Observe.Property(instance: Instance, property: string, callback: (any) -> ()): RBXScriptConnection
	task.spawn(callback, instance[property])
	return instance:GetPropertyChangedSignal(property):Connect(function()
		task.spawn(callback, instance[property])
	end)
end

--[=[
	Observes the given attribute on an instance, immediately calling `callback`
	with the current value and again whenever the attribute changes.

	@param instance Instance -- The instance to observe
	@param attribute string -- The name of the attribute to observe
	@param callback function -- Called with the current value immediately and on every change
	@return RBXScriptConnection
]=]
function Observe.Attribute(instance: Instance, attribute: string, callback: (any) -> ()): RBXScriptConnection
	task.spawn(callback, instance:GetAttribute(attribute))
	return instance:GetAttributeChangedSignal(attribute):Connect(function()
		task.spawn(callback, instance:GetAttribute(attribute))
	end)
end

--[=[
	Observes the children of an instance, immediately calling `callback` with
	the current children and again whenever a child is added or removed.

	Returns a connection-like table with `Disconnect()` and `Connected` members,
	compatible with standard connection cleanup patterns.

	@param instance Instance -- The instance to observe
	@param callback function -- Called with the full children list immediately and on every change
	@return RBXScriptConnection
]=]
function Observe.Children(instance: Instance, callback: (children: { Instance }) -> ()): RBXScriptConnection
	local children = instance:GetChildren()
	task.spawn(callback, children)

	local addedConnection = instance.ChildAdded:Connect(function()
		callback(instance:GetChildren())
	end)
	local removedConnection = instance.ChildRemoved:Connect(function()
		callback(instance:GetChildren())
	end)

	local connection
	connection = {
		Disconnect = function()
			connection.Connected = false
			addedConnection:Disconnect()
			removedConnection:Disconnect()
		end,
		Connected = true,
	}

	return connection
end

return Observe


local ProfileTemplate = {
	AwardedBadges = {},
	PendingBadges = {},
}

-- Prevents accidental modification of the template
local function freezeData(data: {}): ()
	table.freeze(data)

	for _, value: any in pairs(data) do
		if typeof(value) == "table" then
			freezeData(value)
		end
	end
end
freezeData(ProfileTemplate)

return ProfileTemplate


--[[ File Info

	Author(s): ChiefWildin
	Module: ProcessQueue.luau
	Version: 1.1.0

]]

--[=[
	@class ProcessQueue

	Enables processing of a queue of items with an optional delay between each
	item.
]=]
local ProcessQueue = {}
ProcessQueue.__index = ProcessQueue

-- Private Functions

function ProcessQueue:_processQueue()
	if self._isProcessing then
		return
	end

	self._isProcessing = true

	while #self._queue > 0 do
		local item = table.remove(self._queue, 1)

		local success, result = pcall(self._callback, item)

		if not success then
			if self.RetryFailures then
				self:Add(item)
			end

			warn(`Failed to process item ({item}) - {result}`)
		end

		if self._delay > 0 then
			task.wait(self._delay)
		end
	end

	self._isProcessing = false
end

-- Public Functions

--[=[
	Checks if the queue is currently being processed.

	@return boolean
]=]
function ProcessQueue:IsProcessing(): boolean
	return self._isProcessing
end

--[=[
	Adds an item to the queue and begins processing if not already in progress.

	@param item any -- The item to add to the queue.
]=]
function ProcessQueue:Add(item: any)
	table.insert(self._queue, item)
	self:_processQueue()
end

--[=[
	Clears all current items in the queue.
]=]
function ProcessQueue:Clear()
	table.clear(self._queue)
end

--[=[
	Clears the queue and references.
]=]
function ProcessQueue:Destroy()
	table.clear(self._queue)
	self._callback = nil
	setmetatable(self, nil)
end

--[=[
	Creates a new ProcessQueue.

	@param callback (any) -> any -- The function to call for each item in the queue.
	@param delay number? -- Optional delay in seconds between processing each item.
	@return ProcessQueue
]=]
function ProcessQueue.new(callback: (any) -> any, delay: number?)
	local self = setmetatable({}, ProcessQueue)

	self._callback = callback
	self._delay = delay or 0

	self._queue = {}
	self._isProcessing = false

	self.RetryFailures = false

	return self
end

return ProcessQueue


--[[ File Info

    Author: TactBacon, ChiefWildin
    Module: Sounds.lua
    Version: 2.0.1

]]

-- Services

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

-- Dependencies

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

-- Module Declaration

local Sounds = {}

-- Global Variables

local EmittersCache: { [string]: AudioEmitter } = {}
local SoundsCache: { [string]: AudioPlayer } = {}

-- Objects

local SoundContainer = Instance.new("Folder")
if not RunService:IsRunning() then
	SoundContainer:Destroy()
else
	SoundContainer.Name = "SoundContainer"
	SoundContainer.Archivable = false
	SoundContainer.Parent = workspace
end

local SoundInstances = Instance.new("Folder")
if not RunService:IsRunning() then
	SoundInstances:Destroy()
else
	SoundInstances.Name = "SoundInstances" .. (RunService:IsServer() and "Server" or "Client")
	SoundInstances.Archivable = false
	SoundInstances.Parent = SoundService
end

local RandomObj = Random.new()

-- Public Functions

function Sounds:GetEmitter(emitterType: string): AudioEmitter?
	if EmittersCache[emitterType] then
		return EmittersCache[emitterType]
	end

	local emitter = SoundService.Audio.Emitters:FindFirstChild(emitterType, true)
	if emitter and emitter:IsA("AudioEmitter") then
		EmittersCache[emitterType] = emitter
		return emitter
	end

	return nil
end

function Sounds:GetSound(soundName: string): AudioPlayer?
	local sound = SoundsCache[soundName]
	if sound then
		return sound
	end

	SoundsCache[soundName] = SoundService:FindFirstChild(soundName, true)

	return SoundsCache[soundName]
end

function Sounds:PlayExistingSound(sound: AudioPlayer, varyPitch: number?, duration: number?): AudioPlayer
	if varyPitch or duration then
		sound.AutoLoad = true

		local speed = sound:GetAttribute("OriginalPlaybackSpeed")
		if not speed then
			speed = sound.PlaybackSpeed
			sound:SetAttribute("OriginalPlaybackSpeed", speed)
		end

		-- if not sound.IsReady then
		-- 	if not sound:GetAttribute("Warned") then
		-- 		local loadWarning = WarningTimer.new(15, "Sound failed to load - " .. tostring(sound.Asset))
		-- 		while not sound.IsReady and loadWarning:IsAlive() do
		-- 			task.wait(0.1)
		-- 		end
		-- 		loadWarning:Stop()
		-- 	else
		-- 		local start = tick()
		-- 		while not sound.IsReady and tick() - start < 15 do
		-- 			task.wait(0.1)
		-- 		end
		-- 	end
		-- end
		local start = tick()
		while not sound.IsReady and tick() - start < 15 do
			task.wait(0.1)
		end

		-- if it didn't load, don't mess with it
		if not sound.IsReady then
			sound:SetAttribute("Warned", true)
			return sound
		end

		local pitch = varyPitch or 0
		sound.PlaybackSpeed = (sound.TimeLength / (duration or sound.TimeLength)) * speed
			+ RandomObj:NextNumber(-pitch, pitch)
	end

	sound:Stop()
	sound.TimePosition = 0
	sound:Play()

	return sound
end

function Sounds:PlaySound(
	soundName: string,
	shouldClone: boolean?,
	position: (Vector3 | BasePart)?,
	varyPitch: number?,
	duration: number?,
	emitterType: string?
): AudioPlayer?
	if not RunService:IsRunning() then
		return
	end

	local sound = self:GetSound(soundName)
	if not sound then
		warn("Could not find sound: " .. soundName)
		return
	end

	if shouldClone or position then
		local originalSound = sound

		sound = Instance.new("AudioPlayer")
		sound.Name = originalSound.Name
		sound.Asset = originalSound.Asset
		sound.Volume = originalSound.Volume
		sound.PlaybackSpeed = originalSound.PlaybackSpeed
		sound.Looping = originalSound.Looping
		sound:SetAttribute(
			"OriginalPlaybackSpeed",
			originalSound:GetAttribute("OriginalPlaybackSpeed") or sound.PlaybackSpeed
		)

		local wire = Instance.new("Wire")
		wire.SourceInstance = sound
		wire.Parent = sound

		local soundAdornee: Part
		local emitter: AudioEmitter
		if not position then
			local originalWire = originalSound:FindFirstChildWhichIsA("Wire")
			wire.TargetInstance = if originalWire
				then originalWire.TargetInstance
				else SoundService.Audio.Effects.Master_Fader
			sound.Parent = SoundInstances
		else
			if emitterType then
				emitter = self:GetEmitter(emitterType)
				if emitter then
					emitter = emitter:Clone()
				else
					warn(`Emitter type {emitterType} not found for sound {soundName}, using default instead`)
				end
			end
			if not emitter then
				emitter = Instance.new("AudioEmitter")
			end
			emitter.Name = soundName .. "_Emitter"

			wire.TargetInstance = emitter

			if typeof(position) == "Instance" then
				emitter.Parent = position
			else
				soundAdornee = Instance.new("Part")
				soundAdornee.CFrame = CFrame.new(position)
				soundAdornee.CanCollide = false
				soundAdornee.CanTouch = false
				soundAdornee.CanQuery = false
				soundAdornee.AudioCanCollide = false
				soundAdornee.Transparency = 1
				soundAdornee.Anchored = true
				soundAdornee.Size = Vector3.one
				soundAdornee.Parent = SoundContainer
				emitter.Parent = soundAdornee
				sound.Parent = soundAdornee
			end
		end

		if varyPitch or duration then
			task.spawn(function()
				while not sound.IsReady do
					sound.AutoLoad = true
					sound:GetPropertyChangedSignal("IsReady"):Wait()
				end

				local pitch = varyPitch or 0
				sound.PlaybackSpeed = (sound.TimeLength / (duration or sound.TimeLength))
						* sound:GetAttribute("OriginalPlaybackSpeed")
					+ RandomObj:NextNumber(-pitch, pitch)
			end)
		end

		sound.Ended:Connect(function()
			sound:Destroy()
			if emitter then
				emitter:Destroy()
			end
			if soundAdornee then
				soundAdornee:Destroy()
			end
		end)
	end

	sound:Stop()
	sound.TimePosition = 0
	sound:Play()

	return sound
end

return Sounds


return {
	Premium = utf8.char(0xE001),
	Robux = utf8.char(0xE002),
	Verified = utf8.char(0xE000),
}


--[[
    GlabalConfig.lua
    TactBacon
    v1.2.0
]]

export type StateConfig = {
	[string]: {
		["BlockedBy"]: { string }?,
		["Requires"]: { string }?,
		["Deactivates"]: { string }?,
		["Activates"]: { string }?,
		["Tags"]: { string }?,
	},
}

return {
	-- Put states here
} :: StateConfig