--!strict

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local Promise = require(script.Parent.Parent.Packages.Promise)
local Logging = require(script.Parent.Parent.Logging)
local ProfileStore

local Store = nil :: any

--// Table

local PlayerClass = {}
PlayerClass.__index = PlayerClass

PlayerClass.DefaultData = {}

if RunService:IsServer() then
	ProfileStore = require(script.Parent.Parent.Packages.ProfileStore)
end

export type PlayerClassType = typeof(setmetatable(
	{} :: {
		Player: Player,
		Data: typeof(PlayerClass.DefaultData),
		DataStore: any,
		SessionEnd: RBXScriptConnection,
		CustomData: { any },
		Destroy: (self: PlayerClassType) -> (),
	},
	PlayerClass
))

function PlayerClass.new(Player: Player): any
	if RunService:IsClient() then
		error("PlayerClass cannot be ran on client!")
		return
	end

	return Promise.new(function(Resolve)
		local self = {}

		self.Player = Player :: Player
		self.Data = PlayerClass.DefaultData
		self.CustomData = {}
		self.SessionEnd = nil :: any
		self.DataStore = nil :: any

		self.DataStore = Store:StartSessionAsync(`{Player.Name}-{Player.UserId}`, {
			Cancel = function()
				return Player.Parent ~= Players
			end,
		})

		if not self.DataStore then
			Logging.Message({
				Level = "FATAL",
				Message = `Player {Player.Name} (UID: {Player.UserId})'s data failed to load!`,
				LogCat = "HYDROGEN: PlayerClass",
			})
			Player:Kick("Hey, unfortunately your data failed to load! Please try again later!")
		end

		self.DataStore:AddUserId(Player.UserId)
		self.DataStore:Reconcile()
		self.SessionEnd = self.DataStore.OnSessionEnd:Connect(function()
			Player:Kick(`Profile session end - Please rejoin`)
		end)

		self.Data = self.DataStore.Value

		Resolve(setmetatable(self, PlayerClass))
		return
	end)
end

function PlayerClass.SetDefaultData(Data: { any })
	PlayerClass.DefaultData = Data

	if RunService:IsServer() then
		Store = RunService:IsStudio() and ProfileStore.New("Data", PlayerClass.DefaultData).Mock
			or ProfileStore.New("Data", PlayerClass.DefaultData)
	end
end

function PlayerClass.Destroy(self: PlayerClassType)
	if self.DataStore then
		self.DataStore:EndSession()
	end
end

return PlayerClass


--!strict

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

local Network

if RunService:IsServer() then
	Network = require(script.Parent["_PRIVATE_HYDROGEN_network"].server) :: any
else
	Network = require(script.Parent["_PRIVATE_HYDROGEN_network"].client) :: any
end

local Promise = require(script.Parent.Packages.Promise)
local FunctionLib = require(script.Parent.Util.FunctionLib)
local PlayerClass = require(script.PlayerClass)

local PlayerHandler = {}
PlayerHandler.Players = {} :: { [Player]: PlayerClass.PlayerClassType }
PlayerHandler.DataSetCallback = nil :: (Player: Player, Data: { any }) -> ()?
type Handler = typeof(PlayerHandler)

function PlayerHandler.SetDataSetCallback(Callback: (Player: Player, Data: { [string]: any }) -> ())
	PlayerHandler.DataSetCallback = Callback :: any
end

function PlayerHandler.PlayerAdded(self: Handler, Player: Player)
	PlayerClass.new(Player)
		:andThen(function(Object)
			self.Players[Player] = Object
			return self.Players[Player]
		end)
		:catch(warn)
end

function PlayerHandler.SetCustomData(self: Handler, Player: Player, NewData: { any })
	local Data = self.Players[Player].CustomData
	local NewDictionary = FunctionLib:MergeDictionary(Data, NewData)

	self.Players[Player].CustomData = NewDictionary
end

PlayerHandler.onPlayerRemoving = function(self: Handler, Player: Player)
	self.Players[Player]:Destroy()
	self.Players[Player] = nil
end

PlayerHandler.GetPlayerData = function(self: Handler, Player: Player?)
	return Promise.new(function(Resolve, _, Cancel)
		Cancel(function()
			Resolve({})
			return true
		end)

		if RunService:IsClient() then
			local Data = Network.GetPlayerData.Call()

			Resolve(Data)
			return
		end

		if not Player then
			error("On server player must not be nil!")
			return true
		end

		repeat
			task.wait(0.1)
		until self.Players[Player]

		local success, err = pcall(function()
			repeat
				task.wait(0.1)
			until self.Players[Player]["DataStore"] ~= nil

			return true
		end)

		if not success and err then
			warn(err)
			Resolve({})
			return
		end

		Resolve(self.Players[Player]["DataStore"].Data :: typeof(self.Players[Player].DataStore.Data))
	end):catch(warn)
end

PlayerHandler.SetPlayerData = function(self: typeof(PlayerHandler), Player: Player, NewData: {})
	return Promise.new(function(resolve, fail)
		if not RunService:IsServer() then
			fail("Hydrogen PlayerHandler: SetPlayerData Must be ran from the server!")
		end

		local Data = self:GetPlayerData(Player):expect() :: any

		local NewDictionary = FunctionLib:MergeDictionary(Data, NewData)

		if PlayerHandler.DataSetCallback then
			PlayerHandler.DataSetCallback(Player, NewDictionary)
		end

		if self.Players[Player]["DataStore"] then
			for Key, Value in NewDictionary do
				self.Players[Player]["DataStore"].Data[Key] = Value
			end
		end

		self.Players[Player]["Data"] = NewDictionary

		resolve(NewDictionary)
		return NewDictionary
	end):catch(warn)
end

function PlayerHandler.Start(Config: {
	DefaultData: {},
})
	if RunService:IsClient() then
		return
	end

	Network.GetPlayerData.SetCallback(function(Player: Player)
		local PlayerObject = PlayerHandler.Players[Player]
		if not PlayerObject or not PlayerObject.DataStore then
			return {}
		end
		local ClonedTable = FunctionLib:CopyDeep(PlayerObject.DataStore.Data)
		return ClonedTable
	end)

	PlayerClass.SetDefaultData(Config.DefaultData)

	for _, Player in Players:GetPlayers() do
		PlayerHandler:PlayerAdded(Player)
	end

	Players.PlayerAdded:Connect(function(Player)
		PlayerHandler:PlayerAdded(Player)
	end)

	Players.PlayerRemoving:Connect(function(Player)
		PlayerHandler:onPlayerRemoving(Player)
	end)
end

return PlayerHandler


--!strict

local RunService = game:GetService("RunService")
local FunctionLib = require(script.Parent.Util.FunctionLib)
local Network

if RunService:IsServer() then
	Network = require(script.Parent["_PRIVATE_HYDROGEN_network"].server) :: any
else
	Network = require(script.Parent["_PRIVATE_HYDROGEN_network"].client) :: any
end

local SoundHandler = {
	Sounds = nil :: Folder?,
}

local function CheckSound(Sound: string): boolean
	if not SoundHandler.Sounds then
		return false
	end

	if not FunctionLib:FindFirstDescendant(SoundHandler.Sounds, Sound) then
		return false
	end

	return true
end

function SoundHandler._BackendSound(Sound: string, PlayLength: number?, CF: CFrame?, CustomProps: { [string]: any }?)
	if not SoundHandler.Sounds then
		return
	end

	local OriginalSound: Sound = FunctionLib:FindFirstDescendant(SoundHandler.Sounds, Sound) :: Sound

	local NewSound: Sound = OriginalSound:Clone()
	NewSound.Parent = workspace

	if CustomProps then
		for PropName, Property in CustomProps do
			NewSound[PropName] = Property
		end
	end

	local NewPart

	if CF then
		NewPart = Instance.new("Part")
		NewPart.Transparency = 1
		NewPart.Anchored = true
		NewPart.CanCollide = false
		NewPart.Size = Vector3.new(1, 1, 1)
		NewPart.CFrame = CF
		NewPart.Parent = workspace

		NewSound.Parent = NewPart
	end

	NewSound:Play()

	task.spawn(function()
		if PlayLength then
			task.wait(PlayLength)

			NewSound:Destroy()

			if NewPart then
				NewPart:Destroy()
			end

			return
		end

		NewSound.Ended:Wait()

		if NewSound then
			NewSound:Destroy()
		end

		if NewPart then
			NewPart:Destroy()
		end
	end)

	return NewSound
end

function SoundHandler.PlaySound(
	Sound: string,
	Player: Player?,
	PlayLength: number?,
	CF: CFrame?,
	Props: { [string]: any }?
)
	if not CheckSound(Sound) then
		error(`Sound {Sound} doesn't exist!`)
		return
	end

	if RunService:IsClient() then
		return SoundHandler._BackendSound(Sound, PlayLength, CF, Props)
	end

	if Player then
		Network.PlaySoundOnClient.Fire(Player, Sound, CF)
	else
		Network.PlaySoundOnClient.FireAll(Sound, CF)
	end
end

function SoundHandler.Start(Sounds: Folder)
	assert(Sounds, "Hydrogen SoundHandler: Sounds Must be set to a folder")
	SoundHandler.Sounds = Sounds

	if RunService:IsServer() then
		return
	end

	Network.PlaySoundOnClient.On(function(Sound: string, CFrame: CFrame?)
		SoundHandler._BackendSound(Sound, nil, CFrame)
	end)
end

return SoundHandler


--!strict

local FunctionLib = {}
local suffixes = { "K", "M", "B", "T", "Qd", "Qn", "Se", "Sp", "Oc", "No", "Dc", "Udc", "Ddc", "Tdc" }

local None = newproxy(true)

getmetatable(None :: any).__tostring = function()
	return "Sift.None"
end

function FunctionLib:ToSuffixString(n)
	if n == 0 then
		return "0"
	end

	for i = #suffixes, 1, -1 do
		if i == 1 then
			continue
		end

		local v = math.pow(10, i * 3)

		if n >= v then
			return ("%.0f"):format(n / v) .. suffixes[i]
		end
	end
	return tostring(n)
end

function FunctionLib:FindPossibleStringsInString(String: string, Values: { string }): string?
	for _, Value in Values do
		if string.find(String, Value) then
			return Value
		end
	end

	return
end

function FunctionLib:FindStringsInTable(Strings: { string }, Values: { string }): boolean
	for _, String in Strings do
		if not table.find(Values, String) then
			return false
		end
	end

	return true
end

function FunctionLib:SecsToMins(Secs: number, FormatAsString: boolean?): { Mins: number, Secs: number } | string
	local TimeTable = {} :: { Mins: number, Secs: number }

	TimeTable.Mins = math.floor(Secs / 60)
	TimeTable.Secs = Secs % 60

	if not FormatAsString then
		return TimeTable
	end

	local ReturnString = `{TimeTable.Mins}:{TimeTable.Secs}`

	if TimeTable.Mins == 0 then
		ReturnString = `{TimeTable.Secs}s`
	end

	return ReturnString
end

function FunctionLib:FindFirstDescendant(Object: Instance, Name: string)
	local ChildToReturn

	for _, Child in Object:GetDescendants() do
		if Child.Name == Name then
			ChildToReturn = Child
		end
	end

	return ChildToReturn
end

function FunctionLib:ToScientificNotation(N: number): string
	local b = math.floor(math.log(N, 10))
	local a = N / (10 ^ b)

	local notation = `{a}e{b}`

	return notation
end

function FunctionLib:ToRegularNumber(N: string): number
	local Split = string.split(N, "e") :: { string }

	local Num1 = tonumber(Split[1]) :: number
	local Num2 = tonumber(Split[2]) :: number

	if string.sub(Split[2], 1, 1) == "+" then
		Num2 = tonumber(string.sub(Split[2], 2, string.len(Split[2]))) :: number
	end

	return Num1 * (10 ^ Num2)
end

function FunctionLib:CheckIfPartIsPlayer(Hit: BasePart): boolean
	local Parent = Hit.Parent :: BasePart

	if not Parent then
		return false
	end

	if not Parent:FindFirstChildOfClass("Humanoid") then
		return false
	end

	return true
end

--// FUNCTIONS LABELLED BELOW ARE FROM SIFT. ALL CREDIT TO CXMEEL AND CONTRIBUTORS TO SIFT

function FunctionLib:MergeDictionary<T>(...: { any }): T
	local result = {}

	for dictionaryIndex = 1, select("#", ...) do
		local dictionary = select(dictionaryIndex, ...)

		if type(dictionary) ~= "table" then
			continue
		end

		for key, value in pairs(dictionary) do
			result[key] = if value == None then nil else value
		end
	end

	return result :: any
end

function FunctionLib:CopyDeep<T>(dictionary: T): T
	local new = table.clone(dictionary :: any)

	for key, value in pairs(dictionary :: any) do
		if type(value) == "table" then
			new[key] = FunctionLib:CopyDeep(value)
		end
	end

	return new :: any
end

return FunctionLib


--!strict

local RunService = game:GetService("RunService")

--[[

Sophie B 2025 MIT

]]

local Lifecycles = require(script.Lifecycles)

local PossibleClientCycles = {
	"PlayerAdded",
	"PlayerRemoving",
	"Heartbeat",
	"CharacterAdded",
	"CharacterRemoving",
	"RenderStepped",
	"PostSimulation",
}

local PossibleServerCycles = {
	"PlayerAdded",
	"PlayerRemoving",
	"Heartbeat",
	"PostSimulation",
}

local Hydrogen = {
	Server = { ModuleNames = {}, RequiredModules = {} } :: {
		RunLifecycles: boolean? | typeof(PossibleServerCycles)?,
		ModuleNames: {},
		RequiredModules: {},
	},
	Client = { ModuleNames = {}, RequiredModules = {} } :: {
		RunLifecycles: boolean? | typeof(PossibleClientCycles)?,
		ModuleNames: {},
		RequiredModules: {},
	},
}

Hydrogen.PlayerHandler = require(script.Player)
Hydrogen.FunctionLib = require(script.Util.FunctionLib)
Hydrogen.SoundHandler = require(script.Sound)
Hydrogen.Logging = require(script.Logging)
Hydrogen.Lifecycles = require(script.Lifecycles)

function Hydrogen.GetRuntime(): "Server" | "Client"
	return RunService:IsServer() and "Server" :: "Server" or "Client"
end

function Hydrogen.Start(Config: {
	ModuleDirectory: any,
	ModuleSuffix: string?,
	RunLifecycles: boolean? | typeof(PossibleClientCycles)?,
})
	local Runtime = Hydrogen.GetRuntime()

	if not Config.ModuleDirectory then
		if RunService:IsClient() then
			Config.ModuleDirectory = game:GetService("StarterPlayer"):WaitForChild("StarterPlayerScripts") :: Folder
		else
			Config.ModuleDirectory = game:GetService("ServerScriptService") :: any
		end
	end

	if not Config.ModuleSuffix then
		Config.ModuleSuffix = "Handler$"
	end

	for _, Descendant in Config.ModuleDirectory:GetDescendants() do
		if not Descendant:IsA("ModuleScript") then
			continue
		end

		local Name = Descendant.Name :: string

		if not Name:match(`{Config.ModuleSuffix}` :: string) then
			continue
		end

		if table.find(Hydrogen[Runtime].ModuleNames, Descendant.Name) then
			error("Hydrogen failed to load " .. Descendant.Name .. " as there is already a module named that loaded!")
		end

		task.spawn(function()
			local _RequiredDescendant = require(Descendant) :: any
			table.insert(Hydrogen[Runtime].ModuleNames, Descendant.Name)
			table.insert(Hydrogen[Runtime].RequiredModules, Descendant)

			debug.setmemorycategory(Descendant.Name)
		end)
	end

	if Config.RunLifecycles == true then
		if Runtime == "Client" then
			Hydrogen[Runtime].RunLifecycles = PossibleClientCycles
		else
			Hydrogen[Runtime].RunLifecycles = PossibleServerCycles
		end
	end

	Lifecycles.Run(Hydrogen[Runtime].RequiredModules, Hydrogen[Runtime].RunLifecycles)
end

return Hydrogen


--[[

	MIT License

	Copyright (c) 2024 Redon Tech

	Permission is hereby granted, free of charge, to any person obtaining a copy
	of this software and associated documentation files (the "Software"), to deal
	in the Software without restriction, including without limitation the rights
	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
	copies of the Software, and to permit persons to whom the Software is
	furnished to do so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in all
	copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
	SOFTWARE.

--]]

return


--[[
	 _____               _                     _______                 _
	|  __ \             | |                   |__   __|               | |
	| |__) |   ___    __| |   ___    _ __        | |      ___    ___  | |__
	|  _  /   / _ \  / _  |  / _ \  |  _ \       | |     / _ \  / __| |  _ \
	| | \ \  |  __/ | (_| | | (_) | | | | |      | |    |  __/ | (__  | | | |
	|_|  \_\  \___|  \__,_|  \___/  |_| |_|      |_|     \___|  \___| |_| |_|
	Radio System
	Client API

	MIT License
	Copyright (c) 2024 Redon Tech
--]]

-- Types

export type ScriptConnection = {
	disconnect: () -> nil,
}

-- Everything Else

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local shared = ReplicatedStorage:WaitForChild("radioShared")
local comm = require(shared:WaitForChild("comm"))

local client = {}
client.__index = client

function client.init()
	local apiEvents = comm.new("api")
	local data = {
		apiEvents = apiEvents,
		clientApi = apiEvents:bindableEvent("clientApi"),
		connections = {
			clientMessageRecieved = {},
			systemMessageRecieved = {},
			messageHistoryRecieved = {},
			voiceRecieve = {},
			sendMessage = {},
		},
	}
	local self = setmetatable(data, client)

	self._mainConnection = self.clientApi:connect(function(message: string, ...)
		if self.connections[message] then
			for _, connection in pairs(self.connections[message]) do
				connection(...)
			end
		end
	end)

	return self
end

function client:cleanup()
	table.clear(self.connections)
	self._mainConnection:Disconnect()
end

function client:_connect(message: string, func: (...any) -> nil): ScriptConnection
	table.insert(self.connections[message], func)
	local connection = {}
	function connection.disconnect()
		table.remove(self.connections[message], self.connections[message]:find(func))
	end
	return connection
end

function client:clientMessageRecieved(
	func: (channelId: number, player: Player, message: string) -> nil
): ScriptConnection
	return self:_connect("clientMessageRecieved", func)
end

function client:systemMessageRecieved(func: (
	channelId: number,
	message: {
		text: string,
		headerText: string?,
		backgroundColor3: Color3?,
		icon: string?,
		iconColor3: Color3?,
		iconRounded: boolean?,
		sideText: string?,
		sideAccent: Color3?,
	}
) -> nil): ScriptConnection
	return self:_connect("systemMessageRecieved", func)
end

function client:panicButtonActivated(
	func: (channelId: number | boolean, player: Player, message: string) -> nil
): ScriptConnection
	return self:_connect("panicButtonActivated", func)
end

function client:messageHistoryRecieved(func: (channelId: number, history: {}) -> nil): ScriptConnection
	return self:_connect("messageHistoryRecieved", func)
end

function client:voiceRecieve(func: (channelId: number, player: Player) -> nil): ScriptConnection
	return self:_connect("voiceRecieve", func)
end

function client:textActivateChanged(func: (active: boolean) -> nil): ScriptConnection
	return self:_connect("textActivateChanged", func)
end

function client:voiceActivateChanged(func: (active: boolean) -> nil): ScriptConnection
	return self:_connect("voiceActivateChanged", func)
end

function client:getEnabled(): boolean
	return self.apiEvents.folder:WaitForChild("clientApiFunction"):Invoke("getEnabled")
end

function client:setEnabled(enabled: boolean)
	self.clientApi:fire("setEnabled", enabled)
end

return client


--[[
	 _____               _                     _______                 _
	|  __ \             | |                   |__   __|               | |
	| |__) |   ___    __| |   ___    _ __        | |      ___    ___  | |__
	|  _  /   / _ \  / _  |  / _ \  |  _ \       | |     / _ \  / __| |  _ \
	| | \ \  |  __/ | (_| | | (_) | | | | |      | |    |  __/ | (__  | | | |
	|_|  \_\  \___|  \__,_|  \___/  |_| |_|      |_|     \___|  \___| |_| |_|
	Radio System
	Server API

	MIT License
	Copyright (c) 2024 Redon Tech
--]]

-- Types

export type ScriptConnection = {
	disconnect: () -> nil,
}

-- Everything Else

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local shared = ReplicatedStorage:WaitForChild("radioShared")
local comm = require(shared:WaitForChild("comm"))

local server = {}
server.__index = server

function server.init()
	local apiEvents = comm.new("api")
	local data = {
		apiEvents = apiEvents,
		serverApi = apiEvents:bindableEvent("serverApi"),
		connections = {
			receivedClientMessage = {},
			receivedActivateVoice = {},
			authorizedClient = {},
			channelAddPlayer = {},
			channelRemovePlayer = {},
		},
	}
	local self = setmetatable(data, server)

	self._mainConnection = self.serverApi:connect(function(message: string, ...)
		if self.connections[message] then
			for _, connection in pairs(self.connections[message]) do
				connection(...)
			end
		end
	end)

	return self
end

function server:cleanup()
	table.clear(self.connections)
	self._mainConnection:Disconnect()
end

function server:_connect(message: string, func: (...any) -> nil): ScriptConnection
	table.insert(self.connections[message], func)
	local connection = {}
	function connection.disconnect()
		table.remove(self.connections[message], self.connections[message]:find(func))
	end
	return connection
end

function server:receivedClientMessage(
	func: (player: Player, message: string, channelId: number) -> nil
): ScriptConnection
	return self:_connect("receivedClientMessage", func)
end

function server:receivedActivateVoice(func: (player: Player, channelId: number) -> nil): ScriptConnection
	return self:_connect("receivedActivateVoice", func)
end

function server:receivedPanic(func: (player: Player, channelId: number) -> nil): ScriptConnection
	return self:_connect("receivedPanic", func)
end

function server:authorizedClient(func: (player: Player, authorizedChannels: { number }) -> nil): ScriptConnection
	return self:_connect("authorizedClient", func)
end

function server:channelAddPlayer(func: (id: number, player: Player) -> nil): ScriptConnection
	return self:_connect("channelAddPlayer", func)
end

function server:channelRemovePlayer(func: (id: number, player: Player) -> nil): ScriptConnection
	return self:_connect("channelRemovePlayer", func)
end

function server:createSystemMessage(
	channelId: number,
	message: {
		text: string,
		headerText: string?,
		backgroundColor3: Color3?,
		icon: string?,
		iconColor3: Color3?,
		iconRounded: boolean?,
		sideText: string?,
		sideAccent: Color3?,
	}
)
	self.serverApi:fire("createSystemMessage", channelId, message)
end

function server:activatePlayersVoice(channelId: number, player: Player)
	self.serverApi:fire("activatePlayersVoice", channelId, player)
end

return server


local localPlayer = game.Players.LocalPlayer
local ScreenGui = localPlayer:WaitForChild("PlayerGui"):WaitForChild("ScreenGui")
local template = ScreenGui:WaitForChild("Frame"):WaitForChild("template")
template.Visible = false

for i, v in pairs(game.Teams:GetChildren()) do
	local clone = template:Clone()
	clone.Name = v.Name
	clone.Parent = template.Parent
	clone.Visible = true
	clone.Text = v.Name

	clone.MouseButton1Click:Connect(function()
		game.ReplicatedStorage.RemoteEvent:FireServer(v.Name)
	end)
end


game.ReplicatedStorage.RemoteEvent.OnServerEvent:Connect(function(player: Player, teamName: string)
	player.Team = game.Teams[teamName]
	player:LoadCharacter()
end)


--[[
	 _____               _                     _______                 _
	|  __ \             | |                   |__   __|               | |
	| |__) |   ___    __| |   ___    _ __        | |      ___    ___  | |__
	|  _  /   / _ \  / _  |  / _ \  |  _ \       | |     / _ \  / __| |  _ \
	| | \ \  |  __/ | (_| | | (_) | | | | |      | |    |  __/ | (__  | | | |
	|_|  \_\  \___|  \__,_|  \___/  |_| |_|      |_|     \___|  \___| |_| |_|
	Radio System
	Settings
--]]

local settings = {}

--\ CHANNELS /--

--[[
	This configures all avaliable channels, each team can get specific acces as configured below.
	Format:
	[id] = {
		["name"] = "channelName",
	}
--]]
settings.channels = {
	[1] = {
		["name"] = "Main",
	},
	[2] = {
		["name"] = "Tac1",
	},
	[3] = {
		["name"] = "Tac2",
	},
	[4] = {
		["name"] = "Police",
	},
	[5] = {
		["name"] = "Fire",
	},
}

--[[
	How many messages should be stored in the channel history?
	Setting this to 0 will disable history.
	This only affects when a player receives the radio gui,
	it will not change how much history is stored as the channel
	fills up. Basically player join they get x history of the channel.
--]]
settings.channelHistory = 100

--\ VOICE /--

--[[
	How should the voice chat be heard?
	"player" - Audio is from the player's character
	"camera" - Audio is from the player's camera
]]
settings.voiceType = "player"

--[[
	Audio boost
	Increase this if the audio is too quiet, max is 3
	This max is a limit of Roblox, do not report issues if you cant get over 3.
]]
settings.audioBoost = 1

--\ ACCESS /--

--[[
	All access is simply defined by either the channel numbers,
	or false for none,
	or true for all.
--]]

settings.default = false

settings.teams = {
	["Police"] = {
		1,
		2,
		3,
		4,
	},
	["Fire"] = {
		1,
		2,
		3,
		5,
	},
}

settings.users = {
	[123456789000] = true,
	["over_twenty_characters"] = true,
}

--\ PANIC /--

--[[
	Is the panic button enabled?
]]
settings.panicButtonEnabled = true

--[[
	Panic button behavior

	"all" - All channels will recieve panic
	"channel" - Only the current channel will recieve panic
]]
settings.panicBehavior = "all"

--[[
	Determins the message sent when the panic button is pressed
	Use this to give location or other information

	Function MUST return a string
]]
settings.getPanicMessage = function(player: Player): string
	return `{player.Name} has activated the panic button!`
end

--[[
	Sets the panic button cooldown in seconds
	This is per user
]]
settings.panicCooldown = 30

--\ UI /--

--[[
	Where should the UI be positioned?
	TopLeft, TopRight, BottomLeft, BottomRight
--]]
settings.uiPosition = "TopLeft"

settings.overrideUiPosition = nil -- Set to a UDim2 to override the position

--[[
	Change the size of the UI
	Must be a UDim2
]]
settings.uiSize = UDim2.fromScale(0.4, 0.35)

settings.uiMaxSize = Vector2.new(475, math.huge) -- Same as new TextChatService default

--[[
	The keybinds used to enable/disable mic/text
]]
settings.keybinds = {
	text = Enum.KeyCode.T,
	mic = Enum.KeyCode.Y,
	hide = Enum.KeyCode.H, -- You can also click the "Radio" text
}

--[[
	Until Roblox makes public audios possible you have to
	manually remake this audios...
	Default audio is from http://www.w2sjw.com/radio_sounds.html
	sideTone is MDC_Sidetone
	keyDown/keyUp/messageRecieved is MDC1200
	panic button is a free audio taken from the marketplace
	it should work without any changes.

	You can set to nil or false to disable any sounds
--]]
settings.audio = {
	messageRecieved = "rbxassetid://16800392019",
	sideTone = "rbxassetid://16800390322",
	keyDown = "rbxassetid://16800392019",
	keyUp = "rbxassetid://16800392019",
	panic = "rbxassetid://4887758282", -- This should work out of the box
}

--\ CHAT CONFIG /--

--[[
	When the player is on a team with radio access, should the chat window be force removed?

	Removes the chat window, this makes it easier to use the radio when in the TopLeft position.
	**ONLY WORKS WITH NEW TEXT CHAT SERVICE**
--]]
settings.overrideWindowEnabled = true

--[[
	Adds padding to the UI to account for the chat window
	Recommended to set to 0 if not using TopLeft position

	Default is 50
]]
settings.additionalTopPadding = 50

--[[
	Default text chat channel
	Only change this if you have TextChatService.CreateDefaultChannels set to false
]]
settings.defaultTextChannel = "RBXGeneral"

--\ DEVELOPER /--
settings.v = "1.3"
settings.debug = false

return settings


-- Redon Tech Radio Systen

local audio = {}
audio.__index = audio

function audio.new(main: { any }, assetId: string | boolean?, looped: boolean)
	local data = {
		main = main,
		sound = Instance.new("Sound"),
		isEnabled = typeof(assetId) == "string",
	}
	local self = setmetatable(data, audio)

	self.sound.SoundId = assetId
	self.sound.Looped = looped

	return self
end

function audio:play()
	if self.isEnabled == false then
		return
	end

	self.sound:Play()
end

function audio:stop()
	if self.isEnabled == false then
		return
	end

	self.sound:Stop()
end

function audio:setParent(parent: Instance)
	self.sound.Parent = parent
end

return audio


local ReplicatedStorage = game:GetService("ReplicatedStorage")
local shared = ReplicatedStorage:WaitForChild("radioShared")
local Fusion = require(shared:WaitForChild("Fusion"))
local util = require(shared:WaitForChild("util"))

local New, Children = Fusion.New, Fusion.Children

return function(props)
	local message: Frame = New("Frame")({
		Name = props.id,
		Size = UDim2.new(1, 0, 0, 50),
		BackgroundTransparency = 0,
		BackgroundColor3 = props.backgroundColor3 or Color3.fromRGB(50, 50, 50),
		BorderSizePixel = 0,
		ClipsDescendants = true,
		LayoutOrder = props.id,

		[Children] = {
			UICorner = New("UICorner")({
				CornerRadius = UDim.new(0, 10),
			}),
			Icon = if props.icon ~= nil
				then New("ImageLabel")({
					Size = UDim2.new(0, 50, 0, 50),
					Position = UDim2.new(0, 5, 0, 5),
					BackgroundTransparency = 0,
					BackgroundColor3 = Color3.fromRGB(40, 40, 40),
					Image = props.icon,
					ImageColor3 = props.iconColor3 or Color3.fromRGB(255, 255, 255),
					ScaleType = Enum.ScaleType.Fit,

					[Children] = {
						UICorner = New("UICorner")({
							CornerRadius = UDim.new(if props.iconRounded then 1 else 0, 0),
						}),
					},
				})
				else nil,
			SideText = if (props.sideText ~= nil or (props.icon == nil and props.sideText == nil))
				then New("TextLabel")({
					Size = UDim2.new(0, 50, 0, 15),
					AnchorPoint = Vector2.new(0, 0.5),
					Position = UDim2.new(0, 5, 0, 22),
					BackgroundTransparency = 1,
					Text = props.sideText or game.Lighting.TimeOfDay,
					FontFace = Font.new(
						"rbxasset://fonts/families/Arial.json",
						Enum.FontWeight.Bold,
						Enum.FontStyle.Normal
					),
					TextScaled = true,
					TextColor3 = Color3.fromRGB(178, 178, 178),
					TextXAlignment = Enum.TextXAlignment.Left,
					TextYAlignment = Enum.TextYAlignment.Top,
				})
				else nil,
			SideAccent = if props.sideAccent ~= nil
				then New("Frame")({
					Size = UDim2.fromScale(1, 1),
					AnchorPoint = Vector2.new(0.5, 0.5),
					Position = UDim2.fromScale(0.5, 0.5),
					BackgroundColor3 = props.sideAccent,

					[Children] = {
						UICorner = New("UICorner")({
							CornerRadius = UDim.new(0, 10),
						}),
						UIGradient = New("UIGradient")({
							Transparency = NumberSequence.new({
								NumberSequenceKeypoint.new(0, 0),
								NumberSequenceKeypoint.new(0.0075, 0),
								NumberSequenceKeypoint.new(0.0076, 1),
								NumberSequenceKeypoint.new(1, 1),
							}),
						}),
					},
				})
				else nil,
			HeaderText = if props.headerText ~= nil
				then New("TextLabel")({
					Size = UDim2.new(1, -65, 0, 15),
					Position = UDim2.new(0, 60, 0, 5),
					BackgroundTransparency = 1,
					Text = props.headerText,
					FontFace = Font.new(
						"rbxasset://fonts/families/Arial.json",
						Enum.FontWeight.Bold,
						Enum.FontStyle.Normal
					),
					TextScaled = true,
					TextColor3 = Color3.fromRGB(178, 178, 178),
					TextXAlignment = Enum.TextXAlignment.Left,
				})
				else nil,
			Text = New("TextLabel")({
				Name = "Text",
				Size = UDim2.new(1, -65, 1, -20),
				AnchorPoint = Vector2.new(0, 1),
				Position = UDim2.new(0, 60, 1, -5),
				BackgroundTransparency = 1,
				Text = props.text,
				FontFace = Font.new(
					"rbxasset://fonts/families/Arial.json",
					Enum.FontWeight.Regular,
					Enum.FontStyle.Normal
				),
				TextSize = 14,
				RichText = props.richText or false,
				TextColor3 = Color3.fromRGB(255, 255, 255),
				TextXAlignment = Enum.TextXAlignment.Left,
				TextWrapped = true,
			}),
		},
	})

	local connections = {}

	connections[#connections + 1] = message.AncestryChanged:Connect(function()
		local text = message:FindFirstChild("Text")
		if message.Parent ~= nil and text ~= nil then
			message.Size = UDim2.new(
				1,
				0,
				0,
				math.max(60, util.getMessageHeight(text, message, message.AbsoluteSize.X - 65) + 20 + (5 * 2))
			)
		end
	end)

	connections[#connections + 1] = message.Destroying:Connect(function()
		for i = 1, #connections do
			connections[i]:Disconnect()
		end
	end)

	return message
end


local ReplicatedStorage = game:GetService("ReplicatedStorage")
local shared = ReplicatedStorage:WaitForChild("radioShared")
local Fusion = require(shared:WaitForChild("Fusion"))

local New, Children, OnEvent = Fusion.New, Fusion.Children, Fusion.OnEvent

return function(props)
	local messages
	messages = New("ScrollingFrame")({
		Name = "Messages",
		Size = UDim2.fromScale(0.95, 0.606),
		BackgroundTransparency = 1,
		AnchorPoint = Vector2.new(0.5, 0),
		Position = UDim2.fromScale(0.5, 0.329),
		ScrollBarThickness = 5,
		AutomaticCanvasSize = Enum.AutomaticSize.Y,
		CanvasSize = UDim2.new(),

		BottomImage = "rbxasset://textures/ui/Scroll/scroll-middle.png",
		MidImage = "rbxasset://textures/ui/Scroll/scroll-middle.png",
		TopImage = "rbxasset://textures/ui/Scroll/scroll-middle.png",
		VerticalScrollBarInset = Enum.ScrollBarInset.Always,

		[OnEvent("ChildAdded")] = function()
			task.wait() -- Roblox!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
			messages.CanvasPosition = Vector2.new(0, messages.AbsoluteCanvasSize.Y)
		end,

		[Children] = {
			--selene: allow(mixed_table)
			UIListLayout = New("UIListLayout")({
				FillDirection = Enum.FillDirection.Vertical,
				SortOrder = Enum.SortOrder.LayoutOrder,
				VerticalAlignment = Enum.VerticalAlignment.Top,
				HorizontalAlignment = Enum.HorizontalAlignment.Center,
				Padding = UDim.new(0, 5),
			}),

			props[Children],
		},
	})

	return messages
end


local ReplicatedStorage = game:GetService("ReplicatedStorage")
local shared = ReplicatedStorage:WaitForChild("radioShared")
local Fusion = require(shared:WaitForChild("Fusion"))

local New, Children, Computed, OnEvent = Fusion.New, Fusion.Children, Fusion.Computed, Fusion.OnEvent

return function(props)
	return New("TextButton")({
		Name = props.Id,
		LayoutOrder = props.Id,
		Size = UDim2.fromScale(1, 0.9),
		BackgroundTransparency = 1,
		AnchorPoint = Vector2.new(0.5, 0),
		Position = UDim2.fromScale(0.5, 0),

		Text = props.Text,
		FontFace = Computed(function()
			if props.activeChannel:get() == props.Id then
				return Font.new("rbxasset://fonts/families/Arial.json", Enum.FontWeight.Bold, Enum.FontStyle.Normal)
			else
				return Font.new("rbxasset://fonts/families/Arial.json", Enum.FontWeight.Regular, Enum.FontStyle.Normal)
			end
		end),
		TextScaled = true,
		TextColor3 = Color3.fromRGB(255, 255, 255),
		TextXAlignment = Enum.TextXAlignment.Center,

		[OnEvent("Activated")] = function()
			props.activeChannel:set(props.Id)
		end,

		[Children] = {
			Active = New("Frame")({
				Name = "Active",
				Size = UDim2.fromScale(0.75, 0.075),
				BackgroundColor3 = Color3.fromRGB(255, 255, 255),
				BorderSizePixel = 0,
				AnchorPoint = Vector2.new(0.5, 1),
				Position = UDim2.fromScale(0.5, 1),
				ZIndex = 0,

				Visible = Computed(function()
					return props.activeChannel:get() == props.Id
				end),
			}),
		},
	})
end


local ReplicatedStorage = game:GetService("ReplicatedStorage")
local shared = ReplicatedStorage:WaitForChild("radioShared")
local Fusion = require(shared:WaitForChild("Fusion"))

local New, Children, OnEvent = Fusion.New, Fusion.Children, Fusion.OnEvent

return function(props)
	local frame: Frame?
	frame = New("Frame")({
		Name = "Channels",
		Size = UDim2.fromScale(1, 0.087),
		BackgroundColor3 = Color3.fromRGB(35, 35, 35),
		BorderSizePixel = 0,
		AnchorPoint = Vector2.new(0.5, 0),
		Position = UDim2.fromScale(0.5, 0.173),

		[OnEvent("ChildAdded")] = function(child: TextButton)
			if child:IsA("TextButton") then
				if frame == nil then
					return
				end
				for _, v in pairs(frame:GetChildren()) do
					if v:IsA("TextButton") then
						v.Size = UDim2.fromScale(1 / (#frame:GetChildren() - 1), 1)
					end
				end
			end
		end,

		[Children] = {
			--selene: allow(mixed_table)
			UIListLayout = New("UIListLayout")({
				FillDirection = Enum.FillDirection.Horizontal,
				SortOrder = Enum.SortOrder.LayoutOrder,
				Padding = UDim.new(0, 0),
			}),

			props[Children],
		},
	})

	if frame == nil then
		return
	end

	for _, v in pairs(frame:GetChildren()) do
		if v:IsA("TextButton") then
			v.Size = UDim2.fromScale((1 / (#frame:GetChildren() - 1)), 1)
		end
	end

	return frame
end


local ReplicatedStorage = game:GetService("ReplicatedStorage")
local shared = ReplicatedStorage:WaitForChild("radioShared")
local Fusion = require(shared:WaitForChild("Fusion"))

local New, Children, OnEvent = Fusion.New, Fusion.Children, Fusion.OnEvent

return function(props)
	return New("Frame")({
		Name = "Topbar",
		Size = UDim2.fromScale(1, 0.173),
		BackgroundColor3 = Color3.fromRGB(30, 30, 30),
		BorderSizePixel = 0,
		AnchorPoint = Vector2.new(0.5, 0),
		Position = UDim2.fromScale(0.5, 0),

		[Children] = {
			UICorner = New("UICorner")({
				CornerRadius = UDim.new(0, 10),
			}),

			--selene: allow(mixed_table)
			Frame = New("Frame")({
				Name = "Cover",
				Size = UDim2.new(1, 0, 0, 10),
				BackgroundColor3 = Color3.fromRGB(30, 30, 30),
				BorderSizePixel = 0,
				AnchorPoint = Vector2.new(0.5, 1),
				Position = UDim2.fromScale(0.5, 1),
				ZIndex = 0,
			}),

			New("TextButton")({
				Name = "Title",
				Size = UDim2.fromScale(0.2, 1),
				BackgroundTransparency = 1,
				AnchorPoint = Vector2.new(0, 0),
				Position = UDim2.fromScale(0.01, 0),

				Text = "Radio",
				FontFace = Font.new(
					"rbxasset://fonts/families/Arial.json",
					Enum.FontWeight.Bold,
					Enum.FontStyle.Normal
				),
				TextScaled = true,
				TextColor3 = Color3.fromRGB(255, 255, 255),
				TextXAlignment = Enum.TextXAlignment.Left,

				[OnEvent("Activated")] = function()
					props.visible:set(false)
				end,
			}),

			New("TextButton")({
				Name = "Panic",
				Size = UDim2.fromScale(0.2, 0.55),
				BackgroundColor3 = Color3.fromRGB(200, 0, 0),
				AnchorPoint = Vector2.new(1, 0.5),
				Position = UDim2.fromScale(0.83, 0.5),
				AutoButtonColor = true,

				Text = "Panic",
				FontFace = Font.new(
					"rbxasset://fonts/families/Arial.json",
					Enum.FontWeight.Regular,
					Enum.FontStyle.Normal
				),
				TextScaled = true,
				TextColor3 = Color3.fromRGB(255, 255, 255),
				TextXAlignment = Enum.TextXAlignment.Center,

				[Children] = {
					UICorner = New("UICorner")({
						CornerRadius = UDim.new(0.25, 0),
					}),
				},
			}),

			New("ImageButton")({
				Name = "Chat",
				Size = UDim2.fromScale(0.05, 0.6),
				BackgroundTransparency = 1,
				AnchorPoint = Vector2.new(1, 0.5),
				Position = UDim2.fromScale(0.911, 0.5),
				Image = "rbxassetid://16516253569",
				ImageColor3 = Color3.fromRGB(255, 0, 0),
				ScaleType = Enum.ScaleType.Fit,
			}),

			New("ImageButton")({
				Name = "Mic",
				Size = UDim2.fromScale(0.05, 0.6),
				BackgroundTransparency = 1,
				AnchorPoint = Vector2.new(1, 0.5),
				Position = UDim2.fromScale(0.975, 0.5),
				Image = "rbxassetid://16516226674",
				ImageColor3 = Color3.fromRGB(255, 0, 0),
				ScaleType = Enum.ScaleType.Fit,
			}),
		},
	})
end


-- Redon Tech Radio System

local main = {}
main.__index = main

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

local localPlayer = Players.LocalPlayer
local shared = ReplicatedStorage:WaitForChild("radioShared")
local effects = shared:WaitForChild("effects")
local audioEqualizer = effects:WaitForChild("1 - Equalizer")
local audioFader = effects:WaitForChild("3 - Fader")
local Fusion = require(shared:WaitForChild("Fusion"))
local systemSettings = require(shared:WaitForChild("settings"))
local comm = require(shared:WaitForChild("comm"))
local components = script.Parent:WaitForChild("components")
local audio = require(script.Parent:WaitForChild("audio"))

local New, Children, Value, ForPairs, Observer, OnEvent =
	Fusion.New, Fusion.Children, Fusion.Value, Fusion.ForPairs, Fusion.Observer, Fusion.OnEvent
local radioTopbarComponent = require(components:WaitForChild("radioTopbar"))
local radioChannelsComponent = require(components:WaitForChild("radioChannels"))
local radioChannelComponent = require(components:WaitForChild("radioChannel"))
local messagesComponent = require(components:WaitForChild("messages"))
local messageComponent = require(components:WaitForChild("message"))

local print = function(...)
	if systemSettings.debug then
		print(`[RTRS Client]: `, ...)
	end
end
local warn = function(...)
	if systemSettings.debug then
		warn(`[RTRS Client]: `, ...)
	end
end

function main.init()
	print("Initializing Client")
	-- We run this before everything else to prevent errors :D
	if TextChatService.ChatVersion == Enum.ChatVersion.TextChatService then
		local windowConfig = TextChatService:FindFirstChildOfClass("ChatWindowConfiguration")
		windowConfig:SetAttribute("defaultState", windowConfig.Enabled)
	end

	local textEvents = comm.new("text")
	local voiceEvents = comm.new("voice")
	local apiEvents = comm.new("api")
	local data = {
		textEvents = {
			comm = textEvents,
			systemMessage = textEvents:remoteEvent("systemMessage"),
			panicButton = textEvents:remoteEvent("panicButton"),
			sendHistory = textEvents:remoteEvent("sendHistory"),
		},
		voiceEvents = {
			comm = voiceEvents,
			activateVoice = voiceEvents:remoteEvent("activateVoice"),
		},
		apiEvents = apiEvents,
		clientApi = apiEvents:bindableEvent("clientApi"),
		enabled = Value(false),
		visible = Value(nil),
		authorizedChannels = Value({}),
		activeChannel = Value(1),
		activeMessages = Value({}),
		activeVoice = Value(nil),
		activeWire = Value(nil),
		textActive = Value(false),
		voiceActive = Value(false),
		channelConnections = {},
	}
	local self = setmetatable(data, main)

	self:setupObservers()
	self:setupEvents()
	self.channels = self:setupChannels()
	self.mainUi = self:createUi()
	self:requestAuthorizedChannels()

	self.messageRecieved = audio.new(self, systemSettings.audio.messageRecieved, false)
	self.sideTone = audio.new(self, systemSettings.audio.sideTone, true)
	self.keyDown = audio.new(self, systemSettings.audio.keyDown, false)
	self.keyUp = audio.new(self, systemSettings.audio.keyUp, false)
	self.panic = audio.new(self, systemSettings.audio.panic, false)
	if localPlayer.Character then
		self.messageRecieved:setParent(localPlayer.Character.HumanoidRootPart)
		self.sideTone:setParent(localPlayer.Character.HumanoidRootPart)
		self.keyDown:setParent(localPlayer.Character.HumanoidRootPart)
		self.keyUp:setParent(localPlayer.Character.HumanoidRootPart)
		self.panic:setParent(localPlayer.Character.HumanoidRootPart)
	end

	print("Initialized Client")
	return self
end

function main:setupEvents()
	self.textEvents.systemMessage:connect(function(...)
		self:receiveMessage(...)
		self.clientApi:fire("systemMessageRecieved", ...)
	end)

	self.textEvents.sendHistory:connect(function(channelId, history: {})
		self:recieveChannelHistory(channelId, history)
		self.clientApi:fire("messageHistoryRecieved", channelId, history)
	end)

	self.textEvents.panicButton:connect(function(...)
		self:recievePanicButton(...)
		self.clientApi:fire("panicButtonActivated", ...)
	end)

	self.voiceEvents.activateVoice:connect(function(channelId, player)
		self:activateVoice(channelId, player)
		self.clientApi:fire("voiceRecieve", channelId, player)
	end)

	self.clientApiFunction = self.apiEvents:bindableFunction("clientApiFunction", function(message: string)
		if message == "getEnabled" then
			return self.enabled:get()
		end
		return
	end)

	self.clientApi:connect(function(message: string, ...)
		if message == "setEnabled" and typeof(...) == "boolean" then
			self.enabled:set(...)
		end
	end)

	UserInputService.InputBegan:Connect(function(input, robloxProcessedEvent)
		if robloxProcessedEvent == true then
			return
		end
		if input.KeyCode == systemSettings.keybinds.text then
			self.textActive:set(not self.textActive:get())
		elseif input.KeyCode == systemSettings.keybinds.mic then
			self.voiceActive:set(true)
		elseif input.KeyCode == systemSettings.keybinds.hide then
			self.visible:set(not self.visible:get())
		end
	end)

	UserInputService.InputEnded:Connect(function(input, robloxProcessedEvent)
		if robloxProcessedEvent == true then
			return
		end
		if input.KeyCode == systemSettings.keybinds.mic then
			self.voiceActive:set(false)
		end
	end)

	localPlayer:GetPropertyChangedSignal("Team"):Connect(function()
		self:requestAuthorizedChannels()
	end)

	localPlayer.CharacterAdded:Connect(function(character)
		print("Character Added")
		character:WaitForChild("HumanoidRootPart")
		if self.enabled:get() == true then
			print("Radio Enabled")
			local emitterWire = character:WaitForChild("RSEmitter"):WaitForChild("Wire")
			emitterWire.SourceInstance = audioFader
			if systemSettings.voiceType:lower() == "camera" then
				emitterWire.Parent.Parent = workspace.CurrentCamera
			end
			local wire = shared:WaitForChild("wires"):WaitForChild(self.activeChannel:get())
			wire.TargetInstance = audioEqualizer
			self.activeWire:set(wire)
		end
		self.messageRecieved:setParent(character.HumanoidRootPart)
		self.sideTone:setParent(character.HumanoidRootPart)
		self.keyDown:setParent(character.HumanoidRootPart)
		self.keyUp:setParent(character.HumanoidRootPart)
		self.panic:setParent(character.HumanoidRootPart)
	end)
end

function main:setupObservers()
	local activeChannelObserver = Observer(self.activeChannel)
	local activeMessagesObserver = Observer(self.activeMessages)
	local textActiveObserver = Observer(self.textActive)
	local voiceActiveObserver = Observer(self.voiceActive)
	local activeVoiceObserver = Observer(self.activeVoice)
	local enabledObserver = Observer(self.enabled)
	local visibleObserver = Observer(self.visible)

	activeChannelObserver:onChange(function()
		self:activeChannelChange()
	end)

	activeMessagesObserver:onChange(function()
		local activeMessages = self.activeMessages:get()
		if #activeMessages >= 100 then
			table.remove(activeMessages, #activeMessages - 100)
		end
	end)

	textActiveObserver:onChange(function()
		local textActive = self.textActive:get()
		self.clientApi:fire("textActivateChange", textActive)
		self.mainUi.Container.Radio.Topbar.Chat.Image = textActive and "rbxassetid://16516225857"
			or "rbxassetid://16516253569"
		self.mainUi.Container.Radio.Topbar.Chat.ImageColor3 = textActive and Color3.fromRGB(255, 255, 255)
			or Color3.fromRGB(255, 0, 0)

		if textActive == true and self.enabled:get() == true then
			local channel = self.authorizedChannels:get()[self.activeChannel:get()]
			TextChatService:FindFirstChildOfClass("ChatInputBarConfiguration").TargetTextChannel = channel
		else
			TextChatService:FindFirstChildOfClass("ChatInputBarConfiguration").TargetTextChannel =
				TextChatService:FindFirstChild(systemSettings.defaultTextChannel, true)
		end
	end)

	voiceActiveObserver:onChange(function()
		local voiceActive = self.voiceActive:get()
		self.mainUi.Container.Radio.Topbar.Mic.Image = voiceActive and "rbxassetid://16516224290"
			or "rbxassetid://16516226674"
		self.mainUi.Container.Radio.Topbar.Mic.ImageColor3 = voiceActive and Color3.fromRGB(255, 255, 255)
			or Color3.fromRGB(255, 0, 0)
		self.clientApi:fire("voiceActivateChange", voiceActive)

		if voiceActive == true and self.enabled:get() == true then
			if self.activeVoice:get() == nil then
				self.voiceEvents.activateVoice:fire(self.activeChannel:get())
				self.keyDown:play()
			else
				self.sideTone:play()
			end
		else
			if self.activeVoice:get() == localPlayer then
				self.voiceEvents.activateVoice:fire(self.activeChannel:get())
				self.keyUp:play()
			end
			self.sideTone:stop()
		end
	end)

	activeVoiceObserver:onChange(function()
		local activeVoice = self.activeVoice:get()
		if activeVoice == localPlayer then
			self.activeWire:get().TargetInstance = nil
		elseif localPlayer.Character ~= nil then
			self.activeWire:get().TargetInstance = audioEqualizer
		end
	end)

	local function setVisible(forceTo: boolean?)
		local visible = if forceTo ~= nil then forceTo else self.visible:get()
		local enabled = self.enabled:get()

		self.mainUi.Container.Radio.Visible = if enabled == true then visible else false
		self.mainUi.Container.ShowRadio.Visible = if enabled == true then not visible else false
		self.mainUi.DisplayOrder = if visible == true then 0 else 999
	end
	visibleObserver:onChange(setVisible)

	enabledObserver:onChange(function()
		local enabled = self.enabled:get()
		setVisible(enabled)
		self.visible:set(enabled)

		if enabled == true then
			if systemSettings.overrideWindowEnabled == true then
				if TextChatService.ChatVersion == Enum.ChatVersion.TextChatService then
					TextChatService:FindFirstChildOfClass("ChatWindowConfiguration").Enabled = false
				end
			end
		elseif enabled == false then
			TextChatService:FindFirstChildOfClass("ChatInputBarConfiguration").TargetTextChannel =
				TextChatService:FindFirstChild(systemSettings.defaultTextChannel, true)
			if systemSettings.overrideWindowEnabled == true then
				if TextChatService.ChatVersion == Enum.ChatVersion.TextChatService then
					local windowConfig = TextChatService:FindFirstChildOfClass("ChatWindowConfiguration")
					windowConfig.Enabled = windowConfig:GetAttribute("defaultState")
				end
			end

			if self.activeWire:get() ~= nil then
				self.activeWire:get().TargetInstance = nil
				self.activeWire:set(nil)
			end
		end
	end)
end

function main:setupChannels()
	print("Setting up channels")

	local channels = {}
	for id, _ in pairs(systemSettings.channels) do
		local messages = Value({})
		channels[id] = messages

		local messagesObserver = Observer(messages)
		messagesObserver:onChange(function()
			local newMessages = messages:get()
			if #newMessages >= 100 then
				table.remove(newMessages, #newMessages - 100)
			end
		end)
	end

	print("Channels set up")
	return channels
end

-- Combo Features

function main:requestAuthorizedChannels()
	-- clear current messages and authorized channels
	self.activeMessages:set({})
	self.authorizedChannels:set({})
	self.channels = self:setupChannels()
	for _, connection in self.channelConnections do
		connection:Disconnect()
	end

	local success, error = pcall(function()
		local authorizedChannels = {}
		local numberOfChannels = 0
		for channelId, textChannel: TextChannel in pairs(self.textEvents.comm:remoteFunction("authorize")) do
			authorizedChannels[channelId] = textChannel
			numberOfChannels += 1

			table.insert(
				self.channelConnections,
				textChannel.MessageReceived:Connect(function(message: TextChatMessage)
					local player = Players:GetPlayerByUserId(message.TextSource.UserId)
					self:receiveClientMessage({
						channelId = channelId,
						player = player,
						message = message.Text,
					})
					self.clientApi:fire("clientMessageRecieved", channelId, player, message.Text)
				end)
			)
		end
		self.authorizedChannels:set(authorizedChannels)

		if numberOfChannels == 0 then
			self.enabled:set(false)
		else
			self.enabled:set(true)
			if authorizedChannels[self.activeChannel:get()] == nil then
				for channelId, _ in pairs(authorizedChannels) do
					self.activeChannel:set(channelId)
					break
				end
			else
				self:activeChannelChange()
			end
		end
	end)

	if success == false then
		warn(`Failed to retrieve authorized channels\n{error}`)
		self.enabled:set(false)
	end
end

function main:activeChannelChange()
	local activeChannel = self.activeChannel:get()
	local channelMessages = self.channels[activeChannel]:get()
	self.activeMessages:set(table.clone(channelMessages))
	if self.textActive:get() == true then
		local channel = self.authorizedChannels:get()[activeChannel]
		TextChatService:FindFirstChildOfClass("ChatInputBarConfiguration").TargetTextChannel = channel
	end
	if self.activeWire:get() then
		self.activeWire:get().TargetInstance = nil
	end
	if localPlayer.Character ~= nil then
		local wire = shared:WaitForChild("wires"):WaitForChild(activeChannel)
		wire.TargetInstance = audioEqualizer
		self.activeWire:set(wire)
	end
end

-- Voice Features

function main:activateVoice(channelId: number, player: Player)
	if self.activeChannel:get() == channelId then
		self.activeVoice:set(player)
		print(self.activeWire:get().Connected)
	end
end

-- Text Features

function main:recieveChannelHistory(channelId: number, history: {})
	local channel = self.channels[channelId]
	if channel == nil then
		warn("Received history for non-existant channel")
		return
	end

	print("recievingHistory", history)
	for _, message in pairs(history) do
		if message.type == "player" then
			self:receiveClientMessage({
				channelId = channelId,
				player = message.data.player,
				message = message.data.message,
			})
		elseif message.type == "system" then
			self:receiveMessage({ channelId = channelId, message = message.data.message })
		elseif message.type == "panic" then
			self:recievePanicButton({
				channelId = channelId,
				player = message.data.player,
				message = message.data.message,
				noAudio = true,
			})
		end
	end
	if channelId == self.activeChannel:get() then
		self:activeChannelChange()
	end
end

function main:receiveClientMessage(data: { channelId: number, player: Player, message: string })
	local channelId = data.channelId
	local player = data.player
	local message = data.message

	local channel = self.channels[channelId]
	if channel == nil then
		warn("Received message for non-existant channel")
		return
	end

	local channelMessages = channel:get()
	local messageData = {
		id = #channelMessages + 1,
		icon = game.Players:GetUserThumbnailAsync(
			player.UserId,
			Enum.ThumbnailType.HeadShot,
			Enum.ThumbnailSize.Size48x48
		),
		iconRounded = true,
		headerText = player.Name,
		text = message,
		sideAccent = player.TeamColor.Color,
	}
	table.insert(channelMessages, messageData)
	channel:set(channelMessages)

	if self.activeChannel:get() == channelId then
		local activeMessages = self.activeMessages:get()
		table.insert(activeMessages, messageData)
		self.activeMessages:set(activeMessages)
		self.messageRecieved:play()
	end
end

function main:receiveMessage(data: {
	channelId: number,
	message: {
		text: string,
		headerText: string?,
		backgroundColor3: Color3?,
		icon: string?,
		iconColor3: Color3?,
		iconRounded: boolean?,
		sideText: string?,
		sideAccent: Color3?,
	},
	playPanicSound: boolean?,
})
	local channelId = data.channelId
	local message = data.message
	local playPanicSound = data.playPanicSound

	local channel = self.channels[channelId]
	if channel == nil then
		warn("Received message for non-existant channel")
		return
	end

	local channelMessages = channel:get()
	local messageData = {
		id = #channelMessages + 1,
	}
	for i, v in pairs(message) do
		messageData[i] = v
	end
	table.insert(channelMessages, messageData)
	channel:set(channelMessages)

	if self.activeChannel:get() == channelId then
		local activeMessages = self.activeMessages:get()
		table.insert(activeMessages, messageData)
		self.activeMessages:set(activeMessages)
		if playPanicSound == true then
			self.panic:play()
		else
			self.messageRecieved:play()
		end
	end
end

function main:recievePanicButton(
	data: { channelId: number | boolean, player: Player, message: string, noAudio: boolean? }
)
	local channelId = data.channelId
	local player = data.player
	local message = data.message
	local noAudio = data.noAudio

	print("panic", channelId, player, message, noAudio)
	local panicMessage = {
		text = message,
		headerText = player.Name,
		backgroundColor3 = Color3.fromRGB(175, 60, 60),
		icon = game.Players:GetUserThumbnailAsync(
			player.UserId,
			Enum.ThumbnailType.HeadShot,
			Enum.ThumbnailSize.Size48x48
		),
		iconRounded = true,
		sideAccent = player.TeamColor.Color,
	}

	if channelId == true then
		for id, _ in pairs(self.authorizedChannels:get()) do
			self:receiveMessage({
				channelId = id,
				message = panicMessage,
				playPanicSound = if noAudio == true then false else true,
			})
		end
	else
		self:receiveMessage({
			channelId = channelId,
			message = panicMessage,
			playPanicSound = if noAudio == true then false else true,
		})
	end
end

-- UI
local anchorPoint = Vector2.new(0, 0)
local position = UDim2.fromScale(0, 0)
if systemSettings.uiPosition:lower() == "topright" then
	anchorPoint = Vector2.new(1, 0)
	position = UDim2.fromScale(1, 0)
elseif systemSettings.uiPosition:lower() == "bottomleft" then
	anchorPoint = Vector2.new(0, 1)
	position = UDim2.fromScale(0, 1)
elseif systemSettings.uiPosition:lower() == "bottomright" then
	anchorPoint = Vector2.new(1, 1)
	position = UDim2.fromScale(1, 1)
elseif typeof(systemSettings.overrideUiPosition) == "UDim2" then
	position = systemSettings.overrideUiPosition
end

function main:createUi()
	local ui = New("ScreenGui")({
		Name = "radioClient",
		Parent = localPlayer:WaitForChild("PlayerGui"),
		ZIndexBehavior = Enum.ZIndexBehavior.Sibling,

		[Children] = {
			Container = New("Frame")({
				Name = "Container",
				Size = UDim2.fromScale(1, 1),
				BackgroundTransparency = 1,
				AnchorPoint = Vector2.new(0.5, 0.5),
				Position = UDim2.fromScale(0.5, 0.5),

				[Children] = {
					UIPadding = New("UIPadding")({
						PaddingTop = UDim.new(0, 5 + systemSettings.additionalTopPadding),
						PaddingBottom = UDim.new(0, 5),
						PaddingLeft = UDim.new(0, 8),
						PaddingRight = UDim.new(0, 8),
					}),

					ShowRadio = New("TextButton")({
						Name = "ShowRadio",
						Size = UDim2.fromScale(0.084, 0.037),
						BackgroundColor3 = Color3.fromRGB(40, 40, 40),
						AnchorPoint = Vector2.new(0, 1),
						Position = UDim2.fromScale(0, 1),
						Visible = false,

						Text = "Show Radio",
						FontFace = Font.new(
							"rbxasset://fonts/families/Arial.json",
							Enum.FontWeight.Bold,
							Enum.FontStyle.Normal
						),
						TextScaled = true,
						TextColor3 = Color3.fromRGB(255, 255, 255),
						TextXAlignment = Enum.TextXAlignment.Center,

						[OnEvent("Activated")] = function()
							self.visible:set(true)
						end,

						[Children] = {
							UICorner = New("UICorner")({
								CornerRadius = UDim.new(0, 10),
							}),

							UISizeConstraint = New("UISizeConstraint")({
								MaxSize = Vector2.new(200, 50),
							}),

							UIPadding = New("UIPadding")({
								PaddingTop = UDim.new(0, 3),
								PaddingBottom = UDim.new(0, 3),
								PaddingLeft = UDim.new(0, 3),
								PaddingRight = UDim.new(0, 3),
							}),
						},
					}),

					Radio = New("Frame")({
						Name = "Radio",
						Size = systemSettings.uiSize,
						BackgroundColor3 = Color3.fromRGB(40, 40, 40),
						BorderSizePixel = 0,
						AnchorPoint = anchorPoint,
						Position = position,
						Visible = false,

						[Children] = {
							UICorner = New("UICorner")({
								CornerRadius = UDim.new(0, 10),
							}),

							UISizeConstraint = New("UISizeConstraint")({
								MaxSize = systemSettings.uiMaxSize,
							}),

							Topbar = radioTopbarComponent({
								visible = self.visible,
							}),
							Channels = radioChannelsComponent({
								[Children] = {
									ForPairs(self.authorizedChannels, function(id: number, channel: TextChannel)
										return id,
											radioChannelComponent({
												Id = id,
												Text = channel.Name,
												activeChannel = self.activeChannel,
											})
									end, Fusion.cleanup),
								},
							}),
							Messages = messagesComponent({
								[Children] = {
									ForPairs(self.activeMessages, function(id, messageData)
										return id,
											messageComponent({
												id = id,
												backgroundColor3 = messageData.backgroundColor3,
												icon = messageData.icon,
												iconRounded = messageData.iconRounded,
												iconColor3 = messageData.iconColor3,
												sideText = messageData.sideText,
												sideAccent = messageData.sideAccent,
												headerText = messageData.headerText,
												text = messageData.text,
												richText = messageData.richText,
											})
									end, Fusion.cleanup),
								},
							}),
						},
					}),
				},
			}),
		},
	})

	ui.Container.Radio.Topbar.Chat.MouseButton1Click:Connect(function()
		self.textActive:set(not self.textActive:get())
	end)

	ui.Container.Radio.Topbar.Mic.MouseButton1Down:Connect(function()
		self.voiceActive:set(true)
	end)

	ui.Container.Radio.Topbar.Mic.MouseButton1Up:Connect(function()
		self.voiceActive:set(false)
	end)

	ui.Container.Radio.Topbar.Panic.MouseButton1Click:Connect(function()
		self.textEvents.panicButton:fire(self.activeChannel:get())
	end)

	if systemSettings.panicButtonEnabled == false then
		ui.Container.Radio.Topbar.Panic.Visible = false
	end

	return ui
end

return main


-- Redon Tech Radio System

local main = script.Parent:WaitForChild("main")

local success, result = pcall(function()
	return require(main).init()
end)

if not success then
	warn("[RTRS Client]: Client could not start\n", result, "\nTraceback:\n", debug.traceback())
end


--[[
	 _____               _                     _______                 _
	|  __ \             | |                   |__   __|               | |
	| |__) |   ___    __| |   ___    _ __        | |      ___    ___  | |__
	|  _  /   / _ \  / _  |  / _ \  |  _ \       | |     / _ \  / __| |  _ \
	| | \ \  |  __/ | (_| | | (_) | | | | |      | |    |  __/ | (__  | | | |
	|_|  \_\  \___|  \__,_|  \___/  |_| |_|      |_|     \___|  \___| |_| |_|
	Radio System
	Loader
--]]

local settings = script.Parent.settings
local systemSettings = require(settings)

print(`\n		[RTRS]: Initializing System\n		Version: {systemSettings.v}\n		Debug Mode: {systemSettings.debug}`)

if game.ReplicatedStorage:FindFirstChild("radioShared") then
	game.ReplicatedStorage:FindFirstChild("radioShared"):Destroy()
end
local sharedFolder = script.shared
sharedFolder.Name = "radioShared"
sharedFolder.Parent = game.ReplicatedStorage

local wireFolder = Instance.new("Folder")
wireFolder.Name = "wires"
wireFolder.Parent = sharedFolder

local effectFolder = sharedFolder:WaitForChild("effects")
local audioEqualizer = effectFolder:WaitForChild("1 - Equalizer")
local audioDistortion = effectFolder:WaitForChild("2 - Distortion")
local audioFader = effectFolder:WaitForChild("3 - Fader")
audioDistortion.Wire.SourceInstance = audioEqualizer
audioDistortion.Wire.TargetInstance = audioDistortion
audioFader.Wire.SourceInstance = audioDistortion
audioFader.Wire.TargetInstance = audioFader

settings.Parent = sharedFolder

local comm = require(sharedFolder:WaitForChild("comm"))
comm.setup()

local main = require(script:WaitForChild("server"):WaitForChild("main"))
main.init()

-- Startup --
local radioClient = script.client:Clone()
radioClient.Name = "radioClient"
radioClient.Parent = game.StarterPlayer.StarterPlayerScripts

print("\n		[RTRS]: Initialized")


-- Redon Tech Radio System

type SystemMessage = {
	text: string,
	headerText: string?,
	backgroundColor3: Color3?,
	icon: string?,
	iconColor3: Color3?,
	iconRounded: boolean?,
	sideText: string?,
	sideAccent: Color3?,
}

local channel = {}
channel.__index = channel

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local VoiceChatService = game:GetService("VoiceChatService")
local TextChatService = game:GetService("TextChatService")
local TextService = game:GetService("TextService")
local Players = game:GetService("Players")

local shared = ReplicatedStorage:WaitForChild("radioShared")
local systemSettings = require(shared:WaitForChild("settings"))

local print = function(...)
	if systemSettings.debug then
		print(`[RTRS Server]: `, ...)
	end
end
local warn = function(...)
	if systemSettings.debug then
		warn(`[RTRS Server]: `, ...)
	end
end

function channel.init(main: { any }, id: number)
	print(`Initializing Channel {id}`)
	local data = {
		main = main,
		id = id,
		name = systemSettings.channels[id].name,
		messages = {}, -- UNFILTERED
		teams = {},
		isDefaultChannel = (typeof(systemSettings.default) == "boolean" and systemSettings.default == true)
			or (typeof(systemSettings.default) == "table" and table.find(systemSettings.default, id)),
		players = {},
		activeVoice = nil,
	}
	local self = setmetatable(data, channel)

	for team, access in pairs(systemSettings.teams) do
		if
			(typeof(access) == "boolean" and access == true) or (typeof(access) == "table" and table.find(access, id))
		then
			self.teams[team] = true
		end
	end

	self.wire = Instance.new("Wire")
	self.wire.Name = self.id
	self.wire.Parent = shared:WaitForChild("wires")

	self.textChannel = Instance.new("TextChannel")
	self.textChannel.Name = self.name
	self.textChannel.Parent = TextChatService
	self.textChannel.ShouldDeliverCallback = function(...)
		return self:shouldDeliverCallback(...)
	end

	print(`Initialized Channel {id}`)
	return self
end

-- Text Features

function channel:_addMessageToHistory(type: string, data: { [any]: any })
	if systemSettings.channelHistory == 0 then
		return
	end

	table.insert(self.messages, {
		type = type,
		data = data,
	})

	if #self.messages >= systemSettings.channelHistory then
		table.remove(self.messages, #self.messages - systemSettings.channelHistory)
	end
end

function channel:shouldDeliverCallback(message: TextChatMessage, textSource: TextSource): boolean
	local player = Players:GetPlayerByUserId(textSource.UserId)
	if player == nil then
		warn(`{textSource.Name} is not a player`)
		return false
	end

	if self.players[player] == nil then
		warn(`{textSource.Name} is not apart of {self.name}`)
		return false
	end
	print(self.players[player], message.Text)
	if self.players[player] == message.Text then
		warn(`Recieved a duplicate message from {textSource.Name}, ignoring.`)
		return false
	end

	self:_addMessageToHistory("player", {
		player = {
			Name = player.Name,
			TeamColor = player.TeamColor,
			UserId = player.UserId,
		},
		message = message.Text,
	})
	self.players[player] = message.Text

	self.main.serverApi:fire("receivedClientMssage", player, message.Text, self.id)
	return true
end

function channel:receivePanic(player: Player)
	if self.players[player] == nil then
		warn(`{player.Name} is not apart of {self.name}`)
		return
	end

	local message = systemSettings.getPanicMessage(player)
	if typeof(message) ~= "string" then
		warn("getPanicMessage MUST return a string")
		return
	end

	self:_addMessageToHistory("panic", {
		player = {
			Name = player.Name,
			TeamColor = player.TeamColor,
			UserId = player.UserId,
		},
		message = message,
	})
	if systemSettings.panicBehavior:lower() ~= "all" then
		self.main.textEvents.panicButton:fireFilter(function(playerToFilter: Player)
			if self.players[playerToFilter] then
				return true
			end
			return false
		end, { channelId = self.id, player = player, message = message })
	else
		self.main.textEvents.panicButton:fireAll({ channelId = true, player = player, message = message })
	end
end

function channel:sendSystemMessage(message: SystemMessage)
	self:_addMessageToHistory("system", { message = message })
	self.main.textEvents.systemMessage:fireAll({ channelId = self.id, message = message })
end

-- Voice Features

function channel:activateVoice(player: Player)
	if self.activeVoice ~= nil and self.activeVoice ~= player then
		warn(`{player} tried to activate voice while channel is already in use, ignoring.`)
		return
	end
	if self.players[player] == nil then
		warn(`{player.Name} is not apart of {self.name}`)
		return
	end
	if VoiceChatService:IsVoiceEnabledForUserIdAsync(player.UserId) == false then
		warn(`{player.Name} does not have voice chat, ignoring.`)
		return
	end

	self.activeVoice = if self.activeVoice == player then nil else player
	self.main.voiceEvents.activateVoice:fireAll(self.id, self.activeVoice)
	self.wire.SourceInstance = if self.activeVoice ~= nil and self.activeVoice.Character ~= nil
		then self.activeVoice.Character.HumanoidRootPart:FindFirstChild("RSListener")
		else nil
	print(self.activeVoice, self.wire.SourceInstance)
end

-- Player Add/Remove

function channel:addPlayer(player: Player)
	local playerHistory = {}
	for _, message in pairs(self.messages) do
		if message.type == "player" then
			local messageData = message.data
			local filteredMessage = TextService:FilterStringAsync(messageData.message, player.UserId)
				:GetNonChatStringForBroadcastAsync()

			table.insert(playerHistory, {
				type = "player",
				data = {
					player = messageData.player,
					message = filteredMessage,
				},
			})
		else
			table.insert(playerHistory, message)
		end
	end
	self.main.textEvents.sendHistory:fire(player, self.id, playerHistory)

	if self.players[player] then
		print(`[adding,debug] {player.Name} is already apart of {self.name}`)
		return self.textChannel
	end

	self.players[player] = true
	self.textChannel:AddUserAsync(player.UserId)
	self.main.serverApi:fire("channelAddPlayer", self.id, player)
	return self.textChannel
end

function channel:removePlayer(player: Player)
	if self.players[player] == nil then
		print(`[removing,debug] {player.Name} is not apart of {self.name}`)
		return
	end

	self.players[player] = nil
	if self.activeVoice == player then
		self.activeVoice = nil
		self.main.voiceEvents.activateVoice:fireAll(self.id, self.activeVoice)
	end
	self.main.serverApi:fire("channelRemovePlayer", self.id, player)
end

function channel:addPlayerWithChecks(player: Player): boolean
	if
		(typeof(systemSettings.users[player.UserId]) == "boolean" and systemSettings.users[player.UserId] == true)
		or (typeof(systemSettings.users[player.UserId]) == "table" and table.find(
			systemSettings.users[player.UserId],
			self.id
		))
		or (typeof(systemSettings.users[player.Name]) == "boolean" and systemSettings.users[player.Name] == true)
		or (typeof(systemSettings.users[player.Name]) == "table" and table.find(
			systemSettings.users[player.Name],
			self.id
		))
		or (player.Team ~= nil and self.teams[player.Team.Name] == true)
		or self.isDefaultChannel
	then
		return self:addPlayer(player)
	end
	self:removePlayer(player)
	return false
end

return channel


-- Redon Tech Radio System

local main = {}
main.__index = main

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

local shared = ReplicatedStorage:WaitForChild("radioShared")
local systemSettings = require(shared:WaitForChild("settings"))
local comm = require(shared:WaitForChild("comm"))

local print = function(...)
	if systemSettings.debug then
		print(`[RTRS Server]: `, ...)
	end
end
local warn = function(...)
	if systemSettings.debug then
		warn(`[RTRS Server]: `, ...)
	end
end

function main.init()
	print("Initializing Server")
	local textEvents = comm.new("text")
	local voiceEvents = comm.new("voice")
	local apiEvents = comm.new("api")
	local data = {
		textEvents = {
			systemMessage = textEvents:remoteEvent("systemMessage"),
			panicButton = textEvents:remoteEvent("panicButton"),
			sendHistory = textEvents:remoteEvent("sendHistory"),
		},
		voiceEvents = {
			activateVoice = voiceEvents:remoteEvent("activateVoice"),
		},
		apiEvents = apiEvents,
		serverApi = apiEvents:bindableEvent("serverApi"),
		panicCooldowns = {},
	}
	local self = setmetatable(data, main)

	self.channels = self:setupChannels()
	self:setupAudio()

	self.textEvents.authorize = textEvents:remoteFunction("authorize", self, "authorizeClient")
	self.textEvents.panicButton:connect(function(...)
		self:receivePanic(...)
	end)

	self.voiceEvents.activateVoice:connect(function(...)
		self:activateVoice(...)
		self.serverApi:fire("receivedActivateVoice", ...)
	end)

	self.serverApi:connect(function(...)
		self:serverApiEvent(...)
	end)

	print("Initialized Server")
	return self
end

function main:authorizeClient(player: Player)
	local authorizedChannels = {}
	for id, channel in pairs(self.channels) do
		local textChannel = channel:addPlayerWithChecks(player)
		if textChannel ~= false then
			authorizedChannels[id] = textChannel
		end
	end
	self.serverApi:fire("authorizedClient", player, authorizedChannels)

	return authorizedChannels
end

function main:activateVoice(player: Player, channelId: number)
	local channel = self.channels[channelId]
	if channel == nil then
		warn("Received activate voice for non-existant channel")
		return
	end
	channel:activateVoice(player)
end

function main:receivePanic(player: Player, channelId: number)
	if systemSettings.panicButtonEnabled ~= true then
		warn("Panic button is not enabled")
		return
	end

	if
		self.panicCooldowns[player.UserId] ~= nil
		and (os.time() - self.panicCooldowns[player.UserId]) < systemSettings.panicCooldown
	then
		warn("Panic button on cooldown for player")
		return
	end
	self.panicCooldowns[player.UserId] = os.time()

	local channel = self.channels[channelId]
	if channel == nil then
		warn("Received panic for non-existant channel")
		return
	end
	channel:receivePanic(player)
	self.serverApi:fire("receivedPanic", player, channelId)
end

function main:setupChannels()
	print("Setting up channels")

	local channels = {}
	for id, _ in pairs(systemSettings.channels) do
		channels[id] = require(script.Parent:WaitForChild("channel")).init(self, id)
	end

	print("Channels set up")
	return channels
end

function main:characterAdded(input: AudioDeviceInput, character: Model)
	character.ModelStreamingMode = Enum.ModelStreamingMode.Persistent
	local rootPart = character:WaitForChild("HumanoidRootPart")

	local emitter = Instance.new("AudioEmitter")
	emitter.Name = "emitterForListener"
	emitter.AudioInteractionGroup = "RadioSystem"
	emitter.Parent = character

	local wire = Instance.new("Wire")
	wire.SourceInstance = input
	wire.TargetInstance = emitter
	wire.Parent = emitter

	local radioEmitter = Instance.new("AudioEmitter")
	radioEmitter.Name = "RSEmitter"
	radioEmitter.Parent = character
	local wire2 = Instance.new("Wire")
	wire2.TargetInstance = radioEmitter
	wire2.Parent = radioEmitter
	local radioListener = Instance.new("AudioListener")
	radioListener.Name = "RSListener"
	radioListener.AudioInteractionGroup = "RadioSystem"
	radioListener.Parent = rootPart
end

function main:setupAudio()
	print("Setting up voice system")

	local function playerAdded(player: Player)
		local input = player:WaitForChild("AudioDeviceInput")

		player.CharacterAdded:Connect(function(character)
			self:characterAdded(input, character)
		end)
		if player.Character ~= nil then
			self:characterAdded(input, player.Character)
		end
	end

	Players.PlayerAdded:Connect(playerAdded)
	for _, player in pairs(Players:GetPlayers()) do
		playerAdded(player)
	end

	print("Voice setup")
end

function main:serverApiEvent(message: string, ...)
	if message == "createSystemMessage" then
		local channelId: number, data: {} = ...
		local channel = self.channels[channelId]
		if channel == nil then
			warn("Received message for non-existant channel")
			return
		end

		channel:sendSystemMessage(data)
	elseif message == "activatePlayersVoice" then
		local channelId: number, player: Player = ...
		local channel = self.channels[channelId]
		if channel == nil then
			warn("Received message for non-existant channel")
			return
		end

		channel:activateVoice(player)
	end
end

return main


--!nonstrict

--[[
	Constructs a new computed state object, which follows the value of another
	state object using a spring simulation.
]]

local Package = script.Parent.Parent
local PubTypes = require(Package.PubTypes)
local Types = require(Package.Types)
local logError = require(Package.Logging.logError)
local logErrorNonFatal = require(Package.Logging.logErrorNonFatal)
local unpackType = require(Package.Animation.unpackType)
local SpringScheduler = require(Package.Animation.SpringScheduler)
local useDependency = require(Package.Dependencies.useDependency)
local initDependency = require(Package.Dependencies.initDependency)
local updateAll = require(Package.Dependencies.updateAll)
local xtypeof = require(Package.Utility.xtypeof)
local unwrap = require(Package.State.unwrap)

local class = {}

local CLASS_METATABLE = {__index = class}
local WEAK_KEYS_METATABLE = {__mode = "k"}

--[[
	Returns the current value of this Spring object.
	The object will be registered as a dependency unless `asDependency` is false.
]]
function class:get(asDependency: boolean?): any
	if asDependency ~= false then
		useDependency(self)
	end
	return self._currentValue
end

--[[
	Sets the position of the internal springs, meaning the value of this
	Spring will jump to the given value. This doesn't affect velocity.

	If the type doesn't match the current type of the spring, an error will be
	thrown.
]]
function class:setPosition(newValue: PubTypes.Animatable)
	local newType = typeof(newValue)
	if newType ~= self._currentType then
		logError("springTypeMismatch", nil, newType, self._currentType)
	end

	self._springPositions = unpackType(newValue, newType)
	self._currentValue = newValue
	SpringScheduler.add(self)
	updateAll(self)
end

--[[
	Sets the velocity of the internal springs, overwriting the existing velocity
	of this Spring. This doesn't affect position.

	If the type doesn't match the current type of the spring, an error will be
	thrown.
]]
function class:setVelocity(newValue: PubTypes.Animatable)
	local newType = typeof(newValue)
	if newType ~= self._currentType then
		logError("springTypeMismatch", nil, newType, self._currentType)
	end

	self._springVelocities = unpackType(newValue, newType)
	SpringScheduler.add(self)
end

--[[
	Adds to the velocity of the internal springs, on top of the existing
	velocity of this Spring. This doesn't affect position.

	If the type doesn't match the current type of the spring, an error will be
	thrown.
]]
function class:addVelocity(deltaValue: PubTypes.Animatable)
	local deltaType = typeof(deltaValue)
	if deltaType ~= self._currentType then
		logError("springTypeMismatch", nil, deltaType, self._currentType)
	end

	local springDeltas = unpackType(deltaValue, deltaType)
	for index, delta in ipairs(springDeltas) do
		self._springVelocities[index] += delta
	end
	SpringScheduler.add(self)
end

--[[
	Called when the goal state changes value, or when the speed or damping has
	changed.
]]
function class:update(): boolean
	local goalValue = self._goalState:get(false)

	-- figure out if this was a goal change or a speed/damping change
	if goalValue == self._goalValue then
		-- speed/damping change
		local damping = unwrap(self._damping)
		if typeof(damping) ~= "number" then
			logErrorNonFatal("mistypedSpringDamping", nil, typeof(damping))
		elseif damping < 0 then
			logErrorNonFatal("invalidSpringDamping", nil, damping)
		else
			self._currentDamping = damping
		end

		local speed = unwrap(self._speed)
		if typeof(speed) ~= "number" then
			logErrorNonFatal("mistypedSpringSpeed", nil, typeof(speed))
		elseif speed < 0 then
			logErrorNonFatal("invalidSpringSpeed", nil, speed)
		else
			self._currentSpeed = speed
		end

		return false
	else
		-- goal change - reconfigure spring to target new goal
		self._goalValue = goalValue

		local oldType = self._currentType
		local newType = typeof(goalValue)
		self._currentType = newType

		local springGoals = unpackType(goalValue, newType)
		local numSprings = #springGoals
		self._springGoals = springGoals

		if newType ~= oldType then
			-- if the type changed, snap to the new value and rebuild the
			-- position and velocity tables
			self._currentValue = self._goalValue

			local springPositions = table.create(numSprings, 0)
			local springVelocities = table.create(numSprings, 0)
			for index, springGoal in ipairs(springGoals) do
				springPositions[index] = springGoal
			end
			self._springPositions = springPositions
			self._springVelocities = springVelocities

			-- the spring may have been animating before, so stop that
			SpringScheduler.remove(self)
			return true

			-- otherwise, the type hasn't changed, just the goal...
		elseif numSprings == 0 then
			-- if the type isn't animatable, snap to the new value
			self._currentValue = self._goalValue
			return true

		else
			-- if it's animatable, let it animate to the goal
			SpringScheduler.add(self)
			return false
		end
	end
end

local function Spring<T>(
	goalState: PubTypes.Value<T>,
	speed: PubTypes.CanBeState<number>?,
	damping: PubTypes.CanBeState<number>?
): Types.Spring<T>
	-- apply defaults for speed and damping
	if speed == nil then
		speed = 10
	end
	if damping == nil then
		damping = 1
	end

	local dependencySet = {[goalState] = true}
	if xtypeof(speed) == "State" then
		dependencySet[speed] = true
	end
	if xtypeof(damping) == "State" then
		dependencySet[damping] = true
	end

	local self = setmetatable({
		type = "State",
		kind = "Spring",
		dependencySet = dependencySet,
		-- if we held strong references to the dependents, then they wouldn't be
		-- able to get garbage collected when they fall out of scope
		dependentSet = setmetatable({}, WEAK_KEYS_METATABLE),
		_speed = speed,
		_damping = damping,

		_goalState = goalState,
		_goalValue = nil,

		_currentType = nil,
		_currentValue = nil,
		_currentSpeed = unwrap(speed),
		_currentDamping = unwrap(damping),

		_springPositions = nil,
		_springGoals = nil,
		_springVelocities = nil
	}, CLASS_METATABLE)

	initDependency(self)
	-- add this object to the goal state's dependent set
	goalState.dependentSet[self] = true
	self:update()

	return self
end

return Spring


--!nonstrict

--[[
	Constructs a new computed state object, which follows the value of another
	state object using a tween.
]]

local Package = script.Parent.Parent
local PubTypes = require(Package.PubTypes)
local Types = require(Package.Types)
local TweenScheduler = require(Package.Animation.TweenScheduler)
local useDependency = require(Package.Dependencies.useDependency)
local initDependency = require(Package.Dependencies.initDependency)
local logError = require(Package.Logging.logError)
local logErrorNonFatal = require(Package.Logging.logErrorNonFatal)
local xtypeof = require(Package.Utility.xtypeof)

local class = {}

local CLASS_METATABLE = {__index = class}
local WEAK_KEYS_METATABLE = {__mode = "k"}

--[[
	Returns the current value of this Tween object.
	The object will be registered as a dependency unless `asDependency` is false.
]]
function class:get(asDependency: boolean?): any
	if asDependency ~= false then
		useDependency(self)
	end
	return self._currentValue
end

--[[
	Called when the goal state changes value; this will initiate a new tween.
	Returns false as the current value doesn't change right away.
]]
function class:update(): boolean
	local goalValue = self._goalState:get(false)

	-- if the goal hasn't changed, then this is a TweenInfo change.
	-- in that case, if we're not currently animating, we can skip everything
	if goalValue == self._nextValue and not self._currentlyAnimating then
		return false
	end

	local tweenInfo = self._tweenInfo
	if self._tweenInfoIsState then
		tweenInfo = tweenInfo:get()
	end

	-- if we receive a bad TweenInfo, then error and stop the update
	if typeof(tweenInfo) ~= "TweenInfo" then
		logErrorNonFatal("mistypedTweenInfo", nil, typeof(tweenInfo))
		return false
	end

	self._prevValue = self._currentValue
	self._nextValue = goalValue

	self._currentTweenStartTime = os.clock()
	self._currentTweenInfo = tweenInfo

	local tweenDuration = tweenInfo.DelayTime + tweenInfo.Time
	if tweenInfo.Reverses then
		tweenDuration += tweenInfo.Time
	end
	tweenDuration *= tweenInfo.RepeatCount + 1
	self._currentTweenDuration = tweenDuration

	-- start animating this tween
	TweenScheduler.add(self)

	return false
end

local function Tween<T>(
	goalState: PubTypes.StateObject<PubTypes.Animatable>,
	tweenInfo: PubTypes.CanBeState<TweenInfo>?
): Types.Tween<T>
	local currentValue = goalState:get(false)

	-- apply defaults for tween info
	if tweenInfo == nil then
		tweenInfo = TweenInfo.new()
	end

	local dependencySet = {[goalState] = true}
	local tweenInfoIsState = xtypeof(tweenInfo) == "State"

	if tweenInfoIsState then
		dependencySet[tweenInfo] = true
	end

	local startingTweenInfo = tweenInfo
	if tweenInfoIsState then
		startingTweenInfo = startingTweenInfo:get()
	end

	-- If we start with a bad TweenInfo, then we don't want to construct a Tween
	if typeof(startingTweenInfo) ~= "TweenInfo" then
		logError("mistypedTweenInfo", nil, typeof(startingTweenInfo))
	end

	local self = setmetatable({
		type = "State",
		kind = "Tween",
		dependencySet = dependencySet,
		-- if we held strong references to the dependents, then they wouldn't be
		-- able to get garbage collected when they fall out of scope
		dependentSet = setmetatable({}, WEAK_KEYS_METATABLE),
		_goalState = goalState,
		_tweenInfo = tweenInfo,
		_tweenInfoIsState = tweenInfoIsState,

		_prevValue = currentValue,
		_nextValue = currentValue,
		_currentValue = currentValue,

		-- store current tween into separately from 'real' tween into, so it
		-- isn't affected by :setTweenInfo() until next change
		_currentTweenInfo = tweenInfo,
		_currentTweenDuration = 0,
		_currentTweenStartTime = 0,
		_currentlyAnimating = false
	}, CLASS_METATABLE)

	initDependency(self)
	-- add this object to the goal state's dependent set
	goalState.dependentSet[self] = true

	return self
end

return Tween


--!strict

--[[
	Manages batch updating of tween objects.
]]

local RunService = game:GetService("RunService")

local Package = script.Parent.Parent
local Types = require(Package.Types)
local lerpType = require(Package.Animation.lerpType)
local getTweenRatio = require(Package.Animation.getTweenRatio)
local updateAll = require(Package.Dependencies.updateAll)

local TweenScheduler = {}

type Set<T> = {[T]: any}
type Tween = Types.Tween<any>

local WEAK_KEYS_METATABLE = {__mode = "k"}

-- all the tweens currently being updated
local allTweens: Set<Tween> = {}
setmetatable(allTweens, WEAK_KEYS_METATABLE)

--[[
	Adds a Tween to be updated every render step.
]]
function TweenScheduler.add(tween: Tween)
	allTweens[tween] = true
end

--[[
	Removes a Tween from the scheduler.
]]
function TweenScheduler.remove(tween: Tween)
	allTweens[tween] = nil
end

--[[
	Updates all Tween objects.
]]
local function updateAllTweens()
	local now = os.clock()
	-- FIXME: Typed Luau doesn't understand this loop yet
	for tween: Tween in pairs(allTweens :: any) do
		local currentTime = now - tween._currentTweenStartTime

		if currentTime > tween._currentTweenDuration then
			if tween._currentTweenInfo.Reverses then
				tween._currentValue = tween._prevValue
			else
				tween._currentValue = tween._nextValue
			end
			tween._currentlyAnimating = false
			updateAll(tween)
			TweenScheduler.remove(tween)
		else
			local ratio = getTweenRatio(tween._currentTweenInfo, currentTime)
			local currentValue = lerpType(tween._prevValue, tween._nextValue, ratio)
			tween._currentValue = currentValue
			tween._currentlyAnimating = true
			updateAll(tween)
		end
	end
end

RunService:BindToRenderStep(
	"__FusionTweenScheduler",
	Enum.RenderPriority.First.Value,
	updateAllTweens
)

return TweenScheduler


--!strict

--[[
	Given a `tweenInfo` and `currentTime`, returns a ratio which can be used to
	tween between two values over time.
]]

local TweenService = game:GetService("TweenService")

local function getTweenRatio(tweenInfo: TweenInfo, currentTime: number): number
	local delay = tweenInfo.DelayTime
	local duration = tweenInfo.Time
	local reverses = tweenInfo.Reverses
	local numCycles = 1 + tweenInfo.RepeatCount
	local easeStyle = tweenInfo.EasingStyle
	local easeDirection = tweenInfo.EasingDirection

	local cycleDuration = delay + duration
	if reverses then
		cycleDuration += duration
	end

	if currentTime >= cycleDuration * numCycles then
		return 1
	end

	local cycleTime = currentTime % cycleDuration

	if cycleTime <= delay then
		return 0
	end

	local tweenProgress = (cycleTime - delay) / duration
	if tweenProgress > 1 then
		tweenProgress = 2 - tweenProgress
	end

	local ratio = TweenService:GetValue(tweenProgress, easeStyle, easeDirection)
	return ratio
end

return getTweenRatio


--!strict

--[[
	Linearly interpolates the given animatable types by a ratio.
	If the types are different or not animatable, then the first value will be
	returned for ratios below 0.5, and the second value for 0.5 and above.

	FIXME: This function uses a lot of redefinitions to suppress false positives
	from the Luau typechecker - ideally these wouldn't be required
]]

local Package = script.Parent.Parent
local PubTypes = require(Package.PubTypes)
local Oklab = require(Package.Colour.Oklab)

local function lerpType(from: any, to: any, ratio: number): any
	local typeString = typeof(from)

	if typeof(to) == typeString then
		-- both types must match for interpolation to make sense
		if typeString == "number" then
			local to, from = to :: number, from :: number
			return (to - from) * ratio + from

		elseif typeString == "CFrame" then
			local to, from = to :: CFrame, from :: CFrame
			return from:Lerp(to, ratio)

		elseif typeString == "Color3" then
			local to, from = to :: Color3, from :: Color3
			local fromLab = Oklab.to(from)
			local toLab = Oklab.to(to)
			return Oklab.from(
				fromLab:Lerp(toLab, ratio),
				false
			)

		elseif typeString == "ColorSequenceKeypoint" then
			local to, from = to :: ColorSequenceKeypoint, from :: ColorSequenceKeypoint
			local fromLab = Oklab.to(from.Value)
			local toLab = Oklab.to(to.Value)
			return ColorSequenceKeypoint.new(
				(to.Time - from.Time) * ratio + from.Time,
				Oklab.from(
					fromLab:Lerp(toLab, ratio),
					false
				)
			)

		elseif typeString == "DateTime" then
			local to, from = to :: DateTime, from :: DateTime
			return DateTime.fromUnixTimestampMillis(
				(to.UnixTimestampMillis - from.UnixTimestampMillis) * ratio + from.UnixTimestampMillis
			)

		elseif typeString == "NumberRange" then
			local to, from = to :: NumberRange, from :: NumberRange
			return NumberRange.new(
				(to.Min - from.Min) * ratio + from.Min,
				(to.Max - from.Max) * ratio + from.Max
			)

		elseif typeString == "NumberSequenceKeypoint" then
			local to, from = to :: NumberSequenceKeypoint, from :: NumberSequenceKeypoint
			return NumberSequenceKeypoint.new(
				(to.Time - from.Time) * ratio + from.Time,
				(to.Value - from.Value) * ratio + from.Value,
				(to.Envelope - from.Envelope) * ratio + from.Envelope
			)

		elseif typeString == "PhysicalProperties" then
			local to, from = to :: PhysicalProperties, from :: PhysicalProperties
			return PhysicalProperties.new(
				(to.Density - from.Density) * ratio + from.Density,
				(to.Friction - from.Friction) * ratio + from.Friction,
				(to.Elasticity - from.Elasticity) * ratio + from.Elasticity,
				(to.FrictionWeight - from.FrictionWeight) * ratio + from.FrictionWeight,
				(to.ElasticityWeight - from.ElasticityWeight) * ratio + from.ElasticityWeight
			)

		elseif typeString == "Ray" then
			local to, from = to :: Ray, from :: Ray
			return Ray.new(
				from.Origin:Lerp(to.Origin, ratio),
				from.Direction:Lerp(to.Direction, ratio)
			)

		elseif typeString == "Rect" then
			local to, from = to :: Rect, from :: Rect
			return Rect.new(
				from.Min:Lerp(to.Min, ratio),
				from.Max:Lerp(to.Max, ratio)
			)

		elseif typeString == "Region3" then
			local to, from = to :: Region3, from :: Region3
			-- FUTURE: support rotated Region3s if/when they become constructable
			local position = from.CFrame.Position:Lerp(to.CFrame.Position, ratio)
			local halfSize = from.Size:Lerp(to.Size, ratio) / 2
			return Region3.new(position - halfSize, position + halfSize)

		elseif typeString == "Region3int16" then
			local to, from = to :: Region3int16, from :: Region3int16
			return Region3int16.new(
				Vector3int16.new(
					(to.Min.X - from.Min.X) * ratio + from.Min.X,
					(to.Min.Y - from.Min.Y) * ratio + from.Min.Y,
					(to.Min.Z - from.Min.Z) * ratio + from.Min.Z
				),
				Vector3int16.new(
					(to.Max.X - from.Max.X) * ratio + from.Max.X,
					(to.Max.Y - from.Max.Y) * ratio + from.Max.Y,
					(to.Max.Z - from.Max.Z) * ratio + from.Max.Z
				)
			)

		elseif typeString == "UDim" then
			local to, from = to :: UDim, from :: UDim
			return UDim.new(
				(to.Scale - from.Scale) * ratio + from.Scale,
				(to.Offset - from.Offset) * ratio + from.Offset
			)

		elseif typeString == "UDim2" then
			local to, from = to :: UDim2, from :: UDim2
			return from:Lerp(to, ratio)

		elseif typeString == "Vector2" then
			local to, from = to :: Vector2, from :: Vector2
			return from:Lerp(to, ratio)

		elseif typeString == "Vector2int16" then
			local to, from = to :: Vector2int16, from :: Vector2int16
			return Vector2int16.new(
				(to.X - from.X) * ratio + from.X,
				(to.Y - from.Y) * ratio + from.Y
			)

		elseif typeString == "Vector3" then
			local to, from = to :: Vector3, from :: Vector3
			return from:Lerp(to, ratio)

		elseif typeString == "Vector3int16" then
			local to, from = to :: Vector3int16, from :: Vector3int16
			return Vector3int16.new(
				(to.X - from.X) * ratio + from.X,
				(to.Y - from.Y) * ratio + from.Y,
				(to.Z - from.Z) * ratio + from.Z
			)
		end
	end

	-- fallback case: the types are different or not animatable
	if ratio < 0.5 then
		return from
	else
		return to
	end
end

return lerpType


--!strict

--[[
	Packs an array of numbers into a given animatable data type.
	If the type is not animatable, nil will be returned.

	FUTURE: When Luau supports singleton types, those could be used in
	conjunction with intersection types to make this function fully statically
	type checkable.
]]

local Package = script.Parent.Parent
local PubTypes = require(Package.PubTypes)
local Oklab = require(Package.Colour.Oklab)

local function packType(numbers: {number}, typeString: string): PubTypes.Animatable?
	if typeString == "number" then
		return numbers[1]

	elseif typeString == "CFrame" then
		return
			CFrame.new(numbers[1], numbers[2], numbers[3]) *
			CFrame.fromAxisAngle(
				Vector3.new(numbers[4], numbers[5], numbers[6]).Unit,
				numbers[7]
			)

	elseif typeString == "Color3" then
		return Oklab.from(
			Vector3.new(numbers[1], numbers[2], numbers[3]),
			false
		)

	elseif typeString == "ColorSequenceKeypoint" then
		return ColorSequenceKeypoint.new(
			numbers[4],
			Oklab.from(
				Vector3.new(numbers[1], numbers[2], numbers[3]),
				false
			)
		)

	elseif typeString == "DateTime" then
		return DateTime.fromUnixTimestampMillis(numbers[1])

	elseif typeString == "NumberRange" then
		return NumberRange.new(numbers[1], numbers[2])

	elseif typeString == "NumberSequenceKeypoint" then
		return NumberSequenceKeypoint.new(numbers[2], numbers[1], numbers[3])

	elseif typeString == "PhysicalProperties" then
		return PhysicalProperties.new(numbers[1], numbers[2], numbers[3], numbers[4], numbers[5])

	elseif typeString == "Ray" then
		return Ray.new(
			Vector3.new(numbers[1], numbers[2], numbers[3]),
			Vector3.new(numbers[4], numbers[5], numbers[6])
		)

	elseif typeString == "Rect" then
		return Rect.new(numbers[1], numbers[2], numbers[3], numbers[4])

	elseif typeString == "Region3" then
		-- FUTURE: support rotated Region3s if/when they become constructable
		local position = Vector3.new(numbers[1], numbers[2], numbers[3])
		local halfSize = Vector3.new(numbers[4] / 2, numbers[5] / 2, numbers[6] / 2)
		return Region3.new(position - halfSize, position + halfSize)

	elseif typeString == "Region3int16" then
		return Region3int16.new(
			Vector3int16.new(numbers[1], numbers[2], numbers[3]),
			Vector3int16.new(numbers[4], numbers[5], numbers[6])
		)

	elseif typeString == "UDim" then
		return UDim.new(numbers[1], numbers[2])

	elseif typeString == "UDim2" then
		return UDim2.new(numbers[1], numbers[2], numbers[3], numbers[4])

	elseif typeString == "Vector2" then
		return Vector2.new(numbers[1], numbers[2])

	elseif typeString == "Vector2int16" then
		return Vector2int16.new(numbers[1], numbers[2])

	elseif typeString == "Vector3" then
		return Vector3.new(numbers[1], numbers[2], numbers[3])

	elseif typeString == "Vector3int16" then
		return Vector3int16.new(numbers[1], numbers[2], numbers[3])
	else
		return nil
	end
end

return packType


--!strict

--[[
	Returns a 2x2 matrix of coefficients for a given time, damping and speed.
	Specifically, this returns four coefficients - posPos, posVel, velPos, and
	velVel - which can be multiplied with position and velocity like so:

	local newPosition = oldPosition * posPos + oldVelocity * posVel
	local newVelocity = oldPosition * velPos + oldVelocity * velVel

	Special thanks to AxisAngle for helping to improve numerical precision.
]]

local function springCoefficients(time: number, damping: number, speed: number): (number, number, number, number)
	-- if time or speed is 0, then the spring won't move
	if time == 0 or speed == 0 then
		return 1, 0, 0, 1
	end
	local posPos, posVel, velPos, velVel

	if damping > 1 then
		-- overdamped spring
		-- solution to the characteristic equation:
		-- z = -ζω ± Sqrt[ζ^2 - 1] ω
		-- x[t] -> x0(e^(t z2) z1 - e^(t z1) z2)/(z1 - z2)
		--		 + v0(e^(t z1) - e^(t z2))/(z1 - z2)
		-- v[t] -> x0(z1 z2(-e^(t z1) + e^(t z2)))/(z1 - z2)
		--		 + v0(z1 e^(t z1) - z2 e^(t z2))/(z1 - z2)

		local scaledTime = time * speed
		local alpha = math.sqrt(damping^2 - 1)
		local scaledInvAlpha = -0.5 / alpha
		local z1 = -alpha - damping
		local z2 = 1 / z1
		local expZ1 = math.exp(scaledTime * z1)
		local expZ2 = math.exp(scaledTime * z2)

		posPos = (expZ2*z1 - expZ1*z2) * scaledInvAlpha
		posVel = (expZ1 - expZ2) * scaledInvAlpha / speed
		velPos = (expZ2 - expZ1) * scaledInvAlpha * speed
		velVel = (expZ1*z1 - expZ2*z2) * scaledInvAlpha

	elseif damping == 1 then
		-- critically damped spring
		-- x[t] -> x0(e^-tω)(1+tω) + v0(e^-tω)t
		-- v[t] -> x0(t ω^2)(-e^-tω) + v0(1 - tω)(e^-tω)

		local scaledTime = time * speed
		local expTerm = math.exp(-scaledTime)

		posPos = expTerm * (1 + scaledTime)
		posVel = expTerm * time
		velPos = expTerm * (-scaledTime*speed)
		velVel = expTerm * (1 - scaledTime)

	else
		-- underdamped spring
		-- factored out of the solutions to the characteristic equation:
		-- α = Sqrt[1 - ζ^2]
		-- x[t] -> x0(e^-tζω)(α Cos[tα] + ζω Sin[tα])/α
		--       + v0(e^-tζω)(Sin[tα])/α
		-- v[t] -> x0(-e^-tζω)(α^2 + ζ^2 ω^2)(Sin[tα])/α
		--       + v0(e^-tζω)(α Cos[tα] - ζω Sin[tα])/α

		local scaledTime = time * speed
		local alpha = math.sqrt(1 - damping^2)
		local invAlpha = 1 / alpha
		local alphaTime = alpha * scaledTime
		local expTerm = math.exp(-scaledTime*damping)
		local sinTerm = expTerm * math.sin(alphaTime)
		local cosTerm = expTerm * math.cos(alphaTime)
		local sinInvAlpha = sinTerm*invAlpha
		local sinInvAlphaDamp = sinInvAlpha*damping

		posPos = sinInvAlphaDamp + cosTerm
		posVel = sinInvAlpha
		velPos = -(sinInvAlphaDamp*damping + sinTerm*alpha)
		velVel = cosTerm - sinInvAlphaDamp
	end

	return posPos, posVel, velPos, velVel
end

return springCoefficients