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)