summaryrefslogtreecommitdiff
path: root/Data/BuiltIn/Libraries/lua-addons/addons/libs/packets.lua
diff options
context:
space:
mode:
authorchai <chaifix@163.com>2021-11-15 13:53:59 +0800
committerchai <chaifix@163.com>2021-11-15 13:53:59 +0800
commit942a030afd348ab2e02eac8054b43e3c3a72ea48 (patch)
treea13459f39a3d2f1b533fbd1b5ab523d7a621f673 /Data/BuiltIn/Libraries/lua-addons/addons/libs/packets.lua
parente307051a56a54c27f10438fd2025edf61d0dfeed (diff)
*rename
Diffstat (limited to 'Data/BuiltIn/Libraries/lua-addons/addons/libs/packets.lua')
-rw-r--r--Data/BuiltIn/Libraries/lua-addons/addons/libs/packets.lua456
1 files changed, 456 insertions, 0 deletions
diff --git a/Data/BuiltIn/Libraries/lua-addons/addons/libs/packets.lua b/Data/BuiltIn/Libraries/lua-addons/addons/libs/packets.lua
new file mode 100644
index 0000000..f3ab5b0
--- /dev/null
+++ b/Data/BuiltIn/Libraries/lua-addons/addons/libs/packets.lua
@@ -0,0 +1,456 @@
+--[[
+A library to facilitate packet usage
+]]
+
+_libs = _libs or {}
+
+require('lists')
+require('maths')
+require('strings')
+require('functions')
+require('pack')
+
+local table = require('table')
+
+local packets = {}
+
+_libs.packets = packets
+
+if not warning then
+ warning = print+{_addon.name and '%s warning:':format(_addon.name) or 'Warning:'}
+end
+
+__meta = __meta or {}
+__meta.Packet = {__tostring = function(packet)
+ local res = '%s packet 0x%.3X (%s):':format(packet._dir:capitalize(), packet._id, packet._name or 'Unrecognized packet')
+
+ local raw = packets.build(packet)
+ for field in packets.fields(packet._dir, packet._id, raw):it() do
+ res = '%s\n%s: %s':format(res, field.label, tostring(packet[field.label]))
+ if field.fn then
+ res = '%s (%s)':format(res, tostring(field.fn(packet[field.label], raw)))
+ end
+ end
+
+ return res
+end}
+
+--[[
+ Packet database. Feel free to correct/amend it wherever it's lacking.
+]]
+
+packets.data = require('packets/data')
+packets.raw_fields = require('packets/fields')
+
+--[[
+ Lengths for C data types.
+]]
+
+local sizes = {
+ ['unsigned char'] = 8,
+ ['unsigned short'] = 16,
+ ['unsigned int'] = 32,
+ ['unsigned long'] = 64,
+ ['signed char'] = 8,
+ ['signed short'] = 16,
+ ['signed int'] = 32,
+ ['signed long'] = 64,
+ ['char'] = 8,
+ ['short'] = 16,
+ ['int'] = 32,
+ ['long'] = 64,
+ ['bool'] = 8,
+ ['float'] = 32,
+ ['double'] = 64,
+ ['data'] = 8,
+ ['bit'] = 1,
+ ['boolbit'] = 1,
+}
+
+-- This defines whether to treat a type with brackets at the end as an array or something special
+local non_array_types = S{'bit', 'data', 'char'}
+
+-- Pattern to match variable size array
+local pointer_pattern = '(.+)%*'
+-- Pattern to match fixed size array
+local array_pattern = '(.+)%[(.+)%]'
+
+-- Function returns number of bytes, bits, items and type name
+local parse_type = function(field)
+ local ctype = field.ctype
+
+ if ctype:endswith('*') then
+ return nil, 1, ctype:match(pointer_pattern):trim()
+ end
+
+ local type, count_str = ctype:match(array_pattern)
+ type = (type or ctype):trim()
+
+ local array = not non_array_types:contains(type)
+ local count_num = count_str and count_str:number() or 1
+ local type_count = count_str and array and count_num or 1
+
+ local bits = (array and type_count or count_num) * sizes[type];
+
+ return bits, type_count, type
+end
+
+local size
+size = function(fields, count)
+ -- A single field
+ if fields.ctype then
+ local bits, _, type = parse_type(fields)
+ return bits or type == 'char' and 8 or count and count * sizes[type] or 0
+ end
+
+ -- A reference field
+ if fields.ref then
+ return size(fields.ref, count) * (fields.count == '*' and count or fields.count)
+ end
+
+ return fields:reduce(function(acc, field)
+ return acc + size(field, count)
+ end, 0)
+end
+
+local parse
+parse = function(fields, data, index, max, lookup, depth)
+ depth = depth or 0
+ max = max == '*' and 0 or max or 1
+ index = index or 32
+
+ local res = L{}
+ local count = 0
+ local length = 8 * #data
+ while index < length do
+ count = count + 1
+
+ local parsed = L{}
+ local parsed_index = index
+ for field in fields:it() do
+ if field.ctype then
+ -- A regular type field
+ field = table.copy(field)
+ local bits, type_count, type = parse_type(field)
+
+ if not non_array_types:contains(type) and (not bits or type_count > 1) then
+ -- An array field with more than one entry, reparse recursively
+ field.ctype = type
+ local ext, new_index = parse(L{field}, data, parsed_index, not bits and '*' or type_count, nil, depth + 1)
+ parsed = parsed + ext
+ parsed_index = new_index
+ else
+ -- A non-array field or an array field with one entry
+ if max ~= 1 then
+ -- Append indices to labels
+ if lookup then
+ -- Look up index name in provided table
+ local resource = lookup[1][count + lookup[2] - 1]
+ field.label = '%s %s':format(resource and resource.name or 'Unknown %d':format(count + lookup[2] - 1), field.label)
+ else
+ -- Just increment numerically
+ field.label = '%s %d':format(field.label, count)
+ end
+ end
+
+ if parsed_index % 8 ~= 0 and type ~= 'bit' and type ~= 'boolbit' then
+ -- Adjust to byte boundary, if non-bit type
+ parsed_index = 8 * (parsed_index / 8):ceil()
+ end
+
+ if not bits then
+ -- Determine length for pointer types (*)
+ type_count = ((length - parsed_index) / sizes[type]):floor()
+ bits = sizes[type] * type_count
+
+ field.ctype = '%s[%u]':format(type, type_count)
+
+ count = max
+ end
+
+ field.type = type
+ field.index = parsed_index
+ field.length = bits
+ field.count = type_count
+
+ parsed:append(field)
+ parsed_index = parsed_index + bits
+ end
+ else
+ -- A reference field, call the parser recursively
+ local type_count = field.count
+ if not type_count then
+ -- If reference count not explicitly given it must be contained in the packet data
+ type_count = data:byte(field.count_ref + 1)
+ end
+
+ local ext, new_index = parse(field.ref, data, parsed_index, type_count, field.lookup, depth + 1)
+ parsed = parsed + ext
+ parsed_index = new_index
+ end
+ end
+
+ if parsed_index <= length then
+ -- Only add parsed chunk, if within length boundary
+ res = res + parsed
+ index = parsed_index
+ else
+ count = max
+ end
+
+ if count == max then
+ break
+ end
+ end
+
+ return res, index
+end
+
+-- Arguments are:
+-- dir 'incoming' or 'outgoing'
+-- id Packet ID
+-- data Binary packet data, nil if creating a blank packet
+-- ... Any parameters taken by a packet constructor function
+-- If a packet has a variable length field (e.g. char* or ref with count='*') the last value in here must be the count of that field
+function packets.fields(dir, id, data, ...)
+ local fields = packets.raw_fields[dir][id]
+
+ if type(fields) == 'function' then
+ fields = fields(data, ...)
+ end
+
+ if not fields then
+ return nil
+ end
+
+ if not data then
+ local argcount = select('#', ...)
+ local bits = size(fields, argcount > 0 and select(argcount, ...) or nil)
+ data = 0:char():rep(4 + 4 * ((bits or 0) / 32):ceil())
+ end
+
+ return parse(fields, data)
+end
+
+local dummy = {name='Unknown', description='No data available.'}
+
+-- Type identifiers as declared in lpack.c
+-- Windower uses an adjusted set of identifiers
+-- This is marked where applicable
+local pack_ids = {}
+pack_ids['bit'] = 'b' -- Windower exclusive
+pack_ids['boolbit'] = 'q' -- Windower exclusive
+pack_ids['bool'] = 'B' -- Windower exclusive
+pack_ids['unsigned char'] = 'C' -- Originally 'b', replaced by 'bit' for Windower
+pack_ids['unsigned short'] = 'H'
+pack_ids['unsigned int'] = 'I'
+pack_ids['unsigned long'] = 'L'
+pack_ids['signed char'] = 'c'
+pack_ids['signed short'] = 'h'
+pack_ids['signed int'] = 'i'
+pack_ids['signed long'] = 'L'
+pack_ids['char'] = 'c'
+pack_ids['short'] = 'h'
+pack_ids['int'] = 'i'
+pack_ids['long'] = 'l'
+pack_ids['float'] = 'f'
+pack_ids['double'] = 'd'
+pack_ids['data'] = 'A'
+
+local make_pack_string = function(field)
+ local ctype = field.ctype
+
+ if pack_ids[ctype] then
+ return pack_ids[ctype]
+ end
+
+ local type_name, number = ctype:match(array_pattern)
+ if type_name then
+ number = tonumber(number)
+ local pack_id = pack_ids[type_name]
+ if pack_id then
+ if type_name == 'char' then
+ return 'S' .. number -- Windower exclusive
+ else
+ return pack_id .. number
+ end
+ end
+ end
+
+ type_name = ctype:match(pointer_pattern)
+ if type_name then
+ local pack_id = pack_ids[type_name]
+ if pack_id then
+ if type_name == 'char' then
+ return 'z'
+ else
+ return pack_id .. '*'
+ end
+ end
+ end
+
+ return nil
+end
+
+-- Constructor for packets (both injected and parsed).
+-- If data is a string it parses an existing packet, otherwise it will create
+-- a new packet table for injection. In that case, data can ba an optional
+-- table containing values to initialize the packet to.
+--
+-- Example usage
+-- Injection:
+-- local packet = packets.new('outgoing', 0x050, {
+-- ['Inventory Index'] = 27, -- 27th item in the inventory
+-- ['Equipment Slot'] = 15 -- 15th slot, left ring
+-- })
+-- packets.inject(packet)
+--
+-- Injection (Alternative):
+-- local packet = packets.new('outgoing', 0x050)
+-- packet['Inventory Index'] = 27 -- 27th item in the inventory
+-- packet['Equipment Slot'] = 15 -- 15th slot, left ring
+-- packets.inject(packet)
+--
+-- Parsing:
+-- windower.register_event('outgoing chunk', function(id, data)
+-- if id == 0x0B6 then -- outgoing /tell
+-- local packet = packets.parse('outgoing', data)
+-- print(packet['Target Name'], packet['Message'])
+-- end
+-- end)
+function packets.parse(dir, data)
+ local rem = #data % 4
+ if rem ~= 0 then
+ data = data .. 0:char():rep(4 - rem)
+ end
+
+ local res = setmetatable({}, __meta.Packet)
+ res._id, res._size, res._sequence = data:unpack('b9b7H')
+ res._size = res._size * 4
+ res._raw = data
+ res._dir = dir
+ res._name = packets.data[dir][res._id].name
+ res._description = packets.data[dir][res._id].description
+ res._data = data:sub(5)
+
+ local fields = packets.fields(dir, res._id, data)
+ if not fields or #fields == 0 then
+ return res
+ end
+
+ local pack_str = fields:map(make_pack_string):concat()
+
+ for key, val in ipairs({res._data:unpack(pack_str)}) do
+ local field = fields[key]
+ if field then
+ res[field.label] = field.enc and val:decode(field.enc) or val
+ end
+ end
+
+ return res
+end
+
+function packets.new(dir, id, values, ...)
+ values = values or {}
+
+ local packet = setmetatable({}, __meta.Packet)
+ packet._id = id
+ packet._dir = dir
+ packet._sequence = 0
+ packet._args = {...}
+
+ local fields = packets.fields(packet._dir, packet._id, nil, ...)
+ if not fields then
+ warning('Packet 0x%.3X not recognized.':format(id))
+ return packet
+ end
+
+ for field in fields:it() do
+ packet[field.label] = values[field.label]
+
+ -- Data not set
+ if not packet[field.label] then
+ if field.const then
+ packet[field.label] = field.const
+
+ elseif field.ctype == 'bool' or field.ctype == 'boolbit' then
+ packet[field.label] = false
+
+ elseif sizes[field.ctype] or field.ctype:startswith('bit') then
+ packet[field.label] = 0
+
+ elseif field.ctype:startswith('char') or field.ctype:startswith('data') then
+ packet[field.label] = ''
+
+ else
+ warning('Bad packet! Unknown packet C type:', field.ctype)
+ packet._error = true
+
+ end
+ end
+ end
+
+ return packet
+end
+
+local lookup = function(packet, field)
+ local val = packet[field.label]
+ return field.enc and val:encode(field.enc) or val
+end
+
+-- Returns binary data from a packet
+function packets.build(packet)
+ local fields = packets.fields(packet._dir, packet._id, packet._raw, unpack(packet._args or {}))
+ if not fields then
+ error('Packet 0x%.3X not recognized, unable to build.':format(packet._id))
+ return nil
+ end
+
+ local pack_string = fields:map(make_pack_string):concat()
+ local data = pack_string:pack(fields:map(lookup+{packet}):unpack())
+ local rem = #data % 4
+ if rem ~= 0 then
+ data = data .. 0:char():rep(4 - rem)
+ end
+
+ return 'b9b7H':pack(packet._id, 1 + #data / 4, packet._sequence) .. data
+end
+
+-- Injects a packet built with packets.new
+function packets.inject(packet)
+ if packet._error then
+ error('Bad packet, cannot inject')
+ return nil
+ end
+
+ local fields = packets.fields(packet._dir, packet._id, packet._raw)
+ if not fields then
+ error('Packet 0x%.3X not recognized, unable to send.':format(packet._id))
+ return nil
+ end
+
+ packet._raw = packets.build(packet)
+
+ if packet._dir == 'incoming' then
+ windower.packets.inject_incoming(packet._id, packet._raw)
+ elseif packet._dir == 'outgoing' then
+ windower.packets.inject_outgoing(packet._id, packet._raw)
+ else
+ error('Error sending packet, no direction specified. Please specify \'incoming\' or \'outgoing\'.')
+ end
+end
+
+return packets
+
+--[[
+Copyright © 2013-2015, Windower
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+ * Neither the name of Windower nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Windower BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+]]