Compare commits
8 Commits
v2.65.2.28
...
v2.65.2.34
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b9c51db04 | |||
| b1c9b57ef7 | |||
| 5c01ecb2d1 | |||
| 564ba1d85d | |||
| 1c7886463d | |||
| bc23f380db | |||
| b9da617009 | |||
| dc38f33536 |
11
.dockerignore
Normal file
11
.dockerignore
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
.venv
|
||||||
|
.git
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
.pytest_cache
|
||||||
|
*.zip
|
||||||
|
*.spec
|
||||||
|
imgs
|
||||||
|
dist_assets
|
||||||
11
Dockerfile
Normal file
11
Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements-server.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements-server.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
EXPOSE 9123
|
||||||
|
|
||||||
|
CMD ["python", "pyfa_server.py"]
|
||||||
36
build.sh
36
build.sh
@@ -23,8 +23,38 @@ rm -rf build dist
|
|||||||
echo "Building binary with PyInstaller..."
|
echo "Building binary with PyInstaller..."
|
||||||
uv run pyinstaller pyfa.spec
|
uv run pyinstaller pyfa.spec
|
||||||
|
|
||||||
# cp oleacc* dist/pyfa/
|
# Sim server exe (console) into main dist folder
|
||||||
|
if [ -f dist/pyfa_server/pyfa-server.exe ]; then
|
||||||
|
cp dist/pyfa_server/pyfa-server.exe dist/pyfa/
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Docker image (Python server)
|
||||||
|
DOCKER_REPO="${DOCKER_REPO:-docker.site.quack-lab.dev}"
|
||||||
|
IMAGE_NAME="${IMAGE_NAME:-pyfa-server}"
|
||||||
|
COMMIT_SHA=$(git rev-parse --short HEAD)
|
||||||
|
IMAGE_BASE="${DOCKER_REPO}/${IMAGE_NAME}"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Build complete! Binary is located at: dist/pyfa/pyfa.exe"
|
echo "Building Docker image..."
|
||||||
echo "You can run it with: dist/pyfa/pyfa.exe"
|
docker build -t "${IMAGE_BASE}:${COMMIT_SHA}" .
|
||||||
|
|
||||||
|
docker tag "${IMAGE_BASE}:${COMMIT_SHA}" "${IMAGE_BASE}:latest"
|
||||||
|
|
||||||
|
TAGS=$(git tag --points-at HEAD 2>/dev/null || true)
|
||||||
|
if [ -n "$TAGS" ]; then
|
||||||
|
while IFS= read -r tag; do
|
||||||
|
[ -n "$tag" ] && docker tag "${IMAGE_BASE}:${COMMIT_SHA}" "${IMAGE_BASE}:${tag}"
|
||||||
|
done <<< "$TAGS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Build complete! dist/pyfa/pyfa.exe (GUI), dist/pyfa/pyfa-server.exe (POST /simulate :9123)"
|
||||||
|
echo ""
|
||||||
|
echo "Docker image built as:"
|
||||||
|
echo " - ${IMAGE_BASE}:${COMMIT_SHA}"
|
||||||
|
echo " - ${IMAGE_BASE}:latest"
|
||||||
|
if [ -n "$TAGS" ]; then
|
||||||
|
while IFS= read -r tag; do
|
||||||
|
[ -n "$tag" ] && echo " - ${IMAGE_BASE}:${tag}"
|
||||||
|
done <<< "$TAGS"
|
||||||
|
fi
|
||||||
|
|||||||
43
config.py
43
config.py
@@ -1,7 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import yaml
|
import yaml
|
||||||
import wx
|
|
||||||
|
|
||||||
from logbook import CRITICAL, DEBUG, ERROR, FingersCrossedHandler, INFO, Logger, NestedSetup, NullHandler, \
|
from logbook import CRITICAL, DEBUG, ERROR, FingersCrossedHandler, INFO, Logger, NestedSetup, NullHandler, \
|
||||||
StreamHandler, TimedRotatingFileHandler, WARNING
|
StreamHandler, TimedRotatingFileHandler, WARNING
|
||||||
@@ -67,20 +66,34 @@ LOGLEVEL_MAP = {
|
|||||||
CATALOG = 'lang'
|
CATALOG = 'lang'
|
||||||
|
|
||||||
|
|
||||||
slotColourMapDark = {
|
def get_slotColourMapDark():
|
||||||
FittingSlot.LOW: wx.Colour(44, 36, 19), # yellow = low slots 24/13
|
import wx
|
||||||
FittingSlot.MED: wx.Colour(28, 39, 51), # blue = mid slots 8.1/9.5
|
return {
|
||||||
FittingSlot.HIGH: wx.Colour(53, 31, 34), # red = high slots 6.5/11.5
|
FittingSlot.LOW: wx.Colour(44, 36, 19), # yellow = low slots 24/13
|
||||||
FittingSlot.RIG: '',
|
FittingSlot.MED: wx.Colour(28, 39, 51), # blue = mid slots 8.1/9.5
|
||||||
FittingSlot.SUBSYSTEM: ''}
|
FittingSlot.HIGH: wx.Colour(53, 31, 34), # red = high slots 6.5/11.5
|
||||||
errColorDark = wx.Colour(70, 20, 20)
|
FittingSlot.RIG: '',
|
||||||
slotColourMap = {
|
FittingSlot.SUBSYSTEM: ''}
|
||||||
FittingSlot.LOW: wx.Colour(250, 235, 204), # yellow = low slots
|
|
||||||
FittingSlot.MED: wx.Colour(188, 215, 241), # blue = mid slots
|
|
||||||
FittingSlot.HIGH: wx.Colour(235, 204, 209), # red = high slots
|
def get_errColorDark():
|
||||||
FittingSlot.RIG: '',
|
import wx
|
||||||
FittingSlot.SUBSYSTEM: ''}
|
return wx.Colour(70, 20, 20)
|
||||||
errColor = wx.Colour(204, 51, 51)
|
|
||||||
|
|
||||||
|
def get_slotColourMap():
|
||||||
|
import wx
|
||||||
|
return {
|
||||||
|
FittingSlot.LOW: wx.Colour(250, 235, 204), # yellow = low slots
|
||||||
|
FittingSlot.MED: wx.Colour(188, 215, 241), # blue = mid slots
|
||||||
|
FittingSlot.HIGH: wx.Colour(235, 204, 209), # red = high slots
|
||||||
|
FittingSlot.RIG: '',
|
||||||
|
FittingSlot.SUBSYSTEM: ''}
|
||||||
|
|
||||||
|
|
||||||
|
def get_errColor():
|
||||||
|
import wx
|
||||||
|
return wx.Colour(204, 51, 51)
|
||||||
|
|
||||||
|
|
||||||
def getClientSecret():
|
def getClientSecret():
|
||||||
|
|||||||
5
docker-compose.yml
Normal file
5
docker-compose.yml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
services:
|
||||||
|
pyfa-server:
|
||||||
|
image: docker.site.quack-lab.dev/pyfa-server:latest
|
||||||
|
ports:
|
||||||
|
- "9123:9123"
|
||||||
@@ -121,7 +121,12 @@ def _get_cycle_time_s(mod):
|
|||||||
return avg_time_ms / 1000.0
|
return avg_time_ms / 1000.0
|
||||||
|
|
||||||
|
|
||||||
def get_first_burnout_samples(fit, rack_slot, max_time_s, iterations):
|
def has_burnout_samples(fit, rack_slot, max_time_s, iterations):
|
||||||
|
cache_key = (getattr(fit, "ID", None), int(rack_slot), max_time_s, iterations)
|
||||||
|
return cache_key in _burnout_samples_cache
|
||||||
|
|
||||||
|
|
||||||
|
def get_first_burnout_samples(fit, rack_slot, max_time_s, iterations, progress_cb=None):
|
||||||
"""
|
"""
|
||||||
Monte Carlo simulation of time until the first module in the given rack burns out.
|
Monte Carlo simulation of time until the first module in the given rack burns out.
|
||||||
Returns a list of burnout times (seconds). If no burnout happens before max_time_s,
|
Returns a list of burnout times (seconds). If no burnout happens before max_time_s,
|
||||||
@@ -178,7 +183,7 @@ def get_first_burnout_samples(fit, rack_slot, max_time_s, iterations):
|
|||||||
|
|
||||||
samples = []
|
samples = []
|
||||||
|
|
||||||
for _ in range(iterations):
|
for i in range(iterations):
|
||||||
hp = list(base_hp)
|
hp = list(base_hp)
|
||||||
dead = [hp_val <= 0 for hp_val in hp]
|
dead = [hp_val <= 0 for hp_val in hp]
|
||||||
online_counts = dict(base_online_counts)
|
online_counts = dict(base_online_counts)
|
||||||
@@ -280,6 +285,11 @@ def get_first_burnout_samples(fit, rack_slot, max_time_s, iterations):
|
|||||||
|
|
||||||
samples.append(sample_time)
|
samples.append(sample_time)
|
||||||
|
|
||||||
|
if progress_cb is not None:
|
||||||
|
# progress_cb should return True to continue, False to cancel
|
||||||
|
if not progress_cb(i + 1):
|
||||||
|
break
|
||||||
|
|
||||||
_burnout_samples_cache[cache_key] = samples
|
_burnout_samples_cache[cache_key] = samples
|
||||||
return samples
|
return samples
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,8 @@
|
|||||||
|
|
||||||
from eos.const import FittingSlot
|
from eos.const import FittingSlot
|
||||||
from graphs.data.base import SmoothPointGetter
|
from graphs.data.base import SmoothPointGetter
|
||||||
from .calc import get_first_burnout_samples, get_rack_heat_value
|
import wx
|
||||||
|
from .calc import get_first_burnout_samples, get_rack_heat_value, has_burnout_samples
|
||||||
|
|
||||||
|
|
||||||
class _BaseTime2RackHeatGetter(SmoothPointGetter):
|
class _BaseTime2RackHeatGetter(SmoothPointGetter):
|
||||||
@@ -64,9 +65,41 @@ class _BaseTime2BurnoutCdfGetter(SmoothPointGetter):
|
|||||||
iterations = self._iterations
|
iterations = self._iterations
|
||||||
if iterations <= 0:
|
if iterations <= 0:
|
||||||
iterations = self._iterations
|
iterations = self._iterations
|
||||||
samples = get_first_burnout_samples(
|
samples = None
|
||||||
fit=fit, rack_slot=self.rack_slot, max_time_s=max_sim_time, iterations=iterations
|
# Show a progress dialog only on cache miss for expensive runs
|
||||||
)
|
if iterations >= 1000 and not has_burnout_samples(fit, self.rack_slot, max_sim_time, iterations):
|
||||||
|
app = wx.GetApp()
|
||||||
|
parent = app.GetTopWindow() if app is not None else None
|
||||||
|
dlg = wx.ProgressDialog(
|
||||||
|
"Computing burnout CDF",
|
||||||
|
"Running overheating simulations...",
|
||||||
|
maximum=iterations,
|
||||||
|
parent=parent,
|
||||||
|
style=wx.PD_APP_MODAL | wx.PD_ELAPSED_TIME | wx.PD_AUTO_HIDE,
|
||||||
|
)
|
||||||
|
|
||||||
|
def progress_cb(done):
|
||||||
|
# dlg.Update returns (continue, skip)
|
||||||
|
cont, _ = dlg.Update(done)
|
||||||
|
return cont
|
||||||
|
|
||||||
|
try:
|
||||||
|
samples = get_first_burnout_samples(
|
||||||
|
fit=fit,
|
||||||
|
rack_slot=self.rack_slot,
|
||||||
|
max_time_s=max_sim_time,
|
||||||
|
iterations=iterations,
|
||||||
|
progress_cb=progress_cb,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
dlg.Destroy()
|
||||||
|
else:
|
||||||
|
samples = get_first_burnout_samples(
|
||||||
|
fit=fit,
|
||||||
|
rack_slot=self.rack_slot,
|
||||||
|
max_time_s=max_sim_time,
|
||||||
|
iterations=iterations,
|
||||||
|
)
|
||||||
xs = []
|
xs = []
|
||||||
ys = []
|
ys = []
|
||||||
if not samples:
|
if not samples:
|
||||||
@@ -98,7 +131,10 @@ class _BaseTime2BurnoutCdfGetter(SmoothPointGetter):
|
|||||||
if iterations <= 0:
|
if iterations <= 0:
|
||||||
iterations = self._iterations
|
iterations = self._iterations
|
||||||
samples = get_first_burnout_samples(
|
samples = get_first_burnout_samples(
|
||||||
fit=fit, rack_slot=self.rack_slot, max_time_s=max_sim_time, iterations=iterations
|
fit=fit,
|
||||||
|
rack_slot=self.rack_slot,
|
||||||
|
max_time_s=max_sim_time,
|
||||||
|
iterations=iterations,
|
||||||
)
|
)
|
||||||
if not samples:
|
if not samples:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ import wx
|
|||||||
|
|
||||||
import gui.mainFrame
|
import gui.mainFrame
|
||||||
from gui.contextMenu import ContextMenuSingle
|
from gui.contextMenu import ContextMenuSingle
|
||||||
from gui.fitCommands import (
|
|
||||||
GuiConvertMutatedLocalModuleCommand, GuiRevertMutatedLocalModuleCommand,
|
|
||||||
GuiConvertMutatedLocalDroneCommand, GuiRevertMutatedLocalDroneCommand)
|
|
||||||
from service.fit import Fit
|
from service.fit import Fit
|
||||||
|
|
||||||
_t = wx.GetTranslation
|
_t = wx.GetTranslation
|
||||||
@@ -65,6 +62,8 @@ class ChangeItemMutation(ContextMenuSingle):
|
|||||||
return sub
|
return sub
|
||||||
|
|
||||||
def handleMenu(self, event):
|
def handleMenu(self, event):
|
||||||
|
from gui.fitCommands import (
|
||||||
|
GuiConvertMutatedLocalModuleCommand, GuiConvertMutatedLocalDroneCommand)
|
||||||
mutaplasmid, item = self.eventIDs[event.Id]
|
mutaplasmid, item = self.eventIDs[event.Id]
|
||||||
fitID = self.mainFrame.getActiveFit()
|
fitID = self.mainFrame.getActiveFit()
|
||||||
fit = Fit.getInstance().getFit(fitID)
|
fit = Fit.getInstance().getFit(fitID)
|
||||||
@@ -78,6 +77,8 @@ class ChangeItemMutation(ContextMenuSingle):
|
|||||||
fitID=fitID, position=position, mutaplasmid=mutaplasmid))
|
fitID=fitID, position=position, mutaplasmid=mutaplasmid))
|
||||||
|
|
||||||
def activate(self, callingWindow, fullContext, mainItem, i):
|
def activate(self, callingWindow, fullContext, mainItem, i):
|
||||||
|
from gui.fitCommands import (
|
||||||
|
GuiRevertMutatedLocalModuleCommand, GuiRevertMutatedLocalDroneCommand)
|
||||||
fitID = self.mainFrame.getActiveFit()
|
fitID = self.mainFrame.getActiveFit()
|
||||||
fit = Fit.getInstance().getFit(fitID)
|
fit = Fit.getInstance().getFit(fitID)
|
||||||
if mainItem in fit.modules:
|
if mainItem in fit.modules:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from logbook import Logger
|
|||||||
|
|
||||||
import gui.builtinMarketBrowser.pfSearchBox as SBox
|
import gui.builtinMarketBrowser.pfSearchBox as SBox
|
||||||
import gui.globalEvents as GE
|
import gui.globalEvents as GE
|
||||||
from config import slotColourMap, slotColourMapDark
|
from config import get_slotColourMap, get_slotColourMapDark
|
||||||
from eos.saveddata.module import Module
|
from eos.saveddata.module import Module
|
||||||
from eos.const import FittingSlot
|
from eos.const import FittingSlot
|
||||||
from gui.builtinMarketBrowser.events import ItemSelected, RECENTLY_USED_MODULES, CHARGES_FOR_FIT
|
from gui.builtinMarketBrowser.events import ItemSelected, RECENTLY_USED_MODULES, CHARGES_FOR_FIT
|
||||||
@@ -412,7 +412,7 @@ class ItemView(Display):
|
|||||||
|
|
||||||
def columnBackground(self, colItem, item):
|
def columnBackground(self, colItem, item):
|
||||||
if self.sFit.serviceFittingOptions["colorFitBySlot"]:
|
if self.sFit.serviceFittingOptions["colorFitBySlot"]:
|
||||||
colorMap = slotColourMapDark if isDark() else slotColourMap
|
colorMap = get_slotColourMapDark() if isDark() else get_slotColourMap()
|
||||||
return colorMap.get(Module.calculateSlot(item)) or self.GetBackgroundColour()
|
return colorMap.get(Module.calculateSlot(item)) or self.GetBackgroundColour()
|
||||||
else:
|
else:
|
||||||
return self.GetBackgroundColour()
|
return self.GetBackgroundColour()
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ from gui.utils.staticHelpers import DragDropHelper
|
|||||||
from gui.utils.dark import isDark
|
from gui.utils.dark import isDark
|
||||||
from service.fit import Fit
|
from service.fit import Fit
|
||||||
from service.market import Market
|
from service.market import Market
|
||||||
from config import slotColourMap, slotColourMapDark, errColor, errColorDark
|
from config import get_slotColourMap, get_slotColourMapDark, get_errColor, get_errColorDark
|
||||||
from gui.fitCommands.helpers import getSimilarModPositions
|
from gui.fitCommands.helpers import getSimilarModPositions
|
||||||
|
|
||||||
pyfalog = Logger(__name__)
|
pyfalog = Logger(__name__)
|
||||||
@@ -766,9 +766,9 @@ class FittingView(d.Display):
|
|||||||
|
|
||||||
def slotColour(self, slot):
|
def slotColour(self, slot):
|
||||||
if isDark():
|
if isDark():
|
||||||
return slotColourMapDark.get(slot) or self.GetBackgroundColour()
|
return get_slotColourMapDark().get(slot) or self.GetBackgroundColour()
|
||||||
else:
|
else:
|
||||||
return slotColourMap.get(slot) or self.GetBackgroundColour()
|
return get_slotColourMap().get(slot) or self.GetBackgroundColour()
|
||||||
|
|
||||||
def refresh(self, stuff):
|
def refresh(self, stuff):
|
||||||
"""
|
"""
|
||||||
@@ -812,7 +812,7 @@ class FittingView(d.Display):
|
|||||||
hasRestrictionOverriden = not hasRestrictionOverriden
|
hasRestrictionOverriden = not hasRestrictionOverriden
|
||||||
|
|
||||||
if slotMap[mod.slot] or hasRestrictionOverriden: # Color too many modules as red
|
if slotMap[mod.slot] or hasRestrictionOverriden: # Color too many modules as red
|
||||||
self.SetItemBackgroundColour(i, errColorDark if isDark() else errColor)
|
self.SetItemBackgroundColour(i, get_errColorDark() if isDark() else get_errColor())
|
||||||
elif sFit.serviceFittingOptions["colorFitBySlot"]: # Color by slot it enabled
|
elif sFit.serviceFittingOptions["colorFitBySlot"]: # Color by slot it enabled
|
||||||
self.SetItemBackgroundColour(i, self.slotColour(mod.slot))
|
self.SetItemBackgroundColour(i, self.slotColour(mod.slot))
|
||||||
|
|
||||||
|
|||||||
@@ -346,20 +346,7 @@ class CargoInfo:
|
|||||||
return makeReprStr(self, ['itemID', 'amount'])
|
return makeReprStr(self, ['itemID', 'amount'])
|
||||||
|
|
||||||
|
|
||||||
def activeStateLimit(itemIdentity):
|
from service.port.active_state import activeStateLimit
|
||||||
item = Market.getInstance().getItem(itemIdentity)
|
|
||||||
if {
|
|
||||||
'moduleBonusAssaultDamageControl', 'moduleBonusIndustrialInvulnerability',
|
|
||||||
'microJumpDrive', 'microJumpPortalDrive', 'emergencyHullEnergizer',
|
|
||||||
'cynosuralGeneration', 'jumpPortalGeneration', 'jumpPortalGenerationBO',
|
|
||||||
'cloneJumpAccepting', 'cloakingWarpSafe', 'cloakingPrototype', 'cloaking',
|
|
||||||
'massEntanglerEffect5', 'electronicAttributeModifyOnline', 'targetPassively',
|
|
||||||
'cargoScan', 'shipScan', 'surveyScan', 'targetSpectrumBreakerBonus',
|
|
||||||
'interdictionNullifierBonus', 'warpCoreStabilizerActive',
|
|
||||||
'industrialItemCompression'
|
|
||||||
}.intersection(item.effects):
|
|
||||||
return FittingModuleState.ONLINE
|
|
||||||
return FittingModuleState.ACTIVE
|
|
||||||
|
|
||||||
|
|
||||||
def droneStackLimit(fit, itemIdentity):
|
def droneStackLimit(fit, itemIdentity):
|
||||||
|
|||||||
37
pyfa.spec
37
pyfa.spec
@@ -79,8 +79,20 @@ a = Analysis(['pyfa.py'],
|
|||||||
win_private_assemblies=False,
|
win_private_assemblies=False,
|
||||||
cipher=block_cipher)
|
cipher=block_cipher)
|
||||||
|
|
||||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
a_server = Analysis(['pyfa_server.py'],
|
||||||
|
pathex=pathex,
|
||||||
|
binaries=[],
|
||||||
|
datas=added_files,
|
||||||
|
hiddenimports=import_these,
|
||||||
|
hookspath=['dist_assets/pyinstaller_hooks'],
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=['Tkinter'],
|
||||||
|
win_no_prefer_redirects=False,
|
||||||
|
win_private_assemblies=False,
|
||||||
|
cipher=block_cipher)
|
||||||
|
|
||||||
|
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||||
|
pyz_server = PYZ(a_server.pure, a_server.zipped_data, cipher=block_cipher)
|
||||||
|
|
||||||
exe = EXE(
|
exe = EXE(
|
||||||
pyz,
|
pyz,
|
||||||
@@ -96,6 +108,19 @@ exe = EXE(
|
|||||||
contents_directory='app',
|
contents_directory='app',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Sim server. POST /simulate on port 9123.
|
||||||
|
exe_server = EXE(
|
||||||
|
pyz_server,
|
||||||
|
a_server.scripts,
|
||||||
|
exclude_binaries=True,
|
||||||
|
name='pyfa-server',
|
||||||
|
debug=debug,
|
||||||
|
strip=False,
|
||||||
|
upx=upx,
|
||||||
|
console=True,
|
||||||
|
contents_directory='app',
|
||||||
|
)
|
||||||
|
|
||||||
coll = COLLECT(
|
coll = COLLECT(
|
||||||
exe,
|
exe,
|
||||||
a.binaries,
|
a.binaries,
|
||||||
@@ -106,6 +131,16 @@ coll = COLLECT(
|
|||||||
name='pyfa',
|
name='pyfa',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
coll_server = COLLECT(
|
||||||
|
exe_server,
|
||||||
|
a_server.binaries,
|
||||||
|
a_server.zipfiles,
|
||||||
|
a_server.datas,
|
||||||
|
strip=False,
|
||||||
|
upx=upx,
|
||||||
|
name='pyfa_server',
|
||||||
|
)
|
||||||
|
|
||||||
if platform.system() == 'Darwin':
|
if platform.system() == 'Darwin':
|
||||||
info_plist = {
|
info_plist = {
|
||||||
'NSHighResolutionCapable': 'True',
|
'NSHighResolutionCapable': 'True',
|
||||||
|
|||||||
5
pyfa_server.py
Normal file
5
pyfa_server.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Sim server. POST /simulate with JSON body.
|
||||||
|
from scripts.pyfa_sim import _run_http_server
|
||||||
|
|
||||||
|
_run_http_server(9123, None)
|
||||||
@@ -22,3 +22,6 @@ dependencies = [
|
|||||||
"sqlalchemy==1.4.50",
|
"sqlalchemy==1.4.50",
|
||||||
"wxpython==4.2.1",
|
"wxpython==4.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = ["pytest"]
|
||||||
|
|||||||
28
release.sh
28
release.sh
@@ -57,4 +57,32 @@ curl -X POST \
|
|||||||
|
|
||||||
rm "${ZIP}"
|
rm "${ZIP}"
|
||||||
|
|
||||||
|
# Push Docker image
|
||||||
|
DOCKER_REPO="${DOCKER_REPO:-docker.site.quack-lab.dev}"
|
||||||
|
IMAGE_NAME="${IMAGE_NAME:-pyfa-server}"
|
||||||
|
COMMIT_SHA=$(git rev-parse --short HEAD)
|
||||||
|
IMAGE_BASE="${DOCKER_REPO}/${IMAGE_NAME}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Pushing Docker images..."
|
||||||
|
docker push "${IMAGE_BASE}:${COMMIT_SHA}"
|
||||||
|
docker push "${IMAGE_BASE}:latest"
|
||||||
|
TAGS=$(git tag --points-at HEAD 2>/dev/null || true)
|
||||||
|
if [ -n "$TAGS" ]; then
|
||||||
|
while IFS= read -r tag; do
|
||||||
|
[ -n "$tag" ] && docker push "${IMAGE_BASE}:${tag}"
|
||||||
|
done <<< "$TAGS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Docker image pushed as:"
|
||||||
|
echo " - ${IMAGE_BASE}:${COMMIT_SHA}"
|
||||||
|
echo " - ${IMAGE_BASE}:latest"
|
||||||
|
if [ -n "$TAGS" ]; then
|
||||||
|
while IFS= read -r tag; do
|
||||||
|
[ -n "$tag" ] && echo " - ${IMAGE_BASE}:${tag}"
|
||||||
|
done <<< "$TAGS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
echo "Release complete! ${ZIP} uploaded to ${TAG}"
|
echo "Release complete! ${ZIP} uploaded to ${TAG}"
|
||||||
|
|||||||
14
requirements-server.txt
Normal file
14
requirements-server.txt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
logbook==1.7.0.post0
|
||||||
|
numpy==1.26.2
|
||||||
|
matplotlib==3.8.2
|
||||||
|
python-dateutil==2.8.2
|
||||||
|
requests==2.31.0
|
||||||
|
sqlalchemy==1.4.50
|
||||||
|
cryptography==42.0.4
|
||||||
|
markdown2==2.4.11
|
||||||
|
packaging==23.2
|
||||||
|
roman==4.1
|
||||||
|
beautifulsoup4==4.12.2
|
||||||
|
pyyaml==6.0.1
|
||||||
|
python-jose==3.3.0
|
||||||
|
requests-cache==1.1.1
|
||||||
449
scripts/pyfa_sim.py
Normal file
449
scripts/pyfa_sim.py
Normal file
@@ -0,0 +1,449 @@
|
|||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
|
||||||
|
import config
|
||||||
|
import eos.config
|
||||||
|
|
||||||
|
from eos.const import FittingHardpoint
|
||||||
|
from eos.utils.spoolSupport import SpoolOptions, SpoolType
|
||||||
|
|
||||||
|
# POST /simulate request and response shape; included in 400 response body
|
||||||
|
SIMULATE_SCHEMA = {
|
||||||
|
"request": {
|
||||||
|
"fit": "string (required). EFT or multi-line text export of a single fit, e.g. [ShipName, FitName] followed by modules/drones/etc.",
|
||||||
|
"projected_fits": "array (optional). Each element: { \"fit\": string, \"count\": positive integer }.",
|
||||||
|
"command_fits": "array (optional). Each element: { \"fit\": string }.",
|
||||||
|
},
|
||||||
|
"response_200": {
|
||||||
|
"fit": {"id": "int", "name": "string", "ship_type": "string"},
|
||||||
|
"resources": {"hardpoints": {}, "drones": {}, "fighters": {}, "calibration": {}, "powergrid": {}, "cpu": {}, "cargo": {}},
|
||||||
|
"defense": {"hp": {}, "ehp": {}, "resonance": {}, "tank": {}, "effective_tank": {}, "sustainable_tank": {}, "effective_sustainable_tank": {}},
|
||||||
|
"capacitor": {"capacity": {}, "recharge": {}, "use": {}, "delta": {}, "stable": {}, "state": {}, "neutralizer_resistance": {}},
|
||||||
|
"firepower": {"weapon_dps": {}, "drone_dps": {}, "total_dps": {}, "weapon_volley": {}, "drone_volley": {}, "total_volley": {}, "weapons": []},
|
||||||
|
"remote_reps_outgoing": {"current": {}, "pre_spool": {}, "full_spool": {}},
|
||||||
|
"targeting_misc": {"targets_max": {}, "target_range": {}, "scan_resolution": {}, "scan_strength": {}, "scan_type": {}, "jam_chance": {}, "drone_control_range": {}, "speed": {}, "align_time": {}, "signature_radius": {}, "warp_speed": {}, "max_warp_distance": {}, "probe_size": {}, "cargo_capacity": {}, "cargo_used": {}},
|
||||||
|
"price": {"ship": {}, "fittings": {}, "drones_and_fighters": {}, "cargo": {}, "character": {}, "total": {}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _init_pyfa(savepath: str | None) -> None:
|
||||||
|
config.debug = False
|
||||||
|
config.loggingLevel = config.LOGLEVEL_MAP["error"]
|
||||||
|
config.defPaths(savepath)
|
||||||
|
config.defLogging()
|
||||||
|
import eos.db # noqa: F401
|
||||||
|
eos.db.saveddata_meta.create_all() # type: ignore[name-defined]
|
||||||
|
import eos.events # noqa: F401
|
||||||
|
from service import prefetch # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_drone_markers(text: str) -> tuple[str, set[str]]:
|
||||||
|
active_names: set[str] = set()
|
||||||
|
cleaned_lines: list[str] = []
|
||||||
|
for raw_line in text.splitlines():
|
||||||
|
line = raw_line.rstrip()
|
||||||
|
if " xx" in line:
|
||||||
|
idx = line.find(" xx")
|
||||||
|
marker_segment = line[idx + 1 :].strip()
|
||||||
|
|
||||||
|
if marker_segment == "xx":
|
||||||
|
payload = line[:idx].rstrip()
|
||||||
|
parts = payload.split(" x", 1)
|
||||||
|
if len(parts) == 2:
|
||||||
|
type_name = parts[0].strip()
|
||||||
|
if type_name:
|
||||||
|
active_names.add(type_name)
|
||||||
|
line = payload
|
||||||
|
|
||||||
|
cleaned_lines.append(line)
|
||||||
|
|
||||||
|
return "\n".join(cleaned_lines), active_names
|
||||||
|
|
||||||
|
|
||||||
|
def _import_single_fit(raw_text: str) -> tuple[object, set[str]]:
|
||||||
|
from service.port.port import Port
|
||||||
|
|
||||||
|
cleaned_text, active_names = _parse_drone_markers(raw_text)
|
||||||
|
import_type, import_data = Port.importFitFromBuffer(cleaned_text)
|
||||||
|
|
||||||
|
if not import_data or len(import_data) != 1:
|
||||||
|
raise ValueError("Expected exactly one fit in input; got %d" % (len(import_data) if import_data else 0))
|
||||||
|
|
||||||
|
fit = import_data[0]
|
||||||
|
|
||||||
|
if active_names:
|
||||||
|
for drone in fit.drones:
|
||||||
|
if getattr(drone.item, "typeName", None) in active_names:
|
||||||
|
drone.amountActive = drone.amount
|
||||||
|
for fighter in fit.fighters:
|
||||||
|
if getattr(fighter.item, "typeName", None) in active_names:
|
||||||
|
fighter.amount = fighter.amount
|
||||||
|
|
||||||
|
return fit, active_names
|
||||||
|
|
||||||
|
|
||||||
|
def _add_projected_fit(s_fit, target_fit, projected_fit, amount: int) -> None:
|
||||||
|
fit = s_fit.getFit(target_fit.ID)
|
||||||
|
projected = s_fit.getFit(projected_fit.ID, projected=True)
|
||||||
|
if projected is None:
|
||||||
|
raise ValueError("Projected fit %s is not available" % projected_fit.ID)
|
||||||
|
|
||||||
|
if projected in fit.projectedFits and projected.ID in fit.projectedFitDict:
|
||||||
|
projection_info = projected.getProjectionInfo(fit.ID)
|
||||||
|
if projection_info is None:
|
||||||
|
raise ValueError("Projection info missing for projected fit %s" % projected_fit.ID)
|
||||||
|
else:
|
||||||
|
fit.projectedFitDict[projected.ID] = projected
|
||||||
|
eos.db.saveddata_session.flush()
|
||||||
|
eos.db.saveddata_session.refresh(projected)
|
||||||
|
projection_info = projected.getProjectionInfo(fit.ID)
|
||||||
|
if projection_info is None:
|
||||||
|
raise ValueError("Projection info missing after linking projected fit %s" % projected_fit.ID)
|
||||||
|
|
||||||
|
projection_info.amount = amount
|
||||||
|
projection_info.active = True
|
||||||
|
|
||||||
|
|
||||||
|
def _add_command_fit(s_fit, target_fit, command_fit) -> None:
|
||||||
|
fit = s_fit.getFit(target_fit.ID)
|
||||||
|
command = s_fit.getFit(command_fit.ID)
|
||||||
|
if command is None:
|
||||||
|
raise ValueError("Command fit %s is not available" % command_fit.ID)
|
||||||
|
|
||||||
|
if command in fit.commandFits or command.ID in fit.commandFitDict:
|
||||||
|
return
|
||||||
|
|
||||||
|
fit.commandFitDict[command.ID] = command
|
||||||
|
eos.db.saveddata_session.flush()
|
||||||
|
eos.db.saveddata_session.refresh(command)
|
||||||
|
info = command.getCommandInfo(fit.ID)
|
||||||
|
if info is None:
|
||||||
|
raise ValueError("Command info missing for command fit %s" % command_fit.ID)
|
||||||
|
info.active = True
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_resources(fit) -> dict:
|
||||||
|
ship = fit.ship
|
||||||
|
resources: dict = {}
|
||||||
|
|
||||||
|
resources["hardpoints"] = {
|
||||||
|
"turret": {
|
||||||
|
"used": fit.getHardpointsUsed(FittingHardpoint.TURRET),
|
||||||
|
"total": ship.getModifiedItemAttr("turretSlotsLeft"),
|
||||||
|
},
|
||||||
|
"launcher": {
|
||||||
|
"used": fit.getHardpointsUsed(FittingHardpoint.MISSILE),
|
||||||
|
"total": ship.getModifiedItemAttr("launcherSlotsLeft"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resources["drones"] = {
|
||||||
|
"active": fit.activeDrones,
|
||||||
|
"max_active": fit.extraAttributes.get("maxActiveDrones"),
|
||||||
|
"bay_used": fit.droneBayUsed,
|
||||||
|
"bay_capacity": ship.getModifiedItemAttr("droneCapacity"),
|
||||||
|
"bandwidth_used": fit.droneBandwidthUsed,
|
||||||
|
"bandwidth_capacity": ship.getModifiedItemAttr("droneBandwidth"),
|
||||||
|
}
|
||||||
|
|
||||||
|
resources["fighters"] = {
|
||||||
|
"tubes_used": fit.fighterTubesUsed,
|
||||||
|
"tubes_total": fit.fighterTubesTotal,
|
||||||
|
"bay_used": fit.fighterBayUsed,
|
||||||
|
"bay_capacity": ship.getModifiedItemAttr("fighterCapacity"),
|
||||||
|
}
|
||||||
|
|
||||||
|
resources["calibration"] = {
|
||||||
|
"used": fit.calibrationUsed,
|
||||||
|
"total": ship.getModifiedItemAttr("upgradeCapacity"),
|
||||||
|
}
|
||||||
|
|
||||||
|
resources["powergrid"] = {
|
||||||
|
"used": fit.pgUsed,
|
||||||
|
"output": ship.getModifiedItemAttr("powerOutput"),
|
||||||
|
}
|
||||||
|
|
||||||
|
resources["cpu"] = {
|
||||||
|
"used": fit.cpuUsed,
|
||||||
|
"output": ship.getModifiedItemAttr("cpuOutput"),
|
||||||
|
}
|
||||||
|
|
||||||
|
resources["cargo"] = {
|
||||||
|
"used": fit.cargoBayUsed,
|
||||||
|
"capacity": ship.getModifiedItemAttr("capacity"),
|
||||||
|
}
|
||||||
|
|
||||||
|
return resources
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_defense(fit) -> dict:
|
||||||
|
ship = fit.ship
|
||||||
|
|
||||||
|
def _res(attr_name):
|
||||||
|
return ship.getModifiedItemAttr(attr_name)
|
||||||
|
|
||||||
|
resonance = {
|
||||||
|
"armor": {"em": _res("armorEmDamageResonance"), "exp": _res("armorExplosiveDamageResonance"), "kin": _res("armorKineticDamageResonance"), "therm": _res("armorThermalDamageResonance")},
|
||||||
|
"shield": {"em": _res("shieldEmDamageResonance"), "exp": _res("shieldExplosiveDamageResonance"), "kin": _res("shieldKineticDamageResonance"), "therm": _res("shieldThermalDamageResonance")},
|
||||||
|
"hull": {"em": _res("emDamageResonance"), "exp": _res("explosiveDamageResonance"), "kin": _res("kineticDamageResonance"), "therm": _res("thermalDamageResonance")},
|
||||||
|
}
|
||||||
|
defense: dict = {}
|
||||||
|
defense["hp"] = fit.hp
|
||||||
|
defense["ehp"] = fit.ehp
|
||||||
|
defense["resonance"] = resonance
|
||||||
|
defense["tank"] = fit.tank
|
||||||
|
defense["effective_tank"] = fit.effectiveTank
|
||||||
|
defense["sustainable_tank"] = fit.sustainableTank
|
||||||
|
defense["effective_sustainable_tank"] = fit.effectiveSustainableTank
|
||||||
|
return defense
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_capacitor(fit) -> dict:
|
||||||
|
ship = fit.ship
|
||||||
|
cap: dict = {}
|
||||||
|
cap["capacity"] = ship.getModifiedItemAttr("capacitorCapacity")
|
||||||
|
cap["recharge"] = fit.capRecharge
|
||||||
|
cap["use"] = fit.capUsed
|
||||||
|
cap["delta"] = fit.capDelta
|
||||||
|
cap["stable"] = fit.capStable
|
||||||
|
cap["state"] = fit.capState
|
||||||
|
cap["neutralizer_resistance"] = ship.getModifiedItemAttr("energyWarfareResistance", 1)
|
||||||
|
return cap
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_firepower(fit) -> dict:
|
||||||
|
default_spool = eos.config.settings["globalDefaultSpoolupPercentage"]
|
||||||
|
spool = SpoolOptions(SpoolType.SPOOL_SCALE, default_spool, False)
|
||||||
|
wdps = fit.getWeaponDps(spoolOptions=spool)
|
||||||
|
ddps = fit.getDroneDps()
|
||||||
|
wvol = fit.getWeaponVolley(spoolOptions=spool)
|
||||||
|
dvol = fit.getDroneVolley()
|
||||||
|
return {
|
||||||
|
"weapon_dps": wdps.total,
|
||||||
|
"drone_dps": ddps.total,
|
||||||
|
"total_dps": fit.getTotalDps(spoolOptions=spool).total,
|
||||||
|
"weapon_volley": wvol.total,
|
||||||
|
"drone_volley": dvol.total,
|
||||||
|
"total_volley": fit.getTotalVolley(spoolOptions=spool).total,
|
||||||
|
"weapons": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_remote_reps(fit) -> dict:
|
||||||
|
default_spool = eos.config.settings["globalDefaultSpoolupPercentage"]
|
||||||
|
pre = fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 0, True))
|
||||||
|
full = fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 1, True))
|
||||||
|
current = fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, default_spool, False))
|
||||||
|
return {
|
||||||
|
"current": {
|
||||||
|
"capacitor": current.capacitor,
|
||||||
|
"shield": current.shield,
|
||||||
|
"armor": current.armor,
|
||||||
|
"hull": current.hull,
|
||||||
|
},
|
||||||
|
"pre_spool": {
|
||||||
|
"capacitor": pre.capacitor,
|
||||||
|
"shield": pre.shield,
|
||||||
|
"armor": pre.armor,
|
||||||
|
"hull": pre.hull,
|
||||||
|
},
|
||||||
|
"full_spool": {
|
||||||
|
"capacitor": full.capacitor,
|
||||||
|
"shield": full.shield,
|
||||||
|
"armor": full.armor,
|
||||||
|
"hull": full.hull,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_targeting_misc(fit) -> dict:
|
||||||
|
ship = fit.ship
|
||||||
|
misc: dict = {}
|
||||||
|
misc["targets_max"] = fit.maxTargets
|
||||||
|
misc["target_range"] = fit.maxTargetRange
|
||||||
|
misc["scan_resolution"] = ship.getModifiedItemAttr("scanResolution")
|
||||||
|
misc["scan_strength"] = fit.scanStrength
|
||||||
|
misc["scan_type"] = fit.scanType
|
||||||
|
misc["jam_chance"] = fit.jamChance
|
||||||
|
misc["drone_control_range"] = fit.extraAttributes.get("droneControlRange")
|
||||||
|
misc["speed"] = fit.maxSpeed
|
||||||
|
misc["align_time"] = fit.alignTime
|
||||||
|
misc["signature_radius"] = ship.getModifiedItemAttr("signatureRadius")
|
||||||
|
misc["warp_speed"] = fit.warpSpeed
|
||||||
|
misc["max_warp_distance"] = fit.maxWarpDistance
|
||||||
|
misc["probe_size"] = fit.probeSize
|
||||||
|
misc["cargo_capacity"] = ship.getModifiedItemAttr("capacity")
|
||||||
|
misc["cargo_used"] = fit.cargoBayUsed
|
||||||
|
return misc
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_price(fit) -> dict:
|
||||||
|
ship_price = 0.0
|
||||||
|
module_price = 0.0
|
||||||
|
drone_price = 0.0
|
||||||
|
fighter_price = 0.0
|
||||||
|
cargo_price = 0.0
|
||||||
|
booster_price = 0.0
|
||||||
|
implant_price = 0.0
|
||||||
|
|
||||||
|
if fit:
|
||||||
|
ship_price = getattr(fit.ship.item.price, "price", 0.0)
|
||||||
|
|
||||||
|
for module in fit.modules:
|
||||||
|
if not module.isEmpty:
|
||||||
|
module_price += getattr(module.item.price, "price", 0.0)
|
||||||
|
|
||||||
|
for drone in fit.drones:
|
||||||
|
drone_price += getattr(drone.item.price, "price", 0.0) * drone.amount
|
||||||
|
|
||||||
|
for fighter in fit.fighters:
|
||||||
|
fighter_price += getattr(fighter.item.price, "price", 0.0) * fighter.amount
|
||||||
|
|
||||||
|
for cargo in fit.cargo:
|
||||||
|
cargo_price += getattr(cargo.item.price, "price", 0.0) * cargo.amount
|
||||||
|
|
||||||
|
for booster in fit.boosters:
|
||||||
|
booster_price += getattr(booster.item.price, "price", 0.0)
|
||||||
|
|
||||||
|
for implant in fit.appliedImplants:
|
||||||
|
implant_price += getattr(implant.item.price, "price", 0.0)
|
||||||
|
|
||||||
|
total_price = ship_price + module_price + drone_price + fighter_price + cargo_price + booster_price + implant_price
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ship": ship_price,
|
||||||
|
"fittings": module_price,
|
||||||
|
"drones_and_fighters": drone_price + fighter_price,
|
||||||
|
"cargo": cargo_price,
|
||||||
|
"character": booster_price + implant_price,
|
||||||
|
"total": total_price,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_output(main_fit) -> dict:
|
||||||
|
return {
|
||||||
|
"fit": {
|
||||||
|
"id": main_fit.ID,
|
||||||
|
"name": main_fit.name,
|
||||||
|
"ship_type": main_fit.ship.item.typeName,
|
||||||
|
},
|
||||||
|
"resources": _collect_resources(main_fit),
|
||||||
|
"defense": _collect_defense(main_fit),
|
||||||
|
"capacitor": _collect_capacitor(main_fit),
|
||||||
|
"firepower": _collect_firepower(main_fit),
|
||||||
|
"remote_reps_outgoing": _collect_remote_reps(main_fit),
|
||||||
|
"targeting_misc": _collect_targeting_misc(main_fit),
|
||||||
|
"price": _collect_price(main_fit),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def compute_stats(payload: dict, savepath: str | None = None) -> dict:
|
||||||
|
if "fit" not in payload or not isinstance(payload["fit"], str):
|
||||||
|
raise ValueError("Payload must contain a 'fit' field with EFT/text export")
|
||||||
|
_init_pyfa(savepath)
|
||||||
|
from service.fit import Fit as FitService
|
||||||
|
s_fit = FitService.getInstance()
|
||||||
|
if s_fit.character is None:
|
||||||
|
from eos.saveddata.character import Character as saveddata_Character
|
||||||
|
s_fit.character = saveddata_Character.getAll5()
|
||||||
|
main_fit, _ = _import_single_fit(payload["fit"])
|
||||||
|
projected_defs = payload.get("projected_fits", [])
|
||||||
|
command_defs = payload.get("command_fits", [])
|
||||||
|
projected_fits: list[tuple[object, int]] = []
|
||||||
|
for entry in projected_defs:
|
||||||
|
if not isinstance(entry, dict):
|
||||||
|
raise ValueError("Each projected_fits entry must be an object")
|
||||||
|
fit_text = entry.get("fit")
|
||||||
|
count = entry.get("count")
|
||||||
|
if not isinstance(fit_text, str):
|
||||||
|
raise ValueError("Each projected_fits entry must contain a string 'fit'")
|
||||||
|
if not isinstance(count, int) or count <= 0:
|
||||||
|
raise ValueError("Each projected_fits entry must contain a positive integer 'count'")
|
||||||
|
pf, _ = _import_single_fit(fit_text)
|
||||||
|
projected_fits.append((pf, count))
|
||||||
|
command_fits: list[object] = []
|
||||||
|
for entry in command_defs:
|
||||||
|
if not isinstance(entry, dict):
|
||||||
|
raise ValueError("Each command_fits entry must be an object")
|
||||||
|
fit_text = entry.get("fit")
|
||||||
|
if not isinstance(fit_text, str):
|
||||||
|
raise ValueError("Each command_fits entry must contain a string 'fit'")
|
||||||
|
cf, _ = _import_single_fit(fit_text)
|
||||||
|
command_fits.append(cf)
|
||||||
|
for pf, count in projected_fits:
|
||||||
|
_add_projected_fit(s_fit, main_fit, pf, count)
|
||||||
|
for cf in command_fits:
|
||||||
|
_add_command_fit(s_fit, main_fit, cf)
|
||||||
|
s_fit.recalc(main_fit)
|
||||||
|
s_fit.fill(main_fit)
|
||||||
|
return _build_output(main_fit)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_http_server(port: int, savepath: str | None) -> None:
|
||||||
|
def send_error_json(code: int, msg: str, traceback_str: str | None = None):
|
||||||
|
body = {"error": msg, "schema": SIMULATE_SCHEMA}
|
||||||
|
if traceback_str:
|
||||||
|
body["traceback"] = traceback_str
|
||||||
|
return json.dumps(body, indent=2) + "\n"
|
||||||
|
|
||||||
|
class SimulateHandler(BaseHTTPRequestHandler):
|
||||||
|
def send_error(self, code: int, message: str | None = None, explain: str | None = None):
|
||||||
|
msg = message or explain or "Error"
|
||||||
|
self.send_response(code)
|
||||||
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(send_error_json(code, msg).encode("utf-8"))
|
||||||
|
|
||||||
|
def _reply_error(self, code: int, msg: str, traceback_str: str | None = None):
|
||||||
|
self.send_response(code)
|
||||||
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(send_error_json(code, msg, traceback_str).encode("utf-8"))
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
if self.path == "/simulate":
|
||||||
|
self._reply_error(405, "Method not allowed. Use POST.")
|
||||||
|
else:
|
||||||
|
self._reply_error(404, "Not found. POST /simulate with JSON body.")
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
if self.path != "/simulate":
|
||||||
|
self._reply_error(404, "Not found. POST /simulate with JSON body.")
|
||||||
|
return
|
||||||
|
content_length = int(self.headers.get("Content-Length", 0))
|
||||||
|
body = self.rfile.read(content_length).decode("utf-8")
|
||||||
|
try:
|
||||||
|
payload = json.loads(body)
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
self._reply_error(400, "Invalid JSON: %s" % exc)
|
||||||
|
return
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
self._reply_error(400, "Top-level JSON must be an object")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
output = compute_stats(payload, savepath)
|
||||||
|
except ValueError as e:
|
||||||
|
self._reply_error(400, str(e))
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
tb = traceback.format_exc()
|
||||||
|
sys.stderr.write(tb)
|
||||||
|
sys.stderr.flush()
|
||||||
|
self._reply_error(500, str(e), tb)
|
||||||
|
return
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps(output, indent=2, sort_keys=True).encode("utf-8"))
|
||||||
|
self.wfile.write(b"\n")
|
||||||
|
|
||||||
|
with HTTPServer(("", port), SimulateHandler) as httpd:
|
||||||
|
print("POST /simulate on http://127.0.0.1:%s" % port, file=sys.stderr, flush=True)
|
||||||
|
httpd.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
_run_http_server(9123, None)
|
||||||
@@ -28,12 +28,8 @@ from xml.etree import ElementTree
|
|||||||
from xml.dom import minidom
|
from xml.dom import minidom
|
||||||
import gzip
|
import gzip
|
||||||
|
|
||||||
# noinspection PyPackageRequirements
|
|
||||||
import wx
|
|
||||||
|
|
||||||
import config
|
import config
|
||||||
import eos.db
|
import eos.db
|
||||||
from service.esi import Esi
|
|
||||||
|
|
||||||
from eos.saveddata.implant import Implant as es_Implant
|
from eos.saveddata.implant import Implant as es_Implant
|
||||||
from eos.saveddata.character import Character as es_Character, Skill
|
from eos.saveddata.character import Character as es_Character, Skill
|
||||||
@@ -42,7 +38,12 @@ from eos.const import FittingSlot as es_Slot
|
|||||||
from eos.saveddata.fighter import Fighter as es_Fighter
|
from eos.saveddata.fighter import Fighter as es_Fighter
|
||||||
|
|
||||||
pyfalog = Logger(__name__)
|
pyfalog = Logger(__name__)
|
||||||
_t = wx.GetTranslation
|
|
||||||
|
|
||||||
|
def _t(s):
|
||||||
|
import wx
|
||||||
|
return wx.GetTranslation(s)
|
||||||
|
|
||||||
|
|
||||||
class CharacterImportThread(threading.Thread):
|
class CharacterImportThread(threading.Thread):
|
||||||
|
|
||||||
@@ -97,6 +98,7 @@ class CharacterImportThread(threading.Thread):
|
|||||||
pyfalog.error(e)
|
pyfalog.error(e)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
import wx
|
||||||
wx.CallAfter(self.callback)
|
wx.CallAfter(self.callback)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
@@ -132,6 +134,7 @@ class SkillBackupThread(threading.Thread):
|
|||||||
with open(path, mode='w', encoding='utf-8') as backupFile:
|
with open(path, mode='w', encoding='utf-8') as backupFile:
|
||||||
backupFile.write(backupData)
|
backupFile.write(backupData)
|
||||||
|
|
||||||
|
import wx
|
||||||
wx.CallAfter(self.callback)
|
wx.CallAfter(self.callback)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
@@ -386,6 +389,7 @@ class Character:
|
|||||||
|
|
||||||
def apiFetchCallback(self, guiCallback, e=None):
|
def apiFetchCallback(self, guiCallback, e=None):
|
||||||
eos.db.commit()
|
eos.db.commit()
|
||||||
|
import wx
|
||||||
wx.CallAfter(guiCallback, e)
|
wx.CallAfter(guiCallback, e)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -501,6 +505,7 @@ class UpdateAPIThread(threading.Thread):
|
|||||||
try:
|
try:
|
||||||
char = eos.db.getCharacter(self.charID)
|
char = eos.db.getCharacter(self.charID)
|
||||||
|
|
||||||
|
from service.esi import Esi
|
||||||
sEsi = Esi.getInstance()
|
sEsi = Esi.getInstance()
|
||||||
sChar = Character.getInstance()
|
sChar = Character.getInstance()
|
||||||
ssoChar = sChar.getSsoCharacter(char.ID)
|
ssoChar = sChar.getSsoCharacter(char.ID)
|
||||||
|
|||||||
@@ -13,11 +13,9 @@ from service.const import EsiLoginMethod, EsiSsoMode
|
|||||||
from eos.saveddata.ssocharacter import SsoCharacter
|
from eos.saveddata.ssocharacter import SsoCharacter
|
||||||
from service.esiAccess import APIException, GenericSsoError
|
from service.esiAccess import APIException, GenericSsoError
|
||||||
import gui.globalEvents as GE
|
import gui.globalEvents as GE
|
||||||
from gui.ssoLogin import SsoLogin
|
|
||||||
from service.server import StoppableHTTPServer, AuthHandler
|
from service.server import StoppableHTTPServer, AuthHandler
|
||||||
from service.settings import EsiSettings
|
from service.settings import EsiSettings
|
||||||
from service.esiAccess import EsiAccess
|
from service.esiAccess import EsiAccess
|
||||||
import gui.mainFrame
|
|
||||||
|
|
||||||
from requests import Session
|
from requests import Session
|
||||||
|
|
||||||
@@ -140,6 +138,7 @@ class Esi(EsiAccess):
|
|||||||
self.fittings_deleted.add(fittingID)
|
self.fittings_deleted.add(fittingID)
|
||||||
|
|
||||||
def login(self):
|
def login(self):
|
||||||
|
import gui.ssoLogin
|
||||||
start_server = self.settings.get('loginMode') == EsiLoginMethod.SERVER and self.server_base.supports_auto_login
|
start_server = self.settings.get('loginMode') == EsiLoginMethod.SERVER and self.server_base.supports_auto_login
|
||||||
with gui.ssoLogin.SsoLogin(self.server_base, start_server) as dlg:
|
with gui.ssoLogin.SsoLogin(self.server_base, start_server) as dlg:
|
||||||
if dlg.ShowModal() == wx.ID_OK:
|
if dlg.ShowModal() == wx.ID_OK:
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import datetime
|
|||||||
from time import time
|
from time import time
|
||||||
from weakref import WeakSet
|
from weakref import WeakSet
|
||||||
|
|
||||||
import wx
|
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
|
||||||
import eos.db
|
import eos.db
|
||||||
@@ -235,6 +234,7 @@ class Fit:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def getCommandProcessor(cls, fitID):
|
def getCommandProcessor(cls, fitID):
|
||||||
if fitID not in cls.processors:
|
if fitID not in cls.processors:
|
||||||
|
import wx
|
||||||
cls.processors[fitID] = wx.CommandProcessor(maxCommands=100)
|
cls.processors[fitID] = wx.CommandProcessor(maxCommands=100)
|
||||||
return cls.processors[fitID]
|
return cls.processors[fitID]
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ import threading
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
# noinspection PyPackageRequirements
|
|
||||||
import wx
|
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
from sqlalchemy.sql import or_
|
from sqlalchemy.sql import or_
|
||||||
|
|
||||||
@@ -38,7 +36,12 @@ from service.settings import SettingsProvider
|
|||||||
from utils.cjk import isStringCjk
|
from utils.cjk import isStringCjk
|
||||||
|
|
||||||
pyfalog = Logger(__name__)
|
pyfalog = Logger(__name__)
|
||||||
_t = wx.GetTranslation
|
|
||||||
|
|
||||||
|
def _t(s):
|
||||||
|
import wx
|
||||||
|
return wx.GetTranslation(s)
|
||||||
|
|
||||||
|
|
||||||
# Event which tells threads dependent on Market that it's initialized
|
# Event which tells threads dependent on Market that it's initialized
|
||||||
mktRdy = threading.Event()
|
mktRdy = threading.Event()
|
||||||
@@ -77,6 +80,7 @@ class ShipBrowserWorkerThread(threading.Thread):
|
|||||||
set_ = sMkt.getShipList(id_)
|
set_ = sMkt.getShipList(id_)
|
||||||
cache[id_] = set_
|
cache[id_] = set_
|
||||||
|
|
||||||
|
import wx
|
||||||
wx.CallAfter(callback, (id_, set_))
|
wx.CallAfter(callback, (id_, set_))
|
||||||
except (KeyboardInterrupt, SystemExit):
|
except (KeyboardInterrupt, SystemExit):
|
||||||
raise
|
raise
|
||||||
@@ -170,6 +174,7 @@ class SearchWorkerThread(threading.Thread):
|
|||||||
for item in all_results:
|
for item in all_results:
|
||||||
if sMkt.getPublicityByItem(item):
|
if sMkt.getPublicityByItem(item):
|
||||||
item_IDs.add(item.ID)
|
item_IDs.add(item.ID)
|
||||||
|
import wx
|
||||||
wx.CallAfter(callback, sorted(item_IDs))
|
wx.CallAfter(callback, sorted(item_IDs))
|
||||||
|
|
||||||
def scheduleSearch(self, text, callback, filterName=None):
|
def scheduleSearch(self, text, callback, filterName=None):
|
||||||
@@ -268,7 +273,7 @@ class Market:
|
|||||||
self.les_grp = types_Group()
|
self.les_grp = types_Group()
|
||||||
self.les_grp.ID = -1
|
self.les_grp.ID = -1
|
||||||
self.les_grp.name = "Limited Issue Ships"
|
self.les_grp.name = "Limited Issue Ships"
|
||||||
self.les_grp.displayName = _t("Limited Issue Ships")
|
self.les_grp.displayName = "Limited Issue Ships"
|
||||||
self.les_grp.published = True
|
self.les_grp.published = True
|
||||||
ships = self.getCategory("Ship")
|
ships = self.getCategory("Ship")
|
||||||
self.les_grp.category = ships
|
self.les_grp.category = ships
|
||||||
|
|||||||
@@ -1,2 +1,8 @@
|
|||||||
from .efs import EfsPort
|
from .efs import EfsPort
|
||||||
from .port import Port
|
|
||||||
|
|
||||||
|
def __getattr__(name):
|
||||||
|
if name == "Port":
|
||||||
|
from service.port.port import Port
|
||||||
|
return Port
|
||||||
|
raise AttributeError("module %r has no attribute %r" % (__name__, name))
|
||||||
|
|||||||
12
service/port/active_state.py
Normal file
12
service/port/active_state.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Module-state logic shared by port (EFT import) and gui (fit commands).
|
||||||
|
# Uses gamedata (item type + attributes), no hardcoded effect names.
|
||||||
|
|
||||||
|
from eos.const import FittingModuleState
|
||||||
|
from service.market import Market
|
||||||
|
|
||||||
|
|
||||||
|
def activeStateLimit(itemIdentity):
|
||||||
|
item = Market.getInstance().getItem(itemIdentity)
|
||||||
|
if not item.isType("active") or item.getAttribute("activationBlocked", 0) > 0:
|
||||||
|
return FittingModuleState.ONLINE
|
||||||
|
return FittingModuleState.ACTIVE
|
||||||
@@ -31,7 +31,6 @@ from eos.saveddata.fighter import Fighter
|
|||||||
from eos.saveddata.fit import Fit
|
from eos.saveddata.fit import Fit
|
||||||
from eos.saveddata.module import Module
|
from eos.saveddata.module import Module
|
||||||
from eos.saveddata.ship import Ship
|
from eos.saveddata.ship import Ship
|
||||||
from gui.fitCommands.helpers import activeStateLimit
|
|
||||||
from service.const import PortDnaOptions
|
from service.const import PortDnaOptions
|
||||||
from service.fit import Fit as svcFit
|
from service.fit import Fit as svcFit
|
||||||
from service.market import Market
|
from service.market import Market
|
||||||
@@ -80,6 +79,7 @@ def importDnaAlt(string, fitName=None):
|
|||||||
return processImportInfo(info, fitName, "*")
|
return processImportInfo(info, fitName, "*")
|
||||||
|
|
||||||
def processImportInfo(info, fitName, amountSeparator):
|
def processImportInfo(info, fitName, amountSeparator):
|
||||||
|
from gui.fitCommands.helpers import activeStateLimit
|
||||||
sMkt = Market.getInstance()
|
sMkt = Market.getInstance()
|
||||||
f = Fit()
|
f = Fit()
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -16,10 +16,6 @@ from eos.effectHandlerHelpers import HandledList
|
|||||||
from eos.db import gamedata_session, getCategory, getAttributeInfo, getGroup
|
from eos.db import gamedata_session, getCategory, getAttributeInfo, getGroup
|
||||||
from eos.gamedata import Attribute, Effect, Group, Item, ItemEffect
|
from eos.gamedata import Attribute, Effect, Group, Item, ItemEffect
|
||||||
from eos.utils.spoolSupport import SpoolType, SpoolOptions
|
from eos.utils.spoolSupport import SpoolType, SpoolOptions
|
||||||
from gui.fitCommands.calc.module.localAdd import CalcAddLocalModuleCommand
|
|
||||||
from gui.fitCommands.calc.module.localRemove import CalcRemoveLocalModulesCommand
|
|
||||||
from gui.fitCommands.calc.module.changeCharges import CalcChangeModuleChargesCommand
|
|
||||||
from gui.fitCommands.helpers import ModuleInfo
|
|
||||||
|
|
||||||
|
|
||||||
pyfalog = Logger(__name__)
|
pyfalog = Logger(__name__)
|
||||||
@@ -68,6 +64,9 @@ class EfsPort:
|
|||||||
|
|
||||||
if propID is None:
|
if propID is None:
|
||||||
return None
|
return None
|
||||||
|
from gui.fitCommands.calc.module.localAdd import CalcAddLocalModuleCommand
|
||||||
|
from gui.fitCommands.calc.module.localRemove import CalcRemoveLocalModulesCommand
|
||||||
|
from gui.fitCommands.helpers import ModuleInfo
|
||||||
cmd = CalcAddLocalModuleCommand(fitID, ModuleInfo(itemID=propID))
|
cmd = CalcAddLocalModuleCommand(fitID, ModuleInfo(itemID=propID))
|
||||||
cmd.Do()
|
cmd.Do()
|
||||||
if cmd.needsGuiRecalc:
|
if cmd.needsGuiRecalc:
|
||||||
@@ -137,6 +136,7 @@ class EfsPort:
|
|||||||
EfsPort.attrDirectMap(["reloadTime"], stats, mod)
|
EfsPort.attrDirectMap(["reloadTime"], stats, mod)
|
||||||
c = mod.charge
|
c = mod.charge
|
||||||
if c:
|
if c:
|
||||||
|
from gui.fitCommands.calc.module.changeCharges import CalcChangeModuleChargesCommand
|
||||||
sFit.recalc(fit)
|
sFit.recalc(fit)
|
||||||
CalcChangeModuleChargesCommand(
|
CalcChangeModuleChargesCommand(
|
||||||
fit.ID,
|
fit.ID,
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ from eos.saveddata.fit import Fit
|
|||||||
from eos.saveddata.implant import Implant
|
from eos.saveddata.implant import Implant
|
||||||
from eos.saveddata.module import Module
|
from eos.saveddata.module import Module
|
||||||
from eos.saveddata.ship import Ship
|
from eos.saveddata.ship import Ship
|
||||||
from gui.fitCommands.helpers import activeStateLimit
|
|
||||||
from service.const import PortEftOptions
|
from service.const import PortEftOptions
|
||||||
from service.fit import Fit as svcFit
|
from service.fit import Fit as svcFit
|
||||||
from service.market import Market
|
from service.market import Market
|
||||||
@@ -48,6 +47,8 @@ SLOT_ORDER = (FittingSlot.LOW, FittingSlot.MED, FittingSlot.HIGH, FittingSlot.RI
|
|||||||
OFFLINE_SUFFIX = '/offline'
|
OFFLINE_SUFFIX = '/offline'
|
||||||
NAME_CHARS = r'[^,/\[\]]' # Characters which are allowed to be used in name
|
NAME_CHARS = r'[^,/\[\]]' # Characters which are allowed to be used in name
|
||||||
|
|
||||||
|
from service.port.active_state import activeStateLimit
|
||||||
|
|
||||||
|
|
||||||
class MutationExportData:
|
class MutationExportData:
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ from eos.saveddata.fighter import Fighter
|
|||||||
from eos.saveddata.fit import Fit
|
from eos.saveddata.fit import Fit
|
||||||
from eos.saveddata.module import Module
|
from eos.saveddata.module import Module
|
||||||
from eos.saveddata.ship import Ship
|
from eos.saveddata.ship import Ship
|
||||||
from gui.fitCommands.helpers import activeStateLimit
|
|
||||||
from service.fit import Fit as svcFit
|
from service.fit import Fit as svcFit
|
||||||
from service.market import Market
|
from service.market import Market
|
||||||
|
|
||||||
@@ -161,6 +160,7 @@ def exportESI(ofit, exportCharges, exportImplants, exportBoosters, callback):
|
|||||||
|
|
||||||
|
|
||||||
def importESI(string):
|
def importESI(string):
|
||||||
|
from gui.fitCommands.helpers import activeStateLimit
|
||||||
|
|
||||||
sMkt = Market.getInstance()
|
sMkt = Market.getInstance()
|
||||||
fitobj = Fit()
|
fitobj = Fit()
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ from service.port.eft import (
|
|||||||
isValidImplantImport, isValidBoosterImport)
|
isValidImplantImport, isValidBoosterImport)
|
||||||
from service.port.esi import exportESI, importESI
|
from service.port.esi import exportESI, importESI
|
||||||
from service.port.multibuy import exportMultiBuy
|
from service.port.multibuy import exportMultiBuy
|
||||||
from service.port.shipstats import exportFitStats
|
|
||||||
from service.port.xml import importXml, exportXml
|
from service.port.xml import importXml, exportXml
|
||||||
from service.port.muta import parseMutant, parseDynamicItemString, fetchDynamicItem
|
from service.port.muta import parseMutant, parseDynamicItemString, fetchDynamicItem
|
||||||
|
|
||||||
@@ -195,10 +194,14 @@ class Port:
|
|||||||
# TODO: catch the exception?
|
# TODO: catch the exception?
|
||||||
# activeFit is reserved?, bufferStr is unicode? (assume only clipboard string?
|
# activeFit is reserved?, bufferStr is unicode? (assume only clipboard string?
|
||||||
sFit = svcFit.getInstance()
|
sFit = svcFit.getInstance()
|
||||||
|
if sFit.character is None:
|
||||||
|
from eos.saveddata.character import Character as saveddata_Character
|
||||||
|
sFit.character = saveddata_Character.getAll5()
|
||||||
importType, makesNewFits, importData = Port.importAuto(bufferStr, activeFit=activeFit)
|
importType, makesNewFits, importData = Port.importAuto(bufferStr, activeFit=activeFit)
|
||||||
|
|
||||||
if makesNewFits:
|
if makesNewFits:
|
||||||
for fit in importData:
|
fits = [f for f in importData if f is not None]
|
||||||
|
for fit in fits:
|
||||||
fit.character = sFit.character
|
fit.character = sFit.character
|
||||||
fit.damagePattern = sFit.pattern
|
fit.damagePattern = sFit.pattern
|
||||||
fit.targetProfile = sFit.targetProfile
|
fit.targetProfile = sFit.targetProfile
|
||||||
@@ -208,6 +211,7 @@ class Port:
|
|||||||
useCharImplants = sFit.serviceFittingOptions["useCharacterImplantsByDefault"]
|
useCharImplants = sFit.serviceFittingOptions["useCharacterImplantsByDefault"]
|
||||||
fit.implantLocation = ImplantLocation.CHARACTER if useCharImplants else ImplantLocation.FIT
|
fit.implantLocation = ImplantLocation.CHARACTER if useCharImplants else ImplantLocation.FIT
|
||||||
db.save(fit)
|
db.save(fit)
|
||||||
|
return importType, fits
|
||||||
return importType, importData
|
return importType, importData
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -341,4 +345,5 @@ class Port:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def exportFitStats(fit, callback=None):
|
def exportFitStats(fit, callback=None):
|
||||||
return exportFitStats(fit, callback=callback)
|
from service.port.shipstats import exportFitStats as _exportFitStats
|
||||||
|
return _exportFitStats(fit, callback=callback)
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ from eos.saveddata.fighter import Fighter
|
|||||||
from eos.saveddata.fit import Fit
|
from eos.saveddata.fit import Fit
|
||||||
from eos.saveddata.module import Module
|
from eos.saveddata.module import Module
|
||||||
from eos.saveddata.ship import Ship
|
from eos.saveddata.ship import Ship
|
||||||
from gui.fitCommands.helpers import activeStateLimit
|
|
||||||
from service.fit import Fit as svcFit
|
from service.fit import Fit as svcFit
|
||||||
from service.market import Market
|
from service.market import Market
|
||||||
from service.port.muta import renderMutantAttrs, parseMutantAttrs
|
from service.port.muta import renderMutantAttrs, parseMutantAttrs
|
||||||
@@ -155,6 +154,7 @@ def _resolve_module(hardware, sMkt, b_localized):
|
|||||||
|
|
||||||
|
|
||||||
def importXml(text, progress):
|
def importXml(text, progress):
|
||||||
|
from gui.fitCommands.helpers import activeStateLimit
|
||||||
from .port import Port
|
from .port import Port
|
||||||
sMkt = Market.getInstance()
|
sMkt = Market.getInstance()
|
||||||
doc = xml.dom.minidom.parseString(text)
|
doc = xml.dom.minidom.parseString(text)
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import timeit
|
|||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
import math
|
import math
|
||||||
import wx
|
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
|
||||||
from eos import db
|
from eos import db
|
||||||
@@ -51,6 +50,8 @@ class Price:
|
|||||||
sources = {}
|
sources = {}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
# Import market sources so they register; avoid at module level so server path never loads them
|
||||||
|
from service.marketSources import evemarketdata, fuzzwork, cevemarket, evetycoon # noqa: F401
|
||||||
# Start price fetcher
|
# Start price fetcher
|
||||||
self.priceWorkerThread = PriceWorkerThread()
|
self.priceWorkerThread = PriceWorkerThread()
|
||||||
self.priceWorkerThread.daemon = True
|
self.priceWorkerThread.daemon = True
|
||||||
@@ -253,6 +254,7 @@ class PriceWorkerThread(threading.Thread):
|
|||||||
if len(requests) > 0:
|
if len(requests) > 0:
|
||||||
Price.fetchPrices(requests, fetchTimeout, validityOverride)
|
Price.fetchPrices(requests, fetchTimeout, validityOverride)
|
||||||
|
|
||||||
|
import wx
|
||||||
wx.CallAfter(callback)
|
wx.CallAfter(callback)
|
||||||
queue.task_done()
|
queue.task_done()
|
||||||
|
|
||||||
@@ -273,7 +275,3 @@ class PriceWorkerThread(threading.Thread):
|
|||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.running = False
|
self.running = False
|
||||||
|
|
||||||
|
|
||||||
# Import market sources only to initialize price source modules, they register on their own
|
|
||||||
from service.marketSources import evemarketdata, fuzzwork, cevemarket, evetycoon # noqa: E402
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import urllib.error
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
import json
|
import json
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
import wx
|
|
||||||
|
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
|
||||||
@@ -582,6 +581,7 @@ class LocaleSettings:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def supported_languages(cls):
|
def supported_languages(cls):
|
||||||
"""Requires the application to be initialized, otherwise wx.Translation isn't set."""
|
"""Requires the application to be initialized, otherwise wx.Translation isn't set."""
|
||||||
|
import wx
|
||||||
pyfalog.info(f'using "{config.CATALOG}" to fetch languages, relatively base path "{os.getcwd()}"')
|
pyfalog.info(f'using "{config.CATALOG}" to fetch languages, relatively base path "{os.getcwd()}"')
|
||||||
return {x: wx.Locale.FindLanguageInfo(x) for x in wx.Translations.Get().GetAvailableTranslations(config.CATALOG)}
|
return {x: wx.Locale.FindLanguageInfo(x) for x in wx.Translations.Get().GetAvailableTranslations(config.CATALOG)}
|
||||||
|
|
||||||
|
|||||||
46
tests/test_pyfa_sim.py
Normal file
46
tests/test_pyfa_sim.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
sys.path.append(os.path.realpath(os.path.join(script_dir, "..")))
|
||||||
|
|
||||||
|
from scripts import pyfa_sim # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
ISHTAR_SPIDER_FIT = """[Ishtar, Spider]
|
||||||
|
|
||||||
|
Capacitor Power Relay II
|
||||||
|
Drone Damage Amplifier II
|
||||||
|
Explosive Armor Hardener II
|
||||||
|
Multispectrum Energized Membrane II
|
||||||
|
Reactive Armor Hardener
|
||||||
|
Shadow Serpentis EM Armor Hardener
|
||||||
|
|
||||||
|
Cap Recharger II
|
||||||
|
Omnidirectional Tracking Link II, Tracking Speed Script
|
||||||
|
Medium Compact Pb-Acid Cap Battery
|
||||||
|
Republic Fleet Large Cap Battery
|
||||||
|
|
||||||
|
Medium Remote Armor Repairer II
|
||||||
|
Medium Remote Armor Repairer II
|
||||||
|
Medium Remote Armor Repairer II
|
||||||
|
Medium Remote Armor Repairer II
|
||||||
|
|
||||||
|
Medium Explosive Armor Reinforcer II
|
||||||
|
Medium Thermal Armor Reinforcer II
|
||||||
|
|
||||||
|
|
||||||
|
Valkyrie II x5
|
||||||
|
Berserker II x5
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def test_ishtar_spider_remote_armor_reps():
|
||||||
|
payload = {"fit": ISHTAR_SPIDER_FIT}
|
||||||
|
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp:
|
||||||
|
data = pyfa_sim.compute_stats(payload, tmp)
|
||||||
|
armor_rps = data["remote_reps_outgoing"]["current"]["armor"]
|
||||||
|
assert armor_rps is not None
|
||||||
|
assert int(round(armor_rps)) == 171
|
||||||
59
uv.lock
generated
59
uv.lock
generated
@@ -159,6 +159,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
|
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "contourpy"
|
name = "contourpy"
|
||||||
version = "1.3.3"
|
version = "1.3.3"
|
||||||
@@ -369,6 +378,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kiwisolver"
|
name = "kiwisolver"
|
||||||
version = "1.4.9"
|
version = "1.4.9"
|
||||||
@@ -593,6 +611,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
|
{ url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyasn1"
|
name = "pyasn1"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
@@ -634,6 +661,11 @@ dependencies = [
|
|||||||
{ name = "wxpython" },
|
{ name = "wxpython" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "beautifulsoup4", specifier = "==4.12.2" },
|
{ name = "beautifulsoup4", specifier = "==4.12.2" },
|
||||||
@@ -643,6 +675,7 @@ requires-dist = [
|
|||||||
{ name = "matplotlib", specifier = "==3.8.2" },
|
{ name = "matplotlib", specifier = "==3.8.2" },
|
||||||
{ name = "numpy", specifier = "==1.26.2" },
|
{ name = "numpy", specifier = "==1.26.2" },
|
||||||
{ name = "packaging", specifier = "==23.2" },
|
{ name = "packaging", specifier = "==23.2" },
|
||||||
|
{ name = "pytest", marker = "extra == 'dev'" },
|
||||||
{ name = "python-dateutil", specifier = "==2.8.2" },
|
{ name = "python-dateutil", specifier = "==2.8.2" },
|
||||||
{ name = "python-jose", specifier = "==3.3.0" },
|
{ name = "python-jose", specifier = "==3.3.0" },
|
||||||
{ name = "pyyaml", specifier = "==6.0.1" },
|
{ name = "pyyaml", specifier = "==6.0.1" },
|
||||||
@@ -653,6 +686,16 @@ requires-dist = [
|
|||||||
{ name = "sqlalchemy", specifier = "==1.4.50" },
|
{ name = "sqlalchemy", specifier = "==1.4.50" },
|
||||||
{ name = "wxpython", specifier = "==4.2.1" },
|
{ name = "wxpython", specifier = "==4.2.1" },
|
||||||
]
|
]
|
||||||
|
provides-extras = ["dev"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.19.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyparsing"
|
name = "pyparsing"
|
||||||
@@ -663,6 +706,22 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl", hash = "sha256:023b5e7e5520ad96642e2c6db4cb683d3970bd640cdf7115049a6e9c3682df82", size = 121793, upload-time = "2025-12-23T03:14:02.103Z" },
|
{ url = "https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl", hash = "sha256:023b5e7e5520ad96642e2c6db4cb683d3970bd640cdf7115049a6e9c3682df82", size = 121793, upload-time = "2025-12-23T03:14:02.103Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "9.0.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "iniconfig" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dateutil"
|
name = "python-dateutil"
|
||||||
version = "2.8.2"
|
version = "2.8.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user