diff --git a/CHANGES.txt b/CHANGES.txt index 12d5c764..fcd646e7 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -2,11 +2,33 @@ TradeDangerous, Copyright (C) Oliver "kfsone" Smith, July 2014 ============================================================================== +v6.13.0 Mar 03 2015 +. (kfsone) Added "modified" column to ShipVendor table, +. (kfsone) "maddavo" import plugin: + - Added "--opt=shipvendors" which imports his ShipVendor.csv, + - Entries with a 'modified' of "DELETED" will be + - Added "--opt=csvonly" to stop after importing any csv files (no prices), + - Added "--opt=csvs" to import all the csvs + (equivalent to typing --opt=systems --opt=stations --opt=shipvendor) +. (kfsone) "market" sub-command: + - Default behavior is equivalent to "--buy --sell", + - You now only need to specify --buy (-B) or --sell (-S) to list ONLY + those columns, so "trade.py market SOL -vv" now shows both sets of data. +. (kfsone) "local" sub-command: + - Added "--stations" option: only list systems with stations, + - Added "--trading" option: only list stations that are flagged as having + a market or have trade data available + - Added "--blackmarket" option: only list stations with a black market, + - Added "--shipyard" option: only list stations with a ship yard, +. (kfsone) "run" sub-command: + - check from stations for whether they have anything that can be bought, + - better feedback on some edge-cases where a route cannot be found, + - fixed some problems with --via, + - better feedback when using --jumps=0 or --ly=0, + v6.12.4 Mar 02 2015 . (kfsone) Added 175 Systems, . (kfsone) Fixed #193 "run" was ignoring --ls-max, -. (kfsone) Added "--opt=shipvendors" to the maddavo plugin, -. (kfsone) Added "--opt=csvonly" to maddavo plugin (stop after csv imports), + DRy411S : ShipVendors v6.12.3 Mar 01 2015 diff --git a/README.txt b/README.txt index 0ec296d0..9dadc577 100644 --- a/README.txt +++ b/README.txt @@ -213,7 +213,7 @@ RUN sub-command: How many credits to start with e.g. --credits 20000 - + --ly-per N.NN --ly N.NN Maximum distance your ship can jump between systems at full capacity. @@ -551,11 +551,12 @@ IMPORT sub-command: systems: Merge maddavo's System data into local db, stations: Merge maddavo's Station data into local db, shipvendors: Merge maddavo's ShipVendor data into local db, + csvs: Merge all of the above exportcsv: Regenerate System and Station .csv files after merging System/Station data. csvonly: Stop after importing CSV files, no prices, skipdl: Skip doing any downloads. - force: Process prices even if timestamps suggest + force: Process prices even if timestamps suggest there is no new data. use3h: Force download of the 3-hours .prices file use2d: Force download of the 2-days .prices file @@ -664,18 +665,18 @@ MARKET sub-command: Lists items bought / sold at a given station; with --detail (-v) also includes the average price for those items. - trade.py market [--buy] [--sell] [--detail] + trade.py market [--buy | --sell] [--detail] station Name of the station to list, e.g. "paes/ramon" or "ramoncity", --buy -B - List items bought by the station (listed as 'SELL' in-game) + List only items bought by the station (listed as 'SELL' in-game) --sell -S - List items sold by the station (listed as 'BUY' in-game) + List only items sold by the station (listed as 'BUY' in-game) --detail -v @@ -778,13 +779,24 @@ LOCAL sub-command: --pad-size SML? --pad SML? -p - Limit results to stations that match one of the pad sizes - specified. + Limit stations to those that match one of the pad sizes specified. --pad ML? (med, lrg or unknown only) - -o ML? "" "" "" "" + -p ML? "" "" "" "" --pad ? (unknown only), --pad L (large only, ignores unknown) + --stations + Limit results to systems which have stations + + --trading + Limit stations to those which which have markets or trade data. + + --shipyard + Limit stations to those known to have a shipyard. + + --blackmarket + Limit stations to those known to have a black market. + -v Show stations + their distance from star @@ -825,13 +837,29 @@ LOCAL sub-command: Adding detail ('-vv' or '-v -v' or '--detail --detail') would add a count of the number of items we have prices for at each station. + > trade.py local LAVE --trading --ly 4 -vv + System Dist + / Station StnLs Age/days Mkt BMk Shp Pad Itms + ----------------------------------------------------------- + LAVE 0.00 + / Castellan Station 2.34K 2.57 Yes No No Med 37 + / Lave Station 299 7.79 Yes Yes Yes Lrg 33 + / Warinus 863 7.76 Yes Yes No Med 38 + DISO 3.59 + / Shifnalport 284 0.57 Yes Yes Yes Lrg 34 + LEESTI 3.91 + / George Lucas 255 0.58 Yes Yes Yes Lrg 52 + / Kolmogorov Hub 2.96K 1.61 Yes Yes No Med 53 + + > trade.py local SOL --blackmarket --ly 6 -vv + BUY sub-command: Finds stations that are selling / where you can buy, a named list of items or ships. - - trade.py buy + + trade.py buy [-q | -v] [--quantity Q] [--near N] [--ly-per N] [-P | -S] [--limit] [--one-stop | -1] diff --git a/commands/exceptions.py b/commands/exceptions.py index eb04390d..54a83e7b 100644 --- a/commands/exceptions.py +++ b/commands/exceptions.py @@ -37,16 +37,13 @@ def __init__(self, errorStr): self.errorStr = errorStr def __str__(self): return "Error: {}\n".format(self.errorStr) + (""" -This could be due to a lack of price or station data. You -may want to consult the "local -vv" sub-command to see if -there are stations in the area with price data. +Possible causes: +- No profitable runs or routes meet your criteria, +- Missing Systems or Stations along the route (see "local -vv"), +- Missing price data (see "market -vv" or "update -h"), -It can also be caused by a lack of any profitable runs -that match the criteria you specified. - -See '{} update -h' for help entering/updating prices, or -obtain a crowd-sourced '.prices' file from the web, such -as maddavo's (http://www.davek.com.au/td/)". +If you are not sure where to get data from, consider using a crowd-sourcing +project such as "maddavo's" (http://www.davek.com.au/td/). For more help, see the TradeDangerous Wiki: http://kfs.org/td/wiki diff --git a/commands/import_cmd.py b/commands/import_cmd.py index 0e9000f5..e6108879 100644 --- a/commands/import_cmd.py +++ b/commands/import_cmd.py @@ -26,7 +26,12 @@ "is removed, but other stations are not affected." ) name='import' -epilog=None +epilog=( + "This sub-command provides a plugin infrastructure, and comes " + "with a module to import data from Maddavo's Market Share " + "(http://www.davek.com.au/td/).\n" + "See \"import --plug=maddavo --opt=help\" for more help." +) wantsTradeDB=False arguments = [ ] diff --git a/commands/local_cmd.py b/commands/local_cmd.py index bcea527c..865c92b2 100644 --- a/commands/local_cmd.py +++ b/commands/local_cmd.py @@ -36,6 +36,23 @@ metavar='PADSIZES', dest='padSize', ), + ParseArgument('--stations', + help='Limit to systems which have stations.', + action='store_true', + ), + ParseArgument('--trading', + help='Limit stations to ones with price data or flagged as having ' + 'a market.', + action='store_true', + ), + ParseArgument('--blackmarket', + help='Limit stations to those known to have a black market.', + action='store_true', + ), + ParseArgument('--shipyard', + help='Limit stations to those known to have a ship yard.', + action='store_true', + ), ] ###################################################################### @@ -74,16 +91,28 @@ def run(results, cmdenv, tdb): for ID, age in tdb.query(stmt) } + wantStations = cmdenv.stations padSize = cmdenv.padSize + wantTrading = cmdenv.trading + wantShipYard = cmdenv.shipyard + wantBlackMarket = cmdenv.wantBlackMarket + + def station_filter(system): + for station in system.stations: + if wantTrading and not station.isTrading: + continue + if station.blackMarket != 'Y' and wantBlackMarket: + continue + if station.shipyard != 'Y' and wantShipYard: + continue + if padSize and not station.checkPadSize(padSize): + continue + yield station + for (system, dist) in sorted(distances.items(), key=lambda x: x[1]): - row = ResultRow() - row.system = system - row.dist = dist - row.stations = [] - if showStations: - for (station) in system.stations: - if padSize and not station.checkPadSize(padSize): - continue + if showStations or wantStations: + stations = [] + for (station) in station_filter(system): try: age = "{:7.2f}".format(ages[station.ID]) except: @@ -92,7 +121,14 @@ def run(results, cmdenv, tdb): station=station, age=age, ) - row.stations.append(rr) + stations.append(rr) + if not stations and wantStations: + continue + + row = ResultRow() + row.system = system + row.dist = dist + row.stations = stations if showStations else [] results.rows.append(row) return results diff --git a/commands/market_cmd.py b/commands/market_cmd.py index 9acd8699..b45e8b1a 100644 --- a/commands/market_cmd.py +++ b/commands/market_cmd.py @@ -21,16 +21,18 @@ ), ] switches = [ - ParseArgument( - '--buying', '-B', - help='Show items station is buying', - action='store_true', + MutuallyExclusiveGroup( + ParseArgument( + '--buying', '-B', + help='Show items station is buying', + action='store_true', + ), + ParseArgument( + '--selling', '-S', + help='Show items station is selling', + action='store_true', + ), ), - ParseArgument( - '--selling', '-S', - help='Show items station is selling', - action='store_true', - ) ] ###################################################################### @@ -54,11 +56,6 @@ def run(results, cmdenv, tdb): ) buying, selling = cmdenv.buying, cmdenv.selling - if not buying and not selling: - raise CommandLineError( - "Please specify one or both of --buying (-B) " - "or --selling (-S)." - ) results.summary = ResultRow() results.summary.origin = origin @@ -105,7 +102,7 @@ def run(results, cmdenv, tdb): row.buyLevel = level row.demand = render_units(units, level) row.buyAge = float(next(it) or 0) - if buying: + if not selling: hasBuy = (row.buyCr or units or level) else: hasBuy = False @@ -116,7 +113,7 @@ def run(results, cmdenv, tdb): row.sellLevel = level row.supply = render_units(units, level) row.sellAge = float(next(it) or 0) - if selling: + if not buying: hasSell = (row.sellCr or units or level) else: hasSell = False @@ -154,7 +151,7 @@ def render(results, cmdenv, tdb): rowFmt.addColumn('Item', '<', longestLen, key=lambda row: row.item.name()) - if cmdenv.buying: + if not cmdenv.selling: rowFmt.addColumn('Buying', '>', 7, 'n', key=lambda row: row.buyCr, pred=buyPred) @@ -170,7 +167,7 @@ def render(results, cmdenv, tdb): rowFmt.addColumn('Age/Days', '>', 7, '.2f', key=lambda row: row.buyAge, pred=buyPred) - if cmdenv.selling: + if not cmdenv.buying: rowFmt.addColumn('Selling', '>', 7, 'n', key=lambda row: row.sellCr, pred=sellPred) diff --git a/commands/run_cmd.py b/commands/run_cmd.py index 1b2faa2a..914c3664 100644 --- a/commands/run_cmd.py +++ b/commands/run_cmd.py @@ -1,11 +1,10 @@ -from __future__ import absolute_import, with_statement, print_function, division, unicode_literals from commands.commandenv import ResultRow from commands.exceptions import * from commands.parsing import MutuallyExclusiveGroup, ParseArgument from itertools import chain from formatting import RowFormat, ColumnFormat from tradedb import TradeDB, System, Station, describeAge -from tradecalc import TradeCalc, Route +from tradecalc import TradeCalc, Route, NoHopsError import math @@ -96,13 +95,15 @@ default=None, ), ParseArgument('--start-jumps', '-s', - help='Consider stations within this many jumps of the origin (requires --from).', + help='Consider stations within this many jumps of the origin ' + '(requires --from).', dest='startJumps', default=0, type=int, ), ParseArgument('--end-jumps', '-e', - help='Consider stations within this many jumps of the destination (requires --to).', + help='Consider stations within this many jumps of the destination ' + '(requires --to).', dest='endJumps', default=0, type=int, @@ -396,7 +397,7 @@ def expandForJumps(tdb, cmdenv, origins, jumps, srcName): for sys in origSys: for stn in sys.stations: if stn.itemCount and stn not in avoidPlaces: - origins.append(stn) + origins.append(stn) if cmdenv.debug: cmdenv.DEBUG0( @@ -445,22 +446,22 @@ def checkAnchorNotInVia(hops, anchorName, place, viaSet): return if isinstance(place, Station) and place in viaSet: raise CommandLineError( - "{} used in {} and --via with only 2 hops".format( - place.name(), - anchorName, + "{} used in {} and --via with only 2 hops".format( + place.name(), + anchorName, )) -def checkStationSuitability(cmdenv, station, src=None): - if station in cmdenv.avoidPlaces: - if src and src != "--from": +def checkStationSuitability(cmdenv, calc, station, src=None): + if station in cmdenv.avoidPlaces and src != "--from": + if src: raise CommandLineError( "{} station {} is marked to avoid" .format(src, station.name()) ) return False - if station.system in cmdenv.avoidPlaces: - if src and src != "--from": + if station.system in cmdenv.avoidPlaces and src != "--from": + if src: raise CommandLineError( "{} station {} is in system listed in --avoid" .format(src, station.name()) @@ -477,59 +478,74 @@ def checkStationSuitability(cmdenv, station, src=None): if not station.itemCount: if src: raise NoDataError( - "No price data in local database " - "for {} station: {}".format( - src, station.name(), + "No price data in local database " + "for {} station: {}".format( + src, station.name(), )) return False + if src != "--to" and station.ID not in calc.stationsSelling: + if src: + raise NoDataError( + "No buying prices at {}." + .format(station.name()) + ) + return False + if src != "--from" and station.ID not in calc.stationsBuying: + if src: + raise NoDataError( + "No selling prices at {}." + .format(station.name()) + ) + return False mps = cmdenv.padSize if mps and not station.checkPadSize(mps): if src: raise CommandLineError( - "{} station {} does not meet pad-size requirement.\n" - "You specified: {}, Current data for station: {} ({})\n" - "You can use \"trade.py station\" to correct this.".format( - src, station.name(), - mps, station.maxPadSize, - TradeDB.padSizesExt[station.maxPadSize], + "{} station {} does not meet pad-size requirement.\n" + "You specified: {}, Current data for station: {} ({})\n" + "You can use \"trade.py station\" to correct this.".format( + src, station.name(), + mps, station.maxPadSize, + TradeDB.padSizesExt[station.maxPadSize], )) return False bm = cmdenv.blackMarket if bm and station.blackMarket != 'Y': if src and src != "--from": raise CommandLineError( - "{} station {} does not meet black-market " - "requirement.".format( - src, station.name(), + "{} station {} does not meet black-market " + "requirement.".format( + src, station.name(), )) return False mls = cmdenv.maxLs if mls and (station.lsFromStar <= 0 or station.lsFromStar > mls): if src and src != "--from": raise CommandLineError( - "{} station {} does not meet max-ls " - "requirement.".format( - src, station.name(), + "{} station {} does not meet max-ls " + "requirement.".format( + src, station.name(), )) return False maxAge = cmdenv.maxAge if maxAge and station.dataAge > maxAge: if src and src != "--from": raise CommandLineError( - "{} station {} does not meet --age " - "requirement.".format( - src, station.name(), + "{} station {} does not meet --age " + "requirement.".format( + src, station.name(), )) return False return True -def filterStationSet(src, cmdenv, stnList): +def filterStationSet(src, cmdenv, calc, stnList): if not stnList: return stnList filtered = [ place for place in stnList - if isinstance(place, System) or checkStationSuitability(cmdenv, place) + if isinstance(place, System) or \ + checkStationSuitability(cmdenv, calc, place, src) ] if not stnList: raise CommandLineError( @@ -539,7 +555,7 @@ def filterStationSet(src, cmdenv, stnList): return stnList -def checkOrigins(tdb, cmdenv): +def checkOrigins(tdb, cmdenv, calc): if cmdenv.origPlace: if isinstance(cmdenv.origPlace, System): cmdenv.DEBUG0("origPlace: System: {}", cmdenv.origPlace.name()) @@ -551,11 +567,11 @@ def checkOrigins(tdb, cmdenv): cmdenv.origins = [ station for station in cmdenv.origPlace.stations - if checkStationSuitability(cmdenv, station) + if checkStationSuitability(cmdenv, calc, station) ] else: cmdenv.DEBUG0("origPlace: Station: {}", cmdenv.origPlace.name()) - checkStationSuitability(cmdenv, cmdenv.origPlace, '--from') + checkStationSuitability(cmdenv, calc, cmdenv.origPlace, '--from') cmdenv.origins = [ cmdenv.origPlace ] cmdenv.startStation = cmdenv.origPlace cmdenv.origins = expandForJumps( @@ -573,32 +589,34 @@ def checkOrigins(tdb, cmdenv): cmdenv.origins = [ station for station in tdb.stationByID.values() - if checkStationSuitability(cmdenv, station) + if checkStationSuitability(cmdenv, calc, station) ] if cmdenv.startJumps: raise CommandLineError("--start-jumps (-s) only works with --from") if isinstance(cmdenv.origPlace, System) and not cmdenv.startJumps: - cmdenv.origins = filterStationSet('--from', cmdenv, cmdenv.origins) + cmdenv.origins = filterStationSet( + '--from', cmdenv, calc, cmdenv.origins + ) cmdenv.origSystems = list(set( stn.system for stn in cmdenv.origins )) -def checkDestinations(tdb, cmdenv): +def checkDestinations(tdb, cmdenv, calc): cmdenv.destinations = None if cmdenv.destPlace: if isinstance(cmdenv.destPlace, Station): cmdenv.DEBUG0("destPlace: Station: {}", cmdenv.destPlace.name()) - checkStationSuitability(cmdenv, cmdenv.destPlace, '--to') + checkStationSuitability(cmdenv, calc, cmdenv.destPlace, '--to') cmdenv.destinations = [ cmdenv.destPlace ] else: cmdenv.DEBUG0("destPlace: System: {}", cmdenv.destPlace.name()) cmdenv.destinations = [ station for station in cmdenv.destPlace.stations - if checkStationSuitability(cmdenv, station) + if checkStationSuitability(cmdenv, calc, station) ] cmdenv.destinations = expandForJumps( tdb, cmdenv, @@ -621,7 +639,7 @@ def checkDestinations(tdb, cmdenv): cmdenv.goalSystem = dest.system if cmdenv.origPlace and cmdenv.maxJumpsPer == 0: - stations = chain.from_iterable( + stationSrc = chain.from_iterable( system.stations for system in cmdenv.origSystems ) else: @@ -630,21 +648,19 @@ def checkDestinations(tdb, cmdenv): cmdenv.destinations = [ station for station in stationSrc - if checkStationSuitability(cmdenv, station) + if checkStationSuitability(cmdenv, calc, station) ] if isinstance(cmdenv.destPlace, System) and not cmdenv.endJumps: cmdenv.destinations = filterStationSet( - '--to', - cmdenv, - cmdenv.destinations + '--to', cmdenv, calc, cmdenv.destinations ) cmdenv.destSystems = list(set( stn.system for stn in cmdenv.destinations )) -def validateRunArguments(tdb, cmdenv): +def validateRunArguments(tdb, cmdenv, calc): """ Process arguments to the 'run' option. """ @@ -654,9 +670,13 @@ def validateRunArguments(tdb, cmdenv): # I'm going to allow 0 credits as a future way of saying "just fly" if cmdenv.routes < 1: - raise CommandLineError("Maximum routes has to be 1 or higher") + raise CommandLineError( + "Maximum routes has to be 1 or higher." + ) if cmdenv.routes > 1 and cmdenv.checklist: - raise CommandLineError("Checklist can only be applied to a single route.") + raise CommandLineError( + "Checklist can only be applied to a single route." + ) if cmdenv.hops < 1: raise CommandLineError("Minimum of 1 hop required") @@ -674,9 +694,11 @@ def validateRunArguments(tdb, cmdenv): raise CommandLineError("Missing '--ly-per'") if cmdenv.capacity < 0: raise CommandLineError("Invalid (negative) cargo capacity") - if cmdenv.capacity > 1000: - raise CommandLineError("Capacity > 1000 not supported (you specified {})".format( - cmdenv.capacity)) + if cmdenv.capacity > 1200: + raise CommandLineError( + "Capacity > 1200 not supported (you specified {})" + .format( cmdenv.capacity) + ) if cmdenv.limit and cmdenv.limit > cmdenv.capacity: raise CommandLineError("'limit' must be <= capacity") @@ -684,22 +706,23 @@ def validateRunArguments(tdb, cmdenv): raise CommandLineError("'limit' can't be negative, silly") cmdenv.maxUnits = cmdenv.limit if cmdenv.limit else cmdenv.capacity - arbitraryInsuranceBuffer = 42 - if cmdenv.insurance and cmdenv.insurance >= (cmdenv.credits + arbitraryInsuranceBuffer): - raise CommandLineError("Insurance leaves no margin for trade") + if cmdenv.insurance: + arbitraryInsuranceBuffer = 42 + if cmdenv.insurance >= (cmdenv.credits + arbitraryInsuranceBuffer): + raise CommandLineError("Insurance leaves no margin for trade") - checkOrigins(tdb, cmdenv) - - checkDestinations(tdb, cmdenv) + checkOrigins(tdb, cmdenv, calc) + checkDestinations(tdb, cmdenv, calc) # If they're going --from and --to single systems, and they have # specified zero jumps then it's futile to try anything. - if cmdenv.jumps == 0: + if cmdenv.maxJumpsPer == 0 and not cmdenv.direct: if len(cmdenv.origSystems) == 1 and len(cmdenv.destSystems) == 1: - raise CommandLineError( - "Could not find any connections that didn't require at least " - "one jump and --jumps 0 specified." - ) + if cmdenv.origSystems[0] != cmdenv.destSystems[0]: + raise CommandLineError( + "Could not find any connections that didn't require at " + "least one jump and --jumps 0 specified." + ) origins, destns = cmdenv.origins or [], cmdenv.destinations or [] @@ -710,6 +733,10 @@ def validateRunArguments(tdb, cmdenv): avoidSet = set(cmdenv.avoidPlaces or []) viaSet = cmdenv.viaSet = set(cmdenv.viaPlaces) cmdenv.DEBUG0("Via: {}", viaSet) + cmdenv.viaSet = filterStationSet('--via', cmdenv, calc, cmdenv.viaSet) + checkAnchorNotInVia(cmdenv.hops, "--from", cmdenv.origPlace, viaSet) + checkAnchorNotInVia(cmdenv.hops, "--to", cmdenv.destPlace, viaSet) + viaSystems = set() for place in viaSet: if place in avoidSet or place.system in avoidSet: @@ -722,10 +749,27 @@ def validateRunArguments(tdb, cmdenv): else: viaSystems.add(place) - cmdenv.viaSet = filterStationSet('--via', cmdenv, cmdenv.viaSet) - - checkAnchorNotInVia(cmdenv.hops, "--from", cmdenv.origPlace, viaSet) - checkAnchorNotInVia(cmdenv.hops, "--to", cmdenv.destPlace, viaSet) + if cmdenv.maxJumpsPer == 0 and viaSet and not cmdenv.direct: + for via in viaSet: + if via.system not in cmdenv.origSystems: + raise CommandLineError( + "--via {} unreachable with --jumps 0" + .format(via.name()) + ) + cmdenv.origins = [ + origin for origin in cmdenv.origins + if origin.system in viaSystems + ] + cmdenv.origSystems = [ + origin.system for origin in cmdenv.origins + ] + cmdenv.destinations = [ + dest for dest in cmdenv.destinations + if destination.system in viaSystems + ] + cmdenv.destSystems = [ + dest.system for dest in cmdenv.destinations + ] # How many of the hops do not have pre-determined stations. For example, # when the user uses "--from", they pre-determine the starting station. @@ -812,6 +856,8 @@ def filterByVia(routes, viaSet, viaStartPos): ) def checkReachability(tdb, cmdenv): + if cmdenv.direct: + return srcSys, dstSys = cmdenv.origSystems, cmdenv.destSystems if len(srcSys) == 1 and len(dstSys) == 1: srcSys, dstSys = srcSys[0], dstSys[0] @@ -906,7 +952,10 @@ def run(results, cmdenv, tdb): if tdb.tradingCount == 0: raise NoDataError("Database does not contain any profitable trades.") - validateRunArguments(tdb, cmdenv) + # Instantiate the calculator object + calc = TradeCalc(tdb, cmdenv) + + validateRunArguments(tdb, cmdenv, calc) origPlace, viaSet = cmdenv.origPlace, cmdenv.viaSet avoidPlaces = cmdenv.avoidPlaces @@ -914,24 +963,23 @@ def run(results, cmdenv, tdb): goalSystem = cmdenv.goalSystem maxLs = cmdenv.maxLs - startCr = cmdenv.credits - cmdenv.insurance - # seed the route table with starting places - maxPadSize = cmdenv.padSize.upper() if cmdenv.padSize else None + startCr = cmdenv.credits - cmdenv.insurance routes = [ - Route(stations=[src], hops=[], jumps=[], startCr=startCr, gainCr=0, score=0) - for src in cmdenv.origins - if (src not in avoidPlaces) and \ - (src.system not in avoidPlaces) and \ - (src.checkPadSize(maxPadSize)) + Route( + stations=[src], + hops=[], + jumps=[], + startCr=startCr, + gainCr=0, + score=0 + ) + for src in cmdenv.origins ] + numHops = cmdenv.hops lastHop = numHops - 1 viaStartPos = 1 if origPlace else 0 - cmdenv.maxJumps = None - - # Instantiate the calculator object - calc = TradeCalc(tdb, cmdenv) cmdenv.DEBUG1("numHops {}, vias {}, adhocHops {}", numHops, len(viaSet), cmdenv.adhocHops) @@ -965,7 +1013,21 @@ def run(results, cmdenv, tdb): elif cmdenv.debug: cmdenv.DEBUG0("Hop {}...", hopNo+1) - newRoutes = calc.getBestHops(routes, restrictTo=restrictTo) + try: + newRoutes = calc.getBestHops(routes, restrictTo=restrictTo) + except NoHopsError: + if hopNo == 0 and len(cmdenv.origSystems) == 1: + raise NoDataError( + "Couldn't find any trading links within {} x {}ly jumps of {}." + .format( + cmdenv.maxJumpsPer, + cmdenv.maxLyPer, + cmdenv.origSystems[0].name(), + ) + ) + raise NoDataError( + "No routes had reachable trading links at hop #{}".format(hopNo + 1) + ) if not newRoutes: checkReachability(tdb, cmdenv) if hopNo > 0: @@ -980,14 +1042,39 @@ def run(results, cmdenv, tdb): .format(hopNo + 1) ) break + if hopNo == 0: + if cmdenv.origPlace and len(routes) == 1: + errText = ( + "No profitable buyers found for the goods at {}.\n" + "\n" + "You may want to try:\n" + " {} local \"{}\" --ly {} -vv --stations --trading" + .format( + routes[0].lastStation.name(), + sys.argv[0], cmdenv.origPlace.system.name(), + cmdenv.maxJumpsPer * cmdenv.maxLyPer, + ) + ) + if isinstance(cmdenv.origPlace, Station): + errText += ( + "\n" + "or:\n" + " {} market \"{}\" --sell -vv" + .format( + sys.argv[0], cmdenv.origPlace.name(), + ) + ) + raise NoDataError(errText) routes = newRoutes if routes and goalSystem: + # Promote the winning route to the top of the list + # while leaving the remainder of the list intact routes.sort( key=lambda route: - 0 if route.route[-1].system is goalSystem else 1 + 0 if route.lastSystem is goalSystem else 1 ) - if routes[0].route[-1].system is goalSystem: + if routes[0].lastSystem is goalSystem: cmdenv.NOTE("Goal system reached!") break @@ -1021,7 +1108,7 @@ def render(results, cmdenv, tdb): routes = results.data - for i in range(min(len(routes), cmdenv.routes)): + for i in range(min(len(routes), cmdenv.routes)): print(routes[i].detail(cmdenv)) # User wants to be guided through the route. diff --git a/plugins/maddavo_plug.py b/plugins/maddavo_plug.py index 27a819ac..57c81ec7 100644 --- a/plugins/maddavo_plug.py +++ b/plugins/maddavo_plug.py @@ -44,9 +44,11 @@ class ImportPlugin(plugins.ImportPluginBase): dateRe = re.compile(r"(\d\d\d\d-\d\d-\d\d)[ T](\d\d:\d\d:\d\d)") pluginOptions = { - 'systems': "Merge maddavo's System data into local db.", - 'stations': "Merge maddavo's Station data into local db.", - 'shipvendors': "Merge maddavo's ShipVendor data into local db.", + 'csvs': "Merge System, Station and ShipVendor data into " + "the local db.", + 'systems': "Merge System data into local db.", + 'stations': "Merge Station data into local db.", + 'shipvendors': "Merge ShipVendor data into local db.", 'exportcsv': "Regenerate System and Station .csv files after " "merging System/Station data.", 'csvonly': "Stop after csv work, don't import prices", @@ -446,6 +448,10 @@ def run(self): skipDownload = self.getOption("skipdl") forceParse = self.getOption("force") or skipDownload + if self.getOption("csvs"): + self.options["systems"] = True + self.options["stations"] = True + self.options["shipvendors"] = True # Ensure the cache is built and reloaded. tdb.reloadCache() diff --git a/scripts/trade.bat b/scripts/trade.bat index d94bfc9f..276cde24 100644 --- a/scripts/trade.bat +++ b/scripts/trade.bat @@ -10,7 +10,7 @@ rem --== Set default values above to prevent being asked each run ==-- set /P update=Update database from maddavo? (Y\N): if /I not "%update%"=="Y" goto menu :update -..\trade.py import --plug=maddavo --option=stations --option=systems --opt=usefull -v +..\trade.py import --plug=maddavo --opt=stations --opt=usefull -v ..\trade.py import --plug=maddavo --opt=use2d -v ..\trade.py import --plug=maddavo --opt=use3h -v echo Update Complete diff --git a/scripts/tradebrute.bat b/scripts/tradebrute.bat index 4f5cee48..a889807b 100644 --- a/scripts/tradebrute.bat +++ b/scripts/tradebrute.bat @@ -23,7 +23,7 @@ If %update% ==y (goto :update) else (goto :Setup) :update -%python%\python.exe trade.py import --plug=maddavo --opt=systems --opt=stations +%python%\python.exe trade.py import --plug=maddavo --opt=csvs goto :Setup diff --git a/tradecalc.py b/tradecalc.py index 7e47a08d..d3a96c15 100644 --- a/tradecalc.py +++ b/tradecalc.py @@ -80,6 +80,9 @@ def __str__(self): ) +class NoHopsError(TradeException): + pass + ###################################################################### # Stuff that passes for classes (but isn't) @@ -134,6 +137,7 @@ class Route(object): __slots__ = ('route', 'hops', 'startCr', 'gainCr', 'jumps', 'score') def __init__(self, stations, hops, startCr, gainCr, jumps, score): + assert stations self.route = stations self.hops = hops self.startCr = startCr @@ -141,6 +145,26 @@ def __init__(self, stations, hops, startCr, gainCr, jumps, score): self.jumps = jumps self.score = score + @property + def firstStation(self): + """ Returns the first station in the route. """ + return self.route[0] + + @property + def firstSystem(self): + """ Returns the first system in the route. """ + return self.route[0].system + + @property + def lastStation(self): + """ Returns the last station in the route. """ + return self.route[-1] + + @property + def lastSystem(self): + """ Returns the last system in the route. """ + return self.route[-1].system + def plus(self, dst, hop, jumps, score): """ Returns a new route describing the sum of this route plus a new hop. @@ -163,7 +187,7 @@ def __eq__(self, rhs): return self.score == rhs.score and len(self.jumps) == len(rhs.jumps) def str(self): - return "%s -> %s" % (self.route[0].name(), self.route[-1].name()) + return "%s -> %s" % (self.firstStation.name(), self.lastStation.name()) def detail(self, tdenv): """ @@ -172,7 +196,7 @@ def detail(self, tdenv): detail, goalSystem = tdenv.detail, tdenv.goalSystem - credits = self.startCr + credits = self.startCr + (tdenv.insurance or 0) gainCr = 0 route = self.route @@ -317,11 +341,12 @@ def goalDistance(station): gainCr += hopGainCr - if route[-1].system is not goalSystem: - text += goalDistance(route[-1]) + lastStation = self.lastStation + if lastStation.system is not goalSystem: + text += goalDistance(lastStation) text += footer or "" text += endFmt.format( - station=decorateStation(route[-1]), + station=decorateStation(lastStation), gain=gainCr, credits=credits + gainCr ) @@ -760,7 +785,7 @@ def getBestHops(self, routes, restrictTo=None): if stn not in avoidPlaces and \ stn.system not in avoidPlaces ) - def _get_destinations(srcStation): + def station_iterator(srcStation): srcSys = srcStation.system srcDist = srcSys.distanceTo for stn in restrictStations: @@ -771,8 +796,9 @@ def _get_destinations(srcStation): srcDist(stnSys) ) else: - def _get_destinations(srcStation): - yield from tdb.getDestinations( + getDestinations = tdb.getDestinations + def station_iterator(srcStation): + yield from getDestinations( srcStation, maxJumps=maxJumpsPer, maxLyPer=maxLyPer, @@ -780,23 +806,23 @@ def _get_destinations(srcStation): maxPadSize=maxPadSize, maxLsFromStar=maxLsFromStar, ) - - station_iterator = _get_destinations prog = pbar.Progress(len(routes), 25) + connections = 0 + getSelling = self.stationsSelling.get for route in routes: if tdenv.progress: prog.increment(1) tdenv.DEBUG1("Route = {}", route.str()) - srcStation = route.route[-1] + srcStation = route.lastStation srcTradingWith = srcStation.tradingWith - if srcStation.tradingWith is None: + if srcTradingWith is None: srcTradingWith = srcStation.tradingWith = dict() startCr = credits + int(route.gainCr * safetyMargin) routeJumps = len(route.jumps) - srcSelling = self.stationsSelling.get(srcStation.ID, None) + srcSelling = getSelling(srcStation.ID, None) if not srcSelling: tdenv.DEBUG1("Nothing sold - next.") continue @@ -808,7 +834,7 @@ def _get_destinations(srcStation): pass if goalSystem: - origSystem = route.route[0].system + origSystem = route.firstSystem srcSystem = srcStation.system srcGoalDist = srcSystem.distanceTo(goalSystem) srcOrigDist = srcSystem.distanceTo(origSystem) @@ -877,6 +903,8 @@ def considerStation(dstStation, dest, multiplier): if reqBlackMarket and dstStation.blackMarket != 'Y': continue + connections += 1 + if maxAge: stnDataAge = dstStation.dataAge if stnDataAge is None or stnDataAge > maxAge: @@ -926,6 +954,11 @@ def considerStation(dstStation, dest, multiplier): prog.clear() + if connections == 0: + raise NoHopsError( + "No destinations could be reached within the constraints." + ) + result = [] for (dst, route, trade, jumps, ly, score) in bestToDest.values(): result.append(route.plus(dst, trade, jumps, score)) diff --git a/tradedb.py b/tradedb.py index b67bad61..834af95c 100644 --- a/tradedb.py +++ b/tradedb.py @@ -343,6 +343,15 @@ def distFromStar(self, addSuffix=False): return '{:n}K'.format(int(ls / 1000))+suffix return '{:.2f}ly'.format(ls / (365*24*60*60)) + def isTrading(self): + """ + True if the station is thought to be trading. + + A station is considered 'trading' if it has an item count > 0 or + if it's "market" column is flagged 'Y'. + """ + return (self.itemCount > 0 or self.market == 'Y') + def str(self): return self.dbname @@ -1675,17 +1684,13 @@ def getDestinations( stnLs = station.lsFromStar if stnLs <= 0 or stnLs > maxLsFromStar: continue - destStations.append( - Destination( + yield Destination( node.system, station, node.via, node.distLy - ) ) - return destStations - ############################################################ # Ship data.