• There is NO official Otland's Discord server and NO official Otland's server list. The Otland's Staff does not manage any Discord server or server list. Moderators or administrator of any Discord server or server lists have NO connection to the Otland's Staff. Do not get scammed!

Tibia 14 Packet Structure

jo3bingham

Excellent OT User
Joined
Mar 3, 2008
Messages
1,108
Solutions
14
Reaction score
798
GitHub
jo3bingham
I've been contacted by multiple people asking me to look into the packet structure changes that came with the recent Tibia 14 update. As @Marcosvf132 was the first person to ask, and also came to the same conclusion that I did, I want to make sure they get credited as well.

The first thing to point out is the changes to the client Login packet. The 2 bytes to indicate the DatRevision has been changed to a string that is the SHA256 hash of the assets.json file:
Replace:
C#:
DatRevision = message.ReadUInt16();
With:
C#:
var assetsHash = message.ReadString();
This means that the position of the RSA-encrypted block has changed, and it's best to change the following code:
From:
C#:
var rsaStartIndex = _client.VersionNumber >= 124010030 ? 31 : 18;
To:
C#:
_clientInMessage.Seek(16, SeekOrigin.Begin); // Skip to the string values
_clientInMessage.ReadString(); // Client version
_clientInMessage.ReadString(); // Assets.json SHA256 hash
_clientInMessage.Seek(1, SeekOrigin.Current); // Skip ClientPreviewState value
var rsaStartIndex = (int)_clientInMessage.Position;

The previous packet structure was as such:
2 bytes to indicate how many more bytes are in the packet, 4 bytes to indicate the sequence number and if the packet is compressed, then X bytes that are (potentially) compressed and XTEA encrypted. Once XTEA decrypted, the first 2 bytes of the decrypted data indicate how many bytes are remaining. If compressed, these remaining bytes then need to be decompressed via zlib inflate. What's remaining is the raw packet data to be parsed.

The new structure is as such:
2 bytes to indicate how many 8-byte chunks make up the packet data, 4 bytes to indicate the sequence number and if the packet is compressed, then X (value of the first 2 bytes of the packet multiplied by 8) bytes that are (potentially) compressed and XTEA encrypted. Once XTEA decrypted, the first byte of the decrypted data indicates how many padding bytes are at the end (can be between 0-7 bytes). If compressed, the remaining bytes (minus how many padding bytes there are) then need to be decompressed viz zlib inflate. What's remaining is the raw packet data to be parsed.

Here's a couple of examples.
1) First packet (LoginChallenge) from the server after the client sends the server name:
Code:
01 00 00 00 00 00 01 1F FC 1D 00 00 A1 71
01 00 - Numer of 8-byte chunks to make up the packet data. In this case, it is 1.
00 00 00 00 - 4 bytes to indicate the sequence number and if the packet is compressed. In this case, the sequence number is 0 and the packet is not compressed.
01 1F FC 1D 00 00 A1 71 - 8-byte chunks as indicated by the first 2 bytes. In this case, it is just 1 8-byte chunk.
01 - Number of padding bytes at the end of the data. In this case, there is just 1 padding byte at the end.
1F FC 1D 00 00 A1 - Packet data without the padding bytes.
1F - Packet type. In this case, the LoginChallenge packet.
FC 1D 00 00 - First 4 bytes of the LoginChallenge packet is a timestamp value.
A1 - 5th byte of the LoginChallenge packet is a random value.
71 - The padding bytes that are essentially junk bytes.

2) A MoveCreature packet from the server:
Code:
02 00 08 00 00 C0 82 F7 34 53 91 EA 24 A1 CB 78 C7 96 AA D9 33 8A
02 00 - Number of 8-byte chunks to make up the packet data. In this case, it is 16 (2 * 8).
08 00 00 C0 - 4 bytes to indicate the sequence number and if the packet is compressed. In this case, the sequence number is 8 and the presence of C0 in the first byte (since Tibia packets use little-endian byte sequences) indicates it is compressed.
82 F7 34 53 91 EA 24 A1 CB 78 C7 96 AA D9 33 8A - The data to be XTEA decrypted.
The packet data after being XTEA decrypted:
Code:
07 CA 05 93 8C 60 11 00 00 8E 39 E0 10 34 72 AA
07 - Number of padding bytes at the end of the data.
CA 05 93 8C 60 11 00 00 - The data to be decompressed.
8E 39 E0 10 34 72 AA - The padding/junk bytes.
The compressed data after being decompressed:
Code:
6D 71 7E ED 7D 07 01 72 7E ED 7D 07
6D - Packet type. In this case, the MoveCreature packet.
71 7E ED 7D 07 - Position (xyz) of the creature to move.
01 - Stackposition of the creature to move.
72 7E ED 7D 07 - Position (xyz) to move the creature to.
 
Last edited:
Not exactly packet structure related, but the latest update added additional account security via device cookies. When you log in to the client, the JSON data the client sends to the server contains a new "devicecookie" field:
JSON:
{
    "devicecookie":"",
    "email":"***********",
    "password":"***********",
    "stayloggedin":true,
    "type":"login"
}
The first time you log in to an account, the "devicecookie" field will be empty.
When the server responds with the session JSON data, it also contains a "devicecookie" field:
JSON:
{
"session":{...},
"playdata":{
    "worlds":[...],
    "characters":[...]
},
"devicecookie":"Vo2mIE8PvMSBKFd9BrrkT\/DwtswdJasN,ypY4LHVlr3qa0ip3PksiTg==,NEeEh4ENUoRphnkk6To3+Q==",
"loginemail":"****"
}
The "devicecookie" field is three, base64-encoded values separated with a comma; decoding them doesn't output anything interesting (that I can see).

When you close the client, this "devicecookie" and a hash (associated with that account) is saved in clientoptions.json.
JSON:
"deviceCookieOptions": [
    {
        "devicecookie": "hexHd6orwe54QJgQhmDV6VyBJHTgqA==,OUkYJV32ncpVyg5+eQX8Yw==,qi2+CxXmEjdONJATaz02jQ==",
        "hash": "********"
    },
    {
        "devicecookie": "Vo2mIE8PvMSBKFd9BrrkT/DwtswdJasN,ypY4LHVlr3qa0ip3PksiTg==,NEeEh4ENUoRphnkk6To3+Q==",
        "hash": "********"
    }
],

The next time you log in to an account that a previous "devicecookie" has been saved for, that "devicecookie" is sent to the server:
JSON:
{
"devicecookie":"hexHd6orwe54QJgQhmDV6VyBJHTgqA==,OUkYJV32ncpVyg5+eQX8Yw==,qi2+CxXmEjdONJATaz02jQ==",
"email":"***********",
"password":"***********",
"stayloggedin":true,
"type":"login"
}
The server then sends a new "devicecookie" back in the session data.
 
Does anyone have any idea why the client crashes every time I try to connect using the 14.12 protocol?

Diff:
2025-03-17, 07:47:07: Warning: Setting a new default format with a different version or profile after the global shared context is created may cause issues with context sharing.
2025-03-17, 07:47:07: Using spritesheet cache size of 910
2025-03-17, 07:47:08: Asset loading complete
2025-03-17, 07:47:09: QObject: Cannot create children for a parent that is in a different thread.
(Parent is QQmlEngine(0x1c2684d0b40), parent's thread is QThread(0x1c25bace1e0), current thread is QSGRenderThread(0x1c266939ac0)
2025-03-17, 07:47:22: News query failed: "Unrecognized event news."
2025-03-17, 07:47:23: Request connection to gameserver  "tcp://127.0.0.1:7172"  (unprotected:  "tcp://127.0.0.1:7172" )  requested (Charakter "Tester" )
2025-03-17, 07:47:23: Request connection to gameserver  "tcp://127.0.0.1:7172" "Test"
2025-03-17, 07:47:23: Connected to gameserver  "tcp://127.0.0.1:7172" "Test"
2025-03-17, 07:47:23: News query failed: "Unrecognized event news."
2025-03-17, 07:47:23: Error while processing network packet unhandled enum value
2025-03-17, 07:47:23:
 
Back
Top