Reading

This example covers reading binary data with the module

If you're interested in using the module to write binary data, check out the first example.

The BitBuffer is also designed to easily read binary data. This is useful for all sorts of situations, but for this example the information written in the previous page will be used. To review, that data structure looked like this:

local data = {
    name = "John Doe",
    health = 100,
    maxHealth = 100,
    points = 10,
    position = {
        x = 10.55915,
        y = -15.2222,
    },
    bossesKilled = { true, false, true },
}

After the information was written with the BitBuffer to a file, it can be read issue with the buffer. It's as simple as reading the file contents, creating a BitBuffer with the content, and calling the respective read functions in order. If the file is read out of order, or the wrong datatypes are read, it will give unexpected results.

local BitBuffer = ... -- The BitBuffer should be required here.
local fileContents = ... -- The contents of the file should be put here.

local data = {}

local buffer = BitBuffer(fileContents) -- This creates a new BitBuffer with `fileContents` inside it

data.name = buffer:readString()
data.health = buffer:readInt16()
data.maxHealth = buffer:readInt16()
data.points = buffer:readInt16()
data.position = {
    x = buffer:readFloat32(),
    y = buffer:readFloat32(),
}
data.bossesKilled = buffer:readField(3)

With minimal effort, the data is restored... For the most part. All floating point numbers are subject to minor errors, and unfortunately the BitBuffer is no different. As a result, the position data read above will not be exactly correct. This is unavoidable and cannot be fixed. If you want to minimize floating point errors, use integers, or write Float64s instead of Float32s.

Technical Info

This section analyzes the actual output of the BitBuffer and looks at what each byte is for the sake of knowledge.

If viewed in a hex editor (HxD is recommended), the above data structure looks like this when written to the file:

00 00 08 4A 6F 68 6E 20 44 6F 65 00 64 00 64 00 0A 41 28 F2 47 C1 73 8E 22 A0

As mentioned above, the data has to be read in the order it was written in.

So, data.name is first. Since writeString was used to write the name, a 24-bit unsigned integer was written to indicate how long the string is, then the raw bytes of the string are written. Since the number 0x000008 is 8, the string is 8 bytes long, which means it's 4A 6F 68 6E 20 44 6F 65 or John Doe when translated to ASCII.

Next, data.health. This was written as a 16-bit unsigned integer, which means that the value is 0x0064, or 100 in decimal. This process is repeated for data.maxHealth and data.points, which are 0x0064 and 0x000A (or 100 and 10).

Next, the position values data.position.x and data.position.y. These were written as 32-bit floating point numbers, which means that 4 bytes have to be read for each, which are 41 28 F2 47 and C1 73 8E 22 respectively. Rather than explaining floating point numbers in full here, these can be plugged into a site like float.exposed to read their values. When plugged into that site (32-bit floats are commonly called singles) 0x4128F247 is 10.5591497421264648438 and 0xC1738E22 is read as -15.2222003936767578125.

Finally, data.bossesKilled is written as a bitfield of three bools. Because individual bits can't be written to a file, the spare 5 bits are filled with 0s and written as well. The last byte in the file (A0) is 10100000 in binary. Reading the first three bits of the byte gives 101 or {true, false, true}.