1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-04-13 15:57:14 +03:00

Switch EDSM integration to a plugin

This commit is contained in:
Jonathan Harris 2017-07-29 11:35:56 +01:00
parent 93ff800f90
commit e810b87dd4
8 changed files with 377 additions and 405 deletions

View File

@ -54,7 +54,6 @@ import commodity
from commodity import COMMODITY_CSV
import td
import eddn
import edsm
import coriolis
import edshipyard
import loadout
@ -68,7 +67,6 @@ from theme import theme
SERVER_RETRY = 5 # retry pause for Companion servers [s]
EDSM_POLL = 0.1
# Limits on local clock drift from EDDN gateway
DRIFT_THRESHOLD = 3 * 60
@ -87,7 +85,6 @@ class AppWindow:
self.holdofftime = config.getint('querytime') + companion.holdoff
self.session = companion.Session()
self.edsm = edsm.EDSM()
self.eddn = eddn.EDDN(self)
self.w = master
@ -113,19 +110,15 @@ class AppWindow:
self.cmdr_label = tk.Label(frame)
self.ship_label = tk.Label(frame)
self.system_label = tk.Label(frame)
self.cmdr_label.grid(row=1, column=0, sticky=tk.W)
self.ship_label.grid(row=2, column=0, sticky=tk.W)
self.system_label.grid(row=3, column=0, sticky=tk.W)
self.cmdr = tk.Label(frame, anchor=tk.W)
self.ship = HyperlinkLabel(frame, url = self.shipyard_url)
self.system = HyperlinkLabel(frame, compound=tk.RIGHT, url = self.system_url, popup_copy = True)
self.cmdr.grid(row=1, column=1, sticky=tk.EW)
self.ship.grid(row=2, column=1, sticky=tk.EW)
self.system.grid(row=3, column=1, sticky=tk.EW)
for plugin in plug.PLUGINS:
appitem = plugin.get_app(frame)
@ -347,7 +340,6 @@ class AppWindow:
self.cmdr_label['text'] = _('Cmdr') + ':' # Main window
self.ship_label['text'] = (monitor.state['Captain'] and _('Role') or # Multicrew role label in main window
_('Ship')) + ':' # Main window
self.system_label['text'] = _('System') + ':' # Main window
self.button['text'] = self.theme_button['text'] = _('Update') # Update button in main window
if platform == 'darwin':
self.menubar.entryconfigure(1, label=_('File')) # Menu title
@ -434,7 +426,6 @@ class AppWindow:
hotkeymgr.play_good()
self.status['text'] = _('Fetching data...')
self.button['state'] = self.theme_button['state'] = tk.DISABLED
self.edit_menu.entryconfigure(0, state=tk.DISABLED) # Copy
self.w.update_idletasks()
try:
@ -469,11 +460,10 @@ class AppWindow:
self.ship['text'] = companion.ship_map.get(data['ship']['name'].lower(), data['ship']['name'])
monitor.state['ShipID'] = data['ship']['id']
monitor.state['ShipType'] = data['ship']['name'].lower()
if not monitor.system:
self.system['text'] = data['lastSystem']['name']
self.system['image'] = ''
self.status['text'] = ''
self.edit_menu.entryconfigure(0, state=tk.NORMAL) # Copy
if data['commander'].get('credits') is not None:
monitor.state['Credits'] = data['commander']['credits']
monitor.state['Loan'] = data['commander'].get('debt', 0)
# stuff we can do when not docked
err = plug.notify_newdata(data, monitor.is_beta)
@ -484,16 +474,6 @@ class AppWindow:
if config.getint('output') & config.OUT_SHIP:
loadout.export(data)
# Send flightlog EDSM if FSDJump failed to do so
if config.getint('output') & config.OUT_SYS_EDSM and self.edsm.result['img'] == self.edsm._IMG_ERROR and not monitor.is_beta and not monitor.captain and config.get('cmdrs') and monitor.cmdr in config.get('cmdrs') and config.get('edsm_usernames')[config.get('cmdrs').index(monitor.cmdr)]:
try:
self.edsm.writelog(querytime, monitor.system, monitor.coordinates, monitor.state['ShipID'])
except Exception as e:
if __debug__: print_exc()
self.status['text'] = unicode(e)
play_bad = True
self.edsmpoll()
if not (config.getint('output') & ~config.OUT_SHIP & config.OUT_STATION_ANY):
# no station data requested - we're done
pass
@ -546,27 +526,6 @@ class AppWindow:
if not old_status:
self.status['text'] = ''
# Update credits and ship info and send to EDSM
if not monitor.is_beta and config.getint('output') & config.OUT_SYS_EDSM:
try:
if data['commander'].get('credits') is not None:
monitor.state['Credits'] = data['commander']['credits']
monitor.state['Loan'] = data['commander'].get('debt', 0)
self.edsm.setcredits(monitor.state['Credits'], monitor.state['Loan'])
ship = companion.ship(data)
if ship != self.edsm.lastship:
self.edsm.updateship(monitor.state['ShipID'],
monitor.state['ShipType'],
[
('linkToCoriolis', coriolis.url(data, monitor.is_beta)),
('linkToEDShipyard', edshipyard.url(data, monitor.is_beta)),
])
self.edsm.lastship = ship
except Exception as e:
# Not particularly important so silent on failure
if __debug__: print_exc()
except companion.VerificationRequired:
return prefs.AuthenticationDialog(self.w, partial(self.verify, self.getandsend))
@ -627,7 +586,6 @@ class AppWindow:
if not entry:
return
system_changed = monitor.system and self.system['text'] != monitor.system
# Update main window
if monitor.cmdr and monitor.state['Captain']:
@ -649,63 +607,13 @@ class AppWindow:
self.ship_label['text'] = _('Ship') + ':' # Main window
self.ship['text'] = ''
if self.system['text'] != monitor.system:
self.system['text'] = monitor.system or ''
self.edsm.link(monitor.system)
self.edsmpoll()
self.edit_menu.entryconfigure(0, state=monitor.system and tk.NORMAL or tk.DISABLED) # Copy
if entry['event'] in ['Undocked', 'StartJump', 'SetUserShipName', 'ShipyardBuy', 'ShipyardSell', 'ShipyardSwap', 'ModuleBuy', 'ModuleSell', 'MaterialCollected', 'MaterialDiscarded', 'ScientificResearch', 'EngineerCraft', 'Synthesis', 'JoinACrew']:
self.status['text'] = '' # Periodically clear any old error
self.w.update_idletasks()
# Send interesting events to EDSM
if config.getint('output') & config.OUT_SYS_EDSM and not monitor.is_beta and not monitor.captain and config.get('cmdrs') and monitor.cmdr in config.get('cmdrs') and config.get('edsm_usernames')[config.get('cmdrs').index(monitor.cmdr)]:
try:
# Update system status on startup
if entry['event'] in [None, 'StartUp'] and monitor.mode and monitor.system:
self.edsm.lookup(monitor.system)
# Send credits to EDSM on new game (but not on startup - data might be old)
if entry['event'] == 'LoadGame':
self.edsm.setcredits(monitor.state['Credits'], monitor.state['Loan'])
# Send rank info to EDSM on startup or change
if entry['event'] in ['StartUp', 'Progress', 'Promotion'] and monitor.state['Rank']:
self.edsm.setranks(monitor.state['Rank'])
# Send ship info to EDSM on startup or change
if entry['event'] in ['StartUp', 'Loadout', 'LoadGame', 'SetUserShipName'] and monitor.cmdr and monitor.state['ShipID']:
self.edsm.setshipid(monitor.state['ShipID'])
props = []
if monitor.state['ShipIdent'] is not None:
props.append(('shipIdent', monitor.state['ShipIdent']))
if monitor.state['ShipName'] is not None:
props.append(('shipName', monitor.state['ShipName']))
if monitor.state['PaintJob'] is not None:
props.append(('paintJob', monitor.state['PaintJob']))
self.edsm.updateship(monitor.state['ShipID'], monitor.state['ShipType'], props)
elif entry['event'] in ['ShipyardBuy', 'ShipyardSell']:
self.edsm.sellship(entry.get('SellShipID'))
# Send materials info to EDSM on startup or change
if entry['event'] in ['StartUp', 'LoadGame', 'MaterialCollected', 'MaterialDiscarded', 'ScientificResearch', 'EngineerCraft', 'Synthesis']:
self.edsm.setmaterials(monitor.state['Raw'], monitor.state['Manufactured'], monitor.state['Encoded'])
# Send paintjob info to EDSM on change
if entry['event'] in ['ModuleBuy', 'ModuleSell'] and entry['Slot'] == 'PaintJob':
self.edsm.updateship(monitor.state['ShipID'], monitor.state['ShipType'], [('paintJob', monitor.state['PaintJob'])])
# Write EDSM log on change
if monitor.mode and entry['event'] in ['Location', 'FSDJump']:
self.edsm.writelog(timegm(strptime(entry['timestamp'], '%Y-%m-%dT%H:%M:%SZ')), monitor.system, monitor.coordinates, monitor.state['ShipID'])
except Exception as e:
if __debug__: print_exc()
self.status['text'] = unicode(e)
if not config.getint('hotkey_mute'):
hotkeymgr.play_bad()
self.edsmpoll()
# Companion login - do this after EDSM so any EDSM errors don't mask login errors
# Companion login
if entry['event'] in [None, 'StartUp', 'NewCommander', 'LoadGame'] and monitor.cmdr:
if config.get('cmdrs') and monitor.cmdr in config.get('cmdrs'):
prefs.make_current(monitor.cmdr)
@ -737,7 +645,7 @@ class AppWindow:
return
# Plugin backwards compatibility
if system_changed:
if monitor.mode and entry['event'] in ['StartUp', 'Location', 'FSDJump']:
plug.notify_system_changed(timegm(strptime(entry['timestamp'], '%Y-%m-%dT%H:%M:%SZ')), monitor.system, monitor.coordinates)
# Auto-Update after docking
@ -812,13 +720,6 @@ class AppWindow:
if not config.getint('hotkey_mute'):
hotkeymgr.play_bad()
def edsmpoll(self):
result = self.edsm.result
if result['done']:
self.system['image'] = result['img']
else:
self.w.after(int(EDSM_POLL * 1000), self.edsmpoll)
def shipyard_url(self, shipname=None):
if not monitor.cmdr or not monitor.mode:
@ -856,9 +757,6 @@ class AppWindow:
assert False, config.getint('shipyard')
return False
def system_url(self, text):
return text and self.edsm.result['url']
def cooldown(self):
if time() < self.holdofftime:
self.button['text'] = self.theme_button['text'] = _('cooldown {SS}s').format(SS = int(self.holdofftime - time())) # Update button in main window

View File

@ -138,6 +138,9 @@
<Component Guid="*">
<File KeyPath="yes" Source="SourceDir\eddb.py" />
</Component>
<Component Guid="*">
<File KeyPath="yes" Source="SourceDir\edsm.py" />
</Component>
<Component Guid="*">
<File KeyPath="yes" Source="SourceDir\es.strings" />
</Component>
@ -447,6 +450,7 @@
<ComponentRef Id="EDMarketConnector.VisualElementsManifest.xml" />
<ComponentRef Id="EDMC.exe" />
<ComponentRef Id="eddb.py" />
<ComponentRef Id="edsm.py" />
<ComponentRef Id="es.strings" />
<ComponentRef Id="fi.strings" />
<ComponentRef Id="fr.strings" />

View File

@ -19,7 +19,7 @@
/* CQC rank. [stats.py] */
"Amateur" = "Amateur";
/* EDSM setting. [prefs.py] */
/* EDSM setting. [edsm.py] */
"API Key" = "API Key";
/* Tab heading in settings. [prefs.py] */
@ -70,7 +70,7 @@
/* Ranking. [stats.py] */
"Combat" = "Combat";
/* EDSM setting. [prefs.py] */
/* EDSM setting. [edsm.py] */
"Commander Name" = "Commander Name";
/* Combat rank. [stats.py] */
@ -139,7 +139,7 @@
/* Top rank. [stats.py] */
"Elite" = "Elite";
/* Section heading in settings. [prefs.py] */
/* Section heading in settings. [edsm.py] */
"Elite Dangerous Star Map credentials" = "Elite Dangerous Star Map credentials";
/* Ranking. [stats.py] */
@ -430,7 +430,7 @@
/* CQC rank. [stats.py] */
"Semi Professional" = "Semi Professional";
/* [prefs.py] */
/* [edsm.py] */
"Send flight log to Elite Dangerous Star Map" = "Send flight log to Elite Dangerous Star Map";
/* Output setting. [prefs.py] */
@ -475,7 +475,7 @@
/* Explorer rank. [stats.py] */
"Surveyor" = "Surveyor";
/* Main window. [EDMarketConnector.py] */
/* Main window. [edsm.py] */
"System" = "System";
/* Appearance setting. [prefs.py] */

View File

@ -102,7 +102,7 @@ class Config:
# OUT_STAT = 64 # No longer available
OUT_SHIP_CORIOLIS = 128 # Replaced by OUT_SHIP
OUT_STATION_ANY = OUT_MKT_EDDN|OUT_MKT_TD|OUT_MKT_CSV|OUT_SHIP|OUT_SHIP_EDS|OUT_SHIP_CORIOLIS
OUT_SYS_EDSM = 256
# OUT_SYS_EDSM = 256 # Now a plugin
# OUT_SYS_AUTO = 512 # Now always automatic
OUT_MKT_MANUAL = 1024
OUT_SYS_EDDN = 2048

233
edsm.py
View File

@ -1,233 +0,0 @@
import json
import threading
from sys import platform
import time
import urllib2
import Tkinter as tk
from config import appname, applongname, appversion, config
from monitor import monitor
if __debug__:
from traceback import print_exc
if platform=='darwin':
# mimimal implementation of requests interface since OpenSSL 0.9.8 on OSX
# fails to negotiate with Cloudflare unless cipher is forced
import ssl
class Response(object):
def __init__(self, url, status_code, headers, content):
self.url = url
self.status_code = status_code
self.headers = headers # actually a mimetools.Message instance
self.content = content
def json(self):
return json.loads(self.content)
def raise_for_status(self):
if self.status_code >= 400:
raise urllib2.HTTPError(self.url, self.status_code, '%d %s' % (self.status_code, self.status_code >= 500 and 'Server Error' or 'Client Error'), self.headers, None)
class Session(object):
def __init__(self):
self.headers = {}
sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLSv1) # Requires Python >= 2.7.9 on OSX >= 10.10
sslcontext.set_ciphers("ECCdraft:HIGH:!aNULL")
self.opener = urllib2.build_opener(urllib2.HTTPSHandler(context=sslcontext))
self.opener.addheaders = [('User-Agent', '%s/%s' % (appname, appversion))]
def get(self, url, timeout=None, headers={}):
try:
h = self.opener.open(url, timeout=timeout)
r = Response(h.geturl(), h.code, h.info(), h.read())
h.close()
return r
except urllib2.HTTPError, e:
return Response(url, e.code, {}, '') # requests doesn't raise exceptions for HTTP errors
except:
raise
def close(self):
pass
else:
from requests import Session
class EDSM:
_TIMEOUT = 10
FAKE = ['CQC', 'Training', 'Destination'] # Fake systems that shouldn't be sent to EDSM
def __init__(self):
self.result = { 'img': None, 'url': None, 'done': True }
self.syscache = set() # Cache URLs of systems with known coordinates
self.session = Session()
self.lastship = None # Description of last ship that we sent to EDSM
# Can't be in class definition since can only call PhotoImage after window is created
EDSM._IMG_KNOWN = tk.PhotoImage(data = 'R0lGODlhEAAQAMIEAFWjVVWkVWS/ZGfFZ////////////////yH5BAEKAAQALAAAAAAQABAAAAMvSLrc/lAFIUIkYOgNXt5g14Dk0AQlaC1CuglM6w7wgs7rMpvNV4q932VSuRiPjQQAOw==') # green circle
EDSM._IMG_UNKNOWN = tk.PhotoImage(data = 'R0lGODlhEAAQAKEDAGVLJ+ddWO5fW////yH5BAEKAAMALAAAAAAQABAAAAItnI+pywYRQBtA2CtVvTwjDgrJFlreEJRXgKSqwB5keQ6vOKq1E+7IE5kIh4kCADs=') # red circle
EDSM._IMG_NEW = tk.PhotoImage(data = 'R0lGODlhEAAQAMZwANKVHtWcIteiHuiqLPCuHOS1MN22ZeW7ROG6Zuu9MOy+K/i8Kf/DAuvCVf/FAP3BNf/JCf/KAPHHSv7ESObHdv/MBv/GRv/LGP/QBPXOPvjPQfjQSvbRSP/UGPLSae7Sfv/YNvLXgPbZhP7dU//iI//mAP/jH//kFv7fU//fV//ebv/iTf/iUv/kTf/iZ/vgiP/hc/vgjv/jbfriiPriiv7ka//if//jd//sJP/oT//tHv/mZv/sLf/rRP/oYv/rUv/paP/mhv/sS//oc//lkf/mif/sUf/uPv/qcv/uTv/uUv/vUP/qhP/xP//pm//ua//sf//ubf/wXv/thv/tif/slv/tjf/smf/yYP/ulf/2R//2Sv/xkP/2av/0gP/ylf/2df/0i//0j//0lP/5cP/7a//1p//5gf/7ev/3o//2sf/5mP/6kv/2vP/3y//+jP///////////////////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAQABAAAAePgH+Cg4SFhoJKPIeHYT+LhVppUTiPg2hrUkKPXWdlb2xHJk9jXoNJQDk9TVtkYCUkOy4wNjdGfy1UXGJYOksnPiwgFwwYg0NubWpmX1ArHREOFYUyWVNIVkxXQSoQhyMoNVUpRU5EixkcMzQaGy8xhwsKHiEfBQkSIg+GBAcUCIIBBDSYYGiAAUMALFR6FAgAOw==')
EDSM._IMG_ERROR = tk.PhotoImage(data = 'R0lGODlhEAAQAKEBAAAAAP///////////yH5BAEKAAIALAAAAAAQABAAAAIwlBWpeR0AIwwNPRmZuVNJinyWuClhBlZjpm5fqnIAHJPtOd3Hou9mL6NVgj2LplEAADs=') # BBC Mode 5 '?'
# Call an EDSM endpoint with args (which should be quoted)
def call(self, endpoint, args, check_msgnum=True):
try:
idx = config.get('cmdrs').index(monitor.cmdr)
url = 'https://www.edsm.net/%s?commanderName=%s&apiKey=%s&fromSoftware=%s&fromSoftwareVersion=%s' % (
endpoint,
urllib2.quote(config.get('edsm_usernames')[idx].encode('utf-8')),
urllib2.quote(config.get('edsm_apikeys')[idx]),
urllib2.quote(applongname),
urllib2.quote(appversion),
) + args
r = self.session.get(url, timeout=EDSM._TIMEOUT)
r.raise_for_status()
reply = r.json()
if not check_msgnum:
return reply
(msgnum, msg) = reply['msgnum'], reply['msg']
except:
if __debug__: print_exc()
raise Exception(_("Error: Can't connect to EDSM"))
# Message numbers: 1xx = OK, 2xx = fatal error, 3xx = error (but not generated in practice), 4xx = ignorable errors
if msgnum // 100 not in (1,4):
raise Exception(_('Error: EDSM {MSG}').format(MSG=msg))
else:
return reply
# Just set link without doing a lookup
def link(self, system_name):
self.cancel_lookup()
if not system_name or system_name in self.FAKE:
self.result = { 'img': '', 'url': None, 'done': True, 'uncharted': False }
else:
self.result = { 'img': '', 'url': 'https://www.edsm.net/show-system?systemName=%s' % urllib2.quote(system_name), 'done': True, 'uncharted': False }
def lookup(self, system_name, known=0):
self.cancel_lookup()
if system_name in self.FAKE:
self.result = { 'img': '', 'url': None, 'done': True, 'uncharted': False }
elif known or system_name in self.syscache:
self.result = { 'img': EDSM._IMG_KNOWN, 'url': 'https://www.edsm.net/show-system?systemName=%s' % urllib2.quote(system_name), 'done': True, 'uncharted': False }
else:
self.result = { 'img': EDSM._IMG_ERROR, 'url': 'https://www.edsm.net/show-system?systemName=%s' % urllib2.quote(system_name), 'done': True, 'uncharted': False }
data = self.call('api-v1/system', '&sysname=%s&coords=1' % urllib2.quote(system_name), check_msgnum=False)
if data == -1 or not data:
# System not present - but don't create it on the assumption that the caller will
self.result['img'] = EDSM._IMG_NEW
self.result['uncharted'] = True
elif data.get('coords'):
self.result['img'] = EDSM._IMG_KNOWN
self.syscache.add(system_name)
else:
self.result['img'] = EDSM._IMG_UNKNOWN
self.result['uncharted'] = True
# Asynchronous version of the above
def start_lookup(self, system_name, known=0):
self.cancel_lookup()
if system_name in self.FAKE:
self.result = { 'img': '', 'url': None, 'done': True, 'uncharted': False }
elif known or system_name in self.syscache:
self.result = { 'img': EDSM._IMG_KNOWN, 'url': 'https://www.edsm.net/show-system?systemName=%s' % urllib2.quote(system_name), 'done': True, 'uncharted': False }
else:
self.result = { 'img': '', 'url': 'https://www.edsm.net/show-system?systemName=%s' % urllib2.quote(system_name), 'done': False, 'uncharted': False }
self.thread = threading.Thread(target = self.worker, name = 'EDSM worker', args = (system_name, self.result))
self.thread.daemon = True
self.thread.start()
def cancel_lookup(self):
self.thread = None # orphan any existing thread
self.result = { 'img': '', 'url': None, 'done': True } # orphan existing thread's results
def worker(self, system_name, result):
try:
data = self.call('api-v1/system', '&sysname=%s&coords=1' % urllib2.quote(system_name), check_msgnum=False)
if data == -1 or not data:
# System not present - create it
result['img'] = EDSM._IMG_NEW
result['uncharted'] = True
elif data.get('coords'):
result['img'] = EDSM._IMG_KNOWN
self.syscache.add(system_name)
else:
result['img'] = EDSM._IMG_UNKNOWN
result['uncharted'] = True
except:
if __debug__: print_exc()
result['img'] = EDSM._IMG_ERROR
result['done'] = True
# Send flight log and also do lookup
def writelog(self, timestamp, system_name, coordinates, shipid = None):
if system_name in self.FAKE:
self.result = { 'img': '', 'url': None, 'done': True, 'uncharted': False }
return
self.result = { 'img': EDSM._IMG_ERROR, 'url': 'https://www.edsm.net/show-system?systemName=%s' % urllib2.quote(system_name), 'done': True, 'uncharted': False }
args = '&systemName=%s&dateVisited=%s' % (
urllib2.quote(system_name),
urllib2.quote(time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(timestamp))),
)
if coordinates:
args += '&x=%.3f&y=%.3f&z=%.3f' % coordinates
if shipid is not None:
args += '&shipId=%d' % shipid
reply = self.call('api-logs-v1/set-log', args)
if reply.get('systemCreated'):
self.result['img'] = EDSM._IMG_NEW
else:
self.result['img'] = EDSM._IMG_KNOWN
self.syscache.add(system_name)
def setranks(self, ranks):
args = ''
if ranks:
for k,v in ranks.iteritems():
if v is not None:
args += '&%s=%s' % (k, urllib2.quote('%d;%d' % v))
if args:
self.call('api-commander-v1/set-ranks', args)
def setcredits(self, balance, loan):
if balance is not None:
self.call('api-commander-v1/set-credits', '&balance=%d&loan=%d' % (balance, loan))
def setmaterials(self, raw, manufactured, encoded):
self.call('api-commander-v1/set-materials', "&type=data&values=%s" % json.dumps(encoded, separators = (',', ':')))
materials = {}
materials.update(raw)
materials.update(manufactured)
self.call('api-commander-v1/set-materials', "&type=materials&values=%s" % json.dumps(materials, separators = (',', ':')))
def setshipid(self, shipid):
if shipid is not None:
self.call('api-commander-v1/set-ship-id', '&shipId=%d' % shipid)
def updateship(self, shipid, shiptype, props=[]):
if shipid is not None and shiptype:
args = '&shipId=%d&type=%s' % (shipid, shiptype)
for (slot, thing) in props:
args += '&%s=%s' % (slot, urllib2.quote(unicode(thing)))
self.call('api-commander-v1/update-ship', args)
def sellship(self, shipid):
if shipid is not None:
self.call('api-commander-v1/sell-ship', '&shipId=%d' % shipid)

357
plugins/edsm.py Normal file
View File

@ -0,0 +1,357 @@
#
# System display and EDSM lookup
#
import json
import requests
import sys
import time
import urllib2
from calendar import timegm
import Tkinter as tk
from ttkHyperlinkLabel import HyperlinkLabel
import myNotebook as nb
from config import appname, applongname, appversion, config
import companion
import coriolis
import edshipyard
if __debug__:
from traceback import print_exc
EDSM_POLL = 0.1
_TIMEOUT = 10
FAKE = ['CQC', 'Training', 'Destination'] # Fake systems that shouldn't be sent to EDSM
this = sys.modules[__name__] # For holding module globals
this.syscache = set() # Cache URLs of systems with known coordinates
this.session = requests.Session()
this.lastship = None # Description of last ship that we sent to EDSM
this.lastlookup = False # whether the last lookup succeeded
# Game state
this.multicrew = False # don't send captain's ship info to EDSM while on a crew
def plugin_start():
# Can't be earlier since can only call PhotoImage after window is created
this._IMG_KNOWN = tk.PhotoImage(data = 'R0lGODlhEAAQAMIEAFWjVVWkVWS/ZGfFZ////////////////yH5BAEKAAQALAAAAAAQABAAAAMvSLrc/lAFIUIkYOgNXt5g14Dk0AQlaC1CuglM6w7wgs7rMpvNV4q932VSuRiPjQQAOw==') # green circle
this._IMG_UNKNOWN = tk.PhotoImage(data = 'R0lGODlhEAAQAKEDAGVLJ+ddWO5fW////yH5BAEKAAMALAAAAAAQABAAAAItnI+pywYRQBtA2CtVvTwjDgrJFlreEJRXgKSqwB5keQ6vOKq1E+7IE5kIh4kCADs=') # red circle
this._IMG_NEW = tk.PhotoImage(data = 'R0lGODlhEAAQAMZwANKVHtWcIteiHuiqLPCuHOS1MN22ZeW7ROG6Zuu9MOy+K/i8Kf/DAuvCVf/FAP3BNf/JCf/KAPHHSv7ESObHdv/MBv/GRv/LGP/QBPXOPvjPQfjQSvbRSP/UGPLSae7Sfv/YNvLXgPbZhP7dU//iI//mAP/jH//kFv7fU//fV//ebv/iTf/iUv/kTf/iZ/vgiP/hc/vgjv/jbfriiPriiv7ka//if//jd//sJP/oT//tHv/mZv/sLf/rRP/oYv/rUv/paP/mhv/sS//oc//lkf/mif/sUf/uPv/qcv/uTv/uUv/vUP/qhP/xP//pm//ua//sf//ubf/wXv/thv/tif/slv/tjf/smf/yYP/ulf/2R//2Sv/xkP/2av/0gP/ylf/2df/0i//0j//0lP/5cP/7a//1p//5gf/7ev/3o//2sf/5mP/6kv/2vP/3y//+jP///////////////////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAQABAAAAePgH+Cg4SFhoJKPIeHYT+LhVppUTiPg2hrUkKPXWdlb2xHJk9jXoNJQDk9TVtkYCUkOy4wNjdGfy1UXGJYOksnPiwgFwwYg0NubWpmX1ArHREOFYUyWVNIVkxXQSoQhyMoNVUpRU5EixkcMzQaGy8xhwsKHiEfBQkSIg+GBAcUCIIBBDSYYGiAAUMALFR6FAgAOw==')
this._IMG_ERROR = tk.PhotoImage(data = 'R0lGODlhEAAQAKEBAAAAAP///////////yH5BAEKAAIALAAAAAAQABAAAAIwlBWpeR0AIwwNPRmZuVNJinyWuClhBlZjpm5fqnIAHJPtOd3Hou9mL6NVgj2LplEAADs=') # BBC Mode 5 '?'
# Migrate old settings
if not config.get('edsm_cmdrs'):
if isinstance(config.get('cmdrs'), list) and config.get('edsm_usernames') and config.get('edsm_apikeys'):
# Migrate <= 2.34 settings
config.set('edsm_cmdrs', config.get('cmdrs'))
elif config.get('edsm_cmdrname'):
# Migrate <= 2.25 settings. edsm_cmdrs is unknown at this time
config.set('edsm_usernames', [config.get('edsm_cmdrname') or ''])
config.set('edsm_apikeys', [config.get('edsm_apikey') or ''])
config.delete('edsm_cmdrname')
config.delete('edsm_apikey')
if config.getint('output') & 256:
# Migrate <= 2.34 setting
config.set('edsm_out', 1)
config.delete('edsm_autoopen')
config.delete('edsm_historical')
return 'EDSM'
def plugin_app(parent):
this.system_label = tk.Label(parent, text = _('System') + ':') # Main window
this.system = HyperlinkLabel(parent, compound=tk.RIGHT, url = None, popup_copy = True)
return (this.system_label, this.system)
def plugin_prefs(parent, cmdr, is_beta):
PADX = 10
BUTTONX = 12 # indent Checkbuttons and Radiobuttons
PADY = 2 # close spacing
frame = nb.Frame(parent)
frame.columnconfigure(1, weight=1)
HyperlinkLabel(frame, text='Elite Dangerous Star Map', background=nb.Label().cget('background'), url='https://www.edsm.net/', underline=True).grid(columnspan=2, padx=PADX, sticky=tk.W) # Don't translate
this.log = tk.IntVar(value = config.getint('edsm_out') and 1)
this.log_button = nb.Checkbutton(frame, text=_('Send flight log to Elite Dangerous Star Map'), variable=this.log, command=prefsvarchanged)
this.log_button.grid(columnspan=2, padx=BUTTONX, pady=(5,0), sticky=tk.W)
nb.Label(frame).grid(sticky=tk.W) # big spacer
this.label = HyperlinkLabel(frame, text=_('Elite Dangerous Star Map credentials'), background=nb.Label().cget('background'), url='https://www.edsm.net/settings/api', underline=True) # Section heading in settings
this.label.grid(columnspan=2, padx=PADX, sticky=tk.W)
this.cmdr_label = nb.Label(frame, text=_('Cmdr')) # Main window
this.cmdr_label.grid(row=10, padx=PADX, sticky=tk.W)
this.cmdr_text = nb.Label(frame)
this.cmdr_text.grid(row=10, column=1, padx=PADX, pady=PADY, sticky=tk.W)
this.user_label = nb.Label(frame, text=_('Commander Name')) # EDSM setting
this.user_label.grid(row=11, padx=PADX, sticky=tk.W)
this.user = nb.Entry(frame)
this.user.grid(row=11, column=1, padx=PADX, pady=PADY, sticky=tk.EW)
this.apikey_label = nb.Label(frame, text=_('API Key')) # EDSM setting
this.apikey_label.grid(row=12, padx=PADX, sticky=tk.W)
this.apikey = nb.Entry(frame)
this.apikey.grid(row=12, column=1, padx=PADX, pady=PADY, sticky=tk.EW)
prefs_cmdr_changed(cmdr, is_beta)
return frame
def prefs_cmdr_changed(cmdr, is_beta):
this.log_button['state'] = cmdr and not is_beta and tk.NORMAL or tk.DISABLED
this.user['state'] = tk.NORMAL
this.user.delete(0, tk.END)
this.apikey['state'] = tk.NORMAL
this.apikey.delete(0, tk.END)
if cmdr:
this.cmdr_text['text'] = cmdr + (is_beta and ' [Beta]' or '')
cred = credentials(cmdr)
if cred:
this.user.insert(0, cred[0])
this.apikey.insert(0, cred[1])
else:
this.cmdr_text['text'] = _('None') # No hotkey/shortcut currently defined
this.label['state'] = this.cmdr_label['state'] = this.cmdr_text['state'] = this.user_label['state'] = this.user['state'] = this.apikey_label['state'] = this.apikey['state'] = cmdr and not is_beta and this.log.get() and tk.NORMAL or tk.DISABLED
def prefsvarchanged():
this.user_label['state'] = this.user['state'] = this.apikey_label['state'] = this.log.get() and tk.NORMAL or tk.DISABLED
def prefs_changed(cmdr, is_beta):
this.system_label['text'] = _('System') + ':' # Main window
config.set('edsm_out', this.log.get())
if cmdr and not is_beta:
cmdrs = config.get('edsm_cmdrs')
usernames = config.get('edsm_usernames')
apikeys = config.get('edsm_apikeys')
if cmdr in cmdrs:
idx = cmdrs.index(cmdr)
usernames[idx] = this.user.get().strip()
apikeys[idx] = this.apikey.get().strip()
else:
config.set('edsm_cmdrs', cmdrs + [cmdr])
usernames.append(this.user.get().strip())
apikeys.append(this.apikey.get().strip())
config.set('edsm_usernames', usernames)
config.set('edsm_apikeys', apikeys)
def credentials(cmdr):
# Credentials for cmdr
if not cmdr:
return None
cmdrs = config.get('edsm_cmdrs')
if not cmdrs:
# Migrate from <= 2.25
cmdrs = [cmdr]
config.set('edsm_cmdrs', cmdrs)
if cmdr in cmdrs:
idx = cmdrs.index(cmdr)
return (config.get('edsm_usernames')[idx], config.get('edsm_apikeys')[idx])
else:
return None
def journal_entry(cmdr, is_beta, system, station, entry, state):
# Update display
if this.system['text'] != system:
this.system['text'] = system or ''
this.system['image'] = ''
if not system or system in FAKE:
this.system['url'] = None
this.lastlookup = True
else:
this.system['url'] = 'https://www.edsm.net/show-system?systemName=%s' % urllib2.quote(system)
this.lastlookup = False
this.system.update_idletasks()
this.multicrew = bool(state['Role'])
# Send interesting events to EDSM
if config.getint('edsm_out') and not is_beta and not multicrew and credentials(cmdr):
try:
# Send credits to EDSM on new game (but not on startup - data might be old)
if entry['event'] == 'LoadGame':
setcredits(cmdr, state['Credits'], state['Loan'])
# Send rank info to EDSM on startup or change
if entry['event'] in ['StartUp', 'Progress', 'Promotion'] and state['Rank']:
setranks(cmdr, state['Rank'])
# Send ship info to EDSM on startup or change
if entry['event'] in ['StartUp', 'Loadout', 'LoadGame', 'SetUserShipName'] and cmdr and state['ShipID']:
setshipid(cmdr, state['ShipID'])
props = []
if state['ShipIdent'] is not None:
props.append(('shipIdent', state['ShipIdent']))
if state['ShipName'] is not None:
props.append(('shipName', state['ShipName']))
if state['PaintJob'] is not None:
props.append(('paintJob', state['PaintJob']))
updateship(cmdr, state['ShipID'], state['ShipType'], props)
elif entry['event'] in ['ShipyardBuy', 'ShipyardSell']:
sellship(cmdr, entry.get('SellShipID'))
# Send materials info to EDSM on startup or change
if entry['event'] in ['StartUp', 'LoadGame', 'MaterialCollected', 'MaterialDiscarded', 'ScientificResearch', 'EngineerCraft', 'Synthesis']:
setmaterials(cmdr, state['Raw'], state['Manufactured'], state['Encoded'])
# Send paintjob info to EDSM on change
if entry['event'] in ['ModuleBuy', 'ModuleSell'] and entry['Slot'] == 'PaintJob':
updateship(cmdr, state['ShipID'], state['ShipType'], [('paintJob', state['PaintJob'])])
# Write EDSM log on startup and change
if system and entry['event'] in ['Location', 'FSDJump']:
this.lastlookup = False
writelog(cmdr, timegm(time.strptime(entry['timestamp'], '%Y-%m-%dT%H:%M:%SZ')), system, 'StarPos' in entry and tuple(entry['StarPos']), state['ShipID'])
this.lastlookup = True
this.system.update_idletasks()
except Exception as e:
if __debug__: print_exc()
return unicode(e)
def cmdr_data(data, is_beta):
system = data['lastSystem']['name']
if not this.system['text']:
this.system['text'] = system
this.system['image'] = ''
if not system or system in FAKE:
this.system['url'] = None
this.lastlookup = True
else:
this.system['url'] = 'https://www.edsm.net/show-system?systemName=%s' % urllib2.quote(system)
this.lastlookup = False
this.system.update_idletasks()
if config.getint('edsm_out') and not is_beta and not multicrew and credentials(data['commander']['name']):
# Send flightlog to EDSM if FSDJump failed to do so
if not this.lastlookup:
try:
this.lastlookup = False
this.writelog(data['commander']['name'], int(time.time()), system, None, data['ship']['id'])
this.lastlookup = True
this.system.update_idletasks()
except Exception as e:
if __debug__: print_exc()
return unicode(e)
# Update credits and ship info and send to EDSM
try:
if data['commander'].get('credits') is not None:
setcredits(data['commander']['name'], data['commander']['credits'], data['commander'].get('debt', 0))
ship = companion.ship(data)
if ship != this.lastship:
updateship(data['commander']['name'],
data['ship']['id'],
data['ship']['name'].lower(),
[
('linkToCoriolis', coriolis.url(data, is_beta)),
('linkToEDShipyard', edshipyard.url(data, is_beta)),
])
this.lastship = ship
except Exception as e:
# Not particularly important so silent on failure
if __debug__: print_exc()
# Call an EDSM endpoint with args (which should be quoted)
def call(cmdr, endpoint, args, check_msgnum=True):
try:
(username, apikey) = credentials(cmdr)
url = 'https://www.edsm.net/%s?commanderName=%s&apiKey=%s&fromSoftware=%s&fromSoftwareVersion=%s' % (
endpoint,
urllib2.quote(username.encode('utf-8')),
urllib2.quote(apikey),
urllib2.quote(applongname),
urllib2.quote(appversion),
) + args
r = this.session.get(url, timeout=_TIMEOUT)
r.raise_for_status()
reply = r.json()
if not check_msgnum:
return reply
(msgnum, msg) = reply['msgnum'], reply['msg']
except:
if __debug__: print_exc()
raise Exception(_("Error: Can't connect to EDSM"))
# Message numbers: 1xx = OK, 2xx = fatal error, 3xx = error (but not generated in practice), 4xx = ignorable errors
if msgnum // 100 not in (1,4):
raise Exception(_('Error: EDSM {MSG}').format(MSG=msg))
else:
return reply
# Send flight log and also do lookup
def writelog(cmdr, timestamp, system_name, coordinates, shipid = None):
if system_name in FAKE:
return
args = '&systemName=%s&dateVisited=%s' % (
urllib2.quote(system_name),
urllib2.quote(time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(timestamp))),
)
if coordinates:
args += '&x=%.3f&y=%.3f&z=%.3f' % coordinates
if shipid is not None:
args += '&shipId=%d' % shipid
try:
reply = call(cmdr, 'api-logs-v1/set-log', args)
if reply.get('systemCreated'):
this.system['image'] = this._IMG_NEW
else:
this.system['image'] = this._IMG_KNOWN
this.syscache.add(system_name)
except:
this.system['image'] = this._IMG_ERROR
raise
def setranks(cmdr, ranks):
args = ''
if ranks:
for k,v in ranks.iteritems():
if v is not None:
args += '&%s=%s' % (k, urllib2.quote('%d;%d' % v))
if args:
call(cmdr, 'api-commander-v1/set-ranks', args)
def setcredits(cmdr, balance, loan):
if balance is not None:
call(cmdr, 'api-commander-v1/set-credits', '&balance=%d&loan=%d' % (balance, loan))
def setmaterials(cmdr, raw, manufactured, encoded):
call(cmdr, 'api-commander-v1/set-materials', "&type=data&values=%s" % json.dumps(encoded, separators = (',', ':')))
materials = {}
materials.update(raw)
materials.update(manufactured)
call(cmdr, 'api-commander-v1/set-materials', "&type=materials&values=%s" % json.dumps(materials, separators = (',', ':')))
def setshipid(cmdr, shipid):
if shipid is not None:
call(cmdr, 'api-commander-v1/set-ship-id', '&shipId=%d' % shipid)
def updateship(cmdr, shipid, shiptype, props=[]):
if shipid is not None and shiptype:
args = '&shipId=%d&type=%s' % (shipid, shiptype)
for (slot, thing) in props:
args += '&%s=%s' % (slot, urllib2.quote(unicode(thing)))
call(cmdr, 'api-commander-v1/update-ship', args)
def sellship(cmdr, shipid):
if shipid is not None:
call(cmdr, 'api-commander-v1/sell-ship', '&shipId=%d' % shipid)

View File

@ -182,36 +182,6 @@ class PreferencesDialog(tk.Toplevel):
notebook.add(eddnframe, text='EDDN') # Not translated
edsmframe = nb.Frame(notebook)
edsmframe.columnconfigure(1, weight=1)
HyperlinkLabel(edsmframe, text='Elite Dangerous Star Map', background=nb.Label().cget('background'), url='https://www.edsm.net/', underline=True).grid(columnspan=2, padx=PADX, sticky=tk.W) # Don't translate
self.edsm_log = tk.IntVar(value = (output & config.OUT_SYS_EDSM) and 1)
self.edsm_log_button = nb.Checkbutton(edsmframe, text=_('Send flight log to Elite Dangerous Star Map'), variable=self.edsm_log, command=self.outvarchanged)
self.edsm_log_button.grid(columnspan=2, padx=BUTTONX, pady=(5,0), sticky=tk.W)
nb.Label(edsmframe).grid(sticky=tk.W) # big spacer
self.edsm_label = HyperlinkLabel(edsmframe, text=_('Elite Dangerous Star Map credentials'), background=nb.Label().cget('background'), url='https://www.edsm.net/settings/api', underline=True) # Section heading in settings
self.edsm_label.grid(columnspan=2, padx=PADX, sticky=tk.W)
self.edsm_cmdr_label = nb.Label(edsmframe, text=_('Cmdr')) # Main window
self.edsm_cmdr_label.grid(row=10, padx=PADX, sticky=tk.W)
self.edsm_cmdr_text = nb.Label(edsmframe)
self.edsm_cmdr_text.grid(row=10, column=1, padx=PADX, pady=PADY, sticky=tk.W)
self.edsm_user_label = nb.Label(edsmframe, text=_('Commander Name')) # EDSM setting
self.edsm_user_label.grid(row=11, padx=PADX, sticky=tk.W)
self.edsm_user = nb.Entry(edsmframe)
self.edsm_user.grid(row=11, column=1, padx=PADX, pady=PADY, sticky=tk.EW)
self.edsm_apikey_label = nb.Label(edsmframe, text=_('API Key')) # EDSM setting
self.edsm_apikey_label.grid(row=12, padx=PADX, sticky=tk.W)
self.edsm_apikey = nb.Entry(edsmframe)
self.edsm_apikey.grid(row=12, column=1, padx=PADX, pady=PADY, sticky=tk.EW)
notebook.add(edsmframe, text='EDSM') # Not translated
# build plugin prefs tabs
for plugin in plug.PLUGINS:
plugframe = plugin.get_prefs(notebook, monitor.cmdr, monitor.is_beta)
@ -406,22 +376,14 @@ class PreferencesDialog(tk.Toplevel):
self.username.delete(0, tk.END)
self.password['state'] = tk.NORMAL
self.password.delete(0, tk.END)
self.edsm_user['state'] = tk.NORMAL
self.edsm_user.delete(0, tk.END)
self.edsm_apikey['state'] = tk.NORMAL
self.edsm_apikey.delete(0, tk.END)
if monitor.cmdr and config.get('cmdrs') and monitor.cmdr in config.get('cmdrs'):
config_idx = config.get('cmdrs').index(monitor.cmdr)
self.username.insert(0, config.get('fdev_usernames')[config_idx] or '')
self.password.insert(0, config.get_password(config.get('fdev_usernames')[config_idx]) or '')
self.edsm_user.insert(0, config.get('edsm_usernames')[config_idx] or '')
self.edsm_apikey.insert(0, config.get('edsm_apikeys')[config_idx] or '')
elif monitor.cmdr and not config.get('cmdrs') and config.get('username') and config.get('password'):
# migration from <= 2.25
self.username.insert(0, config.get('username') or '')
self.password.insert(0, config.get('password') or '')
self.edsm_user.insert(0,config.get('edsm_cmdrname') or '')
self.edsm_apikey.insert(0, config.get('edsm_apikey') or '')
if self.cmdr is not False: # Don't notify on first run
plug.notify_prefs_cmdr_changed(monitor.cmdr, monitor.is_beta)
self.cmdr = monitor.cmdr
@ -452,11 +414,6 @@ class PreferencesDialog(tk.Toplevel):
self.eddn_system_button['state']= logvalid and tk.NORMAL or tk.DISABLED
self.eddn_delay_button['state'] = logvalid and eddn.replayfile and self.eddn_system.get() and tk.NORMAL or tk.DISABLED
self.edsm_log_button['state'] = logvalid and tk.NORMAL or tk.DISABLED
edsm_state = logvalid and monitor.cmdr and self.edsm_log.get() and tk.NORMAL or tk.DISABLED
self.edsm_label['state'] = self.edsm_cmdr_label['state'] = self.edsm_user_label['state'] = self.edsm_apikey_label['state'] = edsm_state
self.edsm_cmdr_text['state'] = self.edsm_user['state'] = self.edsm_apikey['state'] = edsm_state
def filebrowse(self, title, pathvar):
if platform != 'win32':
import tkFileDialog
@ -598,14 +555,10 @@ class PreferencesDialog(tk.Toplevel):
if not config.get('cmdrs'):
config.set('cmdrs', [self.cmdr])
config.set('fdev_usernames', [self.username.get().strip()])
config.set('edsm_usernames', [self.edsm_user.get().strip()])
config.set('edsm_apikeys', [self.edsm_apikey.get().strip()])
else:
idx = config.get('cmdrs').index(self.cmdr) if self.cmdr in config.get('cmdrs') else -1
_putfirst('cmdrs', idx, self.cmdr)
_putfirst('fdev_usernames', idx, self.username.get().strip())
_putfirst('edsm_usernames', idx, self.edsm_user.get().strip())
_putfirst('edsm_apikeys', idx, self.edsm_apikey.get().strip())
config.set('output',
(self.out_td.get() and config.OUT_MKT_TD) +
@ -614,8 +567,7 @@ class PreferencesDialog(tk.Toplevel):
(self.out_ship.get() and config.OUT_SHIP) +
(self.eddn_station.get() and config.OUT_MKT_EDDN) +
(self.eddn_system.get() and config.OUT_SYS_EDDN) +
(self.eddn_delay.get() and config.OUT_SYS_DELAY) +
(self.edsm_log.get() and config.OUT_SYS_EDSM))
(self.eddn_delay.get() and config.OUT_SYS_DELAY))
config.set('outdir', self.outdir.get().startswith('~') and join(config.home, self.outdir.get()[2:]) or self.outdir.get())
logdir = self.logdir.get()
@ -764,12 +716,8 @@ def migrate(current_cmdr):
config.set_password(config.get('username'), config.get('password')) # Can fail on Linux
config.set('cmdrs', [current_cmdr])
config.set('fdev_usernames', [config.get('username')])
config.set('edsm_usernames', [config.get('edsm_cmdrname') or ''])
config.set('edsm_apikeys', [config.get('edsm_apikey') or ''])
config.delete('username')
config.delete('password')
config.delete('edsm_cmdrname')
config.delete('edsm_apikey')
# Put current Cmdr first in the lists
def make_current(current_cmdr):
@ -777,8 +725,6 @@ def make_current(current_cmdr):
idx = config.get('cmdrs').index(current_cmdr)
_putfirst('cmdrs', idx)
_putfirst('fdev_usernames', idx)
_putfirst('edsm_usernames', idx)
_putfirst('edsm_apikeys', idx)
def _putfirst(setting, config_idx, new_value=None):
assert config_idx>=0 or new_value is not None, (setting, config_idx, new_value)

View File

@ -65,7 +65,7 @@ if sys.platform=='darwin':
APP = 'EDMarketConnector.py'
APPCMD = 'EDMC.py'
SHORTVERSION = ''.join(VERSION.split('.')[:3])
PLUGINS = [ 'plugins/eddb.py' ]
PLUGINS = [ 'plugins/eddb.py', 'plugins/edsm.py' ]
if sys.platform=='darwin':
OPTIONS = { 'py2app':