Created
April 23, 2014 01:40
-
-
Save qaisjp/11200214 to your computer and use it in GitHub Desktop.
MTA Libs: classlib
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
-- Developer: sbx320 | |
-- License: MIT | |
-- Github Repos: https://github.com/sbx320/lua_utils | |
--// classlib | |
--|| @type: Shared | |
--|| @desc: A library providing several tools to enhance OOP with Lua | |
--|| @info: Registers itself into the global namespace | |
--\\ | |
local elementClasses = {} | |
local elementIndex = {} | |
-- Set DEBUG to true to enable some additional checks | |
DEBUG = DEBUG or nil | |
--// new(class, ...) | |
--|| @desc: Creates an instance of 'class' and calls the constructor | |
--|| and all derived_constructors | |
--|| @param: table 'class' - The class which should be instanciated | |
--|| @param: vararg - Parameters passed to the constructor and derived_constructors | |
--|| @return:table - The newly created instance | |
--\\ | |
function new(class, ...) | |
assert(type(class) == "table", "first argument provided to new is not a table") | |
-- DEBUG: Validate that we are not instanciating a class with pure virtual methods | |
if DEBUG then | |
for k, v in pairs(class) do | |
assert(v ~= pure_virtual, "Attempted to instaciate a class with an unimplemented pure virtual method ("..tostring(k)..")") | |
end | |
end | |
local instance = setmetatable( { }, | |
{ | |
__index = class; | |
__super = { class }; | |
__newindex = class.__newindex; | |
__call = class.__call; | |
__len = class.__len; | |
__unm = class.__unm; | |
__add = class.__add; | |
__sub = class.__sub; | |
__mul = class.__mul; | |
__div = class.__div; | |
__pow = class.__pow; | |
__concat = class.__concat; | |
}) | |
-- Call derived constructors | |
-- Weird Lua behaviour requires forwarding of recursive local functions...? | |
local callDerivedConstructor; | |
callDerivedConstructor = function(self, instance, ...) | |
for k, v in pairs(super(self)) do | |
if rawget(v, "derived_constructor") then | |
rawget(v, "derived_constructor")(instance, ...) | |
end | |
local s = super(v) | |
if s then callDerivedConstructor(s, instance, ...) end | |
end | |
end | |
callDerivedConstructor(class, instance, ...) | |
-- Call constructor | |
if rawget(class, "constructor") then | |
rawget(class, "constructor")(instance, ...) | |
end | |
instance.constructor = false | |
-- Add a change handler for all ._changeVARIABLE methods | |
for k, v in pairs(class) do | |
if k:sub(1, 7) == "_change" then | |
addChangeHandler(instance, k:sub(8), v) | |
end | |
end | |
return instance | |
end | |
--// enew(element, class, ...) | |
--|| @desc: Makes an element an instance of 'class' and calls the constructor | |
--|| @param: table 'class' - The class which should be instanciated | |
--|| @param: vararg - Parameters passed to the constructor and derived_constructors | |
--|| @return:element - The element passed | |
--\\ | |
function enew(element, class, ...) | |
-- DEBUG: Validate that we are not instanciating a class with pure virtual methods | |
if DEBUG then | |
for k, v in pairs(class) do | |
assert(v ~= pure_virtual, "Attempted to instaciate a class with an unimplemented pure virtual method ("..tostring(k)..")") | |
end | |
end | |
local instance = setmetatable( { element = element }, | |
{ | |
__index = class; | |
__super = { class }; | |
__newindex = class.__newindex; | |
__call = class.__call; | |
__len = class.__len; | |
__unm = class.__unm; | |
__add = class.__add; | |
__sub = class.__sub; | |
__mul = class.__mul; | |
__div = class.__div; | |
__pow = class.__pow; | |
__concat = class.__concat; | |
}) | |
elementIndex[element] = instance | |
-- Weird Lua behaviour requires forwarding of recursive local functions...? | |
local callDerivedConstructor; | |
callDerivedConstructor = function(self, instance, ...) | |
for k, v in pairs(super(self)) do | |
if rawget(v, "derived_constructor") then | |
rawget(v, "derived_constructor")(instance, ...) | |
end | |
local s = super(v) | |
if s then callDerivedConstructor(s, instance, ...) end | |
end | |
end | |
callDerivedConstructor(class, element, ...) | |
-- Call constructor | |
if rawget(class, "constructor") then | |
rawget(class, "constructor")(element, ...) | |
end | |
element.constructor = false | |
-- Add a change handler for all ._changeVARIABLE methods | |
for k, v in pairs(class) do | |
if k:sub(1, 7) == "_change" then | |
addChangeHandler(instance, k:sub(8), v) | |
end | |
end | |
-- Add the destruction handler | |
addEventHandler( | |
triggerClientEvent ~= nil and | |
"onElementDestroy" or | |
"onClientElementDestroy", element, __removeElementIndex, false, "low-999999") | |
return element | |
end | |
--// registerElementClass(elementType, class) | |
--|| @desc: Registers a class to be used upon element index operations like e.g. | |
--|| getPlayerFromName("MrX"):hello() would search in the class assigned to "player" | |
--|| @param: string 'elementType'- The element type the class is supposed to be assigned to | |
--|| @param: table 'class' - The class which is assigned | |
--\\ | |
function registerElementClass(elementType, class) | |
elementClasses[elementType] = class | |
end | |
--// __removeElementIndex() | |
--|| @desc: This function calls delete on the hidden source parameter to invoke the destructor | |
--|| !!! Avoid calling this function manually unless you know what you're doing! !!! | |
--\\ | |
function __removeElementIndex() | |
delete(source) | |
end | |
--// delete(self, ...) | |
--|| @desc: Deletes an instance and calls the destructor | |
--|| and all derived_destructors | |
--|| @param: table 'self' - The instance to be deleted | |
--|| @param: vararg - Parameters passed to the destructor and derived_destructors | |
--\\ | |
function delete(self, ...) | |
if self.destructor then --if rawget(self, "destructor") then | |
self:destructor(...) | |
end | |
-- Prevent the destructor to be called twice | |
self.destructor = false | |
-- Weird Lua behaviour requires forwarding of recursive local functions...? | |
local callDerivedDestructor; | |
callDerivedDestructor = function(self, instance, ...) | |
for k, v in pairs(super(self)) do | |
if rawget(v, "derived_destructor") then | |
rawget(v, "derived_destructor")(instance, ...) | |
end | |
local s = super(v) | |
if s then callDerivedDestructor(s, instance, ...) end | |
end | |
end | |
-- Cleanup | |
elementIndex[self] = nil | |
end | |
--// super(self) | |
--|| @desc: Gets the superclasses of an instance or class | |
--|| @param: table 'self' - The instance / class to get the parent class of | |
--|| @return:table<table> - The superclasses | |
--\\ | |
function super(self) | |
if isElement(self) then | |
self = elementIndex[self] | |
end | |
local metatable = getmetatable(self) | |
if metatable then return metatable.__super | |
else return {} | |
end | |
end | |
--// instanceof(self, class, direct = false) | |
--|| @desc: Returns if 'self' is an instance of 'class'. If 'direct' is set to true it enforces 'self' | |
--|| to be a direct descendant of 'class' (new(self, class)). If 'direct' is set to false | |
--|| (default) 'self' is allowed to be a instance with any kind of link to 'class' even with | |
--|| multiple levels of inheritance or similar polymorphic connections. | |
--|| @param: table 'self' - The instance to check | |
--|| @param: table 'class' - The class to check | |
--|| @optparam: bool 'direct' (false) - Whether to check for direct inheritance | |
--|| @return:bool - the result of the check | |
--\\ | |
function instanceof(self, class, direct) | |
for k, v in pairs(super(self)) do | |
if v == class then return true end | |
end | |
if direct then return false end | |
local check = false | |
-- Check if any of 'self's base classes is inheriting from 'class' | |
for k, v in pairs(super(self)) do | |
check = instanceof(v, class, false) | |
end | |
return check | |
end | |
--// bind(func, self) | |
--|| @desc: Wraps the function(...) return function(self, ...) end idiom | |
--|| @param: table 'func' - The function to bind | |
--|| @param: vararg ... - The parameters to bind | |
--|| @return:function - the bound function | |
--\\ | |
function bind(func, ...) | |
local boundParams = {...} | |
return | |
function(...) | |
local params = {} | |
local boundParamSize = select("#", unpack(boundParams)) | |
for i = 1, boundParamSize do | |
params[i] = boundParams[i] | |
end | |
local funcParams = {...} | |
for i = 1, select("#", ...) do | |
params[boundParamSize + i] = funcParams[i] | |
end | |
return func(unpack(params)) | |
end | |
end | |
--// load(class, ...) | |
--|| @desc: Creates an instance of 'class' and call the 'load' method | |
--|| @param: table 'class' - The class which should be instanciated | |
--|| @param: vararg - Parameters passed to the 'load' method | |
--|| @return:table - The newly created instance | |
--\\ | |
function load(class, ...) | |
assert(type(class) == "table", "first argument provided to load is not a table") | |
local instance = setmetatable( { }, | |
{ | |
__index = class; | |
__super = { class }; | |
__newindex = class.__newindex; | |
__call = class.__call; | |
}) | |
-- Call load | |
if rawget(class, "load") then | |
rawget(class, "load")(instance, ...) | |
end | |
instance.load = false | |
return instance | |
end | |
--// inherit(from, what) | |
--|| @desc: Creates a new class inheriting from 'from' or sets 'what' to inherit from 'from' | |
--|| @param: table 'from' - The class to inherit from | |
--|| @optparam: table 'what' - The class which should inherit, optional | |
--|| @return:table - The now inheriting class | |
--\\ | |
function inherit(from, what) | |
if not from then | |
outputDebugString("Attempt to inherit a nil table value") | |
outputConsole(debug.traceback()) | |
return {} | |
end | |
if not what then | |
local classt = setmetatable({}, { __index = _inheritIndex, __super = { from } }) | |
if from.onInherit then | |
from.onInherit(classt) | |
end | |
return classt | |
end | |
local metatable = getmetatable(what) or {} | |
local oldsuper = metatable and metatable.__super or {} | |
table.insert(oldsuper, 1, from) | |
metatable.__super = oldsuper | |
metatable.__index = _inheritIndex | |
return setmetatable(what, metatable) | |
end | |
function _inheritIndex(self, key) | |
for k, v in pairs(super(self) or {}) do | |
if v[key] then return v[key] end | |
end | |
return nil | |
end | |
--// pure_virtual() | |
--|| @desc: Yields an error on call. Use like: class.memberfunction = pure_virtual to enforce | |
--|| implementation in derived classes | |
--\\ | |
function pure_virtual() | |
error("Function implementation missing") | |
end | |
--// getTypeName(object) | |
--|| @desc: Returns the type name of the specified object (for debug purpose only) | |
--|| @param: object - the object | |
--|| @return: The type name | |
--\\ | |
function getTypeName(object) | |
return table.find(_G, getmetatable(object).__index) | |
end | |
--// Syntax 1: addChangeHandler(instance, key, func) | |
--|| @desc: addChangeHandler calls 'func' whenever 'key' is changed on 'instance' | |
--|| @param: table instance - any table to watch for changes | |
--|| @param: string key - the key to watch | |
--|| @param: function func - the function to call when the value of instance[key] is changed | |
--|| return anything but nil to override the value. Do not attempt to | |
--|| change instance[key] directly within 'func' as it will cause a | |
--|| stack overflow. Additionally: do not use rawset on the table and | |
--|| key with a changehandler unless you want to face some awkward bugs | |
--|| | |
--|| Parameters for func: function (table/element instance, any value) | |
--|| | |
--|| Syntax 2: addChangeHandler(instance, func, func) | |
--|| @desc: addChangeHandler calls 'func' whenever any key is changed on 'instance' | |
--|| @param: table instance - any table to watch for changes | |
--|| @param: function func - the function to call when the value of any index in instance is changed | |
--|| return anything but nil to override the value. Do not attempt to | |
--|| change the contents of instance directly within 'func' as it will cause a | |
--|| stack overflow. Additionally: do not use rawset on the table with a | |
--|| changehandler unless you want to face some awkward bugs | |
--|| | |
--|| Parameters for func: function (table/element instance, any key, any value) | |
--\\ | |
function addChangeHandler(instance, key, func) | |
if isElement(instance) then | |
instance = elementIndex[instance] | |
end | |
local metatable = getmetatable(instance) or {} | |
if not metatable.__changeHandler then | |
metatable.__changeHandler = {} | |
metatable.__changeData = {} | |
metatable.__realNewindexFunction = metatable.__newindex | |
-- This saves us from checking on each call | |
if type(metatable.__index) == "table" then | |
metatable.__realIndexTable = metatable.__index | |
elseif type(metatable.__index) == "function" then | |
metatable.__realIndexFunction = metatable.__index | |
end | |
metatable.__index = __changeHandlerIndex | |
metatable.__newindex = __changeHandlerNewindex | |
end | |
if type(key) == "function" then | |
func = key | |
metatable.__changeHandler = func | |
else | |
metatable.__changeHandler[key] = func | |
end | |
return setmetatable(instance, metatable) | |
end | |
function __changeHandlerIndex(self, key) | |
local metatable = getmetatable(self) | |
if metatable.__changeData[key] then return metatable.__changeData[key] end | |
return ( | |
-- If we have a __index function use it | |
metatable.__realIndexFunction and | |
metatable.__realIndexFunction(rawget(self, "element") or self, key) or | |
-- If we have a __index table use it | |
metatable.__realIndexTable and | |
metatable.__realIndexTable[key] or | |
-- Else rawget | |
rawget(self, key) | |
) | |
end | |
function __changeHandlerNewindex(self, key, value) | |
local metatable = getmetatable(self) | |
if type(metatable.__changeHandler) == "table" then | |
if metatable.__changeHandler[key] then | |
local ret = metatable.__changeHandler[key](rawget(self, "element") or self, value) | |
if ret ~= nil then | |
value = ret | |
end | |
metatable.__changeData[key] = value | |
setmetatable(self, metatable) | |
return | |
end | |
elseif type(metatable.__changeHandler) == "function" then | |
local ret = metatable.__changeHandler(rawget(self, "element") or self, key, value) | |
if ret ~= nil then | |
value = ret | |
end | |
metatable.__changeData[key] = value | |
setmetatable(self, metatable) | |
return | |
end | |
return ( | |
-- If we have a __newindex function use it | |
metatable.__realNewindexFunction and | |
metatable.__realNewindexFunction(rawget(self, "element") or self, key, value) or | |
-- Else rawset | |
rawset(self, key, value) | |
) | |
end | |
-- -- Magic happens here. -- -- | |
-- Lua's lightuserdatas (which MTA uses for Elements) can have one global shared metatable. This blocks | |
-- the possibility to do debug.setmetatable(element, class), therefore we redirect all class on elements | |
-- to a table. This allows full control about the metaactions on any lightuserdata. | |
-- The debug.setmetatable is applied to root as it will always be an existing element. It could be applied | |
-- to any other element and have the same effect | |
-- Note for 1.4: add "<oop>false</oop>" into the meta | |
debug.setmetatable(root, | |
{ | |
__index = function(self, key) | |
if elementIndex[self] then | |
return elementIndex[self][key] | |
elseif elementClasses[getElementType(self)] then | |
enew(self, elementClasses[getElementType(self)]) | |
return self[key] | |
end | |
end, | |
__newindex = function(self, key, value) | |
if not elementIndex[self] then | |
enew(self, elementClasses[getElementType(self)] or {}) | |
end | |
elementIndex[self][key] = value | |
end, | |
} | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment