├── README.md ├── examples ├── cinemactrl.lua ├── ping.lua ├── shoutbox.lua └── shoutbox_simple.lua └── lua └── autorun └── nettable.lua /README.md: -------------------------------------------------------------------------------- 1 | # gmod-nettable 2 | 3 | Somewhat efficient but painless table networking for Garry's Mod. 4 | 5 | ## Features 6 | - Nettables are identified by a string id on server and client 7 | - ```net.WriteTable``` used by default, but specifying a protocol string to massively reduce payload size is first class supported 8 | - on-demand data requesting (no need to mess with _PlayerInitSpawn_ hook, data is sent automatically when client first requests it) 9 | - Only sends changed data (using a table delta algorithm) 10 | - No table depth limits 11 | 12 | ## Commented example 13 | ```lua 14 | -- Creates a reference to 'shoutboxs' nettable on both SERVER and CLIENT. Nettables 15 | -- with same IDs are connected on SERVER and CLIENT; in this case 'shoutboxs'. 16 | -- 17 | -- 'proto' string is used to make nettable send binary data instead of slightly 18 | -- inefficient net.WriteTable it is optional, so this example would work just 19 | -- as fine if the 'proto' string was removed 20 | -- 21 | -- Note: this example stores shouts in 'msgs' subtable instead of the nettable 22 | -- itself, because protocol strings do not currently support the whole nettable 23 | -- as an array. If 'proto' was removed, shouts could be stored directly in nettable. 24 | local t = nettable.get("shoutboxs", {proto = "[str]:msgs"}) 25 | 26 | if SERVER then 27 | concommand.Add("shouts", function(ply, _, _, raw) 28 | -- Nettable is internally a normal table, so you can use table functions on it 29 | t.msgs = t.msgs or {} 30 | table.insert(t.msgs, raw) 31 | 32 | -- Calling 'commit' sends updates to all players if no 'filter' is specified 33 | nettable.commit(t) 34 | end) 35 | end 36 | if CLIENT then 37 | hook.Add("HUDPaint", "Test", function() 38 | -- Again, nettable is a normal table so it can be iterated normally 39 | for i,v in pairs(t.msgs or {}) do 40 | draw.SimpleText(v, "DermaDefaultBold", 100, 100 + i*15) 41 | end 42 | end) 43 | end 44 | ``` 45 | 46 | See ![examples](examples/) for more examples. 47 | 48 | ## Filters 49 | If specific nettable's data should only be sent to some players, you can pass a filter function in ```opts``` to ```nettable.get```. 50 | ```lua 51 | local secretTable = nettable.get("secret", {filter = function(ply) return ply:IsSuperAdmin() end}) 52 | secretTable.password = "hunter2" 53 | nettable.commit("secret") 54 | ``` 55 | 56 | ## Protocol strings 57 | Protocol strings are a way of specifying what datatypes are sent and in which order. This allows sending only the data instead of numerous headers, type ids and other bloat. 58 | 59 | ### Constraints 60 | - The data types and their order must be exactly the same on both server and client. Names don't have to be the same on both realms, but should be the same to prevent confusion. 61 | - If a nettable key changes, and the key is not specified in the protocol string, it won't be committed. This can be used to advantage to prevent sending some values to clients, but should not be used as a foolproof security measure. 62 | 63 | ### Example 64 | A protocol string is a space separated string containing data types and the associated key names. 65 | 66 | Following table assumes that a nettable is defined as ```local nt = nettable.get("test")``` 67 | 68 | Protocol string | NetTable structure 69 | -----|------ 70 | ```u8:age str:name``` | an unsigned byte for ```nt.age``` and a string for ```nt.name```. 71 | ```{f32:duration str:title}:curmedia``` | a float for ```nt.curmedia.duration``` and a string for ```nt.curmedia.title``` 72 | ```[{str:author str:title}]:mediaqueue``` | for each entry in ```nt.mediaqueue``` array: a string for ```author``` and for ```title``` 73 | 74 | ### Protocol string data types 75 | 76 | Type | Explanation 77 | ------------- | ------------- 78 | int | [VLE](https://en.wikipedia.org/wiki/Variable-length_quantity) integer (use this for arbitrary size integers) 79 | u8/u16/u32 | 8/16/32 bit unsigned integer 80 | i8/i16/i32 | 8/16/32 bit signed integer 81 | f32 | A float 82 | f64 | A double 83 | bool | A boolean 84 | str | A string 85 | [] | An array 86 | {} | A subtable 87 | ply | Garry's Mod: Player 88 | ent | Garry's Mod: Entity 89 | vec | Garry's Mod: Vector 90 | ang | Garry's Mod: Angle 91 | color | Garry's Mod: Color -------------------------------------------------------------------------------- /examples/cinemactrl.lua: -------------------------------------------------------------------------------- 1 | -- Create the nettable that will hold all our cinema data 2 | local data = nettable.get("CinemaData") 3 | data.cinemas = {} 4 | 5 | -- Create a Cinema metatable that will be used for all entries in `cinemas` 6 | local Cinema = {} 7 | Cinema.__index = Cinema 8 | 9 | -- We create a helper method to update the nettable with our cinema data 10 | function Cinema:Update() 11 | nettable.commit(data) 12 | end 13 | 14 | -- Playing a video happens by setting a `cur` field in our cinema instance. We could send a net message or whatever as well 15 | function Cinema:Play(url) 16 | self.cur = {url = url, startTime = CurTime()} 17 | self:Update() 18 | end 19 | 20 | -- Returns how many seconds were elapsed from the start of the video 21 | function Cinema:GetElapsed() 22 | if not self.cur then return 0 end 23 | return CurTime() - self.cur.startTime 24 | end 25 | 26 | -- Create an API using global functions because that's how cool we are 27 | 28 | -- Creates a new Cinema with given id, adds it to nettable and commits it's changes to everyone 29 | function AddCinema(id) 30 | local ci = setmetatable({}, Cinema) 31 | 32 | data.cinemas[id] = ci 33 | nettable.commit(data) 34 | 35 | return ci 36 | end 37 | 38 | concommand.Add("cctrl_add", function(ply, cmd, args) 39 | local ci = AddCinema(args[1]) 40 | 41 | -- Play some Modeselektor like the hipsters we are 42 | ci:Play("https://www.youtube.com/watch?v=3YHBFmMMECg") 43 | 44 | timer.Simple(2, function() 45 | print("Elapsed time hath: ", ci:GetElapsed(), " for video ", ci.cur.url) 46 | end) 47 | end) 48 | -------------------------------------------------------------------------------- /examples/ping.lua: -------------------------------------------------------------------------------- 1 | local t = nettable.get("pings", {proto = "f32:time ply:user"}) 2 | 3 | if SERVER then 4 | concommand.Add("svping", function(ply) 5 | t.time = CurTime() 6 | t.user = ply 7 | nettable.commit(t) 8 | end) 9 | concommand.Add("clrping", function(ply) 10 | t.time = nil 11 | t.user = nil 12 | nettable.commit(t) 13 | end) 14 | end 15 | if CLIENT then 16 | hook.Add("HUDPaint", "Test", function() 17 | draw.SimpleText(string.format("Last ping by %s at %f", (IsValid(t.user) and t.user:Nick() or "NULL"), t.time or -1), "DermaLarge", 100, 100) 18 | end) 19 | end -------------------------------------------------------------------------------- /examples/shoutbox.lua: -------------------------------------------------------------------------------- 1 | local t = nettable.get("shoutbox", {proto = "[{str:msg ply:ply}]:msgs"}) 2 | 3 | if SERVER then 4 | concommand.Add("shout", function(ply, _, _, raw) 5 | t.msgs = t.msgs or {} 6 | table.insert(t.msgs, {ply = ply, msg = raw}) 7 | nettable.commit(t) 8 | end) 9 | concommand.Add("clearshouts", function(ply, _, _, raw) 10 | t.msgs = {} 11 | nettable.commit(t) 12 | end) 13 | end 14 | if CLIENT then 15 | hook.Add("HUDPaint", "Test", function() 16 | for i,v in pairs(t.msgs or {}) do 17 | local clr, name = Color(127, 127, 127), "NULL" 18 | if IsValid(v.ply) then 19 | clr = team.GetColor(v.ply) 20 | name = v.ply:Nick() 21 | end 22 | 23 | draw.SimpleText(name .. ":", "DermaDefaultBold", 10, 100 + i*15, clr) 24 | draw.SimpleText(v.msg, "DermaDefaultBold", 150, 100 + i*15) 25 | end 26 | end) 27 | end -------------------------------------------------------------------------------- /examples/shoutbox_simple.lua: -------------------------------------------------------------------------------- 1 | local t = nettable.get("shoutboxs", {proto = "[str]:msgs"}) 2 | 3 | if SERVER then 4 | concommand.Add("shouts", function(ply, _, _, raw) 5 | t.msgs = t.msgs or {} 6 | table.insert(t.msgs, raw) 7 | nettable.commit(t) 8 | end) 9 | end 10 | if CLIENT then 11 | hook.Add("HUDPaint", "Test", function() 12 | for i,v in pairs(t.msgs or {}) do 13 | draw.SimpleText(v, "DermaDefaultBold", 100, 100 + i*15) 14 | end 15 | end) 16 | end -------------------------------------------------------------------------------- /lua/autorun/nettable.lua: -------------------------------------------------------------------------------- 1 | nettableproto = nettableproto or {} 2 | 3 | local function curry(f) 4 | return function (x) return function (y) return f(x,y) end end 5 | end 6 | local function curry_reverse(f) 7 | return function (y) return function (x) return f(x,y) end end 8 | end 9 | 10 | local lshift, rshift, arshift, band, bor, bxor = bit.lshift, bit.rshift, bit.arshift, bit.band, bit.bor, bit.bxor 11 | 12 | nettableproto.typeHandlers = { 13 | ["u8"] = { read = curry(net.ReadUInt)(8), write = curry_reverse(net.WriteUInt)(8) }, 14 | ["u16"] = { read = curry(net.ReadUInt)(16), write = curry_reverse(net.WriteUInt)(16) }, 15 | ["u32"] = { read = curry(net.ReadUInt)(32), write = curry_reverse(net.WriteUInt)(32) }, 16 | 17 | ["i8"] = { read = curry(net.ReadInt)(8), write = curry_reverse(net.WriteInt)(8) }, 18 | ["i16"] = { read = curry(net.ReadInt)(16), write = curry_reverse(net.WriteInt)(16) }, 19 | ["i32"] = { read = curry(net.ReadInt)(32), write = curry_reverse(net.WriteInt)(32) }, 20 | 21 | -- Variable Length Encoded integer 22 | ["int"] = { 23 | read = function() 24 | local ret = 0 25 | 26 | local readBytes = 0 27 | for i=0,5 do 28 | readBytes = readBytes + 1 29 | 30 | local b = net.ReadUInt(8) 31 | ret = bor(ret, lshift(band(b, 0x7F), 7*i)) 32 | if band(b, 0x80) ~= 0x80 then 33 | break 34 | end 35 | end 36 | 37 | nettable.debug("[Type:VarInt] Read ", readBytes, " varint bytes") 38 | return ret 39 | end, 40 | write = function(value) 41 | local writtenBytes = 1 42 | 43 | while rshift(value, 7) ~= 0 do 44 | net.WriteUInt(bor(band(value, 0x7F), 0x80), 8) 45 | value = rshift(value, 7) 46 | 47 | writtenBytes = writtenBytes + 1 48 | end 49 | net.WriteUInt(band(value, 0x7F), 8) 50 | 51 | nettable.debug("[Type:VarInt] Wrote ", writtenBytes, " varint bytes") 52 | end, 53 | }, 54 | 55 | ["f32"] = { read = net.ReadFloat, write = net.WriteFloat }, 56 | ["f64"] = { read = net.ReadDouble, write = net.WriteDouble }, 57 | 58 | ["bool"] = { read = net.ReadBool, write = net.WriteBool }, 59 | 60 | ["array"] = { 61 | read = function(data) 62 | local size = nettableproto.typeHandlers.int.read() 63 | nettable.debug("[Type:Array] Reading array of size ", size) 64 | 65 | local arr = {} 66 | for i=1,size do 67 | local key = nettableproto.typeHandlers.int.read() 68 | local val = data.type.handler.read(data.type.data) 69 | 70 | nettable.debug("[Type:Table] Setting key ", key, " to value of type ", data.type.type) 71 | 72 | arr[key] = val 73 | end 74 | 75 | return arr 76 | end, 77 | write = function(arr, data) 78 | nettableproto.typeHandlers.int.write(table.Count(arr)) 79 | 80 | for k,v in pairs(arr) do 81 | nettableproto.typeHandlers.int.write(k) 82 | data.type.handler.write(v, data.type.data) 83 | end 84 | end, 85 | 86 | readDeletion = function(data) 87 | local size = nettableproto.typeHandlers.int.read() 88 | nettable.debug("[Type:Array-del] Reading keysArray of size ", size) 89 | 90 | local del = {} 91 | for i=1, size do 92 | local key = nettableproto.typeHandlers.int.read() 93 | del[key] = true 94 | end 95 | return del 96 | end, 97 | writeDeletion = function(del, data) 98 | nettableproto.typeHandlers.int.write(table.Count(del)) 99 | 100 | for k,val in pairs(del) do 101 | nettableproto.typeHandlers.int.write(k) 102 | if val ~= true then 103 | nettable.warn("[Type:Array-del] key ", k, " was not deleted as 'true'!!") 104 | end 105 | end 106 | 107 | nettable.debug("[Type:Array-del] Writing keysArray of size ", table.Count(del)) 108 | end, 109 | }, 110 | ["table"] = { 111 | read = function(data) 112 | local tbl = {} 113 | for _,field in ipairs(data.fields) do 114 | local name = field.name 115 | 116 | local is_provided = net.ReadBool() 117 | nettable.debug("[Type:Table] Reading ", name, " (provided: ", is_provided, ")") 118 | if is_provided then 119 | tbl[name] = field.handler.read(field.data) 120 | end 121 | end 122 | return tbl 123 | end, 124 | write = function(tbl, data) 125 | for _,field in ipairs(data.fields) do 126 | local name = field.name 127 | 128 | local val = tbl[name] 129 | local is_provided = val ~= nil 130 | 131 | net.WriteBool(is_provided) 132 | if is_provided then 133 | field.handler.write(val, field.data) 134 | end 135 | end 136 | end, 137 | 138 | readDeletion = function(data) 139 | local bitfield = nettableproto.typeHandlers.int.read() 140 | nettable.debug("[Type:Table-del] Reading deletedBitfield ", bitfield) 141 | 142 | local del = {} 143 | for i=1, #data.fields do 144 | local _bit = lshift(1, i) 145 | if band(bitfield, _bit) == _bit then 146 | del[data.fields[i].name] = true 147 | end 148 | end 149 | return del 150 | end, 151 | writeDeletion = function(del, data) 152 | local bitfield = 0 153 | 154 | for i,field in ipairs(data.fields) do 155 | if del[field.name] then 156 | bitfield = bor(bitfield, lshift(1, i)) 157 | end 158 | end 159 | 160 | nettableproto.typeHandlers.int.write(bitfield) 161 | nettable.debug("[Type:Table-del] Writing deletedBitfield ", bitfield) 162 | end, 163 | }, 164 | 165 | ["str"] = { read = net.ReadString, write = net.WriteString }, 166 | 167 | ["ply"] = { read = net.ReadEntity, write = net.WriteEntity }, 168 | ["ent"] = { read = net.ReadEntity, write = net.WriteEntity }, 169 | 170 | ["vec"] = { read = net.ReadVector, write = net.WriteVector }, 171 | ["ang"] = { read = net.ReadAngle, write = net.WriteAngle }, 172 | ["color"] = { read = net.ReadColor, write = net.WriteColor }, 173 | } 174 | 175 | function nettableproto.compile(str) 176 | local fields = {} 177 | 178 | local function addtype(typestr, data) 179 | local handler = nettableproto.typeHandlers[typestr] 180 | if not handler then 181 | error("No type handler for type '" .. typestr .. "'") 182 | return 183 | end 184 | 185 | local tbl = { 186 | type = typestr, 187 | handler = handler, 188 | data = data 189 | } 190 | 191 | table.insert(fields, tbl) 192 | end 193 | 194 | local i = 1 195 | local function next() 196 | if i > #str then return false end 197 | 198 | local text = string.sub(str, i) 199 | local letter = string.sub(text, 1, 1) 200 | 201 | if string.match(letter, "%s") then 202 | i = i+1 203 | return true 204 | end 205 | 206 | if string.match(letter, "%a") then 207 | local typestr = string.match(text, "([%a%d]+)") 208 | 209 | addtype(typestr) 210 | 211 | i = i+#typestr 212 | return true 213 | end 214 | 215 | if letter == "{" then 216 | local contents = string.sub(string.match(text, "(%b{})"), 2, -2) 217 | 218 | local parsedContents = nettableproto.compile(contents) 219 | addtype("table", {fields = parsedContents}) 220 | 221 | i = i+#contents+2 222 | return true 223 | end 224 | 225 | if letter == "[" then 226 | local contents = string.sub(string.match(text, "(%b[])"), 2, -2) 227 | 228 | local parsedContents = nettableproto.compile(contents) 229 | if #parsedContents ~= 1 then 230 | return error("Only arrays with one type are supported!") 231 | end 232 | 233 | addtype("array", {type = parsedContents[1]}) 234 | 235 | i = i+#contents+2 236 | return true 237 | end 238 | 239 | if letter == ":" then 240 | local name = string.match(text, ":(%a+)") 241 | 242 | local lastType = fields[#fields] 243 | if not lastType then 244 | return error("Attempting to name a type while none have been created") 245 | end 246 | 247 | lastType.name = name 248 | 249 | i = i+1+#name 250 | return true 251 | end 252 | 253 | error("Invalid char in lexer: '" .. letter .. "'") 254 | end 255 | 256 | while next() do end 257 | 258 | return fields 259 | end 260 | 261 | nettable = nettable or {} 262 | nettable.__tables = nettable.__tables or {} 263 | nettable.__tablemeta = nettable.__tablemeta or {} -- note: stored by table reference instead of id 264 | 265 | nettable.__loaded = nettable.__loaded or false 266 | hook.Add("InitPostEntity", "NetTable_SetLoaded", function() nettable.__loaded = true end) 267 | 268 | local clr_white = Color(255, 255, 255) 269 | local clr_orange = Color(255, 127, 0) 270 | function nettable.log(...) 271 | MsgC(clr_white, "[NetTable] ", ...) 272 | end 273 | function nettable.error(err) 274 | error("[NetTable] " .. tostring(err or "Error")) 275 | end 276 | function nettable.warn(...) 277 | MsgC(clr_orange, "[NetTable-Warning] ", clr_white, ...) 278 | end 279 | 280 | local debug_cvar = CreateConVar("nettable_debug", "0", FCVAR_ARCHIVE) 281 | function nettable.debug(...) 282 | if not debug_cvar:GetBool() then return end 283 | print("[NetTable-D] ", ...) 284 | end 285 | 286 | -- Nettable ids are always strings, but sending them over and over again over network is expensive. 287 | -- This function can be used to convert the string id into eg. a CRC hash 288 | -- Needs to be same on both client and server 289 | nettable.id_hasher = { 290 | init = function(id, meta) 291 | if SERVER then 292 | util.AddNetworkString(id) 293 | meta._IdStringtabled = CurTime() 294 | end 295 | end, 296 | hash = function(id) 297 | return id 298 | end, 299 | 300 | read = function() 301 | local is_stringtable = net.ReadBool() 302 | 303 | if is_stringtable then 304 | local netid = nettableproto.typeHandlers.int.read() 305 | return util.NetworkIDToString(netid) 306 | else 307 | return net.ReadString() 308 | end 309 | end, 310 | write = function(id, meta) 311 | -- Time elapsed since id was added to stringtables. Used to make sure clients have actually received the string table id 312 | local elapsed = CurTime() - (meta._IdStringtabled or CurTime()) 313 | 314 | if elapsed >= 2 then 315 | net.WriteBool(true) 316 | nettableproto.typeHandlers.int.write(util.NetworkStringToID(id)) 317 | 318 | nettable.debug("Using stringid to write nettable id") 319 | else 320 | net.WriteBool(false) 321 | net.WriteString(id) 322 | 323 | nettable.debug("NOT Using stringid to write nettable id") 324 | end 325 | end, 326 | } 327 | -- Example CRC implementation 328 | --[[ 329 | nettable.id_hasher = { 330 | hash = function(id) 331 | return tonumber(util.CRC(id)) 332 | end, 333 | read = function() return net.ReadUInt(64) end, 334 | write = function(id) net.WriteUInt(id, 64) end, 335 | } 336 | ]] 337 | 338 | function nettable.resolveIdTblMeta(id) 339 | local tbl, meta 340 | if type(id) == "string" then 341 | id = nettable.id_hasher.hash(id) 342 | 343 | tbl = nettable.__tables[id] 344 | meta = nettable.__tablemeta[tbl] 345 | else 346 | tbl = id 347 | meta = nettable.__tablemeta[tbl] 348 | id = meta.id 349 | end 350 | 351 | return id, tbl, meta 352 | end 353 | 354 | function nettable.exists(id) 355 | id = nettable.id_hasher.hash(id) 356 | return nettable.__tables[id] ~= nil 357 | end 358 | 359 | nettable.OPT_ALREADY_HASHED = 1 -- id is passed as already hashed and should not be re-hashed 360 | nettable.OPT_DONT_CREATE = 2 -- don't create table if it does not exist 361 | nettable.OPT_AUTOCOMMIT = 4 -- if changes to table should be automatically commited using __newindex hook 362 | 363 | -- You can pass both a table of options or a bitmap to nettable.get, so we need a 364 | -- function to parse that 365 | local function hasOpt(opts, opt) 366 | if not opts then return false end 367 | 368 | local _type = type(opts) 369 | if _type == "number" then 370 | return bit.band(opts, opt) == opt 371 | end 372 | 373 | if _type == "table" then 374 | -- "idHashed" is obsolete but supported table option 375 | if opt == nettable.OPT_ALREADY_HASHED and (opts.idHashed or opts.idAlreadyHashed) then 376 | return true 377 | end 378 | 379 | if opt == nettable.OPT_DONT_CREATE and opts.dontCreate then 380 | return true 381 | end 382 | 383 | if opt == nettable.OPT_AUTOCOMMIT and opts.autoCommit then 384 | return true 385 | end 386 | end 387 | 388 | return false 389 | end 390 | 391 | function nettable.get(id, opts) 392 | local isOptsTable = type(opts) == "table" 393 | 394 | local origId = id 395 | 396 | if hasOpt(opts, nettable.OPT_ALREADY_HASHED) then 397 | id = nettable.id_hasher.hash(id) 398 | end 399 | 400 | local tbl_existed = true 401 | 402 | -- Create table if doesn't exist 403 | local tbl = nettable.__tables[id] 404 | if not tbl then 405 | -- Don't create if not wanted 406 | if hasOpt(opts, nettable.OPT_DONT_CREATE) then return nil end 407 | 408 | tbl = {} 409 | nettable.__tables[id] = tbl 410 | 411 | -- Auto commit functionality 412 | if SERVER and hasOpt(opts, nettable.OPT_AUTOCOMMIT) then 413 | local commitDelay = isOptsTable and opts.commitDelay or 0.1 414 | local function commit() 415 | if commitDelay <= 0 then 416 | nettable.commit(id) 417 | else 418 | timer.Create("NetTable.AutoCommit." .. id, commitDelay, 0, function() 419 | nettable.commit(id) 420 | end) 421 | end 422 | end 423 | 424 | local innerTbl = {} 425 | tbl._values = innerTbl 426 | setmetatable(tbl, { 427 | __index = function(t, key) return innerTbl[key] end, 428 | __newindex = function(t, key, val) 429 | innerTbl[key] = val 430 | commit() 431 | end 432 | }) 433 | end 434 | 435 | tbl_existed = false 436 | end 437 | 438 | -- Create tablemeta if doesn't exist 439 | local meta = nettable.__tablemeta[tbl] 440 | if not meta then 441 | meta = {} 442 | nettable.__tablemeta[tbl] = meta 443 | end 444 | 445 | if not tbl_existed then 446 | local initfn = nettable.id_hasher.init 447 | if initfn then initfn(id, meta) end 448 | end 449 | 450 | meta.id = id 451 | meta.origId = origId 452 | 453 | if isOptsTable and opts.filter then 454 | meta.filter = opts.filter 455 | end 456 | 457 | if isOptsTable and opts.proto then 458 | local compiled = nettableproto.compile(opts.proto) 459 | meta.proto = compiled 460 | end 461 | 462 | -- If nettable didn't exist, request a full update from server 463 | if CLIENT and not tbl_existed then 464 | local function SendRequest() 465 | net.Start("nettable_fullupdate") nettable.id_hasher.write(id, meta) net.SendToServer() 466 | end 467 | 468 | if nettable.__loaded then 469 | SendRequest() 470 | else 471 | hook.Add("InitPostEntity", "NetTable_DeferredRequest:" .. id, SendRequest) 472 | end 473 | 474 | nettable.debug("Requesting a full update from server for '", id, "'") 475 | end 476 | 477 | return tbl 478 | end 479 | 480 | function nettable.addChangeListener(id, listener) 481 | local id, tbl, meta = nettable.resolveIdTblMeta(id) 482 | 483 | if not meta then 484 | return nettable.error("Table '" .. tostring(tbl) .. "' does not have tablemeta. Make sure committed tables are created using nettable.get()") 485 | end 486 | 487 | meta.changeListeners = meta.changeListeners or {} 488 | table.insert(meta.changeListeners, listener) 489 | end 490 | function nettable.setChangeListener(id, listenerId, listener) 491 | local id, tbl, meta = nettable.resolveIdTblMeta(id) 492 | 493 | if not meta then 494 | return nettable.error("Table '" .. tostring(tbl) .. "' does not have tablemeta. Make sure committed tables are created using nettable.get()") 495 | end 496 | 497 | meta.changeListeners = meta.changeListeners or {} 498 | meta.changeListeners[listenerId] = listener 499 | end 500 | 501 | function nettable.createChangeEvent(modified, deleted) 502 | local event = {modified = modified, deleted = deleted} 503 | return event 504 | end 505 | 506 | -- Inspiration from nutscript https://github.com/Chessnut/NutScript/blob/master/gamemode/sh_util.lua#L581 507 | function nettable.computeTableDelta(old, new) 508 | local out, del = {}, {} 509 | 510 | for k, v in pairs(new) do 511 | local oldval = old[k] 512 | 513 | if type(v) == "table" and type(oldval) == "table" then 514 | local out2, del2 = nettable.computeTableDelta(oldval, v) 515 | 516 | for k2,v2 in pairs(out2) do 517 | out[k] = out[k] or {} 518 | out[k][k2] = v2 519 | end 520 | for k2,v2 in pairs(del2) do 521 | del[k] = del[k] or {} 522 | del[k][k2] = v2 523 | end 524 | 525 | elseif oldval == nil or oldval ~= v then 526 | out[k] = v 527 | end 528 | end 529 | 530 | for k,v in pairs(old) do 531 | local newval = new[k] 532 | 533 | if type(v) == "table" and type(newval) == "table" then 534 | local out2, del2 = nettable.computeTableDelta(v, newval) 535 | 536 | for k2,v2 in pairs(out2) do 537 | out[k] = out[k] or {} 538 | out[k][k2] = v2 539 | end 540 | for k2,v2 in pairs(del2) do 541 | del[k] = del[k] or {} 542 | del[k][k2] = v2 543 | end 544 | elseif v ~= nil and newval == nil then 545 | del[k] = true 546 | end 547 | end 548 | 549 | return out, del 550 | end 551 | 552 | function nettable.deepCopy(tbl) 553 | local copy = {} 554 | 555 | for k,v in pairs(tbl) do 556 | if type(v) == "table" then 557 | v = nettable.deepCopy(v) 558 | end 559 | copy[k] = v 560 | end 561 | 562 | return copy 563 | end 564 | 565 | if SERVER then 566 | util.AddNetworkString("nettable_commit") 567 | 568 | -- Helper function to write payload using net messages 569 | function nettable.writeNet(id, meta, modified, deleted) 570 | if meta.proto then 571 | net.WriteBool(true) 572 | 573 | nettable.debug("Using proto for id '", id, "'") 574 | 575 | for _,field in ipairs(meta.proto) do 576 | local name = field.name 577 | local is_modified = modified[name] 578 | 579 | nettable.debug("Proto field '", name, "' mod status: ", is_modified) 580 | 581 | net.WriteBool(is_modified) 582 | if is_modified then 583 | field.handler.write(modified[name], field.data) 584 | end 585 | end 586 | 587 | 588 | local deleted_bitfield = 0 589 | 590 | -- Deleted fields that have custom deletion handlers 591 | local deleted_chandlers = {} 592 | 593 | for i,field in ipairs(meta.proto) do 594 | local name = field.name 595 | local is_deleted = deleted[name] ~= nil 596 | 597 | if is_deleted then 598 | nettable.debug("Proto field '", name, "' is deleted!") 599 | deleted_bitfield = bor(deleted_bitfield, lshift(1, i)) 600 | 601 | -- This field requires custom deletion handling 602 | if field.handler.writeDeletion then 603 | table.insert(deleted_chandlers, field) 604 | end 605 | end 606 | end 607 | 608 | nettableproto.typeHandlers.int.write(deleted_bitfield) 609 | 610 | for _,field in ipairs(deleted_chandlers) do 611 | local name = field.name 612 | local fully_deleted = deleted[name] == true 613 | 614 | net.WriteBool(fully_deleted) 615 | if not fully_deleted then 616 | nettable.debug("Deleting proto field '", name, "' using custom deletion writer") 617 | field.handler.writeDeletion(deleted[name], field.data) 618 | end 619 | end 620 | else 621 | net.WriteBool(false) 622 | net.WriteTable(modified) 623 | net.WriteTable(deleted) 624 | end 625 | end 626 | 627 | function nettable.commit(id) 628 | local id, tbl, meta = nettable.resolveIdTblMeta(id) 629 | 630 | if not meta then 631 | return nettable.error("Table '" .. tostring(tbl) .. "' does not have tablemeta. Make sure committed tables are created using nettable.get()") 632 | end 633 | 634 | local sent = meta.lastSentTable or {} 635 | local modified, deleted = nettable.computeTableDelta(sent, tbl) 636 | 637 | if table.Count(modified) == 0 and table.Count(deleted) == 0 then 638 | nettable.debug("Not committing table; delta was empty") 639 | return 640 | end 641 | 642 | nettable.debug("Sending delta tables {mod=", table.ToString(modified), ", del=", table.ToString(deleted), "}") 643 | 644 | local targets 645 | if meta.filter then 646 | targets = {} 647 | for _,p in pairs(player.GetAll()) do 648 | if meta.filter(p, tbl) then 649 | targets[#targets+1] = p 650 | end 651 | end 652 | end 653 | 654 | if targets then 655 | nettable.debug("Nettable sending to filtered plys: ", table.ToString(targets)) 656 | end 657 | 658 | net.Start("nettable_commit") 659 | nettable.id_hasher.write(id, meta) 660 | nettable.writeNet(id, meta, modified, deleted) 661 | if targets then 662 | net.Send(targets) 663 | else 664 | net.Broadcast() 665 | end 666 | 667 | if meta.changeListeners then 668 | local changeEvent = nettable.createChangeEvent(modified, deleted) 669 | for _,l in pairs(meta.changeListeners) do 670 | l(changeEvent) 671 | end 672 | end 673 | 674 | meta.lastSentTable = nettable.deepCopy(tbl) 675 | end 676 | 677 | util.AddNetworkString("nettable_fullupdate") 678 | net.Receive("nettable_fullupdate", function(len, cl) 679 | local id = nettable.id_hasher.read() 680 | local tbl = nettable.__tables[id] 681 | if not tbl then 682 | nettable.warn("User ", cl, " attempted to request inexistent nettable ", id) 683 | return 684 | end 685 | 686 | local meta = nettable.__tablemeta[tbl] 687 | if meta.filter and not meta.filter(cl, tbl) then 688 | nettable.warn("User ", cl, " attempted to request nettable he's filtered from") 689 | return 690 | end 691 | 692 | nettable.debug("Sending full update to ", cl, " for '", id, "'") 693 | 694 | -- We don't want to send uncommitted changes, so we use deep copy of the last sent table 695 | local modified, deleted = (meta.lastSentTable or {}), {} 696 | 697 | net.Start("nettable_commit") 698 | nettable.id_hasher.write(id, meta) 699 | nettable.writeNet(id, meta, modified, deleted) 700 | net.Send(cl) 701 | end) 702 | end 703 | if CLIENT then 704 | net.Receive("nettable_commit", function(len, cl) 705 | local id = nettable.id_hasher.read() 706 | nettable.debug("Received commit for '" .. id .. "' (size " .. len .. ")") 707 | 708 | local tbl = nettable.get(id) 709 | local meta = nettable.__tablemeta[tbl] 710 | 711 | local using_proto = net.ReadBool() 712 | 713 | local mod, del 714 | if using_proto then 715 | if not meta.proto then 716 | nettable.error("using_proto true on a nettable that does not have proto! Make sure you pass a 'proto' to clientside nettable.") 717 | return 718 | end 719 | 720 | mod, del = {}, {} 721 | 722 | for _,field in ipairs(meta.proto) do 723 | local name = field.name 724 | local is_modified = net.ReadBool() 725 | 726 | if is_modified then 727 | nettable.debug("Proto field '", name, "' has been modified") 728 | mod[name] = field.handler.read(field.data) 729 | end 730 | end 731 | 732 | nettable.debug("Proto mod table ", table.ToString(mod)) 733 | 734 | -- First read bitfield of deleted fields 735 | local deletedFields = nettableproto.typeHandlers.int.read() 736 | nettable.debug("Proto del bitfield ", deletedFields) 737 | 738 | for i,field in ipairs(meta.proto) do 739 | local _bit = lshift(1, i) 740 | local is_deleted = band(deletedFields, _bit) == _bit 741 | 742 | -- If it is deleted, then we delegate to relevant deletion handler 743 | if is_deleted then 744 | local fully_deleted = true 745 | 746 | if field.handler.readDeletion then 747 | fully_deleted = net.ReadBool() 748 | 749 | if not fully_deleted then 750 | nettable.debug("Field '", field.name, "' has been deleted using custom deletion writer") 751 | del[field.name] = field.handler.readDeletion(field.data) 752 | end 753 | end 754 | 755 | if fully_deleted then 756 | nettable.debug("Field '", field.name, "' has been deleted") 757 | 758 | del[field.name] = true 759 | end 760 | end 761 | end 762 | else 763 | mod = net.ReadTable() 764 | del = net.ReadTable() 765 | end 766 | 767 | local function ApplyMod(mod, t, tid) 768 | for k,v in pairs(mod) do 769 | nettable.debug("Applying mod '", k, "=", v, "' to tableid ", tid) 770 | if type(v) == "table" then 771 | t[k] = t[k] or {} 772 | ApplyMod(v, t[k], k) 773 | else 774 | t[k] = v 775 | end 776 | end 777 | end 778 | ApplyMod(mod, tbl, "__main") 779 | 780 | local function ApplyDel(del, t) 781 | for k,v in pairs(del) do 782 | if type(v) == "table" then 783 | t[k] = t[k] or {} 784 | ApplyDel(v, t[k]) 785 | else 786 | t[k] = nil 787 | end 788 | end 789 | end 790 | ApplyDel(del, tbl) 791 | 792 | if meta.changeListeners then 793 | local changeEvent = nettable.createChangeEvent(mod, del) 794 | for _,l in pairs(meta.changeListeners) do 795 | l(changeEvent) 796 | end 797 | end 798 | 799 | nettable.debug("Commit applied") 800 | end) 801 | end 802 | --------------------------------------------------------------------------------