diff --git a/src/Client/Test/window.lua b/src/Client/Test/window.lua new file mode 100644 index 0000000..90089d2 --- /dev/null +++ b/src/Client/Test/window.lua @@ -0,0 +1,15 @@ +local replicated = game:GetService("ReplicatedStorage") + +local types = require(replicated.Modules.Managers.UIManager.Types) +local Window = require(replicated.Modules.Managers.UIManager.Window) + +local window = Window.new(script.Name) + +window:OnBuild(function(self: types.Window): () + self.ScreenGui = Instance.new("ScreenGui") + + self.ScreenGui.Enabled = false + self.ScreenGui.Parent = game:GetService("Players").LocalPlayer.PlayerGui +end) + +return window \ No newline at end of file diff --git a/src/Client/test.client.lua b/src/Client/test.client.lua new file mode 100644 index 0000000..52cb187 --- /dev/null +++ b/src/Client/test.client.lua @@ -0,0 +1,11 @@ +local replicated = game:GetService("ReplicatedStorage") +local uiManager = require(replicated.Modules.Managers.UIManager) +uiManager:Build(script.Parent.Test:GetChildren() :: {}) + +local default = uiManager:GetWindow("window") + +uiManager:OpenWindow(default) + +task.wait(10) + +uiManager:CloseWindow(default) \ No newline at end of file diff --git a/src/Shared/Modules/Managers/UIManager/Button.lua b/src/Shared/Modules/Managers/UIManager/Button.lua new file mode 100644 index 0000000..b8a7866 --- /dev/null +++ b/src/Shared/Modules/Managers/UIManager/Button.lua @@ -0,0 +1,101 @@ +local types = require(script.Parent.Types) + +local button: types.ButtonClass = {} :: types.ButtonClass +button["__index"] = button + +local module = {} + +function module.new(name: string): types.Button + local self = setmetatable({ + Name= name, + _ = {}, + }, button) + + return self +end + +function button:OnBuild(callback: (self: types.Button) -> ()) + self._.OnBuild = callback +end + +function button:OnOpen(callback: (self: types.Button) -> ()) + self._.OnOpen = callback +end + +function button:OnClose(callback: (self: types.Button) -> ()) + self._.OnClose = callback +end + +function button:Build(parent: types.Page | types.Component) + self.Parent = parent + + if self._.OnBuild then + self._.OnBuild(self) + end +end + +function button:Open() + if self.ButtonGui then + self.ButtonGui.Active = true + self.ButtonGui.Visible = true + end + + if self._.OnOpen then + self._.OnOpen(self) + end +end + +function button:Close() + if self.ButtonGui then + self.ButtonGui.Active = false + self.ButtonGui.Visible = false + end + + if self._.OnClose then + self._.OnClose(self) + end +end + +--[[ Events ]] + +function button:OnPressed(callback: (self: types.Button) -> ()) + self._.OnPressed = callback +end + +function button:OnReleased(callback: (self: types.Button) -> ()) + self._.OnReleased = callback +end + +function button:OnHovered(callback: (self: types.Button) -> ()) + self._.OnHovered = callback +end + +function button:OnHoverLeft(callback: (self: types.Button) -> ()) + self._.OnHoverLeft = callback +end + +function button:Pressed() + if self._.OnPressed then + self._.OnPressed(self) + end +end + +function button:Released() + if self._.OnReleased then + self._.OnReleased(self) + end +end + +function button:Hovered() + if self._.OnHovered then + self._.OnHovered(self) + end +end + +function button:HoverLeft() + if self._.OnHoverLeft then + self._.OnHoverLeft(self) + end +end + +return module \ No newline at end of file diff --git a/src/Shared/Modules/Managers/UIManager/Component.lua b/src/Shared/Modules/Managers/UIManager/Component.lua new file mode 100644 index 0000000..7b24ab7 --- /dev/null +++ b/src/Shared/Modules/Managers/UIManager/Component.lua @@ -0,0 +1,101 @@ +local types = require(script.Parent.Types) + +local component: types.ComponentClass = {} :: types.ComponentClass +component["__index"] = component + +local module = {} + +function module.new(name: string): types.Component + local self = setmetatable({ + Name= name, + _ = {}, + }, component) + + return self +end + +--[[ Buttons ]] + +function component:BuildButtons() + for _, button: types.Button in self.Buttons.Stored do + button:Build(self) + end +end + +function component:AddButton(button: types.Button) + self.Buttons.Stored[button.Name] = button +end + +function component:RemoveButtons() + self.Buttons.Active = {} + self.Buttons.Stored = {} +end + +-- + +function component:OnBuild(callback: (self: types.Component) -> ()): () + self._.OnBuild = callback +end + +function component:OnUpdate(command: string, callback: (self: types.Component, parameters: {}) -> ()): () + self._.UpdateCallbacks[command] = callback +end + +function component:OnOpen(callback: (self: types.Component) -> ()): () + self._.OnOpen = callback +end + +function component:OnClose(callback: (self: types.Component) -> ()): () + self._.OnClose = callback +end + +function component:Build(parent: types.Page): () + self:Clean() + + self.Parent = parent + + local window = parent.Parent + self.Container = window.Manager.Packages.Trove.new() + + if self._.OnBuild then + self._.OnBuild(self) + end +end + +function component:Update(command: string, parameters: {}): () + if self._.UpdateCallbacks[command] then + self._.UpdateCallbacks[command](self, parameters) + else + warn("No update function exists inside of component.", self.Name, "| command:", command) + end +end + +function component:Open(): () + if self.Frame then + self.Frame.Visible = true + end + + if self._.OnOpen then + self._.OnOpen(self) + end +end + +function component:Close(): () + if self.Frame then + self.Frame.Visible = false + end + + if self._.OnClose then + self._.OnClose(self) + end +end + +function component:Clean(): () + if self.Container then + self.Container:Remove() + end + + self:RemoveButtons() +end + +return module \ No newline at end of file diff --git a/src/Shared/Modules/Managers/UIManager/Page.lua b/src/Shared/Modules/Managers/UIManager/Page.lua new file mode 100644 index 0000000..7cce007 --- /dev/null +++ b/src/Shared/Modules/Managers/UIManager/Page.lua @@ -0,0 +1,139 @@ +local types = require(script.Parent.Types) + +local page: types.PageClass = {} :: types.PageClass +page["__index"] = page + +local module = {} + +function module.new(name: string): types.Page + local self = setmetatable({ + Name= name, + Buttons = { + Active = {}, + Stored = {}, + }, + Components = { + Open = {}, + Stored = {}, + }, + + _ = {}, + }, page) + + return self +end + +--[[ Buttons ]] + +function page:BuildButtons() + for _, button: types.Button in self.Buttons.Stored do + button:Build(self) + end +end + +function page:AddButton(button: types.Button) + self.Buttons.Stored[button.Name] = button +end + +function page:RemoveButtons() + self.Buttons.Active = {} + self.Buttons.Stored = {} +end + +-- + +--[[ Components ]] + +function page:BuildComponents() + for _, component: types.Button in self.Components.Stored do + component:Build(self) + end +end + +function page:AddComponent(component: types.Component) + self.Components.Stored[component.Name] = component +end + +function page:GetComponent(component: string): types.Component + return self.Components.Stored[component] +end + +function page:RemoveComponents() + for _, component: types.Component in self.Components.Open do + component:Clean() + end + + self.Components.Open = {} + self.Components.Stored = {} +end + +-- + +--[[ Page ]] + +function page:OnBuild(callback: (self: types.Page) -> ()) + self._.OnBuild = callback +end + +function page:OnOpen(callback: (self: types.Page) -> ()) + self._.OnOpen = callback +end + +function page:OnClose(callback: (self: types.Page) -> ()) + self._.OnClose = callback +end + +function page:Build(parent: types.Window) + self:Clean() + + self.Parent = parent + + self.Container = parent.Manager.Packages.Trove.new() + + if self._.OnBuild then + self._.OnBuild(self) + end +end + +function page:Open() + if self.Frame then + self.Frame.Visible = true + end + + if self._.OnOpen then + self._.OnOpen(self) + end +end + +function page:Close() + if self.Frame then + self.Frame.Visible = false + end + + if self._.OnClose then + self._.OnClose(self) + end +end + +function page:Clean() + self:RemoveComponents() + self:RemoveButtons() + + self.Container:Remove() +end + +function page:Remove() + self:Clean() + + if self.Frame then + self.Frame:Destroy() + end +end + +--[[ Helper ]] + +function page:GetManager(): types.Manager + return self.Parent.Manager +end + +return module \ No newline at end of file diff --git a/src/Shared/Modules/Managers/UIManager/Types.lua b/src/Shared/Modules/Managers/UIManager/Types.lua new file mode 100644 index 0000000..b11e85f --- /dev/null +++ b/src/Shared/Modules/Managers/UIManager/Types.lua @@ -0,0 +1,228 @@ +local replicated = game:GetService("ReplicatedStorage") +local trove = require(replicated.Modules.Utility.Trove) + + +-- + +export type ButtonClass = { + Parent: Page | Component, + Name: string, + ButtonGui: GuiButton, + + _: { + OnBuild: (self: Button) -> ()?, + OnOpen: (self: Button) -> ()?, + OnClose: (self: Button) -> ()?, + + OnPressed: (self: Button) -> ()?, + OnReleased: (self: Button) -> ()?, + OnHovered: (self: Button) -> ()?, + OnHoverLeft: (self: Button) -> ()?, + }, + + OnBuild: (self: Button, callback: (self: Button) -> ()) -> (), + OnOpen: (self: Button, callback: (self: Button) -> ()) -> (), + OnClose: (self: Button, callback: (self: Button) -> ()) -> (), + + Build: (self: Button, parent: Page | Component) -> (), + Open: (self: Button) -> (), + Close: (self: Button) -> (), + + OnPressed: (self: Button, callback: (self: Button) -> ()) -> (), + OnReleased: (self: Button, callback: (self: Button) -> ()) -> (), + OnHovered: (self: Button, callback: (self: Button) -> ()) -> (), + OnHoverLeft: (self: Button, callback: (self: Button) -> ()) -> (), + + Pressed: (self: Button) -> (), + Released: (self: Button) -> (), + Hovered: (self: Button) -> (), + HoverLeft: (self: Button) -> (), +} + +export type Button = typeof(setmetatable({} :: ButtonClass, {})) + +-- + +export type ComponentClass = { + Parent: Page, + Name: string, + Frame: Frame, + + Container: trove.Trove, + + Buttons: { + Active: { [string]: Button }, + Stored: { [string]: Button }, + }, + + BuildButtons: (self: Component) -> (), + AddButton: (self: Component, button: Button) -> (), + RemoveButtons: (self: Component) -> (), + + _: { + OnBuild: (self: Component) -> ()?, + OnOpen: (self: Component) -> ()?, + OnClose: (self: Component) -> ()?, + + UpdateCallbacks: { [string]: (self: Page, parameters: {}) -> () }, + }, + + OnBuild: (self: Component, callback: (self: Component) -> ()) -> (), + OnUpdate: (self: Component, command: string, callback: (self: Component) -> ()) -> (), + OnOpen: (self: Component, callback: (self: Component) -> ()) -> (), + OnClose: (self: Component, callback: (self: Component) -> ()) -> (), + + Build: (self: Component, parent: Page) -> (), + Update: (self: Component, command: string, parameters: {}) -> (), + Open: (self: Component) -> (), + Close: (self: Component) -> (), + + Clean: (self: Component) -> (), +} + +export type Component = typeof(setmetatable({} :: ComponentClass, {})) + +-- + +export type PageClass = { + Parent: Window, + Name: string, + Frame: Frame, + + Container: trove.Trove, + + Buttons: { + Active: { [string]: Button }, + Stored: { [string]: Button }, + }, + + BuildButtons: (self: Page) -> (), + AddButton: (self: Page, button: Button) -> (), + RemoveButtons: (self: Page) -> (), + + Components: { + Open: { [string]: Component }, + Stored: { [string]: Component }, + }, + + BuildComponents: (self: Page) -> (), + AddComponent: (self: Page, component: Component) -> (), + GetComponent: (self: Page, component: string) -> (), + RemoveComponents: (self: Page) -> (), + + _: { + OnBuild: (self: Page) -> ()?, + OnOpen: (self: Page) -> ()?, + OnClose: (self: Page) -> ()?, + }, + + OnBuild: (self: Page, callback: (self: Page) -> ()) -> (), + OnOpen: (self: Page, callback: (self: Page) -> ()) -> (), + OnClose: (self: Page, callback: (self: Page) -> ()) -> (), + + Build: (self: Page, parent: Window) -> (), + Open: (self: Page) -> (), + Close: (self: Page) -> (), + + Clean: (self: Page) -> (), + Remove: (self: Page) -> (), + + GetManager: (self: Window) -> (Manager), +} + +export type Page = typeof(setmetatable({} :: PageClass, {})) + +-- + +export type WindowClass = { + Manager: Manager, + Name: string, + ScreenGui: ScreenGui, + Pages: { + Open: { [string]: Page }, + Stored: { [string]: Page }, + }, + + BuildPages: (self: Window) -> (), + AddPage: (self: Window, page: Page) -> (), + GetPage: (self: Page, page: string) -> (), + RemovePages: (self: Window) -> (), + + _: { + OnBuild: (self: Window) -> ()?, + OnOpen: (self: Window) -> ()?, + OnClose: (self: Window) -> ()?, + }, + + OnBuild: (self: Window, callback: (self: Window) -> ()) -> (), + OnOpen: (self: Window, callback: (self: Window) -> ()) -> (), + OnClose: (self: Window, callback: (self: Window) -> ()) -> (), + + Build: (self: Window, manager: Manager) -> (), + Open: (self: Window) -> (), + Close: (self: Window) -> (), + + Clean: (self: Window) -> (), + Remove: (self: Window) -> (), +} + +export type Window = typeof(setmetatable({} :: WindowClass, {})) + +-- + +export type Manager = { + --[=[ + Useful packages that may be used globally by all windows, pages, and so forth. + ]=] + Packages: { + Trove: typeof(trove), + }, + + --[=[ + ```Window``` objects that are either ```Stored```, aka cached, or are in an ```Open``` state. + ]=] + Windows: { + Open: { [string]: Window }, + Stored: { [string]: Window }, + }, + + --[=[ + Locate a ```Window``` object stored under the provided name. + + @param name string + + @return ```Window``` + ]=] + GetWindow: (self: Manager, name: string) -> (Window), + + --[=[ + Build and open a ```Window``` object and log its opened state. + + @param window ```Window``` + ]=] + OpenWindow: (self: Manager, window: Window) -> (), + + --[=[ + Close and remove a ```Window``` object and log its state change. + + @param window ```Window``` + ]=] + CloseWindow: (self: Manager, window: Window) -> (), + + --[=[ + Intended to only be run on the first time that the client boots up. \ + Receives a table of module scripts with which it will use to build the manager windows. + + @param source { ModuleScript } ```Window``` objects. + ]=] + Build: (self: Manager, source: { ModuleScript }) -> (), + + --[=[ + Will clean the manager by removing all the open ```Window``` objects. + ]=] + Clean: (self: Manager) -> (), +} + +-- + +return {} \ No newline at end of file diff --git a/src/Shared/Modules/Managers/UIManager/Window.lua b/src/Shared/Modules/Managers/UIManager/Window.lua new file mode 100644 index 0000000..c5451b2 --- /dev/null +++ b/src/Shared/Modules/Managers/UIManager/Window.lua @@ -0,0 +1,99 @@ +local types = require(script.Parent.Types) + +local window: types.WindowClass = {} :: types.WindowClass +window["__index"] = window + +local module = {} + +function module.new(name: string): types.Window + local self = setmetatable({ + Name= name, + Pages = { + Open = {}, + Stored = {}, + }, + + _ = {}, + }, window) + + return self +end + +function window:BuildPages() + for _, page: types.Page in self.Pages.Stored do + page:Build(self) + end +end + +function window:AddPage(page: types.Page) + self.Pages.Stored[page.Name] = page +end + +function window:GetPage(page: string): types.Page + return self.Pages.Stored[page] +end + +function window:RemovePages() + for _, page: types.Page in self.Pages.Open do + page:Remove() + end + + self.Pages.Open = {} + self.Pages.Stored = {} +end + +function window:OnBuild(callback: (self: types.Window) -> ()) + self._.OnBuild = callback +end + +function window:OnOpen(callback: (self: types.Window) -> ()) + self._.OnOpen = callback +end + +function window:OnClose(callback: (self: types.Window) -> ()) + self._.OnClose = callback +end + +function window:Build(manager: types.Manager) + self:Clean() + + self.Manager = manager + + if self._.OnBuild then + self._.OnBuild(self) + end +end + +function window:Open() + if self.ScreenGui then + self.ScreenGui.Enabled = true + end + + if self._.OnOpen then + self._.OnOpen(self) + end +end + +function window:Close() + if self.ScreenGui then + self.ScreenGui.Enabled = false + end + + if self._.OnClose then + self._.OnClose(self) + end +end + +function window:Clean() + self:RemovePages() +end + +function window:Remove() + self:Clean() + + if self.ScreenGui then + self.ScreenGui:Destroy() + end +end + +return module \ No newline at end of file diff --git a/src/Shared/Modules/Managers/UIManager/init.lua b/src/Shared/Modules/Managers/UIManager/init.lua new file mode 100644 index 0000000..4360c55 --- /dev/null +++ b/src/Shared/Modules/Managers/UIManager/init.lua @@ -0,0 +1,53 @@ +local replicated = game:GetService("ReplicatedStorage") +local trove = require(replicated.Modules.Utility.Trove) +local types = require(script.Types) + +local manager: types.Manager = { + Packages = {Trove = trove}, + + Windows = { + Open = {}, + Stored = {}, + }, +} :: types.Manager + +function manager:GetWindow(name: string) + return self.Windows.Stored[name] +end + +function manager:OpenWindow(window: types.Window) + window:Build(self) + window:Open() + + self.Windows.Open[window.Name] = window +end + +function manager:CloseWindow(window: types.Window) + window:Close() + window:Remove() + + self.Windows.Open[window.Name] = nil +end + +function manager:Build(source: { ModuleScript }) + self:Clean() + + for _, module: ModuleScript in source do + local window: types.Window = require(module) :: types.Window + + self.Windows.Stored[window.Name] = window + end +end + +function manager:Clean() + for _, window in self.Windows.Open do + window:Remove() + end + + self.Windows = { + Open = {}, + Stored = {}, + } +end + +return manager \ No newline at end of file diff --git a/src/Shared/Modules/Utility/Trove.lua b/src/Shared/Modules/Utility/Trove.lua new file mode 100644 index 0000000..449799e --- /dev/null +++ b/src/Shared/Modules/Utility/Trove.lua @@ -0,0 +1,415 @@ +-- Trove +-- Stephen Leitnick +-- October 16, 2021 + +local FN_MARKER = newproxy() +local THREAD_MARKER = newproxy() + +local RunService = game:GetService("RunService") + +local function GetObjectCleanupFunction(object, cleanupMethod) + local t = typeof(object) + if t == "function" then + return FN_MARKER + elseif t == "thread" then + return THREAD_MARKER + end + if cleanupMethod then + return cleanupMethod + end + if t == "Instance" then + return "Destroy" + elseif t == "RBXScriptConnection" then + return "Disconnect" + elseif t == "table" then + if typeof(object.Destroy) == "function" then + return "Destroy" + elseif typeof(object.Disconnect) == "function" then + return "Disconnect" + end + end + error("Failed to get cleanup function for object " .. t .. ": " .. tostring(object), 3) +end + +local function AssertPromiseLike(object) + if + type(object) ~= "table" + or type(object.getStatus) ~= "function" + or type(object.finally) ~= "function" + or type(object.cancel) ~= "function" + then + error("Did not receive a Promise as an argument", 3) + end +end + +--[=[ + @class Trove + A Trove is helpful for tracking any sort of object during + runtime that needs to get cleaned up at some point. +]=] +local Trove = {} +Trove.__index = Trove + +--[=[ + @return Trove + Constructs a Trove object. +]=] +function Trove.new() + local self = setmetatable({}, Trove) + self._objects = {} + self._cleaning = false + return self +end + +export type Trove = typeof(Trove.new()) + +--[=[ + @return Trove + Creates and adds another trove to itself. This is just shorthand + for `trove:Construct(Trove)`. This is useful for contexts where + the trove object is present, but the class itself isn't. + + :::note + This does _not_ clone the trove. In other words, the objects in the + trove are not given to the new constructed trove. This is simply to + construct a new Trove and add it as an object to track. + ::: + + ```lua + local trove = Trove.new() + local subTrove = trove:Extend() + + trove:Clean() -- Cleans up the subTrove too + ``` +]=] +function Trove:Extend() + if self._cleaning then + error("Cannot call trove:Extend() while cleaning", 2) + end + return self:Construct(Trove) +end + +--[=[ + Clones the given instance and adds it to the trove. Shorthand for + `trove:Add(instance:Clone())`. +]=] +function Trove:Clone(instance: Instance): Instance + if self._cleaning then + error("Cannot call trove:Clone() while cleaning", 2) + end + return self:Add(instance:Clone()) +end + +--[=[ + @param class table | (...any) -> any + @param ... any + @return any + Constructs a new object from either the + table or function given. + + If a table is given, the table's `new` + function will be called with the given + arguments. + + If a function is given, the function will + be called with the given arguments. + + The result from either of the two options + will be added to the trove. + + This is shorthand for `trove:Add(SomeClass.new(...))` + and `trove:Add(SomeFunction(...))`. + + ```lua + local Signal = require(somewhere.Signal) + + -- All of these are identical: + local s = trove:Construct(Signal) + local s = trove:Construct(Signal.new) + local s = trove:Construct(function() return Signal.new() end) + local s = trove:Add(Signal.new()) + + -- Even Roblox instances can be created: + local part = trove:Construct(Instance, "Part") + ``` +]=] +function Trove:Construct(class, ...) + if self._cleaning then + error("Cannot call trove:Construct() while cleaning", 2) + end + local object = nil + local t = type(class) + if t == "table" then + object = class.new(...) + elseif t == "function" then + object = class(...) + end + return self:Add(object) +end + +--[=[ + @param signal RBXScriptSignal + @param fn (...: any) -> () + @return RBXScriptConnection + Connects the function to the signal, adds the connection + to the trove, and then returns the connection. + + This is shorthand for `trove:Add(signal:Connect(fn))`. + + ```lua + trove:Connect(workspace.ChildAdded, function(instance) + print(instance.Name .. " added to workspace") + end) + ``` +]=] +function Trove:Connect(signal: RBXScriptSignal, fn) + if self._cleaning then + error("Cannot call trove:Connect() while cleaning", 2) + end + return self:Add(signal:Connect(fn)) +end + +--[=[ + @param signal RBXScriptSignal + @param fn (...: any) -> () + @param metatable { [any]: any? } + @param ... Passed arguements + @return RBXScriptConnection + Connects the function to the signal, adds the connection + to the trove, and then returns the connection. + + This is shorthand for `trove:Add(signal:Connect(fn))`. + + ```lua + trove:ConnectMethod(workspace.ChildAdded, function(instance) + print(instance.Name .. " added to workspace") + end, ...) + ``` +]=] +function Trove:ConnectMethod(signal: RBXScriptSignal, methodName: string, metatable, parameters: {any}) + if self._cleaning then + error("Cannot call trove:Connect() while cleaning", 2) + end + + if not metatable[methodName] then + error("Cannot call trove:Connect() because the method does not exist: " .. methodName, 2) + end + + return self:Add(signal:Connect(function() + metatable[methodName](metatable, table.unpack(parameters)) + end)) +end + +--[=[ + @param name string + @param priority number + @param fn (dt: number) -> () + Calls `RunService:BindToRenderStep` and registers a function in the + trove that will call `RunService:UnbindFromRenderStep` on cleanup. + + ```lua + trove:BindToRenderStep("Test", Enum.RenderPriority.Last.Value, function(dt) + -- Do something + end) + ``` +]=] +function Trove:BindToRenderStep(name: string, priority: number, fn: (dt: number) -> ()) + if self._cleaning then + error("Cannot call trove:BindToRenderStep() while cleaning", 2) + end + RunService:BindToRenderStep(name, priority, fn) + self:Add(function() + RunService:UnbindFromRenderStep(name) + end) +end + +--[=[ + @param promise Promise + @return Promise + Gives the promise to the trove, which will cancel the promise if the trove is cleaned up or if the promise + is removed. The exact promise is returned, thus allowing chaining. + + ```lua + trove:AddPromise(doSomethingThatReturnsAPromise()) + :andThen(function() + print("Done") + end) + -- Will cancel the above promise (assuming it didn't resolve immediately) + trove:Clean() + + local p = trove:AddPromise(doSomethingThatReturnsAPromise()) + -- Will also cancel the promise + trove:Remove(p) + ``` + + :::caution Promise v4 Only + This is only compatible with the [roblox-lua-promise](https://eryn.io/roblox-lua-promise/) library, version 4. + ::: +]=] +function Trove:AddPromise(promise) + if self._cleaning then + error("Cannot call trove:AddPromise() while cleaning", 2) + end + AssertPromiseLike(promise) + if promise:getStatus() == "Started" then + promise:finally(function() + if self._cleaning then + return + end + self:_findAndRemoveFromObjects(promise, false) + end) + self:Add(promise, "cancel") + end + return promise +end + +--[=[ + @param object any -- Object to track + @param cleanupMethod string? -- Optional cleanup name override + @return object: any + Adds an object to the trove. Once the trove is cleaned or + destroyed, the object will also be cleaned up. + + The following types are accepted (e.g. `typeof(object)`): + + | Type | Cleanup | + | ---- | ------- | + | `Instance` | `object:Destroy()` | + | `RBXScriptConnection` | `object:Disconnect()` | + | `function` | `object()` | + | `thread` | `coroutine.close(object)` | + | `table` | `object:Destroy()` _or_ `object:Disconnect()` | + | `table` with `cleanupMethod` | `object:()` | + + Returns the object added. + + ```lua + -- Add a part to the trove, then destroy the trove, + -- which will also destroy the part: + local part = Instance.new("Part") + trove:Add(part) + trove:Destroy() + + -- Add a function to the trove: + trove:Add(function() + print("Cleanup!") + end) + trove:Destroy() + + -- Standard cleanup from table: + local tbl = {} + function tbl:Destroy() + print("Cleanup") + end + trove:Add(tbl) + + -- Custom cleanup from table: + local tbl = {} + function tbl:DoSomething() + print("Do something on cleanup") + end + trove:Add(tbl, "DoSomething") + ``` +]=] +function Trove:Add(object: any, cleanupMethod: string?): any + if self._cleaning then + error("Cannot call trove:Add() while cleaning", 2) + end + local cleanup = GetObjectCleanupFunction(object, cleanupMethod) + table.insert(self._objects, { object, cleanup }) + return object +end + +--[=[ + @param object any -- Object to remove + Removes the object from the Trove and cleans it up. + + ```lua + local part = Instance.new("Part") + trove:Add(part) + trove:Remove(part) + ``` +]=] +function Trove:Remove(object: any): boolean + if self._cleaning then + error("Cannot call trove:Remove() while cleaning", 2) + end + return self:_findAndRemoveFromObjects(object, true) +end + +--[=[ + Cleans up all objects in the trove. This is + similar to calling `Remove` on each object + within the trove. The ordering of the objects + removed is _not_ guaranteed. +]=] +function Trove:Clean() + if self._cleaning then + return + end + self._cleaning = true + for _, obj in self._objects do + self:_cleanupObject(obj[1], obj[2]) + end + table.clear(self._objects) + self._cleaning = false +end + +function Trove:_findAndRemoveFromObjects(object: any, cleanup: boolean): boolean + local objects = self._objects + for i, obj in ipairs(objects) do + if obj[1] == object then + local n = #objects + objects[i] = objects[n] + objects[n] = nil + if cleanup then + self:_cleanupObject(obj[1], obj[2]) + end + return true + end + end + return false +end + +function Trove:_cleanupObject(object, cleanupMethod) + if cleanupMethod == FN_MARKER then + object() + elseif cleanupMethod == THREAD_MARKER then + coroutine.close(object) + else + object[cleanupMethod](object) + end +end + +--[=[ + @param instance Instance + @return RBXScriptConnection + Attaches the trove to a Roblox instance. Once this + instance is removed from the game (parent or ancestor's + parent set to `nil`), the trove will automatically + clean up. + + :::caution + Will throw an error if `instance` is not a descendant + of the game hierarchy. + ::: +]=] +function Trove:AttachToInstance(instance: Instance) + if self._cleaning then + error("Cannot call trove:AttachToInstance() while cleaning", 2) + elseif not instance:IsDescendantOf(game) then + error("Instance is not a descendant of the game hierarchy", 2) + end + return self:Connect(instance.Destroying, function() + self:Destroy() + end) +end + +--[=[ + Alias for `trove:Clean()`. +]=] +function Trove:Destroy() + self:Clean() +end + +return Trove