BotService (Module) [BETA]

(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:

  1. 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)

  1. 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.
  2. Bot:LoadCharacter(self: Bot) → Loading the Bot that can either use UseRealPlayerDeacription Which will load a real player Id from the self.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.

3 Likes

Please not these use. It uses NPCs.

This is just an npc, it’s cool but not really relevant to the feature suggestion linked

The point of the whole bot service feature request was to able to spoof actual players/clients as bots for situations where simple npc stuff like this doesn’t cut it

3 Likes

aren’t we all (kind of like) NPCs?

you can create and control. The NPCs. At least you can create, control and edit the NPC/NPCs that are loaded in.

The point of tyridge’s feature request is to be able to create actual fake players. Players that can listen to remotes and interact with the world, just like a real player.

Zomebody explains best why an official implementation is necessary:

Your module is a NPC spawner utility, which has different use cases.

3 Likes

Which I am trying to recreate its not just an NPC Spawner. I had that planned to basically get to that point at sometime. Including with maybe other things that are custom functions including adding new functions, etc… I do want requests to import into the module.

(RemoteEvents and RemoteFunctions for my BotService module might be difficult but I don’t really care I’ll try and figure it out), and I understand

create actual fake players. Players that can listen to remotes and interact with the world, just like a real player.

I also mentioned in the title it is in Beta.

[UPDATE]
v0.0.1

Added:
Updated BotParams. Added Faces, and Animations.
Added function Bot.Chat(self: Bot, message: string)
Chat function includes.
Whispering and Normally Chatting.
Added Move function.

Code will release sometime.