diff --git a/.gitignore b/.gitignore index 1a9ab063..34826320 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ tmp *2.py *.laccdb +data/TradeDangerous.db diff --git a/README.txt b/README.txt index 4ba3a8d3..e77224ce 100644 --- a/README.txt +++ b/README.txt @@ -1,5 +1,5 @@ ============================================================================== -TradeDangerous v2.09 +TradeDangerous v3.0 Copyright (C) Oliver "kfsone" Smith, July 2014 ============================================================================== @@ -21,61 +21,21 @@ factors that into the shopping for each subsequent hop. == CHANGE LOG ============================================================================== -v2.09 Aug 22/2014 - Command line errors now get a simple explanation rather than a stack dump, - --ly-per and --capacity can now override values from --ship, - Made "--detail" show the "options summary" in addition to --debug doing it, - -v2.08 Aug 21/2014 - Fixed some formatting shenanigans - -v2.07 Aug 21/2014 - Added "--ship" to specify capacity and max light years based on ship, - Use "--detail" multiple times to add more detail, - Changed presentation of routes, "--detail --detail" shows a lot more - information and breaks it up onto more lines - Improved start up time slightly - -v2.06 Aug 17/2014 - Added experimental X52 Pro MFD support to the checklist - -v2.05 Aug 17/2014 - Big code cleanup, - Startup speed improvement, - Fixed --via, - Refactored how avoidance works: - - Avoiding a system prevents jumps to/thru that system, - - Avoiding a station allows jumps thru the system but not dockings, - -v2.04 Aug/17/2014 - Added "--checklist" command to walk you through a route - Added "localedNo()" function to API - -v2.03 Aug/15/2014 - Imported star data from wtbw - Fixed various prices and station names - Fixed minor issues - -v2.02 - "--via" will now accept the via station as the first station on - routes when the user doesn't specify a "--from". - Also made name matching far more flexible. - -v2.01 - "--avoid" now handles stations and system names +v3.0 Aug 30/2014 + Major overhaul. No-longer requires Microsoft Access DB. To update prices, + edit the data/TradeDangerous.prices file. When you next run the tool, new + data will be loaded automatically. + Cleaned up code, normalized the way I name functions, etc. + Added more ship data, etc. + ============================================================================== == Where does it get it's data? ============================================================================== -The data is stored in a simple Microsoft Access 2013 Database because I'm -hand-editing the database and Microsoft Access surprised me by having a really -nice UI for doing this (open the .accdb file with MS Access and open the -'StationCats' query, click the 'v' button on the 'station' header and select -the station you are at to update the prices for it). - -Programmer Note: I used the pypyodbc api so you can replace it with whatever -DB you want. +The data is stored as human-readable text in a .SQL file and a .Prices file. +When this data is loaded, it is saved into an SQLite database file which the +tools use directly until you change either the .SQL or .Prices file. ============================================================================== @@ -292,123 +252,11 @@ argument which also honors the --detail argument. == How can I add or update the data? ============================================================================== -A script is provided, "import.py", which processes a series of simple commands -from a file called "import.txt". - -Syntax for import.txt is fairly primitive. Eventually I intend to replace the -access database with a collection of 'import.txt' files. - - # ... - Comment lines are ignored, as are blank lines. - - #rejectUnknown - Special comment that causes an unrecognized system in a new-star line - to generate an error. - - */:@n.nn[ly][,@n.nn[,...]] - Adds a system with links to other systems. For EMPTY systems (with - no stations), you can specify '*' as the station name. - e.g. - *Dahan/Gateway:Aulin@5.6,Eranin@9.8ly,... - *Hermitage/*:Elsewhere@10.13ly - - @ - Selects the specified station without trying to add it. - e.g. - @aulin - @gateway - @dahan - - - - Finds an item category matching the string and selects it as - the current item category. If the match is ambiguous, an error - will be raised. - NOTE: the name cannot contain a space, e.g. for "Consumer Goods" - just use "consumer" or "goods" - e.g. - -dru - -DRUG - -dRuGs - -rug - -ugs - -cons - -consumer - -goods - - [] - Finds an item matching the name within the current category - and sets a buy (how much the station buys for) and/or sell - price (how much the station sells for) for the item. - If the match is ambiguous, an error will be raised. - If no sell price is specified, it is assumed to be 0. - NOTE: Name cannot contain spaces - e.g. - -cons - appliances 1000 - appl 1000 0 - -chem - pesticides 56 57 - PEST 56 57 - -'*' doesn't select the system, this is because I tend to keep all of -my stations at the top of my import.txt and then tack on item updates -to a single station at the end, and I wanted to make absolutely sure I -had selected the correct station. - - -===================== -== Example import.txt - - - # Add (or update) dahan and describe it's links to Aulin and Eranin. - # If they aren't in the database yet, they will be quietly ignored. - # If they are in the database, a link will be added each way. - *Dahan/Gateway:Aulin@5.6ly,Eranin@9.8ly - - # Add Eranin. - *Eranin/Azeban:Dahan@9.8ly,Hermitage@20.21ly - - # Select Dahan. - # Alternatively: @DAHAN, @GATEWAY or @gateway - @Dahan - - # Select 'Chemicals' category. - # Alternatively: -CHEMICALS, -CHEMicals, etc - -chem - - # Hydrogen fuel is bought by this station for 56cr but not sold here. - # Alternatively: Hydrogen 56 0, HYD 56, etc - hydro 56 - - # Station is selling pesticides for 67cr or paying 58 for them. - # Alternatively: PESTICIDES 67 58, pesti 67 58, etc - pest 67 58 - - # Change to Consumer Goods category, which matches 'cons' - # and specify prices for "Clothing", "Consumer Technology" and "Dom. - # Appliances" - -cons - clo 306 - cons 6049 - # alternatively: dom, DOM, appliances, APPLI, appl, etc - dom. 548 - - -============================================================================== -== Why did you choose MS Access, you moron? -============================================================================== - -I'm a Unix guy but I also like to push myself outside my comfort zone. During -my time at Blizzard I'd actually started to find MS Office 2010 quite -bearable, so I happened to have a free trial of MS Office 365 installed. - -I wanted to throw the data together really, really quickly. So I tried Libre -Office Base. The pain was strong in that one. So, for giggles, I decided to -see just how painful it was in Access and 5 minutes later I had a working -database that was really easy to update exactly the way I wanted. - -It should be trivial to convert it to a different database. - +For pricing changes, take a look at data/TradeDangerous.prices. This rebuilds +the entire database, a future version will allow you to update prices for +specific stations or items and be able to tell you how recent a price value +is (and use that information for adjusting how confident TD is about a +calculation). ============================================================================== == That's nice, but I'm a programmer and I want to ... @@ -419,7 +267,7 @@ Yeah, let me stop you there. from tradedb import * from tradecalc import * - tdb = TradeDB(".\\TradeDangerous.accdb") + tdb = TradeDB() calc = TradeCalc(tdb, capacity=16, margin=0.01, unique=False) Whatever it is you want to do, you can do from there. diff --git a/TradeDangerous.accdb b/TradeDangerous.accdb deleted file mode 100644 index 5f026095..00000000 Binary files a/TradeDangerous.accdb and /dev/null differ diff --git a/buildcache.py b/buildcache.py new file mode 100644 index 00000000..a10600ae --- /dev/null +++ b/buildcache.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python +#--------------------------------------------------------------------- +# Copyright (C) Oliver 'kfsone' Smith 2014 : +# You are free to use, redistribute, or even print and eat a copy of +# this software so long as you include this copyright notice. +# I guarantee there is at least one bug neither of us knew about. +#--------------------------------------------------------------------- +# TradeDangerous :: Modules :: Cache loader +# +# TD works primarily from an SQLite3 database, but the data in that +# is sourced from text files. +# data/TradeDangerous.sql contains the less volatile data - systems, +# ships, etc +# data/TradeDangerous.prices contains a description of the price +# database that is intended to be easily editable and commitable to +# a source repository. +# +# TODO: Split prices into per-system or per-station files so that +# we can tell how old data for a specific system is. + +import re +import sqlite3 +import sys +import os +from collections import namedtuple + +# Find the non-comment part of a string +noCommentRe = re.compile(r'^\s*(?P(?:[^\\#]|\\.)+?)\s*(#|$)') +systemStationRe = re.compile(r'^\@\s*(.*)\s*/\s*(.*)') +categoryRe = re.compile(r'^\+\s*(.*?)\s*$') +itemPriceRe = re.compile(r'^(.*?)\s+(\d+)\s+(\d+)$') + +class PriceEntry(namedtuple('PriceEntry', [ 'stationID', 'itemID', 'asking', 'paying', 'uiOrder' ])): + pass + +def priceLineNegotiator(priceFile, db, debug): + """ + Yields SQL for populating the database with prices + by reading the file handle for price lines. + """ + + stationID, categoryID, uiOrder = None, None, 0 + + cur = db.cursor() + + cur.execute(""" + SELECT station_id, UPPER(system.name) || "/" || station.name + FROM System INNER JOIN Station ON System.system_id = Station.system_id + """) + systemByName = { name: ID for (ID, name) in cur } + + categoriesByName = { name: ID for (ID, name) in cur.execute("SELECT category_id, name FROM category") } + itemsByName = { "{}:{}".format(catID, name): itemID for (catID, itemID, name) in cur.execute("SELECT category_id, item_id, name FROM item") } + + for line in priceFile: + try: + text = noCommentRe.match(line).group('text').strip() + # replace whitespace with single spaces + text = ' '.join(text.split()) # http://stackoverflow.com/questions/2077897 + if not text: continue + + # Check for a station assignment + matches = systemStationRe.match(text) + if matches: + # Change which station we're at + stationName = "%s/%s" % (matches.group(1).upper(), matches.group(2)) + stationID, categoryID, uiOrder = systemByName[stationName], None, 0 + if debug > 1: print("NEW STATION: {}".format(stationName)) + continue + if not stationID: + print("Expecting: '@ SYSTEM / Station'.") + print("Got: {}".format(line)) + sys.exit(1) + + # Check for a category assignment + matches = categoryRe.match(text) + if matches: + categoryName = matches.group(1) + categoryID, uiOrder = categoriesByName[categoryName], 0 + if debug > 1: print("NEW CATEGORY: {}".format(categoryName)) + continue + if not categoryID: + print("Expecting '+ Category Name'.") + print("Got: {}".format(line)) + sys.exit(1) + + matches = itemPriceRe.match(text) + if not matches: + print("Unrecognized line/syntax: {}".format(line)) + sys.exit(1) + itemName, stationPaying, stationAsking = matches.group(1), int(matches.group(2)), int(matches.group(3)) + itemID = itemsByName["{}:{}".format(categoryID, itemName)] + uiOrder += 1 + yield PriceEntry(stationID, itemID, stationPaying, stationAsking, uiOrder) + except (AttributeError, IndexError): + continue + + +def buildCache(dbPath, sqlPath, pricesPath, debug=0): + """ + Rebuilds the SQlite database from source files. + + TD's data is either "stable" - information that rarely changes like Ship + details, star systems etc - and "volatile" - pricing information, etc. + + The stable data starts out in data/TradeDangerous.sql while other data + is stored in custom-formatted text files, e.g. ./TradeDangerous.prices. + + We load both sets of data into an SQLite database, after which we can + avoid the text-processing overhead by simply checking if the text files + are newer than the database. + """ + + # Create an in-memory database to populate with our data. + if debug: print("* Creating temporary database in memory") + tempDB = sqlite3.connect(':memory:') + + # Read the SQL script so we are ready to populate structure, etc. + if debug: print("* Executing SQL Script '%s'" % sqlPath) + with sqlPath.open() as sqlFile: + sqlScript = sqlFile.read() + tempDB.executescript(sqlScript) + + # Parse the prices file + if debug: print("* Processing Prices file '%s'" % pricesPath) + with pricesPath.open() as pricesFile: + bindValues = [] + for price in priceLineNegotiator(pricesFile, tempDB, debug): + if debug > 2: print(price) + bindValues += [ price ] + stmt = """ + INSERT INTO Price (station_id, item_id, sell_to, buy_from, ui_order) + VALUES (?, ?, ?, ?, ?) + """ + tempDB.executemany(stmt, bindValues) + + # Database is ready; copy it to a persistent store. + if debug: print("* Populating SQLite database file '%s'" % dbPath) + if dbPath.exists(): + if debug: print("- Removing old database file") + dbPath.unlink() + + newDB = sqlite3.connect(str(dbPath)) + importScript = "".join(tempDB.iterdump()) + if debug > 3: print(importScript) + newDB.executescript(importScript) + newDB.commit() + + if debug: print("* Finished") + + +def commandLineBuildCache(): + # Because it looks less sloppy that doing this in if __name__ == '__main__'... + from tradedb import TradeDB + dbFilename = TradeDB.defaultDB + sqlFilename = TradeDB.defaultSQL + pricesFilename = TradeDB.defaultPrices + + # Check command line for -w/--debug inputs. + import argparse + parser = argparse.ArgumentParser(description='Build TradeDangerous cache file from source files') + parser.add_argument('--db', help='Specify database file to build. Default: {}'.format(dbFilename), default=dbFilename, required=False) + parser.add_argument('--sql', help='Specify SQL script to execute. Default: {}'.format(sqlFilename), default=sqlFilename, required=False) + parser.add_argument('--prices', help='Specify the prices file to load. Default: {}'.format(pricesFilename), default=pricesFilename, required=False) + parser.add_argument('-f', '--force', dest='force', help='Overwite existing file', default=False, required=False, action='store_true') + parser.add_argument('-w', '--debug', dest='debug', help='Increase level of diagnostic output', default=0, required=False, action='count') + args = parser.parse_args() + + import pathlib + + # Check that the file doesn't already exist. + dbPath, sqlPath, pricesPath = pathlib.Path(args.db), pathlib.Path(args.sql), pathlib.Path(args.prices) + if not args.force: + if dbPath.exists(): + print("{}: ERROR: SQLite3 database '{}' already exists. Please remove it first.".format(sys.argv[0], args.db)) + sys.exit(1) + + if not sqlPath.exists(): + print("SQL file '{}' does not exist.".format(args.sql)) + sys.exit(1) + + if not pricesPath.exists(): + print("Prices file '{}' does not exist.".format(args.prices)) + + buildCache(dbPath, sqlPath, pricesPath, args.debug) + + +if __name__ == '__main__': + commandLineBuildCache() diff --git a/cli.py b/cli.py index ed5d59f5..c46d67df 100644 --- a/cli.py +++ b/cli.py @@ -1,4 +1,15 @@ #!/usr/bin/env python +#--------------------------------------------------------------------- +# Copyright (C) Oliver 'kfsone' Smith 2014 : +# You are free to use, redistribute, or even print and eat a copy of +# this software so long as you include this copyright notice. +# I guarantee there is at least one bug neither of us knew about. +#--------------------------------------------------------------------- +# +# This is a work-in-progress script that allows you to use some of +# TDs functionality from inside a python interpreter such as IDLE or +# just python itself. + from tradedb import * from tradecalc import * @@ -10,7 +21,7 @@ def at(station): global curStation - curStation = tdb.getStation(station) + curStation = tdb.lookupStation(station) def cr(n): global curCredits @@ -26,7 +37,7 @@ def run(dst=None, stn=None, cr=None, cap=None, maxJumps=None, maxLy=None): withCr = cr if cr else curCredits if not dst: return calc.getBestHopFrom(srcStn, withCr, capacity=cap, maxJumps=None, maxLy=None) - dstStn = dst if isinstance(dst, Station) else tdb.getStation(dst) + dstStn = dst if isinstance(dst, Station) else tdb.lookupStation(dst) print(srcStn, dstStn, withCr, cap) return calc.getBestTrade(srcStn, dstStn, withCr, capacity=cap, maxJumps=maxJumps, maxLy=maxLy) @@ -36,7 +47,7 @@ def links(stn=None, maxJumps=None, maxLy=None): print("You don't have a station selected. Use at('name') or links(stn='name')") return None if isinstance(srcStn, str): - srcStn = tdb.getStation(srcStn) + srcStn = tdb.lookupStation(srcStn) return srcStn.stations.getDestinations(maxJumps=maxJumps, maxLy=maxLy) def routes(maxHops=2, stn=None, cr=None, maxJumps=None, maxLy=None, maxRoutes=1, maxJumpsPer=None, maxLyPer=8): @@ -69,7 +80,7 @@ def routes(maxHops=2, stn=None, cr=None, maxJumps=None, maxLy=None, maxRoutes=1, print(routes[i]) def find(item, stn=None): - srcStn = tdb.getStation(stn if stn else curStation) + srcStn = tdb.lookupStation(stn if stn else curStation) qry = """ SELECT p.station_id, p.buy_cr FROM Items AS i diff --git a/data/TradeDangerous.prices b/data/TradeDangerous.prices new file mode 100644 index 00000000..45405136 --- /dev/null +++ b/data/TradeDangerous.prices @@ -0,0 +1,1857 @@ +# SOURCE List of item prices for TradeDangerous. +# +# This file is used by TradeDangerous to populate the +# SQLite databsae with prices, whenever this file is +# updated or you delete the TradeDangerous.db file. + +# FORMAT: +# +# # ... +# A comment +# +# @ SYSTEM NAME/Station Name +# Sets the current station +# e.g. @ CHANGO/Chango Dock +# +# + Product Category +# Sets the current product category +# e.g. + Consumer Tech +# +# Item Name Value Cost +# Item value line. +# Item Name is the name of the item, e.g. Fish +# Value is what the STATION pays for the item, +# Cost is how much the item costs FROM the station +# +# Example: +# @ CHANGO/Chango Dock +# + Chemicals +# Mineral Oil 150 0 +# Hydrogen Fuels 63 0 +# Explosives 150 160 +# +# This gives prives for items under the "Chemicals" +# heading. Mineral oil was listed first and was selling +# at the station for 150cr. +# Hydrogen Fuels was listed 3rd and sells for 63 cr. +# Explosives was listed 2nd and sells for 150 cr AND +# can be bought here for 160cr. +# + +@ ACIHAUT/Cuffey Plant + + Chemicals + Mineral Oil 163 0 + Explosives 147 163 + Hydrogen Fuels 47 48 + + Consumer Items + Clothing 307 0 + Consumer Tech 6550 0 + Dom. Appliances 550 0 + + Drugs + Beer 143 0 + + Foods + Fruit and Vegetables 307 0 + Coffee 1401 0 + Food Cartridges 102 0 + Fish 676 0 + Animal Meat 1394 0 + Grain 180 0 + Tea 1406 0 + Synthetic Meat 233 0 + + Machinery + Mineral Extractors 617 0 + Hel-Static Furnaces 171 0 + + Medicines + Performance Enhancers 7120 0 + Progenitor Cells 7120 0 + Basic Medicines 307 0 + + Metals + Copper 222 239 + Indium 5738 5805 + Aluminium 126 139 + Uranium 2321 2382 + Lithium 1083 1129 + Titanium 782 818 + + Minerals + Indite 2300 0 + Bertrandite 1820 1868 + Gallium 4925 4984 + Gallite 1354 1392 + Coltan 1493 0 + Uraninite 958 0 + Lepidolite 302 323 + Rutile 324 0 + Bauxite 115 0 + Beryllium 7898 7987 + + Technology + Advanced Catalysers 3014 0 + H.E. Suits 259 0 + Resonating Separators 6155 0 + Bioreducing Lichen 1020 0 + + Waste + Biowaste 11 18 + Scrap 91 0 + + +@ AGANIPPE/Julian Market + + Chemicals + Pesticides 199 0 + Hydrogen Fuels 56 0 + + Consumer Items + Clothing 306 0 + Consumer Tech 7110 0 + Dom. Appliances 549 0 + + Drugs + Beer 142 0 + Wine 233 0 + + Foods + Algae 10 15 + Fruit and Vegetables 308 0 + Fish 329 356 + Grain 49 58 + Coffee 1399 0 + Tea 1589 0 + Animal Meat 1399 0 + + Machinery + Crop Harvesters 2340 0 + Marine Supplies 4489 0 + + Medicines + Performance Enhancers 7110 0 + Progenitor Cells 7110 0 + Agri-Medicines 1085 0 + Basic Medicines 306 0 + + Technology + Terrain Enrich Sys 4956 0 + Animal Monitors 289 0 + Aquaponic Systems 259 0 + + Waste + Biowaste 68 0 + + Weapons + Personal Weapons 4489 0 + + +@ ASELLUS PRIMUS/Beagle 2 Landing + + Chemicals + Hydrogen Fuels 56 0 + + Consumer Items + Clothing 252 0 + Consumer Tech 6595 6671 + Dom. Appliances 550 0 + + Drugs + Beer 143 0 + Wine 233 0 + + Foods + Grain 180 0 + Fruit and Vegetables 277 0 + Tea 1405 0 + Coffee 1231 0 + Food Cartridges 102 0 + Fish 645 0 + Synthetic Meat 233 0 + Animal Meat 1231 0 + + Industrial Materials + Superconductors 6949 0 + + Medicines + Performance Enhancers 7116 0 + Basic Medicines 306 0 + Progenitor Cells 7081 0 + + Metals + Palladium 13388 0 + Gold 9169 0 + Cobalt 746 0 + Uranium 2845 0 + Tantalum 3823 0 + Indium 5722 0 + Lithium 1660 0 + Silver 5121 0 + + Minerals + Rutile 324 0 + Beryllium 8618 0 + Gallite 1832 0 + Gallium 5243 0 + + Technology + Terrain Enrich Sys 4615 4671 + Advanced Catalysers 2374 2435 + Aquaponic Systems 129 144 + Animal Monitors 150 166 + Computer Components 550 0 + H.E. Suits 103 115 + Robotics 1539 1581 + Auto-Fabricators 3085 3162 + Resonating Separators 5386 5449 + + Waste + Biowaste 11 18 + Scrap 27 34 + + Weapons + Non-Lethal Wpns 1787 0 + Reactive Armor 1958 0 + Personal Weapons 3966 4064 + + +@ AULIN/Aulin Enterprise + + Chemicals + Pesticides 83 97 + Hydrogen Fuels 56 0 + + Consumer Items + Dom. Appliances 547 0 + Clothing 292 0 + Consumer Tech 6733 0 + + Drugs + Beer 142 0 + + Foods + Animal Meat 1226 0 + Coffee 1226 0 + Fish 643 0 + Grain 180 0 + Synthetic Meat 77 87 + Tea 1399 0 + Fruit and Vegetables 304 0 + + Industrial Materials + Superconductors 7086 0 + + Medicines + Agri-Medicines 624 653 + Basic Medicines 305 0 + Progenitor Cells 6498 6577 + Combat Stabilisers 2218 2277 + Performance Enhancers 6508 6587 + + Metals + Gold 9131 0 + Silver 4996 0 + Lithium 1620 0 + Palladium 12801 0 + Cobalt 742 0 + Indium 6019 0 + Tantalum 4047 0 + Uranium 2833 0 + + Minerals + Rutile 323 0 + Gallium 5407 0 + Gallite 1825 0 + Beryllium 8080 0 + + Technology + Terrain Enrich Sys 3912 3961 + Advanced Catalysers 2247 2306 + H.E. Suits 128 143 + Computer Components 547 0 + Bioreducing Lichen 732 766 + + Waste + Biowaste 11 18 + Scrap 28 35 + + +@ AULIS/Dezhurov Gateway + + Chemicals + Mineral Oil 62 73 + Pesticides 199 0 + Hydrogen Fuels 56 0 + + Consumer Items + Dom. Appliances 550 0 + Consumer Tech 7115 0 + Clothing 306 0 + + Drugs + Beer 158 0 + + Foods + Algae 10 15 + Fruit and Vegetables 116 129 + Coffee 897 937 + Tea 1024 1067 + Fish 329 355 + Animal Meat 1387 0 + Grain 49 58 + + Machinery + Crop Harvesters 2341 0 + Marine Supplies 4491 0 + + Medicines + Basic Medicines 306 0 + Performance Enhancers 7114 0 + Agri-Medicines 1086 0 + Progenitor Cells 7114 0 + + Technology + Aquaponic Systems 259 0 + Terrain Enrich Sys 4675 0 + Animal Monitors 289 0 + + Waste + Biowaste 68 0 + + +@ BD+47 2112/Olivas Settlement + + Chemicals + Explosives 290 0 + Hydrogen Fuels 80 81 + + Consumer Items + Dom. Appliances 551 0 + Clothing 307 0 + Consumer Tech 6566 0 + + Drugs + Beer 143 0 + + Foods + Coffee 1404 0 + Animal Meat 1323 0 + Grain 181 0 + Synthetic Meat 234 0 + Tea 1409 0 + Fruit and Vegetables 307 0 + Food Cartridges 103 0 + Fish 715 0 + + Machinery + Mineral Extractors 622 0 + + Medicines + Performance Enhancers 7137 0 + Basic Medicines 307 0 + Progenitor Cells 7137 0 + + Metals + Cobalt 486 519 + + Minerals + Uraninite 638 715 + Indite 1917 1968 + Bauxite 29 39 + + Technology + H.E. Suits 260 0 + Bioreducing Lichen 1022 0 + + Waste + Biowaste 11 18 + + Weapons + Non-Lethal Wpns 1935 0 + Reactive Armor 2201 0 + + +@ BOLG/Moxon's Mojo + + Chemicals + Hydrogen Fuels 62 0 + + Consumer Items + Consumer Tech 7121 0 + Dom. Appliances 345 369 + Clothing 306 0 + + Drugs + Beer 143 0 + + Foods + Fruit and Vegetables 307 0 + Synthetic Meat 234 0 + Fish 735 0 + Animal Meat 1332 0 + Grain 181 0 + Food Cartridges 14 20 + Algae 95 0 + Coffee 1384 0 + Tea 1407 0 + + Industrial Materials + Polymers 116 0 + Semiconductors 959 0 + Superconductors 7124 0 + + Machinery + Hel-Static Furnaces 45 53 + Mineral Extractors 333 356 + Crop Harvesters 1944 1996 + + Medicines + Progenitor Cells 7124 0 + Basic Medicines 344 0 + Performance Enhancers 7124 0 + + Metals + Indium 6245 0 + Tantalum 4212 0 + Uranium 2848 0 + Lithium 1698 0 + Titanium 1088 0 + Copper 488 0 + Aluminium 324 0 + Gold 9599 0 + + Minerals + Beryllium 8683 0 + Gallium 5476 0 + + Technology + H.E. Suits 259 0 + Robotics 1931 0 + Auto-Fabricators 3946 0 + + Textiles + Synthetic Fabrics 156 0 + Leather 143 0 + Natural Fabrics 409 0 + + Waste + Biowaste 11 18 + Scrap 20 25 + + +@ CHI HERCULIS/Gorbatko + + Chemicals + Hydrogen Fuels 56 0 + Pesticides 199 0 + + Consumer Items + Dom. Appliances 550 0 + Consumer Tech 7114 0 + Clothing 306 0 + + Drugs + Beer 143 0 + + Foods + Algae 11 17 + Fruit and Vegetables 306 0 + Coffee 1399 0 + Tea 1590 0 + Fish 361 389 + Animal Meat 1231 0 + Grain 57 67 + + Machinery + Crop Harvesters 2341 0 + Marine Supplies 4491 0 + + Medicines + Basic Medicines 306 0 + Performance Enhancers 7114 0 + Agri-Medicines 1086 0 + Progenitor Cells 7114 0 + + Technology + Aquaponic Systems 259 0 + Terrain Enrich Sys 4964 0 + Animal Monitors 289 0 + + Waste + Biowaste 68 0 + + +@ CM DRACO/Anderson Escape + + Chemicals + Explosives 291 0 + Hydrogen Fuels 81 82 + + Consumer Items + Clothing 308 0 + Consumer Tech 6588 0 + Dom. Appliances 469 0 + + Drugs + Tobacco 4706 0 + Beer 143 0 + Narcotics 143 0 + Wine 236 0 + Liquor 664 0 + + Foods + Fruit and Vegetables 254 0 + Grain 182 0 + Coffee 1409 0 + Tea 1414 0 + Synthetic Meat 217 0 + Food Cartridges 103 0 + Fish 650 0 + Animal Meat 1239 0 + + Machinery + Mineral Extractors 616 0 + + Medicines + Performance Enhancers 7161 0 + Basic Medicines 309 0 + + Metals + Silver 4131 4181 + Cobalt 387 413 + Palladium 12284 12284 + + Minerals + Uraninite 685 717 + Bauxite 30 39 + Rutile 133 147 + Indite 1923 1974 + Coltan 1139 1188 + + Technology + Bioreducing Lichen 1026 0 + H.E. Suits 261 0 + + Waste + Biowaste 11 18 + + Weapons + Non-Lethal Wpns 1942 0 + Personal Weapons 4523 0 + Reactive Armor 2210 0 + + +@ DAHAN/Gateway + + Chemicals + Mineral Oil 164 0 + Hydrogen Fuels 80 81 + Explosives 145 162 + + Consumer Items + Dom. Appliances 551 0 + Consumer Tech 6566 0 + Clothing 307 0 + + Drugs + Beer 143 0 + + Foods + Synthetic Meat 234 0 + Fish 647 0 + Food Cartridges 103 0 + Fruit and Vegetables 307 0 + Tea 1409 0 + Coffee 1235 0 + Animal Meat 1235 0 + Grain 181 0 + + Industrial Materials + Polymers 29 39 + + Machinery + Hel-Static Furnaces 172 0 + Mineral Extractors 622 0 + + Medicines + Basic Medicines 307 0 + Combat Stabilisers 2997 0 + Performance Enhancers 7137 0 + + Metals + Indium 5751 5819 + Tantalum 3625 3714 + Cobalt 502 535 + Titanium 634 663 + Aluminium 154 170 + + Minerals + Indite 1575 1617 + Bertrandite 2400 0 + Bauxite 113 0 + Gallite 1926 0 + Coltan 1497 0 + Uraninite 908 0 + Lepidolite 622 0 + Rutile 325 0 + + Technology + Advanced Catalysers 2907 0 + H.E. Suits 260 0 + Resonating Separators 5739 0 + Bioreducing Lichen 1022 0 + + Waste + Biowaste 11 18 + Scrap 91 0 + + Weapons + Non-Lethal Wpns 1936 0 + Reactive Armor 2202 0 + + +@ ERANIN/Azeban City + + Chemicals + Mineral Oil 62 73 + Hydrogen Fuels 56 0 + Pesticides 199 0 + + Consumer Items + Dom. Appliances 550 0 + Clothing 252 0 + Consumer Tech 6926 0 + + Drugs + Beer 143 0 + Tobacco 4675 0 + Wine 256 0 + Liquor 659 0 + + Foods + Coffee 1067 1114 + Tea 1243 1297 + Fruit and Vegetables 166 184 + Fish 645 0 + Animal Meat 1067 1114 + Grain 70 83 + + Machinery + Crop Harvesters 2179 0 + + Medicines + Basic Medicines 306 0 + Progenitor Cells 7115 0 + Agri-Medicines 964 0 + Performance Enhancers 7115 0 + + Technology + Animal Monitors 289 0 + Terrain Enrich Sys 4675 0 + + Textiles + Natural Fabrics 237 255 + Leather 46 58 + + Waste + Biowaste 68 0 + + Weapons + Personal Weapons 4280 0 + + +@ G 239-25/Bresnik Mine + + Chemicals + Explosives 290 0 + Hydrogen Fuels 81 81 + + Consumer Items + Dom. Appliances 552 0 + Clothing 308 0 + Consumer Tech 6569 0 + + Drugs + Beer 143 0 + + Foods + Coffee 1404 0 + Tea 1410 0 + Synthetic Meat 234 0 + Food Cartridges 103 0 + Fish 775 0 + Animal Meat 1235 0 + Grain 181 0 + Fruit and Vegetables 301 0 + + Machinery + Mineral Extractors 622 0 + + Medicines + Performance Enhancers 7140 0 + Basic Medicines 307 0 + + Minerals + Gallite 1342 1379 + Bertrandite 1825 1873 + Lepidolite 302 324 + + Technology + H.E. Suits 260 0 + Bioreducing Lichen 1023 0 + + Waste + Biowaste 11 18 + + Weapons + Reactive Armor 2203 0 + Non-Lethal Wpns 1936 0 + + +@ H DRACONIS/Brislington + + Chemicals + Hydrogen Fuels 62 0 + + Consumer Items + Dom. Appliances 290 314 + Consumer Tech 7109 0 + Clothing 116 129 + + Drugs + Beer 142 0 + Wine 233 0 + + Foods + Animal Meat 1398 0 + Coffee 1398 0 + Tea 1589 0 + Synthetic Meat 233 0 + Food Cartridges 102 0 + Fish 772 0 + Grain 180 0 + Fruit and Vegetables 306 0 + Algae 95 0 + + Industrial Materials + Superconductors 7109 0 + Semiconductors 957 0 + Polymers 115 0 + + Medicines + Basic Medicines 306 0 + Performance Enhancers 7109 0 + Progenitor Cells 7109 0 + + Metals + Gold 9889 0 + Uranium 2842 0 + Lithium 1695 0 + Titanium 1085 0 + Indium 6232 0 + Copper 487 0 + Aluminium 324 0 + Tantalum 4204 0 + + Minerals + Beryllium 8665 0 + Gallium 5465 0 + + Technology + H.E. Suits 259 0 + Robotics 1927 0 + Auto-Fabricators 3937 0 + + Textiles + Leather 142 0 + Synthetic Fabrics 156 0 + Natural Fabrics 408 0 + + Waste + Biowaste 11 18 + Scrap 20 24 + + Weapons + Personal Weapons 4488 0 + + +@ I BOOTIS/Chango Dock + + Chemicals + Hydrogen Fuels 58 0 + Pesticides 198 0 + Mineral Oil 51 60 + + Consumer Items + Clothing 116 129 + Consumer Tech 7090 0 + Dom. Appliances 548 0 + + Drugs + Beer 142 0 + + Foods + Food Cartridges 13 19 + Coffee 1393 0 + Synthetic Meat 229 0 + Tea 1563 0 + Fish 457 494 + Animal Meat 1395 0 + Grain 180 0 + Fruit and Vegetables 305 0 + Algae 10 15 + + Industrial Materials + Semiconductors 954 0 + Polymers 115 0 + Superconductors 7090 0 + + Machinery + Crop Harvesters 1935 1990 + Mineral Extractors 394 432 + Hel-Static Furnaces 45 53 + Marine Supplies 4476 0 + + Medicines + Performance Enhancers 7090 0 + Agri-Medicines 943 0 + Basic Medicines 342 0 + Progenitor Cells 7090 0 + + Metals + Lithium 1690 0 + Gold 9136 0 + Tantalum 4178 0 + Uranium 2834 0 + Titanium 1058 0 + Copper 485 0 + Aluminium 323 0 + Indium 6054 0 + + Minerals + Gallium 5387 0 + Beryllium 8617 0 + + Technology + Aquaponic Systems 258 0 + Terrain Enrich Sys 4659 0 + Robotics 1922 0 + Animal Monitors 288 0 + Auto-Fabricators 3839 0 + H.E. Suits 258 0 + Computer Components 346 371 + + Textiles + Leather 142 0 + Natural Fabrics 407 0 + Synthetic Fabrics 155 0 + + Waste + Biowaste 67 0 + Scrap 20 25 + + +@ ITHACA/Hume Depot + + Chemicals + Explosives 290 0 + Hydrogen Fuels 47 48 + + Consumer Items + Clothing 307 0 + Consumer Tech 6563 0 + Dom. Appliances 551 0 + + Drugs + Tobacco 4688 0 + Beer 143 0 + Narcotics 143 0 + Wine 234 0 + Liquor 661 0 + + Foods + Fruit and Vegetables 307 0 + Grain 181 0 + Coffee 1234 0 + Tea 1409 0 + Synthetic Meat 234 0 + Food Cartridges 103 0 + Fish 647 0 + Animal Meat 1256 0 + + Machinery + Mineral Extractors 622 0 + + Medicines + Performance Enhancers 6928 0 + Basic Medicines 307 0 + + Metals + Silver 3938 3985 + Cobalt 386 412 + Palladium 12598 12599 + + Minerals + Uraninite 533 558 + Bauxite 19 25 + Rutile 126 140 + Indite 1574 1616 + Coltan 910 950 + + Technology + Bioreducing Lichen 1022 0 + H.E. Suits 260 0 + + Waste + Biowaste 11 18 + + Weapons + Non-Lethal Wpns 1758 0 + Personal Weapons 4216 0 + Reactive Armor 1963 0 + + +@ KERIES/Derrickson's Escape + + Chemicals + Explosives 291 0 + Hydrogen Fuels 81 82 + + Consumer Items + Clothing 261 0 + Consumer Tech 6588 0 + Dom. Appliances 469 0 + + Drugs + Wine 235 0 + Beer 144 0 + Narcotics 144 0 + Tobacco 4706 0 + Liquor 664 0 + + Foods + Coffee 1410 0 + Synthetic Meat 227 0 + Tea 1414 0 + Food Cartridges 103 0 + Fish 650 0 + Animal Meat 1239 0 + Grain 182 0 + Fruit and Vegetables 272 0 + + Machinery + Mineral Extractors 532 0 + + Medicines + Performance Enhancers 6908 0 + Basic Medicines 304 0 + + Minerals + Indite 1954 2007 + Bertrandite 1830 1878 + Bauxite 19 25 + Gallite 1651 1695 + Lepidolite 303 325 + + Technology + H.E. Suits 261 0 + Bioreducing Lichen 1026 0 + + Waste + Biowaste 11 18 + + Weapons + Non-Lethal Wpns 1942 0 + Reactive Armor 1984 0 + Personal Weapons 4115 0 + + +@ LFT 880/Baker Platform + + Chemicals + Mineral Oil 164 0 + Hydrogen Fuels 57 0 + + Consumer Items + Clothing 279 0 + Consumer Tech 6563 0 + Dom. Appliances 467 0 + + Drugs + Beer 143 0 + Wine 234 0 + + Foods + Fruit and Vegetables 256 0 + Food Cartridges 103 0 + Fish 678 0 + Animal Meat 1234 0 + Tea 1409 0 + Grain 181 0 + Synthetic Meat 234 0 + Coffee 1258 0 + + Machinery + Hel-Static Furnaces 172 0 + + Medicines + Performance Enhancers 7134 0 + Basic Medicines 307 0 + + Metals + Copper 223 239 + Titanium 617 646 + Uranium 1975 2026 + Indium 4966 5024 + Gold 8885 8984 + + Minerals + Bertrandite 2399 0 + Coltan 1436 0 + Beryllium 7150 7231 + Bauxite 116 0 + Rutile 268 0 + Lepidolite 622 0 + Uraninite 960 0 + Indite 2212 0 + Gallite 1858 0 + Gallium 4212 4262 + + Technology + Resonating Separators 5736 0 + Advanced Catalysers 2742 0 + H.E. Suits 260 0 + + Waste + Biowaste 11 18 + Scrap 91 0 + + Weapons + Non-Lethal Wpns 1719 0 + Reactive Armor 1963 0 + Personal Weapons 4099 0 + + +@ LFT 992/Szulkin Mines + + Chemicals + Hydrogen Fuels 81 82 + Explosives 291 0 + + Consumer Items + Dom. Appliances 554 0 + Clothing 308 0 + Consumer Tech 6588 0 + + Drugs + Beer 144 0 + Tobacco 4706 0 + Wine 235 0 + Liquor 664 0 + Narcotics 144 0 + + Foods + Coffee 1410 0 + Animal Meat 1239 0 + Synthetic Meat 235 0 + Fruit and Vegetables 308 0 + Grain 182 0 + Fish 650 0 + Tea 1414 0 + Food Cartridges 103 0 + + Machinery + Mineral Extractors 624 0 + + Medicines + Basic Medicines 308 0 + Performance Enhancers 7161 0 + + Minerals + Gallite 1346 1383 + Bertrandite 1830 1878 + Lepidolite 303 325 + + Technology + H.E. Suits 261 0 + Bioreducing Lichen 1026 0 + + Waste + Biowaste 11 18 + + Weapons + Personal Weapons 4521 0 + Reactive Armor 2209 0 + Non-Lethal Wpns 1942 0 + + +@ LHS 2819/Tasaki Freeport + + Chemicals + Hydrogen Fuels 47 48 + Explosives 291 0 + + Consumer Items + Dom. Appliances 554 0 + Clothing 308 0 + Consumer Tech 6588 0 + + Drugs + Narcotics 144 0 + Liquor 664 0 + Wine 235 0 + Beer 144 0 + Tobacco 4706 0 + + Foods + Fish 778 0 + Fruit and Vegetables 308 0 + Food Cartridges 103 0 + Grain 182 0 + Animal Meat 1239 0 + Coffee 1410 0 + Tea 1414 0 + Synthetic Meat 235 0 + + Machinery + Mineral Extractors 609 0 + + Medicines + Performance Enhancers 7161 0 + Basic Medicines 308 0 + + Minerals + Gallite 1346 1383 + Bertrandite 1830 1878 + + Technology + H.E. Suits 261 0 + Bioreducing Lichen 1026 0 + + Waste + Biowaste 11 18 + + Weapons + Reactive Armor 2209 0 + Non-Lethal Wpns 1904 0 + Personal Weapons 4521 0 + + +@ LHS 2884/Abnett Platform + + Chemicals + Explosives 290 0 + Hydrogen Fuels 47 48 + + Consumer Items + Consumer Tech 6559 0 + Dom. Appliances 547 0 + Clothing 305 0 + + Drugs + Wine 234 0 + Beer 143 0 + + Foods + Synthetic Meat 234 0 + Coffee 1402 0 + Tea 1472 0 + Animal Meat 1233 0 + Food Cartridges 102 0 + Fish 647 0 + Grain 181 0 + Fruit and Vegetables 307 0 + + Machinery + Mineral Extractors 618 0 + + Medicines + Progenitor Cells 7129 0 + Performance Enhancers 7129 0 + Basic Medicines 307 0 + + Metals + Silver 4623 4678 + Gold 8268 8360 + Cobalt 501 535 + + Minerals + Bauxite 19 25 + Indite 1573 1615 + Uraninite 533 558 + Rutile 126 140 + + Technology + H.E. Suits 259 0 + Bioreducing Lichen 1021 0 + + Waste + Biowaste 11 18 + + Weapons + Non-Lethal Wpns 1933 0 + Reactive Armor 1962 0 + Personal Weapons 4096 0 + + +@ LHS 2887/Massimino Dock + + Chemicals + Explosives 150 167 + Hydrogen Fuels 81 81 + Mineral Oil 164 0 + + Consumer Items + Dom. Appliances 504 0 + Consumer Tech 6569 0 + Clothing 253 0 + + Drugs + Beer 143 0 + + Foods + Grain 181 0 + Coffee 1297 0 + Tea 1410 0 + Synthetic Meat 234 0 + Fish 648 0 + Animal Meat 1235 0 + Food Cartridges 103 0 + Fruit and Vegetables 307 0 + + Industrial Materials + Polymers 29 39 + Superconductors 6700 6777 + Semiconductors 683 715 + + Machinery + Hel-Static Furnaces 171 0 + + Medicines + Basic Medicines 308 0 + Performance Enhancers 7141 0 + + Metals + Copper 299 321 + Lithium 1310 1366 + Tantalum 3733 3825 + Indium 5797 5865 + Titanium 785 821 + + Minerals + Gallite 1839 0 + Coltan 1496 0 + Bauxite 116 0 + Bertrandite 2401 0 + Uraninite 961 0 + Lepidolite 622 0 + Rutile 325 0 + Indite 2272 0 + + Technology + Resonating Separators 6025 0 + Advanced Catalysers 3038 0 + H.E. Suits 260 0 + + Textiles + Synthetic Fabrics 57 68 + + Waste + Biowaste 12 18 + Scrap 91 0 + + Weapons + Non-Lethal Wpns 1936 0 + Reactive Armor 2203 0 + + +@ LHS 3006/WCM Transfer Orbital + + Chemicals + Hydrogen Fuels 80 81 + Explosives 290 0 + + Consumer Items + Dom. Appliances 551 0 + Clothing 307 0 + Consumer Tech 6563 0 + + Drugs + Beer 143 0 + + Foods + Tea 1409 0 + Animal Meat 1400 0 + Fruit and Vegetables 307 0 + Grain 181 0 + Coffee 1403 0 + Synthetic Meat 234 0 + Fish 766 0 + Food Cartridges 103 0 + + Machinery + Mineral Extractors 622 0 + + Medicines + Progenitor Cells 7134 0 + Performance Enhancers 7134 0 + Basic Medicines 307 0 + + Minerals + Indite 1916 1967 + Bertrandite 1823 1871 + Bauxite 29 39 + Lepidolite 302 323 + Gallite 1514 1555 + + Technology + H.E. Suits 260 0 + Bioreducing Lichen 1022 0 + + Waste + Biowaste 11 18 + + Weapons + Reactive Armor 2200 0 + Non-Lethal Wpns 1934 0 + + +@ LHS 3262/Louis De Lacaille Prospect + + Chemicals + Pesticides 58 67 + Hydrogen Fuels 56 0 + + Consumer Items + Clothing 272 0 + Consumer Tech 5682 5748 + Dom. Appliances 526 0 + + Drugs + Beer 142 0 + + Foods + Fruit and Vegetables 252 0 + Fish 679 0 + Animal Meat 1228 0 + Grain 180 0 + Food Cartridges 102 0 + Tea 1402 0 + Coffee 1250 0 + Synthetic Meat 233 0 + + Industrial Materials + Superconductors 7099 0 + + Medicines + Performance Enhancers 7099 0 + Basic Medicines 258 0 + Progenitor Cells 7099 0 + + Metals + Lithium 1688 0 + Uranium 2838 0 + Silver 5109 0 + Cobalt 744 0 + Gold 9340 0 + Indium 6033 0 + Palladium 13744 0 + Tantalum 4197 0 + + Minerals + Beryllium 7994 0 + Rutile 323 0 + Gallite 1885 0 + Gallium 5150 0 + + Technology + Auto-Fabricators 2853 2925 + Terrain Enrich Sys 3919 3966 + Animal Monitors 107 119 + Robotics 1249 1283 + H.E. Suits 91 101 + Computer Components 548 0 + + Waste + Biowaste 7 11 + Scrap 29 37 + + Weapons + Non-Lethal Wpns 1249 1283 + Reactive Armor 1446 1485 + + +@ LHS 417/Gernhardt Camp + + Chemicals + Hydrogen Fuels 81 81 + Explosives 290 0 + + Consumer Items + Clothing 308 0 + Consumer Tech 6569 0 + Dom. Appliances 552 0 + + Drugs + Beer 143 0 + + Foods + Tea 1529 0 + Synthetic Meat 234 0 + Fish 732 0 + Food Cartridges 103 0 + Fruit and Vegetables 307 0 + Animal Meat 1404 0 + Grain 181 0 + Coffee 1405 0 + + Machinery + Mineral Extractors 622 0 + + Medicines + Basic Medicines 307 0 + Performance Enhancers 7140 0 + + Metals + Silver 4630 4685 + Cobalt 506 540 + Palladium 13876 13877 + + Minerals + Uraninite 683 715 + Bauxite 23 31 + Rutile 126 140 + Indite 1659 1704 + Coltan 1000 1044 + + Technology + Bioreducing Lichen 1023 0 + H.E. Suits 260 0 + + Waste + Biowaste 7 11 + + Weapons + Reactive Armor 2078 0 + Non-Lethal Wpns 1927 0 + + +@ LHS 5287/Mcarthur's Reach + + Chemicals + Explosives 290 0 + Hydrogen Fuels 47 48 + + Consumer Items + Dom. Appliances 552 0 + Clothing 307 0 + Consumer Tech 6569 0 + + Drugs + Beer 143 0 + + Foods + Coffee 1404 0 + Tea 1410 0 + Synthetic Meat 234 0 + Food Cartridges 103 0 + Fish 757 0 + Animal Meat 1404 0 + Grain 181 0 + Fruit and Vegetables 307 0 + + Machinery + Mineral Extractors 622 0 + + Medicines + Performance Enhancers 7140 0 + Basic Medicines 307 0 + + Minerals + Gallite 1342 1379 + Bertrandite 1825 1873 + Lepidolite 355 380 + + Technology + H.E. Suits 260 0 + Bioreducing Lichen 1023 0 + + Waste + Biowaste 7 11 + + Weapons + Reactive Armor 2203 0 + Non-Lethal Wpns 1936 0 + + +@ LP 64-194/Longyear Survey + + Chemicals + Hydrogen Fuels 67 67 + Explosives 290 0 + + Consumer Items + Clothing 307 0 + Consumer Tech 6566 0 + Dom. Appliances 467 0 + + Drugs + Beer 143 0 + Wine 234 0 + + Foods + Coffee 1404 0 + Fish 679 0 + Fruit and Vegetables 304 0 + Animal Meat 1404 0 + Grain 181 0 + Tea 1409 0 + Synthetic Meat 234 0 + Food Cartridges 103 0 + + Machinery + Mineral Extractors 622 0 + + Medicines + Performance Enhancers 7137 0 + Basic Medicines 307 0 + + Metals + Gold 8282 8375 + + Minerals + Bertrandite 1824 1872 + Bauxite 19 25 + Rutile 126 140 + Coltan 911 950 + + Technology + H.E. Suits 260 0 + Bioreducing Lichen 1022 0 + + Waste + Biowaste 11 18 + + Weapons + Personal Weapons 4101 0 + Reactive Armor 2201 0 + Non-Lethal Wpns 1935 0 + + +@ LP 98-132/Freeport + + Chemicals + Hydrogen Fuels 71 72 + Explosives 289 0 + + Consumer Items + Clothing 254 0 + Consumer Tech 6588 0 + Dom. Appliances 469 0 + + Drugs + Narcotics 125 0 + Wine 235 0 + Tobacco 4706 0 + Beer 143 0 + Liquor 664 0 + + Foods + Coffee 1239 0 + Tea 1414 0 + Animal Meat 1239 0 + Synthetic Meat 224 0 + Fruit and Vegetables 254 0 + Grain 164 0 + Fish 650 0 + Food Cartridges 103 0 + + Machinery + Mineral Extractors 532 0 + + Medicines + Performance Enhancers 6588 0 + Basic Medicines 308 0 + + Metals + Gold 9183 9285 + + Minerals + Bertrandite 1904 1954 + Bauxite 19 25 + Rutile 144 160 + Coltan 994 1037 + + Technology + H.E. Suits 254 0 + Bioreducing Lichen 1026 0 + + Waste + Biowaste 12 18 + + Weapons + Personal Weapons 4115 0 + Reactive Armor 1997 0 + Non-Lethal Wpns 1772 0 + + +@ MAGEC/Xiaoguan + + Chemicals + Hydrogen Fuels 56 0 + Pesticides 199 0 + Mineral Oil 62 73 + + Consumer Items + Consumer Tech 7114 0 + Dom. Appliances 550 0 + Clothing 306 0 + + Drugs + Beer 143 0 + + Foods + Fruit and Vegetables 128 142 + Grain 70 83 + Coffee 1023 1068 + Tea 1198 1249 + Fish 772 0 + Animal Meat 902 942 + + Machinery + Crop Harvesters 2341 0 + + Medicines + Agri-Medicines 1086 0 + Basic Medicines 306 0 + Performance Enhancers 7114 0 + Progenitor Cells 7114 0 + + Technology + Animal Monitors 289 0 + Terrain Enrich Sys 5093 0 + + Textiles + Natural Fabrics 237 255 + Leather 43 53 + + Waste + Biowaste 68 0 + + +@ MORGOR/Romanek's Folly + + Chemicals + Hydrogen Fuels 80 81 + Explosives 289 0 + + Consumer Items + Dom. Appliances 550 0 + Consumer Tech 6553 0 + Clothing 307 0 + + Drugs + Beer 143 0 + Liquor 660 0 + Narcotics 122 0 + Tobacco 4763 0 + Wine 234 0 + + Foods + Tea 1407 0 + Synthetic Meat 234 0 + Food Cartridges 102 0 + Fish 646 0 + Animal Meat 1232 0 + Grain 181 0 + Fruit and Vegetables 307 0 + Coffee 1234 0 + + Machinery + Mineral Extractors 609 0 + + Medicines + Performance Enhancers 7123 0 + Basic Medicines 307 0 + + Minerals + Gallite 1339 1376 + Bertrandite 1827 1876 + Lepidolite 302 323 + + Technology + H.E. Suits 259 0 + Bioreducing Lichen 1020 0 + + Waste + Biowaste 11 18 + + Weapons + Personal Weapons 4093 0 + + +@ NANG TA-KHIAN/Hay Point + + Chemicals + Hydrogen Fuels 62 0 + + Consumer Items + Consumer Tech 6956 0 + Dom. Appliances 345 369 + Clothing 307 0 + + Drugs + Beer 143 0 + Wine 233 0 + + Foods + Grain 180 0 + Synthetic Meat 233 0 + Fish 773 0 + Animal Meat 1401 0 + Food Cartridges 102 0 + Fruit and Vegetables 307 0 + Algae 95 0 + Coffee 1401 0 + Tea 1592 0 + + Industrial Materials + Polymers 115 0 + Semiconductors 958 0 + Superconductors 7120 0 + + Machinery + Mineral Extractors 398 426 + Crop Harvesters 1912 1963 + + Medicines + Progenitor Cells 7120 0 + Basic Medicines 307 0 + Performance Enhancers 7120 0 + + Metals + Indium 6242 0 + Tantalum 4210 0 + Uranium 2846 0 + Lithium 1697 0 + Titanium 1087 0 + Copper 487 0 + Gold 9745 0 + Aluminium 324 0 + + Minerals + Beryllium 8668 0 + Gallium 5473 0 + + Technology + H.E. Suits 259 0 + Robotics 1930 0 + Auto-Fabricators 3943 0 + + Textiles + Leather 143 0 + Natural Fabrics 408 0 + Synthetic Fabrics 156 0 + + Waste + Biowaste 11 18 + Scrap 30 37 + + Weapons + Personal Weapons 3832 3927 + + +@ NARAKA/Novitski Oasis + + Chemicals + Hydrogen Fuels 56 0 + Pesticides 199 0 + + Consumer Items + Clothing 306 0 + Consumer Tech 7103 0 + Dom. Appliances 549 0 + + Drugs + Beer 142 0 + + Foods + Fruit and Vegetables 276 0 + Grain 61 72 + Tea 1588 0 + Animal Meat 881 922 + Fish 771 0 + Coffee 1397 0 + + Machinery + Crop Harvesters 2337 0 + Marine Supplies 4484 0 + + Medicines + Agri-Medicines 1084 0 + Progenitor Cells 7103 0 + Basic Medicines 306 0 + Performance Enhancers 7103 0 + + Technology + Terrain Enrich Sys 4668 0 + Animal Monitors 289 0 + Aquaponic Systems 259 0 + + Textiles + Leather 38 48 + + Waste + Biowaste 68 0 + + +@ OPALA/Romanenko + + Chemicals + Hydrogen Fuels 56 0 + Pesticides 199 0 + + Consumer Items + Dom. Appliances 549 0 + Consumer Tech 7103 0 + Clothing 306 0 + + Drugs + Beer 142 0 + + Foods + Algae 10 16 + Fruit and Vegetables 306 0 + Coffee 1397 0 + Tea 1588 0 + Fish 443 478 + Animal Meat 1397 0 + Grain 71 84 + + Machinery + Crop Harvesters 2337 0 + Marine Supplies 4484 0 + + Medicines + Basic Medicines 306 0 + Performance Enhancers 7103 0 + Agri-Medicines 1084 0 + Progenitor Cells 7103 0 + + Technology + Aquaponic Systems 259 0 + Terrain Enrich Sys 4709 0 + Animal Monitors 289 0 + + Waste + Biowaste 68 0 + + +@ OVID/Bradfield + + Chemicals + Hydrogen Fuels 62 0 + + Consumer Items + Consumer Tech 7116 0 + Clothing 306 0 + Dom. Appliances 550 0 + + Drugs + Beer 143 0 + + Foods + Fish 767 0 + Synthetic Meat 233 0 + Food Cartridges 102 0 + Grain 180 0 + Tea 1591 0 + Animal Meat 1378 0 + Coffee 1400 0 + Fruit and Vegetables 306 0 + Algae 95 0 + + Industrial Materials + Semiconductors 958 0 + Superconductors 7116 0 + Polymers 115 0 + + Machinery + Hel-Static Furnaces 45 53 + Mineral Extractors 301 323 + Crop Harvesters 1911 1962 + Marine Supplies 3911 4008 + + Medicines + Progenitor Cells 7116 0 + Basic Medicines 306 0 + Performance Enhancers 7116 0 + + Metals + Titanium 1086 0 + Lithium 1696 0 + Copper 487 0 + Aluminium 324 0 + Uranium 2845 0 + Tantalum 4207 0 + Indium 6238 0 + Gold 9898 0 + + Minerals + Beryllium 8673 0 + Gallium 5470 0 + + Technology + Computer Components 261 280 + H.E. Suits 259 0 + Robotics 1929 0 + Auto-Fabricators 3941 0 + + Textiles + Leather 143 0 + Natural Fabrics 408 0 + Synthetic Fabrics 156 0 + + Waste + Biowaste 11 18 + Scrap 20 24 + + +@ PI-FANG/Brooks + + Chemicals + Mineral Oil 41 49 + Hydrogen Fuels 58 0 + Pesticides 199 0 + + Consumer Items + Consumer Tech 7099 0 + Clothing 306 0 + Dom. Appliances 548 0 + + Drugs + Beer 120 0 + Wine 211 0 + + Foods + Synthetic Meat 233 0 + Coffee 1396 0 + Fish 771 0 + Animal Meat 1396 0 + Grain 51 60 + Fruit and Vegetables 116 130 + Tea 1587 0 + + Machinery + Crop Harvesters 2336 0 + Marine Supplies 4481 0 + + Medicines + Progenitor Cells 7099 0 + Basic Medicines 306 0 + Performance Enhancers 7099 0 + Agri-Medicines 1084 0 + + Metals + Gold 9874 0 + Silver 5109 0 + + Technology + Animal Monitors 289 0 + Aquaponic Systems 258 0 + Terrain Enrich Sys 5109 0 + + Waste + Biowaste 68 0 + + Weapons + Personal Weapons 4481 0 + + +@ RAKAPILA/Stone Enterprise + + Chemicals + Hydrogen Fuels 62 0 + + Consumer Items + Consumer Tech 7116 0 + Dom. Appliances 345 369 + Clothing 116 129 + + Drugs + Beer 143 0 + + Foods + Animal Meat 1400 0 + Food Cartridges 102 0 + Fruit and Vegetables 306 0 + Synthetic Meat 233 0 + Grain 180 0 + Fish 772 0 + Algae 95 0 + Coffee 1400 0 + Tea 1591 0 + + Industrial Materials + Polymers 115 0 + Semiconductors 958 0 + Superconductors 7116 0 + + Medicines + Performance Enhancers 7116 0 + Progenitor Cells 7116 0 + Basic Medicines 306 0 + + Metals + Gold 9898 0 + Aluminium 324 0 + Copper 487 0 + Titanium 1086 0 + Lithium 1696 0 + Uranium 2845 0 + Tantalum 4207 0 + Indium 6238 0 + + Minerals + Beryllium 8673 0 + Gallium 5470 0 + + Technology + H.E. Suits 259 0 + Robotics 1929 0 + Auto-Fabricators 3941 0 + + Textiles + Synthetic Fabrics 156 0 + Natural Fabrics 408 0 + Leather 143 0 + + Waste + Biowaste 11 18 + Scrap 20 24 + + +@ ROSS 1015/Bowersox + + Chemicals + Explosives 291 0 + Hydrogen Fuels 74 75 + + Consumer Items + Dom. Appliances 553 0 + Clothing 308 0 + Consumer Tech 6588 0 + + Drugs + Beer 143 0 + + Foods + Coffee 1409 0 + Tea 1414 0 + Synthetic Meat 235 0 + Food Cartridges 103 0 + Fish 650 0 + Animal Meat 1239 0 + Grain 182 0 + Fruit and Vegetables 308 0 + + Machinery + Mineral Extractors 624 0 + + Medicines + Performance Enhancers 7162 0 + Basic Medicines 308 0 + + Minerals + Gallite 1346 1383 + Bertrandite 1830 1878 + Lepidolite 303 325 + + Technology + H.E. Suits 261 0 + Bioreducing Lichen 1026 0 + + Waste + Biowaste 11 18 + + Weapons + Reactive Armor 2170 0 + Non-Lethal Wpns 1942 0 + + +@ ROSS 1051/Abetti Platform + + Chemicals + Explosives 150 167 + Hydrogen Fuels 47 48 + Mineral Oil 164 0 + + Consumer Items + Dom. Appliances 467 0 + Consumer Tech 6566 0 + Clothing 253 0 + + Drugs + Beer 143 0 + + Foods + Animal Meat 1235 0 + Coffee 1333 0 + Tea 1409 0 + Synthetic Meat 234 0 + Fish 647 0 + Food Cartridges 103 0 + Grain 181 0 + Fruit and Vegetables 307 0 + + Industrial Materials + Polymers 19 25 + Superconductors 6591 6667 + Semiconductors 533 558 + + Machinery + Hel-Static Furnaces 172 0 + + Medicines + Basic Medicines 307 0 + Performance Enhancers 6921 0 + + Metals + Copper 223 239 + Indium 4997 5055 + Titanium 617 646 + + Minerals + Gallium 4214 4264 + Gallite 1838 0 + Coltan 1495 0 + Bauxite 116 0 + Bertrandite 2400 0 + Uraninite 961 0 + Lepidolite 622 0 + Rutile 325 0 + Indite 2178 0 + + Technology + Resonating Separators 5739 0 + Advanced Catalysers 2742 0 + H.E. Suits 260 0 + + Textiles + Synthetic Fabrics 38 46 + + Waste + Biowaste 7 11 + Scrap 91 0 + + Weapons + Non-Lethal Wpns 1935 0 + Reactive Armor 2173 0 + + +@ TILIAN/Maunder + + Chemicals + Hydrogen Fuels 62 0 + + Consumer Items + Dom. Appliances 342 367 + Consumer Tech 6952 0 + Clothing 116 129 + + Drugs + Beer 143 0 + + Foods + Fruit and Vegetables 306 0 + Fish 772 0 + Animal Meat 1400 0 + Grain 180 0 + Coffee 1400 0 + Food Cartridges 13 19 + Algae 95 0 + Tea 1591 0 + Synthetic Meat 233 0 + + Industrial Materials + Polymers 115 0 + Semiconductors 958 0 + Superconductors 7116 0 + + Machinery + Mineral Extractors 312 334 + Crop Harvesters 1570 1612 + + Medicines + Progenitor Cells 7116 0 + Basic Medicines 116 129 + Performance Enhancers 7116 0 + + Metals + Indium 6238 0 + Tantalum 4207 0 + Uranium 2845 0 + Lithium 1696 0 + Titanium 1086 0 + Copper 487 0 + Gold 9898 0 + Aluminium 324 0 + + Minerals + Beryllium 8673 0 + Gallium 5470 0 + + Technology + H.E. Suits 259 0 + Robotics 1929 0 + Auto-Fabricators 3941 0 + + Textiles + Synthetic Fabrics 158 0 + Leather 143 0 + Natural Fabrics 408 0 + + Waste + Biowaste 7 11 + Scrap 20 24 + + +@ WYRD/Vonarburg Co-operative + + Chemicals + Hydrogen Fuels 56 0 + Pesticides 198 0 + Mineral Oil 62 74 + + Consumer Items + Clothing 305 0 + Consumer Tech 7091 0 + Dom. Appliances 548 0 + + Drugs + Beer 142 0 + Liquor 343 371 + Wine 232 0 + Tobacco 4238 4331 + + Foods + Fruit and Vegetables 305 0 + Grain 70 83 + Tea 981 1033 + Animal Meat 834 880 + Fish 328 358 + Coffee 848 894 + + Machinery + Crop Harvesters 2333 0 + Marine Supplies 4477 0 + + Medicines + Performance Enhancers 7091 0 + Progenitor Cells 7091 0 + Basic Medicines 305 0 + Agri-Medicines 1083 0 + + Technology + Terrain Enrich Sys 4660 0 + Animal Monitors 288 0 + Aquaponic Systems 258 0 + + Textiles + Leather 30 38 + + Waste + Biowaste 68 0 + + Weapons + Personal Weapons 4477 0 diff --git a/data/TradeDangerous.sql b/data/TradeDangerous.sql new file mode 100644 index 00000000..92082da5 --- /dev/null +++ b/data/TradeDangerous.sql @@ -0,0 +1,353 @@ +-- +-- This file contains definitions for all of the tables with +-- the exception of the price data. +-- +-- Price data is stored in a non-SQL format in the top level +-- in a file called "TradeDangerous.prices" +-- +-- If either file is changed, TradeDangerous will rebuild it's +-- sqlite3 database the next time it's run. +-- +-- You can edit this file, if you really need to, if you know +-- what you are doing. Or you can use the 'sqlite3' command +-- to edit the .db database and then use the '.dump' command +-- to regenerate this file, except then you'll lose this nice +-- header and I might have to wag my finger at you. +-- +-- -Oliver + +PRAGMA foreign_keys=ON; +BEGIN TRANSACTION; +CREATE TABLE System + ( + system_id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(40) COLLATE nocase, + pos_x DOUBLE NOT NULL, + pos_y DOUBLE NOT NULL, + pos_z DOUBLE NOT NULL, + modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + + UNIQUE (name) + ); +INSERT INTO "System" VALUES(1,'26 Draconis',-39.0,24.90625,-0.65625,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(2,'Acihaut',-18.5,25.28125,-4.0,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(3,'Aganippe',-11.5625,43.8125,11.625,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(4,'Asellus Primus',-23.9375,40.875,-1.34375,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(5,'Aulin',-19.6875,32.6875,4.75,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(6,'Aulis',-16.46875,44.1875,-11.4375,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(7,'BD+47 2112',-14.78125,33.46875,-0.40625,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(8,'BD+55 1519',-16.9375,44.71875,-16.59375,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(9,'Bolg',-7.90625,34.71875,2.125,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(10,'Chi Herculis',-30.75,39.71875,12.78125,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(11,'CM Draco',-35.6875,30.9375,2.15625,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(12,'Dahan',-19.75,41.78125,-3.1875,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(13,'DN Draconis',-27.09375,21.625,0.78125,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(14,'DP Draconis',-17.5,25.96875,-11.375,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(15,'Eranin',-22.84375,36.53125,-1.1875,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(16,'G 239-25',-22.6875,25.8125,-6.6875,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(17,'GD 319',-19.375,43.625,-12.75,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(18,'h Draconis',-39.84375,29.5625,-3.90625,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(19,'Hermitage',-28.75,25.0,10.4375,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(20,'i Bootis',-22.375,34.84375,4.0,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(21,'Ithaca',-8.09375,44.9375,-9.28125,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(22,'Keries',-18.90625,27.21875,12.59375,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(23,'Lalande 29917',-26.53125,22.15625,-4.5625,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(24,'LFT 1361',-38.78125,24.71875,-0.5,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(25,'LFT 880',-22.8125,31.40625,-18.34375,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(26,'LFT 992',-7.5625,42.59375,0.6875,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(27,'LHS 2819',-30.5,38.5625,-13.4375,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(28,'LHS 2884',-22.0,48.40625,1.78125,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(29,'LHS 2887',-7.34375,26.78125,5.71875,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(30,'LHS 3006',-21.96875,29.09375,-1.71875,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(31,'LHS 3262',-24.125,18.84375,4.90625,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(32,'LHS 417',-18.3125,18.1875,4.90625,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(33,'LHS 5287',-36.40625,48.1875,-0.78125,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(34,'LHS 6309',-33.5625,33.125,13.46875,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(35,'LP 271-25',-10.46875,31.84375,7.3125,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(36,'LP 275-68',-23.34375,25.0625,15.1875,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(37,'LP 64-194',-21.65625,32.21875,-16.21875,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(38,'LP 98-132',-26.78125,37.03125,-4.59375,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(39,'Magec',-32.875,36.15625,15.5,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(40,'Meliae',-17.3125,49.53125,-1.6875,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(41,'Morgor',-15.25,39.53125,-2.25,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(42,'Nang Ta-khian',-18.21875,26.5625,-6.34375,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(43,'Naraka',-34.09375,26.21875,-5.53125,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(44,'Opala',-25.5,35.25,9.28125,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(45,'Ovid',-28.0625,35.15625,14.8125,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(46,'Pi-fang',-34.65625,22.84375,-4.59375,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(47,'Rakapila',-14.90625,33.625,9.125,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(48,'Ross 1015',-6.09375,29.46875,3.03125,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(49,'Ross 1051',-37.21875,44.5,-5.0625,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(50,'Ross 1057',-32.3125,26.1875,-12.4375,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(51,'Styx',-24.3125,37.75,6.03125,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(52,'Surya',-38.46875,39.25,5.40625,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(53,'Tilian',-21.53125,22.3125,10.125,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(54,'WISE 1647+5632',-21.59375,17.71875,1.75,'2014-08-26 15:22:38'); +INSERT INTO "System" VALUES(55,'Wyrd',-11.625,31.53125,-3.9375,'2014-08-26 15:22:38'); +CREATE TABLE Station + ( + station_id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(40) COLLATE nocase, + system_id INTEGER NOT NULL, + ls_from_star DOUBLE NOT NULL, + + UNIQUE (name), + + FOREIGN KEY (system_id) REFERENCES System(system_id) + ON UPDATE CASCADE + ON DELETE CASCADE + ); +INSERT INTO "Station" VALUES(1,'Beagle 2 Landing',4,0.0); +INSERT INTO "Station" VALUES(2,'Gateway',12,0.0); +INSERT INTO "Station" VALUES(3,'Freeport',38,0.0); +INSERT INTO "Station" VALUES(4,'Chango Dock',20,0.0); +INSERT INTO "Station" VALUES(5,'Azeban City',15,0.0); +INSERT INTO "Station" VALUES(6,'Aulin Enterprise',5,0.0); +INSERT INTO "Station" VALUES(7,'WCM Transfer Orbital',30,0.0); +INSERT INTO "Station" VALUES(8,'Romanenko',44,0.0); +INSERT INTO "Station" VALUES(9,'Romanek''s Folly',41,0.0); +INSERT INTO "Station" VALUES(10,'Bradfield',45,0.0); +INSERT INTO "Station" VALUES(11,'Gorbatko',10,0.0); +INSERT INTO "Station" VALUES(12,'Cuffey Plant',2,0.0); +INSERT INTO "Station" VALUES(13,'Hay Point',42,0.0); +INSERT INTO "Station" VALUES(14,'Bresnik Mine',16,0.0); +INSERT INTO "Station" VALUES(15,'Vonarburg Co-operative',55,0.0); +INSERT INTO "Station" VALUES(16,'Olivas Settlement',7,0.0); +INSERT INTO "Station" VALUES(17,'Moxon''s Mojo',9,0.0); +INSERT INTO "Station" VALUES(18,'Bowersox',48,0.0); +INSERT INTO "Station" VALUES(19,'Massimino Dock',29,0.0); +INSERT INTO "Station" VALUES(20,'Stone Enterprise',47,0.0); +INSERT INTO "Station" VALUES(21,'Derrickson''s Escape',22,0.0); +INSERT INTO "Station" VALUES(22,'Xiaoguan',39,0.0); +INSERT INTO "Station" VALUES(23,'Gernhardt Camp',32,0.0); +INSERT INTO "Station" VALUES(24,'Louis De Lacaille Prospect',31,0.0); +INSERT INTO "Station" VALUES(25,'Maunder',53,0.0); +INSERT INTO "Station" VALUES(26,'Novitski Oasis',43,0.0); +INSERT INTO "Station" VALUES(27,'Brooks',46,0.0); +INSERT INTO "Station" VALUES(28,'Julian Market',3,0.0); +INSERT INTO "Station" VALUES(29,'Abnett Platform',28,0.0); +INSERT INTO "Station" VALUES(30,'Mcarthur''s Reach',33,0.0); +INSERT INTO "Station" VALUES(31,'Dezhurov Gateway',6,0.0); +INSERT INTO "Station" VALUES(32,'Hume Depot',21,0.0); +INSERT INTO "Station" VALUES(33,'Szulkin Mines',26,0.0); +INSERT INTO "Station" VALUES(34,'Baker Platform',25,0.0); +INSERT INTO "Station" VALUES(35,'Longyear Survey',37,0.0); +INSERT INTO "Station" VALUES(36,'Tasaki Freeport',27,0.0); +INSERT INTO "Station" VALUES(37,'Abetti Platform',49,0.0); +INSERT INTO "Station" VALUES(38,'Anderson Escape',11,0.0); +INSERT INTO "Station" VALUES(39,'Brislington',18,0.0); +CREATE TABLE Ship + ( + ship_id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(40) COLLATE nocase, + capacity INTEGER NOT NULL, + mass INTEGER NOT NULL, + drive_rating DOUBLE NOT NULL, + max_ly_empty DOUBLE NOT NULL, + max_ly_full DOUBLE NOT NULL, + max_speed INTEGER NOT NULL, + boost_speed INTEGER NOT NULL, + + UNIQUE (name) + ); +INSERT INTO "Ship" VALUES(1,'Eagle',6,52,348.0,6.59,6.0,240,350); +INSERT INTO "Ship" VALUES(2,'Sidewinder',4,47,348.0,8.13,7.25,220,293); +INSERT INTO "Ship" VALUES(3,'Hauler',16,39,348.0,8.74,6.1,200,246); +INSERT INTO "Ship" VALUES(4,'Viper',8,40,348.0,13.49,9.16,320,500); +INSERT INTO "Ship" VALUES(5,'Cobra',36,114,1155.0,9.94,7.3,280,400); +INSERT INTO "Ship" VALUES(6,'Lakon Type 6',100,113,3455.0,29.36,15.64,220,329); +INSERT INTO "Ship" VALUES(7,'Lakon Type 9',440,1275,23720.0,18.22,13.34,130,200); +INSERT INTO "Ship" VALUES(8,'Anaconda',228,2600,52345.0,19.7,17.6,180,235); +CREATE TABLE ShipVendor + ( + ship_id INTEGER NOT NULL, + station_id INTEGER NOT NULL, + cost INTEGER, + + PRIMARY KEY (ship_id, station_id), + + FOREIGN KEY (ship_id) REFERENCES Ship(ship_id) + ON UPDATE CASCADE + ON DELETE CASCADE, + FOREIGN KEY (station_id) REFERENCES Station(station_id) + ON UPDATE CASCADE + ON DELETE CASCADE + ) WITHOUT ROWID +; +INSERT INTO "ShipVendor" VALUES(1,1,0); +INSERT INTO "ShipVendor" VALUES(1,6,0); +INSERT INTO "ShipVendor" VALUES(2,1,0); +INSERT INTO "ShipVendor" VALUES(2,6,0); +INSERT INTO "ShipVendor" VALUES(3,1,0); +INSERT INTO "ShipVendor" VALUES(3,6,0); +INSERT INTO "ShipVendor" VALUES(4,1,0); +INSERT INTO "ShipVendor" VALUES(4,4,0); +INSERT INTO "ShipVendor" VALUES(4,6,0); +INSERT INTO "ShipVendor" VALUES(5,4,0); +INSERT INTO "ShipVendor" VALUES(5,6,0); +INSERT INTO "ShipVendor" VALUES(6,4,0); +INSERT INTO "ShipVendor" VALUES(6,6,0); +INSERT INTO "ShipVendor" VALUES(6,15,0); +INSERT INTO "ShipVendor" VALUES(7,4,0); +INSERT INTO "ShipVendor" VALUES(8,24,0); +CREATE TABLE Upgrade + ( + upgrade_id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(40) COLLATE nocase, + weight NUMBER NOT NULL, + + UNIQUE (name) + ); +CREATE TABLE UpgradeVendor + ( + upgrade_id INTEGER NOT NULL, + station_id INTEGER NOT NULL, + cost INTEGER, + + PRIMARY KEY (upgrade_id, station_id), + + FOREIGN KEY (upgrade_id) REFERENCES Upgrade(upgrade_id) + ON UPDATE CASCADE + ON DELETE CASCADE, + FOREIGN KEY (station_id) REFERENCES Station(station_id) + ON UPDATE CASCADE + ON DELETE CASCADE + ) WITHOUT ROWID +; +CREATE TABLE Category + ( + category_id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(40) COLLATE nocase, + + UNIQUE (name) + ); +INSERT INTO "Category" VALUES(1,'Chemicals'); +INSERT INTO "Category" VALUES(2,'Consumer Items'); +INSERT INTO "Category" VALUES(3,'Foods'); +INSERT INTO "Category" VALUES(4,'Industrial Materials'); +INSERT INTO "Category" VALUES(5,'Medicines'); +INSERT INTO "Category" VALUES(6,'Metals'); +INSERT INTO "Category" VALUES(7,'Minerals'); +INSERT INTO "Category" VALUES(8,'Technology'); +INSERT INTO "Category" VALUES(9,'Waste'); +INSERT INTO "Category" VALUES(10,'Weapons'); +INSERT INTO "Category" VALUES(11,'Drugs'); +INSERT INTO "Category" VALUES(12,'Machinery'); +INSERT INTO "Category" VALUES(13,'Textiles'); +CREATE TABLE Item + ( + item_id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(40) COLLATE nocase, + category_id INTEGER NOT NULL, + + UNIQUE (category_id, name), + + FOREIGN KEY (category_id) REFERENCES Category(category_id) + ON UPDATE CASCADE + ON DELETE CASCADE + ); +INSERT INTO "Item" VALUES(1,'Explosives',1); +INSERT INTO "Item" VALUES(2,'Hydrogen Fuels',1); +INSERT INTO "Item" VALUES(3,'Mineral Oil',1); +INSERT INTO "Item" VALUES(4,'Pesticides',1); +INSERT INTO "Item" VALUES(5,'Clothing',2); +INSERT INTO "Item" VALUES(6,'Consumer Tech',2); +INSERT INTO "Item" VALUES(7,'Dom. Appliances',2); +INSERT INTO "Item" VALUES(8,'Algae',3); +INSERT INTO "Item" VALUES(9,'Animal Meat',3); +INSERT INTO "Item" VALUES(10,'Coffee',3); +INSERT INTO "Item" VALUES(11,'Fish',3); +INSERT INTO "Item" VALUES(12,'Food Cartridges',3); +INSERT INTO "Item" VALUES(13,'Grain',3); +INSERT INTO "Item" VALUES(14,'Tea',3); +INSERT INTO "Item" VALUES(15,'Alloys',4); +INSERT INTO "Item" VALUES(16,'Plastics',4); +INSERT INTO "Item" VALUES(17,'Basic Medicines',5); +INSERT INTO "Item" VALUES(18,'Aluminium',6); +INSERT INTO "Item" VALUES(19,'Copper',6); +INSERT INTO "Item" VALUES(20,'Gold',6); +INSERT INTO "Item" VALUES(21,'Tantalum',6); +INSERT INTO "Item" VALUES(22,'Titanium',6); +INSERT INTO "Item" VALUES(23,'Bauxite',7); +INSERT INTO "Item" VALUES(24,'Coltan',7); +INSERT INTO "Item" VALUES(25,'Rutile',7); +INSERT INTO "Item" VALUES(26,'Advanced Catalysers',8); +INSERT INTO "Item" VALUES(27,'Animal Monitors',8); +INSERT INTO "Item" VALUES(28,'Aquaponic Systems',8); +INSERT INTO "Item" VALUES(29,'Computer Components',8); +INSERT INTO "Item" VALUES(30,'Robotics',8); +INSERT INTO "Item" VALUES(31,'Terrain Enrich Sys',8); +INSERT INTO "Item" VALUES(32,'Biowaste',9); +INSERT INTO "Item" VALUES(33,'Scrap',9); +INSERT INTO "Item" VALUES(34,'Reactive Armor',10); +INSERT INTO "Item" VALUES(35,'Personal Weapons',10); +INSERT INTO "Item" VALUES(36,'Liquor',11); +INSERT INTO "Item" VALUES(37,'Crop Harvesters',12); +INSERT INTO "Item" VALUES(38,'Marine Supplies',12); +INSERT INTO "Item" VALUES(39,'Cotton',13); +INSERT INTO "Item" VALUES(40,'Leather',13); +INSERT INTO "Item" VALUES(41,'Hel-Static Furnaces',12); +INSERT INTO "Item" VALUES(42,'Mineral Extractors',12); +INSERT INTO "Item" VALUES(43,'Progenitor Cells',5); +INSERT INTO "Item" VALUES(44,'Performance Enhancers',5); +INSERT INTO "Item" VALUES(45,'Agri-Medicines',5); +INSERT INTO "Item" VALUES(46,'Combat Stabilisers',5); +INSERT INTO "Item" VALUES(47,'Auto-Fabricators',8); +INSERT INTO "Item" VALUES(48,'H.E. Suits',8); +INSERT INTO "Item" VALUES(49,'Non-Lethal Wpns',10); +INSERT INTO "Item" VALUES(50,'Narcotics',11); +INSERT INTO "Item" VALUES(51,'Resonating Separators',8); +INSERT INTO "Item" VALUES(52,'Bertrandite',7); +INSERT INTO "Item" VALUES(53,'Bioreducing Lichen',8); +INSERT INTO "Item" VALUES(54,'Indite',7); +INSERT INTO "Item" VALUES(55,'Gallite',7); +INSERT INTO "Item" VALUES(56,'Lepidolite',7); +INSERT INTO "Item" VALUES(57,'Beer',11); +INSERT INTO "Item" VALUES(58,'Wine',11); +INSERT INTO "Item" VALUES(59,'Fruit and Vegetables',3); +INSERT INTO "Item" VALUES(60,'Synthetic Meat',3); +INSERT INTO "Item" VALUES(61,'Superconductors',4); +INSERT INTO "Item" VALUES(62,'Silver',6); +INSERT INTO "Item" VALUES(63,'Lithium',6); +INSERT INTO "Item" VALUES(64,'Palladium',6); +INSERT INTO "Item" VALUES(65,'Cobalt',6); +INSERT INTO "Item" VALUES(66,'Indium',6); +INSERT INTO "Item" VALUES(67,'Uranium',6); +INSERT INTO "Item" VALUES(68,'Gallium',7); +INSERT INTO "Item" VALUES(69,'Beryllium',7); +INSERT INTO "Item" VALUES(70,'Semiconductors',4); +INSERT INTO "Item" VALUES(71,'Polymers',4); +INSERT INTO "Item" VALUES(72,'Natural Fabrics',13); +INSERT INTO "Item" VALUES(73,'Synthetic Fabrics',13); +INSERT INTO "Item" VALUES(74,'Uraninite',7); +INSERT INTO "Item" VALUES(75,'Tobacco',11); +CREATE TABLE Price + ( + item_id INTEGER NOT NULL, + station_id INTEGER NOT NULL, + ui_order INTEGER NOT NULL DEFAULT 0, + -- how many credits will the station pay for this item? + sell_to INTEGER NOT NULL, + -- how many credits must you pay to buy at this station? + buy_from INTEGER NOT NULL DEFAULT 0, + modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + + PRIMARY KEY (item_id, station_id), + + FOREIGN KEY (item_id) REFERENCES Item(item_id) + ON UPDATE CASCADE + ON DELETE CASCADE, + FOREIGN KEY (station_id) REFERENCES Station(station_id) + ON UPDATE CASCADE + ON DELETE CASCADE + ) WITHOUT ROWID +; +DELETE FROM sqlite_sequence; +INSERT INTO "sqlite_sequence" VALUES('System',55); +INSERT INTO "sqlite_sequence" VALUES('Station',39); +INSERT INTO "sqlite_sequence" VALUES('Ship',8); +INSERT INTO "sqlite_sequence" VALUES('Category',13); +INSERT INTO "sqlite_sequence" VALUES('Item',75); +CREATE INDEX systems_position ON System (pos_x, pos_y, pos_z); +CREATE INDEX station_systems ON Station (system_id); +COMMIT; diff --git a/data/dbdef.sql b/data/dbdef.sql new file mode 100644 index 00000000..0144af25 --- /dev/null +++ b/data/dbdef.sql @@ -0,0 +1,154 @@ +-- Source for the TradeDangerous database. + +-- I'm using foreign keys for referential integrity. +PRAGMA foreign_keys = ON; + +-- Star systems +CREATE TABLE + System + ( + system_id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(40) COLLATE nocase, + pos_x DOUBLE NOT NULL, + pos_y DOUBLE NOT NULL, + pos_z DOUBLE NOT NULL, + modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + + UNIQUE (name) + ) +; +CREATE INDEX systems_position ON System (pos_x, pos_y, pos_z); + +-- Stations within systems +CREATE TABLE + Station + ( + station_id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(40) COLLATE nocase, + system_id INTEGER NOT NULL, + ls_from_star DOUBLE NOT NULL, + + UNIQUE (name), + + FOREIGN KEY (system_id) REFERENCES System(system_id) + ON UPDATE CASCADE + ON DELETE CASCADE + ) +; +CREATE INDEX station_systems ON Station (system_id); + +-- Ships +CREATE TABLE + Ship + ( + ship_id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(40) COLLATE nocase, + capacity INTEGER NOT NULL, + mass INTEGER NOT NULL, + drive_rating DOUBLE NOT NULL, + max_ly_empty DOUBLE NOT NULL, + max_ly_full DOUBLE NOT NULL, + max_speed INTEGER NOT NULL, + boost_speed INTEGER NOT NULL, + + UNIQUE (name) + ) +; + +-- Where ships can be bought +CREATE TABLE + ShipVendor + ( + ship_id INTEGER NOT NULL, + station_id INTEGER NOT NULL, + cost INTEGER, + + PRIMARY KEY (ship_id, station_id), + + FOREIGN KEY (ship_id) REFERENCES Ship(ship_id) + ON UPDATE CASCADE + ON DELETE CASCADE, + FOREIGN KEY (station_id) REFERENCES Station(station_id) + ON UPDATE CASCADE + ON DELETE CASCADE + ) WITHOUT ROWID +; + +CREATE TABLE + Upgrade + ( + upgrade_id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(40) COLLATE nocase, + weight NUMBER NOT NULL, + + UNIQUE (name) + ) +; + +CREATE TABLE + UpgradeVendor + ( + upgrade_id INTEGER NOT NULL, + station_id INTEGER NOT NULL, + cost INTEGER, + + PRIMARY KEY (upgrade_id, station_id), + + FOREIGN KEY (upgrade_id) REFERENCES Upgrade(upgrade_id) + ON UPDATE CASCADE + ON DELETE CASCADE, + FOREIGN KEY (station_id) REFERENCES Station(station_id) + ON UPDATE CASCADE + ON DELETE CASCADE + ) WITHOUT ROWID +; + +-- Trade items are divided up into categories +CREATE TABLE + Category + ( + category_id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(40) COLLATE nocase, + + UNIQUE (name) + ) +; + +-- Tradeable items +CREATE TABLE + Item + ( + item_id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(40) COLLATE nocase, + category_id INTEGER NOT NULL, + + UNIQUE (category_id, name), + + FOREIGN KEY (category_id) REFERENCES Category(category_id) + ON UPDATE CASCADE + ON DELETE CASCADE + ) +; + +CREATE TABLE + Price + ( + item_id INTEGER NOT NULL, + station_id INTEGER NOT NULL, + ui_order INTEGER NOT NULL DEFAULT 0, + -- how many credits will the station pay for this item? + sell_to INTEGER NOT NULL, + -- how many credits must you pay to buy at this station? + buy_from INTEGER NOT NULL DEFAULT 0, + modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + + PRIMARY KEY (item_id, station_id), + + FOREIGN KEY (item_id) REFERENCES Item(item_id) + ON UPDATE CASCADE + ON DELETE CASCADE, + FOREIGN KEY (station_id) REFERENCES Station(station_id) + ON UPDATE CASCADE + ON DELETE CASCADE + ) WITHOUT ROWID +; diff --git a/data/ships.py b/data/ships.py new file mode 100644 index 00000000..105e4782 --- /dev/null +++ b/data/ships.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +# List of ships known to tradedb + +from collections import namedtuple + +class Ship(namedtuple('Ship', [ 'name', 'capacity', 'mass', 'driveRating', 'maxJump', 'maxJumpFull', 'maxSpeed', 'boostSpeed', 'stations' ])): + pass + +ships = [ + Ship('Eagle', 6, 52, 348, 6.59, 6.00, 240, 350, [ 'Aulin Enterprise', 'Beagle2' ]), + Ship('Sidewinder', 4, 47, 348, 8.13, 7.25, 220, 293, [ 'Aulin Enterprise', 'Beagle2' ]), + Ship('Hauler', 16, 39, 348, 8.74, 6.10, 200, 246, [ 'Aulin Enterprise', 'Beagle2' ]), + Ship('Viper', 8, 40, 348, 13.49, 9.16, 320, 500, [ 'Aulin Enterprise', 'Beagle2', 'Chango Dock' ]), + Ship('Cobra', 36, 114, 1155, 9.94, 7.30, 280, 400, [ 'Aulin Enterprise', 'Chango Dock' ]), + Ship('Lakon Type 6', 100, 113, 3455, 29.36, 15.64, 220, 329, [ 'Aulin Enterprise', 'Chango Dock', 'Vonarburg Co-op' ]), + Ship('Lakon Type 9', 440, 1275, 23720, 18.22, 13.34, 130, 200, [ 'Chango Dock' ]), + Ship('Anaconda', 228, 2600, 52345, 19.70, 17.60, 180, 235, [ 'Louis De Lacaille Prospect' ]), +] diff --git a/data/stars.py b/data/stars.py new file mode 100644 index 00000000..834eda57 --- /dev/null +++ b/data/stars.py @@ -0,0 +1,69 @@ +#! /usr/bin/env python +# Table of stars we know about so far. + +class Star(object): + def __init__(self, name, x, y, z): + self.name, self.x, self.y, self.z = name, x, y, z + self.tdID = None + self.tdStar = None + def __repr__(self): + return "Star(%s,%f,%f,%f)\n" % (self.name, self.x, self.y, self.z) + +# List of star system coordinates provided by wtbw +stars = [ + Star("26 Draconis",-39.000000,24.906250,-0.656250), + Star("Acihaut",-18.500000,25.281250,-4.000000), + Star("Aganippe",-11.562500,43.812500,11.625000), + Star("Asellus Primus",-23.937500,40.875000,-1.343750), + Star("Aulin",-19.687500,32.687500,4.750000), + Star("Aulis",-16.468750,44.187500,-11.437500), + Star("BD+47 2112",-14.781250,33.468750,-0.406250), + Star("BD+55 1519",-16.937500,44.718750,-16.593750), + Star("Bolg",-7.906250,34.718750,2.125000), + Star("Chi Herculis",-30.750000,39.718750,12.781250), + Star("CM Draco",-35.687500,30.937500,2.156250), + Star("Dahan",-19.750000,41.781250,-3.187500), + Star("DN Draconis",-27.093750,21.625000,0.781250), + Star("DP Draconis",-17.500000,25.968750,-11.375000), + Star("Eranin",-22.843750,36.531250,-1.187500), + Star("G 239-25",-22.687500,25.812500,-6.687500), + Star("GD 319",-19.375000,43.625000,-12.750000), + Star("h Draconis",-39.843750,29.562500,-3.906250), + Star("Hermitage",-28.750000,25.000000,10.437500), + Star("i Bootis",-22.375000,34.843750,4.000000), + Star("Ithaca",-8.093750,44.937500,-9.281250), + Star("Keries",-18.906250,27.218750,12.593750), + Star("Lalande 29917",-26.531250,22.156250,-4.562500), + Star("LFT 1361",-38.781250,24.718750,-0.500000), + Star("LFT 880",-22.812500,31.406250,-18.343750), + Star("LFT 992",-7.562500,42.593750,0.687500), + Star("LHS 2819",-30.500000,38.562500,-13.437500), + Star("LHS 2884",-22.000000,48.406250,1.781250), + Star("LHS 2887",-7.343750,26.781250,5.718750), + Star("LHS 3006",-21.968750,29.093750,-1.718750), + Star("LHS 3262",-24.125000,18.843750,4.906250), + Star("LHS 417",-18.312500,18.187500,4.906250), + Star("LHS 5287",-36.406250,48.187500,-0.781250), + Star("LHS 6309",-33.562500,33.125000,13.468750), + Star("LP 271-25",-10.468750,31.843750,7.312500), + Star("LP 275-68",-23.343750,25.062500,15.187500), + Star("LP 64-194",-21.656250,32.218750,-16.218750), + Star("LP 98-132",-26.781250,37.031250,-4.593750), + Star("Magec",-32.875000,36.156250,15.500000), + Star("Meliae",-17.312500,49.531250,-1.687500), + Star("Morgor",-15.250000,39.531250,-2.250000), + Star("Nang Ta-khian",-18.218750,26.562500,-6.343750), + Star("Naraka",-34.093750,26.218750,-5.531250), + Star("Opala",-25.500000,35.250000,9.281250), + Star("Ovid",-28.062500,35.156250,14.812500), + Star("Pi-fang",-34.656250,22.843750,-4.593750), + Star("Rakapila",-14.906250,33.625000,9.125000), + Star("Ross 1015",-6.093750,29.468750,3.031250), + Star("Ross 1051",-37.218750,44.500000,-5.062500), + Star("Ross 1057",-32.312500,26.187500,-12.437500), + Star("Styx",-24.312500,37.750000,6.031250), + Star("Surya",-38.468750,39.250000,5.406250), + Star("Tilian",-21.531250,22.312500,10.125000), + Star("WISE 1647+5632",-21.593750,17.718750,1.750000), + Star("Wyrd",-11.625000,31.531250,-3.937500), +] diff --git a/distances.py b/distances.py deleted file mode 100644 index 25ba99fa..00000000 --- a/distances.py +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/env python -# TradeDangerous :: Scripts :: Populate star database -# TradeDangerous Copyright (C) Oliver 'kfsone' Smith 2014 : -# You are free to use, redistribute, or even print and eat a copy of this -# software so long as you include this copyright notice. I guarantee that -# there is at least one bug neither of us knew about. -# -# Import data from http://forums.frontier.co.uk/showthread.php?t=34824 -# and ensure existing data is correct. - -import math - -from tradedb import * - -class Star(object): - def __init__(self, name, x, y, z, links): - self.name, self.x, self.y, self.z, self.links = name, x, y, z, links - self.tdID = None - self.tdStar = None - def __repr__(self): - return "Star(%s,%f,%f,%f,%s)\n" % (self.name, self.x, self.y, self.z, str(self.links)) - -# List of star system coordinates provided by wtbw -stars = [ - Star("26 Draconis",-39.000000,24.906250,-0.656250, {}), - Star("Acihaut",-18.500000,25.281250,-4.000000, {}), - Star("Aganippe",-11.562500,43.812500,11.625000, {}), - Star("Asellus Primus",-23.937500,40.875000,-1.343750, {}), - Star("Aulin",-19.687500,32.687500,4.750000, {}), - Star("Aulis",-16.468750,44.187500,-11.437500, {}), - Star("BD+47 2112",-14.781250,33.468750,-0.406250, {}), - Star("BD+55 1519",-16.937500,44.718750,-16.593750, {}), - Star("Bolg",-7.906250,34.718750,2.125000, {}), - Star("Chi Herculis",-30.750000,39.718750,12.781250, {}), - Star("CM Draco",-35.687500,30.937500,2.156250, {}), - Star("Dahan",-19.750000,41.781250,-3.187500, {}), - Star("DN Draconis",-27.093750,21.625000,0.781250, {}), - Star("DP Draconis",-17.500000,25.968750,-11.375000, {}), - Star("Eranin",-22.843750,36.531250,-1.187500, {}), - Star("G 239-25",-22.687500,25.812500,-6.687500, {}), - Star("GD 319",-19.375000,43.625000,-12.750000, {}), - Star("h Draconis",-39.843750,29.562500,-3.906250, {}), - Star("Hermitage",-28.750000,25.000000,10.437500, {}), - Star("i Bootis",-22.375000,34.843750,4.000000, {}), - Star("Ithaca",-8.093750,44.937500,-9.281250, {}), - Star("Keries",-18.906250,27.218750,12.593750, {}), - Star("Lalande 29917",-26.531250,22.156250,-4.562500, {}), - Star("LFT 1361",-38.781250,24.718750,-0.500000, {}), - Star("LFT 880",-22.812500,31.406250,-18.343750, {}), - Star("LFT 992",-7.562500,42.593750,0.687500, {}), - Star("LHS 2819",-30.500000,38.562500,-13.437500, {}), - Star("LHS 2884",-22.000000,48.406250,1.781250, {}), - Star("LHS 2887",-7.343750,26.781250,5.718750, {}), - Star("LHS 3006",-21.968750,29.093750,-1.718750, {}), - Star("LHS 3262",-24.125000,18.843750,4.906250, {}), - Star("LHS 417",-18.312500,18.187500,4.906250, {}), - Star("LHS 5287",-36.406250,48.187500,-0.781250, {}), - Star("LHS 6309",-33.562500,33.125000,13.468750, {}), - Star("LP 271-25",-10.468750,31.843750,7.312500, {}), - Star("LP 275-68",-23.343750,25.062500,15.187500, {}), - Star("LP 64-194",-21.656250,32.218750,-16.218750, {}), - Star("LP 98-132",-26.781250,37.031250,-4.593750, {}), - Star("Magec",-32.875000,36.156250,15.500000, {}), - Star("Meliae",-17.312500,49.531250,-1.687500, {}), - Star("Morgor",-15.250000,39.531250,-2.250000, {}), - Star("Nang Ta-khian",-18.218750,26.562500,-6.343750, {}), - Star("Naraka",-34.093750,26.218750,-5.531250, {}), - Star("Opala",-25.500000,35.250000,9.281250, {}), - Star("Ovid",-28.062500,35.156250,14.812500, {}), - Star("Pi-fang",-34.656250,22.843750,-4.593750, {}), - Star("Rakapila",-14.906250,33.625000,9.125000, {}), - Star("Ross 1015",-6.093750,29.468750,3.031250, {}), - Star("Ross 1051",-37.218750,44.500000,-5.062500, {}), - Star("Ross 1057",-32.312500,26.187500,-12.437500, {}), - Star("Styx",-24.312500,37.750000,6.031250, {}), - Star("Surya",-38.468750,39.250000,5.406250, {}), - Star("Tilian",-21.531250,22.312500,10.125000, {}), - Star("WISE 1647+5632",-21.593750,17.718750,1.750000, {}), - Star("Wyrd",-11.625000,31.531250,-3.937500, {}), -] - -# list of Star objects by their name -starsByName = { star.name: star for star in stars } - -# Build up the links table -print("Calculating distance matrix") -for lhsIdx in range(0, len(stars) - 1): - lhs = stars[lhsIdx] - lhsX, lhsY, lhsZ = lhs.x, lhs.y, lhs.z - for rhsIdx in range(lhsIdx + 1, len(stars)): - rhs = stars[rhsIdx] - xDelta, yDelta, zDelta = (rhs.x - lhsX), (rhs.y - lhsY), (rhs.z - lhsZ) - xd2, yd2, zd2 = (xDelta * xDelta), (yDelta * yDelta), (zDelta * zDelta) - # Truncate to 2 decimal places but round up, thus 0.841 becomes 0.842 - distance = math.ceil(math.sqrt(xd2 + yd2 + zd2) * 100) / 100 - lhs.links[rhs] = distance - rhs.links[lhs] = distance - -print("Opening database") -tdb = TradeDB(r'.\TradeDangerous.accdb') - -# Import correct star names. -# Original TD star names were kinda lazy before I added partial matching. -# Check the existing database for a likely target for this star. -print("Importing names") -for star in stars: - starName = star.name - if starName.find('\'') != -1: - raise ValueError("Apostrophe in star name not handled") - - norm = tdb.normalized_str(starName) - tdStation = None - for curStnID, station in tdb.stations.items(): - tdStarName = station.system.system - tdNorm = tdb.normalized_str(tdStarName) - if norm.find(tdNorm) == 0: - if tdStation: - raise ValueError("%s could be %s or %s" % (star.name, tdStation.system.system, tdStarName)) - tdStation = station - if not tdStation: - print("* TD is missing %s" % star.name) - continue - star.tdID = tdStation.ID - star.tdStar = tdStation.system - tdStarName = tdStation.system.system - if starName != tdStarName: - print("%s changing to %s" % (tdStarName, starName)) - tdb.query("UPDATE Stations SET `system` = '%s' WHERE Stations.ID = %d" % (starName, tdStation.ID)).commit() - -# Now process all the links. -print("Importing distance matrix") -for star in stars: - srcID = star.tdID - if not srcID: - continue - for dest, dist in star.links.items(): - dstID = dest.tdID - if not dstID: - continue - if not dest.tdStar in star.tdStar.links: - print("%s had no link to %s" % (star.name, dest.name)) - tdb.query("INSERT INTO Links (`from`, `to`, `distLy`) VALUES (%d, %d, %.2f)" % (srcID, dstID, dist)).commit() - else: - tdDist = star.tdStar.links[dest.tdStar] - if tdDist != dist: - print("%s -> %s is wrong: %.2f vs %.2f" % (star.name, dest.name, tdDist, dist)) - tdb.query("UPDATE Links SET `distLy` = %.2f WHERE `from` = %d AND `to` = %d" % (dist, srcID, dstID)).commit() diff --git a/generate-prices-from-db.py b/generate-prices-from-db.py new file mode 100644 index 00000000..27727337 --- /dev/null +++ b/generate-prices-from-db.py @@ -0,0 +1,108 @@ +#! /usr/bin/env python +# +# Generate .prices data from the current db. +# +# Note: This is NOT intended to be user friendly. If you don't know what this +# script is for, then you can safely ignore it. + +# Main imports. +import sys, os, re +import sqlite3 +from tradedb import TradeDB + +###################################################################### +# Main + +def main(): + print(""" +# SOURCE List of item prices for TradeDangerous. +# +# This file is used by TradeDangerous to populate the +# SQLite databsae with prices, whenever this file is +# updated or you delete the TradeDangerous.db file. + +# FORMAT: +# +# # ... +# A comment +# +# @ SYSTEM NAME/Station Name +# Sets the current station +# e.g. @ CHANGO/Chango Dock +# +# + Product Category +# Sets the current product category +# e.g. + Consumer Tech +# +# Item Name Value Cost +# Item value line. +# Item Name is the name of the item, e.g. Fish +# Value is what the STATION pays for the item, +# Cost is how much the item costs FROM the station +# +# Example: +# @ CHANGO/Chango Dock +# + Chemicals +# Mineral Oil 150 0 +# Hydrogen Fuels 63 0 +# Explosives 150 160 +# +# This gives prives for items under the "Chemicals" +# heading. Mineral oil was listed first and was selling +# at the station for 150cr. +# Hydrogen Fuels was listed 3rd and sells for 63 cr. +# Explosives was listed 2nd and sells for 150 cr AND +# can be bought here for 160cr. +# + +""") + + conn = sqlite3.connect(TradeDB.defaultDB) + cur = conn.cursor() + + systems = { ID: name for (ID, name) in cur.execute("SELECT system_id, name FROM system") } + stations = { ID: name for (ID, name) in cur.execute("SELECT station_id, name FROM station") } + categories = { ID: name for (ID, name) in cur.execute("SELECT category_id, name FROM category") } + items = { ID: name for (ID, name) in cur.execute("SELECT item_id, name FROM item") } + + # find longest item name + longestName = max(items.values(), key=lambda name: len(name)) + longestNameLen = len(longestName) + + cur.execute(""" + SELECT Station.system_id + , Price.station_id + , Item.category_id + , Price.item_id + , Price.sell_to + , Price.buy_from + FROM Station, Item, Category, Price + WHERE Station.station_id = Price.station_id + AND (Item.category_id = Category.category_id) AND Item.item_id = Price.item_id + ORDER BY Station.system_id, Station.station_id, Category.name, Price.ui_order, Price.item_id + """) + lastSys, lastStn, lastCat = None, None, None + for (sysID, stnID, catID, itemID, fromStn, toStn) in cur: + system = systems[sysID] + if system is not lastSys: + if lastStn: print("\n") + lastStn, lastCat = None, None + lastSys = system + + station = stations[stnID] + if station is not lastStn: + if lastStn: print("") + lastCat = None + print("@ {}/{}".format(system.upper(), station)) + lastStn = station + + category = categories[catID] + if category is not lastCat: + print(" + {}".format(category)) + lastCat = category + + print(" {:<{width}} {:7d} {:6d}".format(items[itemID], fromStn, toStn, width=longestNameLen)) + + +if __name__ == "__main__": + main() diff --git a/mfdwrapper.py b/mfdwrapper.py new file mode 100644 index 00000000..030ccbff --- /dev/null +++ b/mfdwrapper.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +#--------------------------------------------------------------------- +# Copyright (C) Oliver 'kfsone' Smith 2014 : +# You are free to use, redistribute, or even print and eat a copy of +# this software so long as you include this copyright notice. +# I guarantee there is at least one bug neither of us knew about. +#--------------------------------------------------------------------- +# TradeDangerous :: Modules :: Multi-function display wrapper +# +# Multi-Function Display wrappers +# + +class DummyMFD(object): + """ + Base class for the MFD drivers, implemented as no-ops so that + you can always use all MFD functions without conditionals. + """ + + def __init__(self): + pass + + + def finish(self): + """ + Close down the driver. + """ + pass + + + def display(self, line1, line2="", line3="", delay=None): + """ + Display data to the MFD. + Arguments: 1-3 lines of text plus optional pause in seconds. + """ + pass + + +class X52ProMFD(DummyMFD): + """ + Wrapper for the Saitek X52 Pro MFD. + """ + + def __init__(self): + try: + import saitek.X52Pro + self.doObj = saitek.X52Pro.SaitekX52Pro() + except DLLError as e: + print("{}: error#{}: Unable to initialize the Saitek X52 Pro module: {}".format(__name__, e.error_code, e.msg), file=sys.stderr) + sys.exit(1) + + self.page = self.doObj.add_page('TD') + self.display('TradeDangerous', 'INITIALIZING') + + + def finish(self): + self.doObj.finish() + + + def display(self, line1, line2="", line3="", delay=None): + self.page[0], self.page[1], self.page[2] = line1, line2, line3 + if delay: + import time + time.sleep(delay) diff --git a/misc/import-accdb-to-sq3.py b/misc/import-accdb-to-sq3.py new file mode 100644 index 00000000..21ac695d --- /dev/null +++ b/misc/import-accdb-to-sq3.py @@ -0,0 +1,256 @@ +#! /usr/bin/env python +###################################################################### +# Copyright (C) Oliver 'kfsone' Smith 2014 : +# You are free to use, redistribute, or even print and eat a copy of +# this software so long as you include this copyright notice. +# I guarantee there is at least one bug neither of us knew about. +###################################################################### +# CAUTION: NOT INTENDED FOR GENERAL END-USER OPERATION. +# If you don't know what this script is for, you can safely ignore it. +###################################################################### +# TradeDangerous :: Misc :: Legacy data import. +# +# Bootstrap a new SQLite3 database from python files and an existing +# Microsoft Access .ACCDB database. +# +# Note: This is NOT intended to be user friendly. If you don't know what this +# script is for, then you can safely ignore it. + +# Main imports. +import sys, os, re + +# We'll need a list of star systems. +import dataseed.stars +import dataseed.ships +from tradedb import * + +# Filenames/URIs +dbDef = "dataseed/dbdef.sql" +inDB = "Driver={Microsoft Access Driver (*.mdb, *.accdb)};DBQ=.\\TradeDangerous.accdb" +outDB = TradeDB.defaultDB + +systems, systemByID = {}, {} +stations, stationByOldID = {}, {} +categories, categoryByOldID = {}, {} +items, itemByOldID = {}, {} +debug = 1 + +# We also track the maximum distance any ship can jump, +# then we use this value to constrain links between stations. +maxJumpDistanceLy = 0.0 + +###################################################################### +# Helpers + +class check_item(object): + """ + Wrapper class that allows us to beautify the output as a sort + of checklist. + + Usage: + with check_item("Step description"): + things_in_step() + """ + margin = 60 + def __init__(self, title): + self.title, self.noop = title, False + def __enter__(self): + print('- {:.<{width}}: '.format(self.title, width=self.margin - 3), end='') + return self + def __exit__(self, type, value, traceback): + if value: # Exception occurred + print("\a\rX {:.<{width}}: ERROR".format(self.title.upper(), width=self.margin - 3)) + print() + else: + print('[+]') if not self.noop else print("\bNO-OP") + +def debug_log(level, message): + if debug >= level: + print(" | {:^54} |".format(message)) + +###################################################################### +# Main + +def main(): + # Destroy the SQLite database if it already exists. + try: os.remove(outDB) + except: pass + + with check_item("Connect to MS Access DB"): + import pypyodbc + inConn = pypyodbc.connect(inDB) + inCur = inConn.cursor() + + with check_item("Connect to SQLite3 DB"): + import sqlite3 + outConn = sqlite3.connect(outDB) + outCur = outConn.cursor() + # Add a function for calculating distance between stars + outConn.create_function("calc_distance_sq", 6, TradeDB.distanceSq) + + with check_item("Apply DDL commands from '%s'" % dbDef): + # Sure, I could have written: outCur.executescript(open(dbDef).read()).commit() + # but my perl days are behind me... + ddlScript = open(dbDef).read() + outCur.executescript(ddlScript) + + with check_item("Populate `System` table"): + # Star data is stored in 'stars.py' + stmt = "INSERT INTO System (name, pos_x, pos_y, pos_z) VALUES (?, ?, ?, ?)" + for star in data.stars.stars: + outCur.execute(stmt, [ star.name, star.x, star.y, star.z ]) + systemID = int(outCur.lastrowid) + system = System(systemID, star.name, star.x, star.y, star.z) + systems[TradeDB.normalizedStr(star.name)] = system + systemByID[systemID] = system + debug_log(1, "{} star systems loaded".format(len(systems))) + + with check_item("Populate `Station` table"): + # We're going to remap the station IDs to new IDs, and we're the accessdb + # hails back to ED:Beta 1 so it doesn't distinguish between systems and + # stations, so we'll need to make the associations between stations + # and the systems they are in. + + # Systems without a station were represented by a station called 'STARNAME*', + # and we're going to want to filter those out. + fakeNameRe = re.compile(r'\*$') + + inCur.execute("SELECT ID, station, system FROM Stations ORDER BY ID") + stmt = "INSERT INTO Station (name, system_id, ls_from_star) VALUES (?, ?, 0)" + for (ID, stationName, systemName) in inCur: + if fakeNameRe.search(stationName): + continue + + system = systems[TradeDB.normalizedStr(systemName)] + + outCur.execute(stmt, [ stationName, system.ID ]) + newStationID = int(outCur.lastrowid) + oldStationID = int(ID) + + station = Station(newStationID, system, stationName) + stations[stationName] = station + stationByOldID[oldStationID] = newStationID + debug_log(1, "{} stations loaded".format(len(stations))) + + with check_item("Populate `Ship` table"): + global maxJumpDistanceLy + # I'm not entirely sure whether I really want this data in the database, + # it seems perfectly fine to maintain it as a python script. + stmt = """ + INSERT INTO Ship + ( name, capacity, mass, drive_rating, max_ly_empty, max_ly_full, max_speed, boost_speed ) + VALUES + ( ?, ?, ?, ?, ?, ?, ?, ? ) + """ + rows = [] + for ship in data.ships.ships: + assert ship.maxJump > 0 and ship.maxJumpFull > 0 + assert ship.maxJumpFull <= ship.maxJump + rows += [ [ + ship.name + , ship.capacity, ship.mass, ship.driveRating + , ship.maxJump, ship.maxJumpFull + , ship.maxSpeed, ship.boostSpeed + ] ] + maxJumpDistanceLy = max(maxJumpDistanceLy, ship.maxJump, ship.maxJumpFull) + outCur.executemany(stmt, rows) + debug_log(1, "{} ships loaded".format(len(data.ships.ships))) + debug_log(1, "Maximum distance any ship jumps: {:.2f}ly".format(maxJumpDistanceLy)) + + shipLocations = 0 + with check_item("Populate `ShipVendor` table"): + stmt = """ + INSERT INTO ShipVendor + ( ship_id, station_id, cost ) + VALUES + ( (SELECT ship_id FROM Ship WHERE Ship.name = ?), ?, ? ) + """ + rows = [] + for ship in data.ships.ships: + for stationName in ship.stations: + station = stations[TradeDB.listSearch("Station", stationName, stations)] + rows += [ [ + ship.name, station.ID, 0 # We don't have prices yet. + ] ] + outCur.executemany(stmt, rows) + shipLocations = len(rows) + debug_log(1, "{} ship locations loaded".format(shipLocations)) + + with check_item("Populate `Upgrade` table") as check: + # TODO: Populate Upgrade + check.noop = True + + with check_item("Populate `UpgradeVendor' table") as check: + # TODO: UpgradeVendor + check.noop = True + + with check_item("Populate `Category` table") as check: + # Copy from accdb and track the newly assigned ID. + inCur.execute("SELECT ID, category FROM Categories ORDER BY ID") + categoryID = 0 + stmt = "INSERT INTO Category (category_id, name) VALUES (?, ?)" + rows = [] + for (ID, categoryName) in inCur: + categoryID += 1 + rows += [ [ categoryID, categoryName ] ] + categories[TradeDB.normalizedStr(categoryName)] = categoryID + categoryByOldID[ID] = categoryID + outCur.executemany(stmt, rows) + debug_log(1, "{} item categories loaded".format(len(categories))) + + with check_item("Populate `Item` table") as check: + inCur.execute("SELECT ID, category_id, item FROM Items") + stmt = "INSERT INTO Item (item_id, category_id, name) VALUES (?, ?, ?)" + rows = [] + itemID = 0 + for (ID, category_id, itemName) in inCur: + itemID += 1 + rows += [ [ itemID, categoryByOldID[category_id], itemName ] ] + items[TradeDB.normalizedStr(itemName)] = itemID + itemByOldID[ID] = itemID + outCur.executemany(stmt, rows) + debug_log(1, "{} items loaded".format(len(items))) + + pricesCount = 0 + with check_item("Populate `Price` table") as check: + inCur.execute("SELECT station_id, item_id, sell_cr, buy_cr, ui_order FROM Prices ORDER BY item_id, station_id") + stmt = """ + INSERT INTO Price + (item_id, station_id, ui_order, sell_to, buy_from) + VALUES + (?, ?, ?, ?, ?) + """ + rows = [] + for (oldStationID, oldItemID, stnPayingCr, stnAskingCr, uiOrder) in inCur: + itemID, stationID = itemByOldID[oldItemID], stationByOldID[oldStationID] + rows += [ [ itemID, stationID, uiOrder or 0, stnPayingCr, stnAskingCr or 0 ] ] + outCur.executemany(stmt, rows) + pricesCount = len(rows) + debug_log(1, "{} prices loaded".format(pricesCount)) + + outConn.commit() + + numLinks = 0 + with check_item("Checking system links/performance") as check: + import math + outCur.execute(""" + SELECT from_system_id, to_system_id, distance_sq + FROM vLink + WHERE from_system_id < to_system_id + AND distance_sq <= ? + """, [ maxJumpDistanceLy ** 2]) + links = {} + for (lhsID, rhsID, distSq) in outCur: + # Round the distance up to the next 100th of a lightyear. + distLy = math.ceil(math.sqrt(distSq) * 100.0) / 100.0 + # Generate a 64-bit identifier that describes a link + # between two systems that can be consistent regardless + # of which way round you use them (that is, you always + # have to do the min/max the same for each 32 bits) + linkID = (min(lhsID, rhsID) << 32) | (max(lhsID, rhsID)) + links[linkID] = distLy + numLinks += 1 + debug_log(1, "Number of links calculated: {}".format(numLinks)) + +if __name__ == "__main__": + main() diff --git a/import.py b/misc/import-text-to-accdb.py similarity index 94% rename from import.py rename to misc/import-text-to-accdb.py index c3ffc006..1e24c5c1 100644 --- a/import.py +++ b/misc/import-text-to-accdb.py @@ -63,7 +63,7 @@ def addLinks(station, links): if m: dst, dist = m.group(1), m.group(2) try: - dstID = tdb.getStation(dst).ID + dstID = tdb.lookupStation(dst).ID try: tdb.query("INSERT INTO Links (`from`, `to`, `distLy`) VALUES (%d, %d, %s)" % (srcID, dstID, dist)).commit() except pypyodbc.IntegrityError: @@ -88,24 +88,24 @@ def changeStation(line): if stnName == '*': stnName = sysName.upper().join('*') try: - station = tdb.getStation(stnName) + station = tdb.lookupStation(stnName) except LookupError: tdb.query("INSERT INTO Stations (system, station) VALUES (?, ?)", [sysName, stnName]).commit() print("Added %s/%s" % (sysName, stnName)) tdb.load() - station = tdb.getStation(stnName) + station = tdb.lookupStation(stnName) if links: addLinks(station, links) else: # Short format: system/station name. - station = tdb.getStation(line) + station = tdb.lookupStation(line) print("Station: ", station) return station def changeCategory(name): - cat = tdb.list_search('category', name, categories) + cat = tdb.listSearch('category', name, categories) print("Category Select: ", cat) return cat @@ -113,7 +113,7 @@ def changeCategory(name): def parseItem(station, cat, line, uiOrder): fields = line.split() itemName, sellCr, buyCr = fields[0], int(fields[1]), int(fields[2] if len(fields) > 2 else 0) - item = tdb.list_search('item', itemName, categories[cat]) + item = tdb.listSearch('item', itemName, categories[cat]) print("Item: ", item, sellCr, buyCr) stationID, itemID = int(station.ID), int(tdb.itemIDs[item]) diff --git a/saitek/DirectOutput.py b/saitek/DirectOutput.py index e6469bc4..7b58c5f7 100644 --- a/saitek/DirectOutput.py +++ b/saitek/DirectOutput.py @@ -9,12 +9,12 @@ This module consists of two classes - DirectOutput and DirectOutputDevice -DirectOutput directly calls C functions within DirectOutput.dll to allow Python control of the Saitek X62 Pro MFD and LEDs. Implemented as a class to allow sharing of dll object amongst functions +DirectOutput directly calls C functions within DirectOutput.dll to allow Python control of the Saitek X52 Pro MFD and LEDs. Implemented as a class to allow sharing of dll object amongst functions DirectOutputDevice is a wrapper around DirectOutput which automates setup and persists the device handle across functions. This class can be directly called or inherited to control an individual device (eg. X52 Pro) Thanks to Spksh and ellF for the C# version of the wrapper which was very helpful in implementing this. -Thanks to Frazzle for the first Python version which Saitek dutifuly rap^H^H^Hbroke. +Thanks to Frazzle for the first Python version (no-longer compatible with the saitek driver). Example Usage: @@ -306,7 +306,7 @@ def __init__(self, dll_path="C:\\Program Files (x86)\\Saitek\\DirectOutput\\Dire innerSpan = TaggedSpan("Creating DirectOutput instance", debug_level >= 2) self.direct_output = DirectOutput(dll_path) except WindowsError as e: - raise DLLError(e.winerror) + raise DLLError(e.winerror) from None innerSpan = TaggedSpan("Initializing DirectOutput(%s)" % self.application_name, debug_level >= 2) result = self.direct_output.Initialize(self.application_name) @@ -551,7 +551,7 @@ def __init__(self, error_code): if error_code == 126: self.msg = "specified file does not exist" elif error_code == 193: - self.msg = "possible 32/64 bit mismatch between Python interpreter and DLL " + self.msg = "possible 32/64 bit mismatch between Python interpreter and DLL. Make sure you have installed both the 32- and 64-bit driver from Saitek's website" else: self.msg = "unspecified error" diff --git a/trade.py b/trade.py index ed503e6a..762615f0 100644 --- a/trade.py +++ b/trade.py @@ -1,22 +1,40 @@ #!/usr/bin/env python +#--------------------------------------------------------------------- +# Copyright (C) Oliver 'kfsone' Smith 2014 : +# You are free to use, redistribute, or even print and eat a copy of +# this software so long as you include this copyright notice. +# I guarantee there is at least one bug neither of us knew about. +#--------------------------------------------------------------------- # TradeDangerous :: Command Line App :: Main Module -# TradeDangerous Copyright (C) Oliver 'kfsone' Smith 2014 : -# You are free to use, redistribute, or even print and eat a copy of this -# software so long as you include this copyright notice. I guarantee that -# there is at least one bug neither of us knew about. # -# We can easily predict the best run from A->B, but the return trip from B->A might -# not give us the best profit. -# The goal here, then, is to find the best multi-hop route. +# TradeDangerous is a powerful set of tools for traders in Frontier +# Development's game "Elite: Dangerous". It's main function is +# calculating the most profitable trades between either individual +# stations or working out "profit runs". # -# TODO: +# I wrote TD because I realized that the best trade run - in terms +# of the "average profit per stop" was rarely as simple as going +# Chango -> Dahan -> Chango. +# +# E:D's economy is complex; sometimes you can make the most profit +# by trading one item A->B and flying a second item B->A. +# But more often you need to fly multiple stations, especially since +# as you are making money different trade options are coming into +# your affordable range. +# +# END USERS: If you are a user looking to find out how to use TD, +# please consult the file "README.txt". +# +# DEVELOPERS: If you are a programmer who wants TD to do something +# cool, please see the TradeDB and TradeCalc modules. TD is designed +# to empower other programmers to do cool stuff. + ###################################################################### # Imports import argparse # For parsing command line args. import sys # Inevitably. -import time ###################################################################### # The thing I hate most about Python is the global lock. What kind @@ -29,6 +47,8 @@ originName, destName, viaName = "Any", "Any", "Any" origins = [] maxUnits = 0 + +# Multi-function display, optional mfd = None ###################################################################### @@ -37,73 +57,7 @@ from tradedb import TradeDB, AmbiguityError from tradecalc import Route, TradeCalc, localedNo -tdb = TradeDB('.\\TradeDangerous.accdb') - -###################################################################### -# Classes - -# Multi-Function Display wrappers - -class DummyMFD(object): - """ - Base class for the MFD drivers, implemented as no-ops so that - you can always use all MFD functions without conditionals. - """ - hopNo = None - - def __init__(self): - pass - - def finish(self): - """ - Close down the driver. - """ - pass - - def display(self, line1, line2="", line3="", delay=None): - """ - Display data to the MFD. - Arguments: 1-3 lines of text plus optional pause in seconds. - """ - pass - - def attention(self, duration): - """ - Draw the user's attention. - """ - print("\a") - - -class X52ProMFD(DummyMFD): - """ - Wrapper for the Saitek X52 Pro MFD. - """ - def __init__(self): - try: - import saitek.X52Pro - self.doObj = saitek.X52Pro.SaitekX52Pro() - except: - raise Exception('Unable to initialize the X52 Pro module. Make sure your X52 is plugged in and you have the drivers installed.') - - self.page = self.doObj.add_page('TD') - self.display('TradeDangerous', 'INITIALIZING', delay=0.25) - - def finish(self): - self.doObj.finish() - - def display(self, line1, line2="", line3="", delay=None): - self.page[0], self.page[1], self.page[2] = line1, line2, line3 - if delay: time.sleep(delay) - - def attention(self, duration): - page = self.page - iterNo = 0 - cutoff = time.time() + duration - while time.time() <= cutoff: - for ledNo in range(0, 20): - page.set_led(ledNo, (iterNo + ledNo) % 4) - iterNo += 1 - time.sleep(0.02) +tdb = TradeDB(debug=0) ###################################################################### # Functions @@ -119,7 +73,8 @@ def __init__(self, errorStr): def __str__(self): return 'Error in command line: %s' % (self.errorStr) -def parse_avoids(avoidances): + +def parseAvoids(avoidances): global avoidItems, avoidSystems, avoidStations # You can use --avoid to specify an item, system or station. @@ -127,19 +82,19 @@ def parse_avoids(avoidances): # Is it an item? item, system, station = None, None, None try: - item = tdb.list_search('Item', avoid, tdb.items.values()) + item = tdb.listSearch('Item', avoid, tdb.items(), key=lambda item: item[0]) avoidItems.append(item) except LookupError: pass # Is it a system perhaps? try: - system = tdb.getSystem(avoid) + system = tdb.lookupSystem(avoid) avoidSystems.append(system) except LookupError: pass # Or perhaps it is a station try: - station = tdb.getStation(avoid) + station = tdb.lookupStation(avoid) if not (system and station.system is system): avoidStations.append(station) except LookupError as e: @@ -155,10 +110,14 @@ def parse_avoids(avoidances): if args.debug: print("Avoiding items %s, systems %s, stations %s" % (avoidItems, avoidSystems, avoidStations)) -def parse_command_line(): + +def parseCommandLine(): global args, origins, originStation, finalStation, viaStation, maxUnits, originName, destName, viaName, mfd parser = argparse.ArgumentParser(description='Trade run calculator') + parser.add_argument('--credits', metavar='CR', help='Number of credits to start with', type=int, required=True) + parser.add_argument('--ship', metavar='name', help='Set capacity and max-ly-per from ship type', type=str, required=False, default=None) + parser.add_argument('--capacity', metavar='N', help='Maximum capacity of cargo hold.', type=int, required=False) parser.add_argument('--from', dest='origin', metavar='STATION', help='Specifies starting system/station', required=False) parser.add_argument('--to', dest='dest', metavar='STATION', help='Specifies final system/station', required=False) parser.add_argument('--via', dest='via', metavar='STATION', help='Require specified station to be en-route', required=False) @@ -166,18 +125,15 @@ def parse_command_line(): parser.add_argument('--hops', metavar='N', help='Number of hops (station-to-station) to run. DEFAULT: 2', type=int, default=2, required=False) parser.add_argument('--jumps-per', metavar='N', dest='maxJumpsPer', help='Maximum jumps (system-to-system) per hop (station-to-station). DEFAULT: 2', type=int, default=2, required=False) parser.add_argument('--ly-per', metavar='N.NN', dest='maxLyPer', help='Maximum light years per individual jump.', type=float, default=None, required=False) - parser.add_argument('--credits', metavar='CR', help='Number of credits to start with', type=int, required=True) - parser.add_argument('--capacity', metavar='N', help='Maximum capacity of cargo hold.', type=int, required=False) - parser.add_argument('--ship', metavar='name', help='Set capacity and max-ly-per from ship type', type=str, required=False, default=None) parser.add_argument('--limit', metavar='N', help='Maximum units of any one cargo item to buy. DEFAULT: 0 (unlimited)', type=int, default=0, required=False) parser.add_argument('--unique', help='Only visit each station once', default=False, required=False, action='store_true') parser.add_argument('--margin', metavar='N.NN', help='Reduce gains by this much to provide a margin of error for market fluctuations (e.g. 0.25 reduces gains by 1/4). 0<=m<=0.25. DEFAULT: 0.01', default=0.01, type=float, required=False) parser.add_argument('--insurance', metavar='CR', help='Reserve at least this many credits to cover insurance', type=int, default=0, required=False) - parser.add_argument('-v', '--detail', help='Give detailed jump information for multi-jump hops', default=0, required=False, action='count') - parser.add_argument('--debug', help='Enable diagnostic output', default=0, required=False, action='count') parser.add_argument('--routes', metavar='N', help='Maximum number of routes to show. DEFAULT: 1', type=int, default=1, required=False) parser.add_argument('--checklist', help='Provide a checklist flow for the route', action='store_true', required=False, default=False) parser.add_argument('--x52-pro', dest='x52pro', help='Enable experimental X52 Pro MFD output', action='store_true', required=False, default=False) + parser.add_argument('--detail', '-v', help='Give detailed jump information for multi-jump hops', default=0, required=False, action='count') + parser.add_argument('--debug', '-w', help='Enable diagnostic output', default=0, required=False, action='count') args = parser.parse_args() @@ -187,18 +143,18 @@ def parse_command_line(): raise CommandLineError("Too many hops without more optimization") if args.avoid: - parse_avoids(args.avoid) + parseAvoids(args.avoid) if args.origin: originName = args.origin - originStation = tdb.getStation(originName) + originStation = tdb.lookupStation(originName) origins = [ originStation ] else: - origins = [ station for station in tdb.stations.values() ] + origins = [ station for station in tdb.stationByID.values() ] if args.dest: destName = args.dest - finalStation = tdb.getStation(destName) + finalStation = tdb.lookupStation(destName) if args.hops == 1 and originStation and finalStation and originStation == finalStation: raise CommandLineError("More than one hop required to use same from/to destination") @@ -206,7 +162,7 @@ def parse_command_line(): if args.hops < 2: raise CommandLineError("Minimum of 2 hops required for a 'via' route") viaName = args.via - viaStation = tdb.getStation(viaName) + viaStation = tdb.lookupStation(viaName) if args.hops == 2: if viaStation == originStation: raise CommandLineError("3+ hops required to go 'via' the origin station") @@ -223,10 +179,10 @@ def parse_command_line(): # the user has explicitly supplied them. E.g. if the user says # --ship sidewinder --capacity 2, use their capacity limit. if args.ship: - ship = tdb.getShip(args.ship) + ship = tdb.lookupShip(args.ship) args.ship = ship if args.capacity is None: args.capacity = ship.capacity - if args.maxLyPer is None: args.maxLyPer = ship.maxJumpFull + if args.maxLyPer is None: args.maxLyPer = ship.maxLyFull if args.capacity is None: raise CommandLineError("Missing '--capacity' or '--ship' argument") if args.maxLyPer is None: @@ -248,7 +204,7 @@ def parse_command_line(): if args.routes < 1: raise CommandLineError("Maximum routes has to be 1 or higher") - if args.unique and args.hops >= len(tdb.stations): + if args.unique and args.hops >= len(tdb.stationByID): raise CommandLineError("Requested unique trip with more hops than there are stations...") if args.unique: if ((originStation and originStation == finalStation) or @@ -259,28 +215,33 @@ def parse_command_line(): if args.checklist and args.routes > 1: raise CommandLineError("Checklist can only be applied to a single route.") - mfd = DummyMFD() if args.x52pro: + from mfdwrapper import X52ProMFD mfd = X52ProMFD() - mfd.display('TradeDangerous', 'CALCULATING', delay=0.25) + if mfd: + mfd.display('TradeDangerous', 'CALCULATING') return args + ###################################################################### -# Processing functions +# Checklist functions def doStep(stepNo, action, detail=None, extra=None): stepNo += 1 - mfd.display("#%d %s" % (stepNo, action), detail or "", extra or "") + if mfd: + mfd.display("#%d %s" % (stepNo, action), detail or "", extra or "") input(" %3d: %s: " % (stepNo, " ".join([item for item in [action, detail, extra] if item]))) return stepNo + def note(str, addBreak=True): print("(i) %s (i)" % str) if addBreak: print() + def doChecklist(route, credits): stepNo, gainCr = 0, 0 stations, hops, jumps = route.route, route.hops, route.jumps @@ -296,16 +257,17 @@ def doChecklist(route, credits): print(route.summary(), "\n") for idx in range(lastHopIdx): - mfd.hopNo = hopNo = idx + 1 + hopNo = idx + 1 + if mfd: mfd.hopNo = hopNo cur, nxt, hop = stations[idx], stations[idx + 1], hops[idx] # Tell them what they need to buy. if args.detail: note("HOP %d of %d" % (hopNo, lastHopIdx)) - note("Buy at %s" % cur) + note("Buy at %s" % cur.str()) for (item, qty) in sorted(hop[0], key=lambda item: item[1] * item[0].gainCr, reverse=True): - stepNo = doStep(stepNo, 'Buy %d x' % qty, item.item, '@ %scr' % localedNo(item.costCr)) + stepNo = doStep(stepNo, 'Buy %d x' % qty, item.item[0], '@ %scr' % localedNo(item.costCr)) if args.detail: stepNo = doStep(stepNo, 'Refuel') print() @@ -316,12 +278,12 @@ def doChecklist(route, credits): for jump in jumps[idx][1:]: stepNo = doStep(stepNo, 'Jump to', '%s' % (jump.str())) if args.detail: - stepNo = doStep(stepNo, 'Dock at', '%s' % nxt) + stepNo = doStep(stepNo, 'Dock at', '%s' % nxt.name()) print() - note("Sell at %s" % nxt) + note("Sell at %s" % nxt.str()) for (item, qty) in sorted(hop[0], key=lambda item: item[1] * item[0].gainCr, reverse=True): - stepNo = doStep(stepNo, 'Sell %s x' % localedNo(qty), item.item, '@ %scr' % localedNo(item.costCr + item.gainCr)) + stepNo = doStep(stepNo, 'Sell %s x' % localedNo(qty), item.item[0], '@ %scr' % localedNo(item.costCr + item.gainCr)) print() gainCr += hop[1] @@ -333,14 +295,16 @@ def doChecklist(route, credits): print("--------------------------------------") print() - mfd.hopNo = None - mfd.display('FINISHED', "+%scr" % localedNo(gainCr), "=%scr" % localedNo(credits + gainCr)) - mfd.attention(3) - time.sleep(1.5) + if mfd: + mfd.hopNo = None + mfd.display('FINISHED', "+%scr" % localedNo(gainCr), "=%scr" % localedNo(credits + gainCr)) + mfd.attention(3) + time.sleep(1.5) + def main(): global tdb - parse_command_line() + parseCommandLine() startCr = args.credits - args.insurance routes = [ @@ -391,6 +355,7 @@ def main(): doChecklist(routes[0], args.credits) return + if __name__ == "__main__": try: main() diff --git a/tradecalc.py b/tradecalc.py index 6a883b54..7cff2f44 100644 --- a/tradecalc.py +++ b/tradecalc.py @@ -1,9 +1,14 @@ #!/usr/bin/env python +#--------------------------------------------------------------------- +# Copyright (C) Oliver 'kfsone' Smith 2014 : +# You are free to use, redistribute, or even print and eat a copy of +# this software so long as you include this copyright notice. +# I guarantee there is at least one bug neither of us knew about. +#--------------------------------------------------------------------- # TradeDangerous :: Modules :: Profit Calculator -# TradeDangerous Copyright (C) Oliver 'kfsone' Smith 2014 : -# You are free to use, redistribute, or even print and eat a copy of this -# software so long as you include this copyright notice. I guarantee that -# there is at least one bug neither of us knew about. +# +# Provides functions for calculating the most profitable trades and +# trade runs based based on a set of encapsulated criteria. ###################################################################### # Imports @@ -21,6 +26,7 @@ # super-whizzy fast and implemented in C. But in Python 3 it's just # a dict. What a dict move. +from tradedb import System, Station from collections import namedtuple TradeLoad = namedtuple('TradeLoad', [ 'items', 'gainCr', 'costCr', 'units' ]) emptyLoad = TradeLoad([], 0, 0, 0) @@ -31,9 +37,11 @@ # Classes class Route(object): - """ Describes a series of CargoRuns, that is CargoLoads + """ + Describes a series of CargoRuns, that is CargoLoads between several stations. E.g. Chango -> Gateway -> Enterprise - """ + """ + def __init__(self, stations, hops, startCr, gainCr, jumps): self.route = stations self.hops = hops @@ -41,21 +49,33 @@ def __init__(self, stations, hops, startCr, gainCr, jumps): self.gainCr = gainCr self.jumps = jumps + def plus(self, dst, hop, jumps): - rvalue = Route(self.route + [dst], self.hops + [hop], self.startCr, self.gainCr + hop[1], self.jumps + [jumps]) - return rvalue + """ + Returns a new route describing the sum of this route plus a new hop. + """ + return Route(self.route + [dst], self.hops + [hop], self.startCr, self.gainCr + hop[1], self.jumps + [jumps]) + def __lt__(self, rhs): if rhs.gainCr < self.gainCr: # reversed return True return rhs.gainCr == self.gainCr and len(rhs.jumps) < len(self.jumps) + + def __eq__(self, rhs): return self.gainCr == rhs.gainCr and len(self.jumps) == len(rhs.jumps) + def str(self): - return "%s -> %s" % (self.route[0], self.route[-1]) + return "%s -> %s" % (self.route[0].str(), self.route[-1].str()) + def detail(self, detail=0): + """ + Return a string describing this route to a given level of detail. + """ + credits = self.startCr gainCr = 0 route = self.route @@ -67,15 +87,15 @@ def detail(self, detail=0): hop = self.hops[i] hopGainCr, hopTonnes = hop[1], 0 text += " >-> " if i == 0 else " + " - text += "At %s/%s, Buy:" % (route[i].system.str().upper(), route[i].station) + text += "At %s/%s, Buy:" % (route[i].system.name(), route[i].name()) for (item, qty) in sorted(hop[0], key=lambda item: item[1] * item[0].gainCr, reverse=True): if detail > 1: - text += "\n | %4d x %-30s" % (qty, item.item) + text += "\n | %4d x %-30s" % (qty, item.item[0]) text += " @ %10scr each, %10scr total" % (localedNo(item.costCr), localedNo(item.costCr * qty)) elif detail: - text += " %d x %s (@%dcr)" % (qty, item.item, item.costCr) + text += " %d x %s (@%dcr)" % (qty, item.item[0], item.costCr) else: - text += " %d x %s" % (qty, item.item) + text += " %d x %s" % (qty, item.item[0]) text += "," hopTonnes += qty text += "\n" @@ -89,12 +109,16 @@ def detail(self, detail=0): text += "\n" gainCr += hopGainCr - text += " <-< %s gaining %scr => %scr total" % (route[-1], localedNo(gainCr), localedNo(credits + gainCr)) + text += " <-< %s gaining %scr => %scr total" % (route[-1].name(), localedNo(gainCr), localedNo(credits + gainCr)) text += "\n" return text def summary(self): + """ + Returns a string giving a short summary of this route. + """ + credits, hops, jumps = self.startCr, self.hops, self.jumps ttlGainCr = sum([hop[1] for hop in hops]) return "\n".join([ @@ -108,27 +132,31 @@ def summary(self): class TradeCalc(object): - """ Container for accessing trade calculations with common properties """ - def __init__(self, tdb, debug=False, capacity=None, maxUnits=None, margin=0.01, unique=False, fit=None): + """ + Container for accessing trade calculations with common properties. + """ + + def __init__(self, tdb, debug=0, capacity=None, maxUnits=None, margin=0.01, unique=False, fit=None): self.tdb = tdb self.debug = debug self.capacity = capacity or 4 self.margin = float(margin) self.unique = unique self.maxUnits = maxUnits or 0 - self.defaultFit = fit or self.fast_fit + self.defaultFit = fit or self.fastFit + - def brute_force_fit(self, items, credits, capacity, maxUnits): + def bruteForceFit(self, items, credits, capacity, maxUnits): """ Brute-force generation of all possible combinations of items. This is provided to make it easy to validate the results of future variants or optimizations of the fit algorithm. """ - def _fit_combos(offset, cr, cap): + def _fitCombos(offset, cr, cap): if offset >= len(items): return emptyLoad # yield items below us too - bestLoad = _fit_combos(offset + 1, cr, cap) + bestLoad = _fitCombos(offset + 1, cr, cap) item = items[offset] itemCost = item.costCr maxQty = min(maxUnits, cap, cr // itemCost) @@ -136,7 +164,7 @@ def _fit_combos(offset, cr, cap): itemGain = item.gainCr for qty in range(maxQty): load = TradeLoad([[item, maxQty]], itemGain * maxQty, itemCost * maxQty, maxQty) - subLoad = _fit_combos(offset + 1, cr - load.costCr, cap - load.units) + subLoad = _fitCombos(offset + 1, cr - load.costCr, cap - load.units) combGain = load.gainCr + subLoad.gainCr if combGain < bestLoad.gainCr: continue @@ -150,16 +178,17 @@ def _fit_combos(offset, cr, cap): bestLoad = TradeLoad(load.items + subLoad.items, load.gainCr + subLoad.gainCr, load.costCr + subLoad.costCr, load.units + subLoad.units) return bestLoad - bestLoad = _fit_combos(0, credits, capacity) + bestLoad = _fitCombos(0, credits, capacity) return bestLoad - def fast_fit(self, items, credits, capacity, maxUnits): + + def fastFit(self, items, credits, capacity, maxUnits): """ Best load calculator using a recursive knapsack-like algorithm to find multiple loads and return the best. """ - def _fit_combos(offset, cr, cap): + def _fitCombos(offset, cr, cap): """ Starting from offset, consider a scenario where we would purchase the maximum number of each item @@ -187,7 +216,7 @@ def _fit_combos(offset, cr, cap): if crLeft > 0 and capLeft > 0: # Solve for the remaining credits and capacity with what # is left in items after the item we just checked. - for subLoad in _fit_combos(offset + 1, crLeft, capLeft): + for subLoad in _fitCombos(offset + 1, crLeft, capLeft): if subLoad.gainCr >= bestGainCr: yield TradeLoad(subLoad.items + loadItems, subLoad.gainCr + loadGainCr, subLoad.costCr + loadCostCr, subLoad.units + maxQty) bestGainCr = subLoad.gainCr @@ -197,12 +226,13 @@ def _fit_combos(offset, cr, cap): offset += 1 bestLoad = emptyLoad - for result in _fit_combos(0, credits, capacity): + for result in _fitCombos(0, credits, capacity): if not bestLoad or (result.gainCr > bestLoad.gainCr or (result.gainCr == bestLoad.gainCr and (result.units < bestLoad.units or (result.units == bestLoad.units and result.costCr < bestLoad.costCr)))): bestLoad = result return bestLoad + def getBestTrade(self, src, dst, credits, capacity=None, avoidItems=None, focusItems=None, fitFunction=None): """ Find the most profitable trade between stations src and dst. @@ -212,10 +242,10 @@ def getBestTrade(self, src, dst, credits, capacity=None, avoidItems=None, focusI """ if not avoidItems: avoidItems = [] if not focusItems: focusItems = [] - if self.debug: print("# %s -> %s with %dcr" % (src, dst, credits)) + if self.debug: print("# %s -> %s with %dcr" % (src.name(), dst.name(), credits)) - if not dst in src.stations: - raise ValueError("%s does not have a link to %s" % (src, dst)) + if not dst in src.tradingWith: + raise ValueError("%s does not have a link to %s" % (src.name(), dst.name())) capacity = capacity or self.capacity if not capacity: @@ -223,7 +253,7 @@ def getBestTrade(self, src, dst, credits, capacity=None, avoidItems=None, focusI maxUnits = self.maxUnits or capacity - items = src.trades[dst.ID] + items = src.tradingWith[dst] if avoidItems: items = [ item for item in items if not item.item in avoidItems ] if focusItems: @@ -253,25 +283,40 @@ def getBestTrade(self, src, dst, credits, capacity=None, avoidItems=None, focusI return fitFunction(items, credits, capacity, maxUnits) - def getBestHopFrom(self, src, credits, capacity=None, maxJumps=None, maxLy=None, maxLyPer=None): - """ Determine the best trade run from a given station. """ - if isinstance(src, str): - src = self.tdb.getStation(src) - hop = None - for (destSys, destStn, jumps, ly, via) in src.getDestinations(maxJumps=maxJumps, maxLy=maxLy, maxLyPer=maxLyPer): - load = self.getBestTrade(src, destStn, credits, capacity=capacity) - if load and (not hop or (load.gainCr > hop.gainCr or (load.gainCr == hop.gainCr and len(jumps) < hop.jumps))): - hop = TradeHop(destSys=destSys, destStn=destStn, load=load.items, gainCr=load.gainCr, jumps=jumps, ly=ly) - return hop + def getBestHopFrom(self, src, credits, capacity=None, maxJumps=None, maxLyPer=None): + """ + Determine the best trade run from a given station. + """ + src = self.tdb.lookupStation(src) + bestHop = None + for dest in src.getDestinations(maxJumps=maxJumps, maxLyPer=maxLyPer): + load = self.getBestTrade(src, dest.station, credits, capacity=capacity) + if not load: + continue + if bestHop: + if load.gainCr > bestHop.gainCr: continue + if load.gainCr == bestHop.gainCr: + if dest.jumps > bestHop.jumps: continue + if dest.jumps == bestHop.jumps: + if dest.ly >= bestHop.ly: + continue + bestHop = TradeHop(destSys=dest.system, destStn=dest.station, load=load.items, gainCr=load.gainCr, jumps=dest.jumps, ly=dest.ly) + return besthop + def getBestHops(self, routes, credits, restrictTo=None, avoidItems=None, avoidPlaces=None, maxJumps=None, - maxLy=None, maxJumpsPer=None, maxLyPer=None): - """ Given a list of routes, try all available next hops from each - route. Store the results by destination so that we pick the - best route-to-point for each destination at each step. If we - have two routes: A->B->D, A->C->D and A->B->D produces more - profit, then there is no point in continuing the A->C->D path. """ + maxJumpsPer=None, maxLyPer=None): + """ + Given a list of routes, try all available next hops from each + route. + + Store the results by destination so that we pick the + best route-to-point for each destination at each step. + + If we have two routes: A->B->D, A->C->D and A->B->D produces + more profit, there's no point continuing the A->C->D path. + """ if not avoidItems: avoidItems = [] if not avoidPlaces: avoidPlaces = [] @@ -291,24 +336,27 @@ def getBestHops(self, routes, credits, if jumpLimit <= 0: continue - for (destSys, destStn, jumps, ly) in src.getDestinations(maxJumps=jumpLimit, maxLy=maxLy, maxLyPer=maxLyPer, avoiding=avoidPlaces): + for dest in src.getDestinations(maxJumps=jumpLimit, maxLyPer=maxLyPer, avoiding=avoidPlaces): if self.debug: - print("#destSys = %s, destStn = %s, jumps = %s, ly = %s" % (destSys.str(), destStn, "->".join([jump.str() for jump in jumps]), ly)) - if not destStn in src.stations: - if self.debug: print("#%s is not in my station list" % destStn) + print("#destSys = %s, destStn = %s, jumps = %s, distLy = %s" % (dest.system.name(), dest.station.name(), "->".join([jump.str() for jump in dest.via]), dest.distLy)) + if not dest.station in src.tradingWith: + if self.debug > 2: print("#%s is not in my station list" % dest.station.name()) continue - if restrictTo and destStn != restrictTo: - if self.debug: print("#%s doesn't match restrict %s" % (destStn, restrictTo)) - continue - if unique and destStn in route.route: - if self.debug: print("#%s is already in the list, not unique" % destStn) + if restrictTo: + if (isinstance(restrictTo, System) and dest.system != restrictTo) \ + or (isinstance(restrictTo, Station) and dest.station != restrictTo): + if self.debug > 2: print("#%s doesn't match restrict %s" % (dest.station.name(), restrictTo)) + continue + if unique and dest.station in route.route: + if self.debug > 2: print("#%s is already in the list, not unique" % dest.station.name()) continue - trade = self.getBestTrade(src, destStn, startCr, avoidItems=avoidItems) + + trade = self.getBestTrade(src, dest.station, startCr, avoidItems=avoidItems) if not trade: - if self.debug: print("#* No trade") + if self.debug > 2: print("#* No trade") continue - dstID = destStn.ID + dstID = dest.station.ID try: # See if there is already a candidate for this destination (bestStn, bestRoute, bestTrade, bestJumps, bestLy) = bestToDest[dstID] @@ -317,12 +365,12 @@ def getBestHops(self, routes, credits, newRouteGainCr = route.gainCr + trade[1] if bestRouteGainCr > newRouteGainCr: continue - if bestRouteGainCr == newRouteGainCr and bestLy <= ly: + if bestRouteGainCr == newRouteGainCr and bestLy <= dest.distLy: continue except KeyError: # No existing candidate, we win by default pass - bestToDest[dstID] = [ destStn, route, trade, jumps, ly ] + bestToDest[dstID] = [ dest.station, route, trade, dest.via, dest.distLy ] result = [] for (dst, route, trade, jumps, ly) in bestToDest.values(): @@ -330,12 +378,9 @@ def getBestHops(self, routes, credits, return result -# A function called 'localedNo'. If you needed this comment, we're -# in a metric heck ton of trouble, and the calculator has yet to find -# a route for selling any number of tons of trouble across any number -# of light years. Maybe we should use imperial heck tons? -# Look. It's the only function in this file, and I'll be damned if -# I can let that fly without a comment. So this is what you get. -def localedNo(num): - """ Returns a locale-formatted version of a number, e.g. 1,234,456. """ + +def localedNo(num): # as in "transformed into the current locale" + """ + Returns a locale-formatted version of a number, e.g. 1,234,456. + """ return locale.format('%d', num, grouping=True) diff --git a/tradedb.py b/tradedb.py index b1ce1748..b11ae7ee 100644 --- a/tradedb.py +++ b/tradedb.py @@ -1,27 +1,33 @@ #!/usr/bin/env python +#--------------------------------------------------------------------- +# Copyright (C) Oliver 'kfsone' Smith 2014 : +# You are free to use, redistribute, or even print and eat a copy of +# this software so long as you include this copyright notice. +# I guarantee there is at least one bug neither of us knew about. +#--------------------------------------------------------------------- # TradeDangerous :: Modules :: Database Module -# TradeDangerous Copyright (C) Oliver 'kfsone' Smith 2014 : -# You are free to use, redistribute, or even print and eat a copy of this -# software so long as you include this copyright notice. I guarantee that -# there is at least one bug neither of us knew about. # -# Provides classes that load and describe the TradeDangerous data set. -# Currently depends on pypyodbc and Microsoft Access 2010+ drivers, -# but I'll switch to a more portable format soon. +# Containers for describing the current TradeDangerous database +# and loading it from the SQLite database cache. ###################################################################### # Imports import re # Because irregular expressions are dull import pypyodbc # Because its documentation was better +import sys from queue import Queue # Because we're British. from collections import namedtuple +import itertools +import math +from pathlib import Path ###################################################################### # Classes class AmbiguityError(Exception): - """ Raised when a search key could match multiple entities. + """ + Raised when a search key could match multiple entities. Attributes: searchKey - the key given to the search routine, first - the first potential match @@ -29,235 +35,403 @@ class AmbiguityError(Exception): """ def __init__(self, lookupType, searchKey, first, second): self.lookupType, self.searchKey, self.first, self.second = lookupType, searchKey, first, second + + def __str__(self): return '%s lookup: "%s" could match either "%s" or "%s"' % (self.lookupType, str(self.searchKey), str(self.first), str(self.second)) + class Trade(object): - """ Describes what it would cost and how much you would gain - when selling an item between two specific stations. """ + """ + Describes what it would cost and how much you would gain + when selling an item between two specific stations. + """ + # TODO: Replace with a class within Station that describes asking and paying. def __init__(self, item, itemID, costCr, gainCr): self.item = item self.itemID = itemID self.costCr = costCr self.gainCr = gainCr + def describe(self): print(self.item, self.itemID, self.costCr, self.gainCr) + def __repr__(self): return "%s (%dcr)" % (self.item, self.costCr) class System(object): - """ Describes a star system, which may contain one or more Station objects, - and lists which stars it has a direct connection to. """ + """ + Describes a star system, which may contain one or more Station objects, + and lists which stars it has a direct connection to. + """ + # TODO: Build the links from an SQL query, it'll save a lot of + # expensive python dictionary lookups. - def __init__(self, system): - self.system = system + def __init__(self, ID, name, posX, posY, posZ): + self.ID, self.dbname, self.posX, self.posY, self.posZ = ID, name, posX, posY, posZ self.links = {} self.stations = [] - def addLink(self, dest, dist): - self.links[dest] = dist + + @staticmethod + def linkSystems(lhs, rhs, distSq): + lhs.links[rhs] = rhs.links[lhs] = math.sqrt(distSq) + def links(self): return list(self.links.keys()) + def addStation(self, station): if not station in self.stations: self.stations.append(station) + + def name(self): + return self.dbname.upper() + + def str(self): - return self.system + return self.dbname + + + def __repr__(self): + return "".format(self.ID, self.dbname, self.posX, self.posY, self.posZ) class Station(object): - """ Describes a station within a given system along with what trade - opportunities it presents. """ + """ + Describes a station within a given system along with what trade + opportunities it presents. + """ - def __init__(self, ID, system, station): - self.ID, self.system, self.station = ID, system, station - self.trades = {} - self.stations = [] + def __init__(self, ID, system, name, lsFromStar=0.0): + self.ID, self.system, self.dbname, self.lsFromStar = ID, system, name, lsFromStar + self.tradingWith = {} # dict[tradingPartnerStation] -> [ available trades ] system.addStation(self) + + def name(self): + return self.dbname + + def addTrade(self, dest, item, itemID, costCr, gainCr): - """ Add a Trade entry from this to a destination station. """ - dstID = dest.ID - if not dstID in self.trades: - self.trades[dstID] = [] - self.stations.append(dest) + """ + Add an entry reflecting that an item can be bought at this + station and sold for a gain at another. + """ + # TODO: Something smarter. + if not dest in self.tradingWith: + self.tradingWith[dest] = [] trade = Trade(item, itemID, costCr, gainCr) - self.trades[dstID].append(trade) + self.tradingWith[dest].append(trade) - def organizeTrades(self): - """ Process the trades-to-destination lists: sort the list into by-gain order. """ - for tradeList in self.trades.values(): - # sort the list in descending gain order - so the mostprofitable item is listed first. - tradeList.sort(key=lambda trade: trade.gainCr, reverse=True) - def getDestinations(self, maxJumps=None, maxLy=None, maxLyPer=None, avoiding=None): - """ Gets a list of the Station destinations that can be reached + def getDestinations(self, maxJumps=None, maxLyPer=None, avoiding=None): + """ + Gets a list of the Station destinations that can be reached from this Station within the specified constraints. - If no constraints are specified, you get a list of everywhere - that this station has a link to in the db where something - the station sells is bought. - """ + """ + + avoiding = avoiding or [] + maxJumps = maxJumps or sys.maxsize + maxLyPer = maxLyPer or float("inf") + + # The open list is the list of nodes we should consider next for + # potential destinations. + # The path list is a list of the destinations we've found and the + # shortest path to them. It doubles as the "closed list". + # The closed list is the list of nodes we've already been to (so + # that we don't create loops A->B->C->A->B->C->...) + + Node = namedtuple('Node', [ 'system', 'via', 'distLy' ]) + + openList = [ Node(self.system, [], 0) ] + pathList = { system.ID: Node(system, None, 0.0) + # include avoids so we only have + # to consult one place for exclusions + for system in avoiding + [ self ] + # the avoid list may contain stations, + # which affects destinations but not vias + if isinstance(system, System) } + + # As long as the open list is not empty, keep iterating. + jumps = 0 + while openList and jumps < maxJumps: + # Expand the search domain by one jump; grab the list of + # nodes that are this many hops out and then clear the list. + ring, openList = openList, [] + # All of the destinations we are about to consider will + # either be on the closed list or they will be +1 jump away. + jumps += 1 + + for node in ring: + for (destSys, destDist) in node.system.links.items(): + if destDist > maxLyPer: continue + dist = node.distLy + destDist + try: + prevNode = pathList[destSys.ID] + # If we already have a shorter path, do nothing + if dist >= prevNode.distLy: continue + except KeyError: pass + # Add to the path list + pathList[destSys.ID] = Node(destSys, node.via, dist) + # Add to the open list but also include node to the via + # list so that it serves as the via list for all next-hops. + openList += [ Node(destSys, node.via + [destSys], dist) ] + + Destination = namedtuple('Destination', [ 'system', 'station', 'via', 'distLy' ]) + + destStations = [] + # always include the local stations, unless the user has indicated they are + # avoiding this system. E.g. if you're in Chango but you've specified you + # want to avoid Chango... + if not self.system in avoiding: + for station in self.system.stations: + if station in self.tradingWith and not station in avoiding: + destStations += [ Destination(self, station, [], 0.0) ] + + avoidStations = [ station for station in avoiding if isinstance(station, Station) ] + epsilon = sys.float_info.epsilon + for node in pathList.values(): + if node.distLy > epsilon: # Values indistinguishable from zero are avoidances + for station in node.system.stations: + destStations += [ Destination(node.system, station, [self.system] + node.via + [station.system], node.distLy) ] - if not avoiding: avoiding = [] - - openList, closedList, destStations = Queue(), [sys for sys in avoiding if isinstance(sys, System)] + [self], [] - openList.put([self.system, [], 0]) - # Sys is always available, so we don't need to import it. maxint was deprecated in favour of maxsize. - # noinspection PyUnresolvedReferences - maxJumpDist = float(maxLyPer or sys.maxsize) - while not openList.empty(): - (sys, jumps, dist) = openList.get() - if maxJumps and len(jumps) > maxJumps: - continue - if maxLy and dist > maxLy: - continue - jumps = list(jumps + [sys]) - for stn in sys.stations: - if not stn in avoiding: - destStations.append([sys, stn, jumps, dist]) - if (maxJumps and len(jumps) > maxJumps): - continue - for (destSys, destDist) in sys.links.items(): - if destDist > maxJumpDist: - continue - if maxLy and dist + destDist > maxLy: - continue - if destSys in closedList: - continue - openList.put([destSys, jumps, dist + destDist]) - closedList.append(destSys) return destStations + def name(self): + return self.dbname + + def str(self): - return '%s %s' % (self.system.str().upper(), self.station) + return '%s %s' % (self.system.name(), self.dbname) def __repr__(self): - return '%s %s' % (self.system.str().upper(), self.station) + return ''.format(self.ID, self.system.name(), self.dbname, self.lsFromStar) + + +class Ship(namedtuple('Ship', [ 'ID', 'dbname', 'capacity', 'mass', 'driveRating', 'maxLyEmpty', 'maxLyFull', 'maxSpeed', 'boostSpeed', 'stations' ])): + def name(self): + return self.dbname -class Ship(namedtuple('Ship', [ 'name', 'capacity', 'maxJump', 'maxJumpFull', 'stations' ])): - pass class TradeDB(object): + """ + Encapsulation for the database layer. + + Attributes: + debug - Debugging level for this instance. + dbmodule - Reference to the database layer we're using (e.g. sqlite3 or pypyodbc). + path - The URI to the database. + conn - The database connection. + + Methods: + load - Reloads entire database. CAUTION: Destructive - Orphans existing records you reference. + loadTrades - Reloads just the price data. CAUTION: Destructive - Orphans existing records. + lookupSystem - Return a system matching "name" with ambiguity detection. + lookupStation - Return a station matching "name" with ambiguity detection. + lookupShip - Return a ship matching "name" with ambiguity detection. + getTrade - Look for a Trade object where item is sold from one stationi and bought at another. + + query - Executes the specified SQL on the db and returns a cursor. + fetch_all - Generator that yields all the rows retrieved by an sql cursor. + + Static methods: + distanceSq - Returns the square of the distance between two points. + listSearch - Performs a partial-match search of a list for a value. + normalizedStr - Normalizes a search index string. + """ + normalizeRe = re.compile(r'[ \t\'\"\.\-_]') - ships = [ - Ship('Sidewinder', 4, 8.13, 7.25, [ 'Aulin Enterprise', 'Beagle2', 'Abnett Platform', 'Moxon\'s Mojo' ]), - Ship('Eagle', 6, 6.59, 6.00, [ 'Aulin Enterprise', 'Beagle2', 'Abnett Platform' ]), - Ship('Hauler', 16, 8.74, 6.10, [ 'Aulin Enterprise', 'Beagle2', 'Moxon\'s Mojo' ]), - Ship('Viper', 8, 13.49, 9.16, [ 'Aulin Enterprise', 'Beagle2', 'Chango Dock' ]), - Ship('Cobra', 36, 9.94, 7.30, [ 'Aulin Enterprise', 'Chango Dock' ]), - Ship('Lakon Type 6', 100, 29.36, 15.64, [ 'Aulin Enterprise', 'Chango Dock', 'Vonarburg Co-op', 'Moxon\'s Mojo' ]), - Ship('Lakon Type 9', 440, 18.22, 13.34, [ 'Chango Dock', 'Moxon\'s Mojo' ]), - Ship('Anaconda', 228, 19.70, 17.60, [ 'Louis De Lacaille Prospect' ]), - ] - - def __init__(self, path='.\\TradeDangerous.accdb', debug=0): - self.path = "Driver={Microsoft Access Driver (*.mdb, *.accdb)};DBQ=" + path + # The DB cache + defaultDB = './data/TradeDangerous.db' + # File containing SQL to build the DB cache from + defaultSQL = './data/TradeDangerous.sql' + # File containing text description of prices + defaultPrices = './data/TradeDangerous.prices' + + + def __init__(self, path=None, sqlFilename=None, pricesFilename=None, debug=0): + self.dbPath = Path(path or TradeDB.defaultDB) + self.dbURI = str(self.dbPath) + self.sqlPath = Path(sqlFilename or TradeDB.defaultSQL) + self.pricesPath = Path(pricesFilename or TradeDB.defaultPrices) self.debug = debug + self.conn, self.dbmodule = None, None + + # Ensure the file exists, otherwise sqlite will create a new database. + self._connectToDB() + + self.load() + + + #### + # Access to the underlying database. + + def _connectToDB(self): + self.reloadCache() + + # Make sure we don't hold on to an existing connection. + if self.conn: + self.conn.close() + self.conn = None + try: - self.conn = pypyodbc.connect(self.path) - except pypyodbc.Error as e: - print("Do you have the requisite Access driver installed? See http://www.microsoft.com/en-us/download/details.aspx?id=13255") + import sqlite3 + self.dbmodule = sqlite3 + self.conn = self.dbmodule.connect(self.dbURI) + except ImportError as e: + print("ERROR: You don't appear to have the Python sqlite3 module installed. Impressive. No, wait, the other one: crazy.") raise e - self.load() - def load(self): - """ Populate/re-populate this instance with data from the TradeDB layer. """ - # Create a cursor. + + def query(self, *args): + """ Perform an SQL query on the DB and return the cursor. """ + if not self.conn: self._connectToDB() cur = self.conn.cursor() + cur.execute(*args) + return cur - # Fetch a list of systems. - cur.execute('SELECT system FROM Stations GROUP BY system') - systems = self.systems = { row[0]: System(row[0]) for row in cur } - - # Fetch a list of links between systems. - # TODO: Store positions, calculate distances on demand - cur.execute("""SELECT frmSys.system, toSys.system, Links.distLy - FROM Stations AS frmSys, Links, Stations as toSys - WHERE frmSys.ID = Links.from AND toSys.ID = Links.to""") - for (srcSysID, dstSysID, distLy) in cur: - srcSys, dstSys = systems[srcSysID], systems[dstSysID] - srcSys.addLink(dstSys, float(distLy)) - - # Fetch the list of stations - cur.execute('SELECT id, system, station FROM Stations') - # Station lookup by ID - self.stations = { row[0]: Station(row[0], self.systems[row[1]], row[2]) for row in cur } - # StationID lookup by System Name - self.systemIDs = { value.system.str().upper(): key for (key, value) in self.stations.items() } - # StationID lookup by Station Name - self.stationIDs = { value.station.upper(): key for (key, value) in self.stations.items() } - - # Populate 'items' from the database - cur.execute('SELECT id, item FROM Items') - self.items = { row[0]: row[1] for row in cur } - self.itemIDs = { name: itemID for (itemID, name) in self.items.items() } - - stations, items = self.stations, self.items - - # Populate the station list with the profitable trades between stations - # Ignore items that have a ui_order of 0 in the prices table (my way of marking an item as defunct or illegal) - cur.execute('SELECT src.station_id, dst.station_id, src.item_id, src.buy_cr, dst.sell_cr' - ' FROM Prices AS src INNER JOIN Prices AS dst ON src.item_id = dst.item_id' - ' WHERE src.buy_cr > 0 AND dst.sell_cr > src.buy_cr' - ' AND src.ui_order > 0 AND dst.ui_order > 0' - ) - for (srcID, dstID, itemID, srcCostCr, dstValueCr) in cur: - srcStn = stations[srcID] - dstStn = stations[dstID] - item = items[itemID] - srcStn.addTrade(dstStn, item, itemID, srcCostCr, dstValueCr - srcCostCr) - - # Post-process the trades and sort them into whatever order we want them in. - for station in stations.values(): - station.organizeTrades() - # In debug mode, check that everything looks sane. - if self.debug: - self._validate() + # following the convention of how fetch_all is written in python modules. + def fetch_all(self, *args): + """ Perform an SQL query on the DB and iterate across the rows. """ + for row in self.query(*args): + yield row - def _validate(self): - # Check that things correctly reference themselves. - for (stnID, stn) in self.stations.items(): - if self.stations[stn.ID] != stn: - raise ValueError("Station not pointing to self correctly" % stn.station) - for (stnName, stnID) in self.stationIDs.items(): - if self.stations[stnID].station.upper() != stnName: - raise ValueError("Station name not pointing to self correctly" % stnName) - for (itemID, item) in self.items.items(): - if self.itemIDs[item] != itemID: - raise ValueError("Item %s not pointing to itself correctly" % item, itemID, item, self.itemIDs[item]) - # Check that system links are bi-directional - for (name, sys) in self.systems.items(): - if not sys.links: - raise ValueError("System %s has no links" % name) - if sys in sys.links: - raise ValueError("System %s has a link to itself!" % name) - if name in sys.links: - raise ValueError("System %s's name occurs in sys.links" % name) - for link in sys.links: - if not sys in link.links: - raise ValueError("System %s does not have a reciprocal link in %s's links" % (name, link.str())) - def getSystem(self, name): - """ Look up a System object by it's name. """ - if isinstance(name, System): + def reloadCache(self): + """ + Checks if the .sql or .prices file is newer than the cache. + """ + + if self.dbPath.exists(): + # We're looking to see if the .sql file or .prices file + # was modified or created more recently than the last time + # we *created* the db file. + dbFileCreatedTimestamp = self.dbPath.stat().st_ctime + + sqlStat, pricesStat = self.sqlPath.stat(), self.pricesPath.stat() + sqlFileTimestamp = max(sqlStat.st_mtime, sqlStat.st_ctime) + pricesFileTimestamp = max(pricesStat.st_mtime, pricesStat.st_ctime) + + if dbFileCreatedTimestamp > max(sqlFileTimestamp, pricesFileTimestamp): + # db is newer. + if self.debug > 1: + print("reloadCache: db file is newer. db:{} > max(sql:{}, prices:{}".format(dbFileCreatedTimestamp, sqlFileTimestamp, pricesFileTimestamp)) + return + + if self.debug: + print("* Rebuilding DB Cache") + else: + if self.debug: + print("* Building DB cache") + + import buildcache + buildcache.buildCache(dbPath=self.dbPath, sqlPath=self.sqlPath, pricesPath=self.pricesPath) + + + #### + # Star system data. + + def systems(self): + """ Iterate through the list of systems. """ + yield from self.systemByID.values() + + + def _loadSystems(self): + """ + Initial load the (raw) list of systems. + If you have previously loaded Systems, this will orphan the old System objects. + """ + stmt = """ + SELECT system_id, name, pos_x, pos_y, pos_z + FROM System + """ + self.cur.execute(stmt) + systemByID, systemByName = {}, {} + for (ID, name, posX, posY, posZ) in self.cur: + systemByID[ID] = systemByName[name] = System(ID, name, posX, posY, posZ) + + self.systemByID, self.systemByName = systemByID, systemByName + if self.debug > 1: print("# Loaded %d Systems" % len(systemByID)) + + + def buildLinks(self, longestJumpLy): + """ + Populate the list of reachable systems for every star system. + + Not every system can reach every other, and we use the longest jump + that can be made by a ship to limit how many connections we consider + to be "links". + """ + + longestJumpSq = longestJumpLy ** 2 # So we don't have to sqrt every distance + + # Generate a series of symmetric pairs (A->B, A->C, A->D, B->C, B->D, C->D) + # so we only calculate each distance once, and then add a link each way. + # (A->B distance populates A->B and B->A, etc) + numLinks = 0 + for (lhs, rhs) in itertools.combinations(self.systemByID.values(), 2): + dX, dY, dZ = rhs.posX - lhs.posX, rhs.posY - lhs.posY, rhs.posZ - lhs.posZ + distSq = (dX * dX) + (dY * dY) + (dZ * dZ) + if distSq <= longestJumpSq: + System.linkSystems(lhs, rhs, distSq) + numLinks += 1 + + if self.debug > 2: print("# Number of links between systems: %d" % numLinks) + + + def lookupSystem(self, key): + """ + Look up a System object by it's name. + """ + if isinstance(key, System): return name - if isinstance(name, Station): + if isinstance(key, Station): return name.system - system = self.list_search("System", name, self.systems.keys()) - return self.systems[system] + return TradeDB.listSearch("System", key, self.systems(), key=lambda system: system.dbname) + + + #### + # Station data. + + def stations(self): + """ Iterate through the list of stations. """ + yield from self.stationByID.values() + - def getStation(self, name): - """ Look up a Station object by it's name or system. """ + def _loadStations(self): + """ + Populate the Station list. + Station constructor automatically adds itself to the System object. + If you have previously loaded Stations, this will orphan the old objects. + """ + stmt = """ + SELECT station_id, system_id, name, ls_from_star + FROM Station + """ + self.cur.execute(stmt) + stationByID, stationByName = {}, {} + systemByID = self.systemByID + for (ID, systemID, name, lsFromStar) in self.cur: + stationByID[ID] = stationByName[name] = Station(ID, systemByID[systemID], name, lsFromStar) + + self.stationByID, self.stationByName = stationByID, stationByName + if self.debug > 1: print("# Loaded %d Stations" % len(stationByID)) + + + def lookupStation(self, name): + """ + Look up a Station object by it's name or system. + """ if isinstance(name, Station): return name if isinstance(name, System): @@ -268,73 +442,238 @@ def getStation(self, name): stationID, station, systemID, system = None, None, None, None try: - systemID = self.list_search("System", name, self.systems.keys()) - system = self.systems[systemID] + system = TradeDB.listSearch("System", name, self.systemByID.values(), key=lambda system: system.dbname) except LookupError: pass try: - stationName = self.list_search("Station", name, self.stationIDs.keys()) - stationID = self.stationIDs[stationName] - station = self.stations[stationID] + station = TradeDB.listSearch("Station", name, self.stationByID.values(), key=lambda station: station.dbname) except LookupError: pass # If neither matched, we have a lookup error. - if not (stationID or systemID): + if not (station or system): raise LookupError("'%s' did not match any station or system." % (name)) # If we matched both a station and a system, make sure they resovle to the # the same station otherwise we have an ambiguity. Some stations have the # same name as their star system (Aulin/Aulin Enterprise) - if systemID and stationID and system != station.system: - raise AmbiguityError('Station', name, system.str(), station.str()) + if system and station and system != station.system: + raise AmbiguityError('Station', name, system.name(), station.name()) - if stationID: - return self.stations[stationID] + if station: + return station # If we only matched a system name, ensure that it's a single station system # otherwise they need to specify a station name. - system = self.systems[systemID] if len(system.stations) != 1: raise ValueError("System '%s' has %d stations, please specify a station instead." % (name, len(system.stations))) return system.stations[0] - def getShip(self, name): - """ Look up a ship by name """ - return self.list_search("Ship", name, self.ships, key=lambda item: item.name) + + #### + # Ship data. + + def ships(self): + """ Iterate through the list of ships. """ + yield from self.shipByID.values() + + + def _loadShips(self): + """ + Populate the Ship list. + If you have previously loaded Ships, this will orphan the old objects. + """ + stmt = """ + SELECT ship_id, name, capacity, mass, drive_rating, max_ly_empty, max_ly_full, max_speed, boost_speed + FROM Ship + """ + self.cur.execute(stmt) + self.shipByID = { row[0]: Ship(*row, stations=[]) for row in self.cur } + + if self.debug > 1: print("# Loaded %d Ships" % len(self.shipByID)) + + + def lookupShip(self, name): + """ + Look up a ship by name + """ + return TradeDB.listSearch("Ship", name, self.shipByID.values(), key=lambda ship: ship.dbname) + + + #### + # Item data. + + def items(self): + """ Iterate through the list of items. """ + yield from self.itemByID.values() + + + # TODO: Provide CATEGORIES so that you can do an item lookup. + def _loadItems(self): + """ + Populate the Item list. + If you have previously loaded Items, this will orphan the old objects. + """ + stmt = """ + SELECT item_id, name + FROM Item + """ + self.cur.execute(stmt) + itemByID = {} + for (ID, name) in self.cur: + itemByID[ID] = name, ID + + self.itemByID = itemByID + if self.debug > 1: print("# Loaded %d Items" % len(itemByID)) + + #### NOTE: We don't provide "lookupItem" because you need to do that + #### based on category (some minerals/metals have the same name). + + + #### + # Price data. + + def loadTrades(self): + """ + Populate the "Trades" table for stations. + + A trade is a connection between two stations where the SRC station + + Ignore items that have a ui_order of 0 (my way of indicating the item is + either unavailable or black market). + + NOTE: Trades MUST be loaded such that they are populated into the + lists in descending order of profit (highest profit first) + """ + + # I could make a view that does this, but then it makes it fiddly to + # port this to another database that perhaps doesn't support views. + stmt = """ + SELECT src.station_id, dst.station_id + , src.item_id + , src.buy_from + , dst.sell_to - src.buy_from AS profit + FROM Price AS src INNER JOIN Price as dst + ON src.item_id = dst.item_id + WHERE src.buy_from > 0 + AND profit > 0 + AND src.ui_order > 0 + AND dst.ui_order > 0 + ORDER BY profit DESC + """ + self.cur.execute(stmt) + stations, items = self.stationByID, self.itemByID + for (srcStnID, dstStnID, itemID, srcCostCr, profitCr) in self.cur: + srcStn, dstStn, item = stations[srcStnID], stations[dstStnID], items[itemID] + srcStn.addTrade(dstStn, item, itemID, srcCostCr, profitCr) + def getTrade(self, src, dst, item): """ Returns a Trade object describing purchase of item from src for sale at dst. """ - srcStn = self.getStation(src) - dstStn = self.getStation(dst) - trades = srcStn.trades[dstStn.ID] - return trades[item] - def query(self, *args): - """ Perform an SQL query on the DB and return the cursor. """ - conn = pypyodbc.connect(self.path) - cur = conn.cursor() - cur.execute(*args) - return cur + # I could write this more compactly, but it makes errors less readable. + srcStn = self.lookupStation(src) + dstStn = self.lookupStation(dst) + return srcStn.tradingWith[dstStn] - def fetch_all(self, sql): - """ Perform an SQL query on the DB and iterate across the rows. """ - for row in self.query(sql): - yield row - def list_search(self, listType, lookup, values, key=lambda item: item): - """ Seaches [values] for 'lookup' for least-ambiguous matches, + def load(self): + """ + Populate/re-populate this instance of TradeDB with data. + WARNING: This will orphan existing records you have + taken references to: + tdb.load() + x = tdb.lookupStation("Aulin") + tdb.load() # x now points to an orphan Aulin + """ + + self.cur = self.conn.cursor() + + # Load raw tables. Stations will be linked to systems, but nothing else. + # TODO: Make station -> system link a post-load action. + self._loadSystems() + self._loadStations() + self._loadShips() + self._loadItems() + + systems, stations, ships, items = self.systemByID, self.stationByID, self.shipByID, self.itemByID + + # Calculate the maximum distance anyone can jump so we can constrain + # the maximum "link" between any two stars. + longestJumper = max(ships.values(), key=lambda ship: ship.maxLyEmpty) + self.maxSystemLinkLy = longestJumper.maxLyEmpty + 0.01 + if self.debug > 2: print("# Max ship jump distance: %s @ %f" % (longestJumper.name(), self.maxSystemLinkLy)) + + self.buildLinks(self.maxSystemLinkLy) + + self.loadTrades() + + # In debug mode, check that everything looks sane. + if self.debug: + self._validate() + + + def _validate(self): + # Check that things correctly reference themselves. + # Check that system links are bi-directional + for (name, sys) in self.systemByName.items(): + if not sys.links: + raise ValueError("System %s has no links" % name) + if sys in sys.links: + raise ValueError("System %s has a link to itself!" % name) + if name in sys.links: + raise ValueError("System %s's name occurs in sys.links" % name) + for link in sys.links: + if not sys in link.links: + raise ValueError("System %s does not have a reciprocal link in %s's links" % (name, link.str())) + + + #### + # General purpose static methods. + + @staticmethod + def distanceSq(lhsX, lhsY, lhsZ, rhsX, rhsY, rhsZ): + """ + Calculate the square of the distance between two points. + + Pythagors theorem: distance = root( (x1-x2)^2 + (y1-y2)^2 + (z1-z2)^2 ) + + But calculating square roots is not cheap and if you don't + need the distance for display, etc, then returning the + square saves an expensive calculation: + + IF root(A^2 + B^2 + C^2) == root(D^2 + E^2 + F^2) + THEN (A^2 + B^2 + C^2) == (D^2 + E^2 + F^2) + + So instead of having to sqrt all of our distances in a complex + set, we can do this: + + maxDistSq = 300 ** 2 # check for items < 300ly + inRange = [] + for (lhs, rhs) in items: + distSq = distanceSq(lhs.x, lhs.y, lhs.z, rhs.x, rhs.y, rhs.z) + if distSq <= maxDistSq: + inRange += [[lhs, rhs]] + """ + return ((rhsX - lhsX) ** 2) + ((rhsY - lhsY) ** 2) + ((rhsZ - lhsZ) ** 2) + + + @staticmethod + def listSearch(listType, lookup, values, key=lambda item: item): + """ + Searches [values] for 'lookup' for least-ambiguous matches, return the matching value as stored in [values]. If [values] contains "bread", "water", "biscuits and "It", searching "ea" will return "bread", "WaT" will return "water" and "i" will return "biscuits". Searching for "a" will raise a ValueError because "a" matches "bread" and "water", but searching for "it" will return "It" because it provides an - exact match of a key. """ + exact match of a key. + """ + needle = TradeDB.normalizedStr(lookup) match = None - needle = self.normalized_str(lookup) for val in values: - normVal = self.normalized_str(key(val)) + normVal = TradeDB.normalizedStr(key(val)) if normVal.find(needle) > -1: # If this is an exact match, ignore ambiguities. if normVal == needle: @@ -346,8 +685,13 @@ def list_search(self, listType, lookup, values, key=lambda item: item): raise LookupError("Error: '%s' doesn't match any %s" % (lookup, listType)) return match - def normalized_str(self, str): - """ Returns a case folded, sanitized version of 'str' suitable for + + @staticmethod + def normalizedStr(str): + """ + Returns a case folded, sanitized version of 'str' suitable for performing simple and partial matches against. Removes whitespace, - hyphens, underscores, periods and apostrophes. """ - return self.normalizeRe.sub('', str).casefold() + hyphens, underscores, periods and apostrophes. + """ + return TradeDB.normalizeRe.sub('', str).casefold() +