--- Manages the cleaning of events and other things.
-- Useful for encapsulating state and make deconstructors easy
-- @classmod Maid
-- @see Signal
local Maid = {}
Maid.ClassName = "Maid"
--- Returns a new Maid object
-- @constructor Maid.new()
-- @treturn Maid
function Maid.new()
local self = {}
self._tasks = {}
return setmetatable(self, Maid)
end
--- Returns Maid[key] if not part of Maid metatable
-- @return Maid[key] value
function Maid:__index(index)
if Maid[index] then
return Maid[index]
else
return self._tasks[index]
end
end
--- Add a task to clean up
-- @usage
-- Maid[key] = (function) Adds a task to perform
-- Maid[key] = (event connection) Manages an event connection
-- Maid[key] = (Maid) Maids can act as an event connection, allowing a Maid to have other maids to clean up.
-- Maid[key] = (Object) Maids can cleanup objects with a `Destroy` method
-- Maid[key] = nil Removes a named task. If the task is an event, it is disconnected. If it is an object, it is destroyed.
function Maid:__newindex(index, newTask)
if Maid[index] ~= nil then
error(("'%s' is reserved"):format(tostring(index)), 2)
end
local tasks = self._tasks
local oldTask = tasks[index]
tasks[index] = newTask
if oldTask then
if type(oldTask) == "function" then
oldTask()
elseif typeof(oldTask) == "RBXScriptConnection" then
oldTask:Disconnect()
elseif oldTask.Destroy then
oldTask:Destroy()
end
end
end
--- Same as indexing, but uses an incremented number as a key.
-- @param task An item to clean
-- @treturn number taskId
function Maid:GiveTask(task)
assert(task)
local taskId = #self._tasks+1
self[taskId] = task
return taskId
end
--- Cleans up all tasks.
-- @alias Destroy
function Maid:DoCleaning()
local tasks = self._tasks
-- Disconnect all events first as we know this is safe
for index, task in pairs(tasks) do
if typeof(task) == "RBXScriptConnection" then
tasks[index] = nil
task:Disconnect()
end
end
-- Clear out tasks table completely, even if clean up tasks add more tasks to the maid
local index, task = next(tasks)
while task ~= nil do
tasks[index] = nil
if type(task) == "function" then
task()
elseif typeof(task) == "RBXScriptConnection" then
task:Disconnect()
elseif task.Destroy then
task:Destroy()
end
index, task = next(tasks)
end
end
--- Alias for DoCleaning()
-- @function Destroy
Maid.Destroy = Maid.DoCleaning
return Maid
SupportLibrary = {};
function SupportLibrary.FindTableOccurrences(Haystack, Needle)
-- Returns the positions of instances of `needle` in table `haystack`
local Positions = {};
-- Add any indexes from `Haystack` that are `Needle`
for Index, Value in pairs(Haystack) do
if Value == Needle then
table.insert(Positions, Index);
end;
end;
return Positions;
end;
function SupportLibrary.FindTableOccurrence(Haystack, Needle)
-- Returns one occurrence of `Needle` in `Haystack`
-- Search for the first instance of `Needle` found and return it
for Index, Value in pairs(Haystack) do
if Value == Needle then
return Index;
end;
end;
-- If no occurrences exist, return `nil`
return nil;
end;
function SupportLibrary.IsInTable(Haystack, Needle)
-- Returns whether the given `Needle` can be found within table `Haystack`
-- Go through every value in `Haystack` and return whether `Needle` is found
for _, Value in pairs(Haystack) do
if Value == Needle then
return true;
end;
end;
-- If no instances were found, return false
return false;
end;
function SupportLibrary.DoTablesMatch(A, B)
-- Returns whether the values of tables A and B are the same
-- Check B table differences
for Index in pairs(A) do
if A[Index] ~= B[Index] then
return false;
end;
end;
-- Check A table differences
for Index in pairs(B) do
if B[Index] ~= A[Index] then
return false;
end;
end;
-- Return true if no differences
return true;
end;
function SupportLibrary.Round(Number, Places)
-- Returns `Number` rounded to the given number of decimal places (from lua-users)
-- Ensure that `Number` is a number
if type(Number) ~= 'number' then
return;
end;
-- Round the number
local Multiplier = 10 ^ (Places or 0);
local RoundedNumber = math.floor(Number * Multiplier + 0.5) / Multiplier;
-- Return the rounded number
return RoundedNumber;
end;
function SupportLibrary.CloneTable(Table)
-- Returns a copy of `Table`
local ClonedTable = {};
-- Copy all values into `ClonedTable`
for Key, Value in pairs(Table) do
ClonedTable[Key] = Value;
end;
-- Return the clone
return ClonedTable;
end;
function SupportLibrary.Merge(Target, ...)
-- Copies members of the given tables into the specified target table
local Tables = { ... }
-- Copy members from each table into target
for TableOrder, Table in ipairs(Tables) do
for Key, Value in pairs(Table) do
Target[Key] = Value
end
end
-- Return target
return Target
end
-- Create symbol representing a blank value
local Blank = newproxy(true)
SupportLibrary.Blank = Blank
getmetatable(Blank).__tostring = function ()
return 'Symbol(Blank)'
end
function SupportLibrary.MergeWithBlanks(Target, ...)
-- Copies members of the given tables into the specified target table, including blank values
local Tables = { ... }
-- Copy members from each table into target
for TableOrder, Table in ipairs(Tables) do
for Key, Value in pairs(Table) do
if Value == Blank then
Target[Key] = nil
else
Target[Key] = Value
end
end
end
-- Return target
return Target
end
function SupportLibrary.GetAllDescendants(Parent)
-- Recursively gets all the descendants of `Parent` and returns them
local Descendants = {};
for _, Child in pairs(Parent:GetChildren()) do
-- Add the direct descendants of `Parent`
table.insert(Descendants, Child);
-- Add the descendants of each child
for _, Subchild in pairs(SupportLibrary.GetAllDescendants(Child)) do
table.insert(Descendants, Subchild);
end;
end;
return Descendants;
end;
function SupportLibrary.GetDescendantsWhichAreA(Object, Class)
-- Returns descendants of `Object` which match `Class`
local Matches = {}
-- Check each descendant
for _, Descendant in pairs(Object:GetDescendants()) do
if Descendant:IsA(Class) then
Matches[#Matches + 1] = Descendant
end
end
-- Return matches
return Matches
end
function SupportLibrary.FilterArray(Array, Callback)
-- Returns a filtered copy of `Array` based on the filter `Callback`
local FilteredArray = {}
-- Add items from `Array` that `Callback` returns `true` on
for Key, Value in ipairs(Array) do
if Callback(Value, Key) then
table.insert(FilteredArray, Value)
end
end
return FilteredArray
end
function SupportLibrary.FilterMap(Map, Callback)
-- Returns a filtered copy of `Map` based on the filter `Callback`
local FilteredMap = {}
-- Add items from `Map` that `Callback` returns `true` on
for Key, Value in ipairs(Map) do
if Callback(Value, Key) then
FilteredMap[Key] = Value
end
end
return FilteredMap
end
function SupportLibrary.GetDescendantCount(Parent)
-- Recursively gets a count of all the descendants of `Parent` and returns them
local Count = 0;
for _, Child in pairs(Parent:GetChildren()) do
-- Count the direct descendants of `Parent`
Count = Count + 1;
-- Count and add the descendants of each child
Count = Count + SupportLibrary.GetDescendantCount(Child);
end;
return Count;
end;
function SupportLibrary.CloneParts(Parts)
-- Returns a table of cloned `Parts`
local Clones = {};
-- Copy the parts into `Clones`
for Index, Part in pairs(Parts) do
Clones[Index] = Part:Clone();
end;
return Clones;
end;
function SupportLibrary.SplitString(String, Delimiter)
-- Returns a table of string `String` split by pattern `Delimiter`
local StringParts = {};
local Pattern = ('([^%s]+)'):format(Delimiter);
-- Capture each separated part
String:gsub(Pattern, function (Part)
table.insert(StringParts, Part);
end);
return StringParts;
end;
function SupportLibrary.GetChildOfClass(Parent, ClassName, Inherit)
-- Returns the first child of `Parent` that is of class `ClassName`
-- or nil if it couldn't find any
-- Look for a child of `Parent` of class `ClassName` and return it
if not Inherit then
for _, Child in pairs(Parent:GetChildren()) do
if Child.ClassName == ClassName then
return Child;
end;
end;
else
for _, Child in pairs(Parent:GetChildren()) do
if Child:IsA(ClassName) then
return Child;
end;
end;
end;
return nil;
end;
function SupportLibrary.GetChildrenOfClass(Parent, ClassName, Inherit)
-- Returns a table containing the children of `Parent` that are
-- of class `ClassName`
local Matches = {};
if not Inherit then
for _, Child in pairs(Parent:GetChildren()) do
if Child.ClassName == ClassName then
table.insert(Matches, Child);
end;
end;
else
for _, Child in pairs(Parent:GetChildren()) do
if Child:IsA(ClassName) then
table.insert(Matches, Child);
end;
end;
end;
return Matches;
end;
function SupportLibrary.HSVToRGB(Hue, Saturation, Value)
-- Returns the RGB equivalent of the given HSV-defined color
-- (adapted from some code found around the web)
-- If it's achromatic, just return the value
if Saturation == 0 then
return Value;
end;
-- Get the hue sector
local HueSector = math.floor(Hue / 60);
local HueSectorOffset = (Hue / 60) - HueSector;
local P = Value * (1 - Saturation);
local Q = Value * (1 - Saturation * HueSectorOffset);
local T = Value * (1 - Saturation * (1 - HueSectorOffset));
if HueSector == 0 then
return Value, T, P;
elseif HueSector == 1 then
return Q, Value, P;
elseif HueSector == 2 then
return P, Value, T;
elseif HueSector == 3 then
return P, Q, Value;
elseif HueSector == 4 then
return T, P, Value;
elseif HueSector == 5 then
return Value, P, Q;
end;
end;
function SupportLibrary.RGBToHSV(Red, Green, Blue)
-- Returns the HSV equivalent of the given RGB-defined color
-- (adapted from some code found around the web)
local Hue, Saturation, Value;
local MinValue = math.min(Red, Green, Blue);
local MaxValue = math.max(Red, Green, Blue);
Value = MaxValue;
local ValueDelta = MaxValue - MinValue;
-- If the color is not black
if MaxValue ~= 0 then
Saturation = ValueDelta / MaxValue;
-- If the color is purely black
else
Saturation = 0;
Hue = -1;
return Hue, Saturation, Value;
end;
if Red == MaxValue then
Hue = (Green - Blue) / ValueDelta;
elseif Green == MaxValue then
Hue = 2 + (Blue - Red) / ValueDelta;
else
Hue = 4 + (Red - Green) / ValueDelta;
end;
Hue = Hue * 60;
if Hue < 0 then
Hue = Hue + 360;
end;
return Hue, Saturation, Value;
end;
function SupportLibrary.IdentifyCommonItem(Items)
-- Returns the common item in table `Items`, or `nil` if
-- they vary
local CommonItem = nil;
for ItemIndex, Item in pairs(Items) do
-- Set the initial item to compare against
if ItemIndex == 1 then
CommonItem = Item;
-- Check if this item is the same as the rest
else
-- If it isn't the same, there is no common item, so just stop right here
if Item ~= CommonItem then
return nil;
end;
end;
end;
-- Return the common item
return CommonItem;
end;
function SupportLibrary.IdentifyCommonProperty(Items, Property)
-- Returns the common `Property` value in the instances given in `Items`
local PropertyVariations = {};
-- Capture all the variations of the property value
for _, Item in pairs(Items) do
table.insert(PropertyVariations, Item[Property]);
end;
-- Return the common property value
return SupportLibrary.IdentifyCommonItem(PropertyVariations);
end;
function SupportLibrary.GetPartCorners(Part)
-- Returns a table of the given part's corners' CFrames
-- Make references to functions called a lot for efficiency
local Insert = table.insert;
local ToWorldSpace = CFrame.new().toWorldSpace;
local NewCFrame = CFrame.new;
-- Get info about the part
local PartCFrame = Part.CFrame;
local SizeX, SizeY, SizeZ = Part.Size.x / 2, Part.Size.y / 2, Part.Size.z / 2;
-- Get each corner
local Corners = {};
Insert(Corners, ToWorldSpace(PartCFrame, NewCFrame(SizeX, SizeY, SizeZ)));
Insert(Corners, ToWorldSpace(PartCFrame, NewCFrame(-SizeX, SizeY, SizeZ)));
Insert(Corners, ToWorldSpace(PartCFrame, NewCFrame(SizeX, -SizeY, SizeZ)));
Insert(Corners, ToWorldSpace(PartCFrame, NewCFrame(SizeX, SizeY, -SizeZ)));
Insert(Corners, ToWorldSpace(PartCFrame, NewCFrame(-SizeX, SizeY, -SizeZ)));
Insert(Corners, ToWorldSpace(PartCFrame, NewCFrame(-SizeX, -SizeY, SizeZ)));
Insert(Corners, ToWorldSpace(PartCFrame, NewCFrame(SizeX, -SizeY, -SizeZ)));
Insert(Corners, ToWorldSpace(PartCFrame, NewCFrame(-SizeX, -SizeY, -SizeZ)));
return Corners;
end;
function SupportLibrary.ImportServices()
-- Adds references to common services into the calling environment
-- Get the calling environment
local CallingEnvironment = getfenv(2);
-- Add the services
CallingEnvironment.Workspace = Game:GetService 'Workspace';
CallingEnvironment.Players = Game:GetService 'Players';
CallingEnvironment.MarketplaceService = Game:GetService 'MarketplaceService';
CallingEnvironment.ContentProvider = Game:GetService 'ContentProvider';
CallingEnvironment.SoundService = Game:GetService 'SoundService';
CallingEnvironment.UserInputService = Game:GetService 'UserInputService';
CallingEnvironment.SelectionService = Game:GetService 'Selection';
CallingEnvironment.CoreGui = Game:GetService 'CoreGui';
CallingEnvironment.HttpService = Game:GetService 'HttpService';
CallingEnvironment.ChangeHistoryService = Game:GetService 'ChangeHistoryService';
CallingEnvironment.ReplicatedStorage = Game:GetService 'ReplicatedStorage';
CallingEnvironment.GroupService = Game:GetService 'GroupService';
CallingEnvironment.ServerScriptService = Game:GetService 'ServerScriptService';
CallingEnvironment.ServerStorage = Game:GetService 'ServerStorage';
CallingEnvironment.StarterGui = Game:GetService 'StarterGui';
CallingEnvironment.RunService = Game:GetService 'RunService';
end;
function SupportLibrary.GetListMembers(List, MemberName)
-- Gets the given member for each object in the given list table
local Members = {}
-- Collect the member values for each item in the list
for Key, Item in ipairs(List) do
Members[Key] = Item[MemberName]
end
-- Return the members
return Members
end
function SupportLibrary.GetMemberMap(List, MemberName)
-- Maps the given items' specified members to each item
local Map = {}
-- Collect member values
for Key, Item in ipairs(List) do
Map[Item] = Item[MemberName]
end
-- Return map
return Map
end
function SupportLibrary.AddUserInputListener(InputState, InputTypeFilter, CatchAll, Callback)
-- Connects to the given user input event and takes care of standard boilerplate code
-- Create input type whitelist
local InputTypes = {}
if type(InputTypeFilter) == 'string' then
InputTypes[InputTypeFilter] = true
elseif type(InputTypeFilter) == 'table' then
InputTypes = SupportLibrary.FlipTable(InputTypeFilter)
end
-- Create a UserInputService listener based on the given `InputState`
return Game:GetService('UserInputService')['Input' .. InputState]:Connect(function (Input, GameProcessedEvent)
-- Make sure this input was not captured by the client (unless `CatchAll` is enabled)
if GameProcessedEvent and not CatchAll then
return;
end;
-- Make sure this is the right input type
if not InputTypes[Input.UserInputType.Name] then
return;
end;
-- Make sure any key input did not occur while typing into a UI
if InputType == Enum.UserInputType.Keyboard and Game:GetService('UserInputService'):GetFocusedTextBox() then
return;
end;
-- Call back upon passing all conditions
Callback(Input);
end);
end;
function SupportLibrary.AddGuiInputListener(Gui, InputState, InputTypeFilter, CatchAll, Callback)
-- Connects to the given GUI user input event and takes care of standard boilerplate code
-- Create input type whitelist
local InputTypes = {}
if type(InputTypeFilter) == 'string' then
InputTypes[InputTypeFilter] = true
elseif type(InputTypeFilter) == 'table' then
InputTypes = SupportLibrary.FlipTable(InputTypeFilter)
end
-- Create a UserInputService listener based on the given `InputState`
return Gui['Input' .. InputState]:Connect(function (Input, GameProcessedEvent)
-- Make sure this input was not captured by the client (unless `CatchAll` is enabled)
if GameProcessedEvent and not CatchAll then
return;
end;
-- Make sure this is the right input type
if not InputTypes[Input.UserInputType.Name] then
return;
end;
-- Call back upon passing all conditions
Callback(Input);
end);
end;
function SupportLibrary.AreKeysPressed(...)
-- Returns whether the given keys are pressed
local RequestedKeysPressed = 0;
-- Get currently pressed keys
local PressedKeys = SupportLibrary.GetListMembers(Game:GetService('UserInputService'):GetKeysPressed(), 'KeyCode');
-- Go through each requested key
for _, Key in pairs({ ... }) do
-- Count requested keys that are pressed
if SupportLibrary.IsInTable(PressedKeys, Key) then
RequestedKeysPressed = RequestedKeysPressed + 1;
end;
end;
-- Return whether all the requested keys are pressed or not
return RequestedKeysPressed == #{...};
end;
function SupportLibrary.ConcatTable(TargetTable, ...)
-- Inserts all values from given source tables into target
local SourceTables = { ... }
-- Insert values from each source table into target
for TableOrder, SourceTable in ipairs(SourceTables) do
for Key, Value in ipairs(SourceTable) do
table.insert(TargetTable, Value)
end
end
-- Return the destination table
return TargetTable
end
function SupportLibrary.ClearTable(Table)
-- Clears out every value in `Table`
-- Clear each index
for Index in pairs(Table) do
Table[Index] = nil;
end;
-- Return the given table
return Table;
end;
function SupportLibrary.Values(Table)
-- Returns all the values in the given table
local Values = {};
-- Go through each key and get each value
for _, Value in pairs(Table) do
table.insert(Values, Value);
end;
-- Return the values
return Values;
end;
function SupportLibrary.Keys(Table)
-- Returns all the keys in the given table
local Keys = {};
-- Go through each key and get each value
for Key in pairs(Table) do
table.insert(Keys, Key);
end;
-- Return the values
return Keys;
end;
function SupportLibrary.Call(Function, ...)
-- Returns a callback to `Function` with the given arguments
local Args = { ... }
return function (...)
return Function(unpack(
SupportLibrary.ConcatTable({}, Args, { ... })
))
end
end
function SupportLibrary.Trim(String)
-- Returns a trimmed version of `String` (adapted from code from lua-users)
return (String:gsub("^%s*(.-)%s*$", "%1"));
end
function SupportLibrary.ChainCall(...)
-- Returns function that passes arguments through given functions and returns the final result
-- Get the given chain of functions
local Chain = { ... };
-- Return the chaining function
return function (...)
-- Get arguments
local Arguments = { ... };
-- Go through each function and store the returned data to reuse in the next function's arguments
for _, Function in ipairs(Chain) do
Arguments = { Function(unpack(Arguments)) };
end;
-- Return the final returned data
return unpack(Arguments);
end;
end;
function SupportLibrary.CountKeys(Table)
-- Returns the number of keys in `Table`
local Count = 0;
-- Count each key
for _ in pairs(Table) do
Count = Count + 1;
end;
-- Return the count
return Count;
end;
function SupportLibrary.Slice(Table, Start, End)
-- Returns values from `Start` to `End` in `Table`
local Slice = {};
-- Go through the given indices
for Index = Start, End do
table.insert(Slice, Table[Index]);
end;
-- Return the slice
return Slice;
end;
function SupportLibrary.FlipTable(Table)
-- Returns a table with keys and values in `Table` swapped
local FlippedTable = {};
-- Flip each key and value
for Key, Value in pairs(Table) do
FlippedTable[Value] = Key;
end;
-- Return the flipped table
return FlippedTable;
end;
function SupportLibrary.ScheduleRecurringTask(TaskFunction, Interval)
-- Repeats `Task` every `Interval` seconds until stopped
-- Create a task object
local Task = {
-- A switch determining if it's running or not
Running = true;
-- A function to stop this task
Stop = function (Task)
Task.Running = false;
end;
-- References to the task function and set interval
TaskFunction = TaskFunction;
Interval = Interval;
};
coroutine.wrap(function (Task)
-- Repeat the task
while wait(Task.Interval) and Task.Running do
Task.TaskFunction();
end;
end)(Task);
-- Return the task object
return Task;
end;
function SupportLibrary.Loop(Interval, Function, ...)
-- Calls the given function repeatedly at the specified interval until stopped
local Args = { ... }
-- Create state
local Running = true
local Stop = function ()
Running = nil
end
-- Start loop
coroutine.wrap(function ()
while wait(Interval) and Running do
Function(unpack(Args))
end
end)()
-- Return stopping callback
return Stop
end
function SupportLibrary.Clamp(Number, Minimum, Maximum)
-- Returns the given number, clamped according to the provided min/max
-- Clamp the number
if Minimum and Number < Minimum then
Number = Minimum;
elseif Maximum and Number > Maximum then
Number = Maximum;
end;
-- Return the clamped number
return Number;
end;
function SupportLibrary.ReverseTable(Table)
-- Returns a new table with values in the opposite order
local ReversedTable = {};
-- Copy each value at the opposite key
for Index, Value in ipairs(Table) do
ReversedTable[#Table - Index + 1] = Value;
end;
-- Return the reversed table
return ReversedTable;
end;
function SupportLibrary.CreateConsecutiveCallDeferrer(MaxInterval)
-- Returns a callback for determining whether to execute consecutive calls
local LastCallTime
local function ShouldExecuteCall()
-- Mark latest call time
local CallTime = tick()
LastCallTime = CallTime
-- Indicate whether call still latest
wait(MaxInterval)
return LastCallTime == CallTime
end
-- Return callback
return ShouldExecuteCall
end
return SupportLibrary;
--- An expensive way to spawn a function. However, unlike spawn(), it executes on the same frame, and
-- unlike coroutines, does not obscure errors
-- @module fastSpawn
return function(func, ...)
assert(type(func) == "function")
local args = {...}
local count = select("#", ...)
local bindable = Instance.new("BindableEvent")
bindable.Event:Connect(function()
func(unpack(args, 1, count))
end)
bindable:Fire()
bindable:Destroy()
end