--[[
    State.lua
    TactBacon
    v1.2.0
]]

-- Services

local HttpService = game:GetService("HttpService")

-- Dependencies

local Maid = shared("Maid") ---@module Maid

-- Constants

local USE_OS_CLOCK_OVER_TICK = true

-- Typings

export type State = {
	Name: string,
	LastActivatedAt: number,
	LastDeactivatedAt: number,
	Maid: {},

	Activate: (self: State) -> (),
	Deactivate: (self: State) -> (),
	BindHandler: (self: State, handler: (self: State) -> (), handlerName: string?) -> string,
	UnbindHandler: (self: State, handlerName: string) -> (),
	ClearHandlers: (self: State) -> (),
	IsActive: (self: State) -> boolean,
	SetDeactivates: (self: State, state: string, deactivates: boolean) -> (),
	SetActivates: (self: State, state: string, activates: boolean) -> (),
	SetBlockedBy: (self: State, state: string, blocked: boolean) -> (),
	GetStatesToActivate: (self: State) -> { [string]: boolean },
	ShouldDeactivate: (self: State, state: string) -> boolean,
	IsConflictingWith: (self: State, state: string) -> boolean,

	new: (stateName: string) -> State,
	__index: State,

	Activated: RBXScriptSignal,
	Deactivated: RBXScriptSignal,

	_activated: BindableEvent,
	_deactivated: BindableEvent,
	_isActive: boolean,
	_handlers: { [string]: (self: State) -> () },
	_blockedBy: { [string]: boolean },
	_deactivates: { [string]: boolean },
	_activates: { [string]: boolean },
	_statesToDeactivate: { [string]: boolean },
	_statesToActivate: { [string]: boolean },

	_callHandlers: (self: State) -> (),
}

-- Main module table

local State: State = {} :: State
State.__index = State

-- Private Functions

function State:_callHandlers()
	for _, handler in self._handlers do
		task.spawn(function()
			handler(self)
		end)
	end
end

-- Public Functions

--Activates the state.
function State:Activate()
	self._isActive = true
	self.LastActivatedAt = USE_OS_CLOCK_OVER_TICK and os.clock() or tick()
	self:_callHandlers()
	self._activated:Fire(self._statesToDeactivate, self._statesToActivate)
end

--Deactivates the state.
function State:Deactivate()
	self._isActive = false
	self.LastDeactivatedAt = USE_OS_CLOCK_OVER_TICK and os.clock() or tick()
	self:_callHandlers()
	self._deactivated:Fire()
end

--Binds a handler to the state.  Handlers are called when the state is
--activated or deactivated and are passed the state and a maid.
--@param handler function The handler `(self: State) -> ()`
--@param handlerName string? The name of the handler.  If not provided, a GUID will be generated.
--@return string HandlerName The name of the handler.
function State:BindHandler(handler: (self: State) -> (), handlerName: string?): string
	local id = handlerName or HttpService:GenerateGUID(false)
	self._handlers[id] = handler
	return id
end

--Unbinds a handler from the state.
--@param handlerName string The name of the handler to unbind.
function State:UnbindHandler(handlerName: string)
	self._handlers[handlerName] = nil
end

--Clears all handlers from the state.
function State:ClearHandlers()
	self._handlers = {}
end

--Returns whether or not the state is active.
--@return boolean IsActive Whether or not the state is active.
function State:IsActive(): boolean
	return self._isActive
end

--Sets whether or not the state is blocked by another state.
--@param state string The name of the state to set the blocked value.
--@param blocked boolean Whether or not the state is blocked by the other state.
function State:SetBlockedBy(state: string, blocked: boolean)
	self._blockedBy[state] = blocked
end

--Sets whether or not the state deactivates another state.
--@param state string The name of the state to set the deactivates value.
--@param deactivates boolean Whether or not the state deactivates the other state.
function State:SetDeactivates(state: string, deactivates: boolean)
	self._deactivates[state] = deactivates
end

--Sets whether or not the state activates another state.
--@param state string The name of the state to set the activate value.
--@param activates boolean Whether or not the state activates the other state.
function State:SetActivates(state: string, activates: boolean)
	self._activates[state] = activates
end

--Returns whether or not the state is conflicting with another state.
--@param state string The name of the state to check.
--@return boolean IsConflictingWith Whether or not the state is conflicting with the other state.
function State:IsConflictingWith(state: string): boolean
	return self._blockedBy[state] == true or self._deactivates[state] == true
end

--Returns whether or not the state deactivates another state.
--@param state string The name of the state to check.
--@return boolean ShouldDeactivate Whether or not the state deactivates the other state.
function State:ShouldDeactivate(state: string): boolean
	return self._deactivates[state] ~= nil
end

--Returns a table of state names that will be activated as well if this one is activated.
--@return table StatesToActivate The states that would be activated.
function State:GetStatesToActivate(): { [string]: boolean }
	local statesToActivate = {}
	for key, value in self._activates do
		if value then
			statesToActivate[key] = true
		end
	end
	return statesToActivate
end

-- Constructor

--Creates a new state.
--@param stateName string The name of the state to create.
--@return table State The newly created state.
function State.new(stateName: string): State
	local self = setmetatable({}, State)

	self.Name = stateName
	self._isActive = false

	self.Maid = Maid.new()

	self._activated = Instance.new("BindableEvent")
	self.Activated = self._activated.Event

	self._deactivated = Instance.new("BindableEvent")
	self.Deactivated = self._deactivated.Event

	self._handlers = {}
	self._blockedBy = {}
	self._deactivates = {}
	self._activates = {}

	self.LastActivatedAt = 0
	self.LastDeactivatedAt = 0

	return self
end

-- Return

return State


--[[ File Info

	Author(s): ChiefWildin
	Module: TimerUtils.luau
	Version: 1.0.0

]]

-- Services

-- Dependencies

-- Types

-- Module Declaration

local TimerUtils = {}

-- Constants

-- Global Variables

-- Objects

-- Private Functions

-- Public Functions

function TimerUtils:TimeToNextSignificantUpdate(timeRemaining: number): number
	if timeRemaining > 86400 then
		return timeRemaining % 86400
	elseif timeRemaining > 3600 then
		return timeRemaining % 3600
	elseif timeRemaining > 60 then
		return timeRemaining % 60
	end

	return 1
end

function TimerUtils:GetStringForTimeRemaining(timeRemaining: number): string
	timeRemaining = math.round(timeRemaining)

	if timeRemaining > 86400 then
		local days = math.round(timeRemaining / 86400)
		return `{days}D`
	elseif timeRemaining > 3600 then
		local hours = timeRemaining // 3600
		local minutes = (timeRemaining % 3600) // 60
		return `{hours}H {minutes}M`
	elseif timeRemaining > 60 then
		local minutes = timeRemaining // 60
		local seconds = timeRemaining % 60
		return `{minutes}M {seconds}S`
	else
		return `{timeRemaining}S`
	end
end

return TimerUtils


--!native
return function(a: number, b: number, t: number)
	return a + (b - a) * t
end


--[[ File Info

	Authors: ChiefWildin
	Module: rcall.luau
	Version: 1.4.0

	Retrying call. Provides a way to call functions with a retry limit and
	delay to protect against calls that fail occasionally, such as web API
	calls.

	See end of file for usage information.

]]

-- Types

type rcallOptions = {
	async: boolean?,
	failWarning: string?,
	retryDelay: number?,
	retryLimit: number?,
	requireResult: boolean?,
	silent: boolean?,
	traceback: boolean?,
	backoff: boolean?,
}

-- Constants

local BASE_DELAY_OFFSET = 1
-- Whether we should force rcall to print a stack trace for debugging
local FORCE_TRACEBACK = false

-- Private Functions

local function MainLoop(params: rcallOptions, callback: (...any) -> ...any, ...: any): ...any | nil
	local retryDelay = params.retryDelay or 2
	local retryLimit = params.retryLimit
	local customMessage = params.failWarning

	local success, result
	local retries = 0
	while not success or (params.requireResult and result == nil) do
		if retryLimit and retries >= retryLimit then
			return nil
		end

		local callResults = table.pack(pcall(callback, ...))
		success = table.remove(callResults, 1)
		result = callResults[1]

		if not success or (params.requireResult and result == nil) then
			if not params.silent then
				if customMessage then
					warn(customMessage)
				end
				-- result might be nil if requiring result, so convert to string
				local errorMessage = tostring(result)
				if params.traceback or FORCE_TRACEBACK then
					errorMessage ..= "\n" .. debug.traceback()
				end
				warn("[rcall] Failed attempt, params:", params, "error:", errorMessage)
			end

			retries += 1
			if retryLimit and retries == retryLimit then
				return nil
			end

			local actualDelay = math.max(
				if params.backoff
					then ((retryDelay + BASE_DELAY_OFFSET) ^ (math.max(retries * 0.5, 1))) - BASE_DELAY_OFFSET
					else retryDelay,
				retryDelay
			)
			task.wait(actualDelay)
		else
			return table.unpack(callResults)
		end
	end

	return nil
end

-- Main Function

--[[
	Repeatedly calls the provided `callback` function until successful. Any
	parameters provided after `callback` will be passed to it. Will return the
	results of `callback` if successful, unless it is run asynchronously.

	---

	`params` is a table with the following optional fields:
	```lua
	{
	    async: boolean, -- Whether to run the callback asynchronously. Defaults to false.
		failWarning: string, -- A custom warning to print with the system-provided warning if the callback fails.
		retryDelay: number, -- The number of seconds to wait between retries. Defaults to 2.
		retryLimit: number, -- The number of times to retry the callback before giving up. If not provided, retries infinitely.
	    requireResult: boolean, -- Whether the callback must return a result to be considered successful. Defaults to false.
		silent: boolean, -- Whether the error warning should be silent. Defaults to false.
	    traceback: boolean, -- Whether the error warning should include a traceback. Defaults to false.
		backoff: boolean, -- Whether to use exponential backoff for retry delays. Defaults to false.
	}
	```

	---

	Example:
	```lua
		rcall({retryLimit = 3}, myFunction, "hello", "world")
	```
]]
local rcall = function(params: rcallOptions, callback: (...any) -> ...any, ...: any): ...any
	local async = params.async or false

	if async then
		task.spawn(MainLoop, params, callback, ...)
		return nil
	else
		return MainLoop(params, callback, ...)
	end
end

return rcall


--[[ File Info
	CharacterUtils.luau
	@TactBacon
	Version: 1.7.3

	Provides utility functions for working with characters.  Adds callbacks for
	character spawning and provides a maid for cleaning up when the character is
	destroyed.

	Any function with the keyword `Local` in it will only work on the client and
	should not be called from the server.
]]

--[[ API

	::GetCharacter(player: Player | string, shouldYield: boolean?): Model?
		Get the character of a player and optionally yield until it exists
		Example:
		```lua
			local character = CharacterUtils:GetCharacter(player, true)
		```

	::GetLocalCharacter(shouldYield: boolean?): Model?
		Get the character of the local player and optionally yield until it
		exists.
		Example:
		```lua
			local character = CharacterUtils:GetLocalCharacter(true)
		```

	::GetAllCharacters(): {Model}
		Get all characters in the game.
		Example:
		```lua
			local characters = CharacterUtils:GetAllCharacters()
		```

	::GetAllNonLocalCharacters(): {Model}
		Get all characters in the game, except for the local player's character.
		Example:
		```lua
			local characters = CharacterUtils:GetAllNonLocalCharacters()
		```

	::OnCharacterSpawned(player: Player, callback: (character: Model, characterMaid: Maid) -> (), callbackName: string)
		Add a callback for when the player spawns that receives the character
	    model and a maid which will clean up any connections or running
	    functions when the player dies.

		This will run the callback immediately if the player is already spawned.
		Example:
		```lua
			CharacterUtils:OnCharacterSpawned(Players.OnlyTwentyCharacters, function(character: Model, characterMaid: Maid)
				print("Character spawned!")

				characterMaid:GiveTask(function()
					print("Character died!")
				end)
			end, "MySpawnCallback")
		```

	::OnAnyCharacterSpawned(callback: (character: Model, characterMaid: Maid) -> (), callbackName: string)
		Add a callback for any player, existing and future, that spawns.

		This will run the callback immediately for all existing players.
		Example:
		```lua
			CharacterUtils:OnAnyCharacterSpawned(function(character: Model, characterMaid: Maid)
				print("Character spawned!")

				characterMaid:GiveTask(function()
					print("Character died!")
				end)
			end, "MySpawnCallback")
		```

	::OnLocalCharacterSpawned(callback: (character: Model, characterMaid: Maid) -> (), callbackName: string)
		Add a callback for when the local player spawns that receives the
		character model and a maid which will clean up any connections or
		running functions when the player dies.

		This will run the callback immediately if the player is already spawned.
		Example:
		```lua
			CharacterUtils:OnLocalCharacterSpawned(function(character: Model, characterMaid: Maid)
				print("Character spawned!")

				characterMaid:GiveTask(function()
					print("Character died!")
				end)
			end, "MySpawnCallback")
		```

	::RemoveOnSpawnedCallback(player: Player, callbackName: string)
		Remove a spawn callback by name from the given player.
		Example:
		```lua
			CharacterUtils:RemoveOnSpawnedCallback(Players.OnlyTwentyCharacters, "MySpawnCallback")
		```

	::RemoveOnAnySpawnedCallback(callbackName: string)
		Remove a spawn callback by name from all players.
		Example:
		```lua
			CharacterUtils:RemoveOnAnySpawnedCallback("MySpawnCallback")
		```

	::RemoveLocalOnSpawnedCallback(callbackName: string)
		Remove a spawn callback by name from the local player.
		Example:
		```lua
			CharacterUtils:RemoveLocalOnSpawnedCallback("MySpawnCallback")
		```

	::RemoveAllCallbacksForPlayer(player: Player)
		Remove all spawn callbacks for the given player.
		Example:
		```lua
			CharacterUtils:RemoveAllCallbacksForPlayer(Players.OnlyTwentyCharacters)
		```

	::RemoveAllLocalCallbacks()
		Remove all spawn callbacks for the local player.
		Example:
		```lua
			CharacterUtils:RemoveAllLocalCallbacks()
		```

	::GetCharacterMaid(player: Player): Maid
		Get the maid for the given player.  This will create a new maid if one
		does not already exist.
		Example:
		```lua
			local characterMaid = CharacterUtils:GetCharacterMaid(Players.OnlyTwentyCharacters)
		```

	::GetLocalCharacterMaid(): Maid
		Get the maid for the local player.  This will create a new maid if one
		does not already exist.
		Example:
		```lua
			local characterMaid = CharacterUtils:GetLocalCharacterMaid()
		```

	::GetChildFromCharacter(player: Player | character: Model, childName: string, shouldYield: boolean?): Instance?
		Get a child from the given player's character.  This will yield until
		the child exists if shouldYield is true.
		Example:
		```lua
			local theirHumanoid = CharacterUtils:GetChildFromCharacter(otherPlayer, "Humanoid", true)
		```

	::GetChildFromLocalCharacter(childName: string, shouldYield: boolean?): Instance?
		Get a child from the local player's character.  This will yield until
		the child exists if shouldYield is true.
		Example:
		```lua
			local myHumanoid = CharacterUtils:GetChildFromLocalCharacter("Humanoid", true)
		```

	::PlayAnimationOnCharacter(character: Model | Player, animationId: string | number, animTrackOptions: AnimTrackOptions?, shouldYield: boolean?): AnimationTrack?
		Play an animation on the given character.  This will yield until the
		character exists if shouldYield is true.
		Example:
		```lua
			local animationTrack = CharacterUtils:PlayAnimationOnCharacter(otherPlayer, 1234567890, {
				fadeTime = 0.1,
				weight = 1,
				speed = 1,
				shouldNotCache = false,
			}, true)
		```

	::PlayAnimationOnLocalCharacter(animationId: string | number, animTrackOptions: AnimTrackOptions?, shouldYield: boolean?): AnimationTrack?
		Play an animation on the local player's character.  This will yield
		until the character exists if shouldYield is true.
		Example:
		```lua
			local animationTrack = CharacterUtils:PlayAnimationOnLocalCharacter("rbxassetid://1234567890", {
				fadeTime = 0.1,
				weight = 1,
				speed = 1,
				shouldNotCache = false,
			}, true)
		```

	::GetCharacterFromPart(part: BasePart): Model?
		Get the character from a part if one exists.  Will also return NPCs.
		Example:
		```lua
			local character = CharacterUtils:GetCharacterFromPart(part)
		```
]]

-- Services

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

-- Dependencies

local Maid = shared("Maid") ---@module Maid
local PlayerUtils = shared("PlayerUtils") ---@module PlayerUtils

-- Typing

type Maid = Maid.Maid
export type AnimTrackOptions = {
	fadeTime: number?,
	weight: number?,
	speed: number?,
	shouldNotCache: boolean?,
}

-- Main Module

--[=[
	@class CharacterUtils

	A set of utility functions for working with characters.
]=]
local CharacterUtils = {}

-- Constants

local IS_CLIENT = RunService:IsClient()

-- Global Variables

local AnySpawnCallbackConnections = {} :: { [string | number]: RBXScriptConnection }
local CharacterChildCache = {} :: { [Model]: { [string]: Instance } }
local CharacterMaids = {} :: { [Player]: { Maid } }
local LocalPlayer = Players.LocalPlayer
local SpawnCallbacks = {} :: { [Player]: {} }

-- Public Functions

--[=[
	Get the character of a player and optionally yield until it exists

	@param player Player -- The player to get the character of
	@param shouldYield boolean? -- Whether or not to yield until the character exists (will still return nil if the player leaves the game)
	@return Model? -- The character of the player if it exists
]=]
function CharacterUtils:GetCharacter(player: Player, shouldYield: boolean?): Model?
	if not player then
		return
	end

	if shouldYield then
		-- Wait for the character to exist
		while player and player.Parent and not player.Character do
			task.wait()
		end

		-- Wait for the character to be parented
		while player and player.Parent and player.Character and not player.Character.Parent do
			task.wait()
		end

		-- The player left the game while we were waiting
		if not player or not player.Parent then
			return
		end
	end

	return if player.Character and player.Character.Parent ~= nil then player.Character else nil :: Model?
end

--[=[
	Get the character of the local player and optionally yield until it exists

	@client
	@param shouldYield boolean? -- Whether or not to yield until the character exists
	@return Model? -- The character of the local player if it exists
]=]
function CharacterUtils:GetLocalCharacter(shouldYield: boolean?): Model?
	if IS_CLIENT then
		return self:GetCharacter(LocalPlayer, shouldYield)
	else
		warn(
			"GetLocalCharacter can only be called from the client, attempted to call from server.\n"
				.. debug.traceback()
		)
	end

	return nil
end

--[=[
	Get all characters of players
	@return {Model} -- The characters of all players
]=]
function CharacterUtils:GetAllCharacters(): { Model }
	local characters = {}

	for _, player in Players:GetPlayers() do
		local character = CharacterUtils:GetCharacter(player)
		if not character then
			continue
		end

		table.insert(characters, character)
	end

	return characters
end

--[=[
	Get all characters of players
	@client
	@return {Model} -- The characters of all players
]=]
function CharacterUtils:GetAllNonLocalCharacters(): { Model }
	local characters = {}

	for _, player in Players:GetPlayers() do
		if player == LocalPlayer then
			continue
		end

		local character = CharacterUtils:GetCharacter(player)
		if not character then
			continue
		end

		table.insert(characters, character)
	end

	return characters
end

--[=[
	Add a callback for when the player spawns and provide a maid for
	cleaning up any connections or running functions when the player dies.

	This will run the callback immediately if the player is already spawned.
	@param player Player -- The player to add the callback for
	@param callback function -- The callback to run when the player spawns
	@param callbackName string -- The name of the callback to add (optional)
	@param priority number? -- The priority of the callback, lower numbers run first (optional, defaults to 0)
]=]
function CharacterUtils:OnCharacterSpawned(
	player: Player,
	callback: (character: Model, characterMaid: Maid, player: Player) -> (),
	callbackName: string?,
	priority: number?
)
	local entry = {
		Callback = callback,
		Priority = priority or 0,
		Name = callbackName,
	}

	if not SpawnCallbacks[player] then
		SpawnCallbacks[player] = {}
	end

	if callbackName then
		for _, otherEntry in SpawnCallbacks[player] do
			if otherEntry.Name == callbackName then
				warn(
					`Attempt to add OnCharacterSpawned for {player.Name} but callback with name {callbackName} already exists\n{debug.traceback()}`
				)
				return
			end
		end
	end

	table.insert(SpawnCallbacks[player], entry)
	table.sort(SpawnCallbacks[player], function(a, b)
		return a.Priority < b.Priority
	end)

	if player.Character and player.Character.Parent then
		local characterMaid = CharacterUtils:GetCharacterMaid(player)
		task.spawn(callback, player.Character, characterMaid, player)
	end
end

--[=[
	Adds a callback for any player, existing and future, that spawns.

	@param callback (character: Model, characterMaid: Maid, player: Player) -> ()
	@param callbackName string? -- The name of the callback to add (optional)
]=]
function CharacterUtils:OnAnyCharacterSpawned(
	callback: (character: Model, characterMaid: Maid, player: Player) -> (),
	callbackName: string?,
	priority: number?
)
	for _, player: Player in pairs(Players:GetPlayers()) do
		self:OnCharacterSpawned(player, callback, callbackName, priority)
	end

	if callbackName then
		AnySpawnCallbackConnections[callbackName] = Players.PlayerAdded:Connect(function(player: Player)
			self:OnCharacterSpawned(player, callback, callbackName, priority)
		end)
	else
		table.insert(
			AnySpawnCallbackConnections,
			Players.PlayerAdded:Connect(function(player: Player)
				self:OnCharacterSpawned(player, callback, callbackName, priority)
			end)
		)
	end
end

--[=[
	Add a callback for when the local player spawns and provide a maid for
	cleaning up any connections or running functions when the player dies.

	@client
	@param callback function(character: Model, characterMaid: Maid) -> () -- The callback to run when the player spawns
	@param callbackName string? -- The name of the callback to add (optional)
]=]
function CharacterUtils:OnLocalCharacterSpawned(
	callback: (character: Model, characterMaid: Maid) -> (),
	callbackName: string?,
	priority: number?
)
	if IS_CLIENT then
		self:OnCharacterSpawned(LocalPlayer, callback, callbackName, priority)
	else
		warn(
			"OnLocalCharacterSpawned can only be called from the client, attempted to call from server.\n"
				.. debug.traceback()
		)
	end
end

--[=[
	Remove a callback for when the player spawns

	@param player Player -- The player to remove the callback for
	@param callbackName string -- The name of the callback to remove
]=]
function CharacterUtils:RemoveOnSpawnedCallback(player: Player, callbackName: string)
	if not SpawnCallbacks[player] then
		warn(
			`Attempt to remove CharacterSpawnCallback ({callbackName}) for {player.Name} but no callbacks exist\n{debug.traceback()}`
		)
		return
	end

	local foundEntry = true
	while foundEntry do
		foundEntry = false
		for i, entry in ipairs(SpawnCallbacks[player]) do
			if entry.Name == callbackName then
				table.remove(SpawnCallbacks[player], i)
				foundEntry = true
				break
			end
		end
	end
end

--[=[
	Remove a callback for when the local player spawns

	@client
	@param callbackName string -- The name of the callback to remove
]=]
function CharacterUtils:RemoveLocalOnSpawnedCallback(callbackName: string)
	if IS_CLIENT then
		self:RemoveOnSpawnedCallback(LocalPlayer, callbackName)
	else
		warn(
			"RemoveLocalOnSpawnedCallback can only be called from the client, attempted to call from server.\n"
				.. debug.traceback()
		)
	end
end

--[=[
	Remove a callback for all players when they spawn, future and existing
	@param callbackName string -- The name of the callback to remove
]=]
function CharacterUtils:RemoveOnAnySpawnedCallback(callbackName: string)
	for _, player: Player in (Players:GetPlayers()) do
		self:RemoveOnSpawnedCallback(player, callbackName)
	end

	if AnySpawnCallbackConnections[callbackName] then
		AnySpawnCallbackConnections[callbackName]:Disconnect()
		AnySpawnCallbackConnections[callbackName] = nil
	end
end

--[=[
	Remove all callbacks for when the player spawns

	@param player Player -- The player to remove the callbacks for
]=]
function CharacterUtils:RemoveAllCallbacksForPlayer(player: Player)
	SpawnCallbacks[player] = {}
end

--[=[
	Remove all callbacks for when the local player spawns
	@client
]=]
function CharacterUtils:RemoveAllLocalCallbacks()
	if IS_CLIENT then
		self:RemoveAllCallbacksForPlayer(LocalPlayer)
	else
		warn(
			"RemoveAllLocalCallbacks can only be called from the client, attempted to call from server.\n"
				.. debug.traceback()
		)
	end
end

--[=[
	Get the maid for the player's character

	@param player Player -- The player to get the CharacterMaid for
	@return table -- The CharacterMaid for the player
]=]
function CharacterUtils:GetCharacterMaid(player: Player | Model): Maid
	if typeof(player) == "Instance" and player:IsA("Model") then
		player = Players:GetPlayerFromCharacter(player)
	end

	assert(player, "Attempt to get a character maid for an invalid player")

	if not CharacterMaids[player] then
		CharacterMaids[player] = Maid.new()
	end

	return CharacterMaids[player]
end

--[=[
	Get the maid for the local player's character
	@client
	@return table -- The CharacterMaid for the local player
]=]
function CharacterUtils:GetLocalCharacterMaid(): Maid?
	if IS_CLIENT then
		return self:GetCharacterMaid(LocalPlayer)
	else
		warn(
			"GetLocalCharacterMaid can only be called from the client, attempted to call from server.\n"
				.. debug.traceback()
		)
	end

	return nil
end

--[=[
	Retrieves a child from a player's character
	@param player Model | Player -- The player to get the child from or alternatively their character
	@param childName string The name of the child to get
	@return Instance? Instance The child if it exists
]=]
function CharacterUtils:GetChildFromCharacter(
	player: Player | Model,
	childName: string,
	shouldYield: boolean?
): Instance?
	local character
	if typeof(player) == "Instance" and player:IsA("Model") then
		character = player
	else
		character = CharacterUtils:GetCharacter(player, shouldYield)
	end
	if not character then
		return
	end
	local cachedChild = CharacterChildCache[character] and CharacterChildCache[character][childName]
	-- The :FindFirstChild() here is a fallback that shouldn't be possible due
	-- to caching, but is left in for safety in case of any event race conditions
	return cachedChild
		or (if shouldYield then character:WaitForChild(childName) else character:FindFirstChild(childName))
end

--[=[
	Retrieves a child from the local player's character

	@client
	@param childName string -- The name of the child to get
	@return Instance? -- The child if it exists
]=]
function CharacterUtils:GetChildFromLocalCharacter(childName: string, shouldYield: boolean?): Instance?
	if IS_CLIENT then
		return CharacterUtils:GetChildFromCharacter(LocalPlayer, childName, shouldYield)
	else
		warn(
			"GetChildFromLocalCharacter can only be called from the client, attempted to call from server.\n"
				.. debug.traceback()
		)
	end

	return nil
end

--[=[
	Plays an animation on a character
	@param character Model | Player -- The character to play the animation on (can pass a player to play the animation on their character)
	@param animationId string | number -- The id of the animation to play, can be a string or number
	@param animTrackOptions table? -- The options to pass to the animation track on play as well as whether or not to cache the animation track
	@param shouldYield boolean? -- Whether or not to yield until the character exists
	@return AnimationTrack? -- The animation track if it was successfully played
]=]
function CharacterUtils:PlayAnimationOnCharacter(
	character: Model? | Player,
	animationId: string | number,
	animTrackOptions: AnimTrackOptions?,
	shouldYield: boolean?
): AnimationTrack?
	assert(typeof(character) == "Instance", "character must be an Instance, given: " .. typeof(character))
	assert(
		character:IsA("Model") or character:IsA("Player"),
		"character must be a Model or a Player, given: " .. character.ClassName
	)

	local playerObject
	if character:IsA("Player") then
		playerObject = character
		character = CharacterUtils:GetCharacter(character, shouldYield)
	else
		playerObject = Players:GetPlayerFromCharacter(character)
	end

	if not character then
		warn("Could not play animation, character not found")
		return
	end

	if not character:IsDescendantOf(workspace) then
		warn("Could not play animation, character is not a descendant of workspace")
		return
	end

	local humanoid = character:FindFirstChildOfClass("Humanoid")
		or character:FindFirstChildOfClass("AnimationController")
	if shouldYield and not humanoid then
		humanoid = character:WaitForChild("Humanoid")
	end
	if not humanoid then
		warn(`Could not play animation, Humanoid/AnimationController not found in {character:GetFullName()}`)
		return
	end

	local options: AnimTrackOptions = animTrackOptions or {}
	animationId = string.match(tostring(animationId), "%d+") or animationId

	if not animationId then
		warn(`Cannot play animation, invalid animationId, given: {animationId}`)
		return
	end

	local characterMaid
	if playerObject then
		characterMaid = CharacterUtils:GetCharacterMaid(playerObject)
		if characterMaid[`Anim.{animationId}`] then
			characterMaid[`Anim.{animationId}`]:Play()
			return characterMaid[`Anim.{animationId}`]
		end
	end

	local animator = humanoid:FindFirstChildOfClass("Animator") or Instance.new("Animator", humanoid)
	local animation = Instance.new("Animation")
	animation.AnimationId = `rbxassetid://{animationId}`
	local animationTrack = animator:LoadAnimation(animation)

	if characterMaid and not options.shouldNotCache then
		characterMaid[`Anim.{animationId}`] = animationTrack
	end

	animationTrack:Play(options.fadeTime, options.weight, options.speed)

	if options.shouldNotCache then
		animationTrack.Ended:Once(function()
			animationTrack:Destroy()
		end)
	end

	return animationTrack
end

--[=[
	Plays an animation on the local player's character

	Example:
	```lua
	local animationTrack = CharacterUtils:PlayAnimationOnLocalCharacter("rbxassetid://1234567890", {
		fadeTime = 0.1,
		weight = 1,
		speed = 1,
		shouldNotCache = false,
	}, true)
	```
	@client
	@param animationId string | number -- The id of the animation to play, can be a string or number
	@param animTrackOptions table? -- The options to pass to the animation track on play as well as whether or not to cache the animation track
	@param shouldYield boolean? -- Whether or not to yield until the character exists
	@return AnimationTrack? -- The animation track if it was successfully played
]=]
function CharacterUtils:PlayAnimationOnLocalCharacter(
	animationId: string | number,
	animTrackOptions: AnimTrackOptions?,
	shouldYield: boolean?
): AnimationTrack?
	if IS_CLIENT then
		return self:PlayAnimationOnCharacter(LocalPlayer, animationId, animTrackOptions, shouldYield)
	else
		warn(
			"PlayAnimationOnLocalCharacter can only be called from the client, attempted to call from server.\n"
				.. debug.traceback()
		)
	end

	return nil
end

--[=[
	Gets the character from a part if one exists.  Will also return NPCs.
	@param part BasePart -- The part to get the character from
	@return Model? -- The character if one exists
]=]
function CharacterUtils:GetCharacterFromPart(part: BasePart): Model?
	local character = part:FindFirstAncestorOfClass("Model")
	local humanoid
	while character and not humanoid do
		humanoid = character:FindFirstChildOfClass("Humanoid")
		if humanoid then
			break
		end
		character = character:FindFirstAncestorOfClass("Model")
	end
	return character
end

-- Initialization

function CharacterUtils:Init()
	local function onPlayerAdded(player: Player, playerMaid)
		local characterMaid = CharacterUtils:GetCharacterMaid(player)

		local function onCharacterAdded(character)
			if not SpawnCallbacks[player] then
				SpawnCallbacks[player] = {}
			end

			CharacterChildCache[character] = {}
			local function indexInstance(instance: Instance)
				CharacterChildCache[character][instance.Name] = instance
			end
			characterMaid:GiveTask(character.ChildRemoved:Connect(function(child: Instance)
				if not CharacterChildCache[character] then
					return
				end

				if CharacterChildCache[character][child.Name] == child then
					-- Attempt replacement with any duplicate named instance
					CharacterChildCache[character][child.Name] = character:FindFirstChild(child.Name)
				end
			end))
			characterMaid:GiveTask(character.ChildAdded:Connect(indexInstance))
			for _, child in character:GetChildren() do
				indexInstance(child)
			end
			characterMaid:GiveTask(function()
				CharacterChildCache[character] = nil
			end)

			-- Automatically in priority order due to the sort on insertion
			for _, entry in SpawnCallbacks[player] do
				task.spawn(entry.Callback, character, characterMaid, player)
			end
		end

		playerMaid:GiveTask(player.CharacterRemoving:Connect(function(_)
			characterMaid:DoCleaning()
		end))

		playerMaid:GiveTask(player.CharacterAdded:Connect(onCharacterAdded))
		if player.Character then
			onCharacterAdded(player.Character)
		end
	end

	PlayerUtils:OnPlayerAdded(onPlayerAdded)

	Players.PlayerRemoving:Connect(function(player: Player)
		if CharacterMaids[player] then
			CharacterMaids[player]:DoCleaning()
			CharacterMaids[player] = nil
		end
		SpawnCallbacks[player] = nil
	end)
end

return CharacterUtils


--[[ File Info

	Author(s): ChiefWildin
	Module: PermissionService.lua
	Version: 1.0.0

]]

--[=[
	@class PermissionService

	Handles centralized permissions for different features.

	:::note
	Update the `GROUP_PERMISSIONS` table to include appropriate permissions
	for the group hosting the project.
	:::
]=]

-- Dependencies

local CacheService = shared("CacheService") ---@module CacheService
local GetRemote = shared("GetRemote") ---@module GetRemote
local rcall = shared("rcall") ---@module rcall

-- Module Declaration

local PermissionService = {}

-- Constants

-- Whether to allow local test server players (UserId < 0) to have all
-- permissions.
local TEST_PLAYERS_ALL_PERMS = true

-- A list of permissions for each group. The key is the group ID and the value
-- is the minimum rank required in that group to have the permission.
local GROUP_PERMISSIONS = {
	["cmdr"] = {
		[13911975] = 253, -- Atomic Horizon / Developer
	},
	["mod"] = {
		[13911975] = 250, -- Atomic Horizon / Moderator
	},
}

-- A list of user IDs for players who should have all permissions. Skips group
-- check. Will award the developer banner to these players.
local ADMINS = {
	[1924093] = true, -- ChiefWildin
	[35637443] = true, -- TactBacon
	[1964419047] = true, -- rib3ye
	[7234662645] = true, -- DoctorKurtJohnson
}

-- Global Variables

local PermissionsRemote = GetRemote("Permissions")
local PermissionsCache = CacheService:CreateCache("Permissions", 50, 60)

-- Public Functions

--[=[
	Checks whether the player has the given permission. Returns `true` if they
	do, or `false` if their rank is too low.

	@param player Player -- The player to check
	@param permission string -- The permission label to check against
	@return boolean -- Whether the player has the permission
	@yields
]=]
function PermissionService:HasPermission(player: Player, permission: string): boolean
	if (player.UserId < 0 and TEST_PLAYERS_ALL_PERMS) or ADMINS[player.UserId] then
		return true
	end

	if not GROUP_PERMISSIONS[permission] then
		return false
	end

	local cachedCheck = PermissionsCache:Get(`{player.UserId}.{permission}`)
	if cachedCheck ~= nil then
		return cachedCheck
	end

	for groupId, minimumRank in GROUP_PERMISSIONS[permission] do
		local rank = rcall({ retryLimit = 3, retryDelay = 2 }, player.GetRankInGroup, player, groupId)
		if rank and rank >= minimumRank then
			PermissionsCache:Set(`{player.UserId}.{permission}`, true)
			return true
		end
	end

	PermissionsCache:Set(`{player.UserId}.{permission}`, false)
	return false
end

-- Task Initialization

function PermissionService:Run()
	PermissionsRemote:OnInvoke(function(player, permission)
		return self:HasPermission(player, permission)
	end)
end

return PermissionService


--[[

	Author(s): ChiefWildin
	Module: PlayerUtils.lua
	Version: 2.0.0

]]

--[=[
	@class PlayerUtils

	Provides utility functions for working with players.
]=]

-- Services

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

-- Dependencies

local Maid = shared("Maid") ---@module Maid
local rcall = shared("rcall") ---@module rcall

-- Types

type Maid = Maid.Maid
--[=[
	@interface UserInfo
	@within PlayerUtils

	.Id number
	.Username string
	.DisplayName string
	.HasVerifiedBadge boolean
]=]
type UserInfo = {
	Id: number,
	Username: string,
	DisplayName: string,
	HasVerifiedBadge: boolean,
}

-- Module

local PlayerUtils = {}

-- Global Variables

local JoinCallbacks = {} :: { [string | number]: (player: Player, playerMaid: Maid) -> () }
local PlayerMaids = {} :: { [Player]: { Maid } }
local UserInfo = {} :: { [number]: UserInfo }

-- Public Functions

--[=[
	Add a callback for any player, existing and future, that joins.

	```lua
	PlayerUtils:OnPlayerAdded(function(player, playerMaid)
		print(`Hello, {player.Name}!`)
	end, "CallbackName")
	```

	@param callback function -- The callback to invoke when a player joins
	@param callbackName string? -- The name of the callback (optional)
]=]
function PlayerUtils:OnPlayerAdded(callback: (player: Player, maid: Maid) -> (), callbackName: string?)
	if not callbackName then
		table.insert(JoinCallbacks, callback)
	else
		JoinCallbacks[callbackName] = callback
	end

	for _, player in Players:GetPlayers() do
		task.spawn(callback, player, self:GetPlayerMaid(player))
	end
end

--[=[
	Remove a spawn callback by name.

	```lua
	PlayerUtils:RemoveJoinCallback("CallbackName")
	```

	@param callbackName string -- The name of the callback to remove
]=]
function PlayerUtils:RemoveJoinCallback(callbackName: string)
	if not JoinCallbacks[callbackName] then
		return
	end

	JoinCallbacks[callbackName] = nil
end

--[=[
	Returns the maid associated with a specific player, which cleans up
	when the player disconnects.

	@param player Player -- The player to get the maid for
	@return Maid -- The PlayerMaid for the player
]=]
function PlayerUtils:GetPlayerMaid(player: Player): Maid
	assert(player, "Must provide a Player object to fetch maid")

	if not PlayerMaids[player] then
		PlayerMaids[player] = Maid.new()
	end

	return PlayerMaids[player]
end

--[=[
	Fetch the user info for the user with the specified UserId from the
	Roblox website.

	@param userId number -- The UserId to get the user info from
	@return UserInfo? -- The user's info
	@yields
]=]
function PlayerUtils:FetchUserInfo(userId: number): UserInfo?
	local cachedInfo = UserInfo[userId]
	if cachedInfo then
		return cachedInfo
	end

	local success, result = rcall({ retryLimit = 3, retryDelay = 5 }, function()
		return UserService:GetUserInfosByUserIdsAsync({ userId })
	end)

	if not success then
		return nil
	end

	if result and typeof(result) == "table" and #result > 0 then
		local userInfo = result[1]
		UserInfo[userId] = userInfo

		return userInfo
	end

	return nil
end

-- Initialization

function PlayerUtils:Init()
	Players.PlayerRemoving:Connect(function(player)
		if PlayerMaids[player] then
			PlayerMaids[player]:Destroy()
			PlayerMaids[player] = nil
		end
	end)

	local function onPlayerAdded(player: Player)
		local playerMaid = PlayerUtils:GetPlayerMaid(player)

		for _, callback in JoinCallbacks do
			task.spawn(callback, player, playerMaid)
		end
	end
	Players.PlayerAdded:Connect(onPlayerAdded)
	for _, player in Players:GetPlayers() do
		onPlayerAdded(player)
	end
end

return PlayerUtils


--!strict

--[[
	LibreGrid V.1.1.0
	Made by @completely_hazel
]]

local LibreGrid = {}

local Players = game:GetService("Players")

local Player = Players.LocalPlayer
local Mouse = Player:GetMouse()

local Types = require(script.Types)

-- Used to check if the ray hit a non-inclined surface
local function IsAnyAbs1(Vector: Vector3)
	return math.abs(Vector.X) >= 1 or math.abs(Vector.Y) >= 1 or math.abs(Vector.Z) >= 1
end

-- Used to calculate how much the reference has to be offset by to align to the grid
local function CalculateOffset(Size: number, GridSize: number)
	if GridSize <= 0 then return 0 end -- Avoid NaN

	return (Size % (GridSize * 2)) / 2
end

local function Main(Raycast: RaycastResult, Reference: BasePart | Model, GridSize: number, Rotation: CFrame,
	Configuration: Types.LibreGridConfiguration): CFrame?

	local Size = if Reference:IsA("BasePart") then Reference.Size else Reference:GetExtentsSize()
	local Normal = Raycast.Normal
	local RaycastPosition = Raycast.Position
	local HitInstance = Raycast.Instance

	if Configuration.WhitelistedNormals and not table.find(Configuration.WhitelistedNormals, Normal) then return end -- Deal with whitelisted normals

	local IsAbs = not IsAnyAbs1(Normal)
	local IsNormalBased = Configuration.NormalBased

	if IsNormalBased == true and HitInstance:IsA("BasePart") then
		RaycastPosition = HitInstance.Position + (Normal * (Size/2)) -- Modify the position so it's based on the normal instead
	end

	local Position = if IsAbs and Configuration.AllowFaceAlignment ~= false then CFrame.lookAlong(RaycastPosition, Normal)
		else CFrame.new(RaycastPosition)

	local Combination = Position * if IsAbs then Rotation:Inverse() else Rotation -- Mix the position and rotation together

	local RelativeSize = Combination:VectorToObjectSpace(Size):Abs()

	local NormalOffset = Normal * (RelativeSize / 2) -- Calculate how much the reference has to be offset to be on the surface
	local GridAlignmentOffset = Vector3.new(
		CalculateOffset(RelativeSize.X, GridSize * if NormalOffset.X == 0 then 1 else 0),
		CalculateOffset(RelativeSize.Y, GridSize * if NormalOffset.Y == 0 then 1 else 0),
		CalculateOffset(RelativeSize.Z, GridSize * if NormalOffset.Z == 0 then 1 else 0)
	) * if IsNormalBased then 0 else 1

	return (if Configuration.ForceAlignedOutput then LibreGrid:RoundCFrame(Combination + GridAlignmentOffset + NormalOffset, GridSize, Configuration.RoundingMethod)
		else LibreGrid:RoundCFrame(Combination, GridSize, Configuration.RoundingMethod) + GridAlignmentOffset + NormalOffset), Raycast
end

-- Creates a default set of raycast params to avoid the reference and the character from being detected.
local function CreateDefaultParams(Reference: Instance)
	local Params = RaycastParams.new()
	Params.FilterType = Enum.RaycastFilterType.Exclude
	Params.FilterDescendantsInstances = {Reference, Player.Character}

	return Params
end

-- Main ray function
local function RayMain(Ray: Ray, Reference: BasePart | Model, GridSize: number,
	Rotation: CFrame?, Configuration: Types.LibreGridConfiguration?)

	Configuration = Configuration or {}
	Rotation = Rotation or CFrame.identity

	local Raycast = workspace:Raycast(Ray.Origin, Ray.Direction,
		(Configuration :: Types.LibreGridConfiguration).RaycastParams or CreateDefaultParams(Reference))

	if not Raycast then return end

	return Main(Raycast, Reference, GridSize, Rotation :: CFrame, Configuration :: Types.LibreGridConfiguration)
end

-- Returns a CFrame using a custom ray. Note that this will return nil if there is no hit.
function LibreGrid:GetCFrameFromRay(Ray: Ray, Reference: BasePart | Model, GridSize: number,
	Rotation: CFrame?, Configuration: Types.LibreGridConfiguration?): (CFrame?, RaycastResult?)

	return RayMain(Ray, Reference, GridSize, Rotation, Configuration)
end

-- Returns a CFrame using the player's current mouse position. Note that this will return nil if there is no hit.
function LibreGrid:GetCFrameAtMousePosition(Reference: BasePart | Model, GridSize: number,
	Rotation: CFrame?, Configuration: Types.LibreGridConfiguration?): (CFrame?, RaycastResult?)

	local MouseRay = Mouse.UnitRay

	return RayMain(Ray.new(MouseRay.Origin, MouseRay.Direction * 15_000), Reference, GridSize, Rotation :: CFrame, Configuration :: Types.LibreGridConfiguration)
end

-- Rounds a CFrame's position to the grid
function LibreGrid:RoundCFrame(NewCFrame: CFrame, GridSize: number, RoundingFunction: Types.RoundingFunction?): CFrame
	if GridSize <= 0 then return NewCFrame end -- Avoid NaN
	local Position = NewCFrame.Position

	RoundingFunction = RoundingFunction or math.round

	return NewCFrame.Rotation + Vector3.new(
		(RoundingFunction :: Types.RoundingFunction)(Position.X/GridSize)*GridSize,
		(RoundingFunction :: Types.RoundingFunction)(Position.Y/GridSize)*GridSize,
		(RoundingFunction :: Types.RoundingFunction)(Position.Z/GridSize)*GridSize
	)
end

return LibreGrid


export type LibreGridConfiguration = {
	RaycastParams: RaycastParams?, -- By default, LibreGrid will blacklist the reference and the player's character.
	ForceAlignedOutput: boolean?, -- Force CFrames to output 100% rounded to the grid at all times (NOT RECOMMENDED). DEFAULT: false
	AllowFaceAlignment: boolean?, -- Allows the module to rotate references to match the face of tilted parts. DEFAULT: true
	NormalBased: boolean?, -- Forces the module to calculate positions based on the normal they're on instead of the ray's hit. DEFAULT: false
	WhitelistedNormals: {Vector3}?, -- If specified, LibreGrid will return nil when operating with normals that aren't specified here. DEFAULT: nil
	RoundingMethod: RoundingFunction? -- Rounding function to be used. DEFAULT: math.round
}

export type RoundingFunction = (number) -> number

return true


local Plugin = script.Parent.Parent

local React = require(Plugin.Packages.React)

local Localization = require(Plugin.Util.Localization)
local Store = require(Plugin.Util.Store)

local Tooltip = require(Plugin.Components.Tooltip)
local TabSet = require(Plugin.Components.TabSet)
local UpdateNotice = require(Plugin.Components.UpdateNotice)

-- TODO: Put these in a separate folder
local MaterialPanel = require(Plugin.Components.MaterialPanel)
local SettingsPanel = require(Plugin.Components.SettingsPanel)

local useStore = require(Plugin.Hooks.useStore)
local useTheme = require(Plugin.Hooks.useTheme)

local function App()
  local theme = useTheme()
  local selectedPanel: string = useStore('Panel')

  local panelElement = React.useMemo(function()
    if selectedPanel == 'Materials' then
      return React.createElement(MaterialPanel)
    elseif selectedPanel == 'Settings' then
      return React.createElement(SettingsPanel)
    else
      return React.createElement('TextLabel', {
        Size = UDim2.fromScale(1, 1),
        Text = "You shouldn't be able to see this.",
      })
    end
  end, { selectedPanel })

  return React.createElement('Frame', {
    BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainBackground),
    BorderSizePixel = 0,
    Size = UDim2.fromScale(1, 1),
  }, {
    TooltipContainer = React.createElement(Tooltip.Container),

    Content = React.createElement('Frame', {
      BackgroundTransparency = 1,
      Size = UDim2.fromScale(1, 1),
    }, {
      UIListLayout = React.createElement('UIListLayout', {
        SortOrder = Enum.SortOrder.LayoutOrder,
      }),

      UpdateNotice = React.createElement(UpdateNotice),

      Navbar = React.createElement(TabSet, {
        AutomaticSize = Enum.AutomaticSize.Y,
        SelectedTab = selectedPanel,
        LayoutOrder = 2,
        Size = UDim2.fromScale(1, 0),
        Tabs = {
          { Key = 'Materials', Icon = 'rbxassetid://5741677639', Text = Localization('Button.Materials') },
          { Key = 'Settings', Icon = 'rbxassetid://5747147099', Text = Localization('Button.Settings') },
        },
        OnTabSelected = function(tabName)
          Store:Set('Panel', tabName)
        end,
      }),

      Body = React.createElement('Frame', {
        BackgroundTransparency = 1,
        LayoutOrder = 3,
        Size = UDim2.fromScale(1, 1),
      }, {
        UIFlexItem = React.createElement('UIFlexItem', {
          FlexMode = Enum.UIFlexMode.Fill,
        }),
        PanelContent = React.createElement(React.Fragment, nil, panelElement),
      }),
    }),
  })
end

return App


local Plugin = script.Parent.Parent.Parent

local React = require(Plugin.Packages.React)

local Localization = require(Plugin.Util.Localization)

local MaterialPreviewImage = require(script.Parent.MaterialPreviewImage)
local Tooltip = require(Plugin.Components.Tooltip)

local useTheme = require(Plugin.Hooks.useTheme)

type Props = {
  Material: Enum.Material,
  Selected: boolean?,

  OnClick: () -> ()?,
}

local function MaterialGridItem(props: Props)
  local theme = useTheme()
  local isHover, setHover = React.useState(false)
  local isPress, setPress = React.useState(false)

  local onClick = React.useCallback(function()
    if props.OnClick then
      props.OnClick()
    end
  end, { props.OnClick })

  local onMouseEnter = React.useCallback(function()
    setHover(true)
  end, {})

  local onMouseLeave = React.useCallback(function()
    setHover(false)
    setPress(false)
  end, {})

  local onMouseButton1Down = React.useCallback(function()
    setPress(true)
  end, {})

  local onMouseButton1Up = React.useCallback(function()
    setPress(false)
  end, {})

  local borderColor = Enum.StudioStyleGuideColor.Border
  if props.Selected then
    borderColor = Enum.StudioStyleGuideColor.LinkText
  end

  local modifier = Enum.StudioStyleGuideModifier.Default
  if isPress then
    modifier = Enum.StudioStyleGuideModifier.Pressed
  elseif isHover then
    modifier = Enum.StudioStyleGuideModifier.Hover
  end

  return React.createElement('ImageButton', {
    AutoButtonColor = false,
    BackgroundColor3 = theme:GetColor(borderColor, modifier),
    [React.Event.MouseButton1Click] = onClick,
    [React.Event.MouseEnter] = onMouseEnter,
    [React.Event.MouseLeave] = onMouseLeave,
    [React.Event.MouseButton1Down] = onMouseButton1Down,
    [React.Event.MouseButton1Up] = onMouseButton1Up,
  }, {
    UICorner = React.createElement('UICorner', {
      CornerRadius = UDim.new(0, 3),
    }),

    UIPadding = React.createElement('UIPadding', {
      PaddingBottom = UDim.new(0, 1),
      PaddingLeft = UDim.new(0, 1),
      PaddingRight = UDim.new(0, 1),
      PaddingTop = UDim.new(0, 1),
    }),

    Tooltip = React.createElement(Tooltip.Trigger, {
      Text = Localization('Materials.' .. props.Material.Name),
    }),

    Frame = React.createElement('Frame', {
      BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Button, modifier),
      Size = UDim2.fromScale(1, 1),
    }, {
      UICorner = React.createElement('UICorner', {
        CornerRadius = UDim.new(0, 2),
      }),

      MaterialImage = React.createElement(MaterialPreviewImage, {
        Material = props.Material,
        Size = UDim2.fromScale(1, 1),
        Use2022Materials = true,
      }),

      UIPadding = React.createElement('UIPadding', {
        PaddingBottom = UDim.new(0, 3),
        PaddingLeft = UDim.new(0, 3),
        PaddingRight = UDim.new(0, 3),
        PaddingTop = UDim.new(0, 3),
      }),
    }),
  })
end

return MaterialGridItem


local React = require(script.Parent.Parent.Parent.Packages.React)

local MATERIAL_IMAGE_BASE = 'rbxasset://textures/TerrainTools/mtrl_%s.png'

type Props = {
  Material: Enum.Material,
  LayoutOrder: number?,
  Size: UDim2?,
  Use2022Materials: boolean?,
}

local function MaterialPreviewImage(props: Props)
  local materialKey = string.lower(props.Material.Name)

  if props.Use2022Materials and props.Material ~= Enum.Material.Air then
    materialKey ..= '_2022'
  end

  return React.createElement('ImageLabel', {
    BackgroundTransparency = 1,
    Image = string.format(MATERIAL_IMAGE_BASE, materialKey),
    LayoutOrder = props.LayoutOrder,
    Size = props.Size,
  }, {
    UICorner = React.createElement('UICorner', {
      CornerRadius = UDim.new(0, 3),
    }),
  })
end

return MaterialPreviewImage


local Plugin = script.Parent.Parent

local React = require(Plugin.Packages.React)

local Constants = require(Plugin.Util.Constants)
local Localization = require(Plugin.Util.Localization)
local Store = require(Plugin.Util.Store)

local MaterialGridItem = require(script.MaterialGridItem)
local ScrollingFrame = require(Plugin.Components.ScrollingFrame)
local Search = require(Plugin.Components.Search)

local useStore = require(Plugin.Hooks.useStore)
local useTheme = require(Plugin.Hooks.useTheme)

local function MaterialPanel()
  local theme = useTheme()
  local currentMaterial: Enum.Material = useStore('Material')
  local searchTerm: string = useStore('SearchTerm')

  local content, assetCount = React.useMemo(function()
    local numberAssets = 0
    local assetsToDisplay = {} :: any

    for _, material in Constants.MATERIALS do
      if string.find(string.lower(material.Name), string.lower(searchTerm), 1, true) then
        assetsToDisplay[material.Name] = React.createElement(MaterialGridItem, {
          Material = material,
          Selected = currentMaterial == material,
          OnClick = function()
            Store:Set('Material', material)
          end,
        })

        numberAssets += 1
      end
    end

    return assetsToDisplay, numberAssets
  end, { currentMaterial, searchTerm } :: { unknown })

  local hasAssets = assetCount ~= 0

  return React.createElement(React.Fragment, nil, {
    UIListLayout = React.createElement('UIListLayout', {
      SortOrder = Enum.SortOrder.LayoutOrder,
    }),

    Header = React.createElement('Frame', {
      AutomaticSize = Enum.AutomaticSize.Y,
      BackgroundTransparency = 1,
      LayoutOrder = 1,
      Size = UDim2.fromScale(1, 0),
    }, {
      UIPadding = React.createElement('UIPadding', {
        PaddingBottom = UDim.new(0, 3),
        PaddingLeft = UDim.new(0, 3),
        PaddingRight = UDim.new(0, 3),
        PaddingTop = UDim.new(0, 3),
      }),
      Search = React.createElement(Search, {
        PlaceholderText = Localization('Plugin.Search'),
        Text = Store:Get('SearchTerm'),
        OnTextChange = function(rbx)
          Store:Set('SearchTerm', rbx.Text)
        end,
      }),
    }),

    MaterialSelector = hasAssets and React.createElement(ScrollingFrame, {
      AutomaticCanvasSize = Enum.AutomaticSize.Y,
      LayoutOrder = 3,
      Size = UDim2.fromScale(1, 1),
    }, {
      UIGridLayout = React.createElement('UIGridLayout', {
        CellPadding = Constants.MATERIAL_GRID_PADDING,
        CellSize = Constants.MATERIAL_GRID_SIZE,
        FillDirectionMaxCells = 5,
        HorizontalAlignment = Enum.HorizontalAlignment.Center,
      }),

      UIFlexItem = React.createElement('UIFlexItem', {
        FlexMode = Enum.UIFlexMode.Shrink,
      }),

      UIPadding = React.createElement('UIPadding', {
        PaddingBottom = UDim.new(0, 3),
        PaddingLeft = UDim.new(0, 3),
        PaddingRight = UDim.new(0, 3),
      }),

      Items = React.createElement(React.Fragment, nil, content),
    }),

    NoResultsFound = not hasAssets and React.createElement('TextLabel', {
      AutomaticSize = Enum.AutomaticSize.Y,
      BackgroundTransparency = 1,
      FontFace = Font.fromEnum(Enum.Font.BuilderSans),
      LayoutOrder = 3,
      Size = UDim2.new(1, 0, 0, 30),
      Text = Localization('Notice.NoResultsFound'),
      TextColor3 = theme:GetColor(Enum.StudioStyleGuideColor.SubText),
      TextSize = 16,
      TextWrapped = true,
    }),
  })
end

return MaterialPanel


local CoreGui = game:GetService('CoreGui')

local Plugin = script.Parent.Parent

local React = require(Plugin.Packages.React)
local ReactRoblox = require(Plugin.Packages.ReactRoblox)

local Util = Plugin.Util
local Constants = require(Util.Constants)
local Settings = require(Util.Settings)
local TerrainEnum = require(Util.TerrainEnum)
local TerrainUtil = require(Util.TerrainUtil)

local useEventConnection = require(Plugin.Hooks.useEventConnection)

local function IsLockedPartAllowed(Part)
  local setting = Settings:Get('IgnoreLockedParts')

  return if setting then setting == Part.Locked else false
end

local function GetColor(Part, isConvertibleToTerrain, isLockedPartAllowed)
  if isConvertibleToTerrain and not isLockedPartAllowed then
    return if #Part:GetChildren() == 0 then Constants.OUTLINE_COLOR_ALLOW else Constants.OUTLINE_COLOR_WARNING
  else
    return Constants.OUTLINE_COLOR_ERROR
  end
end

local function CreateOutline(Part, color)
  local shape, _, size = TerrainUtil:GetPartInfo(Part)

  local DEFAULT_OUTLINE_PROPS = {
    Adornee = Part,
    AlwaysOnTop = true,
    Color3 = color,
    Transparency = 0.3,
    ZIndex = 1,
  }

  local props = table.clone(DEFAULT_OUTLINE_PROPS)

  if shape == TerrainEnum.Shape.Block or shape == TerrainEnum.Shape.Wedge then
    props.Size = size
    return 'BoxHandleAdornment', props
  elseif shape == TerrainEnum.Shape.Ball then
    props.Radius = math.min(size.X, size.Y, size.Z) / 2
    return 'SphereHandleAdornment', props
  elseif shape == TerrainEnum.Shape.Cylinder then
    props.CFrame = CFrame.Angles(math.rad(90), 0, 0)
    props.Height = size.Y
    props.Radius = math.min(size.X, size.Z) / 2
    return 'CylinderHandleAdornment', props
  elseif shape == TerrainEnum.Shape.CylinderRotate then
    props.CFrame = CFrame.Angles(0, math.rad(90), 0)
    props.Height = size.X
    props.Radius = math.min(size.Y, size.Z) / 2
    return 'CylinderHandleAdornment', props
  else
    print('Unknown Shape outline requested. Returning Block shape.')
    props.Size = size
    return 'BoxHandleAdornment', props
  end
end

local function Outline(props)
  local Part, setPart = React.useState(nil)
  local PluginMouse = props.PluginMouse :: PluginMouse

  useEventConnection(PluginMouse.Move, function()
    local camera = workspace.CurrentCamera.CFrame
    local ray = PluginMouse.UnitRay
    local RaycastResults = workspace:Raycast(camera.Position, ray.Direction * Constants.RAYCAST_LENGTH, props.RaycastParams)

    if RaycastResults and not RaycastResults.Instance:IsA('Terrain') then
      -- Only change the state if the part in the RaycastResults is different from the one in the state.
      if RaycastResults.Instance ~= Part then
        setPart(RaycastResults.Instance)
      end
    else
      setPart(nil)
    end
  end)

  if not Part then
    PluginMouse.Icon = 'rbxasset://SystemCursors/Arrow'
    return nil
  end

  local isConvertibleToTerrain = TerrainUtil:IsConvertibleToTerrain(Part)
  local isLockedPartAllowed = IsLockedPartAllowed(Part)

  PluginMouse.Icon = (isConvertibleToTerrain and not isLockedPartAllowed) and '' or 'rbxasset://SystemCursors/Forbidden'

  -- If the setting is disabled, then no need to do anymore work.
  if not Settings:Get('EnabledSelectionBox') then
    return nil
  end

  local color = GetColor(Part, isConvertibleToTerrain, isLockedPartAllowed)
  local outlineClass, outlineProps = CreateOutline(Part, color)

  return ReactRoblox.createPortal({
    PTT_Outline = React.createElement(outlineClass, outlineProps),
  }, CoreGui)
end

return Outline


local ChangeHistoryService = game:GetService('ChangeHistoryService')
local Selection = game:GetService('Selection')
local UserInputService = game:GetService('UserInputService')

local Plugin = script.Parent.Parent

local React = require(Plugin.Packages.React)

local Util = Plugin.Util
local Constants = require(Util.Constants)
local Localization = require(Util.Localization)
local Settings = require(Util.Settings)
local Store = require(Util.Store)
local TerrainUtil = require(Util.TerrainUtil)

local Components = Plugin.Components
local App = require(Components.App)
local Outline = require(Components.Outline)
local StudioWidget = require(Components.StudioWidget)

local function GetInvisibleParts()
  local ignoreList = {}

  for _, descendant in workspace:GetDescendants() do
    if descendant:IsA('BasePart') and descendant.Transparency == 1 then
      table.insert(ignoreList, descendant)
    end
  end

  return ignoreList
end

local PluginApp = React.PureComponent:extend('PluginApp')

function PluginApp:init()
  self.state = {
    guiEnabled = false,
  }

  -- This is a fix for a unintended side effect when undoing,
  -- where it will select the part and give it a outline.
  ChangeHistoryService.OnUndo:Connect(function(waypoint)
    if waypoint == 'PartToTerrain' then
      Selection:Set({})
    end
  end)

  self.plugin = self.props.plugin :: Plugin

  self.plugin.Deactivation:Connect(function()
    self:setState({ guiEnabled = false })
  end)

  -- TODO: Make PluginToolbar & PluginToolbarButton into a Roact component
  self.toolbar = self.plugin:CreateToolbar('mkargus')

  self.button = self.toolbar:CreateButton(
    Localization('Plugin.Name') .. if Constants.IS_DEV_CHANNEL then '[DEV]' else '',
    Localization('Plugin.Desc'),
    Constants.PLUGIN_BUTTON_ICON
  )

  self.button.Click:Connect(function()
    if not self.state.guiEnabled then
      self.plugin:Activate(true)
      self:setState({ guiEnabled = true })
    else
      self.plugin:Deactivate()
    end
  end)

  ----------------------------------------
  -- Plugin Mouse
  ----------------------------------------
  self.pluginMouse = self.plugin:GetMouse()

  self.raycastParams = RaycastParams.new()
  self.raycastParams.IgnoreWater = true
  self.raycastParams.FilterType = Enum.RaycastFilterType.Exclude

  self.PluginMouseClickConnection = self.pluginMouse.Button1Down:Connect(function()
    local cameraPos = workspace.CurrentCamera.CFrame.Position
    local dir = self.pluginMouse.UnitRay.Direction
    local RaycastResults = workspace:Raycast(cameraPos, dir * Constants.RAYCAST_LENGTH, self.raycastParams)

    if not RaycastResults then
      return
    end

    local obj = RaycastResults.Instance

    -- Adds in Terrain Editor's shortcut to select material.
    if UserInputService:IsKeyDown(Enum.KeyCode.LeftAlt) or UserInputService:IsKeyDown(Enum.KeyCode.RightAlt) then
      if obj:IsA('Terrain') then
        Store:Set('Material', RaycastResults.Material)
      end
      return
    end

    if not TerrainUtil:IsConvertibleToTerrain(obj) then
      return
    end

    if Settings:Get('IgnoreLockedParts') and obj.Locked then
      return
    end

    local shape, cframe, size = TerrainUtil:GetPartInfo(obj)
    local material = Store:Get('Material')
    local preserveTerrain = Settings:Get('PreserveTerrain')

    local chRecording = ChangeHistoryService:TryBeginRecording('PartToTerrain')

    local success = TerrainUtil:ConvertToTerrain(shape, material, cframe, size, preserveTerrain)
    if success then
      if Settings:Get('DeletePart') then
        obj.Parent = nil
      end
      ChangeHistoryService:FinishRecording(chRecording, Enum.FinishRecordingOperation.Commit)
    else
      ChangeHistoryService:FinishRecording(chRecording, Enum.FinishRecordingOperation.Cancel)
    end
  end)
end

function PluginApp:componentDidUpdate()
  self.button:SetActive(self.state.guiEnabled)

  if self.state.guiEnabled then
    task.spawn(function()
      self.raycastParams.FilterDescendantsInstances = Settings:Get('IgnoreInvisibleParts') and GetInvisibleParts() or {}
    end)
  end
end

function PluginApp:render()
  local state = self.state

  return React.createElement(StudioWidget, {
    plugin = self.plugin,
    Id = 'PartToTerrain',
    Title = Localization('Plugin.Name') .. if Constants.IS_DEV_CHANNEL then '[DEV]' else '',

    Enabled = state.guiEnabled,

    OverridePreviousState = true,
    FloatingSize = Vector2.new(250, 330),
    MinimumSize = Vector2.new(250, 300),

    onClose = function()
      self.plugin:Deactivate()
    end,
  }, {
    App = React.createElement(App),

    Outline = state.guiEnabled and React.createElement(Outline, {
      PluginMouse = self.pluginMouse,
      RaycastParams = self.raycastParams,
    }),
  })
end

function PluginApp:componentWillUnmount()
  self.toolbar:Destroy()
  self.button:Destroy()

  self.PluginMouseClickConnection:Disconnect()
end

return PluginApp


local Plugin = script.Parent.Parent

local React = require(Plugin.Packages.React)

local useTheme = require(Plugin.Hooks.useTheme)

type Props = {
  AutomaticCanvasSize: Enum.AutomaticSize?,
  CanvasSize: UDim2?,
  Position: UDim2?,
  ScrollBarThickness: number?,
  ScrollingDirection: Enum.ScrollingDirection?,
  Size: UDim2?,
  LayoutOrder: number?,
  children: React.ReactNode,
}

local function ScrollingFrame(props: Props)
  local theme = useTheme()

  return React.createElement('ScrollingFrame', {
    AutomaticCanvasSize = props.AutomaticCanvasSize,
    BackgroundTransparency = 1,
    BorderSizePixel = 0,
    BottomImage = 'rbxasset://textures/StudioSharedUI/ScrollBarBottom.png',
    CanvasSize = props.CanvasSize or UDim2.new(),
    LayoutOrder = props.LayoutOrder,
    MidImage = 'rbxasset://textures/StudioSharedUI/ScrollBarMiddle.png',
    Position = props.Position,
    ScrollBarImageColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainText),
    ScrollBarImageTransparency = 0.8,
    ScrollBarThickness = props.ScrollBarThickness or 8,
    ScrollingDirection = props.ScrollingDirection,
    Size = props.Size,
    TopImage = 'rbxasset://textures/StudioSharedUI/ScrollBarTop.png',
    VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar,
  }, props.children)
end

return ScrollingFrame


local Plugin = script.Parent.Parent

local React = require(Plugin.Packages.React)

local useTheme = require(Plugin.Hooks.useTheme)

type Props = {
  Disabled: boolean?,
  PlaceholderText: string?,
  Position: UDim2?,
  Text: string?,

  OnTextChange: (rbx: TextBox) -> ()?,
}

local function Search(props: Props)
  local theme = useTheme()
  local textBoxRef = React.useRef(nil :: TextBox?)
  local isFocus, setFocus = React.useState(false)
  local isHover, setHover = React.useState(false)

  local onMouseButton1Click = React.useCallback(function()
    if textBoxRef.current then
      textBoxRef.current:CaptureFocus()
    end
  end, {})

  local onTextChange = React.useCallback(function()
    if textBoxRef.current and props.OnTextChange then
      props.OnTextChange(textBoxRef.current)
    end
  end, { props.OnTextChange })

  local onMouseEnter = React.useCallback(function()
    setHover(true)
  end, {})

  local onMouseLeave = React.useCallback(function()
    setHover(false)
  end, {})

  local onFocused = React.useCallback(function()
    setFocus(true)
  end, {})

  local onFocusLost = React.useCallback(function()
    setFocus(false)
  end, {})

  local modifier = Enum.StudioStyleGuideModifier.Default
  if props.Disabled then
    modifier = Enum.StudioStyleGuideModifier.Disabled
  elseif isFocus then
    if theme.Name == 'Light' then
      modifier = Enum.StudioStyleGuideModifier.Default
    else
      modifier = Enum.StudioStyleGuideModifier.Selected
    end
  elseif isHover then
    modifier = Enum.StudioStyleGuideModifier.Hover
  end

  return React.createElement('ImageButton', {
    AutoButtonColor = false,
    AutomaticSize = Enum.AutomaticSize.Y,
    BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.InputFieldBorder, modifier),
    BorderSizePixel = 0,
    Position = props.Position,
    Size = UDim2.fromScale(1, 0),
    [React.Event.MouseButton1Click] = onMouseButton1Click,
    [React.Event.MouseEnter] = onMouseEnter,
    [React.Event.MouseLeave] = onMouseLeave,
  }, {
    UICorner = React.createElement('UICorner', {
      CornerRadius = UDim.new(0, 3),
    }),

    UIPadding = React.createElement('UIPadding', {
      PaddingBottom = UDim.new(0, 1),
      PaddingLeft = UDim.new(0, 1),
      PaddingRight = UDim.new(0, 1),
      PaddingTop = UDim.new(0, 1),
    }),

    UIFlexItem = React.createElement('UIFlexItem', {
      FlexMode = Enum.UIFlexMode.Fill,
    }),

    Container = React.createElement('Frame', {
      AutomaticSize = Enum.AutomaticSize.Y,
      BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground, modifier),
      Size = UDim2.fromScale(1, 1),
    }, {
      UICorner = React.createElement('UICorner', {
        CornerRadius = UDim.new(0, 2),
      }),

      UIPadding = React.createElement('UIPadding', {
        PaddingBottom = UDim.new(0, 3),
        PaddingLeft = UDim.new(0, 3),
        PaddingRight = UDim.new(0, 3),
        PaddingTop = UDim.new(0, 3),
      }),

      UIListLayout = React.createElement('UIListLayout', {
        FillDirection = Enum.FillDirection.Horizontal,
        Padding = UDim.new(0, 3),
        SortOrder = Enum.SortOrder.LayoutOrder,
        VerticalAlignment = Enum.VerticalAlignment.Center,
      }),

      Icon = React.createElement('ImageLabel', {
        BackgroundTransparency = 1,
        ImageColor3 = theme:GetColor(Enum.StudioStyleGuideColor.DimmedText, modifier),
        Image = 'rbxassetid://5927945389',
        ImageRectSize = Vector2.new(96, 96),
        LayoutOrder = 1,
        Size = UDim2.fromOffset(18, 18),
      }),

      Input = React.createElement('TextBox', {
        BackgroundTransparency = 1,
        BorderSizePixel = 0,
        ClearTextOnFocus = false,
        LayoutOrder = 2,
        FontFace = Font.fromEnum(Enum.Font.BuilderSans),
        PlaceholderText = props.PlaceholderText or 'Search',
        PlaceholderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.DimmedText, modifier),
        Size = UDim2.new(1, 0, 0, 20),
        Text = props.Text or '',
        TextColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainText, modifier),
        TextEditable = not props.Disabled,
        TextSize = 16,
        TextTruncate = Enum.TextTruncate.AtEnd,
        TextXAlignment = Enum.TextXAlignment.Left,
        ref = textBoxRef,
        [React.Event.Focused] = onFocused,
        [React.Event.FocusLost] = onFocusLost,
        [React.Change.Text] = onTextChange,
      }, {
        UIFlexItem = React.createElement('UIFlexItem', {
          FlexMode = Enum.UIFlexMode.Shrink,
        }),
      }),
    }),
  })
end

return Search


local Plugin = script.Parent.Parent.Parent

local React = require(Plugin.Packages.React)

local Settings = require(Plugin.Util.Settings)

local ToggleButton = require(Plugin.Components.ToggleButton)

local useTheme = require(Plugin.Hooks.useTheme)

type Props = {
  Title: string,
  SettingKey: string,
  Description: string?,
  Disabled: boolean?,
  LayoutOrder: number?,
}

local function SettingsToggleItem(props: Props)
  local theme = useTheme()
  local currentSettingValue, setSettingValue = React.useState(Settings:Get(props.SettingKey))

  local modifier = Enum.StudioStyleGuideModifier.Default
  if props.Disabled then
    modifier = Enum.StudioStyleGuideModifier.Disabled
  end

  return React.createElement('Frame', {
    AutomaticSize = Enum.AutomaticSize.Y,
    BackgroundTransparency = 1,
    LayoutOrder = props.LayoutOrder,
    Size = UDim2.fromScale(1, 0),
  }, {
    UIListLayout = React.createElement('UIListLayout', {
      SortOrder = Enum.SortOrder.LayoutOrder,
    }),

    Container = React.createElement('Frame', {
      AutomaticSize = Enum.AutomaticSize.Y,
      BackgroundTransparency = 1,
      Size = UDim2.fromScale(1, 0),
    }, {
      UIListLayout = React.createElement('UIListLayout', {
        FillDirection = Enum.FillDirection.Horizontal,
        Padding = UDim.new(0, 3),
        SortOrder = Enum.SortOrder.LayoutOrder,
        VerticalAlignment = Enum.VerticalAlignment.Center,
      }),

      HeaderText = React.createElement('TextLabel', {
        AutomaticSize = Enum.AutomaticSize.Y,
        BackgroundTransparency = 1,
        FontFace = Font.fromEnum(Enum.Font.BuilderSansBold),
        LayoutOrder = 0,
        Size = UDim2.new(1, 0, 0, 20),
        Text = props.Title,
        TextColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainText, modifier),
        TextSize = 18,
        TextWrapped = true,
        TextXAlignment = Enum.TextXAlignment.Left,
      }, {
        UIFlexItem = React.createElement('UIFlexItem', {
          FlexMode = Enum.UIFlexMode.Fill,
        }),
      }),

      Toggle = React.createElement(ToggleButton, {
        Disabled = props.Disabled,
        LayoutOrder = 1,
        Selected = currentSettingValue,
        OnClick = function(newState)
          Settings:Set(props.SettingKey, newState)
          setSettingValue(newState)
        end,
      }),
    }),

    Description = props.Description and React.createElement('TextLabel', {
      AutomaticSize = Enum.AutomaticSize.Y,
      BackgroundTransparency = 1,
      FontFace = Font.fromEnum(Enum.Font.BuilderSans),
      LayoutOrder = 2,
      RichText = true,
      Size = UDim2.fromScale(1, 0),
      Text = props.Description,
      TextColor3 = theme:GetColor(Enum.StudioStyleGuideColor.SubText, modifier),
      TextSize = 16,
      TextWrapped = true,
      TextXAlignment = Enum.TextXAlignment.Left,
    }),
  })
end

return SettingsToggleItem


local Plugin = script.Parent.Parent

local React = require(Plugin.Packages.React)

local Util = Plugin.Util
local createNextOrder = require(Util.createNextOrder)
local Constants = require(Util.Constants)
local Localization = require(Util.Localization)

local useTheme = require(Plugin.Hooks.useTheme)

local Components = Plugin.Components
local ScrollingFrame = require(Components.ScrollingFrame)

local SettingsToggleItem = require(script.SettingsToggleItem)

local function SettingsPanel()
  local theme = useTheme()
  local nextOrder = createNextOrder()

  return React.createElement(ScrollingFrame, {
    AutomaticCanvasSize = Enum.AutomaticSize.Y,
    ScrollingDirection = Enum.ScrollingDirection.Y,
    Size = UDim2.fromScale(1, 1),
  }, {
    UIListLayout = React.createElement('UIListLayout', {
      FillDirection = Enum.FillDirection.Vertical,
      HorizontalAlignment = Enum.HorizontalAlignment.Center,
      Padding = UDim.new(0, 6),
      SortOrder = Enum.SortOrder.LayoutOrder,
    }),

    UIPadding = React.createElement('UIPadding', {
      PaddingLeft = UDim.new(0, 3),
      PaddingRight = UDim.new(0, 3),
      PaddingTop = UDim.new(0, 3),
    }),

    PreserveTerrain = React.createElement(SettingsToggleItem, {
      Title = Localization('Settings.PreserveTerrain'),
      LayoutOrder = nextOrder(),
      Description = Localization('Settings.PreserveTerrainDesc'),
      SettingKey = 'PreserveTerrain',
    }),

    DeletePart = React.createElement(SettingsToggleItem, {
      Title = Localization('Settings.DeletePart'),
      LayoutOrder = nextOrder(),
      Description = Localization('Settings.DeletePartDesc'),
      SettingKey = 'DeletePart',
    }),

    EnabledSelectionBox = React.createElement(SettingsToggleItem, {
      Title = Localization('Settings.EnabledSelectionBox'),
      LayoutOrder = nextOrder(),
      Description = Localization('Settings.EnabledSelectionBoxDesc'),
      SettingKey = 'EnabledSelectionBox',
    }),

    IgnoreLockedParts = React.createElement(SettingsToggleItem, {
      Title = Localization('Settings.IgnoreLockedParts'),
      LayoutOrder = nextOrder(),
      Description = Localization('Settings.IgnoreLockedPartsDesc'),
      SettingKey = 'IgnoreLockedParts',
    }),

    IgnoreInvisibleParts = React.createElement(SettingsToggleItem, {
      Title = Localization('Settings.IgnoreInvisibleParts'),
      LayoutOrder = nextOrder(),
      Description = Localization('Settings.IgnoreInvisiblePartsDesc'),
      SettingKey = 'IgnoreInvisibleParts',
    }),

    VersionText = React.createElement('TextLabel', {
      BackgroundTransparency = 1,
      FontFace = Font.fromEnum(Enum.Font.BuilderSans),
      TextColor3 = theme:GetColor(Enum.StudioStyleGuideColor.DimmedText),
      Text = Localization('Plugin.Version', { Constants.VERSION }),
      TextSize = 16,
      Size = UDim2.new(1, 0, 0, 20),
      LayoutOrder = nextOrder(),
    }),
  })
end

return SettingsPanel


--[[
  Props:
    InitialDockState InitialDockState
    boolean Enabled
    boolean OverridePreviousState
    Vector2 FloatingSize
    Vector2 MinimumSize
    ZIndexBehavior ZIndexBehavior

    function onClose
]]

local Plugin = script.Parent.Parent

local React = require(Plugin.Packages.React)
local ReactRoblox = require(Plugin.Packages.ReactRoblox)

local StudioWidget = React.PureComponent:extend('StudioWidget')

StudioWidget.defaultProps = {
  InitialDockState = Enum.InitialDockState.Float,
  Enabled = false,
  OverridePreviousState = false,
  FloatingSize = Vector2.zero,
  MinimumSize = Vector2.zero,
  ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
}

function StudioWidget:init()
  local props = self.props

  local DockWidgetPluginGuiInfo = DockWidgetPluginGuiInfo.new(
    props.InitialDockState,
    props.Enabled,
    props.OverridePreviousState,
    props.FloatingSize.X,
    props.FloatingSize.Y,
    props.MinimumSize.X,
    props.MinimumSize.Y
  )

  local pluginGui = props.plugin:CreateDockWidgetPluginGui(props.Id, DockWidgetPluginGuiInfo)

  pluginGui.Name = props.Id
  pluginGui.Title = props.Title
  pluginGui.ZIndexBehavior = props.ZIndexBehavior

  pluginGui:BindToClose(props.onClose)

  self.pluginGui = pluginGui
end

function StudioWidget:render()
  return ReactRoblox.createPortal(self.props.children, self.pluginGui)
end

function StudioWidget:componentDidUpdate(lastProps)
  if lastProps.Enabled ~= self.props.Enabled then
    self.pluginGui.Enabled = self.props.Enabled
  end
end

function StudioWidget:componentWillUnmount()
  self.pluginGui:Destroy()
end

return StudioWidget


local Plugin = script.Parent.Parent

local React = require(Plugin.Packages.React)

local Tooltip = require(Plugin.Components.Tooltip)

local useTheme = require(Plugin.Hooks.useTheme)

type Props = {
  Disabled: boolean?,
  DisplayText: boolean?,
  Icon: string?,
  LayoutOrder: number?,
  Selected: boolean?,
  Text: string?,

  OnClick: () -> ()?,
}

local function Tab(props: Props)
  local theme = useTheme()
  local isHover, setHover = React.useState(false)

  local onMouseEnter = React.useCallback(function()
    setHover(true)
  end, {})

  local onMouseLeave = React.useCallback(function()
    setHover(false)
  end, {})

  local onClick = React.useCallback(function()
    if not props.Disabled and props.OnClick then
      props.OnClick()
    end
  end, { props.OnClick, props.Disabled } :: { unknown })

  local iconSize = 20
  if not props.DisplayText then
    iconSize = 24
  end

  local font = Enum.Font.BuilderSans
  local textColor = Enum.StudioStyleGuideColor.MainText
  local borderColor = Enum.StudioStyleGuideColor.Border

  local modifier = Enum.StudioStyleGuideModifier.Default
  if props.Disabled then
    modifier = Enum.StudioStyleGuideModifier.Disabled
  elseif props.Selected then
    modifier = Enum.StudioStyleGuideModifier.Selected
    font = Enum.Font.BuilderSansBold
    if theme.Name == 'Light' then
      textColor = Enum.StudioStyleGuideColor.BrightText
    end
  elseif isHover then
    modifier = Enum.StudioStyleGuideModifier.Hover
  end

  return React.createElement('ImageButton', {
    AutoButtonColor = false,
    AutomaticSize = Enum.AutomaticSize.Y,
    BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.RibbonTab, modifier),
    BorderSizePixel = 0,
    LayoutOrder = props.LayoutOrder,
    [React.Event.MouseButton1Click] = onClick,
    [React.Event.MouseEnter] = onMouseEnter,
    [React.Event.MouseLeave] = onMouseLeave,
  }, {
    UIPadding = React.createElement('UIPadding', {
      PaddingBottom = UDim.new(0, 3),
      PaddingLeft = UDim.new(0, 3),
      PaddingRight = UDim.new(0, 3),
      PaddingTop = UDim.new(0, 3),
    }),

    UIFlexItem = React.createElement('UIFlexItem', {
      FlexMode = Enum.UIFlexMode.Fill,
      ItemLineAlignment = Enum.ItemLineAlignment.Stretch,
    }),

    UIListLayout = React.createElement('UIListLayout', {
      FillDirection = Enum.FillDirection.Horizontal,
      HorizontalAlignment = Enum.HorizontalAlignment.Center,
      Padding = UDim.new(0, 6),
      SortOrder = Enum.SortOrder.LayoutOrder,
      VerticalAlignment = Enum.VerticalAlignment.Center,
    }),

    Borders = React.createElement('Folder', nil, {
      Tooltip = not props.DisplayText and React.createElement(Tooltip.Trigger, {
        Text = props.Text,
      }),

      Selected = props.Selected and React.createElement('Frame', {
        AnchorPoint = Vector2.one,
        BackgroundTransparency = 1,
        Position = UDim2.new(1, 3, 1, 3),
        Size = UDim2.new(1, 6, 1, 6),
      }, {
        Top = React.createElement('Frame', {
          BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.LinkText),
          BorderSizePixel = 0,
          Size = UDim2.new(1, 0, 0, 2),
        }),

        Right = React.createElement('Frame', {
          AnchorPoint = Vector2.one,
          BackgroundColor3 = theme:GetColor(borderColor),
          BorderSizePixel = 0,
          Position = UDim2.fromScale(1, 1),
          Size = UDim2.new(0, 1, 1, -2),
        }),

        Left = React.createElement('Frame', {
          AnchorPoint = Vector2.yAxis,
          BackgroundColor3 = theme:GetColor(borderColor),
          BorderSizePixel = 0,
          Position = UDim2.fromScale(0, 1),
          Size = UDim2.new(0, 1, 1, -2),
        }),
      }),

      Bottom = not props.Selected and React.createElement('Frame', {
        AnchorPoint = Vector2.yAxis,
        BackgroundColor3 = theme:GetColor(borderColor),
        BorderSizePixel = 0,
        Position = UDim2.new(0, -3, 1, 3),
        Size = UDim2.new(1, 6, 0, 1),
      }),
    }),

    ImageLabel = React.createElement('ImageLabel', {
      BackgroundTransparency = 1,
      Image = props.Icon,
      ImageColor3 = theme:GetColor(textColor, modifier),
      LayoutOrder = 1,
      Size = UDim2.fromOffset(iconSize, iconSize),
    }),

    TextLabel = props.DisplayText and React.createElement('TextLabel', {
      AutomaticSize = Enum.AutomaticSize.X,
      BackgroundTransparency = 1,
      FontFace = Font.fromEnum(font),
      LayoutOrder = 2,
      Size = UDim2.fromOffset(0, 24),
      Text = props.Text,
      TextColor3 = theme:GetColor(textColor, modifier),
      TextSize = 16,
    }),
  })
end

return Tab


local TAB_ICON_SIZE = 20
local TAB_INNER_PADDING = 6
local TAB_OUTER_PADDING = 2

local TextService = game:GetService('TextService')

local Plugin = script.Parent.Parent

local React = require(Plugin.Packages.React)

local Tab = require(Plugin.Components.Tab)

local createNextOrder = require(Plugin.Util.createNextOrder)

type Props = {
  AutomaticSize: Enum.AutomaticSize?,
  LayoutOrder: number?,
  SelectedTab: string?,
  Size: UDim2?,
  Tabs: {
    {
      Disabled: boolean?,
      Key: string,
      Icon: string,
      Text: string,
    }
  },

  OnTabSelected: (tab: string) -> ()?,
}

local function canTextBeDisplayed(tabs, tabSize): boolean
  if #tabs > 0 then
    for _, tab in tabs do
      local textWidth = TextService:GetTextSize(tab.Text, 16, Enum.Font.BuilderSansBold, Vector2.new(100000, 20)).X
      local totalWidth = TAB_ICON_SIZE + TAB_INNER_PADDING + textWidth + (TAB_OUTER_PADDING * 2)

      if totalWidth >= tabSize then
        return false
      end
    end
  end

  return true
end

local function TabSet(props: Props)
  local currentWidth, setWidth = React.useState(0)

  local onTabSelected = React.useCallback(function(key: string)
    if props.OnTabSelected then
      props.OnTabSelected(key)
    end
  end, {})

  local onAbsoluteSizeChange = React.useCallback(function(rbx: Frame)
    setWidth(rbx.AbsoluteSize.X)
  end, {})

  local nextOrder = createNextOrder()

  local children = {
    UIListLayout = React.createElement('UIListLayout', {
      FillDirection = Enum.FillDirection.Horizontal,
      HorizontalAlignment = Enum.HorizontalAlignment.Center,
      SortOrder = Enum.SortOrder.LayoutOrder,
    }),
  } :: any

  local textDisplayed = canTextBeDisplayed(props.Tabs, currentWidth / #props.Tabs)

  for _, tab in props.Tabs do
    children[tab.Key] = React.createElement(Tab, {
      Disabled = tab.Disabled,
      DisplayText = textDisplayed,
      Icon = tab.Icon,
      LayoutOrder = nextOrder(),
      Selected = props.SelectedTab == tab.Key,
      Text = tab.Text,
      OnClick = function()
        onTabSelected(tab.Key)
      end,
    })
  end

  return React.createElement('Frame', {
    AutomaticSize = props.AutomaticSize,
    BackgroundTransparency = 1,
    LayoutOrder = props.LayoutOrder,
    Size = props.Size,
    [React.Change.AbsoluteSize] = onAbsoluteSizeChange,
  }, {
    Elements = React.createElement(React.Fragment, nil, children),
  })
end

return TabSet


local Plugin = script.Parent.Parent

local React = require(Plugin.Packages.React)

local useTheme = require(Plugin.Hooks.useTheme)

local IMAGE_BASE = 'rbxasset://textures/RoactStudioWidgets/toggle_%s_%s.png'

type Props = {
  AnchorPoint: Vector2?,
  Disabled: boolean?,
  LayoutOrder: number?,
  Position: UDim2?,
  Selected: boolean?,

  OnClick: (value: boolean) -> ()?,
}

local function ToggleButton(props: Props)
  local theme = useTheme()
  local themeKey = string.lower(theme.Name)

  local modiferKey = 'off'
  if props.Disabled and props.Selected then
    modiferKey = 'on_disable'
  elseif props.Selected then
    modiferKey = 'on'
  elseif props.Disabled then
    modiferKey = 'disable'
  end

  local onClick = React.useCallback(function()
    if not props.Disabled and props.OnClick then
      props.OnClick(not props.Selected)
    end
  end, { props.OnClick, props.Disabled } :: { unknown })

  return React.createElement('ImageButton', {
    AnchorPoint = props.AnchorPoint,
    AutoButtonColor = false,
    BackgroundTransparency = 1,
    Image = string.format(IMAGE_BASE, modiferKey, themeKey),
    LayoutOrder = props.LayoutOrder,
    Position = props.Position,
    Size = UDim2.fromOffset(40, 24),
    [React.Event.MouseButton1Click] = onClick,
  })
end

return ToggleButton


local MarketplaceService = game:GetService('MarketplaceService')

local Plugin = script.Parent.Parent

local React = require(Plugin.Packages.React)

local Constants = require(Plugin.Util.Constants)
local Localization = require(Plugin.Util.Localization)

local useTheme = require(Plugin.Hooks.useTheme)

local function UpdateNotice()
  local theme = useTheme()
  local isOutdated, setOutdated = React.useState(false)

  React.useEffect(function()
    local pluginId = Constants.IS_DEV_CHANNEL and Constants.DEV_UPDATE_CHECKER_ID or Constants.UPDATE_CHECKER_ID

    local thread = task.spawn(function()
      local success, info = pcall(MarketplaceService.GetProductInfo, MarketplaceService, pluginId)

      if success then
        local latestVersion = string.match(info.Description, '([0-9]+%.[0-9]+%.[0-9]+)')
        if latestVersion and latestVersion ~= Constants.VERSION then
          setOutdated(true)
        end
      end
    end)

    return function()
      task.cancel(thread)
    end
  end, {})

  return isOutdated
    and React.createElement('Frame', {
      AutomaticSize = Enum.AutomaticSize.Y,
      BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Titlebar),
      BorderSizePixel = 0,
      Size = UDim2.fromScale(1, 0),
    }, {
      UIListLayout = React.createElement('UIListLayout', {
        FillDirection = Enum.FillDirection.Horizontal,
        Padding = UDim.new(0, 6),
        SortOrder = Enum.SortOrder.LayoutOrder,
      }),

      TextLabel = React.createElement('TextLabel', {
        AutomaticSize = Enum.AutomaticSize.Y,
        BackgroundTransparency = 1,
        FontFace = Font.fromEnum(Enum.Font.BuilderSans),
        LayoutOrder = 2,
        RichText = true,
        Text = Localization('Notice.Outdated'),
        TextColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainText),
        TextSize = 16,
        TextWrapped = true,
        TextXAlignment = Enum.TextXAlignment.Left,
      }, {
        UIFlexItem = React.createElement('UIFlexItem', {
          FlexMode = Enum.UIFlexMode.Fill,
          ItemLineAlignment = Enum.ItemLineAlignment.Stretch,
        }),
      }),

      UIPadding = React.createElement('UIPadding', {
        PaddingBottom = UDim.new(0, 3),
        PaddingLeft = UDim.new(0, 3),
        PaddingRight = UDim.new(0, 3),
        PaddingTop = UDim.new(0, 3),
      }),

      ImageLabel = React.createElement('ImageLabel', {
        BackgroundTransparency = 1,
        Image = 'rbxassetid://7152791295',
        ImageColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainText),
        Size = UDim2.fromOffset(18, 18),
      }),
    })
end

return UpdateNotice


local StudioSettings = settings().Studio

local Plugin = script.Parent.Parent

local React = require(Plugin.Packages.React)

local StudioThemeContext = React.createContext(StudioSettings.Theme)

local function StudioThemeProvider(props)
  local theme, setTheme = React.useState(StudioSettings.Theme)

  React.useEffect(function()
    local connection = StudioSettings.ThemeChanged:Connect(function()
      setTheme(StudioSettings.Theme)
    end)

    return function()
      connection:Disconnect()
    end
  end, {})

  return React.createElement(StudioThemeContext.Provider, {
    value = theme,
  }, props.children)
end

return {
  Context = StudioThemeContext,
  Provider = StudioThemeProvider,
}


local React = require(script.Parent.Parent.Packages.React)

local function useEventConnection<T...>(event: RBXScriptSignal<T...>, callback: (T...) -> ())
  React.useEffect(function()
    local connection = event:Connect(callback)

    return function()
      connection:Disconnect()
    end
  end, {})
end

return useEventConnection


local React = require(script.Parent.Parent.Packages.React)
local useEventConnection = require(script.Parent.useEventConnection)
local Store = require(script.Parent.Parent.Util.Store)

local function useStore(key: string)
  local value, setValue = React.useState(Store:Get(key))

  useEventConnection(Store:GetChangedSignal(key), function(newValue)
    setValue(newValue)
  end)

  return value
end

return useStore


local React = require(script.Parent.Parent.Packages.React)

local StudioTheme = require(script.Parent.Parent.Context.StudioTheme)

local function useTheme(): StudioTheme
  local theme = React.useContext(StudioTheme.Context)
  return theme
end

return useTheme


local Constants = {}

Constants.RAYCAST_LENGTH = 15000
Constants.VOXEL_RESOLUTION = 4

----------------------------------------
-- Meta settings
----------------------------------------
Constants.PLUGIN_BUTTON_ICON = 'rbxassetid://5768049797'

Constants.OUTLINE_COLOR_ALLOW = Color3.fromRGB(56, 142, 60)
Constants.OUTLINE_COLOR_WARNING = Color3.fromRGB(245, 124, 0)
Constants.OUTLINE_COLOR_ERROR = Color3.fromRGB(211, 47, 47)

----------------------------------------
-- Update Checking
----------------------------------------
Constants.IS_DEV_CHANNEL = true
Constants.VERSION = '2025.06.01'
Constants.UPDATE_CHECKER_ID = 261634767
Constants.DEV_UPDATE_CHECKER_ID = 4685764627

----------------------------------------
-- Material related
----------------------------------------
Constants.MATERIAL_GRID_PADDING = UDim2.fromOffset(3, 3)
Constants.MATERIAL_GRID_SIZE = UDim2.fromOffset(55, 55)

Constants.MATERIALS = {
  Enum.Material.Air,
  Enum.Material.Asphalt,
  Enum.Material.Basalt,
  Enum.Material.Brick,
  Enum.Material.Cobblestone,
  Enum.Material.Concrete,
  Enum.Material.CrackedLava,
  Enum.Material.Glacier,
  Enum.Material.Grass,
  Enum.Material.Ground,
  Enum.Material.Ice,
  Enum.Material.LeafyGrass,
  Enum.Material.Limestone,
  Enum.Material.Mud,
  Enum.Material.Pavement,
  Enum.Material.Rock,
  Enum.Material.Salt,
  Enum.Material.Sand,
  Enum.Material.Sandstone,
  Enum.Material.Slate,
  Enum.Material.Snow,
  Enum.Material.Water,
  Enum.Material.WoodPlanks,
}

return table.freeze(Constants)


local RESOLUTION = require(script.Parent.Constants).VOXEL_RESOLUTION

return function(cframe: CFrame, size: Vector3): (Region3, number)
  local inv = cframe:Inverse()
  local x = size * inv.RightVector
  local y = size * inv.UpVector
  local z = size * inv.LookVector

  local w = math.abs(x.X) + math.abs(x.Y) + math.abs(x.Z)
  local h = math.abs(y.X) + math.abs(y.Y) + math.abs(y.Z)
  local d = math.abs(z.X) + math.abs(z.Y) + math.abs(z.Z)

  local pos = cframe.Position
  local halfSize = Vector3.new(w, h, d) / 2

  local region = Region3.new(pos - halfSize, pos + halfSize):ExpandToGrid(RESOLUTION)
  local regionVolume = (region.Size.X / RESOLUTION) * (region.Size.Y / RESOLUTION) * (region.Size.Z / RESOLUTION)

  return region, regionVolume
end


local StudioService = game:GetService('StudioService')

local Table = script.LocalizationTable
local LocaleId = StudioService.StudioLocaleId
local Translator
local FallbackTranslator

Translator = Table:GetTranslator(LocaleId)
if LocaleId ~= 'en_US' then
  FallbackTranslator = Table:GetTranslator('en_US')
end

local function Localization(id: string, args: { any }?): string
  local returnValue
  local success = pcall(function()
    returnValue = Translator:FormatByKey(id, args)
  end)
  if not success and FallbackTranslator or returnValue == '' then
    pcall(function()
      returnValue = FallbackTranslator:FormatByKey(id, args)
    end)
  end

  if not returnValue then
    return id
  end

  return returnValue
end

return Localization


--!nonstrict
local plugin = plugin or script:FindFirstAncestorWhichIsA('Plugin')

local DEFAULT_SETTINGS = {
  DeletePart = true,
  EnabledSelectionBox = true,
  IgnoreLockedParts = false,
  PreserveTerrain = true,
  IgnoreInvisibleParts = true
}

local Settings = {}
Settings._values = table.clone(DEFAULT_SETTINGS)
Settings._updateListeners = {}

if plugin then
  for key, defaultValue in Settings._values do
    local savedValue = plugin:GetSetting(key)

    if savedValue == nil then
      Settings._values[key] = defaultValue
      plugin:SetSetting(key, defaultValue)
    else
      Settings._values[key] = savedValue
    end
  end
end

function Settings:Get(key: string)
  if self._values[key] == nil then
    error('Invaild setting name: ' ..tostring(key), 2)
  end

  return self._values[key]
end

function Settings:Set(key: string, value: any)
  self._values[key] = value

  if plugin then
    plugin:SetSetting(key, value)
  end

  if self._updateListeners[key] then
    for callback in self._updateListeners[key] do
      task.spawn(callback, value)
    end
  end
end

function Settings:onUpdate(key: string, callback)
  local listeners = self._updateListeners[key]
  if listeners == nil then
    listeners = {}
    self._updateListeners[key] = listeners
  end
  listeners[callback] = true

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

return Settings


--!nonstrict
local BasicState = require(script.Parent.Parent.Packages.BasicState)

local State = BasicState.new({
  Material = Enum.Material.Grass,
  Panel = 'Materials',
  SearchTerm = '',
})

State.ProtectType = true

return State