diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..cbf120a7 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "coriolis-data"] + path = coriolis-data + url = git@github.com:cmmcleod/coriolis-data.git diff --git a/README.md b/README.md index e2d48090..e1e4fc48 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,8 @@ This app downloads commodity market and other station data from the game [Elite: * sends the data to the [Elite Dangerous Data Network](http://eddn-gateway.elite-markets.net/) (“EDDNâ€�) from where you and others can use it via online trading tools such as [eddb](http://eddb.io/), [Elite Trade Net](http://etn.io/), [Inara](http://inara.cz), [ED-TD](http://ed-td.space/), [Roguey's](http://roguey.co.uk/elite-dangerous/), etc. * saves the data to files on your computer that you can load into trading tools such as [Slopey's BPC Market Tool](https://forums.frontier.co.uk/showthread.php?t=76081), [Trade Dangerous](https://bitbucket.org/kfsone/tradedangerous/wiki/Home), [Thrudd's Trading Tools](http://www.elitetradingtool.co.uk/), [Inara](http://inara.cz), [mEDI's Elite Tools](https://github.com/mEDI-S/mEDI_s-Elite-Tools), etc. -* saves a record of your ship loadout and/or flight log. +* saves a record of your ship loadout to files on your computer that you can load into [E:D Shipyard](http://www.edshipyard.com), [Coriolis](http://coriolis.io) or [Elite Trade Net](http://etn.io/). +* saves your flight log to a file on your computer and/or sends it to [Elite: Dangerous Star Map](http://www.edsm.net/). Usage -------- @@ -61,7 +62,7 @@ This app can save a variety of data in a variety of formats: * CSV format file - saves commodity market data as files that you can upload to [Thrudd's Trading Tools](http://www.elitetradingtool.co.uk/), [Inara](http://inara.cz) or [mEDI's Elite Tools](https://github.com/mEDI-S/mEDI_s-Elite-Tools). * 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) or [Coriolis](http://coriolis.io). + * 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), [Coriolis](http://coriolis.io) or [Elite Trade Net](http://etn.io/). * Flight log * Elite Dangerous Star Map - sends a record of your location to “[EDSM](http://www.edsm.net/)â€� where you can view your logs under My account → Exploration Logs, and optionally add private comments about a system. diff --git a/coriolis-data b/coriolis-data new file mode 160000 index 00000000..618127db --- /dev/null +++ b/coriolis-data @@ -0,0 +1 @@ +Subproject commit 618127dbb4effb6ad7b6a9352ef32b162d5560f6 diff --git a/coriolis.py b/coriolis.py index bae77c0a..4a559c15 100644 --- a/coriolis.py +++ b/coriolis.py @@ -1,6 +1,7 @@ # Export ship loadout in Coriolis format from collections import OrderedDict +import cPickle import json import os from os.path import join @@ -82,6 +83,10 @@ fixup_map = { } +# Ship masses +ships = cPickle.load(open(join(config.respath, 'ships.p'), 'rb')) + + def export(data, filename=None): querytime = config.getint('querytime') or int(time.time()) @@ -98,6 +103,8 @@ def export(data, filename=None): ])), ]) maxpri = 0 + mass = 0.0 + fsd = None # 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" @@ -123,6 +130,7 @@ def export(data, filename=None): module = outfitting.lookup(v['module'], ship_map) if not module: raise AssertionError('Unknown module %s' % v) # Shouldn't happen + mass += module.get('mass', 0) thing = OrderedDict([ ('class',int(module['class'])), @@ -138,6 +146,8 @@ def export(data, filename=None): loadout['components'][category]['bulkheads'] = module['name'] # Bulkheads are just strings else: loadout['components'][category][standard_map[module['name']]] = thing + if module['name'] == 'Frame Shift Drive': + fsd = module # save for range calculation else: # All other items have a "group" member, some also have a "name" if module['name'] in fixup_map: @@ -173,8 +183,30 @@ def export(data, filename=None): ('priority', maxpri), ]) + # Add mass and range + assert data['ship']['name'].lower() in companion.ship_map, data['ship']['name'] + assert companion.ship_map[data['ship']['name'].lower()] in ships, companion.ship_map[data['ship']['name'].lower()] + try: + # https://github.com/cmmcleod/coriolis/blob/master/app/js/shipyard/module-shipyard.js#L184 + hullMass = ships[companion.ship_map[data['ship']['name'].lower()]]['hullMass'] + mass += hullMass + multiplier = pow(min(data['ship']['fuel']['main']['capacity'], fsd['maxfuel']) / fsd['fuelmul'], 1.0 / fsd['fuelpower']) * fsd['optmass'] + + loadout['stats'] = OrderedDict([ + ('hullMass', hullMass), + ('fuelCapacity', data['ship']['fuel']['main']['capacity']), + ('cargoCapacity', data['ship']['cargo']['capacity']), + ('ladenMass', mass + data['ship']['fuel']['main']['capacity'] + data['ship']['cargo']['capacity']), + ('unladenMass', mass), + ('unladenRange', round(multiplier / (mass + min(data['ship']['fuel']['main']['capacity'], fsd['maxfuel'])), 2)), # fuel for one jump + ('fullTankRange', round(multiplier / (mass + data['ship']['fuel']['main']['capacity']), 2)), + ('ladenRange', round(multiplier / (mass + data['ship']['fuel']['main']['capacity'] + data['ship']['cargo']['capacity']), 2)), + ]) + except: + if __debug__: raise + # Construct description - string = json.dumps(loadout, indent=2) + string = json.dumps(loadout, indent=2, separators=(',', ': ')) if filename: with open(filename, 'wt') as h: diff --git a/eddb.py b/eddb.py index 76a82eee..cc233ed8 100755 --- a/eddb.py +++ b/eddb.py @@ -31,21 +31,23 @@ class EDDB: return (station_id, bool(flags & EDDB.HAS_MARKET), bool(flags & EDDB.HAS_OUTFITTING), bool(flags & EDDB.HAS_SHIPYARD)) -# build system & station database from files systems.json and stations.json from http://eddb.io/api +# build databases from files systems.json, stations.json and modules.json from http://eddb.io/api +# and from https://github.com/cmmcleod/coriolis-data if __name__ == "__main__": import json # still send market and outfitting for currently "suspended" stations - # https://community.elitedangerous.com/galnet/uid/568a999f9657ba5e0986a8de - suspended = set([659, 39328, 5672, 30402, 30653, 21901, 11335]) + # https://community.elitedangerous.com/galnet/uid/569f610b9657ba7d3461ba04 + suspended = set([7, 10035, 9765, 659, 39328, 5672, 30402, 30653, 21901, 11335]) # system_name by system_id - systems = dict([(x['id'], str(x['name'])) for x in json.loads(open('systems.json').read())]) + systems = dict([(x['id'], str(x['name'])) for x in json.load(open('systems.json'))]) - stations = json.loads(open('stations.json').read()) + stations = json.load(open('stations.json')) # check that all populated systems have known coordinates - coords = dict([(x['id'], x['x'] or x['y'] or x['z']) for x in json.loads(open('systems.json').read())]) + coords = dict([(x['id'], x['x'] or x['y'] or x['z']) for x in json.load(open('systems.json'))]) for x in stations: assert x['system_id'] == 17072 or coords[x['system_id']], (x['system_id'], systems[x['system_id']]) @@ -63,3 +65,71 @@ if __name__ == "__main__": for x in stations]) cPickle.dump(station_ids, open('stations.p', 'wb'), protocol = cPickle.HIGHEST_PROTOCOL) + + # Map eddb's names to names displayed in the in-game shipyard + eddb_ship_map = { + 'Sidewinder Mk. I' : 'Sidewinder', + 'Eagle Mk. II' : 'Eagle', + 'Cobra Mk. III' : 'Cobra MkIII', + 'Cobra MK IV' : 'Cobra MkIV', + 'Viper Mk III' : 'Viper MkIII', + 'Viper MK IV' : 'Viper MkIV', + } + + # PP modules (see weapon-map in outfitting.py) + specials = { + 'Retributor' : 'Retributor Beam Laser', + 'Pack-Hound' : 'Pack-Hound Missile Rack', + 'Mining Lance' : 'Mining Lance Beam Laser', + 'Enforcer' : 'Enforcer Cannon', + 'Advanced' : 'Advanced Plasma Accelerator', + 'Distruptor' : 'Pulse Disruptor Laser', + 'Cytoscrambler' : 'Cytoscrambler Burst Laser', + 'Imperial Hammer' : 'Imperial Hammer Rail Gun', + 'Pacifier' : 'Pacifier Frag-Cannon', + 'Prismatic' : 'Prismatic Shield Generator', + } + + # Module masses + modules = {} + for m in json.load(open('modules.json')): + # ignore mount and guidance, and convert strings to ascii to save space + key = (specials.get(m['name'], str(m['name'] or m['group']['name'])), + m['ship'] and eddb_ship_map.get(m['ship'], str(m['ship'])), + str(m['class']), + str(m['rating'])) + if key in modules: + # Test our assumption that mount and guidance don't affect mass + assert modules[key]['mass'] == m.get('mass', 0), '%s !=\n%s' % (key, m) + else: + modules[key] = { 'mass': m.get('mass', 0) } # Some modules don't have mass + + # Add FSD data from Coriolis + for m in json.load(open('coriolis-data/components/standard/frame_shift_drive.json')).values(): + key = ('Frame Shift Drive', None, str(m['class']), str(m['rating'])) + assert key in modules, key + modules[key].update({ + 'optmass' : m['optmass'], + 'maxfuel' : m['maxfuel'], + 'fuelmul' : m['fuelmul'], + 'fuelpower' : m['fuelpower'], + }) + cPickle.dump(modules, open('modules.p', 'wb'), protocol = cPickle.HIGHEST_PROTOCOL) + + + # Map Coriolis's names to names displayed in the in-game shipyard + coriolis_ship_map = { + 'Cobra Mk III' : 'Cobra MkIII', + 'Cobra Mk IV' : 'Cobra MkIV', + 'Viper' : 'Viper MkIII', + 'Viper Mk IV' : 'Viper MkIV', + } + + # Ship masses + ships = {} + for f in os.listdir('coriolis-data/ships'): + if not f.endswith('.json'): continue + for m in json.load(open(join('coriolis-data/ships', f))).values(): + ships[coriolis_ship_map.get(m['properties']['name'], str(m['properties']['name']))] = { 'hullMass' : m['properties']['hullMass'] } + cPickle.dump(ships, open('ships.p', 'wb'), protocol = cPickle.HIGHEST_PROTOCOL) + diff --git a/eddn.py b/eddn.py index f77c8e3a..8577150d 100644 --- a/eddn.py +++ b/eddn.py @@ -73,12 +73,13 @@ def export_commodities(data): def export_outfitting(data): # *Do* send empty modules list - implies station has no outfitting + schemakeys = ['category', 'name', 'mount', 'guidance', 'ship', 'class', 'rating'] modules = [] for v in data['lastStarport'].get('modules', {}).itervalues(): try: module = outfitting.lookup(v, ship_map) if module: - modules.append(module) + modules.append({ k: module[k] for k in schemakeys if k in module }) # just the relevant keys except AssertionError as e: if __debug__: print 'Outfitting: %s' % e # Silently skip unrecognized modules except: diff --git a/loadout.py b/loadout.py index b6eff691..194f030a 100644 --- a/loadout.py +++ b/loadout.py @@ -1,6 +1,7 @@ # Export ship loadout in E:D Shipyard format from collections import defaultdict +import cPickle import os from os.path import join import re @@ -36,6 +37,11 @@ slot_map = { 'fueltank' : 'FS', } + +# Ship masses +ships = cPickle.load(open(join(config.respath, 'ships.p'), 'rb')) + + def export(data, filename=None): def class_rating(module): @@ -49,6 +55,8 @@ def export(data, filename=None): querytime = config.getint('querytime') or int(time.time()) loadout = defaultdict(list) + mass = 0.0 + fsd = None for slot in sorted(data['ship']['modules']): @@ -60,6 +68,7 @@ def export(data, filename=None): if not module: continue cr = class_rating(module) + mass += module.get('mass', 0) # Specials if module['name'] in ['Fuel Tank', 'Cargo Rack']: @@ -67,6 +76,9 @@ def export(data, filename=None): else: name = module['name'] + if name == 'Frame Shift Drive': + fsd = module # save for range calculation + for s in slot_map: if slot.lower().startswith(s): loadout[slot_map[s]].append(cr + name) @@ -92,6 +104,20 @@ def export(data, filename=None): string += '%s: %s\n' % (slot, name) string += '---\nCargo : %d T\nFuel : %d T\n' % (data['ship']['cargo']['capacity'], data['ship']['fuel']['main']['capacity']) + # Add mass and range + assert data['ship']['name'].lower() in companion.ship_map, data['ship']['name'] + assert companion.ship_map[data['ship']['name'].lower()] in ships, companion.ship_map[data['ship']['name'].lower()] + try: + # https://github.com/cmmcleod/coriolis/blob/master/app/js/shipyard/module-shipyard.js#L184 + mass += ships[companion.ship_map[data['ship']['name'].lower()]]['hullMass'] + string += 'Mass : %.1f T empty\n %.1f T full\n' % (mass, mass + data['ship']['fuel']['main']['capacity']+ data['ship']['cargo']['capacity']) + multiplier = pow(min(data['ship']['fuel']['main']['capacity'], fsd['maxfuel']) / fsd['fuelmul'], 1.0 / fsd['fuelpower']) * fsd['optmass'] + string += 'Range : %.2f LY unladen\n %.2f LY laden\n' % ( + multiplier / (mass + data['ship']['fuel']['main']['capacity']), + multiplier / (mass + data['ship']['fuel']['main']['capacity'] + data['ship']['cargo']['capacity'])) + except: + if __debug__: raise + if filename: with open(filename, 'wt') as h: h.write(string) diff --git a/modules.p b/modules.p new file mode 100644 index 00000000..49761e7b Binary files /dev/null and b/modules.p differ diff --git a/outfitting.py b/outfitting.py index d82aef89..e863b7e7 100644 --- a/outfitting.py +++ b/outfitting.py @@ -1,3 +1,5 @@ +import cPickle +from os.path import join import time import companion @@ -209,6 +211,10 @@ internal_map = { } +# Module mass, FSD data etc +moduledata = cPickle.load(open(join(config.respath, 'modules.p'), 'rb')) + + # Given a module description from the Companion API returns a description of the module in the form of a # dict { category, name, [mount], [guidance], [ship], rating, class } using the same terms found in the # English langauge game. For fitted modules, dict also includes { enabled, priority }. @@ -319,8 +325,19 @@ def lookup(module, ship_map): if 'on' in module and 'priority' in module: new['enabled'], new['priority'] = module['on'], module['priority'] # priority is zero-based + # Extra module data + key = (new['name'], 'ship' in new and companion.ship_map.get(name[0]) or None, new['class'], new['rating']) + if __debug__: + assert key in moduledata, key + m = moduledata.get(key, {}) + if new['name'] == 'Frame Shift Drive': + assert 'mass' in m and 'optmass' in m and 'maxfuel' in m and 'fuelmul' in m and 'fuelpower' in m, m + else: + assert 'mass' in m, m + new.update(moduledata.get(key, {})) + # check we've filled out mandatory fields - for thing in ['category', 'name', 'class', 'rating']: + for thing in ['category', 'name', 'class', 'rating']: # Don't consider mass etc as mandatory if not new.get(thing): raise AssertionError('%s: failed to set %s' % (module['id'], thing)) if new['category'] == 'hardpoint' and not new.get('mount'): raise AssertionError('%s: failed to set %s' % (module['id'], 'mount')) diff --git a/setup.py b/setup.py index d7ff694b..0ae3f8a1 100755 --- a/setup.py +++ b/setup.py @@ -69,7 +69,7 @@ if sys.platform=='darwin': 'frameworks': [ 'Sparkle.framework' ], 'excludes': [ 'PIL', 'simplejson' ], 'iconfile': '%s.icns' % APPNAME, - 'resources': ['snd_good.wav', 'snd_bad.wav', 'stations.p', 'systems.p'], + 'resources': ['snd_good.wav', 'snd_bad.wav', 'modules.p', 'ships.p', 'stations.p', 'systems.p'], 'semi_standalone': True, 'site_packages': False, 'plist': { @@ -106,6 +106,8 @@ elif sys.platform=='win32': 'WinSparkle.pdb', # For debugging - don't include in package 'snd_good.wav', 'snd_bad.wav', + 'modules.p', + 'ships.p', 'stations.p', 'systems.p', '%s.VisualElementsManifest.xml' % APPNAME, diff --git a/ships.p b/ships.p new file mode 100644 index 00000000..e1a1674b --- /dev/null +++ b/ships.p @@ -0,0 +1,5 @@ +€}q(UFederal Assault Ship}qUhullMassqMàsU +Viper MkIVq}qhK¾sUFederal Gunship}qhMDsU Viper MkIIIq}qhK