mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2025-04-17 17:42:20 +03:00
Merge branch '2.2'
This commit is contained in:
commit
f94e9042de
@ -3,13 +3,15 @@
|
||||
|
||||
import sys
|
||||
from sys import platform
|
||||
from collections import OrderedDict
|
||||
from functools import partial
|
||||
import json
|
||||
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 +77,6 @@ class AppWindow:
|
||||
self.w.rowconfigure(0, weight=1)
|
||||
self.w.columnconfigure(0, weight=1)
|
||||
|
||||
# Special handling for overrideredict
|
||||
self.w.bind("<Map>", self.onmap)
|
||||
|
||||
plug.load_plugins()
|
||||
|
||||
if platform != 'darwin':
|
||||
@ -124,10 +123,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('<Return>', self.getandsend)
|
||||
self.w.bind('<KP_Enter>', self.getandsend)
|
||||
|
||||
for child in frame.winfo_children():
|
||||
child.grid_configure(padx=5, pady=(platform=='win32' and 1 or 3))
|
||||
@ -242,19 +238,16 @@ class AppWindow:
|
||||
theme.register_highlight(self.station)
|
||||
theme.apply(self.w)
|
||||
|
||||
self.w.bind("<Map>", self.onmap) # Special handling for overrideredict
|
||||
self.w.bind('<Return>', self.getandsend)
|
||||
self.w.bind('<KP_Enter>', self.getandsend)
|
||||
self.w.bind_all('<<Invoke>>', self.getandsend) # Hotkey monitoring
|
||||
self.w.bind_all('<<JournalEvent>>', self.journal_event) # Journal monitoring
|
||||
self.w.bind_all('<<Quit>>', self.onexit) # Updater
|
||||
|
||||
# Load updater after UI creation (for WinSparkle)
|
||||
import update
|
||||
self.updater = update.Updater(self.w)
|
||||
self.w.bind_all('<<Quit>>', self.onexit) # user-generated
|
||||
|
||||
# Install hotkey monitoring
|
||||
self.w.bind_all('<<Invoke>>', self.getandsend) # user-generated
|
||||
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)
|
||||
monitor.start(self.w)
|
||||
|
||||
# First run
|
||||
if not config.get('username') or not config.get('password'):
|
||||
@ -312,6 +305,17 @@ class AppWindow:
|
||||
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
|
||||
@ -331,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) or monitor.is_beta:
|
||||
return # In CQC - do nothing
|
||||
|
||||
if not retrying:
|
||||
if time() < self.holdofftime: # Was invoked by key while in cooldown
|
||||
self.status['text'] = ''
|
||||
@ -356,6 +363,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 (monitor.logfile and self.system['text'] and data['lastSystem']['name'] != self.system['text'])):
|
||||
raise companion.ServerLagging()
|
||||
|
||||
@ -379,24 +388,20 @@ 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
|
||||
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,100 @@ 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()
|
||||
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 '')
|
||||
if system_changed or station_changed:
|
||||
self.status['text'] = ''
|
||||
if not entry or not monitor.mode:
|
||||
return # Fake event or in CQC
|
||||
|
||||
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'))
|
||||
|
||||
# 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'] = ''
|
||||
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.edsmpoll()
|
||||
|
||||
# Auto-Update after docking
|
||||
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
|
||||
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)
|
||||
for thing in entry.keys():
|
||||
if thing.endswith('_Localised'):
|
||||
entry.pop(thing, None)
|
||||
|
||||
# add mandatory StarSystem property to Scan events
|
||||
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'] = ''
|
||||
|
||||
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()
|
||||
|
||||
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
|
||||
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()
|
||||
|
||||
def edsmpoll(self):
|
||||
result = self.edsm.result
|
||||
@ -573,6 +647,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()
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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. [EDMarketConnector.py] */
|
||||
"E:D journal file location" = "E:D journal file location";
|
||||
|
||||
/* Empire rank. [stats.py] */
|
||||
"Earl" = "Earl";
|
||||
|
@ -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";
|
||||
|
@ -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...";
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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...";
|
||||
|
||||
|
@ -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" = "Эрл";
|
||||
|
@ -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" = "Дворянин";
|
||||
|
18
PLUGINS.md
18
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):
|
||||
|
31
README.md
31
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](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/).
|
||||
|
||||
@ -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).
|
||||
@ -70,7 +69,15 @@ 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
|
||||
|
||||
* Station data
|
||||
* 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](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
|
||||
|
||||
@ -120,13 +127,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
|
||||
--------
|
||||
@ -218,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).
|
||||
|
@ -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()
|
||||
|
53
config.py
53
config.py
@ -1,14 +1,14 @@
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
@ -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
|
||||
@ -67,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
|
||||
@ -83,9 +99,12 @@ 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
|
||||
OUT_SYS_EDDN = 2048
|
||||
OUT_SYS_DELAY = 4096
|
||||
|
||||
if platform=='darwin':
|
||||
|
||||
@ -98,6 +117,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')
|
||||
|
||||
self.home = expanduser('~')
|
||||
|
||||
self.respath = getattr(sys, 'frozen', False) and normpath(join(dirname(sys.executable), pardir, 'Resources')) or dirname(__file__)
|
||||
@ -146,19 +167,19 @@ class Config:
|
||||
|
||||
def __init__(self):
|
||||
|
||||
buf = ctypes.create_unicode_buffer(MAX_PATH)
|
||||
ctypes.windll.shell32.SHGetSpecialFolderPathW(0, buf, CSIDL_LOCAL_APPDATA, 0)
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
self.home = buf.value
|
||||
self.home = KnownFolderPath(FOLDERID_Profile) or u'\\'
|
||||
|
||||
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__)
|
||||
|
||||
@ -187,9 +208,7 @@ class Config:
|
||||
RegCloseKey(sparklekey)
|
||||
|
||||
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)
|
||||
self.set('outdir', buf.value)
|
||||
self.set('outdir', KnownFolderPath(FOLDERID_Documents))
|
||||
|
||||
def get(self, key):
|
||||
typ = DWORD()
|
||||
@ -251,6 +270,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__)
|
||||
|
66
eddn.py
66
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,39 @@ 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
|
||||
return False
|
||||
return True
|
||||
|
||||
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'] = {
|
||||
@ -32,7 +73,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 +136,27 @@ class _EDDN:
|
||||
}
|
||||
})
|
||||
|
||||
def export_journal_entry(self, cmdr, is_beta, 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
|
||||
})
|
||||
|
||||
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()
|
||||
|
356
monitor.py
356
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
|
||||
@ -59,64 +60,26 @@ 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
|
||||
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
|
||||
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
|
||||
|
||||
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('<network')
|
||||
end = content.find('</network>')
|
||||
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('<AppConfig>\n\t<Network\n\t\tVerboseLogging="1"\n\t>\n\t</Network>\n</AppConfig>\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('<network')
|
||||
if start >= 0:
|
||||
f.write(content[:start+8] + '\n\t\tVerboseLogging="1"' + content[start+8:])
|
||||
else:
|
||||
start = content.lower().find("</appconfig>")
|
||||
if start >= 0:
|
||||
f.write(content[:start] + '\t<Network\n\t\tVerboseLogging="1"\n\t>\n\t</Network>\n' + content[start:])
|
||||
else:
|
||||
f.write(content) # eh ?
|
||||
return False
|
||||
|
||||
return self.logging_enabled_in_file(appconf)
|
||||
except:
|
||||
if __debug__: print_exc()
|
||||
return False
|
||||
# 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:
|
||||
@ -124,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
|
||||
|
||||
@ -133,18 +96,20 @@ 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('<<MonitorJump>>', self.jump) # user-generated
|
||||
self.root.bind_all('<<MonitorDock>>', self.dock) # user-generated
|
||||
# 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
|
||||
# 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
|
||||
@ -154,19 +119,12 @@ 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(logdir) if x.startswith('netLog.')])
|
||||
self.logfile = logfiles and join(logdir, 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()
|
||||
|
||||
@ -176,18 +134,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):
|
||||
@ -195,30 +154,17 @@ 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)
|
||||
self.root.event_generate('<<JournalEvent>>', when="tail") # Generate null event to update the display at start
|
||||
else:
|
||||
loghandle = None
|
||||
|
||||
@ -227,19 +173,13 @@ class EDLogs(FileSystemEventHandler):
|
||||
|
||||
while True:
|
||||
|
||||
if docked and not updated and not config.getint('output') & config.OUT_MKT_MANUAL:
|
||||
self.root.event_generate('<<MonitorDock>>', 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()
|
||||
@ -255,37 +195,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('<<MonitorJump>>', when="tail")
|
||||
self.event_queue.append(line)
|
||||
if self.event_queue:
|
||||
self.root.event_generate('<<JournalEvent>>', when="tail")
|
||||
|
||||
sleep(self._POLL)
|
||||
|
||||
@ -293,166 +207,38 @@ 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 dock(self, event):
|
||||
# Called from Tkinter's main loop
|
||||
if self.callbacks['Dock']:
|
||||
self.callbacks['Dock']()
|
||||
|
||||
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'))
|
||||
|
||||
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':
|
||||
|
||||
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')
|
||||
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':
|
||||
self.version = entry['gameversion']
|
||||
self.is_beta = 'beta' in entry['gameversion'].lower()
|
||||
elif entry['event'] == 'LoadGame':
|
||||
self.cmdr = entry['Commander']
|
||||
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']:
|
||||
self.station = 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 get_entry(self):
|
||||
if not self.event_queue:
|
||||
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'))
|
||||
|
||||
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):
|
||||
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'))
|
||||
else:
|
||||
return self.parse_entry(self.event_queue.pop(0))
|
||||
|
||||
|
||||
# singleton
|
||||
|
21
plug.py
21
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")
|
||||
|
@ -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):
|
||||
|
66
prefs.py
66
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
|
||||
@ -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
|
||||
@ -113,11 +114,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 +148,30 @@ 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)
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
@ -179,7 +195,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):
|
||||
@ -190,13 +206,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)
|
||||
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)
|
||||
@ -295,14 +312,18 @@ 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 or self.out_eddn.get()) 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 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
|
||||
|
||||
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
|
||||
@ -350,12 +371,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()
|
||||
|
||||
@ -425,13 +446,15 @@ 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.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())
|
||||
|
||||
@ -439,10 +462,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)
|
||||
@ -466,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()
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user