--===========================================================================================================================>
--!optimize 2
--!strict
--===========================================================================================================================>
-- [Name]:
-- Zoner

-- [Author]:
-- IISato

-- [Start]:
-- July 12, 2024

-- [Version]:
-- October 10, 2025
-- 0.9.9

-- [Description]:
-- Inspired by the ZonePlus Module by Nanoblox:
-- https://devforum.roblox.com/t/zoneplus-v320-construct-dynamic-zones-and-effectively-determine-players-and-parts-within-their-boundaries/1017701
-- Based on:  Version: 3.2.0

-- This Module acts as new version of the ZoneController Module.
-- Basically a Zone Singleton meant to be required once:
--===========================================================================================================================>

-- Set a Memory Category for this:
debug.setmemorycategory('[Zoner]')

--[=[

	@class Zoner
	@__index Zoner

	**Zoner** is the high-performance zone manager for your experience — a **per-run-context singleton**
	(one on Server, one on Client) that constructs zones, routes zone updates, and scales detection across
	Actors for parallel execution.

	**What it is**
	- A **module-as-table facade**: you require it once and call its exported factories
	  (`New`, `NewFromRegion`, `NewZoneFromBox`) instead of instantiating classes yourself.
	- Internally, it owns a private `ZonerFolder` with:
	  - `Holders/` → a pool of **ZonerActor** (parallel) and **ZonerGroup** (serial) holders.
	  - `Groups/`  → group containers whose attributes mirror group properties and zone IDs.
	- A global **Tracker Registry** via `SharedTableRegistry` for lightweight, cross-script tracking.

	**How it works**
	- Creating a zone picks a holder type from `ZoneConfig.Execution` (Parallel → Actor, Serial → Group),
	  provisions/loads holders as needed, and balances **capacity** automatically. Each holder’s `Ids`
	  attributes mirror live zone membership, while an internal cache maps identifiers → zone objects for O(1) access.
	- Updates propagate through attribute changes and bindables (`Zone_Update`, `TrackerId_*`) without yielding.

	**Detection & Bounds**
	- Build zones from parts/models/folders **or** from **partless boxes** (`CFrame` + `Size`).
	- Tunable `DetectionCoverage`, `DetectionMethod`, `DetectionMode`, `Simulation`, `Bounds`, and `Rate`
	  with smart defaults. When `DetectionMethod.Automatic`, Zoner selects an efficient strategy; complex
	  modes create internal parts only when required.

	**VM / Actor model**
	- Each **ZonerActor** runs the engine script for the current run context and processes a slice of zones
	  **in parallel**; capacities **auto-scale** with configured thread limits.
	- The `Groups/` container is designed to be **registered in each VM** (per Actor) so group attributes
	  and zone IDs stay **automatically synchronized** across Zoner Actor VMs, making group properties easy
	  to read anywhere they are required.

	**Public surface**
	- `New`, `NewFromRegion`, `NewZoneFromBox` — zone factories returning `Zone` objects.
	- `CreateGroup` — registers a group; its properties and membership are exposed via attributes for all VMs.
	- `RelocateZonerFolder` — safely re-parents the internal folder (server-side).
	- `Enum` — public enums used by configs.
	- `ZONER_COLLISION_GROUP` — collision group name used for internal ZoneParts.

	**Lifecycle**
	- Zoner is created at require-time; call the factory functions directly.
	- Zones can be destroyed individually; if this script is removed, Zoner cleans itself up.

	Example:
	```lua
	--!strict

	-- Require the Zoner Module:
	local Zoner = require(script.Parent.Vendor.Zoner)

	-- Reference a Part Box BasePart in Workspace to use as a Zone reference:
	-- (If enabled, Zoner will automatically update as the ZonePart moves or changes size)
	local ZoneBox: BasePart = workspace.Box

	-- Define a ZoneConfig table:
	-- Any of the following can be left blank, you dont even need to use one:
	local ZoneConfig: Zoner.ZoneConfig = {
		DetectionCoverage = Zoner.Enum.DetectionCoverage.Center;
		DetectionMethod   = Zoner.Enum.DetectionMethod.Efficient;
		DetectionMode     = Zoner.Enum.DetectionMode.Point;

		Simulation = Zoner.Enum.Simulation.PostSimulation;
		Bounds     = Zoner.Enum.Bounds.BoxExact;
		Execution  = Zoner.Enum.Execution.Parallel;
		Rate       = Zoner.Enum.Rate.Immediate;

		NoZonePartPropertyListening = false;
		NoZonePartAddedListening    = false;
		ManualStepping              = false;
	}

	-- Create the Zone Object using the ZoneBox and ZoneConfig:
	local Zone_From_Part: Zoner.Zone = Zoner.New(ZoneBox, ZoneConfig)

	-- Connect to the PlayerEntered Signal:
	-- Returns connection so you can disconnect if youd like:
	local PlayerEntered_Connection =
		Zone_From_Part.PlayerEntered:Connect(function(Player: Player)
			print(`{Player.Name} Entered Zone`)
		end)

	-- Connect to the PlayerExited Signal:
	-- Returns connection so you can disconnect if youd like:
	local PlayerExited_Connection =
		Zone_From_Part.PlayerExited:Connect(function(Player: Player)
			print(`{Player.Name} Exited Zone`)
		end)

	```

]=]
local Zoner = {}; Zoner.__index = Zoner

--===========================================================================================================================>
--[ DEPENDENCIES: ]

-- Import Service:
local SharedTableRegistry = game:GetService('SharedTableRegistry')

local Constants  = {
	Logs = require(`@self/Modules/Internal/Data/Constants/Logs`);
};

local MiscHelpers = require(`@self/Modules/Internal/Utility/MiscHelpers`);

local Plagger     = require(`@self/Modules/Internal/Classes/Plagger`);
local Groups      = require(`@self/Modules/Internal/Classes/Groups`);
local Zone        = require(`@self/Modules/Internal/Classes/Zone`);

local DetectionCoverage = require('@self/Modules/Internal/Data/Enums/DetectionCoverage')
local DetectionMethod   = require('@self/Modules/Internal/Data/Enums/DetectionMethod')
local DetectionMode     = require('@self/Modules/Internal/Data/Enums/DetectionMode')
local Simulation        = require('@self/Modules/Internal/Data/Enums/Simulation')
local Execution         = require('@self/Modules/Internal/Data/Enums/Execution')
local Bounds            = require('@self/Modules/Internal/Data/Enums/Bounds')
local Rate              = require('@self/Modules/Internal/Data/Enums/Rate')

--===========================================================================================================================>
--[ CONSTANTS: ]

--[=[
	@prop Enum Enums
	@within Zoner
	@readonly
	@tag collision_groups

	A reference to all of the Enums used in Zoner for easy access when configuring Zones.

	Accessed: `Zoner.Enum`
]=]
local ENUMS = {
	DetectionCoverage = DetectionCoverage.ENUMS;
	DetectionMethod   = DetectionMethod.ENUMS;
	DetectionMode     = DetectionMode.ENUMS;
	Simulation        = Simulation.ENUMS;
	Execution         = Execution.ENUMS;
	Bounds            = Bounds.ENUMS;
	Rate              = Rate.ENUMS;
}

-- The RunContext/Scope of the Module:
local RUN_CONTEXT: 'Client'|'Server' = if game:GetService('RunService'):IsServer() then 'Server' else 'Client'

--[=[
	@prop ZONER_COLLISION_GROUP string
	@within Zoner
	@readonly
	@tag collision_groups

	The collision group string used by Zoner internally for ZoneParts

	Accessed: `Zoner.ZONER_COLLISION_GROUP`
]=]

local ZONER_COLLISION_GROUP: 'Zoner:ZonePart' = 'Zoner:ZonePart'

-- Default capacity limits for ZonerActors and ZonerGroups
local DEF_MAX_ACTOR_CAPACITY: number = 5;
local DEF_MAX_GROUP_CAPACITY: number = 10;
local DEF_MAX_THREADS:        number = 7;

-- Create a default Configs table constant:
local DEFAULT_ZONE_CONFIG = table.freeze({
	DetectionCoverage = ENUMS.DetectionCoverage.Center,
	DetectionMethod   = ENUMS.DetectionMethod.Automatic,
	DetectionMode     = ENUMS.DetectionMode.Point,
	Simulation        = ENUMS.Simulation.PostSimulation;
	Execution         = ENUMS.Execution.Parallel;
	Bounds            = ENUMS.Bounds.Automatic;
	Rate              = ENUMS.Rate.Fast;

	NoZonePartPropertyListening = false;
	NoZonePartAddedListening    = false;
	ManualStepping              = false;
});

--===========================================================================================================================>
--[ TYPES: ]

-- [ Require Types ]

-- This will inject all types into this context.
local ZonePrimitives = require('@self/Modules/Internal/Types/ZonePrimitives')
local ZoneInstances  = require('@self/Modules/Internal/Types/ZoneInstances')
local ZoneConfig     = require('@self/Modules/Internal/Types/ZoneConfig')
local ZonePublic     = require('@self/Modules/Internal/Types/ZonePublic')

-- [ Register Types ]

-- Zone Holder Type Id string type:
type ZonerHolderTypeId = 'G' | 'A'

type ZoneCollector = {
	--====================================================>
	_Id: string;
	_Zones: {[string]: boolean};
	_Zoner: Zoner;
	--====================================================>
	Add:
		(self: ZoneCollector, Zone: Zone) -> ();
	Remove:
		(self: ZoneCollector, Zone: Zone, DestroyZone: boolean?) -> ();
	Clear:
		(self: ZoneCollector, DestroyZones: boolean?) -> ();
	Destroy:
		(self: ZoneCollector, Zone: Zone) -> ();
	--====================================================>
}

export type Zone<MetaData = {[any]: any}?> = ZonePublic.Zone<MetaData>

--[=[
	@interface ZoneConfig
	@tag public_types
	@within Zoner

	@field DetectionCoverage DetectionCoverage?
	@field DetectionMethod DetectionMethod?
	@field DetectionMode DetectionMode?
	@field Simulation Simulation?
	@field Execution Execution?
	@field Bounds Bounds?
	@field Rate Rate?

	@field ExitDetectionMode DetectionMode?
	@field ExitDetectionCoverage DetectionCoverage?

	@field EnterDetectionMode DetectionMode?
	@field EnterDetectionCoverage DetectionCoverage?

	@field NoZonePartPropertyListening boolean?
	@field NoZonePartAddedListening boolean?
	@field ManualStepping boolean?
]=]
export type ZoneConfig = ZoneConfig.ZoneConfig

--[=[
	@type ZoneContainer (Model | Folder | BasePart | {BasePart} | {[string]: BasePart} | ZoneBoxes )
	@tag public_types
	@within Zoner

	A type of Zone "Part". Instanceless representation of a single Zone Box
]=]
export type ZoneContainer = ZonePrimitives.ZoneContainer

export type ZoneGroupProperties = Groups.ZoneGroupProperties;

-- Create and Export the Module's Object Type:
export type Zoner = typeof(setmetatable({} :: {
	--====================================================>
	_Initialized: boolean;
	--====================================================>
	_Zones: {[string]: Zone.Zone};
	_Connections: {[string]: RBXScriptConnection};
	_TrackerRegistry: {[Instance]: string};
	--====================================================>
	_PlayerHandler: Plagger.Plagger;
	_Events: {
		Zone_Update: BindableEvent;
		TrackerId_Clear: BindableEvent;
		TrackerId_Get: BindableFunction
	};
	--====================================================>
	_ConfigFolder: typeof(script.Config);
	--====================================================>
	_ZonerFolder: ZoneInstances.ZonerFolder;
	--====================================================>
	_ZonerHolders: {[ZoneInstances.ZonerHolder]:
		{Holder: ZoneInstances.ZonerHolder; Type: ZonerHolderTypeId; Binded: boolean; Capacity: NumberRange; IdsEvent: RBXScriptConnection}
	};
	--======================================>
}, {} :: typeof(Zoner)))

--===========================================================================================================================>
--[ HELPERS: ]

--[[
	Updates Zoner Holder Folder and ZoneHolder Folder Total Zone Attributes

	@param Zoner Zoner -- The Zoner instance managing the ZonerHolders.
]]
local function UpdateHolderStatistics(Zoner: Zoner)
	--=======================================================================================================>
	-- TotalZones Variable:
	local TotalZones: number = 0
	-- Loop through the Dictionary of ZonerHolders and Add all the ZonerGroups up:
	for Holder, HolderData in Zoner._ZonerHolders do
		-- Set a Total Zones for that HOLDER Attribute for debugging:
		Holder:SetAttribute('Zone_Total', HolderData.Capacity.Min)
		-- Add the Total Zones:
		TotalZones += HolderData.Capacity.Min
	end
	-- Set the Total Zones Entirely in the Zoner on the Holder Attribute:
	Zoner._ZonerFolder.Holders:SetAttribute('Zone_Total', TotalZones)
	--=======================================================================================================>
end

--[[
	Removes and cleans up a ZonerHolder instance.

	@param Zoner Zoner -- The Zoner instance managing the ZonerHolders.
	@param ZonerHolder Types.ZonerHolder -- The ZonerHolder to be removed.
]]
local function RemoveZonerHolder(Zoner: Zoner, ZonerHolder: ZoneInstances.ZonerHolder)
	--=======================================================================================================>
	local HolderData = Zoner._ZonerHolders[ZonerHolder]
	local HolderType: ZonerHolderTypeId = HolderData.Type

	HolderData.IdsEvent:Disconnect()
	HolderData.IdsEvent = nil :: any
	HolderData.Holder   = nil :: any
	HolderData.Capacity = nil :: any
	HolderData.Type     = nil :: any
	--=======================================================================================================>

	-- If the ZonerHolder is a ZonerActor, send a Message to the Actor to Remove its Folder:
	if HolderType == 'A' then (ZonerHolder :: ZoneInstances.ZonerActor):SendMessage('Remove') end
	-- Remove said Instance (ZonerInstance) from the Trove and Destroy it if its not an Actor:
	if HolderType == 'G' then (ZonerHolder :: ZoneInstances.ZonerGroup):Destroy() end

	-- Clear its Reference in the ZonerHolders Dictionary:
	Zoner._ZonerHolders[ZonerHolder] = nil :: any

	--=======================================================================================================>

	-- Count and Rename all ZonerHolders of the Type to Reorder them:
	-- Initialize a variable to count ZonerActors:
	local NumberOfHolder: number = 0
	-- Loop through the Dictionary of ZonerHolders and Add all the ZonerGroups up:
	for Holder: Instance, Data in Zoner._ZonerHolders do
		-- If Holder is not of the Same Holder Type that we are counting, continue:
		if Data.Type ~= HolderType then continue end
		-- Increment the Amount of Holders:
		NumberOfHolder += 1
		-- Rename the Holder with the new order:
		Holder.Name =
			`Zoner{if HolderType == 'A' then 'Actor' else 'Group'}{NumberOfHolder}` :: string;
	end
	--=======================================================================================================>
end

--[[
	Gets an open ZonerHolder with available capacity.

	@param Zoner Zoner -- The zoner manager to search within.
	@param Type? ZonerHolderTypeId -- Optional type filter ('A' or 'G').

	@return Types.ZonerHolder? -- A holder if one is found, otherwise nil.
]]
local function GetOpenZonerHolder(Zoner: Zoner, Type: ZonerHolderTypeId?): ZoneInstances.ZonerHolder?
	--=======================================================================================================>
	-- Loop through the Array of ZonerActors:
	for ZonerHolder: ZoneInstances.ZonerHolder, HolderData in Zoner._ZonerHolders do
		-- If the Type is not the Type Passed, continue:
		if Type then if HolderData.Type ~= Type then continue end end
		-- If Holder is not yet Binded, YIELD until it is Binded:
		if HolderData.Binded == false then
			repeat task.wait() until HolderData.Binded :: boolean == true;
		end
		-- If the Actor's Min is less than its Max then there is an opening:
		if HolderData.Capacity.Min < HolderData.Capacity.Max then return ZonerHolder else continue end
	end
	-- Return nothing cause nothing was found:
	return nil
	--=======================================================================================================>
end

--[[
	Gets a ZonerHolder that contains the given zone ID.

	@param Zoner Zoner -- The zoner manager.
	@param ZoneId string -- The unique zone identifier.
	@param Type? ZonerHolderTypeId -- Optional filter on holder type.

	@return ZoneInstances.ZonerHolder? -- A holder if one matches the ID.
]]
local function GetZonerHolderFromId(Zoner: Zoner, ZoneId: string, Type: ZonerHolderTypeId?): ZoneInstances.ZonerHolder?
	--=======================================================================================================>
	-- Loop through the Array of ZonerActors:
	for ZonerHolder: ZoneInstances.ZonerHolder, HolderData in Zoner._ZonerHolders do
		-- If the Type is not the Type Passed, continue:
		if Type then if HolderData.Type ~= Type then continue end end
		if HolderData.Holder.Ids:GetAttribute(ZoneId) ~= nil then return ZonerHolder end
	end
	-- Return nothing cause nothing was found:
	return nil
	--=======================================================================================================>
end

--[[
	Removes a zone and all associated references.

	@param Zoner Zoner -- The Zoner manager instance.
	@param Identifier string -- The unique ID of the zone to remove.
]]
local function RemoveZone(Zoner: Zoner, Identifier: string)
	--=======================================================================================================>
	-- Check if a Zone with that ID is stored in the Zones Table, if not, warn and return:
	if not Zoner._Zones[Identifier] then warn(`Could not Remove Zone with Id: "{Identifier}" because Zone was not found!`); return end
	--=======================================================================================================>
	-- Adjust ZonerHolder Capacity/Remove ZonerHolder if Empty:
	do
		--================================================================================>
		-- Find the ZonerHolder which Holds the Zone being Removed:
		local ZonerHolder: ZoneInstances.ZonerHolder? = GetZonerHolderFromId(Zoner, Identifier)
		-- If a ZonerHolder is found and Returned and it has a Dictionary Reference:
		if ZonerHolder and Zoner._ZonerHolders[ZonerHolder] then
			-- Update the ZonerHolder's Capacity by decreasing it by 1:
			Zoner._ZonerHolders[ZonerHolder].Capacity =
				NumberRange.new(math.max(0, Zoner._ZonerHolders[ZonerHolder].Capacity.Min - 1), Zoner._ZonerHolders[ZonerHolder].Capacity.Max)
			-- Set the Identifier Attribute on the Ids Folder to NIL to fully delete it from the Ids folder (currently it woudl be false):
			ZonerHolder.Ids:SetAttribute(Identifier, nil)
			-- If the ZonerHolder Capacity is at Min of 0, meaning Empty, then Remove the ZonerHolder:
			if Zoner._ZonerHolders[ZonerHolder].Capacity.Min == 0 then RemoveZonerHolder(Zoner, ZonerHolder) end
		end
		--================================================================================>
	end
	--=======================================================================================================>
	-- Update the Statistics:
	UpdateHolderStatistics(Zoner)
	--=======================================================================================================>
	-- Remove the Zone Object from the Trove, Destroying it in the Process:
	Zoner._Zones[Identifier]:Destroy()
	-- Clear it from the Dictionary:
	Zoner._Zones[Identifier] = nil :: any
	--=======================================================================================================>
end

--[[
	Callback handler for when the Attributes on the Ids folder change.
	Triggers cleanup if an ID becomes invalidated.

	@param Zoner Zoner -- The Zoner manager.
	@param Type ZonerHolderTypeId -- Whether it is an Actor or Group.
	@param Identifier string -- The attribute key that changed.
]]
local function OnIdsAttributeChanged(Zoner: Zoner, Type: ZonerHolderTypeId, Identifier: string)
	--=======================================================================================================>
	-- Loop through the ZonerHolders Dictionary to find the ZonerActor this Identifier refers to,
	-- without keeping a direct reference to the ZonerActor..
	for Holder, Data in Zoner._ZonerHolders do
		--====================================================================================>
		-- If the Holder Type is not an Actor, continue:
		if Data.Type ~= Type then continue end
		-- Access the Holder's Ids Table and GetAttribute. If the Attribute Value
		-- returned using the Identifier as Key, is nil, then that means this Ids folder does not have that attribute:
		if Data.Holder.Ids:GetAttribute(Identifier) == nil then continue end
		-- Get the Current Boolean State of the Identifier Attribute:
		local ZoneState: boolean = Data.Holder.Ids:GetAttribute(Identifier) :: boolean
		-- If the ZoneState is not True, meaning false or nil, we remove the Zone:
		if Type == 'A' then
			if ZoneState == false then RemoveZone(Zoner, Identifier) end
		else
			if ZoneState ~= true  then RemoveZone(Zoner, Identifier) end
		end
		-- Break this Loop:
		break
		--====================================================================================>
	end
	--=======================================================================================================>
end

--[[
	Creates two folders for organizing zones and their identifiers.

	@param Parent Instance -- The parent instance to parent the folders to.

	@return Folder, Configuration -- The zones folder and ids folder.
]]
local function CreateZoneFolders(): (Folder, Configuration)
	--=======================================================================================================>
	local ZonesFolder = Instance.new('Folder')
	ZonesFolder.Name = 'Zones'
	local IdsFolder = Instance.new('Configuration')
	IdsFolder.Name = 'Ids'
	return ZonesFolder, IdsFolder
	--=======================================================================================================>
end

--[[
	Creates a new ZonerHolder for Zones, either a Group or Actor.

	@param Zoner Zoner -- The Zoner context.
	@param Type ZonerHolderTypeId -- The holder type ('A' or 'G').

	@return ZoneInstances.ZonerHolder -- The new ZonerHolder instance.
]]
local function NewZonerHolder(Zoner: Zoner, Type: ZonerHolderTypeId): ZoneInstances.ZonerHolder
	--=======================================================================================================>
	assert(Type == 'A' or Type == 'G', `'NewZonerHolder' 'Type' was not A or G`)
	--=======================================================================================================>
	-- If Type is 'A' then it is a ZonerActor and do things accordingly:
	-- If the Type is 'G' then it is a ZonerGroup:
	if Type == 'A' then
		--===========================================================================>
		-- Grab the Config Values:
		local ZonerMaxActorCapacity: number = Zoner._ConfigFolder:GetAttribute('ZonerMaxActorCapacity')
		local ZonerMaxThreads:       number = Zoner._ConfigFolder:GetAttribute('ZonerMaxThreads')

		-- Initialize a variable to count ZonerActors:
		local NumberOfZonerActors: number = 0
		local Capacity: NumberRange

		-- Loop through the Dictionary of ZonerHolders and Add all the ZonerActors up:
		for ZonerHolder: ZoneInstances.ZonerHolder, HolderData in Zoner._ZonerHolders do
			if HolderData.Type == Type then NumberOfZonerActors += 1 end
		end

		-- If number of zone actors is greater than 7 (8) then increase all Zone Capacity to ZonerMaxThreads:
		if NumberOfZonerActors == ZonerMaxThreads then

			Zoner._ConfigFolder:SetAttribute('ZonerMaxActorCapacity', ZonerMaxActorCapacity + 1)

			ZonerMaxActorCapacity = Zoner._ConfigFolder:GetAttribute('ZonerMaxActorCapacity')

			-- Loop through the Dictionary of ZonerHolders and Update all the ZonerActor's Capacity Values:
			for ZonerHolder: ZoneInstances.ZonerHolder, HolderData in Zoner._ZonerHolders do
				-- If the HolderType is not an Actor Conitnue:
				if HolderData.Type ~= Type then continue end
				-- Update the Holder's Capacity Value:
				HolderData.Capacity = NumberRange.new(HolderData.Capacity.Min, ZonerMaxActorCapacity)
			end

			-- Will be open because above we are increasing all the Zones total capacity by one:
			return GetOpenZonerHolder(Zoner, 'A') :: ZoneInstances.ZonerHolder
		end

		-- Clone the ZonerActor from the Actors Folder and Set it to the Variable and Add it to the Trove:
		local ZonerActor: ZoneInstances.ZonerActor = script.Holders:WaitForChild('ZonerActor'):Clone() :: ZoneInstances.ZonerActor
		-- Parent the ZoneActor to nil while properties are being changed:
		-- Set the ZoneActor's Name:
		ZonerActor.Parent, ZonerActor.Name = script.Holders, `ZonerActor{NumberOfZonerActors + 1}`

		-- Create the ZonesFolder 'Folder' and IdsFolder 'Configuration'
		local ZonesFolder, IdsFolder = CreateZoneFolders()
		ZonesFolder.Parent, IdsFolder.Parent = ZonerActor, ZonerActor

		--===========================================================================>

		-- Enable the proper Script:
		do
			-- Cache the Script:
			local UsedScript:   Script =
				ZonerActor.Scripts:FindFirstChild(`ZoneEngine.{RUN_CONTEXT}`) :: Script
			-- Cache the Script:
			local UnusedScript: Script =
				ZonerActor.Scripts:FindFirstChild(`ZoneEngine.{if RUN_CONTEXT == 'Client' then 'Server' else 'Client'}`) :: Script

			-- Destroy the Other RunScope Script:
			UnusedScript:Destroy()

			-- Enable the Correct RunScope Script:
			UsedScript.Enabled = true

		end

		-- Get the Starting Capacity:
		do
			-- If number of zone actors is greater than 7 (8) then increase all Zone Capacity to ZonerMaxThreads:
			if NumberOfZonerActors > (ZonerMaxThreads - 1) then
				-- Loop through the Dictionary of ZonerHolders and Update all the ZonerActor's Capacity Values:
				for ZonerHolder: ZoneInstances.ZonerHolder, HolderData in Zoner._ZonerHolders do
					-- If the HolderType is not an Actor Conitnue:
					if HolderData.Type ~= Type then continue end
					-- Update the Holder's Capacity Value:
					HolderData.Capacity = NumberRange.new(HolderData.Capacity.Min, ZonerMaxActorCapacity)
				end
				-- Set the Capacity Variable to a NumberRange:
				Capacity = NumberRange.new(0, ZonerMaxActorCapacity)
			else
				Capacity = NumberRange.new(0, 1)
			end
		end

		--===========================================================================>

		-- Insert the Holder with Data into the Dictionary:
		Zoner._ZonerHolders[ZonerActor] = {
			-- Set a Reference to the Holder Instance:
			Holder = ZonerActor;
			-- Set the Holder's Capacity:
			Capacity = Capacity;
			-- Whether its binded:
			Binded = false;
			-- Set the Holder's Type:
			Type = 'A';
			-- Connect the Id's Event and Store it in the Table:
			IdsEvent = ZonerActor.Ids.AttributeChanged:Connect(function(Attribute: string)
				OnIdsAttributeChanged(Zoner, 'A', Attribute)
			end)
		}

		-- Create a new Unique Idenfitier (Id) for the ZonerHolder:
		ZonerActor:SetAttribute('Id', MiscHelpers.Identifier(false, false, true, 8))

		-- Wait for the ZonerActor to have its Binded Attribute as true, meaning the script had
		-- enough time to execute at its location before parenting it to the new Location:
		repeat task.wait() until ZonerActor:GetAttribute('Binded') == true;
		-- Parent the Holder into the ZonerFolder:
		ZonerActor.Parent = Zoner._ZonerFolder.Holders;

		-- Update the Zone Engine Module's Global Scope with the Holder:
		ZonerActor:SendMessage('UpdateScope', ZonerActor)
		-- Initialize the Groups Singleton in the Actor under the passed ZonesFolder:
		ZonerActor:SendMessage('InitializeGroupsSingleton', Zoner._ZonerFolder)

		-- Wait for the ZonerActor to have its Updated Attribute as true, meaning the script had
		-- enough time to execute UpdateScope:
		repeat task.wait() until ZonerActor:GetAttribute('Updated') == true;

		--===========================================================================>

		-- Return the new ZonerActor:
		return ZonerActor

		--===========================================================================>
	else
		--===========================================================================>

		-- Initialize a variable to count ZonerActors:
		local NumberOfZonerGroups: number = 0

		-- Loop through the Dictionary of ZonerHolders and Add all the ZonerGroups up:
		for ZonerHolder: ZoneInstances.ZonerHolder, HolderData in Zoner._ZonerHolders do
			if HolderData.Type == Type then NumberOfZonerGroups += 1 end
		end

		-- Clone the ZonerGroup from the Actors Folder and Set it to the Variable and Add it to the Trove:
		local ZonerGroup: ZoneInstances.ZonerGroup = script.Holders:WaitForChild('ZonerGroup'):Clone() :: ZoneInstances.ZonerGroup
		-- Parent the ZonerGroup to nil while properties are being changed:
		-- Set the ZonerGroup's Name:
		ZonerGroup.Parent, ZonerGroup.Name = nil, `ZonerGroup{NumberOfZonerGroups + 1}` :: any

		-- Create the ZonesFolder 'Folder' and IdsFolder 'Configuration'
		local ZonesFolder, IdsFolder = CreateZoneFolders()
		ZonesFolder.Parent, IdsFolder.Parent = ZonerGroup, ZonerGroup
		--===========================================================================>

		-- Insert the Holder with Data into the Dictionary:
		Zoner._ZonerHolders[ZonerGroup] = {
			-- Set a Reference to the Holder Instance:
			Holder = ZonerGroup;
			-- Set the Holder's Capacity:
			Capacity = NumberRange.new(0, Zoner._ConfigFolder:GetAttribute('ZonerMaxGroupCapacity'));
			-- Auto Bind:
			Binded = true;
			-- Set the Holder's Type:
			Type = 'G';
			-- Connect the Id's Event and Store it in the Table:
			IdsEvent = ZonerGroup.Ids.AttributeChanged:Connect(function(Attribute: string)
				OnIdsAttributeChanged(Zoner, 'G', Attribute)
			end)
		}

		-- Create a new Unique Idenfitier (Id) for the ZonerHolder:
		ZonerGroup:SetAttribute('Id', MiscHelpers.Identifier(false, false, true, 8))

		-- Parent the Holder into the ZonerFolder:
		ZonerGroup.Parent = Zoner._ZonerFolder.Holders

		-- Update the Zone Module's Global Scope with the Holder:
		Zone.UpdateGlobalScope(ZonerGroup)
		--===========================================================================>

		-- Return the new ZonerGroup:
		return ZonerGroup

		--===========================================================================>
	end
	--=======================================================================================================>
end

--[[
	Adds a new Zone instance into the Zoner registry.

	@param Zoner Zoner -- The zoner instance.
	@param Container ZoneContainer -- What defines the zone bounds (Instance or table).
	@param ZoneConfig ZoneConfig -- Configs controlling detection behavior.

	@return Zone.Zone -- The created and tracked zone.
]]
local function AddZone(Zoner: Zoner, Container: ZonePrimitives.ZoneContainer, ZoneConfig: ZoneConfig.ZoneConfig): Zone.Zone
	--=======================================================================================================>

	-- Cache the ZonerFolder:
	local ZonerFolder = Zoner._ZonerFolder
	-- Cache the Id_Counter Number:
	local Id_Counter: number = ZonerFolder:GetAttribute('Id_Counter') :: number
	-- Create and get a new Unique Identifier string:
	local Identifier: string = `{Id_Counter + 1}`

	-- Check if a Zone with that ID is stored in the Zones Table, if so, warn and return:
	if Zoner._Zones[Identifier] then warn(`Could not Add Zone with Id: "{Identifier}" because Zone already exists!`); return nil :: any end

	-- Update teh Id_Counter so that new Zones can be created:
	ZonerFolder:SetAttribute('Id_Counter', tonumber(Identifier))

	--=======================================================================================================>

	-- Get the HolderType based on Execution Config:
	local HolderType: ZonerHolderTypeId = if ZoneConfig.Execution == ENUMS.Execution.Serial then 'G' else 'A'

	-- Get either an Open ZonerActor, or a new ZonerActor:
	-- Boolean to whether theres a new ZonerHolder or not:
	local ZonerHolder, MakingNewZonerHolder do
		-- Check if theres an Open ZonerHolder, set it to the variable if so:
		ZonerHolder = GetOpenZonerHolder(Zoner, HolderType) :: ZoneInstances.ZonerHolder
		-- If there is no Open ZonerHolder, make a new one, and set the MakingNew variable to true:
		if not ZonerHolder then ZonerHolder, MakingNewZonerHolder = NewZonerHolder(Zoner, HolderType), true end
	end

	--=======================================================================================================>

	-- Increment the Capacity of the ZonerActor by One to Add the Zone to it:
	Zoner._ZonerHolders[ZonerHolder].Capacity =
		NumberRange.new(Zoner._ZonerHolders[ZonerHolder].Capacity.Min + 1, Zoner._ZonerHolders[ZonerHolder].Capacity.Max)

	-- Set the Identifier onto the Ids Folder to Store it:
	ZonerHolder.Ids:SetAttribute(Identifier, true)

	-- If we have made a new ZonerHolder, then set its 'Binded' variable to true, after the initial Capacity has been Set:
	if MakingNewZonerHolder then Zoner._ZonerHolders[ZonerHolder].Binded = true; end

	--=======================================================================================================>

	-- Update the Statistics:
	UpdateHolderStatistics(Zoner)

	--=======================================================================================================>

	-- Construct the Zone Object and add it to the Zones Table with Identifier as the Key:
	Zoner._Zones[Identifier] = Zone.New(Container, ZoneConfig, ZonerHolder, ZonerFolder, Identifier)

	-- Return a REFERENCE to the Zone Object stored in the Table:
	return Zoner._Zones[Identifier] :: any

	--=======================================================================================================>
end

--[[
	Moves the internal Zoner folder to a new location in the hierarchy.

	@param Zoner Zoner -- The zoner manager.
	@param Location? Instance -- The new parent (must be in a safe location).

	@return "Success" | "Failure" -- Whether relocation succeeded.
]]
local function _RelocateZonerFolder(Zoner: Zoner, Location: Instance?): 'Success' | 'Failure'
	--=======================================================================================================>
	-- If we are running on the Server:
	if RUN_CONTEXT == 'Server' then
		-- If Locations is nil, return the
		if Location then
			-- Boolean to check whether the Parent Location is under ServerScriptService, ReplicatedStorage, or ServerStorage:
			local IsInOkayLocation =
				Location:IsDescendantOf(game.ServerScriptService) or
				Location:IsDescendantOf(game.ReplicatedStorage) or
				Location:IsDescendantOf(game.ServerStorage)
			-- If its not in an Okay Location, Make it Parent Directly to ServerScriptService:
			if not IsInOkayLocation then
				warn(`ZonerFolder Relocation Failed because Folder was not under a Service`); return 'Failure'
			end
		else
			Location = script
		end
		-- Reparent the ZonerFolder:
		Zoner._ZonerFolder.Parent = Location :: Instance
		return 'Success'
	else
		return 'Failure'
	end
	--=======================================================================================================>
end

--===========================================================================================================================>
--[ CONSTRUCTOR METHODS: ]

--[[
	Initializes the Zoner singleton and sets up folder structure, troves, Configs, events, and replication systems.

	@return Zoner -- The constructed Zoner instance.
]]
function Zoner._Start(): Zoner
	--=======================================================================================================>

	-- Set a Memory Category for this Zoner:
	debug.setmemorycategory('[Zoner]')

	--=======================================================================================================>

	-- Define Data
	local ZonerData = {
		--====================================================>
		_ConfigFolder = script:WaitForChild('Config') :: typeof(script.Config);
		--====================================================>
		_ZonerHolders  = {} :: {[ZoneInstances.ZonerHolder]:
			{Holder: ZoneInstances.ZonerHolder; Type: ZonerHolderTypeId; Binded: boolean; Capacity: NumberRange; IdsEvent: RBXScriptConnection}
		};
		_Zones = {} :: {[string]: Zone.Zone};
		_Connections   = {} :: {[string]: RBXScriptConnection};
		_Events = {
			Zone_Update     = (script:WaitForChild('Events'):FindFirstChild('Zone_Update')     or Instance.new('BindableEvent'))    :: BindableEvent;
			TrackerId_Clear = (script:WaitForChild('Events'):FindFirstChild('TrackerId_Clear') or Instance.new('BindableEvent'))    :: BindableEvent;
			TrackerId_Get   = (script:WaitForChild('Events'):FindFirstChild('TrackerId_Get')   or Instance.new('BindableFunction')) :: BindableFunction;
		};
		--====================================================>
		_PlayerHandler  = Plagger.New(false);
		_TrackerRegistry = {}; --		_TrackerRegistry = setmetatable({}, {__mode = "k"}) :: any;
		--====================================================>
		_Initialized = script:GetAttribute('Initialized') :: boolean or false;
		--====================================================>
	}

	--=======================================================================================================>

	-- Check Configs and if needed, Correct, Configs:
	if not ZonerData._ConfigFolder then error('Dont delete the Config Folder on Zoner') end

	MiscHelpers.EnsureConfig(ZonerData._ConfigFolder, 'ZonerMaxActorCapacity', DEF_MAX_ACTOR_CAPACITY)
	MiscHelpers.EnsureConfig(ZonerData._ConfigFolder, 'ZonerMaxGroupCapacity', DEF_MAX_GROUP_CAPACITY)
	MiscHelpers.EnsureConfig(ZonerData._ConfigFolder, 'ZonerMaxThreads',       DEF_MAX_THREADS)

	--=======================================================================================================>

	-- if the Initialized Attribute is false, set the Script's Attribute to true:
	if ZonerData._Initialized == false then script:SetAttribute('Initialized', true) end

	--=======================================================================================================>

	-- Create a Bindable Event for this Actor Group for Zone Signals:
	-- Better than BindableEvents per Zone:
	ZonerData._Events.Zone_Update.Name = 'Zone_Update'
	ZonerData._Events.Zone_Update.Parent = script:WaitForChild('Events')

	ZonerData._Events.TrackerId_Clear.Name = 'TrackerId_Clear'
	ZonerData._Events.TrackerId_Clear.Parent = script:WaitForChild('Events')

	ZonerData._Events.TrackerId_Get.Name = 'TrackerId_Get'
	ZonerData._Events.TrackerId_Get.Parent = script:WaitForChild('Events')

	--=======================================================================================================>

	-- Check if a Folder named 'Zoners' already exists:
	local ZonerPointer: ObjectValue = script:FindFirstChild(`ZonerPointer:{RUN_CONTEXT}`, false) or Instance.new('ObjectValue') :: ObjectValue
	-- If no ZonersPointer was found for the RunScope, create one:
	ZonerPointer.Name = `ZonerPointer:{RUN_CONTEXT}`
	ZonerPointer.Parent = script

	if not ZonerPointer.Value then
		-- If one Already Exists, set the reference to it, if not, construct a new Folder:
		ZonerData._ZonerFolder = Instance.new('Folder') :: ZoneInstances.ZonerFolder
		-- Set ZonersFolder Parent to nil for no replication:
		ZonerData._ZonerFolder.Parent = nil
		-- Name said Folder (again possibly):
		ZonerData._ZonerFolder.Name = `Zoner:{RUN_CONTEXT}` :: any

		-- Set a Counter to Keep Track of the Zones Created:
		ZonerData._ZonerFolder:SetAttribute('Id_Counter', 0)

		local HoldersFolder: Folder = Instance.new('Folder')
		HoldersFolder.Name = 'Holders'
		HoldersFolder.Parent = ZonerData._ZonerFolder

		local GroupsFolder: Folder = Instance.new('Folder')
		GroupsFolder.Name = 'Groups'
		GroupsFolder.Parent = ZonerData._ZonerFolder

		if RUN_CONTEXT == 'Server' then
			-- Boolean to check whether the Module is under ServerScriptService, or ServerStorage:
			local ScriptInServerOnlyLocation: boolean =
				script:IsDescendantOf(game.ServerScriptService) or
				script:IsDescendantOf(game.ServerStorage)
			-- Set ZonersFolder Parent to the Script if the Script is a descendant of a Server Service, else set to Server Service:
			ZonerData._ZonerFolder.Parent = if ScriptInServerOnlyLocation then script else game.ServerScriptService
		else
			-- Set ZonersFolder Parent to the Script:
			ZonerData._ZonerFolder.Parent = script
		end
		-- Set the ObjectValue to Point to the Folder:
		ZonerPointer.Value = ZonerData._ZonerFolder
	else
		ZonerData._ZonerFolder = ZonerPointer.Value  :: ZoneInstances.ZonerFolder
	end

	--=======================================================================================================>

	-- Set Metatable to the MetaTable and the current Module
	setmetatable(ZonerData, Zoner)

	--=======================================================================================================>

	-- Connect to the AncestryChanged event of the Script:
	-- We then check if the Script is still a decendent of the WorldModel, meaning it hasnt been destroyed.
	-- We do this to clear the Data in case the Script is Destroyed:
	ZonerData._Connections.Script_AC = script.AncestryChanged:Connect(function()
		if script.Parent == nil or script:IsDescendantOf(game) == false then if ZonerData.Destroy then ZonerData:Destroy() end end
	end)

	-- Connect to the GetPropertyChangedSignal(Parent) event of the Script:
	-- We then check if the Script is still a decendent of the WorldModel, meaning it hasnt been destroyed.
	-- We do this to clear the Data in case the Script is Destroyed:
	ZonerData._Connections.Script_PCS = script:GetPropertyChangedSignal('Parent'):Connect(function()
		if script.Parent == nil or script:IsDescendantOf(game) == false then if ZonerData.Destroy then ZonerData:Destroy() end end
	end)

	-- Connect to the Destroying event of the Script:
	-- We do this to clear the Data in case the Script is Destroyed:
	ZonerData._Connections.Script_DG = script.Destroying:Connect(function()
		if ZonerData.Destroy then ZonerData:Destroy() end
	end)

	-- Connect to the Zoner Signal Bindable Event, and find the Zone for which the id correlates, and call its Received Method:
	ZonerData._Connections.Zone_Signal = ZonerData._Events.Zone_Update.Event:Connect(function(Id: string, ...)
		if Id and ZonerData._Zones[Id] then ZonerData._Zones[Id]:_OnZoneSignalReceived(...) end
	end)

	--=======================================================================================================>

	-- Register the Zoner Tracker Registry table globally:
	-- Set a new Shared Table in the Registery for Tracking Tracker Ids:
	SharedTableRegistry:SetSharedTable('Zoner: Tracker Registry', SharedTable.new())

	-- Retrieve the Zoner Trackers Shared Table:
	local Shared_Tracker_Registry: SharedTable = SharedTableRegistry:GetSharedTable('Zoner: Tracker Registry')

	-- Connect to the TrackerId_Clear Bindable Event to Clear the Tracked item from the registry:
	ZonerData._Connections.TrackerId_Clear = ZonerData._Events.TrackerId_Clear.Event:Connect(function(Trackable: Model | BasePart)
		--===============================================================================>
		--warn('Complete Registry Wipe of Tracker', Trackable)
		-- If one is already made, send it:
		if ZonerData._TrackerRegistry[Trackable] then
			-- Clear the Registry sub table:
			SharedTable.clear(Shared_Tracker_Registry[ZonerData._TrackerRegistry[Trackable]])
			-- Clear the Table from the registry entirely:
			Shared_Tracker_Registry[ZonerData._TrackerRegistry[Trackable]] = nil
			-- Untrack the Instance from the Tracker Registry:
			ZonerData._TrackerRegistry[Trackable] = nil
		end
		--===============================================================================>
	end)

	-- Bind an Invoke function for the TrackerId Get, to return the Id for the sent instance:
	ZonerData._Events.TrackerId_Get.OnInvoke = function(Trackable: Model | BasePart): string
		--===============================================================================>
		-- If one is already made, send it:
		if ZonerData._TrackerRegistry[Trackable] then return ZonerData._TrackerRegistry[Trackable] end

		-- Create a new Identifier for this Tracker:
		ZonerData._TrackerRegistry[Trackable] = MiscHelpers.Identifier(false, false, true, 8);
		-- Register the Tracker with [Id]: {} on the Shared Tracker Registry:
		Shared_Tracker_Registry[ZonerData._TrackerRegistry[Trackable]] = SharedTable.new();
		-- Return the Id string:
		return ZonerData._TrackerRegistry[Trackable]
		--===============================================================================>
	end

	--=======================================================================================================>

	-- Return the MetaTable Data
	return ZonerData

	--=======================================================================================================>
end

--[[
	Destroys the Zoner system and all its managed zones and connections.

	@param self Zoner -- The instance to destroy.
]]
function Zoner.Destroy(self: Zoner)
	--=======================================================================================================>
	for Key, Zone in pairs(self._Zones) do RemoveZone(self, Key) end
	table.clear(self._Zones)
	for Holder, Data in pairs(self._ZonerHolders) do RemoveZonerHolder(self, Holder) end
	table.clear(self._ZonerHolders)
	for Key, Connection in pairs(self._Connections) do Connection:Disconnect() end
	table.clear(self._Connections)

	self._ZonerFolder:Destroy()
	self._PlayerHandler:Destroy()
	--=======================================================================================================>
end

--===========================================================================================================================>

--[[
	Creates a new Zone from a part, model, folder, config, or a list of zone boxes.

	@param self Zoner -- The current Zoner context.
	@param ZoneContainer ZoneContainer -- Boundaries of the zone.
	@param ZoneConfig? ZoneConfig -- Optional zone tuning configs.

	@return Zone -- The constructed zone object.
]]
function Zoner.NewZone(self: Zoner, ZoneContainer: ZonePrimitives.ZoneContainer, ZoneConfig: ZoneConfig.ZoneConfig?): Zone.Zone
	--=======================================================================================================>
	-- Assert Statements for Paramaters:
	do
		--=============================================================================================>
		-- Assert whether the Provided Container is of the proper Instance or Table type:
		assert((typeof(ZoneContainer) == "table" or typeof(ZoneContainer) == "Instance"), Constants.Logs.Zoner.ContainerType)

		-- If the Container Type is an Instance, check type of Instance:
		if typeof(ZoneContainer) == "Instance" then
			-- Assert the Specifics of the Instance Type:
			assert(
				ZoneContainer:IsA('BasePart') or ZoneContainer:IsA('Model') or ZoneContainer:IsA('Folder') or ZoneContainer:IsA('Configuration'),
				Constants.Logs.Zoner.ContainerInstanceType
			)
			-- Assert the Specifics of the Instance Type:
			assert(
				not (ZoneContainer:IsA('Terrain') or ZoneContainer:IsA('Workspace')),
				Constants.Logs.Zoner.ContainerInstanceType
			)
		else
			-- Loop through the Container Table to make sure each Value in the table is an Instance:
			for Index, Unknown in pairs(ZoneContainer) do
				if typeof(Unknown) == "table" then
					if Unknown['CFrame'] and Unknown['Size'] then continue else error(Constants.Logs.Zoner.ContainerBoxTable) end
				elseif typeof(Unknown) == "Instance" then
					if not Unknown:IsA('BasePart') then
						error(Constants.Logs.Zoner.ContainerPartTable1)
					end
				elseif typeof(Unknown) ~= "Instance" then
					if Index == 'CFrame' or Index == 'Size' then
						error(Constants.Logs.Zoner.ContainerBoxTable2)
					else
						error(Constants.Logs.Zoner.ContainerPartTable2)
					end
				end
			end
		end

		if ZoneConfig then
			-- Assert that the DetectionCoverage Number sent is the right Enum value:
			if ZoneConfig.DetectionCoverage then
				assert(DetectionCoverage.HASH.Write[ZoneConfig.DetectionCoverage] ~= nil, Constants.Logs.Zoner.DetectionCoverage)
			end
			-- Assert that the DetectionCoverage Number sent is the right Enum value:
			if ZoneConfig.DetectionMethod then
				assert(DetectionMethod.HASH.Write[ZoneConfig.DetectionMethod] ~= nil, Constants.Logs.Zoner.DetectionMethod)
			end

			-- Assert that the EnterDetectionCoverage Number sent is the right Enum value:
			if ZoneConfig.EnterDetectionCoverage then
				assert(DetectionCoverage.HASH.Write[ZoneConfig.EnterDetectionCoverage] ~= nil, Constants.Logs.Zoner.DetectionCoverage)
			end
			-- Assert that the ExitDetectionCoverage Number sent is the right Enum value:
			if ZoneConfig.ExitDetectionCoverage then
				assert(DetectionCoverage.HASH.Write[ZoneConfig.ExitDetectionCoverage] ~= nil, Constants.Logs.Zoner.DetectionCoverage)
			end

			-- Assert that the ExitDetectionMode Number sent is the right Enum value:
			if ZoneConfig.ExitDetectionMode then
				assert(DetectionMode.HASH.Write[ZoneConfig.ExitDetectionMode] ~= nil, Constants.Logs.Zoner.DetectionMode)
			end
			-- Assert that the EnterDetectionMode Number sent is the right Enum value:
			if ZoneConfig.EnterDetectionMode then
				assert(DetectionMode.HASH.Write[ZoneConfig.EnterDetectionMode] ~= nil, Constants.Logs.Zoner.DetectionMode)
			end

			-- Assert that the DetectionCoverage Number sent is the right Enum value:
			if ZoneConfig.DetectionMode then
				assert(DetectionMode.HASH.Write[ZoneConfig.DetectionMode] ~= nil, Constants.Logs.Zoner.DetectionMode)
			end
			-- Assert that the Simulation Number sent is the right Enum value:
			if ZoneConfig.Simulation then
				assert(Simulation.HASH.Write[ZoneConfig.Simulation] ~= nil, Constants.Logs.Zoner.Simulation)
			end
			-- Assert that the Execution Number sent is the right Enum value:
			if ZoneConfig.Execution then
				assert(Execution.HASH.Write[ZoneConfig.Execution] ~= nil, Constants.Logs.Zoner.Execution)
			end
			-- Assert that the Bounds Number sent is the right Enum value:
			if ZoneConfig.Bounds then
				assert(Bounds.HASH.Write[ZoneConfig.Bounds] ~= nil, Constants.Logs.Zoner.Bounds)
			end
			-- Assert that the Rate Number sent is the right Enum value:
			if ZoneConfig.Rate then
				assert(Rate.HASH.Write[ZoneConfig.Rate] ~= nil, Constants.Logs.Zoner.Rate)
			end
			-- Assert that the ManualStepping sent is a boolean:
			if ZoneConfig.ManualStepping then
				assert(typeof(ZoneConfig.ManualStepping) == 'boolean', Constants.Logs.Zoner.ManualStepping)
			end
			-- Assert that the NoZonePartAddedListening sent is a boolean:
			if ZoneConfig.NoZonePartAddedListening then
				assert(typeof(ZoneConfig.NoZonePartAddedListening) == 'boolean', Constants.Logs.Zoner.NoZonePartAddedListening)
			end
			-- Assert that the NoZonePartPropertyListening sent is a boolean:
			if ZoneConfig.NoZonePartPropertyListening then
				assert(typeof(ZoneConfig.NoZonePartPropertyListening) == 'boolean', Constants.Logs.Zoner.NoZonePartPropertyListening)
			end
		end
		--=============================================================================================>
	end
	--=======================================================================================================>
	-- If no ZoneConfig were sent, we are Filling it out with default values:
	if ZoneConfig then
		-- Loop through the Default Zone Configs and use them to replace any missing Configs in the ZoneConfig Table:
		for Key: string, Config in pairs(DEFAULT_ZONE_CONFIG) do
			if ZoneConfig[Key] == nil then ZoneConfig[Key] = Config end
		end
		-- Loop through the Default Zone Configs and use them to replace any missing Configs in the ZoneConfig Table:
		for Key: string, Config in pairs(ZoneConfig) do
			-- Don't do clean for booleans:
			if typeof(Config) == 'boolean' then continue end
		end
	end
	--=======================================================================================================>
	-- Add the Zone to the Zoner's Table and Attributes, and return the Object to the User:
	return AddZone(self, ZoneContainer, ZoneConfig or DEFAULT_ZONE_CONFIG)
	--=======================================================================================================>
end

--[[
	Creates a new zone from a region defined by a CFrame and Size vector.

	@function NewFromContainer
	@within Zoner

	@param self Zoner -- The current Zoner context.
	@param RegionCFrame CFrame -- Position and rotation of the region.
	@param RegionSize Vector3 -- Dimensions of the region.
	@param ZoneConfig? ZoneConfig -- Optional configuration.

	@return Zone -- The constructed region zone.
]]
function Zoner.NewZoneFromRegion(self: Zoner, RegionCFrame: CFrame, RegionSize: Vector3, ZoneConfig: ZoneConfig.ZoneConfig?): Zone.Zone
	--=======================================================================================================>
	-- If the ZoneConfig's Paramater was passed, we will check whether to create internal parts or not:
	if ZoneConfig then
		--============================================================================================>
		-- If the RegionCFrame has no Rotation, then Make sure if the ZoneConfig is set to PerPart, it is corrected to be BoxExact:
		if RegionCFrame.Rotation:FuzzyEq(CFrame.new(), 0.00001) then
			--==================================================================================>
			-- If a ZoneConfig Table was passed and the
			if ZoneConfig.Bounds == ENUMS.Bounds.PerPart then
				ZoneConfig.Bounds = ENUMS.Bounds.BoxExact
			elseif ZoneConfig.Bounds == ENUMS.Bounds.Automatic then
				ZoneConfig.Bounds = ENUMS.Bounds.BoxExact
			end
			--==================================================================================>
		end
		-- If the DetectionMethod is Automatic, set it to the most efficient DetectionMethod:
		if ZoneConfig.DetectionMethod == ENUMS.DetectionMethod.Automatic then
			ZoneConfig.DetectionMethod = ENUMS.DetectionMethod.Efficient
		end
		-- If the DetectionMethod is Complex, then we need to create internal parts for this Zone, else create a partless zone:
		if ZoneConfig.DetectionMethod == ENUMS.DetectionMethod.Complex then
			--==================================================================================>
			-- Create a new Model Instance to act as a Container:
			local Container: Model = Instance.new("Model"); Container.Name = 'ZoneContainer'
			-- Create a Cube of Parts inside the Zone matching the CFrame and Size:
			MiscHelpers.CreateCube(Container, RegionCFrame, RegionSize)
			-- Return a New Zone Object that has been Relocated:
			return self:NewZone(Container, ZoneConfig):Relocate()
			--==================================================================================>
		else
			-- Return a New PARTLESS Zone Object:
			return self:NewZone({{CFrame = RegionCFrame, Size = RegionSize}}, ZoneConfig)
		end
		--============================================================================================>
	else
		-- Return a New Zone Object:
		return self:NewZone({{CFrame = RegionCFrame, Size = RegionSize}}, ZoneConfig)
	end
	--=======================================================================================================>
end

--[[
	Creates a new Zone Group to be bound to:

	@function NewFromContainer
	@within Zoner

	@param self Zoner -- The current Zoner context.
	@param GroupName string -- Name to store as.
	@param Properties {Exclusive: boolean} -- Properties associated with this group
]]
function Zoner.CreateGroup(self: Zoner, GroupName: string, Properties: Groups.ZoneGroupProperties)
	--=======================================================================================================>
	-- Assert the paramater:
	assert(GroupName ~= nil, `[Zoner]: 'CreateGroup' | 'GroupName' paramater was nil`)
	-- Assert the paramater:
	assert(Properties ~= nil, `[Zoner]: 'CreateGroup' | 'Properties' paramater was nil`)
	-- Return if a group is already stored by that name:
	if Groups.ZoneGroups[GroupName] then warn(`[Zoner]: 'CreateGroup' Group with the name "{GroupName}" already exists!`); return end

	-- Create the Group Instances in serial:
	Groups.CreateGroup(GroupName, Properties, self._ZonerFolder)
	--=======================================================================================================>
end

--===========================================================================================================================>

-- Construct the Zoner Module:
local _Zoner = Zoner._Start()

--[=[
	Creates a new Zone from a part, model, folder, config, or a list of zone boxes.

	@function NewFromContainer
	@within Zoner
	@param ZoneContainer ZoneContainer -- Boundaries of the zone.
	@param ZoneConfig ZoneConfig? -- Optional zone tuning configs.

	@return Zone -- The constructed zone object.
]=]
local NewFromContainer = function(ZoneContainer: ZonePrimitives.ZoneContainer, ZoneConfig: ZoneConfig.ZoneConfig?): Zone
	return _Zoner:NewZone(ZoneContainer, ZoneConfig) :: any
end;

--[=[
	Creates a new Zone from a part, model, folder, config, or a list of zone boxes.

	@function New
	@within Zoner
	@param ZoneContainer ZoneContainer -- Boundaries of the zone.
	@param ZoneConfig ZoneConfig? -- Optional zone tuning configs.

	@return Zone -- The constructed zone object.
]=]
local New = function(ZoneContainer: ZonePrimitives.ZoneContainer, ZoneConfig: ZoneConfig.ZoneConfig?): Zone
	return _Zoner:NewZone(ZoneContainer, ZoneConfig)  :: any
end;

--[=[
	Creates a new zone from a region defined by a CFrame and Size vector.

	@function NewZoneFromRegion
	@within Zoner

	@param RegionCFrame CFrame -- Position and rotation of the region.
	@param RegionSize Vector3 -- Dimensions of the region.
	@param ZoneConfig? ZoneConfig -- Optional configuration.

	@return Zone -- The constructed region zone.
]=]
local NewZoneFromRegion = function(RegionCFrame: CFrame, RegionSize: Vector3, ZoneConfig: ZoneConfig.ZoneConfig?): Zone
	return _Zoner:NewZoneFromRegion(RegionCFrame, RegionSize, ZoneConfig)  :: any
end;

--[=[
	Creates a new zone from a box defined by a CFrame and Size vector.

	@function NewZoneFromBox
	@within Zoner

	@param BoxCFrame CFrame -- Position and rotation of the region.
	@param BoxSize Vector3 -- Dimensions of the region.
	@param ZoneConfig? ZoneConfig -- Optional configuration.

	@return Zone -- The constructed region zone.
]=]
local NewZoneFromBox = function(BoxCFrame: CFrame, BoxSize: Vector3, ZoneConfig: ZoneConfig.ZoneConfig?): Zone
	return _Zoner:NewZoneFromRegion(BoxCFrame, BoxSize, ZoneConfig) :: any
end;

--[=[
	Relocates the Zoner Folder which stores the Zone Instances

	@function RelocateZonerFolder
	@within Zoner

	@param Location Instance? -- the Location to send the Folder (usually a service)
]=]
local RelocateZonerFolder = function(Location: Instance?): 'Success'|'Failure'
	return _RelocateZonerFolder(_Zoner, Location)
end;

--[=[
	Creates a new Zone Group to be bound to:

	@function CreateGroup
	@within Zoner

	@param self Zoner -- The current Zoner context.
	@param GroupName string -- Name to store as.
	@param Properties ZoneGroupProperties -- Properties associated with this group
]=]
local CreateGroup = function(GroupName: string, Properties: Groups.ZoneGroupProperties)
	_Zoner:CreateGroup(GroupName, Properties)
end;

--===========================================================================================================================>

-- [Notice] can only use the . operator on these functions:
-- Return a table containing the only functions and properties needed by the user:
return table.freeze({
	--==========================================================================>
	NewZoneFromRegion   = NewZoneFromRegion;
	NewFromContainer    = NewFromContainer;
	NewZoneFromBox      = NewZoneFromBox;
	New                 = New;

	RelocateZonerFolder = RelocateZonerFolder;
	CreateGroup         = CreateGroup;
	--==========================================================================>
	-- Require the Enum2 Module for Custom Enums:
	-- For referencing the Enums from other scripts:
	Enum = ENUMS;
	-- Default Zone Collision Group:
	ZONER_COLLISION_GROUP = ZONER_COLLISION_GROUP :: 'Zoner:ZonePart'
	--==========================================================================>
})

--===========================================================================================================================>


--Sends messages in bulk through 1 topic to prevent too many topics being used.
--!strict

local NOT_CONNECTED_WARNING_DELAY_SECONDS = 10

local BulkMessagingService = {}
BulkMessagingService.__index = BulkMessagingService

local HttpService = game:GetService("HttpService")

export type BulkMessagingService = {
    Topic: string,
    MessagingService: MessagingService,
    PendingMessages: {[string]: {any}},
    SubscribedEvents: {[string]: BindableEvent},
    PendingFlush: boolean,
} & typeof(setmetatable({}, BulkMessagingService))

--[[
Creates a bulk messaging service.
--]]
function BulkMessagingService.new(MessagingService: MessagingService): BulkMessagingService
    --Create the object.
    local self = setmetatable({
        Topic = "NexusBulkMessagingService",
        MessagingService = MessagingService,
        PendingMessages = {},
        SubscribedEvents = {},
        PendingFlush = false,
    }, BulkMessagingService) :: BulkMessagingService

    --Set up subscribing.
    local SubscribeAsyncComplete = false
    task.spawn(function()
        xpcall(function()
            MessagingService:SubscribeAsync(self.Topic, function(MessageData)
                for Topic, Packets in HttpService:JSONDecode(MessageData.Data) do
                    local Event = self.SubscribedEvents[Topic]
                    if Event then
                        for _,Packet in Packets do
                            Event:Fire({Data = Packet, Sent = MessageData.Sent})
                        end
                    end
                end
            end)
        end, function(ErrorMessage: string)
            warn(`Listening to messaging service failed because {ErrorMessage}`)
        end)
        SubscribeAsyncComplete = true
    end)
    task.delay(NOT_CONNECTED_WARNING_DELAY_SECONDS, function()
        if SubscribeAsyncComplete then return end
        warn(`MessagingService::SubscribeAsync failed to complete in {NOT_CONNECTED_WARNING_DELAY_SECONDS} seconds. Messages may not be received.`)
    end)

    --Return the object.
    return self
end

--[[
Starts the passive loop for flushing messages.
--]]
function BulkMessagingService.StartPassiveLoop(self: BulkMessagingService): ()
    task.spawn(function()
        while true do
            xpcall(function()
                self:FlushMessages()
            end, function(ErrorMessage: string)
                warn("Passive flush failed because "..tostring(ErrorMessage))
            end)
            task.wait(1)
        end
    end)
end

--[[
Returns if an object is too long to send.
--]]
function BulkMessagingService.CanSendObject(self: BulkMessagingService, Object: any): boolean
    return string.len(HttpService:JSONEncode(HttpService:JSONEncode(Object))) <= 800
end

--[[
Clones the pending messages and adds the new message.
Returns the clone of the data.
--]]
function BulkMessagingService.AddPacket(self: BulkMessagingService, Topic: string, Message: any): {[string]: {any}}
    --Clone the pending messages.
    local PendingMessages = {}
    for Topic, Packets in self.PendingMessages do
        PendingMessages[Topic] = {}
        for i, Packet in Packets do
            PendingMessages[Topic][i] = Packet
        end
    end

    --Add the message.
    if not PendingMessages[Topic] then
        PendingMessages[Topic] = {}
    end
    table.insert(PendingMessages[Topic], Message)

    --Return the messages.
    return PendingMessages
end

--[[
Flushes the current messages.
--]]
function BulkMessagingService.FlushMessages(self: BulkMessagingService)
    if self.PendingFlush then
        local MessagesJSON = HttpService:JSONEncode(self.PendingMessages)
        self.PendingMessages = {}
        self.PendingFlush = false
        self.MessagingService:PublishAsync(self.Topic, MessagesJSON)
    end
end

--[[
Publishes a message for a given topic.
--]]
function BulkMessagingService.PublishAsync(self: BulkMessagingService, Topic: string, Message: any): ()
    --Flush if the new message would make the packet too big.
    if not self:CanSendObject(self:AddPacket(Topic, Message)) then
        self:FlushMessages()
    end

    --Add the message to send.
    if not self.PendingMessages[Topic] then
        self.PendingMessages[Topic] = {}
    end
    table.insert(self.PendingMessages[Topic], Message)
    self.PendingFlush = true
end

--[[
Subscribes to messages being sent for a topic.
--]]
function BulkMessagingService.SubscribeAsync(self: BulkMessagingService, Topic: string, Callback: (any) -> ()): RBXScriptConnection
    --Create the event.
    if not self.SubscribedEvents[Topic] then
        self.SubscribedEvents[Topic] = Instance.new("BindableEvent")
    end

    --Connect and return the event.
    return self.SubscribedEvents[Topic].Event:Connect(Callback)
end

return BulkMessagingService


--Implementation of SaveData that does not use external services.
--This should only be used for testing or DataStores going offline.
--!strict

local LocalSaveData = {}
LocalSaveData.__index = LocalSaveData

export type LocalSaveData = {
    Data: {[string]: any},
    OnUpdateEvents: {[string]: BindableEvent},
} & typeof(setmetatable({}, LocalSaveData))

--[[
Creates the memory save data instance.
--]]
function LocalSaveData.new(): LocalSaveData
    return setmetatable({
        Data = {},
        OnUpdateEvents = {},
    }, LocalSaveData) :: LocalSaveData
end

--[[
Sets if messages are sent when a key is
updated or if the data needs to be re-fetched (i.e. string
or entry is >1000 characters). By default, this is true.
--]]
function LocalSaveData.SetSendDataChangeUpdates(self: LocalSaveData, Value: boolean): ()
    --No implementation.
end

--[[
Sets if data can be overwriten if the loading
of data failed. This should be kept true if player data
is involved. By default, this is true.
--]]
function LocalSaveData.SetAllowOverwriteOfFailedLoad(self: LocalSaveData, Value: boolean): ()
    --No implementation.
end

--[[
Returns if the data loaded successfully. If an error
occured (such as a DataStore failure), false is returned.
--]]
function LocalSaveData.DataLoadedSuccessfully(self: LocalSaveData): boolean
    return false
end

--[[
Flushes the data to the DataStore. If the data failed
to initialize and SetAllowOverwriteOfFailedLoad was not set
to true, no data will be flushed to prevent overwriting data.
--]]
function LocalSaveData.Flush(self: LocalSaveData): ()
    --No implementation.
end

--[[
Lists the keys in the save data.
--]]
function LocalSaveData.ListKeys(self: LocalSaveData): {string}
    local Keys = {}
    for Key, _ in self.Data do
        table.insert(Keys, Key)
    end
    return Keys
end

--[[
Returns the stored value for a given key.
--]]
function LocalSaveData.Get(self: LocalSaveData, Key: string): any
    return self.Data[Key]
end

--[[
Sets the stored value for a given key.
--]]
function LocalSaveData.Set(self: LocalSaveData, Key: string, Value: any): ()
    --Ignore the set if the value is the same.
    if Value == self:Get(Key) then
        return
    end

    --Set the value and fire the changed event if it exists.
    self.Data[Key] = Value
    if self.OnUpdateEvents[Key] then
        self.OnUpdateEvents[Key]:Fire(Value)
    end
end

--[[
Increments the stored value for a given key.
--]]
function LocalSaveData.Increment(self: LocalSaveData, Key: string, Value: number): ()
    self:Set(Key, (self.Data[Key] or 0) + Value)
end

--[[
Updates and saves (flushes) the changed values for the key
or keys. If multiple keys are given, the update function
will pass the old values and expect the new values to be returned.
This is intended for important updates that need to happen together.
--]]
function LocalSaveData.Update<T...>(self: LocalSaveData, Keys: string | {string}, UpdateFunction: (T...) -> (T...))
    --Convert the key to a table.
    if type(Keys) == "string" then
        Keys = {Keys}
    end

    --Get the current values.
    local Values = {} :: {any}
    for i, Key in Keys :: {string} do
        Values[i] = self:Get(Key)
    end

    --Update the values.
    local NewValues = table.pack(UpdateFunction(table.unpack(Values)))
    for i, Key in Keys :: {string} do
        self:Set(Key, NewValues[i])
    end
end

--[[
Invokes the given callback when the value for a given
key changes. Returns the connection to disconnect the
changes.
--]]
function LocalSaveData.OnUpdate(self: LocalSaveData, Key: string, Callback: (any) -> ()): RBXScriptConnection
    --Create the event.
    if not self.OnUpdateEvents[Key] then
        self.OnUpdateEvents[Key] = Instance.new("BindableEvent")
    end

    --Connect the event.
    return self.OnUpdateEvents[Key].Event:Connect(Callback)
end

--[[
Disconnects the events.
--]]
function LocalSaveData.Disconnect(self: LocalSaveData): ()
    for _, Event in self.OnUpdateEvents do
        Event:Destroy()
    end
    self.OnUpdateEvents = {}
end

return LocalSaveData


--Handles saving data for a DataStore key.
--!strict

local HttpService = game:GetService("HttpService")

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

local SaveData = {}
SaveData.__index = SaveData

export type SaveData = {
    DataStoreName: string,
    DataStoreKey: string,
    MessagingServiceKey: string,
    DataStoreService: DataStoreService,
    MessagingService: BulkMessagingService.BulkMessagingService,
    Connected: boolean,
    AutoSaveDelay: number,
    MessagingServiceBufferTime: number,
    AllowOverwriteOfFailedLoad: boolean,
    SendDataChangeUpdates: boolean,
    DataLoadSuccessful: boolean,
    Data: {[string]: any},
    PendingUpdates: {{Keys: {string}, UpdateFunction: (any) -> (any)}},
    KeysPendingFetchUpdates: {string},
    OnUpdateEvents: {[string]: BindableEvent},
    SyncId: string,
    LastKeyUpdateMessageTimes: {[string]: number},
    QueuedKeyUpdateMessages: {[string]: boolean},
    DataStore: DataStore?,
    MessagingServiceListenEvent: RBXScriptConnection?,
} & typeof(setmetatable({},SaveData ))

--[[
Creates the save data instance.
--]]
function SaveData.new(DataStoreName: string, Key: string, DataStoreService: DataStoreService, BulkMessagingService: BulkMessagingService.BulkMessagingService): SaveData
    --Create the object.
    local self = setmetatable({
        DataStoreName = DataStoreName,
        DataStoreKey = Key,
        MessagingServiceKey = `NSD_{Key}`,
        DataStoreService = DataStoreService,
        MessagingService = BulkMessagingService,
        Connected = true,
        AutoSaveDelay = 10,
        MessagingServiceBufferTime = 10,
        AllowOverwriteOfFailedLoad = false,
        SendDataChangeUpdates = true,
        DataLoadSuccessful = false,
        Data = {},
        PendingUpdates = {},
        KeysPendingFetchUpdates = {},
        OnUpdateEvents = {},
        SyncId = HttpService:GenerateGUID(),
        LastKeyUpdateMessageTimes = {},
        QueuedKeyUpdateMessages = {},
    }, SaveData) :: SaveData

    --Initialize the data.
    self:Initialize()

    --Return the object.
    return self
end

--[[
Initializes the data and connections.
--]]
function SaveData.Initialize(self: SaveData): ()
    --Connect the messaging service.
    xpcall(function()
        self.MessagingServiceListenEvent = self.MessagingService:SubscribeAsync(self.MessagingServiceKey, function(Message)
            self:HandleRemoteChange(HttpService:JSONDecode(Message.Data))
        end)
    end, function(ErrorMessage: string): ()
        warn(`Failed to subscribe to changes for {self.MessagingServiceKey} because {ErrorMessage}`)
    end)

    --Get the DataStore.
    local Worked, DataStore = pcall(function()
        return self.DataStoreService:GetDataStore(self.DataStoreName)
    end)
    if not Worked then
        error(`Failed to get DataStore for {self.DataStoreName} because {DataStore}`)
        return
    end
    self.DataStore = DataStore

    --Load the data.
    xpcall(function()
        self.Data = DataStore:GetAsync(self.DataStoreKey) or {}
        self.DataLoadSuccessful = true
    end, function(ErrorMessage: string): ()
        warn(`Failed to get data from {self.DataStoreName} -> {self.DataStoreKey} because {ErrorMessage}`)
    end)
end

--[[
Starts flushing the data in the background.
--]]
function SaveData.StartBackgroundFlushing(self: SaveData): ()
    task.spawn(function()
        while self.Connected do
            task.wait(self.AutoSaveDelay)
            pcall(function()
                if self.Connected then
                    self:Flush()
                end
            end)
        end
    end)
end

--[[
Sets if messages are sent when a key is
updated or if the data needs to be re-fetched (i.e. string
or entry is >1000 characters). By default, this is true.
--]]
function SaveData.SetSendDataChangeUpdates(self: SaveData, Value: boolean): ()
    self.SendDataChangeUpdates = Value
end

--[[
Sets if data can be overwriten if the loading
of data failed. This should be kept true if player data
is involved. By default, this is true.
--]]
function SaveData.SetAllowOverwriteOfFailedLoad(self: SaveData, Value: boolean): ()
    self.AllowOverwriteOfFailedLoad = Value
end

--[[
Returns if the data loaded successfully. If an error
occured (such as a DataStore failure), false is returned.
--]]
function SaveData.DataLoadedSuccessfully(self: SaveData): boolean
    return self.DataLoadSuccessful
end

--[[
Returns if an object is below the limit of sending
through the messaging service.
--]]
function SaveData.CanSendObject(self: SaveData, Object: any): boolean
    --This looks weird because the limit is by data, not by message length.
    --1 JSONEncode is required to convert the message to a string, then another
    --JSONEncode is required for the BulkMessagingService. The last JSONEncode
    --seems to be done internally for escaping characters.
    return string.len(HttpService:JSONEncode(HttpService:JSONEncode(HttpService:JSONEncode(Object)))) <= 800
end

--[[
Publishes a change to the messaging service.
--]]
function SaveData.PublishChange(self: SaveData, Object: any): ()
    if self.SendDataChangeUpdates then
        self.MessagingService:PublishAsync(self.MessagingServiceKey, HttpService:JSONEncode(Object))
    end
end

--[[
Publishes a change to the messaging service in the background.
--]]
function SaveData.PublishChangeBackground(self: SaveData, Object: any): ()
    if self.SendDataChangeUpdates then
        task.spawn(function()
            xpcall(function()
                local Key = Object.Key
                if Object.Action == "Set" and Key and self.MessagingServiceBufferTime > 0 then
                    if not self.LastKeyUpdateMessageTimes[Key] or tick() - self.LastKeyUpdateMessageTimes[Key] >= self.MessagingServiceBufferTime then
                        self.LastKeyUpdateMessageTimes[Key] = tick()
                        self:PublishChange(Object)
                    elseif not self.QueuedKeyUpdateMessages[Object.Key] then
                        self.QueuedKeyUpdateMessages[Key] = true
                        task.delay(self.MessagingServiceBufferTime - (tick() - self.LastKeyUpdateMessageTimes[Key]), function()
                            self.QueuedKeyUpdateMessages[Key] = nil
                            self:PublishChangeBackground({Action = "Set", Key = Key, Value = self:Get(Key), SyncId = self.SyncId})
                        end)
                    end
                else
                    self:PublishChange(Object)
                end
            end, function(ErrorMessage: string): ()
                warn(`Failed to publish change for {self.MessagingServiceKey} because {ErrorMessage}`)
            end)
        end)
    end
end

--[[
Internally sets a value. Invokes OnUpdate callbacks if they
are connected.
--]]
function SaveData.InternalSet(self: SaveData, Key: string, Value: any): ()
    --Set the value.
    local ExistingValue = self.Data[Key]
    self.Data[Key] = Value

    --Invoke the OnUpdate connections.
    if Value ~= ExistingValue and self.OnUpdateEvents[Key] then
        self.OnUpdateEvents[Key]:Fire(Value)
    end
end

--[[
Handles remote changes being invoked by the messaging service.
--]]
function SaveData.HandleRemoteChange(self: SaveData, Object: any): ()
    --Ignore remote changes if the DataStore didn't load.
    if not self.DataStore then
        return
    end

    if Object.Action and Object.SyncId ~= self.SyncId then
        if Object.Action == "Fetch" then
            --Fetch data that was too long to send.
            xpcall(function()
                local NewData = self.DataStore:GetAsync(self.DataStoreKey) or {}
                for _, Key in Object.Keys do
                    self:InternalSet(Key, NewData[Key])
                end
            end, function(ErrorMessage: string): ()
                warn(`Failed to fetch changes from {self.DataStoreName} -> {self.DataStoreKey} because {ErrorMessage}`)
            end)
        elseif Object.Action == "Set" then
            --Set the value.
            self:InternalSet(Object.Key, Object.Value)

            --Add the update function.
            table.insert(self.PendingUpdates, {
                Keys = {Object.Key},
                UpdateFunction = function(OldValue)
                    return self.Data[Object.Key]
                end,
            })
        end
    end
end

--[[
Flushes the data to the DataStore. If the data failed
to initialize and SetAllowOverwriteOfFailedLoad was not set
to true, no data will be flushed to prevent overwriting data.
--]]
function SaveData.Flush(self: SaveData): ()
    --Return if there is no data to change.
    if #self.PendingUpdates == 0 and #self.KeysPendingFetchUpdates == 0 then
        return
    end

    --Warn if the data can't be saved.
    if not self.DataStore then
        warn(`DataStore did not initialize correctly; unable to save data for {self.DataStoreName} -> {self.DataStoreKey}`)
        return
    end
    if not self:DataLoadedSuccessfully() and not self.AllowOverwriteOfFailedLoad then
        warn(`The data failed to load; refusing to save data for {self.DataStoreName} -> {self.DataStoreKey}`)
        return
    end

    --Update the data.
    if #self.PendingUpdates > 0 then
        self.DataStore:UpdateAsync(self.DataStoreKey, function(OldData: any): any
            OldData = OldData or {}

            --Invoke the update methods.
            for _, PendingUpdate in self.PendingUpdates do
                local Keys, UpdateFunction = PendingUpdate.Keys, PendingUpdate.UpdateFunction

                --Get the values of the keys.
                local Values = {}
                for i, Key in Keys do
                    Values[i] = self.Data[Key]
                end

                --Update the values.
                local NewValues = table.pack(UpdateFunction(table.unpack(Values)))
                for i, Key in Keys do
                    local Value = NewValues[i]
                    self:InternalSet(Key, Value)
                    OldData[Key] = Value
                end
            end

            --Clear the update methods.
            self.PendingUpdates = {}

            --Set the other keys.
            for Key, Value in OldData do
                self:InternalSet(Key, Value)
            end

            --Return the modified data.
            return OldData
        end)
    end

    --Invoke the update message.
    if #self.KeysPendingFetchUpdates > 0 then
        xpcall(function()
            self:PublishChange({Action = "Fetch", Keys = self.KeysPendingFetchUpdates, SyncId = self.SyncId})
            self.KeysPendingFetchUpdates = {}
        end, function(ErrorMessage: string): ()
            warn("Failed to publish change for "..tostring(self.MessagingServiceKey).." because "..tostring(ErrorMessage))
        end)
    end
end

--[[
Lists the keys in the save data.
--]]
function SaveData.ListKeys(self: SaveData): {string}
    local Keys = {}
    for Key, _ in self.Data do
        table.insert(Keys, Key)
    end
    return Keys
end

--[[
Returns the stored value for a given key.
--]]
function SaveData.Get(self: SaveData, Key: string): any
    return self.Data[Key]
end

--[[
Sets the stored value for a given key.
--]]
function SaveData.Set(self: SaveData, Key: string, Value: any): ()
    --Ignore the set if the value is the same.
    if Value == self:Get(Key) and typeof(Value) ~= "table" then
        return
    end

    --Set the value.
    self:InternalSet(Key, Value)

    --Invoke the change object.
    local ActionObject = {Action = "Set", Key = Key, Value = Value, SyncId = self.SyncId}
    if self:CanSendObject(ActionObject) then
        self:PublishChangeBackground(ActionObject)
    else
        table.insert(self.KeysPendingFetchUpdates, Key)
    end

    --Add the update function.
    table.insert(self.PendingUpdates, {
        Keys = {Key},
        UpdateFunction = function(OldValue: any): any
            return self.Data[Key]
        end,
    })
end

--[[
Increments the stored value for a given key.
--]]
function SaveData.Increment(self: SaveData, Key: string, Value: number): ()
    self:Set(Key, (self.Data[Key] or 0) + Value)
end

--[[
Updates and saves (flushes) the changed values for the key
or keys. If multiple keys are given, the update function
will pass the old values and expect the new values to be returned.
This is intended for important updates that need to happen together.
--]]
function SaveData.Update<T...>(self: SaveData, Keys: string | {string}, UpdateFunction: (T...) -> (T...))
    --Convert the key to a table.
    if type(Keys) == "string" then
        Keys = {Keys}
    end

    --Add the keys to fetch.
    for _, Key in Keys :: {string} do
        table.insert(self.KeysPendingFetchUpdates, Key)
    end

    --Add the update function.
    table.insert(self.PendingUpdates, {
        Keys = Keys,
        UpdateFunction = UpdateFunction,
    } :: any)

    --Flush the data.
    self:Flush()
end

--[[
Invokes the given callback when the value for a given
key changes. Returns the connection to disconnect the
changes.
--]]
function SaveData.OnUpdate(self: SaveData, Key: string, Callback: (any) -> ()): RBXScriptConnection
    --Create the event.
    if not self.OnUpdateEvents[Key] then
        self.OnUpdateEvents[Key] = Instance.new("BindableEvent")
    end

    --Connect the event.
    return self.OnUpdateEvents[Key].Event:Connect(Callback)
end

--[[
Disconnects the events.
--]]
function SaveData.Disconnect(self: SaveData): ()
    self.Connected = false

    --Disconnect the OnUpdates.
    for _, Event in self.OnUpdateEvents do
        Event:Destroy()
    end
    self.OnUpdateEvents = {}

    --Disconnect the messaging service.
    if self.MessagingServiceListenEvent then
        self.MessagingServiceListenEvent:Disconnect()
        self.MessagingServiceListenEvent = nil :: any
    end

    --Flush the data.
    self:Flush()
end

return SaveData


type Constructor<T, A...> = (self: T, A...) -> ()
type Class<T, P, A...> = P & { new: (A...) -> T & P }

--- An utility function to create lua-style classes by settings metatables.
return function<T, P, A...>(constructor: Constructor<T, A...>, prototype: P?): Class<T, P, A...>
	local proto = prototype or {}
	local mt = { __index = proto }

	local function new(...: A...)
		local self = (setmetatable({}, mt) :: any) :: T

		constructor(self, ...)

		return self
	end

	(proto :: any).new = new

	return proto :: any
end


local Types = require(script.Parent.Parent.types)
local Logger = require(script.Parent.Parent.internal.logger)

local function getSingletons(self: Types.InternalLifecycle)
	return self.__singletons
end

local function invokeAll(self: Types.InternalLifecycle, ...: any)
	for _, entry in self.__singletons do
		local method = entry.singleton[entry.fn]

		task.spawn(method, entry.singleton, ...)
	end
end

local function invokeAllSameThread(self: Types.InternalLifecycle, ...: any)
	for _, entry in self.__singletons do
		local method = entry.singleton[entry.fn]

		method(entry.singleton, ...)
	end
end

return function<T>(data: T & Types.CustomLifecycleData): T
	if not data.defaultMethod then
		Logger.critical("a lifecycle is missing the `defaultMethod` field!")
	end

	local dataCopy = (data :: any) :: Types.InternalLifecycle

	dataCopy.__type = "Quebec.Lifecycle"
	dataCopy.__singletons = {}

	dataCopy.getSingletons = getSingletons
	dataCopy.invokeAll = invokeAll
	dataCopy.invokeAllSameThread = invokeAllSameThread

	return dataCopy :: T
end


local RunService = game:GetService("RunService")

local root = script.Parent.Parent

local Types = require(root.types)
local Logger = require(root.internal.logger)
local Registry = require(root.internal.registry)

local isServer = RunService:IsServer()

return function(singletonFolder: Folder, config: Types.FrameworkConfiguration?)
	if config then
		if config.debug ~= nil then
			Logger.enableDebug = config.debug
		end

		if config.info ~= nil then
			Logger.enableInfo = config.info
		end
	end

	Logger.info("running Quebec %s", require(root.version))

	Registry.initialize(if config and config.instances then #config.instances else 1)

	Logger.debug("registry initialized")

	-- Requiring all the singletons once for them to register via Quebec.singleton
	for _, instance in singletonFolder:GetChildren() do
		if instance:IsA("ModuleScript") then
			require(instance)
		end
	end

	Registry.markReady()

	-- Don't connect lifecycles more than one time
	if not Registry.assignMaster() then
		return
	end

	-- Wait for all existing instances to register their singletons
	Registry.onAllReady(function()
		for _, entry in Registry.queryAll() do
			if entry.init and type(entry.init) == "function" then
				entry:init()
			end
		end

		Logger.debug("singleton initialisation done")

		for _, entry in Registry.queryLifecycleHandlers("start") do
			task.spawn(function()
				entry.singleton[entry.method](entry.singleton)
			end)
		end

		Logger.debug("%q lifecycle invoked on all registered singletons", "start")

		if isServer then
			game:BindToClose(function()
				for _, entry in Registry.queryLifecycleHandlers("stop") do
					entry.singleton[entry.method](entry.singleton)
				end

				Logger.debug("%q lifecycle invoked on all registered singletons", "stop")
			end)
		end

		RunService.Heartbeat:Connect(function(dt)
			for _, entry in Registry.queryLifecycleHandlers("onTick") do
				task.spawn(entry.singleton[entry.method], entry.singleton, dt)
			end
		end)

		RunService.Stepped:Connect(function(t, dt)
			for _, entry in Registry.queryLifecycleHandlers("onPhysics") do
				task.spawn(entry.singleton[entry.method], entry.singleton, t, dt)
			end
		end)

		if not isServer then
			RunService.RenderStepped:Connect(function(dt)
				for _, entry in Registry.queryLifecycleHandlers("onRender") do
					task.spawn(entry.singleton[entry.method], entry.singleton, dt)
				end
			end)
		end

		Logger.debug("connected lifecycles")

		for _, custom in Registry.queryCustomLifecycles() do
			task.spawn(custom.run, custom)
		end

		Logger.debug("connected custom lifecycles")
	end)
end


local root = script.Parent.Parent

local Types = require(root.types)
local Registry = require(root.internal.registry)

local function getCustomLifecycle(k, v): (boolean, Types.InternalLifecycle?, string?)
	local lifecycle = nil :: Types.InternalLifecycle?
	local methodName = nil :: string?

	if typeof(k) == "table" and k.__type == "Quebec.Lifecycle" then
		lifecycle = k
		methodName = v
	elseif typeof(v) == "table" and v.__type == "Quebec.Lifecycle" then
		lifecycle = v :: Types.InternalLifecycle

		methodName = lifecycle.defaultMethod
	else
		return false
	end

	return true, lifecycle, methodName
end

return function<T>(singleton: Types.AsSingleton<T>): T
	local internal = singleton :: Types.AsInternalSingleton<T>

	Registry.addSingleton(internal)

	if internal.lifecycles then
		for k, v in internal.lifecycles do
			local isCustom, lifecycle, methodName = getCustomLifecycle(k, v)

			if not isCustom then
				continue
			end

			table.insert(lifecycle.__singletons, {
				singleton = singleton,
				fn = methodName,
			})

			if not lifecycle.__registered then
				lifecycle.__registered = true

				Registry.addCustomLifecycle(lifecycle)
			end
		end
	end

	return singleton :: any
end


-- Re-exported types

local Types = require(script.types)

export type CustomLifecycle<Data> = Types.CustomLifecycle<Data>

-- Main

return {
	singleton = require(script.core.singleton),
	run = require(script.core.run),
	lifecycle = require(script.core.lifecycle),

	version = require(script.version),

	modules = script.modules,

	util = {
		class = require(script.core.class),
	},
}


local RunService = game:GetService("RunService")

local Levels = {
	Information = "inf",
	Debug = "dbg",
	Warning = "wrn",
	Error = "err",
	CriticalError = "cri",
}

local Platform = if RunService:IsClient() then "client" else "server"

local Logger = {
	enableDebug = RunService:IsStudio(),
	enableInfo = true,
}

function Logger.log(level: string, func, fmt: string, ...)
	func(`[ quebec {level} - {Platform} ] {string.format(fmt, ...)}`)
end

function Logger.info(fmt: string, ...)
	if not Logger.enableInfo then
		return
	end

	Logger.log(Levels.Information, print, fmt, ...)
end

function Logger.debug(fmt: string, ...)
	if not Logger.enableDebug then
		return
	end

	Logger.log(Levels.Debug, print, fmt, ...)
end

function Logger.warn(fmt: string, ...)
	Logger.log(Levels.Warning, warn, fmt, ...)
end

function Logger.error(fmt: string, ...)
	pcall(function(...)
		Logger.log(Levels.Error, error, fmt, ...)
	end, ...)
end

function Logger.critical(fmt: string, ...)
	Logger.log(Levels.CriticalError, error, fmt, ...)
end

return Logger


local root = script.Parent.Parent

local Types = require(root.types)

type InternalData = {
	singletons: { Types.Singleton },
	lifecycleHandlers: { [Types.Lifecycle]: { { singleton: Types.Singleton, method: string } } },
	customLifecycles: { Types.InternalLifecycle },
	instanceCount: number,
	instancesReady: number,
	masterAssigned: boolean,
	readyCallback: (() -> unknown)?,
}

local Registry = {
	data = (nil :: any) :: InternalData?, -- Workaround for unrelated types. Please forgive me
}

function Registry.initialize(instanceCount: number)
	if not shared.__QuebecInternalData then
		shared.__QuebecInternalData = {
			singletons = {},
			lifecycleHandlers = {},
			customLifecycles = {},
			instanceCount = 0,
			instancesReady = 0,
			masterAssigned = false,
			readyCallback = nil,
		} :: InternalData
	end

	if not Registry.data then
		Registry.data = shared.__QuebecInternalData
	end

	Registry.data.instanceCount = instanceCount
end

function Registry.addSingleton<T>(singleton: Types.AsInternalSingleton<T>)
	table.insert(Registry.data.singletons, singleton)

	if singleton.lifecycles then
		for k, v in singleton.lifecycles do
			local method = v
			local lifecycle = if type(k) == "number" then v else k
			local t = Registry.data.lifecycleHandlers[lifecycle]

			if not t then
				Registry.data.lifecycleHandlers[lifecycle] = { { method = method, singleton = singleton } }
			else
				table.insert(t, { method = method, singleton = singleton })
			end
		end
	end
end

function Registry.addCustomLifecycle(lifecycle: Types.InternalLifecycle)
	table.insert(Registry.data.customLifecycles, lifecycle)
end

function Registry.queryAll(): { Types.Singleton }
	return Registry.data.singletons
end

function Registry.queryLifecycleHandlers(lifecycle: Types.Lifecycle): { { method: string, singleton: Types.Singleton } }
	return Registry.data.lifecycleHandlers[lifecycle] or {}
end

function Registry.queryCustomLifecycles(): { Types.InternalLifecycle }
	return Registry.data.customLifecycles
end

function Registry.assignMaster(): boolean
	if not Registry.data.masterAssigned then
		Registry.data.masterAssigned = true

		return true
	else
		return false
	end
end

function Registry.onAllReady(cb)
	if Registry.data.readyCallback ~= nil then
		return
	end

	if Registry.data.instanceCount == Registry.data.instancesReady then
		task.spawn(cb)
	else
		Registry.data.readyCallback = cb
	end
end

function Registry.markReady()
	Registry.data.instancesReady += 1

	if Registry.data.instanceCount == Registry.data.instancesReady and Registry.data.readyCallback ~= nil then
		task.spawn(Registry.data.readyCallback)
	end
end

return Registry


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

export type Channel<Args..., Returns...> = {
	instance: BindableFunction,

	invoke: (self: Channel<Args..., Returns...>, Args...) -> Returns...,
	set: (self: Channel<Args..., Returns...>, callback: (Args...) -> Returns...) -> (),
}

local Folder = (function()
	local parent = script

	if RunService:IsServer() then
		parent = ServerStorage
	end

	local instance = Instance.new("Folder")

	instance.Name = HttpService:GenerateGUID()
	instance.Parent = parent

	return instance
end)()

local Channel = {}

Channel.__index = Channel

function Channel.new<Args..., Returns...>(name: string): Channel<Args..., Returns...>
	local self = (setmetatable({}, Channel) :: any) :: Channel<Args...>

	local instance = Folder:FindFirstChild(name)

	if not instance then
		instance = Instance.new("BindableFunction")

		instance.Name = name
		instance.Parent = Folder
	end

	self.instance = instance

	return self
end

function Channel.invoke<Args..., Returns...>(self: Channel<Args...>, ...: Args...): Returns...
	return self.instance:Invoke(...)
end

function Channel.set<Args..., Returns...>(self: Channel<Args...>, callback: (Args...) -> ())
	self.instance.OnInvoke = callback
end

return Channel


local FolderName = "__QBC_EV"

local RunService = game:GetService("RunService")

local Logger = require(script.Parent.Parent.internal.logger)

local event = require(script.event)
local eventTypes = require(script.event.types)

local func = require(script.func)
local funcTypes = require(script.func.types)

-- Re-export some types for quick access

export type Event<Args...> = eventTypes.Event<Args...>
export type Func<Args..., Returns...> = funcTypes.Func<Args..., Returns...>

-- Custom types

export type UntypedEvent = Event<...any>
export type UntypedFunc = Func<...any, ...any>

local Networking = {}

local NetworkingFolder = (function()
	if RunService:IsClient() then
		local folder = script:WaitForChild(FolderName, 5)

		if not folder then
			Logger.warn("unable to find networking folder. did you forget to handle the events on the server?")
			Logger.warn("creating client-sided networking folder")

			folder = Instance.new("Folder")

			folder.Name = FolderName
			folder.Parent = script
		end

		return folder
	else
		local folder = script:FindFirstChild(FolderName)

		if folder == nil then
			folder = Instance.new("Folder")

			folder.Name = FolderName
			folder.Parent = script
		end

		return folder
	end
end)() :: Folder

function Networking.event(name: string): UntypedEvent
	local instance = nil

	if RunService:IsClient() then
		instance = NetworkingFolder:WaitForChild(name)
	else
		instance = NetworkingFolder:FindFirstChild(name)

		if not instance then
			instance = Instance.new("RemoteEvent")

			instance.Name = name
			instance.Parent = NetworkingFolder
		end
	end

	if not instance then
		Logger.critical("unable to setup event %q, instance not created nor found", name)
	end

	return event(instance) :: UntypedEvent
end

function Networking.func(name: string): UntypedFunc
	local instance = nil

	if RunService:IsClient() then
		instance = NetworkingFolder:WaitForChild(name)
	else
		instance = NetworkingFolder:FindFirstChild(name)

		if not instance then
			instance = Instance.new("RemoteFunction")

			instance.Name = name
			instance.Parent = NetworkingFolder
		end
	end

	if not instance then
		Logger.critical("unable to setup function %q, instance not created nor found", name)
	end

	return func(instance) :: UntypedFunc
end

return Networking


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

export type Signal<Args...> = {
	instance: BindableEvent,

	fire: (self: Signal<Args...>, Args...) -> (),
	connect: (self: Signal<Args...>, callback: (Args...) -> ()) -> RBXScriptConnection,
}

local Folder = (function()
	local parent = script

	if RunService:IsServer() then
		parent = ServerStorage
	end

	local instance = Instance.new("Folder")

	instance.Name = HttpService:GenerateGUID()
	instance.Parent = parent

	return instance
end)()

local Signal = {}

Signal.__index = Signal

function Signal.new<Args...>(name: string): Signal<Args...>
	local self = (setmetatable({}, Signal) :: any) :: Signal<Args...>

	local instance = Folder:FindFirstChild(name)

	if not instance then
		instance = Instance.new("BindableEvent")

		instance.Name = name
		instance.Parent = Folder
	end

	self.instance = instance

	return self
end

function Signal.fire<Args...>(self: Signal<Args...>, ...: Args...)
	self.instance:Fire(...)
end

function Signal.connect<Args...>(self: Signal<Args...>, callback: (Args...) -> ())
	return self.instance.Event:Connect(callback)
end

return Signal


-- Configuration

export type FrameworkConfiguration = {
	--- A table defining how many instances are there.
	--- It's only required if you have more than one instance.
	instances: { string }?,

	--- Defaults to false in production, but true in studio. Enabling this will enable debug prints.
	debug: boolean?,

	--- Defaults to true. Enabling this will enable info prints.
	info: boolean?,
}

-- Custom Lifecycle

export type CustomLifecycleData = {
	--- The default method name of the lifecycle.
	defaultMethod: string,
}

export type CustomLifecycle<Data> = Data & {
	--- Returns a list of all singleton entries that have attached to this lifecycle.
	getSingletons: (self: CustomLifecycle<Data>) -> { { singleton: Singleton, fn: string } },

	--- Invokes the current lifecycle on all registered singletons in a new thread with the provided arguments.
	invokeAll: (self: CustomLifecycle<Data>, ...any) -> (),

	--- Invokes the current lifecycle on all registered singletons with the provided arguments.
	---
	--- Note: This function will yield, because it does not utilize `task.spawn` like `invokeAll` does.
	invokeAllSameThread: (self: CustomLifecycle<Data>, ...any) -> (),

	run: ((self: CustomLifecycle<Data>) -> unknown)?,
	onSingletonAdded: ((self: CustomLifecycle<Data>, singleton: Singleton) -> unknown)?,
}

export type InternalLifecycle = CustomLifecycle<CustomLifecycleData & {
	__singletons: { { singleton: Singleton, fn: string } },
	__registered: boolean?,
}>

-- Singleton

export type Lifecycle = "start" | "stop" | "onTick" | "onPhysics" | "onRender"

--- Type defining a Quebec singleton.
export type Singleton = {
	--- Singleton-specific data. Quebec will never access this field.
	data: any?,

	--- Optional lifecycle definitions of thae singleton. These are required if you want to use a lifecycle.
	lifecycles: { [string | number]: string }?,

	--- Optional method used to do early setup when nothing is running.
	init: ((self: Singleton) -> unknown)?,
}

--- Type reserved for internal fields.
export type InternalSingleton = {}

export type AsSingleton<T> = T & Singleton

export type AsInternalSingleton<T> = AsSingleton<T> & InternalSingleton

return {}


local Plugin = script:FindFirstAncestorWhichIsA("Plugin")
local Fusion = require(Plugin:FindFirstChild("Fusion", true))

local Hydrate = Fusion.Hydrate

local COMPONENT_ONLY_PROPERTIES = {
	"Name",
}

type ToolbarProperties = {
	Name: string,
	[any]: any,
}

return function(props: ToolbarProperties): PluginToolbar
	local newToolbar = Plugin:CreateToolbar(props.Name)

	local hydrateProps = table.clone(props)
	for _,propertyName in pairs(COMPONENT_ONLY_PROPERTIES) do
		hydrateProps[propertyName] = nil
	end

	return Hydrate(newToolbar)(hydrateProps)
end