--Main menu for Nexus VR Character Model.
--!strict

local MENU_OPEN_TIME_REQUIREMENT = 1
local MENU_OPEN_TIME = 0.25

local Workspace = game:GetService("Workspace")
local Players = game:GetService("Players")
local GuiService = game:GetService("GuiService")
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")

local NexusVRCharacterModel = script.Parent.Parent
local NexusButton = require(NexusVRCharacterModel:WaitForChild("Packages"):WaitForChild("NexusButton"))
local NexusVRCore = require(NexusVRCharacterModel:WaitForChild("Packages"):WaitForChild("NexusVRCore"))
local Settings = require(NexusVRCharacterModel:WaitForChild("State"):WaitForChild("Settings")).GetInstance()
local VRInputService = require(NexusVRCharacterModel:WaitForChild("State"):WaitForChild("VRInputService")).GetInstance()
local ApiBaseView = require(NexusVRCharacterModel:WaitForChild("UI"):WaitForChild("View"):WaitForChild("ApiBaseView"))
local EnigmaView = require(NexusVRCharacterModel:WaitForChild("UI"):WaitForChild("View"):WaitForChild("EnigmaView"))
local SettingsView = require(NexusVRCharacterModel:WaitForChild("UI"):WaitForChild("View"):WaitForChild("SettingsView"))

local TextButtonFactory = NexusButton.TextButtonFactory.CreateDefault(Color3.fromRGB(0, 170, 255))
TextButtonFactory:SetDefault("Theme", "RoundedCorners")
local ScreenGui3D = NexusVRCore.ScreenGui3D

local MainMenu = {}
MainMenu.__index = MainMenu
local StaticInstance = nil

export type MainMenu = {
    CurrentView: number,
    Views: {any},
    ScreenGui: NexusVRCore.NexusInstanceScreenGui3D,
    ViewAdornFrame: Frame,
    LeftButton: NexusButton.NexusInstanceNexusButton,
    RightButton: NexusButton.NexusInstanceNexusButton,
    ViewTextLabel: TextLabel,
    LeftHandHintVisible: boolean,
    RightHandHintVisible: boolean,
} & typeof(setmetatable({}, MainMenu))

--[[
Creates the main menu.
--]]
function MainMenu.new(): any
    local self = setmetatable({}, MainMenu) :: MainMenu

    --Set up the ScreenGui.
    local MainMenuScreenGui = ScreenGui3D.new()
    MainMenuScreenGui.ResetOnSpawn = false
    MainMenuScreenGui.Enabled = false
    MainMenuScreenGui.CanvasSize = Vector2.new(500, 605)
    MainMenuScreenGui.FieldOfView = 0
    MainMenuScreenGui.Easing = 0.25
    self.ScreenGui = MainMenuScreenGui

    --Create the parent frame, display text, and toggle buttons.
    local ViewAdornFrame = Instance.new("Frame")
    ViewAdornFrame.BackgroundTransparency = 1
    ViewAdornFrame.Size = UDim2.new(0, 500, 0, 500)
    ViewAdornFrame.Parent = MainMenuScreenGui:GetContainer()
    self.ViewAdornFrame = ViewAdornFrame

    local ButtonAdornFrame = Instance.new("Frame")
    ButtonAdornFrame.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
    ButtonAdornFrame.BackgroundTransparency = 0.6 * GuiService.PreferredTransparency
    ButtonAdornFrame.Position = UDim2.new(0, 0, 0, 505)
    ButtonAdornFrame.Size = UDim2.new(1, 0, 0, 100)
    ButtonAdornFrame.Parent = ViewAdornFrame
    GuiService:GetPropertyChangedSignal("PreferredTransparency"):Connect(function()
        ButtonAdornFrame.BackgroundTransparency = 0.6 * GuiService.PreferredTransparency
    end)

    local UICorner = Instance.new("UICorner")
    UICorner.CornerRadius = UDim.new(5 * 0.05, 0)
    UICorner.Parent = ButtonAdornFrame

    local LeftButton, LeftText = TextButtonFactory:Create()
    LeftButton.BorderSize = UDim.new(0.075, 0)
    LeftButton.Size = UDim2.new(0, 80, 0, 80)
    LeftButton.Position = UDim2.new(0, 10, 0, 10)
    LeftButton.Parent = ButtonAdornFrame
    LeftText.Text = "<"
    self.LeftButton = LeftButton

    local RightButton, RightText = TextButtonFactory:Create()
    RightButton.BorderSize = UDim.new(0.075, 0)
    RightButton.Size = UDim2.new(0, 80, 0, 80)
    RightButton.Position = UDim2.new(0, 410, 0, 10)
    RightButton.Parent = ButtonAdornFrame
    RightText.Text = ">"
    self.RightButton = RightButton

    local ViewTextLabel = Instance.new("TextLabel")
    ViewTextLabel.BackgroundTransparency = 1
    ViewTextLabel.Size = UDim2.new(0, 300, 0, 60)
    ViewTextLabel.Position = UDim2.new(0, 100, 0, 20)
    ViewTextLabel.Font = Enum.Font.SourceSansBold
    ViewTextLabel.TextScaled = true
    ViewTextLabel.TextColor3 = Color3.fromRGB(255, 255, 255)
    ViewTextLabel.TextStrokeColor3 = Color3.fromRGB(0, 0, 0)
    ViewTextLabel.TextStrokeTransparency = 0
    ViewTextLabel.Parent = ButtonAdornFrame
    self.ViewTextLabel = ViewTextLabel

    --Set up the default views.
    self.CurrentView = 1
    self.Views = {}
    SettingsView.new(self:CreateView("Settings"));
    EnigmaView.new(self:CreateView("Enigma"), self);
    self:UpdateVisibleView()

    --Connect changing views.
    local DB = true
    LeftButton.MouseButton1Down:Connect(function()
        if not DB then return end
        DB = false

        --De-increment the current view.
        self.CurrentView = self.CurrentView - 1
        if self.CurrentView == 0 then
            self.CurrentView = #self.Views
        end

        --Update the views.
        self:UpdateVisibleView()
        task.wait()
        DB = true
    end)
    RightButton.MouseButton1Down:Connect(function()
        if not DB then return end
        DB = false

        --Increment the current view.
        self.CurrentView = self.CurrentView + 1
        if self.CurrentView > #self.Views then
            self.CurrentView = 1
        end

        --Update the views.
        self:UpdateVisibleView()
        task.wait()
        DB = true
    end)

    --Parent the menu.
    MainMenuScreenGui.Parent = Players.LocalPlayer:WaitForChild("PlayerGui")
    return self
end

--[[
Returns a singleton instance of the character service.
--]]
function MainMenu.GetInstance(): MainMenu
    if not StaticInstance then
        StaticInstance = MainMenu.new()
    end
    return StaticInstance
end

--[[
Sets up opening based on the controllers
being rotated upwards.
--]]
function MainMenu.SetUpOpening(self: MainMenu): ()
    --Create the animation parts.
    local InitialMenuToggleGestureActive = Settings:GetSetting("Menu.MenuToggleGestureActive")
    if InitialMenuToggleGestureActive == nil then
        InitialMenuToggleGestureActive = true
    end

    local LeftAdornPart = Instance.new("Part")
    LeftAdornPart.Transparency = 1
    LeftAdornPart.Size = Vector3.new()
    LeftAdornPart.Anchored = true
    LeftAdornPart.CanCollide = false
    LeftAdornPart.CanQuery = false
    LeftAdornPart.Parent = Workspace.CurrentCamera

    local LeftAdorn = Instance.new("BoxHandleAdornment")
    LeftAdorn.Color3 = Color3.fromRGB(0, 170, 255)
    LeftAdorn.AlwaysOnTop = true
    LeftAdorn.ZIndex = 0
    LeftAdorn.Adornee = LeftAdornPart
    LeftAdorn.Parent = LeftAdornPart

    local RightAdornPart = Instance.new("Part")
    RightAdornPart.Transparency = 1
    RightAdornPart.Size = Vector3.new()
    RightAdornPart.Anchored = true
    RightAdornPart.CanCollide = false
    RightAdornPart.CanQuery = false
    RightAdornPart.Parent = Workspace.CurrentCamera

    local RightAdorn = Instance.new("BoxHandleAdornment")
    RightAdorn.Color3 = Color3.fromRGB(0, 170 , 255)
    RightAdorn.AlwaysOnTop = true
    RightAdorn.ZIndex = 0
    RightAdorn.Adornee = RightAdornPart
    RightAdorn.Parent = RightAdornPart

    local LeftMenuToggleHintAdornPart = Instance.new("Part")
    LeftMenuToggleHintAdornPart.Transparency = 1
    LeftMenuToggleHintAdornPart.Size = Vector3.new(1,1,0)
    LeftMenuToggleHintAdornPart.Anchored = true
    LeftMenuToggleHintAdornPart.CanCollide = false
    LeftMenuToggleHintAdornPart.CanQuery = false
    LeftMenuToggleHintAdornPart.Parent = Workspace.CurrentCamera

    local RightMenuToggleHintAdornPart = Instance.new("Part")
    RightMenuToggleHintAdornPart.Transparency = 1
    RightMenuToggleHintAdornPart.Size = Vector3.new(1,1,0)
    RightMenuToggleHintAdornPart.Anchored = true
    RightMenuToggleHintAdornPart.CanCollide = false
    RightMenuToggleHintAdornPart.CanQuery = false
    RightMenuToggleHintAdornPart.Parent = Workspace.CurrentCamera

    local LeftMenuToggleHintGuiFront = Instance.new("SurfaceGui")
    LeftMenuToggleHintGuiFront.Active = false
    LeftMenuToggleHintGuiFront.Face = Enum.NormalId.Front
    LeftMenuToggleHintGuiFront.CanvasSize = Vector2.new(500,500)
    LeftMenuToggleHintGuiFront.LightInfluence = 0
    LeftMenuToggleHintGuiFront.Enabled = InitialMenuToggleGestureActive
    LeftMenuToggleHintGuiFront.AlwaysOnTop = true
    LeftMenuToggleHintGuiFront.Adornee = LeftMenuToggleHintAdornPart
    LeftMenuToggleHintGuiFront.Parent = LeftMenuToggleHintAdornPart

    local LeftMenuToggleHintFrontArrow = Instance.new("ImageLabel")
    LeftMenuToggleHintFrontArrow.ImageTransparency = 1
    LeftMenuToggleHintFrontArrow.BackgroundTransparency = 1
    LeftMenuToggleHintFrontArrow.Rotation = 180
    LeftMenuToggleHintFrontArrow.Size = UDim2.new(1,0,1,0)
    LeftMenuToggleHintFrontArrow.Image = "rbxassetid://6537091378"
    LeftMenuToggleHintFrontArrow.ImageRectSize = Vector2.new(512,512)
    LeftMenuToggleHintFrontArrow.ImageRectOffset = Vector2.new(0,0)
    LeftMenuToggleHintFrontArrow.Parent = LeftMenuToggleHintGuiFront

    local LeftMenuToggleHintFrontText = Instance.new("ImageLabel")
    LeftMenuToggleHintFrontText.ImageTransparency = 1
    LeftMenuToggleHintFrontText.BackgroundTransparency = 1
    LeftMenuToggleHintFrontText.Size = UDim2.new(1,0,1,0)
    LeftMenuToggleHintFrontText.ZIndex = 2
    LeftMenuToggleHintFrontText.Image = "rbxassetid://6537091378"
    LeftMenuToggleHintFrontText.ImageRectSize = Vector2.new(512,512)
    LeftMenuToggleHintFrontText.ImageRectOffset = Vector2.new(0,512)
    LeftMenuToggleHintFrontText.Parent = LeftMenuToggleHintGuiFront

    local LeftMenuToggleHintGuiBack = Instance.new("SurfaceGui")
    LeftMenuToggleHintGuiBack.Active = false
    LeftMenuToggleHintGuiBack.Face = Enum.NormalId.Back
    LeftMenuToggleHintGuiBack.CanvasSize = Vector2.new(500,500)
    LeftMenuToggleHintGuiBack.LightInfluence = 0
    LeftMenuToggleHintGuiBack.Enabled = InitialMenuToggleGestureActive
    LeftMenuToggleHintGuiBack.AlwaysOnTop = true
    LeftMenuToggleHintGuiBack.Adornee = LeftMenuToggleHintAdornPart
    LeftMenuToggleHintGuiBack.Parent = LeftMenuToggleHintAdornPart

    local LeftMenuToggleHintBackArrow = Instance.new("ImageLabel")
    LeftMenuToggleHintBackArrow.ImageTransparency = 1
    LeftMenuToggleHintBackArrow.BackgroundTransparency = 1
    LeftMenuToggleHintBackArrow.Size = UDim2.new(1,0,1,0)
    LeftMenuToggleHintBackArrow.Image = "rbxassetid://6537091378"
    LeftMenuToggleHintBackArrow.ImageRectSize = Vector2.new(512,512)
    LeftMenuToggleHintBackArrow.ImageRectOffset = Vector2.new(512,0)
    LeftMenuToggleHintBackArrow.Parent = LeftMenuToggleHintGuiBack

    local LeftMenuToggleHintBackText = Instance.new("ImageLabel")
    LeftMenuToggleHintBackText.ImageTransparency = 1
    LeftMenuToggleHintBackText.BackgroundTransparency = 1
    LeftMenuToggleHintBackText.Size = UDim2.new(1,0,1,0)
    LeftMenuToggleHintBackText.ZIndex = 2
    LeftMenuToggleHintBackText.Image = "rbxassetid://6537091378"
    LeftMenuToggleHintBackText.ImageRectSize = Vector2.new(512,512)
    LeftMenuToggleHintBackText.ImageRectOffset = Vector2.new(0,512)
    LeftMenuToggleHintBackText.Parent = LeftMenuToggleHintGuiBack

    local RightMenuToggleHintGuiFront = Instance.new("SurfaceGui")
    RightMenuToggleHintGuiFront.Active = false
    RightMenuToggleHintGuiFront.Face = Enum.NormalId.Front
    RightMenuToggleHintGuiFront.CanvasSize = Vector2.new(500,500)
    RightMenuToggleHintGuiFront.LightInfluence = 0
    RightMenuToggleHintGuiFront.Enabled = InitialMenuToggleGestureActive
    RightMenuToggleHintGuiFront.AlwaysOnTop = true
    RightMenuToggleHintGuiFront.Adornee = RightMenuToggleHintAdornPart
    RightMenuToggleHintGuiFront.Parent = RightMenuToggleHintAdornPart

    local RightMenuToggleHintFrontArrow = Instance.new("ImageLabel")
    RightMenuToggleHintFrontArrow.ImageTransparency = 1
    RightMenuToggleHintFrontArrow.BackgroundTransparency = 1
    RightMenuToggleHintFrontArrow.Size = UDim2.new(1,0,1,0)
    RightMenuToggleHintFrontArrow.Image = "rbxassetid://6537091378"
    RightMenuToggleHintFrontArrow.ImageRectSize = Vector2.new(512,512)
    RightMenuToggleHintFrontArrow.ImageRectOffset = Vector2.new(512,0)
    RightMenuToggleHintFrontArrow.Parent = RightMenuToggleHintGuiFront

    local RightMenuToggleHintFrontText = Instance.new("ImageLabel")
    RightMenuToggleHintFrontText.ImageTransparency = 1
    RightMenuToggleHintFrontText.BackgroundTransparency = 1
    RightMenuToggleHintFrontText.Size = UDim2.new(1,0,1,0)
    RightMenuToggleHintFrontText.ZIndex = 2
    RightMenuToggleHintFrontText.Image = "rbxassetid://6537091378"
    RightMenuToggleHintFrontText.ImageRectSize = Vector2.new(512,512)
    RightMenuToggleHintFrontText.ImageRectOffset = Vector2.new(0,512)
    RightMenuToggleHintFrontText.Parent = RightMenuToggleHintGuiFront

    local RightMenuToggleHintGuiBack = Instance.new("SurfaceGui")
    RightMenuToggleHintGuiBack.Active = false
    RightMenuToggleHintGuiBack.Face = Enum.NormalId.Back
    RightMenuToggleHintGuiBack.CanvasSize = Vector2.new(500,500)
    RightMenuToggleHintGuiBack.LightInfluence = 0
    RightMenuToggleHintGuiBack.Enabled = InitialMenuToggleGestureActive
    RightMenuToggleHintGuiBack.AlwaysOnTop = true
    RightMenuToggleHintGuiBack.Adornee = RightMenuToggleHintAdornPart
    RightMenuToggleHintGuiBack.Parent = RightMenuToggleHintAdornPart

    local RightMenuToggleHintBackArrow = Instance.new("ImageLabel")
    RightMenuToggleHintBackArrow.ImageTransparency = 1
    RightMenuToggleHintBackArrow.BackgroundTransparency = 1
    RightMenuToggleHintBackArrow.Size = UDim2.new(1,0,1,0)
    RightMenuToggleHintBackArrow.Image = "rbxassetid://6537091378"
    RightMenuToggleHintBackArrow.ImageRectSize = Vector2.new(512,512)
    RightMenuToggleHintBackArrow.ImageRectOffset = Vector2.new(0,0)
    RightMenuToggleHintBackArrow.Parent = RightMenuToggleHintGuiBack

    local RightMenuToggleHintBackText = Instance.new("ImageLabel")
    RightMenuToggleHintBackText.BackgroundTransparency = 1
    RightMenuToggleHintBackText.Rotation = 180
    RightMenuToggleHintBackText.ImageTransparency = 1
    RightMenuToggleHintBackText.Size = UDim2.new(1,0,1,0)
    RightMenuToggleHintBackText.ZIndex = 2
    RightMenuToggleHintBackText.Image = "rbxassetid://6537091378"
    RightMenuToggleHintBackText.ImageRectSize = Vector2.new(512,512)
    RightMenuToggleHintBackText.ImageRectOffset = Vector2.new(0,512)
    RightMenuToggleHintBackText.Parent = RightMenuToggleHintGuiBack

    --Connect hiding the hints when the setting changes.
    Settings:GetSettingsChangedSignal("Menu.MenuToggleGestureActive"):Connect(function()
        --Determine if the gesture is active.
        local MenuToggleGestureActive = Settings:GetSetting("Menu.MenuToggleGestureActive")
        if MenuToggleGestureActive == nil then
            MenuToggleGestureActive = true
        end

        --Update the visibility of the hints.
        LeftMenuToggleHintGuiFront.Enabled = MenuToggleGestureActive
        LeftMenuToggleHintGuiBack.Enabled = MenuToggleGestureActive
        RightMenuToggleHintGuiFront.Enabled = MenuToggleGestureActive
        RightMenuToggleHintGuiBack.Enabled = MenuToggleGestureActive
    end)

    --Start checking for the controllers to be upside down.
    --Done in a task since this function is non-yielding.
    local BothControllersUpStartTime
    local MenuToggleReached = false
    task.spawn(function()
        while true do
            --Determine if the gesture is active.
            local MenuToggleGestureActive = Settings:GetSetting("Menu.MenuToggleGestureActive")
            if MenuToggleGestureActive == nil then
                MenuToggleGestureActive = true
            end

            --Get the inputs and determine if the hands are both upside down and pointing forward.
            local VRInputs = VRInputService:GetVRInputs()
            local LeftHandCFrameRelative, RightHandCFrameRelative = VRInputs[Enum.UserCFrame.Head]:Inverse() * VRInputs[Enum.UserCFrame.LeftHand], VRInputs[Enum.UserCFrame.Head]:Inverse() * VRInputs[Enum.UserCFrame.RightHand]
            local LeftHandFacingUp, RightHandFacingUp = LeftHandCFrameRelative.UpVector.Y < 0, RightHandCFrameRelative.UpVector.Y < 0
            local LeftHandFacingForward, RightHandFacingForward = LeftHandCFrameRelative.LookVector.Z < 0, RightHandCFrameRelative.LookVector.Z < 0
            local LeftHandUp, RightHandUp = LeftHandFacingUp and LeftHandFacingForward, RightHandFacingUp and RightHandFacingForward
            local BothHandsUp = MenuToggleGestureActive and LeftHandUp and RightHandUp
            if BothHandsUp then
                BothControllersUpStartTime = BothControllersUpStartTime or tick()
            else
                BothControllersUpStartTime = nil :: any
                MenuToggleReached = false
            end

            --Update the adorn part CFrames.
            local CameraCenterCFrame = Workspace.CurrentCamera:GetRenderCFrame() * VRInputs[Enum.UserCFrame.Head]:Inverse()
            LeftAdornPart.CFrame = CameraCenterCFrame * VRInputs[Enum.UserCFrame.LeftHand] * CFrame.new(0, -0.25, 0.25)
            RightAdornPart.CFrame = CameraCenterCFrame * VRInputs[Enum.UserCFrame.RightHand] * CFrame.new(0, -0.25, 0.25)
            LeftMenuToggleHintAdornPart.CFrame = CameraCenterCFrame * VRInputs[Enum.UserCFrame.LeftHand]
            RightMenuToggleHintAdornPart.CFrame = CameraCenterCFrame * VRInputs[Enum.UserCFrame.RightHand]

            --Update the progress bars.
            if BothControllersUpStartTime and not MenuToggleReached then
                local DeltaTimePercent = (tick() - BothControllersUpStartTime) / MENU_OPEN_TIME_REQUIREMENT
                LeftAdorn.Size = Vector3.new(0.1, 0, 0.25 * DeltaTimePercent)
                RightAdorn.Size = Vector3.new(0.1, 0, 0.25 * DeltaTimePercent)
                LeftAdorn.Visible = true
                RightAdorn.Visible = true

                --Toggle the menu if the time threshold was reached.
                if DeltaTimePercent >= 1 then
                    MenuToggleReached = true
                    task.spawn(function()
                        self:Toggle()
                    end)
                end
            else
                LeftAdorn.Visible = false
                RightAdorn.Visible = false
            end

            --[[
            Updates the given hint parts.
            --]]
            local function UpdateHintParts(Visible,Part,FrontArrow,BackArrow,FrontText,BackText)
                local TweenData = TweenInfo.new(0.25)
                TweenService:Create(Part,TweenData,{
                    Size = Visible and Vector3.new(1, 1, 0) or Vector3.new(1.5, 1.5, 0)
                }):Play()
                TweenService:Create(FrontArrow,TweenData,{
                    ImageTransparency = Visible and 0 or 1
                }):Play()
                TweenService:Create(BackArrow,TweenData,{
                    ImageTransparency = Visible and 0 or 1
                }):Play()
                TweenService:Create(FrontText,TweenData,{
                    ImageTransparency = Visible and 0 or 1
                }):Play()
                TweenService:Create(BackText,TweenData,{
                    ImageTransparency = Visible and 0 or 1
                }):Play()
            end

            --Update the hints.
            local LeftHandHintVisible, RightHandHintVisible = self.ScreenGui.Enabled and not LeftHandUp, self.ScreenGui.Enabled and not RightHandUp
            if self.LeftHandHintVisible ~= LeftHandHintVisible then
                self.LeftHandHintVisible = LeftHandHintVisible
                UpdateHintParts(LeftHandHintVisible, LeftMenuToggleHintAdornPart, LeftMenuToggleHintFrontArrow, LeftMenuToggleHintBackArrow, LeftMenuToggleHintFrontText, LeftMenuToggleHintBackText)
            end
            if self.RightHandHintVisible ~= RightHandHintVisible then
                self.RightHandHintVisible = RightHandHintVisible
                UpdateHintParts(RightHandHintVisible, RightMenuToggleHintAdornPart, RightMenuToggleHintFrontArrow, RightMenuToggleHintBackArrow, RightMenuToggleHintFrontText, RightMenuToggleHintBackText)
            end
            local Rotation = (tick() * 10) % 360
            LeftMenuToggleHintFrontArrow.Rotation = Rotation
            LeftMenuToggleHintBackArrow.Rotation = -Rotation
            RightMenuToggleHintFrontArrow.Rotation = -Rotation
            RightMenuToggleHintBackArrow.Rotation = Rotation

            --Wait to poll again.
            RunService.RenderStepped:Wait()
        end
    end)
end

--[[
Toggles the menu being open.
--]]
function MainMenu.Toggle(self: MainMenu, Visible: boolean?): ()
    if self.ScreenGui.Enabled == Visible then return end

    --Determine the start and end values.
    local StartFieldOfView, EndFieldOfView = (self.ScreenGui.Enabled and math.rad(40) or 0), (self.ScreenGui.Enabled and 0 or math.rad(40))

    --Show the menu if it isn't visible.
    if not self.ScreenGui.Enabled then
        self.ScreenGui.Enabled = true
    end

    --Tween the field of view.
    local StartTime = tick()
    while tick() - StartTime < MENU_OPEN_TIME do
        local Delta = (tick() - StartTime) / MENU_OPEN_TIME
        Delta = (math.sin((Delta - 0.5) * math.pi) / 2) + 0.5
        self.ScreenGui.FieldOfView = StartFieldOfView + ((EndFieldOfView - StartFieldOfView) * Delta)
        RunService.RenderStepped:Wait()
    end

    --Hide thhe menu if it is closed.
    if EndFieldOfView == 0 then
        self.ScreenGui.Enabled = false
    end
end

--[[
Registers a view.
--]]
function MainMenu.RegisterView(self: MainMenu, ViewName: string, ViewInstance: any): ()
    warn("MainMenu::RegisterView is deprecated and may be removed in the future. Use MainMenu::CreateView instead.")

    --Set up the view instance.
    ViewInstance.Visible = false
    ViewInstance.Name = ViewName
    ViewInstance.Parent = self.ViewAdornFrame

    --Store the view.
    table.insert(self.Views, ViewInstance)
end

--[[
Creates a menu view.
--]]
function MainMenu.CreateView(self: MainMenu, InitialViewName: string): any
    --Create and store the view.
    local View = ApiBaseView.new(InitialViewName)
    View.Frame.Parent = (self :: any).ViewAdornFrame
    table.insert(self.Views, View)

    --Connect the events.
    View:GetPropertyChangedSignal("Name"):Connect(function()
        self:UpdateVisibleView()
    end)
    View.Destroyed:Connect(function()
        for i = 1, #self.Views do
            if self.Views[i] == View then
                table.remove(self.Views, i)
                if self.CurrentView > i then
                    self.CurrentView += -1
                end
                break
            end
        end
        self:UpdateVisibleView()
    end)
    return View
end

--[[
Updates the visible view.
--]]
function MainMenu.UpdateVisibleView(self: MainMenu, NewView: string?): ()
    --Update the button visibility.
    self.LeftButton.Visible = (#self.Views > 1)
    self.RightButton.Visible = (#self.Views > 1)

    --Update the display text.
    if NewView then
        for i, View in self.Views do
            if View.Name ~= NewView then continue end
            self.CurrentView = i
            break
        end
    end
    self.ViewTextLabel.Text = self.Views[self.CurrentView].Name

    --Update the view visibilites.
    for i, View in self.Views do
        View.Visible = (i == self.CurrentView)
    end
end

return MainMenu


--Displays a message if R6 is used.
--!strict

local MESSAGE_OPEN_TIME = 0.25

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

local NexusVRCharacterModel = script.Parent.Parent
local NexusButton = require(NexusVRCharacterModel:WaitForChild("Packages"):WaitForChild("NexusButton"))
local NexusVRCore = require(NexusVRCharacterModel:WaitForChild("Packages"):WaitForChild("NexusVRCore"))

local TextButtonFactory = NexusButton.TextButtonFactory.CreateDefault(Color3.fromRGB(0, 170, 255))
TextButtonFactory:SetDefault("Theme", "RoundedCorners")
local ScreenGui3D = NexusVRCore.ScreenGui3D

local R6Message = {}
R6Message.__index = R6Message

export type R6Message = {
    ScreenGui: any,
} & typeof(setmetatable({}, R6Message))

--[[
Creates the R6 message.
--]]
function R6Message.new(): R6Message
    --Set up the ScreenGui.
    local MessageScreenGui = ScreenGui3D.new()
    MessageScreenGui.ResetOnSpawn = false
    MessageScreenGui.Enabled = false
    MessageScreenGui.CanvasSize = Vector2.new(500, 500)
    MessageScreenGui.FieldOfView = 0
    MessageScreenGui.Easing = 0.25

    --Create the object.
    local self = setmetatable({
        ScreenGui = MessageScreenGui,
    }, R6Message) :: R6Message

    --Create the logo and message.
    local Logo = Instance.new("ImageLabel")
    Logo.BackgroundTransparency = 1
    Logo.Size = UDim2.new(0.4, 0, 0.4, 0)
    Logo.Position = UDim2.new(0.3, 0, -0.1, 0)
    Logo.Image = "http://www.roblox.com/asset/?id=1499731139"
    Logo.Parent = MessageScreenGui:GetContainer()

    local UpperText = Instance.new("TextLabel")
    UpperText.BackgroundTransparency = 1
    UpperText.Size = UDim2.new(0.8, 0, 0.1, 0)
    UpperText.Position = UDim2.new(0.1, 0, 0.25, 0)
    UpperText.Font = Enum.Font.SourceSansBold
    UpperText.Text = "R6 Not Supported"
    UpperText.TextScaled = true
    UpperText.TextColor3 = Color3.fromRGB(255, 255, 255)
    UpperText.TextStrokeColor3 = Color3.fromRGB(0, 0, 0)
    UpperText.TextStrokeTransparency = 0
    UpperText.Parent = MessageScreenGui:GetContainer()

    local LowerText = Instance.new("TextLabel")
    LowerText.BackgroundTransparency = 1
    LowerText.Size = UDim2.new(0.8, 0, 0.25, 0)
    LowerText.Position = UDim2.new(0.1, 0, 0.4, 0)
    LowerText.Font = Enum.Font.SourceSansBold
    LowerText.Text = "Nexus VR Character Model does not support using R6. Use R15 instead."
    LowerText.TextScaled = true
    LowerText.TextColor3 = Color3.fromRGB(255, 255, 255)
    LowerText.TextStrokeColor3 = Color3.fromRGB(0, 0, 0)
    LowerText.TextStrokeTransparency = 0
    LowerText.Parent = MessageScreenGui:GetContainer()

    --Create and connect the close button.
    local CloseButton, CloseText = TextButtonFactory:Create()
    CloseButton.Size = UDim2.new(0.3, 0, 0.1, 0)
    CloseButton.Position = UDim2.new(0.35, 0, 0.7, 0)
    CloseButton.Parent = MessageScreenGui:GetContainer()
    CloseText.Text = "Ok"

    CloseButton.MouseButton1Down:Connect(function()
        self:SetOpen(false)
        MessageScreenGui:Destroy()
    end)

    --Parent the message.
    MessageScreenGui.Parent = Players.LocalPlayer:WaitForChild("PlayerGui")
    return self
end

--[[
Sets the window open or closed.
--]]
function R6Message.SetOpen(self: R6Message, Open: boolean): ()
    --Determine the start and end values.
    local StartFieldOfView, EndFieldOfView = (Open and 0 or math.rad(40)), (Open and math.rad(40) or 0)

    --Show the message if it isn't visible.
    if Open then
        self.ScreenGui.Enabled = true
    end

    --Tween the field of view.
    local StartTime = tick()
    while tick() - StartTime < MESSAGE_OPEN_TIME do
        local Delta = (tick() - StartTime) / MESSAGE_OPEN_TIME
        Delta = (math.sin((Delta - 0.5) * math.pi) / 2) + 0.5
        self.ScreenGui.FieldOfView = StartFieldOfView + ((EndFieldOfView - StartFieldOfView) * Delta)
        RunService.RenderStepped:Wait()
    end

    --Hide thhe message if it is closed.
    if EndFieldOfView == 0 then
        self.ScreenGui.Enabled = false
    end
end

--[[
Opens the message.
--]]
function R6Message.Open(self: R6Message): ()
    self:SetOpen(true)
end

return R6Message


--Base view for the menu intended to be used with the API.
--!strict

local GuiService = game:GetService("GuiService")

local NexusVRCharacterModel = script.Parent.Parent.Parent
local NexusInstance = require(NexusVRCharacterModel:WaitForChild("Packages"):WaitForChild("NexusInstance"))

local ApiBaseView = {}
ApiBaseView.__index = ApiBaseView

export type ApiBaseView = {
    Name: string,
    Visible: boolean,
    Destroyed: NexusInstance.TypedEvent<>,
    Frame: Frame,
} & typeof(setmetatable({}, ApiBaseView))
export type NexusInstanceApiBaseView = NexusInstance.NexusInstance<ApiBaseView>

--[[
Creates the view.
--]]
function ApiBaseView.__new(self: NexusInstanceApiBaseView, InitialName: string): ()
    self.Name = InitialName
    self.Destroyed = self:CreateEvent()

    self.Frame = Instance.new("Frame")
    self.Frame.Name = tostring(self.Name)
    self.Frame.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
    self.Frame.BackgroundTransparency = 1
    self.Frame.Size = UDim2.new(1, 0, 1, 0)
    self.Frame.Visible = false
    self.Frame.SizeConstraint = Enum.SizeConstraint.RelativeXX
    self:GetPropertyChangedSignal("Name"):Connect(function()
        self.Frame.Name = tostring(self.Name)
    end)
    self:GetPropertyChangedSignal("Visible"):Connect(function()
        self.Frame.Visible = self.Visible
    end)
end

--[[
Returns the containing frame.
--]]
function ApiBaseView.GetContainer(self: NexusInstanceApiBaseView): Frame
    return self.Frame
end

--[[
Adds the background of the frame.
--]]
function ApiBaseView.AddBackground(self: NexusInstanceApiBaseView): ()
    --Add the corner.
    local UICorner = Instance.new("UICorner")
    UICorner.CornerRadius = UDim.new(0.05, 0)
    UICorner.Parent = self.Frame

    --Enable the transparency.
    self.Frame.BackgroundTransparency = 0.6 * GuiService.PreferredTransparency
    GuiService:GetPropertyChangedSignal("PreferredTransparency"):Connect(function()
        self.Frame.BackgroundTransparency = 0.6 * GuiService.PreferredTransparency
    end)
end

--[[
Destroys the view.
--]]
function ApiBaseView.Destroy(self: NexusInstanceApiBaseView): ()
    self.Destroyed:Fire()
    self.Frame:Destroy()
end

return NexusInstance.ToInstance(ApiBaseView) :: NexusInstance.NexusInstanceClass<typeof(ApiBaseView), (InitialName: string) -> (NexusInstanceApiBaseView)>


--View for the Enigma library.
--!strict

local SUPPORTED_PROTOCOL_VERSIONS = {
    ["1"] = true,
    ["2"] = true,
}

local Workspace = game:GetService("Workspace")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local VRService = game:GetService("VRService")

local NexusVRCharacterModel = script.Parent.Parent.Parent
local Enigma = require(NexusVRCharacterModel:WaitForChild("Packages"):WaitForChild("Enigma"))
local NexusButton = require(NexusVRCharacterModel:WaitForChild("Packages"):WaitForChild("NexusButton"))
local CharacterService = require(NexusVRCharacterModel:WaitForChild("State"):WaitForChild("CharacterService")).GetInstance()
local EnigmaService = require(NexusVRCharacterModel:WaitForChild("State"):WaitForChild("EnigmaService")).GetInstance()
local Settings = require(NexusVRCharacterModel:WaitForChild("State"):WaitForChild("Settings")).GetInstance()
local BaseApiView = require(NexusVRCharacterModel:WaitForChild("UI"):WaitForChild("View"):WaitForChild("ApiBaseView"))

local TextButtonFactory = NexusButton.TextButtonFactory.CreateDefault(Color3.fromRGB(0, 170, 255))
TextButtonFactory:SetDefault("Theme", "RoundedCorners")

local EnigmaView = {}
EnigmaView.__index = EnigmaView

export type EnigmaView = {
    EnigmaText: TextLabel,
    ShowTrackersButton: NexusButton.NexusInstanceNexusButton?,
    DebugTrackersVisible: boolean?,
    CalibrateButton: NexusButton.NexusInstanceNexusButton?,
} & typeof(setmetatable({}, EnigmaView))

--[[
Returns text with rich text color.
--]]
local function AddColor(Text: string, R: number, G: number, B: number): string
    return `<font color="rgb({R},{G},{B})">{Text}</font>`
end

--[[
Creates the Enigma view.
--]]
function EnigmaView.new(View: BaseApiView.NexusInstanceApiBaseView, Menu: any): EnigmaView
    View:AddBackground()

    --Create the view.
    local Container = View:GetContainer()
    local EnigmaText = Instance.new("TextLabel")
    EnigmaText.BackgroundTransparency = 1
    EnigmaText.Size = UDim2.new(0.9, 0, 0.6, 0)
    EnigmaText.Position = UDim2.new(0.05, 0, 0.075, 0)
    EnigmaText.Font = Enum.Font.SourceSansBold
    EnigmaText.Text = ""
    EnigmaText.RichText = true
    EnigmaText.TextWrapped = true
    EnigmaText.TextColor3 = Color3.fromRGB(255, 255, 255)
    EnigmaText.TextStrokeColor3 = Color3.fromRGB(0, 0, 0)
    EnigmaText.TextStrokeTransparency = 0
    EnigmaText.TextSize = 28
    EnigmaText.TextYAlignment = Enum.TextYAlignment.Top
    EnigmaText.Parent = Container

    --Create the object.
    local self = setmetatable({
        EnigmaText = EnigmaText,
    }, EnigmaView) :: EnigmaView

    --Return the object early if Enigma is not enabled.
    if Settings:GetSetting("Extra.EnigmaEnabled") == false then
        EnigmaText.Text = "This game has disabled Enigma."
        return self
    end

    local ShowTrackersButton, ShowTrackersText = TextButtonFactory:Create()
    ShowTrackersButton.Size = UDim2.new(0.5, 0, 0.075, 0)
    ShowTrackersButton.Position = UDim2.new(0.25, 0, 0.775, 0)
    ShowTrackersButton.SizeConstraint = Enum.SizeConstraint.RelativeYY
    ShowTrackersButton.Parent = Container
    ShowTrackersText.Text = " Show Trackers "
    self.ShowTrackersButton = ShowTrackersButton

    self.DebugTrackersVisible = false
    local DB = true
    ShowTrackersButton.MouseButton1Down:Connect(function()
        if not DB then return end
        DB = false
        self.DebugTrackersVisible = not self.DebugTrackersVisible
        if self.DebugTrackersVisible then
            task.spawn(function()
                self:ShowDebugTrackers()
            end)
        end
        ShowTrackersText.Text = (self.DebugTrackersVisible and " Hide Trackers" or " Show Trackers ")
        task.wait()
        DB = true
    end)

    local CalibrateButton, CalibrateText = TextButtonFactory:Create()
    CalibrateButton.Size = UDim2.new(0.5, 0, 0.075, 0)
    CalibrateButton.Position = UDim2.new(0.25, 0, 0.875, 0)
    CalibrateButton.SizeConstraint = Enum.SizeConstraint.RelativeYY
    CalibrateButton.Parent = Container
    CalibrateText.Text = " Calibrate Trackers "
    self.CalibrateButton = CalibrateButton

    CalibrateButton.MouseButton1Down:Connect(function()
        if not DB then return end
        DB = false
        local Character = CharacterService:GetCharacter(Players.LocalPlayer)
        if Character then
            EnigmaService:Calibrate(Character)
        end
        task.wait()
        DB = true
    end)

    --Update the text in the background.
    self:UpdateText()
    task.spawn(function()
        while true do
            self:UpdateText()
            task.wait(0.1)
        end
    end)

    --Present the view if an uncalibratedd input becomes visible.
    task.spawn(function()
        local UncalibratedInputsPrompted = {
            LeftFoot = false,
            RightFoot = false,
        }
        while true do
            --Determine if there are unprompted inputs.
            local AllInputsCalibrated = true
            local InputsToPrompt = {}
            for InputName, Prompted in UncalibratedInputsPrompted do
                if Prompted then continue end
                if not Prompted then
                    AllInputsCalibrated = false
                end
                if not Enigma.Enabled or not Enigma:GetUserCFrameEnabled(InputName :: any) then continue end
                if EnigmaService.Offsets[InputName] then continue end
                table.insert(InputsToPrompt, InputName)
            end
            for _, InputName in InputsToPrompt do
                UncalibratedInputsPrompted[InputName] = true
            end

            --Show the view.
            if #InputsToPrompt > 0 then
                Menu:UpdateVisibleView(View.Name)
                Menu:Toggle(true)
                self:UpdateText()
            end

            --Break the loop if all inputs are prompted.
            if AllInputsCalibrated then break end
            task.wait()
        end
    end)

    --Return the object.
    return self
end

--[[
Updates the Enigma text.
--]]
function EnigmaView.UpdateText(self: EnigmaView): ()
    --Add the tracker information.
    local Text = ""
    local HasLeftFoot, HasRightFoot = Enigma.Enabled and Enigma:GetUserCFrameEnabled("LeftFoot"), Enigma.Enabled and Enigma:GetUserCFrameEnabled("RightFoot")
    if HasLeftFoot then
        if EnigmaService.Offsets.LeftFoot then
            Text = `{Text}Left Foot Tracker: {AddColor("Active", 0, 200, 0)}`
        else
            Text = `{Text}Left Foot Tracker: {AddColor("Requires Calibration", 200, 150, 0)}`
        end
    else
        Text = `{Text}Left Foot Tracker: {AddColor("Inactive", 200, 0, 0)}`
    end
    if HasRightFoot then
        if EnigmaService.Offsets.RightFoot then
            Text = `{Text}\nRight Foot Tracker: {AddColor("Active", 0, 200, 0)}`
        else
            Text = `{Text}\nRight Foot Tracker: {AddColor("Requires Calibration", 200, 150, 0)}`
        end
    else
        Text = `{Text}\nRight Foot Tracker: {AddColor("Inactive", 200, 0, 0)}`
    end

    --Add the data transfer status.
    if Enigma:IsActive() then
        local FocusedTextBox = UserInputService:GetFocusedTextBox()
        if not FocusedTextBox or (FocusedTextBox.Parent and FocusedTextBox.Parent.Name == "EnigmaTextBoxInput") then
            Text = `{Text}\nData transfer: {AddColor("Active", 0, 200, 0)}`
            if Enigma.Input then
                local EnigmaVersion = string.split(Enigma.Input:GetCurrentText(), "|")[1]
                if SUPPORTED_PROTOCOL_VERSIONS[EnigmaVersion] then
                    Text = `{Text}\n\nProtocol version: {EnigmaVersion}`
                else
                    Text = `{Text}\n\nProtocol version: {AddColor(`{EnigmaVersion} (Unsupported)`, 200, 150, 0)}`
                end
            end
        else
            Text = `{Text}\nData transfer: {AddColor("Inactive", 200, 0, 0)}`
        end
        if not HasLeftFoot and not HasRightFoot and Enigma:GetUserCFrameEnabled("None") then
            Text = `{Text}\n\n⚠ Trackers are detected, but the role in SteamVR is "None". They need to have assigned roles through the SteamVR menu.`
        else
            Text = `{Text}\n\nTo calibrate, stand up straight with your head level and facing forward with your feet next to each other pointing forward.`
        end
        if self.ShowTrackersButton then
            self.ShowTrackersButton.Visible = true
        end
        if self.CalibrateButton then
            self.CalibrateButton.Visible = true
        end
    else
        Text = `{Text}\nData transfer: {AddColor("Inactive", 200, 150, 0)}`
        Text = `{Text}\n\n⚠ Enigma requires a desktop application. There may be experience-specific issues when Enigma is active.`
        if self.ShowTrackersButton then
            self.ShowTrackersButton.Visible = false
        end
        if self.CalibrateButton then
            self.CalibrateButton.Visible = false
        end
    end

    --Set the text.
    self.EnigmaText.Text = Text
end

--[[
Shows debug trackers.
--]]
function EnigmaView.ShowDebugTrackers(self: EnigmaView): ()
    if not Enigma.Enabled then return end
    local TrackerParts, TrackerLabels = {}, {}
    local TrackersPartsFolder = Instance.new("Folder")
    TrackersPartsFolder.Name = "NexusVRCharacterModelEnigmaDebugTrackers"
    TrackersPartsFolder.Parent = Workspace.CurrentCamera

    while self.DebugTrackersVisible do
        --Get the visible trackers.
        local ActiveInputs = {}
        local TotalInputs = 0
        local OriginWorldCFrame = Workspace.CurrentCamera:GetRenderCFrame() * VRService:GetUserCFrame(Enum.UserCFrame.Head):Inverse()
        for _, UserCFrameName: Enigma.TrackerRole in Enigma.TrackerRoles do
            local TrackerIndex = 1
            while Enigma:GetUserCFrameEnabled(UserCFrameName, TrackerIndex) do
                local DisplayUserCFrame = (TrackerIndex == 1 and UserCFrameName or `{UserCFrameName} ({TrackerIndex})`)
                ActiveInputs[DisplayUserCFrame] = OriginWorldCFrame * Enigma:GetUserCFrame(UserCFrameName, TrackerIndex)
                TotalInputs += 1
                TrackerIndex += 1
            end
        end

        --Update the debug trackers.
        local CurrentInput = 1
        for InputName, InputCFrame in ActiveInputs do
            if not TrackerParts[CurrentInput] then
                local TrackerPart = Instance.new("Part")
                TrackerPart.BrickColor = BrickColor.new("Institutional white")
                TrackerPart.Material = Enum.Material.Neon
                TrackerPart.Anchored = true
                TrackerPart.CanCollide = false
                TrackerPart.CanTouch = false
                TrackerPart.CanQuery = false
                TrackerPart.Shape = Enum.PartType.Ball
                TrackerPart.Size = Vector3.new(0.2, 0.2, 0.2)
                TrackerPart.Parent = TrackersPartsFolder
                TrackerParts[CurrentInput] = TrackerPart

                local SphereHandleAdornment = Instance.new("SphereHandleAdornment")
                SphereHandleAdornment.Transparency = 0.5
                SphereHandleAdornment.AlwaysOnTop = true
                SphereHandleAdornment.Color3 = Color3.fromRGB(255, 255, 255)
                SphereHandleAdornment.ZIndex = 1
                SphereHandleAdornment.Radius = 0.1
                SphereHandleAdornment.Adornee = TrackerPart
                SphereHandleAdornment.Parent = TrackerPart

                local BillboardGui = Instance.new("BillboardGui")
                BillboardGui.AlwaysOnTop = true
                BillboardGui.Adornee = TrackerPart
                BillboardGui.Size = UDim2.new(2, 0, 0.4, 0)
                BillboardGui.StudsOffset = Vector3.new(0, 0.3 ,0)
                BillboardGui.Parent = TrackerPart

                local TrackerRoleText = Instance.new("TextLabel")
                TrackerRoleText.BackgroundTransparency = 1
                TrackerRoleText.Size = UDim2.new(1, 0, 1, 0)
                TrackerRoleText.TextScaled = true
                TrackerRoleText.TextColor3 = Color3.fromRGB(255, 255, 255)
                TrackerRoleText.TextStrokeColor3 = Color3.fromRGB(0, 0, 0)
                TrackerRoleText.TextStrokeTransparency = 0
                TrackerRoleText.Parent = BillboardGui
                TrackerLabels[CurrentInput] = TrackerRoleText
            end
            TrackerParts[CurrentInput].CFrame = InputCFrame
            TrackerLabels[CurrentInput].Text = InputName
            CurrentInput += 1
        end

        --Remove extra trackers.
        for i = #TrackerParts, TotalInputs + 1, -1 do
            TrackerParts[i]:Destroy()
            TrackerParts[i] = nil
            TrackerLabels[i]:Destroy()
            TrackerLabels[i] = nil
        end

        --Wait to refresh.
        RunService.RenderStepped:Wait()
    end

    --Clear the instances.
    TrackersPartsFolder:Destroy()
end

return EnigmaView


--View for the user settings.
--!strict

local UserInputService = game:GetService("UserInputService")
local VRService = game:GetService("VRService")

local NexusVRCharacterModel = script.Parent.Parent.Parent
local NexusButton = require(NexusVRCharacterModel:WaitForChild("Packages"):WaitForChild("NexusButton"))
local CameraService = require(NexusVRCharacterModel:WaitForChild("State"):WaitForChild("CameraService")).GetInstance()
local ControlService = require(NexusVRCharacterModel:WaitForChild("State"):WaitForChild("ControlService")).GetInstance()
local DefaultCursorService = require(NexusVRCharacterModel:WaitForChild("State"):WaitForChild("DefaultCursorService")).GetInstance()
local Settings = require(NexusVRCharacterModel:WaitForChild("State"):WaitForChild("Settings")).GetInstance()
local VRInputService = require(NexusVRCharacterModel:WaitForChild("State"):WaitForChild("VRInputService")).GetInstance()
local BaseApiView = require(NexusVRCharacterModel:WaitForChild("UI"):WaitForChild("View"):WaitForChild("ApiBaseView"))

local TextButtonFactory = NexusButton.TextButtonFactory.CreateDefault(Color3.fromRGB(0, 170, 255))
TextButtonFactory:SetDefault("Theme", "RoundedCorners")

local SettingsView = {}
SettingsView.__index = SettingsView

export type SettingsView = {

} & typeof(setmetatable({}, SettingsView))

--[[
Creates the settings view.
--]]
function SettingsView.new(View: BaseApiView.NexusInstanceApiBaseView): ()
    View:AddBackground()

    --Create the object.
    local self = setmetatable({}, SettingsView) :: SettingsView

    --Create the header.
    local Container = View:GetContainer()
    local HeaderLogo = Instance.new("ImageLabel")
    HeaderLogo.BackgroundTransparency = 1
    HeaderLogo.Size = UDim2.new(0.4, 0, 0.4, 0)
    HeaderLogo.Position = UDim2.new(0.3, 0, -0.075, 0)
    HeaderLogo.Image = "http://www.roblox.com/asset/?id=1499731139"
    HeaderLogo.Parent = Container

    local NameText = Instance.new("TextLabel")
    NameText.BackgroundTransparency = 1
    NameText.Size = UDim2.new(0.8, 0, 0.1, 0)
    NameText.Position = UDim2.new(0.1, 0, 0.225, 0)
    NameText.Font = Enum.Font.SourceSansBold
    NameText.Text = "Nexus VR Character Model"
    NameText.TextScaled = true
    NameText.TextColor3 = Color3.fromRGB(255, 255, 255)
    NameText.TextStrokeColor3 = Color3.fromRGB(0, 0, 0)
    NameText.TextStrokeTransparency = 0
    NameText.Parent = Container

    --Create the settings.
    local CameraSettingFrame = Instance.new("Frame")
    CameraSettingFrame.BackgroundTransparency = 1
    CameraSettingFrame.Size = UDim2.new(0.8, 0,0.11, 0)
    CameraSettingFrame.Position = UDim2.new(0.1, 0, 0.325, 0)
    CameraSettingFrame.Parent = Container
    self:PopulateSettingsFrame(CameraSettingFrame, "View", function()
        if VRService.AvatarGestures then
            return {"Default"} --TODO: Third-person camera setting is not supported with AvatarGestures.
        end
        return Settings:GetSetting("Camera.EnabledCameraOptions") or {}
    end, function()
        return CameraService.ActiveCamera
    end, function(NewValue)
        CameraService:SetActiveCamera(NewValue)
    end)

    local MovementSettingFrame = Instance.new("Frame")
    MovementSettingFrame.BackgroundTransparency = 1
    MovementSettingFrame.Size = UDim2.new(0.8, 0, 0.11, 0)
    MovementSettingFrame.Position = UDim2.new(0.1, 0, 0.325 + (0.15 * 1), 0)
    MovementSettingFrame.Parent = Container
    self:PopulateSettingsFrame(MovementSettingFrame, "Control", "Movement.EnabledMovementMethods", function()
        return ControlService.ActiveController
    end, function(NewValue)
        ControlService:SetActiveController(NewValue)
    end)

    local CursorSettingFrame = Instance.new("Frame")
    CursorSettingFrame.BackgroundTransparency = 1
    CursorSettingFrame.Size = UDim2.new(0.8, 0, 0.11, 0)
    CursorSettingFrame.Position = UDim2.new(0.1, 0, 0.325 + (0.15 * 2), 0)
    CursorSettingFrame.Parent = Container
    self:PopulateSettingsFrame(CursorSettingFrame, "Roblox VR Cursor", function()
        return DefaultCursorService.CursorOptionsList
    end, function()
        return DefaultCursorService.CurrentCursorState
    end, function(NewValue)
        DefaultCursorService:SetCursorState(NewValue)
    end)

    --Create the callibration settings.
    local RecenterButton, RecenterText = TextButtonFactory:Create()
    RecenterButton.Size = UDim2.new(0.4, 0, 0.075, 0)
    RecenterButton.Position = UDim2.new((VRService.AvatarGestures and 0.3 or 0.075), 0, 0.85, 0)
    RecenterButton.SizeConstraint = Enum.SizeConstraint.RelativeYY
    RecenterButton.Parent = Container
    RecenterText.Text = "Recenter"

    RecenterButton.MouseButton1Down:Connect(function()
        if not VRService.AvatarGestures then
            VRInputService:Recenter()
        else
            UserInputService:RecenterUserHeadCFrame()
        end
    end)

    if not VRService.AvatarGestures then
        local SetEyeLevelButton, SetEyeLevelText = TextButtonFactory:Create()
        SetEyeLevelButton.Size = UDim2.new(0.4, 0, 0.075, 0)
        SetEyeLevelButton.Position = UDim2.new(0.525, 0, 0.85, 0)
        SetEyeLevelButton.SizeConstraint = Enum.SizeConstraint.RelativeYY
        SetEyeLevelButton.Parent = Container
        SetEyeLevelText.Text = " Set Eye Level "

        SetEyeLevelButton.MouseButton1Down:Connect(function()
            VRInputService:SetEyeLevel()
        end)
    end

    --Create the version information.
    local VersionText = Instance.new("TextLabel")
    VersionText.BackgroundTransparency = 1
    VersionText.AnchorPoint = Vector2.new(0.5, 1)
    VersionText.Size = UDim2.new(0.8, 0, 0.04, 0)
    VersionText.Position = UDim2.new(0.5, 0, 1, 0)
    VersionText.Font = Enum.Font.SourceSansBold
    VersionText.Text = `Version {Settings:GetSetting("Version.Tag")} ({Settings:GetSetting("Version.Commit")})`
    VersionText.TextScaled = true
    VersionText.TextColor3 = Color3.fromRGB(255, 255, 255)
    VersionText.TextStrokeColor3 = Color3.fromRGB(0, 0, 0)
    VersionText.TextStrokeTransparency = 0
    VersionText.Parent = Container

    --Return the object.
    return SettingsView
end

--[[
Popuulates a setting frame.
--]]
function SettingsView.PopulateSettingsFrame(self: SettingsView, ContainerFrame: Frame, HeaderName: string, GetOptionsSettings: string | () -> ({string}), GetValueFunction: () -> (string), SetValueFunction: (string) -> ()?): ()
    --Converrt the GetOptionsSettings callback if it is a string.
    local OptionsSetting = nil
    if typeof(GetOptionsSettings) == "string" then
        OptionsSetting = GetOptionsSettings
        GetOptionsSettings = function()
            return Settings:GetSetting(OptionsSetting) or {}
        end
    end

    --Create the frames.
    local LeftButton,LeftText = TextButtonFactory:Create()
    LeftButton.Size = UDim2.new(1, 0, 1, 0)
    LeftButton.Position = UDim2.new(0, 0, 0, 0)
    LeftButton.SizeConstraint = Enum.SizeConstraint.RelativeYY
    LeftButton.Parent = ContainerFrame
    LeftText.Text = "<"

    local RightButton,RightText = TextButtonFactory:Create()
    RightButton.AnchorPoint = Vector2.new(1, 0)
    RightButton.Size = UDim2.new(1, 0, 1, 0)
    RightButton.Position = UDim2.new(1, 0, 0, 0)
    RightButton.SizeConstraint = Enum.SizeConstraint.RelativeYY
    RightButton.Parent = ContainerFrame
    RightText.Text = ">"

    local OptionHeaderText = Instance.new("TextLabel")
    OptionHeaderText.BackgroundTransparency = 1
    OptionHeaderText.Size = UDim2.new(0.8, 0, 0.5, 0)
    OptionHeaderText.Position = UDim2.new(0.1, 0, -0.0125, 0)
    OptionHeaderText.Font = Enum.Font.SourceSansBold
    OptionHeaderText.Text = HeaderName
    OptionHeaderText.TextScaled = true
    OptionHeaderText.TextColor3 = Color3.fromRGB(255, 255, 255)
    OptionHeaderText.TextStrokeColor3 = Color3.fromRGB(0, 0, 0)
    OptionHeaderText.TextStrokeTransparency = 0
    OptionHeaderText.Parent = ContainerFrame

    local OptionText = Instance.new("TextLabel")
    OptionText.BackgroundTransparency = 1
    OptionText.Size = UDim2.new(0.6, 0, 0.7, 0)
    OptionText.Position = UDim2.new(0.2, 0, 0.3, 0)
    OptionText.Font = Enum.Font.SourceSansBold
    OptionText.TextScaled = true
    OptionText.TextColor3 = Color3.fromRGB(255, 255, 255)
    OptionText.TextStrokeColor3 = Color3.fromRGB(0, 0, 0)
    OptionText.TextStrokeTransparency = 0
    OptionText.Parent = ContainerFrame

    --[[
    Updates the settings.
    --]]
    local function UpdateSettings(Increment: number?): ()
        --Get the current value id.
        local InitialValueName = GetValueFunction()
        local CurrentValue = 1
        local Options = (GetOptionsSettings :: () -> ({string}))()
        for i,Option in Options do
            if Option == InitialValueName then
                CurrentValue = i
                break
            end
        end

        --Increment the value.
        if Increment and Increment ~= 0 then
            CurrentValue = CurrentValue + Increment
            if CurrentValue <= 0 then
                CurrentValue = #Options
            end
            if CurrentValue > #Options then
                CurrentValue = 1
            end
        end

        --Update the button visibility.
        LeftButton.Visible = (#Options > 1)
        RightButton.Visible = (#Options > 1)

        --Update the display text.
        OptionText.Text = Options[CurrentValue] or "(N/A)"

        --Set the new value.
        if Increment and Increment ~= 0 and Options[CurrentValue] then
            (SetValueFunction :: (string) -> ())(Options[CurrentValue])
        end
    end

    --Connect the events.
    local DB = true
    if OptionsSetting then
        Settings:GetSettingsChangedSignal(OptionsSetting):Connect(UpdateSettings)
    end
    LeftButton.MouseButton1Down:Connect(function()
        if not DB then return end
        DB = false
        UpdateSettings(-1)
        task.wait()
        DB = true
    end)
    RightButton.MouseButton1Down:Connect(function()
        if not DB then return end
        DB = false
        UpdateSettings(1)
        task.wait()
        DB = true
    end)

    --Update the initial settings.
    UpdateSettings()
end

return SettingsView


--Serializes and deserializes buffers for Nexus Buffered Replication.
--!strict

local BUFFER_PROTOCOL_FIELDS = {
    --Normal.
    {
        Name = "UpdateTime",
        Type = "number",
    },
    {
        Name = "CurrentWalkspeed",
        Type = "number",
    },
    {
        Name = "HeadCFrame",
        Type = "CFrame",
    },
    {
        Name = "LeftHandCFrame",
        Type = "CFrame",
    },
    {
        Name = "RightHandCFrame",
        Type = "CFrame",
    },

    --Enigma.
    {
        Name = "LeftFootCFrame",
        Type = "CFrame",
    },
    {
        Name = "RightFootCFrame",
        Type = "CFrame",
    },
}
local FIELD_TYPE_TO_LENGTH = {
    ["number"] = 8,
    ["CFrame"] = 7 * 4,
}

local BufferProtocol = {}

export type BufferProtocolData = {
    --Normal.
    UpdateTime: number,
    CurrentWalkspeed: number?,
    HeadCFrame: CFrame,
    LeftHandCFrame: CFrame?,
    RightHandCFrame: CFrame?,

    --Enigma.
    LeftFootCFrame: CFrame?,
    RightFootCFrame: CFrame?,
}

--[[
Serializes a request to a buffer.
--]]
function BufferProtocol.Serialize(Data: BufferProtocolData): buffer
    --Determine the fields to write.
    local FieldsToSerialize = {}
    local SerializedLength = 1
    for i, FieldData in BUFFER_PROTOCOL_FIELDS do
        local FieldValue = Data[FieldData.Name]
        if not FieldValue or typeof(FieldValue) ~= FieldData.Type then continue end
        local FieldLength = FIELD_TYPE_TO_LENGTH[FieldData.Type]
        SerializedLength += 1 + FIELD_TYPE_TO_LENGTH[FieldData.Type]
        table.insert(FieldsToSerialize, {
            Index = i,
            Length = FieldLength,
            Data = FieldValue,
        })
    end

    --Write the fields.
    local Buffer = buffer.create(SerializedLength)
    local Offset = 1
    buffer.writei8(Buffer, 0, #FieldsToSerialize)
    for _, Field in FieldsToSerialize do
        --Write the field type.
        buffer.writei8(Buffer, Offset, Field.Index)
        Offset += 1

        --Write the field data.
        local FieldValue = Field.Data
        local FieldType = typeof(FieldValue)
        if FieldType == "number" then
            buffer.writef64(Buffer, Offset, FieldValue)
        elseif FieldType == "CFrame" then
            local Position = FieldValue.Position
            local AxisAngleVector3, AxisAngleRotation = FieldValue:ToAxisAngle()
            buffer.writef32(Buffer, Offset, Position.X)
            buffer.writef32(Buffer, Offset + 4, Position.Y)
            buffer.writef32(Buffer, Offset + 8, Position.Z)
            buffer.writef32(Buffer, Offset + 12, AxisAngleVector3.X)
            buffer.writef32(Buffer, Offset + 16, AxisAngleVector3.Y)
            buffer.writef32(Buffer, Offset + 20, AxisAngleVector3.Z)
            buffer.writef32(Buffer, Offset + 24, AxisAngleRotation)
        end
        Offset += Field.Length
    end
    return Buffer
end

--[[
Deserializes a section of a buffer.
--]]
function BufferProtocol.DeserializeSection(Buffer: buffer, StartOffset: number): (BufferProtocolData, number)
    local Data = {} :: BufferProtocolData
    local InitialOffset = StartOffset
    local TotalFields = buffer.readi8(Buffer, StartOffset)
    StartOffset += 1
    for i = 1, TotalFields do
        local FieldData = BUFFER_PROTOCOL_FIELDS[buffer.readi8(Buffer, StartOffset)]
        StartOffset += 1
        if FieldData.Type == "number" then
            Data[FieldData.Name] = buffer.readf64(Buffer, StartOffset)
            StartOffset += 8
        elseif FieldData.Type == "CFrame" then
            local Position = CFrame.new(buffer.readf32(Buffer, StartOffset), buffer.readf32(Buffer, StartOffset + 4), buffer.readf32(Buffer, StartOffset + 8))
            local AxisAngleVector3, AxisAngleRotation = Vector3.new(buffer.readf32(Buffer, StartOffset + 12), buffer.readf32(Buffer, StartOffset + 16), buffer.readf32(Buffer, StartOffset + 20)), buffer.readf32(Buffer, StartOffset + 24)
            Data[FieldData.Name] = Position * CFrame.fromAxisAngle(AxisAngleVector3, AxisAngleRotation)
            StartOffset += 7 * 4
        end
    end
    return Data, StartOffset - InitialOffset
end

--[[
Deserializess data for players.
--]]
function BufferProtocol.Deserialize(Buffer: buffer): {[number]: BufferProtocolData}
    local BufferLength = buffer.len(Buffer)
    local CurrentIndex = 0
    local Data = {}
    while CurrentIndex < BufferLength do
        local UserId = buffer.readf64(Buffer, CurrentIndex)
        CurrentIndex += 8
        local PlayerData, DataRead = BufferProtocol.DeserializeSection(Buffer, CurrentIndex)
        Data[UserId] = PlayerData
        CurrentIndex += DataRead
    end
    return Data
end

return BufferProtocol


--Helper function that ray casts to find a collidable part.
--!strict

local Workspace = game:GetService("Workspace")

return function(StartPosition: Vector3, Direction: Vector3, IgnoreList: Instance | {Instance}?, CollisionGroup: string?): (BasePart?, Vector3)
    --Convert the collision group.
    if typeof(CollisionGroup) == "Instance" and CollisionGroup:IsA("BasePart") then
        CollisionGroup = CollisionGroup.CollisionGroup
    end

    --Create the parameters.
    local RaycastParameters = RaycastParams.new()
    RaycastParameters.FilterType = Enum.RaycastFilterType.Exclude
    RaycastParameters.FilterDescendantsInstances = {Workspace.CurrentCamera}
    if IgnoreList then
        RaycastParameters:AddToFilter(IgnoreList)
    end
    RaycastParameters.IgnoreWater = true
    if CollisionGroup then
        RaycastParameters.CollisionGroup = CollisionGroup
    end

    while true do
        --Raycast and try again if the hit part isn't collidable.
        local RaycastResult = Workspace:Raycast(StartPosition, Direction, RaycastParameters)
        if not RaycastResult then
            return nil, StartPosition + Direction
        end
        local HitPart,EndPosition = RaycastResult.Instance, RaycastResult.Position
        if HitPart and not HitPart.CanCollide and (not HitPart:IsA("Seat") or not HitPart:IsA("VehicleSeat") or HitPart.Disabled) then
            RaycastParameters:AddToFilter(HitPart)
            continue
        end

        --Return the hit result.
        return HitPart, EndPosition
    end
end


--Displays warnings when loading Nexus VR Character Model.
--!strict

local HttpService = game:GetService("HttpService")

return function(): ()
    --Load the configuration.
    local ConfigurationValue = script.Parent.Parent:WaitForChild("Configuration")
    local Configuration = HttpService:JSONDecode(ConfigurationValue.Value)

    --Determine the suppressed warnings.
    local SupressedWarnings = {}
    if Configuration.Output and Configuration.Output.SuppressWarnings then
        for _, WarningName in Configuration.Output.SuppressWarnings do
            SupressedWarnings[string.lower(WarningName)] = true
        end
    end
    if SupressedWarnings["all"] then return end

    --Build the warnings.
    local Warnings = {
        {
            Key = "MissingNexusVRBackpackEnabled",
            Message = "The configuration entry Extra.NexusVRBackpackEnabled is missing (defaults to true).",
            Condition = function()
               return Configuration.Extra == nil or Configuration.Extra.NexusVRBackpackEnabled == nil
            end,
        },
        {
            Key = "MissingAllowClientToOutputLoadedMessage",
            Message = "The configuration entry Extra.AllowClientToOutputLoadedMessage is missing (defaults to true).",
            Condition = function()
               return Configuration.Output == nil or Configuration.Output.AllowClientToOutputLoadedMessage == nil
            end,
        },
        {
            Key = "MissingDisableHeadLocked",
            Message = "The configuration entry Camera.DisableHeadLocked is missing (defaults to true).",
            Condition = function()
               return Configuration.Camera == nil or Configuration.Camera.DisableHeadLocked == nil
            end,
        },
    }

    --Output the warnings.
    for _, Warning in Warnings do
        if not SupressedWarnings[string.lower(Warning.Key)] and Warning.Condition() then
            warn(Warning.Message)
            warn(`\tThis warning can be disabled by adding \"{Warning.Key}\" or \"All\" to Output.SuppressWarnings in the configuration of Nexus VR Character Model.`)
        end
    end
end


--Loads Nexus VR Character Model.
--!strict

--Version information should be set right before distribution (not committed).
local VERSION_TAG = "dev"
local VERSION_COMMIT = "00000000"

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

local NexusBufferedReplication = require(script:WaitForChild("Packages"):WaitForChild("NexusBufferedReplication"))
local Settings = require(script:WaitForChild("State"):WaitForChild("Settings")).GetInstance()
local BufferProtocol = require(script:WaitForChild("Util"):WaitForChild("BufferProtocol"))

local BufferedRemoteEventSender = NexusBufferedReplication.Sender.BufferedRemoteEventSender
local EnrollableRemoteEvent = NexusBufferedReplication.Sender.EnrollableRemoteEvent

local NexusVRCharacterModel = {}

export type NexusVRCharacterModel = typeof(NexusVRCharacterModel)

--[[
Sets the configuration to use. Intended to be
run once by the server.
--]]
function NexusVRCharacterModel.SetConfiguration(self: NexusVRCharacterModel, Configuration: any): ()
    --Create the value.
    local ConfigurationValue = script:FindFirstChild("Configuration") :: StringValue
    if not ConfigurationValue then
        local NewConfigurationValue = Instance.new("StringValue")
        NewConfigurationValue.Name = "Configuration"
        NewConfigurationValue.Parent = script
        ConfigurationValue = NewConfigurationValue
    end

    --Add the version.
    local HideVersion = false
    if Configuration.Extra then
        HideVersion = (Configuration.Extra.HideVersion == true)
    end
    if not Configuration.Version then
        Configuration.Version = {}
    end
    if not Configuration.Version.Tag then
        Configuration.Version.Tag = (HideVersion and "Hidden" or VERSION_TAG)
    end
    if not Configuration.Version.Commit then
        Configuration.Version.Commit = (HideVersion and "Hidden" or VERSION_COMMIT)
    end

    --Store the configuration.
    ConfigurationValue.Value = HttpService:JSONEncode(Configuration)
    Settings:SetDefaults(Configuration)
end

--[[
Loads Nexus VR Character Model.
--]]
function NexusVRCharacterModel.Load(self: NexusVRCharacterModel): ()
    --Return if a version is already loaded.
    if ReplicatedStorage:FindFirstChild("NexusVRCharacterModel") then
        return
    end

    --Rename and move the script to ReplicatedStorage.
    script.Name = "NexusVRCharacterModel"
    script.Parent = ReplicatedStorage

    --Copy Nexus VR Core.
    --This is a bit hacky, but the only good option in Wally.
    if not ReplicatedStorage:FindFirstChild("NexusVRCore") then
        local Packages = script:FindFirstChild("Packages")
        if Packages then
            local NexusVRCore = Packages:FindFirstChild("nexus-vr-core", true)
            local NexusInstance = Packages:FindFirstChild("nexus-instance", true)
            if NexusVRCore and NexusInstance then
                local NewNexusVRCore = NexusVRCore:Clone()
                NewNexusVRCore.Name = "NexusVRCore"
                NewNexusVRCore:WaitForChild("Packages"):WaitForChild("NexusInstance"):Destroy()

                local NewNexusInstance = NexusInstance:Clone()
                NewNexusInstance.Name = "NexusInstance"
                NewNexusInstance.Parent = NewNexusVRCore:WaitForChild("Packages")
                NewNexusVRCore.Parent = ReplicatedStorage
            end
        end
    end

    --Output any warnings.
    (require(ReplicatedStorage:WaitForChild("NexusVRCharacterModel"):WaitForChild("Util"):WaitForChild("Warnings")) :: any)()

    --Set up the client scripts.
    script:WaitForChild("NexusVRCharacterModelClientLoader").Parent = ReplicatedStorage

    --Set up replication.
    local UpdateInputsEvent = Instance.new("UnreliableRemoteEvent")
    UpdateInputsEvent.Name = "UpdateInputs"
    UpdateInputsEvent.Parent = script

    local ReplicationReadyEvent = Instance.new("RemoteEvent")
    ReplicationReadyEvent.Name = "ReplicationReady"
    ReplicationReadyEvent.Parent = script

    local EnrollableUpdateInputsEvent = EnrollableRemoteEvent.new(UpdateInputsEvent)
    local UpdateInputsBufferedRemoteEventSender = BufferedRemoteEventSender.WithPlayerKeys(EnrollableUpdateInputsEvent :: any, function(Data: BufferProtocol.BufferProtocolData)
        return BufferProtocol.Serialize(Data)
    end)
    UpdateInputsBufferedRemoteEventSender:StartDataSendingWithDelay(1 / 30)

    UpdateInputsEvent.OnServerEvent:Connect(function(Player, Data: BufferProtocol.BufferProtocolData)
        if typeof(Data) ~= "table" then return end
        UpdateInputsBufferedRemoteEventSender:QueueData(Player, Data)
    end)

    ReplicationReadyEvent.OnServerEvent:Connect(function(Player)
        EnrollableUpdateInputsEvent:EnrollPlayer(Player)
    end)

    --Load Nexus VR Backpack.
    if Settings:GetSetting("Extra.NexusVRBackpackEnabled") ~= false then
        (require(10728805649) :: any)()
    end
end

NexusVRCharacterModel.Api = (require(script:WaitForChild("Api")) :: any)()
return NexusVRCharacterModel


export type vector = Vector3

declare vector: {
	create: (x: number, y: number, z: number) -> vector,
	magnitude: (vec: vector | Vector3) -> number,
	normalize: (vec: vector | Vector3) -> vector,
	cross: (vec1: vector | Vector3, vec2: vector | Vector3) -> vector,
	dot: (vec1: vector | Vector3, vec2: vector | Vector3) -> number,
	angle: (vec1: vector | Vector3, vec2: vector | Vector3, axis: (vector | Vector3)?) -> number,
	floor: (vec: vector | Vector3) -> vector,
	ceil: (vec: vector | Vector3) -> vector,
	abs: (vec: vector | Vector3) -> vector,
	sign: (vec: vector | Vector3) -> vector,
	clamp: (vec: vector | Vector3, min: vector | Vector3, max: vector | Vector3) -> vector,
	max: (...vector) -> vector,
	min: (...vector) -> vector,

	zero: vector,
	one: vector,
}


local colorful = require("@vendor/lunePackages/colorful")

local style = colorful.combineStyles
local colors = colorful.color
local modifiers = colorful.modifier

return {
	header = style({ colors.blue, modifiers.bold }),
	detail = style({ colors.white, modifiers.dim }),

	success = style({ colors.green, modifiers.bold }),
	error = style({ colors.red, modifiers.bold }),
	warning = style({ colors.yellow, modifiers.bold }),
}


local async = require("@vendor/lunePackages/async")
local process = require("@lune/process")
local spawnProcess = require("@vendor/lunePackages/spawnProcess")
local spawnSourcemap = require("@ext/spawnSourcemap")
local styles = require("./data/styles")

return function()
	print(styles.header("Installing Wally dependencies"))
	async(spawnProcess("wally", { "install" }))

	print(styles.header("Building sourcemap"))

	async(spawnSourcemap(false))

	print(styles.header("Exporting types"))

	process.spawn("wally-package-types", { "--sourcemap", "sourcemap.json", "Packages" }, {
		stdio = "forward",
	})

	print("\n" .. styles.success("Done!"))
end


local fs = require("@lune/fs")

local function reconcileDirectory(path: string)
	if not fs.isDir(path) then
		fs.writeDir(path)
	end
end

return function()
	reconcileDirectory("build/")
	reconcileDirectory("build/package/")
	reconcileDirectory("build/tests/")
end


local spawnProcess = require("@vendor/lunePackages/spawnProcess")

return function()
	spawnProcess("darklua", {
		"process",
		"src/",
		"build/package/",
		"--watch",
	})

	spawnProcess("darklua", {
		"process",
		"tests/roblox/",
		"build/tests/",
		"--watch",
	})
end


local spawnProcess = require("@vendor/lunePackages/spawnProcess")

return function(watch: boolean?): thread
	local parameters = {
		"sourcemap",
		"test-source.project.json",
		"--output",
		"sourcemap.json",
	}

	if watch then table.insert(parameters, "--watch") end

	return spawnProcess("rojo", parameters)
end


local async = require("@vendor/lunePackages/async")
local colorful = require("@vendor/lunePackages/colorful")
local spawnDarklua = require("@ext/spawnDarklua")
local spawnProcess = require("@vendor/lunePackages/spawnProcess")
local spawnSourcemap = require("@ext/spawnSourcemap")
local task = require("@lune/task")
local process = require("@lune/process")

local function getargs(): {
	timetracing: boolean,
	verbose: boolean,
	testmode: "benchmark" | "dev",
}
	local args = process.args
	return {
		timetracing = table.find(args, "timetracing") ~= nil,
		verbose = table.find(args, "verbose") ~= nil,
		testmode = if table.find(args, "benchmark") ~= nil then "benchmark" else "dev",
	}
end

local args = getargs()
process.env["timetracing"] = tostring(args.timetracing)
process.env["verbose"] = tostring(args.verbose)
process.env["testmode"] = args.testmode

print(colorful.color.blueBright("Starting development server"))

spawnSourcemap(true)

-- avoid sourcemap dependency issues
task.wait(0.5)

spawnDarklua()

async(spawnProcess("rojo", {
	"serve",
	"test-build.project.json",
}))


local installGitDependencies = require("@ext/installGitDependencies")
local installWallyDependencies = require("@ext/installWallyDependencies")
local process = require("@lune/process")

if #process.args == 0 then
	installGitDependencies()
	installWallyDependencies()
elseif process.args[1] == "git" then
	installGitDependencies()
elseif process.args[1] == "wally" then
	installWallyDependencies()
end


local colorful = require("@vendor/lunePackages/colorful")
local fs = require("@lune/fs")
local luau = require("@lune/luau")
local process = require("@lune/process")
local roblox = require("@lune/roblox")
local serde = require("@lune/serde")
local task = require("@lune/task")

local tests = fs.readDir("tests/lune/")
local luaurc = serde.decode("json", fs.readFile(".luaurc"))
local Vector3 = ((roblox :: any).Vector3) :: typeof(Vector3)
local CFrame = (roblox :: any).CFrame :: typeof(CFrame)

local testCases = {}

function injectedRequire(path: string): any
	if string.sub(path, 1, 5) == "@lune" then
		return require(path) :: any
	end

	local workingPath = ""
	local parts = string.split(path, "/")

	if string.sub(parts[1], 1, 1) == "@" then
		workingPath = luaurc.aliases[string.sub(parts[1], 2, -1)]
		table.remove(parts, 1)

		workingPath ..= table.concat(parts, "/")

		local filePath = ""
		if fs.isDir(workingPath) then
			filePath = workingPath .. "/init.luau"
		else
			filePath = workingPath .. ".luau"
		end

		local compiled = luau.compile(fs.readFile(filePath))
		local loaded = luau.load(compiled, {
			environment = env(),
		})

		process.env["__currentModule"] = workingPath
		local result = loaded()
		process.env["__currentModule"] = nil

		return result
	else
		assert(process.env["__currentModule"], "Cannot require non-aliased modules outside of a module context")

		local currentModule = process.env["__currentModule"]

		local source = fs.readFile(currentModule .. path .. ".luau")

		local compiled = luau.compile(source)
		local loaded = luau.load(compiled, {
			environment = env(),
		})

		process.env["__currentModule"] = currentModule .. path
		local result = loaded()
		process.env["__currentModule"] = currentModule

		return result
	end
end

function env()
	return {
		Vector3 = Vector3,
		task = task,
		CFrame = CFrame,

		require = injectedRequire,
	}
end

for _, fileName in tests do
	testCases[fileName] = luau.load(luau.compile(fs.readFile(`tests/lune/{fileName}`)), {
		environment = env(),
	})
end

for fileName, runner in testCases do
	local results = runner()
	print(colorful.modifier.bold(fileName))

	for _, case in results do
		case()
	end
end


local ClientBridge = {}
local metatable = { __index = ClientBridge }
export type Identity = typeof(setmetatable({} :: {}, metatable))

local function constructor(name: string): Identity
	local self = setmetatable({}, metatable)

	return self
end

function ClientBridge.Fire() end

function ClientBridge.Connect() end

function ClientBridge.InvokeServer() end

function ClientBridge.OutboundMiddleware() end

function ClientBridge.InboundMiddleware() end

function ClientBridge.Wait() end

function ClientBridge.Once() end

return {
	new = constructor,
}


local dataModelTree = require("@src/dataModelTree")
local logStrings = require("@src/logStrings")
local logger = require("@src/logger").new()
local identifierMap = require("@src/identifierMap")

local client = {}

function client.init(tree: dataModelTree.Identity)
	for name, id in tree.identifiers:GetAttributes() do
		if typeof(id) ~= "number" then
			logger:warn(string.format(logStrings.errors.ID_PROPERTY_INCORRECT_TYPE, typeof(id)))
			continue
		end

		identifierMap.addIdentifier(name, id)
	end

	tree.identifiers.AttributeChanged:Connect(function(name)
		local id = tree.identifiers:GetAttribute(name)

		if typeof(id) == "number" then
			-- Added case
			identifierMap.addIdentifier(name, id)
		elseif not id then
			-- Removed case
			identifierMap.removeIdentifier(name)
		end
	end)
end

return client


local logger = require("@src/logger").new("client (parallel)")
local logStrings = require("@src/logStrings")
local identifierMap = require("@src/identifierMap")
local dataModelTree = require("@src/dataModelTree")

local tree: dataModelTree.Identity

local function disallowed(name: string): never
	return logger:fatal(string.format(logStrings.errors.PARALLEL_DISALLOWED, name))
end

local ClientBridge = {}
local metatable = { __index = ClientBridge }
export type Identity = typeof(setmetatable(
	{} :: {
		_id: number,
	},
	metatable
))

local function constructor(name: string, timeout: number?): Identity
	if not tree then
		return logger:fatal(logStrings.errors.ACCESS_WITHOUT_INITIALIZATION)
	end

	local self = setmetatable({}, metatable)

	local start = os.clock()
	while (not identifierMap.fromName(name)) and (os.clock() - start <= (timeout or 1)) do
		task.wait()
	end

	local id = identifierMap.fromName(name)
	if not id then
		return logger:fatal(logStrings.errors.ID_TIMEOUT_EXHAUSTED)
	end

	self._id = id

	return self
end

function ClientBridge.Fire(self: Identity, value: any)
	tree.parallel:Fire(self._id, value)
end

function ClientBridge.Connect(self: Identity) end

function ClientBridge.InvokeServer()
	disallowed("ParallelClientBridge:InvokeServer")
end

function ClientBridge.OutboundMiddleware()
	disallowed("ParallelClientBridge:OutboundMiddleware")
end

function ClientBridge.InboundMiddleware()
	disallowed("ParallelClientBridge:InboundMiddleware")
end

function ClientBridge.Wait()
	disallowed("ParallelClientBridge:Wait")
end

function ClientBridge.Once()
	disallowed("ParallelClientBridge:Once")
end

return {
	new = constructor,
	tree = function(passed: dataModelTree.Identity)
		tree = passed
	end,
}


local RunService = game:GetService("RunService")

local serializer = require("@src/core/serializer")
local deserializer = require("@src/core/deserializer")
local types = require("@src/types")
local dataModelTree = require("@src/dataModelTree")
local logger = require("@src/logger").new("main client process")
local logStrings = require("@src/logStrings")

local replicationRate: number = 1 / 61
local lastReplicationTick: number = 0
local queue: types.Queue = {}
local remote: RemoteEvent = nil
local metaRemote: RemoteEvent = nil
local fireServer = Instance.new("RemoteEvent").FireServer

local function _assertRemote(func: string): ()
	if not (metaRemote and remote) then
		return logger:fatal(string.format(logStrings.errors.ACCESS_WITHOUT_INITIALIZATION, func))
	end
end

local client = {}

function client.init(tree: dataModelTree.Identity)
	remote = tree.remote
	metaRemote = tree.meta

	tree.parallel.Event:Connect(client.addEventCall)
	RunService.PostSimulation:Connect(client.step)
	remote.OnClientEvent:Connect(function(packed)
		deserializer.decode(packed[1], packed[2])
	end)

	metaRemote:FireServer("\0")
end

function client.setReplicationRate(newRate: number)
	replicationRate = 1 / newRate
end

function client.addEventCall(id: number, content: unknown)
	table.insert(queue, id)
	table.insert(queue, content)
end

function client.step(deltaTime: number)
	if ((os.clock() - lastReplicationTick) >= replicationRate) and (#queue > 0) then
		local serialized = serializer(queue)
		fireServer(remote, { serialized.events :: any, serialized.buff :: any } :: any)
		table.clear(queue)

		lastReplicationTick = os.clock()
	end
end

return client


--!native
local signal = require("@pkg/signal")

local idToSignal: { [number]: signal.Identity<...any> } = {}

local deserializer = {}

function deserializer.exists(id: number)
	return idToSignal[id] ~= nil
end

function deserializer.registerId(id: number): signal.Identity<...any>
	idToSignal[id] = signal()
	return idToSignal[id]
end

function deserializer.fetchEventSignal(id: number): signal.Identity<...any>
	return idToSignal[id]
end

function deserializer.decode(events: { [number]: { unknown } }, buff: buffer, player: Player?)
	if _G.__timetracing__ == "true" then
		debug.profilebegin("event begin")
	end

	if _G.__verbose__ == "true" then
		print("remote call", player, events, buff)
	end

	-- it's duplicate but its best performing so
	if player then
		-- copypaste this one though
		for index, calls in events do
			local id = buffer.readu8(buff, index - 1)
			local eventSignal = idToSignal[id]
			if not eventSignal then
				warn("todo: handle this case")
				continue
			end

			for _, value in calls do
				-- Event logic
				eventSignal:fire(player, value)
			end
		end
	else
		for index, calls in events do
			local id = buffer.readu8(buff, index - 1)
			local eventSignal = idToSignal[id]
			if not eventSignal then
				warn("todo: handle this case")
				continue
			end

			for _, value in calls do
				-- Event logic
				eventSignal:fire(value)
			end
		end
	end

	if _G.__timetracing__ then
		debug.profilebegin("server event end")
	end
end

return deserializer


--!native
local types = require("@src/types")

local function serializer(queue: types.Queue): types.SerializedPacket
	local outgoingEvents: types.SerializedPacket = {
		events = {},
		buff = buffer.create(0),
	}

	local idToIndex: { number } = {}
	local indexToId: { number } = {}

	for i = 1, #queue, 2 do
		local id = queue[i] :: number
		local content = queue[i + 1] :: Content
		local index = idToIndex[id]

		if index then
			table.insert(outgoingEvents.events[index], content)
		else
			table.insert(indexToId, id)
			index = #indexToId
			idToIndex[id] = index

			outgoingEvents.events[index :: any] = { content }
		end
	end

	local buff = buffer.create(#indexToId)
	for index, id in indexToId do
		buffer.writeu8(buff, index - 1, id)
	end

	outgoingEvents.buff = buff

	return outgoingEvents
end

return serializer


local WFC_TIMEOUT = 3

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

local result = require("@src/util/result")
local logStrings = require("@src/logStrings")

local cached: Identity? = nil

export type Identity = {
	container: Folder,
	remote: RemoteEvent,
	meta: RemoteEvent,
	parallel: BindableEvent,
	parallelConnections: Camera,

	identifiers: Folder,
}

local function create(): Identity
	local container = Instance.new("Folder")
	local remote = Instance.new("RemoteEvent")
	local meta = Instance.new("RemoteEvent")
	local identifiers = Instance.new("Folder")
	local parallel = Instance.new("BindableEvent")
	local parallelConnections = Instance.new("Camera")

	identifiers.Name = "identifiers"
	meta.Name = "meta"
	remote.Name = "remote"
	container.Name = "bridgenet2"
	parallel.Name = "parallel"
	parallelConnections.Name = "parallelConnections"

	identifiers.Parent = container
	meta.Parent = container
	remote.Parent = container
	parallel.Parent = container
	parallelConnections.Parent = container

	container.Parent = ReplicatedStorage

	local tree = {
		container = container,
		remote = remote,
		meta = meta,
		parallel = parallel,
		identifiers = identifiers,
		parallelConnections = parallelConnections,
	}
	cached = tree

	return tree
end

local function find(): result.Identity<Identity>
	local container = ReplicatedStorage:WaitForChild("bridgenet2", WFC_TIMEOUT)
	if not container then
		return result(false, logStrings.errors.INSTANCE_NOT_FOUND)
	end

	if not container:IsA("Folder") then
		return result(false, logStrings.errors.INSTANCE_NOT_VALID)
	end

	local remote = container:WaitForChild("remote", WFC_TIMEOUT)
	if not (remote and remote:IsA("RemoteEvent")) then
		return result(false, logStrings.errors.INSTANCE_NOT_VALID)
	end

	local meta = container:WaitForChild("meta", WFC_TIMEOUT)
	if not (meta and meta:IsA("RemoteEvent")) then
		return result(false, logStrings.errors.INSTANCE_NOT_VALID)
	end

	local parallel = container:WaitForChild("parallel", WFC_TIMEOUT)
	if not (parallel and parallel:IsA("BindableEvent")) then
		return result(false, logStrings.errors.INSTANCE_NOT_VALID)
	end

	local identifiers = container:WaitForChild("identifiers", WFC_TIMEOUT)
	if not (identifiers and identifiers:IsA("Folder")) then
		return result(false, logStrings.errors.INSTANCE_NOT_VALID)
	end

	local parallelConnections = container:WaitForChild("parallelConnections", WFC_TIMEOUT)
	if not (parallelConnections and parallelConnections:IsA("Camera")) then
		return result(false, logStrings.errors.INSTANCE_NOT_VALID)
	end

	local tree = {
		container = container,
		remote = remote,
		parallel = parallel,
		meta = meta,
		parallelConnections = parallelConnections,
		identifiers = identifiers,
	}
	cached = tree

	return result(true, tree)
end

return function(fresh: boolean?): result.Identity<Identity>
	if cached then
		return result(true, cached)
	end

	if RunService:IsServer() then
		if fresh then
			return result(true, create())
		else
			return find()
		end
	else
		return find()
	end
end


local idToName: { [number]: string } = {}
local nameToId: { [string]: number } = {}

export type Identifier = number

local identifierMap = {}

function identifierMap.addIdentifier(name: string, id: Identifier)
	idToName[id] = name
	nameToId[name] = id
end

function identifierMap.removeIdentifier(name: string)
	local id = nameToId[name]

	idToName[id] = nil
	nameToId[name] = nil
end

function identifierMap.fromName(name: string): Identifier?
	return nameToId[name]
end

function identifierMap.fromId(id: Identifier): string?
	return idToName[id]
end

return identifierMap


local RunService = game:GetService("RunService")

local clientActor = require("@src/initialization/clientActor")
local serverActor = require("@src/initialization/serverActor")
local clientSerial = require("@src/initialization/clientSerial")
local serverSerial = require("@src/initialization/serverSerial")

local threadContext: "actor" | "serial" = if script:GetActor() ~= nil then "actor" else "serial"
local runContext: "server" | "client" = if RunService:IsServer() then "server" else "client"

if threadContext == "actor" and runContext == "client" then
	clientActor()
elseif threadContext == "actor" and runContext == "server" then
	serverActor()
elseif threadContext == "serial" and runContext == "client" then
	clientSerial()
elseif threadContext == "serial" and runContext == "server" then
	serverSerial()
end

return {}


local process = require("@src/client/process")
local identifiers = require("@src/server/identifiers")
local dataModelTree = require("@src/dataModelTree")
local logger = require("@src/logger").new("client")
local logStrings = require("@src/logStrings")

return function()
	local treeRes = dataModelTree()
	if not treeRes.success then
		logger:warn(string.format(logStrings.errors.TREE_FAILURE, treeRes.err))
		print(treeRes.trace)
		return logger:fatal("exiting bridgenet2")
	end

	process.init(treeRes.value)
	identifiers.init(treeRes.value)

	logger:log(logStrings.logs.SERIAL_LOADED)
end


local process = require("@src/server/process")
local identifiers = require("@src/server/identifiers")
local dataModelTree = require("@src/dataModelTree")
local logger = require("@src/logger").new("server")
local logStrings = require("@src/logStrings")

return function(): ()
	local treeRes = dataModelTree(true)
	if not treeRes.success then
		logger:warn(string.format(logStrings.errors.TREE_FAILURE, treeRes.err))
		print(treeRes.trace)
		return logger:fatal("exiting bridgenet2")
	end

	process.init(treeRes.value)
	identifiers.init(treeRes.value)

	logger:log(logStrings.logs.SERIAL_LOADED)
end


local errorStrings = {
	INSTANCE_NOT_FOUND = "couldn't find instances under replicatedstorage, waitforchild timeout hit",
	INSTANCE_NOT_VALID = "bridgenet2's internal instance tree wasnt valid, was it modified?",

	ID_PROPERTY_INCORRECT_TYPE = "identifier [%*] has an invalid attribute type, was it modified?",

	ACCESS_WITHOUT_INITIALIZATION = "function [%*] accessed without initialization",

	TREE_FAILURE = "tree failed to initialize: \n\t%*",
	SIGNAL_NOT_INITIALIZED = "tried to connect to event ID [%*], but it wasnt registered",

	PARALLEL_DISALLOWED = "cannot call function %* in parallel",

	ID_TIMEOUT_EXHAUSTED = "TODO error message",
}

local processed: { [any]: any } = {}
for errorName, errorText in pairs(errorStrings) do
	if errorName == "TREE_FAILURE" then
		processed[errorName] = errorText
		continue
	end

	processed[errorName] = `{errorText} [{errorName}]`
end

return {
	errors = processed :: typeof(errorStrings),
	logs = {
		SERIAL_LOADED = "v2.0.0 successfully loaded onto main thread",
	},
}


--!optimize 1
export type LogLevel = "trace" | "log" | "warn" | "fatal" | "halt"

local function getNameFromStack()
	local name = debug.info(3, "s")
	local split = string.split(name, ".")

	return `{split[#split - 1]}/{split[#split]}`
end

local logger = {}
local metatable = { __index = logger }
export type Identity = typeof(setmetatable(
	{} :: {
		_name: string,
	},
	metatable
))

local function constructor(name: string?): Identity
	local self = setmetatable({}, metatable)

	self._name = name or getNameFromStack()

	return self
end

function logger.log(self: Identity, text: string)
	print(`[bn2/{self._name}] {text}`)
end

function logger.warn(self: Identity, text: string)
	warn(`[bn2/{self._name}] {text}`)
end

function logger.fatal(self: Identity, text: string): never
	return error(`[bn2/{self._name}] {text}`, 0)
end

return {
	new = constructor,
}


local signal = require("@pkg/signal")
local process = require("@src/server/process")
local identifiers = require("@src/server/identifiers")
local identifierMap = require("@src/identifierMap")
local deserializer = require("@src/core/deserializer")

local ServerBridge = {}
local metatable = { __index = ServerBridge }
export type Identity = typeof(setmetatable(
	{} :: {
		_id: number,
		_signal: signal.Identity<...any>,
	},
	metatable
))

local function constructor(name: string): Identity
	local self = setmetatable({
		_id = identifierMap.fromName(name) or identifiers.register(name),
	}, metatable)

	self._signal = if deserializer.exists(self._id)
		then deserializer.fetchEventSignal(self._id)
		else deserializer.registerId(self._id)

	return self
end

function ServerBridge.Fire(self: Identity, player: Player, value: any)
	process.firePlayer(player, self._id, value)
end

function ServerBridge.FireAllExcept(self: Identity, exception: Player, value: any) end

function ServerBridge.FireGroup(self: Identity, players: { Player }, value: any)
	for _, player in players do
		process.firePlayer(player, self._id, value)
	end
end

function ServerBridge.FireAll(self: Identity)
	process.fireAll(self._id)
end

function ServerBridge.Connect(self: Identity, callback: (player: Player, data: unknown) -> ()): () -> ()
	return self._signal:connect(callback)
end

function ServerBridge.OnInvoke(self: Identity) end

function ServerBridge.OutboundMiddleware(self: Identity) end

function ServerBridge.InboundMiddleware(self: Identity) end

function ServerBridge.Wait(self: Identity) end

function ServerBridge.Once(self: Identity, callback: (player: Player, data: unknown) -> ()): () -> ()
	return self._signal:once(callback)
end

return {
	new = constructor,
}


local dataModelTree = require("@src/dataModelTree")
local logger = require("@src/logger").new("server identifiers")
local logStrings = require("@src/logStrings")
local identifierMap = require("@src/identifierMap")

local counter: Identifier = 1
local container: Folder

export type Identifier = number

local function assertContainer(func: string): ()
	if not container then
		return logger:fatal(string.format(logStrings.errors.ACCESS_WITHOUT_INITIALIZATION, func))
	end
end

local server = {}

function server.init(tree: dataModelTree.Identity)
	container = tree.container
end

function server.register(name: string): number
	assertContainer("register")

	counter += 1
	local chosenId = counter

	container:SetAttribute(name, chosenId)
	identifierMap.addIdentifier(name, counter)

	return counter
end

return server


--!native
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local dataModelTree = require("@src/dataModelTree")
local types = require("@src/types")
local serializer = require("@src/core/serializer")
local deserializer = require("@src/core/deserializer")

local remote: RemoteEvent
local playerQueues: { [Player]: types.Queue } = {}
local readyPlayers: { [Player]: true } = {}
local fireClient = Instance.new("RemoteEvent").FireClient

local server = {}

function server.init(tree: dataModelTree.Identity)
	tree.meta.OnServerEvent:Connect(function(player, str)
		if str == "\0" then
			readyPlayers[player] = true

			server.emptyPlayerQueue(player)

			if _G.__verbose__ then
				print(`player {player.Name} readied`)
			end
		end
	end)

	remote = tree.remote

	for _, player in Players:GetPlayers() do
		playerQueues[player] = {}
	end

	Players.PlayerAdded:Connect(function(player)
		playerQueues[player] = {}
	end)

	Players.PlayerRemoving:Connect(function(player)
		readyPlayers[player] = nil
		playerQueues[player] = nil
	end)

	RunService.PostSimulation:Connect(server.step)
	tree.remote.OnServerEvent:Connect(function(player, packed)
		deserializer.decode(packed[1], packed[2], player)
	end)
end

function server.firePlayer(player: Player, id: number, content: unknown)
	local queue = playerQueues[player]

	table.insert(queue, id)
	table.insert(queue, content)
end

function server.fireAll(id: number, content: unknown)
	for _, queue in playerQueues do
		table.insert(queue, id)
		table.insert(queue, content)
	end
end

function server.emptyPlayerQueue(player: Player)
	if #playerQueues[player] == 0 then
		return
	end

	if _G.__verbose__ == "true" then
		print(`emptying {player.Name} queue`)
	end

	local serialized = serializer(playerQueues[player])
	fireClient(remote, player, { serialized.events, serialized.buff :: any } :: any)
	table.clear(playerQueues[player])
end

function server.step()
	if _G.__timetracing__ == "true" then
		debug.profilebegin("server begin")
	end

	for player in readyPlayers do
		server.emptyPlayerQueue(player)
	end

	if _G.__timetracing__ == "true" then
		debug.profileend()
	end
end

return server


--[[
	Private
]]
export type Content = unknown
export type SerializedPacket = {
	events: {
		-- The numbers here don't cost anything, because it's an array.
		[number]: {
			-- Each value here is an individual call to an event.
			[number]: Content,
		},
	},

	-- The IDs are packed into this buffer
	buff: buffer,
}

-- { event, content, event, content, event, content }
export type Queue = { number | Content }

return nil


export type Identity<T> = {
	success: true,
	value: T,
} | {
	success: false,
	err: string,
	trace: string,
}

return (
	(
		function<T>(success: boolean, resultOrError: T & string): Identity<T>
			if success then
				return {
					success = true,
					value = resultOrError,
				}
			else
				return {
					success = false,
					trace = debug.traceback(),
					err = resultOrError,
				}
			end
		end
	) :: any
) :: (<T>(success: true, value: T) -> Identity<T>) & (<T>(success: false, err: string) -> Identity<T>)


if _G.__testmode__ == "benchmark" then
	return
end

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local _BridgeNet2 = require(ReplicatedStorage.Packages.BridgeNet2)
local clientProcess = require("@src/client/process")

clientProcess.registerEvent(1)

clientProcess.connect(1, print)


if _G.__testmode__ ~= "benchmark" then
	return
end

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local _BridgeNet2 = require(ReplicatedStorage.Packages.BridgeNet2)
local clientProcess = require("@src/client/process")

clientProcess.registerEvent(1)

clientProcess.connect(1, function(data) end)


if _G.__testmode__ == "benchmark" then
	return
end

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local _BridgeNet2 = require(ReplicatedStorage.Packages.BridgeNet2)
local serverProcess = require("@src/server/process")

serverProcess.addEvent(1)
serverProcess.connect(1, print)

while true do
	serverProcess.fireAll(1, "testing")

	task.wait(1)
end


if _G.__testmode__ ~= "benchmark" then
	return
end

print("running stress test")

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

local _BridgeNet2 = require(ReplicatedStorage.Packages.BridgeNet2)
local serverProcess = require("@src/server/process")

serverProcess.addEvent(1)
serverProcess.connect(1, print)

RunService.Heartbeat:Connect(function()
	debug.profilebegin("benchmark")
	for i = 1, 200 do
		serverProcess.fireAll(1, "")
	end
	debug.profileend()
end)


local function bufferToDecimal(buff: buffer): string
	return string.gsub(buffer.tostring(buff), ".", function(c)
		return string.format("%03i ", string.byte(c :: any))
	end)
end

local isPrimitiveType = { string = true, number = true, boolean = true, buffer = true }

local function isPrimitiveArray(array)
	local max, n = 0, 0

	for key, value in array do
		if typeof(key) ~= "number" then
			return false
		end
		if key <= 0 then
			return false
		end
		if not isPrimitiveType[typeof(value)] then
			return false
		end

		max = if key > max then key else max
		n = n + 1
	end

	return n == max
end

local function formatValue(value)
	if typeof(value) == "buffer" then
		return bufferToDecimal(value)
	end

	if typeof(value) ~= "string" then
		return tostring(value)
	end

	return string.format("%q", value)
end

local function formatKey(key, seq)
	if seq then
		return ""
	end

	if typeof(key) ~= "string" then
		return `[{tostring(key)}] =`
	end

	-- key is a simple identifier
	if key:match("^[%a_][%w_]-$") == key then
		return `{key} = `
	end

	return `[{string.format("%q", key)}] = `
end

local typeSortOrder = {
	["boolean"] = 1,
	["number"] = 2,
	["string"] = 3,
	["function"] = 4,
	["vector"] = 5,
	["buffer"] = 6,
	["thread"] = 7,
	["table"] = 8,
	["userdata"] = 9,
	["nil"] = 10,
}

local function traverseTable(dataTable, seen, indent)
	local output = ""

	local indentStr = string.rep("  ", indent)

	local keys = {}

	for key, value in dataTable do
		if isPrimitiveType[typeof(key)] then
			keys[#keys + 1] = key
		end
	end

	table.sort(keys, function(a, b)
		local typeofTableA, typeofTableB = typeof(dataTable[a]), typeof(dataTable[b])

		if typeofTableA ~= typeofTableB then
			return typeSortOrder[typeofTableA] < typeSortOrder[typeofTableB]
		end

		if typeof(a) == "number" and typeof(b) == "number" then
			return a < b
		end

		return tostring(a) < tostring(b)
	end)

	local inSequence = false
	local previousKey = 0

	for idx, key in keys do
		if typeof(key) == "number" and key > 0 and key - 1 == previousKey then
			previousKey = key
			inSequence = true
		else
			inSequence = false
		end

		local value = dataTable[key]
		if typeof(value) ~= "table" then
			if isPrimitiveType[typeof(value)] then
				output = `{output}{indentStr}{formatKey(key, inSequence)}{formatValue(value)},\n`
			end

			continue
		end

		-- prevents self-referential tables from looping infinitely
		if seen[value] then
			continue
		end

		seen[value] = true

		local hasItems = false
		for key, val in value do
			if isPrimitiveType[typeof(key)] and isPrimitiveType[typeof(val)] then
				hasItems = true
				break
			end

			if typeof(val) == "table" then
				hasItems = true
				break
			end
		end

		if not hasItems then
			output = string.format("%s%s%s{},\n", output, indentStr, formatKey(key, inSequence))
			continue
		end

		if isPrimitiveArray(value) then -- collapse primitive arrays
			output = string.format("%s%s%s{", output, indentStr, formatKey(key, inSequence))

			for index = 1, #value do
				output = output .. formatValue(value[index])
				if index < #value then
					output = output .. ", "
				end
			end

			output = output .. "},\n"
			continue
		end

		output = string.format(
			"%s%s%s{\n%s%s},\n",
			output,
			indentStr,
			formatKey(key, inSequence),
			traverseTable(value, seen, indent + 1),
			indentStr
		)

		seen[value] = nil
	end

	return output
end

local utils = {}

function utils.bufferFormatted(buff: buffer): string
	return bufferToDecimal(buff)
end

function utils.prettyPrint(data: any)
	if typeof(data) == "table" then
		print("{\n" .. traverseTable(data, { [data] = true }, 1) .. "}")
		return
	elseif typeof(data) == "buffer" then
		print(bufferToDecimal(data))
		return
	end

	print(tostring(data))
end

return utils


return {
	CHUNK_SIZE = 8,
	CHUNK_DISTANCE = 10,
	MAX_CHUNK_DISTANCE = 1000,

	MAX_CHUNK_LOADING_ACTORS = 24,

	BLOCK_SIZE = 3,
}