From 0ec2a532a0fc93f98ef2e5db835278d17021dcda Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Mon, 22 Aug 2016 18:26:43 +0100 Subject: [PATCH 01/21] Remove code to enable VerboseLogging again No longer required with Journal --- monitor.py | 93 +----------------------------------------------------- 1 file changed, 1 insertion(+), 92 deletions(-) diff --git a/monitor.py b/monitor.py index 55db5d7c..daa8022f 100644 --- a/monitor.py +++ b/monitor.py @@ -73,50 +73,6 @@ class EDLogs(FileSystemEventHandler): self.callbacks = { 'Jump': None, 'Dock': None } self.last_event = None # for communicating the Jump event - def logging_enabled_in_file(self, appconf): - if not isfile(appconf): - return False - - with open(appconf, 'rU') as f: - content = f.read().lower() - start = content.find('') - if start >= 0 and end >= 0: - return bool(re.search('verboselogging\s*=\s*\"1\"', content[start+8:end])) - else: - return False - - def enable_logging_in_file(self, appconf): - try: - if not exists(appconf): - with open(appconf, 'wt') as f: - f.write('\n\t\n\t\n\n') - return True - - with open(appconf, 'rU') as f: - content = f.read() - f.close() - backup = appconf[:-4] + '_backup.xml' - if exists(backup): - unlink(backup) - rename(appconf, backup) - - with open(appconf, 'wt') as f: - start = content.lower().find('= 0: - f.write(content[:start+8] + '\n\t\tVerboseLogging="1"' + content[start+8:]) - else: - start = content.lower().find("") - if start >= 0: - f.write(content[:start] + '\t\n\t\n' + content[start:]) - else: - f.write(content) # eh ? - return False - - return self.logging_enabled_in_file(appconf) - except: - if __debug__: print_exc() - return False def set_callback(self, name, callback): if name in self.callbacks: @@ -133,10 +89,6 @@ class EDLogs(FileSystemEventHandler): self.stop() self.currentdir = logdir - if not self._logging_enabled(self.currentdir): - # verbose logging reduces likelihood that Docked/Undocked messages will be delayed - self._enable_logging(self.currentdir) - self.root.bind_all('<>', self.jump) # user-generated self.root.bind_all('<>', self.dock) # user-generated @@ -321,18 +273,6 @@ class EDLogs(FileSystemEventHandler): # Apple's SMB implementation is too flaky so assume target machine is OSX return path and isdir(path) and isfile(join(path, pardir, 'AppNetCfg.xml')) - def _logging_enabled(self, path): - if not self._is_valid_logdir(path): - return False - else: - return self.logging_enabled_in_file(join(path, pardir, 'AppConfigLocal.xml')) - - def _enable_logging(self, path): - if not self._is_valid_logdir(path): - return False - else: - return self.enable_logging_in_file(join(path, pardir, 'AppConfigLocal.xml')) - elif platform=='win32': @@ -413,47 +353,16 @@ class EDLogs(FileSystemEventHandler): # Assume target machine is Windows return path and isdir(path) and isfile(join(path, pardir, 'AppConfig.xml')) - def _logging_enabled(self, path): - if not self._is_valid_logdir(path): - return False - else: - return (self.logging_enabled_in_file(join(path, pardir, 'AppConfigLocal.xml')) or - self.logging_enabled_in_file(join(path, pardir, 'AppConfig.xml'))) - - def _enable_logging(self, path): - if not self._is_valid_logdir(path): - return False - else: - return self.enable_logging_in_file(isfile(join(path, pardir, 'AppConfigLocal.xml')) and - join(path, pardir, 'AppConfigLocal.xml') or - join(path, pardir, 'AppConfig.xml')) - elif platform=='linux2': def _logdir(self): return None - # Assume target machine is Windows - def _is_valid_logdir(self, path): + # Assume target machine is Windows return path and isdir(path) and isfile(join(path, pardir, 'AppConfig.xml')) - def _logging_enabled(self, path): - if not self._is_valid_logdir(path): - return False - else: - return (self.logging_enabled_in_file(join(path, pardir, 'AppConfigLocal.xml')) or - self.logging_enabled_in_file(join(path, pardir, 'AppConfig.xml'))) - - def _enable_logging(self, path): - if not self._is_valid_logdir(path): - return False - else: - return self.enable_logging_in_file(isfile(join(path, pardir, 'AppConfigLocal.xml')) and - join(path, pardir, 'AppConfigLocal.xml') or - join(path, pardir, 'AppConfig.xml')) - # singleton monitor = EDLogs() From 4583f0e316cedad1928509ac383e2bf2b31d196d Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Mon, 22 Aug 2016 18:40:40 +0100 Subject: [PATCH 02/21] Monitor the journal file and send events to EDDN Sends 'FSDJump', 'Docked' and 'Scan' events in draft http://schemas.elite-markets.net/eddn/journal/1 format. --- EDMarketConnector.py | 94 +++++++++++++++++++--------- PLUGINS.md | 18 +++--- README.md | 18 ++++-- config.py | 1 + eddn.py | 9 ++- monitor.py | 140 +++++++++++++++++++----------------------- plug.py | 21 +++++++ plugins/About/load.py | 21 ++++--- prefs.py | 31 +++++++--- 9 files changed, 215 insertions(+), 138 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 59b6e7bd..7a5ce618 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -9,7 +9,8 @@ from os import mkdir from os.path import expanduser, isdir, join import re import requests -from time import time, localtime, strftime +from time import time, localtime, strftime, strptime +from calendar import timegm import Tkinter as tk import ttk @@ -75,9 +76,6 @@ class AppWindow: self.w.rowconfigure(0, weight=1) self.w.columnconfigure(0, weight=1) - # Special handling for overrideredict - self.w.bind("", self.onmap) - plug.load_plugins() if platform != 'darwin': @@ -242,6 +240,9 @@ class AppWindow: theme.register_highlight(self.station) theme.apply(self.w) + # Special handling for overrideredict + self.w.bind("", self.onmap) + # Load updater after UI creation (for WinSparkle) import update self.updater = update.Updater(self.w) @@ -252,8 +253,7 @@ class AppWindow: hotkeymgr.register(self.w, config.getint('hotkey_code'), config.getint('hotkey_mods')) # Install log monitoring - monitor.set_callback('Dock', self.getandsend) - monitor.set_callback('Jump', self.system_change) + self.w.bind_all('<>', self.journal_event) # user-generated monitor.start(self.w) # First run @@ -394,9 +394,14 @@ class AppWindow: pass elif not data['commander'].get('docked'): - # signal as error because the user might actually be docked but the server hosting the Companion API hasn't caught up - if not self.status['text']: - self.status['text'] = _("You're not docked at a station!") + if not event and not retrying: + # Silently retry if we got here by 'Automatically update on docking' and the server hasn't caught up + self.w.after(int(SERVER_RETRY * 1000), lambda:self.getandsend(event, True)) + return # early exit to avoid starting cooldown count + else: + # Signal as error because the user might actually be docked but the server hosting the Companion API hasn't caught up + if not self.status['text']: + self.status['text'] = _("You're not docked at a station!") else: # Finally - the data looks sane and we're docked at a station @@ -479,31 +484,62 @@ class AppWindow: except: pass - def system_change(self, event, timestamp, system, coordinates): + # Handle event(s) from the journal + def journal_event(self, event): + while True: + entry = monitor.get_entry() + if entry is None: + return + system_changed = monitor.system and self.system['text'] != monitor.system + station_changed = monitor.station and self.station['text'] != monitor.station - if self.system['text'] != system: - self.system['text'] = system + # Update main window + self.cmdr['text'] = monitor.cmdr or '' + self.system['text'] = monitor.system or '' + self.station['text'] = monitor.station or (EDDB.system(monitor.system) and self.STATION_UNDOCKED or '') - self.system['image'] = '' - self.station['text'] = EDDB.system(system) and self.STATION_UNDOCKED or '' + plug.notify_journal_entry(monitor.cmdr, monitor.system, monitor.station, entry) - plug.notify_system_changed(timestamp, system, coordinates) + if system_changed: + self.system['image'] = '' + timestamp = timegm(strptime(entry['timestamp'], '%Y-%m-%dT%H:%M:%SZ')) - if config.getint('output') & config.OUT_SYS_EDSM: - try: - self.status['text'] = _('Sending data to EDSM...') - self.w.update_idletasks() - self.edsm.writelog(timestamp, system, coordinates) # Do EDSM lookup during EDSM export + # Backwards compatibility + plug.notify_system_changed(timestamp, monitor.system, monitor.coordinates) + + # Update EDSM if we have coordinates - i.e. Location or FSDJump events + if config.getint('output') & config.OUT_SYS_EDSM and monitor.coordinates: + try: + self.status['text'] = _('Sending data to EDSM...') + self.w.update_idletasks() + self.edsm.writelog(timestamp, monitor.system, monitor.coordinates) + self.status['text'] = strftime(_('Last updated at {HH}:{MM}:{SS}').format(HH='%H', MM='%M', SS='%S').encode('utf-8'), localtime(timestamp)).decode('utf-8') + except Exception as e: + if __debug__: print_exc() + self.status['text'] = unicode(e) + if not config.getint('hotkey_mute'): + hotkeymgr.play_bad() + else: + self.edsm.link(monitor.system) self.status['text'] = strftime(_('Last updated at {HH}:{MM}:{SS}').format(HH='%H', MM='%M', SS='%S').encode('utf-8'), localtime(timestamp)).decode('utf-8') - except Exception as e: - if __debug__: print_exc() - self.status['text'] = unicode(e) - if not config.getint('hotkey_mute'): - hotkeymgr.play_bad() - else: - self.edsm.link(system) - self.status['text'] = strftime(_('Last updated at {HH}:{MM}:{SS}').format(HH='%H', MM='%M', SS='%S').encode('utf-8'), localtime(timestamp)).decode('utf-8') - self.edsmpoll() + self.edsmpoll() + + # Send interesting events to EDDN + if (config.getint('output') & config.OUT_SYS_EDDN and monitor.cmdr and + (entry['event'] == 'FSDJump' and system_changed or + entry['event'] == 'Docked' and station_changed or + entry['event'] == 'Scan')): + # strip out properties disallowed by the schema + for thing in ['CockpitBreach', 'BoostUsed', 'FuelLevel', 'FuelUsed', 'JumpDist']: + entry.pop(thing, None) + for thing in entry.keys(): + if thing.endswith('_Localised'): + entry.pop(thing, None) + eddn.export_journal_entry(monitor.cmdr, monitor.is_beta, entry) + + # Auto-Update after docking + if station_changed and config.getint('output') & (config.OUT_MKT_EDDN|config.OUT_MKT_MANUAL) == config.OUT_MKT_EDDN and entry['event'] == 'Docked': + self.w.after(int(SERVER_RETRY * 1000), self.getandsend) def edsmpoll(self): result = self.edsm.result diff --git a/PLUGINS.md b/PLUGINS.md index fe59cf4b..2a26117b 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -68,21 +68,23 @@ Once you have created your plugin and EDMC has loaded it there are two other fun Your events all get called on the main tkinter loop so be sure not to block for very long or the EDMC will appear to freeze. If you have a long running operation then you should take a look at how to do background updates in tkinter - http://effbot.org/zone/tkinter-threads.htm -### Arriving in a System +### Journal Entry -This gets called when EDMC uses the netlog to notice that you have arrived at a new star system. +This gets called when EDMC sees a new entry in the game's journal. ``` -def system_changed(timestamp, system, coordinates): - """ - We arrived at a new system! - """ - sys.stderr.write("{} {}".format(timestamp, system)) +def journal_entry(cmdr, system, station, entry): + if entry['event'] == 'FSDJump': + # We arrived at a new system! + if 'StarPos' in entry: + sys.stderr.write("Arrived at {} ({},{},{})\n".format(entry['StarSystem'], *tuple(entry['StarPos']))) + else: + sys.stderr.write("Arrived at {}\n".format(entry['StarSystem'])) ``` ### Getting Commander Data -This gets called when EDMC has just fetched fresh data from Frontier's servers. +This gets called when EDMC has just fetched fresh Cmdr and station data from Frontier's servers. ``` def cmdr_data(data): diff --git a/README.md b/README.md index 49430c04..c517dbd7 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ Elite: Dangerous Market Connector (EDMC) ======== -This app downloads your data and station data from the game [Elite: Dangerous](https://www.elitedangerous.com/) and, at your choice, either: +This app downloads your Cmdr's data, system, scan and station data from the game [Elite: Dangerous](https://www.elitedangerous.com/) and, at your choice, either: -* sends the station commodity market prices and other station 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 and shopping tools such as [eddb](http://eddb.io/), [Elite Trade Net](http://etn.io/), [Inara](http://inara.cz), [ED-TD](http://ed-td.space/), [Thrudd's Trading Tools](http://www.elitetradingtool.co.uk/), [Roguey's](http://roguey.co.uk/elite-dangerous/), etc. -* saves the station market prices 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. +* sends the station commodity market prices, other station data and system and scan 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, prospecting and shopping tools such as [eddb](http://eddb.io/), [Elite Trade Net](http://etn.io/), [Inara](http://inara.cz), [ED-TD](http://ed-td.space/), [Thrudd's Trading Tools](http://www.elitetradingtool.co.uk/), [Roguey's](http://roguey.co.uk/elite-dangerous/), etc. +* saves the station commodity market prices 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 to files on your computer that you can load into outfitting tools such as [E:D Shipyard](http://www.edshipyard.com), [Coriolis](http://coriolis.io) or [Elite Trade Net](http://etn.io/). * sends your flight log to [Elite: Dangerous Star Map](http://www.edsm.net/). @@ -46,7 +46,7 @@ Setup The first time that you run the app you are prompted for your username and password. This is the same username and password combination that you use to log into the Elite: Dangerous launcher, and is required so that the Frontier servers can send the app *your* data and the data for the station that *you* are docked at. -You can also choose here what data to save (refer to the next section for details), whether to “Update” automatically on docking and/or with a hotkey, and whether to attach your Cmdr name or a [pseudo-anonymized](http://en.wikipedia.org/wiki/Pseudonymity) ID to the data. +You can also choose here what data to save (refer to the next section for details), whether to “Update” Cmdr and station data automatically on docking and/or with a hotkey, and whether to attach your Cmdr name or a [pseudo-anonymized](http://en.wikipedia.org/wiki/Pseudonymity) ID to the data. You will be prompted to authenticate with a “verification code”, which you will shortly receive by email from Frontier. Note that each “verification code” is one-time only - if you enter the code incorrectly or quit the app before @@ -59,8 +59,7 @@ option EDMarketConnector → Preferences (Mac) or File → Settings (Windows) an This app can save a variety of data in a variety of formats: -* Station data - * Elite Dangerous Data Network - sends station commodity market, outfitting and shipyard data to “[EDDN](http://eddn-gateway.elite-markets.net/)” 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/), [Thrudd's Trading Tools](http://www.elitetradingtool.co.uk/), [Roguey's](http://roguey.co.uk/elite-dangerous/), etc. +* Market data * Slopey's BPC format file - saves commodity market data as files that you can load into [Slopey's BPC Market Tool](https://forums.frontier.co.uk/showthread.php?t=76081). * Trade Dangerous format file - saves commodity market data as files that you can load into [Trade Dangerous](https://bitbucket.org/kfsone/tradedangerous/wiki/Home). * 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). @@ -72,6 +71,13 @@ By default these files will be placed in your Documents folder. Since this app w Some options work by reading the Elite: Dangerous game's log files. Normally this app will find the log files but if you find some options greyed-out then adjust the “E:D log file location” setting described [below](#doesnt-track-systems-visited). +### EDDN + +* Station data + * Sends station commodity market, outfitting and shipyard data to “[EDDN](http://eddn-gateway.elite-markets.net/)” 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/), [Thrudd's Trading Tools](http://www.elitetradingtool.co.uk/), [Roguey's](http://roguey.co.uk/elite-dangerous/), etc. +* System and scan data + * Sends general system information and the results of your detailed planet scans to “[EDDN](http://eddn-gateway.elite-markets.net/)” from where you and others can use it via online prospecting tools such as [eddb](http://eddb.io/), [Inara](http://inara.cz), etc. + ### EDSM You can send a record of your location to [Elite: Dangerous Star Map](http://www.edsm.net/) where you can view your flight log under My account → Exploration Logs and optionally add private comments about a system. You will need to register for an account and then follow the “[Elite Dangerous Star Map credentials](http://www.edsm.net/settings/api)” link to obtain your API key. diff --git a/config.py b/config.py index d8e43ef8..84b8950a 100644 --- a/config.py +++ b/config.py @@ -86,6 +86,7 @@ class Config: OUT_SYS_EDSM = 256 # OUT_SYS_AUTO = 512 # Now always automatic OUT_MKT_MANUAL = 1024 + OUT_SYS_EDDN = 2048 if platform=='darwin': diff --git a/eddn.py b/eddn.py index 48e59889..a986d1d4 100644 --- a/eddn.py +++ b/eddn.py @@ -32,7 +32,8 @@ class _EDDN: 'softwareVersion' : appversion, 'uploaderID' : config.getint('anonymous') and hashlib.md5(cmdr.encode('utf-8')).hexdigest() or cmdr.encode('utf-8'), } - msg['message']['timestamp'] = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(config.getint('querytime') or int(time.time()))) + if not msg['message'].get('timestamp'): # already present in journal messages + msg['message']['timestamp'] = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(config.getint('querytime') or int(time.time()))) r = self.session.post(self.UPLOAD, data=json.dumps(msg), timeout=timeout) if __debug__ and r.status_code != requests.codes.ok: @@ -94,5 +95,11 @@ class _EDDN: } }) + def export_journal_entry(self, cmdr, is_beta, entry): + self.send(cmdr, { + '$schemaRef' : 'http://schemas.elite-markets.net/eddn/journal/1' + (is_beta and '/test' or ''), + 'message' : entry + }) + # singleton eddn = _EDDN() diff --git a/monitor.py b/monitor.py index daa8022f..5645b660 100644 --- a/monitor.py +++ b/monitor.py @@ -1,4 +1,6 @@ import atexit +from collections import OrderedDict +import json import re import threading from os import listdir, pardir, rename, unlink, SEEK_SET, SEEK_CUR, SEEK_END @@ -6,8 +8,7 @@ from os.path import basename, exists, isdir, isfile, join from platform import machine import sys from sys import platform -from time import strptime, localtime, mktime, sleep, time -from datetime import datetime +from time import sleep if __debug__: from traceback import print_exc @@ -68,11 +69,18 @@ class EDLogs(FileSystemEventHandler): self.currentdir = None # The actual logdir that we're monitoring self.logfile = None self.observer = None - self.observed = None + self.observed = None # a watchdog ObservedWatch, or None if polling self.thread = None - self.callbacks = { 'Jump': None, 'Dock': None } - self.last_event = None # for communicating the Jump event + self.event_queue = [] # For communicating journal entries back to main thread + # Context for journal handling + self.version = None + self.is_beta = False + self.mode = None + self.cmdr = None + self.system = None + self.station = None + self.coordinates = None def set_callback(self, name, callback): if name in self.callbacks: @@ -89,9 +97,6 @@ class EDLogs(FileSystemEventHandler): self.stop() self.currentdir = logdir - self.root.bind_all('<>', self.jump) # user-generated - self.root.bind_all('<>', self.dock) # user-generated - # Set up a watchog observer. This is low overhead so is left running irrespective of whether monitoring is desired. # File system events are unreliable/non-existent over network drives on Linux. # We can't easily tell whether a path points to a network drive, so assume @@ -108,17 +113,17 @@ class EDLogs(FileSystemEventHandler): # Latest pre-existing logfile - e.g. if E:D is already running. Assumes logs sort alphabetically. try: - logfiles = sorted([x for x in listdir(logdir) if x.startswith('netLog.')]) - self.logfile = logfiles and join(logdir, logfiles[-1]) or None + logfiles = sorted([x for x in listdir(self.currentdir) if x.startswith('Journal.')]) + self.logfile = logfiles and join(self.currentdir, logfiles[-1]) or None except: self.logfile = None if __debug__: - print '%s "%s"' % (polling and 'Polling' or 'Monitoring', logdir) + print '%s "%s"' % (polling and 'Polling' or 'Monitoring', self.currentdir) print 'Start logfile "%s"' % self.logfile if not self.running(): - self.thread = threading.Thread(target = self.worker, name = 'netLog worker') + self.thread = threading.Thread(target = self.worker, name = 'Journal worker') self.thread.daemon = True self.thread.start() @@ -128,18 +133,19 @@ class EDLogs(FileSystemEventHandler): if __debug__: print 'Stopping monitoring' self.currentdir = None + self.version = self.mode = self.cmdr = self.system = self.station = self.coordinates = None + self.is_beta = False if self.observed: self.observed = None self.observer.unschedule_all() self.thread = None # Orphan the worker thread - will terminate at next poll - self.last_event = None def running(self): return self.thread and self.thread.is_alive() def on_created(self, event): # watchdog callback, e.g. client (re)started. - if not event.is_directory and basename(event.src_path).startswith('netLog.'): + if not event.is_directory and basename(event.src_path).startswith('Journal.'): self.logfile = event.src_path def worker(self): @@ -147,30 +153,16 @@ class EDLogs(FileSystemEventHandler): # event_generate() is the only safe way to poke the main thread from this thread: # https://mail.python.org/pipermail/tkinter-discuss/2013-November/003522.html - # e.g.: - # "{18:00:41} System:"Shinrarta Dezhra" StarPos:(55.719,17.594,27.156)ly NormalFlight\r\n" - # or with verboseLogging: - # "{17:20:18} System:"Shinrarta Dezhra" StarPos:(55.719,17.594,27.156)ly Body:69 RelPos:(0.334918,1.20754,1.23625)km NormalFlight\r\n" - # or: - # "... Supercruise\r\n" - # Note that system name may contain parantheses, e.g. "Pipe (stem) Sector PI-T c3-5". - regexp = re.compile(r'\{(.+)\} System:"(.+)" StarPos:\((.+),(.+),(.+)\)ly.* (\S+)') # (localtime, system, x, y, z, context) - - # e.g.: - # "{14:42:11} GetSafeUniversalAddress Station Count 1 moved 0 Docked Not Landed\r\n" - # or: - # "... Undocked Landed\r\n" - # Don't use the simpler "Commander Put ..." message since its more likely to be delayed. - dockre = re.compile(r'\{(.+)\} GetSafeUniversalAddress Station Count \d+ moved \d+ (\S+) ([^\r\n]+)') # (localtime, docked_status, landed_status) - - docked = False # Whether we're docked - updated = False # Whether we've sent an update since we docked - # Seek to the end of the latest log file logfile = self.logfile if logfile: loghandle = open(logfile, 'r') - loghandle.seek(0, SEEK_END) # seek to EOF + for line in loghandle: + try: + self.parse_entry(line) # Some events are of interest even in the past + except: + if __debug__: + print 'Invalid journal entry "%s"' % repr(line) else: loghandle = None @@ -179,19 +171,13 @@ class EDLogs(FileSystemEventHandler): while True: - if docked and not updated and not config.getint('output') & config.OUT_MKT_MANUAL: - self.root.event_generate('<>', when="tail") - updated = True - if __debug__: - print "%s :\t%s %s" % ('Updated', docked and " docked" or "!docked", updated and " updated" or "!updated") - # Check whether new log file started, e.g. client (re)started. if emitter and emitter.is_alive(): newlogfile = self.logfile # updated by on_created watchdog callback else: # Poll try: - logfiles = sorted([x for x in listdir(self.currentdir) if x.startswith('netLog.')]) + logfiles = sorted([x for x in listdir(self.currentdir) if x.startswith('Journal.')]) newlogfile = logfiles and join(self.currentdir, logfiles[-1]) or None except: if __debug__: print_exc() @@ -207,37 +193,11 @@ class EDLogs(FileSystemEventHandler): print 'New logfile "%s"' % logfile if logfile: - system = visited = coordinates = None loghandle.seek(0, SEEK_CUR) # reset EOF flag - for line in loghandle: - match = regexp.match(line) - if match: - (visited, system, x, y, z, context) = match.groups() - if system == 'ProvingGround': - system = 'CQC' - coordinates = (float(x), float(y), float(z)) - else: - match = dockre.match(line) - if match: - if match.group(2) == 'Undocked': - docked = updated = False - elif match.group(2) == 'Docked': - docked = True - # do nothing now in case the API server is lagging, but update on next poll - if __debug__: - print "%s :\t%s %s" % (match.group(2), docked and " docked" or "!docked", updated and " updated" or "!updated") - - if system and not docked and config.getint('output'): - # Convert local time string to UTC date and time - visited_struct = strptime(visited, '%H:%M:%S') - now = localtime() - if now.tm_hour == 0 and visited_struct.tm_hour == 23: - # Crossed midnight between timestamp and poll - now = localtime(time()-12*60*60) # yesterday - time_struct = datetime(now.tm_year, now.tm_mon, now.tm_mday, visited_struct.tm_hour, visited_struct.tm_min, visited_struct.tm_sec).timetuple() # still local time - self.last_event = (mktime(time_struct), system, coordinates) - self.root.event_generate('<>', when="tail") + self.event_queue.append(line) + if self.event_queue: + self.root.event_generate('<>', when="tail") sleep(self._POLL) @@ -245,15 +205,39 @@ class EDLogs(FileSystemEventHandler): if threading.current_thread() != self.thread: return # Terminate - def jump(self, event): - # Called from Tkinter's main loop - if self.callbacks['Jump'] and self.last_event: - self.callbacks['Jump'](event, *self.last_event) + def parse_entry(self, line): + try: + entry = json.loads(line, object_pairs_hook=OrderedDict) # Preserve property order because why not? + entry['timestamp'] # we expect this to exist + if entry['event'] == 'Fileheader': # XXX or 'fileheader' ? + self.version = entry['gameversion'] + self.is_beta = 'beta' in entry['gameversion'].lower() + elif entry['event'] == 'LoadGame': + self.cmdr = entry['Commander'] + self.mode = entry['GameMode'] + elif entry['event'] == 'NewCommander': + self.cmdr = entry['Name'] + elif entry['event'] in ['Undocked']: + self.station = None + self.coordinates = None + elif entry['event'] in ['Location', 'FSDJump', 'Docked']: + if 'StarPos' in entry: + self.coordinates = tuple(entry['StarPos']) + elif self.system != entry['StarSystem']: + self.coordinates = None # Docked event doesn't include coordinates + self.system = entry['StarSystem'] == 'ProvingGround' and 'CQC' or entry['StarSystem'] + self.station = entry.get('StationName') # May be None + return entry + except: + if __debug__: + print 'Invalid journal entry "%s"' % repr(line) + return { 'event': None } - def dock(self, event): - # Called from Tkinter's main loop - if self.callbacks['Dock']: - self.callbacks['Dock']() + def get_entry(self): + if not self.event_queue: + return None + else: + return self.parse_entry(self.event_queue.pop(0)) def is_valid_logdir(self, path): return self._is_valid_logdir(path) diff --git a/plug.py b/plug.py index 6920b90f..7f96c354 100644 --- a/plug.py +++ b/plug.py @@ -87,12 +87,33 @@ def get_plugin_pref(plugname, parent): return None +def notify_journal_entry(cmdr, system, station, entry): + """ + Send a journal entry to each plugin. + :param cmdr: The Cmdr name, or None if not yet known + :param system: The current system, or None if not yet known + :param station: The current station, or None if not docked or not yet known + :param entry: The journal entry as a dictionary + :return: + """ + for plugname in PLUGINS: + journal_entry = _get_plugin_func(plugname, "journal_entry") + if journal_entry: + try: + # Pass a copy of the journal entry in case the callee modifies it + journal_entry(cmdr, system, station, dict(entry)) + except Exception as plugerr: + print plugerr + + def notify_system_changed(timestamp, system, coordinates): """ Send notification data to each plugin when we arrive at a new system. :param timestamp: :param system: :return: + deprecated:: 2.2 + Use :func:`journal_entry` with the 'FSDJump' event. """ for plugname in PLUGINS: system_changed = _get_plugin_func(plugname, "system_changed") diff --git a/plugins/About/load.py b/plugins/About/load.py index 5441f83f..1dbc2e02 100644 --- a/plugins/About/load.py +++ b/plugins/About/load.py @@ -45,18 +45,21 @@ def plugin_app(parent): return plugin_app.status -def system_changed(timestamp, system, coordinates): +def journal_entry(cmdr, system, station, entry): """ - Arrived in a new System - :param timestamp: when we arrived - :param system: the name of the system - :param coordinates: tuple of (x,y,z) ly relative to Sol, or None if unknown + E:D client made a journal entry + :param cmdr: The Cmdr name, or None if not yet known + :param system: The current system, or None if not yet known + :param station: The current station, or None if not docked or not yet known + :param entry: The journal entry as a dictionary :return: """ - if coordinates: - sys.stderr.write("Arrived at {} ({},{},{})\n".format(system, *coordinates)) - else: - sys.stderr.write("Arrived at {}\n".format(system)) + if entry['event'] == 'FSDJump': + # We arrived at a new system! + if 'StarPos' in entry: + sys.stderr.write("Arrived at {} ({},{},{})\n".format(entry['StarSystem'], *tuple(entry['StarPos']))) + else: + sys.stderr.write("Arrived at {}\n".format(entry['StarSystem'])) def cmdr_data(data): diff --git a/prefs.py b/prefs.py index 90c34e7f..7f460efb 100644 --- a/prefs.py +++ b/prefs.py @@ -113,11 +113,9 @@ class PreferencesDialog(tk.Toplevel): outframe = nb.Frame(notebook) outframe.columnconfigure(0, weight=1) - output = config.getint('output') or (config.OUT_MKT_EDDN | config.OUT_SHIP_EDS) # default settings + output = config.getint('output') or (config.OUT_MKT_EDDN | config.OUT_SYS_EDDN | config.OUT_SHIP_EDS) # default settings nb.Label(outframe, text=_('Please choose what data to save')).grid(columnspan=2, padx=PADX, sticky=tk.W) - self.out_eddn= tk.IntVar(value = (output & config.OUT_MKT_EDDN) and 1) - nb.Checkbutton(outframe, text=_('Send station data to the Elite Dangerous Data Network'), variable=self.out_eddn, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.out_csv = tk.IntVar(value = (output & config.OUT_MKT_CSV ) and 1) nb.Checkbutton(outframe, text=_('Market data in CSV format file'), variable=self.out_csv, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.out_bpc = tk.IntVar(value = (output & config.OUT_MKT_BPC ) and 1) @@ -149,13 +147,27 @@ class PreferencesDialog(tk.Toplevel): notebook.add(outframe, text=_('Output')) # Tab heading in settings + eddnframe = nb.Frame(notebook) + + HyperlinkLabel(eddnframe, text='Elite Dangerous Data Network', background=nb.Label().cget('background'), url='https://github.com/jamesremuscat/EDDN/wiki', underline=True).grid(padx=PADX, sticky=tk.W) # Don't translate + self.eddn_station= tk.IntVar(value = (output & config.OUT_MKT_EDDN) and 1) + nb.Checkbutton(eddnframe, text=_('Send station data to the Elite Dangerous Data Network'), variable=self.eddn_station, command=self.outvarchanged).grid(padx=BUTTONX, pady=(5,0), sticky=tk.W) # Output setting + self.eddn_auto_button = nb.Checkbutton(eddnframe, text=_('Automatically update on docking'), variable=self.out_auto, command=self.outvarchanged) # Output setting + self.eddn_auto_button.grid(padx=BUTTONX, sticky=tk.W) + self.eddn_system = tk.IntVar(value = (output & config.OUT_SYS_EDDN) and 1) + self.eddn_system_button = nb.Checkbutton(eddnframe, text=_('Send system and scan data to the Elite Dangerous Data Network'), variable=self.eddn_system, command=self.outvarchanged) # Output setting new in E:D 2.2 + self.eddn_system_button.grid(padx=BUTTONX, pady=(5,0), sticky=tk.W) + + 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, sticky=tk.W) + 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 @@ -196,7 +208,8 @@ class PreferencesDialog(tk.Toplevel): _('Browse...')), # Folder selection button on Windows command = lambda:self.filebrowse(_('E:D log file location'), self.logdir)) self.logbutton.grid(row=10, column=2, padx=PADX, sticky=tk.EW) - nb.Button(configframe, text=_('Default'), command=self.logdir_reset, state = monitor.logdir and tk.NORMAL or tk.DISABLED).grid(column=2, padx=PADX, pady=(5,0), sticky=tk.EW) # Appearance theme and language setting + if monitor.logdir: + nb.Button(configframe, text=_('Default'), command=self.logdir_reset, state = monitor.logdir and tk.NORMAL or tk.DISABLED).grid(column=2, padx=PADX, pady=(5,0), sticky=tk.EW) # Appearance theme and language setting if platform == 'win32': ttk.Separator(configframe, orient=tk.HORIZONTAL).grid(columnspan=3, padx=PADX, pady=PADY*8, sticky=tk.EW) @@ -298,11 +311,14 @@ class PreferencesDialog(tk.Toplevel): logvalid = monitor.is_valid_logdir(logdir) 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() - self.out_auto_button['state'] = (local or self.out_eddn.get()) and logvalid and tk.NORMAL or tk.DISABLED + self.out_auto_button['state'] = local and logvalid and tk.NORMAL or tk.DISABLED self.outdir_label['state'] = local and tk.NORMAL or tk.DISABLED self.outbutton['state'] = local and tk.NORMAL or tk.DISABLED self.outdir['state'] = local and 'readonly' or tk.DISABLED + self.eddn_auto_button['state'] = self.eddn_station.get() and logvalid and tk.NORMAL or tk.DISABLED + self.eddn_system_button['state']= logvalid and tk.NORMAL or tk.DISABLED + self.edsm_log_button['state'] = logvalid and tk.NORMAL or tk.DISABLED edsm_state = logvalid and self.edsm_log.get() and tk.NORMAL or tk.DISABLED self.edsm_label['state'] = edsm_state @@ -425,13 +441,14 @@ class PreferencesDialog(tk.Toplevel): config.set('password', self.password.get().strip()) config.set('output', - (self.out_eddn.get() and config.OUT_MKT_EDDN) + (self.out_bpc.get() and config.OUT_MKT_BPC) + (self.out_td.get() and config.OUT_MKT_TD) + (self.out_csv.get() and config.OUT_MKT_CSV) + (config.OUT_MKT_MANUAL if not self.out_auto.get() else 0) + (self.out_ship_eds.get() and config.OUT_SHIP_EDS) + (self.out_ship_coriolis.get() and config.OUT_SHIP_CORIOLIS) + + (self.eddn_station.get() and config.OUT_MKT_EDDN) + + (self.eddn_system.get() and config.OUT_SYS_EDDN) + (self.edsm_log.get() and config.OUT_SYS_EDSM)) config.set('outdir', self.outdir.get().startswith('~') and join(config.home, self.outdir.get()[2:]) or self.outdir.get()) From 78e001343ce038e079d86fbabf67b3e0e3ac49cb Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Sat, 27 Aug 2016 16:39:24 +0100 Subject: [PATCH 03/21] Log with EDSM only on Location and FSDJump events and not on "Update". "Last updated at ..." message now only shown for API update. --- EDMarketConnector.py | 51 ++++++++++++++++++++++++++------------------ config.py | 1 + 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 7a5ce618..f861df55 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -379,15 +379,6 @@ class AppWindow: loadout.export(data) if config.getint('output') & config.OUT_SHIP_CORIOLIS: coriolis.export(data) - if config.getint('output') & config.OUT_SYS_EDSM: - # Silently catch any EDSM errors here so that they don't prevent station update - try: - self.edsm.lookup(self.system['text'], EDDB.system(self.system['text'])) - except Exception as e: - if __debug__: print_exc() - else: - self.edsm.link(self.system['text']) - self.edsmpoll() if not (config.getint('output') & (config.OUT_MKT_CSV|config.OUT_MKT_TD|config.OUT_MKT_BPC|config.OUT_MKT_EDDN)): # no station data requested - we're done @@ -497,6 +488,8 @@ class AppWindow: self.cmdr['text'] = monitor.cmdr or '' self.system['text'] = monitor.system or '' self.station['text'] = monitor.station or (EDDB.system(monitor.system) and self.STATION_UNDOCKED or '') + if system_changed or station_changed: + self.status['text'] = '' plug.notify_journal_entry(monitor.cmdr, monitor.system, monitor.station, entry) @@ -513,7 +506,7 @@ class AppWindow: self.status['text'] = _('Sending data to EDSM...') self.w.update_idletasks() self.edsm.writelog(timestamp, monitor.system, monitor.coordinates) - self.status['text'] = strftime(_('Last updated at {HH}:{MM}:{SS}').format(HH='%H', MM='%M', SS='%S').encode('utf-8'), localtime(timestamp)).decode('utf-8') + self.status['text'] = '' except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) @@ -521,25 +514,41 @@ class AppWindow: hotkeymgr.play_bad() else: self.edsm.link(monitor.system) - self.status['text'] = strftime(_('Last updated at {HH}:{MM}:{SS}').format(HH='%H', MM='%M', SS='%S').encode('utf-8'), localtime(timestamp)).decode('utf-8') self.edsmpoll() + # Auto-Update after docking + if station_changed and not config.getint('output') & config.OUT_MKT_MANUAL and config.getint('output') & config.OUT_STATION_ANY: + self.w.after(int(SERVER_RETRY * 1000), self.getandsend) + # Send interesting events to EDDN if (config.getint('output') & config.OUT_SYS_EDDN and monitor.cmdr and (entry['event'] == 'FSDJump' and system_changed or entry['event'] == 'Docked' and station_changed or entry['event'] == 'Scan')): - # strip out properties disallowed by the schema - for thing in ['CockpitBreach', 'BoostUsed', 'FuelLevel', 'FuelUsed', 'JumpDist']: - entry.pop(thing, None) - for thing in entry.keys(): - if thing.endswith('_Localised'): - entry.pop(thing, None) - eddn.export_journal_entry(monitor.cmdr, monitor.is_beta, entry) + try: + self.status['text'] = _('Sending data to EDDN...') - # Auto-Update after docking - if station_changed and config.getint('output') & (config.OUT_MKT_EDDN|config.OUT_MKT_MANUAL) == config.OUT_MKT_EDDN and entry['event'] == 'Docked': - self.w.after(int(SERVER_RETRY * 1000), self.getandsend) + # strip out properties disallowed by the schema + for thing in ['CockpitBreach', 'BoostUsed', 'FuelLevel', 'FuelUsed', 'JumpDist']: + entry.pop(thing, None) + for thing in entry.keys(): + if thing.endswith('_Localised'): + entry.pop(thing, None) + + eddn.export_journal_entry(monitor.cmdr, monitor.is_beta, entry) + self.status['text'] = '' + + except requests.exceptions.RequestException as e: + if __debug__: print_exc() + self.status['text'] = _("Error: Can't connect to EDDN") + if not config.getint('hotkey_mute'): + hotkeymgr.play_bad() + + except Exception as e: + if __debug__: print_exc() + self.status['text'] = unicode(e) + if not config.getint('hotkey_mute'): + hotkeymgr.play_bad() def edsmpoll(self): result = self.edsm.result diff --git a/config.py b/config.py index 84b8950a..c1baebe7 100644 --- a/config.py +++ b/config.py @@ -83,6 +83,7 @@ class Config: # OUT_SYS_FILE = 32 # No longer supported # OUT_STAT = 64 # No longer available OUT_SHIP_CORIOLIS = 128 + OUT_STATION_ANY = OUT_MKT_EDDN|OUT_MKT_BPC|OUT_MKT_TD|OUT_MKT_CSV|OUT_SHIP_EDS|OUT_SHIP_CORIOLIS OUT_SYS_EDSM = 256 # OUT_SYS_AUTO = 512 # Now always automatic OUT_MKT_MANUAL = 1024 From 91b5d0a6eadf7e6532f44f2c4ca1ed1a9be61f6c Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Tue, 30 Aug 2016 17:00:48 +0100 Subject: [PATCH 04/21] Point to journal location in SavedGames folder --- L10n/en.template | 4 +- README.md | 10 +---- config.py | 38 +++++++++++----- monitor.py | 115 ++--------------------------------------------- prefs.py | 30 ++++++------- 5 files changed, 50 insertions(+), 147 deletions(-) diff --git a/L10n/en.template b/L10n/en.template index 8975a04f..5a60690d 100644 --- a/L10n/en.template +++ b/L10n/en.template @@ -109,8 +109,8 @@ /* Empire rank. [stats.py] */ "Duke" = "Duke"; -/* Configuration setting. [prefs.py] */ -"E:D log file location" = "E:D log file location"; +/* Location of the new Journal file in E:D 2.2. [prefs.py] */ +"E:D journal file location" = "E:D journal file location"; /* Empire rank. [stats.py] */ "Earl" = "Earl"; diff --git a/README.md b/README.md index c517dbd7..2da16df2 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ This app can save a variety of data in a variety of formats: By default these files will be placed in your Documents folder. Since this app will create a lot of files if you use it for a while you may wish to create a separate folder for the files and tell the app to place them there. -Some options work by reading the Elite: Dangerous game's log files. Normally this app will find the log files but if you find some options greyed-out then adjust the “E:D log file location” setting described [below](#doesnt-track-systems-visited). +Some options work by reading the Elite: Dangerous game's “journal” files. If you're running this app on a different machine from the Elite: Dangerous game then adjust the “E:D journal file location” setting on the Configuration tab to point to the game's journal files. ### EDDN @@ -126,13 +126,7 @@ Ensure that you visit the in-game Commodity Market at a station where you intend This problem is tracked as [Issue #92](https://github.com/Marginal/EDMarketConnector/issues/92). ### Doesn't track Systems visited -This app uses Elite: Dangerous' log files to track the systems and stations that you visit. When looking for the log files, this app assumes: - -- That you're running this app and Elite: Dangerous on the same machine. -- That you're running Elite: Dangerous from Steam, if you have both Steam and non-Steam versions installed. -- That you're running “Horizons” 64bit, if you have both Horizons and Season 1 installed. - -If you find that this app isn't automatically tracking the systems that you visit and/or isn't automatically “updating” on docking (if you have that option selected), or if you're running this app on a different machine from the Elite: Dangerous game then adjust the “E:D log file location” setting on the Configuration tab to point to the game's log files using [this info](https://support.frontier.co.uk/kb/faq.php?id=108) as a guide. +This app uses Elite: Dangerous' “journal” files to track the systems and stations that you visit. If you're running this app on a different machine from the Elite: Dangerous game, or if you find that this app isn't automatically tracking the systems that you visit and/or isn't automatically “updating” on docking (if you have that option selected), then adjust the “E:D journal file location” setting on the Configuration tab to point to the game's journal files. Running from source -------- diff --git a/config.py b/config.py index c1baebe7..b51ca4f6 100644 --- a/config.py +++ b/config.py @@ -1,7 +1,7 @@ import numbers import sys from os import getenv, makedirs, mkdir, pardir -from os.path import expanduser, dirname, isdir, join, normpath +from os.path import expanduser, dirname, exists, isdir, join, normpath from sys import platform @@ -19,14 +19,21 @@ if platform=='darwin': elif platform=='win32': import ctypes + from ctypes.wintypes import * + import uuid - CSIDL_PERSONAL = 0x0005 - CSIDL_LOCAL_APPDATA = 0x001C - CSIDL_PROFILE = 0x0028 + FOLDERID_Documents = uuid.UUID('{FDD39AD0-238F-46AF-ADB4-6C85480369C7}') + FOLDERID_LocalAppData = uuid.UUID('{F1B32785-6FBA-4FCF-9D55-7B8E7F157091}') + FOLDERID_Profile = uuid.UUID('{5E6C858F-0E22-4760-9AFE-EA3317B67173}') + FOLDERID_SavedGames = uuid.UUID('{4C5C32FF-BB9D-43b0-B5B4-2D72E54EAAA4}') + + SHGetKnownFolderPath = ctypes.windll.shell32.SHGetKnownFolderPath + SHGetKnownFolderPath.argtypes = [ctypes.c_char_p, DWORD, HANDLE, ctypes.POINTER(ctypes.c_wchar_p)] + + CoTaskMemFree = ctypes.windll.ole32.CoTaskMemFree + CoTaskMemFree.argtypes = [ctypes.c_void_p] # _winreg that ships with Python 2 doesn't support unicode, so do this instead - from ctypes.wintypes import * - HKEY_CURRENT_USER = 0x80000001 KEY_ALL_ACCESS = 0x000F003F REG_CREATED_NEW_KEY = 0x00000001 @@ -100,6 +107,8 @@ class Config: if not isdir(self.plugin_dir): mkdir(self.plugin_dir) + self.default_journal_dir = join(NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, True)[0], 'Frontier Developments', 'Elite Dangerous', 'Logs') # FIXME: check this + self.home = expanduser('~') self.respath = getattr(sys, 'frozen', False) and normpath(join(dirname(sys.executable), pardir, 'Resources')) or dirname(__file__) @@ -148,19 +157,25 @@ class Config: def __init__(self): - buf = ctypes.create_unicode_buffer(MAX_PATH) - ctypes.windll.shell32.SHGetSpecialFolderPathW(0, buf, CSIDL_LOCAL_APPDATA, 0) + buf = ctypes.c_wchar_p() + SHGetKnownFolderPath(ctypes.create_string_buffer(FOLDERID_LocalAppData.bytes_le), 0, 0, ctypes.byref(buf)) self.app_dir = join(buf.value, appname) if not isdir(self.app_dir): mkdir(self.app_dir) + CoTaskMemFree(buf) self.plugin_dir = join(self.app_dir, 'plugins') if not isdir(self.plugin_dir): mkdir(self.plugin_dir) # expanduser in Python 2 on Windows doesn't handle non-ASCII - http://bugs.python.org/issue13207 - ctypes.windll.shell32.SHGetSpecialFolderPathW(0, buf, CSIDL_PROFILE, 0) + SHGetKnownFolderPath(ctypes.create_string_buffer(FOLDERID_Profile.bytes_le), 0, 0, ctypes.byref(buf)) self.home = buf.value + CoTaskMemFree(buf) + + SHGetKnownFolderPath(ctypes.create_string_buffer(FOLDERID_SavedGames.bytes_le), 0, 0, ctypes.byref(buf)) + self.default_journal_dir = buf.value + CoTaskMemFree(buf) self.respath = dirname(getattr(sys, 'frozen', False) and sys.executable or __file__) @@ -190,8 +205,9 @@ class Config: if not self.get('outdir') or not isdir(self.get('outdir')): buf = ctypes.create_unicode_buffer(MAX_PATH) - ctypes.windll.shell32.SHGetSpecialFolderPathW(0, buf, CSIDL_PERSONAL, 0) + SHGetKnownFolderPath(ctypes.create_string_buffer(FOLDERID_Documents.bytes_le), 0, 0, ctypes.byref(buf)) self.set('outdir', buf.value) + CoTaskMemFree(buf) def get(self, key): typ = DWORD() @@ -253,6 +269,8 @@ class Config: if not isdir(self.plugin_dir): mkdir(self.plugin_dir) + self.default_journal_dir = None + self.home = expanduser('~') self.respath = dirname(__file__) diff --git a/monitor.py b/monitor.py index 5645b660..bb62a33f 100644 --- a/monitor.py +++ b/monitor.py @@ -65,7 +65,6 @@ class EDLogs(FileSystemEventHandler): def __init__(self): FileSystemEventHandler.__init__(self) # futureproofing - not need for current version of watchdog self.root = None - self.logdir = self._logdir() # E:D client's default Logs directory, or None if not found self.currentdir = None # The actual logdir that we're monitoring self.logfile = None self.observer = None @@ -88,8 +87,8 @@ class EDLogs(FileSystemEventHandler): def start(self, root): self.root = root - logdir = config.get('logdir') or self.logdir - if not self.is_valid_logdir(logdir): + logdir = config.get('journaldir') or config.default_journal_dir + if not logdir or not exists(logdir): self.stop() return False @@ -101,7 +100,7 @@ class EDLogs(FileSystemEventHandler): # File system events are unreliable/non-existent over network drives on Linux. # We can't easily tell whether a path points to a network drive, so assume # any non-standard logdir might be on a network drive and poll instead. - polling = bool(config.get('logdir')) and platform != 'win32' + polling = bool(config.get('journaldir')) and platform != 'win32' if not polling and not self.observer: self.observer = Observer() self.observer.daemon = True @@ -239,114 +238,6 @@ class EDLogs(FileSystemEventHandler): else: return self.parse_entry(self.event_queue.pop(0)) - def is_valid_logdir(self, path): - return self._is_valid_logdir(path) - - - if platform=='darwin': - - def _logdir(self): - # https://support.frontier.co.uk/kb/faq.php?id=97 - paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, True) - if len(paths) and self._is_valid_logdir(join(paths[0], 'Frontier Developments', 'Elite Dangerous', 'Logs')): - return join(paths[0], 'Frontier Developments', 'Elite Dangerous', 'Logs') - else: - return None - - def _is_valid_logdir(self, path): - # Apple's SMB implementation is too flaky so assume target machine is OSX - return path and isdir(path) and isfile(join(path, pardir, 'AppNetCfg.xml')) - - - elif platform=='win32': - - def _logdir(self): - - # Try locations described in https://support.elitedangerous.com/kb/faq.php?id=108, in reverse order of age - candidates = [] - - # Steam and Steam libraries - key = HKEY() - if not RegOpenKeyEx(HKEY_CURRENT_USER, r'Software\Valve\Steam', 0, KEY_READ, ctypes.byref(key)): - valtype = DWORD() - valsize = DWORD() - if not RegQueryValueEx(key, 'SteamPath', 0, ctypes.byref(valtype), None, ctypes.byref(valsize)) and valtype.value == REG_SZ: - buf = ctypes.create_unicode_buffer(valsize.value / 2) - if not RegQueryValueEx(key, 'SteamPath', 0, ctypes.byref(valtype), buf, ctypes.byref(valsize)): - steampath = buf.value.replace('/', '\\') # For some reason uses POSIX seperators - steamlibs = [steampath] - try: - # Simple-minded Valve VDF parser - with open(join(steampath, 'config', 'config.vdf'), 'rU') as h: - for line in h: - vals = line.split() - if vals and vals[0].startswith('"BaseInstallFolder_'): - steamlibs.append(vals[1].strip('"').replace('\\\\', '\\')) - except: - pass - for lib in steamlibs: - candidates.append(join(lib, 'steamapps', 'common', 'Elite Dangerous', 'Products')) - RegCloseKey(key) - - # Next try custom installation under the Launcher - if not RegOpenKeyEx(HKEY_LOCAL_MACHINE, - machine().endswith('64') and - r'SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall' or # Assumes that the launcher is a 32bit process - r'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall', - 0, KEY_READ, ctypes.byref(key)): - buf = ctypes.create_unicode_buffer(MAX_PATH) - i = 0 - while True: - size = DWORD(MAX_PATH) - if RegEnumKeyEx(key, i, buf, ctypes.byref(size), None, None, None, None): - break - - subkey = HKEY() - if not RegOpenKeyEx(key, buf, 0, KEY_READ, ctypes.byref(subkey)): - valtype = DWORD() - valsize = DWORD((len('Frontier Developments')+1)*2) - valbuf = ctypes.create_unicode_buffer(valsize.value / 2) - if not RegQueryValueEx(subkey, 'Publisher', 0, ctypes.byref(valtype), valbuf, ctypes.byref(valsize)) and valtype.value == REG_SZ and valbuf.value == 'Frontier Developments': - if not RegQueryValueEx(subkey, 'InstallLocation', 0, ctypes.byref(valtype), None, ctypes.byref(valsize)) and valtype.value == REG_SZ: - valbuf = ctypes.create_unicode_buffer(valsize.value / 2) - if not RegQueryValueEx(subkey, 'InstallLocation', 0, ctypes.byref(valtype), valbuf, ctypes.byref(valsize)): - candidates.append(join(valbuf.value, 'Products')) - RegCloseKey(subkey) - i += 1 - RegCloseKey(key) - - # Standard non-Steam locations - programs = ctypes.create_unicode_buffer(MAX_PATH) - ctypes.windll.shell32.SHGetSpecialFolderPathW(0, programs, CSIDL_PROGRAM_FILESX86, 0) - candidates.append(join(programs.value, 'Frontier', 'Products')), - - applocal = ctypes.create_unicode_buffer(MAX_PATH) - ctypes.windll.shell32.SHGetSpecialFolderPathW(0, applocal, CSIDL_LOCAL_APPDATA, 0) - candidates.append(join(applocal.value, 'Frontier_Developments', 'Products')) - - for game in ['elite-dangerous-64', 'FORC-FDEV-D-1']: # Look for Horizons in all candidate places first - for base in candidates: - if isdir(base): - for d in listdir(base): - if d.startswith(game) and self._is_valid_logdir(join(base, d, 'Logs')): - return join(base, d, 'Logs') - - return None - - def _is_valid_logdir(self, path): - # Assume target machine is Windows - return path and isdir(path) and isfile(join(path, pardir, 'AppConfig.xml')) - - - elif platform=='linux2': - - def _logdir(self): - return None - - def _is_valid_logdir(self, path): - # Assume target machine is Windows - return path and isdir(path) and isfile(join(path, pardir, 'AppConfig.xml')) - # singleton monitor = EDLogs() diff --git a/prefs.py b/prefs.py index 7f460efb..5606e610 100644 --- a/prefs.py +++ b/prefs.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -from os.path import dirname, expanduser, isdir, join, sep +from os.path import dirname, expanduser, exists, isdir, join, sep from sys import platform import Tkinter as tk @@ -191,7 +191,7 @@ class PreferencesDialog(tk.Toplevel): configframe.columnconfigure(1, weight=1) self.logdir = nb.Entry(configframe, takefocus=False) - logdir = config.get('logdir') or monitor.logdir + logdir = config.get('journaldir') or config.default_journal_dir if not logdir: pass elif logdir.startswith(config.home): @@ -202,14 +202,14 @@ class PreferencesDialog(tk.Toplevel): if platform != 'darwin': # Apple's SMB implementation is way too flaky - no filesystem events and bogus NULLs - nb.Label(configframe, text = _('E:D log file location')+':').grid(columnspan=3, padx=PADX, sticky=tk.W) # Configuration setting + nb.Label(configframe, text = _('E:D journal file location')+':').grid(columnspan=3, padx=PADX, sticky=tk.W) # Location of the new Journal file in E:D 2.2 self.logdir.grid(row=10, columnspan=2, padx=(PADX,0), sticky=tk.EW) self.logbutton = nb.Button(configframe, text=(platform=='darwin' and _('Change...') or # Folder selection button on OSX _('Browse...')), # Folder selection button on Windows - command = lambda:self.filebrowse(_('E:D log file location'), self.logdir)) + command = lambda:self.filebrowse(_('E:D journal file location'), self.logdir)) self.logbutton.grid(row=10, column=2, padx=PADX, sticky=tk.EW) - if monitor.logdir: - nb.Button(configframe, text=_('Default'), command=self.logdir_reset, state = monitor.logdir and tk.NORMAL or tk.DISABLED).grid(column=2, padx=PADX, pady=(5,0), sticky=tk.EW) # Appearance theme and language setting + if config.default_journal_dir: + nb.Button(configframe, text=_('Default'), command=self.logdir_reset, state = config.get('journaldir') and tk.NORMAL or tk.DISABLED).grid(column=2, padx=PADX, pady=(5,0), sticky=tk.EW) # Appearance theme and language setting if platform == 'win32': ttk.Separator(configframe, orient=tk.HORIZONTAL).grid(columnspan=3, padx=PADX, pady=PADY*8, sticky=tk.EW) @@ -308,7 +308,7 @@ class PreferencesDialog(tk.Toplevel): def outvarchanged(self): logdir = self.logdir.get().startswith('~') and join(config.home, self.logdir.get()[2:]) or self.logdir.get() - logvalid = monitor.is_valid_logdir(logdir) + logvalid = logdir and exists(logdir) 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() self.out_auto_button['state'] = local and logvalid and tk.NORMAL or tk.DISABLED @@ -366,12 +366,12 @@ class PreferencesDialog(tk.Toplevel): def logdir_reset(self): self.logdir['state'] = tk.NORMAL # must be writable to update self.logdir.delete(0, tk.END) - if not monitor.logdir: - pass - elif monitor.logdir.startswith(config.home): - self.logdir.insert(0, '~' + monitor.logdir[len(config.home):]) + if not config.default_journal_dir: + pass # Can't reset + elif config.default_journal_dir.startswith(config.home): + self.logdir.insert(0, '~' + config.default_journal_dir[len(config.home):]) else: - self.logdir.insert(0, monitor.logdir) + self.logdir.insert(0, config.default_journal_dir) self.logdir['state'] = 'readonly' self.outvarchanged() @@ -456,10 +456,10 @@ class PreferencesDialog(tk.Toplevel): config.set('edsm_apikey', self.edsm_apikey.get().strip()) logdir = self.logdir.get().startswith('~') and join(config.home, self.logdir.get()[2:]) or self.logdir.get() - if monitor.logdir and logdir.lower() == monitor.logdir.lower(): - config.set('logdir', '') # default location + if config.default_journal_dir and logdir.lower() == config.default_journal_dir.lower(): + config.set('journaldir', '') # default location else: - config.set('logdir', logdir) + config.set('journaldir', logdir) if platform in ['darwin','win32']: config.set('hotkey_code', self.hotkey_code) config.set('hotkey_mods', self.hotkey_mods) From 38d348544a076cd04be28f3613afb799be8184c3 Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Fri, 2 Sep 2016 16:15:55 +0100 Subject: [PATCH 05/21] Error when Companion API doesn't match Journal --- EDMarketConnector.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index f861df55..9e3c7a43 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -356,6 +356,8 @@ class AppWindow: self.status['text'] = _("Where are you?!") # Shouldn't happen elif not data.get('ship') or not data['ship'].get('modules') or not data['ship'].get('name','').strip(): self.status['text'] = _("What are you flying?!") # Shouldn't happen + elif monitor.cmdr and data['commander']['name'] != monitor.cmdr: + raise companion.CredentialsError() # Companion API credentials don't match Journal elif auto_update and (not data['commander'].get('docked') or (self.system['text'] and data['lastSystem']['name'] != self.system['text'])): raise companion.ServerLagging() @@ -517,7 +519,7 @@ class AppWindow: self.edsmpoll() # Auto-Update after docking - if station_changed and not config.getint('output') & config.OUT_MKT_MANUAL and config.getint('output') & config.OUT_STATION_ANY: + if station_changed and not monitor.is_beta and not config.getint('output') & config.OUT_MKT_MANUAL and config.getint('output') & config.OUT_STATION_ANY: self.w.after(int(SERVER_RETRY * 1000), self.getandsend) # Send interesting events to EDDN From db543c290174caaeb3d0b740066f68d7b76e8090 Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Tue, 6 Sep 2016 01:54:13 +0100 Subject: [PATCH 06/21] Option to delay sending body data until docked --- EDMarketConnector.py | 7 ++++++ README.md | 1 + config.py | 1 + eddn.py | 57 ++++++++++++++++++++++++++++++++++++++++---- prefs.py | 6 +++++ 5 files changed, 68 insertions(+), 4 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 9e3c7a43..b647addf 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -309,6 +309,12 @@ class AppWindow: if __debug__: print_exc() self.status['text'] = unicode(e) + # Try to obtain exclusive lock on journal cache, even if we don't need it yet + try: + eddn.load() + except Exception as e: + self.status['text'] = unicode(e) + if not getattr(sys, 'frozen', False): self.updater.checkForUpdates() # Sparkle / WinSparkle does this automatically for packaged apps @@ -620,6 +626,7 @@ class AppWindow: if platform!='darwin' or self.w.winfo_rooty()>0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 config.set('geometry', '+{1}+{2}'.format(*self.w.geometry().split('+'))) config.close() + eddn.close() self.updater.close() self.session.close() self.w.destroy() diff --git a/README.md b/README.md index 2da16df2..ca32ec90 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ Some options work by reading the Elite: Dangerous game's “journal” files. If * Sends station commodity market, outfitting and shipyard data to “[EDDN](http://eddn-gateway.elite-markets.net/)” 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/), [Thrudd's Trading Tools](http://www.elitetradingtool.co.uk/), [Roguey's](http://roguey.co.uk/elite-dangerous/), etc. * System and scan data * Sends general system information and the results of your detailed planet scans to “[EDDN](http://eddn-gateway.elite-markets.net/)” from where you and others can use it via online prospecting tools such as [eddb](http://eddb.io/), [Inara](http://inara.cz), etc. + * You can choose to delay sending this information to EDDN until you're next safely docked at a station. Otherwise the information is sent as soon as you enter a system or perform a scan. ### EDSM diff --git a/config.py b/config.py index b51ca4f6..b5d60652 100644 --- a/config.py +++ b/config.py @@ -95,6 +95,7 @@ class Config: # OUT_SYS_AUTO = 512 # Now always automatic OUT_MKT_MANUAL = 1024 OUT_SYS_EDDN = 2048 + OUT_SYS_DELAY = 4096 if platform=='darwin': diff --git a/eddn.py b/eddn.py index a986d1d4..21d29caa 100644 --- a/eddn.py +++ b/eddn.py @@ -4,12 +4,20 @@ from collections import OrderedDict import hashlib import json import numbers +from os import SEEK_SET, SEEK_CUR, SEEK_END +from os.path import exists, join from platform import system import re import requests from sys import platform import time +if platform != 'win32': + from fcntl import lockf, LOCK_EX, LOCK_NB + +if __debug__: + from traceback import print_exc + from config import applongname, appversion, config from companion import category_map @@ -25,6 +33,38 @@ class _EDDN: def __init__(self): self.session = requests.Session() + self.replayfile = None # For delayed messages + + def load(self): + # Try to obtain exclusive access to the journal cache + filename = join(config.app_dir, 'replay.jsonl') + try: + try: + # Try to open existing file + self.replayfile = open(filename, 'r+') + except: + if exists(filename): + raise # Couldn't open existing file + else: + self.replayfile = open(filename, 'w+') # Create file + if platform != 'win32': # open for writing is automatically exclusive on Windows + lockf(self.replayfile, LOCK_EX|LOCK_NB) + except: + if __debug__: print_exc() + if self.replayfile: + self.replayfile.close() + self.replayfile = None + raise Exception("Error: Is another copy of this app already running?") # Shouldn't happen - don't bother localizing + + def flush(self): + self.replayfile.seek(0, SEEK_SET) + for line in self.replayfile: + self.send(*json.loads(line, object_pairs_hook=OrderedDict)) + self.replayfile.truncate(0) + + def close(self): + if self.replayfile: + self.replayfile.close() def send(self, cmdr, msg): msg['header'] = { @@ -96,10 +136,19 @@ class _EDDN: }) def export_journal_entry(self, cmdr, is_beta, entry): - self.send(cmdr, { - '$schemaRef' : 'http://schemas.elite-markets.net/eddn/journal/1' + (is_beta and '/test' or ''), - 'message' : entry - }) + if config.getint('output') & config.OUT_SYS_DELAY and self.replayfile and entry['event'] != 'Docked': + self.replayfile.seek(0, SEEK_END) + self.replayfile.write('%s\n' % json.dumps([cmdr.encode('utf-8'), { + '$schemaRef' : 'http://schemas.elite-markets.net/eddn/journal/1' + (is_beta and '/test' or ''), + 'message' : entry + }])) + self.replayfile.flush() + else: + self.flush() + self.send(cmdr, { + '$schemaRef' : 'http://schemas.elite-markets.net/eddn/journal/1' + (is_beta and '/test' or ''), + 'message' : entry + }) # singleton eddn = _EDDN() diff --git a/prefs.py b/prefs.py index 5606e610..6beef4f8 100644 --- a/prefs.py +++ b/prefs.py @@ -11,6 +11,7 @@ from ttkHyperlinkLabel import HyperlinkLabel import myNotebook as nb from config import applongname, config +from eddn import eddn from hotkey import hotkeymgr from l10n import Translations from monitor import monitor @@ -157,6 +158,9 @@ class PreferencesDialog(tk.Toplevel): self.eddn_system = tk.IntVar(value = (output & config.OUT_SYS_EDDN) and 1) self.eddn_system_button = nb.Checkbutton(eddnframe, text=_('Send system and scan data to the Elite Dangerous Data Network'), variable=self.eddn_system, command=self.outvarchanged) # Output setting new in E:D 2.2 self.eddn_system_button.grid(padx=BUTTONX, pady=(5,0), sticky=tk.W) + self.eddn_delay= tk.IntVar(value = (output & config.OUT_SYS_DELAY) and 1) + self.eddn_delay_button = nb.Checkbutton(eddnframe, text=_('Delay sending until docked'), variable=self.eddn_delay, command=self.outvarchanged) # Output setting under 'Send system and scan data to the Elite Dangerous Data Network' new in E:D 2.2 + self.eddn_delay_button.grid(padx=BUTTONX, sticky=tk.W) notebook.add(eddnframe, text='EDDN') # Not translated @@ -318,6 +322,7 @@ class PreferencesDialog(tk.Toplevel): self.eddn_auto_button['state'] = self.eddn_station.get() and logvalid and tk.NORMAL or tk.DISABLED 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 self.edsm_log.get() and tk.NORMAL or tk.DISABLED @@ -449,6 +454,7 @@ class PreferencesDialog(tk.Toplevel): (self.out_ship_coriolis.get() and config.OUT_SHIP_CORIOLIS) + (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)) config.set('outdir', self.outdir.get().startswith('~') and join(config.home, self.outdir.get()[2:]) or self.outdir.get()) From 7f2cf3a28636938a5efebbd40333759f220a351d Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Tue, 6 Sep 2016 12:26:59 +0100 Subject: [PATCH 07/21] Rationalise startup sequence --- EDMarketConnector.py | 37 +++++++++++++++++-------------------- L10n/en.template | 2 +- eddn.py | 3 ++- monitor.py | 16 +++++++++------- prefs.py | 3 --- 5 files changed, 29 insertions(+), 32 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index b647addf..231d121a 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -122,10 +122,7 @@ class AppWindow: self.theme_button.grid(row=row, columnspan=2, sticky=tk.NSEW) theme.register_alternate((self.button, self.theme_button), {'row':row, 'columnspan':2, 'sticky':tk.NSEW}) self.status.grid(columnspan=2, sticky=tk.EW) - theme.button_bind(self.theme_button, self.getandsend) - self.w.bind('', self.getandsend) - self.w.bind('', self.getandsend) for child in frame.winfo_children(): child.grid_configure(padx=5, pady=(platform=='win32' and 1 or 3)) @@ -240,21 +237,16 @@ class AppWindow: theme.register_highlight(self.station) theme.apply(self.w) - # Special handling for overrideredict - self.w.bind("", self.onmap) + self.w.bind("", self.onmap) # Special handling for overrideredict + self.w.bind('', self.getandsend) + self.w.bind('', self.getandsend) + self.w.bind_all('<>', self.getandsend) # Hotkey monitoring + self.w.bind_all('<>', self.journal_event) # Journal monitoring + self.w.bind_all('<>', self.onexit) # Updater # Load updater after UI creation (for WinSparkle) import update self.updater = update.Updater(self.w) - self.w.bind_all('<>', self.onexit) # user-generated - - # Install hotkey monitoring - self.w.bind_all('<>', self.getandsend) # user-generated - hotkeymgr.register(self.w, config.getint('hotkey_code'), config.getint('hotkey_mods')) - - # Install log monitoring - self.w.bind_all('<>', self.journal_event) # user-generated - monitor.start(self.w) # First run if not config.get('username') or not config.get('password'): @@ -309,15 +301,20 @@ class AppWindow: if __debug__: print_exc() self.status['text'] = unicode(e) - # Try to obtain exclusive lock on journal cache, even if we don't need it yet - try: - eddn.load() - except Exception as e: - self.status['text'] = unicode(e) - if not getattr(sys, 'frozen', False): self.updater.checkForUpdates() # Sparkle / WinSparkle does this automatically for packaged apps + # Try to obtain exclusive lock on journal cache, even if we don't need it yet + if not eddn.load(): + self.status['text'] = 'Error: Is another copy of this app already running?' # Shouldn't happen - don't bother localizing + + # (Re-)install hotkey monitoring + hotkeymgr.register(self.w, config.getint('hotkey_code'), config.getint('hotkey_mods')) + + # (Re-)install log monitoring + if not monitor.start(self.w): + self.status['text'] = 'Error: Check %s' % _('E:D journal file location') # Location of the new Journal file in E:D 2.2 + self.cooldown() # callback after verification code diff --git a/L10n/en.template b/L10n/en.template index 5a60690d..e725146a 100644 --- a/L10n/en.template +++ b/L10n/en.template @@ -109,7 +109,7 @@ /* Empire rank. [stats.py] */ "Duke" = "Duke"; -/* Location of the new Journal file in E:D 2.2. [prefs.py] */ +/* Location of the new Journal file in E:D 2.2. [EDMarketConnector.py] */ "E:D journal file location" = "E:D journal file location"; /* Empire rank. [stats.py] */ diff --git a/eddn.py b/eddn.py index 21d29caa..41ec1fda 100644 --- a/eddn.py +++ b/eddn.py @@ -54,7 +54,8 @@ class _EDDN: if self.replayfile: self.replayfile.close() self.replayfile = None - raise Exception("Error: Is another copy of this app already running?") # Shouldn't happen - don't bother localizing + return False + return True def flush(self): self.replayfile.seek(0, SEEK_SET) diff --git a/monitor.py b/monitor.py index bb62a33f..4b106706 100644 --- a/monitor.py +++ b/monitor.py @@ -96,6 +96,15 @@ class EDLogs(FileSystemEventHandler): self.stop() self.currentdir = logdir + # Latest pre-existing logfile - e.g. if E:D is already running. Assumes logs sort alphabetically. + # Do this before setting up the observer in case the journal directory has gone away + try: + logfiles = sorted([x for x in listdir(self.currentdir) if x.startswith('Journal.')]) + self.logfile = logfiles and join(self.currentdir, logfiles[-1]) or None + except: + self.logfile = None + return False + # Set up a watchog observer. This is low overhead so is left running irrespective of whether monitoring is desired. # File system events are unreliable/non-existent over network drives on Linux. # We can't easily tell whether a path points to a network drive, so assume @@ -110,13 +119,6 @@ class EDLogs(FileSystemEventHandler): if not self.observed and not polling: self.observed = self.observer.schedule(self, self.currentdir) - # Latest pre-existing logfile - e.g. if E:D is already running. Assumes logs sort alphabetically. - try: - logfiles = sorted([x for x in listdir(self.currentdir) if x.startswith('Journal.')]) - self.logfile = logfiles and join(self.currentdir, logfiles[-1]) or None - except: - self.logfile = None - if __debug__: print '%s "%s"' % (polling and 'Polling' or 'Monitoring', self.currentdir) print 'Start logfile "%s"' % self.logfile diff --git a/prefs.py b/prefs.py index 6beef4f8..75c83789 100644 --- a/prefs.py +++ b/prefs.py @@ -489,9 +489,6 @@ class PreferencesDialog(tk.Toplevel): self.callback() def _destroy(self): - # Re-enable hotkey and log monitoring before exit - hotkeymgr.register(self.parent, config.getint('hotkey_code'), config.getint('hotkey_mods')) - monitor.start(self.parent) self.parent.wm_attributes('-topmost', config.getint('always_ontop') and 1 or 0) self.destroy() From b21e181ab11b0ba831feba6a568bb0a257fe9694 Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Tue, 6 Sep 2016 12:32:25 +0100 Subject: [PATCH 08/21] Display current location on startup --- EDMarketConnector.py | 4 ++-- monitor.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 231d121a..25af6613 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -484,8 +484,6 @@ class AppWindow: def journal_event(self, event): while True: entry = monitor.get_entry() - if entry is None: - return system_changed = monitor.system and self.system['text'] != monitor.system station_changed = monitor.station and self.station['text'] != monitor.station @@ -495,6 +493,8 @@ class AppWindow: self.station['text'] = monitor.station or (EDDB.system(monitor.system) and self.STATION_UNDOCKED or '') if system_changed or station_changed: self.status['text'] = '' + if entry is None: + return plug.notify_journal_entry(monitor.cmdr, monitor.system, monitor.station, entry) diff --git a/monitor.py b/monitor.py index 4b106706..9729a680 100644 --- a/monitor.py +++ b/monitor.py @@ -164,6 +164,7 @@ class EDLogs(FileSystemEventHandler): except: if __debug__: print 'Invalid journal entry "%s"' % repr(line) + self.root.event_generate('<>', when="tail") # Generate null event to update the display at start else: loghandle = None From 9fdc5fb947d592f884d56d57087153df053a90ac Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Wed, 7 Sep 2016 15:02:02 +0100 Subject: [PATCH 09/21] Add mandatory StarSystem property to EDDN journal messages --- EDMarketConnector.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 25af6613..25ae6c54 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -529,7 +529,7 @@ class AppWindow: if (config.getint('output') & config.OUT_SYS_EDDN and monitor.cmdr and (entry['event'] == 'FSDJump' and system_changed or entry['event'] == 'Docked' and station_changed or - entry['event'] == 'Scan')): + entry['event'] == 'Scan' and monitor.system)): try: self.status['text'] = _('Sending data to EDDN...') @@ -540,6 +540,10 @@ class AppWindow: if thing.endswith('_Localised'): entry.pop(thing, None) + # add mandatory StarSystem property to Scan events + if 'StarSystem' not in entry: + entry['StarSystem'] = monitor.system + eddn.export_journal_entry(monitor.cmdr, monitor.is_beta, entry) self.status['text'] = '' From 4d3fe1808cd3be6364f446a60a9970f6df1d388c Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Mon, 12 Sep 2016 18:00:43 +0100 Subject: [PATCH 10/21] Drop Slopey's from the description It's no longer maintained. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ca32ec90..1be9a52a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Elite: Dangerous Market Connector (EDMC) This app downloads your Cmdr's data, system, scan and station data from the game [Elite: Dangerous](https://www.elitedangerous.com/) and, at your choice, either: * sends the station commodity market prices, other station data and system and scan 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, prospecting and shopping tools such as [eddb](http://eddb.io/), [Elite Trade Net](http://etn.io/), [Inara](http://inara.cz), [ED-TD](http://ed-td.space/), [Thrudd's Trading Tools](http://www.elitetradingtool.co.uk/), [Roguey's](http://roguey.co.uk/elite-dangerous/), etc. -* saves the station commodity market prices 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 the station commodity market prices to files on your computer that you can load into trading tools such as [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 to files on your computer that you can load into outfitting tools such as [E:D Shipyard](http://www.edshipyard.com), [Coriolis](http://coriolis.io) or [Elite Trade Net](http://etn.io/). * sends your flight log to [Elite: Dangerous Star Map](http://www.edsm.net/). From 6f4a3b37a32cb9b5f8b3b66eea133bdbf6f98aa1 Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Thu, 15 Sep 2016 17:03:42 +0100 Subject: [PATCH 11/21] Send blackmarket messages to EDDN --- EDMarketConnector.py | 51 +++++++++++++++++++++++++++++--------------- eddn.py | 7 ++++++ 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 25ae6c54..a72d27cb 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -3,6 +3,7 @@ import sys from sys import platform +from collections import OrderedDict from functools import partial import json from os import mkdir @@ -526,13 +527,11 @@ class AppWindow: self.w.after(int(SERVER_RETRY * 1000), self.getandsend) # Send interesting events to EDDN - if (config.getint('output') & config.OUT_SYS_EDDN and monitor.cmdr and - (entry['event'] == 'FSDJump' and system_changed or - entry['event'] == 'Docked' and station_changed or - entry['event'] == 'Scan' and monitor.system)): - try: - self.status['text'] = _('Sending data to EDDN...') - + try: + if (config.getint('output') & config.OUT_SYS_EDDN and monitor.cmdr and + (entry['event'] == 'FSDJump' and system_changed or + entry['event'] == 'Docked' and station_changed or + entry['event'] == 'Scan' and monitor.system)): # strip out properties disallowed by the schema for thing in ['CockpitBreach', 'BoostUsed', 'FuelLevel', 'FuelUsed', 'JumpDist']: entry.pop(thing, None) @@ -544,20 +543,38 @@ class AppWindow: if 'StarSystem' not in entry: entry['StarSystem'] = monitor.system + self.status['text'] = _('Sending data to EDDN...') eddn.export_journal_entry(monitor.cmdr, monitor.is_beta, entry) self.status['text'] = '' - except requests.exceptions.RequestException as e: - if __debug__: print_exc() - self.status['text'] = _("Error: Can't connect to EDDN") - if not config.getint('hotkey_mute'): - hotkeymgr.play_bad() + elif (config.getint('output') & config.OUT_MKT_EDDN and monitor.cmdr and + entry['event'] == 'MarketSell' and entry.get('BlackMarket')): + # Construct blackmarket message + msg = OrderedDict([ + ('systemName', monitor.system), + ('stationName', monitor.station), + ('timestamp', entry['timestamp']), + ('name', entry['Type']), + ('sellPrice', entry['SellPrice']), + ('prohibited' , entry.get('IllegalGoods', False)), + ]) + + self.status['text'] = _('Sending data to EDDN...') + eddn.export_blackmarket(monitor.cmdr, monitor.is_beta, msg) + self.status['text'] = '' + + except requests.exceptions.RequestException as e: + if __debug__: print_exc() + self.status['text'] = _("Error: Can't connect to EDDN") + if not config.getint('hotkey_mute'): + hotkeymgr.play_bad() + + except Exception as e: + if __debug__: print_exc() + self.status['text'] = unicode(e) + if not config.getint('hotkey_mute'): + hotkeymgr.play_bad() - except Exception as e: - if __debug__: print_exc() - self.status['text'] = unicode(e) - if not config.getint('hotkey_mute'): - hotkeymgr.play_bad() def edsmpoll(self): result = self.edsm.result diff --git a/eddn.py b/eddn.py index 41ec1fda..2d00a84b 100644 --- a/eddn.py +++ b/eddn.py @@ -151,5 +151,12 @@ class _EDDN: 'message' : entry }) + def export_blackmarket(self, cmdr, is_beta, msg): + self.send(cmdr, { + '$schemaRef' : 'http://schemas.elite-markets.net/eddn/blackmarket/1' + (is_beta and '/test' or ''), + 'message' : msg + }) + + # singleton eddn = _EDDN() From add254978d3ca9934982a9537b003d2791d5afa9 Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Fri, 16 Sep 2016 17:48:56 +0100 Subject: [PATCH 12/21] Update translations --- L10n/cs.strings | 4 ++-- L10n/de.strings | 4 ++-- L10n/es.strings | 4 ++-- L10n/fr.strings | 13 +++++++++++-- L10n/ja.strings | 4 ++-- L10n/nl.strings | 4 ++-- L10n/pl.strings | 10 ++++++++-- L10n/ru.strings | 4 ++-- L10n/uk.strings | 4 ++-- 9 files changed, 33 insertions(+), 18 deletions(-) diff --git a/L10n/cs.strings b/L10n/cs.strings index 71beb215..5b515000 100644 --- a/L10n/cs.strings +++ b/L10n/cs.strings @@ -109,8 +109,8 @@ /* Empire rank. [stats.py] */ "Duke" = "Duke"; -/* Configuration setting. [prefs.py] */ -"E:D log file location" = "Umístění E:D log souboru "; +/* Location of the new Journal file in E:D 2.2. [EDMarketConnector.py] */ +"E:D journal file location" = "E:D umístění souboru deníku"; /* Empire rank. [stats.py] */ "Earl" = "Earl"; diff --git a/L10n/de.strings b/L10n/de.strings index fa6f2a89..b82755b0 100644 --- a/L10n/de.strings +++ b/L10n/de.strings @@ -109,8 +109,8 @@ /* Empire rank. [stats.py] */ "Duke" = "Herzog"; -/* Configuration setting. [prefs.py] */ -"E:D log file location" = "E:D Logdatei Speicherort"; +/* Location of the new Journal file in E:D 2.2. [EDMarketConnector.py] */ +"E:D journal file location" = "E:D Journal Dateispeicherort"; /* Empire rank. [stats.py] */ "Earl" = "Earl"; diff --git a/L10n/es.strings b/L10n/es.strings index 918d0bd5..958db532 100644 --- a/L10n/es.strings +++ b/L10n/es.strings @@ -109,8 +109,8 @@ /* Empire rank. [stats.py] */ "Duke" = "Duque"; -/* Configuration setting. [prefs.py] */ -"E:D log file location" = "Localización del log de E:D"; +/* Location of the new Journal file in E:D 2.2. [EDMarketConnector.py] */ +"E:D journal file location" = "Localización del archivo de Journal de E:D"; /* Empire rank. [stats.py] */ "Earl" = "Conde Palatino"; diff --git a/L10n/fr.strings b/L10n/fr.strings index fe1e3436..de2e00f0 100644 --- a/L10n/fr.strings +++ b/L10n/fr.strings @@ -25,6 +25,9 @@ /* Tab heading in settings. [prefs.py] */ "Appearance" = "Apparence"; +/* Output setting. [prefs.py] */ +"Automatically update on docking" = "Mettre à jour automatiquement à l'amarrage"; + /* Cmdr stats. [stats.py] */ "Balance" = "Solde"; @@ -100,11 +103,14 @@ /* Appearance theme and language setting. [l10n.py] */ "Default" = "Par défaut"; +/* Output setting under 'Send system and scan data to the Elite Dangerous Data Network' new in E:D 2.2. [prefs.py] */ +"Delay sending until docked" = "Retarder l'envoi jusqu'à l'amarrage"; + /* Empire rank. [stats.py] */ "Duke" = "Archiduc"; -/* Configuration setting. [prefs.py] */ -"E:D log file location" = "Emplacement des fichiers de log E:D"; +/* Location of the new Journal file in E:D 2.2. [EDMarketConnector.py] */ +"E:D journal file location" = "Emplacement du journal E:D"; /* Empire rank. [stats.py] */ "Earl" = "Marquis"; @@ -376,6 +382,9 @@ /* Output setting. [prefs.py] */ "Send station data to the Elite Dangerous Data Network" = "Envoyer les données de la station à Elite Dangerous Data Network"; +/* Output setting new in E:D 2.2. [prefs.py] */ +"Send system and scan data to the Elite Dangerous Data Network" = "Envoyer les systèmes et les informations de scan à Elite Dangerous Data Network"; + /* [EDMarketConnector.py] */ "Sending data to EDDN..." = "Envoi des données à EDDN..."; diff --git a/L10n/ja.strings b/L10n/ja.strings index 718c510f..26dce213 100644 --- a/L10n/ja.strings +++ b/L10n/ja.strings @@ -109,8 +109,8 @@ /* Empire rank. [stats.py] */ "Duke" = "Duke"; -/* Configuration setting. [prefs.py] */ -"E:D log file location" = "E:Dゲームクライアントのログ出力先"; +/* Location of the new Journal file in E:D 2.2. [EDMarketConnector.py] */ +"E:D journal file location" = "E:Dゲームクライアントのジャーナルファイル出力先"; /* Empire rank. [stats.py] */ "Earl" = "Earl"; diff --git a/L10n/nl.strings b/L10n/nl.strings index 9f5e831b..3f7479be 100644 --- a/L10n/nl.strings +++ b/L10n/nl.strings @@ -109,8 +109,8 @@ /* Empire rank. [stats.py] */ "Duke" = "Duke"; -/* Configuration setting. [prefs.py] */ -"E:D log file location" = "E:D log bestand locatie"; +/* Location of the new Journal file in E:D 2.2. [EDMarketConnector.py] */ +"E:D journal file location" = "E:D journaal bestand locatie"; /* Empire rank. [stats.py] */ "Earl" = "Earl"; diff --git a/L10n/pl.strings b/L10n/pl.strings index 557acf06..0864e553 100644 --- a/L10n/pl.strings +++ b/L10n/pl.strings @@ -103,11 +103,14 @@ /* Appearance theme and language setting. [l10n.py] */ "Default" = "Domyślne"; +/* Output setting under 'Send system and scan data to the Elite Dangerous Data Network' new in E:D 2.2. [prefs.py] */ +"Delay sending until docked" = "Czekaj z wysłaniem na zadokowanie"; + /* Empire rank. [stats.py] */ "Duke" = "Duke"; -/* Configuration setting. [prefs.py] */ -"E:D log file location" = "E:D położenie pliku dziennika"; +/* Location of the new Journal file in E:D 2.2. [EDMarketConnector.py] */ +"E:D journal file location" = "E:D Lokacja pliku dziennika"; /* Empire rank. [stats.py] */ "Earl" = "Earl"; @@ -379,6 +382,9 @@ /* Output setting. [prefs.py] */ "Send station data to the Elite Dangerous Data Network" = "Wyślij dane do Elite Dangerous Data Network"; +/* Output setting new in E:D 2.2. [prefs.py] */ +"Send system and scan data to the Elite Dangerous Data Network" = "Wyślij dane skanowań do EDDN"; + /* [EDMarketConnector.py] */ "Sending data to EDDN..." = "Wysłanie danych do EDDN..."; diff --git a/L10n/ru.strings b/L10n/ru.strings index 5dc8095f..5d8c274c 100644 --- a/L10n/ru.strings +++ b/L10n/ru.strings @@ -109,8 +109,8 @@ /* Empire rank. [stats.py] */ "Duke" = "Герцог"; -/* Configuration setting. [prefs.py] */ -"E:D log file location" = "Расположение файла журнала E:D"; +/* Location of the new Journal file in E:D 2.2. [EDMarketConnector.py] */ +"E:D journal file location" = "Расположение файла журнала E:D"; /* Empire rank. [stats.py] */ "Earl" = "Эрл"; diff --git a/L10n/uk.strings b/L10n/uk.strings index 52f03809..baa4e2c7 100644 --- a/L10n/uk.strings +++ b/L10n/uk.strings @@ -109,8 +109,8 @@ /* Empire rank. [stats.py] */ "Duke" = "Герцог"; -/* Configuration setting. [prefs.py] */ -"E:D log file location" = "Розміщення файлу-журналу (log E:D)"; +/* Location of the new Journal file in E:D 2.2. [EDMarketConnector.py] */ +"E:D journal file location" = "Росташування файлу-журналу E:D"; /* Empire rank. [stats.py] */ "Earl" = "Дворянин"; From 3dcd72410498e23b942b46bffd01fecf3690096d Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Thu, 22 Sep 2016 17:26:43 +0100 Subject: [PATCH 13/21] Fix Journal path --- config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config.py b/config.py index b5d60652..82118e48 100644 --- a/config.py +++ b/config.py @@ -108,7 +108,7 @@ class Config: if not isdir(self.plugin_dir): mkdir(self.plugin_dir) - self.default_journal_dir = join(NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, True)[0], 'Frontier Developments', 'Elite Dangerous', 'Logs') # FIXME: check this + self.default_journal_dir = join(NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, True)[0], 'Frontier Developments', 'Elite Dangerous') self.home = expanduser('~') @@ -171,11 +171,11 @@ class Config: # expanduser in Python 2 on Windows doesn't handle non-ASCII - http://bugs.python.org/issue13207 SHGetKnownFolderPath(ctypes.create_string_buffer(FOLDERID_Profile.bytes_le), 0, 0, ctypes.byref(buf)) - self.home = buf.value + self.home = buf.value or u'\\' CoTaskMemFree(buf) SHGetKnownFolderPath(ctypes.create_string_buffer(FOLDERID_SavedGames.bytes_le), 0, 0, ctypes.byref(buf)) - self.default_journal_dir = buf.value + self.default_journal_dir = buf.value and join(buf.value, 'Frontier Developments', 'Elite Dangerous') or None CoTaskMemFree(buf) self.respath = dirname(getattr(sys, 'frozen', False) and sys.executable or __file__) From 3ece764486ab3872cc3786c94adbfd95571b7d32 Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Thu, 22 Sep 2016 17:48:29 +0100 Subject: [PATCH 14/21] Remember system coordinates on undocking --- monitor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/monitor.py b/monitor.py index 9729a680..4f1a6b21 100644 --- a/monitor.py +++ b/monitor.py @@ -211,7 +211,7 @@ class EDLogs(FileSystemEventHandler): try: entry = json.loads(line, object_pairs_hook=OrderedDict) # Preserve property order because why not? entry['timestamp'] # we expect this to exist - if entry['event'] == 'Fileheader': # XXX or 'fileheader' ? + if entry['event'] == 'Fileheader': self.version = entry['gameversion'] self.is_beta = 'beta' in entry['gameversion'].lower() elif entry['event'] == 'LoadGame': @@ -221,7 +221,6 @@ class EDLogs(FileSystemEventHandler): self.cmdr = entry['Name'] elif entry['event'] in ['Undocked']: self.station = None - self.coordinates = None elif entry['event'] in ['Location', 'FSDJump', 'Docked']: if 'StarPos' in entry: self.coordinates = tuple(entry['StarPos']) From e8e140199547542b5ac1755dd5df66060a421c10 Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Thu, 22 Sep 2016 23:51:55 +0100 Subject: [PATCH 15/21] Increase journal monitor frequency --- monitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitor.py b/monitor.py index 4f1a6b21..b8796deb 100644 --- a/monitor.py +++ b/monitor.py @@ -60,7 +60,7 @@ else: class EDLogs(FileSystemEventHandler): - _POLL = 5 # New system gets posted to log file before hyperspace ends, so don't need to poll too often + _POLL = 1 # Polling is cheap, so do it often def __init__(self): FileSystemEventHandler.__init__(self) # futureproofing - not need for current version of watchdog From e805f94573a6c60ea90466688d3f3cb0cd8b767f Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Fri, 23 Sep 2016 00:16:37 +0100 Subject: [PATCH 16/21] Don't send any messages while in CQC --- EDMarketConnector.py | 7 +++++-- monitor.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index a72d27cb..38a61e06 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -335,6 +335,9 @@ class AppWindow: auto_update = not event play_sound = (auto_update or int(event.type) == self.EVENT_VIRTUAL) and not config.getint('hotkey_mute') + if monitor.cmdr and not monitor.mode: + return # In CQC - do nothing + if not retrying: if time() < self.holdofftime: # Was invoked by key while in cooldown self.status['text'] = '' @@ -494,8 +497,8 @@ class AppWindow: self.station['text'] = monitor.station or (EDDB.system(monitor.system) and self.STATION_UNDOCKED or '') if system_changed or station_changed: self.status['text'] = '' - if entry is None: - return + if not entry or not monitor.mode: + return # Fake event or in CQC plug.notify_journal_entry(monitor.cmdr, monitor.system, monitor.station, entry) diff --git a/monitor.py b/monitor.py index b8796deb..547368e4 100644 --- a/monitor.py +++ b/monitor.py @@ -216,7 +216,7 @@ class EDLogs(FileSystemEventHandler): self.is_beta = 'beta' in entry['gameversion'].lower() elif entry['event'] == 'LoadGame': self.cmdr = entry['Commander'] - self.mode = entry['GameMode'] + self.mode = entry.get('GameMode') # 'Open', 'Solo', 'Group', or None for CQC elif entry['event'] == 'NewCommander': self.cmdr = entry['Name'] elif entry['event'] in ['Undocked']: From 5b041dfc77ca7e55a1a27e3fe168138509bfee6a Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Fri, 23 Sep 2016 00:53:46 +0100 Subject: [PATCH 17/21] Add new ships in E:D 2.2 --- companion.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/companion.py b/companion.py index 0052d66a..33b18d59 100644 --- a/companion.py +++ b/companion.py @@ -77,6 +77,7 @@ ship_map = { 'anaconda' : 'Anaconda', 'asp' : 'Asp Explorer', 'asp_scout' : 'Asp Scout', + 'belugaliner' : 'Beluga Liner', 'cobramkiii' : 'Cobra MkIII', 'cobramkiv' : 'Cobra MkIV', 'cutter' : 'Imperial Cutter', @@ -97,6 +98,7 @@ ship_map = { 'independant_trader' : 'Keelback', 'orca' : 'Orca', 'python' : 'Python', + 'scout' : 'Taipan Fighter', 'sidewinder' : 'Sidewinder', 'type6' : 'Type-6 Transporter', 'type7' : 'Type-7 Transporter', @@ -166,7 +168,7 @@ class Session: self.state = Session.STATE_INIT self.credentials = None - # yuck suppress InsecurePlatformWarning + # yuck suppress InsecurePlatformWarning under Python < 2.7.9 which lacks SNI support try: from requests.packages import urllib3 urllib3.disable_warnings() From 15eeda23094f215b382cb7bc78e8e204dcdbb606 Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Fri, 23 Sep 2016 00:55:03 +0100 Subject: [PATCH 18/21] 2.20 beta 0 --- config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.py b/config.py index 82118e48..143cd59a 100644 --- a/config.py +++ b/config.py @@ -8,7 +8,7 @@ from sys import platform appname = 'EDMarketConnector' applongname = 'E:D Market Connector' appcmdname = 'EDMC' -appversion = '2.1.7.2' +appversion = '2.2.0.0' update_feed = 'https://marginal.org.uk/edmarketconnector.xml' update_interval = 47*60*60 From b18589f7bfa36a27fbbb1fecbb3228088b6a3c88 Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Sat, 24 Sep 2016 00:12:20 +0100 Subject: [PATCH 19/21] Disable API access if Journal says we're in beta --- EDMarketConnector.py | 2 +- prefs.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 38a61e06..9d3d5965 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -335,7 +335,7 @@ class AppWindow: auto_update = not event play_sound = (auto_update or int(event.type) == self.EVENT_VIRTUAL) and not config.getint('hotkey_mute') - if monitor.cmdr and not monitor.mode: + if (monitor.cmdr and not monitor.mode) or monitor.is_beta: return # In CQC - do nothing if not retrying: diff --git a/prefs.py b/prefs.py index 75c83789..f8c9a396 100644 --- a/prefs.py +++ b/prefs.py @@ -315,12 +315,12 @@ class PreferencesDialog(tk.Toplevel): logvalid = logdir and exists(logdir) 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() - self.out_auto_button['state'] = local and logvalid and tk.NORMAL or tk.DISABLED + self.out_auto_button['state'] = local and logvalid and not monitor.is_beta and tk.NORMAL or tk.DISABLED self.outdir_label['state'] = local and tk.NORMAL or tk.DISABLED self.outbutton['state'] = local and tk.NORMAL or tk.DISABLED self.outdir['state'] = local and 'readonly' or tk.DISABLED - self.eddn_auto_button['state'] = self.eddn_station.get() and logvalid and tk.NORMAL or tk.DISABLED + self.eddn_auto_button['state'] = self.eddn_station.get() and logvalid and not monitor.is_beta and tk.NORMAL or tk.DISABLED 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 From 6519f09495120f1528090b8965956c12622f8bd4 Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Sun, 2 Oct 2016 13:19:22 +0100 Subject: [PATCH 20/21] Tidy use of SHGetKnownFolderPath on Windows Fixes #144 --- config.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/config.py b/config.py index 143cd59a..39b4335b 100644 --- a/config.py +++ b/config.py @@ -74,6 +74,15 @@ elif platform=='win32': RegDeleteValue.restype = LONG RegDeleteValue.argtypes = [HKEY, LPCWSTR] + def KnownFolderPath(guid): + buf = ctypes.c_wchar_p() + if SHGetKnownFolderPath(ctypes.create_string_buffer(guid.bytes_le), 0, 0, ctypes.byref(buf)): + return None + retval = buf.value # copy data + CoTaskMemFree(buf) # and free original + return retval + + elif platform=='linux2': import codecs # requires python-iniparse package - ConfigParser that ships with Python < 3.2 doesn't support unicode @@ -158,25 +167,19 @@ class Config: def __init__(self): - buf = ctypes.c_wchar_p() - SHGetKnownFolderPath(ctypes.create_string_buffer(FOLDERID_LocalAppData.bytes_le), 0, 0, ctypes.byref(buf)) - self.app_dir = join(buf.value, appname) + self.app_dir = join(KnownFolderPath(FOLDERID_LocalAppData), appname) if not isdir(self.app_dir): mkdir(self.app_dir) - CoTaskMemFree(buf) - + self.plugin_dir = join(self.app_dir, 'plugins') if not isdir(self.plugin_dir): mkdir(self.plugin_dir) # expanduser in Python 2 on Windows doesn't handle non-ASCII - http://bugs.python.org/issue13207 - SHGetKnownFolderPath(ctypes.create_string_buffer(FOLDERID_Profile.bytes_le), 0, 0, ctypes.byref(buf)) - self.home = buf.value or u'\\' - CoTaskMemFree(buf) + self.home = KnownFolderPath(FOLDERID_Profile) or u'\\' - SHGetKnownFolderPath(ctypes.create_string_buffer(FOLDERID_SavedGames.bytes_le), 0, 0, ctypes.byref(buf)) - self.default_journal_dir = buf.value and join(buf.value, 'Frontier Developments', 'Elite Dangerous') or None - CoTaskMemFree(buf) + journaldir = KnownFolderPath(FOLDERID_SavedGames) + self.default_journal_dir = journaldir and join(journaldir, 'Frontier Developments', 'Elite Dangerous') or None self.respath = dirname(getattr(sys, 'frozen', False) and sys.executable or __file__) @@ -205,10 +208,7 @@ class Config: RegCloseKey(sparklekey) if not self.get('outdir') or not isdir(self.get('outdir')): - buf = ctypes.create_unicode_buffer(MAX_PATH) - SHGetKnownFolderPath(ctypes.create_string_buffer(FOLDERID_Documents.bytes_le), 0, 0, ctypes.byref(buf)) - self.set('outdir', buf.value) - CoTaskMemFree(buf) + self.set('outdir', KnownFolderPath(FOLDERID_Documents)) def get(self, key): typ = DWORD() From e70ef584127bf6fc49520c42dfa19109f7f0b82e Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Sun, 2 Oct 2016 13:23:58 +0100 Subject: [PATCH 21/21] Point EDDN links to its wiki --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1be9a52a..2440135b 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Elite: Dangerous Market Connector (EDMC) This app downloads your Cmdr's data, system, scan and station data from the game [Elite: Dangerous](https://www.elitedangerous.com/) and, at your choice, either: -* sends the station commodity market prices, other station data and system and scan 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, prospecting and shopping tools such as [eddb](http://eddb.io/), [Elite Trade Net](http://etn.io/), [Inara](http://inara.cz), [ED-TD](http://ed-td.space/), [Thrudd's Trading Tools](http://www.elitetradingtool.co.uk/), [Roguey's](http://roguey.co.uk/elite-dangerous/), etc. +* sends the station commodity market prices, other station data and system and scan data to the [Elite Dangerous Data Network](https://github.com/jamesremuscat/EDDN/wiki) (“EDDN”) from where you and others can use it via online trading, prospecting and shopping tools such as [eddb](http://eddb.io/), [Elite Trade Net](http://etn.io/), [Inara](http://inara.cz), [ED-TD](http://ed-td.space/), [Thrudd's Trading Tools](http://www.elitetradingtool.co.uk/), [Roguey's](http://roguey.co.uk/elite-dangerous/), etc. * saves the station commodity market prices to files on your computer that you can load into trading tools such as [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 to files on your computer that you can load into outfitting tools such as [E:D Shipyard](http://www.edshipyard.com), [Coriolis](http://coriolis.io) or [Elite Trade Net](http://etn.io/). * sends your flight log to [Elite: Dangerous Star Map](http://www.edsm.net/). @@ -74,9 +74,9 @@ Some options work by reading the Elite: Dangerous game's “journal” files. If ### EDDN * Station data - * Sends station commodity market, outfitting and shipyard data to “[EDDN](http://eddn-gateway.elite-markets.net/)” 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/), [Thrudd's Trading Tools](http://www.elitetradingtool.co.uk/), [Roguey's](http://roguey.co.uk/elite-dangerous/), etc. + * Sends station commodity market, outfitting and shipyard data to “[EDDN](https://github.com/jamesremuscat/EDDN/wiki)” 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/), [Thrudd's Trading Tools](http://www.elitetradingtool.co.uk/), [Roguey's](http://roguey.co.uk/elite-dangerous/), etc. * System and scan data - * Sends general system information and the results of your detailed planet scans to “[EDDN](http://eddn-gateway.elite-markets.net/)” from where you and others can use it via online prospecting tools such as [eddb](http://eddb.io/), [Inara](http://inara.cz), etc. + * Sends general system information and the results of your detailed planet scans to “[EDDN](https://github.com/jamesremuscat/EDDN/wiki)” from where you and others can use it via online prospecting tools such as [eddb](http://eddb.io/), [Inara](http://inara.cz), etc. * You can choose to delay sending this information to EDDN until you're next safely docked at a station. Otherwise the information is sent as soon as you enter a system or perform a scan. ### EDSM @@ -219,7 +219,7 @@ Acknowledgements * Thanks to Armando Ota for the Slovenian translation. * Thanks to Cmdr Mila Strelok for the Spanish translation. * Thanks to Taras Velychko for the Ukranian translation. -* Thanks to [James Muscat](https://github.com/jamesremuscat) for [EDDN](https://github.com/jamesremuscat/EDDN) and to [Cmdr Anthor](https://github.com/AnthorNet) for the [stats](http://eddn-gateway.elite-markets.net/). +* Thanks to [James Muscat](https://github.com/jamesremuscat) for [EDDN](https://github.com/jamesremuscat/EDDN/wiki) and to [Cmdr Anthor](https://github.com/AnthorNet) for the [stats](http://eddn-gateway.elite-markets.net/). * Thanks to [Andargor](https://github.com/Andargor) for the idea of using the “Companion” interface in [edce-client](https://github.com/Andargor/edce-client). * Uses [Sparkle](https://github.com/sparkle-project/Sparkle) by [Andy Matuschak](http://andymatuschak.org/) and the [Sparkle Project](https://github.com/sparkle-project). * Uses [WinSparkle](https://github.com/vslavik/winsparkle/wiki) by [Václav Slavík](https://github.com/vslavik).