local Shared = script.Parent
local Promise = require(Shared.Promise)

local Players = game:GetService("Players")
local MarketplaceService = game:GetService("MarketplaceService")
type Promise = Promise.Class

local ownership = {} :: {
    [Player]: {
        [string]: Promise
    }
}

local productInfo = {} :: {
    [string]: Promise
}

export type ProductInfo = {
    AssetId: number,
    AssetTypeId: number,

    Creator: {
        Id: number,
        Name: string,
    },

    Description: string,
    IconImageAssetId: number,
    IsForSale: boolean,
    IsPublicDomain: boolean,
    Name: string,
    PriceInRobux: number,
    ProductId: number,
    ProductType: Enum.AssetType,
    Created: string,
    Updated: string,
}

local playerOwnsItem = Promise.promisify(function (player: Player, itemId: any, infoType: Enum.InfoType?)
    if infoType == Enum.InfoType.Subscription then
        local status = MarketplaceService:GetUserSubscriptionStatusAsync(player, itemId)
        return status.IsSubscribed
    elseif infoType == Enum.InfoType.GamePass then
        return MarketplaceService:UserOwnsGamePassAsync(player.UserId, itemId)
    elseif infoType == Enum.InfoType.Bundle then
        return MarketplaceService:PlayerOwnsBundle(player, itemId)
    elseif infoType == Enum.InfoType.Product then
        return false -- Products cannot be owned.
    else
        return MarketplaceService:PlayerOwnsAsset(player, itemId)
    end
end)

local getProductInfo = Promise.promisify(function (id: number, infoType: Enum.InfoType?)
    return MarketplaceService:GetProductInfo(id, infoType)
end)

local function promiseOwnership(player: Player, id: number, infoType: Enum.InfoType?): Promise
    local infoType = infoType or Enum.InfoType.Asset
    local playerOwnership = ownership[player]

    if not playerOwnership then
        playerOwnership = {}
        ownership[player] = playerOwnership
    end

    local key = `{infoType.Name}:{id}`
    local promise = playerOwnership[key]

    if not promise then
        local new = Promise.retry(playerOwnsItem, 5, player, id, infoType)
        playerOwnership[key] = new

        new:catch(function ()
            if playerOwnership[key] == new then
                playerOwnership[key] = nil
            end
        end)

        promise = new
    end

    return promise
end

local function promiseProductInfo(id: number, infoType: Enum.InfoType?): Promise
    local infoType = infoType or Enum.InfoType.Asset
    local key = `{infoType.Name}:{id}`
    local promise = productInfo[key]

    if not promise then
        local new = Promise.retry(getProductInfo, 5, id, infoType)
        productInfo[key] = new

        new:catch(function ()
            if productInfo[key] == new then
                productInfo[key] = nil
            end
        end)

        promise = new
    end

    return promise
end

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

return table.freeze({
    GetOwnership = promiseOwnership,
    GetProductInfo = promiseProductInfo
})


local Packages = game.ReplicatedStorage.Packages
local Shared = script.Parent

local RunContext = require(Shared.RunContext)
local Network: typeof(require(Packages.Red))

if RunContext.IsEdit then
    Network = require(script.mockNetwork) :: any
else
    Network = require(Packages.Red)
end

return Network


-- Edit-mode version of the Network module.
-- This dummies out the network calls so more modules can be used "safely" in edit mode.

local mockNetwork = {}
local DEBUG = false

local Shared = script.Parent.Parent
local Players = game:GetService("Players")

local StudioService = game:GetService("StudioService")
local localPlayer = Players.LocalPlayer

local Future = require(Shared.Future)
local Signal = require(Shared.Signal)

local function debugWarn(...)
    if DEBUG then
        warn(...)
    end
end

local MockEvent = {}
MockEvent.__index = MockEvent

function mockNetwork.Event<T...>(id, validator)
    local toServer = Signal.new()
    local toClient = Signal.new()

    local onClient
    local onServer

    return {
        Client = function (self)
            return {
                On = function (self, callback)
                    if onClient then
                        onClient:Disconnect()
                    end

                    onClient = toClient:Connect(callback)
                end,

                Fire = function (self, ...)
                    toServer:Fire(player, ...)
                end,
            }
        end,

        Server = function (self)
            return {
                On = function (self, callback)
                    if onServer then
                        onServer:Disconnect()
                    end

                    onServer = toServer:Connect(callback)
                end,

                Fire = function (self, player, ...)
                    toClient:Fire(...)
                end,

                FireAll = function (self, ...)
                    toClient:Fire(...)
                end,

                FireAllExcept = function (self, player, ...)
                    debugWarn("mockNetwork Server FireAllExcept:", id, player, ...)
                end,
            }
        end,
    }
end

function mockNetwork.Function<T...>(id, inValidator, outValidator)
    local callback
    debugWarn("mockNetwork mounted function w/ identifier:", id)

    return {
        Id = id,

        SetCallback = function (self, new)
            callback = new
        end,

        Call = function (self, ...)
            return Future.Try(function (...)
                return callback(localPlayer or {
                    UserId = StudioService:GetUserId()
                }, ...)
            end, ...)
        end,
    }
end

return mockNetwork


--!strict

local PlayerData = {}
PlayerData.__index = PlayerData

local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local Shared = script.Parent

local Trove = require(Shared.Trove)
local Signal = require(Shared.Signal)
local Network = require(Shared.Network)
local Promise = require(Shared.Promise)
local DeltaTable = require(Shared.DeltaTable)
local RunContext = require(Shared.RunContext)

local AUTO_SAVE_INTERVAL = 15
local DEBUG_USE_LIVE_DATA_IN_STUDIO = true

local STORE_NAME = if RunContext.IsStudio and not DEBUG_USE_LIVE_DATA_IN_STUDIO
    then if RunContext.IsEdit
        then "PlayerData_Edit"
        else "PlayerData_Studio"
    else "PlayerData"

local DATA_STORE = if RunContext.IsServer or RunContext.IsEdit
    then DataStoreService:GetDataStore(STORE_NAME)
    else nil

PlayerData.StoreName = STORE_NAME

if RunContext.IsEdit then
    local src = script.Source
    local cached = shared.PlayerDataSource

    if src == cached then
        local data: any = shared.PlayerData

        if data then
            return data :: typeof(PlayerData)
        end
    end

    shared.PlayerDataSource = src
    shared.PlayerData = PlayerData
end

local CONST = table.freeze
type Connection = Signal.Connection

--- The default data structure for a player's save data.
--- This is used as a template for new player data and
--- adding new fields to existing player data.

export type SaveData = {
    Loadout: {

    },

    Stats: {
        XP: number,
        Kills: number,
        Deaths: number,
    },

    -- TODO: Add specific values for each setting.
    Settings: {
        [string]: any,
    },

    Wallet: {
        Credits: number,
    },

    Metadata: {
        LastInventoryView: number,
    },

    _compressionSchema: number?,
}

local Default: SaveData = CONST {
    -- For item drops, keys in the 'Inventory' dictionary will be GUIDs.
    -- The "Item" key is what identifies which item from the ItemData it is (skin, weapon, etc.)
    -- We'll be able to add additional metadata in the future like paint, nametags, stat-trackers, etc.

    Inventory = CONST {
    --  ["GUID"] = {
    --      Item = "Item",
    --      ... = ...,
    --  }
    },

    Loadout = CONST {

    },

    Stats = CONST {
        XP = 0,
        Kills = 0,
        Deaths = 0,
    },

    Settings = CONST {

    },

    Metadata = CONST {
        LastInventoryView = 0,
    },

    Wallet = CONST {
        Credits = 0,
    }
}

local DataCache = {} :: {
    [number]: Class
}

local ViewCache = {} :: {
    [number]: Class
}

type Trove = Trove.Class
type Promise = Promise.Class
type LazyTable = DeltaTable.LazyTable
type Signal<T...> = Signal.Typed<T...>

export type Patch = {
    Path: string,
    Value: any,
}

export type Class = typeof(setmetatable({} :: {
    CurrentData: SaveData,
    Owner: Player?,
    Maid: Trove,

    IsLoaded: boolean,
    Updated: Signal<Patch>,
    Loaded: Signal<boolean>,
    HasPendingChanges: boolean,
}, PlayerData))

local DataInit = Network.Event("DataInit", function (data: unknown): SaveData
    assert(type(data) == "table")
    return data :: SaveData
end)

local DataPatch = Network.Event("DataPatch", function (patch: unknown): LazyTable
    assert(type(patch) == "table")
    return patch :: LazyTable
end)

local compressionRoutines = nil :: {
    Compress: (SaveData) -> LazyTable,
    Decompress: (LazyTable) -> SaveData,
}?

local function getProfileKey(userId: number)
    return `Player${userId}`
end

local function free(data: Class)
    if data.Maid then
        data.Maid:Clean()
    end
end

local function reconcileTable(target: LazyTable, template: LazyTable): ()
    for k, v in pairs(template) do
        if type(k) == "string" then -- Only string keys will be reconciled
            if target[k] == nil then
                if type(v) == "table" then
                    target[k] = DeltaTable.DeepCopy(v)
                else
                    target[k] = v
                end
            elseif type(target[k]) == "table" and type(v) == "table" then
                reconcileTable(target[k], v)
            end
        end
    end
end

PlayerData.Default = Default

--- **[Server-Only]** Forces the player's data to be saved now.
--- * If the player's data is not loaded, the promise will reject.
--- * If the player's data is loaded, the promise will resolve when the data has been saved.
--- * If called on the client, the promise will reject.
--- @return A `Promise` that resolves when the player's data has been saved and rejects with an error message if something went wrong.
function PlayerData.Save(self: Class): Promise
    return Promise.new(function (resolve, reject)
        if not self.IsLoaded then
            reject("PlayerData not loaded!")
            return
        end

        if not (RunContext.IsServer or RunContext.IsEdit) then
            reject("PlayerData.Save can only be called on the server")
            return
        end

        if not self:IsActive() then
            reject("PlayerData is read-only!")
            return
        end

        local owner = self.Owner

        if not owner then
            if RunContext.IsEdit then
                local StudioService = game:GetService("StudioService")

                owner = {
                    UserId = StudioService:GetUserId(),
                } :: any
            else
                reject("PlayerData has no owner!")
                return
            end
        end

        local store = assert(DATA_STORE)
        local key = getProfileKey(assert(owner).UserId)

        store:UpdateAsync(key, function (oldData)
            return self.CurrentData
        end)

        resolve()
    end)
end

--- Gets a `PlayerData` object for the specified player **immediately**.
--- * **It may not be immediately ready for use!** Use `PlayerData.Load` to wait for the data to be loaded through a `Promise`
---   * Check the `IsLoaded` property to see if the data is ready, and/or listen against the `Loaded` signal.
--- * The data is read-only on the client, and on the server when the player is not in-game.
---   * It *can* be modified on the server if the player is in-game, or in edit mode if it belongs to the studio user.
---   * Modifications from the server will be propagated over to the client.
--- * If called on the client or in edit mode, it will return the data for the local user (the `userId` is ignored)
--- * If called on the server, the `userId` argument is **required** and the data for the specified player will be returned.
--- @param userId The userId of the player to get data for. Has no effect on the client or in edit mode.
function PlayerData.Get(userId: number?): Class
    if RunContext.IsServer or RunContext.IsEdit then
        if RunContext.IsEdit and userId == nil then
            local StudioService = game:GetService("StudioService")
            userId = StudioService:GetUserId()
        end

        assert(userId, "PlayerData.Get must be called with a userId on the server")

        local player = RunContext.IsServer and Players:GetPlayerByUserId(userId) or nil
        local cache: {Class} = if player then DataCache else ViewCache

        if not cache[userId] then
            local key = getProfileKey(userId)
            local maid = Trove.new()

            local data: Class = setmetatable({
                CurrentData = DeltaTable.DeepCopy(Default),
                HasPendingChanges = false,
                IsLoaded = false,
                Owner = player,

                Updated = Signal.new(),
                Loaded = Signal.new(),
                Maid = maid,
            }, PlayerData)

            task.spawn(function ()
                local RETRY_COUNT = 5
                local store = assert(DATA_STORE)

                for retry = 1, RETRY_COUNT do
                    local success, currentData = xpcall(function ()
                        return store:GetAsync(key)
                    end, function (err)
                        warn("!! Error while loading profile for player", userId, err, debug.traceback())
                    end)

                    if success then
                        if currentData == nil then
                            currentData = DeltaTable.DeepCopy(Default)
                        end

                        if currentData.RobloxMetaData ~= nil then
                            -- ProfileService holdover, use the Data key.
                            currentData = currentData.Data
                        end

                        if currentData then
                            reconcileTable(currentData, Default)

                            data.CurrentData = currentData
                            data.IsLoaded = true

                            data.Loaded:Fire(true)
                            break
                        end
                    elseif retry < RETRY_COUNT then
                        warn("Failed to load profile data for player", userId, "retrying...")
                        task.wait(retry / 2)
                    elseif player then
                        player:Kick("Failed to load profile data! Please rejoin.")
                        return
                    end
                end

                if data:IsActive() then
                    local autoSave = task.spawn(function ()
                        while task.wait(AUTO_SAVE_INTERVAL) do
                            if data:IsActive() and data.HasPendingChanges then
                                local promise = data:Save()
                                    :andThen(function ()
                                        data.HasPendingChanges = false
                                        warn("Autosaved profile for player", userId)
                                    end)
                                    :catch(function (err)
                                        warn("Failed to autosave profile for player", userId, err)
                                    end)

                                promise:await()
                            end
                        end
                    end)

                    maid:Add(function ()
                        task.cancel(autoSave)
                    end)
                end
            end)

            maid:Add(function ()
                cache[userId] = nil
                data.IsLoaded = false

                data.Loaded:DisconnectAll()
                data.Updated:DisconnectAll()
            end)

            cache[userId] = data
        end

        return cache[userId]
    else
        local myPlayer
        local myUserId

        if RunContext.IsEdit then
            local StudioService = game:GetService("StudioService")
            myUserId = StudioService:GetUserId()
        else
            myPlayer = assert(Players.LocalPlayer)
            myUserId = myPlayer.UserId
        end

        local data = DataCache[myUserId]

        if not data then
            data = setmetatable({
                CurrentData = DeltaTable.DeepCopy(Default),
                HasPendingChanges = false,
                Updated = Signal.new(),
                Owner = myPlayer,

                IsLoaded = false,
                Loaded = Signal.new(),

                Maid = Trove.new(),
            }, PlayerData)

            DataCache[myUserId] = data
        end

        return data
    end
end

--- Returns a promise that resolves when the player's data has been loaded.
--- * See `PlayerData.Get` for more information about the PlayerData object.
--- * If the data is already loaded, the promise will resolve immediately.
--- * If the data is not loaded, the promise will resolve when the data is loaded.
--- @return `Promise<PlayerData>` A promise that resolves when the player's data has been loaded.
function PlayerData.Load(userId: number?): Promise
    local data = PlayerData.Get(userId)

    return Promise.new(function (resolve, reject)
        if data.IsLoaded then
            task.defer(resolve, data)
        else
            data.Loaded:Once(function ()
                if data.IsLoaded then
                    task.defer(resolve, data)
                elseif reject then
                    warn("PlayerData.Load failed for player", userId)
                    reject(data)
                end
            end)
        end
    end)
end

--- **[!! Server Only !!]** Performs a patch operation on the player's data.
--- * The patch function will receive a copy of the player's data to make changes to.
--- * If the patch function executes successfully, those changes will be applied to the player's data.
--- * The changes will be replicated to the client and the `Updated` signal will be fired with each change made.
--- @param patch The function to execute on a snapshot of the player's data.
--- @return `Promise<(), string>` A promise that resolves when the patch operation is complete and rejects with an error message if something went wrong.
function PlayerData.Patch(self: Class, patchFunc: (edit: SaveData) -> ()): Promise
    return Promise.new(function (resolve, reject)
        if not (RunContext.IsServer or RunContext.IsEdit) then
            reject("PlayerData.Patch can only be called on the server")
            return
        end

        if not self.IsLoaded then
            reject("PlayerData not loaded!")
            return
        end

        local copy = DeltaTable.DeepCopy(self.CurrentData)
        patchFunc(copy)

        local changes, count = DeltaTable.Create(self.CurrentData, copy)
        local replicate = DataPatch:Server()

        if count > 0 then
            if self.Owner then
                replicate:Fire(self.Owner, changes)
            end

            DeltaTable.Apply(self.CurrentData, changes, function (path, value)
                self.Updated:Fire({
                    Path = table.concat(path, "/"),
                    Value = value,
                })
            end)

            self.HasPendingChanges = true
        end

        resolve()
    end)
end

--- Fetches a view of the player's current data, but only if it has loaded.
--- * **The returned SaveData is immutable. Do not attempt to modify it.**
--- * !! Will return nil if the data is not loaded yet.
function PlayerData.Read(userId: number?): SaveData?
    if userId == nil or userId > 0 then
        local data = PlayerData.Get(userId)

        if data.IsLoaded then
            return data.CurrentData
        end
    end

    return nil
end

--- Returns a promise that resolves with the player's data.
function PlayerData.PromiseRead(userId: number?): Promise
    return Promise.new(function (resolve, reject)
        local data = PlayerData.Get(userId)

        if data.IsLoaded then
            resolve(data.CurrentData)
        else
            data.Loaded:Once(function ()
                if data.IsLoaded then
                    resolve(data.CurrentData)
                else
                    reject("PlayerData not loaded!")
                end
            end)
        end
    end)
end

--- Connects a callback to be invoked when a specific path in the player's data changes.
function PlayerData.Connect(self: Class, path: string, callback: (value: any) -> ()): Connection
    local myPath = path:lower()

    local conn = self.Updated:Connect(function (patch)
        local path = patch.Path:lower()

        if path == myPath then
            callback(patch.Value)
        end
    end)

    local maid = self.Maid
    maid:Add(conn)

    return conn
end

function PlayerData.IsActive(self: Class)
    return self.Owner ~= nil or RunContext.IsEdit
end

function PlayerData.Release(self: Class, force: boolean?)
    if self:IsActive() then
        local owner = self.Owner

        if owner and owner:IsDescendantOf(game) then
            if force then
                owner:Kick("Your game session was deactivated. Please rejoin!")
            else
                warn("Cannot release active profile for player", owner.UserId)
                return false
            end
        end
    end

    free(self)
    return true
end

--- Sets up the compression routines for the player data.
--- This is handled externally to avoid cyclic dependency.
--- * !! THESE MUST MATCH ON THE SERVER AND CLIENT !!
function PlayerData.SetCompressionRoutines(routines: typeof(assert(compressionRoutines)))
    compressionRoutines = routines
end

if RunContext.IsServer then
    local init = DataInit:Server()

    local function onPlayerAdded(player: Player)
        PlayerData.Load(player.UserId):andThen(function (plrData: Class)
            local data = DeltaTable.DeepCopy(plrData.CurrentData)

            if compressionRoutines then
                data = compressionRoutines.Compress(data)
            end

            init:Fire(player, data)
        end)
    end

    local function onPlayerRemoving(player: Player)
        local data = DataCache[player.UserId]

        if data then
            data:Save()
            free(data)
        end
    end

    Players.PlayerAdded:Connect(onPlayerAdded)
    Players.PlayerRemoving:Connect(onPlayerRemoving)

    for i, player in Players:GetPlayers() do
        onPlayerAdded(player)
    end

    if not RunContext.IsStudio then
        game:BindToClose(function ()
            for userId, data in pairs(DataCache) do
                data:Save()
                free(data)
            end

            warn("done!")
        end)
    end

    PlayerData.StoreName = STORE_NAME
else
    local init = DataInit:Client()
    local patch = DataPatch:Client()

    init:On(function (data: LazyTable)
        if compressionRoutines then
            data = compressionRoutines.Decompress(data)
        end

        local myData = PlayerData.Get()
        myData.CurrentData = data

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

    patch:On(function (patch: LazyTable)
        local myData = PlayerData.Get()

        DeltaTable.Apply(myData.CurrentData, patch, function (path, value)
            myData.Updated:Fire({
                Path = table.concat(path, "/"),
                Value = value,
            })
        end)
    end)
end

return PlayerData


--!strict

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

local isEdit = false
local isServer = RunService:IsServer()
local isClient = RunService:IsClient()
local isStudio = RunService:IsStudio()

local isOnline = game.JobId ~= ""
local isRCC = isOnline and isServer

pcall(function ()
    isEdit = RunService:IsEdit()
end)

return table.freeze({
    IsRCC = isRCC,
    IsEdit = isEdit,

    IsTestRun = isStudio
        and not isEdit,

    IsServer = isServer,
    IsClient = isClient,
    IsStudio = isStudio,
    IsOnline = isOnline,

    IsTenFoot = GuiService:IsTenFootInterface(),
    IsTouch = UserInputService.TouchEnabled and not UserInputService.KeyboardEnabled,
    IsVR = VRService.VREnabled,
})


-- Add any TOML files that you want to include in the Settings table here.

return table.freeze({
    Admins = require(script.Admins),
})


local Shared = script.Parent.Parent

local Signal = require(Shared.Signal)
type Signal<T...> = Signal.Typed<T...>

export type GetEnumImpl<Setting, Options> = (
    setting: Setting,
    userId: number?
) -> Options

export type SetEnumImpl<Setting, Options> = (
    setting: Setting,
    value: Options,
    userId: number?
) -> ()

export type GetEnumSettingImpl<T, Setting, Options> = (
    self: T,
    setting: Setting,
    fallback: Options
) -> Options

export type GetEnumChangedSignalImpl<Setting, Options> = (
    setting: Setting,
    userId: number?
) -> Signal<Options>

export type useEnumSettingImpl<Setting, Options> = (
    setting: Setting
) -> Options

return {}


-- AUTO GENERATED FILE!! DO NOT MANUALLY EDIT!!
-- Use the `Developer/CompileSettings` story to update this.

local EnumTypes = require(script.Parent.EnumTypes)
type GetEnumImpl<Setting, Options> = EnumTypes.GetEnumImpl<Setting, Options>
type SetEnumImpl<Setting, Options> = EnumTypes.SetEnumImpl<Setting, Options>
type GetEnumSettingImpl<T, Setting, Options> = EnumTypes.GetEnumSettingImpl<T, Setting, Options>
type GetEnumChangedSignalImpl<Setting, Options> = EnumTypes.GetEnumChangedSignalImpl<Setting, Options>

export type BoolName =
    -- Example
    | "ExampleBoolean"

export type ColorName =
    | "__UNIMPLEMENTED (DO NOT USE YET)__"

export type NumberName =
    -- Example
    | "ExampleNumber"

export type StringName =
    -- Example
    | "ExampleString"
    | "ExampleEnum"

-- Enums
export type EnumExampleEnum =
    | "Option1"
    | "Option2"
    | "Option3"

-- GetEnum
export type GetEnum =
    -- Example
    & GetEnumImpl<"ExampleEnum", EnumExampleEnum>

-- SetEnum
export type SetEnum =
    -- Example
    & SetEnumImpl<"ExampleEnum", EnumExampleEnum>

-- GetEnumSetting<T>
export type GetEnumSetting<T> =
    -- Example
    & GetEnumSettingImpl<T, "ExampleEnum", EnumExampleEnum>

-- GetEnumChangedSignal
export type GetEnumChangedSignal =
    -- Example
    & GetEnumChangedSignalImpl<"ExampleEnum", EnumExampleEnum>

export type SettingName =
    | BoolName
    | ColorName
    | NumberName
    | StringName

return table.freeze({
    Enums = table.freeze({
        ExampleEnum = newproxy() :: EnumExampleEnum,
    })
})


local Settings = {}
local Shared = script.Parent

local Trove = require(Shared.Trove)
local Signal = require(Shared.Signal)
local Promise = require(Shared.Promise)
local Network = require(Shared.Network)
local RunContext = require(Shared.RunContext)
local PlayerData = require(Shared.PlayerData)

local Frontend = require(script.Frontend)
local Backend = require(script.Backend)
local Types = require(script.Types)

--------------------------------------------------------------------------------
-- TYPES
--------------------------------------------------------------------------------

type PlayerData = PlayerData.Class
type Signal<T...> = Signal.Typed<T...>
type Promise = Promise.Class
type Trove = Trove.Class

export type BoolName = Types.BoolName
export type ColorName = Types.ColorName
export type NumberName = Types.NumberName
export type StringName = Types.StringName
export type SettingName = Types.SettingName

export type Device =
    | "Keyboard"
    | "Gamepad"
    | "Mobile"

type ISetting<TypeName, Type, Metadata = nil> = {
    Name: string,
    Type: TypeName,
    Device: Device?,
    Category: string,
    Description: string?,
    Metadata: Metadata?,
    Title: string?,
    Order: number?,
    Default: Type,
}

export type BoolSetting = ISetting<"Bool", boolean>
export type ColorSetting = ISetting<"Color", Color3>

export type NumberSetting = ISetting<"Number", number, {
    Range: NumberRange?,
    Inc: number?,
}>

export type StringSetting = ISetting<"String", string, {
    DisplayNames: {
        [string]: string,
    }?,

    Options: { string }?,
    Format: string?,
}>

export type Setting =
    | BoolSetting
    | ColorSetting
    | NumberSetting
    | StringSetting

type Category = {
    Order: number,
    Settings: { Setting }
}

export type Value = boolean | Color3 | number | string

local settingsMap = {} :: {
    [string]: Setting
}

local categoryMap = {} :: {
    [string]: Category
}

local signalCache = {} :: {
    [string]: {
        Signal: Signal<any>,
        Current: any,
        Promise: Promise,
        Watcher: Signal.Connection,
    }
}

local immediateLocalCache = {} :: {
    [string]: {
        Value: any,
        Expiration: number,
    }
}

Settings.Enums = Types.Enums

--------------------------------------------------------------------------------
-- NETWORKING & SERIALIZATION
--------------------------------------------------------------------------------

local SET_VALUE = Network.Event("ChangeSetting", function (key, value): (string, any)
    assert(type(key) == "string", "Invalid key")

    local setting = settingsMap[key]
    assert(setting, "Invalid key")

    if setting.Type == "Bool" then
        assert(type(value) == "boolean", "Invalid value")
    elseif setting.Type == "Color" then
        if typeof(value) == "string" then
            value = Color3.fromHex(value)
        end

        assert(typeof(value) == "Color3", "Invalid value")
    elseif setting.Type == "Number" then
        assert(type(value) == "number", "Invalid value")
    elseif setting.Type == "String" then
        assert(type(value) == "string", "Invalid value")
    end

    return key, value
end)

local function deserialize(setting: Setting, value: any): Value?
    if setting.Type == "Bool" then
        if type(value) == "boolean" then
            return value
        end
    elseif setting.Type == "Color" then
        if type(value) == "string" then
            if string.match(value, "^#?%x%x%x%x%x%x$") then
                return Color3.fromHex(value)
            end
        elseif typeof(value) == "Color3" then
            return value
        end
    elseif setting.Type == "Number" then
        if type(value) == "number" then
            local meta = setting.Metadata
            local range = meta and meta.Range
            local inc = meta and meta.Inc

            if range then
                value = math.clamp(value, range.Min, range.Max)
            end

            if inc then
                value //= inc
                value *= inc
            end

            return value
        end
    elseif setting.Type == "String" then
        if type(value) == "string" then
            local meta = setting.Metadata
            local format = meta and meta.Format
            local options = meta and meta.Options

            if options then
                local index = table.find(options, value)

                if index then
                    return value
                end
            elseif format then
                if value:match(format) then
                    return value
                end
            else
                return value
            end
        end
    end

    return nil
end

local function serialize(setting: Setting, value: any): Value?
    if setting.Type == "Bool" then
        if type(value) == "boolean" then
            return value
        end
    elseif setting.Type == "Color" then
        if type(value) == "string" then
            if string.match(value, "^#?%x%x%x%x%x%x$") then
                return value
            end
        elseif typeof(value) == "Color3" then
            return value:ToHex()
        end
    elseif setting.Type == "Number" then
        if type(value) == "number" then
            local meta = setting.Metadata
            local range = meta and meta.Range
            local inc = meta and meta.Inc

            if range then
                value = math.clamp(value, range.Min, range.Max)
            end

            if inc then
                value //= inc
                value *= inc
            end

            return value
        end
    elseif setting.Type == "String" then
        if type(value) == "string" then
            local meta = setting.Metadata
            local format = meta and meta.Format
            local options = meta and meta.Options

            if options then
                local index = table.find(options, value)

                if index then
                    return value
                end
            elseif format then
                if value:match(format) then
                    return value
                end
            else
                return value
            end
        end
    end

    return nil
end

local function getValue(self: Setting, userId: number?): any
    if RunContext.IsClient then
        local now = os.clock()
        local cache = immediateLocalCache[self.Name]

        if cache and now < cache.Expiration then
            return cache.Value
        end
    end

    local data = PlayerData.Read(userId)

    if data then
        local serialized = data.Settings[self.Name]
        local value = deserialize(self, serialized)

        if value ~= nil then
            return value
        end
    end

    return self.Default
end

local function getChangedSignal(self: Setting, userId: number?): Signal<any>
    local cacheKey = `{self.Type}_{self.Name}_{userId}`
    local cached = signalCache[cacheKey]

    if cached then
        return cached.Signal
    end

    local cache = {}
    cache.Signal = Signal.new()
    cache.Current = getValue(self, userId)

    cache.Promise = PlayerData.Load(userId):andThen(function (data: PlayerData)
        local function update(value: any)
            local deserialized = deserialize(self, value)

            if deserialized ~= nil then
                cache.Signal:Fire(deserialized)
                cache.Current = deserialized

                immediateLocalCache[self.Name] = {
                    Value = deserialized,
                    Expiration = os.clock() + 2,
                }
            end
        end

        local init = rawget(data.CurrentData.Settings, self.Name)
        cache.Watcher = data:Connect(`Settings/{self.Name}`, update)
        update(init)
    end)

    signalCache[cacheKey] = cache
    return cache.Signal
end

local function setValueImpl(userId: number, key: string, value: any)
    local data = PlayerData.Get(userId)
    local setting = settingsMap[key]

    if not (setting and data.IsLoaded) then
        return
    end

    data:Patch(function (patch)
        local value = serialize(setting, value)

        if value ~= nil then
            patch.Settings[key] = value

            immediateLocalCache[key] = {
                Value = value,
                Expiration = os.clock() + 2,
            }
        end
    end)
end

local function setValue(self: Setting, value: any, userId: number?)
    local key = self.Name

    if RunContext.IsServer then
        if userId then
            setValueImpl(userId, key, value)
        end
    elseif RunContext.IsEdit then
        local StudioService = game:GetService("StudioService")
        local userId = StudioService:GetUserId()
        setValueImpl(userId, key, value)
    elseif RunContext.IsClient then
        local oldValue = getValue(self, userId)

        if oldValue == value then
            return
        end

        local changed = getChangedSignal(self, userId)
        changed:Fire(value)

        local client = SET_VALUE:Client()
        client:Fire(key, value)

        immediateLocalCache[key] = {
            Value = value,
            Expiration = os.clock() + 2,
        }
    end
end

function Settings.FindSetting(name: string): Setting?
    return settingsMap[name]
end

--------------------------------------------------------------------------------
-- Generic
--------------------------------------------------------------------------------

function Settings.GetValue(name: SettingName, userId: number?): Value
    local info = settingsMap[name]
    assert(info, "bad argument #1 to 'Get' (Setting expected)")
    return getValue(info, userId)
end

function Settings.SetValue(name: SettingName, value: Value, userId: number?)
    local info = settingsMap[name]
    assert(info, "bad argument #1 to 'SetValue' (Setting expected)")
    setValue(info, value, userId)
end

function Settings.GetValueChangedSignal(name: SettingName, userId: number?): Signal<Value>
    local info = settingsMap[name]
    assert(info, "bad argument #1 to 'GetValueChangedSignal' (Setting expected)")
    return getChangedSignal(info, userId)
end

--------------------------------------------------------------------------------
-- Bool
--------------------------------------------------------------------------------

function Settings.GetBool(name: BoolName, userId: number?): boolean
    local info = settingsMap[name]
    assert(info and info.Type == "Bool", "bad argument #1 to 'GetBool' (BoolSetting expected)")
    return getValue(info, userId)
end

function Settings.SetBool(name: BoolName, value: boolean, userId: number?)
    local info = settingsMap[name]
    assert(info and info.Type == "Bool", "bad argument #1 to 'SetBool' (BoolSetting expected)")
    setValue(info, value, userId)
end

function Settings.GetBoolChangedSignal(name: BoolName, userId: number?): Signal<boolean>
    local info = settingsMap[name]
    assert(info and info.Type == "Bool", "bad argument #1 to 'GetBoolChangedSignal' (BoolSetting expected)")
    return getChangedSignal(info, userId)
end

--------------------------------------------------------------------------------
-- Color
--------------------------------------------------------------------------------

function Settings.GetColor(name: ColorName, userId: number?): Color3
    local info = settingsMap[name]
    assert(info and info.Type == "Color", "bad argument #1 to 'GetColor' (ColorSetting expected)")
    return getValue(info, userId)
end

function Settings.SetColor(name: ColorName, value: Color3, userId: number?)
    local info = settingsMap[name]
    assert(info and info.Type == "Color", "bad argument #1 to 'SetColor' (ColorSetting expected)")
    setValue(info, value, userId)
end

function Settings.GetColorChangedSignal(name: ColorName, userId: number?): Signal<Color3>
    local info = settingsMap[name]
    assert(info and info.Type == "Color", "bad argument #1 to 'GetColorChangedSignal' (ColorSetting expected)")
    return getChangedSignal(info, userId)
end

--------------------------------------------------------------------------------
-- Number
--------------------------------------------------------------------------------

function Settings.GetNumber(name: NumberName, userId: number?): number
    local info = settingsMap[name]
    assert(info and info.Type == "Number", "bad argument #1 to 'GetNumber' (NumberSetting expected)")
    return getValue(info, userId)
end

function Settings.SetNumber(name: NumberName, value: number, userId: number?)
    local info = settingsMap[name]
    assert(info and info.Type == "Number", "bad argument #1 to 'SetNumber' (NumberSetting expected)")
    setValue(info, value, userId)
end

function Settings.GetNumberChangedSignal(name: NumberName, userId: number?): Signal<number>
    local info = settingsMap[name]
    assert(info and info.Type == "Number", "bad argument #1 to 'GetNumberChangedSignal' (NumberSetting expected)")
    return getChangedSignal(info, userId)
end

--------------------------------------------------------------------------------
-- String

function Settings.GetString(name: StringName, userId: number?): string
    local info = settingsMap[name]
    assert(info and info.Type == "String", "bad argument #1 to 'GetString' (StringSetting expected)")
    return getValue(info, userId)
end

function Settings.GetStringChangedSignal(name: StringName, userId: number?): Signal<string>
    local info = settingsMap[name]
    assert(info and info.Type == "String", "bad argument #1 to 'GetStringChangedSignal' (StringSetting expected)")
    return getChangedSignal(info, userId)
end

function Settings.SetString(name: StringName, value: string, userId: number?)
    local info = settingsMap[name]
    assert(info and info.Type == "String", "bad argument #1 to 'SetString' (StringSetting expected)")
    setValue(info, value, userId)
end

--------------------------------------------------------------------------------
-- Enum

Settings.SetEnum =
    Settings.SetString :: Types.SetEnum

Settings.GetEnum =
    (Settings.GetString :: any) :: Types.GetEnum

Settings.GetEnumChangedSignal =
    (Settings.GetStringChangedSignal :: any) :: Types.GetEnumChangedSignal

export type GetEnumSetting<T> = Types.GetEnumSetting<T>

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

if RunContext.IsServer then
    local server = SET_VALUE:Server()

    server:On(function (player, key, value)
        local userId = player.UserId
        setValueImpl(userId, key, value)
    end)
end

--------------------------------------------------------------------------------
-- FRONTEND SETTINGS
--------------------------------------------------------------------------------

for categoryName: string, categoryInfo: any in Frontend :: any do
    if type(categoryInfo) ~= "table" then
        continue
    end

    local categoryOrder = if type(categoryInfo["$CategoryOrder"]) == "number"
        then categoryInfo["$CategoryOrder"]
        else 0

    local category = {
        Name = categoryName,
        Order = categoryOrder,
        Settings = {},
    }

    for settingName: string, settingRef: any in pairs(categoryInfo) do
        if type(settingRef) ~= "table" then
            continue
        end

        local default = settingRef.Default

        if default == nil then
            warn("Setting", categoryName, settingName, "is missing a default value")
            continue
        end

        local desc = settingRef.Description
        local device = settingRef.Device
        local title = settingRef.Title
        local order = settingRef.Order

        if desc ~= nil then
            desc = tostring(desc)
        end

        if title ~= nil then
            title = tostring(title)
        end

        if order ~= nil then
            order = tonumber(order)
        end

        if device ~= nil then
            device = tostring(device)
        end

        if type(default) == "boolean" then
            local bool: BoolSetting = {
                Type = "Bool",
                Category = categoryName,
                Name = settingName,
                Title = title,
                Order = order,
                Device = device,
                Default = default,
                Description = desc,
            }

            settingsMap[settingName] = table.freeze(bool)
        elseif type(default) == "number" then
            local min = tonumber(settingRef.Min)
            local max = tonumber(settingRef.Max)
            local inc = tonumber(settingRef.Inc)

            if min and max and min > max then
                max, min = min, max
            end

            if inc then
                inc = math.abs(inc)
            end

            local num: NumberSetting = {
                Type = "Number",
                Category = categoryName,
                Name = settingName,
                Title = title,
                Order = order,
                Device = device,
                Default = default,
                Description = desc,

                Metadata = table.freeze({
                    Range = if min and max
                        then NumberRange.new(min, max)
                        else nil,

                    Inc = inc,
                }),
            }

            settingsMap[settingName] = table.freeze(num)
        elseif type(default) == "string" then
            if default:match("^#?%x%x%x%x%x%x$") then
                local color: ColorSetting = {
                    Type = "Color",
                    Category = categoryName,
                    Name = settingName,
                    Title = title,
                    Order = order,
                    Device = device,
                    Description = desc,
                    Default = Color3.fromHex(default),
                }

                settingsMap[settingName] = table.freeze(color)
            else
                local options = settingRef.Options
                local displayNames = settingRef.DisplayNames
                local format = settingRef.Format and tostring(settingRef.Format)

                if options ~= nil then
                    if type(options) ~= "table" then
                        warn("Setting", categoryName, settingName, "has invalid options")
                        continue
                    end

                    if #options == 0 then
                        warn("Setting", categoryName, settingName, "has no options defined")
                        continue
                    end

                    local index = table.find(options, default)

                    if index == nil then
                        warn("Setting", categoryName, settingName, "does not contain default value", default, "in options:", options)
                        continue
                    end

                    for i, option in ipairs(options) do
                        if type(option) ~= "string" then
                            warn("Setting", categoryName, settingName, "has invalid option at index", i)
                            continue
                        end
                    end

                    options = table.freeze(options)
                end

                if displayNames ~= nil then
                    if type(displayNames) ~= "table" then
                        warn("Setting", categoryName, settingName, "has invalid display names")
                        continue
                    end

                    if not options then
                        warn("Setting", categoryName, settingName, "has display names but no options")
                        continue
                    end

                    for option, displayName in pairs(displayNames) do
                        if type(option) ~= "string" or type(displayName) ~= "string" then
                            warn("Setting", categoryName, settingName, "has invalid display name for option", option)
                            continue
                        end

                        if not table.find(options, option) then
                            warn("Setting", categoryName, settingName, "has display name for non-existent option", option)
                            continue
                        end
                    end

                    displayNames = table.freeze(displayNames)
                end

                local str: StringSetting = {
                    Type = "String",
                    Category = categoryName,
                    Name = settingName,
                    Title = title,
                    Order = order,
                    Device = device,
                    Default = default,
                    Description = desc,

                    Metadata = table.freeze({
                        DisplayNames = displayNames,
                        Options = options,
                        Format = format,
                    }),
                }

                settingsMap[settingName] = table.freeze(str)
            end
        else
            warn("Setting", categoryName, settingName, "has an unsupported default value:", default)
            continue
        end

        table.insert(category.Settings, settingsMap[settingName])
    end

    categoryMap[categoryName] = table.freeze(category)
end

table.freeze(settingsMap)
table.freeze(categoryMap)

function Settings.GetData(): typeof(categoryMap)
    return categoryMap
end

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

for key, value in Backend do
    rawset(Settings :: any, key, value)
end

return table.freeze(Settings) ::
    & typeof(Settings)
    & typeof(Backend)

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


-- Signal
-- MaximumADHD
-- November 29th, 2022

--!native
--!strict

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Packages = ReplicatedStorage.Packages

local module = require(Packages.Signal)
export type Connection = module.Connection
export type Typed<T...> = module.Signal<T...>
export type Generic = Typed<...any>

return module :: {
    new: <Args...>() -> Typed<Args...>,
    Is: (object: any) -> boolean,
    Wrap: <Args...>(signal: RBXScriptSignal<Args...>) -> Typed<Args...>,

    Typed: <T...>(T...) -> Typed<T...>,
    Generic: Typed<...any>,
    Empty: Typed<>,
}


-- StackModifier
-- MaximumADHD
-- March 21st, 2023

--!strict

--- A module for handling complex property stacking and modification.
--- It allows for multiple sources to modify the same property on an instance, with the ability to prioritize and weight each source.
--- Modifiers with higher priorities will override lower priority modifiers, and modifiers with the same priority will attempt to merge together based on their weight values.
local StackModifier = {}

local Shared = game.ReplicatedStorage.Shared
local RunContext = require(Shared.RunContext)
local AVLTree = require(Shared.AVLTree)
local Trove = require(Shared.Trove)
local Tags = require(Shared.Tags)

type AVLTree<T> = AVLTree.Class<T>

type PatchConfig = {
    Weight: number?,
    Priority: number?,
    Additive: boolean?,
}

type MixDef<T> = {
    Zero: T,
    Add: ((self: T, other: T) -> T)?,
    Scale: ((self: T, scale: number) -> T)?,
}

type Scalar = {
    Priority: number,

    Modifiers: {
        [string]: any
    }
}

type PropData = {
    Target: Instance,
    Prop: string,

    Default: any,
    MixDef: MixDef<any>?,

    Additives:{
        [string]: any
    },

    Scalars: AVLTree<Scalar>,
}

local dispatches = {} :: {
    [Instance]: thread,
}

local modifiers = {} :: {
    [Instance]: Trove.Class,
}

local mixable = {} :: {
    [string]: MixDef<any>,
}

local configs = {} :: {
    [string]: PatchConfig
}

local props = {} :: {
    [Instance]: PropData
}

local function lazyMixDef<T>(name: string, zero: T)
    mixable[name] = {
        Zero = zero,
    }
end

local function explicitMixDef<T>(name: string, def: MixDef<T>)
    mixable[name] = def
end

local function mix<T>(mixer: MixDef<T>, value: T, output: T, weight: number?): T
    if weight and weight ~= 1 then
        if mixer.Scale then
            value = mixer.Scale(value, weight)
        else
            value = (value :: any) * weight
        end
    end

    if mixer.Add then
        output = mixer.Add(output, value)
    else
        output = (output :: any) + value
    end

    return output
end

local function sortScalar(a: Scalar, b: Scalar): number
    return a.Priority - b.Priority
end

local function getPropData(propNode: Instance): PropData?
    local propData = props[propNode]

    if propData then
        return propData
    end

    local target = propNode.Parent

    if not target then
        return nil
    end

    local propName = propNode.Name:sub(#"⧉" + 1)
    local default = propNode:GetAttribute("Default")

    if default == nil then
        return nil
    end

    local valueType = typeof(default)
    local mixer = mixable[valueType]

    props[propNode] = {
        Target = target,
        Prop = propName,

        Default = default,
        MixDef = mixer,

        Additives = {},
        Scalars = AVLTree.new(sortScalar),
    }

    propNode.Destroying:Once(function ()
        props[propNode] = nil
    end)

    return props[propNode]
end

local function recompute(propNode: Instance)
    if dispatches[propNode] then
        return
    end

    local propData = getPropData(propNode)

    if not propData then
        return
    end

    dispatches[propNode] = task.defer(function ()
        local mixDef = propData.MixDef
        local target = propData.Target
        local default = propData.Default

        local scalars = propData.Scalars
        local scalarSet = scalars:Last()

        local additive = mixDef and mixDef.Zero
        local finalValue = mixDef and mixDef.Zero or default

        if mixDef then
            for key, value in propData.Additives do
                additive = mix(mixDef, value, additive)
            end
        end

        if scalarSet then
            for key, value in scalarSet.Modifiers do
                if mixDef then
                    finalValue = mix(mixDef, value, finalValue)
                else
                    finalValue = value
                end
            end
        else
            finalValue = default
        end

        if mixDef and additive ~= mixDef.Zero then
            finalValue = mix(mixDef, additive, finalValue)
        end

        local prop = propData.Prop;
        (target :: any)[prop] = finalValue

        if #propNode:GetChildren() == 0 then
            propNode:Destroy()
        end

        dispatches[propNode] = nil
    end)
end

local function onModifierAdded(node: Instance)
    local trove = Trove.new()
    local prop = node.Parent

    local key = node.Name
    local scalar

    local function update()
        if prop then
            local config = configs[key]

            if not config then
                return
            end

            local priority = config.Priority or 0
            local propData = getPropData(prop)

            if not propData then
                return
            end

            if modifiers[node] then
                local value = node:GetAttribute("Value")
                local scalars = propData.Scalars

                if config.Additive then
                    propData.Additives[key] = value
                else
                    local newScalar = scalar or {
                        Priority = priority,
                        Modifiers = {},
                    }

                    local mixDef = propData.MixDef
                    local oldScalar = scalar or scalars:Find(newScalar)

                    -- Prescale the value if it's scalable.
                    if mixDef then
                        local weight = config.Weight
                        value = mix(mixDef, value, mixDef.Zero, weight)
                    end

                    if oldScalar then
                        oldScalar.Modifiers[key] = value
                    else
                        newScalar.Modifiers[key] = value
                        scalars:Insert(newScalar)
                        scalar = newScalar
                    end
                end
            elseif propData then
                if config.Additive then
                    propData.Additives[key] = nil
                elseif scalar then
                    scalar.Modifiers[key] = nil

                    if next(scalar.Modifiers) == nil then
                        propData.Scalars:Remove(scalar)
                    end
                end
            end

            recompute(prop)
        end
    end

    trove:Add(function ()
        modifiers[node] = nil
        update()
    end)

    trove:Connect(node.AncestryChanged, function ()
        if node.Parent then
            prop = node.Parent
            update()
        end
    end)

    local listener = node:GetAttributeChangedSignal("Value")
    trove:Connect(listener, update)

    modifiers[node] = trove
    update()
end

local function onModifierRemoved(node: Instance)
    local trove = modifiers[node]

    if trove then
        trove:Destroy()
    end
end

lazyMixDef("number", 0)
lazyMixDef("UDim", UDim.new())
lazyMixDef("UDim2", UDim2.new())
lazyMixDef("Vector2", Vector2.zero)
lazyMixDef("Vector3", Vector3.zero)

explicitMixDef("Color3", {
    Zero = Color3.new(),

    Add = function(self: Color3, other: Color3)
        return Color3.new(self.R + other.R, self.G + other.G, self.B + other.B)
    end,

    Scale = function(color: Color3, scale: number)
        return Color3.new(color.R * scale, color.G * scale, color.B * scale)
    end,
})

if RunContext.IsClient then
    Tags.Bind("StackModifier", onModifierAdded, onModifierRemoved)
end

----------------------------------------------------------------------------------------------------------------------------
-- API
----------------------------------------------------------------------------------------------------------------------------

local function getNode(name: string, parent: Instance, init: (node: Configuration) -> boolean): Configuration?
    local node = parent:FindFirstChild(name)

    if node and node:IsA("Configuration") then
        return node
    end

    local newNode = Instance.new("Configuration")
    newNode.Archivable = false
    newNode.Name = name

    if init(newNode) then
        newNode.Parent = parent
        return newNode
    end

    return nil
end

local function getPropNode(target: Instance, prop: string): Configuration?
    return getNode("⧉" .. prop, target, function(node)
        return xpcall(function()
            local value = (target :: any)[prop]
            node:SetAttribute("Default", value)
        end, function(err)
            warn("!! Invalid StackModifier:", target:GetFullName(), "->", prop, "caused by:\n", err)
            print(debug.traceback()) -- Ugliest print ever spams my studio
        end)
    end)
end

local function getModifierNode(object: Instance, prop: string, key: string): Configuration?
    local propNode = getPropNode(object, prop)

    if propNode then
        return getNode(key, propNode, function(keyNode)
            local default = propNode:GetAttribute("Default")

            if default ~= nil then
                keyNode:SetAttribute("Value", default)
                keyNode:AddTag("StackModifier")

                return true
            end

            return false
        end)
    end

    return nil
end

--- Registers a configuration for a stack modifier key. This is used to define the priority and weight of each property
--- that uses the stack modifier key, as well as whether the properties should be merged additively.
--- * `PatchConfig` is a table with the following fields:
---   * `Priority` (`number?` = `1`): The priority of each property using the modifier key.
---   * `Weight` (`number?` = `1`): Weight of each modifier when merging values of the same priority, if mergeable.
---   * `Additive` (`boolean?` = `false`): Whether each property should be merged additively to the overall stack.
--- @param key The unique id of the stack modifier key.
--- @param config The configuration for the stack modifier key.
--- @return `string` The unique id of the stack modifier key.
function StackModifier.Register(key: string, config: PatchConfig)
    configs[key] = config
    return key
end

--- Adds a stack modifier to the target instance with the provided string `modKey` indicating the configuration to use.
--- @param target `Instance` The target instance to add the stack modifier to.
--- @param key `string` The unique id of the stack modifier to add.
--- @param props `{ [string]: any }` The properties to add to the stack modifier.
function StackModifier.Patch(target: Instance, modKey: string, props: { [string]: any })
    local config = configs[modKey]
    assert(config, "Unknown StackModifier key: " .. modKey)

    for prop, value in props do
        local modifier = getModifierNode(target, prop, modKey)

        if modifier then
            local current = modifier:GetAttribute("Value")

            if typeof(current) == typeof(value) then
                modifier:SetAttribute("Value", value)
            end
        end
    end
end

--- Removes all stack modifiers with the provided string `modKey` from the target instance.
--- @param target The target instance to remove the stack modifiers from.
--- @param modKey The unique id of the stack modifier to remove.
function StackModifier.Clear(target: Instance, modKey: string)
    for _, desc in target:GetDescendants() do
        if desc.Name ~= modKey then
            continue
        end

        if desc:HasTag("StackModifier") then
            desc:Destroy()
        end
    end
end

return StackModifier


local StringBuilder = {}
StringBuilder.__index = StringBuilder

export type Class = typeof(setmetatable({} :: {
    _stack: number,
    _lines: { string },
}, StringBuilder))

function StringBuilder.new(): Class
    return setmetatable({
        _stack = 0,
        _lines = {},
    }, StringBuilder)
end

function StringBuilder.WriteLine(self: Class, format: string?, ...: any)
    if not format then
        table.insert(self._lines, "")
        return
    end

    local argCount = select("#", ...)
    local stack = string.rep(" ", self._stack * 4)

    if argCount == 0 then
        table.insert(self._lines, `{stack}{format}`)
        return
    end

    local line = string.format(format, ...)

    if #line == 0 then
        table.insert(self._lines, "")
        return
    end

    table.insert(self._lines, `{stack}{line}`)
end

function StringBuilder.OpenStack(self: Class)
    self._stack += 1
end

function StringBuilder.CloseStack(self: Class)
    self._stack -= 1
end

function StringBuilder.GetContent(self: Class): string
    return table.concat(self._lines, "\n")
end

function StringBuilder.GetLineCount(self: Class): number
    return #self._lines
end

function StringBuilder.GetLine(self: Class, index: number): string
    return self._lines[index]
end

function StringBuilder.Clear(self: Class)
    self._stack = 0
    table.clear(self._lines)
end

return StringBuilder


------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Tags
-- MaximumADHD
-- December 22nd, 2020
------------------------------------------------------------------------------------------------------------------------------------------------------------------
--!native
--!strict

local Tags = {}
local Shared = script.Parent
local CollectionService = game:GetService("CollectionService")

local Trove = require(Shared.Trove)
type Trove = Trove.Class

local Signal = require(Shared.Signal)
type Signal<T...> = Signal.Typed<T...>

function Tags.GetTagged(tag: string): { Instance }
    return CollectionService:GetTagged(tag)
end

function Tags.GetInstanceAddedSignal(tag: string): RBXScriptSignal
    return CollectionService:GetInstanceAddedSignal(tag)
end

function Tags.GetInstanceRemovedSignal(tag: string): RBXScriptSignal
    return CollectionService:GetInstanceRemovedSignal(tag)
end

function Tags.FindFirstTagged(tag: string): Instance?
    local tagged = CollectionService:GetTagged(tag)

    if #tagged > 0 then
        return tagged[1]
    end

    return nil
end

function Tags.Bind(tag: string, onAdded: (Instance) -> (), onRemoved: (Instance) -> ()?): Trove.Class
    local maid = Trove.new()
    maid:Connect(Tags.GetInstanceAddedSignal(tag), onAdded)

    if onRemoved then
        maid:Connect(Tags.GetInstanceRemovedSignal(tag), onRemoved)
    end

    for i, tagged in CollectionService:GetTagged(tag) do
        task.spawn(onAdded, tagged)
    end

    return maid
end

function Tags.BindWithMaid(tag: string, onAdded: (Instance, Trove) -> (), onRemoved: (Instance) -> ()?): Trove.Class
    local baseMaid = Trove.new()
    local tagMaids = {}

    local bindMaid = Tags.Bind(tag, function(inst)
        local maid = baseMaid:Extend()
        tagMaids[inst] = maid

        maid:Add(function()
            tagMaids[inst] = nil

            if onRemoved then
                onRemoved(inst)
            end
        end)

        onAdded(inst, maid)
    end, function(inst)
        local maid = tagMaids[inst]

        if maid then
            maid:Clean()
        end
    end)

    bindMaid:Add(baseMaid)
    return bindMaid
end

return Tags


-- Trove
-- MaximumADHD
-- November 30th, 2022

--!native
--!strict

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Packages = ReplicatedStorage.Packages
local Shared = ReplicatedStorage.Shared

local Trove = require(Packages.Trove)
local Signal = require(Shared.Signal)
local Promise = require(Shared.Promise)

type Promise = Promise.Class
type Connection = Signal.Connection
type Signal<T...> = Signal.Typed<T...>

-- stylua: ignore
export type Class = {
	Extend: (self: Class) -> Class,
	Clone: <T>(self: Class, instance: T) -> T,

	Construct: (<T>(self: Class, createFunc: (...any) -> ...any, ...any) -> T)
	         & (<T>(self: Class, classTable: { new: ((...any) -> ...any)? }, ...any) -> T),

	Connect: (<Args...>(self: Class, signal: RBXScriptSignal, fn: (Args...) -> ()) -> RBXScriptConnection)
	       & (<Args...>(self: Class, signal: Signal<Args...>, fn: (Args...) -> ()) -> Connection),

	BindToRenderStep: (self: Class, name: string, priority: number, fn: (dt: number) -> ()) -> (),
	AttachToInstance: (self: Class, instance: Instance) -> RBXScriptConnection,
	AddPromise: (self: Class, promise: Promise) -> Promise,
	Add: <T>(self: Class, object: T, cleanupMethod: string?) -> T,
	Remove: (self: Class, object: any) -> boolean,
	Destroy: (self: Class) -> (),
	Clean: (self: Class) -> (),
}

-- TODO: Actual implementation???
return (Trove :: any) :: {
    new: () -> Class,
}


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local remotes = require(ReplicatedStorage.Shared.remotes)
local names = require(ReplicatedStorage.Shared.names)

local todos: { string } = {}

local function addTodo()
	remotes.addTodo:fire(names.getRandomName())
end

local function removeTodo()
	if #todos > 0 then
		remotes.removeTodo:fire(todos[math.random(#todos)])
	end
end

remotes.getTodos:request():andThen(function(serverTodos)
	todos = serverTodos
	print("New todos:", table.concat(todos, ", "))

	for _ = 1, 3 do
		addTodo()
	end
end)

remotes.todosChanged:connect(function(serverTodos)
	todos = serverTodos
	print("New todos:", table.concat(todos, ", "))
end)

while true do
	task.wait(math.random(1, 3))

	if math.random() > 0.5 then
		addTodo()
	else
		removeTodo()
	end
end


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

local remotes = require(ReplicatedStorage.Shared.remotes)

local todos: { string } = { "Milk", "Eggs" }
local changed = false

remotes.addTodo:connect(function(player, todo)
	table.insert(todos, todo)
	changed = true
end)

remotes.removeTodo:connect(function(player, todo)
	table.remove(todos, table.find(todos, todo) or -1)
	changed = true
end)

remotes.getTodos:onRequest(function(player)
	return todos
end)

RunService.Heartbeat:Connect(function()
	if changed then
		changed = false
		remotes.todosChanged:fireAll(todos)
		print("New todos:", table.concat(todos, ", "))
	end
end)


local names = {
	"Milk",
	"Eggs",
	"Bread",
	"Butter",
	"Cheese",
	"Bacon",
	"Sausage",
	"Ham",
	"Chicken",
	"Beef",
	"Pork",
	"Fish",
	"Rice",
	"Pasta",
	"Potatoes",
	"Onions",
	"Garlic",
	"Tomatoes",
	"Carrots",
	"Peppers",
	"Lettuce",
	"Cucumber",
	"Mushrooms",
	"Spinach",
	"Broccoli",
	"Cauliflower",
	"Celery",
	"Apples",
	"Oranges",
	"Bananas",
	"Grapes",
	"Strawberries",
	"Blueberries",
	"Raspberries",
	"Pineapple",
	"Peaches",
	"Pears",
	"Cherries",
	"Watermelon",
	"Cantaloupe",
	"Honeydew",
	"Lemons",
	"Limes",
	"Avocado",
	"Corn",
	"Beans",
	"Peas",
	"Nuts",
	"Seeds",
	"Olive Oil",
	"Vegetable Oil",
	"Butter",
	"Margarine",
	"Flour",
	"Sugar",
	"Salt",
	"Pepper",
	"Spices",
	"Herbs",
	"Baking Powder",
	"Baking Soda",
	"Yeast",
	"Vanilla Extract",
	"Chocolate",
	"Cocoa Powder",
	"Coffee",
	"Tea",
	"Juice",
	"Milk",
	"Cream",
	"Soda",
	"Water",
	"Ice",
	"Bottled Water",
	"Toilet Paper",
	"Paper Towels",
	"Tissues",
	"Soap",
	"Shampoo",
	"Conditioner",
	"Toothpaste",
	"Toothbrush",
	"Mouthwash",
	"Floss",
	"Deodorant",
	"Razors",
	"Shaving Cream",
	"Lotion",
	"Sunscreen",
	"Makeup",
	"Cotton Swabs",
	"Cotton Balls",
	"Nail Polish",
	"Nail Polish Remover",
	"Batteries",
	"Light Bulbs",
	"Trash Bags",
	"Laundry Detergent",
	"Fabric Softener",
	"Dish Soap",
	"Dishwasher Detergent",
	"Sponges",
	"Dish Towels",
}

local function getRandomName()
	return names[math.random(#names)]
end

return {
	getRandomName = getRandomName,
}


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Remo = require(ReplicatedStorage.Packages.Remo)
local t = require(ReplicatedStorage.Packages.t)

type Remotes = Remo.Remotes<{
	addTodo: Remo.ClientToServer<string>,
	removeTodo: Remo.ClientToServer<string>,
	getTodos: Remo.ServerAsync<(), ({ string })>,
	todosChanged: Remo.ServerToClient<{ string }>,
}>

local todo = t.string
local todoList = t.array(todo :: any)

local remotes: Remotes = Remo.createRemotes({
	addTodo = Remo.remote(todo),
	removeTodo = Remo.remote(todo),
	getTodos = Remo.remote().returns(todoList),
	todosChanged = Remo.remote(todoList),
}, Remo.loggerMiddleware)

return remotes


import { RunService } from "@rbxts/services";
import { remotes } from "shared/remotes";

const todos: string[] = ["Milk", "Eggs"];
let changed = false;

remotes.addTodo.connect((player, todo) => {
	todos.push(todo);
	changed = true;
});

remotes.removeTodo.connect((player, todo) => {
	todos.remove(todos.indexOf(todo));
	changed = true;
});

remotes.getTodos.onRequest((player) => {
	return todos;
});

RunService.Heartbeat.Connect(() => {
	if (changed) {
		changed = false;
		remotes.todosChanged.fireAll(todos);
		print(`New todos: ${todos.join(", ")}`);
	}
});


import { Client, Server, createRemotes, loggerMiddleware, remote } from "@rbxts/remo";
import { t } from "@rbxts/t";

const todo = t.string;
const todoList = t.array(todo);

export const remotes = createRemotes(
	{
		// add a todo to the server
		addTodo: remote<Server, [name: string]>(todo),

		// remove a todo from the server
		removeTodo: remote<Server, [name: string]>(todo),

		// get all todos from the server
		getTodos: remote<Server>().returns<string[]>(todoList),

		// send new todos to clients
		todosChanged: remote<Client, [todos: string[]]>(todoList),
	},
	loggerMiddleware,
);


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local TestEZ: any = require(ReplicatedStorage.DevPackages.TestEZ)

TestEZ.TestBootstrap:run({
	ReplicatedStorage.Packages.Remo,
})


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local include = script:FindFirstAncestor("rbxts_include")
	or script:FindFirstAncestor("include")
	or ReplicatedStorage:FindFirstChild("rbxts_include")
	or script.Parent.Parent

export type PromiseStatus = "Started" | "Resolved" | "Rejected" | "Cancelled"

export type PromiseConstructor = {
	Status: {
		Started: "Started",
		Resolved: "Resolved",
		Rejected: "Rejected",
		Cancelled: "Cancelled",
	},
	new: <T...>(
		resolver: (resolve: (T...) -> (), reject: (any) -> (), onCancel: (abortHandler: () -> ()) -> ()) -> ()
	) -> Promise<T...>,
	defer: <T...>(
		resolver: (resolve: (T...) -> (), reject: (any) -> (), onCancel: (abortHandler: () -> ()) -> ()) -> ()
	) -> Promise<T...>,
	resolve: <T>(value: T | Promise<T>, ...any) -> Promise<T>,
	reject: (value: any) -> Promise<never>,
	try: <T, A...>(callback: (A...) -> T, A...) -> Promise<T>,
	all: <T>(promises: { Promise<T> }) -> Promise<{ T }>,
	fold: <T, U>(
		promises: { T | Promise<T> },
		reducer: (U, T, number) -> U | Promise<U>,
		initialValue: U
	) -> Promise<U>,
	some: <T>(promises: { Promise<T> }, count: number) -> Promise<{ T }>,
	any: <T>(promises: { Promise<T> }) -> Promise<T>,
	allSettled: (promises: { Promise<any> }) -> Promise<{ PromiseStatus }>,
	race: <T>(promises: { Promise<T> }) -> Promise<T>,
	each: <T, U>(list: { T | Promise<T> }, predicate: (T, number) -> U | Promise<U>) -> Promise<{ U }>,
	is: (value: any) -> boolean,
	promisify: <R, A...>(fn: (A...) -> R) -> (A...) -> Promise<R>,
	delay: (seconds: number) -> Promise<number>,
	fromEvent: <T>(event: RBXScriptSignal<T>, predicate: ((T) -> boolean)?) -> Promise<T>,
}

export type _Promise = {
	timeout: (self: _Promise, seconds: number, rejectionValue: unknown) -> _Promise,
	getStatus: (self: _Promise) -> PromiseStatus,
	andThen: (self: _Promise, successHandler: (...any) -> any, failureHandler: ((any) -> any)?) -> _Promise,
	catch: (self: _Promise, failureHandler: (...any) -> any) -> _Promise,
	tap: (self: _Promise, successHandler: (...any) -> ()) -> _Promise,
	andThenCall: <T...>(self: _Promise, successHandler: (T...) -> any, T...) -> _Promise,
	andThenReturn: (self: _Promise, value: any) -> _Promise,
	cancel: (self: _Promise) -> (),
	finally: (self: _Promise, callback: (status: PromiseStatus) -> any) -> _Promise,
	finallyCall: <T...>(self: _Promise, callback: (T...) -> any, T...) -> _Promise,
	finallyReturn: (self: _Promise, value: any) -> _Promise,
	awaitStatus: (self: _Promise) -> (PromiseStatus, ...any),
	await: (self: _Promise) -> (boolean, ...any),
	expect: (self: _Promise) -> ...any,
	now: (self: _Promise, rejectionValue: unknown) -> _Promise,
}

export type Promise<T... = ...any> = {
	timeout: (self: Promise<T...>, seconds: number, rejectionValue: unknown) -> Promise<T...>,
	getStatus: (self: Promise<T...>) -> PromiseStatus,
	andThen: (self: Promise<T...>, successHandler: (T...) -> (), failureHandler: ((...any) -> ())?) -> _Promise,
	catch: (self: Promise<T...>, failureHandler: (any) -> ()) -> Promise<T...>,
	tap: (self: Promise<T...>, successHandler: (T...) -> ()) -> Promise<T...>,
	andThenCall: <U...>(self: Promise<T...>, successHandler: (U...) -> (), U...) -> Promise<T...>,
	andThenReturn: (self: Promise<T...>, value: any) -> _Promise,
	cancel: (self: Promise<T...>) -> (),
	finally: (self: Promise<T...>, callback: (status: PromiseStatus) -> ()) -> _Promise,
	finallyCall: <U...>(self: Promise<T...>, callback: (U...) -> (), U...) -> _Promise,
	finallyReturn: (self: Promise<T...>, value: any) -> _Promise,
	awaitStatus: (self: Promise<T...>) -> (PromiseStatus, T...),
	await: (self: Promise<T...>) -> (boolean, T...),
	expect: (self: Promise<T...>) -> T...,
	now: (self: Promise<T...>, rejectionValue: unknown) -> Promise<T...>,
}

if include and include:FindFirstChild("Promise") then
	return require(include.Promise) :: PromiseConstructor
else
	error(`Could not find Promise from {script:GetFullName()}`)
end


local Promise = require(script.Parent.Parent.Promise)
local types = require(script.Parent.Parent.types)

local compose = require(script.Parent.Parent.utils.compose)
local instances = require(script.Parent.Parent.utils.instances)
local testRemote = require(script.Parent.Parent.utils.testRemote)

local function createRemote(name: string, builder: types.RemoteBuilder): types.Remote
	local connection: RBXScriptConnection?
	local test = testRemote.createTestRemote()
	local connected = true

	local listeners: { (...any) -> () } = {}
	local nextListenerId = 0
	local queue: { { any } } = {}

	local function noop()
		error(`Attempted to use a server-only function on the client remote '{name}'`)
	end

	local self = {
		name = name,
		type = "event" :: "event",
		test = test,
		fireAll = noop,
		fireAllExcept = noop,
		firePlayers = noop,
	} :: types.Remote

	function self:connect(listener)
		assert(connected, `Cannot use destroyed remote '{name}'`)

		local id = nextListenerId
		nextListenerId += 1
		listeners[id] = listener

		if #queue > 0 then
			for _, args in queue do
				task.spawn(listener, table.unpack(args))
			end
			table.clear(queue)
		end

		return function()
			listeners[id] = nil
		end
	end

	function self:promise(predicate, mapper)
		assert(connected, `Cannot promise destroyed event remote '{name}'`)

		return Promise.new(function(resolve, _, onCancel)
			local disconnect
			disconnect = self:connect(function(...)
				if not predicate or predicate(...) then
					disconnect()
					if mapper then
						resolve(mapper(...))
					else
						resolve(...)
					end
				end
			end)

			onCancel(disconnect)
		end)
	end

	function self:fire(...)
		assert(connected, `Cannot use destroyed remote '{name}'`)

		local arguments = table.pack(...)

		instances.promiseRemoteEvent(name):andThen(function(instance)
			instance:FireServer(table.unpack(arguments, 1, arguments.n))
			test:_fire(table.unpack(arguments, 1, arguments.n))
		end, function(error): ()
			warn(`Failed to fire remote '{name}': {error}`)
		end)
	end

	function self:destroy()
		if not connected then
			return
		end

		connected = false

		if connection then
			connection:Disconnect()
			connection = nil
		end

		table.clear(listeners)
		table.clear(queue)
	end

	local emit = compose(builder.metadata.middleware)(function(...): ()
		if next(listeners) then
			for _, listener in listeners do
				task.spawn(listener, ...)
			end
		else
			table.insert(queue, table.pack(...))
		end
	end, self)

	instances.promiseRemoteEvent(name):andThen(function(instance)
		if not connected then
			return
		end

		connection = instance.OnClientEvent:Connect(function(...)
			for index, validator in builder.metadata.parameters do
				local value = select(index, ...)
				assert(validator(value), `Invalid parameter #{index} for remote '{name}': got {value}`)
			end

			emit(...)
		end)
	end, function(error): ()
		warn(`Failed to initialize remote '{name}': {error}`)
	end)

	setmetatable(self :: {}, {
		__call = self.fire,
	})

	return self
end

return createRemote


local RunService = game:GetService("RunService")

return {
	IS_EDIT = RunService:IsStudio() and not RunService:IsRunning(),
	IS_STUDIO = RunService:IsStudio(),
	IS_CLIENT = RunService:IsClient(),
	IS_SERVER = RunService:IsServer(),
	IS_TEST = false,
}


local constants = require(script.Parent.constants)

local function getSender(player: any): Player?
	if
		constants.IS_SERVER
		and (type(player) == "table" or typeof(player) == "Instance")
		and player.ClassName == "Player"
	then
		return player :: Player
	end
	return nil
end

return getSender


local types = require(script.Parent.Parent.types)
local constants = require(script.Parent.Parent.constants)

local SCOPE = constants.IS_SERVER and "client → server" or "server → client"

local function stringify(...)
	local output = {}

	for index = 1, select("#", ...) do
		local value = select(index, ...)

		table.insert(output, `{if index > 1 then "\n" else ""}\t{index}.`)

		if type(value) == "string" then
			table.insert(output, string.format("%q", value))
		elseif type(value) == "userdata" then
			table.insert(output, `{typeof(value)}({value})`)
		else
			table.insert(output, value)
		end
	end

	if #output == 0 then
		return "\t1. (void)\n"
	end

	table.insert(output, "\n")

	return table.unpack(output)
end

local loggerMiddleware: types.Middleware = function(next, remote)
	return function(...)
		if remote.type == "event" then
			print(`\n🟡 ({SCOPE}) {remote.name}\n\n`, stringify(...))
			return next(...)
		end

		print(`\n🟣 ({SCOPE} async) {remote.name}\n`)
		print(`Parameters\n`, stringify(...))

		local results = table.pack(next(...))

		print("Returns\n", stringify(table.unpack(results, 1, results.n)))

		return table.unpack(results, 1, results.n)
	end
end

return loggerMiddleware


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

local Promise = require(script.Parent.Parent.Promise)
local types = require(script.Parent.Parent.types)
local constants = require(script.Parent.Parent.constants)
local getSender = require(script.Parent.Parent.getSender)

local SERVER = "(SERVER)"

export type ThrottleMiddlewareOptions = {
	throttle: number?,
	trailing: boolean?,
	[any]: any,
} | number

local function timeout<T...>(callback: (T...) -> (), amount: number): () -> ()
	local timer = 0
	local connection

	connection = RunService.Heartbeat:Connect(function(deltaTime)
		timer += deltaTime
		if timer >= amount then
			connection:Disconnect()
			callback()
		end
	end)

	return function()
		connection:Disconnect()
	end
end

local function throttleMiddleware(options: ThrottleMiddlewareOptions?): types.Middleware
	local throttle = 0.1
	local trailing = false

	if type(options) == "number" then
		throttle = options
	elseif type(options) == "table" then
		throttle = if options.throttle ~= nil then options.throttle else throttle
		trailing = if options.trailing ~= nil then options.trailing else trailing
	end

	local eventMiddleware: types.Middleware = function(next, remote)
		local timeouts: { [string]: () -> () } = {}
		local cache: { [string]: { [number]: unknown, n: number } } = {}

		return function(...)
			local sender = getSender(...)
			local senderId = if sender then sender.Name else SERVER
			local timeoutId = timeouts[senderId]

			cache[senderId] = table.pack(...)

			if timeoutId then
				if not constants.IS_TEST and constants.IS_STUDIO then
					warn(`🔴 throttled remote '{remote.name}' fired by '{senderId}'`)
				end
				return
			end

			timeouts[senderId] = timeout(function()
				local arguments = cache[senderId]
				timeouts[senderId] = nil
				cache[senderId] = nil

				if trailing then
					next(table.unpack(arguments, 1, arguments.n))
				end
			end, throttle)

			return next(...)
		end
	end

	local asyncMiddleware: types.Middleware = function(next, remote)
		local timeouts: { [string]: () -> () } = {}
		local cache: { [string]: { [number]: unknown, n: number } } = {}
		local pending: { [string]: boolean } = {}
		local watching: { [string]: Promise.Promise } = {}

		local function clearCacheOnPlayerDisconnect(sender: Player)
			if watching[sender.Name] then
				return
			end

			watching[sender.Name] = Promise.fromEvent(Players.PlayerRemoving, function(player)
				return player == sender
			end):andThen(function()
				timeouts[sender.Name] = nil
				cache[sender.Name] = nil
				pending[sender.Name] = nil
				watching[sender.Name] = nil
			end)
		end

		return function(...)
			local sender = getSender(...)
			local senderId = if sender then sender.Name else SERVER

			local timeoutId = timeouts[senderId]
			local isPending = pending[senderId]

			if sender then
				clearCacheOnPlayerDisconnect(sender)
			end

			if timeoutId or isPending then
				-- async remotes should try to return the previous value before
				-- rejecting the request
				local results =
					assert(cache[senderId], `🔴 throttled remote '{remote.name}' requested by '{senderId}'`)
				return table.unpack(results, 1, results.n)
			end

			pending[senderId] = true

			local ok, results = pcall(function(...)
				return table.pack(next(...))
			end, ...)

			pending[senderId] = nil

			timeouts[senderId] = timeout(function()
				timeouts[senderId] = nil
			end, throttle)

			assert(ok, results)

			cache[senderId] = results

			return table.unpack(results, 1, results.n)
		end
	end

	return function(next, remote)
		if remote.type == "event" then
			return eventMiddleware(next, remote)
		elseif remote.type == "function" then
			return asyncMiddleware(next, remote)
		else
			return next
		end
	end
end

return throttleMiddleware


local Players = game:GetService("Players")

local Promise = require(script.Parent.Parent.Promise)
local types = require(script.Parent.Parent.types)

local compose = require(script.Parent.Parent.utils.compose)
local instances = require(script.Parent.Parent.utils.instances)
local testRemote = require(script.Parent.Parent.utils.testRemote)

local function createRemote(name: string, builder: types.RemoteBuilder): types.Remote
	local instance = instances.createRemoteEvent(name, builder.metadata.unreliable)
	local test = testRemote.createTestRemote()
	local connected = true

	local listeners: { (...any) -> () } = {}
	local nextListenerId = 0

	local self = {
		name = name,
		type = "event" :: "event",
		test = test,
	} :: types.Remote

	function self:connect(listener)
		assert(connected, `Cannot connect to destroyed event remote '{name}'`)

		local id = nextListenerId
		nextListenerId += 1
		listeners[id] = listener

		return function()
			listeners[id] = nil
		end
	end

	function self:promise(predicate, mapper)
		assert(connected, `Cannot promise destroyed event remote '{name}'`)

		return Promise.new(function(resolve, _, onCancel)
			local disconnect
			disconnect = self:connect(function(...)
				if not predicate or predicate(...) then
					disconnect()
					if mapper then
						resolve(mapper(...))
					else
						resolve(...)
					end
				end
			end)

			onCancel(disconnect)
		end)
	end

	function self:fire(player, ...)
		assert(connected, `Cannot fire destroyed event remote '{name}'`)
		instance:FireClient(player, ...)
		test:_fire(player, ...) -- do not risk omitting first argument
	end

	function self:fireAll(...)
		assert(connected, `Cannot fire destroyed event remote '{name}'`)
		instance:FireAllClients(...)
		test:_fire(...)
	end

	function self:fireAllExcept(exception, ...)
		assert(connected, `Cannot fire destroyed event remote '{name}'`)
		for _, player in Players:GetPlayers() do
			if player ~= exception then
				instance:FireClient(player, ...)
			end
		end
		test:_fire(...)
	end

	function self:firePlayers(players, ...)
		assert(connected, `Cannot fire destroyed event remote '{name}'`)
		for _, player in players do
			instance:FireClient(player, ...)
		end
		test:_fire(...)
	end

	function self:destroy()
		if not connected then
			return
		end

		connected = false

		instance:Destroy()
		instance = nil :: never

		table.clear(listeners)
	end

	local emit = compose(builder.metadata.middleware)(function(...): ()
		for _, listener in listeners do
			task.spawn(listener, ...)
		end
	end, self)

	instance.OnServerEvent:Connect(function(player: Player, ...)
		for index, validator in builder.metadata.parameters do
			local value = select(index, ...)
			assert(validator(value), `Invalid parameter #{index} for event remote '{name}': got {value}`)
		end

		emit(player, ...)
	end)

	setmetatable(self :: {}, {
		__call = self.fire,
	})

	return self
end

return createRemote


local types = require(script.Parent.Parent.types)

local function compose(middlewares: { types.Middleware }): types.Middleware
	local length = #middlewares

	if length == 0 then
		return function(next)
			return next
		end
	elseif length == 1 then
		return middlewares[1]
	end

	return function(next, ...)
		for index = length, 1, -1 do
			next = middlewares[index](next, ...)
		end

		return next
	end
end

return compose


local Promise = require(script.Parent.Parent.Promise)
local constants = require(script.Parent.Parent.constants)
local mockRemotes = require(script.Parent.mockRemotes)

local container = script.Parent.Parent.container

local function promiseRemoteFunction(name: string): Promise.Promise<RemoteFunction>
	if container:FindFirstChild(name) then
		return Promise.resolve(container[name])
	end

	if constants.IS_EDIT then
		return Promise.resolve(mockRemotes.createMockRemoteFunction(name))
	end

	return Promise.fromEvent(container.ChildAdded, function(child)
		return child:IsA("RemoteFunction") and child.Name == name
	end)
end

local function promiseRemoteEvent(name: string): Promise.Promise<RemoteEvent>
	if container:FindFirstChild(name) then
		return Promise.resolve(container[name])
	end

	if constants.IS_EDIT then
		return Promise.resolve(mockRemotes.createMockRemoteEvent(name))
	end

	return Promise.fromEvent(container.ChildAdded, function(child)
		return child:IsA("RemoteEvent") and child.Name == name
	end)
end

local function createRemoteFunction(name: string): RemoteFunction
	if container:FindFirstChild(name) then
		return container[name]
	end

	if constants.IS_EDIT then
		return mockRemotes.createMockRemoteFunction(name)
	end

	local remoteFunction = Instance.new("RemoteFunction")
	remoteFunction.Name = name
	remoteFunction.Parent = container

	return remoteFunction
end

local function createRemoteEvent(name: string, unreliable: boolean): RemoteEvent
	if container:FindFirstChild(name) then
		return container[name]
	end

	if constants.IS_EDIT then
		return mockRemotes.createMockRemoteEvent(name)
	end

	local remoteEvent = Instance.new(if unreliable then "UnreliableRemoteEvent" else "RemoteEvent")
	remoteEvent.Name = name
	remoteEvent.Parent = container

	return remoteEvent :: RemoteEvent
end

return {
	promiseRemoteFunction = promiseRemoteFunction,
	promiseRemoteEvent = promiseRemoteEvent,
	createRemoteFunction = createRemoteFunction,
	createRemoteEvent = createRemoteEvent,
}


local Players = game:GetService("Players")

local mockRemoteEvents: { [string]: RemoteEvent } = {}
local mockRemoteFunctions: { [string]: RemoteFunction } = {}

local function createMockRemoteEvent(name: string): RemoteEvent
	if mockRemoteEvents[name] then
		return mockRemoteEvents[name]
	end

	local remoteEvent: RemoteEvent = {
		Name = name,
		OnClientEvent = {},
		OnServerEvent = {},
	} :: any

	local clientListeners = {}
	local serverListeners = {}

	function remoteEvent.OnClientEvent:Connect(callback: (any) -> ())
		clientListeners[callback] = true

		return {
			Connected = true,
			Disconnect = function(self: RBXScriptConnection)
				self.Connected = false
				clientListeners[callback] = nil
			end,
		} :: never
	end

	function remoteEvent.OnServerEvent:Connect(callback: (any) -> ())
		serverListeners[callback] = true

		return {
			Connected = true,
			Disconnect = function(self: RBXScriptConnection)
				self.Connected = false
				serverListeners[callback] = nil
			end,
		} :: never
	end

	function remoteEvent:FireClient(_player: Player, ...)
		for callback in clientListeners do
			callback(...)
		end
	end

	function remoteEvent:FireAllClients(...)
		for callback in clientListeners do
			callback(...)
		end
	end

	function remoteEvent:FireServer(...)
		for callback in serverListeners do
			callback(Players.LocalPlayer or {}, ...)
		end
	end

	function remoteEvent:Destroy(): ()
		mockRemoteEvents[name] = nil
		table.clear(clientListeners)
		table.clear(serverListeners)
	end

	mockRemoteEvents[name] = remoteEvent

	return remoteEvent
end

local function createMockRemoteFunction(name: string): RemoteFunction
	if mockRemoteFunctions[name] then
		return mockRemoteFunctions[name]
	end

	local remoteFunction: RemoteFunction = {
		Name = name,
		OnClientInvoke = function() end,
		OnServerInvoke = function() end,
	} :: any

	function remoteFunction:InvokeClient(_player: Player, ...)
		return self.OnClientInvoke(...)
	end

	function remoteFunction:InvokeServer(...)
		return self.OnServerInvoke(Players.LocalPlayer or {} :: any, ...)
	end

	function remoteFunction:Destroy(): ()
		mockRemoteFunctions[name] = nil
		remoteFunction.OnClientInvoke = function() end
		remoteFunction.OnServerInvoke = function() end
	end

	mockRemoteFunctions[name] = remoteFunction

	return remoteFunction
end

local function getMockRemoteEvent(name: string): RemoteEvent?
	return mockRemoteEvents[name]
end

local function getMockRemoteFunction(name: string): RemoteFunction?
	return mockRemoteFunctions[name]
end

local function destroyAll()
	table.clear(mockRemoteEvents)
	table.clear(mockRemoteFunctions)
end

return {
	createMockRemoteEvent = createMockRemoteEvent,
	createMockRemoteFunction = createMockRemoteFunction,
	getMockRemoteEvent = getMockRemoteEvent,
	getMockRemoteFunction = getMockRemoteFunction,
	destroyAll = destroyAll,
}


local types = require(script.Parent.Parent.types)
local getSender = require(script.Parent.Parent.getSender)

local function noop() end

local function createTestRemote(): types.TestRemote
	local listeners: { (...any) -> () } = {}
	local nextListenerId = 0

	local function fire(_self, first, ...)
		local shouldOmitPlayer = getSender(first)

		for _, listener in listeners do
			if shouldOmitPlayer then
				listener(...)
			else
				listener(first, ...)
			end
		end
	end

	local function onFire(_self, listener)
		local id = nextListenerId
		nextListenerId += 1
		listeners[id] = listener

		return function()
			listeners[id] = nil
		end
	end

	local function disconnectAll(_self)
		table.clear(listeners)
	end

	local testRemote: types.TestRemote = {
		_fire = fire,
		onFire = onFire,
		disconnectAll = disconnectAll,
	}

	return testRemote
end

local function createTestAsyncRemote(): types.TestAsyncRemote
	local handler: (...any) -> () = noop

	local function request(_self, first, ...)
		if getSender(first) then
			return handler(...)
		else
			return handler(first, ...)
		end
	end

	local function handleRequest(_self, newHandler)
		handler = newHandler
	end

	local function hasRequestHandler()
		return handler ~= noop
	end

	local function disconnectAll()
		handler = noop
	end

	local testAsyncRemote: types.TestAsyncRemote = {
		_request = request,
		handleRequest = handleRequest,
		hasRequestHandler = hasRequestHandler,
		disconnectAll = disconnectAll,
	}

	return testAsyncRemote
end

return {
	createTestRemote = createTestRemote,
	createTestAsyncRemote = createTestAsyncRemote,
}


local Promise = require(script.Parent.Parent.Promise)

type Promise<T> = Promise.Promise<T>

local function unwrap<T, U...>(promise: T | Promise<T>?, ...: U...): (T, U...)
	if Promise.is(promise) then
		return (promise :: Promise<T>):expect()
	end

	return promise :: T, ...
end

return unwrap


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	A specialised state object for following a goal state smoothly over time,
	using a TweenInfo to shape the motion.

	https://elttob.uk/Fusion/0.3/api-reference/animation/types/tween/
]]

local Package = script.Parent.Parent
local Types = require(Package.Types)
local External = require(Package.External)
-- Memory
local checkLifetime = require(Package.Memory.checkLifetime)
-- Graph
local depend = require(Package.Graph.depend)
local evaluate = require(Package.Graph.evaluate)
-- State
local castToState = require(Package.State.castToState)
local peek = require(Package.State.peek)
-- Animation
local ExternalTime = require(Package.Animation.ExternalTime)
local Stopwatch = require(Package.Animation.Stopwatch)
local lerpType = require(Package.Animation.lerpType)
local getTweenRatio = require(Package.Animation.getTweenRatio)
local getTweenDuration = require(Package.Animation.getTweenDuration)
-- Utility
local nicknames = require(Package.Utility.nicknames)

export type Self<T> = Types.Tween<T> & {
	_activeDuration: number,
	_activeElapsed: number,
	_activeFrom: T,
	_activeTo: T,
	_activeTweenInfo: TweenInfo,
	_goal: Types.UsedAs<T>,
	_stopwatch: Stopwatch.Stopwatch?,
	_tweenInfo: Types.UsedAs<TweenInfo>,
}

local class = {}
class.type = "State"
class.kind = "Tween"
class.timeliness = "eager"

local METATABLE = table.freeze {__index = class}

local function Tween<T>(
	scope: Types.Scope<unknown>,
	goal: Types.UsedAs<T>,
	tweenInfo: Types.UsedAs<TweenInfo>?
): Types.Tween<T>
	local createdAt = os.clock()

	local goalState = castToState(goal)
	local stopwatch = nil
	if goalState ~= nil then
		stopwatch = Stopwatch(scope, ExternalTime(scope))
	end

	local self: Self<T> = setmetatable(
		{
			createdAt = createdAt,
			dependencySet = {},
			dependentSet = {},
			lastChange = nil,
			scope = scope,
			validity = "invalid",
			_activeDuration = nil,
			_activeElapsed = nil,
			_activeFrom = nil,
			_activeTo = nil,
			_activeTweenInfo = nil,
			_EXTREMELY_DANGEROUS_usedAsValue = peek(goal),
			_goal = goal,
			_stopwatch = stopwatch,
			_tweenInfo = tweenInfo or TweenInfo.new()
		},
		METATABLE
	) :: any
	local destroy = function()
		self.scope = nil
		for dependency in pairs(self.dependencySet) do
			dependency.dependentSet[self] = nil
		end
	end
	self.oldestTask = destroy
	nicknames[self.oldestTask] = "Tween"
	table.insert(scope, destroy)

	if goalState ~= nil then
		checkLifetime.bOutlivesA(
			scope, self.oldestTask,
			goalState.scope, goalState.oldestTask,
			checkLifetime.formatters.animationGoal
		)
	end

	local tweenInfoState = castToState(tweenInfo)
	if tweenInfoState ~= nil then
		checkLifetime.bOutlivesA(
			scope, self.oldestTask,
			tweenInfoState.scope, tweenInfoState.oldestTask,
			checkLifetime.formatters.parameter, "tween info"
		)
	end

	-- Eagerly evaluated objects need to evaluate themselves so that they're
	-- valid at all times.
	evaluate(self, true)

	return self
end

function class._evaluate<T>(
	self: Self<T>
): boolean
	local goal = castToState(self._goal)
	-- Allow non-state goals to pass through transparently.
	if goal == nil then
		self._EXTREMELY_DANGEROUS_usedAsValue = self._goal :: T
		return false
	end
	depend(self, goal)
	local newTweenTo = peek(goal)
	-- Protect against NaN goals.
	if newTweenTo ~= newTweenTo then
		External.logWarn("tweenNanGoal")
		return false
	end
	local stopwatch = self._stopwatch :: Stopwatch.Stopwatch
	local tweenInfo = peek(self._tweenInfo) :: TweenInfo
	-- Restart animation when the goal changes, or if the tween info changes
	-- partway through another animation.
	if
		self._activeTo ~= newTweenTo or
		(self._activeElapsed < self._activeDuration and self._activeTweenInfo ~= tweenInfo)
	then
		self._activeDuration = getTweenDuration(tweenInfo)
		self._activeFrom = self._EXTREMELY_DANGEROUS_usedAsValue
		self._activeTo = newTweenTo
		self._activeTweenInfo = tweenInfo
		stopwatch:zero()
		stopwatch:unpause()
	end
	depend(self, stopwatch)
	self._activeElapsed = peek(stopwatch)
	if
		self._activeFrom == self._activeTo or -- endpoints match
		self._activeElapsed >= self._activeDuration or -- animation is done
		typeof(self._activeTo) ~= typeof(self._activeFrom) -- type difference
	then
		self._activeFrom = self._activeTo
		self._activeElapsed = self._activeDuration
		stopwatch:pause()
	end
	-- Compute actual tweened value.
	local ratio = getTweenRatio(tweenInfo, self._activeElapsed)
	local oldValue = self._EXTREMELY_DANGEROUS_usedAsValue
	local newValue = lerpType(self._activeFrom, self._activeTo, ratio) :: T
	-- Protect against NaN after motion.
	if newValue ~= newValue then
		External.logWarn("tweenNanMotion")
		newValue = self._activeTo
	end
	-- Push update and check for similarity.
	-- Don't need to use the similarity test here because this code doesn't
	-- deal with tables, and NaN is already guarded against, so the similarity
	-- test doesn't actually add any new safety here.
	self._EXTREMELY_DANGEROUS_usedAsValue = newValue
	return oldValue ~= newValue
end

table.freeze(class)
return Tween :: Types.TweenConstructor


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	Given a `tweenInfo`, returns how many seconds it will take before the tween
	finishes moving. The result may be infinite if the tween repeats forever.
]]

local TweenService = game:GetService("TweenService")

local function getTweenDuration(
	tweenInfo: TweenInfo
): number
	if tweenInfo.RepeatCount <= -1 then
		return math.huge
	end
	local tweenDuration = tweenInfo.DelayTime + tweenInfo.Time
	if tweenInfo.Reverses then
		tweenDuration += tweenInfo.Time
	end
	tweenDuration *= tweenInfo.RepeatCount + 1
	return tweenDuration
end

return getTweenDuration