<roblox version="4">
  <Item class="Folder" referent="0">
    <Properties>
      <string name="Name">RBLXE_Pack</string>
      <BinaryString name="AttributesSerialize">BAAAAAwAAABSQkxYRV9IYXNoZXMCYAcAACNyYmx4ZS1oYXNoZXMgdjEgZm52MWEzMiBsZgpSZXBsaWNhdGVkU3RvcmFnZS9LZXJuZWwvQ29uZmlnCTlhMzY3Nzk0ClJlcGxpY2F0ZWRTdG9yYWdlL0tlcm5lbC9Db250cmFjdHMJYjUwY2JjNmYKUmVwbGljYXRlZFN0b3JhZ2UvS2VybmVsL0xvYWRlcgkyOWU1NWFmZQpSZXBsaWNhdGVkU3RvcmFnZS9LZXJuZWwvTmV0CTgxZjhjMzIyClJlcGxpY2F0ZWRTdG9yYWdlL0tlcm5lbC9Ob3RpZnkJYjUxNDI1YWYKUmVwbGljYXRlZFN0b3JhZ2UvS2VybmVsL1BhY2thZ2VzL1Ryb3ZlCWI1YTdlYWM5ClJlcGxpY2F0ZWRTdG9yYWdlL0tlcm5lbC9TRlgJODZjY2M2NDEKUmVwbGljYXRlZFN0b3JhZ2UvS2VybmVsL1NjaGVtYQkwYTY4M2EzYwpSZXBsaWNhdGVkU3RvcmFnZS9LZXJuZWwvU2lnbmFsCTRkZGRkZDJjClJlcGxpY2F0ZWRTdG9yYWdlL0tlcm5lbC9VSS9NZW51U3lzdGVtCTgzYmZiMGViClJlcGxpY2F0ZWRTdG9yYWdlL0tlcm5lbC9VSS9QYW5lbEFuaW1hdGlvbnMJODg0MTcxNjkKUmVwbGljYXRlZFN0b3JhZ2UvS2VybmVsL1VJL1BhbmVsQW5pbWF0aW9ucy9Cb3VuY2UJNmE1MTJmMmIKUmVwbGljYXRlZFN0b3JhZ2UvS2VybmVsL1VJL1BpZWNlcwljYmY4MzVmMwpSZXBsaWNhdGVkU3RvcmFnZS9LZXJuZWwvVUkvUm9vdFBlcnNpc3RlbmNlCTE4NWMyNmUwClJlcGxpY2F0ZWRTdG9yYWdlL0tlcm5lbC9VSS9TaGVsbAlhOWVlZDUxMgpSZXBsaWNhdGVkU3RvcmFnZS9LZXJuZWwvVmVyc2lvbgljNmY1Yjg0YgpSZXBsaWNhdGVkU3RvcmFnZS9QYWNrcy9EYXRhc3RvcmVQdXJjaGFzZVN5c3RlbS9NaWdyYXRpb25zL0Zyb21WMgkwMzkxYTNlMQpSZXBsaWNhdGVkU3RvcmFnZS9QYWNrcy9EYXRhc3RvcmVQdXJjaGFzZVN5c3RlbS9NaWdyYXRpb25zL0tlZXAJMTFmYTJkZGUKUmVwbGljYXRlZFN0b3JhZ2UvUGFja3MvRGF0YXN0b3JlUHVyY2hhc2VTeXN0ZW0vUGFja2FnZXMvUHJvbWlzZQk2ZDUzZjgzNgpSZXBsaWNhdGVkU3RvcmFnZS9QYWNrcy9EYXRhc3RvcmVQdXJjaGFzZVN5c3RlbS9TY2hlbWEJMjNhMTE2ZmQKUmVwbGljYXRlZFN0b3JhZ2UvUGFja3MvRGF0YXN0b3JlUHVyY2hhc2VTeXN0ZW0vbWFuaWZlc3QJYTA1Y2QzYmYKU2VydmVyU2NyaXB0U2VydmljZS9LZXJuZWwJYWJiZmZmMmIKU2VydmVyU2NyaXB0U2VydmljZS9QYWNrcy9EYXRhc3RvcmVQdXJjaGFzZVN5c3RlbQkzYjNjNDdiNwpTZXJ2ZXJTY3JpcHRTZXJ2aWNlL1BhY2tzL0RhdGFzdG9yZVB1cmNoYXNlU3lzdGVtL0FjdGlvbnMvV2lwZVN0dWRpb0RhdGEJMzE1ZDk2ZWEKU2VydmVyU2NyaXB0U2VydmljZS9QYWNrcy9EYXRhc3RvcmVQdXJjaGFzZVN5c3RlbS9Db25maWcJMzJmZDJlOGYKU2VydmVyU2NyaXB0U2VydmljZS9QYWNrcy9EYXRhc3RvcmVQdXJjaGFzZVN5c3RlbS9EYXRhUGF0aAk5YTBlMjI4MApTZXJ2ZXJTY3JpcHRTZXJ2aWNlL1BhY2tzL0RhdGFzdG9yZVB1cmNoYXNlU3lzdGVtL0RhdGFTdG9yZU1hbmFnZXIJODdkOTIyNWUKU2VydmVyU2NyaXB0U2VydmljZS9QYWNrcy9EYXRhc3RvcmVQdXJjaGFzZVN5c3RlbS9EZWZhdWx0RGF0YQkxZDFkODIzNApTZXJ2ZXJTY3JpcHRTZXJ2aWNlL1BhY2tzL0RhdGFzdG9yZVB1cmNoYXNlU3lzdGVtL01pZ3JhdGlvbglmZWU4NjZiNQpTZXJ2ZXJTY3JpcHRTZXJ2aWNlL1BhY2tzL0RhdGFzdG9yZVB1cmNoYXNlU3lzdGVtL1B1cmNoYXNlTWFuYWdlcgk3MWM0NmQzNgpTZXJ2ZXJTY3JpcHRTZXJ2aWNlL1BhY2tzL0RhdGFzdG9yZVB1cmNoYXNlU3lzdGVtL1N0dWRpb1NhZmVJZAkwZTE4YjM1NApTZXJ2ZXJTY3JpcHRTZXJ2aWNlL1BhY2tzL0RhdGFzdG9yZVB1cmNoYXNlU3lzdGVtL1ZhbHVlUmVwbGljYXRvcglkYjgxNGVhMApTdGFydGVyUGxheWVyU2NyaXB0cy9LZXJuZWxDbGllbnQJYTAwODgyZjEIAAAAUkJMWEVfSWQCFwAAAERhdGFzdG9yZVB1cmNoYXNlU3lzdGVtEwAAAFJCTFhFX0tlcm5lbFZlcnNpb24CBQAAADMuMC4xDQAAAFJCTFhFX1ZlcnNpb24CBQAAADMuMC4x</BinaryString>
      <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
      <Color3 name="IconTint">
        <R>0</R>
        <G>0</G>
        <B>0</B>
      </Color3>
      <bool name="DefinesCapabilities">false</bool>
      <BinaryString name="SerializedOverrides">
      </BinaryString>
      <int64 name="SourceAssetId">-1</int64>
      <BinaryString name="Tags"></BinaryString>
    </Properties>
    <Item class="Folder" referent="1">
      <Properties>
        <string name="Name">ReplicatedStorage</string>
        <BinaryString name="AttributesSerialize"></BinaryString>
        <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
        <Color3 name="IconTint">
          <R>0</R>
          <G>0</G>
          <B>0</B>
        </Color3>
        <bool name="DefinesCapabilities">false</bool>
        <BinaryString name="SerializedOverrides">
        </BinaryString>
        <int64 name="SourceAssetId">-1</int64>
        <BinaryString name="Tags"></BinaryString>
      </Properties>
      <Item class="Folder" referent="2">
        <Properties>
          <string name="Name">Kernel</string>
          <BinaryString name="AttributesSerialize">AQAAAAwAAABSQkxYRV9IYXNoZXMCNwMAACNyYmx4ZS1oYXNoZXMgdjEgZm52MWEzMiBsZgpSZXBsaWNhdGVkU3RvcmFnZS9LZXJuZWwvQ29uZmlnCTlhMzY3Nzk0ClJlcGxpY2F0ZWRTdG9yYWdlL0tlcm5lbC9Db250cmFjdHMJYjUwY2JjNmYKUmVwbGljYXRlZFN0b3JhZ2UvS2VybmVsL0xvYWRlcgkyOWU1NWFmZQpSZXBsaWNhdGVkU3RvcmFnZS9LZXJuZWwvTmV0CTgxZjhjMzIyClJlcGxpY2F0ZWRTdG9yYWdlL0tlcm5lbC9Ob3RpZnkJYjUxNDI1YWYKUmVwbGljYXRlZFN0b3JhZ2UvS2VybmVsL1BhY2thZ2VzL1Ryb3ZlCWI1YTdlYWM5ClJlcGxpY2F0ZWRTdG9yYWdlL0tlcm5lbC9TRlgJODZjY2M2NDEKUmVwbGljYXRlZFN0b3JhZ2UvS2VybmVsL1NjaGVtYQkwYTY4M2EzYwpSZXBsaWNhdGVkU3RvcmFnZS9LZXJuZWwvU2lnbmFsCTRkZGRkZDJjClJlcGxpY2F0ZWRTdG9yYWdlL0tlcm5lbC9VSS9NZW51U3lzdGVtCTgzYmZiMGViClJlcGxpY2F0ZWRTdG9yYWdlL0tlcm5lbC9VSS9QYW5lbEFuaW1hdGlvbnMJODg0MTcxNjkKUmVwbGljYXRlZFN0b3JhZ2UvS2VybmVsL1VJL1BhbmVsQW5pbWF0aW9ucy9Cb3VuY2UJNmE1MTJmMmIKUmVwbGljYXRlZFN0b3JhZ2UvS2VybmVsL1VJL1BpZWNlcwljYmY4MzVmMwpSZXBsaWNhdGVkU3RvcmFnZS9LZXJuZWwvVUkvUm9vdFBlcnNpc3RlbmNlCTE4NWMyNmUwClJlcGxpY2F0ZWRTdG9yYWdlL0tlcm5lbC9VSS9TaGVsbAlhOWVlZDUxMgpSZXBsaWNhdGVkU3RvcmFnZS9LZXJuZWwvVmVyc2lvbgljNmY1Yjg0YgpTZXJ2ZXJTY3JpcHRTZXJ2aWNlL0tlcm5lbAlhYmJmZmYyYgpTdGFydGVyUGxheWVyU2NyaXB0cy9LZXJuZWxDbGllbnQJYTAwODgyZjE=</BinaryString>
          <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
          <Color3 name="IconTint">
            <R>0</R>
            <G>0</G>
            <B>0</B>
          </Color3>
          <bool name="DefinesCapabilities">false</bool>
          <BinaryString name="SerializedOverrides">
          </BinaryString>
          <int64 name="SourceAssetId">-1</int64>
          <BinaryString name="Tags"></BinaryString>
        </Properties>
        <Item class="ModuleScript" referent="3">
          <Properties>
            <string name="Name">Config</string>
            <BinaryString name="AttributesSerialize"></BinaryString>
            <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
            <ContentId name="LinkedSource">
              <null>
              </null>
            </ContentId>
            <bool name="DefinesCapabilities">false</bool>
            <BinaryString name="SerializedOverrides">
            </BinaryString>
            <string name="Source"><![CDATA[-- Config.lua - loads a pack's game-owned configuration (RBLX Essentials 3.0).
--
-- A pack describes its options in `Packs/<PackId>/Schema` (see Schema.lua).
-- The GAME owns the values, in modules no update ever replaces:
--   ReplicatedStorage.RBLXEssentials.Config.<PackId>     options clients may know
--   ServerScriptService.RBLXEssentials.Config.<PackId>   options marked server = true
-- Both return only what the game changes; the rest comes from the schema.
-- The merged value is validated and frozen. A pack whose configuration is
-- invalid does not boot: running with an unexpected price or product id is
-- worse than a clear error that names the option.
--
-- Pure: every Instance and the require function come in through `opts`.
local Config = {}

-- What a caller passed, for error text: an Instance's ClassName, or the Luau
-- type of anything else passed by mistake (a string, a table).
local function classOf(value)
	return if typeof(value) == "Instance" then value.ClassName else typeof(value)
end

local function loadModule(requireFn, module, label, errors)
	if module == nil then
		return nil
	end
	if typeof(module) ~= "Instance" or not module:IsA("ModuleScript") then
		table.insert(errors, ("%s must be a ModuleScript, got %s"):format(label, classOf(module)))
		return nil
	end
	local ok, value = pcall(requireFn, module)
	if not ok then
		table.insert(errors, ("%s errored: %s"):format(label, tostring(value)))
		return nil
	end
	if value ~= nil and type(value) ~= "table" then
		table.insert(errors, ("%s must return a table, got %s"):format(label, type(value)))
		return nil
	end
	return value
end

local function display(value)
	if type(value) == "table" then
		return "a table"
	end
	return type(value) == "string" and ("%q"):format(value) or tostring(value)
end

-- What Config.load calls on schemaLib: anything else is not the Schema module.
local SCHEMA_FUNCTIONS = {
	"check", "serverFields", "merge", "defaults", "stripServer", "validate", "formatErrors", "freeze",
}

local function isSchemaLib(S)
	if type(S) ~= "table" then
		return false
	end
	for _, name in ipairs(SCHEMA_FUNCTIONS) do
		if type(S[name]) ~= "function" then
			return false
		end
	end
	return true
end

function Config.load(opts)
	-- Tools calls this too. The opts a caller can get wrong (side, packId,
	-- schemaLib, requireFn and the three module arguments) come back as
	-- errors, never as a silent guess or a throw: a side typo used to mean
	-- "client" and skip the server module. A game Config that makes the
	-- merge itself throw (a table that contains itself) still throws, so
	-- callers keep a pcall, as the Loader does.
	if opts.side ~= "Server" and opts.side ~= "Client" then
		return nil, { ('side must be "Server" or "Client", got %s'):format(display(opts.side)) }
	end
	if type(opts.packId) ~= "string" or opts.packId == "" then
		return nil, { ("packId must be non-empty text, got %s"):format(display(opts.packId)) }
	end
	local S = opts.schemaLib
	if opts.schemaModule == nil then
		return nil, {}
	end
	if not isSchemaLib(S) then
		return nil, { ("schemaLib must be the Kernel Schema module, got %s"):format(typeof(S)) }
	end
	if type(opts.requireFn) ~= "function" then
		return nil, { "requireFn must be a function, got " .. typeof(opts.requireFn) }
	end
	if typeof(opts.schemaModule) ~= "Instance" or not opts.schemaModule:IsA("ModuleScript") then
		return nil, { ("Schema must be a ModuleScript, got %s"):format(classOf(opts.schemaModule)) }
	end

	local okFactory, factory = pcall(opts.requireFn, opts.schemaModule)
	if not okFactory then
		return nil, { ("Schema errored: %s"):format(tostring(factory)) }
	end
	if type(factory) ~= "function" then
		return nil, { "Schema must return function(S) ... end" }
	end
	local okRoot, root = pcall(factory, S)
	if not okRoot then
		return nil, { ("Schema errored: %s"):format(tostring(root)) }
	end
	local definitionErrors = S.check(root)
	if #definitionErrors > 0 then
		for index, message in ipairs(definitionErrors) do
			definitionErrors[index] = "Schema: " .. message
		end
		return nil, definitionErrors
	end

	local name = "Config." .. opts.packId
	local sharedLabel = "ReplicatedStorage.RBLXEssentials.Config." .. opts.packId
	local serverLabel = "ServerScriptService.RBLXEssentials.Config." .. opts.packId
	local errors = {}
	local shared = loadModule(opts.requireFn, opts.sharedModule, sharedLabel, errors)
	local server = nil
	if opts.side == "Server" then
		server = loadModule(opts.requireFn, opts.serverModule, serverLabel, errors)
	end
	if #errors > 0 then
		return nil, errors
	end

	local serverFields = S.serverFields(root)
	for key in pairs(shared or {}) do
		if serverFields[key] then
			table.insert(errors, ("%s.%s is server-only and must not reach clients: move it to %s"):format(name, tostring(key), serverLabel))
		end
	end
	for key in pairs(server or {}) do
		if root.fields[key] ~= nil and not serverFields[key] then
			table.insert(errors, ("%s.%s is not server-only: move it to %s"):format(name, tostring(key), sharedLabel))
		end
	end
	if #errors > 0 then
		table.sort(errors)
		return nil, errors
	end

	local value = S.merge(root, S.defaults(root), shared)
	if server ~= nil then
		value = S.merge(root, value, server)
	end
	local clientSide = opts.side == "Client"
	if clientSide then
		value = S.stripServer(root, value)
	end
	local problems = S.validate(root, value, name, { skipServer = clientSide })
	if #problems > 0 then
		return nil, S.formatErrors(problems)
	end
	return S.freeze(value), {}
end

return Config
]]></string>
            <int64 name="SourceAssetId">-1</int64>
            <BinaryString name="Tags"></BinaryString>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="4">
          <Properties>
            <string name="Name">Contracts</string>
            <BinaryString name="AttributesSerialize"></BinaryString>
            <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
            <ContentId name="LinkedSource">
              <null>
              </null>
            </ContentId>
            <bool name="DefinesCapabilities">false</bool>
            <BinaryString name="SerializedOverrides">
            </BinaryString>
            <string name="Source"><![CDATA[-- Contracts.lua - interface piece contracts (RBLX Essentials 3.0).
--
-- A pack declares, in `Packs/<PackId>/Pieces`, every piece of interface it
-- drives and what that piece must contain. The declaration is plain data:
--
--   return {
--       CodesPanel = {
--           contract = 1,              -- bumped only by an incompatible change
--           kind = "Panel",            -- Panel | HudButton | HudWidget | Billboard | Layer
--           doc = "Panel for typing and redeeming codes.",
--           requires = { { path = "Container/Button", class = "GuiButton" } },
--           optional = { { path = "Container/ErrorText", class = "TextLabel" } },
--       },
--       CodesButton = { contract = 1, kind = "HudButton", doc = "...", opens = "CodesPanel" },
--   }
--
-- Kernel 3.0.1: `standsInFor = "<PackId>.<Name>"` marks a stand-in, a copy
-- of a piece another pack owns (the General UI Kit's Codes menu stands in
-- for CodesSystem.CodesPanel). validateSpecs checks its shape and that it
-- names another pack; Pieces.plan refuses the stand-in when the owner piece
-- is declared, and checks that both have the same kind.
--
-- A piece root carries RBLXE_Piece = "<PackId>.<Name>" and RBLXE_Contract.
-- The same check runs in the Kernel (which copy to use) and in Tools (before
-- Play), so both always agree. Pure: Instances come in as arguments.
local Contracts = {}

-- Layer (plan B0): a ScreenGui that is itself the piece, such as a layer of
-- notification templates; like a Billboard piece it is its own entry in
-- Interface, never inside another ScreenGui.
Contracts.KINDS = {
	Panel = "GuiObject",
	HudButton = "GuiButton",
	HudWidget = "GuiObject",
	Billboard = "BillboardGui",
	Layer = "ScreenGui",
}
Contracts.PIECE_ATTRIBUTE = "RBLXE_Piece"
Contracts.CONTRACT_ATTRIBUTE = "RBLXE_Contract"

-- What MenuSystem needs to adopt and open a piece, checked on every piece of
-- that kind on top of its declared `requires`: a Panel is adopted through its
-- UI_Panel tag and registered under its PanelId; a HudButton is adopted
-- through its UI_Interactable tag and opens its panel with
-- Action = "TogglePanel" and TargetPanel. Without these a piece could win on
-- the server and still be unusable on every client. Tags are read with
-- Instance:HasTag, which @lune/roblox implements, so the check stays pure.
Contracts.IMPLICIT = {
	Panel = { tag = "UI_Panel", attributes = { PanelId = true } },
	HudButton = { tag = "UI_Interactable", attributes = { Action = "TogglePanel", TargetPanel = true } },
}

local function validateEntries(prefix, list, label, errors)
	if list == nil then
		return
	end
	if type(list) ~= "table" then
		table.insert(errors, ("%s.%s must be a list"):format(prefix, label))
		return
	end
	for index, entry in ipairs(list) do
		if type(entry) ~= "table" or type(entry.path) ~= "string" or entry.path == ""
			or type(entry.class) ~= "string" or entry.class == "" then
			table.insert(errors, ("%s.%s[%d] needs a non-empty path and class"):format(prefix, label, index))
		end
	end
end

function Contracts.validateSpecs(packId, specs)
	if type(specs) ~= "table" then
		return { ("%s.Pieces must return a table"):format(packId) }
	end
	local errors = {}
	for name, spec in pairs(specs) do
		local prefix = ("%s.Pieces.%s"):format(packId, tostring(name))
		if type(name) ~= "string" or not name:match("^[%a][%w_]*$") then
			table.insert(errors, ("%s: piece names must start with a letter and use letters, digits or _"):format(prefix))
		elseif type(spec) ~= "table" then
			table.insert(errors, ("%s must be a table"):format(prefix))
		else
			if type(spec.contract) ~= "number" or spec.contract < 1 or spec.contract % 1 ~= 0 then
				table.insert(errors, ("%s.contract must be a whole number >= 1"):format(prefix))
			end
			if Contracts.KINDS[spec.kind] == nil then
				table.insert(errors, ("%s.kind must be Panel, HudButton, HudWidget, Billboard or Layer"):format(prefix))
			end
			if type(spec.doc) ~= "string" or spec.doc == "" then
				table.insert(errors, ("%s.doc is required"):format(prefix))
			end
			validateEntries(prefix, spec.requires, "requires", errors)
			validateEntries(prefix, spec.optional, "optional", errors)
			if spec.opens ~= nil and (type(spec.opens) ~= "string" or specs[spec.opens] == nil) then
				table.insert(errors, ("%s.opens must name another piece of %s"):format(prefix, packId))
			end
			if spec.standsInFor ~= nil then
				local ownerPack, ownerName = nil, nil
				if type(spec.standsInFor) == "string" then
					ownerPack, ownerName = Contracts.splitId(spec.standsInFor)
				end
				if ownerPack == nil or ownerPack == packId or not ownerName:match("^[%a][%w_]*$") then
					table.insert(errors, ('%s.standsInFor must be "<PackId>.<Name>" naming a piece of another pack'):format(prefix))
				end
			end
		end
	end
	table.sort(errors)
	return errors
end

function Contracts.resolve(root, path)
	local node = root
	for segment in string.gmatch(path, "[^/]+") do
		node = node:FindFirstChild(segment)
		if node == nil then
			return nil
		end
	end
	return node
end

local function display(value)
	if type(value) == "string" then
		return ("%q"):format(value)
	elseif type(value) == "table" then
		return "a table"
	end
	return tostring(value)
end

-- What makes a spec unusable for check. validateSpecs already rejects all of
-- it for a pack's own Pieces module, but Tools also checks pieces against
-- specs nobody validated, and a check answers with problems, never a throw.
local function specProblems(spec)
	local function problem(name, expected, found)
		return { path = "(spec)", problem = "spec", name = name, expected = expected, found = display(found) }
	end
	if type(spec) ~= "table" then
		return { problem("spec", "a table", spec) }
	end
	local problems = {}
	if Contracts.KINDS[spec.kind] == nil then
		table.insert(problems, problem("kind", "Panel, HudButton, HudWidget, Billboard or Layer", spec.kind))
	end
	if type(spec.contract) ~= "number" or spec.contract < 1 or spec.contract % 1 ~= 0 then
		table.insert(problems, problem("contract", "a whole number >= 1", spec.contract))
	end
	for _, label in ipairs({ "requires", "optional" }) do
		local list = spec[label]
		if list ~= nil and type(list) ~= "table" then
			table.insert(problems, problem(label, "a list", list))
		elseif list ~= nil then
			for index, entry in ipairs(list) do
				if type(entry) ~= "table" or type(entry.path) ~= "string" or entry.path == ""
					or type(entry.class) ~= "string" or entry.class == "" then
					table.insert(problems, problem(("%s[%d]"):format(label, index), "a non-empty path and class", entry))
				end
			end
		end
	end
	return problems
end

function Contracts.check(root, spec)
	local problems = specProblems(spec)
	if typeof(root) ~= "Instance" then
		table.insert(problems, { path = "(root)", problem = "root", expected = "an Instance", found = display(root) })
	end
	if #problems > 0 then
		return problems
	end
	local rootClass = Contracts.KINDS[spec.kind]
	if not root:IsA(rootClass) then
		table.insert(problems, { path = "(root)", problem = "wrong class", expected = rootClass, found = root.ClassName })
	end
	local declared = root:GetAttribute(Contracts.CONTRACT_ATTRIBUTE)
	if declared ~= spec.contract then
		table.insert(problems, { path = "(root)", problem = "contract", expected = spec.contract, found = declared })
	end
	local implicit = Contracts.IMPLICIT[spec.kind]
	if implicit ~= nil then
		if not root:HasTag(implicit.tag) then
			table.insert(problems, { path = "(root)", problem = "tag", expected = implicit.tag })
		end
		local names = {}
		for name in pairs(implicit.attributes) do
			table.insert(names, name)
		end
		table.sort(names)
		for _, name in ipairs(names) do
			local want = implicit.attributes[name]
			local value = root:GetAttribute(name)
			if want == true then
				if type(value) ~= "string" or value == "" then
					table.insert(problems, { path = "(root)", problem = "attribute", name = name, expected = "non-empty text", found = value })
				end
			elseif value ~= want then
				table.insert(problems, { path = "(root)", problem = "attribute", name = name, expected = ("%q"):format(want), found = value })
			end
		end
	end
	for _, entry in ipairs(spec.requires or {}) do
		local found = Contracts.resolve(root, entry.path)
		if found == nil then
			table.insert(problems, { path = entry.path, problem = "missing", expected = entry.class })
		elseif not found:IsA(entry.class) then
			table.insert(problems, { path = entry.path, problem = "wrong class", expected = entry.class, found = found.ClassName })
		end
	end
	for _, entry in ipairs(spec.optional or {}) do
		local found = Contracts.resolve(root, entry.path)
		if found ~= nil and not found:IsA(entry.class) then
			table.insert(problems, { path = entry.path, problem = "wrong class", expected = entry.class, found = found.ClassName })
		end
	end
	return problems
end

function Contracts.formatProblems(problems)
	local parts = {}
	for _, p in ipairs(problems) do
		if p.problem == "missing" then
			table.insert(parts, ("missing %s (%s)"):format(p.path, p.expected))
		elseif p.problem == "contract" then
			table.insert(parts, ("contract version %s, expected %s"):format(tostring(p.found), tostring(p.expected)))
		elseif p.problem == "tag" then
			table.insert(parts, ("missing tag %s"):format(tostring(p.expected)))
		elseif p.problem == "attribute" then
			local found = type(p.found) == "string" and ("%q"):format(p.found) or tostring(p.found)
			table.insert(parts, ("attribute %s is %s, expected %s"):format(p.name, found, tostring(p.expected)))
		elseif p.problem == "spec" then
			table.insert(parts, ("invalid contract: %s is %s, expected %s"):format(p.name, p.found, p.expected))
		elseif p.problem == "root" then
			table.insert(parts, ("piece root is %s, expected %s"):format(p.found, p.expected))
		else
			table.insert(parts, ("%s is %s, expected %s"):format(p.path, tostring(p.found), tostring(p.expected)))
		end
	end
	return table.concat(parts, "; ")
end

function Contracts.splitId(pieceId)
	return string.match(pieceId, "^([^.]+)%.([^.]+)$")
end

return Contracts
]]></string>
            <int64 name="SourceAssetId">-1</int64>
            <BinaryString name="Tags"></BinaryString>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="5">
          <Properties>
            <string name="Name">Loader</string>
            <BinaryString name="AttributesSerialize"></BinaryString>
            <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
            <ContentId name="LinkedSource">
              <null>
              </null>
            </ContentId>
            <bool name="DefinesCapabilities">false</bool>
            <BinaryString name="SerializedOverrides">
            </BinaryString>
            <string name="Source"><![CDATA[-- Boots installed packs in dependency order. Shared by Kernel (server) and
-- KernelClient. Pure: all roots come in via opts, so tests can point it at
-- fixture folders.
--
-- Plan B0 (phase B, before the Kernel 3.0.0 API freeze), in every ctx:
--   * companion(packId): another installed pack's frozen configuration for
--     this side and, once it booted, its API (handle:api()), or nil when
--     that pack is not installed. Optional by definition, like the Notify
--     and SFX facades: no manifest declaration, and never an instance path
--     into another pack;
--   * extension(name, defaults) / extensions(defaults): the game's own
--     modules for this pack, in <Service>.RBLXEssentials.Extensions.<PackId>
--     (a game zone no update or uninstall touches), before the pack's own
--     shipped ones in `defaults`;
--   * settings: only for a pack without a Schema (a game's own pack), with
--     a deprecation notice in report.notices; a pack with a Schema reads
--     ctx.config (and a notice says so when it still has a Settings folder).
local Loader = {}

-- RT-5: after its boot the server publishes which packs did not start, in
-- this attribute of ReplicatedStorage.RBLXEssentials (JSON { failed,
-- skipped }), so each client skips them instead of waiting on remotes a
-- server module never creates.
Loader.SERVER_REPORT_ATTRIBUTE = "RBLXE_ServerReport"

-- The report is attribute data, which Roblox does not store past a size
-- limit, while a reason can be long (a stack traceback, every configuration
-- error of a pack). The limit is PackFormat.HASHES_MAX_LENGTH's. A report
-- over it is encoded again with each reason cut to SERVER_REPORT_REASON_LENGTH
-- bytes, then with no reasons: clients need the ids, and the server output
-- keeps every reason whole.
Loader.SERVER_REPORT_MAX_LENGTH = 100000
Loader.SERVER_REPORT_REASON_LENGTH = 500

local DROPPED_REASON = "see the server output"

-- What clients read instead of a reason that may quote server-only values
-- (Loader.boot marks it in report.private): an invalid configuration names
-- the server Config's values (a repeated code, the text a check quoted), and
-- a server module's error can hold anything it read. The server output keeps
-- every reason whole; clients only need to know the pack did not start.
local PRIVATE_REASONS = {
	configuration = "invalid configuration (the server output names each problem; server-only values are never sent to clients)",
	error = "it errored while starting (the server output has the error; server-only values are never sent to clients)",
}

-- cutReason(reason) -> the reason, or its first SERVER_REPORT_REASON_LENGTH
-- bytes (never ending inside a UTF-8 character) and a note that it was cut.
local function cutReason(reason)
	local text = tostring(reason)
	local stop = Loader.SERVER_REPORT_REASON_LENGTH
	if #text <= stop then
		return text
	end
	-- step back over continuation bytes (0x80 to 0xBF) so the cut falls
	-- between two characters
	while stop > 0 and string.byte(text, stop + 1) >= 0x80 and string.byte(text, stop + 1) < 0xC0 do
		stop -= 1
	end
	return string.sub(text, 1, stop) .. " ... (cut; the server output has it whole)"
end

local function mapReasons(map, reasonFor)
	local out = {}
	for id, reason in pairs(map) do
		out[id] = reasonFor(reason)
	end
	return out
end

-- publicFailed(report) -> report.failed as clients may read it: each reason
-- report.private marks is replaced with its PRIVATE_REASONS text.
local function publicFailed(report)
	local private = if type(report.private) == "table" then report.private else {}
	local out = {}
	for id, reason in pairs(report.failed) do
		local kind = private[id]
		out[id] = if kind ~= nil then (PRIVATE_REASONS[kind] or DROPPED_REASON) else reason
	end
	return out
end

-- encodeServerReport(report, jsonEncode) -> JSON { failed, skipped } of at
-- most SERVER_REPORT_MAX_LENGTH bytes, for every client: a reason
-- report.private marks never leaves the server (publicFailed). Errors when
-- the ids alone do not fit: a client must never skip a shortened list.
function Loader.encodeServerReport(report, jsonEncode)
	report = { failed = publicFailed(report), skipped = report.skipped }
	local encoded = jsonEncode({ failed = report.failed, skipped = report.skipped })
	if #encoded <= Loader.SERVER_REPORT_MAX_LENGTH then
		return encoded
	end
	encoded = jsonEncode({ failed = mapReasons(report.failed, cutReason), skipped = mapReasons(report.skipped, cutReason) })
	if #encoded <= Loader.SERVER_REPORT_MAX_LENGTH then
		return encoded
	end
	local function dropped()
		return DROPPED_REASON
	end
	encoded = jsonEncode({ failed = mapReasons(report.failed, dropped), skipped = mapReasons(report.skipped, dropped) })
	if #encoded <= Loader.SERVER_REPORT_MAX_LENGTH then
		return encoded
	end
	error(string.format("the boot report is too long even without its reasons (%d bytes, limit %d)",
		#encoded, Loader.SERVER_REPORT_MAX_LENGTH), 0)
end

-- decodeServerReport(encoded, jsonDecode) -> { failed, skipped } or nil when
-- there is no usable report. HttpService writes an empty map as [], which
-- decodes to an empty table.
function Loader.decodeServerReport(encoded, jsonDecode)
	if type(encoded) ~= "string" then
		return nil
	end
	local ok, decoded = pcall(jsonDecode, encoded)
	if not ok or type(decoded) ~= "table" then
		return nil
	end
	return {
		failed = if type(decoded.failed) == "table" then decoded.failed else {},
		skipped = if type(decoded.skipped) == "table" then decoded.skipped else {},
	}
end

-- xpcall error handler: appends a stack traceback to the error message when
-- `debug.traceback` is available (real Roblox + the Lune test harness both
-- have it), falling back to a plain tostring() if some sandboxed environment
-- doesn't provide `debug`.
local function tracebackHandler(err)
	if debug and debug.traceback then
		return debug.traceback(tostring(err), 2)
	end
	return tostring(err)
end

local function readManifests(sharedPacks, requireImpl)
	local manifests, errors = {}, {}
	for _, packFolder in ipairs(sharedPacks:GetChildren()) do
		local manifestModule = packFolder:FindFirstChild("manifest")
		if not manifestModule or not manifestModule:IsA("ModuleScript") then
			errors[packFolder.Name] = "no manifest ModuleScript"
			continue
		end
		local ok, manifest = pcall(requireImpl, manifestModule)
		if not ok then
			errors[packFolder.Name] = "manifest error: " .. tostring(manifest)
			continue
		end
		if type(manifest) ~= "table" or type(manifest.id) ~= "string" then
			errors[packFolder.Name] = "manifest must return a table with a string id"
			continue
		end
		if manifest.id ~= packFolder.Name then
			errors[packFolder.Name] = string.format("manifest id %q does not match folder name", manifest.id)
			continue
		end
		manifest = table.clone(manifest)
		manifest.deps = manifest.deps or {}
		manifest.folder = packFolder
		manifests[manifest.id] = manifest
	end
	return manifests, errors
end

local function resolveOrder(manifests)
	local skipped = {}

	-- transitively drop packs whose declared deps are not installed
	local changed = true
	while changed do
		changed = false
		for id, m in pairs(manifests) do
			if not skipped[id] then
				for _, dep in ipairs(m.deps) do
					if not manifests[dep] or skipped[dep] then
						-- Whether dep is simply absent, or exists but was itself already
						-- skipped by an earlier fixed-point pass, the reason is the same:
						-- this pack can't run because dep isn't available. Cycle detection
						-- (and the "dependency cycle" reason) happens later, in the
						-- post-Kahn indegree sweep below -- never here.
						skipped[id] = "missing dependency: " .. dep
						changed = true
						break
					end
				end
			end
		end
	end

	-- Kahn topological sort (alphabetical tie-break for determinism)
	local order, indegree, dependents = {}, {}, {}
	for id in pairs(manifests) do
		if not skipped[id] then indegree[id] = 0 end
	end
	for id, m in pairs(manifests) do
		if not skipped[id] then
			for _, dep in ipairs(m.deps) do
				indegree[id] += 1
				dependents[dep] = dependents[dep] or {}
				table.insert(dependents[dep], id)
			end
		end
	end
	local queue = {}
	for id, deg in pairs(indegree) do
		if deg == 0 then table.insert(queue, id) end
	end
	table.sort(queue)
	while #queue > 0 do
		local id = table.remove(queue, 1)
		table.insert(order, id)
		for _, dependent in ipairs(dependents[id] or {}) do
			indegree[dependent] -= 1
			if indegree[dependent] == 0 then
				local pos = #queue + 1
				for i, q in ipairs(queue) do
					if dependent < q then pos = i break end
				end
				table.insert(queue, pos, dependent)
			end
		end
	end
	for id, deg in pairs(indegree) do
		if deg > 0 and not skipped[id] then
			skipped[id] = "dependency cycle"
		end
	end
	return order, skipped
end

-- moduleNamed(parent, name): the first ModuleScript child called `name`,
-- past any other child of that name (FindFirstChild would stop at a Folder).
local function moduleNamed(parent, name)
	for _, child in ipairs(parent and parent:GetChildren() or {}) do
		if child.Name == name and child:IsA("ModuleScript") then
			return child
		end
	end
	return nil
end

-- extension(root, packId, name, defaults?) -> (module?, from?): the game's
-- module <root>.<packId>.<name> ("game"), else defaults.<name> ("pack");
-- only a ModuleScript counts. `root` is the side's Extensions folder.
function Loader.extension(root, packId, name, defaults)
	local found = moduleNamed(root and root:FindFirstChild(packId), name)
	if found ~= nil then
		return found, "game"
	end
	local shipped = moduleNamed(defaults, name)
	if shipped ~= nil then
		return shipped, "pack"
	end
	return nil, nil
end

-- extensions(root, packId, defaults?) -> { { name, module, from } }: every
-- module of both, one per name: the pack's own in their order (the game's
-- module winning a name it shares), then the game's additions in theirs.
function Loader.extensions(root, packId, defaults)
	local list, taken = {}, {}
	local own = root and root:FindFirstChild(packId)
	for _, child in ipairs(defaults and defaults:GetChildren() or {}) do
		if child:IsA("ModuleScript") and not taken[child.Name] then
			taken[child.Name] = true
			local module, from = Loader.extension(root, packId, child.Name, defaults)
			table.insert(list, { name = child.Name, module = module, from = from })
		end
	end
	for _, child in ipairs(own and own:GetChildren() or {}) do
		if child:IsA("ModuleScript") and not taken[child.Name] then
			taken[child.Name] = true
			table.insert(list, { name = child.Name, module = child, from = "game" })
		end
	end
	return list
end

function Loader.boot(opts)
	local requireImpl = opts.requireFn or require
	-- report.private[id]: why report.failed[id] may quote server-only values
	-- ("configuration" or "error"); encodeServerReport never sends such a
	-- reason to clients, and the server output prints it whole.
	-- report.notices: one line per pack that still gets ctx.settings (plan
	-- B0), or whose Settings folder its Schema leaves unread, printed once
	-- by the Kernel scripts.
	local report = { booted = {}, failed = {}, skipped = {}, private = {}, notices = {} }
	local manifests, manifestErrors = readManifests(opts.sharedPacks, requireImpl)
	for name, err in pairs(manifestErrors) do
		report.failed[name] = err
	end
	local order, skipped = resolveOrder(manifests)
	for id, reason in pairs(skipped) do
		report.skipped[id] = reason
	end

	local apis = {}
	-- Each pack's configuration is loaded once, on its own turn below or
	-- earlier through a companion handle, and shared by both.
	local configs = {}
	local function loadConfig(packId)
		local cached = configs[packId]
		if cached ~= nil then
			return cached
		end
		local m = manifests[packId]
		local configRoots = opts.configRoots or {}
		local ok, value, errors = pcall(opts.configLib.load, {
			packId = packId,
			side = opts.side,
			requireFn = requireImpl,
			schemaLib = opts.schemaLib,
			schemaModule = m.folder:FindFirstChild("Schema"),
			sharedModule = configRoots.shared and configRoots.shared:FindFirstChild(packId) or nil,
			serverModule = configRoots.server and configRoots.server:FindFirstChild(packId) or nil,
		})
		cached = { ok = ok, value = value, errors = errors }
		configs[packId] = cached
		return cached
	end
	-- One frozen handle per companion (plan B0, ruling 4).
	local companions = {}
	local function companionOf(packId)
		local handle = companions[packId]
		if handle ~= nil then
			return handle
		end
		-- No config for a pack whose pieces failed on this side or, on a
		-- client, one the server failed or skipped (nothing it declares is in
		-- force there). Otherwise its config, even for a pack this side skips
		-- or that fails later: handle.api() says whether it booted. The
		-- handle is made once, at the first call.
		local stopped = (opts.pieceFailures ~= nil and opts.pieceFailures[packId] ~= nil)
			or (opts.serverReport ~= nil and (opts.serverReport.failed[packId] ~= nil or opts.serverReport.skipped[packId] ~= nil))
		local config = nil
		if opts.configLib and not stopped then
			local loaded = loadConfig(packId)
			if loaded.ok and type(loaded.errors) == "table" and #loaded.errors == 0 then
				config = loaded.value
			end
		end
		handle = table.freeze({
			id = packId,
			version = manifests[packId].version,
			config = config,
			api = function()
				return apis[packId]
			end,
		})
		companions[packId] = handle
		return handle
	end
	for _, id in ipairs(order) do
		local m = manifests[id]
		local blockedBy
		for _, dep in ipairs(m.deps) do
			if report.failed[dep] or (report.skipped[dep] and not apis[dep]) then
				blockedBy = dep
				break
			end
		end
		if blockedBy then
			report.skipped[id] = "dependency failed: " .. blockedBy
			continue
		end

		-- 3.0 (RT-6): a pack whose Pieces declaration is invalid does not
		-- boot: its pieces can never be resolved (Pieces.collectDeclarations).
		local pieceFailure = opts.pieceFailures and opts.pieceFailures[id]
		if pieceFailure ~= nil then
			report.failed[id] = pieceFailure
			continue
		end
		-- 3.0 (RT-5): on a client, a pack that did not start on the server
		-- does not start either: it would wait for remotes nobody creates.
		local serverReason = opts.serverReport and (opts.serverReport.failed[id] or opts.serverReport.skipped[id])
		if serverReason ~= nil then
			report.failed[id] = "did not start on the server: " .. tostring(serverReason)
			continue
		end

		-- 3.0: game-owned configuration (Config.lua). A pack whose configuration
		-- is invalid fails like a pack whose init errors, so its dependents are
		-- skipped instead of booting against a half-configured dependency.
		-- The load runs in its own pcall: a schema or a Config module that
		-- makes the load throw (a self-referencing table, anything Schema.check
		-- did not foresee) fails this pack alone, never the rest of the boot.
		-- A companion handle may have loaded it already (loadConfig).
		local config = nil
		if opts.configLib then
			local loaded = loadConfig(id)
			if not loaded.ok then
				report.failed[id] = "invalid configuration: " .. tostring(loaded.value)
				report.private[id] = "configuration"
				continue
			end
			if #loaded.errors > 0 then
				report.failed[id] = "invalid configuration:\n    " .. table.concat(loaded.errors, "\n    ")
				report.private[id] = "configuration"
				continue
			end
			config = loaded.value
		end

		local module
		if opts.side == "Server" then
			module = opts.serverPacks and opts.serverPacks:FindFirstChild(id)
		else
			module = m.folder:FindFirstChild("Client")
		end
		if not module or not module:IsA("ModuleScript") then
			-- pack ships no code for this side - still counts as booted
			apis[id] = {}
			table.insert(report.booted, id)
			continue
		end

		-- A pack with a Schema reads ctx.config and gets no ctx.settings; a
		-- pack without one (a game's own pack) keeps its 2.x Settings folder,
		-- and the report says once that this is deprecated (plan B0, ruling 5).
		-- A Settings folder next to a Schema (a hand copy: T.update never
		-- takes a 2.x pack to 3.x) is not read, and the report says so.
		local settings = nil
		if m.folder:FindFirstChild("Schema") == nil then
			settings = m.folder:FindFirstChild("Settings")
			if settings ~= nil then
				table.insert(report.notices, string.format(
					"%s reads its 2.x Settings through ctx.settings, which is deprecated: an official pack stops using it when it is updated to 3.x; a game's own pack should move to a Schema and ctx.config", id))
			end
		elseif m.folder:FindFirstChild("Settings") ~= nil then
			table.insert(report.notices, string.format(
				"%s has a Schema, so its Settings folder is not read: the pack runs on Config.%s. Set there any value the game changed in Settings, then delete that folder", id, id))
		end
		local ctx = {
			packId = id,
			version = m.version,
			folder = m.folder,
			serverFolder = (opts.side == "Server") and module or nil,
			settings = settings,
			config = config,
			-- Plan B0: another installed pack's config and API (nil when it is
			-- not installed), and the game's own modules for this pack.
			companion = function(otherId)
				if type(otherId) ~= "string" or otherId == id or manifests[otherId] == nil then
					return nil
				end
				return companionOf(otherId)
			end,
			extension = function(name, defaults)
				return Loader.extension(opts.extensions, id, name, defaults)
			end,
			extensions = function(defaults)
				return Loader.extensions(opts.extensions, id, defaults)
			end,
			kernel = opts.kernelApi,
			ui = opts.ui,
			-- 3.0: this pack's own interface piece, already resolved by the Kernel.
			getPiece = opts.ui and function(name, timeout)
				return opts.ui:getPiece(id .. "." .. name, timeout)
			end or nil,
			require = function(depId)
				local declared = false
				for _, dep in ipairs(m.deps) do
					if dep == depId then declared = true break end
				end
				assert(declared, string.format(
					"[RBLXE] %s tried to require %s without declaring it in manifest.deps", id, depId))
				return apis[depId]
			end,
		}

		local ok, initOrErr = xpcall(requireImpl, tracebackHandler, module)
		if ok and type(initOrErr) ~= "function" then
			ok, initOrErr = false, "module must return function(ctx)"
		end
		if ok then
			local api
			ok, api = xpcall(initOrErr, tracebackHandler, ctx)
			if ok then
				apis[id] = api or {}
				table.insert(report.booted, id)
			else
				report.failed[id] = tostring(api)
				report.private[id] = "error"
			end
		else
			report.failed[id] = tostring(initOrErr)
			report.private[id] = "error"
		end
	end
	return report
end

function Loader.formatReport(side, report)
	local function keys(map)
		local out = {}
		for k in pairs(map) do table.insert(out, k) end
		table.sort(out)
		return table.concat(out, ",")
	end
	return string.format("[RBLXE] %s booted: %s | failed: %s | skipped: %s",
		side, table.concat(report.booted, ","), keys(report.failed), keys(report.skipped))
end

return Loader
]]></string>
            <int64 name="SourceAssetId">-1</int64>
            <BinaryString name="Tags"></BinaryString>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="6">
          <Properties>
            <string name="Name">Net</string>
            <BinaryString name="AttributesSerialize"></BinaryString>
            <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
            <ContentId name="LinkedSource">
              <null>
              </null>
            </ContentId>
            <bool name="DefinesCapabilities">false</bool>
            <BinaryString name="SerializedOverrides">
            </BinaryString>
            <string name="Source"><![CDATA[-- Per-pack remote namespaces under RS/RBLXEssentials/Kernel/Remotes/<PackId>/.
-- Server creates on demand; clients wait for the server to create.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

local Net = {}

local function kernelFolder()
	return ReplicatedStorage:WaitForChild("RBLXEssentials"):WaitForChild("Kernel")
end

local function serverGet(packId, className, name)
	local kernel = kernelFolder()
	local root = kernel:FindFirstChild("Remotes")
	if not root then
		root = Instance.new("Folder"); root.Name = "Remotes"; root.Parent = kernel
	end
	local packFolder = root:FindFirstChild(packId)
	if not packFolder then
		packFolder = Instance.new("Folder"); packFolder.Name = packId; packFolder.Parent = root
	end
	local remote = packFolder:FindFirstChild(name)
	if remote then
		if remote.ClassName ~= className then
			error(string.format("[RBLXE Net] %s/%s already exists as %s", packId, name, remote.ClassName))
		end
		return remote
	end
	remote = Instance.new(className)
	remote.Name = name
	remote.Parent = packFolder
	return remote
end

function Net.server(packId)
	assert(RunService:IsServer(), "[RBLXE Net] Net.server is server-only")
	return {
		event = function(_, name) return serverGet(packId, "RemoteEvent", name) end,
		func = function(_, name) return serverGet(packId, "RemoteFunction", name) end,
	}
end

function Net.client(packId)
	assert(RunService:IsClient(), "[RBLXE Net] Net.client is client-only")

	-- waitFor(parent, name, deadline) -> the child, or nil once `deadline`
	-- (an os.clock time) has passed. Waits in slices of at most 10 s and
	-- warns once when it has to wait longer than one slice, instead of
	-- yielding with no word. Without a deadline it waits as long as it
	-- takes, as 2.x packs expect.
	local function waitFor(parent, name, deadline)
		local child = parent:FindFirstChild(name)
		local warned = false
		while child == nil do
			local slice = 10
			if deadline ~= nil then
				local left = deadline - os.clock()
				if left <= 0 then
					return nil
				end
				slice = math.min(slice, left)
			end
			child = parent:WaitForChild(name, slice)
			if child == nil and not warned and (deadline == nil or os.clock() < deadline) then
				warned = true
				warn(string.format(
					"[RBLXE] Net.client(%q) still waiting for %s.%s. Is the pack's server module installed and started?",
					packId, parent:GetFullName(), name))
			end
		end
		return child
	end

	-- lookup(name, timeout?) -> the remote, or nil with a warning once
	-- `timeout` seconds have passed (RT-5): one deadline for the Remotes
	-- folder, the pack's folder and the remote itself.
	local function lookup(name, timeout)
		local deadline = if type(timeout) == "number" then os.clock() + timeout else nil
		local remotes = waitFor(kernelFolder(), "Remotes", deadline)
		local packFolder = remotes and waitFor(remotes, packId, deadline)
		local remote = packFolder and waitFor(packFolder, name, deadline)
		if remote == nil then
			warn(string.format(
				"[RBLXE] Net.client(%q): remote %q did not appear within %ss; the pack's server module did not start or never creates it",
				packId, name, tostring(timeout)))
		end
		return remote
	end
	return {
		event = function(_, name, timeout) return lookup(name, timeout) end,
		func = function(_, name, timeout) return lookup(name, timeout) end,
	}
end

return Net
]]></string>
            <int64 name="SourceAssetId">-1</int64>
            <BinaryString name="Tags"></BinaryString>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="7">
          <Properties>
            <string name="Name">Notify</string>
            <BinaryString name="AttributesSerialize"></BinaryString>
            <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
            <ContentId name="LinkedSource">
              <null>
              </null>
            </ContentId>
            <bool name="DefinesCapabilities">false</bool>
            <BinaryString name="SerializedOverrides">
            </BinaryString>
            <string name="Source"><![CDATA[-- Notification facade. Packs call Notify:Toast(...) freely; it is a no-op
-- until a backend registers (Notifications System v2 calls setBackend from
-- its server and client inits).
--
-- WHY THIS IS A FACADE AND NOT A REQUIRE: every pack in the ecosystem wants
-- to announce something, and Notifications System is optional. Without this,
-- five packs would each carry the same twenty lines of FindFirstChild +
-- pcall + degrade - and a pack that declared it in manifest.deps would make
-- an optional companion mandatory, since the plugin refuses to install a
-- pack whose declared deps are missing. Same reasoning, same shape, as
-- SFX.lua, which solved this once already for sound.
--
-- Server methods take a `player`; the client backend's do not (a client only
-- ever notifies its own player), which is why a missing method has to be a
-- no-op rather than an error.
local Notify = {}
local backend = nil

-- A missing method behaves exactly like a missing backend: the client
-- backend has no ChatTo/ToastAll/BannerAll, and a pack must not have to know
-- which side it is on. pcall because a notification is fire-and-forget - a
-- bug in the renderer must never surface as an error inside the pack that
-- called it.
local function forward(method, ...)
	if not backend then
		return
	end
	local fn = backend[method]
	if type(fn) ~= "function" then
		return
	end
	local ok, err = pcall(fn, backend, ...)
	if not ok then
		warn("[RBLXE Notify] backend:" .. method .. " failed: " .. tostring(err))
	end
end

function Notify:Toast(...)
	forward("Toast", ...)
end

function Notify:ToastAll(...)
	forward("ToastAll", ...)
end

function Notify:Chat(...)
	forward("Chat", ...)
end

function Notify:ChatTo(...)
	forward("ChatTo", ...)
end

function Notify:Banner(...)
	forward("Banner", ...)
end

function Notify:BannerAll(...)
	forward("BannerAll", ...)
end

function Notify.setBackend(b)
	backend = b
end

return Notify
]]></string>
            <int64 name="SourceAssetId">-1</int64>
            <BinaryString name="Tags"></BinaryString>
          </Properties>
        </Item>
        <Item class="Folder" referent="8">
          <Properties>
            <string name="Name">Packages</string>
            <BinaryString name="AttributesSerialize"></BinaryString>
            <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
            <Color3 name="IconTint">
              <R>0</R>
              <G>0</G>
              <B>0</B>
            </Color3>
            <bool name="DefinesCapabilities">false</bool>
            <BinaryString name="SerializedOverrides">
            </BinaryString>
            <int64 name="SourceAssetId">-1</int64>
            <BinaryString name="Tags"></BinaryString>
          </Properties>
          <Item class="ModuleScript" referent="9">
            <Properties>
              <string name="Name">Trove</string>
              <BinaryString name="AttributesSerialize"></BinaryString>
              <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
              <ContentId name="LinkedSource">
                <null>
                </null>
              </ContentId>
              <bool name="DefinesCapabilities">false</bool>
              <BinaryString name="SerializedOverrides">
              </BinaryString>
              <string name="Source"><![CDATA[--!strict

local RunService = game:GetService("RunService")

export type Trove = {
	Extend: (self: Trove) -> Trove,
	Clone: <T>(self: Trove, instance: T & Instance) -> T,
	Construct: <T, A...>(self: Trove, class: Constructable<T, A...>, A...) -> T,
	Connect: (self: Trove, signal: SignalLike | RBXScriptSignal, fn: (...any) -> ...any) -> ConnectionLike,
	BindToRenderStep: (self: Trove, name: string, priority: number, fn: (dt: number) -> ()) -> (),
	AddPromise: <T>(self: Trove, promise: T & PromiseLike) -> T,
	Add: <T>(self: Trove, object: T & Trackable, cleanupMethod: string?) -> T,
	Remove: <T>(self: Trove, object: T & Trackable) -> boolean,
	Clean: (self: Trove) -> (),
	WrapClean: (self: Trove) -> () -> (),
	AttachToInstance: (self: Trove, instance: Instance) -> RBXScriptConnection,
	Destroy: (self: Trove) -> (),
}

type TroveInternal = Trove & {
	_objects: { any },
	_cleaning: boolean,
	_findAndRemoveFromObjects: (self: TroveInternal, object: any, cleanup: boolean) -> boolean,
	_cleanupObject: (self: TroveInternal, object: any, cleanupMethod: string?) -> (),
}

--[=[
	@within Trove
	@type Trackable Instance | RBXScriptConnection | ConnectionLike | PromiseLike | thread | ((...any) -> ...any) | Destroyable | DestroyableLowercase | Disconnectable | DisconnectableLowercase
	Represents all trackable objects by Trove.
]=]
export type Trackable =
	Instance
	| RBXScriptConnection
	| ConnectionLike
	| PromiseLike
	| thread
	| ((...any) -> ...any)
	| Destroyable
	| DestroyableLowercase
	| Disconnectable
	| DisconnectableLowercase

--[=[
	@within Trove
	@interface ConnectionLike
	.Connected boolean
	.Disconnect (self) -> ()
]=]
type ConnectionLike = {
	Connected: boolean,
	Disconnect: (self: ConnectionLike) -> (),
}

--[=[
	@within Trove
	@interface SignalLike
	.Connect (self, callback: (...any) -> ...any) -> ConnectionLike
	.Once (self, callback: (...any) -> ...any) -> ConnectionLike
]=]
type SignalLike = {
	Connect: (self: SignalLike, callback: (...any) -> ...any) -> ConnectionLike,
	Once: (self: SignalLike, callback: (...any) -> ...any) -> ConnectionLike,
}

--[=[
	@within Trove
	@interface PromiseLike
	.getStatus (self) -> string
	.finally (self, callback: (...any) -> ...any) -> PromiseLike
	.cancel (self) -> ()
]=]
type PromiseLike = {
	getStatus: (self: PromiseLike) -> string,
	finally: (self: PromiseLike, callback: (...any) -> ...any) -> PromiseLike,
	cancel: (self: PromiseLike) -> (),
}

--[=[
	@within Trove
	@type Constructable { new: (A...) -> T } | (A...) -> T
]=]
type Constructable<T, A...> = { new: (A...) -> T } | (A...) -> T

--[=[
	@within Trove
	@interface Destroyable
	.disconnect (self) -> ()
]=]
type Destroyable = {
	Destroy: (self: Destroyable) -> (),
}

--[=[
	@within Trove
	@interface DestroyableLowercase
	.disconnect (self) -> ()
]=]
type DestroyableLowercase = {
	destroy: (self: DestroyableLowercase) -> (),
}

--[=[
	@within Trove
	@interface Disconnectable
	.disconnect (self) -> ()
]=]
type Disconnectable = {
	Disconnect: (self: Disconnectable) -> (),
}

--[=[
	@within Trove
	@interface DisconnectableLowercase
	.disconnect (self) -> ()
]=]
type DisconnectableLowercase = {
	disconnect: (self: DisconnectableLowercase) -> (),
}

local FN_MARKER = newproxy()
local THREAD_MARKER = newproxy()
local GENERIC_OBJECT_CLEANUP_METHODS = table.freeze({ "Destroy", "Disconnect", "destroy", "disconnect" })

local function GetObjectCleanupFunction(object: any, cleanupMethod: string?)
	local t = typeof(object)

	if t == "function" then
		return FN_MARKER
	elseif t == "thread" then
		return THREAD_MARKER
	end

	if cleanupMethod then
		return cleanupMethod
	end

	if t == "Instance" then
		return "Destroy"
	elseif t == "RBXScriptConnection" then
		return "Disconnect"
	elseif t == "table" then
		for _, genericCleanupMethod in GENERIC_OBJECT_CLEANUP_METHODS do
			if typeof(object[genericCleanupMethod]) == "function" then
				return genericCleanupMethod
			end
		end
	end

	error(`failed to get cleanup function for object {t}: {object}`, 3)
end

local function AssertPromiseLike(object: any)
	if
		typeof(object) ~= "table"
		or typeof(object.getStatus) ~= "function"
		or typeof(object.finally) ~= "function"
		or typeof(object.cancel) ~= "function"
	then
		error("did not receive a promise as an argument", 3)
	end
end

--[=[
	@class Trove
	A Trove is helpful for tracking any sort of object during
	runtime that needs to get cleaned up at some point.
]=]
local Trove = {}
Trove.__index = Trove

--[=[
	@return Trove
	Constructs a Trove object.

	```lua
	local trove = Trove.new()
	```
]=]
function Trove.new(): Trove
	local self = setmetatable({}, Trove)

	self._objects = {}
	self._cleaning = false

	return (self :: any) :: Trove
end

--[=[
	@method Add
	@within Trove
	@param object any -- Object to track
	@param cleanupMethod string? -- Optional cleanup name override
	@return object: any
	Adds an object to the trove. Once the trove is cleaned or
	destroyed, the object will also be cleaned up.

	The following types are accepted (e.g. `typeof(object)`):

	| Type | Cleanup |
	| ---- | ------- |
	| `Instance` | `object:Destroy()` |
	| `RBXScriptConnection` | `object:Disconnect()` |
	| `function` | `object()` |
	| `thread` | `task.cancel(object)` |
	| `table` | `object:Destroy()` _or_ `object:Disconnect()` _or_ `object:destroy()` _or_ `object:disconnect()` |
	| `table` with `cleanupMethod` | `object:<cleanupMethod>()` |

	Returns the object added.

	```lua
	-- Add a part to the trove, then destroy the trove,
	-- which will also destroy the part:
	local part = Instance.new("Part")
	trove:Add(part)
	trove:Destroy()

	-- Add a function to the trove:
	trove:Add(function()
		print("Cleanup!")
	end)
	trove:Destroy()

	-- Standard cleanup from table:
	local tbl = {}
	function tbl:Destroy()
		print("Cleanup")
	end
	trove:Add(tbl)

	-- Custom cleanup from table:
	local tbl = {}
	function tbl:DoSomething()
		print("Do something on cleanup")
	end
	trove:Add(tbl, "DoSomething")
	```
]=]
function Trove.Add(self: TroveInternal, object: Trackable, cleanupMethod: string?): any
	if self._cleaning then
		error("cannot call trove:Add() while cleaning", 2)
	end

	local cleanup = GetObjectCleanupFunction(object, cleanupMethod)
	table.insert(self._objects, { object, cleanup })

	return object
end

--[=[
	@method Clone
	@within Trove
	@return Instance
	Clones the given instance and adds it to the trove. Shorthand for
	`trove:Add(instance:Clone())`.

	```lua
	local clonedPart = trove:Clone(somePart)
	```
]=]
function Trove.Clone(self: TroveInternal, instance: Instance): Instance
	if self._cleaning then
		error("cannot call trove:Clone() while cleaning", 2)
	end

	return self:Add(instance:Clone())
end

--[=[
	@method Construct
	@within Trove
	@param class { new(Args...) -> T } | (Args...) -> T
	@param ... Args...
	@return T
	Constructs a new object from either the
	table or function given.

	If a table is given, the table's `new`
	function will be called with the given
	arguments.

	If a function is given, the function will
	be called with the given arguments.

	The result from either of the two options
	will be added to the trove.

	This is shorthand for `trove:Add(SomeClass.new(...))`
	and `trove:Add(SomeFunction(...))`.

	```lua
	local Signal = require(somewhere.Signal)

	-- All of these are identical:
	local s = trove:Construct(Signal)
	local s = trove:Construct(Signal.new)
	local s = trove:Construct(function() return Signal.new() end)
	local s = trove:Add(Signal.new())

	-- Even Roblox instances can be created:
	local part = trove:Construct(Instance, "Part")
	```
]=]
function Trove.Construct<T, A...>(self: TroveInternal, class: Constructable<T, A...>, ...: A...)
	if self._cleaning then
		error("Cannot call trove:Construct() while cleaning", 2)
	end

	local object = nil
	local t = type(class)
	if t == "table" then
		object = (class :: any).new(...)
	elseif t == "function" then
		object = (class :: any)(...)
	end

	return self:Add(object)
end

--[=[
	@method Connect
	@within Trove
	@param signal RBXScriptSignal
	@param fn (...: any) -> ()
	@return RBXScriptConnection
	Connects the function to the signal, adds the connection
	to the trove, and then returns the connection.

	This is shorthand for `trove:Add(signal:Connect(fn))`.

	```lua
	trove:Connect(workspace.ChildAdded, function(instance)
		print(instance.Name .. " added to workspace")
	end)
	```
]=]
function Trove.Connect(self: TroveInternal, signal: SignalLike, fn: (...any) -> ...any)
	if self._cleaning then
		error("Cannot call trove:Connect() while cleaning", 2)
	end

	return self:Add(signal:Connect(fn))
end

--[=[
	@method BindToRenderStep
	@within Trove
	@param name string
	@param priority number
	@param fn (dt: number) -> ()
	Calls `RunService:BindToRenderStep` and registers a function in the
	trove that will call `RunService:UnbindFromRenderStep` on cleanup.

	```lua
	trove:BindToRenderStep("Test", Enum.RenderPriority.Last.Value, function(dt)
		-- Do something
	end)
	```
]=]
function Trove.BindToRenderStep(self: TroveInternal, name: string, priority: number, fn: (dt: number) -> ())
	if self._cleaning then
		error("cannot call trove:BindToRenderStep() while cleaning", 2)
	end

	RunService:BindToRenderStep(name, priority, fn)

	self:Add(function()
		RunService:UnbindFromRenderStep(name)
	end)
end

--[=[
	@method AddPromise
	@within Trove
	@param promise Promise
	@return Promise
	Gives the promise to the trove, which will cancel the promise if the trove is cleaned up or if the promise
	is removed. The exact promise is returned, thus allowing chaining.

	```lua
	trove:AddPromise(doSomethingThatReturnsAPromise())
		:andThen(function()
			print("Done")
		end)
	-- Will cancel the above promise (assuming it didn't resolve immediately)
	trove:Clean()

	local p = trove:AddPromise(doSomethingThatReturnsAPromise())
	-- Will also cancel the promise
	trove:Remove(p)
	```

	:::caution Promise v4 Only
	This is only compatible with the [roblox-lua-promise](https://eryn.io/roblox-lua-promise/) library, version 4.
	:::
]=]
function Trove.AddPromise(self: TroveInternal, promise: PromiseLike)
	if self._cleaning then
		error("cannot call trove:AddPromise() while cleaning", 2)
	end
	AssertPromiseLike(promise)

	if promise:getStatus() == "Started" then
		promise:finally(function()
			if self._cleaning then
				return
			end
			self:_findAndRemoveFromObjects(promise, false)
		end)

		self:Add(promise, "cancel")
	end

	return promise
end

--[=[
	@method Remove
	@within Trove
	@param object any
	Removes the object from the Trove and cleans it up.

	```lua
	local part = Instance.new("Part")
	trove:Add(part)
	trove:Remove(part)
	```
]=]
function Trove.Remove(self: TroveInternal, object: Trackable): boolean
	if self._cleaning then
		error("cannot call trove:Remove() while cleaning", 2)
	end

	return self:_findAndRemoveFromObjects(object, true)
end

--[=[
	@method Extend
	@within Trove
	@return Trove
	Creates and adds another trove to itself. This is just shorthand
	for `trove:Construct(Trove)`. This is useful for contexts where
	the trove object is present, but the class itself isn't.

	:::note
	This does _not_ clone the trove. In other words, the objects in the
	trove are not given to the new constructed trove. This is simply to
	construct a new Trove and add it as an object to track.
	:::

	```lua
	local trove = Trove.new()
	local subTrove = trove:Extend()

	trove:Clean() -- Cleans up the subTrove too
	```
]=]
function Trove.Extend(self: TroveInternal)
	if self._cleaning then
		error("cannot call trove:Extend() while cleaning", 2)
	end

	return self:Construct(Trove)
end

--[=[
	@method Clean
	@within Trove
	Cleans up all objects in the trove. This is
	similar to calling `Remove` on each object
	within the trove. The ordering of the objects
	removed is _not_ guaranteed.

	```lua
	trove:Clean()
	```
]=]
function Trove.Clean(self: TroveInternal)
	if self._cleaning then
		return
	end

	self._cleaning = true

	for _, obj in self._objects do
		self:_cleanupObject(obj[1], obj[2])
	end

	table.clear(self._objects)
	self._cleaning = false
end

--[=[
	@method WrapClean
	@within Trove
	Returns a function that wraps the trove's `Clean()`
	method. Calling the returned function will clean up
	the trove.

	This is often useful in contexts where functions
	are the primary mode for cleaning up an environment,
	such as in many "observer" patterns.

	```lua
	local cleanup = trove:WrapClean()

	-- Sometime later...
	cleanup()
	```

	```lua
	-- Common observer pattern example:
	someObserver(function()
		local trove = Trove.new()
		-- Foo
		return trove:WrapClean()
	end)
	```
]=]
function Trove.WrapClean(self: TroveInternal)
	return function()
		self:Clean()
	end
end

function Trove._findAndRemoveFromObjects(self: TroveInternal, object: any, cleanup: boolean): boolean
	local objects = self._objects

	for i, obj in objects do
		if obj[1] == object then
			local n = #objects
			objects[i] = objects[n]
			objects[n] = nil

			if cleanup then
				self:_cleanupObject(obj[1], obj[2])
			end

			return true
		end
	end

	return false
end

function Trove._cleanupObject(_self: TroveInternal, object: any, cleanupMethod: string?)
	if cleanupMethod == FN_MARKER then
		task.spawn(object)
	elseif cleanupMethod == THREAD_MARKER then
		pcall(task.cancel, object)
	else
		object[cleanupMethod](object)
	end
end

--[=[
	@method AttachToInstance
	@within Trove
	@param instance Instance
	@return RBXScriptConnection
	Attaches the trove to a Roblox instance. Once this
	instance is removed from the game (parent or ancestor's
	parent set to `nil`), the trove will automatically
	clean up.

	This inverses the ownership of the Trove object, and should
	only be used when necessary. In other words, the attached
	instance dictates when the trove is cleaned up, rather than
	the trove dictating the cleanup of the instance.

	:::caution
	Will throw an error if `instance` is not a descendant
	of the game hierarchy.
	:::

	```lua
	trove:AttachToInstance(somePart)
	trove:Add(function()
		print("Cleaned")
	end)

	-- Destroying the part will cause the trove to clean up, thus "Cleaned" printed:
	somePart:Destroy()
	```
]=]
function Trove.AttachToInstance(self: TroveInternal, instance: Instance)
	if self._cleaning then
		error("cannot call trove:AttachToInstance() while cleaning", 2)
	elseif not instance:IsDescendantOf(game) then
		error("instance is not a descendant of the game hierarchy", 2)
	end

	return self:Connect(instance.Destroying, function()
		self:Destroy()
	end)
end

--[=[
	@method Destroy
	@within Trove
	Alias for `trove:Clean()`.

	```lua
	trove:Destroy()
	```
]=]
function Trove.Destroy(self: TroveInternal)
	self:Clean()
end

return {
	new = Trove.new,
}
]]></string>
              <int64 name="SourceAssetId">-1</int64>
              <BinaryString name="Tags"></BinaryString>
            </Properties>
          </Item>
        </Item>
        <Item class="ModuleScript" referent="10">
          <Properties>
            <string name="Name">SFX</string>
            <BinaryString name="AttributesSerialize"></BinaryString>
            <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
            <ContentId name="LinkedSource">
              <null>
              </null>
            </ContentId>
            <bool name="DefinesCapabilities">false</bool>
            <BinaryString name="SerializedOverrides">
            </BinaryString>
            <string name="Source"><![CDATA[-- Sound facade. Packs call SFX:Play(name) freely; it is a no-op until a sound
-- backend registers (Sound System v2 calls setBackend from its client init).
local SFX = {}
local backend = nil

function SFX:Play(name)
	if backend then
		backend:Play(name)
	end
end

function SFX:PlayOverlap(name)
	if backend then
		backend:PlayOverlap(name)
	end
end

function SFX.setBackend(b)
	backend = b
end

return SFX
]]></string>
            <int64 name="SourceAssetId">-1</int64>
            <BinaryString name="Tags"></BinaryString>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="11">
          <Properties>
            <string name="Name">Schema</string>
            <BinaryString name="AttributesSerialize"></BinaryString>
            <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
            <ContentId name="LinkedSource">
              <null>
              </null>
            </ContentId>
            <bool name="DefinesCapabilities">false</bool>
            <BinaryString name="SerializedOverrides">
            </BinaryString>
            <string name="Source"><![CDATA[-- Schema.lua - declarative configuration schemas (RBLX Essentials 3.0).
--
-- A pack describes the options the GAME may set in `Packs/<PackId>/Schema`,
-- a ModuleScript returning `function(S) return S.config({ ... }) end`, where
-- `S` is this module. One schema serves three readers:
--   * the Kernel at boot: defaults plus validation of the game's Config modules;
--   * Tools in Studio: the same validation, before anyone presses Play;
--   * the reference generator: every field carries a `doc`.
--
-- Values are plain Luau data. The only merge rule: `config`, `record` and
-- `map` merge key by key; a `list` and every scalar are replaced whole (list
-- items still receive their own record defaults).
--
-- Plan A3:
--   * a `server = true` option, and everything under it, has no `default`:
--     this module sits in ReplicatedStorage, so a default would reach every
--     client (RT-10). Such an option is required or S.optional, and the game
--     sets it in the server Config module; a server list starts empty;
--   * any node may carry `check = function(value) -> { { field?, message } }`
--     with a `checkDoc` that states the rule: a constraint over several
--     fields ("Target is required when Type is Stat"), run by validate once
--     the value passed every type check below it.
--
-- Plan B0 (phase B, before the Kernel 3.0.0 API freeze):
--   * S.color: a Color3 (typeof) with finite channels, written in a Config
--     module as Color3.fromRGB(r, g, b);
--   * S.tree: a nested table of text keys whose values are finite numbers,
--     text, true/false or tables of the same kind (at most TREE_DEPTH
--     levels, never a table inside itself), such as a starting profile. It
--     is a value, not a group: a game's tree replaces the default whole;
--   * pinned = true on an option outside lists and maps (never the root):
--     T.migrate refuses a game module whose value there differs from the
--     migrated one, and force never lifts it (a DataStore version, where
--     another value starts every player on an empty profile). The Kernel
--     only checks the flag and lists it in describe.
--
-- Pure: no `game`, no Instances, no yields.
local Schema = {}

-- The kinds whose value has no default of its own when the schema gives
-- none (describe's `required`): every kind but the groups and collections.
local SCALAR = {
	number = true, string = true, boolean = true, enum = true,
	id = true, dataPath = true, utcDate = true, color = true, tree = true,
}
local KNOWN = {
	number = true, string = true, boolean = true, enum = true, id = true,
	dataPath = true, utcDate = true, color = true, tree = true, list = true,
	map = true, record = true, config = true, optional = true,
}
-- How deep an S.tree may nest (plan B0).
Schema.TREE_DEPTH = 32
local ID_NAMES = { asset = "an asset id", product = "a developer product id", gamePass = "a game pass id" }
local DAYS_IN_MONTH = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }

-- ── constructors ─────────────────────────────────────────────────────

local function build(kind, opts, extra)
	local node = { kind = kind }
	if opts ~= nil then
		assert(type(opts) == "table", "schema options must be a table")
		for key, value in pairs(opts) do
			node[key] = value
		end
	end
	if extra ~= nil then
		for key, value in pairs(extra) do
			node[key] = value
		end
	end
	return node
end

function Schema.number(opts) return build("number", opts) end
function Schema.integer(opts) return build("number", opts, { integer = true }) end
function Schema.string(opts) return build("string", opts) end
function Schema.boolean(opts) return build("boolean", opts) end
function Schema.enum(values, opts) return build("enum", opts, { values = values }) end
function Schema.assetId(opts) return build("id", opts, { idKind = "asset" }) end
function Schema.productId(opts) return build("id", opts, { idKind = "product" }) end
function Schema.gamePassId(opts) return build("id", opts, { idKind = "gamePass" }) end
function Schema.dataPath(opts) return build("dataPath", opts) end
function Schema.utcDate(opts) return build("utcDate", opts) end
function Schema.color(opts) return build("color", opts) end
function Schema.tree(opts) return build("tree", opts) end
function Schema.list(item, opts) return build("list", opts, { item = item }) end
function Schema.map(value, opts) return build("map", opts, { value = value }) end
function Schema.record(fields, opts) return build("record", opts, { fields = fields }) end
function Schema.config(fields, opts) return build("config", opts, { fields = fields }) end
function Schema.optional(inner) return { kind = "optional", inner = inner } end

-- ── helpers ──────────────────────────────────────────────────────────

local function unwrap(node)
	if type(node) == "table" and node.kind == "optional" then
		return node.inner
	end
	return node
end

local function copy(value)
	if type(value) ~= "table" then
		return value
	end
	local out = {}
	for key, item in pairs(value) do
		out[key] = copy(item)
	end
	return out
end
Schema.copy = copy

local function sortedKeys(t)
	local keys = {}
	for key in pairs(t) do
		table.insert(keys, key)
	end
	table.sort(keys, function(a, b)
		return tostring(a) < tostring(b)
	end)
	return keys
end

local function childPath(path, key)
	if type(key) == "number" then
		return ("%s[%d]"):format(path, key)
	end
	if type(key) == "string" and key:match("^[%a_][%w_]*$") then
		return path == "" and key or (path .. "." .. key)
	end
	return ("%s[%q]"):format(path, tostring(key))
end
Schema.childPath = childPath

local function isArray(t)
	local count = 0
	for key in pairs(t) do
		if type(key) ~= "number" or key < 1 or key % 1 ~= 0 then
			return false
		end
		count += 1
	end
	for i = 1, count do
		if t[i] == nil then
			return false
		end
	end
	return true
end

local function isDataPath(text)
	if type(text) ~= "string" or text == "" then
		return false
	end
	for segment in (text .. "."):gmatch("(.-)%.") do
		if not segment:match("^[%a_][%w_]*$") then
			return false
		end
	end
	return true
end

function Schema.parseUtcDate(text)
	if type(text) ~= "string" then
		return nil
	end
	local y, m, d = text:match("^(%d%d%d%d)%-(%d%d)%-(%d%d)$")
	if y == nil then
		return nil
	end
	local year, month, day = tonumber(y), tonumber(m), tonumber(d)
	if month < 1 or month > 12 then
		return nil
	end
	local days = DAYS_IN_MONTH[month]
	if month == 2 and ((year % 4 == 0 and year % 100 ~= 0) or year % 400 == 0) then
		days = 29
	end
	if day < 1 or day > days then
		return nil
	end
	return year, month, day
end

local function distance(a, b)
	local la, lb = #a, #b
	if la == 0 then return lb end
	if lb == 0 then return la end
	local previous = {}
	for j = 0, lb do
		previous[j] = j
	end
	for i = 1, la do
		local current = { [0] = i }
		local ca = a:sub(i, i):lower()
		for j = 1, lb do
			local cost = (ca == b:sub(j, j):lower()) and 0 or 1
			current[j] = math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + cost)
		end
		previous = current
	end
	return previous[lb]
end

local function suggest(key, fields)
	local best, bestDistance = nil, nil
	for _, name in ipairs(sortedKeys(fields)) do
		local d = distance(key, name)
		if d <= 2 and (bestDistance == nil or d < bestDistance) then
			best, bestDistance = name, d
		end
	end
	return best
end

-- ── validation ───────────────────────────────────────────────────────

local function push(problems, path, message)
	table.insert(problems, { path = path, message = message })
end

-- validateTree(value, path, problems, seen, depth): S.tree's rule. Every
-- bad entry is reported at its own path; a key that is not text is shown
-- with tostring (childPath's %d would throw on 1.5).
local function validateTree(value, path, problems, seen, depth)
	if type(value) ~= "table" then
		return push(problems, path, "must be a table of text keys")
	end
	if seen[value] then
		return push(problems, path, "contains itself")
	end
	if depth > Schema.TREE_DEPTH then
		return push(problems, path, ("is nested deeper than %d levels"):format(Schema.TREE_DEPTH))
	end
	seen[value] = true
	for _, key in ipairs(sortedKeys(value)) do
		local item = value[key]
		local at = if type(key) == "string" then childPath(path, key) else ("%s[%s]"):format(path, tostring(key))
		if type(key) ~= "string" then
			push(problems, at, "keys must be text")
		elseif type(item) == "number" then
			if item ~= item or item == math.huge or item == -math.huge then
				push(problems, at, "must be a finite number")
			end
		elseif type(item) == "table" then
			validateTree(item, at, problems, seen, depth + 1)
		elseif type(item) ~= "string" and type(item) ~= "boolean" then
			push(problems, at, "must be a number, text, true/false or a table")
		end
	end
	seen[value] = nil
end

local validateNode

-- runCheck(node, value, path, problems): the node's own `check`. Each item it
-- returns is { field?, message }: `field` is a key, or a list of keys, under
-- the node's path. Inside a pcall, so a rule that throws is a problem.
local function runCheck(node, value, path, problems)
	local ok, found = pcall(node.check, value)
	if not ok then
		return push(problems, path, ("could not be checked: %s"):format(tostring(found)))
	end
	if type(found) ~= "table" then
		return
	end
	for _, item in ipairs(found) do
		if type(item) == "table" and type(item.message) == "string" then
			local at = path
			local field = item.field
			if type(field) == "table" then
				for _, key in ipairs(field) do
					at = childPath(at, key)
				end
			elseif field ~= nil then
				at = childPath(at, field)
			end
			push(problems, at, item.message)
		end
	end
end

local validateShape
validateNode = function(node, value, path, problems, opts)
	if node.kind == "optional" then
		if value == nil then
			return
		end
		return validateNode(node.inner, value, path, problems, opts)
	end
	-- A node's own rule runs only over a value that passed every type check
	-- below it: a rule over fields that are already wrong is noise.
	local before = #problems
	validateShape(node, value, path, problems, opts)
	if type(node.check) == "function" and #problems == before then
		runCheck(node, value, path, problems)
	end
end

validateShape = function(node, value, path, problems, opts)
	if value == nil then
		return push(problems, path, "is required")
	end
	local kind = node.kind
	if kind == "number" then
		if type(value) ~= "number" or value ~= value then
			return push(problems, path, "must be a number")
		end
		if node.integer and value % 1 ~= 0 then
			return push(problems, path, "must be a whole number")
		end
		if node.min ~= nil and value < node.min then
			return push(problems, path, ("must be >= %s"):format(tostring(node.min)))
		end
		if node.max ~= nil and value > node.max then
			return push(problems, path, ("must be <= %s"):format(tostring(node.max)))
		end
	elseif kind == "string" then
		if type(value) ~= "string" then
			return push(problems, path, "must be text")
		end
		if node.minLength ~= nil and #value < node.minLength then
			if node.minLength == 1 then
				return push(problems, path, "must not be empty")
			end
			return push(problems, path, ("must have at least %d characters"):format(node.minLength))
		end
		if node.maxLength ~= nil and #value > node.maxLength then
			return push(problems, path, ("must have at most %d characters"):format(node.maxLength))
		end
		if node.pattern ~= nil then
			-- pcall: validate can run on a schema Schema.check never saw, and a
			-- malformed pattern must be reported, never thrown.
			local ok, matched = pcall(string.match, value, node.pattern)
			if not ok then
				return push(problems, path, ("cannot be checked against pattern %s: %s"):format(tostring(node.pattern), tostring(matched)))
			end
			if matched == nil then
				return push(problems, path, ("must match %s"):format(node.patternDoc or node.pattern))
			end
		end
	elseif kind == "boolean" then
		if type(value) ~= "boolean" then
			return push(problems, path, "must be true or false")
		end
	elseif kind == "enum" then
		if type(node.values) ~= "table" or type(value) ~= "string" or table.find(node.values, value) == nil then
			return push(problems, path, ("must be one of: %s"):format(table.concat(node.values or {}, ", ")))
		end
	elseif kind == "id" then
		if type(value) ~= "number" or value % 1 ~= 0 or value < 0 then
			return push(problems, path, ("must be %s: a whole number, 0 for none"):format(ID_NAMES[node.idKind] or "an id"))
		end
	elseif kind == "dataPath" then
		if not isDataPath(value) then
			return push(problems, path, "must be a data path like Stats.Cash")
		end
	elseif kind == "utcDate" then
		if Schema.parseUtcDate(value) == nil then
			return push(problems, path, "must be a UTC date written YYYY-MM-DD")
		end
	elseif kind == "color" then
		if typeof(value) ~= "Color3" then
			return push(problems, path, "must be a Color3 (Color3.fromRGB(r, g, b))")
		end
		-- LuaText cannot write a NaN or infinite channel, so T.config and
		-- T.migrate would fail later on a value the Kernel took.
		for _, channel in ipairs({ value.R, value.G, value.B }) do
			if channel ~= channel or channel == math.huge or channel == -math.huge then
				return push(problems, path, "must have finite channels")
			end
		end
	elseif kind == "tree" then
		validateTree(value, path, problems, {}, 1)
	elseif kind == "list" then
		if type(value) ~= "table" or not isArray(value) then
			return push(problems, path, "must be a list")
		end
		if node.minItems ~= nil and #value < node.minItems then
			push(problems, path, ("must have at least %d entries"):format(node.minItems))
		end
		if node.maxItems ~= nil and #value > node.maxItems then
			push(problems, path, ("must have at most %d entries"):format(node.maxItems))
		end
		for index, item in ipairs(value) do
			validateNode(node.item, item, childPath(path, index), problems, opts)
		end
	elseif kind == "map" then
		if type(value) ~= "table" then
			return push(problems, path, "must be a table")
		end
		for _, key in ipairs(sortedKeys(value)) do
			if type(key) ~= "string" then
				push(problems, path, "keys must be text")
			else
				validateNode(node.value, value[key], childPath(path, key), problems, opts)
			end
		end
	elseif kind == "record" or kind == "config" then
		if type(value) ~= "table" then
			return push(problems, path, "must be a table")
		end
		for _, key in ipairs(sortedKeys(value)) do
			if type(key) ~= "string" or node.fields[key] == nil then
				local hint = type(key) == "string" and suggest(key, node.fields) or nil
				push(problems, childPath(path, key),
					hint and ("is not a known option (did you mean %s?)"):format(hint) or "is not a known option")
			end
		end
		for _, name in ipairs(sortedKeys(node.fields)) do
			local field = node.fields[name]
			if not (kind == "config" and opts.skipServer and unwrap(field).server == true) then
				validateNode(field, value[name], childPath(path, name), problems, opts)
			end
		end
	end
end

function Schema.validate(root, value, rootPath, opts)
	local problems = {}
	validateNode(root, value, rootPath or "", problems, opts or {})
	return problems
end

function Schema.formatErrors(problems)
	local out = {}
	for _, problem in ipairs(problems) do
		table.insert(out, ("%s: %s"):format(problem.path, problem.message))
	end
	return out
end

-- ── defaults and merge ───────────────────────────────────────────────

function Schema.defaults(node)
	local kind = node.kind
	if kind == "optional" then
		if node.inner.default ~= nil then
			return Schema.defaults(node.inner)
		end
		return nil
	elseif kind == "record" or kind == "config" then
		local out = {}
		for name, field in pairs(node.fields) do
			out[name] = Schema.defaults(field)
		end
		return out
	elseif kind == "list" or kind == "map" then
		return Schema.merge(node, nil, node.default or {})
	end
	return copy(node.default)
end

function Schema.merge(node, base, override)
	if override == nil then
		return copy(base)
	end
	local n = unwrap(node)
	if type(override) ~= "table" then
		return copy(override)
	end
	if n.kind == "list" then
		local out = {}
		for index, item in ipairs(override) do
			out[index] = Schema.merge(n.item, Schema.defaults(n.item), item)
		end
		for key, item in pairs(override) do
			if out[key] == nil then
				out[key] = copy(item) -- kept so validation can reject a non-list
			end
		end
		return out
	end
	if n.kind ~= "record" and n.kind ~= "config" and n.kind ~= "map" then
		return copy(override)
	end
	local out = type(base) == "table" and copy(base) or {}
	for key, item in pairs(override) do
		local child = if n.kind == "map" then n.value else n.fields[key]
		if child == nil then
			out[key] = copy(item) -- unknown option: kept so validation reports it
		else
			local current = out[key]
			if current == nil then
				current = Schema.defaults(child)
			end
			out[key] = Schema.merge(child, current, item)
		end
	end
	return out
end

-- ── server-only options ──────────────────────────────────────────────

function Schema.serverFields(root)
	local set = {}
	for name, field in pairs(root.fields) do
		if unwrap(field).server == true then
			set[name] = true
		end
	end
	return set
end

function Schema.stripServer(root, value)
	local out = copy(value)
	for name in pairs(Schema.serverFields(root)) do
		out[name] = nil
	end
	return out
end

-- ── patterns ─────────────────────────────────────────────────────────

-- Luau rejects a malformed pattern only when string.match reaches the bad
-- item, so a probe on one subject lets most of them through ("a[" never
-- fails on "") and they throw later, on the game's value. patternError
-- walks the whole pattern once with the matcher's own rules (lstrlib) and
-- returns the reason the first bad item gives, or nil when every item is
-- well formed.
local MAX_CAPTURES = 32 -- LUA_MAXCAPTURES

-- setEnd(pattern, i) -> the index after the ']' that closes a set whose
-- first character is at i, or nil. As in the matcher, the first character
-- after '[' or '[^' always belongs to the set, so "[]]" and "[^]]" are sets
-- holding ']', and a '%' inside a set escapes the character after it.
local function setEnd(pattern, i)
	local n = #pattern
	if string.sub(pattern, i, i) == "^" then
		i += 1
	end
	repeat
		if i > n then
			return nil
		end
		local c = string.sub(pattern, i, i)
		i += 1
		if c == "%" and i <= n then
			i += 1
		end
	until string.sub(pattern, i, i) == "]"
	return i + 1
end

function Schema.patternError(pattern)
	if type(pattern) ~= "string" then
		return "pattern must be text"
	end
	local n = #pattern
	local i = if string.sub(pattern, 1, 1) == "^" then 2 else 1
	local closed = {} -- closed[k] becomes true when capture k gets its ')'
	local open = {} -- captures still open, innermost last
	while i <= n do
		local c = string.sub(pattern, i, i)
		if c == "(" then
			table.insert(closed, false)
			if #closed > MAX_CAPTURES then
				return "too many captures"
			end
			table.insert(open, #closed)
			i += 1
		elseif c == ")" then
			local index = table.remove(open)
			if index == nil then
				return "invalid pattern capture"
			end
			closed[index] = true
			i += 1
		elseif c == "[" then
			local after = setEnd(pattern, i + 1)
			if after == nil then
				return "missing ']'"
			end
			i = after
		elseif c == "%" then
			if i == n then
				return "ends with '%'"
			end
			local nextChar = string.sub(pattern, i + 1, i + 1)
			if nextChar == "b" then
				if i + 3 > n then
					return "missing arguments to '%b'"
				end
				i += 4
			elseif nextChar == "f" then
				if string.sub(pattern, i + 2, i + 2) ~= "[" then
					return "missing '[' after '%f'"
				end
				local after = setEnd(pattern, i + 3)
				if after == nil then
					return "missing ']'"
				end
				i = after
			elseif string.find(nextChar, "^%d$") then
				if closed[tonumber(nextChar)] ~= true then
					return "invalid capture index %" .. nextChar
				end
				i += 2
			else
				i += 2
			end
		else
			i += 1
		end
	end
	if #open > 0 then
		return "unfinished capture"
	end
	return nil
end

-- ── schema definition check ──────────────────────────────────────────

local function isCount(value)
	return type(value) == "number" and value >= 0 and value % 1 == 0
end

-- The option values validation relies on. A wrong type here would make
-- validation misbehave (a number compared with a string, a boolean
-- concatenated into a message) and a malformed pattern would only fail on
-- the game's value, so the schema is rejected instead.
local function checkOptions(node, path, errors)
	for _, option in ipairs({ "min", "max" }) do
		if node[option] ~= nil and (type(node[option]) ~= "number" or node[option] ~= node[option]) then
			table.insert(errors, ("%s: %s must be a number"):format(path, option))
		end
	end
	for _, option in ipairs({ "minLength", "maxLength", "minItems", "maxItems" }) do
		if node[option] ~= nil and not isCount(node[option]) then
			table.insert(errors, ("%s: %s must be a whole number >= 0"):format(path, option))
		end
	end
	if node.pattern ~= nil then
		if type(node.pattern) ~= "string" then
			table.insert(errors, ("%s: pattern must be text"):format(path))
		else
			local reason = Schema.patternError(node.pattern)
			if reason ~= nil then
				table.insert(errors, ("%s: pattern is not a valid Luau pattern (%s)"):format(path, reason))
			end
		end
	end
	if node.kind == "enum" then
		if type(node.values) ~= "table" or not isArray(node.values) then
			table.insert(errors, ("%s: enum needs a list of text values"):format(path))
		elseif #node.values == 0 then
			table.insert(errors, ("%s: enum needs at least one value"):format(path))
		else
			for _, value in ipairs(node.values) do
				if type(value) ~= "string" then
					table.insert(errors, ("%s: enum values must be text"):format(path))
					break
				end
			end
		end
	end
	if node.check ~= nil then
		local where = if path == "" then "the schema" else path
		if type(node.check) ~= "function" then
			table.insert(errors, ("%s: check must be a function"):format(where))
		elseif type(node.checkDoc) ~= "string" or node.checkDoc == "" then
			table.insert(errors, ("%s: check needs a checkDoc that says its rule"):format(where))
		end
	end
	if node.pinned ~= nil and type(node.pinned) ~= "boolean" then
		table.insert(errors, ("%s: pinned must be true or false"):format(if path == "" then "the schema" else path))
	end
end

-- Validates what the node gives the game when the game leaves it out: its
-- default, with list items and map values filled in from their own
-- defaults, exactly as Config.load merges it. On a group (only reached for
-- an S.optional group, see checkNode) the default is the switch that makes
-- the group present, built by Schema.defaults from its fields' defaults and
-- never from the switch's values: the switch must be `{}`, since values in
-- it would be dropped without a word, and the group it switches on must be
-- valid. In a pcall, because a default the merge cannot copy (a table that
-- contains itself) must become an error in the list, not a throw.
local function checkDefault(node, path, errors)
	local problems = {}
	local ok, err = pcall(function()
		if node.kind == "record" or node.kind == "config" then
			if type(node.default) ~= "table" then
				validateNode(node, node.default, path, problems, {})
			else
				if next(node.default) ~= nil then
					push(problems, path, "an optional group's default only switches it on, its values are not used;"
						.. " give each field its own default and use default = {}")
				end
				validateNode(node, Schema.defaults(node), path, problems, {})
			end
		else
			validateNode(node, Schema.defaults(node), path, problems, {})
		end
	end)
	if not ok then
		table.insert(errors, ("default of %s: cannot be checked (%s)"):format(path, tostring(err)))
		return
	end
	local seen = {}
	for _, problem in ipairs(problems) do
		local line = ("default of %s: %s"):format(problem.path, problem.message)
		if not seen[line] then
			seen[line] = true
			table.insert(errors, line)
		end
	end
end

-- `server` is true under a server-only option: nothing there may carry a
-- default (RT-10). `collection` is true under a list item or a map value:
-- nothing there may be pinned (plan B0), since T.migrate compares a pinned
-- option at one path.
local function checkNode(node, path, errors, topLevel, optional, server, collection)
	if type(node) ~= "table" or type(node.kind) ~= "string" or not KNOWN[node.kind] then
		table.insert(errors, ("%s is not a schema node"):format(path))
		return
	end
	if node.kind == "optional" then
		return checkNode(node.inner, path, errors, topLevel, true, server, collection)
	end
	local kind = node.kind
	local before = #errors
	if node.server == true and not topLevel then
		table.insert(errors, ("%s: server = true is only allowed on top-level options"):format(path))
	end
	if node.pinned == true and collection then
		table.insert(errors, ("%s: pinned is only allowed outside lists and maps"):format(path))
	elseif node.pinned == true and path == "" then
		-- The root has no describe row, so T.migrate would never see the flag.
		table.insert(errors, "the schema: pinned is only allowed on options")
	end
	local serverOnly = server == true or (topLevel and node.server == true)
	checkOptions(node, path, errors)
	if kind == "list" then
		checkNode(node.item, path .. "[]", errors, false, nil, serverOnly, true)
	elseif kind == "map" then
		checkNode(node.value, path .. ".<key>", errors, false, nil, serverOnly, true)
	elseif kind == "record" or kind == "config" then
		if type(node.fields) ~= "table" then
			table.insert(errors, ("%s: needs a table of fields"):format(path))
		else
			for _, name in ipairs(sortedKeys(node.fields)) do
				local field = node.fields[name]
				local fieldPath = childPath(path, name)
				local inner = unwrap(field)
				if type(inner) ~= "table" or type(inner.doc) ~= "string" or inner.doc == "" then
					table.insert(errors, ("%s needs a doc"):format(fieldPath))
				end
				checkNode(field, fieldPath, errors, kind == "config", nil, serverOnly, collection)
			end
		end
	end
	if node.default == nil then
		return
	end
	if serverOnly then
		table.insert(errors, ("%s: a server-only option has no default: the Schema reaches every client, and so would its default. Leave it out (S.optional, or required) and set the value in the server Config module")
			:format(path))
		return
	end
	if (kind == "record" or kind == "config") and not optional then
		-- Schema.defaults builds a group from its fields and reads a group's
		-- default only when the group is S.optional, as the switch that makes
		-- it present. Anywhere else the default has no effect at all.
		table.insert(errors, ("%s: default has no effect on a group that is not optional, give each field its own default")
			:format(path == "" and "the schema" or path))
	elseif #errors == before then
		-- Only on a sound node: a bad option or child is exactly what would
		-- make validating the default throw or report noise.
		checkDefault(node, path, errors)
	end
end

function Schema.check(root)
	if type(root) ~= "table" or root.kind ~= "config" then
		return { "the schema must be S.config({...})" }
	end
	local errors = {}
	checkNode(root, "", errors, false)
	return errors
end

-- ── describe (reference documentation) ───────────────────────────────

local function typeName(node)
	local kind = node.kind
	if kind == "number" then
		local parts = { node.integer and "whole number" or "number" }
		if node.min ~= nil then table.insert(parts, ">= " .. tostring(node.min)) end
		if node.max ~= nil then table.insert(parts, "<= " .. tostring(node.max)) end
		return table.concat(parts, ", ")
	elseif kind == "string" then
		return "text"
	elseif kind == "boolean" then
		return "true/false"
	elseif kind == "enum" then
		return "one of " .. table.concat(node.values or {}, " | ")
	elseif kind == "id" then
		return ID_NAMES[node.idKind] or "an id"
	elseif kind == "dataPath" then
		return "data path (e.g. Stats.Cash)"
	elseif kind == "utcDate" then
		return "UTC date YYYY-MM-DD"
	elseif kind == "color" then
		return "color (Color3)"
	elseif kind == "tree" then
		return "tree (text keys; numbers, text, true/false or nested tables)"
	elseif kind == "record" then
		return "group"
	end
	return kind
end

-- LuaText's number rule: a whole number as %d, else the shortest %.<p>g
-- that reads back exactly (a float32 channel needs up to 17 digits).
local function exactNumber(value)
	if value == math.floor(value) and math.abs(value) < 2 ^ 53 then
		return ("%d"):format(value)
	end
	for precision = 1, 17 do
		local text = ("%." .. precision .. "g"):format(value)
		if tonumber(text) == value then
			return text
		end
	end
	return ("%.17g"):format(value)
end

local function show(value)
	if value == nil then
		return ""
	end
	if typeof(value) == "Color3" then
		-- LuaText's rule (plan B0, planning decision 9): fromRGB only when
		-- every channel is exactly a whole 1/255 step as fromRGB makes it (a
		-- float32), else Color3.new with each channel's exact value.
		local channels, steps = { value.R, value.G, value.B }, {}
		for index, channel in ipairs(channels) do
			local step = math.round(channel * 255)
			if steps ~= nil and step >= 0 and step <= 255 and string.unpack("<f", string.pack("<f", step / 255)) == channel then
				steps[index] = step
			else
				steps = nil
			end
		end
		if steps ~= nil then
			return ("Color3.fromRGB(%d, %d, %d)"):format(steps[1], steps[2], steps[3])
		end
		return ("Color3.new(%s, %s, %s)"):format(exactNumber(channels[1]), exactNumber(channels[2]), exactNumber(channels[3]))
	end
	if type(value) == "string" then
		return ("%q"):format(value)
	end
	if type(value) == "table" then
		local count = 0
		for _ in pairs(value) do
			count += 1
		end
		return count == 0 and "{}" or ("{%d entries}"):format(count)
	end
	return tostring(value)
end

-- The limits a reader needs that `typeName` does not already say: text
-- length and pattern, and list sizes. Worded like the validation messages.
local function plural(count, one, many)
	return ("%d %s"):format(count, count == 1 and one or many)
end

local function constraintsOf(node)
	local out = {}
	if node.kind == "string" then
		if node.minLength == 1 then
			table.insert(out, "not empty")
		elseif node.minLength ~= nil then
			table.insert(out, "at least " .. plural(node.minLength, "character", "characters"))
		end
		if node.maxLength ~= nil then
			table.insert(out, "at most " .. plural(node.maxLength, "character", "characters"))
		end
		if node.pattern ~= nil then
			table.insert(out, "matches " .. tostring(node.patternDoc or node.pattern))
		end
	elseif node.kind == "list" then
		if node.minItems ~= nil then
			table.insert(out, "at least " .. plural(node.minItems, "entry", "entries"))
		end
		if node.maxItems ~= nil then
			table.insert(out, "at most " .. plural(node.maxItems, "entry", "entries"))
		end
	end
	if type(node.checkDoc) == "string" and node.checkDoc ~= "" then
		table.insert(out, node.checkDoc)
	end
	return out
end

-- describe(root) -> rows, one per option, in path order:
--   { path, type, default, doc, server, required,     -- display fields
--     optional, value, example, constraints,          -- for templates and Tools
--     pinned }                                        -- plan B0, for T.migrate
-- `default` is the display text the reference prints. `value` is the
-- default itself, a fresh copy, with the defaults of nested fields filled in
-- (Schema.defaults), nil when there is none. `example` is a copy of the
-- option's `example`. `constraints` lists the limits `type` does not say.
function Schema.describe(root)
	local rows = {}
	-- A list item or a map value is a structural node, not required to carry
	-- its own doc (Schema.check does not demand one there). Its row falls
	-- back to the list/map's own doc; an explicit doc on the item still wins.
	local function walk(node, path, server, parentDoc)
		local optional = node.kind == "optional"
		local n = unwrap(node)
		local isServer = server or n.server == true
		if path ~= "" then
			local doc = n.doc
			if doc == nil or doc == "" then
				doc = parentDoc
			end
			table.insert(rows, {
				path = path,
				type = typeName(n),
				default = show(n.default),
				doc = doc or "",
				server = isServer,
				required = (not optional) and n.default == nil and SCALAR[n.kind] == true,
				optional = optional,
				value = Schema.defaults(node),
				example = copy(n.example),
				constraints = constraintsOf(n),
				pinned = n.pinned == true,
			})
		end
		if n.kind == "record" or n.kind == "config" then
			for _, name in ipairs(sortedKeys(n.fields)) do
				walk(n.fields[name], childPath(path, name), isServer, nil)
			end
		elseif n.kind == "list" then
			walk(n.item, path .. "[]", isServer, n.doc)
		elseif n.kind == "map" then
			walk(n.value, path .. ".<key>", isServer, n.doc)
		end
	end
	walk(root, "", false, nil)
	return rows
end

-- ── freeze ───────────────────────────────────────────────────────────

function Schema.freeze(value)
	if type(value) ~= "table" or table.isfrozen(value) then
		return value
	end
	for _, item in pairs(value) do
		Schema.freeze(item)
	end
	return table.freeze(value)
end

return Schema
]]></string>
            <int64 name="SourceAssetId">-1</int64>
            <BinaryString name="Tags"></BinaryString>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="12">
          <Properties>
            <string name="Name">Signal</string>
            <BinaryString name="AttributesSerialize"></BinaryString>
            <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
            <ContentId name="LinkedSource">
              <null>
              </null>
            </ContentId>
            <bool name="DefinesCapabilities">false</bool>
            <BinaryString name="SerializedOverrides">
            </BinaryString>
            <string name="Source"><![CDATA[-- Minimal signal (connect/fire/once/wait). Handlers run via task.spawn so a
-- slow or erroring handler never blocks the firer.
local Signal = {}
Signal.__index = Signal

local Connection = {}
Connection.__index = Connection

function Connection:Disconnect()
	if not self.Connected then return end
	self.Connected = false
	local conns = self._signal._connections
	for i, c in ipairs(conns) do
		if c == self then table.remove(conns, i) break end
	end
end

function Signal.new()
	return setmetatable({ _connections = {} }, Signal)
end

function Signal:Connect(fn)
	local conn = setmetatable({ _signal = self, _fn = fn, Connected = true }, Connection)
	table.insert(self._connections, conn)
	return conn
end

function Signal:Once(fn)
	local conn
	conn = self:Connect(function(...)
		conn:Disconnect()
		fn(...)
	end)
	return conn
end

function Signal:Fire(...)
	for _, conn in ipairs(table.clone(self._connections)) do
		if conn.Connected then
			task.spawn(conn._fn, ...)
		end
	end
end

function Signal:Wait()
	local thread = coroutine.running()
	self:Once(function(...)
		task.spawn(thread, ...)
	end)
	return coroutine.yield()
end

function Signal:Destroy()
	for _, conn in ipairs(table.clone(self._connections)) do
		conn:Disconnect()
	end
end

return Signal
]]></string>
            <int64 name="SourceAssetId">-1</int64>
            <BinaryString name="Tags"></BinaryString>
          </Properties>
        </Item>
        <Item class="Folder" referent="13">
          <Properties>
            <string name="Name">UI</string>
            <BinaryString name="AttributesSerialize"></BinaryString>
            <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
            <Color3 name="IconTint">
              <R>0</R>
              <G>0</G>
              <B>0</B>
            </Color3>
            <bool name="DefinesCapabilities">false</bool>
            <BinaryString name="SerializedOverrides">
            </BinaryString>
            <int64 name="SourceAssetId">-1</int64>
            <BinaryString name="Tags"></BinaryString>
          </Properties>
          <Item class="ModuleScript" referent="14">
            <Properties>
              <string name="Name">MenuSystem</string>
              <BinaryString name="AttributesSerialize"></BinaryString>
              <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
              <ContentId name="LinkedSource">
                <null>
                </null>
              </ContentId>
              <bool name="DefinesCapabilities">false</bool>
              <BinaryString name="SerializedOverrides">
              </BinaryString>
              <string name="Source"><![CDATA[-- Runtime menu system. Replaces v1's install-time MenusGUI merge
-- (GUIManager + ToggleListener + GUIBridge) with runtime registration.
local CollectionService = game:GetService("CollectionService")
local ContentProvider = game:GetService("ContentProvider")
local TweenService = game:GetService("TweenService")
local Players = game:GetService("Players")

local SFX = require(script.Parent.Parent.SFX)
local buildShell = require(script.Parent.Shell)
local PanelAnimations = require(script.Parent.PanelAnimations)

local TOOLTIP_OFFSET_X = 15
local TOOLTIP_OFFSET_Y = 78
local TOOLTIP_THROTTLE_INTERVAL = 0.016
local ICON_HOVER_SCALE = UDim2.new(1.05, 0, 1.05, 0)
local ICON_DEFAULT_SCALE = UDim2.new(1, 0, 1, 0)
local ICON_HOVER_TWEEN_TIME = 0.1
local ICON_LEAVE_TWEEN_TIME = 0.3
local TOGGLE_COLOR_ON = Color3.fromRGB(85, 255, 0)
local TOGGLE_COLOR_OFF = Color3.fromRGB(255, 50, 50)

-- AdoptPriority is buyer-editable (Studio attribute), so it may arrive as a
-- non-numeric type (e.g. a String) instead of the expected number. Both call
-- sites coerce through this helper rather than trusting the raw attribute -
-- a non-numeric value is treated as priority 0 instead of crashing the
-- table.sort comparator / the <= comparison in _adoptCandidate.
local function readAdoptPriority(inst)
	return tonumber(inst:GetAttribute("AdoptPriority")) or 0
end

-- Deterministic adoption order: highest AdoptPriority first, ties broken by
-- original GetTagged() encounter order (not Luau's unstable table.sort) so a
-- lower-priority duplicate is NEVER reparented during a sweep - it always
-- loses to a higher-priority sibling already processed first, regardless of
-- CollectionService's return order.
local function sortByAdoptPriority(list)
	local decorated = {}
	for i, inst in ipairs(list) do
		decorated[i] = { inst = inst, priority = readAdoptPriority(inst), index = i }
	end
	table.sort(decorated, function(a, b)
		if a.priority ~= b.priority then
			return a.priority > b.priority
		end
		return a.index < b.index
	end)
	local out = {}
	for i, d in ipairs(decorated) do
		out[i] = d.inst
	end
	return out
end

-- Async (task.spawn) image warm-up shared by both panel-acquisition paths:
-- registerPanel's clone (below) and the panel branch of adoption
-- (_adoptCandidate, "parity restoration" fix - adopted panels used to skip
-- this entirely, causing first-open texture pop-in for every StarterGui-
-- shipped panel post-edit-time-GUI-migration). Interactables are NOT
-- preloaded (registerPanel never did for addInteractable's clones either -
-- this only restores parity with registerPanel's own existing behavior, not
-- a new scope).
local function preloadPanelImages(panel)
	task.spawn(function()
		local images = {}
		for _, desc in ipairs(panel:GetDescendants()) do
			if desc:IsA("ImageLabel") or desc:IsA("ImageButton") then
				table.insert(images, desc)
			end
		end
		if #images > 0 then
			ContentProvider:PreloadAsync(images)
		end
	end)
end

local MenuSystem = {}
MenuSystem.__index = MenuSystem

function MenuSystem.new(playerGui, opts)
	local self = setmetatable({}, MenuSystem)
	self._playerGui = playerGui
	-- 3.0: the Kernel's piece runtime (UI/Pieces.lua). When present, every
	-- adoption candidate is offered to it first; nil keeps 2.x behavior.
	self._pieces = opts and opts.pieces or nil
	self._mouse = Players.LocalPlayer:GetMouse()
	self.gui = buildShell(playerGui)
	self._menus = self.gui.Menus
	self._interactables = self.gui.Interactables.Container
	self._tooltip = self.gui.Tooltip
	self._handlers = {}
	self._panels = {}
	self._panelPriority = {}
	self._panelScrollers = {}
	self._interactableRegistry = {}
	self._interactablePriority = {}
	self._animations = {}
	self._wired = {}
	self._toggled = {}

	for name, fn in pairs(PanelAnimations) do
		self._animations[name] = fn
	end

	for _, desc in ipairs(self.gui:GetDescendants()) do
		self:_wireDescendant(desc)
	end
	self.gui.DescendantAdded:Connect(function(desc)
		self:_wireDescendant(desc)
	end)

	for _, button in ipairs(CollectionService:GetTagged("UI_Toggle")) do
		self:_wireToggle(button)
	end
	CollectionService:GetInstanceAddedSignal("UI_Toggle"):Connect(function(button)
		self:_wireToggle(button)
	end)

	-- Edit-time GUI adoption (StarterGui-shipped panels/interactables). Must
	-- run after the shell + wiring above exist so reparenting into Menus /
	-- Interactables.Container flows through the DescendantAdded wiring path
	-- automatically (no separate wiring code needed for adopted instances).
	-- self:_safeAdopt pcall-wraps each candidate: MenuSystem.new runs BEFORE
	-- Loader.boot in KernelClient, so an uncaught error here would take down
	-- the whole client ecosystem, not just adoption - one bad tag must never
	-- do that.
	for _, inst in ipairs(sortByAdoptPriority(CollectionService:GetTagged("UI_Panel"))) do
		self:_safeAdopt(inst, "Panel")
	end
	CollectionService:GetInstanceAddedSignal("UI_Panel"):Connect(function(inst)
		self:_safeAdopt(inst, "Panel")
	end)

	for _, inst in ipairs(sortByAdoptPriority(CollectionService:GetTagged("UI_Interactable"))) do
		self:_safeAdopt(inst, "Interactable")
	end
	CollectionService:GetInstanceAddedSignal("UI_Interactable"):Connect(function(inst)
		self:_safeAdopt(inst, "Interactable")
	end)

	return self
end

-- ── handlers ─────────────────────────────────────────────────────────

function MenuSystem:registerHandlers(handlers)
	for action, fn in pairs(handlers) do
		self._handlers[action] = fn
	end
end

function MenuSystem:dispatch(action, button)
	local fn = self._handlers[action]
	if fn then
		fn(button)
	else
		warn("[RBLXE UI] No handler registered for action:", action)
	end
end

function MenuSystem:registerAnimation(name, fn)
	self._animations[name] = fn
end

-- ── panels ───────────────────────────────────────────────────────────

function MenuSystem:registerPanel(template)
	local panel = template:Clone()
	panel.Visible = false
	panel.Parent = self._menus
	self._panels[panel.Name] = panel
	local scrollers = {}
	for _, desc in ipairs(panel:GetDescendants()) do
		if desc:IsA("ScrollingFrame") then
			table.insert(scrollers, desc)
		end
	end
	self._panelScrollers[panel.Name] = scrollers
	preloadPanelImages(panel)
	return panel
end

function MenuSystem:getPanel(name)
	return self._panels[name]
end

--- The HUD button that actually opens `panelName` - the one this system
--- REGISTERED, not merely one that carries the attribute.
---
--- Packs need this to put a notification badge on their own button, and until
--- 2026-08-25 every one of them scanned the shell for the first GuiButton with
--- a matching `TargetPanel`. That is wrong whenever two packs ship a fallback
--- button for the same panel: the loser of the priority contest is hidden but
--- LEFT IN THE CONTAINER when it was adopted before the winner arrived (see
--- `_adoptCandidate`'s "deposed" branch), so the scan could - and did, for
--- Easy Pet System's Pets button - return an invisible button and light a
--- badge nobody could see. Which of the two a scan hits depends on sweep
--- order, so the packs that appeared to work were only lucky.
function MenuSystem:getInteractable(panelName)
	return self._interactableRegistry["TogglePanel:" .. tostring(panelName)]
end

--- A 3.0 interface piece, already resolved by the Kernel: the game's Interface
--- override when it satisfies its contract, otherwise the pack's default.
--- `pieceId` is "<PackId>.<Name>" (packs use ctx.getPiece("<Name>")). Yields
--- up to `timeout` seconds (default 10) for it to reach PlayerGui. A Panel or
--- HudButton piece (tagged UI_Panel / UI_Interactable) is only useful once
--- this menu adopted it; if it did not, the piece is still returned but a
--- warning says why the menu cannot open it, instead of failing silently.
function MenuSystem:getPiece(pieceId, timeout)
	if self._pieces == nil then
		return nil
	end
	local piece = self._pieces:get(pieceId, timeout)
	if piece ~= nil then
		local tag = (CollectionService:HasTag(piece, "UI_Panel") and "UI_Panel")
			or (CollectionService:HasTag(piece, "UI_Interactable") and "UI_Interactable")
			or nil
		-- M-d: the kind in the server's decision marks a menu piece even when
		-- it lacks its tag (a default that fails its contract).
		local kind = if self._pieces.kindOf ~= nil then self._pieces:kindOf(pieceId) else nil
		local menuPiece = tag ~= nil or kind == "Panel" or kind == "HudButton"
		if menuPiece and not self:_holds(piece) then
			-- A piece that has just arrived is adopted from its own tag signal:
			-- give that one frame before warning.
			task.wait()
			if not self:_holds(piece) then
				warn(("[RBLXE] UI: piece %s (%s) was not adopted by the menu; nothing can open it"):format(pieceId, tag or kind))
			end
		end
	end
	return piece
end

-- Whether `inst` holds a registry slot (an adopted or registered panel or
-- interactable), by identity.
function MenuSystem:_holds(inst)
	for _, registry in ipairs({ self._panels, self._interactableRegistry }) do
		for _, registered in pairs(registry) do
			if registered == inst then
				return true
			end
		end
	end
	return false
end

function MenuSystem:openPanel(name)
	local menu = self._panels[name]
	if not menu or menu.Visible then return end
	SFX:Play("Press")
	menu.Visible = true
	local animFn = self._animations[menu:GetAttribute("Animation") or "Bounce"]
	if animFn then animFn(menu) end
	for _, sf in ipairs(self._panelScrollers[name] or {}) do
		sf.CanvasPosition = Vector2.zero
	end
	if menu:GetAttribute("Exclusive") ~= false then
		for otherName, otherMenu in pairs(self._panels) do
			if otherName ~= name and otherMenu.Visible and otherMenu:GetAttribute("Exclusive") ~= false then
				otherMenu.Visible = false
			end
		end
	end
end

function MenuSystem:closePanel(name)
	local menu = self._panels[name]
	if menu then menu.Visible = false end
end

function MenuSystem:togglePanel(name)
	local menu = self._panels[name]
	if not menu then return end
	if menu.Visible then
		SFX:Play("Press")
		menu.Visible = false
	else
		self:openPanel(name)
	end
end

function MenuSystem:addInteractable(template)
	local button = template:Clone()
	button.Parent = self._interactables
	return button
end

-- ── adoption (edit-time GUI: StarterGui-shipped panels/interactables) ──

-- interactableKey(inst) -> the identity two packs' HUD buttons are deduped
-- by. A button that toggles a panel IS that panel's button, whatever the
-- instance happens to be called, so the key is the panel it opens.
--
-- It used to be `inst.Name`, which only ever worked by coincidence: General
-- UI Kit and Easy Pet System both shipped a fallback "Shop" button and both
-- happened to name it after the panel. Renaming General UI Kit's buttons to
-- "Shop Button" etc. (2026-08-21) broke that coincidence instantly - the two
-- stopped colliding, both got adopted, and the HUD would have shown two Shop
-- buttons side by side, each opening the same panel. Keying off what the
-- button DOES cannot be broken by a rename.
--
-- Anything that is not a panel toggle (a pack's own custom Action, or a
-- decorative interactable with none) falls back to the name: there is
-- nothing better to compare, and two of those are not obviously duplicates.
local function interactableKey(inst)
	if inst:GetAttribute("Action") == "TogglePanel" then
		local target = inst:GetAttribute("TargetPanel")
		if type(target) == "string" and target ~= "" then
			return "TogglePanel:" .. target
		end
	end
	return inst.Name
end

local ADOPT_KIND = {
	Panel = { tag = "UI_Panel", container = "_menus", registry = "_panels", priorities = "_panelPriority" },
	Interactable = { tag = "UI_Interactable", container = "_interactables", registry = "_interactableRegistry", priorities = "_interactablePriority" },
}

-- Entry point for both the sweep (GetTagged) and late-arrival
-- (GetInstanceAddedSignal) paths. Exposed (not local-only) so specs can
-- drive it directly where there is no natural way to simulate a scenario
-- through the tag-add signal (e.g. reentrancy guard).
function MenuSystem:_adoptCandidate(inst, kind)
	local cfg = ADOPT_KIND[kind]
	if not cfg then return end
	-- A UI_Panel/UI_Interactable tag on a non-GuiObject (Folder, ScreenGui,
	-- UIStroke, ...) is not a valid candidate - bail before touching any
	-- GuiObject-only property (Visible, below) that would error on it.
	if not inst:IsA("GuiObject") then return end
	-- Only adopt from the local player's own PlayerGui tree, and never
	-- re-adopt something already living inside our own shell (avoids
	-- re-registration loops for anything CollectionService reports again).
	if not inst:IsDescendantOf(self._playerGui) or inst:IsDescendantOf(self.gui) then
		return
	end

	-- 3.0 pieces: a copy the Kernel did not choose (the default behind a valid
	-- Interface override, or the reverse, or a clone a pack made) is never
	-- adopted. Instances without RBLXE_Piece are always admitted, so 2.x GUIs
	-- keep the priority path below.
	if self._pieces ~= nil and not self._pieces:admit(inst) then
		return
	end

	local registry = self[cfg.registry]
	local priorities = self[cfg.priorities]
	local name = (kind == "Panel" and inst:GetAttribute("PanelId")) or interactableKey(inst)
	local priority = readAdoptPriority(inst)
	local existingPriority = priorities[name]
	local incumbent = registry[name]
	local chosen = self._pieces ~= nil and self._pieces:chose(inst)

	if chosen then
		-- The Kernel already decided this piece: it takes the slot whatever
		-- the AdoptPriority of a 2.x copy, which is deposed (hidden) instead.
		-- Two chosen pieces on one key is a declaration conflict: the first
		-- keeps the slot and both are named.
		local pieceId = inst:GetAttribute("RBLXE_Piece")
		if incumbent ~= nil and incumbent ~= inst and self._pieces:chose(incumbent) then
			inst.Visible = false
			warn(("[RBLXE] UI: pieces %s and %s both use %s '%s'; %s keeps it, %s is left unadopted")
				:format(tostring(incumbent:GetAttribute("RBLXE_Piece")), tostring(pieceId), cfg.tag, tostring(name),
					tostring(incumbent:GetAttribute("RBLXE_Piece")), tostring(pieceId)))
			return
		end
		if incumbent ~= nil and incumbent ~= inst then
			warn(("[RBLXE] UI: %s '%s' %s deposed by the 3.0 piece %s")
				:format(cfg.tag, tostring(name), incumbent:GetFullName(), tostring(pieceId)))
		end
		priority = math.huge
	elseif incumbent ~= nil and self._pieces ~= nil and self._pieces:chose(incumbent) then
		-- A 2.x copy arriving after the piece: the slot stays with the piece.
		inst.Visible = false
		warn(("[RBLXE] UI: %s '%s' %s left unadopted: the slot belongs to the 3.0 piece %s")
			:format(cfg.tag, tostring(name), inst:GetFullName(), tostring(incumbent:GetAttribute("RBLXE_Piece"))))
		return
	elseif existingPriority ~= nil and priority <= existingPriority then
		-- Loses to the incumbent (strictly lower, or a tie): left in place,
		-- unadopted, hidden so a dangling duplicate is never visible/clickable.
		inst.Visible = false
		if incumbent ~= nil and incumbent.Parent == nil then
			warn(("[RBLXE] duplicate %s '%s' left unadopted (priority): the incumbent was destroyed"):format(cfg.tag, name))
		else
			warn(("[RBLXE] duplicate %s '%s' left unadopted (priority)"):format(cfg.tag, name))
		end
		return
	end

	-- A higher-priority late arrival swaps the registration: the previously
	-- registered instance (if any) is deposed - hide it so it doesn't stay
	-- visible/reachable (an open panel, or a still-clickable interactable)
	-- after losing its registry slot, symmetric with the sweep-loser path.
	local previous = registry[name]
	if previous and previous ~= inst then
		previous.Visible = false
	end

	inst.Parent = self[cfg.container]
	if kind == "Panel" then
		inst.Visible = false -- panels default closed; interactables stay visible
	end
	registry[name] = inst
	priorities[name] = priority

	if kind == "Panel" then
		local scrollers = {}
		for _, desc in ipairs(inst:GetDescendants()) do
			if desc:IsA("ScrollingFrame") then
				table.insert(scrollers, desc)
			end
		end
		self._panelScrollers[name] = scrollers
		-- Parity restoration (M3 review fix): registerPanel always warmed up
		-- its clone's images; adopted panels must get the identical treatment
		-- - otherwise every StarterGui-shipped panel (GUK's 13, EPS's own)
		-- loses this warm-up entirely post edit-time-GUI-migration, causing
		-- first-open texture pop-in that registerPanel-based panels never had.
		preloadPanelImages(inst)
	end
end

-- Safety net around _adoptCandidate: MenuSystem.new runs before Loader.boot,
-- so any uncaught error here would kill client boot entirely. A malformed
-- candidate (or anything else unexpected) is logged and skipped instead.
function MenuSystem:_safeAdopt(inst, kind)
	local ok, err = pcall(self._adoptCandidate, self, inst, kind)
	if not ok then
		warn(("[RBLXE] adoption failed for '%s': %s"):format(inst.Name, tostring(err)))
	end
end

function MenuSystem:_findAncestorPanel(instance)
	local current = instance.Parent
	while current and current ~= self.gui do
		-- Identity lookup, not a Name-keyed probe: adopted panels register
		-- under their PanelId attribute, which may differ from the
		-- instance's own Name - self._panels[current.Name] would miss those.
		for _, panel in pairs(self._panels) do
			if panel == current then
				return current
			end
		end
		current = current.Parent
	end
	return nil
end

-- Action routing. Real clicks call this from _wireButton; tests call it
-- directly because Activated cannot be fired programmatically.
function MenuSystem:_onButtonActivated(button)
	local action = button:GetAttribute("Action")
	if not action or action == "" then return end
	if action == "TogglePanel" then
		self:togglePanel(button:GetAttribute("TargetPanel") or "")
	elseif action == "ClosePanel" then
		local target = button:GetAttribute("TargetPanel") or ""
		if target ~= "" then
			self:closePanel(target)
		else
			local ancestor = self:_findAncestorPanel(button)
			if ancestor then ancestor.Visible = false end
		end
	else
		self:dispatch(action, button)
	end
end

-- ── wiring (Task 8 fills these in) ───────────────────────────────────

function MenuSystem:_wireDescendant(desc)
	if desc:IsA("GuiButton") then
		self:_wireButton(desc)
	elseif desc:IsA("GuiObject") then
		self:_wireDecoration(desc)
	end
end

function MenuSystem:_wireButton(button)
	if self._wired[button] then return end
	self._wired[button] = true
	if CollectionService:HasTag(button, "UI_Toggle") then return end
	-- A blocker is a GuiButton only so that it SINKS input - the invisible
	-- full-screen sheet a modal panel puts behind itself so clicks never
	-- reach what it covers. It is not something the player presses, so it
	-- gets no press sound, no hover sound, no icon tween and no tooltip.
	-- Reported by the user against Roulette System's Spin: "cada vez que
	-- clico el fondo del menu Spin suena el SFX de click". Declared with a
	-- tag rather than inferred: 100 GuiButtons across the shipped v2 assets
	-- carry neither a tag nor an Action (Teleport zones, Claim buttons,
	-- Equip/Merge/Delete, the music player's Next/Mute - all wired by pack
	-- code and all of them SHOULD click), so there is no property of a
	-- blocker to detect. It still sinks input exactly as before: absorbing
	-- clicks is what a GuiButton does, not something this wiring conferred.
	if CollectionService:HasTag(button, "UI_Blocker") then return end

	button.Activated:Connect(function()
		SFX:Play(button:GetAttribute("BuySound") and "Buy_1" or "Press")
		self:_onButtonActivated(button)
	end)

	local icon = button:FindFirstChild("Icon")
	button.MouseEnter:Connect(function()
		SFX:Play("Hover")
		if icon then
			icon:TweenSize(ICON_HOVER_SCALE, Enum.EasingDirection.Out, Enum.EasingStyle.Quint, ICON_HOVER_TWEEN_TIME, true)
		end
		if button:GetAttribute("ShowTooltip") then
			self._tooltip.Visible = true
			self._tooltip.ButtonName.Text = button:GetAttribute("TooltipText") or button.Name
		end
	end)
	local lastTooltipMove = 0
	button.MouseMoved:Connect(function()
		if button:GetAttribute("ShowTooltip") then
			local now = tick()
			if now - lastTooltipMove < TOOLTIP_THROTTLE_INTERVAL then return end
			lastTooltipMove = now
			self._tooltip.Position = UDim2.fromOffset(
				self._mouse.X + TOOLTIP_OFFSET_X, self._mouse.Y + TOOLTIP_OFFSET_Y)
		end
	end)
	button.MouseLeave:Connect(function()
		if icon then
			icon:TweenSize(ICON_DEFAULT_SCALE, Enum.EasingDirection.Out, Enum.EasingStyle.Quint, ICON_LEAVE_TWEEN_TIME, true)
		end
		if button:GetAttribute("ShowTooltip") then
			self._tooltip.Visible = false
		end
	end)
end

function MenuSystem:_wireDecoration(element)
	if self._wired[element] then return end
	local animation = element:GetAttribute("Animation")
	if not animation then return end
	if element.Parent == self._menus then return end -- panels use Animation for their open-anim
	self._wired[element] = true
	if animation == "Rotate" then
		local speed = element:GetAttribute("Speed") or 15
		TweenService:Create(element,
			TweenInfo.new(speed, Enum.EasingStyle.Linear, Enum.EasingDirection.Out, -1),
			{ Rotation = 360 }):Play()
	elseif animation == "HoverBob" then
		local basePos = element.Position
		element.MouseEnter:Connect(function()
			element:TweenPosition(basePos - UDim2.fromScale(0, 0.03),
				Enum.EasingDirection.Out, Enum.EasingStyle.Quint, 0.2, true)
		end)
		element.MouseLeave:Connect(function()
			element:TweenPosition(basePos,
				Enum.EasingDirection.Out, Enum.EasingStyle.Quint, 0.2, true)
		end)
	end
end

function MenuSystem:_wireToggle(button)
	if not button:IsA("GuiButton") then return end
	if not button:IsDescendantOf(self._playerGui) then return end -- ignore RS/asset templates
	if self._toggled[button] then return end
	local state = { isEnabled = true }
	self._toggled[button] = state
	local function setVisual()
		button.BackgroundColor3 = state.isEnabled and TOGGLE_COLOR_ON or TOGGLE_COLOR_OFF
		local status = button:FindFirstChild("Status")
		if status then status.Text = state.isEnabled and "ON" or "OFF" end
	end
	setVisual()
	button.MouseButton1Click:Connect(function()
		state.isEnabled = not state.isEnabled
		setVisual()
		local action = button:GetAttribute("Action")
		if action then self:dispatch(action, button) end
	end)
end

return MenuSystem
]]></string>
              <int64 name="SourceAssetId">-1</int64>
              <BinaryString name="Tags"></BinaryString>
            </Properties>
          </Item>
          <Item class="ModuleScript" referent="15">
            <Properties>
              <string name="Name">PanelAnimations</string>
              <BinaryString name="AttributesSerialize"></BinaryString>
              <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
              <ContentId name="LinkedSource">
                <null>
                </null>
              </ContentId>
              <bool name="DefinesCapabilities">false</bool>
              <BinaryString name="SerializedOverrides">
              </BinaryString>
              <string name="Source"><![CDATA[-- PanelAnimations loader
-- Each child ModuleScript is an animation: its name maps to the Animation
-- attribute on UI_Panel, and it must return function(menu: GuiObject).
local Animations = {}

for _, child in ipairs(script:GetChildren()) do
	if child:IsA("ModuleScript") then
		local ok, fn = pcall(require, child)
		if ok and type(fn) == "function" then
			Animations[child.Name] = fn
		end
	end
end

return Animations
]]></string>
              <int64 name="SourceAssetId">-1</int64>
              <BinaryString name="Tags"></BinaryString>
            </Properties>
            <Item class="ModuleScript" referent="16">
              <Properties>
                <string name="Name">Bounce</string>
                <BinaryString name="AttributesSerialize"></BinaryString>
                <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
                <ContentId name="LinkedSource">
                  <null>
                  </null>
                </ContentId>
                <bool name="DefinesCapabilities">false</bool>
                <BinaryString name="SerializedOverrides">
                </BinaryString>
                <string name="Source"><![CDATA[-- Bounce animation: drops from slightly above with Back easing and a small random rotation.
--
-- Works on ANY GuiObject. It used to bail out on anything that was not a
-- CanvasGroup - a guard with nothing behind it, since the animation only ever
-- touches Position and Rotation and never GroupTransparency. Two of General
-- UI Kit's thirteen shipped panels are plain Frames (TradeRequest, Spin), and
-- both ask for Bounce, so both opened with a hard cut and no animation at all
-- while every CanvasGroup panel bounced. Found 2026-08-21 via the plugin's
-- new MENUS Preview button; the guard is removed rather than the panels
-- converted, because a CanvasGroup rasterizes its descendants into a canvas
-- its own size and both panels have content that deliberately overhangs
-- (TradeRequest's Icon at x=-0.07/y=-0.22, Spin's CoinsEffect spawn line at
-- y=-0.1) - converting them would have cropped it.
local TweenService = game:GetService("TweenService")

local DURATION   = 0.5
local OFFSET_Y   = 0.02  -- how far above the resting position the panel starts

return function(menu: GuiObject)
	local origPos = menu.Position
	menu.Position = UDim2.new(origPos.X.Scale, origPos.X.Offset, origPos.Y.Scale - OFFSET_Y, origPos.Y.Offset)
	menu.Rotation = math.random(2) == 1 and 1 or -1

	menu:TweenPosition(
		origPos,
		Enum.EasingDirection.Out, Enum.EasingStyle.Back, DURATION, true
	)
	TweenService:Create(
		menu,
		TweenInfo.new(DURATION, Enum.EasingStyle.Quint, Enum.EasingDirection.Out),
		{ Rotation = 0 }
	):Play()
end
]]></string>
                <int64 name="SourceAssetId">-1</int64>
                <BinaryString name="Tags"></BinaryString>
              </Properties>
            </Item>
          </Item>
          <Item class="ModuleScript" referent="17">
            <Properties>
              <string name="Name">Pieces</string>
              <BinaryString name="AttributesSerialize"></BinaryString>
              <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
              <ContentId name="LinkedSource">
                <null>
                </null>
              </ContentId>
              <bool name="DefinesCapabilities">false</bool>
              <BinaryString name="SerializedOverrides">
              </BinaryString>
              <string name="Source"><![CDATA[-- Pieces.lua - interface pieces (RBLX Essentials 3.0).
--
-- A pack declares the pieces of interface it drives in `Packs/<PackId>/Pieces`
-- (see Contracts.lua). Each piece has one DEFAULT copy, shipped by the pack
-- under StarterGui.RBLXEssentials.<PackId>, and may have one OVERRIDE, owned
-- by the game, under StarterGui.RBLXEssentials.Interface (a theme's piece or
-- a fork). Piece roots carry RBLXE_Piece = "<PackId>.<Name>".
--
-- The server decides once, at boot (`plan`), which copy each piece uses: a
-- valid override, else the default. Each decision is published as the
-- relative path of the chosen instance under the RBLXEssentials root (for
-- example "Interface/ThemeGui/Panel" or "Demo/DemoGui/Panel"), together with
-- the path of every other candidate copy the server saw for that id (the
-- refused ones), reaching every client in the RBLXE_PieceWinners attribute
-- (`encodeDecisions`/`decodeDecisions`). The client (`runtime`) admits only
-- the first instance of a piece whose own path under its nearest
-- RBLXEssentials ancestor matches the winner, and retires the copies that
-- arrive at a refused path or as a second copy at the winner path, so no pack
-- ever holds a copy that is later replaced (the 434 failure). Any other
-- instance carrying RBLXE_Piece (a clone a pack makes at runtime: a toast, a
-- billboard per adornee, a registerPanel copy) is not admitted but is left
-- alone: clones belong to the pack that made them.
--
-- The server makes StarterGui.RBLXEssentials survive character respawns
-- before `plan` runs (UI/RootPersistence.lua), so a piece admitted here
-- stays the same instance for the whole session.
--
-- Pure: every Instance, require function and scheduler comes in as an argument.
local Pieces = {}

Pieces.PIECE_ATTRIBUTE = "RBLXE_Piece"
Pieces.SOURCE_ATTRIBUTE = "RBLXE_Source"
Pieces.WINNERS_ATTRIBUTE = "RBLXE_PieceWinners"
Pieces.LEGACY = "legacy"
Pieces.INTERFACE = "Interface"
Pieces.ROOT = "RBLXEssentials"
-- StarterGui.RBLXEssentials.Interface.Custom holds what the 2.1 to 3.0
-- migration kept without managing it (T.migrate: retired panels and
-- buttons). Nothing there is a candidate for any piece, here or in Tools.
Pieces.CUSTOM = "Custom"
-- How long getPiece waits for PlayerGui.RBLXEssentials (it arrives with the
-- first character) before its own timeout starts counting (RT-4).
Pieces.ROOT_WAIT = 120

local function pieceIdOf(inst)
	local id = inst:GetAttribute(Pieces.PIECE_ATTRIBUTE)
	if type(id) == "string" and id ~= "" then
		return id
	end
	return nil
end

local function sortedKeys(t)
	local keys = {}
	for key in pairs(t) do
		table.insert(keys, key)
	end
	table.sort(keys)
	return keys
end

-- ── declarations ─────────────────────────────────────────────────────

-- collectDeclarations(...) -> (declarations, warnings, failures). `failures`
-- maps the id of a pack whose Pieces module errors or is invalid to the
-- reason its boot fails with (RT-6): its pieces can never be resolved, so
-- the Loader fails it instead of letting it drive an interface nobody sees.
function Pieces.collectDeclarations(packsFolder, requireFn, contracts)
	local declarations, warnings, failures = {}, {}, {}
	for _, packFolder in ipairs(packsFolder:GetChildren()) do
		local module = packFolder:FindFirstChild("Pieces")
		if module ~= nil and module:IsA("ModuleScript") then
			local ok, specs = pcall(requireFn, module)
			if not ok then
				local message = ("%s.Pieces errored: %s"):format(packFolder.Name, tostring(specs))
				table.insert(warnings, message)
				failures[packFolder.Name] = "invalid Pieces: " .. message
			else
				local errors = contracts.validateSpecs(packFolder.Name, specs)
				if #errors > 0 then
					for _, message in ipairs(errors) do
						table.insert(warnings, message)
					end
					failures[packFolder.Name] = "invalid Pieces:\n    " .. table.concat(errors, "\n    ")
				else
					declarations[packFolder.Name] = specs
				end
			end
		end
	end
	table.sort(warnings)
	return declarations, warnings, failures
end

-- ── server plan ──────────────────────────────────────────────────────

-- byId keeps the first copy of each id (sorted by full name); all keeps
-- every copy, so the plan can publish the paths of the ones it refuses.
local function index(list, origin, warnings, all)
	table.sort(list, function(a, b)
		return a:GetFullName() < b:GetFullName()
	end)
	local byId = {}
	for _, inst in ipairs(list) do
		local id = pieceIdOf(inst)
		all[id] = all[id] or {}
		table.insert(all[id], inst)
		if byId[id] ~= nil then
			table.insert(warnings, ("%s: more than one %s piece; using %s"):format(id, origin, byId[id]:GetFullName()))
		else
			byId[id] = inst
		end
	end
	return byId
end

-- The attribute that ties each piece kind to MenuSystem: a Panel is
-- registered under its PanelId, a HudButton opens its TargetPanel. An
-- Interface override must keep the default copy's value, or the button that
-- opens the panel (and every pack that asks for it) would miss it.
local IDENTITY = { Panel = "PanelId", HudButton = "TargetPanel" }

local function nonEmptyText(value)
	if type(value) == "string" and value ~= "" then
		return value
	end
	return nil
end

local function quoted(value)
	if type(value) == "string" then
		return ("%q"):format(value)
	end
	return tostring(value)
end

-- The path of `inst` under `root`, as the names of every node strictly
-- between them, joined by "/" (for example "Interface/ThemeGui/Panel").
local function relativePath(inst, root)
	local segments = {}
	local node = inst
	while node ~= nil and node ~= root do
		table.insert(segments, 1, node.Name)
		node = node.Parent
	end
	return table.concat(segments, "/")
end

-- winners maps each declared piece id to the relative path of the instance
-- chosen to satisfy it; a piece with nothing valid to use has no entry.
-- refused maps every id seen under the root, declared or not, to the paths
-- of its other copies (the default behind a valid override, a rejected
-- override, duplicates, copies of an id no pack declares): the client
-- retires exactly those, and nothing else.
function Pieces.plan(args)
	local contracts = args.contracts
	local root = args.root
	local interface = root and root:FindFirstChild(Pieces.INTERFACE)
	local custom = interface and interface:FindFirstChild(Pieces.CUSTOM)
	local defaultList, overrideList = {}, {}
	if root ~= nil then
		for _, inst in ipairs(root:GetDescendants()) do
			if pieceIdOf(inst) ~= nil and not (custom ~= nil and inst:IsDescendantOf(custom)) then
				if interface ~= nil and inst:IsDescendantOf(interface) then
					table.insert(overrideList, inst)
				else
					table.insert(defaultList, inst)
				end
			end
		end
	end

	local warnings, copies = {}, {}
	local defaults = index(defaultList, "default", warnings, copies)
	local overrides = index(overrideList, "Interface", warnings, copies)
	local declared, order, chosen, fromInterface, kinds = {}, {}, {}, {}, {}
	local detail = {}

	-- Stand-ins (Kernel 3.0.1): a piece with standsInFor = "<PackId>.<Name>"
	-- steps aside when that owner piece is declared. It gets no candidate at
	-- all, so its default and every Interface copy land in `refused` below.
	-- An owner of another kind keeps its piece and the stand-in is refused
	-- with a warning. A HudButton that opens a replaced panel is replaced
	-- with it ("a stand-in's opens follows its panel").
	local replaced = {}
	for _, packId in ipairs(sortedKeys(args.declarations)) do
		local specs = args.declarations[packId]
		for _, name in ipairs(sortedKeys(specs)) do
			local spec = specs[name]
			local ownerId = spec.standsInFor
			if type(ownerId) == "string" then
				local ownerPack, ownerName = contracts.splitId(ownerId)
				local ownerSpecs = ownerPack ~= nil and ownerPack ~= packId and args.declarations[ownerPack] or nil
				local owner = ownerSpecs ~= nil and ownerSpecs[ownerName] or nil
				if type(owner) == "table" then
					local id = packId .. "." .. name
					replaced[id] = true
					if owner.kind == spec.kind then
						table.insert(detail, ("%s stands in for %s: replaced (owner installed)"):format(id, ownerId))
					else
						table.insert(warnings, ("%s stands in for %s, a %s: a %s cannot stand in for it, refused")
							:format(id, ownerId, tostring(owner.kind), tostring(spec.kind)))
					end
				end
			end
		end
	end
	local followed = true
	while followed do
		followed = false
		for _, packId in ipairs(sortedKeys(args.declarations)) do
			local specs = args.declarations[packId]
			for _, name in ipairs(sortedKeys(specs)) do
				local id = packId .. "." .. name
				local opens = specs[name].opens
				if not replaced[id] and type(opens) == "string" and replaced[packId .. "." .. opens] then
					replaced[id] = true
					followed = true
					table.insert(detail, ("%s opens %s.%s: refused with its panel"):format(id, packId, opens))
				end
			end
		end
	end

	-- Pass 1: each piece's contract, and an override's identity (PanelId or
	-- TargetPanel) against its default.
	for _, packId in ipairs(sortedKeys(args.declarations)) do
		local specs = args.declarations[packId]
		for _, name in ipairs(sortedKeys(specs)) do
			local spec = specs[name]
			local id = packId .. "." .. name
			declared[id] = { packId = packId, spec = spec }
			if replaced[id] then
				continue
			end
			kinds[id] = spec.kind
			table.insert(order, id)
			local default = defaults[id]
			if default ~= nil then
				local problems = contracts.check(default, spec)
				if #problems > 0 then
					table.insert(warnings, ("%s: default piece does not satisfy its contract: %s"):format(id, contracts.formatProblems(problems)))
				end
			end
			chosen[id] = default
			local override = overrides[id]
			if override ~= nil then
				local reason = nil
				local problems = contracts.check(override, spec)
				if #problems > 0 then
					reason = contracts.formatProblems(problems)
				else
					local attribute = IDENTITY[spec.kind]
					local want = attribute ~= nil and default ~= nil and nonEmptyText(default:GetAttribute(attribute)) or nil
					local got = attribute ~= nil and override:GetAttribute(attribute) or nil
					if want ~= nil and got ~= want then
						reason = ("%s is %s, the default's is %s"):format(attribute, quoted(got), quoted(want))
					end
				end
				if reason == nil then
					chosen[id] = override
					fromInterface[id] = true
				else
					table.insert(warnings, ("%s: Interface piece rejected, using the default: %s"):format(id, reason))
				end
			end
		end
	end

	-- Pass 2: `opens`. The chosen HudButton must target the PanelId of the
	-- chosen copy of the panel it opens, or it would open nothing.
	for _, id in ipairs(order) do
		local entry = declared[id]
		local opens = entry.spec.opens
		local panel = opens ~= nil and chosen[entry.packId .. "." .. opens] or nil
		if panel ~= nil and chosen[id] ~= nil then
			local panelId = panel:GetAttribute("PanelId")
			local function mismatch(button)
				local target = button:GetAttribute("TargetPanel")
				if target ~= panelId then
					return ("TargetPanel is %s but %s.%s has PanelId %s"):format(quoted(target), entry.packId, opens, quoted(panelId))
				end
				return nil
			end
			local reason = mismatch(chosen[id])
			if reason ~= nil and fromInterface[id] then
				table.insert(warnings, ("%s: Interface piece rejected, using the default: %s"):format(id, reason))
				fromInterface[id] = nil
				chosen[id] = defaults[id]
				reason = chosen[id] ~= nil and mismatch(chosen[id]) or nil
			end
			if reason ~= nil then
				table.insert(warnings, ("%s: default piece does not satisfy its contract: %s"):format(id, reason))
			end
		end
	end

	local winners, refused = {}, {}
	local total, interfaceCount = #order, 0
	for _, id in ipairs(order) do
		local inst = chosen[id]
		if inst == nil then
			table.insert(warnings, ("%s: no piece available (no valid Interface piece and no default)"):format(id))
		else
			winners[id] = relativePath(inst, root)
			if fromInterface[id] then
				interfaceCount += 1
				table.insert(detail, ("%s <- Interface (%s)"):format(id, tostring(inst:GetAttribute(Pieces.SOURCE_ATTRIBUTE) or "custom")))
			end
		end
	end
	for _, id in ipairs(sortedKeys(copies)) do
		local seen = {}
		for _, inst in ipairs(copies[id]) do
			local path = relativePath(inst, root)
			if inst ~= chosen[id] and path ~= winners[id] and not seen[path] then
				seen[path] = true
				refused[id] = refused[id] or {}
				table.insert(refused[id], path)
			end
		end
		if refused[id] ~= nil then
			table.sort(refused[id])
		end
	end
	for _, id in ipairs(sortedKeys(overrides)) do
		if declared[id] == nil then
			table.insert(warnings, ("%s: Interface piece ignored, no installed pack declares it (%s)"):format(id, overrides[id]:GetFullName()))
		end
	end
	table.sort(warnings)

	local lines = {}
	if total > 0 then
		table.insert(lines, ("%d pieces: %d from Interface, %d default"):format(total, interfaceCount, total - interfaceCount))
		for _, line in ipairs(detail) do
			table.insert(lines, line)
		end
	end
	return { winners = winners, refused = refused, kinds = kinds, lines = lines, warnings = warnings }
end

-- The RBLXE_PieceWinners value: the plan's winners and refused paths as JSON.
-- `jsonEncode` is HttpService:JSONEncode in the Kernel; the server calls this
-- inside the same pcall as the plan, so a value JSON cannot encode (an
-- instance name that is not valid UTF-8, for example) falls back to LEGACY
-- instead of stopping the server script.
function Pieces.encodeDecisions(plan, jsonEncode)
	return jsonEncode({ winners = plan.winners, refused = plan.refused or {}, kinds = plan.kinds or {} })
end

-- The client side of encodeDecisions. Returns { winners, refused }, or nil
-- when the server sent no usable decision (missing, LEGACY, not JSON, or not
-- the { winners, refused } shape): every piece is then adopted as in 2.x.
-- An empty winners map encodes as "[]" and decodes to an empty table.
function Pieces.decodeDecisions(encoded, jsonDecode)
	if type(encoded) ~= "string" or encoded == Pieces.LEGACY then
		return nil
	end
	local ok, decoded = pcall(jsonDecode, encoded)
	if not ok or type(decoded) ~= "table" or type(decoded.winners) ~= "table" then
		return nil
	end
	local refused = decoded.refused
	if type(refused) ~= "table" then
		refused = {}
	end
	local kinds = decoded.kinds
	if type(kinds) ~= "table" then
		kinds = {}
	end
	return { winners = decoded.winners, refused = refused, kinds = kinds }
end

-- strays(starterGui, root) -> warnings (M-c): every RBLXE_Piece instance in
-- StarterGui outside StarterGui.RBLXEssentials. Nothing resolves or retires
-- it, so it shows on every client as it is. The server says so once at boot
-- (Kernel.server.lua) and T.validate says it in Studio. A clone a pack makes
-- at runtime never reaches StarterGui, so it never warns.
function Pieces.strays(starterGui, root)
	local warnings = {}
	if starterGui == nil then
		return warnings
	end
	for _, inst in ipairs(starterGui:GetDescendants()) do
		local id = pieceIdOf(inst)
		if id ~= nil and not (root ~= nil and (inst == root or inst:IsDescendantOf(root))) then
			table.insert(warnings, ("%s: %s is outside StarterGui.%s: the Kernel never uses it, and it shows on screen as it is")
				:format(id, inst:GetFullName(), Pieces.ROOT))
		end
	end
	table.sort(warnings)
	return warnings
end

-- ── client runtime ───────────────────────────────────────────────────

function Pieces.originOf(inst)
	local node = inst.Parent
	while node ~= nil do
		if node.Name == Pieces.INTERFACE and node.Parent ~= nil and node.Parent.Name == Pieces.ROOT then
			return "Interface"
		end
		node = node.Parent
	end
	return "Default"
end

-- The path of `inst` under its nearest ancestor named Pieces.ROOT
-- ("RBLXEssentials"), matching what the server published in `plan`'s
-- winners. Nil when `inst` has no such ancestor at all.
local function pathUnderRoot(inst)
	local segments = {}
	local node = inst
	while node ~= nil do
		local parent = node.Parent
		if parent == nil then
			return nil
		end
		table.insert(segments, 1, node.Name)
		if parent.Name == Pieces.ROOT then
			return table.concat(segments, "/")
		end
		node = parent
	end
	return nil
end

local Runtime = {}
Runtime.__index = Runtime

-- decisions is what decodeDecisions returns (or a plan result): winners maps
-- a piece id to the relative path (relativePath/pathUnderRoot) of the
-- instance chosen to satisfy it, refused maps an id to the paths of the
-- copies the server refused. decisions == nil means the server sent no
-- decision at all: 2.x adoption for every piece.
function Pieces.runtime(decisions, opts)
	opts = opts or {}
	local self = setmetatable({}, Runtime)
	self._winners = decisions and decisions.winners or nil
	self._refused = {}
	for id, paths in pairs(decisions and decisions.refused or {}) do
		if type(paths) == "table" then
			local set = {}
			for _, path in ipairs(paths) do
				set[path] = true
			end
			self._refused[id] = set
		end
	end
	self._admitted = {}
	self._decided = setmetatable({}, { __mode = "k" })
	self._kinds = decisions and decisions.kinds or {}
	self._noDecision = {}
	-- RT-4: getPiece counts its timeout only once PlayerGui.RBLXEssentials
	-- exists. attach() records the PlayerGui; without one (a spec) the root
	-- counts as arrived.
	self._playerGui = opts.playerGui
	self._rootWait = opts.rootWait or Pieces.ROOT_WAIT
	self._wait = opts.wait or function(seconds)
		return seconds
	end
	self._warn = opts.warn or function() end
	self._defer = opts.defer or function(fn)
		fn()
	end
	return self
end

-- The declared kind of a piece id ("Panel", "HudButton", ...) as the
-- server's decision carries it, or nil. MenuSystem:getPiece warns from it
-- about a menu piece it never adopted, even one that lacks its tag (M-d).
function Runtime:kindOf(pieceId)
	local kind = self._kinds[pieceId]
	return if type(kind) == "string" then kind else nil
end

-- One warning per id the server's decision does not contain (T-G, M5): no
-- installed pack declares it, or no copy of it was usable.
function Runtime:_warnNoDecision(id, where)
	if self._noDecision[id] then
		return
	end
	self._noDecision[id] = true
	self._warn(("[RBLXE] UI: piece %s has no decision from the server (no installed pack declares it, or no copy of it is usable)%s")
		:format(id, if where ~= nil then ("; %s is not used"):format(where) else ""))
end

function Runtime:_retire(inst)
	pcall(function()
		if inst:IsA("GuiObject") then
			inst.Visible = false
		elseif inst:IsA("LayerCollector") then
			inst.Enabled = false
		end
	end)
	-- Deferred: the clone may still be mid-parenting when it is refused.
	self._defer(function()
		inst:Destroy()
	end)
end

-- Decides once per instance (the answer is cached, so a later move into the
-- MenuSystem shell cannot change it) and returns whether it may be used.
-- Only copies competing for the id are retired: one at a path the server
-- refused, or a second one at the winner path. Any other instance carrying
-- RBLXE_Piece (a clone a pack made) is refused but left alone.
function Runtime:admit(inst)
	local cached = self._decided[inst]
	if cached ~= nil then
		return cached
	end
	local id = pieceIdOf(inst)
	local admitted = true
	if id ~= nil then
		if self._winners == nil then
			self._admitted[id] = self._admitted[id] or inst
		else
			local winnerPath = self._winners[id]
			local path = pathUnderRoot(inst)
			if winnerPath == nil then
				admitted = false
				self:_warnNoDecision(id, inst:GetFullName())
				local refused = self._refused[id]
				if path ~= nil and refused ~= nil and refused[path] then
					self:_retire(inst)
				end
			elseif path ~= nil and path == winnerPath then
				local incumbent = self._admitted[id]
				if incumbent == nil then
					self._admitted[id] = inst
				else
					admitted = false
					if incumbent.Parent == nil then
						self._warn(("[RBLXE] UI: piece %s was destroyed; its new copy %s is refused (pieces are admitted once per session)")
							:format(id, inst:GetFullName()))
					else
						-- M-b: never removed without a word.
						self._warn(("[RBLXE] UI: piece %s: a second copy arrived at %s and was removed; the first copy keeps the piece")
							:format(id, inst:GetFullName()))
					end
					self:_retire(inst)
				end
			else
				admitted = false
				local refused = self._refused[id]
				if path ~= nil and refused ~= nil and refused[path] then
					self:_retire(inst)
				end
			end
		end
	end
	self._decided[inst] = admitted
	return admitted
end

-- Whether `inst` is the copy the server's decision chose for its id (always
-- false in 2.x legacy mode, where nothing was decided). MenuSystem adopts
-- such a piece over any 2.x panel or button with the same key.
function Runtime:chose(inst)
	if self._winners == nil then
		return false
	end
	local id = pieceIdOf(inst)
	return id ~= nil and self._admitted[id] == inst
end

function Runtime:attach(playerGui)
	self._playerGui = playerGui
	local function consider(inst)
		if pieceIdOf(inst) ~= nil then
			self:admit(inst)
		end
	end
	for _, inst in ipairs(playerGui:GetDescendants()) do
		consider(inst)
	end
	return playerGui.DescendantAdded:Connect(consider)
end

-- get(pieceId, timeout?) -> the admitted piece, or nil. An id the server's
-- decision does not contain returns at once (M5): no copy of it will ever be
-- admitted. Otherwise the timeout (default 10 s) counts only while
-- PlayerGui.RBLXEssentials exists (RT-4): the root arrives with the first
-- character, which a slow client or a pack like DatastorePurchaseSystem can
-- delay; waiting for the root is bounded by rootWait on its own.
function Runtime:get(pieceId, timeout)
	if self._winners ~= nil and self._winners[pieceId] == nil then
		self:_warnNoDecision(pieceId, nil)
		return nil
	end
	local limit = timeout or 10
	local elapsed, waitedForRoot = 0, 0
	while true do
		local found = self._admitted[pieceId]
		if found ~= nil and found.Parent ~= nil then
			return found
		end
		local rootHere = self._playerGui == nil or self._playerGui:FindFirstChild(Pieces.ROOT) ~= nil
		if rootHere and elapsed >= limit then
			break
		end
		if not rootHere and waitedForRoot >= self._rootWait then
			self._warn(("[RBLXE] UI: piece %s: PlayerGui.%s did not arrive within %ss (the player has no character yet?)")
				:format(pieceId, Pieces.ROOT, tostring(self._rootWait)))
			return nil
		end
		local step = self._wait(0.1)
		step = if type(step) == "number" and step > 0 then step else 0.1
		if rootHere then
			elapsed += step
		else
			waitedForRoot += step
		end
	end
	self._warn(("[RBLXE] UI: piece %s did not appear within %ss"):format(pieceId, tostring(limit)))
	return nil
end

return Pieces
]]></string>
              <int64 name="SourceAssetId">-1</int64>
              <BinaryString name="Tags"></BinaryString>
            </Properties>
          </Item>
          <Item class="ModuleScript" referent="18">
            <Properties>
              <string name="Name">RootPersistence</string>
              <BinaryString name="AttributesSerialize"></BinaryString>
              <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
              <ContentId name="LinkedSource">
                <null>
                </null>
              </ContentId>
              <bool name="DefinesCapabilities">false</bool>
              <BinaryString name="SerializedOverrides">
              </BinaryString>
              <string name="Source"><![CDATA[-- RootPersistence.lua - keeps StarterGui.RBLXEssentials alive across
-- character respawns (Kernel 2.1.1).
--
-- WHY: Roblox resets PlayerGui on every character spawn. Each StarterGui
-- child is destroyed and cloned again unless it is a top-level
-- LayerCollector with ResetOnSpawn = false. The installer creates
-- StarterGui.RBLXEssentials as a Folder, which has no ResetOnSpawn, so the
-- whole tree was destroyed on the server and cloned again on every respawn
-- (the ResetOnSpawn = false of each pack ScreenGui nested inside is
-- ignored: only top-level LayerCollectors count). That server-side Destroy
-- replicates to every instance the client had moved out of the tree,
-- including every panel and HUD button MenuSystem adopted into
-- RBLXEssentialsGUI, so after the first death the menus and the HUD buttons
-- were gone. Diagnosed natively on 2026-09-23.
--
-- The server calls persist once at boot, before packs load, to turn a
-- non-ScreenGui root into a ScreenGui with ResetOnSpawn = false. The root
-- instance itself is replaced (the old Folder is destroyed), but the new one
-- keeps its name, attributes, tags and children, so every
-- PlayerGui.RBLXEssentials.<PackId>.<Gui> path stays the same: look the root
-- up by name after boot. The holder is neutral (no insets, no safe-area
-- clipping), so it adds no layout of its own, and the pack ScreenGuis nested
-- inside keep rendering as independent layers.
--
-- Pure: no `game` access. `create` is Instance.new and `enums` is Enum,
-- both injected so Lune specs can pass @lune/roblox's versions.
local RootPersistence = {}

RootPersistence.ROOT = "RBLXEssentials"

-- The nearest ancestor of `inst` (not `inst` itself) that is a GuiObject or
-- a LayerCollector, or nil when there is none.
local function nearestGuiAncestor(inst)
	local node = inst.Parent
	while node ~= nil do
		if node:IsA("GuiObject") or node:IsA("LayerCollector") then
			return node
		end
		node = node.Parent
	end
	return nil
end

-- Reports every instance under the root whose behavior changes now that the
-- root persists instead of resetting on spawn:
--   * client code (a LocalScript, or a Script with RunContext Client) now
--     runs once per session instead of on every respawn;
--   * a nested LayerCollector that asked to reset on spawn no longer does;
--   * only when the old root was not a LayerCollector (a Folder, which
--     renders nothing): a GuiObject with no GuiObject or LayerCollector
--     between it and the root now renders. Only the top of each such subtree
--     is reported, so a Frame > Folder > Frame chain gives one warning. When
--     the old root was already a ScreenGui, those objects were already
--     rendering, so they are not reported.
local function audit(root, enums, warnings, rootWasLayerCollector)
	for _, inst in ipairs(root:GetDescendants()) do
		if inst:IsA("LocalScript") or (inst:IsA("Script") and inst.RunContext == enums.RunContext.Client) then
			table.insert(warnings, ("%s: now runs once per session instead of on every respawn"):format(inst:GetFullName()))
		elseif inst:IsA("LayerCollector") then
			if inst.ResetOnSpawn == true then
				table.insert(warnings, ("%s: no longer resets on spawn"):format(inst:GetFullName()))
			end
		elseif inst:IsA("GuiObject") then
			if not rootWasLayerCollector and nearestGuiAncestor(inst) == root then
				table.insert(warnings, ("%s: now renders (it has no ScreenGui of its own)"):format(inst:GetFullName()))
			end
		end
	end
end

-- Makes StarterGui.RBLXEssentials survive respawn. Returns (root, warnings);
-- root is nil only when StarterGui has no RBLXEssentials child.
function RootPersistence.persist(starterGui, create, enums)
	local warnings = {}
	local root = starterGui:FindFirstChild(RootPersistence.ROOT)

	local matches = 0
	for _, child in ipairs(starterGui:GetChildren()) do
		if child.Name == RootPersistence.ROOT then
			matches += 1
		end
	end
	if matches > 1 and root ~= nil then
		table.insert(warnings, ("more than one StarterGui child is named %s; only %s was made persistent")
			:format(RootPersistence.ROOT, root:GetFullName()))
	end

	if root == nil then
		return nil, warnings
	end

	if root:IsA("ScreenGui") then
		-- Already converted, or the root may already be a ScreenGui if the
		-- game replaced it by hand: only make sure it survives respawn, and
		-- audit only when that is a change. Its GuiObjects already rendered.
		if root.ResetOnSpawn then
			root.ResetOnSpawn = false
			audit(root, enums, warnings, true)
		end
		table.sort(warnings)
		return root, warnings
	end

	local holder = create("ScreenGui")
	holder.Name = RootPersistence.ROOT
	holder.ResetOnSpawn = false
	holder.ScreenInsets = enums.ScreenInsets.None
	holder.ClipToDeviceSafeArea = false
	holder.SafeAreaCompatibility = enums.SafeAreaCompatibility.None
	for key, value in pairs(root:GetAttributes()) do
		holder:SetAttribute(key, value)
	end
	for _, tag in ipairs(root:GetTags()) do
		holder:AddTag(tag)
	end

	local moved = {}
	local moveOk, moveErr = pcall(function()
		for _, child in ipairs(root:GetChildren()) do
			child.Parent = holder
			table.insert(moved, child)
		end
	end)
	if not moveOk then
		-- Put back whatever already moved, so nothing is left orphaned.
		local stranded = {}
		for _, child in ipairs(moved) do
			local backOk = pcall(function()
				child.Parent = root
			end)
			if not backOk then
				table.insert(stranded, child)
			end
		end
		if #stranded == 0 then
			holder:Destroy()
		else
			-- Destroying the holder would destroy these children with it.
			for _, child in ipairs(stranded) do
				table.insert(warnings, ("%s could not be returned to %s"):format(child.Name, RootPersistence.ROOT))
			end
		end
		table.insert(warnings, ("%s: root not made persistent; menus, HUD buttons and pack interfaces may stop working after a respawn (%s)")
			:format(RootPersistence.ROOT, tostring(moveErr)))
		table.sort(warnings)
		return root, warnings
	end

	-- Parented before the Folder is destroyed, with no yield in between, so
	-- the moved children always have a root.
	holder.Parent = starterGui
	root:Destroy()

	audit(holder, enums, warnings, false)
	table.sort(warnings)
	return holder, warnings
end

return RootPersistence
]]></string>
              <int64 name="SourceAssetId">-1</int64>
              <BinaryString name="Tags"></BinaryString>
            </Properties>
          </Item>
          <Item class="ModuleScript" referent="19">
            <Properties>
              <string name="Name">Shell</string>
              <BinaryString name="AttributesSerialize"></BinaryString>
              <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
              <ContentId name="LinkedSource">
                <null>
                </null>
              </ContentId>
              <bool name="DefinesCapabilities">false</bool>
              <BinaryString name="SerializedOverrides">
              </BinaryString>
              <string name="Source"><![CDATA[-- Builds the kernel's ScreenGui shell programmatically - the kernel ships no
-- Studio-only GUI assets. Packs contribute their own panels via registerPanel.
return function(playerGui)
	local gui = Instance.new("ScreenGui")
	gui.Name = "RBLXEssentialsGUI"
	gui.ResetOnSpawn = false
	gui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
	gui.DisplayOrder = 5
	-- Parity with v1 MenusGUI: ignore topbar inset to prevent coordinate
	-- shift of scale-positioned content and mouse-follow tooltip.
	gui.IgnoreGuiInset = true

	local menus = Instance.new("Frame")
	menus.Name = "Menus"
	menus.BackgroundTransparency = 1
	menus.Size = UDim2.fromScale(1, 1)
	-- Panels draw ABOVE the HUD. This frame and the interactables layer are
	-- effective render siblings under the ScreenGui - `Interactables` below
	-- is a Folder, not a GuiObject, so it creates no stacking context of its
	-- own and `Container` is compared straight against this frame. Both used
	-- to sit at the default ZIndex 1: a tie, broken only by the order the
	-- children happen to be parented in, which put the HUD on top. Any
	-- full-screen panel then rendered UNDER the nav icons at the left HUD
	-- spot - reported against Roulette System's Spin, whose panel dims the
	-- whole screen at BackgroundTransparency 0.5 and is the first v2 panel
	-- big enough to reach that strip at all. Explicit ranks remove the tie
	-- instead of relying on parenting order. Anything a pack clones in as a
	-- sibling group (GUK's Effects/InviteFriends) keeps the default 1 and so
	-- also stays below panels; the Tooltip (50) still wins over everything.
	menus.ZIndex = 10
	menus.Parent = gui

	-- Folder, not Frame: matches v1 (EC's designed MenusGUI.Interactables) -
	-- layout-transparent, so GuiObject children (Container, and any
	-- pack-cloned sibling groups) size/position relative to the ScreenGui
	-- instead of compressing into an invented strip. Folders have no
	-- Size/Position/AnchorPoint to set.
	local interactables = Instance.new("Folder")
	interactables.Name = "Interactables"
	interactables.Parent = gui

	-- Geometry below matches GUK v2's designed Container (EC lineage;
	-- General UI Kit v2/src/StarterGui/GeneralUIKit/GeneralUIKitGUI.rbxm ->
	-- Interactables.Container pre-M1-unwrap, verified directly by a Lune
	-- probe against `git show 8b0e864:"General UI Kit v2/src/
	-- ReplicatedStorage/GeneralUIKit/Assets/Interactables.rbxm"` - see
	-- task-M1-report.md's I2 fix section for the probe output) as the
	-- kernel default for ALL packs, designed or not: since Task M1, GUK
	-- ships its 10 nav buttons directly adopted into THIS Container (no
	-- pack ever overwrites this geometry anymore - the old "GUK v2
	-- overwrites this after cloning its layout" comment this replaced is
	-- stale post-M1).
	local container = Instance.new("Frame")
	container.Name = "Container"
	container.BackgroundTransparency = 1
	container.AnchorPoint = Vector2.new(0, 0.5)
	container.Position = UDim2.fromScale(0.01, 0.5)
	container.Size = UDim2.fromScale(0.2, 0.38)
	-- Explicit even though it equals the Frame default: this is the low half
	-- of the rank documented on `menus.ZIndex` above, and a silent default is
	-- exactly what produced the tie in the first place.
	container.ZIndex = 1
	container.Parent = interactables

	local grid = Instance.new("UIGridLayout")
	grid.CellSize = UDim2.fromScale(0.24, 0.24)
	grid.CellPadding = UDim2.fromScale(0.03, 0.01)
	grid.FillDirection = Enum.FillDirection.Vertical
	grid.HorizontalAlignment = Enum.HorizontalAlignment.Left
	grid.SortOrder = Enum.SortOrder.LayoutOrder
	grid.Parent = container

	-- The designed Container parents this directly to the FRAME, not to the
	-- UIGridLayout (a UIAspectRatioConstraint parented to a UIGridLayout has
	-- no effect on anything - verified via the probe above: it does nothing
	-- there, which is why this was a real, not merely cosmetic, v1-parity
	-- bug prior to this fix).
	local aspect = Instance.new("UIAspectRatioConstraint")
	aspect.AspectRatio = 0.9341463446617126
	aspect.AspectType = Enum.AspectType.FitWithinMaxSize
	aspect.DominantAxis = Enum.DominantAxis.Height
	aspect.Parent = container

	local tooltip = Instance.new("Frame")
	tooltip.Name = "Tooltip"
	tooltip:SetAttribute("TooltipType", "Button")
	tooltip.Visible = false
	tooltip.BackgroundColor3 = Color3.fromRGB(30, 30, 30)
	tooltip.BackgroundTransparency = 0.15
	tooltip.BorderSizePixel = 0
	tooltip.Size = UDim2.fromOffset(140, 34)
	tooltip.ZIndex = 50
	tooltip.Parent = gui

	local corner = Instance.new("UICorner")
	corner.CornerRadius = UDim.new(0, 6)
	corner.Parent = tooltip

	local label = Instance.new("TextLabel")
	label.Name = "ButtonName"
	label.BackgroundTransparency = 1
	label.Size = UDim2.fromScale(1, 1)
	label.TextColor3 = Color3.fromRGB(255, 255, 255)
	label.TextScaled = true
	label.Font = Enum.Font.GothamBold
	label.Text = ""
	label.ZIndex = 51
	label.Parent = tooltip

	gui.Parent = playerGui
	return gui
end
]]></string>
              <int64 name="SourceAssetId">-1</int64>
              <BinaryString name="Tags"></BinaryString>
            </Properties>
          </Item>
        </Item>
        <Item class="ModuleScript" referent="20">
          <Properties>
            <string name="Name">Version</string>
            <BinaryString name="AttributesSerialize"></BinaryString>
            <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
            <ContentId name="LinkedSource">
              <null>
              </null>
            </ContentId>
            <bool name="DefinesCapabilities">false</bool>
            <BinaryString name="SerializedOverrides">
            </BinaryString>
            <string name="Source"><![CDATA[-- Kernel semver. Single source of truth - the server stamps it as the
-- RBLXE_KernelVersion attribute on ReplicatedStorage.RBLXEssentials at boot,
-- and the Dev Plugin's ExporterV2 reads it through
-- `PackFormat.parseKernelVersion` to stamp the same attribute on every
-- exported RBLXE_Pack container.
--
-- BUMP THIS WITH EVERY KERNEL CHANGE. The kernel unit is shipped inside each
-- pack, and `Installer.decideKernel` replaces an installed kernel ONLY when
-- the incoming version is strictly greater - an equal version is
-- "kept-existing". So a kernel fix that does not raise this number reaches
-- nobody: reinstalling the pack silently keeps the old modules. Learned the
-- hard way on 2026-08-24, when the panels-above-HUD and UI_Blocker fixes
-- both looked like they had not worked after a reinstall (user: "he
-- reinstalado el roulette system en el place de prueba y nada, se ve por
-- detrás el fondo aún").
--
-- 2.0.1 - panels draw above the HUD (Shell ZIndex ranks); UI_Blocker tag
--         skips wiring for a modal's click-eating sheet.
-- 2.0.2 - MenuSystem:getInteractable(panelName): the HUD button a pack's
--         notification badge belongs on. Additive, so an older pack keeps
--         working against this kernel unchanged - but a pack that ASKS for it
--         needs this version present, or it falls back to scanning.
-- 2.1.0 - ctx.kernel.Notify: the notification facade, shaped like SFX. A
--         no-op until Notifications System v2 registers a backend, so every
--         pack can announce unconditionally without declaring a dependency
--         on an optional pack. MINOR, not patch: this adds API that packs
--         built against it will call, and an older kernel does not have it.
-- 2.1.1 - menus and HUD buttons survive character respawn: the server makes
--         StarterGui.RBLXEssentials a ScreenGui with ResetOnSpawn = false at
--         boot (UI/RootPersistence.lua). Before, every respawn destroyed every
--         panel and HUD button MenuSystem had adopted. PATCH: no API change.
-- 3.0.0 - RBLX Essentials 3.0 foundation (design spec 2026-09-23):
--         game-owned configuration (ctx.config from each pack's Schema and
--         the game's Config modules; Schema.lua, Config.lua), interface piece
--         contracts (Contracts.lua) and piece resolution decided on the server
--         and enforced on clients (UI/Pieces.lua, RBLXE_PieceWinners,
--         MenuSystem pieces gate, getPiece, ctx.getPiece). The respawn fix
--         (StarterGui.RBLXEssentials made a persistent ScreenGui at boot, so
--         the pieces a client adopted survive respawns) is not new in 3.0: it
--         shipped in 2.1.1 (UI/RootPersistence.lua) and 3.0 keeps it. The
--         client now waits for game.Loaded
--         before booting. Final A1 fix wave (still 3.0.0, unreleased): a
--         config load that throws fails only its pack, Schema.check rejects
--         unusable option types, a chosen piece takes its MenuSystem slot
--         over any 2.x copy, contracts check what MenuSystem needs (tags,
--         PanelId, TargetPanel, `opens`), and clients retire only the
--         StarterGui copies the server refused, never a pack's own clones.
--         A2a hardening for Tools (still 3.0.0, unreleased): Schema.check
--         walks whole patterns and checks list, map and optional-group
--         defaults; Contracts.check reports unusable specs and roots as
--         problems; Config.load reports a wrong side, packId, schemaLib or
--         module argument as an error. Callers keep their pcall for the
--         rest (Loader.lua does).
--         A2b (still 3.0.0): Schema.describe rows also carry optional,
--         value, example and constraints, for config templates and Tools.
--         A3 (still 3.0.0): Schema refuses defaults under server-only
--         options (RT-10) and takes check/checkDoc rules; getPiece counts
--         from PlayerGui.RBLXEssentials' arrival and answers an undecided id
--         at once; the piece runtime warns where it was silent (T-G, M-b,
--         M-c, M-d); Interface/Custom is never a candidate; an invalid
--         Pieces declaration fails its pack (RT-6); clients skip the packs
--         the server could not start (RBLXE_ServerReport) and Net.client
--         waits can be bounded (RT-5).
--         B0 (still 3.0.0, then frozen): Schema kinds S.color and S.tree
--         and the pinned flag; ctx.companion, ctx.extension(s) (the game's
--         Extensions zone) and no ctx.settings for a pack with a Schema
--         (a notice for the others); the Layer piece kind.
--         MAJOR: official 2.x packs are replaced together
--         with it at release; during development 2.x packs keep ctx.settings
--         and 2.x adoption.
-- 3.0.1 - piece specs may declare standsInFor = "<PackId>.<Name>": a
--         stand-in copy of a piece another pack owns (the General UI Kit
--         ships its 2.x menus this way). Pieces.plan refuses the stand-in,
--         its default and every Interface copy, when the owner piece is
--         declared, and a HudButton that opens a refused stand-in panel is
--         refused with it; without the owner the stand-in is planned like
--         any piece. An owner of another kind keeps its piece and the
--         stand-in is refused with a warning. PATCH: an older Kernel ignores
--         the field and would choose both copies, so a pack that declares
--         stand-ins needs Kernel >= 3.0.1.
return "3.0.1"
]]></string>
            <int64 name="SourceAssetId">-1</int64>
            <BinaryString name="Tags"></BinaryString>
          </Properties>
        </Item>
      </Item>
      <Item class="Folder" referent="21">
        <Properties>
          <string name="Name">Packs</string>
          <BinaryString name="AttributesSerialize"></BinaryString>
          <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
          <Color3 name="IconTint">
            <R>0</R>
            <G>0</G>
            <B>0</B>
          </Color3>
          <bool name="DefinesCapabilities">false</bool>
          <BinaryString name="SerializedOverrides">
          </BinaryString>
          <int64 name="SourceAssetId">-1</int64>
          <BinaryString name="Tags"></BinaryString>
        </Properties>
        <Item class="Folder" referent="22">
          <Properties>
            <string name="Name">DatastorePurchaseSystem</string>
            <BinaryString name="AttributesSerialize">AQAAAAwAAABSQkxYRV9IYXNoZXMCRAQAACNyYmx4ZS1oYXNoZXMgdjEgZm52MWEzMiBsZgpSZXBsaWNhdGVkU3RvcmFnZS9QYWNrcy9EYXRhc3RvcmVQdXJjaGFzZVN5c3RlbS9NaWdyYXRpb25zL0Zyb21WMgkwMzkxYTNlMQpSZXBsaWNhdGVkU3RvcmFnZS9QYWNrcy9EYXRhc3RvcmVQdXJjaGFzZVN5c3RlbS9NaWdyYXRpb25zL0tlZXAJMTFmYTJkZGUKUmVwbGljYXRlZFN0b3JhZ2UvUGFja3MvRGF0YXN0b3JlUHVyY2hhc2VTeXN0ZW0vUGFja2FnZXMvUHJvbWlzZQk2ZDUzZjgzNgpSZXBsaWNhdGVkU3RvcmFnZS9QYWNrcy9EYXRhc3RvcmVQdXJjaGFzZVN5c3RlbS9TY2hlbWEJMjNhMTE2ZmQKUmVwbGljYXRlZFN0b3JhZ2UvUGFja3MvRGF0YXN0b3JlUHVyY2hhc2VTeXN0ZW0vbWFuaWZlc3QJYTA1Y2QzYmYKU2VydmVyU2NyaXB0U2VydmljZS9QYWNrcy9EYXRhc3RvcmVQdXJjaGFzZVN5c3RlbQkzYjNjNDdiNwpTZXJ2ZXJTY3JpcHRTZXJ2aWNlL1BhY2tzL0RhdGFzdG9yZVB1cmNoYXNlU3lzdGVtL0FjdGlvbnMvV2lwZVN0dWRpb0RhdGEJMzE1ZDk2ZWEKU2VydmVyU2NyaXB0U2VydmljZS9QYWNrcy9EYXRhc3RvcmVQdXJjaGFzZVN5c3RlbS9Db25maWcJMzJmZDJlOGYKU2VydmVyU2NyaXB0U2VydmljZS9QYWNrcy9EYXRhc3RvcmVQdXJjaGFzZVN5c3RlbS9EYXRhUGF0aAk5YTBlMjI4MApTZXJ2ZXJTY3JpcHRTZXJ2aWNlL1BhY2tzL0RhdGFzdG9yZVB1cmNoYXNlU3lzdGVtL0RhdGFTdG9yZU1hbmFnZXIJODdkOTIyNWUKU2VydmVyU2NyaXB0U2VydmljZS9QYWNrcy9EYXRhc3RvcmVQdXJjaGFzZVN5c3RlbS9EZWZhdWx0RGF0YQkxZDFkODIzNApTZXJ2ZXJTY3JpcHRTZXJ2aWNlL1BhY2tzL0RhdGFzdG9yZVB1cmNoYXNlU3lzdGVtL01pZ3JhdGlvbglmZWU4NjZiNQpTZXJ2ZXJTY3JpcHRTZXJ2aWNlL1BhY2tzL0RhdGFzdG9yZVB1cmNoYXNlU3lzdGVtL1B1cmNoYXNlTWFuYWdlcgk3MWM0NmQzNgpTZXJ2ZXJTY3JpcHRTZXJ2aWNlL1BhY2tzL0RhdGFzdG9yZVB1cmNoYXNlU3lzdGVtL1N0dWRpb1NhZmVJZAkwZTE4YjM1NApTZXJ2ZXJTY3JpcHRTZXJ2aWNlL1BhY2tzL0RhdGFzdG9yZVB1cmNoYXNlU3lzdGVtL1ZhbHVlUmVwbGljYXRvcglkYjgxNGVhMA==</BinaryString>
            <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
            <Color3 name="IconTint">
              <R>0</R>
              <G>0</G>
              <B>0</B>
            </Color3>
            <bool name="DefinesCapabilities">false</bool>
            <BinaryString name="SerializedOverrides">
            </BinaryString>
            <int64 name="SourceAssetId">-1</int64>
            <BinaryString name="Tags"></BinaryString>
          </Properties>
          <Item class="Folder" referent="23">
            <Properties>
              <string name="Name">Migrations</string>
              <BinaryString name="AttributesSerialize"></BinaryString>
              <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
              <Color3 name="IconTint">
                <R>0</R>
                <G>0</G>
                <B>0</B>
              </Color3>
              <bool name="DefinesCapabilities">false</bool>
              <BinaryString name="SerializedOverrides">
              </BinaryString>
              <int64 name="SourceAssetId">-1</int64>
              <BinaryString name="Tags"></BinaryString>
            </Properties>
            <Item class="ModuleScript" referent="24">
              <Properties>
                <string name="Name">FromV2</string>
                <BinaryString name="AttributesSerialize"></BinaryString>
                <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
                <ContentId name="LinkedSource">
                  <null>
                  </null>
                </ContentId>
                <bool name="DefinesCapabilities">false</bool>
                <BinaryString name="SerializedOverrides">
                </BinaryString>
                <string name="Source"><![CDATA[-- Migrations/FromV2 - Datastore & Purchase System 2.x Settings to the 3.0
-- configuration (plan B1, rulings 1 to 5). T.migrate reads the installed
-- 2.x Packs/DatastorePurchaseSystem/Settings folder into plain data
-- (plugins/Official/Tools/Core/Tree.lua: node = { name, class, value?,
-- attributes, children, deep? }) and calls FromV2(tree, ctx), ctx = {
-- packId, fromVersion, find }. When the game deleted Settings the tree is
-- an empty Folder: 2.x then ran its defaults, and so does this. Pure: data
-- in, data out; no game, no Instances, no script.
--
-- Returns (config, notes, sources, refusals). config holds every option
-- (a refused one is left out). The store versions and DefaultData are
-- pinned, so T.migrate writes them into the game's module even when they
-- equal the 3.0 defaults; AutoSaveInterval and FreeMode are written only
-- when they differ from them. notes = { { path, message, fix?, action? } }
-- says what 2.x did with each value that is not carried as it stood; a note
-- whose message asks for an action (a restart after publishing, a step the
-- user decides) or names something that is not a setting gives its own
-- fix, which T.migrate reports in place of its generic one (B3b acceptance
-- R1); one whose fix asks to tell the user something (restart every server
-- after publishing, Studio uses the live profiles) is marked action =
-- true, which T.migrate reports as migration-action (B3b acceptance R2);
-- every note about a store version ends its first sentence with the store
-- that stays and gives its own fix, never the generic one, which would
-- offer a value players' data uses as one to change (B3b acceptance R5);
-- sources = {
-- [option] = 2.x instance } names the instance each value came from;
-- refusals = { { path, message } } are the values 2.x could not run, or
-- ran from a place Tools cannot read (migrate-v2-refused: force never
-- lifts it).
--
-- It mirrors 2.x exactly (Config.lua's setting() and DefaultData.lua's
-- convertTree, both unchanged between 2.0.0 and 2.1.0); tests/fromv2.spec
-- runs those modules from both releases on the same trees and compares:
--   * a scalar is what setting(name, default) read: the first match of the
--     recursive FindFirstChild(name, true) over the WHOLE Settings tree
--     (ctx.find replays the engine's own answer: a key under Default Data
--     can shadow a setting, and then that key is what 2.x read), then
--     `.Value`, and never a later match. Nothing found, no Value (an
--     instance that is not a value), a nil Value or false meant 2.x's
--     default. NumberPose and KeyframeMarker have a Value that 2.x read but
--     Tree.read does not keep: refused, never replaced by a default;
--   * a store version: text as written (never trimmed; "" and any length
--     too); a number as the text 2.x joined to "PlayerData_" (tostring).
--     Anything else could not be joined, so every load in that environment
--     failed: refused, never replaced by a default (ruling 1);
--   * AutoSaveInterval: a number as it is; text as Luau's arithmetic read it
--     (tonumber), noted. NaN and minus infinity made every save due after
--     10 seconds, as 0 does: carried as 0, noted. Anything else broke the
--     save schedule of every first join (the player was kicked): refused.
--     Infinity made the schedule erratic, and a module cannot write it:
--     refused too;
--   * FreeMode: HasPass and GrantProduct read it by truthiness, so any value
--     but false or nil was on: carried as true, noted (3.0 applies it only
--     in Studio, and not while Studio uses the live store). An instance
--     with no Value made them throw on every call: carried as false, noted;
--   * DefaultData: convertTree of the first child named "Default Data", of
--     any class: a Folder is a group, a NumberValue, IntValue, StringValue
--     or BoolValue its value, anything else dropped, noted; among children
--     with one name the last one 2.x read wins, the earlier noted. A
--     top-level Meta is dropped, noted (2.x's build ignored it). The groups
--     2.x's build added when missing (Stats with Cash and Wins, Pets,
--     Purchases) stay runtime behaviour, never added here: 3.0's
--     DefaultData.build adds them the same way (ruling 3);
--   * WipeStudioData is the plugin's button, not a setting: noted;
--   * a game still on 2.0.0 (ctx.fromVersion) crosses the session ownership
--     protocol 2.1.0 added: noted, with the restart it needs. What counts
--     is what the published servers run, which Tools cannot see: the
--     Studio == live note (the 2.0.0 shape) says the same for a place
--     updated to 2.1.0 in Studio whose update was never published.
local DEFAULTS = { DataStoreVersion = "v1", StudioDataStoreVersion = "v4", AutoSaveInterval = 90 }
local HOMES = {
	DataStoreVersion = "Datastore Settings",
	StudioDataStoreVersion = "Datastore Settings",
	AutoSaveInterval = "Datastore Settings",
	FreeMode = "Purchase Settings",
}
local KNOWN = {
	["Datastore Settings"] = { DataStoreVersion = true, StudioDataStoreVersion = true, AutoSaveInterval = true, WipeStudioData = true },
	["Purchase Settings"] = { FreeMode = true },
	["Default Data"] = {},
}
-- Every ValueBase whose Value a script can read: `.Value` answers even when
-- the instance also has a child named Value.
local VALUE_BASES = {
	BoolValue = true, BrickColorValue = true, CFrameValue = true, Color3Value = true, DoubleConstrainedValue = true,
	IntConstrainedValue = true, IntValue = true, NumberValue = true, ObjectValue = true, RayValue = true,
	StringValue = true, Vector3Value = true,
}
-- The classes that are not values but have a Value property a script can
-- read (Roblox's reflection database): 2.x read it, Tree.read does not.
local UNREADABLE = { KeyframeMarker = true, NumberPose = true }
-- What convertTree kept.
local DATA_CLASSES = { NumberValue = true, IntValue = true, StringValue = true, BoolValue = true }

local function child(node, name)
	for _, item in ipairs(node and node.children or {}) do
		if item.name == name then
			return item
		end
	end
	return nil
end

-- valueOf(node) -> (kind, value): what 2.x's `inst.Value` gave.
--   "none"        nothing found, or `.Value` errored: 2.x's default;
--   "value"       a value's Value (nil when it had none); Tree.read keeps an
--                 ObjectValue's target as a path, 2.x read the Instance:
--                 "instance" for that;
--   "instance"    an Instance: an ObjectValue's target, or the child named
--                 Value of an instance that is not a value;
--   "unreadable"  the Value property of a class in UNREADABLE.
local function valueOf(node)
	if node == nil then
		return "none", nil
	end
	if node.value ~= nil then
		return if node.class == "ObjectValue" then "instance" else "value", node.value
	end
	if VALUE_BASES[node.class] then
		return "value", nil
	end
	if UNREADABLE[node.class] then
		return "unreadable", nil
	end
	local inner = child(node, "Value")
	if inner ~= nil then
		return "instance", inner
	end
	return "none", nil
end

-- aClass(class) -> the class name with its article (B3b acceptance R3
-- review: a note's first sentence is its line in T.migrate's report, which
-- the agent quotes to the user): "an" before a vowel sound (an IntValue, an
-- ObjectValue), "a" before a consonant sound, a U read as "you" too (a
-- UIListLayout, a UnionOperation, a UserSettings).
local function aClass(class)
	local you = string.find(class, "^U%u") ~= nil or string.find(class, "^Uni") ~= nil or string.find(class, "^Us[eu]") ~= nil
	return (if string.find(class, "^[AEIOU]") ~= nil and not you then "an " else "a ") .. class
end

local function describe(node, kind, value)
	if kind == "instance" then
		return if node.class == "ObjectValue" then "an ObjectValue pointing at an instance" else ("%s with a child named Value"):format(aClass(node.class))
	end
	if kind == "unreadable" then
		return aClass(node.class)
	end
	if type(value) == "string" then
		return ("%s %q"):format(aClass(node.class), value)
	end
	return ("%s %s"):format(aClass(node.class), tostring(value))
end

local UNREADABLE_FIX = "whose Value 2.x read, but T.migrate cannot read it (it reads the Value of value instances only). Replace it with a %s holding the same value"
-- B3b acceptance R3 review: a note's first sentence is its line in
-- T.migrate's report (Migrate.brief), so FreeMode's says there what 3.0
-- changes: 2.x applied it on live servers too, 3.0 only in Studio.
local FREE_MODE_3 = "carried as on, but 3.0 applies FreeMode only in Studio, while 2.x applied it on live servers too"
local FREE_MODE_STORE = "Nor does it work in Studio while Studio uses the live store; a live server checks real ownership and warns at start"
-- The fixes of the notes that ask for an action (B3b acceptance R1).
local RESTART_FIX = "Nothing to do in Studio. Tell the user: after publishing the migrated game, restart every server (Migrate To Latest Update, or Shut Down All Servers) so no 2.0.0 server keeps saving next to 3.0 ones"
local SHARED_STORE_FIX = "Nothing to do now: it is carried as the game ran it. Tell the user that Studio sessions use the live profiles; only if they ask, set another StudioDataStoreVersion in Config.DatastorePurchaseSystem, which gives Studio profiles of their own"
local WIPE_FIX = "Nothing to do: it is not a setting but the 2.x plugin's Wipe Studio Data button, and the config module has nothing in its place. The 3.0 plugin keeps that button: never wipe Studio profiles yourself; only the user does, when they ask"

-- B3b acceptance R5 (migrate-5, stumble 1): a store version's note. Every
-- DataStoreVersion and StudioDataStoreVersion note ends its first sentence,
-- its line in T.migrate's report, with the store that stays (stays), and
-- gives its own fix (storeFix): with the generic one an agent offered the
-- user the live store version as a value to decide, and another value
-- starts every player on an empty profile (Schema.lua). A live server uses
-- PlayerData_<DataStoreVersion>, Studio PlayerData_<StudioDataStoreVersion>
-- (DataStoreManager's storeName, as 2.x's Config).
-- storeOf(version) -> that store's name, in quotes when the text is empty
-- or has a space or a quote, so a space at its end shows.
local function storeOf(version)
	local store = "PlayerData_" .. version
	return if version ~= "" and string.find(version, '[%s"]') == nil then store else ("%q"):format(store)
end
local function stays(name, version)
	return ("so the %s store stays %s"):format(if name == "DataStoreVersion" then "live" else "Studio", storeOf(version))
end
local function storeFix(name, version)
	if name == "DataStoreVersion" then
		return ("Nothing to do: the live store stays %s, where 2.x saved every player's data. In a live game, never change DataStoreVersion: another value starts every player on an empty profile (their saved data stays in the old store)"):format(
			storeOf(version))
	end
	return ("Nothing to do: the Studio store stays %s, where 2.x saved the data of Studio sessions. Changing StudioDataStoreVersion touches Studio data only: Studio sessions start on empty profiles in the new store, and live players keep theirs"):format(
		storeOf(version))
end

return function(tree, ctx)
	local config, notes, sources, refusals = {}, {}, {}, {}
	local rootName = tree and tree.name or "Settings"
	local used = {}
	local function note(path, message, fix, action)
		local entry = { path = path, message = message, fix = fix, action = action }
		table.insert(notes, entry)
		return entry
	end
	local function refuse(path, message)
		table.insert(refusals, { path = path, message = message })
	end
	-- readAtOrBelow(path): 2.x read that instance or one inside it.
	local function readAtOrBelow(path)
		for read in pairs(used) do
			if read == path or string.sub(read, 1, #path + 1) == path .. "." then
				return true
			end
		end
		return false
	end
	local empty = tree == nil or #tree.children == 0
	if empty then
		note(rootName, ("is missing or empty, so 2.x ran its defaults: the live store stays %s and the Studio store %s. DataStoreVersion %q, StudioDataStoreVersion %q and an empty DefaultData (only its built-in groups) are written, since they are pinned; AutoSaveInterval 90 and FreeMode off are the 3.0 defaults"):format(
			storeOf(DEFAULTS.DataStoreVersion), storeOf(DEFAULTS.StudioDataStoreVersion), DEFAULTS.DataStoreVersion, DEFAULTS.StudioDataStoreVersion),
			("Nothing to do: the live store stays %s, where 2.x saved every player's data, and the Studio store %s. In a live game, never change DataStoreVersion: another value starts every player on an empty profile (their saved data stays in the old store)"):format(
				storeOf(DEFAULTS.DataStoreVersion), storeOf(DEFAULTS.StudioDataStoreVersion)))
	end
	if ctx ~= nil and ctx.fromVersion == "2.0.0" then
		note(rootName, "belongs to Datastore & Purchase System 2.0.0, whose servers do not honour the session ownership protocol 2.1.0 added to player profiles (docs/releases/2.1.0.md): after publishing the migrated game, restart every server (Migrate To Latest Update, or Shut Down All Servers) so no 2.0.0 server keeps saving next to 3.0 ones",
			RESTART_FIX, true)
	end

	-- setting(name) -> (kind, value, node, path, shadow): 2.x's setting(name,
	-- default); shadow is the note that 2.x read it somewhere else, if any.
	local function setting(name)
		if tree == nil then
			return "none", nil, nil, nil
		end
		local node, path = ctx.find(tree, name, true)
		if node == nil then
			return "none", nil, nil, nil
		end
		used[path] = true
		local home = ("%s.%s.%s"):format(rootName, HOMES[name], name)
		local shadow = nil
		if path ~= home then
			shadow = note(path, ("is the first %s 2.x's recursive search found in Settings, so 2.x read this one%s"):format(name,
				if child(child(tree, HOMES[name]), name) ~= nil then (" and never %s"):format(home) else ""))
		end
		local kind, value = valueOf(node)
		return kind, value, node, path, shadow
	end

	-- ── the two store versions: the DataStore of every profile (ruling 1) ──
	for _, name in ipairs({ "DataStoreVersion", "StudioDataStoreVersion" }) do
		local kind, value, node, path, shadow = setting(name)
		local default = DEFAULTS[name]
		if kind == "unreadable" then
			refuse(path, ("is %s, " .. UNREADABLE_FIX):format(describe(node, kind, value), "StringValue"))
		elseif kind == "none" or value == nil or value == false then
			config[name] = default
			if node ~= nil then
				note(path, ("is %s, so 2.x used %q: written (pinned), %s"):format(
					if kind == "none" then ("%s with no Value"):format(aClass(node.class)) else describe(node, kind, value), default, stays(name, default)),
					storeFix(name, default))
			elseif not empty then
				note(("%s.%s.%s"):format(rootName, HOMES[name], name), ("is missing, so 2.x used %q: written (pinned), %s"):format(default, stays(name, default)),
					storeFix(name, default))
			end
		elseif kind == "value" and type(value) == "string" then
			config[name] = value
			sources[name] = path
		elseif kind == "value" and type(value) == "number" then
			config[name] = tostring(value)
			sources[name] = path
			note(path, ("is %s: 2.x joined it to \"PlayerData_\" as the text %q, carried as that text, %s"):format(describe(node, kind, value), tostring(value),
				stays(name, config[name])), storeFix(name, config[name]))
		else
			refuse(path, ("is %s, which 2.x could not join to \"PlayerData_\": every load of player data %s failed. Set the store version the game really uses as a StringValue"):format(
				describe(node, kind, value), if name == "DataStoreVersion" then "in a live server" else "in Studio"))
		end
		-- B3b acceptance R5: the note that 2.x read it somewhere else says the
		-- store too, once it is known (a refused value has none).
		if shadow ~= nil and config[name] ~= nil then
			shadow.message ..= ", " .. stays(name, config[name])
			shadow.fix = storeFix(name, config[name])
		end
	end
	if config.DataStoreVersion ~= nil and config.DataStoreVersion == config.StudioDataStoreVersion then
		note(sources.StudioDataStoreVersion or rootName, ("is %q, the same as DataStoreVersion (2.0.0 shipped them equal): Studio sessions load and save the live profiles of PlayerData_%s. Carried as it is; the 3.0 server warns at start and keeps FreeMode off in Studio, and you can set another StudioDataStoreVersion once the migration is done%s"):format(
			config.DataStoreVersion, config.DataStoreVersion,
			-- A game installed on 2.0.0 may have updated its place to 2.1.0
			-- in Studio and never published it: what matters is what its
			-- live servers run (the 2.0.0 note below already says it).
			if ctx ~= nil and ctx.fromVersion == "2.0.0" then ""
				else ". If your published game still runs Datastore & Purchase System 2.0.0 (the 2.1.0 update was never published), restart every server after publishing the migrated game, as for a 2.0.0 game (docs/releases/2.1.0.md)"),
			SHARED_STORE_FIX .. (if ctx ~= nil and ctx.fromVersion == "2.0.0" then ""
				else ". If the published game still runs Datastore & Purchase System 2.0.0, they restart every server after publishing the migrated game"),
			true)
	end

	-- ── AutoSaveInterval (ruling 2) ──
	do
		local kind, value, node, path = setting("AutoSaveInterval")
		if kind == "unreadable" then
			refuse(path, ("is %s, " .. UNREADABLE_FIX):format(describe(node, kind, value), "NumberValue"))
		elseif kind == "none" or value == nil or value == false then
			config.AutoSaveInterval = DEFAULTS.AutoSaveInterval
			if node ~= nil then
				note(path, ("is %s, so 2.x saved every 90 seconds, the 3.0 default"):format(
					if kind == "none" then ("%s with no Value"):format(aClass(node.class)) else describe(node, kind, value)))
			end
		else
			local number = if kind == "value" and type(value) == "number" then value
				elseif kind == "value" and type(value) == "string" then tonumber(value)
				else nil
			if number == nil then
				refuse(path, ("is %s, which 2.x could not use as a number of seconds: scheduling the first save failed, so every player was kicked when they joined. Set a NumberValue"):format(
					describe(node, kind, value)))
			elseif number == math.huge then
				refuse(path, ("is %s, infinite: 2.x's save schedule with it was erratic (some saves never came, others came after 10 seconds), and a 3.0 config module cannot write it. Set a finite number (1e9 never saves on a timer)"):format(
					describe(node, kind, value)))
			elseif number ~= number or number == -math.huge then
				config.AutoSaveInterval = 0
				sources.AutoSaveInterval = path
				note(path, ("is %s: 2.x's arithmetic made every autosave due after 10 seconds (the scheduler's floor), which is what 0 does: carried as 0"):format(
					describe(node, kind, value)))
			else
				config.AutoSaveInterval = number
				sources.AutoSaveInterval = path
				if type(value) == "string" then
					note(path, ("is %s: 2.x's arithmetic read it as the number %s, carried as that number"):format(describe(node, kind, value), tostring(number)))
				end
			end
		end
	end

	-- ── FreeMode: HasPass's and GrantProduct's reading (ruling 4) ──
	do
		local kind, value, node, path = setting("FreeMode")
		if node == nil then
			config.FreeMode = false
		elseif kind == "none" then
			config.FreeMode = false
			note(path, ("is %s with no Value, so 2.x's HasPass, GrantProduct and IsFreeMode failed on every call: carried as off"):format(aClass(node.class)))
		elseif kind ~= "unreadable" and (value == nil or value == false) then
			config.FreeMode = false
			sources.FreeMode = path
		else
			config.FreeMode = true
			sources.FreeMode = path
			if node.class == "BoolValue" then
				note(path, ("is on: %s. %s"):format(FREE_MODE_3, FREE_MODE_STORE))
			else
				note(path, ("is %s: %s. In 2.x, HasPass and GrantProduct counted any value but false as on (IsFreeMode did not). %s"):format(
					describe(node, kind, value), FREE_MODE_3, FREE_MODE_STORE))
			end
		end
	end

	-- ── DefaultData: convertTree (ruling 3) ──
	local function convert(node, path)
		local out, setBy = {}, {}
		for _, item in ipairs(node.children) do
			local at = path .. "." .. item.name
			local value = nil
			if item.class == "Folder" then
				value = convert(item, at)
			elseif DATA_CLASSES[item.class] then
				value = item.value
			else
				note(at, ("is %s, which 2.x's Default Data skipped: not carried over"):format(aClass(item.class)))
			end
			if item.class == "Folder" or DATA_CLASSES[item.class] then
				if setBy[item.name] ~= nil then
					note(setBy[item.name], ("has the same name as the later %s, and 2.x kept the later one: not carried over"):format(at))
				end
				out[item.name] = value
				setBy[item.name] = at
			end
		end
		return out, setBy
	end
	local dataNode = child(tree, "Default Data")
	local dataPath = rootName .. ".Default Data"
	if dataNode == nil then
		config.DefaultData = {}
		if not empty then
			note(dataPath, "is missing, so 2.x used only its built-in groups (Stats with Cash 0 and Wins 0, Pets, Purchases): carried as an empty DefaultData, which 3.0 fills the same way")
		end
	else
		local data, setBy = convert(dataNode, dataPath)
		if data.Meta ~= nil then
			note(setBy.Meta, "is named Meta, the profile's own record, which 2.x's build ignored: not carried over")
			data.Meta = nil
		end
		config.DefaultData = data
		sources.DefaultData = dataPath
	end

	-- ── what 2.x never read as a setting ──
	for _, item in ipairs(tree and tree.children or {}) do
		local at = rootName .. "." .. item.name
		local known = KNOWN[item.name]
		if known == nil then
			if not readAtOrBelow(at) then
				note(at, "is not a Datastore & Purchase System 2.x setting, so 2.x never read it: not carried over")
			end
		elseif item.name ~= "Default Data" then
			for _, inner in ipairs(item.children) do
				local innerAt = at .. "." .. inner.name
				if inner.name == "WipeStudioData" and known.WipeStudioData then
					note(innerAt, "is the plugin's Wipe Studio Data button, not a setting: not carried over. The 3.0 plugin keeps that button, which only the user presses (it refuses when Studio and live share a store)",
					WIPE_FIX)
				elseif not known[inner.name] and not readAtOrBelow(innerAt) then
					note(innerAt, "is not a Datastore & Purchase System 2.x setting, so 2.x never read it: not carried over")
				end
			end
		end
	end
	return config, notes, sources, refusals
end
]]></string>
                <int64 name="SourceAssetId">-1</int64>
                <BinaryString name="Tags"></BinaryString>
              </Properties>
            </Item>
            <Item class="ModuleScript" referent="25">
              <Properties>
                <string name="Name">Keep</string>
                <BinaryString name="AttributesSerialize"></BinaryString>
                <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
                <ContentId name="LinkedSource">
                  <null>
                  </null>
                </ContentId>
                <bool name="DefinesCapabilities">false</bool>
                <BinaryString name="SerializedOverrides">
                </BinaryString>
                <string name="Source"><![CDATA[-- Migrations/Keep - what T.migrate must look at in the 2.x pack's own code
-- (plan B1, ruling 7). Everything below "return {" is generated from the
-- published 2.x releases by tools/v3/keep-gen.luau (plan B0, ruling 11):
-- run it again after a 2.x release, and with --check to compare.
--
-- folders: none. Datastore & Purchase System 2.x had no folder for the
--   game's own modules. A game's profile migration now belongs in
--   ServerScriptService.RBLXEssentials.Extensions.DatastorePurchaseSystem.
--   Migration, which the 3.0 server module looks up first.
-- shipped: every script a 2.x release shipped under the pack's own roots
--   (ReplicatedStorage/Packs/DatastorePurchaseSystem, ServerScriptService/
--   Packs/DatastorePurchaseSystem), keyed "<service key>/<path under that
--   service's RBLXEssentials>", with the SourceHash
--   (plugins/Official/src/Shared/SourceHash.lua) of every published version
--   of it. A game that edited one (a profile migration in Migration, product
--   handlers in the server module or PurchaseManager) makes T.migrate
--   refuse without force (migrate-custom-modified) and name the lines it
--   changed; with force the 3.0 version replaces it and the edited copy
--   stays in the protected migration backup. Move a profile migration into
--   Extensions, and product handlers into a pack of the game's own
--   (registerProfileHandler), before forcing.
--   Config, DefaultData, DataStoreManager and StudioSafeId are never
--   forced, whatever the edit (identity, below). A store version or a
--   default hard-coded into Config.lua or DefaultData.lua goes back into
--   the 2.x Settings, with the shipped script restored, before migrating:
--   FromV2 reads only the Settings, so a 3.0 run on them would hand over
--   the Settings' values (v1 when they say v1) and every player would load
--   another store, or a new player another starting profile. A store name
--   edited in DataStoreManager, which builds it (PlayerData_<version>), or
--   a key edited in StudioSafeId, whose GetSafeKey builds it
--   (tostring(GetSafeUserId(player)); DataStoreManager only calls it), has
--   no 3.0 option at all: such a game stays on 2.x for now.
-- identity: those four scripts (plan B3b, B3a's Keep.identity): an edited
--   or missing one is migrate-identity-modified, which force never lifts.
--   For an edited one, its fix puts the published script back (a value the
--   2.x Settings hold moved there first) only once the user agrees, or asks
--   the user; for a missing one, it puts back the game's version, or the
--   published one when the game had none.
-- lines: for each published SourceHash, SourceHash.fnv1a32 of each line of
--   that source, trimmed, blank lines left out, in order. T.migrate names
--   the lines a game changed with them.
return {
	folders = {},
	identity = {
		"ServerScriptService/Packs/DatastorePurchaseSystem/Config",
		"ServerScriptService/Packs/DatastorePurchaseSystem/DataStoreManager",
		"ServerScriptService/Packs/DatastorePurchaseSystem/DefaultData",
		"ServerScriptService/Packs/DatastorePurchaseSystem/StudioSafeId",
	},
	shipped = {
		-- 2.0.0, 2.1.0
		["ReplicatedStorage/Packs/DatastorePurchaseSystem/Packages/Promise"] = { "6d53f836" },
		-- 2.0.0; 2.1.0
		["ReplicatedStorage/Packs/DatastorePurchaseSystem/manifest"] = { "41abcf9f", "29093316" },
		-- 2.0.0; 2.1.0
		["ServerScriptService/Packs/DatastorePurchaseSystem"] = { "125746ae", "76f959cd" },
		-- 2.0.0, 2.1.0
		["ServerScriptService/Packs/DatastorePurchaseSystem/Actions/WipeStudioData"] = { "c40fe1b3" },
		-- 2.0.0, 2.1.0
		["ServerScriptService/Packs/DatastorePurchaseSystem/Config"] = { "4799fdaa" },
		-- 2.0.0, 2.1.0
		["ServerScriptService/Packs/DatastorePurchaseSystem/DataPath"] = { "9a0e2280" },
		-- 2.0.0; 2.1.0
		["ServerScriptService/Packs/DatastorePurchaseSystem/DataStoreManager"] = { "b5d84f05", "87d9225e" },
		-- 2.0.0, 2.1.0
		["ServerScriptService/Packs/DatastorePurchaseSystem/DefaultData"] = { "9d57bd9d" },
		-- 2.0.0, 2.1.0
		["ServerScriptService/Packs/DatastorePurchaseSystem/Migration"] = { "fee866b5" },
		-- 2.0.0; 2.1.0
		["ServerScriptService/Packs/DatastorePurchaseSystem/PurchaseManager"] = { "67b2ce1e", "db4c16d8" },
		-- 2.0.0, 2.1.0
		["ServerScriptService/Packs/DatastorePurchaseSystem/StudioSafeId"] = { "0e18b354" },
		-- 2.0.0, 2.1.0
		["ServerScriptService/Packs/DatastorePurchaseSystem/ValueReplicator"] = { "db814ea0" },
	},
	lines = {
		["0e18b354"] = "3c753427 c810bcd6 05b18514 09d0d170 20cd1d0f 7eb45537 30f7e6c3 cefb0de3 10a9f11d f599469a 151c7975 07927118 c3afe8e2 287f8882 908c08c2 cda40c92 9d2186b6 5feef797 441d008c 981efa61 27432d93 3c9f6ce0 413c6390 4e2309d8 6a8e75aa 91b61305 2388b63d 1a766734 7366cffe de826e49 603d1850 6a8e75aa 214474fd 4200fc5e 9da72a3b 48152d07 e14cc924 6a8e75aa a471d96f 6a8e75aa d358500f 6bf29b69 9ab16ca5 6a8e75aa d42ab072 dfd92cc9 06607f19 ae9ca9f7 6a8e75aa 6a8e75aa b56319bd 6a8e75aa",
		["125746ae"] = "d0719448 20cd1d0f 98406232 b3d4f3e3 1c32994d db84d82c 87d76e8a 2945fb82 d7ebc684 268ed75c 8f350bee b61bbb86 54f53612 3216ddbd 16715ceb 82ee11a3 3a5ca984 4a32db22 4e4e837e 37b2ccf4 edc7c74c 4260d70f f68b2746 f09e722f 20cd1d0f 85a7fb60 d336069b 86fb328e 9d6bd759 afb7f93d c02f2988 2f0aae2e d7fad579 c446fc21 4e348d63 2647636e 45adc5ef 9a5cdea5 e9ca72a8 418e3d10 06288af2 0efe1257 3e7dceeb 42a54e43 56d93dc9 4a0e8026 05089dba f9f856b8 2125b982 550e46d8 6e501d46 04850ac0 6b37033e 5e720d7b 4119abe8 11150029 2bbcaf42 e27e103d fb892158 4b350b0d a9f95eee 2be4336a a10afd2a bb3595c4 2e9b66b4 c23828a1 16b7f63f fc562313 bfc94e69 20cd1d0f bb2d425f 25b5b07e 49f2b000 ee17f303 50753455 155328b1 dcae1451 20cd1d0f 09118751 2c14a7e4 4b4b410d 946f1d29 20cd1d0f dbb13cdb b8c5d4a1 8c0376c0 d609b840 31e267af 4dee7c42 1dcd17de ac00e9d9 168a4e20 70f31494 cd3d1934 a0e4b848 f635e12e 04053bc7 444301f2 fc562313 0fcdcb0b fd10c18c fcbbe3a8 febff2ec 943136d7 5d171fa1 19c17786 4142fd39 8dcbc7b6 acdbf4c3 75d253c5 85ee37bf 6a8e75aa bc4e9d07 83434966 af464080 0611a931 ef66c34b 97119d0e 7ccea23e 6a8e75aa 4142fd39 4142fd39 4142fd39 949c3079 c154d2d1 cca8fd4a 7ccea23e 6a8e75aa 6a8e75aa c680565f 3c1a8f49 4142fd39 f6fd7e38 2cde96ce 4142fd39 0fa21658 b4725586 d413ff26 4649ffe6 3c1a8f49 6a8e75aa 1c09b1e6 6292109e b368953a 4142fd39 4142fd39 d2dcee57 da8bf263 447c7d2b d1a68d3d be713eb3 afc65b37 30eaf820 5dcf8711 6a8e75aa 05fd2b4d 19ae59ad 6a01fdab c8e08831 4142fd39 8dcbc7b6 21fbc03b 6a8e75aa 2dc48a06 6a8e75aa 34a6f48c 8cf01121 6a01fdab 8d4e5fe0 4142fd39 0b6535ac 274bcd3f 6a8e75aa 8bba9d8d 6a8e75aa cc480872 e0f34ef7 6a8e75aa 67747202 ae9f81f6 a35cbcc0 f3925f10 58fb2912 d9e8703d e96dfe14 f464e0e4 dfe59438 a60249f4 55a0fea6 39d262c6 c23975ba 901ca274 22538f18 f80c48a8 6a8e75aa",
		["29093316"] = "67747202 b3e58952 a849de35 c30f3f58 f80c48a8",
		["41abcf9f"] = "67747202 b3e58952 836bce0a c30f3f58 f80c48a8",
		["4799fdaa"] = "900e069a f93c64c9 1065125f ac8af668 e8c67b5a 3a653c00 84e14f9e 9f628ce1 f4661a2a 986476c3 62eb3766 6a8e75aa ea58ed95 11996737 20cf8142 4142fd39 a07993dc 6a8e75aa 64c4294d 67747202 80bd761c 15fcb9e1 e831ea40 c095e8af 25270aff 19311164 ece6bb4c 2098509b 3716d3bc 67680a70 2797058c ff5627cc ff5627cc f80c48a8 6a8e75aa",
		["67b2ce1e"] = "e233ae5b 20cd1d0f c8439a6b 1230380f 2b4e144d 85d6b201 56a15d07 ccef3773 f99dca07 6e11d991 20cd1d0f 709d2044 38f82c17 d15de5c5 2f66117b 801d381e b3299c55 d272eda8 bb3b6157 e05a17f2 506a6220 be9864a4 e364f993 adc9d02f 0a44958c 20cd1d0f f82823b6 5e576c91 3b0b9c3e 20cd1d0f 75da84e0 17665512 cabc49f4 6db6e7a6 990b1de8 cb81d3f8 6a97195c 20cd1d0f dea4a6a9 35a3322b bd53e97d 23861502 6df6b94a a47c423f f74033ac 497b269c c28fb20e bb35904e 28996320 b3f27fa2 c6296445 be4a6626 88889bd4 44f98f29 8f269f16 6a8e75aa 9fc3c0c0 8fa69177 959b5822 c0c24f05 6a8e75aa 6a8e75aa c76d0cb4 20cd1d0f 674430bf 2b3b79f2 1ff6d57f bcacea1e 289c2c87 cc21cad6 8c2ae597 ff4989a2 7132c7ee ef10ebf7 3098c15f 23873069 c2efbc30 471b4c26 1f87d7a7 7d51b023 85ee37bf 6a8e75aa 98bc3cc6 8dcbc7b6 c863ecfa 6a8e75aa 6a8e75aa 09b8e058 4adae112 4dc10082 870aee06 f9237ee6 c7bb1694 7d16d120 9ba0f388 06a72756 dff43f39 be145507 71978316 6a8e75aa 472c1066 9910f592 abe8e280 d26ff3ee 6a8e75aa 6a8e75aa 64b30b0e 6fb65bec 0853accc d1f215cb 256f545a eb6a66ed 9910f592 7577c6ef 760df139 7fade65c 00ad3ace 6a8e75aa b2890ca6 20cd234a 8db21a97 005c426b bc04c08d 6a8e75aa 582f38de f502fb56 42a54e43 e4d049e6 d244eea2 0c9869ae 791ee20c 80c3675a 6292109e cd5a55c7 d636f5e3 0df2bb73 0ff1dfc4 4142fd39 4142fd39 6a8e75aa 977263bc 6a8e75aa 836eae2e dc14b258 4735b1c1 9b6b837d 6ecdb87c c3f07f56 0ad72f75 08552835 67a84083 0192fb15 6d7cab0d 6a8e75aa d0ee1731 6a8e75aa 55537dbb 1e3c639f 3e7dceeb 2033ca59 401c5c57 a107f719 6a8e75aa d4d674be 42bd1d3a 7c6b8271 a107f719 6a8e75aa ec68f34d ec1f8dca d35b3226 38d1db7d 14869b67 db61a1f7 493272bd fbbbdf6c 13b148a6 d69eb187 0da83f03 c83deb5b 4fbcd04a f855ab84 6a8e75aa dcd62af8 08552835 35a788ef 823c3047 9e20ac4c 45f5bef8 036540ae f855ab84 4c792af7 8f579753 6a8e75aa a107f719 6a8e75aa fa9329cb 7de2b239 46f8fc83 a66b4569 6292109e 42a54e43 3e7dceeb 0b5c9f18 fdb7e067 e51df417 d0136e34 352459cc 6a8e75aa 4142fd39 720c0d79 d0d06229 4142fd39 4142fd39 4b616dde 6a8e75aa 67747202 df18f65c 5dcf575c f5db9444 f80c48a8",
		["6d53f836"] = "11c35325 fb0bccc1 3006c30f 9e8c6981 8f4a7f26 5d3fee22 4eac4425 c10a4440 a0513863 bc04c08d 6a8e75aa 303756c3 c41ad9cd 266155ba bc04c08d 6a8e75aa 6a8e75aa 3568a3c0 6a8e75aa 11c35325 6bf1d256 3006c30f f5ba7415 6f741252 d6c2029e de100705 6a8e75aa 5b137928 acc094bf 435a826f 444301f2 f0cd180d a7b04b20 444301f2 fc562313 6a8e75aa 2b7f70d8 b7d33f41 783730e7 9a2db465 1c8700bb 2d936176 555dc8de 621cd814 706550ef 3f6be881 04dee8af bc292080 248fa23a c052577a 7a997c2d f80c48a8 b7c1e0c8 1f7eded0 51720ad7 2eee07f7 aa989ad6 0eefe350 6c1fa810 3fff67d6 a419acba 4ad02fee 35053d65 8315f201 6a8e75aa 4f1b9e76 b30a3e76 bcb448d6 0ea76c25 4bc77df4 6a8e75aa 6a8e75aa 3568a3c0 6a8e75aa 52fbdc36 435bbe06 1cf984e3 6a8e75aa 30d7c1e0 51720ad7 9c7272f5 0a075a34 6a8e75aa a40b2c95 e973262a 798b4ab2 38fa98cd 6a8e75aa 9442abf8 6a8e75aa 27ad07f6 e9763b56 bdd39a92 f80c48a8 785e9f36 1eafb40e 45144e53 a1dec9d6 df91f3ff 45b51286 e4f52759 2c0c9a84 6a8e75aa ab86394a 6a8e75aa 6a8e75aa 11c35325 0a6e02bf 452b6a30 3006c30f b76e0a6a a465d5ad 6a8e75aa 11c35325 5487b004 3006c30f cd5fce96 4fbcbe7e 6a8e75aa c6e66ac5 cab336dd 9d8a9735 82e3a9af 20d8d38b 2f0f8529 656d65a4 6a8e75aa a53be8d0 b2c10c27 4035de2c 0c47547f d21469b1 fc562313 6a8e75aa 6a8e75aa 11c35325 0e1420e8 3006c30f f2b973c8 243928f9 6a8e75aa 11c35325 d9e89c1d 5516648b 3006c30f 49e55b67 9c0091aa 2d36f3e1 2aa424e3 857d2478 bdbf5bf0 03bd1ef1 6a8e75aa 6a8e75aa 6a8e75aa c24c6b24 4c823a04 6a8e75aa 2b7f70d8 aae60e4c 48f2afe4 aa9033d6 51e0d08d de77eeca 11be7598 eda809e0 e6f4c511 2d936176 2b7f70d8 91190c6a 51e0d08d 117ed79d 26014cbf 0b547c5a 2d936176 2b7f70d8 2a616bf5 67c37b83 392e958e 2afce7c0 2a554b8d 2d936176 cfe454ff 3de14b1c 83cdd7b7 87e38aae 76290058 e31db683 f80c48a8 06ad9dd7 edd342d4 902c7795 1b3c2b21 4e1ea35e 6a8e75aa c45413b8 b117ceaf 71bb73f2 73a44191 78e2ef9a cf58a123 065fbb98 33154d14 0b11639c 20a5d17e b7061ca7 98c5ca0d 541e5f45 5f00cfab 4ff4c787 5dbeed6e fcfab827 81f9c91d ad206658 7de9c322 e9fdc5ae caaea239 72271699 efa01463 46f6cc0e b9e1bd5b f80c48a8 49f17871 958f2bae 6a8e75aa 7f877e8c 8d40a911 de8649d9 6a8e75aa f63028c4 d0fcf02c 6a8e75aa 114c8628 1f249f8d 5d380850 d15cf4be bdbf5bf0 98a0b879 6a8e75aa 6a8e75aa b50ce522 6a8e75aa 7b6b052c 15710327 8dcbc7b6 03bd1ef1 6a8e75aa 4142fd39 fa00f2d9 4ffc77af 6a8e75aa 2b7f70d8 d2d3db67 7e9e8353 76789316 1fd5c767 c762b189 13132829 df6dc906 f01ca60f 4142fd39 6a8e75aa 46a0a346 8e2a7297 f6f736cf f4c36889 4cca8adc ab58554b d3b89b47 0bc1c830 b2457bd3 ce6f8fdd 593b957e 3e0f45cb de8dc7b5 20bed5dd a3f060b0 fd78cb44 2d936176 4618d328 4cf5cd44 6a8e75aa ceef82db 1087f6ef 6a8e75aa 2b7f70d8 0e06c55b 3981fe03 1fd5c767 0949695c f60b4186 9dd1f8af d7d22751 4142fd39 6a8e75aa 8e2a7297 a3f060b0 fd78cb44 2d936176 a565438a eda33b7c 0efb1261 752c6f2c cab8a330 a0112362 72c622e4 523b0d5d 8dcbc7b6 03bd1ef1 6a8e75aa 4142fd39 4142fd39 df4215ea 6a8e75aa 6b9feddd d931efe0 2b7f70d8 91b6529b 1fd5c767 367ae400 9adad9c1 0eeac6ac 70e5a262 bdbf5bf0 23205798 aa72d8e1 6985f0a3 0ba2c106 4142fd39 6a8e75aa 6a8e75aa 8e2a7297 63ead650 f0e81c8a 2d936176 6b070bef de51edc7 b6023d07 d4d4d968 4142fd39 6a8e75aa 2b7f70d8 74433329 593b957e b5b80a43 20bed5dd 63ead650 f0e81c8a 2d936176 6c5284f2 de51edc7 455f42c9 54b9bacb 4142fd39 6a8e75aa 11c35325 9f8dd8ad 90fd422c 3006c30f 5a949b3c a82f3bc1 cae5263a c55ab684 4142fd39 6a8e75aa 2b7f70d8 6a9b201d 579a01cd 453dda27 20bed5dd 1fd5c767 1f94a834 3a42cac3 4142fd39 f79c1a89 9c79b39a 4142fd39 a3fbdad6 32678bf5 4142fd39 8e2a7297 69ae1a3f 0e0f853f fd78cb44 2d936176 42943167 70b23181 6a8e75aa 11c35325 792ef6c9 b98983a2 dd75f3f5 3006c30f 60fe08b5 e841fdc3 ccce3001 6a8e75aa cca0229e d0736eeb 9d23d0ec 3eed316b 191ac329 6a8e75aa 6a8e75aa 2c280ef8 ca2c9f47 dd7afb87 6a8e75aa 2b20b3be 6d8e36c2 873a54bb 95ef3e59 9e4c809e 0e266a21 b5a32e20 b0b0fc9a ccdddcc0 9385408d 4aec368f c03f3d35 6a8e75aa 6a8e75aa 6368474a 3cab69ce 5a258941 85ee37bf 6a8e75aa 50f0f5b6 038e25b6 a0059ddf bdbf5bf0 cc108a3b 6a8e75aa 8f5f527e 5eb02042 6dfcd46a 3b348320 6a8e75aa 6a8e75aa e23c8ecf a84e2b41 4aca513f ef40808d d91b342d eaf19153 7ebe8277 a72ab9e2 9b197e7e 3b348320 5eb02042 f756a5c3 6a8e75aa 4142fd39 6a8e75aa 5a258941 3b348320 6a8e75aa 4142fd39 6a8e75aa 2b7f70d8 4df7dd62 4ae8d9e0 fc56b70d 579a01cd 6b10ef7b 20bed5dd ca644442 1fd5c767 183130a4 f135b93a 5458b49d 3c1aa640 f80c48a8 f80d8196 8e2a7297 6b79b8d1 e57eb590 2d936176 c00dda30 985776c6 6a8e75aa 2b7f70d8 d046e27d c862a4d7 3ef06c16 1fd5c767 5ec19a19 1d611661 3165e1f1 bbb93e85 bdbf5bf0 88604186 ced0766b 7216f462 4142fd39 6a8e75aa e1bb45a1 8e2a7297 0cf8c3b5 4ccf2eb3 00441bc4 421967ea 2d936176 af21e40b 0d98675d 6471cf54 24fea982 e4f007c9 af0bb3d5 68d97b55 4142fd39 7a26aec4 25607481 4142fd39 6a8e75aa 2b7f70d8 a06d068d d54705d4 1fd5c767 183130a4 f135b93a 5458b49d 3c1aa640 f80c48a8 ea608153 8e2a7297 6b79b8d1 bda423ac e57eb590 2d936176 79b8ae12 57286126 81c1f29d 6a8e75aa 2b7f70d8 b829d85f 29d3c264 1fd5c767 183130a4 f135b93a 5458b49d 3c1aa640 f80c48a8 dcb734c6 8e2a7297 6b79b8d1 e40b37fe 2d936176 abdc9369 80663216 8ad7fc22 4142fd39 6a8e75aa 2b7f70d8 f021e14b 1fd5c767 183130a4 f135b93a 5458b49d 3c1aa640 f80c48a8 bb8ecab1 8e2a7297 6b79b8d1 8f6c8ed4 2d936176 5c411443 e841fdc3 d271a9ed 6a8e75aa cca0229e d0736eeb 9d23d0ec 3eed316b 3d1b7827 6a8e75aa 6a8e75aa 2c280ef8 2e195a8c dd7afb87 6a8e75aa c907ae75 6d8e36c2 1ba7043c 95ef3e59 9e4c809e 0e266a21 7b68f536 6368474a 3cab69ce 22d8e342 fb7b7da6 8d3e67ee d88ffa8f 6a8e75aa 6a8e75aa 11e076ed 4aec368f c03f3d35 6a8e75aa 4142fd39 a84e2b41 4aca513f ef40808d 24a06f50 eaf19153 4142fd39 6a8e75aa 4142fd39 6a8e75aa 2b7f70d8 274b3fae 2eea43c7 f091db3d 282cc937 20bed5dd 41c940f2 1fd5c767 183130a4 f135b93a 5458b49d 3c1aa640 f80c48a8 0df90677 8e2a7297 6b79b8d1 e40b37fe 2d936176 2c1d430e 974541e5 9d23d0ec 63f3087f 6a8e75aa f9223c83 95ef3e59 4f01fd66 9385408d 4aec368f c03f3d35 6a8e75aa 6a8e75aa 967134f0 9c0091aa 3b348320 26870684 e30e7fdb 6a8e75aa 6a8e75aa d5d2e0a9 85ee37bf 6a8e75aa ef40808d ee81c500 6a8e75aa 19996d23 3b348320 6a8e75aa 4142fd39 6a8e75aa 2b7f70d8 740f0bc3 5cb19462 c49bbdba 579a01cd 5781177d 3135a9dd d519d24c 20bed5dd 1fd5c767 f6d400a4 e1cc7719 6820cc2c 80eb4b04 2f44c391 07b0930a 91de3fea 83956726 4142fd39 4142fd39 11c35325 01bda1e2 b241ddf6 01bda1e2 3e11b9b8 01bda1e2 bd034b27 01bda1e2 3a94e573 3006c30f 8e2a7297 08f5b404 eea0fc02 75a377d2 05c8cfdb a3f6c657 eb683e95 ffc7d1c3 29df9228 74ef3ac2 4ccf2eb3 a8ac9742 7871830f 2d936176 0353e9a5 4fc6139f 3446e864 f9223c83 e20deddb d956136a a649b3df 9385408d 178388e1 09e382e4 6a8e75aa 6a8e75aa 11e076ed fb916979 3b348320 4142fd39 2dfdea49 db1b22a7 5a95641a aa86bd54 1426e065 b3f94f1c bc0b9fc8 e9672e54 604dd0b7 3b348320 918d2f01 2614d16e 705d7e09 1ff7bb47 58cda0d2 c2c8df85 712e8af4 ffd77678 7599744e 3d266837 3b348320 88e07eac 6a8e75aa dc3530d7 2726bca0 c5f3cf21 4142fd39 5bbd5def 9ba854f6 bdbf5bf0 767ac30a 6a8e75aa 6a8e75aa dbea7fdb e9672e54 3b6f30eb 18aa9850 1a63a6d7 3b348320 3f021ee6 6a8e75aa 6a8e75aa 034ff4cc 85ee37bf 6a8e75aa aa776b50 0d5fe2d4 18beca5a 1a63a6d7 3b348320 5a652d36 6a8e75aa b06d067f 6a8e75aa 7f086ae0 4142fd39 6a8e75aa 2b7f70d8 f7b6f29c 6ff10101 71273485 2d936176 52b46cf8 a5a87434 3568a3c0 6a8e75aa a94bddd2 2ffc7382 d2d24248 bc04c08d f50843c5 0066d1b7 1c0554bb 54554c87 b04d4c40 37e42f8c ee2a61d7 e522e976 e295bd3c bc04c08d 6a8e75aa 3568a3c0 6a8e75aa 2b7f70d8 a1a4993e 28db2aca 579a01cd c1b1acd0 20bed5dd 1fd5c767 9264897b 1b335794 8e2a7297 1fd5c767 0fe2bf81 9456a041 4142fd39 8e2a7297 b2c3be55 51842be0 2d936176 406688f4 9c0091aa 70b23181 6a8e75aa 6a8e75aa 2b7f70d8 3bdf8a30 b8ae4fb5 2eea43c7 14a0dc92 20bed5dd 1fd5c767 112926fb 8e2a7297 5b3ea430 51e0d08d f1cc009a 036e4125 2d936176 621cd814 4ac79ab3 0944200b becbc766 cab8a330 3e738f27 493a2768 11214e97 74eac621 04970b7a 8e796e53 6a8e75aa c907ae75 3a55d0ca a27532a2 346f0ca6 6c654c74 5cd9fdd8 527a24fc f80c48a8 1f1bcaac f5e2410a a0112362 ed762409 c65961cc a4f85d50 a3c795f2 ffe34648 72c622e4 c6a5f3ad bdbf5bf0 32b3afcc 6a8e75aa 66351760 6a8e75aa 4142fd39 8c153441 0171b505 b96e5a2d ff4f363e a4f85d50 ed90243a 6d42ee56 95cb2aa2 8f14849d 6a8e75aa fff5bd4d 729bc35e 9ba2e66e f9f3f84a bf870704 e3728904 6a8e75aa bdbf5bf0 a736fc1c 39e9830d 2bf8fe6d f5e2410a 6a8e75aa 6a8e75aa 11e076ed cb748c1e dc727c2d 79986195 bbea7ff2 72c622e4 c6a5f3ad 8e594b80 7927fb27 6a8e75aa b0c06dd3 bdbf5bf0 31a78c5d 78e0dcba 9b9a3151 f9f3f84a 2cc4ae7d 6a8e75aa 6a8e75aa 4142fd39 4142fd39 6a8e75aa 6a8e75aa 2b7f70d8 4d94e839 3af24f93 1fd5c767 2eb595c0 5e488611 7939de9a 04e50d7e 1368fd46 271b768b bdbf5bf0 06fe16c9 6a8e75aa 4142fd39 8e2a7297 7f3501d0 1fd5c767 92834940 a4beca30 bab707ed 75a376fe f8c5577d f909ebd0 2c0c9a84 d174680f 2217e942 fc562313 8e2a7297 f1cc009a 0eb55228 fd78cb44 2d936176 f8b97203 eda33b7c 32d1d888 a4beca30 f5c2af14 b990c097 b69740ad 1ff7bb47 5e03bdfc 77c5eeb2 c9c5df0d ffd77678 cb416e0f d174680f 57409541 fc562313 6a8e75aa 2b7f70d8 ee52df23 9e5e29b1 2d936176 61449b17 2c6cd7ae 6a8e75aa 11c35325 175ca723 d225d7aa 3006c30f 6845aec1 5e5fd0ca dd0fa50a 5d380850 f205c296 c03f3d35 df4215ea 6a8e75aa f76272a9 2b20b3be 3cc40580 ad72d145 e8641e07 c1f3dde2 bb311835 6a8e75aa d17906f1 a961839b bbbc4cc9 6a8e75aa 8505c37e 42525a14 9982f52d aa5dbd95 11e076ed d6162c6f 7404922e 8505c37e fb6213d4 95285fc1 6a8e75aa 4142fd39 0d2d3ffa c23bdcdc 343a1fa8 b246ff10 f928c899 d1ecbdd3 6a8e75aa 06bfb24b 6a8e75aa 2b7f70d8 9e53e97c 2eea43c7 21dd5052 20bed5dd d2034492 46e1d5ed c5fb4402 12da8ff9 fa2630d1 20bed5dd 3ec142b3 4e8590e1 f0e81c8a 2d936176 370bcd42 1215d72c 28f65984 18871ea3 6a8e75aa 2b7f70d8 e164bcc6 6505694b 2eea43c7 21dd5052 20bed5dd 6d5359a2 c5fb4402 acf5a040 fa2630d1 20bed5dd 7c1bc452 f0e81c8a 2d936176 a5986fdc 107b05c7 0fedca8f 6a8e75aa 2b7f70d8 601cd4b0 1fd5c767 e38e1fa0 1cb18968 cea45ba0 3c28da0a 4142fd39 8e2a7297 0dc720be 835dd875 f0e81c8a 2d936176 6fe0a111 fbc27f9a c45fec88 a5c3d39a 0189ecb6 de51edc7 06c112c7 8f27b71d 4142fd39 6a8e75aa c5f3cf21 4142fd39 6a8e75aa 2b7f70d8 8d8a949c 1fd5c767 1d7beb11 8e2a7297 608e010a 1fd5c767 3b7b18ad 5b5b8924 4142fd39 8e2a7297 da33f071 9ea27dec fd78cb44 2d936176 64bf81fa 69c11ce7 de51edc7 d766fd7c f1be7d73 4142fd39 6a8e75aa 2b7f70d8 732966e9 1fd5c767 7bbc3745 8e2a7297 608e010a 1fd5c767 3b7b18ad 0052b4fb 4142fd39 8e2a7297 593b957e 8aa4a4f2 20bed5dd 4824c59c fd78cb44 2d936176 de01a4b7 de51edc7 d766fd7c 8f27b71d 4142fd39 6a8e75aa 2b7f70d8 860951a5 b72592a5 177580c7 1fd5c767 c03f3d35 8e2a7297 2d936176 016bed57 4d73dff9 85ee37bf 6a8e75aa 6cb2219d b30314ce 0cb576a3 6a8e75aa acbab9b7 1b6c9baa ff4d44ff 6a8e75aa 294d2662 43b3a180 6a8e75aa 5df5f5c5 6a8e75aa 11c35325 b140f059 a9ef7e86 3006c30f f5d670ad 4d73dff9 85ee37bf 6a8e75aa 82f47fe5 3dc258e0 dfffb2b6 6a8e75aa 6a8e75aa 11c35325 b90cd321 91edb6b6 3006c30f db81b290 5e5fd0ca 3b4d166b f2f85a1d 11e076ed 683ad753 ba609ab0 28a5c717 54e38c9a 91d0b0ca f6868097 6a8e75aa 4142fd39 db3ceda3 0cb0890a 99319901 65c18aa8 0189ecb6 f4bc4590 8bf3fad0 8613f1d9 d39c72db a6b39466 6a8e75aa 4142fd39 49e80201 f756a5c3 4142fd39 bdbf5bf0 a6b39466 6a8e75aa 6a8e75aa 6a8e75aa 8505c37e 9388d378 1168512e bdbf5bf0 50e1a520 adbda261 6a8e75aa 4142fd39 df4215ea 6a8e75aa 2b7f70d8 35c2577e a74ec31b 9567aca9 7faf5afd c506751b d91b09ec a7c9af9a eacc79fc 8845ad94 dd7f1021 95ee2d9f 24e01084 392ad8c4 75d9a79b 6f1f89ad 1d84cb42 909688d7 424aab7f 6da2ef20 20bed5dd 1fd5c767 6074637c 1905b1bf ceee1490 448318f1 76fde82c 4142fd39 e571fc0f 788dfbe5 4142fd39 840e14d3 1cf5e7df b86874b4 4142fd39 8e2a7297 ba7f9583 f0e81c8a 2d936176 f021bfa1 1b2b7e37 f010c0de 6a8e75aa 2b7f70d8 87536516 68294ae0 da33f071 9ea27dec fd78cb44 2d936176 fa23fbf1 4a84fc5c de51edc7 a0a0ec81 f1be7d73 4142fd39 6a8e75aa 2b7f70d8 768ad74e 1fd5c767 63c5ed84 8e2a7297 608e010a 1fd5c767 cfe0a8f6 0052b4fb 4142fd39 8e2a7297 4824c59c fd78cb44 2d936176 4a3f74dc de51edc7 a0a0ec81 8f27b71d 4142fd39 6a8e75aa 2b7f70d8 57a363d7 0309c82d 9c71f816 c402ebcb 2d936176 b47a89bd 5e5fd0ca 8505c37e f896b653 645ba277 840e14d3 b67669da 4142fd39 ab5d2971 28b26b0b ededc4f4 176d19f7 2c0c9a84 0de53d6d 6a8e75aa 859e9295 c615f26c b246ff10 c615f26c 6a8e75aa 2c6cd7ae 6a8e75aa 8e3eac7d a24b4022 6a8e75aa 2b7f70d8 b6cf0b02 593b957e ed6a0e71 20bed5dd 1fd5c767 bb0fafbe 94330bbd 62b6cc3e bdbf5bf0 c224042e 6a8e75aa 8e2a7297 0309c82d e918c5bb c402ebcb 2d936176 dc2799ab 5cd4ac33 6a8e75aa 5c5922a4 7bb8666d 07bf0b8b 6a8e75aa c5f3cf21 6a8e75aa 2b7f70d8 8e28cd07 1fd5c767 a523953a f1397a9e 4142fd39 b0316b10 c224042e 6a8e75aa 8e2a7297 918ee7f5 1fd5c767 07078701 8e2a7297 76bd5d90 60b4517a 0309c82d bf376142 2d936176 26d2a150 07e924f2 6a8e75aa 6b9feddd c6f56c78 11c35325 4db8253a bc39474d 3fc5c80e 052010b1 3006c30f fbc49713 8505c37e 3e8c57ef 6a8e75aa 7b481ee0 a492a96e 6a8e75aa f4e2e2b6 4d73dff9 a47f2ef8 9fcfa7d5 6a8e75aa 85ee37bf 6a8e75aa e7328304 a47f2ef8 7717771f 272d3fc8 17cfcbce 715669c4 21b11959 2c0c9a84 99d5b651 6a8e75aa fce1f15c 76c17aa6 de8649d9 7ebe8277 6152d4be 2acfbfb7 ff9aff99 3028d47d 2883aa2e 4035de2c 756b3e03 fc562313 6a8e75aa f3fc6a48 df6c9d90 ce9ec389 ef50e619 1ff7bb47 b6acf892 21b11959 ffd77678 7599744e 6a8e75aa d0fcf02c 4142fd39 64f6fd55 dfffb2b6 5654bc88 e834d0da 2237bb56 39a4b735 6a8e75aa 85ee37bf 6a8e75aa cab7184a c8401158 fd7882ed e11e8937 f48b1a90 6a8e75aa 5df5f5c5 6a8e75aa b3c7112d 4d73dff9 85ee37bf 6a8e75aa 13851818 c8401158 f2dcd3f8 37cc9017 fd7882ed 5f2d948a f48b1a90 6a8e75aa bdbf5bf0 441f427a bbf71eb3 f9f256c7 480c32f8 94b1e648 35403d08 dc422f48 f5c3499d c510ace9 85ee37bf 6a8e75aa 58d0b09b 0bc24fc3 5e11fa95 47296cef 6a8e75aa 8de870f2 3e8af42b 85ee37bf 6a8e75aa 99d5b651 5836235e 6a8e75aa 5df5f5c5 6a8e75aa 11c35325 1cdbd7d2 4fdccdc5 2465dea3 3006c30f 7555ca96 98da37e0 c88a9f4a ef569fe9 b3a04df6 bb69fced 6a8e75aa a2742294 c162ec5e 1f33005b ff88c045 f9c70e17 5d105068 f3c5053d 6a8e75aa c4c60622 6a8e75aa 2b7f70d8 ba4d8b4d 1fd5c767 d4a3a423 0a888924 4359199d ce1db5c2 4142fd39 8e2a7297 a431d27c 1c8598ab fd78cb44 2d936176 19250f1d eda33b7c 859e9295 1337464d c5f3cf21 4142fd39 bdbf5bf0 f5c2af14 8a4a1225 f6eefaf3 61aefe51 cb416e0f 6a8e75aa 6a8e75aa 2b7f70d8 a7d7ee56 8d488841 1fd5c767 008283b1 23205798 7d0c8a65 ee2387ef 1336f302 b95e0ab3 bdbf5bf0 0ba2c106 6a8e75aa 4142fd39 6a8e75aa a30fd9ed d0b51cc2 8e2a7297 74ef3ac2 31011c82 2fd2f153 352a3407 e40b37fe 2d936176 8eada4ea c26cd754 8f0ddf98 d3225c68 585683e7 6fc9be61 0c937fc9 bdbf5bf0 a6b07fb8 6a8e75aa 4142fd39 6a8e75aa 2b7f70d8 51c06e92 9977ba8f 8d488841 7c6a0b98 31011c82 2fd2f153 f1cc009a 352a3407 e40b37fe 2d936176 f89cb330 c26cd754 4a4c70d1 69eb7324 d3225c68 585683e7 6fc9be61 c84b27ea 44c470a3 bdbf5bf0 a6b07fb8 6a8e75aa 4142fd39 6a8e75aa 2b7f70d8 411b9eb5 8174b818 aba0c6d4 c5fb4402 a1ce242d 20bed5dd 1fd5c767 ead0dbe5 3c2dcaaf 88f5fa8d e84dfa5b 4142fd39 8e2a7297 74ef3ac2 ee4abcc2 cb342d49 64017422 2d936176 23687686 514dd888 bc04c08d 6a8e75aa c907ae75 cab8a330 89a3933d 6db4202d 72c622e4 c6a5f3ad 6a8e75aa c2692f38 9c8ab947 7e9e59ba 364b0993 7a4cd39b 64c42ea3 97b9db94 d840801d 87ad2b70 bdbf5bf0 a83b0489 6a8e75aa 6e8f655c fce925ed 6a8e75aa 4142fd39 cfeaa3c1 801e9038 6a8e75aa 6a948ab3 4142fd39 6a8e75aa 2b7f70d8 cfe66874 0e70e4a0 a37e289a 7c6a0b98 9cd8f2db f6e616e3 2d936176 fc3cee3f 82744798 9d2186b6 d7edefb6 068f196d 2a4aaf72 6a8e75aa 6a8e75aa 6a8e75aa 0feaf18a",
		["76f959cd"] = "d0719448 20cd1d0f 98406232 b3d4f3e3 1c32994d db84d82c 87d76e8a 2945fb82 d7ebc684 268ed75c 8f350bee b61bbb86 54f53612 3216ddbd 16715ceb 82ee11a3 3a5ca984 4a32db22 4e4e837e 37b2ccf4 edc7c74c 4260d70f f68b2746 f09e722f 20cd1d0f 85a7fb60 d336069b 86fb328e 9d6bd759 afb7f93d c02f2988 2f0aae2e d7fad579 c446fc21 4e348d63 2647636e 45adc5ef 9a5cdea5 e9ca72a8 418e3d10 06288af2 0efe1257 3e7dceeb 42a54e43 56d93dc9 4a0e8026 05089dba f9f856b8 2125b982 550e46d8 6e501d46 04850ac0 6b37033e 5e720d7b 4119abe8 11150029 2bbcaf42 e27e103d fb892158 4b350b0d a9f95eee 2be4336a a10afd2a bb3595c4 2e9b66b4 c23828a1 16b7f63f fc562313 bfc94e69 20cd1d0f bb2d425f 25b5b07e 49f2b000 ee17f303 50753455 155328b1 dcae1451 20cd1d0f 09118751 2c14a7e4 4b4b410d 946f1d29 20cd1d0f dbb13cdb b8c5d4a1 8c0376c0 d609b840 31e267af 4dee7c42 1dcd17de ac00e9d9 168a4e20 70f31494 cd3d1934 a0e4b848 f635e12e 04053bc7 444301f2 fc562313 0fcdcb0b fd10c18c fcbbe3a8 febff2ec 943136d7 5d171fa1 19c17786 4142fd39 8dcbc7b6 acdbf4c3 75d253c5 85ee37bf 6a8e75aa bc4e9d07 83434966 af464080 0611a931 ef66c34b 97119d0e 7ccea23e 6a8e75aa 4142fd39 4142fd39 4142fd39 949c3079 c154d2d1 cca8fd4a 7ccea23e 6a8e75aa 6a8e75aa c680565f 3c1a8f49 4142fd39 f6fd7e38 2cde96ce 4142fd39 0fa21658 b4725586 d413ff26 4649ffe6 3c1a8f49 6a8e75aa 1c09b1e6 6292109e b368953a 4142fd39 4142fd39 d2dcee57 da8bf263 447c7d2b d1a68d3d be713eb3 afc65b37 30eaf820 5dcf8711 6a8e75aa 05fd2b4d 19ae59ad 6a01fdab c8e08831 4142fd39 8dcbc7b6 21fbc03b 6a8e75aa 2dc48a06 6a8e75aa 34a6f48c 8cf01121 6a01fdab 8d4e5fe0 4142fd39 0b6535ac 274bcd3f 6a8e75aa 8bba9d8d 6a8e75aa cc480872 e0f34ef7 6a8e75aa 67747202 ae9f81f6 a35cbcc0 559bbf86 dc884718 6213b9e0 f3925f10 58fb2912 d9e8703d e96dfe14 f464e0e4 dfe59438 a60249f4 55a0fea6 431380e8 39d262c6 eda25666 c23975ba 901ca274 22538f18 f80c48a8 6a8e75aa",
		["87d9225e"] = "d4911a9e bde3efe5 e5031d8c 6e8c17a4 bd675240 ba270482 79fdc91a f80c48a8 ca48da0d 12b8ac69 14a1bee8 e71cc62c 963fdbb7 ed312dc5 6a8e75aa 1d137568 24d47f3c 49b2c51b 365fe857 6a8e75aa 9587d575 6a8e75aa e9d0f99f 3076988c 733ad758 ce0fb68f 4aafc34c b60bd732 6a5ab244 530f2c69 20cd1d0f f0e3f39f 9470d276 7830e2d8 5f86a9cd 559a8b08 b4a1edc0 4d6cb3ff f70b0c4f 8c959a55 f76be948 8f41b275 7676ec7c 968b5dfd 6a8e75aa d65a35e9 5f0b7eec a2b41fca 4c3f7dfa 267d3ad7 7bee8f50 7b787181 72f23da8 118a722a 6a8e75aa 754df234 78e2e509 514bc83e 91e61f4f c95b4bc4 1b62353a df1f5414 85ee37bf 6a8e75aa d178b79b 9b9ff6de 6a8e75aa 43a6c0bd e9d2131a 6a8e75aa 73b4a192 1cc8e44a 3776c40c 75cea83d b4e7ecbf b2cafa94 9542704b 9186afbf e1afbc5e 4142fd39 caaddc97 6a8e75aa defdec4e 9f712bee 49499c3b 8b55aa86 a449e666 0b1d0211 b2770640 29f541f2 91f9079b df1f5414 bf5561f8 6a8e75aa 10d0ba33 fd0119cb bf5561f8 6a8e75aa 7e5f056a 969c7020 d8297aee a39d22a2 5a277980 98dacace e23dc48a 92ebfad0 6a8e75aa 4314ae0a 309356a0 d6b784ea e252e819 2b917dd2 6a8e75aa 73b1bc59 c4ed9a38 15a8ddab a925c8cd fc6fefbb 15dca99f 64afa155 4142fd39 6a8e75aa c5e0276d 8e9e80de 6206674f fd0119cb bf5561f8 6a8e75aa 9b283c8e 760c8cc9 6a8e75aa 24c3b202 6a8e75aa 7903339d c6ab66c7 d40efdc5 f26a15d7 8d48ace8 fbb40d0f a5efb97c 0b9ad786 d1d1c752 977263bc 6a8e75aa 7451b236 6a656839 df1f5414 bc04c08d 6a8e75aa 10d0ba33 fd0119cb bc04c08d 6a8e75aa 28ee321f 992d3318 7d927886 88e26519 5a277980 57cfd669 421c88fd bf39d005 c79c3dac 6a8e75aa f0bb7d4f d6180d0f 58822a04 6a8e75aa 5036ae0c 4142fd39 8e9e80de c982dffb d19bc45a ffcaf489 c198a591 9583f320 633c9e24 6a8e75aa 6a8e75aa 594e8787 fd0119cb af24b84b 6a8e75aa d26e8cac 6a8e75aa a3afdaa5 49b2c51b 233ebf22 8ce00902 e1cd7b87 f98c9df9 6a8e75aa 3568a3c0 6a8e75aa 5ac5ac02 f4edfdbd 2cea0d33 022b0c16 47265215 290472b7 6a8e75aa 2572d6c9 589875ab 6a8e75aa 1955d073 80688e1b 9745f046 7ec1b6f0 8d49220a 4a82b174 6a8e75aa 650afa71 421dc96c 65154905 8a0442c2 6a8e75aa 6a8e75aa 12136b6b 6a8e75aa 0541b4a6 26cea3a2 2cdff234 55695e43 dfc92629 3568a3c0 6a8e75aa eb60d48d d3a92601 8691796a f65ebf3f f68d12d7 3568a3c0 6a8e75aa 15a8ddab 5a880eb6 5e5ffecc 1a9391b2 2dcd64e3 98bfbe16 d439469d 0bfe420b bc1811f7 6ccb25eb c8d60ffe 52b4eb2e dec6adc2 443881a9 6a8e75aa 3df805e1 ac74a732 797f95ff 45476c56 ba49cb52 12705692 6a8e75aa ac8e0cfe 6a8e75aa c44be57c afdb80eb 1c7d32b7 16e70128 eb9ddfb6 6a8e75aa 963fdbb7 5f573553 6dc9c134 42d9fdc7 05a1fa5d 875a44ab 49b2c51b d6ffa5bd 60681a7c 6a8e75aa 9f547310 eb2fb9cf e221d644 e0f98a00 af750ee8 f0a37126 55e7ba50 85ee37bf 6a8e75aa 60a86f44 82eb2eba fe7df634 65c14f2b 34ab3e62 45ec06d3 aca2853b 85ee37bf 6a8e75aa 1f7fe4fc 5f7ccc1c e0f98a00 3dc8d6bb 6a8e75aa dd8bf467 bfe08afc dad67ca4 94e56058 85ee37bf 6a8e75aa 15e9a4ba 601ff546 fe7df634 65c14f2b f802cf32 8313613d e26a66fd 4142fd39 6a8e75aa 30d0c636 87b0e493 f4e448b3 b84f4ad2 3e21ee4f bde44b67 f9537a8f 942b88bf 68f84115 fe3e929f baea68c1 70d84d97 6f8a63eb 8e27f0ff 2b94b75d 52437efa fbd79661 85e81833 6022c430 034e10ce 53d99248 d75454a3 066ea55f 185a2aea acfb7882 57ae03ab 4ffc77af 6a8e75aa e277cc1a 7dc6ed47 f5acd332 d546941d 38398ab9 e5807f50 d067cf8b eeebb35c f0270773 7c96162e 248738d0 426c2bf2 97a5e111 3fabc91d 160d5bea 9f547310 44686528 6a8e75aa 6a8e75aa 4142fd39 fb6e4dc5 6a8e75aa 3432afd1 4f404cfc 6c91cb3a ca802b53 c198a591 2b5ddb66 24260500 6a8e75aa a98cecb8 dd7008b4 922e66bc f5acd332 64cd8af8 69eb1232 6a8e75aa 89f798a7 e10e45cf e684a5b4 21c250fb 9b4fbbf4 11769f70 89f798a7 6a8e75aa 4942b779 c19f3a20 29aad4b6 975b8ad2 9d2f48d5 248738d0 f1623060 a3837021 ed9c843a 6a8e75aa 60fbc6e0 8dcbc7b6 f1623060 03d4bdb9 6ce4d80c f5d23839 bdbf5bf0 85bf3195 6a8e75aa 53d09205 15a8ddab 2259b80d 3a005ebc d5dff2c5 470f0a71 fa61d721 6a8e75aa 16db469b 4bb4b818 f1623060 3ef86a17 7c7e63cd a3837021 927ddced 6a8e75aa 61ec3a2f c2c899db da10f589 ac6de176 ebc6aacc 92deac30 6a8e75aa 1ef986f0 f52600f5 c74e22bd fb8e6211 bc04c08d 6a8e75aa 3f09ef39 f1053989 651d40cc da10f589 8f6a0235 be48e876 27744dfd 8b1905aa 0f5f5773 26e9a0a1 d5c107bf c813a293 6a8e75aa d9ab2e54 010eba01 82a06b57 568d67e4 c3da633a 63324480 f1c2c447 aa28dd58 c2c899db ebc6aacc 3fabc91d 202ddf0e 796e46a4 6a8e75aa 7d5ee063 d4b90bf3 865d0069 da9cdd98 a7d04da7 074bafe9 1b03c172 6a8e75aa c2b7f49d d7818bce 6a8e75aa d0c07042 0efd688f 67082394 e32d49f1 c2c899db 6a656839 39523ae3 1ea6aff3 f6bb30e9 f98c9df9 6a8e75aa 37fd4ceb 60a86f44 e221d644 c3491063 aca2853b ff7c2dca fd5b7eba bc04c08d 6a8e75aa 03a454ad 3568a3c0 6a8e75aa d5303d6b d997b3b5 7584beaa 955aaeba 48d32275 e3537494 6a8e75aa 17412508 e079b352 d83b3afa da10da2a 60a86f44 82eb2eba aca2853b 5ee36135 6a8e75aa 2aa424e3 7dbdc1e1 bdbf5bf0 f6723a05 6a8e75aa 6a8e75aa 6a8e75aa 7f5dc62b 7584beaa 6a8e75aa beeb841d f6d9a952 d9f31298 7132e079 f200cc68 c791f7d4 6a8e75aa 54745419 6a8e75aa bf6c7b80 cdfa0b41 10c380f0 2a2123e6 098734d9 a0ede086 6a8e75aa 958faad4 4f44031c 6a8e75aa 8d417798 ab4fec3c f93d5bae b31b5633 6a8e75aa 015cce71 b157b7f5 58079c7f 0d3e1d44",
		["9a0e2280"] = "35cd28d4 20cd1d0f c4c977fb 4f32024e 3d7276af 8fda288f 94ff9773 20cd1d0f 527e9c11 abcc1f03 c81a13d5 e077988c 1e2752ee 30368e49 a917396a dd30e141 48d3e274 6a8e75aa a29f7397 f4780811 75fdd5e4 6a8e75aa 1459fcf4 48d3e274 6a8e75aa 63ec09dd 6a8e75aa db3301fc 9f2c7b0e a7c888f0 f3b66e81 48d3e274 6a8e75aa df520443 f0a8b023 a897754b 48d3e274 6a8e75aa da2ce9af 6a8e75aa f52c29ad 6a8e75aa 3108f1f9 3d8c7fe8 e17bb5a0 a7c888f0 324f5d40 3e724601 6a8e75aa d9846005 7e0992af 6a8e75aa df520443 76fd6d47 07d9dac9 f38e92a3 c318e4db f0aa8391 d84ef210 281357b9 769ca16a 6a8e75aa ffc8919f 6a8e75aa 8d005228 bf5561f8 6a8e75aa c91e2423 f97d3726 3acd83ae f2098f6f b9d020b4 0fded03e 6a8e75aa cafe0881 f2e6684d 61ecdd79 6a8e75aa 0e92f79a 9a3bd139 58583c4b 6a8e75aa 6c2607b8 8dcbc7b6 608bda07 6a8e75aa c2f49059 6a8e75aa 130083ee",
		["9d57bd9d"] = "346dbbf4 7b82c991 402053ee dec0da32 580105e4 31c56c45 81fd7ff6 4ae77a81 165ff28a d9c1e771 dfecd152 3ad02400 65e391c3 ddf0a17c 6a8e75aa 6a8e75aa 6a7476bb 6a8e75aa f2189d36 29e7095c 295456b8 3ebb2999 4142fd39 0febd176 3ba8165f f4c3818c b9c91fd6 a4bf4670 d710b1e8 f80c48a8 8601f02f c3deefb9 bbf8229e 29fa1575 6a8e75aa 6a8e75aa 977263bc 6a8e75aa 7d590826",
		["b5d84f05"] = "2b215108 20cd1d0f b481ffc9 6084bfc6 8e76f8c8 d0b08f5b 29c8615c 20cd1d0f b0872b52 dfcdf719 4765dd34 fa03624e d78f409a 8d97c4f0 1df10c44 20cd1d0f 1c7aa50c e51d8a56 00724424 272b3928 eca07d87 104966b8 20cd1d0f 54ea9f7d e7754ff1 224a0df5 b39e14c4 97e47563 e61655bb 20cd1d0f fc51867d 5ff7c876 c1f12797 27c1e31b 1a0b2b33 5734ce60 0dfcfca7 d5c4b94c f258f49f 2aa7fb16 f4cdbeba c0a87cc1 6468f017 e5031d8c 6e8c17a4 bd675240 ba270482 79fdc91a f80c48a8 ca48da0d 12b8ac69 14a1bee8 e71cc62c 963fdbb7 ed312dc5 6a8e75aa 1d137568 24d47f3c 49b2c51b 365fe857 6a8e75aa 9587d575 6a8e75aa e9d0f99f 3076988c 733ad758 ce0fb68f 4aafc34c b60bd732 6a5ab244 530f2c69 20cd1d0f f0e3f39f 9470d276 7830e2d8 5f86a9cd 559a8b08 b4a1edc0 4d6cb3ff f70b0c4f 8c959a55 f76be948 8f41b275 7676ec7c 968b5dfd 6a8e75aa d65a35e9 5f0b7eec a2b41fca 4c3f7dfa 267d3ad7 7bee8f50 7b787181 72f23da8 118a722a 6a8e75aa 754df234 78e2e509 514bc83e 91e61f4f c95b4bc4 1b62353a df1f5414 85ee37bf 6a8e75aa d178b79b 9b9ff6de 6a8e75aa 43a6c0bd e9d2131a 6a8e75aa 73b4a192 8f145984 fb962ab7 23f563d5 0c4d7961 23205798 f390ea9c 9dae9dc4 4142fd39 dc48a174 618e75a1 4142fd39 6a8e75aa defdec4e 9f712bee 49499c3b 8b55aa86 a449e666 0b1d0211 b2770640 29f541f2 1d986f22 df1f5414 bf5561f8 6a8e75aa 10d0ba33 fd0119cb bf5561f8 6a8e75aa d8297aee c5e0276d 8e9e80de 6206674f fd0119cb bf5561f8 6a8e75aa 24c3b202 6a8e75aa 7903339d c6ab66c7 d40efdc5 de217225 c04ab646 c4980112 61c9eaef 7451b236 df1f5414 bc04c08d 6a8e75aa 10d0ba33 fd0119cb bc04c08d 6a8e75aa d439469d 7d927886 2074a21d 57cfd669 421c88fd f0bb7d4f d6180d0f f859659e 6a8e75aa 24b1ccb2 4142fd39 8e9e80de 594e8787 fd0119cb af24b84b 6a8e75aa ac8e0cfe 6a8e75aa a3afdaa5 49b2c51b 233ebf22 8ce00902 e1cd7b87 f98c9df9 6a8e75aa 3568a3c0 6a8e75aa 5ac5ac02 f4edfdbd 2cea0d33 022b0c16 47265215 290472b7 6a8e75aa 2572d6c9 589875ab 6a8e75aa 1955d073 80688e1b 9745f046 7ec1b6f0 8d49220a 4a82b174 6a8e75aa 650afa71 421dc96c 65154905 8a0442c2 6a8e75aa 6a8e75aa 12136b6b 6a8e75aa 0541b4a6 26cea3a2 2cdff234 55695e43 dfc92629 3568a3c0 6a8e75aa eb60d48d d3a92601 8691796a f65ebf3f f68d12d7 3568a3c0 6a8e75aa 15a8ddab 5a880eb6 5e5ffecc 1a9391b2 2dcd64e3 98bfbe16 d439469d 0bfe420b bc1811f7 6ccb25eb 52b4eb2e dec6adc2 443881a9 6a8e75aa 26d778cc ac74a732 797f95ff 45476c56 ba49cb52 6a8e75aa ac8e0cfe 6a8e75aa c44be57c afdb80eb 1c7d32b7 16e70128 eb9ddfb6 6a8e75aa 963fdbb7 5f573553 6dc9c134 42d9fdc7 05a1fa5d 875a44ab 49b2c51b d6ffa5bd 60681a7c 6a8e75aa 9f547310 eb2fb9cf e221d644 af750ee8 f0a37126 55e7ba50 85ee37bf 6a8e75aa 60a86f44 5dc7f220 65c14f2b 34ab3e62 45ec06d3 aca2853b 85ee37bf 6a8e75aa 1f7fe4fc 5f7ccc1c 3dc8d6bb 6a8e75aa dd8bf467 bfe08afc dad67ca4 94e56058 85ee37bf 6a8e75aa 15e9a4ba 1de63370 65c14f2b f802cf32 8313613d e26a66fd 4142fd39 6a8e75aa 30d0c636 87b0e493 f4e448b3 b84f4ad2 3e21ee4f bde44b67 f9537a8f 942b88bf 68f84115 fe3e929f baea68c1 70d84d97 6f8a63eb 8e27f0ff 2b94b75d 52437efa fbd79661 85e81833 6022c430 034e10ce 53d99248 d75454a3 066ea55f 185a2aea 4ffc77af 6a8e75aa 3432afd1 4f404cfc 6c91cb3a a8b58c18 e10e45cf e684a5b4 21c250fb 9b4fbbf4 11769f70 a8b58c18 6a8e75aa 4942b779 c19f3a20 29aad4b6 975b8ad2 a6e72ce5 248738d0 f1623060 ed9c843a 6a8e75aa 60fbc6e0 8dcbc7b6 f1623060 03d4bdb9 6ce4d80c f5d23839 bdbf5bf0 85bf3195 6a8e75aa 53d09205 15a8ddab 2259b80d 3a005ebc c575e715 470f0a71 fa61d721 6a8e75aa 16db469b 4bb4b818 f1623060 92deac30 3ef86a17 7c7e63cd f5acd332 d546941d 38398ab9 767dd27f d067cf8b e9cab02a f0270773 7c96162e 248738d0 426c2bf2 97a5e111 3fabc91d 160d5bea 9f547310 44686528 6a8e75aa 6a8e75aa 4142fd39 927ddced 6a8e75aa 61ec3a2f c2c899db da10f589 ac6de176 92deac30 6a8e75aa 1ef986f0 f52600f5 fb8e6211 bc04c08d 6a8e75aa d5303d6b d997b3b5 7584beaa 955aaeba 48d32275 e3537494 6a8e75aa 5dc7f220 2aa424e3 7dbdc1e1 bdbf5bf0 f6723a05 6a8e75aa 6a8e75aa 6a8e75aa 7f5dc62b 7584beaa 6a8e75aa beeb841d f6d9a952 d9f31298 7132e079 f200cc68 c791f7d4 6a8e75aa 54745419 6a8e75aa bf6c7b80 cdfa0b41 10c380f0 2a2123e6 e139b3c0 feeee46e 6a8e75aa 958faad4 4f44031c 6a8e75aa 8d417798 ab4fec3c f93d5bae b31b5633 6a8e75aa 015cce71 b157b7f5 58079c7f 0d3e1d44",
		["c40fe1b3"] = "1b995adc 96066051 e733822c 6a388463 b357926b 49b2c51b 8d301cd7 41e1255b 9d2186b6 5b88c48a 8509ee28 71195580 7369a92d fa65ef90 a5ad6bdc 5c1aaa4f 34f56f5c 1a064af1 021c8d10 f1da7d7d 0de6b468 a58260a7 27c8e01c 3de3dcba 00aa585b 6a8e75aa 349b6d1b 358404c3 6a8e75aa 5a4a4b34 6a8e75aa",
		["db4c16d8"] = "662ea531 f16852cd fa76c7c5 6f43a606 1cf4ba69 44f98f29 8f269f16 6a8e75aa 9fc3c0c0 8fa69177 aa5f4ab9 c0c24f05 6a8e75aa 6a8e75aa c76d0cb4 20cd1d0f 674430bf 2b3b79f2 1ff6d57f bcacea1e 289c2c87 cc21cad6 8c2ae597 ff4989a2 7132c7ee ef10ebf7 3098c15f 23873069 c2efbc30 471b4c26 1f87d7a7 7d51b023 85ee37bf 6a8e75aa 98bc3cc6 8dcbc7b6 c863ecfa 6a8e75aa 6a8e75aa 09b8e058 4adae112 4dc10082 70ae08ad c83b07ce fdecab4d fad95d23 870aee06 f9237ee6 b31025c8 c7bb1694 7d16d120 9ba0f388 06a72756 dff43f39 be145507 71978316 b7526f45 6a8e75aa 89380803 4648e66c 1b00a7e6 595ac888 4280f3b4 d98b2d1c 6a8e75aa 472c1066 9910f592 abe8e280 d26ff3ee 6a8e75aa 6a8e75aa d9919c8a cd340af4 358706a0 13f5f501 ec1720ee 87dd9e9e 9910f592 7577c6ef e8dfed9b b2ccca17 7ebe7c71 3dca7a7d 85a7bfdb 94d312ab 31c48a3f bc04c08d 6a8e75aa 8d5e13a7 c3099d81 7627d8f1 6a8e75aa b2890ca6 20cd234a 8db21a97 9206fd0a 09201db2 7627d8f1 6a8e75aa 8acf7454 b07b1170 6a8e75aa 582f38de f502fb56 42a54e43 e4d049e6 9cb00c57 fb9b2b29 791ee20c 80c3675a 2f4ef028 7bd5653b cd5a55c7 d636f5e3 0df2bb73 0ff1dfc4 4142fd39 4142fd39 3b0c3bd9 202a03d6 f0ac8f36 c8828948 d3f6bab8 ea0859ff 6a8e75aa e6dc2db7 24ff64c8 6a8e75aa 88be53b3 256f545a 245151f6 6a8e75aa 836eae2e dc14b258 4735b1c1 9b6b837d be2cc6ec d655bf60 b3f7017c 3075b2dc 4142fd39 fcb5162d 2d2595e1 6d7cab0d bc04c08d 6a8e75aa 6ecdb87c c3f07f56 0ad72f75 08552835 67a84083 0192fb15 6d7cab0d 6a8e75aa d0ee1731 6a8e75aa 55537dbb 4548428e 3e7dceeb 2033ca59 401c5c57 a107f719 6a8e75aa d4d674be c259940e ec68f34d ec1f8dca d35b3226 38d1db7d 14869b67 db61a1f7 86902fe9 885bfd08 4d3c9431 7c6b8271 a107f719 6a8e75aa 581fd65b d655bf60 e1b38797 d13e0595 4142fd39 d9a9ab85 bdbf5bf0 4207341d 6347f0fc 9ec73f6e d576b610 71205dd1 6a8e75aa f1ee52d5 a1382dad 6a8e75aa 436f95dc fbe1708d 6a8e75aa e1861bef 4cc5494f 39eeaabc d55f2d94 09548e29 43616d90 036540ae 6a8e75aa f855ab84 6a8e75aa 1e3c639f ac76c8e9 dfd0c6e7 ba793897 14d3a1b8 b4c0a6aa 8dcbc7b6 b0095976 a107f719 6a8e75aa 977263bc 6a8e75aa fa9329cb 7de2b239 46f8fc83 a66b4569 6292109e 42a54e43 3e7dceeb 0b5c9f18 fdb7e067 e51df417 d0136e34 352459cc 6a8e75aa 4142fd39 720c0d79 d0d06229 d77924fb 90024819 820a3e83 6a8e75aa 4142fd39 4142fd39 4b616dde 6a8e75aa 67747202 df18f65c 5dcf575c f5db9444 f80c48a8",
		["db814ea0"] = "a988a55c d8325701 7f266ff1 992706b7 7fafe1e8 5b607995 53fdbdad a1187f11 e21201d0 da7ad802 2e36ebcc ebb382d4 d452d547 6a8e75aa 9d654399 f62c8e2c 6a8e75aa bc04c08d 6a8e75aa fb7eb15f bfdc7613 a2492d95 c1b917e7 4f9841d5 48d3e274 6a8e75aa a13457ac 53de0022 74c31e21 b180cf43 34030392 b23735f4 2e5eada2 6a8e75aa 5c7460b1 6a8e75aa 6d79129f 112332cc 03a723ee 31674855 72b5b112 78255439 1bcab2b2 88a4df2b 8fc1d859 6a8e75aa b121784d c2925631 6a8e75aa 6a8e75aa c6aa700d d0484969 6655bce5 5d84bc50 29342ddd 7cea4f67 b41ae7fa 02c0eb54 f20f36c5 b44e1ae2 bdbf5bf0 d9a4aea3 6a8e75aa 6a8e75aa 165ff28a 564e25bc 6a8e75aa bdbf5bf0 59518d34 2c868c41 67b78604 4c016e7e f20f36c5 e0bffa44 bdbf5bf0 fce62ef5 6a8e75aa 6a8e75aa 165ff28a 767c7b32 6a8e75aa 6a8e75aa 6a8e75aa 31749745 b5b5841c 3c2b103b 78a6a635 28f95976 35a7c374 1dcfdc7e 6a8e75aa 19734644 71e53447 d862af1b 3d33b898 7b04101f 85db34c3 6a8e75aa 7bb86778 6a8e75aa e9ab980f",
		["fee866b5"] = "50cd2275 8f9d427c 8e483302 6286cea9 c5217a9a 35b34599 f2820c92 620993f1 71ee1071 6a8e75aa 0559570a 6a8e75aa 59114f0d cf30f5ff f20f36c5 5be6ae50 c18fea38 ecdef414 91f07820 6a8e75aa 98bc31d1 343a94b4 6a8e75aa 6a8e75aa 69570fbc 6a8e75aa e749657a e288009c d620d1b4 b4cb0cba 5a42678e 99a4153e a68f96fb 8ffe2ed1 14f66991 6a8e75aa 0bc266d3",
	},
}
]]></string>
                <int64 name="SourceAssetId">-1</int64>
                <BinaryString name="Tags"></BinaryString>
              </Properties>
            </Item>
          </Item>
          <Item class="Folder" referent="26">
            <Properties>
              <string name="Name">Packages</string>
              <BinaryString name="AttributesSerialize"></BinaryString>
              <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
              <Color3 name="IconTint">
                <R>0</R>
                <G>0</G>
                <B>0</B>
              </Color3>
              <bool name="DefinesCapabilities">false</bool>
              <BinaryString name="SerializedOverrides">
              </BinaryString>
              <int64 name="SourceAssetId">-1</int64>
              <BinaryString name="Tags"></BinaryString>
            </Properties>
            <Item class="ModuleScript" referent="27">
              <Properties>
                <string name="Name">Promise</string>
                <BinaryString name="AttributesSerialize"></BinaryString>
                <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
                <ContentId name="LinkedSource">
                  <null>
                  </null>
                </ContentId>
                <bool name="DefinesCapabilities">false</bool>
                <BinaryString name="SerializedOverrides">
                </BinaryString>
                <string name="Source"><![CDATA[--[[
	An implementation of Promises similar to Promise/A+.
]]

local ERROR_NON_PROMISE_IN_LIST = "Non-promise value passed into %s at index %s"
local ERROR_NON_LIST = "Please pass a list of promises to %s"
local ERROR_NON_FUNCTION = "Please pass a handler function to %s!"
local MODE_KEY_METATABLE = { __mode = "k" }

local function isCallable(value)
	if type(value) == "function" then
		return true
	end

	if type(value) == "table" then
		local metatable = getmetatable(value)
		if metatable and type(rawget(metatable, "__call")) == "function" then
			return true
		end
	end

	return false
end

--[[
	Creates an enum dictionary with some metamethods to prevent common mistakes.
]]
local function makeEnum(enumName, members)
	local enum = {}

	for _, memberName in ipairs(members) do
		enum[memberName] = memberName
	end

	return setmetatable(enum, {
		__index = function(_, k)
			error(string.format("%s is not in %s!", k, enumName), 2)
		end,
		__newindex = function()
			error(string.format("Creating new members in %s is not allowed!", enumName), 2)
		end,
	})
end

--[=[
	An object to represent runtime errors that occur during execution.
	Promises that experience an error like this will be rejected with
	an instance of this object.

	@class Error
]=]
local Error
do
	Error = {
		Kind = makeEnum("Promise.Error.Kind", {
			"ExecutionError",
			"AlreadyCancelled",
			"NotResolvedInTime",
			"TimedOut",
		}),
	}
	Error.__index = Error

	function Error.new(options, parent)
		options = options or {}
		return setmetatable({
			error = tostring(options.error) or "[This error has no error text.]",
			trace = options.trace,
			context = options.context,
			kind = options.kind,
			parent = parent,
			createdTick = os.clock(),
			createdTrace = debug.traceback(),
		}, Error)
	end

	function Error.is(anything)
		if type(anything) == "table" then
			local metatable = getmetatable(anything)

			if type(metatable) == "table" then
				return rawget(anything, "error") ~= nil and type(rawget(metatable, "extend")) == "function"
			end
		end

		return false
	end

	function Error.isKind(anything, kind)
		assert(kind ~= nil, "Argument #2 to Promise.Error.isKind must not be nil")

		return Error.is(anything) and anything.kind == kind
	end

	function Error:extend(options)
		options = options or {}

		options.kind = options.kind or self.kind

		return Error.new(options, self)
	end

	function Error:getErrorChain()
		local runtimeErrors = { self }

		while runtimeErrors[#runtimeErrors].parent do
			table.insert(runtimeErrors, runtimeErrors[#runtimeErrors].parent)
		end

		return runtimeErrors
	end

	function Error:__tostring()
		local errorStrings = {
			string.format("-- Promise.Error(%s) --", self.kind or "?"),
		}

		for _, runtimeError in ipairs(self:getErrorChain()) do
			table.insert(
				errorStrings,
				table.concat({
					runtimeError.trace or runtimeError.error,
					runtimeError.context,
				}, "\n")
			)
		end

		return table.concat(errorStrings, "\n")
	end
end

--[[
	Packs a number of arguments into a table and returns its length.

	Used to cajole varargs without dropping sparse values.
]]
local function pack(...)
	return select("#", ...), { ... }
end

--[[
	Returns first value (success), and packs all following values.
]]
local function packResult(success, ...)
	return success, select("#", ...), { ... }
end

local function makeErrorHandler(traceback)
	assert(traceback ~= nil, "traceback is nil")

	return function(err)
		-- If the error object is already a table, forward it directly.
		-- Should we extend the error here and add our own trace?

		if type(err) == "table" then
			return err
		end

		return Error.new({
			error = err,
			kind = Error.Kind.ExecutionError,
			trace = debug.traceback(tostring(err), 2),
			context = "Promise created at:\n\n" .. traceback,
		})
	end
end

--[[
	Calls a Promise executor with error handling.
]]
local function runExecutor(traceback, callback, ...)
	return packResult(xpcall(callback, makeErrorHandler(traceback), ...))
end

--[[
	Creates a function that invokes a callback with correct error handling and
	resolution mechanisms.
]]
local function createAdvancer(traceback, callback, resolve, reject)
	return function(...)
		local ok, resultLength, result = runExecutor(traceback, callback, ...)

		if ok then
			resolve(unpack(result, 1, resultLength))
		else
			reject(result[1])
		end
	end
end

local function isEmpty(t)
	return next(t) == nil
end

--[=[
	An enum value used to represent the Promise's status.
	@interface Status
	@tag enum
	@within Promise
	.Started "Started" -- The Promise is executing, and not settled yet.
	.Resolved "Resolved" -- The Promise finished successfully.
	.Rejected "Rejected" -- The Promise was rejected.
	.Cancelled "Cancelled" -- The Promise was cancelled before it finished.
]=]
--[=[
	@prop Status Status
	@within Promise
	@readonly
	@tag enums
	A table containing all members of the `Status` enum, e.g., `Promise.Status.Resolved`.
]=]
--[=[
	A Promise is an object that represents a value that will exist in the future, but doesn't right now.
	Promises allow you to then attach callbacks that can run once the value becomes available (known as *resolving*),
	or if an error has occurred (known as *rejecting*).

	@class Promise
	@__index prototype
]=]
local Promise = {
	Error = Error,
	Status = makeEnum("Promise.Status", { "Started", "Resolved", "Rejected", "Cancelled" }),
	_getTime = os.clock,
	_timeEvent = game:GetService("RunService").Heartbeat,
	_unhandledRejectionCallbacks = {},
}
Promise.prototype = {}
Promise.__index = Promise.prototype

function Promise._new(traceback, callback, parent)
	if parent ~= nil and not Promise.is(parent) then
		error("Argument #2 to Promise.new must be a promise or nil", 2)
	end

	local self = {
		-- The executor thread.
		_thread = nil,

		-- Used to locate where a promise was created
		_source = traceback,

		_status = Promise.Status.Started,

		-- A table containing a list of all results, whether success or failure.
		-- Only valid if _status is set to something besides Started
		_values = nil,

		-- Lua doesn't like sparse arrays very much, so we explicitly store the
		-- length of _values to handle middle nils.
		_valuesLength = -1,

		-- Tracks if this Promise has no error observers..
		_unhandledRejection = true,

		-- Queues representing functions we should invoke when we update!
		_queuedResolve = {},
		_queuedReject = {},
		_queuedFinally = {},

		-- The function to run when/if this promise is cancelled.
		_cancellationHook = nil,

		-- The "parent" of this promise in a promise chain. Required for
		-- cancellation propagation upstream.
		_parent = parent,

		-- Consumers are Promises that have chained onto this one.
		-- We track them for cancellation propagation downstream.
		_consumers = setmetatable({}, MODE_KEY_METATABLE),
	}

	if parent and parent._status == Promise.Status.Started then
		parent._consumers[self] = true
	end

	setmetatable(self, Promise)

	local function resolve(...)
		self:_resolve(...)
	end

	local function reject(...)
		self:_reject(...)
	end

	local function onCancel(cancellationHook)
		if cancellationHook then
			if self._status == Promise.Status.Cancelled then
				cancellationHook()
			else
				self._cancellationHook = cancellationHook
			end
		end

		return self._status == Promise.Status.Cancelled
	end

	self._thread = coroutine.create(function()
		local ok, _, result = runExecutor(self._source, callback, resolve, reject, onCancel)

		if not ok then
			reject(result[1])
		end
	end)

	task.spawn(self._thread)

	return self
end

--[=[
	Construct a new Promise that will be resolved or rejected with the given callbacks.

	If you `resolve` with a Promise, it will be chained onto.

	You can safely yield within the executor function and it will not block the creating thread.

	```lua
	local myFunction()
		return Promise.new(function(resolve, reject, onCancel)
			wait(1)
			resolve("Hello world!")
		end)
	end

	myFunction():andThen(print)
	```

	You do not need to use `pcall` within a Promise. Errors that occur during execution will be caught and turned into a rejection automatically. If `error()` is called with a table, that table will be the rejection value. Otherwise, string errors will be converted into `Promise.Error(Promise.Error.Kind.ExecutionError)` objects for tracking debug information.

	You may register an optional cancellation hook by using the `onCancel` argument:

	* This should be used to abort any ongoing operations leading up to the promise being settled.
	* Call the `onCancel` function with a function callback as its only argument to set a hook which will in turn be called when/if the promise is cancelled.
	* `onCancel` returns `true` if the Promise was already cancelled when you called `onCancel`.
	* Calling `onCancel` with no argument will not override a previously set cancellation hook, but it will still return `true` if the Promise is currently cancelled.
	* You can set the cancellation hook at any time before resolving.
	* When a promise is cancelled, calls to `resolve` or `reject` will be ignored, regardless of if you set a cancellation hook or not.

	:::caution
	If the Promise is cancelled, the `executor` thread is closed with `coroutine.close` after the cancellation hook is called.

	You must perform any cleanup code in the cancellation hook: any time your executor yields, it **may never resume**.
	:::

	@param executor (resolve: (...: any) -> (), reject: (...: any) -> (), onCancel: (abortHandler?: () -> ()) -> boolean) -> ()
	@return Promise
]=]
function Promise.new(executor)
	return Promise._new(debug.traceback(nil, 2), executor)
end

function Promise:__tostring()
	return string.format("Promise(%s)", self._status)
end

--[=[
	The same as [Promise.new](/api/Promise#new), except execution begins after the next `Heartbeat` event.

	This is a spiritual replacement for `spawn`, but it does not suffer from the same [issues](https://eryn.io/gist/3db84579866c099cdd5bb2ff37947cec) as `spawn`.

	```lua
	local function waitForChild(instance, childName, timeout)
	  return Promise.defer(function(resolve, reject)
		local child = instance:WaitForChild(childName, timeout)

		;(child and resolve or reject)(child)
	  end)
	end
	```

	@param executor (resolve: (...: any) -> (), reject: (...: any) -> (), onCancel: (abortHandler?: () -> ()) -> boolean) -> ()
	@return Promise
]=]
function Promise.defer(executor)
	local traceback = debug.traceback(nil, 2)
	local promise
	promise = Promise._new(traceback, function(resolve, reject, onCancel)
		local connection
		connection = Promise._timeEvent:Connect(function()
			connection:Disconnect()
			local ok, _, result = runExecutor(traceback, executor, resolve, reject, onCancel)

			if not ok then
				reject(result[1])
			end
		end)
	end)

	return promise
end

-- Backwards compatibility
Promise.async = Promise.defer

--[=[
	Creates an immediately resolved Promise with the given value.

	```lua
	-- Example using Promise.resolve to deliver cached values:
	function getSomething(name)
		if cache[name] then
			return Promise.resolve(cache[name])
		else
			return Promise.new(function(resolve, reject)
				local thing = getTheThing()
				cache[name] = thing

				resolve(thing)
			end)
		end
	end
	```

	@param ... any
	@return Promise<...any>
]=]
function Promise.resolve(...)
	local length, values = pack(...)
	return Promise._new(debug.traceback(nil, 2), function(resolve)
		resolve(unpack(values, 1, length))
	end)
end

--[=[
	Creates an immediately rejected Promise with the given value.

	:::caution
	Something needs to consume this rejection (i.e. `:catch()` it), otherwise it will emit an unhandled Promise rejection warning on the next frame. Thus, you should not create and store rejected Promises for later use. Only create them on-demand as needed.
	:::

	@param ... any
	@return Promise<...any>
]=]
function Promise.reject(...)
	local length, values = pack(...)
	return Promise._new(debug.traceback(nil, 2), function(_, reject)
		reject(unpack(values, 1, length))
	end)
end

--[[
	Runs a non-promise-returning function as a Promise with the
  given arguments.
]]
function Promise._try(traceback, callback, ...)
	local valuesLength, values = pack(...)

	return Promise._new(traceback, function(resolve)
		resolve(callback(unpack(values, 1, valuesLength)))
	end)
end

--[=[
	Begins a Promise chain, calling a function and returning a Promise resolving with its return value. If the function errors, the returned Promise will be rejected with the error. You can safely yield within the Promise.try callback.

	:::info
	`Promise.try` is similar to [Promise.promisify](#promisify), except the callback is invoked immediately instead of returning a new function.
	:::

	```lua
	Promise.try(function()
		return math.random(1, 2) == 1 and "ok" or error("Oh an error!")
	end)
		:andThen(function(text)
			print(text)
		end)
		:catch(function(err)
			warn("Something went wrong")
		end)
	```

	@param callback (...: T...) -> ...any
	@param ... T... -- Additional arguments passed to `callback`
	@return Promise
]=]
function Promise.try(callback, ...)
	return Promise._try(debug.traceback(nil, 2), callback, ...)
end

--[[
	Returns a new promise that:
		* is resolved when all input promises resolve
		* is rejected if ANY input promises reject
]]
function Promise._all(traceback, promises, amount)
	if type(promises) ~= "table" then
		error(string.format(ERROR_NON_LIST, "Promise.all"), 3)
	end

	-- We need to check that each value is a promise here so that we can produce
	-- a proper error rather than a rejected promise with our error.
	for i, promise in pairs(promises) do
		if not Promise.is(promise) then
			error(string.format(ERROR_NON_PROMISE_IN_LIST, "Promise.all", tostring(i)), 3)
		end
	end

	-- If there are no values then return an already resolved promise.
	if #promises == 0 or amount == 0 then
		return Promise.resolve({})
	end

	return Promise._new(traceback, function(resolve, reject, onCancel)
		-- An array to contain our resolved values from the given promises.
		local resolvedValues = {}
		local newPromises = {}

		-- Keep a count of resolved promises because just checking the resolved
		-- values length wouldn't account for promises that resolve with nil.
		local resolvedCount = 0
		local rejectedCount = 0
		local done = false

		local function cancel()
			for _, promise in ipairs(newPromises) do
				promise:cancel()
			end
		end

		-- Called when a single value is resolved and resolves if all are done.
		local function resolveOne(i, ...)
			if done then
				return
			end

			resolvedCount = resolvedCount + 1

			if amount == nil then
				resolvedValues[i] = ...
			else
				resolvedValues[resolvedCount] = ...
			end

			if resolvedCount >= (amount or #promises) then
				done = true
				resolve(resolvedValues)
				cancel()
			end
		end

		onCancel(cancel)

		-- We can assume the values inside `promises` are all promises since we
		-- checked above.
		for i, promise in ipairs(promises) do
			newPromises[i] = promise:andThen(function(...)
				resolveOne(i, ...)
			end, function(...)
				rejectedCount = rejectedCount + 1

				if amount == nil or #promises - rejectedCount < amount then
					cancel()
					done = true

					reject(...)
				end
			end)
		end

		if done then
			cancel()
		end
	end)
end

--[=[
	Accepts an array of Promises and returns a new promise that:
	* is resolved after all input promises resolve.
	* is rejected if *any* input promises reject.

	:::info
	Only the first return value from each promise will be present in the resulting array.
	:::

	After any input Promise rejects, all other input Promises that are still pending will be cancelled if they have no other consumers.

	```lua
	local promises = {
		returnsAPromise("example 1"),
		returnsAPromise("example 2"),
		returnsAPromise("example 3"),
	}

	return Promise.all(promises)
	```

	@param promises {Promise<T>}
	@return Promise<{T}>
]=]
function Promise.all(promises)
	return Promise._all(debug.traceback(nil, 2), promises)
end

--[=[
	Folds an array of values or promises into a single value. The array is traversed sequentially.

	The reducer function can return a promise or value directly. Each iteration receives the resolved value from the previous, and the first receives your defined initial value.

	The folding will stop at the first rejection encountered.
	```lua
	local basket = {"blueberry", "melon", "pear", "melon"}
	Promise.fold(basket, function(cost, fruit)
		if fruit == "blueberry" then
			return cost -- blueberries are free!
		else
			-- call a function that returns a promise with the fruit price
			return fetchPrice(fruit):andThen(function(fruitCost)
				return cost + fruitCost
			end)
		end
	end, 0)
	```

	@since v3.1.0
	@param list {T | Promise<T>}
	@param reducer (accumulator: U, value: T, index: number) -> U | Promise<U>
	@param initialValue U
]=]
function Promise.fold(list, reducer, initialValue)
	assert(type(list) == "table", "Bad argument #1 to Promise.fold: must be a table")
	assert(isCallable(reducer), "Bad argument #2 to Promise.fold: must be a function")

	local accumulator = Promise.resolve(initialValue)
	return Promise.each(list, function(resolvedElement, i)
		accumulator = accumulator:andThen(function(previousValueResolved)
			return reducer(previousValueResolved, resolvedElement, i)
		end)
	end):andThen(function()
		return accumulator
	end)
end

--[=[
	Accepts an array of Promises and returns a Promise that is resolved as soon as `count` Promises are resolved from the input array. The resolved array values are in the order that the Promises resolved in. When this Promise resolves, all other pending Promises are cancelled if they have no other consumers.

	`count` 0 results in an empty array. The resultant array will never have more than `count` elements.

	```lua
	local promises = {
		returnsAPromise("example 1"),
		returnsAPromise("example 2"),
		returnsAPromise("example 3"),
	}

	return Promise.some(promises, 2) -- Only resolves with first 2 promises to resolve
	```

	@param promises {Promise<T>}
	@param count number
	@return Promise<{T}>
]=]
function Promise.some(promises, count)
	assert(type(count) == "number", "Bad argument #2 to Promise.some: must be a number")

	return Promise._all(debug.traceback(nil, 2), promises, count)
end

--[=[
	Accepts an array of Promises and returns a Promise that is resolved as soon as *any* of the input Promises resolves. It will reject only if *all* input Promises reject. As soon as one Promises resolves, all other pending Promises are cancelled if they have no other consumers.

	Resolves directly with the value of the first resolved Promise. This is essentially [[Promise.some]] with `1` count, except the Promise resolves with the value directly instead of an array with one element.

	```lua
	local promises = {
		returnsAPromise("example 1"),
		returnsAPromise("example 2"),
		returnsAPromise("example 3"),
	}

	return Promise.any(promises) -- Resolves with first value to resolve (only rejects if all 3 rejected)
	```

	@param promises {Promise<T>}
	@return Promise<T>
]=]
function Promise.any(promises)
	return Promise._all(debug.traceback(nil, 2), promises, 1):andThen(function(values)
		return values[1]
	end)
end

--[=[
	Accepts an array of Promises and returns a new Promise that resolves with an array of in-place Statuses when all input Promises have settled. This is equivalent to mapping `promise:finally` over the array of Promises.

	```lua
	local promises = {
		returnsAPromise("example 1"),
		returnsAPromise("example 2"),
		returnsAPromise("example 3"),
	}

	return Promise.allSettled(promises)
	```

	@param promises {Promise<T>}
	@return Promise<{Status}>
]=]
function Promise.allSettled(promises)
	if type(promises) ~= "table" then
		error(string.format(ERROR_NON_LIST, "Promise.allSettled"), 2)
	end

	-- We need to check that each value is a promise here so that we can produce
	-- a proper error rather than a rejected promise with our error.
	for i, promise in pairs(promises) do
		if not Promise.is(promise) then
			error(string.format(ERROR_NON_PROMISE_IN_LIST, "Promise.allSettled", tostring(i)), 2)
		end
	end

	-- If there are no values then return an already resolved promise.
	if #promises == 0 then
		return Promise.resolve({})
	end

	return Promise._new(debug.traceback(nil, 2), function(resolve, _, onCancel)
		-- An array to contain our resolved values from the given promises.
		local fates = {}
		local newPromises = {}

		-- Keep a count of resolved promises because just checking the resolved
		-- values length wouldn't account for promises that resolve with nil.
		local finishedCount = 0

		-- Called when a single value is resolved and resolves if all are done.
		local function resolveOne(i, ...)
			finishedCount = finishedCount + 1

			fates[i] = ...

			if finishedCount >= #promises then
				resolve(fates)
			end
		end

		onCancel(function()
			for _, promise in ipairs(newPromises) do
				promise:cancel()
			end
		end)

		-- We can assume the values inside `promises` are all promises since we
		-- checked above.
		for i, promise in ipairs(promises) do
			newPromises[i] = promise:finally(function(...)
				resolveOne(i, ...)
			end)
		end
	end)
end

--[=[
	Accepts an array of Promises and returns a new promise that is resolved or rejected as soon as any Promise in the array resolves or rejects.

	:::warning
	If the first Promise to settle from the array settles with a rejection, the resulting Promise from `race` will reject.

	If you instead want to tolerate rejections, and only care about at least one Promise resolving, you should use [Promise.any](#any) or [Promise.some](#some) instead.
	:::

	All other Promises that don't win the race will be cancelled if they have no other consumers.

	```lua
	local promises = {
		returnsAPromise("example 1"),
		returnsAPromise("example 2"),
		returnsAPromise("example 3"),
	}

	return Promise.race(promises) -- Only returns 1st value to resolve or reject
	```

	@param promises {Promise<T>}
	@return Promise<T>
]=]
function Promise.race(promises)
	assert(type(promises) == "table", string.format(ERROR_NON_LIST, "Promise.race"))

	for i, promise in pairs(promises) do
		assert(Promise.is(promise), string.format(ERROR_NON_PROMISE_IN_LIST, "Promise.race", tostring(i)))
	end

	return Promise._new(debug.traceback(nil, 2), function(resolve, reject, onCancel)
		local newPromises = {}
		local finished = false

		local function cancel()
			for _, promise in ipairs(newPromises) do
				promise:cancel()
			end
		end

		local function finalize(callback)
			return function(...)
				cancel()
				finished = true
				return callback(...)
			end
		end

		if onCancel(finalize(reject)) then
			return
		end

		for i, promise in ipairs(promises) do
			newPromises[i] = promise:andThen(finalize(resolve), finalize(reject))
		end

		if finished then
			cancel()
		end
	end)
end

--[=[
	Iterates serially over the given an array of values, calling the predicate callback on each value before continuing.

	If the predicate returns a Promise, we wait for that Promise to resolve before moving on to the next item
	in the array.

	:::info
	`Promise.each` is similar to `Promise.all`, except the Promises are ran in order instead of all at once.

	But because Promises are eager, by the time they are created, they're already running. Thus, we need a way to defer creation of each Promise until a later time.

	The predicate function exists as a way for us to operate on our data instead of creating a new closure for each Promise. If you would prefer, you can pass in an array of functions, and in the predicate, call the function and return its return value.
	:::

	```lua
	Promise.each({
		"foo",
		"bar",
		"baz",
		"qux"
	}, function(value, index)
		return Promise.delay(1):andThen(function()
		print(("%d) Got %s!"):format(index, value))
		end)
	end)

	--[[
		(1 second passes)
		> 1) Got foo!
		(1 second passes)
		> 2) Got bar!
		(1 second passes)
		> 3) Got baz!
		(1 second passes)
		> 4) Got qux!
	]]
	```

	If the Promise a predicate returns rejects, the Promise from `Promise.each` is also rejected with the same value.

	If the array of values contains a Promise, when we get to that point in the list, we wait for the Promise to resolve before calling the predicate with the value.

	If a Promise in the array of values is already Rejected when `Promise.each` is called, `Promise.each` rejects with that value immediately (the predicate callback will never be called even once). If a Promise in the list is already Cancelled when `Promise.each` is called, `Promise.each` rejects with `Promise.Error(Promise.Error.Kind.AlreadyCancelled`). If a Promise in the array of values is Started at first, but later rejects, `Promise.each` will reject with that value and iteration will not continue once iteration encounters that value.

	Returns a Promise containing an array of the returned/resolved values from the predicate for each item in the array of values.

	If this Promise returned from `Promise.each` rejects or is cancelled for any reason, the following are true:
	- Iteration will not continue.
	- Any Promises within the array of values will now be cancelled if they have no other consumers.
	- The Promise returned from the currently active predicate will be cancelled if it hasn't resolved yet.

	@since 3.0.0
	@param list {T | Promise<T>}
	@param predicate (value: T, index: number) -> U | Promise<U>
	@return Promise<{U}>
]=]
function Promise.each(list, predicate)
	assert(type(list) == "table", string.format(ERROR_NON_LIST, "Promise.each"))
	assert(isCallable(predicate), string.format(ERROR_NON_FUNCTION, "Promise.each"))

	return Promise._new(debug.traceback(nil, 2), function(resolve, reject, onCancel)
		local results = {}
		local promisesToCancel = {}

		local cancelled = false

		local function cancel()
			for _, promiseToCancel in ipairs(promisesToCancel) do
				promiseToCancel:cancel()
			end
		end

		onCancel(function()
			cancelled = true

			cancel()
		end)

		-- We need to preprocess the list of values and look for Promises.
		-- If we find some, we must register our andThen calls now, so that those Promises have a consumer
		-- from us registered. If we don't do this, those Promises might get cancelled by something else
		-- before we get to them in the series because it's not possible to tell that we plan to use it
		-- unless we indicate it here.

		local preprocessedList = {}

		for index, value in ipairs(list) do
			if Promise.is(value) then
				if value:getStatus() == Promise.Status.Cancelled then
					cancel()
					return reject(Error.new({
						error = "Promise is cancelled",
						kind = Error.Kind.AlreadyCancelled,
						context = string.format(
							"The Promise that was part of the array at index %d passed into Promise.each was already cancelled when Promise.each began.\n\nThat Promise was created at:\n\n%s",
							index,
							value._source
						),
					}))
				elseif value:getStatus() == Promise.Status.Rejected then
					cancel()
					return reject(select(2, value:await()))
				end

				-- Chain a new Promise from this one so we only cancel ours
				local ourPromise = value:andThen(function(...)
					return ...
				end)

				table.insert(promisesToCancel, ourPromise)
				preprocessedList[index] = ourPromise
			else
				preprocessedList[index] = value
			end
		end

		for index, value in ipairs(preprocessedList) do
			if Promise.is(value) then
				local success
				success, value = value:await()

				if not success then
					cancel()
					return reject(value)
				end
			end

			if cancelled then
				return
			end

			local predicatePromise = Promise.resolve(predicate(value, index))

			table.insert(promisesToCancel, predicatePromise)

			local success, result = predicatePromise:await()

			if not success then
				cancel()
				return reject(result)
			end

			results[index] = result
		end

		resolve(results)
	end)
end

--[=[
	Checks whether the given object is a Promise via duck typing. This only checks if the object is a table and has an `andThen` method.

	@param object any
	@return boolean -- `true` if the given `object` is a Promise.
]=]
function Promise.is(object)
	if type(object) ~= "table" then
		return false
	end

	local objectMetatable = getmetatable(object)

	if objectMetatable == Promise then
		-- The Promise came from this library.
		return true
	elseif objectMetatable == nil then
		-- No metatable, but we should still chain onto tables with andThen methods
		return isCallable(object.andThen)
	elseif
		type(objectMetatable) == "table"
		and type(rawget(objectMetatable, "__index")) == "table"
		and isCallable(rawget(rawget(objectMetatable, "__index"), "andThen"))
	then
		-- Maybe this came from a different or older Promise library.
		return true
	end

	return false
end

--[=[
	Wraps a function that yields into one that returns a Promise.

	Any errors that occur while executing the function will be turned into rejections.

	:::info
	`Promise.promisify` is similar to [Promise.try](#try), except the callback is returned as a callable function instead of being invoked immediately.
	:::

	```lua
	local sleep = Promise.promisify(wait)

	sleep(1):andThen(print)
	```

	```lua
	local isPlayerInGroup = Promise.promisify(function(player, groupId)
		return player:IsInGroup(groupId)
	end)
	```

	@param callback (...: any) -> ...any
	@return (...: any) -> Promise
]=]
function Promise.promisify(callback)
	return function(...)
		return Promise._try(debug.traceback(nil, 2), callback, ...)
	end
end

--[=[
	Returns a Promise that resolves after `seconds` seconds have passed. The Promise resolves with the actual amount of time that was waited.

	This function is **not** a wrapper around `wait`. `Promise.delay` uses a custom scheduler which provides more accurate timing. As an optimization, cancelling this Promise instantly removes the task from the scheduler.

	:::warning
	Passing `NaN`, infinity, or a number less than 1/60 is equivalent to passing 1/60.
	:::

	```lua
		Promise.delay(5):andThenCall(print, "This prints after 5 seconds")
	```

	@function delay
	@within Promise
	@param seconds number
	@return Promise<number>
]=]
do
	-- uses a sorted doubly linked list (queue) to achieve O(1) remove operations and O(n) for insert

	-- the initial node in the linked list
	local first
	local connection

	function Promise.delay(seconds)
		assert(type(seconds) == "number", "Bad argument #1 to Promise.delay, must be a number.")
		-- If seconds is -INF, INF, NaN, or less than 1 / 60, assume seconds is 1 / 60.
		-- This mirrors the behavior of wait()
		if not (seconds >= 1 / 60) or seconds == math.huge then
			seconds = 1 / 60
		end

		return Promise._new(debug.traceback(nil, 2), function(resolve, _, onCancel)
			local startTime = Promise._getTime()
			local endTime = startTime + seconds

			local node = {
				resolve = resolve,
				startTime = startTime,
				endTime = endTime,
			}

			if connection == nil then -- first is nil when connection is nil
				first = node
				connection = Promise._timeEvent:Connect(function()
					local threadStart = Promise._getTime()

					while first ~= nil and first.endTime < threadStart do
						local current = first
						first = current.next

						if first == nil then
							connection:Disconnect()
							connection = nil
						else
							first.previous = nil
						end

						current.resolve(Promise._getTime() - current.startTime)
					end
				end)
			else -- first is non-nil
				if first.endTime < endTime then -- if `node` should be placed after `first`
					-- we will insert `node` between `current` and `next`
					-- (i.e. after `current` if `next` is nil)
					local current = first
					local next = current.next

					while next ~= nil and next.endTime < endTime do
						current = next
						next = current.next
					end

					-- `current` must be non-nil, but `next` could be `nil` (i.e. last item in list)
					current.next = node
					node.previous = current

					if next ~= nil then
						node.next = next
						next.previous = node
					end
				else
					-- set `node` to `first`
					node.next = first
					first.previous = node
					first = node
				end
			end

			onCancel(function()
				-- remove node from queue
				local next = node.next

				if first == node then
					if next == nil then -- if `node` is the first and last
						connection:Disconnect()
						connection = nil
					else -- if `node` is `first` and not the last
						next.previous = nil
					end
					first = next
				else
					local previous = node.previous
					-- since `node` is not `first`, then we know `previous` is non-nil
					previous.next = next

					if next ~= nil then
						next.previous = previous
					end
				end
			end)
		end)
	end
end

--[=[
	Returns a new Promise that resolves if the chained Promise resolves within `seconds` seconds, or rejects if execution time exceeds `seconds`. The chained Promise will be cancelled if the timeout is reached.

	Rejects with `rejectionValue` if it is non-nil. If a `rejectionValue` is not given, it will reject with a `Promise.Error(Promise.Error.Kind.TimedOut)`. This can be checked with [[Error.isKind]].

	```lua
	getSomething():timeout(5):andThen(function(something)
		-- got something and it only took at max 5 seconds
	end):catch(function(e)
		-- Either getting something failed or the time was exceeded.

		if Promise.Error.isKind(e, Promise.Error.Kind.TimedOut) then
			warn("Operation timed out!")
		else
			warn("Operation encountered an error!")
		end
	end)
	```

	Sugar for:

	```lua
	Promise.race({
		Promise.delay(seconds):andThen(function()
			return Promise.reject(
				rejectionValue == nil
				and Promise.Error.new({ kind = Promise.Error.Kind.TimedOut })
				or rejectionValue
			)
		end),
		promise
	})
	```

	@param seconds number
	@param rejectionValue? any -- The value to reject with if the timeout is reached
	@return Promise
]=]
function Promise.prototype:timeout(seconds, rejectionValue)
	local traceback = debug.traceback(nil, 2)

	return Promise.race({
		Promise.delay(seconds):andThen(function()
			return Promise.reject(rejectionValue == nil and Error.new({
				kind = Error.Kind.TimedOut,
				error = "Timed out",
				context = string.format(
					"Timeout of %d seconds exceeded.\n:timeout() called at:\n\n%s",
					seconds,
					traceback
				),
			}) or rejectionValue)
		end),
		self,
	})
end

--[=[
	Returns the current Promise status.

	@return Status
]=]
function Promise.prototype:getStatus()
	return self._status
end

--[[
	Creates a new promise that receives the result of this promise.

	The given callbacks are invoked depending on that result.
]]
function Promise.prototype:_andThen(traceback, successHandler, failureHandler)
	self._unhandledRejection = false

	-- If we are already cancelled, we return a cancelled Promise
	if self._status == Promise.Status.Cancelled then
		local promise = Promise.new(function() end)
		promise:cancel()

		return promise
	end

	-- Create a new promise to follow this part of the chain
	return Promise._new(traceback, function(resolve, reject, onCancel)
		-- Our default callbacks just pass values onto the next promise.
		-- This lets success and failure cascade correctly!

		local successCallback = resolve
		if successHandler then
			successCallback = createAdvancer(traceback, successHandler, resolve, reject)
		end

		local failureCallback = reject
		if failureHandler then
			failureCallback = createAdvancer(traceback, failureHandler, resolve, reject)
		end

		if self._status == Promise.Status.Started then
			-- If we haven't resolved yet, put ourselves into the queue
			table.insert(self._queuedResolve, successCallback)
			table.insert(self._queuedReject, failureCallback)

			onCancel(function()
				-- These are guaranteed to exist because the cancellation handler is guaranteed to only
				-- be called at most once
				if self._status == Promise.Status.Started then
					table.remove(self._queuedResolve, table.find(self._queuedResolve, successCallback))
					table.remove(self._queuedReject, table.find(self._queuedReject, failureCallback))
				end
			end)
		elseif self._status == Promise.Status.Resolved then
			-- This promise has already resolved! Trigger success immediately.
			successCallback(unpack(self._values, 1, self._valuesLength))
		elseif self._status == Promise.Status.Rejected then
			-- This promise died a terrible death! Trigger failure immediately.
			failureCallback(unpack(self._values, 1, self._valuesLength))
		end
	end, self)
end

--[=[
	Chains onto an existing Promise and returns a new Promise.

	:::warning
	Within the failure handler, you should never assume that the rejection value is a string. Some rejections within the Promise library are represented by [[Error]] objects. If you want to treat it as a string for debugging, you should call `tostring` on it first.
	:::

	You can return a Promise from the success or failure handler and it will be chained onto.

	Calling `andThen` on a cancelled Promise returns a cancelled Promise.

	:::tip
	If the Promise returned by `andThen` is cancelled, `successHandler` and `failureHandler` will not run.

	To run code no matter what, use [Promise:finally].
	:::

	@param successHandler (...: any) -> ...any
	@param failureHandler? (...: any) -> ...any
	@return Promise<...any>
]=]
function Promise.prototype:andThen(successHandler, failureHandler)
	assert(successHandler == nil or isCallable(successHandler), string.format(ERROR_NON_FUNCTION, "Promise:andThen"))
	assert(failureHandler == nil or isCallable(failureHandler), string.format(ERROR_NON_FUNCTION, "Promise:andThen"))

	return self:_andThen(debug.traceback(nil, 2), successHandler, failureHandler)
end

--[=[
	Shorthand for `Promise:andThen(nil, failureHandler)`.

	Returns a Promise that resolves if the `failureHandler` worked without encountering an additional error.

	:::warning
	Within the failure handler, you should never assume that the rejection value is a string. Some rejections within the Promise library are represented by [[Error]] objects. If you want to treat it as a string for debugging, you should call `tostring` on it first.
	:::

	Calling `catch` on a cancelled Promise returns a cancelled Promise.

	:::tip
	If the Promise returned by `catch` is cancelled,  `failureHandler` will not run.

	To run code no matter what, use [Promise:finally].
	:::

	@param failureHandler (...: any) -> ...any
	@return Promise<...any>
]=]
function Promise.prototype:catch(failureHandler)
	assert(failureHandler == nil or isCallable(failureHandler), string.format(ERROR_NON_FUNCTION, "Promise:catch"))
	return self:_andThen(debug.traceback(nil, 2), nil, failureHandler)
end

--[=[
	Similar to [Promise.andThen](#andThen), except the return value is the same as the value passed to the handler. In other words, you can insert a `:tap` into a Promise chain without affecting the value that downstream Promises receive.

	```lua
		getTheValue()
		:tap(print)
		:andThen(function(theValue)
			print("Got", theValue, "even though print returns nil!")
		end)
	```

	If you return a Promise from the tap handler callback, its value will be discarded but `tap` will still wait until it resolves before passing the original value through.

	@param tapHandler (...: any) -> ...any
	@return Promise<...any>
]=]
function Promise.prototype:tap(tapHandler)
	assert(isCallable(tapHandler), string.format(ERROR_NON_FUNCTION, "Promise:tap"))
	return self:_andThen(debug.traceback(nil, 2), function(...)
		local callbackReturn = tapHandler(...)

		if Promise.is(callbackReturn) then
			local length, values = pack(...)
			return callbackReturn:andThen(function()
				return unpack(values, 1, length)
			end)
		end

		return ...
	end)
end

--[=[
	Attaches an `andThen` handler to this Promise that calls the given callback with the predefined arguments. The resolved value is discarded.

	```lua
		promise:andThenCall(someFunction, "some", "arguments")
	```

	This is sugar for

	```lua
		promise:andThen(function()
		return someFunction("some", "arguments")
		end)
	```

	@param callback (...: any) -> any
	@param ...? any -- Additional arguments which will be passed to `callback`
	@return Promise
]=]
function Promise.prototype:andThenCall(callback, ...)
	assert(isCallable(callback), string.format(ERROR_NON_FUNCTION, "Promise:andThenCall"))
	local length, values = pack(...)
	return self:_andThen(debug.traceback(nil, 2), function()
		return callback(unpack(values, 1, length))
	end)
end

--[=[
	Attaches an `andThen` handler to this Promise that discards the resolved value and returns the given value from it.

	```lua
		promise:andThenReturn("some", "values")
	```

	This is sugar for

	```lua
		promise:andThen(function()
			return "some", "values"
		end)
	```

	:::caution
	Promises are eager, so if you pass a Promise to `andThenReturn`, it will begin executing before `andThenReturn` is reached in the chain. Likewise, if you pass a Promise created from [[Promise.reject]] into `andThenReturn`, it's possible that this will trigger the unhandled rejection warning. If you need to return a Promise, it's usually best practice to use [[Promise.andThen]].
	:::

	@param ... any -- Values to return from the function
	@return Promise
]=]
function Promise.prototype:andThenReturn(...)
	local length, values = pack(...)
	return self:_andThen(debug.traceback(nil, 2), function()
		return unpack(values, 1, length)
	end)
end

--[=[
	Cancels this promise, preventing the promise from resolving or rejecting. Does not do anything if the promise is already settled.

	Cancellations will propagate upwards and downwards through chained promises.

	Promises will only be cancelled if all of their consumers are also cancelled. This is to say that if you call `andThen` twice on the same promise, and you cancel only one of the child promises, it will not cancel the parent promise until the other child promise is also cancelled.

	```lua
		promise:cancel()
	```
]=]
function Promise.prototype:cancel()
	if self._status ~= Promise.Status.Started then
		return
	end

	self._status = Promise.Status.Cancelled

	if self._cancellationHook then
		self._cancellationHook()
	end

	coroutine.close(self._thread)

	if self._parent then
		self._parent:_consumerCancelled(self)
	end

	for child in pairs(self._consumers) do
		child:cancel()
	end

	self:_finalize()
end

--[[
	Used to decrease the number of consumers by 1, and if there are no more,
	cancel this promise.
]]
function Promise.prototype:_consumerCancelled(consumer)
	if self._status ~= Promise.Status.Started then
		return
	end

	self._consumers[consumer] = nil

	if next(self._consumers) == nil then
		self:cancel()
	end
end

--[[
	Used to set a handler for when the promise resolves, rejects, or is
	cancelled.
]]
function Promise.prototype:_finally(traceback, finallyHandler)
	self._unhandledRejection = false

	local promise = Promise._new(traceback, function(resolve, reject, onCancel)
		local handlerPromise

		onCancel(function()
			-- The finally Promise is not a proper consumer of self. We don't care about the resolved value.
			-- All we care about is running at the end. Therefore, if self has no other consumers, it's safe to
			-- cancel. We don't need to hold out cancelling just because there's a finally handler.
			self:_consumerCancelled(self)

			if handlerPromise then
				handlerPromise:cancel()
			end
		end)

		local finallyCallback = resolve
		if finallyHandler then
			finallyCallback = function(...)
				local callbackReturn = finallyHandler(...)

				if Promise.is(callbackReturn) then
					handlerPromise = callbackReturn

					callbackReturn
						:finally(function(status)
							if status ~= Promise.Status.Rejected then
								resolve(self)
							end
						end)
						:catch(function(...)
							reject(...)
						end)
				else
					resolve(self)
				end
			end
		end

		if self._status == Promise.Status.Started then
			-- The promise is not settled, so queue this.
			table.insert(self._queuedFinally, finallyCallback)
		else
			-- The promise already settled or was cancelled, run the callback now.
			finallyCallback(self._status)
		end
	end)

	return promise
end

--[=[
	Set a handler that will be called regardless of the promise's fate. The handler is called when the promise is
	resolved, rejected, *or* cancelled.

	Returns a new Promise that:
	- resolves with the same values that this Promise resolves with.
	- rejects with the same values that this Promise rejects with.
	- is cancelled if this Promise is cancelled.

	If the value you return from the handler is a Promise:
	- We wait for the Promise to resolve, but we ultimately discard the resolved value.
	- If the returned Promise rejects, the Promise returned from `finally` will reject with the rejected value from the
	*returned* promise.
	- If the `finally` Promise is cancelled, and you returned a Promise from the handler, we cancel that Promise too.

	Otherwise, the return value from the `finally` handler is entirely discarded.

	:::note Cancellation
	As of Promise v4, `Promise:finally` does not count as a consumer of the parent Promise for cancellation purposes.
	This means that if all of a Promise's consumers are cancelled and the only remaining callbacks are finally handlers,
	the Promise is cancelled and the finally callbacks run then and there.

	Cancellation still propagates through the `finally` Promise though: if you cancel the `finally` Promise, it can cancel
	its parent Promise if it had no other consumers. Likewise, if the parent Promise is cancelled, the `finally` Promise
	will also be cancelled.
	:::

	```lua
	local thing = createSomething()

	doSomethingWith(thing)
		:andThen(function()
			print("It worked!")
			-- do something..
		end)
		:catch(function()
			warn("Oh no it failed!")
		end)
		:finally(function()
			-- either way, destroy thing

			thing:Destroy()
		end)

	```

	@param finallyHandler (status: Status) -> ...any
	@return Promise<...any>
]=]
function Promise.prototype:finally(finallyHandler)
	assert(finallyHandler == nil or isCallable(finallyHandler), string.format(ERROR_NON_FUNCTION, "Promise:finally"))
	return self:_finally(debug.traceback(nil, 2), finallyHandler)
end

--[=[
	Same as `andThenCall`, except for `finally`.

	Attaches a `finally` handler to this Promise that calls the given callback with the predefined arguments.

	@param callback (...: any) -> any
	@param ...? any -- Additional arguments which will be passed to `callback`
	@return Promise
]=]
function Promise.prototype:finallyCall(callback, ...)
	assert(isCallable(callback), string.format(ERROR_NON_FUNCTION, "Promise:finallyCall"))
	local length, values = pack(...)
	return self:_finally(debug.traceback(nil, 2), function()
		return callback(unpack(values, 1, length))
	end)
end

--[=[
	Attaches a `finally` handler to this Promise that discards the resolved value and returns the given value from it.

	```lua
		promise:finallyReturn("some", "values")
	```

	This is sugar for

	```lua
		promise:finally(function()
			return "some", "values"
		end)
	```

	@param ... any -- Values to return from the function
	@return Promise
]=]
function Promise.prototype:finallyReturn(...)
	local length, values = pack(...)
	return self:_finally(debug.traceback(nil, 2), function()
		return unpack(values, 1, length)
	end)
end

--[=[
	Yields the current thread until the given Promise completes. Returns the Promise's status, followed by the values that the promise resolved or rejected with.

	@yields
	@return Status -- The Status representing the fate of the Promise
	@return ...any -- The values the Promise resolved or rejected with.
]=]
function Promise.prototype:awaitStatus()
	self._unhandledRejection = false

	if self._status == Promise.Status.Started then
		local thread = coroutine.running()

		self
			:finally(function()
				task.spawn(thread)
			end)
			-- The finally promise can propagate rejections, so we attach a catch handler to prevent the unhandled
			-- rejection warning from appearing
			:catch(
				function() end
			)

		coroutine.yield()
	end

	if self._status == Promise.Status.Resolved then
		return self._status, unpack(self._values, 1, self._valuesLength)
	elseif self._status == Promise.Status.Rejected then
		return self._status, unpack(self._values, 1, self._valuesLength)
	end

	return self._status
end

local function awaitHelper(status, ...)
	return status == Promise.Status.Resolved, ...
end

--[=[
	Yields the current thread until the given Promise completes. Returns true if the Promise resolved, followed by the values that the promise resolved or rejected with.

	:::caution
	If the Promise gets cancelled, this function will return `false`, which is indistinguishable from a rejection. If you need to differentiate, you should use [[Promise.awaitStatus]] instead.
	:::

	```lua
		local worked, value = getTheValue():await()

	if worked then
		print("got", value)
	else
		warn("it failed")
	end
	```

	@yields
	@return boolean -- `true` if the Promise successfully resolved
	@return ...any -- The values the Promise resolved or rejected with.
]=]
function Promise.prototype:await()
	return awaitHelper(self:awaitStatus())
end

local function expectHelper(status, ...)
	if status ~= Promise.Status.Resolved then
		error((...) == nil and "Expected Promise rejected with no value." or (...), 3)
	end

	return ...
end

--[=[
	Yields the current thread until the given Promise completes. Returns the values that the promise resolved with.

	```lua
	local worked = pcall(function()
		print("got", getTheValue():expect())
	end)

	if not worked then
		warn("it failed")
	end
	```

	This is essentially sugar for:

	```lua
	select(2, assert(promise:await()))
	```

	**Errors** if the Promise rejects or gets cancelled.

	@error any -- Errors with the rejection value if this Promise rejects or gets cancelled.
	@yields
	@return ...any -- The values the Promise resolved with.
]=]
function Promise.prototype:expect()
	return expectHelper(self:awaitStatus())
end

-- Backwards compatibility
Promise.prototype.awaitValue = Promise.prototype.expect

--[[
	Intended for use in tests.

	Similar to await(), but instead of yielding if the promise is unresolved,
	_unwrap will throw. This indicates an assumption that a promise has
	resolved.
]]
function Promise.prototype:_unwrap()
	if self._status == Promise.Status.Started then
		error("Promise has not resolved or rejected.", 2)
	end

	local success = self._status == Promise.Status.Resolved

	return success, unpack(self._values, 1, self._valuesLength)
end

function Promise.prototype:_resolve(...)
	if self._status ~= Promise.Status.Started then
		if Promise.is((...)) then
			(...):_consumerCancelled(self)
		end
		return
	end

	-- If the resolved value was a Promise, we chain onto it!
	if Promise.is((...)) then
		-- Without this warning, arguments sometimes mysteriously disappear
		if select("#", ...) > 1 then
			local message = string.format(
				"When returning a Promise from andThen, extra arguments are " .. "discarded! See:\n\n%s",
				self._source
			)
			warn(message)
		end

		local chainedPromise = ...

		local promise = chainedPromise:andThen(function(...)
			self:_resolve(...)
		end, function(...)
			local maybeRuntimeError = chainedPromise._values[1]

			-- Backwards compatibility < v2
			if chainedPromise._error then
				maybeRuntimeError = Error.new({
					error = chainedPromise._error,
					kind = Error.Kind.ExecutionError,
					context = "[No stack trace available as this Promise originated from an older version of the Promise library (< v2)]",
				})
			end

			if Error.isKind(maybeRuntimeError, Error.Kind.ExecutionError) then
				return self:_reject(maybeRuntimeError:extend({
					error = "This Promise was chained to a Promise that errored.",
					trace = "",
					context = string.format(
						"The Promise at:\n\n%s\n...Rejected because it was chained to the following Promise, which encountered an error:\n",
						self._source
					),
				}))
			end

			self:_reject(...)
		end)

		if promise._status == Promise.Status.Cancelled then
			self:cancel()
		elseif promise._status == Promise.Status.Started then
			-- Adopt ourselves into promise for cancellation propagation.
			self._parent = promise
			promise._consumers[self] = true
		end

		return
	end

	self._status = Promise.Status.Resolved
	self._valuesLength, self._values = pack(...)

	-- We assume that these callbacks will not throw errors.
	for _, callback in ipairs(self._queuedResolve) do
		coroutine.wrap(callback)(...)
	end

	self:_finalize()
end

function Promise.prototype:_reject(...)
	if self._status ~= Promise.Status.Started then
		return
	end

	self._status = Promise.Status.Rejected
	self._valuesLength, self._values = pack(...)

	-- If there are any rejection handlers, call those!
	if not isEmpty(self._queuedReject) then
		-- We assume that these callbacks will not throw errors.
		for _, callback in ipairs(self._queuedReject) do
			coroutine.wrap(callback)(...)
		end
	else
		-- At this point, no one was able to observe the error.
		-- An error handler might still be attached if the error occurred
		-- synchronously. We'll wait one tick, and if there are still no
		-- observers, then we should put a message in the console.

		local err = tostring((...))

		coroutine.wrap(function()
			Promise._timeEvent:Wait()

			-- Someone observed the error, hooray!
			if not self._unhandledRejection then
				return
			end

			-- Build a reasonable message
			local message = string.format("Unhandled Promise rejection:\n\n%s\n\n%s", err, self._source)

			for _, callback in ipairs(Promise._unhandledRejectionCallbacks) do
				task.spawn(callback, self, unpack(self._values, 1, self._valuesLength))
			end

			if Promise.TEST then
				-- Don't spam output when we're running tests.
				return
			end

			warn(message)
		end)()
	end

	self:_finalize()
end

--[[
	Calls any :finally handlers. We need this to be a separate method and
	queue because we must call all of the finally callbacks upon a success,
	failure, *and* cancellation.
]]
function Promise.prototype:_finalize()
	for _, callback in ipairs(self._queuedFinally) do
		-- Purposefully not passing values to callbacks here, as it could be the
		-- resolved values, or rejected errors. If the developer needs the values,
		-- they should use :andThen or :catch explicitly.
		coroutine.wrap(callback)(self._status)
	end

	self._queuedFinally = nil
	self._queuedReject = nil
	self._queuedResolve = nil

	-- Clear references to other Promises to allow gc
	if not Promise.TEST then
		self._parent = nil
		self._consumers = nil
	end

	task.defer(coroutine.close, self._thread)
end

--[=[
	Chains a Promise from this one that is resolved if this Promise is already resolved, and rejected if it is not resolved at the time of calling `:now()`. This can be used to ensure your `andThen` handler occurs on the same frame as the root Promise execution.

	```lua
	doSomething()
		:now()
		:andThen(function(value)
			print("Got", value, "synchronously.")
		end)
	```

	If this Promise is still running, Rejected, or Cancelled, the Promise returned from `:now()` will reject with the `rejectionValue` if passed, otherwise with a `Promise.Error(Promise.Error.Kind.NotResolvedInTime)`. This can be checked with [[Error.isKind]].

	@param rejectionValue? any -- The value to reject with if the Promise isn't resolved
	@return Promise
]=]
function Promise.prototype:now(rejectionValue)
	local traceback = debug.traceback(nil, 2)
	if self._status == Promise.Status.Resolved then
		return self:_andThen(traceback, function(...)
			return ...
		end)
	else
		return Promise.reject(rejectionValue == nil and Error.new({
			kind = Error.Kind.NotResolvedInTime,
			error = "This Promise was not resolved in time for :now()",
			context = ":now() was called at:\n\n" .. traceback,
		}) or rejectionValue)
	end
end

--[=[
	Repeatedly calls a Promise-returning function up to `times` number of times, until the returned Promise resolves.

	If the amount of retries is exceeded, the function will return the latest rejected Promise.

	```lua
	local function canFail(a, b, c)
		return Promise.new(function(resolve, reject)
			-- do something that can fail

			local failed, thing = doSomethingThatCanFail(a, b, c)

			if failed then
				reject("it failed")
			else
				resolve(thing)
			end
		end)
	end

	local MAX_RETRIES = 10
	local value = Promise.retry(canFail, MAX_RETRIES, "foo", "bar", "baz") -- args to send to canFail
	```

	@since 3.0.0
	@param callback (...: P) -> Promise<T>
	@param times number
	@param ...? P
	@return Promise<T>
]=]
function Promise.retry(callback, times, ...)
	assert(isCallable(callback), "Parameter #1 to Promise.retry must be a function")
	assert(type(times) == "number", "Parameter #2 to Promise.retry must be a number")

	local args, length = { ... }, select("#", ...)

	return Promise.resolve(callback(...)):catch(function(...)
		if times > 0 then
			return Promise.retry(callback, times - 1, unpack(args, 1, length))
		else
			return Promise.reject(...)
		end
	end)
end

--[=[
	Repeatedly calls a Promise-returning function up to `times` number of times, waiting `seconds` seconds between each
	retry, until the returned Promise resolves.

	If the amount of retries is exceeded, the function will return the latest rejected Promise.

	@since v3.2.0
	@param callback (...: P) -> Promise<T>
	@param times number
	@param seconds number
	@param ...? P
	@return Promise<T>
]=]
function Promise.retryWithDelay(callback, times, seconds, ...)
	assert(isCallable(callback), "Parameter #1 to Promise.retry must be a function")
	assert(type(times) == "number", "Parameter #2 (times) to Promise.retry must be a number")
	assert(type(seconds) == "number", "Parameter #3 (seconds) to Promise.retry must be a number")

	local args, length = { ... }, select("#", ...)

	return Promise.resolve(callback(...)):catch(function(...)
		if times > 0 then
			Promise.delay(seconds):await()

			return Promise.retryWithDelay(callback, times - 1, seconds, unpack(args, 1, length))
		else
			return Promise.reject(...)
		end
	end)
end

--[=[
	Converts an event into a Promise which resolves the next time the event fires.

	The optional `predicate` callback, if passed, will receive the event arguments and should return `true` or `false`, based on if this fired event should resolve the Promise or not. If `true`, the Promise resolves. If `false`, nothing happens and the predicate will be rerun the next time the event fires.

	The Promise will resolve with the event arguments.

	:::tip
	This function will work given any object with a `Connect` method. This includes all Roblox events.
	:::

	```lua
	-- Creates a Promise which only resolves when `somePart` is touched
	-- by a part named `"Something specific"`.
	return Promise.fromEvent(somePart.Touched, function(part)
		return part.Name == "Something specific"
	end)
	```

	@since 3.0.0
	@param event Event -- Any object with a `Connect` method. This includes all Roblox events.
	@param predicate? (...: P) -> boolean -- A function which determines if the Promise should resolve with the given value, or wait for the next event to check again.
	@return Promise<P>
]=]
function Promise.fromEvent(event, predicate)
	predicate = predicate or function()
		return true
	end

	return Promise._new(debug.traceback(nil, 2), function(resolve, _, onCancel)
		local connection
		local shouldDisconnect = false

		local function disconnect()
			connection:Disconnect()
			connection = nil
		end

		-- We use shouldDisconnect because if the callback given to Connect is called before
		-- Connect returns, connection will still be nil. This happens with events that queue up
		-- events when there's nothing connected, such as RemoteEvents

		connection = event:Connect(function(...)
			local callbackValue = predicate(...)

			if callbackValue == true then
				resolve(...)

				if connection then
					disconnect()
				else
					shouldDisconnect = true
				end
			elseif type(callbackValue) ~= "boolean" then
				error("Promise.fromEvent predicate should always return a boolean")
			end
		end)

		if shouldDisconnect and connection then
			return disconnect()
		end

		onCancel(disconnect)
	end)
end

--[=[
	Registers a callback that runs when an unhandled rejection happens. An unhandled rejection happens when a Promise
	is rejected, and the rejection is not observed with `:catch`.

	The callback is called with the actual promise that rejected, followed by the rejection values.

	@since v3.2.0
	@param callback (promise: Promise, ...: any) -- A callback that runs when an unhandled rejection happens.
	@return () -> () -- Function that unregisters the `callback` when called
]=]
function Promise.onUnhandledRejection(callback)
	table.insert(Promise._unhandledRejectionCallbacks, callback)

	return function()
		local index = table.find(Promise._unhandledRejectionCallbacks, callback)

		if index then
			table.remove(Promise._unhandledRejectionCallbacks, index)
		end
	end
end

return Promise
]]></string>
                <int64 name="SourceAssetId">-1</int64>
                <BinaryString name="Tags"></BinaryString>
              </Properties>
            </Item>
          </Item>
          <Item class="ModuleScript" referent="28">
            <Properties>
              <string name="Name">Schema</string>
              <BinaryString name="AttributesSerialize"></BinaryString>
              <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
              <ContentId name="LinkedSource">
                <null>
                </null>
              </ContentId>
              <bool name="DefinesCapabilities">false</bool>
              <BinaryString name="SerializedOverrides">
              </BinaryString>
              <string name="Source"><![CDATA[-- Schema.lua - the options a game sets for Datastore & Purchase System 3.0,
-- in the Kernel's Schema language (docs/kernel-api.md, section 8). The
-- game's values live in ReplicatedStorage.RBLXEssentials.Config.
-- DatastorePurchaseSystem, a module no update replaces; it returns only
-- what the game changes, and everything else is the default below.
-- T.config.reference("DatastorePurchaseSystem") prints this as a reference.
--
-- Every option is shared (plan B1, rulings 1 to 4): 2.x kept them all in
-- ReplicatedStorage, where every client could read them, none is a secret,
-- and Easy Pet System's shop needs FreeMode on clients.
--
-- DataStoreVersion, StudioDataStoreVersion and DefaultData are pinned (plan
-- B0, ruling 7): they say which DataStore holds each player's profile and
-- what every profile gains, so T.migrate always writes them into the game's
-- module, even equal to these defaults, and never changes them, forced or
-- not. The defaults are what 2.1.0 shipped, so a new game uses the stores a
-- 2.x game used. The defaults of DataStoreVersion, StudioDataStoreVersion
-- and DefaultData never change in any 3.x release: since plan B3a a new
-- game's config module (T.config.create's template) states all three with
-- these defaults, uncommented, as T.migrate states a migrated game's, so
-- for that game these defaults are its store and its starting profile and
-- a later default never moves them (tests/identity.spec.luau).
--
-- It accepts every value 2.x ran (Migrations/FromV2 converts them): any
-- text as a store version ("" and spaces included), the same text for
-- Studio and live (2.0.0 shipped that; the server warns at start), any
-- finite AutoSaveInterval (2.x had no minimum: the scheduler's own floor of
-- about 45 seconds stays in code), and any DefaultData tree (keys with
-- spaces or dots, top-level values, Stats without Cash or Wins).
--
-- Self-contained on purpose: Tools loads this module as a clone with no
-- Parent, so it uses no `script` and requires nothing.
return function(S)
	-- Meta is the profile's own record (SchemaVersion and the session fence):
	-- 2.x ignored a Default Data group with that name.
	local function checkDefaultData(tree)
		if tree.Meta ~= nil then
			return { { field = "Meta", message = "is the profile's own record (SchemaVersion and the session), which DefaultData cannot set: remove it" } }
		end
		return {}
	end

	local function checkInterval(value)
		if value == math.huge or value == -math.huge then
			return { { message = "must be a finite number of seconds" } }
		end
		return {}
	end

	return S.config({
		DataStoreVersion = S.string({
			default = "v1",
			pinned = true,
			doc = "The live DataStore is PlayerData_<this>. Another value starts every player on an empty profile (their saved data stays in the old store).",
		}),
		StudioDataStoreVersion = S.string({
			default = "v4",
			pinned = true,
			doc = "The DataStore used when you play in Studio is PlayerData_<this>. Keep it different from DataStoreVersion by more than spaces or letter case: when they are equal, Studio tests load and save live profiles, and when they differ only in spaces or case they may (the server warns at start, FreeMode stays off and Wipe Studio Data refuses).",
		}),
		AutoSaveInterval = S.number({
			default = 90,
			doc = "Seconds between automatic saves of each player's profile. Players are also saved when they leave and when the server shuts down, and the scheduler never saves one player more often than about every 45 seconds.",
			check = checkInterval,
			checkDoc = "A finite number of seconds",
		}),
		FreeMode = S.boolean({
			default = false,
			doc = "Test purchases without spending Robux: HasPass is true and GrantProduct runs the product's handler. It only works in Studio, and not while StudioDataStoreVersion equals DataStoreVersion, ignoring spaces and letter case (Studio would use live profiles): in a live server gamepasses and products are always real, and the server warns at start when it is on and ignored.",
		}),
		DefaultData = S.tree({
			default = { Stats = { Cash = 0, Wins = 0 } },
			pinned = true,
			doc = "What a new player's profile starts with. Every saved profile also gains each key it lacks when it loads; a value a player already has is never changed or removed. Replaced whole: list every group you want. Stats (Cash 0, Wins 0), Pets and Purchases are added when you leave them out.",
			check = checkDefaultData,
			checkDoc = "No top-level Meta: that group is the profile's own record",
		}),
	})
end
]]></string>
              <int64 name="SourceAssetId">-1</int64>
              <BinaryString name="Tags"></BinaryString>
            </Properties>
          </Item>
          <Item class="ModuleScript" referent="29">
            <Properties>
              <string name="Name">manifest</string>
              <BinaryString name="AttributesSerialize"></BinaryString>
              <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
              <ContentId name="LinkedSource">
                <null>
                </null>
              </ContentId>
              <bool name="DefinesCapabilities">false</bool>
              <BinaryString name="SerializedOverrides">
              </BinaryString>
              <string name="Source"><![CDATA[return {
	id = "DatastorePurchaseSystem",
	version = "3.0.1",
	deps = {},
}
]]></string>
              <int64 name="SourceAssetId">-1</int64>
              <BinaryString name="Tags"></BinaryString>
            </Properties>
          </Item>
        </Item>
      </Item>
    </Item>
    <Item class="Folder" referent="30">
      <Properties>
        <string name="Name">ServerScriptService</string>
        <BinaryString name="AttributesSerialize"></BinaryString>
        <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
        <Color3 name="IconTint">
          <R>0</R>
          <G>0</G>
          <B>0</B>
        </Color3>
        <bool name="DefinesCapabilities">false</bool>
        <BinaryString name="SerializedOverrides">
        </BinaryString>
        <int64 name="SourceAssetId">-1</int64>
        <BinaryString name="Tags"></BinaryString>
      </Properties>
      <Item class="Script" referent="31">
        <Properties>
          <string name="Name">Kernel</string>
          <BinaryString name="AttributesSerialize"></BinaryString>
          <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
          <bool name="Disabled">false</bool>
          <ContentId name="LinkedSource">
            <null>
            </null>
          </ContentId>
          <token name="RunContext">0</token>
          <bool name="DefinesCapabilities">false</bool>
          <BinaryString name="SerializedOverrides">
          </BinaryString>
          <string name="Source"><![CDATA[-- RBLX Essentials Kernel - server boot. The only Script in the ecosystem.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local StarterGui = game:GetService("StarterGui")
local HttpService = game:GetService("HttpService")

local kernelFolder = ReplicatedStorage:WaitForChild("RBLXEssentials"):WaitForChild("Kernel")
local RootPersistence = require(kernelFolder.UI.RootPersistence)

-- Run first, in its own pcall, before any other require or work: make
-- StarterGui.RBLXEssentials survive character respawns (see
-- UI/RootPersistence.lua) before any character spawns. Guarded: a failure
-- here must never stop the packs from booting, and the piece decision below
-- then uses the Folder root as it is.
local persistOk, persistResult, persistWarnings = pcall(RootPersistence.persist, StarterGui, Instance.new, Enum)
local uiRoot
if persistOk then
	uiRoot = persistResult
	for _, message in ipairs(persistWarnings) do
		warn("[RBLXE] UI root: " .. message)
	end
else
	warn("[RBLXE] could not make StarterGui.RBLXEssentials persistent; menus, HUD buttons and pack interfaces may stop working after a respawn: " .. tostring(persistResult))
	uiRoot = StarterGui:FindFirstChild("RBLXEssentials")
end

local Pieces = require(kernelFolder.UI.Pieces)
local Loader = require(kernelFolder.Loader)
local Version = require(kernelFolder.Version)
local Net = require(kernelFolder.Net)
local SFX = require(kernelFolder.SFX)
local Notify = require(kernelFolder.Notify)
local Signal = require(kernelFolder.Signal)
local Trove = require(kernelFolder.Packages.Trove)
local Schema = require(kernelFolder.Schema)
local Config = require(kernelFolder.Config)
local Contracts = require(kernelFolder.Contracts)

local ssRoot = script.Parent -- ServerScriptService/RBLXEssentials
local rsRoot = ReplicatedStorage.RBLXEssentials
local sharedPacks = rsRoot:WaitForChild("Packs")

rsRoot:SetAttribute("RBLXE_KernelVersion", Version)

-- 3.0 pieces: decide once, here, which copy of every declared interface piece
-- the game uses (a valid StarterGui.RBLXEssentials.Interface override, else the
-- pack's default) and publish it for every client, before any pack boots. The
-- JSON encoding runs inside the same pcall: whatever fails here falls back to
-- LEGACY and never stops the server script before Loader.boot.
-- RT-6: packs whose Pieces declaration is invalid; the Loader fails them.
local pieceFailures = {}
local planOk, encodedOrError = pcall(function()
	local declarations, declarationWarnings, declarationFailures = Pieces.collectDeclarations(sharedPacks, require, Contracts)
	pieceFailures = declarationFailures
	local plan = Pieces.plan({
		declarations = declarations,
		root = uiRoot,
		contracts = Contracts,
	})
	for _, message in ipairs(declarationWarnings) do
		warn("[RBLXE] UI: " .. message)
	end
	for _, message in ipairs(Pieces.strays(StarterGui, uiRoot)) do
		warn("[RBLXE] UI: " .. message)
	end
	for _, line in ipairs(plan.lines) do
		print("[RBLXE] UI: " .. line)
	end
	for _, message in ipairs(plan.warnings) do
		warn("[RBLXE] UI: " .. message)
	end
	return Pieces.encodeDecisions(plan, function(value)
		return HttpService:JSONEncode(value)
	end)
end)
if planOk then
	rsRoot:SetAttribute(Pieces.WINNERS_ATTRIBUTE, encodedOrError)
else
	warn("[RBLXE] UI: piece resolution failed, pieces keep 2.x adoption: " .. tostring(encodedOrError))
	rsRoot:SetAttribute(Pieces.WINNERS_ATTRIBUTE, Pieces.LEGACY)
end

local report = Loader.boot({
	side = "Server",
	sharedPacks = sharedPacks,
	serverPacks = ssRoot:WaitForChild("Packs"),
	kernelApi = { Version = Version, Trove = Trove, Signal = Signal, Net = Net, SFX = SFX, Notify = Notify },
	configLib = Config,
	schemaLib = Schema,
	configRoots = { shared = rsRoot:FindFirstChild("Config"), server = ssRoot:FindFirstChild("Config") },
	pieceFailures = pieceFailures,
	extensions = ssRoot:FindFirstChild("Extensions"),
})

-- RT-5: tell every client which packs did not start here. A reason that may
-- quote server-only values (an invalid configuration, a server module's
-- error) reaches clients as a fixed text (Loader.encodeServerReport); the
-- warnings below print it whole in the server output. The encoding and
-- the attribute write share one pcall: a report that cannot be published
-- must never stop the rest of this boot (the warnings below and
-- RBLXE_ServerBooted).
local reportOk, reportError = pcall(function()
	local encodedReport = Loader.encodeServerReport(report, function(value)
		return HttpService:JSONEncode(value)
	end)
	rsRoot:SetAttribute(Loader.SERVER_REPORT_ATTRIBUTE, encodedReport)
end)
if not reportOk then
	warn("[RBLXE] could not publish the boot report for clients; each client waits 30s for it, then boots every pack: " .. tostring(reportError))
end

print(Loader.formatReport("Server", report))
for id, reason in pairs(report.failed) do
	warn(string.format("[RBLXE] %s failed: %s", id, reason))
end
for id, reason in pairs(report.skipped) do
	warn(string.format("[RBLXE] %s skipped: %s", id, reason))
end
for _, notice in ipairs(report.notices or {}) do
	warn("[RBLXE] " .. notice)
end
ssRoot:SetAttribute("RBLXE_ServerBooted", true)
]]></string>
          <int64 name="SourceAssetId">-1</int64>
          <BinaryString name="Tags"></BinaryString>
        </Properties>
      </Item>
      <Item class="Folder" referent="32">
        <Properties>
          <string name="Name">Packs</string>
          <BinaryString name="AttributesSerialize"></BinaryString>
          <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
          <Color3 name="IconTint">
            <R>0</R>
            <G>0</G>
            <B>0</B>
          </Color3>
          <bool name="DefinesCapabilities">false</bool>
          <BinaryString name="SerializedOverrides">
          </BinaryString>
          <int64 name="SourceAssetId">-1</int64>
          <BinaryString name="Tags"></BinaryString>
        </Properties>
        <Item class="ModuleScript" referent="33">
          <Properties>
            <string name="Name">DatastorePurchaseSystem</string>
            <BinaryString name="AttributesSerialize">AQAAAAwAAABSQkxYRV9IYXNoZXMCRAQAACNyYmx4ZS1oYXNoZXMgdjEgZm52MWEzMiBsZgpSZXBsaWNhdGVkU3RvcmFnZS9QYWNrcy9EYXRhc3RvcmVQdXJjaGFzZVN5c3RlbS9NaWdyYXRpb25zL0Zyb21WMgkwMzkxYTNlMQpSZXBsaWNhdGVkU3RvcmFnZS9QYWNrcy9EYXRhc3RvcmVQdXJjaGFzZVN5c3RlbS9NaWdyYXRpb25zL0tlZXAJMTFmYTJkZGUKUmVwbGljYXRlZFN0b3JhZ2UvUGFja3MvRGF0YXN0b3JlUHVyY2hhc2VTeXN0ZW0vUGFja2FnZXMvUHJvbWlzZQk2ZDUzZjgzNgpSZXBsaWNhdGVkU3RvcmFnZS9QYWNrcy9EYXRhc3RvcmVQdXJjaGFzZVN5c3RlbS9TY2hlbWEJMjNhMTE2ZmQKUmVwbGljYXRlZFN0b3JhZ2UvUGFja3MvRGF0YXN0b3JlUHVyY2hhc2VTeXN0ZW0vbWFuaWZlc3QJYTA1Y2QzYmYKU2VydmVyU2NyaXB0U2VydmljZS9QYWNrcy9EYXRhc3RvcmVQdXJjaGFzZVN5c3RlbQkzYjNjNDdiNwpTZXJ2ZXJTY3JpcHRTZXJ2aWNlL1BhY2tzL0RhdGFzdG9yZVB1cmNoYXNlU3lzdGVtL0FjdGlvbnMvV2lwZVN0dWRpb0RhdGEJMzE1ZDk2ZWEKU2VydmVyU2NyaXB0U2VydmljZS9QYWNrcy9EYXRhc3RvcmVQdXJjaGFzZVN5c3RlbS9Db25maWcJMzJmZDJlOGYKU2VydmVyU2NyaXB0U2VydmljZS9QYWNrcy9EYXRhc3RvcmVQdXJjaGFzZVN5c3RlbS9EYXRhUGF0aAk5YTBlMjI4MApTZXJ2ZXJTY3JpcHRTZXJ2aWNlL1BhY2tzL0RhdGFzdG9yZVB1cmNoYXNlU3lzdGVtL0RhdGFTdG9yZU1hbmFnZXIJODdkOTIyNWUKU2VydmVyU2NyaXB0U2VydmljZS9QYWNrcy9EYXRhc3RvcmVQdXJjaGFzZVN5c3RlbS9EZWZhdWx0RGF0YQkxZDFkODIzNApTZXJ2ZXJTY3JpcHRTZXJ2aWNlL1BhY2tzL0RhdGFzdG9yZVB1cmNoYXNlU3lzdGVtL01pZ3JhdGlvbglmZWU4NjZiNQpTZXJ2ZXJTY3JpcHRTZXJ2aWNlL1BhY2tzL0RhdGFzdG9yZVB1cmNoYXNlU3lzdGVtL1B1cmNoYXNlTWFuYWdlcgk3MWM0NmQzNgpTZXJ2ZXJTY3JpcHRTZXJ2aWNlL1BhY2tzL0RhdGFzdG9yZVB1cmNoYXNlU3lzdGVtL1N0dWRpb1NhZmVJZAkwZTE4YjM1NApTZXJ2ZXJTY3JpcHRTZXJ2aWNlL1BhY2tzL0RhdGFzdG9yZVB1cmNoYXNlU3lzdGVtL1ZhbHVlUmVwbGljYXRvcglkYjgxNGVhMA==</BinaryString>
            <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
            <ContentId name="LinkedSource">
              <null>
              </null>
            </ContentId>
            <bool name="DefinesCapabilities">false</bool>
            <BinaryString name="SerializedOverrides">
            </BinaryString>
            <string name="Source"><![CDATA[-- Datastore & Purchase System - server module.
--
-- 3.0 (plan B1): the configuration is ctx.config (Schema.lua), validated
-- and frozen by the Kernel; there is no 2.x Settings folder. Config.lua
-- turns it into what the managers read, with the Studio rule for FreeMode
-- applied once here; a game's own profile migration is the module
-- ServerScriptService.RBLXEssentials.Extensions.DatastorePurchaseSystem.
-- Migration, looked up before the shipped one. The API, the player data,
-- the stores, the session protocol, the PlayerData replica and the
-- PurchaseCompleted event are 2.1.0's.
--
-- Respawn/kick/backfill wiring below is reconciled against v1's
-- DataStoreSystem/Bootstrap.server.lua line by line (see task-5-report.md for
-- the full diff). Key differences from a naive port:
--   * onPlayerAdded is ONE function (Get -> Kick-on-failure -> connect
--     CharacterAdded/Died -> LoadCharacter), matching v1's shape exactly. A
--     split into two separate PlayerAdded connections (one task.spawn'd for
--     Get+LoadCharacter, one plain for CharacterAdded wiring) would race:
--     if manager:Get ever resolves without yielding, LoadCharacter could run
--     before the CharacterAdded/Died connection existed, silently breaking
--     respawn for that player's first life.
--   * `player:LoadCharacter()` is guarded by `player.Parent ~= nil` - the
--     player can leave while manager:Get is yielding on the DataStore round
--     trip; v1 guards this, the naive port did not.
--   * Humanoid wait is `WaitForChild("Humanoid", 10)` with a nil-guard, not
--     an unbounded wait - matches v1 exactly and avoids a stray indefinite
--     yield inside the CharacterAdded connection.
--   * Kick message text matches v1 verbatim: "Failed to load your data.
--     Please rejoin."
--   * `Players.PlayerRemoving` -> `safeId.CleanupPlayer(player)` is wired
--     (v1 had this; the initial sketch omitted it entirely).
--   * `SaveAllImmediate` on BindToClose is pcall-wrapped, matching v1 (a
--     save failure during shutdown must not throw out of BindToClose).
--
-- Trove split (binding decision from Task 3's review, overrides a single
-- shared trove): DataStoreManager and PurchaseManager each call
-- `trove:Clean()` on THEIR OWN trove during Stop()/SaveAllImmediate. Sharing
-- one trove across all three would mean SaveAllImmediate (called from
-- BindToClose) also disconnects PlayerAdded/PlayerRemoving and
-- PurchaseManager's own lifecycle connections - a blast radius v1 never had,
-- since v1's Bootstrap connections were plain top-level `:Connect` calls, not
-- routed through the manager's cleanup trove. Three separate troves keep each
-- owner's Clean() scoped to only what it should own:
--   managerTrove   -> passed into DataStoreManager.new (owns the Heartbeat
--                     save-scheduler connection; cleaned by Stop()/SaveAllImmediate)
--   purchasesTrove -> passed into PurchaseManager.new (owns
--                     PromptGamePassPurchaseFinished / PlayerRemoving->passCache)
--   packTrove      -> this init's own connections (PlayerAdded/PlayerRemoving
--                     for data-load/respawn/safeId cleanup); never cleaned by
--                     either manager, so it always outlives BindToClose saves
return function(ctx)
	local Players = game:GetService("Players")
	local MarketplaceService = game:GetService("MarketplaceService")
	local RunService = game:GetService("RunService")

	local Promise = require(ctx.folder.Packages.Promise)
	local Config = require(script.Config)
	local DefaultData = require(script.DefaultData)
	-- The game's profile migration first (plan B1, ruling 7): no update
	-- replaces Extensions, so an edit there survives every update.
	local migrationModule, migrationFrom = ctx.extension("Migration", script)
	local Migration = require(migrationModule)
	if type(Migration) ~= "table" or type(Migration.migrate) ~= "function" then
		error(("the Migration module from the %s (%s) must return a table with a migrate(loaded, defaults) function"):format(
			if migrationFrom == "game" then "game" else "pack", migrationModule:GetFullName()), 0)
	end
	if migrationFrom == "game" then
		-- DataStoreManager hands every migrate call the same defaults table and
		-- then writes into what comes back; the shipped migrate deep-copies it.
		-- A game's migrate gets its own deep copy for each profile, so a module
		-- that returns or keeps `defaults` never shares one table between two
		-- players.
		local own, shipped = Migration, require(script.Migration)
		Migration = {
			migrate = function(loaded, defaults)
				return own.migrate(loaded, shipped.deepClone(defaults))
			end,
		}
	end
	local StudioSafeId = require(script.StudioSafeId)
	local ValueReplicator = require(script.ValueReplicator)
	local DataStoreManager = require(script.DataStoreManager)
	local PurchaseManager = require(script.PurchaseManager)
	local DataPath = require(script.DataPath)

	local managerTrove = ctx.kernel.Trove.new()
	local purchasesTrove = ctx.kernel.Trove.new()
	local packTrove = ctx.kernel.Trove.new()

	local config = Config(ctx.config, RunService:IsStudio())
	local defaults = DefaultData.build(ctx.config.DefaultData)
	local safeId = StudioSafeId()

	if config.SameStores then
		local live, studio = ctx.config.DataStoreVersion, ctx.config.StudioDataStoreVersion
		local why = if live == studio
			then ("DataStoreVersion and StudioDataStoreVersion are both %q, so Studio sessions load and save the live profiles of PlayerData_%s (2.0.0 shipped them equal)"):format(live, live)
			else ("DataStoreVersion %q and StudioDataStoreVersion %q differ only in spaces or letter case, which may not make another DataStore, so Studio sessions may load and save the live profiles of PlayerData_%s"):format(live, studio, live)
		warn(("[RBLXE] DatastorePurchaseSystem: %s. To keep Studio tests apart, set another StudioDataStoreVersion in ReplicatedStorage.RBLXEssentials.Config.DatastorePurchaseSystem."):format(why))
	end

	local manager = DataStoreManager.new({
		config = config,
		defaults = defaults,
		safeId = safeId,
		Promise = Promise,
		Migration = Migration,
		Replicator = ValueReplicator,
		trove = managerTrove,
	})

	-- The DataStore this server uses, PlayerData_<version> for Studio or live
	-- (plan B1): a diagnostic on the server module, which no client receives.
	-- Naming it opens nothing.
	ctx.serverFolder:SetAttribute("StoreName", manager:StoreName())

	-- PurchaseCompleted - an OPTIONAL extension point, added 2026-08-25.
	--
	-- It fires (player, kind, id, currencySpent) after every purchase this
	-- pack records as granted, where `kind` is "product" or "gamepass". It
	-- exists so Notifications System can announce purchases WITHOUT either
	-- pack declaring the other in manifest.deps - a declared dependency
	-- would make an optional companion mandatory, since the plugin refuses
	-- to install a pack whose deps are missing. Anyone can FindFirstChild it
	-- and connect; nobody has to.
	--
	-- A BindableEvent rather than a field on the returned API on purpose:
	-- the returned table is only reachable through `ctx.require`, which
	-- asserts the caller declared this pack as a dependency. An Instance is
	-- reachable by path, which is the whole point.
	--
	-- DS publishes a neutral fact and decides nothing about how it is shown.
	-- The wording and the on/off switches live in Notifications System's own
	-- settings, where a buyer looks for them.
	local purchaseCompleted = Instance.new("BindableEvent")
	purchaseCompleted.Name = "PurchaseCompleted"
	purchaseCompleted.Parent = ctx.serverFolder
	packTrove:Add(purchaseCompleted)

	-- FreeMode (plan B1, ruling 4) only takes effect in Studio, and not while
	-- Studio uses the live store (Config.lua). PurchaseManager reads `.Value`
	-- as it read the 2.x BoolValue; the configuration is frozen, so it never
	-- changes while the server runs. Clients (Easy Pet System's shop) read the
	-- same decision from the FreeMode attribute of this pack's
	-- ReplicatedStorage folder.
	local freeModeValue = table.freeze({ Value = config.FreeMode })
	ctx.folder:SetAttribute("FreeMode", config.FreeMode)
	if config.FreeModeIgnored == "not Studio" then
		warn("[DatastorePurchaseSystem] FreeMode is on in Config.DatastorePurchaseSystem, but it only works in Studio: "
			.. "gamepass checks and product grants are real in this server. Set FreeMode = false in ReplicatedStorage.RBLXEssentials.Config.DatastorePurchaseSystem.")
	elseif config.FreeModeIgnored == "same stores" then
		warn(("[DatastorePurchaseSystem] FreeMode is on in Config.DatastorePurchaseSystem, but StudioDataStoreVersion equals DataStoreVersion (ignoring spaces and letter case), so Studio may use the live store PlayerData_%s and FreeMode stays off: it would grant passes and products into real players' profiles. Set another StudioDataStoreVersion to use FreeMode in Studio."):format(
			ctx.config.DataStoreVersion))
	end
	local purchases = PurchaseManager.new({
		manager = manager,
		freeModeValue = freeModeValue,
		trove = purchasesTrove,
		onPurchase = function(player, kind, id, currencySpent)
			purchaseCompleted:Fire(player, kind, id, currencySpent)
		end,
	})

	MarketplaceService.ProcessReceipt = purchases._processReceipt

	-- v1 parity: pack owns character loading
	-- (Datastore & Purchase System/src/ServerScriptService/.../DataStoreSystem/Bootstrap.server.lua)
	Players.CharacterAutoLoads = false

	local function onPlayerAdded(player: Player)
		local ok, err = pcall(function()
			manager:Get(player)
		end)
		if not ok then
			warn(("[RBLXE] DatastorePurchaseSystem: data load failed for %s: %s"):format(player.Name, tostring(err)))
			player:Kick("Failed to load your data. Please rejoin.")
			return
		end

		player.CharacterAdded:Connect(function(character)
			local humanoid = character:WaitForChild("Humanoid", 10) :: Humanoid?
			if not humanoid then return end
			humanoid.Died:Connect(function()
				task.delay(Players.RespawnTime, function()
					if player.Parent then
						player:LoadCharacter()
					end
				end)
			end)
		end)

		-- Player may have left while manager:Get was yielding on the DataStore
		-- round trip; guard against loading a character for a departed player.
		if player.Parent ~= nil then
			player:LoadCharacter()
		end
	end

	packTrove:Connect(Players.PlayerAdded, function(player)
		task.spawn(onPlayerAdded, player)
	end)

	packTrove:Connect(Players.PlayerRemoving, function(player)
		safeId.CleanupPlayer(player)
	end)

	-- Backfill: players already in-game when this pack boots (e.g. Loader
	-- runs after PlayerAdded already fired for them) never get a PlayerAdded
	-- event of their own.
	for _, player in ipairs(Players:GetPlayers()) do
		task.spawn(onPlayerAdded, player)
	end

	game:BindToClose(function()
		pcall(function()
			manager:SaveAllImmediate(2.0)
		end)
	end)

	-- Dot-path convenience layer (2026-08-22; replaces v1's Cash-only
	-- PlayerMutators with path-addressed equivalents - see DataPath.lua).
	-- Paths are the ones the plugin's Default Data editor shows
	-- ("Stats.Cash"). SetValue/Increment go through manager:Update so the
	-- replica and the save scheduler see them like any other write;
	-- Increment's optional `floor` clamps (pass 0 for v1's AddCash behaviour).
	local function getValue(player, path)
		return DataPath.get(manager:Get(player), path)
	end
	local function setValue(player, path, value)
		local ok, err = true, nil
		manager:Update(player, function(data)
			ok, err = DataPath.set(data, path, value)
		end)
		if not ok then
			warn("[DataStore] SetValue:", err)
		end
		return ok, err
	end
	local function increment(player, path, delta, floor)
		local newValue, err = nil, nil
		manager:Update(player, function(data)
			newValue, err = DataPath.increment(data, path, delta, floor)
		end)
		if err then
			warn("[DataStore] Increment:", err)
		end
		return newValue, err
	end
	local function isFreeMode()
		return freeModeValue ~= nil and freeModeValue.Value == true
	end

	return {
		Get = function(player) return manager:Get(player) end,
		Update = function(player, fn) return manager:Update(player, fn) end,
		SaveAndWait = function(player, timeout) return manager:SaveAndWait(player, timeout) end,
		ReadSaved = function(userId) return manager:ReadSaved(userId) end,
		StoreName = function() return manager:StoreName() end,
		Save = function(player) return manager:Save(player) end,
		SaveAllImmediate = function(t) return manager:SaveAllImmediate(t) end,
		GetSafeUserId = function(player) return safeId.GetSafeUserId(player) end,
		GetValue = getValue,
		SetValue = setValue,
		Increment = increment,
		IsFreeMode = isFreeMode,
		registerHandler = purchases.registerHandler,
		registerProfileHandler = purchases.registerProfileHandler,
		HasPass = purchases.HasPass,
		HasPassStrict = purchases.HasPassStrict,
		GrantProduct = purchases.GrantProduct,
		InvalidatePass = purchases.InvalidatePass,
		PushRoot = ValueReplicator.PushRoot,
	}
end
]]></string>
            <int64 name="SourceAssetId">-1</int64>
            <BinaryString name="Tags"></BinaryString>
          </Properties>
          <Item class="Folder" referent="34">
            <Properties>
              <string name="Name">Actions</string>
              <BinaryString name="AttributesSerialize"></BinaryString>
              <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
              <Color3 name="IconTint">
                <R>0</R>
                <G>0</G>
                <B>0</B>
              </Color3>
              <bool name="DefinesCapabilities">false</bool>
              <BinaryString name="SerializedOverrides">
              </BinaryString>
              <int64 name="SourceAssetId">-1</int64>
              <BinaryString name="Tags"></BinaryString>
            </Properties>
            <Item class="ModuleScript" referent="35">
              <Properties>
                <string name="Name">WipeStudioData</string>
                <BinaryString name="AttributesSerialize"></BinaryString>
                <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
                <ContentId name="LinkedSource">
                  <null>
                  </null>
                </ContentId>
                <bool name="DefinesCapabilities">false</bool>
                <BinaryString name="SerializedOverrides">
                </BinaryString>
                <string name="Source"><![CDATA[-- Actions/WipeStudioData.lua
-- Wipes all keys in the Studio DataStore. Only runs inside Studio.
-- 2.x: the RBLX Essentials plugin's "Wipe Studio Data" button ran it, with
-- the version read from the pack's Settings. 3.0 has no Settings (plan B1,
-- ruling 8): the store names come from the game's configuration
-- (ReplicatedStorage.RBLXEssentials.Config.DatastorePurchaseSystem over this
-- pack's Schema, loaded with the Kernel's Config.load as the server loads
-- it, from fresh clones so an edit is never read stale), and it refuses,
-- deleting nothing, when the Studio store may be the live one
-- (StudioDataStoreVersion == DataStoreVersion, as 2.0.0 shipped them, or
-- the two differ only in leading or trailing spaces or in letter case,
-- which nobody has checked makes another DataStore: Config.lua's
-- SameStores rule): that would delete every player's live profile. Its
-- 3.0 replacement is a later
-- Tools operation. It deletes every Studio profile for good, so it also
-- refuses, deleting nothing, unless it is called with
-- { confirm = "PlayerData_<StudioDataStoreVersion>" }, the store it would
-- wipe (as T.backups.delete(id, { force = id }) names its backup): the
-- plugin's Wipe Studio Data button passes it after its second click, and an
-- AI assistant only once the user asked for the wipe (docs AI run
-- datastore-ai-setup, 2026-10-09: an agent wiped them on its own).
local DataStoreService = game:GetService("DataStoreService")
local RunService       = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")

local PACK = "DatastorePurchaseSystem"

local function fresh(module)
	return require(module:Clone())
end

-- The store key of Config.lua's SameStores rule.
local function storeKey(version: string): string
	return string.lower((string.gsub(string.gsub(version, "^%s+", ""), "%s+$", "")))
end

local function configModule(service)
	local root = service:FindFirstChild("RBLXEssentials")
	local folder = root and root:FindFirstChild("Config")
	return folder and folder:FindFirstChild(PACK)
end

return function(opts)
	assert(RunService:IsStudio(), "WipeStudioData can only run in Studio")

	local rblx = ReplicatedStorage.RBLXEssentials
	local config, errors = fresh(rblx.Kernel.Config).load({
		packId = PACK,
		side = "Server",
		schemaLib = fresh(rblx.Kernel.Schema),
		requireFn = fresh,
		schemaModule = rblx.Packs[PACK]:FindFirstChild("Schema"),
		sharedModule = configModule(ReplicatedStorage),
		serverModule = configModule(ServerScriptService),
	})
	if config == nil then
		return false, "Nothing was wiped: the configuration of " .. PACK .. " is invalid: " .. table.concat(errors, "; ")
	end
	if config.StudioDataStoreVersion == config.DataStoreVersion then
		return false, ("Nothing was wiped: StudioDataStoreVersion and DataStoreVersion are both %q, so the Studio store PlayerData_%s is the live one, and wiping it would delete every player's profile. Set another StudioDataStoreVersion first."):format(
			config.DataStoreVersion, config.DataStoreVersion)
	end
	if storeKey(config.StudioDataStoreVersion) == storeKey(config.DataStoreVersion) then
		return false, ("Nothing was wiped: StudioDataStoreVersion %q and DataStoreVersion %q differ only in spaces or letter case, which may not make another DataStore, so wiping PlayerData_%s could delete every player's live profile. Set another StudioDataStoreVersion first."):format(
			config.StudioDataStoreVersion, config.DataStoreVersion, config.StudioDataStoreVersion)
	end
	local storeName   = "PlayerData_" .. config.StudioDataStoreVersion
	if type(opts) ~= "table" or opts.confirm ~= storeName then
		return false, ("Nothing was wiped: wiping %s deletes every Studio profile in it for good, so it needs { confirm = %q }. Ask the user first: only once they agree, call it again with that argument (the plugin's Wipe Studio Data button passes it)."):format(
			storeName, storeName)
	end

	local ds    = DataStoreService:GetDataStore(storeName)
	local pages = ds:ListKeysAsync()
	local count = 0

	while true do
		for _, key in ipairs(pages:GetCurrentPage()) do
			ds:RemoveAsync(key.KeyName)
			count += 1
		end
		if pages.IsFinished then break end
		pages:AdvanceToNextPageAsync()
	end

	return true, "Wiped " .. count .. " key(s) from " .. storeName
end
]]></string>
                <int64 name="SourceAssetId">-1</int64>
                <BinaryString name="Tags"></BinaryString>
              </Properties>
            </Item>
          </Item>
          <Item class="ModuleScript" referent="36">
            <Properties>
              <string name="Name">Config</string>
              <BinaryString name="AttributesSerialize"></BinaryString>
              <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
              <ContentId name="LinkedSource">
                <null>
                </null>
              </ContentId>
              <bool name="DefinesCapabilities">false</bool>
              <BinaryString name="SerializedOverrides">
              </BinaryString>
              <string name="Source"><![CDATA[-- Config.lua - what the server module runs with, from the pack's frozen
-- configuration (ctx.config, see Schema.lua; plan B1). 2.x read the same
-- values from its Settings folder; 3.0 has none.
-- Pure: the configuration and whether this server runs in Studio come in as
-- arguments (init.lua asks RunService once, at boot).
--
-- DataStore keeps 2.x's shape, so DataStoreManager reads it unchanged: the
-- two store versions and AutoSaveInterval come from the configuration; the
-- retry and save-smoothing constants stay in code (plan B1, ruling 9).
--
-- `SameStores` says Studio may use the live store (2.0.0 shipped the two
-- versions equal, and a 2.x update kept that): the server warns once. The
-- store names stay exactly as written, as 2.x built them, but this safety
-- gate fails closed: two versions that differ only in leading or trailing
-- spaces or in letter case count as one store, because nobody has checked
-- whether Roblox keeps "PlayerData_v1 " or "PlayerData_V1" apart from
-- "PlayerData_v1" (plan B1). Actions/WipeStudioData applies the same rule.
--
-- FreeMode (plan B1, ruling 4 and the critique's ruling on Studio == live)
-- takes effect only in Studio (the rule of the 2.1.3 security fix), and not
-- even there while Studio may use the live store: it would grant passes and
-- products into real players' profiles. `FreeMode` is true only when the
-- game switched it on, this is Studio and SameStores is false;
-- `FreeModeIgnored` names why a FreeMode the game switched on does not take
-- effect ("not Studio" or "same stores"), so the server warns once, and is
-- false otherwise.
local function storeKey(version: string): string
	return string.lower((string.gsub(string.gsub(version, "^%s+", ""), "%s+$", "")))
end

return function(config, isStudio)
	local freeMode = config.FreeMode == true
	local sameStores = storeKey(config.DataStoreVersion) == storeKey(config.StudioDataStoreVersion)
	local ignored = false
	if freeMode and isStudio ~= true then
		ignored = "not Studio"
	elseif freeMode and sameStores then
		ignored = "same stores"
	end
	return {
		DataStore = {
			DataStoreVersion       = config.DataStoreVersion,
			StudioDataStoreVersion = config.StudioDataStoreVersion,
			AutoSaveInterval       = config.AutoSaveInterval,
			MaxRetries             = 5,
			RetryDelay             = 1,
			SaveSmoothing          = {
				RespectBudget        = true,
				MinGapSeconds        = 1.0,
				JitterFraction       = 0.35,
				PerPlayerMinInterval = 45,
			},
		},
		FreeMode = freeMode and ignored == false,
		FreeModeIgnored = ignored,
		SameStores = sameStores,
	}
end
]]></string>
              <int64 name="SourceAssetId">-1</int64>
              <BinaryString name="Tags"></BinaryString>
            </Properties>
          </Item>
          <Item class="ModuleScript" referent="37">
            <Properties>
              <string name="Name">DataPath</string>
              <BinaryString name="AttributesSerialize"></BinaryString>
              <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
              <ContentId name="LinkedSource">
                <null>
                </null>
              </ContentId>
              <bool name="DefinesCapabilities">false</bool>
              <BinaryString name="SerializedOverrides">
              </BinaryString>
              <string name="Source"><![CDATA[-- DataPath.lua - dot-path helpers over a player's profile table.
--
-- The public API's convenience layer (2026-08-22): v1 shipped a Cash-only
-- `PlayerMutators` (AddCash / SetCash / GetCash, clamped at 0) that only its
-- own demo used; v2 replaces it with path-addressed equivalents that work on
-- ANY key of the profile, using the same dot-paths the plugin's Default Data
-- editor and the leaderboard pickers show ("Stats.Cash", "Inventory.Slots").
--
-- Pure: tables in, tables out. `set`/`increment` create missing intermediate
-- tables so a key the buyer just added in Default Data can be written before
-- a player's profile has been migrated to include it.
local DataPath = {}

-- DataPath.split("Stats.Cash") -> { "Stats", "Cash" }; nil for an empty or
-- malformed path (leading/trailing/double dots).
function DataPath.split(path: string?): { string }?
	if type(path) ~= "string" or path == "" then
		return nil
	end
	local parts = {}
	for part in path:gmatch("[^%.]+") do
		table.insert(parts, part)
	end
	if #parts == 0 or table.concat(parts, ".") ~= path then
		return nil
	end
	return parts
end

-- DataPath.get(data, path) -> value or nil (nil also when a step is not a table).
function DataPath.get(data: any, path: string?): any
	local parts = DataPath.split(path)
	if parts == nil or type(data) ~= "table" then
		return nil
	end
	local cur = data
	for _, key in ipairs(parts) do
		if type(cur) ~= "table" then
			return nil
		end
		cur = cur[key]
	end
	return cur
end

-- DataPath.set(data, path, value) -> ok, err. Creates intermediate tables;
-- refuses to walk THROUGH a non-table value ("Stats.Cash.X" when Cash = 0).
function DataPath.set(data: any, path: string?, value: any): (boolean, string?)
	local parts = DataPath.split(path)
	if parts == nil then
		return false, ("Invalid data path: %q"):format(tostring(path))
	end
	if type(data) ~= "table" then
		return false, "Profile is not a table."
	end
	local cur = data
	for i = 1, #parts - 1 do
		local key = parts[i]
		local nextNode = cur[key]
		if nextNode == nil then
			nextNode = {}
			cur[key] = nextNode
		elseif type(nextNode) ~= "table" then
			return false, ("Cannot set %q: %q is a %s, not a group."):format(path, table.concat(parts, ".", 1, i), type(nextNode))
		end
		cur = nextNode
	end
	cur[parts[#parts]] = value
	return true, nil
end

-- DataPath.increment(data, path, delta, floor?) -> newValue?, err. A missing
-- key counts as 0; a non-number key is refused. `floor` clamps the result
-- (v1's AddCash never went below 0 - pass 0 to keep that behaviour).
function DataPath.increment(data: any, path: string?, delta: number, floor: number?): (number?, string?)
	if type(delta) ~= "number" or delta ~= delta then
		return nil, "Delta must be a number."
	end
	local current = DataPath.get(data, path)
	if current ~= nil and type(current) ~= "number" then
		return nil, ("Cannot increment %q: it holds a %s."):format(tostring(path), type(current))
	end
	local newValue = (current or 0) + delta
	if type(floor) == "number" then
		newValue = math.max(floor, newValue)
	end
	local ok, err = DataPath.set(data, path, newValue)
	if not ok then
		return nil, err
	end
	return newValue, nil
end

return DataPath
]]></string>
              <int64 name="SourceAssetId">-1</int64>
              <BinaryString name="Tags"></BinaryString>
            </Properties>
          </Item>
          <Item class="ModuleScript" referent="38">
            <Properties>
              <string name="Name">DataStoreManager</string>
              <BinaryString name="AttributesSerialize"></BinaryString>
              <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
              <ContentId name="LinkedSource">
                <null>
                </null>
              </ContentId>
              <bool name="DefinesCapabilities">false</bool>
              <BinaryString name="SerializedOverrides">
              </BinaryString>
              <string name="Source"><![CDATA[-- Per-instance profile cache, queued saves and explicit confirmed saves.
-- Services are lazy; callers must coordinate mutations during SaveAndWait.
local Manager = {}
Manager.__index = Manager

local log = {
	warn  = function(msg) warn("[DataStore]", msg) end,
	print = function(msg) print("[DataStore]", msg) end,
}

-- Reproduces v1 Config.lua:16-22 per environment: "PlayerData_" .. (Studio and
-- StudioDataStoreVersion or DataStoreVersion). Called only from getStore(), which
-- is only ever reached from Get()/save paths - never at module load or new().
local function storeName(dsConfig)
	local RunService = game:GetService("RunService")
	return "PlayerData_" .. (RunService:IsStudio() and dsConfig.StudioDataStoreVersion or dsConfig.DataStoreVersion)
end

local function getStore(self)
	if not self._store then
		local DataStoreService = game:GetService("DataStoreService")
		self._store = DataStoreService:GetDataStore(storeName(self._config.DataStore))
	end
	return self._store
end

-- Detects the specific "this place can't use DataStoreService right now" class
-- of error Roblox throws when a place is unpublished or "Studio Access to API
-- Services" is disabled - as opposed to a transient error (throttling, timeout,
-- momentary outage) that the existing retry/backoff logic is meant to ride out.
-- Matched defensively via a lower-cased substring, since Roblox's exact wording
-- varies by call site/version; both substrings below are named directly in the
-- flip-validation finding this function exists to fix (buyer-facing console
-- spam on unpublished/API-disabled Studio places).
--
-- This classification path remains for the published-place-with-"Studio Access
-- to API Services"-disabled case, which throws synchronously and so is still
-- detectable AFTER the real call. It is NOT sufficient on its own: live Play
-- validation (round 2, 2026-08-18) showed that in an UNPUBLISHED Studio place
-- the engine's GetDataStore/GetAsync call can instead block forever without
-- ever returning or throwing - `retryPromise(...):await()` never settles,
-- so this classification never runs and Manager:Get() never returns at all.
-- isStudioUnpublished() below is the proactive first-line guard that covers
-- that case: it never makes the real call to begin with.
local function isApiAccessError(err): boolean
	local msg = string.lower(tostring(err))
	return msg:find("must publish this place", 1, true) ~= nil
		or msg:find("studio access to api services", 1, true) ~= nil
end

-- Proactive guard for the "engine call can block forever" case above: in Studio,
-- an unpublished place always has game.GameId == 0 (published places never do).
-- Checking this BEFORE making the real DataStoreService call lets us short-circuit
-- synchronously instead of depending on a call that may never return. Service/
-- global access here is lazy - called only from the load/save guard paths below,
-- never at module load or inside new() (new() must remain constructible in Lune
-- with no `game`).
local function isStudioUnpublished(): boolean
	return game:GetService("RunService"):IsStudio() and game.GameId == 0
end

-- Trips the session-level "no DataStore API access" flag exactly once (guarded
-- by the early return). After this, loadFromStore/saveToStore/SaveAllImmediate/
-- startScheduler never call DataStoreService again for the lifetime of this
-- Manager instance. In-memory session data (self._cache) keeps working exactly
-- as before; it simply never round-trips to a datastore.
local function markApiUnavailable(self)
	if self._apiUnavailable then
		return
	end
	self._apiUnavailable = true
	log.warn("Studio API access unavailable (place not published, or Studio API access disabled). Player data will NOT persist this session.")
end

local function generateGuid()
	return game:GetService("HttpService"):GenerateGUID(false)
end

local function retryPromise(self, fn)
	local attempt = 0
	return self._Promise.retry(function()
		attempt += 1
		return self._Promise.new(function(resolve, reject)
			-- Promise.retry forwards extra arguments; it does not interpret a backoff function.
			if attempt > 1 then task.wait(self._config.DataStore.RetryDelay * 2 ^ (attempt - 2)) end
			local ok, result = pcall(fn)
			if ok then resolve(result) else reject(result) end
		end)
	end, self._config.DataStore.MaxRetries)
end

-- Resolves (ok, loadedOrErr) for Get(). Once self._apiUnavailable is set, this
-- never touches DataStoreService again - it resolves as "no saved data" so the
-- caller's existing no-data/defaults path (Migration.migrate(nil, defaults))
-- handles it exactly like a first-time player. The FIRST failure of this kind
-- still makes the real GetAsync call (that's how it's detected), classifies
-- the error, warns once, and degrades gracefully for THIS call too - Get()
-- must never throw/kick a player for an api-access failure, only for a
-- genuine (transient) one, which is left byte-for-byte unchanged below.
local function loadFromStore(self, key: string, claimSession: boolean?)
	if self._apiUnavailable then
		return true, nil
	end
	if isStudioUnpublished() then
		markApiUnavailable(self)
		return true, nil
	end

	local claimId = claimSession and generateGuid() or nil
	local observed, priorId, priorStart, rejected = false, nil, nil, false
	local ok, loadedOrErr = retryPromise(self, function()
		if claimId then
			return getStore(self):UpdateAsync(key, function(old, keyInfo)
				local meta = old and old.Meta
				if meta and meta.SessionId == claimId then
					return old, keyInfo and keyInfo:GetUserIds(), keyInfo and keyInfo:GetMetadata()
				end
				-- A retried request must not take ownership back from a newer claimant.
				if not observed then
					observed, priorId, priorStart = true, meta and meta.SessionId, meta and meta.SessionStart
				elseif (meta and meta.SessionId) ~= priorId or (meta and meta.SessionStart) ~= priorStart then
					rejected = true; return nil
				end
				if rejected then return nil end
				local data = self._Migration.migrate(old, self._defaults)
				data.Meta = data.Meta or {}
				data.Meta.SessionId, data.Meta.SessionFenced = claimId, true
				data.Meta.SessionStart = os.time()
				data.Meta.FirstJoinDate = data.Meta.FirstJoinDate or os.time()
				return data, keyInfo and keyInfo:GetUserIds(), keyInfo and keyInfo:GetMetadata()
			end)
		end
		return getStore(self):GetAsync(key)
	end):await()

	if not ok and isApiAccessError(loadedOrErr) then
		markApiUnavailable(self)
		return true, nil
	end

	if claimId and (rejected or (ok and (not loadedOrErr or loadedOrErr.Meta.SessionId ~= claimId))) then
		return false, "Profile ownership changed during load; please rejoin."
	end
	return ok, loadedOrErr
end

-- Resolves `wrote` for performSave(). Once self._apiUnavailable is set, this
-- never touches DataStoreService again - saves are no-ops that report success
-- (wrote = true) so the scheduler does not treat them as failures and retry-
-- loop every ~6-10s. SaveAndWait explicitly excludes this in-memory fallback.
-- A callback alone is not confirmation: the complete UpdateAsync must succeed.
local function copySnapshot(value)
	if type(value) ~= "table" then return value end
	local result = {}
	for key, child in pairs(value) do result[key] = copySnapshot(child) end
	return result
end

local function saveToStore(self, uid: number, key: string, thisSessionStart: number, data: any)
	if self._lostSessions[uid] then return false end
	if self._apiUnavailable then
		return true
	end
	if isStudioUnpublished() then
		markApiUnavailable(self)
		return true
	end

	data = copySnapshot(data) -- freeze before the first yielding store call
	local wrote, lostSession = false, false
	local ok, err = retryPromise(self, function()
		wrote = false -- each attempt/callback must establish its own success
		return getStore(self):UpdateAsync(key, function(old, keyInfo)
			local oldMeta = old and old.Meta
			local oldStart = oldMeta and oldMeta.SessionStart
			if data.Meta and data.Meta.SessionFenced and (not oldMeta or oldMeta.SessionId ~= data.Meta.SessionId) then
				wrote = false; lostSession = true; return nil
			end
			if old == nil then wrote = true; return data end
			if type(oldStart) == "number" and type(thisSessionStart) == "number" and oldStart > thisSessionStart then
				wrote = false; return nil
			end
			wrote = true; return data, keyInfo and keyInfo:GetUserIds(), keyInfo and keyInfo:GetMetadata()
		end)
	end):await()
	if lostSession then
		self._lostSessions[uid] = true
		self._saveQueue[uid], self._nextDue[uid] = nil, nil
		local state = self._playerState[uid]
		if state and state.player and state.player.Parent then
			pcall(function()state.player:Kick("Your profile opened in another server. Please rejoin.")end)
		end
	end

	if not ok and isApiAccessError(err) then
		markApiUnavailable(self)
		wrote = true
	end

	return wrote and (ok or self._apiUnavailable)
end

local function waitForBudget(reqType: Enum.DataStoreRequestType, maxWait: number)
	local DataStoreService = game:GetService("DataStoreService")
	local deadline = os.clock() + maxWait
	while os.clock() < deadline do
		if DataStoreService:GetRequestBudgetForRequestType(reqType) > 0 then return true end
		task.wait(0.1)
	end
	return false
end

local function scheduleNext(self, uid: number, nowClock: number?)
	local now = nowClock or os.clock()
	local interval = self._config.DataStore.AutoSaveInterval
	local jf = (self._config.DataStore.SaveSmoothing and self._config.DataStore.SaveSmoothing.JitterFraction) or 0
	local jitter = interval * jf * (math.random()*2 - 1)
	self._nextDue[uid] = now + math.max(10, interval + jitter)
end

local function scheduleFirstDue(self, uid: number)
	scheduleNext(self, uid, os.clock())
end

-- Minimal per-player leave-tracking handle - see header comment for why this
-- isn't a full kernel Trove instance. Same call shape as v1 (`:Clean()`).
local function newLeaveTrove()
	local handle = { _conn = nil :: RBXScriptConnection? }
	function handle:Connect(signal, fn)
		self._conn = signal:Connect(fn)
	end
	function handle:Clean()
		if self._conn then
			self._conn:Disconnect()
			self._conn = nil
		end
	end
	return handle
end

local function performSave(self, uid: number)
	local data = self._cache[uid]
	if not data then return false end
	if not data.Meta then
		log.warn(("Refusing to save data for uid %d: missing Meta"):format(uid))
		return false
	end
	local hasRealSession = data.Meta.SessionId and data.Meta.SessionStart
	local hasBeenSaved = data.Meta.SaveCount and data.Meta.SaveCount > 0
	local hasFirstJoin = data.Meta.FirstJoinDate
	if not hasRealSession and not hasBeenSaved and not hasFirstJoin then
		log.warn(("Refusing to save data for uid %d: looks like corrupted/empty data"):format(uid))
		return false
	end

	data.Meta = data.Meta or {}
	data.Meta.LastSaveDate = os.time()
	data.Meta.SaveCount = (data.Meta.SaveCount or 0) + 1
	data.Meta.SessionId = data.Meta.SessionId or generateGuid()
	data.Meta.SessionStart = data.Meta.SessionStart or os.time()

	local thisSessionStart = data.Meta.SessionStart
	local wrote = false

	-- Key unification: the ONLY key ever used to save is the one derived and
	-- stored at load time (Get()). No second/independent derivation here.
	local key = self._keys[uid]
	local revision = self._revisions[uid] or 0

	wrote = saveToStore(self, uid, key, thisSessionStart, data)

	if wrote then
		self._lastPlayerSaveAt[uid] = os.clock()
	end

	if self._playerState[uid] and self._playerState[uid].removed and wrote and revision == (self._revisions[uid] or 0) then
		self._cache[uid] = nil
		self._nextDue[uid] = nil
		self._playerState[uid] = nil
		self._keys[uid] = nil -- pairs with self._cache[uid]'s lifecycle; not in v1 (v1 had no stored key)
		self._revisions[uid] = nil
	end

	return wrote
end

local function startScheduler(self)
	if self._schedulerStarted then return end
	self._schedulerStarted = true

	local function failureBackoffSeconds()
		return 6 + math.random() * 4
	end

	local RunService = game:GetService("RunService")

	self._trove:Connect(RunService.Heartbeat, function()
		local now = os.clock()
		local minGap = (self._config.DataStore.SaveSmoothing and self._config.DataStore.SaveSmoothing.MinGapSeconds) or 0
		if (now - self._lastSaveAt) < minGap then return end

		if (self._config.DataStore.SaveSmoothing and self._config.DataStore.SaveSmoothing.RespectBudget) and not self._apiUnavailable then
			local DataStoreService = game:GetService("DataStoreService")
			local budget = DataStoreService:GetRequestBudgetForRequestType(Enum.DataStoreRequestType.UpdateAsync)
			if budget <= 0 then return end
		end

		local perMin = (self._config.DataStore.SaveSmoothing and self._config.DataStore.SaveSmoothing.PerPlayerMinInterval) or 0

		for uid in pairs(self._saveQueue) do
			self._saveQueue[uid] = nil
			if self._lostSessions[uid] then self._nextDue[uid] = nil; continue end
			if self._inFlight[uid] then return end
			if (now - (self._lastPlayerSaveAt[uid] or 0)) < perMin then
				self._nextDue[uid] = math.max(self._nextDue[uid] or 0, (self._lastPlayerSaveAt[uid] or now) + perMin)
				return
			end
			self._inFlight[uid] = true
			local ran, wrote = pcall(performSave, self, uid)
			local ok = ran and wrote
			self._lastSaveAt = now
			if ok then scheduleNext(self, uid, now)
			else self._nextDue[uid] = math.max(self._nextDue[uid] or 0, now + failureBackoffSeconds()) end
			self._inFlight[uid] = nil
			return
		end

		local dueUid, earliest = nil, math.huge
		for uid, due in pairs(self._nextDue) do
			if self._lostSessions[uid] then self._nextDue[uid] = nil; continue end
			if due < earliest then earliest, dueUid = due, uid end
		end
		if not dueUid or earliest > now then return end
		if self._inFlight[dueUid] then return end
		if (now - (self._lastPlayerSaveAt[dueUid] or 0)) < perMin then
			self._nextDue[dueUid] = (self._lastPlayerSaveAt[dueUid] or now) + perMin
			return
		end

		self._inFlight[dueUid] = true
		local ran, wrote = pcall(performSave, self, dueUid)
		local ok = ran and wrote
		self._lastSaveAt = now
		if ok then scheduleNext(self, dueUid, now)
		else self._nextDue[dueUid] = math.max(self._nextDue[dueUid] or 0, now + failureBackoffSeconds()) end
		self._inFlight[dueUid] = nil
	end)
end

-- deps = { config, defaults, safeId, Promise, Migration, Replicator, trove }
-- No game-service access here - constructible in Lune.
function Manager.new(deps)
	local self = setmetatable({}, Manager)

	self._config     = deps.config
	self._defaults    = deps.defaults
	self._safeId      = deps.safeId
	self._Promise     = deps.Promise
	self._Migration    = deps.Migration
	self._Replicator   = deps.Replicator
	self._trove        = deps.trove

	self._store = nil -- lazily created by getStore() on first Get()/save
	self._apiUnavailable = false -- flip validation fix: see markApiUnavailable() above

	self._cache            = {}
	self._loading           = {}
	self._nextDue           = {}
	self._saveQueue         = {}
	self._lastSaveAt        = 0
	self._lastPlayerSaveAt  = {}
	self._schedulerStarted  = false
	self._inFlight          = {}
	self._playerTroves      = {}
	self._playerState       = {}
	self._keys              = {} -- [uid] = safeId.GetSafeKey(player) captured at load - fix #2
	self._lostSessions      = {}
	self._revisions         = {}

	return self
end

local function bindPlayer(self, uid, player)
	if self._playerState[uid] and self._playerState[uid].player == player then return end
	if self._playerTroves[uid] then self._playerTroves[uid]:Clean() end
	local t = newLeaveTrove()
	self._playerTroves[uid] = t
	self._playerState[uid] = { removed = false, player = player }

	t:Connect(player.AncestryChanged, function(_, parent)
		if parent == nil and self._playerState[uid] and self._playerState[uid].player == player then
			t:Clean()
			self._playerTroves[uid] = nil
			if self._cache[uid] then
				self._playerState[uid] = self._playerState[uid] or { removed = true }
				self._playerState[uid].removed = true
				self._saveQueue[uid] = true
				local nowClock = os.clock()
				local perMin = (self._config.DataStore.SaveSmoothing and self._config.DataStore.SaveSmoothing.PerPlayerMinInterval) or 0
				self._nextDue[uid] = math.max(self._nextDue[uid] or 0, (self._lastPlayerSaveAt[uid] or nowClock) + math.max(0.2, perMin * 0.25))
			end
		end
	end)

	self._Replicator.PushRoot(player, "PlayerData", self._cache[uid])
end

function Manager:Get(player: Player)
	local safeId = self._safeId
	local uid = safeId.GetSafeUserId(player)
	if self._lostSessions[uid] then
		local state = self._playerState[uid]
		if not state or state.player == player or (state.player and state.player.Parent) then
			error("Profile session is no longer owned by this server")
		end
		-- A later Player can rejoin this server, but must load the current stored
		-- profile instead of adopting the invalid cache from its old session.
		while self._inFlight[uid] do task.wait(0.1) end
		if self._playerTroves[uid] then self._playerTroves[uid]:Clean() end
		self._cache[uid], self._keys[uid], self._playerState[uid], self._playerTroves[uid] = nil, nil, nil, nil
		self._lostSessions[uid], self._revisions[uid], self._saveQueue[uid], self._nextDue[uid] = nil, nil, nil, nil
	end
	if self._cache[uid] then bindPlayer(self, uid, player); return self._cache[uid] end

	-- Deduplicate concurrent loads: only one GetAsync per uid may be in flight.
	-- A second concurrent Get would otherwise replace cache[uid] with a freshly
	-- migrated table, silently discarding any writes made to the first one.
	while self._loading[uid] do
		task.wait()
		if self._cache[uid] then bindPlayer(self, uid, player); return self._cache[uid] end
	end
	self._loading[uid] = true

	-- Single derivation of the save key, captured now so performSave never
	-- has to (and never does) re-derive it independently.
	local key = safeId.GetSafeKey(player)

	local ok, loadedOrErr = loadFromStore(self, key, true)

	if self._cache[uid] then
		self._loading[uid] = nil
		bindPlayer(self, uid, player)
		return self._cache[uid]
	end

	local data
	if not ok then
		self._loading[uid] = nil
		error(("[DataStore] Failed to load data for user %d: %s"):format(uid, tostring(loadedOrErr)))
	elseif loadedOrErr == nil then
		data = self._Migration.migrate(nil, self._defaults)
	else
		data = self._Migration.migrate(loadedOrErr, self._defaults)
	end

	local now = os.time()
	data.Meta = data.Meta or {}
	data.Meta.SessionStart = now
	data.Meta.LastSaveDate = now
	data.Meta.SessionId = data.Meta.SessionFenced and data.Meta.SessionId or generateGuid()
	if loadedOrErr == nil then
		data.Meta.FirstJoinDate = data.Meta.FirstJoinDate or now
	end

	self._cache[uid] = data
	self._keys[uid] = key
	self._loading[uid] = nil
	scheduleFirstDue(self, uid)
	startScheduler(self)

	bindPlayer(self, uid, player)

	return data
end

function Manager:Update(player: Player, patch: (data: any) -> ())
	local uid = self._safeId.GetSafeUserId(player)
	local data = self:Get(player)
	patch(data)
	self._revisions[uid] = (self._revisions[uid] or 0) + 1
	self._Replicator.PushRoot(player, "PlayerData", data)
end

function Manager:Save(player: Player)
	if not player then return false end
	if self._lostSessions[self._safeId.GetSafeUserId(player)] then return false end
	self._saveQueue[self._safeId.GetSafeUserId(player)] = true
	return true
end

-- A profile-only grant is staged synchronously. Never yield, send remotes or
-- mutate external state in patch. Receipt and reward enter the cache together.
function Manager:ApplyProfilePurchase(player, purchaseId, patch)
	local data = self:Get(player)
	local history = data.Purchases and data.Purchases.ReceiptHistory or {}
	if purchaseId and table.find(history, purchaseId) then return true, false end
	local staged = copySnapshot(data)
	local thread = coroutine.create(patch)
	local ok, granted, result = coroutine.resume(thread, staged)
	if coroutine.status(thread) ~= "dead" then
		pcall(task.cancel, thread); pcall(coroutine.close, thread)
		return false, false, "Profile purchase callback must not yield"
	end
	if not ok or granted ~= true then return false, false, ok and result or granted end
	staged.Purchases = staged.Purchases or {}
	staged.Purchases.ReceiptHistory = staged.Purchases.ReceiptHistory or {}
	if purchaseId then table.insert(staged.Purchases.ReceiptHistory, purchaseId) end
	-- Detach the committed values from any references retained by the callback.
	staged = copySnapshot(staged)
	for key in pairs(data) do data[key] = nil end
	for key, value in pairs(staged) do data[key] = value end
	local uid = self._safeId.GetSafeUserId(player)
	self._revisions[uid] = (self._revisions[uid] or 0) + 1
	self._saveQueue[uid] = true
	pcall(self._Replicator.PushRoot, player, "PlayerData", data)
	return true, true, result
end

function Manager:ReadSaved(userId: number)
	assert(type(userId) == "number" and userId > 0 and userId % 1 == 0, "invalid_user")
	if self._apiUnavailable or isStudioUnpublished() then error("storage_unavailable") end
	local options = Instance.new("DataStoreGetOptions"); options.UseCache = false
	local ok, value = retryPromise(self, function()return getStore(self):GetAsync(tostring(userId), options)end):await()
	if not ok then error(value) end
	return value
end

function Manager:StoreName()
	return storeName(self._config.DataStore)
end

-- Unlike Save (queue only), this confirms a durable write before a handoff.
-- Callers must pause their profile mutations until it returns.
function Manager:SaveAndWait(player: Player, lockTimeout: number?)
	if not player or self._apiUnavailable then return false end
	local uid = self._safeId.GetSafeUserId(player)
	if self._lostSessions[uid] then return false end
	local deadline = os.clock() + (lockTimeout or 15)
	while self._inFlight[uid] do
		if os.clock() >= deadline then return false end
		task.wait(0.1)
	end
	if not self._cache[uid] or self._apiUnavailable then return false end
	self._inFlight[uid] = true
	self._saveQueue[uid] = nil
	local ok, wrote = pcall(performSave, self, uid)
	self._inFlight[uid] = nil
	if ok and wrote and not self._apiUnavailable then
		if self._cache[uid] then scheduleNext(self, uid) end
		return true
	end
	if self._cache[uid] and not self._lostSessions[uid] then self._saveQueue[uid] = true end
	return false
end

function Manager:SaveAllImmediate(maxWaitPerWrite: number?)
	maxWaitPerWrite = maxWaitPerWrite or 2.0
	self._trove:Clean()
	for uid, _ in pairs(self._cache) do
		if not self._apiUnavailable then
			waitForBudget(Enum.DataStoreRequestType.UpdateAsync, maxWaitPerWrite)
		end
		local deadline = os.clock() + maxWaitPerWrite
		while self._inFlight[uid] and os.clock() < deadline do task.wait(0.1) end
		local ok = false
		if not self._inFlight[uid] then
			self._inFlight[uid] = true
			local ran, wrote = pcall(performSave, self, uid)
			self._inFlight[uid] = nil
			ok = ran and wrote
		end
		if ok then
			log.print(("SaveAllImmediate: saved %d"):format(uid))
		else
			log.warn(("SaveAllImmediate: skipped/stale %d"):format(uid))
		end
	end
end

function Manager:Stop()
	self._trove:Clean()
end

-- Test-only accessor: number of entries currently cached on THIS instance.
-- Used by tests/manager.spec.luau to prove two new() calls share no state.
function Manager:_cacheCount(): number
	local n = 0
	for _ in pairs(self._cache) do
		n += 1
	end
	return n
end

-- Test-only hooks (flip-validation fix): exercise the real load/save-vs-store
-- short-circuit logic without needing a live Player/ValueReplicator round trip
-- (Get()/performSave() need a real Player Instance for AncestryChanged/PushRoot,
-- which Lune cannot provide - see tests/manager.spec.luau header comment).
function Manager:_loadFromStoreForTest(key: string, claimSession: boolean?)
	return loadFromStore(self, key, claimSession)
end

function Manager:_saveToStoreForTest(uid: number, key: string, thisSessionStart: number, data: any)
	return saveToStore(self, uid, key, thisSessionStart, data)
end

-- Test-only hook: directly exercises markApiUnavailable()'s idempotency guard
-- (the "warns exactly once" latch) without needing a failing store round trip.
function Manager:_markApiUnavailableForTest()
	return markApiUnavailable(self)
end

-- Exposed for direct pure-function testing (mirrors PurchaseManager.shouldProcess/
-- record's precedent of exposing small pure helpers on the module table).
Manager.isApiAccessError = isApiAccessError

return Manager
]]></string>
              <int64 name="SourceAssetId">-1</int64>
              <BinaryString name="Tags"></BinaryString>
            </Properties>
          </Item>
          <Item class="ModuleScript" referent="39">
            <Properties>
              <string name="Name">DefaultData</string>
              <BinaryString name="AttributesSerialize"></BinaryString>
              <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
              <ContentId name="LinkedSource">
                <null>
                </null>
              </ContentId>
              <bool name="DefinesCapabilities">false</bool>
              <BinaryString name="SerializedOverrides">
              </BinaryString>
              <string name="Source"><![CDATA[-- DefaultData.lua - a new profile's defaults, from the configuration's
-- DefaultData tree (ctx.config.DefaultData, see Schema.lua; plan B1). 2.x
-- built the same table from Settings/Default Data (its convertTree);
-- Migrations/FromV2 converts that folder into the tree exactly as
-- convertTree read it, and build() fills the same groups 2.x's build filled,
-- so every 2.x profile gains exactly what it gained before (the merge into
-- saved profiles is Migration.lua's, unchanged).
-- Pure: the tree comes in as an argument. The Kernel freezes the
-- configuration, and profiles are changed in place, so build() copies the
-- tree first and never returns a table of it.
local DefaultData = {}

local function copy(value)
	if type(value) ~= "table" then
		return value
	end
	local out = {}
	for key, item in pairs(value) do
		out[key] = copy(item)
	end
	return out
end

function DefaultData.build(tree)
	local saved = if type(tree) == "table" then copy(tree) else {}

	local result = {
		Meta      = { SchemaVersion = 1 },
		Stats     = saved.Stats     or { Cash = 0, Wins = 0 },
		Pets      = saved.Pets      or { Equipped = {}, Owned = {} },
		Purchases = saved.Purchases or { Gamepasses = {} },
	}

	-- Every other top-level key of the tree, as 2.x added the other groups of
	-- Settings > Default Data. A top-level Meta is ignored, as in 2.x (the
	-- Schema refuses one).
	for key, value in pairs(saved) do
		if result[key] == nil then
			result[key] = value
		end
	end

	return result
end

return DefaultData
]]></string>
              <int64 name="SourceAssetId">-1</int64>
              <BinaryString name="Tags"></BinaryString>
            </Properties>
          </Item>
          <Item class="ModuleScript" referent="40">
            <Properties>
              <string name="Name">Migration</string>
              <BinaryString name="AttributesSerialize"></BinaryString>
              <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
              <ContentId name="LinkedSource">
                <null>
                </null>
              </ContentId>
              <bool name="DefinesCapabilities">false</bool>
              <BinaryString name="SerializedOverrides">
              </BinaryString>
              <string name="Source"><![CDATA[-- Migration.lua - deepClone/deepMerge/migrate, ported verbatim from v1
-- DataStoreSystem/Modules/DataStoreManager.lua (module-local functions there).
-- Pure: no dependency on game services or sibling modules.

local Migration = {}

function Migration.deepClone(original)
	if typeof(original) ~= "table" then return original end
	local clone = {}
	for key, value in pairs(original) do
		clone[key] = Migration.deepClone(value)
	end
	return clone
end

function Migration.deepMerge(to, from)
	for k, v in pairs(from) do
		if typeof(v) == "table" then
			if to[k] == nil then
				to[k] = Migration.deepClone(v)
			elseif typeof(to[k]) == "table" then
				Migration.deepMerge(to[k], v)
			end
		elseif to[k] == nil then
			to[k] = v
		end
	end
	return to
end

-- v1 read module-level `DefaultData` directly; v2 takes it as a parameter
-- so this module has no load-time dependency on ServerStorage.
function Migration.migrate(loaded, defaults)
	if not loaded then return Migration.deepClone(defaults) end
	local merged = Migration.deepClone(loaded)
	Migration.deepMerge(merged, defaults)
	merged.Meta = merged.Meta or {}
	merged.Meta.SchemaVersion = (defaults.Meta and defaults.Meta.SchemaVersion) or 1
	return merged
end

return Migration
]]></string>
              <int64 name="SourceAssetId">-1</int64>
              <BinaryString name="Tags"></BinaryString>
            </Properties>
          </Item>
          <Item class="ModuleScript" referent="41">
            <Properties>
              <string name="Name">PurchaseManager</string>
              <BinaryString name="AttributesSerialize"></BinaryString>
              <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
              <ContentId name="LinkedSource">
                <null>
                </null>
              </ContentId>
              <bool name="DefinesCapabilities">false</bool>
              <BinaryString name="SerializedOverrides">
              </BinaryString>
              <string name="Source"><![CDATA[-- Per-instance purchase routing. Profile handlers stage receipt and reward
-- together; confirmation waits for a successful profile write. Legacy handlers
-- remain supported but arbitrary external effects are not transactional.
-- Keep receipt IDs indefinitely: automatic eviction can duplicate old retries.
-- Roblox callbacks and the public API use dot syntax on this bound instance.
local function shouldProcess(history, purchaseId)
	return table.find(history, purchaseId) == nil
end

local function record(history, purchaseId, cap)
	table.insert(history, purchaseId)
	while cap and #history > cap do
		table.remove(history, 1)
	end
end

-- deps = { manager, freeModeValue: BoolValue?, trove, onPurchase: function? }
--
-- `onPurchase(player, kind, id, currencySpent)` is an OPTIONAL callback, added
-- 2026-08-25 for Notifications System. It is a plain function rather than an
-- Instance so this module stays constructible in Lune with no `game` global;
-- init.lua owns the BindableEvent it fires. Three call sites, all AFTER the
-- grant is recorded - announcing a purchase that then failed to land would be
-- worse than announcing nothing - and every one of them wrapped in a pcall,
-- because a listener's bug must never break a receipt.
-- No unconditional game-service access here - see header comment. new() is
-- constructible in Lune with zero `game` global at all.
local function new(deps)
	local manager = deps.manager
	local trove = deps.trove

	-- Fire-and-forget: a listener that errors must not change what
	-- ProcessReceipt returns to Roblox.
	local function announce(player, kind, id, currencySpent)
		if type(deps.onPurchase) ~= "function" then
			return
		end
		local ok, err = pcall(deps.onPurchase, player, kind, id, currencySpent)
		if not ok then
			warn("[PurchaseManager] onPurchase listener failed:", err)
		end
	end

	-- productId (number) -> handler function. Instance state (was a module
	-- upvalue in v1).
	local handlers = {}
	local profileHandlers = {}
	local receiptBusy = {}
	local pendingNotices = {}
	local warnedLegacy = {}

	-- uid -> passId -> bool. Instance state (was a module upvalue in v1).
	local passCache = {}
	local passNeedsSave = {}

	local pm = {}

	-- Register a handler for a dev product ID. callback receives (player,
	-- receiptInfo) and must return true if the purchase was granted. v1 had no
	-- duplicate-registration guard - a second registerHandler call for the same
	-- productId silently overwrites the first. Ported verbatim: no guard here either.
	function pm.registerHandler(productId, callback)
		handlers[productId] = callback
		profileHandlers[productId] = nil
	end

	-- grant(player, draft, receipt) -> true, presentationPayload
	-- Only mutate draft; no yields or external effects. Notify after persistence.
	function pm.registerProfileHandler(productId, grant, afterCommit)
		assert(type(grant) == "function", "Profile grant callback required")
		profileHandlers[productId] = {grant=grant, afterCommit=afterCommit}
		handlers[productId] = nil
	end

	function pm.InvalidatePass(player, passId)
		local uid = player.UserId
		if passCache[uid] then
			passCache[uid][passId] = nil
		end
	end

	-- Verifies ownership independently of FreeMode. Saved gamepasses include
	-- gifts and are checked before a negative marketplace cache entry. Only
	-- trusted server code may grant them. API/update failures return false in
	-- the second result so callers can retry; FreeMode never creates ownership.
	function pm.HasPassStrict(player, passId)
		if type(passId) ~= "number" or passId <= 0 or passId % 1 ~= 0 then return false, true end

		local uid = player.UserId
		passCache[uid] = passCache[uid] or {}
		passNeedsSave[uid] = passNeedsSave[uid] or {}
		-- Keep this lookup valid if PlayerRemoving clears the tables while an API yields.
		local cache, needsSave = passCache[uid], passNeedsSave[uid]
		local function queueRegistration()
			local ok, queued = pcall(manager.Save, manager, player)
			if not ok or queued == false then return false end
			needsSave[passId] = nil
			return true
		end

		-- Confirmed positive rights are permanent; negatives cannot hide gifts.
		if cache[passId] == true then
			return true, true
		end

		-- Check datastore
		local dsOk, data = pcall(function() return manager:Get(player) end)
		if dsOk and data and data.Purchases and data.Purchases.Gamepasses and data.Purchases.Gamepasses[tostring(passId)] then
			if needsSave[passId] and not queueRegistration() then return true, false end
			cache[passId] = true
			return true, true
		end
		if cache[passId] == false then
			return false, dsOk and data ~= nil
		end

		-- Fall back to MarketplaceService (lazy fetch: only reached on a cache
		-- AND datastore miss).
		local MarketplaceService = game:GetService("MarketplaceService")
		local mpsOk, owns = pcall(MarketplaceService.UserOwnsGamePassAsync, MarketplaceService, uid, passId)
		if not mpsOk then return false,false end -- transient failure must not poison the negative cache
		local result = owns==true

		-- Persist to datastore if confirmed owned
		if result then
			needsSave[passId] = true
			local stored = pcall(function()
				manager:Update(player, function(d)
					d.Purchases = d.Purchases or {}
					d.Purchases.Gamepasses = d.Purchases.Gamepasses or {}
					d.Purchases.Gamepasses[tostring(passId)] = true
				end)
			end)
			-- Do not cache success before recording it: a failed Update must
			-- retry instead of leaving an API-only entitlement for this session.
			if not stored or not queueRegistration() then return true,false end
		elseif not dsOk or not data then
			-- An API denial cannot establish absence of a saved gift if data failed to load.
			return false, false
		end
		cache[passId] = result

		return result, true
	end

	-- CLEANUP302 (3.0.1, security): PromptGamePassPurchaseFinished comes
	-- from the client side of the purchase prompt, so an exploiter can fire
	-- it with wasPurchased = true for a pass they never bought. It only ever
	-- invalidates the cache (a real purchase must be re-checked at once) and
	-- grants nothing. The public "bought the pass" announcement waits for
	-- HasPassStrict (UserOwnsGamePassAsync, or the saved record), in its own
	-- thread so the handler never yields, and is made once per player and
	-- pass: never for a pass the player's profile already recorded before the
	-- event (an old purchase or a gift), never twice for repeated events.
	local announcedPasses = {}
	function pm._onGamePassPurchaseFinished(player, passId, wasPurchased)
		if not wasPurchased or type(passId) ~= "number" then
			return
		end
		local uid = player.UserId
		local seen = announcedPasses[uid] or {}
		announcedPasses[uid] = seen
		if seen[passId] then
			return
		end
		seen[passId] = true -- claimed now: a second event during the check is ignored
		local knownBefore = passCache[uid] ~= nil and passCache[uid][passId] == true
		pm.InvalidatePass(player, passId)
		task.spawn(function()
			local okData, data = pcall(function() return manager:Get(player) end)
			local saved = okData and type(data) == "table" and type(data.Purchases) == "table" and type(data.Purchases.Gamepasses) == "table"
				and data.Purchases.Gamepasses[tostring(passId)] == true
			if knownBefore or saved then
				return -- owned before this event: not a new purchase
			end
			local okOwns, owns = pcall(pm.HasPassStrict, player, passId)
			if okOwns and owns == true then
				announce(player, "gamepass", passId, nil)
			else
				seen[passId] = nil -- not confirmed: a later real event may still announce
			end
		end)
	end

	function pm.HasPass(player,passId)
		if deps.freeModeValue and deps.freeModeValue.Value then return true end
		return pm.HasPassStrict(player,passId)
	end

	-- Directly grants a dev product in FreeMode without going through
	-- MarketplaceService. Returns true if the handler ran successfully.
	function pm.GrantProduct(player, productId)
		if not (deps.freeModeValue and deps.freeModeValue.Value) then return false end
		local profileHandler = profileHandlers[productId]
		if profileHandler then
			local ok, applied, fresh, payload = pcall(manager.ApplyProfilePurchase, manager, player, nil, function(draft)
				return profileHandler.grant(player, draft, nil)
			end)
			if not ok or not applied then return false end
			if fresh and profileHandler.afterCommit then pcall(profileHandler.afterCommit, player, payload) end
			announce(player, "product", productId, 0)
			return true
		end
		local handler = handlers[productId]
		if not handler then return false end
		local ok, granted = pcall(handler, player, nil)
		if ok and granted then
			-- FreeMode grants announce too: it is how a purchase
			-- announcement can be tested in Studio without spending Robux.
			announce(player, "product", productId, 0)
		end
		return ok and granted
	end

	-- Called by Task 5's `MarketplaceService.ProcessReceipt` binding.
	local function processReceipt(receiptInfo)
		local Players = game:GetService("Players")
		local player = Players:GetPlayerByUserId(receiptInfo.PlayerId)
		if not player then
			return Enum.ProductPurchaseDecision.NotProcessedYet
		end

		local handler = handlers[receiptInfo.ProductId]
		local profileHandler = profileHandlers[receiptInfo.ProductId]

		-- Fix #1: load data, ensure ReceiptHistory exists, dedupe before running
		-- the handler.
		local data = manager:Get(player)
		data.Purchases = data.Purchases or {}
		data.Purchases.ReceiptHistory = data.Purchases.ReceiptHistory or {}
		local history = data.Purchases.ReceiptHistory

		local noticeKey = tostring(receiptInfo.PlayerId)..":"..receiptInfo.PurchaseId
		if shouldProcess(history, receiptInfo.PurchaseId) then
			if not handler and not profileHandler then
				warn("[PurchaseManager] Unhandled product ID:", receiptInfo.ProductId)
				return Enum.ProductPurchaseDecision.NotProcessedYet
			end
			local granted, payload
			if profileHandler then
				local applied, fresh, result = manager:ApplyProfilePurchase(player, receiptInfo.PurchaseId, function(draft)
					return profileHandler.grant(player, draft, receiptInfo)
				end)
				granted, payload = applied, result
			else
				-- Compatibility only: arbitrary legacy callbacks cannot be made
				-- atomic retroactively. Built-in rewards use registerProfileHandler.
				if not warnedLegacy[receiptInfo.ProductId] then
					warnedLegacy[receiptInfo.ProductId] = true
					warn("[PurchaseManager] Migrate product to registerProfileHandler for atomic rewards:", receiptInfo.ProductId)
				end
				granted = handler(player, receiptInfo) == true
				if granted then record(history, receiptInfo.PurchaseId) end
			end
			if not granted then return Enum.ProductPurchaseDecision.NotProcessedYet end
			pendingNotices[noticeKey] = {payload=payload, player=player, afterCommit=profileHandler and profileHandler.afterCommit}
		end
		-- Even a cached duplicate may be an earlier unconfirmed write.
		if not manager:SaveAndWait(player) then return Enum.ProductPurchaseDecision.NotProcessedYet end
		local notice = pendingNotices[noticeKey]
		pendingNotices[noticeKey] = nil
		if notice then
			if notice.afterCommit then pcall(notice.afterCommit, player, notice.payload) end
			announce(player, "product", receiptInfo.ProductId, receiptInfo.CurrencySpent)
		end
		return Enum.ProductPurchaseDecision.PurchaseGranted
	end

	function pm._processReceipt(receiptInfo)
		local uid = receiptInfo.PlayerId
		if receiptBusy[uid] then return Enum.ProductPurchaseDecision.NotProcessedYet end
		receiptBusy[uid] = true
		local ok, result = pcall(processReceipt, receiptInfo)
		receiptBusy[uid] = nil
		if not ok then
			warn("[PurchaseManager] Receipt remains pending:", tostring(result))
			return Enum.ProductPurchaseDecision.NotProcessedYet
		end
		return result
	end

	-- v1 rule 1: PromptGamePassPurchaseFinished / PlayerRemoving move here,
	-- registered on deps.trove instead of connected at require time. Wrapped in
	-- one pcall - see header comment for why (Lune has no `game` global at all;
	-- in live Roblox this pcall is always a no-op success).
	pcall(function()
		local MarketplaceService = game:GetService("MarketplaceService")
		local Players = game:GetService("Players")

		-- Auto-invalidate cache when a pass is purchased mid-session
		trove:Connect(MarketplaceService.PromptGamePassPurchaseFinished, pm._onGamePassPurchaseFinished)

		trove:Connect(Players.PlayerRemoving, function(player)
			passCache[player.UserId] = nil
			announcedPasses[player.UserId] = nil
			passNeedsSave[player.UserId] = nil
			for key, notice in pairs(pendingNotices) do
				if notice.player == player then pendingNotices[key] = nil end
			end
		end)
	end)

	return pm
end

return {
	new = new,
	shouldProcess = shouldProcess,
	record = record,
}
]]></string>
              <int64 name="SourceAssetId">-1</int64>
              <BinaryString name="Tags"></BinaryString>
            </Properties>
          </Item>
          <Item class="ModuleScript" referent="42">
            <Properties>
              <string name="Name">StudioSafeId</string>
              <BinaryString name="AttributesSerialize"></BinaryString>
              <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
              <ContentId name="LinkedSource">
                <null>
                </null>
              </ContentId>
              <bool name="DefinesCapabilities">false</bool>
              <BinaryString name="SerializedOverrides">
              </BinaryString>
              <string name="Source"><![CDATA[-- StudioSafeId.lua - ports v1 DataStoreSystem/Modules/StudioSafeId.lua as a factory.
-- v1 kept `studioIdCache` / `nextStudioId` as module-level state, shared by every caller.
-- v2 moves both into a closure returned by calling this module: two factory instances
-- (StudioSafeId() called twice) get independent caches/counters and share nothing.
--
-- v1 additionally gated synthetic-id generation behind `RunService:IsStudio()` (only
-- assigning synthetic ids for UserId <= 0 while running in Studio; in Play/production it
-- returned player.UserId as-is even if <= 0). Lune's @lune/roblox has no live `game`, so
-- there is no RunService to query here - and per the Config.lua precedent from task 1,
-- that live-service decision is dropped rather than faked. Concretely: positive UserIds
-- always pass straight through (identical to v1 in both branches), and this module now
-- always synthesizes an id for UserId <= 0 instead of only doing so in Studio. Since
-- production UserIds are always positive, this does not change production behavior; it
-- only removes the (never-exercised in prod) pass-through-a-non-positive-id branch.
-- v1's DebugPrintMappings (gated behind IS_STUDIO, print-only) is dropped for the same
-- reason - it is not part of this task's produced interface.

return function()
	-- Cache of unique IDs for Studio test clients (per-factory-instance, not shared).
	local studioIdCache = {} -- [Player] = uniqueId
	local nextStudioId = 1000000 -- Start with high numbers to avoid conflicts

	local function generateStudioId()
		local id = nextStudioId
		nextStudioId = nextStudioId + 1
		return id
	end

	local safeId = {}

	-- Get a safe UserId that works for both real accounts and Studio test clients.
	function safeId.GetSafeUserId(player: Player): number
		if player.UserId > 0 then
			-- Positive UserId (real account) - use as-is.
			return player.UserId
		end

		-- Non-positive UserId - generate/reuse a unique synthetic id.
		if not studioIdCache[player] then
			studioIdCache[player] = generateStudioId()
			warn(string.format("[StudioSafeId] Generated unique ID %d for test client %s (original: %d, Player: %s)",
				studioIdCache[player], player.Name, player.UserId, tostring(player)))
		end
		return studioIdCache[player]
	end

	-- Get a safe string key for DataStore operations.
	function safeId.GetSafeKey(player: Player): string
		return tostring(safeId.GetSafeUserId(player))
	end

	-- Clean up cache when player leaves.
	function safeId.CleanupPlayer(player: Player)
		if studioIdCache[player] then
			studioIdCache[player] = nil
		end
	end

	return safeId
end
]]></string>
              <int64 name="SourceAssetId">-1</int64>
              <BinaryString name="Tags"></BinaryString>
            </Properties>
          </Item>
          <Item class="ModuleScript" referent="43">
            <Properties>
              <string name="Name">ValueReplicator</string>
              <BinaryString name="AttributesSerialize"></BinaryString>
              <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
              <ContentId name="LinkedSource">
                <null>
                </null>
              </ContentId>
              <bool name="DefinesCapabilities">false</bool>
              <BinaryString name="SerializedOverrides">
              </BinaryString>
              <string name="Source"><![CDATA[-- ValueReplicator.lua - ports v1 DataStoreSystem/Modules/ValueReplicator.lua verbatim.
-- Recursively mirrors any Lua table into Folder/Value objects under a root (e.g., "PlayerData").
-- Supports NumberValue, BoolValue, StringValue; arrays become children named "[1]", "[2]"...
-- Stateless: `parent` is only ever used as an Instance parent (v1 called it `player`, but never
-- touched any Player-specific API), so any Instance - a Folder in specs, a Player at runtime -
-- works identically.
-- v1 sets DataReady itself at the end of PushRoot (not by a separate manager) - ported as-is.
local ValueReplicator = {}

local function isArray(t)
	if typeof(t) ~= "table" then return false end
	local n = #t
	for i = 1, n do
		if t[i] == nil then return false end
	end
	for k in pairs(t) do
		if typeof(k) ~= "number" then return false end
	end
	return true
end

local function valueClassOf(v)
	local tv = typeof(v)
	if tv == "number" then return "NumberValue" end
	if tv == "boolean" then return "BoolValue" end
	if tv == "string" then return "StringValue" end
	return nil
end

local function ensureFolder(parent: Instance, name: string): Folder
	local f = parent:FindFirstChild(name)
	if not f or f.ClassName ~= "Folder" then
		if f then f:Destroy() end
		f = Instance.new("Folder")
		f.Name = name
		f.Parent = parent
	end
	return f
end

local function setScalar(parent: Instance, name: string, val: any)
	local className = valueClassOf(val)
	if not className then return end
	local obj = parent:FindFirstChild(name)
	if not obj or obj.ClassName ~= className then
		if obj then obj:Destroy() end
		obj = Instance.new(className)
		obj.Name = name
		obj.Parent = parent
	end
	if obj.Value ~= val then
		obj.Value = val
	end
end

local function mirrorTable(parent: Instance, name: string, t: any)
	local folder = ensureFolder(parent, name)

	if isArray(t) then
		local present = {}
		for i = 1, #t do
			local childName = string.format("[%d]", i)
			present[childName] = true
			local v = t[i]
			if typeof(v) == "table" then
				mirrorTable(folder, childName, v)
			else
				setScalar(folder, childName, v)
			end
		end
		for _, child in ipairs(folder:GetChildren()) do
			if not present[child.Name] then child:Destroy() end
		end
	else
		local seen = {}
		for k, v in pairs(t) do
			local key = tostring(k)
			seen[key] = true
			if typeof(v) == "table" then
				mirrorTable(folder, key, v)
			else
				setScalar(folder, key, v)
			end
		end
		for _, child in ipairs(folder:GetChildren()) do
			if not seen[child.Name] then child:Destroy() end
		end
	end
end

function ValueReplicator.PushRoot(parent: Instance, rootName: string, data: any)
	local root = parent:FindFirstChild(rootName)
	if not root or root.ClassName ~= "Folder" then
		if root then root:Destroy() end
		root = Instance.new("Folder")
		root.Name = rootName
		root.Parent = parent
	end

	mirrorTable(parent, rootName, data)

	local ready = root:FindFirstChild("DataReady")
	if not ready then
		ready = Instance.new("BoolValue")
		ready.Name = "DataReady"
		ready.Parent = root
	end
	ready.Value = true
end

return ValueReplicator
]]></string>
              <int64 name="SourceAssetId">-1</int64>
              <BinaryString name="Tags"></BinaryString>
            </Properties>
          </Item>
        </Item>
      </Item>
    </Item>
    <Item class="Folder" referent="44">
      <Properties>
        <string name="Name">StarterPlayerScripts</string>
        <BinaryString name="AttributesSerialize"></BinaryString>
        <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
        <Color3 name="IconTint">
          <R>0</R>
          <G>0</G>
          <B>0</B>
        </Color3>
        <bool name="DefinesCapabilities">false</bool>
        <BinaryString name="SerializedOverrides">
        </BinaryString>
        <int64 name="SourceAssetId">-1</int64>
        <BinaryString name="Tags"></BinaryString>
      </Properties>
      <Item class="LocalScript" referent="45">
        <Properties>
          <string name="Name">KernelClient</string>
          <BinaryString name="AttributesSerialize"></BinaryString>
          <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
          <bool name="Disabled">false</bool>
          <ContentId name="LinkedSource">
            <null>
            </null>
          </ContentId>
          <token name="RunContext">0</token>
          <bool name="DefinesCapabilities">false</bool>
          <BinaryString name="SerializedOverrides">
          </BinaryString>
          <string name="Source"><![CDATA[-- RBLX Essentials Kernel - client boot. The only LocalScript in the ecosystem.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local HttpService = game:GetService("HttpService")

-- 3.0: packs, Config modules and the server's piece decisions arrive with the
-- first replication; read them only once it is complete.
if not game:IsLoaded() then
	game.Loaded:Wait()
end

local rsRoot = ReplicatedStorage:WaitForChild("RBLXEssentials")
local kernelFolder = rsRoot:WaitForChild("Kernel")
local Loader = require(kernelFolder:WaitForChild("Loader"))
local Version = require(kernelFolder:WaitForChild("Version"))
local Net = require(kernelFolder:WaitForChild("Net"))
local SFX = require(kernelFolder:WaitForChild("SFX"))
local Notify = require(kernelFolder:WaitForChild("Notify"))
local Signal = require(kernelFolder:WaitForChild("Signal"))
local Trove = require(kernelFolder:WaitForChild("Packages"):WaitForChild("Trove"))
local Schema = require(kernelFolder:WaitForChild("Schema"))
local Config = require(kernelFolder:WaitForChild("Config"))
local uiFolder = kernelFolder:WaitForChild("UI")
local MenuSystem = require(uiFolder:WaitForChild("MenuSystem"))
local Pieces = require(uiFolder:WaitForChild("Pieces"))

local playerGui = Players.LocalPlayer:WaitForChild("PlayerGui")

-- The server publishes its piece decisions before a player can join; this
-- bounded wait only covers a server that is still starting.
local encoded = rsRoot:GetAttribute(Pieces.WINNERS_ATTRIBUTE)
local waited = 0
while encoded == nil and waited < 10 do
	waited += task.wait(0.1)
	encoded = rsRoot:GetAttribute(Pieces.WINNERS_ATTRIBUTE)
end
-- nil: every piece is adopted as in 2.x
local decisions = Pieces.decodeDecisions(encoded, function(text)
	return HttpService:JSONDecode(text)
end)
if decisions == nil then
	warn("[RBLXE] UI: no piece decisions from the server; every piece is adopted as in 2.x")
end

-- RT-5: the server publishes which packs did not start there once its boot
-- is done; wait for it (bounded) so this client does not boot them.
local serverEncoded = rsRoot:GetAttribute(Loader.SERVER_REPORT_ATTRIBUTE)
local waitedReport = 0
while serverEncoded == nil and waitedReport < 30 do
	waitedReport += task.wait(0.1)
	serverEncoded = rsRoot:GetAttribute(Loader.SERVER_REPORT_ATTRIBUTE)
end
local serverReport = Loader.decodeServerReport(serverEncoded, function(text)
	return HttpService:JSONDecode(text)
end)
if serverReport == nil then
	warn("[RBLXE] no boot report from the server within 30s; this client boots every pack")
end

local pieces = Pieces.runtime(decisions, { wait = task.wait, warn = warn, defer = task.defer })
pieces:attach(playerGui)
local ui = MenuSystem.new(playerGui, { pieces = pieces })

local report = Loader.boot({
	side = "Client",
	sharedPacks = rsRoot:WaitForChild("Packs"),
	kernelApi = { Version = Version, Trove = Trove, Signal = Signal, Net = Net, SFX = SFX, Notify = Notify },
	ui = ui,
	configLib = Config,
	schemaLib = Schema,
	configRoots = { shared = rsRoot:FindFirstChild("Config") },
	serverReport = serverReport,
	extensions = rsRoot:FindFirstChild("Extensions"),
})

print(Loader.formatReport("Client", report))
for id, reason in pairs(report.failed) do
	warn(string.format("[RBLXE] %s failed: %s", id, reason))
end
for id, reason in pairs(report.skipped) do
	warn(string.format("[RBLXE] %s skipped: %s", id, reason))
end
for _, notice in ipairs(report.notices or {}) do
	warn("[RBLXE] " .. notice)
end
playerGui:SetAttribute("RBLXE_ClientBooted", true)
]]></string>
          <int64 name="SourceAssetId">-1</int64>
          <BinaryString name="Tags"></BinaryString>
        </Properties>
      </Item>
    </Item>
    <Item class="Folder" referent="46">
      <Properties>
        <string name="Name">RBLXE_Major3</string>
        <BinaryString name="AttributesSerialize">AQAAAAoAAABSQkxYRV9Ob3RlAnIAAABSQkxYIEVzc2VudGlhbHMgMy54OiBpbnN0YWxsIGl0IHdpdGggdGhlIFJCTFggRXNzZW50aWFscyAzLjAgcGx1Z2luIG9yIFRvb2xzLiBBIDIueCBwbHVnaW4gcmVmdXNlcyB0aGlzIGNvbnRhaW5lci4=</BinaryString>
        <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
        <Color3 name="IconTint">
          <R>0</R>
          <G>0</G>
          <B>0</B>
        </Color3>
        <bool name="DefinesCapabilities">false</bool>
        <BinaryString name="SerializedOverrides">
        </BinaryString>
        <int64 name="SourceAssetId">-1</int64>
        <BinaryString name="Tags"></BinaryString>
      </Properties>
    </Item>
  </Item>
</roblox>