Merge branch 'development'

Conflicts:
	eos/db/saveddata/queries.py
This commit is contained in:
blitzman
2017-01-10 22:42:01 -05:00
20 changed files with 179 additions and 1766 deletions

View File

@@ -8,7 +8,6 @@ __all__ = [
"booster",
"drone",
"implant",
"fleet",
"damagePattern",
"miscData",
"targetResists",

View File

@@ -0,0 +1,104 @@
# ===============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of pyfa.
#
# pyfa is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyfa is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
import sqlalchemy
import logging
logger = logging.getLogger(__name__)
class DatabaseCleanup:
def __init__(self):
pass
@staticmethod
def OrphanedCharacterSkills(saveddata_engine):
# Finds and fixes database corruption issues.
logger.debug("Start databsae validation and cleanup.")
# Find orphaned character skills.
# This solves an issue where the character doesn't exist, but skills for that character do.
# See issue #917
try:
logger.debug("Running database cleanup for character skills.")
results = saveddata_engine.execute("SELECT COUNT(*) AS num FROM characterSkills "
"WHERE characterID NOT IN (SELECT ID from characters)")
row = results.first()
if row and row['num']:
delete = saveddata_engine.execute("DELETE FROM characterSkills WHERE characterID NOT IN (SELECT ID from characters)")
logger.error("Database corruption found. Cleaning up %d records.", delete.rowcount)
except sqlalchemy.exc.DatabaseError:
logger.error("Failed to connect to database.")
@staticmethod
def OrphanedFitDamagePatterns(saveddata_engine):
# Find orphaned damage patterns.
# This solves an issue where the damage pattern doesn't exist, but fits reference the pattern.
# See issue #777
try:
logger.debug("Running database cleanup for orphaned damage patterns attached to fits.")
results = saveddata_engine.execute("SELECT COUNT(*) AS num FROM fits WHERE damagePatternID NOT IN (SELECT ID FROM damagePatterns) OR damagePatternID IS NULL")
row = results.first()
if row and row['num']:
# Get Uniform damage pattern ID
query = saveddata_engine.execute("SELECT ID FROM damagePatterns WHERE name = 'Uniform'")
rows = query.fetchall()
if len(rows) == 0:
logger.error("Missing uniform damage pattern.")
elif len(rows) > 1:
logger.error("More than one uniform damage pattern found.")
else:
uniform_damage_pattern_id = rows[0]['ID']
update = saveddata_engine.execute("UPDATE 'fits' SET 'damagePatternID' = ? "
"WHERE damagePatternID NOT IN (SELECT ID FROM damagePatterns) OR damagePatternID IS NULL",
uniform_damage_pattern_id)
logger.error("Database corruption found. Cleaning up %d records.", update.rowcount)
except sqlalchemy.exc.DatabaseError:
logger.error("Failed to connect to database.")
@staticmethod
def OrphanedFitCharacterIDs(saveddata_engine):
# Find orphaned character IDs. This solves an issue where the character doesn't exist, but fits reference the pattern.
try:
logger.debug("Running database cleanup for orphaned characters attached to fits.")
results = saveddata_engine.execute("SELECT COUNT(*) AS num FROM fits WHERE characterID NOT IN (SELECT ID FROM characters) OR characterID IS NULL")
row = results.first()
if row['num']:
# Get All 5 character ID
query = saveddata_engine.execute("SELECT ID FROM characters WHERE name = 'All 5'")
rows = query.fetchall()
if len(rows) == 0:
logger.error("Missing 'All 5' character.")
elif len(rows) > 1:
logger.error("More than one 'All 5' character found.")
else:
all5_id = rows[0]['ID']
update = saveddata_engine.execute("UPDATE 'fits' SET 'characterID' = ? "
"WHERE characterID not in (select ID from characters) OR characterID IS NULL",
all5_id)
logger.error("Database corruption found. Cleaning up %d records.", update.rowcount)
except sqlalchemy.exc.DatabaseError:
logger.error("Failed to connect to database.")

View File

@@ -1,65 +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 <http://www.gnu.org/licenses/>.
# ===============================================================================
from sqlalchemy import Table, Column, Integer, ForeignKey, String
from sqlalchemy.orm import mapper, relation
from eos.db import saveddata_meta
from eos.db.saveddata.fit import fits_table
from eos.types import Fleet, Wing, Squad, Fit
gangs_table = Table("gangs", saveddata_meta,
Column("ID", Integer, primary_key=True),
Column("leaderID", ForeignKey("fits.ID")),
Column("boosterID", ForeignKey("fits.ID")),
Column("name", String))
wings_table = Table("wings", saveddata_meta,
Column("ID", Integer, primary_key=True),
Column("gangID", ForeignKey("gangs.ID")),
Column("boosterID", ForeignKey("fits.ID")),
Column("leaderID", ForeignKey("fits.ID")))
squads_table = Table("squads", saveddata_meta,
Column("ID", Integer, primary_key=True),
Column("wingID", ForeignKey("wings.ID")),
Column("leaderID", ForeignKey("fits.ID")),
Column("boosterID", ForeignKey("fits.ID")))
squadmembers_table = Table("squadmembers", saveddata_meta,
Column("squadID", ForeignKey("squads.ID"), primary_key=True),
Column("memberID", ForeignKey("fits.ID"), primary_key=True))
mapper(Fleet, gangs_table,
properties={"wings": relation(Wing, backref="gang"),
"leader": relation(Fit, primaryjoin=gangs_table.c.leaderID == fits_table.c.ID),
"booster": relation(Fit, primaryjoin=gangs_table.c.boosterID == fits_table.c.ID)})
mapper(Wing, wings_table,
properties={"squads": relation(Squad, backref="wing"),
"leader": relation(Fit, primaryjoin=wings_table.c.leaderID == fits_table.c.ID),
"booster": relation(Fit, primaryjoin=wings_table.c.boosterID == fits_table.c.ID)})
mapper(Squad, squads_table,
properties={"leader": relation(Fit, primaryjoin=squads_table.c.leaderID == fits_table.c.ID),
"booster": relation(Fit, primaryjoin=squads_table.c.boosterID == fits_table.c.ID),
"members": relation(Fit,
primaryjoin=squads_table.c.ID == squadmembers_table.c.squadID,
secondaryjoin=squadmembers_table.c.memberID == fits_table.c.ID,
secondary=squadmembers_table)})

View File

@@ -17,14 +17,12 @@
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
from sqlalchemy.sql import and_
from eos.db import saveddata_session, sd_lock
from eos.db.saveddata.fit import projectedFits_table
from eos.db.saveddata.fleet import squadmembers_table
from eos.db.util import processEager, processWhere
from eos.types import *
from eos.db import saveddata_session, sd_lock
from eos.types import *
from eos.db.saveddata.fit import projectedFits_table
from sqlalchemy.sql import and_
import eos.config
configVal = getattr(eos.config, "saveddataCache", None)
@@ -212,52 +210,6 @@ def getFit(lookfor, eager=None):
return fit
@cachedQuery(Fleet, 1, "fleetID")
def getFleet(fleetID, eager=None):
if isinstance(fleetID, int):
if eager is None:
with sd_lock:
fleet = saveddata_session.query(Fleet).get(fleetID)
else:
eager = processEager(eager)
with sd_lock:
fleet = saveddata_session.query(Fleet).options(*eager).filter(Fleet.ID == fleetID).first()
else:
raise TypeError("Need integer as argument")
return fleet
@cachedQuery(Wing, 1, "wingID")
def getWing(wingID, eager=None):
if isinstance(wingID, int):
if eager is None:
with sd_lock:
wing = saveddata_session.query(Wing).get(wingID)
else:
eager = processEager(eager)
with sd_lock:
wing = saveddata_session.query(Wing).options(*eager).filter(Wing.ID == wingID).first()
else:
raise TypeError("Need integer as argument")
return wing
@cachedQuery(Squad, 1, "squadID")
def getSquad(squadID, eager=None):
if isinstance(squadID, int):
if eager is None:
with sd_lock:
squad = saveddata_session.query(Squad).get(squadID)
else:
eager = processEager(eager)
with sd_lock:
squad = saveddata_session.query(Squad).options(*eager).filter(Fleet.ID == squadID).first()
else:
raise TypeError("Need integer as argument")
return squad
def getFitsWithShip(shipID, ownerID=None, where=None, eager=None):
"""
Get all the fits using a certain ship.
@@ -341,14 +293,6 @@ def getFitList(eager=None):
return fits
def getFleetList(eager=None):
eager = processEager(eager)
with sd_lock:
fleets = saveddata_session.query(Fleet).options(*eager).all()
return fleets
@cachedQuery(Price, 1, "typeID")
def getPrice(typeID):
if isinstance(typeID, int):
@@ -472,18 +416,6 @@ def searchFits(nameLike, where=None, eager=None):
return fits
def getSquadsIDsWithFitID(fitID):
if isinstance(fitID, int):
with sd_lock:
squads = saveddata_session.query(squadmembers_table.c.squadID).filter(
squadmembers_table.c.memberID == fitID).all()
squads = tuple(entry[0] for entry in squads)
return squads
else:
raise TypeError("Need integer as argument")
def getProjectedFits(fitID):
if isinstance(fitID, int):
with sd_lock:

View File

@@ -3,10 +3,28 @@
# Used by:
# Modules from group: Missile Launcher Heavy (12 of 12)
# Modules from group: Missile Launcher Rocket (15 of 15)
# Modules named like: Launcher (153 of 153)
type = 'active'
# Modules named like: Launcher (151 of 151)
type = 'active', "projected"
def handler(fit, module, context):
def handler(fit, src, context):
# Set reload time to 10 seconds
module.reloadTime = 10000
src.reloadTime = 10000
if "projected" in context:
if src.item.group.name == 'Missile Launcher Bomb':
# Bomb Launcher Cooldown Timer
moduleReactivationDelay = src.getModifiedItemAttr("moduleReactivationDelay")
# Void and Focused Void Bombs
neutAmount = src.getModifiedChargeAttr("energyNeutralizerAmount")
if moduleReactivationDelay and neutAmount:
fit.addDrain(src, moduleReactivationDelay, neutAmount, 0)
# Lockbreaker Bombs
ecmStrengthBonus = src.getModifiedChargeAttr("scan{0}StrengthBonus".format(fit.scanType))
if ecmStrengthBonus:
strModifier = 1 - ecmStrengthBonus / fit.scanStrength
fit.ecmProjectedStr *= strModifier

View File

@@ -134,7 +134,6 @@ class Fit(object):
self.__capRecharge = None
self.__calculatedTargets = []
self.factorReload = False
self.fleet = None
self.boostsFits = set()
self.gangBoosts = None
self.ecmProjectedStr = 1
@@ -632,14 +631,6 @@ class Fit(object):
groups = ("Energy Weapon", "Hybrid Weapon")
self.modules.filteredItemBoost(lambda mod: mod.item.group.name in groups, "maxRange", value, stackingPenalties=True)
# if effect.isType("offline") or (effect.isType("passive") and thing.state >= State.ONLINE) or \
# (effect.isType("active") and thing.state >= State.ACTIVE):
# # Run effect, and get proper bonuses applied
# try:
# self.register(thing)
# effect.handler(self, thing, context)
# except:
# pass
else:
# Run effect, and get proper bonuses applied
try:
@@ -665,7 +656,6 @@ class Fit(object):
# Don't inspect this, we genuinely want to reassign self
# noinspection PyMethodFirstArgAssignment
self = copy.deepcopy(self)
self.fleet = copied.fleet
logger.debug("Handling self projection - making shadow copy of fit. %r => %r", copied, self)
# we delete the fit because when we copy a fit, flush() is
# called to properly handle projection updates. However, we do
@@ -678,24 +668,6 @@ class Fit(object):
continue
fit.calculateModifiedAttributes(self, True)
#
# for thing in chain(fit.modules, fit.implants, fit.character.skills, (fit.ship,)):
# if thing.item is None:
# continue
# for effect in thing.item.effects.itervalues():
# # And check if it actually has gang boosting effects
# if effect.isType("gang"):
# effect.handler(self, thing, ("commandRun"))
# if self.fleet is not None and withBoosters is True:
# logger.debug("Fleet is set, gathering gang boosts")
#
# self.gangBoosts = self.fleet.recalculateLinear(withBoosters=withBoosters)
#
# timer.checkpoint("Done calculating gang boosts for %r"%self)
# elif self.fleet is None:
# self.gangBoosts = None
# If we're not explicitly asked to project fit onto something,
# set self as target fit

View File

@@ -1,341 +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 <http://www.gnu.org/licenses/>.
# ===============================================================================
from copy import deepcopy
from itertools import chain
from eos.types import Skill, Module, Ship
class Fleet(object):
def calculateModifiedAttributes(self):
# Make sure ALL fits in the gang have been calculated
for c in chain(self.wings, (self.leader,)):
if c is not None:
c.calculateModifiedAttributes()
leader = self.leader
self.booster = booster = self.booster if self.booster is not None else leader
self.broken = False
self.store = store = Store()
store.set(booster, "fleet")
# Go all the way down for each subtree we have.
for wing in self.wings:
wing.calculateGangBonusses(store)
# Check skill requirements and wing amount to see if we break or not
if len(self.wings) == 0 or leader is None or leader.character is None or leader.character.getSkill(
"Fleet Command").level < len(self.wings):
self.broken = True
# Now calculate our own if we aren't broken
if not self.broken:
# We only get our own bonuses *Sadface*
store.apply(leader, "fleet")
def recalculateLinear(self, withBoosters=True, dirtyStorage=None):
self.store = Store()
self.linearBoosts = {}
if withBoosters is True:
if self.leader is not None and self.leader.character is not None and self.leader.character.getSkill(
"Fleet Command").level >= 1:
self.leader.boostsFits.add(self.wings[0].squads[0].members[0].ID)
self.leader.calculateModifiedAttributes()
self.store.set(self.leader, "squad", clearingUpdate=True)
else:
self.store = Store()
if self.leader is not None:
try:
self.leader.boostsFits.remove(self.wings[0].squads[0].members[0].ID)
except KeyError:
pass
self.wings[0].recalculateLinear(self.store, withBoosters=withBoosters, dirtyStorage=dirtyStorage)
return self.linearBoosts
def count(self):
total = 0
for wing in self.wings:
total += wing.count()
return total
def extend(self):
self.wings.append(Wing())
def __deepcopy__(self, memo):
copy = Fleet()
copy.name = self.name
copy.booster = deepcopy(self.booster)
copy.leader = deepcopy(self.leader)
for wing in self.wings:
copy.wings.append(deepcopy(wing))
return copy
class Wing(object):
def calculateModifiedAttributes(self):
for c in chain(self.squads, (self.leader,)):
if c is not None:
c.calculateModifiedAttributes()
def calculateGangBonusses(self, store):
self.broken = False
leader = self.leader
self.booster = booster = self.booster if self.booster is not None else leader
store.set(booster, "wing")
# ALWAYS move down
for squad in self.squads:
squad.calculateGangBonusses(store)
# Check skill requirements and squad amount to see if we break or not
if len(self.squads) == 0 or leader is None or leader.character is None or leader.character.getSkill(
"Wing Command").level < len(self.squads):
self.broken = True
# Check if we aren't broken, if we aren't, boost
if not self.broken:
store.apply(leader, "wing")
else:
# We broke, don't go up
self.gang.broken = True
def recalculateLinear(self, store, withBoosters=True, dirtyStorage=None):
if withBoosters is True:
if self.leader is not None and self.leader.character is not None and self.leader.character.getSkill(
"Wing Command").level >= 1:
self.leader.boostsFits.add(self.squads[0].members[0].ID)
self.leader.calculateModifiedAttributes()
store.set(self.leader, "squad", clearingUpdate=False)
else:
store = Store()
if self.gang.leader is not None:
try:
self.gang.leader.boostsFits.remove(self.squads[0].members[0].ID)
except KeyError:
pass
if self.leader is not None:
try:
self.leader.boostsFits.remove(self.squads[0].members[0].ID)
except KeyError:
pass
self.squads[0].recalculateLinear(store, withBoosters=withBoosters, dirtyStorage=dirtyStorage)
def count(self):
total = 0 if self.leader is None else 1
for squad in self.squads:
total += squad.count()
return total
def extend(self):
self.squads.append(Squad())
def __deepcopy__(self, memo):
copy = Wing()
copy.booster = deepcopy(self.booster)
copy.leader = deepcopy(self.leader)
for squad in self.squads:
copy.squads.append(deepcopy(squad))
return copy
class Squad(object):
def calculateModifiedAttributes(self):
for member in self.members:
member.calculateModifiedAttributes()
def calculateGangBonusses(self, store):
self.broken = False
leader = self.leader
self.booster = booster = self.booster if self.booster is not None else leader
store.set(booster, "squad")
# Check skill requirements and squad size to see if we break or not
if len(self.members) <= 0 or leader is None or leader.character is None or leader.character.getSkill(
"Leadership").level * 2 < len(self.members):
self.broken = True
if not self.broken:
for member in self.members:
store.apply(member, "squad")
else:
self.wing.broken = True
def recalculateLinear(self, store, withBoosters=True, dirtyStorage=None):
if withBoosters is True:
if self.leader is not None and self.leader.character is not None and self.leader.character.getSkill(
"Leadership").level >= 1:
self.leader.boostsFits.add(self.members[0].ID)
self.leader.calculateModifiedAttributes(dirtyStorage=dirtyStorage)
store.set(self.leader, "squad", clearingUpdate=False)
else:
store = Store()
if self.leader is not None:
try:
self.leader.boostsFits.remove(self.members[0].ID)
except KeyError:
pass
if self.wing.leader is not None:
try:
self.wing.leader.boostsFits.remove(self.members[0].ID)
except KeyError:
pass
if self.wing.gang.leader is not None:
try:
self.wing.gang.leader.boostsFits.remove(self.members[0].ID)
except KeyError:
pass
if getattr(self.wing.gang, "linearBoosts", None) is None:
self.wing.gang.linearBoosts = {}
dict = store.bonuses["squad"]
for boostedAttr, boostInfoList in dict.iteritems():
for boostInfo in boostInfoList:
effect, thing = boostInfo
# Get current boost value for given attribute, use 0 as fallback if
# no boosts applied yet
currBoostAmount = self.wing.gang.linearBoosts.get(boostedAttr, (0,))[0]
# Attribute name which is used to get boost value
newBoostAttr = effect.getattr("gangBonus") or "commandBonus"
# Get boost amount for current boost
newBoostAmount = thing.getModifiedItemAttr(newBoostAttr) or 0
# Skill used to modify the gang bonus (for purposes of comparing old vs new)
newBoostSkill = effect.getattr("gangBonusSkill")
# If skill takes part in gang boosting, multiply by skill level
if type(thing) == Skill:
newBoostAmount *= thing.level
# boost the gang bonus based on skill noted in effect file
if newBoostSkill:
newBoostAmount *= thing.parent.character.getSkill(newBoostSkill).level
# If new boost is more powerful, replace older one with it
if abs(newBoostAmount) > abs(currBoostAmount):
self.wing.gang.linearBoosts[boostedAttr] = (newBoostAmount, boostInfo)
def count(self):
return len(self.members)
def __deepcopy__(self, memo):
copy = Squad()
copy.booster = deepcopy(self.booster)
copy.leader = deepcopy(self.leader)
for member in self.members:
copy.members.append(deepcopy(member))
return copy
class Store(object):
def __init__(self):
# Container for gang boosters and their respective bonuses, three-layered
self.bonuses = {}
for dictType in ("fleet", "wing", "squad"):
self.bonuses[dictType] = {}
# Container for boosted fits and corresponding boosts applied onto them
self.boosts = {}
def set(self, fitBooster, layer, clearingUpdate=True):
"""Add all gang boosts of given fit for given layer to boost store"""
if fitBooster is None:
return
# This dict contains all bonuses for specified layer
dict = self.bonuses[layer]
if clearingUpdate is True:
# Clear existing bonuses
dict.clear()
# Go through everything which can be used as gang booster
for thing in chain(fitBooster.modules, fitBooster.implants, fitBooster.character.skills, (fitBooster.ship,)):
if thing.item is None:
continue
for effect in thing.item.effects.itervalues():
# And check if it actually has gang boosting effects
if effect.isType("gang"):
# Attribute which is boosted
boostedAttr = effect.getattr("gangBoost")
# List which contains all bonuses for given attribute for given layer
l = dict.get(boostedAttr)
# If there was no list, create it
if l is None:
l = dict[boostedAttr] = []
# And append effect which is used to boost stuff and carrier of this effect
l.append((effect, thing))
contextMap = {Skill: "skill",
Ship: "ship",
Module: "module"}
def apply(self, fitBoosted, layer):
"""Applies all boosts onto given fit for given layer"""
if fitBoosted is None:
return
# Boosts dict contains all bonuses applied onto given fit
self.boosts[fitBoosted] = boosts = {}
# Go through all bonuses for given layer, and find highest one per boosted attribute
for currLayer in ("fleet", "wing", "squad"):
# Dictionary with boosts for given layer
dict = self.bonuses[currLayer]
for boostedAttr, boostInfoList in dict.iteritems():
for boostInfo in boostInfoList:
effect, thing = boostInfo
# Get current boost value for given attribute, use 0 as fallback if
# no boosts applied yet
currBoostAmount = boosts.get(boostedAttr, (0,))[0]
# Attribute name which is used to get boost value
newBoostAttr = effect.getattr("gangBonus") or "commandBonus"
# Get boost amount for current boost
newBoostAmount = thing.getModifiedItemAttr(newBoostAttr) or 0
# Skill used to modify the gang bonus (for purposes of comparing old vs new)
newBoostSkill = effect.getattr("gangBonusSkill")
# If skill takes part in gang boosting, multiply by skill level
if type(thing) == Skill:
newBoostAmount *= thing.level
# boost the gang bonus based on skill noted in effect file
if newBoostSkill:
newBoostAmount *= thing.parent.character.getSkill(newBoostSkill).level
# If new boost is more powerful, replace older one with it
if abs(newBoostAmount) > abs(currBoostAmount):
boosts[boostedAttr] = (newBoostAmount, boostInfo)
# Don't look further down then current layer, wing commanders don't get squad bonuses and all that
if layer == currLayer:
break
self.modify(fitBoosted)
def getBoosts(self, fit):
"""Return all boosts applied onto given fit"""
return self.boosts.get(fit)
def modify(self, fitBoosted):
# Get all boosts which should be applied onto current fit
boosts = self.getBoosts(fitBoosted)
# Now we got it all figured out, actually do the useful part of all this
for name, info in boosts.iteritems():
# Unpack all data required to run effect properly
effect, thing = info[1]
context = ("gang", self.contextMap[type(thing)])
# Run effect, and get proper bonuses applied
try:
effect.handler(fitBoosted, thing, context)
except:
pass

View File

@@ -39,7 +39,6 @@ from eos.saveddata.implantSet import ImplantSet
from eos.saveddata.booster import Booster
from eos.saveddata.fit import Fit, ImplantLocation
from eos.saveddata.mode import Mode
from eos.saveddata.fleet import Fleet, Wing, Squad
from eos.saveddata.miscData import MiscData
from eos.saveddata.override import Override