1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-04-14 00:07:14 +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
from hotkey import hotkeymgr
from monitor import monitor
from interactions import interactions
from theme import theme
EDDB = eddb.EDDB()
@ -285,6 +286,7 @@ class AppWindow:
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('<<InteractionEvent>>', self.interaction_event) # cmdrHistory monitoring
self.w.bind_all('<<Quit>>', self.onexit) # Updater
# Load updater after UI creation (for WinSparkle)
@ -326,6 +328,9 @@ class AppWindow:
# (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
elif monitor.started:
interactions.start(self.w, monitor.started)
if dologin:
self.login() # Login if not already logged in with this Cmdr
@ -702,6 +707,11 @@ class AppWindow:
# Plugins
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
if monitor.captain:
return
@ -768,6 +778,15 @@ class AppWindow:
if not config.getint('hotkey_mute'):
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):
result = self.edsm.result
@ -879,6 +898,7 @@ class AppWindow:
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
hotkeymgr.unregister()
interactions.close()
monitor.close()
self.eddn.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']))
```
### 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
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_interaction_dir = join(self.default_journal_dir, 'CommanderHistory')
self.home = expanduser('~')
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)
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.identifier = applongname
@ -280,6 +284,8 @@ class Config:
self.default_journal_dir = None
self.default_interaction_dir = None
self.home = expanduser('~')
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
import json
import re
import threading
from os import listdir, pardir, rename, unlink, SEEK_SET, SEEK_CUR, SEEK_END
from os.path import basename, exists, isdir, isfile, join
from platform import machine
import sys
from os import listdir, SEEK_SET, SEEK_CUR, SEEK_END
from os.path import basename, isdir, join
from sys import platform
from time import gmtime, sleep, strftime
from time import gmtime, sleep, strftime, strptime
from calendar import timegm
if __debug__:
from traceback import print_exc
@ -27,9 +25,6 @@ elif platform=='win32':
from watchdog.events import FileSystemEventHandler
import ctypes
CSIDL_LOCAL_APPDATA = 0x001C
CSIDL_PROGRAM_FILESX86 = 0x002A
# _winreg that ships with Python 2 doesn't support unicode, so do this instead
from ctypes.wintypes import *
@ -70,6 +65,7 @@ else:
FileSystemEventHandler = object # dummy
# Journal handler
class EDLogs(FileSystemEventHandler):
_POLL = 1 # Polling is cheap, so do it often
@ -134,7 +130,7 @@ class EDLogs(FileSystemEventHandler):
self.system = None
self.station = None
self.coordinates = None
self.state = {} # Initialized in Fileheader
self.started = None # Timestamp of the LoadGame event
# Cmdr state shared with EDSM and plugins
self.state = {
@ -152,14 +148,10 @@ class EDLogs(FileSystemEventHandler):
'ShipType' : None,
}
def set_callback(self, name, callback):
if name in self.callbacks:
self.callbacks[name] = callback
def start(self, root):
self.root = root
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()
return False
@ -176,7 +168,7 @@ class EDLogs(FileSystemEventHandler):
self.logfile = None
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.
# 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.
@ -190,7 +182,7 @@ class EDLogs(FileSystemEventHandler):
self.observed = self.observer.schedule(self, self.currentdir)
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
if not self.running():
@ -202,7 +194,7 @@ class EDLogs(FileSystemEventHandler):
def stop(self):
if __debug__:
print 'Stopping monitoring'
print 'Stopping monitoring Journal'
self.currentdir = None
self.version = self.mode = self.group = self.cmdr = self.body = self.system = self.station = self.coordinates = None
self.is_beta = False
@ -314,6 +306,7 @@ class EDLogs(FileSystemEventHandler):
self.system = None
self.station = None
self.coordinates = None
self.started = None
self.state = {
'Cargo' : defaultdict(int),
'Credits' : None,
@ -339,6 +332,7 @@ class EDLogs(FileSystemEventHandler):
self.system = None
self.station = None
self.coordinates = None
self.started = timegm(strptime(entry['timestamp'], '%Y-%m-%dT%H:%M:%SZ'))
self.state.update({
'Credits' : entry['Credits'],
'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))
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):
"""
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_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':
# 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
@ -234,6 +238,15 @@ class PreferencesDialog(tk.Toplevel):
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.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':
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.logdir, self.logdir_entry)
self.displaypath(self.interactiondir, self.interactiondir_entry)
logdir = self.logdir.get()
logvalid = logdir and exists(logdir)
@ -494,6 +508,11 @@ class PreferencesDialog(tk.Toplevel):
self.logdir.set(config.default_journal_dir)
self.outvarchanged()
def interactiondir_reset(self):
if config.default_interaction_dir:
self.interactiondir.set(config.default_interaction_dir)
self.outvarchanged()
def themecolorbrowse(self, index):
(rgb, color) = tkColorChooser.askcolor(self.theme_colors[index], title=self.theme_prompts[index], parent=self.parent)
if color:
@ -588,6 +607,13 @@ class PreferencesDialog(tk.Toplevel):
config.set('journaldir', '') # default location
else:
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']:
config.set('hotkey_code', self.hotkey_code)
config.set('hotkey_mods', self.hotkey_mods)