Exemple #1
0
 def __init__(self, slot_index_attr, restriction_type):
     # This attribute's value on holder
     # represents their index of slot
     self.__slot_index_attr = slot_index_attr
     self.__restriction_type = restriction_type
     # All holders which possess index of slot
     # are stored in this container
     # Format: {slot index: {holders}}
     self.__slotted_holders = KeyedSet()
Exemple #2
0
 def __init__(self, max_group_attr, restriction_type):
     # Attribute ID whose value contains group restriction
     # of holder
     self.__max_group_attr = max_group_attr
     self.__restriction_type = restriction_type
     # Container for all tracked holders, keyed
     # by their group ID
     # Format: {group ID: {holders}}
     self.__group_all = KeyedSet()
     # Container for holders, which have max group
     # restriction to become operational
     # Format: {holders}
     self.__group_restricted = set()
Exemple #3
0
class SlotIndexRegister(RestrictionRegister):
    """
    Class which implements common functionality for all
    registers, which track indices of occupied slots and
    disallow multiple holders reside within slot with the
    same index.
    """

    def __init__(self, slot_index_attr, restriction_type):
        # This attribute's value on holder
        # represents their index of slot
        self.__slot_index_attr = slot_index_attr
        self.__restriction_type = restriction_type
        # All holders which possess index of slot
        # are stored in this container
        # Format: {slot index: {holders}}
        self.__slotted_holders = KeyedSet()

    def register_holder(self, holder):
        # Skip items which don't have index specifier
        slot_index = holder.item.attributes.get(self.__slot_index_attr)
        if slot_index is None:
            return
        self.__slotted_holders.add_data(slot_index, holder)

    def unregister_holder(self, holder):
        slot_index = holder.item.attributes.get(self.__slot_index_attr)
        if slot_index is None:
            return
        self.__slotted_holders.rm_data(slot_index, holder)

    def validate(self):
        tainted_holders = {}
        for slot_index in self.__slotted_holders:
            slot_index_holders = self.__slotted_holders[slot_index]
            # If more than one item occupies the same slot, all
            # holders in this slot are tainted
            if len(slot_index_holders) > 1:
                for holder in slot_index_holders:
                    tainted_holders[holder] = SlotIndexErrorData(holder_slot_index=slot_index)
        if tainted_holders:
            raise RegisterValidationError(tainted_holders)

    @property
    def restriction_type(self):
        return self.__restriction_type
Exemple #4
0
 def __init__(self, max_group_attr, restriction_type):
     # Attribute ID whose value contains group restriction
     # of holder
     self.__max_group_attr = max_group_attr
     self.__restriction_type = restriction_type
     # Container for all tracked holders, keyed
     # by their group ID
     # Format: {group ID: {holders}}
     self.__group_all = KeyedSet()
     # Container for holders, which have max group
     # restriction to become operational
     # Format: {holders}
     self.__group_restricted = set()
Exemple #5
0
    def __init__(self, fit):
        # Link tracker which is assigned to fit we're
        # keeping data for
        self._fit = fit

        # Keep track of holders belonging to certain domain
        # Format: {domain: {targetHolders}}
        self.__affectee_domain = KeyedSet()

        # Keep track of holders belonging to certain domain and group
        # Format: {(domain, group): {targetHolders}}
        self.__affectee_domain_group = KeyedSet()

        # Keep track of holders belonging to certain domain and having certain skill requirement
        # Format: {(domain, skill): {targetHolders}}
        self.__affectee_domain_skill = KeyedSet()

        # Keep track of affectors influencing all holders belonging to certain domain
        # Format: {domain: {affectors}}
        self.__affector_domain = KeyedSet()

        # Keep track of affectors influencing holders belonging to certain domain and group
        # Format: {(domain, group): {affectors}}
        self.__affector_domain_group = KeyedSet()

        # Keep track of affectors influencing holders belonging to certain domain and having certain skill requirement
        # Format: {(domain, skill): {affectors}}
        self.__affector_domain_skill = KeyedSet()

        # Keep track of affectors influencing holders directly
        # Format: {targetHolder: {affectors}}
        self.__active_direct_affectors = KeyedSet()

        # Keep track of affectors which influence something directly,
        # but are disabled as their target domain is not available
        # Format: {source_holder: {affectors}}
        self.__disabled_direct_affectors = KeyedSet()
Exemple #6
0
class MutableAttributeMap:
    """
    Calculate, store and provide access to modified attribute values.

    Required arguments:
    holder -- holder, to which this map is assigned
    """

    def __init__(self, holder):
        # Reference to holder for internal needs
        self.__holder = holder
        # Actual container of calculated attributes
        # Format: {attribute ID: value}
        self.__modified_attributes = {}
        # This variable stores map of attributes which cap
        # something, and attributes capped by them. Initialized
        # to None to not waste memory, will be changed to dict
        # when needed.
        # Format {capping attribute ID: {capped attribute IDs}}
        self._cap_map = None

    def __getitem__(self, attr):
        # Special handling for skill level attribute
        if attr == Attribute.skill_level:
            # Attempt to return level attribute of holder
            try:
                val = self.__holder.level
            # Try regular way of getting attribute, if accessing
            # level attribute failed
            except AttributeError:
                pass
            else:
                return val
        # If carrier holder isn't assigned to any fit, then
        # we can use just item's original attributes
        if self.__holder._fit is None:
            val = self.__holder.item.attributes[attr]
            return val
        # If value is stored, it's considered valid
        try:
            val = self.__modified_attributes[attr]
        # Else, we have to run full calculation process
        except KeyError:
            try:
                val = self.__modified_attributes[attr] = self.__calculate(attr)
            except BaseValueError as e:
                msg = 'unable to find base value for attribute {} on item {}'.format(
                    e.args[0], self.__holder.item.id)
                signature = (type(e), self.__holder.item.id, e.args[0])
                self.__holder._fit.eos._logger.warning(msg, child_name='attribute_calculator', signature=signature)
                raise KeyError(attr) from e
            except AttributeMetaError as e:
                msg = 'unable to fetch metadata for attribute {}, requested for item {}'.format(
                    e.args[0], self.__holder.item.id)
                signature = (type(e), self.__holder.item.id, e.args[0])
                self.__holder._fit.eos._logger.error(msg, child_name='attribute_calculator', signature=signature)
                raise KeyError(attr) from e
            self.__holder._fit._link_tracker.clear_holder_attribute_dependents(self.__holder, attr)
        return val

    def __len__(self):
        return len(self.keys())

    def __contains__(self, attr):
        # Seek for attribute in both modified attribute container
        # and original item attributes
        result = attr in self.__modified_attributes or attr in self.__holder.item.attributes
        return result

    def __iter__(self):
        for k in self.keys():
            yield k

    def __delitem__(self, attr):
        # Clear the value in our calculated attributes dictionary
        try:
            del self.__modified_attributes[attr]
        # Do nothing if it wasn't calculated
        except KeyError:
            pass
        # And make sure all other attributes relying on it
        # are cleared too
        else:
            self.__holder._fit._link_tracker.clear_holder_attribute_dependents(self.__holder, attr)

    def __setitem__(self, attr, value):
        # Write value and clear all attributes relying on it
        self.__modified_attributes[attr] = value
        self.__holder._fit._link_tracker.clear_holder_attribute_dependents(self.__holder, attr)

    def get(self, attr, default=None):
        try:
            return self[attr]
        except KeyError:
            return default

    def keys(self):
        # Return union of both dicts
        return self.__modified_attributes.keys() | self.__holder.item.attributes.keys()

    def clear(self):
        """Reset map to its initial state."""
        self.__modified_attributes.clear()
        self._cap_map = None

    def __calculate(self, attr):
        """
        Run calculations to find the actual value of attribute.

        Required arguments:
        attr -- ID of attribute to be calculated

        Return value:
        Calculated attribute value

        Possible exceptions:
        BaseValueError -- attribute cannot be calculated, as its
        base value is not available
        """
        # Attribute object for attribute being calculated
        try:
            attr_meta = self.__holder._fit.eos._cache_handler.get_attribute(attr)
        # Raise error if we can't get to get_attribute method
        # or it can't find requested attribute
        except (AttributeError, AttributeFetchError) as e:
            raise AttributeMetaError(attr) from e
        # Base attribute value which we'll use for modification
        try:
            result = self.__holder.item.attributes[attr]
        # If attribute isn't available on base item,
        # base off its default value
        except KeyError:
            result = attr_meta.default_value
            # If original attribute is not specified and default
            # value isn't available, raise error - without valid
            # base we can't go on
            if result is None:
                raise BaseValueError(attr)
        # Container for non-penalized modifiers
        # Format: {operator: [values]}
        normal_mods = {}
        # Container for penalized modifiers
        # Format: {operator: [values]}
        penalized_mods = {}
        # Now, go through all affectors affecting our holder
        for affector in self.__holder._fit._link_tracker.get_affectors(self.__holder, attr=attr):
            try:
                source_holder, modifier = affector
                operator = modifier.operator
                # Decide if it should be stacking penalized or not, based on stackable property,
                # source item category and operator
                penalize = (
                    attr_meta.stackable is False and
                    source_holder.item.category not in PENALTY_IMMUNE_CATEGORIES and
                    operator in PENALIZABLE_OPERATORS
                )
                try:
                    mod_value = source_holder.attributes[modifier.src_attr]
                # Silently skip current affector: error should already
                # be logged by map before it raised KeyError
                except KeyError:
                    continue
                # Normalize operations to just three types:
                # assignments, additions, multiplications
                try:
                    normalization_func = NORMALIZATION_MAP[operator]
                # Raise error on any unknown operator types
                except KeyError as e:
                    raise OperatorError(operator) from e
                mod_value = normalization_func(mod_value)
                # Add value to appropriate dictionary
                if penalize is True:
                    mod_list = penalized_mods.setdefault(operator, [])
                else:
                    mod_list = normal_mods.setdefault(operator, [])
                mod_list.append(mod_value)
            # Handle operator type failure
            except OperatorError as e:
                msg = 'malformed modifier on item {}: unknown operator {}'.format(
                    source_holder.item.id, e.args[0])
                signature = (type(e), source_holder.item.id, e.args[0])
                self.__holder._fit.eos._logger.warning(msg, child_name='attribute_calculator', signature=signature)
                continue
        # When data gathering is complete, process penalized modifiers
        # They are penalized on per-operator basis
        for operator, mod_list in penalized_mods.items():
            penalized_value = self.__penalize_values(mod_list)
            mod_list = normal_mods.setdefault(operator, [])
            mod_list.append(penalized_value)
        # Calculate result of normal dictionary, according to operator order
        for operator in sorted(normal_mods):
            mod_list = normal_mods[operator]
            # Pick best modifier for assignments, based on high_is_good value
            if operator in ASSIGNMENTS:
                result = max(mod_list) if attr_meta.high_is_good is True else min(mod_list)
            elif operator in ADDITIONS:
                for mod_val in mod_list:
                    result += mod_val
            elif operator in MULTIPLICATIONS:
                for mod_val in mod_list:
                    result *= mod_val
        # If attribute has upper cap, do not let
        # its value to grow above it
        if attr_meta.max_attribute is not None:
            try:
                max_value = self[attr_meta.max_attribute]
            # If max value isn't available, don't
            # cap anything
            except KeyError:
                pass
            else:
                result = min(result, max_value)
                # Let map know that capping attribute
                # restricts current attribute
                if self._cap_map is None:
                    self._cap_map = KeyedSet()
                # Fill cap map with data: capping attribute and capped attribute
                self._cap_map.add_data(attr_meta.max_attribute, attr)
        # Some of attributes are rounded for whatever reason,
        # deal with it after all the calculations
        if attr in LIMITED_PRECISION:
            result = round(result, 2)
        return result

    def __penalize_values(self, mod_list):
        """
        Calculate aggregated factor of passed factors, taking into
        consideration stacking penalty.

        Positional argument:
        mod_list -- list of factors

        Return value:
        Final aggregated factor of passed mod_list
        """
        # Gather positive modifiers into one chain, negative
        # into another
        chain_positive = []
        chain_negative = []
        for mod_val in mod_list:
            # Transform value into form of multiplier - 1 for ease of
            # stacking chain calculation
            mod_val -= 1
            if mod_val >= 0:
                chain_positive.append(mod_val)
            else:
                chain_negative.append(mod_val)
        # Strongest modifiers always go first
        chain_positive.sort(reverse=True)
        chain_negative.sort()
        # Base final multiplier on 1
        list_result = 1
        for chain in (chain_positive, chain_negative):
            # Same for intermediate per-chain result
            chain_result = 1
            for position, modifier in enumerate(chain):
                # Ignore 12th modifier and further as non-significant
                if position > 10:
                    break
                # Apply stacking penalty based on modifier position
                chain_result *= 1 + modifier * PENALTY_BASE ** (position ** 2)
            list_result *= chain_result
        return list_result
Exemple #7
0
    def __calculate(self, attr):
        """
        Run calculations to find the actual value of attribute.

        Required arguments:
        attr -- ID of attribute to be calculated

        Return value:
        Calculated attribute value

        Possible exceptions:
        BaseValueError -- attribute cannot be calculated, as its
        base value is not available
        """
        # Attribute object for attribute being calculated
        try:
            attr_meta = self.__holder._fit.eos._cache_handler.get_attribute(attr)
        # Raise error if we can't get to get_attribute method
        # or it can't find requested attribute
        except (AttributeError, AttributeFetchError) as e:
            raise AttributeMetaError(attr) from e
        # Base attribute value which we'll use for modification
        try:
            result = self.__holder.item.attributes[attr]
        # If attribute isn't available on base item,
        # base off its default value
        except KeyError:
            result = attr_meta.default_value
            # If original attribute is not specified and default
            # value isn't available, raise error - without valid
            # base we can't go on
            if result is None:
                raise BaseValueError(attr)
        # Container for non-penalized modifiers
        # Format: {operator: [values]}
        normal_mods = {}
        # Container for penalized modifiers
        # Format: {operator: [values]}
        penalized_mods = {}
        # Now, go through all affectors affecting our holder
        for affector in self.__holder._fit._link_tracker.get_affectors(self.__holder, attr=attr):
            try:
                source_holder, modifier = affector
                operator = modifier.operator
                # Decide if it should be stacking penalized or not, based on stackable property,
                # source item category and operator
                penalize = (
                    attr_meta.stackable is False and
                    source_holder.item.category not in PENALTY_IMMUNE_CATEGORIES and
                    operator in PENALIZABLE_OPERATORS
                )
                try:
                    mod_value = source_holder.attributes[modifier.src_attr]
                # Silently skip current affector: error should already
                # be logged by map before it raised KeyError
                except KeyError:
                    continue
                # Normalize operations to just three types:
                # assignments, additions, multiplications
                try:
                    normalization_func = NORMALIZATION_MAP[operator]
                # Raise error on any unknown operator types
                except KeyError as e:
                    raise OperatorError(operator) from e
                mod_value = normalization_func(mod_value)
                # Add value to appropriate dictionary
                if penalize is True:
                    mod_list = penalized_mods.setdefault(operator, [])
                else:
                    mod_list = normal_mods.setdefault(operator, [])
                mod_list.append(mod_value)
            # Handle operator type failure
            except OperatorError as e:
                msg = 'malformed modifier on item {}: unknown operator {}'.format(
                    source_holder.item.id, e.args[0])
                signature = (type(e), source_holder.item.id, e.args[0])
                self.__holder._fit.eos._logger.warning(msg, child_name='attribute_calculator', signature=signature)
                continue
        # When data gathering is complete, process penalized modifiers
        # They are penalized on per-operator basis
        for operator, mod_list in penalized_mods.items():
            penalized_value = self.__penalize_values(mod_list)
            mod_list = normal_mods.setdefault(operator, [])
            mod_list.append(penalized_value)
        # Calculate result of normal dictionary, according to operator order
        for operator in sorted(normal_mods):
            mod_list = normal_mods[operator]
            # Pick best modifier for assignments, based on high_is_good value
            if operator in ASSIGNMENTS:
                result = max(mod_list) if attr_meta.high_is_good is True else min(mod_list)
            elif operator in ADDITIONS:
                for mod_val in mod_list:
                    result += mod_val
            elif operator in MULTIPLICATIONS:
                for mod_val in mod_list:
                    result *= mod_val
        # If attribute has upper cap, do not let
        # its value to grow above it
        if attr_meta.max_attribute is not None:
            try:
                max_value = self[attr_meta.max_attribute]
            # If max value isn't available, don't
            # cap anything
            except KeyError:
                pass
            else:
                result = min(result, max_value)
                # Let map know that capping attribute
                # restricts current attribute
                if self._cap_map is None:
                    self._cap_map = KeyedSet()
                # Fill cap map with data: capping attribute and capped attribute
                self._cap_map.add_data(attr_meta.max_attribute, attr)
        # Some of attributes are rounded for whatever reason,
        # deal with it after all the calculations
        if attr in LIMITED_PRECISION:
            result = round(result, 2)
        return result
Exemple #8
0
class MaxGroupRegister(RestrictionRegister):
    """
    Class which implements common functionality for all
    registers, which track maximum number of belonging to
    ship holders in certain state on per-group basis.
    """

    def __init__(self, max_group_attr, restriction_type):
        # Attribute ID whose value contains group restriction
        # of holder
        self.__max_group_attr = max_group_attr
        self.__restriction_type = restriction_type
        # Container for all tracked holders, keyed
        # by their group ID
        # Format: {group ID: {holders}}
        self.__group_all = KeyedSet()
        # Container for holders, which have max group
        # restriction to become operational
        # Format: {holders}
        self.__group_restricted = set()

    def register_holder(self, holder):
        # Ignore holders which do not belong to ship
        if holder._domain != Domain.ship:
            return
        group = holder.item.group
        # Ignore holders, whose item isn't assigned
        # to any group
        if group is None:
            return
        # Having group ID is sufficient condition
        # to enter container of all fitted holders
        self.__group_all.add_data(group, holder)
        # To enter restriction container, original
        # item must have restriction attribute
        if self.__max_group_attr not in holder.item.attributes:
            return
        self.__group_restricted.add(holder)

    def unregister_holder(self, holder):
        # Just clear data containers
        group = holder.item.group
        self.__group_all.rm_data(group, holder)
        self.__group_restricted.discard(holder)

    def validate(self):
        # Container for tainted holders
        tainted_holders = {}
        # Go through all restricted holders
        for holder in self.__group_restricted:
            # Get number of registered holders, assigned to group of current
            # restricted holder, and holder's restriction value
            group = holder.item.group
            group_holders = len(self.__group_all.get(group) or ())
            max_group_restriction = holder.item.attributes[self.__max_group_attr]
            # If number of registered holders from this group is bigger,
            # then current holder is tainted
            if group_holders > max_group_restriction:
                tainted_holders[holder] = MaxGroupErrorData(
                    holder_group=group,
                    max_group=max_group_restriction,
                    group_holders=group_holders
                )
        # Raise error if we detected any tainted holders
        if tainted_holders:
            raise RegisterValidationError(tainted_holders)

    @property
    def restriction_type(self):
        return self.__restriction_type
Exemple #9
0
class MutableAttributeMap:
    """
    Calculate, store and provide access to modified attribute values.

    Required arguments:
    holder -- holder, to which this map is assigned
    """
    def __init__(self, holder):
        # Reference to holder for internal needs
        self.__holder = holder
        # Actual container of calculated attributes
        # Format: {attribute ID: value}
        self.__modified_attributes = {}
        # This variable stores map of attributes which cap
        # something, and attributes capped by them. Initialized
        # to None to not waste memory, will be changed to dict
        # when needed.
        # Format {capping attribute ID: {capped attribute IDs}}
        self._cap_map = None

    def __getitem__(self, attr):
        # Special handling for skill level attribute
        if attr == Attribute.skill_level:
            # Attempt to return level attribute of holder
            try:
                val = self.__holder.level
            # Try regular way of getting attribute, if accessing
            # level attribute failed
            except AttributeError:
                pass
            else:
                return val
        # If carrier holder isn't assigned to any fit, then
        # we can use just item's original attributes
        if self.__holder._fit is None:
            val = self.__holder.item.attributes[attr]
            return val
        # If value is stored, it's considered valid
        try:
            val = self.__modified_attributes[attr]
        # Else, we have to run full calculation process
        except KeyError:
            try:
                val = self.__modified_attributes[attr] = self.__calculate(attr)
            except BaseValueError as e:
                msg = 'unable to find base value for attribute {} on item {}'.format(
                    e.args[0], self.__holder.item.id)
                logger.warning(msg)
                raise KeyError(attr) from e
            except AttributeMetaError as e:
                msg = 'unable to fetch metadata for attribute {}, requested for item {}'.format(
                    e.args[0], self.__holder.item.id)
                logger.error(msg)
                raise KeyError(attr) from e
            self.__holder._fit._link_tracker.clear_holder_attribute_dependents(
                self.__holder, attr)
        return val

    def __len__(self):
        return len(self.keys())

    def __contains__(self, attr):
        # Seek for attribute in both modified attribute container
        # and original item attributes
        result = attr in self.__modified_attributes or attr in self.__holder.item.attributes
        return result

    def __iter__(self):
        for k in self.keys():
            yield k

    def __delitem__(self, attr):
        # Clear the value in our calculated attributes dictionary
        try:
            del self.__modified_attributes[attr]
        # Do nothing if it wasn't calculated
        except KeyError:
            pass
        # And make sure all other attributes relying on it
        # are cleared too
        else:
            self.__holder._fit._link_tracker.clear_holder_attribute_dependents(
                self.__holder, attr)

    def __setitem__(self, attr, value):
        # Write value and clear all attributes relying on it
        self.__modified_attributes[attr] = value
        self.__holder._fit._link_tracker.clear_holder_attribute_dependents(
            self.__holder, attr)

    def get(self, attr, default=None):
        try:
            return self[attr]
        except KeyError:
            return default

    def keys(self):
        # Return union of both dicts
        return self.__modified_attributes.keys(
        ) | self.__holder.item.attributes.keys()

    def clear(self):
        """Reset map to its initial state."""
        self.__modified_attributes.clear()
        self._cap_map = None

    def __calculate(self, attr):
        """
        Run calculations to find the actual value of attribute.

        Required arguments:
        attr -- ID of attribute to be calculated

        Return value:
        Calculated attribute value

        Possible exceptions:
        BaseValueError -- attribute cannot be calculated, as its
        base value is not available
        """
        # Assign base item attributes first to make sure than in case when
        # we're calculating attribute for item/fit without source, it fails
        # with null source error (triggered by accessing item's attribute)
        item_attrs = self.__holder.item.attributes
        # Attribute object for attribute being calculated
        try:
            attr_meta = self.__holder._fit.source.cache_handler.get_attribute(
                attr)
        # Raise error if we can't get metadata for requested attribute
        except (AttributeError, AttributeFetchError) as e:
            raise AttributeMetaError(attr) from e
        # Base attribute value which we'll use for modification
        try:
            result = item_attrs[attr]
        # If attribute isn't available on base item,
        # base off its default value
        except KeyError:
            result = attr_meta.default_value
            # If original attribute is not specified and default
            # value isn't available, raise error - without valid
            # base we can't go on
            if result is None:
                raise BaseValueError(attr)
        # Container for non-penalized modifiers
        # Format: {operator: [values]}
        normal_mods = {}
        # Container for penalized modifiers
        # Format: {operator: [values]}
        penalized_mods = {}
        # Now, go through all affectors affecting our holder
        for affector in self.__holder._fit._link_tracker.get_affectors(
                self.__holder, attr=attr):
            try:
                source_holder, modifier = affector
                operator = modifier.operator
                # Decide if it should be stacking penalized or not, based on stackable property,
                # source item category and operator
                penalize = (attr_meta.stackable is False
                            and source_holder.item.category
                            not in PENALTY_IMMUNE_CATEGORIES
                            and operator in PENALIZABLE_OPERATORS)
                try:
                    mod_value = source_holder.attributes[modifier.src_attr]
                # Silently skip current affector: error should already
                # be logged by map before it raised KeyError
                except KeyError:
                    continue
                # Normalize operations to just three types:
                # assignments, additions, multiplications
                try:
                    normalization_func = NORMALIZATION_MAP[operator]
                # Raise error on any unknown operator types
                except KeyError as e:
                    raise OperatorError(operator) from e
                mod_value = normalization_func(mod_value)
                # Add value to appropriate dictionary
                if penalize is True:
                    mod_list = penalized_mods.setdefault(operator, [])
                else:
                    mod_list = normal_mods.setdefault(operator, [])
                mod_list.append(mod_value)
            # Handle operator type failure
            except OperatorError as e:
                msg = 'malformed modifier on item {}: unknown operator {}'.format(
                    source_holder.item.id, e.args[0])
                logger.warning(msg)
                continue
        # When data gathering is complete, process penalized modifiers
        # They are penalized on per-operator basis
        for operator, mod_list in penalized_mods.items():
            penalized_value = self.__penalize_values(mod_list)
            mod_list = normal_mods.setdefault(operator, [])
            mod_list.append(penalized_value)
        # Calculate result of normal dictionary, according to operator order
        for operator in sorted(normal_mods):
            mod_list = normal_mods[operator]
            # Pick best modifier for assignments, based on high_is_good value
            if operator in ASSIGNMENTS:
                result = max(
                    mod_list) if attr_meta.high_is_good is True else min(
                        mod_list)
            elif operator in ADDITIONS:
                for mod_val in mod_list:
                    result += mod_val
            elif operator in MULTIPLICATIONS:
                for mod_val in mod_list:
                    result *= mod_val
        # If attribute has upper cap, do not let
        # its value to grow above it
        if attr_meta.max_attribute is not None:
            try:
                max_value = self[attr_meta.max_attribute]
            # If max value isn't available, don't
            # cap anything
            except KeyError:
                pass
            else:
                result = min(result, max_value)
                # Let map know that capping attribute
                # restricts current attribute
                if self._cap_map is None:
                    self._cap_map = KeyedSet()
                # Fill cap map with data: capping attribute and capped attribute
                self._cap_map.add_data(attr_meta.max_attribute, attr)
        # Some of attributes are rounded for whatever reason,
        # deal with it after all the calculations
        if attr in LIMITED_PRECISION:
            result = round(result, 2)
        return result

    def __penalize_values(self, mod_list):
        """
        Calculate aggregated factor of passed factors, taking into
        consideration stacking penalty.

        Positional argument:
        mod_list -- list of factors

        Return value:
        Final aggregated factor of passed mod_list
        """
        # Gather positive modifiers into one chain, negative
        # into another
        chain_positive = []
        chain_negative = []
        for mod_val in mod_list:
            # Transform value into form of multiplier - 1 for ease of
            # stacking chain calculation
            mod_val -= 1
            if mod_val >= 0:
                chain_positive.append(mod_val)
            else:
                chain_negative.append(mod_val)
        # Strongest modifiers always go first
        chain_positive.sort(reverse=True)
        chain_negative.sort()
        # Base final multiplier on 1
        list_result = 1
        for chain in (chain_positive, chain_negative):
            # Same for intermediate per-chain result
            chain_result = 1
            for position, modifier in enumerate(chain):
                # Ignore 12th modifier and further as non-significant
                if position > 10:
                    break
                # Apply stacking penalty based on modifier position
                chain_result *= 1 + modifier * PENALTY_BASE**(position**2)
            list_result *= chain_result
        return list_result
Exemple #10
0
    def __calculate(self, attr):
        """
        Run calculations to find the actual value of attribute.

        Required arguments:
        attr -- ID of attribute to be calculated

        Return value:
        Calculated attribute value

        Possible exceptions:
        BaseValueError -- attribute cannot be calculated, as its
        base value is not available
        """
        # Assign base item attributes first to make sure than in case when
        # we're calculating attribute for item/fit without source, it fails
        # with null source error (triggered by accessing item's attribute)
        item_attrs = self.__holder.item.attributes
        # Attribute object for attribute being calculated
        try:
            attr_meta = self.__holder._fit.source.cache_handler.get_attribute(
                attr)
        # Raise error if we can't get metadata for requested attribute
        except (AttributeError, AttributeFetchError) as e:
            raise AttributeMetaError(attr) from e
        # Base attribute value which we'll use for modification
        try:
            result = item_attrs[attr]
        # If attribute isn't available on base item,
        # base off its default value
        except KeyError:
            result = attr_meta.default_value
            # If original attribute is not specified and default
            # value isn't available, raise error - without valid
            # base we can't go on
            if result is None:
                raise BaseValueError(attr)
        # Container for non-penalized modifiers
        # Format: {operator: [values]}
        normal_mods = {}
        # Container for penalized modifiers
        # Format: {operator: [values]}
        penalized_mods = {}
        # Now, go through all affectors affecting our holder
        for affector in self.__holder._fit._link_tracker.get_affectors(
                self.__holder, attr=attr):
            try:
                source_holder, modifier = affector
                operator = modifier.operator
                # Decide if it should be stacking penalized or not, based on stackable property,
                # source item category and operator
                penalize = (attr_meta.stackable is False
                            and source_holder.item.category
                            not in PENALTY_IMMUNE_CATEGORIES
                            and operator in PENALIZABLE_OPERATORS)
                try:
                    mod_value = source_holder.attributes[modifier.src_attr]
                # Silently skip current affector: error should already
                # be logged by map before it raised KeyError
                except KeyError:
                    continue
                # Normalize operations to just three types:
                # assignments, additions, multiplications
                try:
                    normalization_func = NORMALIZATION_MAP[operator]
                # Raise error on any unknown operator types
                except KeyError as e:
                    raise OperatorError(operator) from e
                mod_value = normalization_func(mod_value)
                # Add value to appropriate dictionary
                if penalize is True:
                    mod_list = penalized_mods.setdefault(operator, [])
                else:
                    mod_list = normal_mods.setdefault(operator, [])
                mod_list.append(mod_value)
            # Handle operator type failure
            except OperatorError as e:
                msg = 'malformed modifier on item {}: unknown operator {}'.format(
                    source_holder.item.id, e.args[0])
                logger.warning(msg)
                continue
        # When data gathering is complete, process penalized modifiers
        # They are penalized on per-operator basis
        for operator, mod_list in penalized_mods.items():
            penalized_value = self.__penalize_values(mod_list)
            mod_list = normal_mods.setdefault(operator, [])
            mod_list.append(penalized_value)
        # Calculate result of normal dictionary, according to operator order
        for operator in sorted(normal_mods):
            mod_list = normal_mods[operator]
            # Pick best modifier for assignments, based on high_is_good value
            if operator in ASSIGNMENTS:
                result = max(
                    mod_list) if attr_meta.high_is_good is True else min(
                        mod_list)
            elif operator in ADDITIONS:
                for mod_val in mod_list:
                    result += mod_val
            elif operator in MULTIPLICATIONS:
                for mod_val in mod_list:
                    result *= mod_val
        # If attribute has upper cap, do not let
        # its value to grow above it
        if attr_meta.max_attribute is not None:
            try:
                max_value = self[attr_meta.max_attribute]
            # If max value isn't available, don't
            # cap anything
            except KeyError:
                pass
            else:
                result = min(result, max_value)
                # Let map know that capping attribute
                # restricts current attribute
                if self._cap_map is None:
                    self._cap_map = KeyedSet()
                # Fill cap map with data: capping attribute and capped attribute
                self._cap_map.add_data(attr_meta.max_attribute, attr)
        # Some of attributes are rounded for whatever reason,
        # deal with it after all the calculations
        if attr in LIMITED_PRECISION:
            result = round(result, 2)
        return result
Exemple #11
0
class LinkRegister:
    """
    Keep track of currently existing links between affectors
    (Affector objects) and affectees (holders). This is hard
    requirement for efficient partial attribute recalculation.
    Register is not aware of links between specific attributes,
    doesn't know anything about states and scopes, just
    affectors and affectees.

    Required arguments:
    fit -- fit, to which this register is bound to
    """

    def __init__(self, fit):
        # Link tracker which is assigned to fit we're
        # keeping data for
        self._fit = fit

        # Keep track of holders belonging to certain domain
        # Format: {domain: {targetHolders}}
        self.__affectee_domain = KeyedSet()

        # Keep track of holders belonging to certain domain and group
        # Format: {(domain, group): {targetHolders}}
        self.__affectee_domain_group = KeyedSet()

        # Keep track of holders belonging to certain domain and having certain skill requirement
        # Format: {(domain, skill): {targetHolders}}
        self.__affectee_domain_skill = KeyedSet()

        # Keep track of affectors influencing all holders belonging to certain domain
        # Format: {domain: {affectors}}
        self.__affector_domain = KeyedSet()

        # Keep track of affectors influencing holders belonging to certain domain and group
        # Format: {(domain, group): {affectors}}
        self.__affector_domain_group = KeyedSet()

        # Keep track of affectors influencing holders belonging to certain domain and having certain skill requirement
        # Format: {(domain, skill): {affectors}}
        self.__affector_domain_skill = KeyedSet()

        # Keep track of affectors influencing holders directly
        # Format: {targetHolder: {affectors}}
        self.__active_direct_affectors = KeyedSet()

        # Keep track of affectors which influence something directly,
        # but are disabled as their target domain is not available
        # Format: {source_holder: {affectors}}
        self.__disabled_direct_affectors = KeyedSet()

    def register_affectee(self, target_holder):
        """
        Add passed target holder to register's maps, so it can be affected by
        other holders properly.

        Required arguments:
        target_holder -- holder to register
        """
        for key, affectee_map in self.__get_affectee_maps(target_holder):
            # Add data to map
            affectee_map.add_data(key, target_holder)
        # Check if we have affectors which should directly influence passed holder,
        # but are disabled; enable them if there're any
        enable_direct = self.__get_holder_direct_domain(target_holder)
        if enable_direct is None:
            return
        if enable_direct == Domain.other:
            self.__enable_direct_other(target_holder)
        elif enable_direct in (Domain.character, Domain.ship):
            self.__enable_direct_spec(target_holder, enable_direct)

    def unregister_affectee(self, target_holder):
        """
        Remove passed target holder from register's maps, so holders affecting
        it "know" that its modification is no longer needed.

        Required arguments:
        target_holder -- holder to unregister
        """
        for key, affectee_map in self.__get_affectee_maps(target_holder):
            affectee_map.rm_data(key, target_holder)
        # When removing holder from register, make sure to move modifiers which
        # originate from 'other' holders and directly affect it to disabled map
        disable_direct = self.__get_holder_direct_domain(target_holder)
        if disable_direct is None:
            return
        if disable_direct == Domain.other:
            self.__disable_direct_other(target_holder)
        elif disable_direct in (Domain.character, Domain.ship):
            self.__disable_direct_spec(target_holder)

    def register_affector(self, affector):
        """
        Add passed affector to register's affector maps, so that new holders
        added to fit know that they should be affected by it.

        Required arguments:
        affector -- affector to register
        """
        try:
            key, affector_map = self.__get_affector_map(affector)
            # Actually add data to map
            affector_map.add_data(key, affector)
        except Exception as e:
            self.__handle_affector_errors(e, affector)

    def unregister_affector(self, affector):
        """
        Remove passed affector from register's affector maps, so that
        holders-affectees "know" that they're no longer affected by it.

        Required arguments:
        affector -- affector to unregister
        """
        try:
            key, affector_map = self.__get_affector_map(affector)
            affector_map.rm_data(key, affector)
        # Following block handles exceptions; all of them must be handled
        # when registering affector too, thus they won't appear in log
        # if logger's handler suppresses messages with duplicate
        # signature
        except Exception as e:
            self.__handle_affector_errors(e, affector)

    def get_affectees(self, affector):
        """
        Get all holders influenced by passed affector.

        Required arguments:
        affector -- affector, for which we're seeking for affectees

        Return value:
        Set with holders, being influenced by affector
        """
        source_holder, modifier = affector
        affectees = set()
        try:
            # For direct modification, make set out of single target domain
            if modifier.filter_type is None:
                if modifier.domain == Domain.self_:
                    target = {source_holder}
                elif modifier.domain == Domain.character:
                    char = self._fit.character
                    target = {char} if char is not None else None
                elif modifier.domain == Domain.ship:
                    ship = self._fit.ship
                    target = {ship} if ship is not None else None
                elif modifier.domain == Domain.other:
                    other_holder = self.__get_other_linked_holder(source_holder)
                    target = {other_holder} if other_holder is not None else None
                else:
                    raise DirectDomainError(modifier.domain)
            # For filtered modifications, pick appropriate dictionary and get set
            # with target holders
            elif modifier.filter_type == FilterType.all_:
                key = self.__contextize_filter_domain(affector)
                target = self.__affectee_domain.get(key) or set()
            elif modifier.filter_type == FilterType.group:
                domain = self.__contextize_filter_domain(affector)
                key = (domain, modifier.filter_value)
                target = self.__affectee_domain_group.get(key) or set()
            elif modifier.filter_type == FilterType.skill:
                domain = self.__contextize_filter_domain(affector)
                skill = affector.modifier.filter_value
                key = (domain, skill)
                target = self.__affectee_domain_skill.get(key) or set()
            elif modifier.filter_type == FilterType.skill_self:
                domain = self.__contextize_filter_domain(affector)
                skill = affector.source_holder.item.id
                key = (domain, skill)
                target = self.__affectee_domain_skill.get(key) or set()
            else:
                raise FilterTypeError(modifier.filter_type)
            # Add our set to affectees
            if target is not None:
                affectees.update(target)
        # If passed affector has already been registered and logger prefers
        # to suppress messages with duplicate signatures, following error handling
        # won't produce new log entries
        except Exception as e:
            self.__handle_affector_errors(e, affector)
        return affectees

    def get_affectors(self, target_holder):
        """
        Get all affectors, which influence passed holder.

        Required arguments:
        target_holder -- holder, for which we're seeking for affecting it
        affectors

        Return value:
        Set with affectors, incluencing target_holder
        """
        affectors = set()
        # Add all affectors which directly affect it
        affectors.update(self.__active_direct_affectors.get(target_holder) or set())
        # Then all affectors which affect domain of passed holder
        domain = target_holder._domain
        affectors.update(self.__affector_domain.get(domain) or set())
        # All affectors which affect domain and group of passed holder
        group = target_holder.item.group
        affectors.update(self.__affector_domain_group.get((domain, group)) or set())
        # Same, but for domain & skill requirement of passed holder
        for skill in target_holder.item.required_skills:
            affectors.update(self.__affector_domain_skill.get((domain, skill)) or set())
        return affectors

    # General-purpose auxiliary methods
    def __get_affectee_maps(self, target_holder):
        """
        Helper for affectee register/unregister methods.

        Required arguments:
        target_holder -- holder, for which affectee maps are requested

        Return value:
        List of (key, affecteeMap) tuples, where key should be used to access
        data set (appropriate to passed target_holder) in affecteeMap
        """
        # Container which temporarily holds (key, map) tuples
        affectee_maps = []
        domain = target_holder._domain
        if domain is not None:
            affectee_maps.append((domain, self.__affectee_domain))
            group = target_holder.item.group
            if group is not None:
                affectee_maps.append(((domain, group), self.__affectee_domain_group))
            for skill in target_holder.item.required_skills:
                affectee_maps.append(((domain, skill), self.__affectee_domain_skill))
        return affectee_maps

    def __get_affector_map(self, affector):
        """
        Helper for affector register/unregister methods.

        Required arguments:
        affector -- affector, for which affector map are requested

        Return value:
        (key, affector_map) tuple, where key should be used to access
        data set (appropriate to passed affector) in affector_map

        Possible exceptions:
        FilteredSelfReferenceError -- raised if affector's modifier specifies
        filtered modification and target domain refers self, but affector's
        holder isn't in position to be target for filtered modifications
        DirectDomainError -- raised when affector's modifier target
        domain is not supported for direct modification
        FilteredDomainError -- raised when affector's modifier target
        domain is not supported for filtered modification
        FilterTypeError -- raised when affector's modifier filter type is not
        supported
        """
        source_holder, modifier = affector
        # For each filter type, define affector map and key to use
        if modifier.filter_type is None:
            # For direct modifications, we need to properly pick
            # target holder (it's key) based on domain
            if modifier.domain == Domain.self_:
                affector_map = self.__active_direct_affectors
                key = source_holder
            elif modifier.domain == Domain.character:
                char = self._fit.character
                if char is not None:
                    affector_map = self.__active_direct_affectors
                    key = char
                else:
                    affector_map = self.__disabled_direct_affectors
                    key = source_holder
            elif modifier.domain == Domain.ship:
                ship = self._fit.ship
                if ship is not None:
                    affector_map = self.__active_direct_affectors
                    key = ship
                else:
                    affector_map = self.__disabled_direct_affectors
                    key = source_holder
            # When other domain is referenced, it means direct reference to module's charge
            # or to charge's module-container
            elif modifier.domain == Domain.other:
                other_holder = self.__get_other_linked_holder(source_holder)
                if other_holder is not None:
                    affector_map = self.__active_direct_affectors
                    key = other_holder
                # When no reference available, it means that e.g. charge may be
                # unavailable for now; use disabled affectors map for these
                else:
                    affector_map = self.__disabled_direct_affectors
                    key = source_holder
            else:
                raise DirectDomainError(modifier.domain)
        # For filtered modifications, compose key, making sure reference to self
        # is converted into appropriate real domain
        elif modifier.filter_type == FilterType.all_:
            affector_map = self.__affector_domain
            domain = self.__contextize_filter_domain(affector)
            key = domain
        elif modifier.filter_type == FilterType.group:
            affector_map = self.__affector_domain_group
            domain = self.__contextize_filter_domain(affector)
            key = (domain, modifier.filter_value)
        elif modifier.filter_type == FilterType.skill:
            affector_map = self.__affector_domain_skill
            domain = self.__contextize_filter_domain(affector)
            skill = affector.modifier.filter_value
            key = (domain, skill)
        elif modifier.filter_type == FilterType.skill_self:
            affector_map = self.__affector_domain_skill
            domain = self.__contextize_filter_domain(affector)
            skill = affector.source_holder.item.id
            key = (domain, skill)
        else:
            raise FilterTypeError(modifier.filter_type)
        return key, affector_map

    def __handle_affector_errors(self, error, affector):
        """
        Multiple register methods which get data based on passed affector
        raise similar exception classes. To handle them in consistent fashion,
        it is done from centralized place - this method. If error cannot be
        handled by method, it is re-raised.

        Required arguments:
        error -- Exception instance which was caught and needs to be handled
        affector -- affector object, which was being processed when error occurred
        """
        if isinstance(error, DirectDomainError):
            msg = 'malformed modifier on item {}: unsupported target domain {} for direct modification'.format(
                affector.source_holder.item.id, error.args[0])
            signature = (type(error), affector.source_holder.item.id, error.args[0])
            self._fit.eos._logger.warning(msg, child_name='attribute_calculator', signature=signature)
        elif isinstance(error, FilteredDomainError):
            msg = 'malformed modifier on item {}: unsupported target domain {} for filtered modification'.format(
                affector.source_holder.item.id, error.args[0])
            signature = (type(error), affector.source_holder.item.id, error.args[0])
            self._fit.eos._logger.warning(msg, child_name='attribute_calculator', signature=signature)
        elif isinstance(error, FilteredSelfReferenceError):
            msg = 'malformed modifier on item {}: invalid reference to self for filtered modification'.format(
                affector.source_holder.item.id)
            signature = (type(error), affector.source_holder.item.id)
            self._fit.eos._logger.warning(msg, child_name='attribute_calculator', signature=signature)
        elif isinstance(error, FilterTypeError):
            msg = 'malformed modifier on item {}: invalid filter type {}'.format(
                affector.source_holder.item.id, error.args[0])
            signature = (type(error), affector.source_holder.item.id, error.args[0])
            self._fit.eos._logger.warning(msg, child_name='attribute_calculator', signature=signature)
        else:
            raise error

    # Methods which help to process filtered modifications
    def __contextize_filter_domain(self, affector):
        """
        Convert domain self-reference to real domain, like
        character or ship. Used only in modifications of multiple
        filtered holders, direct modifications are processed out
        of the context of this method.

        Required arguments:
        affector -- affector, whose modifier refers domain in question

        Return value:
        Real contextized domain

        Possible exceptions:
        FilteredSelfReferenceError -- raised if affector's modifier
        refers self, but affector's holder isn't in position to be
        target for filtered modifications
        FilteredDomainError -- raised when affector's modifier
        target domain is not supported for filtered modification
        """
        source_holder = affector.source_holder
        domain = affector.modifier.domain
        # Reference to self is sparingly used in ship effects, so we must convert
        # it to real domain
        if domain == Domain.self_:
            if source_holder is self._fit.ship:
                return Domain.ship
            elif source_holder is self._fit.character:
                return Domain.character
            else:
                raise FilteredSelfReferenceError
        # Just return untouched domain for all other valid cases
        elif domain in (Domain.character, Domain.ship, Domain.space):
            return domain
        # Raise error if domain is invalid
        else:
            raise FilteredDomainError(domain)

    # Methods which help to process direct modifications
    def __get_holder_direct_domain(self, holder):
        """
        Get domain which you need to target to apply
        direct modification to passed holder.

        Required arguments:
        holder -- holder in question

        Return value:
        Domain specification, if holder can be targeted directly
        from the outside, or None if it can't
        """
        # For ship and character it's easy, we're just picking
        # corresponding domain
        if holder is self._fit.ship:
            domain = Domain.ship
        elif holder is self._fit.character:
            domain = Domain.character
        # For "other" domain, we should've checked for presence
        # of other entity - charge's container or module's charge
        elif self.__get_other_linked_holder(holder) is not None:
            domain = Domain.other
        else:
            domain = None
        return domain

    def __enable_direct_spec(self, target_holder, domain):
        """
        Enable temporarily disabled affectors, directly targeting holder in
        specific domain.

        Required arguments:
        target_holder -- holder which is being registered
        domain -- domain, to which holder is being registered
        """
        # Format: {source_holder: [affectors]}
        affectors_to_enable = {}
        # Cycle through all disabled direct affectors
        for source_holder, affector_set in self.__disabled_direct_affectors.items():
            for affector in affector_set:
                modifier = affector.modifier
                # Mark affector as to-be-enabled only when it
                # targets passed target domain
                if modifier.domain == domain:
                    source_affectors = affectors_to_enable.setdefault(source_holder, [])
                    source_affectors.append(affector)
        # Bail if we have nothing to do
        if not affectors_to_enable:
            return
        # Move all of them to direct modification dictionary
        for source_holder, affectors in affectors_to_enable.items():
            self.__disabled_direct_affectors.rm_data_set(source_holder, affectors)
            self.__active_direct_affectors.add_data_set(target_holder, affectors)

    def __disable_direct_spec(self, target_holder):
        """
        Disable affectors, directly targeting holder in specific domain.

        Required arguments:
        target_holder -- holder which is being unregistered
        """
        # Format: {source_holder: [affectors]}
        affectors_to_disable = {}
        # Check all affectors, targeting passed holder
        for affector in self.__active_direct_affectors.get(target_holder) or ():
            # Mark them as to-be-disabled only if they originate from
            # other holder, else they should be removed with passed holder
            if affector.source_holder is not target_holder:
                source_affectors = affectors_to_disable.setdefault(affector.source_holder, [])
                source_affectors.append(affector)
        if not affectors_to_disable:
            return
        # Move data from map to map
        for source_holder, affectors in affectors_to_disable.items():
            self.__active_direct_affectors.rm_data_set(target_holder, affectors)
            self.__disabled_direct_affectors.add_data_set(source_holder, affectors)

    def __enable_direct_other(self, target_holder):
        """
        Enable temporarily disabled affectors, directly targeting passed holder,
        originating from holder in "other" domain.

        Required arguments:
        target_holder -- holder which is being registered
        """
        other_holder = self.__get_other_linked_holder(target_holder)
        # If passed holder doesn't have other domain (charge's module
        # or module's charge), do nothing
        if other_holder is None:
            return
        # Get all disabled affectors which should influence our target_holder
        affectors_to_enable = set()
        for affector in self.__disabled_direct_affectors.get(other_holder) or ():
            modifier = affector.modifier
            if modifier.domain == Domain.other:
                affectors_to_enable.add(affector)
        # Bail if we have nothing to do
        if not affectors_to_enable:
            return
        # Move all of them to direct modification dictionary
        self.__active_direct_affectors.add_data_set(target_holder, affectors_to_enable)
        self.__disabled_direct_affectors.rm_data_set(other_holder, affectors_to_enable)

    def __disable_direct_other(self, target_holder):
        """
        Disable affectors, directly targeting passed holder, originating from
        holder in "other" domain.

        Required arguments:
        target_holder -- holder which is being unregistered
        """
        other_holder = self.__get_other_linked_holder(target_holder)
        if other_holder is None:
            return
        affectors_to_disable = set()
        # Go through all affectors influencing holder being unregistered
        for affector in self.__active_direct_affectors.get(target_holder) or ():
            # If affector originates from other_holder, mark it as
            # to-be-disabled
            if affector.source_holder is other_holder:
                affectors_to_disable.add(affector)
        # Do nothing if we have no such affectors
        if not affectors_to_disable:
            return
        # If we have, move them from map to map
        self.__disabled_direct_affectors.add_data_set(other_holder, affectors_to_disable)
        self.__active_direct_affectors.rm_data_set(target_holder, affectors_to_disable)

    def __get_other_linked_holder(self, holder):
        """
        Attempt to get holder linked via 'other' link,
        like charge's module or module's charge, return
        None if nothing is found.
        """
        if hasattr(holder, 'charge'):
            return holder.charge
        elif hasattr(holder, 'container'):
            return holder.container
        else:
            return None
Exemple #12
0
class MaxGroupRegister(RestrictionRegister):
    """
    Class which implements common functionality for all
    registers, which track maximum number of belonging to
    ship holders in certain state on per-group basis.
    """
    def __init__(self, max_group_attr, restriction_type):
        # Attribute ID whose value contains group restriction
        # of holder
        self.__max_group_attr = max_group_attr
        self.__restriction_type = restriction_type
        # Container for all tracked holders, keyed
        # by their group ID
        # Format: {group ID: {holders}}
        self.__group_all = KeyedSet()
        # Container for holders, which have max group
        # restriction to become operational
        # Format: {holders}
        self.__group_restricted = set()

    def register_holder(self, holder):
        # Ignore holders which do not belong to ship
        if holder._domain != Domain.ship:
            return
        group = holder.item.group
        # Ignore holders, whose item isn't assigned
        # to any group
        if group is None:
            return
        # Having group ID is sufficient condition
        # to enter container of all fitted holders
        self.__group_all.add_data(group, holder)
        # To enter restriction container, original
        # item must have restriction attribute
        if self.__max_group_attr not in holder.item.attributes:
            return
        self.__group_restricted.add(holder)

    def unregister_holder(self, holder):
        # Just clear data containers
        group = holder.item.group
        self.__group_all.rm_data(group, holder)
        self.__group_restricted.discard(holder)

    def validate(self):
        # Container for tainted holders
        tainted_holders = {}
        # Go through all restricted holders
        for holder in self.__group_restricted:
            # Get number of registered holders, assigned to group of current
            # restricted holder, and holder's restriction value
            group = holder.item.group
            group_holders = len(self.__group_all.get(group) or ())
            max_group_restriction = holder.item.attributes[
                self.__max_group_attr]
            # If number of registered holders from this group is bigger,
            # then current holder is tainted
            if group_holders > max_group_restriction:
                tainted_holders[holder] = MaxGroupErrorData(
                    holder_group=group,
                    max_group=max_group_restriction,
                    group_holders=group_holders)
        # Raise error if we detected any tainted holders
        if tainted_holders:
            raise RegisterValidationError(tainted_holders)

    @property
    def restriction_type(self):
        return self.__restriction_type
Exemple #13
0
    def __init__(self, fit):
        # Link tracker which is assigned to fit we're
        # keeping data for
        self._fit = fit

        # Keep track of holders belonging to certain domain
        # Format: {domain: {targetHolders}}
        self.__affectee_domain = KeyedSet()

        # Keep track of holders belonging to certain domain and group
        # Format: {(domain, group): {targetHolders}}
        self.__affectee_domain_group = KeyedSet()

        # Keep track of holders belonging to certain domain and having certain skill requirement
        # Format: {(domain, skill): {targetHolders}}
        self.__affectee_domain_skill = KeyedSet()

        # Keep track of affectors influencing all holders belonging to certain domain
        # Format: {domain: {affectors}}
        self.__affector_domain = KeyedSet()

        # Keep track of affectors influencing holders belonging to certain domain and group
        # Format: {(domain, group): {affectors}}
        self.__affector_domain_group = KeyedSet()

        # Keep track of affectors influencing holders belonging to certain domain and having certain skill requirement
        # Format: {(domain, skill): {affectors}}
        self.__affector_domain_skill = KeyedSet()

        # Keep track of affectors influencing holders directly
        # Format: {targetHolder: {affectors}}
        self.__active_direct_affectors = KeyedSet()

        # Keep track of affectors which influence something directly,
        # but are disabled as their target domain is not available
        # Format: {source_holder: {affectors}}
        self.__disabled_direct_affectors = KeyedSet()
Exemple #14
0
class LinkRegister:
    """
    Keep track of currently existing links between affectors
    (Affector objects) and affectees (holders). This is hard
    requirement for efficient partial attribute recalculation.
    Register is not aware of links between specific attributes,
    doesn't know anything about states and scopes, just
    affectors and affectees.

    Required arguments:
    fit -- fit, to which this register is bound to
    """
    def __init__(self, fit):
        # Link tracker which is assigned to fit we're
        # keeping data for
        self._fit = fit

        # Keep track of holders belonging to certain domain
        # Format: {domain: {targetHolders}}
        self.__affectee_domain = KeyedSet()

        # Keep track of holders belonging to certain domain and group
        # Format: {(domain, group): {targetHolders}}
        self.__affectee_domain_group = KeyedSet()

        # Keep track of holders belonging to certain domain and having certain skill requirement
        # Format: {(domain, skill): {targetHolders}}
        self.__affectee_domain_skill = KeyedSet()

        # Keep track of affectors influencing all holders belonging to certain domain
        # Format: {domain: {affectors}}
        self.__affector_domain = KeyedSet()

        # Keep track of affectors influencing holders belonging to certain domain and group
        # Format: {(domain, group): {affectors}}
        self.__affector_domain_group = KeyedSet()

        # Keep track of affectors influencing holders belonging to certain domain and having certain skill requirement
        # Format: {(domain, skill): {affectors}}
        self.__affector_domain_skill = KeyedSet()

        # Keep track of affectors influencing holders directly
        # Format: {targetHolder: {affectors}}
        self.__active_direct_affectors = KeyedSet()

        # Keep track of affectors which influence something directly,
        # but are disabled as their target domain is not available
        # Format: {source_holder: {affectors}}
        self.__disabled_direct_affectors = KeyedSet()

    def register_affectee(self, target_holder):
        """
        Add passed target holder to register's maps, so it can be affected by
        other holders properly.

        Required arguments:
        target_holder -- holder to register
        """
        for key, affectee_map in self.__get_affectee_maps(target_holder):
            # Add data to map
            affectee_map.add_data(key, target_holder)
        # Check if we have affectors which should directly influence passed holder,
        # but are disabled; enable them if there're any
        enable_direct = self.__get_holder_direct_domain(target_holder)
        if enable_direct is None:
            return
        if enable_direct == Domain.other:
            self.__enable_direct_other(target_holder)
        elif enable_direct in (Domain.character, Domain.ship):
            self.__enable_direct_spec(target_holder, enable_direct)

    def unregister_affectee(self, target_holder):
        """
        Remove passed target holder from register's maps, so holders affecting
        it "know" that its modification is no longer needed.

        Required arguments:
        target_holder -- holder to unregister
        """
        for key, affectee_map in self.__get_affectee_maps(target_holder):
            affectee_map.rm_data(key, target_holder)
        # When removing holder from register, make sure to move modifiers which
        # originate from 'other' holders and directly affect it to disabled map
        disable_direct = self.__get_holder_direct_domain(target_holder)
        if disable_direct is None:
            return
        if disable_direct == Domain.other:
            self.__disable_direct_other(target_holder)
        elif disable_direct in (Domain.character, Domain.ship):
            self.__disable_direct_spec(target_holder)

    def register_affector(self, affector):
        """
        Add passed affector to register's affector maps, so that new holders
        added to fit know that they should be affected by it.

        Required arguments:
        affector -- affector to register
        """
        try:
            key, affector_map = self.__get_affector_map(affector)
            # Actually add data to map
            affector_map.add_data(key, affector)
        except Exception as e:
            self.__handle_affector_errors(e, affector)

    def unregister_affector(self, affector):
        """
        Remove passed affector from register's affector maps, so that
        holders-affectees "know" that they're no longer affected by it.

        Required arguments:
        affector -- affector to unregister
        """
        try:
            key, affector_map = self.__get_affector_map(affector)
            affector_map.rm_data(key, affector)
        # Following block handles exceptions; all of them must be handled
        # when registering affector too
        except Exception as e:
            self.__handle_affector_errors(e, affector)

    def get_affectees(self, affector):
        """
        Get all holders influenced by passed affector.

        Required arguments:
        affector -- affector, for which we're seeking for affectees

        Return value:
        Set with holders, being influenced by affector
        """
        source_holder, modifier = affector
        affectees = set()
        try:
            # For direct modification, make set out of single target domain
            if modifier.filter_type is None:
                if modifier.domain == Domain.self_:
                    target = {source_holder}
                elif modifier.domain == Domain.character:
                    char = self._fit.character
                    target = {char} if char is not None else None
                elif modifier.domain == Domain.ship:
                    ship = self._fit.ship
                    target = {ship} if ship is not None else None
                elif modifier.domain == Domain.other:
                    other_holder = self.__get_other_linked_holder(
                        source_holder)
                    target = {other_holder
                              } if other_holder is not None else None
                else:
                    raise DirectDomainError(modifier.domain)
            # For filtered modifications, pick appropriate dictionary and get set
            # with target holders
            elif modifier.filter_type == FilterType.all_:
                key = self.__contextize_filter_domain(affector)
                target = self.__affectee_domain.get(key) or set()
            elif modifier.filter_type == FilterType.group:
                domain = self.__contextize_filter_domain(affector)
                key = (domain, modifier.filter_value)
                target = self.__affectee_domain_group.get(key) or set()
            elif modifier.filter_type == FilterType.skill:
                domain = self.__contextize_filter_domain(affector)
                skill = affector.modifier.filter_value
                key = (domain, skill)
                target = self.__affectee_domain_skill.get(key) or set()
            elif modifier.filter_type == FilterType.skill_self:
                domain = self.__contextize_filter_domain(affector)
                skill = affector.source_holder.item.id
                key = (domain, skill)
                target = self.__affectee_domain_skill.get(key) or set()
            else:
                raise FilterTypeError(modifier.filter_type)
            # Add our set to affectees
            if target is not None:
                affectees.update(target)
        except Exception as e:
            self.__handle_affector_errors(e, affector)
        return affectees

    def get_affectors(self, target_holder):
        """
        Get all affectors, which influence passed holder.

        Required arguments:
        target_holder -- holder, for which we're seeking for affecting it
        affectors

        Return value:
        Set with affectors, incluencing target_holder
        """
        affectors = set()
        # Add all affectors which directly affect it
        affectors.update(
            self.__active_direct_affectors.get(target_holder) or set())
        # Then all affectors which affect domain of passed holder
        domain = target_holder._domain
        affectors.update(self.__affector_domain.get(domain) or set())
        # All affectors which affect domain and group of passed holder
        group = target_holder.item.group
        affectors.update(
            self.__affector_domain_group.get((domain, group)) or set())
        # Same, but for domain & skill requirement of passed holder
        for skill in target_holder.item.required_skills:
            affectors.update(
                self.__affector_domain_skill.get((domain, skill)) or set())
        return affectors

    # General-purpose auxiliary methods
    def __get_affectee_maps(self, target_holder):
        """
        Helper for affectee register/unregister methods.

        Required arguments:
        target_holder -- holder, for which affectee maps are requested

        Return value:
        List of (key, affecteeMap) tuples, where key should be used to access
        data set (appropriate to passed target_holder) in affecteeMap
        """
        # Container which temporarily holds (key, map) tuples
        affectee_maps = []
        domain = target_holder._domain
        if domain is not None:
            affectee_maps.append((domain, self.__affectee_domain))
            group = target_holder.item.group
            if group is not None:
                affectee_maps.append(
                    ((domain, group), self.__affectee_domain_group))
            for skill in target_holder.item.required_skills:
                affectee_maps.append(
                    ((domain, skill), self.__affectee_domain_skill))
        return affectee_maps

    def __get_affector_map(self, affector):
        """
        Helper for affector register/unregister methods.

        Required arguments:
        affector -- affector, for which affector map are requested

        Return value:
        (key, affector_map) tuple, where key should be used to access
        data set (appropriate to passed affector) in affector_map

        Possible exceptions:
        FilteredSelfReferenceError -- raised if affector's modifier specifies
        filtered modification and target domain refers self, but affector's
        holder isn't in position to be target for filtered modifications
        DirectDomainError -- raised when affector's modifier target
        domain is not supported for direct modification
        FilteredDomainError -- raised when affector's modifier target
        domain is not supported for filtered modification
        FilterTypeError -- raised when affector's modifier filter type is not
        supported
        """
        source_holder, modifier = affector
        # For each filter type, define affector map and key to use
        if modifier.filter_type is None:
            # For direct modifications, we need to properly pick
            # target holder (it's key) based on domain
            if modifier.domain == Domain.self_:
                affector_map = self.__active_direct_affectors
                key = source_holder
            elif modifier.domain == Domain.character:
                char = self._fit.character
                if char is not None:
                    affector_map = self.__active_direct_affectors
                    key = char
                else:
                    affector_map = self.__disabled_direct_affectors
                    key = source_holder
            elif modifier.domain == Domain.ship:
                ship = self._fit.ship
                if ship is not None:
                    affector_map = self.__active_direct_affectors
                    key = ship
                else:
                    affector_map = self.__disabled_direct_affectors
                    key = source_holder
            # When other domain is referenced, it means direct reference to module's charge
            # or to charge's module-container
            elif modifier.domain == Domain.other:
                other_holder = self.__get_other_linked_holder(source_holder)
                if other_holder is not None:
                    affector_map = self.__active_direct_affectors
                    key = other_holder
                # When no reference available, it means that e.g. charge may be
                # unavailable for now; use disabled affectors map for these
                else:
                    affector_map = self.__disabled_direct_affectors
                    key = source_holder
            else:
                raise DirectDomainError(modifier.domain)
        # For filtered modifications, compose key, making sure reference to self
        # is converted into appropriate real domain
        elif modifier.filter_type == FilterType.all_:
            affector_map = self.__affector_domain
            domain = self.__contextize_filter_domain(affector)
            key = domain
        elif modifier.filter_type == FilterType.group:
            affector_map = self.__affector_domain_group
            domain = self.__contextize_filter_domain(affector)
            key = (domain, modifier.filter_value)
        elif modifier.filter_type == FilterType.skill:
            affector_map = self.__affector_domain_skill
            domain = self.__contextize_filter_domain(affector)
            skill = affector.modifier.filter_value
            key = (domain, skill)
        elif modifier.filter_type == FilterType.skill_self:
            affector_map = self.__affector_domain_skill
            domain = self.__contextize_filter_domain(affector)
            skill = affector.source_holder.item.id
            key = (domain, skill)
        else:
            raise FilterTypeError(modifier.filter_type)
        return key, affector_map

    def __handle_affector_errors(self, error, affector):
        """
        Multiple register methods which get data based on passed affector
        raise similar exception classes. To handle them in consistent fashion,
        it is done from centralized place - this method. If error cannot be
        handled by method, it is re-raised.

        Required arguments:
        error -- Exception instance which was caught and needs to be handled
        affector -- affector object, which was being processed when error occurred
        """
        if isinstance(error, DirectDomainError):
            msg = 'malformed modifier on item {}: unsupported target domain {} for direct modification'.format(
                affector.source_holder.item.id, error.args[0])
            logger.warning(msg)
        elif isinstance(error, FilteredDomainError):
            msg = 'malformed modifier on item {}: unsupported target domain {} for filtered modification'.format(
                affector.source_holder.item.id, error.args[0])
            logger.warning(msg)
        elif isinstance(error, FilteredSelfReferenceError):
            msg = 'malformed modifier on item {}: invalid reference to self for filtered modification'.format(
                affector.source_holder.item.id)
            logger.warning(msg)
        elif isinstance(error, FilterTypeError):
            msg = 'malformed modifier on item {}: invalid filter type {}'.format(
                affector.source_holder.item.id, error.args[0])
            logger.warning(msg)
        else:
            raise error

    # Methods which help to process filtered modifications
    def __contextize_filter_domain(self, affector):
        """
        Convert domain self-reference to real domain, like
        character or ship. Used only in modifications of multiple
        filtered holders, direct modifications are processed out
        of the context of this method.

        Required arguments:
        affector -- affector, whose modifier refers domain in question

        Return value:
        Real contextized domain

        Possible exceptions:
        FilteredSelfReferenceError -- raised if affector's modifier
        refers self, but affector's holder isn't in position to be
        target for filtered modifications
        FilteredDomainError -- raised when affector's modifier
        target domain is not supported for filtered modification
        """
        source_holder = affector.source_holder
        domain = affector.modifier.domain
        # Reference to self is sparingly used in ship effects, so we must convert
        # it to real domain
        if domain == Domain.self_:
            if source_holder is self._fit.ship:
                return Domain.ship
            elif source_holder is self._fit.character:
                return Domain.character
            else:
                raise FilteredSelfReferenceError
        # Just return untouched domain for all other valid cases
        elif domain in (Domain.character, Domain.ship, Domain.space):
            return domain
        # Raise error if domain is invalid
        else:
            raise FilteredDomainError(domain)

    # Methods which help to process direct modifications
    def __get_holder_direct_domain(self, holder):
        """
        Get domain which you need to target to apply
        direct modification to passed holder.

        Required arguments:
        holder -- holder in question

        Return value:
        Domain specification, if holder can be targeted directly
        from the outside, or None if it can't
        """
        # For ship and character it's easy, we're just picking
        # corresponding domain
        if holder is self._fit.ship:
            domain = Domain.ship
        elif holder is self._fit.character:
            domain = Domain.character
        # For "other" domain, we should've checked for presence
        # of other entity - charge's container or module's charge
        elif self.__get_other_linked_holder(holder) is not None:
            domain = Domain.other
        else:
            domain = None
        return domain

    def __enable_direct_spec(self, target_holder, domain):
        """
        Enable temporarily disabled affectors, directly targeting holder in
        specific domain.

        Required arguments:
        target_holder -- holder which is being registered
        domain -- domain, to which holder is being registered
        """
        # Format: {source_holder: [affectors]}
        affectors_to_enable = {}
        # Cycle through all disabled direct affectors
        for source_holder, affector_set in self.__disabled_direct_affectors.items(
        ):
            for affector in affector_set:
                modifier = affector.modifier
                # Mark affector as to-be-enabled only when it
                # targets passed target domain
                if modifier.domain == domain:
                    source_affectors = affectors_to_enable.setdefault(
                        source_holder, [])
                    source_affectors.append(affector)
        # Bail if we have nothing to do
        if not affectors_to_enable:
            return
        # Move all of them to direct modification dictionary
        for source_holder, affectors in affectors_to_enable.items():
            self.__disabled_direct_affectors.rm_data_set(
                source_holder, affectors)
            self.__active_direct_affectors.add_data_set(
                target_holder, affectors)

    def __disable_direct_spec(self, target_holder):
        """
        Disable affectors, directly targeting holder in specific domain.

        Required arguments:
        target_holder -- holder which is being unregistered
        """
        # Format: {source_holder: [affectors]}
        affectors_to_disable = {}
        # Check all affectors, targeting passed holder
        for affector in self.__active_direct_affectors.get(target_holder) or (
        ):
            # Mark them as to-be-disabled only if they originate from
            # other holder, else they should be removed with passed holder
            if affector.source_holder is not target_holder:
                source_affectors = affectors_to_disable.setdefault(
                    affector.source_holder, [])
                source_affectors.append(affector)
        if not affectors_to_disable:
            return
        # Move data from map to map
        for source_holder, affectors in affectors_to_disable.items():
            self.__active_direct_affectors.rm_data_set(target_holder,
                                                       affectors)
            self.__disabled_direct_affectors.add_data_set(
                source_holder, affectors)

    def __enable_direct_other(self, target_holder):
        """
        Enable temporarily disabled affectors, directly targeting passed holder,
        originating from holder in "other" domain.

        Required arguments:
        target_holder -- holder which is being registered
        """
        other_holder = self.__get_other_linked_holder(target_holder)
        # If passed holder doesn't have other domain (charge's module
        # or module's charge), do nothing
        if other_holder is None:
            return
        # Get all disabled affectors which should influence our target_holder
        affectors_to_enable = set()
        for affector in self.__disabled_direct_affectors.get(other_holder) or (
        ):
            modifier = affector.modifier
            if modifier.domain == Domain.other:
                affectors_to_enable.add(affector)
        # Bail if we have nothing to do
        if not affectors_to_enable:
            return
        # Move all of them to direct modification dictionary
        self.__active_direct_affectors.add_data_set(target_holder,
                                                    affectors_to_enable)
        self.__disabled_direct_affectors.rm_data_set(other_holder,
                                                     affectors_to_enable)

    def __disable_direct_other(self, target_holder):
        """
        Disable affectors, directly targeting passed holder, originating from
        holder in "other" domain.

        Required arguments:
        target_holder -- holder which is being unregistered
        """
        other_holder = self.__get_other_linked_holder(target_holder)
        if other_holder is None:
            return
        affectors_to_disable = set()
        # Go through all affectors influencing holder being unregistered
        for affector in self.__active_direct_affectors.get(target_holder) or (
        ):
            # If affector originates from other_holder, mark it as
            # to-be-disabled
            if affector.source_holder is other_holder:
                affectors_to_disable.add(affector)
        # Do nothing if we have no such affectors
        if not affectors_to_disable:
            return
        # If we have, move them from map to map
        self.__disabled_direct_affectors.add_data_set(other_holder,
                                                      affectors_to_disable)
        self.__active_direct_affectors.rm_data_set(target_holder,
                                                   affectors_to_disable)

    def __get_other_linked_holder(self, holder):
        """
        Attempt to get holder linked via 'other' link,
        like charge's module or module's charge, return
        None if nothing is found.
        """
        if hasattr(holder, 'charge'):
            return holder.charge
        elif hasattr(holder, 'container'):
            return holder.container
        else:
            return None