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

Merge branch 'beyond'

This commit is contained in:
Jonathan Harris 2018-02-26 17:57:49 +00:00
commit c79398eb12
14 changed files with 3534 additions and 3078 deletions

View File

@ -57,6 +57,7 @@ import prefs
import plug
from hotkey import hotkeymgr
from monitor import monitor
from dashboard import dashboard
from theme import theme
@ -269,6 +270,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('<<DashboardEvent>>', self.dashboard_event) # Dashboard monitoring
self.w.bind_all('<<PluginError>>', self.plugin_error) # Statusbar
self.w.bind_all('<<Quit>>', self.onexit) # Updater
@ -627,6 +629,11 @@ class AppWindow:
if not config.getint('hotkey_mute'):
hotkeymgr.play_bad()
if entry['event'] in ['StartUp', 'LoadGame'] and monitor.started:
# Can start dashboard monitoring
if not dashboard.start(self.w, monitor.started):
print "Can't start Status monitoring"
# Don't send to EDDN while on crew
if monitor.state['Captain']:
return
@ -678,6 +685,17 @@ class AppWindow:
if not config.getint('hotkey_mute'):
hotkeymgr.play_bad()
# Handle Status event
def dashboard_event(self, event):
entry = dashboard.status
if entry:
# Currently we don't do anything with these events
err = plug.notify_dashboard_entry(monitor.cmdr, monitor.is_beta, entry)
if err:
self.status['text'] = err
if not config.getint('hotkey_mute'):
hotkeymgr.play_bad()
# Display asynchronous error from plugin
def plugin_error(self, event=None):
if plug.last_error.get('msg'):
@ -691,6 +709,14 @@ class AppWindow:
if not monitor.cmdr or not monitor.mode:
return False # In CQC - do nothing
if config.getint('shipyard') == config.SHIPYARD_EDSHIPYARD:
return edshipyard.url(monitor.is_beta)
elif config.getint('shipyard') == config.SHIPYARD_CORIOLIS:
pass # Fall through
else:
assert False, config.getint('shipyard')
return False
self.status['text'] = _('Fetching data...')
self.w.update_idletasks()
try:
@ -716,13 +742,7 @@ class AppWindow:
self.status['text'] = _('Error: Frontier server is lagging') # Raised when Companion API server is returning old data, e.g. when the servers are too busy
else:
self.status['text'] = ''
if config.getint('shipyard') == config.SHIPYARD_EDSHIPYARD:
return edshipyard.url(data, monitor.is_beta)
elif config.getint('shipyard') == config.SHIPYARD_CORIOLIS:
return coriolis.url(data, monitor.is_beta)
else:
assert False, config.getint('shipyard')
return False
return coriolis.url(data, monitor.is_beta)
def cooldown(self):
if time() < self.holdofftime:
@ -779,6 +799,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()
dashboard.close()
monitor.close()
plug.notify_stop()
self.eddn.close()

View File

@ -106,7 +106,7 @@ this.status["text"] = "Happy!"
## Events
Once you have created your plugin and EDMC has loaded it there are two other functions you can define to be notified by EDMC when something happens: `journal_entry()` and `cmdr_data()`.
Once you have created your plugin and EDMC has loaded it there are three other functions you can define to be notified by EDMC when something happens: `journal_entry()`, `status()` and `cmdr_data()`.
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
@ -128,6 +128,16 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
sys.stderr.write("Arrived at {}\n".format(entry['StarSystem']))
```
### Player Dashboard
This gets called when something on the player's cockpit display changes - typically about once a second when in orbital flight
```python
def dashboard_entry(cmdr, is_beta, entry):
deployed = entry['Flags'] & 1<<6
sys.stderr.write("Hardpoints {}\n", deployed and "deployed" or "stowed")
```
### Getting Commander Data
This gets called when EDMC has just fetched fresh Cmdr and station data from Frontier's servers.
@ -144,7 +154,7 @@ The data is a dictionary and full of lots of wonderful stuff!
## Error messages
You can display an error in EDMC's status area by returning a string from your `journal_entry()` or `cmdr_data()` function, or asynchronously (e.g. from a "worker" thread that is performing a long running operation) by calling `plug.show_error()`. Either method will cause the "bad" sound to be played (unless the user has muted sound).
You can display an error in EDMC's status area by returning a string from your `journal_entry()`, `status_entry()` or `cmdr_data()` function, or asynchronously (e.g. from a "worker" thread that is performing a long running operation) by calling `plug.show_error()`. Either method will cause the "bad" sound to be played (unless the user has muted sound).
The status area is shared between EDMC itself and all other plugins, so your message won't be displayed for very long. Create a dedicated widget if you need to display routine status information.

View File

@ -73,6 +73,7 @@ ship_map = {
'type7' : 'Type-7 Transporter',
'type9' : 'Type-9 Heavy',
'type9_military' : 'Type-10 Defender',
'typex' : 'Alliance Chieftain',
'viper' : 'Viper MkIII',
'viper_mkiv' : 'Viper MkIV',
'vulture' : 'Vulture',

@ -1 +1 @@
Subproject commit a3ae3e34ad879d1504a7d2fdc3fce4854820051b
Subproject commit 44d57f26a8099cfa8d91815f453c7bba303a63ec

View File

@ -169,11 +169,16 @@ if __name__ == "__main__":
else:
modules[key] = { 'mass': m.get('mass', 0) } # Some modules don't have mass
# 2.5 additions not yet present in coriolis-data
# 3.0 additions not yet present in coriolis-data
modules[('Decontamination Limpet Controller', None, '1', 'E')] = {'mass': 1.3}
modules[('Decontamination Limpet Controller', None, '3', 'E')] = {'mass': 2}
modules[('Decontamination Limpet Controller', None, '5', 'E')] = {'mass': 20}
modules[('Decontamination Limpet Controller', None, '7', 'E')] = {'mass': 128}
modules[('Recon Limpet Controller', None, '1', 'E')] = {'mass': 1.3}
modules[('Recon Limpet Controller', None, '3', 'E')] = {'mass': 2}
modules[('Recon Limpet Controller', None, '5', 'E')] = {'mass': 20}
modules[('Recon Limpet Controller', None, '7', 'E')] = {'mass': 128}
modules[('Research Limpet Controller', None, '1', 'E')] = {'mass': 1.3}
modules = OrderedDict([(k,modules[k]) for k in sorted(modules)]) # sort for easier diffing
cPickle.dump(modules, open('modules.p', 'wb'))

131
dashboard.py Normal file
View File

@ -0,0 +1,131 @@
import json
from calendar import timegm
from operator import itemgetter
from os import listdir, stat
from os.path import isdir, isfile, join
from sys import platform
import time
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
# Status.json handler
class Dashboard(FileSystemEventHandler):
_POLL = 1 # 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.status = {} # Current status for communicating status back to main thread
def start(self, root, started):
self.root = root
self.session_start = started
logdir = config.get('journaldir') or config.default_journal_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('journaldir')) and platform != 'win32'
if not polling and not self.observer:
self.observer = Observer()
self.observer.daemon = True
self.observer.start()
elif polling and self.observer:
self.observer.stop()
self.observer = None
if not self.observed and not polling:
self.observed = self.observer.schedule(self, self.currentdir)
if __debug__:
print '%s Dashboard "%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 Dashboard'
self.currentdir = None
if self.observed:
self.observed = None
self.observer.unschedule_all()
self.status = {}
def close(self):
self.stop()
if self.observer:
self.observer.stop()
if self.observer:
self.observer.join()
self.observer = None
def poll(self, first_time=False):
if not self.currentdir:
# Stopped
self.status = {}
else:
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 (isfile(event.src_path) and 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.
def process(self, logfile=None):
try:
with open(join(self.currentdir, 'Status.json'), 'rb') as h:
entry = json.load(h)
# Status file is shared between beta and live. So filter out status not in this game session.
if timegm(time.strptime(entry['timestamp'], '%Y-%m-%dT%H:%M:%SZ')) >= self.session_start and self.status != entry:
self.status = entry
self.root.event_generate('<<DashboardEvent>>', when="tail")
except:
if __debug__: print_exc()
# singleton
dashboard = Dashboard()

View File

@ -13,6 +13,7 @@ import gzip
from config import config
import companion
import outfitting
from monitor import monitor
# Map API ship names to E:D Shipyard ship names
ship_map = dict(companion.ship_map)
@ -162,9 +163,11 @@ def export(data, filename=None):
# Return a URL for the current ship
def url(data, is_beta):
def url(is_beta):
string = json.dumps(companion.ship(data), ensure_ascii=False, sort_keys=True, separators=(',', ':')).encode('utf-8') # most compact representation
string = json.dumps(monitor.ship(), ensure_ascii=False, sort_keys=True, separators=(',', ':')).encode('utf-8') # most compact representation
if not string:
return False
out = StringIO.StringIO()
with gzip.GzipFile(fileobj=out, mode='w') as f:

5828
modules.p

File diff suppressed because it is too large Load Diff

View File

@ -113,14 +113,19 @@ class EDLogs(FileSystemEventHandler):
'Raw' : defaultdict(int),
'Manufactured' : defaultdict(int),
'Encoded' : defaultdict(int),
'PaintJob' : None,
'Rank' : { 'Combat': None, 'Trade': None, 'Explore': None, 'Empire': None, 'Federation': None, 'CQC': None },
'Rank' : {},
'Reputation' : {},
'Statistics' : {},
'Role' : None, # Crew role - None, Idle, FireCon, FighterCon
'Friends' : set(), # Online friends
'ShipID' : None,
'ShipIdent' : None,
'ShipName' : None,
'ShipType' : None,
'HullValue' : None,
'ModulesValue' : None,
'Rebuy' : None,
'Modules' : None,
}
def start(self, root):
@ -224,24 +229,18 @@ class EDLogs(FileSystemEventHandler):
if self.live:
if self.game_was_running:
# Game is running locally
entry = OrderedDict([
('timestamp', strftime('%Y-%m-%dT%H:%M:%SZ', gmtime())),
('event', 'StartUp'),
('StarSystem', self.system),
('StarPos', self.coordinates),
])
if self.planet:
entry['Body'] = self.planet
entry['Docked'] = bool(self.station)
if self.station:
entry = OrderedDict([
('timestamp', strftime('%Y-%m-%dT%H:%M:%SZ', gmtime())),
('event', 'StartUp'),
('Docked', True),
('StationName', self.station),
('StationType', self.stationtype),
('StarSystem', self.system),
('StarPos', self.coordinates),
])
else:
entry = OrderedDict([
('timestamp', strftime('%Y-%m-%dT%H:%M:%SZ', gmtime())),
('event', 'StartUp'),
('Docked', False),
('StarSystem', self.system),
('StarPos', self.coordinates),
])
entry['StationName'] = self.station
entry['StationType'] = self.stationtype
self.event_queue.append(json.dumps(entry, separators=(', ', ':')))
else:
self.event_queue.append(None) # Generate null event to update the display (with possibly out-of-date info)
@ -325,17 +324,23 @@ class EDLogs(FileSystemEventHandler):
'Raw' : defaultdict(int),
'Manufactured' : defaultdict(int),
'Encoded' : defaultdict(int),
'PaintJob' : None,
'Rank' : { 'Combat': None, 'Trade': None, 'Explore': None, 'Empire': None, 'Federation': None, 'CQC': None },
'Rank' : {},
'Reputation' : {},
'Statistics' : {},
'Role' : None,
'Friends' : set(),
'ShipID' : None,
'ShipIdent' : None,
'ShipName' : None,
'ShipType' : None,
'HullValue' : None,
'ModulesValue' : None,
'Rebuy' : None,
'Modules' : None,
}
elif entry['event'] == 'Commander':
self.live = True # First event in 3.0
elif entry['event'] == 'LoadGame':
self.live = True
self.cmdr = entry['Commander']
self.mode = entry.get('GameMode') # 'Open', 'Solo', 'Group', or None for CQC (and Training - but no LoadGame event)
self.group = entry.get('Group')
@ -350,7 +355,9 @@ class EDLogs(FileSystemEventHandler):
'Captain' : None,
'Credits' : entry['Credits'],
'Loan' : entry['Loan'],
'Rank' : { 'Combat': None, 'Trade': None, 'Explore': None, 'Empire': None, 'Federation': None, 'CQC': None },
'Rank' : {},
'Reputation' : {},
'Statistics' : {},
'Role' : None,
})
elif entry['event'] == 'NewCommander':
@ -367,25 +374,45 @@ class EDLogs(FileSystemEventHandler):
self.state['ShipIdent'] = None
self.state['ShipName'] = None
self.state['ShipType'] = self.canonicalise(entry['ShipType'])
self.state['PaintJob'] = None
self.state['HullValue'] = None
self.state['ModulesValue'] = None
self.state['Rebuy'] = None
self.state['Modules'] = None
elif entry['event'] == 'ShipyardSwap':
self.state['ShipID'] = entry['ShipID']
self.state['ShipIdent'] = None
self.state['ShipName'] = None
self.state['ShipType'] = self.canonicalise(entry['ShipType'])
self.state['PaintJob'] = None
self.state['HullValue'] = None
self.state['ModulesValue'] = None
self.state['Rebuy'] = None
self.state['Modules'] = None
elif entry['event'] == 'Loadout': # Note: Precedes LoadGame, ShipyardNew, follows ShipyardSwap, ShipyardBuy
self.state['ShipID'] = entry['ShipID']
self.state['ShipIdent'] = entry['ShipIdent']
self.state['ShipName'] = entry['ShipName']
self.state['ShipType'] = self.canonicalise(entry['Ship'])
# Ignore other Modules since they're missing Engineer modification details
self.state['PaintJob'] = 'paintjob_%s_default_defaultpaintjob' % self.state['ShipType']
for module in entry['Modules']:
if module.get('Slot') == 'PaintJob' and module.get('Item'):
self.state['PaintJob'] = self.canonicalise(module['Item'])
elif entry['event'] in ['ModuleBuy', 'ModuleSell'] and entry['Slot'] == 'PaintJob':
self.state['PaintJob'] = self.canonicalise(entry.get('BuyItem'))
self.state['HullValue'] = entry.get('HullValue') # not present on exiting Outfitting
self.state['ModulesValue'] = entry.get('ModulesValue') # "
self.state['Rebuy'] = entry.get('Rebuy')
self.state['Modules'] = dict([(thing['Slot'], thing) for thing in entry['Modules']])
elif entry['event'] == 'ModuleBuy':
self.state['Modules'][entry['Slot']] = { 'Slot' : entry['Slot'],
'Item' : self.canonicalise(entry['BuyItem']),
'On' : True,
'Priority' : 1,
'Health' : 1.0,
'Value' : entry['BuyPrice'],
}
elif entry['event'] == 'ModuleSell':
self.state['Modules'].pop(entry['Slot'], None)
elif entry['event'] == 'ModuleSwap':
toitem = self.state['Modules'].get(entry['ToSlot'])
self.state['Modules'][entry['ToSlot']] = self.state['Modules'][entry['FromSlot']]
if toitem:
self.state['Modules'][entry['FromSlot']] = toitem
else:
self.state['Modules'].pop(entry['FromSlot'], None)
elif entry['event'] in ['Undocked']:
self.station = None
self.stationtype = None
@ -403,21 +430,30 @@ class EDLogs(FileSystemEventHandler):
entry.get('StationName')) # May be None
self.stationtype = entry.get('StationType') # May be None
self.stationservices = entry.get('StationServices') # None under E:D < 2.4
elif entry['event'] == 'ApproachBody':
self.planet = entry['Body']
elif entry['event'] == 'SupercruiseExit':
self.planet = entry.get('Body') if entry.get('BodyType') == 'Planet' else None
elif entry['event'] == 'SupercruiseEntry':
elif entry['event'] in ['LeaveBody', 'SupercruiseEntry']:
self.planet = None
elif entry['event'] in ['Rank', 'Promotion']:
for k,v in entry.iteritems():
if k in self.state['Rank']:
self.state['Rank'][k] = (v,0)
payload = dict(entry)
payload.pop('event')
payload.pop('timestamp')
for k,v in payload.iteritems():
self.state['Rank'][k] = (v,0)
elif entry['event'] == 'Progress':
for k,v in entry.iteritems():
if self.state['Rank'].get(k) is not None:
if k in self.state['Rank']:
self.state['Rank'][k] = (self.state['Rank'][k][0], min(v, 100)) # perhaps not taken promotion mission yet
elif entry['event'] in ['Reputation', 'Statistics']:
payload = dict(entry)
payload.pop('event')
payload.pop('timestamp')
self.state[entry['event']] = payload
elif entry['event'] == 'Cargo':
self.live = True # First event in 2.3
self.state['Cargo'] = defaultdict(int)
self.state['Cargo'].update({ self.canonicalise(x['Name']): x['Count'] for x in entry['Inventory'] })
elif entry['event'] in ['CollectCargo', 'MarketBuy', 'BuyDrones', 'MiningRefined']:
@ -432,6 +468,11 @@ class EDLogs(FileSystemEventHandler):
for reward in entry.get('CommodityReward', []):
commodity = self.canonicalise(reward['Name'])
self.state['Cargo'][commodity] += reward.get('Count', 1)
for reward in entry.get('MaterialsReward', []):
if 'Category' in reward: # FIXME: Category not present in E:D 3.0
material = self.canonicalise(reward['Name'])
self.state[reward['Category']][material] += reward.get('Count', 1)
elif entry['event'] == 'SearchAndRescue':
for item in entry.get('Items', []):
commodity = self.canonicalise(item['Name'])
@ -451,15 +492,39 @@ class EDLogs(FileSystemEventHandler):
self.state[entry['Category']][material] -= entry['Count']
if self.state[entry['Category']][material] <= 0:
self.state[entry['Category']].pop(material)
elif entry['event'] in ['EngineerCraft', 'Synthesis']:
elif entry['event'] == 'Synthesis':
for category in ['Raw', 'Manufactured', 'Encoded']:
for x in entry[entry['event'] == 'EngineerCraft' and 'Ingredients' or 'Materials']:
for x in entry['Materials']:
material = self.canonicalise(x['Name'])
if material in self.state[category]:
self.state[category][material] -= x['Count']
if self.state[category][material] <= 0:
self.state[category].pop(material)
elif entry['event'] == 'EngineerCraft' or (entry['event'] == 'EngineerLegacyConvert' and not entry.get('IsPreview')):
for category in ['Raw', 'Manufactured', 'Encoded']:
for x in entry.get('Ingredients', []):
material = self.canonicalise(x['Name'])
if material in self.state[category]:
self.state[category][material] -= x['Count']
if self.state[category][material] <= 0:
self.state[category].pop(material)
module = self.state['Modules'][entry['Slot']]
module['Engineering'] = {
'Engineer' : entry['Engineer'],
'EngineerID' : entry['EngineerID'],
'BlueprintName' : entry['BlueprintName'],
'BlueprintID' : entry['BlueprintID'],
'Level' : entry['Level'],
'Quality' : entry['Quality'],
'Modifiers' : entry['Modifiers'],
}
if 'ExperimentalEffect' in entry:
module['Engineering']['ExperimentalEffect'] = entry['ExperimentalEffect']
module['Engineering']['ExperimentalEffect_Localised'] = entry['ExperimentalEffect_Localised']
else:
module['Engineering'].pop('ExperimentalEffect', None)
module['Engineering'].pop('ExperimentalEffect_Localised', None)
elif entry['event'] == 'EngineerContribution':
commodity = self.canonicalise(entry.get('Commodity'))
if commodity:
@ -579,5 +644,24 @@ class EDLogs(FileSystemEventHandler):
return False
# Return a subset of the received data describing the current ship as a Loadout event
def ship(self):
if not self.state['Modules']:
return None
d = OrderedDict([
('timestamp', strftime('%Y-%m-%dT%H:%M:%SZ', gmtime())),
('event', 'Loadout'),
('Ship', self.state['ShipType']),
('ShipID', self.state['ShipID']),
])
for thing in ['ShipName', 'ShipIdent', 'HullValue', 'ModulesValue', 'Rebuy']:
if self.state[thing]:
d[thing] = self.state[thing]
d['Modules'] = self.state['Modules'].values()
return d
# singleton
monitor = EDLogs()

View File

@ -261,6 +261,7 @@ internal_map = {
'passengercabin' : 'Passenger Cabin',
'prospector' : 'Prospector Limpet Controller',
'refinery' : 'Refinery',
'recon' : 'Recon Limpet Controller',
'repair' : 'Repair Limpet Controller',
'repairer' : 'Auto Field-Maintenance Unit',
'resourcesiphon' : 'Hatch Breaker Limpet Controller',

21
plug.py
View File

@ -226,6 +226,27 @@ def notify_journal_entry(cmdr, is_beta, system, station, entry, state):
return error
def notify_dashboard_entry(cmdr, is_beta, entry):
"""
Send a status entry to each plugin.
:param cmdr: The piloting Cmdr name
:param is_beta: whether the player is in a Beta universe.
:param entry: The status entry as a dictionary
:return: Error message from the first plugin that returns one (if any)
"""
error = None
for plugin in PLUGINS:
status = plugin._get_func('dashboard_entry')
if status:
try:
# Pass a copy of the status entry in case the callee modifies it
newerror = status(cmdr, is_beta, dict(entry))
error = error or newerror
except:
print_exc()
return error
def notify_system_changed(timestamp, system, coordinates):
"""
Send notification data to each plugin when we arrive at a new system.

View File

@ -34,11 +34,14 @@ this.session = requests.Session()
this.queue = Queue() # Items to be sent to EDSM by worker thread
this.discardedEvents = [] # List discarded events from EDSM
this.lastship = None # Description of last ship that we sent to EDSM
this.lastloadout = None # Description of last ship that we sent to EDSM
this.lastlookup = False # whether the last lookup succeeded
# Game state
this.multicrew = False # don't send captain's ship info to EDSM while on a crew
this.coordinates = None
this.newgame = False # starting up - batch initial burst of events
this.newgame_docked = False # starting up while docked
def plugin_start():
# Can't be earlier since can only call PhotoImage after window is created
@ -198,6 +201,16 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
elif entry['event'] == 'LoadGame':
this.coordinates = None
if entry['event'] in ['LoadGame', 'Commander', 'NewCommander']:
this.newgame = True
this.newgame_docked = False
elif entry['event'] == 'StartUp':
this.newgame = False
this.newgame_docked = False
elif entry['event'] == 'Location':
this.newgame = True
this.newgame_docked = entry.get('Docked', False)
# Send interesting events to EDSM
if config.getint('edsm_out') and not is_beta and not this.multicrew and credentials(cmdr) and entry['event'] not in this.discardedEvents:
# Introduce transient states into the event
@ -210,15 +223,7 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
entry.update(transient)
if entry['event'] == 'LoadGame':
# Synthesise Cargo and Materials events on LoadGame since we will have missed them because Cmdr was unknown
cargo = {
'timestamp': entry['timestamp'],
'event': 'Cargo',
'Inventory': [ { 'Name': k, 'Count': v } for k,v in state['Cargo'].iteritems() ],
}
cargo.update(transient)
this.queue.put((cmdr, cargo))
# Synthesise Materials events on LoadGame since we will have missed it
materials = {
'timestamp': entry['timestamp'],
'event': 'Materials',
@ -231,6 +236,14 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
this.queue.put((cmdr, entry))
if entry['event'] == 'Loadout' and 'EDShipyard' not in this.discardedEvents:
url = edshipyard.url(is_beta)
if this.lastloadout != url:
this.lastloadout = url
this.queue.put((cmdr, {
'event': 'EDShipyard', 'timestamp': entry['timestamp'], '_shipId': state['ShipID'], 'url': this.lastloadout
}))
# Update system data
def cmdr_data(data, is_beta):
@ -258,10 +271,6 @@ def cmdr_data(data, is_beta):
this.queue.put((cmdr, {
'event': 'Coriolis', 'timestamp': timestamp, '_shipId': data['ship']['id'], 'url': coriolis.url(data, is_beta)
}))
if 'EDShipyard' not in this.discardedEvents:
this.queue.put((cmdr, {
'event': 'EDShipyard', 'timestamp': timestamp, '_shipId': data['ship']['id'], 'url': edshipyard.url(data, is_beta)
}))
this.lastship = ship
@ -310,14 +319,14 @@ def worker():
if msgnum // 100 == 2:
print('EDSM\t%s %s\t%s' % (msgnum, msg, json.dumps(pending, separators = (',', ': '))))
plug.show_error(_('Error: EDSM {MSG}').format(MSG=msg))
elif not closing:
# Update main window's system status
for i in range(len(pending) - 1, -1, -1):
if pending[i]['event'] in ['StartUp', 'Location', 'FSDJump']:
this.lastlookup = reply['events'][i]
else:
for e, r in zip(pending, reply['events']):
if not closing and e['event'] in ['StartUp', 'Location', 'FSDJump']:
# Update main window's system status
this.lastlookup = r
this.system.event_generate('<<EDSMStatus>>', when="tail") # calls update_status in main thread
break
elif r['msgnum'] // 100 != 1:
print('EDSM\t%s %s\t%s' % (r['msgnum'], r['msg'], json.dumps(e, separators = (',', ': '))))
pending = []
break
@ -334,10 +343,16 @@ def worker():
# Whether any of the entries should be sent immediately
def should_send(entries):
for entry in entries:
if (entry['event'] not in ['CommunityGoal', # Spammed periodically
'Cargo', 'Loadout', 'Materials', 'LoadGame', 'Rank', 'Progress', # Will be followed by 'Docked' or 'Location'
'ShipyardBuy', 'ShipyardNew', 'ShipyardSwap'] and # "
not (entry['event'] == 'Location' and entry.get('Docked'))): # "
if (entry['event'] == 'Cargo' and not this.newgame_docked) or entry['event'] == 'Docked':
# Cargo is the last event on startup, unless starting when docked in which case Docked is the last event
this.newgame = False
this.newgame_docked = False
return True
elif this.newgame:
pass
elif entry['event'] not in ['CommunityGoal', # Spammed periodically
'ModuleBuy', 'ModuleSell', 'ModuleSwap', # will be shortly followed by "Loadout"
'ShipyardBuy', 'ShipyardNew', 'ShipyardSwap']: # "
return True
return False

View File

@ -7,6 +7,7 @@ import json
import requests
import sys
import time
from operator import itemgetter
from Queue import Queue
from threading import Thread
@ -36,11 +37,13 @@ this.cmdr = None
this.multicrew = False # don't send captain's ship info to Inara while on a crew
this.newuser = False # just entered API Key
this.undocked = False # just undocked
this.suppress_docked = False # Skip Docked event after Location if started docked
this.suppress_docked = False # Skip initial Docked event if started docked
this.cargo = None
this.materials = None
this.lastcredits = 0 # Send credit update soon after Startup / new game
this.needfleet = True # Send full fleet update soon after Startup / new game
this.storedmodules = None
this.loadout = None
this.fleet = None
this.shipswap = False # just swapped ship
# URLs
@ -154,7 +157,9 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
this.cargo = None
this.materials = None
this.lastcredits = 0
this.needfleet = True
this.storedmodules = None
this.loadout = None
this.fleet = None
this.shipswap = False
elif entry['event'] in ['Resurrect', 'ShipyardBuy', 'ShipyardSell', 'SellShipOnRebuy']:
# Events that mean a significant change in credits so we should send credits after next "Update"
@ -163,7 +168,7 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
this.suppress_docked = True
# Send location and status on new game or StartUp. Assumes Location is the last event on a new game (other than Docked).
# Send location and status on new game or StartUp. Assumes Cargo is the last event on a new game (other than Docked).
# Always send an update on Docked, FSDJump, Undocked+SuperCruise, Promotion and EngineerProgress.
# Also send material and cargo (if changed) whenever we send an update.
@ -171,8 +176,17 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
try:
old_events = len(this.events) # Will only send existing events if we add a new event below
# Send credits to Inara on startup only - otherwise may be out of date
if entry['event'] == 'Cargo':
add_event('setCommanderCredits', entry['timestamp'],
OrderedDict([
('commanderCredits', state['Credits']),
('commanderLoan', state['Loan']),
]))
this.lastcredits = state['Credits']
# Send rank info to Inara on startup or change
if (entry['event'] in ['StartUp', 'Location'] or this.newuser) and state['Rank']:
if (entry['event'] in ['StartUp', 'Cargo'] or this.newuser):
for k,v in state['Rank'].iteritems():
if v is not None:
add_event('setCommanderRankPilot', entry['timestamp'],
@ -181,6 +195,15 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
('rankValue', v[0]),
('rankProgress', v[1] / 100.0),
]))
for k,v in state['Reputation'].iteritems():
if v is not None:
add_event('setCommanderReputationMajorFaction', entry['timestamp'],
OrderedDict([
('majorfactionName', k.lower()),
('majorfactionReputation', v / 100.0),
]))
add_event('setCommanderGameStatistics', entry['timestamp'], state['Statistics']) # may be out of date
elif entry['event'] == 'Promotion':
for k,v in state['Rank'].iteritems():
if k in entry:
@ -227,27 +250,33 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
]))
# Update ship
if (entry['event'] in ['StartUp', 'Location', 'ShipyardNew'] or
if (entry['event'] in ['StartUp', 'Cargo'] or
(entry['event'] == 'Loadout' and this.shipswap) or
this.newuser):
if entry['event'] == 'ShipyardNew':
add_event('addCommanderShip', entry['timestamp'],
OrderedDict([
('shipType', state['ShipType']),
('shipGameID', state['ShipID']),
]))
add_event('setCommanderShip', entry['timestamp'],
OrderedDict([
('shipType', state['ShipType']),
('shipGameID', state['ShipID']),
('shipName', state['ShipName']), # Can be None
('shipIdent', state['ShipIdent']), # Can be None
('isCurrentShip', True),
]))
data = OrderedDict([
('shipType', state['ShipType']),
('shipGameID', state['ShipID']),
('shipName', state['ShipName']), # Can be None
('shipIdent', state['ShipIdent']), # Can be None
('isCurrentShip', True),
])
if state['HullValue']:
data['shipHullValue'] = state['HullValue']
if state['ModulesValue']:
data['shipModulesValue'] = state['ModulesValue']
data['shipRebuyCost'] = state['Rebuy']
add_event('setCommanderShip', entry['timestamp'], data)
this.loadout = OrderedDict([
('shipType', state['ShipType']),
('shipGameID', state['ShipID']),
('shipLoadout', state['Modules'].values()),
])
add_event('setCommanderShipLoadout', entry['timestamp'], this.loadout)
this.shipswap = False
# Update location
if (entry['event'] in ['StartUp', 'Location'] or this.newuser) and system:
if (entry['event'] in ['StartUp', 'Cargo'] or this.newuser) and system:
this.undocked = False
add_event('setCommanderTravelLocation', entry['timestamp'],
OrderedDict([
@ -262,7 +291,7 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
# Undocked and now docking again. Don't send.
this.undocked = False
elif this.suppress_docked:
# Don't send Docked event on new game - i.e. following 'Location' event
# Don't send initial Docked event on new game
this.suppress_docked = False
else:
add_event('addCommanderTravelDock', entry['timestamp'],
@ -325,7 +354,15 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
#
# Selling / swapping ships
if entry['event'] in ['ShipyardBuy', 'ShipyardSell', 'SellShipOnRebuy', 'ShipyardSwap']:
if entry['event'] == 'ShipyardNew':
add_event('addCommanderShip', entry['timestamp'],
OrderedDict([
('shipType', state['ShipType']),
('shipGameID', state['ShipID']),
]))
this.shipswap = True # Want subsequent Loadout event to be sent immediately
elif entry['event'] in ['ShipyardBuy', 'ShipyardSell', 'SellShipOnRebuy', 'ShipyardSwap']:
if entry['event'] == 'ShipyardSwap':
this.shipswap = True # Don't know new ship name and ident 'til the following Loadout event
if 'StoreShipID' in entry:
@ -363,6 +400,79 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
('transferTime', entry['TransferTime']),
]))
# Fleet
if entry['event'] == 'StoredShips':
fleet = sorted(
[{
'shipType': x['ShipType'],
'shipGameID': x['ShipID'],
'shipName': x.get('Name'),
'isHot': x['Hot'],
'starsystemName': entry['StarSystem'],
'stationName': entry['StationName'],
'marketID': entry['MarketID'],
} for x in entry['ShipsHere']] +
[{
'shipType': x['ShipType'],
'shipGameID': x['ShipID'],
'shipName': x.get('Name'),
'isHot': x['Hot'],
'starsystemName': x.get('StarSystem'), # Not present for ships in transit
'marketID': x.get('ShipMarketID'), # "
} for x in entry['ShipsRemote']],
key = itemgetter('shipGameID')
)
if this.fleet != fleet:
this.fleet = fleet
this.events = [x for x in this.events if x['eventName'] != 'setCommanderShip'] # Remove any unsent
for ship in this.fleet:
add_event('setCommanderShip', entry['timestamp'], ship)
# Loadout
if entry['event'] == 'Loadout':
loadout = OrderedDict([
('shipType', state['ShipType']),
('shipGameID', state['ShipID']),
('shipLoadout', state['Modules'].values()),
])
if this.loadout != loadout:
this.loadout = loadout
this.events = [x for x in this.events if x['eventName'] != 'setCommanderShipLoadout' or x['shipGameID'] != this.loadout['shipGameID']] # Remove any unsent for this ship
add_event('setCommanderShipLoadout', entry['timestamp'], this.loadout)
# Stored modules
if entry['event'] == 'StoredModules':
items = dict([(x['StorageSlot'], x) for x in entry['Items']]) # Impose an order
modules = []
for slot in sorted(items):
item = items[slot]
module = OrderedDict([
('itemName', item['Name']),
('itemValue', item['BuyPrice']),
('isHot', item['Hot']),
])
# Location can be absent if in transit
if 'StarSystem' in item:
module['starsystemName'] = item['StarSystem']
if 'MarketID' in item:
module['marketID'] = item['MarketID']
if 'EngineerModifications' in item:
module['engineering'] = OrderedDict([('blueprintName', item['EngineerModifications'])])
if 'Level' in item:
module['engineering']['blueprintLevel'] = item['Level']
if 'Quality' in item:
module['engineering']['blueprintQuality'] = item['Quality']
modules.append(module)
if this.storedmodules != modules:
# Only send on change
this.storedmodules = modules
this.events = [x for x in this.events if x['eventName'] != 'setCommanderStorageModules'] # Remove any unsent
add_event('setCommanderStorageModules', entry['timestamp'], this.storedmodules)
# Missions
if entry['event'] == 'MissionAccepted':
data = OrderedDict([
@ -410,14 +520,10 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
data['rewardPermits'] = [{ 'starsystemName': x } for x in entry['PermitsAwarded']]
if 'CommodityReward' in entry:
data['rewardCommodities'] = [{ 'itemName': x['Name'], 'itemCount': x['Count'] } for x in entry['CommodityReward']]
if 'MaterialsReward' in entry:
data['rewardMaterials'] = [{ 'itemName': x['Name'], 'itemCount': x['Count'] } for x in entry['MaterialsReward']]
add_event('setCommanderMissionCompleted', entry['timestamp'], data)
# Journal doesn't list rewarded materials directly, just as 'MaterialCollected'
elif (entry['event'] == 'MaterialCollected' and this.events and
this.events[-1]['eventName'] == 'setCommanderMissionCompleted' and
this.events[-1]['eventTimestamp'] == entry['timestamp']):
this.events[-1]['eventData']['rewardMaterials'] = [{ 'itemName': entry['Name'], 'itemCount': entry['Count'] }]
elif entry['event'] == 'MissionFailed':
add_event('setCommanderMissionFailed', entry['timestamp'], { 'missionGameID': entry['MissionID'] })
@ -483,6 +589,9 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
data['tierReached'] = int(goal['TierReached'].split()[-1])
if 'TopRankSize' in goal:
data['topRankSize'] = goal['TopRankSize']
if 'TopTier' in goal:
data['tierMax'] = int(goal['TopTier']['Name'].split()[-1])
data['completionBonus'] = goal['TopTier']['Bonus']
add_event('setCommunityGoal', entry['timestamp'], data)
data = OrderedDict([
@ -503,54 +612,15 @@ def cmdr_data(data, is_beta):
this.cmdr = data['commander']['name']
if config.getint('inara_out') and not is_beta and not this.multicrew and credentials(this.cmdr):
timestamp = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
assets = data['commander']['credits'] - data['commander'].get('debt', 0)
for ship in companion.listify(data.get('ships', [])):
if ship:
assets += ship['value']['total']
if this.needfleet:
if ship['id'] != data['commander']['currentShipId']:
add_event('setCommanderShip', timestamp,
OrderedDict([
('shipType', ship['name']),
('shipGameID', ship['id']),
('shipName', ship.get('shipName')), # Can be None
('shipIdent', ship.get('shipID')), # Can be None
('shipHullValue', ship['value']['hull']),
('shipModulesValue', ship['value']['modules']),
('starsystemName', ship['starsystem']['name']),
('stationName', ship['station']['name']),
]))
else:
add_event('setCommanderShip', timestamp,
OrderedDict([
('shipType', ship['name']),
('shipGameID', ship['id']),
('shipName', ship.get('shipName')), # Can be None
('shipIdent', ship.get('shipID')), # Can be None
('isCurrentShip', True),
('shipHullValue', ship['value']['hull']),
('shipModulesValue', ship['value']['modules']),
]))
if not (CREDIT_RATIO > this.lastcredits / data['commander']['credits'] > 1/CREDIT_RATIO):
this.events = [x for x in this.events if x['eventName'] != 'setCommanderCredits'] # Remove any unsent
add_event('setCommanderCredits', timestamp,
add_event('setCommanderCredits', time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
OrderedDict([
('commanderCredits', data['commander']['credits']),
('commanderAssets', assets),
('commanderLoan', data['commander'].get('debt', 0)),
]))
this.lastcredits = float(data['commander']['credits'])
# *Don't* queue a call to Inara if we're just updating credits - wait for next mandatory event
if this.needfleet:
call()
this.needfleet = False
def add_event(name, timestamp, data):
this.events.append(OrderedDict([
('eventName', name),
@ -603,7 +673,7 @@ def worker():
# Log individual errors and warnings
for data_event, reply_event in zip(data['events'], reply['events']):
if reply_event['eventStatus'] != 200:
print 'Inara\t%s %s\t%s' % (reply_event['eventStatus'], reply_event.get('eventStatusText', ''), json.dumps(data_event, separators = (',', ': ')))
print 'Inara\t%s %s\t%s' % (reply_event['eventStatus'], reply_event.get('eventStatusText', ''), json.dumps(data_event))
if reply_event['eventStatus'] // 100 != 2:
plug.show_error(_('Error: Inara {MSG}').format(MSG = '%s, %s' % (data_event['eventName'], reply_event.get('eventStatusText', reply_event['eventStatus']))))
if data_event['eventName'] in ['addCommanderTravelDock', 'addCommanderTravelFSDJump', 'setCommanderTravelLocation']:

128
ships.p
View File

@ -10,190 +10,196 @@ S'hullMass'
p6
I35
saa(lp7
S'Anaconda'
S'Alliance Chieftain'
p8
a(dp9
g6
I400
I420
saa(lp10
S'Asp Explorer'
S'Anaconda'
p11
a(dp12
g6
I280
I400
saa(lp13
S'Asp Scout'
S'Asp Explorer'
p14
a(dp15
g6
I150
I280
saa(lp16
S'Beluga Liner'
S'Asp Scout'
p17
a(dp18
g6
I950
I150
saa(lp19
S'Cobra MkIII'
S'Beluga Liner'
p20
a(dp21
g6
I180
I950
saa(lp22
S'Cobra MkIV'
S'Cobra MkIII'
p23
a(dp24
g6
I210
I180
saa(lp25
S'Diamondback Explorer'
S'Cobra MkIV'
p26
a(dp27
g6
I260
I210
saa(lp28
S'Diamondback Scout'
S'Diamondback Explorer'
p29
a(dp30
g6
I170
I260
saa(lp31
S'Dolphin'
S'Diamondback Scout'
p32
a(dp33
g6
I140
I170
saa(lp34
S'Eagle'
S'Dolphin'
p35
a(dp36
g6
I50
I140
saa(lp37
S'Federal Assault Ship'
S'Eagle'
p38
a(dp39
g6
I480
I50
saa(lp40
S'Federal Corvette'
S'Federal Assault Ship'
p41
a(dp42
g6
I900
I480
saa(lp43
S'Federal Dropship'
S'Federal Corvette'
p44
a(dp45
g6
I580
I900
saa(lp46
S'Federal Gunship'
S'Federal Dropship'
p47
a(dp48
g6
I580
saa(lp49
S'Fer-de-Lance'
S'Federal Gunship'
p50
a(dp51
g6
I250
I580
saa(lp52
S'Hauler'
S'Fer-de-Lance'
p53
a(dp54
g6
I14
I250
saa(lp55
S'Imperial Clipper'
S'Hauler'
p56
a(dp57
g6
I400
I14
saa(lp58
S'Imperial Courier'
S'Imperial Clipper'
p59
a(dp60
g6
I35
I400
saa(lp61
S'Imperial Cutter'
S'Imperial Courier'
p62
a(dp63
g6
I1100
I35
saa(lp64
S'Imperial Eagle'
S'Imperial Cutter'
p65
a(dp66
g6
I50
I1100
saa(lp67
S'Keelback'
S'Imperial Eagle'
p68
a(dp69
g6
I180
I50
saa(lp70
S'Orca'
S'Keelback'
p71
a(dp72
g6
I290
I180
saa(lp73
S'Python'
S'Orca'
p74
a(dp75
g6
I350
I290
saa(lp76
S'Sidewinder'
S'Python'
p77
a(dp78
g6
I25
I350
saa(lp79
S'Type-10 Defender'
S'Sidewinder'
p80
a(dp81
g6
I1200
I25
saa(lp82
S'Type-6 Transporter'
S'Type-10 Defender'
p83
a(dp84
g6
I155
I1200
saa(lp85
S'Type-7 Transporter'
S'Type-6 Transporter'
p86
a(dp87
g6
I420
I155
saa(lp88
S'Type-9 Heavy'
S'Type-7 Transporter'
p89
a(dp90
g6
I1000
I420
saa(lp91
S'Viper MkIII'
S'Type-9 Heavy'
p92
a(dp93
g6
I50
I850
saa(lp94
S'Viper MkIV'
S'Viper MkIII'
p95
a(dp96
g6
I190
I50
saa(lp97
S'Vulture'
S'Viper MkIV'
p98
a(dp99
g6
I190
saa(lp100
S'Vulture'
p101
a(dp102
g6
I230
saatRp100
saatRp103
.