mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2025-04-18 09:57:40 +03:00
Merge branch 'beyond'
This commit is contained in:
commit
c79398eb12
@ -57,6 +57,7 @@ import prefs
|
|||||||
import plug
|
import plug
|
||||||
from hotkey import hotkeymgr
|
from hotkey import hotkeymgr
|
||||||
from monitor import monitor
|
from monitor import monitor
|
||||||
|
from dashboard import dashboard
|
||||||
from theme import theme
|
from theme import theme
|
||||||
|
|
||||||
|
|
||||||
@ -269,6 +270,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('<<DashboardEvent>>', self.dashboard_event) # Dashboard monitoring
|
||||||
self.w.bind_all('<<PluginError>>', self.plugin_error) # Statusbar
|
self.w.bind_all('<<PluginError>>', self.plugin_error) # Statusbar
|
||||||
self.w.bind_all('<<Quit>>', self.onexit) # Updater
|
self.w.bind_all('<<Quit>>', self.onexit) # Updater
|
||||||
|
|
||||||
@ -627,6 +629,11 @@ class AppWindow:
|
|||||||
if not config.getint('hotkey_mute'):
|
if not config.getint('hotkey_mute'):
|
||||||
hotkeymgr.play_bad()
|
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
|
# Don't send to EDDN while on crew
|
||||||
if monitor.state['Captain']:
|
if monitor.state['Captain']:
|
||||||
return
|
return
|
||||||
@ -678,6 +685,17 @@ class AppWindow:
|
|||||||
if not config.getint('hotkey_mute'):
|
if not config.getint('hotkey_mute'):
|
||||||
hotkeymgr.play_bad()
|
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
|
# Display asynchronous error from plugin
|
||||||
def plugin_error(self, event=None):
|
def plugin_error(self, event=None):
|
||||||
if plug.last_error.get('msg'):
|
if plug.last_error.get('msg'):
|
||||||
@ -691,6 +709,14 @@ class AppWindow:
|
|||||||
if not monitor.cmdr or not monitor.mode:
|
if not monitor.cmdr or not monitor.mode:
|
||||||
return False # In CQC - do nothing
|
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.status['text'] = _('Fetching data...')
|
||||||
self.w.update_idletasks()
|
self.w.update_idletasks()
|
||||||
try:
|
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
|
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:
|
else:
|
||||||
self.status['text'] = ''
|
self.status['text'] = ''
|
||||||
if config.getint('shipyard') == config.SHIPYARD_EDSHIPYARD:
|
return coriolis.url(data, monitor.is_beta)
|
||||||
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
|
|
||||||
|
|
||||||
def cooldown(self):
|
def cooldown(self):
|
||||||
if time() < self.holdofftime:
|
if time() < self.holdofftime:
|
||||||
@ -779,6 +799,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()
|
||||||
|
dashboard.close()
|
||||||
monitor.close()
|
monitor.close()
|
||||||
plug.notify_stop()
|
plug.notify_stop()
|
||||||
self.eddn.close()
|
self.eddn.close()
|
||||||
|
14
PLUGINS.md
14
PLUGINS.md
@ -106,7 +106,7 @@ this.status["text"] = "Happy!"
|
|||||||
|
|
||||||
## Events
|
## 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
|
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']))
|
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
|
### 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.
|
||||||
@ -144,7 +154,7 @@ The data is a dictionary and full of lots of wonderful stuff!
|
|||||||
|
|
||||||
## Error messages
|
## 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.
|
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.
|
||||||
|
|
||||||
|
@ -73,6 +73,7 @@ ship_map = {
|
|||||||
'type7' : 'Type-7 Transporter',
|
'type7' : 'Type-7 Transporter',
|
||||||
'type9' : 'Type-9 Heavy',
|
'type9' : 'Type-9 Heavy',
|
||||||
'type9_military' : 'Type-10 Defender',
|
'type9_military' : 'Type-10 Defender',
|
||||||
|
'typex' : 'Alliance Chieftain',
|
||||||
'viper' : 'Viper MkIII',
|
'viper' : 'Viper MkIII',
|
||||||
'viper_mkiv' : 'Viper MkIV',
|
'viper_mkiv' : 'Viper MkIV',
|
||||||
'vulture' : 'Vulture',
|
'vulture' : 'Vulture',
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit a3ae3e34ad879d1504a7d2fdc3fce4854820051b
|
Subproject commit 44d57f26a8099cfa8d91815f453c7bba303a63ec
|
@ -169,11 +169,16 @@ if __name__ == "__main__":
|
|||||||
else:
|
else:
|
||||||
modules[key] = { 'mass': m.get('mass', 0) } # Some modules don't have mass
|
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, '1', 'E')] = {'mass': 1.3}
|
||||||
modules[('Decontamination Limpet Controller', None, '3', 'E')] = {'mass': 2}
|
modules[('Decontamination Limpet Controller', None, '3', 'E')] = {'mass': 2}
|
||||||
modules[('Decontamination Limpet Controller', None, '5', 'E')] = {'mass': 20}
|
modules[('Decontamination Limpet Controller', None, '5', 'E')] = {'mass': 20}
|
||||||
modules[('Decontamination Limpet Controller', None, '7', 'E')] = {'mass': 128}
|
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
|
modules = OrderedDict([(k,modules[k]) for k in sorted(modules)]) # sort for easier diffing
|
||||||
cPickle.dump(modules, open('modules.p', 'wb'))
|
cPickle.dump(modules, open('modules.p', 'wb'))
|
||||||
|
131
dashboard.py
Normal file
131
dashboard.py
Normal 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()
|
@ -13,6 +13,7 @@ import gzip
|
|||||||
from config import config
|
from config import config
|
||||||
import companion
|
import companion
|
||||||
import outfitting
|
import outfitting
|
||||||
|
from monitor import monitor
|
||||||
|
|
||||||
# Map API ship names to E:D Shipyard ship names
|
# Map API ship names to E:D Shipyard ship names
|
||||||
ship_map = dict(companion.ship_map)
|
ship_map = dict(companion.ship_map)
|
||||||
@ -162,9 +163,11 @@ def export(data, filename=None):
|
|||||||
|
|
||||||
|
|
||||||
# Return a URL for the current ship
|
# 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()
|
out = StringIO.StringIO()
|
||||||
with gzip.GzipFile(fileobj=out, mode='w') as f:
|
with gzip.GzipFile(fileobj=out, mode='w') as f:
|
||||||
|
164
monitor.py
164
monitor.py
@ -113,14 +113,19 @@ class EDLogs(FileSystemEventHandler):
|
|||||||
'Raw' : defaultdict(int),
|
'Raw' : defaultdict(int),
|
||||||
'Manufactured' : defaultdict(int),
|
'Manufactured' : defaultdict(int),
|
||||||
'Encoded' : defaultdict(int),
|
'Encoded' : defaultdict(int),
|
||||||
'PaintJob' : None,
|
'Rank' : {},
|
||||||
'Rank' : { 'Combat': None, 'Trade': None, 'Explore': None, 'Empire': None, 'Federation': None, 'CQC': None },
|
'Reputation' : {},
|
||||||
|
'Statistics' : {},
|
||||||
'Role' : None, # Crew role - None, Idle, FireCon, FighterCon
|
'Role' : None, # Crew role - None, Idle, FireCon, FighterCon
|
||||||
'Friends' : set(), # Online friends
|
'Friends' : set(), # Online friends
|
||||||
'ShipID' : None,
|
'ShipID' : None,
|
||||||
'ShipIdent' : None,
|
'ShipIdent' : None,
|
||||||
'ShipName' : None,
|
'ShipName' : None,
|
||||||
'ShipType' : None,
|
'ShipType' : None,
|
||||||
|
'HullValue' : None,
|
||||||
|
'ModulesValue' : None,
|
||||||
|
'Rebuy' : None,
|
||||||
|
'Modules' : None,
|
||||||
}
|
}
|
||||||
|
|
||||||
def start(self, root):
|
def start(self, root):
|
||||||
@ -224,24 +229,18 @@ class EDLogs(FileSystemEventHandler):
|
|||||||
if self.live:
|
if self.live:
|
||||||
if self.game_was_running:
|
if self.game_was_running:
|
||||||
# Game is running locally
|
# 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:
|
if self.station:
|
||||||
entry = OrderedDict([
|
entry['StationName'] = self.station
|
||||||
('timestamp', strftime('%Y-%m-%dT%H:%M:%SZ', gmtime())),
|
entry['StationType'] = self.stationtype
|
||||||
('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),
|
|
||||||
])
|
|
||||||
self.event_queue.append(json.dumps(entry, separators=(', ', ':')))
|
self.event_queue.append(json.dumps(entry, separators=(', ', ':')))
|
||||||
else:
|
else:
|
||||||
self.event_queue.append(None) # Generate null event to update the display (with possibly out-of-date info)
|
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),
|
'Raw' : defaultdict(int),
|
||||||
'Manufactured' : defaultdict(int),
|
'Manufactured' : defaultdict(int),
|
||||||
'Encoded' : defaultdict(int),
|
'Encoded' : defaultdict(int),
|
||||||
'PaintJob' : None,
|
'Rank' : {},
|
||||||
'Rank' : { 'Combat': None, 'Trade': None, 'Explore': None, 'Empire': None, 'Federation': None, 'CQC': None },
|
'Reputation' : {},
|
||||||
|
'Statistics' : {},
|
||||||
'Role' : None,
|
'Role' : None,
|
||||||
'Friends' : set(),
|
'Friends' : set(),
|
||||||
'ShipID' : None,
|
'ShipID' : None,
|
||||||
'ShipIdent' : None,
|
'ShipIdent' : None,
|
||||||
'ShipName' : None,
|
'ShipName' : None,
|
||||||
'ShipType' : 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':
|
elif entry['event'] == 'LoadGame':
|
||||||
self.live = True
|
|
||||||
self.cmdr = entry['Commander']
|
self.cmdr = entry['Commander']
|
||||||
self.mode = entry.get('GameMode') # 'Open', 'Solo', 'Group', or None for CQC (and Training - but no LoadGame event)
|
self.mode = entry.get('GameMode') # 'Open', 'Solo', 'Group', or None for CQC (and Training - but no LoadGame event)
|
||||||
self.group = entry.get('Group')
|
self.group = entry.get('Group')
|
||||||
@ -350,7 +355,9 @@ class EDLogs(FileSystemEventHandler):
|
|||||||
'Captain' : None,
|
'Captain' : None,
|
||||||
'Credits' : entry['Credits'],
|
'Credits' : entry['Credits'],
|
||||||
'Loan' : entry['Loan'],
|
'Loan' : entry['Loan'],
|
||||||
'Rank' : { 'Combat': None, 'Trade': None, 'Explore': None, 'Empire': None, 'Federation': None, 'CQC': None },
|
'Rank' : {},
|
||||||
|
'Reputation' : {},
|
||||||
|
'Statistics' : {},
|
||||||
'Role' : None,
|
'Role' : None,
|
||||||
})
|
})
|
||||||
elif entry['event'] == 'NewCommander':
|
elif entry['event'] == 'NewCommander':
|
||||||
@ -367,25 +374,45 @@ class EDLogs(FileSystemEventHandler):
|
|||||||
self.state['ShipIdent'] = None
|
self.state['ShipIdent'] = None
|
||||||
self.state['ShipName'] = None
|
self.state['ShipName'] = None
|
||||||
self.state['ShipType'] = self.canonicalise(entry['ShipType'])
|
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':
|
elif entry['event'] == 'ShipyardSwap':
|
||||||
self.state['ShipID'] = entry['ShipID']
|
self.state['ShipID'] = entry['ShipID']
|
||||||
self.state['ShipIdent'] = None
|
self.state['ShipIdent'] = None
|
||||||
self.state['ShipName'] = None
|
self.state['ShipName'] = None
|
||||||
self.state['ShipType'] = self.canonicalise(entry['ShipType'])
|
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
|
elif entry['event'] == 'Loadout': # Note: Precedes LoadGame, ShipyardNew, follows ShipyardSwap, ShipyardBuy
|
||||||
self.state['ShipID'] = entry['ShipID']
|
self.state['ShipID'] = entry['ShipID']
|
||||||
self.state['ShipIdent'] = entry['ShipIdent']
|
self.state['ShipIdent'] = entry['ShipIdent']
|
||||||
self.state['ShipName'] = entry['ShipName']
|
self.state['ShipName'] = entry['ShipName']
|
||||||
self.state['ShipType'] = self.canonicalise(entry['Ship'])
|
self.state['ShipType'] = self.canonicalise(entry['Ship'])
|
||||||
# Ignore other Modules since they're missing Engineer modification details
|
self.state['HullValue'] = entry.get('HullValue') # not present on exiting Outfitting
|
||||||
self.state['PaintJob'] = 'paintjob_%s_default_defaultpaintjob' % self.state['ShipType']
|
self.state['ModulesValue'] = entry.get('ModulesValue') # "
|
||||||
for module in entry['Modules']:
|
self.state['Rebuy'] = entry.get('Rebuy')
|
||||||
if module.get('Slot') == 'PaintJob' and module.get('Item'):
|
self.state['Modules'] = dict([(thing['Slot'], thing) for thing in entry['Modules']])
|
||||||
self.state['PaintJob'] = self.canonicalise(module['Item'])
|
elif entry['event'] == 'ModuleBuy':
|
||||||
elif entry['event'] in ['ModuleBuy', 'ModuleSell'] and entry['Slot'] == 'PaintJob':
|
self.state['Modules'][entry['Slot']] = { 'Slot' : entry['Slot'],
|
||||||
self.state['PaintJob'] = self.canonicalise(entry.get('BuyItem'))
|
'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']:
|
elif entry['event'] in ['Undocked']:
|
||||||
self.station = None
|
self.station = None
|
||||||
self.stationtype = None
|
self.stationtype = None
|
||||||
@ -403,21 +430,30 @@ class EDLogs(FileSystemEventHandler):
|
|||||||
entry.get('StationName')) # May be None
|
entry.get('StationName')) # May be None
|
||||||
self.stationtype = entry.get('StationType') # May be None
|
self.stationtype = entry.get('StationType') # May be None
|
||||||
self.stationservices = entry.get('StationServices') # None under E:D < 2.4
|
self.stationservices = entry.get('StationServices') # None under E:D < 2.4
|
||||||
|
elif entry['event'] == 'ApproachBody':
|
||||||
|
self.planet = entry['Body']
|
||||||
elif entry['event'] == 'SupercruiseExit':
|
elif entry['event'] == 'SupercruiseExit':
|
||||||
self.planet = entry.get('Body') if entry.get('BodyType') == 'Planet' else None
|
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
|
self.planet = None
|
||||||
|
|
||||||
elif entry['event'] in ['Rank', 'Promotion']:
|
elif entry['event'] in ['Rank', 'Promotion']:
|
||||||
for k,v in entry.iteritems():
|
payload = dict(entry)
|
||||||
if k in self.state['Rank']:
|
payload.pop('event')
|
||||||
self.state['Rank'][k] = (v,0)
|
payload.pop('timestamp')
|
||||||
|
for k,v in payload.iteritems():
|
||||||
|
self.state['Rank'][k] = (v,0)
|
||||||
elif entry['event'] == 'Progress':
|
elif entry['event'] == 'Progress':
|
||||||
for k,v in entry.iteritems():
|
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
|
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':
|
elif entry['event'] == 'Cargo':
|
||||||
self.live = True # First event in 2.3
|
|
||||||
self.state['Cargo'] = defaultdict(int)
|
self.state['Cargo'] = defaultdict(int)
|
||||||
self.state['Cargo'].update({ self.canonicalise(x['Name']): x['Count'] for x in entry['Inventory'] })
|
self.state['Cargo'].update({ self.canonicalise(x['Name']): x['Count'] for x in entry['Inventory'] })
|
||||||
elif entry['event'] in ['CollectCargo', 'MarketBuy', 'BuyDrones', 'MiningRefined']:
|
elif entry['event'] in ['CollectCargo', 'MarketBuy', 'BuyDrones', 'MiningRefined']:
|
||||||
@ -432,6 +468,11 @@ class EDLogs(FileSystemEventHandler):
|
|||||||
for reward in entry.get('CommodityReward', []):
|
for reward in entry.get('CommodityReward', []):
|
||||||
commodity = self.canonicalise(reward['Name'])
|
commodity = self.canonicalise(reward['Name'])
|
||||||
self.state['Cargo'][commodity] += reward.get('Count', 1)
|
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':
|
elif entry['event'] == 'SearchAndRescue':
|
||||||
for item in entry.get('Items', []):
|
for item in entry.get('Items', []):
|
||||||
commodity = self.canonicalise(item['Name'])
|
commodity = self.canonicalise(item['Name'])
|
||||||
@ -451,15 +492,39 @@ class EDLogs(FileSystemEventHandler):
|
|||||||
self.state[entry['Category']][material] -= entry['Count']
|
self.state[entry['Category']][material] -= entry['Count']
|
||||||
if self.state[entry['Category']][material] <= 0:
|
if self.state[entry['Category']][material] <= 0:
|
||||||
self.state[entry['Category']].pop(material)
|
self.state[entry['Category']].pop(material)
|
||||||
elif entry['event'] in ['EngineerCraft', 'Synthesis']:
|
elif entry['event'] == 'Synthesis':
|
||||||
for category in ['Raw', 'Manufactured', 'Encoded']:
|
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'])
|
material = self.canonicalise(x['Name'])
|
||||||
if material in self.state[category]:
|
if material in self.state[category]:
|
||||||
self.state[category][material] -= x['Count']
|
self.state[category][material] -= x['Count']
|
||||||
if self.state[category][material] <= 0:
|
if self.state[category][material] <= 0:
|
||||||
self.state[category].pop(material)
|
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':
|
elif entry['event'] == 'EngineerContribution':
|
||||||
commodity = self.canonicalise(entry.get('Commodity'))
|
commodity = self.canonicalise(entry.get('Commodity'))
|
||||||
if commodity:
|
if commodity:
|
||||||
@ -579,5 +644,24 @@ class EDLogs(FileSystemEventHandler):
|
|||||||
return False
|
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
|
# singleton
|
||||||
monitor = EDLogs()
|
monitor = EDLogs()
|
||||||
|
@ -261,6 +261,7 @@ internal_map = {
|
|||||||
'passengercabin' : 'Passenger Cabin',
|
'passengercabin' : 'Passenger Cabin',
|
||||||
'prospector' : 'Prospector Limpet Controller',
|
'prospector' : 'Prospector Limpet Controller',
|
||||||
'refinery' : 'Refinery',
|
'refinery' : 'Refinery',
|
||||||
|
'recon' : 'Recon Limpet Controller',
|
||||||
'repair' : 'Repair Limpet Controller',
|
'repair' : 'Repair Limpet Controller',
|
||||||
'repairer' : 'Auto Field-Maintenance Unit',
|
'repairer' : 'Auto Field-Maintenance Unit',
|
||||||
'resourcesiphon' : 'Hatch Breaker Limpet Controller',
|
'resourcesiphon' : 'Hatch Breaker Limpet Controller',
|
||||||
|
21
plug.py
21
plug.py
@ -226,6 +226,27 @@ def notify_journal_entry(cmdr, is_beta, system, station, entry, state):
|
|||||||
return error
|
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):
|
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.
|
||||||
|
@ -34,11 +34,14 @@ this.session = requests.Session()
|
|||||||
this.queue = Queue() # Items to be sent to EDSM by worker thread
|
this.queue = Queue() # Items to be sent to EDSM by worker thread
|
||||||
this.discardedEvents = [] # List discarded events from EDSM
|
this.discardedEvents = [] # List discarded events from EDSM
|
||||||
this.lastship = None # Description of last ship that we sent to 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
|
this.lastlookup = False # whether the last lookup succeeded
|
||||||
|
|
||||||
# Game state
|
# Game state
|
||||||
this.multicrew = False # don't send captain's ship info to EDSM while on a crew
|
this.multicrew = False # don't send captain's ship info to EDSM while on a crew
|
||||||
this.coordinates = None
|
this.coordinates = None
|
||||||
|
this.newgame = False # starting up - batch initial burst of events
|
||||||
|
this.newgame_docked = False # starting up while docked
|
||||||
|
|
||||||
def plugin_start():
|
def plugin_start():
|
||||||
# Can't be earlier since can only call PhotoImage after window is created
|
# 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':
|
elif entry['event'] == 'LoadGame':
|
||||||
this.coordinates = None
|
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
|
# 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:
|
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
|
# Introduce transient states into the event
|
||||||
@ -210,15 +223,7 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
|
|||||||
entry.update(transient)
|
entry.update(transient)
|
||||||
|
|
||||||
if entry['event'] == 'LoadGame':
|
if entry['event'] == 'LoadGame':
|
||||||
# Synthesise Cargo and Materials events on LoadGame since we will have missed them because Cmdr was unknown
|
# Synthesise Materials events on LoadGame since we will have missed it
|
||||||
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))
|
|
||||||
|
|
||||||
materials = {
|
materials = {
|
||||||
'timestamp': entry['timestamp'],
|
'timestamp': entry['timestamp'],
|
||||||
'event': 'Materials',
|
'event': 'Materials',
|
||||||
@ -231,6 +236,14 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
|
|||||||
|
|
||||||
this.queue.put((cmdr, entry))
|
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
|
# Update system data
|
||||||
def cmdr_data(data, is_beta):
|
def cmdr_data(data, is_beta):
|
||||||
@ -258,10 +271,6 @@ def cmdr_data(data, is_beta):
|
|||||||
this.queue.put((cmdr, {
|
this.queue.put((cmdr, {
|
||||||
'event': 'Coriolis', 'timestamp': timestamp, '_shipId': data['ship']['id'], 'url': coriolis.url(data, is_beta)
|
'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
|
this.lastship = ship
|
||||||
|
|
||||||
|
|
||||||
@ -310,14 +319,14 @@ def worker():
|
|||||||
if msgnum // 100 == 2:
|
if msgnum // 100 == 2:
|
||||||
print('EDSM\t%s %s\t%s' % (msgnum, msg, json.dumps(pending, separators = (',', ': '))))
|
print('EDSM\t%s %s\t%s' % (msgnum, msg, json.dumps(pending, separators = (',', ': '))))
|
||||||
plug.show_error(_('Error: EDSM {MSG}').format(MSG=msg))
|
plug.show_error(_('Error: EDSM {MSG}').format(MSG=msg))
|
||||||
elif not closing:
|
else:
|
||||||
# Update main window's system status
|
for e, r in zip(pending, reply['events']):
|
||||||
for i in range(len(pending) - 1, -1, -1):
|
if not closing and e['event'] in ['StartUp', 'Location', 'FSDJump']:
|
||||||
if pending[i]['event'] in ['StartUp', 'Location', 'FSDJump']:
|
# Update main window's system status
|
||||||
this.lastlookup = reply['events'][i]
|
this.lastlookup = r
|
||||||
this.system.event_generate('<<EDSMStatus>>', when="tail") # calls update_status in main thread
|
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 = []
|
pending = []
|
||||||
|
|
||||||
break
|
break
|
||||||
@ -334,10 +343,16 @@ def worker():
|
|||||||
# Whether any of the entries should be sent immediately
|
# Whether any of the entries should be sent immediately
|
||||||
def should_send(entries):
|
def should_send(entries):
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
if (entry['event'] not in ['CommunityGoal', # Spammed periodically
|
if (entry['event'] == 'Cargo' and not this.newgame_docked) or entry['event'] == 'Docked':
|
||||||
'Cargo', 'Loadout', 'Materials', 'LoadGame', 'Rank', 'Progress', # Will be followed by 'Docked' or 'Location'
|
# Cargo is the last event on startup, unless starting when docked in which case Docked is the last event
|
||||||
'ShipyardBuy', 'ShipyardNew', 'ShipyardSwap'] and # "
|
this.newgame = False
|
||||||
not (entry['event'] == 'Location' and entry.get('Docked'))): # "
|
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 True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
210
plugins/inara.py
210
plugins/inara.py
@ -7,6 +7,7 @@ import json
|
|||||||
import requests
|
import requests
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
from operator import itemgetter
|
||||||
from Queue import Queue
|
from Queue import Queue
|
||||||
from threading import Thread
|
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.multicrew = False # don't send captain's ship info to Inara while on a crew
|
||||||
this.newuser = False # just entered API Key
|
this.newuser = False # just entered API Key
|
||||||
this.undocked = False # just undocked
|
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.cargo = None
|
||||||
this.materials = None
|
this.materials = None
|
||||||
this.lastcredits = 0 # Send credit update soon after Startup / new game
|
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
|
this.shipswap = False # just swapped ship
|
||||||
|
|
||||||
# URLs
|
# URLs
|
||||||
@ -154,7 +157,9 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
|
|||||||
this.cargo = None
|
this.cargo = None
|
||||||
this.materials = None
|
this.materials = None
|
||||||
this.lastcredits = 0
|
this.lastcredits = 0
|
||||||
this.needfleet = True
|
this.storedmodules = None
|
||||||
|
this.loadout = None
|
||||||
|
this.fleet = None
|
||||||
this.shipswap = False
|
this.shipswap = False
|
||||||
elif entry['event'] in ['Resurrect', 'ShipyardBuy', 'ShipyardSell', 'SellShipOnRebuy']:
|
elif entry['event'] in ['Resurrect', 'ShipyardBuy', 'ShipyardSell', 'SellShipOnRebuy']:
|
||||||
# Events that mean a significant change in credits so we should send credits after next "Update"
|
# 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
|
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.
|
# Always send an update on Docked, FSDJump, Undocked+SuperCruise, Promotion and EngineerProgress.
|
||||||
# Also send material and cargo (if changed) whenever we send an update.
|
# 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:
|
try:
|
||||||
old_events = len(this.events) # Will only send existing events if we add a new event below
|
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
|
# 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():
|
for k,v in state['Rank'].iteritems():
|
||||||
if v is not None:
|
if v is not None:
|
||||||
add_event('setCommanderRankPilot', entry['timestamp'],
|
add_event('setCommanderRankPilot', entry['timestamp'],
|
||||||
@ -181,6 +195,15 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
|
|||||||
('rankValue', v[0]),
|
('rankValue', v[0]),
|
||||||
('rankProgress', v[1] / 100.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':
|
elif entry['event'] == 'Promotion':
|
||||||
for k,v in state['Rank'].iteritems():
|
for k,v in state['Rank'].iteritems():
|
||||||
if k in entry:
|
if k in entry:
|
||||||
@ -227,27 +250,33 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
|
|||||||
]))
|
]))
|
||||||
|
|
||||||
# Update ship
|
# Update ship
|
||||||
if (entry['event'] in ['StartUp', 'Location', 'ShipyardNew'] or
|
if (entry['event'] in ['StartUp', 'Cargo'] or
|
||||||
(entry['event'] == 'Loadout' and this.shipswap) or
|
(entry['event'] == 'Loadout' and this.shipswap) or
|
||||||
this.newuser):
|
this.newuser):
|
||||||
if entry['event'] == 'ShipyardNew':
|
data = OrderedDict([
|
||||||
add_event('addCommanderShip', entry['timestamp'],
|
('shipType', state['ShipType']),
|
||||||
OrderedDict([
|
('shipGameID', state['ShipID']),
|
||||||
('shipType', state['ShipType']),
|
('shipName', state['ShipName']), # Can be None
|
||||||
('shipGameID', state['ShipID']),
|
('shipIdent', state['ShipIdent']), # Can be None
|
||||||
]))
|
('isCurrentShip', True),
|
||||||
add_event('setCommanderShip', entry['timestamp'],
|
])
|
||||||
OrderedDict([
|
if state['HullValue']:
|
||||||
('shipType', state['ShipType']),
|
data['shipHullValue'] = state['HullValue']
|
||||||
('shipGameID', state['ShipID']),
|
if state['ModulesValue']:
|
||||||
('shipName', state['ShipName']), # Can be None
|
data['shipModulesValue'] = state['ModulesValue']
|
||||||
('shipIdent', state['ShipIdent']), # Can be None
|
data['shipRebuyCost'] = state['Rebuy']
|
||||||
('isCurrentShip', True),
|
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
|
this.shipswap = False
|
||||||
|
|
||||||
# Update location
|
# 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
|
this.undocked = False
|
||||||
add_event('setCommanderTravelLocation', entry['timestamp'],
|
add_event('setCommanderTravelLocation', entry['timestamp'],
|
||||||
OrderedDict([
|
OrderedDict([
|
||||||
@ -262,7 +291,7 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
|
|||||||
# Undocked and now docking again. Don't send.
|
# Undocked and now docking again. Don't send.
|
||||||
this.undocked = False
|
this.undocked = False
|
||||||
elif this.suppress_docked:
|
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
|
this.suppress_docked = False
|
||||||
else:
|
else:
|
||||||
add_event('addCommanderTravelDock', entry['timestamp'],
|
add_event('addCommanderTravelDock', entry['timestamp'],
|
||||||
@ -325,7 +354,15 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
|
|||||||
#
|
#
|
||||||
|
|
||||||
# Selling / swapping ships
|
# 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':
|
if entry['event'] == 'ShipyardSwap':
|
||||||
this.shipswap = True # Don't know new ship name and ident 'til the following Loadout event
|
this.shipswap = True # Don't know new ship name and ident 'til the following Loadout event
|
||||||
if 'StoreShipID' in entry:
|
if 'StoreShipID' in entry:
|
||||||
@ -363,6 +400,79 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
|
|||||||
('transferTime', entry['TransferTime']),
|
('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
|
# Missions
|
||||||
if entry['event'] == 'MissionAccepted':
|
if entry['event'] == 'MissionAccepted':
|
||||||
data = OrderedDict([
|
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']]
|
data['rewardPermits'] = [{ 'starsystemName': x } for x in entry['PermitsAwarded']]
|
||||||
if 'CommodityReward' in entry:
|
if 'CommodityReward' in entry:
|
||||||
data['rewardCommodities'] = [{ 'itemName': x['Name'], 'itemCount': x['Count'] } for x in entry['CommodityReward']]
|
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)
|
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':
|
elif entry['event'] == 'MissionFailed':
|
||||||
add_event('setCommanderMissionFailed', entry['timestamp'], { 'missionGameID': entry['MissionID'] })
|
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])
|
data['tierReached'] = int(goal['TierReached'].split()[-1])
|
||||||
if 'TopRankSize' in goal:
|
if 'TopRankSize' in goal:
|
||||||
data['topRankSize'] = goal['TopRankSize']
|
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)
|
add_event('setCommunityGoal', entry['timestamp'], data)
|
||||||
|
|
||||||
data = OrderedDict([
|
data = OrderedDict([
|
||||||
@ -503,54 +612,15 @@ def cmdr_data(data, is_beta):
|
|||||||
this.cmdr = data['commander']['name']
|
this.cmdr = data['commander']['name']
|
||||||
|
|
||||||
if config.getint('inara_out') and not is_beta and not this.multicrew and credentials(this.cmdr):
|
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):
|
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
|
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([
|
OrderedDict([
|
||||||
('commanderCredits', data['commander']['credits']),
|
('commanderCredits', data['commander']['credits']),
|
||||||
('commanderAssets', assets),
|
|
||||||
('commanderLoan', data['commander'].get('debt', 0)),
|
('commanderLoan', data['commander'].get('debt', 0)),
|
||||||
]))
|
]))
|
||||||
this.lastcredits = float(data['commander']['credits'])
|
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):
|
def add_event(name, timestamp, data):
|
||||||
this.events.append(OrderedDict([
|
this.events.append(OrderedDict([
|
||||||
('eventName', name),
|
('eventName', name),
|
||||||
@ -603,7 +673,7 @@ def worker():
|
|||||||
# Log individual errors and warnings
|
# Log individual errors and warnings
|
||||||
for data_event, reply_event in zip(data['events'], reply['events']):
|
for data_event, reply_event in zip(data['events'], reply['events']):
|
||||||
if reply_event['eventStatus'] != 200:
|
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:
|
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']))))
|
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']:
|
if data_event['eventName'] in ['addCommanderTravelDock', 'addCommanderTravelFSDJump', 'setCommanderTravelLocation']:
|
||||||
|
128
ships.p
128
ships.p
@ -10,190 +10,196 @@ S'hullMass'
|
|||||||
p6
|
p6
|
||||||
I35
|
I35
|
||||||
saa(lp7
|
saa(lp7
|
||||||
S'Anaconda'
|
S'Alliance Chieftain'
|
||||||
p8
|
p8
|
||||||
a(dp9
|
a(dp9
|
||||||
g6
|
g6
|
||||||
I400
|
I420
|
||||||
saa(lp10
|
saa(lp10
|
||||||
S'Asp Explorer'
|
S'Anaconda'
|
||||||
p11
|
p11
|
||||||
a(dp12
|
a(dp12
|
||||||
g6
|
g6
|
||||||
I280
|
I400
|
||||||
saa(lp13
|
saa(lp13
|
||||||
S'Asp Scout'
|
S'Asp Explorer'
|
||||||
p14
|
p14
|
||||||
a(dp15
|
a(dp15
|
||||||
g6
|
g6
|
||||||
I150
|
I280
|
||||||
saa(lp16
|
saa(lp16
|
||||||
S'Beluga Liner'
|
S'Asp Scout'
|
||||||
p17
|
p17
|
||||||
a(dp18
|
a(dp18
|
||||||
g6
|
g6
|
||||||
I950
|
I150
|
||||||
saa(lp19
|
saa(lp19
|
||||||
S'Cobra MkIII'
|
S'Beluga Liner'
|
||||||
p20
|
p20
|
||||||
a(dp21
|
a(dp21
|
||||||
g6
|
g6
|
||||||
I180
|
I950
|
||||||
saa(lp22
|
saa(lp22
|
||||||
S'Cobra MkIV'
|
S'Cobra MkIII'
|
||||||
p23
|
p23
|
||||||
a(dp24
|
a(dp24
|
||||||
g6
|
g6
|
||||||
I210
|
I180
|
||||||
saa(lp25
|
saa(lp25
|
||||||
S'Diamondback Explorer'
|
S'Cobra MkIV'
|
||||||
p26
|
p26
|
||||||
a(dp27
|
a(dp27
|
||||||
g6
|
g6
|
||||||
I260
|
I210
|
||||||
saa(lp28
|
saa(lp28
|
||||||
S'Diamondback Scout'
|
S'Diamondback Explorer'
|
||||||
p29
|
p29
|
||||||
a(dp30
|
a(dp30
|
||||||
g6
|
g6
|
||||||
I170
|
I260
|
||||||
saa(lp31
|
saa(lp31
|
||||||
S'Dolphin'
|
S'Diamondback Scout'
|
||||||
p32
|
p32
|
||||||
a(dp33
|
a(dp33
|
||||||
g6
|
g6
|
||||||
I140
|
I170
|
||||||
saa(lp34
|
saa(lp34
|
||||||
S'Eagle'
|
S'Dolphin'
|
||||||
p35
|
p35
|
||||||
a(dp36
|
a(dp36
|
||||||
g6
|
g6
|
||||||
I50
|
I140
|
||||||
saa(lp37
|
saa(lp37
|
||||||
S'Federal Assault Ship'
|
S'Eagle'
|
||||||
p38
|
p38
|
||||||
a(dp39
|
a(dp39
|
||||||
g6
|
g6
|
||||||
I480
|
I50
|
||||||
saa(lp40
|
saa(lp40
|
||||||
S'Federal Corvette'
|
S'Federal Assault Ship'
|
||||||
p41
|
p41
|
||||||
a(dp42
|
a(dp42
|
||||||
g6
|
g6
|
||||||
I900
|
I480
|
||||||
saa(lp43
|
saa(lp43
|
||||||
S'Federal Dropship'
|
S'Federal Corvette'
|
||||||
p44
|
p44
|
||||||
a(dp45
|
a(dp45
|
||||||
g6
|
g6
|
||||||
I580
|
I900
|
||||||
saa(lp46
|
saa(lp46
|
||||||
S'Federal Gunship'
|
S'Federal Dropship'
|
||||||
p47
|
p47
|
||||||
a(dp48
|
a(dp48
|
||||||
g6
|
g6
|
||||||
I580
|
I580
|
||||||
saa(lp49
|
saa(lp49
|
||||||
S'Fer-de-Lance'
|
S'Federal Gunship'
|
||||||
p50
|
p50
|
||||||
a(dp51
|
a(dp51
|
||||||
g6
|
g6
|
||||||
I250
|
I580
|
||||||
saa(lp52
|
saa(lp52
|
||||||
S'Hauler'
|
S'Fer-de-Lance'
|
||||||
p53
|
p53
|
||||||
a(dp54
|
a(dp54
|
||||||
g6
|
g6
|
||||||
I14
|
I250
|
||||||
saa(lp55
|
saa(lp55
|
||||||
S'Imperial Clipper'
|
S'Hauler'
|
||||||
p56
|
p56
|
||||||
a(dp57
|
a(dp57
|
||||||
g6
|
g6
|
||||||
I400
|
I14
|
||||||
saa(lp58
|
saa(lp58
|
||||||
S'Imperial Courier'
|
S'Imperial Clipper'
|
||||||
p59
|
p59
|
||||||
a(dp60
|
a(dp60
|
||||||
g6
|
g6
|
||||||
I35
|
I400
|
||||||
saa(lp61
|
saa(lp61
|
||||||
S'Imperial Cutter'
|
S'Imperial Courier'
|
||||||
p62
|
p62
|
||||||
a(dp63
|
a(dp63
|
||||||
g6
|
g6
|
||||||
I1100
|
I35
|
||||||
saa(lp64
|
saa(lp64
|
||||||
S'Imperial Eagle'
|
S'Imperial Cutter'
|
||||||
p65
|
p65
|
||||||
a(dp66
|
a(dp66
|
||||||
g6
|
g6
|
||||||
I50
|
I1100
|
||||||
saa(lp67
|
saa(lp67
|
||||||
S'Keelback'
|
S'Imperial Eagle'
|
||||||
p68
|
p68
|
||||||
a(dp69
|
a(dp69
|
||||||
g6
|
g6
|
||||||
I180
|
I50
|
||||||
saa(lp70
|
saa(lp70
|
||||||
S'Orca'
|
S'Keelback'
|
||||||
p71
|
p71
|
||||||
a(dp72
|
a(dp72
|
||||||
g6
|
g6
|
||||||
I290
|
I180
|
||||||
saa(lp73
|
saa(lp73
|
||||||
S'Python'
|
S'Orca'
|
||||||
p74
|
p74
|
||||||
a(dp75
|
a(dp75
|
||||||
g6
|
g6
|
||||||
I350
|
I290
|
||||||
saa(lp76
|
saa(lp76
|
||||||
S'Sidewinder'
|
S'Python'
|
||||||
p77
|
p77
|
||||||
a(dp78
|
a(dp78
|
||||||
g6
|
g6
|
||||||
I25
|
I350
|
||||||
saa(lp79
|
saa(lp79
|
||||||
S'Type-10 Defender'
|
S'Sidewinder'
|
||||||
p80
|
p80
|
||||||
a(dp81
|
a(dp81
|
||||||
g6
|
g6
|
||||||
I1200
|
I25
|
||||||
saa(lp82
|
saa(lp82
|
||||||
S'Type-6 Transporter'
|
S'Type-10 Defender'
|
||||||
p83
|
p83
|
||||||
a(dp84
|
a(dp84
|
||||||
g6
|
g6
|
||||||
I155
|
I1200
|
||||||
saa(lp85
|
saa(lp85
|
||||||
S'Type-7 Transporter'
|
S'Type-6 Transporter'
|
||||||
p86
|
p86
|
||||||
a(dp87
|
a(dp87
|
||||||
g6
|
g6
|
||||||
I420
|
I155
|
||||||
saa(lp88
|
saa(lp88
|
||||||
S'Type-9 Heavy'
|
S'Type-7 Transporter'
|
||||||
p89
|
p89
|
||||||
a(dp90
|
a(dp90
|
||||||
g6
|
g6
|
||||||
I1000
|
I420
|
||||||
saa(lp91
|
saa(lp91
|
||||||
S'Viper MkIII'
|
S'Type-9 Heavy'
|
||||||
p92
|
p92
|
||||||
a(dp93
|
a(dp93
|
||||||
g6
|
g6
|
||||||
I50
|
I850
|
||||||
saa(lp94
|
saa(lp94
|
||||||
S'Viper MkIV'
|
S'Viper MkIII'
|
||||||
p95
|
p95
|
||||||
a(dp96
|
a(dp96
|
||||||
g6
|
g6
|
||||||
I190
|
I50
|
||||||
saa(lp97
|
saa(lp97
|
||||||
S'Vulture'
|
S'Viper MkIV'
|
||||||
p98
|
p98
|
||||||
a(dp99
|
a(dp99
|
||||||
g6
|
g6
|
||||||
|
I190
|
||||||
|
saa(lp100
|
||||||
|
S'Vulture'
|
||||||
|
p101
|
||||||
|
a(dp102
|
||||||
|
g6
|
||||||
I230
|
I230
|
||||||
saatRp100
|
saatRp103
|
||||||
.
|
.
|
Loading…
x
Reference in New Issue
Block a user