Skip to content

Instantly share code, notes, and snippets.

@michlbro
Created August 2, 2023 23:42
Show Gist options
  • Save michlbro/1e270fa4140414375b8be28204af5a03 to your computer and use it in GitHub Desktop.
Save michlbro/1e270fa4140414375b8be28204af5a03 to your computer and use it in GitHub Desktop.
My custom module initialiser made for Roblox. Helps to make scripting easier by providing a framework for scripts.
--[=[
Module initialiser created by michlbro
USAGE:
-- Table: module
local module = {}
-- initialiser binds the globalenvironment (Core), Print Debug (Print), tool (tool), AddClass (AddClass), GetClass (GetClass)
function module.Init() -- Initialise
module.AddClass(name, class)
module.Core.NewEvent = module.tool.Signal.new()
end
function module.Start() -- Upon Start
local success = module.GetClass(name)
end
-- ModuleScript: Modules
tool:
Signal - Signal library
Javelin - Group signal library
return function(tool)
GlobalEnvironment = {
Players = {}
Event = core.Signal.new()
},
Classes = {},
Modules = {
{
Table: module,
debuggingProperties: {
PCall = true
PrintDebug = true
}
}
}
GlobalDebugging = {
PCall = true,
PrintDebug = true
}
end
-- Script: CoreScript
local Initialiser = require(ModuleInitialiser)
local initialiser = Initialiser.new(require(Modules))
initialiser:Init()
initialiser:Start()
]=]
-- // DEPENDENICES \\ --
--------------------------------------------------------------------------------
-- 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. --
--------------------------------------------------------------------------------
local SignalClass
do
-- 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()
-- Note: We cannot use the initial set of arguments passed to
-- runEventHandlerInFreeThread for a call to the handler, because those
-- arguments would stay on the stack for the duration of the thread's
-- existence, temporarily leaking references. Without access to raw bytecode
-- there's no way for us to clear the "..." references from the stack.
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()
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)
-- Get the freeRunnerThread to the first yield
coroutine.resume(freeRunnerThread)
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
-- Implement Signal:Once() in terms of a connection which disconnects
-- itself before running the handler.
function Signal:Once(fn)
local cn;
cn = self:Connect(function(...)
if cn._connected then
cn:Disconnect()
end
fn(...)
end)
return cn
end
-- Make signal strict
SignalClass = 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
})
end
--------------------------------------------------------------------------------
-- // END OF DEPENDENCIES \\ --
export type DebuggingOptions = {
PCall: boolean,
PrintDebug: boolean,
Enabled: boolean
}
export type ModuleConfig = {
GlobalEnvironment: {
[any]: any
},
Modules: {},
Classes: {[string]: any},
GlobalDebugging: DebuggingOptions
}
export type InitialiserTools = {
Signal: {new: () -> RBXScriptSignal & {Fire: (...any) -> nil}}
}
local Tools = {
Signal = SignalClass
}
local ModuleInitialiser = {}
function ModuleInitialiser:Start()
self.StartSignal:Fire()
end
function ModuleInitialiser:_BindModule(tbl, debugging, globalDebug)
if (globalDebug.Enabled == false) or (debugging.Enabled == false) then
return
end
local environment = {
Core = self._ModuleConfig.GlobalEnvironment,
Print = function(...)
if (globalDebug.PrintDebug and globalDebug.PrintDebug == true) or (debugging.PrintDebug and debugging.PrintDebug == true) then
print(...)
end
end,
AddClass = function(name, class)
if self.Modules[name] then
warn(`WARNING: Class {name} already exists in list of classes. This be overridden.`)
end
self.Modules[name] = class
end,
GetClass = function(name)
return self.Modules[name]
end,
Tool = Tools
}
setmetatable(tbl, {
__index = environment
})
if (globalDebug.PCall and globalDebug.PCall == true) or (debugging.PCall and debugging.PCall == true) then
pcall(tbl.Init)
self.StartSignal:Once(function()
pcall(tbl.Start)
end)
else
tbl.Init()
self.StartSignal:Once(function()
tbl.Start()
end)
end
end
local function new(ModuleConfigCallbackFunction: (InitialiserTools) -> ModuleConfig)
local self = {}
self._ModuleConfig = ModuleConfigCallbackFunction(Tools)
self.Modules = {}
self.Classes = self._ModuleConfig.Classes
self.StartSignal = SignalClass.new()
setmetatable(self, {
__index = ModuleInitialiser
})
for _, module in self._ModuleConfig.Modules do
self:_BindModule(module[1], module[2], self._ModuleConfig.GlobalDebugging)
end
return self
end
return table.freeze(setmetatable({
new = new
}, {}))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment