From 2951648dcf10a09af855be60af7361611cc0ad79 Mon Sep 17 00:00:00 2001 From: Skybladev2 Date: Sun, 5 Apr 2026 21:58:41 +0300 Subject: [PATCH 1/6] fix tab switch when projected is mutually applied --- eos/saveddata/fit.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py index eb4587b61..2b9c6b5ba 100644 --- a/eos/saveddata/fit.py +++ b/eos/saveddata/fit.py @@ -1107,6 +1107,11 @@ def calculateModifiedAttributes(self, targetFit=None, type=CalcType.LOCAL): # tabs. See GH issue 1193 if type == CalcType.COMMAND and targetFit in self.commandFits: pyfalog.debug("{} is in the command listing for COMMAND ({}), do not mark self as calculated (recursive)".format(repr(targetFit), repr(self))) + elif type == CalcType.PROJECTED: + # A projected-source pass only applies this fit's remote effects to the target; it is not a full local + # calculation of this fit. Marking calculated would make switchFit skip recalc, so bidirectional + # projections show wrong stats when switching tabs until the projection is toggled. + pass else: self.__calculated = True From 664e5820cb6d3807631e3add5d4a8c587571e61a Mon Sep 17 00:00:00 2001 From: Skybladev2 Date: Sun, 5 Apr 2026 22:01:52 +0300 Subject: [PATCH 2/6] adujst logs --- eos/saveddata/fit.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py index 2b9c6b5ba..512a57569 100644 --- a/eos/saveddata/fit.py +++ b/eos/saveddata/fit.py @@ -1108,10 +1108,7 @@ def calculateModifiedAttributes(self, targetFit=None, type=CalcType.LOCAL): if type == CalcType.COMMAND and targetFit in self.commandFits: pyfalog.debug("{} is in the command listing for COMMAND ({}), do not mark self as calculated (recursive)".format(repr(targetFit), repr(self))) elif type == CalcType.PROJECTED: - # A projected-source pass only applies this fit's remote effects to the target; it is not a full local - # calculation of this fit. Marking calculated would make switchFit skip recalc, so bidirectional - # projections show wrong stats when switching tabs until the projection is toggled. - pass + pyfalog.debug("{} is projecting onto {} (PROJECTED), do not mark self as calculated (not a full local calc)".format(repr(self), repr(targetFit))) else: self.__calculated = True From 0a6a3ca74de2b328f7ef2724e67a622345635d21 Mon Sep 17 00:00:00 2001 From: Skybladev2 Date: Sun, 12 Apr 2026 20:30:45 +0300 Subject: [PATCH 3/6] Recalculate all graph fits so mutual projections apply to every ship Complements the PROJECTED calc marking fix: the graph must run a full local recalc for each listed fit, not only the active tab, so incoming projected effects are reflected in speeds and damage for every ship in the comparison. Made-with: Cursor --- graphs/gui/frame.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/graphs/gui/frame.py b/graphs/gui/frame.py index 4313b81d7..602839155 100644 --- a/graphs/gui/frame.py +++ b/graphs/gui/frame.py @@ -30,6 +30,7 @@ from gui.auxWindow import AuxiliaryFrame from gui.bitmap_loader import BitmapLoader from service.const import GraphCacheCleanupReason +from service.fit import Fit from service.settings import GraphSettings from . import canvasPanel from .ctrlPanel import GraphControlPanel @@ -239,7 +240,30 @@ def getView(self, idx=None): def clearCache(self, reason, extraData=None): self.getView().clearCache(reason, extraData) + def _ensureGraphFitsRecalculated(self): + """ + Recalculate every fit shown in the graph when multiple ships are listed. + + The main window only runs a full local calculation for the active tab. Other + loaded fits can keep stale ship attributes for incoming projections (mutual + projected effects) until they become active, which breaks multi-fit graphs. + """ + ctrl = self.ctrlPanel + sFit = Fit.getInstance() + seen = set() + fits = [] + for wrapper in ctrl.sources + ctrl.targets: + if not wrapper.isFit or wrapper.item.ID in seen: + continue + seen.add(wrapper.item.ID) + fits.append(wrapper.item) + if len(fits) < 2: + return + for fit in fits: + sFit.recalc(fit) + def draw(self): + self._ensureGraphFitsRecalculated() self.canvasPanel.draw() def resetXMark(self): From a6aab640442a69b9b196b0f5c1516d56cb292900 Mon Sep 17 00:00:00 2001 From: Skybladev2 Date: Sun, 12 Apr 2026 20:39:28 +0300 Subject: [PATCH 4/6] more fixes --- eos/saveddata/fit.py | 19 +++++++++++++++++++ graphs/gui/frame.py | 10 ++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py index 512a57569..0f16527f3 100644 --- a/eos/saveddata/fit.py +++ b/eos/saveddata/fit.py @@ -19,6 +19,7 @@ import datetime import time +from contextlib import contextmanager from copy import deepcopy from itertools import chain from math import ceil, log, sqrt @@ -67,6 +68,22 @@ class Fit: PEAK_RECHARGE = 0.25 + # When > 0, __resetDependentCalcs does not mark projection victims stale. + # Sequential full recalcs for mutually-projected fits (e.g. graph) would + # otherwise set victim.calculated = False without clearing them; the next + # PROJECTED pass then runs clear() and wipes that fit after it was just + # calculated correctly. + _suspendVictimCalcResetDepth = 0 + + @classmethod + @contextmanager + def suspendVictimCalcReset(cls): + cls._suspendVictimCalcResetDepth += 1 + try: + yield + finally: + cls._suspendVictimCalcResetDepth -= 1 + def __init__(self, ship=None, name=""): """Initialize a fit from the program""" self.__ship = None @@ -973,6 +990,8 @@ def __runCommandBoosts(self, runTime="normal"): def __resetDependentCalcs(self): self.calculated = False + if Fit._suspendVictimCalcResetDepth > 0: + return for value in list(self.projectedOnto.values()): if value.victim_fit: # removing a self-projected fit causes victim fit to be None. @todo: look into why. :3 value.victim_fit.calculated = False diff --git a/graphs/gui/frame.py b/graphs/gui/frame.py index 602839155..f382a4933 100644 --- a/graphs/gui/frame.py +++ b/graphs/gui/frame.py @@ -25,6 +25,7 @@ import gui.display import gui.globalEvents as GE import gui.mainFrame +from eos.saveddata.fit import Fit as EosFit from graphs.data.base import FitGraph from graphs.events import RESIST_MODE_CHANGED from gui.auxWindow import AuxiliaryFrame @@ -259,8 +260,13 @@ def _ensureGraphFitsRecalculated(self): fits.append(wrapper.item) if len(fits) < 2: return - for fit in fits: - sFit.recalc(fit) + # Without this, each recalc's __resetDependentCalcs marks projection victims + # as not calculated; the next fit's calculation then invokes those victims in + # PROJECTED mode with calculated=False, which runs clear() and wipes ship state + # built by the previous recalc in the batch. + with EosFit.suspendVictimCalcReset(): + for fit in fits: + sFit.recalc(fit) def draw(self): self._ensureGraphFitsRecalculated() From 42ab122e27b5f62e05c5e1f42963fefc3e850518 Mon Sep 17 00:00:00 2001 From: Skybladev2 Date: Mon, 13 Apr 2026 10:08:38 +0300 Subject: [PATCH 5/6] Keep fix-projected branch scoped to projection engine logic Drop graph frame recalculation changes from this branch so it only contains fit calculation fixes and can be composed with focus-graph cleanly. Made-with: Cursor --- graphs/gui/frame.py | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/graphs/gui/frame.py b/graphs/gui/frame.py index f382a4933..4313b81d7 100644 --- a/graphs/gui/frame.py +++ b/graphs/gui/frame.py @@ -25,13 +25,11 @@ import gui.display import gui.globalEvents as GE import gui.mainFrame -from eos.saveddata.fit import Fit as EosFit from graphs.data.base import FitGraph from graphs.events import RESIST_MODE_CHANGED from gui.auxWindow import AuxiliaryFrame from gui.bitmap_loader import BitmapLoader from service.const import GraphCacheCleanupReason -from service.fit import Fit from service.settings import GraphSettings from . import canvasPanel from .ctrlPanel import GraphControlPanel @@ -241,35 +239,7 @@ def getView(self, idx=None): def clearCache(self, reason, extraData=None): self.getView().clearCache(reason, extraData) - def _ensureGraphFitsRecalculated(self): - """ - Recalculate every fit shown in the graph when multiple ships are listed. - - The main window only runs a full local calculation for the active tab. Other - loaded fits can keep stale ship attributes for incoming projections (mutual - projected effects) until they become active, which breaks multi-fit graphs. - """ - ctrl = self.ctrlPanel - sFit = Fit.getInstance() - seen = set() - fits = [] - for wrapper in ctrl.sources + ctrl.targets: - if not wrapper.isFit or wrapper.item.ID in seen: - continue - seen.add(wrapper.item.ID) - fits.append(wrapper.item) - if len(fits) < 2: - return - # Without this, each recalc's __resetDependentCalcs marks projection victims - # as not calculated; the next fit's calculation then invokes those victims in - # PROJECTED mode with calculated=False, which runs clear() and wipes ship state - # built by the previous recalc in the batch. - with EosFit.suspendVictimCalcReset(): - for fit in fits: - sFit.recalc(fit) - def draw(self): - self._ensureGraphFitsRecalculated() self.canvasPanel.draw() def resetXMark(self): From f2c28ce3c0a3ab69a07f550ac1a3554956abcb38 Mon Sep 17 00:00:00 2001 From: Skybladev2 Date: Tue, 14 Apr 2026 11:14:39 +0300 Subject: [PATCH 6/6] Handle transient owner-less modules during tab switches. Avoid getCycleParameters crashes by falling back to non-reload factoring when module owner is temporarily unset. Made-with: Cursor --- eos/saveddata/module.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/eos/saveddata/module.py b/eos/saveddata/module.py index 99a9eaa61..a989df91b 100644 --- a/eos/saveddata/module.py +++ b/eos/saveddata/module.py @@ -970,8 +970,14 @@ def getCycleParameters(self, reloadOverride=None): # Determine if we'll take into account reload time or not if reloadOverride is not None: factorReload = reloadOverride + elif self.forceReload is not None: + factorReload = self.forceReload + elif self.owner is None: + # Owner can be temporarily unset during fit/tab transitions; default to + # no reload factoring until association is restored. + factorReload = False else: - factorReload = self.owner.factorReload if self.forceReload is None else self.forceReload + factorReload = self.owner.factorReload cycles_until_reload = self.numShots if cycles_until_reload == 0: