controller: Initial polling and parsing of Valorant presence

This commit is contained in:
Jack O'Sullivan 2024-12-12 17:12:13 +00:00
parent 46cdc9cd43
commit 78aa8005bd
2 changed files with 149 additions and 16 deletions

View File

@ -1,7 +1,6 @@
import logging
import os
import sys
import time
import infi.systray
@ -11,33 +10,40 @@ import valconomy
DATA_DIR = os.path.join(os.environ['LOCALAPPDATA'], 'Valconomy')
LOG_FILE = os.path.join(DATA_DIR, 'app.log')
log = logging.getLogger('valconomy')
log = logging.getLogger('valconomy-app')
def data_file(fname: str) -> str:
return os.path.abspath(os.path.join(os.path.dirname(__file__), fname))
running = True
def do_quit(tray: infi.systray.SysTrayIcon):
global running
log.info('Shutting down')
running = False
def do_view_logs(tray: infi.systray.SysTrayIcon):
def do_open_datadir(tray: infi.systray.SysTrayIcon):
os.startfile(DATA_DIR)
def main():
os.makedirs(DATA_DIR, exist_ok=True)
if sys.stderr is None:
# running in console-less mode, redirect to log file
sys.stderr = open(LOG_FILE, 'a')
logging.basicConfig(
format='%(asctime)s %(levelname)s %(message)s', level=logging.INFO,
handlers=(
logging.StreamHandler(sys.stderr), # log to stderr as well as file
logging.FileHandler(LOG_FILE),
))
stream=sys.stderr)
log.info('Starting up')
with infi.systray.SysTrayIcon(data_file('icon.ico'), 'Valconomy', menu_options=(('View logs', None, do_view_logs),), on_quit=do_quit) as tray:
while running:
time.sleep(0.5)
def cb(i: valconomy.RiotPlayerInfo):
print(repr(i))
val_client = valconomy.ValorantLocalClient(cb)
def do_quit(tray: infi.systray.SysTrayIcon):
log.info('Shutting down')
val_client.stop()
with infi.systray.SysTrayIcon(
data_file('icon.ico'), 'Valconomy', on_quit=do_quit,
menu_options=(
('Open data directory', None, do_open_datadir),
)) as tray:
val_client.run()
if __name__ == '__main__':
main()

View File

@ -1 +1,128 @@
import base64
from dataclasses import dataclass
import json
import logging
from pprint import pprint
import os
import time
from typing import Callable
import hid
import requests
import urllib3
# Local HTTPS connection, we need to not verify the cert
urllib3.disable_warnings()
log = logging.getLogger('valconomy')
@dataclass
class ValorantPlayerInfo:
is_idle: bool = False
is_party_owner: bool = False
max_party_size: int = 0
party_size: int = 0
party_state: str = None
queue_type: str = None
game_state: str = None
map: str = None
team: str = None
score: int = 0
enemy_score: int = 0
@dataclass
class RiotPlayerInfo:
uuid: str
name: str
tag: str
state: str
valorant: ValorantPlayerInfo = None
class ValorantLocalClient:
lockfile_path = os.path.join(os.environ['LOCALAPPDATA'], r'Riot Games\Riot Client\Config\lockfile')
poll_interval = 0.5
def __init__(self, callback: Callable[[RiotPlayerInfo], None]):
self.callback = callback
self.port, self.password = None, None
self.running = True
self.players = {}
def _load_credentials(self):
if not os.path.exists(self.lockfile_path):
return False
try:
with open(self.lockfile_path) as f:
for line in f:
if line.startswith('Riot Client'):
client, pid, self.port, self.password, proto = line.rstrip().split(':')
assert proto == 'https'
return True
except FileNotFoundError:
return False
return False
def _do_poll(self):
try:
resp = requests.get(
f'https://127.0.0.1:{self.port}/chat/v4/presences',
auth=('riot', self.password), verify=False)
resp.raise_for_status()
data = resp.json()
except (requests.HTTPError, requests.ConnectionError) as ex:
# Most likely one of these means the Riot Client shut down
log.error(f'Failed to make request to Riot Client: {ex}')
return False
for p in data['presences']:
if p['product'] != 'valorant':
continue
v = json.loads(base64.b64decode(p.pop('private'))) # Valorant-specfic
# pprint(p)
# pprint(v)
val_info = ValorantPlayerInfo(
is_idle=v['isIdle'], is_party_owner=v['isPartyOwner'],
max_party_size=v['maxPartySize'], party_size=v['partySize'],
party_state=v['partyState'],
queue_type=v['queueId'] or None, game_state=v['sessionLoopState'],
map=v['matchMap'] or None,
score=v['partyOwnerMatchScoreAllyTeam'], enemy_score=v['partyOwnerMatchScoreEnemyTeam'])
match v['partyOwnerMatchCurrentTeam']:
case 'Red':
val_info.team = 'attackers'
case 'Blue':
val_info.team = 'defenders'
info = RiotPlayerInfo(p['puuid'], p['game_name'], p['game_tag'], p['state'], valorant=val_info)
last = self.players.get(info.uuid)
if info != last:
self.players[info.uuid] = info
self.callback(info)
return True
def run(self):
while self.running:
if self.password is None:
if not self._load_credentials():
time.sleep(self.poll_interval)
continue
log.info('Detected Riot Client credentials, starting polling')
if not self._do_poll():
self.port, self.password = None, None
time.sleep(self.poll_interval)
def stop(self):
self.running = False