From 125ab31c075e722519565b8add1f5131e2f4d09e Mon Sep 17 00:00:00 2001 From: MohhayScripts <63205995+MohhayScripts@users.noreply.github.com> Date: Wed, 20 Apr 2022 00:10:54 +0200 Subject: [PATCH] Updated SimplePath.lua --- src/SimplePath/Signal.lua | 162 ++++++++++++++++++++ src/{SimplePath.lua => SimplePath/init.lua} | 46 +++--- 2 files changed, 188 insertions(+), 20 deletions(-) create mode 100644 src/SimplePath/Signal.lua rename src/{SimplePath.lua => SimplePath/init.lua} (87%) diff --git a/src/SimplePath/Signal.lua b/src/SimplePath/Signal.lua new file mode 100644 index 0000000..4fa2e99 --- /dev/null +++ b/src/SimplePath/Signal.lua @@ -0,0 +1,162 @@ +-------------------------------------------------------------------------------- +-- 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. -- +-------------------------------------------------------------------------------- + +-- 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() + assert(self._connected, "Can't disconnect a connection twice.", 2) + 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 + +-- 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 +}) + +-- Signal class +local Signal = {} +Signal.__index = Signal + +function Signal.new() + return setmetatable({ + _handlerListHead = false, + }, Signal) +end + +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 + +-- Disconnect all handlers. Since we use a linked list it suffices to clear the +-- reference to the head handler. +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. +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 + +-- Implement Signal:Wait() in terms of a temporary connection using +-- a Signal:Connect() which disconnects itself. +function Signal:Wait() + local waitingCoroutine = coroutine.running() + local cn; + cn = self:Connect(function(...) + cn:Disconnect() + task.spawn(waitingCoroutine, ...) + end) + return coroutine.yield() +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 \ No newline at end of file diff --git a/src/SimplePath.lua b/src/SimplePath/init.lua similarity index 87% rename from src/SimplePath.lua rename to src/SimplePath/init.lua index 1d09866..510f742 100644 --- a/src/SimplePath.lua +++ b/src/SimplePath/init.lua @@ -21,9 +21,13 @@ local DEFAULT_SETTINGS = { local PathfindingService = game:GetService("PathfindingService") local Players = game:GetService("Players") + +local Signal = require(script.Signal) + local function output(func, msg) - func(((func == error and "SimplePath Error: ") or "SimplePath: ")..msg) + func((if func == error then "SimplePath Error: " else "SimplePath: ")..msg) end + local Path = { StatusType = { Idle = "Idle"; @@ -40,10 +44,10 @@ Path.__index = function(table, index) if index == "Stopped" and not table._humanoid then output(error, "Attempt to use Path.Stopped on a non-humanoid.") end - return (table._events[index] and table._events[index].Event) - or (index == "LastError" and table._lastError) - or (index == "Status" and table._status) - or Path[index] + return if table._events[index] then table._events[index] + elseif index == "LastError" then table._lastError + elseif index == "Status" then table._status + else Path[index] end --Used to visualize waypoints @@ -68,9 +72,9 @@ local function createVisualWaypoints(waypoints) visualWaypointClone.Position = waypoint.Position visualWaypointClone.Parent = workspace visualWaypointClone.Color = - (waypoint == waypoints[#waypoints] and Color3.fromRGB(0, 255, 0)) - or (waypoint.Action == Enum.PathWaypointAction.Jump and Color3.fromRGB(255, 0, 0)) - or Color3.fromRGB(255, 139, 0) + if waypoint == waypoints[#waypoints] then Color3.fromRGB(0, 255, 0) + elseif waypoint.Action == Enum.PathWaypointAction.Jump then Color3.fromRGB(255, 0, 0) + else Color3.fromRGB(255, 139, 0) table.insert(visualWaypoints, visualWaypointClone) end return visualWaypoints @@ -199,13 +203,13 @@ function Path.new(agent, agentParameters, override) end local self = setmetatable({ - _settings = override or DEFAULT_SETTINGS; + _settings = if override then override else DEFAULT_SETTINGS; _events = { - Reached = Instance.new("BindableEvent"); - WaypointReached = Instance.new("BindableEvent"); - Blocked = Instance.new("BindableEvent"); - Error = Instance.new("BindableEvent"); - Stopped = Instance.new("BindableEvent"); + Reached = Signal.new(); + WaypointReached = Signal.new(); + Blocked = Signal.new(); + Error = Signal.new(); + Stopped = Signal.new(); }; _agent = agent; _humanoid = agent:FindFirstChildOfClass("Humanoid"); @@ -220,7 +224,7 @@ function Path.new(agent, agentParameters, override) --Configure settings for setting, value in pairs(DEFAULT_SETTINGS) do - self._settings[setting] = self._settings[setting] == nil and value or self._settings[setting] + self._settings[setting] = if self._settings[setting] == nil then value else self._settings[setting] end --Path blocked connection @@ -238,7 +242,7 @@ end --[[ NON-STATIC METHODS ]]-- function Path:Destroy() for _, event in ipairs(self._events) do - event:Destroy() + event:Disconnect() end self._events = nil if rawget(self, "_visualWaypoints") then @@ -277,7 +281,7 @@ function Path:Run(target) end --Parameter check - if not (target and (typeof(target) == "Vector3" or target:IsA("BasePart"))) then + if not (target and (if typeof(target) == "Vector3" then typeof(target) == "Vector3" else target:IsA("BasePart"))) then output(error, "Pathfinding target must be a valid Vector3 or BasePart.") end @@ -291,8 +295,9 @@ function Path:Run(target) end --Compute path + local pathValue = typeof(target) == "Vector3" and target local pathComputed, _ = pcall(function() - self._path:ComputeAsync(self._agent.PrimaryPart.Position, (typeof(target) == "Vector3" and target) or target.Position) + self._path:ComputeAsync(self._agent.PrimaryPart.Position, if pathValue then pathValue else target.Position) end) --Make sure path computation is successful @@ -307,7 +312,8 @@ function Path:Run(target) end --Set status to active; pathfinding starts - self._status = (self._humanoid and Path.StatusType.Active) or Path.StatusType.Idle + local statusValue = self._humanoid and Path.StatusType.Active + self._status = if statusValue then statusValue else Path.StatusType.Idle self._target = target --Set network owner to server to prevent "hops" @@ -329,7 +335,7 @@ function Path:Run(target) self._visualWaypoints = (self.Visualize and createVisualWaypoints(self._waypoints)) --Create a new move connection if it doesn't exist already - self._moveConnection = self._humanoid and (self._moveConnection or self._humanoid.MoveToFinished:Connect(function(...) + self._moveConnection = self._humanoid and (if self._moveConnection then self._moveConnection else self._humanoid.MoveToFinished:Connect(function(...) moveToFinished(self, ...) end))