mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2025-04-17 17:42:20 +03:00
943 lines
42 KiB
Python
943 lines
42 KiB
Python
#
|
||
# Inara sync
|
||
#
|
||
|
||
from collections import OrderedDict
|
||
import json
|
||
import requests
|
||
import sys
|
||
import time
|
||
from operator import itemgetter
|
||
from queue import Queue
|
||
from threading import Thread
|
||
|
||
import tkinter as tk
|
||
from ttkHyperlinkLabel import HyperlinkLabel
|
||
import myNotebook as nb
|
||
|
||
from config import appname, applongname, appversion, config
|
||
import plug
|
||
|
||
if __debug__:
|
||
from traceback import print_exc
|
||
|
||
|
||
_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
|
||
|
||
|
||
this = sys.modules[__name__] # For holding module globals
|
||
this.session = requests.Session()
|
||
this.queue = Queue() # Items to be sent to Inara by worker thread
|
||
this.lastlocation = None # eventData from the last Commander's Flight Log event
|
||
this.lastship = None # eventData from the last addCommanderShip or setCommanderShip event
|
||
|
||
# Cached Cmdr state
|
||
this.events = [] # Unsent events
|
||
this.cmdr = None
|
||
this.FID = None # Frontier ID
|
||
this.multicrew = False # don't send captain's ship info to Inara while on a crew
|
||
this.newuser = False # just entered API Key - send state immediately
|
||
this.newsession = True # starting a new session - wait for Cargo event
|
||
this.undocked = False # just undocked
|
||
this.suppress_docked = False # Skip initial Docked event if started docked
|
||
this.cargo = None
|
||
this.materials = None
|
||
this.lastcredits = 0 # Send credit update soon after Startup / new game
|
||
this.storedmodules = None
|
||
this.loadout = None
|
||
this.fleet = None
|
||
this.shipswap = False # just swapped ship
|
||
|
||
this.last_update_time = time.time() # last time we updated (set to now because we're going to update quickly)
|
||
FLOOD_LIMIT_SECONDS = 45 # minimum time between sending non-major cargo triggered messages
|
||
|
||
|
||
# Main window clicks
|
||
this.system_link = None
|
||
this.system = None
|
||
this.system_address = None
|
||
this.system_population = None
|
||
this.station_link = None
|
||
this.station = None
|
||
this.station_marketid = None
|
||
STATION_UNDOCKED: str = '×' # "Station" name to display when not docked = U+00D7
|
||
|
||
def system_url(system_name):
|
||
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 this.system
|
||
|
||
def station_url(system_name, station_name):
|
||
if system_name:
|
||
if station_name:
|
||
return requests.utils.requote_uri(f'https://inara.cz/galaxy-station/?search={system_name}%20[{station_name}]')
|
||
return system_url(system_name)
|
||
|
||
return this.station or this.system
|
||
|
||
|
||
def plugin_start3(plugin_dir):
|
||
this.thread = Thread(target = worker, name = 'Inara worker')
|
||
this.thread.daemon = True
|
||
this.thread.start()
|
||
return 'Inara'
|
||
|
||
def plugin_app(parent):
|
||
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():
|
||
# Send any unsent events
|
||
call()
|
||
# Signal thread to close and wait for it
|
||
this.queue.put(None)
|
||
this.thread.join()
|
||
this.thread = None
|
||
|
||
def plugin_prefs(parent, cmdr, is_beta):
|
||
|
||
PADX = 10
|
||
BUTTONX = 12 # indent Checkbuttons and Radiobuttons
|
||
PADY = 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=PADX, sticky=tk.W) # Don't translate
|
||
this.log = tk.IntVar(value = config.getint('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=BUTTONX, pady=(5,0), sticky=tk.W)
|
||
|
||
nb.Label(frame).grid(sticky=tk.W) # big spacer
|
||
this.label = HyperlinkLabel(frame, text=_('Inara credentials'), background=nb.Label().cget('background'), url='https://inara.cz/settings-api', underline=True) # Section heading in settings
|
||
this.label.grid(columnspan=2, padx=PADX, sticky=tk.W)
|
||
|
||
this.apikey_label = nb.Label(frame, text=_('API Key')) # EDSM setting
|
||
this.apikey_label.grid(row=12, padx=PADX, sticky=tk.W)
|
||
this.apikey = nb.Entry(frame)
|
||
this.apikey.grid(row=12, column=1, padx=PADX, pady=PADY, sticky=tk.EW)
|
||
|
||
prefs_cmdr_changed(cmdr, is_beta)
|
||
|
||
return frame
|
||
|
||
def prefs_cmdr_changed(cmdr, is_beta):
|
||
this.log_button['state'] = cmdr and not is_beta and tk.NORMAL or 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)
|
||
this.label['state'] = this.apikey_label['state'] = this.apikey['state'] = cmdr and not is_beta and this.log.get() and tk.NORMAL or tk.DISABLED
|
||
|
||
def prefsvarchanged():
|
||
this.label['state'] = this.apikey_label['state'] = this.apikey['state'] = this.log.get() and this.log_button['state'] or tk.DISABLED
|
||
|
||
def prefs_changed(cmdr, is_beta):
|
||
changed = config.getint('inara_out') != this.log.get()
|
||
config.set('inara_out', this.log.get())
|
||
|
||
# Override standard URL functions
|
||
if config.get('system_provider') == 'Inara':
|
||
this.system_link['url'] = system_url(this.system)
|
||
if config.get('station_provider') == 'Inara':
|
||
this.station_link['url'] = station_url(this.system, this.station)
|
||
|
||
if cmdr and not is_beta:
|
||
this.cmdr = cmdr
|
||
this.FID = None
|
||
cmdrs = config.get('inara_cmdrs') or []
|
||
apikeys = config.get('inara_apikeys') or []
|
||
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
|
||
add_event('getCommanderProfile', time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), { 'searchName': cmdr })
|
||
call()
|
||
|
||
def credentials(cmdr):
|
||
# Credentials for cmdr
|
||
if not cmdr:
|
||
return None
|
||
|
||
cmdrs = config.get('inara_cmdrs') or []
|
||
if cmdr in cmdrs and config.get('inara_apikeys'):
|
||
return config.get('inara_apikeys')[cmdrs.index(cmdr)]
|
||
else:
|
||
return None
|
||
|
||
|
||
def journal_entry(cmdr, is_beta, system, station, entry, state):
|
||
|
||
# Send any unsent events when switching accounts
|
||
if cmdr and cmdr != this.cmdr:
|
||
call()
|
||
|
||
this.cmdr = cmdr
|
||
this.FID = state['FID']
|
||
this.multicrew = bool(state['Role'])
|
||
|
||
if entry['event'] == 'LoadGame' or this.newuser:
|
||
# clear cached state
|
||
if entry['event'] == '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 entry['event'] 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 entry['event'] in ['ShipyardNew', 'ShipyardSwap'] or (entry['event'] == 'Location' and entry['Docked']):
|
||
this.suppress_docked = True
|
||
|
||
# Always update, even if we're not the *current* system or station provider.
|
||
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 = entry.get('Population')
|
||
if pop is not None:
|
||
this.system_population = pop
|
||
|
||
this.station = entry.get('StationName') or this.station
|
||
this.station_marketid = entry.get('MarketID') or this.station_marketid
|
||
# We might pick up StationName in DockingRequested, make sure we clear it if leaving
|
||
if entry['event'] in ('Undocked', 'FSDJump', 'SupercruiseEntry'):
|
||
this.station = None
|
||
this.station_marketid = None
|
||
|
||
# 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, EngineerProgress and PowerPlay affiliation.
|
||
# Also send material and cargo (if changed) whenever we send an update.
|
||
|
||
if config.getint('inara_out') and not is_beta and not this.multicrew and credentials(cmdr):
|
||
try:
|
||
old_events = len(this.events) # Will only send existing events if we add a new event below
|
||
|
||
# Dump starting state to Inara
|
||
|
||
if (this.newuser or
|
||
entry['event'] == 'StartUp' or
|
||
(this.newsession and entry['event'] == 'Cargo')):
|
||
this.newuser = False
|
||
this.newsession = False
|
||
|
||
# Send rank info to Inara on startup
|
||
add_event('setCommanderRankPilot', entry['timestamp'],
|
||
[
|
||
OrderedDict([
|
||
('rankName', k.lower()),
|
||
('rankValue', v[0]),
|
||
('rankProgress', v[1] / 100.0),
|
||
]) for k,v in state['Rank'].items() if v is not None
|
||
])
|
||
add_event('setCommanderReputationMajorFaction', entry['timestamp'],
|
||
[
|
||
OrderedDict([
|
||
('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
|
||
add_event('setCommanderRankEngineer', entry['timestamp'],
|
||
[
|
||
OrderedDict([
|
||
('engineerName', k),
|
||
type(v) is tuple and ('rankValue', v[0]) or ('rankStage', v),
|
||
]) for k,v in state['Engineers'].items()
|
||
])
|
||
|
||
# Update location
|
||
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
|
||
data = OrderedDict([
|
||
('shipType', state['ShipType']),
|
||
('shipGameID', state['ShipID']),
|
||
('shipName', state['ShipName']), # Can be None
|
||
('shipIdent', state['ShipIdent']), # Can be None
|
||
('isCurrentShip', True),
|
||
])
|
||
if state['HullValue']:
|
||
data['shipHullValue'] = state['HullValue']
|
||
if state['ModulesValue']:
|
||
data['shipModulesValue'] = state['ModulesValue']
|
||
data['shipRebuyCost'] = state['Rebuy']
|
||
add_event('setCommanderShip', entry['timestamp'], data)
|
||
|
||
this.loadout = make_loadout(state)
|
||
add_event('setCommanderShipLoadout', entry['timestamp'], this.loadout)
|
||
|
||
|
||
# Promotions
|
||
elif entry['event'] == 'Promotion':
|
||
for k,v in state['Rank'].items():
|
||
if k in entry:
|
||
add_event('setCommanderRankPilot', entry['timestamp'],
|
||
OrderedDict([
|
||
('rankName', k.lower()),
|
||
('rankValue', v[0]),
|
||
('rankProgress', 0),
|
||
]))
|
||
elif entry['event'] == 'EngineerProgress' and 'Engineer' in entry:
|
||
add_event('setCommanderRankEngineer', entry['timestamp'],
|
||
OrderedDict([
|
||
('engineerName', entry['Engineer']),
|
||
'Rank' in entry and ('rankValue', entry['Rank']) or ('rankStage', entry['Progress']),
|
||
]))
|
||
|
||
# PowerPlay status change
|
||
if entry['event'] == 'PowerplayJoin':
|
||
add_event('setCommanderRankPower', entry['timestamp'],
|
||
OrderedDict([
|
||
('powerName', entry['Power']),
|
||
('rankValue', 1),
|
||
]))
|
||
elif entry['event'] == 'PowerplayLeave':
|
||
add_event('setCommanderRankPower', entry['timestamp'],
|
||
OrderedDict([
|
||
('powerName', entry['Power']),
|
||
('rankValue', 0),
|
||
]))
|
||
elif entry['event'] == 'PowerplayDefect':
|
||
add_event('setCommanderRankPower', entry['timestamp'],
|
||
OrderedDict([
|
||
('powerName', entry['ToPower']),
|
||
('rankValue', 1),
|
||
]))
|
||
|
||
# Ship change
|
||
if entry['event'] == 'Loadout' and this.shipswap:
|
||
data = OrderedDict([
|
||
('shipType', state['ShipType']),
|
||
('shipGameID', state['ShipID']),
|
||
('shipName', state['ShipName']), # Can be None
|
||
('shipIdent', state['ShipIdent']), # Can be None
|
||
('isCurrentShip', True),
|
||
])
|
||
if state['HullValue']:
|
||
data['shipHullValue'] = state['HullValue']
|
||
if state['ModulesValue']:
|
||
data['shipModulesValue'] = state['ModulesValue']
|
||
data['shipRebuyCost'] = state['Rebuy']
|
||
add_event('setCommanderShip', entry['timestamp'], data)
|
||
|
||
this.loadout = make_loadout(state)
|
||
add_event('setCommanderShipLoadout', entry['timestamp'], this.loadout)
|
||
this.shipswap = False
|
||
|
||
# Location change
|
||
elif entry['event'] == '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:
|
||
add_event('addCommanderTravelDock', entry['timestamp'],
|
||
OrderedDict([
|
||
('starsystemName', system),
|
||
('stationName', station),
|
||
('shipType', state['ShipType']),
|
||
('shipGameID', state['ShipID']),
|
||
]))
|
||
elif entry['event'] == 'Undocked':
|
||
this.undocked = True
|
||
this.station = None
|
||
elif entry['event'] == 'SupercruiseEntry':
|
||
if this.undocked:
|
||
# Staying in system after undocking - send any pending events from in-station action
|
||
add_event('setCommanderTravelLocation', entry['timestamp'],
|
||
OrderedDict([
|
||
('starsystemName', system),
|
||
('shipType', state['ShipType']),
|
||
('shipGameID', state['ShipID']),
|
||
]))
|
||
this.undocked = False
|
||
elif entry['event'] == 'FSDJump':
|
||
this.undocked = False
|
||
add_event('addCommanderTravelFSDJump', entry['timestamp'],
|
||
OrderedDict([
|
||
('starsystemName', entry['StarSystem']),
|
||
('jumpDistance', entry['JumpDist']),
|
||
('shipType', state['ShipType']),
|
||
('shipGameID', state['ShipID']),
|
||
]))
|
||
|
||
if entry.get('Factions'):
|
||
add_event('setCommanderReputationMinorFaction', entry['timestamp'],
|
||
[
|
||
OrderedDict([
|
||
('minorfactionName', f['Name']),
|
||
('minorfactionReputation', f['MyReputation']/100.0),
|
||
]) for f in entry['Factions']
|
||
])
|
||
elif entry['event'] == 'CarrierJump':
|
||
add_event('addCommanderTravelCarrierJump', entry['timestamp'],
|
||
OrderedDict([
|
||
('starsystemName', entry['StarSystem']),
|
||
('stationName', entry['StationName']),
|
||
('marketID', entry['MarketID']),
|
||
('shipType', state['ShipType']),
|
||
('shipGameID', state['ShipID']),
|
||
]))
|
||
if entry.get('Factions'):
|
||
add_event('setCommanderReputationMinorFaction', entry['timestamp'],
|
||
[
|
||
OrderedDict([
|
||
('minorfactionName', f['Name']),
|
||
('minorfactionReputation', f['MyReputation']/100.0),
|
||
]) for f in entry['Factions']
|
||
])
|
||
# Ignore the following 'Docked' event
|
||
this.suppress_docked = True
|
||
|
||
|
||
cargo = [OrderedDict([('itemName', k), ('itemCount', state['Cargo'][k])]) for k in sorted(state['Cargo'])]
|
||
|
||
# if our cargo differers from last we checked, we're at a station,
|
||
# and our flood limit isnt covered, queue an update
|
||
should_poll = this.cargo != cargo and time.time() - this.last_update_time > FLOOD_LIMIT_SECONDS
|
||
|
||
# Send event(s) to Inara
|
||
if entry['event'] == 'ShutDown' or len(this.events) > old_events or should_poll:
|
||
|
||
# Send cargo and materials if changed
|
||
if this.cargo != cargo:
|
||
add_event('setCommanderInventoryCargo', entry['timestamp'], cargo)
|
||
this.cargo = cargo
|
||
materials = []
|
||
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:
|
||
add_event('setCommanderInventoryMaterials', entry['timestamp'], materials)
|
||
this.materials = materials
|
||
|
||
# Queue a call to Inara
|
||
call()
|
||
|
||
except Exception as e:
|
||
if __debug__: print_exc()
|
||
return str(e)
|
||
|
||
#
|
||
# Events that don't need to be sent immediately but will be sent on the next mandatory event
|
||
#
|
||
|
||
# Send credits and stats to Inara on startup only - otherwise may be out of date
|
||
if entry['event'] == 'LoadGame':
|
||
add_event('setCommanderCredits', entry['timestamp'],
|
||
OrderedDict([
|
||
('commanderCredits', state['Credits']),
|
||
('commanderLoan', state['Loan']),
|
||
]))
|
||
this.lastcredits = state['Credits']
|
||
elif entry['event'] == 'Statistics':
|
||
add_event('setCommanderGameStatistics', entry['timestamp'], state['Statistics']) # may be out of date
|
||
|
||
# Selling / swapping ships
|
||
if entry['event'] == 'ShipyardNew':
|
||
add_event('addCommanderShip', entry['timestamp'],
|
||
OrderedDict([
|
||
('shipType', entry['ShipType']),
|
||
('shipGameID', entry['NewShipID']),
|
||
]))
|
||
this.shipswap = True # Want subsequent Loadout event to be sent immediately
|
||
|
||
elif entry['event'] in ['ShipyardBuy', 'ShipyardSell', 'SellShipOnRebuy', 'ShipyardSwap']:
|
||
if entry['event'] == 'ShipyardSwap':
|
||
this.shipswap = True # Don't know new ship name and ident 'til the following Loadout event
|
||
if 'StoreShipID' in entry:
|
||
add_event('setCommanderShip', entry['timestamp'],
|
||
OrderedDict([
|
||
('shipType', entry['StoreOldShip']),
|
||
('shipGameID', entry['StoreShipID']),
|
||
('starsystemName', system),
|
||
('stationName', station),
|
||
]))
|
||
elif 'SellShipID' in entry:
|
||
add_event('delCommanderShip', entry['timestamp'],
|
||
OrderedDict([
|
||
('shipType', entry.get('SellOldShip', entry['ShipType'])),
|
||
('shipGameID', entry['SellShipID']),
|
||
]))
|
||
|
||
elif entry['event'] == 'SetUserShipName':
|
||
add_event('setCommanderShip', entry['timestamp'],
|
||
OrderedDict([
|
||
('shipType', state['ShipType']),
|
||
('shipGameID', state['ShipID']),
|
||
('shipName', state['ShipName']), # Can be None
|
||
('shipIdent', state['ShipIdent']), # Can be None
|
||
('isCurrentShip', True),
|
||
]))
|
||
|
||
elif entry['event'] == 'ShipyardTransfer':
|
||
add_event('setCommanderShipTransfer', entry['timestamp'],
|
||
OrderedDict([
|
||
('shipType', entry['ShipType']),
|
||
('shipGameID', entry['ShipID']),
|
||
('starsystemName', system),
|
||
('stationName', station),
|
||
('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' and not this.newsession:
|
||
loadout = make_loadout(state)
|
||
if this.loadout != loadout:
|
||
this.loadout = loadout
|
||
this.events = [x for x in this.events if x['eventName'] != 'setCommanderShipLoadout' or x['shipGameID'] != this.loadout['shipGameID']] # Remove any unsent for this ship
|
||
add_event('setCommanderShipLoadout', entry['timestamp'], this.loadout)
|
||
|
||
# Stored modules
|
||
if entry['event'] == 'StoredModules':
|
||
items = dict([(x['StorageSlot'], x) for x in entry['Items']]) # Impose an order
|
||
modules = []
|
||
for slot in sorted(items):
|
||
item = items[slot]
|
||
module = OrderedDict([
|
||
('itemName', item['Name']),
|
||
('itemValue', item['BuyPrice']),
|
||
('isHot', item['Hot']),
|
||
])
|
||
|
||
# Location can be absent if in transit
|
||
if 'StarSystem' in item:
|
||
module['starsystemName'] = item['StarSystem']
|
||
if 'MarketID' in item:
|
||
module['marketID'] = item['MarketID']
|
||
|
||
if 'EngineerModifications' in item:
|
||
module['engineering'] = OrderedDict([('blueprintName', item['EngineerModifications'])])
|
||
if 'Level' in item:
|
||
module['engineering']['blueprintLevel'] = item['Level']
|
||
if 'Quality' in item:
|
||
module['engineering']['blueprintQuality'] = item['Quality']
|
||
|
||
modules.append(module)
|
||
|
||
if this.storedmodules != modules:
|
||
# Only send on change
|
||
this.storedmodules = modules
|
||
this.events = [x for x in this.events if x['eventName'] != 'setCommanderStorageModules'] # Remove any unsent
|
||
add_event('setCommanderStorageModules', entry['timestamp'], this.storedmodules)
|
||
|
||
# Missions
|
||
if entry['event'] == 'MissionAccepted':
|
||
data = OrderedDict([
|
||
('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]
|
||
add_event('addCommanderMission', entry['timestamp'], data)
|
||
|
||
elif entry['event'] == 'MissionAbandoned':
|
||
add_event('setCommanderMissionAbandoned', entry['timestamp'], { 'missionGameID': entry['MissionID'] })
|
||
|
||
elif entry['event'] == 'MissionCompleted':
|
||
for x in entry.get('PermitsAwarded', []):
|
||
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 = OrderedDict([ ('minorfactionName', faction['Faction']) ])
|
||
for influence in faction.get('Influence', []):
|
||
if 'Influence' in influence:
|
||
effect['influenceGain'] = len(effect.get('influenceGain', '')) > len(influence['Influence']) and effect['influenceGain'] or influence['Influence'] # pick highest
|
||
if 'Reputation' in faction:
|
||
effect['reputationGain'] = faction['Reputation']
|
||
factioneffects.append(effect)
|
||
if factioneffects:
|
||
data['minorfactionEffects'] = factioneffects
|
||
add_event('setCommanderMissionCompleted', entry['timestamp'], data)
|
||
|
||
elif entry['event'] == 'MissionFailed':
|
||
add_event('setCommanderMissionFailed', entry['timestamp'], { 'missionGameID': entry['MissionID'] })
|
||
|
||
# Combat
|
||
if entry['event'] == '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']
|
||
add_event('addCommanderCombatDeath', entry['timestamp'], data)
|
||
|
||
elif entry['event'] == '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']
|
||
add_event('addCommanderCombatInterdicted', entry['timestamp'], data)
|
||
|
||
elif entry['event'] == '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']
|
||
add_event('addCommanderCombatInterdiction', entry['timestamp'], data)
|
||
|
||
elif entry['event'] == 'EscapeInterdiction':
|
||
add_event('addCommanderCombatInterdictionEscape', entry['timestamp'],
|
||
OrderedDict([('starsystemName', system),
|
||
('opponentName', entry['Interdictor']),
|
||
('isPlayer', entry['IsPlayer']),
|
||
]))
|
||
|
||
elif entry['event'] == 'PVPKill':
|
||
add_event('addCommanderCombatKill', entry['timestamp'],
|
||
OrderedDict([('starsystemName', system),
|
||
('opponentName', entry['Victim']),
|
||
]))
|
||
|
||
# Community Goals
|
||
if entry['event'] == 'CommunityGoal':
|
||
this.events = [x for x in this.events if x['eventName'] not in ['setCommunityGoal', 'setCommanderCommunityGoalProgress']] # Remove any unsent
|
||
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']
|
||
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']
|
||
add_event('setCommanderCommunityGoalProgress', entry['timestamp'], data)
|
||
|
||
# Friends
|
||
if entry['event'] == 'Friends':
|
||
if entry['Status'] in ['Added', 'Online']:
|
||
add_event('addCommanderFriend', entry['timestamp'],
|
||
OrderedDict([('commanderName', entry['Name']),
|
||
('gamePlatform', 'pc'),
|
||
]))
|
||
elif entry['Status'] in ['Declined', 'Lost']:
|
||
add_event('delCommanderFriend', entry['timestamp'],
|
||
OrderedDict([('commanderName', entry['Name']),
|
||
('gamePlatform', 'pc'),
|
||
]))
|
||
|
||
this.newuser = False
|
||
|
||
# Only actually change URLs if we are current provider.
|
||
if config.get('system_provider') == 'Inara':
|
||
this.system_link['text'] = this.system
|
||
this.system_link['url'] = system_url(this.system)
|
||
this.system_link.update_idletasks()
|
||
|
||
if config.get('station_provider') == 'Inara':
|
||
this.station_link['text'] = this.station or (this.system_population and this.system_population > 0 and STATION_UNDOCKED or '')
|
||
this.station_link['url'] = station_url(this.system, this.station)
|
||
this.station_link.update_idletasks()
|
||
|
||
def cmdr_data(data, is_beta):
|
||
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 or data['lastSystem']['name']
|
||
this.station = this.station or data['commander']['docked'] and data['lastStarport']['name']
|
||
|
||
# Override standard URL functions
|
||
if config.get('system_provider') == 'Inara':
|
||
this.system_link['text'] = this.system
|
||
this.system_link['url'] = system_url(this.system)
|
||
this.system_link.update_idletasks()
|
||
if config.get('station_provider') == 'Inara':
|
||
if data['commander']['docked']:
|
||
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'] = ''
|
||
|
||
this.station_link['url'] = station_url(this.system, this.station)
|
||
this.station_link.update_idletasks()
|
||
|
||
if config.getint('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.events = [x for x in this.events if x['eventName'] != 'setCommanderCredits'] # Remove any unsent
|
||
add_event('setCommanderCredits', data['timestamp'],
|
||
OrderedDict([
|
||
('commanderCredits', data['commander']['credits']),
|
||
('commanderLoan', data['commander'].get('debt', 0)),
|
||
]))
|
||
this.lastcredits = float(data['commander']['credits'])
|
||
|
||
|
||
def make_loadout(state):
|
||
modules = []
|
||
for m in state['Modules'].values():
|
||
module = 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 = 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 = 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 add_event(name, timestamp, data):
|
||
this.events.append(OrderedDict([
|
||
('eventName', name),
|
||
('eventTimestamp', timestamp),
|
||
('eventData', data),
|
||
]))
|
||
|
||
|
||
# Queue a call to Inara, handled in Worker thread
|
||
def call(callback=None):
|
||
if not this.events:
|
||
return
|
||
|
||
this.last_update_time = time.time()
|
||
|
||
data = OrderedDict([
|
||
('header', OrderedDict([
|
||
('appName', applongname),
|
||
('appVersion', appversion),
|
||
('APIkey', credentials(this.cmdr)),
|
||
('commanderName', this.cmdr),
|
||
('commanderFrontierID', this.FID),
|
||
])),
|
||
('events', list(this.events)), # shallow copy
|
||
])
|
||
this.events = []
|
||
this.queue.put(('https://inara.cz/inapi/v1/', data, None))
|
||
|
||
# Worker thread
|
||
def worker():
|
||
while True:
|
||
item = this.queue.get()
|
||
if not item:
|
||
return # Closing
|
||
else:
|
||
(url, data, callback) = item
|
||
|
||
retrying = 0
|
||
while retrying < 3:
|
||
try:
|
||
r = this.session.post(url, data=json.dumps(data, separators = (',', ':')), timeout=_TIMEOUT)
|
||
r.raise_for_status()
|
||
reply = r.json()
|
||
status = reply['header']['eventStatus']
|
||
if callback:
|
||
callback(reply)
|
||
elif status // 100 != 2: # 2xx == OK (maybe with warnings)
|
||
# Log fatal errors
|
||
print('Inara\t%s %s' % (reply['header']['eventStatus'], reply['header'].get('eventStatusText', '')))
|
||
print(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:
|
||
print('Inara\t%s %s\t%s' % (reply_event['eventStatus'], reply_event.get('eventStatusText', ''), json.dumps(data_event)))
|
||
if reply_event['eventStatus'] // 100 != 2:
|
||
plug.show_error(_('Error: Inara {MSG}').format(MSG = '%s, %s' % (data_event['eventName'], reply_event.get('eventStatusText', reply_event['eventStatus']))))
|
||
if data_event['eventName'] in ['addCommanderTravelCarrierJump', 'addCommanderTravelDock', 'addCommanderTravelFSDJump', 'setCommanderTravelLocation']:
|
||
this.lastlocation = reply_event.get('eventData', {})
|
||
this.system_link.event_generate('<<InaraLocation>>', when="tail") # calls update_location in main thread
|
||
elif data_event['eventName'] in ['addCommanderShip', 'setCommanderShip']:
|
||
this.lastship = reply_event.get('eventData', {})
|
||
this.system_link.event_generate('<<InaraShip>>', when="tail") # calls update_ship in main thread
|
||
|
||
break
|
||
except:
|
||
if __debug__: print_exc()
|
||
retrying += 1
|
||
else:
|
||
if callback:
|
||
callback(None)
|
||
else:
|
||
plug.show_error(_("Error: Can't connect to Inara"))
|
||
|
||
|
||
# Call inara_notify_location() in this and other interested plugins with Inara's response when changing system or station
|
||
def update_location(event=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(eventData):
|
||
pass
|
||
|
||
# Call inara_notify_ship() in interested plugins with Inara's response when changing ship
|
||
def update_ship(event=None):
|
||
if this.lastship:
|
||
for plugin in plug.provides('inara_notify_ship'):
|
||
plug.invoke(plugin, None, 'inara_notify_ship', this.lastship)
|