# ============================================================================= # Copyright (C) 2010 Diego Duclos # # This file is part of pyfa. # # pyfa is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # pyfa 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with pyfa. If not, see . # ============================================================================= import sys import copy import itertools import json from logbook import Logger import threading from codecs import open from xml.etree import ElementTree from xml.dom import minidom import gzip import config import eos.db from eos.saveddata.implant import Implant as es_Implant from eos.saveddata.character import Character as es_Character, Skill from eos.saveddata.module import Module as es_Module from eos.const import FittingSlot as es_Slot from eos.saveddata.fighter import Fighter as es_Fighter pyfalog = Logger(__name__) def _t(s): import wx return wx.GetTranslation(s) class CharacterImportThread(threading.Thread): def __init__(self, paths, callback): threading.Thread.__init__(self) self.name = "CharacterImport" self.paths = paths self.callback = callback self.running = True def run(self): paths = self.paths sCharacter = Character.getInstance() all5_character = es_Character("All 5", 5) all_skill_ids = [] for skill in all5_character.skills: # Parse out the skill item IDs to make searching it easier later on all_skill_ids.append(skill.itemID) for path in paths: if not self.running: break try: charFile = open(path, mode='r').read() doc = minidom.parseString(charFile) if doc.documentElement.tagName not in ("SerializableCCPCharacter", "SerializableUriCharacter"): pyfalog.error("Incorrect EVEMon XML sheet") raise RuntimeError("Incorrect EVEMon XML sheet") name = doc.getElementsByTagName("name")[0].firstChild.nodeValue securitystatus = doc.getElementsByTagName("securityStatus")[0].firstChild.nodeValue or 0 skill_els = doc.getElementsByTagName("skill") skills = [] for skill in skill_els: if int(skill.getAttribute("typeID")) in all_skill_ids and (0 <= int(skill.getAttribute("level")) <= 5): skills.append({ "typeID": int(skill.getAttribute("typeID")), "level": int(skill.getAttribute("level")), }) else: pyfalog.error( "Attempted to import unknown skill {0} (ID: {1}) (Level: {2})", skill.getAttribute("name"), skill.getAttribute("typeID"), skill.getAttribute("level"), ) char = sCharacter.new(name + " (EVEMon)") sCharacter.apiUpdateCharSheet(char.ID, skills, securitystatus) except (KeyboardInterrupt, SystemExit): raise except Exception as e: pyfalog.error("Exception on character import:") pyfalog.error(e) continue import wx wx.CallAfter(self.callback) def stop(self): self.running = False class SkillBackupThread(threading.Thread): def __init__(self, path, saveFmt, activeFit, callback): threading.Thread.__init__(self) self.name = "SkillBackup" self.path = path self.saveFmt = saveFmt self.activeFit = activeFit self.callback = callback self.running = True def run(self): path = self.path sCharacter = Character.getInstance() backupData = None if self.running: if self.saveFmt == "xml" or self.saveFmt == "emp": backupData = sCharacter.exportXml() else: backupData = sCharacter.exportText() if self.running and backupData is not None: if self.saveFmt == "emp": with gzip.open(path, mode='wb') as backupFile: backupFile.write(backupData.encode()) else: with open(path, mode='w', encoding='utf-8') as backupFile: backupFile.write(backupData) import wx wx.CallAfter(self.callback) def stop(self): self.running = False class Character: instance = None skillReqsDict = {} @classmethod def getInstance(cls): if cls.instance is None: cls.instance = Character() return cls.instance def __init__(self): # Simply initializes default characters in case they aren't in the database yet self.all0() self.all5() def exportText(self): data = "Pyfa exported plan for \"" + self.skillReqsDict['charname'] + "\"\n" data += "=" * 79 + "\n" data += "\n" item = "" try: for s in self.skillReqsDict['skills']: if item == "" or not item == s["item"]: item = s["item"] data += "-" * 79 + "\n" data += "Skills required for {}:\n".format(item) data += "{}{}: {}\n".format(" " * s["indent"], s["skill"], int(s["level"])) data += "-" * 79 + "\n" except (KeyboardInterrupt, SystemExit): raise except Exception: pass return data def exportXml(self): root = ElementTree.Element("plan") root.attrib["name"] = "Pyfa exported plan for " + self.skillReqsDict['charname'] root.attrib["revision"] = config.evemonMinVersion sorts = ElementTree.SubElement(root, "sorting") sorts.attrib["criteria"] = "None" sorts.attrib["order"] = "None" sorts.attrib["groupByPriority"] = "false" skillsSeen = set() for s in self.skillReqsDict['skills']: skillKey = str(s["skillID"]) + "::" + s["skill"] + "::" + str(int(s["level"])) if skillKey in skillsSeen: pass # Duplicate skills confuse EVEMon else: skillsSeen.add(skillKey) entry = ElementTree.SubElement(root, "entry") entry.attrib["skillID"] = str(s["skillID"]) entry.attrib["skill"] = s["skill"] entry.attrib["level"] = str(int(s["level"])) entry.attrib["priority"] = "3" entry.attrib["type"] = "Prerequisite" notes = ElementTree.SubElement(entry, "notes") notes.text = entry.attrib["skill"] # tree = ElementTree.ElementTree(root) data = ElementTree.tostring(root, 'utf-8') prettydata = minidom.parseString(data).toprettyxml(indent=" ") return prettydata @staticmethod def backupSkills(path, saveFmt, activeFit, callback): thread = SkillBackupThread(path, saveFmt, activeFit, callback) pyfalog.debug("Starting backup skills thread.") thread.start() @staticmethod def importCharacter(path, callback): thread = CharacterImportThread(path, callback) pyfalog.debug("Starting import character thread.") thread.start() @staticmethod def all0(): return es_Character.getAll0() def all0ID(self): return self.all0().ID @staticmethod def all5(): return es_Character.getAll5() def all5ID(self): return self.all5().ID @staticmethod def getAlphaCloneList(): return eos.db.getAlphaCloneList() @staticmethod def getCharacterList(): return eos.db.getCharacterList() @staticmethod def getCharacter(identity): char = eos.db.getCharacter(identity) return char def saveCharacter(self, charID): """Save edited skills""" if charID == self.all5ID() or charID == self.all0ID(): return char = eos.db.getCharacter(charID) char.saveLevels() @staticmethod def saveCharacterAs(charID, newName): """Save edited skills as a new character""" char = eos.db.getCharacter(charID) newChar = copy.deepcopy(char) newChar.name = newName eos.db.save(newChar) # revert old char char.revertLevels() return newChar.ID @staticmethod def revertCharacter(charID): """Rollback edited skills""" char = eos.db.getCharacter(charID) char.revertLevels() @staticmethod def getSkillGroups(): cat = eos.db.getCategory(16) groups = [] for grp in cat.groups: if grp.published: groups.append((grp.ID, grp.name)) return sorted(groups, key=lambda x: x[1]) @staticmethod def getSkills(groupID): group = eos.db.getGroup(groupID) skills = [] for skill in group.items: if skill.published is True: skills.append((skill.ID, skill.name)) return sorted(skills, key=lambda x: x[1]) @staticmethod def getSkillsByName(text): items = eos.db.searchSkills(text) skills = [] for skill in items: if skill.published is True: skills.append((skill.ID, skill.name)) return sorted(skills, key=lambda x: x[1]) @staticmethod def setAlphaClone(char, cloneID): char.alphaCloneID = cloneID eos.db.commit() @staticmethod def setSecStatus(char, secStatus): char.secStatus = secStatus eos.db.commit() @staticmethod def getSkillDescription(itemID): return eos.db.getItem(itemID).description @staticmethod def getGroupDescription(groupID): return eos.db.getMarketGroup(groupID).description @staticmethod def getSkillLevel(charID, skillID): skill = eos.db.getCharacter(charID).getSkill(skillID) return float(skill.level) if skill.learned else _t("Not learned"), skill.isDirty @staticmethod def getDirtySkills(charID): return eos.db.getCharacter(charID).dirtySkills @staticmethod def getCharName(charID): return eos.db.getCharacter(charID).name @staticmethod def new(name="New Character"): char = es_Character(name) eos.db.save(char) return char @staticmethod def rename(char, newName): if char.name in ("All 0", "All 5"): pyfalog.info("Cannot rename built in characters.") else: char.name = newName eos.db.commit() @staticmethod def copy(char): newChar = copy.deepcopy(char) eos.db.save(newChar) return newChar @staticmethod def delete(char): eos.db.remove(char) @staticmethod def getApiDetails(charID): # todo: fix this (or get rid of?) return "", "", "", [] char = eos.db.getCharacter(charID) if char.chars is not None: chars = json.loads(char.chars) else: chars = None return char.apiID or "", char.apiKey or "", char.defaultChar or "", chars or [] @staticmethod def getSsoCharacter(charID): char = eos.db.getCharacter(charID) sso = char.getSsoCharacter(config.getClientSecret()) return sso @staticmethod def setSsoCharacter(charID, ssoCharID): char = eos.db.getCharacter(charID) if ssoCharID is not None: sso = eos.db.getSsoCharacter(ssoCharID, config.getClientSecret()) char.setSsoCharacter(sso, config.getClientSecret()) else: char.setSsoCharacter(None, config.getClientSecret()) eos.db.commit() def apiFetch(self, charID, callback): thread = UpdateAPIThread(charID, (self.apiFetchCallback, callback)) thread.start() def apiFetchCallback(self, guiCallback, e=None): eos.db.commit() import wx wx.CallAfter(guiCallback, e) @staticmethod def apiUpdateCharSheet(charID, skills, securitystatus): char = eos.db.getCharacter(charID) char.apiUpdateCharSheet(skills, securitystatus) eos.db.commit() @classmethod def changeLevel(cls, charID, skillID, level, persist=False, ifHigher=False): char = eos.db.getCharacter(charID) skill = char.getSkill(skillID) if ifHigher and level < skill.level: return if isinstance(level, str) or level > 5 or level < 0: skill.setLevel(None, persist) eos.db.commit() elif skill.level != level: cls._trainSkillReqs(char, skill, persist) skill.setLevel(level, persist) eos.db.commit() @classmethod def _trainSkillReqs(cls, char, skill, persist): for childSkillItem, neededSkillLevel in skill.item.requiredSkills.items(): childSkill = char.getSkill(childSkillItem.ID) if childSkill.level < neededSkillLevel: childSkill.setLevel(neededSkillLevel, persist) cls._trainSkillReqs(char, childSkill, persist) @staticmethod def revertLevel(charID, skillID): char = eos.db.getCharacter(charID) skill = char.getSkill(skillID) skill.revert() @staticmethod def saveSkill(charID, skillID): char = eos.db.getCharacter(charID) skill = char.getSkill(skillID) skill.saveLevel() @staticmethod def addImplant(charID, itemID): char = eos.db.getCharacter(charID) if char.ro: pyfalog.error("Trying to add implant to read-only character") return implant = es_Implant(eos.db.getItem(itemID)) char.implants.makeRoom(implant) char.implants.append(implant) eos.db.commit() @staticmethod def removeImplant(charID, implant): char = eos.db.getCharacter(charID) char.implants.remove(implant) eos.db.commit() @staticmethod def getImplants(charID): char = eos.db.getCharacter(charID) return char.implants def checkRequirements(self, fit): # toCheck = [] reqs = {} for thing in itertools.chain(fit.modules, fit.drones, fit.fighters, (fit.ship,), fit.appliedImplants, fit.boosters): if isinstance(thing, es_Module) and thing.slot == es_Slot.RIG: continue for attr in ("item", "charge"): if attr == "charge" and isinstance(thing, es_Fighter): # Fighter Bombers are automatically charged with micro bombs. # These have skill requirements attached, but aren't used in EVE. continue subThing = getattr(thing, attr, None) subReqs = {} if subThing is not None: if isinstance(thing, es_Fighter) and attr == "charge": continue self._checkRequirements(fit.character, subThing, subReqs) if subReqs: reqs[subThing] = subReqs return reqs def _checkRequirements(self, char, subThing, reqs): for req, level in subThing.requiredSkills.items(): name = req.name ID = req.ID info = reqs.get(name) currLevel, subs = info if info is not None else 0, {} if level > currLevel and (char is None or char.getSkill(req).level < level): reqs[name] = (level, ID, subs) self._checkRequirements(char, req, subs) return reqs class UpdateAPIThread(threading.Thread): def __init__(self, charID, callback): threading.Thread.__init__(self) self.name = "CheckUpdate" self.callback = callback self.charID = charID self.running = True def run(self): try: char = eos.db.getCharacter(self.charID) from service.esi import Esi sEsi = Esi.getInstance() sChar = Character.getInstance() ssoChar = sChar.getSsoCharacter(char.ID) if not self.running: self.callback[0](self.callback[1]) return resp = sEsi.getSkills(ssoChar.ID) if not self.running: self.callback[0](self.callback[1]) return # todo: check if alpha. if so, pop up a question if they want to apply it as alpha. Use threading events to set the answer? char.clearSkills() for skillRow in resp["skills"]: char.addSkill(Skill(char, skillRow["skill_id"], skillRow["trained_skill_level"])) if not self.running: self.callback[0](self.callback[1]) return resp = sEsi.getSecStatus(ssoChar.ID) char.secStatus = resp['security_status'] self.callback[0](self.callback[1]) except (KeyboardInterrupt, SystemExit): raise except Exception as ex: pyfalog.warn(ex) self.callback[0](self.callback[1], sys.exc_info()) def stop(self): self.running = False