# =============================================================================== # Copyright (C) 2010 Diego Duclos # # This file is part of eos. # # eos is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 2 of the License, or # (at your option) any later version. # # eos is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with eos. If not, see . # =============================================================================== import time from logbook import Logger from itertools import chain from sqlalchemy.orm import validates, reconstructor import eos import eos.db import eos.config from eos.effectHandlerHelpers import HandledItem, HandledImplantList pyfalog = Logger(__name__) class Character: __itemList = None __itemIDMap = None __itemNameMap = None def __init__(self, name, defaultLevel=None, initSkills=True): self.savedName = name self.__owner = None self.defaultLevel = defaultLevel self.__skills = [] self.__skillIdMap = {} self.dirtySkills = set() self.alphaClone = None self.__secStatus = 0.0 if initSkills: for item in self.getSkillList(): self.addSkill(Skill(self, item.ID, self.defaultLevel)) self.__implants = HandledImplantList() @reconstructor def init(self): self.__skillIdMap = {} for skill in self.__skills: self.__skillIdMap[skill.itemID] = skill # get a list of skills that the character does no have, and add them (removal of old skills happens in the # Skill loading) for skillID in set(self.getSkillIDMap().keys()).difference(set(self.__skillIdMap.keys())): self.addSkill(Skill(self, skillID, self.defaultLevel)) self.dirtySkills = set() self.alphaClone = None if self.alphaCloneID: self.alphaClone = eos.db.getAlphaClone(self.alphaCloneID) @classmethod def getSkillList(cls): if cls.__itemList is None: cls.__itemList = eos.db.getItemsByCategory("Skill") return cls.__itemList @classmethod def getSkillIDMap(cls): if cls.__itemIDMap is None: map = {} for skill in cls.getSkillList(): map[skill.ID] = skill cls.__itemIDMap = map return cls.__itemIDMap @classmethod def getSkillNameMap(cls): if cls.__itemNameMap is None: map = {} for skill in cls.getSkillList(): map[skill.typeName] = skill cls.__itemNameMap = map return cls.__itemNameMap @classmethod def getAll5(cls): all5 = eos.db.getCharacter("All 5") if all5 is None: # We do not have to be afraid of committing here and saving # edited character data. If this ever runs, it will be during the # get character list phase when pyfa first starts all5 = Character("All 5", 5) eos.db.save(all5) return all5 @classmethod def getAll0(cls): all0 = eos.db.getCharacter("All 0") if all0 is None: all0 = Character("All 0") eos.db.save(all0) return all0 def apiUpdateCharSheet(self, skills, secStatus=0.00): self.clearSkills() for skillRow in skills: self.addSkill(Skill(self, skillRow["typeID"], skillRow["level"])) self.secStatus = float(secStatus) def clearSkills(self): del self.__skills[:] self.__skillIdMap.clear() self.dirtySkills.clear() @property def ro(self): return self == self.getAll0() or self == self.getAll5() @property def secStatus(self): if self.name == "All 5": self.__secStatus = 5.00 elif self.name == "All 0": self.__secStatus = 0.00 return self.__secStatus @secStatus.setter def secStatus(self, sec): self.__secStatus = sec @property def owner(self): return self.__owner @owner.setter def owner(self, owner): self.__owner = owner @property def name(self): name = self.savedName if self.isDirty: name += " *" if self.alphaCloneID: name += ' (\u03B1)' return name @name.setter def name(self, name): self.savedName = name def setSsoCharacter(self, character, clientHash): if character is not None: self.__ssoCharacters.append(character) else: for x in self.__ssoCharacters: if x.client == clientHash: self.__ssoCharacters.remove(x) def getSsoCharacter(self, clientHash): return next((x for x in self.__ssoCharacters if x.client == clientHash), None) @property def alphaCloneID(self): return self.__alphaCloneID @alphaCloneID.setter def alphaCloneID(self, cloneID): self.__alphaCloneID = cloneID self.alphaClone = eos.db.getAlphaClone(cloneID) if cloneID is not None else None @property def skills(self): return self.__skills def addSkill(self, skill): if skill.itemID in self.__skillIdMap: oldSkill = self.__skillIdMap[skill.itemID] if skill.level > oldSkill.level: # if new skill is higher, remove old skill (new skill will still append) self.__skills.remove(oldSkill) else: return self.__skillIdMap[skill.itemID] = skill def removeSkill(self, skill): self.__skills.remove(skill) del self.__skillIdMap[skill.itemID] def getSkill(self, item): if isinstance(item, str): item = self.getSkillNameMap()[item] elif isinstance(item, int): item = self.getSkillIDMap()[item] skill = self.__skillIdMap.get(item.ID) if skill is None: skill = Skill(self, item, self.defaultLevel, False, True) self.addSkill(skill) return skill @property def implants(self): return self.__implants @property def isDirty(self): return len(self.dirtySkills) > 0 def saveLevels(self): if self.ro: raise ReadOnlyException("This character is read-only") for skill in self.dirtySkills.copy(): skill.saveLevel() self.dirtySkills = set() eos.db.commit() def revertLevels(self): for skill in self.dirtySkills.copy(): skill.revert() self.dirtySkills = set() def filteredSkillIncrease(self, filter, *args, **kwargs): for element in self.skills: if filter(element): element.increaseItemAttr(*args, **kwargs) def filteredSkillMultiply(self, filter, *args, **kwargs): for element in self.skills: if filter(element): element.multiplyItemAttr(*args, **kwargs) def filteredSkillBoost(self, filter, *args, **kwargs): for element in self.skills: if filter(element): element.boostItemAttr(*args, **kwargs) def calculateModifiedAttributes(self, fit, runTime, forceProjected=False): if forceProjected: return for skill in self.skills: fit.register(skill) skill.calculateModifiedAttributes(fit, runTime) def clear(self): c = chain( self.skills, self.implants ) for stuff in c: if stuff is not None and stuff != self: stuff.clear() def __deepcopy__(self, memo): copy = Character("%s copy" % self.name, initSkills=False) for skill in self.skills: copy.addSkill(Skill(copy, skill.itemID, skill.level, False, skill.learned)) return copy @validates("ID", "name", "ownerID") def validator(self, key, val): map = { "ID" : lambda _val: isinstance(_val, int), "name" : lambda _val: True, "ownerID": lambda _val: isinstance(_val, int) or _val is None } if not map[key](val): raise ValueError(str(val) + " is not a valid value for " + key) else: return val def __repr__(self): return "Character(ID={}, name={}) at {}".format( self.ID, self.name, hex(id(self)) ) class Skill(HandledItem): def __init__(self, character, item, level=0, ro=False, learned=True): self.character = character self.__item = item if not isinstance(item, int) else None self.itemID = item.ID if not isinstance(item, int) else item self.__level = level if learned else None self.commandBonus = 0 self.build(ro) @reconstructor def init(self): self.build(False) self.__item = None def build(self, ro): self.__ro = ro self.__suppressed = False self.activeLevel = self.__level def saveLevel(self): self.__level = self.activeLevel if self in self.character.dirtySkills: self.character.dirtySkills.remove(self) def revert(self): self.activeLevel = self.__level @property def isDirty(self): return self.__level != self.activeLevel @property def learned(self): return self.activeLevel is not None @property def level(self): # @todo: there is a phantom bug that keep popping up about skills not having a character... See #1234 # Remove this at some point when the cause can be determined. if self.character: # Ensure that All 5/0 character have proper skill levels (in case database gets corrupted) if self.character.name == "All 5": self.activeLevel = self.__level = 5 elif self.character.name == "All 0": self.activeLevel = self.__level = 0 elif self.character.alphaClone: return min(self.activeLevel or 0, self.character.alphaClone.getSkillLevel(self) or 0) return self.activeLevel or 0 def setLevel(self, level, persist=False, ignoreRestrict=False): if level is not None and (level < 0 or level > 5): raise ValueError(str(level) + " is not a valid value for level") if hasattr(self, "_Skill__ro") and self.__ro is True: raise ReadOnlyException() self.activeLevel = level # todo: have a way to do bulk skill level editing. Currently, everytime a single skill is changed, this runs, # which affects performance. Should have a checkSkillLevels() or something that is more efficient for bulk. if not ignoreRestrict and eos.config.settings['strictSkillLevels']: start = time.time() for item, rlevel in self.item.requiredFor.items(): if item.group.category.ID == 16: # Skill category if level is None or level < rlevel: skill = self.character.getSkill(item.ID) # print "Removing skill: {}, Dependant level: {}, Required level: {}".format(skill, level, rlevel) skill.setLevel(None, persist) pyfalog.debug("Strict Skill levels enabled, time to process {}: {}".format(self.item.ID, time.time() - start)) if persist: self.saveLevel() else: self.character.dirtySkills.add(self) if self.activeLevel == self.__level and self in self.character.dirtySkills: self.character.dirtySkills.remove(self) @property def item(self): if self.__item is None: self.__item = item = Character.getSkillIDMap().get(self.itemID) if item is None: # This skill is no longer in the database and thus invalid it, get rid of it. self.character.removeSkill(self) return self.__item def getModifiedItemAttr(self, key): if key in self.item.attributes: return self.item.attributes[key].value else: return 0 def calculateModifiedAttributes(self, fit, runTime): if self.__suppressed: # or not self.learned - removed for GH issue 101 return item = self.item if item is None: return for effect in item.effects.values(): if effect.runTime == runTime and \ effect.isType("passive") and \ (not fit.isStructure or effect.isType("structure")) and \ effect.activeByDefault: try: effect.handler(fit, self, ("skill",), None, effect=effect) except AttributeError: continue def clear(self): self.__suppressed = False self.commandBonus = 0 def suppress(self): self.__suppressed = True def isSuppressed(self): return self.__suppressed @validates("characterID", "skillID", "level") def validator(self, key, val): if hasattr(self, "_Skill__ro") and self.__ro is True and key != "characterID": raise ReadOnlyException() map = { "characterID": lambda _val: isinstance(_val, int), "skillID" : lambda _val: isinstance(_val, int) } if not map[key](val): raise ValueError(str(val) + " is not a valid value for " + key) else: return val def __deepcopy__(self, memo): copy = Skill(self.character, self.item, self.level, self.__ro) return copy def __repr__(self): return "Skill(ID={}, name={}) at {}".format( self.item.ID, self.item.name, hex(id(self)) ) class ReadOnlyException(Exception): pass