--!nonstrict
--[[

	TouchThumbstick

--]]
local Players = game:GetService("Players")
local GuiService = game:GetService("GuiService")
local UserInputService = game:GetService("UserInputService")

local UserGameSettings = UserSettings():GetService("UserGameSettings")
local FFlagUserClampClassicThumbstick do
	local success, result = pcall(function()
		return UserSettings():IsUserFeatureEnabled("UserClampClassicThumbstick")
	end)
	FFlagUserClampClassicThumbstick = success and result
end

--[[ Constants ]]--
local ZERO_VECTOR3 = Vector3.new(0,0,0)
local TOUCH_CONTROL_SHEET = "rbxasset://textures/ui/TouchControlsSheet.png"
--[[ The Module ]]--
local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController"))
local TouchThumbstick = setmetatable({}, BaseCharacterController)
TouchThumbstick.__index = TouchThumbstick
function TouchThumbstick.new()
	local self = setmetatable(BaseCharacterController.new() :: any, TouchThumbstick)

	self.isFollowStick = false

	self.thumbstickFrame = nil
	self.moveTouchObject = nil
	self.onTouchMovedConn = nil
	self.onTouchEndedConn = nil
	self.screenPos = nil
	self.stickImage = nil
	self.thumbstickSize = nil -- Float

	return self
end
function TouchThumbstick:Enable(enable: boolean?, uiParentFrame)
	if enable == nil then return false end			-- If nil, return false (invalid argument)
	enable = enable and true or false				-- Force anything non-nil to boolean before comparison
	if self.enabled == enable then return true end	-- If no state change, return true indicating already in requested state

	self.moveVector = ZERO_VECTOR3
	self.isJumping = false

	if enable then
		-- Enable
		if not self.thumbstickFrame then
			self:Create(uiParentFrame)
		end
		self.thumbstickFrame.Visible = true
	else
		-- Disable
		self.thumbstickFrame.Visible = false
		self:OnInputEnded()
	end
	self.enabled = enable
end
function TouchThumbstick:OnInputEnded()
	self.thumbstickFrame.Position = self.screenPos
	self.stickImage.Position = UDim2.new(0, self.thumbstickFrame.Size.X.Offset/2 - self.thumbstickSize/4, 0, self.thumbstickFrame.Size.Y.Offset/2 - self.thumbstickSize/4)

	self.moveVector = ZERO_VECTOR3
	self.isJumping = false
	self.thumbstickFrame.Position = self.screenPos
	self.moveTouchObject = nil
end
function TouchThumbstick:Create(parentFrame)

	if self.thumbstickFrame then
		self.thumbstickFrame:Destroy()
		self.thumbstickFrame = nil
		if self.onTouchMovedConn then
			self.onTouchMovedConn:Disconnect()
			self.onTouchMovedConn = nil
		end
		if self.onTouchEndedConn then
			self.onTouchEndedConn:Disconnect()
			self.onTouchEndedConn = nil
		end
	end

	local minAxis = math.min(parentFrame.AbsoluteSize.X, parentFrame.AbsoluteSize.Y)
	local isSmallScreen = minAxis <= 500
	self.thumbstickSize = isSmallScreen and 70 or 120
	self.screenPos = isSmallScreen and UDim2.new(0, (self.thumbstickSize/2) - 10, 1, -self.thumbstickSize - 20) or
		UDim2.new(0, self.thumbstickSize/2, 1, -self.thumbstickSize * 1.75)

	self.thumbstickFrame = Instance.new("Frame")
	self.thumbstickFrame.Name = "ThumbstickFrame"
	self.thumbstickFrame.Active = true
	self.thumbstickFrame.Visible = false
	self.thumbstickFrame.Size = UDim2.new(0, self.thumbstickSize, 0, self.thumbstickSize)
	self.thumbstickFrame.Position = self.screenPos
	self.thumbstickFrame.BackgroundTransparency = 1

	local outerImage = Instance.new("ImageLabel")
	outerImage.Name = "OuterImage"
	outerImage.Image = TOUCH_CONTROL_SHEET
	outerImage.ImageRectOffset = Vector2.new()
	outerImage.ImageRectSize = Vector2.new(220, 220)
	outerImage.BackgroundTransparency = 1
	outerImage.Size = UDim2.new(0, self.thumbstickSize, 0, self.thumbstickSize)
	outerImage.Position = UDim2.new(0, 0, 0, 0)
	outerImage.Parent = self.thumbstickFrame

	self.stickImage = Instance.new("ImageLabel")
	self.stickImage.Name = "StickImage"
	self.stickImage.Image = TOUCH_CONTROL_SHEET
	self.stickImage.ImageRectOffset = Vector2.new(220, 0)
	self.stickImage.ImageRectSize = Vector2.new(111, 111)
	self.stickImage.BackgroundTransparency = 1
	self.stickImage.Size = UDim2.new(0, self.thumbstickSize/2, 0, self.thumbstickSize/2)
	self.stickImage.Position = UDim2.new(0, self.thumbstickSize/2 - self.thumbstickSize/4, 0, self.thumbstickSize/2 - self.thumbstickSize/4)
	self.stickImage.ZIndex = 2
	self.stickImage.Parent = self.thumbstickFrame

	local centerPosition = nil
	local deadZone = 0.05

	local function DoMove(direction: Vector2)

		local currentMoveVector = direction / (self.thumbstickSize/2)

		-- Scaled Radial Dead Zone
		local inputAxisMagnitude = currentMoveVector.magnitude
		if inputAxisMagnitude < deadZone then
			currentMoveVector = Vector3.new()
		else
			if FFlagUserClampClassicThumbstick then
				currentMoveVector = currentMoveVector.unit * math.min(1, (inputAxisMagnitude - deadZone) / (1 - deadZone))
			else
				currentMoveVector = currentMoveVector.unit * ((inputAxisMagnitude - deadZone) / (1 - deadZone))
			end
			-- NOTE: Making currentMoveVector a unit vector will cause the player to instantly go max speed
			-- must check for zero length vector is using unit
			currentMoveVector = Vector3.new(currentMoveVector.X, 0, currentMoveVector.Y)
		end

		self.moveVector = currentMoveVector
	end

	local function MoveStick(pos: Vector3)
		local relativePosition = Vector2.new(pos.X - centerPosition.X, pos.Y - centerPosition.Y)
		local length = relativePosition.magnitude
		local maxLength = self.thumbstickFrame.AbsoluteSize.X/2
		if self.isFollowStick and length > maxLength then
			local offset = relativePosition.unit * maxLength
			self.thumbstickFrame.Position = UDim2.new(
				0, pos.X - self.thumbstickFrame.AbsoluteSize.X/2 - offset.X,
				0, pos.Y - self.thumbstickFrame.AbsoluteSize.Y/2 - offset.Y)
		else
			length = math.min(length, maxLength)
			relativePosition = relativePosition.unit * length
		end
		self.stickImage.Position = UDim2.new(0, relativePosition.X + self.stickImage.AbsoluteSize.X/2, 0, relativePosition.Y + self.stickImage.AbsoluteSize.Y/2)
	end

	-- input connections
	self.thumbstickFrame.InputBegan:Connect(function(inputObject: InputObject)
		--A touch that starts elsewhere on the screen will be sent to a frame's InputBegan event
		--if it moves over the frame. So we check that this is actually a new touch (inputObject.UserInputState ~= Enum.UserInputState.Begin)
		if self.moveTouchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch
			or inputObject.UserInputState ~= Enum.UserInputState.Begin then
			return
		end

		self.moveTouchObject = inputObject
		self.thumbstickFrame.Position = UDim2.new(0, inputObject.Position.X - self.thumbstickFrame.Size.X.Offset/2, 0, inputObject.Position.Y - self.thumbstickFrame.Size.Y.Offset/2)
		centerPosition = Vector2.new(self.thumbstickFrame.AbsolutePosition.X + self.thumbstickFrame.AbsoluteSize.X/2,
			self.thumbstickFrame.AbsolutePosition.Y + self.thumbstickFrame.AbsoluteSize.Y/2)
		local direction = Vector2.new(inputObject.Position.X - centerPosition.X, inputObject.Position.Y - centerPosition.Y)
	end)

	self.onTouchMovedConn = UserInputService.TouchMoved:Connect(function(inputObject: InputObject, isProcessed: boolean)
		if inputObject == self.moveTouchObject then
			centerPosition = Vector2.new(self.thumbstickFrame.AbsolutePosition.X + self.thumbstickFrame.AbsoluteSize.X/2,
				self.thumbstickFrame.AbsolutePosition.Y + self.thumbstickFrame.AbsoluteSize.Y/2)
			local direction = Vector2.new(inputObject.Position.X - centerPosition.X, inputObject.Position.Y - centerPosition.Y)
			DoMove(direction)
			MoveStick(inputObject.Position)
		end
	end)

	self.onTouchEndedConn = UserInputService.TouchEnded:Connect(function(inputObject, isProcessed)
		if inputObject == self.moveTouchObject then
			self:OnInputEnded()
		end
	end)

	GuiService.MenuOpened:Connect(function()
		if self.moveTouchObject then
			self:OnInputEnded()
		end
	end)

	self.thumbstickFrame.Parent = parentFrame
end
return TouchThumbstick


--!nonstrict
--!nolint GlobalUsedAsLocal

--[[
		VRNavigation
--]]

local VRService = game:GetService("VRService")
local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local PathfindingService = game:GetService("PathfindingService")
local ContextActionService = game:GetService("ContextActionService")
local StarterGui = game:GetService("StarterGui")

--local MasterControl = require(script.Parent)
local PathDisplay = nil
local LocalPlayer = Players.LocalPlayer

--[[ Constants ]]--
local RECALCULATE_PATH_THRESHOLD = 4
local NO_PATH_THRESHOLD = 12
local MAX_PATHING_DISTANCE = 200
local POINT_REACHED_THRESHOLD = 1
local OFFTRACK_TIME_THRESHOLD = 2
local THUMBSTICK_DEADZONE = 0.22

local ZERO_VECTOR3 = Vector3.new(0,0,0)
local XZ_VECTOR3 = Vector3.new(1,0,1)

--[[ Utility Functions ]]--
local function IsFinite(num: number)
	return num == num and num ~= 1/0 and num ~= -1/0
end

local function IsFiniteVector3(vec3)
	return IsFinite(vec3.x) and IsFinite(vec3.y) and IsFinite(vec3.z)
end

local movementUpdateEvent = Instance.new("BindableEvent")
movementUpdateEvent.Name = "MovementUpdate"
movementUpdateEvent.Parent = script

coroutine.wrap(function()
	local PathDisplayModule = script.Parent:WaitForChild("PathDisplay")
	if PathDisplayModule then
		PathDisplay = require(PathDisplayModule)
	end
end)()

--[[ The Class ]]--
local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController"))
local VRNavigation = setmetatable({}, BaseCharacterController)
VRNavigation.__index = VRNavigation

function VRNavigation.new(CONTROL_ACTION_PRIORITY)
	local self = setmetatable(BaseCharacterController.new() :: any, VRNavigation)

	self.CONTROL_ACTION_PRIORITY = CONTROL_ACTION_PRIORITY

	self.navigationRequestedConn = nil
	self.heartbeatConn = nil

	self.currentDestination = nil
	self.currentPath = nil
	self.currentPoints = nil
	self.currentPointIdx = 0

	self.expectedTimeToNextPoint = 0
	self.timeReachedLastPoint = tick()
	self.moving = false

	self.isJumpBound = false
	self.moveLatch = false

	self.userCFrameEnabledConn = nil

	return self
end

function VRNavigation:SetLaserPointerMode(mode)
	pcall(function()
		StarterGui:SetCore("VRLaserPointerMode", mode)
	end)
end

function VRNavigation:GetLocalHumanoid()
	local character = LocalPlayer.Character
	if not character then
		return
	end

	for _, child in pairs(character:GetChildren()) do
		if child:IsA("Humanoid") then
			return child
		end
	end
	return nil
end

function VRNavigation:HasBothHandControllers()
	return VRService:GetUserCFrameEnabled(Enum.UserCFrame.RightHand) and VRService:GetUserCFrameEnabled(Enum.UserCFrame.LeftHand)
end

function VRNavigation:HasAnyHandControllers()
	return VRService:GetUserCFrameEnabled(Enum.UserCFrame.RightHand) or VRService:GetUserCFrameEnabled(Enum.UserCFrame.LeftHand)
end

function VRNavigation:IsMobileVR()
	return UserInputService.TouchEnabled
end

function VRNavigation:HasGamepad()
	return UserInputService.GamepadEnabled
end

function VRNavigation:ShouldUseNavigationLaser()
	--Places where we use the navigation laser:
	-- mobile VR with any number of hands tracked
	-- desktop VR with only one hand tracked
	-- desktop VR with no hands and no gamepad (i.e. with Oculus remote?)
	--using an Xbox controller with a desktop VR headset means no laser since the user has a thumbstick.
	--in the future, we should query thumbstick presence with a features API
	if self:IsMobileVR() then
		return true
	else
		if self:HasBothHandControllers() then
			return false
		end
		if not self:HasAnyHandControllers() then
			return not self:HasGamepad()
		end
		return true
	end
end

function VRNavigation:StartFollowingPath(newPath)
	currentPath = newPath
	currentPoints = currentPath:GetPointCoordinates()
	currentPointIdx = 1
	moving = true

	timeReachedLastPoint = tick()

	local humanoid = self:GetLocalHumanoid()
	if humanoid and humanoid.Torso and #currentPoints >= 1 then
		local dist = (currentPoints[1] - humanoid.Torso.Position).magnitude
		expectedTimeToNextPoint = dist / humanoid.WalkSpeed
	end

	movementUpdateEvent:Fire("targetPoint", self.currentDestination)
end

function VRNavigation:GoToPoint(point)
	currentPath = true
	currentPoints = { point }
	currentPointIdx = 1
	moving = true

	local humanoid = self:GetLocalHumanoid()
	local distance = (humanoid.Torso.Position - point).magnitude
	local estimatedTimeRemaining = distance / humanoid.WalkSpeed

	timeReachedLastPoint = tick()
	expectedTimeToNextPoint = estimatedTimeRemaining

	movementUpdateEvent:Fire("targetPoint", point)
end

function VRNavigation:StopFollowingPath()
	currentPath = nil
	currentPoints = nil
	currentPointIdx = 0
	moving = false
	self.moveVector = ZERO_VECTOR3
end

function VRNavigation:TryComputePath(startPos: Vector3, destination: Vector3)
	local numAttempts = 0
	local newPath = nil

	while not newPath and numAttempts < 5 do
		newPath = PathfindingService:ComputeSmoothPathAsync(startPos, destination, MAX_PATHING_DISTANCE)
		numAttempts = numAttempts + 1

		if newPath.Status == Enum.PathStatus.ClosestNoPath or newPath.Status == Enum.PathStatus.ClosestOutOfRange then
			newPath = nil
			break
		end

		if newPath and newPath.Status == Enum.PathStatus.FailStartNotEmpty then
			startPos = startPos + (destination - startPos).Unit
			newPath = nil
		end

		if newPath and newPath.Status == Enum.PathStatus.FailFinishNotEmpty then
			destination = destination + Vector3.new(0, 1, 0)
			newPath = nil
		end
	end

	return newPath
end

function VRNavigation:OnNavigationRequest(destinationCFrame: CFrame, inputUserCFrame: CFrame)
	local destinationPosition = destinationCFrame.Position
	local lastDestination = self.currentDestination

	if not IsFiniteVector3(destinationPosition) then
		return
	end

	self.currentDestination = destinationPosition

	local humanoid = self:GetLocalHumanoid()
	if not humanoid or not humanoid.Torso then
		return
	end

	local currentPosition = humanoid.Torso.Position
	local distanceToDestination = (self.currentDestination - currentPosition).magnitude

	if distanceToDestination < NO_PATH_THRESHOLD then
		self:GoToPoint(self.currentDestination)
		return
	end

	if not lastDestination or (self.currentDestination - lastDestination).magnitude > RECALCULATE_PATH_THRESHOLD then
		local newPath = self:TryComputePath(currentPosition, self.currentDestination)
		if newPath then
			self:StartFollowingPath(newPath)
			if PathDisplay then
				PathDisplay.setCurrentPoints(self.currentPoints)
				PathDisplay.renderPath()
			end
		else
			self:StopFollowingPath()
			if PathDisplay then
				PathDisplay.clearRenderedPath()
			end
		end
	else
		if moving then
			self.currentPoints[#currentPoints] = self.currentDestination
		else
			self:GoToPoint(self.currentDestination)
		end
	end
end

function VRNavigation:OnJumpAction(actionName, inputState, inputObj)
	if inputState == Enum.UserInputState.Begin then
		self.isJumping = true
	end
	return Enum.ContextActionResult.Sink
end
function VRNavigation:BindJumpAction(active)
	if active then
		if not self.isJumpBound then
			self.isJumpBound = true
			ContextActionService:BindActionAtPriority("VRJumpAction", (function() return self:OnJumpAction() end), false,
				self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.ButtonA)
		end
	else
		if self.isJumpBound then
			self.isJumpBound = false
			ContextActionService:UnbindAction("VRJumpAction")
		end
	end
end

function VRNavigation:ControlCharacterGamepad(actionName, inputState, inputObject)
	if inputObject.KeyCode ~= Enum.KeyCode.Thumbstick1 then return end

	if inputState == Enum.UserInputState.Cancel then
		self.moveVector =  ZERO_VECTOR3
		return
	end

	if inputState ~= Enum.UserInputState.End then
		self:StopFollowingPath()
		if PathDisplay then
			PathDisplay.clearRenderedPath()
		end

		if self:ShouldUseNavigationLaser() then
			self:BindJumpAction(true)
			self:SetLaserPointerMode("Hidden")
		end

		if inputObject.Position.magnitude > THUMBSTICK_DEADZONE then
			self.moveVector = Vector3.new(inputObject.Position.X, 0, -inputObject.Position.Y)
			if self.moveVector.magnitude > 0 then
				self.moveVector = self.moveVector.unit * math.min(1, inputObject.Position.magnitude)
			end

			self.moveLatch = true
		end
	else
		self.moveVector =  ZERO_VECTOR3

		if self:ShouldUseNavigationLaser() then
			self:BindJumpAction(false)
			self:SetLaserPointerMode("Navigation")
		end

		if self.moveLatch then
			self.moveLatch = false
			movementUpdateEvent:Fire("offtrack")
		end
	end
	return Enum.ContextActionResult.Sink
end

function VRNavigation:OnHeartbeat(dt)
	local newMoveVector = self.moveVector
	local humanoid = self:GetLocalHumanoid()
	if not humanoid or not humanoid.Torso then
		return
	end

	if self.moving and self.currentPoints then
		local currentPosition = humanoid.Torso.Position
		local goalPosition = currentPoints[1]
		local vectorToGoal = (goalPosition - currentPosition) * XZ_VECTOR3
		local moveDist = vectorToGoal.magnitude
		local moveDir = vectorToGoal / moveDist

		if moveDist < POINT_REACHED_THRESHOLD then
			local estimatedTimeRemaining = 0
			local prevPoint = currentPoints[1]
			for i, point in pairs(currentPoints) do
				if i ~= 1 then
					local dist = (point - prevPoint).magnitude
					prevPoint = point
					estimatedTimeRemaining = estimatedTimeRemaining + (dist / humanoid.WalkSpeed)
				end
			end

			table.remove(currentPoints, 1)
			currentPointIdx = currentPointIdx + 1

			if #currentPoints == 0 then
				self:StopFollowingPath()
				if PathDisplay then
					PathDisplay.clearRenderedPath()
				end
				return
			else
				if PathDisplay then
					PathDisplay.setCurrentPoints(currentPoints)
					PathDisplay.renderPath()
				end

				local newGoal = currentPoints[1]
				local distanceToGoal = (newGoal - currentPosition).magnitude
				expectedTimeToNextPoint = distanceToGoal / humanoid.WalkSpeed
				timeReachedLastPoint = tick()
			end
		else
			local ignoreTable = {
				(game.Players.LocalPlayer :: Player).Character,
				workspace.CurrentCamera
			}
			local obstructRay = Ray.new(currentPosition - Vector3.new(0, 1, 0), moveDir * 3)
			local obstructPart, obstructPoint, obstructNormal = workspace:FindPartOnRayWithIgnoreList(obstructRay, ignoreTable)

			if obstructPart then
				local heightOffset = Vector3.new(0, 100, 0)
				local jumpCheckRay = Ray.new(obstructPoint + moveDir * 0.5 + heightOffset, -heightOffset)
				local jumpCheckPart, jumpCheckPoint, jumpCheckNormal = workspace:FindPartOnRayWithIgnoreList(jumpCheckRay, ignoreTable)

				local heightDifference = jumpCheckPoint.Y - currentPosition.Y
				if heightDifference < 6 and heightDifference > -2 then
					humanoid.Jump = true
				end
			end

			local timeSinceLastPoint = tick() - timeReachedLastPoint
			if timeSinceLastPoint > expectedTimeToNextPoint + OFFTRACK_TIME_THRESHOLD then
				self:StopFollowingPath()
				if PathDisplay then
					PathDisplay.clearRenderedPath()
				end

				movementUpdateEvent:Fire("offtrack")
			end

			newMoveVector = self.moveVector:Lerp(moveDir, dt * 10)
		end
	end

	if IsFiniteVector3(newMoveVector) then
		self.moveVector = newMoveVector
	end
end

function VRNavigation:OnUserCFrameEnabled()
	if self:ShouldUseNavigationLaser() then
		self:BindJumpAction(false)
		self:SetLaserPointerMode("Navigation")
	else
		self:BindJumpAction(true)
		self:SetLaserPointerMode("Hidden")
	end
end

function VRNavigation:Enable(enable)

	self.moveVector = ZERO_VECTOR3
	self.isJumping = false

	if enable then
		self.navigationRequestedConn = VRService.NavigationRequested:Connect(function(destinationCFrame, inputUserCFrame) self:OnNavigationRequest(destinationCFrame, inputUserCFrame) end)
		self.heartbeatConn = RunService.Heartbeat:Connect(function(dt) self:OnHeartbeat(dt) end)

		ContextActionService:BindAction("MoveThumbstick", (function(actionName, inputState, inputObject) return self:ControlCharacterGamepad(actionName, inputState, inputObject) end),
			false, self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.Thumbstick1)
		ContextActionService:BindActivate(Enum.UserInputType.Gamepad1, Enum.KeyCode.ButtonR2)

		self.userCFrameEnabledConn = VRService.UserCFrameEnabled:Connect(function() self:OnUserCFrameEnabled() end)
		self:OnUserCFrameEnabled()

		VRService:SetTouchpadMode(Enum.VRTouchpad.Left, Enum.VRTouchpadMode.VirtualThumbstick)
		VRService:SetTouchpadMode(Enum.VRTouchpad.Right, Enum.VRTouchpadMode.ABXY)

		self.enabled = true
	else
		-- Disable
		self:StopFollowingPath()

		ContextActionService:UnbindAction("MoveThumbstick")
		ContextActionService:UnbindActivate(Enum.UserInputType.Gamepad1, Enum.KeyCode.ButtonR2)

		self:BindJumpAction(false)
		self:SetLaserPointerMode("Disabled")

		if self.navigationRequestedConn then
			self.navigationRequestedConn:Disconnect()
			self.navigationRequestedConn = nil
		end
		if self.heartbeatConn then
			self.heartbeatConn:Disconnect()
			self.heartbeatConn = nil
		end
		if self.userCFrameEnabledConn then
			self.userCFrameEnabledConn:Disconnect()
			self.userCFrameEnabledConn = nil
		end
		self.enabled = false
	end
end

return VRNavigation


--!nonstrict
--[[
	ControlModule - This ModuleScript implements a singleton class to manage the
	selection, activation, and deactivation of the current character movement controller.
	This script binds to RenderStepped at Input priority and calls the Update() methods
	on the active controller instances.

	The character controller ModuleScripts implement classes which are instantiated and
	activated as-needed, they are no longer all instantiated up front as they were in
	the previous generation of PlayerScripts.

	2018 PlayerScripts Update - AllYourBlox
--]]
local ControlModule = {}
ControlModule.__index = ControlModule

--[[ Roblox Services ]]
--
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local GuiService = game:GetService("GuiService")
local Workspace = game:GetService("Workspace")
local UserGameSettings = UserSettings():GetService("UserGameSettings")
local VRService = game:GetService("VRService")

-- Roblox User Input Control Modules - each returns a new() constructor function used to create controllers as needed
local Keyboard = require(script:WaitForChild("Keyboard"))
local Gamepad = require(script:WaitForChild("Gamepad"))
local DynamicThumbstick = require(script:WaitForChild("DynamicThumbstick"))

local FFlagUserHideControlsWhenMenuOpen
do
	local success, result = pcall(function()
		return UserSettings():IsUserFeatureEnabled("UserHideControlsWhenMenuOpen")
	end)
	FFlagUserHideControlsWhenMenuOpen = success and result
end

local FFlagUserDynamicThumbstickSafeAreaUpdate
do
	local success, result = pcall(function()
		return UserSettings():IsUserFeatureEnabled("UserDynamicThumbstickSafeAreaUpdate")
	end)
	FFlagUserDynamicThumbstickSafeAreaUpdate = success and result
end

local TouchThumbstick = require(script:WaitForChild("TouchThumbstick"))

-- These controllers handle only walk/run movement, jumping is handled by the
-- TouchJump controller if any of these are active
local ClickToMove = require(script:WaitForChild("ClickToMoveController"))
local TouchJump = require(script:WaitForChild("TouchJump"))

local VehicleController = require(script:WaitForChild("VehicleController"))

local CONTROL_ACTION_PRIORITY = Enum.ContextActionPriority.Medium.Value

-- Mapping from movement mode and lastInputType enum values to control modules to avoid huge if elseif switching
local movementEnumToModuleMap = {
	[Enum.TouchMovementMode.DPad] = DynamicThumbstick,
	[Enum.DevTouchMovementMode.DPad] = DynamicThumbstick,
	[Enum.TouchMovementMode.Thumbpad] = DynamicThumbstick,
	[Enum.DevTouchMovementMode.Thumbpad] = DynamicThumbstick,
	[Enum.TouchMovementMode.Thumbstick] = TouchThumbstick,
	[Enum.DevTouchMovementMode.Thumbstick] = TouchThumbstick,
	[Enum.TouchMovementMode.DynamicThumbstick] = DynamicThumbstick,
	[Enum.DevTouchMovementMode.DynamicThumbstick] = DynamicThumbstick,
	[Enum.TouchMovementMode.ClickToMove] = ClickToMove,
	[Enum.DevTouchMovementMode.ClickToMove] = ClickToMove,

	-- Current default
	[Enum.TouchMovementMode.Default] = DynamicThumbstick,

	[Enum.ComputerMovementMode.Default] = Keyboard,
	[Enum.ComputerMovementMode.KeyboardMouse] = Keyboard,
	[Enum.DevComputerMovementMode.KeyboardMouse] = Keyboard,
	[Enum.DevComputerMovementMode.Scriptable] = nil,
	[Enum.ComputerMovementMode.ClickToMove] = ClickToMove,
	[Enum.DevComputerMovementMode.ClickToMove] = ClickToMove,
}

-- Keyboard controller is really keyboard and mouse controller
local computerInputTypeToModuleMap = {
	[Enum.UserInputType.Keyboard] = Keyboard,
	[Enum.UserInputType.MouseButton1] = Keyboard,
	[Enum.UserInputType.MouseButton2] = Keyboard,
	[Enum.UserInputType.MouseButton3] = Keyboard,
	[Enum.UserInputType.MouseWheel] = Keyboard,
	[Enum.UserInputType.MouseMovement] = Keyboard,
	[Enum.UserInputType.Gamepad1] = Gamepad,
	[Enum.UserInputType.Gamepad2] = Gamepad,
	[Enum.UserInputType.Gamepad3] = Gamepad,
	[Enum.UserInputType.Gamepad4] = Gamepad,
}

local lastInputType

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

	-- The Modules above are used to construct controller instances as-needed, and this
	-- table is a map from Module to the instance created from it
	self.controllers = {}

	self.activeControlModule = nil -- Used to prevent unnecessarily expensive checks on each input event
	self.activeController = nil
	self.touchJumpController = nil
	self.moveFunction = Players.LocalPlayer.Move
	self.humanoid = nil
	self.lastInputType = Enum.UserInputType.None
	self.controlsEnabled = true

	-- For Roblox self.vehicleController
	self.humanoidSeatedConn = nil
	self.vehicleController = nil

	self.touchControlFrame = nil

	if FFlagUserHideControlsWhenMenuOpen then
		GuiService.MenuOpened:Connect(function()
			if self.touchControlFrame and self.touchControlFrame.Visible then
				self.touchControlFrame.Visible = false
			end
		end)

		GuiService.MenuClosed:Connect(function()
			if self.touchControlFrame then
				self.touchControlFrame.Visible = true
			end
		end)
	end

	self.vehicleController = VehicleController.new(CONTROL_ACTION_PRIORITY)

	Players.LocalPlayer.CharacterAdded:Connect(function(char)
		self:OnCharacterAdded(char)
	end)
	Players.LocalPlayer.CharacterRemoving:Connect(function(char)
		self:OnCharacterRemoving(char)
	end)
	if Players.LocalPlayer.Character then
		self:OnCharacterAdded(Players.LocalPlayer.Character)
	end

	RunService:BindToRenderStep("ControlScriptRenderstep", Enum.RenderPriority.Input.Value, function(dt)
		self:OnRenderStepped(dt)
	end)

	UserInputService.LastInputTypeChanged:Connect(function(newLastInputType)
		self:OnLastInputTypeChanged(newLastInputType)
	end)

	UserGameSettings:GetPropertyChangedSignal("TouchMovementMode"):Connect(function()
		self:OnTouchMovementModeChange()
	end)
	Players.LocalPlayer:GetPropertyChangedSignal("DevTouchMovementMode"):Connect(function()
		self:OnTouchMovementModeChange()
	end)

	UserGameSettings:GetPropertyChangedSignal("ComputerMovementMode"):Connect(function()
		self:OnComputerMovementModeChange()
	end)
	Players.LocalPlayer:GetPropertyChangedSignal("DevComputerMovementMode"):Connect(function()
		self:OnComputerMovementModeChange()
	end)

	--[[ Touch Device UI ]]
	--
	self.playerGui = nil
	self.touchGui = nil
	self.playerGuiAddedConn = nil

	GuiService:GetPropertyChangedSignal("TouchControlsEnabled"):Connect(function()
		self:UpdateTouchGuiVisibility()
		self:UpdateActiveControlModuleEnabled()
	end)

	if UserInputService.TouchEnabled then
		self.playerGui = Players.LocalPlayer:FindFirstChildOfClass("PlayerGui")
		if self.playerGui then
			self:CreateTouchGuiContainer()
			self:OnLastInputTypeChanged(UserInputService:GetLastInputType())
		else
			self.playerGuiAddedConn = Players.LocalPlayer.ChildAdded:Connect(function(child)
				if child:IsA("PlayerGui") then
					self.playerGui = child
					self:CreateTouchGuiContainer()
					self.playerGuiAddedConn:Disconnect()
					self.playerGuiAddedConn = nil
					self:OnLastInputTypeChanged(UserInputService:GetLastInputType())
				end
			end)
		end
	else
		self:OnLastInputTypeChanged(UserInputService:GetLastInputType())
	end

	return self
end

-- Convenience function so that calling code does not have to first get the activeController
-- and then call GetMoveVector on it. When there is no active controller, this function returns the
-- zero vector
function ControlModule:GetMoveVector(): Vector3
	if self.activeController then
		return self.activeController:GetMoveVector()
	end
	return Vector3.new(0, 0, 0)
end

function ControlModule:GetActiveController()
	return self.activeController
end

-- Checks for conditions for enabling/disabling the active controller and updates whether the active controller is enabled/disabled
function ControlModule:UpdateActiveControlModuleEnabled()
	-- helpers for disable/enable
	local disable = function()
		self.activeController:Enable(false)

		if self.moveFunction then
			self.moveFunction(Players.LocalPlayer, Vector3.new(0, 0, 0), true)
		end
	end

	local enable = function()
		if self.activeControlModule == ClickToMove then
			-- For ClickToMove, when it is the player's choice, we also enable the full keyboard controls.
			-- When the developer is forcing click to move, the most keyboard controls (WASD) are not available, only jump.
			self.activeController:Enable(
				true,
				Players.LocalPlayer.DevComputerMovementMode == Enum.DevComputerMovementMode.UserChoice,
				self.touchJumpController
			)
		elseif self.touchControlFrame then
			self.activeController:Enable(true, self.touchControlFrame)
		else
			self.activeController:Enable(true)
		end
	end

	-- there is no active controller
	if not self.activeController then
		return
	end

	-- developer called ControlModule:Disable(), don't turn back on
	if not self.controlsEnabled then
		disable()
		return
	end

	-- GuiService.TouchControlsEnabled == false and the active controller is a touch controller,
	-- disable controls
	if
		not GuiService.TouchControlsEnabled
		and UserInputService.TouchEnabled
		and (
			self.activeControlModule == ClickToMove
			or self.activeControlModule == TouchThumbstick
			or self.activeControlModule == DynamicThumbstick
		)
	then
		disable()
		return
	end

	-- no settings prevent enabling controls
	enable()
end

function ControlModule:Enable(enable: boolean?)
	if enable == nil then
		enable = true
	end
	self.controlsEnabled = enable

	if not self.activeController then
		return
	end

	self:UpdateActiveControlModuleEnabled()
end

-- For those who prefer distinct functions
function ControlModule:Disable()
	self.controlsEnabled = false

	self:UpdateActiveControlModuleEnabled()
end

-- Returns module (possibly nil) and success code to differentiate returning nil due to error vs Scriptable
function ControlModule:SelectComputerMovementModule(): ({}?, boolean)
	if not (UserInputService.KeyboardEnabled or UserInputService.GamepadEnabled) then
		return nil, false
	end

	local computerModule
	local DevMovementMode = Players.LocalPlayer.DevComputerMovementMode

	if DevMovementMode == Enum.DevComputerMovementMode.UserChoice then
		computerModule = computerInputTypeToModuleMap[lastInputType]
		if
			UserGameSettings.ComputerMovementMode == Enum.ComputerMovementMode.ClickToMove
			and computerModule == Keyboard
		then
			-- User has ClickToMove set in Settings, prefer ClickToMove controller for keyboard and mouse lastInputTypes
			computerModule = ClickToMove
		end
	else
		-- Developer has selected a mode that must be used.
		computerModule = movementEnumToModuleMap[DevMovementMode]

		-- computerModule is expected to be nil here only when developer has selected Scriptable
		if (not computerModule) and DevMovementMode ~= Enum.DevComputerMovementMode.Scriptable then
			warn("No character control module is associated with DevComputerMovementMode ", DevMovementMode)
		end
	end

	if computerModule then
		return computerModule, true
	elseif DevMovementMode == Enum.DevComputerMovementMode.Scriptable then
		-- Special case where nil is returned and we actually want to set self.activeController to nil for Scriptable
		return nil, true
	else
		-- This case is for when computerModule is nil because of an error and no suitable control module could
		-- be found.
		return nil, false
	end
end

-- Choose current Touch control module based on settings (user, dev)
-- Returns module (possibly nil) and success code to differentiate returning nil due to error vs Scriptable
function ControlModule:SelectTouchModule(): ({}?, boolean)
	if not UserInputService.TouchEnabled then
		return nil, false
	end
	local touchModule
	local DevMovementMode = Players.LocalPlayer.DevTouchMovementMode
	if DevMovementMode == Enum.DevTouchMovementMode.UserChoice then
		touchModule = movementEnumToModuleMap[UserGameSettings.TouchMovementMode]
	elseif DevMovementMode == Enum.DevTouchMovementMode.Scriptable then
		return nil, true
	else
		touchModule = movementEnumToModuleMap[DevMovementMode]
	end
	return touchModule, true
end

local function calculateRawMoveVector(humanoid: Humanoid, cameraRelativeMoveVector: Vector3): Vector3
	local camera = Workspace.CurrentCamera
	if not camera then
		return cameraRelativeMoveVector
	end

	if humanoid:GetState() == Enum.HumanoidStateType.Swimming then
		return camera.CFrame:VectorToWorldSpace(cameraRelativeMoveVector)
	end

	local cameraCFrame = camera.CFrame

	if VRService.VREnabled and humanoid.RootPart then
		-- movement relative to VR frustum
		local cameraDelta = humanoid.RootPart.CFrame.Position - cameraCFrame.Position
		if cameraDelta.Magnitude < 3 then -- "nearly" first person
			local vrFrame = VRService:GetUserCFrame(Enum.UserCFrame.Head)
			cameraCFrame = cameraCFrame * vrFrame
		end
	end

	local c, s
	local _, _, _, R00, R01, R02, _, _, R12, _, _, R22 = cameraCFrame:GetComponents()
	if R12 < 1 and R12 > -1 then
		-- X and Z components from back vector.
		c = R22
		s = R02
	else
		-- In this case the camera is looking straight up or straight down.
		-- Use X components from right and up vectors.
		c = R00
		s = -R01 * math.sign(R12)
	end
	local norm = math.sqrt(c * c + s * s)
	return Vector3.new(
		(c * cameraRelativeMoveVector.X + s * cameraRelativeMoveVector.Z) / norm,
		0,
		(c * cameraRelativeMoveVector.Z - s * cameraRelativeMoveVector.X) / norm
	)
end

function ControlModule:OnRenderStepped(dt)
	if self.activeController and self.activeController.enabled and self.humanoid then
		-- Give the controller a chance to adjust its state
		self.activeController:OnRenderStepped(dt)

		-- Now retrieve info from the controller
		local moveVector = self.activeController:GetMoveVector()
		local cameraRelative = self.activeController:IsMoveVectorCameraRelative()

		local clickToMoveController = self:GetClickToMoveController()
		if self.activeController ~= clickToMoveController then
			if moveVector.magnitude > 0 then
				-- Clean up any developer started MoveTo path
				clickToMoveController:CleanupPath()
			else
				-- Get move vector for developer started MoveTo
				clickToMoveController:OnRenderStepped(dt)
				moveVector = clickToMoveController:GetMoveVector()
				cameraRelative = clickToMoveController:IsMoveVectorCameraRelative()
			end
		end

		-- Are we driving a vehicle ?
		local vehicleConsumedInput = false
		if self.vehicleController then
			moveVector, vehicleConsumedInput =
				self.vehicleController:Update(moveVector, cameraRelative, self.activeControlModule == Gamepad)
		end

		-- If not, move the player
		-- Verification of vehicleConsumedInput is commented out to preserve legacy behavior,
		-- in case some game relies on Humanoid.MoveDirection still being set while in a VehicleSeat
		--if not vehicleConsumedInput then
		if cameraRelative then
			moveVector = calculateRawMoveVector(self.humanoid, moveVector)
		end
		self.moveFunction(Players.LocalPlayer, moveVector, false)
		--end

		-- And make them jump if needed
		self.humanoid.Jump = self.activeController:GetIsJumping()
			or (self.touchJumpController and self.touchJumpController:GetIsJumping())
	end
end

function ControlModule:OnHumanoidSeated(active: boolean, currentSeatPart: BasePart)
	if active then
		if currentSeatPart and currentSeatPart:IsA("VehicleSeat") then
			if not self.vehicleController then
				self.vehicleController = self.vehicleController.new(CONTROL_ACTION_PRIORITY)
			end
			self.vehicleController:Enable(true, currentSeatPart)
		end
	else
		if self.vehicleController then
			self.vehicleController:Enable(false, currentSeatPart)
		end
	end
end

function ControlModule:OnCharacterAdded(char)
	self.humanoid = char:FindFirstChildOfClass("Humanoid")
	while not self.humanoid do
		char.ChildAdded:wait()
		self.humanoid = char:FindFirstChildOfClass("Humanoid")
	end

	self:UpdateTouchGuiVisibility()

	if self.humanoidSeatedConn then
		self.humanoidSeatedConn:Disconnect()
		self.humanoidSeatedConn = nil
	end
	self.humanoidSeatedConn = self.humanoid.Seated:Connect(function(active, currentSeatPart)
		self:OnHumanoidSeated(active, currentSeatPart)
	end)
end

function ControlModule:OnCharacterRemoving(char)
	self.humanoid = nil

	self:UpdateTouchGuiVisibility()
end

function ControlModule:UpdateTouchGuiVisibility()
	if self.touchGui then
		local doShow = self.humanoid and GuiService.TouchControlsEnabled
		self.touchGui.Enabled = not not doShow -- convert to bool
	end
end

-- Helper function to lazily instantiate a controller if it does not yet exist,
-- disable the active controller if it is different from the on being switched to,
-- and then enable the requested controller. The argument to this function must be
-- a reference to one of the control modules, i.e. Keyboard, Gamepad, etc.

-- This function should handle all controller enabling and disabling without relying on
-- ControlModule:Enable() and Disable()
function ControlModule:SwitchToController(controlModule)
	-- controlModule is invalid, just disable current controller
	if not controlModule then
		if self.activeController then
			self.activeController:Enable(false)
		end
		self.activeController = nil
		self.activeControlModule = nil
		return
	end

	-- first time switching to this control module, should instantiate it
	if not self.controllers[controlModule] then
		self.controllers[controlModule] = controlModule.new(CONTROL_ACTION_PRIORITY)
	end

	-- switch to the new controlModule
	if self.activeController ~= self.controllers[controlModule] then
		if self.activeController then
			self.activeController:Enable(false)
		end
		self.activeController = self.controllers[controlModule]
		self.activeControlModule = controlModule -- Only used to check if controller switch is necessary

		if
			self.touchControlFrame
			and (
				self.activeControlModule == ClickToMove
				or self.activeControlModule == TouchThumbstick
				or self.activeControlModule == DynamicThumbstick
			)
		then
			if not self.controllers[TouchJump] then
				self.controllers[TouchJump] = TouchJump.new()
			end
			self.touchJumpController = self.controllers[TouchJump]
			self.touchJumpController:Enable(true, self.touchControlFrame)
		else
			if self.touchJumpController then
				self.touchJumpController:Enable(false)
			end
		end

		self:UpdateActiveControlModuleEnabled()
	end
end

function ControlModule:OnLastInputTypeChanged(newLastInputType)
	if lastInputType == newLastInputType then
		warn("LastInputType Change listener called with current type.")
	end
	lastInputType = newLastInputType

	if lastInputType == Enum.UserInputType.Touch then
		-- TODO: Check if touch module already active
		local touchModule, success = self:SelectTouchModule()
		if success then
			while not self.touchControlFrame do
				wait()
			end
			self:SwitchToController(touchModule)
		end
	elseif computerInputTypeToModuleMap[lastInputType] ~= nil then
		local computerModule = self:SelectComputerMovementModule()
		if computerModule then
			self:SwitchToController(computerModule)
		end
	end

	self:UpdateTouchGuiVisibility()
end

-- Called when any relevant values of GameSettings or LocalPlayer change, forcing re-evalulation of
-- current control scheme
function ControlModule:OnComputerMovementModeChange()
	local controlModule, success = self:SelectComputerMovementModule()
	if success then
		self:SwitchToController(controlModule)
	end
end

function ControlModule:OnTouchMovementModeChange()
	local touchModule, success = self:SelectTouchModule()
	if success then
		while not self.touchControlFrame do
			wait()
		end
		self:SwitchToController(touchModule)
	end
end

function ControlModule:CreateTouchGuiContainer()
	if self.touchGui then
		self.touchGui:Destroy()
	end

	-- Container for all touch device guis
	self.touchGui = Instance.new("ScreenGui")
	self.touchGui.Name = math.random()
	self.touchGui.ResetOnSpawn = false
	self.touchGui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
	self:UpdateTouchGuiVisibility()

	if FFlagUserDynamicThumbstickSafeAreaUpdate then
		self.touchGui.ClipToDeviceSafeArea = false
	end

	self.touchControlFrame = Instance.new("Frame")
	self.touchControlFrame.Name = "TouchControlFrame"
	self.touchControlFrame.Size = UDim2.new(1, 0, 1, 0)
	self.touchControlFrame.BackgroundTransparency = 1
	self.touchControlFrame.Parent = self.touchGui

	self.touchGui.Parent = self.playerGui
end

function ControlModule:GetClickToMoveController()
	if not self.controllers[ClickToMove] then
		self.controllers[ClickToMove] = ClickToMove.new(CONTROL_ACTION_PRIORITY)
	end
	return self.controllers[ClickToMove]
end

return ControlModule.new()


--!nonstrict
-- Roblox character sound script

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

local AtomicBinding = require(script:WaitForChild("AtomicBinding"))

local function loadFlag(flag: string)
	local success, result = pcall(function()
		return UserSettings():IsUserFeatureEnabled(flag)
	end)
	return success and result
end

local SOUND_DATA: { [string]: { [string]: any } } = {
	Climbing = {
		SoundId = "rbxasset://sounds/action_footsteps_plastic.mp3",
		Looped = true,
	},
	Died = {
		SoundId = "rbxasset://sounds/uuhhh.mp3",
	},
	FreeFalling = {
		SoundId = "rbxasset://sounds/action_falling.mp3",
		Looped = true,
	},
	GettingUp = {
		SoundId = "rbxasset://sounds/action_get_up.mp3",
	},
	Jumping = {
		SoundId = "rbxasset://sounds/action_jump.mp3",
	},
	Landing = {
		SoundId = "rbxasset://sounds/action_jump_land.mp3",
	},
	Running = {
		SoundId = "rbxasset://sounds/action_footsteps_plastic.mp3",
		Looped = true,
		Pitch = 1.85,
	},
	Splash = {
		SoundId = "rbxasset://sounds/impact_water.mp3",
	},
	Swimming = {
		SoundId = "rbxasset://sounds/action_swim.mp3",
		Looped = true,
		Pitch = 1.6,
	},
}

-- map a value from one range to another
local function map(x: number, inMin: number, inMax: number, outMin: number, outMax: number): number
	return (x - inMin) * (outMax - outMin) / (inMax - inMin) + outMin
end

local function playSound(sound: Sound)
	sound.TimePosition = 0
	sound.Playing = true
end

local function shallowCopy(t)
	local out = {}
	for k, v in pairs(t) do
		out[k] = v
	end
	return out
end

local function initializeSoundSystem(instances)
	local player = instances.player
	local humanoid = instances.humanoid
	local rootPart = instances.rootPart

	local sounds: { [string]: Sound } = {}

	-- initialize sounds
	for name: string, props: { [string]: any } in pairs(SOUND_DATA) do
		local sound: Sound = Instance.new("Sound")
		sound.Name = name

		-- set default values
		sound.Archivable = false
		sound.RollOffMinDistance = 5
		sound.RollOffMaxDistance = 150
		sound.Volume = 0.65

		for propName, propValue: any in pairs(props) do
			(sound :: any)[propName] = propValue
		end

		sound.Parent = rootPart
		sounds[name] = sound
	end

	local playingLoopedSounds: { [Sound]: boolean? } = {}

	local function stopPlayingLoopedSounds(except: Sound?)
		for sound in pairs(shallowCopy(playingLoopedSounds)) do
			if sound ~= except then
				sound.Playing = false
				playingLoopedSounds[sound] = nil
			end
		end
	end

	-- state transition callbacks.
	local stateTransitions: { [Enum.HumanoidStateType]: () -> () } = {
		[Enum.HumanoidStateType.FallingDown] = function()
			stopPlayingLoopedSounds()
		end,

		[Enum.HumanoidStateType.GettingUp] = function()
			stopPlayingLoopedSounds()
			playSound(sounds.GettingUp)
		end,

		[Enum.HumanoidStateType.Jumping] = function()
			stopPlayingLoopedSounds()
			playSound(sounds.Jumping)
		end,

		[Enum.HumanoidStateType.Swimming] = function()
			local verticalSpeed = math.abs(rootPart.AssemblyLinearVelocity.Y)
			if verticalSpeed > 0.1 then
				sounds.Splash.Volume = math.clamp(map(verticalSpeed, 100, 350, 0.28, 1), 0, 1)
				playSound(sounds.Splash)
			end
			stopPlayingLoopedSounds(sounds.Swimming)
			sounds.Swimming.Playing = true
			playingLoopedSounds[sounds.Swimming] = true
		end,

		[Enum.HumanoidStateType.Freefall] = function()
			sounds.FreeFalling.Volume = 0
			stopPlayingLoopedSounds(sounds.FreeFalling)
			playingLoopedSounds[sounds.FreeFalling] = true
		end,

		[Enum.HumanoidStateType.Landed] = function()
			stopPlayingLoopedSounds()
			local verticalSpeed = math.abs(rootPart.AssemblyLinearVelocity.Y)
			if verticalSpeed > 75 then
				sounds.Landing.Volume = math.clamp(map(verticalSpeed, 50, 100, 0, 1), 0, 1)
				playSound(sounds.Landing)
			end
		end,

		[Enum.HumanoidStateType.Running] = function()
			stopPlayingLoopedSounds(sounds.Running)
			sounds.Running.Playing = true
			playingLoopedSounds[sounds.Running] = true
		end,

		[Enum.HumanoidStateType.Climbing] = function()
			local sound = sounds.Climbing
			if math.abs(rootPart.AssemblyLinearVelocity.Y) > 0.1 then
				sound.Playing = true
				stopPlayingLoopedSounds(sound)
			else
				stopPlayingLoopedSounds()
			end
			playingLoopedSounds[sound] = true
		end,

		[Enum.HumanoidStateType.Seated] = function()
			stopPlayingLoopedSounds()
		end,

		[Enum.HumanoidStateType.Dead] = function()
			stopPlayingLoopedSounds()
			playSound(sounds.Died)
		end,
	}

	-- updaters for looped sounds
	local loopedSoundUpdaters: { [Sound]: (number, Sound, Vector3) -> () } = {
		[sounds.Climbing] = function(dt: number, sound: Sound, vel: Vector3)
			sound.Playing = vel.Magnitude > 0.1
		end,

		[sounds.FreeFalling] = function(dt: number, sound: Sound, vel: Vector3): ()
			if vel.Magnitude > 75 then
				sound.Volume = math.clamp(sound.Volume + 0.9 * dt, 0, 1)
			else
				sound.Volume = 0
			end
		end,

		[sounds.Running] = function(dt: number, sound: Sound, vel: Vector3)
			sound.Playing = vel.Magnitude > 0.5 and humanoid.MoveDirection.Magnitude > 0.5
		end,
	}

	-- state substitutions to avoid duplicating entries in the state table
	local stateRemap: { [Enum.HumanoidStateType]: Enum.HumanoidStateType } = {
		[Enum.HumanoidStateType.RunningNoPhysics] = Enum.HumanoidStateType.Running,
	}

	local activeState: Enum.HumanoidStateType = stateRemap[humanoid:GetState()] or humanoid:GetState()

	local function transitionTo(state)
		local transitionFunc: () -> () = stateTransitions[state]

		if transitionFunc then
			transitionFunc()
		end

		activeState = state
	end

	transitionTo(activeState)

	local stateChangedConn = humanoid.StateChanged:Connect(function(_, state)
		state = stateRemap[state] or state

		if state ~= activeState then
			transitionTo(state)
		end
	end)

	local steppedConn = RunService.Stepped:Connect(function(_, worldDt: number)
		-- update looped sounds on stepped
		for sound in pairs(playingLoopedSounds) do
			local updater: (number, Sound, Vector3) -> () = loopedSoundUpdaters[sound]

			if updater then
				updater(worldDt, sound, rootPart.AssemblyLinearVelocity)
			end
		end
	end)

	local function terminate()
		stateChangedConn:Disconnect()
		steppedConn:Disconnect()

		-- Unparent all sounds and empty sounds table
		-- This is needed in order to support the case where initializeSoundSystem might be called more than once for the same player,
		-- which might happen in case player character is unparented and parented back on server and reset-children mechanism is active.
		for name: string, sound: Sound in pairs(sounds) do
			sound:Destroy()
		end
		table.clear(sounds)
	end

	return terminate
end

local binding = AtomicBinding.new({
	humanoid = "Humanoid",
	rootPart = "HumanoidRootPart",
}, initializeSoundSystem)

local playerConnections = {}

local function characterAdded(character)
	binding:bindRoot(character)
end

local function characterRemoving(character)
	binding:unbindRoot(character)
end

local function playerAdded(player: Player)
	local connections = playerConnections[player]
	if not connections then
		connections = {}
		playerConnections[player] = connections
	end

	if player.Character then
		characterAdded(player.Character)
	end
	table.insert(connections, player.CharacterAdded:Connect(characterAdded))
	table.insert(connections, player.CharacterRemoving:Connect(characterRemoving))
end

local function playerRemoving(player: Player)
	local connections = playerConnections[player]
	if connections then
		for _, conn in ipairs(connections) do
			conn:Disconnect()
		end
		playerConnections[player] = nil
	end

	if player.Character then
		characterRemoving(player.Character)
	end
end

for _, player in ipairs(Players:GetPlayers()) do
	task.spawn(playerAdded, player)
end
Players.PlayerAdded:Connect(playerAdded)
Players.PlayerRemoving:Connect(playerRemoving)

return nil


-- Gradually regenerates the Humanoid's Health over time.

local REGEN_RATE = 1 / 100 -- Regenerate this fraction of MaxHealth per second.
local REGEN_STEP = 1 -- Wait this long between each regeneration step.

--------------------------------------------------------------------------------

local Character = script.Parent
local Humanoid = Character:WaitForChild "Humanoid"

--------------------------------------------------------------------------------

while true do
	while Humanoid.Health < Humanoid.MaxHealth do
		local dt = wait(REGEN_STEP)
		local dh = dt * REGEN_RATE * Humanoid.MaxHealth
		Humanoid.Health = math.min(Humanoid.Health + dh, Humanoid.MaxHealth)
	end
	Humanoid.HealthChanged:Wait()
end


--[[
This file is for use by Benchmarker (https://boatbomber.itch.io/benchmarker)

|WARNING| THIS RUNS IN YOUR REAL ENVIRONMENT. |WARNING|
--]]

return {
	ParameterGenerator = function()
		return
	end,

	BeforeAll = function() end,
	AfterAll = function() end,
	BeforeEach = function() end,
	AfterEach = function() end,

	Functions = {
		["A"] = function(Profiler) end,

		["B"] = function(Profiler) end,
	},
}


--[[
This file is for use by Benchmarker (https://boatbomber.itch.io/benchmarker)

|WARNING| THIS RUNS IN YOUR REAL ENVIRONMENT. |WARNING|
--]]

local N = 1000

return {
	ParameterGenerator = function()
		local arr = table.freeze({ "a", "b", "c", "d", "e" })
		local value = arr[math.random(1, #arr)]

		return arr, value
	end,

	BeforeAll = function() end,
	AfterAll = function() end,
	BeforeEach = function() end,
	AfterEach = function() end,

	Functions = {
		["table.find"] = function(Profiler, arr, value)
			for _ = 1, N do
				table.find(arr, value)
			end
		end,

		["manual"] = function(Profiler, arr, value)
			for _ = 1, N do
				for _, v in ipairs(arr) do
					if v == value then
						break
					end
				end
			end
		end,
	},
}


--[[
This file is for use by Benchmarker (https://boatbomber.itch.io/benchmarker)

|WARNING| THIS RUNS IN YOUR REAL ENVIRONMENT. |WARNING|
--]]

local parameters = 10
local N = 100

return {
	ParameterGenerator = function()
		return parameters, unpack(table.create(parameters, "value"), 1, parameters)
	end,

	BeforeAll = function() end,
	AfterAll = function() end,
	BeforeEach = function() end,
	AfterEach = function() end,

	Functions = {
		["select"] = function(Profiler, parameters, ...)
			local function getArg(index, ...)
				return (select(index, ...))
			end

			for param = 1, parameters do
				Profiler.Begin(tostring(param))

				for _ = 1, N do
					getArg(param, ...)
				end

				Profiler.End()
			end
		end,

		["wrap + index"] = function(Profiler, parameters, ...)
			local function getArg(index, ...)
				return ({ ... })[index]
			end

			for param = 1, parameters do
				Profiler.Begin(tostring(param))

				for _ = 1, N do
					getArg(param, ...)
				end

				Profiler.End()
			end
		end,

		["wrap + rawget"] = function(Profiler, parameters, ...)
			local function getArg(index, ...)
				return rawget({ ... }, index)
			end

			for param = 1, parameters do
				Profiler.Begin(tostring(param))

				for _ = 1, N do
					getArg(param, ...)
				end

				Profiler.End()
			end
		end,

		["variables"] = function(Profiler, parameters, ...)
			if parameters ~= 10 then
				error("Parameters ~= 10")
			end

			do
				Profiler.Begin("1")
				local _v = ...
				Profiler.End()
			end

			do
				Profiler.Begin("2")
				local _, _v = ...
				Profiler.End()
			end

			do
				Profiler.Begin("3")
				local _, _, _v = ...
				Profiler.End()
			end

			do
				Profiler.Begin("4")
				local _, _, _, _v = ...
				Profiler.End()
			end

			do
				Profiler.Begin("5")
				local _, _, _, _, _v = ...
				Profiler.End()
			end

			do
				Profiler.Begin("6")
				local _, _, _, _, _, _v = ...
				Profiler.End()
			end

			do
				Profiler.Begin("7")
				local _, _, _, _, _, _, _v = ...
				Profiler.End()
			end

			do
				Profiler.Begin("8")
				local _, _, _, _, _, _, _, _v = ...
				Profiler.End()
			end

			do
				Profiler.Begin("9")
				local _, _, _, _, _, _, _, _, _v = ...
				Profiler.End()
			end

			do
				Profiler.Begin("10")
				local _, _, _, _, _, _, _, _, _, _v = ...
				Profiler.End()
			end
		end,
	},
}


--[[
This file is for use by Benchmarker (https://boatbomber.itch.io/benchmarker)

|WARNING| THIS RUNS IN YOUR REAL ENVIRONMENT. |WARNING|
--]]

local N = 100

return {
	ParameterGenerator = function()
		return function()
			return "value1", "value2", "value3"
		end
	end,

	BeforeAll = function() end,
	AfterAll = function() end,
	BeforeEach = function() end,
	AfterEach = function() end,

	Functions = {
		["variables"] = function(Profiler, func)
			for _ = 1, N do
				local a1, a2, a3 = func()

				local v = 1
				local _ = v

				-- hook

				local _1, _2, _3 = a1, a2, a3
			end
		end,

		["uncached variadic"] = function(Profiler, func)
			for _ = 1, N do
				local v = 1
				local _1, _2, _3 = (function(...)
					local _ = v

					-- hook

					return ...
				end)(func())
			end
		end,

		["cached variadic"] = function(Profiler, func)
			for _ = 1, N do
				local _1, _2, _3 = (function(...)
					-- Not using mutable upvalues outside of this closure allows it to be cached (meaning a new function object doesn't get created each time it's declared like this).
					local v = 1
					local _ = v

					-- hook

					return ...
				end)(func())
			end
		end,
	},
}


--[[
This file is for use by Benchmarker (https://boatbomber.itch.io/benchmarker)

|WARNING| THIS RUNS IN YOUR REAL ENVIRONMENT. |WARNING|
--]]

return {
	ParameterGenerator = function()
		local n = 1000
		local t = table.create(n, "value")
		return function()
			return unpack(t, 1, n)
		end
	end,

	BeforeAll = function() end,
	AfterAll = function() end,
	BeforeEach = function() end,
	AfterEach = function() end,

	Functions = {
		["table.pack + unpack"] = function(Profiler, func)
			Profiler.Begin("pack")
			local packed = table.pack(func())
			Profiler.End()

			-- hook

			Profiler.Begin("unpack")
			unpack(packed, 1, packed.n)
			Profiler.End()
		end,

		["uncached variadic"] = function(Profiler, func)
			Profiler.Begin("pack");
			(function(...)
				Profiler.End()

				-- hook

				Profiler.Begin("unpack")
				return ...
			end)(func())
			Profiler.End()
		end,

		["cached variadic"] = function(Profiler, func)
			Profiler.Begin("pack");
			(function(_Profiler, ...)
				-- Not using mutable upvalues outside of this closure allows it to be cached (meaning a new function object doesn't get created each time it's declared like this).
				_Profiler.End()

				-- hook

				_Profiler.Begin("unpack")
				return ...
			end)(Profiler, func())
			Profiler.End()
		end,
	},
}


--[[
This file is for use by Benchmarker (https://boatbomber.itch.io/benchmarker)

|WARNING| THIS RUNS IN YOUR REAL ENVIRONMENT. |WARNING|
--]]

local N = 100

return {
	ParameterGenerator = function()
		local parameters = math.random(1, 10)
		return unpack(table.create(parameters, "value"), 1, parameters)
	end,

	BeforeAll = function() end,
	AfterAll = function() end,
	BeforeEach = function() end,
	AfterEach = function() end,

	Functions = {
		["reference"] = function(Profiler, ...)
			local arr, length = { ... }, select("#", ...)
			for _ = 1, N do
				for index = 1, length do
					local _ = arr[index]
				end
			end
		end,

		["table.pack"] = function(Profiler, ...)
			for _ = 1, N do
				local arr = table.pack(...)
				local length = arr.n

				for index = 1, length do
					local _ = arr[index]
				end
			end
		end,

		["wrap + select"] = function(Profiler, ...)
			for _ = 1, N do
				local arr, length = { ... }, select("#", ...)

				for index = 1, length do
					local _ = arr[index]
				end
			end
		end,

		["select"] = function(Profiler, ...)
			for _ = 1, N do
				local length = select("#", ...)

				for index = 1, length do
					local _ = (select(index, ...))
				end
			end
		end,
	},
}


--[[
This file is for use by Benchmarker (https://boatbomber.itch.io/benchmarker)

|WARNING| THIS RUNS IN YOUR REAL ENVIRONMENT. |WARNING|
--]]

local N = 1000
local keys = { "A", "B", "C", "D", "E", "F" }

local lookup = {
	["A"] = "a",
	["C"] = "c",
	["E"] = "e",
}

local magic = setmetatable(table.clone(lookup), {
	__index = function(_, key)
		return key
	end,
})

return {
	ParameterGenerator = function()
		return keys[math.random(1, #keys)]
	end,

	BeforeAll = function() end,
	AfterAll = function() end,
	BeforeEach = function() end,
	AfterEach = function() end,

	Functions = {
		["reference"] = function(Profiler, key)
			for _ = 1, N do
				local _ = lookup[key]
			end
		end,

		["lookup + or"] = function(Profiler, key)
			for _ = 1, N do
				local _ = lookup[key] or key
			end
		end,

		["lookup + __index"] = function(Profiler, key)
			for _ = 1, N do
				local _ = magic[key]
			end
		end,
	},
}


--[[
This file is for use by Benchmarker (https://boatbomber.itch.io/benchmarker)

|WARNING| THIS RUNS IN YOUR REAL ENVIRONMENT. |WARNING|
--]]

local N = 100

return {
	ParameterGenerator = function()
		local parameters = math.random(1, 10)
		return unpack(table.create(parameters, 1), 1, parameters)
	end,

	BeforeAll = function() end,
	AfterAll = function() end,
	BeforeEach = function() end,
	AfterEach = function() end,

	Functions = {
		["table.pack"] = function(Profiler, ...)
			for _ = 1, N do
				local arr = table.pack(...)
				local length = arr.n

				for index = 1, length do
					arr[index] = arr[index] + 1
				end

				unpack(arr, 1, length)
			end
		end,

		["wrap + select"] = function(Profiler, ...)
			for _ = 1, N do
				local arr, length = { ... }, select("#", ...)

				for index = 1, length do
					arr[index] = arr[index] + 1
				end

				unpack(arr, 1, length)
			end
		end,

		["select"] = function(Profiler, ...)
			for _ = 1, N do
				local length = select("#", ...)
				local arr = table.create(length)

				for index = 1, length do
					arr[index] = (select(index, ...)) + 1
				end

				unpack(arr, 1, length)
			end
		end,
	},
}


local Network = require("@client/network")
local MessageTypes = require("@shared/output/messageTypes")
local Signal = require("@shared/signal")
local Functions = require("@shared/functions")

local Output = {}
Output.MessageType = MessageTypes
Output.MessageOut = Signal.new()
Output.MessageIdDropped = Signal.new()

function Output:append(messageType, message, dateTime)
	assert(messageType, "No messageType given.")
	assert(MessageTypes[messageType], "Invalid message type.")
	assert(message, "No message given.")

	Output.MessageOut:Fire(nil, dateTime or DateTime.now(), message, messageType)
end

function Output:appendWithId(messageType, message, dateTime, id)
	assert(messageType, "No messageType given.")
	assert(MessageTypes[messageType], "Invalid message type.")
	assert(message, "No message given.")

	id = id or Functions.randomString()
	Output.MessageOut:Fire(id, dateTime or DateTime.now(), message, messageType)

	return id
end

function Output:edit(id, messageType, message, dateTime)
	assert(id, "No message id given.")
	assert(messageType, "No messageType given.")
	assert(MessageTypes[messageType], "Invalid message type.")
	assert(message, "No message given.")

	Output.MessageOut:Fire(id, dateTime or DateTime.now(), message, messageType)
end

function Output:dropId(id)
	assert(id, "No message id given.")

	Output.MessageIdDropped:Fire(id)
end

Network:RegisterEvent("output::append", function(...)
	Output:append(...)
end)

Network:RegisterEvent("output::appendWithId", function(...)
	Output:appendWithId(...)
end)

Network:RegisterEvent("output::edit", function(...)
	Output:edit(...)
end)

Network:RegisterEvent("output::dropId", function(...)
	Output:dropId(...)
end)

return Output


local Output = require("@client/output")
local Network = require("@client/network")
local UI = require("@client/ui")
local Assets = require("@shared/assets")
local WorkerManagers = require("@shared/workerManagers")

local ScriptTypes = require("@shared/scriptManager/scriptTypes")

local Module = {}
Module.ScriptType = ScriptTypes

local hosts = Assets:get("hosts")
local function getHost(name)
	local host = hosts:WaitForChild(name)
	host.Parent = nil

	if host.ClassName ~= "ModuleScript" then
		host.Enabled = true
	end

	return host
end

local scriptHosts = {
	[ScriptTypes.LocalScript] = getHost("localScript"),
	[ScriptTypes.ModuleScript] = getHost("moduleScript"),
	Worker = getHost("worker"),
}

function Module:CreateScript(owner, scriptType, name, source, arguments: (table | BindableFunction)?): BaseScript
	assert(owner, "No owner given.")
	assert(scriptType, "No script type given.")
	assert(ScriptTypes[scriptType], "Invalid script type.")
	assert(name == nil or type(name) == "string", "Invalid name datatype.")
	assert(source, "No source given.")
	assert(
		arguments == nil
			or type(arguments) == "table"
			or (typeof(arguments) == "Instance" and arguments.ClassName == "BindableFunction"),
		"Invalid arguments datatype."
	)

	if not name then
		if scriptType == ScriptTypes.LocalScript then
			name = "LocalScript"
		elseif scriptType == ScriptTypes.ModuleScript then
			name = "ModuleScript"
		else
			name = "Script"
		end
	end

	if type(arguments) == "table" then
		local list = arguments
		arguments = Instance.new("BindableFunction")
		arguments.OnInvoke = function()
			-- Single use
			arguments.OnInvoke = function() end
			arguments:Destroy()

			return unpack(list, 1, list.n or #list)
		end
	end

	local sourceId = WorkerManagers.main:Invoke("CreateSourceId", owner, scriptType, source, arguments)
	local host = scriptHosts[scriptType]:Clone()
	host.Name = name
	host:SetAttribute("__sb_source", sourceId) -- TODO: Protect attribute
	host:AddTag("__sb_script") -- TODO: Protect tag

	return host
end

function Module:StopScripts()
	WorkerManagers:Send("stopScripts", true)
end

function Module:StopLocalScripts()
	WorkerManagers:Send("stopScripts", false)
end

WorkerManagers:onInvoke("fetchSource", function(scriptType, sourceId: string)
	return Network:InvokeServer("fetchSource", scriptType, sourceId)
end)

WorkerManagers:onInvoke("compile", function(source: string)
	return Network:InvokeServer("compile", source)
end)

WorkerManagers:on("makeScriptPrompt", function(bindable: BindableEvent, owner: Player, script: BaseScript)
	UI:MakeScriptPrompt(bindable, owner, script)
end)

WorkerManagers:on("log", function(message: string)
	Output:append(Output.MessageType.Log, message)
end)

WorkerManagers:on("warn", function(message: string)
	Output:append(Output.MessageType.Warning, message)
end)

WorkerManagers:on("error", function(message: string)
	Output:append(Output.MessageType.Error, message)
end)

WorkerManagers:on("info", function(message: string)
	Output:append(Output.MessageType.Information, message)
end)

WorkerManagers:onInvoke(
	"newScript",
	function(owner: Player, parent: Instance, source: string, arguments: (table | BindableFunction)?)
		if type(arguments) == "Instance" and arguments.ClassName == "BindableFunction" then
			arguments = table.pack(arguments:Invoke())
		end

		return Network:InvokeServer("newScript", owner, parent, source, arguments)
	end
)

WorkerManagers:onInvoke(
	"newLocalScript",
	function(owner: Player, source: string, arguments: (table | BindableFunction)?)
		return Module:CreateScript(owner, ScriptTypes.LocalScript, nil, source, arguments)
	end
)

WorkerManagers:onInvoke(
	"newModuleScript",
	function(owner: Player, source: string, arguments: (table | BindableFunction)?)
		return Module:CreateScript(owner, ScriptTypes.ModuleScript, nil, source, arguments)
	end
)

do
	local currentWorker = 1
	WorkerManagers:on("newWorker", function(owner: Player, source: string, name: string, arguments: table?)
		local host = scriptHosts.Worker:Clone()
		host.Name = name

		if _G.workerThreads < 1 then
			WorkerManagers.main:Send("NewWorker", owner, source, host, arguments)
		else
			WorkerManagers[currentWorker]:Send("NewWorker", owner, source, host, arguments)

			-- Round robin is used to split up workers
			currentWorker = currentWorker % _G.workerThreads + 1
		end
	end)
end

return Module


local Log = require("@shared/log")
local Assets = require("@shared/assets")

local scripts = Assets:get("starterScripts"):GetChildren()
local starterScripts = {}

function starterScripts:Init()
	Log.debug("Starting starter scripts...")
	for _, starterScript in ipairs(scripts) do
		task.defer(require, starterScript:Clone())
	end
end

return starterScripts


local Players = game:GetService("Players")
local UserInputService = game:GetService("UserInputService")
local StarterGui = game:GetService("StarterGui")

local Commands = require("@client/commands")
local Network = require("@client/network")
local CommandInputContext = require("@shared/commands/inputContexts")
local Maid = require("@shared/maid")
local MessageType = require("@shared/output/messageTypes")

local create = require("@self/create")

local APP_ASPECT_RATIO = 9 / 4 -- How many times wider it is than taller (width / height)
local APP_MAX_SIZE, APP_MIN_SIZE = 325, 200
local APP_PADDING = 16
local APP_CORNER_SIZE = 8
local APP_COLUMN_WIDTH, APP_ROW_HEIGHT = 0.225, 0.095
local APP_BACKGROUND_COLOR = Color3.new(0, 0, 0)
local APP_CONTENT_COLOR = Color3.new(1, 1, 1)
local APP_TEXT_FONT =
	Font.new("rbxasset://fonts/families/RobotoMono.json", Enum.FontWeight.Medium, Enum.FontStyle.Normal)
local APP_TEXT_PADDING = {
	PaddingBottom = UDim.new(0, 6),
	PaddingLeft = UDim.new(0, 8),
	PaddingRight = UDim.new(0, 8),
	PaddingTop = UDim.new(0, 6),
}

local APP_SCROLL_PADDING = 4
local APP_SCROLL_THICKNESS = 4
local APP_SCROLL_TRANSPARENCY = 0

local OUTPUT_TRANSPARENCY = 0.6
local OUTPUT_TEXT_FONT = APP_TEXT_FONT
local OUTPUT_TEXT_SIZE = 16 -- If the UI is att max size
local OUTPUT_MESSAGE_COLORS = {
	[MessageType.Log] = Color3.fromRGB(204, 204, 204),
	[MessageType.Warning] = Color3.fromRGB(255, 142, 60),
	[MessageType.Error] = Color3.fromRGB(255, 68, 68),
	[MessageType.Success] = Color3.fromRGB(0, 204, 103),
	[MessageType.Information] = Color3.fromRGB(128, 215, 255),
	[MessageType.System] = Color3.fromRGB(209, 93, 255),
}

local SCRIPT_LIST_TEXT_FONT = APP_TEXT_FONT
local SCRIPT_LIST_TEXT_SIZE = 16
local SCRIPT_LIST_TEXT_COLOR = APP_CONTENT_COLOR

local COMMAND_BAR_TRANSPARENCY = 0
local COMMAND_BAR_INPUT_KEY = Enum.KeyCode.Quote

local App = {}
App.OutputOpen = true

local AppMaid = Maid.new()

local root

local commandBarBackground
local commandBarInput
local outputToggleLabel
local commandBar

local output
local outputScale
local outputList

local scriptListScale
local scriptListScroll

local function updateScale()
	local scale = root.AbsoluteSize.Y / APP_MAX_SIZE
	local size = UDim2.fromScale(1 / scale, 1 / scale)

	outputScale.Scale = scale
	outputList.Size = size

	scriptListScale.Scale = scale
	scriptListScroll.Size = size
end

local function setOutputOpen(open)
	App.OutputOpen = open

	commandBarBackground.Size = UDim2.new(1, 0, 1, if open then APP_CORNER_SIZE else 0)
	outputToggleLabel.Text = if open then "Close output" else "Open output"
	output.Visible = open
end

local function processCommandBarInput()
	local input = commandBarInput.Text
	commandBarInput.Text = ""
	Commands:Process(input, CommandInputContext.CommandBar)
end

local outputLabels = {}
local function clearOutput()
	for _, label in ipairs(outputLabels) do
		label:Destroy()
	end
	table.clear(outputLabels)

	outputList.CanvasPosition = Vector2.zero
end

function App.onMessageOut(id, dateTime, message, messageType)
	local formattedDateTime = dateTime:FormatLocalTime("LTS", Players.LocalPlayer.LocaleId)

	-- Determine if we are scrolled all down (with a one pixel safe zone),
	-- if we are then we should automatically scroll to the bottom after adding a new label.
	local wasAtBottom = outputList.AbsoluteCanvasSize.Y - outputList.AbsoluteWindowSize.Y - 1
		<= outputList.CanvasPosition.Y

	local label
	if id and outputLabels[id] then
		label = outputLabels[id]
	else
		label = create("TextLabel")({
			Parent = outputList,
			AutomaticSize = Enum.AutomaticSize.XY,
			BackgroundTransparency = 1,
			FontFace = OUTPUT_TEXT_FONT,
			TextSize = OUTPUT_TEXT_SIZE,
			TextWrapped = true,
			TextXAlignment = Enum.TextXAlignment.Left,
			TextYAlignment = Enum.TextYAlignment.Center,
		})

		table.insert(outputLabels, label)
		if id then
			outputLabels[id] = label
		end
	end

	label.Text = `{formattedDateTime}  {message}`
	label.TextColor3 = OUTPUT_MESSAGE_COLORS[messageType] or OUTPUT_MESSAGE_COLORS[MessageType.Log]

	if wasAtBottom then
		outputList.CanvasPosition =
			Vector2.new(outputList.CanvasPosition.X, outputList.AbsoluteCanvasSize.Y - outputList.AbsoluteWindowSize.Y)
	end
end

function App.onMessageIdDropped(id)
	outputLabels[id] = nil
end

function App.scriptPrompt(bindable: BindableEvent, owner: Player, script: BaseScript)
	local bindableFunction = Instance.new("BindableFunction")
	function bindableFunction.OnInvoke(button)
		if button == "Allow" then
			return bindable:Fire(true)
		elseif button == "Decline" then
			return bindable:Fire(false)
		end

		bindable:Fire()
	end

	StarterGui:SetCore("SendNotification", {
		Title = `{script.ClassName} request`,
		Text = `Allow {owner.DisplayName}'s script to load on your client?\n({script:GetFullName()})`,
		Duration = 15,
		Callback = bindableFunction,
		Button1 = "Allow",
		Button2 = "Decline",
	})

	local thread = task.delay(15 + 1, bindableFunction.Invoke, bindableFunction)
	bindable.Event:Once(function()
		bindableFunction:Destroy()
		task.defer(task.cancel, thread)
	end)
end

local scriptList = {}
local function addScript(name: string)
	local script = scriptList[name]
	if script then
		script:Destroy()
	end

	scriptList[name] = create("TextLabel")({
		Name = name,
		Parent = scriptListScroll,
		AutomaticSize = Enum.AutomaticSize.XY,
		BackgroundTransparency = 1,
		FontFace = SCRIPT_LIST_TEXT_FONT,
		Text = name,
		TextColor3 = SCRIPT_LIST_TEXT_COLOR,
		TextSize = SCRIPT_LIST_TEXT_SIZE,
		TextWrapped = false,
		TextXAlignment = Enum.TextXAlignment.Center,
		TextYAlignment = Enum.TextYAlignment.Center,
	})
end
Network:RegisterEvent("addScript", addScript)

local function removeScript(name: string)
	local script = scriptList[name]
	if script then
		script:Destroy()
	end

	scriptList[name] = nil
end
Network:RegisterEvent("removeScript", removeScript)

task.defer(function()
	for _, name in ipairs(Network:InvokeServer("getScriptList")) do
		addScript(name)
	end
end)

function App.mount(screenGui)
	AppMaid:DoCleaning()

	commandBarBackground = create("Frame")({
		AnchorPoint = Vector2.new(0, 1),
		BackgroundColor3 = APP_BACKGROUND_COLOR,
		BackgroundTransparency = COMMAND_BAR_TRANSPARENCY,
		BorderSizePixel = 0,
		Position = UDim2.fromScale(0, 1),
		ZIndex = 0,

		create("UICorner")({
			CornerRadius = UDim.new(0, APP_CORNER_SIZE),
		}),
	})

	commandBarInput = create("TextBox")({
		BackgroundTransparency = 1,
		ClearTextOnFocus = false,
		Size = UDim2.fromScale(1, 1),
		FontFace = APP_TEXT_FONT,
		PlaceholderColor3 = APP_CONTENT_COLOR:Lerp(APP_BACKGROUND_COLOR, 0.5),
		TextColor3 = APP_CONTENT_COLOR,
		PlaceholderText = `Click here or press {UserInputService:GetStringForKeyCode(COMMAND_BAR_INPUT_KEY)} to run a command`,
		Text = "",
		TextScaled = true,
		TextXAlignment = Enum.TextXAlignment.Left,
		TextYAlignment = Enum.TextYAlignment.Center,

		ReturnPressedFromOnScreenKeyboard = processCommandBarInput,
		FocusLost = function(enterPressed, inputObject)
			if not enterPressed then
				return
			end

			-- If enterPressed is true but no inputObject exists then the input was forced via :ReleaseFocus(true).
			if not inputObject then
				return
			end

			processCommandBarInput()
		end,
	})

	AppMaid:GiveTask(UserInputService.InputBegan:Connect(function(input)
		if UserInputService:GetFocusedTextBox() ~= nil or input.UserInputType ~= Enum.UserInputType.Keyboard then
			return
		end

		if input.KeyCode == COMMAND_BAR_INPUT_KEY then
			task.wait() -- Yield to avoid typing into the commandbar while capturing focus.

			commandBarInput:CaptureFocus()
		end
	end))

	outputToggleLabel = create("TextLabel")({
		BackgroundTransparency = 1,
		Size = UDim2.fromScale(1, 1),
		FontFace = APP_TEXT_FONT,
		TextColor3 = APP_CONTENT_COLOR,
		TextScaled = true,
	})

	commandBar = create("Frame")({
		AnchorPoint = Vector2.new(0, 1),
		BackgroundTransparency = 1,
		BorderSizePixel = 0,
		Position = UDim2.fromScale(0, 1),
		Size = UDim2.fromScale(1, APP_ROW_HEIGHT),
		ZIndex = 2,
		ClipsDescendants = true,

		commandBarBackground,

		create("TextButton")({
			BackgroundTransparency = 1,
			Size = UDim2.fromScale(APP_COLUMN_WIDTH, 1),
			Text = "",

			MouseButton1Click = function()
				setOutputOpen(not App.OutputOpen)
			end,

			create("UIPadding")({ APP_TEXT_PADDING }),

			outputToggleLabel,
		}),

		create("Frame")({
			BackgroundColor3 = Color3.new(1, 1, 1),
			BackgroundTransparency = 0.5 * (1 + COMMAND_BAR_TRANSPARENCY),
			BorderSizePixel = 0,
			Position = UDim2.fromScale(APP_COLUMN_WIDTH, 0),
			Size = UDim2.new(0, 1, 1, 0),
			ZIndex = 2,
		}),

		create("Frame")({
			AnchorPoint = Vector2.new(1, 0),
			BackgroundTransparency = 1,
			Position = UDim2.fromScale(1, 0),
			Size = UDim2.fromScale(1 - APP_COLUMN_WIDTH, 1),

			create("UIPadding")({ APP_TEXT_PADDING }),

			commandBarInput,
		}),
	})

	outputScale = create("UIScale")({})
	outputList = create("ScrollingFrame")({
		BackgroundTransparency = 1,
		BorderSizePixel = 0,
		AutomaticCanvasSize = Enum.AutomaticSize.XY,
		CanvasSize = UDim2.new(),
		ScrollBarImageColor3 = APP_CONTENT_COLOR,
		ScrollBarImageTransparency = APP_SCROLL_TRANSPARENCY,
		ScrollBarThickness = APP_SCROLL_THICKNESS,

		outputScale,

		create("UIPadding")({
			PaddingBottom = APP_TEXT_PADDING.PaddingBottom - UDim.new(0, APP_SCROLL_PADDING),
			PaddingLeft = APP_TEXT_PADDING.PaddingLeft - UDim.new(0, APP_SCROLL_PADDING),
			PaddingRight = APP_TEXT_PADDING.PaddingRight - UDim.new(0, APP_SCROLL_PADDING),
			PaddingTop = APP_TEXT_PADDING.PaddingTop - UDim.new(0, APP_SCROLL_PADDING),
		}),

		create("UIListLayout")({}),
	})

	scriptListScale = create("UIScale")({})
	scriptListScroll = create("ScrollingFrame")({
		BackgroundTransparency = 1,
		BorderSizePixel = 0,
		AutomaticCanvasSize = Enum.AutomaticSize.XY,
		CanvasSize = UDim2.new(),
		ScrollBarImageColor3 = APP_CONTENT_COLOR,
		ScrollBarImageTransparency = APP_SCROLL_TRANSPARENCY,
		ScrollBarThickness = APP_SCROLL_THICKNESS,

		scriptListScale,

		create("UIPadding")({
			PaddingBottom = APP_TEXT_PADDING.PaddingBottom - UDim.new(0, APP_SCROLL_PADDING),
			PaddingLeft = APP_TEXT_PADDING.PaddingLeft - UDim.new(0, APP_SCROLL_PADDING),
			PaddingRight = APP_TEXT_PADDING.PaddingRight - UDim.new(0, APP_SCROLL_PADDING),
			PaddingTop = APP_TEXT_PADDING.PaddingTop - UDim.new(0, APP_SCROLL_PADDING),
		}),

		create("UIListLayout")({
			SortOrder = Enum.SortOrder.Name,
		}),
	})

	output = create("Frame")({
		BackgroundTransparency = 1,
		Size = UDim2.fromScale(1, 1 - APP_ROW_HEIGHT),
		ClipsDescendants = true,

		create("Frame")({
			BackgroundColor3 = APP_BACKGROUND_COLOR,
			BackgroundTransparency = OUTPUT_TRANSPARENCY,
			BorderSizePixel = 0,
			Size = UDim2.fromScale(1, 1 / (1 - APP_ROW_HEIGHT)),

			create("UICorner")({
				CornerRadius = UDim.new(0, APP_CORNER_SIZE),
			}),

			create("Frame")({
				BackgroundTransparency = 1,
				Size = UDim2.fromScale(APP_COLUMN_WIDTH, APP_ROW_HEIGHT),

				create("UIPadding")({ APP_TEXT_PADDING }),

				create("TextLabel")({
					BackgroundTransparency = 1,
					Size = UDim2.fromScale(1, 1),
					FontFace = APP_TEXT_FONT,
					TextColor3 = APP_CONTENT_COLOR,
					Text = "Scripts",
					TextScaled = true,
				}),
			}),

			create("Frame")({
				BackgroundColor3 = Color3.new(1, 1, 1),
				BackgroundTransparency = 0.5 * (1 + OUTPUT_TRANSPARENCY),
				BorderSizePixel = 0,
				Position = UDim2.fromScale(0, APP_ROW_HEIGHT),
				Size = UDim2.new(1, 0, 0, 1),
				ZIndex = 2,
			}),

			create("Frame")({
				AnchorPoint = Vector2.new(1, 0),
				BackgroundTransparency = 1,
				Position = UDim2.fromScale(1, 0),
				Size = UDim2.fromScale(1 - APP_COLUMN_WIDTH, APP_ROW_HEIGHT),

				create("UIPadding")({ APP_TEXT_PADDING }),

				create("TextLabel")({
					BackgroundTransparency = 1,
					Size = UDim2.fromScale(1, 1),
					FontFace = APP_TEXT_FONT,
					TextColor3 = APP_CONTENT_COLOR,
					Text = "Output",
					TextScaled = true,
				}),
			}),

			create("TextButton")({
				AnchorPoint = Vector2.new(1, 0),
				AutomaticSize = Enum.AutomaticSize.X,
				BackgroundTransparency = 1,
				Position = UDim2.fromScale(1, 0),
				Size = UDim2.fromScale(0, APP_ROW_HEIGHT),
				Text = "",

				MouseButton1Click = clearOutput,

				create("UIPadding")({ APP_TEXT_PADDING }),

				create("TextLabel")({
					AutomaticSize = Enum.AutomaticSize.X,
					BackgroundTransparency = 1,
					Size = UDim2.fromScale(0, 1),
					FontFace = APP_TEXT_FONT,
					Text = "Clear",
					TextColor3 = APP_CONTENT_COLOR,
					TextScaled = true,
				}),
			}),

			create("Frame")({
				BackgroundColor3 = Color3.new(1, 1, 1),
				BackgroundTransparency = 0.5 * (1 + OUTPUT_TRANSPARENCY),
				BorderSizePixel = 0,
				Position = UDim2.fromScale(APP_COLUMN_WIDTH, 0),
				Size = UDim2.new(0, 1, 1, 0),
				ZIndex = 2,
			}),

			create("Frame")({
				BackgroundTransparency = 1,
				Position = UDim2.fromScale(0, APP_ROW_HEIGHT),
				Size = UDim2.fromScale(APP_COLUMN_WIDTH, 1 - APP_ROW_HEIGHT * 2),

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

				scriptListScroll,
			}),

			create("Frame")({
				AnchorPoint = Vector2.new(1, 0),
				BackgroundTransparency = 1,
				Position = UDim2.fromScale(1, APP_ROW_HEIGHT),
				Size = UDim2.fromScale(1 - APP_COLUMN_WIDTH, 1 - APP_ROW_HEIGHT * 2),

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

				outputList,
			}),
		}),
	})

	create("UIPadding")({
		Parent = screenGui,
		PaddingBottom = UDim.new(0, APP_PADDING),
		PaddingLeft = UDim.new(0, APP_PADDING),
		PaddingRight = UDim.new(0, APP_PADDING),
		PaddingTop = UDim.new(0, APP_PADDING),
	})

	root = create("Frame")({
		Parent = screenGui,
		AnchorPoint = Vector2.new(0, 1),
		BackgroundTransparency = 1,
		Position = UDim2.fromScale(0, 1),
		Size = UDim2.fromScale(0.5, 1),

		create("UIAspectRatioConstraint")({
			AspectRatio = APP_ASPECT_RATIO,
		}),

		create("UISizeConstraint")({
			MaxSize = Vector2.new(APP_MAX_SIZE * APP_ASPECT_RATIO, APP_MAX_SIZE),
			MinSize = Vector2.new(APP_MIN_SIZE * APP_ASPECT_RATIO, APP_MIN_SIZE),
		}),

		commandBar,
		output,
	})

	updateScale()
	AppMaid:GiveTask(root:GetPropertyChangedSignal("AbsoluteSize"):Connect(updateScale))

	clearOutput()
	setOutputOpen(App.OutputOpen)

	-- Recreate scriptList
	for name, _ in scriptList do
		addScript(name)
	end
end

return App


local function applyProperties(instance, properties)
	for key, value in properties do
		if type(key) == "number" and typeof(value) == "Instance" then
			value.Parent = instance

			continue
		end

		if type(value) == "table" then
			applyProperties(instance, value)

			continue
		end

		if typeof(instance[key]) == "RBXScriptSignal" then
			instance[key]:Connect(value)

			continue
		end

		instance[key] = value
	end
end

return function(class)
	return function(properties)
		-- We should set Parent last.
		local parent = properties.Parent
		properties.Parent = nil

		local instance = Instance.new(class)
		applyProperties(instance, properties)
		instance.Parent = parent

		return instance
	end
end


local Players = game:GetService("Players")

local Output = require("@client/output")
local Log = require("@shared/log")
local Maid = require("@shared/maid")
local Signal = require("@shared/signal")
local Functions = require("@shared/functions")

local App = require("@self/app")
local create = require("@self/create")

local UI = {}
UI.PlayerGui = nil
UI.ScreenGui = nil
UI.IsLoaded = false
UI.Loaded = Signal.new()

local UIMaid = Maid.new()
function UI:Create()
	Log.debug("Creating UI...")
	UIMaid:DoCleaning()

	local screenGui = create("ScreenGui")({
		Name = Functions.randomInstanceName(),
		Archivable = false,
		ScreenInsets = Enum.ScreenInsets.CoreUISafeInsets,
		DisplayOrder = 2147483647,
		ResetOnSpawn = false,
		ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
	})

	UIMaid.screenGui = screenGui
	UIMaid.screenGuiChanged = screenGui.Changed:Connect(function(property)
		if property == "AbsoluteSize" or property == "AbsolutePosition" then
			return
		end

		if property == "Parent" and screenGui.Parent == UI.PlayerGui then
			return
		end

		UI:Create()
	end)

	UI.ScreenGui = screenGui

	App.mount(screenGui)
	screenGui.Parent = UI.PlayerGui
end

function UI:WaitUntilLoaded()
	if not UI.IsLoaded then
		UI.Loaded:Wait()
	end
end

function UI:MakeScriptPrompt(bindable: BindableEvent, owner: Player, script: BaseScript)
	App.scriptPrompt(bindable, owner, script)
end

function UI:IsScriptBuilderUI(screenGui: ScreenGui?)
	if not screenGui then
		return false
	end

	return screenGui == UI.ScreenGui
end

function UI:Init()
	Log.debug("Initializing UI...")

	-- Remove loading screen.
	game:GetService("ReplicatedFirst"):RemoveDefaultLoadingScreen()

	-- Wait for LocalPlayer and PlayerGui, then create UI.
	task.defer(function()
		local localPlayer = Players.LocalPlayer or Players:GetPropertyChangedSignal("LocalPlayer"):Wait()

		UI.PlayerGui = localPlayer:FindFirstChildOfClass("PlayerGui")
		while not UI.PlayerGui do
			localPlayer.ChildAdded:Wait()
			UI.PlayerGui = localPlayer:FindFirstChildOfClass("PlayerGui")
		end

		UI:Create()
		Output.MessageOut:Connect(App.onMessageOut)
		Output.MessageIdDropped:Connect(App.onMessageIdDropped)

		UI.IsLoaded = true
		UI.Loaded:Fire()
	end)
end

return UI


local ipairs = ipairs
local table = table

local lookup = {}
local list = table.freeze(require("@self/list.yaml"))
for _, class in ipairs(list) do
	lookup[class] = true
end

return table.freeze(lookup)


local table = table
local ipairs = ipairs

local SharedStackTrace = require("@shared/stackTrace")
local Functions = require("@shared/functions")
local VM = require("@client/wm/sandbox/vm")

local vmStart = VM.vmStart
local vmEnd = VM.vmEnd
local getDebugInfo = VM.getDebugInfo
local getRunning = VM.getRunning

local StackTrace = setmetatable({}, { __index = SharedStackTrace })

export type StackTrace = SharedStackTrace.StackTrace

-- Get the source of the worker manager (to work with stack traces)
local wmSource = debug.info(Functions.empty, "s")

function StackTrace.filter(stackTrace: StackTrace, running: VM.RunningInfo?): StackTrace
	local newStackTrace = {}

	local currentStack: VM.RunningInfo? = running or getRunning()
	for _, data in ipairs(stackTrace) do
		-- Check if the stack is external
		if data.source ~= wmSource then
			table.insert(newStackTrace, data)

			continue
		end

		if not currentStack then
			continue
		end

		local line = tonumber(data.line)
		if line < vmStart or line > vmEnd then
			-- If the line is before vmStart or after vmEnd, it's not a user owned stack
			continue
		end

		local vmSource, vmLine, vmName = getDebugInfo(currentStack)
		table.insert(newStackTrace, table.freeze({ source = vmSource, line = vmLine, name = vmName }))

		currentStack = currentStack.stackParent
	end

	return table.freeze(newStackTrace)
end

return StackTrace


-- This doesn't persist between servers and is just an example.
local datas = {}
local function getUserData(userId: number)
	local data = datas[userId]
	if data then
		return data
	end

	data = {
		scripts = {},
	}
	datas[userId] = data

	return data
end

local PlayerList = require("@shared/playerList")
PlayerList.Removed:Connect(function(player)
	datas[player.UserId] = nil
end)

type sourceType = "source" | "http"

local DB = {}

function DB:GetScripts(userId: number): { string }
	local scripts = {}
	for name in getUserData(userId).scripts do
		table.insert(scripts, name)
	end

	return scripts
end

function DB:GetScript(userId: number, scriptName: string): { sourceType: sourceType, source: string }?
	local script = getUserData(userId).scripts[scriptName]
	if not script then
		return
	end

	return {
		sourceType = script[1],
		source = script[2],
	}
end

function DB:SaveScript(userId: number, scriptName: string, sourceType: sourceType, source: string)
	getUserData(userId).scripts[scriptName] = { sourceType, source }
end

function DB:RemoveScript(userId: number, scriptName: string)
	getUserData(userId).scripts[scriptName] = nil
end

return table.freeze(DB)


--!strict

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

local services = require("@shared/network/services")
local RequestMethods = require("@shared/network/requestMethods")
local ServerRequestMethod = RequestMethods.Server
local ClientRequestMethod = RequestMethods.Client

local Log = require("@shared/log")
local Functions = require("@shared/functions")
local MLDSA87 = require("@shared/crypto/MlDSA").ML_DSA_87
local MLKEM = require("@shared/crypto/MlKEM").MLKEM_1024
local CSPRNG = require("@shared/crypto/CSPRNG")
local Base64 = require("@shared/crypto/base64")
local Protection = require("@shared/protection")
local PlayerList = require("@shared/playerList")

local Utils = require("@shared/network/utils")

-- add extra entropy
CSPRNG.AddEntropyProvider(Utils.robloxEntropyProvider)

local dsaServerPrivateKey: buffer = Base64.Decode(buffer.fromstring(_G.dsaServerPrivateKey))
-- local dsaServerPublicKey: buffer = Base64.Decode(buffer.fromstring(_G.dsaServerPublicKey))

local kemServerPrivateKey: buffer = Base64.Decode(buffer.fromstring(_G.kemServerPrivateKey))
-- local kemServerPublicKey: buffer = Base64.Decode(buffer.fromstring(_G.kemServerPublicKey))

local clientNetworkData: {
	[Player]: {
		kemPublicKey: buffer,

		staticKey: buffer,

		nonce: number, -- Last server used nonce, next nonce to use should be this + 1
		lastNonce: number, -- Last nonce client sent, new nonces should always be greater than this one
	},
} =
	{}

local connectedPlayers: { [Player]: boolean } = {} -- We keep track of players that have successfully connected to the remote, so we don't accidentally send stuff to them without them actually listening to the remote
local exchanged: { [Player]: boolean } = {} -- We lock keys after the first handshake
PlayerList.Removed:Connect(function(player: Player)
	clientNetworkData[player] = nil
	connectedPlayers[player] = nil
	exchanged[player] = nil
end)

local Network = {}
Network.attributeName, Network.attributeValue = CSPRNG.Random(), CSPRNG.Random()

local registeredEvents: { [string]: <Args...>(Args...) -> () } = {}
function Network:RegisterEvent<Args...>(name: string, callback: (Args...) -> ())
	if registeredEvents[name] then
		Log.warn(`Network event "{name}" was overwritten.`)
	end

	registeredEvents[name] = callback :: any
end

local registeredFunctions: { [string]: <Args..., Ret...>(Args...) -> Ret... } = {}
function Network:RegisterFunction<Args..., Ret...>(name: string, callback: (Args...) -> Ret...)
	if registeredFunctions[name] then
		Log.warn(`Network function "{name}" was overwritten.`)
	end

	registeredFunctions[name] = callback :: any
end

local function encryptAnythingForPlayer(player: Player, data)
	local networkData = clientNetworkData[player]
	assert(networkData, `Failed to get network data for {player}`)

	return Utils.encryptAnything(networkData, data)
end

local function decryptFromPlayerAndVarargs(player: Player, ciphertext, nonce, tag, instanceArray)
	local networkData = clientNetworkData[player]
	assert(networkData, `Failed to get network data for {player}`)

	return Utils.decryptAnythingAndVerify(networkData, ciphertext, nonce, tag, instanceArray)
end

local remote: RemoteFunction, remoteParent: Instance

local function onRequest(player: Player, requestType, ...): ...any
	-- Key exchange and handshake requests should be the only times we return something from remote functions, the only reason we are using them is to prevent logging
	-- When returning something from remote functions we are risking it never getting sent back to the client if the remote function were to get destroyed while the callback is executing
	-- In that case we just allow the client to resend the request (it's also why we are not locking the key here)

	-- We allow them for the handshake because if the remote gets destroyed the client would have to make another handshake to the new remote anyways, and because if we did a client invoke it could get intercepted

	if requestType == ServerRequestMethod.KeyExchange then
		if exchanged[player] then
			local outputSignature = buffer.create(MLDSA87.SigByteLen)
			local messageToSign = buffer.fromstring("alreadyExchanged@" .. os.time()) -- this can be static because of randomness, but still
			local randomness = CSPRNG.RandomBytes(MLDSA87.SigningSeedByteLen)

			local signSuccess = MLDSA87.Sign(
				randomness,
				dsaServerPrivateKey,
				messageToSign,
				Utils.getDsaContext(player),
				outputSignature
			)

			if not signSuccess then
				-- explicitly prevent changing keys & allow client to prevent potential mitm as fast as possible
				return
			end

			return outputSignature, messageToSign
		end

		local kemPublicKey, kemCiphertext = ...

		if type(kemPublicKey) ~= "buffer" or buffer.len(kemPublicKey) ~= Utils.ML_KEM_1024_PUBLIC_KEY_SIZE then
			return
		end

		if type(kemCiphertext) ~= "buffer" or buffer.len(kemCiphertext) ~= Utils.ML_KEM_1024_CIPHERTEXT_SIZE then
			return
		end

		local sharedSecret = MLKEM.Decapsulate(kemServerPrivateKey, kemCiphertext)

		if clientNetworkData[player] == nil then
			clientNetworkData[player] = {
				kemPublicKey = kemPublicKey,
				staticKey = sharedSecret,
				nonce = -1,
				lastNonce = -1,
			}
		end

		local outputSignature = buffer.create(MLDSA87.SigByteLen)
		local messageToSign = buffer.fromstring("exchanged@" .. os.time()) -- this can be static because of randomness, but still
		local randomness = CSPRNG.RandomBytes(MLDSA87.SigningSeedByteLen)

		local signSuccess =
			MLDSA87.Sign(randomness, dsaServerPrivateKey, messageToSign, Utils.getDsaContext(player), outputSignature)

		if not signSuccess then
			return
		end

		exchanged[player] = true

		return outputSignature, messageToSign
	elseif requestType == ServerRequestMethod.Handshake then
		-- if the player has not exchanged keys, or has already sent a handshake, return
		if not exchanged[player] or connectedPlayers[player] then
			return
		end

		local check = decryptFromPlayerAndVarargs(player, ...)
		if type(check) ~= "string" then
			return
		end

		connectedPlayers[player] = true -- We will assume that the handshake succeeds for the client aswell

		return encryptAnythingForPlayer(player, check)
	elseif requestType == ServerRequestMethod.Event then
		-- Minimize the yielding time for the client if the server is calculating stuff
		task.spawn(function(...)
			local decryptedData: {
				id: string,
				eventName: string,
				arguments: { any },
			}? =
				decryptFromPlayerAndVarargs(player, ...)
			if
				type(decryptedData) ~= "table"
				or type(decryptedData.eventName) ~= "string"
				or type(decryptedData.arguments) ~= "table"
			then
				return
			end

			local callback = registeredEvents[decryptedData.eventName]
			if not callback then
				return
			end

			callback(player, unpack(decryptedData.arguments))
		end, ...)
	elseif requestType == ServerRequestMethod.Invoke then
		-- Minimize the yielding time for the client if the server is calculating stuff
		task.spawn(function(...)
			local decryptedData: {
				id: string,
				functionName: string,
				arguments: { any },
			}? =
				decryptFromPlayerAndVarargs(player, ...)

			if
				type(decryptedData) ~= "table"
				or type(decryptedData.id) ~= "string"
				or type(decryptedData.functionName) ~= "string"
				or type(decryptedData.arguments) ~= "table"
			then
				return
			end

			local callback = registeredFunctions[decryptedData.functionName]
			if not callback then
				return
			end

			(function(...)
				while not connectedPlayers[player] or not remote or remote.Parent ~= remoteParent do
					task.wait()
				end

				remote:InvokeClient(
					player,
					ClientRequestMethod.InvokeResult,
					encryptAnythingForPlayer(player, { id = decryptedData.id, arguments = { ... } })
				)
			end)(callback(player, unpack(decryptedData.arguments)))
		end, ...)
	end

	return
end

function Network:FireClient(player: Player, eventName: string, ...: any)
	task.spawn(function(...)
		while not connectedPlayers[player] or not remote or remote.Parent ~= remoteParent do
			task.wait()
		end

		remote:InvokeClient(
			player,
			ClientRequestMethod.Event,
			encryptAnythingForPlayer(player, {
				eventName = eventName,
				arguments = { ... },
			})
		)
	end, ...)
end

function Network:FireAllClients(eventName: string, ...: any)
	for _, player: Player in ipairs(Players:GetPlayers()) do
		Network:FireClient(player, eventName, ...)
	end
end

function Network:Init()
	local lastRemoteFix = os.clock() -- We keep track of this so we can recreate the remote every 5 seconds, incase the remote get deleted on someones client
	local function fixRemote()
		lastRemoteFix = os.clock()

		if remote then
			task.delay(0, function(oldRemote)
				Protection.remove(oldRemote)
				oldRemote:Destroy()
			end, remote)
		end

		table.clear(connectedPlayers)

		remote = Instance.new("RemoteFunction")
		remote.Name = Functions.randomInstanceName()
		remote.Archivable = false
		remote:SetAttribute(Network.attributeName, Network.attributeValue)

		Protection.add(remote, "write")

		remoteParent = if #services > 1 then services[CSPRNG.RandomInt(1, #services)] else services[1]
		remote.Parent = remoteParent
	end

	fixRemote()
	remote.OnServerInvoke = onRequest

	-- Already leaked magic 🤯 (I know better methods but I don't want to leak them ;) - EwDev)
	local args: any = table.create(80, task.defer)
	table.insert(args, function()
		if
			not remote
			or remote.Parent ~= remoteParent
			or remote:GetAttribute(Network.attributeName) ~= Network.attributeValue
			or #remote:GetChildren() > 2
			or os.clock() - lastRemoteFix >= 5
		then
			fixRemote()
		end

		remote.OnServerInvoke = onRequest
	end)

	RunService.PostSimulation:Connect(function()
		pcall(unpack(args))
	end)
end

return Network


local Players = game:GetService("Players")

local Network = require("@server/network")
local MessageTypes = require("@shared/output/messageTypes")
local Functions = require("@shared/functions")

local Output = {}
Output.MessageType = MessageTypes

function Output:appendTo(player, messageType, message, dateTime)
	assert(messageType, "No messageType given.")
	assert(MessageTypes[messageType], "Invalid message type.")
	assert(message, "No message given.")

	Network:FireClient(player, "output::append", messageType, message, dateTime)
end

function Output:appendToAll(messageType, message, dateTime)
	assert(messageType, "No messageType given.")
	assert(MessageTypes[messageType], "Invalid message type.")
	assert(message, "No message given.")

	Network:FireAllClients("output::append", messageType, message, dateTime)
end

function Output:appendToOthers(excludedPlayer, messageType, message, dateTime)
	assert(messageType, "No messageType given.")
	assert(MessageTypes[messageType], "Invalid message type.")
	assert(message, "No message given.")

	local players = Players:GetPlayers()
	local index = table.find(players, excludedPlayer)
	if index then
		table.remove(players, index)
	end

	for _, player in players do
		Network:FireClient(player, "output::append", messageType, message, dateTime)
	end
end

function Output:appendWithIdTo(player, messageType, message, dateTime)
	assert(messageType, "No messageType given.")
	assert(MessageTypes[messageType], "Invalid message type.")
	assert(message, "No message given.")

	local id = Functions.randomString()
	Network:FireClient(player, "output::appendWithId", messageType, message, dateTime, id)

	return id
end

function Output:editFrom(player, id, messageType, message, dateTime)
	assert(messageType, "No messageType given.")
	assert(MessageTypes[messageType], "Invalid message type.")
	assert(message, "No message given.")

	Network:FireClient(player, "output::edit", id, messageType, message, dateTime)
end

function Output:dropIdFrom(player, id)
	Network:FireClient(player, "output::dropId", id)
end

return Output