Skip to content

Instantly share code, notes, and snippets.

@Anaminus
Created November 4, 2018 09:00
Show Gist options
  • Select an option

  • Save Anaminus/1f64f3dbc0ec8ed49350298d178ccfb5 to your computer and use it in GitHub Desktop.

Select an option

Save Anaminus/1f64f3dbc0ec8ed49350298d178ccfb5 to your computer and use it in GitHub Desktop.
--[[
Core
Contains core functions to be used by the rest of the framework.
]]
local Core = {}
local stringFormat = string.format
local function Pack(...)
return {n = select("#", ...), ...}
end
Core.Pack = Pack
local function countFormatSpec(s)
local n = 0
local i = 0
while i <= #s do
local c = s:sub(i,i)
if c == "%" and i < #s then
n = n + 1
if s:sub(i+1,i+1) == "%" then
i = i + 1
end
end
i = i + 1
end
return n
end
local function Printf(...)
local ok, result = pcall(stringFormat, ...)
if not ok then
error(result, 2)
end
local level = select(countFormatSpec((...))+2, ...) or 1
if type(level) ~= "number" then
error(stringFormat("level argument must be a number (got %s)", type(level)), 2)
end
print(result)
end
local function Warnf(...)
local ok, result = pcall(stringFormat, ...)
if not ok then
error(result, 2)
end
local level = select(countFormatSpec((...))+2, ...) or 1
if type(level) ~= "number" then
error(stringFormat("level argument must be a number (got %s)", type(level)), 2)
end
warn(result)
end
local function Errorf(...)
local ok, result = pcall(stringFormat, ...)
if not ok then
error(result, 2)
end
local level = select(countFormatSpec((...))+2, ...) or 1
if type(level) ~= "number" then
error(stringFormat("level argument must be a number (got %s)", type(level)), 2)
end
error(result, level + 1)
end
Core.Printf = Printf
Core.Warnf = Warnf
Core.Errorf = Errorf
local LuaTypeNS = "lua:"
local RobloxTypeNS = "rbx:"
local ClassTypeNS = "class:"
local ClassOfTypeNS = "classof:"
local EnumTypeNS = "enum:"
-- Throws a "bad argument" error.
local function badArgument(arg, fn, msg, level)
local err = {"bad argument", nil, nil, nil}
if arg then
err[#err+1] = stringFormat(" #%d", arg)
end
if fn then
err[#err+1] = stringFormat(" to %s", fn)
end
if msg then
err[#err+1] = stringFormat(": %s", msg)
end
Errorf(table.concat(err), (level or 2) + 1)
end
-- Returns a message indicating an unexpected type.
local function BadType(got, ...)
local nargs = select("#", ...)
local args = {...}
local types = {}
if nargs > 0 then
for i = 1, nargs do
local t = args[i]
if typeof(t) == "Enum" then
types[i] = EnumTypeNS .. tostring(t)
else
types[i] = tostring(t)
end
end
if #types > 1 then
types[#types] = "or " .. types[#types]
end
else
types[1] = "value"
end
if typeof(got) == "Enum" then
got = EnumTypeNS .. tostring(got)
else
got = tostring(got)
end
return stringFormat("%s expected, got %s", table.concat(types, #types == 2 and " " or ", "), got)
end
Core.BadType = BadType
-- Throws a "bad argument" error indicating an unexpected type.
local function badArgumentType(arg, fn, got, ...)
badArgument(arg, fn, BadType(got, ...), 2+1)
end
local function badArgumentTypeLevel(level, arg, fn, got, ...)
badArgument(arg, fn, BadType(got, ...), level+1)
end
--[[$
function checkarg_arg(arg, fn)
if not CheckArgs then return end
_put(string.format([=[
if arg ~= nil and type(arg) ~= "number" then
badArgumentType(%d, %q, type(arg), "number", nil)
end
]=], arg, fn))
end
function checkarg_fn(arg, fn)
if not CheckArgs then return end
_put(string.format([=[
if fn ~= nil and type(fn) ~= "string" then
badArgumentType(%d, %q, type(fn), "string", nil)
end
]=], arg, fn))
end
function checkarg_vararg(arg, fn)
if not CheckArgs then return end
_put(string.format([=[
if select("#", ...) == 0 then
badArgument(%d, %q, "value expected")
end
]=], arg, fn))
end
function checkarg_value(arg, fn)
if not CheckArgs then return end
end
function checkarg_level(arg, fn)
if not CheckArgs then return end
_put(string.format([=[
if type(level) ~= "number" then
badArgumentType(%d, %q, type(level), "number")
end
]=], arg, fn))
end
function checkarg_msg(arg, fn)
if not CheckArgs then return end
_put(string.format([=[
if msg ~= nil and type(msg) ~= "string" and type(msg) ~= "function" then
badArgumentType(%d, %q, type(msg), "string", nil)
end
]=], arg, fn))
end
]]
-- Type returns the type of a value. First it tries MetaType. If that returns
-- nil, then it tries LuaType. If that returns a userdata, then it returns the
-- result of RobloxType.
local function Type(...)
--# checkarg_vararg (1, "Type")
local value = ...
if value == nil then
return nil
end
-- Check __type.
local mt = getmetatable(value)
if type(mt) == "table" then
local t = mt.__type
if type(t) == "function" then
t = t(value)
end
if t ~= nil then
return tostring(t)
end
end
-- Check type().
local t = type(value)
if t ~= "userdata" then
return LuaTypeNS .. t
end
-- Check typeof().
local t = typeof(value)
if t == "Instance" then
return ClassTypeNS .. value.ClassName
elseif t == "EnumItem" then
return value.EnumType
elseif t == "userdata" then
return LuaTypeNS .. t
end
return RobloxTypeNS .. t
end
-- MetaType returns the type of a value from the value's "__type" metamethod,
-- converted to a string. If __type is a function, then the result of the
-- function is used. Returns nil if the type could not be found.
local function MetaType(...)
--# checkarg_vararg (1, "MetaType")
local value = ...
local mt = getmetatable(value)
if type(mt) == "table" then
local t = mt.__type
if type(t) == "function" then
t = t(value)
end
if t ~= nil then
return tostring(t)
end
end
return nil
end
Core.Type = Type
Core.MetaType = MetaType
-- IsType returns whether a value is of one or more types.
local function IsType(value, ...)
local nargs = select("#", ...)
if nargs == 0 then
badArgument(2, "IsType", "value expected")
end
local args = {...}
for i = 1, nargs do
local t = args[i]
if t == nil then
if value == nil then
return true, nil
end
elseif type(t) == "string" then
local ns, typ = t:match("^(.*:)(.*)$")
if ns == nil then
if MetaType(value) == t then
return true, t
end
elseif ns == LuaTypeNS then
if type(value) == typ then
return true, t
end
elseif ns == RobloxTypeNS then
if typeof(value) == typ then
return true, t
end
elseif ns == ClassTypeNS then
if typeof(value) == "Instance" and value.ClassName == typ then
return true, t
end
elseif ns == ClassOfTypeNS then
if typeof(value) == "Instance" and value:IsA(typ) then
return true, t
end
else
badArgument(i+1, "IsType", stringFormat("invalid type namespace %q", ns))
end
elseif typeof(t) == "Enum" then
if typeof(value) == "EnumItem" and value.EnumType == t then
return true, t
end
else
badArgumentType(i+1, "IsType", Type(t), "string", "Enum", "nil")
end
end
return false
end
Core.IsType = IsType
local function ArgType(arg, fn, value, ...)
--#if CheckArgs then
--# checkarg_arg (1, "ArgType")
--# checkarg_fn (2, "ArgType")
--# checkarg_value (3, "ArgType")
if select("#", ...) == 0 then
if value == nil then
badArgumentTypeLevel(2+1, arg, fn, nil)
end
return
end
local ok, t = IsType(value, ...)
if ok then
return t
end
badArgumentTypeLevel(2+1, arg, fn, Type(value), ...)
--#end
end
local function ArgTypeLevel(level, arg, fn, value, ...)
--#if CheckArgs then
--# checkarg_level (1, "ArgTypeLevel")
--# checkarg_arg (2, "ArgTypeLevel")
--# checkarg_fn (3, "ArgTypeLevel")
--# checkarg_value (4, "ArgTypeLevel")
if select("#", ...) == 0 then
if value == nil then
badArgumentTypeLevel(level+1, arg, fn, nil)
end
return
end
local ok, t = IsType(value, ...)
if ok then
return t
end
badArgumentTypeLevel(level+1, arg, fn, Type(value), ...)
--#end
end
Core.ArgType = ArgType
Core.ArgTypeLevel = ArgTypeLevel
local function AssertType(msg, value, ...)
--# checkarg_msg (1, "AssertType")
--# checkarg_value (2, "AssertType")
local err
if select("#", ...) == 0 then
if value ~= nil then
return
end
err = BadType(nil)
else
local ok, t = IsType(value, ...)
if ok then
return t
end
err = BadType(Type(value), ...)
end
if type(msg) == "function" then
msg = tostring(msg())
end
if msg then
err = stringFormat("%s: %s", msg, err)
end
Errorf(err, 2)
end
local function AssertTypeLevel(level, msg, value, ...)
--# checkarg_level (1, "AssertTypeLevel")
--# checkarg_msg (2, "AssertTypeLevel")
--# checkarg_value (3, "AssertTypeLevel")
local err
if select("#", ...) == 0 then
if value ~= nil then
return
end
err = BadType(nil)
else
local ok, t = IsType(value, ...)
if ok then
return t
end
err = BadType(Type(value), ...)
end
if type(msg) == "function" then
msg = tostring(msg())
end
if msg then
err = stringFormat("%s: %s", msg, err)
end
Errorf(err, level+1)
end
Core.AssertType = AssertType
Core.AssertTypeLevel = AssertTypeLevel
local mtWeakK = {__mode = "k"}
local mtWeakV = {__mode = "v"}
local mtWeakKV = {__mode = "kv"}
-- WeakTable creates a weak table with the given mode. Tables created with
-- this function that have the same mode will share the same metatable.
function Core.WeakTable(mode, t)
--#if CheckArgs then
ArgType(1, "WeakTable", mode, "lua:string")
ArgType(2, "WeakTable", t, nil, "lua:table")
--#end
local k = mode:lower():match("k")
local v = mode:lower():match("v")
if k and v then
return setmetatable(t or {}, mtWeakKV)
elseif k then
return setmetatable(t or {}, mtWeakK)
elseif v then
return setmetatable(t or {}, mtWeakV)
end
Errorf("mode must contain 'k' or 'v'", 2)
end
local function condpcall(...)
local ok, cond = pcall(...)
return ok and cond
end
-- FindFirstChild returns the first child of an instance that satisfies a
-- given condition.
local function FindFirstChild(instance, cond)
--#if CheckArgs then
ArgType(1, "FindFirstChild", instance, "classof:Instance")
ArgType(2, "FindFirstChild", cond, "lua:function")
--#end
local children = instance:GetChildren()
for i = 1, #children do
if condpcall(cond, children[i]) then
return children[i]
end
end
return nil
end
-- WaitForChild finds or waits for a child that satisfies a given condition to
-- be added to the given instance. The condition function must not yield.
local function WaitForChild(instance, cond)
--#if CheckArgs then
ArgType(1, "WaitForChild", instance, "classof:Instance")
ArgType(2, "WaitForChild", cond, "lua:function")
--#end
local children = instance:GetChildren()
for i = 1, #children do
if condpcall(cond, children[i]) then
return children[i]
end
end
while true do
local child = instance.ChildAdded:Wait()
if condpcall(cond, child) then
return child
end
end
end
-- FindFirstAncestor returns the first ancestor of an instance that satisfies
-- a given condition.
local function FindFirstAncestor(instance, cond)
--#if CheckArgs then
ArgType(1, "FindFirstAncestor", instance, "classof:Instance")
ArgType(2, "FindFirstAncestor", cond, "lua:function")
--#end
instance = instance.Parent
while instance ~= nil and not condpcall(cond, instance) do
instance = instance.Parent
end
return instance
end
-- GetChildren returns a list of all the children of an instance. An optional
-- condition function filters each child.
local function GetChildren(instance, cond)
--#if CheckArgs then
ArgType(1, "GetChildren", instance, "classof:Instance")
ArgType(2, "GetChildren", cond, nil, "lua:function")
--#end
if not cond then
return instance:GetChildren()
end
local results = {}
local children = instance:GetChildren()
for i = 1, #children do
local child = children[i]
if condpcall(cond, child) then
results[#results+1] = child
end
end
return results
end
local function getDescendants(descendants, parent)
local children = parent:GetChildren()
for i = 1, #children do
local child = children[i]
descendants[#descendants+1] = child
getDescendants(descendants, child)
end
end
local function getDescendantsCond(descendants, parent, cond)
local children = parent:GetChildren()
for i = 1, #children do
local child = children[i]
if condpcall(cond, child) then
descendants[#descendants+1] = child
getDescendants(descendants, child)
end
end
end
-- GetDescendants returns a top-down list of all the descendants of an
-- instance. An optional condition function filters each descendant.
local function GetDescendants(instance, cond)
--#if CheckArgs then
ArgType(1, "GetDescendants", instance, "classof:Instance")
ArgType(2, "GetDescendants", cond, nil, "lua:function")
--#end
local descendants = {}
if not cond then
getDescendants(descendants, instance)
return descendants
end
getDescendantsCond(descendants, instance, cond)
return descendants
end
Core.FindFirstChild = FindFirstChild
Core.WaitForChild = WaitForChild
Core.FindFirstAncestor = FindFirstAncestor
Core.GetChildren = GetChildren
Core.GetDescendants = GetDescendants
local manifest
local findManifest = function(child)
return child.Name == "Manifest" and child:IsA("StringValue")
end
if game:GetService("RunService"):IsServer() then
manifest = {
Core = {},
Client = {},
Server = {},
}
do
local function r(manifest, instance, name, depth)
for _, child in pairs(instance:GetChildren()) do
name[depth] = child.Name
if child:IsA("ModuleScript") then
manifest[table.concat(name, ".")] = true
end
r(manifest, child, name, depth+1)
end
name[depth] = nil
end
local ClientModules = FindFirstChild(game:GetService("ReplicatedStorage"), function(child)
return child.Name == "Modules" and child:IsA("Folder")
end)
if ClientModules == nil then
Errorf("missing Client Modules", 2)
end
local ServerModules = FindFirstChild(game:GetService("ServerScriptService"), function(child)
return child.Name == "Modules" and child:IsA("Folder")
end)
if ServerModules == nil then
Errorf("missing Server Modules", 2)
end
r(manifest.Core, script, {}, 1)
r(manifest.Client, ClientModules, {}, 1)
r(manifest.Server, ServerModules, {}, 1)
end
local replicator = FindFirstChild(script, findManifest)
if not replicator then
replicator = Instance.new("StringValue")
replicator.Name = "Manifest"
replicator.Parent = script
end
local function serialize(modules)
local list = {}
for module in pairs(modules) do
list[#list+1] = module
end
table.sort(list)
return table.concat(list, ",")
end
replicator.Value =
"Core:" .. serialize(manifest.Core) .. ";" ..
"Client:" .. serialize(manifest.Client) .. ";" ..
"Server:" .. serialize(manifest.Server) .. ";"
end
if game:GetService("RunService"):IsClient() then
local replicator = WaitForChild(script, findManifest)
while replicator.Value == "" do
replicator.Changed:Wait()
end
for name, modules in replicator.Value:gmatch("(%w+):(.-);") do
local set = {}
for module in modules:gmatch("[^,]+") do
set[module] = true
end
manifest[name] = set
end
end
local requireStack = {}
function requireStack:push(thread, module)
local stack = self[thread]
if stack == nil then
stack = {}
self[thread] = stack
end
for i = 1, #stack do
if stack[i] == module then
Errorf("cyclic dependency detected at module %s", module:GetFullName(), 4)
end
end
stack[#stack+1] = module
end
function requireStack:pop(thread, module)
local stack = self[thread]
if stack == nil or #stack == 0 then
Errorf("attempt to pop empty stack", 2)
end
if stack[#stack] ~= module then
Errorf("attempt to pop mismatched stack item", 2)
end
stack[#stack] = nil
if #stack == 0 then
self[thread] = nil
end
end
local function resolveRequirePath(parent, path, manifestName)
if manifestName and not manifest[manifestName][path] then
Warnf("attempt to require untracked module %s:%s", manifestName, path, 2)
end
for name in path:gmatch("[^%.]+") do
if parent == game then
local ok, service = pcall(game.GetService, game, name)
if ok and service then
parent = service
end
else
parent = WaitForChild(parent, function(child)
return child.Name == name
end)
end
end
return parent
end
local function requireBase(parent, soft)
local thread = coroutine.running()
requireStack:push(thread, parent)
local results
if soft then
results = Pack(pcall(require, parent))
else
results = Pack(require(parent))
end
requireStack:pop(thread, parent)
return unpack(results, 1, results.n)
end
-- Require requires a module in the game tree using a dot-separated string to
-- refer to the module.
function Core.Require(path, soft)
--#if CheckArgs then
local pathT =
ArgType(1, "Require", path, "lua:string", "classof:ModuleScript")
ArgType(2, "Require", soft, nil, "lua:boolean")
--#end
if pathT == "classof:ModuleScript" then
return requireBase(path, soft)
end
return requireBase(resolveRequirePath(game, path), soft)
end
-- RequireCore requires a core module using a dot-separated string to refer to
-- the module.
function Core.RequireCore(path, soft)
--#if CheckArgs then
ArgType(1, "RequireCore", path, "lua:string")
ArgType(2, "RequireCore", soft, nil, "lua:boolean")
--#end
return requireBase(resolveRequirePath(script, path, "Core"), soft)
end
-- RequireClient requires a module from the client module folder, using a
-- dot-separated string to refer to the module.
function Core.RequireClient(path, soft)
--#if CheckArgs then
ArgType(1, "RequireClient", path, "lua:string")
ArgType(2, "RequireClient", soft, nil, "lua:boolean")
--#end
local Modules = WaitForChild(game:GetService("ReplicatedFirst"), function(child)
return child:IsA("Folder") and child.Name == "Modules"
end)
return requireBase(resolveRequirePath(Modules, path, "Client"), soft)
end
-- RequireServer requires a module from the server module folder, using a
-- dot-separated string to refer to the module.
function Core.RequireServer(path, soft)
--#if CheckArgs then
ArgType(1, "RequireServer", path, "lua:string")
ArgType(2, "RequireServer", soft, nil, "lua:boolean")
--#end
local Modules = WaitForChild(game:GetService("ServerScriptService"), function(child)
return child:IsA("Folder") and child.Name == "Modules"
end)
return requireBase(resolveRequirePath(Modules, path, "Server"), soft)
end
return Core
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment