1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-04-17 17:42:20 +03:00

Add interaction() plugin callback

Fixes #220
This commit is contained in:
Jonathan Harris 2017-06-18 02:32:25 +01:00
parent cf9ff04082
commit 1e51d2f29b
7 changed files with 236 additions and 18 deletions

View File

@ -62,6 +62,7 @@ import prefs
import plug import plug
from hotkey import hotkeymgr from hotkey import hotkeymgr
from monitor import monitor from monitor import monitor
from interactions import interactions
from theme import theme from theme import theme
EDDB = eddb.EDDB() EDDB = eddb.EDDB()
@ -285,6 +286,7 @@ class AppWindow:
self.w.bind('<KP_Enter>', self.getandsend) self.w.bind('<KP_Enter>', self.getandsend)
self.w.bind_all('<<Invoke>>', self.getandsend) # Hotkey monitoring self.w.bind_all('<<Invoke>>', self.getandsend) # Hotkey monitoring
self.w.bind_all('<<JournalEvent>>', self.journal_event) # Journal monitoring self.w.bind_all('<<JournalEvent>>', self.journal_event) # Journal monitoring
self.w.bind_all('<<InteractionEvent>>', self.interaction_event) # cmdrHistory monitoring
self.w.bind_all('<<Quit>>', self.onexit) # Updater self.w.bind_all('<<Quit>>', self.onexit) # Updater
# Load updater after UI creation (for WinSparkle) # Load updater after UI creation (for WinSparkle)
@ -326,6 +328,9 @@ class AppWindow:
# (Re-)install log monitoring # (Re-)install log monitoring
if not monitor.start(self.w): 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.status['text'] = 'Error: Check %s' % _('E:D journal file location') # Location of the new Journal file in E:D 2.2
elif monitor.started:
interactions.start(self.w, monitor.started)
if dologin: if dologin:
self.login() # Login if not already logged in with this Cmdr self.login() # Login if not already logged in with this Cmdr
@ -702,6 +707,11 @@ class AppWindow:
# Plugins # Plugins
plug.notify_journal_entry(monitor.cmdr, monitor.system, monitor.station, entry, monitor.state) plug.notify_journal_entry(monitor.cmdr, monitor.system, monitor.station, entry, monitor.state)
if entry['event'] in ['StartUp', 'LoadGame'] and monitor.started:
# Can start interaction monitoring
if not interactions.start(self.w, monitor.started):
self.status['text'] = 'Error: Check %s' % _('E:D interaction log location') # Setting for the log file that contains recent interactions with other Cmdrs
# Don't send to EDDN while on crew # Don't send to EDDN while on crew
if monitor.captain: if monitor.captain:
return return
@ -768,6 +778,15 @@ class AppWindow:
if not config.getint('hotkey_mute'): if not config.getint('hotkey_mute'):
hotkeymgr.play_bad() hotkeymgr.play_bad()
# Handle interaction event(s) from cmdrHistory
def interaction_event(self, event):
while True:
entry = interactions.get_entry()
if not entry:
return
# Currently we don't do anything with these events
plug.notify_interaction(monitor.cmdr, entry)
def edsmpoll(self): def edsmpoll(self):
result = self.edsm.result result = self.edsm.result
@ -879,6 +898,7 @@ class AppWindow:
config.set('geometry', '+{1}+{2}'.format(*self.w.geometry().split('+'))) config.set('geometry', '+{1}+{2}'.format(*self.w.geometry().split('+')))
self.w.withdraw() # Following items can take a few seconds, so hide the main window while they happen self.w.withdraw() # Following items can take a few seconds, so hide the main window while they happen
hotkeymgr.unregister() hotkeymgr.unregister()
interactions.close()
monitor.close() monitor.close()
self.eddn.close() self.eddn.close()
self.updater.close() self.updater.close()

View File

@ -98,6 +98,17 @@ def journal_entry(cmdr, system, station, entry, state):
sys.stderr.write("Arrived at {}\n".format(entry['StarSystem'])) sys.stderr.write("Arrived at {}\n".format(entry['StarSystem']))
``` ```
### Player Interaction
This gets called when the player interacts with another Cmdr in-game.
If EDMC is started while the game is already running EDMC will send the last few interaction events from the current game session.
```
def interaction(cmdr, entry):
sys.stderr.write("Interacted: {} with Cmdr {}\n".format(', '.join(entry['Interactions']), entry['Name'].encode('utf-8'))
```
### Getting Commander Data ### Getting Commander Data
This gets called when EDMC has just fetched fresh Cmdr and station data from Frontier's servers. This gets called when EDMC has just fetched fresh Cmdr and station data from Frontier's servers.

View File

@ -125,6 +125,8 @@ class Config:
self.default_journal_dir = join(NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, True)[0], 'Frontier Developments', 'Elite Dangerous') self.default_journal_dir = join(NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, True)[0], 'Frontier Developments', 'Elite Dangerous')
self.default_interaction_dir = join(self.default_journal_dir, 'CommanderHistory')
self.home = expanduser('~') self.home = expanduser('~')
self.respath = getattr(sys, 'frozen', False) and normpath(join(dirname(sys.executable), pardir, 'Resources')) or dirname(__file__) self.respath = getattr(sys, 'frozen', False) and normpath(join(dirname(sys.executable), pardir, 'Resources')) or dirname(__file__)
@ -187,6 +189,8 @@ class Config:
journaldir = KnownFolderPath(FOLDERID_SavedGames) journaldir = KnownFolderPath(FOLDERID_SavedGames)
self.default_journal_dir = journaldir and join(journaldir, 'Frontier Developments', 'Elite Dangerous') or None self.default_journal_dir = journaldir and join(journaldir, 'Frontier Developments', 'Elite Dangerous') or None
self.default_interaction_dir = join(KnownFolderPath(FOLDERID_LocalAppData), 'Frontier Developments', 'Elite Dangerous', 'CommanderHistory')
self.respath = dirname(getattr(sys, 'frozen', False) and sys.executable or __file__) self.respath = dirname(getattr(sys, 'frozen', False) and sys.executable or __file__)
self.identifier = applongname self.identifier = applongname
@ -280,6 +284,8 @@ class Config:
self.default_journal_dir = None self.default_journal_dir = None
self.default_interaction_dir = None
self.home = expanduser('~') self.home = expanduser('~')
self.respath = dirname(__file__) self.respath = dirname(__file__)

141
interactions.py Normal file
View File

@ -0,0 +1,141 @@
import json
from operator import itemgetter
from os import listdir, stat
from os.path import getmtime, isdir, join
from sys import platform
if __debug__:
from traceback import print_exc
from config import config
if platform=='darwin':
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
elif platform=='win32':
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
else:
# Linux's inotify doesn't work over CIFS or NFS, so poll
FileSystemEventHandler = object # dummy
# CommanderHistory handler
class Interactions(FileSystemEventHandler):
_POLL = 5 # Fallback polling interval
def __init__(self):
FileSystemEventHandler.__init__(self) # futureproofing - not need for current version of watchdog
self.root = None
self.currentdir = None # The actual logdir that we're monitoring
self.observer = None
self.observed = None # a watchdog ObservedWatch, or None if polling
self.seen = [] # interactions that we've already processed
self.interaction_queue = [] # For communicating interactions back to main thread
def start(self, root, started):
self.root = root
self.session_start = started
logdir = config.get('interactiondir') or config.default_interaction_dir
if not logdir or not isdir(logdir):
self.stop()
return False
if self.currentdir and self.currentdir != logdir:
self.stop()
self.currentdir = logdir
# Set up a watchdog observer.
# 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('interactiondir')) and platform != 'win32'
if not polling and not self.observer:
self.observer = Observer()
self.observer.daemon = True
self.observer.start()
if not self.observed and not polling:
self.observed = self.observer.schedule(self, self.currentdir)
if __debug__:
print '%s interactions "%s"' % (polling and 'Polling' or 'Monitoring', self.currentdir)
# Even if we're not intending to poll, poll at least once to process pre-existing
# data and to check whether the watchdog thread has crashed due to events not\
# being supported on this filesystem.
self.root.after(self._POLL * 1000/2, self.poll, True)
return True
def stop(self):
if __debug__:
print 'Stopping monitoring interactions'
self.currentdir = None
if self.observed:
self.observed = None
self.observer.unschedule_all()
def close(self):
self.stop()
if self.observer:
self.observer.stop()
self.observer.join()
self.observer = None
def poll(self, first_time=False):
self.process()
if first_time:
# Watchdog thread
emitter = self.observed and self.observer._emitter_for_watch[self.observed] # Note: Uses undocumented attribute
if emitter and emitter.is_alive():
return # Watchdog thread still running - stop polling
self.root.after(self._POLL * 1000, self.poll) # keep polling
def on_modified(self, event):
# watchdog callback - DirModifiedEvent on macOS, FileModifiedEvent on Windows
if event.is_directory or stat(event.src_path).st_size: # Can get on_modified events when the file is emptied
self.process(event.src_path if not event.is_directory else None)
# Can be called either in watchdog thread or, if polling, in main thread. The code assumes not both.
def process(self, logfile=None):
if not logfile:
for logfile in [x for x in listdir(self.currentdir) if x.endswith('.cmdrHistory')]:
if self.session_start and getmtime(join(self.currentdir, logfile)) >= self.session_start:
break
else:
return
try:
# cmdrHistory file is shared between beta and live. So filter out interactions not in this game session.
start = self.session_start + 11644473600 # Game time is 369 years in the future
with open(join(self.currentdir, logfile), 'rb') as h:
current = [x for x in json.load(h)['Interactions'] if x['Epoch'] >= start]
new = [x for x in current if x not in self.seen] # O(n^2) comparison but currently limited to 10x10
self.interaction_queue.extend(sorted(new, key=itemgetter('Epoch'))) # sort by time
self.seen = current
if self.interaction_queue:
self.root.event_generate('<<InteractionEvent>>', when="tail")
except:
if __debug__: print_exc()
def get_entry(self):
if not self.interaction_queue:
return None
else:
return self.interaction_queue.pop(0)
# singleton
interactions = Interactions()

View File

@ -1,14 +1,12 @@
import atexit
from collections import defaultdict, OrderedDict from collections import defaultdict, OrderedDict
import json import json
import re import re
import threading import threading
from os import listdir, pardir, rename, unlink, SEEK_SET, SEEK_CUR, SEEK_END from os import listdir, SEEK_SET, SEEK_CUR, SEEK_END
from os.path import basename, exists, isdir, isfile, join from os.path import basename, isdir, join
from platform import machine
import sys
from sys import platform from sys import platform
from time import gmtime, sleep, strftime from time import gmtime, sleep, strftime, strptime
from calendar import timegm
if __debug__: if __debug__:
from traceback import print_exc from traceback import print_exc
@ -27,9 +25,6 @@ elif platform=='win32':
from watchdog.events import FileSystemEventHandler from watchdog.events import FileSystemEventHandler
import ctypes import ctypes
CSIDL_LOCAL_APPDATA = 0x001C
CSIDL_PROGRAM_FILESX86 = 0x002A
# _winreg that ships with Python 2 doesn't support unicode, so do this instead # _winreg that ships with Python 2 doesn't support unicode, so do this instead
from ctypes.wintypes import * from ctypes.wintypes import *
@ -70,6 +65,7 @@ else:
FileSystemEventHandler = object # dummy FileSystemEventHandler = object # dummy
# Journal handler
class EDLogs(FileSystemEventHandler): class EDLogs(FileSystemEventHandler):
_POLL = 1 # Polling is cheap, so do it often _POLL = 1 # Polling is cheap, so do it often
@ -134,7 +130,7 @@ class EDLogs(FileSystemEventHandler):
self.system = None self.system = None
self.station = None self.station = None
self.coordinates = None self.coordinates = None
self.state = {} # Initialized in Fileheader self.started = None # Timestamp of the LoadGame event
# Cmdr state shared with EDSM and plugins # Cmdr state shared with EDSM and plugins
self.state = { self.state = {
@ -152,14 +148,10 @@ class EDLogs(FileSystemEventHandler):
'ShipType' : None, 'ShipType' : None,
} }
def set_callback(self, name, callback):
if name in self.callbacks:
self.callbacks[name] = callback
def start(self, root): def start(self, root):
self.root = root self.root = root
logdir = config.get('journaldir') or config.default_journal_dir logdir = config.get('journaldir') or config.default_journal_dir
if not logdir or not exists(logdir): if not logdir or not isdir(logdir):
self.stop() self.stop()
return False return False
@ -176,7 +168,7 @@ class EDLogs(FileSystemEventHandler):
self.logfile = None self.logfile = None
return False return False
# Set up a watchog observer. This is low overhead so is left running irrespective of whether monitoring is desired. # Set up a watchdog observer.
# File system events are unreliable/non-existent over network drives on Linux. # 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 # 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. # any non-standard logdir might be on a network drive and poll instead.
@ -190,7 +182,7 @@ class EDLogs(FileSystemEventHandler):
self.observed = self.observer.schedule(self, self.currentdir) self.observed = self.observer.schedule(self, self.currentdir)
if __debug__: if __debug__:
print '%s "%s"' % (polling and 'Polling' or 'Monitoring', self.currentdir) print '%s Journal "%s"' % (polling and 'Polling' or 'Monitoring', self.currentdir)
print 'Start logfile "%s"' % self.logfile print 'Start logfile "%s"' % self.logfile
if not self.running(): if not self.running():
@ -202,7 +194,7 @@ class EDLogs(FileSystemEventHandler):
def stop(self): def stop(self):
if __debug__: if __debug__:
print 'Stopping monitoring' print 'Stopping monitoring Journal'
self.currentdir = None self.currentdir = None
self.version = self.mode = self.group = self.cmdr = self.body = self.system = self.station = self.coordinates = None self.version = self.mode = self.group = self.cmdr = self.body = self.system = self.station = self.coordinates = None
self.is_beta = False self.is_beta = False
@ -314,6 +306,7 @@ class EDLogs(FileSystemEventHandler):
self.system = None self.system = None
self.station = None self.station = None
self.coordinates = None self.coordinates = None
self.started = None
self.state = { self.state = {
'Cargo' : defaultdict(int), 'Cargo' : defaultdict(int),
'Credits' : None, 'Credits' : None,
@ -339,6 +332,7 @@ class EDLogs(FileSystemEventHandler):
self.system = None self.system = None
self.station = None self.station = None
self.coordinates = None self.coordinates = None
self.started = timegm(strptime(entry['timestamp'], '%Y-%m-%dT%H:%M:%SZ'))
self.state.update({ self.state.update({
'Credits' : entry['Credits'], 'Credits' : entry['Credits'],
'Loan' : entry['Loan'], 'Loan' : entry['Loan'],

20
plug.py
View File

@ -143,6 +143,26 @@ def notify_journal_entry(cmdr, system, station, entry, state):
sys.stderr.write('%s: %s\n' % (plugname, plugerr)) sys.stderr.write('%s: %s\n' % (plugname, plugerr))
def notify_interaction(cmdr, entry):
"""
Send an interaction entry to each plugin.
:param cmdr: The piloting Cmdr name
:param entry: The interaction entry as a dictionary
:return:
"""
for plugname in PLUGINS:
interaction = _get_plugin_func(plugname, "interaction")
if interaction:
try:
# Pass a copy of the interaction entry in case the callee modifies it
interaction(cmdr, dict(entry))
except Exception as plugerr:
if __debug__:
print_exc()
else:
sys.stderr.write('%s: %s\n' % (plugname, plugerr))
def notify_system_changed(timestamp, system, coordinates): def notify_system_changed(timestamp, system, coordinates):
""" """
Send notification data to each plugin when we arrive at a new system. Send notification data to each plugin when we arrive at a new system.

View File

@ -223,6 +223,10 @@ class PreferencesDialog(tk.Toplevel):
self.logdir.set(config.get('journaldir') or config.default_journal_dir or '') self.logdir.set(config.get('journaldir') or config.default_journal_dir or '')
self.logdir_entry = nb.Entry(configframe, takefocus=False) self.logdir_entry = nb.Entry(configframe, takefocus=False)
self.interactiondir = tk.StringVar()
self.interactiondir.set(config.get('interactiondir') or config.default_interaction_dir or '')
self.interactiondir_entry = nb.Entry(configframe, takefocus=False)
if platform != 'darwin': if platform != 'darwin':
# Apple's SMB implementation is way too flaky - no filesystem events and bogus NULLs # Apple's SMB implementation is way too flaky - no filesystem events and bogus NULLs
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 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
@ -234,6 +238,15 @@ class PreferencesDialog(tk.Toplevel):
if config.default_journal_dir: 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 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
nb.Label(configframe, text = _('E:D interaction log location')+':').grid(columnspan=3, padx=PADX, sticky=tk.W) # Setting for the log file that contains recent interactions with other Cmdrs
self.interactiondir_entry.grid(row=15, columnspan=2, padx=(PADX,0), sticky=tk.EW)
self.interactionbutton = 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 interaction log location'), self.interactiondir))
self.interactionbutton.grid(row=15, column=2, padx=PADX, sticky=tk.EW)
if config.default_interaction_dir:
nb.Button(configframe, text=_('Default'), command=self.interactiondir_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': if platform == 'win32':
ttk.Separator(configframe, orient=tk.HORIZONTAL).grid(columnspan=3, padx=PADX, pady=PADY*8, sticky=tk.EW) ttk.Separator(configframe, orient=tk.HORIZONTAL).grid(columnspan=3, padx=PADX, pady=PADY*8, sticky=tk.EW)
@ -406,6 +419,7 @@ class PreferencesDialog(tk.Toplevel):
self.displaypath(self.outdir, self.outdir_entry) self.displaypath(self.outdir, self.outdir_entry)
self.displaypath(self.logdir, self.logdir_entry) self.displaypath(self.logdir, self.logdir_entry)
self.displaypath(self.interactiondir, self.interactiondir_entry)
logdir = self.logdir.get() logdir = self.logdir.get()
logvalid = logdir and exists(logdir) logvalid = logdir and exists(logdir)
@ -494,6 +508,11 @@ class PreferencesDialog(tk.Toplevel):
self.logdir.set(config.default_journal_dir) self.logdir.set(config.default_journal_dir)
self.outvarchanged() self.outvarchanged()
def interactiondir_reset(self):
if config.default_interaction_dir:
self.interactiondir.set(config.default_interaction_dir)
self.outvarchanged()
def themecolorbrowse(self, index): def themecolorbrowse(self, index):
(rgb, color) = tkColorChooser.askcolor(self.theme_colors[index], title=self.theme_prompts[index], parent=self.parent) (rgb, color) = tkColorChooser.askcolor(self.theme_colors[index], title=self.theme_prompts[index], parent=self.parent)
if color: if color:
@ -588,6 +607,13 @@ class PreferencesDialog(tk.Toplevel):
config.set('journaldir', '') # default location config.set('journaldir', '') # default location
else: else:
config.set('journaldir', logdir) config.set('journaldir', logdir)
interactiondir = self.interactiondir.get()
if config.default_journal_dir and interactiondir.lower() == config.default_interaction_dir.lower():
config.set('interactiondir', '') # default location
else:
config.set('interactiondir', interactiondir)
if platform in ['darwin','win32']: if platform in ['darwin','win32']:
config.set('hotkey_code', self.hotkey_code) config.set('hotkey_code', self.hotkey_code)
config.set('hotkey_mods', self.hotkey_mods) config.set('hotkey_mods', self.hotkey_mods)