tradedangerous 12.7.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- py.typed +1 -0
- trade.py +49 -0
- tradedangerous/__init__.py +43 -0
- tradedangerous/cache.py +1381 -0
- tradedangerous/cli.py +136 -0
- tradedangerous/commands/TEMPLATE.py +74 -0
- tradedangerous/commands/__init__.py +244 -0
- tradedangerous/commands/buildcache_cmd.py +102 -0
- tradedangerous/commands/buy_cmd.py +427 -0
- tradedangerous/commands/commandenv.py +372 -0
- tradedangerous/commands/exceptions.py +94 -0
- tradedangerous/commands/export_cmd.py +150 -0
- tradedangerous/commands/import_cmd.py +222 -0
- tradedangerous/commands/local_cmd.py +243 -0
- tradedangerous/commands/market_cmd.py +207 -0
- tradedangerous/commands/nav_cmd.py +252 -0
- tradedangerous/commands/olddata_cmd.py +270 -0
- tradedangerous/commands/parsing.py +221 -0
- tradedangerous/commands/rares_cmd.py +298 -0
- tradedangerous/commands/run_cmd.py +1521 -0
- tradedangerous/commands/sell_cmd.py +262 -0
- tradedangerous/commands/shipvendor_cmd.py +60 -0
- tradedangerous/commands/station_cmd.py +68 -0
- tradedangerous/commands/trade_cmd.py +181 -0
- tradedangerous/commands/update_cmd.py +67 -0
- tradedangerous/corrections.py +55 -0
- tradedangerous/csvexport.py +234 -0
- tradedangerous/db/__init__.py +27 -0
- tradedangerous/db/adapter.py +192 -0
- tradedangerous/db/config.py +107 -0
- tradedangerous/db/engine.py +259 -0
- tradedangerous/db/lifecycle.py +332 -0
- tradedangerous/db/locks.py +208 -0
- tradedangerous/db/orm_models.py +500 -0
- tradedangerous/db/paths.py +113 -0
- tradedangerous/db/utils.py +661 -0
- tradedangerous/edscupdate.py +565 -0
- tradedangerous/edsmupdate.py +474 -0
- tradedangerous/formatting.py +210 -0
- tradedangerous/fs.py +156 -0
- tradedangerous/gui.py +1146 -0
- tradedangerous/mapping.py +133 -0
- tradedangerous/mfd/__init__.py +103 -0
- tradedangerous/mfd/saitek/__init__.py +3 -0
- tradedangerous/mfd/saitek/directoutput.py +678 -0
- tradedangerous/mfd/saitek/x52pro.py +195 -0
- tradedangerous/misc/checkpricebounds.py +287 -0
- tradedangerous/misc/clipboard.py +49 -0
- tradedangerous/misc/coord64.py +83 -0
- tradedangerous/misc/csvdialect.py +57 -0
- tradedangerous/misc/derp-sentinel.py +35 -0
- tradedangerous/misc/diff-system-csvs.py +159 -0
- tradedangerous/misc/eddb.py +81 -0
- tradedangerous/misc/eddn.py +349 -0
- tradedangerous/misc/edsc.py +437 -0
- tradedangerous/misc/edsm.py +121 -0
- tradedangerous/misc/importeddbstats.py +54 -0
- tradedangerous/misc/prices-json-exp.py +179 -0
- tradedangerous/misc/progress.py +194 -0
- tradedangerous/plugins/__init__.py +249 -0
- tradedangerous/plugins/edcd_plug.py +371 -0
- tradedangerous/plugins/eddblink_plug.py +861 -0
- tradedangerous/plugins/edmc_batch_plug.py +133 -0
- tradedangerous/plugins/spansh_plug.py +2647 -0
- tradedangerous/prices.py +211 -0
- tradedangerous/submit-distances.py +422 -0
- tradedangerous/templates/Added.csv +37 -0
- tradedangerous/templates/Category.csv +17 -0
- tradedangerous/templates/RareItem.csv +143 -0
- tradedangerous/templates/TradeDangerous.sql +338 -0
- tradedangerous/tools.py +40 -0
- tradedangerous/tradecalc.py +1302 -0
- tradedangerous/tradedb.py +2320 -0
- tradedangerous/tradeenv.py +313 -0
- tradedangerous/tradeenv.pyi +109 -0
- tradedangerous/tradeexcept.py +131 -0
- tradedangerous/tradeorm.py +183 -0
- tradedangerous/transfers.py +192 -0
- tradedangerous/utils.py +243 -0
- tradedangerous/version.py +16 -0
- tradedangerous-12.7.6.dist-info/METADATA +106 -0
- tradedangerous-12.7.6.dist-info/RECORD +87 -0
- tradedangerous-12.7.6.dist-info/WHEEL +5 -0
- tradedangerous-12.7.6.dist-info/entry_points.txt +3 -0
- tradedangerous-12.7.6.dist-info/licenses/LICENSE +373 -0
- tradedangerous-12.7.6.dist-info/top_level.txt +2 -0
- tradegui.py +24 -0
|
@@ -0,0 +1,1521 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from itertools import chain
|
|
3
|
+
import math
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
import typing
|
|
7
|
+
|
|
8
|
+
from tradedangerous.tradedb import describeAge, TradeDB, Station, System
|
|
9
|
+
from tradedangerous.tradecalc import NoHopsError, Route, TradeCalc, UserAbortedRun
|
|
10
|
+
|
|
11
|
+
from .commandenv import ResultRow
|
|
12
|
+
from .exceptions import CommandLineError, NoDataError
|
|
13
|
+
from .parsing import (
|
|
14
|
+
BlackMarketSwitch, FleetCarrierArgument, MutuallyExclusiveGroup,
|
|
15
|
+
NoPlanetSwitch, OdysseyArgument, PadSizeArgument, ParseArgument,
|
|
16
|
+
PlanetaryArgument,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if typing.TYPE_CHECKING:
|
|
20
|
+
from tradedangerous import TradeEnv
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
######################################################################
|
|
24
|
+
# Parser config
|
|
25
|
+
|
|
26
|
+
help = 'Calculate best trade run.'
|
|
27
|
+
name = 'run'
|
|
28
|
+
epilog = None
|
|
29
|
+
usesTradeData = True
|
|
30
|
+
|
|
31
|
+
arguments = [
|
|
32
|
+
ParseArgument('--capacity',
|
|
33
|
+
help = 'Maximum capacity of cargo hold.',
|
|
34
|
+
metavar = 'N',
|
|
35
|
+
type = int,
|
|
36
|
+
),
|
|
37
|
+
ParseArgument('--credits',
|
|
38
|
+
help = 'Starting credits.',
|
|
39
|
+
metavar = 'CR',
|
|
40
|
+
type = "credits",
|
|
41
|
+
),
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
switches = [
|
|
45
|
+
ParseArgument('--from', '-f',
|
|
46
|
+
help = 'Starting system/station.',
|
|
47
|
+
dest = 'starting',
|
|
48
|
+
metavar = 'STATION',
|
|
49
|
+
),
|
|
50
|
+
MutuallyExclusiveGroup(
|
|
51
|
+
ParseArgument('--to', '-t',
|
|
52
|
+
help = 'Final system/station.',
|
|
53
|
+
dest = 'ending',
|
|
54
|
+
metavar = 'PLACE',
|
|
55
|
+
default = None,
|
|
56
|
+
),
|
|
57
|
+
ParseArgument('--towards', '-T',
|
|
58
|
+
help = (
|
|
59
|
+
'Choose a route that continually reduces the '
|
|
60
|
+
'distance towards this system.'
|
|
61
|
+
),
|
|
62
|
+
dest = 'goalSystem',
|
|
63
|
+
metavar = 'SYSTEM',
|
|
64
|
+
default = None,
|
|
65
|
+
),
|
|
66
|
+
ParseArgument('--loop',
|
|
67
|
+
help = 'Return to the starting station.',
|
|
68
|
+
action = 'store_true',
|
|
69
|
+
default = False,
|
|
70
|
+
),
|
|
71
|
+
),
|
|
72
|
+
ParseArgument('--via',
|
|
73
|
+
help = 'Require specified systems/stations to be en-route.',
|
|
74
|
+
action = 'append',
|
|
75
|
+
metavar = 'PLACE[,PLACE,...]',
|
|
76
|
+
),
|
|
77
|
+
ParseArgument('--avoid',
|
|
78
|
+
help = 'Exclude an item, system or station from trading. '
|
|
79
|
+
'Partial matches allowed, '
|
|
80
|
+
'e.g. "dom.App" or "domap" matches "Dom. Appliances".',
|
|
81
|
+
action = 'append',
|
|
82
|
+
),
|
|
83
|
+
MutuallyExclusiveGroup(
|
|
84
|
+
ParseArgument('--direct',
|
|
85
|
+
help = "Assume destinations are reachable without worrying "
|
|
86
|
+
"about jumps.",
|
|
87
|
+
action = 'store_true',
|
|
88
|
+
),
|
|
89
|
+
ParseArgument('--hops',
|
|
90
|
+
help = 'Number of hops (station-to-station) to run.',
|
|
91
|
+
default = 2,
|
|
92
|
+
type = int,
|
|
93
|
+
metavar = 'N',
|
|
94
|
+
),
|
|
95
|
+
),
|
|
96
|
+
ParseArgument('--jumps-per',
|
|
97
|
+
help = 'Maximum number of jumps (system-to-system) per hop.',
|
|
98
|
+
default = 2,
|
|
99
|
+
dest = 'maxJumpsPer',
|
|
100
|
+
metavar = 'N',
|
|
101
|
+
type = int,
|
|
102
|
+
),
|
|
103
|
+
ParseArgument('--ly-per',
|
|
104
|
+
help = 'Maximum light years per jump.',
|
|
105
|
+
dest = 'maxLyPer',
|
|
106
|
+
metavar = 'N.NN',
|
|
107
|
+
type = float,
|
|
108
|
+
),
|
|
109
|
+
ParseArgument('--empty-ly',
|
|
110
|
+
help = 'Maximum light years ship can jump when empty.',
|
|
111
|
+
dest = 'emptyLyPer',
|
|
112
|
+
metavar = 'N.NN',
|
|
113
|
+
type = float,
|
|
114
|
+
default = None,
|
|
115
|
+
),
|
|
116
|
+
ParseArgument('--start-jumps', '-s',
|
|
117
|
+
help = 'Consider stations within this many jumps of the origin '
|
|
118
|
+
'(requires --from).',
|
|
119
|
+
dest = 'startJumps',
|
|
120
|
+
default = 0,
|
|
121
|
+
type = int,
|
|
122
|
+
),
|
|
123
|
+
ParseArgument('--end-jumps', '-e',
|
|
124
|
+
help = 'Consider stations within this many jumps of the destination '
|
|
125
|
+
'(requires --to).',
|
|
126
|
+
dest = 'endJumps',
|
|
127
|
+
default = 0,
|
|
128
|
+
type = int,
|
|
129
|
+
),
|
|
130
|
+
ParseArgument('--show-jumps', '-J',
|
|
131
|
+
help = 'Show detail of jumps between hops.',
|
|
132
|
+
dest = 'showJumps',
|
|
133
|
+
action = 'store_true',
|
|
134
|
+
),
|
|
135
|
+
ParseArgument('--limit',
|
|
136
|
+
help = 'Maximum units of any one cargo item to buy (0: unlimited).',
|
|
137
|
+
metavar = 'N',
|
|
138
|
+
type = int,
|
|
139
|
+
),
|
|
140
|
+
ParseArgument('--age', '--max-days-old', '-MD',
|
|
141
|
+
help = 'Maximum age (in days) of trade data to use.',
|
|
142
|
+
metavar = 'DAYS',
|
|
143
|
+
type = float,
|
|
144
|
+
dest = 'maxAge',
|
|
145
|
+
),
|
|
146
|
+
PadSizeArgument(),
|
|
147
|
+
MutuallyExclusiveGroup(
|
|
148
|
+
NoPlanetSwitch(),
|
|
149
|
+
PlanetaryArgument(),
|
|
150
|
+
),
|
|
151
|
+
FleetCarrierArgument(),
|
|
152
|
+
OdysseyArgument(),
|
|
153
|
+
BlackMarketSwitch(),
|
|
154
|
+
ParseArgument('--ls-penalty', '--lsp',
|
|
155
|
+
help = "Penalty per 1kls stations are from their stars.",
|
|
156
|
+
default = 12.5,
|
|
157
|
+
type = float,
|
|
158
|
+
dest = 'lsPenalty'
|
|
159
|
+
),
|
|
160
|
+
ParseArgument('--ls-max',
|
|
161
|
+
help = 'Only consider stations upto this many ls from their star.',
|
|
162
|
+
metavar = 'LS',
|
|
163
|
+
dest = 'maxLs',
|
|
164
|
+
type = int,
|
|
165
|
+
default = 0,
|
|
166
|
+
),
|
|
167
|
+
ParseArgument('--gain-per-ton', '--gpt',
|
|
168
|
+
help = 'Specify the minimum gain per ton of cargo',
|
|
169
|
+
dest = 'minGainPerTon',
|
|
170
|
+
type = "credits",
|
|
171
|
+
default = 1
|
|
172
|
+
),
|
|
173
|
+
ParseArgument('--max-gain-per-ton', '--mgpt',
|
|
174
|
+
help = 'Specify the maximum gain per ton of cargo',
|
|
175
|
+
dest = 'maxGainPerTon',
|
|
176
|
+
type = "credits",
|
|
177
|
+
default = 0
|
|
178
|
+
),
|
|
179
|
+
ParseArgument('--unique',
|
|
180
|
+
help = 'Only visit each station once.',
|
|
181
|
+
action = 'store_true',
|
|
182
|
+
default = False,
|
|
183
|
+
),
|
|
184
|
+
ParseArgument('--loop-interval', '-li',
|
|
185
|
+
help = (
|
|
186
|
+
'Require this many hops between visits to the same station. '
|
|
187
|
+
'A value of 1 would be the default behavior, so a value of '
|
|
188
|
+
'2 is the minimum allowed.'
|
|
189
|
+
),
|
|
190
|
+
type = int,
|
|
191
|
+
default = None,
|
|
192
|
+
dest = 'loopInt',
|
|
193
|
+
),
|
|
194
|
+
ParseArgument('--margin',
|
|
195
|
+
help = 'Reduce gains made on each hop to provide a margin of error '
|
|
196
|
+
'for market fluctuations (e.g: 0.25 reduces gains by 1/4). '
|
|
197
|
+
'0<: N<: 0.25.',
|
|
198
|
+
default = 0.00,
|
|
199
|
+
metavar = 'N.NN',
|
|
200
|
+
type = float,
|
|
201
|
+
),
|
|
202
|
+
ParseArgument('--insurance',
|
|
203
|
+
help = 'Reserve at least this many credits to cover insurance.',
|
|
204
|
+
default = 0,
|
|
205
|
+
metavar = 'CR',
|
|
206
|
+
type = "credits",
|
|
207
|
+
),
|
|
208
|
+
ParseArgument('--routes',
|
|
209
|
+
help = 'Maximum number of routes to show. DEFAULT: 1',
|
|
210
|
+
default = 1,
|
|
211
|
+
metavar = 'N',
|
|
212
|
+
type = int,
|
|
213
|
+
),
|
|
214
|
+
ParseArgument('--max-routes',
|
|
215
|
+
help = 'At the end of each hop, limit the number of routes '
|
|
216
|
+
'that continue to the next round to the top N '
|
|
217
|
+
'highest scoring',
|
|
218
|
+
default = 0,
|
|
219
|
+
metavar = 'N',
|
|
220
|
+
type = int,
|
|
221
|
+
dest = 'maxRoutes',
|
|
222
|
+
),
|
|
223
|
+
ParseArgument('--checklist',
|
|
224
|
+
help = 'Provide a checklist flow for the route.',
|
|
225
|
+
action = 'store_true',
|
|
226
|
+
default = False,
|
|
227
|
+
),
|
|
228
|
+
ParseArgument('--x52-pro',
|
|
229
|
+
help = 'Enable experimental X52 Pro MFD output.',
|
|
230
|
+
action = 'store_true',
|
|
231
|
+
default = False,
|
|
232
|
+
dest = 'x52pro',
|
|
233
|
+
),
|
|
234
|
+
ParseArgument('--prune-score',
|
|
235
|
+
help = "From the 3rd hop on, only consider routes which have at least this percentage of the current best route's score.",
|
|
236
|
+
dest = 'pruneScores',
|
|
237
|
+
type = float,
|
|
238
|
+
default = 0,
|
|
239
|
+
),
|
|
240
|
+
ParseArgument('--prune-hops',
|
|
241
|
+
help = 'Changes which hop --prune-score takes effect from.',
|
|
242
|
+
default = 3,
|
|
243
|
+
type = int,
|
|
244
|
+
dest = 'pruneHops',
|
|
245
|
+
),
|
|
246
|
+
ParseArgument('--progress', '-P',
|
|
247
|
+
help = 'Show hop progress',
|
|
248
|
+
default = False,
|
|
249
|
+
action = 'store_true',
|
|
250
|
+
),
|
|
251
|
+
ParseArgument('--supply',
|
|
252
|
+
help = 'Only considers items which have at least this many units.',
|
|
253
|
+
default = None,
|
|
254
|
+
type = int,
|
|
255
|
+
),
|
|
256
|
+
ParseArgument('--demand',
|
|
257
|
+
help = 'Only considers items which have at least this much demand.',
|
|
258
|
+
default = None,
|
|
259
|
+
type = int
|
|
260
|
+
),
|
|
261
|
+
ParseArgument('--summary',
|
|
262
|
+
help = 'Summary layout of route instructions.',
|
|
263
|
+
action = 'store_true',
|
|
264
|
+
),
|
|
265
|
+
ParseArgument('--shorten',
|
|
266
|
+
help = '(Requires --to) Find the shortest route with the best gpt.',
|
|
267
|
+
action = 'store_true',
|
|
268
|
+
),
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
######################################################################
|
|
273
|
+
# Helpers
|
|
274
|
+
|
|
275
|
+
# Do some basic syntax validation before we waste time loading data.
|
|
276
|
+
def validateRunArgumentsFast(cmdenv):
|
|
277
|
+
"""
|
|
278
|
+
Fast-fail argument checks that should run BEFORE any database
|
|
279
|
+
access or TradeCalc construction.
|
|
280
|
+
"""
|
|
281
|
+
# --towards requires --from
|
|
282
|
+
if cmdenv.goalSystem and not getattr(cmdenv, "starting", None):
|
|
283
|
+
raise CommandLineError("--towards requires --from")
|
|
284
|
+
|
|
285
|
+
# --start-jumps requires --from
|
|
286
|
+
if cmdenv.startJumps and not getattr(cmdenv, "starting", None):
|
|
287
|
+
raise CommandLineError("--start-jumps requires --from")
|
|
288
|
+
|
|
289
|
+
# --end-jumps requires --to
|
|
290
|
+
if cmdenv.endJumps and not getattr(cmdenv, "ending", None):
|
|
291
|
+
raise CommandLineError("--end-jumps requires --to")
|
|
292
|
+
|
|
293
|
+
# --shorten only valid with --to
|
|
294
|
+
if cmdenv.shorten and not cmdenv.destPlace:
|
|
295
|
+
raise CommandLineError("--shorten only works with --to.")
|
|
296
|
+
|
|
297
|
+
class Checklist:
|
|
298
|
+
"""
|
|
299
|
+
Class for encapsulating display of a route as a series of
|
|
300
|
+
steps to be 'checked off' as the user passes through them.
|
|
301
|
+
"""
|
|
302
|
+
|
|
303
|
+
def __init__(self, tdb, cmdenv):
|
|
304
|
+
self.tdb = tdb
|
|
305
|
+
self.cmdenv = cmdenv
|
|
306
|
+
self.mfd = cmdenv.mfd
|
|
307
|
+
|
|
308
|
+
def doStep(self, action, detail = None, extra = None):
|
|
309
|
+
self.stepNo += 1
|
|
310
|
+
try:
|
|
311
|
+
self.mfd.display(
|
|
312
|
+
"#{} {}".format(self.stepNo, action),
|
|
313
|
+
detail or "",
|
|
314
|
+
extra or ""
|
|
315
|
+
)
|
|
316
|
+
except AttributeError:
|
|
317
|
+
pass
|
|
318
|
+
input(
|
|
319
|
+
" {:<3}: {}: "
|
|
320
|
+
.format(
|
|
321
|
+
self.stepNo,
|
|
322
|
+
" ".join(item for item in (action, detail, extra) if item)
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
def note(self, str, addBreak = True):
|
|
327
|
+
print("(i) {} (i){}".format(str, "\n" if addBreak else ""))
|
|
328
|
+
|
|
329
|
+
def run(self, route, cr):
|
|
330
|
+
mfd = self.mfd
|
|
331
|
+
stations, hops, jumps = route.route, route.hops, route.jumps
|
|
332
|
+
lastHopIdx = len(stations) - 1
|
|
333
|
+
gainCr = 0
|
|
334
|
+
self.stepNo = 0
|
|
335
|
+
|
|
336
|
+
heading = "(i) BEGINNING CHECKLIST FOR {} (i)".format(route.text(lambda x, y: y))
|
|
337
|
+
print(heading, "\n", '-' * len(heading), "\n\n", sep = '')
|
|
338
|
+
|
|
339
|
+
cmdenv = self.cmdenv
|
|
340
|
+
if cmdenv.detail:
|
|
341
|
+
print(route.summary())
|
|
342
|
+
print()
|
|
343
|
+
|
|
344
|
+
for idx in range(lastHopIdx):
|
|
345
|
+
hopNo = idx + 1
|
|
346
|
+
cur, nxt, hop = stations[idx], stations[idx + 1], hops[idx]
|
|
347
|
+
sortedTradeOptions = sorted(
|
|
348
|
+
hop[0],
|
|
349
|
+
key=lambda tradeOption: tradeOption[1] * tradeOption[0].gainCr,
|
|
350
|
+
reverse=True
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# Tell them what they need to buy.
|
|
354
|
+
if cmdenv.detail:
|
|
355
|
+
self.note("HOP {} of {}".format(hopNo, lastHopIdx))
|
|
356
|
+
|
|
357
|
+
self.note("Buy at {}".format(cur.name()))
|
|
358
|
+
for (trade, qty) in sortedTradeOptions:
|
|
359
|
+
self.doStep(
|
|
360
|
+
'Buy {:n} x'.format(qty),
|
|
361
|
+
trade.name(),
|
|
362
|
+
'@ {}cr / {} old'.format(
|
|
363
|
+
trade.costCr,
|
|
364
|
+
describeAge(trade.srcAge),
|
|
365
|
+
))
|
|
366
|
+
if cmdenv.detail:
|
|
367
|
+
self.doStep('Refuel')
|
|
368
|
+
print()
|
|
369
|
+
|
|
370
|
+
# If there is a next hop, describe how to get there.
|
|
371
|
+
self.note(
|
|
372
|
+
"Fly {}"
|
|
373
|
+
.format(
|
|
374
|
+
" -> ".join(jump.name() for jump in jumps[idx])
|
|
375
|
+
)
|
|
376
|
+
)
|
|
377
|
+
if idx < len(hops) and jumps[idx]:
|
|
378
|
+
for jump in jumps[idx][1:]:
|
|
379
|
+
self.doStep('Jump to', jump.name())
|
|
380
|
+
if cmdenv.detail:
|
|
381
|
+
self.doStep('Dock at', nxt.text())
|
|
382
|
+
print()
|
|
383
|
+
|
|
384
|
+
self.note("Sell at {}".format(nxt.name()))
|
|
385
|
+
for (trade, qty) in sortedTradeOptions:
|
|
386
|
+
self.doStep(
|
|
387
|
+
'Sell {:n} x'.format(qty),
|
|
388
|
+
trade.name(),
|
|
389
|
+
'@ {:n}cr / {} old'.format(
|
|
390
|
+
trade.costCr + trade.gainCr,
|
|
391
|
+
describeAge(trade.dstAge),
|
|
392
|
+
))
|
|
393
|
+
print()
|
|
394
|
+
|
|
395
|
+
gainCr += hop[1]
|
|
396
|
+
if cmdenv.detail and gainCr > 0:
|
|
397
|
+
self.note("GAINED: {:n}cr, CREDITS: {:n}cr".format(
|
|
398
|
+
gainCr, cr + gainCr))
|
|
399
|
+
|
|
400
|
+
if hopNo < lastHopIdx:
|
|
401
|
+
print("\n--------------------------------------\n")
|
|
402
|
+
|
|
403
|
+
if mfd:
|
|
404
|
+
mfd.display('FINISHED',
|
|
405
|
+
"+{:n}cr".format(gainCr),
|
|
406
|
+
"={:n}cr".format(cr + gainCr))
|
|
407
|
+
mfd.attention(3)
|
|
408
|
+
from time import sleep
|
|
409
|
+
sleep(1.5)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def expandForJumps(tdb, cmdenv, calc, origin, jumps, srcName, purpose):
|
|
413
|
+
"""
|
|
414
|
+
Find all the stations you could reach if you made a given
|
|
415
|
+
number of jumps away from the origin list.
|
|
416
|
+
"""
|
|
417
|
+
|
|
418
|
+
assert jumps
|
|
419
|
+
|
|
420
|
+
maxLyPer = cmdenv.emptyLyPer or cmdenv.maxLyPer
|
|
421
|
+
avoidPlaces = cmdenv.avoidPlaces
|
|
422
|
+
cmdenv.DEBUG0(
|
|
423
|
+
"expanding {} reach from {} by {} jumps at {}ly per jump",
|
|
424
|
+
srcName,
|
|
425
|
+
origin.name(),
|
|
426
|
+
jumps,
|
|
427
|
+
maxLyPer,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# Correct behaviour: destinations (--to) require buying data; origins (--from) require selling data.
|
|
431
|
+
if srcName == "--to":
|
|
432
|
+
tradingList = calc.stationsBuying
|
|
433
|
+
elif srcName == "--from":
|
|
434
|
+
tradingList = calc.stationsSelling
|
|
435
|
+
else:
|
|
436
|
+
raise Exception("Unknown src")
|
|
437
|
+
|
|
438
|
+
# Ensure O(1) membership checks regardless of the underlying container type.
|
|
439
|
+
trading_ids = tradingList if isinstance(tradingList, set) else set(tradingList)
|
|
440
|
+
|
|
441
|
+
stations: set[Station] = set()
|
|
442
|
+
origins: set[System | Station] = {origin}
|
|
443
|
+
avoid: set[System | Station] = set(avoidPlaces)
|
|
444
|
+
|
|
445
|
+
for jump in range(jumps):
|
|
446
|
+
if not origins:
|
|
447
|
+
break
|
|
448
|
+
if getattr(cmdenv, "debug", False):
|
|
449
|
+
cmdenv.DEBUG1(
|
|
450
|
+
"Ring {}: {}",
|
|
451
|
+
jump,
|
|
452
|
+
[sys.dbname for sys in origins]
|
|
453
|
+
)
|
|
454
|
+
thisJump, origins = origins, set()
|
|
455
|
+
for system in thisJump:
|
|
456
|
+
avoid.add(system)
|
|
457
|
+
for stn in system.stations or ():
|
|
458
|
+
if stn.ID not in trading_ids:
|
|
459
|
+
if getattr(cmdenv, "debug", False):
|
|
460
|
+
cmdenv.DEBUG2(
|
|
461
|
+
"X {}/{} not in trading list",
|
|
462
|
+
stn.system.dbname, stn.dbname,
|
|
463
|
+
)
|
|
464
|
+
continue
|
|
465
|
+
if not checkStationSuitability(cmdenv, calc, stn):
|
|
466
|
+
if getattr(cmdenv, "debug", False):
|
|
467
|
+
cmdenv.DEBUG2(
|
|
468
|
+
"X {}/{} was not suitable",
|
|
469
|
+
stn.system.dbname, stn.dbname,
|
|
470
|
+
)
|
|
471
|
+
continue
|
|
472
|
+
if getattr(cmdenv, "debug", False):
|
|
473
|
+
cmdenv.DEBUG2(
|
|
474
|
+
"- {}/{} meets requirements",
|
|
475
|
+
stn.system.dbname, stn.dbname,
|
|
476
|
+
)
|
|
477
|
+
stations.add(stn)
|
|
478
|
+
for dest, dist in tdb.genSystemsInRange(system, maxLyPer):
|
|
479
|
+
if dest not in avoid:
|
|
480
|
+
origins.add(dest)
|
|
481
|
+
|
|
482
|
+
if getattr(cmdenv, "debug", False):
|
|
483
|
+
cmdenv.DEBUG0(
|
|
484
|
+
"Expanded {} stations: {}",
|
|
485
|
+
srcName,
|
|
486
|
+
[stn.name() for stn in stations]
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
if not stations:
|
|
490
|
+
if not cmdenv.emptyLyPer:
|
|
491
|
+
extra = (
|
|
492
|
+
"\nIf you are willing to make unladen jumps for the sake "
|
|
493
|
+
"of a better route, consider using --empty."
|
|
494
|
+
)
|
|
495
|
+
else:
|
|
496
|
+
extra = ""
|
|
497
|
+
raise CommandLineError(
|
|
498
|
+
"No {} stations with suitable trade data could be found "
|
|
499
|
+
"within {} {}ly jump{} of {} that meet all of your critera.{}"
|
|
500
|
+
.format(
|
|
501
|
+
purpose, maxLyPer,
|
|
502
|
+
jumps, "s" if jumps > 1 else "",
|
|
503
|
+
origin.name(),
|
|
504
|
+
extra,
|
|
505
|
+
)
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
stations = list(stations)
|
|
509
|
+
stations.sort(key=lambda stn: stn.ID)
|
|
510
|
+
|
|
511
|
+
return stations
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def checkForEmptyStationList(category, focusPlace, stationList, jumps):
|
|
515
|
+
if stationList:
|
|
516
|
+
return
|
|
517
|
+
if jumps:
|
|
518
|
+
raise NoDataError(
|
|
519
|
+
"Local database has no price data for any "
|
|
520
|
+
"stations within {} jumps of {} ({})".format(
|
|
521
|
+
jumps,
|
|
522
|
+
focusPlace.name(),
|
|
523
|
+
category,
|
|
524
|
+
))
|
|
525
|
+
if isinstance(focusPlace, System):
|
|
526
|
+
raise NoDataError(
|
|
527
|
+
"Local database either has no price data for "
|
|
528
|
+
"stations in {} ({}) or could not find any that "
|
|
529
|
+
"met your requirements (e.g. pad-size). "
|
|
530
|
+
"Check \"trade.py local -vv --ly 0 {}\"".format(
|
|
531
|
+
focusPlace.name(),
|
|
532
|
+
category,
|
|
533
|
+
focusPlace.name(),
|
|
534
|
+
))
|
|
535
|
+
raise NoDataError(
|
|
536
|
+
"Local database has no price data for {} ({})".format(
|
|
537
|
+
focusPlace.name(),
|
|
538
|
+
category,
|
|
539
|
+
))
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def checkAnchorNotInVia(hops, anchorName, place, viaSet):
|
|
543
|
+
"""
|
|
544
|
+
Ensure that '--to' or '--from' is not in the via set.
|
|
545
|
+
"""
|
|
546
|
+
|
|
547
|
+
if hops != 2:
|
|
548
|
+
return
|
|
549
|
+
if isinstance(place, Station) and place in viaSet:
|
|
550
|
+
raise CommandLineError(
|
|
551
|
+
"{} used in {} and --via with only 2 hops".format(
|
|
552
|
+
place.name(),
|
|
553
|
+
anchorName,
|
|
554
|
+
))
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def checkStationSuitability(cmdenv, calc, station, src = None):
|
|
558
|
+
cmdenv.DEBUG2(
|
|
559
|
+
"checking {} (ls={}, bm={}, pad={}, plt={}, flc={}, ody={}, mkt={}, shp={}) "
|
|
560
|
+
"for {} suitability",
|
|
561
|
+
station.name(),
|
|
562
|
+
station.lsFromStar,
|
|
563
|
+
station.blackMarket,
|
|
564
|
+
station.maxPadSize,
|
|
565
|
+
station.planetary,
|
|
566
|
+
station.fleet,
|
|
567
|
+
station.odyssey,
|
|
568
|
+
station.market,
|
|
569
|
+
station.shipyard,
|
|
570
|
+
src or "any",
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
if station in cmdenv.avoidPlaces and src != "--from":
|
|
574
|
+
if src:
|
|
575
|
+
raise CommandLineError(
|
|
576
|
+
"{} station {} is marked to avoid"
|
|
577
|
+
.format(src, station.name())
|
|
578
|
+
)
|
|
579
|
+
return False
|
|
580
|
+
if station.system in cmdenv.avoidPlaces and src != "--from":
|
|
581
|
+
if src:
|
|
582
|
+
raise CommandLineError(
|
|
583
|
+
"{} station {} is in system listed in --avoid"
|
|
584
|
+
.format(src, station.name())
|
|
585
|
+
)
|
|
586
|
+
return False
|
|
587
|
+
if station.market == 'N':
|
|
588
|
+
if src:
|
|
589
|
+
raise CommandLineError(
|
|
590
|
+
"{} station {} is flagged as having no market".format(
|
|
591
|
+
src, station.name()
|
|
592
|
+
)
|
|
593
|
+
)
|
|
594
|
+
return False
|
|
595
|
+
if not station.itemCount:
|
|
596
|
+
if src:
|
|
597
|
+
raise NoDataError(
|
|
598
|
+
"No price data in local database "
|
|
599
|
+
"for {} station: {}".format(
|
|
600
|
+
src, station.name(),
|
|
601
|
+
))
|
|
602
|
+
return False
|
|
603
|
+
if src != "--to" and station.ID not in calc.stationsSelling:
|
|
604
|
+
if src:
|
|
605
|
+
raise NoDataError(
|
|
606
|
+
"No buying prices at {}."
|
|
607
|
+
.format(station.name())
|
|
608
|
+
)
|
|
609
|
+
return False
|
|
610
|
+
if src != "--from" and station.ID not in calc.stationsBuying:
|
|
611
|
+
if src:
|
|
612
|
+
raise NoDataError(
|
|
613
|
+
"No selling prices at {}."
|
|
614
|
+
.format(station.name())
|
|
615
|
+
)
|
|
616
|
+
return False
|
|
617
|
+
mps = cmdenv.padSize
|
|
618
|
+
if mps and not station.checkPadSize(mps):
|
|
619
|
+
if src:
|
|
620
|
+
raise CommandLineError(
|
|
621
|
+
"{} station {} does not meet pad-size requirement.\n"
|
|
622
|
+
"You specified: {}, Current data for station: {} ({})\n"
|
|
623
|
+
"You can use \"trade.py station\" to correct this.".format(
|
|
624
|
+
src, station.name(),
|
|
625
|
+
mps, station.maxPadSize,
|
|
626
|
+
TradeDB.padSizesExt[station.maxPadSize],
|
|
627
|
+
))
|
|
628
|
+
return False
|
|
629
|
+
pla = cmdenv.planetary
|
|
630
|
+
if pla and not station.checkPlanetary(pla):
|
|
631
|
+
if src:
|
|
632
|
+
raise CommandLineError(
|
|
633
|
+
"{} station {} does not meet planetary requirement.\n"
|
|
634
|
+
"You specified: {}, Current data for station: {} ({})\n"
|
|
635
|
+
"You can use \"trade.py station\" to correct this.".format(
|
|
636
|
+
src, station.name(),
|
|
637
|
+
pla, station.planetary,
|
|
638
|
+
TradeDB.planetStatesExt[station.planetary],
|
|
639
|
+
))
|
|
640
|
+
return False
|
|
641
|
+
flc = cmdenv.fleet
|
|
642
|
+
if flc and not station.checkFleet(flc):
|
|
643
|
+
if src:
|
|
644
|
+
raise CommandLineError(
|
|
645
|
+
"{} station {} does not meet fleet carrier requirement.\n"
|
|
646
|
+
"You specified: {}, Current data for station: {} ({})\n"
|
|
647
|
+
"You can use \"trade.py station\" to correct this.".format(
|
|
648
|
+
src, station.name(),
|
|
649
|
+
flc, station.fleet,
|
|
650
|
+
TradeDB.fleetStatesExt[station.fleet],
|
|
651
|
+
))
|
|
652
|
+
return False
|
|
653
|
+
ody = cmdenv.odyssey
|
|
654
|
+
if ody and not station.checkOdyssey(ody):
|
|
655
|
+
if src:
|
|
656
|
+
raise CommandLineError(
|
|
657
|
+
"{} station {} does not meet odyssey requirement.\n"
|
|
658
|
+
"You specified: {}, Current data for station: {} ({})\n"
|
|
659
|
+
"You can use \"trade.py station\" to correct this.".format(
|
|
660
|
+
src, station.name(),
|
|
661
|
+
ody, station.odyssey,
|
|
662
|
+
TradeDB.odysseyStatesExt[station.odyssey],
|
|
663
|
+
))
|
|
664
|
+
return False
|
|
665
|
+
np = cmdenv.noPlanet
|
|
666
|
+
if np and station.planetary != 'N':
|
|
667
|
+
if src and src != "--from":
|
|
668
|
+
raise CommandLineError(
|
|
669
|
+
"{} station {} does not meet no-planet "
|
|
670
|
+
"requirement.".format(
|
|
671
|
+
src, station.name(),
|
|
672
|
+
))
|
|
673
|
+
return False
|
|
674
|
+
bm = cmdenv.blackMarket
|
|
675
|
+
if bm and station.blackMarket != 'Y':
|
|
676
|
+
if src and src != "--from":
|
|
677
|
+
raise CommandLineError(
|
|
678
|
+
"{} station {} does not meet black-market "
|
|
679
|
+
"requirement.".format(
|
|
680
|
+
src, station.name(),
|
|
681
|
+
))
|
|
682
|
+
return False
|
|
683
|
+
mls = cmdenv.maxLs
|
|
684
|
+
if mls and (station.lsFromStar <= 0 or station.lsFromStar > mls):
|
|
685
|
+
if src and src != "--from":
|
|
686
|
+
raise CommandLineError(
|
|
687
|
+
"{} station {} does not meet max-ls "
|
|
688
|
+
"requirement.".format(
|
|
689
|
+
src, station.name(),
|
|
690
|
+
))
|
|
691
|
+
return False
|
|
692
|
+
maxAge, stnAge = cmdenv.maxAge, station.dataAge or float("inf")
|
|
693
|
+
if maxAge and stnAge > maxAge:
|
|
694
|
+
if src and src != "--from":
|
|
695
|
+
raise CommandLineError(
|
|
696
|
+
"{} station {} does not meet --age "
|
|
697
|
+
"requirement.".format(
|
|
698
|
+
src, station.name(),
|
|
699
|
+
))
|
|
700
|
+
return False
|
|
701
|
+
return True
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def filterStationSet(src, cmdenv, calc, stnList):
|
|
705
|
+
if not stnList:
|
|
706
|
+
return stnList
|
|
707
|
+
if getattr(cmdenv, "debug", False):
|
|
708
|
+
cmdenv.DEBUG0(
|
|
709
|
+
"filtering {} station list: {}",
|
|
710
|
+
src,
|
|
711
|
+
",".join(station.name() for station in stnList),
|
|
712
|
+
)
|
|
713
|
+
stnList = tuple(
|
|
714
|
+
place for place in stnList
|
|
715
|
+
if isinstance(place, System) or checkStationSuitability(cmdenv, calc, place, src)
|
|
716
|
+
)
|
|
717
|
+
if not stnList:
|
|
718
|
+
# Preserve original message formatting (no behavior change)
|
|
719
|
+
raise CommandLineError("No {src} station met your criteria.")
|
|
720
|
+
return stnList
|
|
721
|
+
|
|
722
|
+
def checkOrigins(tdb, cmdenv, calc):
|
|
723
|
+
# Compute eligibility once: stations must both sell and buy (same as suitability with src=None).
|
|
724
|
+
eligible_ids = set(calc.stationsSelling) & set(calc.stationsBuying)
|
|
725
|
+
|
|
726
|
+
setattr(cmdenv, "_origin", cmdenv.origPlace) # store the original so we can overwrite it
|
|
727
|
+
if cmdenv.origPlace:
|
|
728
|
+
if cmdenv.startJumps and cmdenv.startJumps > 0:
|
|
729
|
+
cmdenv.origins = expandForJumps(
|
|
730
|
+
tdb, cmdenv, calc,
|
|
731
|
+
cmdenv.origPlace.system,
|
|
732
|
+
cmdenv.startJumps,
|
|
733
|
+
"--from", "starting",
|
|
734
|
+
)
|
|
735
|
+
cmdenv.origPlace = None
|
|
736
|
+
elif isinstance(cmdenv.origPlace, System):
|
|
737
|
+
cmdenv.DEBUG0("origPlace: System: {}", cmdenv.origPlace.name())
|
|
738
|
+
if not cmdenv.origPlace.stations:
|
|
739
|
+
raise CommandLineError(
|
|
740
|
+
"No stations at --from system, {}"
|
|
741
|
+
.format(cmdenv.origPlace.name())
|
|
742
|
+
)
|
|
743
|
+
cmdenv.origins = tuple(
|
|
744
|
+
station
|
|
745
|
+
for station in cmdenv.origPlace.stations
|
|
746
|
+
if station.ID in eligible_ids and checkStationSuitability(cmdenv, calc, station)
|
|
747
|
+
)
|
|
748
|
+
else:
|
|
749
|
+
cmdenv.DEBUG0("origPlace: Station: {}", cmdenv.origPlace.name())
|
|
750
|
+
checkStationSuitability(cmdenv, calc, cmdenv.origPlace, '--from')
|
|
751
|
+
cmdenv.origins = (cmdenv.origPlace,)
|
|
752
|
+
cmdenv.startStation = cmdenv.origPlace
|
|
753
|
+
checkForEmptyStationList(
|
|
754
|
+
"--from", cmdenv.origPlace,
|
|
755
|
+
cmdenv.origins, cmdenv.startJumps
|
|
756
|
+
)
|
|
757
|
+
else:
|
|
758
|
+
if cmdenv.startJumps:
|
|
759
|
+
raise CommandLineError("--start-jumps (-s) only works with --from")
|
|
760
|
+
cmdenv.DEBUG0("using all suitable origins")
|
|
761
|
+
cmdenv.origins = tuple(
|
|
762
|
+
station
|
|
763
|
+
for station in tdb.stationByID.values()
|
|
764
|
+
if station.ID in eligible_ids and checkStationSuitability(cmdenv, calc, station)
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
if not cmdenv.startJumps and isinstance(cmdenv.origPlace, System):
|
|
768
|
+
cmdenv.origins = filterStationSet(
|
|
769
|
+
'--from', cmdenv, calc, cmdenv.origins
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
cmdenv.origSystems = tuple(set(
|
|
773
|
+
stn.system for stn in cmdenv.origins
|
|
774
|
+
))
|
|
775
|
+
|
|
776
|
+
def checkDestinations(tdb, cmdenv, calc):
|
|
777
|
+
cmdenv.destinations = None
|
|
778
|
+
showProgress = bool(getattr(cmdenv, "progress", False))
|
|
779
|
+
hb_interval = 0.5
|
|
780
|
+
last_hb = 0.0
|
|
781
|
+
spinner = ("|", "/", "-", "\\")
|
|
782
|
+
spin_i = 0
|
|
783
|
+
|
|
784
|
+
def heartbeat(seen, kept):
|
|
785
|
+
nonlocal last_hb, spin_i
|
|
786
|
+
if not showProgress:
|
|
787
|
+
return
|
|
788
|
+
now = time.time()
|
|
789
|
+
if (now - last_hb) < hb_interval:
|
|
790
|
+
return
|
|
791
|
+
last_hb = now
|
|
792
|
+
s = spinner[spin_i]
|
|
793
|
+
spin_i = (spin_i + 1) % len(spinner)
|
|
794
|
+
sys.stdout.write(
|
|
795
|
+
f"\r{s} Scanning stations… examined {seen:n} kept {kept:n}"
|
|
796
|
+
)
|
|
797
|
+
sys.stdout.flush()
|
|
798
|
+
|
|
799
|
+
if cmdenv.destPlace:
|
|
800
|
+
if cmdenv.endJumps and cmdenv.endJumps > 0:
|
|
801
|
+
cmdenv.destinations = expandForJumps(
|
|
802
|
+
tdb, cmdenv, calc,
|
|
803
|
+
cmdenv.destPlace.system,
|
|
804
|
+
cmdenv.endJumps,
|
|
805
|
+
"--to", "destination",
|
|
806
|
+
)
|
|
807
|
+
cmdenv.destPlace = None
|
|
808
|
+
elif isinstance(cmdenv.destPlace, Station):
|
|
809
|
+
cmdenv.DEBUG0("destPlace: Station: {}", cmdenv.destPlace.name())
|
|
810
|
+
# Single-station --to: hard fail if unsuitable.
|
|
811
|
+
checkStationSuitability(cmdenv, calc, cmdenv.destPlace, '--to')
|
|
812
|
+
cmdenv.destinations = (cmdenv.destPlace,)
|
|
813
|
+
else:
|
|
814
|
+
cmdenv.DEBUG0("destPlace: System: {}", cmdenv.destPlace.name())
|
|
815
|
+
# System --to: examine all stations, keeping only those that pass --to suitability.
|
|
816
|
+
dests, seen, kept = [], 0, 0
|
|
817
|
+
for station in cmdenv.destPlace.stations:
|
|
818
|
+
seen += 1
|
|
819
|
+
try:
|
|
820
|
+
ok = checkStationSuitability(cmdenv, calc, station, '--to')
|
|
821
|
+
except (CommandLineError, NoDataError):
|
|
822
|
+
ok = False
|
|
823
|
+
if ok:
|
|
824
|
+
dests.append(station)
|
|
825
|
+
kept += 1
|
|
826
|
+
heartbeat(seen, kept)
|
|
827
|
+
cmdenv.destinations = tuple(dests)
|
|
828
|
+
if showProgress:
|
|
829
|
+
sys.stdout.write("\n")
|
|
830
|
+
sys.stdout.flush()
|
|
831
|
+
checkForEmptyStationList(
|
|
832
|
+
"--to", cmdenv.destPlace,
|
|
833
|
+
cmdenv.destinations, cmdenv.endJumps
|
|
834
|
+
)
|
|
835
|
+
else:
|
|
836
|
+
if cmdenv.endJumps:
|
|
837
|
+
raise CommandLineError("--end-jumps (-e) only works with --to")
|
|
838
|
+
cmdenv.DEBUG0("Using all available destinations")
|
|
839
|
+
if cmdenv.goalSystem:
|
|
840
|
+
dest = tdb.lookupPlace(cmdenv.goalSystem)
|
|
841
|
+
cmdenv.goalSystem = dest.system
|
|
842
|
+
|
|
843
|
+
if cmdenv.origPlace and cmdenv.maxJumpsPer == 0:
|
|
844
|
+
stationSrc = chain.from_iterable(
|
|
845
|
+
system.stations for system in cmdenv.origSystems
|
|
846
|
+
)
|
|
847
|
+
else:
|
|
848
|
+
stationSrc = tdb.stationByID.values()
|
|
849
|
+
|
|
850
|
+
eligible_ids = set(calc.stationsSelling) & set(calc.stationsBuying)
|
|
851
|
+
|
|
852
|
+
dests, seen, kept = [], 0, 0
|
|
853
|
+
for station in stationSrc:
|
|
854
|
+
seen += 1
|
|
855
|
+
if station.ID in eligible_ids and checkStationSuitability(cmdenv, calc, station):
|
|
856
|
+
dests.append(station)
|
|
857
|
+
kept += 1
|
|
858
|
+
heartbeat(seen, kept)
|
|
859
|
+
cmdenv.destinations = tuple(dests)
|
|
860
|
+
if showProgress:
|
|
861
|
+
sys.stdout.write("\n")
|
|
862
|
+
sys.stdout.flush()
|
|
863
|
+
|
|
864
|
+
if not cmdenv.endJumps and isinstance(cmdenv.destPlace, System):
|
|
865
|
+
cmdenv.destinations = filterStationSet(
|
|
866
|
+
'--to', cmdenv, calc, cmdenv.destinations
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
cmdenv.destSystems = tuple(set(
|
|
870
|
+
stn.system for stn in cmdenv.destinations
|
|
871
|
+
))
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
def validateRunArguments(tdb, cmdenv, calc):
|
|
875
|
+
"""
|
|
876
|
+
Process arguments to the 'run' option.
|
|
877
|
+
"""
|
|
878
|
+
|
|
879
|
+
if cmdenv.credits < 0:
|
|
880
|
+
raise CommandLineError("Invalid (negative) value for initial credits")
|
|
881
|
+
# I'm going to allow 0 credits as a future way of saying "just fly"
|
|
882
|
+
|
|
883
|
+
if cmdenv.routes < 1:
|
|
884
|
+
raise CommandLineError(
|
|
885
|
+
"Maximum routes has to be 1 or higher."
|
|
886
|
+
)
|
|
887
|
+
if cmdenv.routes > 1 and cmdenv.checklist:
|
|
888
|
+
raise CommandLineError(
|
|
889
|
+
"Checklist can only be applied to a single route."
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
if cmdenv.hops < 1:
|
|
893
|
+
raise CommandLineError("Minimum of 1 hop required")
|
|
894
|
+
if cmdenv.hops > 32:
|
|
895
|
+
raise CommandLineError("Too many hops without more optimization")
|
|
896
|
+
|
|
897
|
+
if cmdenv.maxJumpsPer < 0:
|
|
898
|
+
raise CommandLineError("Negative jumps: you're already there?")
|
|
899
|
+
if cmdenv.direct:
|
|
900
|
+
cmdenv.hops = 1
|
|
901
|
+
cmdenv.maxJumpsPer = cmdenv.maxLyPer = 10000
|
|
902
|
+
|
|
903
|
+
if cmdenv.capacity is None:
|
|
904
|
+
raise CommandLineError("Missing '--capacity'")
|
|
905
|
+
if cmdenv.maxLyPer is None and not cmdenv.direct:
|
|
906
|
+
raise CommandLineError("Missing '--ly-per'")
|
|
907
|
+
if cmdenv.capacity < 0:
|
|
908
|
+
raise CommandLineError("Invalid (negative) cargo capacity")
|
|
909
|
+
if cmdenv.capacity > 1500:
|
|
910
|
+
cmdenv.WARN("Capacity > 1500 not supported (you specified {})", cmdenv.capacity)
|
|
911
|
+
cmdenv.WARN("Forcing jumps per hop to 1.")
|
|
912
|
+
cmdenv.maxJumpsPer = 1
|
|
913
|
+
if cmdenv.hops > 2:
|
|
914
|
+
cmdenv.WARN("{} hops? Press [CTRL][C] to quit.", cmdenv.hops)
|
|
915
|
+
if not cmdenv.supply:
|
|
916
|
+
cmdenv.WARN("Please provide a '--supply' value.")
|
|
917
|
+
cmdenv.supply = cmdenv.capacity * 10
|
|
918
|
+
cmdenv.DEBUG0("'supply' minimum set to {}.", cmdenv.supply)
|
|
919
|
+
if not cmdenv.demand:
|
|
920
|
+
cmdenv.WARN("Please provide a '--demand' value.")
|
|
921
|
+
cmdenv.demand = cmdenv.capacity * 10
|
|
922
|
+
cmdenv.DEBUG0("'demand' minimum set to {}.", cmdenv.demand)
|
|
923
|
+
# raise CommandLineError(
|
|
924
|
+
# "Capacity > 1500 not supported (you specified {})"
|
|
925
|
+
# .format(cmdenv.capacity)
|
|
926
|
+
# )
|
|
927
|
+
|
|
928
|
+
if cmdenv.limit and cmdenv.limit > cmdenv.capacity:
|
|
929
|
+
raise CommandLineError("'limit' must be <= capacity")
|
|
930
|
+
if cmdenv.limit and cmdenv.limit < 0:
|
|
931
|
+
raise CommandLineError("'limit' can't be negative, silly")
|
|
932
|
+
cmdenv.maxUnits = cmdenv.limit if cmdenv.limit else cmdenv.capacity
|
|
933
|
+
|
|
934
|
+
if cmdenv.insurance:
|
|
935
|
+
arbitraryInsuranceBuffer = 42
|
|
936
|
+
if cmdenv.insurance >= (cmdenv.credits + arbitraryInsuranceBuffer):
|
|
937
|
+
raise CommandLineError("Insurance leaves no margin for trade")
|
|
938
|
+
|
|
939
|
+
if cmdenv.loop:
|
|
940
|
+
if cmdenv.unique:
|
|
941
|
+
raise CommandLineError("Cannot use --unique and --loop together")
|
|
942
|
+
if cmdenv.direct:
|
|
943
|
+
raise CommandLineError("Cannot use --direct and --loop together")
|
|
944
|
+
|
|
945
|
+
if cmdenv.loopInt:
|
|
946
|
+
if cmdenv.loopInt < 2:
|
|
947
|
+
raise CommandLineError(
|
|
948
|
+
"--loop-int must be 2 or higher to have any effect. "
|
|
949
|
+
)
|
|
950
|
+
if cmdenv.loopInt > cmdenv.hops and not cmdenv.unique:
|
|
951
|
+
cmdenv.NOTE("--loop-int > hops implies --unique")
|
|
952
|
+
cmdenv.unique = True
|
|
953
|
+
|
|
954
|
+
if cmdenv.shorten:
|
|
955
|
+
if cmdenv.loop:
|
|
956
|
+
raise CommandLineError(
|
|
957
|
+
"Cannot use --shorten and --loop together"
|
|
958
|
+
)
|
|
959
|
+
if not cmdenv.ending:
|
|
960
|
+
raise CommandLineError(
|
|
961
|
+
"--shorten only works with --to."
|
|
962
|
+
)
|
|
963
|
+
|
|
964
|
+
if cmdenv.goalSystem and not cmdenv.origPlace:
|
|
965
|
+
raise CommandLineError("--towards requires --from")
|
|
966
|
+
|
|
967
|
+
checkOrigins(tdb, cmdenv, calc)
|
|
968
|
+
checkDestinations(tdb, cmdenv, calc)
|
|
969
|
+
|
|
970
|
+
# If they're going --from and --to single systems, and they have
|
|
971
|
+
# specified zero jumps then it's futile to try anything.
|
|
972
|
+
if cmdenv.maxJumpsPer == 0 and not cmdenv.direct:
|
|
973
|
+
if len(cmdenv.origSystems) == 1 and len(cmdenv.destSystems) == 1:
|
|
974
|
+
if cmdenv.origSystems[0] != cmdenv.destSystems[0]:
|
|
975
|
+
raise CommandLineError(
|
|
976
|
+
"Could not find any connections that didn't require at "
|
|
977
|
+
"least one jump and --jumps 0 specified."
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
origins, destns = cmdenv.origins or (), cmdenv.destinations or ()
|
|
981
|
+
|
|
982
|
+
if cmdenv.hops == 1 and len(origins) == 1 and len(destns) == 1:
|
|
983
|
+
if origins == destns:
|
|
984
|
+
raise CommandLineError("Same to/from; more than one hop required.")
|
|
985
|
+
|
|
986
|
+
avoidSet = set(cmdenv.avoidPlaces or ())
|
|
987
|
+
viaSet = cmdenv.viaSet = set(cmdenv.viaPlaces)
|
|
988
|
+
cmdenv.DEBUG0("Via: {}", viaSet)
|
|
989
|
+
cmdenv.viaSet = filterStationSet('--via', cmdenv, calc, cmdenv.viaSet)
|
|
990
|
+
checkAnchorNotInVia(cmdenv.hops, "--from", cmdenv.origPlace, viaSet)
|
|
991
|
+
checkAnchorNotInVia(cmdenv.hops, "--to", cmdenv.destPlace, viaSet)
|
|
992
|
+
|
|
993
|
+
viaSystems = set()
|
|
994
|
+
for place in viaSet:
|
|
995
|
+
if place in avoidSet or place.system in avoidSet:
|
|
996
|
+
raise CommandLineError(
|
|
997
|
+
'"--via {}" conflicts with --avoid'
|
|
998
|
+
.format(place.name())
|
|
999
|
+
)
|
|
1000
|
+
if isinstance(place, Station):
|
|
1001
|
+
viaSystems.add(place.system)
|
|
1002
|
+
else:
|
|
1003
|
+
viaSystems.add(place)
|
|
1004
|
+
|
|
1005
|
+
if cmdenv.maxJumpsPer == 0 and viaSet and not cmdenv.direct:
|
|
1006
|
+
for via in viaSet:
|
|
1007
|
+
if via.system not in cmdenv.origSystems:
|
|
1008
|
+
raise CommandLineError(
|
|
1009
|
+
"--via {} unreachable with --jumps 0"
|
|
1010
|
+
.format(via.name())
|
|
1011
|
+
)
|
|
1012
|
+
cmdenv.origins = tuple(
|
|
1013
|
+
origin for origin in cmdenv.origins
|
|
1014
|
+
if origin.system in viaSystems
|
|
1015
|
+
)
|
|
1016
|
+
cmdenv.origSystems = tuple(
|
|
1017
|
+
origin.system for origin in cmdenv.origins
|
|
1018
|
+
)
|
|
1019
|
+
cmdenv.destinations = tuple(
|
|
1020
|
+
dest for dest in cmdenv.destinations
|
|
1021
|
+
if dest.system in viaSystems
|
|
1022
|
+
)
|
|
1023
|
+
cmdenv.destSystems = tuple(
|
|
1024
|
+
dest.system for dest in cmdenv.destinations
|
|
1025
|
+
)
|
|
1026
|
+
|
|
1027
|
+
# How many of the hops do not have pre-determined stations. For example,
|
|
1028
|
+
# when the user uses "--from", they pre-determine the starting station.
|
|
1029
|
+
fixedRoutePoints = 0
|
|
1030
|
+
if cmdenv.origPlace:
|
|
1031
|
+
fixedRoutePoints += 1
|
|
1032
|
+
if cmdenv.destPlace:
|
|
1033
|
+
fixedRoutePoints += 1
|
|
1034
|
+
totalRoutePoints = cmdenv.hops + 1
|
|
1035
|
+
adhocRoutePoints = totalRoutePoints - fixedRoutePoints
|
|
1036
|
+
if len(viaSystems) > adhocRoutePoints:
|
|
1037
|
+
raise CommandLineError(
|
|
1038
|
+
"Route is not long enough for the list of '--via' "
|
|
1039
|
+
"destinations you gave. Reduce the vias or try again "
|
|
1040
|
+
"with '--hops {}' or greater.\n".format(
|
|
1041
|
+
len(viaSet) + fixedRoutePoints - 1
|
|
1042
|
+
))
|
|
1043
|
+
cmdenv.adhocHops = adhocRoutePoints - 1
|
|
1044
|
+
|
|
1045
|
+
if cmdenv.unique and cmdenv.hops >= len(tdb.stationByID):
|
|
1046
|
+
raise CommandLineError(
|
|
1047
|
+
"Requested unique trip with more hops than there are stations..."
|
|
1048
|
+
)
|
|
1049
|
+
if cmdenv.unique:
|
|
1050
|
+
# if there's only one start and stop...
|
|
1051
|
+
if len(origins) == 1 and len(destns) == 1:
|
|
1052
|
+
if origins[0] is destns[0]:
|
|
1053
|
+
raise CommandLineError("Can't have same from/to with --unique")
|
|
1054
|
+
if viaSet:
|
|
1055
|
+
if len(origins) == 1 and origins[0] in viaSet:
|
|
1056
|
+
raise CommandLineError("Can't have --from station in --via list with --unique")
|
|
1057
|
+
if len(destns) == 1 and destns[0] in viaSet:
|
|
1058
|
+
raise CommandLineError("Can't have --to station in --via list with --unique")
|
|
1059
|
+
|
|
1060
|
+
if cmdenv.mfd:
|
|
1061
|
+
cmdenv.mfd.display("Loading Trades")
|
|
1062
|
+
|
|
1063
|
+
if cmdenv.pruneScores and cmdenv.pruneHops:
|
|
1064
|
+
if cmdenv.pruneScores > 99:
|
|
1065
|
+
raise CommandLineError("--prune-score value percentile exceed 99.")
|
|
1066
|
+
if cmdenv.pruneHops < 2:
|
|
1067
|
+
raise CommandLineError("--prune-hops must 2 or more.")
|
|
1068
|
+
else:
|
|
1069
|
+
cmdenv.pruneScores = cmdenv.pruneHops = 0
|
|
1070
|
+
|
|
1071
|
+
######################################################################
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
def filterByVia(routes, viaSet, viaStartPos):
|
|
1075
|
+
if not routes:
|
|
1076
|
+
return ()
|
|
1077
|
+
|
|
1078
|
+
matchedRoutes = []
|
|
1079
|
+
partialRoutes = {}
|
|
1080
|
+
maxMet = 0
|
|
1081
|
+
for route in routes:
|
|
1082
|
+
met = 0
|
|
1083
|
+
for hop in route.route[viaStartPos:]:
|
|
1084
|
+
if hop in viaSet or hop.system in viaSet:
|
|
1085
|
+
met += 1
|
|
1086
|
+
if met > 0:
|
|
1087
|
+
if met >= len(viaSet):
|
|
1088
|
+
matchedRoutes.append(route)
|
|
1089
|
+
else:
|
|
1090
|
+
if met > maxMet:
|
|
1091
|
+
partialRoutes[met] = []
|
|
1092
|
+
if met >= maxMet:
|
|
1093
|
+
maxMet = met
|
|
1094
|
+
partialRoutes[met].append(route)
|
|
1095
|
+
|
|
1096
|
+
if matchedRoutes:
|
|
1097
|
+
return matchedRoutes, None
|
|
1098
|
+
|
|
1099
|
+
if not maxMet:
|
|
1100
|
+
raise NoDataError(
|
|
1101
|
+
"No routes were found which matched your 'via' selections."
|
|
1102
|
+
)
|
|
1103
|
+
|
|
1104
|
+
return partialRoutes[maxMet], (
|
|
1105
|
+
"SORRY: No runs visited all of your via destinations. "
|
|
1106
|
+
"Listing runs that matched at least {}.".format(
|
|
1107
|
+
maxMet
|
|
1108
|
+
)
|
|
1109
|
+
)
|
|
1110
|
+
|
|
1111
|
+
|
|
1112
|
+
def checkReachability(tdb, cmdenv):
|
|
1113
|
+
if cmdenv.direct:
|
|
1114
|
+
return
|
|
1115
|
+
srcSys, dstSys = cmdenv.origSystems, cmdenv.destSystems
|
|
1116
|
+
if len(srcSys) == 1 and len(dstSys) == 1:
|
|
1117
|
+
srcSys, dstSys = srcSys[0], dstSys[0]
|
|
1118
|
+
if srcSys != dstSys:
|
|
1119
|
+
maxLyPer = cmdenv.maxLyPer
|
|
1120
|
+
avoiding = tuple(
|
|
1121
|
+
avoid for avoid in cmdenv.avoidPlaces
|
|
1122
|
+
if isinstance(avoid, System)
|
|
1123
|
+
)
|
|
1124
|
+
route = tdb.getRoute(
|
|
1125
|
+
srcSys, dstSys, maxLyPer, avoiding,
|
|
1126
|
+
)
|
|
1127
|
+
if not route:
|
|
1128
|
+
raise CommandLineError(
|
|
1129
|
+
"No route between {} and {} with a {}ly/jump limit."
|
|
1130
|
+
.format(
|
|
1131
|
+
srcSys.name(), dstSys.name(),
|
|
1132
|
+
maxLyPer,
|
|
1133
|
+
)
|
|
1134
|
+
)
|
|
1135
|
+
|
|
1136
|
+
# Were there just not enough hops?
|
|
1137
|
+
jumpLimit = cmdenv.maxJumpsPer * cmdenv.hops
|
|
1138
|
+
routeJumps = len(route) - 1
|
|
1139
|
+
if jumpLimit < routeJumps:
|
|
1140
|
+
hopsRequired = math.ceil(routeJumps / cmdenv.maxJumpsPer)
|
|
1141
|
+
jumpsRequired = math.ceil(routeJumps / cmdenv.hops)
|
|
1142
|
+
raise CommandLineError(
|
|
1143
|
+
"Shortest route between {src} and {dst} at {jumply} "
|
|
1144
|
+
"ly per jump requires at least {minjumps} jumps. "
|
|
1145
|
+
"Your current settings (--hops {hops} --jumps {jumps}) "
|
|
1146
|
+
"allows a maximum of {jumplimit}.\n"
|
|
1147
|
+
"\n"
|
|
1148
|
+
"You may need --hops={althops} or --jumps={altjumps}.\n"
|
|
1149
|
+
"\n"
|
|
1150
|
+
"See also:\n"
|
|
1151
|
+
" --towards (aka -T),"
|
|
1152
|
+
" --start-jumps (-s),"
|
|
1153
|
+
" --end-jumps (-e),"
|
|
1154
|
+
" --direct.\n"
|
|
1155
|
+
.format(
|
|
1156
|
+
src = srcSys.name(),
|
|
1157
|
+
dst = dstSys.name(),
|
|
1158
|
+
jumply = cmdenv.maxLyPer,
|
|
1159
|
+
minjumps = routeJumps,
|
|
1160
|
+
hops = cmdenv.hops,
|
|
1161
|
+
jumps = cmdenv.maxJumpsPer,
|
|
1162
|
+
jumplimit = jumpLimit,
|
|
1163
|
+
althops = hopsRequired,
|
|
1164
|
+
altjumps = jumpsRequired,
|
|
1165
|
+
)
|
|
1166
|
+
)
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
def routeFailedRestrictions(
|
|
1170
|
+
tdb, cmdenv, restrictTo, maxLs, hopNo
|
|
1171
|
+
):
|
|
1172
|
+
"""
|
|
1173
|
+
Generate exception text indicating we couldn't complete a
|
|
1174
|
+
route given the restrictions supplied. If the user has
|
|
1175
|
+
specified detail, check if there is a route at all.
|
|
1176
|
+
"""
|
|
1177
|
+
|
|
1178
|
+
places = list(
|
|
1179
|
+
set(
|
|
1180
|
+
chain.from_iterable(
|
|
1181
|
+
(place,) if isinstance(place, Station) else place.stations
|
|
1182
|
+
for place in restrictTo
|
|
1183
|
+
)
|
|
1184
|
+
)
|
|
1185
|
+
)
|
|
1186
|
+
places.sort(key = lambda stn: stn.dbname)
|
|
1187
|
+
|
|
1188
|
+
dests = ", ".join(place.name() for place in places)
|
|
1189
|
+
|
|
1190
|
+
return (
|
|
1191
|
+
"SORRY: Could not find any routes that delivered a profit to "
|
|
1192
|
+
"{} at hop #{}\n"
|
|
1193
|
+
"You may need to add more hops to your route or adjust your "
|
|
1194
|
+
"filters/restrictions.\n"
|
|
1195
|
+
.format(
|
|
1196
|
+
dests, hopNo + 1
|
|
1197
|
+
)
|
|
1198
|
+
)
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
def extraRouteProgress(routes):
|
|
1202
|
+
bestGain = max(routes, key = lambda route: route.gainCr).gainCr
|
|
1203
|
+
worstGain = min(routes, key = lambda route: route.gainCr).gainCr
|
|
1204
|
+
if bestGain != worstGain:
|
|
1205
|
+
gainText = "{:n}-{:n}cr gain".format(worstGain, bestGain)
|
|
1206
|
+
else:
|
|
1207
|
+
gainText = "{:n}cr gain".format(bestGain)
|
|
1208
|
+
|
|
1209
|
+
bestGPT = int(max(routes, key = lambda route: route.gpt).gpt)
|
|
1210
|
+
worstGPT = int(min(routes, key = lambda route: route.gpt).gpt)
|
|
1211
|
+
if bestGPT != worstGPT:
|
|
1212
|
+
gptText = "{:n}-{:n}cr/ton".format(worstGPT, bestGPT)
|
|
1213
|
+
else:
|
|
1214
|
+
gptText = "{:n}cr/ton".format(bestGPT)
|
|
1215
|
+
|
|
1216
|
+
return ".. {}, {}".format(gainText, gptText)
|
|
1217
|
+
|
|
1218
|
+
######################################################################
|
|
1219
|
+
# Perform query and populate result set
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
def run(results, cmdenv, tdb):
|
|
1223
|
+
cmdenv.DEBUG1("loading trades")
|
|
1224
|
+
|
|
1225
|
+
if tdb.tradingCount == 0:
|
|
1226
|
+
raise NoDataError("Database does not contain any profitable trades.")
|
|
1227
|
+
|
|
1228
|
+
# Always show a friendly heads-up before heavy work begins.
|
|
1229
|
+
print("Searching for quality trades. This may take a few minutes. Please be patient.", flush=True)
|
|
1230
|
+
|
|
1231
|
+
# Instantiate the calculator object
|
|
1232
|
+
calc = TradeCalc(tdb, cmdenv)
|
|
1233
|
+
|
|
1234
|
+
validateRunArguments(tdb, cmdenv, calc)
|
|
1235
|
+
|
|
1236
|
+
origPlace, viaSet = cmdenv.origPlace, cmdenv.viaSet
|
|
1237
|
+
stopStations = cmdenv.destinations
|
|
1238
|
+
goalSystem = cmdenv.goalSystem
|
|
1239
|
+
maxLs = cmdenv.maxLs
|
|
1240
|
+
|
|
1241
|
+
# seed the route table with starting places
|
|
1242
|
+
startCr = cmdenv.credits - cmdenv.insurance
|
|
1243
|
+
routes = [
|
|
1244
|
+
Route(
|
|
1245
|
+
stations = (src,),
|
|
1246
|
+
hops = (),
|
|
1247
|
+
jumps = (),
|
|
1248
|
+
startCr = startCr,
|
|
1249
|
+
gainCr = 0,
|
|
1250
|
+
score = 0,
|
|
1251
|
+
)
|
|
1252
|
+
for src in cmdenv.origins
|
|
1253
|
+
]
|
|
1254
|
+
|
|
1255
|
+
numHops = cmdenv.hops
|
|
1256
|
+
lastHop = numHops - 1
|
|
1257
|
+
viaStartPos = 1 if origPlace else 0
|
|
1258
|
+
|
|
1259
|
+
cmdenv.DEBUG1("numHops {}, vias {}, adhocHops {}",
|
|
1260
|
+
numHops, len(viaSet), cmdenv.adhocHops)
|
|
1261
|
+
|
|
1262
|
+
results.summary = ResultRow()
|
|
1263
|
+
results.summary.exception = ""
|
|
1264
|
+
|
|
1265
|
+
if cmdenv.loop:
|
|
1266
|
+
routePickPred = lambda route: \
|
|
1267
|
+
route.lastStation is route.firstStation
|
|
1268
|
+
elif cmdenv.shorten:
|
|
1269
|
+
if not cmdenv.destPlace:
|
|
1270
|
+
routePickPred = lambda route: \
|
|
1271
|
+
route.lastStation is route.firstStation
|
|
1272
|
+
elif isinstance(cmdenv.destPlace, System):
|
|
1273
|
+
routePickPred = lambda route: \
|
|
1274
|
+
route.lastSystem is cmdenv.destPlace
|
|
1275
|
+
else:
|
|
1276
|
+
routePickPred = lambda route: \
|
|
1277
|
+
route.lastStation is cmdenv.destPlace
|
|
1278
|
+
else:
|
|
1279
|
+
routePickPred = None
|
|
1280
|
+
|
|
1281
|
+
pickedRoutes = []
|
|
1282
|
+
|
|
1283
|
+
pruneMod = cmdenv.pruneScores / 100
|
|
1284
|
+
|
|
1285
|
+
if cmdenv.loop:
|
|
1286
|
+
distancePruning = lambda rt, distLeft: \
|
|
1287
|
+
rt.lastSystem.distanceTo(rt.firstSystem) <= distLeft
|
|
1288
|
+
elif cmdenv.destPlace and not cmdenv.direct:
|
|
1289
|
+
distancePruning = lambda rt, distLeft: \
|
|
1290
|
+
any(
|
|
1291
|
+
stop for stop in stopSystems
|
|
1292
|
+
if rt.lastSystem.distanceTo(stop) <= distLeft
|
|
1293
|
+
)
|
|
1294
|
+
else:
|
|
1295
|
+
distancePruning = False
|
|
1296
|
+
|
|
1297
|
+
if distancePruning:
|
|
1298
|
+
maxHopDistLy = cmdenv.maxJumpsPer * cmdenv.maxLyPer
|
|
1299
|
+
if not cmdenv.loop:
|
|
1300
|
+
stopSystems = {stop.system for stop in stopStations}
|
|
1301
|
+
|
|
1302
|
+
for hopNo in range(numHops):
|
|
1303
|
+
restrictTo = None
|
|
1304
|
+
if hopNo == lastHop and stopStations:
|
|
1305
|
+
restrictTo = set(stopStations)
|
|
1306
|
+
manualRestriction = bool(cmdenv.destPlace)
|
|
1307
|
+
elif len(viaSet) > cmdenv.adhocHops:
|
|
1308
|
+
restrictTo = viaSet
|
|
1309
|
+
manualRestriction = True
|
|
1310
|
+
|
|
1311
|
+
if distancePruning:
|
|
1312
|
+
preCrop = len(routes)
|
|
1313
|
+
distLeft = maxHopDistLy * (numHops - hopNo)
|
|
1314
|
+
routes[:] = [rt for rt in routes if distancePruning(rt, distLeft)]
|
|
1315
|
+
if not routes:
|
|
1316
|
+
if pickedRoutes:
|
|
1317
|
+
break
|
|
1318
|
+
raise NoDataError(
|
|
1319
|
+
"No routes are in-range of any end stations at the end of hop {}"
|
|
1320
|
+
.format(hopNo)
|
|
1321
|
+
)
|
|
1322
|
+
if (pruned := preCrop - len(routes)):
|
|
1323
|
+
cmdenv.NOTE("Pruned {} origins too far from any end stations", pruned)
|
|
1324
|
+
|
|
1325
|
+
if hopNo >= 1 and (cmdenv.maxRoutes or pruneMod):
|
|
1326
|
+
routes.sort()
|
|
1327
|
+
if pruneMod and hopNo + 1 >= cmdenv.pruneHops and len(routes) > 10:
|
|
1328
|
+
crop = int(len(routes) * pruneMod)
|
|
1329
|
+
routes[:] = routes[:-crop]
|
|
1330
|
+
cmdenv.NOTE("Pruned {} origins", crop)
|
|
1331
|
+
|
|
1332
|
+
if cmdenv.maxRoutes and len(routes) > cmdenv.maxRoutes:
|
|
1333
|
+
routes[:] = routes[:cmdenv.maxRoutes]
|
|
1334
|
+
|
|
1335
|
+
if cmdenv.progress:
|
|
1336
|
+
extra = ""
|
|
1337
|
+
if hopNo > 0 and cmdenv.detail > 1:
|
|
1338
|
+
extra = extraRouteProgress(routes)
|
|
1339
|
+
print(
|
|
1340
|
+
"* Hop {:3n}: {:.>10n} origins {}"
|
|
1341
|
+
.format(hopNo + 1, len(routes), extra)
|
|
1342
|
+
)
|
|
1343
|
+
elif cmdenv.debug:
|
|
1344
|
+
cmdenv.DEBUG0("Hop {}...", hopNo + 1)
|
|
1345
|
+
|
|
1346
|
+
try:
|
|
1347
|
+
newRoutes = calc.getBestHops(routes, restrictTo = restrictTo)
|
|
1348
|
+
|
|
1349
|
+
except KeyboardInterrupt:
|
|
1350
|
+
cmdenv.DEBUG0("** Keyboard Interrupt")
|
|
1351
|
+
if hopNo == 0 or not routes:
|
|
1352
|
+
raise UserAbortedRun("before any routes calculated")
|
|
1353
|
+
# Until python 3.14 it's discouraged to break from an exception, so
|
|
1354
|
+
# lets make sure we don't mistake there being anything to process.
|
|
1355
|
+
calc.aborted = True
|
|
1356
|
+
newRoutes = []
|
|
1357
|
+
|
|
1358
|
+
except NoHopsError:
|
|
1359
|
+
if hopNo == 0 and len(cmdenv.origSystems) == 1:
|
|
1360
|
+
raise NoDataError(
|
|
1361
|
+
"Couldn't find any trading links within {} x {}ly jumps of {}."
|
|
1362
|
+
.format(
|
|
1363
|
+
cmdenv.maxJumpsPer,
|
|
1364
|
+
cmdenv.maxLyPer,
|
|
1365
|
+
cmdenv.origSystems[0].name(),
|
|
1366
|
+
)
|
|
1367
|
+
)
|
|
1368
|
+
raise NoDataError(
|
|
1369
|
+
"No routes had reachable trading links at hop #{}".format(hopNo + 1)
|
|
1370
|
+
)
|
|
1371
|
+
|
|
1372
|
+
if calc.aborted:
|
|
1373
|
+
cmdenv.DEBUG0("** User Aborted")
|
|
1374
|
+
break
|
|
1375
|
+
|
|
1376
|
+
if not newRoutes:
|
|
1377
|
+
assert not calc.aborted, "internal error"
|
|
1378
|
+
# First attempt to find a route is a special case because the current
|
|
1379
|
+
# route list is the source.
|
|
1380
|
+
if hopNo == 0:
|
|
1381
|
+
no_routes_on_first_hop(cmdenv, calc)
|
|
1382
|
+
# no return
|
|
1383
|
+
|
|
1384
|
+
# If we've already got some winners (e.g. on --shorten)
|
|
1385
|
+
if pickedRoutes:
|
|
1386
|
+
break
|
|
1387
|
+
|
|
1388
|
+
checkReachability(tdb, cmdenv)
|
|
1389
|
+
|
|
1390
|
+
if restrictTo and manualRestriction:
|
|
1391
|
+
results.summary.exception += routeFailedRestrictions(
|
|
1392
|
+
tdb, cmdenv, restrictTo, maxLs, hopNo
|
|
1393
|
+
)
|
|
1394
|
+
break
|
|
1395
|
+
|
|
1396
|
+
results.summary.exception += f"SORRY: Could not find profitable destinations beyond hop #{hopNo+1:n}\n"
|
|
1397
|
+
break
|
|
1398
|
+
|
|
1399
|
+
routes[:] = newRoutes
|
|
1400
|
+
if goalSystem:
|
|
1401
|
+
# Promote the winning route to the top of the list
|
|
1402
|
+
# while leaving the remainder of the list intact
|
|
1403
|
+
routes.sort(
|
|
1404
|
+
key = lambda route:
|
|
1405
|
+
0 if route.lastSystem is goalSystem else 1
|
|
1406
|
+
)
|
|
1407
|
+
if routes[0].lastSystem is goalSystem:
|
|
1408
|
+
cmdenv.NOTE("Goal system reached!")
|
|
1409
|
+
routes = routes[:1]
|
|
1410
|
+
break
|
|
1411
|
+
|
|
1412
|
+
if calc.aborted:
|
|
1413
|
+
break
|
|
1414
|
+
|
|
1415
|
+
if routePickPred:
|
|
1416
|
+
pickedRoutes.extend(
|
|
1417
|
+
route for route in routes if routePickPred(route)
|
|
1418
|
+
)
|
|
1419
|
+
|
|
1420
|
+
if cmdenv.loop or cmdenv.shorten:
|
|
1421
|
+
cmdenv.DEBUG0("Using {} picked routes", len(pickedRoutes))
|
|
1422
|
+
routes = pickedRoutes
|
|
1423
|
+
# normalise the scores for fairness...
|
|
1424
|
+
for route in routes:
|
|
1425
|
+
cmdenv.DEBUG0(
|
|
1426
|
+
"{} hops, {} score, {} gpt",
|
|
1427
|
+
len(route.hops), route.score, route.gpt
|
|
1428
|
+
)
|
|
1429
|
+
route.score /= len(route.hops)
|
|
1430
|
+
|
|
1431
|
+
if not routes:
|
|
1432
|
+
if calc.aborted:
|
|
1433
|
+
raise UserAbortedRun("before any routes found")
|
|
1434
|
+
raise NoDataError(
|
|
1435
|
+
"No profitable trades matched your critera, or price data along the route is missing."
|
|
1436
|
+
)
|
|
1437
|
+
|
|
1438
|
+
if viaSet:
|
|
1439
|
+
routes, caution = filterByVia(routes, viaSet, viaStartPos)
|
|
1440
|
+
if caution:
|
|
1441
|
+
results.summary.exception += caution + "\n"
|
|
1442
|
+
|
|
1443
|
+
routes.sort()
|
|
1444
|
+
results.data = routes
|
|
1445
|
+
|
|
1446
|
+
if calc.aborted:
|
|
1447
|
+
results.summary.exception += str(UserAbortedRun("results may be incomplete or inaccurate")) + "\n"
|
|
1448
|
+
|
|
1449
|
+
return results
|
|
1450
|
+
|
|
1451
|
+
|
|
1452
|
+
def no_routes_on_first_hop(cmdenv: TradeEnv, calc: TradeCalc) -> None:
|
|
1453
|
+
""" handle the special case where run found no routes on the first hop. """
|
|
1454
|
+
# Is it because you ctrl-c'd?
|
|
1455
|
+
if calc.aborted:
|
|
1456
|
+
raise UserAbortedRun("during first hop before any routes found")
|
|
1457
|
+
|
|
1458
|
+
# The raw name they provide with --from is stored as cmdenv.starting, and resolved
|
|
1459
|
+
# to a System or Station in cmdenv.origPlace, however checkOrigins may set that to
|
|
1460
|
+
# None if we're doing --start-jumps to indicate there's no "single" origin. So we
|
|
1461
|
+
# saved a copy of it to cmdenv._origin.
|
|
1462
|
+
start_place = getattr(cmdenv, "_origin")
|
|
1463
|
+
if not start_place:
|
|
1464
|
+
# Ok, we were doing some kind of open-ended galaxy wide query
|
|
1465
|
+
raise NoDataError("Could not find any trade links in the galaxy with those criteria.")
|
|
1466
|
+
|
|
1467
|
+
# Find the system name - all "locations" have a system property including Systems.
|
|
1468
|
+
start_system = start_place.system.name()
|
|
1469
|
+
|
|
1470
|
+
# How far did you say you were willing to go?
|
|
1471
|
+
max_ly = cmdenv.maxJumpsPer * cmdenv.maxLyPer
|
|
1472
|
+
|
|
1473
|
+
errText = (
|
|
1474
|
+
f"No suitable and profitable buyers found at/relative to {start_place}.\n"
|
|
1475
|
+
"\n"
|
|
1476
|
+
"You may want to try:\n"
|
|
1477
|
+
f" {sys.argv[0]} local \"{start_system}\" --ly {max_ly} -vv --stations --trading"
|
|
1478
|
+
)
|
|
1479
|
+
|
|
1480
|
+
# If they had specified a station, give them a little extra help.
|
|
1481
|
+
if isinstance(start_place, Station):
|
|
1482
|
+
errText += (
|
|
1483
|
+
"\n"
|
|
1484
|
+
"or:\n"
|
|
1485
|
+
f" {sys.argv[0]} market \"{start_place}\" --sell -vv"
|
|
1486
|
+
)
|
|
1487
|
+
|
|
1488
|
+
raise NoDataError(errText)
|
|
1489
|
+
|
|
1490
|
+
|
|
1491
|
+
######################################################################
|
|
1492
|
+
# Transform result set into output
|
|
1493
|
+
|
|
1494
|
+
|
|
1495
|
+
def render(results, cmdenv, tdb):
|
|
1496
|
+
if (exception := results.summary.exception.strip()):
|
|
1497
|
+
style = ""
|
|
1498
|
+
lines = exception.split("\n")
|
|
1499
|
+
max_line_len = max(len(line) for line in lines)
|
|
1500
|
+
if cmdenv.color:
|
|
1501
|
+
style = "yellow on grey15" # yellow on a darkish background, so we're sure it's not on a light background
|
|
1502
|
+
# Pad all the lines to the same length
|
|
1503
|
+
exception = "\n".join(f"{line:{max_line_len}s}" for line in lines)
|
|
1504
|
+
|
|
1505
|
+
# TODO: should use a rich panel when --color is set
|
|
1506
|
+
cmdenv.console.print('#' * max_line_len, style=style)
|
|
1507
|
+
cmdenv.console.print(exception, style=style)
|
|
1508
|
+
cmdenv.console.print('#' * max_line_len, style=style)
|
|
1509
|
+
# Ring the console bell and add a blank line
|
|
1510
|
+
cmdenv.console.print("\a")
|
|
1511
|
+
|
|
1512
|
+
routes = results.data
|
|
1513
|
+
|
|
1514
|
+
for i in range(min(len(routes), cmdenv.routes)):
|
|
1515
|
+
print(routes[i].detail(cmdenv))
|
|
1516
|
+
|
|
1517
|
+
# User wants to be guided through the route.
|
|
1518
|
+
if cmdenv.checklist:
|
|
1519
|
+
assert cmdenv.routes == 1
|
|
1520
|
+
cl = Checklist(tdb, cmdenv)
|
|
1521
|
+
cl.run(routes[0], cmdenv.credits)
|