# # 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, 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)