valconomy/controller/valconomy.py

328 lines
8.9 KiB
Python
Raw Normal View History

import base64
from dataclasses import dataclass
2024-12-12 19:36:22 +00:00
from enum import Enum
import json
import logging
from pprint import pprint
import os
import time
from typing import Callable
2024-12-12 14:15:12 +00:00
import hid
import requests
import urllib3
# Local HTTPS connection, we need to not verify the cert
urllib3.disable_warnings()
2024-12-12 19:36:22 +00:00
# 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
2024-12-12 19:36:22 +00:00
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
2024-12-12 19:36:22 +00:00
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
2024-12-12 19:36:22 +00:00
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:
2024-12-12 19:36:22 +00:00
self.callback(info, False)
next_players[info.uuid] = info
for uuid in self.players.keys() - next_players.keys():
self.callback(self.players[uuid], True)
2024-12-12 19:36:22 +00:00
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():
2024-12-12 19:36:22 +00:00
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
2024-12-12 19:36:22 +00:00
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)