<repo_name>gurka/TibiaTools<file_sep>/tibiatools/dat.py
"""Functions to read and parse the Tibia data (Tibia.dat) file.

At the moment only the Tibia 7.1 data file is supported.
"""

from ._utils import readU8, readU16, readU32


def parseDatFile(filename):
    """Opens and parses the given Tibia data file.

    Currently only reads items.

    Returns a dict with all items, where the key is the item's id
    and the value is a dict with the following key-value pairs:

    'id': The item's id.

    'flags': A dict where the key is the flag and the value is a
             list of additional values for the flag.

    'width': The item's width (in number of sprites).

    'height': The item's height (in number of sprites).

    'exactSize': If width or height is greater than 1 this value
                 will be the item's exact size (in number of pixels),
                 otherwise it will be None.

                 'TODO': If this value is set, is the item drawn with
                 exactSize * exactSize pixels (i.e. a square), or is
                 this value the larger or smaller side of the item?

    'noLayers': The item's number of layers.

    'patternWidth': The item's number of patterns (x-axis).

    'patternHeight': The item's number of patterns (y-axis).

    'noPhases': The item's number of phases (animations).

    'sprites': A list of all sprite ids for the item. The length of
               this list will be width * height * noLayers *
               patternWidth * patternHeight * noPhases.
    """
    items = dict()

    with open(filename, "rb") as f:
        checksum = readU32(f)
        print(checksum)

        noItems = readU16(f)
        readU16(f)  # Number of outfits
        readU16(f)  # Number of effects
        readU16(f)  # Number of distances (TODO: better name)

        # First item has id 100, so iterate from 100 to 100 + noItems
        for itemId in range(100, 100 + noItems):
            flags = dict()
            flag = readU8(f)
            while flag != 0xFF:

                if flag == 0x00:
                    speed = readU16(f)
                    flags[flag] = [speed]

                elif flag == 0x10:
                    level = readU16(f)
                    color = readU16(f)
                    flags[flag] = [level, color]

                elif flag in [0x07, 0x08]:
                    maxLength = readU16(f)
                    flags[flag] = [maxLength]

                elif flag in [0x13, 0x16]:
                    offset = readU16(f)
                    flags[flag] = [offset]

                elif flag == 0x1A:
                    value = readU16(f)
                    flags[flag] = [value]

                else:
                    flags[flag] = []

                flag = readU8(f)  # Next flag

            # Width and height is the number of sprites (x and y axis) this item is drawn with
            # width=1, height=1 is drawn with 1 sprite, which means 32x32 pixels
            # width=2, height=2 is drawn with 4 sprites, which means 64x64 pixels
            width = readU8(f)
            height = readU8(f)

            # Exact size is used when an item with width > 1 or height > 1 is smaller than
            # the actual sprites size
            # I.e. an item with width=2, height=2 can have exactSize=40
            # It is drawn using 4 sprites but only 40x40 pixels needs to be drawn
            if width > 1 or height > 1:
                exactSize = readU8(f)
            else:
                exactSize = None

            # Multiple layers are used when an item is drawn using multiple sprites
            # at the same position
            noLayers = readU8(f)

            # Pattern width and height are used when multiple items with the same id are
            # drawn besides eachother, to make them blend together better
            # If grass (let's say itemId = 1) is drawn in a 4x4 pattern, the map might have:
            # 1111
            # 1111
            # But grass has patternWidth=4, patternHeight=4,
            # so the sprites used to draw the grass are:
            # 1234
            # 5678
            # If the item is stackable and has more than 8 sprites,
            # then patternWidth=4 and patternHeight=2 and "count" is used to
            # choose which sprite to draw
            patternWidth = readU8(f)
            patternHeight = readU8(f)

            # Phases are used to animate items, the phase to choose is based on
            # current time / game step:
            # currentPhase = gamestep % noPhases
            noPhases = readU8(f)

            # TODO: The below is probably not 100% correct
            # Draw order and read order is probably the same...
            # If the item has width=2 and height=2 there will be 4 sprites to draw:
            # AB
            # CD
            # They needs to be drawn in order D, C, B, A, and is probably read in
            # this order as well.

            # The sprites are read in a specific order. If we have an item with:
            # width=2, height=2, noLayers=2, patternWidth=2, patternHeight=2, noPhases=2
            # Then the order is:
            # phase 0 - patternHeight 0 - patternWidth 0 - layer 0 - height 0 - width 0
            # phase 0 - patternHeight 0 - patternWidth 0 - layer 0 - height 0 - width 1
            # phase 0 - patternHeight 0 - patternWidth 0 - layer 0 - height 1 - width 0
            # phase 0 - patternHeight 0 - patternWidth 0 - layer 0 - height 1 - width 1
            # phase 0 - patternHeight 0 - patternWidth 0 - layer 1 - height 0 - width 0
            # phase 0 - patternHeight 0 - patternWidth 0 - layer 1 - height 0 - width 1
            # phase 0 - patternHeight 0 - patternWidth 0 - layer 1 - height 1 - width 0
            # phase 0 - patternHeight 0 - patternWidth 0 - layer 1 - height 1 - width 1
            # and so on...

            # So the get the spriteId for
            # phase=1, pattern width=1, pattern height=1, layer=0, height=1, width=0:
            # index = 1 * patternHeight * patternWidth * layer * height * width
            # index += 1 * patternWidth * layer * height * width
            # index += 0 * layer * height * width
            # index += 0 * height * width
            # index += 1 * width
            # index += 0
            # spriteId = sprites[index]

            # The order to draw part of an item (each sprite) is important
            # If the item has width = 2 and height = 2 the item is drawn using 4 sprites:
            # 12
            # 34
            # And we need to draw them in the order 4, 3, 2, 1

            sprites = []
            for sprite in range(noPhases * patternHeight * patternWidth *
                                noLayers * height * width):
                sprites.append(readU16(f))

            items[itemId] = {
                'id': itemId,
                'flags': flags,
                'width': width,
                'height': height,
                'exactSize': exactSize,
                'noLayers': noLayers,
                'patternWidth': patternWidth,
                'patternHeight': patternHeight,
                'noPhases': noPhases,
                'sprites': sprites,
            }

    return items


def flagToString(flag, extras=[]):
    """Converts the given flag to a string that describes it.

    The optional argument extra should be a list of any additional
    values for this flag.
    """

    if flag == 0x00:
        return "isGround - Speed: {}".format(extras[0])

    elif flag == 0x01:
        return "isOnTop"

    elif flag == 0x02:
        return "walkThrough"

    elif flag == 0x03:
        return "isContainer"

    elif flag == 0x04:
        # If sprites == 8 (patternWidth: 4, patternHeight: 2)
        # Then sprites => count
        return "isStackable"

    elif flag == 0x05:
        # Right-click -> use with crosshair
        return "isUsable"

    elif flag == 0x06:
        # Only one item... (1124)
        return "ladderUp"

    elif flag == 0x07:
        return "isWriteable - Max length: {}".format(extras[0])

    elif flag == 0x08:
        return "isReadable - Max length: {}".format(extras[0])

    elif flag == 0x09:
        return "isFluidContainer"

    elif flag == 0x0A:
        return "isSplash"

    elif flag == 0x0B:
        return "isBlocking"

    elif flag == 0x0C:
        return "isImmovable"

    elif flag == 0x0D:
        return "blocksMissile"

    elif flag == 0x0E:
        return "blocksCreature"

    elif flag == 0x0F:
        return "isEquipable"

    elif flag == 0x10:
        return "hasLight - Light level: {} Light color: {}".format(extras[0], extras[1])

    elif flag == 0x11:
        return "canSeeUnder"

    elif flag == 0x12:
        return "noLevelChange"

    elif flag == 0x13:
        return "drawOffset - Offset: {}".format(extras[0])

    elif flag == 0x14:
        return "playerColorTemplates"

    elif flag == 0x15:
        # No item with this flag
        return "unknown (0x15)"

    elif flag == 0x16:
        return "offsetLifebar - Offset: {}".format(extras[0])

    elif flag == 0x17:
        # No item with this flag
        return "unknown (0x17)"

    elif flag == 0x18:
        return "noDecay"

    elif flag == 0x19:
        return "animationWhenIdle"

    elif flag == 0x1A:
        # Right-click -> Use
        return "canDoAction - Value: {}".format(extras[0])

    else:
        return "unknown (0x{:02X})".format(flag)
<file_sep>/dat_test.py
#!/usr/bin/env python3
from tibiatools import dat

if __name__ == '__main__':
    items = dat.parseDatFile("Tibia.dat")
    for itemId in items:
        print(itemId)
<file_sep>/tibiatools/_utils.py
"""Internal functions used by tibiatools"""


def readU8(f):
    """Reads an unsigned 8 bit integer from file object `f`."""
    return int.from_bytes(f.read(1), byteorder='little', signed=False)


def readU16(f):
    """Reads an unsigned 16 bit integer from file object `f`."""
    return int.from_bytes(f.read(2), byteorder='little', signed=False)


def readU32(f):
    """Reads an unsigned 32 bit integer from file object `f`."""
    return int.from_bytes(f.read(4), byteorder='little', signed=False)
