diff --git a/Makefile b/Makefile index f839665..0218ab8 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,6 @@ +mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST))) +current_dir := $(notdir $(patsubst %/,%,$(dir $(mkfile_path)))) + love: zip -9r bin/horsehorse.love ./* diff --git a/art/sprites/shark-unicorn.png b/art/sprites/shark-unicorn.png new file mode 100644 index 0000000..9bba336 Binary files /dev/null and b/art/sprites/shark-unicorn.png differ diff --git a/lib/bitser/.travis.yml b/lib/bitser/.travis.yml new file mode 100644 index 0000000..9c5854a --- /dev/null +++ b/lib/bitser/.travis.yml @@ -0,0 +1,26 @@ +language: python +sudo: false + +env: + - LUA="luajit=2.0" + +before_install: + - pip install hererocks + - hererocks lua_install -r^ --$LUA + - export PATH=$PATH:$PWD/lua_install/bin + +install: + - luarocks install luacheck + - luarocks install busted + - luarocks install luacov + - luarocks install luacov-coveralls + - luarocks install middleclass + - wget https://raw.githubusercontent.com/bartbes/slither/b9cf6daa1e8995093aa80a40ee9ff98402eeb602/slither.lua + - wget https://raw.githubusercontent.com/vrld/hump/038bc9025f1cb850355f4b073357b087b8122da9/class.lua + +script: + - luacheck --std max+busted bitser.lua spec --globals love --no-max-line-length + - busted --verbose --coverage + +after_success: + - luacov-coveralls --include bitser -e $TRAVIS_BUILD_DIR/lua_install diff --git a/lib/bitser/README.md b/lib/bitser/README.md new file mode 100644 index 0000000..68a5f61 --- /dev/null +++ b/lib/bitser/README.md @@ -0,0 +1,53 @@ +# bitser + +[![Build Status](https://travis-ci.org/gvx/bitser.svg?branch=master)](https://travis-ci.org/gvx/bitser) +[![Coverage Status](https://coveralls.io/repos/github/gvx/bitser/badge.svg?branch=master)](https://coveralls.io/github/gvx/bitser?branch=master) + +Serializes and deserializes Lua values with LuaJIT. + +```lua +local bitser = require 'bitser' + +bitser.register('someResource', someResource) +bitser.registerClass(SomeClass) + +serializedString = bitser.dumps(someValue) +someValue = bitser.loads(serializedString) +``` + +Documentation can be found in [USAGE.md](USAGE.md). + +Pull requests, bug reports and other feedback welcome! :heart: + +Bitser is released under the ISC license (functionally equivalent to the BSD +2-Clause and MIT licenses). + +Please note that bitser requires LuaJIT for its `ffi` library and JIT compilation. Without JIT, it may or may not run, but it will be much slower than usual. This primarily affects Android and iOS, because JIT is disabled on those platforms. + +## Why would I use this? + +Because it's fast. Because it produces tiny output. Because the name means "snappier" +or "unfriendlier" in Dutch. Because it's safe to use with untrusted data. + +Because it's inspired by [binser](https://github.com/bakpakin/binser), which is great. + +## How do I use the benchmark thingy? + +Download zero or more of [binser.lua](https://raw.githubusercontent.com/bakpakin/binser/master/binser.lua), +[ser.lua](https://raw.githubusercontent.com/gvx/Ser/master/ser.lua), +[smallfolk.lua](https://raw.githubusercontent.com/gvx/Smallfolk/master/smallfolk.lua), +[serpent.lua](https://raw.githubusercontent.com/pkulchenko/serpent/master/src/serpent.lua) and +[MessagePack.lua](https://raw.githubusercontent.com/fperrad/lua-MessagePack/master/src/MessagePack.lua), and run: + + love . + +You do need [LÖVE](https://love2d.org/) for that. + +You can add more cases in the folder `cases/` (check out `_new.lua`), and add other +serializers to the benchmark in `main.lua`. If you do either of those things, please +send me a pull request! + +## You can register classes? + +Yes. At the moment, bitser supports MiddleClass, SECL, hump.class, Slither and Moonscript classes (and +probably some other class libraries by accident). diff --git a/lib/bitser/USAGE.md b/lib/bitser/USAGE.md new file mode 100644 index 0000000..2819c02 --- /dev/null +++ b/lib/bitser/USAGE.md @@ -0,0 +1,234 @@ +* [Basic usage](#basic-usage) +* [Serializing class instances](#serializing-class-instances) +* [Advanced usage](#advanced-usage) +* [Reference](#reference) + * [`bitser.dumps`](#dumps) + * [`bitser.dumpLoveFile`](#dumplovefile) + * [`bitser.loads`](#loads) + * [`bitser.loadData`](#loaddata) + * [`bitser.loadLoveFile`](#loadlovefile) + * [`bitser.register`](#register) + * [`bitser.registerClass`](#registerclass) + * [`bitser.unregister`](#unregister) + * [`bitser.unregisterClass`](#unregisterclass) + * [`bitser.reserveBuffer`](#reservebuffer) + * [`bitser.clearBuffer`](#clearbuffer) + +# Basic usage + +```lua +local bitser = require 'bitser' + +-- some_thing can be almost any lua value +local binary_data = bitser.dumps(some_thing) + +-- binary_data is a string containing some serialized value +local copy_of_some_thing = bitser.loads(binary_data) +``` + +Bitser can't dump values of type `function`, `userdata` or `thread`, or anything that +contains one of those. If you need to, look into [`bitser.register`](#register). + +# Serializing class instances + +All you need to make bitser correctly serialize your class instances is register that class: + +```lua +-- this is usually enough +bitser.registerClass(MyClass) + +-- if you use Slither, you can add it to __attributes__ +class 'MyClass' { + __attributes__ = {bitser.registerClass}, + -- insert rest of class here +} + +local data = bitser.dumps(MyClass(42)) +local instance = bitser.loads(data) +``` + +Note that classnames need to be unique to avoid confusion, so if you have two different classes named `Foo` you'll need to do +something like: + +```lua +-- in module_a.lua +bitser.registerClass('module_a.Foo', Foo) + +-- in module_b.lua +bitser.registerClass('module_b.Foo', Foo) +``` + +See the reference sections on [`bitser.registerClass`](#registerclass) and +[`bitser.unregisterClass`](#unregisterclass) for more information. + +## Supported class libraries + +* MiddleClass +* SECL +* hump.class +* Slither +* Moonscript classes + +# Advanced usage + +If you use [LÖVE](https://love2d.org/), you'll want to use [`bitser.dumpLoveFile`](#dumplovefile) and [`bitser.loadLoveFile`](#loadlovefile) if you want to serialize to the save directory. You also might have images and other resources that you'll need to register, like follows: + +```lua +function love.load() + bad_guy_img = bitser.register('bad_guy_img', love.graphics.newImage('img/bad_guy.png')) + if love.filesystem.exists('save_point.dat') then + level_data = bitser.loadLoveFile('save_point.dat') + else + level_data = create_level_data() + end +end + +function save_point_reached() + bitser.dumpLoveFile('save_point.dat', level_data) +end +``` + +# Reference + +## dumps + +```lua +string = bitser.dumps(value) +``` + +Basic serialization of `value` into a Lua string. + +See also: [`bitser.loads`](#loads). + +## dumpLoveFile + +```lua +bitser.dumpLoveFile(file_name, value) +``` + +Serializes `value` and writes the result to `file_name` more efficiently than serializing to a string and writing +that string to a file. Only useful if you're running [LÖVE](https://love2d.org/). + +See also: [`bitser.loadLoveFile`](#loadlovefile). + +## loads + +```lua +value = bitser.loads(string) +``` + +Deserializes `value` from `string`. + +See also: [`bitser.dumps`](#dumps). + +## loadData + +```lua +value = bitser.loadData(light_userdata, size) +``` + +Deserializes `value` from raw data. You probably won't need to use this function ever. + +When running [LÖVE](https://love2d.org/), you would use it like this: + +```lua +value = bitser.loadData(data:getPointer(), data:getSize()) +``` + +Where `data` is an instance of a subclass of [Data](https://love2d.org/wiki/Data). + +## loadLoveFile + +```lua +value = bitser.loadLoveFile(file_name) +``` + +Reads from `file_name` and deserializes `value` more efficiently than reading the file and then deserializing that string. +Only useful if you're running [LÖVE](https://love2d.org/). + +See also: [`bitser.dumpLoveFile`](#dumplovefile). + +## register + +```lua +resource = bitser.register(name, resource) +``` + +Registers the value `resource` with the name `name`, which has to be a unique string. Registering static resources like images, +functions, classes, huge strings and LuaJIT ctypes, makes sure bitser doesn't attempt to serialize them, but only stores a named +reference to them. + +Returns the registered resource as a convenience. + +See also: [`bitser.unregister`](#unregister). + +## registerClass + +```lua +class = bitser.registerClass(class) +class = bitser.registerClass(name, class) +class = bitser.registerClass(name, class, classkey, deserializer) +``` + +Registers the class `class`, so that bitser can correctly serialize and deserialize instances of `class`. + +Note that if you want to serialize the class _itself_, you'll need to [register the class as a resource](#register). + +Most of the time the first variant is enough, but some class libraries don't store the +class name on the class object itself, in which case you'll need to use the second variant. + +Class names also have to be unique, so if you use multiple classes with the same name, you'll need to use the second +variant as well to give them different names. + +The arguments `classkey` and `deserializer` exist so you can hook in unsupported class libraries without needing +to patch bitser. [See the list of supported class libraries](#supported-class-libraries). + +If not nil, the argument `classkey` should be a string such that +`rawget(obj, classkey) == class` for any `obj` whose type is `class`. This is done so that key is skipped for serialization. + +If not nil, the argument `deserializer` should be a function such that `deserializer(obj, class)` returns a valid +instance of `class` with the properties of `obj`. `deserializer` is allowed to mutate `obj`. + +Returns the registered class as a convenience. + +See also: [`bitser.unregisterClass`](#unregisterclass). + +## unregister + +```lua +bitser.unregister(name) +``` + +Deregisters the previously registered value with the name `name`. + +See also: [`bitser.register`](#register). + +## unregisterClass + +```lua +bitser.unregisterClass(name) +``` + +Deregisters the previously registered class with the name `name`. Note that this works by name and not value, +which is useful in a context where you don't have a reference to the class you want to unregister. + +See also: [`bitser.registerClass`](#registerclass). + +## reserveBuffer + +```lua +bitser.reserveBuffer(num_bytes) +``` + +Makes sure the buffer used for reading and writing serialized data is at least `num_bytes` large. +You probably don't need to ever use this function. + +## clearBuffer + +```lua +bitser.clearBuffer() +``` + +Frees up the buffer used for reading and writing serialized data for garbage collection. +You'll rarely need to use this function, except if you needed a huge buffer before and now only need a small buffer +(or are done (de)serializing altogether). Most of the time, using this function will decrease performance needlessly. diff --git a/lib/bitser/cases/_new.lua b/lib/bitser/cases/_new.lua new file mode 100644 index 0000000..3ef06c3 --- /dev/null +++ b/lib/bitser/cases/_new.lua @@ -0,0 +1,3 @@ +-- write your own! +-- data to be tested, repetitions, number of tries +return {}, 10000, 3 \ No newline at end of file diff --git a/lib/bitser/cases/bigtable.lua b/lib/bitser/cases/bigtable.lua new file mode 100644 index 0000000..45e07c2 --- /dev/null +++ b/lib/bitser/cases/bigtable.lua @@ -0,0 +1,7 @@ +local t = {} + +for i = 1, 2000 do + t[i] = 100 +end + +return t, 500, 5 \ No newline at end of file diff --git a/lib/bitser/cases/cdata.lua b/lib/bitser/cases/cdata.lua new file mode 100644 index 0000000..3227bed --- /dev/null +++ b/lib/bitser/cases/cdata.lua @@ -0,0 +1,20 @@ +local ffi = require("ffi") + +ffi.cdef[[ +struct simple_struct { + int a; + int b; +}; + +struct nested_struct { + int a; + struct simple_struct b; +}; +]] + +local int_data = ffi.new('int', 5) + +local struct_data = ffi.new('struct nested_struct', {10, {20, 30}}) + + +return {int_data, struct_data, {ffi.new("int",1),5,ffi.new("int",67)}}, 1000, 3 \ No newline at end of file diff --git a/lib/bitser/cases/cthulhu.lua b/lib/bitser/cases/cthulhu.lua new file mode 100644 index 0000000..179b89d --- /dev/null +++ b/lib/bitser/cases/cthulhu.lua @@ -0,0 +1,7 @@ +local cthulhu = {{}, {}, {}} +cthulhu.fhtagn = cthulhu +cthulhu[1][cthulhu[2]] = cthulhu[3] +cthulhu[2][cthulhu[1]] = cthulhu[2] +cthulhu[3][cthulhu[3]] = cthulhu + +return cthulhu, 10000, 3 \ No newline at end of file diff --git a/lib/bitser/cases/intkeys.lua b/lib/bitser/cases/intkeys.lua new file mode 100644 index 0000000..bcfb1ef --- /dev/null +++ b/lib/bitser/cases/intkeys.lua @@ -0,0 +1,7 @@ +local t = {} + +for i = 1, 200 do + t[math.random(1000)] = math.random(100) +end + +return t, 30000, 5 \ No newline at end of file diff --git a/lib/bitser/cases/metatable.lua b/lib/bitser/cases/metatable.lua new file mode 100644 index 0000000..c3a2069 --- /dev/null +++ b/lib/bitser/cases/metatable.lua @@ -0,0 +1,5 @@ +-- test metatables +local metatable = {__mode = 'k', foo={1,2,3,4,5,6,7,8,10,11,12}} +metatable.__index = metatable + +return setmetatable({test=true}, metatable), 10000, 3 diff --git a/lib/bitser/cases/shared_table.lua b/lib/bitser/cases/shared_table.lua new file mode 100644 index 0000000..429b64b --- /dev/null +++ b/lib/bitser/cases/shared_table.lua @@ -0,0 +1,6 @@ +local t = {} +local x = {10, 50, 40, 30, 20} +for i = 1, 40 do + t[i] = x +end +return t, 10000, 3 \ No newline at end of file diff --git a/lib/bitser/conf.lua b/lib/bitser/conf.lua new file mode 100644 index 0000000..ebf29ea --- /dev/null +++ b/lib/bitser/conf.lua @@ -0,0 +1,4 @@ +function love.conf(t) + t.version = "11.3" + t.console = true +end \ No newline at end of file diff --git a/lib/bitser/init.lua b/lib/bitser/init.lua new file mode 100644 index 0000000..e929baa --- /dev/null +++ b/lib/bitser/init.lua @@ -0,0 +1,491 @@ +--[[ +Copyright (c) 2020, Jasmijn Wellner + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +]] + +local VERSION = '1.1' + +local floor = math.floor +local pairs = pairs +local type = type +local insert = table.insert +local getmetatable = getmetatable +local setmetatable = setmetatable + +local ffi = require("ffi") +local buf_pos = 0 +local buf_size = -1 +local buf = nil +local buf_is_writable = true +local writable_buf = nil +local writable_buf_size = nil +local SEEN_LEN = {} + +local function Buffer_prereserve(min_size) + if buf_size < min_size then + buf_size = min_size + buf = ffi.new("uint8_t[?]", buf_size) + buf_is_writable = true + end +end + +local function Buffer_clear() + buf_size = -1 + buf = nil + buf_is_writable = true + writable_buf = nil + writable_buf_size = nil +end + +local function Buffer_makeBuffer(size) + if not buf_is_writable then + buf = writable_buf + buf_size = writable_buf_size + writable_buf = nil + writable_buf_size = nil + buf_is_writable = true + end + buf_pos = 0 + Buffer_prereserve(size) +end + +local function Buffer_newReader(str) + Buffer_makeBuffer(#str) + ffi.copy(buf, str, #str) +end + +local function Buffer_newDataReader(data, size) + if buf_is_writable then + writable_buf = buf + writable_buf_size = buf_size + end + buf_is_writable = false + buf_pos = 0 + buf_size = size + buf = ffi.cast("uint8_t*", data) +end + +local function Buffer_reserve(additional_size) + while buf_pos + additional_size > buf_size do + buf_size = buf_size * 2 + local oldbuf = buf + buf = ffi.new("uint8_t[?]", buf_size) + buf_is_writable = true + ffi.copy(buf, oldbuf, buf_pos) + end +end + +local function Buffer_write_byte(x) + Buffer_reserve(1) + buf[buf_pos] = x + buf_pos = buf_pos + 1 +end + +local function Buffer_write_raw(data, len) + Buffer_reserve(len) + ffi.copy(buf + buf_pos, data, len) + buf_pos = buf_pos + len +end + +local function Buffer_write_string(s) + Buffer_write_raw(s, #s) +end + +local function Buffer_write_data(ct, len, ...) + Buffer_write_raw(ffi.new(ct, ...), len) +end + +local function Buffer_ensure(numbytes) + if buf_pos + numbytes > buf_size then + error("malformed serialized data") + end +end + +local function Buffer_read_byte() + Buffer_ensure(1) + local x = buf[buf_pos] + buf_pos = buf_pos + 1 + return x +end + +local function Buffer_read_string(len) + Buffer_ensure(len) + local x = ffi.string(buf + buf_pos, len) + buf_pos = buf_pos + len + return x +end + +local function Buffer_read_raw(data, len) + ffi.copy(data, buf + buf_pos, len) + buf_pos = buf_pos + len + return data +end + +local function Buffer_read_data(ct, len) + return Buffer_read_raw(ffi.new(ct), len) +end + +local resource_registry = {} +local resource_name_registry = {} +local class_registry = {} +local class_name_registry = {} +local classkey_registry = {} +local class_deserialize_registry = {} + +local serialize_value + +local function write_number(value, _) + if floor(value) == value and value >= -2147483648 and value <= 2147483647 then + if value >= -27 and value <= 100 then + --small int + Buffer_write_byte(value + 27) + elseif value >= -32768 and value <= 32767 then + --short int + Buffer_write_byte(250) + Buffer_write_data("int16_t[1]", 2, value) + else + --long int + Buffer_write_byte(245) + Buffer_write_data("int32_t[1]", 4, value) + end + else + --double + Buffer_write_byte(246) + Buffer_write_data("double[1]", 8, value) + end +end + +local function write_string(value, _) + if #value < 32 then + --short string + Buffer_write_byte(192 + #value) + else + --long string + Buffer_write_byte(244) + write_number(#value) + end + Buffer_write_string(value) +end + +local function write_nil(_, _) + Buffer_write_byte(247) +end + +local function write_boolean(value, _) + Buffer_write_byte(value and 249 or 248) +end + +local function write_table(value, seen) + local classkey + local metatable = getmetatable(value) + local classname = (class_name_registry[value.class] -- MiddleClass + or class_name_registry[value.__baseclass] -- SECL + or class_name_registry[metatable] -- hump.class + or class_name_registry[value.__class__] -- Slither + or class_name_registry[value.__class]) -- Moonscript class + if classname then + classkey = classkey_registry[classname] + Buffer_write_byte(242) + serialize_value(classname, seen) + elseif metatable then + Buffer_write_byte(253) + else + Buffer_write_byte(240) + end + local len = #value + write_number(len, seen) + for i = 1, len do + serialize_value(value[i], seen) + end + local klen = 0 + for k in pairs(value) do + if (type(k) ~= 'number' or floor(k) ~= k or k > len or k < 1) and k ~= classkey then + klen = klen + 1 + end + end + write_number(klen, seen) + for k, v in pairs(value) do + if (type(k) ~= 'number' or floor(k) ~= k or k > len or k < 1) and k ~= classkey then + serialize_value(k, seen) + serialize_value(v, seen) + end + end + if metatable and not classname then + serialize_value(metatable, seen) + end +end + +local function write_cdata(value, seen) + local ty = ffi.typeof(value) + if ty == value then + -- ctype + Buffer_write_byte(251) + serialize_value(tostring(ty):sub(7, -2), seen) + return + end + -- cdata + Buffer_write_byte(252) + serialize_value(ty, seen) + local len = ffi.sizeof(value) + write_number(len) + Buffer_write_raw(ffi.typeof('$[1]', ty)(value), len) +end + +local types = {number = write_number, string = write_string, table = write_table, boolean = write_boolean, ["nil"] = write_nil, cdata = write_cdata} + +serialize_value = function(value, seen) + if seen[value] then + local ref = seen[value] + if ref < 64 then + --small reference + Buffer_write_byte(128 + ref) + else + --long reference + Buffer_write_byte(243) + write_number(ref, seen) + end + return + end + local t = type(value) + if t ~= 'number' and t ~= 'boolean' and t ~= 'nil' and t ~= 'cdata' then + seen[value] = seen[SEEN_LEN] + seen[SEEN_LEN] = seen[SEEN_LEN] + 1 + end + if resource_name_registry[value] then + local name = resource_name_registry[value] + if #name < 16 then + --small resource + Buffer_write_byte(224 + #name) + Buffer_write_string(name) + else + --long resource + Buffer_write_byte(241) + write_string(name, seen) + end + return + end + (types[t] or + error("cannot serialize type " .. t) + )(value, seen) +end + +local function serialize(value) + Buffer_makeBuffer(4096) + local seen = {[SEEN_LEN] = 0} + serialize_value(value, seen) +end + +local function add_to_seen(value, seen) + insert(seen, value) + return value +end + +local function reserve_seen(seen) + insert(seen, 42) + return #seen +end + +local function deserialize_value(seen) + local t = Buffer_read_byte() + if t < 128 then + --small int + return t - 27 + elseif t < 192 then + --small reference + return seen[t - 127] + elseif t < 224 then + --small string + return add_to_seen(Buffer_read_string(t - 192), seen) + elseif t < 240 then + --small resource + return add_to_seen(resource_registry[Buffer_read_string(t - 224)], seen) + elseif t == 240 or t == 253 then + --table + local v = add_to_seen({}, seen) + local len = deserialize_value(seen) + for i = 1, len do + v[i] = deserialize_value(seen) + end + len = deserialize_value(seen) + for _ = 1, len do + local key = deserialize_value(seen) + v[key] = deserialize_value(seen) + end + if t == 253 then + setmetatable(v, deserialize_value(seen)) + end + return v + elseif t == 241 then + --long resource + local idx = reserve_seen(seen) + local value = resource_registry[deserialize_value(seen)] + seen[idx] = value + return value + elseif t == 242 then + --instance + local instance = add_to_seen({}, seen) + local classname = deserialize_value(seen) + local class = class_registry[classname] + local classkey = classkey_registry[classname] + local deserializer = class_deserialize_registry[classname] + local len = deserialize_value(seen) + for i = 1, len do + instance[i] = deserialize_value(seen) + end + len = deserialize_value(seen) + for _ = 1, len do + local key = deserialize_value(seen) + instance[key] = deserialize_value(seen) + end + if classkey then + instance[classkey] = class + end + return deserializer(instance, class) + elseif t == 243 then + --reference + return seen[deserialize_value(seen) + 1] + elseif t == 244 then + --long string + return add_to_seen(Buffer_read_string(deserialize_value(seen)), seen) + elseif t == 245 then + --long int + return Buffer_read_data("int32_t[1]", 4)[0] + elseif t == 246 then + --double + return Buffer_read_data("double[1]", 8)[0] + elseif t == 247 then + --nil + return nil + elseif t == 248 then + --false + return false + elseif t == 249 then + --true + return true + elseif t == 250 then + --short int + return Buffer_read_data("int16_t[1]", 2)[0] + elseif t == 251 then + --ctype + return ffi.typeof(deserialize_value(seen)) + elseif t == 252 then + local ctype = deserialize_value(seen) + local len = deserialize_value(seen) + local read_into = ffi.typeof('$[1]', ctype)() + Buffer_read_raw(read_into, len) + return ctype(read_into[0]) + else + error("unsupported serialized type " .. t) + end +end + +local function deserialize_MiddleClass(instance, class) + return setmetatable(instance, class.__instanceDict) +end + +local function deserialize_SECL(instance, class) + return setmetatable(instance, getmetatable(class)) +end + +local deserialize_humpclass = setmetatable + +local function deserialize_Slither(instance, class) + return getmetatable(class).allocate(instance) +end + +local function deserialize_Moonscript(instance, class) + return setmetatable(instance, class.__base) +end + +return {dumps = function(value) + serialize(value) + return ffi.string(buf, buf_pos) +end, dumpLoveFile = function(fname, value) + serialize(value) + assert(love.filesystem.write(fname, ffi.string(buf, buf_pos))) +end, loadLoveFile = function(fname) + local serializedData, error = love.filesystem.newFileData(fname) + assert(serializedData, error) + Buffer_newDataReader(serializedData:getPointer(), serializedData:getSize()) + local value = deserialize_value({}) + -- serializedData needs to not be collected early in a tail-call + -- so make sure deserialize_value returns before loadLoveFile does + return value +end, loadData = function(data, size) + if size == 0 then + error('cannot load value from empty data') + end + Buffer_newDataReader(data, size) + return deserialize_value({}) +end, loads = function(str) + if #str == 0 then + error('cannot load value from empty string') + end + Buffer_newReader(str) + return deserialize_value({}) +end, register = function(name, resource) + assert(not resource_registry[name], name .. " already registered") + resource_registry[name] = resource + resource_name_registry[resource] = name + return resource +end, unregister = function(name) + resource_name_registry[resource_registry[name]] = nil + resource_registry[name] = nil +end, registerClass = function(name, class, classkey, deserializer) + if not class then + class = name + name = class.__name__ or class.name or class.__name + end + if not classkey then + if class.__instanceDict then + -- assume MiddleClass + classkey = 'class' + elseif class.__baseclass then + -- assume SECL + classkey = '__baseclass' + end + -- assume hump.class, Slither, Moonscript class or something else that doesn't store the + -- class directly on the instance + end + if not deserializer then + if class.__instanceDict then + -- assume MiddleClass + deserializer = deserialize_MiddleClass + elseif class.__baseclass then + -- assume SECL + deserializer = deserialize_SECL + elseif class.__index == class then + -- assume hump.class + deserializer = deserialize_humpclass + elseif class.__name__ then + -- assume Slither + deserializer = deserialize_Slither + elseif class.__base then + -- assume Moonscript class + deserializer = deserialize_Moonscript + else + error("no deserializer given for unsupported class library") + end + end + class_registry[name] = class + classkey_registry[name] = classkey + class_deserialize_registry[name] = deserializer + class_name_registry[class] = name + return class +end, unregisterClass = function(name) + class_name_registry[class_registry[name]] = nil + classkey_registry[name] = nil + class_deserialize_registry[name] = nil + class_registry[name] = nil +end, reserveBuffer = Buffer_prereserve, clearBuffer = Buffer_clear, version = VERSION} diff --git a/lib/bitser/main.lua b/lib/bitser/main.lua new file mode 100644 index 0000000..9286ba2 --- /dev/null +++ b/lib/bitser/main.lua @@ -0,0 +1,195 @@ +local found_bitser, bitser = pcall(require, 'bitser') +local found_binser, binser = pcall(require, 'binser') +local found_ser, ser = pcall(require, 'ser') +local found_serpent, serpent = pcall(require, 'serpent') +local found_smallfolk, smallfolk = pcall(require, 'smallfolk') +local found_msgpack, msgpack = pcall(require, 'MessagePack') + +local cases +local selected_case = 1 + +local sers = {} +local desers = {} + +if found_bitser then + sers.bitser = bitser.dumps + desers.bitser = bitser.loads + bitser.reserveBuffer(1024 * 1024) +end + +if found_binser then + sers.binser = binser.s + desers.binser = binser.d +end + +if found_ser then + sers.ser = ser + desers.ser = loadstring +end + +if found_serpent then + sers.serpent = serpent.dump + desers.serpent = loadstring +end + +if found_smallfolk then + sers.smallfolk = smallfolk.dumps + desers.smallfolk = smallfolk.loads +end + +if found_msgpack then + sers.msgpack = msgpack.pack + desers.msgpack = msgpack.unpack +end + +local view_absolute = true +local resultname = "serialisation time in seconds" + +function love.load() + cases = love.filesystem.getDirectoryItems("cases") + state = 'select_case' + love.graphics.setBackgroundColor(1, 230/256, 220/256) + love.graphics.setColor(40/256, 30/256, 0/256) + love.window.setTitle("Select a benchmark testcase") +end + +function love.keypressed(key) + if state == 'select_case' then + if key == 'up' then + selected_case = (selected_case - 2) % #cases + 1 + elseif key == 'down' then + selected_case = selected_case % #cases + 1 + elseif key == 'return' then + state = 'calculate_results' + love.window.setTitle("Running benchmark...") + end + elseif state == 'results' then + if key == 'r' then + view_absolute = not view_absolute + elseif key == 'right' then + if results == results_ser then + results = results_deser + resultname = "deserialisation time in seconds" + elseif results == results_deser then + results = results_size + resultname = "size of output in bytes" + elseif results == results_size then + results = results_ser + resultname = "serialisation time in seconds" + end + elseif key == 'left' then + if results == results_ser then + results = results_size + resultname = "size of output in bytes" + elseif results == results_deser then + results = results_ser + resultname = "serialisation time in seconds" + elseif results == results_size then + results = results_deser + resultname = "deserialisation time in seconds" + end + elseif key == 'escape' then + state = 'select_case' + love.window.setTitle("Select a benchmark testcase") + end + end +end + +function love.draw() + if state == 'select_case' then + for i, case in ipairs(cases) do + love.graphics.print(case, selected_case == i and 60 or 20, i * 20) + end + local i = 2 + love.graphics.print('serialisation libraries installed:', 200, 20) + for sername in pairs(sers) do + love.graphics.print(sername, 200, i * 20) + i = i + 1 + end + elseif state == 'calculate_results' then + love.graphics.print("Running benchmark...", 20, 20) + love.graphics.print("This may take a while", 20, 40) + state = 'calculate_results_2' + elseif state == 'calculate_results_2' then + local data, iters, tries = love.filesystem.load("cases/" .. cases[selected_case])() + results_ser = {} + results = results_ser + resultname = "serialisation time in seconds" + results_size = {} + results_deser = {} + errors = {} + for sername, serializer in pairs(sers) do + results_ser[sername] = math.huge + results_deser[sername] = math.huge + end + local outputs = {} + for try = 1, tries do + for sername, serializer in pairs(sers) do + local output + local success, diff = pcall(function() + local t = os.clock() + for i = 1, iters do + output = serializer(data) + end + return os.clock() - t + end) + if not success and not errors[sername] then + errors[sername] = diff + elseif success and diff < results_ser[sername] then + results_ser[sername] = diff + end + if try == 1 then + outputs[sername] = output + results_size[sername] = output and #output or math.huge + end + end + end + for try = 1, tries do + for sername, deserializer in pairs(desers) do + local input = outputs[sername] + local success, diff = pcall(function() + local t = os.clock() + for i = 1, iters / 10 do + deserializer(input) + end + return os.clock() - t + end) + if not success and not errors[sername] then + errors[sername] = diff + elseif success and diff < results_deser[sername] then + results_deser[sername] = diff + end + end + end + state = 'results' + love.window.setTitle("Results for " .. cases[selected_case]) + elseif state == 'results' then + local results_min = math.huge + local results_max = -math.huge + for sername, result in pairs(results) do + if result < results_min then + results_min = result + end + if result > results_max and result < math.huge then + results_max = result + end + end + if view_absolute then results_min = 0 end + local i = 1 + for sername, result in pairs(results) do + love.graphics.print(sername, 20, i * 20) + if result == math.huge then + love.graphics.setColor(220/256, 30/256, 0) + love.graphics.rectangle('fill', 100, i * 20, 780 - 100, 18) + love.graphics.setColor(40/256, 30/256, 0) + love.graphics.print(errors[sername], 102, i * 20 + 2) + else + love.graphics.rectangle('fill', 100, i * 20, (780 - 100) * (result - results_min) / (results_max - results_min), 18) + end + i = i + 1 + end + love.graphics.print(results_min, 100, i * 20) + love.graphics.print(results_max, 780 - love.graphics.getFont():getWidth(results_max), i * 20) + love.graphics.print(resultname .." (smaller is better; try left, right, R, escape)", 100, i * 20 + 20) + end +end diff --git a/lib/bitser/spec/bitser_spec.lua b/lib/bitser/spec/bitser_spec.lua new file mode 100644 index 0000000..9c31ddd --- /dev/null +++ b/lib/bitser/spec/bitser_spec.lua @@ -0,0 +1,343 @@ +local ffi = require 'ffi' + +_G.love = {filesystem = {newFileData = function() + return {getPointer = function() + local buf = ffi.new("uint8_t[?]", #love.s) + ffi.copy(buf, love.s, #love.s) + return buf + end, getSize = function() + return #love.s + end} +end, write = function(_, s) + love.s = s + return true +end}} + +local bitser = require 'bitser' + +local function serdeser(value) + return bitser.loads(bitser.dumps(value)) +end + +local function test_serdeser(value) + assert.are.same(serdeser(value), value) +end + +local function test_serdeser_idempotent(value) + assert.are.same(bitser.dumps(serdeser(value)), bitser.dumps(value)) +end + +describe("bitser", function() + it("serializes simple values", function() + test_serdeser(true) + test_serdeser(false) + test_serdeser(nil) + test_serdeser(1) + test_serdeser(-1) + test_serdeser(0) + test_serdeser(100000000) + test_serdeser(1.234) + test_serdeser(10 ^ 20) + test_serdeser(1/0) + test_serdeser(-1/0) + test_serdeser("") + test_serdeser("hullo") + test_serdeser([[this + is a longer string + such a long string + that it won't fit + in the "short string" representation + no it won't + listen to me + it won't]]) + local nan = serdeser(0/0) + assert.is_not.equal(nan, nan) + end) + it("serializes simple tables", function() + test_serdeser({}) + test_serdeser({10, 11, 12}) + test_serdeser({foo = 10, bar = 99, [true] = false}) + test_serdeser({[1000] = 9000}) + test_serdeser({{}}) + end) + it("serializes tables with tables as keys", function() + local thekey = {"Heyo"} + assert.are.same(thekey, (next(serdeser({[thekey] = 12})))) + end) + it("serializes cyclic tables", function() + local cthulhu = {{}, {}, {}} + cthulhu.fhtagn = cthulhu + --note: this does not test tables as keys because assert.are.same doesn't like that + cthulhu[1].cthulhu = cthulhu[3] + cthulhu[2].cthulhu = cthulhu[2] + cthulhu[3].cthulhu = cthulhu + + test_serdeser(cthulhu) + end) + it("serializes resources", function() + local temp_resource = {} + bitser.register("temp_resource", temp_resource) + assert.are.equal(serdeser({this = temp_resource}).this, temp_resource) + bitser.unregister("temp_resource") + end) + it("serializes many resources", function() + local max = 1000 + local t = {} + for i = 1, max do + bitser.register(tostring(i), i) + t[i] = i + end + test_serdeser(t) + for i = 1, max do + bitser.unregister(tostring(i)) + end + end) + it("serializes deeply nested tables", function() + local max = 1000 + local t = {} + for _ = 1, max do + t.t = {} + t = t.t + end + test_serdeser(t) + end) + it("serializes MiddleClass instances", function() + local class = require("middleclass") + local Horse = bitser.registerClass(class('Horse')) + function Horse:initialize(name) + self.name = name + self[1] = 'instance can be sequence' + end + local bojack = Horse('Bojack Horseman') + test_serdeser(bojack) + assert.is_true(serdeser(bojack):isInstanceOf(Horse)) + bitser.unregisterClass('Horse') + end) + it("serializes SECL instances", function() + local class_mt = {} + + function class_mt:__index(key) + return self.__baseclass[key] + end + + local class = setmetatable({ __baseclass = {} }, class_mt) + + function class:new(...) + local c = {} + c.__baseclass = self + setmetatable(c, getmetatable(self)) + if c.init then + c:init(...) + end + return c + end + + local Horse = bitser.registerClass('Horse', class:new()) + function Horse:init(name) + self.name = name + self[1] = 'instance can be sequence' + end + local bojack = Horse:new('Bojack Horseman') + test_serdeser(bojack) + assert.are.equal(serdeser(bojack).__baseclass, Horse) + bitser.unregisterClass('Horse') + end) + it("serializes hump.class instances", function() + local class = require("class") + local Horse = bitser.registerClass('Horse', class{}) + function Horse:init(name) + self.name = name + self[1] = 'instance can be sequence' + end + local bojack = Horse('Bojack Horseman') + test_serdeser(bojack) + assert.are.equal(getmetatable(serdeser(bojack)), Horse) + bitser.unregisterClass('Horse') + end) + it("serializes Slither instances", function() + local class = require("slither") + local Horse = class 'Horse' { + __attributes__ = {bitser.registerClass}, + __init__ = function(self, name) + self.name = name + self[1] = 'instance can be sequence' + end + } + local bojack = Horse('Bojack Horseman') + test_serdeser(bojack) + assert.is_true(class.isinstance(serdeser(bojack), Horse)) + bitser.unregisterClass('Horse') + end) + it("serializes Moonscript class instances", function() + local Horse + do + local _class_0 + local _base_0 = {} + _base_0.__index = _base_0 + _class_0 = setmetatable({ + __init = function(self, name) + self.name = name + self[1] = 'instance can be sequence' + end, + __base = _base_0, + __name = "Horse"}, { + __index = _base_0, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + Horse = _class_0 + end + assert.are.same(Horse.__name, "Horse") -- to shut coveralls up + bitser.registerClass(Horse) + local bojack = Horse('Bojack Horseman') + test_serdeser(bojack) + local new_bojack = serdeser(bojack) + assert.are.equal(new_bojack.__class, Horse) + bitser.unregisterClass('Horse') + end) + it("serializes custom class instances", function() + local Horse_mt = bitser.registerClass('Horse', {__index = {}}, nil, setmetatable) + local function Horse(name) + local self = {} + self.name = name + self[1] = 'instance can be sequence' + return setmetatable(self, Horse_mt) + end + local bojack = Horse('Bojack Horseman') + test_serdeser(bojack) + assert.are.equal(getmetatable(serdeser(bojack)), Horse_mt) + bitser.unregisterClass('Horse') + end) + it("serializes classes that repeat keys", function() + local my_mt = {"hi"} + local works = { foo = 'a', bar = {baz = 'b'}, } + local broken = { foo = 'a', bar = {foo = 'b'}, } + local more_broken = { + foo = 'a', + baz = {foo = 'b', bar = 'c'}, + quz = {bar = 'd', bam = 'e'} + } + setmetatable(works, my_mt) + setmetatable(broken, my_mt) + setmetatable(more_broken, my_mt) + bitser.registerClass("Horse", my_mt, nil, setmetatable) + test_serdeser(works) + test_serdeser(broken) + test_serdeser(more_broken) + bitser.unregisterClass('Horse') + end) + it("serializes big data", function() + local text = "this is a lot of nonsense, please disregard, we need a lot of data to get past 4 KiB (114 characters should do it)" + local t = {} + for i = 1, 40 do + t[i] = text .. i -- no references allowed! + end + test_serdeser(t) + end) + it("serializes many references", function() + local max = 1000 + local t = {} + local t2 = {} + for i = 1, max do + t.t = {} + t = t.t + t2[i] = t + end + test_serdeser({t, t2}) + end) + it("serializes resources with long names", function() + local temp_resource = {} + bitser.register("temp_resource_or_whatever", temp_resource) + assert.are.equal(serdeser({this = temp_resource}).this, temp_resource) + bitser.unregister("temp_resource_or_whatever") + end) + it("serializes resources with the same name as serialized strings", function() + local temp_resource = {} + bitser.register('temp', temp_resource) + test_serdeser({temp='temp', {temp_resource}}) + bitser.unregister('temp') + end) + it("serializes resources with the same long name as serialized strings", function() + local temp_resource = {} + bitser.register('temp_resource_or_whatever', temp_resource) + test_serdeser({temp_resource_or_whatever='temp_resource_or_whatever', {temp_resource}}) + bitser.unregister('temp_resource_or_whatever') + end) + it("cannot serialize functions", function() + assert.has_error(function() bitser.dumps(function() end) end, "cannot serialize type function") + end) + it("cannot serialize unsupported class libraries without explicit deserializer", function() + assert.has_error(function() bitser.registerClass('Horse', {mane = 'majestic'}) end, "no deserializer given for unsupported class library") + end) + it("cannot deserialize values from unassigned type bytes", function() + assert.has_error(function() bitser.loads("\254") end, "unsupported serialized type 254") + assert.has_error(function() bitser.loads("\255") end, "unsupported serialized type 255") + end) + it("can load from raw data", function() + assert.are.same(bitser.loadData(ffi.new("uint8_t[4]", 195, 103, 118, 120), 4), "gvx") + end) + it("will not read from zero length data", function() + assert.has_error(function() bitser.loadData(ffi.new("uint8_t[1]", 0), 0) end) + end) + it("will not read from zero length string", function() + assert.has_error(function() bitser.loads("") end) + end) + it("will not read past the end of the buffer", function() + assert.has_error(function() bitser.loadData(ffi.new("uint8_t[4]", 196, 103, 118, 120), 4) end) + end) + it("can clear the buffer", function() + bitser.clearBuffer() + end) + it("can write to new buffer after reading from read-only buffer", function() + test_serdeser("bitser") + bitser.loadData(ffi.new("uint8_t[4]", 195, 103, 118, 120), 4) + test_serdeser("bitser") + end) + it("can dump and load LÖVE files", function() + local v = {value = "value"} + bitser.dumpLoveFile("some_file_name", v) + assert.are.same(v, bitser.loadLoveFile("some_file_name")) + end) + it("can read and write simple cdata", function() + test_serdeser(ffi.new('double', 42.5)) + end) + it("can read and write cdata with a registered ctype", function() + pcall(ffi.cdef,[[ + struct some_struct { + int a; + double b; + }; + ]]) + local value = ffi.new('struct some_struct', 42, 1.25) + bitser.register('struct_type', ffi.typeof(value)) + test_serdeser_idempotent(value) + bitser.unregister('struct_type') + end) + it("can read and write cdata without registering its ctype", function() + pcall(ffi.cdef,[[ + struct some_struct { + int a; + double b; + }; + ]]) + local value = ffi.new('struct some_struct', 42, 1.25) + test_serdeser_idempotent(value) + end) + it("cannot read from anonymous structs", function() + local v = bitser.dumps(ffi.new('struct { int a; }')) + assert.has_error(function() bitser.loads(v) end) + end) + it("can read and write simple multiple cdata of the same ctype without getting confused", function() + test_serdeser({ffi.new('double', 42.5), ffi.new('double', 12), ffi.new('double', 0.01)}) + end) + it("can read and write metatables", function() + local t = setmetatable({foo="foo"}, {__index = {bar="bar"}}) + test_serdeser(t) + assert.are.same(getmetatable(t), getmetatable(serdeser(t))) + assert.are.same(serdeser(t).bar, "bar") + end) +end) diff --git a/lib/sock/.travis.yml b/lib/sock/.travis.yml new file mode 100644 index 0000000..5d83a27 --- /dev/null +++ b/lib/sock/.travis.yml @@ -0,0 +1,32 @@ +language: python +sudo: required + +env: + - LUA="luajit=2.0" + - LUA="luajit=2.1" + +before_install: + - pip install hererocks + - hererocks lua_install -r^ --$LUA + - export PATH=$PATH:$PWD/lua_install/bin + - git clone https://github.com/lsalzman/enet.git + - sudo apt-get install -y dh-autoreconf + - cd enet + - autoreconf -vfi + - ./configure && sudo make && sudo make install + - cd .. + +install: + - luarocks install busted + - luarocks install enet + - luarocks install luacov + - luarocks install luacov-coveralls + - luarocks install luacheck + +after_success: + - luacov-coveralls --exclude "bitser.lua" -e $TRAVIS_BUILD_DIR/lua_install + +script: + # - luacheck --std=max+busted *.lua spec --new-globals=enet+bitser --no-max-line-length --ignore="61." --include-files sock.lua sock_spec.lua + - busted --verbose --coverage --no-auto-insulate -p "sock_spec.lua" spec + diff --git a/lib/sock/CHANGELOG b/lib/sock/CHANGELOG new file mode 100644 index 0000000..c646f97 --- /dev/null +++ b/lib/sock/CHANGELOG @@ -0,0 +1,42 @@ +CHANGELOG +========= + +0.3.0 +----- + +* Renamed 'data format' to 'schema' + * `Server:setDataFormat` is now `Server:setSchema` + * Added `Client:setSchema` +* Added enet's range coding compression + * Added `Server:enableCompression` + * Added `Client:enableCompression` +* Added optional code for `Client:connect` +* Added `Server:getClientCount` +* Added `Server:destroy` +* Added `Client:reset` +* Added `Client:isConnected` +* Added `Client:isConnecting` +* Added `Client:isDisconnected` +* Added `Client:isDisconnecting` + +0.2.1 +----- + +* Fixed bitser not being required using relative location + +0.2.0 +----- + +* Added custom serialization support + * Added Client:setSerialization + * Added Server:setSerialization +* Changed 'emit' functions to 'send' + * 'Client:emit' is now 'Client:send' + * 'Server:emitToAll' is now 'Server:emitToAll' + * 'Server:emitToAllBut' is now 'Server:emitToAllBut' +* Added new 'Server:sendToPeer' function + +0.1.0 +----- + +* Initial release diff --git a/lib/sock/LICENSE b/lib/sock/LICENSE new file mode 100644 index 0000000..778fd1e --- /dev/null +++ b/lib/sock/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Cameron McHenry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/sock/README.md b/lib/sock/README.md new file mode 100644 index 0000000..16c0fbd --- /dev/null +++ b/lib/sock/README.md @@ -0,0 +1,90 @@ +# sock.lua + +[![Build Status](https://travis-ci.org/camchenry/sock.lua.svg?branch=master)](https://travis-ci.org/camchenry/sock.lua) +[![Coverage Status](https://coveralls.io/repos/github/camchenry/sock.lua/badge.svg?branch=master)](https://coveralls.io/github/camchenry/sock.lua?branch=master) + +sock.lua is a networking library for LÖVE games. Its goal is to make getting started with networking as easy as possible. + +[Documentation](https://camchenry.github.io/sock.lua/) + +**sock requires [enet](https://github.com/leafo/lua-enet) (which comes with LÖVE 0.9 and up.)** + +## Features + +- Event trigger system makes it easy to add behavior to network events. +- Can send images and files over the network. +- Can use a custom serialization library. +- Logs events, errors, and warnings that occur. + +# Installation + +1. Clone or download sock.lua. +2. Clone or download [bitser](https://github.com/gvx/bitser).\* +3. Place bitser.lua in the same directory as sock.lua. +4. Require the library and start using it. `sock = require 'sock'` + +\* If custom serialization support is needed, look at [setSerialization](https://camchenry.github.io/sock.lua//index.html#Server:setSerialization). + +# Example + +```lua +local sock = require "sock" + +-- client.lua +function love.load() + -- Creating a new client on localhost:22122 + client = sock.newClient("localhost", 22122) + + -- Creating a client to connect to some ip address + client = sock.newClient("198.51.100.0", 22122) + + -- Called when a connection is made to the server + client:on("connect", function(data) + print("Client connected to the server.") + end) + + -- Called when the client disconnects from the server + client:on("disconnect", function(data) + print("Client disconnected from the server.") + end) + + -- Custom callback, called whenever you send the event from the server + client:on("hello", function(msg) + print("The server replied: " .. msg) + end) + + client:connect() + + -- You can send different types of data + client:send("greeting", "Hello, my name is Inigo Montoya.") + client:send("isShooting", true) + client:send("bulletsLeft", 1) + client:send("position", { + x = 465.3, + y = 50, + }) +end + +function love.update(dt) + client:update() +end +``` + +```lua +-- server.lua +function love.load() + -- Creating a server on any IP, port 22122 + server = sock.newServer("*", 22122) + + -- Called when someone connects to the server + server:on("connect", function(data, client) + -- Send a message back to the connected client + local msg = "Hello from the server!" + client:send("hello", msg) + end) +end + +function love.update(dt) + server:update() +end +``` diff --git a/lib/sock/config.ld b/lib/sock/config.ld new file mode 100644 index 0000000..993d042 --- /dev/null +++ b/lib/sock/config.ld @@ -0,0 +1,16 @@ +project = 'sock.lua' +title = 'sock.lua Documentation' +description = 'sock.lua is a networking library for Lua/LÖVE games.' +file = 'sock.lua' +dir = 'docs' + +style = 'docstyle' +template = 'docstyle' +format = 'markdown' +one = true +all = true +no_lua_ref = true +not_luadoc = false +wrap = true +sort = true +no_space_before_args = true diff --git a/lib/sock/docs/index.html b/lib/sock/docs/index.html new file mode 100644 index 0000000..bd24115 --- /dev/null +++ b/lib/sock/docs/index.html @@ -0,0 +1,3850 @@ + + + + sock.lua Documentation + + + + + + + + + + + + + +
+ + +
+

sock.lua

+
+ +
+ + + +
+ + +

A Lua networking library for LÖVE games.

+

+ +

+

+ +
+

Tables

+
+ +
+
+ 🔗 +

CONNECTING_STATES

+ +
+

+ States that represent the client connecting to a server. +

+

+ + + +

+ + +

Fields:

+
+
+ + connecting + +
+
+ In the process of connecting to the server. + + +
+ + acknowledging_connect + +
+
+ + + + + +
+ + connection_pending + +
+
+ + + + + +
+ + connection_succeeded + +
+
+ + + + + +
+ + + + + +
+
+
+ 🔗 +

CONNECTION_STATES

+ +
+

+ All of the possible connection statuses for a client connection. +

+

+ + + +

+ + +

Fields:

+
+
+ + disconnected + +
+
+ Disconnected from the server. + + +
+ + connecting + +
+
+ In the process of connecting to the server. + + +
+ + acknowledging_connect + +
+
+ + + + + +
+ + connection_pending + +
+
+ + + + + +
+ + connection_succeeded + +
+
+ + + + + +
+ + connected + +
+
+ Successfully connected to the server. + + +
+ + disconnect_later + +
+
+ Disconnecting, but only after sending all queued packets. + + +
+ + disconnecting + +
+
+ In the process of disconnecting from the server. + + +
+ + acknowledging_disconnect + +
+
+ + + + + +
+ + zombie + +
+
+ + + + + +
+ + unknown + +
+
+ + + + + +
+ + + +

See also:

+ + + +
+
+
+ 🔗 +

DISCONNECTING_STATES

+ +
+

+ States that represent the client disconnecting from a server. +

+

+ + + +

+ + +

Fields:

+
+
+ + disconnect_later + +
+
+ Disconnecting, but only after sending all queued packets. + + +
+ + disconnecting + +
+
+ In the process of disconnecting from the server. + + +
+ + acknowledging_disconnect + +
+
+ + + + + +
+ + + + + +
+
+
+ 🔗 +

SEND_MODES

+ +
+

+ Valid modes for sending messages. +

+

+ + + +

+ + +

Fields:

+
+
+ + reliable + +
+
+ Message is guaranteed to arrive, and arrive in the order in which it is sent. + + +
+ + unsequenced + +
+
+ Message has no guarantee on the order that it arrives. + + +
+ + unreliable + +
+
+ Message is not guaranteed to arrive. + + +
+ + + + + +
+
+
+
+
+
+

Class Server

+
+ +

+ Manages all clients and receives network events. +

+
+
+ 🔗 +

Server:destroy()

+ +
+

+ Destroys the server and frees the port it is bound to. +

+

+ + + +

+ + + + + + + +
+
+
+ 🔗 +

Server:enableCompression()

+ +
+

+ Enables an adaptive order-2 PPM range coder for the transmitted data of all peers. +

+

+ Both the client and server must both either have compression enabled or disabled.

+ +

Note: lua-enet does not currently expose a way to disable the compression after it has been enabled. +

+ + + + + + + +
+
+
+ 🔗 +

Server:getAddress()

+ +
+

+ Get the IP address or hostname that the server was created with. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + string + + + +
  2. +
+ + + + +
+
+
+ 🔗 +

Server:getClient(peer)

+ +
+

+ Gets the Client object associated with an enet peer. +

+

+ + + +

+ + +

Parameters:

+
+
+ + peer + peer + +
+
+ An enet peer. + + +
+ +

Returns:

+
    +
  1. + Client + Object associated with the peer. +
  2. +
+ + + + +
+
+
+ 🔗 +

Server:getClientByConnectId(connectId)

+ +
+

+ Gets the Client object that has the given connection id. +

+

+ + + +

+ + +

Parameters:

+
+
+ + number + connectId + +
+
+ The unique client connection id. + + +
+ +

Returns:

+
    +
  1. + Client + + + +
  2. +
+ + + + +
+
+
+ 🔗 +

Server:getClientByIndex(index)

+ +
+

+ Get the Client object that has the given peer index. +

+

+ + + +

+ + +

Parameters:

+
+
+ + index + +
+
+ + + + + +
+ +

Returns:

+
    +
  1. + Client + + + +
  2. +
+ + + + +
+
+
+ 🔗 +

Server:getClientCount()

+ +
+

+ Get the number of Clients that are currently connected to the server. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + number + The number of active clients. +
  2. +
+ + + + +
+
+
+ 🔗 +

Server:getClients()

+ +
+

+ Get the table of Clients actively connected to the server. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + {Client,...} +
  2. +
+ + + + +
+
+
+ 🔗 +

Server:getDefaultSendMode()

+ +
+

+ Get the default send mode. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + string + + + +
  2. +
+ + +

See also:

+ + + +
+
+
+ 🔗 +

Server:getLastServiceTime()

+ +
+

+ Get the last time when network events were serviced. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + number + Timestamp of the last time events were serviced. +
  2. +
+ + + + +
+
+
+ 🔗 +

Server:getMaxChannels()

+ +
+

+ Get the number of allocated channels. +

+

+ Channels are zero-indexed, e.g. 16 channels allocated means that the + maximum channel that can be used is 15. +

+ + + +

Returns:

+
    +
  1. + number + Number of allocated channels. +
  2. +
+ + + + +
+
+
+ 🔗 +

Server:getMaxPeers()

+ +
+

+ Get the number of allocated slots for peers. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + number + Number of allocated slots. +
  2. +
+ + + + +
+
+
+ 🔗 +

Server:getMessageTimeout()

+ +
+

+ Get the timeout for packets. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + number + Time to wait for incoming packets in milliseconds. + initial default is 0. +
  2. +
+ + + + +
+
+
+ 🔗 +

Server:getPeerByIndex(index)

+ +
+

+ Get the enet_peer that has the given index. +

+

+ + + +

+ + +

Parameters:

+
+
+ + index + +
+
+ + + + + +
+ +

Returns:

+
    +
  1. + enet_peer + The underlying enet peer object. +
  2. +
+ + + + +
+
+
+ 🔗 +

Server:getPort()

+ +
+

+ Get the port that the server is hosted on. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + number + + + +
  2. +
+ + + + +
+
+
+ 🔗 +

Server:getSendMode()

+ +
+

+ Get the current send mode. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + string + + + +
  2. +
+ + +

See also:

+ + + +
+
+
+ 🔗 +

Server:getSocketAddress()

+ +
+

+ Get the socket address of the host. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + string + A description of the socket address, in the format + "A.B.C.D:port" where A.B.C.D is the IP address of the used socket. +
  2. +
+ + + + +
+
+
+ 🔗 +

Server:getTotalReceivedData()

+ +
+

+ Get the total received data since the server was created. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + number + The total received data in bytes. +
  2. +
+ + + + +
+
+
+ 🔗 +

Server:getTotalReceivedPackets()

+ +
+

+ Get the total number of packets (messages) received since the server was created. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + number + The total number of received packets. +
  2. +
+ + +

See also:

+ + + +
+
+
+ 🔗 +

Server:getTotalSentData()

+ +
+

+ Get the total sent data since the server was created. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + number + The total sent data in bytes. +
  2. +
+ + + + +
+
+
+ 🔗 +

Server:getTotalSentPackets()

+ +
+

+ Get the total number of packets (messages) sent since the server was created. +

+

+ Everytime a message is sent or received, the corresponding figure is incremented. + Therefore, this is not necessarily an accurate indicator of how many packets were actually + exchanged over the network. +

+ + + +

Returns:

+
    +
  1. + number + The total number of sent packets. +
  2. +
+ + + + +
+
+
+ 🔗 +

Server:log(event, data)

+ +
+

+ Log an event. +

+

+ Alias for Server.logger:log. +

+ + +

Parameters:

+
+
+ + string + event + +
+
+ The type of event that happened. + + +
+ + string + data + +
+
+ The message to log. + + +
+ + + + +

Usage:

+
    +
    if somethingBadHappened then
    +    server:log("error", "Something bad happened!")
    +end
    +
+ +
+
+
+ 🔗 +

Server:on(event, callback)

+ +
+

+ Add a callback to an event. +

+

+ + + +

+ + +

Parameters:

+
+
+ + string + event + +
+
+ The event that will trigger the callback. + + +
+ + function + callback + +
+
+ The callback to be triggered. + + +
+ +

Returns:

+
    +
  1. + function + The callback that was passed in. +
  2. +
+ + + +

Usage:

+
    +
    server:on("connect", function(data, client)
    +    print("Client connected!")
    +end)
    +
+ +
+
+
+ 🔗 +

Server:removeCallback(callback)

+ +
+

+ Remove a specific callback for an event. +

+

+ + + +

+ + +

Parameters:

+
+
+ + function + callback + +
+
+ The callback to remove. + + +
+ +

Returns:

+
    +
  1. + boolean + Whether or not the callback was removed. +
  2. +
+ + + +

Usage:

+
    +
    local callback = server:on("chatMessage", function(message)
    +    print(message)
    +end)
    +server:removeCallback(callback)
    +
+ +
+
+
+ 🔗 +

Server:resetSendSettings()

+ +
+

+ Reset all send options to their default values. +

+

+ + + +

+ + + + + + + +
+
+
+ 🔗 +

Server:sendToAll(event, data)

+ +
+

+ Send a message to all clients. +

+

+ + + +

+ + +

Parameters:

+
+
+ + string + event + +
+
+ The event to trigger with this message. + + +
+ + data + +
+
+ The data to send. + + +
+ + + + +

Usage:

+
    +
    server:sendToAll("gameStarting", true)
    +
+ +
+
+
+ 🔗 +

Server:sendToAllBut(client, event, data)

+ +
+

+ Send a message to all clients, except one. +

+

+ Useful for when the client does something locally, but other clients + need to be updated at the same time. This way avoids duplicating objects by + never sending its own event to itself in the first place. +

+ + +

Parameters:

+
+
+ + Client + client + +
+
+ The client to not receive the message. + + +
+ + string + event + +
+
+ The event to trigger with this message. + + +
+ + data + +
+
+ The data to send. + + +
+ + + + + +
+
+
+ 🔗 +

Server:sendToPeer(peer, event, data)

+ +
+

+ Send a message to a single peer. +

+

+ Useful to send data to a newly connected player + without sending to everyone who already received it. +

+ + +

Parameters:

+
+
+ + enet_peer + peer + +
+
+ The enet peer to receive the message. + + +
+ + string + event + +
+
+ The event to trigger with this message. + + +
+ + data + +
+
+ data to send to the peer. + + +
+ + + + +

Usage:

+
    +
    server:sendToPeer(peer, "initialGameInfo", {...})
    +
+ +
+
+
+ 🔗 +

Server:setBandwidthLimit(incoming, outgoing)

+ +
+

+ Set the incoming and outgoing bandwidth limits. +

+

+ + + +

+ + +

Parameters:

+
+
+ + number + incoming + +
+
+ The maximum incoming bandwidth in bytes. + + +
+ + number + outgoing + +
+
+ The maximum outgoing bandwidth in bytes. + + +
+ + + + + +
+
+
+ 🔗 +

Server:setDefaultSendChannel(channel)

+ +
+

+ Set the default send channel for all future outgoing messages. +

+

+ The initial default is 0. +

+ + +

Parameters:

+
+
+ + number + channel + +
+
+ Channel to send data on. + + +
+ + + + + +
+
+
+ 🔗 +

Server:setDefaultSendMode(mode)

+ +
+

+ Set the default send mode for all future outgoing messages. +

+

+ + The initial default is "reliable". +

+ + +

Parameters:

+
+
+ + string + mode + +
+
+ A valid send mode. + + +
+ + + +

See also:

+ + + +
+
+
+ 🔗 +

Server:setMaxChannels(limit)

+ +
+

+ Set the maximum number of channels. +

+

+ + + +

+ + +

Parameters:

+
+
+ + number + limit + +
+
+ The maximum number of channels allowed. If it is 0, + then the maximum number of channels available on the system will be used. + + +
+ + + + + +
+
+
+ 🔗 +

Server:setMessageTimeout(timeout)

+ +
+

+ Set the timeout to wait for packets. +

+

+ + + +

+ + +

Parameters:

+
+
+ + number + timeout + +
+
+ Time to wait for incoming packets in milliseconds. The + initial default is 0. + + +
+ + + + + +
+
+
+ 🔗 +

Server:setSchema(event, schema)

+ +
+

+ Set the data schema for an event. +

+

+ Schemas allow you to set a specific format that the data will be sent. If the + client and server both know the format ahead of time, then the table keys + do not have to be sent across the network, which saves bandwidth. +

+ + +

Parameters:

+
+
+ + string + event + +
+
+ The event to set the data schema for. + + +
+ + {string,...} + schema + +
+
+ The data schema. + + +
+ + + + +

Usage:

+
    +
    server = sock.newServer(...)
    +client = sock.newClient(...)
    +
    +-- Without schemas
    +client:send("update", {
    +    x = 4,
    +    y = 100,
    +    vx = -4.5,
    +    vy = 23.1,
    +    rotation = 1.4365,
    +})
    +server:on("update", function(data, client)
    +    -- data = {
    +    --    x = 4,
    +    --    y = 100,
    +    --    vx = -4.5,
    +    --    vy = 23.1,
    +    --    rotation = 1.4365,
    +    -- }
    +end)
    +
    +
    +-- With schemas
    +server:setSchema("update", {
    +    "x",
    +    "y",
    +    "vx",
    +    "vy",
    +    "rotation",
    +})
    +-- client no longer has to send the keys, saving bandwidth
    +client:send("update", {
    +    4,
    +    100,
    +    -4.5,
    +    23.1,
    +    1.4365,
    +})
    +server:on("update", function(data, client)
    +    -- data = {
    +    --    x = 4,
    +    --    y = 100,
    +    --    vx = -4.5,
    +    --    vy = 23.1,
    +    --    rotation = 1.4365,
    +    -- }
    +end)
    +
+ +
+
+
+ 🔗 +

Server:setSendChannel(channel)

+ +
+

+ Set the send channel for the next outgoing message. +

+

+ + The channel will be reset after the next message. Channels are zero-indexed + and cannot exceed the maximum number of channels allocated. The initial + default is 0. +

+ + +

Parameters:

+
+
+ + number + channel + +
+
+ Channel to send data on. + + +
+ + + + +

Usage:

+
    +
    server:setSendChannel(2) -- the third channel
    +server:sendToAll("importantEvent", "The message")
    +
+ +
+
+
+ 🔗 +

Server:setSendMode(mode)

+ +
+

+ Set the send mode for the next outgoing message. +

+

+ + The mode will be reset after the next message is sent. The initial default + is "reliable". +

+ + +

Parameters:

+
+
+ + string + mode + +
+
+ A valid send mode. + + +
+ + + +

See also:

+ + +

Usage:

+
    +
    server:setSendMode("unreliable")
    +server:sendToAll("playerState", {...})
    +
+ +
+
+
+ 🔗 +

Server:setSerialization(serialize, deserialize)

+ +
+

+ Set the serialization functions for sending and receiving data. +

+

+ Both the client and server must share the same serialization method. +

+ + +

Parameters:

+
+
+ + function + serialize + +
+
+ The serialization function to use. + + +
+ + function + deserialize + +
+
+ The deserialization function to use. + + +
+ + + + +

Usage:

+
    +
    bitser = require "bitser" -- or any library you like
    +server = sock.newServer("localhost", 22122)
    +server:setSerialization(bitser.dumps, bitser.loads)
    +
+ +
+
+
+ 🔗 +

Server:update()

+ +
+

+ Check for network events and handle them. +

+

+ + + +

+ + + + + + + +
+
+
+
+
+
+

Class Client

+
+ +

+ Connects to servers. +

+
+
+ 🔗 +

Client:connect(code)

+ +
+

+ Connect to the chosen server. +

+

+ Connection will not actually occur until the next time Client:update is called. +

+ + +

Parameters:

+
+
+ + optional number + code + +
+
+ A number that can be associated with the connect event. + + +
+ + + + + +
+
+
+ 🔗 +

Client:disconnect(code)

+ +
+

+ Disconnect from the server, if connected. +

+

+ The client will disconnect the + next time that network messages are sent. +

+ + +

Parameters:

+
+
+ + optional number + code + +
+
+ A code to associate with this disconnect event. + + +
+ + + + + +
+
+
+ 🔗 +

Client:disconnectLater(code)

+ +
+

+ Disconnect from the server, if connected. +

+

+ The client will disconnect after + sending all queued packets. +

+ + +

Parameters:

+
+
+ + optional number + code + +
+
+ A code to associate with this disconnect event. + + +
+ + + + + +
+
+
+ 🔗 +

Client:disconnectNow(code)

+ +
+

+ Disconnect from the server, if connected. +

+

+ The client will disconnect immediately. +

+ + +

Parameters:

+
+
+ + optional number + code + +
+
+ A code to associate with this disconnect event. + + +
+ + + + + +
+
+
+ 🔗 +

Client:enableCompression()

+ +
+

+ Enables an adaptive order-2 PPM range coder for the transmitted data of all peers. +

+

+ Both the client and server must both either have compression enabled or disabled.

+ +

Note: lua-enet does not currently expose a way to disable the compression after it has been enabled. +

+ + + + + + + +
+
+
+ 🔗 +

Client:getAddress()

+ +
+

+ Get the IP address or hostname that the client was created with. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + string + + + +
  2. +
+ + + + +
+
+
+ 🔗 +

Client:getConnectId()

+ +
+

+ Get the unique connection id, if connected. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + number + The connection id. +
  2. +
+ + + + +
+
+
+ 🔗 +

Client:getDefaultSendMode()

+ +
+

+ Get the default send mode. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + string + + + +
  2. +
+ + +

See also:

+ + + +
+
+
+ 🔗 +

Client:getIndex()

+ +
+

+ Get the index of the enet peer. +

+

+ All peers of an ENet host are kept in an array. This function finds and returns the index of the peer of its host structure. +

+ + + +

Returns:

+
    +
  1. + number + The index of the peer. +
  2. +
+ + + + +
+
+
+ 🔗 +

Client:getLastServiceTime()

+ +
+

+ Get the last time when network events were serviced. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + number + Timestamp of the last time events were serviced. +
  2. +
+ + + + +
+
+
+ 🔗 +

Client:getMaxChannels()

+ +
+

+ Get the number of allocated channels. +

+

+ Channels are zero-indexed, e.g. 16 channels allocated means that the + maximum channel that can be used is 15. +

+ + + +

Returns:

+
    +
  1. + number + Number of allocated channels. +
  2. +
+ + + + +
+
+
+ 🔗 +

Client:getMessageTimeout()

+ +
+

+ Get the timeout for packets. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + number + Time to wait for incoming packets in milliseconds. + initial default is 0. +
  2. +
+ + + + +
+
+
+ 🔗 +

Client:getPeerByIndex(index)

+ +
+

+ Get the enet_peer that has the given index. +

+

+ + + +

+ + +

Parameters:

+
+
+ + index + +
+
+ + + + + +
+ +

Returns:

+
    +
  1. + enet_peer + The underlying enet peer object. +
  2. +
+ + + + +
+
+
+ 🔗 +

Client:getPort()

+ +
+

+ Get the port that the client is connecting to. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + number + + + +
  2. +
+ + + + +
+
+
+ 🔗 +

Client:getRoundTripTime()

+ +
+

+ Return the round trip time (RTT, or ping) to the server, if connected. +

+

+ It can take a few seconds for the time to approach an accurate value. +

+ + + +

Returns:

+
    +
  1. + number + The round trip time. +
  2. +
+ + + + +
+
+
+ 🔗 +

Client:getSendMode()

+ +
+

+ Get the current send mode. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + string + + + +
  2. +
+ + +

See also:

+ + + +
+
+
+ 🔗 +

Client:getSocketAddress()

+ +
+

+ Get the socket address of the host. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + string + A description of the socket address, in the format "A.B.C.D:port" where A.B.C.D is the IP address of the used socket. +
  2. +
+ + + + +
+
+
+ 🔗 +

Client:getState()

+ +
+

+ Get the current connection state, if connected. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + string + The connection state. +
  2. +
+ + +

See also:

+ + + +
+
+
+ 🔗 +

Client:getTotalReceivedData()

+ +
+

+ Get the total received data since the server was created. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + number + The total received data in bytes. +
  2. +
+ + + + +
+
+
+ 🔗 +

Client:getTotalReceivedPackets()

+ +
+

+ Get the total number of packets (messages) received since the client was created. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + number + The total number of received packets. +
  2. +
+ + +

See also:

+ + + +
+
+
+ 🔗 +

Client:getTotalSentData()

+ +
+

+ Get the total sent data since the server was created. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + number + The total sent data in bytes. +
  2. +
+ + + + +
+
+
+ 🔗 +

Client:getTotalSentPackets()

+ +
+

+ Get the total number of packets (messages) sent since the client was created. +

+

+ Everytime a message is sent or received, the corresponding figure is incremented. + Therefore, this is not necessarily an accurate indicator of how many packets were actually + exchanged over the network. +

+ + + +

Returns:

+
    +
  1. + number + The total number of sent packets. +
  2. +
+ + + + +
+
+
+ 🔗 +

Client:isConnected()

+ +
+

+ Gets whether the client is connected to the server. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + boolean + Whether the client is connected to the server. +
  2. +
+ + + +

Usage:

+
    +
    client:connect()
    +client:isConnected() -- false
    +-- After a few client updates
    +client:isConnected() -- true
    +
+ +
+
+
+ 🔗 +

Client:isConnecting()

+ +
+

+ Gets whether the client is connecting to the server. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + boolean + Whether the client is connected to the server. +
  2. +
+ + + +

Usage:

+
    +
    client:connect()
    +client:isConnecting() -- true
    +-- After a few client updates
    +client:isConnecting() -- false
    +client:isConnected() -- true
    +
+ +
+
+
+ 🔗 +

Client:isDisconnected()

+ +
+

+ Gets whether the client is disconnected from the server. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + boolean + Whether the client is connected to the server. +
  2. +
+ + + +

Usage:

+
    +
    client:disconnect()
    +client:isDisconnected() -- false
    +-- After a few client updates
    +client:isDisconnected() -- true
    +
+ +
+
+
+ 🔗 +

Client:isDisconnecting()

+ +
+

+ Gets whether the client is disconnecting from the server. +

+

+ + + +

+ + + +

Returns:

+
    +
  1. + boolean + Whether the client is connected to the server. +
  2. +
+ + + +

Usage:

+
    +
    client:disconnect()
    +client:isDisconnecting() -- true
    +-- After a few client updates
    +client:isDisconnecting() -- false
    +client:isDisconnected() -- true
    +
+ +
+
+
+ 🔗 +

Client:log(event, data)

+ +
+

+ Log an event. +

+

+ Alias for Client.logger:log. +

+ + +

Parameters:

+
+
+ + string + event + +
+
+ The type of event that happened. + + +
+ + string + data + +
+
+ The message to log. + + +
+ + + + +

Usage:

+
    +
    if somethingBadHappened then
    +    client:log("error", "Something bad happened!")
    +end
    +
+ +
+
+
+ 🔗 +

Client:on(event, callback)

+ +
+

+ Add a callback to an event. +

+

+ + + +

+ + +

Parameters:

+
+
+ + string + event + +
+
+ The event that will trigger the callback. + + +
+ + function + callback + +
+
+ The callback to be triggered. + + +
+ +

Returns:

+
    +
  1. + function + The callback that was passed in. +
  2. +
+ + + +

Usage:

+
    +
    client:on("connect", function(data)
    +    print("Connected to the server!")
    +end)
    +
+ +
+
+
+ 🔗 +

Client:removeCallback(callback)

+ +
+

+ Remove a specific callback for an event. +

+

+ + + +

+ + +

Parameters:

+
+
+ + function + callback + +
+
+ The callback to remove. + + +
+ +

Returns:

+
    +
  1. + boolean + Whether or not the callback was removed. +
  2. +
+ + + +

Usage:

+
    +
    local callback = client:on("chatMessage", function(message)
    +    print(message)
    +end)
    +client:removeCallback(callback)
    +
+ +
+
+
+ 🔗 +

Client:reset(client)

+ +
+

+ Forcefully disconnects the client. +

+

+ The server is not notified of the disconnection. +

+ + +

Parameters:

+
+
+ + Client + client + +
+
+ The client to reset. + + +
+ + + + + +
+
+
+ 🔗 +

Client:resetSendSettings()

+ +
+

+ Reset all send options to their default values. +

+

+ + + +

+ + + + + + + +
+
+
+ 🔗 +

Client:send(event, data)

+ +
+

+ Send a message to the server. +

+

+ + + +

+ + +

Parameters:

+
+
+ + string + event + +
+
+ The event to trigger with this message. + + +
+ + data + +
+
+ The data to send. + + +
+ + + + + +
+
+
+ 🔗 +

Client:setBandwidthLimit(incoming, outgoing)

+ +
+

+ Set the incoming and outgoing bandwidth limits. +

+

+ + + +

+ + +

Parameters:

+
+
+ + number + incoming + +
+
+ The maximum incoming bandwidth in bytes. + + +
+ + number + outgoing + +
+
+ The maximum outgoing bandwidth in bytes. + + +
+ + + + + +
+
+
+ 🔗 +

Client:setDefaultSendChannel(channel)

+ +
+

+ Set the default send channel for all future outgoing messages. +

+

+ The initial default is 0. +

+ + +

Parameters:

+
+
+ + number + channel + +
+
+ Channel to send data on. + + +
+ + + + + +
+
+
+ 🔗 +

Client:setDefaultSendMode(mode)

+ +
+

+ Set the default send mode for all future outgoing messages. +

+

+ + The initial default is "reliable". +

+ + +

Parameters:

+
+
+ + string + mode + +
+
+ A valid send mode. + + +
+ + + +

See also:

+ + + +
+
+
+ 🔗 +

Client:setMaxChannels(limit)

+ +
+

+ Set the maximum number of channels. +

+

+ + + +

+ + +

Parameters:

+
+
+ + number + limit + +
+
+ The maximum number of channels allowed. If it is 0, + then the maximum number of channels available on the system will be used. + + +
+ + + + + +
+
+
+ 🔗 +

Client:setMessageTimeout(timeout)

+ +
+

+ Set the timeout to wait for packets. +

+

+ + + +

+ + +

Parameters:

+
+
+ + number + timeout + +
+
+ Time to wait for incoming packets in milliseconds. The initial + default is 0. + + +
+ + + + + +
+
+
+ 🔗 +

Client:setPingInterval(interval)

+ +
+

+ Set how frequently to ping the server. +

+

+ The round trip time is updated each time a ping is sent. The initial + default is 500ms. +

+ + +

Parameters:

+
+
+ + number + interval + +
+
+ The interval, in milliseconds. + + +
+ + + + + +
+
+
+ 🔗 +

Client:setSchema(event, schema)

+ +
+

+ Set the data schema for an event. +

+

+ Schemas allow you to set a specific format that the data will be sent. If the + client and server both know the format ahead of time, then the table keys + do not have to be sent across the network, which saves bandwidth. +

+ + +

Parameters:

+
+
+ + string + event + +
+
+ The event to set the data schema for. + + +
+ + {string,...} + schema + +
+
+ The data schema. + + +
+ + + + +

Usage:

+
    +
    server = sock.newServer(...)
    +client = sock.newClient(...)
    +
    +-- Without schemas
    +server:send("update", {
    +    x = 4,
    +    y = 100,
    +    vx = -4.5,
    +    vy = 23.1,
    +    rotation = 1.4365,
    +})
    +client:on("update", function(data)
    +    -- data = {
    +    --    x = 4,
    +    --    y = 100,
    +    --    vx = -4.5,
    +    --    vy = 23.1,
    +    --    rotation = 1.4365,
    +    -- }
    +end)
    +
    +
    +-- With schemas
    +client:setSchema("update", {
    +    "x",
    +    "y",
    +    "vx",
    +    "vy",
    +    "rotation",
    +})
    +-- client no longer has to send the keys, saving bandwidth
    +server:send("update", {
    +    4,
    +    100,
    +    -4.5,
    +    23.1,
    +    1.4365,
    +})
    +client:on("update", function(data)
    +    -- data = {
    +    --    x = 4,
    +    --    y = 100,
    +    --    vx = -4.5,
    +    --    vy = 23.1,
    +    --    rotation = 1.4365,
    +    -- }
    +end)
    +
+ +
+
+
+ 🔗 +

Client:setSendChannel(channel)

+ +
+

+ Set the send channel for the next outgoing message. +

+

+ + The channel will be reset after the next message. Channels are zero-indexed + and cannot exceed the maximum number of channels allocated. The initial + default is 0. +

+ + +

Parameters:

+
+
+ + number + channel + +
+
+ Channel to send data on. + + +
+ + + + +

Usage:

+
    +
    client:setSendChannel(2) -- the third channel
    +client:send("important", "The message")
    +
+ +
+
+
+ 🔗 +

Client:setSendMode(mode)

+ +
+

+ Set the send mode for the next outgoing message. +

+

+ + The mode will be reset after the next message is sent. The initial default + is "reliable". +

+ + +

Parameters:

+
+
+ + string + mode + +
+
+ A valid send mode. + + +
+ + + +

See also:

+ + +

Usage:

+
    +
    client:setSendMode("unreliable")
    +client:send("position", {...})
    +
+ +
+
+
+ 🔗 +

Client:setSerialization(serialize, deserialize)

+ +
+

+ Set the serialization functions for sending and receiving data. +

+

+ Both the client and server must share the same serialization method. +

+ + +

Parameters:

+
+
+ + function + serialize + +
+
+ The serialization function to use. + + +
+ + function + deserialize + +
+
+ The deserialization function to use. + + +
+ + + + +

Usage:

+
    +
    bitser = require "bitser" -- or any library you like
    +client = sock.newClient("localhost", 22122)
    +client:setSerialization(bitser.dumps, bitser.loads)
    +
+ +
+
+
+ 🔗 +

Client:setThrottle(interval, acceleration, deceleration)

+ +
+

+ Change the probability at which unreliable packets should not be dropped. +

+

+ + + +

+ + +

Parameters:

+
+
+ + number + interval + +
+
+ Interval, in milliseconds, over which to measure lowest mean RTT. (default: 5000ms) + + +
+ + number + acceleration + +
+
+ Rate at which to increase the throttle probability as mean RTT declines. (default: 2) + + +
+ + number + deceleration + +
+
+ Rate at which to decrease the throttle probability as mean RTT increases. + + +
+ + + + + +
+
+
+ 🔗 +

Client:setTimeout(limit, minimum, maximum)

+ +
+

+ Set the parameters for attempting to reconnect if a timeout is detected. +

+

+ + + +

+ + +

Parameters:

+
+
+ + optional number + limit + +
+
+ A factor that is multiplied with a value that based on the average round trip time to compute the timeout limit. (default: 32) + + +
+ + optional number + minimum + +
+
+ Timeout value in milliseconds that a reliable packet has to be acknowledged if the variable timeout limit was exceeded. (default: 5000) + + +
+ + optional number + maximum + +
+
+ Fixed timeout in milliseconds for which any packet has to be acknowledged. + + +
+ + + + + +
+
+
+ 🔗 +

Client:update()

+ +
+

+ Check for network events and handle them. +

+

+ + + +

+ + + + + + + +
+
+
+
+
+
+

sock

+
+ +
+
+ 🔗 +

newClient(serverOrAddress, port, maxChannels)

+ +
+

+ Creates a new Client instance. +

+

+ + + +

+ + +

Parameters:

+
+
+ + optional string/peer + serverOrAddress + +
+
+ Usually the IP address or hostname to connect to. It can also be an enet peer. (default: "localhost") + + +
+ + optional number + port + +
+
+ Port number of the server to connect to. (default: 22122) + + +
+ + optional number + maxChannels + +
+
+ Maximum channels available to send and receive data. (default: 1) + + +
+ +

Returns:

+
    +
  1. + A new Client object. +
  2. +
+ + +

See also:

+ + +

Usage:

+
    +
    local sock = require "sock"
    +
    + -- Client that will connect to localhost:22122 (by default)
    +client = sock.newClient()
    +
    + -- Client that will connect to localhost:1234
    +client = sock.newClient("localhost", 1234)
    +
    + -- Client that will connect to 123.45.67.89:1234, using two channels
    + -- NOTE: Server must also allocate two channels!
    +client = sock.newClient("123.45.67.89", 1234, 2)
    +
+ +
+
+
+ 🔗 +

newServer(address, port, maxPeers, maxChannels, inBandwidth, outBandwidth)

+ +
+

+ Creates a new Server object. +

+

+ + + +

+ + +

Parameters:

+
+
+ + optional string + address + +
+
+ Hostname or IP address to bind to. (default: "localhost") + + +
+ + optional number + port + +
+
+ Port to listen to for data. (default: 22122) + + +
+ + optional number + maxPeers + +
+
+ Maximum peers that can connect to the server. (default: 64) + + +
+ + optional number + maxChannels + +
+
+ Maximum channels available to send and receive data. (default: 1) + + +
+ + optional number + inBandwidth + +
+
+ Maximum incoming bandwidth (default: 0) + + +
+ + optional number + outBandwidth + +
+
+ Maximum outgoing bandwidth (default: 0) + + +
+ +

Returns:

+
    +
  1. + A new Server object. +
  2. +
+ + +

See also:

+ + +

Usage:

+
    +
    local sock = require "sock"
    +
    + -- Local server hosted on localhost:22122 (by default)
    +server = sock.newServer()
    +
    + -- Local server only, on port 1234
    +server = sock.newServer("localhost", 1234)
    +
    + -- Server hosted on static IP 123.45.67.89, on port 22122
    +server = sock.newServer("123.45.67.89", 22122)
    +
    + -- Server hosted on any IP, on port 22122
    +server = sock.newServer("*", 22122)
    +
    + -- Limit peers to 10, channels to 2
    +server = sock.newServer("*", 22122, 10, 2)
    +
    + -- Limit incoming/outgoing bandwidth to 1kB/s (1000 bytes/s)
    +server = sock.newServer("*", 22122, 10, 2, 1000, 1000)
    +
+ +
+
+
+
+
+ + +
+
+
+Generated by LDoc 1.4.3 +Last updated 2017-07-24 20:20:49 +
+
+ + diff --git a/lib/sock/docs/ldoc.css b/lib/sock/docs/ldoc.css new file mode 100644 index 0000000..ea29eeb --- /dev/null +++ b/lib/sock/docs/ldoc.css @@ -0,0 +1,223 @@ +/* + * Tag styles + */ +body { + box-sizing: border-box; + margin: 0; + color: #222; + font-size: 1.8em; +} + +ul, ol { + list-style-type: disc; +} + +pre { + background-color: #f7f7f7; + border: 1px solid #ccc; + border-radius: 3px; + box-shadow: 0px 2px 1px #eee; + padding: 10px; + margin: 10px 0 10px 0; + overflow: auto; +} + +p { + max-width: 70ch; +} + +a { + color: #07f; + text-decoration: none; +} +/* make the target distinct; helps when we're navigating to a function */ +a:target + * { + background-color: #fff824; +} + +/* + * Class styles + */ +.header { + background: #56CCF2; /* fallback for old browsers */ + background: -webkit-linear-gradient(to right, #2F80ED, #56CCF2); /* Chrome 10-25, Safari 5.1-6 */ + background: linear-gradient(to right, #2F80ED, #56CCF2); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ + color: #fff; + text-align: center; + padding: 4rem; +} + +.project-title { + margin: 0; +} + +#main { + padding: 1.5rem; + background: #fff; +} + +#navigation { + float: left; + width: 12.5rem; + vertical-align: top; + overflow: visible; +} + +/* Sidebar */ +.sidebar { + flex: 1 0 40rem; + z-index: 99; + transition: all 0.2s ease; +} +#sidebar_toggle { + display: none; +} +.sidebar_toggle_label { + display: flex; + position: fixed; + justify-content: center; + align-items: center; + z-index: 999; + width: 3rem; + height: 3rem; + margin: 0.5rem; + background-color: #eee; + border: 1px solid #bbb; + border-radius: 3px; + opacity: 1; + cursor: pointer; +} +.sidebar_toggle:checked ~ .sidebar { + z-index: -1; + transform: translateX(-100%); + opacity: 0; +} +.sidebar_toggle:checked ~ .contents { + transform: translateX(0); + margin-right: 0; +} +.sidebar_toggle_label:before { + content: '◀'; +} +.sidebar_toggle:checked + .sidebar_toggle_label:before { + content: '▶'; +} + +.table-of-contents { + position: fixed; + overflow-y: scroll; + top: 0; + bottom: 0; + width: 40rem; + padding: 1.5rem; + padding-top: 4rem; + background: #f7f7f7; +} + +.contents { + transition: transform 0.2s ease; + transform: translateX(40rem); + margin-right: 40rem; +} + +.section { + margin-top: 3rem; +} +.section-header { + font-weight: bold; + border-bottom: 1px solid #ccc; + margin-top: 8rem; +} +.section-description { +} +.section-content { + padding-left: 1rem; +} + +.function_def { + margin-top: 2.5rem; +} +.function_def:first-child { + margin-top: 0; +} + +.function_def li { + padding-bottom: 4px; +} + +.function_name { + display: inline-block; + font-weight: 400; +} + +.anchor_link { + font-size: 85%; +} +.anchor_link:focus { +} + +.type { + font-weight: bolder; + font-style: italic; +} +.parameter_info .type { + font-weight: bold; +} + +.parameter_info { + background: #f7f7f7; + padding: 5px; + font-family: monospace; +} + +/* + * Syntax highlighting + */ +pre .comment { color: #3a3432; font-style: italic; } +pre .constant { color: #01a252; } +pre .string { color: #01a252; } +pre .number { color: #01a252; } +pre .escape { color: #844631; } +pre .library { color: #0e7c6b; } +pre .marker { color: #512b1e; background: #fedc56; } +pre .operator { color: #4a4543; } +pre .keyword { color: #a16a94; } +pre .user-keyword { color: #01a0e4; } +pre .preprocessor, +pre .prepro { color: #db2d20; } +pre .global { color: #db2d20; } +pre .prompt { color: #558817; } +pre .url { color: #272fc2; text-decoration: underline; } + +/* print rules */ +@media print { + body { + font: 12pt "Times New Roman", "TimeNR", Times, serif; + } + a { font-weight: bold; color: #004080; text-decoration: underline; } + + #main { + background-color: #ffffff; + border-left: 0px; + } + + #container { + margin-left: 2%; + margin-right: 2%; + background-color: #ffffff; + } + + #content { + padding: 1em; + background-color: #ffffff; + } + + #navigation { + display: none; + } + pre.example { + font-family: "Droid Sans Mono", "Consolas", "Andale Mono", monospace; + font-size: 10pt; + page-break-inside: avoid; + } +} diff --git a/lib/sock/docs/source/sock.lua.html b/lib/sock/docs/source/sock.lua.html new file mode 100644 index 0000000..64fbc64 --- /dev/null +++ b/lib/sock/docs/source/sock.lua.html @@ -0,0 +1,1461 @@ + + + + sock.lua Documentation + + + + + + + + + + + + + +
+ + +
+

sock.lua

+
+ +
+ + + +
+ + +

sock.lua

+
+
+--- A Lua networking library for LÖVE games.
+-- * [Source code](https://github.com/camchenry/sock.lua)
+-- * [Examples](https://github.com/camchenry/sock.lua/tree/master/examples)
+-- @module sock
+
+local sock = {
+    _VERSION     = 'sock.lua v0.3.0',
+    _DESCRIPTION = 'A Lua networking library for LÖVE games',
+    _URL         = 'https://github.com/camchenry/sock.lua',
+    _LICENSE     = [[
+        MIT License
+
+        Copyright (c) 2016 Cameron McHenry
+
+        Permission is hereby granted, free of charge, to any person obtaining a copy
+        of this software and associated documentation files (the "Software"), to deal
+        in the Software without restriction, including without limitation the rights
+        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+        copies of the Software, and to permit persons to whom the Software is
+        furnished to do so, subject to the following conditions:
+
+        The above copyright notice and this permission notice shall be included in all
+        copies or substantial portions of the Software.
+
+        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+        SOFTWARE.
+    ]]
+}
+
+require "enet"
+
+-- Current folder trick
+-- http://kiki.to/blog/2014/04/12/rule-5-beware-of-multiple-files/
+local currentFolder = (...):gsub('%.[^%.]+$', '')
+
+local bitserLoaded = false
+
+if bitser then
+    bitserLoaded = true
+end
+
+-- Try to load some common serialization libraries
+-- This is for convenience, you may still specify your own serializer
+if not bitserLoaded then
+    bitserLoaded, bitser = pcall(require, "bitser")
+end
+
+-- Try to load relatively
+if not bitserLoaded then
+    bitserLoaded, bitser = pcall(require, currentFolder .. ".bitser")
+end
+
+-- links variables to keys based on their order
+-- note that it only works for boolean and number values, not strings
+local function zipTable(items, keys)
+    local data = {}
+
+    -- convert variable at index 1 into the value for the key value at index 1, and so on
+    for i, value in ipairs(items) do
+        local key = keys[i]
+
+        data[key] = value
+    end
+
+    return data
+end
+
+--- All of the possible connection statuses for a client connection.
+-- @see Client:getState
+sock.CONNECTION_STATES = {
+    "disconnected",             -- Disconnected from the server.
+    "connecting",               -- In the process of connecting to the server.
+    "acknowledging_connect",    --
+    "connection_pending",       --
+    "connection_succeeded",     --
+    "connected",                -- Successfully connected to the server.
+    "disconnect_later",         -- Disconnecting, but only after sending all queued packets.
+    "disconnecting",            -- In the process of disconnecting from the server.
+    "acknowledging_disconnect", --
+    "zombie",                   --
+    "unknown",                  --
+}
+
+--- States that represent the client connecting to a server.
+sock.CONNECTING_STATES = {
+    "connecting",               -- In the process of connecting to the server.
+    "acknowledging_connect",    --
+    "connection_pending",       --
+    "connection_succeeded",     --
+}
+
+--- States that represent the client disconnecting from a server.
+sock.DISCONNECTING_STATES = {
+    "disconnect_later",         -- Disconnecting, but only after sending all queued packets.
+    "disconnecting",            -- In the process of disconnecting from the server.
+    "acknowledging_disconnect", --
+}
+
+--- Valid modes for sending messages.
+sock.SEND_MODES = {
+    "reliable",     -- Message is guaranteed to arrive, and arrive in the order in which it is sent.
+    "unsequenced",  -- Message has no guarantee on the order that it arrives.
+    "unreliable",   -- Message is not guaranteed to arrive.
+}
+
+local function isValidSendMode(mode)
+    for _, validMode in ipairs(sock.SEND_MODES) do
+        if mode == validMode then
+            return true
+        end
+    end
+    return false
+end
+
+local Logger = {}
+local Logger_mt = {__index = Logger}
+
+local function newLogger(source)
+    local logger = setmetatable({
+        source          = source,
+        messages        = {},
+
+        -- Makes print info more concise, but should still log the full line
+        shortenLines    = true,
+        -- Print all incoming event data
+        printEventData  = false,
+        printErrors     = true,
+        printWarnings   = true,
+    }, Logger_mt)
+
+    return logger
+end
+
+function Logger:log(event, data)
+    local time = os.date("%X") -- something like 24:59:59
+    local shortLine = ("[%s] %s"):format(event, data)
+    local fullLine  = ("[%s][%s][%s] %s"):format(self.source, time, event, data)
+
+    -- The printed message may or may not be the full message
+    local line = fullLine
+    if self.shortenLines then
+        line = shortLine
+    end
+
+    if self.printEventData then
+        print(line)
+    elseif self.printErrors and event == "error" then
+        print(line)
+    elseif self.printWarnings and event == "warning" then
+        print(line)
+    end
+
+    -- The logged message is always the full message
+    table.insert(self.messages, fullLine)
+
+    -- TODO: Dump to a log file
+end
+
+local Listener = {}
+local Listener_mt = {__index = Listener}
+
+local function newListener()
+    local listener = setmetatable({
+        triggers        = {},
+        schemas         = {},
+    }, Listener_mt)
+
+    return listener
+end
+
+-- Adds a callback to a trigger
+-- Returns: the callback function
+function Listener:addCallback(event, callback)
+    if not self.triggers[event] then
+        self.triggers[event] = {}
+    end
+
+    table.insert(self.triggers[event], callback)
+
+    return callback
+end
+
+-- Removes a callback on a given trigger
+-- Returns a boolean indicating if the callback was removed
+function Listener:removeCallback(callback)
+    for _, triggers in pairs(self.triggers) do
+        for i, trigger in pairs(triggers) do
+            if trigger == callback then
+                table.remove(triggers, i)
+                return true
+            end
+        end
+    end
+    return false
+end
+
+-- Accepts: event (string), schema (table)
+-- Returns: nothing
+function Listener:setSchema(event, schema)
+    self.schemas[event] = schema
+end
+
+-- Activates all callbacks for a trigger
+-- Returns a boolean indicating if any callbacks were triggered
+function Listener:trigger(event, data, client)
+    if self.triggers[event] then
+        for _, trigger in pairs(self.triggers[event]) do
+            -- Event has a pre-existing schema defined
+            if self.schemas[event] then
+                data = zipTable(data, self.schemas[event])
+            end
+            trigger(data, client)
+        end
+        return true
+    else
+        return false
+    end
+end
+
+--- Manages all clients and receives network events.
+-- @type Server
+local Server = {}
+local Server_mt = {__index = Server}
+
+--- Check for network events and handle them.
+function Server:update()
+    local event = self.host:service(self.messageTimeout)
+
+    while event do
+        if event.type == "connect" then
+            local eventClient = sock.newClient(event.peer)
+            eventClient:setSerialization(self.serialize, self.deserialize)
+            table.insert(self.peers, event.peer)
+            table.insert(self.clients, eventClient)
+            self:_activateTriggers("connect", event.data, eventClient)
+            self:log(event.type, tostring(event.peer) .. " connected")
+
+        elseif event.type == "receive" then
+            local eventName, data = self:__unpack(event.data)
+            local eventClient = self:getClient(event.peer)
+
+            self:_activateTriggers(eventName, data, eventClient)
+            self:log(eventName, data)
+
+        elseif event.type == "disconnect" then
+            -- remove from the active peer list
+            for i, peer in pairs(self.peers) do
+                if peer == event.peer then
+                    table.remove(self.peers, i)
+                end
+            end
+            local eventClient = self:getClient(event.peer)
+            for i, client in pairs(self.clients) do
+                if client == eventClient then
+                    table.remove(self.clients, i)
+                end
+            end
+            self:_activateTriggers("disconnect", event.data, eventClient)
+            self:log(event.type, tostring(event.peer) .. " disconnected")
+
+        end
+
+        event = self.host:service(self.messageTimeout)
+    end
+end
+
+-- Creates the unserialized message that will be used in callbacks
+-- In: serialized message (string)
+-- Out: event (string), data (mixed)
+function Server:__unpack(data)
+    if not self.deserialize then
+        self:log("error", "Can't deserialize message: deserialize was not set")
+        error("Can't deserialize message: deserialize was not set")
+    end
+
+    local message = self.deserialize(data)
+    local eventName, data = message[1], message[2]
+    return eventName, data
+end
+
+-- Creates the serialized message that will be sent over the network
+-- In: event (string), data (mixed)
+-- Out: serialized message (string)
+function Server:__pack(event, data)
+    local message = {event, data}
+    local serializedMessage
+
+    if not self.serialize then
+        self:log("error", "Can't serialize message: serialize was not set")
+        error("Can't serialize message: serialize was not set")
+    end
+
+    -- 'Data' = binary data class in Love
+    if type(data) == "userdata" and data.type and data:typeOf("Data") then
+        message[2] = data:getString()
+        serializedMessage = self.serialize(message)
+    else
+        serializedMessage = self.serialize(message)
+    end
+
+    return serializedMessage
+end
+
+--- Send a message to all clients, except one.
+-- Useful for when the client does something locally, but other clients
+-- need to be updated at the same time. This way avoids duplicating objects by
+-- never sending its own event to itself in the first place.
+-- @tparam Client client The client to not receive the message.
+-- @tparam string event The event to trigger with this message.
+-- @param data The data to send.
+function Server:sendToAllBut(client, event, data)
+    local serializedMessage = self:__pack(event, data)
+
+    for _, p in pairs(self.peers) do
+        if p ~= client.connection then
+            self.packetsSent = self.packetsSent + 1
+            p:send(serializedMessage, self.sendChannel, self.sendMode)
+        end
+    end
+
+    self:resetSendSettings()
+end
+
+--- Send a message to all clients.
+-- @tparam string event The event to trigger with this message.
+-- @param data The data to send.
+--@usage
+--server:sendToAll("gameStarting", true)
+function Server:sendToAll(event, data)
+    local serializedMessage = self:__pack(event, data)
+
+    self.packetsSent = self.packetsSent + #self.peers
+
+    self.host:broadcast(serializedMessage, self.sendChannel, self.sendMode)
+
+    self:resetSendSettings()
+end
+
+--- Send a message to a single peer. Useful to send data to a newly connected player
+-- without sending to everyone who already received it.
+-- @tparam enet_peer peer The enet peer to receive the message.
+-- @tparam string event The event to trigger with this message.
+-- @param data data to send to the peer.
+--@usage
+--server:sendToPeer(peer, "initialGameInfo", {...})
+function Server:sendToPeer(peer, event, data)
+    local serializedMessage = self:__pack(event, data)
+
+    self.packetsSent = self.packetsSent + 1
+
+    peer:send(serializedMessage, self.sendChannel, self.sendMode)
+
+    self:resetSendSettings()
+end
+
+--- Add a callback to an event.
+-- @tparam string event The event that will trigger the callback.
+-- @tparam function callback The callback to be triggered.
+-- @treturn function The callback that was passed in.
+--@usage
+--server:on("connect", function(data, client)
+--    print("Client connected!")
+--end)
+function Server:on(event, callback)
+    return self.listener:addCallback(event, callback)
+end
+
+function Server:_activateTriggers(event, data, client)
+    local result = self.listener:trigger(event, data, client)
+
+    self.packetsReceived = self.packetsReceived + 1
+
+    if not result then
+        self:log("warning", "Tried to activate trigger: '" .. tostring(event) .. "' but it does not exist.")
+    end
+end
+
+--- Remove a specific callback for an event.
+-- @tparam function callback The callback to remove.
+-- @treturn boolean Whether or not the callback was removed.
+--@usage
+--local callback = server:on("chatMessage", function(message)
+--    print(message)
+--end)
+--server:removeCallback(callback)
+function Server:removeCallback(callback)
+    return self.listener:removeCallback(callback)
+end
+
+--- Log an event.
+-- Alias for Server.logger:log.
+-- @tparam string event The type of event that happened.
+-- @tparam string data The message to log.
+--@usage
+--if somethingBadHappened then
+--    server:log("error", "Something bad happened!")
+--end
+function Server:log(event, data)
+    return self.logger:log(event, data)
+end
+
+--- Reset all send options to their default values.
+function Server:resetSendSettings()
+    self.sendMode = self.defaultSendMode
+    self.sendChannel = self.defaultSendChannel
+end
+
+--- Enables an adaptive order-2 PPM range coder for the transmitted data of all peers. Both the client and server must both either have compression enabled or disabled.
+--
+-- Note: lua-enet does not currently expose a way to disable the compression after it has been enabled.
+function Server:enableCompression()
+    return self.host:compress_with_range_coder()
+end
+
+--- Destroys the server and frees the port it is bound to.
+function Server:destroy()
+    self.host:destroy()
+end
+
+--- Set the send mode for the next outgoing message.
+-- The mode will be reset after the next message is sent. The initial default
+-- is "reliable".
+-- @tparam string mode A valid send mode.
+-- @see SEND_MODES
+-- @usage
+--server:setSendMode("unreliable")
+--server:sendToAll("playerState", {...})
+function Server:setSendMode(mode)
+    if not isValidSendMode(mode) then
+        self:log("warning", "Tried to use invalid send mode: '" .. mode .. "'. Defaulting to reliable.")
+        mode = "reliable"
+    end
+
+    self.sendMode = mode
+end
+
+--- Set the default send mode for all future outgoing messages.
+-- The initial default is "reliable".
+-- @tparam string mode A valid send mode.
+-- @see SEND_MODES
+function Server:setDefaultSendMode(mode)
+    if not isValidSendMode(mode) then
+        self:log("error", "Tried to set default send mode to invalid mode: '" .. mode .. "'")
+        error("Tried to set default send mode to invalid mode: '" .. mode .. "'")
+    end
+
+    self.defaultSendMode = mode
+end
+
+--- Set the send channel for the next outgoing message.
+-- The channel will be reset after the next message. Channels are zero-indexed
+-- and cannot exceed the maximum number of channels allocated. The initial
+-- default is 0.
+-- @tparam number channel Channel to send data on.
+-- @usage
+--server:setSendChannel(2) -- the third channel
+--server:sendToAll("importantEvent", "The message")
+function Server:setSendChannel(channel)
+    if channel > (self.maxChannels - 1) then
+        self:log("warning", "Tried to use invalid channel: " .. channel .. " (max is " .. self.maxChannels - 1 .. "). Defaulting to 0.")
+        channel = 0
+    end
+
+    self.sendChannel = channel
+end
+
+--- Set the default send channel for all future outgoing messages.
+-- The initial default is 0.
+-- @tparam number channel Channel to send data on.
+function Server:setDefaultSendChannel(channel)
+   self.defaultSendChannel = channel
+end
+
+--- Set the data schema for an event.
+--
+-- Schemas allow you to set a specific format that the data will be sent. If the
+-- client and server both know the format ahead of time, then the table keys
+-- do not have to be sent across the network, which saves bandwidth.
+-- @tparam string event The event to set the data schema for.
+-- @tparam {string,...} schema The data schema.
+-- @usage
+-- server = sock.newServer(...)
+-- client = sock.newClient(...)
+--
+-- -- Without schemas
+-- client:send("update", {
+--     x = 4,
+--     y = 100,
+--     vx = -4.5,
+--     vy = 23.1,
+--     rotation = 1.4365,
+-- })
+-- server:on("update", function(data, client)
+--     -- data = {
+--     --    x = 4,
+--     --    y = 100,
+--     --    vx = -4.5,
+--     --    vy = 23.1,
+--     --    rotation = 1.4365,
+--     -- }
+-- end)
+--
+--
+-- -- With schemas
+-- server:setSchema("update", {
+--     "x",
+--     "y",
+--     "vx",
+--     "vy",
+--     "rotation",
+-- })
+-- -- client no longer has to send the keys, saving bandwidth
+-- client:send("update", {
+--     4,
+--     100,
+--     -4.5,
+--     23.1,
+--     1.4365,
+-- })
+-- server:on("update", function(data, client)
+--     -- data = {
+--     --    x = 4,
+--     --    y = 100,
+--     --    vx = -4.5,
+--     --    vy = 23.1,
+--     --    rotation = 1.4365,
+--     -- }
+-- end)
+function Server:setSchema(event, schema)
+    return self.listener:setSchema(event, schema)
+end
+
+--- Set the incoming and outgoing bandwidth limits.
+-- @tparam number incoming The maximum incoming bandwidth in bytes.
+-- @tparam number outgoing The maximum outgoing bandwidth in bytes.
+function Server:setBandwidthLimit(incoming, outgoing)
+    return self.host:bandwidth_limit(incoming, outgoing)
+end
+
+--- Set the maximum number of channels.
+-- @tparam number limit The maximum number of channels allowed. If it is 0,
+-- then the maximum number of channels available on the system will be used.
+function Server:setMaxChannels(limit)
+    self.host:channel_limit(limit)
+end
+
+--- Set the timeout to wait for packets.
+-- @tparam number timeout Time to wait for incoming packets in milliseconds. The
+-- initial default is 0.
+function Server:setMessageTimeout(timeout)
+    self.messageTimeout = timeout
+end
+
+--- Set the serialization functions for sending and receiving data.
+-- Both the client and server must share the same serialization method.
+-- @tparam function serialize The serialization function to use.
+-- @tparam function deserialize The deserialization function to use.
+-- @usage
+--bitser = require "bitser" -- or any library you like
+--server = sock.newServer("localhost", 22122)
+--server:setSerialization(bitser.dumps, bitser.loads)
+function Server:setSerialization(serialize, deserialize)
+    assert(type(serialize) == "function", "Serialize must be a function, got: '"..type(serialize).."'")
+    assert(type(deserialize) == "function", "Deserialize must be a function, got: '"..type(deserialize).."'")
+    self.serialize = serialize
+    self.deserialize = deserialize
+end
+
+--- Gets the Client object associated with an enet peer.
+-- @tparam peer peer An enet peer.
+-- @treturn Client Object associated with the peer.
+function Server:getClient(peer)
+    for _, client in pairs(self.clients) do
+        if peer == client.connection then
+            return client
+        end
+    end
+end
+
+--- Gets the Client object that has the given connection id.
+-- @tparam number connectId The unique client connection id.
+-- @treturn Client
+function Server:getClientByConnectId(connectId)
+    for _, client in pairs(self.clients) do
+        if connectId == client.connectId then
+            return client
+        end
+    end
+end
+
+--- Get the Client object that has the given peer index.
+-- @treturn Client
+function Server:getClientByIndex(index)
+    for _, client in pairs(self.clients) do
+        if index == client:getIndex() then
+            return client
+        end
+    end
+end
+
+--- Get the enet_peer that has the given index.
+-- @treturn enet_peer The underlying enet peer object.
+function Server:getPeerByIndex(index)
+    return self.host:get_peer(index)
+end
+
+--- Get the total sent data since the server was created.
+-- @treturn number The total sent data in bytes.
+function Server:getTotalSentData()
+    return self.host:total_sent_data()
+end
+
+--- Get the total received data since the server was created.
+-- @treturn number The total received data in bytes.
+function Server:getTotalReceivedData()
+    return self.host:total_received_data()
+end
+--- Get the total number of packets (messages) sent since the server was created.
+-- Everytime a message is sent or received, the corresponding figure is incremented.
+-- Therefore, this is not necessarily an accurate indicator of how many packets were actually
+-- exchanged over the network.
+-- @treturn number The total number of sent packets.
+function Server:getTotalSentPackets()
+    return self.packetsSent
+end
+
+--- Get the total number of packets (messages) received since the server was created.
+-- @treturn number The total number of received packets.
+-- @see Server:getTotalSentPackets
+function Server:getTotalReceivedPackets()
+    return self.packetsReceived
+end
+
+--- Get the last time when network events were serviced.
+-- @treturn number Timestamp of the last time events were serviced.
+function Server:getLastServiceTime()
+    return self.host:service_time()
+end
+
+--- Get the number of allocated slots for peers.
+-- @treturn number Number of allocated slots.
+function Server:getMaxPeers()
+    return self.maxPeers
+end
+
+--- Get the number of allocated channels.
+-- Channels are zero-indexed, e.g. 16 channels allocated means that the
+-- maximum channel that can be used is 15.
+-- @treturn number Number of allocated channels.
+function Server:getMaxChannels()
+    return self.maxChannels
+end
+
+--- Get the timeout for packets.
+-- @treturn number Time to wait for incoming packets in milliseconds.
+-- initial default is 0.
+function Server:getMessageTimeout()
+    return self.messageTimeout
+end
+
+--- Get the socket address of the host.
+-- @treturn string A description of the socket address, in the format
+-- "A.B.C.D:port" where A.B.C.D is the IP address of the used socket.
+function Server:getSocketAddress()
+    return self.host:get_socket_address()
+end
+
+--- Get the current send mode.
+-- @treturn string
+-- @see SEND_MODES
+function Server:getSendMode()
+    return self.sendMode
+end
+
+--- Get the default send mode.
+-- @treturn string
+-- @see SEND_MODES
+function Server:getDefaultSendMode()
+    return self.defaultSendMode
+end
+
+--- Get the IP address or hostname that the server was created with.
+-- @treturn string
+function Server:getAddress()
+    return self.address
+end
+
+--- Get the port that the server is hosted on.
+-- @treturn number
+function Server:getPort()
+    return self.port
+end
+
+--- Get the table of Clients actively connected to the server.
+-- @return {Client,...}
+function Server:getClients()
+    return self.clients
+end
+
+--- Get the number of Clients that are currently connected to the server.
+-- @treturn number The number of active clients.
+function Server:getClientCount()
+    return #self.clients
+end
+
+
+--- Connects to servers.
+-- @type Client
+local Client = {}
+local Client_mt = {__index = Client}
+
+--- Check for network events and handle them.
+function Client:update()
+    local event = self.host:service(self.messageTimeout)
+
+    while event do
+        if event.type == "connect" then
+            self:_activateTriggers("connect", event.data)
+            self:log(event.type, "Connected to " .. tostring(self.connection))
+        elseif event.type == "receive" then
+            local eventName, data = self:__unpack(event.data)
+
+            self:_activateTriggers(eventName, data)
+            self:log(eventName, data)
+
+        elseif event.type == "disconnect" then
+            self:_activateTriggers("disconnect", event.data)
+            self:log(event.type, "Disconnected from " .. tostring(self.connection))
+        end
+
+        event = self.host:service(self.messageTimeout)
+    end
+end
+
+--- Connect to the chosen server.
+-- Connection will not actually occur until the next time Client:update is called.
+-- @tparam ?number code A number that can be associated with the connect event.
+function Client:connect(code)
+    -- number of channels for the client and server must match
+    self.connection = self.host:connect(self.address .. ":" .. self.port, self.maxChannels, code)
+    self.connectId = self.connection:connect_id()
+end
+
+--- Disconnect from the server, if connected. The client will disconnect the
+-- next time that network messages are sent.
+-- @tparam ?number code A code to associate with this disconnect event.
+-- @todo Pass the code into the disconnect callback on the server
+function Client:disconnect(code)
+    code = code or 0
+    self.connection:disconnect(code)
+end
+
+--- Disconnect from the server, if connected. The client will disconnect after
+-- sending all queued packets.
+-- @tparam ?number code A code to associate with this disconnect event.
+-- @todo Pass the code into the disconnect callback on the server
+function Client:disconnectLater(code)
+    code = code or 0
+    self.connection:disconnect_later(code)
+end
+
+--- Disconnect from the server, if connected. The client will disconnect immediately.
+-- @tparam ?number code A code to associate with this disconnect event.
+-- @todo Pass the code into the disconnect callback on the server
+function Client:disconnectNow(code)
+    code = code or 0
+    self.connection:disconnect_now(code)
+end
+
+--- Forcefully disconnects the client. The server is not notified of the disconnection.
+-- @tparam Client client The client to reset.
+function Client:reset()
+    if self.connection then
+        self.connection:reset()
+    end
+end
+
+-- Creates the unserialized message that will be used in callbacks
+-- In: serialized message (string)
+-- Out: event (string), data (mixed)
+function Client:__unpack(data)
+    if not self.deserialize then
+        self:log("error", "Can't deserialize message: deserialize was not set")
+        error("Can't deserialize message: deserialize was not set")
+    end
+
+    local message = self.deserialize(data)
+    local eventName, data = message[1], message[2]
+    return eventName, data
+end
+
+-- Creates the serialized message that will be sent over the network
+-- In: event (string), data (mixed)
+-- Out: serialized message (string)
+function Client:__pack(event, data)
+    local message = {event, data}
+    local serializedMessage
+
+    if not self.serialize then
+        self:log("error", "Can't serialize message: serialize was not set")
+        error("Can't serialize message: serialize was not set")
+    end
+
+    -- 'Data' = binary data class in Love
+    if type(data) == "userdata" and data.type and data:typeOf("Data") then
+        message[2] = data:getString()
+        serializedMessage = self.serialize(message)
+    else
+        serializedMessage = self.serialize(message)
+    end
+
+    return serializedMessage
+end
+
+--- Send a message to the server.
+-- @tparam string event The event to trigger with this message.
+-- @param data The data to send.
+function Client:send(event, data)
+    local serializedMessage = self:__pack(event, data)
+
+    self.connection:send(serializedMessage, self.sendChannel, self.sendMode)
+
+    self.packetsSent = self.packetsSent + 1
+
+    self:resetSendSettings()
+end
+
+--- Add a callback to an event.
+-- @tparam string event The event that will trigger the callback.
+-- @tparam function callback The callback to be triggered.
+-- @treturn function The callback that was passed in.
+--@usage
+--client:on("connect", function(data)
+--    print("Connected to the server!")
+--end)
+function Client:on(event, callback)
+    return self.listener:addCallback(event, callback)
+end
+
+function Client:_activateTriggers(event, data)
+    local result = self.listener:trigger(event, data)
+
+    self.packetsReceived = self.packetsReceived + 1
+
+    if not result then
+        self:log("warning", "Tried to activate trigger: '" .. tostring(event) .. "' but it does not exist.")
+    end
+end
+
+--- Remove a specific callback for an event.
+-- @tparam function callback The callback to remove.
+-- @treturn boolean Whether or not the callback was removed.
+--@usage
+--local callback = client:on("chatMessage", function(message)
+--    print(message)
+--end)
+--client:removeCallback(callback)
+function Client:removeCallback(callback)
+    return self.listener:removeCallback(callback)
+end
+
+--- Log an event.
+-- Alias for Client.logger:log.
+-- @tparam string event The type of event that happened.
+-- @tparam string data The message to log.
+--@usage
+--if somethingBadHappened then
+--    client:log("error", "Something bad happened!")
+--end
+function Client:log(event, data)
+    return self.logger:log(event, data)
+end
+
+--- Reset all send options to their default values.
+function Client:resetSendSettings()
+    self.sendMode = self.defaultSendMode
+    self.sendChannel = self.defaultSendChannel
+end
+
+--- Enables an adaptive order-2 PPM range coder for the transmitted data of all peers. Both the client and server must both either have compression enabled or disabled.
+--
+-- Note: lua-enet does not currently expose a way to disable the compression after it has been enabled.
+function Client:enableCompression()
+    return self.host:compress_with_range_coder()
+end
+
+--- Set the send mode for the next outgoing message.
+-- The mode will be reset after the next message is sent. The initial default
+-- is "reliable".
+-- @tparam string mode A valid send mode.
+-- @see SEND_MODES
+-- @usage
+--client:setSendMode("unreliable")
+--client:send("position", {...})
+function Client:setSendMode(mode)
+    if not isValidSendMode(mode) then
+        self:log("warning", "Tried to use invalid send mode: '" .. mode .. "'. Defaulting to reliable.")
+        mode = "reliable"
+    end
+
+    self.sendMode = mode
+end
+
+--- Set the default send mode for all future outgoing messages.
+-- The initial default is "reliable".
+-- @tparam string mode A valid send mode.
+-- @see SEND_MODES
+function Client:setDefaultSendMode(mode)
+    if not isValidSendMode(mode) then
+        self:log("error", "Tried to set default send mode to invalid mode: '" .. mode .. "'")
+        error("Tried to set default send mode to invalid mode: '" .. mode .. "'")
+    end
+
+    self.defaultSendMode = mode
+end
+
+--- Set the send channel for the next outgoing message.
+-- The channel will be reset after the next message. Channels are zero-indexed
+-- and cannot exceed the maximum number of channels allocated. The initial
+-- default is 0.
+-- @tparam number channel Channel to send data on.
+-- @usage
+--client:setSendChannel(2) -- the third channel
+--client:send("important", "The message")
+function Client:setSendChannel(channel)
+    if channel > (self.maxChannels - 1) then
+        self:log("warning", "Tried to use invalid channel: " .. channel .. " (max is " .. self.maxChannels - 1 .. "). Defaulting to 0.")
+        channel = 0
+    end
+
+    self.sendChannel = channel
+end
+
+--- Set the default send channel for all future outgoing messages.
+-- The initial default is 0.
+-- @tparam number channel Channel to send data on.
+function Client:setDefaultSendChannel(channel)
+    self.defaultSendChannel = channel
+end
+
+--- Set the data schema for an event.
+--
+-- Schemas allow you to set a specific format that the data will be sent. If the
+-- client and server both know the format ahead of time, then the table keys
+-- do not have to be sent across the network, which saves bandwidth.
+-- @tparam string event The event to set the data schema for.
+-- @tparam {string,...} schema The data schema.
+-- @usage
+-- server = sock.newServer(...)
+-- client = sock.newClient(...)
+--
+-- -- Without schemas
+-- server:send("update", {
+--     x = 4,
+--     y = 100,
+--     vx = -4.5,
+--     vy = 23.1,
+--     rotation = 1.4365,
+-- })
+-- client:on("update", function(data)
+--     -- data = {
+--     --    x = 4,
+--     --    y = 100,
+--     --    vx = -4.5,
+--     --    vy = 23.1,
+--     --    rotation = 1.4365,
+--     -- }
+-- end)
+--
+--
+-- -- With schemas
+-- client:setSchema("update", {
+--     "x",
+--     "y",
+--     "vx",
+--     "vy",
+--     "rotation",
+-- })
+-- -- client no longer has to send the keys, saving bandwidth
+-- server:send("update", {
+--     4,
+--     100,
+--     -4.5,
+--     23.1,
+--     1.4365,
+-- })
+-- client:on("update", function(data)
+--     -- data = {
+--     --    x = 4,
+--     --    y = 100,
+--     --    vx = -4.5,
+--     --    vy = 23.1,
+--     --    rotation = 1.4365,
+--     -- }
+-- end)
+function Client:setSchema(event, schema)
+    return self.listener:setSchema(event, schema)
+end
+
+--- Set the maximum number of channels.
+-- @tparam number limit The maximum number of channels allowed. If it is 0,
+-- then the maximum number of channels available on the system will be used.
+function Client:setMaxChannels(limit)
+    self.host:channel_limit(limit)
+end
+
+--- Set the timeout to wait for packets.
+-- @tparam number timeout Time to wait for incoming packets in milliseconds. The initial
+-- default is 0.
+function Client:setMessageTimeout(timeout)
+    self.messageTimeout = timeout
+end
+
+--- Set the incoming and outgoing bandwidth limits.
+-- @tparam number incoming The maximum incoming bandwidth in bytes.
+-- @tparam number outgoing The maximum outgoing bandwidth in bytes.
+function Client:setBandwidthLimit(incoming, outgoing)
+    return self.host:bandwidth_limit(incoming, outgoing)
+end
+
+--- Set how frequently to ping the server.
+-- The round trip time is updated each time a ping is sent. The initial
+-- default is 500ms.
+-- @tparam number interval The interval, in milliseconds.
+function Client:setPingInterval(interval)
+    if self.connection then
+        self.connection:ping_interval(interval)
+    end
+end
+
+--- Change the probability at which unreliable packets should not be dropped.
+-- @tparam number interval Interval, in milliseconds, over which to measure lowest mean RTT. (default: 5000ms)
+-- @tparam number acceleration Rate at which to increase the throttle probability as mean RTT declines. (default: 2)
+-- @tparam number deceleration Rate at which to decrease the throttle probability as mean RTT increases.
+function Client:setThrottle(interval, acceleration, deceleration)
+    interval = interval or 5000
+    acceleration = acceleration or 2
+    deceleration = deceleration or 2
+    if self.connection then
+        self.connection:throttle_configure(interval, acceleration, deceleration)
+    end
+end
+
+--- Set the parameters for attempting to reconnect if a timeout is detected.
+-- @tparam ?number limit A factor that is multiplied with a value that based on the average round trip time to compute the timeout limit. (default: 32)
+-- @tparam ?number minimum Timeout value in milliseconds that a reliable packet has to be acknowledged if the variable timeout limit was exceeded. (default: 5000)
+-- @tparam ?number maximum Fixed timeout in milliseconds for which any packet has to be acknowledged.
+function Client:setTimeout(limit, minimum, maximum)
+    limit = limit or 32
+    minimum = minimum or 5000
+    maximum = maximum or 30000
+    if self.connection then
+        self.connection:timeout(limit, minimum, maximum)
+    end
+end
+
+--- Set the serialization functions for sending and receiving data.
+-- Both the client and server must share the same serialization method.
+-- @tparam function serialize The serialization function to use.
+-- @tparam function deserialize The deserialization function to use.
+-- @usage
+--bitser = require "bitser" -- or any library you like
+--client = sock.newClient("localhost", 22122)
+--client:setSerialization(bitser.dumps, bitser.loads)
+function Client:setSerialization(serialize, deserialize)
+    assert(type(serialize) == "function", "Serialize must be a function, got: '"..type(serialize).."'")
+    assert(type(deserialize) == "function", "Deserialize must be a function, got: '"..type(deserialize).."'")
+    self.serialize = serialize
+    self.deserialize = deserialize
+end
+
+--- Gets whether the client is connected to the server.
+-- @treturn boolean Whether the client is connected to the server.
+-- @usage
+-- client:connect()
+-- client:isConnected() -- false
+-- -- After a few client updates
+-- client:isConnected() -- true
+function Client:isConnected()
+    return self.connection ~= nil and self:getState() == "connected"
+end
+
+--- Gets whether the client is disconnected from the server.
+-- @treturn boolean Whether the client is connected to the server.
+-- @usage
+-- client:disconnect()
+-- client:isDisconnected() -- false
+-- -- After a few client updates
+-- client:isDisconnected() -- true
+function Client:isDisconnected()
+    return self.connection ~= nil and self:getState() == "disconnected"
+end
+
+--- Gets whether the client is connecting to the server.
+-- @treturn boolean Whether the client is connected to the server.
+-- @usage
+-- client:connect()
+-- client:isConnecting() -- true
+-- -- After a few client updates
+-- client:isConnecting() -- false
+-- client:isConnected() -- true
+function Client:isConnecting()
+    local inConnectingState = false
+    for _, state in ipairs(sock.CONNECTING_STATES) do
+        if state == self:getState() then
+            inConnectingState = true
+            break
+        end
+    end
+    return self.connection ~= nil and inConnectingState
+end
+
+--- Gets whether the client is disconnecting from the server.
+-- @treturn boolean Whether the client is connected to the server.
+-- @usage
+-- client:disconnect()
+-- client:isDisconnecting() -- true
+-- -- After a few client updates
+-- client:isDisconnecting() -- false
+-- client:isDisconnected() -- true
+function Client:isDisconnecting()
+    local inDisconnectingState = false
+    for _, state in ipairs(sock.DISCONNECTING_STATES) do
+        if state == self:getState() then
+            inDisconnectingState = true
+            break
+        end
+    end
+    return self.connection ~= nil and inDisconnectingState
+end
+
+--- Get the total sent data since the server was created.
+-- @treturn number The total sent data in bytes.
+function Client:getTotalSentData()
+    return self.host:total_sent_data()
+end
+
+--- Get the total received data since the server was created.
+-- @treturn number The total received data in bytes.
+function Client:getTotalReceivedData()
+    return self.host:total_received_data()
+end
+
+--- Get the total number of packets (messages) sent since the client was created.
+-- Everytime a message is sent or received, the corresponding figure is incremented.
+-- Therefore, this is not necessarily an accurate indicator of how many packets were actually
+-- exchanged over the network.
+-- @treturn number The total number of sent packets.
+function Client:getTotalSentPackets()
+    return self.packetsSent
+end
+
+--- Get the total number of packets (messages) received since the client was created.
+-- @treturn number The total number of received packets.
+-- @see Client:getTotalSentPackets
+function Client:getTotalReceivedPackets()
+    return self.packetsReceived
+end
+
+--- Get the last time when network events were serviced.
+-- @treturn number Timestamp of the last time events were serviced.
+function Client:getLastServiceTime()
+    return self.host:service_time()
+end
+
+--- Get the number of allocated channels.
+-- Channels are zero-indexed, e.g. 16 channels allocated means that the
+-- maximum channel that can be used is 15.
+-- @treturn number Number of allocated channels.
+function Client:getMaxChannels()
+    return self.maxChannels
+end
+
+--- Get the timeout for packets.
+-- @treturn number Time to wait for incoming packets in milliseconds.
+-- initial default is 0.
+function Client:getMessageTimeout()
+    return self.messageTimeout
+end
+
+--- Return the round trip time (RTT, or ping) to the server, if connected.
+-- It can take a few seconds for the time to approach an accurate value.
+-- @treturn number The round trip time.
+function Client:getRoundTripTime()
+    if self.connection then
+        return self.connection:round_trip_time()
+    end
+end
+
+--- Get the unique connection id, if connected.
+-- @treturn number The connection id.
+function Client:getConnectId()
+    if self.connection then
+        return self.connection:connect_id()
+    end
+end
+
+--- Get the current connection state, if connected.
+-- @treturn string The connection state.
+-- @see CONNECTION_STATES
+function Client:getState()
+    if self.connection then
+        return self.connection:state()
+    end
+end
+
+--- Get the index of the enet peer. All peers of an ENet host are kept in an array. This function finds and returns the index of the peer of its host structure.
+-- @treturn number The index of the peer.
+function Client:getIndex()
+    if self.connection then
+        return self.connection:index()
+    end
+end
+
+--- Get the socket address of the host.
+-- @treturn string A description of the socket address, in the format "A.B.C.D:port" where A.B.C.D is the IP address of the used socket.
+function Client:getSocketAddress()
+    return self.host:get_socket_address()
+end
+
+--- Get the enet_peer that has the given index.
+-- @treturn enet_peer The underlying enet peer object.
+function Client:getPeerByIndex(index)
+    return self.host:get_peer(index)
+end
+
+--- Get the current send mode.
+-- @treturn string
+-- @see SEND_MODES
+function Client:getSendMode()
+    return self.sendMode
+end
+
+--- Get the default send mode.
+-- @treturn string
+-- @see SEND_MODES
+function Client:getDefaultSendMode()
+    return self.defaultSendMode
+end
+
+--- Get the IP address or hostname that the client was created with.
+-- @treturn string
+function Client:getAddress()
+    return self.address
+end
+
+--- Get the port that the client is connecting to.
+-- @treturn number
+function Client:getPort()
+    return self.port
+end
+
+--- Creates a new Server object.
+-- @tparam ?string address Hostname or IP address to bind to. (default: "localhost")
+-- @tparam ?number port Port to listen to for data. (default: 22122)
+-- @tparam ?number maxPeers Maximum peers that can connect to the server. (default: 64)
+-- @tparam ?number maxChannels Maximum channels available to send and receive data. (default: 1)
+-- @tparam ?number inBandwidth Maximum incoming bandwidth (default: 0)
+-- @tparam ?number outBandwidth Maximum outgoing bandwidth (default: 0)
+-- @return A new Server object.
+-- @see Server
+-- @within sock
+-- @usage
+--local sock = require "sock"
+--
+-- -- Local server hosted on localhost:22122 (by default)
+--server = sock.newServer()
+--
+-- -- Local server only, on port 1234
+--server = sock.newServer("localhost", 1234)
+--
+-- -- Server hosted on static IP 123.45.67.89, on port 22122
+--server = sock.newServer("123.45.67.89", 22122)
+--
+-- -- Server hosted on any IP, on port 22122
+--server = sock.newServer("*", 22122)
+--
+-- -- Limit peers to 10, channels to 2
+--server = sock.newServer("*", 22122, 10, 2)
+--
+-- -- Limit incoming/outgoing bandwidth to 1kB/s (1000 bytes/s)
+--server = sock.newServer("*", 22122, 10, 2, 1000, 1000)
+sock.newServer = function(address, port, maxPeers, maxChannels, inBandwidth, outBandwidth)
+    address         = address or "localhost"
+    port            = port or 22122
+    maxPeers        = maxPeers or 64
+    maxChannels     = maxChannels or 1
+    inBandwidth     = inBandwidth or 0
+    outBandwidth    = outBandwidth or 0
+
+    local server = setmetatable({
+        address         = address,
+        port            = port,
+        host            = nil,
+
+        messageTimeout  = 0,
+        maxChannels     = maxChannels,
+        maxPeers        = maxPeers,
+        sendMode        = "reliable",
+        defaultSendMode = "reliable",
+        sendChannel     = 0,
+        defaultSendChannel = 0,
+
+        peers           = {},
+        clients         = {},
+
+        listener        = newListener(),
+        logger          = newLogger("SERVER"),
+
+        serialize       = nil,
+        deserialize     = nil,
+
+        packetsSent     = 0,
+        packetsReceived = 0,
+    }, Server_mt)
+
+    -- ip, max peers, max channels, in bandwidth, out bandwidth
+    -- number of channels for the client and server must match
+    server.host = enet.host_create(server.address .. ":" .. server.port, server.maxPeers, server.maxChannels)
+
+    if not server.host then
+        error("Failed to create the host. Is there another server running on :"..server.port.."?")
+    end
+
+    server:setBandwidthLimit(inBandwidth, outBandwidth)
+
+    if bitserLoaded then
+        server:setSerialization(bitser.dumps, bitser.loads)
+    end
+
+    return server
+end
+
+--- Creates a new Client instance.
+-- @tparam ?string/peer serverOrAddress Usually the IP address or hostname to connect to. It can also be an enet peer. (default: "localhost")
+-- @tparam ?number port Port number of the server to connect to. (default: 22122)
+-- @tparam ?number maxChannels Maximum channels available to send and receive data. (default: 1)
+-- @return A new Client object.
+-- @see Client
+-- @within sock
+-- @usage
+--local sock = require "sock"
+--
+-- -- Client that will connect to localhost:22122 (by default)
+--client = sock.newClient()
+--
+-- -- Client that will connect to localhost:1234
+--client = sock.newClient("localhost", 1234)
+--
+-- -- Client that will connect to 123.45.67.89:1234, using two channels
+-- -- NOTE: Server must also allocate two channels!
+--client = sock.newClient("123.45.67.89", 1234, 2)
+sock.newClient = function(serverOrAddress, port, maxChannels)
+    serverOrAddress = serverOrAddress or "localhost"
+    port            = port or 22122
+    maxChannels     = maxChannels or 1
+
+    local client = setmetatable({
+        address         = nil,
+        port            = nil,
+        host            = nil,
+
+        connection      = nil,
+        connectId       = nil,
+
+        messageTimeout  = 0,
+        maxChannels     = maxChannels,
+        sendMode        = "reliable",
+        defaultSendMode = "reliable",
+        sendChannel     = 0,
+        defaultSendChannel = 0,
+
+        listener        = newListener(),
+        logger          = newLogger("CLIENT"),
+
+        serialize       = nil,
+        deserialize     = nil,
+
+        packetsReceived = 0,
+        packetsSent     = 0,
+    }, Client_mt)
+
+    -- Two different forms for client creation:
+    -- 1. Pass in (address, port) and connect to that.
+    -- 2. Pass in (enet peer) and set that as the existing connection.
+    -- The first would be the common usage for regular client code, while the
+    -- latter is mostly used for creating clients in the server-side code.
+
+    -- First form: (address, port)
+    if port ~= nil and type(port) == "number" and serverOrAddress ~= nil and type(serverOrAddress) == "string" then
+        client.address = serverOrAddress
+        client.port = port
+        client.host = enet.host_create()
+
+    -- Second form: (enet peer)
+    elseif type(serverOrAddress) == "userdata" then
+        client.connection = serverOrAddress
+        client.connectId = client.connection:connect_id()
+    end
+
+    if bitserLoaded then
+        client:setSerialization(bitser.dumps, bitser.loads)
+    end
+
+    return client
+end
+
+return sock
+ + +
+
+
+Generated by LDoc 1.4.3 +Last updated 2017-07-24 19:32:15 +
+
+ + diff --git a/lib/sock/docstyle/ldoc.css b/lib/sock/docstyle/ldoc.css new file mode 100644 index 0000000..ea29eeb --- /dev/null +++ b/lib/sock/docstyle/ldoc.css @@ -0,0 +1,223 @@ +/* + * Tag styles + */ +body { + box-sizing: border-box; + margin: 0; + color: #222; + font-size: 1.8em; +} + +ul, ol { + list-style-type: disc; +} + +pre { + background-color: #f7f7f7; + border: 1px solid #ccc; + border-radius: 3px; + box-shadow: 0px 2px 1px #eee; + padding: 10px; + margin: 10px 0 10px 0; + overflow: auto; +} + +p { + max-width: 70ch; +} + +a { + color: #07f; + text-decoration: none; +} +/* make the target distinct; helps when we're navigating to a function */ +a:target + * { + background-color: #fff824; +} + +/* + * Class styles + */ +.header { + background: #56CCF2; /* fallback for old browsers */ + background: -webkit-linear-gradient(to right, #2F80ED, #56CCF2); /* Chrome 10-25, Safari 5.1-6 */ + background: linear-gradient(to right, #2F80ED, #56CCF2); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ + color: #fff; + text-align: center; + padding: 4rem; +} + +.project-title { + margin: 0; +} + +#main { + padding: 1.5rem; + background: #fff; +} + +#navigation { + float: left; + width: 12.5rem; + vertical-align: top; + overflow: visible; +} + +/* Sidebar */ +.sidebar { + flex: 1 0 40rem; + z-index: 99; + transition: all 0.2s ease; +} +#sidebar_toggle { + display: none; +} +.sidebar_toggle_label { + display: flex; + position: fixed; + justify-content: center; + align-items: center; + z-index: 999; + width: 3rem; + height: 3rem; + margin: 0.5rem; + background-color: #eee; + border: 1px solid #bbb; + border-radius: 3px; + opacity: 1; + cursor: pointer; +} +.sidebar_toggle:checked ~ .sidebar { + z-index: -1; + transform: translateX(-100%); + opacity: 0; +} +.sidebar_toggle:checked ~ .contents { + transform: translateX(0); + margin-right: 0; +} +.sidebar_toggle_label:before { + content: '◀'; +} +.sidebar_toggle:checked + .sidebar_toggle_label:before { + content: '▶'; +} + +.table-of-contents { + position: fixed; + overflow-y: scroll; + top: 0; + bottom: 0; + width: 40rem; + padding: 1.5rem; + padding-top: 4rem; + background: #f7f7f7; +} + +.contents { + transition: transform 0.2s ease; + transform: translateX(40rem); + margin-right: 40rem; +} + +.section { + margin-top: 3rem; +} +.section-header { + font-weight: bold; + border-bottom: 1px solid #ccc; + margin-top: 8rem; +} +.section-description { +} +.section-content { + padding-left: 1rem; +} + +.function_def { + margin-top: 2.5rem; +} +.function_def:first-child { + margin-top: 0; +} + +.function_def li { + padding-bottom: 4px; +} + +.function_name { + display: inline-block; + font-weight: 400; +} + +.anchor_link { + font-size: 85%; +} +.anchor_link:focus { +} + +.type { + font-weight: bolder; + font-style: italic; +} +.parameter_info .type { + font-weight: bold; +} + +.parameter_info { + background: #f7f7f7; + padding: 5px; + font-family: monospace; +} + +/* + * Syntax highlighting + */ +pre .comment { color: #3a3432; font-style: italic; } +pre .constant { color: #01a252; } +pre .string { color: #01a252; } +pre .number { color: #01a252; } +pre .escape { color: #844631; } +pre .library { color: #0e7c6b; } +pre .marker { color: #512b1e; background: #fedc56; } +pre .operator { color: #4a4543; } +pre .keyword { color: #a16a94; } +pre .user-keyword { color: #01a0e4; } +pre .preprocessor, +pre .prepro { color: #db2d20; } +pre .global { color: #db2d20; } +pre .prompt { color: #558817; } +pre .url { color: #272fc2; text-decoration: underline; } + +/* print rules */ +@media print { + body { + font: 12pt "Times New Roman", "TimeNR", Times, serif; + } + a { font-weight: bold; color: #004080; text-decoration: underline; } + + #main { + background-color: #ffffff; + border-left: 0px; + } + + #container { + margin-left: 2%; + margin-right: 2%; + background-color: #ffffff; + } + + #content { + padding: 1em; + background-color: #ffffff; + } + + #navigation { + display: none; + } + pre.example { + font-family: "Droid Sans Mono", "Consolas", "Andale Mono", monospace; + font-size: 10pt; + page-break-inside: avoid; + } +} diff --git a/lib/sock/docstyle/ldoc.ltp b/lib/sock/docstyle/ldoc.ltp new file mode 100644 index 0000000..e358daa --- /dev/null +++ b/lib/sock/docstyle/ldoc.ltp @@ -0,0 +1,277 @@ + + + + $(ldoc.title) + + + + + + + +# local no_spaces = ldoc.no_spaces +# local use_li = ldoc.use_li +# local display_name = ldoc.display_name +# local iter = ldoc.modules.iter +# local function M(txt,item) return ldoc.markup(txt,item,ldoc.plain) end +# local nowrap = ldoc.wrap and '' or 'nowrap' +# local orig_ldoc_href = ldoc.href +# local function H(see) +# local ref = orig_ldoc_href(see) +# ref = ref:gsub('index.html', '') +# return ref +# end +# ldoc.href = H + + + + + + +
+ + +
+

$(ldoc.project)

+
+ +
+ + + +
+ + +# if ldoc.body then -- verbatim HTML as contents; 'non-code' entries + $(ldoc.body) +# elseif module then -- module documentation +

$(M(module.summary,module))

+

$(M(module.description,module))

+# if module.tags.include then + $(M(ldoc.include_file(module.tags.include))) +# end +# if module.usage then +# local li,il = use_li(module.usage) +

Usage:

+
    +# for usage in iter(module.usage) do + $(li)
    $(ldoc.escape(usage))
    $(il) +# end -- for +
+# end -- if usage +# if module.info then +

Info:

+
    +# for tag, value in module.info:iter() do +
  • $(tag): $(M(value,module))
  • +# end +
+# end -- if module.info + +# --- currently works for both Functions and Tables. The params field either contains +# --- function parameters or table fields. +# local show_return = not ldoc.no_return_or_parms +# local show_parms = show_return +# for kind, items in module.kinds() do +# local section_link_name = kind:gsub("Class ", ""):gsub(" ", "") + +# end -- for kinds + +# else -- if module; project-level contents + +# if ldoc.description then +

$(M(ldoc.description,nil))

+# end +# if ldoc.full_description then +

$(M(ldoc.full_description,nil))

+# end + +# for kind, mods in ldoc.kinds() do +

$(kind)

+# kind = kind:lower() + +# for m in mods() do + + + + +# end -- for modules +
$(m.name)$(M(ldoc.strip_header(m.summary),m))
+# end -- for kinds +# end -- if module + +
+
+
+Generated by LDoc 1.4.3 +Last updated $(ldoc.updatetime) +
+
+ + diff --git a/lib/sock/examples/hello/bitser.lua b/lib/sock/examples/hello/bitser.lua new file mode 100644 index 0000000..6bbfeb2 --- /dev/null +++ b/lib/sock/examples/hello/bitser.lua @@ -0,0 +1,424 @@ +--[[ +Copyright (c) 2016, Robin Wellner + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +]] + +local floor = math.floor +local pairs = pairs +local type = type +local insert = table.insert +local getmetatable = getmetatable +local setmetatable = setmetatable + +local ffi = require("ffi") +local buf_pos = 0 +local buf_size = -1 +local buf = nil +local writable_buf = nil +local writable_buf_size = nil + +local function Buffer_prereserve(min_size) + if buf_size < min_size then + buf_size = min_size + buf = ffi.new("uint8_t[?]", buf_size) + end +end + +local function Buffer_clear() + buf_size = -1 + buf = nil + writable_buf = nil + writable_buf_size = nil +end + +local function Buffer_makeBuffer(size) + if writable_buf then + buf = writable_buf + buf_size = writable_buf_size + writable_buf = nil + writable_buf_size = nil + end + buf_pos = 0 + Buffer_prereserve(size) +end + +local function Buffer_newReader(str) + Buffer_makeBuffer(#str) + ffi.copy(buf, str, #str) +end + +local function Buffer_newDataReader(data, size) + writable_buf = buf + writable_buf_size = buf_size + buf_pos = 0 + buf_size = size + buf = ffi.cast("uint8_t*", data) +end + +local function Buffer_reserve(additional_size) + while buf_pos + additional_size > buf_size do + buf_size = buf_size * 2 + local oldbuf = buf + buf = ffi.new("uint8_t[?]", buf_size) + ffi.copy(buf, oldbuf, buf_pos) + end +end + +local function Buffer_write_byte(x) + Buffer_reserve(1) + buf[buf_pos] = x + buf_pos = buf_pos + 1 +end + +local function Buffer_write_string(s) + Buffer_reserve(#s) + ffi.copy(buf + buf_pos, s, #s) + buf_pos = buf_pos + #s +end + +local function Buffer_write_data(ct, len, ...) + Buffer_reserve(len) + ffi.copy(buf + buf_pos, ffi.new(ct, ...), len) + buf_pos = buf_pos + len +end + +local function Buffer_ensure(numbytes) + if buf_pos + numbytes > buf_size then + error("malformed serialized data") + end +end + +local function Buffer_read_byte() + Buffer_ensure(1) + local x = buf[buf_pos] + buf_pos = buf_pos + 1 + return x +end + +local function Buffer_read_string(len) + Buffer_ensure(len) + local x = ffi.string(buf + buf_pos, len) + buf_pos = buf_pos + len + return x +end + +local function Buffer_read_data(ct, len) + Buffer_ensure(len) + local x = ffi.new(ct) + ffi.copy(x, buf + buf_pos, len) + buf_pos = buf_pos + len + return x +end + +local resource_registry = {} +local resource_name_registry = {} +local class_registry = {} +local class_name_registry = {} +local classkey_registry = {} +local class_deserialize_registry = {} + +local serialize_value + +local function write_number(value, _) + if floor(value) == value and value >= -2147483648 and value <= 2147483647 then + if value >= -27 and value <= 100 then + --small int + Buffer_write_byte(value + 27) + elseif value >= -32768 and value <= 32767 then + --short int + Buffer_write_byte(250) + Buffer_write_data("int16_t[1]", 2, value) + else + --long int + Buffer_write_byte(245) + Buffer_write_data("int32_t[1]", 4, value) + end + else + --double + Buffer_write_byte(246) + Buffer_write_data("double[1]", 8, value) + end +end + +local function write_string(value, seen) + if #value < 32 then + --short string + Buffer_write_byte(192 + #value) + else + --long string + Buffer_write_byte(244) + write_number(#value, seen) + end + Buffer_write_string(value) +end + +local function write_nil(_, _) + Buffer_write_byte(247) +end + +local function write_boolean(value, _) + Buffer_write_byte(value and 249 or 248) +end + +local function write_table(value, seen) + local classkey + local class = (class_name_registry[value.class] -- MiddleClass + or class_name_registry[value.__baseclass] -- SECL + or class_name_registry[getmetatable(value)] -- hump.class + or class_name_registry[value.__class__]) -- Slither + if class then + classkey = classkey_registry[class] + Buffer_write_byte(242) + write_string(class) + else + Buffer_write_byte(240) + end + local len = #value + write_number(len, seen) + for i = 1, len do + serialize_value(value[i], seen) + end + local klen = 0 + for k in pairs(value) do + if (type(k) ~= 'number' or floor(k) ~= k or k > len or k < 1) and k ~= classkey then + klen = klen + 1 + end + end + write_number(klen, seen) + for k, v in pairs(value) do + if (type(k) ~= 'number' or floor(k) ~= k or k > len or k < 1) and k ~= classkey then + serialize_value(k, seen) + serialize_value(v, seen) + end + end +end + +local types = {number = write_number, string = write_string, table = write_table, boolean = write_boolean, ["nil"] = write_nil} + +serialize_value = function(value, seen) + if seen[value] then + local ref = seen[value] + if ref < 64 then + --small reference + Buffer_write_byte(128 + ref) + else + --long reference + Buffer_write_byte(243) + write_number(ref, seen) + end + return + end + local t = type(value) + if t ~= 'number' and t ~= 'boolean' and t ~= 'nil' then + seen[value] = seen.len + seen.len = seen.len + 1 + end + if resource_name_registry[value] then + local name = resource_name_registry[value] + if #name < 16 then + --small resource + Buffer_write_byte(224 + #name) + Buffer_write_string(name) + else + --long resource + Buffer_write_byte(241) + write_string(name, seen) + end + return + end + (types[t] or + error("cannot serialize type " .. t) + )(value, seen) +end + +local function serialize(value) + Buffer_makeBuffer(4096) + local seen = {len = 0} + serialize_value(value, seen) +end + +local function add_to_seen(value, seen) + insert(seen, value) + return value +end + +local function reserve_seen(seen) + insert(seen, 42) + return #seen +end + +local function deserialize_value(seen) + local t = Buffer_read_byte() + if t < 128 then + --small int + return t - 27 + elseif t < 192 then + --small reference + return seen[t - 127] + elseif t < 224 then + --small string + return add_to_seen(Buffer_read_string(t - 192), seen) + elseif t < 240 then + --small resource + return add_to_seen(resource_registry[Buffer_read_string(t - 224)], seen) + elseif t == 240 then + --table + local v = add_to_seen({}, seen) + local len = deserialize_value(seen) + for i = 1, len do + v[i] = deserialize_value(seen) + end + len = deserialize_value(seen) + for _ = 1, len do + local key = deserialize_value(seen) + v[key] = deserialize_value(seen) + end + return v + elseif t == 241 then + --long resource + local idx = reserve_seen(seen) + local value = resource_registry[deserialize_value(seen)] + seen[idx] = value + return value + elseif t == 242 then + --instance + local instance = add_to_seen({}, seen) + local classname = deserialize_value(seen) + local class = class_registry[classname] + local classkey = classkey_registry[classname] + local deserializer = class_deserialize_registry[classname] + local len = deserialize_value(seen) + for i = 1, len do + instance[i] = deserialize_value(seen) + end + len = deserialize_value(seen) + for _ = 1, len do + local key = deserialize_value(seen) + instance[key] = deserialize_value(seen) + end + if classkey then + instance[classkey] = class + end + return deserializer(instance, class) + elseif t == 243 then + --reference + return seen[deserialize_value(seen) + 1] + elseif t == 244 then + --long string + return add_to_seen(Buffer_read_string(deserialize_value(seen)), seen) + elseif t == 245 then + --long int + return Buffer_read_data("int32_t[1]", 4)[0] + elseif t == 246 then + --double + return Buffer_read_data("double[1]", 8)[0] + elseif t == 247 then + --nil + return nil + elseif t == 248 then + --false + return false + elseif t == 249 then + --true + return true + elseif t == 250 then + --short int + return Buffer_read_data("int16_t[1]", 2)[0] + else + error("unsupported serialized type " .. t) + end +end + +local function deserialize_MiddleClass(instance, class) + return setmetatable(instance, class.__instanceDict) +end + +local function deserialize_SECL(instance, class) + return setmetatable(instance, getmetatable(class)) +end + +local deserialize_humpclass = setmetatable + +local function deserialize_Slither(instance, class) + return getmetatable(class).allocate(instance) +end + +return {dumps = function(value) + serialize(value) + return ffi.string(buf, buf_pos) +end, dumpLoveFile = function(fname, value) + serialize(value) + love.filesystem.write(fname, ffi.string(buf, buf_pos)) +end, loadLoveFile = function(fname) + local serializedData = love.filesystem.newFileData(fname) + Buffer_newDataReader(serializedData:getPointer(), serializedData:getSize()) + return deserialize_value({}) +end, loadData = function(data, size) + Buffer_newDataReader(data, size) + return deserialize_value({}) +end, loads = function(str) + Buffer_newReader(str) + return deserialize_value({}) +end, register = function(name, resource) + assert(not resource_registry[name], name .. " already registered") + resource_registry[name] = resource + resource_name_registry[resource] = name + return resource +end, unregister = function(name) + resource_name_registry[resource_registry[name]] = nil + resource_registry[name] = nil +end, registerClass = function(name, class, classkey, deserializer) + if not class then + class = name + name = class.__name__ or class.name + end + if not classkey then + if class.__instanceDict then + -- assume MiddleClass + classkey = 'class' + elseif class.__baseclass then + -- assume SECL + classkey = '__baseclass' + end + -- assume hump.class, Slither, or something else that doesn't store the + -- class directly on the instance + end + if not deserializer then + if class.__instanceDict then + -- assume MiddleClass + deserializer = deserialize_MiddleClass + elseif class.__baseclass then + -- assume SECL + deserializer = deserialize_SECL + elseif class.__index == class then + -- assume hump.class + deserializer = deserialize_humpclass + elseif class.__name__ then + -- assume Slither + deserializer = deserialize_Slither + else + error("no deserializer given for unsupported class library") + end + end + class_registry[name] = class + classkey_registry[name] = classkey + class_deserialize_registry[name] = deserializer + class_name_registry[class] = name + return class +end, unregisterClass = function(name) + class_name_registry[class_registry[name]] = nil + classkey_registry[name] = nil + class_deserialize_registry[name] = nil + class_registry[name] = nil +end, reserveBuffer = Buffer_prereserve, clearBuffer = Buffer_clear} diff --git a/lib/sock/examples/hello/main.lua b/lib/sock/examples/hello/main.lua new file mode 100644 index 0000000..b8df1b7 --- /dev/null +++ b/lib/sock/examples/hello/main.lua @@ -0,0 +1,47 @@ +-- Loading sock from root directory relative to this one +-- This is not required in your own projects +package.path = package.path .. ";../?.lua" +local sock = require "sock" +local binser = require "spec.binser" + +function love.load() + client = sock.newClient("localhost", 22122) + server = sock.newServer("localhost", 22122) + + -- If the connect/disconnect callbacks aren't defined some warnings will + -- be thrown, but nothing bad will happen. + + -- Called when someone connects to the server + server:on("connect", function(data, peer) + local msg = "Hello from server!" + peer:send("hello", msg) + end) + + + -- Called when a connection is made to the server + client:on("connect", function(data) + print("Client connected to the server.") + end) + + -- Custom callback, called whenever you send the event from the server + client:on("hello", function(msg) + print(msg) + end) + + client:connect() +end + +function love.update(dt) + server:update() + client:update() + + if love.math.random() > 0.95 then + server:sendToAll("hello", "This is an update message") + end +end + +function love.keypressed(key) + if key == "q" then + client:reset() + end +end diff --git a/lib/sock/examples/image/hello.png b/lib/sock/examples/image/hello.png new file mode 100644 index 0000000..f37a33f Binary files /dev/null and b/lib/sock/examples/image/hello.png differ diff --git a/lib/sock/examples/image/main.lua b/lib/sock/examples/image/main.lua new file mode 100644 index 0000000..defe25d --- /dev/null +++ b/lib/sock/examples/image/main.lua @@ -0,0 +1,48 @@ +-- Loading sock from root directory relative to this one +-- This is not required in your own projects +package.path = package.path .. ";../../?.lua" +local sock = require "sock" +local bitser = require "spec.bitser" + +function love.load() + client = sock.newClient("localhost", 22122) + server = sock.newServer("*", 22122) + client:setSerialization(bitser.dumps, bitser.loads) + server:setSerialization(bitser.dumps, bitser.loads) + client:enableCompression() + server:enableCompression() + + client:connect() + + client:on("image", function(data) + local file = love.filesystem.newFileData(data, "") + receivedImage = love.image.newImageData(file) + receivedImage = love.graphics.newImage(receivedImage) + end) + + server:on("connect", function(data, client) + local image = love.filesystem.newFileData("hello.png") + server:sendToAll("image", image) + end) + + lastModified = 0 +end + +function love.update(dt) + server:update() + client:update() + + if lastModified < love.filesystem.getLastModified("hello.png") then + -- Sleep for some milliseconds for the image to write to disk + love.timer.sleep(0.2) + lastModified = love.filesystem.getLastModified("hello.png") + local image = love.filesystem.newFileData("hello.png") + server:sendToAll("image", image) + end +end + +function love.draw() + if receivedImage then + love.graphics.draw(receivedImage, 100, 100) + end +end diff --git a/lib/sock/examples/pong/client/main.lua b/lib/sock/examples/pong/client/main.lua new file mode 100644 index 0000000..e87a11c --- /dev/null +++ b/lib/sock/examples/pong/client/main.lua @@ -0,0 +1,116 @@ +package.path = package.path .. ";../../?.lua" +sock = require "sock" +bitser = require "spec.bitser" + +function love.load() + -- how often an update is sent out + tickRate = 1/60 + tick = 0 + + client = sock.newClient("localhost", 22122) + client:setSerialization(bitser.dumps, bitser.loads) + client:setSchema("playerState", { + "index", + "player", + }) + + -- store the client's index + -- playerNumber is nil otherwise + client:on("playerNum", function(num) + playerNumber = num + end) + + -- receive info on where the players are located + client:on("playerState", function(data) + local index = data.index + local player = data.player + + -- only accept updates for the other player + if playerNumber and index ~= playerNumber then + players[index] = player + end + end) + + client:on("ballState", function(data) + ball = data + end) + + client:on("scores", function(data) + scores = data + end) + + client:connect() + + function newPlayer(x, y) + return { + x = x, + y = y, + w = 20, + h = 100, + } + end + + function newBall(x, y) + return { + x = x, + y = y, + vx = 150, + vy = 150, + w = 15, + h = 15, + } + end + + local marginX = 50 + + players = { + newPlayer(marginX, love.graphics.getHeight()/2), + newPlayer(love.graphics.getWidth() - marginX, love.graphics.getHeight()/2) + } + + scores = {0, 0} + + ball = newBall(love.graphics.getWidth()/2, love.graphics.getHeight()/2) +end + +function love.update(dt) + client:update() + + if client:getState() == "connected" then + tick = tick + dt + + -- simulate the ball locally, and receive corrections from the server + ball.x = ball.x + ball.vx * dt + ball.y = ball.y + ball.vy * dt + end + + if tick >= tickRate then + tick = 0 + + if playerNumber then + local mouseY = love.mouse.getY() + local playerY = mouseY - players[playerNumber].h/2 + + -- Update our own player position and send it to the server + players[playerNumber].y = playerY + client:send("mouseY", playerY) + end + end +end + +function love.draw() + for _, player in pairs(players) do + love.graphics.rectangle('fill', player.x, player.y, player.w, player.h) + end + + love.graphics.rectangle('fill', ball.x, ball.y, ball.w, ball.h) + + love.graphics.print(client:getState(), 5, 5) + if playerNumber then + love.graphics.print("Player " .. playerNumber, 5, 25) + else + love.graphics.print("No player number assigned", 5, 25) + end + local score = ("%d - %d"):format(scores[1], scores[2]) + love.graphics.print(score, 5, 45) +end diff --git a/lib/sock/examples/pong/server/main.lua b/lib/sock/examples/pong/server/main.lua new file mode 100644 index 0000000..a8360e5 --- /dev/null +++ b/lib/sock/examples/pong/server/main.lua @@ -0,0 +1,136 @@ +package.path = package.path .. ";../../?.lua" +sock = require "sock" +bitser = require "spec.bitser" + +-- Utility functions +function isColliding(this, other) + return this.x < other.x + other.w and + this.y < other.y + other.h and + this.x + this.w > other.x and + this.y + this.h > other.y +end + +function love.load() + -- how often an update is sent out + tickRate = 1/60 + tick = 0 + + server = sock.newServer("*", 22122, 2) + server:setSerialization(bitser.dumps, bitser.loads) + + -- Players are being indexed by peer index here, definitely not a good idea + -- for a larger game, but it's good enough for this application. + server:on("connect", function(data, client) + -- tell the peer what their index is + client:send("playerNum", client:getIndex()) + end) + + -- receive info on where a player is located + server:on("mouseY", function(y, client) + local index = client:getIndex() + players[index].y = y + end) + + + function newPlayer(x, y) + return { + x = x, + y = y, + w = 20, + h = 100, + } + end + + function newBall(x, y) + return { + x = x, + y = y, + vx = 150, + vy = 150, + w = 15, + h = 15, + } + end + + local marginX = 50 + + players = { + newPlayer(marginX, love.graphics.getHeight()/2), + newPlayer(love.graphics.getWidth() - marginX, love.graphics.getHeight()/2) + } + + scores = {0, 0} + + ball = newBall(love.graphics.getWidth()/2, love.graphics.getHeight()/2) +end + +function love.update(dt) + server:update() + + -- wait until 2 players connect to start playing + local enoughPlayers = #server.clients >= 2 + if not enoughPlayers then return end + + for i, player in pairs(players) do + -- This is a naive solution, if the ball is inside the paddle it might bug out + -- But hey, it's low stakes pong + if isColliding(ball, player) then + ball.vx = ball.vx * -1 + ball.vy = ball.vy * -1 + end + end + + -- Left/Right bounds + if ball.x < 0 or ball.x > love.graphics.getWidth() then + if ball.x < 0 then + scores[2] = scores[2] + 1 + else + scores[1] = scores[1] + 1 + end + + server:sendToAll("scores", scores) + + ball.x = love.graphics.getWidth()/2 + ball.y = love.graphics.getHeight()/2 + ball.vx = ball.vx * -1 + ball.vy = ball.vy * -1 + end + + -- Top/Bottom bounds + if ball.y < 0 or ball.y > love.graphics.getHeight() - ball.h then + ball.vy = ball.vy * -1 + + if ball.y < 0 then + ball.y = 0 + end + + if ball.y > love.graphics.getHeight() - ball.h then + ball.y = love.graphics.getHeight() - ball.h + end + end + + ball.x = ball.x + ball.vx * dt + ball.y = ball.y + ball.vy * dt + + tick = tick + dt + + if tick >= tickRate then + tick = 0 + + for i, player in pairs(players) do + server:sendToAll("playerState", {i, player}) + end + + server:sendToAll("ballState", ball) + end +end + +function love.draw() + for i, player in pairs(players) do + love.graphics.rectangle('fill', player.x, player.y, player.w, player.h) + end + + love.graphics.rectangle('fill', ball.x, ball.y, ball.w, ball.h) + local score = ("%d - %d"):format(scores[1], scores[2]) + love.graphics.print(score, 5, 5) +end diff --git a/lib/sock/init.lua b/lib/sock/init.lua new file mode 100755 index 0000000..796183c --- /dev/null +++ b/lib/sock/init.lua @@ -0,0 +1,1418 @@ + +--- A Lua networking library for LÖVE games. +-- * [Source code](https://github.com/camchenry/sock.lua) +-- * [Examples](https://github.com/camchenry/sock.lua/tree/master/examples) +-- @module sock + +local sock = { + _VERSION = 'sock.lua v0.3.0', + _DESCRIPTION = 'A Lua networking library for LÖVE games', + _URL = 'https://github.com/camchenry/sock.lua', + _LICENSE = [[ + MIT License + + Copyright (c) 2016 Cameron McHenry + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + ]] +} + +local enet = require "enet" + +-- Current folder trick +-- http://kiki.to/blog/2014/04/12/rule-5-beware-of-multiple-files/ +local currentFolder = (...):gsub('%.[^%.]+$', '') + +local bitserLoaded = false + +if bitser then + bitserLoaded = true +end + +-- Try to load some common serialization libraries +-- This is for convenience, you may still specify your own serializer +if not bitserLoaded then + bitserLoaded, bitser = pcall(require, "bitser") +end + +-- Try to load relatively +if not bitserLoaded then + bitserLoaded, bitser = pcall(require, currentFolder .. ".bitser") +end + +-- links variables to keys based on their order +-- note that it only works for boolean and number values, not strings +local function zipTable(items, keys, event) + local data = {} + + -- convert variable at index 1 into the value for the key value at index 1, and so on + for i, value in ipairs(items) do + local key = keys[i] + + if not key then + error("Event '"..event.."' missing data key. Is the schema different between server and client?") + end + + data[key] = value + end + + return data +end + +--- All of the possible connection statuses for a client connection. +-- @see Client:getState +sock.CONNECTION_STATES = { + "disconnected", -- Disconnected from the server. + "connecting", -- In the process of connecting to the server. + "acknowledging_connect", -- + "connection_pending", -- + "connection_succeeded", -- + "connected", -- Successfully connected to the server. + "disconnect_later", -- Disconnecting, but only after sending all queued packets. + "disconnecting", -- In the process of disconnecting from the server. + "acknowledging_disconnect", -- + "zombie", -- + "unknown", -- +} + +--- States that represent the client connecting to a server. +sock.CONNECTING_STATES = { + "connecting", -- In the process of connecting to the server. + "acknowledging_connect", -- + "connection_pending", -- + "connection_succeeded", -- +} + +--- States that represent the client disconnecting from a server. +sock.DISCONNECTING_STATES = { + "disconnect_later", -- Disconnecting, but only after sending all queued packets. + "disconnecting", -- In the process of disconnecting from the server. + "acknowledging_disconnect", -- +} + +--- Valid modes for sending messages. +sock.SEND_MODES = { + "reliable", -- Message is guaranteed to arrive, and arrive in the order in which it is sent. + "unsequenced", -- Message has no guarantee on the order that it arrives. + "unreliable", -- Message is not guaranteed to arrive. +} + +local function isValidSendMode(mode) + for _, validMode in ipairs(sock.SEND_MODES) do + if mode == validMode then + return true + end + end + return false +end + +local Logger = {} +local Logger_mt = {__index = Logger} + +local function newLogger(source) + local logger = setmetatable({ + source = source, + messages = {}, + + -- Makes print info more concise, but should still log the full line + shortenLines = true, + -- Print all incoming event data + printEventData = false, + printErrors = true, + printWarnings = true, + }, Logger_mt) + + return logger +end + +function Logger:log(event, data) + local time = os.date("%X") -- something like 24:59:59 + local shortLine = ("[%s] %s"):format(event, data) + local fullLine = ("[%s][%s][%s] %s"):format(self.source, time, event, data) + + -- The printed message may or may not be the full message + local line = fullLine + if self.shortenLines then + line = shortLine + end + + if self.printEventData then + print(line) + elseif self.printErrors and event == "error" then + print(line) + elseif self.printWarnings and event == "warning" then + print(line) + end + + -- The logged message is always the full message + table.insert(self.messages, fullLine) + + -- TODO: Dump to a log file +end + +local Listener = {} +local Listener_mt = {__index = Listener} + +local function newListener() + local listener = setmetatable({ + triggers = {}, + schemas = {}, + }, Listener_mt) + + return listener +end + +-- Adds a callback to a trigger +-- Returns: the callback function +function Listener:addCallback(event, callback) + if not self.triggers[event] then + self.triggers[event] = {} + end + + table.insert(self.triggers[event], callback) + + return callback +end + +-- Removes a callback on a given trigger +-- Returns a boolean indicating if the callback was removed +function Listener:removeCallback(callback) + for _, triggers in pairs(self.triggers) do + for i, trigger in pairs(triggers) do + if trigger == callback then + table.remove(triggers, i) + return true + end + end + end + return false +end + +-- Accepts: event (string), schema (table) +-- Returns: nothing +function Listener:setSchema(event, schema) + self.schemas[event] = schema +end + +-- Activates all callbacks for a trigger +-- Returns a boolean indicating if any callbacks were triggered +function Listener:trigger(event, data, client) + if self.triggers[event] then + for _, trigger in pairs(self.triggers[event]) do + -- Event has a pre-existing schema defined + if self.schemas[event] then + data = zipTable(data, self.schemas[event], event) + end + trigger(data, client) + end + return true + else + return false + end +end + +--- Manages all clients and receives network events. +-- @type Server +local Server = {} +local Server_mt = {__index = Server} + +--- Check for network events and handle them. +function Server:update() + local event = self.host:service(self.messageTimeout) + + while event do + if event.type == "connect" then + local eventClient = sock.newClient(event.peer) + eventClient:setSerialization(self.serialize, self.deserialize) + table.insert(self.peers, event.peer) + table.insert(self.clients, eventClient) + self:_activateTriggers("connect", event.data, eventClient) + self:log(event.type, tostring(event.peer) .. " connected") + + elseif event.type == "receive" then + local eventName, data = self:__unpack(event.data) + local eventClient = self:getClient(event.peer) + + self:_activateTriggers(eventName, data, eventClient) + self:log(eventName, data) + + elseif event.type == "disconnect" then + -- remove from the active peer list + for i, peer in pairs(self.peers) do + if peer == event.peer then + table.remove(self.peers, i) + end + end + local eventClient = self:getClient(event.peer) + for i, client in pairs(self.clients) do + if client == eventClient then + table.remove(self.clients, i) + end + end + self:_activateTriggers("disconnect", event.data, eventClient) + self:log(event.type, tostring(event.peer) .. " disconnected") + + end + + event = self.host:service(self.messageTimeout) + end +end + +-- Creates the unserialized message that will be used in callbacks +-- In: serialized message (string) +-- Out: event (string), data (mixed) +function Server:__unpack(data) + if not self.deserialize then + self:log("error", "Can't deserialize message: deserialize was not set") + error("Can't deserialize message: deserialize was not set") + end + + local message = self.deserialize(data) + local eventName, data = message[1], message[2] + return eventName, data +end + +-- Creates the serialized message that will be sent over the network +-- In: event (string), data (mixed) +-- Out: serialized message (string) +function Server:__pack(event, data) + local message = {event, data} + local serializedMessage + + if not self.serialize then + self:log("error", "Can't serialize message: serialize was not set") + error("Can't serialize message: serialize was not set") + end + + -- 'Data' = binary data class in Love + if type(data) == "userdata" and data.type and data:typeOf("Data") then + message[2] = data:getString() + serializedMessage = self.serialize(message) + else + serializedMessage = self.serialize(message) + end + + return serializedMessage +end + +--- Send a message to all clients, except one. +-- Useful for when the client does something locally, but other clients +-- need to be updated at the same time. This way avoids duplicating objects by +-- never sending its own event to itself in the first place. +-- @tparam Client client The client to not receive the message. +-- @tparam string event The event to trigger with this message. +-- @param data The data to send. +function Server:sendToAllBut(client, event, data) + local serializedMessage = self:__pack(event, data) + + for _, p in pairs(self.peers) do + if p ~= client.connection then + self.packetsSent = self.packetsSent + 1 + p:send(serializedMessage, self.sendChannel, self.sendMode) + end + end + + self:resetSendSettings() +end + +--- Send a message to all clients. +-- @tparam string event The event to trigger with this message. +-- @param data The data to send. +--@usage +--server:sendToAll("gameStarting", true) +function Server:sendToAll(event, data) + local serializedMessage = self:__pack(event, data) + + self.packetsSent = self.packetsSent + #self.peers + + self.host:broadcast(serializedMessage, self.sendChannel, self.sendMode) + + self:resetSendSettings() +end + +--- Send a message to a single peer. Useful to send data to a newly connected player +-- without sending to everyone who already received it. +-- @tparam enet_peer peer The enet peer to receive the message. +-- @tparam string event The event to trigger with this message. +-- @param data data to send to the peer. +--@usage +--server:sendToPeer(peer, "initialGameInfo", {...}) +function Server:sendToPeer(peer, event, data) + local serializedMessage = self:__pack(event, data) + + self.packetsSent = self.packetsSent + 1 + + peer:send(serializedMessage, self.sendChannel, self.sendMode) + + self:resetSendSettings() +end + +--- Add a callback to an event. +-- @tparam string event The event that will trigger the callback. +-- @tparam function callback The callback to be triggered. +-- @treturn function The callback that was passed in. +--@usage +--server:on("connect", function(data, client) +-- print("Client connected!") +--end) +function Server:on(event, callback) + return self.listener:addCallback(event, callback) +end + +function Server:_activateTriggers(event, data, client) + local result = self.listener:trigger(event, data, client) + + self.packetsReceived = self.packetsReceived + 1 + + if not result then + self:log("warning", "Tried to activate trigger: '" .. tostring(event) .. "' but it does not exist.") + end +end + +--- Remove a specific callback for an event. +-- @tparam function callback The callback to remove. +-- @treturn boolean Whether or not the callback was removed. +--@usage +--local callback = server:on("chatMessage", function(message) +-- print(message) +--end) +--server:removeCallback(callback) +function Server:removeCallback(callback) + return self.listener:removeCallback(callback) +end + +--- Log an event. +-- Alias for Server.logger:log. +-- @tparam string event The type of event that happened. +-- @tparam string data The message to log. +--@usage +--if somethingBadHappened then +-- server:log("error", "Something bad happened!") +--end +function Server:log(event, data) + return self.logger:log(event, data) +end + +--- Reset all send options to their default values. +function Server:resetSendSettings() + self.sendMode = self.defaultSendMode + self.sendChannel = self.defaultSendChannel +end + +--- Enables an adaptive order-2 PPM range coder for the transmitted data of all peers. Both the client and server must both either have compression enabled or disabled. +-- +-- Note: lua-enet does not currently expose a way to disable the compression after it has been enabled. +function Server:enableCompression() + return self.host:compress_with_range_coder() +end + +--- Destroys the server and frees the port it is bound to. +function Server:destroy() + self.host:destroy() +end + +--- Set the send mode for the next outgoing message. +-- The mode will be reset after the next message is sent. The initial default +-- is "reliable". +-- @tparam string mode A valid send mode. +-- @see SEND_MODES +-- @usage +--server:setSendMode("unreliable") +--server:sendToAll("playerState", {...}) +function Server:setSendMode(mode) + if not isValidSendMode(mode) then + self:log("warning", "Tried to use invalid send mode: '" .. mode .. "'. Defaulting to reliable.") + mode = "reliable" + end + + self.sendMode = mode +end + +--- Set the default send mode for all future outgoing messages. +-- The initial default is "reliable". +-- @tparam string mode A valid send mode. +-- @see SEND_MODES +function Server:setDefaultSendMode(mode) + if not isValidSendMode(mode) then + self:log("error", "Tried to set default send mode to invalid mode: '" .. mode .. "'") + error("Tried to set default send mode to invalid mode: '" .. mode .. "'") + end + + self.defaultSendMode = mode +end + +--- Set the send channel for the next outgoing message. +-- The channel will be reset after the next message. Channels are zero-indexed +-- and cannot exceed the maximum number of channels allocated. The initial +-- default is 0. +-- @tparam number channel Channel to send data on. +-- @usage +--server:setSendChannel(2) -- the third channel +--server:sendToAll("importantEvent", "The message") +function Server:setSendChannel(channel) + if channel > (self.maxChannels - 1) then + self:log("warning", "Tried to use invalid channel: " .. channel .. " (max is " .. self.maxChannels - 1 .. "). Defaulting to 0.") + channel = 0 + end + + self.sendChannel = channel +end + +--- Set the default send channel for all future outgoing messages. +-- The initial default is 0. +-- @tparam number channel Channel to send data on. +function Server:setDefaultSendChannel(channel) + self.defaultSendChannel = channel +end + +--- Set the data schema for an event. +-- +-- Schemas allow you to set a specific format that the data will be sent. If the +-- client and server both know the format ahead of time, then the table keys +-- do not have to be sent across the network, which saves bandwidth. +-- @tparam string event The event to set the data schema for. +-- @tparam {string,...} schema The data schema. +-- @usage +-- server = sock.newServer(...) +-- client = sock.newClient(...) +-- +-- -- Without schemas +-- client:send("update", { +-- x = 4, +-- y = 100, +-- vx = -4.5, +-- vy = 23.1, +-- rotation = 1.4365, +-- }) +-- server:on("update", function(data, client) +-- -- data = { +-- -- x = 4, +-- -- y = 100, +-- -- vx = -4.5, +-- -- vy = 23.1, +-- -- rotation = 1.4365, +-- -- } +-- end) +-- +-- +-- -- With schemas +-- server:setSchema("update", { +-- "x", +-- "y", +-- "vx", +-- "vy", +-- "rotation", +-- }) +-- -- client no longer has to send the keys, saving bandwidth +-- client:send("update", { +-- 4, +-- 100, +-- -4.5, +-- 23.1, +-- 1.4365, +-- }) +-- server:on("update", function(data, client) +-- -- data = { +-- -- x = 4, +-- -- y = 100, +-- -- vx = -4.5, +-- -- vy = 23.1, +-- -- rotation = 1.4365, +-- -- } +-- end) +function Server:setSchema(event, schema) + return self.listener:setSchema(event, schema) +end + +--- Set the incoming and outgoing bandwidth limits. +-- @tparam number incoming The maximum incoming bandwidth in bytes. +-- @tparam number outgoing The maximum outgoing bandwidth in bytes. +function Server:setBandwidthLimit(incoming, outgoing) + return self.host:bandwidth_limit(incoming, outgoing) +end + +--- Set the maximum number of channels. +-- @tparam number limit The maximum number of channels allowed. If it is 0, +-- then the maximum number of channels available on the system will be used. +function Server:setMaxChannels(limit) + self.host:channel_limit(limit) +end + +--- Set the timeout to wait for packets. +-- @tparam number timeout Time to wait for incoming packets in milliseconds. The +-- initial default is 0. +function Server:setMessageTimeout(timeout) + self.messageTimeout = timeout +end + +--- Set the serialization functions for sending and receiving data. +-- Both the client and server must share the same serialization method. +-- @tparam function serialize The serialization function to use. +-- @tparam function deserialize The deserialization function to use. +-- @usage +--bitser = require "bitser" -- or any library you like +--server = sock.newServer("localhost", 22122) +--server:setSerialization(bitser.dumps, bitser.loads) +function Server:setSerialization(serialize, deserialize) + assert(type(serialize) == "function", "Serialize must be a function, got: '"..type(serialize).."'") + assert(type(deserialize) == "function", "Deserialize must be a function, got: '"..type(deserialize).."'") + self.serialize = serialize + self.deserialize = deserialize +end + +--- Gets the Client object associated with an enet peer. +-- @tparam peer peer An enet peer. +-- @treturn Client Object associated with the peer. +function Server:getClient(peer) + for _, client in pairs(self.clients) do + if peer == client.connection then + return client + end + end +end + +--- Gets the Client object that has the given connection id. +-- @tparam number connectId The unique client connection id. +-- @treturn Client +function Server:getClientByConnectId(connectId) + for _, client in pairs(self.clients) do + if connectId == client.connectId then + return client + end + end +end + +--- Get the Client object that has the given peer index. +-- @treturn Client +function Server:getClientByIndex(index) + for _, client in pairs(self.clients) do + if index == client:getIndex() then + return client + end + end +end + +--- Get the enet_peer that has the given index. +-- @treturn enet_peer The underlying enet peer object. +function Server:getPeerByIndex(index) + return self.host:get_peer(index) +end + +--- Get the total sent data since the server was created. +-- @treturn number The total sent data in bytes. +function Server:getTotalSentData() + return self.host:total_sent_data() +end + +--- Get the total received data since the server was created. +-- @treturn number The total received data in bytes. +function Server:getTotalReceivedData() + return self.host:total_received_data() +end +--- Get the total number of packets (messages) sent since the server was created. +-- Everytime a message is sent or received, the corresponding figure is incremented. +-- Therefore, this is not necessarily an accurate indicator of how many packets were actually +-- exchanged over the network. +-- @treturn number The total number of sent packets. +function Server:getTotalSentPackets() + return self.packetsSent +end + +--- Get the total number of packets (messages) received since the server was created. +-- @treturn number The total number of received packets. +-- @see Server:getTotalSentPackets +function Server:getTotalReceivedPackets() + return self.packetsReceived +end + +--- Get the last time when network events were serviced. +-- @treturn number Timestamp of the last time events were serviced. +function Server:getLastServiceTime() + return self.host:service_time() +end + +--- Get the number of allocated slots for peers. +-- @treturn number Number of allocated slots. +function Server:getMaxPeers() + return self.maxPeers +end + +--- Get the number of allocated channels. +-- Channels are zero-indexed, e.g. 16 channels allocated means that the +-- maximum channel that can be used is 15. +-- @treturn number Number of allocated channels. +function Server:getMaxChannels() + return self.maxChannels +end + +--- Get the timeout for packets. +-- @treturn number Time to wait for incoming packets in milliseconds. +-- initial default is 0. +function Server:getMessageTimeout() + return self.messageTimeout +end + +--- Get the socket address of the host. +-- @treturn string A description of the socket address, in the format +-- "A.B.C.D:port" where A.B.C.D is the IP address of the used socket. +function Server:getSocketAddress() + return self.host:get_socket_address() +end + +--- Get the current send mode. +-- @treturn string +-- @see SEND_MODES +function Server:getSendMode() + return self.sendMode +end + +--- Get the default send mode. +-- @treturn string +-- @see SEND_MODES +function Server:getDefaultSendMode() + return self.defaultSendMode +end + +--- Get the IP address or hostname that the server was created with. +-- @treturn string +function Server:getAddress() + return self.address +end + +--- Get the port that the server is hosted on. +-- @treturn number +function Server:getPort() + return self.port +end + +--- Get the table of Clients actively connected to the server. +-- @return {Client,...} +function Server:getClients() + return self.clients +end + +--- Get the number of Clients that are currently connected to the server. +-- @treturn number The number of active clients. +function Server:getClientCount() + return #self.clients +end + + +--- Connects to servers. +-- @type Client +local Client = {} +local Client_mt = {__index = Client} + +--- Check for network events and handle them. +function Client:update() + local event = self.host:service(self.messageTimeout) + + while event do + if event.type == "connect" then + self:_activateTriggers("connect", event.data) + self:log(event.type, "Connected to " .. tostring(self.connection)) + elseif event.type == "receive" then + local eventName, data = self:__unpack(event.data) + + self:_activateTriggers(eventName, data) + self:log(eventName, data) + + elseif event.type == "disconnect" then + self:_activateTriggers("disconnect", event.data) + self:log(event.type, "Disconnected from " .. tostring(self.connection)) + end + + event = self.host:service(self.messageTimeout) + end +end + +--- Connect to the chosen server. +-- Connection will not actually occur until the next time `Client:update` is called. +-- @tparam ?number code A number that can be associated with the connect event. +function Client:connect(code) + -- number of channels for the client and server must match + self.connection = self.host:connect(self.address .. ":" .. self.port, self.maxChannels, code) + self.connectId = self.connection:connect_id() +end + +--- Disconnect from the server, if connected. The client will disconnect the +-- next time that network messages are sent. +-- @tparam ?number code A code to associate with this disconnect event. +-- @todo Pass the code into the disconnect callback on the server +function Client:disconnect(code) + code = code or 0 + self.connection:disconnect(code) +end + +--- Disconnect from the server, if connected. The client will disconnect after +-- sending all queued packets. +-- @tparam ?number code A code to associate with this disconnect event. +-- @todo Pass the code into the disconnect callback on the server +function Client:disconnectLater(code) + code = code or 0 + self.connection:disconnect_later(code) +end + +--- Disconnect from the server, if connected. The client will disconnect immediately. +-- @tparam ?number code A code to associate with this disconnect event. +-- @todo Pass the code into the disconnect callback on the server +function Client:disconnectNow(code) + code = code or 0 + self.connection:disconnect_now(code) +end + +--- Forcefully disconnects the client. The server is not notified of the disconnection. +-- @tparam Client client The client to reset. +function Client:reset() + if self.connection then + self.connection:reset() + end +end + +-- Creates the unserialized message that will be used in callbacks +-- In: serialized message (string) +-- Out: event (string), data (mixed) +function Client:__unpack(data) + if not self.deserialize then + self:log("error", "Can't deserialize message: deserialize was not set") + error("Can't deserialize message: deserialize was not set") + end + + local message = self.deserialize(data) + local eventName, data = message[1], message[2] + return eventName, data +end + +-- Creates the serialized message that will be sent over the network +-- In: event (string), data (mixed) +-- Out: serialized message (string) +function Client:__pack(event, data) + local message = {event, data} + local serializedMessage + + if not self.serialize then + self:log("error", "Can't serialize message: serialize was not set") + error("Can't serialize message: serialize was not set") + end + + -- 'Data' = binary data class in Love + if type(data) == "userdata" and data.type and data:typeOf("Data") then + message[2] = data:getString() + serializedMessage = self.serialize(message) + else + serializedMessage = self.serialize(message) + end + + return serializedMessage +end + +--- Send a message to the server. +-- @tparam string event The event to trigger with this message. +-- @param data The data to send. +function Client:send(event, data) + local serializedMessage = self:__pack(event, data) + + self.connection:send(serializedMessage, self.sendChannel, self.sendMode) + + self.packetsSent = self.packetsSent + 1 + + self:resetSendSettings() +end + +--- Add a callback to an event. +-- @tparam string event The event that will trigger the callback. +-- @tparam function callback The callback to be triggered. +-- @treturn function The callback that was passed in. +--@usage +--client:on("connect", function(data) +-- print("Connected to the server!") +--end) +function Client:on(event, callback) + return self.listener:addCallback(event, callback) +end + +function Client:_activateTriggers(event, data) + local result = self.listener:trigger(event, data) + + self.packetsReceived = self.packetsReceived + 1 + + if not result then + self:log("warning", "Tried to activate trigger: '" .. tostring(event) .. "' but it does not exist.") + end +end + +--- Remove a specific callback for an event. +-- @tparam function callback The callback to remove. +-- @treturn boolean Whether or not the callback was removed. +--@usage +--local callback = client:on("chatMessage", function(message) +-- print(message) +--end) +--client:removeCallback(callback) +function Client:removeCallback(callback) + return self.listener:removeCallback(callback) +end + +--- Log an event. +-- Alias for Client.logger:log. +-- @tparam string event The type of event that happened. +-- @tparam string data The message to log. +--@usage +--if somethingBadHappened then +-- client:log("error", "Something bad happened!") +--end +function Client:log(event, data) + return self.logger:log(event, data) +end + +--- Reset all send options to their default values. +function Client:resetSendSettings() + self.sendMode = self.defaultSendMode + self.sendChannel = self.defaultSendChannel +end + +--- Enables an adaptive order-2 PPM range coder for the transmitted data of all peers. Both the client and server must both either have compression enabled or disabled. +-- +-- Note: lua-enet does not currently expose a way to disable the compression after it has been enabled. +function Client:enableCompression() + return self.host:compress_with_range_coder() +end + +--- Set the send mode for the next outgoing message. +-- The mode will be reset after the next message is sent. The initial default +-- is "reliable". +-- @tparam string mode A valid send mode. +-- @see SEND_MODES +-- @usage +--client:setSendMode("unreliable") +--client:send("position", {...}) +function Client:setSendMode(mode) + if not isValidSendMode(mode) then + self:log("warning", "Tried to use invalid send mode: '" .. mode .. "'. Defaulting to reliable.") + mode = "reliable" + end + + self.sendMode = mode +end + +--- Set the default send mode for all future outgoing messages. +-- The initial default is "reliable". +-- @tparam string mode A valid send mode. +-- @see SEND_MODES +function Client:setDefaultSendMode(mode) + if not isValidSendMode(mode) then + self:log("error", "Tried to set default send mode to invalid mode: '" .. mode .. "'") + error("Tried to set default send mode to invalid mode: '" .. mode .. "'") + end + + self.defaultSendMode = mode +end + +--- Set the send channel for the next outgoing message. +-- The channel will be reset after the next message. Channels are zero-indexed +-- and cannot exceed the maximum number of channels allocated. The initial +-- default is 0. +-- @tparam number channel Channel to send data on. +-- @usage +--client:setSendChannel(2) -- the third channel +--client:send("important", "The message") +function Client:setSendChannel(channel) + if channel > (self.maxChannels - 1) then + self:log("warning", "Tried to use invalid channel: " .. channel .. " (max is " .. self.maxChannels - 1 .. "). Defaulting to 0.") + channel = 0 + end + + self.sendChannel = channel +end + +--- Set the default send channel for all future outgoing messages. +-- The initial default is 0. +-- @tparam number channel Channel to send data on. +function Client:setDefaultSendChannel(channel) + self.defaultSendChannel = channel +end + +--- Set the data schema for an event. +-- +-- Schemas allow you to set a specific format that the data will be sent. If the +-- client and server both know the format ahead of time, then the table keys +-- do not have to be sent across the network, which saves bandwidth. +-- @tparam string event The event to set the data schema for. +-- @tparam {string,...} schema The data schema. +-- @usage +-- server = sock.newServer(...) +-- client = sock.newClient(...) +-- +-- -- Without schemas +-- server:send("update", { +-- x = 4, +-- y = 100, +-- vx = -4.5, +-- vy = 23.1, +-- rotation = 1.4365, +-- }) +-- client:on("update", function(data) +-- -- data = { +-- -- x = 4, +-- -- y = 100, +-- -- vx = -4.5, +-- -- vy = 23.1, +-- -- rotation = 1.4365, +-- -- } +-- end) +-- +-- +-- -- With schemas +-- client:setSchema("update", { +-- "x", +-- "y", +-- "vx", +-- "vy", +-- "rotation", +-- }) +-- -- client no longer has to send the keys, saving bandwidth +-- server:send("update", { +-- 4, +-- 100, +-- -4.5, +-- 23.1, +-- 1.4365, +-- }) +-- client:on("update", function(data) +-- -- data = { +-- -- x = 4, +-- -- y = 100, +-- -- vx = -4.5, +-- -- vy = 23.1, +-- -- rotation = 1.4365, +-- -- } +-- end) +function Client:setSchema(event, schema) + return self.listener:setSchema(event, schema) +end + +--- Set the maximum number of channels. +-- @tparam number limit The maximum number of channels allowed. If it is 0, +-- then the maximum number of channels available on the system will be used. +function Client:setMaxChannels(limit) + self.host:channel_limit(limit) +end + +--- Set the timeout to wait for packets. +-- @tparam number timeout Time to wait for incoming packets in milliseconds. The initial +-- default is 0. +function Client:setMessageTimeout(timeout) + self.messageTimeout = timeout +end + +--- Set the incoming and outgoing bandwidth limits. +-- @tparam number incoming The maximum incoming bandwidth in bytes. +-- @tparam number outgoing The maximum outgoing bandwidth in bytes. +function Client:setBandwidthLimit(incoming, outgoing) + return self.host:bandwidth_limit(incoming, outgoing) +end + +--- Set how frequently to ping the server. +-- The round trip time is updated each time a ping is sent. The initial +-- default is 500ms. +-- @tparam number interval The interval, in milliseconds. +function Client:setPingInterval(interval) + if self.connection then + self.connection:ping_interval(interval) + end +end + +--- Change the probability at which unreliable packets should not be dropped. +-- @tparam number interval Interval, in milliseconds, over which to measure lowest mean RTT. (default: 5000ms) +-- @tparam number acceleration Rate at which to increase the throttle probability as mean RTT declines. (default: 2) +-- @tparam number deceleration Rate at which to decrease the throttle probability as mean RTT increases. +function Client:setThrottle(interval, acceleration, deceleration) + interval = interval or 5000 + acceleration = acceleration or 2 + deceleration = deceleration or 2 + if self.connection then + self.connection:throttle_configure(interval, acceleration, deceleration) + end +end + +--- Set the parameters for attempting to reconnect if a timeout is detected. +-- @tparam ?number limit A factor that is multiplied with a value that based on the average round trip time to compute the timeout limit. (default: 32) +-- @tparam ?number minimum Timeout value in milliseconds that a reliable packet has to be acknowledged if the variable timeout limit was exceeded. (default: 5000) +-- @tparam ?number maximum Fixed timeout in milliseconds for which any packet has to be acknowledged. +function Client:setTimeout(limit, minimum, maximum) + limit = limit or 32 + minimum = minimum or 5000 + maximum = maximum or 30000 + if self.connection then + self.connection:timeout(limit, minimum, maximum) + end +end + +--- Set the serialization functions for sending and receiving data. +-- Both the client and server must share the same serialization method. +-- @tparam function serialize The serialization function to use. +-- @tparam function deserialize The deserialization function to use. +-- @usage +--bitser = require "bitser" -- or any library you like +--client = sock.newClient("localhost", 22122) +--client:setSerialization(bitser.dumps, bitser.loads) +function Client:setSerialization(serialize, deserialize) + assert(type(serialize) == "function", "Serialize must be a function, got: '"..type(serialize).."'") + assert(type(deserialize) == "function", "Deserialize must be a function, got: '"..type(deserialize).."'") + self.serialize = serialize + self.deserialize = deserialize +end + +--- Gets whether the client is connected to the server. +-- @treturn boolean Whether the client is connected to the server. +-- @usage +-- client:connect() +-- client:isConnected() -- false +-- -- After a few client updates +-- client:isConnected() -- true +function Client:isConnected() + return self.connection ~= nil and self:getState() == "connected" +end + +--- Gets whether the client is disconnected from the server. +-- @treturn boolean Whether the client is connected to the server. +-- @usage +-- client:disconnect() +-- client:isDisconnected() -- false +-- -- After a few client updates +-- client:isDisconnected() -- true +function Client:isDisconnected() + return self.connection ~= nil and self:getState() == "disconnected" +end + +--- Gets whether the client is connecting to the server. +-- @treturn boolean Whether the client is connected to the server. +-- @usage +-- client:connect() +-- client:isConnecting() -- true +-- -- After a few client updates +-- client:isConnecting() -- false +-- client:isConnected() -- true +function Client:isConnecting() + local inConnectingState = false + for _, state in ipairs(sock.CONNECTING_STATES) do + if state == self:getState() then + inConnectingState = true + break + end + end + return self.connection ~= nil and inConnectingState +end + +--- Gets whether the client is disconnecting from the server. +-- @treturn boolean Whether the client is connected to the server. +-- @usage +-- client:disconnect() +-- client:isDisconnecting() -- true +-- -- After a few client updates +-- client:isDisconnecting() -- false +-- client:isDisconnected() -- true +function Client:isDisconnecting() + local inDisconnectingState = false + for _, state in ipairs(sock.DISCONNECTING_STATES) do + if state == self:getState() then + inDisconnectingState = true + break + end + end + return self.connection ~= nil and inDisconnectingState +end + +--- Get the total sent data since the server was created. +-- @treturn number The total sent data in bytes. +function Client:getTotalSentData() + return self.host:total_sent_data() +end + +--- Get the total received data since the server was created. +-- @treturn number The total received data in bytes. +function Client:getTotalReceivedData() + return self.host:total_received_data() +end + +--- Get the total number of packets (messages) sent since the client was created. +-- Everytime a message is sent or received, the corresponding figure is incremented. +-- Therefore, this is not necessarily an accurate indicator of how many packets were actually +-- exchanged over the network. +-- @treturn number The total number of sent packets. +function Client:getTotalSentPackets() + return self.packetsSent +end + +--- Get the total number of packets (messages) received since the client was created. +-- @treturn number The total number of received packets. +-- @see Client:getTotalSentPackets +function Client:getTotalReceivedPackets() + return self.packetsReceived +end + +--- Get the last time when network events were serviced. +-- @treturn number Timestamp of the last time events were serviced. +function Client:getLastServiceTime() + return self.host:service_time() +end + +--- Get the number of allocated channels. +-- Channels are zero-indexed, e.g. 16 channels allocated means that the +-- maximum channel that can be used is 15. +-- @treturn number Number of allocated channels. +function Client:getMaxChannels() + return self.maxChannels +end + +--- Get the timeout for packets. +-- @treturn number Time to wait for incoming packets in milliseconds. +-- initial default is 0. +function Client:getMessageTimeout() + return self.messageTimeout +end + +--- Return the round trip time (RTT, or ping) to the server, if connected. +-- It can take a few seconds for the time to approach an accurate value. +-- @treturn number The round trip time. +function Client:getRoundTripTime() + if self.connection then + return self.connection:round_trip_time() + end +end + +--- Get the unique connection id, if connected. +-- @treturn number The connection id. +function Client:getConnectId() + if self.connection then + return self.connection:connect_id() + end +end + +--- Get the current connection state, if connected. +-- @treturn string The connection state. +-- @see CONNECTION_STATES +function Client:getState() + if self.connection then + return self.connection:state() + end +end + +--- Get the index of the enet peer. All peers of an ENet host are kept in an array. This function finds and returns the index of the peer of its host structure. +-- @treturn number The index of the peer. +function Client:getIndex() + if self.connection then + return self.connection:index() + end +end + +--- Get the socket address of the host. +-- @treturn string A description of the socket address, in the format "A.B.C.D:port" where A.B.C.D is the IP address of the used socket. +function Client:getSocketAddress() + return self.host:get_socket_address() +end + +--- Get the enet_peer that has the given index. +-- @treturn enet_peer The underlying enet peer object. +function Client:getPeerByIndex(index) + return self.host:get_peer(index) +end + +--- Get the current send mode. +-- @treturn string +-- @see SEND_MODES +function Client:getSendMode() + return self.sendMode +end + +--- Get the default send mode. +-- @treturn string +-- @see SEND_MODES +function Client:getDefaultSendMode() + return self.defaultSendMode +end + +--- Get the IP address or hostname that the client was created with. +-- @treturn string +function Client:getAddress() + return self.address +end + +--- Get the port that the client is connecting to. +-- @treturn number +function Client:getPort() + return self.port +end + +--- Creates a new Server object. +-- @tparam ?string address Hostname or IP address to bind to. (default: "localhost") +-- @tparam ?number port Port to listen to for data. (default: 22122) +-- @tparam ?number maxPeers Maximum peers that can connect to the server. (default: 64) +-- @tparam ?number maxChannels Maximum channels available to send and receive data. (default: 1) +-- @tparam ?number inBandwidth Maximum incoming bandwidth (default: 0) +-- @tparam ?number outBandwidth Maximum outgoing bandwidth (default: 0) +-- @return A new Server object. +-- @see Server +-- @within sock +-- @usage +--local sock = require "sock" +-- +-- -- Local server hosted on localhost:22122 (by default) +--server = sock.newServer() +-- +-- -- Local server only, on port 1234 +--server = sock.newServer("localhost", 1234) +-- +-- -- Server hosted on static IP 123.45.67.89, on port 22122 +--server = sock.newServer("123.45.67.89", 22122) +-- +-- -- Server hosted on any IP, on port 22122 +--server = sock.newServer("*", 22122) +-- +-- -- Limit peers to 10, channels to 2 +--server = sock.newServer("*", 22122, 10, 2) +-- +-- -- Limit incoming/outgoing bandwidth to 1kB/s (1000 bytes/s) +--server = sock.newServer("*", 22122, 10, 2, 1000, 1000) +sock.newServer = function(address, port, maxPeers, maxChannels, inBandwidth, outBandwidth) + address = address or "localhost" + port = port or 22122 + maxPeers = maxPeers or 64 + maxChannels = maxChannels or 1 + inBandwidth = inBandwidth or 0 + outBandwidth = outBandwidth or 0 + + local server = setmetatable({ + address = address, + port = port, + host = nil, + + messageTimeout = 0, + maxChannels = maxChannels, + maxPeers = maxPeers, + sendMode = "reliable", + defaultSendMode = "reliable", + sendChannel = 0, + defaultSendChannel = 0, + + peers = {}, + clients = {}, + + listener = newListener(), + logger = newLogger("SERVER"), + + serialize = nil, + deserialize = nil, + + packetsSent = 0, + packetsReceived = 0, + }, Server_mt) + + -- ip, max peers, max channels, in bandwidth, out bandwidth + -- number of channels for the client and server must match + server.host = enet.host_create(server.address .. ":" .. server.port, server.maxPeers, server.maxChannels) + + if not server.host then + error("Failed to create the host. Is there another server running on :"..server.port.."?") + end + + server:setBandwidthLimit(inBandwidth, outBandwidth) + + if bitserLoaded then + server:setSerialization(bitser.dumps, bitser.loads) + end + + return server +end + +--- Creates a new Client instance. +-- @tparam ?string/peer serverOrAddress Usually the IP address or hostname to connect to. It can also be an enet peer. (default: "localhost") +-- @tparam ?number port Port number of the server to connect to. (default: 22122) +-- @tparam ?number maxChannels Maximum channels available to send and receive data. (default: 1) +-- @return A new Client object. +-- @see Client +-- @within sock +-- @usage +--local sock = require "sock" +-- +-- -- Client that will connect to localhost:22122 (by default) +--client = sock.newClient() +-- +-- -- Client that will connect to localhost:1234 +--client = sock.newClient("localhost", 1234) +-- +-- -- Client that will connect to 123.45.67.89:1234, using two channels +-- -- NOTE: Server must also allocate two channels! +--client = sock.newClient("123.45.67.89", 1234, 2) +sock.newClient = function(serverOrAddress, port, maxChannels) + serverOrAddress = serverOrAddress or "localhost" + port = port or 22122 + maxChannels = maxChannels or 1 + + local client = setmetatable({ + address = nil, + port = nil, + host = nil, + + connection = nil, + connectId = nil, + + messageTimeout = 0, + maxChannels = maxChannels, + sendMode = "reliable", + defaultSendMode = "reliable", + sendChannel = 0, + defaultSendChannel = 0, + + listener = newListener(), + logger = newLogger("CLIENT"), + + serialize = nil, + deserialize = nil, + + packetsReceived = 0, + packetsSent = 0, + }, Client_mt) + + -- Two different forms for client creation: + -- 1. Pass in (address, port) and connect to that. + -- 2. Pass in (enet peer) and set that as the existing connection. + -- The first would be the common usage for regular client code, while the + -- latter is mostly used for creating clients in the server-side code. + + -- First form: (address, port) + if port ~= nil and type(port) == "number" and serverOrAddress ~= nil and type(serverOrAddress) == "string" then + client.address = serverOrAddress + client.port = port + client.host = enet.host_create() + + -- Second form: (enet peer) + elseif type(serverOrAddress) == "userdata" then + client.connection = serverOrAddress + client.connectId = client.connection:connect_id() + end + + if bitserLoaded then + client:setSerialization(bitser.dumps, bitser.loads) + end + + return client +end + +return sock diff --git a/lib/sock/spec/binser.lua b/lib/sock/spec/binser.lua new file mode 100644 index 0000000..3ed2696 --- /dev/null +++ b/lib/sock/spec/binser.lua @@ -0,0 +1,626 @@ +-- binser.lua + +--[[ +Copyright (c) 2016 Calvin Rose + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +]] + +local assert = assert +local error = error +local select = select +local pairs = pairs +local getmetatable = getmetatable +local setmetatable = setmetatable +local tonumber = tonumber +local type = type +local loadstring = loadstring +local concat = table.concat +local char = string.char +local byte = string.byte +local format = string.format +local sub = string.sub +local dump = string.dump +local floor = math.floor +local frexp = math.frexp +local ldexp = math.ldexp +local unpack = unpack or table.unpack + +-- NIL = 202 +-- FLOAT = 203 +-- TRUE = 204 +-- FALSE = 205 +-- STRING = 206 +-- TABLE = 207 +-- REFERENCE = 208 +-- CONSTRUCTOR = 209 +-- FUNCTION = 210 +-- RESOURCE = 211 + +local mts = {} +local ids = {} +local serializers = {} +local deserializers = {} +local resources = {} +local resources_by_name = {} + +local function pack(...) + return {...}, select("#", ...) +end + +local function not_array_index(x, len) + return type(x) ~= "number" or x < 1 or x > len or x ~= floor(x) +end + +local function type_check(x, tp, name) + assert(type(x) == tp, + format("Expected parameter %q to be of type %q.", name, tp)) +end + +-- Copyright (C) 2012-2015 Francois Perrad. +-- number serialization code modified from https://github.com/fperrad/lua-MessagePack +-- Encode a number as a big-endian ieee-754 double (or a small integer) +local function number_to_str(n) + if floor(n) == n then -- int + if n <= 100 and n >= -27 then -- 1 byte, 7 bits of data + return char(n + 27) + elseif n <= 8191 and n >= -8192 then -- 2 bytes, 14 bits of data + n = n + 8192 + return char(128 + (floor(n / 0x100) % 0x100), n % 0x100) + end + end + local sign = 0 + if n < 0.0 then + sign = 0x80 + n = -n + end + local m, e = frexp(n) -- mantissa, exponent + if m ~= m then + return char(203, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) + elseif m == 1/0 then + if sign == 0 then + return char(203, 0x7F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) + else + return char(203, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) + end + end + e = e + 0x3FE + if e < 1 then -- denormalized numbers + m = m * ldexp(0.5, 53 + e) + e = 0 + else + m = (m * 2 - 1) * ldexp(0.5, 53) + end + return char(203, + sign + floor(e / 0x10), + (e % 0x10) * 0x10 + floor(m / 0x1000000000000), + floor(m / 0x10000000000) % 0x100, + floor(m / 0x100000000) % 0x100, + floor(m / 0x1000000) % 0x100, + floor(m / 0x10000) % 0x100, + floor(m / 0x100) % 0x100, + m % 0x100) +end + +-- Copyright (C) 2012-2015 Francois Perrad. +-- number deserialization code also modified from https://github.com/fperrad/lua-MessagePack +local function number_from_str(str, index) + local b = byte(str, index) + if b < 128 then + return b - 27, index + 1 + elseif b < 192 then + return byte(str, index + 1) + 0x100 * (b - 128) - 8192, index + 2 + end + local b1, b2, b3, b4, b5, b6, b7, b8 = byte(str, index + 1, index + 8) + local sign = b1 > 0x7F and -1 or 1 + local e = (b1 % 0x80) * 0x10 + floor(b2 / 0x10) + local m = ((((((b2 % 0x10) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8 + local n + if e == 0 then + if m == 0 then + n = sign * 0.0 + else + n = sign * ldexp(m / ldexp(0.5, 53), -1022) + end + elseif e == 0x7FF then + if m == 0 then + n = sign * (1/0) + else + n = 0.0/0.0 + end + else + n = sign * ldexp(1.0 + m / ldexp(0.5, 53), e - 0x3FF) + end + return n, index + 9 +end + +local types = {} + +types["nil"] = function(x, visited, accum) + accum[#accum + 1] = "\202" +end + +function types.number(x, visited, accum) + accum[#accum + 1] = number_to_str(x) +end + +function types.boolean(x, visited, accum) + accum[#accum + 1] = x and "\204" or "\205" +end + +function types.string(x, visited, accum) + local alen = #accum + if visited[x] then + accum[alen + 1] = "\208" + accum[alen + 2] = number_to_str(visited[x]) + else + visited[x] = visited.next + visited.next = visited.next + 1 + accum[alen + 1] = "\206" + accum[alen + 2] = number_to_str(#x) + accum[alen + 3] = x + end +end + +local function check_custom_type(x, visited, accum) + local res = resources[x] + if res then + accum[#accum + 1] = "\211" + types[type(res)](res, visited, accum) + return true + end + local mt = getmetatable(x) + local id = mt and ids[mt] + if id then + if x == visited.temp then + error("Infinite loop in constructor.") + end + visited.temp = x + accum[#accum + 1] = "\209" + types[type(id)](id, visited, accum) + local args, len = pack(serializers[id](x)) + accum[#accum + 1] = number_to_str(len) + for i = 1, len do + local arg = args[i] + types[type(arg)](arg, visited, accum) + end + visited[x] = visited.next + visited.next = visited.next + 1 + return true + end +end + +function types.userdata(x, visited, accum) + if visited[x] then + accum[#accum + 1] = "\208" + accum[#accum + 1] = number_to_str(visited[x]) + else + if check_custom_type(x, visited, accum) then return end + error("Cannot serialize this userdata.") + end +end + +function types.table(x, visited, accum) + if visited[x] then + accum[#accum + 1] = "\208" + accum[#accum + 1] = number_to_str(visited[x]) + else + if check_custom_type(x, visited, accum) then return end + visited[x] = visited.next + visited.next = visited.next + 1 + local xlen = #x + accum[#accum + 1] = "\207" + accum[#accum + 1] = number_to_str(xlen) + for i = 1, xlen do + local v = x[i] + types[type(v)](v, visited, accum) + end + local key_count = 0 + for k in pairs(x) do + if not_array_index(k, xlen) then + key_count = key_count + 1 + end + end + accum[#accum + 1] = number_to_str(key_count) + for k, v in pairs(x) do + if not_array_index(k, xlen) then + types[type(k)](k, visited, accum) + types[type(v)](v, visited, accum) + end + end + end +end + +types["function"] = function(x, visited, accum) + if visited[x] then + accum[#accum + 1] = "\208" + accum[#accum + 1] = number_to_str(visited[x]) + else + if check_custom_type(x, visited, accum) then return end + visited[x] = visited.next + visited.next = visited.next + 1 + local str = dump(x) + accum[#accum + 1] = "\210" + accum[#accum + 1] = number_to_str(#str) + accum[#accum + 1] = str + end +end + +types.cdata = function(x, visited, accum) + if visited[x] then + accum[#accum + 1] = "\208" + accum[#accum + 1] = number_to_str(visited[x]) + else + if check_custom_type(x, visited, #accum) then return end + error("Cannot serialize this cdata.") + end +end + +types.thread = function() error("Cannot serialize threads.") end + +local function deserialize_value(str, index, visited) + local t = byte(str, index) + if not t then return end + if t < 128 then + return t - 27, index + 1 + elseif t < 192 then + return byte(str, index + 1) + 0x100 * (t - 128) - 8192, index + 2 + elseif t == 202 then + return nil, index + 1 + elseif t == 203 then + return number_from_str(str, index) + elseif t == 204 then + return true, index + 1 + elseif t == 205 then + return false, index + 1 + elseif t == 206 then + local length, dataindex = deserialize_value(str, index + 1, visited) + local nextindex = dataindex + length + local substr = sub(str, dataindex, nextindex - 1) + visited[#visited + 1] = substr + return substr, nextindex + elseif t == 207 then + local count, nextindex = number_from_str(str, index + 1) + local ret = {} + visited[#visited + 1] = ret + for i = 1, count do + ret[i], nextindex = deserialize_value(str, nextindex, visited) + end + count, nextindex = number_from_str(str, nextindex) + for i = 1, count do + local k, v + k, nextindex = deserialize_value(str, nextindex, visited) + v, nextindex = deserialize_value(str, nextindex, visited) + ret[k] = v + end + return ret, nextindex + elseif t == 208 then + local ref, nextindex = number_from_str(str, index + 1) + return visited[ref], nextindex + elseif t == 209 then + local count + local name, nextindex = deserialize_value(str, index + 1, visited) + count, nextindex = number_from_str(str, nextindex) + local args = {} + for i = 1, count do + args[i], nextindex = deserialize_value(str, nextindex, visited) + end + local ret = deserializers[name](unpack(args)) + visited[#visited + 1] = ret + return ret, nextindex + elseif t == 210 then + local length, dataindex = deserialize_value(str, index + 1, visited) + local nextindex = dataindex + length + local ret = loadstring(sub(str, dataindex, nextindex - 1)) + visited[#visited + 1] = ret + return ret, nextindex + elseif t == 211 then + local res, nextindex = deserialize_value(str, index + 1, visited) + return resources_by_name[res], nextindex + else + error("Could not deserialize type byte " .. t .. ".") + end +end + +local function serialize(...) + local visited = {next = 1} + local accum = {} + for i = 1, select("#", ...) do + local x = select(i, ...) + types[type(x)](x, visited, accum) + end + return concat(accum) +end + +local function make_file_writer(file) + return setmetatable({}, { + __newindex = function(_, _, v) + file:write(v) + end + }) +end + +local function serialize_to_file(path, mode, ...) + local file, err = io.open(path, mode) + assert(file, err) + local visited = {next = 1} + local accum = make_file_writer(file) + for i = 1, select("#", ...) do + local x = select(i, ...) + types[type(x)](x, visited, accum) + end + -- flush the writer + file:flush() + file:close() +end + +local function writeFile(path, ...) + return serialize_to_file(path, "wb", ...) +end + +local function appendFile(path, ...) + return serialize_to_file(path, "ab", ...) +end + +local function deserialize(str) + assert(type(str) == "string", "Expected string to deserialize.") + local vals = {} + local index = 1 + local visited = {} + local len = 0 + local val + while index do + val, index = deserialize_value(str, index, visited) + if index then + len = len + 1 + vals[len] = val + end + end + return vals, len +end + +local function deserializeN(str, n) + assert(type(str) == "string", "Expected string to deserialize.") + n = n or 1 + assert(type(n) == "number", "Expected a number for parameter n.") + assert(n > 0 and floor(n) == n, "N must be a poitive integer.") + local vals = {} + local index = 1 + local visited = {} + local len = 0 + local val + while index and len < n do + val, index = deserialize_value(str, index, visited) + if index then + len = len + 1 + vals[len] = val + end + end + return unpack(vals, 1, n) +end + +local function readFile(path) + local file, err = io.open(path, "rb") + assert(file, err) + local str = file:read("*all") + file:close() + return deserialize(str) +end + +local function default_deserialize(metatable) + return function(...) + local ret = {} + for i = 1, select("#", ...), 2 do + ret[select(i, ...)] = select(i + 1, ...) + end + return setmetatable(ret, metatable) + end +end + +local function default_serialize(x) + assert(type(x) == "table", + "Default serialization for custom types only works for tables.") + local args = {} + local len = 0 + for k, v in pairs(x) do + args[len + 1], args[len + 2] = k, v + len = len + 2 + end + return unpack(args, 1, len) +end + +-- Templating + +local function normalize_template(template) + local ret = {} + for i = 1, #template do + ret[i] = template[i] + end + local non_array_part = {} + -- The non-array part of the template (nested templates) have to be deterministic, so they are sorted. + -- This means that inherently non deterministicly sortable keys (tables, functions) should NOT be used + -- in templates. Looking for way around this. + for k in pairs(template) do + if not_array_index(k, #template) then + non_array_part[#non_array_part + 1] = k + end + end + table.sort(non_array_part) + for i = 1, #non_array_part do + local name = non_array_part[i] + ret[#ret + 1] = {name, normalize_template(template[name])} + end + return ret +end + +local function templatepart_serialize(part, argaccum, x, len) + local extras = {} + local extracount = 0 + for k, v in pairs(x) do + extras[k] = v + extracount = extracount + 1 + end + for i = 1, #part do + extracount = extracount - 1 + if type(part[i]) == "table" then + extras[part[i][1]] = nil + len = templatepart_serialize(part[i][2], argaccum, x[part[i][1]], len) + else + extras[part[i]] = nil + len = len + 1 + argaccum[len] = x[part[i]] + end + end + if extracount > 0 then + argaccum[len + 1] = extras + else + argaccum[len + 1] = nil + end + return len + 1 +end + +local function templatepart_deserialize(ret, part, values, vindex) + for i = 1, #part do + local name = part[i] + if type(name) == "table" then + local newret = {} + ret[name[1]] = newret + vindex = templatepart_deserialize(newret, name[2], values, vindex) + else + ret[name] = values[vindex] + vindex = vindex + 1 + end + end + local extras = values[vindex] + if extras then + for k, v in pairs(extras) do + ret[k] = v + end + end + return vindex + 1 +end + +local function template_serializer_and_deserializer(metatable, template) + return function(x) + argaccum = {} + local len = templatepart_serialize(template, argaccum, x, 0) + return unpack(argaccum, 1, len) + end, function(...) + local ret = {} + local len = select("#", ...) + local args = {...} + templatepart_deserialize(ret, template, args, 1) + return setmetatable(ret, metatable) + end +end + +local function register(metatable, name, serialize, deserialize) + name = name or metatable.name + serialize = serialize or metatable._serialize + deserialize = deserialize or metatable._deserialize + if not serialize then + if metatable._template then + local t = normalize_template(metatable._template) + serialize, deserialize = template_serializer_and_deserializer(metatable, t) + elseif not deserialize then + serialize = default_serialize + deserialize = default_deserialize(metatable) + else + serialize = metatable + end + end + type_check(metatable, "table", "metatable") + type_check(name, "string", "name") + type_check(serialize, "function", "serialize") + type_check(deserialize, "function", "deserialize") + assert(not ids[metatable], "Metatable already registered.") + assert(not mts[name], ("Name %q already registered."):format(name)) + mts[name] = metatable + ids[metatable] = name + serializers[name] = serialize + deserializers[name] = deserialize + return metatable +end + +local function unregister(item) + local name, metatable + if type(item) == "string" then -- assume name + name, metatable = item, mts[item] + else -- assume metatable + name, metatable = ids[item], item + end + type_check(name, "string", "name") + type_check(metatable, "table", "metatable") + mts[name] = nil + ids[metatable] = nil + serializers[name] = nil + deserializers[name] = nil + return metatable +end + +local function registerClass(class, name) + name = name or class.name + if class.__instanceDict then -- middleclass + register(class.__instanceDict, name) + else -- assume 30log or similar library + register(class, name) + end + return class +end + +local function registerResource(resource, name) + type_check(name, "string", "name") + assert(not resources[resource], + "Resource already registered.") + assert(not resources_by_name[name], + format("Resource %q already exists.", name)) + resources_by_name[name] = resource + resources[resource] = name + return resource +end + +local function unregisterResource(name) + type_check(name, "string", "name") + assert(resources_by_name[name], format("Resource %q does not exist.", name)) + local resource = resources_by_name[name] + resources_by_name[name] = nil + resources[resource] = nil + return resource +end + +return { + -- aliases + s = serialize, + d = deserialize, + dn = deserializeN, + r = readFile, + w = writeFile, + a = appendFile, + + serialize = serialize, + deserialize = deserialize, + deserializeN = deserializeN, + readFile = readFile, + writeFile = writeFile, + appendFile = appendFile, + register = register, + unregister = unregister, + registerResource = registerResource, + unregisterResource = unregisterResource, + registerClass = registerClass +} diff --git a/lib/sock/spec/bitser.lua b/lib/sock/spec/bitser.lua new file mode 100644 index 0000000..e91d341 --- /dev/null +++ b/lib/sock/spec/bitser.lua @@ -0,0 +1,424 @@ +--[[ +Copyright (c) 2016, Robin Wellner + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +]] + +local floor = math.floor +local pairs = pairs +local type = type +local insert = table.insert +local getmetatable = getmetatable +local setmetatable = setmetatable + +local ffi = require("ffi") +local buf_pos = 0 +local buf_size = -1 +local buf = nil +local writable_buf = nil +local writable_buf_size = nil + +local function Buffer_prereserve(min_size) + if buf_size < min_size then + buf_size = min_size + buf = ffi.new("uint8_t[?]", buf_size) + end +end + +local function Buffer_clear() + buf_size = -1 + buf = nil + writable_buf = nil + writable_buf_size = nil +end + +local function Buffer_makeBuffer(size) + if writable_buf then + buf = writable_buf + buf_size = writable_buf_size + writable_buf = nil + writable_buf_size = nil + end + buf_pos = 0 + Buffer_prereserve(size) +end + +local function Buffer_newReader(str) + Buffer_makeBuffer(#str) + ffi.copy(buf, str, #str) +end + +local function Buffer_newDataReader(data, size) + writable_buf = buf + writable_buf_size = buf_size + buf_pos = 0 + buf_size = size + buf = ffi.cast("uint8_t*", data) +end + +local function Buffer_reserve(additional_size) + while buf_pos + additional_size > buf_size do + buf_size = buf_size * 2 + local oldbuf = buf + buf = ffi.new("uint8_t[?]", buf_size) + ffi.copy(buf, oldbuf, buf_pos) + end +end + +local function Buffer_write_byte(x) + Buffer_reserve(1) + buf[buf_pos] = x + buf_pos = buf_pos + 1 +end + +local function Buffer_write_string(s) + Buffer_reserve(#s) + ffi.copy(buf + buf_pos, s, #s) + buf_pos = buf_pos + #s +end + +local function Buffer_write_data(ct, len, ...) + Buffer_reserve(len) + ffi.copy(buf + buf_pos, ffi.new(ct, ...), len) + buf_pos = buf_pos + len +end + +local function Buffer_ensure(numbytes) + if buf_pos + numbytes > buf_size then + error("malformed serialized data") + end +end + +local function Buffer_read_byte() + Buffer_ensure(1) + local x = buf[buf_pos] + buf_pos = buf_pos + 1 + return x +end + +local function Buffer_read_string(len) + Buffer_ensure(len) + local x = ffi.string(buf + buf_pos, len) + buf_pos = buf_pos + len + return x +end + +local function Buffer_read_data(ct, len) + Buffer_ensure(len) + local x = ffi.new(ct) + ffi.copy(x, buf + buf_pos, len) + buf_pos = buf_pos + len + return x +end + +local resource_registry = {} +local resource_name_registry = {} +local class_registry = {} +local class_name_registry = {} +local classkey_registry = {} +local class_deserialize_registry = {} + +local serialize_value + +local function write_number(value, _) + if floor(value) == value and value >= -2147483648 and value <= 2147483647 then + if value >= -27 and value <= 100 then + --small int + Buffer_write_byte(value + 27) + elseif value >= -32768 and value <= 32767 then + --short int + Buffer_write_byte(250) + Buffer_write_data("int16_t[1]", 2, value) + else + --long int + Buffer_write_byte(245) + Buffer_write_data("int32_t[1]", 4, value) + end + else + --double + Buffer_write_byte(246) + Buffer_write_data("double[1]", 8, value) + end +end + +local function write_string(value, _) + if #value < 32 then + --short string + Buffer_write_byte(192 + #value) + else + --long string + Buffer_write_byte(244) + write_number(#value) + end + Buffer_write_string(value) +end + +local function write_nil(_, _) + Buffer_write_byte(247) +end + +local function write_boolean(value, _) + Buffer_write_byte(value and 249 or 248) +end + +local function write_table(value, seen) + local classkey + local classname = (class_name_registry[value.class] -- MiddleClass + or class_name_registry[value.__baseclass] -- SECL + or class_name_registry[getmetatable(value)] -- hump.class + or class_name_registry[value.__class__]) -- Slither + if classname then + classkey = classkey_registry[classname] + Buffer_write_byte(242) + serialize_value(classname, seen) + else + Buffer_write_byte(240) + end + local len = #value + write_number(len, seen) + for i = 1, len do + serialize_value(value[i], seen) + end + local klen = 0 + for k in pairs(value) do + if (type(k) ~= 'number' or floor(k) ~= k or k > len or k < 1) and k ~= classkey then + klen = klen + 1 + end + end + write_number(klen, seen) + for k, v in pairs(value) do + if (type(k) ~= 'number' or floor(k) ~= k or k > len or k < 1) and k ~= classkey then + serialize_value(k, seen) + serialize_value(v, seen) + end + end +end + +local types = {number = write_number, string = write_string, table = write_table, boolean = write_boolean, ["nil"] = write_nil} + +serialize_value = function(value, seen) + if seen[value] then + local ref = seen[value] + if ref < 64 then + --small reference + Buffer_write_byte(128 + ref) + else + --long reference + Buffer_write_byte(243) + write_number(ref, seen) + end + return + end + local t = type(value) + if t ~= 'number' and t ~= 'boolean' and t ~= 'nil' then + seen[value] = seen.len + seen.len = seen.len + 1 + end + if resource_name_registry[value] then + local name = resource_name_registry[value] + if #name < 16 then + --small resource + Buffer_write_byte(224 + #name) + Buffer_write_string(name) + else + --long resource + Buffer_write_byte(241) + write_string(name, seen) + end + return + end + (types[t] or + error("cannot serialize type " .. t) + )(value, seen) +end + +local function serialize(value) + Buffer_makeBuffer(4096) + local seen = {len = 0} + serialize_value(value, seen) +end + +local function add_to_seen(value, seen) + insert(seen, value) + return value +end + +local function reserve_seen(seen) + insert(seen, 42) + return #seen +end + +local function deserialize_value(seen) + local t = Buffer_read_byte() + if t < 128 then + --small int + return t - 27 + elseif t < 192 then + --small reference + return seen[t - 127] + elseif t < 224 then + --small string + return add_to_seen(Buffer_read_string(t - 192), seen) + elseif t < 240 then + --small resource + return add_to_seen(resource_registry[Buffer_read_string(t - 224)], seen) + elseif t == 240 then + --table + local v = add_to_seen({}, seen) + local len = deserialize_value(seen) + for i = 1, len do + v[i] = deserialize_value(seen) + end + len = deserialize_value(seen) + for _ = 1, len do + local key = deserialize_value(seen) + v[key] = deserialize_value(seen) + end + return v + elseif t == 241 then + --long resource + local idx = reserve_seen(seen) + local value = resource_registry[deserialize_value(seen)] + seen[idx] = value + return value + elseif t == 242 then + --instance + local instance = add_to_seen({}, seen) + local classname = deserialize_value(seen) + local class = class_registry[classname] + local classkey = classkey_registry[classname] + local deserializer = class_deserialize_registry[classname] + local len = deserialize_value(seen) + for i = 1, len do + instance[i] = deserialize_value(seen) + end + len = deserialize_value(seen) + for _ = 1, len do + local key = deserialize_value(seen) + instance[key] = deserialize_value(seen) + end + if classkey then + instance[classkey] = class + end + return deserializer(instance, class) + elseif t == 243 then + --reference + return seen[deserialize_value(seen) + 1] + elseif t == 244 then + --long string + return add_to_seen(Buffer_read_string(deserialize_value(seen)), seen) + elseif t == 245 then + --long int + return Buffer_read_data("int32_t[1]", 4)[0] + elseif t == 246 then + --double + return Buffer_read_data("double[1]", 8)[0] + elseif t == 247 then + --nil + return nil + elseif t == 248 then + --false + return false + elseif t == 249 then + --true + return true + elseif t == 250 then + --short int + return Buffer_read_data("int16_t[1]", 2)[0] + else + error("unsupported serialized type " .. t) + end +end + +local function deserialize_MiddleClass(instance, class) + return setmetatable(instance, class.__instanceDict) +end + +local function deserialize_SECL(instance, class) + return setmetatable(instance, getmetatable(class)) +end + +local deserialize_humpclass = setmetatable + +local function deserialize_Slither(instance, class) + return getmetatable(class).allocate(instance) +end + +return {dumps = function(value) + serialize(value) + return ffi.string(buf, buf_pos) +end, dumpLoveFile = function(fname, value) + serialize(value) + love.filesystem.write(fname, ffi.string(buf, buf_pos)) +end, loadLoveFile = function(fname) + local serializedData = love.filesystem.newFileData(fname) + Buffer_newDataReader(serializedData:getPointer(), serializedData:getSize()) + return deserialize_value({}) +end, loadData = function(data, size) + Buffer_newDataReader(data, size) + return deserialize_value({}) +end, loads = function(str) + Buffer_newReader(str) + return deserialize_value({}) +end, register = function(name, resource) + assert(not resource_registry[name], name .. " already registered") + resource_registry[name] = resource + resource_name_registry[resource] = name + return resource +end, unregister = function(name) + resource_name_registry[resource_registry[name]] = nil + resource_registry[name] = nil +end, registerClass = function(name, class, classkey, deserializer) + if not class then + class = name + name = class.__name__ or class.name + end + if not classkey then + if class.__instanceDict then + -- assume MiddleClass + classkey = 'class' + elseif class.__baseclass then + -- assume SECL + classkey = '__baseclass' + end + -- assume hump.class, Slither, or something else that doesn't store the + -- class directly on the instance + end + if not deserializer then + if class.__instanceDict then + -- assume MiddleClass + deserializer = deserialize_MiddleClass + elseif class.__baseclass then + -- assume SECL + deserializer = deserialize_SECL + elseif class.__index == class then + -- assume hump.class + deserializer = deserialize_humpclass + elseif class.__name__ then + -- assume Slither + deserializer = deserialize_Slither + else + error("no deserializer given for unsupported class library") + end + end + class_registry[name] = class + classkey_registry[name] = classkey + class_deserialize_registry[name] = deserializer + class_name_registry[class] = name + return class +end, unregisterClass = function(name) + class_name_registry[class_registry[name]] = nil + classkey_registry[name] = nil + class_deserialize_registry[name] = nil + class_registry[name] = nil +end, reserveBuffer = Buffer_prereserve, clearBuffer = Buffer_clear} diff --git a/lib/sock/spec/sock_spec.lua b/lib/sock/spec/sock_spec.lua new file mode 100644 index 0000000..28a3359 --- /dev/null +++ b/lib/sock/spec/sock_spec.lua @@ -0,0 +1,543 @@ +package.path = package.path .. ";../?.lua" +local bitser = require "spec.bitser" +-- this is a hack :) +package.loaded['bitser'] = bitser +local sock = require "sock" + +describe('sock.lua core', function() + it("creates clients", function() + local client = sock.newClient() + assert.are_not.equal(client, nil) + + assert.equal(client.address, "localhost") + assert.equal(client.port, 22122) + assert.equal(client.maxChannels, 1) + end) + + it("creates clients on localhost", function() + local client = sock.newClient("localhost") + assert.truthy(client) + + local client = sock.newClient("localhost", 22122) + assert.truthy(client) + + local client = sock.newClient("127.0.0.1") + assert.truthy(client) + + local client = sock.newClient("127.0.0.1", 22122) + assert.truthy(client) + end) + + it("creates servers", function() + local server = sock.newServer() + assert.are_not.equal(server, nil) + + assert.equal(server.address, "localhost") + assert.equal(server.port, 22122) + assert.equal(server.maxPeers, 64) + assert.equal(server.maxChannels, 1) + + server:destroy() + end) +end) + +describe("the client", function() + it("connects to a server", function() + local client = sock.newClient("localhost", 22122) + local server = sock.newServer("0.0.0.0", 22122) + + local connected = false + client:on("connect", function(data) + connected = true + end) + + client:connect() + + client:update() + server:update() + client:update() + server:update() + + assert.True(client:isConnected()) + assert.True(connected) + + server:destroy() + end) + + it("adds callbacks", function() + local client = sock.newClient() + + local helloCallback = function() + print("hello") + end + + local callback = client:on("helloMessage", helloCallback) + assert.equal(helloCallback, callback) + + local found = false + for i, callbacks in pairs(client.listener.triggers) do + for j, callback in pairs(callbacks) do + if callback == helloCallback then + found = true + end + end + end + assert.True(found) + end) + + it("removes callbacks", function() + local client = sock.newClient() + + local helloCallback = function() + print("hello") + end + + local callback = client:on("helloMessage", helloCallback) + + assert.True(client:removeCallback(callback)) + end) + + it("does not remove non-existent callbacks", function() + local client = sock.newClient() + + local nonsense = function() end + + assert.False(client:removeCallback(nonsense)) + end) + + it("triggers callbacks", function() + local client = sock.newClient() + + local handled = false + local handleConnect = function() + handled = true + end + + client:on("connect", handleConnect) + client:_activateTriggers("connect", "connection event test") + + assert.True(handled) + end) + + it("sets send channel", function() + local client = sock.newClient(nil, nil, 8) + + client:setSendChannel(7) + assert.equal(client.sendChannel, 7) + end) + + it('can send a message with a schema', function() + local client = sock.newClient("localhost", 22122) + local server = sock.newServer("0.0.0.0", 22122) + + client:connect() + + client:update() + server:update() + client:update() + + local received = false + + server:setSchema('test', { + 'first', + 'second', + 'third', + }) + client:setSchema('test', { + 'first', + 'second', + 'third', + }) + server:on('test', function(data, client) + assert.are.same(data, { + first = 'this is the first message', + second = 'this is the second message', + third = 'this is the third message', + }) + received = true + end) + + client:send('test', { + 'this is the first message', + 'this is the second message', + 'this is the third message', + }) + client:update() + server:update() + + assert.True(received) + + server:destroy() + end) + + insulate('can send', function() + before_each(function() + _G.client = sock.newClient("localhost", 22122) + _G.server = sock.newServer("0.0.0.0", 22122) + + client:connect() + + client:update() + server:update() + client:update() + end) + + after_each(function() + server:destroy() + end) + + it('a string', function() + local received = false + + server:on('test', function(data, client) + assert.equal(data, 'this is the test string') + received = true + end) + + client:send('test', 'this is the test string') + client:update() + server:update() + + assert.True(received) + end) + + it('an integer', function() + local received = false + + server:on('test', function(data, client) + assert.equal(data, 12345678) + received = true + end) + + client:send('test', 12345678) + client:update() + server:update() + + assert.True(received) + end) + + it('a floating point number', function() + local received = false + + server:on('test', function(data, client) + assert.equal(data, 0.123456789) + received = true + end) + + client:send('test', 0.123456789) + client:update() + server:update() + + assert.True(received) + end) + + it('a huge number', function() + local received = false + + server:on('test', function(data, client) + assert.equal(data, math.huge) + received = true + end) + + client:send('test', math.huge) + client:update() + server:update() + + assert.True(received) + end) + + it('a negative huge number', function() + local received = false + + server:on('test', function(data, client) + assert.equal(data, -math.huge) + received = true + end) + + client:send('test', -math.huge) + client:update() + server:update() + + assert.True(received) + end) + + it('a boolean', function() + local received = false + + server:on('test', function(data, client) + assert.equal(data, false) + received = true + end) + + client:send('test', false) + client:update() + server:update() + + assert.True(received) + end) + + it('nil', function() + local received = false + + server:on('test', function(data, client) + assert.equal(data, nil) + received = true + end) + + client:send('test', nil) + client:update() + server:update() + + assert.True(received) + end) + + it('a table', function() + local received = false + + server:on('test', function(data, client) + assert.are.same(data, { + a = 0.12, + b = -987345, + c = "test", + d = true, + e = {}, + }) + received = true + end) + + client:send('test', { + a = 0.12, + b = -987345, + c = "test", + d = true, + e = {}, + }) + client:update() + server:update() + + assert.True(received) + end) + + it('a table array', function() + local received = false + + server:on('test', function(data, client) + assert.are.same(data, { + 0.12, + -987345, + "test", + true, + {}, + }) + received = true + end) + + client:send('test', { + 0.12, + -987345, + "test", + true, + {}, + }) + client:update() + server:update() + + assert.True(received) + end) + end) +end) + +describe('the server', function() + insulate('can send', function() + before_each(function() + _G.client = sock.newClient("localhost", 22122) + _G.server = sock.newServer("0.0.0.0", 22122) + + client:connect() + + client:update() + server:update() + client:update() + server:update() + end) + + after_each(function() + server:destroy() + end) + + it('a string', function() + local received = false + + client:on('test', function(data, client) + assert.equal(data, 'this is the test string') + received = true + end) + + server:sendToAll('test', 'this is the test string') + server:update() + client:update() + server:update() + client:update() + + assert.True(received) + end) + + it('an integer', function() + local received = false + + client:on('test', function(data, client) + assert.equal(data, 12345678) + received = true + end) + + server:sendToAll('test', 12345678) + client:update() + server:update() + client:update() + server:update() + + assert.True(received) + end) + + it('a floating point number', function() + local received = false + + client:on('test', function(data, client) + assert.equal(data, 0.123456789) + received = true + end) + + server:sendToAll('test', 0.123456789) + server:update() + client:update() + server:update() + client:update() + + assert.True(received) + end) + + it('a huge number', function() + local received = false + + client:on('test', function(data, client) + assert.equal(data, math.huge) + received = true + end) + + server:sendToAll('test', math.huge) + server:update() + client:update() + server:update() + client:update() + + assert.True(received) + end) + + it('a negative huge number', function() + local received = false + + client:on('test', function(data, client) + assert.equal(data, -math.huge) + received = true + end) + + server:sendToAll('test', -math.huge) + server:update() + client:update() + server:update() + client:update() + + assert.True(received) + end) + + it('a boolean', function() + local received = false + + client:on('test', function(data, client) + assert.equal(data, false) + received = true + end) + + server:sendToAll('test', false) + server:update() + client:update() + server:update() + client:update() + + assert.True(received) + end) + + it('nil', function() + local received = false + + client:on('test', function(data, client) + assert.equal(data, nil) + received = true + end) + + server:sendToAll('test', nil) + server:update() + client:update() + server:update() + client:update() + + assert.True(received) + end) + + it('a table', function() + local received = false + + client:on('test', function(data, client) + assert.are.same(data, { + a = 0.12, + b = -987345, + c = "test", + d = true, + e = {}, + }) + received = true + end) + + server:sendToAll('test', { + a = 0.12, + b = -987345, + c = "test", + d = true, + e = {}, + }) + server:update() + client:update() + server:update() + client:update() + + assert.True(received) + end) + + it('a table array', function() + local received = false + + client:on('test', function(data, client) + assert.are.same(data, { + 0.12, + -987345, + "test", + true, + {}, + }) + received = true + end) + + server:sendToAll('test', { + 0.12, + -987345, + "test", + true, + {}, + }) + server:update() + client:update() + + assert.True(received) + end) + end) +end) diff --git a/main.lua b/main.lua index 25b575f..09660b3 100644 --- a/main.lua +++ b/main.lua @@ -2,6 +2,8 @@ class = require "lib/middleclass" wind = require "lib/windfield" stalker = require "lib/STALKER-X" +bitser = require "lib/bitser" +sock = require "lib/sock" SWORDLENGTH = 40; KNIFELENGTH = 10 SWORDWIDTH = 3 @@ -22,7 +24,7 @@ function love.load() camera = stalker() - mainmenu_load() + menu_load(makeMainMenu()) end @@ -55,13 +57,10 @@ function love.keyreleased (key) end --- MAIN-MENU +-- MENUS ---------------------------------------- -function mainmenu_load() - local mainMenu = makeMainMenu() - mainMenu:install() - - camera = stalker() +function menu_load(menu) + menu:install() end @@ -69,15 +68,36 @@ function makeMainMenu() return Menu:new(100, 100, 30, 50, 3, { {love.graphics.newText(a_ttf, "Local"), function () lobby_load(LocalLobby) end}, + {love.graphics.newText(a_ttf, "Net"), + function () menu_load(makeNetMenu()) end}, {love.graphics.newText(a_ttf, "Quit"), function () love.event.quit(0) end }}) end +function makeNetMenu() + return Menu:new(100, 100, 30, 50, 3, { + {love.graphics.newText(a_ttf, "Join"), + function () + local addressBox = + TextBox:new(100, 100, 3, 99, "127.0.0.1", "Address/Host: ", + function (text) lobby_load(ClientLobby, text) end) + addressBox:install() + end}, + {love.graphics.newText(a_ttf, "Host"), + function () lobby_load(HostLobby) end}, + {love.graphics.newText(a_ttf, "Back"), + function () menu_load(makeMainMenu()) end}}) +end + + +-- NET +---------------------------------------- + -- GAME LOBBY ---------------------------------------- -function lobby_load(lobbyClass) - lobby = lobbyClass:new() +function lobby_load(lobbyClass, arg) + lobby = lobbyClass:new(arg) lobby:install() end @@ -107,9 +127,6 @@ function Fighter:initialize(game, x, y, character, swordType, swordSide) self.directionals = {} self.deadPieces = {} - self.sprite = love.graphics.newImage("art/sprites/" - .. CHARACTERS[self.character]["file"]) - self:initBody(x, y) self:initSword() self:initShield() @@ -130,7 +147,8 @@ end function Fighter:draw() local x,y = self.body:getWorldPoints(self.body.shape:getPoints()) - love.graphics.draw(self.sprite, x, y, self.body:getAngle(), 1, 1) + love.graphics.draw(CHARACTERS[self.character], x, y, self.body:getAngle(), + 1, 1) end @@ -406,7 +424,7 @@ function Lobby:draw() local rowX = 10 local rowY = (25 * i) + 50 - love.graphics.draw(lobbiest.sprite, rowX, rowY, 0, 1, 1) + love.graphics.draw(CHARACTERS[lobbiest.character], rowX, rowY, 0, 1, 1) if (lobbiest.swordType == "normal") then love.graphics.rectangle("fill", rowX + 18, rowY + SWORDWIDTH, SWORDLENGTH, SWORDWIDTH) @@ -459,7 +477,7 @@ function Lobby:keypressed(key) self:newLocalLobbiest() else for i,lobbiest in pairs(self.localLobbiests) do - lobbiest:keypressed(key) + lobbiest:keypressed(key, self) end end end @@ -492,6 +510,16 @@ function Lobby:lobbiestsN() end +function Lobby:localLobbiestTables() + local lobbiests = {} + table.foreach(self.localLobbiests, + function (k, lobbiest) + table.insert(lobbiests, lobbiest:toTable()) + end) + return lobbiests +end + + -- LOCAL LOBBY ---------------------------------------- LocalLobby = class("LocalLobby", Lobby) @@ -510,20 +538,135 @@ function LocalLobby:keypressed(key) end +-- NET - HOST LOBBY +---------------------------------------- +HostLobby = class("HostLobby", Lobby) + +function HostLobby:initialize() + Lobby.initialize(self) + self.server = sock.newServer("*", 13371) + self.server:setSerialization(bitser.dumps, bitser.loads) + + self.server:on("connect", + function (data, client) + print("GOD IS DEAD BUT THERE'S A CONNECTION") + end) + + self.server:on("lobbiestsUpdate", + function (localLobbiests, client) + for i,v in pairs(localLobbiests) do + print(v["name"]) + end + self:updateLobbiests(localLobbiests, client) + end) +end + + +function HostLobby:update(dt) + self.server:update() +end + + +function HostLobby:sendLobbiests() +-- for i,v in pairs(localLobbiests) do + self.serverToAll:send("lobbiestsUpdate", self:localLobbiestTables()) +end + + +function HostLobby:newLocalLobbiest() + Lobby.newLocalLobbiest(self) + self:sendLobbiests() +end + + +function HostLobby:updateLobbiests(localLobbiests, client) + local newRemotes = {} + for k,lobbiest in pairs(self.remoteLobbiests) do + if (lobbiest.client == client) then + self.remoteLobbiestsN = self.remoteLobbiestsN - 1 + else + table.insert(newRemotes, lobbiest) + end + end + + for k,lobbiest in pairs(localLobbiests) do + table.insert(newRemotes, ClientLobbiest:new(self, client, lobbiest)) + self.remoteLobbiestsN = self.remoteLobbiestsN + 1 + end + + self.remoteLobbiests = newRemotes +end + + +-- NET - CLIENT LOBBY +---------------------------------------- +ClientLobby = class("ClientLobby", Lobby) + +function ClientLobby:initialize(address) + Lobby.initialize(self) + self.status = "Connecting..." + + self.client = sock.newClient(address, 13371) + self.client:setSerialization(bitser.dumps, bitser.loads) + + self.client:on("connect", + function (data) + self.status = "Connected!" + self.client:send("lobbiestsUpdate", self:localLobbiestTables()) + end) + + self.client:on("disconnect", + function (data) + self.status = "Disconnected" + end) + + self.client:on("lobbiestsUpdate", + function (lobbiests) + self.remoteLobbiests = lobbiests + end) + self.client:connect() +end + + +function ClientLobby:update(dt) + self.client:update() +end + + +function ClientLobby:draw() + Lobby.draw(self) + + love.graphics.draw(love.graphics.newText(self.ttf, self.status), + 200, 10, 0, self.scale) +end + + +function ClientLobby:updateLobbiests(localLobbiests) + + +function ClientLobby:newLocalLobbiest() + Lobby.newLocalLobbiest(self) + self.client:send("lobbiestsUpdate", self:localLobbiestTables()) +end + + -- LOBBIEST proposed fighter ---------------------------------------- Lobbiest = class("Lobbiest") function Lobbiest:initialize(lobby, name) - self.lobby = lobby self.name = name or NAMES[math.random(1, table.maxn(NAMES))] self.character = math.random(1, table.maxn(CHARACTERS)) - self.sprite = love.graphics.newImage("art/sprites/" - .. CHARACTERS[self.character]["file"]) self.swordType = "normal" end +function Lobbiest:toTable() + return {["name"] = self.name, ["character"] = self.character, + ["swordType"] = self.swordType} +end + + -- LOCAL LOBBIEST ---------------------------------------- LocalLobbiest = class("LocalLobbiest", Lobbiest) @@ -534,7 +677,7 @@ function LocalLobbiest:initialize(lobby, name, keymap) end -function LocalLobbiest:keypressed(key, playerNo) +function LocalLobbiest:keypressed(key, lobby) if (key == self.keymap["accel"]) then if (self.swordType == "normal") then self.swordType = "knife" elseif (self.swordType == "knife") then self.swordType = "normal" @@ -544,7 +687,7 @@ function LocalLobbiest:keypressed(key, playerNo) local textBox = TextBox:new(20, 400, 3, 10, self.name, "Name: ", function (text) self.name = text - self.lobby:install() + lobby:install() end) textBox:install(false, drawFunction, nil, false) @@ -555,16 +698,43 @@ function LocalLobbiest:keypressed(key, playerNo) else self.character = self.character + 1 end - self.sprite = love.graphics.newImage("art/sprites/" - .. CHARACTERS[self.character]["file"]) end end +function LocalLobbiest:toTable() + local tablo = Lobbiest.toTable(self) + tablo["keymap"] = self.keymap + return tablo +end + + +-- CLIENT LOBBIEST used by HostLobby +---------------------------------------- +ClientLobbiest = class("ClientLobbiest", Lobbiest) + +function ClientLobbiest:initialize(lobby, client, lobbiestTable) + self.client = client + + self.name = lobbiestTable["name"] + self.keymap = lobbiestTable["keymap"] + self.swordType = lobbiestTable["swordType"] + self.character = lobbiestTable["character"] +end + + +-- HOST LOBBIEST used by ClientLobby +---------------------------------------- +HostLobbiest = class("HostLobbiest", Lobbiest) + +function HostLobbiest:initialize(lobby) + Lobbiest.initialize(self, lobby) +end + + -- MENU used for creating menus (lol) ---------------------------------------- Menu = class("Menu") - function Menu:initialize(x, y, offset_x, offset_y, scale, menuItems) self.x,self.y = x,y self.offset_x,self.offset_y = offset_x,offset_y @@ -706,18 +876,11 @@ end -- UTIL -------------------------------------------------------------------------------- -function split(inputString, seperator) - local newString = {} - for stringBit in string.gmatch(inputString, "([^"..seperator.."]+)") do - table.insert(newString, stringBit) - end - return newString -end - - -- Install the important 'hook' functions (draw, update, keypressed/released) -- If any of the 'old' functions passed are not nil, then both the new and -- old will be added into the new corresponding hook function +-- This function is too god damn long and it makes me want to cry +-- Could be pretty easily shortened, now that I think about it function hookInstall(newUpdate, newDraw, newPress, newRelease, oldUpdate, oldDraw, oldPress, oldRelease) local ignored = 1 @@ -757,9 +920,12 @@ end -- CHARACTERS ------------------------------------------ CHARACTERS = {} -CHARACTERS[1] = {["file"] = "jellyfish-lion.png", ["name"] = "Lion Jellyfish", ["desc"] = "hey, hey. you know whats shocking?", ["author"] = "rapidpunches", ["license"] = "CC-BY-SA 4.0"} -CHARACTERS[2] = {["file"] = "jellyfish-n.png", "Jellyfish N", "(electricity)", "rapidpunches", "CC-BY-SA 4.0"} -CHARACTERS[3] = {["file"] = "shark-unicorn.png", "Shark-Unicorn", "A masterpiece", "My little bro", "CC-BY-SA 4.0"} +-- Lion Jellyfish by rapidpunches, CC-BY-SA 4.0 +CHARACTERS[1] = love.graphics.newImage("art/sprites/jellyfish-lion.png") +-- N Jellyfish by rapidpunches, CC-BY-SA 4.0 +CHARACTERS[2] = love.graphics.newImage("art/sprites/jellyfish-n.png") +-- Something Indecipherable by my little brother (<3<3), CC-BY-SA 4.0 +CHARACTERS[3] = love.graphics.newImage("art/sprites/shark-unicorn.png") -- DEFAULT NAMES ------------------------------------------