(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
Playerclass. Hope that’s okay with that. Also the UserId randomzied, including CharacterApperanceId. - Bot:LoadCharacter(self: Bot) → Loading the Bot that can either use
UseRealPlayerDeacriptionWhich will load a real player Id from theself.Descriptionbut 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.
Current Suggestions: 0
Questions: 0
Questions answered: 0