├── LICENSE ├── README.md └── include.lua /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Fernando Lopez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lua class 2 | A library to add support for OOP on Lua with a really nice and elegant style. This was created and tested by me on a personal project in a videogame called "Multi Theft Auto", however it may be useful for someone else someday, as it should work anywhere with Lua <3, if you like it feel free to give it a star and follow me please. 3 | 4 | ## How to use? 5 | Just download the include.lua file and include it to your project, it will work in any project that uses lua. This library was designed and tested on Lua 5.1 6 | * Note: For Lua 5.3 you'll have to change `unpack` function for `table.unpack`. 7 | 8 | ## Create your first class 9 | Its pretty easy, as said it adds support to create classes using an elegant syntax 10 | 11 | ```Lua 12 | class 'HelloWorld' { 13 | constructor = function(self, arg1, arg2, ...) 14 | 15 | end, 16 | 17 | destructor = function(self) 18 | 19 | end, 20 | 21 | greet = function(self) 22 | print('Hello world!'); 23 | end, 24 | } 25 | ``` 26 | 27 | ## Inheritance 28 | The library supports class single inheritance and multiple inheritance preserving a nice style. 29 | 30 | ### Single class inheritance 31 | ```Lua 32 | class 'Gamemode' { 33 | constructor = function(self) 34 | self.players = {}; 35 | self.map = false; 36 | self.state = GamemodeState.NotRunning; 37 | end, 38 | 39 | destructor = function(self) 40 | -- Do something with players I guess 41 | end, 42 | 43 | unloadMapForPlayer = function(self, player) 44 | player:triggerClientEvent('unloadMap'); 45 | end, 46 | } 47 | 48 | class 'RaceGamemode' (Gamemode) { 49 | constructor = function(self) 50 | self.vehicles = {}; 51 | self.checkpoints = {}; 52 | end, 53 | 54 | destructor = function(self) 55 | self:unloadPlayers(); 56 | end, 57 | 58 | unloadPlayers = function(self) 59 | for _, player in pairs(self.players) do 60 | self:unloadMapForPlayer(player); 61 | self:destroyPlayerVehicle(player); 62 | end 63 | end, 64 | 65 | destroyPlayerVehicle = function(self, player) 66 | self.vehicles[player]:destroy(); 67 | self.vehicles[player] = nil; 68 | end, 69 | } 70 | ``` 71 | 72 | ### Multiple class inheritance 73 | ```Lua 74 | include 'gamemodes.lua' 75 | 76 | class 'TeamGamemode' { 77 | constructor = function(self) 78 | self.teamA = Team(...) 79 | self.teamB = Team(...) 80 | end, 81 | 82 | assignPlayerTeam = function(self, player) 83 | local randomTeam = math.random(0, 1) 84 | if (randomTeam == 0) then 85 | player:setTeam(self.teamA); 86 | else 87 | player:setTeam(self.teamB); 88 | end 89 | end, 90 | } 91 | 92 | class 'Deathmatch' (Gamemode, TeamGamemode) { 93 | constructor = function(self, arguments) 94 | self.settings = arguments; 95 | end, 96 | 97 | onPlayerJoin = function(self, player) 98 | self:assignPlayerTeam(player); 99 | self:checkGameState(); 100 | end, 101 | 102 | checkGameState = function(self) 103 | if (self:getAlivePlayersCount() < 1) then 104 | self:startNextRound(); 105 | end 106 | end, 107 | } 108 | ``` 109 | 110 | ## Creating objects/instances 111 | Just call your class name like a normal function and you're ready to go! 112 | 113 | ```Lua 114 | include 'deathmatch.lua' 115 | 116 | g_Deathmatch = Deathmatch { randomMaps = true, maxPlayers = 16, pingLimit = 280 }; 117 | g_Race = RaceGamemode(); 118 | ``` 119 | 120 | ## Partial classes 121 | Isn't a great practice at all but sometimes you may want to keep a specific-purpose class with its related topic on a file. Lets supose you have a class for x functionality and it has an "Utils" class, you may want to keep that part of the code on the same file, also you have another class for Utils for your y functionality. In Lua the reasonable behaviour would be to overwrite existing class for the one of y functionality. Using this library you'll be able to have both and they'll be merged into a single class; Example: 122 | 123 | ```Lua 124 | -- file mapmanager.lua 125 | class 'MapManager' { 126 | -- ... 127 | } 128 | 129 | class 'Utils' { 130 | getFileHash = function(path) 131 | -- ... 132 | end, 133 | } 134 | 135 | -- file scriptloader.lua 136 | class 'ScriptLoader' { 137 | -- ... 138 | } 139 | 140 | class 'Utils' { 141 | generateEmptyEnvironment = function(self) 142 | -- ... 143 | end, 144 | } 145 | 146 | -- Ending Utils class 147 | class 'Utils' { 148 | getFileHash = function(path) 149 | -- ... 150 | end, 151 | 152 | generateEmptyEnvironment = function(self) 153 | -- ... 154 | end, 155 | } 156 | ``` 157 | -------------------------------------------------------------------------------- /include.lua: -------------------------------------------------------------------------------- 1 | local function table_copy(source) 2 | local result = {} 3 | 4 | for key, value in pairs(source) do 5 | if type(value) == "table" then 6 | result[key] = table_copy(value) 7 | else 8 | result[key] = value 9 | end 10 | end 11 | 12 | return result 13 | end 14 | 15 | local function table_merge(...) 16 | -- Validate arguments 17 | local arguments = { ... } 18 | 19 | for index = 1, select("#", ...) do 20 | assert(type(arguments[index]) == "table", "Expected table-only argument, got " .. type(arguments[index]) .. " at index " .. index) 21 | end 22 | 23 | assert(#arguments >= 2, "Expected at least two tables to merge") 24 | 25 | -- Generate merged table 26 | local result = {} 27 | 28 | for _, source in pairs(arguments) do 29 | for key, value in pairs(source) do 30 | if type(value) == "table" then 31 | result[key] = table_copy(value) 32 | else 33 | result[key] = value 34 | end 35 | end 36 | end 37 | 38 | return result 39 | end 40 | 41 | local function registerClass(name, superClasses, definition) 42 | -- Look for a previous class and merge it with new definition 43 | local partialClass = _G[name] 44 | if partialClass and type(partialClass) == "table" then 45 | definition = table_merge(partialClass, definition) 46 | end 47 | 48 | -- Generate class list 49 | local classList = { definition } 50 | local addedClasses = {} 51 | 52 | local function insertClassSuper(super) 53 | if not addedClasses[super] then 54 | -- Block this class from being added 55 | addedClasses[super] = true 56 | 57 | -- Insert this class 58 | table.insert(classList, 1, super) 59 | 60 | -- Insert class parents 61 | local behavior = getmetatable(super) 62 | if behavior and behavior.__classlist then 63 | for _, superClass in pairs(behavior.__classlist) do 64 | insertClassSuper(superClass) 65 | end 66 | end 67 | end 68 | end 69 | 70 | for _, super in pairs(superClasses) do 71 | insertClassSuper(super) 72 | end 73 | 74 | -- Define class behavior 75 | local behavior = { __classlist = classList } 76 | 77 | function behavior:__tostring() 78 | return name 79 | end 80 | 81 | function behavior:__index(key) 82 | for _, super in pairs(behavior.__classlist) do 83 | local value = rawget(super, key) 84 | if value ~= nil then 85 | return value 86 | end 87 | end 88 | 89 | return nil 90 | end 91 | 92 | function behavior:__call(...) 93 | -- Generate instance 94 | local object = {} 95 | 96 | setmetatable( 97 | object, 98 | { 99 | __class = definition, 100 | __add = definition.__add, 101 | __sub = definition.__sub, 102 | __mul = definition.__mul, 103 | __div = definition.__div, 104 | __mod = definition.__mod, 105 | __pow = definition.__pow, 106 | __unm = definition.__unm, 107 | __concat = definition.__concat, 108 | __len = definition.__len, 109 | __eq = definition.__eq, 110 | __lt = definition.__lt, 111 | __le = definition.__le, 112 | 113 | __tostring = function() 114 | return name 115 | end, 116 | 117 | __index = function(_, key) 118 | local value 119 | 120 | local classIndex = definition.__index 121 | if classIndex and type(classIndex) == "function" then 122 | value = classIndex(object, key) 123 | end 124 | 125 | if value == nil then 126 | value = definition[key] 127 | end 128 | 129 | return value 130 | end, 131 | 132 | __newindex = function(_, key, value) 133 | local classNewIndex = definition.__newindex 134 | if classNewIndex and type(classNewIndex) == "function" then 135 | local result = classNewIndex(object, key, value) 136 | if result == true then 137 | return true 138 | end 139 | end 140 | 141 | return rawset(object, key, value) 142 | end, 143 | } 144 | ) 145 | 146 | -- Call constructor(s) 147 | recursiveCall(object, "constructor", ...) 148 | 149 | -- Disable constructor 150 | rawset(object, "constructor", false) 151 | 152 | return object 153 | end 154 | 155 | function definition:destroy() 156 | -- Call destructor(s) 157 | recursiveCall(self, true, "destructor") 158 | 159 | -- Disable destructor 160 | rawset(self, "destructor", false) 161 | 162 | -- Remove attached metatable 163 | setmetatable(self, nil) 164 | end 165 | 166 | setmetatable(definition, behavior) 167 | 168 | -- Register class into global environment 169 | _G[name] = definition 170 | end 171 | 172 | local function parseClass(name, ...) 173 | -- Validate argument list 174 | local arguments = { ... } 175 | 176 | for index = 1, select("#", ...) do 177 | assert(arguments[index] ~= nil, "A nil parameter was passed to class " .. name .. ", check scope or spell") 178 | assert(type(arguments[index]) == "table", "Non-table parameter passed to class " .. name .. " - " .. type(arguments[index])) 179 | end 180 | 181 | assert(#arguments ~= 0, "Can't create class without super classes or definition") 182 | 183 | -- Handle class creation if we dont have any super classes 184 | if #arguments == 1 and not getmetatable(arguments[1]) then 185 | return registerClass(name, {}, arguments[1]) 186 | end 187 | 188 | -- Return a function that will listen for definition 189 | return function(definition) 190 | assert(type(definition) == "table", "Expected definition body for class") 191 | assert(not getmetatable(definition), "Class definition isn't valid as it has an attached metatable") 192 | 193 | return registerClass(name, arguments, definition) 194 | end 195 | end 196 | 197 | function class(name) 198 | assert(type(name) == "string", "Expected [string] name for class constructor, got " .. type(name)) 199 | 200 | return function(...) 201 | return parseClass(name, ...) 202 | end 203 | end 204 | 205 | local function table_reverse(source) 206 | local result = {} 207 | 208 | for _, value in pairs(source) do 209 | table.insert(result, 1, value) 210 | end 211 | 212 | return result 213 | end 214 | 215 | local unpack = unpack or table.unpack 216 | 217 | function recursiveCall(object, method, ...) 218 | local reversed = false 219 | local arguments = { ... } 220 | 221 | if type(method) == "boolean" then 222 | reversed = method 223 | method = arguments[1] 224 | table.remove(arguments, 1) 225 | end 226 | 227 | if method then 228 | local class = classof(object) 229 | local behavior = getmetatable(class) 230 | 231 | if behavior and behavior.__classlist then 232 | local classList = behavior.__classlist 233 | if reversed then 234 | classList = table_reverse(classList) 235 | end 236 | 237 | for _, class in pairs(classList) do 238 | local funct = rawget(class, method) 239 | if funct and type(funct) == "function" then 240 | funct(object, unpack(arguments)) 241 | end 242 | end 243 | end 244 | end 245 | 246 | return false 247 | end 248 | 249 | function classinherits(derived, parent) 250 | if isclass(derived) and isclass(parent) then 251 | local behavior = getmetatable(derived) 252 | if behavior and behavior.__classlist then 253 | for _, class in pairs(behavior.__classlist) do 254 | if class == parent then 255 | return true 256 | end 257 | end 258 | end 259 | end 260 | 261 | return false 262 | end 263 | 264 | function instanceof(object, class) 265 | if isinstance(object) and isclass(class) then 266 | if classinherits(classof(object), class) then 267 | return true 268 | end 269 | end 270 | 271 | return false 272 | end 273 | 274 | function typeof(value) 275 | if isinstance(value) then 276 | value = classof(value) 277 | else 278 | value = type(value) 279 | end 280 | 281 | return tostring(value) 282 | end 283 | 284 | function classof(object) 285 | if isinstance(object) then 286 | local behavior = getmetatable(object) 287 | if behavior and behavior.__class then 288 | return behavior.__class 289 | end 290 | end 291 | 292 | return false 293 | end 294 | 295 | function isclass(value) 296 | if type(value) == "table" then 297 | local behavior = getmetatable(value) 298 | if behavior then 299 | if behavior.__call then 300 | return true 301 | end 302 | end 303 | end 304 | 305 | return false 306 | end 307 | 308 | function isinstance(value) 309 | if type(value) == "table" then 310 | local behavior = getmetatable(value) 311 | if behavior then 312 | if behavior.__class then 313 | return true 314 | end 315 | end 316 | end 317 | 318 | return false 319 | end 320 | --------------------------------------------------------------------------------