1
0
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:
Jonathan Harris 2016-10-08 11:28:48 +01:00
commit f94e9042de
20 changed files with 438 additions and 428 deletions

View File

@ -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()

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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...";

View File

@ -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";

View File

@ -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";

View File

@ -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...";

View File

@ -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" = "Эрл";

View File

@ -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" = "Дворянин";

View File

@ -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):

View File

@ -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&nbsp;Shipyard](http://www.edshipyard.com), [Coriolis](http://coriolis.io) or [Elite Trade Net](http://etn.io/).
* sends your flight log to [Elite:&nbsp;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).

View File

@ -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()

View File

@ -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
View File

@ -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()

View File

@ -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
View File

@ -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")

View File

@ -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):

View File

@ -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()