local DraggerSchemaCore = script.Parent
local FreeformDragger = require(DraggerSchemaCore.FreeformDragger)

local Packages = DraggerSchemaCore.Parent
local DraggerFramework = require(Packages.DraggerFramework)

local Utility = DraggerFramework.Utility
local getGeometry = Utility.getGeometry
local getFaceInstance = Utility.getFaceInstance

type DraggerContext = DraggerFramework.DraggerContext
type DraggerToolModel = DraggerFramework.DraggerToolModel
type DraggerStateType = DraggerFramework.DraggerStateType
type FreeformDragInfo = FreeformDragger.FreeformDragInfo

export type DragInfoEvent = {
	ClickedItem: BasePart,
	ClickedPosition: Vector3,
	SelectionNowContainsSelectable: boolean,
	SelectionDidContainSelectable: boolean,
	DoubleClicked: boolean,
}

--[[
	Get the "basis point" (the closest clicked vertex on the part) and put in
	the local space of the primaryCFrame of the selection.
	Also, check if there is any FaceInstance on the clicked face, and if
	there is, record it for potential FaceInstance selection on mouse up.
]]

local function buildFreeformDragInfo(draggerContext: DraggerContext, draggerToolModel: DraggerToolModel, event: DragInfoEvent): FreeformDragInfo
	local closestVertex = Vector3.zero
	local attachmentBeingDragged

	if event.ClickedItem:IsA("Attachment") then
		closestVertex = event.ClickedItem.WorldPosition
		attachmentBeingDragged = event.ClickedItem
	else
		local geometry = getGeometry(event.ClickedItem, event.ClickedPosition)
		local closestDistance = math.huge
		for _, vert in ipairs(geometry.vertices) do
			local distance = (vert.position - event.ClickedPosition).Magnitude
			if distance < closestDistance then
				closestDistance = distance
				closestVertex = vert.position
			end
		end
	end

	local localMainCFrame = draggerToolModel._selectionInfo:getLocalBoundingBox()
	local closestVertexInPrimarySpace = localMainCFrame:PointToObjectSpace(closestVertex)
	local clickInPrimarySpace = localMainCFrame:PointToObjectSpace(event.ClickedPosition)

	-- Find the FaceInstance to potentially select if we never actually start
	-- dragging before mouse up.
	local clickedFaceInstance
	if (event.SelectionDidContainSelectable or event.DoubleClicked) and
		event.ClickedItem:IsA("BasePart") then
		clickedFaceInstance = getFaceInstance(event.ClickedItem, event.ClickedPosition)
	end

	return {
		basisPoint = closestVertexInPrimarySpace,
		clickPoint = clickInPrimarySpace,
		clickedFaceInstance = clickedFaceInstance,
		attachmentBeingDragged = attachmentBeingDragged,
	}
end

return function(draggerContext: DraggerContext, draggerToolModel: DraggerToolModel, event: DragInfoEvent): (DraggerStateType, FreeformDragInfo?)
	if not event.ClickedItem then
		return "DragSelecting", nil
	elseif event.ClickedItem:IsA("Constraint") or
		event.ClickedItem:IsA("WeldConstraint") or
		event.ClickedItem:IsA("NoCollisionConstraint") then
		-- Note: WeldConstraint and NoCollisionConstraint ARE NOT Constraints,
		-- but the act like they are, so we do need all the checks.

		-- Do nothing here: When clicking a constraint, do not allow the
		-- fall-through of starting a box select.
		return "Ready", nil
	elseif event.SelectionNowContainsSelectable then
		if draggerToolModel:doesAllowFreeformDrag() then
			return "FreeformSelectionDrag", buildFreeformDragInfo(draggerContext, draggerToolModel, event)
		else
			return "Ready", nil
		end
	else
		return "DragSelecting", nil
	end
end


return function(draggerContext)
	-- Nothing to do, the incremental updates as the mouse moves are sufficient
	-- to get the selection up to date in the core schema.
end


local DraggerSchemaCore = script.Parent
local shouldSelectSubPart = require(DraggerSchemaCore.shouldSelectSubPart)
local getSelectableWithCache = require(DraggerSchemaCore.getSelectableWithCache)

local Packages = DraggerSchemaCore.Parent
local DraggerFramework = require(Packages.DraggerFramework)

type DraggerContext = DraggerFramework.DraggerContext
type Gizmo = DraggerFramework.Gizmo

type Target =
	| BasePart
	| Gizmo

return function(draggerContext: DraggerContext, mouseRay: Ray, currentSelection: { Instance }): (Instance?, Target?, number?)
	local shouldDrillSelection = shouldSelectSubPart(draggerContext)
	local hitResult = workspace:Raycast(mouseRay.Origin, mouseRay.Direction)

	-- Selection favoring: If there is a selected object and a non-selected
	-- object almost exactly coincident underneath the mouse, then we should
	-- favor the selected one, even if due to floating point error the non
	-- selected one comes out slightly closer.
	-- Without this case, if you duplicate objects and try to drag them, you
	-- may end up dragging only one of the objects because you clicked on the
	-- old non-selected copy, as opposed to the selected one you meant to.
	local hitPosition = mouseRay.Origin + mouseRay.Direction
	local hitItem: (BasePart | Gizmo)?

	if hitResult then
		hitPosition = hitResult.Position
		hitItem = hitResult.Instance

		local params = RaycastParams.new()
		params.FilterType = Enum.RaycastFilterType.Include
		params.FilterDescendantsInstances = currentSelection

		local hitSelected = workspace:Raycast(mouseRay.Origin, mouseRay.Direction, params)

		if hitSelected and hitSelected.Position:FuzzyEq(hitPosition) then
			hitItem = hitSelected.Instance
		end
	end

	local hitDistance = (mouseRay.Origin - hitPosition).Magnitude
	local hitResult = draggerContext:gizmoRaycast(mouseRay.Origin, mouseRay.Direction)

	if hitResult and
		(draggerContext:shouldDrawConstraintsOnTop() or (hitResult.Distance < hitDistance)) then
		hitDistance = hitResult.Distance
		hitItem = hitResult.Instance
	end

	if hitItem then
		local hitSelectable = getSelectableWithCache(hitItem, shouldDrillSelection, {})
		return hitSelectable, hitItem, hitDistance
	else
		return nil, nil, nil
	end
end


local DraggerSchemaCore = script.Parent
local Packages = DraggerSchemaCore.Parent

local dispatchWorldClick = require(DraggerSchemaCore.dispatchWorldClick)
type FreeformDragInfo = dispatchWorldClick.FreeformDragInfo

local DraggerFramework = require(Packages.DraggerFramework)
type DraggerContext = DraggerFramework.DraggerContext

return function(draggerContext: DraggerContext, currentSelection: { Instance }, dragInfo: FreeformDragInfo?, isDoubleClick: boolean): { Instance }?
	-- dragInfo will be nil when we didn't start a potential freeform drag
	if dragInfo and dragInfo.clickedFaceInstance then
		return { dragInfo.clickedFaceInstance }
	end

	return nil
end


--[[
	Get the selectable object from the passed-in part or model.

	If the instance is an Attachment or Constraint, it is returned directly.

	If the instance belongs to a model the top-level model is returned, unless
	the alt key is held, in which case the part itself is returned. Locked parts
	are not considered selectable.

	getMostNested specifies whether to drill down to the most nested selectable
	object instead of taking the least nested object like normal.

	Accept a cache parameter to optimize the box selection tight loop case
]]

return function(instance: Instance, getMostNested: boolean, cache: { [Instance]: Instance }): Instance?
	-- First, the easy nil and attachment cases.
	if not instance then
		return nil
	elseif instance:IsA("Attachment") or instance:IsA("Constraint") or
		instance:IsA("WeldConstraint") or instance:IsA("NoCollisionConstraint") then
		-- Note, this attachment case has to come before the fast-flat-model
		-- optimization, otherwise selecting an attachment might result in the
		-- parent part of the attachment being selected instead for cases with
		-- parts nested under other parts.
		return instance
	end

	-- Fast-flat-model optimization. Most places have a large number of parts
	-- directly underneath models with no further nesting of models. We have a
	-- nice optimization in this case, where we can cache what selectable
	-- a given parent will result in. That way we can handle many of the
	-- selectable determinations with a single hashtable lookup.
	local instanceParent = instance.Parent
	local fastCached = instanceParent and cache[instanceParent]

	if fastCached then
		-- Note: We don't need to worry about the locked case here. In order to
		-- return a Model as a selectable thanks to a locked part here, we would
		-- already have had to have already found an unlocked part in the same
		-- model, in which case it does not hurt to return it.
		return fastCached
	end

	-- Make sure that instance is a model or non-locked instance
	if instance:IsA("BasePart") then
		if instance.Locked then
			return nil
		end
	elseif not (instance:IsA("Model") or instance:IsA("Tool")) then
		return nil
	end

	if getMostNested then
		return instance
	else
		local selectableInstance = instance
		local candidate = instanceParent

		while candidate do
			if candidate:IsA("Model") and candidate ~= workspace then
				selectableInstance = candidate
			end

			candidate = candidate.Parent
		end

		-- Add to cache. Note: We could add all of the entries in the hierarchy
		-- between instance and selectableInstance to the cache, but that
		-- actually has worse perf, since it costs time to modify cache, while
		-- most models are flat and thus don't benefit from it.

		if selectableInstance ~= instance then
			if instanceParent then
				cache[instanceParent] = selectableInstance
			end
		end

		return selectableInstance
	end
end


local DraggerSchemaCore = script.Parent
local Packages = DraggerSchemaCore.Parent

local DraggerFramework = require(Packages.DraggerFramework)
type DraggerContext = DraggerFramework.DraggerContext

return function(draggerContext: DraggerContext, hoverSelectable: Instance): string?
	if draggerContext:areConstraintDetailsShown() and
		(hoverSelectable:IsA("Attachment") or hoverSelectable:IsA("Constraint")) then
		-- Don't show hover boxes for constraints with visible details, they
		-- have their own special hover highlighting.
		return nil
	else
		-- Note: We're using SelectionBox directly here. For other schemas which
		-- aren't based on Instances, you would return a class which :extends
		-- Component or PureComponent and knows how to adorn your selectables.
		return "SelectionBox"
	end
end


--[[
	DraggerSchema, Core Dragger Implementation

	A schema describing the dragger framework implementation which the core
	modeling tools use, where the user selects and transforms an aggregate of
	parts and attachments.
]]

local TransformHandlesImplementation = require(script.TransformHandlesImplementation)
local ExtrudeHandlesImplementation = require(script.ExtrudeHandlesImplementation)
local BoundsChangedTracker = require(script.BoundsChangedTracker)
local HoverEscapeDetector = require(script.HoverEscapeDetector)
local FreeformDragger = require(script.FreeformDragger)
local SelectionInfo = require(script.SelectionInfo)
local Selection = require(script.Selection)

local getSelectionBoxComponent = require(script.getSelectionBoxComponent)
local isExclusiveSelectable = require(script.isExclusiveSelectable)
local dispatchWorldClick = require(script.dispatchWorldClick)
local getNextSelectables = require(script.getNextSelectables)
local addUndoWaypoint = require(script.addUndoWaypoint)
local getMouseTarget = require(script.getMouseTarget)
local setActivePoint = require(script.setActivePoint)
local beginBoxSelect = require(script.beginBoxSelect)
local endBoxSelect = require(script.endBoxSelect)
local setHover = require(script.setHover)

local DraggerSchema = {
	-- class Selection
	-- (for convenience, doesn't have to be part of the schema, you must pass
	-- a selection object conforming to this API to the DraggerContext)
	-- A class describing what objects are currently selected and notifying the
	-- dragger framework when the selection changes. This class should expose
	-- the same API as RBX::SelectionService (Get/Set/SelectionChanged).
	-- NOTE: The selectables that will be passed to Selection:Set are the same
	-- selectables that the framework got from Selection:Get / getMouseTarget /
	-- beginBoxSelect.
	-- NOTE: The :Set method on this object will be called with an extra "hint"
	-- parameter, which is a table like so: {Removed = {...}, Added = {...}}.
	-- This hint parameter describes a way that the selection can be more
	-- efficiently updated by first removing and then adding some selectables to
	-- it rather than entirely replacing the existing selection. You can
	-- completely ignore the hint if you want.
	Selection = Selection,

	-- class TransformHandlesImplementation
	-- (for convenience, doesn't have to be part of the schema, you must pass
	-- this to the MoveHandles constructor)
	TransformHandlesImplementation = TransformHandlesImplementation,

	-- class ExtrudeHandlesImplementation
	-- (for convenience, doesn't have to be part of the schema, you must pass
	-- this to the ExtrudeHandles constructor)
	ExtrudeHandlesImplementation = ExtrudeHandlesImplementation,

	-- function getSelectionBoxComponent(draggerContext, selectable)
	-- May return nil if you do your own hovered object rendering by making
	-- use of the Schema.setHover calls, use this if you want to have a similar
	-- pulsing hovered object highlight to the core draggers.
	-- getSelectionBoxComponent should return a kind of Roact element which
	-- has same props as an RBX::SelectionBox, but where Adornee can point at
	-- the type of selectable in your schema (which may not be an Instance).
	getSelectionBoxComponent = getSelectionBoxComponent,

	-- class HoverEscapeDetector
	-- A class detecting when a given hovered object changes in some way that
	-- may cause it to no longer be the hovered object thanks to reasons other
	-- than the dragger changing it.
	HoverEscapeDetector = HoverEscapeDetector,

	-- function getMouseTarget(draggerContext, mouseRay, currentSelection)
	-- Get:
	--  * the current selectable
	--  * the hit item within that selectable
	--  * the distance along the mouseRay to the surface of the hit item
	-- The selectable is the thing that will be added to the selection,
	-- and the hit item within it gives optional additional context about why
	-- the selectable was selected to other schema specific code.
	getMouseTarget = getMouseTarget,

	-- function setHover(draggerContext, hoverSelectable, hoverItem)
	-- Sets what the environment should treat as the currently hovered object,
	-- if the environment needs to draw the hovered object differently in some
	-- way.
	setHover = setHover,

	-- function setActivePoint(draggerContext, selectionInfo)
	-- Sets the "active point", that is, the location where inserted or
	-- duplicated objects will be initially placed.
	setActivePoint = setActivePoint,

	-- function isExclusiveSelectable(draggerContext, selectable, item)
	-- When clicking on the given selectable/item, should the selection be
	-- cleared and set to exactly that item and nothing else?
	isExclusiveSelectable = isExclusiveSelectable,

	-- function dispatchClick(draggerContext, clickEvent)
	-- Decide what the dragger should do when clicking in the 3d space, given
	-- a clickEvent, which is a table: {
	--     ClickedSelectable, -- The selectable that was clicked
	--     ClickedItem, -- The item in the selectable which was clicked
	--     SelectionDidContainSelectable, -- did the old selection previously
	--         contain the Selectable that was clicked?
	--     SelectionNowContainsSelectable, -- does the new selection now
	--         contain the Selectable that was clicked?
	-- }
	-- The function should return one of the following states (as a string):
	--  * "Ready" -- Do nothing until the next click
	--  * "DragSelecting" -- Begin a rubber band select
	--  * "FreeformSelectionDrag" -- Begin dragging the current selection
	-- The dragger will then enter the state specified by the return value.
	dispatchWorldClick = dispatchWorldClick,

	-- function getNextSelectables(draggerContext, currentSelection, dragInfo, isDoubleClick)
	-- When a freeform drag fails to be started due to the user clicking and
	-- releasing without moving their mouse, the dragger will try to select
	-- a list of "next" selectables instead.
	-- For the core draggers, this means selecting a face instance underneath
	-- the clicked part if one exists.
	-- This function returns a list of those next selectables to select, or nil
	-- if the old selection should be kept.
	-- The "dragInfo" parameter is the dragInfo which was returned by
	-- dispatchClick, or nil if no freeform drag was attempted.
	getNextSelectables = getNextSelectables,

	-- class FreeformDragger
	-- .new(draggerContext, draggerToolModel, dragInfo)
	--     Begins a drag with a dragInfo that was returned from dispatchClick.
	-- :rotate(axis)
	---    Rotate the dragged parts around a given axis (T/R key hookup)
	-- :update()
	--     Update the drag with a given mouse target.
	-- :render()
	--     Optional rendering function which returns a Roact component
	--     representing any snaps / joints / other UI associated with the drag.
	-- :destroy()
	--     Finish the freeform drag.
	FreeformDragger = FreeformDragger,

	-- function beginBoxSelect(draggerContext)
	-- Returns an array of tables structured like so:
	--     {Center = Vector3, Selectable = ...}
	-- containing each candidate selectable to potentially be included,
	-- in the selection after a box select, and where each one is located.
	beginBoxSelect = beginBoxSelect,

	-- function endBoxSelect(draggerContext, finalSelection)
	-- Does any final cleanup / finalization that should be done after a box
	-- select has occurred. In the core schema, we don't have to do anything
	-- in this function, but if you were implementing some form of soft
	-- selection (where you only update your model classes once at the end of
	-- the box select), you would want this hook.
	endBoxSelect = endBoxSelect,

	-- class BoundsChangedTracker
	-- Tracks changes to the position of the selected items due to factors
	-- external to the dragger. For instance, when you press Ctrl+Z, the
	-- ChangeHistoryService may move the selected items, and the dragger needs
	-- to know that this happened so that it can rerender the handles around the
	-- new location of the moved items.
	-- .new(draggerContext, onChangedHandler)
	--     When an item moves, the function onChangedHandler should be called
	--     with the item which moved.
	-- :setSelection(selectionInfo)
	--     Set the items to track position changes for
	-- :install()
	--     Begin tracking bounds changes on the target parts
	-- :uninstall()
	--     Stop tracking bounds changes on the target parts
	BoundsChangedTracker = BoundsChangedTracker,

	-- class SelectionInfo
	-- Exposes derived aggregate information about the selection.
	-- It is returned as an object so that some of the information can be lazily
	-- calculated if desired.
	-- The returned SelectionInfo can contain whatever fields and methods you
	-- need depending on your tool implementations, however, the following are
	-- are required for the DraggerFramework code:
	-- SelectionInfo
	-- .new(draggerContext, selection)
	--     Construct a SelectionInfo for a given selection of selectables.
	-- :isEmpty()
	--     Returns true when there were no items in the selection to calculate
	--     a well formed SelectionInfo from. When this returns true, the other
	--     functions on SelectionInfo will not be called by the framework.
	-- :getBoundingBox() -> (CFrame at, Vector3 offset, Vector3 size)
	--     Returns the primary "basis" of the selection which the bounding box
	--     is oriented around in `at`, as well as the offset of the center of
	--     the bounding box from that basis, and the size of the bounding box.
	--     Supporting "Use Local Space" means changing the return value of this
	--     method depending on whether the option is set.
	-- :doesContainItem(item)
	--     Returns whether a given item is part of the selection or not.
	-- :isDynamic()
	--     For optimization purposes, do fields of the SelectionInfo change
	--     every single frame? If this is hardcoded to true then you don't need
	--     to implement BoundsChangedTracker, as the bounds are assumed to be
	--     changing every frame... at the cost of significantly worse perf.
	SelectionInfo = SelectionInfo,

	-- function addUndoWaypoint(draggerContext, waypointName)
	-- Add an undo waypoint. A simple implementation would be to simply delegate
	-- to draggerContext:addUndoWaypoint, however if you have some Lua-side
	-- undo state then you have an opportunity to edit that state in this.
	addUndoWaypoint = addUndoWaypoint,
}

export type Selection = Selection.Class
export type SelectionInfo = SelectionInfo.Class
export type FreeformDragger = FreeformDragger.Class
export type HoverEscapeDetector = HoverEscapeDetector.Class
export type BoundsChangedTracker = BoundsChangedTracker.Class
export type IDraggerSchema = typeof(DraggerSchema)

return DraggerSchema


-- Attachments like to be exclusively selected unless we're trying to multiple
-- select them.
local Packages = script.Parent.Parent

local DraggerFramework = require(Packages.DraggerFramework)
type DraggerContext = DraggerFramework.DraggerContext

return function(draggerContext: DraggerContext, selectable: Instance?, item: Instance)
	return item:IsA("Attachment") and not draggerContext:shouldExtendSelection()
end


local DraggerSchemaCore = script.Parent
local SelectionInfo = require(DraggerSchemaCore.SelectionInfo)

local Packages = DraggerSchemaCore.Parent
local DraggerFramework = require(Packages.DraggerFramework)

type DraggerContext = DraggerFramework.DraggerContext
type SelectionInfo = SelectionInfo.Class

return function(draggerContext: DraggerContext, selectionInfo: SelectionInfo)
	if not selectionInfo:isEmpty() then
		local cframe, offset = selectionInfo:getBoundingBox()
		draggerContext:setInsertPoint(cframe * offset)
	end
end


-- For the core manipulators, just set the Studio Service hovered instance to
-- the instance which the mouse was directly hovered over (hoverItem).
local DraggerSchemaCore = script.Parent
local Packages = DraggerSchemaCore.Parent

local DraggerFramework = require(Packages.DraggerFramework)
type DraggerContext = DraggerFramework.DraggerContext

return function(draggerContext: DraggerContext, hoverSelectable: Instance?, hoverItem: Instance?)
	draggerContext:setHoverInstance(hoverItem)
end


local DraggerSchemaCore = script.Parent
local Packages = DraggerSchemaCore.Parent

local DraggerFramework = require(Packages.DraggerFramework)
type DraggerContext = DraggerFramework.DraggerContext

return function(draggerContext: DraggerContext)
    -- Geometric mode: hover -> select model, alt-hover -> select sub part
    -- Physical mode: hover -> select sub part, alt-hover -> select model
    --                Physical mode has a user setting that will invert this if desired
    local altBehavior = draggerContext:areConstraintsEnabled() and draggerContext:shouldSelectScopeByDefault()
    return altBehavior ~= draggerContext:isAltKeyDown()
end


--!strict
local EasyStore = {}
EasyStore.__index = EasyStore

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)

export type Config = {
    MockDataStoreService: DataStoreService?,
    AutoSaveInterval: number?,
}

type ClassInternal<T = any> = typeof(setmetatable({} :: {
    Default: T,

    _dataCache: {
        [number]: any,
    },

    _viewCache: {
        [number]: any,
    },

    _maid: Trove?,
    _autoSaveInterval: number,

    _netInit: Network.Class<LazyTable>,
    _netPatch: Network.Class<LazyTable>,

    _dataStoreService: DataStoreService?,
    _dataStoreName: string,
}, EasyStore))

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

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

type PlayerDataInternal<T = any> = typeof(setmetatable({} :: {
    CurrentData: T,
    Server: ClassInternal<T>,

    Owner: Player?,
    Maid: Trove,

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

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

local function free(data: PlayerDataInternal)
    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

function PlayerData.Save<T>(self: PlayerDataInternal<T>): 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 dataStore = self.Server:GetDataStore()
        local key = getProfileKey(assert(owner).UserId)

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

        resolve()
    end)
end

function EasyStore.Get<T>(self: ClassInternal<T>, userId: number?): PlayerDataInternal<T>
    local default = self.Default

    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, "EasyStore:Get must be called with a userId on the server")

        local player = (RunContext.IsServer or RunContext.IsEdit) and Players:GetPlayerByUserId(userId) or nil
        local cache: { PlayerDataInternal<T> } = if player then self._dataCache else self._viewCache

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

            local data: PlayerDataInternal<T> = setmetatable({
                CurrentData = DeltaTable.DeepCopy(default),
                Server = self,

                HasPendingChanges = false,
                IsLoaded = false,
                Owner = player,

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

            task.spawn(function ()
                local RETRY_COUNT = 5
                local store = self:GetDataStore()

                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 and not rawget(default :: any, "RobloxMetaData") then
                            -- ProfileService holdover, use the Data key.
                            currentData = currentData.Data
                        end

                        if currentData then
                            reconcileTable(currentData, default :: any)
                            data.CurrentData = currentData

                            data.IsLoaded = true
                            data.Loaded:Fire(true)

                            break
                        end
                    elseif retry < RETRY_COUNT then
                        task.wait(retry / 2)
                    end
                end

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

                maid:Add(function ()
                    task.cancel(autoSave)
                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: PlayerDataInternal<T> = self._dataCache[myUserId]

        if not data then
            data = setmetatable({
                CurrentData = DeltaTable.DeepCopy(default),
                Server = self,

                HasPendingChanges = false,
                Updated = Signal.new(),
                Owner = myPlayer,

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

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

            self._dataCache[myUserId] = data
        end

        return data
    end
end

function EasyStore.Load<T>(self: ClassInternal<T>, userId: number?): Promise
    local data = self: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("DataStore:Load failed for player", userId)
                    reject(data)
                end
            end)
        end
    end)
end

function PlayerData.Patch<T>(self: PlayerDataInternal<T>, patchFunc: (edit: T) -> ()): 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 = self.Server._netPatch:Server()

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

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

            self.HasPendingChanges = true
        end

        resolve()
    end)
end

function EasyStore.Read<T>(self: ClassInternal<T>, userId: number?): T?
    if userId == nil or userId > 0 then
        local data = self:Get(userId)

        if data.IsLoaded then
            return data.CurrentData
        end
    end

    return nil
end

function EasyStore.PromiseRead<T>(self: ClassInternal<T>, userId: number?): Promise
    return Promise.new(function (resolve, reject)
        local data = self: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

function PlayerData.Connect(self: PlayerDataInternal, path: string, callback: (value: any) -> ()): Signal.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: PlayerDataInternal): boolean
    return self.Owner ~= nil or RunContext.IsEdit
end

function PlayerData.Release(self: PlayerDataInternal, force: boolean?): 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

function EasyStore.GetDataStore(self: ClassInternal): DataStore
    if RunContext.IsServer or RunContext.IsEdit then
        local dataStoreService = assert(self._dataStoreService)
        return dataStoreService:GetDataStore(self._dataStoreName)
    end

    error("DataStore is not available on the client!")
end

function EasyStore.Start<T>(self: ClassInternal<T>): Trove
    if self._maid then
        return self._maid
    end

    local maid = Trove.new()
    self._maid = maid

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

        local function onPlayerAdded(player: Player)
            EasyStore.Load(self, player.UserId):andThen(function (plrData: PlayerDataInternal<T>)
                local data = DeltaTable.DeepCopy(plrData.CurrentData)
                init:Fire(player, data)
            end)

            self:Load(player.UserId)
        end

        local function onPlayerRemoving(player: Player)
            local data: PlayerDataInternal<T> = self._dataCache[player.UserId]

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

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

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

        local dead = false

        maid:Add(function ()
            dead = true
        end)

        game:BindToClose(function ()
            if dead then
                return
            end

            local promises = {}

            for userId, data in pairs(self._dataCache) do
                local promise = data:Save()
                    :andThen(function ()
                        free(data)
                    end)

                table.insert(promises, promise)
            end

            local all = Promise.all(promises)
            all:expect()
        end)
    else
        local init = self._netInit:Client()
        local patch = self._netPatch:Client()

        init:On(function (data: LazyTable)
            local myData = self:Get()
            myData.CurrentData = data :: any

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

        patch:On(function (patch: LazyTable)
            local myData = self:Get()

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

    return maid
end

local function create<TEMPLATE>(storeName: string, default: TEMPLATE, config: Config?): ClassInternal<TEMPLATE>
    local dataStoreService = (config and config.MockDataStoreService) or DataStoreService

    local netInit = Network.Event(`DataStore_{storeName}_DataInit`, function (data: unknown): LazyTable
        assert(type(data) == "table")
        return data :: LazyTable
    end)

    local netPatch = Network.Event(`DataStore_{storeName}_DataPatch`, function (patch: unknown): LazyTable
        assert(type(patch) == "table")
        return patch :: LazyTable
    end)

    local store = setmetatable({
        Default = default,

        _dataCache = {},
        _viewCache = {},

        _netInit = netInit :: any,
        _netPatch = netPatch :: any,

        _dataStoreName = storeName,
        _dataStoreService = dataStoreService,
        _autoSaveInterval = (config and config.AutoSaveInterval) or 15,
    }, EasyStore)

    return store
end

--------------------------------------------------------------------------------------------------------------------------------------------
-- PUBLIC EXPORTS
-- !! FIXME: Use proper metatable types when Luau is more stable with generics.
--------------------------------------------------------------------------------------------------------------------------------------------

--- Represents a player's data profile in EasyStore.
export type PlayerData<SaveData = any> = {
    --- The current state of the player's data. This is immutable and should only be modified through `PlayerData:Patch` on the server.
    CurrentData: SaveData,

    --- The `Player` who owns this data profile.
    Owner: Player?,

    --- Flag indicating whether the player's data has finished loading.
    IsLoaded: boolean,

    --- Flag indicating whether there are unsaved changes to the player's data and a save is pending.
    HasPendingChanges: boolean,

    --- **[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.
    Save: (self: PlayerData<SaveData>) -> Promise,

    --- **[!! 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.
    Patch: (self: PlayerData<SaveData>, patchFunc: (edit: SaveData) -> ()) -> Promise,

    --- Connects a callback to be invoked when a specific path in the player's data changes.
    --- A path is a string of keys in the data table separated by slashes (`/`).
    --- * For example, to listen for changes to `CurrentData.Inventory.Gold`, the path would be `"Inventory/Gold"`.
    --- * The callback will be invoked with the new value at that path whenever it changes.
    --- @param path The path to listen for changes on.
    --- @param callback The function to invoke when the value at the specified path changes.
    --- @return A `Signal.Connection` that can be used to disconnect the callback.
    Connect: (self: PlayerData<SaveData>, path: string, callback: (value: any) -> ()) -> Signal.Connection,

    --- Returns true if the player's data is available to be modified on the server.
    IsActive: (self: PlayerData<SaveData>) -> boolean,

    --- Releases the player's data profile, saving it if necessary.
    --- * If the player's data is active (the player is in-game), and `force` is not set to true, the release will fail and return false.
    --- * If the player's data is not active, or `force` is true, the profile will be released and the function will return true.
    --- @param force Whether to forcefully release the profile even if it is active. Defaults to false.
    --- @return `boolean` Whether the profile was successfully released.
    Release: (self: PlayerData<SaveData>, force: boolean?) -> boolean,
}

--- Represents an EasyStore instance for managing player data.
--- Can exist on the server, client, or in edit mode.
--- Use `EasyStore.Create` to create a new instance.
export type Class<SaveData = any> = {
    --- 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. Ignored on the client and in edit mode.
    Get: (self: Class<SaveData>, userId: number?) -> PlayerData<SaveData>,

    --- 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.
    Load: (self: Class<SaveData>, userId: number?) -> Promise,

    --- 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.
    Read: (self: Class<SaveData>, userId: number?) -> SaveData?,

    --- Returns a promise that resolves with the player's data if it has loaded.
    --- Will reject if the data fails to load for any reason.
    PromiseRead: (self: Class<SaveData>, userId: number?) -> Promise,

    --- **[!! Server Only !!]** Gets the DataStore object used by this EasyStore instance.
    --- * Will error if called on the client.
    --- @return `DataStore` The DataStore object used by this EasyStore instance.
    GetDataStore: (self: Class<SaveData>) -> DataStore,

    --- Starts the DataStore. Sets up player added/removed handlers on the server, and network event handlers on the client.
    --- Returns a `Trove` that can be used to clean up the DataStore if ever necessary.
    --- @return `Trove` A maid that can be used to clean up the DataStore.
    Start: (self: Class<SaveData>) -> Trove,

    --- The default data structure used for new players.
    Default: SaveData,
}

--- Main EasyStore module for managing player data with Roblox DataStores.
export type EasyStore = {
    --- Creates a new EasyStore instance using the specified DataStore name, provided default data structure, and optional configuration.
    --- * Make sure to call `EasyStore:Start()` on the returned instance to initialize it!
    --- @param storeName The name of the DataStore to use.
    --- @param default A dictionary describing the default data structure for new players, and the general type shape of the data through Luau's type system.
    --- @param config Optional configuration for the EasyStore instance.
    --- @param config.MockDataStoreService An optional mock DataStoreService to use instead of the real one. Defaults to `game:GetService("DataStoreService")`.
    --- @param config.AutoSaveInterval The interval, in seconds, at which to autosave player data. Defaults to 15 seconds.
    Create: <TEMPLATE>(storeName: string, default: TEMPLATE, config: Config?) -> Class<TEMPLATE>,
}

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

local result: EasyStore = table.freeze({
    Create = create,
}) :: any

return result


--!strict

local LinearPath = {}
LinearPath.__index = LinearPath

export type Class = typeof(setmetatable({} :: {
    Dists: { number },
    Points: { Vector3 },
    Normals: { Vector3 },
    Length: number,
}, LinearPath))

--- Creates a new LinearPath from a list of points.
function LinearPath.new(points: { Vector3 }): Class
    local length = 0
    local count = #points

    local dists = table.create(count)
    local normals = table.create(count)
    table.insert(dists, 0)

    for i = 2, count do
        local prev = points[i - 1]
        local curr = points[i]

        local offset = (curr - prev)
        local dist = offset.Magnitude
        local normal = offset / dist

        length += dist
        dists[i] = length
        normals[i - 1] = normal

        if i == count then
            normals[i] = normal
        end
    end

    local graph = {
        Dists = dists,
        Points = points,
        Length = length,
        Normals = normals,
    }

    return setmetatable(graph, LinearPath)
end

--- Creates a LinearPath from a Path instance.
function LinearPath.fromPath(path: Path): Class
    local waypoints: { PathWaypoint } = path:GetWaypoints()
    local points: { Vector3 } = table.create(#waypoints)

    for i, waypoint in waypoints do
        points[i] = waypoint.Position
    end

    return LinearPath.new(points)
end

--- Creates an empty LinearPath at the world origin.
function LinearPath.empty()
    return setmetatable({
        Dists = { 0 },
        Length = 0,

        Points = { Vector3.zero },
        Normals = { Vector3.xAxis },
    }, LinearPath)
end

--- Gets the position on the path at the given distance.
function LinearPath.GetPosition(self: Class, dist: number)
    local points = self.Points
    local dists = self.Dists

    if dist < 0 then
        return points[1]
    elseif dist > self.Length then
        return points[#points]
    end

    local L, R = 1, #dists
    local M: number

    while L <= R do
        M = (L + R) // 2

        if dists[M] < dist then
            L = M + 1
        elseif dists[M] > dist then
            R = M - 1
        else
            break
        end
    end

    -- Correct overshoot
    while dists[M] > dist do
        M -= 1
    end

    local p0, p1 = points[M], points[M + 1]
    local d0, d1 = dists[M], dists[M + 1]

    if p1 and d1 then
        if d0 > 0 then
            local alpha = (dist - d0) / (d1 - d0)
            return p0:Lerp(p1, alpha)
        else
            return p0:Lerp(p1, dist / d1)
        end
    else
        return p0 or Vector3.zero
    end
end

--- Finds the closest point on the path to the target point, returning the distance along the path.
function LinearPath.ClosestPoint(self: Class, target: Vector3): number
    local dists = self.Dists
    local points = self.Points

    local closestDist = math.huge
    local bestPoint = -1

    for i = 1, #points - 1 do
        local pointA = points[i]
        local pointB = points[i + 1]

        local offset = (pointB - pointA)
        local length = dists[i + 1] - dists[i]

        local dir = offset / length
        local dot = dir:Dot(target - pointA)

        local onGraph = pointA + (dir * dot)
        local dist = (target - onGraph).Magnitude

        if dist < closestDist then
            bestPoint = dists[i] + dot
            closestDist = dist
        end
    end

    return bestPoint
end

--- Gets the distance along the path at the given point index.
function LinearPath.GetDist(self: Class, index: number): number
    local dist = self.Dists[index // 1]

    if dist then
        return dist
    elseif index < 1 then
        return 0
    else
        return self.Length
    end
end

return LinearPath


--!strict
local Shared = script.Parent

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

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

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

type OwnershipRecord = {
    Promise: Promise?,
    Changed: Signal<boolean>?,
}

local ownership = {} :: {
    [Player]: {
        [string]: OwnershipRecord,
    },
}

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

local mockPurchaseChecks = {} :: {
    [string]: () -> boolean,
}

local function addMockPurchaseCheck(id: string, check: () -> boolean)
    assert(RunContext.IsServer, "Mock purchase checks can only be added from the server!")
    mockPurchaseChecks[id] = check
end

local function removeMockPurchaseCheck(id: string)
    assert(RunContext.IsServer, "Mock purchase checks can only be removed from the server!")
    mockPurchaseChecks[id] = nil
end

local function canMockPurchase()
    for i, check in mockPurchaseChecks do
        if not check() then
            return false
        end
    end

    return true
end

export type Ownership = {
    Id: number,
    InfoType: Enum.InfoType,
}

export type VapidOwnership = {
    Id: number?,
    InfoType: Enum.InfoType?,
}

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

    Creator: {
        Id: number,
        Name: string,
        CreatorType: "User" | "Group",
        CreatorTargetId: number,
        HasVerifiedBadge: boolean,
    },

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

local playerOwnsItem = Promise.promisify(function(player: Player, itemId: any, infoType: Enum.InfoType?)
    local userId = player.UserId

    if userId < 1 then
        return RunContext.IsStudio
    end

    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:PlayerOwnsBundleAsync(player, itemId)
    elseif infoType == Enum.InfoType.Product then
        return false
    else
        return MarketplaceService:PlayerOwnsAssetAsync(player, itemId)
    end
end)

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

local GRANT_PURCHASE = Network.Event("Marketplace_GrantPurchase", function (player, id, infoType)
    assert(typeof(player) == "Instance" and player:IsA("Player"))
    assert(id ~= nil)

    if infoType == nil then
        return player, id, Enum.InfoType.Asset
    end

    assert(typeof(infoType) == "EnumItem" and infoType:IsA("InfoType"))
    return player, id, infoType
end)

local PROMPT_PURCHASE = Network.Event("Marketplace_PromptPurchase", function (id, infoType)
    assert(id ~= nil)

    if infoType == nil then
        return id, Enum.InfoType.Asset
    end

    assert(typeof(infoType) == "EnumItem" and infoType:IsA("InfoType"))
    return id, infoType
end)

local promptPurchaseFinished = Signal.new() :: Signal<Player, any, Enum.InfoType>

local function getOwnershipRecord<T>(player: Player, id: T, infoType: Enum.InfoType?): OwnershipRecord
    local infoType = infoType or Enum.InfoType.Asset

    local ownershipTable = ownership[player]
        or (function()
            ownership[player] = {}
            return ownership[player]
        end)()

    local key = `{infoType.Name}:{id}`

    local data = ownershipTable[key]
        or (function()
            ownershipTable[key] = {
                Promise = nil,
                Changed = Signal.new(),
            }

            return ownershipTable[key]
        end)()

    return data
end

local function promiseOwnership(player: Player, id: number, infoType: Enum.InfoType?): Promise
    local data = getOwnershipRecord(player, id, infoType)

    if not data.Promise then
        local new = Promise.retryWithDelay(playerOwnsItem, 5, 2, player, id, infoType)
        data.Promise = new

        new:catch(function(err)
            if data.Promise == new then
                data.Promise = nil
            end
        end)
    end

    return assert(data.Promise)
end

local function getOwnershipChangedSignal(player: Player, id: number, infoType: Enum.InfoType?): Signal<boolean>
    local data = getOwnershipRecord(player, id, infoType)

    if not data.Changed then
        data.Changed = Signal.new()
    end

    return assert(data.Changed)
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

local function bulkResolveOwnership(player: Player, ownershipList: { VapidOwnership }): Promise
    local n = #ownershipList
    local promises = table.create(n) :: { Promise }

    for i = 1, n do
        local rec = ownershipList[i]

        if rec.Id == nil or rec.Id <= 0 then
            promises[i] = Promise.resolve(false)
            continue
        end

        promises[i] = promiseOwnership(player, rec.Id, rec.InfoType)
    end

    return Promise.allSettled(promises):andThen(function()
        local results = table.create(n)

        for i = 1, n do
            local p = promises[i]
            local ok, val = p:await()
            results[i] = (ok and val) or false
        end

        return results
    end)
end

local function grantPurchase<T>(player: Player, id: T, infoType: Enum.InfoType)
    local data = getOwnershipRecord(player, id, infoType)

    if data.Changed then
        data.Changed:FireDeferred(true)
    end

    if data.Promise then
        data.Promise:cancel()
    end

    if RunContext.IsServer then
        local server = GRANT_PURCHASE:Server()
        server:FireAll(player, id, infoType)
    end

    data.Promise = Promise.resolve(true)
    promptPurchaseFinished:Fire(player, id, infoType)
end

local function promptPurchase(id: number, infoType: Enum.InfoType?, player: Player?)
    if RunContext.IsClient and not RunContext.IsEdit then
        local client = PROMPT_PURCHASE:Client()
        client:Fire(id, infoType or Enum.InfoType.Asset)
        return
    end

    if RunContext.IsEdit then
        player = Players.LocalPlayer
    end

    if not player then
        return
    end

    local canMock = canMockPurchase()

    if canMock or RunContext.IsEdit then
        local success, owned = promiseOwnership(player, id, infoType):await()

        if not (success and owned) then
            if canMock then
                warn("MOCKING PURCHASE", id, infoType, "FOR PLAYER", player)
            elseif RunContext.IsEdit then
                warn("MOCKING PURCHASE IN EDIT MODE", id, infoType, "FOR PLAYER", player)
            end

            grantPurchase(player, id, infoType or Enum.InfoType.Asset)
        end

        return
    end

    if infoType == Enum.InfoType.GamePass then
        MarketplaceService:PromptGamePassPurchase(player, id)
    elseif infoType == Enum.InfoType.Bundle then
        MarketplaceService:PromptBundlePurchase(player, id)
    elseif infoType == Enum.InfoType.Product then
        MarketplaceService:PromptProductPurchase(player, id)
    else
        MarketplaceService:PromptPurchase(player, id)
    end
end

if RunContext.IsServer or RunContext.IsEdit then
    local promptServer = PROMPT_PURCHASE:Server()

    promptServer:On(function (player, id, infoType)
        promptPurchase(id :: any, infoType, player)
    end)

    local function createPurchaseFinishedCallback(infoType: Enum.InfoType)
        return function <T>(player: Instance, id: T, purchased: boolean)
            if not player:IsA("Player") then
                return
            end

            if not purchased then
                return
            end

            grantPurchase(player, id, infoType)
        end
    end

    MarketplaceService.PromptPurchaseFinished:Connect(createPurchaseFinishedCallback(Enum.InfoType.Asset))
    MarketplaceService.PromptBundlePurchaseFinished:Connect(createPurchaseFinishedCallback(Enum.InfoType.Bundle))
    MarketplaceService.PromptGamePassPurchaseFinished:Connect(createPurchaseFinishedCallback(Enum.InfoType.GamePass))
    MarketplaceService.PromptSubscriptionPurchaseFinished:Connect(createPurchaseFinishedCallback(Enum.InfoType.Subscription))

    local function onPlayerRemoving(player: Player)
        local ownershipTable = ownership[player]

        if ownershipTable then
            for i, data in ownershipTable do
                if data.Changed then
                    data.Changed:Destroy()
                end

                if data.Promise then
                    data.Promise:cancel()
                end
            end

            ownership[player] = nil
        end
    end

    Players.PlayerRemoving:Connect(onPlayerRemoving)
elseif not RunContext.IsEdit then
    local client = GRANT_PURCHASE:Client()
    client:On(grantPurchase)
end

return table.freeze({
    GetOwnership = promiseOwnership,
    PromptPurchase = promptPurchase,
    GetProductInfo = promiseProductInfo,
    BulkResolveOwnership = bulkResolveOwnership,
    GetOwnershipChangedSignal = getOwnershipChangedSignal,

    AddMockPurchaseCheck = addMockPurchaseCheck,
    RemoveMockPurchaseCheck = removeMockPurchaseCheck,

    PromptPurchaseFinished = promptPurchaseFinished,
})


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

local PropSquash = require(script.PropSquash)
local ArraySquash = require(Shared.ArraySquash)
local ReflectionService = game:GetService("ReflectionService")

local Accumulated = ArraySquash.Accumulated
local AccI32 = Accumulated.I32

local STRING = Squash.string()
local VLQ = Squash.vlq()

type Cursor = Squash.Cursor

type INST = {
    Index: number,
    ObjectIds: { number },
}

type PROP = {
    Name: string,
    ClassIndex: number,
    TypeIndex: number,
    Values: { any },
}

---------------------------------------------------------------------------------------
-- FIXME BEGIN: Remove these once Luau LSP properly grabs these type declarations

type ReflectionType = {
    EngineType: string,
    ScriptType: string?,
    EnumType: string?,
    InstanceType: string?
}

type ReflectedProperty = {
    Name: string,
    Serialized: boolean,

    Type: ReflectionType,
    ContentType: Enum.AssetType?,
    -- DefaultValue: any,
    -- MinValue: number?,
    -- MaxValue: number?

    Display: {
        Category: string,
        DeprecationMessage: string?,
        LayoutOrder: number,
    }?,

    Permits: {
        Read: SecurityCapabilities?,
        ReadParallel: SecurityCapabilities?,
        Write: SecurityCapabilities?,
        WriteParallel: SecurityCapabilities?,
    },
}

type ReflectedClass = {
    Name: string,
    Superclass: string?,
    Subclasses: { string },

    Display: {
        Category: string,
        DeprecationMessage: string?,
        LayoutOrder: number,
    }?,

    Permits: {
        GetService: SecurityCapabilities?,
        New: SecurityCapabilities?,
    }
}

type ReflectedProperties = { ReflectedProperty }
type ReflectedClasses = { ReflectedClass }

-- FIXME END
---------------------------------------------------------------------------------------

local propSetters = {} :: {
    [string]: (Instance, any) -> ()
}

propSetters.Tags = function (inst, tags)
    for i, tag in tags do
        inst:AddTag(tag)
    end
end

propSetters.Attributes = function (inst, attributes)
    for name, value in attributes do
        inst:SetAttribute(name, value)
    end
end

local function serialize(root: Instance): buffer
    local cursor = Squash.cursor()

    local instances: { Instance } = root:GetDescendants()
    table.insert(instances, 1, root) -- Include the root instance

    -- Collect classes and assign ref IDs
    local refMap = {}
    local classNames = {}

    local classMap = {} :: {
        [string]: INST
    }

    for i, inst in instances do
        local className = inst.ClassName

        local classInfo = classMap[className] or (function ()
            local index = #classNames + 1
            classNames[index] = className

            classMap[className] = {
                Index = index,
                ObjectIds = {},
            }

            return classMap[className]
        end)()

        refMap[inst] = i
        inst:SetAttribute("__msref", i)

        table.insert(classInfo.ObjectIds, i)
    end

    -- Collect properties
    local props = {}

    for i, className in ipairs(classNames) do
        local class = classMap[className]
        local objectIds = class.ObjectIds
        local propData: ReflectedProperties = ReflectionService:GetPropertiesOfClass(className)

        for i, prop in propData do
            local display = prop.Display
            local permits = prop.Permits

            if not (prop.Serialized and permits.Write) then
                continue
            end

            if display and display.DeprecationMessage then
                continue
            end

            local name = prop.Name
            local propType = prop.Type

            local engineType = propType.EngineType
            local typeIndex = PropSquash.TypeEnum[engineType]

            if typeIndex == nil then
                continue
            end

            local prop = {
                Name = name,
                ClassIndex = i,
                TypeIndex = typeIndex,
                Values = {},
            }

            for i, objectId in ipairs(objectIds) do
                local instance = instances[objectId]
                local value = (instance :: any)[name]
                table.insert(prop.Values, value)
            end

            table.insert(props, prop)
        end

        -- Manually collect tags and attributes
        local attributeMap = {}
        local tagMap = {}

        for i, objectId in objectIds do
            local object = instances[objectId]
            local attributes = object:GetAttributes()
            local tags = object:GetTags()

            if #tags > 0 then
                tagMap[objectId] = tags
            end

            if next(attributes) then
                attributeMap[objectId] = attributes
            end
        end

        local collections = {
            Tags = tagMap,
            Attributes = attributeMap
        }

        for key: string, map: any in pairs(collections) do
            if not next(map) then
                continue
            end

            local prop = {
                Name = key,
                ClassIndex = i,
                TypeIndex = PropSquash.TypeEnum[key],
                Values = {},
            }

            for i, objectId in ipairs(class.ObjectIds) do
                prop.Values[i] = map[objectId] or {}
            end

            table.insert(props, prop)
        end
    end

    -- ~~~~~~~~~~~~~~~~~~  BEGIN SERIALIZATION  ~~~~~~~~~~~~~~~~~~
    -- (Squash is weird and requires us to write things in reverse)

    --------------------------
    -- 4: Instance Hierarchy
    --------------------------

    local parentIds = {}

    for i, inst in instances do
        local inst = instances[i]
        local parent = inst.Parent
        parentIds[i] = parent and refMap[parent] or 0
    end

    AccI32.ser(cursor, parentIds)

    --------------------------
    -- 3: Properties
    --------------------------

    for i = #props, 1, -1 do
        local prop = props[i]
        local values = prop.Values
        local typeName = PropSquash.TypeIds[prop.TypeIndex]

        -- 3e: Property Values
        PropSquash.CompressArray(cursor, typeName, values)

        -- 3d: Property Type
        VLQ.ser(cursor, prop.TypeIndex)

        -- 3c: Class Index
        VLQ.ser(cursor, prop.ClassIndex)

        -- 3b: Property Name
        STRING.ser(cursor, prop.Name)
    end

    -- 3a: Property Count
    VLQ.ser(cursor, #props)

    --------------------------
    -- 2: Class Names
    --------------------------

    for i = #classNames, 1, -1 do
        local className = classNames[i]
        local class = classMap[className]

        -- 2d: Object Ids
        AccI32.ser(cursor, class.ObjectIds)

        -- 2c: Class Index
        VLQ.ser(cursor, class.Index)

        -- 2b: Class Name
        STRING.ser(cursor, className)
    end

    -- 2a: Class Count
    VLQ.ser(cursor, #classNames)

    --------------------------
    -- 1: Header
    --------------------------

    -- 1b: Version
    VLQ.ser(cursor, 1)

    -- 1a: Magic
    STRING.ser(cursor, "RBXMSQSH")

    -- ~~~~~~~~ END SERIALIZATION ~~~~~~~~

    for i, object in instances do
        object:SetAttribute("__msref", nil)
    end

    return Squash.tobuffer(cursor)
end

local function deserialize(buf: buffer): Instance
    local cursor = Squash.frombuffer(buf)

    local magic = STRING.des(cursor)
    assert(magic == "RBXMSQSH", "ModelSquash - Provided buffer is not a valid ModelSquash buffer!")

    local version = VLQ.des(cursor)

    if version ~= 1 then
        error("ModelSquash - Unsupported format version! (Expected 1)")
    end

    -- Deserialize classes
    local objects = {}
    local classMap = {}
    local numClasses = VLQ.des(cursor)

    for i = 1, numClasses do
        local className = STRING.des(cursor)
        local classIndex = VLQ.des(cursor)
        local objIds = AccI32.des(cursor)

        for i, id in objIds do
            objects[id] = Instance.new(className)
        end

        classMap[classIndex] = {
            Name = className,
            ObjectIds = objIds,
        }
    end

    -- Deserialize properties
    local numProps = VLQ.des(cursor)

    for i = 1, numProps do
        local propName = STRING.des(cursor)
        local classIndex = VLQ.des(cursor)
        local typeIndex = VLQ.des(cursor)

        local typeName = PropSquash.TypeIds[typeIndex]
        local classInfo = classMap[classIndex]
        local objectIds = classInfo.ObjectIds

        local setter = propSetters[propName]
        local ok, result = PropSquash.DecompressArray(cursor, typeName)
        local values = ok and result or {}

        for j, objectId in ipairs(objectIds) do
            local inst = objects[objectId]
            local value = values[j]

            if typeName == "Ref" then
                value = objects[value]
            end

            if setter then
                setter(inst, value)
            else
                local success = pcall(function ()
                    (inst :: any)[propName] = value
                end)

                if not success then
                    warn("ModelSquash - Could not set property value:", classInfo.Name, propName, value)
                end
            end
        end
    end

    -- Deserialize hierarchy
    local parentIds = AccI32.des(cursor)

    for i, object in ipairs(objects) do
        local parentRef = parentIds[i]

        if parentRef > 0 then
            object.Parent = objects[parentRef]
        end
    end

    -- Return the root object
    return objects[1]
end

return table.freeze({
    ser = serialize,
    des = deserialize,

    Serialize = serialize,
    Deserialize = deserialize,
})


local function index(key: string): string
    return `|{key:gsub("|", "\\|")}`
end

local function addLock(inst: Instance, lock: string, key: string)
    local list = tostring(inst:GetAttribute(lock) or "")
    key = index(key)

    if not list:find(key) then
        list ..= key
        inst:SetAttribute(lock, list)
    end
end

local function removeLock(inst: Instance, lock: string, key: string)
    local list = tostring(inst:GetAttribute(lock) or "")
    key = index(key)

    if list:find(key) then
        list = list:gsub(key, "")
        inst:SetAttribute(lock, list)
    end
end

local function hasLock(inst: Instance, lock: string, key: string?): boolean
    local list = tostring(inst:GetAttribute(lock) or "")

    if key then
        key = index(key)
        return list:find(key) ~= nil
    else
        return list ~= ""
    end
end

local function getLockChangedSignal(inst: Instance, lock: string)
    return inst:GetAttributeChangedSignal(lock)
end

return table.freeze({
    AddLock = addLock,
    HasLock = hasLock,
    RemoveLock = removeLock,
    GetLockChangedSignal = getLockChangedSignal,
})


--!strict

local Client = {}
Client.__index = Client

local Network = script.Parent
local Types = require(Network.Types)
local Signal = Types.Signal

type Signal<T...> = Types.Signal<T...>
type EventOptions = Types.EventOptions
type Validator<T...> = Types.Validator<T...>

export type Class<T... = ...any> = typeof(setmetatable({} :: {
    Id: string,
    Validator: Validator<T...>,
    Dispatch: Signal<T...>,
    Receiver: (T...) -> ()?,
}, Client))

function Client.Fire<T...>(self: Class<T...>, ...: T...)
    self.Dispatch:Fire(...)
end

function Client.On<T...>(self: Class<T...>, callback: (T...) -> ())
    self.Receiver = function (...: any)
        if pcall(self.Validator, ...) then
            callback(...)
        end
    end
end

local function newClient<T...>(id: string, validate: Validator<T...>): Class<T...>
    return setmetatable({
        Id = id,
        Validator = validate,
        Receiver = nil :: ((T...) -> ())?,
        Dispatch = Signal.new() :: Signal<T...>,
    }, Client)
end

return table.freeze({
    new = newClient,
})


--!strict

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

local reliable = script:FindFirstChildOfClass("RemoteEvent")
local unreliable = script:FindFirstChildOfClass("UnreliableRemoteEvent")

if not (reliable and unreliable) then
    if RunContext.IsServer or RunContext.IsEdit then
        if not reliable then
            local newReliable = Instance.new("RemoteEvent")
            newReliable.Name = "Reliable"
            newReliable.Parent = script
            reliable = newReliable
        end

        if not unreliable then
            local newUnreliable = Instance.new("UnreliableRemoteEvent")
            newUnreliable.Name = "Unreliable"
            newUnreliable.Parent = script
            unreliable = newUnreliable
        end
    else
        local newReliable = script:WaitForChild("Reliable")
        assert(newReliable and newReliable:IsA("RemoteEvent"), "RemoteEvent not found!")

        local newUnreliable = script:WaitForChild("Unreliable")
        assert(newUnreliable and newUnreliable:IsA("UnreliableRemoteEvent"), "UnreliableRemoteEvent not found!")

        reliable = newReliable
        unreliable = newUnreliable
    end
end

return table.freeze({
    Reliable = assert(reliable),
    Unreliable = assert(unreliable),
})


--!strict

local Server = {}
Server.__index = Server

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

local Types = require(Network.Types)
local Signal = Types.Signal

type Signal<T...> = Types.Signal<T...>
type EventOptions = Types.EventOptions
type Validator<T...> = Types.Validator<T...>

export type Class<T... = ...any> = typeof(setmetatable({} :: {
    Id: string,
    Validator: Validator<T...>,
    Dispatch: Signal<(Player, T...)>,
    Receiver: (Player, T...) -> ()?,
}, Server))

function Server.Fire<T...>(self: Class<T...>, player: Player, ...: T...)
    self.Dispatch:Fire(player, ...)
end

function Server.FireAll<T...>(self: Class<T...>, ...: T...)
    for _, player in Players:GetPlayers() do
        self.Dispatch:Fire(player, ...)
    end
end

function Server.FireAllExcept<T...>(self: Class<T...>, except: Player, ...: T...)
    for _, player in Players:GetPlayers() do
        if player ~= except then
            self.Dispatch:Fire(player, ...)
        end
    end
end

function Server.FireList<T...>(self: Class<T...>, list: { Player }, ...: T...)
    for _, player in list do
        self.Dispatch:Fire(player, ...)
    end
end

function Server.FireWithFilter<T...>(self: Class<T...>, filter: (Player) -> boolean, ...: T...)
    for _, player in Players:GetPlayers() do
        if filter(player) then
            self.Dispatch:Fire(player, ...)
        end
    end
end

function Server.On<T...>(self: Class<T...>, callback: (player: Player, T...) -> ())
    self.Receiver = function (player, ...: any)
        if pcall(self.Validator, ...) then
            callback(player, ...)
        end
    end
end

local function newServer<T...>(id: string, validate: Validator<T...>): Class<T...>
    return setmetatable({
        Id = id,
        Validator = validate,
        Receiver = nil :: ((Player, T...) -> ())?,
        Dispatch = Signal.new() :: Signal<(Player, T...)>,
    }, Server)
end

return table.freeze({
    new = newServer,
})


--!strict

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

export type Signal<T...> = Signal.Signal<T...>
export type Validator<T...> = (...unknown) -> T...

export type EventOptions = {
    Name: string,
    Reliable: boolean?,
    Unreliable: boolean?,
}

return {
    Signal = Signal,
}


--!strict
local Shared = script.Parent

local RunContext = require(Shared.RunContext)
local Players = game:GetService("Players")

local Client = require(script.Client)
local Server = require(script.Server)
local Types = require(script.Types)

type Client<T...> = Client.Class<T...>
type Server<T...> = Server.Class<T...>

type EventOptions = Types.EventOptions
type Validator<T...> = Types.Validator<T...>

local Remotes = require(script.Remotes)
local Unreliable = Remotes.Unreliable
local Reliable = Remotes.Reliable

local Event = {}
Event.__index = Event

export type Class<T... = ...any> = typeof(setmetatable({} :: {
    Id: string,
    Reliable: boolean,

    _server: Server<T...>?,
    _client: Client<T...>?,
}, Event))

function Event.Server<T...>(self: Class<T...>): Server<T...>
    return assert(self._server, "Server events can only be accessed from the server!")
end

function Event.Client<T...>(self: Class<T...>): Client<T...>
    return assert(self._client, "Client events can only be accessed from the client!")
end

local eventIndex = {} :: {
    [string]: Class
}

local function newEvent<T...>(id: string | EventOptions, validate: Validator<T...>): Class<T...>
    local name, reliable

    if type(id) == "string" then
        name = id
        reliable = true
    else
        name = id.Name

        if id.Reliable ~= nil then
            reliable = id.Reliable
        elseif id.Unreliable ~= nil then
            reliable = not id.Unreliable
        else
            reliable = true
        end
    end

    assert(name, "Event name must be provided!")
    assert(validate, "Event validator must be provided!")

    local event: Class<T...> = setmetatable({
        Id = name,
        Reliable = reliable,

        _client = nil :: Client<T...>?,
        _server = nil :: Server<T...>?,
    }, Event)

    if eventIndex[name] then
        error(`Event with name {name} already exists!`)
    end

    eventIndex[name] = event :: any

    if RunContext.IsEdit then
        local server = Server.new(name, validate)
        local client = Client.new(name, validate)

        server.Dispatch:Connect(function (player: Player, ...: any)
            if pcall(validate, ...) then
                if client.Receiver then
                    client.Receiver(...)
                end
            end
        end)

        client.Dispatch:Connect(function (...: any)
            if pcall(validate, ...) then
                local player = Players.LocalPlayer

                if player and server.Receiver then
                    server.Receiver(player, ...)
                end
            end
        end)

        event._server = server
        event._client = client
    elseif RunContext.IsServer then
        local server = Server.new(name, validate)

        server.Dispatch:Connect(function (player: Player, ...: any)
            if pcall(validate, ...) then
                if reliable then
                    Reliable:FireClient(player, name, ...)
                else
                    Unreliable:FireClient(player, name, ...)
                end
            end
        end)

        event._server = server
    elseif RunContext.IsClient then
        local client = Client.new(name, validate)

        client.Dispatch:Connect(function (...: any)
            if pcall(validate, ...) then
                if reliable then
                    Reliable:FireServer(name, ...)
                else
                    Unreliable:FireServer(name, ...)
                end
            end
        end)

        event._client = client
    end

    return event
end

local function newReliableEvent<T...>(name: string, validate: Validator<T...>): Class<T...>
    local options: EventOptions | string = {
        Name = name,
        Reliable = true
    }

    return newEvent(options, validate)
end

local function newUnreliableEvent<T...>(name: string, validate: Validator<T...>): Class<T...>
    local options: EventOptions | string = {
        Name = name,
        Unreliable = true
    }

    return newEvent(options, validate)
end

local function createEventReceiver<T...>(reliable: boolean, nameIndex: number, consumer: (Class, T...) -> ())
    return function (...)
        local args = { ... }
        local eventName = table.remove(args, nameIndex)
        local event = eventName and eventIndex[eventName]

        if event and event.Reliable == reliable then
            consumer(event, table.unpack(args))
        end
    end
end

if RunContext.IsServer then
    local function onServerReceive(event: Class, player: Player, ...: any)
        local server = event:Server()

        if server.Receiver then
            server.Receiver(player, ...)
        end
    end

    Unreliable.OnServerEvent:Connect(createEventReceiver(false, 2, onServerReceive))
    Reliable.OnServerEvent:Connect(createEventReceiver(true, 2, onServerReceive))
elseif RunContext.IsClient then
    local function onClientReceive(event: Class, ...: any)
        local client = event:Client()

        if client.Receiver then
            client.Receiver(...)
        end
    end

    Unreliable.OnClientEvent:Connect(createEventReceiver(false, 1, onClientReceive))
    Reliable.OnClientEvent:Connect(createEventReceiver(true, 1, onClientReceive))
end

return table.freeze({
    Event = newEvent,
    ReliableEvent = newReliableEvent,
    UnreliableEvent = newUnreliableEvent,
})


local Buffer = {}
Buffer.__index = Buffer

export type Class = typeof(setmetatable({} :: {
    _buf: buffer,
    _pos: number,

    _bitsValue: number,
    _bitsLength: number,
}, Buffer))

export type LowHigh = {
    Low: number,
    High: number,
}

function Buffer.new(buf: buffer, pos: number?): Class
    if pos then
        local len = buffer.len(buf)
        assert(pos >= 0 and pos <= len, "Position out of bounds!")
    end

    return setmetatable({
        _buf = buf,
        _pos = pos or 0,

        _bitsValue = 0,
        _bitsLength = 0,
    }, Buffer)
end

local function checkRead(self: Class, size: number)
    local len = buffer.len(self._buf)
    assert(self._pos + size <= len, "Attempt to read beyond buffer length!")
end

function Buffer.GetLength(self: Class): number
    return buffer.len(self._buf)
end

function Buffer.GetPosition(self: Class): number
    return self._pos
end

function Buffer.GetBuffer(self: Class): buffer
    return self._buf
end

function Buffer.SetPosition(self: Class, pos: number)
    local len = buffer.len(self._buf)
    assert(pos >= 0 and pos <= len, "Position out of bounds!")

    self._pos = pos
end

function Buffer.Skip(self: Class, size: number)
    checkRead(self, size)
    self._pos += size
end

function Buffer.ReadInt8(self: Class): number
    checkRead(self, 1)

    local value = buffer.readi8(self._buf, self._pos)
    self._pos += 1

    return value
end

function Buffer.ReadUInt8(self: Class): number
    checkRead(self, 1)

    local value = buffer.readu8(self._buf, self._pos)
    self._pos += 1

    return value
end

function Buffer.ReadInt16(self: Class): number
    checkRead(self, 2)

    local value = buffer.readi16(self._buf, self._pos)
    self._pos += 2

    return value
end

function Buffer.ReadUInt16(self: Class): number
    checkRead(self, 2)

    local value = buffer.readu16(self._buf, self._pos)
    self._pos += 2

    return value
end

function Buffer.ReadInt32(self: Class): number
    checkRead(self, 4)

    local value = buffer.readi32(self._buf, self._pos)
    self._pos += 4

    return value
end

function Buffer.ReadUInt32(self: Class): number
    checkRead(self, 4)

    local value = buffer.readu32(self._buf, self._pos)
    self._pos += 4

    return value
end

function Buffer.ReadInt64(self: Class): LowHigh
    checkRead(self, 8)

    local low = buffer.readu32(self._buf, self._pos)
    local high = buffer.readi32(self._buf, self._pos + 4)
    self._pos += 8

    return {
        Low = low,
        High = high,
    }
end

function Buffer.ReadUInt64(self: Class): LowHigh
    checkRead(self, 8)

    local low = buffer.readu32(self._buf, self._pos)
    local high = buffer.readu32(self._buf, self._pos + 4)
    self._pos += 8

    return {
        Low = low,
        High = high,
    }
end

function Buffer.ReadFloat32(self: Class): number
    checkRead(self, 4)

    local value = buffer.readf32(self._buf, self._pos)
    self._pos += 4

    return value
end

function Buffer.ReadFloat64(self: Class): number
    checkRead(self, 8)

    local value = buffer.readf64(self._buf, self._pos)
    self._pos += 8

    return value
end

function Buffer.ReadVarInt(self: Class): number
    local result = 0
    local shift = 0

    while true do
        local inByte = self:ReadUInt8()
        result = bit32.bor(result, bit32.lshift(bit32.band(inByte, 0x7F), shift))

        if not bit32.btest(inByte, 0x80) then
            break
        end

        shift += 7
    end

    return result
end

function Buffer.ReadBytes(self: Class, size: number): { number }
    local bytes = table.create(size, 0)
    checkRead(self, size)

    for i = 1, size do
        bytes[i] = self:ReadUInt8()
    end

    return bytes
end

function Buffer.ReadString(self: Class, length: number?): string
    local str = ""

    while true do
        local char = self:ReadUInt8()

        if length == nil then
            if char == 0 then
                break
            end

            str ..= string.char(char)
        else
            str ..= string.char(char)

            if #str >= length then
                break
            end
        end
    end

    return str
end

function Buffer.ReadVector2(self: Class): Vector2
    local x = self:ReadFloat32()
    local y = self:ReadFloat32()
    return Vector2.new(x, y)
end

function Buffer.ReadVector3(self: Class): Vector3
    local x = self:ReadFloat32()
    local y = self:ReadFloat32()
    local z = self:ReadFloat32()
    return Vector3.new(x, y, z)
end

function Buffer.FlushBits(self: Class)
    self._bitsValue = 0
    self._bitsLength = 0
end

function Buffer.ReadBits(self: Class, n: number)
    local bitsValue = self._bitsValue
    local bitsLength = self._bitsLength

    while bitsLength < n do
        local byte = self:ReadUInt8()

        for i = 0, 7 do
            bitsValue = bit32.bor(bit32.lshift(bitsValue, 1), bit32.band(bit32.rshift(byte, i), 1))
        end

        bitsLength += 8
    end

    local value = 0

    for bit = 0, n - 1 do
        bitsLength -= 1
        value = bit32.bor(value, bit32.lshift(bit32.band(bit32.rshift(bitsValue, bitsLength), 1), bit))
    end

    self._bitsValue = bitsValue
    self._bitsLength = bitsLength

    return value
end

return Buffer


local CONST = table.freeze

return CONST({
    MeshEncoderType = CONST({
        Invalid = -1,
        PointCloud = 0,
        TriangularMesh = 1,
    }),

    MeshEncodingMethod = CONST({
        Sequential = 0,
        EdgeBreaker = 1,
    }),

    MetadataConstants = CONST({
        FlagMask = 0x8000,
    }),

    SequentialAttributeEncodingMethod = CONST({
        Generic = 0,
        Integer = 1,
        Quantization = 2,
        Normals = 3,
    }),

    SequentialIndicesEncodingMethod = CONST({
        Compressed = 0,
        Uncompressed = 1,
    }),

    PredictionEncodingMethod = CONST({
        None = -2,
        Difference = 0,
        Parallelogram = 1,
        ConstrainedMultiParallelogram = 4,
        TexCoordsPortable = 5,
        GeometricNormal = 6,
    }),

    PredictionSchemeTransformMethod = CONST({
        Wrap = 1,
        NormalOctahedronCanonicalized = 3,
    }),

    MeshTraversalMethod = CONST({
        DepthFirst = 0,
        PredictionDegree = 1,
    }),

    MeshAttributeEncodingMethod = CONST({
        VertexAttribute = 0,
        CornerAttribute = 1,
    }),

    EdgeBreakerEncodingMethod = CONST({
        Standard = 0,
        Valence = 2,
    }),

    EdgeBreakerConstants = CONST({
        InvalidCornerIndex = -1,
        LeftFaceEdge = 0,
        RightFaceEdge = 1,
        TexCoordsNumComponents = 2,
        MaxNumParallelograms = 4,
        MaxPriority = 3,
    }),

    EdgeBreakerBitPatternConstants = CONST({
        TopologyC = 0,
        TopologyS = 1,
        TopologyL = 3,
        TopologyR = 5,
        TopologyE = 7,
    }),

    ValenceEdgeBreakerConstants = CONST({
        MinValence = 2,
        MaxValence = 7,
        NumUniqueValences = 6,
    }),

    ANSConstants = CONST({
        RAbsAnsP8Precision = 256,
        RAbsAnsP10Precision = 1024,
        RAbsLBase = 4096,
        IOBase = 256,
        LRansBase = 4096,
        TaggedRansBase = 16384,
        TaggedRansPrecision = 4096,
    }),

    SymbolEncodingMethod = CONST({
        TaggedSymbols = 0,
        RawSymbols = 1,
    }),

    AttributeTypes = CONST({
        Position = 0,
        Normal = 1,
        Color = 2,
        TexCoord = 3,
        Generic = 4,
    }),

    DataTypes = CONST({
        INVALID = 0,
        I8 = 1,
        U8 = 2,
        I16 = 3,
        U16 = 4,
        I32 = 5,
        U32 = 6,
        I64 = 7,
        U64 = 8,
        F32 = 9,
        F64 = 10,
        Bool = 11,
    }),

    DataSizes = CONST({
        [0] = 0, -- INVALID
        [1] = 1, -- I8
        [2] = 1, -- U8
        [3] = 2, -- I16
        [4] = 2, -- U16
        [5] = 4, -- I32
        [6] = 4, -- U32
        [7] = 8, -- I64
        [8] = 8, -- U64
        [9] = 4, -- F32
        [10] = 8, -- F64
        [11] = 1, -- Bool
    })
})


local Draco = script.Parent
local RobloxMesh = Draco.Parent
local Buffer = require(RobloxMesh.Buffer)

local DEBUG = false
type Buffer = Buffer.Class

local debugStack = 0

local function nameof(sourceTable: { [string]: number }, value: number?): string
    if value == nil then
        return "nil"
    end

    for name, val in pairs(sourceTable) do
        if val == value then
            return `{value} ({name})`
        end
    end

    return `{value} (!!INVALID)`
end

local function incDebugStack()
    if DEBUG then
        debugStack += 1
    end
end

local function debugPrint(fmt: string, ...: any)
    if DEBUG then
        local indent = string.rep("..", debugStack)
        print(`[DRACO] {indent}{fmt:format(...)}`)
    end
end

local function decDebugStack()
    if DEBUG then
        debugStack -= 1
    end
end

local function debugBufferState(buf: Buffer)
    if not DEBUG then
        return
    end

    -- Save position before reading
    local pos = buf:GetPosition()
    local size = buf:GetLength()

    local startPos = math.max(0, pos - 16)
    local endPos = math.min(size, pos + 16)

    local length = endPos - startPos
    buf:SetPosition(startPos)

    local data = buf:ReadString(length)
    local at = 0

    data = data:gsub(".", function (char)
        local hex = string.format("%02X", char:byte())
        at += 1

        if at == (pos - startPos) then
            hex = `[{hex}] `
        else
            hex = `{hex} `
        end

        return hex
    end)

    -- Restore original position
    buf:SetPosition(pos)
    incDebugStack()

    debugPrint(`[Buffer State] | Pos: {pos}/{size} | Data {startPos}-{endPos}: {data}`)
    decDebugStack()
end

local function debugError(fmt: string, ...: any)
    error(`[DRACO] {fmt:format(...)}`, 2)
end

local function debugAssert<T>(value: T?, fmt: string, ...: any): T
    return assert(value, `[DRACO] {fmt:format(...)}`)
end

return table.freeze({
    NameOf = nameof,
    Print = debugPrint,
    Error = debugError,
    Assert = debugAssert,
    StackOpen = incDebugStack,
    StackClose = decDebugStack,
    PrintBufferState = debugBufferState,
})


local Draco = script.Parent
local RobloxMesh = Draco.Parent

local Buffer = require(RobloxMesh.Buffer)
local Debug = require(Draco.Debug)

local Rans = {}
Rans.__index = Rans

type ProbEnt = {
    prob: number,
    cumProb: number,
}

export type BitState = {
    BitsValue: number,
    BitsLength: number,
}

export type Class = typeof(setmetatable({} :: {
    ProbabilityTable: { ProbEnt },
    LookupTable: { number },
    RawBuf: buffer?,
    StartPos: number,

    Offset: number,
    Base: number,
    Precision: number,
    State: number,
    ProbZero: number,
}, Rans))

local U8 = Buffer.ReadUInt8
local LEB128 = Buffer.ReadVarInt

local debugPrint = Debug.Print
type Buffer = Buffer.Class

local function newRans(): Class
    return setmetatable({
        ProbabilityTable = {},
        LookupTable = {},

        RawBuf = nil,
        StartPos = 0,
        Offset = 1,

        Base = 0,
        Precision = 0,

        State = 0,
        ProbZero = 0,
    }, Rans)
end

local function decodeTables(self: Class, buf: Buffer, expectedCumProb: number)
    local startPos = buf:GetPosition()
    local numSymbols = LEB128(buf)

    local probabilityTable: { ProbEnt } = {}
    local lookupTable: { number } = {}

    local cumProb = 0
    local actProb = 0
    local i = 0

    while i < numSymbols do
        local data = U8(buf)
        local token = bit32.band(data, 3)

        if token == 3 then
            local offset = bit32.rshift(data, 2)

            for j = 0, offset do
                probabilityTable[i + j + 1] = {prob = 0, cumProb = cumProb }
            end

            i += offset + 1
        else
            local prob = bit32.rshift(data, 2)

            for j = 0, token - 1 do
                local eb = U8(buf)
                prob = bit32.bor(prob, bit32.lshift(eb, 8 * (j + 1) - 2))
            end

            probabilityTable[i + 1] = {
                prob = prob,
                cumProb = cumProb
            }

            cumProb += prob

            for j = actProb, cumProb - 1 do
                lookupTable[j + 1] = i
            end

            actProb = cumProb
            i += 1
        end
    end

    self.ProbabilityTable = probabilityTable
    self.LookupTable = lookupTable

    local endPos = buf:GetPosition()
    debugPrint(`decodeTables consumed {endPos - startPos} bytes for {numSymbols} symbols, and expected cumProb {expectedCumProb}, actual cumProb {cumProb}`)
end

local function start(self: Class, rawBuf: buffer, startPos: number, dataSize: number, base: number, precision: number)
    local offset = dataSize
    local lastByte = buffer.readu8(rawBuf, startPos + offset - 1)
    local x = bit32.rshift(lastByte, 6)

    if x == 0 then
        self.Offset = offset - 1
        self.State = bit32.band(lastByte, 0x3F)
    elseif x == 1 then
        self.Offset = offset - 2
        self.State = bit32.band(
            bit32.bor(
                bit32.lshift(buffer.readu8(rawBuf, startPos + offset - 1), 8),
                buffer.readu8(rawBuf, startPos + offset - 2)
            ),
            0x3FFF
        )
    elseif x == 2 then
        self.Offset = offset - 3
        self.State = bit32.band(
            bit32.bor(
                bit32.lshift(buffer.readu8(rawBuf, startPos + offset - 1), 16),
                bit32.lshift(buffer.readu8(rawBuf, startPos + offset - 2), 8),
                buffer.readu8(rawBuf, startPos + offset - 3)
            ),
            0x3FFFFF
        )
    elseif x == 3 then
        self.Offset = offset - 4
        self.State = bit32.band(
            bit32.bor(
                bit32.lshift(buffer.readu8(rawBuf, startPos + offset - 1), 24),
                bit32.lshift(buffer.readu8(rawBuf, startPos + offset - 2), 16),
                bit32.lshift(buffer.readu8(rawBuf, startPos + offset - 3), 8),
                buffer.readu8(rawBuf, startPos + offset - 4)
            ),
            0x3FFFFFFF
        )
    end

    self.RawBuf = rawBuf
    self.StartPos = startPos

    self.Base = base
    self.State += base
    self.Precision = precision
end

function Rans.ReadSymbol(self: Class): number
    while self.State < self.Base and self.Offset > 0 do
        self.Offset -= 1
        self.State = bit32.bor(
            bit32.lshift(self.State, 8),
            buffer.readu8(self.RawBuf :: buffer, self.StartPos + self.Offset)
        )
    end

    local quo = self.State // self.Precision
    local rem = self.State % self.Precision

    local symbol = self.LookupTable[rem + 1] or 0
    local probEntry = self.ProbabilityTable[symbol + 1]

    local prob = probEntry and probEntry.prob or 1
    local cumProb = probEntry and probEntry.cumProb or 0

    self.State = quo * prob + rem - cumProb
    return symbol
end

function Rans.InitSymbols(self: Class, buf: Buffer, bitLength: number)
    local precisionBits = (3 * bitLength) // 2
    precisionBits = math.clamp(precisionBits, 12, 20)

    local precision = bit32.lshift(1, precisionBits)
    local base = precision * 4

    decodeTables(self, buf, precision)

    local dataSize = LEB128(buf)
    local startPos = buf:GetPosition()
    local rawBuf = buf:GetBuffer()

    start(self, rawBuf, startPos, dataSize, base, precision)
    buf:Skip(dataSize)
end

return table.freeze({
    new = newRans,
})


--!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)

export type Label = | "Edit" | "Server" | "Client"

return table.freeze({
    -- The identity of the current run context.
    Identity = (if isEdit then "Edit" elseif isServer then "Server" else "Client") :: Label,

    --- Is this running on a dedicated Roblox server? (RCC = Roblox Cloud Compute)
    IsRCC = isRCC,

    --- Is the current run an edit session?
    IsEdit = isEdit,

    --- Is the current run a test session in Studio?
    IsTestRun = isStudio and not isEdit,

    --- Is this running from a server context?
    IsServer = isServer,

    --- Is this running from a client context?
    IsClient = isClient,

    --- Is this running in Roblox Studio?
    IsStudio = isStudio,

    --- Is there a dedicated server instance associated with this session (i.e. we are a dedicated server or a client connected to one)?
    IsOnline = isOnline,

    --- Is the current device using a ten-foot interface (e.g. console)?
    IsTenFoot = GuiService:IsTenFootInterface(),

    --- Is the current device a touch-enabled device (e.g. mobile)?
    IsTouch = UserInputService.TouchEnabled,

    --- Is the current device a VR device?
    IsVR = VRService.VREnabled,
})