diff options
Diffstat (limited to 'gamemode/server')
| -rw-r--r-- | gamemode/server/database.lua | 106 | ||||
| -rw-r--r-- | gamemode/server/glon.lua | 365 | ||||
| -rw-r--r-- | gamemode/server/pon.lua | 361 | ||||
| -rw-r--r-- | gamemode/server/von.lua | 801 |
4 files changed, 1575 insertions, 58 deletions
diff --git a/gamemode/server/database.lua b/gamemode/server/database.lua index 20efafc..9f8608d 100644 --- a/gamemode/server/database.lua +++ b/gamemode/server/database.lua @@ -2,84 +2,74 @@ --[[ Uses: GM.StorageMethod - Can be: "Text", "Static Map", "LZWCompressed" - If method is "Static Map", then GM.StoreageStaticMap must be a table of strings to string ID's. - GM.StorageBackend - Can be: "File" + Can be: "File:Text", "File:Compressed" + GM.Compressor + Can be: "GLON", "VON", "PON", "JSON" GM.StorageDebug Can be: true, false Enables extra debugging to find errors easily ]] +--[[ +Provides: + loadTable(uniqueID) + loads a table with the given unique id, and returns it + storeTable(uniqueID, table) + stores the given table into the database, needs a uniqueid to find it again. +]] print("Hello from database.lua!") +GAMEMODE = GAMEMODE or {} function loadTable(uniqueIdentifier) - - return nil + local rfile = file.Open("gmstranded/" .. uniqueIdentifier .. ".txt", "r", "DATA") + local text = rfile:Read(rfile:Size()) + if(GAMEMODE.StorageMethod == "File:Compressed") then + text = util.Decompress(text) + end + return convertTextTable(text) end function storeTable(uniqueIdentifier, table) - if (GM.StorageDebug) then - print("Asked to store:") - PrintTable(table) + local storetext = convertTableText(table) + if(GAMEMODE.StorageMethod == "File:Compressed") then + storetext = util.Compress(storetext) end - if (GM.StorageDebug) then - for k,v in pairs(tbl) do - if (isfunction(v)) then - print("Database.lua error: tried to save a table that has a function in it!") - PrintTable(tbl) - print("At field:") - print(k) - end - end + if !(file.IsDir("gmstranded","DATA")) then + file.CreateDir("gmstranded") end - + local tfile = file.Open("gmstranded/" .. uniqueIdentifier .. ".txt", "w","DATA") + tfile:Write(storetext) + tfile:Flush() + tfile:Close() end ---Remove symbols that might be used as part of the storage scheme -local function encodeSymbolic(mstring) - local replacements = { - {"\\", "\\\\"}, - {"{","\\{"}, - {"}","\\}"}, - {"(","\\("}, - {")","\\)"}, - } - local ostring = mstring - for k,v in pairs(replacements) do - ostring = string.Replace(ostring,v[1],v[2]) +function convertTableText(tbl) + if(GAMEMODE.Compressor == "GLON") then + return glon.encode(tbl) + elseif(GAMEMODE.Compressor == "VON") then + return von.serialize(tbl) + elseif(GAMEMODE.Compressor == "PON") then + return pon.encode(tbl) + elseif(GAMEMODE.Compressor == "JSON") then + return util.TableToJSON(tbl) + else + error("GM.Compressor is not set correctly! It is:" .. GM.Compressor .. " but should be one of:GLON,VON,PON,JSON") end end -local function convertTableText(tbl) - local output = "t(" - local function addtoout(k) - if (istable(k)) then - output = output .. convertTableText(k) - elseif (isstring(k)) then - output = output .. "s(" .. encodeSymbolic(k) .. ")" - elseif (isnumber(k)) then - output = output .. "n(" .. k .. ")" - elseif (isbool(k)) then - if (k) then output = output .. "b(true)" - else output = output .. "b(false)" end - elseif (isangle(k)) then - output = output .. "a(" .. k.p .. "," .. k.y .. "," .. k.r .. ")" - elseif (isvector(k)) then - output = output .. "v(" .. k.x .. "," .. k.y .. "," .. k.z .. ")" - else - assert(true,"Tried to store unknown value type:") - printi(k) - end - end - for k,v in pairs(tbl) do - addtoout(k) - output = output .. ":" - addtoout(v) +function convertTextTable(text) + if(GAMEMODE.Compressor == "GLON") then + return glon.decode(text) + elseif(GAMEMODE.Compressor == "VON") then + return von.deserialize(text) + elseif(GAMEMODE.Compressor == "PON") then + return pon.decode(text) + elseif(GAMEMODE.Compressor == "JSON") then + return util.JSONToTable(text) + else + error("GM.Compressor is not set correctly! It is:" .. GM.Compressor .. " but should be one of:GLON,VON,PON,JSON") end - output = output .. ")" end - --[[ local function convertTableStatic(tbl) if (GM.StorageStaticMap == nil) then diff --git a/gamemode/server/glon.lua b/gamemode/server/glon.lua new file mode 100644 index 0000000..ccc7376 --- /dev/null +++ b/gamemode/server/glon.lua @@ -0,0 +1,365 @@ +-- GLON: Garry's Mod Lua Object Notation +-- A dialect of LON: Lua Object Notation +-- Made entirely by Deco Da Man +-- Types: + -- 2: table + -- 3: array + -- 4: fasle boolean + -- 5: true boolean + -- 6: number (NOT COMPRESSED, it isn't worth it) + -- 7: string + ---- non-LON types start here! + -- 8: Vector (NOT COMPRESSED, it isn't worth it) + -- 9: Angle (NOT COMPRESSED, it isn't worth it) + -- 10: Entity (Can do players, vehicles, npcs, weapons and any other type of entity (-1 for null entity)) + -- 11: Player (By UserID) + -- 12: CEffectData + -- 13: ConVar (Not ClientConVar) + -- 15: Color + -- 255: reference (Sends the ID of the table to use (for "local t = {} t.a=t")) +local pairs = pairs +local type = type +local string = string +local math = math +local tostring = tostring +local ValidEntity = ValidEntity +local error = error +local print = print +local setmetatable = setmetatable +local Vector = Vector +local Angle = Angle +local Entity = Entity +local EffectData = EffectData +local GetConVar = GetConVar +local tonumber = tonumber +local player = player +module("glon") +local idcount = {} +local encode_types +local decode_types +local function InDataEscape(s) + return string.gsub(string.gsub(s, "([\1\2])", "\2%1"), "%z", "\2\3") +end +encode_types = { + ["nil"] = {nil, function() + return "", nil + end}, + table = {2, function(o, rtabs) + for k,v in pairs(rtabs) do + if v == o then + return tostring( k ) .. "\1", 255 + end + end + rtabs[#rtabs + 1] = o + local is_array = true + local i = 0 + for k,v in pairs(o) do + i = i + 1 + if k ~= i or type(k) ~= "number" or math.floor(k) ~= k then + is_array = false + break end + end + local s = "" + for k,v in pairs(o) do + if not is_array then + s = s .. Write(k, rtabs) + end + s = s .. Write(v, rtabs) + end + return s .. "\1", is_array and 3 + end}, + boolean = {4, function(o) + return "", o and 5 + end}, + number = {6, function(o) + o = o == 0 and "" or o + return tostring(o) .. "\1" + end}, + string = {7, function(o) + return InDataEscape(o) .. "\1" + end}, + -- non-LON types start here! + Vector = {8, function(o) + return o.x .. "\1" .. o.y .. "\1" .. o.z .. "\1" + end}, + Angle = {9, function(o) + return o.p .. "\1" .. o.y .. "\1" .. o.r .. "\1" + end}, + Entity = {10, function(o) + return (ValidEntity(o) and o:EntIndex() or -1) .. "\1" + end}, + Player = {11, function(o) + return o:EntIndex() .. "\1" + end}, + CEffectData = {12, function(o, rtabs) + local t = {} + if o:GetAngle() ~= Angle(0,0,0) then + t.a = o:GetAngle() + end + if o:GetAttachment() ~= 0 then + t.h = o:GetAttachment() + end + if o:GetEntity():IsValid() then + t.e = o:GetEntity() + end + if o:GetMagnitude() ~= 0 then + t.m = o:GetMagnitude() + end + if o:GetNormal() ~= Vector(0,0,0) then + t.n = o:GetNormal() + end + if o:GetOrigin() ~= Vector(0,0,0) then + t.o = o:GetOrigin() + end + if o:GetRadius() ~= 0 then + t.r = o:GetRadius() + end + if o:GetScale() ~= 0 then + t.c = o:GetScale() + end + if o:GetStart() ~= 0 then + t.s = o:GetStart() + end + if o:GetSurfaceProp() ~= 0 then + t.p = o:GetSurfaceProp() + end + return encode_types.table[2](t, rtabs) + end}, + ConVar = {13, function(o) + return InDataEscape(o:GetName()) .. "\1" + end}, + PhysObj = {14, function(o) + local parent, obj, id = o:GetEntity() + for i = 1, parent:GetPhysicsObjectCount() do + obj = parent:GetPhysicsObjectNum() + if obj == o then + id = i + break end + end + return parent:EntIndex() .. "\1" .. id .. "\1" + end}, + Color = {15, function(o) + return o.r .. "\1" .. o.g .. "\1" .. o.b .. "\1" .. o.a .. "\1" + end}, +} +function Write(data, rtabs) + local t = encode_types[type(data)] + if t then + local data, id_override = t[2](data, rtabs) + local char = id_override or t[1] or "" + if char ~= "" then char = string.char(char) end + return char .. (data or "") + else + error(string.format("Tried to write unwriteable type: %s", + type(data))) + end +end +local CEffectDataTranslation = { + a = "Angle", + h = "Attachment", + e = "Entity", + m = "Magnitude", + n = "Normal", + o = "Origin", + r = "Radius", + c = "Scale", + s = "Start", + p = "SurfaceProp", +} +decode_types = { + -- \2\6omg\1\6omgavalue\1\1 + [2 ] = function(reader, rtabs) -- table + local t, c, pos = {}, reader:Next() + rtabs[#rtabs + 1] = t + local stage = false + local key + while true do + c, pos = reader:Peek() + if c == "\1" then + if stage then + error(string.format("Expected value to match key at %s! (Got EO Table)", + pos)) + else + reader:Next() + return t + end + else + if stage then + t[key] = Read(reader, rtabs) + else + key = Read(reader, rtabs) + end + stage = not stage + end + end + end, + [3] = function(reader, rtabs) -- array + local t, i, c, pos = {}, 1, reader:Next() + rtabs[#rtabs + 1] = t + while true do + c, pos = reader:Peek() + if c == "\1" then + reader:Next() + return t + else + t[i] = Read(reader, rtabs) + i = i + 1 + end + end + end, + [4 ] = function(reader) -- false boolean + reader:Next() + return false + end, + [5 ] = function(reader) -- true boolean + reader:Next() + return true + end, + [6 ] = function(reader) -- number + local s, c, pos, e = "", reader:Next() + while true do + c = reader:Next() + if not c then + error(string.format("Expected \1 to end number at %s! (Got EOF!)", + pos)) + elseif c == "\1" then + break + else + s = s .. c + end + end + if s == "" then s = "0" end + local n = tonumber(s) + if not n then + error(string.format("Invalid number at %s! (%q)", + pos, s)) + end + return n + end, + [7 ] = function(reader) -- string + local s, c, pos, e = "", reader:Next() + while true do + c = reader:Next() + if not c then + error(string.format("Expected unescaped \1 to end string at position %s! (Got EOF)", + pos)) + elseif e then + if c == "\3" then + s = s .. "\0" + else + s = s .. c + end + e = false + elseif c == "\2" then + e = true + elseif c == "\1" then + return s + else + s = s .. c + end + end + end, + [8 ] = function(reader) -- Vector + local x = decode_types[6](reader) + reader:StepBack() + local y = decode_types[6](reader) + reader:StepBack() + local z = decode_types[6](reader) + return Vector(x, y, z) + end, + [9 ] = function(reader) -- Angle + local p = decode_types[6](reader) + reader:StepBack() + local y = decode_types[6](reader) + reader:StepBack() + local r = decode_types[6](reader) + return Angle(p, y, r) + end, + [10 ] = function(reader) -- Entity + return Entity(decode_types[6](reader)) + end, + [11 ] = function(reader) -- Player + local num = decode_types[6](reader) + return player.GetByID(num) + end, + [12 ] = function(reader, rtabs) -- CEffectData + local t = decode_types[2](reader, rtabs) + local d = EffectData() + for k,v in pairs(t) do + d["Set" .. CEffectDataTranslation[k]](d, v) + end + return d + end, + [13 ] = function(reader) -- ConVar + return GetConVar(decode_types[7](reader)) + end, + [15 ] = function(reader) -- Color + local r = decode_types[6](reader) + reader:StepBack() + local g = decode_types[6](reader) + reader:StepBack() + local b = decode_types[6](reader) + reader:StepBack() + local a = decode_types[6](reader) + return Color(r, g, b, a) + end, + [255] = function(reader, rtabs) -- Reference + return rtabs[decode_types[6](reader) - 1] + end +} +function Read(reader, rtabs) + local t, pos = reader:Peek() + if not t then + error(string.format("Expected type ID at %s! (Got EOF)", + pos)) + else + local dt = decode_types[string.byte(t)] + if not dt then + error(string.format("Unknown type ID, %s!", + string.byte(t))) + else + return dt(reader, rtabs or {0}) + end + end +end +local reader_meta = {} +reader_meta.__index = reader_meta +function reader_meta:Next() + self.i = self.i + 1 + self.c = string.sub(self.s, self.i, self.i) + if self.c == "" then self.c = nil end + self.p = string.sub(self.s, self.i + 1, self.i + 1) + if self.p == "" then self.p = nil end + return self.c, self.i +end +function reader_meta:StepBack() + self.i = self.i-1 + self.c = string.sub(self.s, self.i, self.i) + if self.c == "" then self.c = nil end + self.p = string.sub(self.s, self.i + 1, self.i + 1) + if self.p == "" then self.p = nil end + return self.c, self.i +end +function reader_meta:Peek() + return self.p, self.i + 1 +end +function decode(data) + if type(data) == "nil" then + return nil + elseif type(data) ~= "string" then + error(string.format("Expected string to decode! (Got type %s)", + type(data) + )) + elseif data:len() == 0 then + return nil + end + return Read(setmetatable({ + s = data, + i = 0, + c = string.sub(data, 0, 0), + p = string.sub(data, 1, 1), + }, reader_meta), {}) +end +function encode(data) + return Write(data, {0}) -- to use the first key, to prevent it from interfereing with \1s. You can have up to 254 unique tables (including arrays) +end diff --git a/gamemode/server/pon.lua b/gamemode/server/pon.lua new file mode 100644 index 0000000..3c48a90 --- /dev/null +++ b/gamemode/server/pon.lua @@ -0,0 +1,361 @@ +--[[ +RECOMMENDED VERSION +VERSION 1.2.1 +Copyright thelastpenguin™ + You may use this for any purpose as long as: + - You don't remove this copyright notice. + - You don't claim this to be your own. + - You properly credit the author, thelastpenguin™, if you publish your work based on (and/or using) this. + + If you modify the code for any purpose, the above still applies to the modified code. + + The author is not held responsible for any d amages incured from the use of pON, you use it at your own risk. +DATA TYPES SUPPORTED: + - tables - k,v - pointers + - strings - k,v - pointers + - numbers - k,v + - booleans- k,v + - Vectors - k,v + - Angles - k,v + - Entities- k,v + - Players - k,v + +CHANGE LOG +V 1.1.0 + - Added Vehicle, NPC, NextBot, Player, Weapon +V 1.2.0 + - Added custom handling for k,v tables without any array component. +V 1.2.1 + - fixed deserialization bug. +V 1.3.0 + - added custom handling of strings without any escaped characters. +V 1.4.0 + - added detection of numbers without requiring 'n' datatype. (10 datatypes one for each num it could start with) + +THANKS TO... + - VERCAS for the inspiration. +]] + + +local pon = {}; +_G.pon = pon; + +local type, count = type, table.Count ; +local tonumber = tonumber ; + +do + local encode = {}; + + local tryCache ; + + local cacheSize = 0; + + encode['table'] = function( self, tbl, output, cache ) + + if( cache[ tbl ] )then + output[ #output + 1 ] = '('..cache[tbl]..')'; + return ; + else + cacheSize = cacheSize + 1; + cache[ tbl ] = cacheSize; + end + -- CALCULATE COMPONENT SIZES + local nSize = #tbl; + local kvSize = count( tbl ) - nSize; + + if( nSize == 0 and kvSize > 0 )then + output[ #output + 1 ] = '['; + else + output[ #output + 1 ] = '{'; + + if nSize > 0 then + for i = 1, nSize do + local v = tbl[ i ]; + if v == nil then continue end + local tv = type( v ); + -- HANDLE POINTERS + if( tv == 'string' )then + local pid = cache[ v ]; + if( pid )then + output[ #output + 1 ] = '('..pid..')'; + else + cacheSize = cacheSize + 1; + cache[ v ] = cacheSize; + + self.string( self, v, output, cache ); + end + else + self[ tv ]( self, v, output, cache ); + end + end + end + end + + if( kvSize > 0 )then + if( nSize > 0 )then + output[ #output + 1 ] = '~'; + end + for k,v in next, tbl do + if( type( k ) ~= 'number' or k < 1 or k > nSize )then + local tk, tv = type( k ), type( v ); + + -- THE KEY + if( tk == 'string' )then + local pid = cache[ k ]; + if( pid )then + output[ #output + 1 ] = '('..pid..')'; + else + cacheSize = cacheSize + 1; + cache[ k ] = cacheSize; + + self.string( self, k, output, cache ); + end + else + self[ tk ]( self, k, output, cache ); + end + + -- THE VALUE + if( tv == 'string' )then + local pid = cache[ v ]; + if( pid )then + output[ #output + 1 ] = '('..pid..')'; + else + cacheSize = cacheSize + 1; + cache[ v ] = cacheSize; + + self.string( self, v, output, cache ); + end + else + self[ tv ]( self, v, output, cache ); + end + + end + end + end + output[ #output + 1 ] = '}'; + end + -- ENCODE STRING + local gsub = string.gsub ; + encode['string'] = function( self, str, output ) + --if tryCache( str, output ) then return end + local estr, count = gsub( str, ";", "\\;"); + if( count == 0 )then + output[ #output + 1 ] = '\''..str..';'; + else + output[ #output + 1 ] = '"'..estr..'";'; + end + end + -- ENCODE NUMBER + encode['number'] = function( self, num, output ) + output[ #output + 1 ] = tonumber( num )..';'; + end + -- ENCODE BOOLEAN + encode['boolean'] = function( self, val, output ) + output[ #output + 1 ] = val and 't' or 'f' + end + -- ENCODE VECTOR + encode['Vector'] = function( self, val, output ) + output[ #output + 1 ] = ('v'..val.x..','..val.y)..(','..val.z..';'); + end + -- ENCODE ANGLE + encode['Angle'] = function( self, val, output ) + output[ #output + 1 ] = ('a'..val.p..','..val.y)..(','..val.r..';'); + end + encode['Entity'] = function( self, val, output ) + output[ #output + 1] = 'E'..(IsValid( val ) and (val:EntIndex( )..';') or '#'); + end + encode['Player'] = encode['Entity']; + encode['Vehicle'] = encode['Entity']; + encode['Weapon'] = encode['Entity']; + encode['NPC'] = encode['Entity']; + encode['NextBot'] = encode['Entity']; + + do + local empty, concat = table.Empty, table.concat ; + function pon.encode( tbl ) + local output = {}; + cacheSize = 0; + encode[ 'table' ]( encode, tbl, output, {} ); + local res = concat( output ); + + return res; + end + end +end + +do + local tonumber = tonumber ; + local find, sub, gsub, Explode = string.find, string.sub, string.gsub, string.Explode ; + local Vector, Angle, Entity = Vector, Angle, Entity ; + + local decode = {}; + decode['{'] = function( self, index, str, cache ) + + local cur = {}; + cache[ #cache + 1 ] = cur; + + local k, v, tk, tv = 1, nil, nil, nil; + while( true )do + tv = sub( str, index, index ); + if( not tv or tv == '~' )then + index = index + 1; + break ; + end + if( tv == '}' )then + return index + 1, cur; + end + + -- READ THE VALUE + index = index + 1; + index, v = self[ tv ]( self, index, str, cache ); + cur[ k ] = v; + + k = k + 1; + end + + while( true )do + tk = sub( str, index, index ); + if( not tk or tk == '}' )then + index = index + 1; + break ; + end + + -- READ THE KEY + + index = index + 1; + index, k = self[ tk ]( self, index, str, cache ); + + -- READ THE VALUE + tv = sub( str, index, index ); + index = index + 1; + index, v = self[ tv ]( self, index, str, cache ); + + cur[ k ] = v; + end + + return index, cur; + end + decode['['] = function( self, index, str, cache ) + + local cur = {}; + cache[ #cache + 1 ] = cur; + + local k, v, tk, tv = 1, nil, nil, nil; + while( true )do + tk = sub( str, index, index ); + if( not tk or tk == '}' )then + index = index + 1; + break ; + end + + -- READ THE KEY + + index = index + 1; + index, k = self[ tk ]( self, index, str, cache ); + -- READ THE VALUE + tv = sub( str, index, index ); + index = index + 1; + index, v = self[ tv ]( self, index, str, cache ); + + cur[ k ] = v; + end + + return index, cur; + end + + -- STRING + decode['"'] = function( self, index, str, cache ) + local finish = find( str, '";', index, true ); + local res = gsub( sub( str, index, finish - 1 ), '\\;', ';' ); + index = finish + 2; + + cache[ #cache + 1 ] = res; + return index, res; + end + -- STRING NO ESCAPING NEEDED + decode['\''] = function( self, index, str, cache ) + local finish = find( str, ';', index, true ); + local res = sub( str, index, finish - 1 ) + index = finish + 1; + + cache[ #cache + 1 ] = res; + return index, res; + end + + -- NUMBER + decode['n'] = function( self, index, str, cache ) + index = index - 1; + local finish = find( str, ';', index, true ); + local num = tonumber( sub( str, index, finish - 1 ) ); + index = finish + 1; + return index, num; + end + decode['0'] = decode['n']; + decode['1'] = decode['n']; + decode['2'] = decode['n']; + decode['3'] = decode['n']; + decode['4'] = decode['n']; + decode['5'] = decode['n']; + decode['6'] = decode['n']; + decode['7'] = decode['n']; + decode['8'] = decode['n']; + decode['9'] = decode['n']; + decode['-'] = decode['n']; + + -- POINTER + decode['('] = function( self, index, str, cache ) + local finish = find( str, ')', index, true ); + local num = tonumber( sub( str, index, finish - 1 ) ); + index = finish + 1; + return index, cache[ num ]; + end + + -- BOOLEAN. ONE DATA TYPE FOR YES, ANOTHER FOR NO. + decode[ 't' ] = function( self, index ) + return index, true; + end + decode[ 'f' ] = function( self, index ) + return index, false; + end + + -- VECTOR + decode[ 'v' ] = function( self, index, str, cache ) + local finish = find( str, ';', index, true ); + local vecStr = sub( str, index, finish - 1 ); + index = finish + 1; -- update the index. + local segs = Explode( ',', vecStr, false ); + return index, Vector( tonumber( segs[1] ), tonumber( segs[2] ), tonumber( segs[3] ) ); + end + -- ANGLE + decode[ 'a' ] = function( self, index, str, cache ) + local finish = find( str, ';', index, true ); + local angStr = sub( str, index, finish - 1 ); + index = finish + 1; -- update the index. + local segs = Explode( ',', angStr, false ); + return index, Angle( tonumber( segs[1] ), tonumber( segs[2] ), tonumber( segs[3] ) ); + end + -- ENTITY + decode[ 'E' ] = function( self, index, str, cache ) + if( str[index] == '#' )then + index = index + 1; + return NULL ; + else + local finish = find( str, ';', index, true ); + local num = tonumber( sub( str, index, finish - 1 ) ); + index = finish + 1; + return index, Entity( num ); + end + end + -- PLAYER + decode[ 'P' ] = function( self, index, str, cache ) + local finish = find( str, ';', index, true ); + local num = tonumber( sub( str, index, finish - 1 ) ); + index = finish + 1; + return index, Entity( num ) or NULL; + end + + function pon.decode( data ) + local _, res = decode[sub(data,1,1)]( decode, 2, data, {}); + return res; + end +end diff --git a/gamemode/server/von.lua b/gamemode/server/von.lua new file mode 100644 index 0000000..9d118c0 --- /dev/null +++ b/gamemode/server/von.lua @@ -0,0 +1,801 @@ +--[[ vON 1.3.4 + Copyright 2012-2014 Alexandru-Mihai Maftei + aka Vercas + GitHub Repository: + https://github.com/vercas/vON + You may use this for any purpose as long as: + - You don't remove this copyright notice. + - You don't claim this to be your own. + - You properly credit the author (Vercas) if you publish your work based on (and/or using) this. + If you modify the code for any purpose, the above obligations still apply. + If you make any interesting modifications, try forking the GitHub repository instead. + Instead of copying this code over for sharing, rather use the link: + https://github.com/vercas/vON/blob/master/von.lua + The author may not be held responsible for any damage or losses directly or indirectly caused by + the use of vON. + If you disagree with the above, don't use the code. +----------------------------------------------------------------------------------------------------------------------------- + + Thanks to the following people for their contribution: + - Divran Suggested improvements for making the code quicker. + Suggested an excellent new way of deserializing strings. + Lead me to finding an extreme flaw in string parsing. + - pennerlord Provided some performance tests to help me improve the code. + - Chessnut Reported bug with handling of nil values when deserializing array components. + - People who contributed on the GitHub repository by reporting bugs, posting fixes, etc. +----------------------------------------------------------------------------------------------------------------------------- + + The vanilla types supported in this release of vON are: + - table + - number + - boolean + - string + - nil + The Garry's Mod-specific types supported in this release are: + - Vector + - Angle + + Entities: + - Entity + - Vehicle + - Weapon + - NPC + - Player + - NextBot + These are the types one would normally serialize. +----------------------------------------------------------------------------------------------------------------------------- + + New in this version: + - Fixed addition of extra entity types. I messed up really badly. +--]] + + + +local _deserialize, _serialize, _d_meta, _s_meta, d_findVariable, s_anyVariable +local sub, gsub, find, insert, concat, error, tonumber, tostring, type, next = string.sub, string.gsub, string.find, table.insert, table.concat, error, tonumber, tostring, type, next + + + +--[[ This section contains localized functions which (de)serialize + variables according to the types found. ]] + + + +-- This is kept away from the table for speed. +function d_findVariable(s, i, len, lastType, jobstate) + local i, c, typeRead, val = i or 1 + + -- Keep looping through the string. + while true do + -- Stop at the end. Throw an error. This function MUST NOT meet the end! + if i > len then + error("vON: Reached end of string, cannot form proper variable.") + end + + -- Cache the character. Nobody wants to look for the same character ten times. + c = sub(s, i, i) + + -- If it just read a type definition, then a variable HAS to come after it. + if typeRead then + -- Attempt to deserialize a variable of the freshly read type. + val, i = _deserialize[lastType](s, i, len, false, jobstate) + -- Return the value read, the index of the last processed character, and the type of the last read variable. + return val, i, lastType + + -- @ means nil. It should not even appear in the output string of the serializer. Nils are useless to store. + elseif c == "@" then + return nil, i, lastType + + -- $ means a table reference will follow - a number basically. + elseif c == "$" then + lastType = "table_reference" + typeRead = true + + -- n means a number will follow. Base 10... :C + elseif c == "n" then + lastType = "number" + typeRead = true + + -- b means boolean flags. + elseif c == "b" then + lastType = "boolean" + typeRead = true + + -- ' means the start of a string. + elseif c == "'" then + lastType = "string" + typeRead = true + + -- " means the start of a string prior to version 1.2.0. + elseif c == "\"" then + lastType = "oldstring" + typeRead = true + + -- { means the start of a table! + elseif c == "{" then + lastType = "table" + typeRead = true + + +--[[ Garry's Mod types go here ]] + + -- e means an entity ID will follow. + elseif c == "e" then + lastType = "Entity" + typeRead = true +--[[ + -- c means a vehicle ID will follow. + elseif c == "c" then + lastType = "Vehicle" + typeRead = true + -- w means a weapon entity ID will follow. + elseif c == "w" then + lastType = "Weapon" + typeRead = true + -- x means a NPC ID will follow. + elseif c == "x" then + lastType = "NPC" + typeRead = true +--]] + -- p means a player ID will follow. + -- Kept for backwards compatibility. + elseif c == "p" then + lastType = "Entity" + typeRead = true + + -- v means a vector will follow. 3 numbers. + elseif c == "v" then + lastType = "Vector" + typeRead = true + + -- a means an Euler angle will follow. 3 numbers. + elseif c == "a" then + lastType = "Angle" + typeRead = true + +--[[ Garry's Mod types end here ]] + + + -- If no type has been found, attempt to deserialize the last type read. + elseif lastType then + val, i = _deserialize[lastType](s, i, len, false, jobstate) + return val, i, lastType + + -- This will occur if the very first character in the vON code is wrong. + else + error("vON: Malformed data... Can't find a proper type definition. Char#" .. i .. ":" .. c) + end + + -- Move the pointer one step forward. + i = i + 1 + end +end + +-- This is kept away from the table for speed. +-- Yeah, ton of parameters. +function s_anyVariable(data, lastType, isNumeric, isKey, isLast, jobstate) + local tp = type(data) + + if jobstate[1] and jobstate[2][data] then + tp = "table_reference" + end + + -- Basically, if the type changes. + if lastType ~= tp then + -- Remember the new type. Caching the type is useless. + lastType = tp + + if _serialize[lastType] then + -- Return the serialized data and the (new) last type. + -- The second argument, which is true now, means that the data type was just changed. + return _serialize[lastType](data, true, isNumeric, isKey, isLast, false, jobstate), lastType + else + error("vON: No serializer defined for type \"" .. lastType .. "\"!") + end + end + + -- Otherwise, simply serialize the data. + return _serialize[lastType](data, false, isNumeric, isKey, isLast, false, jobstate), lastType +end + + + +--[[ This section contains the tables with the functions necessary + for decoding basic Lua data types. ]] + + + +_deserialize = { +-- Well, tables are very loose... +-- The first table doesn't have to begin and end with { and }. + ["table"] = function(s, i, len, unnecessaryEnd, jobstate) + local ret, numeric, i, c, lastType, val, ind, expectValue, key = {}, true, i or 1, nil, nil, nil, 1 + -- Locals, locals, locals, locals, locals, locals, locals, locals and locals. + + if sub(s, i, i) == "#" then + local e = find(s, "#", i + 2, true) + + if e then + local id = tonumber(sub(s, i + 1, e - 1)) + + if id then + if jobstate[1][id] and not jobstate[2] then + error("vON: There already is a table of reference #" .. id .. "! Missing an option maybe?") + end + + jobstate[1][id] = ret + + i = e + 1 + else + error("vON: Malformed table! Reference ID starting at char #" .. i .. " doesn't contain a number!") + end + else + error("vON: Malformed table! Cannot find end of reference ID start at char #" .. i .. "!") + end + end + + -- Keep looping. + while true do + -- Until it meets the end. + if i > len then + -- Yeah, if the end is unnecessary, it won't spit an error. The main chunk doesn't require an end, for example. + if unnecessaryEnd then + return ret, i + + -- Otherwise, the data has to be damaged. + else + error("vON: Reached end of string, incomplete table definition.") + end + end + + -- Cache the character. + c = sub(s, i, i) + --print(i, "table char:", c, tostring(unnecessaryEnd)) + + -- If it's the end of a table definition, return. + if c == "}" then + return ret, i + + -- If it's the component separator, switch to key:value pairs. + elseif c == "~" then + numeric = false + + elseif c == ";" then + -- Lol, nothing! + -- Remenant from numbers, for faster parsing. + + -- OK, now, if it's on the numeric component, simply add everything encountered. + elseif numeric then + -- Find a variable and it's value + val, i, lastType = d_findVariable(s, i, len, lastType, jobstate) + -- Add it to the table. + ret[ind] = val + + ind = ind + 1 + + -- Otherwise, if it's the key:value component... + else + -- If a value is expected... + if expectValue then + -- Read it. + val, i, lastType = d_findVariable(s, i, len, lastType, jobstate) + -- Add it? + ret[key] = val + -- Clean up. + expectValue, key = false, nil + + -- If it's the separator... + elseif c == ":" then + -- Expect a value next. + expectValue = true + + -- But, if there's a key read already... + elseif key then + -- Then this is malformed. + error("vON: Malformed table... Two keys declared successively? Char#" .. i .. ":" .. c) + + -- Otherwise the key will be read. + else + -- I love multi-return and multi-assignement. + key, i, lastType = d_findVariable(s, i, len, lastType, jobstate) + end + end + + i = i + 1 + end + + return nil, i + end, + +-- Just a number which points to a table. + ["table_reference"] = function(s, i, len, unnecessaryEnd, jobstate) + local i, a = i or 1 + -- Locals, locals, locals, locals + + a = find(s, "[;:}~]", i) + + if a then + local n = tonumber(sub(s, i, a - 1)) + + if n then + return jobstate[1][n] or error("vON: Table reference does not point to a (yet) known table!"), a - 1 + else + error("vON: Table reference definition does not contain a valid number!") + end + end + + -- Using %D breaks identification of negative numbers. :( + + error("vON: Number definition started... Found no end.") + end, + + +-- Numbers are weakly defined. +-- The declaration is not very explicit. It'll do it's best to parse the number. +-- Has various endings: \n, }, ~, : and ;, some of which will force the table deserializer to go one char backwards. + ["number"] = function(s, i, len, unnecessaryEnd, jobstate) + local i, a = i or 1 + -- Locals, locals, locals, locals + + a = find(s, "[;:}~]", i) + + if a then + return tonumber(sub(s, i, a - 1)) or error("vON: Number definition does not contain a valid number!"), a - 1 + end + + -- Using %D breaks identification of negative numbers. :( + + error("vON: Number definition started... Found no end.") + end, + + +-- A boolean is A SINGLE CHARACTER, either 1 for true or 0 for false. +-- Any other attempt at boolean declaration will result in a failure. + ["boolean"] = function(s, i, len, unnecessaryEnd, jobstate) + local c = sub(s,i,i) + -- Only one character is needed. + + -- If it's 1, then it's true + if c == "1" then + return true, i + + -- If it's 0, then it's false. + elseif c == "0" then + return false, i + end + + -- Any other supposely "boolean" is just a sign of malformed data. + error("vON: Invalid value on boolean type... Char#" .. i .. ": " .. c) + end, + + +-- Strings prior to 1.2.0 + ["oldstring"] = function(s, i, len, unnecessaryEnd, jobstate) + local res, i, a = "", i or 1 + -- Locals, locals, locals, locals + + while true do + a = find(s, "\"", i, true) + + if a then + if sub(s, a - 1, a - 1) == "\\" then + res = res .. sub(s, i, a - 2) .. "\"" + i = a + 1 + else + return res .. sub(s, i, a - 2), a + end + else + error("vON: Old string definition started... Found no end.") + end + end + end, + +-- Strings after 1.2.0 + ["string"] = function(s, i, len, unnecessaryEnd, jobstate) + local res, i, a = "", i or 1 + -- Locals, locals, locals, locals + + while true do + a = find(s, "\"", i, true) + + if a then + if sub(s, a - 1, a - 1) == "\\" then + res = res .. sub(s, i, a - 2) .. "\"" + i = a + 1 + else + return res .. sub(s, i, a - 1), a + end + else + error("vON: String definition started... Found no end.") + end + end + end, +} + + + +_serialize = { +-- Uh. Nothing to comment. +-- Ton of parameters. +-- Makes stuff faster than simply passing it around in locals. +-- table.concat works better than normal concatenations WITH LARGE-ISH STRINGS ONLY. + ["table"] = function(data, mustInitiate, isNumeric, isKey, isLast, first, jobstate) + --print(string.format("data: %s; mustInitiate: %s; isKey: %s; isLast: %s; nice: %s; indent: %s; first: %s", tostring(data), tostring(mustInitiate), tostring(isKey), tostring(isLast), tostring(nice), tostring(indent), tostring(first))) + + local result, keyvals, len, keyvalsLen, keyvalsProgress, val, lastType, newIndent, indentString = {}, {}, #data, 0, 0 + -- Locals, locals, locals, locals, locals, locals, locals, locals, locals and locals. + + -- First thing to be done is separate the numeric and key:value components of the given table in two tables. + -- pairs(data) is slower than next, data as far as my tests tell me. + for k, v in next, data do + -- Skip the numeric keyz. + if type(k) ~= "number" or k < 1 or k > len or (k % 1 ~= 0) then -- k % 1 == 0 is, as proven by personal benchmarks, + keyvals[#keyvals + 1] = k -- the quickest way to check if a number is an integer. + end -- k % 1 ~= 0 is the fastest way to check if a number + end -- is NOT an integer. > is proven slower. + + keyvalsLen = #keyvals + + -- Main chunk - no initial character. + if not first then + result[#result + 1] = "{" + end + + if jobstate[1] and jobstate[1][data] then + if jobstate[2][data] then + error("vON: Table #" .. jobstate[1][data] .. " written twice..?") + end + + result[#result + 1] = "#" + result[#result + 1] = jobstate[1][data] + result[#result + 1] = "#" + + jobstate[2][data] = true + end + + -- Add numeric values. + if len > 0 then + for i = 1, len do + val, lastType = s_anyVariable(data[i], lastType, true, false, i == len and not first, jobstate) + result[#result + 1] = val + end + end + + -- If there are key:value pairs. + if keyvalsLen > 0 then + -- Insert delimiter. + result[#result + 1] = "~" + + -- Insert key:value pairs. + for _i = 1, keyvalsLen do + keyvalsProgress = keyvalsProgress + 1 + + val, lastType = s_anyVariable(keyvals[_i], lastType, false, true, false, jobstate) + + result[#result + 1] = val..":" + + val, lastType = s_anyVariable(data[keyvals[_i]], lastType, false, false, keyvalsProgress == keyvalsLen and not first, jobstate) + + result[#result + 1] = val + end + end + + -- Main chunk needs no ending character. + if not first then + result[#result + 1] = "}" + end + + return concat(result) + end, + +-- Number which points to table. + ["table_reference"] = function(data, mustInitiate, isNumeric, isKey, isLast, first, jobstate) + data = jobstate[1][data] + + -- If a number hasn't been written before, add the type prefix. + if mustInitiate then + if isKey or isLast then + return "$"..data + else + return "$"..data..";" + end + end + + if isKey or isLast then + return data + else + return data..";" + end + end, + + +-- Normal concatenations is a lot faster with small strings than table.concat +-- Also, not so branched-ish. + ["number"] = function(data, mustInitiate, isNumeric, isKey, isLast, first, jobstate) + -- If a number hasn't been written before, add the type prefix. + if mustInitiate then + if isKey or isLast then + return "n"..data + else + return "n"..data..";" + end + end + + if isKey or isLast then + return data + else + return data..";" + end + end, + + +-- I hope gsub is fast enough. + ["string"] = function(data, mustInitiate, isNumeric, isKey, isLast, first, jobstate) + if sub(data, #data, #data) == "\\" then -- Hah, old strings fix this best. + return "\"" .. gsub(data, "\"", "\\\"") .. "v\"" + end + + return "'" .. gsub(data, "\"", "\\\"") .. "\"" + end, + + +-- Fastest. + ["boolean"] = function(data, mustInitiate, isNumeric, isKey, isLast, first, jobstate) + -- Prefix if we must. + if mustInitiate then + if data then + return "b1" + else + return "b0" + end + end + + if data then + return "1" + else + return "0" + end + end, + + +-- Fastest. + ["nil"] = function(data, mustInitiate, isNumeric, isKey, isLast, first, jobstate) + return "@" + end, +} + + + +--[[ This section handles additions necessary for Garry's Mod. ]] + + + +if gmod then -- Luckily, a specific table named after the game is present in Garry's Mod. + local Entity = Entity + + + + local extra_deserialize = { +-- Entities are stored simply by the ID. They're meant to be transfered, not stored anyway. +-- Exactly like a number definition, except it begins with "e". + ["Entity"] = function(s, i, len, unnecessaryEnd, jobstate) + local i, a = i or 1 + -- Locals, locals, locals, locals + + a = find(s, "[;:}~]", i) + + if a then + return Entity(tonumber(sub(s, i, a - 1))), a - 1 + end + + error("vON: Entity ID definition started... Found no end.") + end, + + +-- A pair of 3 numbers separated by a comma (,). + ["Vector"] = function(s, i, len, unnecessaryEnd, jobstate) + local i, a, x, y, z = i or 1 + -- Locals, locals, locals, locals + + a = find(s, ",", i) + + if a then + x = tonumber(sub(s, i, a - 1)) + i = a + 1 + end + + a = find(s, ",", i) + + if a then + y = tonumber(sub(s, i, a - 1)) + i = a + 1 + end + + a = find(s, "[;:}~]", i) + + if a then + z = tonumber(sub(s, i, a - 1)) + end + + if x and y and z then + return Vector(x, y, z), a - 1 + end + + error("vON: Vector definition started... Found no end.") + end, + + +-- A pair of 3 numbers separated by a comma (,). + ["Angle"] = function(s, i, len, unnecessaryEnd, jobstate) + local i, a, p, y, r = i or 1 + -- Locals, locals, locals, locals + + a = find(s, ",", i) + + if a then + p = tonumber(sub(s, i, a - 1)) + i = a + 1 + end + + a = find(s, ",", i) + + if a then + y = tonumber(sub(s, i, a - 1)) + i = a + 1 + end + + a = find(s, "[;:}~]", i) + + if a then + r = tonumber(sub(s, i, a - 1)) + end + + if p and y and r then + return Angle(p, y, r), a - 1 + end + + error("vON: Angle definition started... Found no end.") + end, + } + + local extra_serialize = { +-- Same as numbers, except they start with "e" instead of "n". + ["Entity"] = function(data, mustInitiate, isNumeric, isKey, isLast, first, jobstate) + data = data:EntIndex() + + if mustInitiate then + if isKey or isLast then + return "e"..data + else + return "e"..data..";" + end + end + + if isKey or isLast then + return data + else + return data..";" + end + end, + + +-- 3 numbers separated by a comma. + ["Vector"] = function(data, mustInitiate, isNumeric, isKey, isLast, first, jobstate) + if mustInitiate then + if isKey or isLast then + return "v"..data.x..","..data.y..","..data.z + else + return "v"..data.x..","..data.y..","..data.z..";" + end + end + + if isKey or isLast then + return data.x..","..data.y..","..data.z + else + return data.x..","..data.y..","..data.z..";" + end + end, + + +-- 3 numbers separated by a comma. + ["Angle"] = function(data, mustInitiate, isNumeric, isKey, isLast, first, jobstate) + if mustInitiate then + if isKey or isLast then + return "a"..data.p..","..data.y..","..data.r + else + return "a"..data.p..","..data.y..","..data.r..";" + end + end + + if isKey or isLast then + return data.p..","..data.y..","..data.r + else + return data.p..","..data.y..","..data.r..";" + end + end, + } + + for k, v in pairs(extra_serialize) do + _serialize[k] = v + end + + for k, v in pairs(extra_deserialize) do + _deserialize[k] = v + end + + local extraEntityTypes = { "Vehicle", "Weapon", "NPC", "Player", "NextBot" } + + for i = 1, #extraEntityTypes do + _serialize[extraEntityTypes[i]] = _serialize.Entity + end +end + + + +--[[ This section exposes the functions of the library. ]] + + + +local function checkTableForRecursion(tab, checked, assoc) + local id = checked.ID + + if not checked[tab] and not assoc[tab] then + assoc[tab] = id + checked.ID = id + 1 + else + checked[tab] = true + end + + for k, v in pairs(tab) do + if type(k) == "table" and not checked[k] then + checkTableForRecursion(k, checked, assoc) + end + + if type(v) == "table" and not checked[v] then + checkTableForRecursion(v, checked, assoc) + end + end +end + + + +local _s_table = _serialize.table +local _d_table = _deserialize.table + +_d_meta = { + __call = function(self, str, allowIdRewriting) + if type(str) == "string" then + return _d_table(str, nil, #str, true, {{}, allowIdRewriting}) + end + + error("vON: You must deserialize a string, not a "..type(str)) + end +} +_s_meta = { + __call = function(self, data, checkRecursion) + if type(data) == "table" then + if checkRecursion then + local assoc, checked = {}, {ID = 1} + + checkTableForRecursion(data, checked, assoc) + + return _s_table(data, nil, nil, nil, nil, true, {assoc, {}}) + end + + return _s_table(data, nil, nil, nil, nil, true, {false}) + end + + error("vON: You must serialize a table, not a "..type(data)) + end +} + + + +von = { + version = "1.3.4", + versionNumber = 1003004, -- Reserving 3 digits per version component. + + deserialize = setmetatable(_deserialize,_d_meta), + serialize = setmetatable(_serialize,_s_meta) +} + +return von |
