Files
pyfa/eos/saveddata/character.py

466 lines
14 KiB
Python

# ===============================================================================
# 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 <http://www.gnu.org/licenses/>.
# ===============================================================================
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