diff --git a/eos/config.py b/eos/config.py
index 75f797d9c..ec64e2bd3 100644
--- a/eos/config.py
+++ b/eos/config.py
@@ -23,7 +23,8 @@ else:
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/db/gamedata/queries.py b/eos/db/gamedata/queries.py
index 30f51fe6b..db333fdbd 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 701c4131c..70a051c8f 100644
--- a/eos/gamedata.py
+++ b/eos/gamedata.py
@@ -238,6 +238,7 @@ class Item(EqBase):
def init(self):
self.__race = None
self.__requiredSkills = None
+ self.__requiredFor = None
self.__moved = False
self.__offensive = None
self.__assistive = None
@@ -290,6 +291,8 @@ class Item(EqBase):
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:
@@ -297,8 +300,7 @@ class Item(EqBase):
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 = {}
@@ -308,7 +310,7 @@ class Item(EqBase):
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])
@@ -318,6 +320,23 @@ class Item(EqBase):
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
+
+ # Get relevant attribute values from db (required skill IDs and levels) for our item
+ q = eos.db.getRequiredFor(self.ID, self.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",
diff --git a/eos/saveddata/character.py b/eos/saveddata/character.py
index bb316e01d..7febd82c2 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 @@ from sqlalchemy.orm import validates, reconstructor
import eos
import eos.db
+import eos.config
from eos.effectHandlerHelpers import HandledItem, HandledImplantBoosterList
pyfalog = Logger(__name__)
@@ -324,8 +326,8 @@ class Skill(HandledItem):
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 +335,24 @@ class Skill(HandledItem):
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)
+ 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()
+ 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/gui/builtinPreferenceViews/pyfaEnginePreferences.py b/gui/builtinPreferenceViews/pyfaEnginePreferences.py
index 4a0f421bc..f9fba7612 100644
--- a/gui/builtinPreferenceViews/pyfaEnginePreferences.py
+++ b/gui/builtinPreferenceViews/pyfaEnginePreferences.py
@@ -24,6 +24,8 @@ class PFFittingEnginePref(PreferenceView):
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 @@ class PFFittingEnginePref(PreferenceView):
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 @@ class PFFittingEnginePref(PreferenceView):
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 @@ class PFFittingEnginePref(PreferenceView):
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 e75ef55ce..acdfb398b 100644
--- a/gui/builtinPreferenceViews/pyfaGeneralPreferences.py
+++ b/gui/builtinPreferenceViews/pyfaGeneralPreferences.py
@@ -21,6 +21,8 @@ class PFGeneralPref(PreferenceView):
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 @@ class PFGeneralPref(PreferenceView):
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.'))
diff --git a/gui/characterEditor.py b/gui/characterEditor.py
index a96868e0b..ab9874fe7 100644
--- a/gui/characterEditor.py
+++ b/gui/characterEditor.py
@@ -435,14 +435,28 @@ class SkillTreeView(wx.Panel):
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/gui/itemStats.py b/gui/itemStats.py
index 0a49b3846..2596413d9 100644
--- a/gui/itemStats.py
+++ b/gui/itemStats.py
@@ -191,6 +191,10 @@ class ItemStatsContainer(wx.Panel):
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")
@@ -762,6 +766,55 @@ class ItemRequirements(wx.Panel):
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)
diff --git a/service/character.py b/service/character.py
index daac57b1b..f1bdfb8d3 100644
--- a/service/character.py
+++ b/service/character.py
@@ -383,12 +383,9 @@ class Character(object):
return
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()