Source code for PyPoE.poe.file.idt
"""
Overview
===============================================================================
+----------+------------------------------------------------------------------+
| Path | PyPoE/poe/file/idt.py |
+----------+------------------------------------------------------------------+
| Version | 1.0.0a0 |
+----------+------------------------------------------------------------------+
| Revision | $Id: f6432f6155b2d277be48be55bd8a89f893a14485 $ |
+----------+------------------------------------------------------------------+
| Author | Omega_K2 |
+----------+------------------------------------------------------------------+
Description
===============================================================================
File Format handler for Grinding Gear Games' .idt format.
.idt files are generally used to link the inventory texture to an object.
Agreement
===============================================================================
See PyPoE/LICENSE
Documentation
===============================================================================
Public API
-------------------------------------------------------------------------------
.. autoclass:: IDTFile
.. autoclass:: TextureRecord
.. autoclass:: CoordinateRecord
Internal API
-------------------------------------------------------------------------------
.. autoclass:: TextureList
.. autoclass:: CoordinateList
"""
# =============================================================================
# Imports
# =============================================================================
# Python
import codecs
import re
# self
from PyPoE.shared.containers import Record, TypedList, TypedContainerMeta
from PyPoE.poe.file.shared import AbstractFile, ParserError
# =============================================================================
# Globals
# =============================================================================
__all__ = ['IDTFile', 'TextureRecord', 'CoordinateRecord']
# =============================================================================
# Classes
# =============================================================================
[docs]class CoordinateRecord(Record):
"""
Object that represents a single coordinate with the relevant attributes
Attributes
----------
x : int
x-coordinate
y : int
y-coordinate
"""
__slots__ = ['x', 'y']
[docs] def __init__(self, x, y):
"""
Parameters
----------
x : int
x-coordinate
y : int
y-coordinate
"""
self.x = int(x)
self.y = int(y)
[docs]class CoordinateList(TypedList, metaclass=TypedContainerMeta):
"""
A list that only accepts :class:`CoordinateRecord` instances.
"""
ACCEPTED_TYPES = CoordinateRecord
[docs]class TextureRecord(Record):
"""
Object that represents a single texture with the relevant attributes
Attributes
----------
'name' : str
name (internal path) of the texture
'records' : CoordinateList[CoordinateRecord]
:class:`CoordinateList` of :class:`CoordinateRecord` instances for this
texture.
"""
__slots__ = ['name', 'records']
[docs] def __init__(self, name, records=None):
"""
Parameters
----------
name : str
name (internal path) of the texture
records : None or CoordinateList[CoordinateRecord]
:class:`CoordinateList` of :class:`CoordinateRecord` instances for
this texture. If None, an empty :class:`CoordinateList` will be
created.
Raises
------
TypeError
If records is of invalid type
TypeError
If the containing types of records are invalid
"""
self.name = name
if records is None:
self.records = CoordinateList()
elif isinstance(records, CoordinateList):
self.records = records
elif isinstance(records, list):
self.records = CoordinateList(records)
else:
raise TypeError('records must a valid CoordinateList.')
[docs]class TextureList(TypedList, metaclass=TypedContainerMeta):
"""
A list that only accepts TextureRecord instances.
"""
ACCEPTED_TYPES = TextureRecord
[docs]class IDTFile(AbstractFile):
"""
Encapsulated in-memory representation of .idt files.
"""
# complete match
_regex_parse = re.compile(
r'^'
r'version (?P<version>[0-9]+)[\r\n]*'
r'image "(?P<image>[\w\./\\_\'\-]+)"[\r\n]*'
r'(?P<texture_count>[0-9]+)[\r\n]*'
r'(?P<textures>.*)' # Match the rest
r'$',
re.UNICODE | re.MULTILINE | re.DOTALL
)
# for findall
_regex_texture = re.compile(
r'^'
r'(?P<name>[a-zA-Z]+)[ ]+'
r'(?P<count>[0-9]+)[ ]+'
r'(?P<coordinates>(?:[0-9]+[ ]*)*)'
r'[\r\n]*$',
re.UNICODE | re.MULTILINE | re.DOTALL
)
# for findall
_regex_coordinates = re.compile(
r'(?P<x>[0-9]+)[ ]+'
r'(?P<y>[0-9]+)[ ]*'
r'',
re.UNICODE | re.MULTILINE | re.DOTALL
)
EXTENSION = '.idt'
[docs] def __init__(self, data=None):
"""
Creates a new IDTFile instance.
Optionally data can be specified to initialize the object in memory
with the given data. The same can be achieved by simply setting the
relevant attributes.
Note that :meth:`IDTFile.read` will override any initial data.
Parameters
----------
data : dict or None
Take a dict containing the data to create this object and it's
attributes with. The dict should match the structure of the classes
attributes and the respective sub attributes.
Raises
------
TypeError
if dict contains data of invalid types
"""
if data is None:
self.version = 0
self._image = None
self._records = TextureList()
else:
tex = TextureList()
for tex_record in data['records']:
x = CoordinateList()
for coord_record in tex_record['records']:
x.append(CoordinateRecord(**coord_record))
kwargs = tex_record.copy()
del kwargs['records']
tex.append(TextureRecord(records=x, **kwargs))
self.version = data['version']
self.image = data['image']
self._records = tex
# Properties
def _get_records(self):
"""
Get records
Returns
-------
TextureList[TextureRecord]
List of stored :class:`TextureRecord` instances
"""
return self._records
def _set_records(self, value):
"""
Set records
Parameters
----------
value : TextureList[TextureRecord]
value to set the records to
Raises
------
TypeError
if the record is an invalid texture list
"""
if isinstance(value, TextureList):
self._records = value
elif isinstance(value, list):
self._records = TextureList(value)
else:
raise TypeError('records must be a valid TextureList.')
records = property(fget=_get_records, fset=_set_records)
def _get_image(self):
"""
Get image path
Returns
-------
str
image path relative to content.ggpk root
"""
return self._image
def _set_image(self, value):
"""
Set image path
Parameters
----------
value : str
image path relative to content.ggpk root
"""
self._image = value.replace('\\', '/')
image = property(fget=_get_image, fset=_set_image)
# Private
def _write(self, buffer):
out = []
out.append('version %s\n' % self.version)
out.append('image "%s"\n' % self._image)
out.append('%s\n' % len(self._records))
for tex_record in self._records:
out.append('%s %s' % (tex_record.name, len(tex_record.records)))
for coord_record in tex_record.records:
out.append(' %s %s' % (coord_record.x, coord_record.y))
out.append('\n')
''.join(out).encode('utf-16_le')
buffer.write(codecs.BOM_UTF16_LE + ''.join(out).encode('utf-16_le'))
def _read(self, buffer, *args, **kwargs):
# Should detect little endian byte order accordingly and remove the BOM
data = buffer.read().decode('utf-16')
match = self._regex_parse.match(data)
if not match:
raise ParserError('Failed to find the base information. File may not be a .idt file or malformed.')
textures = TextureList()
for tex_match in self._regex_texture.finditer(match.group('textures')):
coordinates = CoordinateList()
for coord_match in self._regex_coordinates.finditer(tex_match.group('coordinates')):
coordinates.append(CoordinateRecord(**coord_match.groupdict()))
if len(coordinates) != int(tex_match.group('count')):
raise ParserError('Amount of found coordinates (%s) does not match the amount of specified coordinates (%s)' % (len(coordinates), tex_match.group('count')))
textures.append(TextureRecord(tex_match.group('name'), coordinates))
if len(textures) != int(match.group('texture_count')):
raise ParserError('Amount of found textures (%s) does not match the amount of specified textures (%s)' % (len(textures), match.group('texture_count')))
self._records = textures
self.version = int(match.group('version'))
self.image = match.group('image')