mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2025-04-15 08:40:34 +03:00
This is old code, pre-dating the move to EDCD. We could only possibly set the system name and address from this, and would always have already set this from login Location event, or any jump-in event.
1599 lines
61 KiB
Python
1599 lines
61 KiB
Python
"""Inara Sync."""
|
||
|
||
import json
|
||
import threading
|
||
import time
|
||
import tkinter as tk
|
||
from collections import OrderedDict, defaultdict, deque
|
||
from operator import itemgetter
|
||
from threading import Lock, Thread
|
||
from typing import TYPE_CHECKING, Any, Callable, Deque, Dict, List, Mapping, NamedTuple, Optional
|
||
from typing import OrderedDict as OrderedDictT
|
||
from typing import Sequence, Union, cast
|
||
|
||
import requests
|
||
|
||
import killswitch
|
||
import myNotebook as nb # noqa: N813
|
||
import plug
|
||
import timeout_session
|
||
from companion import CAPIData
|
||
from config import applongname, appversion, config
|
||
from EDMCLogging import get_main_logger
|
||
from ttkHyperlinkLabel import HyperlinkLabel
|
||
|
||
logger = get_main_logger()
|
||
|
||
if TYPE_CHECKING:
|
||
def _(x: str) -> str:
|
||
return x
|
||
|
||
|
||
_TIMEOUT = 20
|
||
FAKE = ('CQC', 'Training', 'Destination') # Fake systems that shouldn't be sent to Inara
|
||
CREDIT_RATIO = 1.05 # Update credits if they change by 5% over the course of a session
|
||
|
||
|
||
# These need to be defined above This
|
||
class Credentials(NamedTuple):
|
||
"""Credentials holds the set of credentials required to identify an inara API payload to inara."""
|
||
|
||
cmdr: Optional[str]
|
||
fid: Optional[str]
|
||
api_key: str
|
||
|
||
|
||
EVENT_DATA = Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]
|
||
|
||
|
||
class Event(NamedTuple):
|
||
"""Event represents an event for the Inara API."""
|
||
|
||
name: str
|
||
timestamp: str
|
||
data: EVENT_DATA
|
||
|
||
|
||
class This:
|
||
"""Holds module globals."""
|
||
|
||
def __init__(self):
|
||
self.session = timeout_session.new_session()
|
||
self.thread: Thread
|
||
self.lastlocation = None # eventData from the last Commander's Flight Log event
|
||
self.lastship = None # eventData from the last addCommanderShip or setCommanderShip event
|
||
|
||
# Cached Cmdr state
|
||
self.cmdr: Optional[str] = None
|
||
self.FID: Optional[str] = None # Frontier ID
|
||
self.multicrew: bool = False # don't send captain's ship info to Inara while on a crew
|
||
self.newuser: bool = False # just entered API Key - send state immediately
|
||
self.newsession: bool = True # starting a new session - wait for Cargo event
|
||
self.undocked: bool = False # just undocked
|
||
self.suppress_docked = False # Skip initial Docked event if started docked
|
||
self.cargo: Optional[List[OrderedDictT[str, Any]]] = None
|
||
self.materials: Optional[List[OrderedDictT[str, Any]]] = None
|
||
self.lastcredits: int = 0 # Send credit update soon after Startup / new game
|
||
self.storedmodules: Optional[List[OrderedDictT[str, Any]]] = None
|
||
self.loadout: Optional[OrderedDictT[str, Any]] = None
|
||
self.fleet: Optional[List[OrderedDictT[str, Any]]] = None
|
||
self.shipswap: bool = False # just swapped ship
|
||
self.on_foot = False
|
||
|
||
self.timer_run = True
|
||
|
||
# Main window clicks
|
||
self.system_link: tk.Widget = None # type: ignore
|
||
self.system: Optional[str] = None # type: ignore
|
||
self.system_address: Optional[str] = None # type: ignore
|
||
self.system_population: Optional[int] = None
|
||
self.station_link: tk.Widget = None # type: ignore
|
||
self.station = None
|
||
self.station_marketid = None
|
||
|
||
# Prefs UI
|
||
self.log: 'tk.IntVar'
|
||
self.log_button: nb.Checkbutton
|
||
self.label: HyperlinkLabel
|
||
self.apikey: nb.Entry
|
||
self.apikey_label: HyperlinkLabel
|
||
|
||
self.events: Dict[Credentials, Deque[Event]] = defaultdict(deque)
|
||
self.event_lock: Lock = threading.Lock() # protects events, for use when rewriting events
|
||
|
||
def filter_events(self, key: Credentials, predicate: Callable[[Event], bool]) -> None:
|
||
"""
|
||
filter_events is the equivalent of running filter() on any event list in the events dict.
|
||
|
||
it will automatically handle locking, and replacing the event list with the filtered version.
|
||
|
||
:param key: the key to filter
|
||
:param predicate: the predicate to use while filtering
|
||
"""
|
||
with self.event_lock:
|
||
tmp = self.events[key].copy()
|
||
self.events[key].clear()
|
||
self.events[key].extend(filter(predicate, tmp))
|
||
|
||
|
||
this = This()
|
||
# last time we updated, if unset in config this is 0, which means an instant update
|
||
LAST_UPDATE_CONF_KEY = 'inara_last_update'
|
||
EVENT_COLLECT_TIME = 31 # Minimum time to take collecting events before requesting a send
|
||
WORKER_WAIT_TIME = 35 # Minimum time for worker to wait between sends
|
||
|
||
STATION_UNDOCKED: str = '×' # "Station" name to display when not docked = U+00D7
|
||
|
||
|
||
TARGET_URL = 'https://inara.cz/inapi/v1/'
|
||
|
||
|
||
def system_url(system_name: str) -> str:
|
||
"""Get a URL for the current system."""
|
||
if this.system_address:
|
||
return requests.utils.requote_uri(f'https://inara.cz/galaxy-starsystem/?search={this.system_address}')
|
||
|
||
elif system_name:
|
||
return requests.utils.requote_uri(f'https://inara.cz/galaxy-starsystem/?search={system_name}')
|
||
|
||
return ''
|
||
|
||
|
||
def station_url(system_name: str, station_name: str) -> str:
|
||
"""
|
||
Get a URL for the current station.
|
||
|
||
If there is no station, the system URL is returned.
|
||
|
||
:param system_name: The name of the current system
|
||
:param station_name: The name of the current station, if any
|
||
:return: A URL to inara for the given system and station
|
||
"""
|
||
if system_name and station_name:
|
||
return requests.utils.requote_uri(f'https://inara.cz/galaxy-station/?search={system_name}%20[{station_name}]')
|
||
|
||
# monitor state might think these are gone, but we don't yet
|
||
if this.system and this.station:
|
||
return requests.utils.requote_uri(f'https://inara.cz/galaxy-station/?search={this.system}%20[{this.station}]')
|
||
|
||
if system_name:
|
||
return system_url(system_name)
|
||
|
||
return ''
|
||
|
||
|
||
def plugin_start3(plugin_dir: str) -> str:
|
||
"""
|
||
Start this plugin.
|
||
|
||
Start the worker thread to handle sending to Inara API.
|
||
"""
|
||
logger.debug('Starting worker thread...')
|
||
this.thread = Thread(target=new_worker, name='Inara worker')
|
||
this.thread.daemon = True
|
||
this.thread.start()
|
||
logger.debug('Done.')
|
||
|
||
return 'Inara'
|
||
|
||
|
||
def plugin_app(parent: tk.Tk) -> None:
|
||
"""Plugin UI setup Hook."""
|
||
this.system_link = parent.children['system'] # system label in main window
|
||
this.station_link = parent.children['station'] # station label in main window
|
||
this.system_link.bind_all('<<InaraLocation>>', update_location)
|
||
this.system_link.bind_all('<<InaraShip>>', update_ship)
|
||
|
||
|
||
def plugin_stop() -> None:
|
||
"""Plugin shutdown hook."""
|
||
logger.debug('We have no way to ask new_worker to stop, but...')
|
||
# The Newthis/new_worker doesn't have a method to ask the new_worker to
|
||
# stop. We're relying on it being a daemon thread and thus exiting when
|
||
# there are no non-daemon (i.e. main) threads running.
|
||
|
||
this.timer_run = False
|
||
|
||
logger.debug('Done.')
|
||
|
||
|
||
def plugin_prefs(parent: tk.Tk, cmdr: str, is_beta: bool) -> tk.Frame:
|
||
"""Plugin Preferences UI hook."""
|
||
x_padding = 10
|
||
x_button_padding = 12 # indent Checkbuttons and Radiobuttons
|
||
y_padding = 2 # close spacing
|
||
|
||
frame = nb.Frame(parent)
|
||
frame.columnconfigure(1, weight=1)
|
||
|
||
HyperlinkLabel(
|
||
frame, text='Inara', background=nb.Label().cget('background'), url='https://inara.cz/', underline=True
|
||
).grid(columnspan=2, padx=x_padding, sticky=tk.W) # Don't translate
|
||
|
||
this.log = tk.IntVar(value=config.get_int('inara_out') and 1)
|
||
this.log_button = nb.Checkbutton(
|
||
frame, text=_('Send flight log and Cmdr status to Inara'), variable=this.log, command=prefsvarchanged
|
||
)
|
||
|
||
this.log_button.grid(columnspan=2, padx=x_button_padding, pady=(5, 0), sticky=tk.W)
|
||
|
||
nb.Label(frame).grid(sticky=tk.W) # big spacer
|
||
|
||
# Section heading in settings
|
||
this.label = HyperlinkLabel(
|
||
frame,
|
||
text=_('Inara credentials'),
|
||
background=nb.Label().cget('background'),
|
||
url='https://inara.cz/settings-api',
|
||
underline=True
|
||
)
|
||
|
||
this.label.grid(columnspan=2, padx=x_padding, sticky=tk.W)
|
||
|
||
this.apikey_label = nb.Label(frame, text=_('API Key')) # Inara setting
|
||
this.apikey_label.grid(row=12, padx=x_padding, sticky=tk.W)
|
||
this.apikey = nb.Entry(frame)
|
||
this.apikey.grid(row=12, column=1, padx=x_padding, pady=y_padding, sticky=tk.EW)
|
||
|
||
prefs_cmdr_changed(cmdr, is_beta)
|
||
|
||
return frame
|
||
|
||
|
||
def prefs_cmdr_changed(cmdr: str, is_beta: bool) -> None:
|
||
"""Plugin commander change hook."""
|
||
this.log_button['state'] = tk.NORMAL if cmdr and not is_beta else tk.DISABLED
|
||
this.apikey['state'] = tk.NORMAL
|
||
this.apikey.delete(0, tk.END)
|
||
if cmdr:
|
||
cred = credentials(cmdr)
|
||
if cred:
|
||
this.apikey.insert(0, cred)
|
||
|
||
state: str = tk.DISABLED
|
||
if cmdr and not is_beta and this.log.get():
|
||
state = tk.NORMAL
|
||
|
||
this.label['state'] = state
|
||
this.apikey_label['state'] = state
|
||
this.apikey['state'] = state
|
||
|
||
|
||
def prefsvarchanged():
|
||
"""Preferences window change hook."""
|
||
state = tk.DISABLED
|
||
if this.log.get():
|
||
state = this.log_button['state']
|
||
|
||
this.label['state'] = state
|
||
this.apikey_label['state'] = state
|
||
this.apikey['state'] = state
|
||
|
||
|
||
def prefs_changed(cmdr: str, is_beta: bool) -> None:
|
||
"""Preferences window closed hook."""
|
||
changed = config.get_int('inara_out') != this.log.get()
|
||
config.set('inara_out', this.log.get())
|
||
|
||
if cmdr and not is_beta:
|
||
this.cmdr = cmdr
|
||
this.FID = None
|
||
cmdrs = config.get_list('inara_cmdrs', default=[])
|
||
apikeys = config.get_list('inara_apikeys', default=[])
|
||
if cmdr in cmdrs:
|
||
idx = cmdrs.index(cmdr)
|
||
apikeys.extend([''] * (1 + idx - len(apikeys)))
|
||
changed |= (apikeys[idx] != this.apikey.get().strip())
|
||
apikeys[idx] = this.apikey.get().strip()
|
||
|
||
else:
|
||
config.set('inara_cmdrs', cmdrs + [cmdr])
|
||
changed = True
|
||
apikeys.append(this.apikey.get().strip())
|
||
|
||
config.set('inara_apikeys', apikeys)
|
||
|
||
if this.log.get() and changed:
|
||
this.newuser = True # Send basic info at next Journal event
|
||
new_add_event(
|
||
'getCommanderProfile', time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), {'searchName': cmdr}
|
||
)
|
||
|
||
|
||
def credentials(cmdr: Optional[str]) -> Optional[str]:
|
||
"""
|
||
Get the credentials for the current commander.
|
||
|
||
:param cmdr: Commander name to search for credentials
|
||
:return: Credentials for the given commander or None
|
||
"""
|
||
if not cmdr:
|
||
return None
|
||
|
||
cmdrs = config.get_list('inara_cmdrs', default=[])
|
||
if cmdr in cmdrs and config.get_list('inara_apikeys'):
|
||
return config.get_list('inara_apikeys')[cmdrs.index(cmdr)]
|
||
|
||
else:
|
||
return None
|
||
|
||
|
||
def journal_entry( # noqa: C901, CCR001
|
||
cmdr: str, is_beta: bool, system: str, station: str, entry: Dict[str, Any], state: Dict[str, Any]
|
||
) -> str:
|
||
"""
|
||
Journal entry hook.
|
||
|
||
:return: str - empty if no error, else error string.
|
||
"""
|
||
if (ks := killswitch.get_disabled('plugins.inara.journal')).disabled:
|
||
logger.warning(f'Inara support has been disabled via killswitch: {ks.reason}')
|
||
plug.show_error(_('Inara disabled. See Log.'))
|
||
return ''
|
||
|
||
elif (ks := killswitch.get_disabled(f'plugins.inara.journal.event.{entry["event"]}')).disabled:
|
||
logger.warning(f'event {entry["event"]} processing has been disabled via killswitch: {ks.reason}')
|
||
|
||
this.on_foot = state['OnFoot']
|
||
event_name: str = entry['event']
|
||
this.cmdr = cmdr
|
||
this.FID = state['FID']
|
||
this.multicrew = bool(state['Role'])
|
||
|
||
if event_name == 'LoadGame' or this.newuser:
|
||
# clear cached state
|
||
if event_name == 'LoadGame':
|
||
# User setup Inara API while at the loading screen - proceed as for new session
|
||
this.newuser = False
|
||
this.newsession = True
|
||
|
||
else:
|
||
this.newuser = True
|
||
this.newsession = False
|
||
|
||
this.undocked = False
|
||
this.suppress_docked = False
|
||
this.cargo = None
|
||
this.materials = None
|
||
this.lastcredits = 0
|
||
this.storedmodules = None
|
||
this.loadout = None
|
||
this.fleet = None
|
||
this.shipswap = False
|
||
this.system = None
|
||
this.system_address = None
|
||
this.station = None
|
||
this.station_marketid = None
|
||
|
||
elif event_name in ('Resurrect', 'ShipyardBuy', 'ShipyardSell', 'SellShipOnRebuy'):
|
||
# Events that mean a significant change in credits so we should send credits after next "Update"
|
||
this.lastcredits = 0
|
||
|
||
elif event_name in ('ShipyardNew', 'ShipyardSwap') or (event_name == 'Location' and entry['Docked']):
|
||
this.suppress_docked = True
|
||
|
||
# Always update our system address even if we're not currently the provider for system or station, but dont update
|
||
# on events that contain "future" data, such as FSDTarget
|
||
if entry['event'] in ('Location', 'Docked', 'CarrierJump', 'FSDJump'):
|
||
this.system_address = entry.get('SystemAddress') or this.system_address
|
||
this.system = entry.get('StarSystem') or this.system
|
||
|
||
# We need pop == 0 to set the value so as to clear 'x' in systems with
|
||
# no stations.
|
||
pop: Optional[int] = entry.get('Population')
|
||
if pop is not None:
|
||
this.system_population = pop
|
||
|
||
this.station = entry.get('StationName', this.station)
|
||
# on_foot station detection
|
||
if entry['event'] == 'Location' and entry['BodyType'] == 'Station':
|
||
this.station = entry['Body']
|
||
|
||
this.station_marketid = entry.get('MarketID', this.station_marketid) or this.station_marketid
|
||
# We might pick up StationName in DockingRequested, make sure we clear it if leaving
|
||
if event_name in ('Undocked', 'FSDJump', 'SupercruiseEntry'):
|
||
this.station = None
|
||
this.station_marketid = None
|
||
|
||
if entry['event'] == 'Embark' and not entry.get('OnStation'):
|
||
# If we're embarking OnStation to a Taxi/Dropship we'll also get an
|
||
# Undocked event.
|
||
this.station = None
|
||
this.station_marketid = None
|
||
|
||
if config.get_int('inara_out') and not is_beta and not this.multicrew and credentials(cmdr):
|
||
current_creds = Credentials(this.cmdr, this.FID, str(credentials(this.cmdr)))
|
||
try:
|
||
# Dump starting state to Inara
|
||
if (this.newuser or event_name == 'StartUp' or (this.newsession and event_name == 'Cargo')):
|
||
this.newuser = False
|
||
this.newsession = False
|
||
|
||
# Send rank info to Inara on startup
|
||
new_add_event(
|
||
'setCommanderRankPilot',
|
||
entry['timestamp'],
|
||
[
|
||
{'rankName': k.lower(), 'rankValue': v[0], 'rankProgress': v[1] / 100.0}
|
||
for k, v in state['Rank'].items() if v is not None
|
||
]
|
||
)
|
||
|
||
# Don't send the API call with no values.
|
||
if state['Reputation']:
|
||
new_add_event(
|
||
'setCommanderReputationMajorFaction',
|
||
entry['timestamp'],
|
||
[
|
||
{'majorfactionName': k.lower(), 'majorfactionReputation': v / 100.0}
|
||
for k, v in state['Reputation'].items() if v is not None
|
||
]
|
||
)
|
||
|
||
if state['Engineers']: # Not populated < 3.3
|
||
to_send_list: List[Mapping[str, Any]] = []
|
||
for k, v in state['Engineers'].items():
|
||
e = {'engineerName': k}
|
||
if isinstance(v, tuple):
|
||
e['rankValue'] = v[0]
|
||
|
||
else:
|
||
e['rankStage'] = v
|
||
|
||
to_send_list.append(e)
|
||
|
||
new_add_event(
|
||
'setCommanderRankEngineer',
|
||
entry['timestamp'],
|
||
to_send_list,
|
||
)
|
||
|
||
# Update location
|
||
# Might not be available if this event is a 'StartUp' and we're replaying
|
||
# a log.
|
||
# XXX: This interferes with other more specific setCommanderTravelLocation events in the same
|
||
# batch.
|
||
# if system:
|
||
# new_add_event(
|
||
# 'setCommanderTravelLocation',
|
||
# entry['timestamp'],
|
||
# OrderedDict([
|
||
# ('starsystemName', system),
|
||
# ('stationName', station), # Can be None
|
||
# ])
|
||
# )
|
||
|
||
# Update ship
|
||
if state['ShipID']: # Unknown if started in Fighter or SRV
|
||
cur_ship: Dict[str, Any] = {
|
||
'shipType': state['ShipType'],
|
||
'shipGameID': state['ShipID'],
|
||
'shipName': state['ShipName'],
|
||
'shipIdent': state['ShipIdent'],
|
||
'isCurrentShip': True,
|
||
|
||
}
|
||
|
||
if state['HullValue']:
|
||
cur_ship['shipHullValue'] = state['HullValue']
|
||
|
||
if state['ModulesValue']:
|
||
cur_ship['shipModulesValue'] = state['ModulesValue']
|
||
|
||
cur_ship['shipRebuyCost'] = state['Rebuy']
|
||
new_add_event('setCommanderShip', entry['timestamp'], cur_ship)
|
||
|
||
this.loadout = make_loadout(state)
|
||
new_add_event('setCommanderShipLoadout', entry['timestamp'], this.loadout)
|
||
|
||
# Promotions
|
||
elif event_name == 'Promotion':
|
||
for k, v in state['Rank'].items():
|
||
if k in entry:
|
||
new_add_event(
|
||
'setCommanderRankPilot',
|
||
entry['timestamp'],
|
||
{'rankName': k.lower(), 'rankValue': v[0], 'rankProgress': 0}
|
||
)
|
||
|
||
elif event_name == 'EngineerProgress' and 'Engineer' in entry:
|
||
# TODO: due to this var name being used above, the types are weird
|
||
to_send_dict = {'engineerName': entry['Engineer']}
|
||
if 'Rank' in entry:
|
||
to_send_dict['rankValue'] = entry['Rank']
|
||
|
||
else:
|
||
to_send_dict['rankStage'] = entry['Progress']
|
||
|
||
new_add_event(
|
||
'setCommanderRankEngineer',
|
||
entry['timestamp'],
|
||
to_send_dict
|
||
)
|
||
|
||
# PowerPlay status change
|
||
if event_name == 'PowerplayJoin':
|
||
new_add_event(
|
||
'setCommanderRankPower',
|
||
entry['timestamp'],
|
||
{'powerName': entry['Power'], 'rankValue': 1}
|
||
)
|
||
|
||
elif event_name == 'PowerplayLeave':
|
||
new_add_event(
|
||
'setCommanderRankPower',
|
||
entry['timestamp'],
|
||
{'powerName': entry['Power'], 'rankValue': 0}
|
||
)
|
||
|
||
elif event_name == 'PowerplayDefect':
|
||
new_add_event(
|
||
'setCommanderRankPower',
|
||
entry['timestamp'],
|
||
{'powerName': entry['ToPower'], 'rankValue': 1}
|
||
)
|
||
|
||
# Ship change
|
||
if event_name == 'Loadout' and this.shipswap:
|
||
cur_ship = {
|
||
'shipType': state['ShipType'],
|
||
'shipGameID': state['ShipID'],
|
||
'shipName': state['ShipName'], # Can be None
|
||
'shipIdent': state['ShipIdent'], # Can be None
|
||
'isCurrentShip': True,
|
||
}
|
||
|
||
if state['HullValue']:
|
||
cur_ship['shipHullValue'] = state['HullValue']
|
||
|
||
if state['ModulesValue']:
|
||
cur_ship['shipModulesValue'] = state['ModulesValue']
|
||
|
||
cur_ship['shipRebuyCost'] = state['Rebuy']
|
||
new_add_event('setCommanderShip', entry['timestamp'], cur_ship)
|
||
|
||
this.loadout = make_loadout(state)
|
||
new_add_event('setCommanderShipLoadout', entry['timestamp'], this.loadout)
|
||
this.shipswap = False
|
||
|
||
# Location change
|
||
elif event_name == 'Docked':
|
||
if this.undocked:
|
||
# Undocked and now docking again. Don't send.
|
||
this.undocked = False
|
||
|
||
elif this.suppress_docked:
|
||
# Don't send initial Docked event on new game
|
||
this.suppress_docked = False
|
||
|
||
else:
|
||
to_send = {
|
||
'starsystemName': system,
|
||
'stationName': station,
|
||
'shipType': state['ShipType'],
|
||
'shipGameID': state['ShipID'],
|
||
}
|
||
|
||
if entry.get('Taxi'):
|
||
# we're in a taxi, dont store ShipType or shipGameID
|
||
del to_send['shipType']
|
||
del to_send['shipGameID']
|
||
|
||
# We were in a taxi. What kind?
|
||
if state['Dropship'] is not None and state['Dropship']:
|
||
to_send['isTaxiDropship'] = True
|
||
|
||
elif state['Taxi'] is not None and state['Taxi']:
|
||
to_send['isTaxiShuttle'] = True
|
||
|
||
else: # we dont know one way or another. Given we were told it IS a taxi, assume its a shuttle.
|
||
to_send['isTaxiShuttle'] = True
|
||
|
||
if 'MarketID' in entry:
|
||
to_send['marketID'] = entry['MarketID']
|
||
|
||
# TODO: we _can_ include a Body name here, but I'm not entirely sure how best to go about doing that
|
||
|
||
new_add_event(
|
||
'addCommanderTravelDock',
|
||
entry['timestamp'],
|
||
to_send
|
||
)
|
||
|
||
elif event_name == 'Undocked':
|
||
this.undocked = True
|
||
this.station = None
|
||
|
||
elif event_name == 'SupercruiseEntry':
|
||
this.undocked = False
|
||
|
||
elif event_name == 'SupercruiseExit':
|
||
to_send = {
|
||
'starsystemName': entry['StarSystem'],
|
||
}
|
||
|
||
if entry['BodyType'] == 'Planet':
|
||
to_send['starsystemBodyName'] = entry['Body']
|
||
|
||
new_add_event('setCommanderTravelLocation', entry['timestamp'], to_send)
|
||
|
||
elif event_name == 'ApproachSettlement':
|
||
# If you're near a Settlement on login this event is recorded, but
|
||
# we might not yet have system logged for use.
|
||
if system:
|
||
to_send = {
|
||
'starsystemName': system,
|
||
'stationName': entry['Name'],
|
||
'marketid': entry['MarketID'],
|
||
'starsystemBodyName': entry['BodyName'],
|
||
'starsystemBodyCoords': [entry['Latitude'], entry['Longitude']]
|
||
}
|
||
new_add_event('setCommanderTravelLocation', entry['timestamp'], to_send)
|
||
|
||
elif event_name == 'FSDJump':
|
||
this.undocked = False
|
||
to_send = {
|
||
'starsystemName': entry['StarSystem'],
|
||
'starsystemCoords': entry['StarPos'],
|
||
'jumpDistance': entry['JumpDist'],
|
||
'shipType': state['ShipType'],
|
||
'shipGameID': state['ShipID'],
|
||
}
|
||
|
||
if state['Taxi'] is not None and state['Taxi']:
|
||
del to_send['shipType']
|
||
del to_send['shipGameID']
|
||
|
||
# taxi. What kind?
|
||
if state['Dropship'] is not None and state['Dropship']:
|
||
to_send['isTaxiDropship'] = True
|
||
|
||
else:
|
||
to_send['isTaxiShuttle'] = True
|
||
|
||
new_add_event(
|
||
'addCommanderTravelFSDJump',
|
||
entry['timestamp'],
|
||
to_send
|
||
)
|
||
|
||
if entry.get('Factions'):
|
||
new_add_event(
|
||
'setCommanderReputationMinorFaction',
|
||
entry['timestamp'],
|
||
[
|
||
{'minorfactionName': f['Name'], 'minorfactionReputation': f['MyReputation'] / 100.0}
|
||
for f in entry['Factions']
|
||
]
|
||
)
|
||
|
||
elif event_name == 'CarrierJump':
|
||
to_send = {
|
||
'starsystemName': entry['StarSystem'],
|
||
'stationName': entry['StationName'],
|
||
'marketID': entry['MarketID'],
|
||
'shipType': state['ShipType'],
|
||
'shipGameID': state['ShipID'],
|
||
}
|
||
|
||
if 'StarPos' in entry:
|
||
to_send['starsystemCoords'] = entry['StarPos']
|
||
|
||
new_add_event(
|
||
'addCommanderTravelCarrierJump',
|
||
entry['timestamp'],
|
||
to_send
|
||
)
|
||
|
||
if entry.get('Factions'):
|
||
new_add_event(
|
||
'setCommanderReputationMinorFaction',
|
||
entry['timestamp'],
|
||
[
|
||
{'minorfactionName': f['Name'], 'minorfactionReputation': f['MyReputation'] / 100.0}
|
||
for f in entry['Factions']
|
||
]
|
||
)
|
||
|
||
# Ignore the following 'Docked' event
|
||
this.suppress_docked = True
|
||
|
||
cargo: List[OrderedDictT[str, Any]]
|
||
cargo = [OrderedDict({'itemName': k, 'itemCount': state['Cargo'][k]}) for k in sorted(state['Cargo'])]
|
||
|
||
# Send cargo and materials if changed
|
||
if this.cargo != cargo:
|
||
new_add_event('setCommanderInventoryCargo', entry['timestamp'], cargo)
|
||
this.cargo = cargo
|
||
|
||
materials: List[OrderedDictT[str, Any]] = []
|
||
for category in ('Raw', 'Manufactured', 'Encoded'):
|
||
materials.extend(
|
||
[OrderedDict([('itemName', k), ('itemCount', state[category][k])]) for k in sorted(state[category])]
|
||
)
|
||
|
||
if this.materials != materials:
|
||
new_add_event('setCommanderInventoryMaterials', entry['timestamp'], materials)
|
||
this.materials = materials
|
||
|
||
except Exception as e:
|
||
logger.debug('Adding events', exc_info=e)
|
||
return str(e)
|
||
|
||
# Send credits and stats to Inara on startup only - otherwise may be out of date
|
||
if event_name == 'LoadGame':
|
||
new_add_event(
|
||
'setCommanderCredits',
|
||
entry['timestamp'],
|
||
{'commanderCredits': state['Credits'], 'commanderLoan': state['Loan']}
|
||
)
|
||
|
||
this.lastcredits = state['Credits']
|
||
|
||
elif event_name == 'Statistics':
|
||
new_add_event('setCommanderGameStatistics', entry['timestamp'], state['Statistics']) # may be out of date
|
||
|
||
# Selling / swapping ships
|
||
if event_name == 'ShipyardNew':
|
||
new_add_event(
|
||
'addCommanderShip',
|
||
entry['timestamp'],
|
||
{'shipType': entry['ShipType'], 'shipGameID': entry['NewShipID']}
|
||
)
|
||
|
||
this.shipswap = True # Want subsequent Loadout event to be sent immediately
|
||
|
||
elif event_name in ('ShipyardBuy', 'ShipyardSell', 'SellShipOnRebuy', 'ShipyardSwap'):
|
||
if event_name == 'ShipyardSwap':
|
||
this.shipswap = True # Don't know new ship name and ident 'til the following Loadout event
|
||
|
||
if 'StoreShipID' in entry:
|
||
new_add_event(
|
||
'setCommanderShip',
|
||
entry['timestamp'],
|
||
{
|
||
'shipType': entry['StoreOldShip'],
|
||
'shipGameID': entry['StoreShipID'],
|
||
'starsystemName': system,
|
||
'stationName': station,
|
||
}
|
||
)
|
||
|
||
elif 'SellShipID' in entry:
|
||
new_add_event(
|
||
'delCommanderShip',
|
||
entry['timestamp'],
|
||
{
|
||
'shipType': entry.get('SellOldShip', entry['ShipType']),
|
||
'shipGameID': entry['SellShipID'],
|
||
}
|
||
)
|
||
|
||
elif event_name == 'SetUserShipName':
|
||
new_add_event(
|
||
'setCommanderShip',
|
||
entry['timestamp'],
|
||
{
|
||
'shipType': state['ShipType'],
|
||
'shipGameID': state['ShipID'],
|
||
'shipName': state['ShipName'], # Can be None
|
||
'shipIdent': state['ShipIdent'], # Can be None
|
||
'isCurrentShip': True,
|
||
}
|
||
)
|
||
|
||
elif event_name == 'ShipyardTransfer':
|
||
new_add_event(
|
||
'setCommanderShipTransfer',
|
||
entry['timestamp'],
|
||
{
|
||
'shipType': entry['ShipType'],
|
||
'shipGameID': entry['ShipID'],
|
||
'starsystemName': system,
|
||
'stationName': station,
|
||
'transferTime': entry['TransferTime'],
|
||
}
|
||
)
|
||
|
||
# Fleet
|
||
if event_name == 'StoredShips':
|
||
fleet: List[OrderedDictT[str, Any]] = sorted(
|
||
[OrderedDict({
|
||
'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']] +
|
||
[OrderedDict({
|
||
'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.filter_events(current_creds, lambda e: e.name != 'setCommanderShip')
|
||
|
||
# this.events = [x for x in this.events if x['eventName'] != 'setCommanderShip'] # Remove any unsent
|
||
for ship in this.fleet:
|
||
new_add_event('setCommanderShip', entry['timestamp'], ship)
|
||
|
||
# Loadout
|
||
if event_name == 'Loadout' and not this.newsession:
|
||
loadout = make_loadout(state)
|
||
if this.loadout != loadout:
|
||
this.loadout = loadout
|
||
|
||
this.filter_events(
|
||
current_creds,
|
||
lambda e: (
|
||
e.name != 'setCommanderShipLoadout'
|
||
or cast(dict, e.data)['shipGameID'] != cast(dict, this.loadout)['shipGameID'])
|
||
)
|
||
|
||
new_add_event('setCommanderShipLoadout', entry['timestamp'], this.loadout)
|
||
|
||
# Stored modules
|
||
if event_name == 'StoredModules':
|
||
items = {mod['StorageSlot']: mod for mod in entry['Items']} # Impose an order
|
||
modules: List[OrderedDictT[str, Any]] = []
|
||
for slot in sorted(items):
|
||
item = items[slot]
|
||
module: OrderedDictT[str, Any] = 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
|
||
# Remove any unsent
|
||
this.filter_events(current_creds, lambda e: e.name != 'setCommanderStorageModules')
|
||
|
||
# this.events = list(filter(lambda e: e['eventName'] != 'setCommanderStorageModules', this.events))
|
||
new_add_event('setCommanderStorageModules', entry['timestamp'], this.storedmodules)
|
||
|
||
# Missions
|
||
if event_name == 'MissionAccepted':
|
||
data: OrderedDictT[str, Any] = OrderedDict([
|
||
('missionName', entry['Name']),
|
||
('missionGameID', entry['MissionID']),
|
||
('influenceGain', entry['Influence']),
|
||
('reputationGain', entry['Reputation']),
|
||
('starsystemNameOrigin', system),
|
||
('stationNameOrigin', station),
|
||
('minorfactionNameOrigin', entry['Faction']),
|
||
])
|
||
|
||
# optional mission-specific properties
|
||
for (iprop, prop) in [
|
||
('missionExpiry', 'Expiry'), # Listed as optional in the docs, but always seems to be present
|
||
('starsystemNameTarget', 'DestinationSystem'),
|
||
('stationNameTarget', 'DestinationStation'),
|
||
('minorfactionNameTarget', 'TargetFaction'),
|
||
('commodityName', 'Commodity'),
|
||
('commodityCount', 'Count'),
|
||
('targetName', 'Target'),
|
||
('targetType', 'TargetType'),
|
||
('killCount', 'KillCount'),
|
||
('passengerType', 'PassengerType'),
|
||
('passengerCount', 'PassengerCount'),
|
||
('passengerIsVIP', 'PassengerVIPs'),
|
||
('passengerIsWanted', 'PassengerWanted'),
|
||
]:
|
||
|
||
if prop in entry:
|
||
data[iprop] = entry[prop]
|
||
|
||
new_add_event('addCommanderMission', entry['timestamp'], data)
|
||
|
||
elif event_name == 'MissionAbandoned':
|
||
new_add_event('setCommanderMissionAbandoned', entry['timestamp'], {'missionGameID': entry['MissionID']})
|
||
|
||
elif event_name == 'MissionCompleted':
|
||
for x in entry.get('PermitsAwarded', []):
|
||
new_add_event('addCommanderPermit', entry['timestamp'], {'starsystemName': x})
|
||
|
||
data = OrderedDict([('missionGameID', entry['MissionID'])])
|
||
if 'Donation' in entry:
|
||
data['donationCredits'] = entry['Donation']
|
||
|
||
if 'Reward' in entry:
|
||
data['rewardCredits'] = entry['Reward']
|
||
|
||
if 'PermitsAwarded' in entry:
|
||
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']]
|
||
|
||
factioneffects = []
|
||
for faction in entry.get('FactionEffects', []):
|
||
effect: OrderedDictT[str, Any] = OrderedDict([('minorfactionName', faction['Faction'])])
|
||
for influence in faction.get('Influence', []):
|
||
if 'Influence' in influence:
|
||
highest_gain = influence['Influence']
|
||
if len(effect.get('influenceGain', '')) > len(highest_gain):
|
||
highest_gain = effect['influenceGain']
|
||
|
||
effect['influenceGain'] = highest_gain
|
||
|
||
if 'Reputation' in faction:
|
||
effect['reputationGain'] = faction['Reputation']
|
||
|
||
factioneffects.append(effect)
|
||
|
||
if factioneffects:
|
||
data['minorfactionEffects'] = factioneffects
|
||
|
||
new_add_event('setCommanderMissionCompleted', entry['timestamp'], data)
|
||
|
||
elif event_name == 'MissionFailed':
|
||
new_add_event('setCommanderMissionFailed', entry['timestamp'], {'missionGameID': entry['MissionID']})
|
||
|
||
# Combat
|
||
if event_name == 'Died':
|
||
data = OrderedDict([('starsystemName', system)])
|
||
if 'Killers' in entry:
|
||
data['wingOpponentNames'] = [x['Name'] for x in entry['Killers']]
|
||
|
||
elif 'KillerName' in entry:
|
||
data['opponentName'] = entry['KillerName']
|
||
|
||
new_add_event('addCommanderCombatDeath', entry['timestamp'], data)
|
||
|
||
elif event_name == 'Interdicted':
|
||
data = OrderedDict([('starsystemName', system),
|
||
('isPlayer', entry['IsPlayer']),
|
||
('isSubmit', entry['Submitted']),
|
||
])
|
||
|
||
if 'Interdictor' in entry:
|
||
data['opponentName'] = entry['Interdictor']
|
||
|
||
elif 'Faction' in entry:
|
||
data['opponentName'] = entry['Faction']
|
||
|
||
elif 'Power' in entry:
|
||
data['opponentName'] = entry['Power']
|
||
|
||
new_add_event('addCommanderCombatInterdicted', entry['timestamp'], data)
|
||
|
||
elif event_name == 'Interdiction':
|
||
data = OrderedDict([
|
||
('starsystemName', system),
|
||
('isPlayer', entry['IsPlayer']),
|
||
('isSuccess', entry['Success']),
|
||
])
|
||
|
||
if 'Interdicted' in entry:
|
||
data['opponentName'] = entry['Interdicted']
|
||
|
||
elif 'Faction' in entry:
|
||
data['opponentName'] = entry['Faction']
|
||
|
||
elif 'Power' in entry:
|
||
data['opponentName'] = entry['Power']
|
||
|
||
new_add_event('addCommanderCombatInterdiction', entry['timestamp'], data)
|
||
|
||
elif event_name == 'EscapeInterdiction':
|
||
new_add_event(
|
||
'addCommanderCombatInterdictionEscape',
|
||
entry['timestamp'],
|
||
{
|
||
'starsystemName': system,
|
||
'opponentName': entry['Interdictor'],
|
||
'isPlayer': entry['IsPlayer'],
|
||
}
|
||
)
|
||
|
||
elif event_name == 'PVPKill':
|
||
new_add_event(
|
||
'addCommanderCombatKill',
|
||
entry['timestamp'],
|
||
{
|
||
'starsystemName': system,
|
||
'opponentName': entry['Victim'],
|
||
}
|
||
)
|
||
|
||
# New Odyssey features
|
||
elif event_name == 'DropshipDeploy':
|
||
new_add_event(
|
||
'addCommanderTravelLand',
|
||
entry['timestamp'],
|
||
{
|
||
'starsystemName': entry['StarSystem'],
|
||
'starsystemBodyName': entry['Body'],
|
||
'isTaxiDropship': True,
|
||
}
|
||
)
|
||
|
||
elif event_name == 'Touchdown':
|
||
# Touchdown has FAR more info available on Odyssey vs Horizons:
|
||
# Horizons:
|
||
# {"timestamp":"2021-05-31T09:10:54Z","event":"Touchdown",
|
||
# "PlayerControlled":true,"Latitude":46.691929,"Longitude":-92.679977}
|
||
#
|
||
# Odyssey:
|
||
# {"timestamp":"2021-05-31T08:48:08Z","event":"Touchdown","PlayerControlled":true,"Taxi":false,
|
||
# "Multicrew":false,"StarSystem":"Gateway","SystemAddress":2832631665362,"Body":"Saunder's Rock","BodyID":2,
|
||
# "OnStation":false,"OnPlanet":true,"Latitude":54.79665,"Longitude":-99.498253}
|
||
#
|
||
# So we're going to do a lot of checking here and bail out if we dont like the look of ANYTHING here
|
||
|
||
to_send_data: Optional[Dict[str, Any]] = {} # This is a glorified sentinel until lower down.
|
||
# On Horizons, neither of these exist on TouchDown
|
||
star_system_name = entry.get('StarSystem', this.system)
|
||
body_name = entry.get('Body', state['Body'] if state['BodyType'] == 'Planet' else None)
|
||
|
||
if star_system_name is None:
|
||
logger.warning('Refusing to update addCommanderTravelLand as we dont have a StarSystem!')
|
||
to_send_data = None
|
||
|
||
if body_name is None:
|
||
logger.warning('Refusing to update addCommanderTravelLand as we dont have a Body!')
|
||
to_send_data = None
|
||
|
||
if (op := entry.get('OnPlanet')) is not None and not op:
|
||
logger.warning('Refusing to update addCommanderTravelLand when OnPlanet is False!')
|
||
logger.warning(f'{entry=}')
|
||
to_send_data = None
|
||
|
||
if not entry['PlayerControlled']:
|
||
logger.info("Not updating inara addCommanderTravelLand for autonomous recall landing")
|
||
to_send_data = None
|
||
|
||
if to_send_data is not None:
|
||
# Above checks passed. Lets build and send this!
|
||
to_send_data['starsystemName'] = star_system_name # Required
|
||
to_send_data['starsystemBodyName'] = body_name # Required
|
||
|
||
# Following are optional
|
||
|
||
# lat/long is always there unless its an automated (recall) landing. Thus as we're sure its _not_
|
||
# we can assume this exists. If it doesn't its a bug anyway.
|
||
to_send_data['starsystemBodyCoords'] = [entry['Latitude'], entry['Longitude']]
|
||
if state.get('ShipID') is not None:
|
||
to_send_data['shipGameID'] = state['ShipID']
|
||
|
||
if state.get('ShipType') is not None:
|
||
to_send_data['shipType'] = state['ShipType']
|
||
|
||
to_send_data['isTaxiShuttle'] = False
|
||
to_send_data['isTaxiDropShip'] = False
|
||
|
||
new_add_event('addCommanderTravelLand', entry['timestamp'], to_send_data)
|
||
|
||
elif event_name == 'ShipLockerMaterials':
|
||
odyssey_plural_microresource_types = ('Items', 'Components', 'Data', 'Consumables')
|
||
# we're getting new data here. so reset it on inara's side just to be sure that we set everything right
|
||
reset_data = [{'itemType': t} for t in odyssey_plural_microresource_types]
|
||
set_data = []
|
||
for typ in odyssey_plural_microresource_types:
|
||
set_data.extend([
|
||
{'itemName': thing['Name'], 'itemCount': thing['Count'], 'itemType': typ} for thing in entry[typ]
|
||
])
|
||
|
||
new_add_event('resetCommanderInventory', entry['timestamp'], reset_data)
|
||
new_add_event('setCommanderInventory', entry['timestamp'], set_data)
|
||
|
||
elif event_name in ('CreateSuitLoadout', 'SuitLoadout'):
|
||
# CreateSuitLoadout and SuitLoadout are pretty much the same event:
|
||
# ╙─╴% cat Journal.* | jq 'select(.event == "SuitLoadout" or .event == "CreateSuitLoadout") | keys' -c \
|
||
# | uniq
|
||
#
|
||
# ["LoadoutID","LoadoutName","Modules","SuitID","SuitMods","SuitName","SuitName_Localised","event",
|
||
# "timestamp"]
|
||
|
||
to_send = {
|
||
'loadoutGameID': entry['LoadoutID'],
|
||
'loadoutName': entry['LoadoutName'],
|
||
'suitGameID': entry['SuitID'],
|
||
'suitType': entry['SuitName'],
|
||
'suitMods': entry['SuitMods'],
|
||
'suitLoadout': [
|
||
{
|
||
'slotName': x['SlotName'],
|
||
'itemName': x['ModuleName'],
|
||
'itemClass': x['Class'],
|
||
'itemGameID': x['SuitModuleID'],
|
||
'engineering': [{'blueprintName': mod} for mod in x['WeaponMods']],
|
||
} for x in entry['Modules']
|
||
],
|
||
}
|
||
|
||
new_add_event('setCommanderSuitLoadout', entry['timestamp'], to_send)
|
||
|
||
elif event_name == 'DeleteSuitLoadout':
|
||
new_add_event('delCommanderSuitLoadout', entry['timestamp'], {'loadoutGameID': entry['LoadoutID']})
|
||
|
||
elif event_name == 'RenameSuitLoadout':
|
||
to_send = {
|
||
'loadoutGameID': entry['LoadoutID'],
|
||
'loadoutName': entry['LoadoutName'],
|
||
# may as well...
|
||
'suitType': entry['SuitName'],
|
||
'suitGameID': entry['SuitID']
|
||
}
|
||
new_add_event('updateCommanderSuitLoadout', entry['timestamp'], {})
|
||
|
||
elif event_name == 'LoadoutEquipModule':
|
||
to_send = {
|
||
'loadoutGameID': entry['LoadoutID'],
|
||
'loadoutName': entry['LoadoutName'],
|
||
'suitType': entry['SuitName'],
|
||
'suitGameID': entry['SuitID'],
|
||
'suitLoadout': [
|
||
{
|
||
'slotName': entry['SlotName'],
|
||
'itemName': entry['ModuleName'],
|
||
'itemGameID': entry['SuitModuleID'],
|
||
'itemClass': entry['Class'],
|
||
'engineering': [{'blueprintName': mod} for mod in entry['WeaponMods']],
|
||
}
|
||
],
|
||
}
|
||
|
||
new_add_event('updateCommanderSuitLoadout', entry['timestamp'], to_send)
|
||
|
||
elif event_name == 'Location':
|
||
to_send = {
|
||
'starsystemName': entry['StarSystem'],
|
||
'starsystemCoords': entry['StarPos'],
|
||
}
|
||
|
||
if entry['Docked']:
|
||
to_send['stationName'] = entry['StationName']
|
||
to_send['marketID'] = entry['MarketID']
|
||
|
||
if entry['Docked'] and entry['BodyType'] == 'Planet':
|
||
# we're Docked, but we're not on a Station, thus we're docked at a planetary base of some kind
|
||
# and thus, we SHOULD include starsystemBodyName
|
||
to_send['starsystemBodyName'] = entry['Body']
|
||
|
||
if 'Longitude' in entry and 'Latitude' in entry:
|
||
# These were included thus we are landed
|
||
to_send['starsystemBodyCoords'] = [entry['Latitude'], entry['Longitude']]
|
||
# if we're not Docked, but have these, we're either landed or close enough that it doesn't matter.
|
||
to_send['starsystemBodyName'] = entry['Body']
|
||
|
||
new_add_event('setCommanderTravelLocation', entry['timestamp'], to_send)
|
||
|
||
# Community Goals
|
||
if event_name == 'CommunityGoal':
|
||
# Remove any unsent
|
||
this.filter_events(
|
||
current_creds, lambda e: e.name not in ('setCommunityGoal', 'setCommanderCommunityGoalProgress')
|
||
)
|
||
|
||
# this.events = list(filter(
|
||
# lambda e: e['eventName'] not in ('setCommunityGoal', 'setCommanderCommunityGoalProgress'),
|
||
# this.events
|
||
# ))
|
||
|
||
for goal in entry['CurrentGoals']:
|
||
data = OrderedDict([
|
||
('communitygoalGameID', goal['CGID']),
|
||
('communitygoalName', goal['Title']),
|
||
('starsystemName', goal['SystemName']),
|
||
('stationName', goal['MarketName']),
|
||
('goalExpiry', goal['Expiry']),
|
||
('isCompleted', goal['IsComplete']),
|
||
('contributorsNum', goal['NumContributors']),
|
||
('contributionsTotal', goal['CurrentTotal']),
|
||
])
|
||
|
||
if 'TierReached' in goal:
|
||
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']
|
||
|
||
new_add_event('setCommunityGoal', entry['timestamp'], data)
|
||
|
||
data = OrderedDict([
|
||
('communitygoalGameID', goal['CGID']),
|
||
('contribution', goal['PlayerContribution']),
|
||
('percentileBand', goal['PlayerPercentileBand']),
|
||
])
|
||
|
||
if 'Bonus' in goal:
|
||
data['percentileBandReward'] = goal['Bonus']
|
||
|
||
if 'PlayerInTopRank' in goal:
|
||
data['isTopRank'] = goal['PlayerInTopRank']
|
||
|
||
new_add_event('setCommanderCommunityGoalProgress', entry['timestamp'], data)
|
||
|
||
# Friends
|
||
if event_name == 'Friends':
|
||
if entry['Status'] in ['Added', 'Online']:
|
||
new_add_event(
|
||
'addCommanderFriend',
|
||
entry['timestamp'],
|
||
{
|
||
'commanderName': entry['Name'],
|
||
'gamePlatform': 'pc',
|
||
}
|
||
)
|
||
|
||
elif entry['Status'] in ['Declined', 'Lost']:
|
||
new_add_event(
|
||
'delCommanderFriend',
|
||
entry['timestamp'],
|
||
{
|
||
'commanderName': entry['Name'],
|
||
'gamePlatform': 'pc',
|
||
}
|
||
)
|
||
|
||
this.newuser = False
|
||
|
||
# Only actually change URLs if we are current provider.
|
||
if config.get_str('system_provider') == 'Inara':
|
||
this.system_link['text'] = this.system
|
||
# Do *NOT* set 'url' here, as it's set to a function that will call
|
||
# through correctly. We don't want a static string.
|
||
this.system_link.update_idletasks()
|
||
|
||
if config.get_str('station_provider') == 'Inara':
|
||
to_set: str = cast(str, this.station)
|
||
if not to_set:
|
||
if this.system_population is not None and this.system_population > 0:
|
||
to_set = STATION_UNDOCKED
|
||
else:
|
||
to_set = ''
|
||
|
||
this.station_link['text'] = to_set
|
||
# Do *NOT* set 'url' here, as it's set to a function that will call
|
||
# through correctly. We don't want a static string.
|
||
this.station_link.update_idletasks()
|
||
|
||
return '' # No error
|
||
|
||
|
||
def cmdr_data(data: CAPIData, is_beta): # noqa: CCR001
|
||
"""CAPI event hook."""
|
||
this.cmdr = data['commander']['name']
|
||
|
||
# Always store initially, even if we're not the *current* system provider.
|
||
if not this.station_marketid:
|
||
this.station_marketid = data['commander']['docked'] and data['lastStarport']['id']
|
||
|
||
# Only trust CAPI if these aren't yet set
|
||
this.system = this.system if this.system else data['lastSystem']['name']
|
||
|
||
if not this.station and data['commander']['docked']:
|
||
this.station = data['lastStarport']['name']
|
||
|
||
# Override standard URL functions
|
||
if config.get_str('system_provider') == 'Inara':
|
||
this.system_link['text'] = this.system
|
||
# Do *NOT* set 'url' here, as it's set to a function that will call
|
||
# through correctly. We don't want a static string.
|
||
this.system_link.update_idletasks()
|
||
|
||
if config.get_str('station_provider') == 'Inara':
|
||
if data['commander']['docked'] or this.on_foot and this.station:
|
||
this.station_link['text'] = this.station
|
||
|
||
elif data['lastStarport']['name'] and data['lastStarport']['name'] != "":
|
||
this.station_link['text'] = STATION_UNDOCKED
|
||
|
||
else:
|
||
this.station_link['text'] = ''
|
||
|
||
# Do *NOT* set 'url' here, as it's set to a function that will call
|
||
# through correctly. We don't want a static string.
|
||
this.station_link.update_idletasks()
|
||
|
||
if config.get_int('inara_out') and not is_beta and not this.multicrew and credentials(this.cmdr):
|
||
if not (CREDIT_RATIO > this.lastcredits / data['commander']['credits'] > 1/CREDIT_RATIO):
|
||
this.filter_events(
|
||
Credentials(this.cmdr, this.FID, str(credentials(this.cmdr))),
|
||
lambda e: e.name != 'setCommanderCredits'
|
||
)
|
||
|
||
# this.events = [x for x in this.events if x['eventName'] != 'setCommanderCredits'] # Remove any unsent
|
||
new_add_event(
|
||
'setCommanderCredits',
|
||
data['timestamp'],
|
||
{
|
||
'commanderCredits': data['commander']['credits'],
|
||
'commanderLoan': data['commander'].get('debt', 0),
|
||
}
|
||
)
|
||
|
||
this.lastcredits = int(data['commander']['credits'])
|
||
|
||
|
||
def make_loadout(state: Dict[str, Any]) -> OrderedDictT[str, Any]: # noqa: CCR001
|
||
"""
|
||
Construct an inara loadout from an event.
|
||
|
||
:param state: The event / state to construct the event from
|
||
:return: The constructed loadout
|
||
"""
|
||
modules = []
|
||
for m in state['Modules'].values():
|
||
module: OrderedDictT[str, Any] = OrderedDict([
|
||
('slotName', m['Slot']),
|
||
('itemName', m['Item']),
|
||
('itemHealth', m['Health']),
|
||
('isOn', m['On']),
|
||
('itemPriority', m['Priority']),
|
||
])
|
||
|
||
if 'AmmoInClip' in m:
|
||
module['itemAmmoClip'] = m['AmmoInClip']
|
||
|
||
if 'AmmoInHopper' in m:
|
||
module['itemAmmoHopper'] = m['AmmoInHopper']
|
||
|
||
if 'Value' in m:
|
||
module['itemValue'] = m['Value']
|
||
|
||
if 'Hot' in m:
|
||
module['isHot'] = m['Hot']
|
||
|
||
if 'Engineering' in m:
|
||
engineering: OrderedDictT[str, Any] = OrderedDict([
|
||
('blueprintName', m['Engineering']['BlueprintName']),
|
||
('blueprintLevel', m['Engineering']['Level']),
|
||
('blueprintQuality', m['Engineering']['Quality']),
|
||
])
|
||
|
||
if 'ExperimentalEffect' in m['Engineering']:
|
||
engineering['experimentalEffect'] = m['Engineering']['ExperimentalEffect']
|
||
|
||
engineering['modifiers'] = []
|
||
for mod in m['Engineering']['Modifiers']:
|
||
modifier: OrderedDictT[str, Any] = OrderedDict([
|
||
('name', mod['Label']),
|
||
])
|
||
|
||
if 'OriginalValue' in mod:
|
||
modifier['value'] = mod['Value']
|
||
modifier['originalValue'] = mod['OriginalValue']
|
||
modifier['lessIsGood'] = mod['LessIsGood']
|
||
|
||
else:
|
||
modifier['value'] = mod['ValueStr']
|
||
|
||
engineering['modifiers'].append(modifier)
|
||
|
||
module['engineering'] = engineering
|
||
|
||
modules.append(module)
|
||
|
||
return OrderedDict([
|
||
('shipType', state['ShipType']),
|
||
('shipGameID', state['ShipID']),
|
||
('shipLoadout', modules),
|
||
])
|
||
|
||
|
||
def new_add_event(
|
||
name: str,
|
||
timestamp: str,
|
||
data: EVENT_DATA,
|
||
cmdr: Optional[str] = None,
|
||
fid: Optional[str] = None
|
||
):
|
||
"""
|
||
Add a journal event to the queue, to be sent to inara at the next opportunity.
|
||
|
||
If provided, use the given cmdr name over the current one
|
||
|
||
:param name: name of the event
|
||
:param timestamp: timestamp of the event
|
||
:param data: payload for the event
|
||
:param cmdr: the commander to send as, defaults to the current commander
|
||
"""
|
||
if cmdr is None:
|
||
cmdr = this.cmdr
|
||
|
||
if fid is None:
|
||
fid = this.FID
|
||
|
||
api_key = credentials(this.cmdr)
|
||
if api_key is None:
|
||
logger.warning(f"cannot find an API key for cmdr {this.cmdr!r}")
|
||
return
|
||
|
||
key = Credentials(str(cmdr), str(fid), api_key) # this fails type checking due to `this` weirdness, hence str()
|
||
|
||
with this.event_lock:
|
||
this.events[key].append(Event(name, timestamp, data))
|
||
|
||
|
||
def new_worker():
|
||
"""
|
||
Handle sending events to the Inara API.
|
||
|
||
Will only ever send one message per WORKER_WAIT_TIME, regardless of status.
|
||
"""
|
||
logger.debug('Starting...')
|
||
while True:
|
||
events = get_events()
|
||
if (res := killswitch.get_disabled("plugins.inara.worker")).disabled:
|
||
logger.warning(f"Inara worker disabled via killswitch. ({res.reason})")
|
||
continue
|
||
|
||
for creds, event_list in events.items():
|
||
if not event_list:
|
||
continue
|
||
|
||
data = {
|
||
'header': {
|
||
'appName': applongname,
|
||
'appVersion': str(appversion()),
|
||
'APIkey': creds.api_key,
|
||
'commanderName': creds.cmdr,
|
||
'commanderFrontierID': creds.fid,
|
||
},
|
||
'events': [
|
||
{'eventName': e.name, 'eventTimestamp': e.timestamp, 'eventData': e.data} for e in event_list
|
||
]
|
||
}
|
||
logger.info(f'sending {len(data["events"])} events for {creds.cmdr}')
|
||
logger.trace(f'Events:\n{json.dumps(data)}\n')
|
||
try_send_data(TARGET_URL, data)
|
||
|
||
time.sleep(WORKER_WAIT_TIME)
|
||
|
||
logger.debug('Done.')
|
||
|
||
|
||
def get_events(clear: bool = True) -> Dict[Credentials, List[Event]]:
|
||
"""
|
||
Fetch a frozen copy of all events from the current queue.
|
||
|
||
:param clear: whether or not to clear the queues as we go, defaults to True
|
||
:return: the frozen event list
|
||
"""
|
||
out: Dict[Credentials, List[Event]] = {}
|
||
with this.event_lock:
|
||
for key, events in this.events.items():
|
||
out[key] = list(events)
|
||
if clear:
|
||
events.clear()
|
||
|
||
return out
|
||
|
||
|
||
def try_send_data(url: str, data: Mapping[str, Any]) -> None:
|
||
"""
|
||
Attempt repeatedly to send the payload forward.
|
||
|
||
:param url: target URL for the payload
|
||
:param data: the payload
|
||
"""
|
||
for i in range(3):
|
||
logger.debug(f"sending data to API, attempt #{i}")
|
||
try:
|
||
if send_data(url, data):
|
||
break
|
||
|
||
except Exception as e:
|
||
logger.debug('unable to send events', exc_info=e)
|
||
return
|
||
|
||
|
||
def send_data(url: str, data: Mapping[str, Any]) -> bool: # noqa: CCR001
|
||
"""
|
||
Write a set of events to the inara API.
|
||
|
||
:param url: the target URL to post to
|
||
:param data: the data to POST
|
||
:return: success state
|
||
"""
|
||
r = this.session.post(url, data=json.dumps(data, separators=(',', ':')), timeout=_TIMEOUT)
|
||
r.raise_for_status()
|
||
reply = r.json()
|
||
status = reply['header']['eventStatus']
|
||
|
||
if status // 100 != 2: # 2xx == OK (maybe with warnings)
|
||
# Log fatal errors
|
||
logger.warning(f'Inara\t{status} {reply["header"].get("eventStatusText", "")}')
|
||
logger.debug(f'JSON data:\n{json.dumps(data, indent=2, separators = (",", ": "))}')
|
||
plug.show_error(_('Error: Inara {MSG}').format(MSG=reply['header'].get('eventStatusText', status)))
|
||
|
||
else:
|
||
# Log individual errors and warnings
|
||
for data_event, reply_event in zip(data['events'], reply['events']):
|
||
if reply_event['eventStatus'] != 200:
|
||
if ("Everything was alright, the near-neutral status just wasn't stored."
|
||
not in reply_event.get("eventStatusText")):
|
||
logger.warning(f'Inara\t{status} {reply_event.get("eventStatusText", "")}')
|
||
logger.debug(f'JSON data:\n{json.dumps(data_event)}')
|
||
|
||
if reply_event['eventStatus'] // 100 != 2:
|
||
plug.show_error(_('Error: Inara {MSG}').format(
|
||
MSG=f'{data_event["eventName"]},'
|
||
f'{reply_event.get("eventStatusText", reply_event["eventStatus"])}'
|
||
))
|
||
|
||
if data_event['eventName'] in (
|
||
'addCommanderTravelCarrierJump',
|
||
'addCommanderTravelDock',
|
||
'addCommanderTravelFSDJump',
|
||
'setCommanderTravelLocation'
|
||
):
|
||
this.lastlocation = reply_event.get('eventData', {})
|
||
# calls update_location in main thread
|
||
if not config.shutting_down:
|
||
this.system_link.event_generate('<<InaraLocation>>', when="tail")
|
||
|
||
elif data_event['eventName'] in ['addCommanderShip', 'setCommanderShip']:
|
||
this.lastship = reply_event.get('eventData', {})
|
||
# calls update_ship in main thread
|
||
if not config.shutting_down:
|
||
this.system_link.event_generate('<<InaraShip>>', when="tail")
|
||
|
||
return True # regardless of errors above, we DID manage to send it, therefore inform our caller as such
|
||
|
||
|
||
def update_location(event=None) -> None:
|
||
"""
|
||
Update other plugins with our response to system and station changes.
|
||
|
||
:param event: Unused and ignored, defaults to None
|
||
"""
|
||
if this.lastlocation:
|
||
for plugin in plug.provides('inara_notify_location'):
|
||
plug.invoke(plugin, None, 'inara_notify_location', this.lastlocation)
|
||
|
||
|
||
def inara_notify_location(event_data) -> None:
|
||
"""Unused."""
|
||
pass
|
||
|
||
|
||
def update_ship(event=None) -> None:
|
||
"""
|
||
Update other plugins with our response to changing.
|
||
|
||
:param event: Unused and ignored, defaults to None
|
||
"""
|
||
if this.lastship:
|
||
for plugin in plug.provides('inara_notify_ship'):
|
||
plug.invoke(plugin, None, 'inara_notify_ship', this.lastship)
|