def _fallback_obj_container(tag):
def _impl_modules():
def _detect_non_level_objects_other(section):
def _fallback_levelobject(section):
def _blacklist_nonlevel(section):
def write_autoload_modules(): class Fragment(BytesModel):

    """Represents a container section and its content.

    Attributes
    ----------
    dbytes : DstBytes
        The source this fragment has been read from.
    container : Section
        The container section of this fragment.
    classes : ClassesRegistry
        The registry used for contained objects and tags. Default is the
        ``DefaultRegistry``.

    """

    __slots__ = ('_raw_data', 'container', 'dbytes', 'classes')

    default_container = None

    @classproperty
    def base_container(cls):

        """The base container of this class.

        Defaults to None, or the base section of `default_container` if set.

        """

        con = cls.default_container
        if con is None:
            return None
        if con.has_version():
            return Section(con, version=None)
        return con

    @classproperty
    def container_versions(cls):

        """The container versions supported by this class.

        Defaults to None, or the version of `default_container` if it has a
        version.

        """

        con = cls.default_container
        if con is None or not con.has_version():
            return None
        return con.version

    @classmethod
    def get_default_container(cls):

        """Get the default container when creating this class.

        Returns
        -------
        container : Section
            `default_container` if it is not None.

            If `default_container` is None, returns `base_container`, with the
            highest version in `container_versions` if the section has a
            version and both are not None. If it has no version, it is returned
            as-is. Otherwise, returns None.

        """

        defcon = cls.default_container
        if defcon is not None:
            return defcon
        base = cls.base_container
        if base is None:
            return None
        if not base.has_version():
            return base
        versions = cls.container_versions
        if versions is None:
            return None
        if isinstance(versions, numbers.Integral):
            version = versions
        else:
            version = max(versions)
        return Section(base, version=version)

    is_interesting = False

    @classproperty
    def class_tag(cls):

        """Tag of this class.

        Default is the class name with "Fragment" suffix removed, and raises
        AttributeError if the name does not end with "Fragment".

        """

        name = cls.__name__
        if not name.endswith('Fragment') or name == 'Fragment':
            raise AttributeError(f"Could not get class tag for {cls!r}")
        return name[:-8]

    def __init__(self, dbytes=None, **kw):
        self.classes = kw.pop('classes', DefaultClasses)
        super().__init__(dbytes=dbytes, **kw)

    def _init_defaults(self):
        super()._init_defaults()
        con = self.get_default_container()
        if con is not None:
            self.container = con

    def _read(self, dbytes, *, container=None, classes=None, child_prober=None):

        """Read data of the Fragment.

        Parameters
        ----------
        container : Section
            Container Section of this Fragment. `dbytes` needs to be positioned
            at `content_start` of the section if set. If omitted, it is read
            from `dbytes` before reading its content.
        classes : ClassesRegistry
            If not None, it is first assigned to the `classes` attribute of the
            object.
        child_prober : str
            Specifies the child object prober name for ObjectFragment. Only
            accepted here to allow passing it without checking for this
            object's type.

        """

        self.dbytes = dbytes
        if classes is not None:
            self.classes = classes
        if container is None:
            container = Section(dbytes, seek_end=False)
        self.container = container
        self.end_pos = container.end_pos
        self._read_section_data(dbytes, container)

    def visit_write(self, dbytes):
        con = getattr(self, 'container', None)
        sec = self._get_write_section(con)
        with dbytes.write_section(sec):
            yield self._visit_write_section_data(dbytes, sec)

    def _get_write_section(self, sec):
        return sec or self.get_default_container()

    @property
    def raw_data(self):

        """The unmodified raw bytes of this fragment.

        Only valid for fragments that have been read or if this property has
        been assigned first.

        This attribute is initialized lazily. File errors may occur on access.

        Returns
        -------
        data : bytes
            The bytes of this fragment.

        Raises
        ------
        AttributeError
            If this fragment has no assigned raw data and has no source and
            container information to initialize it.

        """

        try:
            return self._raw_data
        except AttributeError:
            pass
        try:
            dbytes = self.dbytes
            sec = self.container
            start = sec.content_start
            size = sec.content_size
        except AttributeError:
            raise AttributeError(f"Missing source and/or container information")
        with dbytes:
            dbytes.seek(start)
            data = dbytes.read_bytes(size)
            self._raw_data = data
        return data

    @raw_data.setter
    def raw_data(self, value):
        self._raw_data = value

    def clone(self):

        """Clones the fragment.

        Subclasses should override `_clone_data` to copy values to the new
        instance. The default just copies `raw_data`.

        The container is always copied, but only its type information is
        retained.

        """

        new = type(self)()
        try:
            con = self.container
        except AttributeError:
            pass
        else:
            new.container = Section(con)
        self._clone_data(new)
        return new

    def _clone_data(self, new):
        new.raw_data = self.raw_data

    def _read_section_data(self, dbytes, sec):
        """Read data of the given section."""

    def _visit_write_section_data(self, dbytes, sec):

        """Write data of the given section (trampolined).

        Default implementation delegates to `_write_section_data`.

        """

        self._write_section_data(dbytes, sec)
        return
        yield

    def _write_section_data(self, dbytes, sec):

        """Non-trampolined version of `_write_section_data`.

        The default implementation just writes this object's `raw_data`.

        """

        dbytes.write_bytes(self.raw_data)

    def __str__(self):
        from io import StringIO
        sio = StringIO()
        flags = ('allprops', 'sections', 'transform', 'offset',
                 'objects', 'subobjects', 'groups', 'fragments')
        self.print(file=sio, flags=flags)
        return sio.getvalue()

    def _print_type(self, p):
        hint = ''
        try:
            tag = self.class_tag
        except AttributeError:
            tag = None
        if tag is None:
            type_str = 'Unknown'
        else:
            type_str = repr(tag)

        actual_str = ''
        ver_str = ''
        overridden = False
        try:
            container = self.container
        except AttributeError:
            pass
        else:
            try:
                con_tag = self.classes.fragments.get_tag(container)
            except KeyError:
                con_tag = None
            if con_tag != tag:
                overridden = tag is not None
                if con_tag is None:
                    actual_str = " (container Unknown)"
                else:
                    actual_str = f" (container {con_tag!r})"

            if not overridden and tag is not None and container.has_version():
                version = self.container.version
                ver_str = f" version {version!r}"

        if overridden:
            hint = ' [overridden]'
        elif tag is None:
            hint = ' [dummy]'
        else:
            hint = ''

        p(f"Fragment: {type_str}{actual_str}{ver_str}{hint}")

    def _visit_print_data(self, p):
        if 'sections' in p.flags:
            try:
                container = self.container
            except AttributeError:
                pass
            else:
                p(f"Container:")
                with p.tree_children(1):
                    yield container.visit_print(p) class MaterialFragment(Fragment):

    base_container = Section.base(Magic[3], 0x3)
    container_versions = 1, 2

    have_content = False

    def __init__(self, *args, **kw):
        self.materials = MaterialSet()
        Fragment.__init__(self, *args, **kw)

    def _clone_data(self, new):
        dest = new.materials
        for matname, mat in self.materials.items():
            destmat = dest.get_or_add(matname)
            for colname, col in mat.items():
                destmat[colname] = col

    def _read_section_data(self, dbytes, sec):
        if sec.content_size >= 4:
            self.have_content = True
            self.materials.read(dbytes)

    def _write_section_data(self, dbytes, sec):
        if self.materials or self.have_content:
            self.materials.write(dbytes)

    def _visit_print_data(self, p):
        yield super()._visit_print_data(p)
        if 'allprops' in p.flags and self.materials:
            self.materials.print(p) class TrackNodeFragment(BaseConstructFragment):

    base_container = Section.base(Magic[2], 0x16)
    container_versions = 2

    _construct_ = Struct(
        'parent_id' / Default(UInt, 0),
        'snap_id' / Default(UInt, 0),
        'conn_id' / Default(UInt, 0),
        'primary' / Default(Byte, 0),
    )

    def _visit_print_data(self, p):
        yield super()._visit_print_data(p)
        if 'sections' in p.flags or 'track' in p.flags:
            p(f"Parent ID: {self.parent_id}")
            p(f"Snapped to: {self.snap_id}")
            p(f"Connection ID: {self.conn_id}")
            p(f"Primary: {self.primary and 'yes' or 'no'}") class AnimatorFragment(BaseConstructFragment):

    base_container = Section.base(Magic[2], 0x9a)
    container_versions = 7, 10, 11

    _construct_ = Struct(
        # 2: hinge
        'motion_mode' / Default(UInt, 2),
        'do_scale' / Default(Byte, 0),
        'scale_exponents' / Default(Float[3], (0, 1, 0)),
        'do_rotate' / Default(Byte, 1),
        'rotate_axis' / Default(Float[3], (0, 1, 0)),
        'rotate_global' / Default(Byte, 0),
        'rotate_magnitude' / Default(Float, 90),
        'centerpoint' / Default(Float[3], (0, 0, 0)),
        # 0: none
        'translate_type' / Default(UInt, 0),
        'translate_vector' / Default(Float[3], (0, 10, 0)),
        'follow_track_distance' / Default(Float, 25),
        'double_pivot_distance' / Default(If(this._params.sec.version >= 11, Float), 0.0),
        'follow_percent_of_track' / Default(If(this._params.sec.version >= 10, Byte), 1),
        'wrap_around' / Default(If(this._params.sec.version >= 10, Byte), 1),
        'projectile_gravity' / Default(Float[3], (0, -25, 0)),
        'delay' / Default(Float, 1),
        'duration' / Default(Float, 1),
        'time_offset' / Default(Float, 0),
        'do_loop' / Default(Byte, 1),
        # 1: pingpong
        'extrapolation_type' / Default(If(this._params.sec.version <= 7, UInt), 1),
        'do_extend' / Default(If(this._params.sec.version >= 10, Byte), 0),
        # 3: ease in out
        'curve_type' / Default(UInt, 3),
        'editor_anim_time' / Default(Float, 0),
        'use_custom_pong_values' / Default(Byte, 0),
        'pong_delay' / Default(Float, 1),
        'pong_duration' / Default(Float, 1),
        # 2: ease in out
        'pong_curve_type' / Default(UInt, 2),
        'anim_physics' / Default(Byte, 1),
        'always_animate' / Default(Byte, 0),
        # 1: play
        'default_action' / Default(UInt, 1),
        # 1: play
        'trigger_on_action' / Default(UInt, 1),
        'trigger_wait_for_anim_finish' / Default(Byte, 0),
        'trigger_on_reset' / Default(Byte, 0),
        # 2: play reverse
        'trigger_off_action' / Default(UInt, 2),
        'trigger_off_wait_for_anim_finish' / Default(Byte, 0),
        'trigger_off_reset' / Default(Byte, 0),
    ) class EventListenerFragment(BaseConstructFragment):

    base_container = Section.base(Magic[2], 0x8a)
    container_versions = 1, 2

    _construct_ = Struct(
        'event_name' / Default(DstString, "Event 0"),
        'delay' / If(this._params.sec.version >= 2, Default(Float, 0.0)),
    ) class named_property_getter(property):
class BaseNamedProperty(property):
class TupleStructNamedProperty(BaseNamedProperty):
class StructNamedProperty(TupleStructNamedProperty):
class ByteNamedProperty(StructNamedProperty):
class StringNamedProperty(BaseNamedProperty):
class NamedPropertiesFragment(Fragment):
class RaceEndLogicFragment(NamedPropertiesFragment):
class EnableAbilitiesTriggerFragment(NamedPropertiesFragment):
class OldCarScreenTextDecodeTriggerFragment(bases.BaseCarScreenTextDecodeTrigger, NamedPropertiesFragment):
class OldInfoDisplayLogicFragment(bases.BaseInfoDisplayLogic, NamedPropertiesFragment):
class OldTeleporterEntranceFragment(bases.BaseTeleporterEntrance, NamedPropertiesFragment):
class OldTeleporterExitFragment(bases.BaseTeleporterExit, NamedPropertiesFragment):
class OldInterpolateToPositionOnTriggerFragment(
        bases.BaseInterpolateToPositiononTrigger, NamedPropertiesFragment):
    def __init__(self, propname, default=None):
        def fget(obj):
    def __call__(self, func):
    def __init__(self, propname, default=None):
        def fget(obj, objtype=None):
        def fset(obj, value):
        def fdel(obj):
    def __init__(self, propname, struct, default=None):
    def _from_bytes(self, data):
    def _to_bytes(self, value):
    def _from_bytes(self, data):
    def _to_bytes(self, value):
    def __init__(self, propname, default=None):
    def __init__(self, propname, default=None):
    def _from_bytes(self, data):
    def _to_bytes(self, inst, value):
    def _fields_(cls):
    def class_tag(cls):
    def __init__(self, *args, **kw):
    def _clone_data(self, new):
    def _read_section_data(self, dbytes, sec):
    def _write_section_data(self, dbytes, sec):
    def _visit_print_data(self, p):
    def _visit_print_data(self, p):
    def abilities(self):
    def _visit_print_data(self, p):
    def texts(self):
def _create_property_fragment_classes():
def add_property_fragments_to_collector(coll):
    KNOWN_ABILITIES = (
        'EnableFlying', 'EnableJumping',
        'EnableBoosting', 'EnableJetRotating',
    )
PROPERTY_FRAGS = (
    (Section(Magic[2], 0x25, 0), "PopupBlockerLogic"),
    (Section(Magic[2], 0x42, 0), "ObjectSpawnCircle"),
    (Section(Magic[2], 0x39, 0), "ParticleEmitLogic"),
    (Section(Magic[2], 0x26, 0), "Light"),
    (Section(Magic[2], 0x28, 0), "SmoothRandomPosition"),
    (Section(Magic[2], 0x45, 0), None),
    (Section(Magic[2], 0x24, 0), "OldFlyingRingLogic"),
    (Section(Magic[2], 0x50, 0), "Pulse"),
    # found on Damnation
    (Section(Magic[2], 0x59, 0), None),
    (Section(Magic[2], 0x4b, 0), None),
    (Section(Magic[2], 0x4e, 0), None),
    (Section(Magic[2], 0x3a, 0), None),
    (Section(Magic[2], 0x58, 0), None),
    (Section(Magic[2], 0x2c, 0), None),
    # found on Hextreme
    (Section(Magic[2], 0x1b, 0), None),
    # found on "Brief Chaos" object EmpireBrokenBuilding006_PiecesSeparated
    (Section(Magic[2], 0x44, 0), None),
    # from v5 AudioEventTrigger
    (Section(Magic[2], 0x74, 0), None),
    # from v5 CubeMIDI
    (Section(Magic[2], 0x3d, 0), None),
    # from v9 RumbleZone
    (Section(Magic[2], 0x66, 0), None),
    # from v8 PulseCore
    (Section(Magic[2], 0x4f, 0), None),
    # from v8 KillGridBox
    (Section(Magic[2], 0x82, 0), None),
    # from v5 WarpAnchor
    (Section(Magic[2], 0x6e, 0), None),
    # from v7 PlanetWithSphericalGravity
    (Section(Magic[2], 0x5f, 0), None),
    # from v5 VirusSpiritShard
    (Section(Magic[2], 0x67, 0), None),
    # from v9 WingCorruptionZone
    (Section(Magic[2], 0x53, 0), None),
    # from v8 IntroCutsceneLight
    (Section(Magic[2], 0x55, 0), None),
    # from v9 CutsceneLightning
    (Section(Magic[2], 0x6f, 0), None),
    # from s8 (map "The Matrix Arena")
    (Section(Magic[2], 0x17, 0), None), # from EmpireCircle
    (Section(Magic[2], 0x1f, 0), None), # from Teleporter
    # from v3 WarningPulseLight
    (Section(Magic[2], 0x65, 0), None),
    # from v1 LaserMid (sub of VirusLaserTriCircleRotating)
    (Section(Magic[2], 0x1a, 0), None),
    # from v5 EmpireProximityDoor
    (Section(Magic[2], 0x76, 0), None),
    # from v5 VirusSpiritWarpTeaser
    (Section(Magic[2], 0x7e, 0), None),
    # from v1 GlobalFog
    (Section(Magic[2], 0x60, 0), None),
    # from v8 DisableLocalCarWarnings
    (Section(Magic[2], 0x62, 0), None),
    # from v7 AdventureAbilitySettings
    (Section(Magic[2], 0x4d, 0), None),
    # from v3 VirusBase
    # very old versions (map "birthday bash court") don't use offsets
    (Section(Magic[2], 0x27, 0), None),
    # from s8 (map "The Pumpkin Patch")
    (Section(Magic[2], 0x38, 0), None),
    # from SoccerGoalLogic
    (Section(Magic[2], 0x29, 0), None),
    (Section(Magic[2], 0x3e, 0), "OldTeleporterEntrance"),
    (Section(Magic[2], 0x3f, 0), "OldTeleporterExit"),
    (Section(Magic[2], 0x5d, 0), "RaceEndLogic"),
    (Section(Magic[2], 0x5e, 0), "EnableAbilitiesTrigger"),
    (Section(Magic[2], 0x57, 0), "OldCarScreenTextDecodeTrigger"),
    (Section(Magic[2], 0x4a, 0), "OldInfoDisplayLogic"),
    (Section(Magic[2], 0x43, 0), "OldInterpolateToPositionOnTrigger"),
    (Section(Magic[2], 0x93, 0), "CarVoiceTrigger"),
) class EventTrigger(LevelObject):

    default_transform = Transform.fill(scale=(35, 35, 35)) UTF_16_DECODE = codecs.getdecoder('utf-16-le')
UTF_16_ENCODE = codecs.getencoder('utf-16-le')
S_COLOR_RGBA = Struct("<4f")
S_FLOAT = Struct("<f")
S_DOUBLE = Struct("<d")
S_FLOAT3 = Struct("<fff")
S_FLOAT4 = Struct("<ffff")
S_BYTE = Struct('b')
S_INT = Struct('<i')
S_LONG = Struct('<q')
S_UINT = Struct('<I')
S_ULONG = Struct('<Q')
S_UINT2 = Struct("<II")
S_UINT3 = Struct("<III")
SKIP_BYTES = b'\xFD\xFF\xFF\x7F'
MAGIC_1 = 11111111
MAGIC_2 = 22222222
MAGIC_3 = 33333333
MAGIC_5 = 55555555
MAGIC_6 = 66666666
MAGIC_7 = 77777777
MAGIC_8 = 88888888
MAGIC_9 = 99999999
MAGIC_12 = 12121212
MAGIC_32 = 32323232
CATCH_EXCEPTIONS = (ValueError, EOFError)
S_SEC_BASE = Struct("<IQ")
    MIN_SIZE = 12 # 4b (magic) + 8b (data_size)
class ErrorPosition(namedtuple('_ErrorPosition', ['start', 'error'])):
class BytesModel(object):
class Section(BytesModel):
class DstBytes(object):
    def __repr__(self):
    def get(cls, ex):
    def first(cls, ex):
    def maybe(cls, dbytes, **kw):
    def iter_n_maybe(cls, dbytes, n, **kw):
    def lazy_n_maybe(cls, dbytes, n, *, start_pos=None, **kw):
    def __init__(self, dbytes=None, **kw):
    def read(self, dbytes, *, seek_end=True, **kw):
    def _read(self, dbytes):
    def _init_defaults(self):
    def _after_init(self):
    def write(self, dbytes, **kw):
    def visit_write(self, dbytes):
    def __repr__(self):
    def _repr_detail(self):
    def print(self, file=None, flags=(), p=None):
    def visit_print(self, p):
    def _print_type(self, p):
    def _print_offset(self, p):
    def _visit_print_data(self, p):
    def _visit_print_children(self, p):
    def base(cls, *args, **kw):
    def __init__(self, *args, **kw):
    def _init_from_args(self, *args, any_version=False, base=None, **kw):
    def __repr__(self):
    def __getitem__(self, key):
    def to_key(self, noversion=False):
    def from_key(cls, key):
    def has_version(self):
    def _read(self, dbytes):
    def _write_header(self, dbytes):
    def _print_type(self, p):
    def _print_offset(self, p):
    def __init__(self, file):
    def __repr__(self):
    def in_memory(cls):
    def from_data(cls, data):
    def from_arg(cls, arg):
    def _write_arg(cls, obj, arg, write_mode='wb'):
    def __enter__(self):
    def __exit__(self, exc_type, exc_value, exc_traceback):
    def read_bytes(self, n):
    def read_byte(self):
    def read_var_int(self):
    def read_uint(self):
    def read_int(self):
    def read_ulong(self):
    def read_long(self):
    def read_struct(self, st):
    def read_str(self):
    def read_id(self):
    def write_bytes(self, data):
    def write_uint(self, value):
    def write_int(self, value):
    def write_ulong(self, value):
    def write_long(self, value):
    def write_var_int(self, value):
    def require_equal_uint4(self, expect):
    def write_str(self, s):
    def write_id(self, id_):
    def stable_iter(self, source, *, start_pos=None):
        def gen():
    def write_size(self):
    def write_num_subsections(self):
    def write_section(self, *args, **kw): def disable_writes(dbytes):
    def do_raise(*args, **kwargs):
def check_exceptions(obj):
def iter_objects(objects, with_groups=False):
def iter_level_objects(level, with_groups=False):
def write_read(obj, read_func=None, do_check_exceptions=True):
def read_or_skip(cls, filename, *args, **kw):
    def read_obj_pre(self, dbytes):
    def modify_obj(self, obj):
    def open(self, filename):
    def test_read(self):
    def test_write_read(self):
    def assertSeqAlmostEqual(self, a, b, msg="", **kw):
def assertLargeEqual(test, first, second, *, msg="", path="<obj>"):
def small_stack(size):
class WriteReadTest(unittest.TestCase):
class ExtraAssertMixin(object): class ExtraAssertMixin(object):

    def assertSeqAlmostEqual(self, a, b, msg="", **kw):
        if msg:
            msg = f" :\n{msg}"
        if len(a) != len(b):
            self.assertEqual(a, b)
            raise AssertionError(f"{a} != {b}\na={a}\nb={b}{msg}")
        for i, (va, vb) in enumerate(zip(a, b)):
            if isinstance(va, Sequence):
                self.assertSeqAlmostEqual(va, vb)
            else:
                self.assertAlmostEqual(va, vb, msg=f"\nindex={i}\na={a}\nb={b}{msg}", **kw) def write_read(obj, read_func=None, do_check_exceptions=True):
    if read_func is None:
        read_func = type(obj)

    dbytes = DstBytes.in_memory()

    obj.write(dbytes)
    dbytes.seek(0)
    disable_writes(dbytes)
    result = read_func(dbytes)

    if do_check_exceptions:
        check_exceptions(result)

    return result, dbytes.file
import unittest
from distance import DefaultClasses
from distance.base import Fragment
from distance.levelobjects import MaterialFragment
from distance._impl.fragments.levelfragments import (
    TrackNodeFragment,
    AnimatorFragment,
    EventListenerFragment,
)
from distance._impl.fragments.npfragments import (
    NamedPropertiesFragment,
    PopupBlockerLogicFragment,
    ObjectSpawnCircleFragment,
    StructNamedProperty,
)
from distance._impl.level_objects.objects import EventTrigger
from distance.bytes import SKIP_BYTES, DstBytes, Section, Magic, S_UINT
from tests import common
from tests.common import ExtraAssertMixin, write_read



class Base(object):

    class WriteReadTest(common.WriteReadTest):

        prober = DefaultClasses.fragments

        def test_clone(self):
            obj = self.read_obj_pre(self.filename)
            modified = self.modify_obj(obj)

            cloned = modified.clone()
            res, buf = write_read(cloned, read_func=self.read_obj)

            self.verify_obj(res)

        def test_probe(self):
            res = self.prober.read(self.filename)
            res = self.modify_obj(res)

            self.assertEqual(self.read_obj, type(res))
            self.verify_obj(res)


class UnknownTest(Base.WriteReadTest):

    filename = "tests/in/fragment/material splineroad.frag"

    read_obj_pre = Fragment

    read_obj = MaterialFragment

    def verify_obj(self, frag):
        MaterialTest.verify_obj(self, frag)


class CreateTest(unittest.TestCase):

    def test_write_cloned_created(self):
        frag = Fragment(raw_data=b'')

        clone = frag.clone()

        clone.container = Section(Magic[3], 1, 0)

        db = DstBytes.in_memory()
        clone.write(db)
        db.seek(0)
        res = Section(db)

        self.assertEqual(Section(Magic[3], 1, 0).to_key(), res.to_key())
        self.assertEqual(0, res.content_size)


class TracknodeTest(Base.WriteReadTest):

    filename = "tests/in/fragment/tracknode splineroad.frag"

    read_obj = TrackNodeFragment

    def verify_obj(self, frag):
        self.assertEqual(79, frag.parent_id)
        self.assertEqual(100, frag.snap_id)


class MaterialTest(Base.WriteReadTest):

    filename = "tests/in/fragment/material splineroad.frag"

    read_obj = MaterialFragment

    def verify_obj(self, frag):
        mats = frag.materials
        panel_color = mats['empire_panel_light']['_Color']
        self.assertAlmostEqual(0.50588, panel_color[0], places=5)
        self.assertAlmostEqual(0.50588, panel_color[1], places=5)
        self.assertAlmostEqual(0.50588, panel_color[2], places=5)
        self.assertAlmostEqual(1.00000, panel_color[3], places=5)
        self.assertEqual(4, len(mats))
        self.assertEqual([2, 3, 3, 3], [len(cols) for cols in mats.values()])


class MaterialV2Test(Base.WriteReadTest):

    filename = "tests/in/fragment/material v2.frag"

    read_obj = MaterialFragment

    def verify_obj(self, frag):
        mats = frag.materials
        color = mats['KillGridInfinitePlaneEditorPreview']['_Color']
        self.assertAlmostEqual(0.955882, color[0], places=5)
        self.assertAlmostEqual(0.063257, color[1], places=5)
        self.assertAlmostEqual(0.063257, color[2], places=5)
        self.assertAlmostEqual(0.443137, color[3], places=5)
        self.assertEqual(1, len(mats))


class PopupBlockerLogicTest(Base.WriteReadTest):

    filename = "tests/in/fragment/popupblockerlogic empire.frag"

    read_obj = PopupBlockerLogicFragment

    def verify_obj(self, frag):
        props = frag.props
        self.assertEqual(SKIP_BYTES, props['HoloDistance'])
        self.assertEqual(8, len(props))


class ObjectSpawnCircleTest(Base.WriteReadTest):

    filename = "tests/in/fragment/objectspawncircle lightningspawner.frag"


    read_obj = ObjectSpawnCircleFragment