From c5b2c4b34ce6776b463e83236538434c91b86867 Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Fri, 22 Jan 2016 05:09:19 +0000 Subject: [PATCH] Add mass and range to E:D Shipyard and Coriolis. Means that these files can be directly loaded into ETN. --- .gitmodules | 3 ++ README.md | 5 ++-- coriolis-data | 1 + coriolis.py | 34 ++++++++++++++++++++- eddb.py | 80 ++++++++++++++++++++++++++++++++++++++++++++++---- eddn.py | 3 +- loadout.py | 26 ++++++++++++++++ modules.p | Bin 0 -> 33948 bytes outfitting.py | 19 +++++++++++- setup.py | 4 ++- ships.p | 5 ++++ 11 files changed, 169 insertions(+), 11 deletions(-) create mode 100644 .gitmodules create mode 160000 coriolis-data create mode 100644 modules.p create mode 100644 ships.p 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 0000000000000000000000000000000000000000..49761e7b605d478284f625327c003f25e2cf98f3 GIT binary patch literal 33948 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