From c116aed03c42b1a1c1ca3337b78565281a4fe935 Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Sat, 1 Aug 2015 23:49:10 +0100 Subject: [PATCH] Add support for saving loadout in Coriolis format. Ref #20. --- EDMarketConnector.py | 9 ++- README.md | 2 +- config.py | 3 +- coriolis.py | 164 +++++++++++++++++++++++++++++++++++++++++++ prefs.py | 22 +++--- 5 files changed, 186 insertions(+), 14 deletions(-) create mode 100644 coriolis.py diff --git a/EDMarketConnector.py b/EDMarketConnector.py index b335e3ae..0482a8ad 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -21,6 +21,7 @@ import bpc import td import eddn import loadout +import coriolis import flightlog import stats import chart @@ -213,8 +214,10 @@ class AppWindow: chart.export(data) if config.getint('output') & config.OUT_LOG: flightlog.export(data) - if config.getint('output') & config.OUT_SHIP: + if config.getint('output') & config.OUT_SHIP_EDS: loadout.export(data) + if config.getint('output') & config.OUT_SHIP_CORIOLIS: + coriolis.export(data) return else: @@ -230,8 +233,10 @@ class AppWindow: chart.export(data) if config.getint('output') & config.OUT_LOG: flightlog.export(data) - if config.getint('output') & config.OUT_SHIP: + if config.getint('output') & config.OUT_SHIP_EDS: loadout.export(data) + if config.getint('output') & config.OUT_SHIP_CORIOLIS: + coriolis.export(data) if not (config.getint('output') & (config.OUT_CSV|config.OUT_TD|config.OUT_BPC|config.OUT_EDDN)): # no further output requested diff --git a/README.md b/README.md index 67c0375c..16a63f5b 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ This app can save a variety of data in a variety of formats: * CSV format - saves commodity market data as files that you can upload to [Thrudd's Trading Tools](http://www.elitetradingtool.co.uk/) or [Inara](http://inara.cz). * Ship loadout - * After every outfitting change saves a record of your ship loadout as a file that you can open in a text editor and that you can import into [E:D Shipyard](http://www.edshipyard.com). + * After every outfitting change saves a record of your ship loadout as a file that you can open in a text editor and that you can import into [E:D Shipyard](http://www.edshipyard.com) or [Coriolis](http://coriolis.io). * Flight log * Adds a record of your location, ship and cargo to a file that you can open in a text editor or a spreadsheet program such as Excel. Note: Don't edit, rename or move this file - take a copy if you wish to change it. diff --git a/config.py b/config.py index 6b71c2fa..95cb23bd 100644 --- a/config.py +++ b/config.py @@ -69,9 +69,10 @@ class Config: OUT_BPC = 2 OUT_TD = 4 OUT_CSV = 8 - OUT_SHIP = 16 + OUT_SHIP_EDS = 16 OUT_LOG = 32 OUT_STAT = 64 + OUT_SHIP_CORIOLIS = 128 if platform=='darwin': diff --git a/coriolis.py b/coriolis.py new file mode 100644 index 00000000..2bf3961d --- /dev/null +++ b/coriolis.py @@ -0,0 +1,164 @@ +# Export ship loadout in Coriolis format + +from collections import OrderedDict +import json +import os +from os.path import join +import re +import time + +from config import config +import outfitting +import companion + + +# Map draft EDDN outfitting to Coriolis +# https://raw.githubusercontent.com/jamesremuscat/EDDN/master/schemas/outfitting-v1.0-draft.json +# http://cdn.coriolis.io/schemas/ship-loadout/1.json + +ship_map = dict(companion.ship_map) +ship_map['Asp'] = 'Asp Explorer' + +category_map = { + 'standard' : 'standard', + 'internal' : 'internal', + 'hardpoint' : 'hardpoints', + 'utility' : 'utility', +} + +slot_map = { + 'HugeHardpoint' : 'hardpoints', + 'LargeHardpoint' : 'hardpoints', + 'MediumHardpoint' : 'hardpoints', + 'SmallHardpoint' : 'hardpoints', + 'TinyHardpoint' : 'utility', + 'Slot' : 'internal', +} + +standard_map = OrderedDict([ # in output order + ('Armour', 'bulkheads'), + ('Power Plant', 'powerPlant'), + ('Thrusters', 'thrusters'), + ('Frame Shift Drive', 'frameShiftDrive'), + ('Life Support', 'lifeSupport'), + ('Power Distributor', 'powerDistributor'), + ('Sensors', 'sensors'), + ('Fuel Tank', 'fuelTank'), +]) + +weaponmount_map = { + 'Fixed' : 'Fixed', + 'Gimballed' : 'Gimballed', + 'Turreted' : 'Turret', +} + + +# Modules that have a name as well as a group +bulkheads = outfitting.armour_map.values() +scanners = [x[0] for x in outfitting.stellar_map.values()] +countermeasures = [x[0] for x in outfitting.countermeasure_map.values()] +fixup_map = { + 'Advanced Plasma Accelerator' : ('Plasma Accelerator', 'Advanced'), + 'Cytoscrambler Burst Laser' : ('Burst Laser', 'Cytoscrambler'), + 'Enforcer Cannon' : ('Multi-cannon', 'Enforcer'), + 'Frame Shift Drive Interdictor' : ('FSD Interdictor', None), + 'Imperial Hammer Rail Gun' : ('Rail Gun', 'Imperial Hammer'), + 'Impulse Mine Launcher' : ('Mine Launcher', 'Impulse'), + 'Mining Lance Beam Laser' : ('Mining Laser', 'Mining Lance'), + 'Multi-Cannon' : ('Multi-cannon', None), + 'Pacifier Frag-Cannon' : ('Fragment Cannon', 'Pacifier'), + 'Pack-Hound Missile Rack' : ('Missile Rack', 'Pack-Hound'), + 'Pulse Disruptor Laser' : ('Pulse Laser', 'Distruptor'), # Note sp + 'Standard Docking Computer' : ('Docking Computer', 'Standard Docking Computer'), +} + + +def export(data): + + querytime = config.getint('querytime') or int(time.time()) + + ship = companion.ship_map.get(data['ship']['name'], data['ship']['name']) + + loadout = OrderedDict([ # Mimic Coriolis export ordering + ('$schema', 'http://cdn.coriolis.io/schemas/ship-loadout/1.json#'), + ('name', ship_map.get(data['ship']['name'], data['ship']['name'])), + ('ship', ship_map.get(data['ship']['name'], data['ship']['name'])), + ('components', OrderedDict([ + ('standard', OrderedDict([(x,None) for x in standard_map.values()])), + ('hardpoints', []), + ('utility', []), + ('internal', []), + ])), + ]) + + # Correct module ordering relies on the fact that "Slots" in the data are correctly ordered alphabetically. + # Correct hardpoint ordering additionally relies on the fact that "Huge" < "Large" < "Medium" < "Small" + for slot in sorted(data['ship']['modules']): + + v = data['ship']['modules'][slot] + try: + if not v: + # Need to add nulls for empty slots. Assumes that standard slots can't be empty. + for s in slot_map: + if slot.startswith(s): + loadout['components'][slot_map[s]].append(None) + break + continue + + module = outfitting.lookup(v['module']) + if not module: continue + + category = loadout['components'][category_map[module['category']]] + thing = OrderedDict([ + ('class', module['class']), + ('rating', module['rating']), + ]) + + if module['name'] in bulkheads: + # Bulkheads are just strings + category['bulkheads'] = module['name'] + elif module['category'] == 'standard': + # Standard items are indexed by "group" rather than containing a "group" member + category[standard_map[module['name']]] = thing + else: + # All other items have a "group" member, some also have a "name" + if module['name'] in scanners: + thing['group'] = 'Scanner' + thing['name'] = module['name'] + elif module['name'] in countermeasures: + thing['group'] = 'Countermeasure' + thing['name'] = module['name'] + elif module['name'] in fixup_map: + thing['group'], name = fixup_map[module['name']] + if name: thing['name'] = name + else: + thing['group'] = module['name'] + + if 'mount' in module: + thing['mount'] = weaponmount_map[module['mount']] + if 'guidance' in module: + thing['missile'] = module['guidance'][0] # not mentioned in schema + + category.append(thing) + + except AssertionError as e: + if __debug__: print 'Loadout: %s' % e + continue # Silently skip unrecognized modules + except: + if __debug__: raise + + # Construct description + string = json.dumps(loadout, indent=2) + + # Look for last ship of this type + regexp = re.compile(re.escape(ship) + '\.\d\d\d\d\-\d\d\-\d\dT\d\d\.\d\d\.\d\d\.json') + oldfiles = sorted([x for x in os.listdir(config.get('outdir')) if regexp.match(x)]) + if oldfiles: + with open(join(config.get('outdir'), oldfiles[-1]), 'rU') as h: + if h.read() == string: + return # same as last time - don't write + + # Write + filename = join(config.get('outdir'), '%s.%s.json' % (ship, time.strftime('%Y-%m-%dT%H.%M.%S', time.localtime(querytime)))) + with open(filename, 'wt') as h: + h.write(string) diff --git a/prefs.py b/prefs.py index 467b8073..b245dd33 100644 --- a/prefs.py +++ b/prefs.py @@ -78,7 +78,7 @@ class PreferencesDialog(tk.Toplevel): outframe.grid(padx=10, pady=10, sticky=tk.NSEW) outframe.columnconfigure(0, weight=1) - output = config.getint('output') or (config.OUT_EDDN | config.OUT_SHIP | config.OUT_STAT) + output = config.getint('output') or (config.OUT_EDDN | config.OUT_SHIP_EDS | config.OUT_STAT) ttk.Label(outframe, text="Please choose what data to save").grid(row=0, columnspan=2, padx=5, pady=3, sticky=tk.W) self.out_eddn= tk.IntVar(value = (output & config.OUT_EDDN) and 1 or 0) ttk.Checkbutton(outframe, text="Send station data to the Elite Dangerous Data Network", variable=self.out_eddn).grid(row=1, columnspan=2, padx=5, sticky=tk.W) @@ -88,19 +88,21 @@ class PreferencesDialog(tk.Toplevel): ttk.Checkbutton(outframe, text="Market data in Trade Dangerous format", variable=self.out_td, command=self.outvarchanged).grid(row=3, columnspan=2, padx=5, sticky=tk.W) self.out_csv = tk.IntVar(value = (output & config.OUT_CSV ) and 1 or 0) ttk.Checkbutton(outframe, text="Market data in CSV format", variable=self.out_csv, command=self.outvarchanged).grid(row=4, columnspan=2, padx=5, sticky=tk.W) - self.out_ship= tk.IntVar(value = (output & config.OUT_SHIP) and 1 or 0) - ttk.Checkbutton(outframe, text="Ship loadout in E:D Shipyard format", variable=self.out_ship, command=self.outvarchanged).grid(row=5, columnspan=2, padx=5, sticky=tk.W) + self.out_ship_eds= tk.IntVar(value = (output & config.OUT_SHIP_EDS) and 1 or 0) + ttk.Checkbutton(outframe, text="Ship loadout in E:D Shipyard format", variable=self.out_ship_eds, command=self.outvarchanged).grid(row=5, columnspan=2, padx=5, sticky=tk.W) + self.out_ship_coriolis= tk.IntVar(value = (output & config.OUT_SHIP_CORIOLIS) and 1 or 0) + ttk.Checkbutton(outframe, text="Ship loadout in Coriolis format", variable=self.out_ship_coriolis, command=self.outvarchanged).grid(row=6, columnspan=2, padx=5, sticky=tk.W) self.out_log = tk.IntVar(value = (output & config.OUT_LOG ) and 1 or 0) - ttk.Checkbutton(outframe, text="Flight log", variable=self.out_log, command=self.outvarchanged).grid(row=6, columnspan=2, padx=5, sticky=tk.W) + ttk.Checkbutton(outframe, text="Flight log", variable=self.out_log, command=self.outvarchanged).grid(row=7, columnspan=2, padx=5, sticky=tk.W) self.out_stat= tk.IntVar(value = have_openpyxl and (output & config.OUT_STAT) and 1 or 0) - ttk.Checkbutton(outframe, text="Cmdr statistics", variable=self.out_stat, command=self.outvarchanged, state=have_openpyxl and tk.NORMAL or tk.DISABLED).grid(row=7, columnspan=2, padx=5, sticky=tk.W) + ttk.Checkbutton(outframe, text="Cmdr statistics", variable=self.out_stat, command=self.outvarchanged, state=have_openpyxl and tk.NORMAL or tk.DISABLED).grid(row=8, columnspan=2, padx=5, sticky=tk.W) - ttk.Label(outframe, text=(platform=='darwin' and 'Where:' or 'File location:')).grid(row=8, padx=5, pady=(5,0), sticky=tk.NSEW) + ttk.Label(outframe, text=(platform=='darwin' and 'Where:' or 'File location:')).grid(row=9, padx=5, pady=(5,0), sticky=tk.NSEW) self.outbutton = ttk.Button(outframe, text=(platform=='darwin' and 'Change...' or 'Browse...'), command=self.outbrowse) - self.outbutton.grid(row=8, column=1, padx=5, pady=(5,0), sticky=tk.NSEW) + self.outbutton.grid(row=9, column=1, padx=5, pady=(5,0), sticky=tk.NSEW) self.outdir = ttk.Entry(outframe) self.outdir.insert(0, config.get('outdir')) - self.outdir.grid(row=9, columnspan=2, padx=5, pady=5, sticky=tk.EW) + self.outdir.grid(row=10, columnspan=2, padx=5, pady=5, sticky=tk.EW) self.outvarchanged() privacyframe = ttk.LabelFrame(frame, text='Privacy') @@ -127,7 +129,7 @@ class PreferencesDialog(tk.Toplevel): #self.wait_window(self) # causes duplicate events on OSX def outvarchanged(self): - local = self.out_bpc.get() or self.out_td.get() or self.out_csv.get() or self.out_ship.get() or self.out_log.get() or self.out_stat.get() + local = self.out_bpc.get() or self.out_td.get() or self.out_csv.get() or self.out_ship_eds.get() or self.out_ship_coriolis.get() or self.out_log.get() or self.out_stat.get() self.outbutton['state'] = local and tk.NORMAL or tk.DISABLED self.outdir['state'] = local and 'readonly' or tk.DISABLED @@ -166,7 +168,7 @@ class PreferencesDialog(tk.Toplevel): credentials = (config.get('username'), config.get('password')) config.set('username', self.username.get().strip()) config.set('password', self.password.get().strip()) - config.set('output', (self.out_eddn.get() and config.OUT_EDDN or 0) + (self.out_bpc.get() and config.OUT_BPC or 0) + (self.out_td.get() and config.OUT_TD or 0) + (self.out_csv.get() and config.OUT_CSV or 0) + (self.out_ship.get() and config.OUT_SHIP or 0) + (self.out_log.get() and config.OUT_LOG or 0) + (self.out_stat.get() and config.OUT_STAT or 0)) + config.set('output', (self.out_eddn.get() and config.OUT_EDDN or 0) + (self.out_bpc.get() and config.OUT_BPC or 0) + (self.out_td.get() and config.OUT_TD or 0) + (self.out_csv.get() and config.OUT_CSV or 0) + (self.out_ship_eds.get() and config.OUT_SHIP_EDS or 0) + (self.out_log.get() and config.OUT_LOG or 0) + (self.out_stat.get() and config.OUT_STAT or 0) + (self.out_ship_coriolis.get() and config.OUT_SHIP_CORIOLIS or 0)) config.set('outdir', self.outdir.get().strip()) config.set('anonymous', self.out_anon.get()) self.destroy()