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 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, 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 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