From d0a4fa1c9dca822da2a6d1bdc6528fd70caef56e Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sat, 22 Apr 2017 02:43:06 -0400 Subject: [PATCH 1/5] Get strict skills working. Todo: add engine setting --- eos/db/gamedata/queries.py | 23 ++++++++++++++++++++++- eos/gamedata.py | 20 +++++++++++++++++++- eos/saveddata/character.py | 21 ++++++++++++++++----- service/character.py | 7 ++----- 4 files changed, 59 insertions(+), 12 deletions(-) diff --git a/eos/db/gamedata/queries.py b/eos/db/gamedata/queries.py index 30f51fe6b3..db333fdbdf 100644 --- a/eos/db/gamedata/queries.py +++ b/eos/db/gamedata/queries.py @@ -17,7 +17,7 @@ # along with eos. If not, see . # =============================================================================== -from sqlalchemy.orm import join, exc +from sqlalchemy.orm import join, exc, aliased from sqlalchemy.sql import and_, or_, select import eos.config @@ -315,3 +315,24 @@ def directAttributeRequest(itemIDs, attrIDs): result = gamedata_session.execute(q).fetchall() return result + +def getRequiredFor(itemID, attrMapping): + Attribute1 = aliased(Attribute) + Attribute2 = aliased(Attribute) + + skillToLevelClauses = [] + + for attrSkill, attrLevel in attrMapping.iteritems(): + skillToLevelClauses.append(and_(Attribute1.attributeID == attrSkill, Attribute2.attributeID == attrLevel)) + + queryOr = or_(*skillToLevelClauses) + + q = select((Attribute2.typeID, Attribute2.value), + and_(Attribute1.value == itemID, queryOr), + from_obj=[ + join(Attribute1, Attribute2, Attribute1.typeID == Attribute2.typeID) + ]) + + result = gamedata_session.execute(q).fetchall() + + return result diff --git a/eos/gamedata.py b/eos/gamedata.py index 8700229dca..e0c4ac8632 100644 --- a/eos/gamedata.py +++ b/eos/gamedata.py @@ -250,6 +250,7 @@ def moveAttrs(self): def init(self): self.__race = None self.__requiredSkills = None + self.__requiredFor = None self.__moved = False self.__offensive = None self.__assistive = None @@ -330,6 +331,24 @@ def requiredSkills(self): requiredSkills[item] = skillLvl return self.__requiredSkills + @property + def requiredFor(self): + if self.__requiredFor is None: + self.__requiredFor = dict() + + # Map containing attribute IDs we may need for required skills + srqIDMap = {182: 277, 183: 278, 184: 279, 1285: 1286, 1289: 1287, 1290: 1288} + + # Get relevant attribute values from db (required skill IDs and levels) for our item + q = eos.db.getRequiredFor(self.ID, srqIDMap) + + for itemID, lvl in q: + # Fetch item from database and fill map + item = eos.db.getItem(itemID) + self.__requiredFor[item] = lvl + + return self.__requiredFor + factionMap = { 500001: "caldari", 500002: "minmatar", @@ -442,7 +461,6 @@ def requiresSkill(self, skill, level=None): @property def price(self): - # todo: use `from sqlalchemy import inspect` instead (need to verify it works in old and new OS X builds) if self.__price is not None and getattr(self.__price, '_sa_instance_state', None): pyfalog.debug("Price data for {} was deleted, resetting object".format(self.ID)) diff --git a/eos/saveddata/character.py b/eos/saveddata/character.py index bb316e01d2..9a1bc686df 100644 --- a/eos/saveddata/character.py +++ b/eos/saveddata/character.py @@ -324,8 +324,8 @@ def level(self): return self.activeLevel or 0 - @level.setter - def level(self, level): + def setLevel(self, level, persist=False): + if (level < 0 or level > 5) and level is not None: raise ValueError(str(level) + " is not a valid value for level") @@ -333,10 +333,21 @@ def level(self, level): raise ReadOnlyException() self.activeLevel = level - self.character.dirtySkills.add(self) - if self.activeLevel == self.__level and self in self.character.dirtySkills: - self.character.dirtySkills.remove(self) + for item, rlevel in self.item.requiredFor.iteritems(): + if item.group.category.ID == 16: # Skill category + if level < rlevel: + skill = self.character.getSkill(item.ID) + #print "Removing skill: {}, Dependant level: {}, Required level: {}".format(skill, level, rlevel) + skill.setLevel(None, persist) + + 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): diff --git a/service/character.py b/service/character.py index 602bcb2731..f53b1e4248 100644 --- a/service/character.py +++ b/service/character.py @@ -371,12 +371,9 @@ def changeLevel(charID, skillID, level, persist=False): char = eos.db.getCharacter(charID) skill = char.getSkill(skillID) if isinstance(level, basestring) or level > 5 or level < 0: - skill.level = None + skill.setLevel(None, persist) else: - skill.level = level - - if persist: - skill.saveLevel() + skill.setLevel(level, persist) eos.db.commit() From 58abdcfa221442040bb59028451c46c00e111618 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sat, 22 Apr 2017 03:09:18 -0400 Subject: [PATCH 2/5] Add dependants tab for skills --- gui/itemStats.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/gui/itemStats.py b/gui/itemStats.py index 41226464cc..bb940c653e 100644 --- a/gui/itemStats.py +++ b/gui/itemStats.py @@ -191,6 +191,10 @@ def __init__(self, parent, stuff, item, context=None): self.reqs = ItemRequirements(self.nbContainer, stuff, item) self.nbContainer.AddPage(self.reqs, "Requirements") + if context == "Skill": + self.dependants = ItemDependants(self.nbContainer, stuff, item) + self.nbContainer.AddPage(self.dependants, "Dependants") + self.effects = ItemEffects(self.nbContainer, stuff, item) self.nbContainer.AddPage(self.effects, "Effects") @@ -759,6 +763,55 @@ def getFullSkillTree(self, parentSkill, parent, sbIconId): self.skillIdHistory.append(skill.ID) +class ItemDependants(wx.Panel): + def __init__(self, parent, stuff, item): + wx.Panel.__init__(self, parent, style=wx.TAB_TRAVERSAL) + + # itemId is set by the parent. + self.romanNb = ["0", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X"] + self.skillIdHistory = [] + mainSizer = wx.BoxSizer(wx.VERTICAL) + + self.reqTree = wx.TreeCtrl(self, style=wx.TR_DEFAULT_STYLE | wx.TR_HIDE_ROOT | wx.NO_BORDER) + + mainSizer.Add(self.reqTree, 1, wx.ALL | wx.EXPAND, 0) + + self.SetSizer(mainSizer) + self.root = self.reqTree.AddRoot("WINRARZOR") + self.reqTree.SetPyData(self.root, None) + + self.imageList = wx.ImageList(16, 16) + self.reqTree.SetImageList(self.imageList) + skillBookId = self.imageList.Add(BitmapLoader.getBitmap("skill_small", "gui")) + + self.getFullSkillTree(item, self.root, skillBookId) + + self.Layout() + + def getFullSkillTree(self, parentSkill, parent, sbIconId): + levelToItems = {} + + for item, level in parentSkill.requiredFor.iteritems(): + if level not in levelToItems: + levelToItems[level] = [] + levelToItems[level].append(item) + + for x in sorted(levelToItems.keys()): + items = levelToItems[x] + items.sort(key=lambda x: x.name) + + child = self.reqTree.AppendItem(parent, "Level {}".format(self.romanNb[int(x)]), sbIconId) + for item in items: + + if item.icon: + bitmap = BitmapLoader.getBitmap(item.icon.iconFile, "icons") + itemIcon = self.imageList.Add(bitmap) if bitmap else -1 + else: + itemIcon = -1 + + grand = self.reqTree.AppendItem(child, "{}".format(item.name), itemIcon) + + class ItemEffects(wx.Panel): def __init__(self, parent, stuff, item): wx.Panel.__init__(self, parent) From b21c85059886f631120c445eb21e09075d346fa0 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sat, 22 Apr 2017 03:11:25 -0400 Subject: [PATCH 3/5] move skill mapping to item class --- eos/gamedata.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/eos/gamedata.py b/eos/gamedata.py index e0c4ac8632..a55ff3baa7 100644 --- a/eos/gamedata.py +++ b/eos/gamedata.py @@ -303,6 +303,8 @@ def deleteOverride(self, attr): eos.db.saveddata_session.delete(override) eos.db.commit() + srqIDMap = {182: 277, 183: 278, 184: 279, 1285: 1286, 1289: 1287, 1290: 1288} + @property def requiredSkills(self): if self.__requiredSkills is None: @@ -310,8 +312,7 @@ def requiredSkills(self): self.__requiredSkills = requiredSkills # Map containing attribute IDs we may need for required skills # { requiredSkillX : requiredSkillXLevel } - srqIDMap = {182: 277, 183: 278, 184: 279, 1285: 1286, 1289: 1287, 1290: 1288} - combinedAttrIDs = set(srqIDMap.iterkeys()).union(set(srqIDMap.itervalues())) + combinedAttrIDs = set(self.srqIDMap.iterkeys()).union(set(self.srqIDMap.itervalues())) # Map containing result of the request # { attributeID : attributeValue } skillAttrs = {} @@ -321,7 +322,7 @@ def requiredSkills(self): attrVal = attrInfo[2] skillAttrs[attrID] = attrVal # Go through all attributeID pairs - for srqIDAtrr, srqLvlAttr in srqIDMap.iteritems(): + for srqIDAtrr, srqLvlAttr in self.srqIDMap.iteritems(): # Check if we have both in returned result if srqIDAtrr in skillAttrs and srqLvlAttr in skillAttrs: skillID = int(skillAttrs[srqIDAtrr]) @@ -337,10 +338,9 @@ def requiredFor(self): self.__requiredFor = dict() # Map containing attribute IDs we may need for required skills - srqIDMap = {182: 277, 183: 278, 184: 279, 1285: 1286, 1289: 1287, 1290: 1288} # Get relevant attribute values from db (required skill IDs and levels) for our item - q = eos.db.getRequiredFor(self.ID, srqIDMap) + q = eos.db.getRequiredFor(self.ID, self.srqIDMap) for itemID, lvl in q: # Fetch item from database and fill map From c9a04e886a8b8aea80bb9b434113f822ed6f147b Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sat, 22 Apr 2017 14:02:40 -0400 Subject: [PATCH 4/5] Add eos setting for strict skills, and update info in skill tree without doing a repopulation --- eos/config.py | 3 ++- eos/saveddata/character.py | 17 +++++++++++------ gui/characterEditor.py | 18 ++++++++++++++++-- service/character.py | 1 + 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/eos/config.py b/eos/config.py index 75f797d9c6..ec64e2bd3f 100644 --- a/eos/config.py +++ b/eos/config.py @@ -23,7 +23,8 @@ pyfalog.debug("Saveddata connection string: {0}", saveddata_connectionstring) settings = { - "useStaticAdaptiveArmorHardener": False + "useStaticAdaptiveArmorHardener": False, + "strictSkillLevels": True, } # Autodetect path, only change if the autodetection bugs out. diff --git a/eos/saveddata/character.py b/eos/saveddata/character.py index 9a1bc686df..7febd82c21 100644 --- a/eos/saveddata/character.py +++ b/eos/saveddata/character.py @@ -17,6 +17,7 @@ # along with eos. If not, see . # =============================================================================== +import time from logbook import Logger from itertools import chain @@ -25,6 +26,7 @@ import eos import eos.db +import eos.config from eos.effectHandlerHelpers import HandledItem, HandledImplantBoosterList pyfalog = Logger(__name__) @@ -334,12 +336,15 @@ def setLevel(self, level, persist=False): self.activeLevel = level - for item, rlevel in self.item.requiredFor.iteritems(): - if item.group.category.ID == 16: # Skill category - if level < rlevel: - skill = self.character.getSkill(item.ID) - #print "Removing skill: {}, Dependant level: {}, Required level: {}".format(skill, level, rlevel) - skill.setLevel(None, persist) + if eos.config.settings['strictSkillLevels']: + start = time.time() + for item, rlevel in self.item.requiredFor.iteritems(): + if item.group.category.ID == 16: # Skill category + if 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() diff --git a/gui/characterEditor.py b/gui/characterEditor.py index 00963f6c9c..85c47ab15c 100644 --- a/gui/characterEditor.py +++ b/gui/characterEditor.py @@ -411,14 +411,28 @@ def changeLevel(self, event): skillID = self.skillTreeListCtrl.GetPyData(selection) if level is not None: - self.skillTreeListCtrl.SetItemText(selection, "Level %d" % level if isinstance(level, int) else level, 1) sChar.changeLevel(char.ID, skillID, level, persist=True) elif event.Id == self.revertID: sChar.revertLevel(char.ID, skillID) elif event.Id == self.saveID: sChar.saveSkill(char.ID, skillID) - self.skillTreeListCtrl.SetItemTextColour(selection, None) + # After saving the skill, we need to update not just the selected skill, but all open skills due to strict skill + # level setting. We don't want to refresh tree, as that will lose all expanded categories and users location + # within the tree. Thus, we loop through the tree and refresh the info. + child, cookie = self.skillTreeListCtrl.GetFirstChild(self.root) + while child.IsOk(): + # child = Skill category + grand, cookie2 = self.skillTreeListCtrl.GetFirstChild(child) + while grand.IsOk(): + # grand = Skill (or "dummy" if not expanded) + if self.skillTreeListCtrl.GetItemText(grand) != "dummy": + lvl, dirty = sChar.getSkillLevel(char.ID, self.skillTreeListCtrl.GetPyData(grand)) + self.skillTreeListCtrl.SetItemText(grand, "Level {}".format(lvl) if not isinstance(lvl, basestring) else lvl, 1) + if not dirty: + self.skillTreeListCtrl.SetItemTextColour(grand, None) + grand, cookie2 = self.skillTreeListCtrl.GetNextChild(child, cookie2) + child, cookie = self.skillTreeListCtrl.GetNextChild(self.root, cookie) dirtySkills = sChar.getDirtySkills(char.ID) dirtyGroups = set([skill.item.group.ID for skill in dirtySkills]) diff --git a/service/character.py b/service/character.py index f53b1e4248..edda9fe581 100644 --- a/service/character.py +++ b/service/character.py @@ -370,6 +370,7 @@ def apiUpdateCharSheet(charID, skills): def changeLevel(charID, skillID, level, persist=False): char = eos.db.getCharacter(charID) skill = char.getSkill(skillID) + if isinstance(level, basestring) or level > 5 or level < 0: skill.setLevel(None, persist) else: From 86e6250a630dc5258178e2b74cc7cffde62c6ee3 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sat, 22 Apr 2017 14:49:13 -0400 Subject: [PATCH 5/5] Add strict skills to preference pane --- .../pyfaEnginePreferences.py | 20 +++++++++++++++++++ .../pyfaGeneralPreferences.py | 3 +++ 2 files changed, 23 insertions(+) diff --git a/gui/builtinPreferenceViews/pyfaEnginePreferences.py b/gui/builtinPreferenceViews/pyfaEnginePreferences.py index 4a0f421bc1..f9fba76126 100644 --- a/gui/builtinPreferenceViews/pyfaEnginePreferences.py +++ b/gui/builtinPreferenceViews/pyfaEnginePreferences.py @@ -24,6 +24,8 @@ def populatePanel(self, panel): mainSizer = wx.BoxSizer(wx.VERTICAL) + helpCursor = wx.StockCursor(wx.CURSOR_QUESTION_ARROW) + self.engine_settings = EOSSettings.getInstance() self.stTitle = wx.StaticText(panel, wx.ID_ANY, self.title, wx.DefaultPosition, wx.DefaultSize, 0) @@ -36,8 +38,20 @@ def populatePanel(self, panel): self.cbGlobalForceReload = wx.CheckBox(panel, wx.ID_ANY, u"Factor in reload time when calculating capacitor usage, damage, and tank.", wx.DefaultPosition, wx.DefaultSize, 0) + mainSizer.Add(self.cbGlobalForceReload, 0, wx.ALL | wx.EXPAND, 5) + self.cbStrictSkillLevels = wx.CheckBox(panel, wx.ID_ANY, + u"Enforce strict skill level requirements", + wx.DefaultPosition, wx.DefaultSize, 0) + self.cbStrictSkillLevels.SetCursor(helpCursor) + self.cbStrictSkillLevels.SetToolTip(wx.ToolTip( + u'When enabled, skills will check their dependencies\' requirements when their levels change and reset ' + + u'skills that no longer meet the requirement.\neg: Setting Drones from level V to IV will reset the Heavy ' + + u'Drone Operation skill, as that requires Drones V')) + + mainSizer.Add(self.cbStrictSkillLevels, 0, wx.ALL | wx.EXPAND, 5) + self.cbUniversalAdaptiveArmorHardener = wx.CheckBox(panel, wx.ID_ANY, u"When damage profile is Uniform, set Reactive Armor " + u"Hardener to match (old behavior).", @@ -73,6 +87,9 @@ def populatePanel(self, panel): self.cbGlobalForceReload.SetValue(self.sFit.serviceFittingOptions["useGlobalForceReload"]) self.cbGlobalForceReload.Bind(wx.EVT_CHECKBOX, self.OnCBGlobalForceReloadStateChange) + self.cbStrictSkillLevels.SetValue(self.engine_settings.get("strictSkillLevels")) + self.cbStrictSkillLevels.Bind(wx.EVT_CHECKBOX, self.OnCBStrictSkillLevelsChange) + self.cbUniversalAdaptiveArmorHardener.SetValue(self.engine_settings.get("useStaticAdaptiveArmorHardener")) self.cbUniversalAdaptiveArmorHardener.Bind(wx.EVT_CHECKBOX, self.OnCBUniversalAdaptiveArmorHardenerChange) @@ -82,6 +99,9 @@ def populatePanel(self, panel): def OnCBGlobalForceReloadStateChange(self, event): self.sFit.serviceFittingOptions["useGlobalForceReload"] = self.cbGlobalForceReload.GetValue() + def OnCBStrictSkillLevelsChange(self, event): + self.engine_settings.set("strictSkillLevels", self.cbStrictSkillLevels.GetValue()) + def OnCBUniversalAdaptiveArmorHardenerChange(self, event): self.engine_settings.set("useStaticAdaptiveArmorHardener", self.cbUniversalAdaptiveArmorHardener.GetValue()) diff --git a/gui/builtinPreferenceViews/pyfaGeneralPreferences.py b/gui/builtinPreferenceViews/pyfaGeneralPreferences.py index e75ef55ce7..acdfb398b7 100644 --- a/gui/builtinPreferenceViews/pyfaGeneralPreferences.py +++ b/gui/builtinPreferenceViews/pyfaGeneralPreferences.py @@ -21,6 +21,8 @@ def populatePanel(self, panel): self.openFitsSettings = SettingsProvider.getInstance().getSettings("pyfaPrevOpenFits", {"enabled": False, "pyfaOpenFits": []}) + helpCursor = wx.StockCursor(wx.CURSOR_QUESTION_ARROW) + mainSizer = wx.BoxSizer(wx.VERTICAL) self.stTitle = wx.StaticText(panel, wx.ID_ANY, self.title, wx.DefaultPosition, wx.DefaultSize, 0) @@ -97,6 +99,7 @@ def populatePanel(self, panel): self.stMarketDelay = wx.StaticText(panel, wx.ID_ANY, u"Market Search Delay (ms):", wx.DefaultPosition, wx.DefaultSize, 0) self.stMarketDelay.Wrap(-1) + self.stMarketDelay.SetCursor(helpCursor) self.stMarketDelay.SetToolTip( wx.ToolTip('The delay between a keystroke and the market search. Can help reduce lag when typing fast in the market search box.'))