--!native
--!strict
local ReplicatedFirst = game:GetService("ReplicatedFirst")

local Types = require(game:GetService("ReplicatedFirst"):WaitForChild("EngineShared"):WaitForChild("EngineTypes"))
local LoggerModule = require(ReplicatedFirst:WaitForChild("EngineShared"):WaitForChild("Logger"))
local RunnerLogger = LoggerModule.new("VEngine::ClientRunner", false, 4)

local EngineRequire = require(ReplicatedFirst.EngineShared.EngineRequire)
local EngineEnvironmentManager = require(ReplicatedFirst.EngineShared.EngineEnvironment)

RunnerLogger:PrintInformation("Installing exception handler...")
do
	local exceptionHandlerLogger = LoggerModule.new("VEngine::Client::UnhandledExceptionHandler", false, nil)
	game:GetService("ScriptContext").Error:Connect(function(message: string, stackTrace: string, scrpt: Instance)
		exceptionHandlerLogger:PrintError("WARNING! -- Unhandled exception has been caught!")
		exceptionHandlerLogger:PrintError(
			("Exception Message: '%s'; Script: '%s'; IsNilParent: '%s'; IsActor: '%s'"):format(
				message,
				scrpt and scrpt:GetFullName() or "No script Instance?",
				(not scrpt and "Invalid Script Instance") or (scrpt and scrpt.Parent and "Yes" or "No"),
				scrpt and scrpt:GetActor() and "Yes" or "No"
			)
		)
		exceptionHandlerLogger:PrintError(("Stack Trace: '%s'"):format(stackTrace))
	end)
end

--[[
	ReplicatedFirst scripts must run before us. Wait until we are signaled.
]]
RunnerLogger:PrintInformation("Waiting for the preload phase to complete...")
while not ReplicatedFirst:GetAttribute("Loaded__Preload") do
	task.wait()
end
RunnerLogger:PrintInformation("Preload phase completed! Beginning VEngine::Client")

RunnerLogger:PrintInformation("Phase 1/2: Building Shared...")
do
	local totalModuleCount = 0
	local erroredModules = 0
	local initializedModules = 0
	local loadedModules = {}
	local compatibleSharedModules = {}

	RunnerLogger:PrintInformation("Composing modules compatible with the BaseEngineModule signature...")
	for _, moduleScript in ReplicatedFirst.Shared:GetDescendants() do
		if moduleScript:IsA("ModuleScript") then
			task.spawn(function()
				local m: Types.BaseEngineModule? = EngineRequire.protected(moduleScript)

				-- m is assumed to be a BaseEngineModule script, and it is also assumed it complies with its constraints, but, just to be sure, we validate it.

				if typeof(m) ~= "table" or typeof(m.ModuleName) ~= "string" or typeof(m.Initialize) ~= "function" then
					return -- Not a BaseEngineModule compatible module.
				end

				if compatibleSharedModules[m.ModuleName] then
					-- Same module name.
					RunnerLogger:PrintError(
						`Cowardly refusing to initialize a module that has the same VEngineModule::name field as this leads to undefined behaviour.\n\nVEngine Modules in conflict:\n\n- '{moduleScript:GetFullName()}' loaded by VEngine as {m.ModuleName}\n- '{compatibleSharedModules[m.ModuleName]:GetFullName()}' loaded by VEngine as {m.ModuleName}`
					)
					return
				end
				compatibleSharedModules[m.ModuleName] = moduleScript

				table.insert(loadedModules, m)

				EngineEnvironmentManager:PushEngineModule(m)
				totalModuleCount += 1
			end)
		end
	end

	RunnerLogger:PrintInformation("Modules composed. Executing Pre-Initialization routine...")

	for _, moduleScript in loadedModules do
		print(`- Shared/{moduleScript.ModuleName}`)
	end

	-- Pollute their environment with the Hooked functions and a custom VEngine logger.
	for _, module in loadedModules do
		for _, func in module do
			if typeof(func) == "function" then
				local l =
					LoggerModule.new(string.format("VEngine::SharedModule::Client::'%s'", module.ModuleName), false, 4)
				l:PolluteEnvironment(func :: () -> ())
			end
		end
	end

	for _, requiredModule in loadedModules do
		RunnerLogger:PrintInformation(
			string.format("Running preinitialization routine for 'Shared/%s')...", requiredModule.ModuleName)
		)

		task.spawn(function()
			local _, msg = pcall(function()
				debug.setmemorycategory("Shared/" .. requiredModule.ModuleName)
				task.synchronize()
				requiredModule:PreInitialize(EngineEnvironmentManager) -- Env table cannot be modified either way; it is frozen.
				task.synchronize()
				debug.resetmemorycategory()
			end)

			if msg then
				erroredModules += 1
				RunnerLogger:PrintError(
					string.format(
						"Failed to preinitialize VEngine module 'Shared/%s');\nError: %s",
						requiredModule.ModuleName,
						msg
					)
				)
			else
				initializedModules += 1
			end
		end)
	end

	RunnerLogger:PrintInformation("Waiting for all Pre-Initialization scripts to complete...")

	repeat
		task.wait()
	until initializedModules == totalModuleCount
	initializedModules = 0

	RunnerLogger:PrintInformation("Modules Pre-Initialized. Executing Initialization routine...")

	for _, requiredModule in loadedModules do
		RunnerLogger:PrintInformation(
			string.format("Running initialization routine for 'Shared/%s')...", requiredModule.ModuleName)
		)

		task.spawn(function()
			local _, msg = pcall(function()
				debug.setmemorycategory("Shared/" .. requiredModule.ModuleName)
				task.synchronize()
				requiredModule:Initialize(EngineEnvironmentManager) -- Env table cannot be modified either way; it is frozen.
				task.synchronize()
				debug.resetmemorycategory()
			end)

			if msg then
				erroredModules += 1
				RunnerLogger:PrintError(
					string.format(
						"Failed to initialize VEngine module 'Shared/%s');\nError: %s",
						requiredModule.ModuleName,
						msg
					)
				)
			else
				initializedModules += 1
			end
		end)
	end

	RunnerLogger:PrintInformation("Shared modules initialization completed!")
end

RunnerLogger:PrintInformation("Phase 2/2: Building Client-Exclusive...")
do
	local clientScripts = ReplicatedFirst:WaitForChild("ClientCore")

	local erroredModules = 0
	local initializedModules = 0
	local totalModuleCount = 0
	local loadedModules = {}
	local foundModules = {}

	RunnerLogger:PrintInformation("Requiring Client-Exclusive Modules...")
	for _, moduleScript in clientScripts:GetDescendants() do
		if moduleScript:IsA("ModuleScript") then
			task.spawn(function()
				local m: Types.BaseEngineModule? = EngineRequire.protected(moduleScript)

				-- m is assumed to be a BaseEngineModule script, and it is also assumed it complies with its constraints, but, just to be sure, we validate it.

				if typeof(m) ~= "table" or typeof(m.ModuleName) ~= "string" or typeof(m.Initialize) ~= "function" then
					RunnerLogger:PrintError(
						"Cowardly refusing to initialize a module that does not comply with the BaseEngineModule definition!"
					)
					return
				end

				if foundModules[m.ModuleName] then
					-- Same module name.
					RunnerLogger:PrintError(
						`Cowardly refusing to initialize a module that has the same VEngineModule::name field as this leads to undefined behaviour.\n\nVEngine Modules in conflict:\n\n- '{moduleScript:GetFullName()}' loaded by VEngine as {m.ModuleName}\n- '{foundModules[m.ModuleName]:GetFullName()}' loaded by VEngine as {m.ModuleName}`
					)
					return
				end
				foundModules[m.ModuleName] = moduleScript
				table.insert(loadedModules, m)

				EngineEnvironmentManager:PushEngineModule(m)
				totalModuleCount += 1
			end)
		end
	end

	for _, module in EngineEnvironmentManager:GetLoadedModules() do
		for _, func in module do
			if typeof(func) == "function" then
				local l = LoggerModule.new(string.format("VEngine::Module::Client::'%s'", module.ModuleName), false, 4)
				l:PolluteEnvironment(func :: () -> ())
			end
		end
	end

	RunnerLogger:PrintInformation("Executing Client Exclusive preinitialization scripts:")
	for _, moduleScript in loadedModules do
		print(`- Client/{moduleScript.ModuleName}`)
	end

	for _, requiredModule in loadedModules do
		RunnerLogger:PrintInformation(
			string.format("Running preinitialization routine for 'Client/%s')...", requiredModule.ModuleName)
		)

		task.spawn(function()
			local _, msg = pcall(function()
				debug.setmemorycategory("Client/" .. requiredModule.ModuleName)
				task.synchronize()
				requiredModule:PreInitialize(EngineEnvironmentManager) -- Env table cannot be modified either way; it is frozen.
				task.synchronize()
				debug.resetmemorycategory()
			end)

			if msg then
				erroredModules += 1
				RunnerLogger:PrintError(
					string.format(
						"Failed to preinitialize VEngine module 'Client/%s');\nError: %s",
						requiredModule.ModuleName,
						msg
					)
				)
			else
				initializedModules += 1
			end
		end)
	end

	RunnerLogger:PrintInformation("Waiting for all Pre-Initialization scripts to complete...")

	repeat
		task.wait()
	until initializedModules == totalModuleCount
	initializedModules = 0

	for _, requiredModule in loadedModules do
		RunnerLogger:PrintInformation(
			string.format("Running initialization routine for 'Client/%s')...", requiredModule.ModuleName)
		)

		task.spawn(function()
			local _, msg = pcall(function()
				debug.setmemorycategory("Client/" .. requiredModule.ModuleName)
				task.synchronize()
				requiredModule:Initialize(EngineEnvironmentManager) -- Env table cannot be modified either way; it is frozen.
				task.synchronize()
				debug.resetmemorycategory()
			end)

			if msg then
				erroredModules += 1
				RunnerLogger:PrintError(
					string.format(
						"Failed to initialize VEngine module 'Client/%s');\nError: %s",
						requiredModule.ModuleName,
						msg
					)
				)
			else
				initializedModules += 1
			end
		end)
	end
end


-- No check required due to require(module_)
--!native
--!nocheck

--[[
	ReplicatedFirst modules are not meant to have dependencies, and therefore we won't push the modules we load into the environment,
	nor will we use the EngineEnvironmentManager at all on this section.
]]

local oPrint = print
local oWarn = warn
local function print(str, ...)
	oPrint("[VEngine::ReplicatedFirst] " .. (#{ ... } == 0 and str or string.format(str, ...)))
end

local function warn(str, ...)
	oWarn("[VEngine::ReplicatedFirst] " .. (#{ ... } == 0 and str or string.format(str, ...)))
end

print("Waiting for scripts...")
local scripts = game:GetService("ReplicatedFirst"):WaitForChild("Preinit")
print("Executing pre-initialization scripts:")

local initializedModules = 0
local moduleCount = 0

for _, moduleScript in pairs(scripts:GetDescendants()) do
	if moduleScript:IsA("ModuleScript") then
		oPrint(`- ReplicatedFirst/Game/{moduleScript.Name}`)
		moduleCount += 1
	end
end

for _, moduleScript in scripts:GetDescendants() do
	if moduleScript:IsA("ModuleScript") then
		local function __run_protected_require(module: ModuleScript)
			if typeof(module) ~= "Instance" or not module:IsA("ModuleScript") then
				error(string.format("Cannot require %s as it is not a ModuleScript.", tostring(module)))
			end

			local success, ret: any = pcall(function(module_: ModuleScript)
				return require(module_)
			end, module)

			if not success then
				error(
					string.format(
						"Failed to require module %s! Error received: '%s'",
						module:GetFullName(),
						tostring(ret)
					)
				)
				return nil
			end

			return ret
		end

		local m = __run_protected_require(moduleScript)

		if typeof(m) ~= "table" or typeof(m.ModuleName) ~= "string" or typeof(m.Initialize) ~= "function" then
			warn("Cowardly refusing to initialize a module that does not comply with the BaseEngineModule definition!")
			continue
		end

		print(
			string.format(
				"Running initialization routine for %s (Module: ReplicatedFirst/Game/%s)...",
				m.ModuleName,
				moduleScript.Name
			)
		)

		task.spawn(function()
			local _, msg = pcall(function()
				debug.setmemorycategory("ReplicatedFirst/" .. m.ModuleName)
				task.synchronize()
				m:Initialize(nil)
				task.synchronize()
				debug.resetmemorycategory()
			end)

			if msg then
				warn(
					string.format(
						"Failed to initialize VEngine module '%s' (Module: ReplicatedFirst/Game/%s);\nError: %s",
						m.ModuleName,
						moduleScript.Name,
						msg
					)
				)
			else
				initializedModules += 1
			end
		end)
	end
end

while moduleCount ~= initializedModules do
	task.wait()
end

game:GetService("ReplicatedFirst"):SetAttribute("Loaded__Preload", true)


--!strict
--!native

local ReplicatedFirst = game:GetService("ReplicatedFirst")
local ServerStorage = game:GetService("ServerStorage")

local Types = require(ReplicatedFirst.EngineShared.EngineTypes)
local LoggerModule = require(ReplicatedFirst.EngineShared.Logger)
local RunnerLogger = LoggerModule.new("VEngine::ServerRunner", false, 4)

local EngineRequire = require(ReplicatedFirst.EngineShared.EngineRequire)
local EngineEnvironmentManager = require(ReplicatedFirst.EngineShared.EngineEnvironment)

do
	local exceptionHandlerLogger = LoggerModule.new("VEngine::Server::UnhandledExceptionHandler", false, nil)
	game:GetService("ScriptContext").Error:Connect(function(message: string, stackTrace: string, scrpt: Instance)
		exceptionHandlerLogger:PrintError("WARNING! -- Unhandled exception has been caught!")
		exceptionHandlerLogger:PrintError(
			("Exception Message: '%s'; Script: '%s'; IsNilParent: '%s'; IsActor: '%s'"):format(
				message,
				scrpt and scrpt:GetFullName() or "No script Instance?",
				(not scrpt and "Invalid Script Instance") or (scrpt and scrpt.Parent and "Yes" or "No"),
				scrpt and scrpt:GetActor() and "Yes" or "No"
			)
		)
		exceptionHandlerLogger:PrintError(("Stack Trace: '%s'"):format(stackTrace))
	end)
end

RunnerLogger:PrintInformation("Beginning VEngine::Server")

RunnerLogger:PrintInformation("Phase 1/2: Building Shared...")
do
	local totalModuleCount = 0
	local erroredModules = 0
	local initializedModules = 0
	local loadedModules = {}
	local compatibleSharedModules = {}

	RunnerLogger:PrintInformation("Composing modules compatible with the BaseEngineModule signature...")
	for _, moduleScript in ReplicatedFirst.Shared:GetDescendants() do
		if moduleScript:IsA("ModuleScript") then
			task.spawn(function()
				local m: Types.BaseEngineModule? = EngineRequire.protected(moduleScript)

				-- m is assumed to be a BaseEngineModule script, and it is also assumed it complies with its constraints, but, just to be sure, we validate it.

				if typeof(m) ~= "table" or typeof(m.ModuleName) ~= "string" or typeof(m.Initialize) ~= "function" then
					return -- Not a BaseEngineModule compatible module.
				end

				if compatibleSharedModules[m.ModuleName] then
					-- Same module name.
					RunnerLogger:PrintError(
						`Cowardly refusing to initialize a module that has the same VEngineModule::name field as this leads to undefined behaviour.\n\nVEngine Modules in conflict:\n\n- '{moduleScript:GetFullName()}' loaded by VEngine as {m.ModuleName}\n- '{compatibleSharedModules[m.ModuleName]:GetFullName()}' loaded by VEngine as {m.ModuleName}`
					)
					return
				end
				compatibleSharedModules[m.ModuleName] = moduleScript

				table.insert(loadedModules, m)

				EngineEnvironmentManager:PushEngineModule(m)
				totalModuleCount += 1
			end)
		end
	end

	RunnerLogger:PrintInformation("Modules composed. Executing Pre-Initialization routine...")

	for _, moduleScript in loadedModules do
		print(`- Shared/{moduleScript.ModuleName}`)
	end

	-- Pollute their environment with the Hooked functions and a custom VEngine logger.
	for _, module in loadedModules do
		for _, func in module do
			if typeof(func) == "function" then
				local l =
					LoggerModule.new(string.format("VEngine::SharedModule::Server::'%s'", module.ModuleName), false, 4)
				l:PolluteEnvironment(func :: () -> ())
			end
		end
	end

	for _, requiredModule in loadedModules do
		RunnerLogger:PrintInformation(
			string.format("Running preinitialization routine for 'Shared/%s')...", requiredModule.ModuleName)
		)

		task.spawn(function()
			local _, msg = pcall(function()
				debug.setmemorycategory("Shared/" .. requiredModule.ModuleName)
				task.synchronize()
				requiredModule:PreInitialize(EngineEnvironmentManager) -- Env table cannot be modified either way; it is frozen.
				task.synchronize()
				debug.resetmemorycategory()
			end)

			if msg then
				erroredModules += 1
				RunnerLogger:PrintError(
					string.format(
						"Failed to preinitialize VEngine module 'Shared/%s');\nError: %s",
						requiredModule.ModuleName,
						msg
					)
				)
			else
				initializedModules += 1
			end
		end)
	end

	RunnerLogger:PrintInformation("Waiting for all Pre-Initialization scripts to complete...")

	repeat
		task.wait()
	until initializedModules == totalModuleCount
	initializedModules = 0

	RunnerLogger:PrintInformation("Modules Pre-Initialized. Executing Initialization routine...")

	for _, requiredModule in loadedModules do
		RunnerLogger:PrintInformation(
			string.format("Running initialization routine for 'Shared/%s')...", requiredModule.ModuleName)
		)

		task.spawn(function()
			local _, msg = pcall(function()
				debug.setmemorycategory("Shared/" .. requiredModule.ModuleName)
				task.synchronize()
				requiredModule:Initialize(EngineEnvironmentManager) -- Env table cannot be modified either way; it is frozen.
				task.synchronize()
				debug.resetmemorycategory()
			end)

			if msg then
				erroredModules += 1
				RunnerLogger:PrintError(
					string.format(
						"Failed to initialize VEngine module 'Shared/%s');\nError: %s",
						requiredModule.ModuleName,
						msg
					)
				)
			else
				initializedModules += 1
			end
		end)
	end

	RunnerLogger:PrintInformation("Shared modules initialization completed!")
end

RunnerLogger:PrintInformation("Phase 2/2: Building Client-Exclusive...")
do
	local clientScripts = ServerStorage.GameCore

	local erroredModules = 0
	local initializedModules = 0
	local totalModuleCount = 0
	local loadedModules = {}
	local foundModules = {}

	RunnerLogger:PrintInformation("Requiring Client-Exclusive Modules...")
	for _, moduleScript in clientScripts:GetDescendants() do
		if moduleScript:IsA("ModuleScript") then
			task.spawn(function()
				local m: Types.BaseEngineModule? = EngineRequire.protected(moduleScript)

				-- m is assumed to be a BaseEngineModule script, and it is also assumed it complies with its constraints, but, just to be sure, we validate it.

				if typeof(m) ~= "table" or typeof(m.ModuleName) ~= "string" or typeof(m.Initialize) ~= "function" then
					RunnerLogger:PrintError(
						"Cowardly refusing to initialize a module that does not comply with the BaseEngineModule definition!"
					)
					return
				end

				if foundModules[m.ModuleName] then
					-- Same module name.
					RunnerLogger:PrintError(
						`Cowardly refusing to initialize a module that has the same VEngineModule::name field as this leads to undefined behaviour.\n\nVEngine Modules in conflict:\n\n- '{moduleScript:GetFullName()}' loaded by VEngine as {m.ModuleName}\n- '{foundModules[m.ModuleName]:GetFullName()}' loaded by VEngine as {m.ModuleName}`
					)
					return
				end
				foundModules[m.ModuleName] = moduleScript
				table.insert(loadedModules, m)

				EngineEnvironmentManager:PushEngineModule(m)
				totalModuleCount += 1
			end)
		end
	end

	for _, module in EngineEnvironmentManager:GetLoadedModules() do
		for _, func in module do
			if typeof(func) == "function" then
				local l = LoggerModule.new(string.format("VEngine::Module::Server::'%s'", module.ModuleName), false, 4)
				l:PolluteEnvironment(func :: () -> ())
			end
		end
	end

	RunnerLogger:PrintInformation("Executing Client Exclusive preinitialization scripts:")
	for _, moduleScript in loadedModules do
		print(`- Server/{moduleScript.ModuleName}`)
	end

	for _, requiredModule in loadedModules do
		RunnerLogger:PrintInformation(
			string.format("Running preinitialization routine for 'Server/%s')...", requiredModule.ModuleName)
		)

		task.spawn(function()
			local _, msg = pcall(function()
				debug.setmemorycategory("Server/" .. requiredModule.ModuleName)
				task.synchronize()
				requiredModule:PreInitialize(EngineEnvironmentManager) -- Env table cannot be modified either way; it is frozen.
				task.synchronize()
				debug.resetmemorycategory()
			end)

			if msg then
				erroredModules += 1
				RunnerLogger:PrintError(
					string.format(
						"Failed to preinitialize VEngine module 'Server/%s');\nError: %s",
						requiredModule.ModuleName,
						msg
					)
				)
			else
				initializedModules += 1
			end
		end)
	end

	RunnerLogger:PrintInformation("Waiting for all Pre-Initialization scripts to complete...")

	repeat
		task.wait()
	until initializedModules == totalModuleCount
	initializedModules = 0

	for _, requiredModule in loadedModules do
		RunnerLogger:PrintInformation(
			string.format("Running initialization routine for 'Server/%s')...", requiredModule.ModuleName)
		)

		task.spawn(function()
			local _, msg = pcall(function()
				debug.setmemorycategory("Server/" .. requiredModule.ModuleName)
				task.synchronize()
				requiredModule:Initialize(EngineEnvironmentManager) -- Env table cannot be modified either way; it is frozen.
				task.synchronize()
				debug.resetmemorycategory()
			end)

			if msg then
				erroredModules += 1
				RunnerLogger:PrintError(
					string.format(
						"Failed to initialize VEngine module 'Server/%s');\nError: %s",
						requiredModule.ModuleName,
						msg
					)
				)
			else
				initializedModules += 1
			end
		end)
	end
end


--!nocheck

local module =
	require(game:GetService("ReplicatedFirst"):WaitForChild("EngineShared"):WaitForChild("EngineModule")).new()
local Types = require(game:GetService("ReplicatedFirst"):WaitForChild("EngineShared"):WaitForChild("EngineTypes"))

local function getXor()
	local xorKey = os.time() + os.clock() / task.wait(0.03)

	local t = {}

	return setmetatable({}, {
		__index = function(_: { any }, idx)
			local xored = ""
			for _, s in pairs({ string.byte(idx, 1, #idx) }) do
				xored = xored .. string.char(bit32.bxor(s, xorKey) % 255)
			end

			return t[xored]
		end,
		__newindex = function(_: { any }, idx, v)
			local xored = ""
			for _, s in pairs({ string.byte(idx, 1, #idx) }) do
				xored = xored .. string.char(bit32.bxor(s, xorKey) % 255)
			end
			t[xored] = v

			return t[xored]
		end,
	})
end

local moduleData = getXor()

function module:Initialize(_: Types.EngineEnvironmentManager)
	moduleData["__scannerThreads"] = getXor()

	moduleData["__scannerThreads"]["__coregui"] = task.defer(function()
		local a = setmetatable({ newproxy(true), game.CoreGui }, { __mode = "v" })

		while task.wait() do
			if a[1] and not a[2] or not a[1] and a[2] then
				debug.info(1, "f")() -- Praise democracy!
			elseif not a[1] and not a[2] then
				a = setmetatable({ newproxy(true), game.CoreGui }, { __mode = "v" })
			end
		end
	end)

	moduleData["__scannerThreads"]["__virtualInputManager"] = task.defer(function()
		local a = setmetatable({ newproxy(true), game.VirtualInputManager }, { __mode = "v" })

		while task.wait() do
			if a[1] and not a[2] or not a[1] and a[2] then
				debug.info(1, "f")() -- Praise democracy!
			elseif not a[1] and not a[2] then
				a = setmetatable({ newproxy(true), game.CoreGui }, { __mode = "v" })
			end
		end
	end)
end

return module


--!strict
local ReplicatedFirst = game:GetService("ReplicatedFirst")
local TweenService = game:GetService("TweenService")

local LoggerModule = require(ReplicatedFirst.EngineShared.Logger)
local EngineTypes = require(game:GetService("ReplicatedFirst"):WaitForChild("EngineShared"):WaitForChild("EngineTypes"))

local moduleBaseConstructor = require(ReplicatedFirst:WaitForChild("EngineShared"):WaitForChild("EngineModule"))

local AudioManager: EngineTypes.EngineAudioManager = moduleBaseConstructor.new() :: EngineTypes.EngineAudioManager

local IsInitialized = false

local function InitializeIfUninitialized()
	if not IsInitialized then
		AudioManager:Initialize(require(ReplicatedFirst:WaitForChild("EngineShared"):WaitForChild("EngineEnvironment")))
	end
end

AudioManager.ModuleName = "Engine/EngineAudioManager"

function AudioManager:Initialize(_: EngineTypes.EngineEnvironmentManager)
	if IsInitialized then
		return -- Avoid Re-Initialization.
	end
	for _, func in AudioManager :: { () -> () } do
		if typeof(func) == "function" then
			local l =
				LoggerModule.new(string.format("VEngine::Module::Client::'%s'", AudioManager.ModuleName), false, 4)
			l:PolluteEnvironment(func)
		end
	end

	AudioManager.SfxFolder = Instance.new("Folder")
	AudioManager.SfxFolder.Parent = workspace
	AudioManager.SfxFolder.Name = "Sound__AudioManagerManaged"

	AudioManager.SoundCache = {} :: { [string]: Sound }
	IsInitialized = true
end

--- Obtain a sound from the AudioManager cache.
--- @param soundName string The name of the sound.
--- @return Sound? sound The sound that was cached. May be nil if it is not loaded.
function AudioManager:GetSound(soundName: string): Sound?
	InitializeIfUninitialized()
	task.synchronize()
	return self.SoundCache[soundName]
end

function AudioManager:FadeIn(
	soundName: string,
	targetVolume: number,
	fadeInTime: number,
	fadeEasing: Enum.EasingStyle
): RBXScriptSignal?
	InitializeIfUninitialized()
	if not soundName then
		error("No sound name provided.")
		return
	end
	if not targetVolume then
		targetVolume = 0.5
	end
	if not fadeInTime then
		fadeInTime = 0
	end
	if not fadeEasing then
		fadeEasing = Enum.EasingStyle.Sine
	end
	if not self.SoundCache[soundName] then
		error(
			`Cannot obtain sound {soundName} as the AudioManager does not acknowledge it as a loaded sound! Have you forgotten to initialize the AudioManager?`
		)
		return nil
	end

	local sound = self.SoundCache[soundName]
	sound:Stop()
	local t = TweenService:Create(sound, TweenInfo.new(fadeInTime, fadeEasing), {
		Volume = targetVolume,
	})
	sound.Volume = 0.00001
	sound:Play()
	task.wait()
	t:Play()
	return t.Completed
end

function AudioManager:FadeOut(soundName: string, fadeOutTime: number, fadeEasing: Enum.EasingStyle): RBXScriptSignal?
	InitializeIfUninitialized()
	if not soundName then
		error("No sound name provided.")
		return
	end
	if not fadeOutTime then
		fadeOutTime = 0
	end
	if not fadeEasing then
		fadeEasing = Enum.EasingStyle.Sine
	end
	if not self.SoundCache[soundName] then
		error(
			`Cannot obtain sound {soundName} as the AudioManager does not acknowledge it as a loaded sound! Have you forgotten to initialize the AudioManager?`
		)
		return nil
	end
	task.synchronize()

	local sound = self.SoundCache[soundName]
	local t = TweenService:Create(sound, TweenInfo.new(fadeOutTime, fadeEasing), {
		Volume = 0,
	})
	t:Play()
	t.Completed:Once(function(playbackState)
		if playbackState == Enum.PlaybackState.Completed then
			sound:Pause()
			sound.Volume = 0.5 -- Set volume to default.
		end
	end)
	return t.Completed
end

function AudioManager:GetSoundContainer(): Folder
	InitializeIfUninitialized()
	return self.SfxFolder
end

function AudioManager:StopAllSounds(exclusionList: { string })
	task.synchronize()
	for soundName, sound in self.SoundCache do
		if table.find(exclusionList, soundName) then
			continue
		end
		sound:Stop()
	end
end

function AudioManager:FadeOutAllSounds(exclusionList: { string })
	task.synchronize()
	for soundName, _ in self.SoundCache do
		if table.find(exclusionList, soundName) then
			continue
		end
		self:FadeOut(soundName, 1.5, Enum.EasingStyle.Sine)
	end
	task.wait(1.5)
end

function AudioManager:CreateSound(soundName: string, soundId: string): Sound
	InitializeIfUninitialized()

	if self.SoundCache[soundName] or self:GetSoundContainer():FindFirstChild(soundName) then
		if not self.SoundCache[soundName] then
			local sound = self:GetSoundContainer():FindFirstChild(soundName) :: Sound
			self.SoundCache[soundName] = sound
		end
		return self.SoundCache[soundName] -- Sound already created.
	end

	task.synchronize()
	local newSound = Instance.new("Sound")
	newSound.Name = soundName
	newSound.Parent = self:GetSoundContainer() -- Only one SFX Container should be ever present in-game.
	newSound.SoundId = soundId

	self.SoundCache[soundName] = newSound
	return self.SoundCache[soundName]
end

return AudioManager :: EngineTypes.EngineAudioManager


--!strict

local Types = require(game:GetService("ReplicatedFirst"):WaitForChild("EngineShared"):WaitForChild("EngineTypes"))

local b
b = {
	[""] = {
		Value = 0,
		Name = "",
		EnumType = b,
	},
} :: Types.EngineEnum

local enumBase = {}

function enumBase:CreateEnumItem(name: string, value: number, enumType: Types.EngineEnum): Types.EngineEnumItem
	return table.freeze({
		Name = name,
		Value = value,
		EnumType = enumType,
	}) :: Types.EngineEnumItem
end

return enumBase


--!strict

local DataStoreService = game:GetService("DataStoreService")
local PlayerService = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ReplicatedFirst = game:GetService("ReplicatedFirst")
local RunService = game:GetService("RunService")

local Red = require(ReplicatedStorage.ThirdPartyShared.Red)
local Types = require(game:GetService("ReplicatedFirst"):WaitForChild("EngineShared"):WaitForChild("EngineTypes"))

local EngineDatastoreValidation: Types.EngineDatastoreValidation = RunService:IsServer()
		and require(ReplicatedFirst.EngineShared.EngineDatastoreValidation) :: any
	or {} -- Clients cannot use this module.
local LoggerConstructor = require(ReplicatedFirst.EngineShared.Logger)

local logger = LoggerConstructor.new("EngineKeybinds", false, 5)

if RunService:IsClient() and not ReplicatedStorage:GetAttribute("KeybindsSystem_Initialized") then
	logger:PrintWarning(
		"WARNING: EngineKeybinds has been used on the client, but the server is yet to initialize it! This is a first time warning, a spin lock will now begin."
	)
	repeat
		task.wait(1)
	until ReplicatedStorage:GetAttribute("KeybindsSystem_Initialized")
end

local engineKeybindsModule = {}

if not ReplicatedFirst:WaitForChild("Shared"):WaitForChild("Enums"):WaitForChild("GameKeybinds", 5) then
	logger:PrintCritical("Failed to load enum database! EngineKeybinds will not be initialized!")
	return (
		{
			Initialize = function(_, _)
				error("Keybinds system not initialized")
			end,
			SetKeybind = function(_, _, _)
				error("Keybinds system not initialized")
			end,
			GetKeybind = function(_)
				error("Keybinds system not initialized")
			end,
		} :: any
	) :: Types.EngineKeybinds
else
	engineKeybindsModule.ExistingKeybinds =
		require(ReplicatedFirst:WaitForChild("Shared"):WaitForChild("Enums"):WaitForChild("GameKeybinds"))
end

local redEvents = {
	["set"] = Red.Function(
		"__engine_keybind__set",
		function(keybindEnum: any, keycode: any): (Types.EngineEnumItem?, Enum.KeyCode?)
			if typeof(keybindEnum) ~= "table" then
				logger:PrintWarning(
					"Attempted to call __engine_keybind_set function using an invalid type as the keybindEnum! Sending nil instead."
				)
				keybindEnum = nil
			end
			if typeof(keycode) ~= "EnumItem" then
				logger:PrintWarning(
					"Attempted to call __engine_keybind_set function using an invalid type as the keycode! Sending nil instead."
				)
				keycode = nil
			end
			return keybindEnum, keycode
		end,
		function(...: unknown)
			return ...
		end
	),
}

function engineKeybindsModule:GetKeybind(keybindEnum: Types.EngineEnumItem): Enum.KeyCode?
	local keybinds = PlayerService.LocalPlayer:WaitForChild("Keybinds")

	local keybindValue = keybinds:WaitForChild(keybindEnum.Name)

	if keybindValue and keybindValue:IsA("StringValue") then
		-- Map string value into Enum.

		for _, enumVal in Enum.KeyCode:GetEnumItems() do
			if enumVal.Name == keybindValue.Value then
				return enumVal -- Keycode found.
			end
		end
	end

	return nil -- No keycode matched.
end

function engineKeybindsModule:SetKeybind(keybindEnum: Types.EngineEnumItem, keycode: Enum.KeyCode)
	local success, ret = redEvents.set:Call(keybindEnum, keycode):Await()

	if not ret or not success then
		logger:PrintCritical("// Keybind system not initialized or internal system error //")
		error(nil)
		return
	end

	-- Operation successful.
end

-- #region Server binds
function engineKeybindsModule:Initialize()
	if RunService:IsServer() then
		local keybindDatastore = DataStoreService:GetDataStore("VEngineInternal/Keybinds")
		PlayerService.PlayerAdded:Connect(function(player: Player) -- TODO: Complete keybind system.
			-- Initialize keybinds folders
			local extracted: { [string]: Enum.KeyCode }
			local i = 0
			while i < 3 do
				local success, itemOrError = pcall(function(player_: Player)
					return keybindDatastore:GetAsync(tostring(player_.UserId))
				end, player)

				if success and itemOrError then
					extracted = itemOrError
					logger:PrintInformation("Data retrieval succeeded on " .. tostring(i) .. " attempts.")
					break
				elseif success and not itemOrError then
					logger:PrintInformation("Player has never saved keybinds before; Initializing from scratch!")
					break
				elseif not success then
					logger:PrintError(
						"DataStore retrieval failed with error '"
							.. tostring(itemOrError)
							.. "'. Attempt "
							.. tostring(i)
					)
				end
				i += 1
			end

			if not extracted then
				extracted = engineKeybindsModule.ExistingKeybinds.GetDefaultKeybinds()
			end

			local keyBinds = Instance.new("Configuration")
			keyBinds.Parent = player
			keyBinds.Name = "Keybinds"

			for indx, value in extracted do
				local strVal = Instance.new("StringValue")
				strVal.Parent = keyBinds
				strVal.Name = indx
				strVal.Value = typeof(value) == "EnumItem" and value.Name
					or typeof(value) == "string" and value
					or "Unknown" -- Assume it is a String
			end
		end)

		PlayerService.PlayerRemoving:Connect(function(player: Player)
			local keybinds = player:FindFirstChild("Keybinds")
			if not keybinds then
				logger:PrintCritical(
					"Player does not have keybinds in its Player object, was it removed by a third party script? "
				)
				return
			end

			-- Parse into table.
			local keybinds_t = {}

			for _, value in keybinds:GetChildren() do
				keybinds_t[value.Name] = (value :: StringValue).Value
			end

			local validationResult = EngineDatastoreValidation.IsValidForDatastore(keybinds_t, "table", 255)

			if validationResult ~= EngineDatastoreValidation.ValidationFailureReason.Success then
				logger:PrintCritical(
					"Validation engine failed with error: "
						.. tostring(validationResult)
						.. " this is not meant to happen, was invalid data placed into the keybind Value?"
				)
				return
			end

			local i = 0
			while i < 3 do
				local success, itemOrError = pcall(function(plr: Player)
					return keybindDatastore:SetAsync(tostring(plr.UserId), keybinds_t)
				end, player)

				if success then
					logger:PrintInformation("Data save succeeded on " .. tostring(i) .. " attempts.")
					break
				elseif not success then
					logger:PrintError(
						"DataStore save failed with error '" .. tostring(itemOrError) .. "'. Attempt " .. tostring(i)
					)
				end
				i += 1
			end
		end)

		redEvents.set:SetCallback(function(player: Player, keybindEnum: any, keycode: any)
			local keybindName = keybindEnum.Name
			local keybinds = player:FindFirstChild("Keybinds")

			if not keybinds then
				logger:PrintError("ERROR: Keybinds were not initialized correctly?")
				return nil
			end

			local keybindValue = keybinds:FindFirstChild(keybindName)

			if keybindValue and keybindValue:IsA("StringValue") then
				keybindValue.Value = keycode.Name
				return true
			end

			return nil
		end)

		ReplicatedStorage:SetAttribute("KeybindsSystem_Initialized", true)
	end
end
-- #endregion Server binds

return (engineKeybindsModule :: any) :: Types.EngineKeybinds


--!strict

local moduleConstructor = {}

local Types = require(game:GetService("ReplicatedFirst"):WaitForChild("EngineShared"):WaitForChild("EngineTypes"))

function moduleConstructor.new(): Types.BaseEngineModule
	--- The base of a module in VEngine, containing the minimum required methods and properties to initialize one.
	--- @class BaseEngineModule
	local module = {}

	module.ModuleName = "EngineModule"
	module.LoadOrder = 0

	function module:PreInitialize(_: Types.EngineEnvironmentManager)
		error(
			"This has to be overriden by you, the developer. You should override it to give your module functionality!"
		)
	end

	function module:Initialize(_: Types.EngineEnvironmentManager)
		print(
			"This has not been overriden by you, the developer. You should override it to give your module functionality!"
		)
	end

	return module :: Types.BaseEngineModule
end

return moduleConstructor


--!strict

local t = {}

local Types = require(game:GetService("ReplicatedFirst"):WaitForChild("EngineShared"):WaitForChild("EngineTypes"))

--- Require engine modules in a protected fashion. This is assumed to ONLY be used on Engine modules.
--- @param module ModuleScript The module script that will be required
--- @return BaseEngineModule BaseEngineModule the required module.
function t.protected(module: ModuleScript): Types.BaseEngineModule?
	if typeof(module) ~= "Instance" or not module:IsA("ModuleScript") then
		error(string.format("Cannot require %s as it is not a ModuleScript.", tostring(module)))
	end

	local success: boolean, ret: string | Types.BaseEngineModule = pcall(function(): Types.BaseEngineModule
		return require(module) :: Types.BaseEngineModule
	end)

	if not success or typeof(ret) == "string" then
		warn(string.format("Failed to require module %s! Error received: '%s'", module:GetFullName(), tostring(ret)))
		return nil
	end

	return ret
end

return t


--!strict
local module = {}

function module.DeepClone(t: { any }): { any }
	if typeof(t) ~= "table" then
		return {}
	end

	local nTable = {}

	for idx, member in t do
		if typeof(member) == "table" then
			nTable[idx] = module.DeepClone(member)
		else
			nTable[idx] = member
		end
	end

	return nTable
end

return module


--!strict

--[[
	Copyright 2023 Dottik - All Rights Reserved

	This script is a module of the State Machine.
	This module aims to represent a possible state on a state machine.
]]

local State = {}

local Types = require(game:GetService("ReplicatedFirst"):WaitForChild("EngineShared"):WaitForChild("EngineTypes"))

function State.new(stateName: string): Types.State
	--- @class State
	local self = {
		--- The name of the state.
		Name = stateName,
		--- Executes a step in this state.
		--- @param LoggerInstance Logger The logger instance associated with this State's currently executing state machine.
		--- @param stateMachineMemory table A snapshot of the memory of the State Machine executing this state.
		--- @return State nextState The next state; If nil the state machine should stop.
		ExecuteState = function(LoggerInstance: Types.Logger, stateMachineMemory: {}): Types.State?
			local _ = LoggerInstance
			local _ = stateMachineMemory
			return nil
		end,
	}

	return self :: Types.State
end

return State


--!strict

--[[
	Copyright 2023 Dottik - All Rights Reserved

	This script is a module of the State Machine.

	This module represents the main runtime of the State Machine.
]]

local ReplicatedFirst = game:GetService("ReplicatedFirst")

local LoggerModule = require(ReplicatedFirst:WaitForChild("EngineShared"):WaitForChild("Logger"))
local Types = require(game:GetService("ReplicatedFirst"):WaitForChild("EngineShared"):WaitForChild("EngineTypes"))
local EngineUtilities = require(ReplicatedFirst:WaitForChild("EngineShared"):WaitForChild("EngineUtilities"))

local module = {}

--- Creates a State Machine.
--- @param machineName string
--- @param states table The
--- @param enableDebugPrints boolean Debug prints
--- @return StateMachine createdStateMachine The state machine that was created with the given parameters.
function module.CreateStateMachine(
	machineName: string,
	states: { Types.State },
	enableDebugPrints: boolean
): Types.StateMachine
	--- @class StateMachine
	local __self = {
		--- The valid states of the state machine.
		StateMachineStates = states,
		--- Fired when the state changes.
		--- Invoked with parameter #1 as previous state and parameter #2 as the new state
		OnStateChanged = Instance.new("BindableEvent"),
		--- Fired when the State Machine exits.
		--- Invoked with parameter #1 as the last state before exiting.
		OnExit = Instance.new("BindableEvent"),
		--- Fired when the State Machine has obtained its initial state and is ready.
		Ready = Instance.new("BindableEvent"),
	} :: Types.StateMachine

	local internalState = {
		--- The current State.
		StateMachine_CurrentState = nil,
		--- A shared piece of memory between all states.
		StateMachineMemory = {},
		--- The thread that executs the State Machine.
		MachineThread = nil :: thread?,
		--- Simple getter to the Current State.
		GetCurrentState = function(self): Types.State
			return self.StateMachine_CurrentState
		end,
	} :: any

	--- Starts the machine and returns the Thread that is currently running the state machine.
	--- @return thread machineThread The machine thread
	function __self:StartMachine()
		internalState.MachineThread = task.spawn(function()
			local LoggerInstance = LoggerModule.new(("StateMachine_%s"):format(machineName), true)
			LoggerInstance:PrintInformation("Initialising State Machine.")
			LoggerInstance:PrintInformation("Found States: " .. #states)
			-- For each game tick we will tick the state machine once.
			if internalState.StateMachine_CurrentState == nil then
				for _, value in __self.StateMachineStates do
					if value.Name == "Start" or value.Name == "Idle" then
						-- Found the idle/starting state. Break out.
						LoggerInstance:PrintInformation("Found initial State Machine state!")
						internalState.StateMachine_CurrentState = value
						break
					end
				end
				if internalState.StateMachine_CurrentState == nil then
					LoggerInstance:PrintCritical(
						"[State Machine Step] Failure! Cannot find the initial State Machine state! Please name it something like 'Start' or 'Idle'!"
					)
					return
				end
			end

			__self.Ready:Fire()

			while task.wait() do -- Main Loop.
				local currentState = internalState:GetCurrentState()
				if currentState then
					if not table.find(__self.StateMachineStates, internalState:GetCurrentState()) then
						LoggerInstance:PrintCritical(
							"[State Machine Step] Cowardly refusing to transfer control to a State that has not been registered"
						)
					end

					local nextState = currentState.ExecuteState(LoggerInstance, internalState.StateMachineMemory)
					if not nextState then
						__self.OnExit:Fire(currentState)
						if enableDebugPrints then
							LoggerInstance:PrintWarning(
								("[State Machine Step] State '%s' requested the State machine to stop."):format(
									currentState.Name
								)
							)
						end
						break
					end
					if currentState ~= nextState then
						if enableDebugPrints then
							LoggerInstance:PrintInformation(
								("[State Machine Step] Transferring state control | %s --> %s"):format(
									currentState.Name,
									nextState.Name
								)
							)
						end
						__self.OnStateChanged:Fire(currentState, nextState)
						internalState.StateMachine_CurrentState = nextState
					end
				else
					__self.OnExit:Fire(currentState)
					LoggerInstance:PrintCritical(
						"[State Machine Step] Whoops, this is weird! The current state has not been set, and this has not been accounted for!"
					)
				end
			end
		end)
		return internalState.MachineThread
	end

	---	Stops the State Machine safely.
	--- @return boolean result If true, the State Machine was stopped correctly, else an error occurred
	function __self:StopMachine()
		local success = pcall(task.cancel, internalState.MachineThread)
		__self.OnExit:Fire(internalState.StateMachine_CurrentState)
		internalState.MachineThread = nil
		return success
	end

	--- Returns whether or not the State Machine is running
	--- @return boolean isStateMachineRunning Whether or not the state machine is running.
	function __self:IsStateMachineRunning()
		return not not internalState.MachineThread
	end

	---	Modifies the internal state of the state machine externally.
	--- @param newState table The new state you want to set the state machine to have.
	function __self:ModifyMemory(newState: {})
		local oldState = internalState.StateMachineMemory
		internalState.StateMachineMemory = EngineUtilities.DeepClone(newState)
		return oldState
	end

	--- Returns a deep clone of the State Machine's current memory
	--- @return table stateMachineMemory The memory of the state machine as a table.
	function __self:GetState()
		return EngineUtilities.DeepClone(internalState.StateMachineMemory)
	end

	--- Releases all resources and destroyed any instances attached to this State Machine instance.
	function __self:Dispose()
		__self:StopMachine()
		table.clear(internalState.StateMachineMemory)
		table.clear(internalState)
		table.clear(__self.StateMachineStates)
		__self.OnExit:Destroy()
		__self.OnStateChanged:Destroy()
		__self.Ready:Destroy()
	end

	return __self :: Types.StateMachine
end

return module


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

local engineKeybinds

local EngineTypes = require(ReplicatedFirst.EngineShared.EngineTypes)
local moduleBaseConstructor = require(ReplicatedFirst:WaitForChild("EngineShared"):WaitForChild("EngineModule"))

local module = moduleBaseConstructor.new()

module.ModuleName = "ClientInitialization"

function module:PreInitialize(_: EngineTypes.EngineEnvironmentManager)
	print("Pre-Initiaialization step...")
	engineKeybinds =
		require(ReplicatedFirst:WaitForChild("EngineShared"):WaitForChild("EngineKeybinds")) :: EngineTypes.EngineKeybinds
	engineKeybinds:Initialize()
end

function module:Initialize(_: EngineTypes.EngineEnvironmentManager) end

return module


--!strict
local Players = game:GetService("Players")
local ReplicatedFirst = game:GetService("ReplicatedFirst")

local EngineKeybinds = require(ReplicatedFirst.EngineShared.EngineKeybinds) :: any
local EngineTypes = require(ReplicatedFirst.EngineShared.EngineTypes)

local moduleBaseConstructor = require(ReplicatedFirst.EngineShared.EngineModule)

local module = moduleBaseConstructor.new()

module.ModuleName = "ServerInitialization"

function module:PreInitialize(_: EngineTypes.EngineEnvironmentManager)
	print("Pre-init server")
	EngineKeybinds:Initialize()
end

function module:Initialize(_: EngineTypes.EngineEnvironmentManager)
	print("Initializing server...")
end

return module


--!strict

local keybindMap

keybindMap = {
	Running = {
		Value = 1,
		Name = "Running",
		EnumType = keybindMap,
	},
	TiltRight = {
		Value = 2,
		Name = "TiltRight",
		EnumType = keybindMap,
	},
	TiltLeft = {
		Value = 3,
		Name = "TiltLeft",
		EnumType = keybindMap,
	},
}
function keybindMap.GetDefaultKeybinds()
	return {
		Running = Enum.KeyCode.LeftShift,
		TiltLeft = Enum.KeyCode.Q,
		TiltRight = Enum.KeyCode.E,
	}
end

return table.freeze(keybindMap)


--!strict

local ReplicatedFirst = game:GetService("ReplicatedFirst")

local EngineTypes = require(ReplicatedFirst.EngineShared.EngineTypes)
local ModuleBaseConstructor = require(ReplicatedFirst.EngineShared.EngineModule)

local module = ModuleBaseConstructor.new()

module.ModuleName = "Shared/Managers/ExampleManager"

function module:PreInitialize(_: EngineTypes.EngineEnvironmentManager)
	print("Hello, from preinit!")
end

function module:Initialize(_: EngineTypes.EngineEnvironmentManager)
	print("Hello, from init!")
end

return module


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

local GameTypes = require(ReplicatedFirst.Shared.GameTypes)

local struct = {} :: GameTypes.ExampleStructureFactory

function struct.new(ferris: string, foo: number): GameTypes.ExampleStructure
	return table.freeze({
		Ferris = ferris,
		Foo = foo,
	})
end

return table.freeze(struct)


--===========================================================================================================================>
--!strict
--===========================================================================================================================>

-- Wait for the Events folder:
local Events_Folder = script.Parent.Parent:WaitForChild('Events')

-- Cache reference both the ZoneId Add and ZoneId Remove RemoteEvents:
local ZoneId_Add    = Events_Folder:WaitForChild('ZoneId_Add')
local ZoneId_Remove = Events_Folder:WaitForChild('ZoneId_Remove')

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

-- If Streaming is not enabled in this place, then we dont need any server involvement with Ids:
if workspace.StreamingEnabled == false then
	ZoneId_Add:Destroy(); ZoneId_Remove:Destroy(); script:Destroy(); return;
end

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

-- Connect on the Server to the event to update the tags of the sent instances:
ZoneId_Add.OnServerEvent:Connect(function(Player: Player, Id: string, Parts: {BasePart}, Holders: {Instance})
	--======================================================================================================>

	-- Runtime type checking:
	if typeof(Id) ~= 'string'     then warn('ZoneId_Add: Id was not a string'); return end
	if typeof(Parts) ~= 'table'   then warn('ZoneId_Add: Parts was not a table'); return end
	if typeof(Holders) ~= 'table' then warn('ZoneId_Add: Parts was not a table'); return end

	-- Dynamically create the Client based Tags:
	local ZonePart_Tag = `{Id}:ZonePart:Client`;
	local Holder_Tag   = `{Id}:Holder:Client`;

	-- Loop through the ZoneParts to add tags:
	for Index: number, Holder: Instance in ipairs(Holders) do
		-- If the Part already has a Tag, then continue:
		-- Add the Holder tag to the Holder:
		if Holder:HasTag(Holder_Tag) then continue else Holder:AddTag(Holder_Tag)end
	end

	for Index: number, Part: BasePart in ipairs(Parts) do
		-- If the Part already has a Tag, then continue:
		-- Add the ZonePart tag to the ZonePart:
		if Part:HasTag(ZonePart_Tag) then continue else Part:AddTag(ZonePart_Tag) end
	end
	--======================================================================================================>
end)

-- Connect on the Server to the event to update the tags of the sent instances:
ZoneId_Remove.OnServerEvent:Connect(function(Player: Player, Id: string, Parts: {BasePart}, Holders: {Instance})
	--======================================================================================================>

	-- Runtime type checking:
	if typeof(Id) ~= 'string'     then warn('ZoneId_Add: Id was not a string'); return end
	if typeof(Parts) ~= 'table'   then warn('ZoneId_Add: Parts was not a table'); return end
	if typeof(Holders) ~= 'table' then warn('ZoneId_Add: Parts was not a table'); return end

	-- Dynamically create the Client based Tags:
	local ZonePart_Tag = `{Id}:ZonePart:Client`;
	local Holder_Tag   = `{Id}:Holder:Client`;

	-- Loop through the ZoneParts to add tags:
	for Index: number, Holder: Instance in ipairs(Holders) do
		-- If the Part already has a Tag, then continue:
		-- Add the Holder tag to the Holder:
		if Holder:HasTag(Holder_Tag) then continue else Holder:RemoveTag(Holder_Tag)end
	end

	for Index: number, Part: BasePart in ipairs(Parts) do
		-- If the Part already has a Tag, then continue:
		-- Add the ZonePart tag to the ZonePart:
		if Part:HasTag(ZonePart_Tag) then continue else Part:RemoveTag(ZonePart_Tag) end
	end
	--======================================================================================================>
end)

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


--!strict

local function DO_NOTHING() end

export type Connection<A...> = {
	Connected:				boolean,
	Disconnect:				(self: Connection<A...>) -> (),
	_function:				(A...) -> (),
	_next:					Connection<A...>?,
	_previous:				Connection<A...>,
	_signal:				Signal<A...>,
}

export type Signal<A...> = {
	Connections:			number,
	OnConnectionChanged:     ()->(),
	new: 					() -> Signal<A...>,
	Fire: 					(self: Signal<A...>, A...) -> (),
	Wait:					(self: Signal<A...>) -> A...,
	Once:					(self: Signal<A...>, func: (A...) -> ()) -> Connection<A...>,
	Connect: 				(self: Signal<A...>, func: (A...) -> ()) -> Connection<A...>,
	DisconnectAll: 			(self: Signal<A...>) -> (),
	Destroy:				(self: Signal<A...>) -> (),
	_next:					Connection<A...>?,
}

local threads: {thread} = {}

local function Call<A...>(func: (A...) -> (), thread: thread, ...: A...): ()
	func(...)
	table.insert(threads, thread)
end

local function Yield(): ()
	while true do Call(coroutine.yield()) end
end

local Signal, Connection = {}, {}
Signal.__index, Connection.__index = Signal, Connection

local function Disconnect<A...>(self: Connection<A...>): ()
	if self.Connected then self.Connected = false else return end
	local next, previous = self._next, self._previous
	if next then next._previous = previous end
	self._signal.Connections -= 1
	self._signal.OnConnectionChanged()
	previous._next = next
end

local function New<A...>(): Signal<A...>
	return (setmetatable({Connections = 0; OnConnectionChanged = DO_NOTHING}, Signal) :: any) :: Signal<A...>
end

local function Fire<A...>(self: Signal<A...>, ...: A...): ()
	local link = self._next
	while link do
		local length, thread = #threads, nil
		if length == 0 then
			thread = coroutine.create(Yield)
			coroutine.resume(thread)
		else
			thread = threads[length]
			threads[length] = nil
		end
		task.spawn(thread, link._function, thread, ...)
		link = link._next
	end
end

local function Connect<A...>(self: Signal<A...>, func: (A...) -> ()): Connection<A...>
	local next = self._next
	local link = {Connected = true, _previous = self, _signal = self, _function = func, _next = next} :: any
	if next ~= nil then next._previous = link end
	self.Connections += 1
	self.OnConnectionChanged()
	self._next = link
	return setmetatable(link, Connection) :: Connection<A...>
end

local function Once<A...>(self: Signal<A...>, func: (A...) -> ()): Connection<A...>
	local connection
	connection = Connect(self, function(...)
		Disconnect(connection)
		func(...)
	end)
	return connection
end

local function Wait<A...>(self: Signal<A...>): A...
	local thread, connection = coroutine.running(), nil
	connection = Connect(self, function(...)
		Disconnect(connection)
		if coroutine.status(thread) == "suspended" then task.spawn(thread, ...) end
	end)
	return coroutine.yield()
end

local function DisconnectAll<A...>(self: Signal<A...>): ()
	local link = self._next
	while link do
		link.Connected = false
		link = link._next
	end
	self._next = nil
	self.Connections = 0
end

local function Destroy<A...>(self: Signal<A...>): ()
	self:DisconnectAll()
	self.OnConnectionChanged = DO_NOTHING
end

Signal.new = New
Signal.New = New
Signal.Fire = Fire
Signal.Wait = Wait
Signal.Once = Once
Signal.Connect = Connect
Signal.DisconnectAll = DisconnectAll

Signal.Destroy = Destroy

Connection.Disconnect = Disconnect

return Signal


--===========================================================================================================================>
--!optimize 2
--!strict
--===========================================================================================================================>

-- Set Memory Category:
debug.setmemorycategory("[Zone]: Groups")

--[=[
	@class Groups
	@private

	Manages **Zone Groups** within a Zoner system.
	Each Group keeps a fast in-memory dictionary of Zone IDs (attributes on a `Zones` folder) so lookups like
	`IsZoneInGroup` are O(1).

	**Design (Module-as-Table, shared per-VM)**
	- Exposes a **module table** that caches group data.
	- Because it’s a table, any script that `require`s it **within the same VM** shares the same state (an in-VM singleton).
	- Multiple modules in the same VM can read group properties from this shared cache without walking the DataModel.

	**VM Registration (Actors)**
	- Register this module inside **each** Zoner Actor VM: `require` it and call `InitializeActorListeners` with that VM’s `ZonerFolder`.
	- Each VM keeps its **own synchronized copy** of group data, automatically updated by attribute-change signals on that VM’s `ZonerFolder`.
	- This keeps groups and their properties up to date **in every Actor VM**, making property access fast and straightforward.

	**Lifecycle**
	- Call `InitializeActorListeners` once (per VM) with the root `ZonerFolder` to index existing groups and attach listeners.
	- Later, call `DeinitializeActorListeners` to disconnect signals and clear all cached data.

	**Data Flow**
	- Group folders live under `ZonerFolder.Groups`.
	- Each group contains a `Zones` `Configuration` whose attributes are Zone IDs with boolean values (`true` or `nil`).
	- Attribute changes on that `Zones` configuration drive the internal `Zones` map for the group.

	**Threading**
	- Uses `RBXScriptConnection` signals; none of the public APIs yield.

	@since 0.9.9
]=]
local Groups = {}

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

-- [ Require Types: ]

-- This will inject all types into this context.
local ZoneInstances = require(`../Types/ZoneInstances`)

--[=[
	@interface ZoneGroupData
	@within Groups
	@field Zones { [string]: true? } --Map of ZoneId -> true (present) or nil (absent) for O(1) membership checks.
	@field ZonesFolder Configuration --Reference to the group's `Zones` folder (source of truth via attributes).
	@field Properties ZoneGroupProperties --Snapshot of group properties from `GetAttributes()` on the group.
	@field AttributeChanged RBXScriptConnection --Connection tracking attribute changes on `ZonesFolder`.

	@since 0.9.9
]=]
export type ZoneGroupData = {
	Zones: { [string]: true? },
	ZonesFolder: Configuration,
	Properties: ZoneGroupProperties,
	AttributeChanged: RBXScriptConnection,
}

--[=[
	@interface ZoneGroupProperties
	@within Groups
	@field Exclusive boolean --Whether tracked items can be in more than one zone of this group simultaneously.

	@since 0.9.8
]=]
export type ZoneGroupProperties = {
	Exclusive: boolean
};

--[=[
	@type ZoneGroups {[string]: ZoneGroupData}
	@within Groups
	Map of group name -> `ZoneGroupData`.
]=]
export type ZoneGroups = {
	[string]: ZoneGroupData,
}

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

--[=[
	@prop ZoneGroups ZoneGroups
	@within Groups
	Holds all known groups keyed by group name. Populated by `InitializeActorListeners`. Cleared by `DeinitializeActorListeners`.
]=]
Groups.ZoneGroups       = {} :: { [string]: ZoneGroupData }
--[=[
	@prop ZoneGroupAdded RBXScriptConnection?
	@within Groups
	Connection for when a group is added under `ZonerFolder.Groups`. Established by `InitializeActorListeners`.
]=]
Groups.ZoneGroupAdded   = nil :: RBXScriptConnection?
--[=[
	@prop ZoneGroupRemoved RBXScriptConnection?
	@within Groups
	Connection for when a group is removed from `ZonerFolder.Groups`. Established by `InitializeActorListeners`.
]=]
Groups.ZoneGroupRemoved = nil :: RBXScriptConnection?
--[=[
	@prop ActorInitialized boolean
	@within Groups
	Whether this Groups singleton has been initialized by the VM that required it.
]=]
Groups.ActorInitialized = false :: boolean

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

--[=[
	@private
	@within Groups
	@since 0.9.9
	@param Group Types.GroupsFolderGroup The groups-folder child representing a single zone group.
	Initializes a group's cached data and begins listening to its `Zones` attribute changes.
]=]
local function SetupGroup(Group: ZoneInstances.GroupsFolderGroup)
	--===========================================================================>

	-- Store a local reference to the created dictionary so that we can have it as the only captured local variable:
	local GroupData: ZoneGroupData

	-- Create the GroupData dictionary in the Groups table:
	Groups.ZoneGroups[Group.Name] = {
		-- Create a dictionary for the ZoneIds that are registered with this Group:
		Zones = Group.Zones:GetAttributes() or {} :: any,
		-- Store a reference to the ZonesFolder so that we can access it internally:
		ZonesFolder = Group.Zones,
		-- Store the dictionary of properties related to this Group:
		Properties = Group:GetAttributes() :: ZoneGroupProperties,
		-- Connect a AttributeChanged event to the ZonerFolder so that we can update our internal table whenever Zones are added or removed from the Group:
		AttributeChanged = Group.Zones.AttributeChanged:Connect(function(ZoneId: string)
			-- Update the internal Zones table. If the attribute returned on the ZonesFolder is nil, it clears the ZoneId reference, if its not, it set it to true.
			-- Remembering the ZonesFolder has attributes for ZoneIds that are either true or nil:
			GroupData.Zones[ZoneId] = GroupData.ZonesFolder:GetAttribute(ZoneId) :: true?
		end),
	}

	-- Set local reference to the GroupData Dictionary:
	GroupData = Groups.ZoneGroups[Group.Name]

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

--[=[
	@private
	@within Groups
	@since 0.9.9
	@param Group Types.GroupsFolderGroup --The groups-folder child to tear down.
	Releases listeners, clears cached tables, and removes the group from `ZoneGroups`.
]=]
local function RemoveGroup(Group: ZoneInstances.GroupsFolderGroup)
	--===========================================================================>

	-- Reference the GroupData Dictionary:
	local GroupData: ZoneGroupData = Groups.ZoneGroups[Group.Name]
	-- If no GroupData could be indexed, warn:
	if not GroupData then
		warn(`[Zoner]:// No group by the name of "{Group.Name}" was able to be removed`)
		return
	end

	-- Clear up the AttributeChanged signal connection:
	if GroupData.AttributeChanged then
		GroupData.AttributeChanged:Disconnect()
		GroupData.AttributeChanged = nil :: any
	end

	-- Clear the Zones table:
	table.clear(GroupData.Zones)
	-- Clear the data table:
	table.clear(GroupData)

	-- Clear the Group from the main Groups table:
	Groups.ZoneGroups[Group.Name] = nil :: any

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

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

--[=[
	@function InitializeActorListeners
	@within Groups
	@since 0.9.9

	Initialize listeners on the root Zoner folder (per VM).

	@param ZonerFolder Types.ZonerFolder --The root folder containing the `Groups` container and group children.

	Scans existing groups under `ZonerFolder.Groups` and attaches listeners so the internal cache mirrors:
	- Group property attributes on the group folder.
	- Zone membership attributes on each group's `Zones` configuration.
]=]
function Groups.InitializeActorListeners(ZonerFolder: ZoneInstances.ZonerFolder)
	--=======================================================================================================>
	-- If the Groups Singleton is already Initialized then return:
	if Groups.ActorInitialized then return end; Groups.ActorInitialized = true;

	-- Connect to the GroupAdded to setup Groups as they are created:
	Groups.ZoneGroupAdded = ZonerFolder.Groups.ChildAdded:Connect(SetupGroup :: any)
	-- Connect to the GroupRemoved to remove Groups as they are deleted:
	Groups.ZoneGroupRemoved = ZonerFolder.Groups.ChildAdded:Connect(RemoveGroup :: any)

	-- Loop through the children of the Groups to setup any that are already created:
	for Index: number, Group in ipairs(ZonerFolder.Groups:GetChildren()) do
		SetupGroup(Group :: ZoneInstances.GroupsFolderGroup)
	end
	--=======================================================================================================>
end

--[=[
	@function DeinitializeActorListeners
	@within Groups
	@since 0.9.9

	Stop and clean up all listeners and cached state created by `InitializeActorListeners`.

	Disconnects per-group attribute listeners, clears cached maps, and disconnects group add/remove listeners.
]=]
function Groups.DeinitializeActorListeners()
	--=======================================================================================================>
	-- If the Groups Singleton is already Initialized then return:
	if Groups.ActorInitialized == false then return end; Groups.ActorInitialized = false;

	-- Loop through all the ZoneGroups and clear out their Zones and Data:
	for Key: string, GroupData: ZoneGroupData in Groups.ZoneGroups do
		-- Clear up the AttributeChanged signal connection:
		if GroupData.AttributeChanged then
			GroupData.AttributeChanged:Disconnect()
			GroupData.AttributeChanged = nil :: any
		end
		-- Clear the Zones table:
		table.clear(GroupData.Zones)
		-- Clear the data table:
		table.clear(GroupData)
	end

	-- Clear all Groups from the Dictionary:
	table.clear(Groups.ZoneGroups)

	-- Clear out the Signal Connection:
	if Groups.ZoneGroupAdded then
		Groups.ZoneGroupAdded:Disconnect()
		Groups.ZoneGroupAdded = nil :: any
	end
	-- Clear out the Signal Connection:
	if Groups.ZoneGroupRemoved then
		Groups.ZoneGroupRemoved:Disconnect()
		Groups.ZoneGroupRemoved = nil :: any
	end
	--=======================================================================================================>
end

--[=[
	@function IsZoneInGroup
	@within Groups
	@since 0.9.9

	Check whether a ZoneId is currently recorded in a given group.

	@param ZoneId string --The Zone identifier (attribute name on the group's `Zones` folder).
	@param GroupKey string --The group name key (index into `ZoneGroups`).
	@return boolean --Whether the given `ZoneId` is part of the group right now.
]=]
function Groups.IsZoneInGroup(ZoneId: string, GroupKey: string): boolean
	return Groups.ZoneGroups[GroupKey].Zones[ZoneId] or false
end

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

--[=[
	@function CreateGroup
	@within Groups
	@since 0.9.9

	Create a new group folder under `ZonerFolder.Groups` for **serial** execution contexts, mirror its properties,
	and begin listening to its `Zones` attributes.

	@param GroupName string --Name of the group to create.
	@param Properties ZoneGroupProperties --Properties to set as attributes on the group.
	@param ZonerFolder Types.ZonerFolder --The owning Zoner folder for this VM.
]=]
function Groups.CreateGroup(GroupName: string, Properties: ZoneGroupProperties, ZonerFolder: ZoneInstances.ZonerFolder)
	--=======================================================================================================>
	-- Create the Group Folder Instance for VM communication:
	local Group_Folder = Instance.new('Configuration')
	Group_Folder.Name = GroupName

	-- Set the properties on the GroupFolder:
	for Key: string, Value in pairs(Properties) do
		Group_Folder:SetAttribute(Key, Value)
	end

	Group_Folder.Parent = ZonerFolder.Groups

	-- Create the Zones Folder Instance to track Zones and their Ids, registed to this group:
	local Zones_Folder = Instance.new('Configuration')
	Zones_Folder.Name = 'Zones'
	Zones_Folder.Parent = Group_Folder;

	-- Setup the Group with listening in Serial:
	SetupGroup(Group_Folder :: ZoneInstances.GroupsFolderGroup)
	--=======================================================================================================>
end

--[=[
	@function AddZoneToGroup
	@within Groups
	@since 0.9.9

	Bind a zone to a group in **serial** contexts by setting the ID attribute on the group's `Zones` config
	and updating the in-memory cache.

	@param ZoneId string --Zone identifier to add.
	@param GroupName string --Target group key.
	@param ZonerFolder Types.ZonerFolder --The owning Zoner folder for this VM.
]=]
function Groups.AddZoneToGroup(ZoneId: string, GroupName: string, ZonerFolder: ZoneInstances.ZonerFolder)
	--=======================================================================================================>
	-- Find the GroupFolder underneath the Groups Folder:
	local Group_Folder: ZoneInstances.GroupsFolderGroup? = ZonerFolder.Groups:FindFirstChild(GroupName) :: any
	-- If Group Folder is found, Bind the Zone to the Group:
	if Group_Folder then
		-- Set the table group hash to true:
		Groups.ZoneGroups[GroupName].Zones[ZoneId] = true
		-- Set the Attribute on the Zones Folder to the Id, to record that this Zone is bound to this group:
		Group_Folder.Zones:SetAttribute(ZoneId, true)
	else
		warn(`[Groups]: 'AddZoneToGroup' | No Group exists by the name: "{GroupName}"`)
	end
	--=======================================================================================================>
end

--[=[
	@function RemoveZoneFromGroup
	@within Groups
	@since 0.9.9

	Unbind a zone from a group in **serial** contexts by clearing the ID attribute and cache entry.

	@param ZoneId string --Zone identifier to remove.
	@param GroupName string --Target group key.
	@param ZonerFolder Types.ZonerFolder --The owning Zoner folder for this VM.
]=]
function Groups.RemoveZoneFromGroup(ZoneId: string, GroupName: string, ZonerFolder: ZoneInstances.ZonerFolder)
	--=======================================================================================================>
	-- Find the GroupFolder underneath the Groups Folder:
	local Group_Folder: ZoneInstances.GroupsFolderGroup? = ZonerFolder.Groups:FindFirstChild(GroupName) :: any
	-- If Group Folder is found, Bind the Zone to the Group:
	if Group_Folder then
		-- Set the table group hash to nil:
		Groups.ZoneGroups[GroupName].Zones[ZoneId] = nil
		-- Set the Attribute on the Zones Folder to nil, to record that this Zone is no longer bound to this group:
		Group_Folder.Zones:SetAttribute(ZoneId, nil)
	else
		warn(`[Groups]: 'RemoveZoneFromGroup' | No Group exists by the name: "{GroupName}"`)
	end
	--=======================================================================================================>
end

--[=[
	@function RemoveZoneFromGroups
	@within Groups
	@private
	@since 0.9.9

	Remove a zone from **all** groups for this VM if present.

	@param ZoneId string --Zone identifier to purge.
	@param ZonerFolder Types.ZonerFolder --The owning Zoner folder for this VM.
]=]
function Groups.RemoveZoneFromGroups(ZoneId: string, ZonerFolder: ZoneInstances.ZonerFolder)
	-- Loop through all the ZoneGroups registered, Check if Zone is in that Group, if so, Remove that Zone from that Group:
	for GroupKey, GroupData in Groups.ZoneGroups do
		if Groups.IsZoneInGroup(ZoneId, GroupKey) then Groups.RemoveZoneFromGroup(ZoneId, GroupKey, ZonerFolder) end
	end
end

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

-- Return a Frozen Module Table:
return Groups

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


--===========================================================================================================================>
--!optimize 2
--!strict
--===========================================================================================================================>

-- Set Memory Category:
debug.setmemorycategory('[Zoner]: Plagger')

-- Define Module table
local PlaggerModule = {
	--=========================================================================>
	-- Grab the RunService RunScope that this Code is running on:
	_RunScope = (if game:GetService('RunService'):IsServer() then 'Server' else 'Client' ):: 'Client' | 'Server';
	-- Grab the boolean attribute whether the Module has been Ran Once to connect to the Players Events:
	_CoreRunning = {
		-- Grab the boolean attribute whether the Module has been Ran Once to connect to the Players Events:
		Server = (script:GetAttribute('Server_CoreRunning') or false) :: boolean;
		-- Grab the boolean attribute whether the Module has been Ran Once to connect to the Players Events:
		Client = (script:GetAttribute('Client_CoreRunning') or false) :: boolean;
	};
	--=========================================================================>
	-- Create an Empty Table to Store the Player Dictionaries:
	_Players = {} :: any;
	--=========================================================================>
	-- Dictionary of Tags:
	_Tags = {
		Player          = 'Zoner:Player'                :: 'Zoner:Player';
		ClientCharacter = 'Zoner:Client:Character'      :: 'Zoner:Client:Character';
		ServerCharacter = 'Zoner:Server:Character'      :: 'Zoner:Server:Character';
		LocalCharacter  = 'Zoner:Client:LocalCharacter' :: 'Zoner:Client:LocalCharacter';
	};
	--=========================================================================>
}

--===========================================================================================================================>
--[ SERVICES: ]

-- Grab the CollectionService and Players Service:
local CollectionService, Players = game:GetService('CollectionService'), game:GetService('Players')

local Zignal = require('../../External/Zignal')

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

type PlayerObject = {
	Events: { CharacterAdded: RBXScriptConnection; CharacterRemoved: RBXScriptConnection };
	Destroy: (self: PlayerObject) -> ()
};

-- Create and Export the Module's Object Type:
export type Plagger = typeof(setmetatable({} :: {
	--======================================>
	_TaggedOnly: boolean?;
	_LocalOnly:  boolean?;
	_InActor:    boolean?;
	--===========================================>
	_LocalPlayerName: string?;
	--===========================================>
	_Events: {[string]: RBXScriptConnection};
	--===========================================>
	_CharacterList: {[Player]: Model};
	_PlayerList:    {[string]: Player};
	--===========================================>
	-- Meta Index Variables:
	-- Some External references to internal variable:
	Characters: {[Player]: Model};
	Players:    {[string]: Player};

	_PlayerAdded:   Zignal.Signal<Player>?;
	_PlayerRemoved: Zignal.Signal<Player>?;

	_CharacterRemoved:   Zignal.Signal<Model>?;
	_CharacterAdded: Zignal.Signal<Model>?;
	--======================================>
}, {} :: {__index: typeof(PlaggerModule)}))

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

-- Function which when called will setup a new Character Object on the Server
function PlaggerModule.New(TaggedOnly: boolean?, LocalOnly: boolean?, InActor: boolean?): Plagger
	--=======================================================================================================>

	-- Set Memory Category:
	debug.setmemorycategory('[Zoner]: Plagger')

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

	-- Initialize Object values and tables:
	local PlaggerData= {
		--===========================================>
		_TaggedOnly = TaggedOnly;
		_LocalOnly  = LocalOnly;
		_InActor    = InActor;
		--===========================================>
		-- Create an Empty Table to Store the Player Objects:
		_PlayerList = {};
		-- Create an Empty Table to Store the Character Objects:
		_CharacterList = {};
		-- Create an Empty Table to store the Event Connections:
		_Events = {};
		--===========================================>
	} :: any

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

	-- If we are running on the Server, and LocalOnly Paramater was set to true, fix it with false, since that paramater is for clients only:
	if PlaggerModule._RunScope == 'Server' and LocalOnly == true then PlaggerData._LocalOnly = false end

	-- Only Run ONE version of this Module as non Tagged. IE using the Player.Added Events:
	-- And Make all other subsequent calls to this Module use the CollectionService Tagging:
	if (TaggedOnly == false or TaggedOnly == nil) and PlaggerModule._CoreRunning[PlaggerModule._RunScope] ~= true then
		-- Set the RunScope_CoreRunning Attribute to true:
		--script:SetAttribute(`{PlaggerModule._RunScope}_CoreRunning`, true)
		-- Update the Module Variable:
		--PlaggerModule._CoreRunning[PlaggerModule._RunScope] = true
		-- Set the TaggedOnly to false:
		PlaggerData._TaggedOnly = false
	else
		-- Set the TaggedOnly boolean to true since the Players Events have already been connected to:
		PlaggerData._TaggedOnly = true
	end

	-- If we are Running on the Client and Creating this Object on the Client and LocalOnly boolean is true, Grab the LocalPlayer's Name:
	if PlaggerData._TaggedOnly == false and PlaggerModule._RunScope == 'Client' then
		-- Yield until a LocalPlayer Exists:
		if not Players.LocalPlayer then repeat task.wait() until Players.LocalPlayer end
		-- Set the LocalPlayer Name Variable:
		PlaggerData._LocalPlayerName = Players.LocalPlayer.Name
	end

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

	-- Set the Object metatable:
	setmetatable(PlaggerData, PlaggerModule)

	-- Initialize the Object:
	PlaggerData:_Initialize()

	-- Return the FROZEN Object:
	return PlaggerData :: Plagger

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

-- Function which whell called will destroy all the local data for this Character Instance
function PlaggerModule.Destroy(self: Plagger)
	--=======================================================================================================>

	-- Loop through all the Events and Disconnect them:
	for Key: string, Connection in self._Events do
		self._Events[Key]:Disconnect()
		self._Events[Key] = nil :: any
	end

	-- If we are the Original CoreRunning Object, clear the Players Table and set Script CoreRunning Attribute back to false:
	if self._TaggedOnly == false then

		-- Loop through all the Player Objects and call their Destroy Method:
		for Player: Player, Data in self._Players do Data:Destroy() end
		-- Clear the Module Players Table:
		table.clear(PlaggerModule._Players)

		-- Set the RunScope_CoreRunning Attribute to false:
		--script:SetAttribute(`{PlaggerModule._RunScope}_CoreRunning`, false)
		-- Update the Module Variable:
		--PlaggerModule[`{PlaggerModule._RunScope}_CoreRunning`] = false

	end

	if self._PlayerRemoved then self._PlayerRemoved:Destroy(); self._PlayerRemoved = nil end
	if self._PlayerAdded then self._PlayerAdded:Destroy(); self._PlayerAdded = nil end
	if self._CharacterAdded then self._CharacterAdded:Destroy(); self._CharacterAdded = nil end
	if self._CharacterRemoved then self._CharacterRemoved:Destroy(); self._CharacterRemoved = nil end

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

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

-- Initialization function to start/setup the Object's initial data:
function PlaggerModule._Initialize(self: Plagger)
	for Index: number, Function: string in ipairs({'_SetObjects', '_SetEvents'}) do self[Function](self) end
end

-- Function to Set up initial Player Object Data:
function PlaggerModule._SetObjects(self: Plagger)
	--=======================================================================================================>

	-- Get all the Current Players in Game:
	-- If Tagged is true, then get the players from the Player Tag, else get the Players from the Players Service:
	local PlayersGot: {Player} = if self._TaggedOnly then
		CollectionService:GetTagged(self._Tags.Player) else Players:GetPlayers()

	-- Loop through all the Current In Game Players and Add them to the Data Object:
	for Index: number, Player: Player in ipairs(PlayersGot) do self:_AddPlayer(Player) end

	-- Clear from Memory
	PlayersGot = nil :: any

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

	-- If we are in a TaggedOnly Object, then Grab the Characters Via the Proper CollectionService Tag:
	if self._TaggedOnly then
		--===============================================================================>
		-- Get all the Current Characters in Game:
		-- Get the Character from the Character Tag:
		local CharactersGot: {Model} =
			CollectionService:GetTagged(if self._LocalOnly == true then self._Tags.LocalCharacter else self._Tags.ServerCharacter)

		-- Loop through all the Current In Game Characters and Add them to the Data Object:
		for Index: number, Character: Model in ipairs(CharactersGot) do
			self:_AddCharacter(self._PlayerList[Character.Name], Character)
		end

		-- Clear from Memory
		CharactersGot = nil :: any
		--===============================================================================>
	end

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

-- Function to Set up Initial Events:
function PlaggerModule._SetEvents(self: Plagger)
	--=======================================================================================================>

	if self._TaggedOnly then
		--==========================================================================================>

		-- Grab the Character Tag based on if we are looking for Local only or not:
		local CharacterTag = if self._LocalOnly == true then self._Tags.LocalCharacter else self._Tags.ServerCharacter
		-- If the InActor Variable is true, then Connect Each Event in Parallel to allow updates in Parallel:
		local ConnectMethod = if self._InActor then 'ConnectParallel' else 'Connect'

		-- Grab the PlayerAdded Signal:
		local PlayerAddedSignal   = CollectionService:GetInstanceAddedSignal(self._Tags.Player)
		-- Grab the PlayerRemoved Signal:
		local PlayerRemovedSignal = CollectionService:GetInstanceRemovedSignal(self._Tags.Player)

		-- Grab the CharacterAdded Signal:
		local CharacterAddedSignal   = CollectionService:GetInstanceAddedSignal(CharacterTag)
		-- Grab the CharacterRemoved Signal:
		local CharacterRemovedSignal = CollectionService:GetInstanceRemovedSignal(CharacterTag)

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

		-- Connect to the Instance Added Event:
		self._Events['PlayerAdded'] = PlayerAddedSignal[ConnectMethod](PlayerAddedSignal, function(Player: Player)
			self:_AddPlayer(Player)
		end)
		-- Connect to the Instance Removed Event:
		self._Events['PlayerRemoved'] = PlayerAddedSignal[ConnectMethod](PlayerRemovedSignal, function(Player: Player)
			self:_RemovePlayer(Player)
		end)

		-- Connect to the Instance Added Event:
		self._Events['CharacterAdded'] = CharacterAddedSignal[ConnectMethod](CharacterAddedSignal, function(Character: Model)
			-- Cant use GetPlayerFromCharacter in parallel:
			local Player: Player
			-- Get the Player from the Character in order to use it for a Key:
			for PlayerName: string, CurrentPlayer: Player in pairs(self._PlayerList) do
				if CurrentPlayer.Character == Character then Player = CurrentPlayer; break end
			end
			-- Add the Character:
			self:_AddCharacter(Player, Character)
		end)

		-- Connect to the Instance Removed Event:
		self._Events['CharacterRemoved'] = CharacterRemovedSignal[ConnectMethod](CharacterRemovedSignal, function(Character: Model)
			-- Cant use GetPlayerFromCharacter in parallel:
			local Player: Player
			-- Get the Player from the Character in order to use it for a Key:
			for PlayerName: string, CurrentPlayer: Player in pairs(self._PlayerList) do
				if CurrentPlayer.Character == Character then Player = CurrentPlayer; break end
			end
			-- Remove the Character:
			self:_RemoveCharacter(self._PlayerList[Player.Name], Character)
		end)

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

		-- Connect to the PlayerAdded Event:
		self._Events['PlayerAdded'] = Players.PlayerAdded:Connect(function(Player: Player) self:_AddPlayer(Player) end)
		-- Connect to the PlayerRemoving Event:
		self._Events['PlayerRemoving'] = Players.PlayerRemoving:Connect(function(Player: Player) self:_RemovePlayer(Player) end)

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

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

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

-- Method to Add a Player to the Data Object:
function PlaggerModule._AddPlayer(self: Plagger, Player: Player)
	--=======================================================================================================>

	-- If LocalOnly is true, and the LocalPlayerName is not nil, and the Player passed's name does not equal the LocalPlayer's Name, RETURN:
	if self._LocalOnly and self._LocalPlayerName and Player.Name ~= self._LocalPlayerName then return end

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

	-- If the Player is already Added to the Players Table, then return:
	if self._PlayerList[Player.Name] then warn('Player already added to PlayerList'); return end

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

	-- If we are running on the Core Object:
	if not self._TaggedOnly then
		--==============================================================================================>

		-- If the Player is already Added to the Players Table, then return:
		if self._Players[Player] then warn('Player Already Added to Table'); return end

		--==============================================================================================>
		-- Create a Player Object Table:
		self._Players[Player] = { Events = {} } :: any

		-- Create a Destroy function that will Clean up all the Player Events:
		self._Players[Player].Destroy = function(self)
			for Key, Connection in self.Events do self.Events[Key]:Disconnect(); self.Events[Key] = nil :: any end
		end

		if self._LocalOnly == true then
			if Player.Name ~= self._LocalPlayerName then return end
		end

		-- Connect to the Player Character Added Event to Add Tags to the Character:
		self._Players[Player].Events['CharacterAdded'] = Player.CharacterAdded:Connect(function(Character: Model)
			-- Add the Character:
			self:_AddCharacter(self._PlayerList[Player.Name], Character)
		end)

		-- Connect to the Player Character Added Event to Add Tags to the Character:
		self._Players[Player].Events['CharacterRemoved'] = Player.CharacterRemoving:Connect(function(Character: Model)
			-- Remove the Character:
			self:_RemoveCharacter(self._PlayerList[Player.Name], Character)
		end)

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

		-- Call the AddCharacter Method incase the Player already has a Character:
		self:_AddCharacter(Player, Player.Character)

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

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

	-- Insert the Player into the PlayerList
	self._PlayerList[Player.Name] = Player

	if self._PlayerAdded then self._PlayerAdded:Fire(Player) end

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

-- Method to Remove a Player from the Data Object:
function PlaggerModule._RemovePlayer(self: Plagger, Player: Player)
	--=======================================================================================================>

	-- If the Player Name does not have a Player in the table, return:
	if not self._PlayerList[Player.Name] then return end

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

	-- If we are running on the Core Object:
	if not self._TaggedOnly then
		--==============================================================================================>
		-- If the Player does not have a Player Object in the table, return:
		if not self._Players[Player] then return end
		--==============================================================================================>
		-- Remove the Tag from the Player if it has it:
		if Player:HasTag(self._Tags.Player) == true then Player:RemoveTag(self._Tags.Player) end
		-- Destroy the Player Object:
		self._Players[Player]:Destroy()
		-- Remove the Reference to the Player in the Table:
		self._Players[Player] = nil
		--==============================================================================================>
	end

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

	-- Remove the Player from the PlayerList
	self._PlayerList[Player.Name] = nil

	if self._PlayerRemoved then self._PlayerRemoved:Fire(Player) end

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

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

-- Method to Add a tag to the Player's Character:
function PlaggerModule._AddCharacter(self: Plagger, Player: Player, Character: Model?)
	--=======================================================================================================>
	-- If the Character passed is nil then return:
	if Character == nil then return end
	if not Player then warn('Player was nil in AddCharacter in Plagger'); return end

	-- If this is not Running in TaggedOnly, and the Character does not already have the Tag, then Add it:
	--if self._TaggedOnly == false and Character:HasTag(self._Tags.ServerCharacter) == false then Character:AddTag(self._Tags.ServerCharacter) end

	-- If this is Running in the Core Client Object:
	if self._TaggedOnly == false and self._RunScope == 'Client' then
		-- if the Player Name is the same as the LocalPlayerName, attempt to add the localcharacter tag:
		if Player.Name == self._LocalPlayerName then
			-- If this is Running in LocalOnly, and the Character does not already have the LocalCharacter Tag, then Add it:
			--if Character:HasTag(self._Tags.LocalCharacter) == false then Character:AddTag(self._Tags.LocalCharacter) end
		end
	end

	if self._LocalOnly == true then if Player.Name ~= self._LocalPlayerName then return end end

	-- Insert the Character into the CharacterList:
	self._CharacterList[Player] = Character

	if self._CharacterAdded then self._CharacterAdded:Fire(Character) end

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

-- Method to Remove a tag from the Player's Character:
function PlaggerModule._RemoveCharacter(self: Plagger, Player: Player, Character: Model)
	--=======================================================================================================>

	-- If the Character passed is nil then return:
	if Character == nil :: any then return end
	if not Player then warn('Player was nil in RemoveCharacter in Plagger'); return end

	-- If the Character is not in the List, return:
	if not self._CharacterList[Player] then return end

	-- If this is not Running in TaggedOnly, and the Character does have the Tag, then Remove it:
	if self._TaggedOnly == false and Character:HasTag(self._Tags.ServerCharacter) == true then Character:RemoveTag(self._Tags.ServerCharacter) end

	-- If this is Running in the Core Client Object:
	if self._TaggedOnly == false and self._RunScope == 'Client' then
		-- if the Player Name is the same as the LocalPlayerName, attempt to add the localcharacter tag:
		if Player.Name == self._LocalPlayerName then
			-- If this is Running in LocalOnly, and the Character already has the LocalCharacter Tag, then Remove it:
			--if Character:HasTag(self._Tags.LocalCharacter) == true then Character:RemoveTag(self._Tags.LocalCharacter) end
		end
	end

	-- Remove the Character from the CharacterList
	self._CharacterList[Player] = nil

	if self._CharacterRemoved then self._CharacterRemoved:Fire(Character) end

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

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

-- @Public
-- Method to get the PlayersList:
function PlaggerModule.GetPlayers(self: Plagger): {[string]: Player}
	return self._PlayerList
end

-- @Public
-- Method to get the CharacterList:
function PlaggerModule.GetCharacters(self: Plagger): {[Player]: Model}
	return self._CharacterList
end

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

-- @Public
-- Method to get the PlayerAdded (Tagged) Signal:
function PlaggerModule.GetPlayerAddedSignal(self: Plagger): Zignal.Signal<Player>
	--return CollectionService:GetInstanceAddedSignal(self._Tags.Player)
	if not self._PlayerAdded then self._PlayerAdded = Zignal.New() end;
	return self._PlayerAdded :: Zignal.Signal<Player>
end

-- @Public
-- Method to get the PlayerRemoved (Un-Tagged) Signal:
function PlaggerModule.GetPlayerRemovedSignal(self: Plagger): Zignal.Signal<Player>
	if not self._PlayerRemoved then self._PlayerRemoved = Zignal.New() end;
	return self._PlayerRemoved :: Zignal.Signal<Player>
	--return CollectionService:GetInstanceRemovedSignal(self._Tags.Player)
end

-- @Public
-- Method to get the CharacterAdded (Tagged) Signal:
function PlaggerModule.GetCharacterAddedSignal(self: Plagger): Zignal.Signal<Model>
	if not self._CharacterAdded then self._CharacterAdded = Zignal.New() end;
	return self._CharacterAdded :: Zignal.Signal<Model>
	--return CollectionService:GetInstanceAddedSignal(if self._LocalOnly == true then self._Tags.LocalCharacter else self._Tags.ServerCharacter)
end

-- @Public
-- Method to get the CharacterRemoved (Un-Tagged) Signal:
function PlaggerModule.GetCharacterRemovedSignal(self: Plagger): Zignal.Signal<Model>
	if not self._CharacterRemoved then self._CharacterRemoved = Zignal.New() end;
	return self._CharacterRemoved :: Zignal.Signal<Model>
	--return CollectionService:GetInstanceRemovedSignal(if self._LocalOnly == true then self._Tags.LocalCharacter else self._Tags.ServerCharacter)
end

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

-- Create the MetaIndex function:
function PlaggerModule.__index(self: Plagger, Index: string): any
	--=======================================================================================================>
	-- Specific Indexing:
	--=======================================================================================================>
	if Index == 'Characters' then return self._CharacterList end
	if Index == 'Players'    then return self._PlayerList    end
	--=======================================================================================================>
	-- If Index is in the immediate Module tree, return that value:
	if PlaggerModule[Index] then return PlaggerModule[Index] end
	--=======================================================================================================>
	-- Return False if all else fails!
	return false
	--=======================================================================================================>
end

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

-- Return the Module Object:
return table.freeze({New = PlaggerModule.New; new = PlaggerModule.New})

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