Skip to content

Instantly share code, notes, and snippets.

@Corecii
Last active February 24, 2024 20:44
Show Gist options
  • Save Corecii/76e742f7d5fb5095fe14bb3c1a55d4e2 to your computer and use it in GitHub Desktop.
Save Corecii/76e742f7d5fb5095fe14bb3c1a55d4e2 to your computer and use it in GitHub Desktop.
Pcall with traceback for Roblox
--[[
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
@hacker2008hama
Copy link

474545527😂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment