diff --git a/CHANGES.txt b/CHANGES.txt index b8d50292..728252c0 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -2,6 +2,17 @@ TradeDangerous, Copyright (C) Oliver "kfsone" Smith, July 2014 ============================================================================== +v6.12.3 Mar 01 2015 +. (kfsone) Improved how we handle some edge cases with --from and --to, +. (kfsone) Improved feedback when requesting an unreachable journey, + e.g. --from selianciens --to eravate --ly 9.23 --hops 2 --jumps 2 + (the journey requires at least 9 jumps but the options only allow 4), +. (kfsone) Fixed #185 --from and --to giving a "set" error, +. (kfsone) Fixed #188 Allow multiple ships per "shipvendor", +. (kfsone) Fixed #190 "station -r" wasn't updated the .csv file, +. (kfsone) Cleaned up the output of "local" command when showing stations, ++ DRy411S : ShipVendors + v6.12.2 Feb 26 2015 . (kfsone) "run" command: - added "--direct" option: diff --git a/commands/local_cmd.py b/commands/local_cmd.py index a65954aa..bcea527c 100644 --- a/commands/local_cmd.py +++ b/commands/local_cmd.py @@ -1,11 +1,11 @@ from __future__ import absolute_import, with_statement, print_function, division, unicode_literals from commands.commandenv import ResultRow from commands.parsing import MutuallyExclusiveGroup, ParseArgument -from formatting import RowFormat, ColumnFormat +from formatting import RowFormat, ColumnFormat, max_len +from itertools import chain from tradedb import TradeDB from tradeexcept import TradeException -import itertools import math ###################################################################### @@ -108,12 +108,10 @@ def render(results, cmdenv, tdb): )) # Compare system names so we can tell - longestNamed = max(results.rows, - key=lambda row: len(row.system.name())) - longestNameLen = max(len(longestNamed.system.name()), 16) + maxSysLen = max_len(results.rows, key=lambda row: row.system.name()) sysRowFmt = RowFormat().append( - ColumnFormat("System", '<', longestNameLen, + ColumnFormat("System", '<', maxSysLen, key=lambda row: row.system.name()) ).append( ColumnFormat("Dist", '>', '7', '.2f', @@ -122,11 +120,24 @@ def render(results, cmdenv, tdb): showStations = cmdenv.detail if showStations: + maxStnLen = max_len( + chain.from_iterable( + row.system.stations for row in results.rows + ), + key=lambda row: row.dbname + ) + maxLsLen = max_len( + chain.from_iterable( + row.system.stations for row in results.rows + ), + key=lambda row: row.distFromStar() + ) + maxLsLen = max(maxLsLen, 5) stnRowFmt = RowFormat(prefix=' / ').append( - ColumnFormat("Station", '<', 32, + ColumnFormat("Station", '<', maxStnLen + 1, key=lambda row: row.station.str()) ).append( - ColumnFormat("StnLs", '>', '10', + ColumnFormat("StnLs", '>', maxLsLen, key=lambda row: row.station.distFromStar()) ).append( ColumnFormat("Age/days", '>', 7, diff --git a/commands/run_cmd.py b/commands/run_cmd.py index c58962e8..c2998e7a 100644 --- a/commands/run_cmd.py +++ b/commands/run_cmd.py @@ -7,6 +7,8 @@ from tradedb import TradeDB, System, Station, describeAge from tradecalc import TradeCalc, Route +import math + ###################################################################### # Parser config @@ -29,19 +31,19 @@ ] switches = [ - ParseArgument('--from', + ParseArgument('--from', '-f', help='Starting system/station.', dest='starting', metavar='STATION', ), MutuallyExclusiveGroup( - ParseArgument('--to', + ParseArgument('--to', '-t', help='Final system/station.', dest='ending', metavar='PLACE', default=None, ), - ParseArgument('--towards', + ParseArgument('--towards', '-T', help=( 'Choose a route that continually reduces the ' 'distance towards this system.' @@ -234,9 +236,20 @@ def __init__(self, tdb, cmdenv): def doStep(self, action, detail=None, extra=None): self.stepNo += 1 try: - self.mfd.display("#{} {}".format(self.stepNo, action), detail or "", extra or "") - except AttributeError: pass - input(" {:<3}: {}: ".format(self.stepNo, " ".join([item for item in [action, detail, extra] if item]))) + self.mfd.display( + "#{} {}".format(self.stepNo, action), + detail or "", + extra or "" + ) + except AttributeError: + pass + input( + " {:<3}: {}: " + .format( + self.stepNo, + " ".join(item for item in [action, detail, extra] if item) + ) + ) def note(self, str, addBreak=True): @@ -261,7 +274,11 @@ def run(self, route, cr): for idx in range(lastHopIdx): hopNo = idx + 1 cur, nxt, hop = stations[idx], stations[idx + 1], hops[idx] - sortedTradeOptions = sorted(hop[0], key=lambda tradeOption: tradeOption[1] * tradeOption[0].gainCr, reverse=True) + sortedTradeOptions = sorted( + hop[0], + key=lambda tradeOption: \ + tradeOption[1] * tradeOption[0].gainCr, reverse=True + ) # Tell them what they need to buy. if cmdenv.detail: @@ -281,7 +298,12 @@ def run(self, route, cr): print() # If there is a next hop, describe how to get there. - self.note("Fly {}".format(" -> ".join([ jump.name() for jump in jumps[idx] ]))) + self.note( + "Fly {}" + .format( + " -> ".join(jump.name() for jump in jumps[idx]) + ) + ) if idx < len(hops) and jumps[idx]: for jump in jumps[idx][1:]: self.doStep('Jump to', jump.name()) @@ -436,6 +458,20 @@ def checkAnchorNotInVia(hops, anchorName, place, viaSet): def checkStationSuitability(cmdenv, station, src=None): + if station in cmdenv.avoidPlaces: + if src and src != "--from": + raise CommandLineError( + "{} station {} is marked to avoid" + .format(src, station.name()) + ) + return False + if station.system in cmdenv.avoidPlaces: + if src and src != "--from": + raise CommandLineError( + "{} station {} is in system listed in --avoid" + .format(src, station.name()) + ) + return False if station.market == 'N': if src: raise CommandLineError( @@ -509,30 +545,7 @@ def filterStationSet(src, cmdenv, stnList): return stnList -def validateRunArguments(tdb, cmdenv): - """ - Process arguments to the 'run' option. - """ - - if cmdenv.credits < 0: - raise CommandLineError("Invalid (negative) value for initial credits") - # 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") - if cmdenv.routes > 1 and cmdenv.checklist: - raise CommandLineError("Checklist can only be applied to a single route.") - - if cmdenv.hops < 1: - raise CommandLineError("Minimum of 1 hop required") - if cmdenv.hops > 32: - raise CommandLineError("Too many hops without more optimization") - - if cmdenv.maxJumpsPer < 0: - raise CommandLineError("Negative jumps: you're already there?") - if cmdenv.direct: - cmdenv.hops = 1 - +def checkOrigins(tdb, cmdenv): if cmdenv.origPlace: if isinstance(cmdenv.origPlace, System): cmdenv.DEBUG0("origPlace: System: {}", cmdenv.origPlace.name()) @@ -571,6 +584,15 @@ def validateRunArguments(tdb, cmdenv): 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.origSystems = set( + stn.system for stn in cmdenv.origins + ) + + +def checkDestinations(tdb, cmdenv): cmdenv.destinations = None if cmdenv.destPlace: if isinstance(cmdenv.destPlace, Station): @@ -603,47 +625,114 @@ def validateRunArguments(tdb, cmdenv): raise CommandLineError("--towards requires --from") dest = tdb.lookupPlace(cmdenv.goalSystem) cmdenv.goalSystem = dest.system + + if cmdenv.origPlace and cmdenv.maxJumpsPer == 0: + stations = chain.from_iterable( + system.stations for system in cmdenv.origSystems + ) + else: + stationSrc = tdb.stationByID.values() + cmdenv.destinations = [ station - for station in tdb.stationByID.values() + for station in stationSrc if checkStationSuitability(cmdenv, station) ] + if isinstance(cmdenv.destPlace, System) and not cmdenv.endJumps: + cmdenv.destinations = filterStationSet( + '--to', + cmdenv, + cmdenv.destinations + ) + + cmdenv.destSystems = set( + stn.system for stn in cmdenv.destinations + ) + +def validateRunArguments(tdb, cmdenv): + """ + Process arguments to the 'run' option. + """ + + if cmdenv.credits < 0: + raise CommandLineError("Invalid (negative) value for initial credits") + # 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") + if cmdenv.routes > 1 and cmdenv.checklist: + raise CommandLineError("Checklist can only be applied to a single route.") + + if cmdenv.hops < 1: + raise CommandLineError("Minimum of 1 hop required") + if cmdenv.hops > 32: + raise CommandLineError("Too many hops without more optimization") + + if cmdenv.maxJumpsPer < 0: + raise CommandLineError("Negative jumps: you're already there?") + if cmdenv.direct: + cmdenv.hops = 1 + + if cmdenv.capacity is None: + raise CommandLineError("Missing '--capacity'") + if cmdenv.maxLyPer is None and not cmdenv.direct: + 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.limit and cmdenv.limit > cmdenv.capacity: + raise CommandLineError("'limit' must be <= capacity") + if cmdenv.limit and cmdenv.limit < 0: + 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") + + checkOrigins(tdb, cmdenv) + + checkDestinations(tdb, cmdenv) + + # 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 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." + ) + origins, destns = cmdenv.origins or [], cmdenv.destinations or [] if cmdenv.hops == 1 and len(origins) == 1 and len(destns) == 1: if origins == destns: raise CommandLineError("Same to/from; more than one hop required.") + avoidSet = set(cmdenv.avoidPlaces or []) viaSet = cmdenv.viaSet = set(cmdenv.viaPlaces) cmdenv.DEBUG0("Via: {}", viaSet) viaSystems = set() for place in viaSet: + if place in avoidSet or place.system in avoidSet: + raise CommandLineError( + '"--via {}" conflicts with --avoid' + .format(place.name()) + ) if isinstance(place, Station): - if not place.itemCount: - raise NoDataError( - "No price data available for via station {}.".format( - place.name() - )) viaSystems.add(place.system) 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) - avoids = cmdenv.avoidPlaces or [] - for via in viaSet: - if isinstance(via, Station): - conflict = (via in avoids or via.system in avoids) - else: - conflict = (via in avoids) - if conflict: - raise CommandLineError( - "Via {} conflicts with avoid list".format( - via - )) - # How many of the hops do not have pre-determined stations. For example, # when the user uses "--from", they pre-determine the starting station. fixedRoutePoints = 0 @@ -662,35 +751,10 @@ def validateRunArguments(tdb, cmdenv): )) cmdenv.adhocHops = adhocRoutePoints - 1 - if cmdenv.capacity is None: - raise CommandLineError("Missing '--capacity'") - if cmdenv.maxLyPer is None and not cmdenv.direct: - 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.limit and cmdenv.limit > cmdenv.capacity: - raise CommandLineError("'limit' must be <= capacity") - if cmdenv.limit and cmdenv.limit < 0: - 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") - - # Filter from, via and to stations based on additional user criteria: - if not isinstance(cmdenv.origPlace, Station) and not cmdenv.startJumps: - cmdenv.origins = filterStationSet('--from', cmdenv, cmdenv.origins) - if not isinstance(cmdenv.destPlace, Station) and not cmdenv.endJumps: - cmdenv.destinations = filterStationSet('--to', cmdenv, cmdenv.destinations) - cmdenv.viaSet = filterStationSet('--via', cmdenv, cmdenv.viaSet) - if cmdenv.unique and cmdenv.hops >= len(tdb.stationByID): - raise CommandLineError("Requested unique trip with more hops than there are stations...") + raise CommandLineError( + "Requested unique trip with more hops than there are stations..." + ) if cmdenv.unique: # if there's only one start and stop... if len(origins) == 1 and len(destns) == 1: @@ -753,6 +817,91 @@ def filterByVia(routes, viaSet, viaStartPos): ) ) +def checkReachability(tdb, cmdenv): + srcSys, dstSys = cmdenv.origSystems, cmdenv.destSystems + if len(srcSys) == 1 and len(dstSys) == 1: + srcSys, dstSys = next(iter(srcSys)), next(iter(dstSys)) + if srcSys != dstSys: + maxLyPer = cmdenv.maxLyPer + avoiding = [ + avoid for avoid in cmdenv.avoidPlaces + if isinstance(avoid, System) + ] + route = tdb.getRoute( + srcSys, dstSys, maxLyPer, avoiding, + ) + if not route: + raise CommandLineError( + "No route between {} and {} with a {}ly/jump limit." + .format( + srcSys.name(), dstSys.name(), + maxLyPer, + ) + ) + + # Were there just not enough hops? + jumpLimit = cmdenv.maxJumpsPer * cmdenv.hops + if jumpLimit < len(route): + routeJumps = len(route) - 1 + hopsRequired = math.ceil(routeJumps / cmdenv.maxJumpsPer) + jumpsRequired = math.ceil(routeJumps / cmdenv.hops) + raise CommandLineError( + "Shortest route between {src} and {dst} at {jumply} " + "ly per jump requires at least {minjumps} jumps. " + "Your current settings (--hops {hops} --jumps {jumps}) " + "allows a maximum of {jumplimit}.\n" + "\n" + "You may need --hops={althops} or --jumps={altjumps}.\n" + "\n" + "See also:\n" + " --towards (aka -T)," + " --start-jumps (-s)," + " --end-jumps (-e)," + " --direct.\n" + .format( + src=srcSys.name(), + dst=dstSys.name(), + jumply=cmdenv.maxLyPer, + minjumps=routeJumps, + hops=cmdenv.hops, + jumps=cmdenv.maxJumpsPer, + jumplimit=jumpLimit, + althops=hopsRequired, + altjumps=jumpsRequired, + ) + ) + + +def routeFailedRestrictions( + tdb, cmdenv, restrictTo, maxLs, hopNo + ): + """ + Generate exception text indicating we couldn't complete a + route given the restrictions supplied. If the user has + specified detail, check if there is a route at all. + """ + + places = list( + set( + chain.from_iterable( + [place] if isinstance(place, Station) else place.stations + for place in restrictTo + ) + ) + ) + places.sort(key=lambda stn: stn.dbname) + + dests = ", ".join(place.name() for place in places) + + return ( + "SORRY: Could not find any routes that delivered a profit to " + "{} at hop #{}\n" + "You may need to add more hops to your route or adjust your " + "filters/restrictions.\n" + .format( + dests, hopNo + 1 + ) + ) ###################################################################### # Perform query and populate result set @@ -823,43 +972,21 @@ def run(results, cmdenv, tdb): cmdenv.DEBUG0("Hop {}...", hopNo+1) newRoutes = calc.getBestHops(routes, restrictTo=restrictTo) - if not newRoutes and hopNo > 0: - if restrictTo: - restrictTo = set(chain.from_iterable( - [place] if isinstance(place, Station) else place.stations - for place in restrictTo - )) - if not maxLs: - lsCheck = lambda stn: True - else: - lsCheck = lambda stn: \ - stn.maxLsFromStar > 0 and \ - stn.maxLsFromStar < maxLs - restrictTo = set( - stn for stn in restrictTo - if stn not in avoidPlaces - and stn.system not in avoidPlaces - and stn.checkPadSize(maxPadSize) - and lsCheck(stn) - ) - dests = ", ".join([ - place.name() for place in restrictTo[0:-1] - ]) - if len(restrictTo) > 1: - dests += " or " + restrictTo[-1].name() + if not newRoutes: + checkReachability(tdb, cmdenv) + if hopNo > 0: + if restrictTo: + results.summary.exception += routeFailedRestrictions( + tdb, cmdenv, restrictTo, maxLs, hopNo + ) + break results.summary.exception += ( - "SORRY: Could not find any routes that " - "delivered a profit to {} at hop #{}\n" - "You may need to add more hops to your route.\n" - .format( - dests, hopNo + 1 - ) + "SORRY: Could not find profitable destinations " + "beyond hop #{:n}\n" + .format(hopNo + 1) ) break - results.summary.exception += ( - "SORRY: Could not find routes beyond hop #%d\n" % (hopNo + 1) - ) - break + routes = newRoutes if routes and goalSystem: routes.sort( diff --git a/commands/shipvendor_cmd.py b/commands/shipvendor_cmd.py index 3eeac635..5ddbe805 100644 --- a/commands/shipvendor_cmd.py +++ b/commands/shipvendor_cmd.py @@ -2,6 +2,7 @@ from commands.commandenv import ResultRow from commands.exceptions import CommandLineError from commands.parsing import MutuallyExclusiveGroup, ParseArgument +from itertools import chain from tradedb import AmbiguityError from tradedb import System, Station from tradedb import TradeDB @@ -21,15 +22,15 @@ arguments = [ ParseArgument( 'origin', - help='Specify the full name of the station (SYS NAME/STN NAME is also supported).', + help='Specify the full name of the station ' + '(SYS NAME/STN NAME is also supported).', metavar='STATIONNAME', type=str, ), ParseArgument( 'ship', - help='Ship name', - metavar='SHIPTYPE', - type=str, + help='Comma or space separated list of ship names.', + nargs='+', ), ] switches = [ @@ -85,18 +86,15 @@ def removeShipVendor(tdb, cmdenv, station, ship): return ship -def checkResultAndExportShipVendors(tdb, cmdenv, result): - if not result: - return None +def maybeExportToCSV(tdb, cmdenv): if cmdenv.noExport: cmdenv.DEBUG0("no-export set, not exporting stations") - return None + return lines, csvPath = csvexport.exportTableToFile(tdb, cmdenv, "ShipVendor") cmdenv.NOTE("{} updated.", csvPath) - return None -def checkShipPresent(tdb, ship, station): +def checkShipPresent(tdb, station, ship): # Ask the database how many rows it sees claiming # this ship is sold at that station. The value will # be zero if we don't have an entry, otherwise it @@ -125,32 +123,51 @@ def run(results, cmdenv, tdb): station.name() )) - try: - ship = tdb.lookupShip(cmdenv.ship) - except LookupError: - raise CommandLineError("Unrecognized Ship: {}".format(cmdenv.station)) - - # Lets see if that ship sails from the specified port. - shipPresent = checkShipPresent(tdb, ship, station) - if cmdenv.add: - if shipPresent: - raise CommandLineError( - "{} is already listed at {}" - .format(ship.name(), station.name()) - ) - result = addShipVendor(tdb, cmdenv, station, ship) - return checkResultAndExportShipVendors(tdb, cmdenv, result) + action = addShipVendor elif cmdenv.remove: - if not shipPresent: - raise CommandLineError( - "{} is not listed at {}" - .format(ship.name(), station.name()) - ) - result = removeShipVendor(tdb, cmdenv, station, ship) - return checkResultAndExportShipVendors(tdb, cmdenv, result) + action = removeShipVendor else: + ###TODO: + ### if not cmdenv.ship: + ### List ships at this station. raise CommandLineError( "You must specify --add or --remove" ) + ships = {} + shipNames = chain.from_iterable( + name.split(",") for name in cmdenv.ship + ) + for shipName in shipNames: + try: + ship = tdb.lookupShip(shipName) + except LookupError: + raise CommandLineError("Unrecognized Ship: {}".format(shipName)) + + # Lets see if that ship sails from the specified port. + shipPresent = checkShipPresent(tdb, station, ship) + if cmdenv.add: + if shipPresent: + raise CommandLineError( + "{} is already listed at {}" + .format(ship.name(), station.name()) + ) + ships[ship.ID] = ship + else: + if not shipPresent: + raise CommandLineError( + "{} is not listed at {}" + .format(ship.name(), station.name()) + ) + ships[ship.ID] = ship + + # We've checked that everything should be good. + dataToExport = False + for ship in ships.values(): + if action(tdb, cmdenv,station, ship): + dataToExport = True + + maybeExportToCSV(tdb, cmdenv) + + return None diff --git a/commands/station_cmd.py b/commands/station_cmd.py index 29c465b3..f719c83c 100644 --- a/commands/station_cmd.py +++ b/commands/station_cmd.py @@ -263,6 +263,7 @@ def removeStation(tdb, cmdenv, station): db.commit() cmdenv.NOTE("{} (#{}) removed from {} database.", station.name(), station.ID, tdb.dbPath) + return True def checkResultAndExportStations(tdb, cmdenv, result): diff --git a/data/ShipVendor.csv b/data/ShipVendor.csv index f0cb7d3f..5ea7f190 100644 --- a/data/ShipVendor.csv +++ b/data/ShipVendor.csv @@ -531,6 +531,13 @@ unq:!name@System.system_id,unq:name@Station.station_id,unq:name@Ship.ship_id 'MIRATEJE','Ocampo Station','Cobra' 'MIRATEJE','Ocampo Station','Type 6' 'MIRATEJE','Ocampo Station','Viper' +'MS DRACONIS','Piper Holdings','Adder' +'MS DRACONIS','Piper Holdings','Cobra' +'MS DRACONIS','Piper Holdings','Eagle' +'MS DRACONIS','Piper Holdings','Hauler' +'MS DRACONIS','Piper Holdings','Sidewinder' +'MS DRACONIS','Piper Holdings','Type 6' +'MS DRACONIS','Piper Holdings','Viper' 'NADUNINDA','Robson Mines','Asp' 'NADUNINDA','Robson Mines','Eagle' 'NADUNINDA','Robson Mines','Hauler' diff --git a/tradedb.py b/tradedb.py index 6f6a04c8..c6fb9cb2 100644 --- a/tradedb.py +++ b/tradedb.py @@ -778,7 +778,7 @@ def updateLocalSystem( db.execute(""" UPDATE System SET name=?, - x=?, y=?, z=?, + pos_x=?, pos_y=?, pos_z=?, added=(SELECT added_id FROM Added WHERE name = ?), modified=DATETIME(?) """, [