From aaa5a6ae18336afa3cf72e15daf030d8345e6b01 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Tue, 30 Jun 2015 13:51:15 -0400 Subject: [PATCH 01/20] Gracefully handle invalid boosters in database (both itemIDs that don't exist as well as non-booster items). Implants need a little more work --- eos/effectHandlerHelpers.py | 12 ++++++++---- eos/saveddata/booster.py | 10 +++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/eos/effectHandlerHelpers.py b/eos/effectHandlerHelpers.py index b3b4c75ee..ecf20d9e1 100644 --- a/eos/effectHandlerHelpers.py +++ b/eos/effectHandlerHelpers.py @@ -233,10 +233,14 @@ class HandledImplantBoosterList(HandledList): self.__slotCache = {} def append(self, implant): - if self.__slotCache.has_key(implant.slot): - raise ValueError("Implant/Booster slot already in use, remove the old one first or set replace = True") - self.__slotCache[implant.slot] = implant - HandledList.append(self, implant) + try: + if self.__slotCache.has_key(implant.slot): + raise ValueError("Implant/Booster slot already in use, remove the old one first or set replace = True") + self.__slotCache[implant.slot] = implant + HandledList.append(self, implant) + except: + # if anything goes wrong, simply remove the item + eos.db.remove(implant) def remove(self, implant): HandledList.remove(self, implant) diff --git a/eos/saveddata/booster.py b/eos/saveddata/booster.py index 001a163de..d2125404f 100644 --- a/eos/saveddata/booster.py +++ b/eos/saveddata/booster.py @@ -46,9 +46,13 @@ class Booster(HandledItem, ItemAttrShortcut): def __fetchItemInfo(self): import eos.db - self.__item = eos.db.getItem(self.itemID) - self.__slot = self.__calculateSlot(self.__item) - self.build() + item = eos.db.getItem(self.itemID) + if item: + self.__item = item + self.__slot = self.__calculateSlot(self.__item) + self.build() + else: + raise ValueError("Invalid item as Booster:", self.itemID) def iterSideEffects(self): return self.__sideEffects.__iter__() From fa2b1e38217a74d90b9fbe8986c6a365f7f31aed Mon Sep 17 00:00:00 2001 From: blitzmann Date: Wed, 1 Jul 2015 02:20:56 -0400 Subject: [PATCH 02/20] Handle invalid modules. This streamlines the module init code from both program and database sources. When loading from the database, we ensure that the module item is actually an item. If not, we set a flag to delete it (which is picked up by the collection class)(can't use exceptions as there's no place to catch them) --- eos/effectHandlerHelpers.py | 4 ++ eos/saveddata/module.py | 131 +++++++++++++----------------------- 2 files changed, 52 insertions(+), 83 deletions(-) diff --git a/eos/effectHandlerHelpers.py b/eos/effectHandlerHelpers.py index ecf20d9e1..8c7f87e2d 100644 --- a/eos/effectHandlerHelpers.py +++ b/eos/effectHandlerHelpers.py @@ -115,10 +115,14 @@ class HandledModuleList(HandledList): del self[emptyPosition] mod.position = emptyPosition HandledList.insert(self, emptyPosition, mod) + if mod.isInvalid: + self.remove(mod) return mod.position = len(self) HandledList.append(self, mod) + if mod.isInvalid: + self.remove(mod) def insert(self, index, mod): mod.position = index diff --git a/eos/saveddata/module.py b/eos/saveddata/module.py index 4688e53c4..cf77c918d 100644 --- a/eos/saveddata/module.py +++ b/eos/saveddata/module.py @@ -23,6 +23,7 @@ from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, C from eos.effectHandlerHelpers import HandledItem, HandledCharge from eos.enum import Enum from eos.mathUtils import floorFloat +import eos.db class State(Enum): OFFLINE = -1 @@ -49,95 +50,75 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): MINING_ATTRIBUTES = ("miningAmount", ) def __init__(self, item): - self.__item = item if item != None else 0 + """Initialize a module from the program""" + self.__item = item + self.__charge = None + self.__invalid = False self.itemID = item.ID if item is not None else None - self.__charge = 0 self.projected = False self.state = State.ONLINE + self.build() + + @reconstructor + def init(self): + """Initialize a module form the database and validate""" + self.__item = None + self.__charge = None + self.__invalid = False + + if self.itemID: + # if item does not exist, set invalid + item = eos.db.getItem(self.itemID) + if item is None: + self.__invalid = True + self.__item = item + if self.chargeID: + # if charge does not exist, just ignore it. This doesn't remove it + # from the database, but it will allow the fit to load and the user + # to add another charge + charge = eos.db.getItem(self.chargeID) + if charge: + self.__charge = charge + + self.build() + + def build(self): + """Builds internal module variables from both init's""" + if self.__item and self.__item.category.name != "Module": + self.__invalid = True + if self.__charge and self.__charge.category.name != "Charge": + self.__charge = None + self.__dps = None self.__miningyield = None self.__volley = None self.__reloadTime = None self.__reloadForce = None self.__chargeCycles = None + self.__hardpoint = Hardpoint.NONE self.__itemModifiedAttributes = ModifiedAttributeDict() - self.__slot = None - - if item != None: - self.__itemModifiedAttributes.original = item.attributes - self.__hardpoint = self.__calculateHardpoint(item) - self.__slot = self.__calculateSlot(item) - self.__chargeModifiedAttributes = ModifiedAttributeDict() + self.__slot = self.dummySlot # defaults to None - @reconstructor - def init(self): - if self.dummySlot is None: - self.__item = None - self.__charge = None - self.__volley = None - self.__dps = None - self.__miningyield = None - self.__reloadTime = None - self.__reloadForce = None - self.__chargeCycles = None - else: - self.__slot = self.dummySlot - self.__item = 0 - self.__charge = 0 - self.__dps = 0 - self.__miningyield = 0 - self.__volley = 0 - self.__reloadTime = 0 - self.__reloadForce = None - self.__chargeCycles = 0 - self.__hardpoint = Hardpoint.NONE - self.__itemModifiedAttributes = ModifiedAttributeDict() - self.__chargeModifiedAttributes = ModifiedAttributeDict() - - def __fetchItemInfo(self): - import eos.db - item = eos.db.getItem(self.itemID) - self.__item = item - self.__itemModifiedAttributes = ModifiedAttributeDict() - self.__itemModifiedAttributes.original = item.attributes - self.__hardpoint = self.__calculateHardpoint(item) - self.__slot = self.__calculateSlot(item) - - def __fetchChargeInfo(self): - self.__chargeModifiedAttributes = ModifiedAttributeDict() - if self.chargeID is not None: - import eos.db - charge = eos.db.getItem(self.chargeID) - self.__charge = charge - self.__chargeModifiedAttributes.original = charge.attributes - else: - self.__charge = 0 + if self.__item: + self.__itemModifiedAttributes.original = self.__item.attributes + self.__hardpoint = self.__calculateHardpoint(self.__item) + self.__slot = self.__calculateSlot(self.__item) + if self.__charge: + self.__chargeModifiedAttributes.original = self.__charge.attributes @classmethod def buildEmpty(cls, slot): empty = Module(None) empty.__slot = slot - empty.__hardpoint = Hardpoint.NONE - empty.__item = 0 - empty.__charge = 0 empty.dummySlot = slot - empty.__itemModifiedAttributes = ModifiedAttributeDict() - empty.__chargeModifiedAttributes = ModifiedAttributeDict() - return empty @classmethod def buildRack(cls, slot): empty = Rack(None) empty.__slot = slot - empty.__hardpoint = Hardpoint.NONE - empty.__item = 0 - empty.__charge = 0 empty.dummySlot = slot - empty.__itemModifiedAttributes = ModifiedAttributeDict() - empty.__chargeModifiedAttributes = ModifiedAttributeDict() - return empty @property @@ -146,11 +127,10 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): @property def hardpoint(self): - if self.__item is None: - self.__fetchItemInfo() - return self.__hardpoint - + @property + def isInvalid(self): + return self.__invalid @property def numCharges(self): if self.charge is None: @@ -263,38 +243,23 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): @property def slot(self): - if self.__item is None: - self.__fetchItemInfo() - return self.__slot @property def itemModifiedAttributes(self): - if self.__item is None: - self.__fetchItemInfo() - return self.__itemModifiedAttributes @property def chargeModifiedAttributes(self): - if self.__charge is None: - self.__fetchChargeInfo() - return self.__chargeModifiedAttributes @property def item(self): - if self.__item is None: - self.__fetchItemInfo() - return self.__item if self.__item != 0 else None @property def charge(self): - if self.__charge is None: - self.__fetchChargeInfo() - return self.__charge if self.__charge != 0 else None @charge.setter From 1c18a5207c43c4376212c51660619365fffde4af Mon Sep 17 00:00:00 2001 From: blitzmann Date: Wed, 1 Jul 2015 13:32:31 -0400 Subject: [PATCH 03/20] System Effects are wrapped in Module class, even though they are not modules. Account for this. --- eos/saveddata/module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eos/saveddata/module.py b/eos/saveddata/module.py index cf77c918d..3fea48593 100644 --- a/eos/saveddata/module.py +++ b/eos/saveddata/module.py @@ -84,7 +84,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): def build(self): """Builds internal module variables from both init's""" - if self.__item and self.__item.category.name != "Module": + if self.__item and self.__item.category.name != "Module" and self.__item.group.name != "Effect Beacon": self.__invalid = True if self.__charge and self.__charge.category.name != "Charge": self.__charge = None From f737f292e3e6a4d913af2fbe3973825ba11a0bbf Mon Sep 17 00:00:00 2001 From: blitzmann Date: Wed, 1 Jul 2015 13:34:19 -0400 Subject: [PATCH 04/20] Refine appending projected modules. Ensure that module can actually be projected, and also ensure that we only have 1 system effect running at a time. Invalid modules are removed at earliest opportunity as we are later accessing attributes that may not be there for corrupted data. --- eos/effectHandlerHelpers.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/eos/effectHandlerHelpers.py b/eos/effectHandlerHelpers.py index 8c7f87e2d..0266df4ea 100644 --- a/eos/effectHandlerHelpers.py +++ b/eos/effectHandlerHelpers.py @@ -272,9 +272,28 @@ class HandledImplantBoosterList(HandledList): class HandledProjectedModList(HandledList): def append(self, proj): + if proj.isInvalid: + # we must include it before we remove it. doing it this way ensures + # rows and relationships in databse are removed as well + HandledList.append(self, proj) + self.remove(proj) + proj.projected = True + isSystemEffect = proj.item.group.name == "Effect Beacon" + + if isSystemEffect: + # remove other system effects - only 1 per fit plz + oldEffect = next((m for m in self if m.item.group.name == "Effect Beacon"), None) + + if oldEffect: + self.remove(oldEffect) + HandledList.append(self, proj) + # Remove non-projectable modules + if not proj.item.isType("projected") and not isSystemEffect: + self.remove(proj) + class HandledProjectedDroneList(HandledDroneList): def append(self, proj): proj.projected = True From bcc77f11cd68fa15e94d98af6f30f87e0b78d0ca Mon Sep 17 00:00:00 2001 From: blitzmann Date: Wed, 1 Jul 2015 14:50:08 -0400 Subject: [PATCH 05/20] Handle invalid projected drones --- eos/effectHandlerHelpers.py | 6 ++- eos/saveddata/drone.py | 77 +++++++++++++++++-------------------- eos/saveddata/module.py | 5 ++- 3 files changed, 44 insertions(+), 44 deletions(-) diff --git a/eos/effectHandlerHelpers.py b/eos/effectHandlerHelpers.py index 0266df4ea..98997e8da 100644 --- a/eos/effectHandlerHelpers.py +++ b/eos/effectHandlerHelpers.py @@ -297,7 +297,11 @@ class HandledProjectedModList(HandledList): class HandledProjectedDroneList(HandledDroneList): def append(self, proj): proj.projected = True - list.append(self, proj) + HandledList.append(self, proj) + + # Remove invalid or non-projectable drones + if proj.isInvalid or not proj.item.isType("projected"): + self.remove(proj) class HandledProjectedFitList(HandledList): def append(self, proj): diff --git a/eos/saveddata/drone.py b/eos/saveddata/drone.py index 289674f1e..0c7f9a541 100644 --- a/eos/saveddata/drone.py +++ b/eos/saveddata/drone.py @@ -20,81 +20,76 @@ from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, ChargeAttrShortcut from eos.effectHandlerHelpers import HandledItem, HandledCharge from sqlalchemy.orm import validates, reconstructor +import eos.db class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): DAMAGE_TYPES = ("em", "kinetic", "explosive", "thermal") MINING_ATTRIBUTES = ("miningAmount",) def __init__(self, item): - if item.category.name != "Drone": - raise ValueError("Passed item is not a drone") - + """Initialize a drone from the program""" self.__item = item - self.__charge = None - self.itemID = item.ID + self.__invalid = False + self.itemID = item.ID if item is not None else None self.amount = 0 self.amountActive = 0 - self.__dps = None - self.__volley = None - self.__miningyield = None self.projected = False - self.__itemModifiedAttributes = ModifiedAttributeDict() - self.itemModifiedAttributes.original = self.item.attributes + self.build() + @reconstructor def init(self): + """Initialize a drone from the database and validate""" + self.__item = None + self.__invalid = False + + if self.itemID: + # if item does not exist, set invalid + item = eos.db.getItem(self.itemID) + if item is None: + self.__invalid = True + self.__item = item + + self.build() + + def build(self): + if self.__item and self.__item.category.name != "Drone": + self.__invalid = True + + self.__charge = None self.__dps = None self.__volley = None self.__miningyield = None - self.__item = None - self.__charge = None - - def __fetchItemInfo(self): - import eos.db - self.__item = eos.db.getItem(self.itemID) - self.__charge = None self.__itemModifiedAttributes = ModifiedAttributeDict() - self.__itemModifiedAttributes.original = self.item.attributes - - def __fetchChargeInfo(self): - chargeID = self.getModifiedItemAttr("entityMissileTypeID") self.__chargeModifiedAttributes = ModifiedAttributeDict() - if chargeID is not None: - import eos.db - charge = eos.db.getItem(int(chargeID)) - self.__charge = charge - self.chargeModifiedAttributes.original = charge.attributes - else: - self.__charge = 0 + if self.__item: + self.__itemModifiedAttributes.original = self.__item.attributes + chargeID = self.getModifiedItemAttr("entityMissileTypeID") + if chargeID is not None: + charge = eos.db.getItem(int(chargeID)) + self.__charge = charge + self.__chargeModifiedAttributes.original = charge.attributes @property def itemModifiedAttributes(self): - if self.__item is None: - self.__fetchItemInfo() - return self.__itemModifiedAttributes @property def chargeModifiedAttributes(self): - if self.__charge is None: - self.__fetchChargeInfo() - return self.__chargeModifiedAttributes @property - def item(self): - if self.__item is None: - self.__fetchItemInfo() + def isInvalid(self): + return self.__invalid + @property + def item(self): return self.__item @property def charge(self): - if self.__charge is None: - self.__fetchChargeInfo() - - return self.__charge if self.__charge != 0 else None + return self.__charge @property def dealsDamage(self): diff --git a/eos/saveddata/module.py b/eos/saveddata/module.py index 3fea48593..c472c0927 100644 --- a/eos/saveddata/module.py +++ b/eos/saveddata/module.py @@ -61,7 +61,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): @reconstructor def init(self): - """Initialize a module form the database and validate""" + """Initialize a module from the database and validate""" self.__item = None self.__charge = None self.__invalid = False @@ -128,9 +128,11 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): @property def hardpoint(self): return self.__hardpoint + @property def isInvalid(self): return self.__invalid + @property def numCharges(self): if self.charge is None: @@ -481,7 +483,6 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): def getValidCharges(self): validCharges = set() - import eos.db for i in range(5): itemChargeGroup = self.getModifiedItemAttr('chargeGroup' + str(i)) if itemChargeGroup is not None: From fa9f324f782da154bc6cb7afa415d60a21f95f60 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Wed, 1 Jul 2015 14:55:05 -0400 Subject: [PATCH 06/20] Handle invalid drones --- eos/effectHandlerHelpers.py | 27 +++------------------------ 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/eos/effectHandlerHelpers.py b/eos/effectHandlerHelpers.py index 98997e8da..349e89277 100644 --- a/eos/effectHandlerHelpers.py +++ b/eos/effectHandlerHelpers.py @@ -164,32 +164,11 @@ class HandledDroneList(HandledList): return d def append(self, drone): - list.append(self, drone) + HandledList.append(self, drone) - def remove(self, drone): - HandledList.remove(self, drone) + if drone.isInvalid: + self.remove(drone) - def appendItem(self, item, amount = 1): - if amount < 1: ValueError("Amount of drones to add should be >= 1") - d = self.findFirst(item) - - if d is None: - d = eos.types.Drone(item) - self.append(d) - - d.amount += amount - return d - - def removeItem(self, item, amount): - if amount < 1: ValueError("Amount of drones to remove should be >= 1") - d = self.findFirst(item) - if d is None: return - d.amount -= amount - if d.amount <= 0: - self.remove(d) - return None - - return d class HandledCargoList(HandledList): # shameless copy of HandledDroneList From 4a5ae9f6f1c1ff552aa5bd329ad815017e38d457 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Wed, 1 Jul 2015 15:21:27 -0400 Subject: [PATCH 07/20] Handle invalid cargo. Noticed that cargo nor drones are removed from the database with these methods. Not sure why - projected drones and modules are correctly removed in similar ways --- eos/effectHandlerHelpers.py | 32 +++++--------------------------- eos/saveddata/cargo.py | 37 ++++++++++++++++++++++--------------- 2 files changed, 27 insertions(+), 42 deletions(-) diff --git a/eos/effectHandlerHelpers.py b/eos/effectHandlerHelpers.py index 349e89277..df2addf16 100644 --- a/eos/effectHandlerHelpers.py +++ b/eos/effectHandlerHelpers.py @@ -167,13 +167,11 @@ class HandledDroneList(HandledList): HandledList.append(self, drone) if drone.isInvalid: + # @todo figure out why this DOES NOT remove drone from database self.remove(drone) class HandledCargoList(HandledList): - # shameless copy of HandledDroneList - # I have no idea what this does, but I needed it - # @todo: investigate this def find(self, item): for d in self: if d.item == item: @@ -184,32 +182,12 @@ class HandledCargoList(HandledList): return d def append(self, cargo): - list.append(self, cargo) + HandledList.append(self, cargo) - def remove(self, cargo): - HandledList.remove(self, cargo) + if cargo.isInvalid: + # @todo figure out why this DOES NOT remove the cargo from database + self.remove(cargo) - def appendItem(self, item, qty = 1): - if qty < 1: ValueError("Amount of cargo to add should be >= 1") - d = self.findFirst(item) - - if d is None: - d = eos.types.Cargo(item) - self.append(d) - - d.qty += qty - return d - - def removeItem(self, item, qty): - if qty < 1: ValueError("Amount of cargo to remove should be >= 1") - d = self.findFirst(item) - if d is None: return - d.qty -= qty - if d.qty <= 0: - self.remove(d) - return None - - return d class HandledImplantBoosterList(HandledList): def __init__(self): diff --git a/eos/saveddata/cargo.py b/eos/saveddata/cargo.py index 95ef072b6..fa68de1a0 100644 --- a/eos/saveddata/cargo.py +++ b/eos/saveddata/cargo.py @@ -20,40 +20,47 @@ from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, ChargeAttrShortcut from eos.effectHandlerHelpers import HandledItem, HandledCharge from sqlalchemy.orm import validates, reconstructor +import eos.db + -# Cargo class copied from Implant class and hacked to make work. \o/ -# @todo: clean me up, Scotty class Cargo(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): def __init__(self, item): + """Initialize cargo from the program""" self.__item = item - self.itemID = item.ID + self.__invalid = False + self.itemID = item.ID if item is not None else None self.amount = 0 self.__itemModifiedAttributes = ModifiedAttributeDict() - self.__itemModifiedAttributes.original = self.item.attributes + self.__itemModifiedAttributes.original = item.attributes @reconstructor def init(self): + """Initialize cargo from the database and validate""" self.__item = None - - def __fetchItemInfo(self): - import eos.db - self.__item = eos.db.getItem(self.itemID) + self.__invalid = False self.__itemModifiedAttributes = ModifiedAttributeDict() - self.__itemModifiedAttributes.original = self.__item.attributes + + if self.itemID: + # if item does not exist, set invalid + item = eos.db.getItem(self.itemID) + if item is None: + self.__invalid = True + self.__item = item + + if self.__item: + self.__itemModifiedAttributes.original = self.__item.attributes @property def itemModifiedAttributes(self): - if self.__item is None: - self.__fetchItemInfo() - return self.__itemModifiedAttributes @property - def item(self): - if self.__item is None: - self.__fetchItemInfo() + def isInvalid(self): + return self.__invalid + @property + def item(self): return self.__item def clear(self): From 51696c509fee3f560fac383ddc2cdc8e113e2662 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Wed, 1 Jul 2015 20:54:40 -0400 Subject: [PATCH 08/20] Merged Cargo and Drone collection class (essentially the same). Utilized SQLAlchemy's `flag_modified()` to force SA to update DB (in this case, remove the entry) --- eos/db/saveddata/fit.py | 8 +++---- eos/effectHandlerHelpers.py | 45 ++++++++++++------------------------- eos/saveddata/fit.py | 7 +++--- 3 files changed, 20 insertions(+), 40 deletions(-) diff --git a/eos/db/saveddata/fit.py b/eos/db/saveddata/fit.py index 6dfea588b..478a576a6 100644 --- a/eos/db/saveddata/fit.py +++ b/eos/db/saveddata/fit.py @@ -27,9 +27,7 @@ from eos.db.saveddata.drone import drones_table from eos.db.saveddata.cargo import cargo_table from eos.db.saveddata.implant import fitImplants_table from eos.types import Fit, Module, User, Booster, Drone, Cargo, Implant, Character, DamagePattern, TargetResists -from eos.effectHandlerHelpers import HandledModuleList, HandledDroneList, \ -HandledImplantBoosterList, HandledProjectedModList, HandledProjectedDroneList, \ -HandledProjectedFitList, HandledCargoList +from eos.effectHandlerHelpers import * fits_table = Table("fits", saveddata_meta, Column("ID", Integer, primary_key = True), @@ -56,9 +54,9 @@ mapper(Fit, fits_table, primaryjoin = and_(modules_table.c.fitID == fits_table.c.ID, modules_table.c.projected == True)), "owner" : relation(User, backref = "fits"), "_Fit__boosters" : relation(Booster, collection_class = HandledImplantBoosterList, cascade='all, delete, delete-orphan', single_parent=True), - "_Fit__drones" : relation(Drone, collection_class = HandledDroneList, cascade='all, delete, delete-orphan', single_parent=True, + "_Fit__drones" : relation(Drone, collection_class = HandledDroneCargoList, cascade='all, delete, delete-orphan', single_parent=True, primaryjoin = and_(drones_table.c.fitID == fits_table.c.ID, drones_table.c.projected == False)), - "_Fit__cargo" : relation(Cargo, collection_class = HandledCargoList, cascade='all, delete, delete-orphan', single_parent=True, + "_Fit__cargo" : relation(Cargo, collection_class = HandledDroneCargoList, cascade='all, delete, delete-orphan', single_parent=True, primaryjoin = and_(cargo_table.c.fitID == fits_table.c.ID)), "_Fit__projectedDrones" : relation(Drone, collection_class = HandledProjectedDroneList, cascade='all, delete, delete-orphan', single_parent=True, primaryjoin = and_(drones_table.c.fitID == fits_table.c.ID, drones_table.c.projected == True)), diff --git a/eos/effectHandlerHelpers.py b/eos/effectHandlerHelpers.py index df2addf16..be669a24a 100644 --- a/eos/effectHandlerHelpers.py +++ b/eos/effectHandlerHelpers.py @@ -17,6 +17,7 @@ # along with eos. If not, see . #=============================================================================== +from sqlalchemy.orm.attributes import flag_modified import eos.db import eos.types @@ -153,41 +154,23 @@ class HandledModuleList(HandledList): if mod.getModifiedItemAttr("subSystemSlot") == slot: del self[i] -class HandledDroneList(HandledList): +class HandledDroneCargoList(HandledList): def find(self, item): - for d in self: - if d.item == item: - yield d + for o in self: + if o.item == item: + yield o def findFirst(self, item): - for d in self.find(item): - return d + for o in self.find(item): + return o - def append(self, drone): - HandledList.append(self, drone) - - if drone.isInvalid: - # @todo figure out why this DOES NOT remove drone from database - self.remove(drone) - - -class HandledCargoList(HandledList): - def find(self, item): - for d in self: - if d.item == item: - yield d - - def findFirst(self, item): - for d in self.find(item): - return d - - def append(self, cargo): - HandledList.append(self, cargo) - - if cargo.isInvalid: - # @todo figure out why this DOES NOT remove the cargo from database - self.remove(cargo) + def append(self, obj): + HandledList.append(self, obj) + if obj.isInvalid: + # we must flag it as modified, otherwise it will not be removed from the database + flag_modified(obj, "itemID") + self.remove(obj) class HandledImplantBoosterList(HandledList): def __init__(self): @@ -251,7 +234,7 @@ class HandledProjectedModList(HandledList): if not proj.item.isType("projected") and not isSystemEffect: self.remove(proj) -class HandledProjectedDroneList(HandledDroneList): +class HandledProjectedDroneList(HandledDroneCargoList): def append(self, proj): proj.projected = True HandledList.append(self, proj) diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py index f8328371f..12cf1a7bd 100644 --- a/eos/saveddata/fit.py +++ b/eos/saveddata/fit.py @@ -17,8 +17,7 @@ # along with eos. If not, see . #=============================================================================== -from eos.effectHandlerHelpers import HandledList, HandledModuleList, HandledDroneList, HandledImplantBoosterList, \ -HandledProjectedFitList, HandledProjectedModList, HandledProjectedDroneList, HandledCargoList +from eos.effectHandlerHelpers import * from eos.modifiedAttributeDict import ModifiedAttributeDict from sqlalchemy.orm import validates, reconstructor from itertools import chain @@ -50,8 +49,8 @@ class Fit(object): def __init__(self): self.__modules = HandledModuleList() - self.__drones = HandledDroneList() - self.__cargo = HandledCargoList() + self.__drones = HandledDroneCargoList() + self.__cargo = HandledDroneCargoList() self.__implants = HandledImplantBoosterList() self.__boosters = HandledImplantBoosterList() self.__projectedFits = HandledProjectedFitList() From 717080b58cb3defc3408df015285fd79b543e176 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Thu, 2 Jul 2015 00:48:32 -0400 Subject: [PATCH 09/20] Handle invalid implants and boosters. Uses a different method to ensure implant and booster slot is not duplicated. Still need to modify existing databases to remove Booster table constraint. Reverts a previous commit: "Gracefully handle invalid boosters in database (both itemIDs that don't exist as well as non-booster items). Implants need a little more work" (aaa5a6ae18336afa3cf72e15daf030d8345e6b01) --- eos/db/saveddata/booster.py | 2 +- eos/db/saveddata/fit.py | 2 +- eos/effectHandlerHelpers.py | 58 ++++++++++++----------------------- eos/saveddata/booster.py | 60 ++++++++++++++++++++----------------- eos/saveddata/implant.py | 44 ++++++++++++++++----------- service/fit.py | 2 -- 6 files changed, 79 insertions(+), 89 deletions(-) diff --git a/eos/db/saveddata/booster.py b/eos/db/saveddata/booster.py index 459bb76b5..a31904b12 100644 --- a/eos/db/saveddata/booster.py +++ b/eos/db/saveddata/booster.py @@ -29,7 +29,7 @@ boosters_table = Table("boosters", saveddata_meta, Column("itemID", Integer), Column("fitID", Integer, ForeignKey("fits.ID"), nullable = False), Column("active", Boolean), - UniqueConstraint("itemID", "fitID")) + ) activeSideEffects_table = Table("boostersActiveSideEffects", saveddata_meta, Column("boosterID", ForeignKey("boosters.ID"), primary_key = True), diff --git a/eos/db/saveddata/fit.py b/eos/db/saveddata/fit.py index 478a576a6..0677728d8 100644 --- a/eos/db/saveddata/fit.py +++ b/eos/db/saveddata/fit.py @@ -60,7 +60,7 @@ mapper(Fit, fits_table, primaryjoin = and_(cargo_table.c.fitID == fits_table.c.ID)), "_Fit__projectedDrones" : relation(Drone, collection_class = HandledProjectedDroneList, cascade='all, delete, delete-orphan', single_parent=True, primaryjoin = and_(drones_table.c.fitID == fits_table.c.ID, drones_table.c.projected == True)), - "_Fit__implants" : relation(Implant, collection_class = HandledImplantBoosterList, cascade='all, delete, delete-orphan', single_parent=True, + "_Fit__implants" : relation(Implant, collection_class = HandledImplantBoosterList, cascade='all, delete, delete-orphan', backref='fit', single_parent=True, primaryjoin = fitImplants_table.c.fitID == fits_table.c.ID, secondaryjoin = fitImplants_table.c.implantID == Implant.ID, secondary = fitImplants_table), diff --git a/eos/effectHandlerHelpers.py b/eos/effectHandlerHelpers.py index be669a24a..1311dcf6a 100644 --- a/eos/effectHandlerHelpers.py +++ b/eos/effectHandlerHelpers.py @@ -164,57 +164,37 @@ class HandledDroneCargoList(HandledList): for o in self.find(item): return o - def append(self, obj): - HandledList.append(self, obj) + def append(self, thing): + HandledList.append(self, thing) - if obj.isInvalid: + if thing.isInvalid: # we must flag it as modified, otherwise it will not be removed from the database - flag_modified(obj, "itemID") - self.remove(obj) + flag_modified(thing, "itemID") + self.remove(thing) class HandledImplantBoosterList(HandledList): - def __init__(self): - self.__slotCache = {} + def append(self, thing): + if thing.isInvalid: + HandledList.append(self, thing) + self.remove(thing) - def append(self, implant): - try: - if self.__slotCache.has_key(implant.slot): - raise ValueError("Implant/Booster slot already in use, remove the old one first or set replace = True") - self.__slotCache[implant.slot] = implant - HandledList.append(self, implant) - except: - # if anything goes wrong, simply remove the item - eos.db.remove(implant) + # if needed, remove booster that was occupying slot + oldObj = next((m for m in self if m.slot == thing.slot), None) + if oldObj: + self.remove(oldObj) - def remove(self, implant): - HandledList.remove(self, implant) - del self.__slotCache[implant.slot] - # While we deleted this implant, in edge case seems like not all references - # to it are removed and object still lives in session; forcibly remove it, - # or otherwise when adding the same booster twice booster's table (typeID, fitID) - # constraint will report database integrity error - # TODO: make a proper fix, probably by adjusting fit-boosters sqlalchemy relationships - eos.db.remove(implant) + HandledList.append(self, thing) - def freeSlot(self, slot): - if hasattr(slot, "slot"): - slot = slot.slot - - try: - implant = self.__slotCache[slot] - except KeyError: - return False - try: - self.remove(implant) - except ValueError: - return False - return True + def remove(self, thing): + # We must flag it as modified, otherwise it not be removed from the database + flag_modified(thing, "itemID") + HandledList.remove(self, thing) class HandledProjectedModList(HandledList): def append(self, proj): if proj.isInvalid: # we must include it before we remove it. doing it this way ensures - # rows and relationships in databse are removed as well + # rows and relationships in database are removed as well HandledList.append(self, proj) self.remove(proj) diff --git a/eos/saveddata/booster.py b/eos/saveddata/booster.py index d2125404f..b9b9a5d95 100644 --- a/eos/saveddata/booster.py +++ b/eos/saveddata/booster.py @@ -20,39 +20,48 @@ from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut from eos.effectHandlerHelpers import HandledItem from sqlalchemy.orm import reconstructor, validates +import eos.db class Booster(HandledItem, ItemAttrShortcut): def __init__(self, item): - self.__slot = self.__calculateSlot(item) - self.itemID = item.ID self.__item = item + self.__invalid = False + self.itemID = item.ID if item is not None else None self.active = True self.build() @reconstructor def init(self): + """Initialize a booster from the database and validate""" self.__item = None + self.__invalid = False + + if self.itemID: + # if item does not exist, set invalid + item = eos.db.getItem(self.itemID) + if item is None: + self.__invalid = True + self.__item = item + + self.build() def build(self): - self.__itemModifiedAttributes = ModifiedAttributeDict() - self.__itemModifiedAttributes.original = self.__item.attributes - self.__sideEffects = [] - for effect in self.item.effects.itervalues(): - if effect.isType("boosterSideEffect"): - s = SideEffect(self) - s.effect = effect - s.active = effect.ID in self.__activeSideEffectIDs - self.__sideEffects.append(s) + if self.__item and self.__item.group.name != "Booster": + self.__invalid = True - def __fetchItemInfo(self): - import eos.db - item = eos.db.getItem(self.itemID) - if item: - self.__item = item + self.__itemModifiedAttributes = ModifiedAttributeDict() + self.__sideEffects = [] + + if self.__item: + self.__itemModifiedAttributes.original = self.__item.attributes self.__slot = self.__calculateSlot(self.__item) - self.build() - else: - raise ValueError("Invalid item as Booster:", self.itemID) + + for effect in self.__item.effects.itervalues(): + if effect.isType("boosterSideEffect"): + s = SideEffect(self) + s.effect = effect + s.active = effect.ID in self.__activeSideEffectIDs + self.__sideEffects.append(s) def iterSideEffects(self): return self.__sideEffects.__iter__() @@ -66,23 +75,18 @@ class Booster(HandledItem, ItemAttrShortcut): @property def itemModifiedAttributes(self): - if self.__item is None: - self.__fetchItemInfo() - return self.__itemModifiedAttributes @property - def slot(self): - if self.__item is None: - self.__fetchItemInfo() + def isInvalid(self): + return self.__invalid + @property + def slot(self): return self.__slot @property def item(self): - if self.__item is None: - self.__fetchItemInfo() - return self.__item def __calculateSlot(self, item): diff --git a/eos/saveddata/implant.py b/eos/saveddata/implant.py index 036eabbc3..ed7002f31 100644 --- a/eos/saveddata/implant.py +++ b/eos/saveddata/implant.py @@ -20,46 +20,54 @@ from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut from eos.effectHandlerHelpers import HandledItem from sqlalchemy.orm import validates, reconstructor +import eos.db class Implant(HandledItem, ItemAttrShortcut): def __init__(self, item): - self.__slot = self.__calculateSlot(item) self.__item = item - self.itemID = item.ID + self.__invalid = False + self.itemID = item.ID if item is not None else None self.active = True - self.__itemModifiedAttributes = ModifiedAttributeDict() - self.__itemModifiedAttributes.original = self.item.attributes + self.build() @reconstructor def init(self): self.__item = None + self.__invalid = False + + if self.itemID: + # if item does not exist, set invalid + item = eos.db.getItem(self.itemID) + if item is None: + self.__invalid = True + self.__item = item + + self.build() + + def build(self): + if self.__item and self.__item.category.name != "Implant": + self.__invalid = True - def __fetchItemInfo(self): - import eos.db - self.__item = eos.db.getItem(self.itemID) self.__itemModifiedAttributes = ModifiedAttributeDict() - self.__itemModifiedAttributes.original = self.__item.attributes - self.__slot = self.__calculateSlot(self.__item) + + if self.__item: + self.__itemModifiedAttributes.original = self.__item.attributes + self.__slot = self.__calculateSlot(self.__item) @property def itemModifiedAttributes(self): - if self.__item is None: - self.__fetchItemInfo() - return self.__itemModifiedAttributes @property - def slot(self): - if self.__item is None: - self.__fetchItemInfo() + def isInvalid(self): + return self.__invalid + @property + def slot(self): return self.__slot @property def item(self): - if self.__item is None: - self.__fetchItemInfo() - return self.__item def __calculateSlot(self, item): diff --git a/service/fit.py b/service/fit.py index 5360a376a..f6c1f362d 100644 --- a/service/fit.py +++ b/service/fit.py @@ -274,7 +274,6 @@ class Fit(object): except ValueError: return False - fit.implants.freeSlot(implant) fit.implants.append(implant) self.recalc(fit) return True @@ -300,7 +299,6 @@ class Fit(object): except ValueError: return False - fit.boosters.freeSlot(booster) fit.boosters.append(booster) self.recalc(fit) return True From e1ce67256951c52202baf07cd9f3bfe6fcd8e3e0 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Thu, 2 Jul 2015 11:22:26 -0400 Subject: [PATCH 10/20] Move flag_modified to HandledList.remove() so that it takes care of all our use cases. Give fits an itemID like everything else so that projected fits can be removed correctly by this logic. No reason for them to be special snowflakes. --- eos/db/saveddata/fit.py | 2 ++ eos/effectHandlerHelpers.py | 12 +++++------- eos/saveddata/cargo.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/eos/db/saveddata/fit.py b/eos/db/saveddata/fit.py index 0677728d8..0c5edaee9 100644 --- a/eos/db/saveddata/fit.py +++ b/eos/db/saveddata/fit.py @@ -53,6 +53,8 @@ mapper(Fit, fits_table, "_Fit__projectedModules" : relation(Module, collection_class = HandledProjectedModList, cascade='all, delete, delete-orphan', single_parent=True, primaryjoin = and_(modules_table.c.fitID == fits_table.c.ID, modules_table.c.projected == True)), "owner" : relation(User, backref = "fits"), + "itemID" : fits_table.c.shipID, + "shipID" : fits_table.c.shipID, "_Fit__boosters" : relation(Booster, collection_class = HandledImplantBoosterList, cascade='all, delete, delete-orphan', single_parent=True), "_Fit__drones" : relation(Drone, collection_class = HandledDroneCargoList, cascade='all, delete, delete-orphan', single_parent=True, primaryjoin = and_(drones_table.c.fitID == fits_table.c.ID, drones_table.c.projected == False)), diff --git a/eos/effectHandlerHelpers.py b/eos/effectHandlerHelpers.py index 1311dcf6a..a417fa041 100644 --- a/eos/effectHandlerHelpers.py +++ b/eos/effectHandlerHelpers.py @@ -102,6 +102,11 @@ class HandledList(list): except AttributeError: pass + def remove(self, thing): + # We must flag it as modified, otherwise it not be removed from the database + flag_modified(thing, "itemID") + list.remove(self, thing) + class HandledModuleList(HandledList): def append(self, mod): emptyPosition = float("Inf") @@ -168,8 +173,6 @@ class HandledDroneCargoList(HandledList): HandledList.append(self, thing) if thing.isInvalid: - # we must flag it as modified, otherwise it will not be removed from the database - flag_modified(thing, "itemID") self.remove(thing) class HandledImplantBoosterList(HandledList): @@ -185,11 +188,6 @@ class HandledImplantBoosterList(HandledList): HandledList.append(self, thing) - def remove(self, thing): - # We must flag it as modified, otherwise it not be removed from the database - flag_modified(thing, "itemID") - HandledList.remove(self, thing) - class HandledProjectedModList(HandledList): def append(self, proj): if proj.isInvalid: diff --git a/eos/saveddata/cargo.py b/eos/saveddata/cargo.py index fa68de1a0..7f7542a0b 100644 --- a/eos/saveddata/cargo.py +++ b/eos/saveddata/cargo.py @@ -23,7 +23,7 @@ from sqlalchemy.orm import validates, reconstructor import eos.db -class Cargo(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): +class Cargo(HandledItem, ItemAttrShortcut): def __init__(self, item): """Initialize cargo from the program""" From ca08f8d8da9f2121b5e80b14299ba4a11ba8f707 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Thu, 2 Jul 2015 14:14:16 -0400 Subject: [PATCH 11/20] Handle fits with invalid ships by removing and deleting them when loaded. --- eos/db/saveddata/queries.py | 28 ++++- eos/saveddata/fit.py | 49 +++++--- eos/saveddata/mode.py | 22 +--- eos/saveddata/ship.py | 30 +---- eos/slotFill.py | 223 ------------------------------------ service/fit.py | 4 +- 6 files changed, 71 insertions(+), 285 deletions(-) delete mode 100644 eos/slotFill.py diff --git a/eos/db/saveddata/queries.py b/eos/db/saveddata/queries.py index be7c078fc..fe4f0310c 100644 --- a/eos/db/saveddata/queries.py +++ b/eos/db/saveddata/queries.py @@ -185,6 +185,12 @@ def getFit(lookfor, eager=None): fit = saveddata_session.query(Fit).options(*eager).filter(Fit.ID == fitID).first() else: raise TypeError("Need integer as argument") + + if fit.isInvalid: + with sd_lock: + removeInvalid([fit]) + return None + return fit @cachedQuery(Fleet, 1, "fleetID") @@ -244,9 +250,10 @@ def getFitsWithShip(shipID, ownerID=None, where=None, eager=None): filter = processWhere(filter, where) eager = processEager(eager) with sd_lock: - fits = saveddata_session.query(Fit).options(*eager).filter(filter).all() + fits = removeInvalid(saveddata_session.query(Fit).options(*eager).filter(filter).all()) else: raise TypeError("ShipID must be integer") + return fits def getBoosterFits(ownerID=None, where=None, eager=None): @@ -264,7 +271,8 @@ def getBoosterFits(ownerID=None, where=None, eager=None): filter = processWhere(filter, where) eager = processEager(eager) with sd_lock: - fits = saveddata_session.query(Fit).options(*eager).filter(filter).all() + fits = removeInvalid(saveddata_session.query(Fit).options(*eager).filter(filter).all()) + return fits def countAllFits(): @@ -295,7 +303,8 @@ def countFitsWithShip(shipID, ownerID=None, where=None, eager=None): def getFitList(eager=None): eager = processEager(eager) with sd_lock: - fits = saveddata_session.query(Fit).options(*eager).all() + fits = removeInvalid(saveddata_session.query(Fit).options(*eager).all()) + return fits def getFleetList(eager=None): @@ -385,7 +394,8 @@ def searchFits(nameLike, where=None, eager=None): filter = processWhere(Fit.name.like(nameLike, escape="\\"), where) eager = processEager(eager) with sd_lock: - fits = saveddata_session.query(Fit).options(*eager).filter(filter).all() + fits = removeInvalid(saveddata_session.query(Fit).options(*eager).filter(filter).all()) + return fits def getSquadsIDsWithFitID(fitID): @@ -406,6 +416,16 @@ def getProjectedFits(fitID): else: raise TypeError("Need integer as argument") +def removeInvalid(fits): + invalids = [f for f in fits if f.isInvalid] + + if invalids: + map(fits.remove, invalids) + map(saveddata_session.delete, invalids) + saveddata_session.commit() + + return fits + def add(stuff): with sd_lock: saveddata_session.add(stuff) diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py index 12cf1a7bd..ceb73b258 100644 --- a/eos/saveddata/fit.py +++ b/eos/saveddata/fit.py @@ -27,6 +27,7 @@ from math import sqrt, log, asinh from eos.types import Drone, Cargo, Ship, Character, State, Slot, Module, Implant, Booster, Skill from eos.saveddata.module import State from eos.saveddata.mode import Mode +import eos.db import time try: @@ -47,7 +48,12 @@ class Fit(object): PEAK_RECHARGE = 0.25 - def __init__(self): + def __init__(self, ship=None, name=""): + """Initialize a fit from the program""" + # use @mode.setter's to set __attr and IDs. This will set mode as well + self.ship = ship + + self.__invalid = False self.__modules = HandledModuleList() self.__drones = HandledDroneCargoList() self.__cargo = HandledDroneCargoList() @@ -58,23 +64,37 @@ class Fit(object): self.__projectedDrones = HandledProjectedDroneList() self.__character = None self.__owner = None - self.shipID = None + self.projected = False - self.name = "" - self.fleet = None - self.boostsFits = set() - self.gangBoosts = None + self.name = name self.timestamp = time.time() - self.ecmProjectedStr = 1 self.modeID = None + self.build() @reconstructor def init(self): + """Initialize a drone from the database and validate""" + self.__ship = None + self.__mode = None + self.__invalid = False + + if self.shipID: + # if item does not exist, set invalid + item = eos.db.getItem(self.shipID) + if item is None or item.category.name != "Ship": + self.__invalid = True + else: + self.__ship = Ship(item) + + if self.modeID and self.__ship: + item = eos.db.getItem(self.modeID) + # Don't need to verify if it's a proper item, as checkModeItem assures this + self.__mode = self.ship.checkModeItem(item) + self.build() def build(self): - from eos import db self.__extraDrains = [] self.__ehp = None self.__weaponDPS = None @@ -99,11 +119,6 @@ class Fit(object): self.ecmProjectedStr = 1 self.extraAttributes = ModifiedAttributeDict(self) self.extraAttributes.original = self.EXTRA_ATTRIBUTES - self.ship = Ship(db.getItem(self.shipID)) if self.shipID is not None else None - if self.ship is not None: - self.mode = self.ship.checkModeItem(db.getItem(self.modeID) if self.modeID else None) - else: - self.mode = None @property def targetResists(self): @@ -127,13 +142,17 @@ class Fit(object): self.__ehp = None self.__effectiveTank = None + @property + def isInvalid(self): + return self.__invalid + @property def mode(self): - return self._mode + return self.__mode @mode.setter def mode(self, mode): - self._mode = mode + self.__mode = mode self.modeID = mode.item.ID if mode is not None else None @property diff --git a/eos/saveddata/mode.py b/eos/saveddata/mode.py index 83d0c0bc2..3a344d74d 100644 --- a/eos/saveddata/mode.py +++ b/eos/saveddata/mode.py @@ -19,36 +19,24 @@ from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut from eos.effectHandlerHelpers import HandledItem +import eos.db class Mode(ItemAttrShortcut, HandledItem): - def __init__(self, item): + + if item.group.name != "Ship Modifiers": + raise ValueError('Passed item "%s" (category: (%s)) is not a Ship Modifier'%(item.name, item.category.name)) + self.__item = item self.__itemModifiedAttributes = ModifiedAttributeDict() - - if not isinstance(item, int): - self.__buildOriginal() - - def __fetchItemInfo(self): - import eos.db - self.__item = eos.db.getItem(self.__item) - self.__buildOriginal() - - def __buildOriginal(self): self.__itemModifiedAttributes.original = self.item.attributes @property def item(self): - if isinstance(self.__item, int): - self.__fetchItemInfo() - return self.__item @property def itemModifiedAttributes(self): - if isinstance(self.__item, int): - self.__fetchItemInfo() - return self.__itemModifiedAttributes # @todo: rework to fit only on t3 dessy diff --git a/eos/saveddata/ship.py b/eos/saveddata/ship.py index 03eeef3d4..123b56b73 100644 --- a/eos/saveddata/ship.py +++ b/eos/saveddata/ship.py @@ -20,6 +20,7 @@ from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut from eos.effectHandlerHelpers import HandledItem from eos.saveddata.mode import Mode +import eos.db class Ship(ItemAttrShortcut, HandledItem): def __init__(self, item): @@ -28,33 +29,18 @@ class Ship(ItemAttrShortcut, HandledItem): raise ValueError('Passed item "%s" (category: (%s)) is not under Ship category'%(item.name, item.category.name)) self.__item = item + self.__modeItems = self.__getModeItems() self.__itemModifiedAttributes = ModifiedAttributeDict() - self.__modeItems = self._getModeItems() - if not isinstance(item, int): - self.__buildOriginal() + self.__itemModifiedAttributes.original = self.item.attributes self.commandBonus = 0 - def __fetchItemInfo(self): - import eos.db - self.__item = eos.db.getItem(self.__item) - self.__buildOriginal() - - def __buildOriginal(self): - self.__itemModifiedAttributes.original = self.item.attributes - @property def item(self): - if isinstance(self.__item, int): - self.__fetchItemInfo() - return self.__item @property def itemModifiedAttributes(self): - if isinstance(self.__item, int): - self.__fetchItemInfo() - return self.__itemModifiedAttributes def clear(self): @@ -96,21 +82,17 @@ class Ship(ItemAttrShortcut, HandledItem): def modes(self): return [Mode(item) for item in self.__modeItems] if self.__modeItems else None - def _getModeItems(self): + def __getModeItems(self): """ Returns a list of valid mode items for ship. Note that this returns the valid Item objects, not the Mode objects. Returns None if not a t3 dessy """ - # @todo: is there a better way to determine this that isn't hardcoded groupIDs? - if self.item.groupID != 1305: + if self.item.group.name != "Tactical Destroyer": return None - modeGroupID = 1306 - import eos.db - items = [] - g = eos.db.getGroup(modeGroupID, eager=("items.icon", "items.attributes")) + g = eos.db.getGroup("Ship Modifiers", eager=("items.icon", "items.attributes")) for item in g.items: # Rely on name detection because race is not reliable if item.name.lower().startswith(self.item.name.lower()): diff --git a/eos/slotFill.py b/eos/slotFill.py deleted file mode 100644 index ce00a07ae..000000000 --- a/eos/slotFill.py +++ /dev/null @@ -1,223 +0,0 @@ -#=============================================================================== -# 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 . -#=============================================================================== - -from eos.types import Slot, Fit, Module, State -import random -import copy -import math -import bisect -import itertools -import time - -class SlotFill(object): - def __init__(self, original, modules, attributeWeights=None, propertyWeights=None, specificWeights=None, defaultState = State.ACTIVE): - self.original = original - self.attributeWeights = attributeWeights or {} - self.propertyWeights = propertyWeights or {} - self.specificWeights = specificWeights or [] - self.state = State.ACTIVE - self.modules = map(self.__newModule, modules) - - def __newModule(self, item): - m = Module(item) - m.state = self.state - return m - - def __getMetaParent(self, item): - metaGroup = item.metaGroup - return item if metaGroup is None else metaGroup.parent - - def fitness(self, fit, chromosome): - modList = fit.modules - modAttr = fit.ship.getModifiedItemAttr - - modList.extend(chromosome) - fit.clear() - fit.calculateModifiedAttributes() - - if not fit.fits: - del modList[-len(chromosome):] - return 0 - - weight = 0 - for attr, value in self.attributeWeights.iteritems(): - weight += modAttr(attr) * (value if value >= 0 else 1.0 / -value) - - for prop, value in self.propertyWeights.iteritems(): - weight += getattr(fit, prop) * (value if value >= 0 else 1.0 / -value) - - for specific in self.specificWeights: - weight += specific(fit) - - totalVars = (fit.ship.getModifiedItemAttr("powerOutput"), - fit.ship.getModifiedItemAttr("cpuOutput"), - fit.ship.getModifiedItemAttr('upgradeCapacity')) - - usedVars = (fit.pgUsed, fit.cpuUsed, fit.calibrationUsed) - - total = 0 - used = 0 - for tv, uv in zip(totalVars, usedVars): - if uv > tv: - del modList[-len(chromosome):] - return 0 - - del modList[-len(chromosome):] - - - return weight - - - def run(self, elite = 0.05, crossoverChance = 0.8, slotMutationChance = 0.5, typeMutationChance = 0.5): - #Use a copy of the original for all our calcs. We don't want to damage it - fit = copy.deepcopy(self.original) - fit.unfill() - - #First of all, lets check the number of slots we got to play with - chromLength = -1 - slotAmounts = {} - for type in Slot.getTypes(): - slot = Slot.getValue(type) - amount = fit.getSlotsFree(slot) - if amount > 0: - slotAmounts[slot] = amount - - chromLength += amount - - if not slotAmounts: - #Nothing to do, joy - return - - slotModules = {} - metaModules = {} - - for slotType in slotAmounts: - slotModules[slotType] = modules = [] - - for module in self.modules: - #Store the variations of each base for ease and speed - metaParent = self.__getMetaParent(module.item) - metaList = metaModules.get(metaParent) - if metaList is None: - metaList = metaModules[metaParent] = [] - metaList.append(module) - - #Sort stuff by slotType for ease and speed - slot = module.slot - if slot in slotModules: - slotModules[slot].append(module) - - for slotType, modules in slotModules.iteritems(): - if len(modules) == 0: - chromLength -= slotAmounts[slotType] - del slotAmounts[slotType] - - #Now, we need an initial set, first thing to do is decide how big that set will be - setSize = 10 - - #Grab some variables locally for performance improvements - rchoice = random.choice - rrandom = random.random - rrandint = random.randint - bbisect = bisect.bisect - ccopy = copy.copy - - #Get our list for storage of our chromosomes - chromosomes = [] - - # Helpers - weigher = lambda chromosome: (self.fitness(fit, chromosome), chromosome) - keyer = lambda info: info[0] - - eliteCutout = int(math.floor(setSize * (1 - elite))) - lastEl = setSize - 1 - - #Generate our initial set entirely randomly - #Subtelies to take in mind: - # * modules of the same slotType are kept together for easy cross-overing - state = self.state - for _ in xrange(setSize): - chrom = [] - for type, amount in slotAmounts.iteritems(): - for _ in xrange(amount): - chrom.append(rchoice(slotModules[type])) - - chromosomes.append(weigher(chrom)) - - #Sort our initial set - chromosomes.sort(key=keyer) - currentGeneration = chromosomes - - #Yield the best result from our initial set, this is gonna be pretty bad - yield currentGeneration[lastEl] - - #Setup's done, now we can actualy apply our genetic algorithm to optimize all this - while True: - moo = time.time() - #First thing we do, we're gonna be elitair - #Grab the top x%, we'll put em in the next generation - nextGeneration = [] - for i in xrange(lastEl, eliteCutout - 1, -1): - nextGeneration.append(currentGeneration[i]) - - #Figure out our ratios to do our roulette wheel - fitnessList = map(keyer, currentGeneration) - totalFitness = float(sum(fitnessList)) - - curr = 0 - ratios = [] - for fitness in fitnessList: - curr += fitness - ratios.append(curr / (totalFitness or 1)) - - t = 0 - #Do our pairing - for _ in xrange(0, eliteCutout): - # Crossover chance - mother = currentGeneration[bbisect(ratios, rrandom())][1] - father = currentGeneration[bbisect(ratios, rrandom())][1] - if rrandom() <= crossoverChance: - crosspoint = rrandint(0, chromLength) - luke = mother[:crosspoint] + father[crosspoint:] - else: - luke = father - - #Chance for slot mutation - if rrandom() <= slotMutationChance: - target = rrandint(0, chromLength) - mod = luke[target] - luke[target] = rchoice(slotModules[mod.slot]) - - if rrandom() <= typeMutationChance: - #Mutation of an item to another one of the same type - target = rrandint(0, chromLength) - mod = luke[target] - vars = metaModules[self.__getMetaParent(mod.item)] - luke[target] = rchoice(vars) - - tt = time.time() - nextGeneration.append(weigher(luke)) - t += time.time() - tt - - print "time spent weighing: ", t - - nextGeneration.sort(key=keyer) - currentGeneration = nextGeneration - print "total time spent this iteration:", time.time() - moo - yield currentGeneration[lastEl] diff --git a/service/fit.py b/service/fit.py index f6c1f362d..f74d40590 100644 --- a/service/fit.py +++ b/service/fit.py @@ -149,8 +149,8 @@ class Fit(object): return fit.modules[pos] def newFit(self, shipID, name=None): - fit = eos.types.Fit() - fit.ship = eos.types.Ship(eos.db.getItem(shipID)) + ship = eos.types.Ship(eos.db.getItem(shipID)) + fit = eos.types.Fit(ship) fit.name = name if name is not None else "New %s" % fit.ship.item.name fit.damagePattern = self.pattern fit.targetResists = self.targetResists From 539360d5f6c1f093ef431662d5699517f556ebfa Mon Sep 17 00:00:00 2001 From: blitzmann Date: Thu, 2 Jul 2015 15:04:07 -0400 Subject: [PATCH 12/20] Remove old debug print --- gui/resistsEditor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gui/resistsEditor.py b/gui/resistsEditor.py index 0d26be5da..8dcfe4b0f 100644 --- a/gui/resistsEditor.py +++ b/gui/resistsEditor.py @@ -342,7 +342,6 @@ class ResistsEditorDlg(wx.Dialog): self.patternChanged() def showInput(self, bool): - print self.namePicker.IsShown(), bool if bool and not self.namePicker.IsShown(): self.ccResists.Hide() self.namePicker.Show() From 84b1e0ac41d7bf5a80e2b0b6b39e974910fe0700 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Thu, 2 Jul 2015 19:34:02 -0400 Subject: [PATCH 13/20] Migrate boosters table to new schema that drops the UNIQUE constraint (causes issues and is unneeded) --- eos/db/migrations/upgrade9.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 eos/db/migrations/upgrade9.py diff --git a/eos/db/migrations/upgrade9.py b/eos/db/migrations/upgrade9.py new file mode 100644 index 000000000..ae7b66ad5 --- /dev/null +++ b/eos/db/migrations/upgrade9.py @@ -0,0 +1,23 @@ +""" +Migration 9 + +Effectively drops UNIQUE constraint from boosters table. SQLite does not support +this, so we have to copy the table to the updated schema and then rename it +""" + +tmpTable = """ +CREATE TABLE boostersTemp ( + 'ID' INTEGER NOT NULL, + 'itemID' INTEGER, + 'fitID' INTEGER NOT NULL, + 'active' BOOLEAN, + PRIMARY KEY(ID), + FOREIGN KEY('fitID') REFERENCES fits ('ID') +) +""" + +def upgrade(saveddata_engine): + saveddata_engine.execute(tmpTable) + saveddata_engine.execute("INSERT INTO boostersTemp (ID, itemID, fitID, active) SELECT ID, itemID, fitID, active FROM boosters") + saveddata_engine.execute("DROP TABLE boosters") + saveddata_engine.execute("ALTER TABLE boostersTemp RENAME TO boosters") From 5991d19b3e96f5bd2ad4afb33e6aa5ceef540ea8 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sat, 4 Jul 2015 00:32:28 -0400 Subject: [PATCH 14/20] Allow Subsystems as modules. --- eos/saveddata/module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eos/saveddata/module.py b/eos/saveddata/module.py index c472c0927..135987275 100644 --- a/eos/saveddata/module.py +++ b/eos/saveddata/module.py @@ -84,7 +84,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): def build(self): """Builds internal module variables from both init's""" - if self.__item and self.__item.category.name != "Module" and self.__item.group.name != "Effect Beacon": + if self.__item and self.__item.category.name not in ("Module", "Subsystem") and self.__item.group.name != "Effect Beacon": self.__invalid = True if self.__charge and self.__charge.category.name != "Charge": self.__charge = None From aaa60cbc143eb5f752b348754c052e16b41df23b Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sat, 4 Jul 2015 23:16:20 -0400 Subject: [PATCH 15/20] Fix instance where some items were being re-added due to lack of return. Also, implement some basic logging. --- config.py | 11 ++++++++++- eos/effectHandlerHelpers.py | 7 +++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/config.py b/config.py index 32c24962f..b7b7c3397 100644 --- a/config.py +++ b/config.py @@ -32,7 +32,16 @@ gameDB = None # TODO: move back to pyfa.py main loop # We moved it here just to avoid rebuilding windows skeleton for now (any change to pyfa.py needs it) import logging -logging.basicConfig() +import logging.handlers + +format = '%(asctime)s %(name)-24s %(levelname)-8s %(message)s' +logging.basicConfig(format=format, level=logging.DEBUG) +handler = logging.handlers.RotatingFileHandler("log.txt", maxBytes=10000, backupCount=3) +formatter = logging.Formatter(format) +handler.setFormatter(formatter) +logging.getLogger('').addHandler(handler) + +logging.info("Starting pyfa") def defPaths(): global pyfaPath diff --git a/eos/effectHandlerHelpers.py b/eos/effectHandlerHelpers.py index a417fa041..c658cf0a9 100644 --- a/eos/effectHandlerHelpers.py +++ b/eos/effectHandlerHelpers.py @@ -20,6 +20,9 @@ from sqlalchemy.orm.attributes import flag_modified import eos.db import eos.types +import logging + +logger = logging.getLogger(__name__) class HandledList(list): def filteredItemPreAssign(self, filter, *args, **kwargs): @@ -180,10 +183,12 @@ class HandledImplantBoosterList(HandledList): if thing.isInvalid: HandledList.append(self, thing) self.remove(thing) + return # if needed, remove booster that was occupying slot oldObj = next((m for m in self if m.slot == thing.slot), None) if oldObj: + logging.info("Slot %d occupied with %s, replacing with %s", thing.slot, oldObj.item.name, thing.item.name) self.remove(oldObj) HandledList.append(self, thing) @@ -195,6 +200,7 @@ class HandledProjectedModList(HandledList): # rows and relationships in database are removed as well HandledList.append(self, proj) self.remove(proj) + return proj.projected = True isSystemEffect = proj.item.group.name == "Effect Beacon" @@ -204,6 +210,7 @@ class HandledProjectedModList(HandledList): oldEffect = next((m for m in self if m.item.group.name == "Effect Beacon"), None) if oldEffect: + logging.info("System effect occupied with %s, replacing with %s", oldEffect.item.name, proj.item.name) self.remove(oldEffect) HandledList.append(self, proj) From 7959593c6c2ccb7c8ff4bdb6ea66ae6819670cbd Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sun, 5 Jul 2015 00:31:52 -0400 Subject: [PATCH 16/20] Improve object initialization and add support for logging the errors. --- eos/saveddata/booster.py | 48 ++++++++++++++++++++++------------------ eos/saveddata/cargo.py | 20 ++++++++--------- eos/saveddata/drone.py | 44 +++++++++++++++++++----------------- eos/saveddata/fit.py | 2 +- eos/saveddata/implant.py | 34 +++++++++++++++------------- eos/saveddata/module.py | 42 +++++++++++++++++++++-------------- 6 files changed, 104 insertions(+), 86 deletions(-) diff --git a/eos/saveddata/booster.py b/eos/saveddata/booster.py index b9b9a5d95..c0874fe94 100644 --- a/eos/saveddata/booster.py +++ b/eos/saveddata/booster.py @@ -21,11 +21,17 @@ from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut from eos.effectHandlerHelpers import HandledItem from sqlalchemy.orm import reconstructor, validates import eos.db +import logging + +logger = logging.getLogger(__name__) class Booster(HandledItem, ItemAttrShortcut): def __init__(self, item): self.__item = item - self.__invalid = False + + if self.isInvalid: + raise ValueError("Passed item is not a Booster") + self.itemID = item.ID if item is not None else None self.active = True self.build() @@ -34,34 +40,32 @@ class Booster(HandledItem, ItemAttrShortcut): def init(self): """Initialize a booster from the database and validate""" self.__item = None - self.__invalid = False if self.itemID: - # if item does not exist, set invalid - item = eos.db.getItem(self.itemID) - if item is None: - self.__invalid = True - self.__item = item + self.__item = eos.db.getItem(self.itemID) + if self.__item is None: + logger.error("Item (id: %d) does not exist", self.itemID) + return + + if self.isInvalid: + logger.error("Item (id: %d) is not a Booser", self.itemID) + return self.build() def build(self): - if self.__item and self.__item.group.name != "Booster": - self.__invalid = True - - self.__itemModifiedAttributes = ModifiedAttributeDict() + """ Build object. Assumes proper and valid item already set """ self.__sideEffects = [] + self.__itemModifiedAttributes = ModifiedAttributeDict() + self.__itemModifiedAttributes.original = self.__item.attributes + self.__slot = self.__calculateSlot(self.__item) - if self.__item: - self.__itemModifiedAttributes.original = self.__item.attributes - self.__slot = self.__calculateSlot(self.__item) - - for effect in self.__item.effects.itervalues(): - if effect.isType("boosterSideEffect"): - s = SideEffect(self) - s.effect = effect - s.active = effect.ID in self.__activeSideEffectIDs - self.__sideEffects.append(s) + for effect in self.__item.effects.itervalues(): + if effect.isType("boosterSideEffect"): + s = SideEffect(self) + s.effect = effect + s.active = effect.ID in self.__activeSideEffectIDs + self.__sideEffects.append(s) def iterSideEffects(self): return self.__sideEffects.__iter__() @@ -79,7 +83,7 @@ class Booster(HandledItem, ItemAttrShortcut): @property def isInvalid(self): - return self.__invalid + return self.__item is None or self.__item.group.name != "Booster" @property def slot(self): diff --git a/eos/saveddata/cargo.py b/eos/saveddata/cargo.py index 7f7542a0b..26509d525 100644 --- a/eos/saveddata/cargo.py +++ b/eos/saveddata/cargo.py @@ -21,14 +21,15 @@ from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, C from eos.effectHandlerHelpers import HandledItem, HandledCharge from sqlalchemy.orm import validates, reconstructor import eos.db +import logging +logger = logging.getLogger(__name__) class Cargo(HandledItem, ItemAttrShortcut): def __init__(self, item): """Initialize cargo from the program""" self.__item = item - self.__invalid = False self.itemID = item.ID if item is not None else None self.amount = 0 self.__itemModifiedAttributes = ModifiedAttributeDict() @@ -38,18 +39,15 @@ class Cargo(HandledItem, ItemAttrShortcut): def init(self): """Initialize cargo from the database and validate""" self.__item = None - self.__invalid = False - self.__itemModifiedAttributes = ModifiedAttributeDict() if self.itemID: - # if item does not exist, set invalid - item = eos.db.getItem(self.itemID) - if item is None: - self.__invalid = True - self.__item = item + self.__item = eos.db.getItem(self.itemID) + if self.__item is None: + logger.error("Item (id: %d) does not exist", self.itemID) + return - if self.__item: - self.__itemModifiedAttributes.original = self.__item.attributes + self.__itemModifiedAttributes = ModifiedAttributeDict() + self.__itemModifiedAttributes.original = self.__item.attributes @property def itemModifiedAttributes(self): @@ -57,7 +55,7 @@ class Cargo(HandledItem, ItemAttrShortcut): @property def isInvalid(self): - return self.__invalid + return self.__item is None @property def item(self): diff --git a/eos/saveddata/drone.py b/eos/saveddata/drone.py index 0c7f9a541..1a63d57a3 100644 --- a/eos/saveddata/drone.py +++ b/eos/saveddata/drone.py @@ -21,6 +21,9 @@ from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, C from eos.effectHandlerHelpers import HandledItem, HandledCharge from sqlalchemy.orm import validates, reconstructor import eos.db +import logging + +logger = logging.getLogger(__name__) class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): DAMAGE_TYPES = ("em", "kinetic", "explosive", "thermal") @@ -29,47 +32,48 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): def __init__(self, item): """Initialize a drone from the program""" self.__item = item - self.__invalid = False + + if self.isInvalid: + raise ValueError("Passed item is not a Drone") + self.itemID = item.ID if item is not None else None self.amount = 0 self.amountActive = 0 self.projected = False self.build() - @reconstructor def init(self): """Initialize a drone from the database and validate""" self.__item = None - self.__invalid = False if self.itemID: - # if item does not exist, set invalid - item = eos.db.getItem(self.itemID) - if item is None: - self.__invalid = True - self.__item = item + self.__item = eos.db.getItem(self.itemID) + if self.__item is None: + logger.error("Item (id: %d) does not exist", self.itemID) + return + + if self.isInvalid: + logger.error("Item (id: %d) is not a Drone", self.itemID) + return self.build() def build(self): - if self.__item and self.__item.category.name != "Drone": - self.__invalid = True - + """ Build object. Assumes proper and valid item already set """ self.__charge = None self.__dps = None self.__volley = None self.__miningyield = None self.__itemModifiedAttributes = ModifiedAttributeDict() - self.__chargeModifiedAttributes = ModifiedAttributeDict() + self.__itemModifiedAttributes.original = self.__item.attributes - if self.__item: - self.__itemModifiedAttributes.original = self.__item.attributes - chargeID = self.getModifiedItemAttr("entityMissileTypeID") - if chargeID is not None: - charge = eos.db.getItem(int(chargeID)) - self.__charge = charge - self.__chargeModifiedAttributes.original = charge.attributes + self.__chargeModifiedAttributes = ModifiedAttributeDict() + chargeID = self.getModifiedItemAttr("entityMissileTypeID") + if chargeID is not None: + charge = eos.db.getItem(int(chargeID)) + self.__charge = charge + self.__chargeModifiedAttributes.original = charge.attributes @property def itemModifiedAttributes(self): @@ -81,7 +85,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): @property def isInvalid(self): - return self.__invalid + return self.__item is None or self.__item.category.name != "Drone" @property def item(self): diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py index ceb73b258..c7ba96c3f 100644 --- a/eos/saveddata/fit.py +++ b/eos/saveddata/fit.py @@ -74,7 +74,7 @@ class Fit(object): @reconstructor def init(self): - """Initialize a drone from the database and validate""" + """Initialize a fit from the database and validate""" self.__ship = None self.__mode = None self.__invalid = False diff --git a/eos/saveddata/implant.py b/eos/saveddata/implant.py index ed7002f31..64670d769 100644 --- a/eos/saveddata/implant.py +++ b/eos/saveddata/implant.py @@ -21,11 +21,17 @@ from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut from eos.effectHandlerHelpers import HandledItem from sqlalchemy.orm import validates, reconstructor import eos.db +import logging + +logger = logging.getLogger(__name__) class Implant(HandledItem, ItemAttrShortcut): def __init__(self, item): self.__item = item - self.__invalid = False + + if self.isInvalid: + raise ValueError("Passed item is not an Implant") + self.itemID = item.ID if item is not None else None self.active = True self.build() @@ -33,26 +39,24 @@ class Implant(HandledItem, ItemAttrShortcut): @reconstructor def init(self): self.__item = None - self.__invalid = False if self.itemID: - # if item does not exist, set invalid - item = eos.db.getItem(self.itemID) - if item is None: - self.__invalid = True - self.__item = item + self.__item = eos.db.getItem(self.itemID) + if self.__item is None: + logger.error("Item (id: %d) does not exist", self.itemID) + return + + if self.isInvalid: + logger.error("Item (id: %d) is not an Implant", self.itemID) + return self.build() def build(self): - if self.__item and self.__item.category.name != "Implant": - self.__invalid = True - + """ Build object. Assumes proper and valid item already set """ self.__itemModifiedAttributes = ModifiedAttributeDict() - - if self.__item: - self.__itemModifiedAttributes.original = self.__item.attributes - self.__slot = self.__calculateSlot(self.__item) + self.__itemModifiedAttributes.original = self.__item.attributes + self.__slot = self.__calculateSlot(self.__item) @property def itemModifiedAttributes(self): @@ -60,7 +64,7 @@ class Implant(HandledItem, ItemAttrShortcut): @property def isInvalid(self): - return self.__invalid + return self.__item is None or self.__item.category.name != "Implant" @property def slot(self): diff --git a/eos/saveddata/module.py b/eos/saveddata/module.py index 135987275..a4fdbec07 100644 --- a/eos/saveddata/module.py +++ b/eos/saveddata/module.py @@ -24,6 +24,9 @@ from eos.effectHandlerHelpers import HandledItem, HandledCharge from eos.enum import Enum from eos.mathUtils import floorFloat import eos.db +import logging + +logger = logging.getLogger(__name__) class State(Enum): OFFLINE = -1 @@ -52,8 +55,11 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): def __init__(self, item): """Initialize a module from the program""" self.__item = item + + if item is not None and self.isInvalid: + raise ValueError("Passed item is not a Module") + self.__charge = None - self.__invalid = False self.itemID = item.ID if item is not None else None self.projected = False self.state = State.ONLINE @@ -64,28 +70,28 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): """Initialize a module from the database and validate""" self.__item = None self.__charge = None - self.__invalid = False + + # we need this early if module is invalid and returns early + self.__slot = self.dummySlot if self.itemID: - # if item does not exist, set invalid - item = eos.db.getItem(self.itemID) - if item is None: - self.__invalid = True - self.__item = item + self.__item = eos.db.getItem(self.itemID) + if self.__item is None: + logger.error("Item (id: %d) does not exist", self.itemID) + return + + if self.isInvalid: + logger.error("Item (id: %d) is not a Module", self.itemID) + return + if self.chargeID: - # if charge does not exist, just ignore it. This doesn't remove it - # from the database, but it will allow the fit to load and the user - # to add another charge - charge = eos.db.getItem(self.chargeID) - if charge: - self.__charge = charge + self.__charge = eos.db.getItem(self.chargeID) self.build() def build(self): - """Builds internal module variables from both init's""" - if self.__item and self.__item.category.name not in ("Module", "Subsystem") and self.__item.group.name != "Effect Beacon": - self.__invalid = True + """ Builds internal module variables from both init's """ + if self.__charge and self.__charge.category.name != "Charge": self.__charge = None @@ -131,7 +137,9 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): @property def isInvalid(self): - return self.__invalid + if self.isEmpty: + return False + return self.__item is None or (self.__item.category.name not in ("Module", "Subsystem") and self.__item.group.name != "Effect Beacon") @property def numCharges(self): From 41b8db346f17c493b50dfe7013796df398af65d9 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sun, 5 Jul 2015 01:16:53 -0400 Subject: [PATCH 17/20] Fix broken drone drag --- gui/droneView.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/gui/droneView.py b/gui/droneView.py index ddf16bf72..f71c7cb25 100644 --- a/gui/droneView.py +++ b/gui/droneView.py @@ -36,7 +36,8 @@ class DroneViewDrop(wx.PyDropTarget): def OnData(self, x, y, t): if self.GetData(): - self.dropFn(x, y, int(self.dropData.GetText())) + data = self.dropData.GetText().split(':') + self.dropFn(x, y, data) return t class DroneView(d.Display): @@ -72,7 +73,7 @@ class DroneView(d.Display): self.Bind(wx.EVT_LIST_BEGIN_DRAG, self.startDrag) - self.SetDropTarget(DroneViewDrop(self.mergeDrones)) + self.SetDropTarget(DroneViewDrop(self.handleDragDrop)) def OnLeaveWindow(self, event): self.SetToolTip(None) @@ -117,17 +118,27 @@ class DroneView(d.Display): row = event.GetIndex() if row != -1: data = wx.PyTextDataObject() - data.SetText(str(self.GetItemData(row))) + data.SetText("drone:"+str(row)) dropSource = wx.DropSource(self) dropSource.SetData(data) res = dropSource.DoDragDrop() - def mergeDrones(self, x, y, itemID): - srcRow = self.FindItemData(-1,itemID) - dstRow, _ = self.HitTest((x, y)) - if srcRow != -1 and dstRow != -1: - self._merge(srcRow, dstRow) + def handleDragDrop(self, x, y, data): + ''' + Handles dragging of items from various pyfa displays which support it + + data is list with two indices: + data[0] is hard-coded str of originating source + data[1] is typeID or index of data we want to manipulate + ''' + if data[0] == "drone": # we want to merge drones + srcRow = int(data[1]) + dstRow, _ = self.HitTest((x, y)) + if srcRow != -1 and dstRow != -1: + self._merge(srcRow, dstRow) + elif data[0] == "market": + wx.PostEvent(self.mainFrame, mb.ItemSelected(itemID=int(data[1]))) def _merge(self, src, dst): sFit = service.Fit.getInstance() From 3773d1c28e6e1608985cfdbd452475b03774fe9b Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sun, 5 Jul 2015 12:57:04 -0400 Subject: [PATCH 18/20] Improvements to fit initializations and logging --- eos/saveddata/fit.py | 25 +++++++++++++++---------- eos/saveddata/ship.py | 26 ++++++++++---------------- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py index c7ba96c3f..5c03d6a2a 100644 --- a/eos/saveddata/fit.py +++ b/eos/saveddata/fit.py @@ -29,6 +29,9 @@ from eos.saveddata.module import State from eos.saveddata.mode import Mode import eos.db import time +import logging + +logger = logging.getLogger(__name__) try: from collections import OrderedDict @@ -53,7 +56,6 @@ class Fit(object): # use @mode.setter's to set __attr and IDs. This will set mode as well self.ship = ship - self.__invalid = False self.__modules = HandledModuleList() self.__drones = HandledDroneCargoList() self.__cargo = HandledDroneCargoList() @@ -77,20 +79,23 @@ class Fit(object): """Initialize a fit from the database and validate""" self.__ship = None self.__mode = None - self.__invalid = False if self.shipID: - # if item does not exist, set invalid item = eos.db.getItem(self.shipID) - if item is None or item.category.name != "Ship": - self.__invalid = True - else: + if item is None: + logger.error("Item (id: %d) does not exist", self.shipID) + return + + try: self.__ship = Ship(item) + except ValueError: + logger.error("Item (id: %d) is not a Ship", self.shipID) + return if self.modeID and self.__ship: item = eos.db.getItem(self.modeID) - # Don't need to verify if it's a proper item, as checkModeItem assures this - self.__mode = self.ship.checkModeItem(item) + # Don't need to verify if it's a proper item, as validateModeItem assures this + self.__mode = self.ship.validateModeItem(item) self.build() @@ -144,7 +149,7 @@ class Fit(object): @property def isInvalid(self): - return self.__invalid + return self.__ship is None @property def mode(self): @@ -172,7 +177,7 @@ class Fit(object): self.__ship = ship self.shipID = ship.item.ID if ship is not None else None # set mode of new ship - self.mode = self.ship.checkModeItem(None) if ship is not None else None + self.mode = self.ship.validateModeItem(None) if ship is not None else None @property def drones(self): diff --git a/eos/saveddata/ship.py b/eos/saveddata/ship.py index 123b56b73..86123bfc1 100644 --- a/eos/saveddata/ship.py +++ b/eos/saveddata/ship.py @@ -21,6 +21,9 @@ from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut from eos.effectHandlerHelpers import HandledItem from eos.saveddata.mode import Mode import eos.db +import logging + +logger = logging.getLogger(__name__) class Ship(ItemAttrShortcut, HandledItem): def __init__(self, item): @@ -53,25 +56,16 @@ class Ship(ItemAttrShortcut, HandledItem): if effect.runTime == runTime and effect.isType("passive"): effect.handler(fit, self, ("ship",)) - def checkModeItem(self, item): - """ - Checks if provided item is a valid mode. - - If ship has modes, and current item is not valid, return forced mode - else if mode is valid, return Mode - else if ship does not have modes, return None - - @todo: rename this - """ + def validateModeItem(self, item): + """ Checks if provided item is a valid mode """ items = self.__modeItems - if items != None: - if item == None or item not in items: - # We have a tact dessy, but mode is None or not valid. Force new mode + if items is not None: + # if we have items, then we are in a tactical destroyer and must have a mode + if item is None or item not in items: + # If provided item is invalid mode, force new one return Mode(items[0]) - elif item in items: - # We have a valid mode - return Mode(item) + return Mode(item) return None @property From 3cc51aaf89713aca6899c3082a88b4e7dc3d52d6 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sun, 5 Jul 2015 13:15:26 -0400 Subject: [PATCH 19/20] Change logging location to ~/.pyfa and set default level to WARN --- config.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/config.py b/config.py index 416984d15..0dd580420 100644 --- a/config.py +++ b/config.py @@ -1,6 +1,11 @@ import os import sys +# TODO: move all logging back to pyfa.py main loop +# We moved it here just to avoid rebuilding windows skeleton for now (any change to pyfa.py needs it) +import logging +import logging.handlers + # Load variable overrides specific to distribution type try: import configforced @@ -12,6 +17,8 @@ debug = False # Defines if our saveddata will be in pyfa root or not saveInRoot = False +logLevel = logging.WARN + # Version data version = "1.12.1" tag = "git" @@ -25,20 +32,6 @@ staticPath = None saveDB = None gameDB = None -# TODO: move back to pyfa.py main loop -# We moved it here just to avoid rebuilding windows skeleton for now (any change to pyfa.py needs it) -import logging -import logging.handlers - -format = '%(asctime)s %(name)-24s %(levelname)-8s %(message)s' -logging.basicConfig(format=format, level=logging.DEBUG) -handler = logging.handlers.RotatingFileHandler("log.txt", maxBytes=10000, backupCount=3) -formatter = logging.Formatter(format) -handler.setFormatter(formatter) -logging.getLogger('').addHandler(handler) - -logging.info("Starting pyfa") - def defPaths(): global pyfaPath global savePath @@ -64,6 +57,15 @@ def defPaths(): savePath = unicode(os.path.expanduser(os.path.join("~", ".pyfa")), sys.getfilesystemencoding()) + format = '%(asctime)s %(name)-24s %(levelname)-8s %(message)s' + logging.basicConfig(format=format, level=logLevel) + handler = logging.handlers.RotatingFileHandler(os.path.join(savePath, "log.txt"), maxBytes=1000000, backupCount=3) + formatter = logging.Formatter(format) + handler.setFormatter(formatter) + logging.getLogger('').addHandler(handler) + + logging.info("Starting pyfa") + # Redirect stderr to file if we're requested to do so stderrToFile = getattr(configforced, "stderrToFile", None) if stderrToFile is True: From 1b5e0467fc6f7eae10fcc928ceff0483f1cec6ea Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sun, 5 Jul 2015 13:59:18 -0400 Subject: [PATCH 20/20] Save browser sizes --- gui/mainFrame.py | 13 ++++++++++--- gui/marketBrowser.py | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 82c33791b..362e0b134 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -146,6 +146,7 @@ class MainFrame(wx.Frame): self.marketBrowser = MarketBrowser(self.notebookBrowsers) self.notebookBrowsers.AddPage(self.marketBrowser, "Market", tabImage = marketImg, showClose = False) + self.marketBrowser.splitter.SetSashPosition(self.marketHeight) self.shipBrowser = ShipBrowser(self.notebookBrowsers) self.notebookBrowsers.AddPage(self.shipBrowser, "Ships", tabImage = shipBrowserImg, showClose = False) @@ -159,8 +160,8 @@ class MainFrame(wx.Frame): self.notebookBrowsers.SetSelection(1) self.splitter.SplitVertically(self.notebookBrowsers, self.FitviewAdditionsPanel) - self.splitter.SetMinimumPaneSize(204) - self.splitter.SetSashPosition(300) + self.splitter.SetMinimumPaneSize(220) + self.splitter.SetSashPosition(self.browserWidth) cstatsSizer = wx.BoxSizer(wx.VERTICAL) @@ -227,7 +228,7 @@ class MainFrame(wx.Frame): def LoadMainFrameAttribs(self): - mainFrameDefaultAttribs = {"wnd_width": 1000, "wnd_height": 680, "wnd_maximized": False} + mainFrameDefaultAttribs = {"wnd_width": 1000, "wnd_height": 680, "wnd_maximized": False, "browser_width": 300, "market_height": 0} self.mainFrameAttribs = service.SettingsProvider.getInstance().getSettings("pyfaMainWindowAttribs", mainFrameDefaultAttribs) if self.mainFrameAttribs["wnd_maximized"]: @@ -241,6 +242,9 @@ class MainFrame(wx.Frame): self.SetSize((width, height)) self.SetMinSize((mainFrameDefaultAttribs["wnd_width"], mainFrameDefaultAttribs["wnd_height"])) + self.browserWidth = self.mainFrameAttribs["browser_width"] + self.marketHeight = self.mainFrameAttribs["market_height"] + def UpdateMainFrameAttribs(self): if self.IsIconized(): return @@ -250,6 +254,9 @@ class MainFrame(wx.Frame): self.mainFrameAttribs["wnd_height"] = height self.mainFrameAttribs["wnd_maximized"] = self.IsMaximized() + self.mainFrameAttribs["browser_width"] = self.notebookBrowsers.GetSize()[0] + self.mainFrameAttribs["market_height"] = self.marketBrowser.marketView.GetSize()[1] + def SetActiveStatsWindow(self, wnd): self.activeStatsWnd = wnd diff --git a/gui/marketBrowser.py b/gui/marketBrowser.py index 1c8329a1b..7f4404c59 100644 --- a/gui/marketBrowser.py +++ b/gui/marketBrowser.py @@ -51,7 +51,7 @@ class MarketBrowser(wx.Panel): self.itemView = ItemView(self.splitter, self) self.splitter.SplitHorizontally(self.marketView, self.itemView) - self.splitter.SetMinimumPaneSize(250) + self.splitter.SetMinimumPaneSize(150) # Setup our buttons for metaGroup selection # Same fix as for search box on macs,