--!strict

-- Jecs Definitions

type i53 = number
type i24 = number

type Ty = { Entity }
type ArchetypeId = number

type Column = { any }

type Map<K, V> = { [K]: V }

export type Archetype = {
	id: number,
	types: Ty,
	type: string,
	entities: { Entity },
	columns: { Column },
	columns_map: { [Component]: Column }
}

export type QueryInner = {
	compatible_archetypes: { Archetype },
	archetypes_map: { [number]: number },
	ids: { Component },
	filter_with: { Component },
	filter_without: { Component },
	next: () -> (Entity, ...any),
	-- world: World,
}

export type Entity<T = any> ={ __T: T }
export type Component<T = any> ={ __T: T }
export type Id<T = any> = { __T: T }
export type Pair
 = Id

type ecs_id_t<T = unknown> = Id<T> | Entity<T> | Pair<T, "Tag"> | Pair<"Tag", T>
type ecs_entity_t<T = unknown> = Id<T> | Entity<T> | Pair<T, "Tag"> | Pair<"Tag", T>
export type Item<T...> = (self: Query<T...>) -> (Entity, T...)
export type Iter<T...> = (query: Query<T...>) -> () -> (Entity, T...)

export type Cached_Query_Iter<T...> = (query: Cached_Query<T...>) -> () -> (Entity, T...)

type TypePack<T...> = (T...) -> never

export type Cached_Query<T...> = typeof(setmetatable(
	{} :: {
		iter: Cached_Query_Iter<T...>,
		archetypes: (Cached_Query<T...>, override: boolean?) -> { Archetype },
		has: (Cached_Query<T...>, Entity) -> boolean,
		fini: (Cached_Query<T...>) -> (),

		ids: { Id<any> },
		filter_with: { Id<any> }?,
		filter_without: { Id<any> }?,
		archetypes_map: { [number]: number },
		-- world: World
	},
	{} :: {
		__iter: Cached_Query_Iter<T...>,
	})
)

export type Query<T...> = typeof(setmetatable(
	{} :: {
		iter: Iter<T...>,
		with: ((Query<T...>, ...Component) -> Query<T...>),
		without: ((Query<T...>, ...Component) -> Query<T...>),
		archetypes: (Query<T...>) -> { Archetype },
		cached: (Query<T...>) -> Cached_Query<T...>,
		has: (Query<T...>, Entity) -> boolean,
		ids: { Id<any> },
		filter_with: { Id<any> }?,
		filter_without: { Id<any> }?,
		-- world: World
	},
	{} :: {
		__iter: Iter<T...>,
	}
	))

type QueryArm<T...> = () -> ()

export type Observer = {
	callback: (archetype: Archetype) -> (),
	query: QueryInner,
}

export type World = {
	archetype_edges: Map<number, Map<Entity, Archetype>>,
	archetype_index: { [string]: Archetype },
	ROOT_ARCHETYPE: Archetype,

	max_component_id: number,
	max_archetype_id: number,

	observable: Map<Component, Map<Component, { Observer }>>,

	added: <T>(World, Component<T>, (e: Entity, id: Id<T>, value: T, oldarchetype: Archetype) -> ()) -> () -> (),
	removed: <T>(World, Component<T>, (e: Entity, id: Id<T>, delete: boolean?) -> ()) -> () -> (),
	changed: <T>(World, Component<T>, (e: Entity, id: Id<T>, value: T, oldarchetype: Archetype) -> ()) -> () -> (),

	--- Enforce a check on entities to be created within desired range
	range: (self: World, range_begin: number, range_end: number?) -> (),

	--- Creates a new entity
	entity:
		& ((self: World) -> Entity<nil>)
	& ((self: World, id: Entity) -> Entity)
	& ((self: World, id: number) -> Entity)
	& (<T>(self: World, id: Component<T>) -> Component<T>),
	--- Creates a new entity located in the first 256 ids.
	--- These should be used for static components for fast access.
	component: <T>(self: World) -> Entity<T>,

	--- Gets the target of an relationship. For example, when a user calls
	--- `world:target(id, ChildOf(parent), 0)`, you will obtain the parent entity.
	target: <T, a>(
		self: World,
		id: Entity<T> | number,
		relation: Component,
		index: number?
	) -> Entity<unknown>?,

	--- Deletes an entity and all it's related components and relationships.
	delete: <T>(self: World, id: Entity<T>) -> (),

	--- Adds a component to the entity with no value
	add: (
		self: World,
		id: Entity<any>,
		component: Component
	) -> (),

	--- Assigns a value to a component on the given entity
	set: <T, a>(self: World, id: Entity<T>, component: Component, data: a) -> (),

	cleanup: (self: World) -> (),

	-- Removes all components from the entity
	clear: (self: World, entity: Entity) -> (),
	--- Removes a component from the given entity
	remove: <T, a>(self: World, id: Entity<T>, component: Component) -> (),
	--- Retrieves the value of up to 4 components. These values may be nil.
	get: & (<T, a>(World, Entity<T> | number, Component) -> a?)
	& (<T, a, b>(World, Entity<T> | number, Component, Component) -> (a?, b?))
	& (<T, a, b, c>(World, Entity<T> | number, Component, Component, Component<c>) -> (a?, b?, c?))
	& (<T, a, b, c, d>(World, Entity<T> | number, Component, Component, Component<c>, Component<d>) -> (a?, b?, c?, d?)),

	--- Returns whether the entity has the ID.
	has: (<T, a>(World, Entity<T>, Component) -> boolean)
	& (<T, a, b>(World, Entity<T>, Component, Component) -> boolean)
	& (<T, a, b, c>(World, Entity<T>, Component, Component, Component<c>) -> boolean)
	& <T, a, b, c, d>(World, Entity<T>, Component, Component, Component<c>, Component<d>) -> boolean,

	--- Get parent (target of ChildOf relationship) for entity. If there is no ChildOf relationship pair, it will return nil.
	parent: <T>(self: World, entity: Entity<T>) -> Entity?,

	--- Checks if the world contains the given entity
	contains: <T>(self: World, entity: Entity<T>) -> boolean,

	--- Checks if the entity exists
	exists: <T>(self: World, entity: Entity<T>) -> boolean,

	each: <T>(self: World, id: Id<T>) -> () -> Entity,

	children: <T>(self: World, id: Id<T>) -> () -> Entity,

	--- Searches the world for entities that match a given query
	query: ((World) -> Query<nil>)
	& ((World, Component) -> Query)
	& ((World, Component, Component) -> Query)
	& ((World, Component, Component, Component<C>) -> Query)
	& ((World, Component, Component, Component<C>, Component<D>) -> Query)
	& ((World, Component, Component, Component<C>, Component<D>, Component<E>) -> Query)
	& ((World, Component, Component, Component<C>, Component<D>, Component<E>, Component<F>) -> Query)
	& ((
		World,
		Component,
		Component,
		Component<C>,
		Component<D>,
		Component<E>,
		Component<F>,
		Component<G>
		) -> Query)
	& ((
		World,
		Component,
		Component,
		Component<C>,
		Component<D>,
		Component<E>,
		Component<F>,
		Component<G>,
		Component<H>,
		...Component<any>
		) -> Query),
}

-- UI definitions

export type UITEMPLATE_JExplorerUi = Frame & {
	InstanceTree: Frame & {
		TreeScroller: ScrollingFrame,
		ScrollContainer: Frame,

		ScrollBackgroundHorizontal: Frame,
		ScrollBackgroundVertical: Frame,
	},

	Topbar: Frame & {
		UIStroke: UIStroke,
		PathBar: Frame & {
			Segments: ScrollingFrame & {
				PathSegment: TextButton,
			}
		},
		TabBar: Frame & {
			Tabs: ScrollingFrame,
		},
		Options: ImageButton,
		QueryInputFrame: Frame & {
			Scroll: ScrollingFrame & {
				QueryInput: TextBox,
			},
			SelectQueried: TextButton,
			QueryCount: TextLabel,
		}
	}
}

type UITEMPLATE_InstanceFrame_Main = Frame & {
	Overlay: Frame & {
		Select: ImageButton,
	},
	ClassIcon: ImageLabel,
	InstanceName: TextBox,
	Arrow: ImageButton,
	Insert: ImageButton,

	PackageLinkIcon: ImageLabel,
	PendingChanges: ImageLabel,
	Reclass: ImageButton
}

export type UITEMPLATE_InstanceFrame = Frame & {
	Buffer: Frame,
	Ruler: Frame,
	Main: UITEMPLATE_InstanceFrame_Main,
}

export type UITEMPLATE_EditorFrame = Frame & {
	Items: ScrollingFrame & {
		Content: Frame & {
			UIListLayout: UIListLayout,
		},
		Overlay: Frame,
	},
	ScrollBackgroundHorizontal: Frame,
	ScrollBackgroundVertical: Frame,
}

export type UITEMPLATE_TabFrame = Frame & {
	NameBox: TextButton,
	Button: TextButton,
	Icon: ImageLabel,
}

type Array<T> = {T}
export type InstanceId = string
export type TempId = string
type UDFValue = string
type PlaceId = string

export type SerializedStyleRule = {
	display_name: string,
	selector: string,
	priority: number,
	props: {[string]: UDFValue},
	rules: {SerializedStyleRule},
	tokens: {[string]: SerializedStyleToken},
	tags: {string},
	attributes: {[string]: UDFValue}
}
export type SerializedStyleToken = {
	value: UDFValue,
	tags: {string}
}
export type SavedState = {
	repo_proxies: {
		[PlaceId]: Array<{
			display_name: string,
			repo_id: string,
			root_inst_id: InstanceId
		}>
	},
	tabs: {
		[PlaceId]: Array<{
			display_name: string,
			icon: string,
			icon_color: UDFValue,
			inst_ids: {InstanceId},
			layout_order: number
		}>
	},
	display_order: {
		default_order: number,
		class_order: {string}
	},
	data_version: number,
	invisible: {[string]: boolean},
	settings: {[string]: UDFValue},
	style: {
		rules: {SerializedStyleRule},
		tokens: {[string]: SerializedStyleToken}
	},
	icon_colors: {
		[PlaceId]: {[InstanceId]: UDFValue}
	},
	versions: {
		[PlaceId]: {
			[InstanceId]: {
				{
					timestamp: typeof(DateTime.now():ToUniversalTime()),
					props: {[string]: UDFValue},
				}
			}
		}
	},
}

export type VisibleViewportInfo = {
	visibleArray: Array<{
		entity: ExplorerEntity,
		yoffset: number,
		indentoffset: number
	}>,
	entityToIndex: {[ExplorerEntity]: number},
	entityToVisibleIndex: {[ExplorerEntity]: number},
	explorerFrameHeight: number,
	expandedHeightIndex: {[ExplorerEntity]: number},
	flatCount: number,
	visibleExpandedAncestors: Array<{
		stickyY: number?,
		rulerHeight: number
	}>,
	stickyStackDepth: number,
	viewportTop: number,
	visibleSet: {[ExplorerEntity]: boolean}
}
export type ItemGroup<T=any> = any
type Signal<T... = ()> = any
export type DragInfo_SetInstTypeProperty = {
	-- Set instance type properties
	Type: "setproperty",
	TargetInstances: {Instance},
	PropertyName: string
}
export type DragInfo_ReparentInstances = {
	-- Drag into
	Type: "reparent",
	Entities: {ExplorerEntity},
}
export type StudioIconResult = {
	Image: string,
	ImageRectOffset: Vector2,
	ImageRectSize: Vector2
}

------------------------------------ Context Menu
export type MenuDef = {
	Id: string,
	Priority: number,
	Condition: (manager: Manager, target: ExplorerEntity) -> boolean,
	Contents: ((utils: ContextMenuUtils) -> {any}) | {any},
}
export type ActionDef = {
	Id: string?,
	Callback: (manager: Manager, target: ExplorerEntity) -> (),
	Text: string,
	Desc: string?,
	Icon: (() -> string)?
}

export type ContextMenuConfigModuleReturnValue =
|	ContextMenuConfigs
|	(Plugin, Manager, ContextMenuUtils, {any}) -> ContextMenuConfigs

export type ContextMenuConfigs = {
	Menus: {MenuDef},
	Actions: {[string]: ActionDef}
}

export type ContextMenuUtils = {
	createInterface: (defaultName: string) -> ContextMenuInterface,
	getMenuIds: () -> {string},
	createModifiedMenu: (menuId: string, newContents: (ContextMenuInterface) -> {any}) -> MenuDef,

	createSubMenu: (id: string, title: string, icon: any?, content: {any}) -> any,
	createSection: (sectionName: string) -> any,
}
export type ContextMenuInterface = {
	getMenuContents: () -> {any},
	getSubMenu: (name: string) -> ContextMenuInterface,

	remove: (idx: number) -> (),
	insert: (idx: number, v: any) -> (),
	inline: (idx: number, arr: {any}) -> (),

	findAction: (name: string) -> number,
	findSubMenu: (name: string) -> number,

	relocate: (name: string, newIndex: number) -> (),
	flipOrder: () -> (),

	getSection: (sectionName: string) -> (number, number),
	addSection: (idx: number, sectionName: string, content: {any}) -> (),
}
export type ContextMenu = {
	Menu: PluginMenu,
	SubMenus: {ContextMenu},
	Contents: {any},
	Id: string,
	Actions: {PluginAction},

	ShowAsync: (self: ContextMenu, targetOverride: any?) -> (),
	Instantiate: (self: ContextMenu) -> ()
}

------------------------------------ end Context Menu
export type Tab = {
	TabFrame: GuiObject,
	Button: GuiButton,
	InsertButton: GuiButton?,
	RemoveButton: GuiButton?,
	NameBox: TextBox,
	IconLabel: ImageLabel,

	Roots: {ExplorerEntity},
	Name: string,
	Icon: string,
	IconColor: Color3,

	Toggle: () -> (),
	Remove: () -> (),
	QueryDescendants: (selector: string) -> {Instance}
}
export type RepoProxy = {
	DisplayName: string,
	RepoId: string,
	Root: Instance,

	Remove: () -> ()
}
--export type InstanceFrame = {
--	Instance: typeof(ui.InstanceFrame),
--	Connections: Group.Group,
--	CurrentEntity: ExplorerEntity?,
--	TagsToRemove: {string},

--	Assign: (InstanceFrame, entity: ExplorerEntity?, parent: ExplorerEntity?) -> (),
--	ToggleSelection: (InstanceFrame, selected: boolean) -> (),
--	Reuse: (InstanceFrame) -> ()
--}
export type ExplorerFrame = Id
export type PropertyItem = {
	name: string,
	type: ReflectionType,
	writable: boolean,
	readable: boolean
}

export type ExplorerEntity = Id
export type QueryEntity = Id
export type FrameState = {
	isLMBActive: boolean,
	tagsToRemove: {string},
	hasBeenSelected: boolean,
	shouldDeselect: boolean,
	lastClickTime: number,
	lastPackageIconPos: number,
	isAssigning: boolean,
	setIconColor: Color3?,
	icon: ImageLabel,
	packageLinkIcon: ImageLabel,
	isVisible: boolean,
	instanceName: TextBox,
	pendingChanges: ImageLabel,
	arrow: GuiButton,
	hasPackageLink: boolean,
	insertInstanceButton: GuiButton,
	reclassButton: GuiButton,
	currentFrameName: string?,
	packageLinkIconVisible: boolean,
	pendingChangesIconVisible: boolean,
	reclassIconVisible: boolean,
	currentNameBoundX: number,
	currentFrameX: number,
	lastFrameX: number,
	overlayFrame: Frame,
	isSticky: boolean,

	linkIconPos: UDim2?,
	pendingChangesIconPos: UDim2?,
	reclassIconPos: UDim2?
}

export type Components = {
	Selected: Component,
	Expanded: Component,
	AncestryLocked: Component,
	ArrowInvisible: Component,
	QueryAncestor: Component,
	QueryExpanded: Component,
	QueryInvisible: Component,
	Invisible: Component,
	IsQueried: Component,
	PendingChanges: Component,
	Deleted: Component,
	DisplayTopLevel: Component,
	HasPackageLink: Component,
	HasSideIcons: Component,

	CleanupTicket: Component<{}>,
	DeletionTicket: Component<{}>,
	ChildAddedTicket: Component<{children: {Instance}}>,
	ChildMovedTicket: Component<{children: {Instance}}>,
	CreationTicket: Component<{children: {Instance}}>,
	PropAssignTicket: Component<{ty: "virtual" | "instance"}>,

	FrameReuseTicket: Component<{frame: ExplorerFrame}>,
	FrameTagAssignTicket: Component<{frame: ExplorerFrame}>,

	FRAMEWORLD_FrameSetParentTicket: Component<{newParent: Instance?}>,
	FRAMEWORLD_Main: Component<UITEMPLATE_InstanceFrame_Main>,
	FRAMEWORLD_CurrentParent: Component<Instance>,
	FRAMEWORLD_Ruler: Component<GuiObject>,
	FRAMEWORLD_IsSticky: Component,
	QUERYWORLD_ComponentHash: Component<string>,

	Instance: Component<Instance>,
	Frame: Component<ExplorerFrame>,
	Entity: Component<ExplorerEntity>,
	FrameState: Component<FrameState>,
	FrameIconColor: Component<Color3>,

	Connections: Component<ItemGroup<RBXScriptConnection>>,
	ScriptErrors: Component<{string}>,
	ScriptWarns: Component<{string}>,
	QueryLinkedEntities: Component<{ExplorerEntity}>,
	LinkedQueryEntity: Component<QueryEntity>,
	ParentTabs: Component<{[Tab]: boolean}>,
}

export type ContextMenuInfo = {
	Menu: ContextMenu,
	Condition: (manager: Manager, target: ExplorerEntity, instance: Instance) -> boolean,
	Priority: number
}

export type WidgetState = {
	World: World,
	QueryWorld: World,
	FrameWorld: World,
	InstanceTree: GuiObject,
	TreeScroller: ScrollingFrame,

	Connections: ItemGroup<RBXScriptConnection>,
	Entities: {ExplorerEntity},
	Widget: DockWidgetPluginGui,
	CurrentSelection: {ExplorerEntity},
	Favorited: {ExplorerEntity},
	Tabs: {Tab},
	RepoProxies: {RepoProxy},
	SavedState: SavedState,

	LastSelectedEntity: ExplorerEntity?,
	ContextTarget: ExplorerEntity?,

	CurrentlyRenaming: ExplorerEntity?,
	CurrentlyInserting: {Instance}?,
	IsDragging: boolean,

	JExplorerUI: UITEMPLATE_JExplorerUi,
	DefaultInstanceActionsArray: {any},
	InstanceContextMenus: {[string]: ContextMenuInfo},
	MiscContextMenus: {[string]: ContextMenu},
	OverridePathBarText: string?,
	Enabled: boolean
}

export type HierarchyState = {
	StateCache: {[ExplorerEntity]: {invisible: boolean, expanded: boolean, isTopLevel: boolean}},
	ChildrenMap: {[ExplorerEntity]: {ExplorerEntity}},

	cleanupEntities: (Manager) -> boolean,
	cleanupEntity: (Manager, ExplorerEntity) -> (),
	rebuildHierarchyCache: (Manager) -> ({[ExplorerEntity]: {ExplorerEntity}}, {ExplorerEntity}),
	getFlattenedVisibleEntities: (Manager, invisible: {ExplorerEntity}, isScroller: boolean) -> ({ExplorerEntity}, {[ExplorerEntity]: number}, {number}),

	invalidateEntityStateCache: (ExplorerEntity) -> (),

	flushEntityCreationQueue: (Manager) -> boolean,
	queueForEntityCreation: (Manager, parent: ExplorerEntity, children: {Instance}) -> (),

	flushEntityCleanupQueue: (Manager) -> boolean,
	queueForEntityCleanup: (Manager, entity: ExplorerEntity) -> (),

	flushOnChildAddedQueue: (Manager) -> boolean,
	queueForOnChildAdded: (Manager, new_parent: ExplorerEntity, child: Instance) -> (),

	flushOnChildRemovedQueue: (Manager) -> boolean,
	queueForOnChildRemoved: (Manager, old_parent: ExplorerEntity, child: Instance) -> (),

	stepFlushEntityDeletionQueue: (Manager) -> boolean,
	queueForEntityDeletion: (Manager, entity: ExplorerEntity) -> (),

	getEntitiesBetween: (Manager, e1: ExplorerEntity, e2: ExplorerEntity) -> {ExplorerEntity},

	flushEntityPropAssignQueue: (Manager) -> (),

	getOrCreateInstanceEntity: (Manager, instance: Instance, parent: ExplorerEntity?, dontSetInInstanceToEntity: boolean?) -> ExplorerEntity,
	createVirtualEntity: (Manager, options: {
		ClassName: string,
		Name: string,
		ChildAmount: number,
		ClassIcon: StudioIconResult,
		PropTags: {Id},
		Parent: ExplorerEntity?
	}, extra: {[string]: any}) -> ExplorerEntity,
	createInstanceEntity: (Manager, instance: Instance, parent: ExplorerEntity?, dontSetInInstanceToEntity: boolean?) -> ExplorerEntity,

	deregisterFromQueryWorld: (Manager, entity: ExplorerEntity) -> (),
	registerIntoQueryWorld: (Manager, entity: ExplorerEntity, className: string, tags: {Id}, attributes: {Id}) -> (),

	updateEntityOrder: (Manager, entity: ExplorerEntity) -> (),
	getChildren: (ExplorerEntity) -> {ExplorerEntity},
}

export type ExplorerFrameLib = {
	fillGlobalPoolToLimit: (Manager) -> (),
	--fillLocalPoolToLimit: (Manager) -> (),
	initializeFrameCaches: (Manager) -> (),

	getExplorerFrame: (Manager) -> ExplorerFrame,
	assignExplorerFrame: (Manager, ExplorerFrame, assignee: ExplorerEntity?, parent: ExplorerEntity?, parentIsGame: boolean, assignSetParentTicket: boolean) -> boolean?,
	flushExplorerFrameTagAssignQueue: (Manager) -> boolean,

	queueForExplorerFrameReuse: (Manager, ExplorerFrame, ExplorerEntity?) -> (),
	flushFrameReuseQueue: (Manager) -> boolean,
	flushFrameSetParentQueue: (Manager) -> boolean,

	updateSelectionVisuals: (Manager, updateSelection: boolean) -> (),
	updateExpandedVisuals: (Manager) -> (),
	updateCollapsedVisuals: (Manager) -> (),

	updateRightEdgeIcons: (Manager, frame: ExplorerFrame, containerWidth: number?, frameX: number?, entity: ExplorerEntity?, frameState: FrameState?) -> (),
	setExplorerFrameIconColor: (Manager, frame: ExplorerFrame, iconColor: Color3?) -> (),
}

export type VirtualScroller = {
	clearHeightCache: (Manager) -> (),
	invalidateEntityHeightCache: (Manager, ExplorerEntity) -> (),

	getVisibleViewportInfo: (Manager, top: number, isScroller: boolean) -> VisibleViewportInfo,
	getLastVisibleViewportInfo: (Manager) -> VisibleViewportInfo?,
	update: (Manager, isScroller: boolean, forceUpdateIcons: boolean?) -> VisibleViewportInfo?,
	rebuildAndUpdate: (Manager, isScroller: boolean) -> (),
	trackScrolling: (Manager) -> (RBXScriptConnection, thread),

	IsScrolling: boolean,
	IsVirtualScrollerUpdating: boolean
}

export type buttonItem = {
	button: Frame,
	className: string,
	category: string
}
export type ClassSelectionMenuInfo = {
	currentOnSelectCallback: ((class: string) -> ())?,
	currentInsertTarget: number,
	classSelectList: any,
	currentVisibleButtons: {buttonItem},
	searchbar: TextBox,
	highlightFrame: Frame,
	setScope: (scope: string) -> (),
	setScopeInfo: (scope: string, frequentClasses: {string}) -> ()
}
export type ClassSelectionMenu = {
	init: (Manager) -> (),
	show: (Manager, buttonAbsolutePos: Vector2, buttonAbsoluteSize: Vector2, onSelectClass: (class: string) -> ()) -> (),
	menuInfo: ClassSelectionMenuInfo
}

export type DeferredTaskScheduler = {
	ChildGraphDirty: boolean,

	FrameReuseQueuePending: boolean,
	FrameTagAssignQueuePending: boolean,
	EntityCleanupQueuePending: boolean,
	EntityCreationQueuePending: boolean,
	EntityChildAddedQueuePending: boolean,
	EntityChildRemovedQueuePending: boolean,
	EntityPropAssignQueuePending: boolean,
}

export type SharedTabState = {
	currentlyPickingTabRoots: Tab?,
	tabInsertListener: RBXScriptConnection?,
	currentlyOpenTab: Tab?,
	contextTargetTab: Tab?
}

export type ActionInfos = {[string]: {
	Id: string,
	Text: string,
	Desc: string?,
	Icon: (() -> string)?,
	Triggered: ((target: any) -> ())?
}}

export type Manager = {
	CleanupFns: {[string]: any},
	WidgetState: WidgetState,
	TabState: SharedTabState,
	Components: Components,
	Toolbar: PluginToolbar,
	Storage: {
		repoRoots: Instance,
		idObjects: Instance,
		publicTypedef: ModuleScript,
		self: Instance
	},

	CurrentTheme: string,

	TabChanged: Signal<Tab, Tab?>,
	SelectionChanged: Signal<{ExplorerEntity}>,

	InstTreeAbsoluteSizeY: number,
	InstTreeAbsoluteSizeX: number,

	QueryTagMap: {[string]: Id},
	QueryCtMap: {[string]: Id},
	QueryAttrMap: {[string]: Id},
	AttributeUsageAmounts: {[string]: number},

	Script: Instance,

	ExplorerFrame: ExplorerFrameLib,
	HierarchyState: HierarchyState,
	VirtualScroller: VirtualScroller,
	ClassSelectionMenu: ClassSelectionMenu,
	DeferredTaskScheduler: DeferredTaskScheduler,

	ActionInfos: ActionInfos,
	BindableActions: {[string]: PluginAction},

	ItemLists: {
		InsertInstance: any
	},
	Style: any,
	AllTab: Tab,

	InstanceToEntity: {[Instance]: ExplorerEntity},
	FrameToInstanceFrame: {[Instance]: ExplorerFrame},
	IconColorMap: {[InstanceId]: Color3},

	ChildAmountCache: {[ExplorerEntity]: number},
	ClassNameCache: {[ExplorerEntity]: string},
	NameCache: {[ExplorerEntity]: string},

	IdToInstance: {[InstanceId]: Instance},
	InstanceToId: {[Instance]: InstanceId},
	IdToObjectValue: {[InstanceId]: ObjectValue},

	TempIdToInstance: {[TempId]: Instance},
	InstanceToTempId: {[Instance]: TempId},

	TabFrameToTab: {[Instance]: Tab},

	createPluginAction: (actionId: string, text: string, statusTip: string, iconId: string?) -> PluginAction,
	takeContextTarget: () -> ExplorerEntity,

	connectInputBegan: (fn: (input: InputObject) -> ()) -> RBXScriptConnection,
	connectInputEnded: (fn: (input: InputObject) -> ()) -> RBXScriptConnection,
	isKeyDown: (key: Enum.KeyCode) -> (boolean),
	isInputActive: (input: Enum.UserInputType) -> (boolean),

	registerTab: (name: string, icon: string, roots: {ExplorerEntity}, registerIntoTabsArray: boolean) -> Tab,

	getPropertyTag: (world: World, propName: string) -> Id,

	triggerDuplicateTarget: () -> (),
	triggerDuplicateTargetWithIncrementName: () -> (),
	triggerDeleteTarget: () -> (),
	triggerEscapeTarget: () -> (),

	createContextMenu: (id: string, contents: {any}, title: string?, icon: (() -> string)?) -> ContextMenu,
	registerRightClickContextMenu: (menuId: string, menu: ContextMenu, condition: (manager: Manager, target: ExplorerEntity) -> boolean, priority: number) -> (),

	getEntityAt: (x: number, y: number, filteredParent: Instance?) -> (Instance?, ExplorerEntity?, Tab?),

	onTagAdded: (inst: Instance, callback: (tag: string) -> ()) -> RBXScriptConnection,
	onTagRemoved: (inst: Instance, callback: (tag: string) -> ()) -> RBXScriptConnection,

	getOrCreateInstId: (instance: Instance) -> string,
	getInstId: (instance: Instance) -> string?,
	getInstFromId: (id: string) -> Instance?,

	getTempId: (instance: Instance) -> string,
	getInstFromTempId: (id: string) -> Instance,

	getSetting: (name: string) -> any,
	setSetting: (name: string, value: any) -> (),

	isExplorerEntityInvisible: (entity: ExplorerEntity) -> boolean,
	isExplorerEntityDeleted: (entity: ExplorerEntity) -> boolean,
	isExplorerEntityExpanded: (entity: ExplorerEntity) -> boolean,

	getPropertyComponentsForClass: (World, class: string) -> {Id},

	isInstanceExcluded: (inst: Instance) -> boolean,

	getSetPropertyDragInfo: (instances: {Instance}, propName: string) -> any,
	getDragEntitiesDragInfo: (entities: {ExplorerEntity}) -> any,
	unpackDragInfo: (dragInfo: any) -> DragInfo_ReparentInstances | DragInfo_SetInstTypeProperty,

	addExplorerEntitiesToSelection: (entities: {ExplorerEntity}, updateSelection: boolean) -> (),
	setExplorerEntitiesAsSelection: (entities: {ExplorerEntity}, updateSelection: boolean) -> (),

	refreshQueryTags: (entity: ExplorerEntity, tagEnt: Id?) -> (),
	refreshQueryAttributes: (entity: ExplorerEntity, attrEnt: Id?) -> (),

	startDraggingSelected: () -> (),

	ChildOf: (ent: ExplorerEntity) -> Pair<Id, ExplorerEntity>,

	init: (connections: ItemGroup<RBXScriptConnection>, widget: DockWidgetPluginGui) -> (),

	frameOf: (frame: ExplorerFrame) -> UITEMPLATE_InstanceFrame?,
	parentOf: (entity: ExplorerEntity) -> ExplorerEntity?,
	instanceOf: (entity: ExplorerEntity) -> Instance?,
	updateParent: (entity: ExplorerEntity, newParent: ExplorerEntity?) -> (),

	convertEntitiesInto: (entities: {ExplorerEntity}, class: string) -> {ExplorerEntity},
	insertClassInto: (entity: ExplorerEntity, class: string) -> Instance?,

	jumpCanvasTo: (x: number?, y: number?, epsilon: number?) -> (),

	saveState: () -> {
		save: () -> (),
		all: () -> (),
		icon_colors: () -> (),
		style: () -> (),
		versions: () -> (),
		tabs: () -> (),
		repo_proxies: () -> (),
		settings: () -> (),
		order: () -> (),
		invisible: () -> (),
	},
	loadState: (state: SavedState) -> ({
		icon_colors: () -> (),
		style: () -> any,
		versions: () -> {
			[Instance]: {
				{
					timestamp: DateTime,
					props: {[string]: any},
				}
			}
		},
		tabs: () -> (),
		repo_proxies: () -> (),
		settings: () -> (),
		order: () -> (),
		invisible: () -> (),
	}, SavedState)
}

type Editor = {
	Widget: DockWidgetPluginGui,

	init: (Manager) -> (),
}

export type StyleEditor = Editor & {

}

export type SettingEditor = Editor & {
	Configs: {[string]: any},

	getSetting: (Manager: Manager, name: string) -> any,
	setSetting: (Manager: Manager, name: string, value: any) -> (),
	createSetting: (list: any, settingName: string, displayName: string, valueType: string, defaultValue: any, onChange: ((newValue: any) -> ())?) -> (),
}

export type OrderEditor = Editor & {
	ClassPriorities: {string},
	DefaultPriority: number,
	PriorityVersion: number
}

export type VersionControl = Editor & {
	InstanceHistoryWidget: DockWidgetPluginGui,
	InstanceHistoryUIList: any,

	VersionViewerWidget: DockWidgetPluginGui,
	VersionViewerUIList: any,

	SourceViewerWidget: DockWidgetPluginGui,
	SourceViewerMainframe: UITEMPLATE_EditorFrame,

	Token: any?,
	RepoId: string,
	Versions: {
		[Instance]: {
			{
				timestamp: DateTime,
				props: {[string]: any},
			}
		}
	},
	PendingChanges: {
		[Instance]: {
			changedProps: {[string]: any},
			originalProps: {[string]: any}
		}
	},

	openSourceViewer: (src: string) -> (),
	showInstanceHistoryWidget: (inst: Instance) -> (),
	createInstanceVersion: (inst: Instance, changedProp: string, newValue: any) -> boolean,
	appendInstanceVersion: (inst: Instance, changedProp: string, newValue: any) -> boolean,

	addPendingChange: (inst: Instance, changedProp: string, newValue: any) -> (),
	appendPendingChanges: (inst: Instance) -> boolean,
	createPendingChanges: (inst: Instance) -> boolean,
	discardPendingChanges: (inst: Instance) -> boolean,

	createRepoProxy: (display_name: string, repo_id: string, root: Instance, register_into_proxies_array: boolean) -> RepoProxy
}

export type InstanceVisibilityEditor = Editor & {
	InvisibleClasses: {[string]: boolean},
	InvisibleInstances: {[Instance]: boolean},

	setInvisible: (className: string, invisible: boolean) -> boolean,
}

export type PropertyEditor = Editor & {
	PropertyList: any,

	CurrentSetNilTarget: {instances: {Instance}, name: string}?,
	SetNilMenu: PluginMenu,

	updateProperties: (instances: {Instance}) -> (),
	clearFramePools: () -> ()
}

export type ContextMenuEditor = Editor & {

}

export type JextensionEditor = Editor & {

}

export type StateEditor = Editor & {

}

export type ExplorerFrameEditor = Editor & {

}

export type Jexplorer = {
	Manager: Manager,
	StyleEditor: StyleEditor,
	SettingEditor: SettingEditor,
	OrderEditor: OrderEditor,
	VersionControl: VersionControl,
	PropertyEditor: PropertyEditor,

	Dependencies: {[string]: any},

	OnLoad: RBXScriptSignal,
	OnClose: RBXScriptSignal,
	ModifyIconStep: RBXScriptSignal
}

return nil


print("hello from cloud print")

return {
    message = "hello from cloud return",
}


--!strict

--[[
	Copyright 2026 Ryan Luu

	Licensed under the Apache License, Version 2.0 (the "License");
	you may not use this file except in compliance with the License.
	You may obtain a copy of the License at

		http://www.apache.org/licenses/LICENSE-2.0

	Unless required by applicable law or agreed to in writing, software
	distributed under the License is distributed on an "AS IS" BASIS,
	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
	See the License for the specific language governing permissions and
	limitations under the License.
]]

--[[
	By using Purse, you agree to provide attribution in one of the following ways:
		1. Keep the attribution unmodified and enabled
		2. Provide credit in your experience description by name (Purse) and creator (@WinnersTakesAll)
		3. Have a DevForum topic linked in your experience description, crediting with the above

	For questions or exceptions to the attribution terms, reach out through the social links:
		https://create.roblox.com/store/asset/112061170330936

	Thank you for supporting Purse.
]]

const MarketplaceService = game:GetService("MarketplaceService")
const RunService = game:GetService("RunService")

const VERSION = "1.1.5"

-- Print attribution. Do not modify without reading above
if not RunService:IsStudio() and RunService:IsClient() then
	print(`👛 Running Purse v{VERSION} by @WinnersTakesAll`)
end

-- Check for updates. You may modify the below
local latestVersion: string?

const success, result = pcall(function()
	return MarketplaceService:GetProductInfoAsync(112061170330936)
end)

if success then
	latestVersion = string.match(result.Name, "v(%d+%.%d+%.%d+)")
end

if latestVersion and latestVersion ~= VERSION then
	warn(
		`A new version of Purse (v{VERSION} -> v{latestVersion}) is available: https://create.roblox.com/store/asset/112061170330936`
	)
end


--!strict

const ContextActionService = game:GetService("ContextActionService")
const BackpackScript = require(script.Parent)

const ACTION_NAME = "BackpackToggle"

const function handleAction(actionName, inputState, _inputObject)
	if actionName == ACTION_NAME and inputState == Enum.UserInputState.Begin then
		if BackpackScript.OpenClose then
			BackpackScript.OpenClose()
		end
	end
end

ContextActionService:BindAction(ACTION_NAME, handleAction, false, Enum.KeyCode.Backquote)


--!strict

const StarterGui = game:GetService("StarterGui")

task.spawn(function()
	while task.wait(3) do
		if StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.Backpack) then
			warn("Purse and CoreGui backpack enabled at the same time")
			break
		end
	end
end)


--!strict

const BackpackScript = require(script.Parent)
const Icon = require(script.Parent.Parent.topbarplus)

const icon = Icon.new()
icon:modifyTheme({
	{ "IconLabelContainer", "TargetWidth", 0 }, -- Force minimum width
	{ "IconLabel", "AutoLocalize", false }, -- Don't translate font icon
})
icon:setLabel("backpack")
icon:setOrder(-1)
icon:setTextSize(24)
icon:setTextFont(
	"rbxasset://LuaPackages/Packages/_Index/BuilderIcons/BuilderIcons/BuilderIcons.json",
	Enum.FontWeight.Bold,
	Enum.FontStyle.Normal,
	"Selected"
)
icon:setTextFont(
	"rbxasset://LuaPackages/Packages/_Index/BuilderIcons/BuilderIcons/BuilderIcons.json",
	Enum.FontWeight.Regular,
	Enum.FontStyle.Normal,
	"Deselected"
)
icon:bindToggleKey(Enum.KeyCode.Backquote)
icon:autoDeselect(false)
icon:setCaption("Inventory")

BackpackScript.StateChanged.Event:Connect(function(isNowOpen)
	if isNowOpen then
		icon:select()
	else
		icon:deselect()
	end
end)

icon.toggled:Connect(function(_isSelected, fromSource)
	if fromSource == "User" then
		if BackpackScript.OpenClose then
			BackpackScript.OpenClose()
		end
	end
end)


--!nonstrict
--!nolint DeprecatedApi

-- Backpack Version 5.1
-- OnlyTwentyCharacters, SolarCrane

local GuiService = game:GetService("GuiService") -- ROBLOX deviation: use GuiService instead of UserGameSettings
local Players = game:GetService("Players")
local player = Players.LocalPlayer
local playerGui = player.PlayerGui
local BackpackGui = Instance.new("ScreenGui") -- ROBLOX deviation: use BackpackGui instead of RobloxGui (CoreGui only)
BackpackGui.Name = "BackpackGui"
BackpackGui.ResetOnSpawn = false
BackpackGui.Parent = playerGui
-- local CorePackages = game:GetService("CorePackages")
-- local Modules = RobloxGui.Modules
-- local InExperienceAppChatModal = require(CorePackages.Workspace.Packages.AppChat).App.InExperienceAppChatModal
-- local getFFlagAppChatCoreUIConflictFix = require(CorePackages.Workspace.Packages.SharedFlags).getFFlagAppChatCoreUIConflictFix
local FFlagMountCoreGuiBackpack = true -- ROBLOX deviation: always true for now
-- local isInExperienceUIVREnabled = require(CorePackages.Workspace.Packages.SharedExperimentDefinition).isInExperienceUIVREnabled

local BackpackScript = {}
BackpackScript.OpenClose = nil -- Function to toggle open/close
BackpackScript.IsHotbarVisible = false
BackpackScript.IsOpen = false

-- ROBLOX deviation START: parent to script for script capabilities
local stateChangedBindableEvent = Instance.new("BindableEvent")
stateChangedBindableEvent.Parent = script
BackpackScript.StateChanged = stateChangedBindableEvent -- Fires after any open/close, passes IsNowOpen
-- ROBLOX deviation END

BackpackScript.ModuleName = "Backpack"
BackpackScript.KeepVRTopbarOpen = true
BackpackScript.VRIsExclusive = true
BackpackScript.VRClosesNonExclusive = true

local ICON_SIZE = 60
local FONT_SIZE = Enum.FontSize.Size14
local ICON_BUFFER = 5

local BACKGROUND_FADE_DEFAULT = 0.50
local backgroundFade = BACKGROUND_FADE_DEFAULT * GuiService.PreferredTransparency -- ROBLOX deviation: use GuiService instead of UserGameSettings
local BACKGROUND_COLOR = Color3.new(31 / 255, 31 / 255, 31 / 255)

local VR_FADE_TIME = 1
local VR_PANEL_RESOLUTION = 64

local SLOT_DRAGGABLE_COLOR = Color3.new(49 / 255, 49 / 255, 49 / 255)
local SLOT_EQUIP_COLOR = Color3.new(90 / 255, 142 / 255, 233 / 255)
local SLOT_EQUIP_THICKNESS = 0.1 -- Relative
local SLOT_FADE_LOCKED_DEFAULT = 0.50 -- Locked means undraggable
local slotFadeLocked = SLOT_FADE_LOCKED_DEFAULT * GuiService.PreferredTransparency -- ROBLOX deviation: use GuiService instead of UserGameSettings
local SLOT_BORDER_COLOR = Color3.new(1, 1, 1) -- Appears when dragging

local TOOLTIP_BUFFER = 6
local TOOLTIP_HEIGHT = 16
local TOOLTIP_OFFSET = -25 -- From top

local ARROW_IMAGE_OPEN = "rbxasset://textures/ui/Backpack_Open.png"
local ARROW_IMAGE_CLOSE = "rbxasset://textures/ui/Backpack_Close.png"
local ARROW_SIZE = UDim2.fromOffset(14, 9)
local ARROW_HOTKEY = Enum.KeyCode.Backquote.Value --TODO: Hookup '~' too?
local ARROW_HOTKEY_STRING = "`"
local ARROW_HOVER_COLOR = Color3.new(0, 162 / 255, 1)

local HOTBAR_SLOTS_FULL = 10
local HOTBAR_SLOTS_VR = 6
local HOTBAR_SLOTS_MINI = 3
local HOTBAR_SLOTS_WIDTH_CUTOFF = 1024 -- Anything smaller is MINI
local HOTBAR_OFFSET_FROMBOTTOM = -30 -- Offset to make room for the Health GUI

local INVENTORY_ROWS_FULL = 4
local INVENTORY_ROWS_VR = 3
local INVENTORY_ROWS_MINI = 2
local INVENTORY_HEADER_SIZE = 40
local INVENTORY_ARROWS_BUFFER_VR = 40

local SEARCH_BUFFER = 5
local SEARCH_WIDTH = 200
local SEARCH_TEXT = "Search"

local SEARCH_TEXT_OFFSET_FROMLEFT = 0
local SEARCH_BACKGROUND_COLOR = Color3.new(0.37, 0.37, 0.37)
local SEARCH_BACKGROUND_FADE_DEFAULT = 0.15
local searchBackgroundFade = SEARCH_BACKGROUND_FADE_DEFAULT * GuiService.PreferredTransparency -- ROBLOX deviation: use GuiService instead of UserGameSettings

local DOUBLE_CLICK_TIME = 0.5

local ZERO_KEY_VALUE = Enum.KeyCode.Zero.Value
local DROP_HOTKEY_VALUE = Enum.KeyCode.Backspace.Value

local GAMEPAD_INPUT_TYPES = {
	[Enum.UserInputType.Gamepad1] = true,
	[Enum.UserInputType.Gamepad2] = true,
	[Enum.UserInputType.Gamepad3] = true,
	[Enum.UserInputType.Gamepad4] = true,
	[Enum.UserInputType.Gamepad5] = true,
	[Enum.UserInputType.Gamepad6] = true,
	[Enum.UserInputType.Gamepad7] = true,
	[Enum.UserInputType.Gamepad8] = true,
}

local ContextActionService = game:GetService("ContextActionService")
local GamepadService = game:GetService("GamepadService")
local PlayersService = game:GetService("Players")
local StarterGui = game:GetService("StarterGui")
local UserInputService = game:GetService("UserInputService")
local VRService = game:GetService("VRService")
-- RobloxGui:WaitForChild("Modules"):WaitForChild("TenFootInterface")
local IsTenFootInterface = GuiService:IsTenFootInterface() -- ROBLOX deviation: use GuiService
local function Create(instanceType) -- ROBLOX deviation: use a custom Create function
	return function(data)
		local obj = Instance.new(instanceType)
		local parent = nil
		for k, v in pairs(data) do
			if type(k) == "number" then
				v.Parent = obj
			elseif k == "Parent" then
				parent = v
			else
				obj[k] = v
			end
		end
		if parent then
			obj.Parent = parent
		end
		return obj
	end
end
-- local GameTranslator = require(script.GameTranslator) -- ROBLOX deviation: remove GameTranslator

pcall(function()
	local LocalizationService = game:GetService("LocalizationService")
	local CorescriptLocalization = LocalizationService:GetCorescriptLocalizations()[1]
	SEARCH_TEXT = CorescriptLocalization:GetString(LocalizationService.RobloxLocaleId, "BACKPACK_SEARCH")
end)

local TopbarEnabled = true

if IsTenFootInterface then
	ICON_SIZE = 100
	FONT_SIZE = Enum.FontSize.Size24
end

local GamepadActionsBound = false

local IS_PHONE = UserInputService.TouchEnabled and workspace.CurrentCamera.ViewportSize.X < HOTBAR_SLOTS_WIDTH_CUTOFF -- ROBLOX deviation: use workspace.CurrentCamera.ViewportSize instead of GuiService:GetScreenResolution()

local Player = PlayersService.LocalPlayer

local MainFrame = nil
local HotbarFrame = nil
local OpenInventoryButton = nil
local CloseInventoryButton = nil
local InventoryFrame = nil
local VRInventorySelector = nil
local ScrollingFrame = nil
local UIGridFrame = nil
local UIGridLayout = nil
local ScrollUpInventoryButton = nil
local ScrollDownInventoryButton = nil

local Character = nil
local Humanoid = nil
local Backpack = nil

local Slots = {} -- List of all Slots by index
local LowestEmptySlot = nil
local SlotsByTool = {} -- Map of Tools to their assigned Slots
local HotkeyFns = {} -- Map of KeyCode values to their assigned behaviors
local Dragging = {} -- Only used to check if anything is being dragged, to disable other input
local FullHotbarSlots = 0 -- Now being used to also determine whether or not LB and RB on the gamepad are enabled.
local ActiveHopper = nil -- NOTE: HopperBin
local StarterToolFound = false -- Special handling is required for the gear currently equipped on the site
local WholeThingEnabled = true -- ROBLOX deviation: always emulate being enabled
local TextBoxFocused = false -- ANY TextBox, not just the search box
local ViewingSearchResults = false -- If the results of a search are currently being viewed
local HotkeyStrings = {} -- Used for eating/releasing hotkeys
local CharConns = {} -- Holds character connections to be cleared later
local GamepadEnabled = false -- determines if our gui needs to be gamepad friendly
local TimeOfLastToolChange = 0

local NumberOfHotbarSlots = IS_PHONE and HOTBAR_SLOTS_MINI or HOTBAR_SLOTS_FULL -- Number of slots shown at the bottom
local NumberOfInventoryRows = IS_PHONE and INVENTORY_ROWS_MINI or INVENTORY_ROWS_FULL -- How many rows in the popped-up inventory
local BackpackPanel = nil

local lastEquippedSlot = nil

BackpackScript.BackpackEmpty = Create("BindableEvent")({
	Name = "BackpackEmpty",
	Parent = script, -- ROBLOX deviation: parent to script for script capabilities
})

BackpackScript.BackpackItemAdded = Create("BindableEvent")({
	Name = "BackpackAdded",
	Parent = script, -- ROBLOX deviation: parent to script for script capabilities
})

BackpackScript.BackpackItemRemoved = Create("BindableEvent")({
	Name = "BackpackRemoved",
	Parent = script, -- ROBLOX deviation: parent to script for script capabilities
})

local function NewGui(className, objectName)
	local newGui = Instance.new(className)
	newGui.Name = objectName
	newGui.BackgroundColor3 = Color3.new(0, 0, 0)
	newGui.BackgroundTransparency = 1
	newGui.BorderColor3 = Color3.new(0, 0, 0)
	newGui.BorderSizePixel = 0
	newGui.Size = UDim2.fromScale(1, 1)
	if className:match("Text") then
		newGui.TextColor3 = Color3.new(1, 1, 1)
		newGui.Text = ""
		newGui.Font = Enum.Font.SourceSans
		newGui.FontSize = FONT_SIZE
		newGui.TextWrapped = true
		if className == "TextButton" then
			newGui.Font = Enum.Font.SourceSansBold
			newGui.BorderSizePixel = 1
		end
	end
	return newGui
end

local function FindLowestEmpty()
	for i = 1, NumberOfHotbarSlots do
		local slot = Slots[i]
		if not slot.Tool then
			return slot
		end
	end
	return nil
end

local function isInventoryEmpty()
	for i = NumberOfHotbarSlots + 1, #Slots do
		local slot = Slots[i]
		if slot and slot.Tool then
			return false
		end
	end
	return true
end

BackpackScript.IsInventoryEmpty = isInventoryEmpty

local function UseGazeSelection()
	return false -- disabled in new VR system
end

local function AdjustHotbarFrames()
	local inventoryOpen = InventoryFrame.Visible -- (Show all)
	local visualTotal = inventoryOpen and NumberOfHotbarSlots or FullHotbarSlots
	local visualIndex = 0
	local hotbarIsVisible = (visualTotal >= 1)

	for i = 1, NumberOfHotbarSlots do
		local slot = Slots[i]
		if slot.Tool or inventoryOpen then
			visualIndex = visualIndex + 1
			slot:Readjust(visualIndex, visualTotal)
			slot.Frame.Visible = true
		else
			slot.Frame.Visible = false
		end
	end

	OpenInventoryButton.Visible = not inventoryOpen and (hotbarIsVisible or not isInventoryEmpty())
	OpenInventoryButton.Position = UDim2.new(0.5, -15, 1, hotbarIsVisible and -110 or -50)
	BackpackScript.IsHotbarVisible = hotbarIsVisible
end

local function UpdateScrollingFrameCanvasSize()
	local countX = math.floor(ScrollingFrame.AbsoluteSize.X / (ICON_SIZE + ICON_BUFFER))
	local maxRow = math.ceil((#UIGridFrame:GetChildren() - 1) / countX)
	local canvasSizeY = maxRow * (ICON_SIZE + ICON_BUFFER) + ICON_BUFFER
	ScrollingFrame.CanvasSize = UDim2.fromOffset(0, canvasSizeY)
end

local function AdjustInventoryFrames()
	for i = NumberOfHotbarSlots + 1, #Slots do
		local slot = Slots[i]
		slot.Frame.LayoutOrder = slot.Index
		slot.Frame.Visible = (slot.Tool ~= nil)
	end
	UpdateScrollingFrameCanvasSize()
end

local function UpdateBackpackLayout()
	HotbarFrame.Size = UDim2.new(
		0,
		ICON_BUFFER + (NumberOfHotbarSlots * (ICON_SIZE + ICON_BUFFER)),
		0,
		ICON_BUFFER + ICON_SIZE + ICON_BUFFER
	)
	HotbarFrame.Position = UDim2.new(0.5, -HotbarFrame.Size.X.Offset / 2, 1, -HotbarFrame.Size.Y.Offset)
	InventoryFrame.Size = UDim2.new(
		0,
		HotbarFrame.Size.X.Offset,
		0,
		(HotbarFrame.Size.Y.Offset * NumberOfInventoryRows) + INVENTORY_HEADER_SIZE
	)
	InventoryFrame.Position = UDim2.new(
		0.5,
		-InventoryFrame.Size.X.Offset / 2,
		1,
		HotbarFrame.Position.Y.Offset - InventoryFrame.Size.Y.Offset
	)

	ScrollingFrame.Size = UDim2.new(1, ScrollingFrame.ScrollBarThickness + 1, 1, -INVENTORY_HEADER_SIZE)
	ScrollingFrame.Position = UDim2.fromOffset(0, INVENTORY_HEADER_SIZE)
	AdjustHotbarFrames()
	AdjustInventoryFrames()
end

local function Clamp(low, high, num)
	return math.min(high, math.max(low, num))
end

local function CheckBounds(guiObject, x, y)
	local pos = guiObject.AbsolutePosition
	local size = guiObject.AbsoluteSize
	return (x > pos.X and x <= pos.X + size.X and y > pos.Y and y <= pos.Y + size.Y)
end

local function GetOffset(guiObject, point)
	local centerPoint = guiObject.AbsolutePosition + (guiObject.AbsoluteSize / 2)
	return (centerPoint - point).magnitude
end

local function DisableActiveHopper() --NOTE: HopperBin
	ActiveHopper:ToggleSelect()
	SlotsByTool[ActiveHopper]:UpdateEquipView()
	ActiveHopper = nil
end

local function UnequipAllTools() --NOTE: HopperBin
	if Humanoid then
		Humanoid:UnequipTools()
		if ActiveHopper then
			DisableActiveHopper()
		end
	end
end

local function EquipNewTool(tool) --NOTE: HopperBin
	UnequipAllTools()
	if tool:IsA("HopperBin") then
		tool:ToggleSelect()
		SlotsByTool[tool]:UpdateEquipView()
		ActiveHopper = tool
	else
		--Humanoid:EquipTool(tool) --NOTE: This would also unequip current Tool
		tool.Parent = Character --TODO: Switch back to above line after EquipTool is fixed!
	end
end

local function IsEquipped(tool)
	return tool and ((tool:IsA("HopperBin") and tool.Active) or tool.Parent == Character) --NOTE: HopperBin
end

local function MakeSlot(parent, index)
	index = index or (#Slots + 1)

	-- Slot Definition --

	local slot = {}
	slot.Tool = nil
	slot.Index = index
	slot.Frame = nil

	local SlotFrameParent = nil
	local SlotFrame = nil
	local FakeSlotFrame = nil
	local ToolIcon = nil
	local ToolName = nil
	local ToolChangeConn = nil
	local HighlightFrame = nil
	local SelectionObj = nil

	--NOTE: The following are only defined for Hotbar Slots
	local ToolTip = nil
	local SlotNumber = nil

	-- Slot Functions --

	local function UpdateSlotFading()
		SlotFrame.SelectionImageObject = nil
		SlotFrame.BackgroundTransparency = SlotFrame.Draggable and 0 or slotFadeLocked
		SlotFrame.BackgroundColor3 = SlotFrame.Draggable and SLOT_DRAGGABLE_COLOR or BACKGROUND_COLOR
	end

	function slot:Readjust(visualIndex, visualTotal) --NOTE: Only used for Hotbar slots
		local centered = HotbarFrame.Size.X.Offset / 2
		local sizePlus = ICON_BUFFER + ICON_SIZE
		local midpointish = (visualTotal / 2) + 0.5
		local factor = visualIndex - midpointish
		SlotFrame.Position = UDim2.fromOffset(centered - (ICON_SIZE / 2) + (sizePlus * factor), ICON_BUFFER)
	end

	function slot:Fill(tool)
		if not tool then
			return self:Clear()
		end

		self.Tool = tool

		local function assignToolData()
			local icon = tool.TextureId
			ToolIcon.Image = icon

			if icon ~= "" then
				ToolName.Visible = false
			else
				ToolName.Visible = true
			end

			ToolName.Text = tool.Name -- ROBLOX deviation: remove GameTranslator

			if ToolTip and tool:IsA("Tool") then --NOTE: HopperBin
				ToolTip.Text = tool.ToolTip -- ROBLOX deviation: remove GameTranslator
				local width = ToolTip.TextBounds.X + TOOLTIP_BUFFER
				ToolTip.Size = UDim2.fromOffset(width, TOOLTIP_HEIGHT)
				ToolTip.Position = UDim2.new(0.5, -width / 2, 0, TOOLTIP_OFFSET)
			end
		end
		assignToolData()

		if ToolChangeConn then
			ToolChangeConn:disconnect()
			ToolChangeConn = nil
		end

		ToolChangeConn = tool.Changed:connect(function(property)
			if property == "TextureId" or property == "Name" or property == "ToolTip" then
				assignToolData()
			end
		end)

		local hotbarSlot = (self.Index <= NumberOfHotbarSlots)
		local inventoryOpen = InventoryFrame.Visible

		if not hotbarSlot or inventoryOpen then
			SlotFrame.Draggable = true
		end

		self:UpdateEquipView()

		if hotbarSlot then
			FullHotbarSlots = FullHotbarSlots + 1
			-- If using a controller, determine whether or not we can enable BindCoreAction("RBXHotbarEquip", etc)
			if WholeThingEnabled then
				if FullHotbarSlots >= 1 and not GamepadActionsBound then
					-- Player added first item to a hotbar slot, enable BindCoreAction
					GamepadActionsBound = true
					-- ROBLOX deviation START: change action name from RBX to Backpack
					ContextActionService:BindAction(
						"BackpackHotbarEquip",
						changeToolFunc,
						false,
						Enum.KeyCode.ButtonL1,
						Enum.KeyCode.ButtonR1
					)
					-- ROBLOX deviation END
				end
			end
		end

		SlotsByTool[tool] = self
		LowestEmptySlot = FindLowestEmpty()
	end

	function slot:Clear()
		if not self.Tool then
			return
		end

		if ToolChangeConn then
			ToolChangeConn:disconnect()
			ToolChangeConn = nil
		end

		ToolIcon.Image = ""
		ToolName.Text = ""
		if ToolTip then
			ToolTip.Text = ""
			ToolTip.Visible = false
		end
		SlotFrame.Draggable = false

		self:UpdateEquipView(true) -- Show as unequipped

		if self.Index <= NumberOfHotbarSlots then
			FullHotbarSlots = FullHotbarSlots - 1
			if FullHotbarSlots < 1 then
				GamepadActionsBound = false
				ContextActionService:UnbindAction("BackpackHotbarEquip") -- ROBLOX deviation: change action name from RBX to Backpack
			end
		end

		SlotsByTool[self.Tool] = nil
		self.Tool = nil
		LowestEmptySlot = FindLowestEmpty()
	end

	function slot:UpdateEquipView(unequippedOverride)
		if not unequippedOverride and IsEquipped(self.Tool) then -- Equipped
			lastEquippedSlot = slot
			if not HighlightFrame then
				HighlightFrame = NewGui("Frame", "Equipped")
				HighlightFrame.ZIndex = SlotFrame.ZIndex
				local t = SLOT_EQUIP_THICKNESS
				local dataTable = { -- Relative sizes and positions
					{ t, 1, 0, 0 },
					{ 1 - 2 * t, t, t, 0 },
					{ t, 1, 1 - t, 0 },
					{ 1 - 2 * t, t, t, 1 - t },
				}
				for _, data in pairs(dataTable) do
					local edgeFrame = NewGui("Frame", "Edge")
					edgeFrame.BackgroundTransparency = 0
					edgeFrame.BackgroundColor3 = SLOT_EQUIP_COLOR
					edgeFrame.Size = UDim2.fromScale(data[1], data[2])
					edgeFrame.Position = UDim2.fromScale(data[3], data[4])
					edgeFrame.ZIndex = HighlightFrame.ZIndex
					edgeFrame.Parent = HighlightFrame
				end
			end
			HighlightFrame.Parent = SlotFrame
		else -- In the Backpack
			if HighlightFrame then
				HighlightFrame.Parent = nil
			end
		end
		UpdateSlotFading()
	end

	function slot:IsEquipped()
		return IsEquipped(self.Tool)
	end

	function slot:Delete()
		Dragging[SlotFrame] = nil
		SlotFrame:Destroy() --NOTE: Also clears connections
		table.remove(Slots, self.Index)
		local newSize = #Slots

		-- Now adjust the rest (both visually and representationally)
		for i = self.Index, newSize do
			Slots[i]:SlideBack()
		end

		UpdateScrollingFrameCanvasSize()
	end

	function slot:Swap(targetSlot) --NOTE: This slot (self) must not be empty!
		local myTool, otherTool = self.Tool, targetSlot.Tool
		self:Clear()
		if otherTool then -- (Target slot might be empty)
			targetSlot:Clear()
			self:Fill(otherTool)
		end
		if myTool then
			targetSlot:Fill(myTool)
		else
			targetSlot:Clear()
		end
	end

	function slot:SlideBack() -- For inventory slot shifting
		self.Index = self.Index - 1
		SlotFrame.Name = self.Index
		SlotFrame.LayoutOrder = self.Index
	end

	function slot:TurnNumber(on)
		if SlotNumber then
			SlotNumber.Visible = on
		end
	end

	function slot:SetClickability(on) -- (Happens on open/close arrow)
		if self.Tool then
			SlotFrame.Draggable = not on
			UpdateSlotFading()
		end
	end

	function slot:CheckTerms(terms)
		local hits = 0
		local function checkEm(str, term)
			local _, n = str:lower():gsub(term, "")
			hits = hits + n
		end
		local tool = self.Tool
		if tool then
			for term in pairs(terms) do
				checkEm(ToolName.Text, term)
				if tool:IsA("Tool") then --NOTE: HopperBin
					local toolTipText = ToolTip and ToolTip.Text or ""
					checkEm(toolTipText, term)
				end
			end
		end
		return hits
	end

	-- Slot select logic, activated by clicking or pressing hotkey
	function slot:Select(isProcessed)
		if isProcessed then
			return
		end

		local tool = slot.Tool
		if tool then
			if IsEquipped(tool) then --NOTE: HopperBin
				UnequipAllTools()
			elseif tool.Parent == Backpack then
				EquipNewTool(tool)
			end
		end
	end

	-- Slot Init Logic --

	SlotFrame = NewGui("TextButton", index)
	SlotFrame.BackgroundColor3 = BACKGROUND_COLOR
	SlotFrame.BorderColor3 = SLOT_BORDER_COLOR
	SlotFrame.Text = ""
	SlotFrame.AutoButtonColor = false
	SlotFrame.BorderSizePixel = 0
	SlotFrame.Size = UDim2.fromOffset(ICON_SIZE, ICON_SIZE)
	SlotFrame.Active = true
	SlotFrame.Draggable = false
	SlotFrame.BackgroundTransparency = slotFadeLocked
	SlotFrame.MouseButton1Click:connect(function()
		changeSlot(slot)
	end)
	slot.Frame = SlotFrame

	do
		local selectionObjectClipper = NewGui("Frame", "SelectionObjectClipper")
		selectionObjectClipper.Visible = false
		selectionObjectClipper.Parent = SlotFrame

		SelectionObj = NewGui("ImageLabel", "Selector")
		SelectionObj.Size = UDim2.fromScale(1, 1)
		SelectionObj.Image = "rbxasset://textures/ui/Keyboard/key_selection_9slice.png"
		SelectionObj.ScaleType = Enum.ScaleType.Slice
		SelectionObj.SliceCenter = Rect.new(12, 12, 52, 52)
		SelectionObj.Parent = selectionObjectClipper
	end

	ToolIcon = NewGui("ImageLabel", "Icon")
	ToolIcon.Size = UDim2.fromScale(0.8, 0.8)
	ToolIcon.Position = UDim2.fromScale(0.1, 0.1)
	ToolIcon.Parent = SlotFrame

	ToolName = NewGui("TextLabel", "ToolName")
	ToolName.Size = UDim2.new(1, -2, 1, -2)
	ToolName.Position = UDim2.fromOffset(1, 1)
	ToolName.Parent = SlotFrame

	slot.Frame.LayoutOrder = slot.Index

	if index <= NumberOfHotbarSlots then -- Hotbar-Specific Slot Stuff
		-- ToolTip stuff
		ToolTip = NewGui("TextLabel", "ToolTip")
		ToolTip.TextWrapped = false
		ToolTip.TextYAlignment = Enum.TextYAlignment.Top
		ToolTip.BackgroundColor3 = Color3.new(0.4, 0.4, 0.4)
		ToolTip.BackgroundTransparency = 0
		ToolTip.Visible = false
		ToolTip.Parent = SlotFrame
		SlotFrame.MouseEnter:connect(function()
			if ToolTip.Text ~= "" then
				ToolTip.Visible = true
			end
		end)
		SlotFrame.MouseLeave:connect(function()
			ToolTip.Visible = false
		end)

		function slot:MoveToInventory()
			if slot.Index <= NumberOfHotbarSlots then -- From a Hotbar slot
				local tool = slot.Tool
				self:Clear() --NOTE: Order matters here
				local newSlot = MakeSlot(UIGridFrame)
				newSlot:Fill(tool)
				if IsEquipped(tool) then -- Also unequip it --NOTE: HopperBin
					UnequipAllTools()
				end
				-- Also hide the inventory slot if we're showing results right now
				if ViewingSearchResults then
					newSlot.Frame.Visible = false
					newSlot.Parent = InventoryFrame
				end
			end
		end

		-- Show label and assign hotkeys for 1-9 and 0 (zero is always last slot when > 10 total)
		if index < 10 or index == NumberOfHotbarSlots then -- NOTE: Hardcoded on purpose!
			local slotNum = (index < 10) and index or 0
			SlotNumber = NewGui("TextLabel", "Number")
			SlotNumber.Text = slotNum
			SlotNumber.Size = UDim2.fromScale(0.15, 0.15)
			SlotNumber.Visible = false
			SlotNumber.Parent = SlotFrame

			HotkeyFns[ZERO_KEY_VALUE + slotNum] = function(isProcessed)
				slot:Select(isProcessed)
			end
		end
	end

	do -- Dragging Logic
		local startPoint = SlotFrame.Position
		local lastUpTime = 0
		local startParent = nil

		SlotFrame.DragBegin:connect(function(dragPoint)
			Dragging[SlotFrame] = true
			startPoint = dragPoint

			SlotFrame.BorderSizePixel = 2

			-- Raise above other slots
			SlotFrame.ZIndex = 2
			ToolIcon.ZIndex = 2
			ToolName.ZIndex = 2
			SlotFrame.Parent.ZIndex = 2
			if SlotNumber then
				SlotNumber.ZIndex = 2
			end
			if HighlightFrame then
				HighlightFrame.ZIndex = 2
				for _, child in pairs(HighlightFrame:GetChildren()) do
					child.ZIndex = 2
				end
			end

			-- Circumvent the ScrollingFrame's ClipsDescendants property
			startParent = SlotFrame.Parent
			if startParent == UIGridFrame then
				local oldAbsolutPos = SlotFrame.AbsolutePosition
				local newPosition = UDim2.new(
					0,
					SlotFrame.AbsolutePosition.X - InventoryFrame.AbsolutePosition.X,
					0,
					SlotFrame.AbsolutePosition.Y - InventoryFrame.AbsolutePosition.Y
				)
				SlotFrame.Parent = InventoryFrame
				SlotFrame.Position = newPosition

				FakeSlotFrame = NewGui("Frame", "FakeSlot")
				FakeSlotFrame.LayoutOrder = SlotFrame.LayoutOrder
				FakeSlotFrame.Size = SlotFrame.Size
				FakeSlotFrame.BackgroundTransparency = 1
				FakeSlotFrame.Parent = UIGridFrame
			end
		end)

		SlotFrame.DragStopped:connect(function(x, y)
			if FakeSlotFrame then
				FakeSlotFrame:Destroy()
			end

			local now = tick()
			SlotFrame.Position = startPoint
			SlotFrame.Parent = startParent

			SlotFrame.BorderSizePixel = 0

			-- Restore height
			SlotFrame.ZIndex = 1
			ToolIcon.ZIndex = 1
			ToolName.ZIndex = 1
			startParent.ZIndex = 1
			if SlotNumber then
				SlotNumber.ZIndex = 1
			end
			if HighlightFrame then
				HighlightFrame.ZIndex = 1
				for _, child in pairs(HighlightFrame:GetChildren()) do
					child.ZIndex = 1
				end
			end

			Dragging[SlotFrame] = nil

			-- Make sure the tool wasn't dropped
			if not slot.Tool then
				return
			end

			-- Check where we were dropped
			if CheckBounds(InventoryFrame, x, y) then
				if slot.Index <= NumberOfHotbarSlots then
					slot:MoveToInventory()
				end
				-- Check for double clicking on an inventory slot, to move into empty hotbar slot
				if slot.Index > NumberOfHotbarSlots and now - lastUpTime < DOUBLE_CLICK_TIME then
					if LowestEmptySlot then
						local myTool = slot.Tool
						slot:Clear()
						LowestEmptySlot:Fill(myTool)
						slot:Delete()
					end
					now = 0 -- Resets the timer
				end
			elseif CheckBounds(HotbarFrame, x, y) then
				local closest = { math.huge, nil }
				for i = 1, NumberOfHotbarSlots do
					local otherSlot = Slots[i]
					local offset = GetOffset(otherSlot.Frame, Vector2.new(x, y))
					if offset < closest[1] then
						closest = { offset, otherSlot }
					end
				end
				local closestSlot = closest[2]
				if closestSlot ~= slot then
					slot:Swap(closestSlot)
					if slot.Index > NumberOfHotbarSlots then
						local tool = slot.Tool
						if not tool then -- Clean up after ourselves if we're an inventory slot that's now empty
							slot:Delete()
						else -- Moved inventory slot to hotbar slot, and gained a tool that needs to be unequipped
							if IsEquipped(tool) then --NOTE: HopperBin
								UnequipAllTools()
							end
							-- Also hide the inventory slot if we're showing results right now
							if ViewingSearchResults then
								slot.Frame.Visible = false
								slot.Frame.Parent = InventoryFrame
							end
						end
					end
				end
			else
				-- local tool = slot.Tool
				-- if tool.CanBeDropped then --TODO: HopperBins
				-- tool.Parent = workspace
				-- --TODO: Move away from character
				-- end
				if slot.Index <= NumberOfHotbarSlots then
					slot:MoveToInventory() --NOTE: Temporary
				end
			end

			lastUpTime = now
		end)
	end

	-- All ready!
	SlotFrame.Parent = parent
	Slots[index] = slot

	if index > NumberOfHotbarSlots then
		UpdateScrollingFrameCanvasSize()
		-- Scroll to new inventory slot, if we're open and not viewing search results
		if InventoryFrame.Visible and not ViewingSearchResults then
			local offset = ScrollingFrame.CanvasSize.Y.Offset - ScrollingFrame.AbsoluteSize.Y
			ScrollingFrame.CanvasPosition = Vector2.new(0, math.max(0, offset))
		end
	end

	return slot
end

-- NOTE: We should probably migrate to a 2 collection system:
-- One collection for the hotbar and another collection for the inventory
local function SetNumberOfHotbarSlots(numSlots)
	if NumberOfHotbarSlots ~= numSlots then
		local prevNumberOfSlots = NumberOfHotbarSlots
		local newNumberOfSlots = numSlots
		-- If we are shrinking the number of slots we need
		-- to move around our tools to the right locations
		if prevNumberOfSlots > newNumberOfSlots then
			-- Delete the slots that are now no longer in the Hotbar
			-- Iterate backwards as to not corrupt our iterator
			for i = prevNumberOfSlots, newNumberOfSlots + 1, -1 do
				local slot = Slots[i]
				if slot then
					slot:MoveToInventory()
					slot:Delete()
				end
			end
			-- Also need to slide-back the inventory slots now that they are indexed earlier
			for i = prevNumberOfSlots, newNumberOfSlots + 1, -1 do
				local slot = Slots[i]
				if not slot.Tool then
					slot:Delete()
				end
			end
		else -- If we added more slots
			for i = prevNumberOfSlots, newNumberOfSlots do
				-- Incrementally add hotbar slots
				NumberOfHotbarSlots = i
				if Slots[i] then
					-- Move old
					local oldSlot = Slots[i]
					local oldTool = Slots[i].Tool

					local newSlot = MakeSlot(HotbarFrame, i)

					if oldTool then
						newSlot:Fill(oldTool)
					elseif not LowestEmptySlot then
						LowestEmptySlot = newSlot
					end
				else
					local slot = MakeSlot(HotbarFrame, i)

					slot.Frame.Visible = false

					if not LowestEmptySlot then
						LowestEmptySlot = slot
					end
				end
			end
		end
		NumberOfHotbarSlots = numSlots
		FullHotbarSlots = 0
		for i = 1, NumberOfHotbarSlots do
			if Slots[i] and Slots[i].Tool then
				FullHotbarSlots = FullHotbarSlots + 1
			end
		end

		UpdateBackpackLayout()
	end
end

local function OnChildAdded(child) -- To Character or Backpack
	if not child:IsA("Tool") and not child:IsA("HopperBin") then --NOTE: HopperBin
		if child:IsA("Humanoid") and child.Parent == Character then
			Humanoid = child
		end
		return
	end
	local tool = child

	if tool.Parent == Character then
		TimeOfLastToolChange = tick()
	end

	if ActiveHopper and tool.Parent == Character then --NOTE: HopperBin
		DisableActiveHopper()
	end

	--TODO: Optimize / refactor / do something else
	if not StarterToolFound and tool.Parent == Character and not SlotsByTool[tool] then
		local starterGear = Player:FindFirstChild("StarterGear")
		if starterGear then
			if starterGear:FindFirstChild(tool.Name) then
				StarterToolFound = true
				local slot = LowestEmptySlot or MakeSlot(UIGridFrame)
				for i = slot.Index, 1, -1 do
					local curr = Slots[i] -- An empty slot, because above
					local pIndex = i - 1
					if pIndex > 0 then
						local prev = Slots[pIndex] -- Guaranteed to be full, because above
						prev:Swap(curr)
					else
						curr:Fill(tool)
					end
				end
				-- Have to manually unequip a possibly equipped tool
				for _, child in pairs(Character:GetChildren()) do
					if child:IsA("Tool") and child ~= tool then
						child.Parent = Backpack
					end
				end
				AdjustHotbarFrames()
				return -- We're done here
			end
		end
	end

	-- The tool is either moving or new
	local slot = SlotsByTool[tool]
	if slot then
		slot:UpdateEquipView()
	else -- New! Put into lowest hotbar slot or new inventory slot
		slot = LowestEmptySlot or MakeSlot(UIGridFrame)
		slot:Fill(tool)
		if slot.Index <= NumberOfHotbarSlots and not InventoryFrame.Visible then
			AdjustHotbarFrames()
		end
		if tool:IsA("HopperBin") then --NOTE: HopperBin
			if tool.Active then
				UnequipAllTools()
				ActiveHopper = tool
			end
		end
	end

	BackpackScript.BackpackItemAdded:Fire()
end

local function OnChildRemoved(child) -- From Character or Backpack
	if not child:IsA("Tool") and not child:IsA("HopperBin") then --NOTE: HopperBin
		return
	end
	local tool = child

	TimeOfLastToolChange = tick()

	-- Ignore this event if we're just moving between the two
	local newParent = tool.Parent
	if newParent == Character or newParent == Backpack then
		return
	end

	local slot = SlotsByTool[tool]
	if slot then
		slot:Clear()
		if slot.Index > NumberOfHotbarSlots then -- Inventory slot
			slot:Delete()
		elseif not InventoryFrame.Visible then
			AdjustHotbarFrames()
		end
	end

	if tool == ActiveHopper then --NOTE: HopperBin
		ActiveHopper = nil
	end

	BackpackScript.BackpackItemRemoved:Fire()
	if isInventoryEmpty() then
		BackpackScript.BackpackEmpty:Fire()
	end
end

local function OnCharacterAdded(character)
	-- First, clean up any old slots
	for i = #Slots, 1, -1 do
		local slot = Slots[i]
		if slot.Tool then
			slot:Clear()
		end
		if i > NumberOfHotbarSlots then
			slot:Delete()
		end
	end
	ActiveHopper = nil --NOTE: HopperBin

	-- And any old connections
	for _, conn in pairs(CharConns) do
		conn:disconnect()
	end
	CharConns = {}

	-- Hook up the new character
	Character = character
	table.insert(CharConns, character.ChildRemoved:connect(OnChildRemoved))
	table.insert(CharConns, character.ChildAdded:connect(OnChildAdded))
	for _, child in pairs(character:GetChildren()) do
		OnChildAdded(child)
	end
	--NOTE: Humanoid is set inside OnChildAdded

	-- And the new backpack, when it gets here
	Backpack = Player:WaitForChild("Backpack")
	table.insert(CharConns, Backpack.ChildRemoved:connect(OnChildRemoved))
	table.insert(CharConns, Backpack.ChildAdded:connect(OnChildAdded))
	for _, child in pairs(Backpack:GetChildren()) do
		OnChildAdded(child)
	end

	AdjustHotbarFrames()
end

local function OnInputBegan(input, isProcessed)
	-- Pass through keyboard hotkeys when not typing into a TextBox and not disabled (except for the Drop key)
	if
		input.UserInputType == Enum.UserInputType.Keyboard
		and not TextBoxFocused
		and (WholeThingEnabled or input.KeyCode.Value == DROP_HOTKEY_VALUE)
	then
		local hotkeyBehavior = HotkeyFns[input.KeyCode.Value]
		if hotkeyBehavior then
			hotkeyBehavior(isProcessed)
		end
	end

	local inputType = input.UserInputType
	if not isProcessed then
		if inputType == Enum.UserInputType.MouseButton1 or inputType == Enum.UserInputType.Touch then
			if InventoryFrame.Visible then
				BackpackScript.OpenClose()
			end
		end
	end
end

local function OnUISChanged(property)
	if property == "KeyboardEnabled" or property == "VREnabled" then
		local on = UserInputService.KeyboardEnabled and not VRService.VREnabled
		for i = 1, NumberOfHotbarSlots do
			Slots[i]:TurnNumber(on)
		end
	end
end

local lastChangeToolInputObject = nil
local lastChangeToolInputTime = nil
local maxEquipDeltaTime = 0.06
local noOpFunc = function() end
local selectDirection = Vector2.new(0, 0)
local hotbarVisible = false

function unbindAllGamepadEquipActions()
	-- ROBLOX deviation START: change action name from RBX to Backpack
	ContextActionService:UnbindAction("BackpackHasGamepadFocus")
	ContextActionService:UnbindAction("BackpackCloseInventory")
	-- ROBLOX deviation END
end

local function setHotbarVisibility(visible, isInventoryScreen)
	for i = 1, NumberOfHotbarSlots do
		local hotbarSlot = Slots[i]
		if hotbarSlot and hotbarSlot.Frame and (isInventoryScreen or hotbarSlot.Tool) then
			hotbarSlot.Frame.Visible = visible
		end
	end
end

local function getInputDirection(inputObject)
	local buttonModifier = 1
	if inputObject.UserInputState == Enum.UserInputState.End then
		buttonModifier = -1
	end

	if inputObject.KeyCode == Enum.KeyCode.Thumbstick1 then
		local magnitude = inputObject.Position.magnitude

		if magnitude > 0.98 then
			local normalizedVector =
				Vector2.new(inputObject.Position.x / magnitude, -inputObject.Position.y / magnitude)
			selectDirection = normalizedVector
		else
			selectDirection = Vector2.new(0, 0)
		end
	elseif inputObject.KeyCode == Enum.KeyCode.DPadLeft then
		selectDirection = Vector2.new(selectDirection.x - 1 * buttonModifier, selectDirection.y)
	elseif inputObject.KeyCode == Enum.KeyCode.DPadRight then
		selectDirection = Vector2.new(selectDirection.x + 1 * buttonModifier, selectDirection.y)
	elseif inputObject.KeyCode == Enum.KeyCode.DPadUp then
		selectDirection = Vector2.new(selectDirection.x, selectDirection.y - 1 * buttonModifier)
	elseif inputObject.KeyCode == Enum.KeyCode.DPadDown then
		selectDirection = Vector2.new(selectDirection.x, selectDirection.y + 1 * buttonModifier)
	else
		selectDirection = Vector2.new(0, 0)
	end

	return selectDirection
end

local selectToolExperiment = function(actionName, inputState, inputObject)
	local inputDirection = getInputDirection(inputObject)

	if inputDirection == Vector2.new(0, 0) then
		return
	end

	local angle = math.atan2(inputDirection.y, inputDirection.x) - math.atan2(-1, 0)
	if angle < 0 then
		angle = angle + (math.pi * 2)
	end

	local quarterPi = (math.pi * 0.25)

	local index = (angle / quarterPi) + 1
	index = math.floor(index + 0.5) -- round index to whole number
	if index > NumberOfHotbarSlots then
		index = 1
	end

	if index > 0 then
		local selectedSlot = Slots[index]
		if selectedSlot and selectedSlot.Tool and not selectedSlot:IsEquipped() then
			selectedSlot:Select()
		end
	else
		UnequipAllTools()
	end
end

changeToolFunc = function(actionName, inputState, inputObject)
	if inputState ~= Enum.UserInputState.Begin then
		return
	end

	if lastChangeToolInputObject then
		if
			(
				lastChangeToolInputObject.KeyCode == Enum.KeyCode.ButtonR1
				and inputObject.KeyCode == Enum.KeyCode.ButtonL1
			)
			or (
				lastChangeToolInputObject.KeyCode == Enum.KeyCode.ButtonL1
				and inputObject.KeyCode == Enum.KeyCode.ButtonR1
			)
		then
			if (tick() - lastChangeToolInputTime) <= maxEquipDeltaTime then
				UnequipAllTools()
				lastChangeToolInputObject = inputObject
				lastChangeToolInputTime = tick()
				return
			end
		end
	end

	lastChangeToolInputObject = inputObject
	lastChangeToolInputTime = tick()

	delay(maxEquipDeltaTime, function()
		if lastChangeToolInputObject ~= inputObject then
			return
		end

		local moveDirection = 0
		if inputObject.KeyCode == Enum.KeyCode.ButtonL1 then
			moveDirection = -1
		else
			moveDirection = 1
		end

		for i = 1, NumberOfHotbarSlots do
			local hotbarSlot = Slots[i]
			if hotbarSlot:IsEquipped() then
				local newSlotPosition = moveDirection + i
				local hitEdge = false
				if newSlotPosition > NumberOfHotbarSlots then
					newSlotPosition = 1
					hitEdge = true
				elseif newSlotPosition < 1 then
					newSlotPosition = NumberOfHotbarSlots
					hitEdge = true
				end

				local origNewSlotPos = newSlotPosition
				while not Slots[newSlotPosition].Tool do
					newSlotPosition = newSlotPosition + moveDirection
					if newSlotPosition == origNewSlotPos then
						return
					end

					if newSlotPosition > NumberOfHotbarSlots then
						newSlotPosition = 1
						hitEdge = true
					elseif newSlotPosition < 1 then
						newSlotPosition = NumberOfHotbarSlots
						hitEdge = true
					end
				end

				if hitEdge then
					UnequipAllTools()
					lastEquippedSlot = nil
				else
					Slots[newSlotPosition]:Select()
				end
				return
			end
		end

		if lastEquippedSlot and lastEquippedSlot.Tool then
			lastEquippedSlot:Select()
			return
		end

		local startIndex = moveDirection == -1 and NumberOfHotbarSlots or 1
		local endIndex = moveDirection == -1 and 1 or NumberOfHotbarSlots
		for i = startIndex, endIndex, moveDirection do
			if Slots[i].Tool then
				Slots[i]:Select()
				return
			end
		end
	end)
end

function getGamepadSwapSlot()
	for i = 1, #Slots do
		if Slots[i].Frame.BorderSizePixel > 0 then
			return Slots[i]
		end
	end
end

function changeSlot(slot)
	local gamepadCursorEnabled = GamepadService.GamepadCursorEnabled

	if slot.Frame == GuiService.SelectedObject and not gamepadCursorEnabled then
		local currentlySelectedSlot = getGamepadSwapSlot()

		if currentlySelectedSlot then
			currentlySelectedSlot.Frame.BorderSizePixel = 0
			if currentlySelectedSlot ~= slot then
				slot:Swap(currentlySelectedSlot)
				VRInventorySelector.SelectionImageObject.Visible = false

				if slot.Index > NumberOfHotbarSlots and not slot.Tool then
					if GuiService.SelectedObject == slot.Frame then
						GuiService.SelectedObject = currentlySelectedSlot.Frame
					end
					slot:Delete()
				end

				if currentlySelectedSlot.Index > NumberOfHotbarSlots and not currentlySelectedSlot.Tool then
					if GuiService.SelectedObject == currentlySelectedSlot.Frame then
						GuiService.SelectedObject = slot.Frame
					end
					currentlySelectedSlot:Delete()
				end
			end
		else
			local startSize = slot.Frame.Size
			local startPosition = slot.Frame.Position
			slot.Frame:TweenSizeAndPosition(
				startSize + UDim2.fromOffset(10, 10),
				startPosition - UDim2.fromOffset(5, 5),
				Enum.EasingDirection.Out,
				Enum.EasingStyle.Quad,
				0.1,
				true,
				function()
					slot.Frame:TweenSizeAndPosition(
						startSize,
						startPosition,
						Enum.EasingDirection.In,
						Enum.EasingStyle.Quad,
						0.1,
						true
					)
				end
			)
			slot.Frame.BorderSizePixel = 3
			VRInventorySelector.SelectionImageObject.Visible = true
		end
	else
		slot:Select()
		VRInventorySelector.SelectionImageObject.Visible = false
	end
end

function enableGamepadInventoryControl()
	local goBackOneLevel = function(actionName, inputState, inputObject)
		if inputState ~= Enum.UserInputState.Begin then
			return
		end

		local selectedSlot = getGamepadSwapSlot()
		if selectedSlot then
			local selectedSlot = getGamepadSwapSlot()
			if selectedSlot then
				selectedSlot.Frame.BorderSizePixel = 0
				return
			end
		elseif InventoryFrame.Visible then
			BackpackScript.OpenClose()
		end
	end

	-- ROBLOX deviation START: change action name from RBX to Backpack
	ContextActionService:BindAction("BackpackHasGamepadFocus", noOpFunc, false, Enum.UserInputType.Gamepad1)
	ContextActionService:BindAction(
		"BackpackCloseInventory",
		goBackOneLevel,
		false,
		Enum.KeyCode.ButtonB,
		Enum.KeyCode.ButtonStart
	)
	-- ROBLOX deviation END

	-- Gaze select will automatically select the object for us!
	if not UseGazeSelection() then
		GuiService.SelectedObject = HotbarFrame:FindFirstChild("1")
	end
end

function disableGamepadInventoryControl()
	unbindAllGamepadEquipActions()

	for i = 1, NumberOfHotbarSlots do
		local hotbarSlot = Slots[i]
		if hotbarSlot and hotbarSlot.Frame then
			hotbarSlot.Frame.BorderSizePixel = 0
		end
	end

	if GuiService.SelectedObject and GuiService.SelectedObject:IsDescendantOf(MainFrame) then
		GuiService.SelectedObject = nil
	end
end

local function bindBackpackHotbarAction()
	if WholeThingEnabled and not GamepadActionsBound then
		GamepadActionsBound = true
		-- ROBLOX deviation START: change action name from RBX to Backpack
		ContextActionService:BindAction(
			"BackpackHotbarEquip",
			changeToolFunc,
			false,
			Enum.KeyCode.ButtonL1,
			Enum.KeyCode.ButtonR1
		)
		-- ROBLOX deviation END
	end
end

local function unbindBackpackHotbarAction()
	disableGamepadInventoryControl()
	GamepadActionsBound = false
	ContextActionService:UnbindAction("BackpackHotbarEquip") -- ROBLOX deviation: change action name from RBX to Backpack
end

function gamepadDisconnected()
	if not VRService.VREnabled then
		GamepadEnabled = false
		disableGamepadInventoryControl()
	end
end

function gamepadConnected()
	if not VRService.VREnabled then
		GamepadEnabled = true
		GuiService:AddSelectionParent("BackpackSelection", MainFrame) -- ROBLOX deviation: change action name from RBX to Backpack

		if FullHotbarSlots >= 1 then
			bindBackpackHotbarAction()
		end

		if InventoryFrame.Visible then
			enableGamepadInventoryControl()
		end
	end
end

-- ROBLOX deviation START: disable CoreGui toggling for backpack
-- local function OnCoreGuiChanged(coreGuiType, enabled)
-- 	-- Check for enabling/disabling the whole thing
-- 	if coreGuiType == Enum.CoreGuiType.Backpack or coreGuiType == Enum.CoreGuiType.All then
-- 		enabled = enabled and TopbarEnabled
-- 		WholeThingEnabled = enabled
-- 		if FFlagMountCoreGuiBackpack then
-- 			if enabled then
-- 				MainFrame.Parent = RobloxGui
-- 			else
-- 				MainFrame.Parent = nil
-- 			end
-- 		else
-- 			MainFrame.Visible = enabled
-- 		end

-- 		-- Eat/Release hotkeys (Doesn't affect UserInputService)
-- 		for _, keyString in pairs(HotkeyStrings) do
-- 			if enabled then
-- 				GuiService:AddKey(keyString)
-- 			else
-- 				GuiService:RemoveKey(keyString)
-- 			end
-- 		end

-- 		if enabled then
-- 			if FullHotbarSlots >= 1 then
-- 				bindBackpackHotbarAction()
-- 			end
-- 		else
-- 			unbindBackpackHotbarAction()
-- 		end
-- 	end
-- end
-- ROBLOX deviation END

local function MakeVRRoundButton(name, image)
	local newButton = NewGui("ImageButton", name)
	newButton.Size = UDim2.fromOffset(40, 40)
	newButton.Image = "rbxasset://textures/ui/Keyboard/close_button_background.png"

	local buttonIcon = NewGui("ImageLabel", "Icon")
	buttonIcon.Size = UDim2.fromScale(0.5, 0.5)
	buttonIcon.Position = UDim2.fromScale(0.25, 0.25)
	buttonIcon.Image = image
	buttonIcon.Parent = newButton

	local buttonSelectionObject = NewGui("ImageLabel", "Selection")
	buttonSelectionObject.Size = UDim2.fromScale(0.9, 0.9)
	buttonSelectionObject.Position = UDim2.fromScale(0.05, 0.05)
	buttonSelectionObject.Image = "rbxasset://textures/ui/Keyboard/close_button_selection.png"
	newButton.SelectionImageObject = buttonSelectionObject

	return newButton, buttonIcon, buttonSelectionObject
end

-- Make the main frame, which (mostly) covers the screen
MainFrame = NewGui("Frame", "Backpack")
if FFlagMountCoreGuiBackpack then
	if StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.Backpack) then
		MainFrame.Parent = BackpackGui
	end
else
	MainFrame.Visible = false
	MainFrame.Parent = BackpackGui
end

-- Make the HotbarFrame, which holds only the Hotbar Slots
HotbarFrame = NewGui("Frame", "Hotbar")
HotbarFrame.Parent = MainFrame

-- Make all the Hotbar Slots
for i = 1, NumberOfHotbarSlots do
	local slot = MakeSlot(HotbarFrame, i)
	slot.Frame.Visible = false

	if not LowestEmptySlot then
		LowestEmptySlot = slot
	end
end

-- Up arrow to open the inventory
OpenInventoryButton = NewGui("ImageButton", "OpenInventory")
do
	OpenInventoryButton.Size = UDim2.fromOffset(30, 30)
	OpenInventoryButton.Image = "rbxasset://textures/ui/Backpack/ScrollUpArrow.png"
	OpenInventoryButton.MouseButton1Click:connect(function()
		BackpackScript.OpenClose()
	end)
	OpenInventoryButton.SelectionGained:connect(function()
		OpenInventoryButton.ImageColor3 = ARROW_HOVER_COLOR
	end)
	OpenInventoryButton.SelectionLost:connect(function()
		OpenInventoryButton.ImageColor3 = Color3.new(1, 1, 1)
	end)
	local openInventoryButtonSelectionObject = NewGui("Frame", "Selection")
	openInventoryButtonSelectionObject.Visible = false
	OpenInventoryButton.SelectionImageObject = openInventoryButtonSelectionObject
end

CloseInventoryButton = MakeVRRoundButton("CloseInventory", "rbxasset://textures/ui/Keyboard/close_button_icon.png")
CloseInventoryButton.Position = UDim2.fromOffset(0, -50)
CloseInventoryButton.MouseButton1Click:connect(function()
	if InventoryFrame.Visible then
		BackpackScript.OpenClose()
	end
end)

LeftBumperButton = NewGui("ImageLabel", "LeftBumper")
LeftBumperButton.Size = UDim2.fromOffset(40, 40)
LeftBumperButton.Position = UDim2.new(0, -LeftBumperButton.Size.X.Offset, 0.5, -LeftBumperButton.Size.Y.Offset / 2)

RightBumperButton = NewGui("ImageLabel", "RightBumper")
RightBumperButton.Size = UDim2.fromOffset(40, 40)
RightBumperButton.Position = UDim2.new(1, 0, 0.5, -RightBumperButton.Size.Y.Offset / 2)

-- Make the Inventory, which holds the ScrollingFrame, the header, and the search box
InventoryFrame = NewGui("Frame", "Inventory")
InventoryFrame.BackgroundTransparency = backgroundFade
InventoryFrame.BackgroundColor3 = BACKGROUND_COLOR
InventoryFrame.Active = true
InventoryFrame.Visible = false
InventoryFrame.Parent = MainFrame

VRInventorySelector = NewGui("TextButton", "VRInventorySelector")
VRInventorySelector.Position = UDim2.new(0, 0, 0, 0)
VRInventorySelector.Size = UDim2.fromScale(1, 1)
VRInventorySelector.BackgroundTransparency = 1
VRInventorySelector.Text = ""
VRInventorySelector.Parent = InventoryFrame

local selectorImage = NewGui("ImageLabel", "Selector")
selectorImage.Size = UDim2.fromScale(1, 1)
selectorImage.Image = "rbxasset://textures/ui/Keyboard/key_selection_9slice.png"
selectorImage.ScaleType = Enum.ScaleType.Slice
selectorImage.SliceCenter = Rect.new(12, 12, 52, 52)
selectorImage.Visible = false
VRInventorySelector.SelectionImageObject = selectorImage

-- Make the ScrollingFrame, which holds the rest of the Slots (however many)
ScrollingFrame = NewGui("ScrollingFrame", "ScrollingFrame")
ScrollingFrame.Selectable = false
ScrollingFrame.CanvasSize = UDim2.new(0, 0, 0, 0)
ScrollingFrame.Parent = InventoryFrame

UIGridFrame = NewGui("Frame", "UIGridFrame")
UIGridFrame.Selectable = false
UIGridFrame.Size = UDim2.new(1, -(ICON_BUFFER * 2), 1, 0)
UIGridFrame.Position = UDim2.fromOffset(ICON_BUFFER, 0)
UIGridFrame.Parent = ScrollingFrame

UIGridLayout = Instance.new("UIGridLayout")
UIGridLayout.SortOrder = Enum.SortOrder.LayoutOrder
UIGridLayout.CellSize = UDim2.fromOffset(ICON_SIZE, ICON_SIZE)
UIGridLayout.CellPadding = UDim2.fromOffset(ICON_BUFFER, ICON_BUFFER)
UIGridLayout.Parent = UIGridFrame

ScrollUpInventoryButton = MakeVRRoundButton("ScrollUpButton", "rbxasset://textures/ui/Backpack/ScrollUpArrow.png")
ScrollUpInventoryButton.Size = UDim2.fromOffset(34, 34)
ScrollUpInventoryButton.Position =
	UDim2.new(0.5, -ScrollUpInventoryButton.Size.X.Offset / 2, 0, INVENTORY_HEADER_SIZE + 3)
ScrollUpInventoryButton.Icon.Position = ScrollUpInventoryButton.Icon.Position - UDim2.fromOffset(0, 2)
ScrollUpInventoryButton.MouseButton1Click:connect(function()
	ScrollingFrame.CanvasPosition = Vector2.new(
		ScrollingFrame.CanvasPosition.X,
		Clamp(
			0,
			ScrollingFrame.CanvasSize.Y.Offset - ScrollingFrame.AbsoluteWindowSize.Y,
			ScrollingFrame.CanvasPosition.Y - (ICON_BUFFER + ICON_SIZE)
		)
	)
end)

ScrollDownInventoryButton = MakeVRRoundButton("ScrollDownButton", "rbxasset://textures/ui/Backpack/ScrollUpArrow.png")
ScrollDownInventoryButton.Rotation = 180
ScrollDownInventoryButton.Icon.Position = ScrollDownInventoryButton.Icon.Position - UDim2.fromOffset(0, 2)
ScrollDownInventoryButton.Size = UDim2.fromOffset(34, 34)
ScrollDownInventoryButton.Position =
	UDim2.new(0.5, -ScrollDownInventoryButton.Size.X.Offset / 2, 1, -ScrollDownInventoryButton.Size.Y.Offset - 3)
ScrollDownInventoryButton.MouseButton1Click:connect(function()
	ScrollingFrame.CanvasPosition = Vector2.new(
		ScrollingFrame.CanvasPosition.X,
		Clamp(
			0,
			ScrollingFrame.CanvasSize.Y.Offset - ScrollingFrame.AbsoluteWindowSize.Y,
			ScrollingFrame.CanvasPosition.Y + (ICON_BUFFER + ICON_SIZE)
		)
	)
end)

ScrollingFrame.Changed:connect(function(prop)
	if prop == "AbsoluteWindowSize" or prop == "CanvasPosition" or prop == "CanvasSize" then
		local canScrollUp = ScrollingFrame.CanvasPosition.Y ~= 0
		local canScrollDown = ScrollingFrame.CanvasPosition.Y
			< ScrollingFrame.CanvasSize.Y.Offset - ScrollingFrame.AbsoluteWindowSize.Y

		ScrollUpInventoryButton.Visible = canScrollUp
		ScrollDownInventoryButton.Visible = canScrollDown
	end
end)

-- Position the frames and sizes for the Backpack GUI elements
UpdateBackpackLayout()

--Make the gamepad hint frame
local gamepadHintsFrame = Create("Frame")({
	Name = "GamepadHintsFrame",
	Size = UDim2.fromOffset(HotbarFrame.Size.X.Offset, (IsTenFootInterface and 95 or 60)),
	BackgroundTransparency = 1,
	Visible = false,
	Parent = MainFrame,
})

local function addGamepadHint(hintImage, hintImageLarge, hintText)
	local hintFrame = Create("Frame")({
		Name = "HintFrame",
		Size = UDim2.new(1, 0, 1, -5),
		Position = UDim2.new(0, 0, 0, 0),
		BackgroundTransparency = 1,
		Parent = gamepadHintsFrame,
	})

	local hintImage = Create("ImageLabel")({
		Name = "HintImage",
		Size = (IsTenFootInterface and UDim2.fromOffset(90, 90) or UDim2.fromOffset(60, 60)),
		BackgroundTransparency = 1,
		Image = (IsTenFootInterface and hintImageLarge or hintImage),
		Parent = hintFrame,
	})

	local hintText = Create("TextLabel")({
		Name = "HintText",
		Position = UDim2.fromOffset((IsTenFootInterface and 100 or 70), 0),
		Size = UDim2.new(1, -(IsTenFootInterface and 100 or 70), 1, 0),
		Font = Enum.Font.SourceSansBold,
		FontSize = (IsTenFootInterface and Enum.FontSize.Size36 or Enum.FontSize.Size24),
		BackgroundTransparency = 1,
		Text = hintText,
		TextColor3 = Color3.new(1, 1, 1),
		TextXAlignment = Enum.TextXAlignment.Left,
		TextWrapped = true,
		Parent = hintFrame,
	})
	local textSizeConstraint = Instance.new("UITextSizeConstraint", hintText)
	textSizeConstraint.MaxTextSize = hintText.TextSize
end

local function resizeGamepadHintsFrame()
	gamepadHintsFrame.Size =
		UDim2.new(HotbarFrame.Size.X.Scale, HotbarFrame.Size.X.Offset, 0, (IsTenFootInterface and 95 or 60))
	gamepadHintsFrame.Position = UDim2.new(
		HotbarFrame.Position.X.Scale,
		HotbarFrame.Position.X.Offset,
		InventoryFrame.Position.Y.Scale,
		InventoryFrame.Position.Y.Offset - gamepadHintsFrame.Size.Y.Offset
	)

	local spaceTaken = 0

	local gamepadHints = gamepadHintsFrame:GetChildren()
	--First get the total space taken by all the hints
	for i = 1, #gamepadHints do
		gamepadHints[i].Size = UDim2.new(1, 0, 1, -5)
		gamepadHints[i].Position = UDim2.new(0, 0, 0, 0)
		spaceTaken = spaceTaken + (gamepadHints[i].HintText.Position.X.Offset + gamepadHints[i].HintText.TextBounds.X)
	end

	--The space between all the frames should be equal
	local spaceBetweenElements = (gamepadHintsFrame.AbsoluteSize.X - spaceTaken) / (#gamepadHints - 1)
	for i = 1, #gamepadHints do
		gamepadHints[i].Position = (
			i == 1 and UDim2.new(0, 0, 0, 0)
			or UDim2.new(
				0,
				gamepadHints[i - 1].Position.X.Offset + gamepadHints[i - 1].Size.X.Offset + spaceBetweenElements,
				0,
				0
			)
		)
		gamepadHints[i].Size =
			UDim2.new(0, (gamepadHints[i].HintText.Position.X.Offset + gamepadHints[i].HintText.TextBounds.X), 1, -5)
	end
end

addGamepadHint(
	"rbxasset://textures/ui/Controls/DesignSystem/ButtonX.png",
	"rbxasset://textures/ui/Controls/DesignSystem/ButtonX@2x.png",
	"Remove From Hotbar"
)
addGamepadHint(
	"rbxasset://textures/ui/Controls/DesignSystem/ButtonA.png",
	"rbxasset://textures/ui/Controls/DesignSystem/ButtonA@2x.png",
	"Select/Swap"
)
addGamepadHint(
	"rbxasset://textures/ui/Controls/DesignSystem/ButtonB.png",
	"rbxasset://textures/ui/Controls/DesignSystem/ButtonB@2x.png",
	"Close Backpack"
)

local searchFrame = NewGui("Frame", "Search")
do -- Search stuff
	searchFrame.BackgroundColor3 = SEARCH_BACKGROUND_COLOR
	searchFrame.BackgroundTransparency = searchBackgroundFade
	searchFrame.Size = UDim2.fromOffset(SEARCH_WIDTH - (SEARCH_BUFFER * 2), INVENTORY_HEADER_SIZE - (SEARCH_BUFFER * 2))
	searchFrame.Position = UDim2.new(1, -searchFrame.Size.X.Offset - SEARCH_BUFFER, 0, SEARCH_BUFFER)
	searchFrame.Parent = InventoryFrame

	local searchBox = NewGui("TextBox", "TextBox")
	searchBox.Text = SEARCH_TEXT
	searchBox.ClearTextOnFocus = false
	searchBox.FontSize = Enum.FontSize.Size24
	searchBox.TextXAlignment = Enum.TextXAlignment.Left
	searchBox.Size = searchFrame.Size - UDim2.fromOffset(SEARCH_TEXT_OFFSET_FROMLEFT, 0)
	searchBox.Position = UDim2.fromOffset(SEARCH_TEXT_OFFSET_FROMLEFT, 0)
	searchBox.Parent = searchFrame

	local xButton = NewGui("TextButton", "X")
	xButton.Text = "x"
	xButton.TextColor3 = SLOT_EQUIP_COLOR
	xButton.FontSize = Enum.FontSize.Size24
	xButton.TextYAlignment = Enum.TextYAlignment.Bottom
	xButton.BackgroundColor3 = SEARCH_BACKGROUND_COLOR
	xButton.BackgroundTransparency = 0
	xButton.Size = UDim2.new(
		0,
		searchFrame.Size.Y.Offset - (SEARCH_BUFFER * 2),
		0,
		searchFrame.Size.Y.Offset - (SEARCH_BUFFER * 2)
	)
	xButton.Position = UDim2.new(1, -xButton.Size.X.Offset - (SEARCH_BUFFER * 2), 0.5, -xButton.Size.Y.Offset / 2)
	xButton.ZIndex = 0
	xButton.Visible = false
	xButton.BorderSizePixel = 0
	xButton.Parent = searchFrame

	local function search()
		local terms = {}
		for word in searchBox.Text:gmatch("%S+") do
			terms[word:lower()] = true
		end

		local hitTable = {}
		for i = NumberOfHotbarSlots + 1, #Slots do -- Only search inventory slots
			local slot = Slots[i]
			local hits = slot:CheckTerms(terms)
			table.insert(hitTable, { slot, hits })
			slot.Frame.Visible = false
			slot.Frame.Parent = InventoryFrame
		end

		table.sort(hitTable, function(left, right)
			return left[2] > right[2]
		end)
		ViewingSearchResults = true

		local hitCount = 0
		for i, data in ipairs(hitTable) do
			local slot, hits = data[1], data[2]
			if hits > 0 then
				slot.Frame.Visible = true
				slot.Frame.Parent = UIGridFrame
				slot.Frame.LayoutOrder = NumberOfHotbarSlots + hitCount
				hitCount = hitCount + 1
			end
		end

		ScrollingFrame.CanvasPosition = Vector2.new(0, 0)
		UpdateScrollingFrameCanvasSize()

		xButton.ZIndex = 3
	end

	local function clearResults()
		if xButton.ZIndex > 0 then
			ViewingSearchResults = false
			for i = NumberOfHotbarSlots + 1, #Slots do
				local slot = Slots[i]
				slot.Frame.LayoutOrder = slot.Index
				slot.Frame.Parent = UIGridFrame
				slot.Frame.Visible = true
			end
			xButton.ZIndex = 0
		end
		UpdateScrollingFrameCanvasSize()
	end

	local function reset()
		clearResults()
		searchBox.Text = SEARCH_TEXT
	end

	local function onChanged(property)
		if property == "Text" then
			local text = searchBox.Text
			if text == "" then
				clearResults()
			elseif text ~= SEARCH_TEXT then
				search()
			end
			xButton.Visible = (text ~= "" and text ~= SEARCH_TEXT)
		end
	end

	local function onFocused()
		if searchBox.Text == SEARCH_TEXT then
			searchBox.Text = ""
		end
	end

	local function focusLost(enterPressed)
		if enterPressed then
			--TODO: Could optimize
			search()
		elseif searchBox.Text == "" then
			searchBox.Text = SEARCH_TEXT
		end
	end

	searchBox.Focused:connect(onFocused)
	xButton.MouseButton1Click:connect(reset)
	searchBox.Changed:connect(onChanged)
	searchBox.FocusLost:connect(focusLost)

	BackpackScript.StateChanged.Event:connect(function(isNowOpen)
		xButton.Modal = isNowOpen -- Allows free mouse movement even in first person
		if not isNowOpen then
			reset()
		end
	end)

	HotkeyFns[Enum.KeyCode.Escape.Value] = function(isProcessed)
		if isProcessed then -- Pressed from within a TextBox
			reset()
		elseif InventoryFrame.Visible then
			BackpackScript.OpenClose()
		end
	end

	local function detectGamepad(lastInputType)
		if lastInputType == Enum.UserInputType.Gamepad1 and not VRService.VREnabled then
			searchFrame.Visible = false
		else
			searchFrame.Visible = true
		end
	end
	UserInputService.LastInputTypeChanged:connect(detectGamepad)
end

do -- Make the Inventory expand/collapse arrow (unless TopBar)
	local removeHotBarSlot = function(name, state, input)
		if state ~= Enum.UserInputState.Begin then
			return
		end
		if not GuiService.SelectedObject then
			return
		end

		for i = 1, NumberOfHotbarSlots do
			if Slots[i].Frame == GuiService.SelectedObject and Slots[i].Tool then
				Slots[i]:MoveToInventory()
				return
			end
		end
	end

	local function openClose()
		if not next(Dragging) then -- Only continue if nothing is being dragged
			InventoryFrame.Visible = not InventoryFrame.Visible
			local nowOpen = InventoryFrame.Visible
			AdjustHotbarFrames()
			HotbarFrame.Active = not HotbarFrame.Active
			for i = 1, NumberOfHotbarSlots do
				Slots[i]:SetClickability(not nowOpen)
			end
		end

		if InventoryFrame.Visible then
			if GamepadEnabled then
				if GAMEPAD_INPUT_TYPES[UserInputService:GetLastInputType()] then
					resizeGamepadHintsFrame()
					gamepadHintsFrame.Visible = true
				end
				enableGamepadInventoryControl()
			end
		else
			if GamepadEnabled then
				gamepadHintsFrame.Visible = false
			end
			disableGamepadInventoryControl()
		end

		-- ROBLOX deviation START: change action name from RBX to Backpack
		if InventoryFrame.Visible then
			ContextActionService:BindAction("BackpackRemoveSlot", removeHotBarSlot, false, Enum.KeyCode.ButtonX)
		else
			ContextActionService:UnbindAction("BackpackRemoveSlot")
		end
		-- ROBLOX deviation END

		BackpackScript.IsOpen = InventoryFrame.Visible
		BackpackScript.StateChanged:Fire(InventoryFrame.Visible)
	end

	-- HotkeyFns[ARROW_HOTKEY] = function()
	-- 	if GuiService.MenuIsOpen then
	-- 		return
	-- 	end

	-- 	openClose()
	-- end
	StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.Backpack, false) -- ROBLOX deviation: disable CoreGui Backpack
	BackpackScript.OpenClose = openClose -- Exposed
end

-- Now that we're done building the GUI, we connect to all the major events

-- Wait for the player if LocalPlayer wasn't ready earlier
while not Player do
	wait()
	Player = PlayersService.LocalPlayer
end

-- Listen to current and all future characters of our player
Player.CharacterAdded:connect(OnCharacterAdded)
if Player.Character then
	OnCharacterAdded(Player.Character)
end

do -- Hotkey stuff
	-- Init HotkeyStrings, used for eating hotkeys
	for i = 0, 9 do
		table.insert(HotkeyStrings, tostring(i))
	end
	table.insert(HotkeyStrings, ARROW_HOTKEY_STRING)

	-- Listen to key down
	UserInputService.InputBegan:connect(OnInputBegan)

	-- Listen to ANY TextBox gaining or losing focus, for disabling all hotkeys
	UserInputService.TextBoxFocused:connect(function()
		TextBoxFocused = true
	end)
	UserInputService.TextBoxFocusReleased:connect(function()
		TextBoxFocused = false
	end)

	-- Manual unequip for HopperBins on drop button pressed
	HotkeyFns[DROP_HOTKEY_VALUE] = function() --NOTE: HopperBin
		if ActiveHopper then
			UnequipAllTools()
		end
	end

	-- Listen to keyboard status, for showing/hiding hotkey labels
	UserInputService.Changed:connect(OnUISChanged)
	OnUISChanged("KeyboardEnabled")

	-- Listen to gamepad status, for allowing gamepad style selection/equip
	if UserInputService:GetGamepadConnected(Enum.UserInputType.Gamepad1) then
		gamepadConnected()
	end
	UserInputService.GamepadConnected:connect(function(gamepadEnum)
		if gamepadEnum == Enum.UserInputType.Gamepad1 then
			gamepadConnected()
		end
	end)
	UserInputService.GamepadDisconnected:connect(function(gamepadEnum)
		if gamepadEnum == Enum.UserInputType.Gamepad1 then
			gamepadDisconnected()
		end
	end)
end

-- ROBLOX deviation START: disable CoreGui toggling for backpack
-- function BackpackScript:TopbarEnabledChanged(enabled)
-- 	TopbarEnabled = enabled
-- 	-- Update coregui to reflect new topbar status
-- 	OnCoreGuiChanged(Enum.CoreGuiType.Backpack, StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.Backpack))
-- end

-- -- Listen to enable/disable signals from the StarterGui
-- StarterGui.CoreGuiChangedSignal:connect(OnCoreGuiChanged)
-- local backpackType, healthType = Enum.CoreGuiType.Backpack, Enum.CoreGuiType.Health
-- OnCoreGuiChanged(backpackType, StarterGui:GetCoreGuiEnabled(backpackType))
-- OnCoreGuiChanged(healthType, StarterGui:GetCoreGuiEnabled(healthType))
-- ROBLOX deviation END

GuiService.MenuOpened:Connect(function()
	-- ROBLOX deviation: remove FFlag isInExperienceUIVREnabled
	if BackpackScript.IsOpen then
		BackpackScript.OpenClose()
	end
end)

-- ROBLOX deviation START: remove FFlag AppChatCoreUIConflictFix
-- if getFFlagAppChatCoreUIConflictFix() then
-- 	InExperienceAppChatModal.default.visibilitySignal.Event:Connect(function(visible)
-- 		if visible and BackpackScript.IsOpen then
-- 			BackpackScript.OpenClose()
-- 		end
-- 	end)
-- end
-- ROBLOX deviation END

local function OnPreferredTransparencyChanged()
	local preferredTransparency = GuiService.PreferredTransparency -- ROBLOX deviation: use GuiService instead of UserGameSettings

	backgroundFade = BACKGROUND_FADE_DEFAULT * GuiService.PreferredTransparency -- ROBLOX deviation: use GuiService instead of UserGameSettings
	InventoryFrame.BackgroundTransparency = backgroundFade

	slotFadeLocked = SLOT_FADE_LOCKED_DEFAULT * preferredTransparency
	for _, slot in ipairs(Slots) do
		slot.Frame.BackgroundTransparency = slotFadeLocked
	end

	searchBackgroundFade = SEARCH_BACKGROUND_FADE_DEFAULT * GuiService.PreferredTransparency -- ROBLOX deviation: use GuiService instead of UserGameSettings
	searchFrame.BackgroundTransparency = searchBackgroundFade
end
GuiService:GetPropertyChangedSignal("PreferredTransparency"):connect(OnPreferredTransparencyChanged)

return BackpackScript


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

local logger = require("./lib/logger").new(4)
local executeCommand = require("./lib/executeCommand")

local didError = false

local function execCommandWrapper(...)
	local data = executeCommand(...)

	if data.stderr ~= "" then
		didError = true
	end

	return data
end

local function recursiveCopy(src, dest)
	local files = fs.readDir(src)

	for _, file in files do
		local path = src .. "/" .. file
		local meta = fs.metadata(path)

		if meta.kind == "file" then
			fs.writeDir(dest)
			fs.writeFile(`{dest}/{file}`, fs.readFile(path))
		elseif meta.kind == "dir" then
			recursiveCopy(path, `{dest}/{file}`)
		end
	end
end

if not fs.isDir("packages") and not fs.isDir("Packages") and fs.isFile("wally.toml") then
	execCommandWrapper("lune", { "run", "install-packages" })
end

local hasDarkLua = fs.isFile(".darklua.json")

execCommandWrapper("rojo", { "sourcemap", "sourcemap.project.json", "-o", "sourcemap.json" })

if hasDarkLua then
	execCommandWrapper("darklua", { "process", "src", "temp" })
else
    recursiveCopy("src", "temp")
end

-- Optional processing steps here

recursiveCopy("temp", "dist")

-- get rid of stories; no need for those to be in the actual build
if fs.isDir("dist/ui/stories") then
	fs.removeDir("dist/ui/stories")
end

if not didError then
	execCommandWrapper("rojo", { "build", "default.project.json", "-o", "build.rbxm" })
	logger:success("Build complete!")
else
	logger:error("Build failed due to errors.")
end

fs.removeDir("temp")


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

local logger = require("./lib/logger").new(4)
local fileWatcher = require("./lib/fileWatcher")
local executeCommand = require("./lib/executeCommand")

local function recursiveCopy(src, dest)
	local files = fs.readDir(src)

	for _, file in files do
		local path = src .. "/" .. file

		if fs.isFile(path) then
			fs.writeDir(dest)
			fs.writeFile(`{dest}/{file}`, fs.readFile(path))
		elseif fs.isDir(path) then
			recursiveCopy(path, `{dest}/{file}`)
		end
	end
end

local hasDarkLua = fs.isFile(".darklua.json")

local function processFiles()
	if fs.isFile(".darklua.json") then
		executeCommand("rojo", { "sourcemap", "sourcemap.project.json", "-o", "sourcemap.json" })

		local status = executeCommand("darklua", { "process", "src", "temp", "-c", ".darklua.dev.json" })

		if status.code == 0 then
            -- Optional processing steps here
			recursiveCopy("temp", "dist")
		else
			logger:error("Did not update files due to Darklua error, check console for more information.")
		end

		fs.removeDir("temp")
	end
end

processFiles()

logger:success("Watching for file changes..")

task.spawn(fileWatcher, "./src", function(files)
	logger:info(`Detected changes in {#files} file(s)..`)

	local success, err = pcall(function()
		processFiles()
	end)

	if not success then
		logger:error(`Failed to process files: {err}`)
	end
end, function(files)
	if not hasDarkLua then
		return
	end

	for _, file in files do
		logger:info(`{file} deleted, removing from build..`)

		if file:sub(1, 6) == "./src/" then
			file = file:sub(7)
		end

		if fs.isFile(`dist/{file}`) then
			fs.removeFile(`dist/{file}`)
		elseif fs.isDir(`dist/{file}`) then
			fs.removeDir(`dist/{file}`)
		end
	end
end)

executeCommand("rojo", { "serve", "dev.project.json" }, {
	stdio = "inherit",
})


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

local logger = require("./lib/logger").new(4)
local executeCommand = require("./lib/executeCommand")

if fs.isDir("packages") then
	fs.removeDir("packages")
end

local status = executeCommand("wally", {
	"install",
}, {
    stdio = "inherit"
}, true)

if not status.ok then
    logger:error("Failed to install packages!")
    return
end

if not fs.isDir("packages") then
    fs.writeDir("packages")
end

-- do this so the project file doesn't refer to files that don't exist and cause rojo to fail
if fs.isFile("src/server/network.zap") then
	executeCommand("zap", { "src/server/network.zap" })
end

executeCommand("rojo", {
	"sourcemap",
	fs.isFile("sourcemap.project.json") and "sourcemap.project.json" or "default.project.json",
	"-o",
	"sourcemap.json",
})

if fs.isDir("Packages") then
	local packageTypeStatus = executeCommand("wally-package-types", {
		"--sourcemap",
		"sourcemap.json",
		"./Packages",
	}, nil, true)

	if packageTypeStatus.ok then
		logger:info("Successfully generated package types!")
	else
		logger:error("Failed to generate package types!")
	end

    fs.move("./Packages", "./packages", true)
end

logger:info("Installed packages!")