• 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!

Lets learn how attributes are stored!

Mazen

Developer
Joined
Aug 20, 2007
Messages
612
Reaction score
38
Location
Sweden
A few months ago, or a year ago I asked Elf to explain how item attributes were stored in the database. As you know, what's inside player_items.attributes is binary, and is not as easy to read as normal values in the database. So Elf answered me:

Learn binary.
How rude of him, not even an explanation?
The reason why I want to know is because I want to improve the communication with the OT database outside the game. That would give me the opportunity to control my server from the outside, like from a website. And btw, something like this should be documented, or at least have a small description or something.

Today, this came into my mind a second time. Now because I couldn't find this kind of information anywhere. I started taking a look at the source code myself, and I studied how attributes were stored. But I could not fully decode the whole piece of information. I'm stuck. And I'll show you how far I've come.

There are two ways for the server to store attributes. The first way is only for stackable items. They have only one attribute, and that's their "count". The binary information for this can look like the following example:

Code:
0x0F05

If we split the binary value, we will have two bytes:

0F and 05. 0F(15 as decimal) is the type of the attribute, and 05 is the value. That's the attribute you will have to for example 5 apples in a stack.

5apples.th.png


The second type of binary information is used in case there are many attributes for the same item. It can look something like this:
Code:
0x800200030061696402480d0000030075696402a90c0000

To explain further, this is an item with the actionid 3400 and the uniqueid 3241. That's two attributes, uniqueid and actionid. And both have a value. I've split the "map" to make it more clear:
Code:
1. 2.   3.                   4.
80 0200 030061696402480d0000 030075696402a90c0000

This is what I know about the values:
1. The type of a attribute "map", 0x80.
2. Numbers of attributes, in this case 0x0002, or just 2.
3. Information for the actionid.
4. Information for the uniqueid.

Now let's split each attribute:
Code:
5.     6.     7.     8.     9.
0300   6169   6402   480d   0000   (Actionid: 0d48, 3400)
0300   7569   6402   a90c   0000   (Uniqueid: 0ca9, 3241)

5. unknown
6. unknown
7. unknown
8. Attribute value.
9. unknown

You see that 4 values are for me unknown, that's why I'm stuck. I want to know what they are for. So guys, I'm asking you for help to solve this with me. If we do, I'll write a tutorial about this, or at least something better than this thread. And maybe in the future someone decides to make use of this.


So what do you think?
 
Last edited:
Attribute Types (taken from the source code):
Code:
	ATTR_END = 0,
	ATTR_DESCRIPTION = 1,
	ATTR_EXT_FILE = 2,
	ATTR_TILE_FLAGS = 3,
	ATTR_ACTION_ID = 4,
	ATTR_UNIQUE_ID = 5,
	ATTR_TEXT = 6,
	ATTR_DESC = 7,
	ATTR_TELE_DEST = 8,
	ATTR_ITEM = 9,
	ATTR_DEPOT_ID = 10,
	ATTR_EXT_SPAWN_FILE = 11,
	ATTR_RUNE_CHARGES = 12,
	ATTR_EXT_HOUSE_FILE = 13,
	ATTR_HOUSEDOORID = 14,
	ATTR_COUNT = 15,
	ATTR_DURATION = 16,
	ATTR_DECAYING_STATE = 17,
	ATTR_WRITTENDATE = 18,
	ATTR_WRITTENBY = 19,
	ATTR_SLEEPERGUID = 20,
	ATTR_SLEEPSTART = 21,
	ATTR_CHARGES = 22,
	ATTR_CONTAINER_ITEMS = 23,
	ATTR_NAME = 30,
	ATTR_PLURALNAME = 31,
	ATTR_ATTACK = 33,
	ATTR_EXTRAATTACK = 34,
	ATTR_DEFENSE = 35,
	ATTR_EXTRADEFENSE = 36,
	ATTR_ARMOR = 37,
	ATTR_ATTACKSPEED = 38,
	ATTR_HITCHANCE = 39,
	ATTR_SHOOTRANGE = 40,
	ATTR_ARTICLE = 41,
	ATTR_SCRIPTPROTECTED = 42,
	ATTR_DUALWIELD = 43,
	ATTR_ATTRIBUTE_MAP = 128
 
Great work Mazen. I agree there should be some documentation on TFS core :) they would see a lot more contribution by the community if they did.

I will look into it as well.
 
Last edited:
How rude of him, not even an explanation?
Meh, you'll get used to it after some time. He doesn't likes to waste time writing more words than enough. Also, consider yourself lucky he didn't make a reply involving your brain or the logout button.
 
Thanks for the response guys. I'm glad you agree with me about this.
 
Here:

80 0200 030061696402480d0000 030075696402a90c0000

Lets go clean it to make it more easy to understand:

80 02 00 03 00 61 69 64 02 48 0d 00 00 03 00 75 69 64 02 a9 0c 00 00

Now, what this numbers says:

{header of the message}
80 = is an byte that represents the decimal number 128, the constant ATTR_ATTRIBUTE_MAP on server source, say that message start here, .. i belive that it ever will be 80 in first byte of all messages
02 00 = is an double value that represents the decimal number 2, that say the count of attributes

{attributes}

{first}
03 00 = is an double value that represents the decimal number 3, it is the string lenght of the first attribute name, it is necessary to read the next bytes sequence
61 69 64 = are three bytes that are a ASCII representation of the attribute name, in this case, is a string "aid", our action id
02 = is an byte that represents the decimal number 2, say the data type of the attribute value, in this case, an integer (see obs more below), it is necessary to read the next bytes
48 0d 00 00 = is an integer value that represents the decimal number 3400, our aid value


{second}
03 00 = is an double value that represents the decimal number 3, it is the string lenght of the first attribute name, it is necessary to read the next bytes sequence
75 69 64 = are three bytes that are a ASCII representation of the attribute name, in this case, is a string "uid"
02 = is an byte that represents the decimal number 2, say the data type of the attribute value, in this case, an integer (see obs more below), it is necessary to read the next bytes
a9 0c 00 00 = is an integer value that represents the decimal number 3241, our uid value

Obs:

When you will read the value of attribute after read the attribute string length and the attribute string, this byte represents an constant of the data type, and ever will be one of the following code:

enum Type
{
NONE = 0,
STRING = 1,
INTEGER = 2,
FLOAT = 3,
BOOLEAN = 4
}

The correct mode to read the data will depend by the type that this data are, for integers (as in your exemple of action and unique ids) the value ever will be next 4 bytes that represents, if the value type are a string we need another way to read the data (in this case will be like we made to read the attribute name, reading the string length first)... To not make notthing wrong, ever check the sources.

Is that... Hope that I can help you and some people to understand the item attributes storage.
And, sorry for my very poor English...
 
ooh nice posts i always wondered how they did all that with binary. so what is the last "0000" for?
 
SlashX, thank you VERY much. Yes, that's exactly what I was looking for. So I guess the mystery is solved then lol.
I'll see how to make use of this.

ooh nice posts i always wondered how they did all that with binary. so what is the last "0000" for?
That's a part of the value, as SlashX explained.
 
Last edited:
Here is a script in PHP that reads the attributes. ;)

But since i don't know how floats look like, they are not supported by the script.

PHP:
	/* Made by Mazen */

	class Opentibia_Item_Attributes {
		private $value = array();
		
		const NUM_UNKNOWN = 0;
		const NUM_TINY = 1;
		const NUM_SHORT = 2;
		const NUM_INT = 4;
		const NUM_DOUBLE = 8;
		
		public function __construct($attributes_bin) {
			$match = array();
			
			if (preg_match("#0x([0-9a-fA-F]*)#", $attributes_bin, $match)) {
				$bin_value = $match[1];
				
				if (strlen($bin_value) == 4) {
					$bin_attribute = array(
						"type" => substr($bin_value, 0, 2),
						"value" => substr($bin_value, 2, 2)
					);
					
					if ($bin_attribute["type"] == "0F") {
						$this->value["count"] = hexdec($bin_attribute["value"]);
					}
				} elseif (strlen($bin_value) >= 4) {
					$header = array(
						"type" => substr($bin_value, 0, 2),
						"attributes_count" => substr($bin_value, 2, 4)
					);
					
					if ($header["type"] == "80") {
						$attributes = substr($bin_value, 6);

						$format = array(
							array(self::NUM_SHORT, "name_len", "", false), 
							array(self::NUM_UNKNOWN, "name", "", false), 
							array(self::NUM_TINY, "value_format", "", true), 
							array(self::NUM_UNKNOWN, "value", "", false)
						);
						
						$tmp_position = 0;
						$tmp_attribute = $format;
						$tmp_bytes_left = $tmp_attribute[0][0];
						
						foreach ($this->_split_bytes($attributes) as $i => $byte) {
							$tmp_bytes_left--;
							
							if ($tmp_bytes_left >= 0) {
								$tmp_attribute[$tmp_position][2] .= $byte;
								
								if (array_key_exists($tmp_position+1, $tmp_attribute) && $tmp_attribute[$tmp_position+1][0] == self::NUM_UNKNOWN) {
									
									if ($tmp_bytes_left == 0) {
										if ($tmp_attribute[$tmp_position][3]) {
											$types = array(
												0 => "empty", 
												1 => "string", 
												2 => self::NUM_INT,
												3 => "float",
												4 => self::NUM_TINY,
											);
											
											$value = $types[(int)$tmp_attribute[$tmp_position][2]];
											if (is_numeric($value)) {
												$tmp_attribute[$tmp_position+1][0] = $value;
											} else {
												switch ($value) {
													case "string": {
														$tmp_attribute[$tmp_position+1] = array(self::NUM_INT, "value_len", "", false);
														$tmp_attribute[$tmp_position+2] = array(self::NUM_UNKNOWN, "value", "", false);
														
														break;
													}

													case "float": {
														// Floats are yet not supperted.
														break;
													}
													
													case "empty": {
														array_pop($tmp_attribute);
														break;
													}
												}
											}
										} else {
											if ($tmp_bytes_left == 0) {
												$tmp_attribute[$tmp_position+1][0] = (int)$this->_arrange_bytes($tmp_attribute[$tmp_position][2]);
											}
										}
									}
								}
							}
							
							if ($tmp_bytes_left <= 0) {
								$tmp_position++;
								
								if (array_key_exists($tmp_position, $tmp_attribute) && $tmp_attribute[$tmp_position][0] > 0) {
									$tmp_bytes_left = $tmp_attribute[$tmp_position][0];
								} else {
									if (array_key_exists(3, $tmp_attribute)) {
										
										if ($tmp_attribute[3][1] == "value") {
											$this->value[$this->_bytes_to_string($tmp_attribute[1][2])] = hexdec("0x" . $this->_arrange_bytes($tmp_attribute[3][2]));
										} elseif ($tmp_attribute[3][1] == "value_len") {
											$this->value[$this->_bytes_to_string($tmp_attribute[1][2])] = $this->_bytes_to_string($tmp_attribute[4][2]);
										}
									}
									
									
									
									$tmp_position = 0;
									$tmp_attribute = $format;
									$tmp_bytes_left = $tmp_attribute[0][0];
								}
							}
						}
					}
				}
			}
		}
		
		public function _split_bytes($hex) {
			$bytes = array();
			
			if (!($hex % 1)) {
				
				for ($char = 0; $char < strlen($hex); $char = $char + 2) {
					$bytes[] = substr($hex, $char, 2);
				}
			}
			
			return $bytes;
		}
		
		public function _arrange_bytes($hex) {
			return implode("", array_reverse($this->_split_bytes($hex)));
		}
		
		public function _bytes_to_string($hex) {
			$string = "";
			
			foreach ($this->_split_bytes($hex) as $byte) {
				$string .= chr("0x" . $byte);
			}
			
			return $string;
		}
		
		public function getAttributes() {
			return $this->value;
		}
	}

Example:
PHP:
	$test = new Opentibia_Item_Attributes("0x800300040064617465024614e14d04007465787401040000006869696906007772697465720108000000474d205275667573");
	print_r($test -> getAttributes());

Output:
Code:
Array ( [date] => 1306596422 [text] => hiii [writer] => GM Rufus )

scrolln.png
 
Last edited:
@mazen
No problem, I'm happy to have helped you.

About the PHP code, I will leave here one little code in PHP made by me to write an custom attribute for some item, using the OTS_Buffer class of POT to it:

PHP:
$buffer = new OTS_Buffer();
			
$ATTRIBUTE_MAP = 128;
			
$VALUE_TYPE_INTEGER = 2;
			
$buffer->putChar($ATTRIBUTE_MAP);
$buffer->putShort(1); //attributes count
$buffer->putString("myCustomAttr"); //attribute name
$buffer->putChar($VALUE_TYPE_INTEGER); //attribute value data type
$buffer->putLong(10000); //attribute value

//we can now put the $string in some query
$string = $buffer->getBuffer();

Using OTS_Buffer of POT are an easy way to write/read any type of binary data, if you know your structure.

@soul4soul

so what is the last "0000" for?

An integer is formed by 4 bytes, the pair of null bytes quoted (00 00) are unused part of the integer. Note that even unused, this bytes must exists to integrity of the binary data structure.
 
Last edited:
No problem, I'm happy to have helped you.
Thx m8. :)

About the PHP code, I will leave here one little code in PHP made by me to write an custom attribute for some item, using the OTS_Buffer class of POT to it:

PHP:
$buffer = new OTS_Buffer();
			
$ATTRIBUTE_MAP = 128;
			
$VALUE_TYPE_INTEGER = 2;
			
$buffer->putChar($ATTRIBUTE_MAP);
$buffer->putShort(1); //attributes count
$buffer->putString("myCustomAttr"); //attribute name
$buffer->putChar($VALUE_TYPE_INTEGER); //attribute value data type
$buffer->putLong(10000); //attribute value

//we can now put the $string in some query
$string = $buffer->getBuffer();

Using OTS_Buffer of POT are an easy way to write/read any type of binary data, if you know your structure.

@soul4soul



An integer is formed by 4 bytes, the pair of null bytes quoted (00 00) are unused part of the integer. Note that even unused, this bytes must exists to integrity of the binary data structure.
I've taken a look at the OTS_Buffer class, but it doesn't seem to be able to read the data the same way I did it. It can create binary and return binary, but it cannot decode the information. Obviously because that's a global class that should work for all binary data. So there are no formats of data it can understand.

EDIT: You're right. The buffer method seems to be easier, and since we know the format ourselves it's easy for us to get the information just by the buffer class.
 
Last edited:
Back
Top