Пример #1
0
 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()
Пример #2
0
 def __init__(self, slotIndexAttr, restrictionType):
     # This attribute's value on holder
     # represents their index of slot
     self.__slotIndexAttr = slotIndexAttr
     self.__restrictionType = restrictionType
     # All holders which possess index of slot
     # are stored in this container
     # Format: {slot index: {holders}}
     self.__slottedHolders = KeyedSet()
Пример #3
0
 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()
Пример #4
0
class SlotIndexRegister(RestrictionRegister):
    """
    Class which implements common functionality for all
    registers, which track indices of occupied slots and
    disallow multiple holders reside within slot with the
    same index.
    """

    def __init__(self, slotIndexAttr, restrictionType):
        # This attribute's value on holder
        # represents their index of slot
        self.__slotIndexAttr = slotIndexAttr
        self.__restrictionType = restrictionType
        # All holders which possess index of slot
        # are stored in this container
        # Format: {slot index: {holders}}
        self.__slottedHolders = KeyedSet()

    def registerHolder(self, holder):
        # Skip items which don't have index specifier
        try:
            slotIndex = holder.item.attributes[self.__slotIndexAttr]
        except KeyError:
            return
        self.__slottedHolders.addData(slotIndex, holder)

    def unregisterHolder(self, holder):
        try:
            slotIndex = holder.item.attributes[self.__slotIndexAttr]
        except KeyError:
            return
        self.__slottedHolders.rmData(slotIndex, holder)

    def validate(self):
        taintedHolders = {}
        for slotIndex in self.__slottedHolders:
            slotIndexHolders = self.__slottedHolders[slotIndex]
            # If more than one item occupies the same slot, all
            # holders in this slot are tainted
            if len(slotIndexHolders) > 1:
                for holder in slotIndexHolders:
                    taintedHolders[holder] = SlotIndexErrorData(holderSlotIndex=slotIndex)
        if taintedHolders:
            raise RegisterValidationError(taintedHolders)

    @property
    def restrictionType(self):
        return self.__restrictionType
Пример #5
0
class SlotIndexRegister(RestrictionRegister):
    """
    Class which implements common functionality for all
    registers, which track indices of occupied slots and
    disallow multiple holders reside within slot with the
    same index.
    """
    def __init__(self, slotIndexAttr, restrictionType):
        # This attribute's value on holder
        # represents their index of slot
        self.__slotIndexAttr = slotIndexAttr
        self.__restrictionType = restrictionType
        # All holders which possess index of slot
        # are stored in this container
        # Format: {slot index: {holders}}
        self.__slottedHolders = KeyedSet()

    def registerHolder(self, holder):
        # Skip items which don't have index specifier
        try:
            slotIndex = holder.item.attributes[self.__slotIndexAttr]
        except KeyError:
            return
        self.__slottedHolders.addData(slotIndex, holder)

    def unregisterHolder(self, holder):
        try:
            slotIndex = holder.item.attributes[self.__slotIndexAttr]
        except KeyError:
            return
        self.__slottedHolders.rmData(slotIndex, holder)

    def validate(self):
        taintedHolders = {}
        for slotIndex in self.__slottedHolders:
            slotIndexHolders = self.__slottedHolders[slotIndex]
            # If more than one item occupies the same slot, all
            # holders in this slot are tainted
            if len(slotIndexHolders) > 1:
                for holder in slotIndexHolders:
                    taintedHolders[holder] = SlotIndexErrorData(
                        holderSlotIndex=slotIndex)
        if taintedHolders:
            raise RegisterValidationError(taintedHolders)

    @property
    def restrictionType(self):
        return self.__restrictionType
Пример #6
0
 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()
Пример #7
0
 def __init__(self, slotIndexAttr, restrictionType):
     # This attribute's value on holder
     # represents their index of slot
     self.__slotIndexAttr = slotIndexAttr
     self.__restrictionType = restrictionType
     # All holders which possess index of slot
     # are stored in this container
     # Format: {slot index: {holders}}
     self.__slottedHolders = KeyedSet()
Пример #8
0
class SkillUniquenessRegister(RestrictionRegister):
    """
    Implements restriction:
    Fit can't have more than one skill based on the same type.

    Details:
    Only holders having level attribute and item typeID other
    than None are tracked.
    """

    def __init__(self):
        # Container for skill holders
        # Format: {holder id: {holders}}
        self.__skillHolders = KeyedSet()

    def registerHolder(self, holder):
        # Only holders which have level attribute are tracked as skills
        if hasattr(holder, 'level') is True and holder.item.id is not None:
            self.__skillHolders.addData(holder.item.id, holder)

    def unregisterHolder(self, holder):
        self.__skillHolders.rmData(holder.item.id, holder)

    def validate(self):
        taintedHolders = {}
        # Go through all skill IDs
        for skillId in self.__skillHolders:
            skillHolders = self.__skillHolders[skillId]
            # If there's at least two skills with the same ID,
            # taint these holders
            if len(skillHolders) > 1:
                for holder in skillHolders:
                    taintedHolders[holder] = SkillUniquenessErrorData(skill=skillId)
        if taintedHolders:
            raise RegisterValidationError(taintedHolders)

    @property
    def restrictionType(self):
        return Restriction.skillUniqueness
Пример #9
0
class SkillUniquenessRegister(RestrictionRegister):
    """
    Implements restriction:
    Fit can't have more than one skill based on the same type.

    Details:
    Only holders having level attribute and item typeID other
    than None are tracked.
    """

    def __init__(self):
        # Container for skill holders
        # Format: {holder id: {holders}}
        self.__skillHolders = KeyedSet()

    def registerHolder(self, holder):
        # Only holders which have level attribute are tracked as skills
        if hasattr(holder, "level") is True and holder.item.id is not None:
            self.__skillHolders.addData(holder.item.id, holder)

    def unregisterHolder(self, holder):
        self.__skillHolders.rmData(holder.item.id, holder)

    def validate(self):
        taintedHolders = {}
        # Go through all skill IDs
        for skillId in self.__skillHolders:
            skillHolders = self.__skillHolders[skillId]
            # If there's at least two skills with the same ID,
            # taint these holders
            if len(skillHolders) > 1:
                for holder in skillHolders:
                    taintedHolders[holder] = SkillUniquenessErrorData(skill=skillId)
        if taintedHolders:
            raise RegisterValidationError(taintedHolders)

    @property
    def restrictionType(self):
        return Restriction.skillUniqueness
Пример #10
0
 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()
Пример #11
0
    def __init__(self, fit):
        # Link tracker which is assigned to fit we're
        # keeping data for
        self._fit = fit

        # Keep track of holders belonging to certain 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()
Пример #12
0
    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()
Пример #13
0
    def __init__(self, fit):
        # Link tracker which is assigned to fit we're
        # keeping data for
        self._fit = fit

        # Keep track of holders belonging to certain 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()
Пример #14
0
 def __init__(self):
     # Container for skill holders
     # Format: {holder id: {holders}}
     self.__skillHolders = KeyedSet()
Пример #15
0
class MaxGroupRegister(RestrictionRegister):
    """
    Class which implements common functionality for all
    registers, which track maximum number of belonging to
    ship holders in certain state on per-group basis.
    """

    def __init__(self, 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
Пример #16
0
    def __calculate(self, attrId):
        """
        Run calculations to find the actual value of attribute.

        Positional arguments:
        attrId -- ID of attribute to be calculated

        Return value:
        Calculated attribute value

        Possible exceptions:
        BaseValueError -- attribute cannot be calculated, as its
        base value is not available
        """
        # Attribute object for attribute being calculated
        try:
            attrMeta = self.__holder._fit.eos._cacheHandler.getAttribute(
                attrId)
        # Raise error if we can't get to getAttribute method
        # or it can't find requested attribute
        except (AttributeError, AttributeFetchError) as e:
            raise AttributeMetaError(attrId) from e
        # Base attribute value which we'll use for modification
        try:
            result = self.__holder.item.attributes[attrId]
        # If attribute isn't available on base item,
        # base off its default value
        except KeyError:
            result = attrMeta.defaultValue
            # If original attribute is not specified and default
            # value isn't available, raise error - without valid
            # base we can't go on
            if result is None:
                raise BaseValueError(attrId)
        # Container for non-penalized modifiers
        # Format: {operator: [values]}
        normalMods = {}
        # Container for penalized modifiers
        # Format: {operator: [values]}
        penalizedMods = {}
        # Now, go through all affectors affecting our holder
        for affector in self.__holder._fit._linkTracker.getAffectors(
                self.__holder, attrId=attrId):
            try:
                sourceHolder, modifier = affector
                operator = modifier.operator
                # Decide if it should be stacking penalized or not, based on stackable property,
                # source item category and operator
                penalize = (attrMeta.stackable is False
                            and sourceHolder.item.categoryId
                            not in penaltyImmuneCategories
                            and operator in penalizableOperators)
                try:
                    modValue = sourceHolder.attributes[
                        modifier.sourceAttributeId]
                # Silently skip current affector: error should already
                # be logged by map before it raised KeyError
                except KeyError:
                    continue
                # Normalize operations to just three types:
                # assignments, additions, multiplications
                try:
                    normalizationFunc = normalizationMap[operator]
                # Raise error on any unknown operator types
                except KeyError as e:
                    raise OperatorError(operator) from e
                modValue = normalizationFunc(modValue)
                # Add value to appropriate dictionary
                if penalize is True:
                    modList = penalizedMods.setdefault(operator, [])
                else:
                    modList = normalMods.setdefault(operator, [])
                modList.append(modValue)
            # Handle operator type failure
            except OperatorError as e:
                msg = 'malformed modifier on item {}: unknown operator {}'.format(
                    sourceHolder.item.id, e.args[0])
                signature = (type(e), sourceHolder.item.id, e.args[0])
                self.__holder._fit.eos._logger.warning(
                    msg, childName='attributeCalculator', signature=signature)
                continue
        # When data gathering is complete, process penalized modifiers
        # They are penalized on per-operator basis
        for operator, modList in penalizedMods.items():
            penalizedValue = self.__penalizeValues(modList)
            modList = normalMods.setdefault(operator, [])
            modList.append(penalizedValue)
        # Calculate result of normal dictionary, according to operator order
        for operator in sorted(normalMods):
            modList = normalMods[operator]
            # Pick best modifier for assignments, based on highIsGood value
            if operator in assignments:
                result = max(modList) if attrMeta.highIsGood is True else min(
                    modList)
            elif operator in additions:
                for modVal in modList:
                    result += modVal
            elif operator in multiplications:
                for modVal in modList:
                    result *= modVal
        # If attribute has upper cap, do not let
        # its value to grow above it
        if attrMeta.maxAttributeId is not None:
            try:
                maxValue = self[attrMeta.maxAttributeId]
            # If max value isn't available, don't
            # cap anything
            except KeyError:
                pass
            else:
                result = min(result, maxValue)
                # Let map know that capping attribute
                # restricts current attribute
                if self._capMap is None:
                    self._capMap = KeyedSet()
                # Fill cap map with data: capping attribute and capped attribute
                self._capMap.addData(attrMeta.maxAttributeId, attrId)
        return result
Пример #17
0
class LinkRegister:
    """
    Keep track of currently existing links between affectors
    (Affector objects) and affectees (holders). This is hard
    requirement for efficient partial attribute recalculation.
    Register is not aware of links between specific attributes,
    doesn't know anything about states and 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)
Пример #18
0
class MutableAttributeMap:
    """
    Calculate, store and provide access to modified attribute values.

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

    __slots__ = ("__holder", "__modifiedAttributes", "_capMap")

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

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

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

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

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

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

    def __setitem__(self, attrId, value):
        # Write value and clear all attributes relying on it
        self.__modifiedAttributes[attrId] = value
        self.__holder.fit._linkTracker.clearHolderAttributeDependents(self.__holder, attrId)

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

    def keys(self):
        # Return union of both keys which are already calculated in
        return self.__modifiedAttributes.keys() | self.__holder.item.attributes.keys()

    def clear(self):
        self.__modifiedAttributes.clear()

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

        Positional arguments:
        attrId -- ID of attribute to be calculated

        Return value:
        Calculated attribute value

        Possible exceptions:
        BaseValueError -- attribute cannot be calculated, as its
        base value is not available
        """
        # Attribute object for attribute being calculated
        try:
            attrMeta = self.__holder.item._cacheHandler.getAttribute(attrId)
        # Raise error if we can't get to getAttribute method
        # or it can't find requested attribute
        except (AttributeError, AttributeFetchError) as e:
            raise AttributeMetaError(attrId) from e
        # Base attribute value which we'll use for modification
        try:
            result = self.__holder.item.attributes[attrId]
        # If attribute isn't available on base item,
        # base off its default value
        except KeyError:
            result = attrMeta.defaultValue
            # If original attribute is not specified and default
            # value isn't available, raise error - without valid
            # base we can't go on
            if result is None:
                raise BaseValueError(attrId)
        # Container for non-penalized modifiers
        # Format: {operator: [values]}
        normalMods = {}
        # Container for penalized modifiers
        # Format: {operator: [values]}
        penalizedMods = {}
        # Now, go through all affectors affecting our holder
        for affector in self.__holder.fit._linkTracker.getAffectors(self.__holder, attrId=attrId):
            try:
                sourceHolder, modifier = affector
                operator = modifier.operator
                # Decide if it should be stacking penalized or not, based on stackable property,
                # source item category and operator
                penalize = (attrMeta.stackable is False and not sourceHolder.item.categoryId in penaltyImmuneCategories
                            and operator in penalizableOperators)
                try:
                    modValue = sourceHolder.attributes[modifier.sourceAttributeId]
                # Silently skip current affector: error should already
                # be logged by map before it raised KeyError
                except KeyError:
                    continue
                # Normalize operations to just three types:
                # assignments, additions, multiplications
                try:
                    normalizationFunc = normalizationMap[operator]
                # Raise error on any unknown operator types
                except KeyError as e:
                    raise OperatorError(operator) from e
                modValue = normalizationFunc(modValue)
                # Add value to appropriate dictionary
                if penalize is True:
                    try:
                        modList = penalizedMods[operator]
                    except KeyError:
                        modList = penalizedMods[operator] = []
                else:
                    try:
                        modList = normalMods[operator]
                    except KeyError:
                        modList = normalMods[operator] = []
                modList.append(modValue)
            # Handle operator type failure
            except OperatorError as e:
                msg = "malformed modifier on item {}: unknown operator {}".format(sourceHolder.item.id, e.args[0])
                signature = (type(e), sourceHolder.item.id, e.args[0])
                self.__holder.fit._eos._logger.warning(msg, childName="attributeCalculator", signature=signature)
                continue
        # When data gathering is complete, process penalized modifiers
        # They are penalized on per-operator basis
        for operator, modList in penalizedMods.items():
            penalizedValue = self.__penalizeValues(modList)
            try:
                modList = normalMods[operator]
            except KeyError:
                modList = normalMods[operator] = []
            modList.append(penalizedValue)
        # Calculate result of normal dictionary, according to operator order
        for operator in sorted(normalMods):
            modList = normalMods[operator]
            # Pick best modifier for assignments, based on highIsGood value
            if operator in assignments:
                result = max(modList) if attrMeta.highIsGood is True else min(modList)
            elif operator in additions:
                for modVal in modList:
                    result += modVal
            elif operator in multiplications:
                for modVal in modList:
                    result *= modVal
        # If attribute has upper cap, do not let
        # its value to grow above it
        if attrMeta.maxAttributeId is not None:
            try:
                maxValue = self[attrMeta.maxAttributeId]
            # If max value isn't available, don't
            # cap anything
            except KeyError:
                pass
            else:
                result = min(result, maxValue)
                # Let map know that capping attribute
                # restricts current attribute
                if self._capMap is None:
                    self._capMap = KeyedSet()
                # Fill cap map with data: capping attribute and capped attribute
                self._capMap.addData(attrMeta.maxAttributeId, attrId)
        return result

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

        Positional argument:
        modList -- list of factors

        Return value:
        Final aggregated factor of passed modList
        """
        # Gather positive modifiers into one chain, negative
        # into another
        chainPositive = []
        chainNegative = []
        for modVal in modList:
            # Transform value into form of multiplier - 1 for ease of
            # stacking chain calculation
            modVal -= 1
            if modVal >= 0:
                chainPositive.append(modVal)
            else:
                chainNegative.append(modVal)
        # Strongest modifiers always go first
        chainPositive.sort(reverse=True)
        chainNegative.sort()
        # Base final multiplier on 1
        listResult = 1
        for chain in (chainPositive, chainNegative):
            # Same for intermediate per-chain result
            chainResult = 1
            for position, modifier in enumerate(chain):
                # Ignore 12th modifier and further as non-significant
                if position > 10:
                    break
                # Apply stacking penalty based on modifier position
                chainResult *= 1 + modifier * penaltyBase ** (position ** 2)
            listResult *= chainResult
        return listResult
Пример #19
0
class MaxGroupRegister(RestrictionRegister):
    """
    Class which implements common functionality for all
    registers, which track maximum number of belonging to
    ship holders in certain state on per-group basis.
    """
    def __init__(self, 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
Пример #20
0
 def __init__(self):
     # Container for skill holders
     # Format: {holder id: {holders}}
     self.__skillHolders = KeyedSet()
Пример #21
0
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
Пример #22
0
class LinkRegister:
    """
    Keep track of currently existing links between affectors
    (Affector objects) and affectees (holders). This is hard
    requirement for efficient partial attribute recalculation.
    Register is not aware of links between specific attributes,
    doesn't know anything about states and 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)
Пример #23
0
class MutableAttributeMap:
    """
    Calculate, store and provide access to modified attribute values.

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

    __slots__ = ('__holder', '__modifiedAttributes', '_capMap')

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

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

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

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

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

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

    def __setitem__(self, attrId, value):
        # Write value and clear all attributes relying on it
        self.__modifiedAttributes[attrId] = value
        self.__holder._fit._linkTracker.clearHolderAttributeDependents(
            self.__holder, attrId)

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

    def keys(self):
        # Return union of both keys which are already calculated in
        return self.__modifiedAttributes.keys(
        ) | self.__holder.item.attributes.keys()

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

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

        Positional arguments:
        attrId -- ID of attribute to be calculated

        Return value:
        Calculated attribute value

        Possible exceptions:
        BaseValueError -- attribute cannot be calculated, as its
        base value is not available
        """
        # Attribute object for attribute being calculated
        try:
            attrMeta = self.__holder._fit.eos._cacheHandler.getAttribute(
                attrId)
        # Raise error if we can't get to getAttribute method
        # or it can't find requested attribute
        except (AttributeError, AttributeFetchError) as e:
            raise AttributeMetaError(attrId) from e
        # Base attribute value which we'll use for modification
        try:
            result = self.__holder.item.attributes[attrId]
        # If attribute isn't available on base item,
        # base off its default value
        except KeyError:
            result = attrMeta.defaultValue
            # If original attribute is not specified and default
            # value isn't available, raise error - without valid
            # base we can't go on
            if result is None:
                raise BaseValueError(attrId)
        # Container for non-penalized modifiers
        # Format: {operator: [values]}
        normalMods = {}
        # Container for penalized modifiers
        # Format: {operator: [values]}
        penalizedMods = {}
        # Now, go through all affectors affecting our holder
        for affector in self.__holder._fit._linkTracker.getAffectors(
                self.__holder, attrId=attrId):
            try:
                sourceHolder, modifier = affector
                operator = modifier.operator
                # Decide if it should be stacking penalized or not, based on stackable property,
                # source item category and operator
                penalize = (attrMeta.stackable is False
                            and sourceHolder.item.categoryId
                            not in penaltyImmuneCategories
                            and operator in penalizableOperators)
                try:
                    modValue = sourceHolder.attributes[
                        modifier.sourceAttributeId]
                # Silently skip current affector: error should already
                # be logged by map before it raised KeyError
                except KeyError:
                    continue
                # Normalize operations to just three types:
                # assignments, additions, multiplications
                try:
                    normalizationFunc = normalizationMap[operator]
                # Raise error on any unknown operator types
                except KeyError as e:
                    raise OperatorError(operator) from e
                modValue = normalizationFunc(modValue)
                # Add value to appropriate dictionary
                if penalize is True:
                    modList = penalizedMods.setdefault(operator, [])
                else:
                    modList = normalMods.setdefault(operator, [])
                modList.append(modValue)
            # Handle operator type failure
            except OperatorError as e:
                msg = 'malformed modifier on item {}: unknown operator {}'.format(
                    sourceHolder.item.id, e.args[0])
                signature = (type(e), sourceHolder.item.id, e.args[0])
                self.__holder._fit.eos._logger.warning(
                    msg, childName='attributeCalculator', signature=signature)
                continue
        # When data gathering is complete, process penalized modifiers
        # They are penalized on per-operator basis
        for operator, modList in penalizedMods.items():
            penalizedValue = self.__penalizeValues(modList)
            modList = normalMods.setdefault(operator, [])
            modList.append(penalizedValue)
        # Calculate result of normal dictionary, according to operator order
        for operator in sorted(normalMods):
            modList = normalMods[operator]
            # Pick best modifier for assignments, based on highIsGood value
            if operator in assignments:
                result = max(modList) if attrMeta.highIsGood is True else min(
                    modList)
            elif operator in additions:
                for modVal in modList:
                    result += modVal
            elif operator in multiplications:
                for modVal in modList:
                    result *= modVal
        # If attribute has upper cap, do not let
        # its value to grow above it
        if attrMeta.maxAttributeId is not None:
            try:
                maxValue = self[attrMeta.maxAttributeId]
            # If max value isn't available, don't
            # cap anything
            except KeyError:
                pass
            else:
                result = min(result, maxValue)
                # Let map know that capping attribute
                # restricts current attribute
                if self._capMap is None:
                    self._capMap = KeyedSet()
                # Fill cap map with data: capping attribute and capped attribute
                self._capMap.addData(attrMeta.maxAttributeId, attrId)
        return result

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

        Positional argument:
        modList -- list of factors

        Return value:
        Final aggregated factor of passed modList
        """
        # Gather positive modifiers into one chain, negative
        # into another
        chainPositive = []
        chainNegative = []
        for modVal in modList:
            # Transform value into form of multiplier - 1 for ease of
            # stacking chain calculation
            modVal -= 1
            if modVal >= 0:
                chainPositive.append(modVal)
            else:
                chainNegative.append(modVal)
        # Strongest modifiers always go first
        chainPositive.sort(reverse=True)
        chainNegative.sort()
        # Base final multiplier on 1
        listResult = 1
        for chain in (chainPositive, chainNegative):
            # Same for intermediate per-chain result
            chainResult = 1
            for position, modifier in enumerate(chain):
                # Ignore 12th modifier and further as non-significant
                if position > 10:
                    break
                # Apply stacking penalty based on modifier position
                chainResult *= 1 + modifier * penaltyBase**(position**2)
            listResult *= chainResult
        return listResult
Пример #24
0
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
Пример #25
0
    def __calculate(self, attrId):
        """
        Run calculations to find the actual value of attribute.

        Positional arguments:
        attrId -- ID of attribute to be calculated

        Return value:
        Calculated attribute value

        Possible exceptions:
        BaseValueError -- attribute cannot be calculated, as its
        base value is not available
        """
        # Attribute object for attribute being calculated
        try:
            attrMeta = self.__holder.item._cacheHandler.getAttribute(attrId)
        # Raise error if we can't get to getAttribute method
        # or it can't find requested attribute
        except (AttributeError, AttributeFetchError) as e:
            raise AttributeMetaError(attrId) from e
        # Base attribute value which we'll use for modification
        try:
            result = self.__holder.item.attributes[attrId]
        # If attribute isn't available on base item,
        # base off its default value
        except KeyError:
            result = attrMeta.defaultValue
            # If original attribute is not specified and default
            # value isn't available, raise error - without valid
            # base we can't go on
            if result is None:
                raise BaseValueError(attrId)
        # Container for non-penalized modifiers
        # Format: {operator: [values]}
        normalMods = {}
        # Container for penalized modifiers
        # Format: {operator: [values]}
        penalizedMods = {}
        # Now, go through all affectors affecting our holder
        for affector in self.__holder.fit._linkTracker.getAffectors(self.__holder, attrId=attrId):
            try:
                sourceHolder, modifier = affector
                operator = modifier.operator
                # Decide if it should be stacking penalized or not, based on stackable property,
                # source item category and operator
                penalize = (attrMeta.stackable is False and not sourceHolder.item.categoryId in penaltyImmuneCategories
                            and operator in penalizableOperators)
                try:
                    modValue = sourceHolder.attributes[modifier.sourceAttributeId]
                # Silently skip current affector: error should already
                # be logged by map before it raised KeyError
                except KeyError:
                    continue
                # Normalize operations to just three types:
                # assignments, additions, multiplications
                try:
                    normalizationFunc = normalizationMap[operator]
                # Raise error on any unknown operator types
                except KeyError as e:
                    raise OperatorError(operator) from e
                modValue = normalizationFunc(modValue)
                # Add value to appropriate dictionary
                if penalize is True:
                    try:
                        modList = penalizedMods[operator]
                    except KeyError:
                        modList = penalizedMods[operator] = []
                else:
                    try:
                        modList = normalMods[operator]
                    except KeyError:
                        modList = normalMods[operator] = []
                modList.append(modValue)
            # Handle operator type failure
            except OperatorError as e:
                msg = "malformed modifier on item {}: unknown operator {}".format(sourceHolder.item.id, e.args[0])
                signature = (type(e), sourceHolder.item.id, e.args[0])
                self.__holder.fit._eos._logger.warning(msg, childName="attributeCalculator", signature=signature)
                continue
        # When data gathering is complete, process penalized modifiers
        # They are penalized on per-operator basis
        for operator, modList in penalizedMods.items():
            penalizedValue = self.__penalizeValues(modList)
            try:
                modList = normalMods[operator]
            except KeyError:
                modList = normalMods[operator] = []
            modList.append(penalizedValue)
        # Calculate result of normal dictionary, according to operator order
        for operator in sorted(normalMods):
            modList = normalMods[operator]
            # Pick best modifier for assignments, based on highIsGood value
            if operator in assignments:
                result = max(modList) if attrMeta.highIsGood is True else min(modList)
            elif operator in additions:
                for modVal in modList:
                    result += modVal
            elif operator in multiplications:
                for modVal in modList:
                    result *= modVal
        # If attribute has upper cap, do not let
        # its value to grow above it
        if attrMeta.maxAttributeId is not None:
            try:
                maxValue = self[attrMeta.maxAttributeId]
            # If max value isn't available, don't
            # cap anything
            except KeyError:
                pass
            else:
                result = min(result, maxValue)
                # Let map know that capping attribute
                # restricts current attribute
                if self._capMap is None:
                    self._capMap = KeyedSet()
                # Fill cap map with data: capping attribute and capped attribute
                self._capMap.addData(attrMeta.maxAttributeId, attrId)
        return result
Пример #26
0
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