(Mainly on this custom made for: BotService - Create and control fake players)
I hope this “Resource” will be helpful. <3
Since (Suprisingly) people like the A* Pathfinding Modulethat I made. So I’ll give you a new Module .(Which has a Service in the name)
This is the BotService. Create and Control Fake Players. Which I will describe these Fake Players as Bots…
This Module will help make NPCs test your game.
Features:
- Custom Params:
BotParams: Its a way to set up a Bot by using these as the following:
RigType: string → Use “R15” for an R15 Rig or “R6” for a R6 Rig
GenderType: string → Can be used for Simply using: “Male” or “Female” or Your very own.
Genders: Based on the GenderType. Example:
local Params = botService.BotParams.new()
Params.UseRealPlayerDescriptions = false
Params.RigType = math.random(1,2) == 2 and "R15" or "R6"
Params.GenderType = "Male"
Params.Genders = {
["Male"] = {
["Accessories"] = {
["BackAccessory"] = { 0; 6829556357; 6470135113; };
["FaceAccessory"] = { 0; 376526673; 376527500 };
["FrontAccessory"] = { 0; };
["HairAccessory"] = { 0; 3814474927; 376524487; 62234425; 451221329; 4637254498; };
["HatAccessory"] = { 0; 7212278970; 7212268341; 6829585000; 6555565708; 5917433699; 1772336109; };
["NeckAccessory"] = { 0; 7893377446; 376527115 };
["ShoulderAccessory"] = { 0; 4619597156; 6375710342; };
["WaistAccessory"] = { 0; }
};
["Clothing"] = {
["Classic"] = {
["GraphicTShirt"] = { 0; };
["Pants"] = { 0; 7673101417; 6829667358; 382537950; 398633812; 398635338; 382538503; 144076760; 4637601297 };
["Shirt"] = { 0; 7673098764; 7427983453; 6829670577; 3670737444; 607785314; 398633584; 398635081; 382538059; 382538295; 144076358; 4637596615 }
} or Layered
};
["Heads"] = { 0; 86498048; 4637245706; 4637163809; };
["Bundles"] = { 109; 238; 605; 687 };
["BodyColors"] = {
{
["HeadColor"] = BrickColor.random();
["LeftArmColor"] = BrickColor.random();
["LeftLegColor"] = BrickColor.random();
["RightArmColor"] = BrickColor.random();
["RightLegColor"] = BrickColor.random();
["TorsoColor"] = BrickColor.random();
}
}
}
}
UseRealPlayerDescriptions: boolean → For this use it will generate a real HumanoidDescription. (I will explain later on).
(Now were onto the main frame)
- BotService:CreateBot(Params: BotParams) → The mainfunction to creating a Bot but it is kind of turning to a
Player
class. Hope that’s okay with that. Also the UserId randomzied, including CharacterApperanceId. - Bot:LoadCharacter(self: Bot) → Loading the Bot that can either use
UseRealPlayerDeacription
Which will load a real player Id from theself.Description
but if its false then it will use the Gender thatyou type in GenderType and will also use GenderType from the Genders.
That’s pretty much it. I will update soon. If you want to try it out yourself I’ll justgive the rbxm file and also code.
BotService.rbxm (66.7 KB)
Main Code:
--// Services
local Players = game:GetService("Players")
local UserService = game:GetService("UserService")
local MarketplaceService = game:GetService("MarketplaceService")
local AssetService = game:GetService("AssetService")
local botService = {}
local botService_mt = { __index = botService }
local Bot = {}
local Bot_mt = { __index = Bot }
botService.BotParams = {}
--// Variables
local rigTypes = script:WaitForChild("RigTypes")
local maxRequestAttempts = 100
local savedModules: {
["Avatar"]: typeof(require(script.Modules:WaitForChild("Avatar"))) ;
["Generator"]: typeof(require(script.Modules:WaitForChild("Generator")));
["Signal"]: typeof(require(script.Library:WaitForChild("Signal")))
} = {}
for _, child in ipairs(script.Modules:GetChildren()) do
if child:IsA("ModuleScript") then
savedModules[child.Name] = require(child)
end
end
for _, child in ipairs(script.Library:GetChildren()) do
if child:IsA("ModuleScript") then
savedModules[child.Name] = require(child)
end
end
warn(savedModules)
--// Types
type GenderTypes = "Male" | "Female"| any
type BodyColorType = "HeadColor" | "LeftArmColor" | "LeftLegColor" | "RightArmColor" | "RightLegColor" | "TorsoColor"
type AccessoryType = "BackAccessory" | "FaceAccessory" | "FrontAccessory" | "HairAccessory" | "HatAccessory" | "NeckAccessory" | "ShoulderAccessory" | "WaistAccessory"
type ClassicType = "Shirt" | "Pants" | "GraphicTShirt"
type LayeredType = "Shirt" |"TShirt" |"Jacket" | "Sweater"| "Pants" | "Shorts" | "DressSkirt" | "LeftShoe" | "RightShoe"
type ShoeCategory = "Right" | "Left"
type ClothingType = "Classic" | "Layered"
type AccessoriesData = { [AccessoryType]: {number}; };
type ClothingData = { ["Classic"]: { [ClassicType]: {number}; }; ["Layered"]: { [LayeredType]: {number} } }
type Gender = {
["Heads"]:{number};
["Faces"]: {number};
["Bundles"]: {number};
["Accessories"]: { [AccessoryType]: {number}; };
["Clothing"]: { ["Classic"]: { [ClassicType]: {number}; }; ["Layered"]: { [LayeredType]: {number} } };
["BodyColors"]: { { [BodyColorType]: BrickColor | Color3 } }
}
type Genders = {
[GenderTypes]: Gender;
}
export type BotParams = {
["RigType"]: "R15" | "R6";
["GenderType"]: GenderTypes;
["Genders"]: Genders;
["UseRealPlayerDescriptions"]: boolean;
}
export type Bot = {
--// Main Variables
Character: Model?; --// For the Loading Generated Character
Humanoid: Humanoid; --// For having the Character Loaded
Description: HumanoidDescription?; --// For having the Character Loaded
Name: string;
DisplayName: string;
UserId: number;
CharacterAppearanceId: number;
CanLoadCharacterAppearance: typeof(game.Players.LocalPlayer.CanLoadCharacterAppearance);
Team: Team?;
TeamColor:BrickColor;
--// Script Signals
CharacterAdded: typeof(game.Players.LocalPlayer.CharacterAdded); --typeof(savedModules.Signal.new());
CharacterAppearanceLoaded: typeof(game.Players.LocalPlayer.CharacterAppearanceLoaded);
Chatted: typeof(game.Players.LocalPlayer.Chatted);
--// Params
RigType: "R15" | "R6"; --// For knowing what type of rig the character is
Gender: GenderTypes;
GenderData: Gender;
Accessories: AccessoriesData;
Clothing: ClothingData;
Bundles: {number};
BodyColors: { { [BodyColorType]: BrickColor | Color3 } };
Heads: {number};
--// Functions
LoadCharacter: (self: Bot) -> ();
}
--// Variables
local AssetChart = {
[Enum.AssetType.HairAccessory.Value] = Enum.AccessoryType.Hair,
[Enum.AssetType.PantsAccessory.Value] = Enum.AccessoryType.Pants,
[Enum.AssetType.ShirtAccessory.Value] = Enum.AccessoryType.Shirt,
[Enum.AssetType.ShortsAccessory.Value] = Enum.AccessoryType.Shorts,
[Enum.AssetType.ShoulderAccessory.Value] = Enum.AccessoryType.Shoulder,
[Enum.AssetType.DressSkirtAccessory.Value] = Enum.AccessoryType.DressSkirt,
[Enum.AssetType.Hat.Value] = Enum.AccessoryType.Hat,
[Enum.AssetType.WaistAccessory.Value] = Enum.AccessoryType.Waist,
[Enum.AssetType.FaceAccessory.Value] = Enum.AccessoryType.Face,
[Enum.AssetType.NeckAccessory.Value] = Enum.AccessoryType.Neck,
[Enum.AssetType.FrontAccessory.Value] = Enum.AccessoryType.Front,
[Enum.AssetType.BackAccessory.Value] = Enum.AccessoryType.Back,
[Enum.AssetType.TShirtAccessory.Value] = Enum.AccessoryType.TShirt,
[Enum.AssetType.JacketAccessory.Value] = Enum.AccessoryType.Jacket,
[Enum.AssetType.SweaterAccessory.Value] = Enum.AccessoryType.Sweater,
[Enum.AssetType.LeftShoeAccessory.Value] = Enum.AccessoryType.LeftShoe,
[Enum.AssetType.RightShoeAccessory.Value] = Enum.AccessoryType.RightShoe,
[Enum.AssetType.Shirt.Value] = "Shirt",
[Enum.AssetType.Pants.Value] = "Pants",
[Enum.AssetType.TShirt.Value] = "GraphicTShirt",
[Enum.AssetType.Face.Value] = Enum.AssetType.Face,
[Enum.AssetType.Head.Value] = Enum.AssetType.Head,
[Enum.AssetType.DynamicHead.Value] = Enum.AssetType.DynamicHead,
}
local BodyPartAssetChart = {
[Enum.AssetType.LeftArm.Value] = Enum.AssetType.LeftArm,
[Enum.AssetType.LeftLeg.Value] = Enum.AssetType.LeftLeg,
[Enum.AssetType.RightArm.Value] = Enum.AssetType.RightArm,
[Enum.AssetType.RightLeg.Value] = Enum.AssetType.RightLeg,
[Enum.AssetType.Torso.Value] = Enum.AssetType.Torso,
}
--// Functions
local function getAssetTypeId(assetType: string)
for i,v in pairs(Enum.AssetType:GetEnumItems()) do
if v.Name == assetType then
return v.Value
end
end
end
-- Function to fetch body parts from a bundle ID
local function getBodyPartsFromBundle(bundleID)
local success, bundleDetails = pcall(function()
return AssetService:GetBundleDetailsAsync(bundleID)
end)
if success and bundleDetails then
-- Filter out the body parts, excluding animations
local bodyParts = {}
for _, item in pairs(bundleDetails.Items) do
-- Fetch detailed product info using item ID
local itemInfoSuccess, itemInfo = pcall(function()
return MarketplaceService:GetProductInfo(item.Id)
end)
-- If the product info is retrieved successfully and it's a body part, add to the list
if itemInfoSuccess and itemInfo then
local assetTypeId = itemInfo.AssetTypeId
if BodyPartAssetChart[assetTypeId] ~= nil then
table.insert(bodyParts, itemInfo)
end
end
end
return bodyParts
else
warn("Failed to get bundle details for ID: " .. tostring(bundleID))
return nil
end
end
local function checkValidAsset(assetId: number)
local success, result = pcall(function()
return MarketplaceService:GetProductInfo(assetId, Enum.InfoType.Asset)
end)
print(result)
end
local function GenerateRandomID()
return math.random(1,1300000000)
end
local function getUserInfo(Id: number)
local success, info = pcall(function()
return UserService:GetUserInfosByUserIdsAsync({Id})
end)
if not success then
return false, {
Id = 0;
Username = "Unknown";
DisplayName = "Unknown";
HasVerifiedBadge = false;
}
else
return true, info[1]
end
end
local function SimpleRayCast(Start,End,IgnoreList)
if IgnoreList == nil then
IgnoreList = {}
elseif type(IgnoreList) ~= "table" then
IgnoreList = {IgnoreList}
end
local RaycastParams = RaycastParams.new()
RaycastParams.FilterDescendantsInstances = IgnoreList
RaycastParams.FilterType = Enum.RaycastFilterType.Blacklist
RaycastParams.IgnoreWater = true
RaycastParams.RespectCanCollide = true
local RaycastResult = game.Workspace:Raycast(Start, (End - Start).Unit * (End - Start).Magnitude, RaycastParams)
if RaycastResult == nil then
return RaycastResult
else
return RaycastResult.Instance, RaycastResult.Position
end
end
local function SimpleRayCastWhitelisted(Start,End,Whitelist)
if Whitelist == nil then
Whitelist = {}
elseif type(Whitelist) ~= "table" then
Whitelist = {Whitelist}
end
local RaycastParams = RaycastParams.new()
RaycastParams.FilterDescendantsInstances = Whitelist
RaycastParams.FilterType = Enum.RaycastFilterType.Whitelist
RaycastParams.IgnoreWater = true
--RaycastParams.RespectCanCollide = true
local RaycastResult = game.Workspace:Raycast(Start, (End - Start).Unit * (End - Start).Magnitude, RaycastParams)
if RaycastResult == nil then
return RaycastResult
else
return RaycastResult.Instance, RaycastResult.Position
end
end
local function GetPartsInRegion3(Min,Max,IgnoreList)
local Region = Region3.new(Min, Max)
local Parts = workspace:FindPartsInRegion3WithIgnoreList(Region, IgnoreList) -- ignore part
return Parts
end
local function GetAllSpawns()
local Spawns = {}
for _,Descendant in pairs(game.Workspace:GetDescendants()) do
if Descendant:IsA("SpawnLocation") then
table.insert(Spawns, Descendant)
end
end
return Spawns
end
local function GetRandomAvailableTeam()
local Teams = game.Teams:GetChildren()
local AvailableTeams = {}
for _,Team in pairs(Teams) do
if Team:IsA("Team") and Team.AutoAssignable == true then
table.insert(AvailableTeams, Team)
end
end
local Team = #AvailableTeams > 0 and AvailableTeams[math.random(1, #AvailableTeams)] or nil
return Team
end
local function GetTeamFromBrickColor(LookForBrickColor)
for _, Team in pairs(game.Teams:GetChildren()) do
if Team.TeamColor == LookForBrickColor then
return Team
end
end
end
function GetViableSpawns(Team)
local PossibleSpawns = {}
local PossibleUnobstructedSpawns = {}
for _,Spawn in pairs(GetAllSpawns()) do
if Spawn.Enabled == true and (Spawn.Neutral == true or (Team and Spawn.TeamColor == Team.TeamColor)) then
table.insert(PossibleSpawns, Spawn)
end
end
for _,Spawn in pairs(PossibleSpawns) do
local _,TopPosition = SimpleRayCastWhitelisted(Spawn.Position + Vector3.new(0,5,0), Spawn.Position, {Spawn})
local MidPosition = TopPosition + Vector3.new(0,2.5,0)
local PartsInWay = GetPartsInRegion3(MidPosition + Vector3.new(-2,-2.5,-2),MidPosition + Vector3.new(2,2.5,2),{Spawn})
if #PartsInWay == 0 then
table.insert(PossibleUnobstructedSpawns, Spawn)
end
end
if #PossibleUnobstructedSpawns == 0 then
return PossibleSpawns
else
return PossibleUnobstructedSpawns
end
end
function GetNextSpawn(Team)
local ViableSpawns = GetViableSpawns(Team)
if #ViableSpawns > 0 then
local ChosenSpawn = ViableSpawns[math.random(1,#ViableSpawns)]
return ChosenSpawn
else
return Vector3.new(0, 100, 0)
end
end
function botService.BotParams.new(): BotParams
return setmetatable({}, {
__index = {
UseRealPlayerDescriptions = false;
RigType = math.random(1,2) == 2 and Enum.HumanoidRigType.R15 or Enum.HumanoidRigType.R6;
GenderType = math.random(1,2) == 2 and "Male" or "Female";
Genders = {
["Male"] = {};
["Female"] = {};
}
}
})
end
--local playerTest = game.Players:CreateLocalPlayer()
--playerTest.CharacterAppearanceLoaded:Connect()
function botService:CreateBot(Params: BotParams): Bot
local self = setmetatable({}, Bot_mt)
self.Character = nil; --// For the Loading Generated Character
self.Description = nil; --// For having the Character Loaded
self.UserId = GenerateRandomID()
self.CharacterAppearanceId = self.UserId
self.CanLoadCharacterAppearance = true
self.Team = nil
self.TeamColor = BrickColor.new("White")
self.Name = nil
self.DisplayName = nil;
local Success = false
local Tries = 0
while (not Success) and Tries < maxRequestAttempts do
local _, info = getUserInfo(self.UserId)
Success = pcall(function ()
self.Name = info.Username
self.DisplayName = info.DisplayName
if Params.UseRealPlayerDescriptions then
self.Description = game.Players:GetHumanoidDescriptionFromUserId(self.UserId)
end
end)
Tries += 1
if Tries % 4 == 0 and (not Success) then
self.UserId = GenerateRandomID(); self.CharacterAppearanceId = self.UserId
wait(0.25)
end
end
self.CharacterAdded = savedModules.Signal.new()
self.CharacterAppearanceLoaded = savedModules.Signal.new()
self.Chatted = savedModules.Signal.new()
self.Team = GetRandomAvailableTeam()
if self.Team then
self.TeamColor = self.Team.TeamColor
end
--// Params
self.RigType = Params.RigType --// For knowing what type of rig the character is
self.Gender = Params.GenderType
self.GenderData = Params.Genders[self.Gender]
self.Accessories = self.GenderData["Accessories"]
self.Clothing = self.GenderData.Clothing
self.Bundles = self.GenderData["Bundles"]
self.BodyColors = self.GenderData["BodyColors"]
self.Heads = self.GenderData["Heads"]
return self
end
function Bot.LoadCharacter(self: Bot)
if self.Character then
self.Character:Destroy()
self.Character = nil
end
local NewCharacter
if self.RigType == "R15" then
NewCharacter = rigTypes:WaitForChild("R15"):Clone()
else
NewCharacter = rigTypes:WaitForChild("R6"):Clone()
end
local Scripts = {}
for _, Descendant in pairs(NewCharacter:GetDescendants()) do
if Descendant:IsA("Script") then
table.insert(Scripts, {Descendant, Descendant.Disabled})
Descendant.Disabled = true
end
end
NewCharacter.Parent = script
NewCharacter.Name = self.Name
NewCharacter.Humanoid.DisplayName = self.DisplayName
if self.CanLoadCharacterAppearance then
if self.Description then
NewCharacter.Humanoid:ApplyDescription(self.Description)
self.CharacterAppearanceLoaded:Fire(NewCharacter)
else
local Avatar = savedModules.Avatar.new(NewCharacter.Humanoid)
local accessoryArray = {}
local clothingArray = {
["Classic"] = {};
["Layered"] = {};
}
local accessoriesData = self.Accessories
if accessoriesData then
for n, v in accessoriesData do
if not accessoryArray[n] then
if n ~= "HatAccessory" then
accessoryArray[n] = savedModules.Generator:Random(v, 1)
else
accessoryArray[n] = savedModules.Generator:Random(v, 2)
end
end
end
end
print("Accessories:", accessoryArray)
for _, v in accessoryArray do
Avatar:Wear(v)
end
local clothingData = self.Clothing
if clothingData then
local classicData = clothingData["Classic"]
if classicData then
for n, v in classicData do
if not clothingArray["Classic"][n] then
clothingArray["Classic"][n] = savedModules.Generator:Random(v, 1)
end
end
for n, v in clothingArray["Classic"] do
Avatar:Wear(v)
end
end
end
if self.Bundles then
local bundle = self.Bundles[math.random(1,#self.Bundles)]
local bundleData = getBodyPartsFromBundle(bundle)
for _, data in pairs(bundleData) do
print("Valid:",checkValidAsset(data.AssetId))
Avatar:BodyPart({data.AssetId})
end
end
if self.Heads then
local head = self.Heads[math.random(1,#self.Heads)]
Avatar:BodyPart({head})
end
if self.BodyColors then
local bodyColor = self.BodyColors[math.random(1,#self.BodyColors)]
Avatar:Paint(bodyColor)
end
self.Description = Avatar.Description
Avatar:Apply()
self.CharacterAppearanceLoaded:Fire(NewCharacter)
end
end
NewCharacter.Parent = workspace
self.CharacterAdded:Fire(NewCharacter)
local NextSpawn = GetNextSpawn(self.Team)
--either SpawnLocation or Vector3
if typeof(NextSpawn) == "Vector3" then
local SpawnPos = NextSpawn
NewCharacter:SetPrimaryPartCFrame(CFrame.new(NewCharacter:GetPrimaryPartCFrame().p))
NewCharacter:MoveTo(SpawnPos)
else
local SpawnPos = NextSpawn.Position
local SpawnRot = NextSpawn.Rotation
NewCharacter:SetPrimaryPartCFrame(CFrame.new(NewCharacter:GetPrimaryPartCFrame().p) * CFrame.Angles(math.rad(SpawnRot.x),math.rad(SpawnRot.y),math.rad(SpawnRot.z)))
NewCharacter:MoveTo(SpawnPos)
local ForceField = Instance.new("ForceField")
ForceField.Parent = NewCharacter
game.Debris:AddItem(ForceField, NextSpawn.Duration)
end
NewCharacter.HumanoidRootPart:SetNetworkOwner(nil)
NewCharacter.Humanoid.Touched:Connect(function(Hit)
if Hit:IsA("SpawnLocation") and self.Team ~= GetTeamFromBrickColor(Hit.TeamColor) and Hit.AllowTeamChangeOnTouch == true then
self.Team = GetTeamFromBrickColor(Hit.TeamColor)
end
end)
self.Character = NewCharacter
NewCharacter.Humanoid.Died:Connect(function() task.delay(game.Players.RespawnTime, function() self:LoadCharacter() end) end)
for _, Script in pairs(Scripts) do
Script[1].Disabled = Script[2]
end
end
return botService
Modules I used:
Character Generator (Generator) Module
Signal:
-- -----------------------------------------------------------------------------
-- Batched Yield-Safe Signal Implementation --
-- This is a Signal class which has effectively identical behavior to a --
-- normal RBXScriptSignal, with the only difference being a couple extra --
-- stack frames at the bottom of the stack trace when an error is thrown. --
-- This implementation caches runner coroutines, so the ability to yield in --
-- the signal handlers comes at minimal extra cost over a naive signal --
-- implementation that either always or never spawns a thread. --
-- --
-- API: --
-- local Signal = require(THIS MODULE) --
-- local sig = Signal.new() --
-- local connection = sig:Connect(function(arg1, arg2, ...) ... end) --
-- sig:Fire(arg1, arg2, ...) --
-- connection:Disconnect() --
-- sig:DisconnectAll() --
-- local arg1, arg2, ... = sig:Wait() --
-- --
-- Licence: --
-- Licenced under the MIT licence. --
-- --
-- Authors: --
-- stravant - July 31st, 2021 - Created the file. --
-- sleitnick - August 3rd, 2021 - Modified for Knit. --
-- -----------------------------------------------------------------------------
-- The currently idle thread to run the next handler on
local freeRunnerThread = nil
-- Function which acquires the currently idle handler runner thread, runs the
-- function fn on it, and then releases the thread, returning it to being the
-- currently idle one.
-- If there was a currently idle runner thread already, that's okay, that old
-- one will just get thrown and eventually GCed.
local function acquireRunnerThreadAndCallEventHandler(fn, ...)
local acquiredRunnerThread = freeRunnerThread
freeRunnerThread = nil
fn(...)
-- The handler finished running, this runner thread is free again.
freeRunnerThread = acquiredRunnerThread
end
-- Coroutine runner that we create coroutines of. The coroutine can be
-- repeatedly resumed with functions to run followed by the argument to run
-- them with.
local function runEventHandlerInFreeThread(...)
acquireRunnerThreadAndCallEventHandler(...)
while true do
acquireRunnerThreadAndCallEventHandler(coroutine.yield())
end
end
-- Connection class
local Connection = {}
Connection.__index = Connection
function Connection.new(signal, fn)
return setmetatable({
_connected = true,
_signal = signal,
_fn = fn,
_next = false,
}, Connection)
end
function Connection:Disconnect()
if not self._connected then return end
self._connected = false
-- Unhook the node, but DON'T clear it. That way any fire calls that are
-- currently sitting on this node will be able to iterate forwards off of
-- it, but any subsequent fire calls will not hit it, and it will be GCed
-- when no more fire calls are sitting on it.
if self._signal._handlerListHead == self then
self._signal._handlerListHead = self._next
else
local prev = self._signal._handlerListHead
while prev and prev._next ~= self do
prev = prev._next
end
if prev then
prev._next = self._next
end
end
end
Connection.Destroy = Connection.Disconnect
-- Make Connection strict
setmetatable(Connection, {
__index = function(_tb, key)
error(("Attempt to get Connection::%s (not a valid member)"):format(tostring(key)), 2)
end,
__newindex = function(_tb, key, _value)
error(("Attempt to set Connection::%s (not a valid member)"):format(tostring(key)), 2)
end
})
--[=[
@class Signal
Signals allow events to be dispatched and handled.
For example:
```lua
local signal = Signal.new()
signal:Connect(function(msg)
print("Got message:", msg)
end)
signal:Fire("Hello world!")
```
]=]
local Signal = {}
Signal.__index = Signal
--[=[
Constructs a new Signal
@return Signal
]=]
function Signal.new()
local self = setmetatable({
_handlerListHead = false,
_proxyHandler = nil,
}, Signal)
return self
end
--[=[
Constructs a new Signal that wraps around an RBXScriptSignal.
@param rbxScriptSignal RBXScriptSignal -- Existing RBXScriptSignal to wrap
@return Signal
For example:
```lua
local signal = Signal.Wrap(workspace.ChildAdded)
signal:Connect(function(part) print(part.Name .. " added") end)
Instance.new("Part").Parent = workspace
```
]=]
function Signal.Wrap(rbxScriptSignal)
assert(typeof(rbxScriptSignal) == "RBXScriptSignal", "Argument #1 to Signal.Wrap must be a RBXScriptSignal; got " .. typeof(rbxScriptSignal))
local signal = Signal.new()
signal._proxyHandler = rbxScriptSignal:Connect(function(...)
signal:Fire(...)
end)
return signal
end
--[=[
Checks if the given object is a Signal.
@param obj any -- Object to check
@return boolean -- `true` if the object is a Signal.
]=]
function Signal.Is(obj)
return type(obj) == "table" and getmetatable(obj) == Signal
end
--[=[
Connects a function to the signal, which will be called anytime the signal is fired.
@param fn (...any) -> nil
@return Connection -- A connection to the signal
]=]
function Signal:Connect(fn)
local connection = Connection.new(self, fn)
if self._handlerListHead then
connection._next = self._handlerListHead
self._handlerListHead = connection
else
self._handlerListHead = connection
end
return connection
end
function Signal:GetConnections()
local items = {}
local item = self._handlerListHead
while item do
table.insert(items, item)
item = item._next
end
return items
end
-- Disconnect all handlers. Since we use a linked list it suffices to clear the
-- reference to the head handler.
--[=[
Disconnects all connections from the signal.
]=]
function Signal:DisconnectAll()
self._handlerListHead = false
end
-- Signal:Fire(...) implemented by running the handler functions on the
-- coRunnerThread, and any time the resulting thread yielded without returning
-- to us, that means that it yielded to the Roblox scheduler and has been taken
-- over by Roblox scheduling, meaning we have to make a new coroutine runner.
--[=[
Fire the signal, which will call all of the connected functions with the given arguments.
@param ... any -- Arguments to pass to the connected functions
]=]
function Signal:Fire(...)
local item = self._handlerListHead
while item do
if item._connected then
if not freeRunnerThread then
freeRunnerThread = coroutine.create(runEventHandlerInFreeThread)
end
task.spawn(freeRunnerThread, item._fn, ...)
end
item = item._next
end
end
--[=[
Same as `Fire`, but uses `task.defer` internally & doesn't take advantage of thread reuse.
@param ... any -- Arguments to pass to the connected functions
]=]
function Signal:FireDeferred(...)
local item = self._handlerListHead
while item do
task.defer(item._fn, ...)
item = item._next
end
end
--[=[
Yields the current thread until the signal is fired, and returns the arguments fired from the signal.
@return ... any -- Arguments passed to the signal when it was fired
@yields
]=]
function Signal:Wait()
local waitingCoroutine = coroutine.running()
local cn
cn = self:Connect(function(...)
cn:Disconnect()
task.spawn(waitingCoroutine, ...)
end)
return coroutine.yield()
end
--[=[
Cleans up the signal.
]=]
function Signal:Destroy()
self:DisconnectAll()
local proxyHandler = rawget(self, "_proxyHandler")
if proxyHandler then
proxyHandler:Disconnect()
end
end
-- Make signal strict
setmetatable(Signal, {
__index = function(_tb, key)
error(("Attempt to get Signal::%s (not a valid member)"):format(tostring(key)), 2)
end,
__newindex = function(_tb, key, _value)
error(("Attempt to set Signal::%s (not a valid member)"):format(tostring(key)), 2)
end
})
return Signal
Please feel free if you ask any questions.