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 __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()
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
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()
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
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
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
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
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
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
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
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