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, maxGroupAttr, restrictionType): # Attribute ID whose value contains group restriction # of holder self.__maxGroupAttr = maxGroupAttr self.__restrictionType = restrictionType # Container for all tracked holders, keyed # by their group ID # Format: {group ID: {holders}} self.__groupAll = KeyedSet() # Container for holders, which have max group # restriction to become operational # Format: {holders} self.__maxGroupRestricted = set() def registerHolder(self, holder): # Ignore holders which do not belong to ship if holder._location != Location.ship: return groupId = holder.item.groupId # Ignore holders, whose item isn't assigned # to any group if groupId is None: return # Having group ID is enough condition # to enter container of all fitted holders self.__groupAll.addData(groupId, holder) # To enter restriction container, original # item must have restriction attribute if self.__maxGroupAttr not in holder.item.attributes: return self.__maxGroupRestricted.add(holder) def unregisterHolder(self, holder): # Just clear data containers groupId = holder.item.groupId self.__groupAll.rmData(groupId, holder) self.__maxGroupRestricted.discard(holder) def validate(self): # Container for tainted holders taintedHolders = {} # Go through all restricted holders for holder in self.__maxGroupRestricted: # Get number of registered holders, assigned to group of current # restricted holder, and holder's restriction value groupId = holder.item.groupId groupHolders = len(self.__groupAll.get(groupId) or ()) maxGroupRestriction = holder.item.attributes[self.__maxGroupAttr] # If number of registered holders from this group is bigger, # then current holder is tainted if groupHolders > maxGroupRestriction: taintedHolders[holder] = MaxGroupErrorData(maxGroup=maxGroupRestriction, holderGroup=groupId, groupHolders=groupHolders) # Raise error if we detected any tainted holders if taintedHolders: raise RegisterValidationError(taintedHolders) @property def restrictionType(self): return self.__restrictionType
class LinkRegister: """ Keep track of links between holders, which is required for efficient partial attribute recalculation. Positional arguments: tracker -- LinkTracker object, which uses this register """ def __init__(self, tracker): # The fit we're keeping track of things for self.__tracker = tracker # Keep track of holders belonging to certain location # Format: {location: {targetHolders}} self.__affecteeLocation = KeyedSet() # Keep track of holders belonging to certain location and group # Format: {(location, group): {targetHolders}} self.__affecteeLocationGroup = KeyedSet() # Keep track of holders belonging to certain location and having certain skill requirement # Format: {(location, skill): {targetHolders}} self.__affecteeLocationSkill = KeyedSet() # Keep track of affectors influencing all holders belonging to certain location # Format: {location: {affectors}} self.__affectorLocation = KeyedSet() # Keep track of affectors influencing holders belonging to certain location and group # Format: {(location, group): {affectors}} self.__affectorLocationGroup = KeyedSet() # Keep track of affectors influencing holders belonging to certain location and having certain skill requirement # Format: {(location, skill): {affectors}} self.__affectorLocationSkill = KeyedSet() # Keep track of affectors influencing holders directly # Format: {targetHolder: {affectors}} self.__activeDirectAffectors = KeyedSet() # Keep track of affectors which influence locOther, but are disabled # as their target location is not available # Format: {sourceHolder: {affectors}} self.__disabledDirectAffectors = KeyedSet() def __getAffecteeMaps(self, targetHolder): """ Helper for affectee register/unregister methods. Positional arguments: targetHolder -- 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 targetHolder) in affecteeMap """ # Container which temporarily holds (key, map) tuples affecteeMaps = [] location = targetHolder._location if location is not None: affecteeMaps.append((location, self.__affecteeLocation)) group = targetHolder.item.groupId if group is not None: affecteeMaps.append(((location, group), self.__affecteeLocationGroup)) for skill in targetHolder.item.requiredSkills: affecteeMaps.append(((location, skill), self.__affecteeLocationSkill)) return affecteeMaps def __getAffectorMap(self, affector): """ Helper for affector register/unregister methods. Positional arguments: affector -- affector, for which affector map are requested Return value: (key, affectorMap) tuple, where key should be used to access data set (appropriate to passed affector) in affectorMap Possible exceptions: FilteredSelfReferenceError -- raised if affector's modifier specifies filtered modification and target location refers self, but affector's holder isn't in position to be target for filtered modifications DirectLocationError -- raised when affector's modifier target location is not supported for direct modification FilteredLocationError -- raised when affector's modifier target location is not supported for filtered modification FilterTypeError -- raised when affector's modifier filter type is not supported """ sourceHolder, modifier = affector # For each filter type, define affector map and key to use if modifier.filterType is None: # For single item modifications, we need to properly pick # target holder (it's key) based on location if modifier.location == Location.self_: affectorMap = self.__activeDirectAffectors key = sourceHolder elif modifier.location == Location.character: char = self.__tracker._fit.character if char is not None: affectorMap = self.__activeDirectAffectors key = char else: affectorMap = self.__disabledDirectAffectors key = sourceHolder elif modifier.location == Location.ship: ship = self.__tracker._fit.ship if ship is not None: affectorMap = self.__activeDirectAffectors key = ship else: affectorMap = self.__disabledDirectAffectors key = sourceHolder # When other location is referenced, it means direct reference to module's charge # or to charge's module-container elif modifier.location == Location.other: try: otherHolder = sourceHolder._other except AttributeError: otherHolder = None if otherHolder is not None: affectorMap = self.__activeDirectAffectors key = otherHolder # When no reference available, it means that e.g. charge may be # unavailable for now; use disabled affectors map for these else: affectorMap = self.__disabledDirectAffectors key = sourceHolder else: raise DirectLocationError(modifier.location) # For massive modifications, compose key, making sure reference to self # is converted into appropriate real location elif modifier.filterType == FilterType.all_: affectorMap = self.__affectorLocation location = self.__contextizeFilterLocation(affector) key = location elif modifier.filterType == FilterType.group: affectorMap = self.__affectorLocationGroup location = self.__contextizeFilterLocation(affector) key = (location, modifier.filterValue) elif modifier.filterType == FilterType.skill: affectorMap = self.__affectorLocationSkill location = self.__contextizeFilterLocation(affector) skill = self.__contextizeSkillrqId(affector) key = (location, skill) else: raise FilterTypeError(modifier.filterType) return key, affectorMap def __contextizeFilterLocation(self, affector): """ Convert location self-reference to real location, like character or ship. Used only in modifications of multiple filtered holders, direct modifications are processed out of the context of this method. Positional arguments: affector -- affector, whose modifier refers location in question Return value: Real contextized location Possible exceptions: FilteredSelfReferenceError -- raised if affector's modifier refers self, but affector's holder isn't in position to be target for massive filtered modifications FilteredLocationError -- raised when affector's modifier target location is not supported for filtered modification """ sourceHolder = affector.sourceHolder targetLocation = affector.modifier.location # Reference to self is sparingly used in ship effects, so we must convert # it to real location if targetLocation == Location.self_: if sourceHolder is self.__tracker._fit.ship: return Location.ship elif sourceHolder is self.__tracker._fit.character: return Location.character else: raise FilteredSelfReferenceError # Just return untouched location for all other valid cases elif targetLocation in (Location.character, Location.ship, Location.space): return targetLocation # Raise error if location is invalid else: raise FilteredLocationError(targetLocation) def __contextizeSkillrqId(self, affector): """ Convert typeID self-reference into real typeID. Positional arguments: affector -- affector, whose modifier refers some type via ID Return value: Real typeID, taken from affector's holder carrier """ skillId = affector.modifier.filterValue if skillId == InvType.self_: skillId = affector.sourceHolder.item.id return skillId def __enableDirectSpec(self, targetHolder, targetLocation): """ Enable temporarily disabled affectors, directly targeting holder in specific location. Positional arguments: targetHolder -- holder which is being registered targetLocation -- location, to which holder is being registered """ affectorsToEnable = set() # Cycle through all disabled direct affectors for affectorSet in self.__disabledDirectAffectors.values(): for affector in affectorSet: modifier = affector.modifier # Mark affector as to-be-enabled only when it specifically # targets passed target location, and not holders assigned # to it if modifier.location == targetLocation and modifier.filterType is None: affectorsToEnable.add(affector) # Bail if we have nothing to do if not affectorsToEnable: return # Move all of them to direct modification dictionary self.__activeDirectAffectors.addDataSet(targetHolder, affectorsToEnable) self.__disabledDirectAffectors.rmDataSet(affector.sourceHolder, affectorsToEnable) def __disableDirectSpec(self, targetHolder): """ Disable affectors, directly targeting holder in specific location. Positional arguments: targetHolder -- holder which is being unregistered """ affectorsToDisable = set() # Check all affectors, targeting passed holder for affector in self.__activeDirectAffectors.get(targetHolder) or (): # Mark them as to-be-disabled only if they originate from # other holder, else they should be removed with passed holder if affector.sourceHolder is not targetHolder: affectorsToDisable.add(affector) if not affectorsToDisable: return # Move data from map to map self.__disabledDirectAffectors.addDataSet(affector.sourceHolder, affectorsToDisable) self.__activeDirectAffectors.rmDataSet(targetHolder, affectorsToDisable) def __enableDirectOther(self, targetHolder): """ Enable temporarily disabled affectors, directly targeting passed holder, originating from holder in "other" location. Positional arguments: targetHolder -- holder which is being registered """ try: otherHolder = targetHolder._other except AttributeError: otherHolder = None # If passed holder doesn't have other location (charge's module # or module's charge), do nothing if otherHolder is None: return # Get all disabled affectors which should influence our targetHolder affectorsToEnable = set() for affector in self.__disabledDirectAffectors.get(otherHolder) or (): modifier = affector.modifier if modifier.location == Location.other and modifier.filterType is None: affectorsToEnable.add(affector) # Bail if we have nothing to do if not affectorsToEnable: return # Move all of them to direct modification dictionary self.__activeDirectAffectors.addDataSet(targetHolder, affectorsToEnable) self.__disabledDirectAffectors.rmDataSet(otherHolder, affectorsToEnable) def __disableDirectOther(self, targetHolder): """ Disable affectors, directly targeting passed holder, originating from holder in "other" location. Positional arguments: targetHolder -- holder which is being unregistered """ try: otherHolder = targetHolder._other except AttributeError: otherHolder = None if otherHolder is None: return affectorsToDisable = set() # Go through all affectors influencing holder being unregistered for affector in self.__activeDirectAffectors.get(targetHolder) or (): # If affector originates from otherHolder, mark it as # to-be-disabled if affector.sourceHolder is otherHolder: affectorsToDisable.add(affector) # Do nothing if we have no such affectors if not affectorsToDisable: return # If we have, move them from map to map self.__disabledDirectAffectors.addDataSet(otherHolder, affectorsToDisable) self.__activeDirectAffectors.rmDataSet(targetHolder, affectorsToDisable) def registerAffectee(self, targetHolder, enableDirect=None): """ Add passed target holder to register's maps, so it can be affected by other holders properly. Positional arguments: targetHolder -- holder to register Keyword arguments: enableDirect -- when some location specification is passed, register checks if there're any modifications which should directly apply to holder in that location (or, in case with "other" location, originate from holder in that location and apply to passed targetHolder) and enables them (default None) """ for key, affecteeMap in self.__getAffecteeMaps(targetHolder): # Add data to map affecteeMap.addData(key, targetHolder) # Check if we have affectors which should directly influence passed holder, # but are disabled directEnablers = {Location.ship: (self.__enableDirectSpec, (targetHolder, Location.ship), {}), Location.character: (self.__enableDirectSpec, (targetHolder, Location.character), {}), Location.other: (self.__enableDirectOther, (targetHolder,), {})} try: method, args, kwargs = directEnablers[enableDirect] except KeyError: pass else: method(*args, **kwargs) def unregisterAffectee(self, targetHolder, disableDirect=None): """ Remove passed target holder from register's maps, so holders affecting it "know" that its modification is no longer needed. Positional arguments: targetHolder -- holder to unregister Keyword arguments: disableDirect -- when some location specification is passed, register checks if there're any modifications which are directly applied to holder in that location (or, in case with "other" location, originate from holder in that location and apply to passed targetHolder) and disables them (default None) """ for key, affecteeMap in self.__getAffecteeMaps(targetHolder): affecteeMap.rmData(key, targetHolder) # When removing holder from register, make sure to move modifiers which # originate from other holders and directly affect it to disabled map directEnablers = {Location.ship: (self.__disableDirectSpec, (targetHolder,), {}), Location.character: (self.__disableDirectSpec, (targetHolder,), {}), Location.other: (self.__disableDirectOther, (targetHolder,), {})} try: method, args, kwargs = directEnablers[disableDirect] except KeyError: pass else: method(*args, **kwargs) def registerAffector(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. Positional arguments: affector -- affector to register """ try: key, affectorMap = self.__getAffectorMap(affector) # Actually add data to map affectorMap.addData(key, affector) except DirectLocationError as e: msg = "malformed modifier on item {}: unsupported target location {} for direct modification".format(affector.sourceHolder.item.id, e.args[0]) signature = (type(e), affector.sourceHolder.item.id, e.args[0]) self.__tracker._fit._eos._logger.warning(msg, childName="attributeCalculator", signature=signature) except FilteredLocationError as e: msg = "malformed modifier on item {}: unsupported target location {} for filtered modification".format(affector.sourceHolder.item.id, e.args[0]) signature = (type(e), affector.sourceHolder.item.id, e.args[0]) self.__tracker._fit._eos._logger.warning(msg, childName="attributeCalculator", signature=signature) except FilteredSelfReferenceError as e: msg = "malformed modifier on item {}: invalid reference to self for filtered modification".format(affector.sourceHolder.item.id) signature = (type(e), affector.sourceHolder.item.id) self.__tracker._fit._eos._logger.warning(msg, childName="attributeCalculator", signature=signature) except FilterTypeError as e: msg = "malformed modifier on item {}: invalid filter type {}".format(affector.sourceHolder.item.id, e.args[0]) signature = (type(e), affector.sourceHolder.item.id, e.args[0]) self.__tracker._fit._eos._logger.warning(msg, childName="attributeCalculator", signature=signature) def unregisterAffector(self, affector): """ Remove passed affector from register's affector maps, so that holders-affectees "know" that they're no longer affected by it. Positional arguments: affector -- affector to unregister """ try: key, affectorMap = self.__getAffectorMap(affector) affectorMap.rmData(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 DirectLocationError as e: msg = "malformed modifier on item {}: unsupported target location {} for direct modification".format(affector.sourceHolder.item.id, e.args[0]) signature = (type(e), affector.sourceHolder.item.id, e.args[0]) self.__tracker._fit._eos._logger.warning(msg, childName="attributeCalculator", signature=signature) except FilteredLocationError as e: msg = "malformed modifier on item {}: unsupported target location {} for filtered modification".format(affector.sourceHolder.item.id, e.args[0]) signature = (type(e), affector.sourceHolder.item.id, e.args[0]) self.__tracker._fit._eos._logger.warning(msg, childName="attributeCalculator", signature=signature) except FilteredSelfReferenceError as e: msg = "malformed modifier on item {}: invalid reference to self for filtered modification".format(affector.sourceHolder.item.id) signature = (type(e), affector.sourceHolder.item.id) self.__tracker._fit._eos._logger.warning(msg, childName="attributeCalculator", signature=signature) except FilterTypeError as e: msg = "malformed modifier on item {}: invalid filter type {}".format(affector.sourceHolder.item.id, e.args[0]) signature = (type(e), affector.sourceHolder.item.id, e.args[0]) self.__tracker._fit._eos._logger.warning(msg, childName="attributeCalculator", signature=signature) def getAffectees(self, affector): """ Get all holders influenced by passed affector. Positional arguments: affector -- affector, for which we're seeking for affectees Return value: Set with holders, being influenced by affector """ sourceHolder, modifier = affector affectees = set() try: # For direct modification, make set out of single target location if modifier.filterType is None: if modifier.location == Location.self_: target = {sourceHolder} elif modifier.location == Location.character: char = self.__tracker._fit.character target = {char} if char is not None else None elif modifier.location == Location.ship: ship = self.__tracker._fit.ship target = {ship} if ship is not None else None elif modifier.location == Location.other: try: otherHolder = sourceHolder._other except AttributeError: otherHolder = None target = {otherHolder} if otherHolder is not None else None else: raise DirectLocationError(modifier.location) # For filtered modifications, pick appropriate dictionary and get set # with target holders elif modifier.filterType == FilterType.all_: key = self.__contextizeFilterLocation(affector) target = self.__affecteeLocation.get(key) or set() elif modifier.filterType == FilterType.group: location = self.__contextizeFilterLocation(affector) key = (location, modifier.filterValue) target = self.__affecteeLocationGroup.get(key) or set() elif modifier.filterType == FilterType.skill: location = self.__contextizeFilterLocation(affector) skill = self.__contextizeSkillrqId(affector) key = (location, skill) target = self.__affecteeLocationSkill.get(key) or set() else: raise FilterTypeError(modifier.filterType) # 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 DirectLocationError as e: msg = "malformed modifier on item {}: unsupported target location {} for direct modification".format(sourceHolder.item.id, e.args[0]) signature = (type(e), sourceHolder.item.id, e.args[0]) self.__tracker._fit._eos._logger.warning(msg, childName="attributeCalculator", signature=signature) except FilteredLocationError as e: msg = "malformed modifier on item {}: unsupported target location {} for filtered modification".format(sourceHolder.item.id, e.args[0]) signature = (type(e), sourceHolder.item.id, e.args[0]) self.__tracker._fit._eos._logger.warning(msg, childName="attributeCalculator", signature=signature) except FilteredSelfReferenceError as e: msg = "malformed modifier on item {}: invalid reference to self for filtered modification".format(sourceHolder.item.id) signature = (type(e), sourceHolder.item.id) self.__tracker._fit._eos._logger.warning(msg, childName="attributeCalculator", signature=signature) except FilterTypeError as e: msg = "malformed modifier on item {}: invalid filter type {}".format(sourceHolder.item.id, e.args[0]) signature = (type(e), sourceHolder.item.id, e.args[0]) self.__tracker._fit._eos._logger.warning(msg, childName="attributeCalculator", signature=signature) return affectees def getAffectors(self, targetHolder): """ Get all affectors, which influence passed holder. Positional arguments: targetHolder -- holder, for which we're seeking for affecting it affectors Return value: Set with affectors, incluencing targetHolder """ affectors = set() # Add all affectors which directly affect it affectors.update(self.__activeDirectAffectors.get(targetHolder) or set()) # Then all affectors which affect location of passed holder location = targetHolder._location affectors.update(self.__affectorLocation.get(location) or set()) # All affectors which affect location and group of passed holder group = targetHolder.item.groupId affectors.update(self.__affectorLocationGroup.get((location, group)) or set()) # Same, but for location & skill requirement of passed holder for skill in targetHolder.item.requiredSkills: affectors.update(self.__affectorLocationSkill.get((location, skill)) or set()) return affectors
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 contexts, just affectors and affectees. Positional 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 location # Format: {location: {targetHolders}} self.__affecteeLocation = KeyedSet() # Keep track of holders belonging to certain location and group # Format: {(location, group): {targetHolders}} self.__affecteeLocationGroup = KeyedSet() # Keep track of holders belonging to certain location and having certain skill requirement # Format: {(location, skill): {targetHolders}} self.__affecteeLocationSkill = KeyedSet() # Keep track of affectors influencing all holders belonging to certain location # Format: {location: {affectors}} self.__affectorLocation = KeyedSet() # Keep track of affectors influencing holders belonging to certain location and group # Format: {(location, group): {affectors}} self.__affectorLocationGroup = KeyedSet() # Keep track of affectors influencing holders belonging to certain location and having certain skill requirement # Format: {(location, skill): {affectors}} self.__affectorLocationSkill = KeyedSet() # Keep track of affectors influencing holders directly # Format: {targetHolder: {affectors}} self.__activeDirectAffectors = KeyedSet() # Keep track of affectors which influence something directly, # but are disabled as their target location is not available # Format: {sourceHolder: {affectors}} self.__disabledDirectAffectors = KeyedSet() def registerAffectee(self, targetHolder): """ Add passed target holder to register's maps, so it can be affected by other holders properly. Positional arguments: targetHolder -- holder to register """ for key, affecteeMap in self.__getAffecteeMaps(targetHolder): # Add data to map affecteeMap.addData(key, targetHolder) # Check if we have affectors which should directly influence passed holder, # but are disabled; enable them if there're any enableDirect = self.__getHolderDirectLocation(targetHolder) if enableDirect is None: return if enableDirect == Location.other: self.__enableDirectOther(targetHolder) elif enableDirect in (Location.character, Location.ship): self.__enableDirectSpec(targetHolder, enableDirect) def unregisterAffectee(self, targetHolder): """ Remove passed target holder from register's maps, so holders affecting it "know" that its modification is no longer needed. Positional arguments: targetHolder -- holder to unregister """ for key, affecteeMap in self.__getAffecteeMaps(targetHolder): affecteeMap.rmData(key, targetHolder) # When removing holder from register, make sure to move modifiers which # originate from 'other' holders and directly affect it to disabled map disableDirect = self.__getHolderDirectLocation(targetHolder) if disableDirect is None: return if disableDirect is None: return if disableDirect == Location.other: self.__disableDirectOther(targetHolder) elif disableDirect in (Location.character, Location.ship): self.__disableDirectSpec(targetHolder) def registerAffector(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. Positional arguments: affector -- affector to register """ try: key, affectorMap = self.__getAffectorMap(affector) # Actually add data to map affectorMap.addData(key, affector) except Exception as e: self.__handleAffectorErrors(e, affector) def unregisterAffector(self, affector): """ Remove passed affector from register's affector maps, so that holders-affectees "know" that they're no longer affected by it. Positional arguments: affector -- affector to unregister """ try: key, affectorMap = self.__getAffectorMap(affector) affectorMap.rmData(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.__handleAffectorErrors(e, affector) def getAffectees(self, affector): """ Get all holders influenced by passed affector. Positional arguments: affector -- affector, for which we're seeking for affectees Return value: Set with holders, being influenced by affector """ sourceHolder, modifier = affector affectees = set() try: # For direct modification, make set out of single target location if modifier.filterType is None: if modifier.location == Location.self_: target = {sourceHolder} elif modifier.location == Location.character: char = self._fit.character target = {char} if char is not None else None elif modifier.location == Location.ship: ship = self._fit.ship target = {ship} if ship is not None else None elif modifier.location == Location.other: try: otherHolder = sourceHolder._other except AttributeError: otherHolder = None target = {otherHolder} if otherHolder is not None else None else: raise DirectLocationError(modifier.location) # For filtered modifications, pick appropriate dictionary and get set # with target holders elif modifier.filterType == FilterType.all_: key = self.__contextizeFilterLocation(affector) target = self.__affecteeLocation.get(key) or set() elif modifier.filterType == FilterType.group: location = self.__contextizeFilterLocation(affector) key = (location, modifier.filterValue) target = self.__affecteeLocationGroup.get(key) or set() elif modifier.filterType == FilterType.skill: location = self.__contextizeFilterLocation(affector) skill = affector.modifier.filterValue key = (location, skill) target = self.__affecteeLocationSkill.get(key) or set() elif modifier.filterType == FilterType.skillSelf: location = self.__contextizeFilterLocation(affector) skill = affector.sourceHolder.item.id key = (location, skill) target = self.__affecteeLocationSkill.get(key) or set() else: raise FilterTypeError(modifier.filterType) # 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.__handleAffectorErrors(e, affector) return affectees def getAffectors(self, targetHolder): """ Get all affectors, which influence passed holder. Positional arguments: targetHolder -- holder, for which we're seeking for affecting it affectors Return value: Set with affectors, incluencing targetHolder """ affectors = set() # Add all affectors which directly affect it affectors.update(self.__activeDirectAffectors.get(targetHolder) or set()) # Then all affectors which affect location of passed holder location = targetHolder._location affectors.update(self.__affectorLocation.get(location) or set()) # All affectors which affect location and group of passed holder group = targetHolder.item.groupId affectors.update(self.__affectorLocationGroup.get((location, group)) or set()) # Same, but for location & skill requirement of passed holder for skill in targetHolder.item.requiredSkills: affectors.update(self.__affectorLocationSkill.get((location, skill)) or set()) return affectors # General-purpose auxiliary methods def __getAffecteeMaps(self, targetHolder): """ Helper for affectee register/unregister methods. Positional arguments: targetHolder -- 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 targetHolder) in affecteeMap """ # Container which temporarily holds (key, map) tuples affecteeMaps = [] location = targetHolder._location if location is not None: affecteeMaps.append((location, self.__affecteeLocation)) group = targetHolder.item.groupId if group is not None: affecteeMaps.append(((location, group), self.__affecteeLocationGroup)) for skill in targetHolder.item.requiredSkills: affecteeMaps.append(((location, skill), self.__affecteeLocationSkill)) return affecteeMaps def __getAffectorMap(self, affector): """ Helper for affector register/unregister methods. Positional arguments: affector -- affector, for which affector map are requested Return value: (key, affectorMap) tuple, where key should be used to access data set (appropriate to passed affector) in affectorMap Possible exceptions: FilteredSelfReferenceError -- raised if affector's modifier specifies filtered modification and target location refers self, but affector's holder isn't in position to be target for filtered modifications DirectLocationError -- raised when affector's modifier target location is not supported for direct modification FilteredLocationError -- raised when affector's modifier target location is not supported for filtered modification FilterTypeError -- raised when affector's modifier filter type is not supported """ sourceHolder, modifier = affector # For each filter type, define affector map and key to use if modifier.filterType is None: # For direct modifications, we need to properly pick # target holder (it's key) based on location if modifier.location == Location.self_: affectorMap = self.__activeDirectAffectors key = sourceHolder elif modifier.location == Location.character: char = self._fit.character if char is not None: affectorMap = self.__activeDirectAffectors key = char else: affectorMap = self.__disabledDirectAffectors key = sourceHolder elif modifier.location == Location.ship: ship = self._fit.ship if ship is not None: affectorMap = self.__activeDirectAffectors key = ship else: affectorMap = self.__disabledDirectAffectors key = sourceHolder # When other location is referenced, it means direct reference to module's charge # or to charge's module-container elif modifier.location == Location.other: try: otherHolder = sourceHolder._other except AttributeError: otherHolder = None if otherHolder is not None: affectorMap = self.__activeDirectAffectors key = otherHolder # When no reference available, it means that e.g. charge may be # unavailable for now; use disabled affectors map for these else: affectorMap = self.__disabledDirectAffectors key = sourceHolder else: raise DirectLocationError(modifier.location) # For filtered modifications, compose key, making sure reference to self # is converted into appropriate real location elif modifier.filterType == FilterType.all_: affectorMap = self.__affectorLocation location = self.__contextizeFilterLocation(affector) key = location elif modifier.filterType == FilterType.group: affectorMap = self.__affectorLocationGroup location = self.__contextizeFilterLocation(affector) key = (location, modifier.filterValue) elif modifier.filterType == FilterType.skill: affectorMap = self.__affectorLocationSkill location = self.__contextizeFilterLocation(affector) skill = affector.modifier.filterValue key = (location, skill) elif modifier.filterType == FilterType.skillSelf: affectorMap = self.__affectorLocationSkill location = self.__contextizeFilterLocation(affector) skill = affector.sourceHolder.item.id key = (location, skill) else: raise FilterTypeError(modifier.filterType) return key, affectorMap def __handleAffectorErrors(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. Positional 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, DirectLocationError): msg = 'malformed modifier on item {}: unsupported target location {} for direct modification'.format(affector.sourceHolder.item.id, error.args[0]) signature = (type(error), affector.sourceHolder.item.id, error.args[0]) self._fit.eos._logger.warning(msg, childName='attributeCalculator', signature=signature) elif isinstance(error, FilteredLocationError): msg = 'malformed modifier on item {}: unsupported target location {} for filtered modification'.format(affector.sourceHolder.item.id, error.args[0]) signature = (type(error), affector.sourceHolder.item.id, error.args[0]) self._fit.eos._logger.warning(msg, childName='attributeCalculator', signature=signature) elif isinstance(error, FilteredSelfReferenceError): msg = 'malformed modifier on item {}: invalid reference to self for filtered modification'.format(affector.sourceHolder.item.id) signature = (type(error), affector.sourceHolder.item.id) self._fit.eos._logger.warning(msg, childName='attributeCalculator', signature=signature) elif isinstance(error, FilterTypeError): msg = 'malformed modifier on item {}: invalid filter type {}'.format(affector.sourceHolder.item.id, error.args[0]) signature = (type(error), affector.sourceHolder.item.id, error.args[0]) self._fit.eos._logger.warning(msg, childName='attributeCalculator', signature=signature) else: raise error # Methods which help to process filtered modifications def __contextizeFilterLocation(self, affector): """ Convert location self-reference to real location, like character or ship. Used only in modifications of multiple filtered holders, direct modifications are processed out of the context of this method. Positional arguments: affector -- affector, whose modifier refers location in question Return value: Real contextized location Possible exceptions: FilteredSelfReferenceError -- raised if affector's modifier refers self, but affector's holder isn't in position to be target for filtered modifications FilteredLocationError -- raised when affector's modifier target location is not supported for filtered modification """ sourceHolder = affector.sourceHolder targetLocation = affector.modifier.location # Reference to self is sparingly used in ship effects, so we must convert # it to real location if targetLocation == Location.self_: if sourceHolder is self._fit.ship: return Location.ship elif sourceHolder is self._fit.character: return Location.character else: raise FilteredSelfReferenceError # Just return untouched location for all other valid cases elif targetLocation in (Location.character, Location.ship, Location.space): return targetLocation # Raise error if location is invalid else: raise FilteredLocationError(targetLocation) # Methods which help to process direct modifications def __getHolderDirectLocation(self, holder): """ Get location which you need to target to apply direct modification to passed holder. Positional arguments: holder -- holder in question Return value: Location 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 location if holder is self._fit.ship: location = Location.ship elif holder is self._fit.character: location = Location.character # For "other" location, we should've checked for presence # of other entity - charge's container or module's charge elif getattr(holder, '_other', None) is not None: location = Location.other else: location = None return location def __enableDirectSpec(self, targetHolder, targetLocation): """ Enable temporarily disabled affectors, directly targeting holder in specific location. Positional arguments: targetHolder -- holder which is being registered targetLocation -- location, to which holder is being registered """ # Format: {sourceHolder: [affectors]} affectorsToEnable = {} # Cycle through all disabled direct affectors for sourceHolder, affectorSet in self.__disabledDirectAffectors.items(): for affector in affectorSet: modifier = affector.modifier # Mark affector as to-be-enabled only when it # targets passed target location if modifier.location == targetLocation: sourceAffectors = affectorsToEnable.setdefault(sourceHolder, []) sourceAffectors.append(affector) # Bail if we have nothing to do if not affectorsToEnable: return # Move all of them to direct modification dictionary for sourceHolder, affectors in affectorsToEnable.items(): self.__disabledDirectAffectors.rmDataSet(sourceHolder, affectors) self.__activeDirectAffectors.addDataSet(targetHolder, affectors) def __disableDirectSpec(self, targetHolder): """ Disable affectors, directly targeting holder in specific location. Positional arguments: targetHolder -- holder which is being unregistered """ # Format: {sourceHolder: [affectors]} affectorsToDisable = {} # Check all affectors, targeting passed holder for affector in self.__activeDirectAffectors.get(targetHolder) or (): # Mark them as to-be-disabled only if they originate from # other holder, else they should be removed with passed holder if affector.sourceHolder is not targetHolder: sourceAffectors = affectorsToDisable.setdefault(affector.sourceHolder, []) sourceAffectors.append(affector) if not affectorsToDisable: return # Move data from map to map for sourceHolder, affectors in affectorsToDisable.items(): self.__activeDirectAffectors.rmDataSet(targetHolder, affectors) self.__disabledDirectAffectors.addDataSet(sourceHolder, affectors) def __enableDirectOther(self, targetHolder): """ Enable temporarily disabled affectors, directly targeting passed holder, originating from holder in "other" location. Positional arguments: targetHolder -- holder which is being registered """ try: otherHolder = targetHolder._other except AttributeError: otherHolder = None # If passed holder doesn't have other location (charge's module # or module's charge), do nothing if otherHolder is None: return # Get all disabled affectors which should influence our targetHolder affectorsToEnable = set() for affector in self.__disabledDirectAffectors.get(otherHolder) or (): modifier = affector.modifier if modifier.location == Location.other: affectorsToEnable.add(affector) # Bail if we have nothing to do if not affectorsToEnable: return # Move all of them to direct modification dictionary self.__activeDirectAffectors.addDataSet(targetHolder, affectorsToEnable) self.__disabledDirectAffectors.rmDataSet(otherHolder, affectorsToEnable) def __disableDirectOther(self, targetHolder): """ Disable affectors, directly targeting passed holder, originating from holder in "other" location. Positional arguments: targetHolder -- holder which is being unregistered """ try: otherHolder = targetHolder._other except AttributeError: otherHolder = None if otherHolder is None: return affectorsToDisable = set() # Go through all affectors influencing holder being unregistered for affector in self.__activeDirectAffectors.get(targetHolder) or (): # If affector originates from otherHolder, mark it as # to-be-disabled if affector.sourceHolder is otherHolder: affectorsToDisable.add(affector) # Do nothing if we have no such affectors if not affectorsToDisable: return # If we have, move them from map to map self.__disabledDirectAffectors.addDataSet(otherHolder, affectorsToDisable) self.__activeDirectAffectors.rmDataSet(targetHolder, affectorsToDisable)
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, maxGroupAttr, restrictionType): # Attribute ID whose value contains group restriction # of holder self.__maxGroupAttr = maxGroupAttr self.__restrictionType = restrictionType # Container for all tracked holders, keyed # by their group ID # Format: {group ID: {holders}} self.__groupAll = KeyedSet() # Container for holders, which have max group # restriction to become operational # Format: {holders} self.__maxGroupRestricted = set() def registerHolder(self, holder): # Ignore holders which do not belong to ship if holder._location != Location.ship: return groupId = holder.item.groupId # Ignore holders, whose item isn't assigned # to any group if groupId is None: return # Having group ID is enough condition # to enter container of all fitted holders self.__groupAll.addData(groupId, holder) # To enter restriction container, original # item must have restriction attribute if self.__maxGroupAttr not in holder.item.attributes: return self.__maxGroupRestricted.add(holder) def unregisterHolder(self, holder): # Just clear data containers groupId = holder.item.groupId self.__groupAll.rmData(groupId, holder) self.__maxGroupRestricted.discard(holder) def validate(self): # Container for tainted holders taintedHolders = {} # Go through all restricted holders for holder in self.__maxGroupRestricted: # Get number of registered holders, assigned to group of current # restricted holder, and holder's restriction value groupId = holder.item.groupId groupHolders = len(self.__groupAll.get(groupId) or ()) maxGroupRestriction = holder.item.attributes[self.__maxGroupAttr] # If number of registered holders from this group is bigger, # then current holder is tainted if groupHolders > maxGroupRestriction: taintedHolders[holder] = MaxGroupErrorData( maxGroup=maxGroupRestriction, holderGroup=groupId, groupHolders=groupHolders) # Raise error if we detected any tainted holders if taintedHolders: raise RegisterValidationError(taintedHolders) @property def restrictionType(self): return self.__restrictionType
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 contexts, just affectors and affectees. Positional 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 location # Format: {location: {targetHolders}} self.__affecteeLocation = KeyedSet() # Keep track of holders belonging to certain location and group # Format: {(location, group): {targetHolders}} self.__affecteeLocationGroup = KeyedSet() # Keep track of holders belonging to certain location and having certain skill requirement # Format: {(location, skill): {targetHolders}} self.__affecteeLocationSkill = KeyedSet() # Keep track of affectors influencing all holders belonging to certain location # Format: {location: {affectors}} self.__affectorLocation = KeyedSet() # Keep track of affectors influencing holders belonging to certain location and group # Format: {(location, group): {affectors}} self.__affectorLocationGroup = KeyedSet() # Keep track of affectors influencing holders belonging to certain location and having certain skill requirement # Format: {(location, skill): {affectors}} self.__affectorLocationSkill = KeyedSet() # Keep track of affectors influencing holders directly # Format: {targetHolder: {affectors}} self.__activeDirectAffectors = KeyedSet() # Keep track of affectors which influence something directly, # but are disabled as their target location is not available # Format: {sourceHolder: {affectors}} self.__disabledDirectAffectors = KeyedSet() def registerAffectee(self, targetHolder): """ Add passed target holder to register's maps, so it can be affected by other holders properly. Positional arguments: targetHolder -- holder to register """ for key, affecteeMap in self.__getAffecteeMaps(targetHolder): # Add data to map affecteeMap.addData(key, targetHolder) # Check if we have affectors which should directly influence passed holder, # but are disabled; enable them if there're any enableDirect = self.__getHolderDirectLocation(targetHolder) if enableDirect is None: return if enableDirect == Location.other: self.__enableDirectOther(targetHolder) elif enableDirect in (Location.character, Location.ship): self.__enableDirectSpec(targetHolder, enableDirect) def unregisterAffectee(self, targetHolder): """ Remove passed target holder from register's maps, so holders affecting it "know" that its modification is no longer needed. Positional arguments: targetHolder -- holder to unregister """ for key, affecteeMap in self.__getAffecteeMaps(targetHolder): affecteeMap.rmData(key, targetHolder) # When removing holder from register, make sure to move modifiers which # originate from 'other' holders and directly affect it to disabled map disableDirect = self.__getHolderDirectLocation(targetHolder) if disableDirect is None: return if disableDirect is None: return if disableDirect == Location.other: self.__disableDirectOther(targetHolder) elif disableDirect in (Location.character, Location.ship): self.__disableDirectSpec(targetHolder) def registerAffector(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. Positional arguments: affector -- affector to register """ try: key, affectorMap = self.__getAffectorMap(affector) # Actually add data to map affectorMap.addData(key, affector) except Exception as e: self.__handleAffectorErrors(e, affector) def unregisterAffector(self, affector): """ Remove passed affector from register's affector maps, so that holders-affectees "know" that they're no longer affected by it. Positional arguments: affector -- affector to unregister """ try: key, affectorMap = self.__getAffectorMap(affector) affectorMap.rmData(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.__handleAffectorErrors(e, affector) def getAffectees(self, affector): """ Get all holders influenced by passed affector. Positional arguments: affector -- affector, for which we're seeking for affectees Return value: Set with holders, being influenced by affector """ sourceHolder, modifier = affector affectees = set() try: # For direct modification, make set out of single target location if modifier.filterType is None: if modifier.location == Location.self_: target = {sourceHolder} elif modifier.location == Location.character: char = self._fit.character target = {char} if char is not None else None elif modifier.location == Location.ship: ship = self._fit.ship target = {ship} if ship is not None else None elif modifier.location == Location.other: try: otherHolder = sourceHolder._other except AttributeError: otherHolder = None target = {otherHolder} if otherHolder is not None else None else: raise DirectLocationError(modifier.location) # For filtered modifications, pick appropriate dictionary and get set # with target holders elif modifier.filterType == FilterType.all_: key = self.__contextizeFilterLocation(affector) target = self.__affecteeLocation.get(key) or set() elif modifier.filterType == FilterType.group: location = self.__contextizeFilterLocation(affector) key = (location, modifier.filterValue) target = self.__affecteeLocationGroup.get(key) or set() elif modifier.filterType == FilterType.skill: location = self.__contextizeFilterLocation(affector) skill = affector.modifier.filterValue key = (location, skill) target = self.__affecteeLocationSkill.get(key) or set() elif modifier.filterType == FilterType.skillSelf: location = self.__contextizeFilterLocation(affector) skill = affector.sourceHolder.item.id key = (location, skill) target = self.__affecteeLocationSkill.get(key) or set() else: raise FilterTypeError(modifier.filterType) # 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.__handleAffectorErrors(e, affector) return affectees def getAffectors(self, targetHolder): """ Get all affectors, which influence passed holder. Positional arguments: targetHolder -- holder, for which we're seeking for affecting it affectors Return value: Set with affectors, incluencing targetHolder """ affectors = set() # Add all affectors which directly affect it affectors.update( self.__activeDirectAffectors.get(targetHolder) or set()) # Then all affectors which affect location of passed holder location = targetHolder._location affectors.update(self.__affectorLocation.get(location) or set()) # All affectors which affect location and group of passed holder group = targetHolder.item.groupId affectors.update( self.__affectorLocationGroup.get((location, group)) or set()) # Same, but for location & skill requirement of passed holder for skill in targetHolder.item.requiredSkills: affectors.update( self.__affectorLocationSkill.get((location, skill)) or set()) return affectors # General-purpose auxiliary methods def __getAffecteeMaps(self, targetHolder): """ Helper for affectee register/unregister methods. Positional arguments: targetHolder -- 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 targetHolder) in affecteeMap """ # Container which temporarily holds (key, map) tuples affecteeMaps = [] location = targetHolder._location if location is not None: affecteeMaps.append((location, self.__affecteeLocation)) group = targetHolder.item.groupId if group is not None: affecteeMaps.append( ((location, group), self.__affecteeLocationGroup)) for skill in targetHolder.item.requiredSkills: affecteeMaps.append( ((location, skill), self.__affecteeLocationSkill)) return affecteeMaps def __getAffectorMap(self, affector): """ Helper for affector register/unregister methods. Positional arguments: affector -- affector, for which affector map are requested Return value: (key, affectorMap) tuple, where key should be used to access data set (appropriate to passed affector) in affectorMap Possible exceptions: FilteredSelfReferenceError -- raised if affector's modifier specifies filtered modification and target location refers self, but affector's holder isn't in position to be target for filtered modifications DirectLocationError -- raised when affector's modifier target location is not supported for direct modification FilteredLocationError -- raised when affector's modifier target location is not supported for filtered modification FilterTypeError -- raised when affector's modifier filter type is not supported """ sourceHolder, modifier = affector # For each filter type, define affector map and key to use if modifier.filterType is None: # For direct modifications, we need to properly pick # target holder (it's key) based on location if modifier.location == Location.self_: affectorMap = self.__activeDirectAffectors key = sourceHolder elif modifier.location == Location.character: char = self._fit.character if char is not None: affectorMap = self.__activeDirectAffectors key = char else: affectorMap = self.__disabledDirectAffectors key = sourceHolder elif modifier.location == Location.ship: ship = self._fit.ship if ship is not None: affectorMap = self.__activeDirectAffectors key = ship else: affectorMap = self.__disabledDirectAffectors key = sourceHolder # When other location is referenced, it means direct reference to module's charge # or to charge's module-container elif modifier.location == Location.other: try: otherHolder = sourceHolder._other except AttributeError: otherHolder = None if otherHolder is not None: affectorMap = self.__activeDirectAffectors key = otherHolder # When no reference available, it means that e.g. charge may be # unavailable for now; use disabled affectors map for these else: affectorMap = self.__disabledDirectAffectors key = sourceHolder else: raise DirectLocationError(modifier.location) # For filtered modifications, compose key, making sure reference to self # is converted into appropriate real location elif modifier.filterType == FilterType.all_: affectorMap = self.__affectorLocation location = self.__contextizeFilterLocation(affector) key = location elif modifier.filterType == FilterType.group: affectorMap = self.__affectorLocationGroup location = self.__contextizeFilterLocation(affector) key = (location, modifier.filterValue) elif modifier.filterType == FilterType.skill: affectorMap = self.__affectorLocationSkill location = self.__contextizeFilterLocation(affector) skill = affector.modifier.filterValue key = (location, skill) elif modifier.filterType == FilterType.skillSelf: affectorMap = self.__affectorLocationSkill location = self.__contextizeFilterLocation(affector) skill = affector.sourceHolder.item.id key = (location, skill) else: raise FilterTypeError(modifier.filterType) return key, affectorMap def __handleAffectorErrors(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. Positional 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, DirectLocationError): msg = 'malformed modifier on item {}: unsupported target location {} for direct modification'.format( affector.sourceHolder.item.id, error.args[0]) signature = (type(error), affector.sourceHolder.item.id, error.args[0]) self._fit.eos._logger.warning(msg, childName='attributeCalculator', signature=signature) elif isinstance(error, FilteredLocationError): msg = 'malformed modifier on item {}: unsupported target location {} for filtered modification'.format( affector.sourceHolder.item.id, error.args[0]) signature = (type(error), affector.sourceHolder.item.id, error.args[0]) self._fit.eos._logger.warning(msg, childName='attributeCalculator', signature=signature) elif isinstance(error, FilteredSelfReferenceError): msg = 'malformed modifier on item {}: invalid reference to self for filtered modification'.format( affector.sourceHolder.item.id) signature = (type(error), affector.sourceHolder.item.id) self._fit.eos._logger.warning(msg, childName='attributeCalculator', signature=signature) elif isinstance(error, FilterTypeError): msg = 'malformed modifier on item {}: invalid filter type {}'.format( affector.sourceHolder.item.id, error.args[0]) signature = (type(error), affector.sourceHolder.item.id, error.args[0]) self._fit.eos._logger.warning(msg, childName='attributeCalculator', signature=signature) else: raise error # Methods which help to process filtered modifications def __contextizeFilterLocation(self, affector): """ Convert location self-reference to real location, like character or ship. Used only in modifications of multiple filtered holders, direct modifications are processed out of the context of this method. Positional arguments: affector -- affector, whose modifier refers location in question Return value: Real contextized location Possible exceptions: FilteredSelfReferenceError -- raised if affector's modifier refers self, but affector's holder isn't in position to be target for filtered modifications FilteredLocationError -- raised when affector's modifier target location is not supported for filtered modification """ sourceHolder = affector.sourceHolder targetLocation = affector.modifier.location # Reference to self is sparingly used in ship effects, so we must convert # it to real location if targetLocation == Location.self_: if sourceHolder is self._fit.ship: return Location.ship elif sourceHolder is self._fit.character: return Location.character else: raise FilteredSelfReferenceError # Just return untouched location for all other valid cases elif targetLocation in (Location.character, Location.ship, Location.space): return targetLocation # Raise error if location is invalid else: raise FilteredLocationError(targetLocation) # Methods which help to process direct modifications def __getHolderDirectLocation(self, holder): """ Get location which you need to target to apply direct modification to passed holder. Positional arguments: holder -- holder in question Return value: Location 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 location if holder is self._fit.ship: location = Location.ship elif holder is self._fit.character: location = Location.character # For "other" location, we should've checked for presence # of other entity - charge's container or module's charge elif getattr(holder, '_other', None) is not None: location = Location.other else: location = None return location def __enableDirectSpec(self, targetHolder, targetLocation): """ Enable temporarily disabled affectors, directly targeting holder in specific location. Positional arguments: targetHolder -- holder which is being registered targetLocation -- location, to which holder is being registered """ # Format: {sourceHolder: [affectors]} affectorsToEnable = {} # Cycle through all disabled direct affectors for sourceHolder, affectorSet in self.__disabledDirectAffectors.items( ): for affector in affectorSet: modifier = affector.modifier # Mark affector as to-be-enabled only when it # targets passed target location if modifier.location == targetLocation: sourceAffectors = affectorsToEnable.setdefault( sourceHolder, []) sourceAffectors.append(affector) # Bail if we have nothing to do if not affectorsToEnable: return # Move all of them to direct modification dictionary for sourceHolder, affectors in affectorsToEnable.items(): self.__disabledDirectAffectors.rmDataSet(sourceHolder, affectors) self.__activeDirectAffectors.addDataSet(targetHolder, affectors) def __disableDirectSpec(self, targetHolder): """ Disable affectors, directly targeting holder in specific location. Positional arguments: targetHolder -- holder which is being unregistered """ # Format: {sourceHolder: [affectors]} affectorsToDisable = {} # Check all affectors, targeting passed holder for affector in self.__activeDirectAffectors.get(targetHolder) or (): # Mark them as to-be-disabled only if they originate from # other holder, else they should be removed with passed holder if affector.sourceHolder is not targetHolder: sourceAffectors = affectorsToDisable.setdefault( affector.sourceHolder, []) sourceAffectors.append(affector) if not affectorsToDisable: return # Move data from map to map for sourceHolder, affectors in affectorsToDisable.items(): self.__activeDirectAffectors.rmDataSet(targetHolder, affectors) self.__disabledDirectAffectors.addDataSet(sourceHolder, affectors) def __enableDirectOther(self, targetHolder): """ Enable temporarily disabled affectors, directly targeting passed holder, originating from holder in "other" location. Positional arguments: targetHolder -- holder which is being registered """ try: otherHolder = targetHolder._other except AttributeError: otherHolder = None # If passed holder doesn't have other location (charge's module # or module's charge), do nothing if otherHolder is None: return # Get all disabled affectors which should influence our targetHolder affectorsToEnable = set() for affector in self.__disabledDirectAffectors.get(otherHolder) or (): modifier = affector.modifier if modifier.location == Location.other: affectorsToEnable.add(affector) # Bail if we have nothing to do if not affectorsToEnable: return # Move all of them to direct modification dictionary self.__activeDirectAffectors.addDataSet(targetHolder, affectorsToEnable) self.__disabledDirectAffectors.rmDataSet(otherHolder, affectorsToEnable) def __disableDirectOther(self, targetHolder): """ Disable affectors, directly targeting passed holder, originating from holder in "other" location. Positional arguments: targetHolder -- holder which is being unregistered """ try: otherHolder = targetHolder._other except AttributeError: otherHolder = None if otherHolder is None: return affectorsToDisable = set() # Go through all affectors influencing holder being unregistered for affector in self.__activeDirectAffectors.get(targetHolder) or (): # If affector originates from otherHolder, mark it as # to-be-disabled if affector.sourceHolder is otherHolder: affectorsToDisable.add(affector) # Do nothing if we have no such affectors if not affectorsToDisable: return # If we have, move them from map to map self.__disabledDirectAffectors.addDataSet(otherHolder, affectorsToDisable) self.__activeDirectAffectors.rmDataSet(targetHolder, affectorsToDisable)
class SkillRequirementRegister(RestrictionRegister): """ Implements restriction: To use holder, all its skill requirements must be met. Details: Only holders having level attribute are tracked. Original item attributes are taken to determine skill and skill level requirements. If corresponding skill is found, but its skill level is None, check for holder is failed. """ def __init__(self): # Container for skill holders, for ease of # access # Format: {holder id: {holders}} self.__skillHolders = KeyedSet() # Set with holders which have any skill requirements # Format: {holders} self.__restrictedHolders = set() def registerHolder(self, holder): # Only holders which belong to character and have # level attribute are tracked as skills if hasattr(holder, "level") is True: self.__skillHolders.addData(holder.item.id, holder) # Holders which have any skill requirement are tracked if holder.item.requiredSkills: self.__restrictedHolders.add(holder) def unregisterHolder(self, holder): self.__skillHolders.rmData(holder.item.id, holder) self.__restrictedHolders.discard(holder) def validate(self): taintedHolders = {} # Go through restricted holders for holder in self.__restrictedHolders: # Container for skill requirement errors skillRequirementErrors = [] # Check each skill requirement for requiredSkillId in holder.item.requiredSkills: requiredSkillLevel = holder.item.requiredSkills[requiredSkillId] skillHolders = self.__skillHolders.get(requiredSkillId) or () # Pick max level of all skill holders, absence of skill # is considered as skill level set to None skillLevel = None for skillHolder in skillHolders: skillHolderLevel = skillHolder.level if skillLevel is None: skillLevel = skillHolderLevel elif skillHolderLevel is not None: skillLevel = max(skillLevel, skillHolderLevel) # Last check - if skill level is lower than expected, current holder # is tainted; mark it so and move to the next one if skillLevel is None or skillLevel < requiredSkillLevel: skillRequirementError = SkillRequirementErrorData(skill=requiredSkillId, level=skillLevel, requiredLevel=requiredSkillLevel) skillRequirementErrors.append(skillRequirementError) if skillRequirementErrors: taintedHolders[holder] = tuple(skillRequirementErrors) if taintedHolders: raise RegisterValidationError(taintedHolders) @property def restrictionType(self): return Restriction.skillRequirement
class SkillRequirementRegister(RestrictionRegister): """ Implements restriction: To use holder, all its skill requirements must be met. Details: Only holders having level attribute are tracked. Original item attributes are taken to determine skill and skill level requirements. If corresponding skill is found, but its skill level is None, check for holder is failed. """ def __init__(self): # Container for skill holders, for ease of # access # Format: {holder id: {holders}} self.__skillHolders = KeyedSet() # Set with holders which have any skill requirements # Format: {holders} self.__restrictedHolders = set() def registerHolder(self, holder): # Only holders which belong to character and have # level attribute are tracked as skills if hasattr(holder, 'level') is True: self.__skillHolders.addData(holder.item.id, holder) # Holders which have any skill requirement are tracked if holder.item.requiredSkills: self.__restrictedHolders.add(holder) def unregisterHolder(self, holder): self.__skillHolders.rmData(holder.item.id, holder) self.__restrictedHolders.discard(holder) def validate(self): taintedHolders = {} # Go through restricted holders for holder in self.__restrictedHolders: # Container for skill requirement errors skillRequirementErrors = [] # Check each skill requirement for requiredSkillId in holder.item.requiredSkills: requiredSkillLevel = holder.item.requiredSkills[ requiredSkillId] skillHolders = self.__skillHolders.get(requiredSkillId) or () # Pick max level of all skill holders, absence of skill # is considered as skill level set to None skillLevel = None for skillHolder in skillHolders: skillHolderLevel = skillHolder.level if skillLevel is None: skillLevel = skillHolderLevel elif skillHolderLevel is not None: skillLevel = max(skillLevel, skillHolderLevel) # Last check - if skill level is lower than expected, current holder # is tainted; mark it so and move to the next one if skillLevel is None or skillLevel < requiredSkillLevel: skillRequirementError = SkillRequirementErrorData( skill=requiredSkillId, level=skillLevel, requiredLevel=requiredSkillLevel) skillRequirementErrors.append(skillRequirementError) if skillRequirementErrors: taintedHolders[holder] = tuple(skillRequirementErrors) if taintedHolders: raise RegisterValidationError(taintedHolders) @property def restrictionType(self): return Restriction.skillRequirement