Packets for dummies

Sometimes you need to share your knowledge. This is where you can do it. A place for POL and Script guides.
Post Reply
Polytropon
New User
Posts: 19
Joined: Wed Sep 15, 2010 9:47 pm

Packets for dummies

Post by Polytropon » Thu Nov 03, 2011 12:26 am

Although I´m no packet-wizard, I think my limited experience may help someone. I doubt the guide is perfect, so feel free to make corrections (or edit if you are a moderator). I tried not to give too many things as obvious, so this would be a good starter guide to the world of packets. If some paragraph is difficult to understand due to my non-native English grammar, please tell me so and I´ll correct it.


What is a packet?
A packet is a chunk of data, client/server exchanged. Essentially, a long hexadecimal number. All communication between server and clients is made by packets. Also, a packet is an object and as so, it has its own methods (see http://docs.polserver.com/pol098/objref.php#Packet)


There are three types of packets:
-Those sent by clients
-Those sent by the server
-Those that can be sent either by the client or the server

All packets start with an identifier (it´s "name") that determine what kind of packet it is. The list of packets is in the documentation: http://docs.polserver.com/packets/index.php. An example:

Code: Select all

Packet Name: Login Request
Last Modified: 2009-02-28 15:42:51
Modified By: MuadDib

Packet: 0x80
Sent By: Client
Size: 62 Bytes

Packet Build
BYTE[1] Command
BYTE[30] Account Name
BYTE[30] Password
BYTE[1] NextLoginKey value from uo.cfg on client machine.
What does it means? The packet is a long hex number, sized in bytes. 62 bytes, in this example. The packet build explains how the data is assorted. In this case, the packet starts with its identifier (Command) which size is 1 byte. The next 30 bytes are the account name, the next 30 the password, the last byte is "NextLoginKey". In that order! As there is no division between the data inside the packet (it´s just a long number, as I said before), you have to know in what position (in bytes) you want to search/modify whatever you want to mess with.

Remember: "BYTE" is the length of each piece of information. Then if you want to modify "Password", you have to look from the 31th byte, as the "Command" and "Account Name" are BYTE[1] + BYTE[30]. If tried to modify, for example, the 30 bytes of "Password" from byte 25, you would modify the last 5 bytes of "Account Name", resulting in a corrupted packet.


There are two types of packets, size-speaking: with fixed length and with variable length. The example is a fixed length packet (as the documentation says: "Size: 62 Bytes"). This means this kind of packet must always be 62 bytes (in this case, if a value like "Account Name" is shorter than the 30 bytes, its filled with zeroes [not 100% sure, please tell me if I´m wrong] until it reaches the correct length). The variable length packets have their length specified inside the packet (as far as I´ve noticed, the second byte is always the size of the packet).

In my experience, if you send an incorrect-sized packet to a client, there is a 99% chance you will crash it. Haven´t tested with the server, but I imagine it´s not good.


So, how do I handle packets?
There are to ways:
- You create a packet from scratch and send it via SendPacket() function or method.
- You intercept a packet before it reaches it´s destination (client or server) and modify/delete/resend it. The script you use to do this is called a "PacketHook"


Let´s go for the packethooks (packethook script prototypes are well explained in packethooks.txt, located in the root of the POL directory. For the sake of this tutorial, I will plagiarize a big part of that text)

A packethook is an script, which runs each time certain packet type is sent. The data of the packet becomes the script parameters. If the script returns 1 the packet never arrives its destination, if returns 0 the packet continues it´s way after the scripts finish running.

Usually (at least in my limited imagination), you use packethooks for two kind of stuff: to modify a packet (e.g. editing the players speech strings to change the word "Communist" for "Evil Reds" in you mccarthyistic shard -?- ) or to define certain action triggered by the packet (e.g. to reveal a hidden player when you walk over him).

Creating a packethook:
First, you create in any package a file named uopacket.cfg, with the following structure:

Code: Select all

Packet 0x2342 [color=red][Packet ID as it appears in the documentation][/color]
{
  Length (fixed integer length or 'variable' without quotes)
  [ReceiveFunction (scriptname)]
  [SendFunction    (scriptname)]
  [SubCommandOffset (integer)]
  [SubCommandLength (1, 2 or 4)]
  [Version (1 or 2)]
  [Client (client version string)]
}
ReceiveFunction: what function will run when the packet is received by the server
SendFunction: what function will run when the packet is sent by the server
Remember that some packets are sent by the server and the client alike, so you may use both lines for the same packet.

Version: sometimes newer client versions change the structure of certain packets, so a single hook can´t handle both packet versions (or sometimes you just need to hook the newer packets for compatibility issues). Version lets you define up to two packethook versions ("Version 1" or "Version 2"), each pointing to different Send/ReceiveFunctions

Client If you define a Version, you next want to discriminate what packets are to be hooked. This line allows to specify the minimum client version this packethook-version will affect (note, all pre-login packets before 6.0.5.0 do not know the client's version number in the core).The example below, is how you can hook 0xB9 that was changed from 3 bytes to 5 bytes in client 6.0.14.2. If you want a hook for the older version, just set Version to 1 as default Client is 1.25.25.0 and it will work on all previous client versions to your 6.0.14.2 hook.

Code: Select all

Packet 0xB9
{
  5
  SendFunction    scriptname:functionname
  Version 2
  Client 6.0.14.2
}
The SubCommand stuff has meaning for certain packets with "sub-packets" inside. Essentially, you can hook a "sub-packet" instead of the whole packet. As I never used them, I will shamelessly copy/paste a part of packets.txt:

SubCommandOffset is the 0-based offset into the packet that contains the
sub-command ID number, if applicable for this packet. SubCommandLength is the
number of bytes to extract to determine the sub command (ex. 2 for 0xBF, 1 for
0x12).

To define a SubPacket (an entry for hooking a sub-command of a packet), put this
in a packaged uopacket.cfg:

SubPacket (main packet ID byte)
{
SubCommandID (sub-command id)
[ReceiveFunction (scriptname:functionname)]
[SendFunction (scriptname:functionname)]
[Is6017 (0/1)]
}

SubCommandID is the 2-byte ID to be found at the parent packet's SubCommandOffset.
You need not define Receive and Send functions for the parent packet if you define
subpacket entries. If a subcommand is received or being sent that is not hooked,
the default behavior will occur.
As normal, the parent packet entry must only be defined once.
Please do not try to hook Sub-Sub-Commands (like the 0xBF 0x06 Party System
subsubcommands), instead use a case statement in the subcommand hook.




So, that´s with the uopacket.cfg. The next thing to do is to create an exported function (placed, of course, where you pointed to in the .cfg). That function starts like this:

exported function functionname( character, byref packet )

Character is either who sent or who received the packet. Note that the packet parameter is passed by reference (if it wasn´t, you would be modifying a copy and not the actual packet). Again remember: if the function returns 1, the packet is destroyed.

So, you have the packet and you want to take the data contained in it. You have to use the packet-object methods:

Code: Select all

.GetInt16(offset)
.GetInt32(offset)
.GetString(offset, largo)
etc...
Basically, the method .GetInt16(3) returns the following:
"a 16bits(2bytes)-long integer, offset-ed 3 bytes to the right from the packet start"

An example. Packet [0x09], SingleClick, sent by the client:

Code: Select all

Packet Name: Single Click
Last Modified: 2008-10-11 09:57:22
Modified By: MuadDib

Packet: 0x09
Sent By: Client
Size: 5 bytes

Packet Build
BYTE[1] cmd
BYTE[4] ID of single clicked object
In this example, the packethook needs to intercept the packet and get a objtref, so it needs the object serial that is the second chunk of data of the packet. The exported function:

Code: Select all

exported function singleClick(who, byref packet)
  var serial:=[b]packet.GetInt32(1);[/b]
  var objeto:=SystemFindObjectBySerial(serial);
  (do something...)
The script uses "GetInt" because the ID is an integer, "32" because the ID is 4 bytes long (4 bytes * 8 bits = 32) and "1" because the ID is offset-ed one byte to the right (as the first byte is occupied by "cmd", the packet identifier). Then, GetInt32(1). Just be careful if you use .GetString(offset, length): the offset is measured in bytes but the "length" parameter is measured in characters!


Another example, a -simplified- UNICODE text packet and what the methods return:

Code: Select all

Packet Build
BYTE[1] cmd
BYTE[2] length
BYTE[1] Type
BYTE[2] Color
BYTE[2] Font
BYTE[4] Language (Null Terminated)
BYTE[?] UNICODE Message (Null terminated)
packet.GetInt8(0) -- returns cmd
packet.GetInt16(1) -- returns lenght
packet.GetInt8(3) ---- returns Type
packet.GetInt16(4) --- returns Color
packet.GetInt16(6) ---- returns Font
packet.GetInt32(8) ---- returns Language

You may have noted that there is a "BYTE[?]" in the example. That is because the packet is variable-sized:
packet.GetUnicodeString(12, length_of_text) --------- returns Message

but we know the length of the packet, as it´s contained in the packet. So if I´m not wrong:

length_of_text := ( GetInt16(1) - 13 ) / 2;
GetInt16(1) : Int containing the packet size (in bytes)
13: the first 12 bytes + the last byte of the Unicode Message (as it´s "Null terminated", and you don´t want the null character)
2: The unicode character´s length is 2 bytes (as the docs say), that´s why we divide by two



By the way, there is a difference between Little Endian and Big Endian that´s not worth to explain here, but it´s a matter of notation. Let´s say that some packets types require you to use the "Flipped" version of the method to access certain data (e.g. "GetInt16Flipped()" instead of "GetInt16()" ). As far as I know, those situations are all noted in the packet documentation, but personally I don´t recall ever used them.


If you want to modify the packet, you use the "Set" methods, which operate the same way the "Get" methods (SetSize(), SetInt16(), SetString(), etc.). Then, to let a modified packet reach it´s destination (client or server) you go for the loved "return 0".
But maybe you like to return 1, and want to try this:

Code: Select all

packet.SendPacket()
The little problem is that if the packet is still the same type, it will be automatically hooked again and -if you didn´t make previsions about it- you will end in an infinite loop which will freeze the server.

All packethooks are "Run_To_Completion", as if you have gave them the Set_Critical(1) option. That means no other script will run until the packethook is done: any big loops will lag and anything that requires waiting will freeze the server completely, like a Sleep() or RequestInput(). Also, be careful when hooking packets that are sent a lot (like WalkRequest) as you may flood the server with Run_To_Completion scripts.


Homemade Packets
The SendPacket(who, packet) from uo.em allows you to send a packet to a client. The "packet" parameter is a stringed hexadecimal, containing the packet. For example, for the season packet:

Code: Select all

Packet Build
BYTE[1] 0xBC
BYTE[1] Season Flag
BYTE[1] Play Sound (0/1)

Notes
Season Flag:
0: Spring
1: Summer
2: Fall
3: Winter
4: Desolation
You send the parameters like this:

Code: Select all

SendPacket(who,"BC0001");   // spring
                  
SendPacket(who,"BC0401"); // desolation
                  
 SendPacket(who,"BC0201");    // fall
Note the structure of the packet, as we saw before:
BC: Packet ID
04: Season flag (in this case, "desolation")
01: Play Sound (in this case, "true")
That´s why: "BC0401"

Another more gentle way to create a packet is to use CreatePacket() from polsys.em. Then you modify the packet as you wish with it´s own methods, and use the .SendPacket() method to send it to a client. There is a hallucination script that someone posted that works this way, by sending "false" graphics packets to a certain client for an amount of time. You can also use .SendAreaPacket() to send the same packet to all players in an area.

The same season example as before, with CreatePacket(type, length).

Code: Select all

var paquete := CreatePacket(0xBC, 3); 
paquete.SetInt8(1, 4); // season flag: 4 - desolation
paquete.SetInt(2, 0) // don´t play sound
SendPacket(player, paquete);


Conclusions
Well, there aren´t any conclusions. But let´s remember the important stuff:
- The first byte is the packet ID, you search the documentation about packets based on it
- The length of the packet must be right or bad things will happen
- Packethooks must be efficient scripts or the shard will freeze, as all get Critical priority


Tips or things worth noting, in my limited experience:
- GetInt8() will get you one byte, GetInt16() two bytes, GetInt32() four bytes. As a packet is just a number, you can get a 3 byte long int by merging (not adding!) a GetInt16() and a GetInt8() (with the correct offset, of course).
- If you are going to do strange client-packets sending, as force-walk or redrawing of characters, note that some of this actions require sending several packets to avoid horrible out-of-syncs.
- If you hook speechrequest packets and prevent them from reaching the server (e.g. by placing the text with SayAbove() ), you will disable speechevents (you should let the packet reach the server and hook the response to avoid this). I asume that this will happen with other event-related packets.
- If you are curious about what packets are sent in certain condition and feel the documentation isn´t exhaustive, you can always get a packet-sniffer and do some packet-hunting. It´s like exploring the deep ocean.
Last edited by Polytropon on Sun Nov 06, 2011 7:42 am, edited 3 times in total.

Tomi
POL Developer
Posts: 475
Joined: Tue Feb 21, 2006 5:08 pm

Re: Packets for dummies

Post by Tomi » Sun Nov 06, 2011 6:02 am

just a small note about the uopacket.cfg packet definition

Code: Select all

Packet [Packet ID byte]
{
  Length (fixed integer length or 'variable' without quotes)
  [ReceiveFunction (scriptname)]
  [SendFunction    (scriptname)]
  [SubCommandOffset (integer)]
  [SubCommandLength (1, 2 or 4)]
  [Is6017 (0/1)]
}
was changed to this

Code: Select all

Packet (packet ID byte)
{
  Length (fixed integer length or 'variable' without quotes)
  [ReceiveFunction (scriptname:functionname)]
  [SendFunction    (scriptname:functionname)]
  [SubCommandOffset (integer)]
  [SubCommandLength (1, 2 or 4)]
  [Version (1 or 2)]
  [Client (client version string)]
}

this is from packethooks.txt that comes with cores

Code: Select all

Version is used to define multiple packethooks of the same packet type. This is due
to major packet changes by EA. It allows you to hook packets that have a different
packet structure based on the client version. Default is 1. Max right now is 2.

Client string is used to determine the minimum required client version for a
packethook to work with. Note, all pre-login packets before 6.0.5.0 do not know
the client's version number in the core.

The example below, is how you can hook 0xB9 that was changed from 3 bytes to 5
bytes in client 6.0.14.2. If you want a hook for the older version, just set
Version to 1 as default Client is 1.25.25.0 and it will work on all previous
client versions to your 6.0.14.2 hook.
Packet 0xB9
{
  5
  SendFunction    scriptname:functionname
  Version 2
  Client 6.0.14.2
}

Polytropon
New User
Posts: 19
Joined: Wed Sep 15, 2010 9:47 pm

Re: Packets for dummies

Post by Polytropon » Sun Nov 06, 2011 7:44 am

Thanks, updated.
I forgot that I live in a 098 world.

Post Reply