Files
j-lin-lmg a9da3093f7 user failsafe false (#168)
user failsafe false 

for all games that use pydirectinput
2025-10-03 10:24:59 -07:00

263 lines
8.6 KiB
Python

"""Misc utility functions"""
from argparse import ArgumentParser
import logging
import os
from pathlib import Path
from zipfile import ZipFile
import time
import pydirectinput as user
import pyautogui as gui
import requests
import vgamepad as vg
import json
import re
import sys
user.FAILSAFE = False
class LTTGamePad360(vg.VX360Gamepad):
"""
Class extension for the virtual game pad library
Many of the in built functions for this library are super useful but a bit unwieldy to use.
This class extension provides some useful functions to make your code look a little cleaner when
implemented in our harnesses.
"""
def single_press(self, button=vg.XUSB_BUTTON.XUSB_GAMEPAD_DPAD_DOWN, pause=0.1):
"""
Custom function to perform a single press of a specified gamepad button
button --> must be a XUSB_BUTTON class, defaults to dpad down
Button Options:
XUSB_GAMEPAD_DPAD_UP
XUSB_GAMEPAD_DPAD_DOWN
XUSB_GAMEPAD_DPAD_LEFT
XUSB_GAMEPAD_DPAD_RIGHT
XUSB_GAMEPAD_START
XUSB_GAMEPAD_BACK
XUSB_GAMEPAD_LEFT_THUMB
XUSB_GAMEPAD_RIGHT_THUMB
XUSB_GAMEPAD_LEFT_SHOULDER
XUSB_GAMEPAD_RIGHT_SHOULDER
XUSB_GAMEPAD_GUIDE
XUSB_GAMEPAD_A
XUSB_GAMEPAD_B
XUSB_GAMEPAD_X
XUSB_GAMEPAD_Y
pause --> the delay between pressing and releasing the button, defaults to 0.1 if not specified
"""
self.press_button(button=button)
self.update()
time.sleep(pause)
self.release_button(button=button)
self.update()
def press_n_times(self, button: vg.XUSB_BUTTON, n: int, pause: float):
"""
Sometimes we need to press a certain gamepad button multiple times in a row, this loop does that for you
"""
for _ in range(n):
self.single_press(button)
time.sleep(pause)
class LTTGamePadDS4(vg.VDS4Gamepad):
"""
Class extension for the virtual game pad library
Many of the in built functions for this library are super useful but a bit unwieldy to use.
This class extension provides some useful functions to make your code look a little cleaner when
implemented in our harnesses.
"""
def single_button_press(self, button=vg.DS4_BUTTONS.DS4_BUTTON_CROSS, fastpause=0.05):
"""
Custom function to perform a single press of a specified gamepad digital button
button --> must be a DS4_BUTTONS class, defaults to cross
Button Options:
DS4_BUTTON_THUMB_RIGHT
DS4_BUTTON_THUMB_LEFT
DS4_BUTTON_OPTIONS
DS4_BUTTON_SHARE
DS4_BUTTON_TRIGGER_RIGHT
DS4_BUTTON_TRIGGER_LEFT
DS4_BUTTON_SHOULDER_RIGHT
DS4_BUTTON_SHOULDER_LEFT
DS4_BUTTON_TRIANGLE
DS4_BUTTON_CIRCLE
DS4_BUTTON_CROSS
DS4_BUTTON_SQUARE
pause --> the delay between pressing and releasing the button, defaults to 0.05 if not specified
"""
self.press_button(button=button)
self.update()
time.sleep(fastpause)
self.release_button(button=button)
self.update()
def button_press_n_times(self, button: vg.DS4_BUTTONS, n: int, pause: float):
"""
Sometimes we need to press a certain gamepad button multiple times in a row, this loop does that for you
"""
for _ in range(n):
self.single_button_press(button)
time.sleep(pause)
def single_dpad_press(self, direction=vg.DS4_DPAD_DIRECTIONS.DS4_BUTTON_DPAD_SOUTH, pause=0.1):
"""
Custom function to perform a single press of a specified gamepad button
button --> must be a DS4_DPAD_DIRECTIONS class, defaults to dpad south
DPAD Options:
DS4_BUTTON_DPAD_NONE
DS4_BUTTON_DPAD_NORTHWEST
DS4_BUTTON_DPAD_WEST
DS4_BUTTON_DPAD_SOUTHWEST
DS4_BUTTON_DPAD_SOUTH
DS4_BUTTON_DPAD_SOUTHEAST
DS4_BUTTON_DPAD_EAST
DS4_BUTTON_DPAD_NORTHEAST
DS4_BUTTON_DPAD_NORTH
pause --> the delay between pressing and releasing the button, defaults to 0.1 if not specified
"""
self.directional_pad(direction=direction)
self.update()
time.sleep(pause)
self.reset()
self.update()
def dpad_press_n_times(self, direction: vg.DS4_DPAD_DIRECTIONS, n: int, pause: float):
"""
Sometimes we need to press a certain dpad direction multiple times in a row, this loop does that for you
"""
for _ in range(n):
self.single_dpad_press(direction)
time.sleep(pause)
def clickme(x: int, y: int):
"""Pyautogui's click function sucks, this should do the trick"""
gui.moveTo(x, y)
time.sleep(0.2)
gui.mouseDown()
time.sleep(0.2)
gui.mouseUp()
def mouse_scroll_n_times(n: int, scroll_amount: int, pause: float):
"""
Pyautogui's mouse scroll function often fails to actually scroll in game menus, this functions solves that problem
n --> the number of times you want to scroll, should be a positive integer
scroll_amount --> positive is scroll up, negative is scroll down
pause --> the amount of time to pause betwee subsequent scrolls
"""
for _ in range(n):
gui.vscroll(scroll_amount)
time.sleep(pause)
def int_time() -> int:
"""Returns the current time in seconds since epoch as an integer"""
return int(time.time())
def press_n_times(key: str, n: int, pause: float = 0.5):
"""A helper function press the same button multiple times"""
for _ in range(n):
user.press(key)
time.sleep(pause)
def remove_files(paths: list[str]) -> None:
"""Removes files specified by provided list of file paths.
Does nothing for a path that does not exist.
"""
for path in paths:
try:
os.remove(path)
logging.info("Removed file: %s", path)
except FileNotFoundError:
logging.info("File already removed: %s", path)
def download_file(download_url: str, destination: Path) -> None:
"""Downloads file from given url to destination"""
response = requests.get(download_url, allow_redirects=True, timeout=120)
with open(destination, 'wb') as f:
f.write(response.content)
def extract_archive(zip_file: Path, destination_dir: Path) -> None:
"""Extract all contents of an archive"""
with ZipFile(zip_file, 'r') as zip_object:
zip_object.extractall(path=destination_dir)
def extract_file_from_archive(zip_file: Path, member_path: str, destination_dir: Path) -> None:
"""Extract a single file memeber from an archive"""
with ZipFile(zip_file, 'r') as zip_object:
zip_object.extract(member_path, path=destination_dir)
def find_eg_game_version(gamefoldername: str) -> str:
"""Find the version of the specific game (e.g., AlanWake2) from the launcher installed data."""
installerdat = r"C:\ProgramData\Epic\UnrealEngineLauncher\LauncherInstalled.dat"
try:
# Open the file and read its entire content
with open(installerdat, encoding="utf-8") as file:
file_content = file.read()
# Check if the "InstallationList" section is in the content
installation_list_match = re.search(r'"InstallationList":\s*(\[[^\]]*\])', file_content)
if not installation_list_match:
print("No InstallationList found.")
return None
# Extract the InstallationList part from the file
installation_list_json = installation_list_match.group(1)
# Load the installation list as JSON
installation_list = json.loads(installation_list_json)
# Loop through each item in the installation list
for game in installation_list:
# Check if the game's InstallLocation contains the target string (AlanWake2)
if gamefoldername in game.get("InstallLocation", ""):
# Return the AppVersion for this game
return game.get("AppVersion", None)
except Exception as e:
print(f"Error: {e}")
return None
def find_word(keras_service, word, msg, timeout=30, interval=1):
"""Function to call Keras service to find a word in the screen"""
if keras_service.wait_for_word(word=word, timeout=timeout, interval=interval) is None:
logging.error(msg)
sys.exit(1)
def keras_args():
"""helper function to get args for keras"""
parser = ArgumentParser()
parser.add_argument("--kerasHost", dest="keras_host",
help="Host for Keras OCR service", required=True)
parser.add_argument("--kerasPort", dest="keras_port",
help="Port for Keras OCR service", required=True)
return parser.parse_args()