valconomy/controller/valconomy.py

328 lines
8.9 KiB
Python

import base64
from dataclasses import dataclass
from enum import Enum
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()
# Disable so we can debug without a log for every request
logging.getLogger('urllib3.connectionpool').disabled = True
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
def full_name(self) -> str:
return f'{self.name}#{self.tag}'
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, bool], 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
next_players = {}
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.callback(info, False)
next_players[info.uuid] = info
for uuid in self.players.keys() - next_players.keys():
self.callback(self.players[uuid], True)
self.players = next_players
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():
for p in self.players.values():
self.callback(p, True)
self.players = {}
self.port, self.password = None, None
time.sleep(self.poll_interval)
def stop(self):
self.running = False
class EconomyDecision(Enum):
BUY = 0
SAVE = 1
MATCH_TEAM = 2
class ValconomyHandler:
# Valorant isn't open
def none(self):
log.info('Val Time soon?')
# Welcome the user (back) to the game!
def menu(self, info: RiotPlayerInfo, was_idle: bool=False):
if was_idle:
log.info(f"Welcome back, {info.name}!")
else:
log.info(f"It's Val Time, {info.name}!")
# The user is idle in the menu...
def idle(self, info: RiotPlayerInfo):
log.info(f'Come back soon, {info.name}...')
# Just entered queue
def queue_start(self, info: RiotPlayerInfo):
if info.valorant.is_party_owner and info.valorant.queue_type == 'unrated':
log.info('Uhhhh should that be comp, MoonStar?')
else:
log.info(f'Hope you find a game quickly, {info.name}!')
def match_found(self, info: RiotPlayerInfo):
if info.valorant.queue_type == 'premier-seasonmatch':
log.info('Do the Cosmonauts proud!')
else:
log.info("I hope it's not Split...")
# Loaded into the agent select
def pregame(self, info: RiotPlayerInfo):
if info.valorant.map == '/Game/Maps/Bonsai/Bonsai':
log.info('Ewwwww, Split....')
else:
log.info('Pick a good agent!')
# Game where we're not providing economy help
def game_generic(self, info: RiotPlayerInfo):
match info.valorant.queue_type:
case 'hurm':
log.info('Have a good TDM!')
case 'deathmatch':
log.info('Have a good deathmatch!')
case 'ggteam':
log.info('Have a good Escalation!')
case 'spikerush':
log.info('Have a good Spike Rush!')
case _:
if info.valorant.max_party_size == 12:
log.info('Have a good custom!')
else:
log.info('Have a good game!')
# Loaded into the game
def game_start(self, info: RiotPlayerInfo):
log.info('OK best of luck!')
# Round started
def round_start(self, info: RiotPlayerInfo, economy: EconomyDecision):
match economy:
case EconomyDecision.BUY:
log.info('You should buy!')
case EconomyDecision.SAVE:
log.info('Time to save...')
case EconomyDecision.MATCH_TEAM:
log.info('Follow the team economy!')
def game_over(self, info: RiotPlayerInfo, won: bool):
if won:
log.info('Well played!')
else:
log.info('Hard luck...')
class HIDValconomyHandler(ValconomyHandler):
pass
class GameState(Enum):
NONE = 0
MENU = 1
IDLE = 2
IN_QUEUE = 3
GAME_FOUND = 4
PRE_GAME = 5
IN_GAME = 6
GAME_OVER = 7
IN_GAME_GENERIC = 8
class ValconomyStateMachine:
def __init__(self, player_uuid: str, handler: ValconomyHandler):
self.uuid = player_uuid
self.handler = handler
self.last = None
self.score = -1
self.score_enemy = -1
def handle_state(self, s: GameState, p: RiotPlayerInfo):
log.info(f'{self.last} -> {s}')
if s == GameState.IN_GAME:
if p.valorant.score == self.score and p.valorant.enemy_score == self.score_enemy:
return
eco = EconomyDecision.MATCH_TEAM
if p.valorant.score == 0 and self.score == -1:
# First round
eco = EconomyDecision.BUY
# TODO: ......
self.handler.round_start(p, eco)
self.score = p.valorant.score
self.score_enemy = p.valorant.enemy_score
return
if s == self.last:
return
if s != GameState.GAME_OVER:
self.score = self.score_enemy = -1
match s:
case GameState.IN_GAME_GENERIC:
self.handler.game_generic(p)
case GameState.PRE_GAME:
self.handler.pregame(p)
case GameState.GAME_FOUND:
self.handler.match_found(p)
case GameState.IN_QUEUE:
self.handler.match_found(p)
case GameState.IDLE:
self.handler.idle(p)
case GameState.MENU:
self.handler.menu(p, self.last == GameState.IDLE)
case GameState.NONE:
self.handler.none()
case GameState.GAME_OVER:
self.handler.game_over(p, self.score > self.score_enemy)
self.last = s
def handle_presence(self, p: RiotPlayerInfo, gone: bool):
log.debug(f'{repr(p)}, gone: {gone}')
if p.uuid != self.uuid:
return
if gone:
self.handle_state(GameState.NONE, p)
return
if p.valorant.party_state == 'MATCHMADE_GAME_STARTING':
self.handle_state(GameState.GAME_FOUND, p)
return
if p.valorant.party_state == 'MATCHMAKING':
self.handle_state(GameState.IN_QUEUE, p)
return
if p.valorant.queue_type in ('unrated', 'competitive', 'premier-seasonmatch', 'swiftplay'):
if p.valorant.game_state == 'PREGAME':
self.handle_state(GameState.PRE_GAME, p)
return
if p.valorant.game_state == 'INGAME':
if self.score > 0 or self.score_enemy > 0 and p.valorant.score == p.valorant.enemy_score == 0:
self.handle_state(GameState.GAME_OVER, p)
else:
self.handle_state(GameState.IN_GAME, p)
return
if p.valorant.game_state in ('PREGAME', 'INGAME'):
self.handle_state(GameState.IN_GAME_GENERIC, p)
return
if p.valorant.game_state == 'MENUS':
if p.valorant.is_idle:
self.handle_state(GameState.IDLE, p)
return
self.handle_state(GameState.MENU, p)