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 DstOptional(subcon, otherwise=None):
def _get_subcons(con):
    def __init__(cls, name, bases, dct):
    def _init_defaults(self):
    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 construct_property(cls, name, doc=None):
    def fget(self):
    def fset(self, value):
    def fdel(self):
def ExposeConstructFields(target=None, only=None):
    def decorate(target):
            def pop_name(con):
class ConstructMeta(type):
class BaseConstructFragment(Fragment, metaclass=ConstructMeta): def format_duration(msec):
    if msec is None:
        return "None"
    if math.isnan(msec):
        return "NaN"
    if not isinstance(msec, int):
        msec = int(msec)
    negative = msec < 0
    if negative:
        msec = -msec
    hours = ""
    if msec >= 3600000:
        hours = f"{msec // 3600000}:"
        msec = msec % 3600000
    mins = msec // 60000
    msec %= 60000
    sec = msec // 1000
    msec %= 1000
    return f"{'-' if negative else ''}{hours}{mins:02}:{sec:02}.{msec:03}" def format_duration_dhms(msec):
    if msec is None:
        return "None"
    if math.isnan(msec):
        return "NaN"
    if not isinstance(msec, int):
        msec = int(msec)
    if msec == 0:
        return "0"
    result = []
    if msec < 0:
        msec = -msec
        result.append("-")
    for n, unit in ("d", 86400000), ("h", 3600000), ("m", 60000), ("s", 1000), ("ms", 1):
        v = msec // unit
        msec %= unit
        if v:
            result.append(f"{v}{n}")
    return ' '.join(result) def format_distance(meters):
    if math.isnan(meters):
        return "NaN"
    km = int(meters) / 1000.0
    return f"{km} km" def format_speed(kph):
    return f"{kph:.02f} km/h" class Completion(FancyEnum):

    UNPLAYED = 0
    STARTED = 1
    COMPLETED = 2
    BRONZE = 3
    SILVER = 4
    GOLD = 5
    DIAMOND = 6

    Names = {
        UNPLAYED: "Unplayed",
        STARTED: "Did not finish",
        COMPLETED: "Completed",
        BRONZE: "Bronze",
        SILVER: "Silver",
        GOLD: "Gold",
        DIAMOND: "Diamond",
    } class Mode(FancyEnum):

    SPRINT = 1
    STUNT = 2
    SOCCER = 3
    FREE_ROAM = 4
    TAG = 5
    CHALLENGE = 8
    ADVENTURE = 9
    SPEED_AND_STYLE = 10
    TRACKMOGRIFY = 11
    MAIN_MENU = 13
    LOST_TO_ECHOES = 14

    Names = {
        SPRINT: "Sprint",
        STUNT: "Stunt",
        SOCCER: "Soccer",
        FREE_ROAM: "Free Roam",
        TAG: "Reverse Tag",
        CHALLENGE: "Challenge",
        ADVENTURE: "Adventure",
        SPEED_AND_STYLE: "Speed and Style",
        TRACKMOGRIFY: "Trackmogrify",
        MAIN_MENU: "Main Menu",
        LOST_TO_ECHOES: "Lost to Echoes",
    } TIMED_MODES = {Mode.SPRINT, Mode.SPEED_AND_STYLE, Mode.CHALLENGE} class CollectorGroup(object):

    """A group of ClassCollector.

    Used to register classes of modules for later collection by a
    ClassesRegistry.

    Categories are accessed via attribute access. New categories are created on
    first access.

    """

    def __init__(self):
        self._colls = {}

    def __getattr__(self, name):
        try:
            return self._colls[name]
        except KeyError:
            coll = ClassCollector()
            self._colls[name] = coll
            setattr(self, name, coll)
            return coll
from itertools import islice
from construct import (
    Struct, Default, Computed, PrefixedArray, StopIf, If, Rebuild,
    Bytes,
    this, len_,
)
from distance.bytes import Magic, Section
from distance.construct import (
    BaseConstructFragment,
    UInt, Int, Double, Long, DstString, Remainder, MagicConst, DstOptional,
)
from distance.printing import (
    format_duration, format_duration_dhms,
    format_distance, format_speed,
)
from distance.constants import Completion, Mode, TIMED_MODES
from distance.classes import CollectorGroup
    def _visit_print_data(self, p):
        yield super()._visit_print_data(p)
        levels = self.levels
        p(f"Level count: {len(levels)}")
        with p.tree_children(len(levels)):
            for level in levels:
                p.tree_next_child()
                p(f"Level path: {level.level_path!r}")
                for mode, (score, comp) in enumerate(zip(level.scores, level.completion)):
                    if comp != Completion.UNPLAYED:
                        p(format_score(mode, score, comp))
        _print_stringentries(p, "Unlocked Levels", "Levels", self.officials, num_per_row=5)
        _print_stringentries(p, "Found tricks", "Tricks", self.tricks, num_per_row=5)
        _print_stringentries(p, "Unlocked adventure stages", "Level", self.unlocked_adventures)
        _print_stringentries(p, "Some levels", "Level", self.somelevels)
        if levels:
            comps = [0] * 4
            total = 0
            for level in levels:
                for score, comp in zip(level.scores, level.completion):
                    comp -= Completion.BRONZE
                    if comp >= 0:
                        total += comp + 1
                        comps[comp] += 1
            p(f"Medal points: {total}")
            with p.tree_children(len(comps)):
                for comp, num in enumerate(comps, Completion.BRONZE):
                    p.tree_next_child()
                    p(f"{Completion.to_name(comp)} medals: {num}")



@Classes.fragments.fragment(any_version=True)
class ProfileStatsFragment(BaseConstructFragment):

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

    is_interesting = True

    _construct_ = Struct(
        'version' / Computed(this._params.sec.version),
        'unk_0' / Bytes(4),
        'deaths' / Long,
        'laser_deaths' / Long,
        'reset_deaths' / Long,
        'impact_deaths' / Long,
        'overheat_deaths' / Long,
        'killgrid_deaths' / Long,
        'gibs_seconds' / Double,
        'driven_meters' / Double,
        'forward_meters' / Double,
        'reverse_meters' / Double,
        'air_fly_meters' / Double,
        'air_nofly_meters' / Double,
        'wallride_meters' / Double,
        'ceilingride_meters' / Double,
        'grinding_meters' / Double,
        'boost_seconds' / Double,
        'grip_seconds' / Double,
        'splits' / Long,
        'impacts' / Long,
        'checkpoints' / Long,
        'jumps' / Long,
        'wings' / Long,
        'wings_closes' / Long,
        'horns' / Long,
        'tricks' / Long,
        'ev' / Long,
        'lamps' / Long,
        'pumpkins' / Long,
        'eggs' / Long,
        'top_speed_total' / Double,
        'top_speed_forward' / Double,
        'top_speed_back' / Double,
        'cooldowns' / Long,
        'total_seconds' / Double,
        'editor_working_seconds' / Double,
        'editor_playing_seconds' / Double,
        MagicConst(1),
        'offline_times' / Default(PrefixedArray(UInt, Double), ()),
        MagicConst(1),
        'modes_unknown' / Default(PrefixedArray(UInt, Long), ()),
        MagicConst(1),
        'online_times' / Default(PrefixedArray(UInt, Double), ()),
        StopIf(this.version < 1),
        'unk_6' / Bytes(8),
        MagicConst(1),
        'trackmogrify_mods' / Default(PrefixedArray(UInt, DstString), ()),
    )

    def _visit_print_data(self, p):
        yield super()._visit_print_data(p)

        def ps(stat, name):
            value = getattr(self, stat)
            if value is None:
                val_str = "None"
            elif stat.endswith('_seconds'):
                val_str = format_duration_dhms(value * 1000)
            elif stat.endswith('_meters'):
                val_str = format_distance(value)
            elif stat == 'ev':
                val_str = f"{value:,d} eV"
            else:
                val_str = repr(value)
            p(f"{name}: {val_str}")

        ps('total_seconds', "All play time")
        with p.tree_children():
            total_in_modes = sum(self.offline_times) + sum(self.online_times)
            p.tree_next_child()
            p(f"Total time in modes: {format_duration_dhms(total_in_modes * 1000)}")
            total_offline = sum(self.offline_times)
            total_online = sum(self.online_times)
            p.tree_next_child()
            p(f"Total time in offline modes: {format_duration_dhms(total_offline * 1000)}")
            with p.tree_children():
                for mode, time in enumerate(self.offline_times):
                    if time >= 0.001:
                        p.tree_next_child()

                        p(f"Time playing {Mode.to_name(mode)} offline: {format_duration_dhms(time * 1000)}")