Skip to content
Stjepan Bakrac edited this page Apr 27, 2020 · 2 revisions

This page describes how packets are used by SE and how we process them in core and expose them to the addon environment, through the core mechanism as well as the packet service and the packets library. This article will discuss the various layers of packet management starting from our high level API and work its way down to the bare bones UDP layer near the end, since that is the priority which will concern most addon developers.

Note that the majority of this article (unless mentioned otherwise) deals with packet chunks, not proper UDP packets. They are the chunks of individual semantic data the game interprets, and several chunks can be part of a single UDP packet which gets transmitted between the server and client. Windower 4's API also referred to them as chunks and had corresponding events (e.g. incoming chunk). Their relationship, as well as how to access and use them will be mentioned near the end.

Also note that this article assumes you to be familiar (not necessarily proficient) with Lua syntax.

Derived libraries

The main way to interact with packets is to not. Instead most developers will use existing libraries which serve up data gathered from packets. For example, to use packets containing pet information the right approach is to consult the pet library, which does just that, gather packets related to pets, parse the information contained inside and present them to the user in an accessible fashion.

Obviously this won't work if you are interested in information that no library yet provides, or even want to write a new library which contains that information.

Packet library

If you know that the information you're after is not found in one of the existing libraries you need to get it from packets directly.

First you need to figure out if the packet is already known to our API or not. To do that, consult the types.lua file. If you find a packet containing the info you can already access it through our API. In that case, skip to the known packets section. Otherwise read the section about unknown packets below.

Unknown packets

If we don't have the packet in our API yet you need to figure out its ID and its structure. This tutorial will not cover that aspect, as there are various tools that help with that.

Once you figure out the packet and its structure add it to our API in the following format:

types[direction][id] = struct({
    field_1 = {pos_1, type_1},
    field_2 = {pos_2, type_2},
    --[[ ... ]]
    field_n = {pos_n, type_n},
})

In this case, the id is the integral ID of the packet you want to add, direction either 'incoming' or 'outgoing'. The field_x names are names by which you'll be able to access the packet's various fields. The pos_x variable indicates the byte position (0-indexed) of the field, and type_x indicates the type of the field. See the packets library for more information on that.

Known packets

After you have found or added the packet you can use it with the packets library. There are three mechanisms to use:

  1. packets[direction][id].last will retrieve the last received packet of the specified direction and id. You will be able to access all the fields defined in the types.lua file as table keys.

    Thie following example will print the last received chat message:

    print(packets.incoming[0x017].last.message)
  2. packets[direction][id]:register(fn) will register the function fn to run every time a packet of the specified direction and id is received. The sole argument passed to the function is the packet table.

    The following example will print the message of any chat message you receive:

    packets.incoming[0x017]:register(function(packet)
        print(packet.message)
    end)
  3. packets[direction]:register_init(fn_table) takes a table of ID -> function mappings and execute the respective functions every time a packet of the specified direction and IDs mentioned in the table is received. Additionally it will execute all provided functions in the table for the last of each received packet of the respective type, in order of reception.

    Assume that you have just zoned into Port Jeuno (ID 246). This sends a 0x00B packet when you zone out and a 0x00A packet when you zone back in. The following will print the IP of the last zone out packet and the zone ID of the last zone in packet (i.e. the current zone's IP and ID), and subsequently print those each time you zone from here on out:

    packets.incoming.register_init({
        [{0x00A}] = function(packet)
            print(packet.zone_id)
        end,
        [{0x00B}] = function(packet)
            print(packet.ip)
        end,

There are further mechanisms the library provides, see the respective page for more details, but these are the three basic mechanisms to work with packets. The last method (register_init) is mostly useful for services, but may also be used by regular addons.

Packet service

The packet service, like other services, will be running the entire time and gathering packet data. It does so by subscribing to the core packet events which sends a raw packet with only the necessary information parsed out to a Lua function.

It should never be interacted with directly, the packet library handles that and exposes all its information to the developer. This paragraph will just give an over of how it works and what it does, which will help with understanding the packet library.

The difference between the service and the library is that the service holds all the actual data. The library merely queries the service for it, or listens to events exposed by the service (such as incoming packet events).

The service starts with Windower and is meant to run at all times. For this reason services are meant to do as little work as possible, to minimize possible bugs and thus ensure a high availability. Simply restarting the service on an error will break many other libraries, because the service builds a cache of previously sent packets and other libraries rely on packets that have been sent in the past when they are loaded. For example, the account library relies on the last known 0x00A and 0x00B packets to determine whether or not the player is currently logged in. So on start it retrieves the last of those packets from the packet service and interprets them in order of their arrival. If the service unloads due to an error it loses that cache, and libraries that are loaded afterwards will be missing data.

Sometimes it's not even enough to cache just one packet of any ID. As an example, the equipment packet 0x050 is sent for each slot. If we only cached the last sent packet of that ID, we would only get one of the 16 possible equipment slots. Caching just 16 of them also wouldn't work, since we could miss out on certain slots if other slots change their equipment more often. For these cases we can define a field of the packet to disambiguate the cache for.

Take the 0x050 example mentioned above. This is what the field definition looks like for that packet:

-- Equipment
types.incoming[0x050] = struct({cache = {'slot_id'}}, {
    bag_index           = {0x00, uint8},
    slot_id             = {0x01, slot},
    bag_id              = {0x02, bag},
})

The first line defines a cache property, which refers to the slot_id field. That means that the service will cache all packets of that ID for each unique slot_id, i.e. for every equipment slot in this case.

The packets library takes this into account and treats packets with different cache values as separate. So while it's possible to register a function to any 0x050 packet like so:

packets.incoming[0x050]:register(fn)

You can also specify any cached fields, which act as sub-IDs:

-- This only reacts to equipment items for the legs slot
packets.incoming[0x050][0x7]:register(fn)

This works for all previously mentioned functions of the packets library, including .last:

local legs_packet = packets.incoming[0x050][0x7].last
print(legs_packet.bag_id, legs_packet.bag_index)

Core packet interface

All mechanisms discussed so far are based on the packets and packet_service packages, which are regular Lua packages. They are based on a mechanism provided by Windower core, which is an internal module called packet:

local packet = require('core.packet')

This is a straightforward mechanism provided by Windower directly. It only exposes incoming and outgoing packet events and passes a packet object to each registered function. This object contains header information (ID, size, sequence code), some meta-information (whether or not it was injected, blocked, etc.) as well as the raw data in the form of a binary string.

This binary string is parsed by the packet service to create packet objects based on the information in the types.lua file, which is then served up by the packets library.

The raw events are not intended to be used by most addons, unless there are special concerns, such as for performance. Otherwise using the packet library, or, if available, the dedicated special libraries.

UDP packets

UDP packets are the actual data packets which are exchanged between the server and the client. These are parsed into separate chunks, which form the "packets" discussed up until this point.

There are rarely reasons to use UDP packets, but if that is necessary, they are exposed through the same core packet mechanism as described above, in the udp sub-table:

local packet = require('packet')
local udp = require('udp')

This table provides the same functions and events as the packet chunk mechanism, only with a different packet structure.

Clone this wiki locally