Last active
February 24, 2024 20:44
-
-
Save Corecii/76e742f7d5fb5095fe14bb3c1a55d4e2 to your computer and use it in GitHub Desktop.
Pcall with traceback for Roblox
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
Use: | |
On success: | |
local success, return1, return2, ... = tpcall(func, arg1, arg2, ...) | |
On error: | |
local success, error, traceback = tpcall(func, arg1, arg2, ...) | |
--]] | |
--[[ runner.lua: belongs inside tpcall.lua | |
return setmetatable({}, { | |
__call = function(_, func, ...) | |
return func(...) | |
end | |
}) | |
--]] | |
local PREGENERATE_RUNNERS_COUNT = 10 | |
local runnerBase = script.runner | |
local runnersFree = {} | |
local runnersById = {} | |
local runnersByCoroutine = {} | |
local count = 0 | |
local function getFreeRunner(running) | |
if runnersByCoroutine[running] then | |
return runnersByCoroutine[running] | |
else | |
local freeRunner = next(runnersFree) | |
if freeRunner then | |
return freeRunner | |
else | |
count = count + 1 | |
local uniqueId = "tpcall:"..count | |
runnerBase.Name = uniqueId | |
local newRunner = require(runnerBase:Clone()) | |
newRunner.id = uniqueId | |
runnersById[uniqueId] = newRunner | |
return newRunner | |
end | |
end | |
end | |
for i = 1, PREGENERATE_RUNNERS_COUNT do | |
count = count + 1 | |
local uniqueId = "tpcall:"..count | |
runnerBase.Name = uniqueId | |
local newRunner = require(runnerBase:Clone()) | |
newRunner.id = uniqueId | |
runnersById[uniqueId] = newRunner | |
runnersFree[newRunner] = true | |
end | |
local lastErrorId, lastError, lastTrace | |
game:GetService("ScriptContext").Error:Connect(function(err, trace, scr) | |
local tpcallId = trace:match("(tpcall:%d+)") | |
if tpcallId then | |
-- for some reason, the error message includes the script and | |
-- line number. The script can have any name, and the error is | |
-- arbitrary, so we can't use pattern matching on the error. | |
-- We *can* use it on the trace though, which includes the script | |
-- name and error. Using this, we can find length counts and get | |
-- a substring of the actual error! | |
-- This approach isn't perfect. It relies on the ", line %d+ -" format. | |
-- If for some reason your script has a name like that then it will | |
-- produce improper output | |
local scriptName, errorLine = trace:match("^(.-), line (%d+) %- [^\n]*\n") | |
if scriptName then | |
if err:sub(1, #scriptName) == scriptName then | |
lastError = err:sub(#scriptName + #errorLine + 4) | |
else | |
lastError = err | |
end | |
else | |
lastError = err:match("^"..tpcallId..":%d+: (.*)$") or err | |
end | |
lastErrorId = tpcallId | |
lastTrace = trace:match("^(.*)\n[^\n]+\n[^\n]+\n$") or "" | |
runnersById[tpcallId].event:Fire() | |
end | |
end) | |
return function(func, ...) | |
local runner = getFreeRunner(coroutine.running()) | |
local initialCoroutine, initialEvent = runner.coroutine, runner.event | |
-- We have been given the runner *for our coroutine*. | |
-- This means it's "stacked" on top of another coroutine and event. | |
-- (and we are *guaranteed* to finish first, since we are | |
-- stacked on top of the coroutine that called this tpcall) | |
-- In fact, initialCoroutine and coroutine.running() should be | |
-- the same! We only switch to a "new" coroutine when we move | |
-- into the BindableEvent, but it's guaranteed that the | |
-- BindableEvent coroutine finishes before we continue. We use | |
-- BindableEvents to yield until that new coroutine is finished. | |
-- The one exception: when this call is at the top of the tpcall | |
-- "stack". In that scenario, initialCoroutine and initialEvent | |
-- are nil. This is how tpcall knows when a runner is free again: | |
-- it's free when the initialCoroutine and initialEvent are nil. | |
-- If there is an error, lastErrorId = runnerId | |
-- If there is no error, results ~= nil | |
local results | |
local args = {...} | |
local event = Instance.new("BindableEvent") | |
runner.event = event | |
local running | |
local conn | |
conn = event.Event:Connect(function() | |
conn:disconnect() | |
running = coroutine.running() | |
runner.coroutine = running | |
runnersByCoroutine[running] = runner | |
results = {runner(func, unpack(args))} | |
event:Fire() | |
end) | |
runnersFree[runner] = nil | |
event:Fire() | |
local runnerId = runner.id | |
if not results and lastErrorId ~= runnerId then | |
event.Event:Wait() | |
end | |
runnersByCoroutine[running] = nil | |
runner.coroutine, runner.event = initialCoroutine, initialEvent | |
if not initialCoroutine then | |
runnersFree[runner] = true | |
end | |
if lastErrorId == runnerId then | |
lastErrorId = nil | |
return false, lastError, lastTrace | |
else | |
return true, unpack(results) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
474545527😂